Feat/99 add get gamestate command on server side #263
@@ -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.
|
||||
|
||||
@@ -154,6 +154,21 @@ public class ServerApp {
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list
|
||||
.GetLobbyListHandler(responseDispatcher, lobbyManager));
|
||||
|
||||
// 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",
|
||||
@@ -169,7 +184,6 @@ public class ServerApp {
|
||||
.get_lobby_status.GetLobbyStatusHandler(
|
||||
responseDispatcher, lobbyManager, userRegistry));
|
||||
|
||||
|
||||
// JOIN_LOBBY registration
|
||||
parserDispatcher.register(
|
||||
"JOIN_LOBBY",
|
||||
|
||||
+74
@@ -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));
|
||||
}
|
||||
}
|
||||
+16
@@ -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);
|
||||
}
|
||||
}
|
||||
+31
@@ -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;
|
||||
}
|
||||
}
|
||||
+114
@@ -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";
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user