8 Commits

Author SHA1 Message Date
Ticho Hidding
5501c3893f bitboard merge 2025-12-15 10:28:33 +01:00
Ticho Hidding
ffdec38e5d Merge branch 'Development' into ReversiML
# Conflicts:
#	game/src/main/java/org/toop/game/games/reversi/ReversiR.java
2025-12-13 15:04:40 +01:00
Stef
1ae79daef0 Added documentation to player classes and improved method names (#295) 2025-12-10 13:17:01 +01:00
Stef
cd8eb99559 Merge 292 into development (#293)
Applied template method pattern to abstract player
2025-12-10 12:39:40 +01:00
Ticho Hidding
03dc6130e2 merge commit 2025-12-08 14:55:32 +01:00
Ticho Hidding
ca7f9e8ecf Merge branch 'Development' into ReversiML
# Conflicts:
#	app/src/main/java/org/toop/Main.java
#	app/src/main/java/org/toop/app/game/ReversiGame.java
#	game/pom.xml
#	game/src/main/java/org/toop/game/reversi/Reversi.java
#	game/src/main/java/org/toop/game/reversi/ReversiAI.java
#	game/src/test/java/org/toop/game/tictactoe/ReversiTest.java
2025-12-08 11:58:32 +01:00
Ticho Hidding
f6d90ed439 added some useful testing methods.
made training slightly better.
2025-12-08 11:36:31 +01:00
Ticho Hidding
7e913ff50f Machine learning for reversi.
performance improvements for reversi.getlegalmoves
2025-12-02 10:59:33 +01:00
79 changed files with 1136 additions and 1272 deletions

View File

@@ -1,14 +1,10 @@
package org.toop.app; package org.toop.app;
import javafx.application.Application;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyEvent; import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.toop.app.widget.Primitive; import org.toop.app.widget.Primitive;
import org.toop.app.widget.WidgetContainer; import org.toop.app.widget.WidgetContainer;
import org.toop.app.widget.complex.LoadingWidget; import org.toop.app.widget.complex.LoadingWidget;
@@ -20,15 +16,9 @@ import org.toop.framework.audio.*;
import org.toop.framework.audio.events.AudioEvents; import org.toop.framework.audio.events.AudioEvents;
import org.toop.framework.eventbus.EventFlow; import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.eventbus.GlobalEventBus; import org.toop.framework.eventbus.GlobalEventBus;
import org.toop.framework.game.BitboardGame; import org.toop.game.machinelearning.NeuralNetwork;
import org.toop.framework.game.games.reversi.BitboardReversi; import org.toop.framework.networking.NetworkingClientEventListener;
import org.toop.framework.game.games.tictactoe.BitboardTicTacToe; import org.toop.framework.networking.NetworkingClientManager;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.networking.connection.NetworkingClientEventListener;
import org.toop.framework.networking.connection.NetworkingClientManager;
import org.toop.framework.networking.server.GameDefinition;
import org.toop.framework.networking.server.MasterServer;
import org.toop.framework.networking.server.Server;
import org.toop.framework.resource.ResourceLoader; import org.toop.framework.resource.ResourceLoader;
import org.toop.framework.resource.ResourceManager; import org.toop.framework.resource.ResourceManager;
import org.toop.framework.resource.events.AssetLoaderEvents; import org.toop.framework.resource.events.AssetLoaderEvents;
@@ -38,9 +28,13 @@ import org.toop.framework.resource.resources.SoundEffectAsset;
import org.toop.local.AppContext; import org.toop.local.AppContext;
import org.toop.local.AppSettings; import org.toop.local.AppSettings;
import java.time.Duration; import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@@ -101,17 +95,6 @@ public final class App extends Application {
WidgetContainer.setCurrentView(loading); WidgetContainer.setCurrentView(loading);
var games = new ConcurrentHashMap<String, Class<? extends TurnBasedGame>>();
games.put("tictactoe", BitboardTicTacToe.class);
games.put("reversi", BitboardReversi.class);
var a = new MasterServer(6666, games, Duration.ofSeconds(5));
try {
a.start();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
setOnLoadingSuccess(loading); setOnLoadingSuccess(loading);
EventFlow loadingFlow = new EventFlow(); EventFlow loadingFlow = new EventFlow();
@@ -155,6 +138,13 @@ public final class App extends Application {
} }
stage.show(); stage.show();
//startML();
}
private void startML() {
NeuralNetwork nn = new NeuralNetwork();
nn.init();
} }
private void setKeybinds(StackPane root) { private void setKeybinds(StackPane root) {

View File

@@ -14,14 +14,14 @@ import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.gameFramework.controller.GameController; import org.toop.framework.gameFramework.controller.GameController;
import org.toop.framework.eventbus.GlobalEventBus; import org.toop.framework.eventbus.GlobalEventBus;
import org.toop.framework.gameFramework.model.player.Player; import org.toop.framework.gameFramework.model.player.Player;
import org.toop.framework.networking.connection.clients.TournamentNetworkingClient; import org.toop.framework.networking.clients.TournamentNetworkingClient;
import org.toop.framework.networking.connection.events.NetworkEvents; import org.toop.framework.networking.events.NetworkEvents;
import org.toop.framework.networking.connection.types.NetworkingConnector; import org.toop.framework.networking.types.NetworkingConnector;
import org.toop.framework.game.games.reversi.BitboardReversi; import org.toop.game.games.reversi.BitboardReversi;
import org.toop.framework.game.games.tictactoe.BitboardTicTacToe; import org.toop.game.games.tictactoe.BitboardTicTacToe;
import org.toop.framework.game.players.ArtificialPlayer; import org.toop.game.players.ArtificialPlayer;
import org.toop.framework.game.players.OnlinePlayer; import org.toop.game.players.OnlinePlayer;
import org.toop.framework.game.players.RandomAI; import org.toop.game.players.ai.RandomAI;
import org.toop.local.AppContext; import org.toop.local.AppContext;
import java.util.List; import java.util.List;
@@ -214,15 +214,15 @@ public final class Server {
switch (type) { switch (type) {
case TICTACTOE ->{ case TICTACTOE ->{
Player[] players = new Player[2]; Player<BitboardTicTacToe>[] players = new Player[2];
players[(myTurn + 1) % 2] = new OnlinePlayer(response.opponent()); players[(myTurn + 1) % 2] = new OnlinePlayer<>(response.opponent());
players[myTurn] = new ArtificialPlayer(new RandomAI(), user); players[myTurn] = new ArtificialPlayer<>(new RandomAI<BitboardTicTacToe>(), user);
gameController = new TicTacToeBitController(players); gameController = new TicTacToeBitController(players);
} }
case REVERSI -> { case REVERSI -> {
Player[] players = new Player[2]; Player<BitboardReversi>[] players = new Player[2];
players[(myTurn + 1) % 2] = new OnlinePlayer(response.opponent()); players[(myTurn + 1) % 2] = new OnlinePlayer<>(response.opponent());
players[myTurn] = new ArtificialPlayer(new RandomAI(), user); players[myTurn] = new ArtificialPlayer<>(new RandomAI<BitboardReversi>(), user);
gameController = new ReversiBitController(players);} gameController = new ReversiBitController(players);}
default -> new ErrorPopup("Unsupported game type."); default -> new ErrorPopup("Unsupported game type.");

View File

@@ -14,7 +14,7 @@ import org.toop.framework.gameFramework.view.GUIEvents;
import java.util.function.Consumer; import java.util.function.Consumer;
public abstract class BitGameCanvas implements GameCanvas { public abstract class BitGameCanvas<T extends TurnBasedGame<T>> implements GameCanvas<T> {
protected record Cell(float x, float y, float width, float height) { protected record Cell(float x, float y, float width, float height) {
public boolean isInside(double x, double y) { public boolean isInside(double x, double y) {
return x >= this.x && x <= this.x + width && return x >= this.x && x <= this.x + width &&

View File

@@ -1,7 +1,8 @@
package org.toop.app.canvas; package org.toop.app.canvas;
import javafx.scene.canvas.Canvas; import javafx.scene.canvas.Canvas;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
public interface GameCanvas extends GameDrawer{ public interface GameCanvas<T extends TurnBasedGame<T>> extends GameDrawer<T>{
Canvas getCanvas(); Canvas getCanvas();
} }

View File

@@ -2,6 +2,6 @@ package org.toop.app.canvas;
import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.framework.gameFramework.model.game.TurnBasedGame;
public interface GameDrawer { public interface GameDrawer<T extends TurnBasedGame<T>> {
void redraw(TurnBasedGame gameCopy); void redraw(T gameCopy);
} }

View File

@@ -2,9 +2,12 @@ package org.toop.app.canvas;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import org.toop.app.App; import org.toop.app.App;
import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.game.games.reversi.BitboardReversi;
public class ReversiBitCanvas extends BitGameCanvas { import java.util.Arrays;
import java.util.function.Consumer;
public class ReversiBitCanvas extends BitGameCanvas<BitboardReversi> {
public ReversiBitCanvas() { public ReversiBitCanvas() {
super(Color.GRAY, new Color(0f, 0.4f, 0.2f, 1f), (App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3, 8, 8, 5, true); super(Color.GRAY, new Color(0f, 0.4f, 0.2f, 1f), (App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3, 8, 8, 5, true);
canvas.setOnMouseMoved(event -> { canvas.setOnMouseMoved(event -> {
@@ -30,7 +33,7 @@ public class ReversiBitCanvas extends BitGameCanvas {
} }
@Override @Override
public void redraw(TurnBasedGame gameCopy) { public void redraw(BitboardReversi gameCopy) {
clearAll(); clearAll();
long[] board = gameCopy.getBoard(); long[] board = gameCopy.getBoard();
loopOverBoard(board[0], (i) -> drawDot(Color.WHITE, i)); loopOverBoard(board[0], (i) -> drawDot(Color.WHITE, i));

View File

@@ -2,9 +2,12 @@ package org.toop.app.canvas;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import org.toop.app.App; import org.toop.app.App;
import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.game.games.tictactoe.BitboardTicTacToe;
public class TicTacToeBitCanvas extends BitGameCanvas{ import java.util.Arrays;
import java.util.function.Consumer;
public class TicTacToeBitCanvas extends BitGameCanvas<BitboardTicTacToe>{
public TicTacToeBitCanvas() { public TicTacToeBitCanvas() {
super( super(
Color.GRAY, Color.GRAY,
@@ -19,7 +22,7 @@ public class TicTacToeBitCanvas extends BitGameCanvas{
} }
@Override @Override
public void redraw(TurnBasedGame gameCopy) { public void redraw(BitboardTicTacToe gameCopy) {
clearAll(); clearAll();
drawMoves(gameCopy.getBoard()); drawMoves(gameCopy.getBoard());
} }

View File

@@ -15,10 +15,10 @@ import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.game.threadBehaviour.ThreadBehaviour; import org.toop.framework.gameFramework.model.game.threadBehaviour.ThreadBehaviour;
import org.toop.framework.gameFramework.model.player.Player; import org.toop.framework.gameFramework.model.player.Player;
import org.toop.framework.gameFramework.view.GUIEvents; import org.toop.framework.gameFramework.view.GUIEvents;
import org.toop.framework.networking.connection.events.NetworkEvents; import org.toop.framework.networking.events.NetworkEvents;
import org.toop.framework.game.players.LocalPlayer; import org.toop.game.players.LocalPlayer;
public class GenericGameController implements GameController { public class GenericGameController<T extends TurnBasedGame<T>> implements GameController {
protected final EventFlow eventFlow = new EventFlow(); protected final EventFlow eventFlow = new EventFlow();
// Logger for logging // Logger for logging
@@ -28,13 +28,13 @@ public class GenericGameController implements GameController {
protected final GameView gameView; protected final GameView gameView;
// Reference to game canvas // Reference to game canvas
protected final GameCanvas canvas; protected final GameCanvas<T> canvas;
protected final TurnBasedGame game; // Reference to game instance protected final TurnBasedGame<T> game; // Reference to game instance
private final ThreadBehaviour gameThreadBehaviour; private final ThreadBehaviour gameThreadBehaviour;
// TODO: Change gameType to automatically happen with either dependency injection or something else. // TODO: Change gameType to automatically happen with either dependency injection or something else.
public GenericGameController(GameCanvas canvas, TurnBasedGame game, ThreadBehaviour gameThreadBehaviour, String gameType) { public GenericGameController(GameCanvas<T> canvas, T game, ThreadBehaviour gameThreadBehaviour, String gameType) {
logger.info("Creating: " + this.getClass()); logger.info("Creating: " + this.getClass());
this.canvas = canvas; this.canvas = canvas;
@@ -55,9 +55,7 @@ public class GenericGameController implements GameController {
// Listen to updates // Listen to updates
eventFlow eventFlow
.listen(GUIEvents.GameEnded.class, this::onGameFinish, false) .listen(GUIEvents.GameEnded.class, this::onGameFinish, false)
.listen(GUIEvents.PlayerAttemptedMove.class, event -> { .listen(GUIEvents.PlayerAttemptedMove.class, event -> {if (getCurrentPlayer() instanceof LocalPlayer<T> lp){lp.setLastMove(event.move());}}, false);
if (getCurrentPlayer() instanceof LocalPlayer lp){lp.setMove(event.move());}
}, false);
} }
public void start(){ public void start(){
@@ -72,7 +70,7 @@ public class GenericGameController implements GameController {
gameThreadBehaviour.stop(); gameThreadBehaviour.stop();
} }
public Player getCurrentPlayer(){ public Player<T> getCurrentPlayer(){
return game.getPlayer(getCurrentPlayerIndex()); return game.getPlayer(getCurrentPlayerIndex());
} }
@@ -99,7 +97,7 @@ public class GenericGameController implements GameController {
stop(); stop();
} }
public Player getPlayer(int player){ public Player<T> getPlayer(int player){
if (player < 0 || player >= 2){ // TODO: Make game turn player count if (player < 0 || player >= 2){ // TODO: Make game turn player count
logger.error("Invalid player index"); logger.error("Invalid player index");
throw new IllegalArgumentException("player out of range"); throw new IllegalArgumentException("player out of range");

View File

@@ -3,19 +3,18 @@ package org.toop.app.gameControllers;
import org.toop.app.canvas.ReversiBitCanvas; import org.toop.app.canvas.ReversiBitCanvas;
import org.toop.framework.gameFramework.model.game.threadBehaviour.ThreadBehaviour; import org.toop.framework.gameFramework.model.game.threadBehaviour.ThreadBehaviour;
import org.toop.framework.gameFramework.model.player.Player; import org.toop.framework.gameFramework.model.player.Player;
import org.toop.framework.game.gameThreads.LocalThreadBehaviour; import org.toop.game.gameThreads.LocalThreadBehaviour;
import org.toop.framework.game.gameThreads.OnlineThreadBehaviour; import org.toop.game.gameThreads.OnlineThreadBehaviour;
import org.toop.framework.game.games.reversi.BitboardReversi; import org.toop.game.games.reversi.BitboardReversi;
import org.toop.framework.game.players.OnlinePlayer; import org.toop.game.players.OnlinePlayer;
public class ReversiBitController extends GenericGameController { public class ReversiBitController extends GenericGameController<BitboardReversi> {
public ReversiBitController(Player[] players) { public ReversiBitController(Player<BitboardReversi>[] players) {
BitboardReversi game = new BitboardReversi(); BitboardReversi game = new BitboardReversi(players);
game.init(players); ThreadBehaviour thread = new LocalThreadBehaviour<>(game);
ThreadBehaviour thread = new LocalThreadBehaviour(game); for (Player<BitboardReversi> player : players) {
for (Player player : players) { if (player instanceof OnlinePlayer<BitboardReversi>){
if (player instanceof OnlinePlayer){ thread = new OnlineThreadBehaviour<>(game);
thread = new OnlineThreadBehaviour(game);
} }
} }
super(new ReversiBitCanvas(), game, thread, "Reversi"); super(new ReversiBitCanvas(), game, thread, "Reversi");

View File

@@ -3,19 +3,19 @@ package org.toop.app.gameControllers;
import org.toop.app.canvas.TicTacToeBitCanvas; import org.toop.app.canvas.TicTacToeBitCanvas;
import org.toop.framework.gameFramework.model.game.threadBehaviour.ThreadBehaviour; import org.toop.framework.gameFramework.model.game.threadBehaviour.ThreadBehaviour;
import org.toop.framework.gameFramework.model.player.Player; import org.toop.framework.gameFramework.model.player.Player;
import org.toop.framework.game.gameThreads.LocalThreadBehaviour; import org.toop.game.gameThreads.LocalFixedRateThreadBehaviour;
import org.toop.framework.game.gameThreads.OnlineThreadBehaviour; import org.toop.game.gameThreads.LocalThreadBehaviour;
import org.toop.framework.game.games.tictactoe.BitboardTicTacToe; import org.toop.game.gameThreads.OnlineThreadBehaviour;
import org.toop.framework.game.players.OnlinePlayer; import org.toop.game.games.tictactoe.BitboardTicTacToe;
import org.toop.game.players.OnlinePlayer;
public class TicTacToeBitController extends GenericGameController { public class TicTacToeBitController extends GenericGameController<BitboardTicTacToe> {
public TicTacToeBitController(Player[] players) { public TicTacToeBitController(Player<BitboardTicTacToe>[] players) {
BitboardTicTacToe game = new BitboardTicTacToe(); BitboardTicTacToe game = new BitboardTicTacToe(players);
game.init(players); ThreadBehaviour thread = new LocalThreadBehaviour<>(game);
ThreadBehaviour thread = new LocalThreadBehaviour(game); for (Player<BitboardTicTacToe> player : players) {
for (Player player : players) { if (player instanceof OnlinePlayer<BitboardTicTacToe>){
if (player instanceof OnlinePlayer){ thread = new OnlineThreadBehaviour<>(game);
thread = new OnlineThreadBehaviour(game);
} }
} }
super(new TicTacToeBitCanvas(), game, thread , "TicTacToe"); super(new TicTacToeBitCanvas(), game, thread , "TicTacToe");

View File

@@ -6,17 +6,17 @@ import org.toop.app.gameControllers.ReversiBitController;
import org.toop.app.gameControllers.TicTacToeBitController; import org.toop.app.gameControllers.TicTacToeBitController;
import org.toop.framework.gameFramework.controller.GameController; import org.toop.framework.gameFramework.controller.GameController;
import org.toop.framework.gameFramework.model.player.Player; import org.toop.framework.gameFramework.model.player.Player;
import org.toop.framework.game.games.reversi.BitboardReversi; import org.toop.game.games.reversi.BitboardReversi;
import org.toop.framework.game.games.tictactoe.BitboardTicTacToe; import org.toop.game.games.tictactoe.BitboardTicTacToe;
import org.toop.framework.game.players.ArtificialPlayer; import org.toop.game.players.ArtificialPlayer;
import org.toop.framework.game.players.LocalPlayer; import org.toop.game.players.LocalPlayer;
import org.toop.app.widget.Primitive; import org.toop.app.widget.Primitive;
import org.toop.app.widget.complex.PlayerInfoWidget; import org.toop.app.widget.complex.PlayerInfoWidget;
import org.toop.app.widget.complex.ViewWidget; import org.toop.app.widget.complex.ViewWidget;
import org.toop.app.widget.popup.ErrorPopup; import org.toop.app.widget.popup.ErrorPopup;
import org.toop.app.widget.tutorial.*; import org.toop.app.widget.tutorial.*;
import org.toop.framework.game.players.MiniMaxAI; import org.toop.game.players.ai.MiniMaxAI;
import org.toop.framework.game.players.RandomAI; import org.toop.game.players.ai.RandomAI;
import org.toop.local.AppContext; import org.toop.local.AppContext;
import javafx.geometry.Pos; import javafx.geometry.Pos;
@@ -52,14 +52,14 @@ public class LocalMultiplayerView extends ViewWidget {
switch (information.type) { switch (information.type) {
case TICTACTOE: case TICTACTOE:
if (information.players[0].isHuman) { if (information.players[0].isHuman) {
players[0] = new LocalPlayer(information.players[0].name); players[0] = new LocalPlayer<>(information.players[0].name);
} else { } else {
players[0] = new ArtificialPlayer(new RandomAI(), "Random AI"); players[0] = new ArtificialPlayer<>(new RandomAI<BitboardTicTacToe>(), "Random AI");
} }
if (information.players[1].isHuman) { if (information.players[1].isHuman) {
players[1] = new LocalPlayer(information.players[1].name); players[1] = new LocalPlayer<>(information.players[1].name);
} else { } else {
players[1] = new ArtificialPlayer(new MiniMaxAI(9), "MiniMax AI"); players[1] = new ArtificialPlayer<>(new MiniMaxAI<BitboardTicTacToe>(9), "MiniMax AI");
} }
if (AppSettings.getSettings().getTutorialFlag() && AppSettings.getSettings().getFirstTTT()) { if (AppSettings.getSettings().getTutorialFlag() && AppSettings.getSettings().getFirstTTT()) {
new ShowEnableTutorialWidget( new ShowEnableTutorialWidget(
@@ -80,14 +80,14 @@ public class LocalMultiplayerView extends ViewWidget {
break; break;
case REVERSI: case REVERSI:
if (information.players[0].isHuman) { if (information.players[0].isHuman) {
players[0] = new LocalPlayer(information.players[0].name); players[0] = new LocalPlayer<>(information.players[0].name);
} else { } else {
players[0] = new ArtificialPlayer(new RandomAI(), "Random AI"); players[0] = new ArtificialPlayer<>(new RandomAI<BitboardReversi>(), "Random AI");
} }
if (information.players[1].isHuman) { if (information.players[1].isHuman) {
players[1] = new LocalPlayer(information.players[1].name); players[1] = new LocalPlayer<>(information.players[1].name);
} else { } else {
players[1] = new ArtificialPlayer(new MiniMaxAI(6), "MiniMax"); players[1] = new ArtificialPlayer<>(new MiniMaxAI<BitboardReversi>(6), "MiniMax");
} }
if (AppSettings.getSettings().getTutorialFlag() && AppSettings.getSettings().getFirstReversi()) { if (AppSettings.getSettings().getTutorialFlag() && AppSettings.getSettings().getFirstReversi()) {
new ShowEnableTutorialWidget( new ShowEnableTutorialWidget(

View File

@@ -146,7 +146,14 @@
<artifactId>error_prone_annotations</artifactId> <artifactId>error_prone_annotations</artifactId>
<version>2.42.0</version> <version>2.42.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.deeplearning4j</groupId>
<artifactId>deeplearning4j-nn</artifactId>
<version>1.0.0-M2.1</version>
<scope>compile</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>

View File

@@ -1,55 +0,0 @@
package org.toop.framework.game.players;
import org.toop.framework.gameFramework.model.player.*;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
/**
* Represents a player controlled by an AI in a game.
* <p>
* This player uses an {@link AbstractAI} instance to determine its moves. The generic
* parameter {@code T} specifies the type of {@link GameR} the AI can handle.
* </p>
*
* @param <T> the specific type of game this AI player can play
*/
public class ArtificialPlayer extends AbstractPlayer {
/** The AI instance used to calculate moves. */
private final AI ai;
/**
* Constructs a new ArtificialPlayer using the specified AI.
*
* @param ai the AI instance that determines moves for this player
*/
public ArtificialPlayer(AI ai, String name) {
super(name);
this.ai = ai;
}
public ArtificialPlayer(ArtificialPlayer other) {
super(other);
this.ai = other.ai.deepCopy();
}
/**
* Determines the next move for this player using its AI.
* <p>
* This method overrides {@link AbstractPlayer#getMove(GameR)}. Because the AI is
* typed to {@code T}, a runtime cast is required. It is the caller's
* responsibility to ensure that {@code gameCopy} is of type {@code T}.
* </p>
*
* @param gameCopy a copy of the current game state
* @return the integer representing the chosen move
* @throws ClassCastException if {@code gameCopy} is not of type {@code T}
*/
public long getMove(TurnBasedGame gameCopy) {
return ai.getMove(gameCopy);
}
@Override
public ArtificialPlayer deepCopy() {
return new ArtificialPlayer(this);
}
}

View File

@@ -1,85 +0,0 @@
package org.toop.framework.game.players;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.AbstractPlayer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class LocalPlayer extends AbstractPlayer {
// Future can be used with event system, IF unsubscribeAfterSuccess works...
// private CompletableFuture<Integer> LastMove = new CompletableFuture<>();
private CompletableFuture<Long> LastMove;
public LocalPlayer(String name) {
super(name);
}
public LocalPlayer(LocalPlayer other) {
super(other);
}
@Override
public long getMove(TurnBasedGame gameCopy) {
return getValidMove(gameCopy);
}
public void setMove(long move) {
LastMove.complete(move);
}
// TODO: helper function, would like to replace to get rid of this method
public static boolean contains(int[] array, int value){
for (int i : array) if (i == value) return true;
return false;
}
private long getMove2(TurnBasedGame gameCopy) {
LastMove = new CompletableFuture<>();
long move = 0;
try {
move = LastMove.get();
System.out.println(Long.toBinaryString(move));
} catch (InterruptedException | ExecutionException e) {
// TODO: Add proper logging.
e.printStackTrace();
}
return move;
}
protected long getValidMove(TurnBasedGame gameCopy){
// Get this player's valid moves
long validMoves = gameCopy.getLegalMoves();
// Make sure provided move is valid
// TODO: Limit amount of retries?
// TODO: Stop copying game so many times
long move = getMove2(gameCopy.deepCopy());
while ((validMoves & move) == 0) {
System.out.println("Not a valid move, try again");
move = getMove2(gameCopy.deepCopy());
}
return move;
}
@Override
public LocalPlayer deepCopy() {
return new LocalPlayer(this.getName());
}
/*public void register() {
// Listening to PlayerAttemptedMove
new EventFlow().listen(GUIEvents.PlayerAttemptedMove.class, event -> {
if (!LastMove.isDone()) {
LastMove.complete(event.move()); // complete the future
}
}, true); // auto-unsubscribe
}
// This blocks until the next move arrives
public int take() throws ExecutionException, InterruptedException {
int move = LastMove.get(); // blocking
LastMove = new CompletableFuture<>(); // reset for next move
return move;
}*/
}

View File

@@ -1,36 +0,0 @@
package org.toop.framework.game.players;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.AbstractPlayer;
import org.toop.framework.gameFramework.model.player.Player;
/**
* Represents a player controlled remotely or over a network.
* <p>
* This class extends {@link AbstractPlayer} and can be used to implement game logic
* where moves are provided by an external source (e.g., another user or a server).
* Currently, this class is a placeholder and does not implement move logic.
* </p>
*/
public class OnlinePlayer extends AbstractPlayer {
/**
* Constructs a new OnlinePlayer.
* <p>
* Currently, no additional initialization is performed. Subclasses or
* future implementations should provide mechanisms to receive moves from
* an external source.
*/
public OnlinePlayer(String name) {
super(name);
}
public OnlinePlayer(OnlinePlayer other) {
super(other);
}
@Override
public Player deepCopy() {
return new OnlinePlayer(this);
}
}

View File

@@ -1,7 +1,8 @@
package org.toop.framework.gameFramework.controller; package org.toop.framework.gameFramework.controller;
import org.toop.framework.gameFramework.model.game.SupportsOnlinePlay;
import org.toop.framework.gameFramework.model.game.threadBehaviour.Controllable; import org.toop.framework.gameFramework.model.game.threadBehaviour.Controllable;
import org.toop.framework.networking.connection.events.NetworkEvents; import org.toop.framework.networking.events.NetworkEvents;
public interface GameController extends Controllable, UpdatesGameUI { public interface GameController extends Controllable, UpdatesGameUI {
/** Called when it is this player's turn to make a move. */ /** Called when it is this player's turn to make a move. */

View File

@@ -2,6 +2,6 @@ package org.toop.framework.gameFramework.model.game;
import org.toop.framework.gameFramework.model.player.Player; import org.toop.framework.gameFramework.model.player.Player;
public interface PlayerProvider { public interface PlayerProvider<T extends TurnBasedGame<T>> {
Player getPlayer(int index); Player<T> getPlayer(int index);
} }

View File

@@ -1,5 +1,7 @@
package org.toop.framework.gameFramework.model.game; package org.toop.framework.gameFramework.model.game;
import org.toop.framework.networking.events.NetworkEvents;
/** /**
* Interface for games that support online multiplayer play. * Interface for games that support online multiplayer play.
* <p> * <p>

View File

@@ -1,9 +1,6 @@
package org.toop.framework.gameFramework.model.game; package org.toop.framework.gameFramework.model.game;
import org.toop.framework.gameFramework.model.player.Player; public interface TurnBasedGame<T extends TurnBasedGame<T>> extends Playable, DeepCopyable<T>, PlayerProvider<T>, BoardProvider {
public interface TurnBasedGame extends Playable, PlayerProvider, BoardProvider, DeepCopyable<TurnBasedGame> {
void init(Player[] players);
int getCurrentTurn(); int getCurrentTurn();
int getPlayerCount(); int getPlayerCount();
int getWinner(); int getWinner();

View File

@@ -16,14 +16,14 @@ import java.util.function.Consumer;
* a running flag, a game reference, and a logger. * a running flag, a game reference, and a logger.
* Subclasses implement the actual game-loop logic. * Subclasses implement the actual game-loop logic.
*/ */
public abstract class AbstractThreadBehaviour implements ThreadBehaviour { public abstract class AbstractThreadBehaviour<T extends TurnBasedGame<T>> implements ThreadBehaviour {
private LongPairConsumer onSendMove; private LongPairConsumer onSendMove;
private Runnable onUpdateUI; private Runnable onUpdateUI;
/** Indicates whether the game loop or event processing is active. */ /** Indicates whether the game loop or event processing is active. */
protected final AtomicBoolean isRunning = new AtomicBoolean(); protected final AtomicBoolean isRunning = new AtomicBoolean();
/** The game instance controlled by this behaviour. */ /** The game instance controlled by this behaviour. */
protected final TurnBasedGame game; protected final T game;
/** Logger for the subclass to report errors or debug info. */ /** Logger for the subclass to report errors or debug info. */
protected final Logger logger = LogManager.getLogger(this.getClass()); protected final Logger logger = LogManager.getLogger(this.getClass());
@@ -33,7 +33,7 @@ public abstract class AbstractThreadBehaviour implements ThreadBehaviour {
* *
* @param game the turn-based game to control * @param game the turn-based game to control
*/ */
public AbstractThreadBehaviour(TurnBasedGame game) { public AbstractThreadBehaviour(T game) {
this.game = game; this.game = game;
} }

View File

@@ -3,5 +3,5 @@ package org.toop.framework.gameFramework.model.player;
import org.toop.framework.gameFramework.model.game.DeepCopyable; import org.toop.framework.gameFramework.model.game.DeepCopyable;
import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.framework.gameFramework.model.game.TurnBasedGame;
public interface AI extends MoveProvider, DeepCopyable<AI> { public interface AI<T extends TurnBasedGame<T>> extends MoveProvider<T>, DeepCopyable<AI<T>> {
} }

View File

@@ -12,6 +12,6 @@ import org.toop.framework.gameFramework.model.game.TurnBasedGame;
* *
* @param <T> the specific type of game this AI can play, extending {@link GameR} * @param <T> the specific type of game this AI can play, extending {@link GameR}
*/ */
public abstract class AbstractAI implements AI { public abstract class AbstractAI<T extends TurnBasedGame<T>> implements AI<T> {
// Concrete AI implementations should override findBestMove(T game, int depth) // Concrete AI implementations should override findBestMove(T game, int depth)
} }

View File

@@ -5,46 +5,66 @@ import org.apache.logging.log4j.Logger;
import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.framework.gameFramework.model.game.TurnBasedGame;
/** /**
* Abstract class representing a player in a game. * Base class for players in a turn-based game.
* <p> *
* Players are entities that can make moves based on the current state of a game. * @param <T> the game type
* player types, such as human players or AI players.
* </p>
* <p>
* Subclasses should override the {@link #getMove(GameR)} method to provide
* specific move logic.
* </p>
*/ */
public abstract class AbstractPlayer implements Player { public abstract class AbstractPlayer<T extends TurnBasedGame<T>> implements Player<T> {
private final Logger logger = LogManager.getLogger(this.getClass());
private final Logger logger = LogManager.getLogger(this.getClass());
private final String name; private final String name;
/**
* Creates a new player with the given name.
*
* @param name the player name
*/
protected AbstractPlayer(String name) { protected AbstractPlayer(String name) {
this.name = name; this.name = name;
} }
protected AbstractPlayer(AbstractPlayer other) { /**
* Creates a copy of another player.
*
* @param other the player to copy
*/
protected AbstractPlayer(AbstractPlayer<T> other) {
this.name = other.name; this.name = other.name;
} }
/** /**
* Determines the next move based on the provided game state. * Gets the player's move for the given game state.
* A deep copy is provided so the player cannot modify the real state.
* <p> * <p>
* The default implementation throws an {@link UnsupportedOperationException}, * This method uses the Template Method Pattern: it defines the fixed
* indicating that concrete subclasses must override this method to provide * algorithm and delegates the variable part to {@link #determineMove(T)}.
* actual move logic.
* </p>
* *
* @param gameCopy a snapshot of the current game state * @param game the current game
* @return an integer representing the chosen move * @return the chosen move
* @throws UnsupportedOperationException if the method is not overridden
*/ */
public long getMove(TurnBasedGame gameCopy) { public final long getMove(T game) {
logger.error("Method getMove not implemented."); return determineMove(game.deepCopy());
throw new UnsupportedOperationException("Not supported yet.");
} }
public String getName(){
/**
* Determines the player's move using a safe copy of the game.
* <p>
* This method is called by {@link #getMove(T)} and should contain
* the player's strategy for choosing a move.
*
* @param gameCopy a deep copy of the game
* @return the chosen move
*/
protected abstract long determineMove(T gameCopy);
/**
* Returns the player's name.
*
* @return the name
*/
public String getName() {
return this.name; return this.name;
} }
} }

View File

@@ -2,6 +2,6 @@ package org.toop.framework.gameFramework.model.player;
import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.framework.gameFramework.model.game.TurnBasedGame;
public interface MoveProvider { public interface MoveProvider<T extends TurnBasedGame<T>> {
long getMove(TurnBasedGame game); long getMove(T game);
} }

View File

@@ -1,5 +1,7 @@
package org.toop.framework.gameFramework.model.player; package org.toop.framework.gameFramework.model.player;
import org.toop.framework.gameFramework.model.game.DeepCopyable; import org.toop.framework.gameFramework.model.game.DeepCopyable;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
public interface Player extends NameProvider, MoveProvider, DeepCopyable<Player> {} public interface Player<T extends TurnBasedGame<T>> extends NameProvider, MoveProvider<T>, DeepCopyable<Player<T>> {
}

View File

@@ -1,13 +1,13 @@
package org.toop.framework.networking.connection; package org.toop.framework.networking;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.toop.framework.SnowflakeGenerator; import org.toop.framework.SnowflakeGenerator;
import org.toop.framework.eventbus.EventFlow; import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.eventbus.bus.EventBus; import org.toop.framework.eventbus.bus.EventBus;
import org.toop.framework.networking.connection.events.NetworkEvents; import org.toop.framework.networking.events.NetworkEvents;
import org.toop.framework.networking.connection.exceptions.ClientNotFoundException; import org.toop.framework.networking.exceptions.ClientNotFoundException;
import org.toop.framework.networking.connection.interfaces.NetworkingClientManager; import org.toop.framework.networking.interfaces.NetworkingClientManager;
public class NetworkingClientEventListener { public class NetworkingClientEventListener {
private static final Logger logger = LogManager.getLogger(NetworkingClientEventListener.class); private static final Logger logger = LogManager.getLogger(NetworkingClientEventListener.class);

View File

@@ -1,4 +1,4 @@
package org.toop.framework.networking.connection; package org.toop.framework.networking;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@@ -10,13 +10,13 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.toop.framework.eventbus.EventFlow; import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.eventbus.bus.EventBus; import org.toop.framework.eventbus.bus.EventBus;
import org.toop.framework.networking.connection.events.NetworkEvents; import org.toop.framework.networking.events.NetworkEvents;
import org.toop.framework.networking.connection.exceptions.ClientNotFoundException; import org.toop.framework.networking.exceptions.ClientNotFoundException;
import org.toop.framework.networking.connection.exceptions.CouldNotConnectException; import org.toop.framework.networking.exceptions.CouldNotConnectException;
import org.toop.framework.networking.connection.interfaces.NetworkingClient; import org.toop.framework.networking.interfaces.NetworkingClient;
import org.toop.framework.networking.connection.types.NetworkingConnector; import org.toop.framework.networking.types.NetworkingConnector;
public class NetworkingClientManager implements org.toop.framework.networking.connection.interfaces.NetworkingClientManager { public class NetworkingClientManager implements org.toop.framework.networking.interfaces.NetworkingClientManager {
private static final Logger logger = LogManager.getLogger(NetworkingClientManager.class); private static final Logger logger = LogManager.getLogger(NetworkingClientManager.class);
private final EventBus eventBus; private final EventBus eventBus;

View File

@@ -1,4 +1,4 @@
package org.toop.framework.networking.connection.clients; package org.toop.framework.networking.clients;
import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*; import io.netty.channel.*;
@@ -12,9 +12,9 @@ import io.netty.util.CharsetUtil;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.toop.framework.eventbus.bus.EventBus; import org.toop.framework.eventbus.bus.EventBus;
import org.toop.framework.networking.connection.exceptions.CouldNotConnectException; import org.toop.framework.networking.exceptions.CouldNotConnectException;
import org.toop.framework.networking.connection.handlers.NetworkingGameClientHandler; import org.toop.framework.networking.handlers.NetworkingGameClientHandler;
import org.toop.framework.networking.connection.interfaces.NetworkingClient; import org.toop.framework.networking.interfaces.NetworkingClient;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;

View File

@@ -1,3 +0,0 @@
package org.toop.framework.networking.connection.types;
public record ServerMessage(String message) {}

View File

@@ -1,4 +1,4 @@
package org.toop.framework.networking.connection.events; package org.toop.framework.networking.events;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -6,8 +6,8 @@ import java.util.concurrent.CompletableFuture;
import org.toop.annotations.AutoResponseResult; import org.toop.annotations.AutoResponseResult;
import org.toop.framework.eventbus.GlobalEventBus; import org.toop.framework.eventbus.GlobalEventBus;
import org.toop.framework.eventbus.events.*; import org.toop.framework.eventbus.events.*;
import org.toop.framework.networking.connection.interfaces.NetworkingClient; import org.toop.framework.networking.interfaces.NetworkingClient;
import org.toop.framework.networking.connection.types.NetworkingConnector; import org.toop.framework.networking.types.NetworkingConnector;
/** /**
* Defines all event types related to the networking subsystem. * Defines all event types related to the networking subsystem.

View File

@@ -1,4 +1,4 @@
package org.toop.framework.networking.connection.exceptions; package org.toop.framework.networking.exceptions;
/** /**
* Thrown when an operation is attempted on a networking client * Thrown when an operation is attempted on a networking client

View File

@@ -1,4 +1,4 @@
package org.toop.framework.networking.connection.exceptions; package org.toop.framework.networking.exceptions;
public class CouldNotConnectException extends RuntimeException { public class CouldNotConnectException extends RuntimeException {

View File

@@ -1,4 +1,4 @@
package org.toop.framework.networking.connection.exceptions; package org.toop.framework.networking.exceptions;
public class NetworkingInitializationException extends RuntimeException { public class NetworkingInitializationException extends RuntimeException {
public NetworkingInitializationException(String message, Throwable cause) { public NetworkingInitializationException(String message, Throwable cause) {

View File

@@ -1,4 +1,4 @@
package org.toop.framework.networking.connection.handlers; package org.toop.framework.networking.handlers;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInboundHandlerAdapter;
@@ -9,7 +9,7 @@ import java.util.regex.Pattern;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.toop.framework.eventbus.bus.EventBus; import org.toop.framework.eventbus.bus.EventBus;
import org.toop.framework.networking.connection.events.NetworkEvents; import org.toop.framework.networking.events.NetworkEvents;
public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter { public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
private static final Logger logger = LogManager.getLogger(NetworkingGameClientHandler.class); private static final Logger logger = LogManager.getLogger(NetworkingGameClientHandler.class);

View File

@@ -1,6 +1,6 @@
package org.toop.framework.networking.connection.interfaces; package org.toop.framework.networking.interfaces;
import org.toop.framework.networking.connection.exceptions.CouldNotConnectException; import org.toop.framework.networking.exceptions.CouldNotConnectException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;

View File

@@ -1,8 +1,8 @@
package org.toop.framework.networking.connection.interfaces; package org.toop.framework.networking.interfaces;
import org.toop.framework.networking.connection.exceptions.ClientNotFoundException; import org.toop.framework.networking.exceptions.ClientNotFoundException;
import org.toop.framework.networking.connection.exceptions.CouldNotConnectException; import org.toop.framework.networking.exceptions.CouldNotConnectException;
import org.toop.framework.networking.connection.types.NetworkingConnector; import org.toop.framework.networking.types.NetworkingConnector;
public interface NetworkingClientManager { public interface NetworkingClientManager {
void startClient( void startClient(

View File

@@ -1,30 +0,0 @@
package org.toop.framework.networking.server;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
public class Game implements OnlineGame<TurnBasedGame> {
private long id;
private User[] users;
private TurnBasedGame game;
public Game(TurnBasedGame game, User... users) {
this.game = game;
this.users = users;
}
@Override
public long id() {
return id;
}
@Override
public TurnBasedGame game() {
return game;
}
@Override
public User[] users() {
return users;
}
}

View File

@@ -1,43 +0,0 @@
package org.toop.framework.networking.server;
import org.toop.framework.SnowflakeGenerator;
public class GameChallenge {
private final long id = SnowflakeGenerator.nextId(); // I don't need this, but the tournament server uses it...
private final ServerUser from;
private final ServerUser to;
private final String gameType;
private final SimpleTimer timer;
private boolean isChallengeAccepted = false;
public GameChallenge(ServerUser from, ServerUser to, String gameType, SimpleTimer timer) {
this.from = from;
this.to = to;
this.gameType = gameType;
this.timer = timer;
}
public long id() {
return id;
}
public ServerUser[] getUsers() {
return new ServerUser[]{from, to};
}
public void forceExpire() {
timer.forceExpire();
}
public String acceptChallenge() {
isChallengeAccepted = true;
timer.forceExpire();
return gameType;
}
public boolean isExpired() {
return timer.isExpired();
}
}

View File

@@ -1,33 +0,0 @@
package org.toop.framework.networking.server;
import java.time.Instant;
import java.time.Duration;
public class GameChallengeTimer implements SimpleTimer {
private final Instant createdAt;
private final Duration timeout;
private boolean isExpired = false;
public GameChallengeTimer(Duration duration) {
this.createdAt = Instant.now();
this.timeout = duration;
}
@Override
public void forceExpire() {
this.isExpired = true;
}
@Override
public boolean isExpired() {
if (this.isExpired) return true;
return Instant.now().isAfter(createdAt.plus(timeout));
}
@Override
public long secondsRemaining() {
return Duration.between(Instant.now(), createdAt.plus(timeout)).toSeconds();
}
}

View File

@@ -1,28 +0,0 @@
package org.toop.framework.networking.server;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import java.lang.reflect.InvocationTargetException;
public class GameDefinition<T> {
private final String name;
private final Class<T> game;
public GameDefinition(String name, Class<T> game) {
this.name = name;
this.game = game;
}
public String name() {
return name;
}
public T create(String... users) {
try {
return game.getDeclaredConstructor().newInstance(users);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -1,8 +0,0 @@
package org.toop.framework.networking.server;
public interface GameServer {
// List<?> gameTypes();
// List<?> ongoingGames();
// void startGame(String gameType, User... users);
// String[] onlineUsers();
}

View File

@@ -1,70 +0,0 @@
package org.toop.framework.networking.server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.nio.NioIoHandler;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import org.toop.framework.SnowflakeGenerator;
import org.toop.framework.game.BitboardGame;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import java.time.Duration;
import java.util.Map;
public class MasterServer {
private final int port;
public final Server gs;
public MasterServer(int port, Map<String, Class<? extends TurnBasedGame>> gameTypes, Duration challengeDuration) {
this.port = port;
this.gs = new Server(gameTypes, challengeDuration);
}
public void start() throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(workerGroup);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.option(ChannelOption.SO_BACKLOG, 128);
bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
bootstrap.handler(new LoggingHandler(LogLevel.INFO));
bootstrap.childHandler(
new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LineBasedFrameDecoder(8192));
pipeline.addLast(new StringDecoder());
pipeline.addLast(new StringEncoder());
long userid = SnowflakeGenerator.nextId();
User user = new User(userid, ""+userid);
pipeline.addLast(new ServerHandler(user, gs));
}
}
);
ChannelFuture future = bootstrap.bind(port).sync();
System.out.println("MasterServer listening on port " + port);
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}

View File

@@ -1,7 +0,0 @@
package org.toop.framework.networking.server;
public interface MessageStore {
void add(String message);
String get();
void reset();
}

View File

@@ -1,7 +0,0 @@
package org.toop.framework.networking.server;
public interface OnlineGame<T> {
long id();
T game();
User[] users();
}

View File

@@ -1,4 +0,0 @@
package org.toop.framework.networking.server;
public record ParsedMessage(String command, String... args) {}

View File

@@ -1,4 +0,0 @@
package org.toop.framework.networking.server;
public class Parser {
}

View File

@@ -1,4 +0,0 @@
package org.toop.framework.networking.server;
public interface ServableGame {
}

View File

@@ -1,176 +0,0 @@
package org.toop.framework.networking.server;
import org.toop.framework.game.players.LocalPlayer;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.Player;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.time.Duration;
public class Server implements GameServer {
final private Map<String, Class<? extends TurnBasedGame>> gameTypes;
final private Map<Long, ServerUser> users = new ConcurrentHashMap<>();
final private List<GameChallenge> gameChallenges = new CopyOnWriteArrayList<>();
final private List<OnlineGame<TurnBasedGame>> games = new CopyOnWriteArrayList<>();
final private Duration challengeDuration;
final private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
public Server(Map<String, Class<? extends TurnBasedGame>> gameTypes, Duration challengeDuration) {
this.gameTypes = gameTypes;
this.challengeDuration = challengeDuration;
scheduler.schedule(this::serverTask, 0, TimeUnit.MILLISECONDS);
}
private void serverTask() {
checkChallenges();
scheduler.schedule(this::serverTask, 500, TimeUnit.MILLISECONDS);
}
public void addUser(ServerUser user) {
users.putIfAbsent(user.id(), user);
}
public void removeUser(ServerUser user) {
users.remove(user.id());
}
public String[] gameTypes() {
return gameTypes.keySet().toArray(new String[0]);
}
public List<OnlineGame<TurnBasedGame>> ongoingGames() {
return games;
}
public ServerUser getUser(String username) {
return users.values().stream().filter(e -> e.name().equalsIgnoreCase(username)).findFirst().orElse(null);
}
public ServerUser getUser(long id) {
return users.get(id);
}
public void challengeUser(String fromUser, String toUser, String gameType) {
ServerUser from = getUser(fromUser);
if (from == null) {
return;
}
if (!gameTypes.containsKey(gameType)) {
from.sendMessage("ERR gametype not found");
return;
}
ServerUser to = getUser(toUser);
if (to == null) {
from.sendMessage("ERR user not found");
return;
}
var ch = new GameChallenge(from, to, gameType, new GameChallengeTimer(challengeDuration));
to.sendMessage(
"\"SVR GAME CHALLENGE {CHALLENGER: \"%s\", GAMETYPE: \"%s\", CHALLENGENUMBER: \"%s\"}"
.formatted(from.name(), gameType, ch.id())
);
if (!isValidChallenge(ch)) {
warnUserExpiredChallenge(from, ch.id());
ch.forceExpire();
return;
}
gameChallenges.addLast(ch);
}
private void warnUserExpiredChallenge(ServerUser user, long challengeId) {
user.sendMessage("SVR GAME CHALLENGE CANCELLED {CHALLENGENUMBER: \"" + challengeId + "\"}");
}
private boolean isValidChallenge(GameChallenge gameChallenge) {
for (var user : gameChallenge.getUsers()) {
if (users.get(user.id()) == null) {
return false;
}
if (user.games().length > 0) {
return false;
}
if (gameChallenge.isExpired()) {
return false;
}
}
return true;
}
public void checkChallenges() {
for (int i = gameChallenges.size() - 1; i >= 0; i--) {
var challenge = gameChallenges.get(i);
if (isValidChallenge(challenge)) continue;
if (challenge.isExpired()) {
Arrays.stream(challenge.getUsers()).forEach(user -> warnUserExpiredChallenge(user, challenge.id()));
gameChallenges.remove(i);
}
}
}
public void acceptChallenge(long challengeId) {
for (var challenge : gameChallenges) {
if (challenge.id() == challengeId) {
startGame(challenge.acceptChallenge(), (User[]) challenge.getUsers());
break;
}
}
}
public List<GameChallenge> gameChallenges() {
return gameChallenges;
}
public void startGame(String gameType, User... users) {
if (!gameTypes.containsKey(gameType)) return;
try {
Player[] players = new Player[users.length];
for (int i = 0; i < users.length; i++) {
players[i] = new LocalPlayer(users[i].name());
}
var game = new Game(gameTypes.get(gameType).getDeclaredConstructor().newInstance(), users);
game.game().init(players);
games.addLast(game);
} catch (Exception ignored) {}
}
// public void checkGames() {
// for (int i = games.size() - 1; i >= 0; i--) {
// var game = games.get(i);
// if (game.game().getWinner() >= 0) games.remove(i);
// }
// }
public String[] onlineUsers() {
return users.values().stream().map(ServerUser::name).toArray(String[]::new);
}
public void closeServer() {
scheduler.shutdown();
gameChallenges.clear();
games.clear();
users.clear();
gameTypes.clear();
}
}

View File

@@ -1,138 +0,0 @@
package org.toop.framework.networking.server;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class ServerHandler extends SimpleChannelInboundHandler<String> {
private final User user;
private final Server server;
public ServerHandler(User user, Server server) {
this.user = user;
this.server = server;
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
ctx.writeAndFlush("WELCOME " + user.id() + "\n");
user.setCtx(ctx);
server.addUser(user); // TODO set correct name on login
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
ParsedMessage p = parse(msg);
if (p == null) return;
IO.println(p.command() + " " + Arrays.toString(p.args()));
switch (p.command()) {
case "ping" -> ctx.writeAndFlush("PONG\n");
case "login" -> handleLogin(p);
case "get" -> handleGet(p);
case "subscribe" -> handleSubscribe(p);
case "move" -> handleMove(p);
case "challenge" -> handleChallenge(p);
case "message" -> handleMessage(p);
case "help" -> handleHelp(p);
default -> ctx.writeAndFlush("ERROR Unknown command\n");
}
}
private boolean allowedArgs(String... args) {
if (args.length < 1) return false;
return true;
}
private void handleLogin(ParsedMessage p) {
if (!allowedArgs(p.args())) return;
user.setName(p.args()[0]);
}
private void handleGet(ParsedMessage p) {
if (!allowedArgs(p.args())) return;
switch (p.args()[0]) {
case "playerlist" -> user.ctx().writeAndFlush(Arrays.toString(server.onlineUsers()));
case "gamelist" -> user.ctx().writeAndFlush(Arrays.toString(server.gameTypes()));
}
}
private void handleSubscribe(ParsedMessage p) {
// TODO
}
private void handleHelp(ParsedMessage p) {
// TODO
}
private void handleMessage(ParsedMessage p) {
// TODO
}
private void handleChallenge(ParsedMessage p) {
if(!allowedArgs(p.args())) return;
if (p.args().length < 2) return;
if (p.args()[0].equalsIgnoreCase("accept")) {
try {
long id = Long.parseLong(p.args()[1]);
if (id <= 0) {
user.sendMessage("ERR id must be a positive number");
return;
}
server.acceptChallenge(id);
} catch (NumberFormatException e) {
user.sendMessage("ERR id is not a valid number or too big");
return;
}
return;
}
server.challengeUser(user.name(), p.args()[0], p.args()[1]);
}
private void handleMove(ParsedMessage p) {
// TODO
}
private ParsedMessage parse(String msg) {
// TODO, what if empty string.
if (msg.isEmpty()) return null;
msg = msg.trim().toLowerCase();
List<String> parts = new LinkedList<>(List.of(msg.split(" ")));
if (parts.size() > 1) {
String command = parts.removeFirst();
return new ParsedMessage(command, parts.toArray(String[]::new));
}
else {
return new ParsedMessage(msg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
server.removeUser(user);
}
}

View File

@@ -1,27 +0,0 @@
package org.toop.framework.networking.server;
import java.util.Queue;
public class ServerMessageStore implements MessageStore {
Queue<String> messageQueue;
public ServerMessageStore(Queue<String> messageQueue) {
this.messageQueue = messageQueue;
}
@Override
public void add(String message) {
messageQueue.offer(message);
}
@Override
public String get() {
return messageQueue.poll();
}
@Override
public void reset() {
messageQueue.clear();
}
}

View File

@@ -1,11 +0,0 @@
package org.toop.framework.networking.server;
public interface ServerUser {
long id();
String name();
Game[] games();
void addGame(Game game);
void removeGame(Game game);
void setName(String name);
void sendMessage(String message);
}

View File

@@ -1,7 +0,0 @@
package org.toop.framework.networking.server;
public interface SimpleTimer {
void forceExpire();
boolean isExpired();
long secondsRemaining();
}

View File

@@ -1,66 +0,0 @@
package org.toop.framework.networking.server;
import io.netty.channel.ChannelHandlerContext;
import java.util.ArrayList;
import java.util.List;
public class User implements ServerUser {
final private long id;
private String name;
private final List<Game> games = new ArrayList<>();
private ChannelHandlerContext connectionContext;
public User(long userId, String name) {
this.id = userId;
this.name = name;
}
@Override
public long id() {
return id;
}
@Override
public String name() {
return name;
}
@Override
public void addGame(Game game) {
games.add(game);
}
@Override
public void removeGame(Game game) {
games.remove(game);
}
@Override
public Game[] games() {
return games.toArray(new Game[0]);
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public void sendMessage(String message) {
IO.println(message);
ctx().channel().writeAndFlush(message);
}
public ChannelHandlerContext ctx() {
return connectionContext;
}
public void setCtx(ChannelHandlerContext ctx) {
this.connectionContext = ctx;
}
}

View File

@@ -1,4 +1,4 @@
package org.toop.framework.networking.connection.types; package org.toop.framework.networking.types;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;

View File

@@ -1,3 +1,3 @@
package org.toop.framework.networking.connection.types; package org.toop.framework.networking.types;
public record ServerCommand(long clientId, String command) {} public record ServerCommand(long clientId, String command) {}

View File

@@ -0,0 +1,3 @@
package org.toop.framework.networking.types;
public record ServerMessage(String message) {}

View File

@@ -9,7 +9,7 @@
//import org.mockito.*; //import org.mockito.*;
//import org.toop.framework.SnowflakeGenerator; //import org.toop.framework.SnowflakeGenerator;
//import org.toop.framework.eventbus.EventFlow; //import org.toop.framework.eventbus.EventFlow;
//import org.toop.framework.networking.connection.events.NetworkEvents; //import org.toop.framework.networking.events.NetworkEvents;
// //
//class NetworkingClientManagerTest { //class NetworkingClientManagerTest {
// //

View File

@@ -1,4 +1,4 @@
//package org.toop.framework.networking.connection.events; //package org.toop.framework.networking.events;
// //
//import static org.junit.jupiter.api.Assertions.*; //import static org.junit.jupiter.api.Assertions.*;
// //

View File

@@ -1,167 +0,0 @@
package org.toop.framework.networking.server;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.toop.framework.gameFramework.model.game.PlayResult;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.Player;
import java.time.Duration;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
public class ServerTest {
static class TurnBasedGameMock implements TurnBasedGame {
private Player[] players;
public TurnBasedGameMock() {}
@Override
public void init(Player[] players) {
this.players = players;
}
@Override
public int getCurrentTurn() {
return 0;
}
@Override
public int getPlayerCount() {
return 0;
}
@Override
public int getWinner() {
return 0;
}
@Override
public long[] getBoard() {
return new long[0];
}
@Override
public TurnBasedGame deepCopy() {
return null;
}
@Override
public long getLegalMoves() {
return 0;
}
@Override
public PlayResult play(long move) {
return null;
}
@Override
public Player getPlayer(int index) {
return null;
}
}
static class TestUser implements ServerUser {
final private long id;
private String name;
public TestUser(long id, String name) {
this.id = id;
this.name = name;
}
@Override
public long id() {
return id;
}
@Override
public String name() {
return name;
}
@Override
public Game[] games() {
return new Game[0];
}
@Override
public void addGame(Game game) {
}
@Override
public void removeGame(Game game) {
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public void sendMessage(String message) {
}
}
private Server server;
private Duration waitTime = Duration.ofSeconds(2);
@BeforeEach
void setup() {
var games = new ConcurrentHashMap<String, Class<? extends TurnBasedGame>>();
games.put("tictactoe", TurnBasedGameMock.class);
games.put("reversi", TurnBasedGameMock.class);
server = new Server(games, waitTime);
}
@Test
void testGameTypes() {
String[] expected = {"tictactoe", "reversi"};
String[] actual = server.gameTypes();
Arrays.sort(expected);
Arrays.sort(actual);
Assertions.assertArrayEquals(expected, actual);
}
@Test
void testChallenge() {
server.addUser(new TestUser(1, "test1"));
server.addUser(new TestUser(2, "test2"));
server.challengeUser("test1", "test2", "tictactoe");
IO.println(server.gameChallenges());
Assertions.assertEquals(1, server.gameChallenges().size());
try {
Thread.sleep(waitTime.plusMillis(100));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Assertions.assertEquals(0, server.gameChallenges().size());
}
@Test
void testStartGame() {
server.startGame("tictactoe", new User(0, "A"), new User(1, "B"));
Assertions.assertEquals(1, server.ongoingGames().size());
server.startGame("reversi", new User(0, "A"), new User(1, "B"));
Assertions.assertEquals(2, server.ongoingGames().size());
}
}

View File

@@ -1,17 +0,0 @@
$client = New-Object System.Net.Sockets.TcpClient("localhost", 6666)
$stream = $client.GetStream()
$writer = New-Object System.IO.StreamWriter($stream)
$reader = New-Object System.IO.StreamReader($stream)
$client2 = New-Object System.Net.Sockets.TcpClient("localhost", 6666)
$stream2 = $client2.GetStream()
$writer2 = New-Object System.IO.StreamWriter($stream2)
$reader2 = New-Object System.IO.StreamReader($stream2)
$writer.WriteLine("login john")
$writer.Flush()
$reader.ReadLine()
$writer2.WriteLine("login hendrik")
$writer2.Flush()
$reader2.ReadLine()

View File

@@ -105,6 +105,16 @@
<version>0.1</version> <version>0.1</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>org.deeplearning4j</groupId>
<artifactId>deeplearning4j-core</artifactId>
<version>1.0.0-M2.1</version>
</dependency>
<dependency>
<groupId>org.nd4j</groupId>
<artifactId>nd4j-native-platform</artifactId>
<version>1.0.0-M2.1</version>
</dependency>
</dependencies> </dependencies>

View File

@@ -1,44 +1,39 @@
package org.toop.framework.game; package org.toop.game;
import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.Player; import org.toop.framework.gameFramework.model.player.Player;
import java.util.Arrays; import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
// There is AI performance to be gained by getting rid of non-primitives and thus speeding up deepCopy // There is AI performance to be gained by getting rid of non-primitives and thus speeding up deepCopy
public abstract class BitboardGame implements TurnBasedGame { public abstract class BitboardGame<T extends BitboardGame<T>> implements TurnBasedGame<T> {
private final int columnSize; private final int columnSize;
private final int rowSize; private final int rowSize;
private Player[] players; private Player<T>[] players;
// long is 64 bits. Every game has a limit of 64 cells maximum. // long is 64 bits. Every game has a limit of 64 cells maximum.
private long[] playerBitboard; private final long[] playerBitboard;
private int currentTurn = 0; private int currentTurn = 0;
private int playerCount;
public BitboardGame(int columnSize, int rowSize, int playerCount) { public BitboardGame(int columnSize, int rowSize, int playerCount, Player<T>[] players) {
this.columnSize = columnSize; this.columnSize = columnSize;
this.rowSize = rowSize; this.rowSize = rowSize;
this.playerCount = playerCount;
}
@Override
public void init(Player[] players) {
this.players = players; this.players = players;
this.playerBitboard = new long[playerCount]; this.playerBitboard = new long[playerCount];
Arrays.fill(playerBitboard, 0L); Arrays.fill(playerBitboard, 0L);
} }
public BitboardGame(BitboardGame other) { public BitboardGame(BitboardGame<T> other) {
this.columnSize = other.columnSize; this.columnSize = other.columnSize;
this.rowSize = other.rowSize; this.rowSize = other.rowSize;
this.playerBitboard = other.playerBitboard.clone(); this.playerBitboard = other.playerBitboard.clone();
this.currentTurn = other.currentTurn; this.currentTurn = other.currentTurn;
this.players = Arrays.stream(other.players) this.players = Arrays.stream(other.players)
.map(Player::deepCopy) .map(Player<T>::deepCopy)
.toArray(Player[]::new); .toArray(Player[]::new);
} }
@@ -66,7 +61,7 @@ public abstract class BitboardGame implements TurnBasedGame {
return getCurrentPlayerIndex(); return getCurrentPlayerIndex();
} }
public Player getPlayer(int index) {return players[index];} public Player<T> getPlayer(int index) {return players[index];}
public int getCurrentPlayerIndex() { public int getCurrentPlayerIndex() {
return currentTurn % playerBitboard.length; return currentTurn % playerBitboard.length;
@@ -76,7 +71,7 @@ public abstract class BitboardGame implements TurnBasedGame {
return (currentTurn + 1) % playerBitboard.length; return (currentTurn + 1) % playerBitboard.length;
} }
public Player getCurrentPlayer(){ public Player<T> getCurrentPlayer(){
return players[getCurrentPlayerIndex()]; return players[getCurrentPlayerIndex()];
} }

View File

@@ -1,3 +1,3 @@
package org.toop.framework.game; package org.toop.game;
// TODO: Remove this, only used in ReversiCanvas. Needs to not // TODO: Remove this, only used in ReversiCanvas. Needs to not
public record Move(int position, char value) {} public record Move(int position, char value) {}

View File

@@ -1,4 +1,4 @@
package org.toop.framework.game.gameThreads; package org.toop.game.gameThreads;
import org.toop.framework.eventbus.EventFlow; import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.gameFramework.GameState; import org.toop.framework.gameFramework.GameState;
@@ -16,7 +16,7 @@ import java.util.function.Consumer;
* Runs a separate thread that executes game turns at a fixed frequency (default 60 updates/sec), * Runs a separate thread that executes game turns at a fixed frequency (default 60 updates/sec),
* applying player moves, updating the game state, and dispatching UI events. * applying player moves, updating the game state, and dispatching UI events.
*/ */
public class LocalFixedRateThreadBehaviour extends AbstractThreadBehaviour implements Runnable { public class LocalFixedRateThreadBehaviour<T extends TurnBasedGame<T>> extends AbstractThreadBehaviour<T> implements Runnable {
/** /**
@@ -24,7 +24,7 @@ public class LocalFixedRateThreadBehaviour extends AbstractThreadBehaviour imple
* *
* @param game the game instance * @param game the game instance
*/ */
public LocalFixedRateThreadBehaviour(TurnBasedGame game) { public LocalFixedRateThreadBehaviour(T game) {
super(game); super(game);
} }
@@ -59,7 +59,7 @@ public class LocalFixedRateThreadBehaviour extends AbstractThreadBehaviour imple
if (now >= nextUpdate) { if (now >= nextUpdate) {
nextUpdate += UPDATE_INTERVAL; nextUpdate += UPDATE_INTERVAL;
Player currentPlayer = game.getPlayer(game.getCurrentTurn()); Player<T> currentPlayer = game.getPlayer(game.getCurrentTurn());
long move = currentPlayer.getMove(game.deepCopy()); long move = currentPlayer.getMove(game.deepCopy());
PlayResult result = game.play(move); PlayResult result = game.play(move);

View File

@@ -1,4 +1,4 @@
package org.toop.framework.game.gameThreads; package org.toop.game.gameThreads;
import org.toop.framework.eventbus.EventFlow; import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.gameFramework.model.game.threadBehaviour.AbstractThreadBehaviour; import org.toop.framework.gameFramework.model.game.threadBehaviour.AbstractThreadBehaviour;
@@ -16,14 +16,14 @@ import java.util.function.Consumer;
* Repeatedly gets the current player's move, applies it to the game, * Repeatedly gets the current player's move, applies it to the game,
* updates the UI, and stops when the game ends or {@link #stop()} is called. * updates the UI, and stops when the game ends or {@link #stop()} is called.
*/ */
public class LocalThreadBehaviour extends AbstractThreadBehaviour implements Runnable { public class LocalThreadBehaviour<T extends TurnBasedGame<T>> extends AbstractThreadBehaviour<T> implements Runnable {
/** /**
* Creates a new behaviour for a local turn-based game. * Creates a new behaviour for a local turn-based game.
* *
* @param game the game instance * @param game the game instance
*/ */
public LocalThreadBehaviour(TurnBasedGame game) { public LocalThreadBehaviour(T game) {
super(game); super(game);
} }
@@ -48,7 +48,7 @@ public class LocalThreadBehaviour extends AbstractThreadBehaviour implements Run
@Override @Override
public void run() { public void run() {
while (isRunning.get()) { while (isRunning.get()) {
Player currentPlayer = game.getPlayer(game.getCurrentTurn()); Player<T> currentPlayer = game.getPlayer(game.getCurrentTurn());
long move = currentPlayer.getMove(game.deepCopy()); long move = currentPlayer.getMove(game.deepCopy());
PlayResult result = game.play(move); PlayResult result = game.play(move);

View File

@@ -1,4 +1,4 @@
package org.toop.framework.game.gameThreads; package org.toop.game.gameThreads;
import org.toop.framework.eventbus.EventFlow; import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.gameFramework.model.game.threadBehaviour.AbstractThreadBehaviour; import org.toop.framework.gameFramework.model.game.threadBehaviour.AbstractThreadBehaviour;
@@ -6,7 +6,7 @@ import org.toop.framework.gameFramework.view.GUIEvents;
import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.game.SupportsOnlinePlay; import org.toop.framework.gameFramework.model.game.SupportsOnlinePlay;
import org.toop.framework.gameFramework.model.player.Player; import org.toop.framework.gameFramework.model.player.Player;
import org.toop.framework.game.players.OnlinePlayer; import org.toop.game.players.OnlinePlayer;
/** /**
* Handles online multiplayer game logic. * Handles online multiplayer game logic.
@@ -14,17 +14,17 @@ import org.toop.framework.game.players.OnlinePlayer;
* Reacts to server events, sending moves and updating the game state * Reacts to server events, sending moves and updating the game state
* for the local player while receiving moves from other players. * for the local player while receiving moves from other players.
*/ */
public class OnlineThreadBehaviour extends AbstractThreadBehaviour implements SupportsOnlinePlay { public class OnlineThreadBehaviour<T extends TurnBasedGame<T>> extends AbstractThreadBehaviour<T> implements SupportsOnlinePlay {
/** /**
* Creates behaviour and sets the first local player * Creates behaviour and sets the first local player
* (non-online player) from the given array. * (non-online player) from the given array.
*/ */
public OnlineThreadBehaviour(TurnBasedGame game) { public OnlineThreadBehaviour(T game) {
super(game); super(game);
} }
/** Finds the first non-online player in the array. */ /** Finds the first non-online player in the array. */
private int getFirstNotOnlinePlayer(Player[] players) { private int getFirstNotOnlinePlayer(Player<T>[] players) {
for (int i = 0; i < players.length; i++) { for (int i = 0; i < players.length; i++) {
if (!(players[i] instanceof OnlinePlayer)) { if (!(players[i] instanceof OnlinePlayer)) {
return i; return i;

View File

@@ -1,6 +1,7 @@
package org.toop.framework.game.gameThreads; package org.toop.game.gameThreads;
import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.networking.events.NetworkEvents;
/** /**
* Online thread behaviour that adds a fixed delay before processing * Online thread behaviour that adds a fixed delay before processing
@@ -9,14 +10,14 @@ import org.toop.framework.gameFramework.model.game.TurnBasedGame;
* This is identical to {@link OnlineThreadBehaviour}, but inserts a * This is identical to {@link OnlineThreadBehaviour}, but inserts a
* short sleep before delegating to the base implementation. * short sleep before delegating to the base implementation.
*/ */
public class OnlineWithSleepThreadBehaviour extends OnlineThreadBehaviour { public class OnlineWithSleepThreadBehaviour<T extends TurnBasedGame<T>> extends OnlineThreadBehaviour<T> {
/** /**
* Creates the behaviour and forwards the players to the base class. * Creates the behaviour and forwards the players to the base class.
* *
* @param game the online-capable turn-based game * @param game the online-capable turn-based game
*/ */
public OnlineWithSleepThreadBehaviour(TurnBasedGame game) { public OnlineWithSleepThreadBehaviour(T game) {
super(game); super(game);
} }

View File

@@ -1,20 +1,19 @@
package org.toop.framework.game.games.reversi; package org.toop.game.games.reversi;
import org.toop.framework.game.BitboardGame;
import org.toop.framework.gameFramework.GameState; import org.toop.framework.gameFramework.GameState;
import org.toop.framework.gameFramework.model.game.PlayResult; import org.toop.framework.gameFramework.model.game.PlayResult;
import org.toop.framework.gameFramework.model.player.Player; import org.toop.framework.gameFramework.model.player.Player;
import org.toop.framework.game.BitboardGame; import org.toop.game.BitboardGame;
public class BitboardReversi extends BitboardGame { public class BitboardReversi extends BitboardGame<BitboardReversi> {
public record Score(int black, int white) {} public record Score(int black, int white) {}
private final long notAFile = 0xfefefefefefefefeL; private final long notAFile = 0xfefefefefefefefeL;
private final long notHFile = 0x7f7f7f7f7f7f7f7fL; private final long notHFile = 0x7f7f7f7f7f7f7f7fL;
public BitboardReversi() { public BitboardReversi(Player<BitboardReversi>[] players) {
super(8, 8, 2); super(8, 8, 2, players);
// Black (player 0) // Black (player 0)
setPlayerBitboard(0, (1L << (3 + 4 * 8)) | (1L << (4 + 3 * 8))); setPlayerBitboard(0, (1L << (3 + 4 * 8)) | (1L << (4 + 3 * 8)));
@@ -23,11 +22,6 @@ public class BitboardReversi extends BitboardGame {
setPlayerBitboard(1, (1L << (3 + 3 * 8)) | (1L << (4 + 4 * 8))); setPlayerBitboard(1, (1L << (3 + 3 * 8)) | (1L << (4 + 4 * 8)));
} }
@Override
public void init(Player[] players) {
super.init(players);
}
public BitboardReversi(BitboardReversi other) { public BitboardReversi(BitboardReversi other) {
super(other); super(other);
} }
@@ -173,4 +167,13 @@ public class BitboardReversi extends BitboardGame {
private long shift(long bit, int shift, long mask) { private long shift(long bit, int shift, long mask) {
return shift > 0 ? (bit << shift) & mask : (bit >>> -shift) & mask; return shift > 0 ? (bit << shift) & mask : (bit >>> -shift) & mask;
} }
public boolean isGameOver(){
BitboardReversi copy = this.deepCopy();
if (copy.getLegalMoves() == 0){
nextTurn();
return copy.getLegalMoves() == 0;
}
return false;
}
} }

View File

@@ -1,11 +1,11 @@
package org.toop.framework.game.games.tictactoe; package org.toop.game.games.tictactoe;
import org.toop.framework.gameFramework.GameState; import org.toop.framework.gameFramework.GameState;
import org.toop.framework.gameFramework.model.game.PlayResult; import org.toop.framework.gameFramework.model.game.PlayResult;
import org.toop.framework.gameFramework.model.player.Player; import org.toop.framework.gameFramework.model.player.Player;
import org.toop.framework.game.BitboardGame; import org.toop.game.BitboardGame;
public class BitboardTicTacToe extends BitboardGame { public class BitboardTicTacToe extends BitboardGame<BitboardTicTacToe> {
private final long[] winningLines = { private final long[] winningLines = {
0b111000000L, // top row 0b111000000L, // top row
0b000111000L, // middle row 0b000111000L, // middle row
@@ -17,15 +17,9 @@ public class BitboardTicTacToe extends BitboardGame {
0b001010100L // anti-diagonal 0b001010100L // anti-diagonal
}; };
public BitboardTicTacToe() { public BitboardTicTacToe(Player<BitboardTicTacToe>[] players) {
super(3, 3, 2); super(3, 3, 2, players);
} }
@Override
public void init(Player[] players) {
super.init(players);
}
public BitboardTicTacToe(BitboardTicTacToe other) { public BitboardTicTacToe(BitboardTicTacToe other) {
super(other); super(other);
} }

View File

@@ -0,0 +1,228 @@
package org.toop.game.machinelearning;
import org.deeplearning4j.nn.conf.MultiLayerConfiguration;
import org.deeplearning4j.nn.conf.NeuralNetConfiguration;
import org.deeplearning4j.nn.conf.layers.DenseLayer;
import org.deeplearning4j.nn.conf.layers.OutputLayer;
import org.deeplearning4j.nn.multilayer.MultiLayerNetwork;
import org.deeplearning4j.nn.weights.WeightInit;
import org.deeplearning4j.util.ModelSerializer;
import org.nd4j.linalg.activations.Activation;
import org.nd4j.linalg.api.ndarray.INDArray;
import org.nd4j.linalg.dataset.DataSet;
import org.nd4j.linalg.factory.Nd4j;
import org.nd4j.linalg.learning.config.Adam;
import org.nd4j.linalg.lossfunctions.LossFunctions;
import org.toop.framework.gameFramework.GameState;
import org.toop.framework.gameFramework.model.game.PlayResult;
import org.toop.framework.gameFramework.model.player.AbstractAI;
import org.toop.framework.gameFramework.model.player.Player;
import org.toop.game.games.reversi.BitboardReversi;
import org.toop.game.players.ArtificialPlayer;
import org.toop.game.players.ai.MiniMaxAI;
import org.toop.game.players.ai.RandomAI;
import org.toop.game.players.ai.ReversiAIML;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static java.lang.Math.abs;
import static java.lang.Math.random;
public class NeuralNetwork {
private MultiLayerConfiguration conf;
private MultiLayerNetwork model;
private AbstractAI<BitboardReversi> opponentAI;
private AbstractAI<BitboardReversi> opponentMM = new MiniMaxAI<>(6);
private AbstractAI<BitboardReversi> opponentRand = new RandomAI<>();
private AbstractAI<BitboardReversi> opponentAIML = new ReversiAIML<>();
private Player[] playerSet = new Player[4];
public NeuralNetwork() {}
public void init(){
initPlayers();
conf = new NeuralNetConfiguration.Builder()
.updater(new Adam(0.001))
.weightInit(WeightInit.XAVIER) //todo understand
.list()
.layer(new DenseLayer.Builder()
.nIn(64)
.nOut(128)
.activation(Activation.RELU)
.build())
.layer(new OutputLayer.Builder(LossFunctions.LossFunction.MCXENT)
.nIn(128)
.nOut(64)
.activation(Activation.SOFTMAX)
.build())
.build();
model = new MultiLayerNetwork(conf);
IO.println(model.params());
loadModel();
IO.println(model.params());
model.init();
IO.println(model.summary());
model.setLearningRate(0.0003);
trainingLoop();
saveModel();
}
public void initPlayers(){
playerSet[0] = new ArtificialPlayer<>(new MiniMaxAI<BitboardReversi>(6),"MiniMaxAI");
playerSet[1] = new ArtificialPlayer<>(new RandomAI<BitboardReversi>(),"RandomAI");
playerSet[2] = new ArtificialPlayer<>(new ReversiAIML<BitboardReversi>(),"MachineLearningAI");
}
public void saveModel(){
File modelFile = new File("reversi-model.zip");
try {
ModelSerializer.writeModel(model, modelFile, true);
}catch (Exception e){
e.printStackTrace();
}
}
public void loadModel(){
File modelFile = new File("reversi-model.zip");
try {
model = ModelSerializer.restoreMultiLayerNetwork(modelFile);
} catch (IOException e) {
e.printStackTrace();
}
}
public void trainingLoop(){
int totalGames = 5000;
double epsilon = 0.05;
long start = System.nanoTime();
for (int game = 0; game<totalGames; game++){
char modelPlayer = random()<0.5?'B':'W';
BitboardReversi reversi = new BitboardReversi(new Player[2]);
opponentAI = getOpponentAI();
List<StateAction> gameHistory = new ArrayList<>();
PlayResult state = new PlayResult(GameState.NORMAL,reversi.getCurrentTurn());
double reward = 0;
while (state.state() != GameState.DRAW && state.state() != GameState.WIN){
int curr = reversi.getCurrentTurn();
long move;
if (curr == modelPlayer) {
long[] input = reversi.getBoard();
if (Math.random() < epsilon) {
long moves = reversi.getLegalMoves();
move = (long) (Math.random() * Long.bitCount(moves) - .5f);
} else {
INDArray boardInput = Nd4j.create(new long[][]{input});
INDArray prediction = model.output(boardInput);
int location = pickLegalMove(prediction, reversi);
gameHistory.add(new StateAction(input, location));
move = location;
}
}else{
move = opponentAI.getMove(reversi);
}
state = reversi.play(move);
}
//IO.println(model.params());
BitboardReversi.Score score = reversi.getScore();
int scoreDif = abs(score.black() - score.white());
if (score.black() > score.white()){
reward = 1 + ((scoreDif / 64.0) * 0.5);
}else if (score.black() < score.white()){
reward = -1 - ((scoreDif / 64.0) * 0.5);
}else{
reward = 0;
}
if (modelPlayer == 'W'){
reward = -reward;
}
for (StateAction step : gameHistory){
trainFromHistory(step, reward);
}
//IO.println("Wr: " + (double)p1wins/(game+1) + " draws: " + draws);
if(game % 100 == 0){
IO.println("Completed game " + game + " | Reward: " + reward);
//IO.println(Arrays.toString(reversi.getBoardDouble()));
}
}
long end = System.nanoTime();
IO.println((end-start));
}
private int pickLegalMove(INDArray prediction, BitboardReversi reversi) {
double[] logits = prediction.toDoubleVector();
long legalMoves = reversi.getLegalMoves();
if (legalMoves == 0L) {
return -1;
}
if (Math.random() < 0.01) {
int randomIndex = (int) (Math.random() * Long.bitCount(legalMoves));
long moves = legalMoves;
for (int i = 0; i < randomIndex; i++) {
moves &= moves - 1;
}
return Long.numberOfTrailingZeros(moves);
}
int bestMove = -1;
double bestVal = Double.NEGATIVE_INFINITY;
long moves = legalMoves;
while (moves != 0L) {
int move = Long.numberOfTrailingZeros(moves);
double value = logits[move];
if (value > bestVal) {
bestVal = value;
bestMove = move;
}
moves &= moves - 1;
}
return bestMove;
}
private AbstractAI<BitboardReversi> getOpponentAI(){
return switch ((int) (Math.random() * 4)) {
case 0 -> opponentRand;
case 1 -> opponentMM;
case 2 -> opponentAIML;
default -> opponentRand;
};
}
private void trainFromHistory(StateAction step, double reward){
double[] output = new double[64];
output[step.action] = reward;
DataSet ds = new DataSet(
Nd4j.create(new long[][] { step.state }),
Nd4j.create(new double[][] { output })
);
model.fit(ds);
}
}

View File

@@ -0,0 +1,10 @@
package org.toop.game.machinelearning;
public class StateAction {
long[] state;
int action;
public StateAction(long[] state, int action) {
this.state = state;
this.action = action;
}
}

View File

@@ -0,0 +1,55 @@
package org.toop.game.players;
import org.toop.framework.gameFramework.model.player.*;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
/**
* Represents a player controlled by an AI.
*
* @param <T> the type of turn-based game
*/
public class ArtificialPlayer<T extends TurnBasedGame<T>> extends AbstractPlayer<T> {
private final AI<T> ai;
/**
* Creates a new AI-controlled player.
*
* @param ai the AI controlling this player
* @param name the player's name
*/
public ArtificialPlayer(AI<T> ai, String name) {
super(name);
this.ai = ai;
}
/**
* Creates a copy of another AI-controlled player.
*
* @param other the player to copy
*/
public ArtificialPlayer(ArtificialPlayer<T> other) {
super(other);
this.ai = other.ai.deepCopy();
}
/**
* Determines the player's move using the AI.
*
* @param gameCopy a copy of the current game
* @return the move chosen by the AI
*/
protected long determineMove(T gameCopy) {
return ai.getMove(gameCopy);
}
/**
* Creates a deep copy of this AI player.
*
* @return a copy of this player
*/
@Override
public ArtificialPlayer<T> deepCopy() {
return new ArtificialPlayer<>(this);
}
}

View File

@@ -0,0 +1,87 @@
package org.toop.game.players;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.AbstractPlayer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
/**
* Represents a local player who provides moves manually.
*
* @param <T> the type of turn-based game
*/
public class LocalPlayer<T extends TurnBasedGame<T>> extends AbstractPlayer<T> {
private CompletableFuture<Long> LastMove = new CompletableFuture<>();
/**
* Creates a new local player with the given name.
*
* @param name the player's name
*/
public LocalPlayer(String name) {
super(name);
}
/**
* Creates a copy of another local player.
*
* @param other the player to copy
*/
public LocalPlayer(LocalPlayer<T> other) {
super(other);
this.LastMove = other.LastMove;
}
/**
* Waits for and returns the player's next legal move.
*
* @param gameCopy a copy of the current game
* @return the chosen move
*/
@Override
protected long determineMove(T gameCopy) {
long legalMoves = gameCopy.getLegalMoves();
long move;
do {
move = getLastMove();
} while ((legalMoves & move) == 0);
return move;
}
/**
* Sets the player's last move.
*
* @param move the move to set
*/
public void setLastMove(long move) {
LastMove.complete(move);
}
/**
* Waits for the next move from the player.
*
* @return the chosen move or 0 if interrupted
*/
private long getLastMove() {
LastMove = new CompletableFuture<>(); // Reset the future
try {
return LastMove.get();
} catch (ExecutionException | InterruptedException e) {
return 0;
}
}
/**
* Creates a deep copy of this local player.
*
* @return a copy of this player
*/
@Override
public LocalPlayer<T> deepCopy() {
return new LocalPlayer<>(this);
}
}

View File

@@ -0,0 +1,51 @@
package org.toop.game.players;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.AbstractPlayer;
import org.toop.framework.gameFramework.model.player.Player;
/**
* Represents a player that participates online.
*
* @param <T> the type of turn-based game
*/
public class OnlinePlayer<T extends TurnBasedGame<T>> extends AbstractPlayer<T> {
/**
* Creates a new online player with the given name.
*
* @param name the name of the player
*/
public OnlinePlayer(String name) {
super(name);
}
/**
* Creates a copy of another online player.
*
* @param other the player to copy
*/
public OnlinePlayer(OnlinePlayer<T> other) {
super(other);
}
/**
* {@inheritDoc}
* <p>
* This method is not supported for online players.
*
* @throws UnsupportedOperationException always
*/
@Override
protected long determineMove(T gameCopy) {
throw new UnsupportedOperationException("An online player does not support determining move");
}
/**
* {@inheritDoc}
*/
@Override
public Player<T> deepCopy() {
return new OnlinePlayer<>(this);
}
}

View File

@@ -1,4 +1,4 @@
package org.toop.framework.game.players; package org.toop.game.players.ai;
import org.toop.framework.gameFramework.GameState; import org.toop.framework.gameFramework.GameState;
import org.toop.framework.gameFramework.model.game.PlayResult; import org.toop.framework.gameFramework.model.game.PlayResult;
@@ -9,7 +9,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
public class MiniMaxAI extends AbstractAI { public class MiniMaxAI<T extends TurnBasedGame<T>> extends AbstractAI<T> {
private final int maxDepth; private final int maxDepth;
private final Random random = new Random(); private final Random random = new Random();
@@ -18,17 +18,17 @@ public class MiniMaxAI extends AbstractAI {
this.maxDepth = depth; this.maxDepth = depth;
} }
public MiniMaxAI(MiniMaxAI other) { public MiniMaxAI(MiniMaxAI<T> other) {
this.maxDepth = other.maxDepth; this.maxDepth = other.maxDepth;
} }
@Override @Override
public MiniMaxAI deepCopy() { public MiniMaxAI<T> deepCopy() {
return new MiniMaxAI(this); return new MiniMaxAI<>(this);
} }
@Override @Override
public long getMove(TurnBasedGame game) { public long getMove(T game) {
long legalMoves = game.getLegalMoves(); long legalMoves = game.getLegalMoves();
if (legalMoves == 0) return 0; if (legalMoves == 0) return 0;
@@ -39,7 +39,7 @@ public class MiniMaxAI extends AbstractAI {
long movesLoop = legalMoves; long movesLoop = legalMoves;
while (movesLoop != 0) { while (movesLoop != 0) {
long move = 1L << Long.numberOfTrailingZeros(movesLoop); long move = 1L << Long.numberOfTrailingZeros(movesLoop);
TurnBasedGame copy = game.deepCopy(); T copy = game.deepCopy();
PlayResult result = copy.play(move); PlayResult result = copy.play(move);
int score; int score;
@@ -75,7 +75,7 @@ public class MiniMaxAI extends AbstractAI {
* @param beta Beta value * @param beta Beta value
* @return score of the position * @return score of the position
*/ */
private int getMoveScore(TurnBasedGame game, int depth, boolean maximizing, int aiPlayer, int alpha, int beta) { private int getMoveScore(T game, int depth, boolean maximizing, int aiPlayer, int alpha, int beta) {
long legalMoves = game.getLegalMoves(); long legalMoves = game.getLegalMoves();
// Terminal state // Terminal state
@@ -95,7 +95,7 @@ public class MiniMaxAI extends AbstractAI {
while (movesLoop != 0) { while (movesLoop != 0) {
long move = 1L << Long.numberOfTrailingZeros(movesLoop); long move = 1L << Long.numberOfTrailingZeros(movesLoop);
TurnBasedGame copy = game.deepCopy(); T copy = game.deepCopy();
PlayResult result = copy.play(move); PlayResult result = copy.play(move);
int score; int score;
@@ -130,7 +130,7 @@ public class MiniMaxAI extends AbstractAI {
* @param aiPlayer AI's player index * @param aiPlayer AI's player index
* @return heuristic score * @return heuristic score
*/ */
private int evaluateBoard(TurnBasedGame game, int aiPlayer) { private int evaluateBoard(T game, int aiPlayer) {
long[] board = game.getBoard(); long[] board = game.getBoard();
int aiCount = 0; int aiCount = 0;
int opponentCount = 0; int opponentCount = 0;

View File

@@ -1,4 +1,4 @@
package org.toop.framework.game.players; package org.toop.game.players.ai;
import org.toop.framework.gameFramework.model.game.TurnBasedGame; import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.AbstractAI; import org.toop.framework.gameFramework.model.player.AbstractAI;
@@ -6,19 +6,19 @@ import org.toop.framework.gameFramework.model.player.AbstractAI;
import java.util.Random; import java.util.Random;
public class RandomAI extends AbstractAI { public class RandomAI<T extends TurnBasedGame<T>> extends AbstractAI<T> {
public RandomAI() { public RandomAI() {
super(); super();
} }
@Override @Override
public RandomAI deepCopy() { public RandomAI<T> deepCopy() {
return new RandomAI(); return new RandomAI<T>();
} }
@Override @Override
public long getMove(TurnBasedGame game) { public long getMove(T game) {
long legalMoves = game.getLegalMoves(); long legalMoves = game.getLegalMoves();
int move = new Random().nextInt(Long.bitCount(legalMoves)); int move = new Random().nextInt(Long.bitCount(legalMoves));
return nthBitIndex(legalMoves, move); return nthBitIndex(legalMoves, move);

View File

@@ -0,0 +1,80 @@
package org.toop.game.players.ai;
import org.deeplearning4j.nn.multilayer.MultiLayerNetwork;
import org.deeplearning4j.util.ModelSerializer;
import org.nd4j.linalg.api.ndarray.INDArray;
import org.nd4j.linalg.factory.Nd4j;
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
import org.toop.framework.gameFramework.model.player.AI;
import org.toop.framework.gameFramework.model.player.AbstractAI;
import org.toop.game.games.reversi.BitboardReversi;
import java.io.IOException;
import java.io.InputStream;
import static java.lang.Math.random;
public class ReversiAIML<T extends TurnBasedGame<T>> extends AbstractAI<T> {
MultiLayerNetwork model;
public ReversiAIML() {
InputStream is = getClass().getResourceAsStream("/reversi-model.zip");
try {
assert is != null;
model = ModelSerializer.restoreMultiLayerNetwork(is);
} catch (IOException e) {}
}
private int pickLegalMove(INDArray prediction, BitboardReversi reversi) {
double[] logits = prediction.toDoubleVector();
long legalMoves = reversi.getLegalMoves();
if (legalMoves == 0L) {
return -1;
}
if (Math.random() < 0.01) {
int randomIndex = (int) (Math.random() * Long.bitCount(legalMoves));
long moves = legalMoves;
for (int i = 0; i < randomIndex; i++) {
moves &= moves - 1;
}
return Long.numberOfTrailingZeros(moves);
}
int bestMove = -1;
double bestVal = Double.NEGATIVE_INFINITY;
long moves = legalMoves;
while (moves != 0L) {
int move = Long.numberOfTrailingZeros(moves);
double value = logits[move];
if (value > bestVal) {
bestVal = value;
bestMove = move;
}
moves &= moves - 1;
}
return bestMove;
}
@Override
public long getMove(T game) {
long[] input = game.getBoard();
INDArray boardInput = Nd4j.create(new long[][] { input });
INDArray prediction = model.output(boardInput);
int move = pickLegalMove(prediction,(BitboardReversi) game);
return move;
}
@Override
public ReversiAIML<T> deepCopy() {
return new ReversiAIML();
}
}

View File

@@ -0,0 +1,272 @@
/*//todo fix this mess
package org.toop.game.tictactoe;
import java.util.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.toop.framework.gameFramework.model.player.AbstractAI;
import org.toop.framework.gameFramework.model.player.Player;
import org.toop.game.AI;
import org.toop.game.enumerators.GameState;
import org.toop.game.games.reversi.ReversiAIR;
import org.toop.game.games.reversi.ReversiR;
import org.toop.game.records.Move;
import org.toop.game.reversi.Reversi;
import org.toop.game.reversi.ReversiAI;
import org.toop.game.players.ai.ReversiAIML;
import org.toop.game.games.reversi.ReversiAISimple;
import static org.junit.jupiter.api.Assertions.*;
class ReversiTest {
private ReversiR game;
private ReversiAIR ai;
private ReversiAIML aiml;
private ReversiAISimple aiSimple;
private AbstractAI<ReversiR> player1;
private AbstractAI<ReversiR> player2;
private Player[] players = new Player[2];
@BeforeEach
void setup() {
game = new ReversiR(players);
ai = new ReversiAIR();
aiml = new ReversiAIML();
aiSimple = new ReversiAISimple();
}
@Test
void testCorrectStartPiecesPlaced() {
assertNotNull(game);
assertEquals('W', game.getBoard()[27]);
assertEquals('B', game.getBoard()[28]);
assertEquals('B', game.getBoard()[35]);
assertEquals('W', game.getBoard()[36]);
}
@Test
void testGetLegalMovesAtStart() {
Move[] moves = game.getLegalMoves();
List<Move> expectedMoves = List.of(
new Move(19, 'B'),
new Move(26, 'B'),
new Move(37, 'B'),
new Move(44, 'B')
);
assertNotNull(moves);
assertTrue(moves.length > 0);
assertMovesMatchIgnoreOrder(expectedMoves, Arrays.asList(moves));
}
private void assertMovesMatchIgnoreOrder(List<Move> expected, List<Move> actual) {
assertEquals(expected.size(), actual.size());
for (int i = 0; i < expected.size(); i++) {
assertTrue(actual.contains(expected.get(i)));
assertTrue(expected.contains(actual.get(i)));
}
}
@Test
void testMakeValidMoveFlipsPieces() {
game.play(new Move(19, 'B'));
assertEquals('B', game.getBoard()[19]);
assertEquals('B', game.getBoard()[27], "Piece should have flipped to B");
}
@Test
void testMakeInvalidMoveDoesNothing() {
char[] before = game.getBoard().clone();
game.play(new Move(0, 'B'));
assertArrayEquals(before, game.getBoard(), "Board should not change on invalid move");
}
@Test
void testTurnSwitchesAfterValidMove() {
char current = game.getCurrentPlayer();
game.play(game.getLegalMoves()[0]);
assertNotEquals(current, game.getCurrentPlayer(), "Player turn should switch after a valid move");
}
@Test
void testCountScoreCorrectlyAtStart() {
long start = System.nanoTime();
Reversi.Score score = game.getScore();
assertEquals(2, score.player1Score()); // Black
assertEquals(2, score.player2Score()); // White
long end = System.nanoTime();
IO.println((end - start));
}
@Test
void zLegalMovesInCertainPosition() {
game.play(new Move(19, 'B'));
game.play(new Move(20, 'W'));
Move[] moves = game.getLegalMoves();
List<Move> expectedMoves = List.of(
new Move(13, 'B'),
new Move(21, 'B'),
new Move(29, 'B'),
new Move(37, 'B'),
new Move(45, 'B'));
assertNotNull(moves);
assertTrue(moves.length > 0);
IO.println(Arrays.toString(moves));
assertMovesMatchIgnoreOrder(expectedMoves, Arrays.asList(moves));
}
@Test
void testCountScoreCorrectlyAtEnd() {
for (int i = 0; i < 1; i++) {
game = new Reversi();
Move[] legalMoves = game.getLegalMoves();
while (legalMoves.length > 0) {
game.play(legalMoves[(int) (Math.random() * legalMoves.length)]);
legalMoves = game.getLegalMoves();
}
Reversi.Score score = game.getScore();
IO.println(score.player1Score());
IO.println(score.player2Score());
for (int r = 0; r < game.getRowSize(); r++) {
char[] row = Arrays.copyOfRange(game.getBoard(), r * game.getColumnSize(), (r + 1) * game.getColumnSize());
IO.println(Arrays.toString(row));
}
}
}
@Test
void testPlayerMustSkipTurnIfNoValidMoves() {
game.play(new Move(19, 'B'));
game.play(new Move(34, 'W'));
game.play(new Move(45, 'B'));
game.play(new Move(11, 'W'));
game.play(new Move(42, 'B'));
game.play(new Move(54, 'W'));
game.play(new Move(37, 'B'));
game.play(new Move(46, 'W'));
game.play(new Move(63, 'B'));
game.play(new Move(62, 'W'));
game.play(new Move(29, 'B'));
game.play(new Move(50, 'W'));
game.play(new Move(55, 'B'));
game.play(new Move(30, 'W'));
game.play(new Move(53, 'B'));
game.play(new Move(38, 'W'));
game.play(new Move(61, 'B'));
game.play(new Move(52, 'W'));
game.play(new Move(51, 'B'));
game.play(new Move(60, 'W'));
game.play(new Move(59, 'B'));
assertEquals('B', game.getCurrentPlayer());
game.play(ai.findBestMove(game, 5));
game.play(ai.findBestMove(game, 5));
}
@Test
void testGameShouldEndIfNoValidMoves() {
//European Grand Prix Ghent 2017: Replay Hassan - Verstuyft J. (3-17)
game.play(new Move(19, 'B'));
game.play(new Move(20, 'W'));
game.play(new Move(29, 'B'));
game.play(new Move(22, 'W'));
game.play(new Move(21, 'B'));
game.play(new Move(34, 'W'));
game.play(new Move(23, 'B'));
game.play(new Move(13, 'W'));
game.play(new Move(26, 'B'));
game.play(new Move(18, 'W'));
game.play(new Move(12, 'B'));
game.play(new Move(4, 'W'));
game.play(new Move(17, 'B'));
game.play(new Move(31, 'W'));
GameState stateTurn15 = game.play(new Move(39, 'B'));
assertEquals(GameState.NORMAL, stateTurn15);
GameState stateTurn16 = game.play(new Move(16, 'W'));
assertEquals(GameState.WIN, stateTurn16);
GameState stateTurn17 = game.play(new Move(5, 'B'));
assertNull(stateTurn17);
Reversi.Score score = game.getScore();
assertEquals(3, score.player1Score());
assertEquals(17, score.player2Score());
}
@Test
void testAISelectsLegalMove() {
Move move = ai.findBestMove(game, 4);
assertNotNull(move);
assertTrue(containsMove(game.getLegalMoves(), move), "AI should always choose a legal move");
}
private boolean containsMove(Move[] moves, Move move) {
for (Move m : moves) {
if (m.equals(move)) return true;
}
return false;
}
@Test
void testAis() {
player1 = aiml;
player2 = ai;
testAIvsAIML();
player2 = aiSimple;
testAIvsAIML();
player1 = ai;
testAIvsAIML();
player2 = aiml;
testAIvsAIML();
player1 = aiml;
testAIvsAIML();
player1 = aiSimple;
testAIvsAIML();
}
@Test
void testAIvsAIML() {
if(player1 == null || player2 == null) {
player1 = aiml;
player2 = ai;
}
int totalGames = 2000;
IO.println("Testing... " + player1.getClass().getSimpleName() + " vs " + player2.getClass().getSimpleName() + " for " + totalGames + " games");
int p1wins = 0;
int p2wins = 0;
int draws = 0;
List<Integer> moves = new ArrayList<>();
for (int i = 0; i < totalGames; i++) {
game = new ReversiR();
while (!game.isGameOver()) {
char curr = game.getCurrentPlayer();
Move move;
if (curr == 'B') {
move = player1.findBestMove(game, 5);
} else {
move = player2.findBestMove(game, 5);
}
if (i%500 == 0) moves.add(move.position());
game.play(move);
}
if (i%500 == 0) {
IO.println(moves);
moves.clear();
}
int winner = game.getWinner();
if (winner == 1) {
p1wins++;
} else if (winner == 2) {
p2wins++;
} else {
draws++;
}
}
IO.println("p1 winrate: " + p1wins + "/" + totalGames + " = " + (double) p1wins / totalGames + "\np2wins: " + p2wins + " draws: " + draws);
}
}
*/

View File

@@ -0,0 +1,78 @@
package org.toop.game.tictactoe;
import java.util.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.toop.framework.gameFramework.model.player.Player;
import org.toop.game.games.reversi.BitboardReversi;
import org.toop.game.players.ArtificialPlayer;
import org.toop.game.players.ai.MiniMaxAI;
import org.toop.game.players.ai.RandomAI;
import static org.junit.jupiter.api.Assertions.*;
public class TestReversi {
private BitboardReversi game;
private Player[] players;
@BeforeEach
void setup(){
players = new Player[2];
players[0] = new ArtificialPlayer<BitboardReversi>(new RandomAI<BitboardReversi>(),"randomAI");
players[1] = new ArtificialPlayer<BitboardReversi>(new MiniMaxAI<BitboardReversi>(10),"miniMaxAI");
game = new BitboardReversi(players);
}
@Test
void testCorrectStartPiecesPlaced() {
assertNotNull(game);
long[] board = game.getBoard();
IO.println(Long.toBinaryString(board[0]));
IO.println(Long.toBinaryString(board[1]));
long black = board[0];
long white = board[1];
assertEquals(1L, ((white >>> 27) & 1L)); //checks if the 27-shifted long has a 1 bit
assertEquals(1L, ((black >>> 28) & 1L));
assertEquals(1L, ((black >>> 35) & 1L));
assertEquals(1L, ((white >>> 36) & 1L));
}
@Test
void testPlayGames(){
int totalGames = 1;
long start = System.nanoTime();
long midtime = System.nanoTime();
int p1wins = 0;
int p2wins = 0;
int draws = 0;
for (int i = 0; i < totalGames; i++){
game = new BitboardReversi(players);
while(!game.isGameOver()){
midtime = System.nanoTime();
int currentTurn = game.getCurrentTurn();
long move = players[currentTurn].getMove(game.deepCopy());
game.play(move);
IO.println(System.nanoTime() - midtime);
}
switch (game.getWinner()){
case 0:
p1wins++;
break;
case 1:
p2wins++;
break;
case -1:
draws++;
break;
}
}
System.out.println(System.nanoTime() - start);
IO.println(p1wins + " " + p2wins + " " + draws);
assertEquals(totalGames, p1wins + p2wins + draws);
IO.println("p1 wr: " + p1wins + "/" + totalGames + " = " + (double) p1wins / totalGames);
}
}