diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java index fe06b87..64513d5 100644 --- a/app/src/main/java/org/toop/app/App.java +++ b/app/src/main/java/org/toop/app/App.java @@ -1,15 +1,8 @@ package org.toop.app; -import java.util.Stack; -import javafx.application.Application; -import javafx.application.Platform; -import javafx.scene.Scene; -import javafx.scene.layout.StackPane; -import javafx.stage.Stage; -import org.toop.app.layer.Layer; -import org.toop.app.layer.layers.MainLayer; -import org.toop.app.layer.layers.QuitPopup; -import org.toop.framework.audio.VolumeControl; +import org.toop.app.view.ViewStack; +import org.toop.app.view.views.MainView; +import org.toop.app.view.views.QuitView; import org.toop.framework.audio.events.AudioEvents; import org.toop.framework.eventbus.EventFlow; import org.toop.framework.resource.ResourceManager; @@ -17,154 +10,110 @@ import org.toop.framework.resource.resources.CssAsset; import org.toop.local.AppContext; import org.toop.local.AppSettings; -public final class App extends Application { - private static Stage stage; - private static Scene scene; - private static StackPane root; +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; + +public final class App extends Application { + private static Stage stage; + private static Scene scene; - private static Stack stack; private static int height; private static int width; - private static boolean isQuitting; + private static boolean isQuitting; - public static void run(String[] args) { - launch(args); - } + public static void run(String[] args) { + launch(args); + } - @Override - public void start(Stage stage) throws Exception { + @Override + public void start(Stage stage) throws Exception { final StackPane root = new StackPane(); - final Scene scene = new Scene(root); + final Scene scene = new Scene(root); + ViewStack.setup(scene); - stage.setTitle(AppContext.getString("appTitle")); - stage.setWidth(1080); - stage.setHeight(720); + stage.setTitle(AppContext.getString("app-title")); + stage.setWidth(1080); + stage.setHeight(720); - stage.setOnCloseRequest( - event -> { - event.consume(); + stage.setOnCloseRequest(event -> { + event.consume(); + startQuit(); + }); - if (!isQuitting) { - quitPopup(); - } - }); + stage.setScene(scene); + stage.setResizable(false); - stage.setScene(scene); - stage.setResizable(false); + stage.show(); - stage.show(); + App.stage = stage; + App.scene = scene; - App.stage = stage; - App.scene = scene; - App.root = root; + App.width = (int)stage.getWidth(); + App.height = (int)stage.getHeight(); - App.stack = new Stack<>(); + App.isQuitting = false; - App.width = (int) stage.getWidth(); - App.height = (int) stage.getHeight(); + AppSettings.applySettings(); + new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).asyncPostEvent(); - App.isQuitting = false; + ViewStack.push(new MainView()); + } - new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).postEvent(); + public static void startQuit() { + if (isQuitting) { + return; + } - final AppSettings settings = new AppSettings(); - settings.applySettings(); + ViewStack.push(new QuitView()); + isQuitting = true; + } - activate(new MainLayer()); - } + public static void stopQuit() { + ViewStack.pop(); + isQuitting = false; + } - public static void activate(Layer layer) { - Platform.runLater( - () -> { - popAll(); - push(layer); - }); - } + public static void quit() { + ViewStack.cleanup(); + stage.close(); + } - public static void push(Layer layer) { - Platform.runLater( - () -> { - root.getChildren().addLast(layer.getLayer()); - stack.push(layer); - }); - } + public static void reload() { + stage.setTitle(AppContext.getString("app-title")); + ViewStack.reload(); + } - public static void pop() { - Platform.runLater( - () -> { - root.getChildren().removeLast(); - stack.pop(); + public static void setFullscreen(boolean fullscreen) { + stage.setFullScreen(fullscreen); - isQuitting = false; - }); - } + width = (int) stage.getWidth(); + height = (int) stage.getHeight(); - public static void popAll() { - Platform.runLater( - () -> { - final int childrenCount = root.getChildren().size(); + reload(); + } - for (int i = 0; i < childrenCount; i++) { - try { - root.getChildren().removeLast(); - } catch (Exception e) { - IO.println(e); // TODO: Use logger - } - } + public static void setStyle(String theme, String layoutSize) { + final int stylesCount = scene.getStylesheets().size(); - stack.removeAllElements(); - }); - } + for (int i = 0; i < stylesCount; i++) { + scene.getStylesheets().removeLast(); + } - public static void quitPopup() { - Platform.runLater( - () -> { - push(new QuitPopup()); - isQuitting = true; - }); - } + scene.getStylesheets().add(ResourceManager.get("general.css").getUrl()); + scene.getStylesheets().add(ResourceManager.get(theme + ".css").getUrl()); + scene.getStylesheets().add(ResourceManager.get(layoutSize + ".css").getUrl()); - public static void quit() { - new EventFlow().addPostEvent(new AudioEvents.StopAudioManager()).postEvent(); - stage.close(); - } + reload(); + } - public static void reloadAll() { - stage.setTitle(AppContext.getString("appTitle")); + public static int getWidth() { + return width; + } - for (final Layer layer : stack) { - layer.reload(); - } - } - - public static void setFullscreen(boolean fullscreen) { - stage.setFullScreen(fullscreen); - - width = (int) stage.getWidth(); - height = (int) stage.getHeight(); - - reloadAll(); - } - - public static void setStyle(String theme, String layoutSize) { - final int stylesCount = scene.getStylesheets().size(); - - for (int i = 0; i < stylesCount; i++) { - scene.getStylesheets().removeLast(); - } - - scene.getStylesheets().add(ResourceManager.get(theme + ".css").getUrl()); - scene.getStylesheets().add(ResourceManager.get(layoutSize + ".css").getUrl()); - - reloadAll(); - } - - public static int getWidth() { - return width; - } - - public static int getHeight() { - return height; - } -} + public static int getHeight() { + return height; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/GameInformation.java b/app/src/main/java/org/toop/app/GameInformation.java index 0a847fb..5380003 100644 --- a/app/src/main/java/org/toop/app/GameInformation.java +++ b/app/src/main/java/org/toop/app/GameInformation.java @@ -14,8 +14,8 @@ public class GameInformation { public static int maxDepth(Type type) { return switch (type) { - case TICTACTOE -> 5; - case REVERSI -> 0; // Todo + case TICTACTOE -> 5; // Todo. 5 seems to always draw or win. could increase to 9 but that might affect performance + case REVERSI -> 10; // Todo. 10 is a guess. might be too slow or too bad. }; } } @@ -23,7 +23,7 @@ public class GameInformation { public static class Player { public String name = ""; public boolean isHuman = true; - public int computerDifficulty = 0; + public int computerDifficulty = 1; public int computerThinkTime = 1; } diff --git a/app/src/main/java/org/toop/app/Server.java b/app/src/main/java/org/toop/app/Server.java index f3ec468..8ec565a 100644 --- a/app/src/main/java/org/toop/app/Server.java +++ b/app/src/main/java/org/toop/app/Server.java @@ -1,9 +1,11 @@ package org.toop.app; +import org.toop.app.game.ReversiGame; import org.toop.app.game.TicTacToeGame; import org.toop.app.view.ViewStack; import org.toop.app.view.views.ChallengeView; import org.toop.app.view.views.ErrorView; +import org.toop.app.view.views.OnlineView; import org.toop.app.view.views.SendChallengeView; import org.toop.app.view.views.ServerView; import org.toop.framework.eventbus.EventFlow; @@ -11,6 +13,7 @@ import org.toop.framework.networking.events.NetworkEvents; import org.toop.local.AppContext; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -93,6 +96,10 @@ public final class Server { } private void sendChallenge(String opponent) { + if (!isPolling) { + return; + } + ViewStack.push(new SendChallengeView(this, opponent, (playerInformation, gameType) -> { new EventFlow().addPostEvent(new NetworkEvents.SendChallenge(clientId, opponent, gameType)) .listen(NetworkEvents.GameMatchResponse.class, e -> { @@ -108,13 +115,20 @@ public final class Server { information.players[0].name = user; information.players[1].name = opponent; - new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame); + switch (type) { + case TICTACTOE: new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; + case REVERSI: new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; + } } }).postEvent(); })); } private void handleReceivedChallenge(NetworkEvents.ChallengeResponse response) { + if (!isPolling) { + return; + } + String challengerName = response.challengerName(); challengerName = challengerName.substring(challengerName.indexOf("\"") + 1); challengerName = challengerName.substring(0, challengerName.indexOf("\"")); @@ -145,23 +159,25 @@ public final class Server { information.players[1].name = e.opponent(); switch (type) { - case TICTACTOE: - new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame); - break; - case REVERSI: - break; + case TICTACTOE: new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; + case REVERSI: new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; } } }); })); } + private void sendMessage(String message) { + new EventFlow().addPostEvent(new NetworkEvents.SendMessage(clientId, message)).postEvent(); + } + private void disconnect() { - // Todo + new EventFlow().addPostEvent(new NetworkEvents.CloseClient(clientId)).postEvent(); + ViewStack.push(new OnlineView()); } private void forfeitGame() { - // Todo + new EventFlow().addPostEvent(new NetworkEvents.SendForfeit(clientId)).postEvent(); } private void exitGame() { @@ -182,6 +198,12 @@ public final class Server { public List getGamesList() { final List list = new ArrayList(); list.add("tic-tac-toe"); // Todo: get games list from server and check if the game is supported + list.add("reversi"); + + new EventFlow().addPostEvent(new NetworkEvents.SendGetGamelist(clientId)) + .listen(NetworkEvents.GamelistResponse.class, e -> { + System.out.println(Arrays.toString(e.gamelist())); + }).postEvent(); return list; } diff --git a/app/src/main/java/org/toop/app/canvas/GameCanvas.java b/app/src/main/java/org/toop/app/canvas/GameCanvas.java index b742be7..52d0521 100644 --- a/app/src/main/java/org/toop/app/canvas/GameCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/GameCanvas.java @@ -9,6 +9,10 @@ import java.util.function.Consumer; public abstract class GameCanvas { protected record Cell(float x, float y, float width, float height) { + public boolean isInside(double x, double y) { + return x >= this.x && x <= this.x + width && + y >= this.y && y <= this.y + height; + } } protected final Canvas canvas; @@ -19,18 +23,15 @@ public abstract class GameCanvas { protected final int width; protected final int height; - protected final int rows; - protected final int columns; + protected final int rowSize; + protected final int columnSize; protected final int gapSize; protected final boolean edges; protected final Cell[] cells; - protected GameCanvas(Color color, int width, int height, int rows, int columns, int gapSize, boolean edges, Consumer onCellClicked) { - width += gapSize * 2; - height += gapSize * 2; - + protected GameCanvas(Color color, int width, int height, int rowSize, int columnSize, int gapSize, boolean edges, Consumer onCellClicked) { canvas = new Canvas(width, height); graphics = canvas.getGraphicsContext2D(); @@ -39,23 +40,23 @@ public abstract class GameCanvas { this.width = width; this.height = height; - this.rows = rows; - this.columns = columns; + this.rowSize = rowSize; + this.columnSize = columnSize; this.gapSize = gapSize; this.edges = edges; - cells = new Cell[rows * columns]; + cells = new Cell[rowSize * columnSize]; - final float cellWidth = ((float) width - gapSize * rows) / rows; - final float cellHeight = ((float) height - gapSize * columns) / columns; + final float cellWidth = ((float)width - gapSize * rowSize - gapSize) / rowSize; + final float cellHeight = ((float)height - gapSize * columnSize - gapSize) / columnSize; - for (int y = 0; y < columns; y++) { - final float startY = gapSize + y * cellHeight + y * gapSize; + for (int y = 0; y < columnSize; y++) { + final float startY = y * cellHeight + y * gapSize + gapSize; - for (int x = 0; x < rows; x++) { - final float startX = gapSize + x * cellWidth + x * gapSize; - cells[y * rows + x] = new Cell(startX, startY, cellWidth, cellHeight); + for (int x = 0; x < rowSize; x++) { + final float startX = x * cellWidth + x * gapSize + gapSize; + cells[x + y * rowSize] = new Cell(startX, startY, cellWidth, cellHeight); } } @@ -64,11 +65,15 @@ public abstract class GameCanvas { return; } - final int column = (int)((event.getX() / this.width) * rows); - final int row = (int)((event.getY() / this.height) * columns); + final int column = (int)((event.getX() / this.width) * rowSize); + final int row = (int)((event.getY() / this.height) * columnSize); - event.consume(); - onCellClicked.accept(column + row * rows); + final Cell cell = cells[column + row * rowSize]; + + if (cell.isInside(event.getX(), event.getY())) { + event.consume(); + onCellClicked.accept(column + row * rowSize); + } }); render(); @@ -81,20 +86,22 @@ public abstract class GameCanvas { public void render() { graphics.setFill(color); - for (int x = 1; x < rows; x++) { - graphics.fillRect(cells[x].x() - gapSize, 0, gapSize, height + gapSize); + for (int x = 0; x < rowSize - 1; x++) { + final float start = cells[x].x + cells[x].width; + graphics.fillRect(start, gapSize, gapSize, height - gapSize * 2); } - for (int y = 1; y < columns; y++) { - graphics.fillRect(0, cells[y * rows].y() - gapSize, width + gapSize, gapSize); + for (int y = 0; y < columnSize - 1; y++) { + final float start = cells[y * rowSize].y + cells[y * rowSize].height; + graphics.fillRect(gapSize, start, width - gapSize * 2, gapSize); } if (edges) { - graphics.fillRect(0, 0, gapSize, height + gapSize); - graphics.fillRect(0, 0, width + gapSize, gapSize); + graphics.fillRect(0, 0, width, gapSize); + graphics.fillRect(0, 0, gapSize, height); - graphics.fillRect(width + gapSize, 0, gapSize, height + gapSize * 2); - graphics.fillRect(0, height + gapSize, width + gapSize * 2, gapSize); + graphics.fillRect(width - gapSize, 0, gapSize, height); + graphics.fillRect(0, height - gapSize, width, gapSize); } } diff --git a/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java b/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java new file mode 100644 index 0000000..277937c --- /dev/null +++ b/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java @@ -0,0 +1,34 @@ +package org.toop.app.canvas; + +import javafx.scene.paint.Color; + +import java.util.function.Consumer; + +public final class ReversiCanvas extends GameCanvas { + public ReversiCanvas(Color color, int width, int height, Consumer onCellClicked) { + super(color, width, height, 8, 8, 10, true, onCellClicked); + drawStartingDots(); + } + + public void drawDot(Color color, int cell) { + final float x = cells[cell].x() + gapSize; + final float y = cells[cell].y() + gapSize; + + final float width = cells[cell].width() - gapSize * 2; + final float height = cells[cell].height() - gapSize * 2; + + graphics.setFill(color); + graphics.fillOval(x, y, width, height); + } + + public void drawStartingDots() { + drawDot(Color.BLACK, 28); + drawDot(Color.WHITE, 36); + drawDot(Color.BLACK, 35); + drawDot(Color.WHITE, 27); + } + + public void drawLegalPosition(int cell) { + drawDot(new Color(1.0f, 0.0f, 0.0f, 0.5f), cell); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/game/ReversiGame.java b/app/src/main/java/org/toop/app/game/ReversiGame.java new file mode 100644 index 0000000..18527fe --- /dev/null +++ b/app/src/main/java/org/toop/app/game/ReversiGame.java @@ -0,0 +1,257 @@ +package org.toop.app.game; + +import org.toop.app.App; +import org.toop.app.GameInformation; +import org.toop.app.canvas.ReversiCanvas; +import org.toop.app.view.ViewStack; +import org.toop.app.view.views.GameView; +import org.toop.app.view.views.LocalMultiplayerView; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.networking.events.NetworkEvents; +import org.toop.game.Game; +import org.toop.game.reversi.Reversi; +import org.toop.game.reversi.ReversiAI; + +import javafx.geometry.Pos; +import javafx.scene.paint.Color; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public final class ReversiGame { + private final GameInformation information; + + private final int myTurn; + private final BlockingQueue moveQueue; + + private final Reversi game; + private final ReversiAI ai; + + private final GameView view; + private final ReversiCanvas canvas; + + private final AtomicBoolean isRunning; + + public ReversiGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage) { + this.information = information; + + this.myTurn = myTurn; + moveQueue = new LinkedBlockingQueue(); + + game = new Reversi(); + ai = new ReversiAI(); + + isRunning = new AtomicBoolean(true); + + if (onForfeit == null || onExit == null) { + view = new GameView(null, () -> { + isRunning.set(false); + ViewStack.push(new LocalMultiplayerView(information)); + }, null); + } else { + view = new GameView(onForfeit, () -> { + isRunning.set(false); + onExit.run(); + }, onMessage); + } + + canvas = new ReversiCanvas(Color.GRAY, + (App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3, + (cell) -> { + if (onForfeit == null || onExit == null) { + if (information.players[game.getCurrentTurn()].isHuman) { + final char value = game.getCurrentTurn() == 0? 'B' : 'W'; + + try { + moveQueue.put(new Game.Move(cell, value)); + } catch (InterruptedException _) {} + } + } else { + if (information.players[0].isHuman) { + final char value = myTurn == 0? 'B' : 'W'; + + try { + moveQueue.put(new Game.Move(cell, value)); + } catch (InterruptedException _) {} + } + } + }); + + view.add(Pos.CENTER, canvas.getCanvas()); + ViewStack.push(view); + + if (onForfeit == null || onExit == null) { + new Thread(this::localGameThread).start(); + } else { + new EventFlow() + .listen(NetworkEvents.GameMoveResponse.class, this::onMoveResponse) + .listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse) + .listen(NetworkEvents.ReceivedMessage.class, this::onReceivedMessage); + + setGameLabels(myTurn == 0); + } + + updateCanvas(); + } + + public ReversiGame(GameInformation information) { + this(information, 0, null, null, null); + } + + private void localGameThread() { + while (isRunning.get()) { + final int currentTurn = game.getCurrentTurn(); + final char currentValue = currentTurn == 0? 'B' : 'W'; + final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); + + view.nextPlayer(information.players[currentTurn].isHuman, + information.players[currentTurn].name, + String.valueOf(currentValue), + information.players[nextTurn].name); + + Game.Move move = null; + + if (information.players[currentTurn].isHuman) { + try { + final Game.Move wants = moveQueue.take(); + final Game.Move[] legalMoves = game.getLegalMoves(); + + for (final Game.Move legalMove : legalMoves) { + if (legalMove.position() == wants.position() && + legalMove.value() == wants.value()) { + move = wants; + break; + } + } + } catch (InterruptedException _) {} + } else { + final long start = System.currentTimeMillis(); + + move = ai.findBestMove(game, information.players[currentTurn].computerDifficulty); + + if (information.players[currentTurn].computerThinkTime > 0) { + final long elapsedTime = System.currentTimeMillis() - start; + final long sleepTime = information.players[currentTurn].computerThinkTime * 1000L - elapsedTime; + + try { + Thread.sleep((long) (sleepTime * Math.random())); + } catch (InterruptedException _) {} + } + } + + if (move == null) { + continue; + } + + final Game.State state = game.play(move); + updateCanvas(); + + if (state != Game.State.NORMAL) { + if (state == Game.State.WIN) { + view.gameOver(true, information.players[currentTurn].name); + } else if (state == Game.State.DRAW) { + view.gameOver(false, ""); + } + + isRunning.set(false); + } + } + } + + private void onMoveResponse(NetworkEvents.GameMoveResponse response) { + if (!isRunning.get()) { + return; + } + + char playerChar; + + if (response.player().equalsIgnoreCase(information.players[0].name)) { + playerChar = myTurn == 0? 'B' : 'W'; + } else { + playerChar = myTurn == 0? 'W' : 'B'; + } + + final Game.Move move = new Game.Move(Integer.parseInt(response.move()), playerChar); + final Game.State state = game.play(move); + + if (state != Game.State.NORMAL) { + if (state == Game.State.WIN) { + if (response.player().equalsIgnoreCase(information.players[0].name)) { + view.gameOver(true, information.players[0].name); + } else { + view.gameOver(false, information.players[1].name); + } + } else if (state == Game.State.DRAW) { + view.gameOver(false, ""); + } + } + + updateCanvas(); + setGameLabels(game.getCurrentTurn() == myTurn); + } + + private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) { + if (!isRunning.get()) { + return; + } + + moveQueue.clear(); + + int position = -1; + + if (information.players[0].isHuman) { + try { + position = moveQueue.take().position(); + } catch (InterruptedException _) {} + } else { + final Game.Move move = ai.findBestMove(game, information.players[0].computerDifficulty); + + assert move != null; + position = move.position(); + } + + new EventFlow().addPostEvent(new NetworkEvents.SendMove(response.clientId(), (short) position)) + .postEvent(); + } + + private void onReceivedMessage(NetworkEvents.ReceivedMessage msg) { + if (!isRunning.get()) { + return; + } + + view.updateChat(msg.message()); + } + + private void updateCanvas() { + // Todo: this is very inefficient. still very fast but if the grid is bigger it might cause issues. improve. + + canvas.clear(); + canvas.render(); + + for (int i = 0; i < game.board.length; i++) { + if (game.board[i] == 'B') { + canvas.drawDot(Color.BLACK, i); + } else if (game.board[i] == 'W') { + canvas.drawDot(Color.WHITE, i); + } + } + + final Game.Move[] legalMoves = game.getLegalMoves(); + + for (final Game.Move legalMove : legalMoves) { + canvas.drawLegalPosition(legalMove.position()); + } + } + + private void setGameLabels(boolean isMe) { + final int currentTurn = game.getCurrentTurn(); + final char currentValue = currentTurn == 0? 'B' : 'W'; + + view.nextPlayer(isMe, + information.players[isMe? 0 : 1].name, + String.valueOf(currentValue), + information.players[isMe? 1 : 0].name); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/game/TicTacToeGame.java b/app/src/main/java/org/toop/app/game/TicTacToeGame.java index 07f589a..cddef0a 100644 --- a/app/src/main/java/org/toop/app/game/TicTacToeGame.java +++ b/app/src/main/java/org/toop/app/game/TicTacToeGame.java @@ -17,6 +17,8 @@ import javafx.scene.paint.Color; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; public final class TicTacToeGame { private final GameInformation information; @@ -30,7 +32,9 @@ public final class TicTacToeGame { private final GameView view; private final TicTacToeCanvas canvas; - public TicTacToeGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit) { + private AtomicBoolean isRunning; + + public TicTacToeGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage) { this.information = information; this.myTurn = myTurn; @@ -39,12 +43,18 @@ public final class TicTacToeGame { game = new TicTacToe(); ai = new TicTacToeAI(); + isRunning = new AtomicBoolean(true); + if (onForfeit == null || onExit == null) { view = new GameView(null, () -> { + isRunning.set(false); ViewStack.push(new LocalMultiplayerView(information)); - }); + }, null); } else { - view = new GameView(onForfeit, onExit); + view = new GameView(onForfeit, () -> { + isRunning.set(false); + onExit.run(); + }, onMessage); } canvas = new TicTacToeCanvas(Color.GRAY, @@ -84,10 +94,12 @@ public final class TicTacToeGame { } } - private void localGameThread() { - boolean isRunning = true; + public TicTacToeGame(GameInformation information) { + this(information, 0, null, null, null); + } - while (isRunning) { + private void localGameThread() { + while (isRunning.get()) { final int currentTurn = game.getCurrentTurn(); final char currentValue = currentTurn == 0? 'X' : 'O'; final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); @@ -146,12 +158,16 @@ public final class TicTacToeGame { view.gameOver(false, ""); } - isRunning = false; + isRunning.set(false); } } } private void onMoveResponse(NetworkEvents.GameMoveResponse response) { + if (!isRunning.get()) { + return; + } + char playerChar; if (response.player().equalsIgnoreCase(information.players[0].name)) { @@ -185,6 +201,10 @@ public final class TicTacToeGame { } private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) { + if (!isRunning.get()) { + return; + } + moveQueue.clear(); int position = -1; @@ -205,7 +225,11 @@ public final class TicTacToeGame { } private void onReceivedMessage(NetworkEvents.ReceivedMessage msg) { - view.updateChat("anon", msg.message()); + if (!isRunning.get()) { + return; + } + + view.updateChat(msg.message()); } private void setGameLabels(boolean isMe) { diff --git a/app/src/main/java/org/toop/app/layer/layers/MainLayer.java b/app/src/main/java/org/toop/app/layer/layers/MainLayer.java deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/org/toop/app/layer/layers/OptionsPopup.java b/app/src/main/java/org/toop/app/layer/layers/OptionsPopup.java deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/org/toop/app/layer/layers/game/TicTacToeLayer.java b/app/src/main/java/org/toop/app/layer/layers/game/TicTacToeLayer.java deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/org/toop/app/view/views/GameView.java b/app/src/main/java/org/toop/app/view/views/GameView.java index e8f12cb..3834159 100644 --- a/app/src/main/java/org/toop/app/view/views/GameView.java +++ b/app/src/main/java/org/toop/app/view/views/GameView.java @@ -6,8 +6,12 @@ import org.toop.local.AppContext; import javafx.geometry.Pos; import javafx.scene.control.Button; +import javafx.scene.control.ListView; +import javafx.scene.control.TextField; import javafx.scene.text.Text; +import java.util.function.Consumer; + public final class GameView extends View { private static class GameOverView extends View { private final boolean iWon; @@ -65,7 +69,10 @@ public final class GameView extends View { private final Text nextPlayerHeader; - public GameView(Runnable onForfeit, Runnable onExit) { + private final ListView chatListView; + private final TextField chatInput; + + public GameView(Runnable onForfeit, Runnable onExit, Consumer onMessage) { assert onExit != null; super(true, "bg-primary"); @@ -78,6 +85,19 @@ public final class GameView extends View { forfeitButton = null; } + if (onMessage != null) { + chatListView = new ListView(); + + chatInput = input(); + chatInput.setOnAction(_ -> { + onMessage.accept(chatInput.getText()); + chatInput.setText(""); + }); + } else { + chatListView = null; + chatInput = null; + } + exitButton = button(); exitButton.setText(AppContext.getString("exit")); exitButton.setOnAction(_ -> onExit.run()); @@ -110,6 +130,15 @@ public final class GameView extends View { exitButton ) ); + + if (chatListView != null) { + add(Pos.BOTTOM_RIGHT, + fit(vboxFill( + chatListView, + chatInput + ) + )); + } } public void nextPlayer(boolean isMe, String currentPlayer, String currentMove, String nextPlayer) { @@ -125,8 +154,15 @@ public final class GameView extends View { } } - public void updateChat(String player, String message) { - // Todo + public void updateChat(String message) { + if (chatListView == null) { + return; + } + + final Text messageText = text(); + messageText.setText(message); + + chatListView.getItems().add(messageText); } public void gameOver(boolean iWon, String winner) { diff --git a/app/src/main/java/org/toop/app/view/views/LocalMultiplayerView.java b/app/src/main/java/org/toop/app/view/views/LocalMultiplayerView.java index 13cf837..a4e72d9 100644 --- a/app/src/main/java/org/toop/app/view/views/LocalMultiplayerView.java +++ b/app/src/main/java/org/toop/app/view/views/LocalMultiplayerView.java @@ -1,6 +1,7 @@ package org.toop.app.view.views; import org.toop.app.GameInformation; +import org.toop.app.game.ReversiGame; import org.toop.app.game.TicTacToeGame; import org.toop.app.view.View; import org.toop.app.view.ViewStack; @@ -42,8 +43,8 @@ public final class LocalMultiplayerView extends View { } switch (information.type) { - case TICTACTOE: new TicTacToeGame(information, 0, null, null); - case REVERSI: break; + case TICTACTOE: new TicTacToeGame(information); break; + case REVERSI: new ReversiGame(information); break; } }); diff --git a/app/src/main/java/org/toop/app/view/views/LocalView.java b/app/src/main/java/org/toop/app/view/views/LocalView.java index 215a6c2..2089e5c 100644 --- a/app/src/main/java/org/toop/app/view/views/LocalView.java +++ b/app/src/main/java/org/toop/app/view/views/LocalView.java @@ -21,7 +21,7 @@ public final class LocalView extends View { final Button reversiButton = button(); reversiButton.setText(AppContext.getString("reversi")); - reversiButton.setOnAction(_ -> {}); + reversiButton.setOnAction(_ -> { ViewStack.push(new LocalMultiplayerView(GameInformation.Type.REVERSI)); }); add(Pos.CENTER, fit(vboxFill( diff --git a/app/src/main/java/org/toop/app/view/views/OptionsView.java b/app/src/main/java/org/toop/app/view/views/OptionsView.java index c3e1fe5..3b8eaaa 100644 --- a/app/src/main/java/org/toop/app/view/views/OptionsView.java +++ b/app/src/main/java/org/toop/app/view/views/OptionsView.java @@ -3,6 +3,7 @@ package org.toop.app.view.views; import org.toop.app.App; import org.toop.app.view.View; import org.toop.app.view.ViewStack; +import org.toop.framework.audio.VolumeControl; import org.toop.framework.audio.events.AudioEvents; import org.toop.framework.eventbus.EventFlow; import org.toop.local.AppContext; @@ -143,7 +144,7 @@ public final class OptionsView extends View { masterVolumeSlider.valueProperty().addListener((_, _, newValue) -> { AppSettings.getSettings().setVolume(newValue.intValue()); - new EventFlow().addPostEvent(new AudioEvents.ChangeVolume(newValue.doubleValue())).asyncPostEvent(); + new EventFlow().addPostEvent(new AudioEvents.ChangeVolume(newValue.doubleValue(), VolumeControl.MASTERVOLUME)).asyncPostEvent(); }); } @@ -158,7 +159,7 @@ public final class OptionsView extends View { effectsVolumeSlider.valueProperty().addListener((_, _, newValue) -> { AppSettings.getSettings().setFxVolume(newValue.intValue()); - new EventFlow().addPostEvent(new AudioEvents.ChangeFxVolume(newValue.doubleValue())).asyncPostEvent(); + new EventFlow().addPostEvent(new AudioEvents.ChangeVolume(newValue.doubleValue(), VolumeControl.FX)).asyncPostEvent(); }); } @@ -173,7 +174,7 @@ public final class OptionsView extends View { musicVolumeSlider.valueProperty().addListener((_, _, newValue) -> { AppSettings.getSettings().setMusicVolume(newValue.intValue()); - new EventFlow().addPostEvent(new AudioEvents.ChangeMusicVolume(newValue.doubleValue())).asyncPostEvent(); + new EventFlow().addPostEvent(new AudioEvents.ChangeVolume(newValue.doubleValue(), VolumeControl.MUSIC)).asyncPostEvent(); }); } diff --git a/app/src/main/java/org/toop/local/AppSettings.java b/app/src/main/java/org/toop/local/AppSettings.java index 270d53f..2e26fbe 100644 --- a/app/src/main/java/org/toop/local/AppSettings.java +++ b/app/src/main/java/org/toop/local/AppSettings.java @@ -1,7 +1,5 @@ package org.toop.local; -import java.io.File; -import java.util.Locale; import org.toop.app.App; import org.toop.framework.audio.VolumeControl; import org.toop.framework.audio.events.AudioEvents; @@ -11,57 +9,59 @@ import org.toop.framework.resource.ResourceMeta; import org.toop.framework.resource.resources.SettingsAsset; import org.toop.framework.settings.Settings; +import java.io.File; +import java.util.Locale; + public class AppSettings { + private static SettingsAsset settingsAsset; - private SettingsAsset settingsAsset; + public static void applySettings() { + settingsAsset = getPath(); + if (!settingsAsset.isLoaded()) { + settingsAsset.load(); + } - public void applySettings() { - this.settingsAsset = getPath(); - if (!this.settingsAsset.isLoaded()) { - this.settingsAsset.load(); - } + Settings settingsData = settingsAsset.getContent(); - Settings settingsData = this.settingsAsset.getContent(); + AppContext.setLocale(Locale.of(settingsData.locale)); + App.setFullscreen(settingsData.fullScreen); + new EventFlow() + .addPostEvent(new AudioEvents.ChangeVolume(settingsData.volume, VolumeControl.MASTERVOLUME)) + .asyncPostEvent(); + new EventFlow() + .addPostEvent(new AudioEvents.ChangeVolume(settingsData.fxVolume, VolumeControl.FX)) + .asyncPostEvent(); + new EventFlow() + .addPostEvent(new AudioEvents.ChangeVolume(settingsData.musicVolume, VolumeControl.MUSIC)) + .asyncPostEvent(); + App.setStyle(settingsAsset.getTheme(), settingsAsset.getLayoutSize()); + } - AppContext.setLocale(Locale.of(settingsData.locale)); - App.setFullscreen(settingsData.fullScreen); - new EventFlow() - .addPostEvent(new AudioEvents.ChangeVolume(settingsData.volume, VolumeControl.MASTERVOLUME)) - .asyncPostEvent(); - new EventFlow() - .addPostEvent(new AudioEvents.ChangeVolume(settingsData.fxVolume, VolumeControl.FX)) - .asyncPostEvent(); - new EventFlow() - .addPostEvent(new AudioEvents.ChangeVolume(settingsData.musicVolume, VolumeControl.MUSIC)) - .asyncPostEvent(); - App.setStyle(settingsAsset.getTheme(), settingsAsset.getLayoutSize()); - } + public static SettingsAsset getPath() { + if (settingsAsset == null) { + String os = System.getProperty("os.name").toLowerCase(); + String basePath; - public SettingsAsset getPath() { - if (this.settingsAsset == null) { - String os = System.getProperty("os.name").toLowerCase(); - String basePath; + if (os.contains("win")) { + basePath = System.getenv("APPDATA"); + if (basePath == null) { + basePath = System.getProperty("user.home"); + } + } else if (os.contains("mac")) { + basePath = System.getProperty("user.home") + "/Library/Application Support"; + } else { + basePath = System.getProperty("user.home") + "/.config"; + } - if (os.contains("win")) { - basePath = System.getenv("APPDATA"); - if (basePath == null) { - basePath = System.getProperty("user.home"); - } - } else if (os.contains("mac")) { - basePath = System.getProperty("user.home") + "/Library/Application Support"; - } else { - basePath = System.getProperty("user.home") + "/.config"; - } + File settingsFile = + new File(basePath + File.separator + "ISY1" + File.separator + "settings.json"); +// this.settingsAsset = new SettingsAsset(settingsFile); + ResourceManager.addAsset(new ResourceMeta<>("settings.json", new SettingsAsset(settingsFile))); + } + return ResourceManager.get("settings.json"); + } - File settingsFile = - new File(basePath + File.separator + "ISY1" + File.separator + "settings.json"); - - return new SettingsAsset(settingsFile); -// this.settingsAsset = new SettingsAsset(settingsFile); // TODO -// ResourceManager.addAsset(new ResourceMeta<>("settings.json", new SettingsAsset(settingsFile))); // TODO - } - - return this.settingsAsset; -// return ResourceManager.get("settings.json"); // TODO - } -} + public static SettingsAsset getSettings() { + return settingsAsset; + } +} \ No newline at end of file diff --git a/app/src/main/resources/assets/audio/music/blitzkrieg.mp3 b/app/src/main/resources/assets/audio/music/blitzkrieg.mp3 new file mode 100644 index 0000000..04b38f6 Binary files /dev/null and b/app/src/main/resources/assets/audio/music/blitzkrieg.mp3 differ diff --git a/app/src/main/resources/assets/audio/music/getlucky.mp3 b/app/src/main/resources/assets/audio/music/getlucky.mp3 new file mode 100644 index 0000000..8d1699f Binary files /dev/null and b/app/src/main/resources/assets/audio/music/getlucky.mp3 differ diff --git a/app/src/main/resources/assets/audio/music/roarofthejungledragon.mp3 b/app/src/main/resources/assets/audio/music/roarofthejungledragon.mp3 new file mode 100644 index 0000000..9704671 Binary files /dev/null and b/app/src/main/resources/assets/audio/music/roarofthejungledragon.mp3 differ diff --git a/app/src/main/resources/assets/style/dark.css b/app/src/main/resources/assets/style/dark.css index 614c459..9cb894a 100644 --- a/app/src/main/resources/assets/style/dark.css +++ b/app/src/main/resources/assets/style/dark.css @@ -73,11 +73,6 @@ -fx-text-fill: #e0f2e9; } -.container { - -fx-background-color: linear-gradient(to bottom, #2b3a3e, #1a2224); - -fx-background-radius: 6; -} - .input { -fx-background-color: linear-gradient(to bottom, #1f3a3b, #124040); -fx-background-radius: 6; diff --git a/app/src/main/resources/assets/style/general.css b/app/src/main/resources/assets/style/general.css index 205bb3b..deeda43 100644 --- a/app/src/main/resources/assets/style/general.css +++ b/app/src/main/resources/assets/style/general.css @@ -6,9 +6,11 @@ .container, .credits-container { -fx-alignment: TOP_CENTER; + -fx-background-color: transparent; } -.fit { +.fit, +.fit .viewport { -fx-background-color: transparent; -fx-border-color: transparent; } diff --git a/app/src/main/resources/assets/style/high-contrast.css b/app/src/main/resources/assets/style/high-contrast.css index 6fa3343..244b203 100644 --- a/app/src/main/resources/assets/style/high-contrast.css +++ b/app/src/main/resources/assets/style/high-contrast.css @@ -73,11 +73,6 @@ -fx-text-fill: #f0fff0; } -.container { - -fx-background-color: linear-gradient(to bottom, #121e1c, #0c1210); - -fx-background-radius: 6; -} - .input { -fx-background-color: linear-gradient(to bottom, #15331a, #0e2b15); -fx-background-radius: 6; diff --git a/app/src/main/resources/assets/style/large.css b/app/src/main/resources/assets/style/large.css index 0678816..f57a7bd 100644 --- a/app/src/main/resources/assets/style/large.css +++ b/app/src/main/resources/assets/style/large.css @@ -23,11 +23,6 @@ -fx-spacing: 14; } -.credits-container { - -fx-alignment: CENTER; - -fx-padding: 28; -} - .current-player { -fx-font-size: 32px; } diff --git a/app/src/main/resources/assets/style/light.css b/app/src/main/resources/assets/style/light.css index 094d620..c959d0d 100644 --- a/app/src/main/resources/assets/style/light.css +++ b/app/src/main/resources/assets/style/light.css @@ -1,13 +1,13 @@ .bg-primary { - -fx-background-color: linear-gradient(to bottom, #f0f6f4, #dbe9e5); + -fx-background-color: linear-gradient(to bottom, #e8f1ef, #cfded9); } .bg-secondary { - -fx-background-color: linear-gradient(to bottom, #d0e0db, #b7cdc6); + -fx-background-color: linear-gradient(to bottom, #c3d6d1, #a9c2bb); } .bg-popup { - -fx-background-color: rgba(240, 248, 250, 0.9); + -fx-background-color: rgba(224, 240, 242, 0.95); -fx-background-radius: 8; -fx-effect: dropshadow(gaussian, #a0c4b088, 6, 0, 0, 2); } @@ -18,7 +18,7 @@ -fx-border-color: #5caf5c; -fx-cursor: hand; -fx-effect: dropshadow(gaussian, #7abf7aaa, 5, 0, 0, 2); - -fx-text-fill: #2a3a2a; + -fx-text-fill: #2e4d2e; -fx-font-weight: bold; } @@ -51,7 +51,7 @@ .combo-box .list-cell { -fx-background-color: transparent; - -fx-text-fill: #588758; + -fx-text-fill: #2e4d2e; } .combo-box .list-view { @@ -61,7 +61,7 @@ } .combo-box-popup .list-cell { - -fx-text-fill: #588758; + -fx-text-fill: #2e4d2e; } .combo-box-popup .list-cell:hover { @@ -70,19 +70,14 @@ .combo-box-popup .list-cell:selected { -fx-background-color: #7ac27a; - -fx-text-fill: #f0faf0; -} - -.container { - -fx-background-color: linear-gradient(to bottom, #e9f2ee, #cde3d9); - -fx-background-radius: 6; + -fx-text-fill: #ffffff; } .input { -fx-background-color: linear-gradient(to bottom, #e6f0ec, #c8dbcd); -fx-background-radius: 6; -fx-border-color: #5caf5c; - -fx-text-fill: #588758; + -fx-text-fill: #2e4d2e; -fx-font-weight: normal; } @@ -127,13 +122,13 @@ } .text { - -fx-fill: #588758; + -fx-fill: #2e4d2e; -fx-font-weight: normal; - -fx-text-fill: #588758; + -fx-text-fill: #2e4d2e; } .header { - -fx-fill: #aad3aa; + -fx-fill: #2b5c2b; -fx-font-weight: bold; - -fx-text-fill: #aad3aa; + -fx-text-fill: #2b5c2b; } \ No newline at end of file diff --git a/app/src/main/resources/assets/style/medium.css b/app/src/main/resources/assets/style/medium.css index 1bec5a3..94f849d 100644 --- a/app/src/main/resources/assets/style/medium.css +++ b/app/src/main/resources/assets/style/medium.css @@ -23,11 +23,6 @@ -fx-spacing: 10; } -.credits-container { - -fx-alignment: CENTER; - -fx-padding: 20; -} - .current-player { -fx-font-size: 24px; } diff --git a/app/src/main/resources/assets/style/small.css b/app/src/main/resources/assets/style/small.css index 44e8117..7b450d5 100644 --- a/app/src/main/resources/assets/style/small.css +++ b/app/src/main/resources/assets/style/small.css @@ -23,11 +23,6 @@ -fx-spacing: 6; } -.credits-container { - -fx-alignment: CENTER; - -fx-padding: 12; -} - .current-player { -fx-font-size: 16px; } diff --git a/game/src/main/java/org/toop/game/Game.java b/game/src/main/java/org/toop/game/Game.java index 9b4259c..579821e 100644 --- a/game/src/main/java/org/toop/game/Game.java +++ b/game/src/main/java/org/toop/game/Game.java @@ -7,11 +7,14 @@ public abstract class Game { NORMAL, DRAW, WIN, + MOVE_SKIPPED, } public record Move(int position, char value) {} - public static final char EMPTY = (char) 0; + public record Score(int player1Score, int player2Score) {} + + public static final char EMPTY = (char)0; public final int rowSize; public final int columnSize; diff --git a/game/src/main/java/org/toop/game/othello/Othello.java b/game/src/main/java/org/toop/game/othello/Othello.java deleted file mode 100644 index 435527a..0000000 --- a/game/src/main/java/org/toop/game/othello/Othello.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.toop.game.othello; - -import org.toop.game.TurnBasedGame; - -public final class Othello extends TurnBasedGame { - Othello() { - super(8, 8, 2); - } - - @Override - public Move[] getLegalMoves() { - return new Move[0]; - } - - @Override - public State play(Move move) { - return null; - } -} diff --git a/game/src/main/java/org/toop/game/othello/OthelloAI.java b/game/src/main/java/org/toop/game/othello/OthelloAI.java deleted file mode 100644 index 40f147c..0000000 --- a/game/src/main/java/org/toop/game/othello/OthelloAI.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.toop.game.othello; - -import org.toop.game.AI; -import org.toop.game.Game; - -public final class OthelloAI extends AI { - @Override - public Game.Move findBestMove(Othello game, int depth) { - return null; - } -} diff --git a/game/src/main/java/org/toop/game/reversi/Reversi.java b/game/src/main/java/org/toop/game/reversi/Reversi.java new file mode 100644 index 0000000..e378b45 --- /dev/null +++ b/game/src/main/java/org/toop/game/reversi/Reversi.java @@ -0,0 +1,196 @@ +package org.toop.game.reversi; + +import org.toop.game.Game; +import org.toop.game.TurnBasedGame; +import org.toop.game.tictactoe.TicTacToe; + +import java.awt.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + + +public final class Reversi extends TurnBasedGame { + private int movesTaken; + public static final char FIRST_MOVE = 'B'; + private Set filledCells = new HashSet<>(); + + public Reversi() { + super(8, 8, 2); + addStartPieces(); + } + + public Reversi(Reversi other) { + super(other); + this.movesTaken = other.movesTaken; + this.filledCells = other.filledCells; + } + + + private void addStartPieces() { + board[27] = 'W'; + board[28] = 'B'; + board[35] = 'B'; + board[36] = 'W'; + updateFilledCellsSet(); + } + private void updateFilledCellsSet() { + for (int i = 0; i < 64; i++) { + if (board[i] == 'W' || board[i] == 'B') { + filledCells.add(new Point(i % columnSize, i / rowSize)); + } + } + } + + @Override + public Move[] getLegalMoves() { + final ArrayList legalMoves = new ArrayList<>(); + char[][] boardGrid = makeBoardAGrid(); + char currentPlayer = (currentTurn==0) ? 'B' : 'W'; + Set adjCell = getAdjacentCells(boardGrid); + for (Point point : adjCell){ + Move[] moves = getFlipsForPotentialMove(point,boardGrid,currentPlayer); + int score = moves.length; + if (score > 0){ + legalMoves.add(new Move(point.x + point.y * rowSize, currentPlayer)); + } + } + return legalMoves.toArray(new Move[0]); + } + + private Set getAdjacentCells(char[][] boardGrid) { + Set possibleCells = new HashSet<>(); + for (Point point : filledCells) { //for every filled cell + for (int deltaColumn = -1; deltaColumn <= 1; deltaColumn++){ //check adjacent cells + for (int deltaRow = -1; deltaRow <= 1; deltaRow++){ //orthogonally and diagonally + int newX = point.x + deltaColumn, newY = point.y + deltaRow; + if (deltaColumn == 0 && deltaRow == 0 //continue if out of bounds + || !isOnBoard(newX, newY)) { + continue; + } + if (boardGrid[newY][newX] == Game.EMPTY) { //check if the cell is empty + possibleCells.add(new Point(newX, newY)); //and then add it to the set of possible moves + } + } + } + } + return possibleCells; + } + + public Move[] getFlipsForPotentialMove(Point point, char[][] boardGrid, char currentPlayer) { + final ArrayList movesToFlip = new ArrayList<>(); + for (int deltaColumn = -1; deltaColumn <= 1; deltaColumn++) { + for (int deltaRow = -1; deltaRow <= 1; deltaRow++) { + if (deltaColumn == 0 && deltaRow == 0){ + continue; + } + Move[] moves = getFlipsInDirection(point,boardGrid,currentPlayer,deltaColumn,deltaRow); + if (moves != null) { + movesToFlip.addAll(Arrays.asList(moves)); + } + } + } + return movesToFlip.toArray(new Move[0]); + } + + private Move[] getFlipsInDirection(Point point, char[][] boardGrid, char currentPlayer, int dirX, int dirY) { + char opponent = getOpponent(currentPlayer); + final ArrayList movesToFlip = new ArrayList<>(); + int x = point.x + dirX; + int y = point.y + dirY; + + if (!isOnBoard(x, y) || boardGrid[y][x] != opponent) { + return null; + } + + while (isOnBoard(x, y) && boardGrid[y][x] == opponent) { + + movesToFlip.add(new Move(x+y*rowSize, currentPlayer)); + x += dirX; + y += dirY; + } + if (isOnBoard(x, y) && boardGrid[y][x] == currentPlayer) { + return movesToFlip.toArray(new Move[0]); + } + return null; + } + + private boolean isOnBoard(int x, int y) { + return x >= 0 && x < columnSize && y >= 0 && y < rowSize; + } + + public char[][] makeBoardAGrid() { + char[][] boardGrid = new char[rowSize][columnSize]; + for (int i = 0; i < 64; i++) { + boardGrid[i / rowSize][i % columnSize] = board[i]; //boardGrid[y / row] [x / column] + } + return boardGrid; + } + @Override + public State play(Move move) { + Move[] legalMoves = getLegalMoves(); + boolean moveIsLegal = false; + for (Move legalMove : legalMoves) { + if (move.equals(legalMove)) { + moveIsLegal = true; + break; + } + } + if (moveIsLegal) { + Move[] moves = getFlipsForPotentialMove(new Point(move.position()%columnSize,move.position()/rowSize), makeBoardAGrid(), move.value()); + board[move.position()] = move.value(); + IO.println(move.position() +" "+ move.value()); + for (Move m : moves) { + board[m.position()] = m.value(); + } + filledCells.add(new Point(move.position() % rowSize, move.position() / columnSize)); + //updateFilledCellsSet(); + nextTurn(); + if (getLegalMoves().length == 0) { + skipMyTurn(); + return State.MOVE_SKIPPED; + } + return State.NORMAL; + } + + return null; + } + + public void skipMyTurn(){ + IO.println("TURN " + getCurrentPlayer() + " SKIPPED"); + nextTurn(); + } + + public char getCurrentPlayer() { + if (currentTurn == 0){ + return 'B'; + } + else { + return 'W'; + } + } + + private char getOpponent(char currentPlayer){ + if (currentPlayer == 'B') { + return 'W'; + } + else { + return 'B'; + } + } + + public Game.Score getScore(){ + int player1Score = 0, player2Score = 0; + for (int count = 0; count < rowSize * columnSize; count++) { + if (board[count] == 'W') { + player1Score += 1; + } + if (board[count] == 'B') { + player2Score += 1; + } + } + return new Game.Score(player1Score, player2Score); + } + +} \ No newline at end of file diff --git a/game/src/main/java/org/toop/game/reversi/ReversiAI.java b/game/src/main/java/org/toop/game/reversi/ReversiAI.java new file mode 100644 index 0000000..4f0865e --- /dev/null +++ b/game/src/main/java/org/toop/game/reversi/ReversiAI.java @@ -0,0 +1,11 @@ +package org.toop.game.reversi; + +import org.toop.game.AI; +import org.toop.game.Game; + +public final class ReversiAI extends AI { + @Override + public Game.Move findBestMove(Reversi game, int depth) { + return game.getLegalMoves()[0]; + } +} diff --git a/game/src/test/java/org/toop/game/tictactoe/ReversiTest.java b/game/src/test/java/org/toop/game/tictactoe/ReversiTest.java new file mode 100644 index 0000000..f251b89 --- /dev/null +++ b/game/src/test/java/org/toop/game/tictactoe/ReversiTest.java @@ -0,0 +1,165 @@ +package org.toop.game.tictactoe; + +import org.toop.game.Game; + +import java.util.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.toop.game.reversi.Reversi; +import org.toop.game.reversi.ReversiAI; + +import static org.junit.jupiter.api.Assertions.*; + +class ReversiTest { + private Reversi game; + private ReversiAI ai; + + @BeforeEach + void setup() { + game = new Reversi(); + ai = new ReversiAI(); + } + + + @Test + void testCorrectStartPiecesPlaced() { + assertNotNull(game); + assertEquals('W',game.board[27]); + assertEquals('B',game.board[28]); + assertEquals('B',game.board[35]); + assertEquals('W',game.board[36]); + } + + @Test + void testGetLegalMovesAtStart() { + Game.Move[] moves = game.getLegalMoves(); + List expectedMoves = List.of( + new Game.Move(19,'B'), + new Game.Move(26,'B'), + new Game.Move(37,'B'), + new Game.Move(44,'B') + ); + assertNotNull(moves); + assertTrue(moves.length > 0); + assertMovesMatchIgnoreOrder(expectedMoves, Arrays.asList(moves)); + } + + private void assertMovesMatchIgnoreOrder(List expected, List 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 Game.Move(19, 'B')); + assertEquals('B', game.board[19]); + assertEquals('B', game.board[27], "Piece should have flipped to B"); + } + + @Test + void testMakeInvalidMoveDoesNothing() { + char[] before = game.board.clone(); + game.play(new Game.Move(0, 'B')); + assertArrayEquals(before, game.board, "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(); + Game.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 Game.Move(19, 'B')); + game.play(new Game.Move(20, 'W')); + Game.Move[] moves = game.getLegalMoves(); + List expectedMoves = List.of( + new Game.Move(13,'B'), + new Game.Move(21, 'B'), + new Game.Move(29, 'B'), + new Game.Move(37, 'B'), + new Game.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(); + Game.Move[] legalMoves = game.getLegalMoves(); + while(legalMoves.length > 0) { + game.play(legalMoves[(int)(Math.random()*legalMoves.length)]); + legalMoves = game.getLegalMoves(); + } + Game.Score score = game.getScore(); + IO.println(score.player1Score()); + IO.println(score.player2Score()); + char[][] grid = game.makeBoardAGrid(); + for (char[] chars : grid) { + IO.println(Arrays.toString(chars)); + } + + } + } + + @Test + void testPlayerMustSkipTurnIfNoValidMoves() { + game.play(new Game.Move(19, 'B')); + game.play(new Game.Move(34, 'W')); + game.play(new Game.Move(45, 'B')); + game.play(new Game.Move(11, 'W')); + game.play(new Game.Move(42, 'B')); + game.play(new Game.Move(54, 'W')); + game.play(new Game.Move(37, 'B')); + game.play(new Game.Move(46, 'W')); + game.play(new Game.Move(63, 'B')); + game.play(new Game.Move(62, 'W')); + game.play(new Game.Move(29, 'B')); + game.play(new Game.Move(50, 'W')); + game.play(new Game.Move(55, 'B')); + game.play(new Game.Move(30, 'W')); + game.play(new Game.Move(53, 'B')); + game.play(new Game.Move(38, 'W')); + game.play(new Game.Move(61, 'B')); + game.play(new Game.Move(52, 'W')); + game.play(new Game.Move(51, 'B')); + game.play(new Game.Move(60, 'W')); + game.play(new Game.Move(59, 'B')); + assertEquals('B', game.getCurrentPlayer()); + game.play(ai.findBestMove(game,5)); + game.play(ai.findBestMove(game,5)); + } + + @Test + void testAISelectsLegalMove() { + Game.Move move = ai.findBestMove(game,4); + assertNotNull(move); + assertTrue(containsMove(game.getLegalMoves(),move), "AI should always choose a legal move"); + } + + private boolean containsMove(Game.Move[] moves, Game.Move move) { + for (Game.Move m : moves) { + if (m.equals(move)) return true; + } + return false; + } +}