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 e2e6f2a..eef8e31 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
@@ -196,6 +196,20 @@ public class ServerApp {
.PlayerRaiseHandler(
responseDispatcher, userRegistry, lobbyManager));
+ // CALL registration
+ parserDispatcher.register(
+ "CALL",
+ new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call
+ .PlayerCallParser());
+ commandRouter.register(
+ ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call.PlayerCallRequest
+ .class,
+ (ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
+ ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call
+ .PlayerCallRequest>)
+ new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call
+ .PlayerCallHandler(responseDispatcher, userRegistry, lobbyManager));
+
// GET_LOBBY_STATUS registration
parserDispatcher.register(
"GET_LOBBY_STATUS",
diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/call/PlayerCallHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/call/PlayerCallHandler.java
new file mode 100644
index 0000000..cdaffc9
--- /dev/null
+++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/call/PlayerCallHandler.java
@@ -0,0 +1,96 @@
+package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call;
+
+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 `CALL` command.
+ *
+ *
This handler validates the session, resolves the lobby (by `GAME_ID` when provided or by
+ * session username otherwise), checks for a running game and forwards the call action to the {@code
+ * GameController}.
+ */
+public class PlayerCallHandler extends CommandHandler {
+ private final UserRegistry userRegistry;
+ private final LobbyManager lobbyManager;
+
+ /**
+ * Creates a new {@code PlayerCallHandler}.
+ *
+ * @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 PlayerCallHandler(
+ ResponseDispatcher responseDispatcher,
+ UserRegistry userRegistry,
+ LobbyManager lobbyManager) {
+ super(responseDispatcher);
+ this.userRegistry = userRegistry;
+ this.lobbyManager = lobbyManager;
+ }
+
+ /**
+ * Execute the call request: resolve lobby by `GAME_ID` if present, otherwise by session user,
+ * verify game is running, and forward the call to the game controller.
+ *
+ * @param request the parsed {@link PlayerCallRequest}
+ */
+ @Override
+ public void execute(PlayerCallRequest request) {
+ Optional 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.playerCall(PlayerId.of(username));
+ } catch (RuntimeException e) {
+ responseDispatcher.dispatch(
+ new ErrorResponse(request.getContext(), "GAME_ACTION_FAILED", e.getMessage()));
+ return;
+ }
+
+ responseDispatcher.dispatch(new OkResponse(request.getContext()));
+ }
+}
diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/call/PlayerCallParser.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/call/PlayerCallParser.java
new file mode 100644
index 0000000..dd3ce0d
--- /dev/null
+++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/call/PlayerCallParser.java
@@ -0,0 +1,28 @@
+package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call;
+
+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 `CALL` command.
+ *
+ * Accepts an optional `GAME_ID` parameter. If provided, the handler will target the specified
+ * lobby; otherwise the server resolves the lobby by the requesting session's user.
+ */
+public class PlayerCallParser implements CommandParser {
+ @Override
+ public PlayerCallRequest 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 errors handled by framework
+ }
+
+ return new PlayerCallRequest(primitiveRequest.context(), gameId);
+ }
+}
diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/call/PlayerCallRequest.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/call/PlayerCallRequest.java
new file mode 100644
index 0000000..c144b0d
--- /dev/null
+++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/call/PlayerCallRequest.java
@@ -0,0 +1,34 @@
+package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call;
+
+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 `CALL` command.
+ *
+ * Contains an optional `gameId` when the client targets a specific lobby, otherwise the server
+ * will resolve the lobby from the requesting session's user.
+ */
+public class PlayerCallRequest extends Request {
+ private final Integer gameId;
+
+ /**
+ * Creates a new {@code PlayerCallRequest}.
+ *
+ * @param context the request context
+ * @param gameId optional game id of the targeted lobby (may be {@code null})
+ */
+ public PlayerCallRequest(RequestContext context, Integer gameId) {
+ super(context);
+ this.gameId = gameId;
+ }
+
+ /**
+ * Returns the optional target game id.
+ *
+ * @return the game id or {@code null} if not provided
+ */
+ public Integer getGameId() {
+ return gameId;
+ }
+}