Merge branch 'main' into feat/lobby-expiry-broadcast

This commit is contained in:
Jona Walpert
2026-04-12 14:54:09 +02:00
4 changed files with 103 additions and 35 deletions
@@ -39,6 +39,7 @@ public class GameService {
* @return The current GameState object representing the state of the game. * @return The current GameState object representing the state of the game.
*/ */
public int getPot() { public int getPot() {
ensureState();
return state.pot; return state.pot;
} }
@@ -48,6 +49,7 @@ public class GameService {
* @return The current GameState object representing the state of the game. * @return The current GameState object representing the state of the game.
*/ */
public List<Card> getCommunityCards() { public List<Card> getCommunityCards() {
ensureState();
return state.communityCards; return state.communityCards;
} }
@@ -57,6 +59,7 @@ public class GameService {
* @return A list of Player objects representing the players in the current game state. * @return A list of Player objects representing the players in the current game state.
*/ */
public List<Player> getPlayers() { public List<Player> getPlayers() {
ensureState();
return state.players; return state.players;
} }
@@ -98,10 +101,19 @@ public class GameService {
* yet. * yet.
*/ */
public Player getWinner() { public Player getWinner() {
ensureState();
if (state.winnerIndex < 0) { if (state.winnerIndex < 0) {
return null; return null;
} }
return state.players.get(state.winnerIndex); return state.players.get(state.winnerIndex);
} }
/** Ensure that the game state has been initialized before accessing it. */
private void ensureState() {
if (state == null) {
throw new IllegalStateException("Call refresh() first");
}
}
} }
@@ -34,30 +34,37 @@ public class GameClient {
* @return A GameState object representing the current state of the game, as parsed * @return A GameState object representing the current state of the game, as parsed
*/ */
public GameState fetchGameState() { public GameState fetchGameState() {
List<String> responseLines = client.processCommand("GET_GAME_STATE\nGAME_ID=" + gameId); List<String> responseLines = client.processCommand("GET_GAME_STATE GAME_ID=" + gameId);
if (responseLines == null || responseLines.isEmpty()) { if (responseLines == null || responseLines.isEmpty()) {
throw new RuntimeException("Empty server response"); throw new RuntimeException("Empty server response");
} }
String fullResponse = String.join("\n", responseLines); if (responseLines.get(0).startsWith("-ERR")) {
throw new RuntimeException("Server Error: " + String.join(" ", responseLines));
}
if (!responseLines.get(0).startsWith("+OK")) {
throw new RuntimeException("Invalid response: missing +OK");
}
String fullResponse = String.join("\n", responseLines);
return parse(fullResponse); return parse(fullResponse);
} }
/** Send a CALL command to the server to indicate that the player wants to call */ /** Send a CALL command to the server to indicate that the player wants to call */
public void sendCall() { public void sendCall() {
client.processCommand("CALL\nGAME_ID=" + gameId); client.processCommand("CALL GAME_ID=" + gameId);
} }
/** Send a FOLD command to the server to indicate that the player wants to fold */ /** Send a FOLD command to the server to indicate that the player wants to fold */
public void sendFold() { public void sendFold() {
client.processCommand("FOLD\nGAME_ID=" + gameId); client.processCommand("FOLD GAME_ID=" + gameId);
} }
/** Send a BET command to the server to indicate that the player wants to bet */ /** Send a BET command to the server to indicate that the player wants to bet */
public void sendBet(int amount) { public void sendBet(int amount) {
client.processCommand("BET\nGAME_ID=" + gameId + "\nAMOUNT=" + amount); client.processCommand("BET GAME_ID=" + gameId + " AMOUNT=" + amount);
} }
/** /**
@@ -66,7 +73,7 @@ public class GameClient {
* @param amount The amount to raise to * @param amount The amount to raise to
*/ */
public void sendRaise(int amount) { public void sendRaise(int amount) {
client.processCommand("RAISE\nGAME_ID=" + gameId + "\nAMOUNT=" + amount); client.processCommand("RAISE GAME_ID=" + gameId + " AMOUNT=" + amount);
} }
/** /**
@@ -76,8 +83,12 @@ public class GameClient {
* @return A GameState object representing the current state of the game. * @return A GameState object representing the current state of the game.
*/ */
private GameState parse(String input) { private GameState parse(String input) {
GameState state = new GameState();
if (input.startsWith("-ERR")) {
throw new RuntimeException("Server returned Error:\n" + input);
}
GameState state = new GameState();
ParserContext ctx = new ParserContext(state); ParserContext ctx = new ParserContext(state);
for (String raw : input.split("\n")) { for (String raw : input.split("\n")) {
@@ -156,6 +167,7 @@ public class GameClient {
state.winnerIndex = intVal(line); state.winnerIndex = intVal(line);
} else { } else {
return false; return false;
// throw new RuntimeException("Unknown global field: " + line);
} }
return true; return true;
} }
@@ -171,7 +183,7 @@ public class GameClient {
private boolean handleSections(String line, ParserContext ctx) { private boolean handleSections(String line, ParserContext ctx) {
if (line.equals("END")) { if (line.equals("END")) {
ctx.inPlayers = false; // ctx.inPlayers = false;
ctx.inPlayerCards = false; ctx.inPlayerCards = false;
ctx.inCommunityCards = false; ctx.inCommunityCards = false;
ctx.currentPlayer = null; ctx.currentPlayer = null;
@@ -48,6 +48,7 @@ public class CasinoGameController {
private PlayerId myPlayerId; private PlayerId myPlayerId;
private TaskbarController taskbarController; private TaskbarController taskbarController;
private Image dealerImage; private Image dealerImage;
private javafx.animation.Timeline timeline;
private static final int TOTAL_SLOTS = 5; private static final int TOTAL_SLOTS = 5;
private static final int PLAYER_SLOTS = 2; private static final int PLAYER_SLOTS = 2;
@@ -166,6 +167,8 @@ public class CasinoGameController {
// uiTest(); // uiTest();
// or
// empty display only (optional) // empty display only (optional)
renderCommunityCards(List.of()); renderCommunityCards(List.of());
renderPlayerCards(List.of()); renderPlayerCards(List.of());
@@ -277,29 +280,58 @@ public class CasinoGameController {
*/ */
private void startLoop() { private void startLoop() {
javafx.animation.Timeline t = timeline =
new javafx.animation.Timeline( new javafx.animation.Timeline(
new javafx.animation.KeyFrame( new javafx.animation.KeyFrame(
javafx.util.Duration.seconds(UI_UPDATE_INTERVAL_SECONDS), javafx.util.Duration.seconds(UI_UPDATE_INTERVAL_SECONDS),
e -> updateUI())); e -> updateUI()));
t.setCycleCount(javafx.animation.Animation.INDEFINITE); timeline.setCycleCount(javafx.animation.Animation.INDEFINITE);
t.play(); timeline.play();
} }
/** /**
* Update the UI by fetching the latest game state from the GameService and updating all * Stop the UI update loop, which can be called when the game ends or when the user exits the
* relevant components. * game screen to prevent unnecessary updates and resource usage.
*/
public void stop() {
if (timeline != null) {
timeline.stop();
}
}
/**
* Triggers an asynchronous UI update by fetching the current game state from the GameService.
*/ */
private void updateUI() { private void updateUI() {
java.util.concurrent.CompletableFuture.supplyAsync(
() -> {
try { try {
GameState s = gameService.refresh(); return gameService.refresh();
} catch (Exception e) {
LOGGER.severe("GameService Error: " + e.getMessage());
return null;
}
})
.thenAccept(
s -> {
if (s == null) { if (s == null) {
return; return;
} }
javafx.application.Platform.runLater(() -> applyState(s));
});
}
/**
* Updates all visual UI components with the data from the provided GameState. This includes
* community cards, pot, player information, and the taskbar.
*
* @param s The current state of the game to be rendered.
*/
private void applyState(GameState s) {
renderCommunityCards(s.communityCards); renderCommunityCards(s.communityCards);
renderPot(s.pot); renderPot(s.pot);
@@ -309,10 +341,8 @@ public class CasinoGameController {
updateGameInfo(s); updateGameInfo(s);
highlightDealer(s); highlightDealer(s);
updateTaskbar(s); if (taskbarController != null) {
taskbarController.update(s, myPlayerId);
} catch (Exception e) {
LOGGER.severe("Error: UI Update failed: " + e.getMessage());
} }
} }
@@ -323,14 +353,20 @@ public class CasinoGameController {
*/ */
private void updatePlayerCards(GameState s) { private void updatePlayerCards(GameState s) {
int active = s.activePlayer; if (s.players == null || myPlayerId == null) {
if (active >= 0 && active < s.players.size()) {
Player p = s.players.get(active);
renderPlayerCards(p.getCards());
} else {
renderPlayerCards(List.of()); renderPlayerCards(List.of());
return;
} }
for (Player p : s.players) {
if (p.getId().equals(myPlayerId)) {
renderPlayerCards(p.getCards());
return;
}
}
// fallback
renderPlayerCards(List.of());
} }
/** /**
@@ -357,9 +393,10 @@ public class CasinoGameController {
* @param s The current game state. * @param s The current game state.
*/ */
private void updateTaskbar(GameState s) { private void updateTaskbar(GameState s) {
if (taskbarController != null) {
taskbarController.update(s, myPlayerId); taskbarController.update(s, myPlayerId);
} }
}
/** /**
* Update the player status components with the latest player information from the game state. * Update the player status components with the latest player information from the game state.
@@ -27,6 +27,13 @@ public class GetLobbyStatusResponse extends SuccessResponse {
"LOBBY", "LOBBY",
lb -> { lb -> {
lb.param("ID", lobby.getId().value()); lb.param("ID", lobby.getId().value());
// STATUS indicates whether a game has been started in this
// lobby
lb.param(
"STATUS",
lobby.getGameController() == null
? "CREATED"
: "RUNNING");
lb.param("NAME", lobby.getName()); lb.param("NAME", lobby.getName());
lb.block( lb.block(
"PLAYERS", "PLAYERS",