Feat/102 add bet command on server side #266
@@ -62,6 +62,7 @@ This document describes the protocol for client-server communication in our appl
|
||||
- [Success Response](#success-response)
|
||||
- [Example Request](#example-request)
|
||||
- [Example Response](#example-response)
|
||||
- [BET command](#bet-command)
|
||||
- [SEND_MESSAGE command](#send_message-command)
|
||||
- [Required pre-execution checks](#required-pre-execution-checks)
|
||||
- [Request Parameters](#request-parameters)
|
||||
@@ -663,6 +664,63 @@ GET_GAME_STATE GAME_ID=1
|
||||
END
|
||||
```
|
||||
|
||||
## BET command
|
||||
|
||||
The `BET` command lets the currently logged-in player place a bet in the ongoing game for their lobby.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
- [`UserLoggedInCheck`](#userloggedincheck)
|
||||
|
||||
### Request Parameters
|
||||
|
||||
| Parameter Name | Type | Optional | Description |
|
||||
| :------------- | :--- | :------: | :---------- |
|
||||
| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to target. If omitted the server resolves the lobby by the requesting session's user. |
|
||||
| `AMOUNT` | `int` | no | Amount the player wants to bet |
|
||||
|
||||
### Implementation notes
|
||||
|
||||
- Parser: `PlayerBetParser` — reads `AMOUNT` and optionally `GAME_ID`.
|
||||
- Handler: `PlayerBetHandler` — validates the session, lobby (by id when provided or else by session), player's turn and balance and forwards to `GameController`.
|
||||
|
||||
### Success Response
|
||||
|
||||
No additional response fields. Server replies with `+OK` on success.
|
||||
|
||||
### Error Response
|
||||
|
||||
| Code | Description |
|
||||
| :--- | :---------- |
|
||||
| `NOT_YOUR_TURN` | The player attempted to bet when not their turn |
|
||||
| `INSUFFICIENT_FUNDS` | Player does not have enough chips |
|
||||
| `INVALID_AMOUNT` | Amount parameter is invalid |
|
||||
| `GAME_NOT_STARTED` | No game is running in the lobby |
|
||||
| `NOT_IN_LOBBY` | Requesting user is not a member of the lobby |
|
||||
| `LOBBY_NOT_FOUND` | The specified `GAME_ID` does not exist |
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
BET GAME_ID=1 AMOUNT=50
|
||||
```
|
||||
|
||||
### Example Response (success)
|
||||
|
||||
```
|
||||
+OK
|
||||
END
|
||||
```
|
||||
|
||||
### Example Response (error)
|
||||
|
||||
```
|
||||
-ERR
|
||||
CODE=INSUFFICIENT_FUNDS
|
||||
MSG=Not enough chips
|
||||
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.
|
||||
|
||||
@@ -169,6 +169,18 @@ public class ServerApp {
|
||||
.GetGameStateHandler(
|
||||
responseDispatcher, lobbyManager, userRegistry));
|
||||
|
||||
// BET registration
|
||||
parserDispatcher.register(
|
||||
"BET",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet
|
||||
.PlayerBetRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet
|
||||
.PlayerBetHandler(responseDispatcher, userRegistry, lobbyManager));
|
||||
|
||||
// GET_LOBBY_STATUS registration
|
||||
parserDispatcher.register(
|
||||
"GET_LOBBY_STATUS",
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
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.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.OkResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Handler for the `BET` command.
|
||||
*
|
||||
* <p>This handler validates the request (amount >= 0), resolves the user and target lobby (by
|
||||
* `GAME_ID` when provided or by session username otherwise), checks that a game is running and then
|
||||
* forwards the action to the {@code GameController}.
|
||||
*
|
||||
* <p>On failure the handler dispatches an {@link ErrorResponse} with an appropriate error code
|
||||
* (e.g. {@code INVALID_AMOUNT}, {@code NOT_IN_LOBBY}, {@code LOBBY_NOT_FOUND}, {@code
|
||||
* GAME_NOT_STARTED}, {@code GAME_ACTION_FAILED}). On success it dispatches an {@link OkResponse}.
|
||||
*/
|
||||
public class PlayerBetHandler extends CommandHandler<PlayerBetRequest> {
|
||||
private final UserRegistry userRegistry;
|
||||
private final LobbyManager lobbyManager;
|
||||
|
||||
/**
|
||||
* Creates a new {@code PlayerBetHandler}.
|
||||
*
|
||||
* @param responseDispatcher dispatcher used to send responses back to the client
|
||||
* @param userRegistry registry to resolve users from session ids
|
||||
* @param lobbyManager manager used to lookup lobbies and their game controllers
|
||||
*/
|
||||
public PlayerBetHandler(
|
||||
ResponseDispatcher responseDispatcher,
|
||||
UserRegistry userRegistry,
|
||||
LobbyManager lobbyManager) {
|
||||
super(responseDispatcher);
|
||||
this.userRegistry = userRegistry;
|
||||
this.lobbyManager = lobbyManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the bet request: validate parameters, resolve lobby and game, and forward the bet
|
||||
* action to the game controller. Sends an {@link ErrorResponse} on failure or an {@link
|
||||
* OkResponse} on success.
|
||||
*
|
||||
* @param request the parsed {@link PlayerBetRequest}
|
||||
*/
|
||||
@Override
|
||||
public void execute(PlayerBetRequest request) {
|
||||
if (request.getAmount() < 0) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "INVALID_AMOUNT", "Amount must be non-negative"));
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
|
||||
userRegistry.getBySessionId(request.getSessionId());
|
||||
if (opt.isEmpty()) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
|
||||
return;
|
||||
}
|
||||
|
||||
String username = opt.get().getName();
|
||||
|
||||
Integer gameId = request.getGameId();
|
||||
var lobby =
|
||||
(gameId != null)
|
||||
? lobbyManager.getLobby(LobbyId.of(gameId))
|
||||
: lobbyManager.getLobbyByUsername(username);
|
||||
|
||||
if (lobby == null) {
|
||||
if (gameId != null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||
} else {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
GameController game = lobby.getGameController();
|
||||
if (game == null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "GAME_NOT_STARTED", "Game not started"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
game.playerBet(PlayerId.of(username), request.getAmount());
|
||||
} catch (Exception e) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(request.getContext(), "GAME_ACTION_FAILED", e.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Parser for the `BET` command.
|
||||
*
|
||||
* <p>Parses the request parameters and builds a {@link PlayerBetRequest}. Expected parameters:
|
||||
*
|
||||
* <ul>
|
||||
* <li>`GAME_ID` (optional) - numeric id of the lobby/game to target
|
||||
* <li>`AMOUNT` (required) - amount to bet
|
||||
* </ul>
|
||||
*/
|
||||
public class PlayerBetParser implements CommandParser<PlayerBetRequest> {
|
||||
/**
|
||||
* Parse a primitive request into a {@link PlayerBetRequest}.
|
||||
*
|
||||
* @param primitiveRequest the raw parsed request containing parameters and context
|
||||
* @return a {@link PlayerBetRequest} with the parsed values (gameId may be null)
|
||||
*/
|
||||
@Override
|
||||
public PlayerBetRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
RequestParameterAccessor accessor =
|
||||
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||
|
||||
Integer gameId = null;
|
||||
try {
|
||||
gameId = accessor.optional("GAME_ID", null, Integer::parseInt);
|
||||
} catch (Exception e) {
|
||||
// parse handled by framework
|
||||
}
|
||||
|
||||
int amount = accessor.require("AMOUNT", Integer::parseInt);
|
||||
|
||||
return new PlayerBetRequest(primitiveRequest.context(), gameId, amount);
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet;
|
||||
|
||||
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 for the `BET` command.
|
||||
*
|
||||
* <p>Contains the optional target `gameId` and the `amount` the player wants to bet.
|
||||
*/
|
||||
public class PlayerBetRequest extends Request {
|
||||
private final Integer gameId;
|
||||
private final int amount;
|
||||
|
||||
/**
|
||||
* Creates a new {@code PlayerBetRequest}.
|
||||
*
|
||||
* @param context the request context
|
||||
* @param gameId optional game id of the targeted lobby (may be {@code null})
|
||||
* @param amount the bet amount (non-negative)
|
||||
*/
|
||||
public PlayerBetRequest(RequestContext context, Integer gameId, int amount) {
|
||||
super(context);
|
||||
this.gameId = gameId;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the optional target game id.
|
||||
*
|
||||
* @return the game id or {@code null} if not provided
|
||||
*/
|
||||
public Integer getGameId() {
|
||||
return gameId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested bet amount.
|
||||
*
|
||||
* @return the bet amount
|
||||
*/
|
||||
public int getAmount() {
|
||||
return amount;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.game;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.BetAction;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.BlindAction;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.CallAction;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.FoldAction;
|
||||
@@ -180,6 +181,16 @@ public class GameController {
|
||||
engine.processAction(new RaiseAction(playerId, amount));
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a player's bet action by sending a BetAction to the GameEngine.
|
||||
*
|
||||
* @param playerId The ID of the player who is betting.
|
||||
* @param amount The amount the player is betting.
|
||||
*/
|
||||
public void playerBet(PlayerId playerId, int amount) {
|
||||
engine.processAction(new BetAction(playerId, amount));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current game state from the GameEngine.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user