mirror of
https://github.com/2OOP/pism.git
synced 2026-02-04 19:04:49 +00:00
Merge branch 'Demo3' into Development
This commit is contained in:
@@ -3,12 +3,17 @@ package org.toop.app;
|
|||||||
public class GameInformation {
|
public class GameInformation {
|
||||||
public enum Type {
|
public enum Type {
|
||||||
TICTACTOE,
|
TICTACTOE,
|
||||||
REVERSI;
|
REVERSI,
|
||||||
|
CONNECT4,
|
||||||
|
BATTLESHIP;
|
||||||
|
|
||||||
|
|
||||||
public static int playerCount(Type type) {
|
public static int playerCount(Type type) {
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
case TICTACTOE -> 2;
|
case TICTACTOE -> 2;
|
||||||
case REVERSI -> 2;
|
case REVERSI -> 2;
|
||||||
|
case CONNECT4 -> 2;
|
||||||
|
case BATTLESHIP -> 2;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,6 +21,8 @@ public class GameInformation {
|
|||||||
return switch (type) {
|
return switch (type) {
|
||||||
case TICTACTOE -> 5; // Todo. 5 seems to always draw or win. could increase to 9 but that might affect performance
|
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 REVERSI -> 10; // Todo. 10 is a guess. might be too slow or too bad.
|
||||||
|
case CONNECT4 -> 7;
|
||||||
|
case BATTLESHIP -> 5;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package org.toop.app;
|
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.ReversiGame;
|
||||||
import org.toop.app.game.TicTacToeGame;
|
import org.toop.app.game.TicTacToeGame;
|
||||||
import org.toop.app.view.ViewStack;
|
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.eventbus.EventFlow;
|
||||||
import org.toop.framework.networking.clients.TournamentNetworkingClient;
|
import org.toop.framework.networking.clients.TournamentNetworkingClient;
|
||||||
import org.toop.framework.networking.events.NetworkEvents;
|
import org.toop.framework.networking.events.NetworkEvents;
|
||||||
import org.toop.framework.networking.interfaces.NetworkingClient;
|
|
||||||
import org.toop.framework.networking.types.NetworkingConnector;
|
import org.toop.framework.networking.types.NetworkingConnector;
|
||||||
import org.toop.local.AppContext;
|
import org.toop.local.AppContext;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public final class Server {
|
public final class Server {
|
||||||
private String user = "";
|
private String user = "";
|
||||||
|
|
||||||
private long clientId = -1;
|
private long clientId = -1;
|
||||||
private List<String> onlinePlayers = new CopyOnWriteArrayList<String>();
|
|
||||||
private List<String> gameList = new CopyOnWriteArrayList<>();
|
private final List<String> onlinePlayers = new CopyOnWriteArrayList<>();
|
||||||
|
private final List<String> gameList = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
private ServerView view;
|
private ServerView view;
|
||||||
|
|
||||||
private boolean isPolling = true;
|
private boolean isPolling = true;
|
||||||
|
|
||||||
|
private AtomicBoolean isSingleGame = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
private ScheduledExecutorService scheduler;
|
||||||
|
|
||||||
public static GameInformation.Type gameToType(String game) {
|
public static GameInformation.Type gameToType(String game) {
|
||||||
if (game.equalsIgnoreCase("tic-tac-toe")) {
|
if (game.equalsIgnoreCase("tic-tac-toe")) {
|
||||||
return GameInformation.Type.TICTACTOE;
|
return GameInformation.Type.TICTACTOE;
|
||||||
} else if (game.equalsIgnoreCase("reversi")) {
|
} else if (game.equalsIgnoreCase("reversi")) {
|
||||||
return GameInformation.Type.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;
|
return null;
|
||||||
@@ -51,7 +56,7 @@ public final class Server {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int parsedPort = -1;
|
int parsedPort;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
parsedPort = Integer.parseInt(port);
|
parsedPort = Integer.parseInt(port);
|
||||||
@@ -60,7 +65,7 @@ public final class Server {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.isEmpty()) {
|
if (user.isEmpty() || user.matches("^[0-9].*")) {
|
||||||
ViewStack.push(new ErrorView(AppContext.getString("invalid-username")));
|
ViewStack.push(new ErrorView(AppContext.getString("invalid-username")));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -71,7 +76,6 @@ public final class Server {
|
|||||||
new NetworkingConnector(ip, parsedPort, 10, 1, TimeUnit.SECONDS)
|
new NetworkingConnector(ip, parsedPort, 10, 1, TimeUnit.SECONDS)
|
||||||
)
|
)
|
||||||
.onResponse(NetworkEvents.StartClientResponse.class, e -> {
|
.onResponse(NetworkEvents.StartClientResponse.class, e -> {
|
||||||
// TODO add if unsuccessful
|
|
||||||
this.user = user;
|
this.user = user;
|
||||||
clientId = e.clientId();
|
clientId = e.clientId();
|
||||||
|
|
||||||
@@ -81,39 +85,32 @@ public final class Server {
|
|||||||
ViewStack.push(view);
|
ViewStack.push(view);
|
||||||
|
|
||||||
startPopulateScheduler();
|
startPopulateScheduler();
|
||||||
|
|
||||||
populateGameList();
|
populateGameList();
|
||||||
|
|
||||||
}).postEvent();
|
}).postEvent();
|
||||||
|
|
||||||
new EventFlow().listen(this::handleReceivedChallenge);
|
new EventFlow().listen(this::handleReceivedChallenge)
|
||||||
}
|
.listen(this::handleMatchResponse);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendChallenge(String opponent) {
|
private void sendChallenge(String opponent) {
|
||||||
if (!isPolling) {
|
if (!isPolling) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewStack.push(new SendChallengeView(this, opponent, (playerInformation, gameType) -> {
|
ViewStack.push(new SendChallengeView(this, opponent, (playerInformation, gameType) -> {
|
||||||
new EventFlow().addPostEvent(new NetworkEvents.SendChallenge(clientId, opponent, gameType))
|
new EventFlow().addPostEvent(new NetworkEvents.SendChallenge(clientId, opponent, gameType)).postEvent();
|
||||||
.listen(NetworkEvents.GameMatchResponse.class, e -> {
|
/* .listen(NetworkEvents.GameMatchResponse.class, e -> {
|
||||||
if (e.clientId() == clientId) {
|
if (e.clientId() == clientId) {
|
||||||
isPolling = false;
|
isPolling = false;
|
||||||
onlinePlayers.clear();
|
onlinePlayers.clear();
|
||||||
|
|
||||||
final GameInformation.Type type = gameToType(gameType);
|
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);
|
final GameInformation information = new GameInformation(type);
|
||||||
information.players[0] = playerInformation;
|
information.players[0] = playerInformation;
|
||||||
@@ -121,54 +118,73 @@ public final class Server {
|
|||||||
information.players[1].name = opponent;
|
information.players[1].name = opponent;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TICTACTOE: new TicTacToeGame(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); break;
|
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 handleReceivedChallenge(NetworkEvents.ChallengeResponse response) {
|
private void handleMatchResponse(NetworkEvents.GameMatchResponse response) {
|
||||||
if (!isPolling) {
|
if (!isPolling) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String challengerName = response.challengerName();
|
String gameType = extractQuotedValue(response.gameType());
|
||||||
challengerName = challengerName.substring(challengerName.indexOf("\"") + 1);
|
|
||||||
challengerName = challengerName.substring(0, challengerName.indexOf("\""));
|
|
||||||
|
|
||||||
String gameType = response.gameType();
|
if (response.clientId() == clientId) {
|
||||||
gameType = gameType.substring(gameType.indexOf("\"") + 1);
|
|
||||||
gameType = gameType.substring(0, gameType.indexOf("\""));
|
|
||||||
|
|
||||||
final String finalGameType = gameType;
|
|
||||||
|
|
||||||
ViewStack.push(new ChallengeView(challengerName, gameType, (playerInformation) -> {
|
|
||||||
final int challengeId = Integer.parseInt(response.challengeId().substring(18, response.challengeId().length() - 2));
|
|
||||||
new EventFlow().addPostEvent(new NetworkEvents.SendAcceptChallenge(clientId, challengeId)).postEvent();
|
|
||||||
|
|
||||||
ViewStack.pop();
|
|
||||||
|
|
||||||
new EventFlow().listen(NetworkEvents.GameMatchResponse.class, e -> {
|
|
||||||
if (e.clientId() == clientId) {
|
|
||||||
isPolling = false;
|
isPolling = false;
|
||||||
onlinePlayers.clear();
|
onlinePlayers.clear();
|
||||||
|
|
||||||
final GameInformation.Type type = gameToType(finalGameType);
|
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 = response.playerToMove().equalsIgnoreCase(response.opponent()) ? 1 : 0;
|
||||||
|
|
||||||
final GameInformation information = new GameInformation(type);
|
final GameInformation information = new GameInformation(type);
|
||||||
information.players[0] = playerInformation;
|
//information.players[0] = playerInformation;
|
||||||
information.players[0].name = user;
|
information.players[0].name = user;
|
||||||
information.players[1].name = e.opponent();
|
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) {
|
switch (type) {
|
||||||
case TICTACTOE: new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break;
|
case TICTACTOE ->
|
||||||
case REVERSI: new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage); break;
|
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 = 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().replaceAll("\\D", ""));
|
||||||
|
new EventFlow().addPostEvent(new NetworkEvents.SendAcceptChallenge(clientId, challengeId)).postEvent();
|
||||||
|
ViewStack.pop();
|
||||||
|
isSingleGame.set(true);
|
||||||
|
|
||||||
|
//new EventFlow().listen(NetworkEvents.GameMatchResponse.class, e -> {
|
||||||
|
|
||||||
|
|
||||||
|
//});
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +195,7 @@ public final class Server {
|
|||||||
private void disconnect() {
|
private void disconnect() {
|
||||||
new EventFlow().addPostEvent(new NetworkEvents.CloseClient(clientId)).postEvent();
|
new EventFlow().addPostEvent(new NetworkEvents.CloseClient(clientId)).postEvent();
|
||||||
isPolling = false;
|
isPolling = false;
|
||||||
|
stopScheduler();
|
||||||
ViewStack.push(new OnlineView());
|
ViewStack.push(new OnlineView());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,41 +205,69 @@ public final class Server {
|
|||||||
|
|
||||||
private void exitGame() {
|
private void exitGame() {
|
||||||
forfeitGame();
|
forfeitGame();
|
||||||
|
|
||||||
ViewStack.push(view);
|
ViewStack.push(view);
|
||||||
startPopulateScheduler();
|
startPopulateScheduler();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void gameOver(){
|
||||||
|
ViewStack.pop();
|
||||||
|
ViewStack.push(view);
|
||||||
|
startPopulateScheduler();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private void startPopulateScheduler() {
|
private void startPopulateScheduler() {
|
||||||
isPolling = true;
|
isPolling = true;
|
||||||
|
isSingleGame.set(false);
|
||||||
|
stopScheduler();
|
||||||
|
|
||||||
EventFlow getPlayerlistFlow = new EventFlow()
|
new EventFlow()
|
||||||
.addPostEvent(new NetworkEvents.SendGetPlayerlist(clientId))
|
|
||||||
.listen(NetworkEvents.PlayerlistResponse.class, e -> {
|
.listen(NetworkEvents.PlayerlistResponse.class, e -> {
|
||||||
if (e.clientId() == clientId) {
|
if (e.clientId() == clientId) {
|
||||||
onlinePlayers = new ArrayList<>(List.of(e.playerlist()));
|
onlinePlayers.clear();
|
||||||
|
onlinePlayers.addAll(List.of(e.playerlist()));
|
||||||
onlinePlayers.removeIf(name -> name.equalsIgnoreCase(user));
|
onlinePlayers.removeIf(name -> name.equalsIgnoreCase(user));
|
||||||
|
|
||||||
view.update(onlinePlayers);
|
view.update(onlinePlayers);
|
||||||
}
|
}
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||||
scheduler.schedule(() -> populatePlayerList(scheduler, getPlayerlistFlow::postEvent), 0, TimeUnit.MILLISECONDS);
|
scheduler.scheduleAtFixedRate(() -> {
|
||||||
|
if (isPolling) {
|
||||||
|
new EventFlow().addPostEvent(new NetworkEvents.SendGetPlayerlist(clientId)).postEvent();
|
||||||
|
} else {
|
||||||
|
stopScheduler();
|
||||||
|
}
|
||||||
|
}, 0, 5, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopScheduler() {
|
||||||
|
if (scheduler != null && !scheduler.isShutdown()) {
|
||||||
|
scheduler.shutdownNow();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void gamesListFromServerHandler(NetworkEvents.GamelistResponse event) {
|
private void gamesListFromServerHandler(NetworkEvents.GamelistResponse event) {
|
||||||
|
gameList.clear();
|
||||||
gameList.addAll(List.of(event.gamelist()));
|
gameList.addAll(List.of(event.gamelist()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void populateGameList() {
|
public void populateGameList() {
|
||||||
new EventFlow().addPostEvent(new NetworkEvents.SendGetGamelist(clientId))
|
new EventFlow().addPostEvent(new NetworkEvents.SendGetGamelist(clientId))
|
||||||
.listen(NetworkEvents.GamelistResponse.class,
|
.listen(NetworkEvents.GamelistResponse.class, this::gamesListFromServerHandler, true)
|
||||||
this::gamesListFromServerHandler, true
|
.postEvent();
|
||||||
).postEvent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getGameList() {
|
public List<String> getGameList() {
|
||||||
return gameList;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
11
app/src/main/java/org/toop/app/canvas/Connect4Canvas.java
Normal file
11
app/src/main/java/org/toop/app/canvas/Connect4Canvas.java
Normal file
@@ -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<Integer> onCellClicked) {
|
||||||
|
super(color, Color.TRANSPARENT, width, height, 7, 6, 10, true, onCellClicked);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
package org.toop.app.canvas;
|
package org.toop.app.canvas;
|
||||||
|
|
||||||
|
import javafx.animation.KeyFrame;
|
||||||
|
import javafx.animation.Timeline;
|
||||||
import javafx.scene.canvas.Canvas;
|
import javafx.scene.canvas.Canvas;
|
||||||
import javafx.scene.canvas.GraphicsContext;
|
import javafx.scene.canvas.GraphicsContext;
|
||||||
import javafx.scene.input.MouseButton;
|
import javafx.scene.input.MouseButton;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
|
import javafx.util.Duration;
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@@ -19,6 +22,7 @@ public abstract class GameCanvas {
|
|||||||
protected final GraphicsContext graphics;
|
protected final GraphicsContext graphics;
|
||||||
|
|
||||||
protected final Color color;
|
protected final Color color;
|
||||||
|
protected final Color backgroundColor;
|
||||||
|
|
||||||
protected final int width;
|
protected final int width;
|
||||||
protected final int height;
|
protected final int height;
|
||||||
@@ -31,11 +35,12 @@ public abstract class GameCanvas {
|
|||||||
|
|
||||||
protected final Cell[] cells;
|
protected final Cell[] cells;
|
||||||
|
|
||||||
protected GameCanvas(Color color, int width, int height, int rowSize, int columnSize, int gapSize, boolean edges, Consumer<Integer> onCellClicked) {
|
protected GameCanvas(Color color, Color backgroundColor, int width, int height, int rowSize, int columnSize, int gapSize, boolean edges, Consumer<Integer> onCellClicked) {
|
||||||
canvas = new Canvas(width, height);
|
canvas = new Canvas(width, height);
|
||||||
graphics = canvas.getGraphicsContext2D();
|
graphics = canvas.getGraphicsContext2D();
|
||||||
|
|
||||||
this.color = color;
|
this.color = color;
|
||||||
|
this.backgroundColor = backgroundColor;
|
||||||
|
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
@@ -48,8 +53,8 @@ public abstract class GameCanvas {
|
|||||||
|
|
||||||
cells = new Cell[rowSize * columnSize];
|
cells = new Cell[rowSize * columnSize];
|
||||||
|
|
||||||
final float cellWidth = ((float)width - gapSize * rowSize - gapSize) / rowSize;
|
final float cellWidth = ((float) width - gapSize * rowSize - gapSize) / rowSize;
|
||||||
final float cellHeight = ((float)height - gapSize * columnSize - gapSize) / columnSize;
|
final float cellHeight = ((float) height - gapSize * columnSize - gapSize) / columnSize;
|
||||||
|
|
||||||
for (int y = 0; y < columnSize; y++) {
|
for (int y = 0; y < columnSize; y++) {
|
||||||
final float startY = y * cellHeight + y * gapSize + gapSize;
|
final float startY = y * cellHeight + y * gapSize + gapSize;
|
||||||
@@ -65,8 +70,8 @@ public abstract class GameCanvas {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final int column = (int)((event.getX() / this.width) * rowSize);
|
final int column = (int) ((event.getX() / this.width) * rowSize);
|
||||||
final int row = (int)((event.getY() / this.height) * columnSize);
|
final int row = (int) ((event.getY() / this.height) * columnSize);
|
||||||
|
|
||||||
final Cell cell = cells[column + row * rowSize];
|
final Cell cell = cells[column + row * rowSize];
|
||||||
|
|
||||||
@@ -79,11 +84,10 @@ public abstract class GameCanvas {
|
|||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clear() {
|
private void render() {
|
||||||
graphics.clearRect(0, 0, width, height);
|
graphics.setFill(backgroundColor);
|
||||||
}
|
graphics.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
public void render() {
|
|
||||||
graphics.setFill(color);
|
graphics.setFill(color);
|
||||||
|
|
||||||
for (int x = 0; x < rowSize - 1; x++) {
|
for (int x = 0; x < rowSize - 1; x++) {
|
||||||
@@ -116,6 +120,76 @@ public abstract class GameCanvas {
|
|||||||
graphics.fillRect(x, y, width, height);
|
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() {
|
public Canvas getCanvas() {
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,21 +6,10 @@ import java.util.function.Consumer;
|
|||||||
|
|
||||||
public final class ReversiCanvas extends GameCanvas {
|
public final class ReversiCanvas extends GameCanvas {
|
||||||
public ReversiCanvas(Color color, int width, int height, Consumer<Integer> onCellClicked) {
|
public ReversiCanvas(Color color, int width, int height, Consumer<Integer> onCellClicked) {
|
||||||
super(color, width, height, 8, 8, 10, true, onCellClicked);
|
super(color, Color.GREEN, width, height, 8, 8, 5, true, onCellClicked);
|
||||||
drawStartingDots();
|
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() {
|
public void drawStartingDots() {
|
||||||
drawDot(Color.BLACK, 28);
|
drawDot(Color.BLACK, 28);
|
||||||
drawDot(Color.WHITE, 36);
|
drawDot(Color.WHITE, 36);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import java.util.function.Consumer;
|
|||||||
|
|
||||||
public final class TicTacToeCanvas extends GameCanvas {
|
public final class TicTacToeCanvas extends GameCanvas {
|
||||||
public TicTacToeCanvas(Color color, int width, int height, Consumer<Integer> onCellClicked) {
|
public TicTacToeCanvas(Color color, int width, int height, Consumer<Integer> 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) {
|
public void drawX(Color color, int cell) {
|
||||||
|
|||||||
274
app/src/main/java/org/toop/app/game/Connect4Game.java
Normal file
274
app/src/main/java/org/toop/app/game/Connect4Game.java
Normal file
@@ -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<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 final AtomicBoolean isRunning;
|
||||||
|
|
||||||
|
public Connect4Game(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer<String> onMessage, Runnable onGameOver) {
|
||||||
|
this.information = information;
|
||||||
|
this.myTurn = myTurn;
|
||||||
|
this.onGameOver = onGameOver;
|
||||||
|
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();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.toop.app.game;
|
package org.toop.app.game;
|
||||||
|
|
||||||
|
import javafx.animation.SequentialTransition;
|
||||||
import org.toop.app.App;
|
import org.toop.app.App;
|
||||||
import org.toop.app.GameInformation;
|
import org.toop.app.GameInformation;
|
||||||
import org.toop.app.canvas.ReversiCanvas;
|
import org.toop.app.canvas.ReversiCanvas;
|
||||||
@@ -24,6 +25,7 @@ public final class ReversiGame {
|
|||||||
private final GameInformation information;
|
private final GameInformation information;
|
||||||
|
|
||||||
private final int myTurn;
|
private final int myTurn;
|
||||||
|
private Runnable onGameOver;
|
||||||
private final BlockingQueue<Game.Move> moveQueue;
|
private final BlockingQueue<Game.Move> moveQueue;
|
||||||
|
|
||||||
private final Reversi game;
|
private final Reversi game;
|
||||||
@@ -33,17 +35,20 @@ public final class ReversiGame {
|
|||||||
private final ReversiCanvas canvas;
|
private final ReversiCanvas canvas;
|
||||||
|
|
||||||
private final AtomicBoolean isRunning;
|
private final AtomicBoolean isRunning;
|
||||||
|
private final AtomicBoolean isPaused;
|
||||||
|
|
||||||
public ReversiGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer<String> onMessage) {
|
public ReversiGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer<String> onMessage, Runnable onGameOver) {
|
||||||
this.information = information;
|
this.information = information;
|
||||||
|
|
||||||
this.myTurn = myTurn;
|
this.myTurn = myTurn;
|
||||||
|
this.onGameOver = onGameOver;
|
||||||
moveQueue = new LinkedBlockingQueue<Game.Move>();
|
moveQueue = new LinkedBlockingQueue<Game.Move>();
|
||||||
|
|
||||||
game = new Reversi();
|
game = new Reversi();
|
||||||
ai = new ReversiAI();
|
ai = new ReversiAI();
|
||||||
|
|
||||||
isRunning = new AtomicBoolean(true);
|
isRunning = new AtomicBoolean(true);
|
||||||
|
isPaused = new AtomicBoolean(false);
|
||||||
|
|
||||||
if (onForfeit == null || onExit == null) {
|
if (onForfeit == null || onExit == null) {
|
||||||
view = new GameView(null, () -> {
|
view = new GameView(null, () -> {
|
||||||
@@ -57,7 +62,7 @@ public final class ReversiGame {
|
|||||||
}, onMessage);
|
}, onMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas = new ReversiCanvas(Color.GRAY,
|
canvas = new ReversiCanvas(Color.BLACK,
|
||||||
(App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3,
|
(App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3,
|
||||||
(cell) -> {
|
(cell) -> {
|
||||||
if (onForfeit == null || onExit == null) {
|
if (onForfeit == null || onExit == null) {
|
||||||
@@ -84,6 +89,7 @@ public final class ReversiGame {
|
|||||||
|
|
||||||
if (onForfeit == null || onExit == null) {
|
if (onForfeit == null || onExit == null) {
|
||||||
new Thread(this::localGameThread).start();
|
new Thread(this::localGameThread).start();
|
||||||
|
setGameLabels(information.players[0].isHuman);
|
||||||
} else {
|
} else {
|
||||||
new EventFlow()
|
new EventFlow()
|
||||||
.listen(NetworkEvents.GameMoveResponse.class, this::onMoveResponse)
|
.listen(NetworkEvents.GameMoveResponse.class, this::onMoveResponse)
|
||||||
@@ -93,22 +99,30 @@ public final class ReversiGame {
|
|||||||
setGameLabels(myTurn == 0);
|
setGameLabels(myTurn == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCanvas();
|
updateCanvas(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReversiGame(GameInformation information) {
|
public ReversiGame(GameInformation information) {
|
||||||
this(information, 0, null, null, null);
|
this(information, 0, null, null, null,null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void localGameThread() {
|
private void localGameThread() {
|
||||||
while (isRunning.get()) {
|
while (isRunning.get()) {
|
||||||
|
if (isPaused.get()) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(200);
|
||||||
|
} catch (InterruptedException _) {}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
final int currentTurn = game.getCurrentTurn();
|
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);
|
final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type);
|
||||||
|
|
||||||
view.nextPlayer(information.players[currentTurn].isHuman,
|
view.nextPlayer(information.players[currentTurn].isHuman,
|
||||||
information.players[currentTurn].name,
|
information.players[currentTurn].name,
|
||||||
String.valueOf(currentValue),
|
currentValue,
|
||||||
information.players[nextTurn].name);
|
information.players[nextTurn].name);
|
||||||
|
|
||||||
Game.Move move = null;
|
Game.Move move = null;
|
||||||
@@ -146,7 +160,7 @@ public final class ReversiGame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final Game.State state = game.play(move);
|
final Game.State state = game.play(move);
|
||||||
updateCanvas();
|
updateCanvas(true);
|
||||||
|
|
||||||
if (state != Game.State.NORMAL) {
|
if (state != Game.State.NORMAL) {
|
||||||
if (state == Game.State.WIN) {
|
if (state == Game.State.WIN) {
|
||||||
@@ -180,18 +194,29 @@ public final class ReversiGame {
|
|||||||
if (state == Game.State.WIN) {
|
if (state == Game.State.WIN) {
|
||||||
if (response.player().equalsIgnoreCase(information.players[0].name)) {
|
if (response.player().equalsIgnoreCase(information.players[0].name)) {
|
||||||
view.gameOver(true, information.players[0].name);
|
view.gameOver(true, information.players[0].name);
|
||||||
|
gameOver();
|
||||||
} else {
|
} else {
|
||||||
view.gameOver(false, information.players[1].name);
|
view.gameOver(false, information.players[1].name);
|
||||||
|
gameOver();
|
||||||
}
|
}
|
||||||
} else if (state == Game.State.DRAW) {
|
} else if (state == Game.State.DRAW) {
|
||||||
view.gameOver(false, "");
|
view.gameOver(false, "");
|
||||||
|
game.play(move);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCanvas();
|
updateCanvas(false);
|
||||||
setGameLabels(game.getCurrentTurn() == myTurn);
|
setGameLabels(game.getCurrentTurn() == myTurn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void gameOver() {
|
||||||
|
if (onGameOver == null){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isRunning.set(false);
|
||||||
|
onGameOver.run();
|
||||||
|
}
|
||||||
|
|
||||||
private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) {
|
private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) {
|
||||||
if (!isRunning.get()) {
|
if (!isRunning.get()) {
|
||||||
return;
|
return;
|
||||||
@@ -224,11 +249,9 @@ public final class ReversiGame {
|
|||||||
view.updateChat(msg.message());
|
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.
|
// Todo: this is very inefficient. still very fast but if the grid is bigger it might cause issues. improve.
|
||||||
|
canvas.clearAll();
|
||||||
canvas.clear();
|
|
||||||
canvas.render();
|
|
||||||
|
|
||||||
for (int i = 0; i < game.board.length; i++) {
|
for (int i = 0; i < game.board.length; i++) {
|
||||||
if (game.board[i] == 'B') {
|
if (game.board[i] == 'B') {
|
||||||
@@ -238,20 +261,44 @@ public final class ReversiGame {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Game.Move[] flipped = game.getMostRecentlyFlippedPieces();
|
||||||
|
|
||||||
|
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();
|
final Game.Move[] legalMoves = game.getLegalMoves();
|
||||||
|
|
||||||
for (final Game.Move legalMove : legalMoves) {
|
for (final Game.Move legalMove : legalMoves) {
|
||||||
canvas.drawLegalPosition(legalMove.position());
|
canvas.drawLegalPosition(legalMove.position());
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
animation.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setGameLabels(boolean isMe) {
|
private void setGameLabels(boolean isMe) {
|
||||||
final int currentTurn = game.getCurrentTurn();
|
final int currentTurn = game.getCurrentTurn();
|
||||||
final char currentValue = currentTurn == 0? 'B' : 'W';
|
final String currentValue = currentTurn == 0? "BLACK" : "WHITE";
|
||||||
|
|
||||||
view.nextPlayer(isMe,
|
view.nextPlayer(isMe,
|
||||||
information.players[isMe? 0 : 1].name,
|
information.players[isMe? 0 : 1].name,
|
||||||
String.valueOf(currentValue),
|
currentValue,
|
||||||
information.players[isMe? 1 : 0].name);
|
information.players[isMe? 1 : 0].name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,7 @@ public final class TicTacToeGame {
|
|||||||
private final GameInformation information;
|
private final GameInformation information;
|
||||||
|
|
||||||
private final int myTurn;
|
private final int myTurn;
|
||||||
|
private Runnable onGameOver;
|
||||||
private final BlockingQueue<Game.Move> moveQueue;
|
private final BlockingQueue<Game.Move> moveQueue;
|
||||||
|
|
||||||
private final TicTacToe game;
|
private final TicTacToe game;
|
||||||
@@ -32,12 +33,13 @@ public final class TicTacToeGame {
|
|||||||
private final GameView view;
|
private final GameView view;
|
||||||
private final TicTacToeCanvas canvas;
|
private final TicTacToeCanvas canvas;
|
||||||
|
|
||||||
private AtomicBoolean isRunning;
|
private final AtomicBoolean isRunning;
|
||||||
|
|
||||||
public TicTacToeGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer<String> onMessage) {
|
public TicTacToeGame(GameInformation information, int myTurn, Runnable onForfeit, Runnable onExit, Consumer<String> onMessage, Runnable onGameOver) {
|
||||||
this.information = information;
|
this.information = information;
|
||||||
|
|
||||||
this.myTurn = myTurn;
|
this.myTurn = myTurn;
|
||||||
|
this.onGameOver = onGameOver;
|
||||||
moveQueue = new LinkedBlockingQueue<Game.Move>();
|
moveQueue = new LinkedBlockingQueue<Game.Move>();
|
||||||
|
|
||||||
game = new TicTacToe();
|
game = new TicTacToe();
|
||||||
@@ -95,18 +97,18 @@ public final class TicTacToeGame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public TicTacToeGame(GameInformation information) {
|
public TicTacToeGame(GameInformation information) {
|
||||||
this(information, 0, null, null, null);
|
this(information, 0, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void localGameThread() {
|
private void localGameThread() {
|
||||||
while (isRunning.get()) {
|
while (isRunning.get()) {
|
||||||
final int currentTurn = game.getCurrentTurn();
|
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);
|
final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type);
|
||||||
|
|
||||||
view.nextPlayer(information.players[currentTurn].isHuman,
|
view.nextPlayer(information.players[currentTurn].isHuman,
|
||||||
information.players[currentTurn].name,
|
information.players[currentTurn].name,
|
||||||
String.valueOf(currentValue),
|
currentValue,
|
||||||
information.players[nextTurn].name);
|
information.players[nextTurn].name);
|
||||||
|
|
||||||
Game.Move move = null;
|
Game.Move move = null;
|
||||||
@@ -183,11 +185,16 @@ public final class TicTacToeGame {
|
|||||||
if (state == Game.State.WIN) {
|
if (state == Game.State.WIN) {
|
||||||
if (response.player().equalsIgnoreCase(information.players[0].name)) {
|
if (response.player().equalsIgnoreCase(information.players[0].name)) {
|
||||||
view.gameOver(true, information.players[0].name);
|
view.gameOver(true, information.players[0].name);
|
||||||
|
gameOver();
|
||||||
} else {
|
} else {
|
||||||
view.gameOver(false, information.players[1].name);
|
view.gameOver(false, information.players[1].name);
|
||||||
|
gameOver();
|
||||||
}
|
}
|
||||||
} else if (state == Game.State.DRAW) {
|
} else if (state == Game.State.DRAW) {
|
||||||
|
if(game.getLegalMoves().length == 0) { //only return draw in online multiplayer if the game is actually over.
|
||||||
view.gameOver(false, "");
|
view.gameOver(false, "");
|
||||||
|
gameOver();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +207,14 @@ public final class TicTacToeGame {
|
|||||||
setGameLabels(game.getCurrentTurn() == myTurn);
|
setGameLabels(game.getCurrentTurn() == myTurn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void gameOver() {
|
||||||
|
if (onGameOver == null){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isRunning.set(false);
|
||||||
|
onGameOver.run();
|
||||||
|
}
|
||||||
|
|
||||||
private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) {
|
private void onYourTurnResponse(NetworkEvents.YourTurnResponse response) {
|
||||||
if (!isRunning.get()) {
|
if (!isRunning.get()) {
|
||||||
return;
|
return;
|
||||||
@@ -214,7 +229,12 @@ public final class TicTacToeGame {
|
|||||||
position = moveQueue.take().position();
|
position = moveQueue.take().position();
|
||||||
} catch (InterruptedException _) {}
|
} catch (InterruptedException _) {}
|
||||||
} else {
|
} 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;
|
assert move != null;
|
||||||
position = move.position();
|
position = move.position();
|
||||||
@@ -234,11 +254,11 @@ public final class TicTacToeGame {
|
|||||||
|
|
||||||
private void setGameLabels(boolean isMe) {
|
private void setGameLabels(boolean isMe) {
|
||||||
final int currentTurn = game.getCurrentTurn();
|
final int currentTurn = game.getCurrentTurn();
|
||||||
final char currentValue = currentTurn == 0? 'X' : 'O';
|
final String currentValue = currentTurn == 0? "X" : "O";
|
||||||
|
|
||||||
view.nextPlayer(isMe,
|
view.nextPlayer(isMe,
|
||||||
information.players[isMe? 0 : 1].name,
|
information.players[isMe? 0 : 1].name,
|
||||||
String.valueOf(currentValue),
|
currentValue,
|
||||||
information.players[isMe? 1 : 0].name);
|
information.players[isMe? 1 : 0].name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.toop.app.view.views;
|
package org.toop.app.view.views;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
import org.toop.app.view.View;
|
import org.toop.app.view.View;
|
||||||
import org.toop.app.view.ViewStack;
|
import org.toop.app.view.ViewStack;
|
||||||
import org.toop.app.view.displays.SongDisplay;
|
import org.toop.app.view.displays.SongDisplay;
|
||||||
@@ -151,6 +152,7 @@ public final class GameView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void nextPlayer(boolean isMe, String currentPlayer, String currentMove, String nextPlayer) {
|
public void nextPlayer(boolean isMe, String currentPlayer, String currentMove, String nextPlayer) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
currentPlayerHeader.setText(currentPlayer);
|
currentPlayerHeader.setText(currentPlayer);
|
||||||
currentMoveHeader.setText(currentMove);
|
currentMoveHeader.setText(currentMove);
|
||||||
|
|
||||||
@@ -161,6 +163,8 @@ public final class GameView extends View {
|
|||||||
} else {
|
} else {
|
||||||
currentPlayerHeader.getStyleClass().remove("my-turn");
|
currentPlayerHeader.getStyleClass().remove("my-turn");
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateChat(String message) {
|
public void updateChat(String message) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.toop.app.view.views;
|
package org.toop.app.view.views;
|
||||||
|
|
||||||
import org.toop.app.GameInformation;
|
import org.toop.app.GameInformation;
|
||||||
|
import org.toop.app.game.Connect4Game;
|
||||||
import org.toop.app.game.ReversiGame;
|
import org.toop.app.game.ReversiGame;
|
||||||
import org.toop.app.game.TicTacToeGame;
|
import org.toop.app.game.TicTacToeGame;
|
||||||
import org.toop.app.view.View;
|
import org.toop.app.view.View;
|
||||||
@@ -46,6 +47,8 @@ public final class LocalMultiplayerView extends View {
|
|||||||
switch (information.type) {
|
switch (information.type) {
|
||||||
case TICTACTOE: new TicTacToeGame(information); break;
|
case TICTACTOE: new TicTacToeGame(information); break;
|
||||||
case REVERSI: new ReversiGame(information); break;
|
case REVERSI: new ReversiGame(information); break;
|
||||||
|
case CONNECT4: new Connect4Game(information); break;
|
||||||
|
//case BATTLESHIP: new BattleshipGame(information); break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,15 @@ public final class LocalView extends View {
|
|||||||
reversiButton.setText(AppContext.getString("reversi"));
|
reversiButton.setText(AppContext.getString("reversi"));
|
||||||
reversiButton.setOnAction(_ -> { ViewStack.push(new LocalMultiplayerView(GameInformation.Type.REVERSI)); });
|
reversiButton.setOnAction(_ -> { ViewStack.push(new LocalMultiplayerView(GameInformation.Type.REVERSI)); });
|
||||||
|
|
||||||
|
final Button connect4Button = button();
|
||||||
|
connect4Button.setText(AppContext.getString("connect4"));
|
||||||
|
connect4Button.setOnAction(_ -> { ViewStack.push(new LocalMultiplayerView(GameInformation.Type.CONNECT4)); });
|
||||||
|
|
||||||
add(Pos.CENTER,
|
add(Pos.CENTER,
|
||||||
fit(vboxFill(
|
fit(vboxFill(
|
||||||
ticTacToeButton,
|
ticTacToeButton,
|
||||||
reversiButton
|
reversiButton,
|
||||||
|
connect4Button
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ public final class SendChallengeView extends View {
|
|||||||
|
|
||||||
final Button cancelButton = button();
|
final Button cancelButton = button();
|
||||||
cancelButton.setText(AppContext.getString("cancel"));
|
cancelButton.setText(AppContext.getString("cancel"));
|
||||||
cancelButton.setOnAction(_ -> { ViewStack.pop(); });
|
cancelButton.setOnAction(_ -> {
|
||||||
|
ViewStack.pop(); });
|
||||||
|
|
||||||
final List<Node> nodes = new ArrayList<>();
|
final List<Node> nodes = new ArrayList<>();
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ style=Style
|
|||||||
the-game-ended-in-a-draw=The game ended in a draw
|
the-game-ended-in-a-draw=The game ended in a draw
|
||||||
theme=Theme
|
theme=Theme
|
||||||
tic-tac-toe=Tic Tac Toe
|
tic-tac-toe=Tic Tac Toe
|
||||||
|
connect4=Connect 4
|
||||||
to-a-game-of=to a game of
|
to-a-game-of=to a game of
|
||||||
volume=Volume
|
volume=Volume
|
||||||
windowed=Windowed
|
windowed=Windowed
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ style=Stijl
|
|||||||
the-game-ended-in-a-draw=Het spel eindigde in een gelijkspel
|
the-game-ended-in-a-draw=Het spel eindigde in een gelijkspel
|
||||||
theme=Thema
|
theme=Thema
|
||||||
tic-tac-toe=Boter Kaas en Eieren
|
tic-tac-toe=Boter Kaas en Eieren
|
||||||
|
connect4=Vier op een rij
|
||||||
to-a-game-of=voor een spelletje
|
to-a-game-of=voor een spelletje
|
||||||
volume=Volume
|
volume=Volume
|
||||||
windowed=Venstermodus
|
windowed=Venstermodus
|
||||||
|
|||||||
114
game/src/main/java/org/toop/game/Connect4/Connect4.java
Normal file
114
game/src/main/java/org/toop/game/Connect4/Connect4.java
Normal file
@@ -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<Move> 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
63
game/src/main/java/org/toop/game/Connect4/Connect4AI.java
Normal file
63
game/src/main/java/org/toop/game/Connect4/Connect4AI.java
Normal file
@@ -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<Connect4> {
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ public final class Reversi extends TurnBasedGame {
|
|||||||
private int movesTaken;
|
private int movesTaken;
|
||||||
public static final char FIRST_MOVE = 'B';
|
public static final char FIRST_MOVE = 'B';
|
||||||
private Set<Point> filledCells = new HashSet<>();
|
private Set<Point> filledCells = new HashSet<>();
|
||||||
|
private Move[] mostRecentlyFlippedPieces;
|
||||||
|
|
||||||
public Reversi() {
|
public Reversi() {
|
||||||
super(8, 8, 2);
|
super(8, 8, 2);
|
||||||
@@ -138,27 +139,37 @@ public final class Reversi extends TurnBasedGame {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (moveIsLegal) {
|
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();
|
board[move.position()] = move.value();
|
||||||
IO.println(move.position() +" "+ move.value());
|
|
||||||
for (Move m : moves) {
|
for (Move m : moves) {
|
||||||
board[m.position()] = m.value();
|
board[m.position()] = m.value();
|
||||||
}
|
}
|
||||||
filledCells.add(new Point(move.position() % rowSize, move.position() / columnSize));
|
filledCells.add(new Point(move.position() % rowSize, move.position() / columnSize));
|
||||||
//updateFilledCellsSet();
|
|
||||||
nextTurn();
|
nextTurn();
|
||||||
if (getLegalMoves().length == 0) {
|
if (getLegalMoves().length == 0) {
|
||||||
skipMyTurn();
|
skipMyTurn();
|
||||||
|
if (getLegalMoves().length > 0) {
|
||||||
return State.MOVE_SKIPPED;
|
return State.MOVE_SKIPPED;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
Score score = getScore();
|
||||||
|
if (score.player1Score() == score.player2Score()) {
|
||||||
|
return State.DRAW;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return State.WIN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return State.NORMAL;
|
return State.NORMAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void skipMyTurn(){
|
public void skipMyTurn(){
|
||||||
IO.println("TURN " + getCurrentPlayer() + " SKIPPED");
|
IO.println("TURN " + getCurrentPlayer() + " SKIPPED");
|
||||||
|
//todo notify user that a turn has been skipped
|
||||||
nextTurn();
|
nextTurn();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,14 +194,32 @@ public final class Reversi extends TurnBasedGame {
|
|||||||
public Game.Score getScore(){
|
public Game.Score getScore(){
|
||||||
int player1Score = 0, player2Score = 0;
|
int player1Score = 0, player2Score = 0;
|
||||||
for (int count = 0; count < rowSize * columnSize; count++) {
|
for (int count = 0; count < rowSize * columnSize; count++) {
|
||||||
if (board[count] == 'W') {
|
if (board[count] == 'B') {
|
||||||
player1Score += 1;
|
player1Score += 1;
|
||||||
}
|
}
|
||||||
if (board[count] == 'B') {
|
if (board[count] == 'W') {
|
||||||
player2Score += 1;
|
player2Score += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new Game.Score(player1Score, player2Score);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,8 @@ import org.toop.game.Game;
|
|||||||
public final class ReversiAI extends AI<Reversi> {
|
public final class ReversiAI extends AI<Reversi> {
|
||||||
@Override
|
@Override
|
||||||
public Game.Move findBestMove(Reversi game, int depth) {
|
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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public final class TicTacToeAI extends AI<TicTacToe> {
|
|||||||
|
|
||||||
final Game.Move[] legalMoves = game.getLegalMoves();
|
final Game.Move[] legalMoves = game.getLegalMoves();
|
||||||
|
|
||||||
if (legalMoves.length <= 0) {
|
if (legalMoves.length == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +38,24 @@ public final class TicTacToeAI extends AI<TicTacToe> {
|
|||||||
|
|
||||||
return bestMove != null? bestMove : legalMoves[(int)(Math.random() * legalMoves.length)];
|
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) {
|
private int getMoveScore(TicTacToe game, int depth, Game.Move move, boolean maximizing) {
|
||||||
final TicTacToe copy = new TicTacToe(game);
|
final TicTacToe copy = new TicTacToe(game);
|
||||||
|
|||||||
@@ -149,6 +149,34 @@ class ReversiTest {
|
|||||||
game.play(ai.findBestMove(game,5));
|
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
|
@Test
|
||||||
void testAISelectsLegalMove() {
|
void testAISelectsLegalMove() {
|
||||||
Game.Move move = ai.findBestMove(game,4);
|
Game.Move move = ai.findBestMove(game,4);
|
||||||
|
|||||||
Reference in New Issue
Block a user