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
This commit is contained in:
@@ -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