Feat/100 add start game command on server side #265

Merged
jona.walpert merged 3 commits from feat/100-add-start-game-command-on-server-side into main 2026-04-12 12:53:44 +02:00
6 changed files with 281 additions and 1 deletions
@@ -82,6 +82,7 @@ This document describes the protocol for client-server communication in our appl
- [Required pre-execution checks](#required-pre-execution-checks)
- [Request Parameters](#request-parameters)
- [Success Response](#success-response)
- [START_GAME command](#start_game-command)
- [GET_LOBBY_STATUS command](#get_lobby_status-command)
- [Required pre-execution checks](#required-pre-execution-checks)
- [Request Parameters](#request-parameters)
@@ -547,8 +548,64 @@ END
```
-ERR
CODE=LOBBY_NOT_FOUND
MESSAGE=Lobby not found
MSG=Lobby not found
END
## START_GAME command
The `START_GAME` command requests the server to start a new game in the specified lobby. The
server requires an explicit numeric `ID` parameter identifying the target lobby.
### Required pre-execution checks
- [`UserLoggedInCheck`](#userloggedincheck)
### Request Parameters
| Parameter Name | Type | Optional | Description |
| :------------- | :--- | :------: | :---------- |
| `ID` | `int` | no | Numeric id of the target lobby |
### Implementation notes
- Parser: `StartGameParser` — reads the required `ID` parameter and builds `StartGameRequest`.
- Handler: `StartGameHandler` — resolves the lobby by id, checks the requester is a member of the
lobby, verifies a game is not already running and that enough players are present, initializes
`GameController` and attaches it to the lobby. On success a structured `START_GAME` success
response containing `GAME` and `MESSAGE` fields is returned.
### Success Response
| Field | Type | Description |
| :------- | :------ | :-------------------------------- |
| `GAME` | `int` | Numeric id of the started game (uses lobby id) |
| `MESSAGE`| `String`| Human-readable status message |
### Error Response
| Code | Description |
| :--------------- | :---------------------------------------- |
| `MISSING_PARAMETER` | Required parameter `ID` missing (parser) |
| `LOBBY_NOT_FOUND` | The specified lobby id does not exist |
| `NOT_IN_LOBBY` | Requesting user is not a member of the lobby |
| `NOT_ENOUGH_PLAYERS` | Not enough players to start the game |
| `ALREADY_STARTED` | A game is already running in the lobby |
| `USER_NOT_LOGGED_IN` | No user associated with the session |
### Example Request
```
START_GAME ID=1
```
### Example Response (success)
```
+OK
GAME=1
MESSAGE=Game started successfully
END
```
```
## GET_LOBBY_LIST command
@@ -266,5 +266,19 @@ public class ServerApp {
.JoinLobbyRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
.JoinLobbyHandler(responseDispatcher, lobbyManager, userRegistry));
// START_GAME registration
parserDispatcher.register(
"START_GAME",
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
.StartGameParser());
commandRouter.register(
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
.StartGameRequest.class,
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
.StartGameRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
.StartGameHandler(responseDispatcher, lobbyManager, userRegistry));
}
}
@@ -0,0 +1,113 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game;
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.GameEngine;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.RoundManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.TurnManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.RuleEngine;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
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.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;
import java.util.ArrayList;
import java.util.Optional;
/**
* Handler for START_GAME: creates a GameEngine+GameController for the lobby and starts the game.
*/
public class StartGameHandler extends CommandHandler<StartGameRequest> {
private final LobbyManager lobbyManager;
private final UserRegistry userRegistry;
private static final int DEFAULT_START_CHIPS = 20000;
public StartGameHandler(
ResponseDispatcher responseDispatcher,
LobbyManager lobbyManager,
UserRegistry userRegistry) {
super(responseDispatcher);
this.lobbyManager = lobbyManager;
this.userRegistry = userRegistry;
addCheck(new UserLoggedInCheck(userRegistry));
}
@Override
public void execute(StartGameRequest request) {
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
userRegistry.getBySessionId(request.getSessionId());
if (opt.isEmpty()) {
// Guard: UserLoggedInCheck should normally handle this
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "USER_NOT_LOGGED_IN", "User not logged in"));
return;
}
String username = opt.get().getName();
// Resolve lobby by provided ID (required)
LobbyId lid = LobbyId.of(request.getId());
Lobby lobby = lobbyManager.getLobby(lid);
if (lobby == null) {
responseDispatcher.dispatch(
new ErrorResponse(request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
return;
}
// Ensure requester is part of the lobby
if (!lobby.getPlayerNames().contains(username)) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "NOT_IN_LOBBY", "User not in specified lobby"));
return;
}
// Prevent double-start
if (lobby.getGameController() != null) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(),
"ALREADY_STARTED",
"Game has already been started for this lobby"));
return;
}
// Basic player count check
if (lobby.getPlayerNames().size() < 2) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(),
"NOT_ENOUGH_PLAYERS",
"Not enough players to start the game"));
return;
}
// Create engine + controller
GameState state = new GameState();
GameEngine engine =
new GameEngine(
state,
new RuleEngine(new ArrayList<>()),
new RoundManager(),
new TurnManager());
GameController game = new GameController(engine);
// Add all players from lobby
for (String playerName : lobby.getPlayerNames()) {
game.addPlayer(PlayerId.of(playerName), DEFAULT_START_CHIPS);
}
// Start the game and attach to lobby
game.startGame();
lobby.initGame(game);
responseDispatcher.dispatch(
new StartGameResponse(
request.getContext(), lid.value(), "Game started successfully"));
}
}
@@ -0,0 +1,27 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game;
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 `START_GAME` command.
*
* <p>Expected request parameters:
*
* <ul>
* <li>`ID` (int) — numeric lobby identifier (required)
* </ul>
*
* <p>The parser builds a {@link StartGameRequest} containing the parsed lobby id and the original
* request context.
*/
public class StartGameParser implements CommandParser<StartGameRequest> {
@Override
public StartGameRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor =
new RequestParameterAccessor(primitiveRequest.parameters());
int id = accessor.require("ID", Integer::parseInt);
return new StartGameRequest(primitiveRequest.context(), id);
}
}
@@ -0,0 +1,35 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game;
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 to start a game in a specific lobby.
*
* <p>The `START_GAME` request requires the numeric lobby id parameter `ID` identifying the target
* lobby. The request carries the usual {@link RequestContext} (session id, request id) via the base
* class.
*/
public class StartGameRequest extends Request {
private final int id;
/**
* Create a new {@link StartGameRequest}.
*
* @param context the request context
* @param id numeric lobby id to start the game in
*/
public StartGameRequest(RequestContext context, int id) {
super(context);
this.id = id;
}
/**
* Returns the numeric lobby id provided by the client.
*
* @return lobby id
*/
public int getId() {
return id;
}
}
@@ -0,0 +1,34 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game;
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;
/**
* Success response for {@code START_GAME} containing the started game's identifier and an
* informational message.
*/
public class StartGameResponse extends SuccessResponse {
/**
* Create a standard start-game response with a default success message.
*
* @param context the request context
* @param gameId numeric id of the started game (uses lobby id)
*/
public StartGameResponse(RequestContext context, int gameId) {
this(context, gameId, "Game started successfully");
}
/**
* Create a start-game response with a custom message.
*
* @param context the request context
* @param gameId numeric id of the started game (uses lobby id)
* @param message informational message for the client
*/
public StartGameResponse(RequestContext context, int gameId, String message) {
super(
context,
ResponseBody.builder().param("GAME", gameId).param("MESSAGE", message).build());
}
}