diff --git a/app/src/main/java/org/toop/app/GameInformation.java b/app/src/main/java/org/toop/app/GameInformation.java index 5380003..bca254f 100644 --- a/app/src/main/java/org/toop/app/GameInformation.java +++ b/app/src/main/java/org/toop/app/GameInformation.java @@ -3,12 +3,17 @@ package org.toop.app; public class GameInformation { public enum Type { TICTACTOE, - REVERSI; + REVERSI, + CONNECT4, + BATTLESHIP; + public static int playerCount(Type type) { return switch (type) { case TICTACTOE -> 2; case REVERSI -> 2; + case CONNECT4 -> 2; + case BATTLESHIP -> 2; }; } @@ -16,6 +21,8 @@ public class GameInformation { return switch (type) { 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. + case CONNECT4 -> 7; + case BATTLESHIP -> 5; }; } } diff --git a/app/src/main/java/org/toop/app/Server.java b/app/src/main/java/org/toop/app/Server.java index 8d71c4a..9421004 100644 --- a/app/src/main/java/org/toop/app/Server.java +++ b/app/src/main/java/org/toop/app/Server.java @@ -1,6 +1,6 @@ package org.toop.app; -import com.google.common.util.concurrent.AbstractScheduledService; +import org.toop.app.game.Connect4Game; import org.toop.app.game.ReversiGame; import org.toop.app.game.TicTacToeGame; import org.toop.app.view.ViewStack; @@ -12,34 +12,39 @@ import org.toop.app.view.views.ServerView; import org.toop.framework.eventbus.EventFlow; import org.toop.framework.networking.clients.TournamentNetworkingClient; import org.toop.framework.networking.events.NetworkEvents; -import org.toop.framework.networking.interfaces.NetworkingClient; import org.toop.framework.networking.types.NetworkingConnector; import org.toop.local.AppContext; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; public final class Server { private String user = ""; - private long clientId = -1; - private List onlinePlayers = new CopyOnWriteArrayList(); - private List gameList = new CopyOnWriteArrayList<>(); + + private final List onlinePlayers = new CopyOnWriteArrayList<>(); + private final List gameList = new CopyOnWriteArrayList<>(); private ServerView view; - private boolean isPolling = true; + private AtomicBoolean isSingleGame = new AtomicBoolean(false); + + private ScheduledExecutorService scheduler; + public static GameInformation.Type gameToType(String game) { if (game.equalsIgnoreCase("tic-tac-toe")) { return GameInformation.Type.TICTACTOE; } else if (game.equalsIgnoreCase("reversi")) { return GameInformation.Type.REVERSI; + } else if (game.equalsIgnoreCase("connect4")) { + return GameInformation.Type.CONNECT4; +// } else if (game.equalsIgnoreCase("battleship")) { +// return GameInformation.Type.BATTLESHIP; } return null; @@ -51,7 +56,7 @@ public final class Server { return; } - int parsedPort = -1; + int parsedPort; try { parsedPort = Integer.parseInt(port); @@ -60,18 +65,17 @@ public final class Server { return; } - if (user.isEmpty()) { + if (user.isEmpty() || user.matches("^[0-9].*")) { ViewStack.push(new ErrorView(AppContext.getString("invalid-username"))); return; } new EventFlow() .addPostEvent(NetworkEvents.StartClient.class, - new TournamentNetworkingClient(), - new NetworkingConnector(ip, parsedPort, 10, 1, TimeUnit.SECONDS) + new TournamentNetworkingClient(), + new NetworkingConnector(ip, parsedPort, 10, 1, TimeUnit.SECONDS) ) .onResponse(NetworkEvents.StartClientResponse.class, e -> { - // TODO add if unsuccessful this.user = user; clientId = e.clientId(); @@ -81,39 +85,32 @@ public final class Server { ViewStack.push(view); startPopulateScheduler(); - - populateGameList(); + populateGameList(); }).postEvent(); - new EventFlow().listen(this::handleReceivedChallenge); - } - - private void populatePlayerList(ScheduledExecutorService scheduler, Runnable populatingTask) { - - final long DELAY = 5; - - if (!isPolling) scheduler.shutdown(); - else { - populatingTask.run(); - scheduler.schedule(() -> populatePlayerList(scheduler, populatingTask), DELAY, TimeUnit.SECONDS); - } + new EventFlow().listen(this::handleReceivedChallenge) + .listen(this::handleMatchResponse); } private void sendChallenge(String opponent) { - if (!isPolling) { - return; - } + 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 -> { + new EventFlow().addPostEvent(new NetworkEvents.SendChallenge(clientId, opponent, gameType)).postEvent(); + /* .listen(NetworkEvents.GameMatchResponse.class, e -> { if (e.clientId() == clientId) { isPolling = false; onlinePlayers.clear(); final GameInformation.Type type = gameToType(gameType); - final int myTurn = e.playerToMove().equalsIgnoreCase(e.opponent())? 1 : 0; + if (type == null) { + ViewStack.push(new ErrorView("Unsupported game type: " + gameType)); + return; + } + + final int myTurn = e.playerToMove().equalsIgnoreCase(e.opponent()) ? 1 : 0; final GameInformation information = new GameInformation(type); information.players[0] = playerInformation; @@ -121,54 +118,73 @@ public final class Server { information.players[1].name = opponent; 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; + case TICTACTOE -> new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); + case REVERSI -> new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); + case CONNECT4 -> new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); + default -> ViewStack.push(new ErrorView("Unsupported game type.")); } } - }).postEvent(); + }) */ + ViewStack.pop(); + isSingleGame.set(true); })); } + private void handleMatchResponse(NetworkEvents.GameMatchResponse response) { + if (!isPolling) return; + + String gameType = extractQuotedValue(response.gameType()); + + if (response.clientId() == clientId) { + isPolling = false; + onlinePlayers.clear(); + + final GameInformation.Type type = gameToType(gameType); + if (type == null) { + ViewStack.push(new ErrorView("Unsupported game type: " + gameType)); + return; + } + + final int myTurn = response.playerToMove().equalsIgnoreCase(response.opponent()) ? 1 : 0; + + final GameInformation information = new GameInformation(type); + //information.players[0] = playerInformation; + information.players[0].name = user; + information.players[0].isHuman = false; + information.players[0].computerDifficulty = 5; + information.players[1].name = response.opponent(); + + Runnable onGameOverRunnable = isSingleGame.get()? null: this::gameOver; + + + switch (type) { + case TICTACTOE -> + new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); + case REVERSI -> + new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); + case CONNECT4 -> + new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage, onGameOverRunnable); + default -> ViewStack.push(new ErrorView("Unsupported game type.")); + } + } + } + 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("\"")); - - String gameType = response.gameType(); - gameType = gameType.substring(gameType.indexOf("\"") + 1); - gameType = gameType.substring(0, gameType.indexOf("\"")); + if (!isPolling) return; + String challengerName = extractQuotedValue(response.challengerName()); + String gameType = extractQuotedValue(response.gameType()); final String finalGameType = gameType; - ViewStack.push(new ChallengeView(challengerName, gameType, (playerInformation) -> { - final int challengeId = Integer.parseInt(response.challengeId().substring(18, response.challengeId().length() - 2)); + final int challengeId = Integer.parseInt(response.challengeId().replaceAll("\\D", "")); new EventFlow().addPostEvent(new NetworkEvents.SendAcceptChallenge(clientId, challengeId)).postEvent(); - ViewStack.pop(); + isSingleGame.set(true); - new EventFlow().listen(NetworkEvents.GameMatchResponse.class, e -> { - if (e.clientId() == clientId) { - isPolling = false; - onlinePlayers.clear(); + //new EventFlow().listen(NetworkEvents.GameMatchResponse.class, e -> { - final GameInformation.Type type = gameToType(finalGameType); - final int myTurn = e.playerToMove().equalsIgnoreCase(e.opponent())? 1 : 0; - final GameInformation information = new GameInformation(type); - information.players[0] = playerInformation; - information.players[0].name = user; - information.players[1].name = e.opponent(); - - 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; - } - } - }); + //}); })); } @@ -179,6 +195,7 @@ public final class Server { private void disconnect() { new EventFlow().addPostEvent(new NetworkEvents.CloseClient(clientId)).postEvent(); isPolling = false; + stopScheduler(); ViewStack.push(new OnlineView()); } @@ -188,41 +205,69 @@ public final class Server { private void exitGame() { forfeitGame(); - ViewStack.push(view); startPopulateScheduler(); } + private void gameOver(){ + ViewStack.pop(); + ViewStack.push(view); + startPopulateScheduler(); + + } + private void startPopulateScheduler() { isPolling = true; + isSingleGame.set(false); + stopScheduler(); - EventFlow getPlayerlistFlow = new EventFlow() - .addPostEvent(new NetworkEvents.SendGetPlayerlist(clientId)) + new EventFlow() .listen(NetworkEvents.PlayerlistResponse.class, e -> { - if (e.clientId() == clientId) { - onlinePlayers = new ArrayList<>(List.of(e.playerlist())); - onlinePlayers.removeIf(name -> name.equalsIgnoreCase(user)); + if (e.clientId() == clientId) { + onlinePlayers.clear(); + onlinePlayers.addAll(List.of(e.playerlist())); + onlinePlayers.removeIf(name -> name.equalsIgnoreCase(user)); + view.update(onlinePlayers); + } + }, false); - view.update(onlinePlayers); + scheduler = Executors.newSingleThreadScheduledExecutor(); + scheduler.scheduleAtFixedRate(() -> { + if (isPolling) { + new EventFlow().addPostEvent(new NetworkEvents.SendGetPlayerlist(clientId)).postEvent(); + } else { + stopScheduler(); } - }, false); - - final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - scheduler.schedule(() -> populatePlayerList(scheduler, getPlayerlistFlow::postEvent), 0, TimeUnit.MILLISECONDS); + }, 0, 5, TimeUnit.SECONDS); } - private void gamesListFromServerHandler(NetworkEvents.GamelistResponse event) { - gameList.addAll(List.of(event.gamelist())); - } + private void stopScheduler() { + if (scheduler != null && !scheduler.isShutdown()) { + scheduler.shutdownNow(); + } + } + + private void gamesListFromServerHandler(NetworkEvents.GamelistResponse event) { + gameList.clear(); + gameList.addAll(List.of(event.gamelist())); + } public void populateGameList() { new EventFlow().addPostEvent(new NetworkEvents.SendGetGamelist(clientId)) - .listen(NetworkEvents.GamelistResponse.class, - this::gamesListFromServerHandler, true - ).postEvent(); + .listen(NetworkEvents.GamelistResponse.class, this::gamesListFromServerHandler, true) + .postEvent(); } - public List getGameList() { - return gameList; - } + public List getGameList() { + return gameList; + } + + private String extractQuotedValue(String s) { + int first = s.indexOf('"'); + int last = s.lastIndexOf('"'); + if (first >= 0 && last > first) { + return s.substring(first + 1, last); + } + return s; + } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/canvas/Connect4Canvas.java b/app/src/main/java/org/toop/app/canvas/Connect4Canvas.java new file mode 100644 index 0000000..913bb62 --- /dev/null +++ b/app/src/main/java/org/toop/app/canvas/Connect4Canvas.java @@ -0,0 +1,11 @@ +package org.toop.app.canvas; + +import javafx.scene.paint.Color; + +import java.util.function.Consumer; + +public class Connect4Canvas extends GameCanvas { + public Connect4Canvas(Color color, int width, int height, Consumer onCellClicked) { + super(color, Color.TRANSPARENT, width, height, 7, 6, 10, true, onCellClicked); + } +} \ No newline at end of file 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 52d0521..1a002a6 100644 --- a/app/src/main/java/org/toop/app/canvas/GameCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/GameCanvas.java @@ -1,9 +1,12 @@ package org.toop.app.canvas; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.input.MouseButton; import javafx.scene.paint.Color; +import javafx.util.Duration; import java.util.function.Consumer; @@ -19,6 +22,7 @@ public abstract class GameCanvas { protected final GraphicsContext graphics; protected final Color color; + protected final Color backgroundColor; protected final int width; protected final int height; @@ -31,11 +35,12 @@ public abstract class GameCanvas { protected final Cell[] cells; - protected GameCanvas(Color color, int width, int height, int rowSize, int columnSize, int gapSize, boolean edges, Consumer onCellClicked) { + protected GameCanvas(Color color, Color backgroundColor, int width, int height, int rowSize, int columnSize, int gapSize, boolean edges, Consumer onCellClicked) { canvas = new Canvas(width, height); graphics = canvas.getGraphicsContext2D(); this.color = color; + this.backgroundColor = backgroundColor; this.width = width; this.height = height; @@ -48,8 +53,8 @@ public abstract class GameCanvas { cells = new Cell[rowSize * columnSize]; - final float cellWidth = ((float)width - gapSize * rowSize - gapSize) / rowSize; - final float cellHeight = ((float)height - gapSize * columnSize - gapSize) / columnSize; + final float cellWidth = ((float) width - gapSize * rowSize - gapSize) / rowSize; + final float cellHeight = ((float) height - gapSize * columnSize - gapSize) / columnSize; for (int y = 0; y < columnSize; y++) { final float startY = y * cellHeight + y * gapSize + gapSize; @@ -65,8 +70,8 @@ public abstract class GameCanvas { return; } - final int column = (int)((event.getX() / this.width) * rowSize); - final int row = (int)((event.getY() / this.height) * columnSize); + final int column = (int) ((event.getX() / this.width) * rowSize); + final int row = (int) ((event.getY() / this.height) * columnSize); final Cell cell = cells[column + row * rowSize]; @@ -79,11 +84,10 @@ public abstract class GameCanvas { render(); } - public void clear() { - graphics.clearRect(0, 0, width, height); - } + private void render() { + graphics.setFill(backgroundColor); + graphics.fillRect(0, 0, width, height); - public void render() { graphics.setFill(color); for (int x = 0; x < rowSize - 1; x++) { @@ -116,6 +120,76 @@ public abstract class GameCanvas { graphics.fillRect(x, y, width, height); } + public void clear(int cell) { + final float x = cells[cell].x(); + final float y = cells[cell].y(); + + final float width = cells[cell].width(); + final float height = cells[cell].height(); + + graphics.clearRect(x, y, width, height); + + graphics.setFill(backgroundColor); + graphics.fillRect(x, y, width, height); + } + + public void clearAll() { + for (int i = 0; i < cells.length; i++) { + clear(i); + } + } + + 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); + } + + private void drawDotScaled(Color color, int cell, double scale) { + final float cx = cells[cell].x() + gapSize; + final float cy = cells[cell].y() + gapSize; + + final float fullWidth = cells[cell].width() - gapSize * 2; + final float height = cells[cell].height() - gapSize * 2; + + final float scaledWidth = (float)(fullWidth * scale); + final float offsetX = (fullWidth - scaledWidth) / 2; + + graphics.setFill(color); + graphics.fillOval(cx + offsetX, cy, scaledWidth, height); + } + + public Timeline flipDot(Color fromColor, Color toColor, int cell) { + final int steps = 60; + final long duration = 250; + final double interval = duration / (double) steps; + + final Timeline timeline = new Timeline(); + + for (int i = 0; i <= steps; i++) { + final double t = i / (double) steps; + final KeyFrame keyFrame = new KeyFrame(Duration.millis(i * interval), + _ -> { + clear(cell); + + final double scale = t <= 0.5 ? 1 - 2 * t : 2 * t - 1; + final Color currentColor = t < 0.5 ? fromColor : toColor; + + drawDotScaled(currentColor, cell, scale); + } + ); + + timeline.getKeyFrames().add(keyFrame); + } + + return timeline; + } + public Canvas getCanvas() { return canvas; } diff --git a/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java b/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java index 277937c..8eec0ad 100644 --- a/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/ReversiCanvas.java @@ -6,21 +6,10 @@ 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); + super(color, Color.GREEN, width, height, 8, 8, 5, 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); diff --git a/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java b/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java index 06d0b31..d7ccbc8 100644 --- a/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java @@ -6,7 +6,7 @@ import java.util.function.Consumer; public final class TicTacToeCanvas extends GameCanvas { public TicTacToeCanvas(Color color, int width, int height, Consumer onCellClicked) { - super(color, width, height, 3, 3, 30, false, onCellClicked); + super(color, Color.TRANSPARENT, width, height, 3, 3, 30, false, onCellClicked); } public void drawX(Color color, int cell) { diff --git a/app/src/main/java/org/toop/app/game/Connect4Game.java b/app/src/main/java/org/toop/app/game/Connect4Game.java new file mode 100644 index 0000000..bd063b0 --- /dev/null +++ b/app/src/main/java/org/toop/app/game/Connect4Game.java @@ -0,0 +1,274 @@ +package org.toop.app.game; + +import javafx.geometry.Pos; +import javafx.scene.paint.Color; +import org.toop.app.App; +import org.toop.app.GameInformation; +import org.toop.app.canvas.Connect4Canvas; +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.Connect4.Connect4; +import org.toop.game.Connect4.Connect4AI; +import org.toop.game.Game; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public class Connect4Game { + private final GameInformation information; + + private final int myTurn; + private Runnable onGameOver; + private final BlockingQueue moveQueue; + + private final Connect4 game; + private final Connect4AI ai; + private final int columnSize = 7; + private final int rowSize = 6; + + private final GameView view; + private final Connect4Canvas canvas; + + private final AtomicBoolean isRunning; + + public Connect4Game(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage, Runnable onGameOver) { + this.information = information; + this.myTurn = myTurn; + this.onGameOver = onGameOver; + moveQueue = new LinkedBlockingQueue(); + + + game = new Connect4(); + ai = new Connect4AI(); + + 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 Connect4Canvas(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? 'X' : 'O'; + + try { + moveQueue.put(new Game.Move(cell%columnSize, value)); + } catch (InterruptedException _) {} + } + } else { + if (information.players[0].isHuman) { + final char value = myTurn == 0? 'X' : 'O'; + + try { + moveQueue.put(new Game.Move(cell%columnSize, value)); + } catch (InterruptedException _) {} + } + } + }); + + view.add(Pos.CENTER, canvas.getCanvas()); + ViewStack.push(view); + + if (onForfeit == null || onExit == null) { + new Thread(this::localGameThread).start(); + setGameLabels(information.players[0].isHuman); + } 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 Connect4Game(GameInformation information) { + this(information, 0, null, null, null, null); + } + private void localGameThread() { + while (isRunning.get()) { + final int currentTurn = game.getCurrentTurn(); + final String currentValue = currentTurn == 0? "RED" : "BLUE"; + final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); + + view.nextPlayer(information.players[currentTurn].isHuman, + information.players[currentTurn].name, + 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 = Math.abs(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 (move.value() == 'X') { + canvas.drawX(Color.INDIANRED, move.position()); + } else if (move.value() == 'O') { + canvas.drawO(Color.ROYALBLUE, move.position()); + } +*/ + 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? 'X' : 'O'; + } else { + playerChar = myTurn == 0? 'O' : 'X'; + } + + 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); + gameOver(); + } else { + view.gameOver(false, information.players[1].name); + gameOver(); + } + } else if (state == Game.State.DRAW) { + view.gameOver(false, ""); + gameOver(); + } + } + + if (move.value() == 'X') { + canvas.drawDot(Color.INDIANRED, move.position()); + } else if (move.value() == 'O') { + canvas.drawDot(Color.ROYALBLUE, move.position()); + } + + updateCanvas(); + setGameLabels(game.getCurrentTurn() == myTurn); + } + + private void gameOver() { + if (onGameOver == null){ + return; + } + isRunning.set(false); + onGameOver.run(); + } + + 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() { + canvas.clearAll(); + + for (int i = 0; i < game.board.length; i++) { + if (game.board[i] == 'X') { + canvas.drawDot(Color.RED, i); + } else if (game.board[i] == 'O') { + canvas.drawDot(Color.BLUE, i); + } + } + } + + private void setGameLabels(boolean isMe) { + final int currentTurn = game.getCurrentTurn(); + final String currentValue = currentTurn == 0? "RED" : "BLUE"; + + view.nextPlayer(isMe, + information.players[isMe? 0 : 1].name, + currentValue, + information.players[isMe? 1 : 0].name); + } +} diff --git a/app/src/main/java/org/toop/app/game/ReversiGame.java b/app/src/main/java/org/toop/app/game/ReversiGame.java index 18527fe..64f0b6c 100644 --- a/app/src/main/java/org/toop/app/game/ReversiGame.java +++ b/app/src/main/java/org/toop/app/game/ReversiGame.java @@ -1,5 +1,6 @@ package org.toop.app.game; +import javafx.animation.SequentialTransition; import org.toop.app.App; import org.toop.app.GameInformation; import org.toop.app.canvas.ReversiCanvas; @@ -24,7 +25,8 @@ public final class ReversiGame { private final GameInformation information; private final int myTurn; - private final BlockingQueue moveQueue; + private Runnable onGameOver; + private final BlockingQueue moveQueue; private final Reversi game; private final ReversiAI ai; @@ -33,17 +35,20 @@ public final class ReversiGame { private final ReversiCanvas canvas; private final AtomicBoolean isRunning; + private final AtomicBoolean isPaused; - public ReversiGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage) { + public ReversiGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage, Runnable onGameOver) { this.information = information; this.myTurn = myTurn; - moveQueue = new LinkedBlockingQueue(); + this.onGameOver = onGameOver; + moveQueue = new LinkedBlockingQueue(); game = new Reversi(); ai = new ReversiAI(); isRunning = new AtomicBoolean(true); + isPaused = new AtomicBoolean(false); if (onForfeit == null || onExit == null) { view = new GameView(null, () -> { @@ -57,7 +62,7 @@ public final class ReversiGame { }, onMessage); } - canvas = new ReversiCanvas(Color.GRAY, + canvas = new ReversiCanvas(Color.BLACK, (App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3, (cell) -> { if (onForfeit == null || onExit == null) { @@ -84,6 +89,7 @@ public final class ReversiGame { if (onForfeit == null || onExit == null) { new Thread(this::localGameThread).start(); + setGameLabels(information.players[0].isHuman); } else { new EventFlow() .listen(NetworkEvents.GameMoveResponse.class, this::onMoveResponse) @@ -93,22 +99,30 @@ public final class ReversiGame { setGameLabels(myTurn == 0); } - updateCanvas(); + updateCanvas(false); } public ReversiGame(GameInformation information) { - this(information, 0, null, null, null); + this(information, 0, null, null, null,null); } private void localGameThread() { while (isRunning.get()) { + if (isPaused.get()) { + try { + Thread.sleep(200); + } catch (InterruptedException _) {} + + continue; + } + final int currentTurn = game.getCurrentTurn(); - final char currentValue = currentTurn == 0? 'B' : 'W'; + final String currentValue = currentTurn == 0? "BLACK" : "WHITE"; final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); view.nextPlayer(information.players[currentTurn].isHuman, information.players[currentTurn].name, - String.valueOf(currentValue), + currentValue, information.players[nextTurn].name); Game.Move move = null; @@ -146,7 +160,7 @@ public final class ReversiGame { } final Game.State state = game.play(move); - updateCanvas(); + updateCanvas(true); if (state != Game.State.NORMAL) { if (state == Game.State.WIN) { @@ -180,18 +194,29 @@ public final class ReversiGame { if (state == Game.State.WIN) { if (response.player().equalsIgnoreCase(information.players[0].name)) { view.gameOver(true, information.players[0].name); + gameOver(); } else { view.gameOver(false, information.players[1].name); + gameOver(); } } else if (state == Game.State.DRAW) { view.gameOver(false, ""); + game.play(move); } } - updateCanvas(); + updateCanvas(false); setGameLabels(game.getCurrentTurn() == myTurn); } + private void gameOver() { + if (onGameOver == null){ + return; + } + isRunning.set(false); + onGameOver.run(); + } + private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) { if (!isRunning.get()) { return; @@ -224,11 +249,9 @@ public final class ReversiGame { view.updateChat(msg.message()); } - private void updateCanvas() { + private void updateCanvas(boolean animate) { // Todo: this is very inefficient. still very fast but if the grid is bigger it might cause issues. improve. - - canvas.clear(); - canvas.render(); + canvas.clearAll(); for (int i = 0; i < game.board.length; i++) { if (game.board[i] == 'B') { @@ -238,20 +261,44 @@ public final class ReversiGame { } } - final Game.Move[] legalMoves = game.getLegalMoves(); + final Game.Move[] flipped = game.getMostRecentlyFlippedPieces(); - for (final Game.Move legalMove : legalMoves) { - canvas.drawLegalPosition(legalMove.position()); + final SequentialTransition animation = new SequentialTransition(); + isPaused.set(true); + + if (animate && flipped != null) { + for (final Game.Move flip : flipped) { + canvas.clear(flip.position()); + + final Color from = flip.value() == 'W' ? Color.BLACK : Color.WHITE; + final Color to = flip.value() == 'W' ? Color.WHITE : Color.BLACK; + + canvas.drawDot(from, flip.position()); + + animation.getChildren().addFirst(canvas.flipDot(from, to, flip.position())); + } } + + animation.setOnFinished(_ -> { + isPaused.set(false); + + final Game.Move[] legalMoves = game.getLegalMoves(); + + for (final Game.Move legalMove : legalMoves) { + canvas.drawLegalPosition(legalMove.position()); + } + }); + + animation.play(); } private void setGameLabels(boolean isMe) { final int currentTurn = game.getCurrentTurn(); - final char currentValue = currentTurn == 0? 'B' : 'W'; + final String currentValue = currentTurn == 0? "BLACK" : "WHITE"; view.nextPlayer(isMe, information.players[isMe? 0 : 1].name, - String.valueOf(currentValue), + 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 cddef0a..1ca98a9 100644 --- a/app/src/main/java/org/toop/app/game/TicTacToeGame.java +++ b/app/src/main/java/org/toop/app/game/TicTacToeGame.java @@ -24,7 +24,8 @@ public final class TicTacToeGame { private final GameInformation information; private final int myTurn; - private final BlockingQueue moveQueue; + private Runnable onGameOver; + private final BlockingQueue moveQueue; private final TicTacToe game; private final TicTacToeAI ai; @@ -32,13 +33,14 @@ public final class TicTacToeGame { private final GameView view; private final TicTacToeCanvas canvas; - private AtomicBoolean isRunning; + private final AtomicBoolean isRunning; - public TicTacToeGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage) { + public TicTacToeGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage, Runnable onGameOver) { this.information = information; this.myTurn = myTurn; - moveQueue = new LinkedBlockingQueue(); + this.onGameOver = onGameOver; + moveQueue = new LinkedBlockingQueue(); game = new TicTacToe(); ai = new TicTacToeAI(); @@ -95,18 +97,18 @@ public final class TicTacToeGame { } public TicTacToeGame(GameInformation information) { - this(information, 0, null, null, null); + this(information, 0, null, null, null, null); } private void localGameThread() { while (isRunning.get()) { final int currentTurn = game.getCurrentTurn(); - final char currentValue = currentTurn == 0? 'X' : 'O'; + final String currentValue = currentTurn == 0? "X" : "O"; final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type); view.nextPlayer(information.players[currentTurn].isHuman, information.players[currentTurn].name, - String.valueOf(currentValue), + currentValue, information.players[nextTurn].name); Game.Move move = null; @@ -183,11 +185,16 @@ public final class TicTacToeGame { if (state == Game.State.WIN) { if (response.player().equalsIgnoreCase(information.players[0].name)) { view.gameOver(true, information.players[0].name); + gameOver(); } else { view.gameOver(false, information.players[1].name); + gameOver(); } } else if (state == Game.State.DRAW) { - view.gameOver(false, ""); + if(game.getLegalMoves().length == 0) { //only return draw in online multiplayer if the game is actually over. + view.gameOver(false, ""); + gameOver(); + } } } @@ -200,6 +207,14 @@ public final class TicTacToeGame { setGameLabels(game.getCurrentTurn() == myTurn); } + private void gameOver() { + if (onGameOver == null){ + return; + } + isRunning.set(false); + onGameOver.run(); + } + private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) { if (!isRunning.get()) { return; @@ -214,7 +229,12 @@ public final class TicTacToeGame { position = moveQueue.take().position(); } catch (InterruptedException _) {} } else { - final Game.Move move = ai.findBestMove(game, information.players[0].computerDifficulty); + final Game.Move move; + if (information.players[1].name.equalsIgnoreCase("pism")) { + move = ai.findWorstMove(game,9); + }else{ + move = ai.findBestMove(game, information.players[0].computerDifficulty); + } assert move != null; position = move.position(); @@ -234,11 +254,11 @@ public final class TicTacToeGame { private void setGameLabels(boolean isMe) { final int currentTurn = game.getCurrentTurn(); - final char currentValue = currentTurn == 0? 'X' : 'O'; + final String currentValue = currentTurn == 0? "X" : "O"; view.nextPlayer(isMe, information.players[isMe? 0 : 1].name, - String.valueOf(currentValue), + currentValue, information.players[isMe? 1 : 0].name); } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.java b/app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.java deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/org/toop/app/view/views/ChallengeView.java b/app/src/main/java/org/toop/app/view/views/ChallengeView.java index 55b526f..a403de9 100644 --- a/app/src/main/java/org/toop/app/view/views/ChallengeView.java +++ b/app/src/main/java/org/toop/app/view/views/ChallengeView.java @@ -56,7 +56,7 @@ public final class ChallengeView extends View { final Button denyButton = button(); denyButton.setText(AppContext.getString("deny")); denyButton.setOnAction(_ -> { - ViewStack.pop(); + ViewStack.pop(); }); final List nodes = new ArrayList<>(); 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 0ea669b..df69240 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 @@ -1,5 +1,6 @@ package org.toop.app.view.views; +import javafx.application.Platform; import org.toop.app.view.View; import org.toop.app.view.ViewStack; import org.toop.app.view.displays.SongDisplay; @@ -151,16 +152,19 @@ public final class GameView extends View { } public void nextPlayer(boolean isMe, String currentPlayer, String currentMove, String nextPlayer) { - currentPlayerHeader.setText(currentPlayer); - currentMoveHeader.setText(currentMove); + Platform.runLater(() -> { + currentPlayerHeader.setText(currentPlayer); + currentMoveHeader.setText(currentMove); - nextPlayerHeader.setText(nextPlayer); + nextPlayerHeader.setText(nextPlayer); + + if (isMe) { + currentPlayerHeader.getStyleClass().add("my-turn"); + } else { + currentPlayerHeader.getStyleClass().remove("my-turn"); + } + }); - if (isMe) { - currentPlayerHeader.getStyleClass().add("my-turn"); - } else { - currentPlayerHeader.getStyleClass().remove("my-turn"); - } } public void updateChat(String message) { 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 88c9e84..e99de78 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.Connect4Game; import org.toop.app.game.ReversiGame; import org.toop.app.game.TicTacToeGame; import org.toop.app.view.View; @@ -46,6 +47,8 @@ public final class LocalMultiplayerView extends View { switch (information.type) { case TICTACTOE: new TicTacToeGame(information); break; case REVERSI: new ReversiGame(information); break; + case CONNECT4: new Connect4Game(information); break; + //case BATTLESHIP: new BattleshipGame(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 550bdb9..c4a1c4b 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 @@ -24,10 +24,15 @@ public final class LocalView extends View { reversiButton.setText(AppContext.getString("reversi")); reversiButton.setOnAction(_ -> { ViewStack.push(new LocalMultiplayerView(GameInformation.Type.REVERSI)); }); - add(Pos.CENTER, + final Button connect4Button = button(); + connect4Button.setText(AppContext.getString("connect4")); + connect4Button.setOnAction(_ -> { ViewStack.push(new LocalMultiplayerView(GameInformation.Type.CONNECT4)); }); + + add(Pos.CENTER, fit(vboxFill( ticTacToeButton, - reversiButton + reversiButton, + connect4Button )) ); diff --git a/app/src/main/java/org/toop/app/view/views/SendChallengeView.java b/app/src/main/java/org/toop/app/view/views/SendChallengeView.java index 5d88a13..e7635f7 100644 --- a/app/src/main/java/org/toop/app/view/views/SendChallengeView.java +++ b/app/src/main/java/org/toop/app/view/views/SendChallengeView.java @@ -55,7 +55,8 @@ public final class SendChallengeView extends View { final Button cancelButton = button(); cancelButton.setText(AppContext.getString("cancel")); - cancelButton.setOnAction(_ -> { ViewStack.pop(); }); + cancelButton.setOnAction(_ -> { + ViewStack.pop(); }); final List nodes = new ArrayList<>(); diff --git a/app/src/main/resources/assets/localization/localization_en.properties b/app/src/main/resources/assets/localization/localization_en.properties index 588bb32..48eb167 100644 --- a/app/src/main/resources/assets/localization/localization_en.properties +++ b/app/src/main/resources/assets/localization/localization_en.properties @@ -62,6 +62,7 @@ style=Style the-game-ended-in-a-draw=The game ended in a draw theme=Theme tic-tac-toe=Tic Tac Toe +connect4=Connect 4 to-a-game-of=to a game of volume=Volume windowed=Windowed diff --git a/app/src/main/resources/assets/localization/localization_nl.properties b/app/src/main/resources/assets/localization/localization_nl.properties index 4890a20..2042468 100644 --- a/app/src/main/resources/assets/localization/localization_nl.properties +++ b/app/src/main/resources/assets/localization/localization_nl.properties @@ -62,6 +62,7 @@ style=Stijl the-game-ended-in-a-draw=Het spel eindigde in een gelijkspel theme=Thema tic-tac-toe=Boter Kaas en Eieren +connect4=Vier op een rij to-a-game-of=voor een spelletje volume=Volume windowed=Venstermodus diff --git a/game/src/main/java/org/toop/game/Connect4/Connect4.java b/game/src/main/java/org/toop/game/Connect4/Connect4.java new file mode 100644 index 0000000..45f776d --- /dev/null +++ b/game/src/main/java/org/toop/game/Connect4/Connect4.java @@ -0,0 +1,114 @@ +package org.toop.game.Connect4; + +import org.toop.game.TurnBasedGame; + +import java.util.ArrayList; + +public class Connect4 extends TurnBasedGame { + private int movesLeft; + + public Connect4() { + super(6, 7, 2); + movesLeft = board.length; + } + + public Connect4(Connect4 other) { + super(other); + movesLeft = other.movesLeft; + } + + @Override + public Move[] getLegalMoves() { + final ArrayList legalMoves = new ArrayList<>(); + final char currentValue = getCurrentValue(); + + for (int i = 0; i < columnSize; i++) { + if (board[i] == EMPTY) { + legalMoves.add(new Move(i, currentValue)); + } + } + return legalMoves.toArray(new Move[0]); + } + + @Override + public State play(Move move) { + assert move != null; + assert move.position() >= 0 && move.position() < board.length; + assert move.value() == getCurrentValue(); + + int lowestEmptySpot = move.position(); + for (int i = 0; i < rowSize; i++) { + int checkMovePosition = move.position() + columnSize * i; + if (checkMovePosition < board.length) { + if (board[checkMovePosition] == EMPTY) { + lowestEmptySpot = checkMovePosition; + } + } + } + board[lowestEmptySpot] = move.value(); + movesLeft--; + + if (checkForWin()) { + return State.WIN; + } + + nextTurn(); + + + return State.NORMAL; + } + + private boolean checkForWin() { + char[][] boardGrid = makeBoardAGrid(); + + for (int row = 0; row < rowSize; row++) { + for (int col = 0; col < columnSize; col++) { + char cell = boardGrid[row][col]; + if (cell == ' ' || cell == 0) continue; + + if (col + 3 < columnSize && + cell == boardGrid[row][col + 1] && + cell == boardGrid[row][col + 2] && + cell == boardGrid[row][col + 3]) { + return true; + } + + if (row + 3 < rowSize && + cell == boardGrid[row + 1][col] && + cell == boardGrid[row + 2][col] && + cell == boardGrid[row + 3][col]) { + return true; + } + + if (row + 3 < rowSize && col + 3 < columnSize && + cell == boardGrid[row + 1][col + 1] && + cell == boardGrid[row + 2][col + 2] && + cell == boardGrid[row + 3][col + 3]) { + return true; + } + + if (row + 3 < rowSize && col - 3 >= 0 && + cell == boardGrid[row + 1][col - 1] && + cell == boardGrid[row + 2][col - 2] && + cell == boardGrid[row + 3][col - 3]) { + return true; + } + } + } + return false; + } + + public char[][] makeBoardAGrid() { + char[][] boardGrid = new char[rowSize][columnSize]; + for (int i = 0; i < rowSize*columnSize; i++) { + boardGrid[i / columnSize][i % columnSize] = board[i]; //boardGrid[y -> row] [x -> column] + } + return boardGrid; + } + + private char getCurrentValue() { + return currentTurn == 0 ? 'X' : 'O'; + } +} + + diff --git a/game/src/main/java/org/toop/game/Connect4/Connect4AI.java b/game/src/main/java/org/toop/game/Connect4/Connect4AI.java new file mode 100644 index 0000000..420ab0d --- /dev/null +++ b/game/src/main/java/org/toop/game/Connect4/Connect4AI.java @@ -0,0 +1,63 @@ +package org.toop.game.Connect4; + +import org.toop.game.AI; +import org.toop.game.Game; +import org.toop.game.tictactoe.TicTacToe; + +public class Connect4AI extends AI { + + + public Game.Move findBestMove(Connect4 game, int depth) { + assert game != null; + assert depth >= 0; + + final Game.Move[] legalMoves = game.getLegalMoves(); + + if (legalMoves.length <= 0) { + return null; + } + + int bestScore = -depth; + Game.Move bestMove = null; + + for (final Game.Move move : legalMoves) { + final int score = getMoveScore(game, depth, move, true); + + if (score > bestScore) { + bestMove = move; + bestScore = score; + } + } + + return bestMove != null? bestMove : legalMoves[(int)(Math.random() * legalMoves.length)]; + } + + private int getMoveScore(Connect4 game, int depth, Game.Move move, boolean maximizing) { + final Connect4 copy = new Connect4(game); + final Game.State state = copy.play(move); + + switch (state) { + case Game.State.DRAW: return 0; + case Game.State.WIN: return maximizing? depth + 1 : -depth - 1; + } + + if (depth <= 0) { + return 0; + } + + final Game.Move[] legalMoves = copy.getLegalMoves(); + int score = maximizing? depth + 1 : -depth - 1; + + for (final Game.Move next : legalMoves) { + if (maximizing) { + score = Math.min(score, getMoveScore(copy, depth - 1, next, false)); + } else { + score = Math.max(score, getMoveScore(copy, depth - 1, next, true)); + } + } + + return score; + } + + +} diff --git a/game/src/main/java/org/toop/game/reversi/Reversi.java b/game/src/main/java/org/toop/game/reversi/Reversi.java index e378b45..e86de86 100644 --- a/game/src/main/java/org/toop/game/reversi/Reversi.java +++ b/game/src/main/java/org/toop/game/reversi/Reversi.java @@ -15,6 +15,7 @@ public final class Reversi extends TurnBasedGame { private int movesTaken; public static final char FIRST_MOVE = 'B'; private Set filledCells = new HashSet<>(); + private Move[] mostRecentlyFlippedPieces; public Reversi() { super(8, 8, 2); @@ -138,27 +139,37 @@ public final class Reversi extends TurnBasedGame { } } if (moveIsLegal) { - Move[] moves = getFlipsForPotentialMove(new Point(move.position()%columnSize,move.position()/rowSize), makeBoardAGrid(), move.value()); + Move[] moves = sortMovesFromCenter(getFlipsForPotentialMove(new Point(move.position()%columnSize,move.position()/rowSize), makeBoardAGrid(), move.value()),move); + mostRecentlyFlippedPieces = moves; 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; + if (getLegalMoves().length > 0) { + return State.MOVE_SKIPPED; + } + else { + Score score = getScore(); + if (score.player1Score() == score.player2Score()) { + return State.DRAW; + } + else { + return State.WIN; + } + } } return State.NORMAL; } - return null; } public void skipMyTurn(){ IO.println("TURN " + getCurrentPlayer() + " SKIPPED"); + //todo notify user that a turn has been skipped nextTurn(); } @@ -183,14 +194,32 @@ public final class Reversi extends TurnBasedGame { public Game.Score getScore(){ int player1Score = 0, player2Score = 0; for (int count = 0; count < rowSize * columnSize; count++) { - if (board[count] == 'W') { + if (board[count] == 'B') { player1Score += 1; } - if (board[count] == 'B') { + if (board[count] == 'W') { player2Score += 1; } } return new Game.Score(player1Score, player2Score); } + public Move[] sortMovesFromCenter(Move[] moves, Move center) { + int centerX = center.position()%columnSize; + int centerY = center.position()/rowSize; + Arrays.sort(moves, (a, b) -> { + int dxA = a.position()%columnSize - centerX; + int dyA = a.position()/rowSize - centerY; + int dxB = b.position()%columnSize - centerX; + int dyB = b.position()/rowSize - centerY; + int distA = dxA * dxA + dyA * dyA; + int distB = dxB * dxB + dyB * dyB; + + return Integer.compare(distA, distB); + }); + return moves; + } + public Move[] getMostRecentlyFlippedPieces() { + return mostRecentlyFlippedPieces; + } } \ 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 index 4f0865e..a532a62 100644 --- a/game/src/main/java/org/toop/game/reversi/ReversiAI.java +++ b/game/src/main/java/org/toop/game/reversi/ReversiAI.java @@ -6,6 +6,8 @@ import org.toop.game.Game; public final class ReversiAI extends AI { @Override public Game.Move findBestMove(Reversi game, int depth) { - return game.getLegalMoves()[0]; + Game.Move[] moves = game.getLegalMoves(); + int inty = (int)(Math.random() * moves.length-.5f); + return moves[inty]; } } diff --git a/game/src/main/java/org/toop/game/tictactoe/TicTacToeAI.java b/game/src/main/java/org/toop/game/tictactoe/TicTacToeAI.java index afc61b8..e780976 100644 --- a/game/src/main/java/org/toop/game/tictactoe/TicTacToeAI.java +++ b/game/src/main/java/org/toop/game/tictactoe/TicTacToeAI.java @@ -11,7 +11,7 @@ public final class TicTacToeAI extends AI { final Game.Move[] legalMoves = game.getLegalMoves(); - if (legalMoves.length <= 0) { + if (legalMoves.length == 0) { return null; } @@ -38,6 +38,24 @@ public final class TicTacToeAI extends AI { return bestMove != null? bestMove : legalMoves[(int)(Math.random() * legalMoves.length)]; } + public Game.Move findWorstMove(TicTacToe game, int depth){ + + + Game.Move[] legalMoves = game.getLegalMoves(); + + int bestScore = -depth; + Game.Move bestMove = null; + + for (final Game.Move move : legalMoves) { + final int score = getMoveScore(game, depth, move, false); + + if (score > bestScore) { + bestMove = move; + bestScore = score; + } + } + return bestMove; + } private int getMoveScore(TicTacToe game, int depth, Game.Move move, boolean maximizing) { final TicTacToe copy = new TicTacToe(game); diff --git a/game/src/test/java/org/toop/game/tictactoe/ReversiTest.java b/game/src/test/java/org/toop/game/tictactoe/ReversiTest.java index f251b89..fa3b897 100644 --- a/game/src/test/java/org/toop/game/tictactoe/ReversiTest.java +++ b/game/src/test/java/org/toop/game/tictactoe/ReversiTest.java @@ -149,6 +149,34 @@ class ReversiTest { game.play(ai.findBestMove(game,5)); } + @Test + void testGameShouldEndIfNoValidMoves() { + //European Grand Prix Ghent 2017: Replay Hassan - Verstuyft J. (3-17) + game.play(new Game.Move(19, 'B')); + game.play(new Game.Move(20, 'W')); + game.play(new Game.Move(29, 'B')); + game.play(new Game.Move(22, 'W')); + game.play(new Game.Move(21, 'B')); + game.play(new Game.Move(34, 'W')); + game.play(new Game.Move(23, 'B')); + game.play(new Game.Move(13, 'W')); + game.play(new Game.Move(26, 'B')); + game.play(new Game.Move(18, 'W')); + game.play(new Game.Move(12, 'B')); + game.play(new Game.Move(4, 'W')); + game.play(new Game.Move(17, 'B')); + game.play(new Game.Move(31, 'W')); + Game.State stateTurn15 = game.play(new Game.Move(39, 'B')); + assertEquals(Game.State.NORMAL, stateTurn15); + Game.State stateTurn16 = game.play(new Game.Move(16, 'W')); + assertEquals(Game.State.WIN, stateTurn16); + Game.State stateTurn17 = game.play(new Game.Move(5, 'B')); + assertNull(stateTurn17); + Game.Score score = game.getScore(); + assertEquals(3, score.player1Score()); + assertEquals(17, score.player2Score()); + } + @Test void testAISelectsLegalMove() { Game.Move move = ai.findBestMove(game,4);