mirror of
https://github.com/2OOP/pism.git
synced 2026-02-04 19:04:49 +00:00
connect4 with minimax AI
This commit is contained in:
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
13
app/src/main/java/org/toop/app/canvas/Connect4Canvas.java
Normal file
13
app/src/main/java/org/toop/app/canvas/Connect4Canvas.java
Normal file
@@ -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<Integer> onCellClicked) {
|
||||
super(color, width, height, 6, 7, 10, true, onCellClicked);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
263
app/src/main/java/org/toop/app/game/Connect4Game.java
Normal file
263
app/src/main/java/org/toop/app/game/Connect4Game.java
Normal file
@@ -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<Game.Move> 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<String> onMessage) {
|
||||
this.information = information;
|
||||
this.myTurn = myTurn;
|
||||
moveQueue = new LinkedBlockingQueue<Game.Move>();
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
))
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user