From c3f764f33db0eff81ff34ab20f8c34fd46fb75f0 Mon Sep 17 00:00:00 2001 From: Ticho Hidding Date: Sun, 19 Oct 2025 22:01:33 +0200 Subject: [PATCH] connect4 with minimax AI --- .../java/org/toop/app/GameInformation.java | 9 +- app/src/main/java/org/toop/app/Server.java | 9 +- .../org/toop/app/canvas/Connect4Canvas.java | 13 + .../java/org/toop/app/canvas/GameCanvas.java | 51 +++- .../org/toop/app/canvas/TicTacToeCanvas.java | 25 -- .../java/org/toop/app/game/Connect4Game.java | 263 ++++++++++++++++++ .../app/view/views/LocalMultiplayerView.java | 3 + .../org/toop/app/view/views/LocalView.java | 9 +- .../localization/localization_en.properties | 1 + .../localization/localization_nl.properties | 1 + .../java/org/toop/game/Connect4/Connect4.java | 114 ++++++++ .../org/toop/game/Connect4/Connect4AI.java | 63 +++++ 12 files changed, 520 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/org/toop/app/canvas/Connect4Canvas.java create mode 100644 app/src/main/java/org/toop/app/game/Connect4Game.java create mode 100644 game/src/main/java/org/toop/game/Connect4/Connect4.java create mode 100644 game/src/main/java/org/toop/game/Connect4/Connect4AI.java 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..2437b37 100644 --- a/app/src/main/java/org/toop/app/Server.java +++ b/app/src/main/java/org/toop/app/Server.java @@ -1,6 +1,7 @@ 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; @@ -40,7 +41,11 @@ public final class Server { 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; } @@ -123,6 +128,7 @@ public final class Server { 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 CONNECT4: new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; } } }).postEvent(); @@ -166,6 +172,7 @@ public final class Server { 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 CONNECT4: new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break; } } }); 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..7842832 --- /dev/null +++ b/app/src/main/java/org/toop/app/canvas/Connect4Canvas.java @@ -0,0 +1,13 @@ +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, width, height, 6, 7, 10, true, onCellClicked); + } + + +} 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 bc4f1cd..3854f82 100644 --- a/app/src/main/java/org/toop/app/canvas/GameCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/GameCanvas.java @@ -5,6 +5,7 @@ import javafx.scene.canvas.GraphicsContext; import javafx.scene.input.MouseButton; import javafx.scene.paint.Color; +import java.util.Arrays; import java.util.function.Consumer; public abstract class GameCanvas { @@ -48,31 +49,30 @@ 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 * columnSize - gapSize) / columnSize; + final float cellHeight = ((float)height - gapSize * rowSize - gapSize) / rowSize; - for (int y = 0; y < columnSize; y++) { + for (int y = 0; y < rowSize; y++) { final float startY = y * cellHeight + y * gapSize + gapSize; - for (int x = 0; x < rowSize; x++) { + for (int x = 0; x < columnSize; x++) { final float startX = x * cellWidth + x * gapSize + gapSize; - cells[x + y * rowSize] = new Cell(startX, startY, cellWidth, cellHeight); + cells[x + y * columnSize] = new Cell(startX, startY, cellWidth, cellHeight); } } - canvas.setOnMouseClicked(event -> { if (event.getButton() != MouseButton.PRIMARY) { 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) * columnSize); + final int row = (int)((event.getY() / this.height) * rowSize); - final Cell cell = cells[column + row * rowSize]; + final Cell cell = cells[column + row * columnSize]; if (cell.isInside(event.getX(), event.getY())) { event.consume(); - onCellClicked.accept(column + row * rowSize); + onCellClicked.accept(column + row * columnSize); } }); @@ -91,12 +91,12 @@ public abstract class GameCanvas { public void render() { graphics.setFill(color); - for (int x = 0; x < rowSize - 1; x++) { + for (int x = 0; x < columnSize - 1; x++) { final float start = cells[x].x + cells[x].width; graphics.fillRect(start, gapSize, gapSize, height - gapSize * 2); } - for (int y = 0; y < columnSize - 1; y++) { + for (int y = 0; y < rowSize; y++) { final float start = cells[y * rowSize].y + cells[y * rowSize].height; graphics.fillRect(gapSize, start, width - gapSize * 2, gapSize); } @@ -121,6 +121,33 @@ public abstract class GameCanvas { graphics.fillRect(x, y, width, height); } + public void drawX(Color color, int cell) { + graphics.setStroke(color); + graphics.setLineWidth(gapSize); + + 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.strokeLine(x, y, x + width, y + height); + graphics.strokeLine(x + width, y, x, y + height); + } + + public void drawO(Color color, int cell) { + graphics.setStroke(color); + graphics.setLineWidth(gapSize); + + 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.strokeOval(x, y, width, height); + } + public Canvas getCanvas() { return canvas; } 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..451fd3e 100644 --- a/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java +++ b/app/src/main/java/org/toop/app/canvas/TicTacToeCanvas.java @@ -9,30 +9,5 @@ public final class TicTacToeCanvas extends GameCanvas { super(color, width, height, 3, 3, 30, false, onCellClicked); } - public void drawX(Color color, int cell) { - graphics.setStroke(color); - graphics.setLineWidth(gapSize); - 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.strokeLine(x, y, x + width, y + height); - graphics.strokeLine(x + width, y, x, y + height); - } - - public void drawO(Color color, int cell) { - graphics.setStroke(color); - graphics.setLineWidth(gapSize); - - 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.strokeOval(x, y, width, height); - } } \ No newline at end of file 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..d31c91f --- /dev/null +++ b/app/src/main/java/org/toop/app/game/Connect4Game.java @@ -0,0 +1,263 @@ +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 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 AtomicBoolean isRunning; + + public Connect4Game(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer onMessage) { + this.information = information; + this.myTurn = myTurn; + 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(); + } 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); + } + 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); + + 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 = 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); + } else { + view.gameOver(false, information.players[1].name); + } + } else if (state == Game.State.DRAW) { + view.gameOver(false, ""); + } + } + + if (move.value() == 'X') { + canvas.drawX(Color.INDIANRED, move.position()); + } else if (move.value() == 'O') { + canvas.drawO(Color.ROYALBLUE, move.position()); + } + + updateCanvas(); + setGameLabels(game.getCurrentTurn() == myTurn); + } + + private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) { + new EventFlow().addPostEvent(new NetworkEvents.SendCommand(response.clientId(), "MESSAGE hoi")) + .postEvent(); + + 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.clear(); + canvas.render(); + + for (int i = 0; i < game.board.length; i++) { + if (game.board[i] == 'X') { + canvas.drawX(Color.RED, i); + } else if (game.board[i] == 'O') { + canvas.drawO(Color.BLUE, i); + } + } + } + + private void setGameLabels(boolean isMe) { + final int currentTurn = game.getCurrentTurn(); + final char currentValue = currentTurn == 0? 'X' : 'O'; + + view.nextPlayer(isMe, + information.players[isMe? 0 : 1].name, + String.valueOf(currentValue), + information.players[isMe? 1 : 0].name); + } +} 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 a4e72d9..92759cd 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; @@ -45,6 +46,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 2089e5c..d0b81c5 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 @@ -23,10 +23,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/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; + } + + +}