Merge branch 'feat/97-add-joinlobby-command-on-server-side' into 'main'
Feat: Add addPlayer command on sever side Closes #97 See merge request cs108-fs26/Gruppe-13!102
This commit was merged in pull request #258.
This commit is contained in:
@@ -453,4 +453,64 @@ GET_NEXT_MESSAGE
|
|||||||
+OK
|
+OK
|
||||||
TYPE=GLOBAL GAME=-1 USER=player1 TARGET=null TIME=9:30 TEXT="Guten Tag"
|
TYPE=GLOBAL GAME=-1 USER=player1 TARGET=null TIME=9:30 TEXT="Guten Tag"
|
||||||
END
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (success)
|
||||||
|
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
## JOIN_LOBBY command
|
||||||
|
|
||||||
|
The `JOIN_LOBBY` command requests the server to add the currently logged-in user to the lobby with the given identifier. The server resolves the username from the session; clients must only provide the `ID` parameter.
|
||||||
|
|
||||||
|
### 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: `JoinLobbyParser` — reads the `ID` parameter and builds `JoinLobbyRequest`.
|
||||||
|
- Handler: `JoinLobbyHandler` — resolves the username from `UserRegistry` using the session id, attempts to add the user to the lobby via `LobbyManager.addPlayerToLobby(...)` and returns appropriate responses.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
|
||||||
|
No additional response fields. The server replies with a simple `+OK` on success.
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
| :--- | :---------- |
|
||||||
|
| `USER_NOT_LOGGED_IN` | No user associated with the session (pre-execution check failed) |
|
||||||
|
| `LOBBY_NOT_FOUND` | The specified lobby id does not exist |
|
||||||
|
| `LOBBY_FULL_OR_ALREADY_IN` | Lobby is full or the user is already in the lobby |
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```
|
||||||
|
JOIN_LOBBY ID=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (success)
|
||||||
|
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (error)
|
||||||
|
|
||||||
|
```
|
||||||
|
-ERR
|
||||||
|
CODE=LOBBY_NOT_FOUND
|
||||||
|
MESSAGE=Lobby not found
|
||||||
|
END
|
||||||
```
|
```
|
||||||
@@ -24,6 +24,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingRequest;
|
|||||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageHandler;
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageHandler;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageParser;
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageParser;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageRequest;
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageRequest;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob;
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager;
|
||||||
@@ -85,7 +86,8 @@ public class ServerApp {
|
|||||||
SESSION_DISCONNECT_JOB_PERIOD,
|
SESSION_DISCONNECT_JOB_PERIOD,
|
||||||
TimeUnit.SECONDS);
|
TimeUnit.SECONDS);
|
||||||
|
|
||||||
registerCommands(dispatcher, router, responseDispatcher, userRegistry);
|
LobbyManager lobbyManager = new LobbyManager();
|
||||||
|
registerCommands(dispatcher, router, responseDispatcher, userRegistry, lobbyManager);
|
||||||
|
|
||||||
NetworkManager networkManager = new NetworkManager(port, sessionManager, router);
|
NetworkManager networkManager = new NetworkManager(port, sessionManager, router);
|
||||||
networkManager.start();
|
networkManager.start();
|
||||||
@@ -102,7 +104,8 @@ public class ServerApp {
|
|||||||
CommandParserDispatcher parserDispatcher,
|
CommandParserDispatcher parserDispatcher,
|
||||||
CommandRouter commandRouter,
|
CommandRouter commandRouter,
|
||||||
ResponseDispatcher responseDispatcher,
|
ResponseDispatcher responseDispatcher,
|
||||||
UserRegistry userRegistry) {
|
UserRegistry userRegistry,
|
||||||
|
LobbyManager lobbyManager) {
|
||||||
parserDispatcher.register("PING", new PingParser());
|
parserDispatcher.register("PING", new PingParser());
|
||||||
commandRouter.register(PingRequest.class, new PingHandler(responseDispatcher));
|
commandRouter.register(PingRequest.class, new PingHandler(responseDispatcher));
|
||||||
|
|
||||||
@@ -136,5 +139,19 @@ public class ServerApp {
|
|||||||
parserDispatcher.register("LIST_USERS", new ListUsersParser());
|
parserDispatcher.register("LIST_USERS", new ListUsersParser());
|
||||||
commandRouter.register(
|
commandRouter.register(
|
||||||
ListUsersRequest.class, new ListUsersHandler(responseDispatcher, userRegistry));
|
ListUsersRequest.class, new ListUsersHandler(responseDispatcher, userRegistry));
|
||||||
|
|
||||||
|
// JOIN_LOBBY registration
|
||||||
|
parserDispatcher.register(
|
||||||
|
"JOIN_LOBBY",
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||||
|
.JoinLobbyParser());
|
||||||
|
commandRouter.register(
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||||
|
.JoinLobbyRequest.class,
|
||||||
|
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||||
|
.JoinLobbyRequest>)
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||||
|
.JoinLobbyHandler(responseDispatcher, lobbyManager, userRegistry));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.add_player;
|
||||||
|
|
||||||
|
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.OkResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/** Handler for the ADD_PLAYER command. */
|
||||||
|
public class AddPlayerHandler extends CommandHandler<AddPlayerRequest> {
|
||||||
|
private final UserRegistry userRegistry;
|
||||||
|
|
||||||
|
public AddPlayerHandler(UserRegistry userRegistry, ResponseDispatcher responseDispatcher) {
|
||||||
|
super(responseDispatcher);
|
||||||
|
this.userRegistry = userRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(AddPlayerRequest request) {
|
||||||
|
Optional<User> created =
|
||||||
|
userRegistry.registerIfAvailable(
|
||||||
|
request.getName(), request.getContext().sessionId());
|
||||||
|
if (created.isEmpty()) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(request.getContext(), "NAME_TAKEN", "Name already taken"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.add_player;
|
||||||
|
|
||||||
|
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 ADD_PLAYER command. */
|
||||||
|
public class AddPlayerParser implements CommandParser<AddPlayerRequest> {
|
||||||
|
@Override
|
||||||
|
public AddPlayerRequest parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
RequestParameterAccessor accessor =
|
||||||
|
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||||
|
|
||||||
|
String name = accessor.require("NAME");
|
||||||
|
|
||||||
|
int chips = accessor.require("CHIPS", Integer::parseInt);
|
||||||
|
|
||||||
|
return new AddPlayerRequest(primitiveRequest.context(), name, chips);
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.add_player;
|
||||||
|
|
||||||
|
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 data for the ADD_PLAYER command. */
|
||||||
|
public class AddPlayerRequest extends Request {
|
||||||
|
private final String name;
|
||||||
|
private final int chips;
|
||||||
|
|
||||||
|
public AddPlayerRequest(RequestContext context, String name, int chips) {
|
||||||
|
super(context);
|
||||||
|
this.name = name;
|
||||||
|
this.chips = chips;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getChips() {
|
||||||
|
return chips;
|
||||||
|
}
|
||||||
|
}
|
||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck;
|
||||||
|
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.OkResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the `JOIN_LOBBY` command.
|
||||||
|
*
|
||||||
|
* <p>Resolves the username from the session (requires {@link UserLoggedInCheck}), looks up the
|
||||||
|
* target lobby and attempts to add the user. On success an `+OK` response is dispatched; on failure
|
||||||
|
* an appropriate {@link ErrorResponse} with one of the error codes `LOBBY_NOT_FOUND`,
|
||||||
|
* `USER_NOT_LOGGED_IN` or `LOBBY_FULL_OR_ALREADY_IN` is returned.
|
||||||
|
*/
|
||||||
|
public class JoinLobbyHandler extends CommandHandler<JoinLobbyRequest> {
|
||||||
|
private final LobbyManager lobbyManager;
|
||||||
|
private final UserRegistry userRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link JoinLobbyHandler}.
|
||||||
|
*
|
||||||
|
* @param responseDispatcher dispatcher used to send responses back to the client
|
||||||
|
* @param lobbyManager manager providing lobby state and operations
|
||||||
|
* @param userRegistry registry to resolve session -> user mappings
|
||||||
|
*/
|
||||||
|
public JoinLobbyHandler(
|
||||||
|
ResponseDispatcher responseDispatcher,
|
||||||
|
LobbyManager lobbyManager,
|
||||||
|
UserRegistry userRegistry) {
|
||||||
|
super(responseDispatcher);
|
||||||
|
this.lobbyManager = lobbyManager;
|
||||||
|
this.userRegistry = userRegistry;
|
||||||
|
addCheck(new UserLoggedInCheck(userRegistry));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the join-lobby request.
|
||||||
|
*
|
||||||
|
* @param request the parsed {@link JoinLobbyRequest}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void execute(JoinLobbyRequest request) {
|
||||||
|
LobbyId lid = LobbyId.of(request.getId());
|
||||||
|
var lobby = lobbyManager.getLobby(lid);
|
||||||
|
if (lobby == null) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var maybeUser = userRegistry.getBySessionId(request.getContext().sessionId());
|
||||||
|
if (maybeUser.isEmpty()) {
|
||||||
|
// UserLoggedInCheck should normally prevent this; keep safe fallback
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(),
|
||||||
|
"USER_NOT_LOGGED_IN",
|
||||||
|
"No user associated with session"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = maybeUser.get();
|
||||||
|
String username = user.getName();
|
||||||
|
|
||||||
|
boolean ok = lobbyManager.addPlayerToLobby(username, lid);
|
||||||
|
if (!ok) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(),
|
||||||
|
"LOBBY_FULL_OR_ALREADY_IN",
|
||||||
|
"Lobby full or user already in lobby"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby;
|
||||||
|
|
||||||
|
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 `JOIN_LOBBY` command.
|
||||||
|
*
|
||||||
|
* <p>Expected request parameters:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>`ID` (int) — numeric lobby identifier (required)
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>The parser builds a {@link JoinLobbyRequest} containing the parsed lobby id and the original
|
||||||
|
* request context.
|
||||||
|
*/
|
||||||
|
public class JoinLobbyParser implements CommandParser<JoinLobbyRequest> {
|
||||||
|
/**
|
||||||
|
* Parse the incoming primitive request into a {@link JoinLobbyRequest}.
|
||||||
|
*
|
||||||
|
* @param primitiveRequest the raw primitive request
|
||||||
|
* @return a {@link JoinLobbyRequest} with the parsed lobby id and context
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public JoinLobbyRequest parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
RequestParameterAccessor accessor =
|
||||||
|
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||||
|
int id = accessor.require("ID", Integer::parseInt);
|
||||||
|
return new JoinLobbyRequest(primitiveRequest.context(), id);
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby;
|
||||||
|
|
||||||
|
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 data for the `JOIN_LOBBY` command.
|
||||||
|
*
|
||||||
|
* <p>Contains the lobby id the client wants to join and inherits the {@link Request} contextual
|
||||||
|
* information.
|
||||||
|
*/
|
||||||
|
public class JoinLobbyRequest extends Request {
|
||||||
|
private final int id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link JoinLobbyRequest}.
|
||||||
|
*
|
||||||
|
* @param context the request context
|
||||||
|
* @param id numeric lobby id to join
|
||||||
|
*/
|
||||||
|
public JoinLobbyRequest(RequestContext context, int id) {
|
||||||
|
super(context);
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the lobby id requested by the client.
|
||||||
|
*
|
||||||
|
* @return numeric lobby id
|
||||||
|
*/
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||||
|
|
||||||
|
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 java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/** Represents a single lobby: id, name, players and an optional GameController. */
|
||||||
|
public class Lobby {
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(Lobby.class.getName());
|
||||||
|
|
||||||
|
public enum AddResult {
|
||||||
|
NOT_ADDED,
|
||||||
|
ADDED,
|
||||||
|
ADDED_AND_STARTED
|
||||||
|
}
|
||||||
|
|
||||||
|
private final LobbyId id;
|
||||||
|
private final String name;
|
||||||
|
private final List<String> playerNames = new CopyOnWriteArrayList<>();
|
||||||
|
private volatile GameController gameController;
|
||||||
|
|
||||||
|
public Lobby(LobbyId id, String name) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LobbyId getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getPlayerNames() {
|
||||||
|
return List.copyOf(playerNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to add a player to this lobby.
|
||||||
|
*
|
||||||
|
* @return true if added, false if already present or full
|
||||||
|
*/
|
||||||
|
public boolean addPlayer(String playerName, int maxPlayers) {
|
||||||
|
if (playerName == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
synchronized (playerNames) {
|
||||||
|
if (playerNames.contains(playerName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (playerNames.size() >= maxPlayers) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
playerNames.add(playerName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean removePlayer(String playerName) {
|
||||||
|
return playerNames.remove(playerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initGame(GameController controller) {
|
||||||
|
this.gameController = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GameController getGameController() {
|
||||||
|
return gameController;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomically add a player and, if the lobby reached {@code autoStartPlayers} and no game exists
|
||||||
|
* yet, create and start a game. The operation is synchronized on the internal player list to
|
||||||
|
* avoid races when multiple joins happen concurrently.
|
||||||
|
*
|
||||||
|
* @return {@link AddResult} indicating whether the player was added and if a game was started
|
||||||
|
*/
|
||||||
|
public AddResult addPlayerAndMaybeStart(
|
||||||
|
String playerName, int maxPlayers, int autoStartPlayers, int defaultStartChips) {
|
||||||
|
if (playerName == null) {
|
||||||
|
return AddResult.NOT_ADDED;
|
||||||
|
}
|
||||||
|
synchronized (playerNames) {
|
||||||
|
if (playerNames.contains(playerName)) {
|
||||||
|
return AddResult.NOT_ADDED;
|
||||||
|
}
|
||||||
|
if (playerNames.size() >= maxPlayers) {
|
||||||
|
return AddResult.NOT_ADDED;
|
||||||
|
}
|
||||||
|
playerNames.add(playerName);
|
||||||
|
|
||||||
|
// If threshold reached and no game running, create and start game here
|
||||||
|
if (gameController == null && playerNames.size() == autoStartPlayers) {
|
||||||
|
try {
|
||||||
|
GameState state = new GameState();
|
||||||
|
GameEngine engine =
|
||||||
|
new GameEngine(
|
||||||
|
state,
|
||||||
|
new RuleEngine(new ArrayList<>()),
|
||||||
|
new RoundManager(),
|
||||||
|
new TurnManager());
|
||||||
|
GameController game = new GameController(engine);
|
||||||
|
|
||||||
|
for (String p : playerNames) {
|
||||||
|
game.addPlayer(PlayerId.of(p), defaultStartChips);
|
||||||
|
}
|
||||||
|
|
||||||
|
game.startGame();
|
||||||
|
this.gameController = game;
|
||||||
|
LOGGER.info(() -> "Auto-started game in lobby " + id.value());
|
||||||
|
return AddResult.ADDED_AND_STARTED;
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOGGER.log(Level.WARNING, "Auto-start failed for lobby " + id.value(), e);
|
||||||
|
return AddResult.ADDED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AddResult.ADDED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||||
|
|
||||||
|
/** Listener interface for lobby lifecycle events. */
|
||||||
|
public interface LobbyEventListener {
|
||||||
|
/** Called when a game is automatically started in the given lobby. */
|
||||||
|
void onGameStarted(LobbyId lobbyId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||||
|
|
||||||
|
/** Typesafe wrapper for lobby IDs (1-8). */
|
||||||
|
public record LobbyId(int value) {
|
||||||
|
public static LobbyId of(int value) {
|
||||||
|
return new LobbyId(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby.AddResult;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/** Manages dynamic creation of up to 8 lobbies and maps players to lobbies. */
|
||||||
|
public class LobbyManager {
|
||||||
|
private static final int MAX_LOBBIES = 8;
|
||||||
|
private static final int DEFAULT_MAX_PLAYERS = 4;
|
||||||
|
private static final int AUTO_START_PLAYERS = 4;
|
||||||
|
private static final int DEFAULT_START_CHIPS = 20000;
|
||||||
|
|
||||||
|
private final Map<LobbyId, Lobby> activeLobbies = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, LobbyId> playerToLobby = new ConcurrentHashMap<>();
|
||||||
|
private final List<LobbyEventListener> listeners = new CopyOnWriteArrayList<>();
|
||||||
|
private final int maxPlayersPerLobby;
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(LobbyManager.class.getName());
|
||||||
|
|
||||||
|
public LobbyManager() {
|
||||||
|
this(DEFAULT_MAX_PLAYERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LobbyManager(int maxPlayersPerLobby) {
|
||||||
|
this.maxPlayersPerLobby = maxPlayersPerLobby;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a new lobby with an automatic id (1..8). Returns null if none available. */
|
||||||
|
public synchronized LobbyId createNewLobby(String name) {
|
||||||
|
if (activeLobbies.size() >= MAX_LOBBIES) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (int i = 1; i <= MAX_LOBBIES; i++) {
|
||||||
|
LobbyId id = LobbyId.of(i);
|
||||||
|
if (!activeLobbies.containsKey(id)) {
|
||||||
|
Lobby lobby = new Lobby(id, name == null ? ("Room " + i) : name);
|
||||||
|
activeLobbies.put(id, lobby);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Lobby getLobby(LobbyId id) {
|
||||||
|
if (id == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return activeLobbies.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Lobby getLobbyByUsername(String username) {
|
||||||
|
LobbyId id = playerToLobby.get(username);
|
||||||
|
if (id == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return activeLobbies.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean addPlayerToLobby(String username, LobbyId lobbyId) {
|
||||||
|
Lobby lobby = getLobby(lobbyId);
|
||||||
|
if (lobby == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
AddResult result =
|
||||||
|
lobby.addPlayerAndMaybeStart(
|
||||||
|
username, maxPlayersPerLobby, AUTO_START_PLAYERS, DEFAULT_START_CHIPS);
|
||||||
|
if (result != AddResult.NOT_ADDED) {
|
||||||
|
playerToLobby.put(username, lobbyId);
|
||||||
|
if (result == AddResult.ADDED_AND_STARTED) {
|
||||||
|
LOGGER.info(
|
||||||
|
() ->
|
||||||
|
"Lobby "
|
||||||
|
+ lobbyId.value()
|
||||||
|
+ " reached "
|
||||||
|
+ AUTO_START_PLAYERS
|
||||||
|
+ " players; game started.");
|
||||||
|
notifyGameStarted(lobbyId);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addListener(LobbyEventListener listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeListener(LobbyEventListener listener) {
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyGameStarted(LobbyId lobbyId) {
|
||||||
|
for (LobbyEventListener l : listeners) {
|
||||||
|
try {
|
||||||
|
l.onGameStarted(lobbyId);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOGGER.warning(
|
||||||
|
() ->
|
||||||
|
"Listener threw while handling game-start for lobby "
|
||||||
|
+ lobbyId.value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean removePlayer(String username) {
|
||||||
|
LobbyId id = playerToLobby.remove(username);
|
||||||
|
if (id == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Lobby lobby = activeLobbies.get(id);
|
||||||
|
if (lobby == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean removed = lobby.removePlayer(username);
|
||||||
|
// If lobby becomes empty, remove it
|
||||||
|
if (lobby.getPlayerNames().isEmpty()) {
|
||||||
|
activeLobbies.remove(id);
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Lobby> getAllLobbies() {
|
||||||
|
return activeLobbies.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the given action to every player username in the lobby identified by {@code lobbyId}.
|
||||||
|
* This is a small helper that keeps iteration logic centralized and avoids leaking internal
|
||||||
|
* collections to callers.
|
||||||
|
*/
|
||||||
|
public void broadcast(LobbyId lobbyId, java.util.function.Consumer<String> action) {
|
||||||
|
Lobby lobby = getLobby(lobbyId);
|
||||||
|
if (lobby == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String username : lobby.getPlayerNames()) {
|
||||||
|
action.accept(username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user