Fix: checkstyle

This commit is contained in:
Julian Kropff
2026-04-13 02:40:20 +02:00
parent 52b5d5b8c2
commit 8c2130d0d6
16 changed files with 497 additions and 344 deletions
@@ -10,11 +10,8 @@ import org.apache.logging.log4j.Logger;
/**
* Entry point and bootstrap helper for the Casono client application.
*
* <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)
* <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 {
@@ -81,7 +78,8 @@ public class ClientApp {
ClientService clientService = new ClientService(host, port);
setSharedClientService(clientService);
java.util.concurrent.ExecutorService bg = java.util.concurrent.Executors.newSingleThreadExecutor(
java.util.concurrent.ExecutorService bg =
java.util.concurrent.Executors.newSingleThreadExecutor(
r -> {
Thread t = new Thread(r);
t.setDaemon(true);
@@ -115,16 +113,15 @@ public class ClientApp {
});
} catch (RuntimeException e) {
LOGGER.warn(
"Could not establish initial connection for startup login: {}",
e.getMessage());
"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 });
Launcher.main(new String[] {arg, username});
} else {
Launcher.main(new String[] { arg });
Launcher.main(new String[] {arg});
}
}
@@ -158,8 +158,8 @@ 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.
* 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.
*/
@@ -18,7 +18,10 @@ public class GameService {
* @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");
if (client == null) {
throw new IllegalArgumentException("GameClient must not be null");
}
this.client = client;
LOG.info("GameService created");
}
@@ -47,10 +50,16 @@ public class GameService {
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));
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;
}
@@ -78,7 +87,8 @@ public class GameService {
/**
* Retrieves the list of players currently in the game.
*
* @return A list of Player objects representing the players in the game. A list of Player objects representing the players in the game.
* @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();
@@ -92,22 +102,20 @@ public class GameService {
*/
public Player getWinner() {
ensureState();
if (state.winnerIndex < 0 || state.players == null || state.winnerIndex >= state.players.size()) {
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.
*/
/** Retrieves the current phase of the game. */
public void call() {
client.sendCall();
}
/**
* Retrieves the current phase of the game.
*/
/** Retrieves the current phase of the game. */
public void fold() {
client.sendFold();
}
@@ -130,9 +138,7 @@ public class GameService {
client.sendRaise(amount);
}
/**
* Ensures 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("GameService used before any successful refresh()");
@@ -26,10 +26,8 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* The ClientService class is responsible for managing the connection to the
* server, sending
* commands, and receiving responses. It uses a TcpTransport to communicate with
* the server and an
* The ClientService class is responsible for managing the connection to the server, sending
* commands, and receiving responses. It uses a TcpTransport to communicate with the server and an
* ExecutorService to handle asynchronous requests.
*/
public class ClientService {
@@ -43,17 +41,17 @@ public class ClientService {
private final AtomicInteger idGenerator;
private Logger logger;
private final Map<Integer, ArrayBlockingQueue<ParsedResponse>> pendingResponses = new ConcurrentHashMap<>();
private final CopyOnWriteArrayList<Consumer<List<String>>> eventListeners = new CopyOnWriteArrayList<>();
private final Map<Integer, ArrayBlockingQueue<ParsedResponse>> pendingResponses =
new ConcurrentHashMap<>();
private final CopyOnWriteArrayList<Consumer<List<String>>> eventListeners =
new CopyOnWriteArrayList<>();
private Thread readerThread = null;
private final AtomicBoolean running = new AtomicBoolean(false);
private static final int READER_JOIN_TIMEOUT_MS = 500;
/**
* Constructs a ClientService with the given server IP and port. It establishes
* a socket
* connection to the server and initializes the TcpTransport and ExecutorService
* for
* Constructs a ClientService with the given server IP and port. It establishes a socket
* connection to the server and initializes the TcpTransport and ExecutorService for
* communication.
*
* @param ip The IP address of the server to connect to.
@@ -81,7 +79,8 @@ public class ClientService {
private void startReaderThread() {
running.set(true);
readerThread = new Thread(
readerThread =
new Thread(
() -> {
while (running.get()) {
try {
@@ -139,8 +138,9 @@ public class ClientService {
continue;
}
if (trimmed.isEmpty())
if (trimmed.isEmpty()) {
continue;
}
line = line.replaceFirst("^\\s+", "");
trimmed = line.trim();
@@ -210,11 +210,9 @@ public class ClientService {
}
}
private record ParsedPacket(boolean success, List<String> lines, Deque<String> blockStack) {
}
private record ParsedPacket(boolean success, List<String> lines, Deque<String> blockStack) {}
private record StatusCheckResult(boolean success) {
}
private record StatusCheckResult(boolean success) {}
private boolean isContainerStart(String token) {
return token.equals("LOBBIES")
@@ -228,8 +226,7 @@ public class ClientService {
}
/**
* Constructs a ClientService in offline mode. No network connection will be
* attempted and calls
* Constructs a ClientService in offline mode. No network connection will be attempted and calls
* to processCommand will throw a RuntimeException.
*
* @param offline true to create an offline (no-network) client service
@@ -242,21 +239,19 @@ public class ClientService {
this.executor = Executors.newSingleThreadExecutor();
}
/**
* Returns true if this ClientService is running in offline mode (no network).
*/
/** Returns true if this ClientService is running in offline mode (no network). */
public boolean isOffline() {
return offlineMode;
}
// Allow primitive values to contain hyphens (UUIDs) in addition to
// digits/words/colons
static Pattern responseRex = Pattern.compile(
static Pattern responseRex =
Pattern.compile(
"(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[+-]?[-\\d\\w:]+))");
/**
* Removes escape characters from a string, specifically converting escaped
* single quotes (\')
* Removes escape characters from a string, specifically converting escaped single quotes (\')
* back to regular single quotes (').
*
* @param input The escaped string to process.
@@ -267,10 +262,8 @@ public class ClientService {
}
/**
* Converts a list of raw string parameters into a list of
* {@link RequestParameter} objects. It
* uses a regex matcher to distinguish between quoted strings (which are
* unescaped) and
* Converts a list of raw string parameters into a list of {@link RequestParameter} objects. It
* uses a regex matcher to distinguish between quoted strings (which are unescaped) and
* primitive values.
*
* @param input A list of raw strings to be parsed.
@@ -295,12 +288,10 @@ public class ClientService {
.toList();
}
private static record ParsedResponse(boolean success, List<String> lines) {
}
private static record ParsedResponse(boolean success, List<String> lines) {}
/**
* Register an event listener that receives unsolicited event payload lines (no
* status prefix).
* Register an event listener that receives unsolicited event payload lines (no status prefix).
*/
public void addEventListener(Consumer<List<String>> listener) {
eventListeners.add(listener);
@@ -311,17 +302,13 @@ public class ClientService {
}
/**
* Sends a command to the server and processes the multi-line response. It
* handles the protocol
* handshake (expecting +OK), strips leading tabs from response lines, and
* collects them until
* Sends a command to the server and processes the multi-line response. It handles the protocol
* handshake (expecting +OK), strips leading tabs from response lines, and collects them until
* the "END" marker is reached.
*
* @param message The raw command string to be sent to the transport layer.
* @return A list of response lines received from the server (excluding protocol
* markers).
* @throws RuntimeException if the server responds with an error or if a
* communication failure
* @return A list of response lines received from the server (excluding protocol markers).
* @throws RuntimeException if the server responds with an error or if a communication failure
* occurs.
*/
protected List<String> processCommand(String message) {
@@ -333,7 +320,8 @@ public class ClientService {
ArrayBlockingQueue<ParsedResponse> q = new ArrayBlockingQueue<>(1);
pendingResponses.put(reqId, q);
Future<?> writeFuture = executor.submit(
Future<?> writeFuture =
executor.submit(
() -> {
try {
clienttcptransport.write(new RawPacket(reqId, message));
@@ -372,15 +360,11 @@ public class ClientService {
}
/**
* Helper method to send a request to the server using the ExecutorService. It
* submits the
* request as a Runnable task and waits for its completion. If the task is
* interrupted or
* encounters an execution exception, it throws a RuntimeException with the
* appropriate cause.
* Helper method to send a request to the server using the ExecutorService. It submits the
* request as a Runnable task and waits for its completion. If the task is interrupted or
* encounters an execution exception, it throws a RuntimeException with the appropriate cause.
*
* @param request The Runnable task representing the request to be sent to the
* server.
* @param request The Runnable task representing the request to be sent to the server.
*/
private void sendRequest(Runnable request) {
Future<?> future = executor.submit(request);
@@ -394,12 +378,9 @@ public class ClientService {
}
/**
* Helper method to extract the cause of an exception and return it as a
* RuntimeException. If
* the cause is null, it returns the original exception as a RuntimeException.
* If the cause is
* already a RuntimeException, it returns it directly. Otherwise, it wraps the
* cause in a new
* Helper method to extract the cause of an exception and return it as a RuntimeException. If
* the cause is null, it returns the original exception as a RuntimeException. If the cause is
* already a RuntimeException, it returns it directly. Otherwise, it wraps the cause in a new
* RuntimeException and returns it.
*
* @param e The exception from which to extract the cause.
@@ -417,10 +398,8 @@ public class ClientService {
}
/**
* Closes the socket connection to the server and shuts down the
* ExecutorService. It also closes
* the TcpTransport used for communication. If any IOException occurs during
* this process, it
* Closes the socket connection to the server and shuts down the ExecutorService. It also closes
* the TcpTransport used for communication. If any IOException occurs during this process, it
* prints the exception to the console.
*/
public void closeSocket() {
@@ -13,20 +13,13 @@ import java.util.logging.Logger;
/**
* 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) ...
* <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
* <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 {
@@ -37,8 +30,10 @@ public class GameClient {
private final int gameId;
public GameClient(ClientService client, int gameId) {
if (client == null)
if (client == null) {
throw new IllegalArgumentException("ClientService must not be null");
}
this.client = client;
this.gameId = gameId;
LOG.info(() -> "GameClient initialized for gameId=" + gameId);
@@ -78,7 +73,8 @@ public class GameClient {
try {
GameState state = parseGameState(joined);
LOG.info(
() -> "Parsed state: phase="
() ->
"Parsed state: phase="
+ state.phase
+ " pot="
+ state.pot
@@ -119,8 +115,9 @@ public class GameClient {
ParseState state = new ParseState();
for (String raw : input.split("\n")) {
String line = raw.trim();
if (line.isEmpty() || line.startsWith("+OK"))
if (line.isEmpty() || line.startsWith("+OK")) {
continue;
}
if (!parseGlobalField(s, line)
&& !parsePlayerStructure(s, state, line)
@@ -170,8 +167,11 @@ public class GameClient {
state.insidePlayer = true;
return true;
}
if (line.equals("CARDS"))
if (line.equals("CARDS")) {
return true;
}
if (line.equals("CARD")) {
state.currentCard = new Card("", "");
if (state.insidePlayer && state.currentPlayer != null) {
@@ -181,6 +181,7 @@ public class GameClient {
}
return true;
}
if (line.equals("END")) {
if (state.currentCard != null) {
state.currentCard = null;
@@ -194,8 +195,10 @@ public class GameClient {
}
private boolean parseCardData(ParseState state, String line) {
if (state.currentCard == null)
if (state.currentCard == null) {
return false;
}
if (line.startsWith("VALUE=")) {
state.currentCard.setValue(value(line));
return true;
@@ -208,24 +211,30 @@ public class GameClient {
}
private boolean parsePlayerData(ParseState state, String line) {
if (state.currentPlayer == null)
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;
}
@@ -244,8 +253,10 @@ public class GameClient {
}
private static PlayerState parseState(String s) {
if (s == null)
if (s == null) {
return PlayerState.ACTIVE;
}
return switch (s.trim().toUpperCase()) {
case "ACTIVE" -> PlayerState.ACTIVE;
case "FOLDED" -> PlayerState.FOLDED;
@@ -255,8 +266,10 @@ public class GameClient {
}
private static String extractValue(String joined, String key) {
if (joined == null)
if (joined == null) {
return null;
}
for (String raw : joined.split("\n")) {
String line = raw.trim();
if (line.startsWith(key + "=")) {
@@ -46,10 +46,12 @@ 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.
* 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.
* @param chatController The ChatController instance responsible for handling chat logic and
* communication with the server.
*/
public ChatBoxController(String username, ChatController chatController) {
this.username = username;
@@ -136,6 +136,8 @@ 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;
@@ -155,7 +157,10 @@ public class CasinoGameController {
* @return A string key representing the card, formatted as "suit:value".
*/
private String cardKey(Card c) {
if (c == null) return "null";
if (c == null) {
return "null";
}
String suit = (c.getSuit() == null) ? "" : c.getSuit().toLowerCase();
String value = (c.getValue() == null) ? "" : c.getValue().toLowerCase();
return suit + ":" + value;
@@ -238,11 +243,15 @@ public class CasinoGameController {
}
/**
* Set the context for the chat functionality by providing the username, ClientService, and lobby ID.
* 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.
* @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;
@@ -252,7 +261,8 @@ public class CasinoGameController {
}
/**
* Initialize the chat UI if all necessary information (username, ClientService, and lobby ID) is available and the chat has not already been initialized.
* 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) {
@@ -278,8 +288,8 @@ public class CasinoGameController {
Scene scene = new Scene(root);
chatStage.setScene(scene);
chatStage.setWidth(400);
chatStage.setHeight(600);
chatStage.setWidth(CHAT_WIDTH);
chatStage.setHeight(CHAT_HEIGHT);
chatStage.setOnCloseRequest(event -> chatInitialized = false);
@@ -435,8 +445,8 @@ public class CasinoGameController {
return;
}
CompletableFuture
.supplyAsync(() -> {
CompletableFuture.supplyAsync(
() -> {
try {
return gameService.refresh();
} catch (Exception e) {
@@ -445,18 +455,20 @@ public class CasinoGameController {
return null;
}
})
.thenAccept(state -> {
.thenAccept(
state -> {
if (state == null) {
LOGGER.info("No state received yet.");
return;
}
javafx.application.Platform.runLater(() -> {
javafx.application.Platform.runLater(
() -> {
applyState(state);
});
})
.exceptionally(ex -> {
.exceptionally(
ex -> {
LOGGER.severe("UI update Error: " + ex.getMessage());
ex.printStackTrace();
return null;
@@ -471,11 +483,17 @@ public class CasinoGameController {
*/
private void applyState(GameState s) {
if (s == null) return;
if (s == null) {
return;
}
LOGGER.info("applyState -> phase=" + s.phase +
" pot=" + s.pot +
" players=" + (s.players != null ? s.players.size() : 0));
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();
@@ -507,17 +525,25 @@ public class CasinoGameController {
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));
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.
* @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()) {
@@ -536,10 +562,13 @@ public class CasinoGameController {
}
/**
* Retrieve the hole cards of the current player based on their PlayerId and the list of players in the game state.
* 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.
* @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) {
@@ -613,9 +642,13 @@ public class CasinoGameController {
}
/**
* 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.
* 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 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.
* @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> players) {
if (players == null || players.isEmpty()) {
@@ -626,10 +659,15 @@ public class CasinoGameController {
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());
LOGGER.info(
"updatePlayers: total="
+ players.size()
+ " myPlayerId="
+ myPlayerId
+ " meFound="
+ meFound
+ " opponents="
+ opponents.size());
setOpponent(player1Controller, opponents, 0);
setOpponent(player2Controller, opponents, 1);
@@ -641,19 +679,25 @@ public class CasinoGameController {
}
/**
* 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.
* 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.
* @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;
if (p == null) {
continue;
}
PlayerId pid = p.getId();
boolean isMe = myPlayerId != null && pid != null && pid.equals(myPlayerId);
@@ -674,34 +718,57 @@ public class CasinoGameController {
}
/**
* Set the opponent player information in the specified PlayerStatusController based on the provided list of opponents and the index of the opponent to display.
* 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 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;
if (slot == null) {
return;
}
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.
* 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) {}
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.
* 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);
if (player1Controller != null) {
player1Controller.setPlayer(null);
}
if (player2Controller != null) {
player2Controller.setPlayer(null);
}
if (player3Controller != null) {
player3Controller.setPlayer(null);
}
safeRefresh(player1Controller);
safeRefresh(player2Controller);
safeRefresh(player3Controller);
@@ -713,9 +780,17 @@ public class CasinoGameController {
* @param s The current game state containing the dealer index.
*/
private void highlightDealer(GameState s) {
if (player1Controller != null) player1Controller.setDealer(false);
if (player2Controller != null) player2Controller.setDealer(false);
if (player3Controller != null) player3Controller.setDealer(false);
if (player1Controller != null) {
player1Controller.setDealer(false);
}
if (player2Controller != null) {
player2Controller.setDealer(false);
}
if (player3Controller != null) {
player3Controller.setDealer(false);
}
myDealerIcon.setVisible(false);
@@ -737,23 +812,31 @@ public class CasinoGameController {
}
java.util.List<Player> opponents = buildOpponents(s.players);
if (opponents.size() > 0 && samePlayer(opponents.get(0), dealerPlayer) && player1Controller != null) {
if (opponents.size() > 0
&& samePlayer(opponents.get(0), dealerPlayer)
&& player1Controller != null) {
player1Controller.setDealer(true);
}
if (opponents.size() > 1 && samePlayer(opponents.get(1), dealerPlayer) && player2Controller != null) {
if (opponents.size() > 1
&& samePlayer(opponents.get(1), dealerPlayer)
&& player2Controller != null) {
player2Controller.setDealer(true);
}
if (opponents.size() > 2 && samePlayer(opponents.get(2), dealerPlayer) && player3Controller != null) {
if (opponents.size() > 2
&& samePlayer(opponents.get(2), dealerPlayer)
&& player3Controller != null) {
player3Controller.setDealer(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.
* 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.
* @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) {
@@ -1225,9 +1308,11 @@ public class CasinoGameController {
}
/**
* 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.
* 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.
* @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;
@@ -1238,9 +1323,11 @@ public class CasinoGameController {
}
/**
* Resolve the TaskbarController instance to be used for updating the taskbar with game information.
* 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.
* @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) {
@@ -30,10 +30,9 @@ public class CasinoGameUI extends Application {
private static final int DEFAULT_WIDTH = 1200;
private static final int DEFAULT_HEIGHT = 800;
private static final int GUEST_ID_LENGTH = 8;
/**
* Default constructor for the CasinoGameUI application.
*/
/** Default constructor for the CasinoGameUI application. */
public CasinoGameUI() {
// default no-arg constructor
}
@@ -66,24 +65,26 @@ public class CasinoGameUI extends Application {
}
/**
* The main entry point for the JavaFX application. This method is called after the application is
* 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.
* @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();
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.");
+ "Call CasinoGameUI.setClientService(...)"
+ " or start via ClientApp with a shared connection.");
}
String effectiveUsername = normalize(username);
@@ -94,13 +95,19 @@ public class CasinoGameUI extends Application {
}
if (effectiveUsername == null) {
effectiveUsername = "Guest-" + UUID.randomUUID().toString().substring(0, 8);
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));
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"));
@@ -136,13 +143,17 @@ public class CasinoGameUI extends Application {
* @return the normalized string, or null if the input is null or blank
*/
private static String normalize(String s) {
if (s == null) return null;
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.
* The main method serves as the entry point for the application. It launches the JavaFX
* application.
*
* @param args command line arguments (not used)
*/
@@ -361,7 +361,9 @@ public class TaskbarController {
}
if (state == null || state.currentBet <= 0) {
LOGGER.warn("Raise not possible: current bet is {}", state != null ? state.currentBet : null);
LOGGER.warn(
"Raise not possible: current bet is {}",
state != null ? state.currentBet : null);
return;
}
@@ -9,6 +9,7 @@ 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;
@@ -17,7 +18,6 @@ import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.Node;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import org.apache.logging.log4j.LogManager;
@@ -108,7 +108,8 @@ public class CasinomainuiController {
}
/**
* Initializes the chat UI if a valid ClientService is available. If the client is offline or the
* Initializes the chat UI if a valid ClientService is available. If the client is offline or
* the
*
* @param clientService
*/
@@ -134,7 +135,8 @@ public class CasinomainuiController {
}
/**
* Resolves the username to be used in the chat. It first checks for a shared username set at the
* 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.
*/
@@ -143,7 +145,9 @@ public class CasinomainuiController {
if (shared != null && !shared.isBlank()) {
return shared.trim();
}
if (usernameField != null && usernameField.getText() != null && !usernameField.getText().isBlank()) {
if (usernameField != null
&& usernameField.getText() != null
&& !usernameField.getText().isBlank()) {
return usernameField.getText().trim();
}
return "Guest";
@@ -2,7 +2,6 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import java.util.ArrayList;
import java.util.Collections;
@@ -30,6 +29,7 @@ public class LobbyButtonGridManager {
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);
@@ -553,16 +553,28 @@ public class LobbyButtonGridManager {
try {
var cs = lobbyClient.getClientService();
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI.setClientService(cs);
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI.setLobbyId(lobbyId);
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI
.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();
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, 8);
username =
"Guest-"
+ java.util
.UUID
.randomUUID()
.toString()
.substring(0, GUEST_ID_LENGTH);
}
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI.setUsername(username);
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);
new ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI()
.start(gameStage);
} catch (Exception e) {
LOGGER.error("Game UI failed: {}", e.getMessage());
@@ -78,7 +78,8 @@ public class GetGameStateHandler extends CommandHandler<GetGameStateRequest> {
return username.trim();
}
return userRegistry.getBySessionId(request.getSessionId())
return userRegistry
.getBySessionId(request.getSessionId())
.map(User::getName)
.map(String::trim)
.filter(name -> !name.isEmpty())
@@ -17,38 +17,24 @@ import java.util.Objects;
/**
* Response carrying a snapshot of the current game state using the project's wire format.
*
* Wire format (relevant parts):
* <p>Wire format (relevant parts):
*
* +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>+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
*
* 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).
* <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(Objects.requireNonNull(game, "game must not be null").getState(), requestingUsername));
public GetGameStateResponse(
RequestContext context, GameController game, String requestingUsername) {
super(
context,
buildBody(
Objects.requireNonNull(game, "game must not be null").getState(),
requestingUsername));
}
private static ResponseBody buildBody(GameState state, String requestingUsername) {
@@ -72,9 +58,14 @@ public class GetGameStateResponse extends SuccessResponse {
private static int computeGlobalCurrentBet(GameState state) {
int globalCurrentBet = 0;
for (Player p : state.getPlayers()) {
if (p == null) continue;
if (p == null) {
continue;
}
int b = state.getCurrentBet(p.getId());
if (b > globalCurrentBet) globalCurrentBet = b;
if (b > globalCurrentBet) {
globalCurrentBet = b;
}
}
return globalCurrentBet;
}
@@ -87,24 +78,34 @@ public class GetGameStateResponse extends SuccessResponse {
}
for (Card c : community) {
if (c == null) continue;
builder.block("CARD", card -> {
if (c == null) {
continue;
}
builder.block(
"CARD",
card -> {
card.param("VALUE", rankToWire(c.getRank()));
card.param("SUIT", suitToWire(c.getSuit()));
});
}
}
private static void appendPlayers(ResponseBodyBuilder builder, GameState state, String requestingUsername) {
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;
if (p == null) {
continue;
}
PlayerId pid = p.getId();
String name = (pid == null || pid.value() == null) ? "" : pid.value().trim();
builder.block("PLAYER", pblock -> {
builder.block(
"PLAYER",
pblock -> {
pblock.param("NAME", name);
pblock.param("CHIPS", p.getChips());
pblock.param("BET", pid == null ? 0 : state.getCurrentBet(pid));
@@ -115,8 +116,13 @@ public class GetGameStateResponse extends SuccessResponse {
if (hole != null) {
for (Card hc : hole) {
if (hc == null) continue;
pblock.block("CARD", cblock -> {
if (hc == null) {
continue;
}
pblock.block(
"CARD",
cblock -> {
cblock.param("VALUE", rankToWire(hc.getRank()));
cblock.param("SUIT", suitToWire(hc.getSuit()));
});
@@ -128,7 +134,10 @@ public class GetGameStateResponse extends SuccessResponse {
}
private static String rankToWire(Rank r) {
if (r == null) return "";
if (r == null) {
return "";
}
return switch (r) {
case TWO -> "2";
case THREE -> "3";
@@ -147,7 +156,10 @@ public class GetGameStateResponse extends SuccessResponse {
}
private static String suitToWire(Suit s) {
if (s == null) return "";
if (s == null) {
return "";
}
return switch (s) {
case HEARTS -> "H";
case DIAMONDS -> "D";
@@ -1,7 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.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.LobbyId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
@@ -11,13 +11,15 @@ import java.util.ArrayList;
import java.util.List;
/**
* RoundManager manages hand lifecycle and phase progression (PREFLOP -> FLOP -> TURN -> RIVER -> SHOWDOWN).
* 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>
* <li>progresses phases once betting round is finished</li>
* <li>deals community cards (3/1/1)</li>
* <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 {
@@ -56,10 +58,14 @@ public class RoundManager {
int target = state.getTableState().getCurrentBet();
for (Player p : state.getPlayers()) {
if (p == null) continue;
if (p == null) {
continue;
}
// be tolerant if codebase mixes status + boolean flags
if (p.getStatus() == PlayerStatus.FOLDED || p.isFolded()) continue;
if (p.getStatus() == PlayerStatus.FOLDED || p.isFolded()) {
continue;
}
int bet = state.getCurrentBet(p.getId());
if (bet < target && !p.isAllIn()) {
@@ -75,7 +81,9 @@ public class RoundManager {
case FLOP -> dealTurn(state);
case TURN -> dealRiver(state);
case RIVER -> showdown(state);
case SHOWDOWN -> { /* nothing */ }
case SHOWDOWN -> {
/* nothing */
}
}
}
@@ -92,7 +100,9 @@ public class RoundManager {
Deck deck = state.getDeck();
for (Player p : state.getPlayers()) {
if (p == null) continue;
if (p == null) {
continue;
}
PlayerId pid = p.getId();
Card c1 = deck.draw();
@@ -103,20 +113,28 @@ public class RoundManager {
}
/**
* 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)).
* 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;
if (playerList.size() < 2) {
return;
}
// pick first two non-folded players as SB/BB
Player sb = null;
Player bb = null;
for (Player p : playerList) {
if (p == null) continue;
if (p.isFolded()) continue;
if (p == null) {
continue;
}
if (p.isFolded()) {
continue;
}
if (sb == null) {
sb = p;
@@ -126,7 +144,9 @@ public class RoundManager {
}
}
if (sb == null || bb == null) return;
if (sb == null || bb == null) {
return;
}
sb.removeChips(SMALL_BLIND);
bb.removeChips(BIG_BLIND);
@@ -14,10 +14,12 @@ import java.util.Map;
* 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>
* <li>{@code currentBets} always contains an entry for every player in {@code playerOrder}.</li>
* <li>{@code holeCards} maps every player to a mutable list; if not present, it is created on demand.</li>
* <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 {
@@ -42,6 +44,8 @@ public class GameState {
private Deck deck;
private static final int DEALER_OFFSET = 3;
// Getters
public Collection<Player> getPlayers() {
List<Player> ordered = new ArrayList<>(playerOrder.size());
@@ -121,7 +125,7 @@ public class GameState {
}
// Heads-up: dealer (small blind) acts first preflop.
int first = (size == 2) ? dealerIndex : (dealerIndex + 3) % size;
int first = (size == 2) ? dealerIndex : (dealerIndex + DEALER_OFFSET) % size;
currentPlayerIndex = first;
if (!canPlayerAct(currentPlayerIndex)) {
nextPlayer();
@@ -180,8 +184,7 @@ public class GameState {
}
/**
* Reset bets to 0 for ALL players (do not clear the map).
* Also resets table current bet to 0.
* Reset bets to 0 for ALL players (do not clear the map). Also resets table current bet to 0.
*/
public void resetBets() {
for (PlayerId id : playerOrder) {
@@ -205,7 +208,10 @@ public class GameState {
// Player helpers
public Player getPlayer(PlayerId id) {
Player player = players.get(id);
if (player == null) throw new RuntimeException("Player not found: " + id);
if (player == null) {
throw new RuntimeException("Player not found: " + id);
}
return player;
}
@@ -221,8 +227,8 @@ public class GameState {
// Cards
/**
* Gives (overwrites) the two hole cards for the given player.
* Ensures stable list identity (important if other code holds references).
* 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 = holeCards.computeIfAbsent(playerId, k -> new ArrayList<>());
@@ -282,11 +288,12 @@ public class GameState {
* Starts a new hand by resetting the game state for the next round of poker.
*
* <p>This:
*
* <ul>
* <li>sets {@code phase=PREFLOP}</li>
* <li>resets pot/bets/commitments</li>
* <li>clears community + hole cards</li>
* <li>creates & shuffles a new deck</li>
* <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() {