mirror of
https://github.com/2OOP/pism.git
synced 2026-02-04 10:54:51 +00:00
widget system almost complete
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
package org.toop.app;
|
||||
|
||||
import org.toop.app.widget.Widget;
|
||||
import org.toop.app.widget.WidgetContainer;
|
||||
import org.toop.app.widget.display.SongDisplay;
|
||||
import org.toop.app.widget.popup.QuitPopup;
|
||||
import org.toop.app.widget.primary.MainPrimary;
|
||||
import org.toop.app.widget.view.MainView;
|
||||
import org.toop.framework.audio.events.AudioEvents;
|
||||
import org.toop.framework.eventbus.EventFlow;
|
||||
import org.toop.framework.resource.ResourceManager;
|
||||
@@ -65,7 +64,7 @@ public final class App extends Application {
|
||||
AppSettings.applySettings();
|
||||
new EventFlow().addPostEvent(new AudioEvents.StartBackgroundMusic()).asyncPostEvent();
|
||||
|
||||
WidgetContainer.add(Pos.CENTER, new MainPrimary());
|
||||
WidgetContainer.add(Pos.CENTER, new MainView());
|
||||
WidgetContainer.add(Pos.BOTTOM_RIGHT, new SongDisplay());
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,11 @@ package org.toop.app;
|
||||
import org.toop.app.game.Connect4Game;
|
||||
import org.toop.app.game.ReversiGame;
|
||||
import org.toop.app.game.TicTacToeGame;
|
||||
import org.toop.app.view.ViewStack;
|
||||
import org.toop.app.view.views.ChallengeView;
|
||||
import org.toop.app.view.views.ErrorView;
|
||||
import org.toop.app.view.views.OnlineView;
|
||||
import org.toop.app.view.views.SendChallengeView;
|
||||
import org.toop.app.view.views.ServerView;
|
||||
import org.toop.app.widget.WidgetContainer;
|
||||
import org.toop.app.widget.popup.ChallengePopup;
|
||||
import org.toop.app.widget.popup.ErrorPopup;
|
||||
import org.toop.app.widget.popup.SendChallengePopup;
|
||||
import org.toop.app.widget.view.ServerView;
|
||||
import org.toop.framework.eventbus.EventFlow;
|
||||
import org.toop.framework.networking.clients.TournamentNetworkingClient;
|
||||
import org.toop.framework.networking.events.NetworkEvents;
|
||||
@@ -29,10 +28,10 @@ public final class Server {
|
||||
private final List<String> onlinePlayers = new CopyOnWriteArrayList<>();
|
||||
private final List<String> gameList = new CopyOnWriteArrayList<>();
|
||||
|
||||
private ServerView view;
|
||||
private ServerView primary;
|
||||
private boolean isPolling = true;
|
||||
|
||||
private AtomicBoolean isSingleGame = new AtomicBoolean(false);
|
||||
private final AtomicBoolean isSingleGame = new AtomicBoolean(false);
|
||||
|
||||
private ScheduledExecutorService scheduler;
|
||||
|
||||
@@ -52,7 +51,7 @@ public final class Server {
|
||||
|
||||
public Server(String ip, String port, String user) {
|
||||
if (ip.split("\\.").length < 4) {
|
||||
ViewStack.push(new ErrorView("\"" + ip + "\" " + AppContext.getString("is-not-a-valid-ip-address")));
|
||||
new ErrorPopup("\"" + ip + "\" " + AppContext.getString("is-not-a-valid-ip-address"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -61,12 +60,12 @@ public final class Server {
|
||||
try {
|
||||
parsedPort = Integer.parseInt(port);
|
||||
} catch (NumberFormatException _) {
|
||||
ViewStack.push(new ErrorView("\"" + port + "\" " + AppContext.getString("is-not-a-valid-port")));
|
||||
new ErrorPopup("\"" + port + "\" " + AppContext.getString("is-not-a-valid-port"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.isEmpty() || user.matches("^[0-9].*")) {
|
||||
ViewStack.push(new ErrorView(AppContext.getString("invalid-username")));
|
||||
new ErrorPopup(AppContext.getString("invalid-username"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -81,8 +80,8 @@ public final class Server {
|
||||
|
||||
new EventFlow().addPostEvent(new NetworkEvents.SendLogin(clientId, user)).postEvent();
|
||||
|
||||
view = new ServerView(user, this::sendChallenge, this::disconnect);
|
||||
ViewStack.push(view);
|
||||
primary = new ServerView(user, this::sendChallenge, this::disconnect);
|
||||
WidgetContainer.getCurrentView().transitionNext(primary);
|
||||
|
||||
startPopulateScheduler();
|
||||
populateGameList();
|
||||
@@ -96,38 +95,10 @@ public final class Server {
|
||||
private void sendChallenge(String opponent) {
|
||||
if (!isPolling) return;
|
||||
|
||||
|
||||
ViewStack.push(new SendChallengeView(this, opponent, (playerInformation, gameType) -> {
|
||||
new SendChallengePopup(this, opponent, (playerInformation, gameType) -> {
|
||||
new EventFlow().addPostEvent(new NetworkEvents.SendChallenge(clientId, opponent, gameType)).postEvent();
|
||||
/* .listen(NetworkEvents.GameMatchResponse.class, e -> {
|
||||
if (e.clientId() == clientId) {
|
||||
isPolling = false;
|
||||
onlinePlayers.clear();
|
||||
|
||||
final GameInformation.Type type = gameToType(gameType);
|
||||
if (type == null) {
|
||||
ViewStack.push(new ErrorView("Unsupported game type: " + gameType));
|
||||
return;
|
||||
}
|
||||
|
||||
final int myTurn = e.playerToMove().equalsIgnoreCase(e.opponent()) ? 1 : 0;
|
||||
|
||||
final GameInformation information = new GameInformation(type);
|
||||
information.players[0] = playerInformation;
|
||||
information.players[0].name = user;
|
||||
information.players[1].name = opponent;
|
||||
|
||||
switch (type) {
|
||||
case TICTACTOE -> new TicTacToeGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage);
|
||||
case REVERSI -> new ReversiGame(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage);
|
||||
case CONNECT4 -> new Connect4Game(information, myTurn, this::forfeitGame, this::exitGame, this::sendMessage);
|
||||
default -> ViewStack.push(new ErrorView("Unsupported game type."));
|
||||
}
|
||||
}
|
||||
}) */
|
||||
ViewStack.pop();
|
||||
isSingleGame.set(true);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private void handleMatchResponse(NetworkEvents.GameMatchResponse response) {
|
||||
@@ -141,7 +112,7 @@ public final class Server {
|
||||
|
||||
final GameInformation.Type type = gameToType(gameType);
|
||||
if (type == null) {
|
||||
ViewStack.push(new ErrorView("Unsupported game type: " + gameType));
|
||||
new ErrorPopup("Unsupported game type: " + gameType);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -164,7 +135,7 @@ public final class Server {
|
||||
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."));
|
||||
default -> new ErrorPopup("Unsupported game type.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,17 +146,11 @@ public final class Server {
|
||||
String challengerName = extractQuotedValue(response.challengerName());
|
||||
String gameType = extractQuotedValue(response.gameType());
|
||||
final String finalGameType = gameType;
|
||||
ViewStack.push(new ChallengeView(challengerName, gameType, (playerInformation) -> {
|
||||
new ChallengePopup(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 -> {
|
||||
|
||||
|
||||
//});
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private void sendMessage(String message) {
|
||||
@@ -196,7 +161,7 @@ public final class Server {
|
||||
new EventFlow().addPostEvent(new NetworkEvents.CloseClient(clientId)).postEvent();
|
||||
isPolling = false;
|
||||
stopScheduler();
|
||||
ViewStack.push(new OnlineView());
|
||||
primary.transitionPrevious();
|
||||
}
|
||||
|
||||
private void forfeitGame() {
|
||||
@@ -205,15 +170,11 @@ public final class Server {
|
||||
|
||||
private void exitGame() {
|
||||
forfeitGame();
|
||||
ViewStack.push(view);
|
||||
startPopulateScheduler();
|
||||
}
|
||||
|
||||
private void gameOver(){
|
||||
ViewStack.pop();
|
||||
ViewStack.push(view);
|
||||
startPopulateScheduler();
|
||||
|
||||
}
|
||||
|
||||
private void startPopulateScheduler() {
|
||||
@@ -227,7 +188,7 @@ public final class Server {
|
||||
onlinePlayers.clear();
|
||||
onlinePlayers.addAll(List.of(e.playerlist()));
|
||||
onlinePlayers.removeIf(name -> name.equalsIgnoreCase(user));
|
||||
view.update(onlinePlayers);
|
||||
primary.update(onlinePlayers);
|
||||
}
|
||||
}, false);
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ public final class ReversiCanvas extends GameCanvas {
|
||||
}
|
||||
|
||||
public void drawLegalPosition(Color color, int cell) {
|
||||
fill(new Color(color.getRed() * 0.25, color.getGreen() * 0.25, color.getBlue() * 0.25, 1.0), cell);
|
||||
drawDot(new Color(color.getRed() * 0.5, color.getGreen() * 0.5, color.getBlue() * 0.5, 1.0), cell);
|
||||
drawDot(new Color(color.getRed(), color.getGreen(), color.getBlue(), 0.25), cell);
|
||||
}
|
||||
}
|
||||
120
app/src/main/java/org/toop/app/game/BaseGameThread.java
Normal file
120
app/src/main/java/org/toop/app/game/BaseGameThread.java
Normal file
@@ -0,0 +1,120 @@
|
||||
package org.toop.app.game;
|
||||
|
||||
import org.toop.app.GameInformation;
|
||||
import org.toop.app.widget.WidgetContainer;
|
||||
import org.toop.app.widget.view.GameView;
|
||||
import org.toop.framework.eventbus.EventFlow;
|
||||
import org.toop.framework.networking.events.NetworkEvents;
|
||||
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;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public abstract class BaseGameThread<TGame extends Game, TAI, TCanvas> {
|
||||
protected final GameInformation information;
|
||||
protected final int myTurn;
|
||||
protected final Runnable onGameOver;
|
||||
protected final BlockingQueue<Game.Move> moveQueue;
|
||||
|
||||
protected final TGame game;
|
||||
protected final TAI ai;
|
||||
|
||||
protected final GameView primary;
|
||||
protected final TCanvas canvas;
|
||||
|
||||
protected final AtomicBoolean isRunning = new AtomicBoolean(true);
|
||||
|
||||
protected BaseGameThread(
|
||||
GameInformation information,
|
||||
int myTurn,
|
||||
Runnable onForfeit,
|
||||
Runnable onExit,
|
||||
Consumer<String> onMessage,
|
||||
Runnable onGameOver,
|
||||
Supplier<TGame> gameSupplier,
|
||||
Supplier<TAI> aiSupplier,
|
||||
Function<Consumer<Integer>, TCanvas> canvasFactory) {
|
||||
|
||||
this.information = information;
|
||||
this.myTurn = myTurn;
|
||||
this.onGameOver = onGameOver;
|
||||
this.moveQueue = new LinkedBlockingQueue<>();
|
||||
|
||||
this.game = gameSupplier.get();
|
||||
this.ai = aiSupplier.get();
|
||||
|
||||
if (onForfeit == null || onExit == null) {
|
||||
primary = new GameView(null, () -> {
|
||||
isRunning.set(false);
|
||||
WidgetContainer.getCurrentView().transitionPrevious();
|
||||
}, null);
|
||||
} else {
|
||||
primary = new GameView(onForfeit, () -> {
|
||||
isRunning.set(false);
|
||||
onExit.run();
|
||||
}, onMessage);
|
||||
}
|
||||
|
||||
this.canvas = canvasFactory.apply(this::onCellClicked);
|
||||
|
||||
addCanvasToPrimary();
|
||||
|
||||
WidgetContainer.getCurrentView().transitionNext(primary);
|
||||
|
||||
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);
|
||||
|
||||
setGameLabels(myTurn == 0);
|
||||
}
|
||||
|
||||
private void onCellClicked(int cell) {
|
||||
if (!isRunning.get()) return;
|
||||
|
||||
final int currentTurn = getCurrentTurn();
|
||||
if (!information.players[currentTurn].isHuman) return;
|
||||
|
||||
final char value = getSymbolForTurn(currentTurn);
|
||||
|
||||
try {
|
||||
moveQueue.put(new Game.Move(cell, value));
|
||||
} catch (InterruptedException _) {}
|
||||
}
|
||||
|
||||
protected void gameOver() {
|
||||
if (onGameOver != null) {
|
||||
isRunning.set(false);
|
||||
onGameOver.run();
|
||||
}
|
||||
}
|
||||
|
||||
protected void setGameLabels(boolean isMe) {
|
||||
final int currentTurn = getCurrentTurn();
|
||||
final String turnName = getNameForTurn(currentTurn);
|
||||
|
||||
primary.nextPlayer(
|
||||
isMe,
|
||||
information.players[isMe ? 0 : 1].name,
|
||||
turnName,
|
||||
information.players[isMe ? 1 : 0].name
|
||||
);
|
||||
}
|
||||
|
||||
protected abstract void addCanvasToPrimary();
|
||||
|
||||
protected abstract int getCurrentTurn();
|
||||
protected abstract char getSymbolForTurn(int turn);
|
||||
protected abstract String getNameForTurn(int turn);
|
||||
|
||||
protected abstract void onMoveResponse(NetworkEvents.GameMoveResponse response);
|
||||
protected abstract void onYourTurnResponse(NetworkEvents.YourTurnResponse response);
|
||||
|
||||
protected abstract void localGameThread();
|
||||
}
|
||||
@@ -4,9 +4,8 @@ import javafx.animation.SequentialTransition;
|
||||
import org.toop.app.App;
|
||||
import org.toop.app.GameInformation;
|
||||
import org.toop.app.canvas.ReversiCanvas;
|
||||
import org.toop.app.view.ViewStack;
|
||||
import org.toop.app.view.views.GameView;
|
||||
import org.toop.app.view.views.LocalMultiplayerView;
|
||||
import org.toop.app.widget.WidgetContainer;
|
||||
import org.toop.app.widget.view.GameView;
|
||||
import org.toop.framework.eventbus.EventFlow;
|
||||
import org.toop.framework.networking.events.NetworkEvents;
|
||||
import org.toop.game.Game;
|
||||
@@ -25,13 +24,13 @@ public final class ReversiGame {
|
||||
private final GameInformation information;
|
||||
|
||||
private final int myTurn;
|
||||
private Runnable onGameOver;
|
||||
private final Runnable onGameOver;
|
||||
private final BlockingQueue<Game.Move> moveQueue;
|
||||
|
||||
private final Reversi game;
|
||||
private final ReversiAI ai;
|
||||
|
||||
private final GameView view;
|
||||
private final GameView primary;
|
||||
private final ReversiCanvas canvas;
|
||||
|
||||
private final AtomicBoolean isRunning;
|
||||
@@ -42,7 +41,7 @@ public final class ReversiGame {
|
||||
|
||||
this.myTurn = myTurn;
|
||||
this.onGameOver = onGameOver;
|
||||
moveQueue = new LinkedBlockingQueue<Game.Move>();
|
||||
moveQueue = new LinkedBlockingQueue<>();
|
||||
|
||||
game = new Reversi();
|
||||
ai = new ReversiAI();
|
||||
@@ -51,12 +50,12 @@ public final class ReversiGame {
|
||||
isPaused = new AtomicBoolean(false);
|
||||
|
||||
if (onForfeit == null || onExit == null) {
|
||||
view = new GameView(null, () -> {
|
||||
primary = new GameView(null, () -> {
|
||||
isRunning.set(false);
|
||||
ViewStack.push(new LocalMultiplayerView(information));
|
||||
WidgetContainer.getCurrentView().transitionPrevious();
|
||||
}, null);
|
||||
} else {
|
||||
view = new GameView(onForfeit, () -> {
|
||||
primary = new GameView(onForfeit, () -> {
|
||||
isRunning.set(false);
|
||||
onExit.run();
|
||||
}, onMessage);
|
||||
@@ -84,8 +83,8 @@ public final class ReversiGame {
|
||||
}
|
||||
});
|
||||
|
||||
view.add(Pos.CENTER, canvas.getCanvas());
|
||||
ViewStack.push(view);
|
||||
primary.add(Pos.CENTER, canvas.getCanvas());
|
||||
WidgetContainer.getCurrentView().transitionNext(primary);
|
||||
|
||||
if (onForfeit == null || onExit == null) {
|
||||
new Thread(this::localGameThread).start();
|
||||
@@ -93,8 +92,7 @@ public final class ReversiGame {
|
||||
} else {
|
||||
new EventFlow()
|
||||
.listen(NetworkEvents.GameMoveResponse.class, this::onMoveResponse)
|
||||
.listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse)
|
||||
.listen(NetworkEvents.ReceivedMessage.class, this::onReceivedMessage);
|
||||
.listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse);
|
||||
|
||||
setGameLabels(myTurn == 0);
|
||||
}
|
||||
@@ -120,7 +118,7 @@ public final class ReversiGame {
|
||||
final String currentValue = currentTurn == 0? "BLACK" : "WHITE";
|
||||
final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type);
|
||||
|
||||
view.nextPlayer(information.players[currentTurn].isHuman,
|
||||
primary.nextPlayer(information.players[currentTurn].isHuman,
|
||||
information.players[currentTurn].name,
|
||||
currentValue,
|
||||
information.players[nextTurn].name);
|
||||
@@ -164,9 +162,9 @@ public final class ReversiGame {
|
||||
|
||||
if (state != Game.State.NORMAL) {
|
||||
if (state == Game.State.WIN) {
|
||||
view.gameOver(true, information.players[currentTurn].name);
|
||||
primary.gameOver(true, information.players[currentTurn].name);
|
||||
} else if (state == Game.State.DRAW) {
|
||||
view.gameOver(false, "");
|
||||
primary.gameOver(false, "");
|
||||
}
|
||||
|
||||
isRunning.set(false);
|
||||
@@ -193,14 +191,14 @@ public final class ReversiGame {
|
||||
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);
|
||||
primary.gameOver(true, information.players[0].name);
|
||||
gameOver();
|
||||
} else {
|
||||
view.gameOver(false, information.players[1].name);
|
||||
primary.gameOver(false, information.players[1].name);
|
||||
gameOver();
|
||||
}
|
||||
} else if (state == Game.State.DRAW) {
|
||||
view.gameOver(false, "");
|
||||
primary.gameOver(false, "");
|
||||
game.play(move);
|
||||
}
|
||||
}
|
||||
@@ -241,14 +239,6 @@ public final class ReversiGame {
|
||||
.postEvent();
|
||||
}
|
||||
|
||||
private void onReceivedMessage(NetworkEvents.ReceivedMessage msg) {
|
||||
if (!isRunning.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.updateChat(msg.message());
|
||||
}
|
||||
|
||||
private void updateCanvas(boolean animate) {
|
||||
// Todo: this is very inefficient. still very fast but if the grid is bigger it might cause issues. improve.
|
||||
canvas.clearAll();
|
||||
@@ -266,8 +256,8 @@ public final class ReversiGame {
|
||||
final SequentialTransition animation = new SequentialTransition();
|
||||
isPaused.set(true);
|
||||
|
||||
final Color fromColor = game.getCurrentPlayer() == 0? Color.WHITE : Color.BLACK;
|
||||
final Color toColor = game.getCurrentPlayer() == 0? Color.BLACK : Color.WHITE;
|
||||
final Color fromColor = game.getCurrentPlayer() == 'W'? Color.WHITE : Color.BLACK;
|
||||
final Color toColor = game.getCurrentPlayer() == 'W'? Color.BLACK : Color.WHITE;
|
||||
|
||||
if (animate && flipped != null) {
|
||||
for (final Game.Move flip : flipped) {
|
||||
@@ -283,7 +273,7 @@ public final class ReversiGame {
|
||||
final Game.Move[] legalMoves = game.getLegalMoves();
|
||||
|
||||
for (final Game.Move legalMove : legalMoves) {
|
||||
canvas.drawLegalPosition(toColor, legalMove.position());
|
||||
canvas.drawLegalPosition(fromColor, legalMove.position());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -294,7 +284,7 @@ public final class ReversiGame {
|
||||
final int currentTurn = game.getCurrentTurn();
|
||||
final String currentValue = currentTurn == 0? "BLACK" : "WHITE";
|
||||
|
||||
view.nextPlayer(isMe,
|
||||
primary.nextPlayer(isMe,
|
||||
information.players[isMe? 0 : 1].name,
|
||||
currentValue,
|
||||
information.players[isMe? 1 : 0].name);
|
||||
|
||||
@@ -3,9 +3,8 @@ package org.toop.app.game;
|
||||
import org.toop.app.App;
|
||||
import org.toop.app.GameInformation;
|
||||
import org.toop.app.canvas.TicTacToeCanvas;
|
||||
import org.toop.app.view.ViewStack;
|
||||
import org.toop.app.view.views.GameView;
|
||||
import org.toop.app.view.views.LocalMultiplayerView;
|
||||
import org.toop.app.widget.WidgetContainer;
|
||||
import org.toop.app.widget.view.GameView;
|
||||
import org.toop.framework.eventbus.EventFlow;
|
||||
import org.toop.framework.networking.events.NetworkEvents;
|
||||
import org.toop.game.Game;
|
||||
@@ -24,13 +23,13 @@ public final class TicTacToeGame {
|
||||
private final GameInformation information;
|
||||
|
||||
private final int myTurn;
|
||||
private Runnable onGameOver;
|
||||
private final Runnable onGameOver;
|
||||
private final BlockingQueue<Game.Move> moveQueue;
|
||||
|
||||
private final TicTacToe game;
|
||||
private final TicTacToeAI ai;
|
||||
|
||||
private final GameView view;
|
||||
private final GameView primary;
|
||||
private final TicTacToeCanvas canvas;
|
||||
|
||||
private final AtomicBoolean isRunning;
|
||||
@@ -48,12 +47,12 @@ public final class TicTacToeGame {
|
||||
isRunning = new AtomicBoolean(true);
|
||||
|
||||
if (onForfeit == null || onExit == null) {
|
||||
view = new GameView(null, () -> {
|
||||
primary = new GameView(null, () -> {
|
||||
isRunning.set(false);
|
||||
ViewStack.push(new LocalMultiplayerView(information));
|
||||
WidgetContainer.getCurrentView().transitionPrevious();
|
||||
}, null);
|
||||
} else {
|
||||
view = new GameView(onForfeit, () -> {
|
||||
primary = new GameView(onForfeit, () -> {
|
||||
isRunning.set(false);
|
||||
onExit.run();
|
||||
}, onMessage);
|
||||
@@ -81,16 +80,15 @@ public final class TicTacToeGame {
|
||||
}
|
||||
});
|
||||
|
||||
view.add(Pos.CENTER, canvas.getCanvas());
|
||||
ViewStack.push(view);
|
||||
primary.add(Pos.CENTER, canvas.getCanvas());
|
||||
WidgetContainer.getCurrentView().transitionNext(primary);
|
||||
|
||||
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);
|
||||
.listen(NetworkEvents.YourTurnResponse.class, this::onYourTurnResponse);
|
||||
|
||||
setGameLabels(myTurn == 0);
|
||||
}
|
||||
@@ -106,7 +104,7 @@ public final class TicTacToeGame {
|
||||
final String currentValue = currentTurn == 0? "X" : "O";
|
||||
final int nextTurn = (currentTurn + 1) % GameInformation.Type.playerCount(information.type);
|
||||
|
||||
view.nextPlayer(information.players[currentTurn].isHuman,
|
||||
primary.nextPlayer(information.players[currentTurn].isHuman,
|
||||
information.players[currentTurn].name,
|
||||
currentValue,
|
||||
information.players[nextTurn].name);
|
||||
@@ -155,9 +153,9 @@ public final class TicTacToeGame {
|
||||
|
||||
if (state != Game.State.NORMAL) {
|
||||
if (state == Game.State.WIN) {
|
||||
view.gameOver(true, information.players[currentTurn].name);
|
||||
primary.gameOver(true, information.players[currentTurn].name);
|
||||
} else if (state == Game.State.DRAW) {
|
||||
view.gameOver(false, "");
|
||||
primary.gameOver(false, "");
|
||||
}
|
||||
|
||||
isRunning.set(false);
|
||||
@@ -184,15 +182,15 @@ public final class TicTacToeGame {
|
||||
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);
|
||||
primary.gameOver(true, information.players[0].name);
|
||||
gameOver();
|
||||
} else {
|
||||
view.gameOver(false, information.players[1].name);
|
||||
primary.gameOver(false, information.players[1].name);
|
||||
gameOver();
|
||||
}
|
||||
} 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, "");
|
||||
primary.gameOver(false, "");
|
||||
gameOver();
|
||||
}
|
||||
}
|
||||
@@ -244,19 +242,11 @@ public final class TicTacToeGame {
|
||||
.postEvent();
|
||||
}
|
||||
|
||||
private void onReceivedMessage(NetworkEvents.ReceivedMessage msg) {
|
||||
if (!isRunning.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.updateChat(msg.message());
|
||||
}
|
||||
|
||||
private void setGameLabels(boolean isMe) {
|
||||
final int currentTurn = game.getCurrentTurn();
|
||||
final String currentValue = currentTurn == 0? "X" : "O";
|
||||
|
||||
view.nextPlayer(isMe,
|
||||
primary.nextPlayer(isMe,
|
||||
information.players[isMe? 0 : 1].name,
|
||||
currentValue,
|
||||
information.players[isMe? 1 : 0].name);
|
||||
|
||||
177
app/src/main/java/org/toop/app/game/TicTacToeGameThread.java
Normal file
177
app/src/main/java/org/toop/app/game/TicTacToeGameThread.java
Normal file
@@ -0,0 +1,177 @@
|
||||
package org.toop.app.game;
|
||||
|
||||
import org.toop.app.App;
|
||||
import org.toop.app.GameInformation;
|
||||
import org.toop.app.canvas.TicTacToeCanvas;
|
||||
import org.toop.framework.eventbus.EventFlow;
|
||||
import org.toop.framework.networking.events.NetworkEvents;
|
||||
import org.toop.game.Game;
|
||||
import org.toop.game.tictactoe.TicTacToe;
|
||||
import org.toop.game.tictactoe.TicTacToeAI;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
public final class TicTacToeGameThread extends BaseGameThread<TicTacToe, TicTacToeAI, TicTacToeCanvas> {
|
||||
public TicTacToeGameThread(GameInformation info, int myTurn, Runnable onForfeit, Runnable onExit, Consumer<String> onMessage, Runnable onGameOver) {
|
||||
super(info, myTurn, onForfeit, onExit, onMessage, onGameOver,
|
||||
TicTacToe::new,
|
||||
TicTacToeAI::new,
|
||||
clickHandler -> new TicTacToeCanvas(Color.GRAY, (App.getHeight() / 4) * 3, (App.getHeight() / 4) * 3, clickHandler)
|
||||
);
|
||||
}
|
||||
|
||||
public TicTacToeGameThread(GameInformation info) {
|
||||
this(info, 0, null, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addCanvasToPrimary() {
|
||||
primary.add(Pos.CENTER, canvas.getCanvas());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getCurrentTurn() {
|
||||
return game.getCurrentTurn();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected char getSymbolForTurn(int turn) {
|
||||
return turn == 0 ? 'X' : 'O';
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getNameForTurn(int turn) {
|
||||
return turn == 0 ? "X" : "O";
|
||||
}
|
||||
|
||||
private void drawMove(Game.Move move) {
|
||||
if (move.value() == 'X') canvas.drawX(Color.RED, move.position());
|
||||
else canvas.drawO(Color.BLUE, move.position());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected 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)) {
|
||||
primary.gameOver(true, information.players[0].name);
|
||||
gameOver();
|
||||
} else {
|
||||
primary.gameOver(false, information.players[1].name);
|
||||
gameOver();
|
||||
}
|
||||
} else if (state == Game.State.DRAW) {
|
||||
if (game.getLegalMoves().length == 0) {
|
||||
primary.gameOver(false, "");
|
||||
gameOver();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawMove(move);
|
||||
setGameLabels(game.getCurrentTurn() == myTurn);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected 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;
|
||||
if (information.players[1].name.equalsIgnoreCase("pism")) {
|
||||
move = ai.findWorstMove(game,9);
|
||||
}else{
|
||||
move = ai.findBestMove(game, information.players[0].computerDifficulty);
|
||||
}
|
||||
|
||||
assert move != null;
|
||||
position = move.position();
|
||||
}
|
||||
|
||||
new EventFlow().addPostEvent(new NetworkEvents.SendMove(response.clientId(), (short)position))
|
||||
.postEvent();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void localGameThread() {
|
||||
while (isRunning.get()) {
|
||||
final int currentTurn = game.getCurrentTurn();
|
||||
setGameLabels(currentTurn == myTurn);
|
||||
|
||||
Game.Move move = null;
|
||||
|
||||
if (information.players[currentTurn].isHuman) {
|
||||
try {
|
||||
final Game.Move wants = moveQueue.take();
|
||||
final Game.Move[] legalMoves = game.getLegalMoves();
|
||||
|
||||
for (final Game.Move legalMove : legalMoves) {
|
||||
if (legalMove.position() == wants.position() &&
|
||||
legalMove.value() == wants.value()) {
|
||||
move = wants;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException _) {}
|
||||
} else {
|
||||
final long start = System.currentTimeMillis();
|
||||
|
||||
move = ai.findBestMove(game, information.players[currentTurn].computerDifficulty);
|
||||
|
||||
if (information.players[currentTurn].computerThinkTime > 0) {
|
||||
final long elapsedTime = System.currentTimeMillis() - start;
|
||||
final long sleepTime = information.players[currentTurn].computerThinkTime * 1000L - elapsedTime;
|
||||
|
||||
try {
|
||||
Thread.sleep((long)(sleepTime * Math.random()));
|
||||
} catch (InterruptedException _) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (move == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final Game.State state = game.play(move);
|
||||
drawMove(move);
|
||||
|
||||
if (state != Game.State.NORMAL) {
|
||||
if (state == Game.State.WIN) {
|
||||
primary.gameOver(information.players[currentTurn].isHuman, information.players[currentTurn].name);
|
||||
} else if (state == Game.State.DRAW) {
|
||||
primary.gameOver(false, "");
|
||||
}
|
||||
|
||||
isRunning.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,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.game.TicTacToeGameThread;
|
||||
import org.toop.app.view.View;
|
||||
import org.toop.app.view.ViewStack;
|
||||
import org.toop.app.view.displays.SongDisplay;
|
||||
@@ -45,10 +45,10 @@ public final class LocalMultiplayerView extends View {
|
||||
}
|
||||
|
||||
switch (information.type) {
|
||||
case TICTACTOE: new TicTacToeGame(information); break;
|
||||
case TICTACTOE: new TicTacToeGameThread(information); break;
|
||||
case REVERSI: new ReversiGame(information); break;
|
||||
case CONNECT4: new Connect4Game(information); break;
|
||||
//case BATTLESHIP: new BattleshipGame(information); break;
|
||||
// case BATTLESHIP: new BattleshipGame(information); break;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -23,8 +23,10 @@ public final class Primitive {
|
||||
var header = new Text();
|
||||
header.getStyleClass().add("header");
|
||||
|
||||
header.setText(AppContext.getString(key));
|
||||
header.textProperty().bind(AppContext.bindToKey(key));
|
||||
if (!key.isEmpty()) {
|
||||
header.setText(AppContext.getString(key));
|
||||
header.textProperty().bind(AppContext.bindToKey(key));
|
||||
}
|
||||
|
||||
return header;
|
||||
}
|
||||
@@ -33,8 +35,10 @@ public final class Primitive {
|
||||
var text = new Text();
|
||||
text.getStyleClass().add("text");
|
||||
|
||||
text.setText(AppContext.getString(key));
|
||||
text.textProperty().bind(AppContext.bindToKey(key));
|
||||
if (!key.isEmpty()) {
|
||||
text.setText(AppContext.getString(key));
|
||||
text.textProperty().bind(AppContext.bindToKey(key));
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
@@ -43,8 +47,10 @@ public final class Primitive {
|
||||
var button = new Button();
|
||||
button.getStyleClass().add("button");
|
||||
|
||||
button.setText(AppContext.getString(key));
|
||||
button.textProperty().bind(AppContext.bindToKey(key));
|
||||
if (!key.isEmpty()) {
|
||||
button.setText(AppContext.getString(key));
|
||||
button.textProperty().bind(AppContext.bindToKey(key));
|
||||
}
|
||||
|
||||
if (onAction != null) {
|
||||
button.setOnAction(_ ->
|
||||
@@ -58,8 +64,10 @@ public final class Primitive {
|
||||
var input = new TextField();
|
||||
input.getStyleClass().add("input");
|
||||
|
||||
input.setPromptText(AppContext.getString(promptKey));
|
||||
input.promptTextProperty().bind(AppContext.bindToKey(promptKey));
|
||||
if (!promptKey.isEmpty()) {
|
||||
input.setPromptText(AppContext.getString(promptKey));
|
||||
input.promptTextProperty().bind(AppContext.bindToKey(promptKey));
|
||||
}
|
||||
|
||||
input.setText(text);
|
||||
|
||||
@@ -133,7 +141,11 @@ public final class Primitive {
|
||||
hbox.getStyleClass().add("container");
|
||||
hbox.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
|
||||
|
||||
hbox.getChildren().addAll(nodes);
|
||||
for (var node : nodes) {
|
||||
if (node != null) {
|
||||
hbox.getChildren().add(node);
|
||||
}
|
||||
}
|
||||
|
||||
return hbox;
|
||||
}
|
||||
@@ -143,7 +155,11 @@ public final class Primitive {
|
||||
vbox.getStyleClass().add("container");
|
||||
vbox.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
|
||||
|
||||
vbox.getChildren().addAll(nodes);
|
||||
for (var node : nodes) {
|
||||
if (node != null) {
|
||||
vbox.getChildren().add(node);
|
||||
}
|
||||
}
|
||||
|
||||
return vbox;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
package org.toop.app.widget;
|
||||
|
||||
import org.toop.app.widget.complex.PopupWidget;
|
||||
import org.toop.app.widget.complex.PrimaryWidget;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import org.toop.app.widget.complex.ViewWidget;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
public final class WidgetContainer {
|
||||
private static final Deque<PopupWidget> popups = new ArrayDeque<>();
|
||||
|
||||
private static StackPane root;
|
||||
private static ViewWidget currentView;
|
||||
|
||||
public static synchronized StackPane setup() {
|
||||
if (root != null) {
|
||||
@@ -21,7 +17,7 @@ public final class WidgetContainer {
|
||||
}
|
||||
|
||||
root = new StackPane();
|
||||
root.getStyleClass().add("bg-primary");
|
||||
root.getStyleClass().add("bg-view");
|
||||
|
||||
return root;
|
||||
}
|
||||
@@ -38,15 +34,14 @@ public final class WidgetContainer {
|
||||
|
||||
StackPane.setAlignment(widget.getNode(), position);
|
||||
|
||||
if (widget instanceof PrimaryWidget) {
|
||||
root.getChildren().addFirst(widget.getNode());
|
||||
if (widget instanceof ViewWidget view) {
|
||||
root.getChildren().addFirst(view.getNode());
|
||||
currentView = view;
|
||||
} else if (widget instanceof PopupWidget popup) {
|
||||
currentView.add(Pos.CENTER, popup);
|
||||
} else {
|
||||
root.getChildren().add(widget.getNode());
|
||||
}
|
||||
|
||||
if (widget instanceof PopupWidget popup) {
|
||||
popups.push(popup);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,15 +51,15 @@ public final class WidgetContainer {
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
root.getChildren().remove(widget.getNode());
|
||||
|
||||
if (widget instanceof PrimaryWidget) {
|
||||
for (var popup : popups) {
|
||||
root.getChildren().remove(popup.getNode());
|
||||
}
|
||||
|
||||
popups.clear();
|
||||
if (widget instanceof PopupWidget popup) {
|
||||
currentView.remove(popup);
|
||||
} else {
|
||||
root.getChildren().remove(widget.getNode());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static ViewWidget getCurrentView() {
|
||||
return currentView;
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,21 @@ import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
|
||||
public class ConfirmWidget implements Widget {
|
||||
private final HBox buttonsContainer;
|
||||
private final Text messageText;
|
||||
private final VBox container;
|
||||
|
||||
public ConfirmWidget(String confirm) {
|
||||
buttonsContainer = Primitive.hbox();
|
||||
container = Primitive.vbox(Primitive.header(confirm), buttonsContainer);
|
||||
messageText = Primitive.text("");
|
||||
container = Primitive.vbox(Primitive.header(confirm), messageText, Primitive.separator(), buttonsContainer);
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
messageText.setText(message);
|
||||
}
|
||||
|
||||
public void addButton(String key, Runnable onClick) {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package org.toop.app.widget.complex;
|
||||
|
||||
import org.toop.app.GameInformation;
|
||||
import org.toop.app.widget.Primitive;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
public class PlayerInfoWidget {
|
||||
private final GameInformation.Player information;
|
||||
private final VBox container;
|
||||
|
||||
public PlayerInfoWidget(GameInformation.Player information) {
|
||||
this.information = information;
|
||||
container = Primitive.vbox(
|
||||
buildToggle().getNode(),
|
||||
buildContent()
|
||||
);
|
||||
}
|
||||
|
||||
private ToggleWidget buildToggle() {
|
||||
return new ToggleWidget(
|
||||
"computer", "player",
|
||||
information.isHuman,
|
||||
isHuman -> {
|
||||
information.isHuman = isHuman;
|
||||
container.getChildren().setAll(
|
||||
buildToggle().getNode(),
|
||||
buildContent()
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private Node buildContent() {
|
||||
if (information.isHuman) {
|
||||
var nameInput = new LabeledInputWidget(
|
||||
"name",
|
||||
"enter-your-name",
|
||||
information.name,
|
||||
newName -> information.name = newName
|
||||
);
|
||||
|
||||
return nameInput.getNode();
|
||||
} else {
|
||||
if (information.name == null || information.name.isEmpty()) {
|
||||
information.name = "Pism Bot";
|
||||
}
|
||||
|
||||
var playerName = Primitive.text("");
|
||||
playerName.setText(information.name);
|
||||
|
||||
var nameDisplay = Primitive.vbox(
|
||||
Primitive.text("name"),
|
||||
playerName
|
||||
);
|
||||
|
||||
var difficultySlider = new LabeledSliderWidget(
|
||||
"computer-difficulty",
|
||||
0, 5,
|
||||
information.computerDifficulty,
|
||||
newVal -> information.computerDifficulty = newVal
|
||||
);
|
||||
|
||||
var thinkTimeSlider = new LabeledSliderWidget(
|
||||
"computer-think-time",
|
||||
0, 5,
|
||||
information.computerThinkTime,
|
||||
newVal -> information.computerThinkTime = newVal
|
||||
);
|
||||
|
||||
return Primitive.vbox(
|
||||
nameDisplay,
|
||||
difficultySlider.getNode(),
|
||||
thinkTimeSlider.getNode()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public Node getNode() {
|
||||
return container;
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package org.toop.app.widget.complex;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
import org.toop.app.widget.Primitive;
|
||||
|
||||
public abstract class PrimaryWidget extends StackWidget {
|
||||
private PrimaryWidget previous = null;
|
||||
|
||||
public PrimaryWidget() {
|
||||
super("bg-primary");
|
||||
}
|
||||
|
||||
public void transitionNext(PrimaryWidget primary) {
|
||||
primary.previous = this;
|
||||
replace(Pos.CENTER, primary);
|
||||
|
||||
var backButton = Primitive.button("back", () -> {
|
||||
primary.transitionPrevious();
|
||||
});
|
||||
|
||||
primary.add(Pos.BOTTOM_LEFT, Primitive.vbox(backButton));
|
||||
}
|
||||
|
||||
public void transitionPrevious() {
|
||||
if (previous == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
replace(Pos.CENTER, previous);
|
||||
previous = null;
|
||||
}
|
||||
|
||||
public void reload(PrimaryWidget primary) {
|
||||
primary.previous = previous;
|
||||
replace(Pos.CENTER, primary);
|
||||
|
||||
var backButton = Primitive.button("back", () -> {
|
||||
primary.transitionPrevious();
|
||||
});
|
||||
|
||||
primary.add(Pos.BOTTOM_LEFT, Primitive.vbox(backButton));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.toop.app.widget.complex;
|
||||
|
||||
import org.toop.app.widget.Primitive;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
|
||||
public abstract class ViewWidget extends StackWidget {
|
||||
private ViewWidget previous = null;
|
||||
|
||||
public ViewWidget() {
|
||||
super("bg-primary");
|
||||
}
|
||||
|
||||
public void transition(ViewWidget view) {
|
||||
view.previous = this;
|
||||
replace(Pos.CENTER, view);
|
||||
}
|
||||
|
||||
public void transitionNext(ViewWidget view) {
|
||||
view.previous = this;
|
||||
replace(Pos.CENTER, view);
|
||||
|
||||
var backButton = Primitive.button("back", () -> {
|
||||
view.transitionPrevious();
|
||||
});
|
||||
|
||||
view.add(Pos.BOTTOM_LEFT, Primitive.vbox(backButton));
|
||||
}
|
||||
|
||||
public void transitionPrevious() {
|
||||
if (previous == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
replace(Pos.CENTER, previous);
|
||||
previous = null;
|
||||
}
|
||||
|
||||
public void reload(ViewWidget view) {
|
||||
view.previous = previous;
|
||||
replace(Pos.CENTER, view);
|
||||
|
||||
var backButton = Primitive.button("back", () -> {
|
||||
view.transitionPrevious();
|
||||
});
|
||||
|
||||
view.add(Pos.BOTTOM_LEFT, Primitive.vbox(backButton));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.toop.app.widget.popup;
|
||||
|
||||
import org.toop.app.GameInformation;
|
||||
import org.toop.app.widget.Primitive;
|
||||
import org.toop.app.widget.complex.PlayerInfoWidget;
|
||||
import org.toop.app.widget.complex.PopupWidget;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
|
||||
public final class ChallengePopup extends PopupWidget {
|
||||
private final GameInformation.Player playerInformation;
|
||||
private final String challenger;
|
||||
private final String game;
|
||||
private final Consumer<GameInformation.Player> onAccept;
|
||||
|
||||
public ChallengePopup(String challenger, String game, Consumer<GameInformation.Player> onAccept) {
|
||||
this.challenger = challenger;
|
||||
this.game = game;
|
||||
this.onAccept = onAccept;
|
||||
|
||||
this.playerInformation = new GameInformation.Player();
|
||||
|
||||
setupLayout();
|
||||
}
|
||||
|
||||
private void setupLayout() {
|
||||
var challengeText = Primitive.text("you-were-challenged-by");
|
||||
|
||||
var challengerHeader = Primitive.header("");
|
||||
challengerHeader.setText(challenger);
|
||||
|
||||
var gameText = Primitive.text("to-a-game-of");
|
||||
gameText.setText(gameText.getText() + " " + game);
|
||||
|
||||
var acceptButton = Primitive.button("accept", () -> onAccept.accept(playerInformation));
|
||||
var denyButton = Primitive.button("deny", () -> hide());
|
||||
|
||||
var leftSection = Primitive.vbox(
|
||||
challengeText,
|
||||
challengerHeader,
|
||||
gameText,
|
||||
Primitive.separator(),
|
||||
Primitive.hbox(
|
||||
acceptButton,
|
||||
denyButton
|
||||
)
|
||||
);
|
||||
|
||||
var playerInfoWidget = new PlayerInfoWidget(playerInformation);
|
||||
|
||||
add(Pos.CENTER,
|
||||
Primitive.hbox(
|
||||
leftSection,
|
||||
playerInfoWidget.getNode()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
16
app/src/main/java/org/toop/app/widget/popup/ErrorPopup.java
Normal file
16
app/src/main/java/org/toop/app/widget/popup/ErrorPopup.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package org.toop.app.widget.popup;
|
||||
|
||||
import org.toop.app.widget.complex.ConfirmWidget;
|
||||
import org.toop.app.widget.complex.PopupWidget;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
|
||||
public class ErrorPopup extends PopupWidget {
|
||||
public ErrorPopup(String error) {
|
||||
var confirmWidget = new ConfirmWidget("error");
|
||||
confirmWidget.setMessage(error);
|
||||
confirmWidget.addButton("ok", this::hide);
|
||||
|
||||
add(Pos.CENTER, confirmWidget);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.toop.app.widget.popup;
|
||||
|
||||
import org.toop.app.widget.complex.ConfirmWidget;
|
||||
import org.toop.app.widget.complex.PopupWidget;
|
||||
import org.toop.local.AppContext;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
|
||||
public final class GameOverPopup extends PopupWidget {
|
||||
public GameOverPopup(boolean iWon, String winner) {
|
||||
var confirmWidget = new ConfirmWidget("game-over");
|
||||
|
||||
if (winner.isEmpty()) {
|
||||
confirmWidget.setMessage(AppContext.getString("the-game-ended-in-a-draw"));
|
||||
} else if (iWon) {
|
||||
confirmWidget.setMessage(AppContext.getString("you-win"));
|
||||
} else {
|
||||
confirmWidget.setMessage(AppContext.getString("you-lost-against") + ": " + winner);
|
||||
}
|
||||
|
||||
confirmWidget.addButton("ok", () -> hide());
|
||||
|
||||
add(Pos.CENTER, confirmWidget);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package org.toop.app.widget.popup;
|
||||
|
||||
import org.toop.app.GameInformation;
|
||||
import org.toop.app.Server;
|
||||
import org.toop.app.widget.Primitive;
|
||||
import org.toop.app.widget.complex.LabeledChoiceWidget;
|
||||
import org.toop.app.widget.complex.PlayerInfoWidget;
|
||||
import org.toop.app.widget.complex.PopupWidget;
|
||||
import org.toop.local.AppContext;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
public final class SendChallengePopup extends PopupWidget {
|
||||
private final Server server;
|
||||
private final String opponent;
|
||||
private final BiConsumer<GameInformation.Player, String> onSend;
|
||||
|
||||
private final GameInformation.Player playerInformation;
|
||||
|
||||
public SendChallengePopup(Server server, String opponent, BiConsumer<GameInformation.Player, String> onSend) {
|
||||
this.server = server;
|
||||
this.opponent = opponent;
|
||||
this.onSend = onSend;
|
||||
|
||||
this.playerInformation = new GameInformation.Player();
|
||||
|
||||
setupLayout();
|
||||
}
|
||||
|
||||
private void setupLayout() {
|
||||
// --- Left side: challenge text and buttons ---
|
||||
var challengeText = Primitive.text("challenge");
|
||||
|
||||
var opponentHeader = Primitive.header(opponent);
|
||||
|
||||
var gameText = Primitive.text("to-a-game-of");
|
||||
|
||||
var games = server.getGameList();
|
||||
var gameChoice = new LabeledChoiceWidget<>(
|
||||
"game",
|
||||
new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(String game) {
|
||||
return AppContext.getString(game);
|
||||
}
|
||||
@Override
|
||||
public String fromString(String s) { return null; }
|
||||
},
|
||||
games.getFirst(),
|
||||
newGame -> {
|
||||
playerInformation.computerDifficulty = Math.min(
|
||||
playerInformation.computerDifficulty,
|
||||
GameInformation.Type.maxDepth(Server.gameToType(newGame))
|
||||
);
|
||||
},
|
||||
games.toArray(new String[0])
|
||||
);
|
||||
|
||||
var sendButton = Primitive.button(
|
||||
"send",
|
||||
() -> onSend.accept(playerInformation, gameChoice.getValue())
|
||||
);
|
||||
|
||||
var cancelButton = Primitive.button("cancel", () -> hide());
|
||||
|
||||
var leftSection = Primitive.vbox(
|
||||
challengeText,
|
||||
opponentHeader,
|
||||
gameText,
|
||||
gameChoice.getNode(),
|
||||
Primitive.separator(),
|
||||
Primitive.hbox(
|
||||
sendButton,
|
||||
cancelButton
|
||||
)
|
||||
);
|
||||
|
||||
var playerInfoWidget = new PlayerInfoWidget(playerInformation);
|
||||
|
||||
add(Pos.CENTER,
|
||||
Primitive.hbox(
|
||||
leftSection,
|
||||
playerInfoWidget.getNode()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package org.toop.app.widget.primary;
|
||||
|
||||
import org.toop.app.widget.Primitive;
|
||||
import org.toop.app.widget.complex.PrimaryWidget;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
|
||||
public class LocalPrimary extends PrimaryWidget {
|
||||
public LocalPrimary() {
|
||||
var ticTacToeButton = Primitive.button("tic-tac-toe", () -> {
|
||||
});
|
||||
|
||||
var reversiButton = Primitive.button("reversi", () -> {
|
||||
});
|
||||
|
||||
var connect4Button = Primitive.button("connect4", () -> {
|
||||
});
|
||||
|
||||
add(Pos.CENTER, Primitive.vbox(
|
||||
ticTacToeButton,
|
||||
reversiButton,
|
||||
connect4Button
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.toop.app.widget.primary;
|
||||
package org.toop.app.widget.view;
|
||||
|
||||
import org.toop.app.App;
|
||||
import org.toop.app.widget.Primitive;
|
||||
import org.toop.app.widget.complex.PrimaryWidget;
|
||||
import org.toop.app.widget.complex.ViewWidget;
|
||||
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
@@ -14,8 +14,8 @@ import javafx.scene.layout.Region;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.util.Duration;
|
||||
|
||||
public class CreditsPrimary extends PrimaryWidget {
|
||||
public CreditsPrimary() {
|
||||
public class CreditsView extends ViewWidget {
|
||||
public CreditsView() {
|
||||
var scrumMasterCredit = newCredit("scrum-master", "Stef");
|
||||
var productOwnerCredit = newCredit("product-owner", "Omar");
|
||||
var mergeCommanderCredit = newCredit("merge-commander", "Bas");
|
||||
96
app/src/main/java/org/toop/app/widget/view/GameView.java
Normal file
96
app/src/main/java/org/toop/app/widget/view/GameView.java
Normal file
@@ -0,0 +1,96 @@
|
||||
package org.toop.app.widget.view;
|
||||
|
||||
import org.toop.app.widget.Primitive;
|
||||
import org.toop.app.widget.complex.ViewWidget;
|
||||
import org.toop.app.widget.popup.GameOverPopup;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.text.Text;
|
||||
|
||||
public final class GameView extends ViewWidget {
|
||||
private final Text currentPlayerHeader;
|
||||
private final Text currentMoveHeader;
|
||||
private final Text nextPlayerHeader;
|
||||
|
||||
private final Button forfeitButton;
|
||||
private final Button exitButton;
|
||||
|
||||
private final TextField chatInput;
|
||||
|
||||
public GameView(Runnable onForfeit, Runnable onExit, Consumer<String> onMessage) {
|
||||
currentPlayerHeader = Primitive.header("");
|
||||
currentMoveHeader = Primitive.header("");
|
||||
nextPlayerHeader = Primitive.header("");
|
||||
|
||||
if (onForfeit != null) {
|
||||
forfeitButton = Primitive.button("forfeit", () -> onForfeit.run());
|
||||
} else {
|
||||
forfeitButton = null;
|
||||
}
|
||||
|
||||
exitButton = Primitive.button("exit", () -> {
|
||||
onExit.run();
|
||||
transitionPrevious();
|
||||
});
|
||||
|
||||
if (onMessage != null) {
|
||||
chatInput = Primitive.input("enter-your-message", "", null);
|
||||
chatInput.setOnAction(_ -> {
|
||||
onMessage.accept(chatInput.getText());
|
||||
chatInput.clear();
|
||||
});
|
||||
} else {
|
||||
chatInput = null;
|
||||
}
|
||||
|
||||
setupLayout();
|
||||
}
|
||||
|
||||
private void setupLayout() {
|
||||
var playerInfo = Primitive.vbox(
|
||||
currentPlayerHeader,
|
||||
Primitive.hbox(
|
||||
Primitive.separator(),
|
||||
currentMoveHeader,
|
||||
Primitive.separator()
|
||||
),
|
||||
nextPlayerHeader
|
||||
);
|
||||
|
||||
add(Pos.TOP_RIGHT, playerInfo);
|
||||
|
||||
var buttons = Primitive.vbox(
|
||||
forfeitButton,
|
||||
exitButton
|
||||
);
|
||||
|
||||
add(Pos.BOTTOM_LEFT, buttons);
|
||||
|
||||
if (chatInput != null) {
|
||||
add(Pos.BOTTOM_RIGHT, Primitive.vbox(chatInput));
|
||||
}
|
||||
}
|
||||
|
||||
public void nextPlayer(boolean isMe, String currentPlayer, String currentMove, String nextPlayer) {
|
||||
Platform.runLater(() -> {
|
||||
currentPlayerHeader.setText(currentPlayer);
|
||||
currentMoveHeader.setText(currentMove);
|
||||
nextPlayerHeader.setText(nextPlayer);
|
||||
|
||||
if (isMe) {
|
||||
currentPlayerHeader.getStyleClass().add("my-turn");
|
||||
} else {
|
||||
currentPlayerHeader.getStyleClass().remove("my-turn");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void gameOver(boolean iWon, String winner) {
|
||||
new GameOverPopup(iWon, winner).show(Pos.CENTER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.toop.app.widget.view;
|
||||
|
||||
import org.toop.app.GameInformation;
|
||||
import org.toop.app.game.Connect4Game;
|
||||
import org.toop.app.game.ReversiGame;
|
||||
import org.toop.app.game.TicTacToeGameThread;
|
||||
import org.toop.app.widget.Primitive;
|
||||
import org.toop.app.widget.complex.PlayerInfoWidget;
|
||||
import org.toop.app.widget.complex.ViewWidget;
|
||||
import org.toop.app.widget.popup.ErrorPopup;
|
||||
import org.toop.local.AppContext;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
public class LocalMultiplayerView extends ViewWidget {
|
||||
private final GameInformation information;
|
||||
|
||||
public LocalMultiplayerView(GameInformation.Type type) {
|
||||
this(new GameInformation(type));
|
||||
}
|
||||
|
||||
public LocalMultiplayerView(GameInformation information) {
|
||||
this.information = information;
|
||||
var playButton = Primitive.button("play", () -> {
|
||||
for (var player : information.players) {
|
||||
if (player.isHuman && player.name.isEmpty()) {
|
||||
new ErrorPopup(AppContext.getString("please-enter-your-name")).show(Pos.CENTER);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (information.type) {
|
||||
case TICTACTOE -> new TicTacToeGameThread(information);
|
||||
case REVERSI -> new ReversiGame(information);
|
||||
case CONNECT4 -> new Connect4Game(information);
|
||||
// case BATTLESHIP -> new BattleshipGame(information);
|
||||
}
|
||||
});
|
||||
|
||||
var playerSection = setupPlayerSections();
|
||||
|
||||
add(Pos.CENTER, Primitive.vbox(
|
||||
playerSection,
|
||||
Primitive.separator(),
|
||||
playButton
|
||||
));
|
||||
}
|
||||
|
||||
private ScrollPane setupPlayerSections() {
|
||||
int playerCount = GameInformation.Type.playerCount(information.type);
|
||||
VBox[] playerBoxes = new VBox[playerCount];
|
||||
|
||||
for (int i = 0; i < playerCount; i++) {
|
||||
var player = information.players[i];
|
||||
|
||||
var playerHeader = Primitive.header("");
|
||||
playerHeader.setText("player" + " #" + (i + 1));
|
||||
|
||||
var playerWidget = new PlayerInfoWidget(player);
|
||||
|
||||
playerBoxes[i] = Primitive.vbox(
|
||||
playerHeader,
|
||||
Primitive.separator(),
|
||||
playerWidget.getNode()
|
||||
);
|
||||
}
|
||||
|
||||
return Primitive.scroll(Primitive.hbox(
|
||||
playerBoxes
|
||||
));
|
||||
}
|
||||
}
|
||||
29
app/src/main/java/org/toop/app/widget/view/LocalView.java
Normal file
29
app/src/main/java/org/toop/app/widget/view/LocalView.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package org.toop.app.widget.view;
|
||||
|
||||
import org.toop.app.GameInformation;
|
||||
import org.toop.app.widget.Primitive;
|
||||
import org.toop.app.widget.complex.ViewWidget;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
|
||||
public class LocalView extends ViewWidget {
|
||||
public LocalView() {
|
||||
var ticTacToeButton = Primitive.button("tic-tac-toe", () -> {
|
||||
transitionNext(new LocalMultiplayerView(GameInformation.Type.TICTACTOE));
|
||||
});
|
||||
|
||||
var reversiButton = Primitive.button("reversi", () -> {
|
||||
transitionNext(new LocalMultiplayerView(GameInformation.Type.REVERSI));
|
||||
});
|
||||
|
||||
var connect4Button = Primitive.button("connect4", () -> {
|
||||
transitionNext(new LocalMultiplayerView(GameInformation.Type.CONNECT4));
|
||||
});
|
||||
|
||||
add(Pos.CENTER, Primitive.vbox(
|
||||
ticTacToeButton,
|
||||
reversiButton,
|
||||
connect4Button
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,27 @@
|
||||
package org.toop.app.widget.primary;
|
||||
package org.toop.app.widget.view;
|
||||
|
||||
import org.toop.app.App;
|
||||
import org.toop.app.widget.Primitive;
|
||||
import org.toop.app.widget.complex.PrimaryWidget;
|
||||
import org.toop.app.widget.complex.ViewWidget;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
|
||||
public class MainPrimary extends PrimaryWidget {
|
||||
public MainPrimary() {
|
||||
public class MainView extends ViewWidget {
|
||||
public MainView() {
|
||||
var localButton = Primitive.button("local", () -> {
|
||||
transitionNext(new LocalPrimary());
|
||||
transitionNext(new LocalView());
|
||||
});
|
||||
|
||||
var onlineButton = Primitive.button("online", () -> {
|
||||
transitionNext(new OnlinePrimary());
|
||||
transitionNext(new OnlineView());
|
||||
});
|
||||
|
||||
var creditsButton = Primitive.button("credits", () -> {
|
||||
transitionNext(new CreditsPrimary());
|
||||
transitionNext(new CreditsView());
|
||||
});
|
||||
|
||||
var optionsButton = Primitive.button("options", () -> {
|
||||
transitionNext(new OptionsPrimary());
|
||||
transitionNext(new OptionsView());
|
||||
});
|
||||
|
||||
var quitButton = Primitive.button("quit", () -> {
|
||||
@@ -1,14 +1,14 @@
|
||||
package org.toop.app.widget.primary;
|
||||
package org.toop.app.widget.view;
|
||||
|
||||
import org.toop.app.Server;
|
||||
import org.toop.app.widget.Primitive;
|
||||
import org.toop.app.widget.complex.LabeledInputWidget;
|
||||
import org.toop.app.widget.complex.PrimaryWidget;
|
||||
import org.toop.app.widget.complex.ViewWidget;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
|
||||
public class OnlinePrimary extends PrimaryWidget {
|
||||
public OnlinePrimary() {
|
||||
public class OnlineView extends ViewWidget {
|
||||
public OnlineView() {
|
||||
var serverInformationHeader = Primitive.header("server-information");
|
||||
|
||||
var serverIPInput = new LabeledInputWidget("ip-address", "enter-the-server-ip", "", _ -> {});
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.toop.app.widget.primary;
|
||||
package org.toop.app.widget.view;
|
||||
|
||||
import org.toop.app.App;
|
||||
import org.toop.app.widget.Primitive;
|
||||
import org.toop.app.widget.complex.LabeledChoiceWidget;
|
||||
import org.toop.app.widget.complex.LabeledSliderWidget;
|
||||
import org.toop.app.widget.complex.PrimaryWidget;
|
||||
import org.toop.app.widget.complex.ViewWidget;
|
||||
import org.toop.app.widget.complex.ToggleWidget;
|
||||
import org.toop.framework.audio.VolumeControl;
|
||||
import org.toop.framework.audio.events.AudioEvents;
|
||||
@@ -18,8 +18,8 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
public class OptionsPrimary extends PrimaryWidget {
|
||||
public OptionsPrimary() {
|
||||
public class OptionsView extends ViewWidget {
|
||||
public OptionsView() {
|
||||
add(Pos.CENTER, Primitive.hbox(
|
||||
generalSection(),
|
||||
volumeSection(),
|
||||
@@ -42,7 +42,7 @@ public class OptionsPrimary extends PrimaryWidget {
|
||||
newLocale -> {
|
||||
AppSettings.getSettings().setLocale(newLocale.toString());
|
||||
AppContext.setLocale(newLocale);
|
||||
reload(new OptionsPrimary());
|
||||
reload(new OptionsView());
|
||||
},
|
||||
AppContext.getLocalization().getAvailableLocales().toArray(new Locale[0])
|
||||
);
|
||||
60
app/src/main/java/org/toop/app/widget/view/ServerView.java
Normal file
60
app/src/main/java/org/toop/app/widget/view/ServerView.java
Normal file
@@ -0,0 +1,60 @@
|
||||
package org.toop.app.widget.view;
|
||||
|
||||
import org.toop.app.widget.Primitive;
|
||||
import org.toop.app.widget.complex.ViewWidget;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ListView;
|
||||
|
||||
public final class ServerView extends ViewWidget {
|
||||
private final String user;
|
||||
private final Consumer<String> onPlayerClicked;
|
||||
private final Runnable onDisconnect;
|
||||
|
||||
private final ListView<Button> listView;
|
||||
|
||||
public ServerView(String user, Consumer<String> onPlayerClicked, Runnable onDisconnect) {
|
||||
this.user = user;
|
||||
this.onPlayerClicked = onPlayerClicked;
|
||||
this.onDisconnect = onDisconnect;
|
||||
|
||||
this.listView = new ListView<>();
|
||||
|
||||
setupLayout();
|
||||
}
|
||||
|
||||
private void setupLayout() {
|
||||
var playerHeader = Primitive.header(user);
|
||||
|
||||
var playerListSection = Primitive.vbox(
|
||||
playerHeader,
|
||||
Primitive.separator(),
|
||||
listView
|
||||
);
|
||||
|
||||
add(Pos.CENTER, playerListSection);
|
||||
|
||||
var disconnectButton = Primitive.button("disconnect", () -> {
|
||||
onDisconnect.run();
|
||||
transitionPrevious();
|
||||
});
|
||||
|
||||
add(Pos.BOTTOM_LEFT, Primitive.vbox(disconnectButton));
|
||||
}
|
||||
|
||||
public void update(List<String> players) {
|
||||
Platform.runLater(() -> {
|
||||
listView.getItems().clear();
|
||||
|
||||
for (String player : players) {
|
||||
var playerButton = Primitive.button(player, () -> onPlayerClicked.accept(player));
|
||||
listView.getItems().add(playerButton);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user