From 1b6e8ce6d6ff25ab5d2090c89a5fb28c9fc3eb40 Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sat, 11 Apr 2026 22:02:09 +0200 Subject: [PATCH 1/2] Docs: add documentation for get_gamestate command --- .../networking/commands/protocol-document.md | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/documents/docs/networking/commands/protocol-document.md b/documents/docs/networking/commands/protocol-document.md index 86da90d..8ac0631 100644 --- a/documents/docs/networking/commands/protocol-document.md +++ b/documents/docs/networking/commands/protocol-document.md @@ -56,6 +56,12 @@ This document describes the protocol for client-server communication in our appl - [Success Response](#success-response) - [Example Request](#example-request) - [Example Response](#example-response) + - [GET_GAME_STATE command](#get_game_state-command) + - [Required pre-execution checks](#required-pre-execution-checks) + - [Request Parameters](#request-parameters) + - [Success Response](#success-response) + - [Example Request](#example-request) + - [Example Response](#example-response) - [SEND_MESSAGE command](#send_message-command) - [Required pre-execution checks](#required-pre-execution-checks) - [Request Parameters](#request-parameters) @@ -595,6 +601,68 @@ GET_LOBBY_LIST END ``` +## GET_GAME_STATE command + +The `GET_GAME_STATE` command returns a snapshot of the current game for a lobby. It includes global fields (`PHASE`, `POT`, `CURRENT_BET`, `DEALER`, `ACTIVE_PLAYER`), repeated `CARD` blocks for community cards and repeated `PLAYER` blocks for per-player information. If the requester is a player in the game, their hole cards are included inside their `PLAYER` block as nested `CARD` blocks. + +### Required pre-execution checks +- [`UserLoggedInCheck`](#userloggedincheck) + +### Request Parameters +| Parameter Name | Type | Optional | Description | +| :------------- | :----- | :------: | :---------- | +| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to query. If omitted the server will resolve the lobby by the requesting session's associated user. +| `USERNAME` | `String` | yes | Optional username to query the game state for (server will also accept session resolution). + +### Implementation notes + +- Parser: `GetGameStateParser` — accepts optional `GAME_ID` and `USERNAME` and builds `GetGameStateRequest`. +- Handler: `GetGameStateHandler` — resolves the target lobby by `GAME_ID` when provided, otherwise by `USERNAME` or session user; if a game is running a `GetGameStateResponse` is returned. +- Response: `GetGameStateResponse` — uses repeated `CARD` and `PLAYER` blocks to match the client parser expectations. + +### Success Response + +Top-level fields and collections (example): + +``` ++OK + PHASE=PRE_FLOP + POT=150 + CURRENT_BET=50 + DEALER=2 + ACTIVE_PLAYER=1 + CARD + VALUE=K + SUIT=H + END + PLAYER + NAME=player1 + CHIPS=1000 + BET=50 + STATE=ACTIVE + CARD + VALUE=A + SUIT=S + END + END +END +``` + +### Example Request + +``` +GET_GAME_STATE GAME_ID=1 +``` + +### Example Response (error) + +``` +-ERR + CODE=GAME_NOT_STARTED + MESSAGE=Game not started +END +``` + ## GET_LOBBY_STATUS command The `GET_LOBBY_STATUS` command requests the server to return the current state of a lobby, including the list of players and their ready state. -- 2.52.0 From 54a7be074fb3b40fc67ec48e19816725b7e18c19 Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sat, 11 Apr 2026 22:04:47 +0200 Subject: [PATCH 2/2] Feat: Add get_game_state_command on server side --- .../dbis/cs108/casono/server/ServerApp.java | 18 ++- .../get_game_state/GetGameStateHandler.java | 74 ++++++++++++ .../get_game_state/GetGameStateParser.java | 16 +++ .../get_game_state/GetGameStateRequest.java | 31 +++++ .../get_game_state/GetGameStateResponse.java | 114 ++++++++++++++++++ 5 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateHandler.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateParser.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateRequest.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateResponse.java diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java index c00aa84..f9007eb 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java @@ -154,7 +154,22 @@ public class ServerApp { new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list .GetLobbyListHandler(responseDispatcher, lobbyManager)); - // GET_LOBBY_STATUS registration + // GET_GAME_STATE registration + parserDispatcher.register( + "GET_GAME_STATE", + new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state + .GetGameStateParser()); + commandRouter.register( + ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state + .GetGameStateRequest.class, + (ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler< + ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game + .get_game_state.GetGameStateRequest>) + new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state + .GetGameStateHandler( + responseDispatcher, lobbyManager, userRegistry)); + + // GET_LOBBY_STATUS registration parserDispatcher.register( "GET_LOBBY_STATUS", new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status @@ -169,7 +184,6 @@ public class ServerApp { .get_lobby_status.GetLobbyStatusHandler( responseDispatcher, lobbyManager, userRegistry)); - // JOIN_LOBBY registration parserDispatcher.register( "JOIN_LOBBY", diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateHandler.java new file mode 100644 index 0000000..17e6a96 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateHandler.java @@ -0,0 +1,74 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state; + +import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry; +import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher; + +/** Handler for GET_GAME_STATE: returns pot, phase, community cards and per-player info. */ +public class GetGameStateHandler extends CommandHandler { + private final LobbyManager lobbyManager; + private final UserRegistry userRegistry; + + public GetGameStateHandler( + ResponseDispatcher responseDispatcher, + LobbyManager lobbyManager, + UserRegistry userRegistry) { + super(responseDispatcher); + this.lobbyManager = lobbyManager; + this.userRegistry = userRegistry; + addCheck(new UserLoggedInCheck(userRegistry)); + } + + @Override + public void execute(GetGameStateRequest request) { + Integer gameId = request.getGameId(); + String username = request.getUsername(); + + Lobby lobby = null; + if (gameId != null) { + lobby = lobbyManager.getLobby(LobbyId.of(gameId)); + if (lobby == null) { + responseDispatcher.dispatch( + new ErrorResponse( + request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found")); + return; + } + } else { + if (username == null) { + var maybeUser = userRegistry.getBySessionId(request.getSessionId()); + if (maybeUser.isEmpty()) { + responseDispatcher.dispatch( + new ErrorResponse( + request.getContext(), "NOT_LOGGED_IN", "User not logged in")); + return; + } + User u = maybeUser.get(); + username = u.getName(); + } + + lobby = lobbyManager.getLobbyByUsername(username); + if (lobby == null) { + responseDispatcher.dispatch( + new ErrorResponse( + request.getContext(), "NOT_IN_LOBBY", "User not in a lobby")); + return; + } + } + + var game = lobby.getGameController(); + if (game == null) { + responseDispatcher.dispatch( + new ErrorResponse( + request.getContext(), "GAME_NOT_STARTED", "Game not started")); + return; + } + + responseDispatcher.dispatch(new GetGameStateResponse(request.getContext(), game, username)); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateParser.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateParser.java new file mode 100644 index 0000000..96e7a6f --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateParser.java @@ -0,0 +1,16 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor; + +public class GetGameStateParser implements CommandParser { + @Override + public GetGameStateRequest parse(PrimitiveRequest primitiveRequest) { + RequestParameterAccessor accessor = + new RequestParameterAccessor(primitiveRequest.parameters()); + Integer gameId = accessor.optional("GAME_ID", null, Integer::parseInt); + String username = accessor.optional("USERNAME", null); + return new GetGameStateRequest(primitiveRequest.context(), username, gameId); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateRequest.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateRequest.java new file mode 100644 index 0000000..5072617 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateRequest.java @@ -0,0 +1,31 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state; + +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext; + +/** + * Request object for `GET_GAME_STATE`. + * + *

Either `USERNAME` or `GAME_ID` may be provided by the client. If both are omitted the handler + * will resolve the username from the session. + */ +public class GetGameStateRequest extends Request { + private final String username; + private final Integer gameId; + + public GetGameStateRequest(RequestContext context, String username, Integer gameId) { + super(context); + this.username = username; + this.gameId = gameId; + } + + /** Returns the optional username provided in the request. */ + public String getUsername() { + return username; + } + + /** Returns the optional game id provided in the request. */ + public Integer getGameId() { + return gameId; + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateResponse.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateResponse.java new file mode 100644 index 0000000..455abe8 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateResponse.java @@ -0,0 +1,114 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state; + +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Card; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Rank; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Suit; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.Player; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder; +import java.util.List; + +/** Response carrying a snapshot of the current game state using the project's wire format. */ +public class GetGameStateResponse extends SuccessResponse { + public GetGameStateResponse( + RequestContext context, GameController game, String requestingUsername) { + super(context, buildBody(game.getState(), requestingUsername)); + } + + private static ResponseBody buildBody(GameState state, String requestingUsername) { + int globalCurrentBet = computeGlobalCurrentBet(state); + + ResponseBodyBuilder builder = ResponseBody.builder(); + builder.param("PHASE", state.getPhase() == null ? "UNKNOWN" : state.getPhase().name()); + builder.param("POT", state.getPot().getAmount()); + builder.param("CURRENT_BET", globalCurrentBet); + builder.param("DEALER", state.getDealerIndex()); + builder.param("ACTIVE_PLAYER", state.getCurrentPlayerIndex()); + + appendCommunityCards(builder, state); + appendPlayers(builder, state, requestingUsername); + + return builder.build(); + } + + private static int computeGlobalCurrentBet(GameState state) { + int globalCurrentBet = 0; + for (Player p : state.getPlayers()) { + int b = state.getCurrentBet(p.getId()); + if (b > globalCurrentBet) { + globalCurrentBet = b; + } + } + return globalCurrentBet; + } + + private static void appendCommunityCards(ResponseBodyBuilder builder, GameState state) { + for (Card c : state.getCommunityCards()) { + builder.block( + "CARD", + card -> { + card.param("VALUE", rankToWire(c.getRank())); + card.param("SUIT", suitToWire(c.getSuit())); + }); + } + } + + private static void appendPlayers( + ResponseBodyBuilder builder, GameState state, String requestingUsername) { + for (Player p : state.getPlayers()) { + PlayerId pid = p.getId(); + builder.block( + "PLAYER", + pblock -> { + pblock.param("NAME", pid.value()); + pblock.param("CHIPS", p.getChips()); + pblock.param("BET", state.getCurrentBet(pid)); + pblock.param("STATE", p.isFolded() ? "FOLDED" : "ACTIVE"); + + if (requestingUsername != null && requestingUsername.equals(pid.value())) { + List hole = state.getHoleCards(pid); + for (Card hc : hole) { + pblock.block( + "CARD", + cblock -> { + cblock.param("VALUE", rankToWire(hc.getRank())); + cblock.param("SUIT", suitToWire(hc.getSuit())); + }); + } + } + }); + } + } + + private static String rankToWire(Rank r) { + return switch (r) { + case TWO -> "2"; + case THREE -> "3"; + case FOUR -> "4"; + case FIVE -> "5"; + case SIX -> "6"; + case SEVEN -> "7"; + case EIGHT -> "8"; + case NINE -> "9"; + case TEN -> "10"; + case JACK -> "J"; + case QUEEN -> "Q"; + case KING -> "K"; + case ACE -> "A"; + }; + } + + private static String suitToWire(Suit s) { + return switch (s) { + case HEARTS -> "H"; + case DIAMONDS -> "D"; + case CLUBS -> "C"; + case SPADES -> "S"; + }; + } +} -- 2.52.0