diff --git a/app/src/main/java/org/toop/app/canvas/GameDrawer.java b/app/src/main/java/org/toop/app/canvas/GameDrawer.java new file mode 100644 index 0000000..261334c --- /dev/null +++ b/app/src/main/java/org/toop/app/canvas/GameDrawer.java @@ -0,0 +1,7 @@ +package org.toop.app.canvas; + +import org.toop.framework.gameFramework.model.game.TurnBasedGame; + +public interface GameDrawer> { + void redraw(T gameCopy); +} diff --git a/framework/src/main/java/org/toop/framework/gameFramework/controller/UpdatesGameUI.java b/framework/src/main/java/org/toop/framework/gameFramework/controller/UpdatesGameUI.java new file mode 100644 index 0000000..ce32bb7 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/gameFramework/controller/UpdatesGameUI.java @@ -0,0 +1,10 @@ +package org.toop.framework.gameFramework.controller; + +/** + * Interface for classes that can trigger a UI update. + */ +public interface UpdatesGameUI { + + /** Called to refresh or update the game UI. */ + void updateUI(); +} diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/game/BoardProvider.java b/framework/src/main/java/org/toop/framework/gameFramework/model/game/BoardProvider.java new file mode 100644 index 0000000..fb70cf8 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/game/BoardProvider.java @@ -0,0 +1,5 @@ +package org.toop.framework.gameFramework.model.game; + +public interface BoardProvider { + long[] getBoard(); +} diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/game/Playable.java b/framework/src/main/java/org/toop/framework/gameFramework/model/game/Playable.java new file mode 100644 index 0000000..21956b0 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/game/Playable.java @@ -0,0 +1,24 @@ +package org.toop.framework.gameFramework.model.game; + +import org.toop.framework.gameFramework.GameState; + +/** + * Interface for turn-based games that can be played and queried for legal moves. + */ +public interface Playable { + + /** + * Returns the moves that are currently valid in the game. + * + * @return an array of integers representing legal moves + */ + long getLegalMoves(); + + /** + * Plays the given move and returns the resulting game state. + * + * @param move the move to apply + * @return the {@link GameState} and additional info after the move + */ + PlayResult play(long move); +} diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/game/PlayerProvider.java b/framework/src/main/java/org/toop/framework/gameFramework/model/game/PlayerProvider.java new file mode 100644 index 0000000..8db47a3 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/game/PlayerProvider.java @@ -0,0 +1,7 @@ +package org.toop.framework.gameFramework.model.game; + +import org.toop.framework.gameFramework.model.player.Player; + +public interface PlayerProvider> { + Player getPlayer(int index); +} diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/game/SupportsOnlinePlay.java b/framework/src/main/java/org/toop/framework/gameFramework/model/game/SupportsOnlinePlay.java new file mode 100644 index 0000000..1cc1641 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/game/SupportsOnlinePlay.java @@ -0,0 +1,20 @@ +package org.toop.framework.gameFramework.model.game; + +import org.toop.framework.networking.events.NetworkEvents; + +/** + * Interface for games that support online multiplayer play. + *

+ * Methods are called in response to network events from the server. + */ +public interface SupportsOnlinePlay { + + /** Called when it is this player's turn to make a move. */ + void onYourTurn(long clientId); + + /** Called when a move from another player is received. */ + void onMoveReceived(long move); + + /** Called when the game has finished, with the final result. */ + void gameFinished(String condition); +} diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/game/threadBehaviour/Controllable.java b/framework/src/main/java/org/toop/framework/gameFramework/model/game/threadBehaviour/Controllable.java new file mode 100644 index 0000000..cefbbd4 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/game/threadBehaviour/Controllable.java @@ -0,0 +1,7 @@ +package org.toop.framework.gameFramework.model.game.threadBehaviour; + +public interface Controllable { + void start(); + + void stop(); +} diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/player/MoveProvider.java b/framework/src/main/java/org/toop/framework/gameFramework/model/player/MoveProvider.java new file mode 100644 index 0000000..b9ab448 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/player/MoveProvider.java @@ -0,0 +1,7 @@ +package org.toop.framework.gameFramework.model.player; + +import org.toop.framework.gameFramework.model.game.TurnBasedGame; + +public interface MoveProvider> { + long getMove(T game); +} diff --git a/framework/src/main/java/org/toop/framework/gameFramework/model/player/NameProvider.java b/framework/src/main/java/org/toop/framework/gameFramework/model/player/NameProvider.java new file mode 100644 index 0000000..850f0f2 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/gameFramework/model/player/NameProvider.java @@ -0,0 +1,5 @@ +package org.toop.framework.gameFramework.model.player; + +public interface NameProvider { + String getName(); +} diff --git a/framework/src/main/java/org/toop/framework/networking/NetworkingClientEventListener.java b/framework/src/main/java/org/toop/framework/networking/NetworkingClientEventListener.java new file mode 100644 index 0000000..5a9a08d --- /dev/null +++ b/framework/src/main/java/org/toop/framework/networking/NetworkingClientEventListener.java @@ -0,0 +1,152 @@ +package org.toop.framework.networking; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.toop.framework.SnowflakeGenerator; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.eventbus.bus.EventBus; +import org.toop.framework.networking.events.NetworkEvents; +import org.toop.framework.networking.exceptions.ClientNotFoundException; +import org.toop.framework.networking.interfaces.NetworkingClientManager; + +public class NetworkingClientEventListener { + private static final Logger logger = LogManager.getLogger(NetworkingClientEventListener.class); + + private final NetworkingClientManager clientManager; + + /** Starts a connection manager, to manage, connections. */ + public NetworkingClientEventListener(EventBus eventBus, NetworkingClientManager clientManager) { + this.clientManager = clientManager; + new EventFlow(eventBus) + .listen(NetworkEvents.StartClient.class, this::handleStartClient, false) + .listen(NetworkEvents.SendCommand.class, this::handleCommand, false) + .listen(NetworkEvents.SendLogin.class, this::handleSendLogin, false) + .listen(NetworkEvents.SendLogout.class, this::handleSendLogout, false) + .listen(NetworkEvents.SendGetPlayerlist.class, this::handleSendGetPlayerlist, false) + .listen(NetworkEvents.SendGetGamelist.class, this::handleSendGetGamelist, false) + .listen(NetworkEvents.SendSubscribe.class, this::handleSendSubscribe, false) + .listen(NetworkEvents.SendMove.class, this::handleSendMove, false) + .listen(NetworkEvents.SendChallenge.class, this::handleSendChallenge, false) + .listen(NetworkEvents.SendAcceptChallenge.class, this::handleSendAcceptChallenge, false) + .listen(NetworkEvents.SendForfeit.class, this::handleSendForfeit, false) + .listen(NetworkEvents.SendMessage.class, this::handleSendMessage, false) + .listen(NetworkEvents.SendHelp.class, this::handleSendHelp, false) + .listen(NetworkEvents.SendHelpForCommand.class, this::handleSendHelpForCommand, false) + .listen(NetworkEvents.CloseClient.class, this::handleCloseClient, false) + .listen(NetworkEvents.Reconnect.class, this::handleReconnect, false) + .listen(NetworkEvents.ChangeAddress.class, this::handleChangeAddress, false) + .listen(NetworkEvents.RequestsAllClients.class, this::handleGetAllConnections, false) + .listen(NetworkEvents.ForceCloseAllClients.class, this::handleShutdownAll, false); + } + + void handleStartClient(NetworkEvents.StartClient event) { + long clientId = SnowflakeGenerator.nextId(); + new EventFlow().addPostEvent(new NetworkEvents.CreatedIdForClient(clientId, event.identifier())).postEvent(); + clientManager.startClient( + clientId, + event.networkingClient(), + event.networkingConnector(), + () -> new EventFlow().addPostEvent(new NetworkEvents.StartClientResponse(clientId, true, event.identifier())).postEvent(), + () -> new EventFlow().addPostEvent(new NetworkEvents.StartClientResponse(clientId, false, event.identifier())).postEvent() + ); + } + + private void sendCommand(long clientId, String command) { + try { + clientManager.sendCommand(clientId, command); + } catch (ClientNotFoundException e) { + logger.error(e); + } + } + + private void handleCommand(NetworkEvents.SendCommand event) { + String args = String.join(" ", event.args()); + sendCommand(event.clientId(), args); + } + + private void handleSendLogin(NetworkEvents.SendLogin event) { + sendCommand(event.clientId(), String.format("LOGIN %s", event.username())); + } + + private void handleSendLogout(NetworkEvents.SendLogout event) { + sendCommand(event.clientId(), "LOGOUT"); + } + + private void handleSendGetPlayerlist(NetworkEvents.SendGetPlayerlist event) { + sendCommand(event.clientId(), "GET PLAYERLIST"); + } + + private void handleSendGetGamelist(NetworkEvents.SendGetGamelist event) { + sendCommand(event.clientId(), "GET GAMELIST"); + } + + private void handleSendSubscribe(NetworkEvents.SendSubscribe event) { + sendCommand(event.clientId(), String.format("SUBSCRIBE %s", event.gameType())); + } + + private void handleSendMove(NetworkEvents.SendMove event) { + sendCommand(event.clientId(), String.format("MOVE %d", event.moveNumber())); + } + + private void handleSendChallenge(NetworkEvents.SendChallenge event) { + sendCommand(event.clientId(), String.format("CHALLENGE %s %s", event.usernameToChallenge(), event.gameType())); + } + + private void handleSendAcceptChallenge(NetworkEvents.SendAcceptChallenge event) { + sendCommand(event.clientId(), String.format("CHALLENGE ACCEPT %d", event.challengeId())); + } + + private void handleSendForfeit(NetworkEvents.SendForfeit event) { + sendCommand(event.clientId(), "FORFEIT"); + } + + private void handleSendMessage(NetworkEvents.SendMessage event) { + sendCommand(event.clientId(), String.format("MESSAGE %s", event.message())); + } + + private void handleSendHelp(NetworkEvents.SendHelp event) { + sendCommand(event.clientId(), "HELP"); + } + + private void handleSendHelpForCommand(NetworkEvents.SendHelpForCommand event) { + sendCommand(event.clientId(), String.format("HELP %s", event.command())); + } + + private void handleReconnect(NetworkEvents.Reconnect event) { + clientManager.startClient( + event.clientId(), + event.networkingClient(), + event.networkingConnector(), + () -> new EventFlow().addPostEvent(new NetworkEvents.ReconnectResponse(true, event.identifier())).postEvent(), + () -> new EventFlow().addPostEvent(new NetworkEvents.ReconnectResponse(false, event.identifier())).postEvent() + ); + } + + private void handleChangeAddress(NetworkEvents.ChangeAddress event) { + clientManager.startClient( + event.clientId(), + event.networkingClient(), + event.networkingConnector(), + () -> new EventFlow().addPostEvent(new NetworkEvents.ChangeAddressResponse(true, event.identifier())).postEvent(), + () -> new EventFlow().addPostEvent(new NetworkEvents.ChangeAddressResponse(false, event.identifier())).postEvent() + ); + } + + void handleCloseClient(NetworkEvents.CloseClient event) { + try { + this.clientManager.closeClient(event.clientId()); + } catch (ClientNotFoundException e) { + logger.error(e); + } + } + + void handleGetAllConnections(NetworkEvents.RequestsAllClients request) { +// List a = new ArrayList<>(this.networkClients.values()); +// request.future().complete(a); + // TODO + } + + public void handleShutdownAll(NetworkEvents.ForceCloseAllClients request) { + // TODO + } +} diff --git a/framework/src/main/java/org/toop/framework/networking/connection/clients/TournamentNetworkingClient.java b/framework/src/main/java/org/toop/framework/networking/connection/clients/TournamentNetworkingClient.java index afaf48a..7d8a1d7 100644 --- a/framework/src/main/java/org/toop/framework/networking/connection/clients/TournamentNetworkingClient.java +++ b/framework/src/main/java/org/toop/framework/networking/connection/clients/TournamentNetworkingClient.java @@ -1,4 +1,8 @@ +<<<<<<<< HEAD:framework/src/main/java/org/toop/framework/networking/connection/clients/TournamentNetworkingClient.java package org.toop.framework.networking.connection.clients; +======== +package org.toop.framework.networking.clients; +>>>>>>>> refs/remotes/origin/main:framework/src/main/java/org/toop/framework/networking/clients/TournamentNetworkingClient.java import io.netty.bootstrap.Bootstrap; import io.netty.channel.*; @@ -12,10 +16,16 @@ import io.netty.util.CharsetUtil; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.toop.framework.eventbus.bus.EventBus; +<<<<<<<< HEAD:framework/src/main/java/org/toop/framework/networking/connection/clients/TournamentNetworkingClient.java import org.toop.framework.networking.connection.events.NetworkEvents; import org.toop.framework.networking.connection.exceptions.CouldNotConnectException; import org.toop.framework.networking.connection.handlers.NetworkingGameClientHandler; import org.toop.framework.networking.connection.interfaces.NetworkingClient; +======== +import org.toop.framework.networking.exceptions.CouldNotConnectException; +import org.toop.framework.networking.handlers.NetworkingGameClientHandler; +import org.toop.framework.networking.interfaces.NetworkingClient; +>>>>>>>> refs/remotes/origin/main:framework/src/main/java/org/toop/framework/networking/clients/TournamentNetworkingClient.java import java.net.InetSocketAddress; @@ -24,7 +34,10 @@ public class TournamentNetworkingClient implements NetworkingClient { private final EventBus eventBus; private Channel channel; +<<<<<<<< HEAD:framework/src/main/java/org/toop/framework/networking/connection/clients/TournamentNetworkingClient.java private long clientId; +======== +>>>>>>>> refs/remotes/origin/main:framework/src/main/java/org/toop/framework/networking/clients/TournamentNetworkingClient.java public TournamentNetworkingClient(EventBus eventBus) { this.eventBus = eventBus; @@ -37,7 +50,10 @@ public class TournamentNetworkingClient implements NetworkingClient { @Override public void connect(long clientId, String host, int port) throws CouldNotConnectException { +<<<<<<<< HEAD:framework/src/main/java/org/toop/framework/networking/connection/clients/TournamentNetworkingClient.java this.clientId = clientId; +======== +>>>>>>>> refs/remotes/origin/main:framework/src/main/java/org/toop/framework/networking/clients/TournamentNetworkingClient.java try { Bootstrap bootstrap = new Bootstrap(); EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory()); @@ -57,6 +73,7 @@ public class TournamentNetworkingClient implements NetworkingClient { pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8)); pipeline.addLast(handler); } + }); ChannelFuture channelFuture = bootstrap.connect(host, port).sync(); this.channel = channelFuture.channel(); @@ -78,7 +95,10 @@ public class TournamentNetworkingClient implements NetworkingClient { logger.info("Connection {} sent message: '{}' ", this.channel.remoteAddress(), literalMsg); } else { logger.warn("Cannot send message: '{}', connection inactive. ", literalMsg); +<<<<<<<< HEAD:framework/src/main/java/org/toop/framework/networking/connection/clients/TournamentNetworkingClient.java eventBus.post(new NetworkEvents.ClosedConnection(clientId)); +======== +>>>>>>>> refs/remotes/origin/main:framework/src/main/java/org/toop/framework/networking/clients/TournamentNetworkingClient.java } } diff --git a/framework/src/main/java/org/toop/framework/networking/connection/exceptions/NetworkingInitializationException.java b/framework/src/main/java/org/toop/framework/networking/connection/exceptions/NetworkingInitializationException.java index 3b2582f..9a88c6d 100644 --- a/framework/src/main/java/org/toop/framework/networking/connection/exceptions/NetworkingInitializationException.java +++ b/framework/src/main/java/org/toop/framework/networking/connection/exceptions/NetworkingInitializationException.java @@ -1,4 +1,8 @@ +<<<<<<<< HEAD:framework/src/main/java/org/toop/framework/networking/connection/exceptions/NetworkingInitializationException.java package org.toop.framework.networking.connection.exceptions; +======== +package org.toop.framework.networking.exceptions; +>>>>>>>> refs/remotes/origin/main:framework/src/main/java/org/toop/framework/networking/exceptions/NetworkingInitializationException.java public class NetworkingInitializationException extends RuntimeException { public NetworkingInitializationException(String message, Throwable cause) { diff --git a/framework/src/main/java/org/toop/framework/networking/connection/handlers/NetworkingGameClientHandler.java b/framework/src/main/java/org/toop/framework/networking/connection/handlers/NetworkingGameClientHandler.java index a313d97..f3728f0 100644 --- a/framework/src/main/java/org/toop/framework/networking/connection/handlers/NetworkingGameClientHandler.java +++ b/framework/src/main/java/org/toop/framework/networking/connection/handlers/NetworkingGameClientHandler.java @@ -1,4 +1,8 @@ +<<<<<<<< HEAD:framework/src/main/java/org/toop/framework/networking/connection/handlers/NetworkingGameClientHandler.java package org.toop.framework.networking.connection.handlers; +======== +package org.toop.framework.networking.handlers; +>>>>>>>> refs/remotes/origin/main:framework/src/main/java/org/toop/framework/networking/handlers/NetworkingGameClientHandler.java import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; @@ -9,7 +13,11 @@ import java.util.regex.Pattern; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.toop.framework.eventbus.bus.EventBus; +<<<<<<<< HEAD:framework/src/main/java/org/toop/framework/networking/connection/handlers/NetworkingGameClientHandler.java import org.toop.framework.networking.connection.events.NetworkEvents; +======== +import org.toop.framework.networking.events.NetworkEvents; +>>>>>>>> refs/remotes/origin/main:framework/src/main/java/org/toop/framework/networking/handlers/NetworkingGameClientHandler.java public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter { private static final Logger logger = LogManager.getLogger(NetworkingGameClientHandler.class); diff --git a/framework/src/main/java/org/toop/framework/networking/exceptions/ClientNotFoundException.java b/framework/src/main/java/org/toop/framework/networking/exceptions/ClientNotFoundException.java new file mode 100644 index 0000000..2506b26 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/networking/exceptions/ClientNotFoundException.java @@ -0,0 +1,25 @@ +package org.toop.framework.networking.exceptions; + +/** + * Thrown when an operation is attempted on a networking client + * that does not exist or has already been closed. + */ +public class ClientNotFoundException extends RuntimeException { + + private final long clientId; + + public ClientNotFoundException(long clientId) { + super("Networking client with ID " + clientId + " was not found."); + this.clientId = clientId; + } + + public ClientNotFoundException(long clientId, Throwable cause) { + super("Networking client with ID " + clientId + " was not found.", cause); + this.clientId = clientId; + } + + public long getClientId() { + return clientId; + } + +} \ No newline at end of file diff --git a/framework/src/main/java/org/toop/framework/networking/exceptions/CouldNotConnectException.java b/framework/src/main/java/org/toop/framework/networking/exceptions/CouldNotConnectException.java new file mode 100644 index 0000000..839fb0b --- /dev/null +++ b/framework/src/main/java/org/toop/framework/networking/exceptions/CouldNotConnectException.java @@ -0,0 +1,21 @@ +package org.toop.framework.networking.exceptions; + +public class CouldNotConnectException extends RuntimeException { + + private final long clientId; + + public CouldNotConnectException(long clientId) { + super("Networking client with ID " + clientId + " could not connect."); + this.clientId = clientId; + } + + public CouldNotConnectException(long clientId, Throwable cause) { + super("Networking client with ID " + clientId + " could not connect.", cause); + this.clientId = clientId; + } + + public long getClientId() { + return clientId; + } + +} \ No newline at end of file diff --git a/framework/src/main/java/org/toop/framework/networking/interfaces/NetworkingClient.java b/framework/src/main/java/org/toop/framework/networking/interfaces/NetworkingClient.java new file mode 100644 index 0000000..09b215c --- /dev/null +++ b/framework/src/main/java/org/toop/framework/networking/interfaces/NetworkingClient.java @@ -0,0 +1,13 @@ +package org.toop.framework.networking.interfaces; + +import org.toop.framework.networking.exceptions.CouldNotConnectException; + +import java.net.InetSocketAddress; + +public interface NetworkingClient { + InetSocketAddress getAddress(); + void connect(long clientId, String host, int port) throws CouldNotConnectException; + boolean isActive(); + void writeAndFlush(String msg); + void closeConnection(); +} diff --git a/framework/src/main/java/org/toop/framework/networking/interfaces/NetworkingClientManager.java b/framework/src/main/java/org/toop/framework/networking/interfaces/NetworkingClientManager.java new file mode 100644 index 0000000..c236080 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/networking/interfaces/NetworkingClientManager.java @@ -0,0 +1,17 @@ +package org.toop.framework.networking.interfaces; + +import org.toop.framework.networking.exceptions.ClientNotFoundException; +import org.toop.framework.networking.exceptions.CouldNotConnectException; +import org.toop.framework.networking.types.NetworkingConnector; + +public interface NetworkingClientManager { + void startClient( + long id, + NetworkingClient nClient, + NetworkingConnector nConnector, + Runnable onSuccess, + Runnable onFailure + ) throws CouldNotConnectException; + void sendCommand(long id, String command) throws ClientNotFoundException; + void closeClient(long id) throws ClientNotFoundException; +} diff --git a/framework/src/main/java/org/toop/framework/networking/types/NetworkingConnector.java b/framework/src/main/java/org/toop/framework/networking/types/NetworkingConnector.java new file mode 100644 index 0000000..ee6ed44 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/networking/types/NetworkingConnector.java @@ -0,0 +1,5 @@ +package org.toop.framework.networking.types; + +import java.util.concurrent.TimeUnit; + +public record NetworkingConnector(String host, int port, int reconnectAttempts, long timeout, TimeUnit timeUnit) {} \ No newline at end of file diff --git a/framework/src/main/java/org/toop/framework/networking/types/ServerCommand.java b/framework/src/main/java/org/toop/framework/networking/types/ServerCommand.java new file mode 100644 index 0000000..e11bb61 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/networking/types/ServerCommand.java @@ -0,0 +1,3 @@ +package org.toop.framework.networking.types; + +public record ServerCommand(long clientId, String command) {} diff --git a/framework/src/main/java/org/toop/framework/networking/types/ServerMessage.java b/framework/src/main/java/org/toop/framework/networking/types/ServerMessage.java new file mode 100644 index 0000000..606607d --- /dev/null +++ b/framework/src/main/java/org/toop/framework/networking/types/ServerMessage.java @@ -0,0 +1,3 @@ +package org.toop.framework.networking.types; + +public record ServerMessage(String message) {} diff --git a/game/src/main/java/org/toop/game/BitboardGame.java b/game/src/main/java/org/toop/game/BitboardGame.java new file mode 100644 index 0000000..4ebdb95 --- /dev/null +++ b/game/src/main/java/org/toop/game/BitboardGame.java @@ -0,0 +1,86 @@ +package org.toop.game; + +import org.toop.framework.gameFramework.model.game.TurnBasedGame; +import org.toop.framework.gameFramework.model.player.Player; + +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 +public abstract class BitboardGame> implements TurnBasedGame { + private final int columnSize; + private final int rowSize; + + private Player[] players; + + // long is 64 bits. Every game has a limit of 64 cells maximum. + private final long[] playerBitboard; + private int currentTurn = 0; + + public BitboardGame(int columnSize, int rowSize, int playerCount, Player[] players) { + this.columnSize = columnSize; + this.rowSize = rowSize; + this.players = players; + this.playerBitboard = new long[playerCount]; + + Arrays.fill(playerBitboard, 0L); + } + + public BitboardGame(BitboardGame other) { + this.columnSize = other.columnSize; + this.rowSize = other.rowSize; + + this.playerBitboard = other.playerBitboard.clone(); + this.currentTurn = other.currentTurn; + this.players = Arrays.stream(other.players) + .map(Player::deepCopy) + .toArray(Player[]::new); + } + + public int getColumnSize() { + return this.columnSize; + } + + public int getRowSize() { + return this.rowSize; + } + + public long getPlayerBitboard(int player) { + return this.playerBitboard[player]; + } + + public void setPlayerBitboard(int player, long bitboard) { + this.playerBitboard[player] = bitboard; + } + + public int getPlayerCount() { + return playerBitboard.length; + } + + public int getCurrentTurn() { + return getCurrentPlayerIndex(); + } + + public Player getPlayer(int index) {return players[index];} + + public int getCurrentPlayerIndex() { + return currentTurn % playerBitboard.length; + } + + public int getNextPlayer() { + return (currentTurn + 1) % playerBitboard.length; + } + + public Player getCurrentPlayer(){ + return players[getCurrentPlayerIndex()]; + } + + + + @Override + public long[] getBoard() {return this.playerBitboard;} + + public void nextTurn() { + currentTurn++; + } +} \ No newline at end of file diff --git a/game/src/main/java/org/toop/game/Move.java b/game/src/main/java/org/toop/game/Move.java new file mode 100644 index 0000000..62a2b1a --- /dev/null +++ b/game/src/main/java/org/toop/game/Move.java @@ -0,0 +1,3 @@ +package org.toop.game; +// TODO: Remove this, only used in ReversiCanvas. Needs to not +public record Move(int position, char value) {} diff --git a/game/src/main/java/org/toop/game/gameThreads/LocalFixedRateThreadBehaviour.java b/game/src/main/java/org/toop/game/gameThreads/LocalFixedRateThreadBehaviour.java new file mode 100644 index 0000000..0a9da7f --- /dev/null +++ b/game/src/main/java/org/toop/game/gameThreads/LocalFixedRateThreadBehaviour.java @@ -0,0 +1,88 @@ +package org.toop.game.gameThreads; + +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.gameFramework.GameState; +import org.toop.framework.gameFramework.model.game.PlayResult; +import org.toop.framework.gameFramework.model.game.threadBehaviour.AbstractThreadBehaviour; +import org.toop.framework.gameFramework.view.GUIEvents; +import org.toop.framework.gameFramework.model.game.TurnBasedGame; +import org.toop.framework.gameFramework.model.player.Player; + +import java.util.function.Consumer; + +/** + * Handles local turn-based game logic at a fixed update rate. + *

+ * 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. + */ +public class LocalFixedRateThreadBehaviour> extends AbstractThreadBehaviour implements Runnable { + + + /** + * Creates a fixed-rate behaviour for a local turn-based game. + * + * @param game the game instance + */ + public LocalFixedRateThreadBehaviour(T game) { + super(game); + } + + /** Starts the game loop thread if not already running. */ + @Override + public void start() { + if (isRunning.compareAndSet(false, true)) { + new Thread(this).start(); + } + } + + /** Stops the game loop after the current iteration. */ + @Override + public void stop() { + isRunning.set(false); + } + + /** + * Main loop running at a fixed rate. + *

+ * Fetches the current player's move, applies it to the game, + * updates the UI, and handles game-ending states. + */ + @Override + public void run() { + final int UPS = 1; + final long UPDATE_INTERVAL = 1_000_000_000L / UPS; + long nextUpdate = System.nanoTime(); + + while (isRunning.get()) { + long now = System.nanoTime(); + if (now >= nextUpdate) { + nextUpdate += UPDATE_INTERVAL; + + Player currentPlayer = game.getPlayer(game.getCurrentTurn()); + long move = currentPlayer.getMove(game.deepCopy()); + PlayResult result = game.play(move); + + updateUI(); + + GameState state = result.state(); + switch (state) { + case WIN, DRAW -> { + isRunning.set(false); + new EventFlow().addPostEvent(GUIEvents.GameEnded.class, state == GameState.WIN, result.player()).postEvent(); + } + case NORMAL, TURN_SKIPPED -> { /* continue */ } + default -> { + logger.error("Unexpected state {}", state); + isRunning.set(false); + throw new RuntimeException("Unknown state: " + state); + } + } + } else { + try { + Thread.sleep(10); + } catch (InterruptedException ignored) {} + } + } + } +} diff --git a/game/src/main/java/org/toop/game/gameThreads/LocalThreadBehaviour.java b/game/src/main/java/org/toop/game/gameThreads/LocalThreadBehaviour.java new file mode 100644 index 0000000..79c57f9 --- /dev/null +++ b/game/src/main/java/org/toop/game/gameThreads/LocalThreadBehaviour.java @@ -0,0 +1,76 @@ +package org.toop.game.gameThreads; + +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.gameFramework.model.game.threadBehaviour.AbstractThreadBehaviour; +import org.toop.framework.gameFramework.view.GUIEvents; +import org.toop.framework.gameFramework.model.game.PlayResult; +import org.toop.framework.gameFramework.GameState; +import org.toop.framework.gameFramework.model.game.TurnBasedGame; +import org.toop.framework.gameFramework.model.player.Player; + +import java.util.function.Consumer; + +/** + * Handles local turn-based game logic in its own thread. + *

+ * 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. + */ +public class LocalThreadBehaviour> extends AbstractThreadBehaviour implements Runnable { + + /** + * Creates a new behaviour for a local turn-based game. + * + * @param game the game instance + */ + public LocalThreadBehaviour(T game) { + super(game); + } + + /** Starts the game loop in a new thread. */ + @Override + public void start() { + if (isRunning.compareAndSet(false, true)) { + new Thread(this).start(); + } + } + + /** Stops the game loop after the current iteration. */ + @Override + public void stop() { + isRunning.set(false); + } + + /** + * Main game loop: gets the current player's move, applies it, + * updates the UI, and handles end-of-game states. + */ + @Override + public void run() { + while (isRunning.get()) { + Player currentPlayer = game.getPlayer(game.getCurrentTurn()); + long move = currentPlayer.getMove(game.deepCopy()); + PlayResult result = game.play(move); + + updateUI(); + + GameState state = result.state(); + switch (state) { + case WIN, DRAW -> { + isRunning.set(false); + new EventFlow().addPostEvent( + GUIEvents.GameEnded.class, + state == GameState.WIN, + result.player() + ).postEvent(); + } + case NORMAL, TURN_SKIPPED -> { /* continue normally */ } + default -> { + logger.error("Unexpected state {}", state); + isRunning.set(false); + throw new RuntimeException("Unknown state: " + state); + } + } + } + } +} diff --git a/game/src/main/java/org/toop/game/gameThreads/OnlineThreadBehaviour.java b/game/src/main/java/org/toop/game/gameThreads/OnlineThreadBehaviour.java new file mode 100644 index 0000000..ae9aa88 --- /dev/null +++ b/game/src/main/java/org/toop/game/gameThreads/OnlineThreadBehaviour.java @@ -0,0 +1,84 @@ +package org.toop.game.gameThreads; + +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.gameFramework.model.game.threadBehaviour.AbstractThreadBehaviour; +import org.toop.framework.gameFramework.view.GUIEvents; +import org.toop.framework.gameFramework.model.game.TurnBasedGame; +import org.toop.framework.gameFramework.model.game.SupportsOnlinePlay; +import org.toop.framework.gameFramework.model.player.Player; +import org.toop.game.players.OnlinePlayer; + +/** + * Handles online multiplayer game logic. + *

+ * Reacts to server events, sending moves and updating the game state + * for the local player while receiving moves from other players. + */ +public class OnlineThreadBehaviour> extends AbstractThreadBehaviour implements SupportsOnlinePlay { + /** + * Creates behaviour and sets the first local player + * (non-online player) from the given array. + */ + public OnlineThreadBehaviour(T game) { + super(game); + } + + /** Finds the first non-online player in the array. */ + private int getFirstNotOnlinePlayer(Player[] players) { + for (int i = 0; i < players.length; i++) { + if (!(players[i] instanceof OnlinePlayer)) { + return i; + } + } + throw new RuntimeException("All players are online players"); + } + + /** Starts processing network events for the local player. */ + @Override + public void start() { + isRunning.set(true); + } + + /** Stops processing network events. */ + @Override + public void stop() { + isRunning.set(false); + } + + /** + * Called when the server notifies that it is the local player's turn. + * Sends the generated move back to the server. + */ + @Override + public void onYourTurn(long clientId) { + if (!isRunning.get()) return; + long move = game.getPlayer(game.getCurrentTurn()).getMove(game.deepCopy()); + sendMove(clientId, move); + } + + /** + * Handles a move received from the server for any player. + * Updates the game state and triggers a UI refresh. + */ + public void onMoveReceived(long move) { + if (!isRunning.get()) return; + game.play(move); + + updateUI(); + } + + /** + * Handles the end of the game as notified by the server. + * Updates the UI to show a win or draw result for the local player. + */ + public void gameFinished(String condition) { + switch(condition.toUpperCase()){ + case "WIN", "LOSS" -> new EventFlow().addPostEvent(GUIEvents.GameEnded.class, true, game.getWinner()).postEvent(); + case "DRAW" -> new EventFlow().addPostEvent(GUIEvents.GameEnded.class, false, -1).postEvent(); + default -> { + logger.error("Invalid condition"); + throw new RuntimeException("Unknown condition"); + } + } + } +} diff --git a/game/src/main/java/org/toop/game/gameThreads/OnlineWithSleepThreadBehaviour.java b/game/src/main/java/org/toop/game/gameThreads/OnlineWithSleepThreadBehaviour.java new file mode 100644 index 0000000..a666f8d --- /dev/null +++ b/game/src/main/java/org/toop/game/gameThreads/OnlineWithSleepThreadBehaviour.java @@ -0,0 +1,40 @@ +package org.toop.game.gameThreads; + +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 + * the local player's turn. + *

+ * This is identical to {@link OnlineThreadBehaviour}, but inserts a + * short sleep before delegating to the base implementation. + */ +public class OnlineWithSleepThreadBehaviour> extends OnlineThreadBehaviour { + + /** + * Creates the behaviour and forwards the players to the base class. + * + * @param game the online-capable turn-based game + */ + public OnlineWithSleepThreadBehaviour(T game) { + super(game); + } + + /** + * Waits briefly before handling the "your turn" event. + * + * @param event the network event indicating it's this client's turn + */ + @Override + public void onYourTurn(long clientId) { + + try { + Thread.sleep(50); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + super.onYourTurn(clientId); + } +} diff --git a/game/src/main/java/org/toop/game/games/reversi/BitboardReversi.java b/game/src/main/java/org/toop/game/games/reversi/BitboardReversi.java new file mode 100644 index 0000000..f380bef --- /dev/null +++ b/game/src/main/java/org/toop/game/games/reversi/BitboardReversi.java @@ -0,0 +1,170 @@ +package org.toop.game.games.reversi; + +import org.toop.framework.gameFramework.GameState; +import org.toop.framework.gameFramework.model.game.PlayResult; +import org.toop.framework.gameFramework.model.player.Player; +import org.toop.game.BitboardGame; + +public class BitboardReversi extends BitboardGame { + + public record Score(int black, int white) {} + + private final long notAFile = 0xfefefefefefefefeL; + private final long notHFile = 0x7f7f7f7f7f7f7f7fL; + + public BitboardReversi(Player[] players) { + super(8, 8, 2, players); + + // Black (player 0) + setPlayerBitboard(0, (1L << (3 + 4 * 8)) | (1L << (4 + 3 * 8))); + + // White (player 1) + setPlayerBitboard(1, (1L << (3 + 3 * 8)) | (1L << (4 + 4 * 8))); + } + + public BitboardReversi(BitboardReversi other) { + super(other); + } + + public long getLegalMoves() { + final long player = getPlayerBitboard(getCurrentPlayerIndex()); + final long opponent = getPlayerBitboard(getNextPlayer()); + + long legalMoves = 0L; + + // north & south + legalMoves |= computeMoves(player, opponent, 8, -1L); + legalMoves |= computeMoves(player, opponent, -8, -1L); + + // east & west + legalMoves |= computeMoves(player, opponent, 1, notAFile); + legalMoves |= computeMoves(player, opponent, -1, notHFile); + + // north-east & north-west & south-east & south-west + legalMoves |= computeMoves(player, opponent, 9, notAFile); + legalMoves |= computeMoves(player, opponent, 7, notHFile); + legalMoves |= computeMoves(player, opponent, -7, notAFile); + legalMoves |= computeMoves(player, opponent, -9, notHFile); + + return legalMoves; + } + + public long getFlips(long move) { + final long player = getPlayerBitboard(getCurrentPlayerIndex()); + final long opponent = getPlayerBitboard(getNextPlayer()); + + long flips = 0L; + + // north & south + flips |= computeFlips(move, player, opponent, 8, -1L); + flips |= computeFlips(move, player, opponent, -8, -1L); + + // east & west + flips |= computeFlips(move, player, opponent, 1, notAFile); + flips |= computeFlips(move, player, opponent, -1, notHFile); + + // north-east & north-west & south-east & south-west + flips |= computeFlips(move, player, opponent, 9, notAFile); + flips |= computeFlips(move, player, opponent, 7, notHFile); + flips |= computeFlips(move, player, opponent, -7, notAFile); + flips |= computeFlips(move, player, opponent, -9, notHFile); + + return flips; + } + + @Override + public BitboardReversi deepCopy() {return new BitboardReversi(this);} + + public PlayResult play(long move) { + final long flips = getFlips(move); + + long player = getPlayerBitboard(getCurrentPlayerIndex()); + long opponent = getPlayerBitboard(getNextPlayer()); + + player |= move | flips; + opponent &= ~flips; + + setPlayerBitboard(getCurrentPlayerIndex(), player); + setPlayerBitboard(getNextPlayer(), opponent); + + nextTurn(); + + final long nextLegalMoves = getLegalMoves(); + + if (nextLegalMoves == 0) { + nextTurn(); + + final long skippedLegalMoves = getLegalMoves(); + + if (skippedLegalMoves == 0) { + int winner = getWinner(); + + if (winner == -1) { + return new PlayResult(GameState.DRAW, -1); + } + + return new PlayResult(GameState.WIN, winner); + } + + return new PlayResult(GameState.TURN_SKIPPED, getCurrentPlayerIndex()); + } + + return new PlayResult(GameState.NORMAL, getCurrentPlayerIndex()); + } + + public Score getScore() { + return new Score( + Long.bitCount(getPlayerBitboard(0)), + Long.bitCount(getPlayerBitboard(1)) + ); + } + + public int getWinner(){ + final long black = getPlayerBitboard(0); + final long white = getPlayerBitboard(1); + + final int blackCount = Long.bitCount(black); + final int whiteCount = Long.bitCount(white); + + if (blackCount == whiteCount){ + return -1; + } + else if (blackCount > whiteCount){ + return 0; + } + else{ + return 1; + } + } + + private long computeMoves(long player, long opponent, int shift, long mask) { + long moves = shift(player, shift, mask) & opponent; + long captured = moves; + + while (moves != 0) { + moves = shift(moves, shift, mask) & opponent; + captured |= moves; + } + + long landing = shift(captured, shift, mask); + return landing & ~(player | opponent); + } + + private long computeFlips(long move, long player, long opponent, int shift, long mask) { + long flips = 0L; + long pos = move; + + while (true) { + pos = shift(pos, shift, mask); + if (pos == 0) return 0L; + + if ((pos & opponent) != 0) flips |= pos; + else if ((pos & player) != 0) return flips; + else return 0L; + } + } + + private long shift(long bit, int shift, long mask) { + return shift > 0 ? (bit << shift) & mask : (bit >>> -shift) & mask; + } +} \ No newline at end of file diff --git a/game/src/main/java/org/toop/game/games/tictactoe/BitboardTicTacToe.java b/game/src/main/java/org/toop/game/games/tictactoe/BitboardTicTacToe.java new file mode 100644 index 0000000..0927431 --- /dev/null +++ b/game/src/main/java/org/toop/game/games/tictactoe/BitboardTicTacToe.java @@ -0,0 +1,103 @@ +package org.toop.game.games.tictactoe; + +import org.toop.framework.gameFramework.GameState; +import org.toop.framework.gameFramework.model.game.PlayResult; +import org.toop.framework.gameFramework.model.player.Player; +import org.toop.game.BitboardGame; + +public class BitboardTicTacToe extends BitboardGame { + private final long[] winningLines = { + 0b111000000L, // top row + 0b000111000L, // middle row + 0b000000111L, // bottom row + 0b100100100L, // left column + 0b010010010L, // middle column + 0b001001001L, // right column + 0b100010001L, // diagonal + 0b001010100L // anti-diagonal + }; + + public BitboardTicTacToe(Player[] players) { + super(3, 3, 2, players); + } + public BitboardTicTacToe(BitboardTicTacToe other) { + super(other); + } + + public long getLegalMoves() { + final long xBitboard = getPlayerBitboard(0); + final long oBitboard = getPlayerBitboard(1); + + final long taken = (xBitboard | oBitboard); + return (~taken) & 0x1ffL; + } + + public int getWinner(){ + return getCurrentPlayerIndex(); + } + + public PlayResult play(long move) { + // Player loses if move is invalid + if ((move & getLegalMoves()) == 0 || Long.bitCount(move) != 1){ + return new PlayResult(GameState.WIN, getNextPlayer()); + } + + // Move is legal, make move + long playerBitboard = getPlayerBitboard(getCurrentPlayerIndex()); + playerBitboard |= move; + + setPlayerBitboard(getCurrentPlayerIndex(), playerBitboard); + + // Check if current player won + if (checkWin(playerBitboard)) { + return new PlayResult(GameState.WIN, getCurrentPlayerIndex()); + } + + // Proceed to next turn + nextTurn(); + + + // Check for early draw + if (getLegalMoves() == 0L || checkEarlyDraw()) { + return new PlayResult(GameState.DRAW, -1); + } + + // Nothing weird happened, continue on as normal + return new PlayResult(GameState.NORMAL, -1); + } + + private boolean checkWin(long board) { + for (final long line : winningLines) { + if ((board & line) == line) { + return true; + } + } + + return false; + } + + private boolean checkEarlyDraw() { + final long xBitboard = getPlayerBitboard(0); + final long oBitboard = getPlayerBitboard(1); + + final long taken = (xBitboard | oBitboard); + final long empty = (~taken) & 0x1FFL; + + for (final long line : winningLines) { + if (((line & xBitboard) != 0 && (line & oBitboard) != 0)) { + continue; + } + + if ((line & empty) != 0) { + return false; + } + } + + return true; + } + + @Override + public BitboardTicTacToe deepCopy() { + return new BitboardTicTacToe(this); + } +} \ No newline at end of file diff --git a/game/src/main/java/org/toop/game/players/ArtificialPlayer.java b/game/src/main/java/org/toop/game/players/ArtificialPlayer.java new file mode 100644 index 0000000..418cbed --- /dev/null +++ b/game/src/main/java/org/toop/game/players/ArtificialPlayer.java @@ -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 in a game. + *

+ * 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. + *

+ * + * @param 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. + *

+ * 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}. + *

+ * + * @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(T gameCopy) { + return ai.getMove(gameCopy); + } + + @Override + public ArtificialPlayer deepCopy() { + return new ArtificialPlayer(this); + } +} diff --git a/game/src/main/java/org/toop/game/players/LocalPlayer.java b/game/src/main/java/org/toop/game/players/LocalPlayer.java new file mode 100644 index 0000000..8f3b94d --- /dev/null +++ b/game/src/main/java/org/toop/game/players/LocalPlayer.java @@ -0,0 +1,86 @@ +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; + +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 LastMove = new CompletableFuture<>(); + + private CompletableFuture LastMove; + + public LocalPlayer(String name) { + super(name); + } + + public LocalPlayer(LocalPlayer other) { + super(other); + } + + @Override + public long getMove(T 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(T 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(T 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; + }*/ +} diff --git a/game/src/main/java/org/toop/game/players/MiniMaxAI.java b/game/src/main/java/org/toop/game/players/MiniMaxAI.java new file mode 100644 index 0000000..440bb50 --- /dev/null +++ b/game/src/main/java/org/toop/game/players/MiniMaxAI.java @@ -0,0 +1,165 @@ +package org.toop.game.players; + +import org.toop.framework.gameFramework.GameState; +import org.toop.framework.gameFramework.model.game.PlayResult; +import org.toop.framework.gameFramework.model.game.TurnBasedGame; +import org.toop.framework.gameFramework.model.player.AbstractAI; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class MiniMaxAI> extends AbstractAI { + + private final int maxDepth; + private final Random random = new Random(); + + public MiniMaxAI(int depth) { + this.maxDepth = depth; + } + + public MiniMaxAI(MiniMaxAI other) { + this.maxDepth = other.maxDepth; + } + + @Override + public MiniMaxAI deepCopy() { + return new MiniMaxAI<>(this); + } + + @Override + public long getMove(T game) { + long legalMoves = game.getLegalMoves(); + if (legalMoves == 0) return 0; + + List bestMoves = new ArrayList<>(); + int bestScore = Integer.MIN_VALUE; + int aiPlayer = game.getCurrentTurn(); + + long movesLoop = legalMoves; + while (movesLoop != 0) { + long move = 1L << Long.numberOfTrailingZeros(movesLoop); + T copy = game.deepCopy(); + PlayResult result = copy.play(move); + + int score; + switch (result.state()) { + case WIN -> score = (result.player() == aiPlayer ? maxDepth : -maxDepth); + case DRAW -> score = 0; + default -> score = getMoveScore(copy, maxDepth - 1, false, aiPlayer, Integer.MIN_VALUE, Integer.MAX_VALUE); + } + + if (score > bestScore) { + bestScore = score; + bestMoves.clear(); + bestMoves.add(move); + } else if (score == bestScore) { + bestMoves.add(move); + } + + movesLoop &= movesLoop - 1; + } + + long chosenMove = bestMoves.get(random.nextInt(bestMoves.size())); + return chosenMove; + } + + /** + * Recursive minimax with alpha-beta pruning and heuristic evaluation. + * + * @param game Current game state + * @param depth Remaining depth + * @param maximizing True if AI is maximizing, false if opponent + * @param aiPlayer AI's player index + * @param alpha Alpha value + * @param beta Beta value + * @return score of the position + */ + private int getMoveScore(T game, int depth, boolean maximizing, int aiPlayer, int alpha, int beta) { + long legalMoves = game.getLegalMoves(); + + // Terminal state + PlayResult lastResult = null; + if (legalMoves == 0) { + lastResult = new PlayResult(GameState.DRAW, -1); + } + + // If the game is over or depth limit reached, evaluate + if (depth <= 0 || legalMoves == 0) { + if (lastResult != null) return 0; + return evaluateBoard(game, aiPlayer); + } + + int bestScore = maximizing ? Integer.MIN_VALUE : Integer.MAX_VALUE; + long movesLoop = legalMoves; + + while (movesLoop != 0) { + long move = 1L << Long.numberOfTrailingZeros(movesLoop); + T copy = game.deepCopy(); + PlayResult result = copy.play(move); + + int score; + switch (result.state()) { + case WIN -> score = (result.player() == aiPlayer ? depth : -depth); + case DRAW -> score = 0; + default -> score = getMoveScore(copy, depth - 1, !maximizing, aiPlayer, alpha, beta); + } + + if (maximizing) { + bestScore = Math.max(bestScore, score); + alpha = Math.max(alpha, bestScore); + } else { + bestScore = Math.min(bestScore, score); + beta = Math.min(beta, bestScore); + } + + // Alpha-beta pruning + if (beta <= alpha) break; + + movesLoop &= movesLoop - 1; + } + + return bestScore; + } + + /** + * Simple heuristic evaluation for Reversi-like games. + * Positive = good for AI, Negative = good for opponent. + * + * @param game Game state + * @param aiPlayer AI's player index + * @return heuristic score + */ + private int evaluateBoard(T game, int aiPlayer) { + long[] board = game.getBoard(); + int aiCount = 0; + int opponentCount = 0; + + // Count pieces for AI vs opponent + for (int i = 0; i < board.length; i++) { + long bits = board[i]; + for (int j = 0; j < 64; j++) { + if ((bits & (1L << j)) != 0) { + // Assume player 0 occupies even indices, player 1 occupies odd + if ((i * 64 + j) % game.getPlayerCount() == aiPlayer) aiCount++; + else opponentCount++; + } + } + } + + // Mobility (number of legal moves) + int mobility = Long.bitCount(game.getLegalMoves()); + + // Corner control (top-left, top-right, bottom-left, bottom-right) + int corners = 0; + long[] cornerMasks = {1L << 0, 1L << 7, 1L << 56, 1L << 63}; + for (long mask : cornerMasks) { + for (long b : board) { + if ((b & mask) != 0) corners += 1; + } + } + + // Weighted sum + return (aiCount - opponentCount) + 2 * mobility + 5 * corners; + } +} diff --git a/game/src/main/java/org/toop/game/players/OnlinePlayer.java b/game/src/main/java/org/toop/game/players/OnlinePlayer.java new file mode 100644 index 0000000..9f011c0 --- /dev/null +++ b/game/src/main/java/org/toop/game/players/OnlinePlayer.java @@ -0,0 +1,36 @@ +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 controlled remotely or over a network. + *

+ * 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. + *

+ */ +public class OnlinePlayer> extends AbstractPlayer { + + /** + * Constructs a new OnlinePlayer. + *

+ * 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); + } +} diff --git a/game/src/main/java/org/toop/game/players/RandomAI.java b/game/src/main/java/org/toop/game/players/RandomAI.java new file mode 100644 index 0000000..2d0fe02 --- /dev/null +++ b/game/src/main/java/org/toop/game/players/RandomAI.java @@ -0,0 +1,38 @@ +package org.toop.game.players; + +import org.toop.framework.gameFramework.model.game.TurnBasedGame; +import org.toop.framework.gameFramework.model.player.AbstractAI; + +import java.util.Random; + + +public class RandomAI> extends AbstractAI { + + public RandomAI() { + super(); + } + + @Override + public RandomAI deepCopy() { + return new RandomAI(); + } + + @Override + public long getMove(T game) { + long legalMoves = game.getLegalMoves(); + int move = new Random().nextInt(Long.bitCount(legalMoves)); + return nthBitIndex(legalMoves, move); + } + + public static long nthBitIndex(long bb, int n) { + while (bb != 0) { + int tz = Long.numberOfTrailingZeros(bb); + if (n == 0) { + return 1L << tz; + } + bb &= bb - 1; // clear the least significant 1 + n--; + } + return 0L; // not enough 1s + } +}