Merge branch 'feat/ms-4-integration' into 'main'
Feat/ms 4 integration Closes #59, #45, #52, #72, #73, #68, #71, and #36 See merge request cs108-fs26/Gruppe-13!126
This commit was merged in pull request #282.
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- vim-markdown-toc -->
|
||||
|
||||
Der Pokertisch ist ein unglaublich faszinierender Erlebnisraum, in dem man sehr viel lernen kann: über sich selbst, über andere Menschen und über Fragen wie: wie treffe ich eigentlich Entscheidungen, wie gehe ich mit Stress und Unsicherheit um und wie gut ich darin bin, mich in andere hineinzuversetzen und Situationen richtig einzuschätzen.
|
||||
Der Pokertisch ist ein unglaublich faszinierender Erlebnisraum, in dem man sehr viel lernen kann: über sich selbst, über andere Menschen und über Fragen wie: Wie treffe ich eigentlich Entscheidungen, wie gehe ich mit Stress und Unsicherheit um und wie gut ich darin bin, mich in andere hineinzuversetzen und Situationen richtig einzuschätzen.
|
||||
|
||||
Damit Du in diesem Erlebnisraum starten kannst, ist es – wie bei jedem Spiel – notwendig, zuerst die Grundregeln und den Spielablauf zu verstehen.
|
||||
|
||||
@@ -61,7 +61,7 @@ Regel: *50 Acting in Turn 🟢*
|
||||
|
||||
## Beispiel Preflop
|
||||
|
||||
Julian schaut seine Karten an und entscheidet sich direkt für einen Call von **600 Chips**.
|
||||
Julian schaut seine Karten an und entscheidet sich direkt für einen Raise von **600 Chips**.
|
||||
|
||||

|
||||
|
||||
@@ -107,7 +107,7 @@ Wieder beginnt eine neue Setzrunde.
|
||||
|
||||
Jona setzt diesmal **3000 Chips**. Lars bezahlt erneut (Call), weil seine Hand weiterhin gut spielbar ist.
|
||||
|
||||

|
||||

|
||||
|
||||
Regel: *53 Action Out of Turn 🟡*
|
||||
|
||||
@@ -119,7 +119,7 @@ Dies ist die letzte Entscheidung im Spiel.
|
||||
|
||||
Jona setzt **5000 Chips**.
|
||||
|
||||

|
||||

|
||||
|
||||
Lars muss jetzt entscheiden: Fold, Call oder Raise auf 10000 Chips.
|
||||
|
||||
@@ -131,7 +131,7 @@ Wenn nach der letzten Setzrunde noch zwei Spieler übrig sind, kommt es zum Show
|
||||
|
||||
Beide Spieler zeigen ihre Karten offen. Gewonnen hat die **beste 5-Karten-Kombination aus Handkarten und Gemeinschaftskarten**.
|
||||
|
||||

|
||||

|
||||
|
||||
Regel: *12 Cards Speak at Showdown 🟢*
|
||||
Regel: *16 Face Up for All-Ins 🟢*
|
||||
@@ -149,11 +149,11 @@ Je höher die Kombination, desto stärker die Hand und desto wahrscheinlicher de
|
||||
|
||||
Jona: 2. Paar:
|
||||
|
||||

|
||||

|
||||
|
||||
Lars: 1. Paar:
|
||||
|
||||

|
||||

|
||||
|
||||
Da zwei Paare in der Rangfolge über einem einzelnen Paar stehen, gewinnt Jona diese Runde.
|
||||
|
||||
|
||||
Binary file not shown.
@@ -10,9 +10,8 @@ import org.apache.logging.log4j.Logger;
|
||||
/**
|
||||
* Entry point and bootstrap helper for the Casono client application.
|
||||
*
|
||||
* <p>This class is responsible for two tasks: - performing an optional startup LOGIN when a
|
||||
* username is supplied on the command line, and - providing a shared {@link ClientService} instance
|
||||
* that the UI can reuse so the initial connection remains active.
|
||||
* <p>Responsibilities: - Parse CLI args (host:port, optional username) - Store username centrally
|
||||
* so UI can reuse it - Create a shared ClientService and do startup LOGIN (optional)
|
||||
*/
|
||||
public class ClientApp {
|
||||
|
||||
@@ -21,16 +20,13 @@ public class ClientApp {
|
||||
/** Shared client connection used when a username is provided at startup. */
|
||||
private static volatile ClientService sharedClientService;
|
||||
|
||||
private static volatile String sharedUsername;
|
||||
|
||||
/** Default constructor. */
|
||||
public ClientApp() {
|
||||
// Default constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shared {@link ClientService} instance created during startup, or {@code null} if
|
||||
* none exists. The UI may call this to reuse the connection that already performed the initial
|
||||
* LOGIN.
|
||||
*/
|
||||
public static ClientService getSharedClientService() {
|
||||
return sharedClientService;
|
||||
}
|
||||
@@ -39,64 +35,89 @@ public class ClientApp {
|
||||
sharedClientService = cs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the client application with the given address.
|
||||
*
|
||||
* @param arg Address in the format "ip:port".
|
||||
* @throws IllegalArgumentException if the address format is invalid.
|
||||
*/
|
||||
public static String getSharedUsername() {
|
||||
return sharedUsername;
|
||||
}
|
||||
|
||||
private static void setSharedUsername(String username) {
|
||||
sharedUsername = username;
|
||||
LOGGER.info("sharedUsername set to '{}'", getSharedUsername());
|
||||
}
|
||||
|
||||
public static void start(String arg, String username) {
|
||||
String[] hostPort = parseHostAndPort(arg);
|
||||
String host = hostPort[0];
|
||||
int port = Integer.parseInt(hostPort[1]);
|
||||
|
||||
configureServerProperties(host, port);
|
||||
setSharedUsername(username != null && !username.isBlank() ? username.trim() : null);
|
||||
|
||||
if (getSharedUsername() != null) {
|
||||
performStartupLogin(host, port, username);
|
||||
}
|
||||
|
||||
launchUI(arg, username);
|
||||
}
|
||||
|
||||
private static String[] parseHostAndPort(String arg) {
|
||||
String[] parts = arg.split(":", 2);
|
||||
if (parts.length != 2) {
|
||||
throw new IllegalArgumentException("Address must be in format <ip>:<port>.");
|
||||
throw new IllegalArgumentException("The address must be in the format <ip>:<port>.");
|
||||
}
|
||||
String host = parts[0];
|
||||
int port = Integer.parseInt(parts[1]);
|
||||
return parts;
|
||||
}
|
||||
|
||||
private static void configureServerProperties(String host, int port) {
|
||||
LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host);
|
||||
System.setProperty("casono.server.host", host);
|
||||
System.setProperty("casono.server.port", String.valueOf(port));
|
||||
// If a username was provided, create a shared connection and perform
|
||||
// the initial LOGIN asynchronously on that connection. The connection
|
||||
// is intentionally NOT closed so the UI can reuse it.
|
||||
if (username != null && !username.isBlank()) {
|
||||
try {
|
||||
ClientService clientService = new ClientService(host, port);
|
||||
setSharedClientService(clientService);
|
||||
}
|
||||
|
||||
java.util.concurrent.ExecutorService bg =
|
||||
java.util.concurrent.Executors.newSingleThreadExecutor(
|
||||
r -> {
|
||||
Thread t = new Thread(r);
|
||||
t.setDaemon(true);
|
||||
t.setName("casono-startup-login");
|
||||
return t;
|
||||
});
|
||||
bg.submit(
|
||||
() -> {
|
||||
try {
|
||||
LobbyClient lobbyClient = new LobbyClient(clientService);
|
||||
LoginResult res = lobbyClient.login(username);
|
||||
LOGGER.info(
|
||||
"Assigned username='{}' id={}",
|
||||
res.getUsername(),
|
||||
res.getId());
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.warn("Startup login failed: {}", e.getMessage());
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Unexpected error during startup login", e);
|
||||
} finally {
|
||||
bg.shutdown();
|
||||
private static void performStartupLogin(String host, int port, String username) {
|
||||
try {
|
||||
ClientService clientService = new ClientService(host, port);
|
||||
setSharedClientService(clientService);
|
||||
|
||||
java.util.concurrent.ExecutorService bg =
|
||||
java.util.concurrent.Executors.newSingleThreadExecutor(
|
||||
r -> {
|
||||
Thread t = new Thread(r);
|
||||
t.setDaemon(true);
|
||||
t.setName("casono-startup-login");
|
||||
return t;
|
||||
});
|
||||
|
||||
bg.submit(
|
||||
() -> {
|
||||
try {
|
||||
LobbyClient lobbyClient = new LobbyClient(clientService);
|
||||
LoginResult res = lobbyClient.login(username);
|
||||
|
||||
if (res != null
|
||||
&& res.getUsername() != null
|
||||
&& !res.getUsername().isBlank()) {
|
||||
setSharedUsername(res.getUsername().trim());
|
||||
}
|
||||
});
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.warn(
|
||||
"Could not establish initial connection for startup login: {}",
|
||||
e.getMessage());
|
||||
// UI will create its own connection when it initializes
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.info(
|
||||
"Assigned username='{}' id={}",
|
||||
res != null ? res.getUsername() : null,
|
||||
res != null ? res.getId() : null);
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.warn("Startup login failed: {}", e.getMessage());
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Unexpected error during startup login", e);
|
||||
} finally {
|
||||
bg.shutdown();
|
||||
}
|
||||
});
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.warn(
|
||||
"Could not establish initial connection for startup login: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void launchUI(String arg, String username) {
|
||||
if (username != null && !username.isBlank()) {
|
||||
Launcher.main(new String[] {arg, username});
|
||||
} else {
|
||||
@@ -104,19 +125,13 @@ public class ClientApp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point. {@code <host:port>} (e.g. {@code 127.0.0.1:1234})
|
||||
*
|
||||
* <p>If a username is provided the application attempts a startup LOGIN on the shared
|
||||
* connection; the UI will still start and will reuse the connection when possible. The username
|
||||
* is not propagated via a system property for UI display.
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
if (args == null || args.length == 0) {
|
||||
throw new IllegalArgumentException("Address argument required: <host:port>");
|
||||
}
|
||||
// Optional username argument
|
||||
|
||||
String username = args.length > 1 && args[1] != null && !args[1].isBlank() ? args[1] : null;
|
||||
|
||||
start(args[0], username);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.WeakHashMap;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
@@ -19,8 +20,14 @@ import org.jspecify.annotations.Nullable;
|
||||
*/
|
||||
public class ChatController {
|
||||
|
||||
/** Ensures one active message poller per physical ClientService connection. */
|
||||
private static final Map<ClientService, ChatController> ACTIVE_CONTROLLERS =
|
||||
new WeakHashMap<>();
|
||||
|
||||
private final String username;
|
||||
|
||||
private final ClientService clientService;
|
||||
|
||||
private final ChatClient chatClient;
|
||||
|
||||
public ChatBoxController getChatBoxController() {
|
||||
@@ -58,24 +65,43 @@ public class ChatController {
|
||||
*/
|
||||
public ChatController(String username, ClientService clientService) {
|
||||
this.username = username;
|
||||
this.clientService = clientService;
|
||||
chatClient = new ChatClient(clientService);
|
||||
chatModelMap = new LinkedHashMap<>();
|
||||
localUserList = new ArrayList<>();
|
||||
this.chatBoxController = new ChatBoxController(username, this);
|
||||
this.logger = LogManager.getLogger(ChatController.class);
|
||||
this.timer = new Timer();
|
||||
|
||||
registerAsActiveController(clientService);
|
||||
|
||||
this.timer = new Timer(true);
|
||||
timer.schedule(
|
||||
new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
receiveMessage();
|
||||
checkWhisperUsers();
|
||||
try {
|
||||
receiveMessage();
|
||||
checkWhisperUsers();
|
||||
} catch (RuntimeException e) {
|
||||
logger.warn("Chat refresh failed: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
},
|
||||
0,
|
||||
REFRESH_TIME);
|
||||
}
|
||||
|
||||
private void registerAsActiveController(ClientService clientService) {
|
||||
synchronized (ACTIVE_CONTROLLERS) {
|
||||
ChatController oldController = ACTIVE_CONTROLLERS.get(clientService);
|
||||
if (oldController != null && oldController != this) {
|
||||
oldController.shutdown();
|
||||
logger.info("Replaced previous ChatController poller for shared ClientService");
|
||||
}
|
||||
ACTIVE_CONTROLLERS.put(clientService, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to be activated, if a lobby has been chosen. It will update the UI and add a new
|
||||
* ChatModel to hold the Data for the Lobby Chat
|
||||
@@ -84,8 +110,14 @@ public class ChatController {
|
||||
*/
|
||||
public void setLobbyChat(int lobbyId) {
|
||||
this.lobbyId = lobbyId;
|
||||
ChatKey key = new ChatKey(ChatType.LOBBY);
|
||||
ChatModel existingModel = chatModelMap.get(key);
|
||||
if (existingModel != null) {
|
||||
existingModel.lobbyId = lobbyId;
|
||||
return;
|
||||
}
|
||||
ChatModel lobbyChatModel = new ChatModel(ChatType.LOBBY, username, lobbyId, null);
|
||||
chatModelMap.put(new ChatKey(ChatType.LOBBY), lobbyChatModel);
|
||||
chatModelMap.put(key, lobbyChatModel);
|
||||
this.chatBoxController.addChatTab("Lobby", lobbyChatModel);
|
||||
}
|
||||
|
||||
@@ -107,7 +139,7 @@ public class ChatController {
|
||||
chatModelMap.get(new ChatKey(ChatType.GLOBAL)).addMessage(msg);
|
||||
break;
|
||||
case ChatType.LOBBY:
|
||||
if (msg.lobbyId == lobbyId) {
|
||||
if (msg.lobbyId == getActiveLobbyChatId()) {
|
||||
chatModelMap
|
||||
.computeIfAbsent(
|
||||
new ChatKey(ChatType.LOBBY),
|
||||
@@ -147,6 +179,20 @@ public class ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the lobbyId of the currently active lobby chat, to check if incoming lobby
|
||||
* messages belong to the same lobby.
|
||||
*
|
||||
* @return the lobbyId of the currently active lobby chat.
|
||||
*/
|
||||
private int getActiveLobbyChatId() {
|
||||
ChatModel lobbyModel = chatModelMap.get(new ChatKey(ChatType.LOBBY));
|
||||
if (lobbyModel != null) {
|
||||
return lobbyModel.lobbyId;
|
||||
}
|
||||
return lobbyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to process the usernames of other users connected to the server, after they got polled
|
||||
* by the {@link ChatClient}.
|
||||
@@ -162,11 +208,33 @@ public class ChatController {
|
||||
for (String user : users) {
|
||||
String value = user.split("\\=")[1];
|
||||
logger.info(value);
|
||||
if (!(localUserList.contains(value) || value.equals(username))) {
|
||||
localUserList.add(value);
|
||||
logger.info("adding new whisper user");
|
||||
chatBoxController.addWhisperUser(value);
|
||||
}
|
||||
addWhisperUser(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a whisper target locally and updates the UI if it is a new user.
|
||||
*
|
||||
* @param user target username
|
||||
*/
|
||||
public synchronized void addWhisperUser(String user) {
|
||||
if (user == null || user.isBlank()) {
|
||||
return;
|
||||
}
|
||||
if (!(localUserList.contains(user) || user.equals(username))) {
|
||||
localUserList.add(user);
|
||||
logger.info("adding new whisper user");
|
||||
chatBoxController.addWhisperUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
/** Stops polling background tasks for this chat controller instance. */
|
||||
public void shutdown() {
|
||||
timer.cancel();
|
||||
synchronized (ACTIVE_CONTROLLERS) {
|
||||
if (ACTIVE_CONTROLLERS.get(clientService) == this) {
|
||||
ACTIVE_CONTROLLERS.remove(clientService);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,41 +2,72 @@ package ch.unibas.dmi.dbis.cs108.casono.client.game;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.GameClient;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Service class responsible for managing the game state and providing methods to interact with the
|
||||
* game.
|
||||
*/
|
||||
public class GameService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(GameService.class.getName());
|
||||
|
||||
private final GameClient client;
|
||||
private GameState state;
|
||||
|
||||
/**
|
||||
* Constructs a GameService with the given GameClient for communication with the server.
|
||||
* Constructs a GameService with the specified GameClient.
|
||||
*
|
||||
* @param client The GameClient instance used to send commands and receive responses from the
|
||||
* server.
|
||||
* @param client The GameClient used to communicate with the server. Must not be null.
|
||||
*/
|
||||
public GameService(GameClient client) {
|
||||
if (client == null) {
|
||||
throw new IllegalArgumentException("GameClient must not be null");
|
||||
}
|
||||
|
||||
this.client = client;
|
||||
LOG.info("GameService created");
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the game state by fetching the latest state from the server using the GameClient.
|
||||
* Refreshes the game state by fetching the latest state from the server using the GameClient.
|
||||
*
|
||||
* @return The updated GameState object representing the current state of the game after
|
||||
* refreshing.
|
||||
* @return The updated GameState after refreshing.
|
||||
*/
|
||||
public GameState refresh() {
|
||||
state = client.fetchGameState();
|
||||
LOG.fine("Refreshing game state...");
|
||||
|
||||
GameState newState;
|
||||
try {
|
||||
newState = client.fetchGameState();
|
||||
} catch (Exception e) {
|
||||
// Shouldn't happen with the new GameClient, but keep service stable.
|
||||
LOG.log(Level.WARNING, "refresh() failed: " + e.getMessage(), e);
|
||||
return state;
|
||||
}
|
||||
|
||||
if (newState == null) {
|
||||
LOG.fine("No new state (null) -> keeping previous state");
|
||||
return state;
|
||||
}
|
||||
|
||||
this.state = newState;
|
||||
|
||||
LOG.info(
|
||||
() ->
|
||||
"State updated: phase="
|
||||
+ state.phase
|
||||
+ " pot="
|
||||
+ state.pot
|
||||
+ " players="
|
||||
+ (state.players != null ? state.players.size() : 0)
|
||||
+ " community="
|
||||
+ (state.communityCards != null ? state.communityCards.size() : 0));
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current game state.
|
||||
* Retrieves the current phase of the game.
|
||||
*
|
||||
* @return The current GameState object representing the state of the game.
|
||||
* @return The current game phase as a string (e.g., "Pre-Flop", "Flop", "Turn", "River").
|
||||
*/
|
||||
public int getPot() {
|
||||
ensureState();
|
||||
@@ -44,76 +75,82 @@ public class GameService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current game state.
|
||||
* Retrieves the current phase of the game.
|
||||
*
|
||||
* @return The current GameState object representing the state of the game.
|
||||
* @return The current game phase as a string (e.g., "Pre-Flop", "Flop", "Turn", "River").
|
||||
*/
|
||||
public List<Card> getCommunityCards() {
|
||||
ensureState();
|
||||
return state.communityCards;
|
||||
return state.communityCards != null ? state.communityCards : List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of players in the current game state.
|
||||
* Retrieves the list of players currently in the game.
|
||||
*
|
||||
* @return A list of Player objects representing the players in the current game state.
|
||||
* @return A list of Player objects representing the players in the game. A list of Player
|
||||
* objects representing the players in the game.
|
||||
*/
|
||||
public List<Player> getPlayers() {
|
||||
ensureState();
|
||||
return state.players;
|
||||
return state.players != null ? state.players : List.of();
|
||||
}
|
||||
|
||||
/** Send a CALL command to the server to indicate that the player wants to call. */
|
||||
/**
|
||||
* Retrieves the index of the current player whose turn it is.
|
||||
*
|
||||
* @return The index of the current player in the players list, or -1 if not available.
|
||||
*/
|
||||
public Player getWinner() {
|
||||
ensureState();
|
||||
if (state.winnerIndex < 0
|
||||
|| state.players == null
|
||||
|| state.winnerIndex >= state.players.size()) {
|
||||
return null;
|
||||
}
|
||||
return state.players.get(state.winnerIndex);
|
||||
}
|
||||
|
||||
/** Retrieves the current phase of the game. */
|
||||
public void call() {
|
||||
client.sendCall();
|
||||
}
|
||||
|
||||
/** Send a FOLD command to the server to indicate that the player wants to fold. */
|
||||
/** Retrieves the current phase of the game. */
|
||||
public void fold() {
|
||||
client.sendFold();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a BET command to the server to indicate that the player wants to bet with the specified
|
||||
* amount.
|
||||
* Retrieves the current phase of the game.
|
||||
*
|
||||
* @param amount The amount the player wants to bet.
|
||||
* @param amount The amount to bet. Must be a positive integer.
|
||||
*/
|
||||
public void bet(int amount) {
|
||||
client.sendBet(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a RAISE command to the server to indicate that the player wants to raise to the
|
||||
* specified amount.
|
||||
* Retrieves the current phase of the game.
|
||||
*
|
||||
* @param amount The amount the player wants to raise to.
|
||||
* @param amount The amount to raise. Must be a positive integer.
|
||||
*/
|
||||
public void raise(int amount) {
|
||||
client.sendRaise(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the winner of the game if there is one. If the game is still ongoing or if there is no
|
||||
* winner, this method returns null.
|
||||
*
|
||||
* @return The Player object representing the winner of the game, or null if there is no winner
|
||||
* yet.
|
||||
*/
|
||||
public Player getWinner() {
|
||||
ensureState();
|
||||
|
||||
if (state.winnerIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return state.players.get(state.winnerIndex);
|
||||
}
|
||||
|
||||
/** Ensure that the game state has been initialized before accessing it. */
|
||||
/** Ensures that the game state has been initialized before accessing it. */
|
||||
private void ensureState() {
|
||||
if (state == null) {
|
||||
throw new IllegalStateException("Call refresh() first");
|
||||
throw new IllegalStateException("GameService used before any successful refresh()");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current GameState without refreshing from the server.
|
||||
*
|
||||
* @return The current GameState, or null if it has not been initialized yet.
|
||||
*/
|
||||
public GameState peekStateOrNull() {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ import java.util.List;
|
||||
* dealer position, active player, community cards, and player information.
|
||||
*/
|
||||
public class GameState {
|
||||
public List<Player> players = new ArrayList<>();
|
||||
public List<Card> communityCards = new ArrayList<>();
|
||||
|
||||
public String phase;
|
||||
public int pot;
|
||||
public int currentBet;
|
||||
public int dealer;
|
||||
public int activePlayer;
|
||||
public int winnerIndex = -1;
|
||||
|
||||
public List<Card> communityCards = new ArrayList<>();
|
||||
public List<Player> players = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -79,21 +79,11 @@ public class ChatClient {
|
||||
public List<String> getUsers() {
|
||||
logger.info("Asking server for list of users");
|
||||
List<String> users = clientService.processCommand("LIST_USERS");
|
||||
List<String> parameters = new ArrayList<String>();
|
||||
for (int i = 0; i < users.size(); i++) {
|
||||
String line = users.get(i);
|
||||
if (line.equals("USERS")) {
|
||||
continue;
|
||||
} else if (line.equals("END")) {
|
||||
break;
|
||||
}
|
||||
line = line.replaceFirst("^\t", "");
|
||||
|
||||
if (line.equals("USER")) {
|
||||
String username = users.get(i + 1);
|
||||
username = username.replaceFirst("^\t", "");
|
||||
username = username.replaceFirst("^\t", "");
|
||||
parameters.add(username);
|
||||
List<String> parameters = new ArrayList<>();
|
||||
for (String line : users) {
|
||||
String trimmed = line == null ? "" : line.trim();
|
||||
if (trimmed.startsWith("USERNAME=")) {
|
||||
parameters.add(trimmed);
|
||||
}
|
||||
}
|
||||
return parameters;
|
||||
|
||||
@@ -5,7 +5,9 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
@@ -105,50 +107,91 @@ public class ClientService {
|
||||
String responseText = rp.payload();
|
||||
logger.info("Raw message '{}'", responseText);
|
||||
|
||||
ParsedPacket parsed = parsePacketContent(responseText);
|
||||
|
||||
logger.debug(
|
||||
"Parsed response lines (rid={}, success={}, openBlocks={}): {}",
|
||||
rid,
|
||||
parsed.success,
|
||||
parsed.blockStack.size(),
|
||||
parsed.lines);
|
||||
|
||||
handleParsedResponse(rid, parsed.success, parsed.lines);
|
||||
}
|
||||
|
||||
private ParsedPacket parsePacketContent(String responseText) {
|
||||
boolean hasStatus = false;
|
||||
boolean success = false;
|
||||
List<String> lines = new ArrayList<>();
|
||||
Deque<String> blockStack = new ArrayDeque<>();
|
||||
|
||||
for (String rawLine : responseText.split("\\n")) {
|
||||
String line = rawLine;
|
||||
String trimmed = line.trim();
|
||||
|
||||
if (!hasStatus) {
|
||||
if ("+OK".equals(line)) {
|
||||
success = true;
|
||||
StatusCheckResult status = checkStatus(trimmed);
|
||||
if (status != null) {
|
||||
success = status.success;
|
||||
hasStatus = true;
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("-ERR") || line.startsWith("-ERROR")) {
|
||||
success = false;
|
||||
hasStatus = true;
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("END".equals(line)) {
|
||||
if (trimmed.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
line = line.replaceFirst("^\\s+", "");
|
||||
trimmed = line.trim();
|
||||
|
||||
if (isContainerStart(trimmed)) {
|
||||
blockStack.push(trimmed);
|
||||
lines.add(trimmed);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("END".equals(trimmed)) {
|
||||
if (!blockStack.isEmpty()) {
|
||||
blockStack.pop();
|
||||
lines.add("END");
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (line.startsWith("\t")) {
|
||||
line = line.substring(1);
|
||||
}
|
||||
lines.add(line);
|
||||
}
|
||||
|
||||
if (!hasStatus) {
|
||||
if (responseText.contains("+OK")) {
|
||||
success = true;
|
||||
hasStatus = true;
|
||||
} else if (responseText.contains("-ERR") || responseText.contains("-ERROR")) {
|
||||
success = false;
|
||||
hasStatus = true;
|
||||
} else {
|
||||
// best effort: treat as success
|
||||
success = true;
|
||||
hasStatus = true;
|
||||
}
|
||||
success = inferStatus(responseText);
|
||||
}
|
||||
|
||||
return new ParsedPacket(success, lines, blockStack);
|
||||
}
|
||||
|
||||
private StatusCheckResult checkStatus(String trimmed) {
|
||||
if ("+OK".equals(trimmed)) {
|
||||
return new StatusCheckResult(true);
|
||||
}
|
||||
if (trimmed.startsWith("-ERR") || trimmed.startsWith("-ERROR")) {
|
||||
return new StatusCheckResult(false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean inferStatus(String responseText) {
|
||||
if (responseText.contains("+OK")) {
|
||||
return true;
|
||||
}
|
||||
if (responseText.contains("-ERR") || responseText.contains("-ERROR")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void handleParsedResponse(int rid, boolean success, List<String> lines)
|
||||
throws InterruptedException {
|
||||
if (rid == 0) {
|
||||
for (Consumer<List<String>> l : eventListeners) {
|
||||
try {
|
||||
@@ -167,6 +210,21 @@ public class ClientService {
|
||||
}
|
||||
}
|
||||
|
||||
private record ParsedPacket(boolean success, List<String> lines, Deque<String> blockStack) {}
|
||||
|
||||
private record StatusCheckResult(boolean success) {}
|
||||
|
||||
private boolean isContainerStart(String token) {
|
||||
return token.equals("LOBBIES")
|
||||
|| token.equals("LOBBY")
|
||||
|| token.equals("PLAYERS")
|
||||
|| token.equals("PLAYER")
|
||||
|| token.equals("USERS")
|
||||
|| token.equals("USER")
|
||||
|| token.equals("CARDS")
|
||||
|| token.equals("CARD");
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a ClientService in offline mode. No network connection will be attempted and calls
|
||||
* to processCommand will throw a RuntimeException.
|
||||
|
||||
@@ -5,320 +5,277 @@ import ch.unibas.dmi.dbis.cs108.casono.client.game.GameState;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.Player;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* The GameClient class is responsible for communicating with the server to retrieve the current
|
||||
* game state. It sends a command to the server and parses the response into a structured GameState
|
||||
* object.
|
||||
* Fetches and parses game state from server.
|
||||
*
|
||||
* <p>Protocol notes (based on your current server implementation): - Success replies contain:
|
||||
* PHASE, POT, CURRENT_BET, DEALER, ACTIVE_PLAYER, then blocks: - CARD ... END (community cards on
|
||||
* root level) - PLAYER ... (NAME/CHIPS/BET/STATE + optional CARD blocks for requesting player) ...
|
||||
* END - Error replies contain: -ERR then CODE=..., MSG=..., END
|
||||
*
|
||||
* <p>Important: - The server does NOT wrap cards in a "CARDS" container. - CARD blocks appear
|
||||
* either: - at root level -> community cards - inside PLAYER -> hole cards for that player (usually
|
||||
* only for the requesting user)
|
||||
*/
|
||||
public class GameClient {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(GameClient.class.getName());
|
||||
|
||||
private final ClientService client;
|
||||
private final int gameId;
|
||||
|
||||
/**
|
||||
* Constructs a GameClient with the given ClientService for communication.
|
||||
*
|
||||
* @param client The ClientService instance used to send commands and receive responses from the
|
||||
* server.
|
||||
*/
|
||||
public GameClient(ClientService client, int gameId) {
|
||||
if (client == null) {
|
||||
throw new IllegalArgumentException("ClientService must not be null");
|
||||
}
|
||||
|
||||
this.client = client;
|
||||
this.gameId = gameId;
|
||||
LOG.info(() -> "GameClient initialized for gameId=" + gameId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the current game state from the server by sending a "GET_GAME_STATE"
|
||||
*
|
||||
* @return A GameState object representing the current state of the game, as parsed
|
||||
*/
|
||||
public GameState fetchGameState() {
|
||||
List<String> responseLines = client.processCommand("GET_GAME_STATE GAME_ID=" + gameId);
|
||||
final String cmd = "GET_GAME_STATE GAME_ID=" + gameId;
|
||||
|
||||
if (responseLines == null || responseLines.isEmpty()) {
|
||||
throw new RuntimeException("Empty server response");
|
||||
List<String> lines;
|
||||
try {
|
||||
lines = client.processCommand(cmd);
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.SEVERE, "processCommand failed for " + cmd + ": " + e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (responseLines.get(0).startsWith("-ERR")) {
|
||||
throw new RuntimeException("Server Error: " + String.join(" ", responseLines));
|
||||
if (lines == null || lines.isEmpty()) {
|
||||
LOG.warning("Empty server response for " + cmd);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!responseLines.get(0).startsWith("+OK")) {
|
||||
throw new RuntimeException("Invalid response: missing +OK");
|
||||
LOG.info(() -> "GET_GAME_STATE raw lines count=" + lines.size() + " lines=" + lines);
|
||||
|
||||
String joined = String.join("\n", lines);
|
||||
|
||||
if (joined.contains("-ERR") || joined.contains("-ERROR")) {
|
||||
String code = extractValue(joined, "CODE");
|
||||
String msg = extractValue(joined, "MSG");
|
||||
LOG.info(() -> "GET_GAME_STATE returned -ERR code=" + code + " msg=" + msg);
|
||||
|
||||
if ("GAME_NOT_STARTED".equalsIgnoreCase(code)) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String fullResponse = String.join("\n", responseLines);
|
||||
return parse(fullResponse);
|
||||
try {
|
||||
GameState state = parseGameState(joined);
|
||||
LOG.info(
|
||||
() ->
|
||||
"Parsed state: phase="
|
||||
+ state.phase
|
||||
+ " pot="
|
||||
+ state.pot
|
||||
+ " players="
|
||||
+ (state.players != null ? state.players.size() : 0)
|
||||
+ " community="
|
||||
+ (state.communityCards != null
|
||||
? state.communityCards.size()
|
||||
: 0));
|
||||
return state;
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.SEVERE, "Failed parsing game state. Payload=\n" + joined, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Send a CALL command to the server to indicate that the player wants to call */
|
||||
public void sendCall() {
|
||||
client.processCommand("CALL GAME_ID=" + gameId);
|
||||
}
|
||||
|
||||
/** Send a FOLD command to the server to indicate that the player wants to fold */
|
||||
public void sendFold() {
|
||||
client.processCommand("FOLD GAME_ID=" + gameId);
|
||||
}
|
||||
|
||||
/** Send a BET command to the server to indicate that the player wants to bet */
|
||||
public void sendBet(int amount) {
|
||||
client.processCommand("BET GAME_ID=" + gameId + " AMOUNT=" + amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a RAISE command to the server to indicate that the player wants to raise
|
||||
*
|
||||
* @param amount The amount to raise to
|
||||
*/
|
||||
public void sendRaise(int amount) {
|
||||
client.processCommand("RAISE GAME_ID=" + gameId + " AMOUNT=" + amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the raw server response string into a structured GameState object.
|
||||
*
|
||||
* @param input The raw server response string containing the game state information.
|
||||
* @return A GameState object representing the current state of the game.
|
||||
*/
|
||||
private GameState parse(String input) {
|
||||
|
||||
if (input.startsWith("-ERR")) {
|
||||
throw new RuntimeException("Server returned Error:\n" + input);
|
||||
}
|
||||
|
||||
GameState state = new GameState();
|
||||
ParserContext ctx = new ParserContext(state);
|
||||
private GameState parseGameState(String input) {
|
||||
GameState s = new GameState();
|
||||
s.players = new ArrayList<>();
|
||||
s.communityCards = new ArrayList<>();
|
||||
|
||||
ParseState state = new ParseState();
|
||||
for (String raw : input.split("\n")) {
|
||||
String line = raw.trim();
|
||||
|
||||
if (shouldSkip(line)) {
|
||||
if (line.isEmpty() || line.startsWith("+OK")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handleGlobal(line, ctx)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handleSections(line, ctx)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handlePlayer(line, ctx)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handleCards(line, ctx)) {
|
||||
continue;
|
||||
if (!parseGlobalField(s, line)
|
||||
&& !parsePlayerStructure(s, state, line)
|
||||
&& !parseCardData(state, line)
|
||||
&& !parsePlayerData(state, line)) {
|
||||
LOG.fine(() -> "Ignored line: '" + line + "'");
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
return s;
|
||||
}
|
||||
|
||||
/** Holds the mutable parsing state while processing the server response. */
|
||||
private static class ParserContext {
|
||||
GameState state;
|
||||
Player currentPlayer;
|
||||
Card currentCard;
|
||||
|
||||
boolean inPlayers;
|
||||
boolean inPlayerCards;
|
||||
boolean inCommunityCards;
|
||||
|
||||
ParserContext(GameState state) {
|
||||
this.state = state;
|
||||
}
|
||||
private boolean parseGlobalField(GameState s, String line) {
|
||||
return switch (line) {
|
||||
case String l when l.startsWith("PHASE=") -> {
|
||||
s.phase = value(l);
|
||||
yield true;
|
||||
}
|
||||
case String l when l.startsWith("POT=") -> {
|
||||
s.pot = intVal(l);
|
||||
yield true;
|
||||
}
|
||||
case String l when l.startsWith("CURRENT_BET=") -> {
|
||||
s.currentBet = intVal(l);
|
||||
yield true;
|
||||
}
|
||||
case String l when l.startsWith("DEALER=") -> {
|
||||
s.dealer = intVal(l);
|
||||
yield true;
|
||||
}
|
||||
case String l when l.startsWith("ACTIVE_PLAYER=") -> {
|
||||
s.activePlayer = intVal(l);
|
||||
yield true;
|
||||
}
|
||||
case String l when l.startsWith("WINNER=") -> {
|
||||
s.winnerIndex = intVal(l);
|
||||
yield true;
|
||||
}
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a line should be ignored.
|
||||
*
|
||||
* @param line the current input line
|
||||
* @return true if the line is empty or a status message, false otherwise
|
||||
*/
|
||||
private boolean shouldSkip(String line) {
|
||||
return line.isEmpty() || line.startsWith("+OK");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses global game state properties (e.g., phase, pot, current bet).
|
||||
*
|
||||
* @param line the current input line
|
||||
* @param ctx the parser context containing the game state
|
||||
* @return true if the line was handled, false otherwise
|
||||
*/
|
||||
private boolean handleGlobal(String line, ParserContext ctx) {
|
||||
GameState state = ctx.state;
|
||||
|
||||
if (line.startsWith("PHASE=")) {
|
||||
state.phase = value(line);
|
||||
} else if (line.startsWith("POT=")) {
|
||||
state.pot = intVal(line);
|
||||
} else if (line.startsWith("CURRENT_BET=")) {
|
||||
state.currentBet = intVal(line);
|
||||
} else if (line.startsWith("DEALER=")) {
|
||||
state.dealer = intVal(line);
|
||||
} else if (line.startsWith("ACTIVE_PLAYER=")) {
|
||||
state.activePlayer = intVal(line);
|
||||
} else if (line.startsWith("WINNER=")) {
|
||||
state.winnerIndex = intVal(line);
|
||||
} else {
|
||||
return false;
|
||||
// throw new RuntimeException("Unknown global field: " + line);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes section markers such as PLAYERS, PLAYER, CARDS, and END. Updates the parser context
|
||||
* accordingly.
|
||||
*
|
||||
* @param line the current input line
|
||||
* @param ctx the parser context
|
||||
* @return true if the line was handled, false otherwise
|
||||
*/
|
||||
private boolean handleSections(String line, ParserContext ctx) {
|
||||
|
||||
if (line.equals("END")) {
|
||||
// ctx.inPlayers = false;
|
||||
ctx.inPlayerCards = false;
|
||||
ctx.inCommunityCards = false;
|
||||
ctx.currentPlayer = null;
|
||||
ctx.currentCard = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.equals("PLAYERS")) {
|
||||
ctx.inPlayers = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean parsePlayerStructure(GameState s, ParseState state, String line) {
|
||||
if (line.equals("PLAYER")) {
|
||||
ctx.currentPlayer = new Player();
|
||||
ctx.state.players.add(ctx.currentPlayer);
|
||||
state.currentPlayer = new Player();
|
||||
s.players.add(state.currentPlayer);
|
||||
state.currentCard = null;
|
||||
state.insidePlayer = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.equals("CARDS")) {
|
||||
if (ctx.inPlayers && ctx.currentPlayer != null) {
|
||||
ctx.inPlayerCards = true;
|
||||
ctx.inCommunityCards = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.equals("CARD")) {
|
||||
state.currentCard = new Card("", "");
|
||||
if (state.insidePlayer && state.currentPlayer != null) {
|
||||
state.currentPlayer.addCard(state.currentCard);
|
||||
} else {
|
||||
ctx.inCommunityCards = true;
|
||||
ctx.inPlayerCards = false;
|
||||
s.communityCards.add(state.currentCard);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.equals("END")) {
|
||||
if (state.currentCard != null) {
|
||||
state.currentCard = null;
|
||||
} else if (state.insidePlayer) {
|
||||
state.insidePlayer = false;
|
||||
state.currentPlayer = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean parseCardData(ParseState state, String line) {
|
||||
if (state.currentCard == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (line.startsWith("VALUE=")) {
|
||||
state.currentCard.setValue(value(line));
|
||||
return true;
|
||||
}
|
||||
if (line.startsWith("SUIT=")) {
|
||||
state.currentCard.setSuit(value(line));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean parsePlayerData(ParseState state, String line) {
|
||||
if (state.currentPlayer == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (line.startsWith("USERNAME=") || line.startsWith("NAME=")) {
|
||||
state.currentPlayer.setId(PlayerId.of(value(line)));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.startsWith("CHIPS=")) {
|
||||
state.currentPlayer.setChips(intVal(line));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.startsWith("BET=")) {
|
||||
state.currentPlayer.setBet(intVal(line));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.startsWith("STATE=")) {
|
||||
state.currentPlayer.setState(parseState(value(line)));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses properties of the current player (e.g., name, chips, bet, state).
|
||||
*
|
||||
* @param line the current input line
|
||||
* @param ctx the parser context
|
||||
* @return true if the line was handled, false otherwise
|
||||
*/
|
||||
private boolean handlePlayer(String line, ParserContext ctx) {
|
||||
Player p = ctx.currentPlayer;
|
||||
|
||||
if (p == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (line.startsWith("NAME=")) {
|
||||
p.setId(PlayerId.of(value(line)));
|
||||
} else if (line.startsWith("CHIPS=")) {
|
||||
p.setChips(intVal(line));
|
||||
} else if (line.startsWith("BET=")) {
|
||||
p.setBet(intVal(line));
|
||||
} else if (line.startsWith("STATE=")) {
|
||||
p.setState(parseState(value(line)));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
private static class ParseState {
|
||||
Player currentPlayer;
|
||||
Card currentCard;
|
||||
boolean insidePlayer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses card-related lines and assigns cards to the current player or the community cards.
|
||||
*
|
||||
* @param line the current input line
|
||||
* @param ctx the parser context
|
||||
* @return true if the line was handled, false otherwise
|
||||
*/
|
||||
private boolean handleCards(String line, ParserContext ctx) {
|
||||
|
||||
if (line.startsWith("CARD")) {
|
||||
ctx.currentCard = new Card("", "");
|
||||
|
||||
if (ctx.inPlayerCards && ctx.currentPlayer != null) {
|
||||
ctx.currentPlayer.addCard(ctx.currentCard);
|
||||
} else if (ctx.inCommunityCards) {
|
||||
ctx.state.communityCards.add(ctx.currentCard);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.startsWith("VALUE=") && ctx.currentCard != null) {
|
||||
ctx.currentCard.setValue(value(line));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.startsWith("SUIT=") && ctx.currentCard != null) {
|
||||
ctx.currentCard.setSuit(value(line));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to extract the value from a line in the format KEY=VALUE.
|
||||
*
|
||||
* @param line The input line from which to extract the value, expected to be in the format
|
||||
* KEY=VALUE.
|
||||
* @return The extracted value part of the input line, which is the substring after the first
|
||||
* '=' character.
|
||||
*/
|
||||
private String value(String line) {
|
||||
private static String value(String line) {
|
||||
return line.split("=", 2)[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to extract an integer value from a line in the format KEY=VALUE.
|
||||
*
|
||||
* @param line The input line from which to extract the integer value, expected to be in the
|
||||
* format KEY=VALUE where VALUE is an integer.
|
||||
* @return The extracted integer value from the input line, parsed from the substring after the
|
||||
* first '=' character.
|
||||
*/
|
||||
private int intVal(String line) {
|
||||
private static int intVal(String line) {
|
||||
return Integer.parseInt(value(line));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to parse a string representation of a player's state into the corresponding
|
||||
* PlayerState enum value.
|
||||
*
|
||||
* @param s The input string representing the player's state, expected to be one of ACTIVE,
|
||||
* FOLDED, or DEALER (case-insensitive).
|
||||
* @return The corresponding PlayerState enum value based on the input string. If the input does
|
||||
* not match any known state, it defaults to PlayerState.ACTIVE.
|
||||
*/
|
||||
private PlayerState parseState(String s) {
|
||||
return switch (s.toUpperCase()) {
|
||||
private static PlayerState parseState(String s) {
|
||||
if (s == null) {
|
||||
return PlayerState.ACTIVE;
|
||||
}
|
||||
|
||||
return switch (s.trim().toUpperCase()) {
|
||||
case "ACTIVE" -> PlayerState.ACTIVE;
|
||||
case "FOLDED" -> PlayerState.FOLDED;
|
||||
case "DEALER" -> PlayerState.DEALER;
|
||||
default -> PlayerState.ACTIVE;
|
||||
};
|
||||
}
|
||||
|
||||
private static String extractValue(String joined, String key) {
|
||||
if (joined == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (String raw : joined.split("\n")) {
|
||||
String line = raw.trim();
|
||||
if (line.startsWith(key + "=")) {
|
||||
return line.substring((key + "=").length()).replace("'", "");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -129,4 +130,80 @@ public class LobbyClient {
|
||||
}
|
||||
return new LoginResult(assigned, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the server for the list of available lobbies.
|
||||
*
|
||||
* @return list of LobbyInfo objects representing current lobbies
|
||||
*/
|
||||
public List<LobbyInfo> getLobbyList() {
|
||||
List<String> lines = client.processCommand("GET_LOBBY_LIST");
|
||||
|
||||
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||
|
||||
List<LobbyInfo> result = new ArrayList<>();
|
||||
|
||||
Integer currentId = null;
|
||||
String currentName = null;
|
||||
Integer currentPlayerCount = null;
|
||||
|
||||
for (RequestParameter p : params) {
|
||||
String key = p.key().toUpperCase();
|
||||
String val = p.value();
|
||||
switch (key) {
|
||||
case "ID":
|
||||
// If we were collecting a lobby, flush it
|
||||
if (currentId != null) {
|
||||
result.add(
|
||||
new LobbyInfo(
|
||||
currentId,
|
||||
currentName,
|
||||
currentPlayerCount == null ? 0 : currentPlayerCount));
|
||||
currentName = null;
|
||||
currentPlayerCount = null;
|
||||
}
|
||||
try {
|
||||
currentId = Integer.parseInt(val);
|
||||
} catch (NumberFormatException e) {
|
||||
currentId = null;
|
||||
}
|
||||
break;
|
||||
case "NAME":
|
||||
currentName = val;
|
||||
break;
|
||||
case "PLAYER_COUNT":
|
||||
try {
|
||||
currentPlayerCount = Integer.parseInt(val);
|
||||
} catch (NumberFormatException e) {
|
||||
currentPlayerCount = 0;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentId != null) {
|
||||
result.add(
|
||||
new LobbyInfo(
|
||||
currentId,
|
||||
currentName,
|
||||
currentPlayerCount == null ? 0 : currentPlayerCount));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Simple data holder for lobby metadata returned by the server. */
|
||||
public static final class LobbyInfo {
|
||||
public final int id;
|
||||
public final String name;
|
||||
public final int playerCount;
|
||||
|
||||
public LobbyInfo(int id, String name, int playerCount) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.playerCount = playerCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,14 @@ public class ChatBoxController {
|
||||
|
||||
private String ressource = "/ui-structure/components/chatui/chattab.fxml";
|
||||
|
||||
/**
|
||||
* Constructor for the ChatBoxController, initializes the necessary fields and data structures
|
||||
* for managing chat tabs and whisper chats.
|
||||
*
|
||||
* @param username The username of the current user.
|
||||
* @param chatController The ChatController instance responsible for handling chat logic and
|
||||
* communication with the server.
|
||||
*/
|
||||
public ChatBoxController(String username, ChatController chatController) {
|
||||
this.username = username;
|
||||
this.chatController = chatController;
|
||||
|
||||
+472
-46
@@ -1,20 +1,32 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.Card;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameState;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.Player;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.PlayerStatusController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.TaskbarController;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Logger;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
/**
|
||||
* Controller for the casino gaming area.
|
||||
@@ -43,12 +55,18 @@ public class CasinoGameController {
|
||||
@FXML private VBox player2;
|
||||
@FXML private VBox player3;
|
||||
@FXML private ImageView myDealerIcon;
|
||||
@FXML private AnchorPane chatContainer;
|
||||
|
||||
private GameService gameService;
|
||||
private PlayerId myPlayerId;
|
||||
@FXML private TaskbarController taskbarIncludeController;
|
||||
private TaskbarController taskbarController;
|
||||
private Image dealerImage;
|
||||
private javafx.animation.Timeline timeline;
|
||||
private boolean gameStarted = false;
|
||||
private java.util.List<String> lastCommunityKeys = java.util.List.of();
|
||||
private java.util.List<String> lastMyCardKeys = java.util.List.of();
|
||||
private int lastPot = Integer.MIN_VALUE;
|
||||
|
||||
private static final int TOTAL_SLOTS = 5;
|
||||
private static final int PLAYER_SLOTS = 2;
|
||||
@@ -118,12 +136,52 @@ public class CasinoGameController {
|
||||
private static final long CHIP_SCALE_DURATION_MS = 220;
|
||||
private static final long CHIP_DROP_DURATION_MS = 250;
|
||||
private static final long CHIP_STAGGER_DELAY_MULTIPLIER = 35L;
|
||||
private static final double CHAT_WIDTH = 400;
|
||||
private static final double CHAT_HEIGHT = 600;
|
||||
|
||||
private ChatController chatController;
|
||||
private String chatUsername;
|
||||
private ClientService chatClientService;
|
||||
private int chatLobbyId = -1;
|
||||
private boolean chatInitialized;
|
||||
|
||||
/** Standard constructor. Used by FXML. */
|
||||
public CasinoGameController() {
|
||||
// default constructor for FXML
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique key for a given card based on its suit and value.
|
||||
*
|
||||
* @param c The card for which to generate the key.
|
||||
* @return A string key representing the card, formatted as "suit:value".
|
||||
*/
|
||||
private String cardKey(Card c) {
|
||||
if (c == null) {
|
||||
return "null";
|
||||
}
|
||||
|
||||
String suit = (c.getSuit() == null) ? "" : c.getSuit().toLowerCase();
|
||||
String value = (c.getValue() == null) ? "" : c.getValue().toLowerCase();
|
||||
return suit + ":" + value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a list of unique keys for a list of cards.
|
||||
*
|
||||
* @param cards The list of cards for which to generate keys.
|
||||
* @param slots The number of slots to generate keys for.
|
||||
* @return A list of string keys representing the cards, with "null" for any empty slots.
|
||||
*/
|
||||
private java.util.List<String> cardKeys(java.util.List<Card> cards, int slots) {
|
||||
java.util.List<String> keys = new java.util.ArrayList<>(slots);
|
||||
for (int i = 0; i < slots; i++) {
|
||||
Card c = (cards != null && i < cards.size()) ? cards.get(i) : null;
|
||||
keys.add(cardKey(c));
|
||||
}
|
||||
return java.util.List.copyOf(keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the GameService for this controller. This method must be called before starting the UI to
|
||||
* ensure that the controller has access to the game logic and can update the interface
|
||||
@@ -133,6 +191,10 @@ public class CasinoGameController {
|
||||
*/
|
||||
public void setGameService(GameService gameService) {
|
||||
this.gameService = gameService;
|
||||
TaskbarController controller = resolveTaskbarController();
|
||||
if (controller != null && myPlayerId != null) {
|
||||
controller.setGameService(gameService, myPlayerId);
|
||||
}
|
||||
}
|
||||
|
||||
/** Set the PlayerId of the current player. */
|
||||
@@ -140,6 +202,11 @@ public class CasinoGameController {
|
||||
public void initialize() {
|
||||
LOGGER.info("INIT UI");
|
||||
|
||||
taskbarController = resolveTaskbarController();
|
||||
if (taskbarController != null && gameService != null && myPlayerId != null) {
|
||||
taskbarController.setGameService(gameService, myPlayerId);
|
||||
}
|
||||
|
||||
if (communityCardsBox == null) {
|
||||
LOGGER.warning("communityCardsBox is NULL");
|
||||
return;
|
||||
@@ -172,6 +239,71 @@ public class CasinoGameController {
|
||||
// empty display only (optional)
|
||||
renderCommunityCards(List.of());
|
||||
renderPlayerCards(List.of());
|
||||
initializeChatIfPossible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the context for the chat functionality by providing the username, ClientService, and
|
||||
* lobby ID.
|
||||
*
|
||||
* @param username The username of the player, used for chat identification. Must not be null or
|
||||
* blank.
|
||||
* @param clientService The ClientService instance used for communicating with the server. Must
|
||||
* not be null.
|
||||
* @param lobbyId The ID of the lobby associated with the chat, used to determine the chat
|
||||
* context. Must be a non-negative integer.
|
||||
*/
|
||||
public void setChatContext(String username, ClientService clientService, int lobbyId) {
|
||||
this.chatUsername = username;
|
||||
this.chatClientService = clientService;
|
||||
this.chatLobbyId = lobbyId;
|
||||
initializeChatIfPossible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the chat UI if all necessary information (username, ClientService, and lobby ID)
|
||||
* is available and the chat has not already been initialized.
|
||||
*/
|
||||
private void initializeChatIfPossible() {
|
||||
if (chatInitialized || chatClientService == null || chatUsername == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
chatController = new ChatController(chatUsername, chatClientService);
|
||||
|
||||
URL resource = getClass().getResource("/ui-structure/components/chatui/chatbox.fxml");
|
||||
FXMLLoader loader = new FXMLLoader(resource);
|
||||
loader.setController(chatController.getChatBoxController());
|
||||
|
||||
Parent root = loader.load();
|
||||
|
||||
Stage chatStage = new Stage();
|
||||
|
||||
chatStage.setTitle("Casono");
|
||||
|
||||
String iconPath = getClass().getResource("/images/logoinverted.png").toExternalForm();
|
||||
chatStage.getIcons().add(new Image(iconPath));
|
||||
|
||||
Scene scene = new Scene(root);
|
||||
chatStage.setScene(scene);
|
||||
|
||||
chatStage.setWidth(CHAT_WIDTH);
|
||||
chatStage.setHeight(CHAT_HEIGHT);
|
||||
|
||||
chatStage.setOnCloseRequest(event -> chatInitialized = false);
|
||||
|
||||
chatStage.show();
|
||||
|
||||
if (chatLobbyId >= 0) {
|
||||
chatController.setLobbyChat(chatLobbyId);
|
||||
}
|
||||
|
||||
chatInitialized = true;
|
||||
|
||||
} catch (IOException e) {
|
||||
LOGGER.warning("Could not initialize game chat UI: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Test method to demonstrate the UI functionality with sample data.
|
||||
@@ -298,6 +430,9 @@ public class CasinoGameController {
|
||||
if (timeline != null) {
|
||||
timeline.stop();
|
||||
}
|
||||
if (chatController != null) {
|
||||
chatController.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -305,22 +440,38 @@ public class CasinoGameController {
|
||||
*/
|
||||
private void updateUI() {
|
||||
|
||||
java.util.concurrent.CompletableFuture.supplyAsync(
|
||||
if (gameService == null) {
|
||||
LOGGER.warning("GameService is null, skipping update.");
|
||||
return;
|
||||
}
|
||||
|
||||
CompletableFuture.supplyAsync(
|
||||
() -> {
|
||||
try {
|
||||
return gameService.refresh();
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("GameService Error: " + e.getMessage());
|
||||
LOGGER.severe("refresh failed: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.thenAccept(
|
||||
s -> {
|
||||
if (s == null) {
|
||||
state -> {
|
||||
if (state == null) {
|
||||
LOGGER.info("No state received yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
javafx.application.Platform.runLater(() -> applyState(s));
|
||||
javafx.application.Platform.runLater(
|
||||
() -> {
|
||||
applyState(state);
|
||||
});
|
||||
})
|
||||
.exceptionally(
|
||||
ex -> {
|
||||
LOGGER.severe("UI update Error: " + ex.getMessage());
|
||||
ex.printStackTrace();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -332,20 +483,108 @@ public class CasinoGameController {
|
||||
*/
|
||||
private void applyState(GameState s) {
|
||||
|
||||
renderCommunityCards(s.communityCards);
|
||||
renderPot(s.pot);
|
||||
if (s == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
updatePlayers(s.players);
|
||||
updatePlayerCards(s);
|
||||
LOGGER.info(
|
||||
"applyState -> phase="
|
||||
+ s.phase
|
||||
+ " pot="
|
||||
+ s.pot
|
||||
+ " players="
|
||||
+ (s.players != null ? s.players.size() : 0));
|
||||
|
||||
List<Player> players = (s.players != null) ? s.players : List.of();
|
||||
List<Card> community = (s.communityCards != null) ? s.communityCards : List.of();
|
||||
List<Card> myCards = getMyCards(players);
|
||||
|
||||
java.util.List<String> newCommunityKeys = cardKeys(community, TOTAL_SLOTS);
|
||||
if (!newCommunityKeys.equals(lastCommunityKeys)) {
|
||||
lastCommunityKeys = newCommunityKeys;
|
||||
renderCommunityCards(community);
|
||||
}
|
||||
|
||||
java.util.List<String> newMyCardKeys = cardKeys(myCards, PLAYER_SLOTS);
|
||||
if (!newMyCardKeys.equals(lastMyCardKeys)) {
|
||||
lastMyCardKeys = newMyCardKeys;
|
||||
renderPlayerCards(myCards);
|
||||
}
|
||||
|
||||
if (s.pot != lastPot) {
|
||||
lastPot = s.pot;
|
||||
renderPot(s.pot);
|
||||
}
|
||||
|
||||
updatePlayers(players);
|
||||
syncWhisperTargetsFromPlayers(players);
|
||||
updateGameInfo(s);
|
||||
highlightDealer(s);
|
||||
updateTaskbar(s);
|
||||
|
||||
if (taskbarController != null) {
|
||||
taskbarController.update(s, myPlayerId);
|
||||
LOGGER.info("myPlayerId=" + myPlayerId);
|
||||
for (int i = 0; i < players.size(); i++) {
|
||||
Player p = players.get(i);
|
||||
LOGGER.info(
|
||||
"state.players["
|
||||
+ i
|
||||
+ "] id="
|
||||
+ (p != null ? p.getId() : null)
|
||||
+ " chips="
|
||||
+ (p != null ? p.getChips() : null)
|
||||
+ " bet="
|
||||
+ (p != null ? p.getBet() : null)
|
||||
+ " state="
|
||||
+ (p != null ? p.getState() : null));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize the whisper chat targets with the current list of players.
|
||||
*
|
||||
* @param players The list of players currently in the game, used to update the whisper chat
|
||||
* targets in the chat controller.
|
||||
*/
|
||||
private void syncWhisperTargetsFromPlayers(List<Player> players) {
|
||||
if (chatController == null || players == null || players.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<String> seen = new HashSet<>();
|
||||
for (Player player : players) {
|
||||
if (player == null) {
|
||||
continue;
|
||||
}
|
||||
String name = player.getName();
|
||||
if (name != null && !name.isBlank() && seen.add(name)) {
|
||||
chatController.addWhisperUser(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the hole cards of the current player based on their PlayerId and the list of players
|
||||
* in the game state.
|
||||
*
|
||||
* @param players The list of players currently in the game, used to find the player matching
|
||||
* myPlayerId and retrieve their hole cards.
|
||||
* @return A list of Card objects representing the current player's hole cards, or an empty list
|
||||
* if the player is not found or has no cards.
|
||||
*/
|
||||
private List<Card> getMyCards(List<Player> players) {
|
||||
|
||||
if (myPlayerId == null || players == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
for (Player p : players) {
|
||||
if (p.getId() != null && p.getId().equals(myPlayerId)) {
|
||||
return p.getCards() != null ? p.getCards() : List.of();
|
||||
}
|
||||
}
|
||||
|
||||
return List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's hole cards based on the active player in the game state.
|
||||
*
|
||||
@@ -393,37 +632,146 @@ public class CasinoGameController {
|
||||
* @param s The current game state.
|
||||
*/
|
||||
private void updateTaskbar(GameState s) {
|
||||
if (taskbarController != null) {
|
||||
taskbarController.update(s, myPlayerId);
|
||||
TaskbarController controller = resolveTaskbarController();
|
||||
if (controller != null) {
|
||||
if (gameService != null && myPlayerId != null) {
|
||||
controller.setGameService(gameService, myPlayerId);
|
||||
}
|
||||
controller.update(s, myPlayerId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player status components with the latest player information from the game state.
|
||||
* Update the opponent player slots based on the list of players in the game state. This method
|
||||
* identifies the opponents by comparing their PlayerIds with myPlayerId and updates the
|
||||
* corresponding PlayerStatusControllers for each opponent slot.
|
||||
*
|
||||
* @param p The list of players in the current game state.
|
||||
* @param players The list of players currently in the game, used to determine which players are
|
||||
* opponents and update their display in the UI accordingly. If the list is null or empty,
|
||||
* all opponent slots will be cleared.
|
||||
*/
|
||||
private void updatePlayers(List<Player> p) {
|
||||
|
||||
if (p == null) {
|
||||
private void updatePlayers(List<Player> players) {
|
||||
if (players == null || players.isEmpty()) {
|
||||
clearOpponentSlots();
|
||||
return;
|
||||
}
|
||||
|
||||
if (p.size() > PLAYER_INDEX_0) {
|
||||
player1Controller.setPlayer(p.get(PLAYER_INDEX_0));
|
||||
java.util.List<Player> opponents = buildOpponents(players);
|
||||
boolean meFound = opponents.size() != players.size();
|
||||
|
||||
LOGGER.info(
|
||||
"updatePlayers: total="
|
||||
+ players.size()
|
||||
+ " myPlayerId="
|
||||
+ myPlayerId
|
||||
+ " meFound="
|
||||
+ meFound
|
||||
+ " opponents="
|
||||
+ opponents.size());
|
||||
|
||||
setOpponent(player1Controller, opponents, 0);
|
||||
setOpponent(player2Controller, opponents, 1);
|
||||
setOpponent(player3Controller, opponents, 2);
|
||||
|
||||
safeRefresh(player1Controller);
|
||||
safeRefresh(player2Controller);
|
||||
safeRefresh(player3Controller);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a list of opponent players by filtering out the current player (identified by
|
||||
* myPlayerId) from the provided list of players. If the current player is not found in the
|
||||
* list, it assumes all players are opponents and returns the original list.
|
||||
*
|
||||
* @param players The list of players to filter, which may include the current player and their
|
||||
* opponents. This list is typically obtained from the game state and may be null or empty,
|
||||
* in which case an empty list of opponents will be returned.
|
||||
* @return A list of Player objects representing the opponents, excluding the current player if
|
||||
* found. If the current player is not found, the original list of players is returned as
|
||||
* opponents.
|
||||
*/
|
||||
private java.util.List<Player> buildOpponents(List<Player> players) {
|
||||
java.util.List<Player> opponents = new java.util.ArrayList<>();
|
||||
boolean meFound = false;
|
||||
|
||||
for (Player p : players) {
|
||||
if (p == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PlayerId pid = p.getId();
|
||||
boolean isMe = myPlayerId != null && pid != null && pid.equals(myPlayerId);
|
||||
|
||||
if (isMe) {
|
||||
meFound = true;
|
||||
} else {
|
||||
opponents.add(p);
|
||||
}
|
||||
}
|
||||
|
||||
if (p.size() > PLAYER_INDEX_1) {
|
||||
player2Controller.setPlayer(p.get(PLAYER_INDEX_1));
|
||||
if (!meFound) {
|
||||
opponents.clear();
|
||||
opponents.addAll(players);
|
||||
}
|
||||
|
||||
if (p.size() > PLAYER_INDEX_2) {
|
||||
player3Controller.setPlayer(p.get(PLAYER_INDEX_2));
|
||||
return opponents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the opponent player information in the specified PlayerStatusController based on the
|
||||
* provided list of opponents and the index of the opponent to display.
|
||||
*
|
||||
* @param slot The PlayerStatusController instance representing the opponent slot in the UI,
|
||||
* which will be updated with the player information if available.
|
||||
* @param list The list of opponent players to choose from, which is typically built by
|
||||
* filtering the game state's player list to exclude the current player.
|
||||
* @param index The index of the opponent in the list to display in the specified slot.
|
||||
*/
|
||||
private void setOpponent(PlayerStatusController slot, java.util.List<Player> list, int index) {
|
||||
if (slot == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
player1Controller.refresh();
|
||||
player2Controller.refresh();
|
||||
player3Controller.refresh();
|
||||
slot.setPlayer(index < list.size() ? list.get(index) : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely refresh the PlayerStatusController by calling its refresh method, while catching and
|
||||
* ignoring any exceptions that may occur during the refresh process.
|
||||
*
|
||||
* @param c The PlayerStatusController instance to refresh, which may be null.
|
||||
*/
|
||||
private void safeRefresh(PlayerStatusController c) {
|
||||
if (c == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
c.refresh();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all opponent slots in the UI by setting their player information to null and refreshing
|
||||
* their display.
|
||||
*/
|
||||
private void clearOpponentSlots() {
|
||||
if (player1Controller != null) {
|
||||
player1Controller.setPlayer(null);
|
||||
}
|
||||
|
||||
if (player2Controller != null) {
|
||||
player2Controller.setPlayer(null);
|
||||
}
|
||||
|
||||
if (player3Controller != null) {
|
||||
player3Controller.setPlayer(null);
|
||||
}
|
||||
|
||||
safeRefresh(player1Controller);
|
||||
safeRefresh(player2Controller);
|
||||
safeRefresh(player3Controller);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -432,30 +780,69 @@ public class CasinoGameController {
|
||||
* @param s The current game state containing the dealer index.
|
||||
*/
|
||||
private void highlightDealer(GameState s) {
|
||||
if (player1Controller != null) {
|
||||
player1Controller.setDealer(false);
|
||||
}
|
||||
|
||||
player1Controller.setDealer(false);
|
||||
player2Controller.setDealer(false);
|
||||
player3Controller.setDealer(false);
|
||||
if (player2Controller != null) {
|
||||
player2Controller.setDealer(false);
|
||||
}
|
||||
|
||||
if (player3Controller != null) {
|
||||
player3Controller.setDealer(false);
|
||||
}
|
||||
|
||||
myDealerIcon.setVisible(false);
|
||||
|
||||
int dealer = s.dealer;
|
||||
if (s == null || s.players == null || s.players.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (s.dealer < 0 || s.dealer >= s.players.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dealer == DEALER_PLAYER_1) {
|
||||
Player dealerPlayer = s.players.get(s.dealer);
|
||||
if (dealerPlayer == null || dealerPlayer.getId() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (myPlayerId != null && myPlayerId.equals(dealerPlayer.getId())) {
|
||||
myDealerIcon.setVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
java.util.List<Player> opponents = buildOpponents(s.players);
|
||||
if (opponents.size() > 0
|
||||
&& samePlayer(opponents.get(0), dealerPlayer)
|
||||
&& player1Controller != null) {
|
||||
player1Controller.setDealer(true);
|
||||
}
|
||||
|
||||
if (dealer == DEALER_PLAYER_2) {
|
||||
if (opponents.size() > 1
|
||||
&& samePlayer(opponents.get(1), dealerPlayer)
|
||||
&& player2Controller != null) {
|
||||
player2Controller.setDealer(true);
|
||||
}
|
||||
|
||||
if (dealer == DEALER_PLAYER_3) {
|
||||
if (opponents.size() > 2
|
||||
&& samePlayer(opponents.get(2), dealerPlayer)
|
||||
&& player3Controller != null) {
|
||||
player3Controller.setDealer(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (dealer == DEALER_MYSELF) {
|
||||
myDealerIcon.setVisible(true);
|
||||
/**
|
||||
* Compare two Player objects to determine if they represent the same player based on their
|
||||
* PlayerIds. This method checks for null values and compares the IDs for equality.
|
||||
*
|
||||
* @param a The first Player object to compare, which may be null.
|
||||
* @param b The second Player object to compare, which may be null.
|
||||
* @return true if both Player objects are non-null and have the same non-null PlayerId, false
|
||||
* otherwise.
|
||||
*/
|
||||
private boolean samePlayer(Player a, Player b) {
|
||||
if (a == null || b == null || a.getId() == null || b.getId() == null) {
|
||||
return false;
|
||||
}
|
||||
return a.getId().equals(b.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -758,23 +1145,29 @@ public class CasinoGameController {
|
||||
return BACKSIDE;
|
||||
}
|
||||
|
||||
String rawSuit = card.getSuit();
|
||||
String rawValue = card.getValue();
|
||||
if (rawSuit == null || rawValue == null) {
|
||||
return BACKSIDE;
|
||||
}
|
||||
|
||||
String suit =
|
||||
switch (card.getSuit().toLowerCase()) {
|
||||
case "hearts" -> "heart";
|
||||
case "diamonds" -> "diamond";
|
||||
case "clubs", "cross" -> "cross";
|
||||
case "spades", "pik" -> "pik";
|
||||
default -> card.getSuit().toLowerCase();
|
||||
switch (rawSuit.trim().toLowerCase()) {
|
||||
case "h", "hearts", "heart" -> "heart";
|
||||
case "d", "diamonds", "diamond" -> "diamond";
|
||||
case "c", "clubs", "club", "cross" -> "cross";
|
||||
case "s", "spades", "spade", "pik" -> "pik";
|
||||
default -> rawSuit.trim().toLowerCase();
|
||||
};
|
||||
|
||||
String value =
|
||||
switch (card.getValue().toLowerCase()) {
|
||||
switch (rawValue.trim().toLowerCase()) {
|
||||
case "a", "ace" -> "ace";
|
||||
case "k", "king" -> "king";
|
||||
case "q", "queen" -> "queen";
|
||||
case "j", "jack" -> "jack";
|
||||
case "10", "t" -> "10";
|
||||
default -> card.getValue().toLowerCase();
|
||||
default -> rawValue.trim().toLowerCase();
|
||||
};
|
||||
|
||||
String path = "/images/card-" + suit + "-" + value + "-3.png";
|
||||
@@ -913,4 +1306,37 @@ public class CasinoGameController {
|
||||
|
||||
st.play();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the PlayerId of the current player, which is used to identify the player's hole cards and
|
||||
* update the taskbar with the correct player information.
|
||||
*
|
||||
* @param id The PlayerId of the current player, which should match the PlayerId in the game
|
||||
* state for the player's own information to be displayed correctly.
|
||||
*/
|
||||
public void setMyPlayerId(PlayerId id) {
|
||||
this.myPlayerId = id;
|
||||
TaskbarController controller = resolveTaskbarController();
|
||||
if (controller != null && gameService != null && myPlayerId != null) {
|
||||
controller.setGameService(gameService, myPlayerId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the TaskbarController instance to be used for updating the taskbar with game
|
||||
* information.
|
||||
*
|
||||
* @return The TaskbarController instance to use for taskbar updates, which may be the directly
|
||||
* injected taskbarController or the one obtained from the taskbarIncludeController.
|
||||
*/
|
||||
private TaskbarController resolveTaskbarController() {
|
||||
if (taskbarController != null) {
|
||||
return taskbarController;
|
||||
}
|
||||
if (taskbarIncludeController != null) {
|
||||
taskbarController = taskbarIncludeController;
|
||||
return taskbarController;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.GameClient;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Logger;
|
||||
import javafx.application.Application;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
@@ -12,50 +18,144 @@ import javafx.stage.Stage;
|
||||
*
|
||||
* <p>Starts the JavaFX application, loads the graphical user interface from the FXML file, and
|
||||
* initializes the main stage for the game.
|
||||
*
|
||||
* <p>Tasks: - Loads the FXML interface "/ui-structure/Casinogameui.fxml". - Loads the application
|
||||
* icon from "/images/logoinverted.png". - Starts the application in full-screen mode.
|
||||
*/
|
||||
public class CasinoGameUI extends Application {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(CasinoGameUI.class.getName());
|
||||
|
||||
private static ClientService clientService;
|
||||
|
||||
private static String username;
|
||||
private static int lobbyId = -1;
|
||||
|
||||
private static final int DEFAULT_WIDTH = 1200;
|
||||
private static final int DEFAULT_HEIGHT = 800;
|
||||
private static final int GUEST_ID_LENGTH = 8;
|
||||
|
||||
/** default constructor */
|
||||
/** Default constructor for the CasinoGameUI application. */
|
||||
public CasinoGameUI() {
|
||||
// default no-arg constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ClientService instance to be used by the application.
|
||||
*
|
||||
* @param clientService the ClientService instance to set
|
||||
*/
|
||||
public static void setClientService(ClientService clientService) {
|
||||
CasinoGameUI.clientService = clientService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the main stage of the application.
|
||||
* Sets the username to be used by the application.
|
||||
*
|
||||
* @param stage The main stage provided by the system.
|
||||
* @throws IOException If the FXML file or resources cannot be loaded.
|
||||
* @param username the username to set
|
||||
*/
|
||||
public static void setUsername(String username) {
|
||||
CasinoGameUI.username = username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the lobby ID to be used by the application.
|
||||
*
|
||||
* @param lobbyId the lobby ID to set
|
||||
*/
|
||||
public static void setLobbyId(int lobbyId) {
|
||||
CasinoGameUI.lobbyId = lobbyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The main entry point for the JavaFX application. This method is called after the application
|
||||
* is
|
||||
*
|
||||
* @param stage the primary stage for this application, onto which the application scene can be
|
||||
* set. Applications may create other stages, if needed, but they will not be primary
|
||||
* stages.
|
||||
* @throws IOException
|
||||
*/
|
||||
@Override
|
||||
public void start(Stage stage) throws IOException {
|
||||
|
||||
if (clientService == null) {
|
||||
clientService =
|
||||
ch.unibas.dmi.dbis.cs108.casono.client.ClientApp.getSharedClientService();
|
||||
}
|
||||
if (clientService == null) {
|
||||
throw new IllegalStateException(
|
||||
"CasinoGameUI: clientService is null. "
|
||||
+ "Call CasinoGameUI.setClientService(...)"
|
||||
+ " or start via ClientApp with a shared connection.");
|
||||
}
|
||||
|
||||
String effectiveUsername = normalize(username);
|
||||
|
||||
if (effectiveUsername == null) {
|
||||
effectiveUsername =
|
||||
normalize(ch.unibas.dmi.dbis.cs108.casono.client.ClientApp.getSharedUsername());
|
||||
}
|
||||
|
||||
if (effectiveUsername == null) {
|
||||
effectiveUsername =
|
||||
"Guest-" + UUID.randomUUID().toString().substring(0, GUEST_ID_LENGTH);
|
||||
}
|
||||
|
||||
LOG.info(
|
||||
"CasinoGameUI starting: effectiveUsername='"
|
||||
+ effectiveUsername
|
||||
+ "', injectedUsername='"
|
||||
+ username
|
||||
+ "', sharedUsername='"
|
||||
+ ch.unibas.dmi.dbis.cs108.casono.client.ClientApp.getSharedUsername()
|
||||
+ "', hasClientService="
|
||||
+ (clientService != null));
|
||||
|
||||
FXMLLoader fxmlLoader =
|
||||
new FXMLLoader(CasinoGameUI.class.getResource("/ui-structure/Casinogameui.fxml"));
|
||||
Scene scene = new Scene(fxmlLoader.load(), DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
Parent root = fxmlLoader.load();
|
||||
CasinoGameController controller = fxmlLoader.getController();
|
||||
|
||||
int gameId = 1; // TODO echte gameId einsetzen
|
||||
GameClient gameClient = new GameClient(clientService, gameId);
|
||||
GameService gameService = new GameService(gameClient);
|
||||
controller.setGameService(gameService);
|
||||
|
||||
controller.setMyPlayerId(PlayerId.of(effectiveUsername));
|
||||
controller.setChatContext(effectiveUsername, clientService, lobbyId);
|
||||
|
||||
Scene scene = new Scene(root, DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
stage.setTitle("Casono");
|
||||
|
||||
String iconPath = getClass().getResource("/images/logoinverted.png").toExternalForm();
|
||||
stage.getIcons().add(new javafx.scene.image.Image(iconPath));
|
||||
|
||||
stage.setScene(scene);
|
||||
stage.setFullScreen(true);
|
||||
stage.setOnHidden(e -> controller.stop());
|
||||
stage.show();
|
||||
|
||||
controller.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starting point of the application.
|
||||
* Normalizes a string by trimming whitespace and converting blank strings to null.
|
||||
*
|
||||
* @param args Command line arguments.
|
||||
* @param s the string to normalize
|
||||
* @return the normalized string, or null if the input is null or blank
|
||||
*/
|
||||
private static String normalize(String s) {
|
||||
if (s == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String t = s.trim();
|
||||
return t.isBlank() ? null : t;
|
||||
}
|
||||
|
||||
/**
|
||||
* The main method serves as the entry point for the application. It launches the JavaFX
|
||||
* application.
|
||||
*
|
||||
* @param args command line arguments (not used)
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
launch();
|
||||
|
||||
+31
-15
@@ -37,6 +37,7 @@ public class TaskbarController {
|
||||
|
||||
private GameService gameService;
|
||||
private PlayerId myPlayerId;
|
||||
private GameState lastState;
|
||||
private double xOffset = 0;
|
||||
private double yOffset = 0;
|
||||
private static final double TASKBAR_SCALE = 0.95;
|
||||
@@ -225,6 +226,8 @@ public class TaskbarController {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastState = state;
|
||||
|
||||
Player me =
|
||||
state.players.stream()
|
||||
.filter(p -> myPlayerId.equals(p.getId()))
|
||||
@@ -341,22 +344,35 @@ public class TaskbarController {
|
||||
@FXML
|
||||
private void onInputPlayerRaise() {
|
||||
|
||||
String input = taskbarInput.getText();
|
||||
|
||||
try {
|
||||
int amount = Integer.parseInt(input.trim());
|
||||
|
||||
gameService.raise(amount);
|
||||
|
||||
LOGGER.info("Player RAISE {}", amount);
|
||||
|
||||
taskbarInput.clear();
|
||||
|
||||
refreshGame();
|
||||
|
||||
} catch (NumberFormatException e) {
|
||||
LOGGER.error("Invalid raise amount");
|
||||
if (gameService == null) {
|
||||
LOGGER.error("GameService not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
GameState state = lastState;
|
||||
if (state == null) {
|
||||
try {
|
||||
state = gameService.refresh();
|
||||
lastState = state;
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to refresh game state for raise: {}", e.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (state == null || state.currentBet <= 0) {
|
||||
LOGGER.warn(
|
||||
"Raise not possible: current bet is {}",
|
||||
state != null ? state.currentBet : null);
|
||||
return;
|
||||
}
|
||||
|
||||
int raiseAmount = state.currentBet;
|
||||
gameService.raise(raiseAmount);
|
||||
LOGGER.info("Player RAISE by {} (target: double table bet)", raiseAmount);
|
||||
|
||||
taskbarInput.clear();
|
||||
refreshGame();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+76
@@ -1,10 +1,15 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.Button;
|
||||
@@ -31,11 +36,13 @@ public class CasinomainuiController {
|
||||
@FXML private VBox casinoTable;
|
||||
@FXML private TextField usernameField;
|
||||
@FXML private Button loginButton;
|
||||
@FXML private AnchorPane chatContainer;
|
||||
|
||||
private LobbyButtonTranslationManager translationManager;
|
||||
private LobbyButtonGridManager gridManager;
|
||||
private int nextButtonId = 1;
|
||||
private LobbyClient lobbyClient;
|
||||
private ChatController chatController;
|
||||
|
||||
/** Default constructor used by FXMLLoader. */
|
||||
public CasinomainuiController() {
|
||||
@@ -75,9 +82,75 @@ public class CasinomainuiController {
|
||||
// LobbyClient will use the provided ClientService; in offline mode calls will
|
||||
// fail with RuntimeException
|
||||
lobbyClient = new LobbyClient(clientService);
|
||||
// Fetch existing lobbies from server on startup so newly-created lobbies
|
||||
// by other clients are immediately visible.
|
||||
try {
|
||||
if (!lobbyClient.getClientService().isOffline()) {
|
||||
var lobbies = lobbyClient.getLobbyList();
|
||||
int bid = nextButtonId;
|
||||
for (var li : lobbies) {
|
||||
try {
|
||||
translationManager.addLobbyButton(bid++, li.id);
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Could not add lobby button: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
nextButtonId = bid;
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.warn("Failed to fetch lobby list at startup: {}", e.getMessage());
|
||||
}
|
||||
casinoTable.getChildren().clear();
|
||||
casinoTable.getChildren().add(gridManager.getGridPane());
|
||||
gridManager.renderLobbyButtons();
|
||||
|
||||
initializeChat(clientService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the chat UI if a valid ClientService is available. If the client is offline or
|
||||
* the
|
||||
*
|
||||
* @param clientService
|
||||
*/
|
||||
private void initializeChat(ClientService clientService) {
|
||||
if (clientService == null || clientService.isOffline() || chatContainer == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String username = resolveChatUsername();
|
||||
chatController = new ChatController(username, clientService);
|
||||
URL resource = getClass().getResource("/ui-structure/components/chatui/chatbox.fxml");
|
||||
FXMLLoader loader = new FXMLLoader(resource);
|
||||
loader.setController(chatController.getChatBoxController());
|
||||
Node chatNode = loader.load();
|
||||
chatContainer.getChildren().setAll(chatNode);
|
||||
AnchorPane.setTopAnchor(chatNode, 0.0);
|
||||
AnchorPane.setBottomAnchor(chatNode, 0.0);
|
||||
AnchorPane.setLeftAnchor(chatNode, 0.0);
|
||||
AnchorPane.setRightAnchor(chatNode, 0.0);
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("Could not initialize lobby chat UI: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the username to be used in the chat. It first checks for a shared username set at
|
||||
* the
|
||||
*
|
||||
* @return The resolved username, or "Guest" if no valid username is found.
|
||||
*/
|
||||
private String resolveChatUsername() {
|
||||
String shared = ClientApp.getSharedUsername();
|
||||
if (shared != null && !shared.isBlank()) {
|
||||
return shared.trim();
|
||||
}
|
||||
if (usernameField != null
|
||||
&& usernameField.getText() != null
|
||||
&& !usernameField.getText().isBlank()) {
|
||||
return usernameField.getText().trim();
|
||||
}
|
||||
return "Guest";
|
||||
}
|
||||
|
||||
/** Handles the login button action. Validates input and calls LobbyClient.login(). */
|
||||
@@ -121,6 +194,9 @@ public class CasinomainuiController {
|
||||
/** Handles the exit button action to close the application. */
|
||||
@FXML
|
||||
public void handleexitbutton() {
|
||||
if (chatController != null) {
|
||||
chatController.shutdown();
|
||||
}
|
||||
Platform.exit();
|
||||
}
|
||||
|
||||
|
||||
+225
-45
@@ -28,6 +28,8 @@ public class LobbyButtonGridManager {
|
||||
private static final int REFRESH_INTERVAL_SECONDS = 5;
|
||||
private static final int INITIAL_DELAY_SECONDS = 5;
|
||||
private static final int COLS = 4;
|
||||
private static final int MAX_BUTTONS = 8;
|
||||
private static final int GUEST_ID_LENGTH = 8;
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
|
||||
|
||||
@@ -66,40 +68,106 @@ public class LobbyButtonGridManager {
|
||||
|
||||
// Subscribe to server-initiated events so closed lobbies are removed
|
||||
// immediately
|
||||
clientService.addEventListener(
|
||||
lines -> {
|
||||
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||
String evt = null;
|
||||
String lidStr = null;
|
||||
for (RequestParameter p : params) {
|
||||
if ("EVENT".equalsIgnoreCase(p.key())) {
|
||||
evt = p.value();
|
||||
} else if ("LOBBY_ID".equalsIgnoreCase(p.key())) {
|
||||
lidStr = p.value();
|
||||
}
|
||||
}
|
||||
if (evt != null && "LOBBY_CLOSED".equalsIgnoreCase(evt) && lidStr != null) {
|
||||
int lid;
|
||||
try {
|
||||
lid = Integer.parseInt(lidStr);
|
||||
} catch (NumberFormatException ex) {
|
||||
return;
|
||||
}
|
||||
// find button id(s) for this lobby and remove mapping
|
||||
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||
Integer toRemove = null;
|
||||
for (Map.Entry<Integer, Integer> e : mapping.entrySet()) {
|
||||
if (e.getValue() != null && e.getValue().intValue() == lid) {
|
||||
toRemove = e.getKey();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (toRemove != null) {
|
||||
translationManager.removeLobbyButton(toRemove);
|
||||
javafx.application.Platform.runLater(this::renderLobbyButtons);
|
||||
}
|
||||
}
|
||||
});
|
||||
clientService.addEventListener(this::handleClientServiceEvent);
|
||||
}
|
||||
|
||||
private static record EventInfo(String event, String lobbyId) {}
|
||||
|
||||
private EventInfo extractEventInfo(List<RequestParameter> params) {
|
||||
String evt = null;
|
||||
String lidStr = null;
|
||||
for (RequestParameter p : params) {
|
||||
if ("EVENT".equalsIgnoreCase(p.key())) {
|
||||
evt = p.value();
|
||||
} else if ("LOBBY_ID".equalsIgnoreCase(p.key())) {
|
||||
lidStr = p.value();
|
||||
}
|
||||
}
|
||||
return new EventInfo(evt, lidStr);
|
||||
}
|
||||
|
||||
private Integer tryParseInt(String s) {
|
||||
try {
|
||||
return Integer.parseInt(s);
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleLobbyClosed(int lid) {
|
||||
LOGGER.info(
|
||||
"LOBBY_CLOSED event for lobby {} — mapping before: {}",
|
||||
lid,
|
||||
translationManager.getButtonIdToLobbyId());
|
||||
// find button id(s) for this lobby and remove mapping
|
||||
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||
Integer toRemove = null;
|
||||
for (Map.Entry<Integer, Integer> e : mapping.entrySet()) {
|
||||
if (e.getValue() != null && e.getValue().intValue() == lid) {
|
||||
toRemove = e.getKey();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (toRemove != null) {
|
||||
LOGGER.info(
|
||||
"Removing mapping for button {} -> lobby {} (LOBBY_CLOSED event)",
|
||||
toRemove,
|
||||
lid);
|
||||
translationManager.removeLobbyButton(toRemove);
|
||||
LOGGER.debug("Mapping after removal: {}", translationManager.getButtonIdToLobbyId());
|
||||
javafx.application.Platform.runLater(this::renderLobbyButtons);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleLobbyCreated(int lid) {
|
||||
LOGGER.info(
|
||||
"LOBBY_CREATED event for lobby {} — mapping before: {}",
|
||||
lid,
|
||||
translationManager.getButtonIdToLobbyId());
|
||||
|
||||
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||
|
||||
// If this lobby is already known, nothing to do
|
||||
if (mapping.containsValue(lid)) {
|
||||
LOGGER.debug("Lobby {} already mapped, skipping", lid);
|
||||
return;
|
||||
}
|
||||
|
||||
// find next free button id (1..MAX_BUTTONS)
|
||||
for (int candidate = 1; candidate <= MAX_BUTTONS; candidate++) {
|
||||
if (!mapping.containsKey(candidate)) {
|
||||
try {
|
||||
translationManager.addLobbyButton(candidate, lid);
|
||||
LOGGER.info("Mapped new lobby {} to button {}", lid, candidate);
|
||||
LOGGER.debug(
|
||||
"Mapping after add: {}", translationManager.getButtonIdToLobbyId());
|
||||
javafx.application.Platform.runLater(this::renderLobbyButtons);
|
||||
break;
|
||||
} catch (Exception ex) {
|
||||
// grid full? try next candidate
|
||||
LOGGER.debug("Could not map created lobby {}: {}", lid, ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleClientServiceEvent(List<String> lines) {
|
||||
EventInfo info = extractEventInfo(ClientService.convertToRequestParameters(lines));
|
||||
|
||||
if (info.event() == null || info.lobbyId() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Integer lid = tryParseInt(info.lobbyId());
|
||||
if (lid == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("LOBBY_CLOSED".equalsIgnoreCase(info.event())) {
|
||||
handleLobbyClosed(lid);
|
||||
} else if ("LOBBY_CREATED".equalsIgnoreCase(info.event())) {
|
||||
handleLobbyCreated(lid);
|
||||
}
|
||||
}
|
||||
|
||||
private void startPeriodicRefresh(long initialDelay, long period) {
|
||||
@@ -110,8 +178,29 @@ public class LobbyButtonGridManager {
|
||||
private void refreshMappings() {
|
||||
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||
|
||||
// Always attempt to sync with server-side lobby list so clients that start
|
||||
// after a lobby was created will discover it. This keeps the local mapping
|
||||
// in sync with the server without requiring a restart.
|
||||
List<LobbyClient.LobbyInfo> serverLobbies = null;
|
||||
java.util.Set<Integer> serverIds = new java.util.HashSet<>();
|
||||
try {
|
||||
serverLobbies = lobbyClient.getLobbyList();
|
||||
if (serverLobbies != null) {
|
||||
for (var li : serverLobbies) {
|
||||
serverIds.add(li.id);
|
||||
}
|
||||
}
|
||||
reconcileWithServerLobbies(serverLobbies);
|
||||
} catch (Exception ex) {
|
||||
LOGGER.debug("Could not fetch lobby list: {}", ex.getMessage());
|
||||
}
|
||||
|
||||
if (mapping.isEmpty()) {
|
||||
return;
|
||||
// mapping may still be empty after reconciliation
|
||||
if (translationManager.getButtonIdToLobbyId().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
mapping = translationManager.getButtonIdToLobbyId();
|
||||
}
|
||||
|
||||
List<Map.Entry<Integer, Integer>> entries = new ArrayList<>(mapping.entrySet());
|
||||
@@ -136,15 +225,88 @@ public class LobbyButtonGridManager {
|
||||
executor)
|
||||
.thenAccept(
|
||||
status -> {
|
||||
// Do not remove a mapping just because the status fetch
|
||||
// temporarily returned null. Only remove mappings when
|
||||
// the server no longer lists the lobby (reconcileWithServerLobbies
|
||||
// handles removals based on authoritative server list).
|
||||
if (status == null) {
|
||||
translationManager.removeLobbyButton(buttonId);
|
||||
|
||||
javafx.application.Platform.runLater(this::renderLobbyButtons);
|
||||
if (!serverIds.contains(lobbyId)) {
|
||||
translationManager.removeLobbyButton(buttonId);
|
||||
javafx.application.Platform.runLater(
|
||||
this::renderLobbyButtons);
|
||||
} else {
|
||||
// treat as created/running later; keep mapping
|
||||
LOGGER.debug(
|
||||
"Missing status for lobby {} - keeping mapping",
|
||||
lobbyId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void reconcileWithServerLobbies(List<LobbyClient.LobbyInfo> serverLobbies) {
|
||||
if (serverLobbies == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||
|
||||
// Build set of lobby ids returned by server
|
||||
java.util.Set<Integer> serverIds = new java.util.HashSet<>();
|
||||
for (var li : serverLobbies) {
|
||||
serverIds.add(li.id);
|
||||
}
|
||||
|
||||
LOGGER.debug("Reconciling mappings. serverIds={} mapping={}", serverIds, mapping);
|
||||
|
||||
// Remove mappings for lobbies that no longer exist
|
||||
java.util.List<Integer> toRemove = new java.util.ArrayList<>();
|
||||
for (var e : mapping.entrySet()) {
|
||||
Integer lid = e.getValue();
|
||||
if (lid == null) {
|
||||
continue;
|
||||
}
|
||||
if (!serverIds.contains(lid)) {
|
||||
toRemove.add(e.getKey());
|
||||
}
|
||||
}
|
||||
for (Integer bid : toRemove) {
|
||||
LOGGER.info(
|
||||
"Reconciling: removing mapping for button {} (server no longer lists lobby)",
|
||||
bid);
|
||||
translationManager.removeLobbyButton(bid);
|
||||
}
|
||||
|
||||
// Add server lobbies that are not yet mapped
|
||||
for (var li : serverLobbies) {
|
||||
if (mapping.containsValue(li.id)) {
|
||||
continue;
|
||||
}
|
||||
// find next free button id (1..MAX_BUTTONS)
|
||||
for (int candidate = 1; candidate <= MAX_BUTTONS; candidate++) {
|
||||
if (!mapping.containsKey(candidate)) {
|
||||
try {
|
||||
translationManager.addLobbyButton(candidate, li.id);
|
||||
LOGGER.info(
|
||||
"Reconciling: added mapping button {} -> lobby {}",
|
||||
candidate,
|
||||
li.id);
|
||||
break;
|
||||
} catch (Exception ex) {
|
||||
// grid full? try next candidate
|
||||
LOGGER.debug(
|
||||
"Could not add mapping for lobby {}: {}", li.id, ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!toRemove.isEmpty() || !serverLobbies.isEmpty()) {
|
||||
javafx.application.Platform.runLater(this::renderLobbyButtons);
|
||||
}
|
||||
}
|
||||
|
||||
public void renderLobbyButtons() {
|
||||
gridPane.getChildren().clear();
|
||||
|
||||
@@ -216,9 +378,8 @@ public class LobbyButtonGridManager {
|
||||
LobbyStatus status = parseLobbyStatus(statusStr);
|
||||
|
||||
String path =
|
||||
getImagePathForButton(
|
||||
buttonId,
|
||||
status == null ? LobbyStatus.CREATED : status);
|
||||
getImagePathForLobby(
|
||||
lobbyId, status == null ? LobbyStatus.CREATED : status);
|
||||
|
||||
Image img = safeLoadImage(path);
|
||||
|
||||
@@ -253,11 +414,11 @@ public class LobbyButtonGridManager {
|
||||
}
|
||||
}
|
||||
|
||||
private String getImagePathForButton(int buttonId, LobbyStatus status) {
|
||||
private String getImagePathForLobby(int lobbyId, LobbyStatus status) {
|
||||
|
||||
String statusStr = status == LobbyStatus.CREATED ? "created" : "running";
|
||||
|
||||
return String.format(BUTTON_IMAGE_TEMPLATE, buttonId, statusStr);
|
||||
return String.format(BUTTON_IMAGE_TEMPLATE, lobbyId, statusStr);
|
||||
}
|
||||
|
||||
private Image safeLoadImage(String path) {
|
||||
@@ -315,7 +476,7 @@ public class LobbyButtonGridManager {
|
||||
executor)
|
||||
.thenAccept(
|
||||
status -> {
|
||||
String path = getImagePathForButton(buttonId, status);
|
||||
String path = getImagePathForLobby(lobbyId, status);
|
||||
|
||||
javafx.application.Platform.runLater(
|
||||
() -> {
|
||||
@@ -390,8 +551,27 @@ public class LobbyButtonGridManager {
|
||||
});
|
||||
|
||||
try {
|
||||
var cs = lobbyClient.getClientService();
|
||||
|
||||
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI
|
||||
.setClientService(lobbyClient.getClientService());
|
||||
.setClientService(cs);
|
||||
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI.setLobbyId(
|
||||
lobbyId);
|
||||
|
||||
String username =
|
||||
ch.unibas.dmi.dbis.cs108.casono.client.ClientApp
|
||||
.getSharedUsername();
|
||||
if (username == null || username.isBlank()) {
|
||||
username =
|
||||
"Guest-"
|
||||
+ java.util
|
||||
.UUID
|
||||
.randomUUID()
|
||||
.toString()
|
||||
.substring(0, GUEST_ID_LENGTH);
|
||||
}
|
||||
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI.setUsername(
|
||||
username);
|
||||
|
||||
new ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI()
|
||||
.start(gameStage);
|
||||
|
||||
@@ -54,7 +54,7 @@ public class ServerApp {
|
||||
private static final int USER_CLEANUP_JOB_RECONNECT_THRESHOLD = 10;
|
||||
private static final int SESSION_DISCONNECT_JOB_DELAY = 0;
|
||||
private static final int SESSION_DISCONNECT_JOB_PERIOD = 2;
|
||||
private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 5;
|
||||
private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 30;
|
||||
private static final int LOBBY_EXPIRY_SECONDS = 30;
|
||||
private static final int LOBBY_CLEANUP_INITIAL_DELAY_SECONDS = 5;
|
||||
private static final int LOBBY_CLEANUP_PERIOD_SECONDS = 5;
|
||||
@@ -94,7 +94,12 @@ public class ServerApp {
|
||||
TimeUnit.SECONDS);
|
||||
|
||||
LobbyManager lobbyManager = new LobbyManager();
|
||||
registerCommands(dispatcher, router, responseDispatcher, userRegistry, lobbyManager);
|
||||
registerCommands(
|
||||
dispatcher,
|
||||
router,
|
||||
responseDispatcher,
|
||||
userRegistry,
|
||||
new CommandContext(lobbyManager, sessionManager));
|
||||
|
||||
// Periodic cleanup: remove empty lobbies older than 30s and notify affected
|
||||
// users
|
||||
@@ -135,6 +140,9 @@ public class ServerApp {
|
||||
networkManager.start();
|
||||
}
|
||||
|
||||
private static record CommandContext(
|
||||
LobbyManager lobbyManager, SessionManager sessionManager) {}
|
||||
|
||||
/**
|
||||
* Registers command parsers and handlers.
|
||||
*
|
||||
@@ -147,7 +155,7 @@ public class ServerApp {
|
||||
CommandRouter commandRouter,
|
||||
ResponseDispatcher responseDispatcher,
|
||||
UserRegistry userRegistry,
|
||||
LobbyManager lobbyManager) {
|
||||
CommandContext context) {
|
||||
parserDispatcher.register("PING", new PingParser());
|
||||
commandRouter.register(PingRequest.class, new PingHandler(responseDispatcher));
|
||||
|
||||
@@ -166,7 +174,8 @@ public class ServerApp {
|
||||
|
||||
parserDispatcher.register("SEND_MESSAGE", new SendMessageParser());
|
||||
commandRouter.register(
|
||||
SendMessageRequest.class, new SendMessageHandler(responseDispatcher, userRegistry));
|
||||
SendMessageRequest.class,
|
||||
new SendMessageHandler(responseDispatcher, userRegistry, context.lobbyManager()));
|
||||
|
||||
parserDispatcher.register("GET_MESSAGE_COUNT", new GetMessageCountParser());
|
||||
commandRouter.register(
|
||||
@@ -194,7 +203,7 @@ public class ServerApp {
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
|
||||
.get_lobby_list.GetLobbyListRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list
|
||||
.GetLobbyListHandler(responseDispatcher, lobbyManager));
|
||||
.GetLobbyListHandler(responseDispatcher, context.lobbyManager()));
|
||||
|
||||
// GET_GAME_STATE registration
|
||||
parserDispatcher.register(
|
||||
@@ -209,7 +218,7 @@ public class ServerApp {
|
||||
.get_game_state.GetGameStateRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state
|
||||
.GetGameStateHandler(
|
||||
responseDispatcher, lobbyManager, userRegistry));
|
||||
responseDispatcher, context.lobbyManager(), userRegistry));
|
||||
|
||||
// BET registration
|
||||
parserDispatcher.register(
|
||||
@@ -221,7 +230,8 @@ public class ServerApp {
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet
|
||||
.PlayerBetRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet
|
||||
.PlayerBetHandler(responseDispatcher, userRegistry, lobbyManager));
|
||||
.PlayerBetHandler(
|
||||
responseDispatcher, userRegistry, context.lobbyManager()));
|
||||
|
||||
// RAISE registration
|
||||
parserDispatcher.register(
|
||||
@@ -236,7 +246,7 @@ public class ServerApp {
|
||||
.PlayerRaiseRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise
|
||||
.PlayerRaiseHandler(
|
||||
responseDispatcher, userRegistry, lobbyManager));
|
||||
responseDispatcher, userRegistry, context.lobbyManager()));
|
||||
|
||||
// CALL registration
|
||||
parserDispatcher.register(
|
||||
@@ -250,7 +260,8 @@ public class ServerApp {
|
||||
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));
|
||||
.PlayerCallHandler(
|
||||
responseDispatcher, userRegistry, context.lobbyManager()));
|
||||
|
||||
// FOLD registration
|
||||
parserDispatcher.register(
|
||||
@@ -264,7 +275,8 @@ public class ServerApp {
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold
|
||||
.PlayerFoldRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold
|
||||
.PlayerFoldHandler(responseDispatcher, userRegistry, lobbyManager));
|
||||
.PlayerFoldHandler(
|
||||
responseDispatcher, userRegistry, context.lobbyManager()));
|
||||
|
||||
// GET_LOBBY_STATUS registration
|
||||
parserDispatcher.register(
|
||||
@@ -279,7 +291,7 @@ public class ServerApp {
|
||||
.get_lobby_status.GetLobbyStatusRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
|
||||
.get_lobby_status.GetLobbyStatusHandler(
|
||||
responseDispatcher, lobbyManager, userRegistry));
|
||||
responseDispatcher, context.lobbyManager(), userRegistry));
|
||||
|
||||
// CREATE_LOBBY registration
|
||||
parserDispatcher.register(
|
||||
@@ -293,7 +305,10 @@ public class ServerApp {
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
|
||||
.create_lobby.CreateLobbyRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby
|
||||
.CreateLobbyHandler(responseDispatcher, lobbyManager));
|
||||
.CreateLobbyHandler(
|
||||
responseDispatcher,
|
||||
context.lobbyManager(),
|
||||
context.sessionManager()));
|
||||
|
||||
// JOIN_LOBBY registration
|
||||
parserDispatcher.register(
|
||||
@@ -307,7 +322,8 @@ public class ServerApp {
|
||||
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));
|
||||
.JoinLobbyHandler(
|
||||
responseDispatcher, context.lobbyManager(), userRegistry));
|
||||
|
||||
// START_GAME registration
|
||||
parserDispatcher.register(
|
||||
@@ -321,6 +337,7 @@ public class ServerApp {
|
||||
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));
|
||||
.StartGameHandler(
|
||||
responseDispatcher, context.lobbyManager(), userRegistry));
|
||||
}
|
||||
}
|
||||
|
||||
+16
-2
@@ -28,9 +28,9 @@ public class GetGameStateHandler extends CommandHandler<GetGameStateRequest> {
|
||||
@Override
|
||||
public void execute(GetGameStateRequest request) {
|
||||
Integer gameId = request.getGameId();
|
||||
String username = request.getUsername();
|
||||
String username = resolveUsername(request);
|
||||
|
||||
Lobby lobby = null;
|
||||
Lobby lobby;
|
||||
if (gameId != null) {
|
||||
lobby = lobbyManager.getLobby(LobbyId.of(gameId));
|
||||
if (lobby == null) {
|
||||
@@ -71,4 +71,18 @@ public class GetGameStateHandler extends CommandHandler<GetGameStateRequest> {
|
||||
|
||||
responseDispatcher.dispatch(new GetGameStateResponse(request.getContext(), game, username));
|
||||
}
|
||||
|
||||
private String resolveUsername(GetGameStateRequest request) {
|
||||
String username = request.getUsername();
|
||||
if (username != null && !username.isBlank()) {
|
||||
return username.trim();
|
||||
}
|
||||
|
||||
return userRegistry
|
||||
.getBySessionId(request.getSessionId())
|
||||
.map(User::getName)
|
||||
.map(String::trim)
|
||||
.filter(name -> !name.isEmpty())
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
+72
-16
@@ -12,25 +12,44 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessR
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/** Response carrying a snapshot of the current game state using the project's wire format. */
|
||||
/**
|
||||
* Response carrying a snapshot of the current game state using the project's wire format.
|
||||
*
|
||||
* <p>Wire format (relevant parts):
|
||||
*
|
||||
* <p>+OK PHASE=... POT=... CURRENT_BET=... DEALER=... ACTIVE_PLAYER=... CARD VALUE=... SUIT=... END
|
||||
* PLAYER NAME=... CHIPS=... BET=... STATE=... CARD (only for requesting user) VALUE=... SUIT=...
|
||||
* END END END
|
||||
*
|
||||
* <p>Notes: - Community cards are always emitted at top-level (0..5). If none exist (e.g. PREFLOP),
|
||||
* none are emitted. - Hole cards are only emitted for the requesting player (privacy).
|
||||
*/
|
||||
public class GetGameStateResponse extends SuccessResponse {
|
||||
|
||||
public GetGameStateResponse(
|
||||
RequestContext context, GameController game, String requestingUsername) {
|
||||
super(context, buildBody(game.getState(), requestingUsername));
|
||||
super(
|
||||
context,
|
||||
buildBody(
|
||||
Objects.requireNonNull(game, "game must not be null").getState(),
|
||||
requestingUsername));
|
||||
}
|
||||
|
||||
private static ResponseBody buildBody(GameState state, String requestingUsername) {
|
||||
int globalCurrentBet = computeGlobalCurrentBet(state);
|
||||
Objects.requireNonNull(state, "state must not be null");
|
||||
|
||||
ResponseBodyBuilder builder = ResponseBody.builder();
|
||||
|
||||
builder.param("PHASE", state.getPhase() == null ? "UNKNOWN" : state.getPhase().name());
|
||||
builder.param("POT", state.getPot().getAmount());
|
||||
builder.param("CURRENT_BET", globalCurrentBet);
|
||||
builder.param("POT", state.getPot() == null ? 0 : state.getPot().getAmount());
|
||||
builder.param("CURRENT_BET", computeGlobalCurrentBet(state));
|
||||
builder.param("DEALER", state.getDealerIndex());
|
||||
builder.param("ACTIVE_PLAYER", state.getCurrentPlayerIndex());
|
||||
|
||||
appendCommunityCards(builder, state);
|
||||
|
||||
appendPlayers(builder, state, requestingUsername);
|
||||
|
||||
return builder.build();
|
||||
@@ -39,6 +58,10 @@ public class GetGameStateResponse extends SuccessResponse {
|
||||
private static int computeGlobalCurrentBet(GameState state) {
|
||||
int globalCurrentBet = 0;
|
||||
for (Player p : state.getPlayers()) {
|
||||
if (p == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int b = state.getCurrentBet(p.getId());
|
||||
if (b > globalCurrentBet) {
|
||||
globalCurrentBet = b;
|
||||
@@ -48,7 +71,17 @@ public class GetGameStateResponse extends SuccessResponse {
|
||||
}
|
||||
|
||||
private static void appendCommunityCards(ResponseBodyBuilder builder, GameState state) {
|
||||
for (Card c : state.getCommunityCards()) {
|
||||
List<Card> community = state.getCommunityCards();
|
||||
|
||||
if (community == null || community.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Card c : community) {
|
||||
if (c == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.block(
|
||||
"CARD",
|
||||
card -> {
|
||||
@@ -60,25 +93,40 @@ public class GetGameStateResponse extends SuccessResponse {
|
||||
|
||||
private static void appendPlayers(
|
||||
ResponseBodyBuilder builder, GameState state, String requestingUsername) {
|
||||
String req = (requestingUsername == null) ? null : requestingUsername.trim();
|
||||
|
||||
for (Player p : state.getPlayers()) {
|
||||
if (p == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PlayerId pid = p.getId();
|
||||
String name = (pid == null || pid.value() == null) ? "" : pid.value().trim();
|
||||
|
||||
builder.block(
|
||||
"PLAYER",
|
||||
pblock -> {
|
||||
pblock.param("NAME", pid.value());
|
||||
pblock.param("NAME", name);
|
||||
pblock.param("CHIPS", p.getChips());
|
||||
pblock.param("BET", state.getCurrentBet(pid));
|
||||
pblock.param("BET", pid == null ? 0 : state.getCurrentBet(pid));
|
||||
pblock.param("STATE", p.isFolded() ? "FOLDED" : "ACTIVE");
|
||||
|
||||
if (requestingUsername != null && requestingUsername.equals(pid.value())) {
|
||||
if (req != null && pid != null && req.equals(name)) {
|
||||
List<Card> hole = state.getHoleCards(pid);
|
||||
for (Card hc : hole) {
|
||||
pblock.block(
|
||||
"CARD",
|
||||
cblock -> {
|
||||
cblock.param("VALUE", rankToWire(hc.getRank()));
|
||||
cblock.param("SUIT", suitToWire(hc.getSuit()));
|
||||
});
|
||||
|
||||
if (hole != null) {
|
||||
for (Card hc : hole) {
|
||||
if (hc == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pblock.block(
|
||||
"CARD",
|
||||
cblock -> {
|
||||
cblock.param("VALUE", rankToWire(hc.getRank()));
|
||||
cblock.param("SUIT", suitToWire(hc.getSuit()));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -86,6 +134,10 @@ public class GetGameStateResponse extends SuccessResponse {
|
||||
}
|
||||
|
||||
private static String rankToWire(Rank r) {
|
||||
if (r == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return switch (r) {
|
||||
case TWO -> "2";
|
||||
case THREE -> "3";
|
||||
@@ -104,6 +156,10 @@ public class GetGameStateResponse extends SuccessResponse {
|
||||
}
|
||||
|
||||
private static String suitToWire(Suit s) {
|
||||
if (s == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return switch (s) {
|
||||
case HEARTS -> "H";
|
||||
case DIAMONDS -> "D";
|
||||
|
||||
+25
-1
@@ -3,15 +3,25 @@ package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_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.network.command.execution.CommandHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||
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;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
|
||||
|
||||
public class CreateLobbyHandler extends CommandHandler<CreateLobbyRequest> {
|
||||
private final LobbyManager lobbyManager;
|
||||
private final SessionManager sessionManager;
|
||||
|
||||
public CreateLobbyHandler(ResponseDispatcher responseDispatcher, LobbyManager lobbyManager) {
|
||||
public CreateLobbyHandler(
|
||||
ResponseDispatcher responseDispatcher,
|
||||
LobbyManager lobbyManager,
|
||||
SessionManager sessionManager) {
|
||||
super(responseDispatcher);
|
||||
this.lobbyManager = lobbyManager;
|
||||
this.sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -28,5 +38,19 @@ public class CreateLobbyHandler extends CommandHandler<CreateLobbyRequest> {
|
||||
}
|
||||
|
||||
responseDispatcher.dispatch(new CreateLobbyResponse(request.getContext(), id.value()));
|
||||
|
||||
// broadcast LOBBY_CREATED event to all connected sessions (requestId=0)
|
||||
for (Session s : sessionManager.getAllSessions()) {
|
||||
RequestContext ctx = new RequestContext(s.getId(), 0);
|
||||
SuccessResponse ev =
|
||||
new SuccessResponse(
|
||||
ctx,
|
||||
ResponseBody.builder()
|
||||
.param("EVENT", "LOBBY_CREATED")
|
||||
.param("LOBBY_ID", id.value())
|
||||
.build()) {};
|
||||
|
||||
responseDispatcher.dispatch(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+40
-3
@@ -1,6 +1,10 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||
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.OkResponse;
|
||||
@@ -8,6 +12,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatch
|
||||
|
||||
public class SendMessageHandler extends CommandHandler<SendMessageRequest> {
|
||||
private final UserRegistry userRegistry;
|
||||
private final LobbyManager lobbyManager;
|
||||
|
||||
/**
|
||||
* Constructs a new SendMessageHandler with the required dispatcher and user registry.
|
||||
@@ -15,9 +20,13 @@ public class SendMessageHandler extends CommandHandler<SendMessageRequest> {
|
||||
* @param responseDispatcher The dispatcher used to send responses back to clients.
|
||||
* @param userRegistry The registry containing all currently connected users.
|
||||
*/
|
||||
public SendMessageHandler(ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
|
||||
public SendMessageHandler(
|
||||
ResponseDispatcher responseDispatcher,
|
||||
UserRegistry userRegistry,
|
||||
LobbyManager lobbyManager) {
|
||||
super(responseDispatcher);
|
||||
this.userRegistry = userRegistry;
|
||||
this.lobbyManager = lobbyManager;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,7 +39,7 @@ public class SendMessageHandler extends CommandHandler<SendMessageRequest> {
|
||||
@Override
|
||||
public void execute(SendMessageRequest request) {
|
||||
Message message = request.getMessage();
|
||||
broadcast(message);
|
||||
broadcast(request, message);
|
||||
OkResponse response = new OkResponse(request.getContext());
|
||||
responseDispatcher.dispatch(response);
|
||||
}
|
||||
@@ -41,7 +50,35 @@ public class SendMessageHandler extends CommandHandler<SendMessageRequest> {
|
||||
*
|
||||
* @param message The {@link Message} object to be broadcast.
|
||||
*/
|
||||
public void broadcast(Message message) {
|
||||
public void broadcast(SendMessageRequest request, Message message) {
|
||||
if (message != null && message.getMessageType() == ChatType.LOBBY && lobbyManager != null) {
|
||||
LobbyId targetLobbyId = resolveLobbyIdForMessage(request, message);
|
||||
if (targetLobbyId != null) {
|
||||
lobbyManager.broadcast(
|
||||
targetLobbyId,
|
||||
username ->
|
||||
userRegistry
|
||||
.getByUsername(username)
|
||||
.ifPresent(user -> user.enqueueMessage(message)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
userRegistry.getAllUsers().forEach(user -> user.enqueueMessage(message));
|
||||
}
|
||||
|
||||
private LobbyId resolveLobbyIdForMessage(SendMessageRequest request, Message message) {
|
||||
if (message.lobbyId >= 0) {
|
||||
LobbyId idFromMessage = LobbyId.of(message.lobbyId);
|
||||
if (lobbyManager.getLobby(idFromMessage) != null) {
|
||||
return idFromMessage;
|
||||
}
|
||||
}
|
||||
|
||||
return userRegistry
|
||||
.getBySessionId(request.getContext().sessionId())
|
||||
.map(user -> lobbyManager.getLobbyByUsername(user.getName()))
|
||||
.map(Lobby::getId)
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -29,7 +29,6 @@ public class GameController {
|
||||
|
||||
private int dealerIndex = 0;
|
||||
|
||||
private static final int NEXT_PLAYER_OFFSET = 3;
|
||||
private static final int DEALER_OFFSET = 1;
|
||||
private static final int SMALL_BLIND_OFFSET = 1;
|
||||
private static final int BIG_BLIND_OFFSET = 2;
|
||||
@@ -81,13 +80,13 @@ public class GameController {
|
||||
dealHoleCards();
|
||||
postBlinds();
|
||||
|
||||
int nextPlayerIndex = (dealerIndex + NEXT_PLAYER_OFFSET) % players.size();
|
||||
engine.getState().setCurrentPlayerIndex(nextPlayerIndex);
|
||||
engine.getState().setCurrentPlayerToPreflopFirstToAct();
|
||||
}
|
||||
|
||||
/** Rotates the dealer position to the next player in the list. */
|
||||
private void rotateDealer() {
|
||||
dealerIndex = (dealerIndex + DEALER_OFFSET) % players.size();
|
||||
engine.getState().setDealerIndex(dealerIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+139
-63
@@ -1,133 +1,209 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Card;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Deck;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.Player;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerStatus;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GamePhase;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* RoundManager is responsible for managing the flow of a poker game round. It handles the
|
||||
* progression of the game through its various phases (pre-flop, flop, turn, river) and manages
|
||||
* player actions such as posting blinds and dealing cards. The RoundManager ensures that the game
|
||||
* state is updated correctly based on player actions and the current phase of the game.
|
||||
* RoundManager manages hand lifecycle and phase progression (PREFLOP -> FLOP -> TURN -> RIVER ->
|
||||
* SHOWDOWN).
|
||||
*
|
||||
* <p>This implementation:
|
||||
*
|
||||
* <ul>
|
||||
* <li>starts a new hand (reset + hole cards + blinds)
|
||||
* <li>progresses phases once betting round is finished
|
||||
* <li>deals community cards (3/1/1)
|
||||
* </ul>
|
||||
*/
|
||||
public class RoundManager {
|
||||
|
||||
public static final int SMALL_BLIND = 100;
|
||||
public static final int BIG_BLIND = 200;
|
||||
|
||||
/**
|
||||
* Starts a new hand by initializing the game state, dealing cards to players, and posting
|
||||
* blinds. This method sets the hand as active and resets any necessary state variables to
|
||||
* prepare for a new round of play.
|
||||
*
|
||||
* @param state The game state to be used for starting the new hand.
|
||||
*/
|
||||
public void startNewHand(GameState state) {
|
||||
state.setHandActive(true);
|
||||
state.resetBets();
|
||||
// Use GameState's canonical reset
|
||||
state.startNewHand();
|
||||
|
||||
dealCards(state);
|
||||
// Ensure deck exists
|
||||
ensureDeck(state);
|
||||
|
||||
// Deal hole cards
|
||||
dealHoleCards(state);
|
||||
|
||||
// Post blinds
|
||||
postBlinds(state);
|
||||
|
||||
// Preflop always starts left of BB (or dealer in heads-up).
|
||||
state.setCurrentPlayerToPreflopFirstToAct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the betting round is finished and advances the game phase if necessary. This method
|
||||
* evaluates the current bets of all active players and determines if the betting round can be
|
||||
* concluded. If all players have met the current bet or are all-in, the game phase is advanced
|
||||
* to the next stage.
|
||||
*
|
||||
* @param state The current game state to be evaluated for betting round progression.
|
||||
*/
|
||||
public void progressIfNeeded(GameState state) {
|
||||
if (state.getPhase() == null) {
|
||||
state.setPhase(GamePhase.PREFLOP);
|
||||
}
|
||||
|
||||
if (isBettingRoundFinished(state)) {
|
||||
advancePhase(state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the betting round is finished by checking if all active players have met the
|
||||
* current bet or are all-in. This method iterates through all players in the game state and
|
||||
* evaluates their bets against the current bet on the table.
|
||||
*
|
||||
* @param state The current game state to be evaluated for betting round completion.
|
||||
* @return true if the betting round is finished, false otherwise.
|
||||
*/
|
||||
private boolean isBettingRoundFinished(GameState state) {
|
||||
|
||||
int target = state.getTableState().getCurrentBet();
|
||||
|
||||
for (Player p : state.getPlayers()) {
|
||||
if (p == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (p.getStatus() == PlayerStatus.FOLDED) {
|
||||
// be tolerant if codebase mixes status + boolean flags
|
||||
if (p.getStatus() == PlayerStatus.FOLDED || p.isFolded()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int bet = state.getCurrentBet(p.getId());
|
||||
|
||||
// The player must have placed at least one bet
|
||||
if (bet < target && !p.isAllIn()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the game phase to the next stage (flop, turn, river, or showdown) based on the
|
||||
* current phase of the game. This method is called when the betting round is finished and
|
||||
* updates the game state accordingly to reflect the new phase of play.
|
||||
*
|
||||
* @param state The current game state to be updated with the new phase.
|
||||
*/
|
||||
private void advancePhase(GameState state) {
|
||||
|
||||
switch (state.getPhase()) {
|
||||
case PREFLOP -> dealFlop(state);
|
||||
case FLOP -> dealTurn(state);
|
||||
case TURN -> dealRiver(state);
|
||||
case RIVER -> showdown(state);
|
||||
case SHOWDOWN -> {
|
||||
/* nothing */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureDeck(GameState state) {
|
||||
Deck deck = state.getDeck();
|
||||
if (deck == null) {
|
||||
deck = new Deck();
|
||||
deck.shuffle();
|
||||
state.setDeck(deck);
|
||||
}
|
||||
}
|
||||
|
||||
private void dealHoleCards(GameState state) {
|
||||
Deck deck = state.getDeck();
|
||||
|
||||
for (Player p : state.getPlayers()) {
|
||||
if (p == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PlayerId pid = p.getId();
|
||||
Card c1 = deck.draw();
|
||||
Card c2 = deck.draw();
|
||||
|
||||
state.giveHoleCards(pid, c1, c2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the posting of blinds at the start of a new hand. This method identifies the players
|
||||
* responsible for posting the small and big blinds, updates their chip counts, adds the blind
|
||||
* amounts to the pot, and updates the current bets for those players in the game state.
|
||||
*
|
||||
* @param state The current game state to be updated with the posted blinds.
|
||||
* NOTE: Blind assignment here is intentionally minimal because GameState does not expose
|
||||
* seating order. If you want correct dealer-relative blinds, add helpers in GameState (e.g.
|
||||
* getPlayerIdAt(int)).
|
||||
*/
|
||||
private void postBlinds(GameState state) {
|
||||
List<Player> playerList = new ArrayList<>(state.getPlayers());
|
||||
if (playerList.size() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
Player sb = playerList.get(0);
|
||||
Player bb = playerList.get(1);
|
||||
// pick first two non-folded players as SB/BB
|
||||
Player sb = null;
|
||||
Player bb = null;
|
||||
|
||||
int smallBlind = SMALL_BLIND;
|
||||
int bigBlind = BIG_BLIND;
|
||||
for (Player p : playerList) {
|
||||
if (p == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.removeChips(smallBlind);
|
||||
bb.removeChips(bigBlind);
|
||||
if (p.isFolded()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
state.addToPot(smallBlind + bigBlind);
|
||||
if (sb == null) {
|
||||
sb = p;
|
||||
} else {
|
||||
bb = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
state.setCurrentBet(sb.getId(), smallBlind);
|
||||
state.setCurrentBet(bb.getId(), bigBlind);
|
||||
if (sb == null || bb == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.getTableState().setCurrentBet(bigBlind);
|
||||
sb.removeChips(SMALL_BLIND);
|
||||
bb.removeChips(BIG_BLIND);
|
||||
|
||||
state.addToPot(SMALL_BLIND + BIG_BLIND);
|
||||
|
||||
state.setCurrentBet(sb.getId(), SMALL_BLIND);
|
||||
state.setCurrentBet(bb.getId(), BIG_BLIND);
|
||||
|
||||
state.getTableState().setCurrentBet(BIG_BLIND);
|
||||
}
|
||||
|
||||
private void dealCards(GameState state) {}
|
||||
private void dealFlop(GameState state) {
|
||||
ensureDeck(state);
|
||||
Deck deck = state.getDeck();
|
||||
|
||||
private void dealFlop(GameState state) {}
|
||||
state.addCommunityCard(deck.draw());
|
||||
state.addCommunityCard(deck.draw());
|
||||
state.addCommunityCard(deck.draw());
|
||||
|
||||
private void dealTurn(GameState state) {}
|
||||
state.setPhase(GamePhase.FLOP);
|
||||
|
||||
private void dealRiver(GameState state) {}
|
||||
// new betting round
|
||||
state.resetBets();
|
||||
state.setCurrentPlayerToPostflopFirstToAct();
|
||||
}
|
||||
|
||||
private void showdown(GameState state) {}
|
||||
private void dealTurn(GameState state) {
|
||||
ensureDeck(state);
|
||||
Deck deck = state.getDeck();
|
||||
|
||||
state.addCommunityCard(deck.draw());
|
||||
|
||||
state.setPhase(GamePhase.TURN);
|
||||
|
||||
// new betting round
|
||||
state.resetBets();
|
||||
state.setCurrentPlayerToPostflopFirstToAct();
|
||||
}
|
||||
|
||||
private void dealRiver(GameState state) {
|
||||
ensureDeck(state);
|
||||
Deck deck = state.getDeck();
|
||||
|
||||
state.addCommunityCard(deck.draw());
|
||||
|
||||
state.setPhase(GamePhase.RIVER);
|
||||
|
||||
// new betting round
|
||||
state.resetBets();
|
||||
state.setCurrentPlayerToPostflopFirstToAct();
|
||||
}
|
||||
|
||||
private void showdown(GameState state) {
|
||||
state.setPhase(GamePhase.SHOWDOWN);
|
||||
|
||||
// winner evaluation is elsewhere (rules/controller)
|
||||
// do NOT reset cards here; clients still need board visible during showdown
|
||||
}
|
||||
}
|
||||
|
||||
+1
-4
@@ -18,9 +18,6 @@ public class TurnManager {
|
||||
* @param state The current game state that will be updated to reflect the next player's turn.
|
||||
*/
|
||||
public void nextPlayer(GameState state) {
|
||||
|
||||
int next = (state.getCurrentPlayerIndex() + 1) % state.getPlayers().size();
|
||||
|
||||
state.setCurrentPlayerIndex(next);
|
||||
state.nextPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-4
@@ -6,8 +6,6 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.Rule;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.RuleViolationException;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The ActionOrderRule class implements the Rule interface and defines the validation logic for
|
||||
@@ -31,8 +29,7 @@ public class ActionOrderRule implements Rule {
|
||||
|
||||
PlayerId actingPlayerId = action.getPlayerId();
|
||||
|
||||
List<Player> playerList = new ArrayList<>(state.getPlayers());
|
||||
Player current = playerList.get(state.getCurrentPlayerIndex());
|
||||
Player current = state.getCurrentPlayer();
|
||||
|
||||
if (!current.getId().equals(actingPlayerId)) {
|
||||
throw new RuleViolationException("Not your turn");
|
||||
|
||||
+117
-268
@@ -11,225 +11,162 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* The GameState class encapsulates the entire state of a poker game at any given moment. It
|
||||
* maintains information about the players, the pot, the current phase of the game, the dealer
|
||||
* position, and the cards in play. This class is central to managing the flow of the game and
|
||||
* ensuring that all actions and decisions are based on an accurate representation of the current
|
||||
* game state.
|
||||
* The GameState class encapsulates the entire state of a poker game at any
|
||||
* given moment.
|
||||
*
|
||||
* <p>
|
||||
* Important invariants this implementation maintains:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code playerOrder} defines the stable seating/turn order.
|
||||
* <li>{@code currentBets} always contains an entry for every player in
|
||||
* {@code playerOrder}.
|
||||
* <li>{@code holeCards} maps every player to a mutable list; if not present, it
|
||||
* is created on
|
||||
* demand.
|
||||
* </ul>
|
||||
*/
|
||||
public class GameState {
|
||||
|
||||
private Map<PlayerId, Player> players = new HashMap<>();
|
||||
|
||||
private List<PlayerId> playerOrder = new ArrayList<>();
|
||||
private final Map<PlayerId, Player> players = new HashMap<>();
|
||||
private final List<PlayerId> playerOrder = new ArrayList<>();
|
||||
|
||||
private Pot pot = new Pot();
|
||||
|
||||
private TableState tableState = new TableState();
|
||||
private final TableState tableState = new TableState();
|
||||
|
||||
private int currentPlayerIndex;
|
||||
|
||||
private boolean handActive;
|
||||
|
||||
private boolean allowOutOfTurn;
|
||||
|
||||
private GamePhase phase;
|
||||
|
||||
private int dealerIndex;
|
||||
|
||||
private Map<PlayerId, Integer> currentBets = new HashMap<>();
|
||||
private final Map<PlayerId, Integer> currentBets = new HashMap<>();
|
||||
private final Map<PlayerId, Integer> playerBetCommitments = new HashMap<>();
|
||||
|
||||
private Map<PlayerId, Integer> playerBetCommitments = new HashMap<>();
|
||||
|
||||
private Map<PlayerId, List<Card>> holeCards = new HashMap<>();
|
||||
|
||||
private List<Card> communityCards = new ArrayList<>();
|
||||
|
||||
// private final Set<PlayerId> foldedPlayers = new HashSet<>();
|
||||
private final Map<PlayerId, List<Card>> holeCards = new HashMap<>();
|
||||
private final List<Card> communityCards = new ArrayList<>();
|
||||
|
||||
private Deck deck;
|
||||
|
||||
// Getter
|
||||
private static final int DEALER_OFFSET = 3;
|
||||
|
||||
/**
|
||||
* Returns the list of players currently in the game.
|
||||
*
|
||||
* @return A list of Player objects representing the players in the game.
|
||||
*/
|
||||
// Getters
|
||||
public Collection<Player> getPlayers() {
|
||||
return players.values();
|
||||
List<Player> ordered = new ArrayList<>(playerOrder.size());
|
||||
for (PlayerId id : playerOrder) {
|
||||
Player p = players.get(id);
|
||||
if (p != null) {
|
||||
ordered.add(p);
|
||||
}
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current player whose turn it is to act.
|
||||
*
|
||||
* @return The Player object representing the current player.
|
||||
*/
|
||||
public Player getCurrentPlayer() {
|
||||
PlayerId id = playerOrder.get(currentPlayerIndex);
|
||||
return players.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the current player in the players list.
|
||||
*
|
||||
* @return An integer representing the index of the current player.
|
||||
*/
|
||||
public int getCurrentPlayerIndex() {
|
||||
return currentPlayerIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether a hand is currently active in the game.
|
||||
*
|
||||
* @return true if a hand is active, false otherwise.
|
||||
*/
|
||||
public boolean isHandActive() {
|
||||
return handActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current phase of the game (e.g., PREFLOP, FLOP, TURN, RIVER).
|
||||
*
|
||||
* @return The GamePhase enum value representing the current phase of the game.
|
||||
*/
|
||||
public GamePhase getPhase() {
|
||||
return phase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current state of the table, including player statuses and positions.
|
||||
*
|
||||
* @return A TableState object representing the current state of the table.
|
||||
*/
|
||||
public TableState getTableState() {
|
||||
return tableState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current pot, which contains the total amount of chips bet by players in the
|
||||
* current hand.
|
||||
*
|
||||
* @return A Pot object representing the current pot.
|
||||
*/
|
||||
public Pot getPot() {
|
||||
return pot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current deck of cards being used in the game.
|
||||
*
|
||||
* @return A Deck object representing the current deck of cards.
|
||||
*/
|
||||
public Deck getDeck() {
|
||||
return deck;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of community cards currently on the table.
|
||||
*
|
||||
* @return A list of Card objects representing the community cards.
|
||||
*/
|
||||
public List<Card> getCommunityCards() {
|
||||
return communityCards;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hole cards for a specific player based on their ID.
|
||||
*
|
||||
* @param playerId The ID of the player whose hole cards are being requested.
|
||||
* @return A list of Card objects representing the player's hole cards, or an empty list if the
|
||||
* player has no hole cards.
|
||||
*/
|
||||
/** Always returns a mutable list (never null). */
|
||||
public List<Card> getHoleCards(PlayerId playerId) {
|
||||
return holeCards.getOrDefault(playerId, new ArrayList<>());
|
||||
return holeCards.computeIfAbsent(playerId, k -> new ArrayList<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the dealer in the players list.
|
||||
*
|
||||
* @return An integer representing the index of the dealer.
|
||||
*/
|
||||
public int getDealerIndex() {
|
||||
return dealerIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map of player IDs to their current hole cards.
|
||||
*
|
||||
* @return A map where the key is the player ID and the value is a list of Card objects
|
||||
* representing the player's hole cards.
|
||||
*/
|
||||
public Map<PlayerId, List<Card>> getPlayerCards() {
|
||||
return holeCards;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of players currently in the game.
|
||||
*
|
||||
* @return An integer representing the number of players in the game.
|
||||
*/
|
||||
public int getPlayerCount() {
|
||||
return players.size();
|
||||
}
|
||||
|
||||
// SETTER
|
||||
|
||||
/**
|
||||
* Sets the index of the current player in the players list.
|
||||
*
|
||||
* @param index An integer representing the index of the current player.
|
||||
*/
|
||||
// Setters / Mutators
|
||||
public void setCurrentPlayerIndex(int index) {
|
||||
if (playerOrder.isEmpty()) {
|
||||
this.currentPlayerIndex = 0;
|
||||
return;
|
||||
}
|
||||
this.currentPlayerIndex = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether a hand is currently active in the game.
|
||||
*
|
||||
* @param handActive A boolean value indicating whether a hand is active.
|
||||
*/
|
||||
public void setCurrentPlayerToPreflopFirstToAct() {
|
||||
int size = playerOrder.size();
|
||||
if (size == 0) {
|
||||
currentPlayerIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Heads-up: dealer (small blind) acts first preflop.
|
||||
int first = (size == 2) ? dealerIndex : (dealerIndex + DEALER_OFFSET) % size;
|
||||
currentPlayerIndex = first;
|
||||
if (!canPlayerAct(currentPlayerIndex)) {
|
||||
nextPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
public void setCurrentPlayerToPostflopFirstToAct() {
|
||||
int size = playerOrder.size();
|
||||
if (size == 0) {
|
||||
currentPlayerIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
int first = (dealerIndex + 1) % size;
|
||||
currentPlayerIndex = first;
|
||||
if (!canPlayerAct(currentPlayerIndex)) {
|
||||
nextPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
public void setHandActive(boolean handActive) {
|
||||
this.handActive = handActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current phase of the game.
|
||||
*
|
||||
* @param phase The GamePhase enum value representing the new phase of the game.
|
||||
*/
|
||||
public void setPhase(GamePhase phase) {
|
||||
this.phase = phase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the index of the dealer in the players list.
|
||||
*
|
||||
* @param dealerIndex An integer representing the index of the dealer.
|
||||
*/
|
||||
public void setDealerIndex(int dealerIndex) {
|
||||
this.dealerIndex = dealerIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current deck of cards being used in the game.
|
||||
*
|
||||
* @param deck A Deck object representing the new deck of cards to be used in the game.
|
||||
*/
|
||||
public void setDeck(Deck deck) {
|
||||
this.deck = deck;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a player to the game with the specified ID and initial chip count. This method creates a
|
||||
* new Player object, adds it to the list of players, and initializes the player's current bet
|
||||
* and bet commitment in the game state.
|
||||
*
|
||||
* @param id The ID of the player to add.
|
||||
* @param chips The initial number of chips the player has.
|
||||
*/
|
||||
public void addPlayer(PlayerId id, int chips) {
|
||||
|
||||
Player player = new Player(id, chips);
|
||||
|
||||
players.put(id, player);
|
||||
@@ -237,87 +174,45 @@ public class GameState {
|
||||
|
||||
currentBets.put(id, 0);
|
||||
playerBetCommitments.put(id, 0);
|
||||
|
||||
holeCards.computeIfAbsent(id, k -> new ArrayList<>());
|
||||
}
|
||||
|
||||
// BETTING LOGIC
|
||||
|
||||
/**
|
||||
* Retrieves the current bet amount for a specific player based on their ID.
|
||||
*
|
||||
* @param playerId The ID of the player whose current bet is being requested.
|
||||
* @return An integer representing the current bet amount for the specified player, or 0 if the
|
||||
* player has not placed any bets.
|
||||
*/
|
||||
// Betting
|
||||
public int getCurrentBet(PlayerId playerId) {
|
||||
return currentBets.getOrDefault(playerId, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current bet amount for a specific player based on their ID. This method updates the
|
||||
* currentBets map with the new bet amount for the specified player.
|
||||
*
|
||||
* @param playerId The ID of the player whose current bet is being set.
|
||||
* @param amount The new bet amount to be set for the specified player.
|
||||
*/
|
||||
public void setCurrentBet(PlayerId playerId, int amount) {
|
||||
currentBets.put(playerId, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the current bets for all players by clearing the currentBets map. This method is
|
||||
* typically called at the start of a new hand to ensure that all players' bets are reset to
|
||||
* zero.
|
||||
* Reset bets to 0 for ALL players (do not clear the map). Also resets table
|
||||
* current bet to 0.
|
||||
*/
|
||||
public void resetBets() {
|
||||
currentBets.clear();
|
||||
for (PlayerId id : playerOrder) {
|
||||
currentBets.put(id, 0);
|
||||
}
|
||||
tableState.setCurrentBet(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a specified amount to the pot. This method updates the total amount in the pot by adding
|
||||
* the given amount to it.
|
||||
*
|
||||
* @param amount The amount of chips to be added to the pot.
|
||||
*/
|
||||
public void addToPot(int amount) {
|
||||
pot.add(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether out-of-turn actions are allowed in the game. Out-of-turn actions refer to
|
||||
* players being able to act when it is not their turn, which can be a feature in some poker
|
||||
* variants or game modes.
|
||||
*
|
||||
* @return true if out-of-turn actions are allowed, false otherwise.
|
||||
*/
|
||||
public boolean isAllowOutOfTurn() {
|
||||
return allowOutOfTurn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether out-of-turn actions are allowed in the game. This method updates the
|
||||
* allowOutOfTurn flag, which determines if players can act when it is not their turn.
|
||||
*
|
||||
* @param allowOutOfTurn A boolean value indicating whether out-of-turn actions should be
|
||||
* allowed in the game.
|
||||
*/
|
||||
public void setAllowOutOfTurn(boolean allowOutOfTurn) {
|
||||
this.allowOutOfTurn = allowOutOfTurn;
|
||||
}
|
||||
|
||||
// PLAYER HELPERS
|
||||
|
||||
/**
|
||||
* Retrieves a player from the game based on their ID. This method searches the list of players
|
||||
* for a player with the specified ID and returns it. If no player with the given ID is found, a
|
||||
* RuntimeException is thrown.
|
||||
*
|
||||
* @param id The ID of the player to retrieve.
|
||||
* @return The Player object corresponding to the specified ID.
|
||||
* @throws RuntimeException if no player with the given ID is found in the game.
|
||||
*/
|
||||
// Player helpers
|
||||
public Player getPlayer(PlayerId id) {
|
||||
Player player = players.get(id);
|
||||
|
||||
if (player == null) {
|
||||
throw new RuntimeException("Player not found: " + id);
|
||||
}
|
||||
@@ -325,125 +220,90 @@ public class GameState {
|
||||
return player;
|
||||
}
|
||||
|
||||
// BET COMMITMENTS
|
||||
|
||||
/**
|
||||
* Retrieves the current bet commitment for a specific player based on their ID. A bet
|
||||
* commitment represents the total amount a player has committed to the pot in the current hand,
|
||||
* including all bets, raises, and calls they have made.
|
||||
*
|
||||
* @param playerId The ID of the player whose current bet commitment is being requested.
|
||||
* @return An integer representing the current bet commitment for the specified player, or 0 if
|
||||
* the player has not made any bet commitments.
|
||||
*/
|
||||
// Bet commitments
|
||||
public int getCurrentBetCommitment(PlayerId playerId) {
|
||||
return playerBetCommitments.getOrDefault(playerId, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current bet commitment for a specific player based on their ID. This method updates
|
||||
* the playerBetCommitments map with the new bet commitment amount for the specified player.
|
||||
*
|
||||
* @param playerId The ID of the player whose current bet commitment is being set.
|
||||
* @param amount The new bet commitment amount to be set for the specified player.
|
||||
*/
|
||||
public void setCurrentBetCommitment(PlayerId playerId, int amount) {
|
||||
playerBetCommitments.put(playerId, amount);
|
||||
}
|
||||
|
||||
// CARDS
|
||||
// Cards
|
||||
|
||||
/**
|
||||
* Gives hole cards to a specific player based on their ID. This method takes two Card objects
|
||||
* representing the player's hole cards and adds them to the holeCards map under the player's
|
||||
* ID.
|
||||
*
|
||||
* @param playerId The ID of the player to whom the hole cards are being given.
|
||||
* @param c1 The first Card object representing one of the player's hole cards.
|
||||
* @param c2 The second Card object representing the other hole card for the player.
|
||||
* Gives (overwrites) the two hole cards for the given player. Ensures stable
|
||||
* list identity
|
||||
* (important if other code holds references).
|
||||
*/
|
||||
public void giveHoleCards(PlayerId playerId, Card c1, Card c2) {
|
||||
|
||||
List<Card> cards = new ArrayList<>();
|
||||
List<Card> cards = holeCards.computeIfAbsent(playerId, k -> new ArrayList<>());
|
||||
cards.clear();
|
||||
cards.add(c1);
|
||||
cards.add(c2);
|
||||
|
||||
holeCards.put(playerId, cards);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a community card to the game state. This method takes a Card object representing a
|
||||
* community card and adds it to the list of community cards on the table.
|
||||
*
|
||||
* @param card The Card object representing the community card to be added to the game state.
|
||||
*/
|
||||
public void addCommunityCard(Card card) {
|
||||
communityCards.add(card);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the community cards by clearing the list of community cards. This method is typically
|
||||
* called at the start of a new hand to ensure that all community cards from the previous hand
|
||||
* are removed from the game state.
|
||||
*/
|
||||
public void resetCommunityCards() {
|
||||
communityCards.clear();
|
||||
}
|
||||
|
||||
// TURN MANAGEMENT
|
||||
|
||||
/**
|
||||
* Advances the turn to the next player in the players list. This method updates the
|
||||
* currentPlayerIndex by incrementing it and wrapping around to the start of the list if
|
||||
* necessary, ensuring that the turn order is maintained correctly throughout the game.
|
||||
*/
|
||||
// Turn / dealer management
|
||||
public void nextPlayer() {
|
||||
if (playerOrder.isEmpty()) {
|
||||
currentPlayerIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
int start = currentPlayerIndex;
|
||||
|
||||
do {
|
||||
currentPlayerIndex = (currentPlayerIndex + 1) % playerOrder.size();
|
||||
|
||||
Player p = getCurrentPlayer();
|
||||
|
||||
if (!p.isFolded() && !p.isAllIn()) {
|
||||
if (canPlayerAct(currentPlayerIndex)) {
|
||||
return;
|
||||
}
|
||||
|
||||
} while (currentPlayerIndex != start);
|
||||
}
|
||||
|
||||
// Dealer Rotation
|
||||
private boolean canPlayerAct(int index) {
|
||||
if (index < 0 || index >= playerOrder.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PlayerId id = playerOrder.get(index);
|
||||
Player p = players.get(id);
|
||||
return p != null && !p.isFolded() && !p.isAllIn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates the dealer position to the next player in the players list. This method updates the
|
||||
* dealerIndex by incrementing it and wrapping around to the start of the list if necessary,
|
||||
* ensuring that the dealer position rotates correctly after each hand.
|
||||
*/
|
||||
public void rotateDealer() {
|
||||
dealerIndex = (dealerIndex + 1) % playerOrder.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current dealer based on the dealerIndex. This method returns the Player object
|
||||
* corresponding to the current dealer position in the players list.
|
||||
*
|
||||
* @return The Player object representing the current dealer.
|
||||
*/
|
||||
public Player getDealer() {
|
||||
PlayerId id = playerOrder.get(dealerIndex);
|
||||
return players.get(id);
|
||||
}
|
||||
|
||||
// HAND RESET
|
||||
// Hand reset / lifecycle
|
||||
|
||||
/**
|
||||
* Starts a new hand by resetting the game state for the next round of poker. This method sets
|
||||
* the handActive flag to true, resets the game phase to PREFLOP, clears the pot, resets all
|
||||
* player bets and bet commitments, clears the community cards, and initializes a new shuffled
|
||||
* deck of cards for the new hand.
|
||||
* Starts a new hand by resetting the game state for the next round of poker.
|
||||
*
|
||||
* <p>
|
||||
* This:
|
||||
*
|
||||
* <ul>
|
||||
* <li>sets {@code phase=PREFLOP}
|
||||
* <li>resets pot/bets/commitments
|
||||
* <li>clears community + hole cards
|
||||
* <li>creates & shuffles a new deck
|
||||
* </ul>
|
||||
*/
|
||||
public void startNewHand() {
|
||||
|
||||
handActive = true;
|
||||
phase = GamePhase.PREFLOP;
|
||||
|
||||
@@ -455,6 +315,10 @@ public class GameState {
|
||||
resetCommunityCards();
|
||||
holeCards.clear();
|
||||
|
||||
for (PlayerId id : playerOrder) {
|
||||
holeCards.put(id, new ArrayList<>());
|
||||
}
|
||||
|
||||
for (Player player : players.values()) {
|
||||
player.setFolded(false);
|
||||
}
|
||||
@@ -462,28 +326,13 @@ public class GameState {
|
||||
deck = new Deck();
|
||||
deck.shuffle();
|
||||
|
||||
currentPlayerIndex = (dealerIndex + 1) % playerOrder.size();
|
||||
setCurrentPlayerToPreflopFirstToAct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Folds a player in the current hand. This method adds the specified player's ID to the set of
|
||||
* folded players, indicating that the player has folded and is no longer active in the current
|
||||
* hand.
|
||||
*
|
||||
* @param playerId The ID of the player who is folding.
|
||||
*/
|
||||
public void foldPlayer(PlayerId playerId) {
|
||||
getPlayer(playerId).setFolded(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific player has folded in the current hand. This method checks if the
|
||||
* specified player's ID is present in the set of folded players, indicating that the player has
|
||||
* folded.
|
||||
*
|
||||
* @param playerId The ID of the player to check for folding status.
|
||||
* @return true if the player has folded, false otherwise.
|
||||
*/
|
||||
public boolean isFolded(PlayerId playerId) {
|
||||
return getPlayer(playerId).isFolded();
|
||||
}
|
||||
|
||||
+28
-15
@@ -21,6 +21,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketException;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
@@ -50,29 +51,21 @@ public class SessionReader implements Runnable {
|
||||
RawRequest rawRequest = null;
|
||||
RequestContext requestContext = null;
|
||||
try {
|
||||
// Step 1: Read from transport
|
||||
rawPacket = transport.read();
|
||||
session.updateLastInboundActivity();
|
||||
logger.debug("Recieved: {}", rawPacket);
|
||||
requestContext = new RequestContext(session.getId(), rawPacket.requestId());
|
||||
|
||||
// Step 2: Syntax validation and conversion into transport object
|
||||
rawRequest = ProtocolParser.parse(rawPacket.payload());
|
||||
logger.debug("Parsed request to {}", rawRequest);
|
||||
|
||||
PrimitiveRequest primitiveRequest =
|
||||
new PrimitiveRequest(
|
||||
requestContext, rawRequest.command(), rawRequest.parameters());
|
||||
logger.debug("Converted to {}", primitiveRequest);
|
||||
|
||||
// Step 3: Parse into Request and execute Request
|
||||
Request request = dispatcher.parse(primitiveRequest);
|
||||
router.execute(request);
|
||||
processRawPacket(rawPacket);
|
||||
} catch (EOFException e) {
|
||||
logger.info("Client disconnected");
|
||||
eventBus.publish(new DisconnectEvent(session.getId()));
|
||||
break;
|
||||
|
||||
} catch (SocketException e) {
|
||||
// Connection reset or other socket-level error — treat as client disconnect
|
||||
logger.info("Client socket error / connection reset: {}", e.getMessage());
|
||||
eventBus.publish(new DisconnectEvent(session.getId()));
|
||||
break;
|
||||
|
||||
} catch (TokenizerException | ProtocolParserException e) {
|
||||
logger.error("Error occured while parsing request. RawPacket: {}", rawPacket, e);
|
||||
|
||||
@@ -117,6 +110,26 @@ public class SessionReader implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
private void processRawPacket(RawPacket rawPacket)
|
||||
throws TokenizerException,
|
||||
ProtocolParserException,
|
||||
UnknownCommandException,
|
||||
ResponseDispatchException,
|
||||
MissingParameterException,
|
||||
IOException {
|
||||
RawRequest rawRequest = ProtocolParser.parse(rawPacket.payload());
|
||||
logger.debug("Parsed request to {}", rawRequest);
|
||||
|
||||
RequestContext requestContext = new RequestContext(session.getId(), rawPacket.requestId());
|
||||
|
||||
PrimitiveRequest primitiveRequest =
|
||||
new PrimitiveRequest(requestContext, rawRequest.command(), rawRequest.parameters());
|
||||
logger.debug("Converted to {}", primitiveRequest);
|
||||
|
||||
Request request = dispatcher.parse(primitiveRequest);
|
||||
router.execute(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helperfunction to send ErrorResponse to client
|
||||
*
|
||||
|
||||
@@ -113,7 +113,8 @@
|
||||
<!-- Leave blank -->
|
||||
</VBox>
|
||||
|
||||
<fx:include source="components/chatui/chatbox.fxml" GridPane.columnIndex="2"/>
|
||||
<!-- RIGHT SIDE (Chat Box) -->
|
||||
<AnchorPane fx:id="chatContainer" GridPane.columnIndex="2" />
|
||||
|
||||
</children>
|
||||
</GridPane>
|
||||
|
||||
@@ -82,24 +82,24 @@
|
||||
</Button>
|
||||
|
||||
<!-- Username input and login button -->
|
||||
<HBox alignment="CENTER_LEFT" spacing="10"
|
||||
GridPane.rowIndex="0"
|
||||
GridPane.columnIndex="0"
|
||||
GridPane.halignment="LEFT"
|
||||
GridPane.valignment="TOP">
|
||||
<GridPane.margin>
|
||||
<!-- place below the 'CREATE A LOBBY' button -->
|
||||
<Insets top="100" left="20" />
|
||||
</GridPane.margin>
|
||||
<TextField fx:id="usernameField"
|
||||
promptText="Username"
|
||||
styleClass="gray-input-field"
|
||||
maxWidth="180" />
|
||||
<Button text="Login"
|
||||
fx:id="loginButton"
|
||||
onAction="#handleLoginButton"
|
||||
styleClass="button-create-lobby" />
|
||||
</HBox>
|
||||
<!-- <HBox alignment="CENTER_LEFT" spacing="10"-->
|
||||
<!-- GridPane.rowIndex="0"-->
|
||||
<!-- GridPane.columnIndex="0"-->
|
||||
<!-- GridPane.halignment="LEFT"-->
|
||||
<!-- GridPane.valignment="TOP">-->
|
||||
<!-- <GridPane.margin>-->
|
||||
<!-- <!– place below the 'CREATE A LOBBY' button –>-->
|
||||
<!-- <Insets top="100" left="20" />-->
|
||||
<!-- </GridPane.margin>-->
|
||||
<!-- <TextField fx:id="usernameField"-->
|
||||
<!-- promptText="Username"-->
|
||||
<!-- styleClass="gray-input-field"-->
|
||||
<!-- maxWidth="180" />-->
|
||||
<!-- <Button text="Login"-->
|
||||
<!-- fx:id="loginButton"-->
|
||||
<!-- onAction="#handleLoginButton"-->
|
||||
<!-- styleClass="button-create-lobby" />-->
|
||||
<!-- </HBox>-->
|
||||
|
||||
<VBox fx:id="casinoTable"
|
||||
alignment="CENTER"
|
||||
@@ -126,7 +126,7 @@
|
||||
</VBox>
|
||||
|
||||
<!-- RIGHT SIDE (Chat Box) -->
|
||||
<fx:include source="components/chatui/chatbox.fxml" GridPane.columnIndex="2"/>
|
||||
<AnchorPane fx:id="chatContainer" GridPane.columnIndex="2" />
|
||||
</children>
|
||||
</GridPane>
|
||||
</AnchorPane>
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
<!-- Eingabebereich -->
|
||||
<HBox fx:id="controlBar" spacing="10">
|
||||
<!-- TODO: Größe des TextFields dynamisch anpassen -->
|
||||
<TextField fx:id="inputField" promptText="Nachricht eingeben..." styleClass="gray-input-field" HBox.hgrow="ALWAYS" />
|
||||
<Button fx:id="sendButton" styleClass="yellow-button" text="SENDEN" />
|
||||
<TextField fx:id="inputField" promptText="Enter your message..." styleClass="gray-input-field" HBox.hgrow="ALWAYS" />
|
||||
<Button fx:id="sendButton" styleClass="yellow-button" text="SEND" />
|
||||
</HBox>
|
||||
</children>
|
||||
|
||||
|
||||
+15
-30
@@ -88,16 +88,8 @@ public class GameControllerTest {
|
||||
game.playerCall(PlayerId.of("Jona"));
|
||||
game.playerRaise(PlayerId.of("Lars"), 1200);
|
||||
|
||||
game.dealFlop();
|
||||
|
||||
List<Card> board = game.getCommunityCards();
|
||||
assertEquals(3, board.size());
|
||||
|
||||
game.dealTurn();
|
||||
assertEquals(4, game.getCommunityCards().size());
|
||||
|
||||
game.dealRiver();
|
||||
assertEquals(5, game.getCommunityCards().size());
|
||||
assertTrue(board.size() >= 3 && board.size() <= 5);
|
||||
|
||||
for (Card c : game.getCommunityCards()) {
|
||||
|
||||
@@ -113,7 +105,7 @@ public class GameControllerTest {
|
||||
|
||||
assertNotNull(mathis);
|
||||
|
||||
assertEquals(5, game.getCommunityCards().size());
|
||||
assertTrue(game.getCommunityCards().size() >= 3);
|
||||
|
||||
assertEquals(4, game.getState().getPlayers().size());
|
||||
|
||||
@@ -189,15 +181,13 @@ public class GameControllerTest {
|
||||
|
||||
assertTrue(potAfter >= potBefore, "The pot must be larger after actions");
|
||||
|
||||
game.dealFlop();
|
||||
|
||||
assertEquals(3, game.getCommunityCards().size(), "The flop must have 3 cards");
|
||||
|
||||
game.dealTurn();
|
||||
game.playerCall(PlayerId.of("Julian"));
|
||||
|
||||
assertEquals(4, game.getCommunityCards().size(), "A turn must result in 4 cards");
|
||||
|
||||
game.dealRiver();
|
||||
game.playerCall(PlayerId.of("Jona"));
|
||||
|
||||
assertEquals(5, game.getCommunityCards().size(), "The river must consist of 5 cards");
|
||||
|
||||
@@ -291,16 +281,13 @@ public class GameControllerTest {
|
||||
game.playerCall(PlayerId.of("Julian"));
|
||||
game.playerCall(PlayerId.of("Jona"));
|
||||
|
||||
game.dealFlop();
|
||||
assertTrue(game.getCommunityCards().size() >= 3);
|
||||
|
||||
game.playerCall(PlayerId.of("Julian"));
|
||||
game.playerFold(PlayerId.of("Jona"));
|
||||
for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) {
|
||||
game.playerCall(PlayerId.of("Julian"));
|
||||
}
|
||||
|
||||
game.dealTurn();
|
||||
game.playerCall(PlayerId.of("Julian"));
|
||||
|
||||
game.dealRiver();
|
||||
game.playerCall(PlayerId.of("Julian"));
|
||||
assertEquals(5, game.getCommunityCards().size());
|
||||
|
||||
List<Card> board = game.getCommunityCards();
|
||||
|
||||
@@ -378,13 +365,12 @@ public class GameControllerTest {
|
||||
|
||||
assertTrue(game.getState().getPot().getAmount() >= potBefore);
|
||||
|
||||
game.dealFlop();
|
||||
assertEquals(3, game.getCommunityCards().size());
|
||||
|
||||
game.dealTurn();
|
||||
game.playerCall(PlayerId.of("Julian"));
|
||||
assertEquals(4, game.getCommunityCards().size());
|
||||
|
||||
game.dealRiver();
|
||||
game.playerCall(PlayerId.of("Mathis"));
|
||||
assertEquals(5, game.getCommunityCards().size());
|
||||
|
||||
Set<String> boardSeen = new HashSet<>();
|
||||
@@ -462,13 +448,12 @@ public class GameControllerTest {
|
||||
|
||||
assertTrue(activePlayers >= 1, "At least one player must remain");
|
||||
|
||||
game.dealFlop();
|
||||
assertEquals(3, game.getCommunityCards().size());
|
||||
assertTrue(game.getCommunityCards().size() >= 3);
|
||||
|
||||
game.dealTurn();
|
||||
assertEquals(4, game.getCommunityCards().size());
|
||||
for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) {
|
||||
game.playerCall(PlayerId.of("BigStack"));
|
||||
}
|
||||
|
||||
game.dealRiver();
|
||||
assertEquals(5, game.getCommunityCards().size());
|
||||
|
||||
Set<String> allCards = new HashSet<>();
|
||||
|
||||
Reference in New Issue
Block a user