From 2ab27ac3eda33a38bc7eb724f5faf00da8ed8c21 Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sat, 11 Apr 2026 17:41:45 +0200 Subject: [PATCH] Feat: Add lobby class and Manager with conenction to Game engine. The gameengine will be satrte when 4 players joined the lobby. This is made possible by the callback-driven-Listener Refs #73 --- .../casono/server/domain/lobby/Lobby.java | 131 ++++++++++++++++ .../domain/lobby/LobbyEventListener.java | 7 + .../casono/server/domain/lobby/LobbyId.java | 8 + .../server/domain/lobby/LobbyManager.java | 144 ++++++++++++++++++ 4 files changed, 290 insertions(+) create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/Lobby.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyEventListener.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyId.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyManager.java diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/Lobby.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/Lobby.java new file mode 100644 index 0000000..7653685 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/Lobby.java @@ -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 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 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; + } + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyEventListener.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyEventListener.java new file mode 100644 index 0000000..d6fee3a --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyEventListener.java @@ -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); +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyId.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyId.java new file mode 100644 index 0000000..457aea0 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyId.java @@ -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); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyManager.java new file mode 100644 index 0000000..2179e8a --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/lobby/LobbyManager.java @@ -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 activeLobbies = new ConcurrentHashMap<>(); + private final Map playerToLobby = new ConcurrentHashMap<>(); + private final List 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 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 action) { + Lobby lobby = getLobby(lobbyId); + if (lobby == null) { + return; + } + for (String username : lobby.getPlayerNames()) { + action.accept(username); + } + } +}