Feat/99 add get gamestate command on server side #263

Merged
jona.walpert merged 2 commits from feat/99-add-get-gamestate-command-on-server-side into main 2026-04-11 22:08:20 +02:00
6 changed files with 319 additions and 2 deletions
@@ -56,6 +56,12 @@ This document describes the protocol for client-server communication in our appl
- [Success Response](#success-response) - [Success Response](#success-response)
- [Example Request](#example-request) - [Example Request](#example-request)
- [Example Response](#example-response) - [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) - [SEND_MESSAGE command](#send_message-command)
- [Required pre-execution checks](#required-pre-execution-checks) - [Required pre-execution checks](#required-pre-execution-checks)
- [Request Parameters](#request-parameters) - [Request Parameters](#request-parameters)
@@ -595,6 +601,68 @@ GET_LOBBY_LIST
END 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 ## 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. 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.
@@ -154,7 +154,22 @@ public class ServerApp {
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list
.GetLobbyListHandler(responseDispatcher, lobbyManager)); .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( parserDispatcher.register(
"GET_LOBBY_STATUS", "GET_LOBBY_STATUS",
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.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( .get_lobby_status.GetLobbyStatusHandler(
responseDispatcher, lobbyManager, userRegistry)); responseDispatcher, lobbyManager, userRegistry));
// JOIN_LOBBY registration // JOIN_LOBBY registration
parserDispatcher.register( parserDispatcher.register(
"JOIN_LOBBY", "JOIN_LOBBY",
@@ -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<GetGameStateRequest> {
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));
}
}
@@ -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<GetGameStateRequest> {
@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);
}
}
@@ -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`.
*
* <p>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;
}
}
@@ -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<Card> 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";
};
}
}