Merge branch 'main' into feat/93-add-serverside-create-lobby-command
@@ -7,6 +7,12 @@ import org.apache.logging.log4j.Logger;
|
||||
|
||||
/** Main entry point for Casono application. Handles client and server startup. */
|
||||
public final class Main {
|
||||
|
||||
private static final int MIN_ARGS_FOR_USERNAME = 3;
|
||||
private static final int ARGS_COUNT_SERVER = 2;
|
||||
private static final int ARGS_COUNT_CLIENT_MIN = 2;
|
||||
private static final int ARGS_COUNT_CLIENT_WITH_USER = 3;
|
||||
|
||||
/**
|
||||
* Main entry point for Casono.
|
||||
*
|
||||
@@ -17,10 +23,13 @@ public final class Main {
|
||||
printUsage();
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
switch (args[0]) {
|
||||
case "server" -> ServerApp.start(args[1]);
|
||||
case "client" -> ClientApp.start(args[1]);
|
||||
case "client" -> {
|
||||
String address = args[1];
|
||||
String username = args.length >= MIN_ARGS_FOR_USERNAME ? args[2] : null;
|
||||
ClientApp.start(address, username);
|
||||
}
|
||||
default -> {
|
||||
printUsage();
|
||||
System.exit(1);
|
||||
@@ -29,12 +38,15 @@ public final class Main {
|
||||
}
|
||||
|
||||
private static boolean isValid(String[] args) {
|
||||
if (args.length != 2) {
|
||||
if (args.length < ARGS_COUNT_SERVER) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return switch (args[0]) {
|
||||
case "server", "client" -> true;
|
||||
case "server" -> args.length == ARGS_COUNT_SERVER;
|
||||
case "client" ->
|
||||
args.length == ARGS_COUNT_CLIENT_MIN
|
||||
|| args.length == ARGS_COUNT_CLIENT_WITH_USER;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
@@ -45,7 +57,7 @@ public final class Main {
|
||||
"""
|
||||
Usage:
|
||||
java -jar xyz.jar server <listenPort>
|
||||
java -jar xyz.jar client <serverIp>:<serverPort>
|
||||
java -jar xyz.jar client <serverIp>:<serverPort> [<username>]
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,51 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client;
|
||||
|
||||
/**
|
||||
* Entry point for the Casono client application. Handles client startup and connection parameters.
|
||||
*/
|
||||
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.network.LoginResult;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.Launcher;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Entry point for the Casono client application. Handles client startup and connection parameters.
|
||||
* Entry point and bootstrap helper for the Casono client application.
|
||||
*
|
||||
* <p>Default constructor for the 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.
|
||||
*/
|
||||
public class ClientApp {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(ClientApp.class);
|
||||
|
||||
/** Shared client connection used when a username is provided at startup. */
|
||||
private static volatile ClientService sharedClientService;
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
||||
private static void setSharedClientService(ClientService cs) {
|
||||
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 void start(String arg) {
|
||||
public static void start(String arg, String username) {
|
||||
String[] parts = arg.split(":", 2);
|
||||
if (parts.length != 2) {
|
||||
throw new IllegalArgumentException("Address must be in format <ip>:<port>.");
|
||||
@@ -36,16 +54,69 @@ public class ClientApp {
|
||||
int port = Integer.parseInt(parts[1]);
|
||||
|
||||
LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host);
|
||||
// Expose the chosen host/port to the UI via system properties so controllers
|
||||
// (which read System.getProperty("casono.server.host"/"casono.server.port"))
|
||||
// can obtain the correct connection information.
|
||||
System.setProperty("casono.server.host", host);
|
||||
System.setProperty("casono.server.port", Integer.toString(port));
|
||||
// Forward the original address argument to the launcher as well.
|
||||
Launcher.main(new String[] {arg});
|
||||
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();
|
||||
}
|
||||
});
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.warn(
|
||||
"Could not establish initial connection for startup login: {}",
|
||||
e.getMessage());
|
||||
// UI will create its own connection when it initializes
|
||||
}
|
||||
}
|
||||
|
||||
if (username != null && !username.isBlank()) {
|
||||
Launcher.main(new String[] {arg, username});
|
||||
} else {
|
||||
Launcher.main(new String[] {arg});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
start(args[0]);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,53 @@ package ch.unibas.dmi.dbis.cs108.casono.client.game;
|
||||
|
||||
/** Represents a playing card with a value and suit. */
|
||||
public class Card {
|
||||
public String value;
|
||||
public String suit;
|
||||
private String value;
|
||||
private String suit;
|
||||
|
||||
/**
|
||||
* Constructs a Card with the specified value and suit.
|
||||
*
|
||||
* @param value The value of the card (e.g., 2, 3, ..., 10, J, Q, K, A).
|
||||
* @param suit The suit of the card (e.g., Hearts, Diamonds, Clubs, Spades).
|
||||
*/
|
||||
public Card(String value, String suit) {
|
||||
this.value = value;
|
||||
this.suit = suit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the value of the card.
|
||||
*
|
||||
* @return The value of the card.
|
||||
*/
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the suit of the card.
|
||||
*
|
||||
* @return The suit of the card.
|
||||
*/
|
||||
public String getSuit() {
|
||||
return suit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of the card.
|
||||
*
|
||||
* @param value The value to set for the card.
|
||||
*/
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the suit of the card.
|
||||
*
|
||||
* @param suit The suit to set for the card.
|
||||
*/
|
||||
public void setSuit(String suit) {
|
||||
this.suit = suit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.game;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.GameClient;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Service class responsible for managing the game state and providing methods to interact with the
|
||||
* game.
|
||||
*/
|
||||
public class GameService {
|
||||
|
||||
private final GameClient client;
|
||||
private GameState state;
|
||||
|
||||
/**
|
||||
* Constructs a GameService with the given GameClient for communication with the server.
|
||||
*
|
||||
* @param client The GameClient instance used to send commands and receive responses from the
|
||||
* server.
|
||||
*/
|
||||
public GameService(GameClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh 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.
|
||||
*/
|
||||
public GameState refresh() {
|
||||
state = client.fetchGameState();
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current game state.
|
||||
*
|
||||
* @return The current GameState object representing the state of the game.
|
||||
*/
|
||||
public int getPot() {
|
||||
return state.pot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current game state.
|
||||
*
|
||||
* @return The current GameState object representing the state of the game.
|
||||
*/
|
||||
public List<Card> getCommunityCards() {
|
||||
return state.communityCards;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of players in the current game state.
|
||||
*
|
||||
* @return A list of Player objects representing the players in the current game state.
|
||||
*/
|
||||
public List<Player> getPlayers() {
|
||||
return state.players;
|
||||
}
|
||||
|
||||
/** Send a CALL command to the server to indicate that the player wants to call. */
|
||||
public void call() {
|
||||
client.sendCall();
|
||||
}
|
||||
|
||||
/** Send a FOLD command to the server to indicate that the player wants to fold. */
|
||||
public void fold() {
|
||||
client.sendFold();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a BET command to the server to indicate that the player wants to bet with the specified
|
||||
* amount.
|
||||
*
|
||||
* @param amount The amount the player wants to bet.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param amount The amount the player wants to raise to.
|
||||
*/
|
||||
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() {
|
||||
if (state.winnerIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return state.players.get(state.winnerIndex);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ public class GameState {
|
||||
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<>();
|
||||
|
||||
@@ -8,10 +8,186 @@ import java.util.List;
|
||||
* (e.g., "active", "folded"), and their hole cards.
|
||||
*/
|
||||
public class Player {
|
||||
public String name;
|
||||
public int chips;
|
||||
public int bet;
|
||||
public String state;
|
||||
|
||||
public List<Card> cards = new ArrayList<>();
|
||||
private PlayerId id;
|
||||
private int chips;
|
||||
private int bet;
|
||||
private PlayerState state;
|
||||
|
||||
private List<Card> cards = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Constructs a new Player with default values. The player's state is set to ACTIVE by default.
|
||||
*/
|
||||
public Player() {
|
||||
this.state = PlayerState.ACTIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new Player with the specified ID and initial chip count. The player's state is
|
||||
* set to ACTIVE by default.
|
||||
*
|
||||
* @param id The unique identifier for the player.
|
||||
* @param chips The initial number of chips the player has.
|
||||
*/
|
||||
public Player(PlayerId id, int chips) {
|
||||
this.id = id;
|
||||
this.chips = chips;
|
||||
this.bet = 0;
|
||||
this.state = PlayerState.ACTIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the unique identifier of the player.
|
||||
*
|
||||
* @return The player's ID.
|
||||
*/
|
||||
public PlayerId getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the unique identifier of the player.
|
||||
*
|
||||
* @param id The player's ID to set.
|
||||
*/
|
||||
public void setId(PlayerId id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the display name of the player. If the player's ID is not set, it returns an empty
|
||||
* string.
|
||||
*
|
||||
* @return The player's name.
|
||||
*/
|
||||
public String getName() {
|
||||
return id != null ? id.value() : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current chip count of the player.
|
||||
*
|
||||
* @return The number of chips the player has.
|
||||
*/
|
||||
public int getChips() {
|
||||
return chips;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current chip count of the player.
|
||||
*
|
||||
* @param chips The number of chips to set for the player.
|
||||
*/
|
||||
public void setChips(int chips) {
|
||||
this.chips = chips;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current bet amount of the player.
|
||||
*
|
||||
* @return The amount the player has currently bet in the ongoing hand.
|
||||
*/
|
||||
public int getBet() {
|
||||
return bet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current bet amount of the player.
|
||||
*
|
||||
* @param bet The amount the player has currently bet in the ongoing hand to set.
|
||||
*/
|
||||
public void setBet(int bet) {
|
||||
this.bet = bet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current state of the player (e.g., ACTIVE, FOLDED, ALL_IN).
|
||||
*
|
||||
* @return The player's current state in the game.
|
||||
*/
|
||||
public PlayerState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current state of the player (e.g., ACTIVE, FOLDED, ALL_IN).
|
||||
*
|
||||
* @param state The player's current state in the game to set.
|
||||
*/
|
||||
public void setState(PlayerState state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the player is currently the dealer.
|
||||
*
|
||||
* @return True if the player's state is DEALER, false otherwise.
|
||||
*/
|
||||
public boolean isDealer() {
|
||||
return state == PlayerState.DEALER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of hole cards currently held by the player.
|
||||
*
|
||||
* @return A list of Card objects representing the player's hole cards.
|
||||
*/
|
||||
public List<Card> getCards() {
|
||||
return cards;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the list of hole cards currently held by the player.
|
||||
*
|
||||
* @param cards A list of Card objects representing the player's hole cards to set.
|
||||
*/
|
||||
public void setCards(List<Card> cards) {
|
||||
this.cards = cards;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a card to the player's list of hole cards.
|
||||
*
|
||||
* @param card The Card object to be added to the player's hole cards.
|
||||
*/
|
||||
public void addCard(Card card) {
|
||||
this.cards.add(card);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the specified amount of chips to the player's current chip count.
|
||||
*
|
||||
* @param amount The number of chips to add to the player's current chip count.
|
||||
*/
|
||||
public void addChips(int amount) {
|
||||
if (amount < 0) {
|
||||
throw new IllegalArgumentException("Amount cannot be negative");
|
||||
}
|
||||
this.chips += amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the specified amount of chips from the player's current chip count.
|
||||
*
|
||||
* @param amount The number of chips to remove from the player's current chip count.
|
||||
*/
|
||||
public void removeChips(int amount) {
|
||||
if (amount < 0) {
|
||||
throw new IllegalArgumentException("Amount cannot be negative");
|
||||
}
|
||||
if (amount > this.chips) {
|
||||
throw new IllegalArgumentException(
|
||||
"Not enough chips. Available: " + this.chips + ", Requested: " + amount);
|
||||
}
|
||||
this.chips -= amount;
|
||||
}
|
||||
|
||||
/** Sets the player's state to FOLDED, indicating that they have folded in the current hand. */
|
||||
public void fall() {
|
||||
this.state = PlayerState.FOLDED;
|
||||
if (this.id != null) {
|
||||
this.id = PlayerId.of(this.id.value() + " (Fall)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.game;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/** Represents a unique identifier for a player in the client domain. */
|
||||
public record PlayerId(String value) {
|
||||
|
||||
/**
|
||||
* Constructs a PlayerId with the given string value, ensuring it is not null or empty.
|
||||
*
|
||||
* @param value the string value representing the player's unique identifier.
|
||||
*/
|
||||
public PlayerId {
|
||||
Objects.requireNonNull(value, "PlayerId cannot be null");
|
||||
value = value.trim();
|
||||
|
||||
if (value.isEmpty()) {
|
||||
throw new IllegalArgumentException("PlayerId cannot be empty");
|
||||
}
|
||||
}
|
||||
|
||||
/** Factory method to create a PlayerId. */
|
||||
public static PlayerId of(String value) {
|
||||
return new PlayerId(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string representation of the PlayerId, which is the encapsulated value.
|
||||
*
|
||||
* @return the string value of the PlayerId.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this PlayerId with another object for equality.
|
||||
*
|
||||
* @param o the object to compare to.
|
||||
* @return true if this PlayerId is equal to the given object, false otherwise.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PlayerId playerId = (PlayerId) o;
|
||||
return Objects.equals(value, playerId.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hash code for this PlayerId.
|
||||
*
|
||||
* @return the hash code of the PlayerId.
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.game;
|
||||
|
||||
/** Enumeration representing the possible states of a player in the poker game. */
|
||||
public enum PlayerState {
|
||||
ACTIVE,
|
||||
FOLDED,
|
||||
DEALER
|
||||
}
|
||||
@@ -138,33 +138,64 @@ public class ClientService {
|
||||
() -> {
|
||||
try {
|
||||
writeToTransport(message);
|
||||
String responseText = null;
|
||||
String responseText = clienttcptransport.read().payload();
|
||||
logger.info("Raw message '{}'", responseText);
|
||||
|
||||
responseText = clienttcptransport.read().payload();
|
||||
logger.info("Raw message '" + responseText + "'");
|
||||
Boolean success = null;
|
||||
int count = 0;
|
||||
for (String line : responseText.split("\n")) {
|
||||
if (success == null) {
|
||||
boolean hasStatus = false;
|
||||
boolean success = false;
|
||||
for (String rawLine : responseText.split("\n")) {
|
||||
String line = rawLine;
|
||||
if (!hasStatus) {
|
||||
if ("+OK".equals(line)) {
|
||||
success = true;
|
||||
hasStatus = true;
|
||||
continue;
|
||||
|
||||
} else if (("-ERROR").equals(responseText)) {
|
||||
success = false;
|
||||
}
|
||||
if (line.startsWith("-ERR") || line.startsWith("-ERROR")) {
|
||||
success = false;
|
||||
hasStatus = true;
|
||||
continue;
|
||||
}
|
||||
// ignore any lines before the status indicator
|
||||
continue;
|
||||
} else if ("END".equals(line)) {
|
||||
}
|
||||
|
||||
if ("END".equals(line)) {
|
||||
break;
|
||||
}
|
||||
line = line.replaceFirst("^\t", "");
|
||||
|
||||
// strip a single leading tab if present (protocol formatting)
|
||||
if (line.startsWith("\t")) {
|
||||
line = line.substring(1);
|
||||
}
|
||||
response.add(line);
|
||||
}
|
||||
if (success != null && success) {
|
||||
return;
|
||||
} else {
|
||||
throw new RuntimeException("Error in " + message + ": " + response);
|
||||
|
||||
if (!hasStatus) {
|
||||
// Fallback for servers that do not place the status on a
|
||||
// dedicated line: try to detect status markers anywhere
|
||||
// in the payload to remain compatible with older servers.
|
||||
if (responseText.contains("+OK")) {
|
||||
success = true;
|
||||
hasStatus = true;
|
||||
} else if (responseText.contains("-ERR")
|
||||
|| responseText.contains("-ERROR")) {
|
||||
success = false;
|
||||
hasStatus = true;
|
||||
} else {
|
||||
throw new RuntimeException(
|
||||
"No status line in response for '"
|
||||
+ message
|
||||
+ "': "
|
||||
+ responseText);
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new RuntimeException("Error in " + message + ": " + response);
|
||||
} catch (Exception e) {
|
||||
throw getRuntimeException(e);
|
||||
}
|
||||
@@ -200,15 +231,14 @@ public class ClientService {
|
||||
* @return A RuntimeException representing the cause of the original exception.
|
||||
*/
|
||||
private static RuntimeException getRuntimeException(Exception e) {
|
||||
Throwable reason = e.getCause();
|
||||
RuntimeException re;
|
||||
if (reason == null) {
|
||||
reason = e;
|
||||
} else if (reason instanceof RuntimeException rte) {
|
||||
re = rte;
|
||||
Throwable cause = e.getCause();
|
||||
if (cause == null) {
|
||||
return e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);
|
||||
}
|
||||
re = new RuntimeException(reason);
|
||||
return re;
|
||||
if (cause instanceof RuntimeException) {
|
||||
return (RuntimeException) cause;
|
||||
}
|
||||
return new RuntimeException(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,8 @@ package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.Card;
|
||||
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.List;
|
||||
|
||||
/**
|
||||
@@ -13,6 +15,7 @@ import java.util.List;
|
||||
public class GameClient {
|
||||
|
||||
private final ClientService client;
|
||||
private final int gameId;
|
||||
|
||||
/**
|
||||
* Constructs a GameClient with the given ClientService for communication.
|
||||
@@ -20,80 +23,290 @@ public class GameClient {
|
||||
* @param client The ClientService instance used to send commands and receive responses from the
|
||||
* server.
|
||||
*/
|
||||
public GameClient(ClientService client) {
|
||||
public GameClient(ClientService client, int gameId) {
|
||||
this.client = client;
|
||||
this.gameId = gameId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current game state from the server by sending a command and parsing the
|
||||
* response.
|
||||
* 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.
|
||||
* @return A GameState object representing the current state of the game, as parsed
|
||||
*/
|
||||
public GameState getGameState() {
|
||||
List<String> response = client.processCommand("GET_GAME_STATE");
|
||||
return parseGameState(response);
|
||||
public GameState fetchGameState() {
|
||||
List<String> responseLines = client.processCommand("GET_GAME_STATE\nGAME_ID=" + gameId);
|
||||
|
||||
if (responseLines == null || responseLines.isEmpty()) {
|
||||
throw new RuntimeException("Empty server response");
|
||||
}
|
||||
|
||||
String fullResponse = String.join("\n", responseLines);
|
||||
|
||||
return parse(fullResponse);
|
||||
}
|
||||
|
||||
/** Send a CALL command to the server to indicate that the player wants to call */
|
||||
public void sendCall() {
|
||||
client.processCommand("CALL\nGAME_ID=" + gameId);
|
||||
}
|
||||
|
||||
/** Send a FOLD command to the server to indicate that the player wants to fold */
|
||||
public void sendFold() {
|
||||
client.processCommand("FOLD\nGAME_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\nGAME_ID=" + gameId + "\nAMOUNT=" + amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the raw response from the server into a structured GameState object.
|
||||
* Send a RAISE command to the server to indicate that the player wants to raise
|
||||
*
|
||||
* @param input The raw response string from the server.
|
||||
* @param amount The amount to raise to
|
||||
*/
|
||||
public void sendRaise(int amount) {
|
||||
client.processCommand("RAISE\nGAME_ID=" + gameId + "\nAMOUNT=" + 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 parseGameState(List<String> input) {
|
||||
|
||||
private GameState parse(String input) {
|
||||
GameState state = new GameState();
|
||||
|
||||
// String[] lines = input.split("\n");
|
||||
ParserContext ctx = new ParserContext(state);
|
||||
|
||||
Player currentPlayer = null;
|
||||
Card currentCard = null;
|
||||
for (String raw : input.split("\n")) {
|
||||
String line = raw.trim();
|
||||
|
||||
for (String rawLine : input) {
|
||||
|
||||
String line = rawLine.trim();
|
||||
|
||||
if (line.startsWith("+OK") || line.equals("END")) {
|
||||
if (shouldSkip(line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith("PHASE=")) {
|
||||
state.phase = line.split("=")[1];
|
||||
} else if (line.startsWith("POT=")) {
|
||||
state.pot = Integer.parseInt(line.split("=")[1]);
|
||||
} else if (line.startsWith("CURRENT_BET=")) {
|
||||
state.currentBet = Integer.parseInt(line.split("=")[1]);
|
||||
} else if (line.startsWith("DEALER=")) {
|
||||
state.dealer = Integer.parseInt(line.split("=")[1]);
|
||||
} else if (line.startsWith("ACTIVE_PLAYER=")) {
|
||||
state.activePlayer = Integer.parseInt(line.split("=")[1]);
|
||||
} else if (line.startsWith("PLAYER")) {
|
||||
currentPlayer = new Player();
|
||||
state.players.add(currentPlayer);
|
||||
} else if (line.startsWith("NAME=") && currentPlayer != null) {
|
||||
currentPlayer.name = line.split("=")[1];
|
||||
} else if (line.startsWith("CHIPS=") && currentPlayer != null) {
|
||||
currentPlayer.chips = Integer.parseInt(line.split("=")[1]);
|
||||
} else if (line.startsWith("BET=") && currentPlayer != null) {
|
||||
currentPlayer.bet = Integer.parseInt(line.split("=")[1]);
|
||||
} else if (line.startsWith("STATE=") && currentPlayer != null) {
|
||||
currentPlayer.state = line.split("=")[1];
|
||||
} else if (line.startsWith("CARD")) {
|
||||
currentCard = new Card();
|
||||
if (handleGlobal(line, ctx)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentPlayer != null) {
|
||||
currentPlayer.cards.add(currentCard);
|
||||
} else {
|
||||
state.communityCards.add(currentCard);
|
||||
}
|
||||
} else if (line.startsWith("VALUE=") && currentCard != null) {
|
||||
currentCard.value = line.split("=")[1];
|
||||
} else if (line.startsWith("SUIT=") && currentCard != null) {
|
||||
currentCard.suit = line.split("=")[1];
|
||||
if (handleSections(line, ctx)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handlePlayer(line, ctx)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handleCards(line, ctx)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/** 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
if (line.equals("PLAYER")) {
|
||||
ctx.currentPlayer = new Player();
|
||||
ctx.state.players.add(ctx.currentPlayer);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.equals("CARDS")) {
|
||||
if (ctx.inPlayers && ctx.currentPlayer != null) {
|
||||
ctx.inPlayerCards = true;
|
||||
ctx.inCommunityCards = false;
|
||||
} else {
|
||||
ctx.inCommunityCards = true;
|
||||
ctx.inPlayerCards = false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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) {
|
||||
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()) {
|
||||
case "ACTIVE" -> PlayerState.ACTIVE;
|
||||
case "FOLDED" -> PlayerState.FOLDED;
|
||||
case "DEALER" -> PlayerState.DEALER;
|
||||
default -> PlayerState.ACTIVE;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The LobbyClient class is responsible for communicating with the server to manage game lobbies. It
|
||||
* provides methods to create a lobby, join a lobby, and fetch the current status of a lobby by
|
||||
@@ -65,8 +68,23 @@ public class LobbyClient {
|
||||
* Logs in to the server with the given username by sending a "LOGIN" command.
|
||||
*
|
||||
* @param user The username to log in with.
|
||||
* @return a {@link LoginResult} containing the assigned username and id as returned by the
|
||||
* server
|
||||
*/
|
||||
public void login(String user) {
|
||||
client.processCommand("LOGIN USERNAME=" + user);
|
||||
public LoginResult login(String user) {
|
||||
List<String> lines = client.processCommand("LOGIN USERNAME=" + user);
|
||||
|
||||
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||
|
||||
String assigned = user;
|
||||
String id = null;
|
||||
for (RequestParameter p : params) {
|
||||
if ("USERNAME".equalsIgnoreCase(p.key())) {
|
||||
assigned = p.value();
|
||||
} else if ("ID".equalsIgnoreCase(p.key())) {
|
||||
id = p.value();
|
||||
}
|
||||
}
|
||||
return new LoginResult(assigned, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||
|
||||
/**
|
||||
* Result of a successful login request.
|
||||
*
|
||||
* <p>Holds the username assigned by the server and the server-side id as a string (UUID). The
|
||||
* client previously attempted to parse the id as an integer which failed when the server returned a
|
||||
* UUID; this class therefore stores the id as a {@link String}.
|
||||
*/
|
||||
public class LoginResult {
|
||||
private final String username;
|
||||
private final String id;
|
||||
|
||||
/**
|
||||
* Create a login result.
|
||||
*
|
||||
* @param username the assigned username
|
||||
* @param id the assigned id (UUID string) returned by the server
|
||||
*/
|
||||
public LoginResult(String username, String id) {
|
||||
this.username = username;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the assigned username
|
||||
*/
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the assigned id as returned by the server (UUID string)
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,19 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
|
||||
|
||||
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.ui.gameui.gameuicomponents.PlayerStatusController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.TaskbarController;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/**
|
||||
@@ -15,24 +27,853 @@ import javafx.scene.layout.VBox;
|
||||
*/
|
||||
public class CasinoGameController {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(CasinoGameController.class.getName());
|
||||
|
||||
@FXML private Label tableText;
|
||||
@FXML private VBox casinoTable;
|
||||
@FXML private HBox communityCardsBox;
|
||||
@FXML private VBox casinoTableInnerBox;
|
||||
@FXML private HBox playerCardsBox;
|
||||
@FXML private HBox potBox;
|
||||
@FXML private PlayerStatusController playerStatusController;
|
||||
@FXML private PlayerStatusController player1Controller;
|
||||
@FXML private PlayerStatusController player2Controller;
|
||||
@FXML private PlayerStatusController player3Controller;
|
||||
@FXML private VBox player1;
|
||||
@FXML private VBox player2;
|
||||
@FXML private VBox player3;
|
||||
@FXML private ImageView myDealerIcon;
|
||||
|
||||
private GameService gameService;
|
||||
private PlayerId myPlayerId;
|
||||
private TaskbarController taskbarController;
|
||||
private Image dealerImage;
|
||||
|
||||
private static final int TOTAL_SLOTS = 5;
|
||||
private static final int PLAYER_SLOTS = 2;
|
||||
private int pot = 0;
|
||||
private static final String BACKSIDE = "/images/card-background-3.png";
|
||||
private static final String DEALER_IMAGE_PATH = "/images/chip-dealer-blue-3.png";
|
||||
private static final double TABLE_OFFSET_Y_FACTOR = 0.09;
|
||||
private static final double PLAYER_CARDS_OFFSET_Y_FACTOR = 0.35;
|
||||
private static final double MOVE_FROM_X = -40;
|
||||
private static final double MOVE_FROM_Y = -10;
|
||||
private static final double MOVE_TO_X = 0;
|
||||
private static final double MOVE_TO_Y = 0;
|
||||
private static final double FADE_FROM = 0;
|
||||
private static final double FADE_TO = 1;
|
||||
private static final double SCALE_FROM = 0.95;
|
||||
private static final double SCALE_TO = 1;
|
||||
private static final double SETTLE_FROM_Y = 0;
|
||||
private static final double SETTLE_TO_Y = 10;
|
||||
private static final boolean SETTLE_AUTO_REVERSE = true;
|
||||
private static final int SETTLE_CYCLE_COUNT = 2;
|
||||
private static final long ANIMATION_DURATION_MS = 350;
|
||||
private static final long SETTLE_DURATION_MS = 150;
|
||||
private static final long STAGGER_DELAY_MS = 120;
|
||||
private static final double HOVER_SCALE_ON = 1.08;
|
||||
private static final double HOVER_SCALE_OFF = 1.0;
|
||||
private static final double HOVER_LIFT_ON = -8;
|
||||
private static final double HOVER_LIFT_OFF = 0;
|
||||
private static final long HOVER_DURATION_MS = 180;
|
||||
private static final long UI_UPDATE_INTERVAL_SECONDS = 1;
|
||||
private static final int PLAYER_INDEX_0 = 0;
|
||||
private static final int PLAYER_INDEX_1 = 1;
|
||||
private static final int PLAYER_INDEX_2 = 2;
|
||||
private static final int DEALER_INDEX_PLAYER_1 = 0;
|
||||
private static final int DEALER_INDEX_PLAYER_2 = 1;
|
||||
private static final int DEALER_INDEX_PLAYER_3 = 2;
|
||||
private static final int DEALER_PLAYER_1 = 0;
|
||||
private static final int DEALER_PLAYER_2 = 1;
|
||||
private static final int DEALER_PLAYER_3 = 2;
|
||||
private static final int DEALER_MYSELF = 3;
|
||||
private static final double CARD_START_OPACITY = 0.0;
|
||||
private static final double CARD_START_SCALE = 0.85;
|
||||
private static final double COMMUNITY_CARD_HEIGHT_RATIO = 0.22;
|
||||
private static final double COMMUNITY_CARD_WIDTH_RATIO = 0.11;
|
||||
private static final double PLAYER_CARD_HEIGHT_RATIO = 0.30;
|
||||
private static final double PLAYER_CARD_WIDTH_RATIO = 0.15;
|
||||
private static final double PLAYER_CARDS_Y_OFFSET_FACTOR = 0.10;
|
||||
private static final int[] CHIP_VALUES = {
|
||||
100000, 50000, 20000, 10000,
|
||||
5000, 2000, 1000, 500,
|
||||
200, 100, 50, 20,
|
||||
10, 5, 2, 1
|
||||
};
|
||||
private static final double CHIP_HEIGHT_RATIO = 0.08;
|
||||
private static final double CHIP_WIDTH_RATIO = 0.04;
|
||||
private static final double CHIP_OPACITY_START = 0.0;
|
||||
private static final double CHIP_SCALE_START = 0.6;
|
||||
private static final double CHIP_SCALE_END = 1.0;
|
||||
private static final double CHIP_RANDOM_X_FACTOR = 20.0;
|
||||
private static final double CHIP_RANDOM_X_CENTER = 0.5;
|
||||
private static final double CHIP_RANDOM_Y_BASE = -30.0;
|
||||
private static final double CHIP_RANDOM_Y_VARIATION = 20.0;
|
||||
private static final double CHIP_TRANSLATE_RESET_X = 0.0;
|
||||
private static final double CHIP_TRANSLATE_RESET_Y = 0.0;
|
||||
private static final double CHIP_FADE_TO = 1.0;
|
||||
private static final double CHIP_SCALE_TO = 1.0;
|
||||
private static final long CHIP_FADE_DURATION_MS = 200;
|
||||
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;
|
||||
|
||||
/** Standard constructor. Used by FXML. */
|
||||
public CasinoGameController() {
|
||||
// default constructor for FXML
|
||||
}
|
||||
|
||||
@FXML private Label welcomeText;
|
||||
@FXML private VBox casinoTable;
|
||||
/**
|
||||
* 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
|
||||
* accordingly.
|
||||
*
|
||||
* @param gameService The GameService instance that provides access to the game state and logic.
|
||||
*/
|
||||
public void setGameService(GameService gameService) {
|
||||
this.gameService = gameService;
|
||||
}
|
||||
|
||||
// TODO: Test logic: will be replaced by real game interactions,
|
||||
// once the game engine is finished
|
||||
/** Set the PlayerId of the current player. */
|
||||
@FXML
|
||||
public void initialize() {
|
||||
LOGGER.info("INIT UI");
|
||||
|
||||
if (communityCardsBox == null) {
|
||||
LOGGER.warning("communityCardsBox is NULL");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
var url = getClass().getResource(DEALER_IMAGE_PATH);
|
||||
|
||||
if (url != null) {
|
||||
dealerImage = new Image(url.toExternalForm(), true);
|
||||
myDealerIcon.setImage(dealerImage);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.warning("Dealer icon load failed");
|
||||
}
|
||||
|
||||
myDealerIcon.setVisible(false);
|
||||
|
||||
javafx.stage.Screen screen = javafx.stage.Screen.getPrimary();
|
||||
double screenHeight = screen.getBounds().getHeight();
|
||||
casinoTableInnerBox.setTranslateY(-screenHeight * TABLE_OFFSET_Y_FACTOR);
|
||||
playerCardsBox.setTranslateY(screenHeight * PLAYER_CARDS_OFFSET_Y_FACTOR);
|
||||
|
||||
// uiTest();
|
||||
|
||||
// empty display only (optional)
|
||||
renderCommunityCards(List.of());
|
||||
renderPlayerCards(List.of());
|
||||
}
|
||||
|
||||
// Test method to demonstrate the UI functionality with sample data.
|
||||
// @FXML
|
||||
// public void uiTest() {
|
||||
// if (communityCardsBox == null) {
|
||||
// LOGGER.info("communityCardsBox is NULL");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
// renderCommunityCards(
|
||||
// List.of(
|
||||
// new Card("ace", "hearts"),
|
||||
// new Card("king", "spades"),
|
||||
// new Card("10", "diamonds")));
|
||||
//
|
||||
// renderPlayerCards(List.of(new Card("10", "spades"), new Card("king", "spades")));
|
||||
// } catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
//
|
||||
// Player mathis = new Player(PlayerId.of("Mathis"), 20000);
|
||||
// mathis.setState(PlayerState.ACTIVE);
|
||||
//
|
||||
// Player jona = new Player(PlayerId.of("Jona"), 20000);
|
||||
// jona.setState(PlayerState.FOLDED);
|
||||
//
|
||||
// Player lars = new Player(PlayerId.of("Lars"), 20000);
|
||||
// lars.setState(PlayerState.ACTIVE);
|
||||
//
|
||||
// player1Controller.setPlayer(mathis);
|
||||
// player2Controller.setPlayer(jona);
|
||||
// player3Controller.setPlayer(lars);
|
||||
//
|
||||
// LOGGER.info(
|
||||
// "Player 1: "
|
||||
// + mathis.getName()
|
||||
// + " | $"
|
||||
// + mathis.getChips()
|
||||
// + " | "
|
||||
// + mathis.getState());
|
||||
// LOGGER.info(
|
||||
// "Player 2: "
|
||||
// + jona.getName()
|
||||
// + " | $"
|
||||
// + jona.getChips()
|
||||
// + " | "
|
||||
// + jona.getState());
|
||||
// LOGGER.info(
|
||||
// "Player 3: "
|
||||
// + lars.getName()
|
||||
// + " | $"
|
||||
// + lars.getChips()
|
||||
// + " | "
|
||||
// + lars.getState());
|
||||
//
|
||||
// if (playerStatusController != null) {
|
||||
// playerStatusController.setPlayer(mathis);
|
||||
// } else {
|
||||
// LOGGER.warning("PlayerStatusController is NULL");
|
||||
// }
|
||||
//
|
||||
// javafx.stage.Screen screen = javafx.stage.Screen.getPrimary();
|
||||
// double screenHeight = screen.getBounds().getHeight();
|
||||
//
|
||||
// casinoTableInnerBox.setTranslateY(-screenHeight * 0.08);
|
||||
// playerCardsBox.setTranslateY(screenHeight * 0.35);
|
||||
//
|
||||
// renderPot(500);
|
||||
//
|
||||
// lars.removeChips(500);
|
||||
// LOGGER.info("Lars: $" + lars.getChips());
|
||||
//
|
||||
// player3Controller.refresh();
|
||||
//
|
||||
// mathis.fall();
|
||||
// player1Controller.refresh();
|
||||
//
|
||||
// GameState s = new GameState();
|
||||
// s.dealer = 3;
|
||||
// highlightDealer(s);
|
||||
//
|
||||
// s.phase = "FLOP";
|
||||
//
|
||||
// updateGameInfo(s);
|
||||
//
|
||||
// s.winnerIndex = 1;
|
||||
//
|
||||
// updateGameInfo(s);
|
||||
// }
|
||||
|
||||
/** Start the UI update loop. */
|
||||
public void start() {
|
||||
|
||||
if (gameService == null) {
|
||||
throw new IllegalStateException("GameService not set!");
|
||||
}
|
||||
|
||||
startLoop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary test method that performs a placeholder action when the table is clicked.
|
||||
*
|
||||
* <p>In the final implementation, this will be replaced by the game logic.
|
||||
* Start a loop that periodically updates the UI by fetching the latest game state from the
|
||||
* GameService.
|
||||
*/
|
||||
@FXML
|
||||
public void onTableClick() {
|
||||
welcomeText.setText("Einsatz akzeptiert!");
|
||||
private void startLoop() {
|
||||
|
||||
javafx.animation.Timeline t =
|
||||
new javafx.animation.Timeline(
|
||||
new javafx.animation.KeyFrame(
|
||||
javafx.util.Duration.seconds(UI_UPDATE_INTERVAL_SECONDS),
|
||||
e -> updateUI()));
|
||||
|
||||
t.setCycleCount(javafx.animation.Animation.INDEFINITE);
|
||||
t.play();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the UI by fetching the latest game state from the GameService and updating all
|
||||
* relevant components.
|
||||
*/
|
||||
private void updateUI() {
|
||||
|
||||
try {
|
||||
GameState s = gameService.refresh();
|
||||
|
||||
if (s == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderCommunityCards(s.communityCards);
|
||||
renderPot(s.pot);
|
||||
|
||||
updatePlayers(s.players);
|
||||
updatePlayerCards(s);
|
||||
|
||||
updateGameInfo(s);
|
||||
highlightDealer(s);
|
||||
|
||||
updateTaskbar(s);
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("Error: UI Update failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's hole cards based on the active player in the game state.
|
||||
*
|
||||
* @param s The current game state.
|
||||
*/
|
||||
private void updatePlayerCards(GameState s) {
|
||||
|
||||
int active = s.activePlayer;
|
||||
|
||||
if (active >= 0 && active < s.players.size()) {
|
||||
Player p = s.players.get(active);
|
||||
renderPlayerCards(p.getCards());
|
||||
} else {
|
||||
renderPlayerCards(List.of());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the game information display, such as the current phase and the winner if the hand has
|
||||
* ended.
|
||||
*
|
||||
* @param s The current game state.
|
||||
*/
|
||||
private void updateGameInfo(GameState s) {
|
||||
|
||||
String text = "Phase: " + s.phase;
|
||||
|
||||
if (s.winnerIndex >= 0 && s.winnerIndex < s.players.size()) {
|
||||
Player winner = s.players.get(s.winnerIndex);
|
||||
text = "Winner: " + winner.getName();
|
||||
}
|
||||
|
||||
tableText.setText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the taskbar with the current game state and the player's ID.
|
||||
*
|
||||
* @param s The current game state.
|
||||
*/
|
||||
private void updateTaskbar(GameState s) {
|
||||
|
||||
taskbarController.update(s, myPlayerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player status components with the latest player information from the game state.
|
||||
*
|
||||
* @param p The list of players in the current game state.
|
||||
*/
|
||||
private void updatePlayers(List<Player> p) {
|
||||
|
||||
if (p == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (p.size() > PLAYER_INDEX_0) {
|
||||
player1Controller.setPlayer(p.get(PLAYER_INDEX_0));
|
||||
}
|
||||
|
||||
if (p.size() > PLAYER_INDEX_1) {
|
||||
player2Controller.setPlayer(p.get(PLAYER_INDEX_1));
|
||||
}
|
||||
|
||||
if (p.size() > PLAYER_INDEX_2) {
|
||||
player3Controller.setPlayer(p.get(PLAYER_INDEX_2));
|
||||
}
|
||||
|
||||
player1Controller.refresh();
|
||||
player2Controller.refresh();
|
||||
player3Controller.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight the dealer in the UI based on the dealer index in the game state.
|
||||
*
|
||||
* @param s The current game state containing the dealer index.
|
||||
*/
|
||||
private void highlightDealer(GameState s) {
|
||||
|
||||
player1Controller.setDealer(false);
|
||||
player2Controller.setDealer(false);
|
||||
player3Controller.setDealer(false);
|
||||
|
||||
myDealerIcon.setVisible(false);
|
||||
|
||||
int dealer = s.dealer;
|
||||
|
||||
if (dealer == DEALER_PLAYER_1) {
|
||||
player1Controller.setDealer(true);
|
||||
}
|
||||
|
||||
if (dealer == DEALER_PLAYER_2) {
|
||||
player2Controller.setDealer(true);
|
||||
}
|
||||
|
||||
if (dealer == DEALER_PLAYER_3) {
|
||||
player3Controller.setDealer(true);
|
||||
}
|
||||
|
||||
if (dealer == DEALER_MYSELF) {
|
||||
myDealerIcon.setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the community cards on the table based on the list of cards provided.
|
||||
*
|
||||
* @param cards The list of community cards to display.
|
||||
*/
|
||||
private void renderCommunityCards(List<Card> cards) {
|
||||
|
||||
communityCardsBox.getChildren().clear();
|
||||
|
||||
if (cards == null) {
|
||||
cards = List.of();
|
||||
}
|
||||
|
||||
for (int i = 0; i < TOTAL_SLOTS; i++) {
|
||||
|
||||
ImageView view = new ImageView();
|
||||
styleCommunityCard(view);
|
||||
|
||||
Image image;
|
||||
|
||||
if (i < cards.size() && cards.get(i) != null) {
|
||||
image = loadImageSafe(mapCardToImage(cards.get(i)));
|
||||
} else {
|
||||
image = loadImageSafe(BACKSIDE);
|
||||
}
|
||||
|
||||
view.setImage(image);
|
||||
|
||||
view.setOnMouseEntered(e -> animateCardHover(view, true));
|
||||
view.setOnMouseExited(e -> animateCardHover(view, false));
|
||||
|
||||
view.setOpacity(CARD_START_OPACITY);
|
||||
|
||||
view.setScaleX(CARD_START_SCALE);
|
||||
view.setScaleY(CARD_START_SCALE);
|
||||
|
||||
communityCardsBox.getChildren().add(view);
|
||||
|
||||
animateCardAppear(view, i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the player's hole cards based on the list of cards provided.
|
||||
*
|
||||
* @param cards The list of hole cards to display for the player.
|
||||
*/
|
||||
private void renderPlayerCards(List<Card> cards) {
|
||||
|
||||
playerCardsBox.getChildren().clear();
|
||||
|
||||
playerCardsBox.getChildren().add(myDealerIcon);
|
||||
|
||||
if (cards == null) {
|
||||
cards = List.of();
|
||||
}
|
||||
|
||||
for (int i = 0; i < PLAYER_SLOTS; i++) {
|
||||
|
||||
ImageView view = new ImageView();
|
||||
stylePlayerCard(view);
|
||||
|
||||
Image image;
|
||||
|
||||
if (i < cards.size() && cards.get(i) != null) {
|
||||
image = loadImageSafe(mapCardToImage(cards.get(i)));
|
||||
} else {
|
||||
image = loadImageSafe(BACKSIDE);
|
||||
}
|
||||
|
||||
view.setImage(image);
|
||||
|
||||
view.setOnMouseEntered(e -> animateCardHover(view, true));
|
||||
view.setOnMouseExited(e -> animateCardHover(view, false));
|
||||
|
||||
view.setOpacity(CARD_START_OPACITY);
|
||||
|
||||
view.setScaleX(CARD_START_SCALE);
|
||||
view.setScaleY(CARD_START_SCALE);
|
||||
|
||||
playerCardsBox.getChildren().add(view);
|
||||
|
||||
animateCardAppear(view, i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an image from the given path safely, falling back to a default backside image if the
|
||||
* specified image cannot be found.
|
||||
*
|
||||
* @param path The path to the image resource to load.
|
||||
* @return The loaded Image object.
|
||||
*/
|
||||
private Image loadImageSafe(String path) {
|
||||
|
||||
var stream = getClass().getResourceAsStream(path);
|
||||
|
||||
if (stream == null) {
|
||||
LOGGER.warning("IMAGE NOT FOUND: " + path);
|
||||
stream = getClass().getResourceAsStream(BACKSIDE);
|
||||
}
|
||||
|
||||
return new Image(stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply styling to the given ImageView for community cards, including CSS classes and size
|
||||
* bindings.
|
||||
*
|
||||
* @param view The ImageView to style as a community card.
|
||||
*/
|
||||
private void styleCommunityCard(ImageView view) {
|
||||
|
||||
view.getStyleClass().add("community-card");
|
||||
|
||||
view.setPreserveRatio(true);
|
||||
view.setSmooth(true);
|
||||
|
||||
javafx.scene.Scene scene = communityCardsBox.getScene();
|
||||
|
||||
if (scene != null) {
|
||||
bindCommunityCardSize(view, scene);
|
||||
} else {
|
||||
communityCardsBox
|
||||
.sceneProperty()
|
||||
.addListener(
|
||||
(obs, oldScene, newScene) -> {
|
||||
if (newScene != null) {
|
||||
bindCommunityCardSize(view, newScene);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply styling to the given ImageView for player hole cards, including CSS classes and size
|
||||
* bindings.
|
||||
*
|
||||
* @param view The ImageView to style as a player hole card.
|
||||
*/
|
||||
private void stylePlayerCard(ImageView view) {
|
||||
|
||||
view.getStyleClass().add("player-card");
|
||||
|
||||
view.setPreserveRatio(true);
|
||||
view.setSmooth(true);
|
||||
|
||||
javafx.scene.Scene scene = playerCardsBox.getScene();
|
||||
|
||||
if (scene != null) {
|
||||
bindPlayerCardSize(view, scene);
|
||||
} else {
|
||||
playerCardsBox
|
||||
.sceneProperty()
|
||||
.addListener(
|
||||
(obs, oldScene, newScene) -> {
|
||||
if (newScene != null) {
|
||||
bindPlayerCardSize(view, newScene);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the size of the given ImageView to the scene dimensions for community cards.
|
||||
*
|
||||
* @param view The ImageView representing a community card whose size should be bound to the
|
||||
* scene dimensions.
|
||||
* @param scene The JavaFX Scene to which the ImageView belongs, used for binding the size
|
||||
* properties.
|
||||
*/
|
||||
private void bindCommunityCardSize(ImageView view, javafx.scene.Scene scene) {
|
||||
|
||||
view.fitHeightProperty().bind(scene.heightProperty().multiply(COMMUNITY_CARD_HEIGHT_RATIO));
|
||||
|
||||
view.fitWidthProperty().bind(scene.widthProperty().multiply(COMMUNITY_CARD_WIDTH_RATIO));
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the size of the given ImageView to the scene dimensions for player hole cards.
|
||||
*
|
||||
* @param view The ImageView representing a player hole card whose size should be bound to the
|
||||
* scene dimensions.
|
||||
* @param scene The JavaFX Scene to which the ImageView belongs, used for binding the size
|
||||
* properties.
|
||||
*/
|
||||
private void bindPlayerCardSize(ImageView view, javafx.scene.Scene scene) {
|
||||
|
||||
view.fitHeightProperty().bind(scene.heightProperty().multiply(PLAYER_CARD_HEIGHT_RATIO));
|
||||
|
||||
view.fitWidthProperty().bind(scene.widthProperty().multiply(PLAYER_CARD_WIDTH_RATIO));
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate the appearance of a card by applying a sequence of transitions, including movement,
|
||||
* fading, scaling, and settling.
|
||||
*
|
||||
* @param view The ImageView representing the card to animate.
|
||||
* @param index The index of the card in the display order.
|
||||
*/
|
||||
private void animateCardAppear(ImageView view, int index) {
|
||||
|
||||
javafx.animation.TranslateTransition move =
|
||||
new javafx.animation.TranslateTransition(
|
||||
javafx.util.Duration.millis(ANIMATION_DURATION_MS), view);
|
||||
|
||||
move.setFromX(MOVE_FROM_X);
|
||||
move.setToX(MOVE_TO_X);
|
||||
|
||||
move.setFromY(MOVE_FROM_Y);
|
||||
move.setToY(MOVE_TO_Y);
|
||||
|
||||
move.setInterpolator(javafx.animation.Interpolator.EASE_OUT);
|
||||
|
||||
javafx.animation.FadeTransition fade =
|
||||
new javafx.animation.FadeTransition(
|
||||
javafx.util.Duration.millis(ANIMATION_DURATION_MS), view);
|
||||
|
||||
fade.setFromValue(FADE_FROM);
|
||||
fade.setToValue(FADE_TO);
|
||||
|
||||
javafx.animation.ScaleTransition scale =
|
||||
new javafx.animation.ScaleTransition(
|
||||
javafx.util.Duration.millis(ANIMATION_DURATION_MS), view);
|
||||
|
||||
scale.setFromX(SCALE_FROM);
|
||||
scale.setFromY(SCALE_FROM);
|
||||
scale.setToX(SCALE_TO);
|
||||
scale.setToY(SCALE_TO);
|
||||
scale.setInterpolator(javafx.animation.Interpolator.EASE_OUT);
|
||||
|
||||
javafx.animation.TranslateTransition settle =
|
||||
new javafx.animation.TranslateTransition(
|
||||
javafx.util.Duration.millis(SETTLE_DURATION_MS), view);
|
||||
|
||||
settle.setFromY(SETTLE_FROM_Y);
|
||||
settle.setToY(SETTLE_TO_Y);
|
||||
settle.setAutoReverse(SETTLE_AUTO_REVERSE);
|
||||
settle.setCycleCount(SETTLE_CYCLE_COUNT);
|
||||
|
||||
javafx.animation.SequentialTransition st =
|
||||
new javafx.animation.SequentialTransition(
|
||||
new javafx.animation.ParallelTransition(move, fade, scale), settle);
|
||||
|
||||
st.setDelay(javafx.util.Duration.millis(index * STAGGER_DELAY_MS));
|
||||
|
||||
st.play();
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate a card hover effect by scaling and lifting the card when hovered and resetting it
|
||||
* when not hovered.
|
||||
*
|
||||
* @param view The ImageView representing the card to animate on hover.
|
||||
* @param hover A boolean indicating whether the card is being hovered (true) or not (false).
|
||||
*/
|
||||
private void animateCardHover(ImageView view, boolean hover) {
|
||||
|
||||
double scale = hover ? HOVER_SCALE_ON : HOVER_SCALE_OFF;
|
||||
double lift = hover ? HOVER_LIFT_ON : HOVER_LIFT_OFF;
|
||||
|
||||
javafx.animation.ScaleTransition st =
|
||||
new javafx.animation.ScaleTransition(
|
||||
javafx.util.Duration.millis(HOVER_DURATION_MS), view);
|
||||
st.setToX(scale);
|
||||
st.setToY(scale);
|
||||
|
||||
javafx.animation.TranslateTransition tt =
|
||||
new javafx.animation.TranslateTransition(
|
||||
javafx.util.Duration.millis(HOVER_DURATION_MS), view);
|
||||
tt.setToY(lift);
|
||||
|
||||
st.play();
|
||||
tt.play();
|
||||
}
|
||||
|
||||
/**
|
||||
* Position the player's hole cards box on the screen based on a predefined offset factor
|
||||
* relative to the screen height.
|
||||
*/
|
||||
private void positionPlayerCards() {
|
||||
|
||||
javafx.stage.Screen screen = javafx.stage.Screen.getPrimary();
|
||||
double screenHeight = screen.getBounds().getHeight();
|
||||
|
||||
playerCardsBox.setTranslateY(screenHeight * PLAYER_CARDS_Y_OFFSET_FACTOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a Card object to the corresponding image path based on its suit and value.
|
||||
*
|
||||
* @param card The Card object to be mapped to an image path.
|
||||
* @return The string path to the image representing the given card.
|
||||
*/
|
||||
private String mapCardToImage(Card card) {
|
||||
|
||||
if (card == 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();
|
||||
};
|
||||
|
||||
String value =
|
||||
switch (card.getValue().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();
|
||||
};
|
||||
|
||||
String path = "/images/card-" + suit + "-" + value + "-3.png";
|
||||
|
||||
// LOGGER.info("CARD PATH: " + path);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the pot by calculating the required chips based on the pot amount and displaying them
|
||||
* with animations.
|
||||
*
|
||||
* @param pot The total amount in the pot that needs to be represented with chips on the UI.
|
||||
*/
|
||||
private void renderPot(int pot) {
|
||||
|
||||
potBox.getChildren().clear();
|
||||
|
||||
int remaining = pot;
|
||||
|
||||
int index = 0;
|
||||
|
||||
for (int chipValue : CHIP_VALUES) {
|
||||
|
||||
while (remaining >= chipValue) {
|
||||
|
||||
ImageView chip =
|
||||
new ImageView(loadImageSafe("/images/chip-" + chipValue + "-5.png"));
|
||||
|
||||
if (chip.getImage() == null) {
|
||||
LOGGER.warning("Missing chip image: " + chipValue);
|
||||
break;
|
||||
}
|
||||
|
||||
styleChip(chip);
|
||||
|
||||
potBox.getChildren().add(chip);
|
||||
|
||||
animateChipAppear(chip, index);
|
||||
|
||||
remaining -= chipValue;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
// LOGGER.info("POT RENDERED: " + pot + " -> remaining: " + remaining);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply styling to the given ImageView for chips, including CSS classes and size bindings.
|
||||
*
|
||||
* @param view The ImageView to style as a chip in the pot display.
|
||||
*/
|
||||
private void styleChip(ImageView view) {
|
||||
|
||||
view.getStyleClass().add("chips-icon");
|
||||
|
||||
view.setPreserveRatio(true);
|
||||
view.setSmooth(true);
|
||||
|
||||
javafx.scene.Scene scene = potBox.getScene();
|
||||
|
||||
if (scene != null) {
|
||||
bindChipSize(view, scene);
|
||||
} else {
|
||||
potBox.sceneProperty()
|
||||
.addListener(
|
||||
(obs, oldScene, newScene) -> {
|
||||
if (newScene != null) {
|
||||
bindChipSize(view, newScene);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the size of the given ImageView to the scene dimensions for chips in the pot display.
|
||||
*
|
||||
* @param view The ImageView representing a chip whose size should be bound to the scene
|
||||
* dimensions.
|
||||
* @param scene The JavaFX Scene to which the ImageView belongs, used for binding the size
|
||||
* properties.
|
||||
*/
|
||||
private void bindChipSize(ImageView view, javafx.scene.Scene scene) {
|
||||
|
||||
view.fitHeightProperty()
|
||||
.bind(
|
||||
scene.heightProperty().multiply(CHIP_HEIGHT_RATIO) // kleiner als Karten
|
||||
);
|
||||
|
||||
view.fitWidthProperty().bind(scene.widthProperty().multiply(CHIP_WIDTH_RATIO));
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate the appearance of a chip by applying a sequence of transitions, including random
|
||||
* initial positioning, fading, scaling, and dropping into place.
|
||||
*
|
||||
* @param view The ImageView representing the chip to animate.
|
||||
* @param index The index of the chip in the display order, used to stagger the animation timing
|
||||
* for multiple chips.
|
||||
*/
|
||||
private void animateChipAppear(ImageView view, int index) {
|
||||
|
||||
view.setOpacity(CHIP_OPACITY_START);
|
||||
view.setScaleX(CHIP_SCALE_START);
|
||||
view.setScaleY(CHIP_SCALE_START);
|
||||
|
||||
double randomX = (Math.random() - CHIP_RANDOM_X_CENTER) * CHIP_RANDOM_X_FACTOR;
|
||||
double randomY = CHIP_RANDOM_Y_BASE - Math.random() * CHIP_RANDOM_Y_VARIATION;
|
||||
|
||||
view.setTranslateX(randomX);
|
||||
view.setTranslateY(randomY);
|
||||
|
||||
javafx.animation.FadeTransition fade =
|
||||
new javafx.animation.FadeTransition(
|
||||
javafx.util.Duration.millis(CHIP_FADE_DURATION_MS), view);
|
||||
fade.setToValue(CHIP_FADE_TO);
|
||||
|
||||
javafx.animation.ScaleTransition scale =
|
||||
new javafx.animation.ScaleTransition(
|
||||
javafx.util.Duration.millis(CHIP_SCALE_DURATION_MS), view);
|
||||
scale.setToX(CHIP_SCALE_TO);
|
||||
scale.setToY(CHIP_SCALE_TO);
|
||||
|
||||
javafx.animation.TranslateTransition drop =
|
||||
new javafx.animation.TranslateTransition(
|
||||
javafx.util.Duration.millis(CHIP_DROP_DURATION_MS), view);
|
||||
drop.setToX(CHIP_TRANSLATE_RESET_X);
|
||||
drop.setToY(CHIP_TRANSLATE_RESET_Y);
|
||||
|
||||
javafx.animation.SequentialTransition st =
|
||||
new javafx.animation.SequentialTransition(fade, scale, drop);
|
||||
|
||||
st.setDelay(javafx.util.Duration.millis(index * CHIP_STAGGER_DELAY_MULTIPLIER));
|
||||
|
||||
st.play();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,26 +7,30 @@ import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
/**
|
||||
* Main class for the Casono Game UI.
|
||||
*
|
||||
* <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 {
|
||||
|
||||
// Static field for ClientService (workaround for JavaFX Application launch)
|
||||
private static ClientService staticClientService;
|
||||
private static ClientService clientService;
|
||||
|
||||
public static void setClientService(ClientService clientService) {
|
||||
staticClientService = clientService;
|
||||
}
|
||||
private static final int DEFAULT_WIDTH = 1200;
|
||||
private static final int DEFAULT_HEIGHT = 800;
|
||||
|
||||
public static ClientService getClientService() {
|
||||
return staticClientService;
|
||||
}
|
||||
|
||||
/** Default no-arg constructor. */
|
||||
/** default constructor */
|
||||
public CasinoGameUI() {
|
||||
// default no-arg constructor
|
||||
}
|
||||
|
||||
private static final int DEFAULT_WIDTH = 1200;
|
||||
private static final int DEFAULT_HEIGHT = 800;
|
||||
public static void setClientService(ClientService clientService) {
|
||||
CasinoGameUI.clientService = clientService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the main stage of the application.
|
||||
@@ -39,7 +43,7 @@ public class CasinoGameUI extends Application {
|
||||
FXMLLoader fxmlLoader =
|
||||
new FXMLLoader(CasinoGameUI.class.getResource("/ui-structure/Casinogameui.fxml"));
|
||||
Scene scene = new Scene(fxmlLoader.load(), DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
stage.setTitle("Casono (GAME)");
|
||||
stage.setTitle("Casono");
|
||||
|
||||
String iconPath = getClass().getResource("/images/logoinverted.png").toExternalForm();
|
||||
stage.getIcons().add(new javafx.scene.image.Image(iconPath));
|
||||
@@ -49,7 +53,7 @@ public class CasinoGameUI extends Application {
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point of the application.
|
||||
* Starting point of the application.
|
||||
*
|
||||
* @param args Command line arguments.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.Player;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState;
|
||||
import java.util.logging.Logger;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.Pane;
|
||||
|
||||
/**
|
||||
* Controller for displaying a player's status in the poker game, including their name, chip count
|
||||
* and whether they are the dealer.
|
||||
*/
|
||||
public class PlayerStatusController {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(PlayerStatusController.class.getName());
|
||||
|
||||
@FXML private Label playerName;
|
||||
@FXML private Label playerMoney;
|
||||
@FXML private ImageView dealerIcon;
|
||||
@FXML private Pane parent;
|
||||
|
||||
private Image dealerImage;
|
||||
private Player player;
|
||||
private static final String DEALER_IMAGE_PATH = "/images/chip-dealer-blue-3.png";
|
||||
private static final double DEALER_ICON_X_FACTOR = 0.8;
|
||||
|
||||
/** Initialize the controller, load dealer image, and set up bindings. */
|
||||
@FXML
|
||||
public void initialize() {
|
||||
|
||||
try {
|
||||
var url = getClass().getResource(DEALER_IMAGE_PATH);
|
||||
|
||||
if (url != null) {
|
||||
dealerImage = new Image(url.toExternalForm(), true);
|
||||
dealerIcon.setImage(dealerImage);
|
||||
} else {
|
||||
LOGGER.warning("Dealer image not found in resources!");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("Error: loading dealer image:");
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
dealerIcon.layoutXProperty().bind(parent.widthProperty().multiply(DEALER_ICON_X_FACTOR));
|
||||
dealerIcon.setVisible(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind player to UI
|
||||
*
|
||||
* @param player The player whose status is to be displayed.
|
||||
*/
|
||||
public void setPlayer(Player player) {
|
||||
this.player = player;
|
||||
refresh();
|
||||
}
|
||||
|
||||
/** Refresh UI safely */
|
||||
public void refresh() {
|
||||
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// NAME (safe)
|
||||
String name = player.getName();
|
||||
playerName.setText(name != null ? name : "-");
|
||||
|
||||
// MONEY
|
||||
playerMoney.setText(player.getChips() + " $");
|
||||
|
||||
// DEALER
|
||||
boolean isDealer = player.getState() == PlayerState.DEALER;
|
||||
|
||||
dealerIcon.setVisible(isDealer);
|
||||
|
||||
if (isDealer && dealerImage != null) {
|
||||
dealerIcon.setImage(dealerImage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* External quick update
|
||||
*
|
||||
* @param name The player's name to display.
|
||||
* @param chips The player's chip count to display.
|
||||
*/
|
||||
public void updatePlayer(String name, int chips) {
|
||||
playerName.setText(name != null ? name : "-");
|
||||
playerMoney.setText(chips + " $");
|
||||
}
|
||||
|
||||
/**
|
||||
* Force dealer state
|
||||
*
|
||||
* @param isDealer Whether the player is the dealer or not.
|
||||
*/
|
||||
public void setDealer(boolean isDealer) {
|
||||
dealerIcon.setVisible(isDealer);
|
||||
|
||||
if (isDealer && dealerImage != null) {
|
||||
dealerIcon.setImage(dealerImage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,14 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
|
||||
|
||||
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.game.PlayerState;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
@@ -18,22 +25,225 @@ import org.apache.logging.log4j.Logger;
|
||||
*/
|
||||
public class TaskbarController {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class);
|
||||
|
||||
@FXML private HBox taskbar;
|
||||
@FXML private TextField taskbarInput;
|
||||
@FXML private Button betButton;
|
||||
@FXML private Button callButton;
|
||||
@FXML private Button foldButton;
|
||||
@FXML private Button raiseButton;
|
||||
@FXML private Label moneyLabel;
|
||||
|
||||
private GameService gameService;
|
||||
private PlayerId myPlayerId;
|
||||
private double xOffset = 0;
|
||||
private double yOffset = 0;
|
||||
private static final double TASKBAR_SCALE = 0.95;
|
||||
private static final int MIN_CREDITS = 5;
|
||||
private static final int MAX_CREDITS = 100000;
|
||||
private static final int CREDIT_STEP = 5;
|
||||
private static final String STYLE_YELLOW_BUTTON = "yellow-button";
|
||||
private static final String STYLE_GRAY_BUTTON = "gray-button";
|
||||
private static final String STYLE_RED_BUTTON = "red-button";
|
||||
private static final String STYLE_YELLOW_INPUT = "yellow-input-field";
|
||||
private static final String STYLE_GRAY_INPUT = "gray-input-field";
|
||||
private static final String STYLE_RED_INPUT = "red-input-field";
|
||||
private static final double SCALE_NORMAL = 1.0;
|
||||
|
||||
/** Standard constructor. Used by FXML. */
|
||||
public TaskbarController() {
|
||||
// default constructor for FXML
|
||||
}
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class);
|
||||
/**
|
||||
* Updates the state of the basic action buttons (Bet, Call, Fold, Raise) and the input field
|
||||
*
|
||||
* @param isMyTurn Indicates whether it is currently the player's turn.
|
||||
* @param isOut Indicates whether the player is currently out of the game (folded or all-in).
|
||||
*/
|
||||
private void updateBasicButtons(boolean isMyTurn, boolean isOut) {
|
||||
|
||||
@FXML private HBox taskbar;
|
||||
@FXML private TextField taskbarInput;
|
||||
if (isOut) {
|
||||
setRedButton(betButton);
|
||||
setRedButton(callButton);
|
||||
setRedButton(foldButton);
|
||||
setRedButton(raiseButton);
|
||||
setRedInputField(taskbarInput);
|
||||
return;
|
||||
}
|
||||
|
||||
private double xOffset = 0;
|
||||
private double yOffset = 0;
|
||||
private static final double TASKBAR_SCALE = 0.9;
|
||||
private static final int MIN_CREDITS = 5;
|
||||
private static final int MAX_CREDITS = 100000;
|
||||
private static final int CREDIT_STEP = 5;
|
||||
if (isMyTurn) {
|
||||
activateButton(betButton);
|
||||
activateButton(callButton);
|
||||
activateButton(foldButton);
|
||||
activateButton(raiseButton);
|
||||
activateInputField(taskbarInput);
|
||||
} else {
|
||||
deactivateButton(betButton);
|
||||
deactivateButton(callButton);
|
||||
deactivateButton(foldButton);
|
||||
deactivateButton(raiseButton);
|
||||
deactivateInputField(taskbarInput);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given button to a disabled state with red styling, indicating that the player is out
|
||||
*
|
||||
* @param b The button to be styled as red and disabled
|
||||
*/
|
||||
private void setRedButton(Button b) {
|
||||
b.setDisable(true);
|
||||
|
||||
b.getStyleClass().remove(STYLE_YELLOW_BUTTON);
|
||||
b.getStyleClass().remove(STYLE_GRAY_BUTTON);
|
||||
|
||||
if (!b.getStyleClass().contains(STYLE_RED_BUTTON)) {
|
||||
b.getStyleClass().add(STYLE_RED_BUTTON);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given input field to a disabled state with red styling, indicating that the player
|
||||
* is out
|
||||
*
|
||||
* @param t The text field to be styled as red and disabled
|
||||
*/
|
||||
private void setRedInputField(TextField t) {
|
||||
t.setDisable(true);
|
||||
|
||||
t.getStyleClass().remove(STYLE_YELLOW_INPUT);
|
||||
t.getStyleClass().remove(STYLE_GRAY_INPUT);
|
||||
|
||||
if (!t.getStyleClass().contains(STYLE_RED_INPUT)) {
|
||||
t.getStyleClass().add(STYLE_RED_INPUT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the given button by enabling it and applying yellow styling, indicating that it is
|
||||
* the player's turn
|
||||
*
|
||||
* @param b The button to be activated and styled for the player's turn
|
||||
*/
|
||||
private void activateButton(Button b) {
|
||||
b.setDisable(false);
|
||||
|
||||
b.getStyleClass().remove(STYLE_GRAY_BUTTON);
|
||||
|
||||
if (!b.getStyleClass().contains(STYLE_YELLOW_BUTTON)) {
|
||||
b.getStyleClass().add(STYLE_YELLOW_BUTTON);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the given input field by enabling it and applying yellow styling, indicating that
|
||||
* it is the player's turn
|
||||
*
|
||||
* @param t The text field to be activated and styled for the player's turn
|
||||
*/
|
||||
private void activateInputField(TextField t) {
|
||||
t.setDisable(false);
|
||||
|
||||
t.getStyleClass().remove(STYLE_GRAY_INPUT);
|
||||
|
||||
if (!t.getStyleClass().contains(STYLE_YELLOW_INPUT)) {
|
||||
t.getStyleClass().add(STYLE_YELLOW_INPUT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivates the given button by disabling it and applying gray styling, indicating that it is
|
||||
* not the player's turn
|
||||
*
|
||||
* @param b The button to be deactivated and styled for non-active state
|
||||
*/
|
||||
private void deactivateButton(Button b) {
|
||||
b.setDisable(true);
|
||||
|
||||
b.getStyleClass().remove(STYLE_YELLOW_BUTTON);
|
||||
b.getStyleClass().remove(STYLE_RED_BUTTON);
|
||||
|
||||
if (!b.getStyleClass().contains(STYLE_GRAY_BUTTON)) {
|
||||
b.getStyleClass().add(STYLE_GRAY_BUTTON);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivates the given input field by disabling it and applying gray styling, indicating that
|
||||
* it is not the player's turn
|
||||
*
|
||||
* @param t The text field to be deactivated and styled for non-active state
|
||||
*/
|
||||
private void deactivateInputField(TextField t) {
|
||||
t.setDisable(true);
|
||||
|
||||
t.getStyleClass().remove(STYLE_YELLOW_INPUT);
|
||||
t.getStyleClass().remove(STYLE_RED_INPUT);
|
||||
|
||||
if (!t.getStyleClass().contains(STYLE_GRAY_INPUT)) {
|
||||
t.getStyleClass().add(STYLE_GRAY_INPUT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the displayed amount of money the player has. The amount is shown in the format "X$".
|
||||
*
|
||||
* @param amount The amount of money to display for the player.
|
||||
*/
|
||||
public void setMoney(int amount) {
|
||||
moneyLabel.setText(amount + "$");
|
||||
}
|
||||
|
||||
public void setGameService(GameService gameService, PlayerId myPlayerId) {
|
||||
this.gameService = gameService;
|
||||
this.myPlayerId = myPlayerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the taskbar controller. This method is called automatically after the FXML
|
||||
* components have been loaded.
|
||||
*/
|
||||
@FXML
|
||||
public void initialize() {
|
||||
updateBasicButtons(false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the taskbar based on the current game state and the player's status. It checks if
|
||||
* it's the player's turn and whether they are out of the game (folded or all-in) to adjust the
|
||||
* button states and displayed money accordingly.
|
||||
*
|
||||
* @param state
|
||||
* @param myPlayerId
|
||||
*/
|
||||
public void update(GameState state, PlayerId myPlayerId) {
|
||||
|
||||
if (state == null || state.players == null || myPlayerId == null) {
|
||||
LOGGER.warn("Cannot update taskbar: invalid input");
|
||||
return;
|
||||
}
|
||||
|
||||
Player me =
|
||||
state.players.stream()
|
||||
.filter(p -> myPlayerId.equals(p.getId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (me == null) {
|
||||
LOGGER.error("Player not found in GameState!");
|
||||
return;
|
||||
}
|
||||
|
||||
int myIndex = state.players.indexOf(me);
|
||||
|
||||
boolean isMyTurn = state.activePlayer == myIndex;
|
||||
boolean isOut = me.getState() == PlayerState.FOLDED;
|
||||
|
||||
updateBasicButtons(isMyTurn, isOut);
|
||||
setMoney(me.getChips());
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the taskbar is clicked with the mouse. Saves the relative position for later,
|
||||
@@ -72,8 +282,8 @@ public class TaskbarController {
|
||||
*/
|
||||
@FXML
|
||||
private void onTaskbarReleased(MouseEvent event) {
|
||||
taskbar.setScaleX(1.0);
|
||||
taskbar.setScaleY(1.0);
|
||||
taskbar.setScaleX(SCALE_NORMAL);
|
||||
taskbar.setScaleY(SCALE_NORMAL);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,6 +307,76 @@ public class TaskbarController {
|
||||
processBet();
|
||||
}
|
||||
|
||||
/** Called when the Call button is clicked. */
|
||||
@FXML
|
||||
private void onInputPlayerCall() {
|
||||
|
||||
if (gameService == null) {
|
||||
LOGGER.error("GameService not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
gameService.call();
|
||||
LOGGER.info("Player CALL");
|
||||
|
||||
refreshGame();
|
||||
}
|
||||
|
||||
/** Called when the Fold button is clicked. */
|
||||
@FXML
|
||||
private void onInputPlayerFold() {
|
||||
|
||||
if (gameService == null) {
|
||||
LOGGER.error("GameService not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
gameService.fold();
|
||||
LOGGER.info("Player FOLD");
|
||||
|
||||
refreshGame();
|
||||
}
|
||||
|
||||
/** Called when the Raise button is clicked. */
|
||||
@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");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the game state by fetching the latest state from the GameService and updating the
|
||||
* taskbar accordingly.
|
||||
*/
|
||||
private void refreshGame() {
|
||||
|
||||
try {
|
||||
GameState newState = gameService.refresh();
|
||||
update(newState, myPlayerId);
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to refresh game state: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the Exit button is clicked. Closes the current game stage and opens the lobby UI.
|
||||
*/
|
||||
@FXML
|
||||
private void onExitButtonClick() {
|
||||
javafx.application.Platform.runLater(
|
||||
@@ -109,7 +389,7 @@ public class TaskbarController {
|
||||
try {
|
||||
new Casinomainui().start(new javafx.stage.Stage());
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Fehler beim Starten der Lobby-UI: {}", e.getMessage());
|
||||
LOGGER.error("Error: starting the lobby UI: {}", e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -120,19 +400,34 @@ public class TaskbarController {
|
||||
* displayed on the console.
|
||||
*/
|
||||
private void processBet() {
|
||||
|
||||
if (gameService == null) {
|
||||
LOGGER.error("Error: GameService not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
String input = taskbarInput.getText();
|
||||
|
||||
try {
|
||||
|
||||
int credits = Integer.parseInt(input.trim());
|
||||
|
||||
if (credits >= MIN_CREDITS && credits <= MAX_CREDITS && credits % CREDIT_STEP == 0) {
|
||||
// TODO: Credits must be sent to the GameEngine
|
||||
LOGGER.info("Bet set: {} Casono Credits", credits);
|
||||
|
||||
gameService.bet(credits);
|
||||
|
||||
LOGGER.info("Bet placed: {}", credits);
|
||||
|
||||
taskbarInput.clear();
|
||||
|
||||
refreshGame();
|
||||
|
||||
} else {
|
||||
LOGGER.error("Error: Only increments of 5 (5, 10, ... 100,000) are allowed!");
|
||||
LOGGER.error("Error: Bet must be between 5 and 100000 and multiple of 5");
|
||||
}
|
||||
|
||||
} catch (NumberFormatException e) {
|
||||
LOGGER.error("Error: Please enter only one number!");
|
||||
LOGGER.error("Error: Invalid number entered");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||
import javafx.application.Platform;
|
||||
@@ -36,12 +37,16 @@ public class CasinomainuiController {
|
||||
private int nextButtonId = 1;
|
||||
private LobbyClient lobbyClient;
|
||||
|
||||
/** Default constructor for dependency injection by FXMLLoader. */
|
||||
/** Default constructor used by FXMLLoader. */
|
||||
public CasinomainuiController() {
|
||||
// Default constructor
|
||||
}
|
||||
|
||||
/** Initializes the UI components and sets default values. */
|
||||
/**
|
||||
* Initializes the UI components and sets default values. If a shared {@link
|
||||
* ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService} exists (created at application
|
||||
* start), the controller reuses it so the connection remains open and already-logged-in.
|
||||
*/
|
||||
@FXML
|
||||
public void initialize() {
|
||||
titleLabel.setText("Casono");
|
||||
@@ -51,16 +56,18 @@ public class CasinomainuiController {
|
||||
translationManager = LobbyButtonTranslationManager.getInstance();
|
||||
String host = System.getProperty("casono.server.host");
|
||||
int port = Integer.parseInt(System.getProperty("casono.server.port"));
|
||||
ClientService clientService;
|
||||
try {
|
||||
clientService = new ClientService(host, port);
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.warn(
|
||||
"Could not connect to server {}:{} — starting in offline mode: {}",
|
||||
host,
|
||||
port,
|
||||
e.getMessage());
|
||||
clientService = new ClientService(true); // offline mode
|
||||
ClientService clientService = ClientApp.getSharedClientService();
|
||||
if (clientService == null) {
|
||||
try {
|
||||
clientService = new ClientService(host, port);
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.warn(
|
||||
"Could not connect to server {}:{} — starting in offline mode: {}",
|
||||
host,
|
||||
port,
|
||||
e.getMessage());
|
||||
clientService = new ClientService(true); // offline mode
|
||||
}
|
||||
}
|
||||
gridManager =
|
||||
new LobbyButtonGridManager(
|
||||
@@ -117,7 +124,11 @@ public class CasinomainuiController {
|
||||
Platform.exit();
|
||||
}
|
||||
|
||||
/** Handles creation of a new lobby button. */
|
||||
/**
|
||||
* Handles creation of a new lobby button. Attempts to create a lobby on the server via the
|
||||
* {@link LobbyButtonGridManager} and registers the new button in the local translation manager.
|
||||
* Errors are logged and displayed as an informational alert.
|
||||
*/
|
||||
@FXML
|
||||
public void handleCreateLobbyButton() {
|
||||
if (translationManager.isFull()) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager;
|
||||
@@ -85,7 +86,8 @@ public class ServerApp {
|
||||
SESSION_DISCONNECT_JOB_PERIOD,
|
||||
TimeUnit.SECONDS);
|
||||
|
||||
registerCommands(dispatcher, router, responseDispatcher, userRegistry);
|
||||
LobbyManager lobbyManager = new LobbyManager();
|
||||
registerCommands(dispatcher, router, responseDispatcher, userRegistry, lobbyManager);
|
||||
|
||||
NetworkManager networkManager = new NetworkManager(port, sessionManager, router);
|
||||
networkManager.start();
|
||||
@@ -102,7 +104,8 @@ public class ServerApp {
|
||||
CommandParserDispatcher parserDispatcher,
|
||||
CommandRouter commandRouter,
|
||||
ResponseDispatcher responseDispatcher,
|
||||
UserRegistry userRegistry) {
|
||||
UserRegistry userRegistry,
|
||||
LobbyManager lobbyManager) {
|
||||
parserDispatcher.register("PING", new PingParser());
|
||||
commandRouter.register(PingRequest.class, new PingHandler(responseDispatcher));
|
||||
|
||||
@@ -136,5 +139,118 @@ public class ServerApp {
|
||||
parserDispatcher.register("LIST_USERS", new ListUsersParser());
|
||||
commandRouter.register(
|
||||
ListUsersRequest.class, new ListUsersHandler(responseDispatcher, userRegistry));
|
||||
|
||||
// GET_LOBBY_LIST registration
|
||||
parserDispatcher.register(
|
||||
"GET_LOBBY_LIST",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list
|
||||
.GetLobbyListParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list
|
||||
.GetLobbyListRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
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));
|
||||
|
||||
// GET_GAME_STATE registration
|
||||
parserDispatcher.register(
|
||||
"GET_GAME_STATE",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state
|
||||
.GetGameStateParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state
|
||||
.GetGameStateRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game
|
||||
.get_game_state.GetGameStateRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state
|
||||
.GetGameStateHandler(
|
||||
responseDispatcher, lobbyManager, userRegistry));
|
||||
|
||||
// BET registration
|
||||
parserDispatcher.register(
|
||||
"BET",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
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));
|
||||
|
||||
// RAISE registration
|
||||
parserDispatcher.register(
|
||||
"RAISE",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise
|
||||
.PlayerRaiseParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseRequest
|
||||
.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise
|
||||
.PlayerRaiseRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise
|
||||
.PlayerRaiseHandler(
|
||||
responseDispatcher, userRegistry, lobbyManager));
|
||||
|
||||
// CALL registration
|
||||
parserDispatcher.register(
|
||||
"CALL",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call
|
||||
.PlayerCallParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call.PlayerCallRequest
|
||||
.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
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));
|
||||
|
||||
// FOLD registration
|
||||
parserDispatcher.register(
|
||||
"FOLD",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold
|
||||
.PlayerFoldParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold.PlayerFoldRequest
|
||||
.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
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));
|
||||
|
||||
// GET_LOBBY_STATUS registration
|
||||
parserDispatcher.register(
|
||||
"GET_LOBBY_STATUS",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status
|
||||
.GetLobbyStatusParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status
|
||||
.GetLobbyStatusRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
|
||||
.get_lobby_status.GetLobbyStatusRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
|
||||
.get_lobby_status.GetLobbyStatusHandler(
|
||||
responseDispatcher, lobbyManager, userRegistry));
|
||||
|
||||
// JOIN_LOBBY registration
|
||||
parserDispatcher.register(
|
||||
"JOIN_LOBBY",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||
.JoinLobbyParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||
.JoinLobbyRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||
.JoinLobbyRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||
.JoinLobbyHandler(responseDispatcher, lobbyManager, userRegistry));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.add_player;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Handler for the ADD_PLAYER command. */
|
||||
public class AddPlayerHandler extends CommandHandler<AddPlayerRequest> {
|
||||
private final UserRegistry userRegistry;
|
||||
|
||||
public AddPlayerHandler(UserRegistry userRegistry, ResponseDispatcher responseDispatcher) {
|
||||
super(responseDispatcher);
|
||||
this.userRegistry = userRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(AddPlayerRequest request) {
|
||||
Optional<User> created =
|
||||
userRegistry.registerIfAvailable(
|
||||
request.getName(), request.getContext().sessionId());
|
||||
if (created.isEmpty()) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(request.getContext(), "NAME_TAKEN", "Name already taken"));
|
||||
return;
|
||||
}
|
||||
|
||||
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.add_player;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||
|
||||
/** Parser for the ADD_PLAYER command. */
|
||||
public class AddPlayerParser implements CommandParser<AddPlayerRequest> {
|
||||
@Override
|
||||
public AddPlayerRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
RequestParameterAccessor accessor =
|
||||
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||
|
||||
String name = accessor.require("NAME");
|
||||
|
||||
int chips = accessor.require("CHIPS", Integer::parseInt);
|
||||
|
||||
return new AddPlayerRequest(primitiveRequest.context(), name, chips);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.add_player;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
|
||||
/** Request data for the ADD_PLAYER command. */
|
||||
public class AddPlayerRequest extends Request {
|
||||
private final String name;
|
||||
private final int chips;
|
||||
|
||||
public AddPlayerRequest(RequestContext context, String name, int chips) {
|
||||
super(context);
|
||||
this.name = name;
|
||||
this.chips = chips;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getChips() {
|
||||
return chips;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
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.ErrorResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Handler for the `BET` command.
|
||||
*
|
||||
* <p>This handler validates the request (amount >= 0), resolves the user and target lobby (by
|
||||
* `GAME_ID` when provided or by session username otherwise), checks that a game is running and then
|
||||
* forwards the action to the {@code GameController}.
|
||||
*
|
||||
* <p>On failure the handler dispatches an {@link ErrorResponse} with an appropriate error code
|
||||
* (e.g. {@code INVALID_AMOUNT}, {@code NOT_IN_LOBBY}, {@code LOBBY_NOT_FOUND}, {@code
|
||||
* GAME_NOT_STARTED}, {@code GAME_ACTION_FAILED}). On success it dispatches an {@link OkResponse}.
|
||||
*/
|
||||
public class PlayerBetHandler extends CommandHandler<PlayerBetRequest> {
|
||||
private final UserRegistry userRegistry;
|
||||
private final LobbyManager lobbyManager;
|
||||
|
||||
/**
|
||||
* Creates a new {@code PlayerBetHandler}.
|
||||
*
|
||||
* @param responseDispatcher dispatcher used to send responses back to the client
|
||||
* @param userRegistry registry to resolve users from session ids
|
||||
* @param lobbyManager manager used to lookup lobbies and their game controllers
|
||||
*/
|
||||
public PlayerBetHandler(
|
||||
ResponseDispatcher responseDispatcher,
|
||||
UserRegistry userRegistry,
|
||||
LobbyManager lobbyManager) {
|
||||
super(responseDispatcher);
|
||||
this.userRegistry = userRegistry;
|
||||
this.lobbyManager = lobbyManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the bet request: validate parameters, resolve lobby and game, and forward the bet
|
||||
* action to the game controller. Sends an {@link ErrorResponse} on failure or an {@link
|
||||
* OkResponse} on success.
|
||||
*
|
||||
* @param request the parsed {@link PlayerBetRequest}
|
||||
*/
|
||||
@Override
|
||||
public void execute(PlayerBetRequest request) {
|
||||
if (request.getAmount() < 0) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "INVALID_AMOUNT", "Amount must be non-negative"));
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
|
||||
userRegistry.getBySessionId(request.getSessionId());
|
||||
if (opt.isEmpty()) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
|
||||
return;
|
||||
}
|
||||
|
||||
String username = opt.get().getName();
|
||||
|
||||
Integer gameId = request.getGameId();
|
||||
var lobby =
|
||||
(gameId != null)
|
||||
? lobbyManager.getLobby(LobbyId.of(gameId))
|
||||
: lobbyManager.getLobbyByUsername(username);
|
||||
|
||||
if (lobby == null) {
|
||||
if (gameId != null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||
} else {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
GameController game = lobby.getGameController();
|
||||
if (game == null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "GAME_NOT_STARTED", "Game not started"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
game.playerBet(PlayerId.of(username), request.getAmount());
|
||||
} catch (Exception e) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(request.getContext(), "GAME_ACTION_FAILED", e.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||
|
||||
/**
|
||||
* Parser for the `BET` command.
|
||||
*
|
||||
* <p>Parses the request parameters and builds a {@link PlayerBetRequest}. Expected parameters:
|
||||
*
|
||||
* <ul>
|
||||
* <li>`GAME_ID` (optional) - numeric id of the lobby/game to target
|
||||
* <li>`AMOUNT` (required) - amount to bet
|
||||
* </ul>
|
||||
*/
|
||||
public class PlayerBetParser implements CommandParser<PlayerBetRequest> {
|
||||
/**
|
||||
* Parse a primitive request into a {@link PlayerBetRequest}.
|
||||
*
|
||||
* @param primitiveRequest the raw parsed request containing parameters and context
|
||||
* @return a {@link PlayerBetRequest} with the parsed values (gameId may be null)
|
||||
*/
|
||||
@Override
|
||||
public PlayerBetRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
RequestParameterAccessor accessor =
|
||||
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||
|
||||
Integer gameId = null;
|
||||
try {
|
||||
gameId = accessor.optional("GAME_ID", null, Integer::parseInt);
|
||||
} catch (Exception e) {
|
||||
// parse handled by framework
|
||||
}
|
||||
|
||||
int amount = accessor.require("AMOUNT", Integer::parseInt);
|
||||
|
||||
return new PlayerBetRequest(primitiveRequest.context(), gameId, amount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
|
||||
/**
|
||||
* Request for the `BET` command.
|
||||
*
|
||||
* <p>Contains the optional target `gameId` and the `amount` the player wants to bet.
|
||||
*/
|
||||
public class PlayerBetRequest extends Request {
|
||||
private final Integer gameId;
|
||||
private final int amount;
|
||||
|
||||
/**
|
||||
* Creates a new {@code PlayerBetRequest}.
|
||||
*
|
||||
* @param context the request context
|
||||
* @param gameId optional game id of the targeted lobby (may be {@code null})
|
||||
* @param amount the bet amount (non-negative)
|
||||
*/
|
||||
public PlayerBetRequest(RequestContext context, Integer gameId, int amount) {
|
||||
super(context);
|
||||
this.gameId = gameId;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the optional target game id.
|
||||
*
|
||||
* @return the game id or {@code null} if not provided
|
||||
*/
|
||||
public Integer getGameId() {
|
||||
return gameId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested bet amount.
|
||||
*
|
||||
* @return the bet amount
|
||||
*/
|
||||
public int getAmount() {
|
||||
return amount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
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.ErrorResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Handler for the `CALL` command.
|
||||
*
|
||||
* <p>This handler validates the session, resolves the lobby (by `GAME_ID` when provided or by
|
||||
* session username otherwise), checks for a running game and forwards the call action to the {@code
|
||||
* GameController}.
|
||||
*/
|
||||
public class PlayerCallHandler extends CommandHandler<PlayerCallRequest> {
|
||||
private final UserRegistry userRegistry;
|
||||
private final LobbyManager lobbyManager;
|
||||
|
||||
/**
|
||||
* Creates a new {@code PlayerCallHandler}.
|
||||
*
|
||||
* @param responseDispatcher dispatcher used to send responses back to the client
|
||||
* @param userRegistry registry to resolve users from session ids
|
||||
* @param lobbyManager manager used to lookup lobbies and their game controllers
|
||||
*/
|
||||
public PlayerCallHandler(
|
||||
ResponseDispatcher responseDispatcher,
|
||||
UserRegistry userRegistry,
|
||||
LobbyManager lobbyManager) {
|
||||
super(responseDispatcher);
|
||||
this.userRegistry = userRegistry;
|
||||
this.lobbyManager = lobbyManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the call request: resolve lobby by `GAME_ID` if present, otherwise by session user,
|
||||
* verify game is running, and forward the call to the game controller.
|
||||
*
|
||||
* @param request the parsed {@link PlayerCallRequest}
|
||||
*/
|
||||
@Override
|
||||
public void execute(PlayerCallRequest request) {
|
||||
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
|
||||
userRegistry.getBySessionId(request.getSessionId());
|
||||
if (opt.isEmpty()) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
|
||||
return;
|
||||
}
|
||||
|
||||
String username = opt.get().getName();
|
||||
|
||||
Integer gameId = request.getGameId();
|
||||
var lobby =
|
||||
(gameId != null)
|
||||
? lobbyManager.getLobby(LobbyId.of(gameId))
|
||||
: lobbyManager.getLobbyByUsername(username);
|
||||
|
||||
if (lobby == null) {
|
||||
if (gameId != null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||
} else {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
GameController game = lobby.getGameController();
|
||||
if (game == null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "GAME_NOT_STARTED", "Game not started"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
game.playerCall(PlayerId.of(username));
|
||||
} catch (RuntimeException e) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(request.getContext(), "GAME_ACTION_FAILED", e.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||
|
||||
/**
|
||||
* Parser for the `CALL` command.
|
||||
*
|
||||
* <p>Accepts an optional `GAME_ID` parameter. If provided, the handler will target the specified
|
||||
* lobby; otherwise the server resolves the lobby by the requesting session's user.
|
||||
*/
|
||||
public class PlayerCallParser implements CommandParser<PlayerCallRequest> {
|
||||
@Override
|
||||
public PlayerCallRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
RequestParameterAccessor accessor =
|
||||
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||
|
||||
Integer gameId = null;
|
||||
try {
|
||||
gameId = accessor.optional("GAME_ID", null, Integer::parseInt);
|
||||
} catch (Exception e) {
|
||||
// parse errors handled by framework
|
||||
}
|
||||
|
||||
return new PlayerCallRequest(primitiveRequest.context(), gameId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
|
||||
/**
|
||||
* Request for the `CALL` command.
|
||||
*
|
||||
* <p>Contains an optional `gameId` when the client targets a specific lobby, otherwise the server
|
||||
* will resolve the lobby from the requesting session's user.
|
||||
*/
|
||||
public class PlayerCallRequest extends Request {
|
||||
private final Integer gameId;
|
||||
|
||||
/**
|
||||
* Creates a new {@code PlayerCallRequest}.
|
||||
*
|
||||
* @param context the request context
|
||||
* @param gameId optional game id of the targeted lobby (may be {@code null})
|
||||
*/
|
||||
public PlayerCallRequest(RequestContext context, Integer gameId) {
|
||||
super(context);
|
||||
this.gameId = gameId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the optional target game id.
|
||||
*
|
||||
* @return the game id or {@code null} if not provided
|
||||
*/
|
||||
public Integer getGameId() {
|
||||
return gameId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
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.ErrorResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Handler for the `FOLD` command.
|
||||
*
|
||||
* <p>This handler validates the session, resolves the lobby (by `GAME_ID` when provided or by
|
||||
* session username otherwise), checks for a running game and forwards the fold action to the {@code
|
||||
* GameController}.
|
||||
*/
|
||||
public class PlayerFoldHandler extends CommandHandler<PlayerFoldRequest> {
|
||||
private final UserRegistry userRegistry;
|
||||
private final LobbyManager lobbyManager;
|
||||
|
||||
/**
|
||||
* Creates a new {@code PlayerFoldHandler}.
|
||||
*
|
||||
* @param responseDispatcher dispatcher used to send responses back to the client
|
||||
* @param userRegistry registry to resolve users from session ids
|
||||
* @param lobbyManager manager used to lookup lobbies and their game controllers
|
||||
*/
|
||||
public PlayerFoldHandler(
|
||||
ResponseDispatcher responseDispatcher,
|
||||
UserRegistry userRegistry,
|
||||
LobbyManager lobbyManager) {
|
||||
super(responseDispatcher);
|
||||
this.userRegistry = userRegistry;
|
||||
this.lobbyManager = lobbyManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the fold request: resolve lobby by `GAME_ID` if present, otherwise by session user,
|
||||
* verify game is running, and forward the fold to the game controller.
|
||||
*
|
||||
* @param request the parsed {@link PlayerFoldRequest}
|
||||
*/
|
||||
@Override
|
||||
public void execute(PlayerFoldRequest request) {
|
||||
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
|
||||
userRegistry.getBySessionId(request.getSessionId());
|
||||
if (opt.isEmpty()) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
|
||||
return;
|
||||
}
|
||||
|
||||
String username = opt.get().getName();
|
||||
|
||||
Integer gameId = request.getGameId();
|
||||
var lobby =
|
||||
(gameId != null)
|
||||
? lobbyManager.getLobby(LobbyId.of(gameId))
|
||||
: lobbyManager.getLobbyByUsername(username);
|
||||
|
||||
if (lobby == null) {
|
||||
if (gameId != null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||
} else {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
GameController game = lobby.getGameController();
|
||||
if (game == null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "GAME_NOT_STARTED", "Game not started"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
game.playerFold(PlayerId.of(username));
|
||||
} catch (RuntimeException e) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(request.getContext(), "GAME_ACTION_FAILED", e.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||
|
||||
/**
|
||||
* Parser for the `FOLD` command.
|
||||
*
|
||||
* <p>Accepts an optional `GAME_ID` parameter. If provided, the handler will target the specified
|
||||
* lobby; otherwise the server resolves the lobby by the requesting session's user.
|
||||
*/
|
||||
public class PlayerFoldParser implements CommandParser<PlayerFoldRequest> {
|
||||
@Override
|
||||
public PlayerFoldRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
RequestParameterAccessor accessor =
|
||||
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||
|
||||
Integer gameId = null;
|
||||
try {
|
||||
gameId = accessor.optional("GAME_ID", null, Integer::parseInt);
|
||||
} catch (Exception e) {
|
||||
// parse handled by framework
|
||||
}
|
||||
|
||||
return new PlayerFoldRequest(primitiveRequest.context(), gameId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
|
||||
/**
|
||||
* Request for the `FOLD` command.
|
||||
*
|
||||
* <p>Contains an optional `gameId` when the client targets a specific lobby, otherwise the server
|
||||
* will resolve the lobby from the requesting session's user.
|
||||
*/
|
||||
public class PlayerFoldRequest extends Request {
|
||||
private final Integer gameId;
|
||||
|
||||
/**
|
||||
* Creates a new {@code PlayerFoldRequest}.
|
||||
*
|
||||
* @param context the request context
|
||||
* @param gameId optional game id of the targeted lobby (may be {@code null})
|
||||
*/
|
||||
public PlayerFoldRequest(RequestContext context, Integer gameId) {
|
||||
super(context);
|
||||
this.gameId = gameId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the optional target game id.
|
||||
*
|
||||
* @return the game id or {@code null} if not provided
|
||||
*/
|
||||
public Integer getGameId() {
|
||||
return gameId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck;
|
||||
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.User;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
|
||||
/** Handler for GET_GAME_STATE: returns pot, phase, community cards and per-player info. */
|
||||
public class GetGameStateHandler extends CommandHandler<GetGameStateRequest> {
|
||||
private final LobbyManager lobbyManager;
|
||||
private final UserRegistry userRegistry;
|
||||
|
||||
public GetGameStateHandler(
|
||||
ResponseDispatcher responseDispatcher,
|
||||
LobbyManager lobbyManager,
|
||||
UserRegistry userRegistry) {
|
||||
super(responseDispatcher);
|
||||
this.lobbyManager = lobbyManager;
|
||||
this.userRegistry = userRegistry;
|
||||
addCheck(new UserLoggedInCheck(userRegistry));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(GetGameStateRequest request) {
|
||||
Integer gameId = request.getGameId();
|
||||
String username = request.getUsername();
|
||||
|
||||
Lobby lobby = null;
|
||||
if (gameId != null) {
|
||||
lobby = lobbyManager.getLobby(LobbyId.of(gameId));
|
||||
if (lobby == null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (username == null) {
|
||||
var maybeUser = userRegistry.getBySessionId(request.getSessionId());
|
||||
if (maybeUser.isEmpty()) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
|
||||
return;
|
||||
}
|
||||
User u = maybeUser.get();
|
||||
username = u.getName();
|
||||
}
|
||||
|
||||
lobby = lobbyManager.getLobbyByUsername(username);
|
||||
if (lobby == null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var game = lobby.getGameController();
|
||||
if (game == null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "GAME_NOT_STARTED", "Game not started"));
|
||||
return;
|
||||
}
|
||||
|
||||
responseDispatcher.dispatch(new GetGameStateResponse(request.getContext(), game, username));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||
|
||||
public class GetGameStateParser implements CommandParser<GetGameStateRequest> {
|
||||
@Override
|
||||
public GetGameStateRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
RequestParameterAccessor accessor =
|
||||
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||
Integer gameId = accessor.optional("GAME_ID", null, Integer::parseInt);
|
||||
String username = accessor.optional("USERNAME", null);
|
||||
return new GetGameStateRequest(primitiveRequest.context(), username, gameId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
|
||||
/**
|
||||
* Request object for `GET_GAME_STATE`.
|
||||
*
|
||||
* <p>Either `USERNAME` or `GAME_ID` may be provided by the client. If both are omitted the handler
|
||||
* will resolve the username from the session.
|
||||
*/
|
||||
public class GetGameStateRequest extends Request {
|
||||
private final String username;
|
||||
private final Integer gameId;
|
||||
|
||||
public GetGameStateRequest(RequestContext context, String username, Integer gameId) {
|
||||
super(context);
|
||||
this.username = username;
|
||||
this.gameId = gameId;
|
||||
}
|
||||
|
||||
/** Returns the optional username provided in the request. */
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
/** Returns the optional game id provided in the request. */
|
||||
public Integer getGameId() {
|
||||
return gameId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Card;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Rank;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Suit;
|
||||
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.state.GameState;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
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.builder.ResponseBodyBuilder;
|
||||
import java.util.List;
|
||||
|
||||
/** Response carrying a snapshot of the current game state using the project's wire format. */
|
||||
public class GetGameStateResponse extends SuccessResponse {
|
||||
public GetGameStateResponse(
|
||||
RequestContext context, GameController game, String requestingUsername) {
|
||||
super(context, buildBody(game.getState(), requestingUsername));
|
||||
}
|
||||
|
||||
private static ResponseBody buildBody(GameState state, String requestingUsername) {
|
||||
int globalCurrentBet = computeGlobalCurrentBet(state);
|
||||
|
||||
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("DEALER", state.getDealerIndex());
|
||||
builder.param("ACTIVE_PLAYER", state.getCurrentPlayerIndex());
|
||||
|
||||
appendCommunityCards(builder, state);
|
||||
appendPlayers(builder, state, requestingUsername);
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static int computeGlobalCurrentBet(GameState state) {
|
||||
int globalCurrentBet = 0;
|
||||
for (Player p : state.getPlayers()) {
|
||||
int b = state.getCurrentBet(p.getId());
|
||||
if (b > globalCurrentBet) {
|
||||
globalCurrentBet = b;
|
||||
}
|
||||
}
|
||||
return globalCurrentBet;
|
||||
}
|
||||
|
||||
private static void appendCommunityCards(ResponseBodyBuilder builder, GameState state) {
|
||||
for (Card c : state.getCommunityCards()) {
|
||||
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) {
|
||||
for (Player p : state.getPlayers()) {
|
||||
PlayerId pid = p.getId();
|
||||
builder.block(
|
||||
"PLAYER",
|
||||
pblock -> {
|
||||
pblock.param("NAME", pid.value());
|
||||
pblock.param("CHIPS", p.getChips());
|
||||
pblock.param("BET", state.getCurrentBet(pid));
|
||||
pblock.param("STATE", p.isFolded() ? "FOLDED" : "ACTIVE");
|
||||
|
||||
if (requestingUsername != null && requestingUsername.equals(pid.value())) {
|
||||
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()));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static String rankToWire(Rank r) {
|
||||
return switch (r) {
|
||||
case TWO -> "2";
|
||||
case THREE -> "3";
|
||||
case FOUR -> "4";
|
||||
case FIVE -> "5";
|
||||
case SIX -> "6";
|
||||
case SEVEN -> "7";
|
||||
case EIGHT -> "8";
|
||||
case NINE -> "9";
|
||||
case TEN -> "10";
|
||||
case JACK -> "J";
|
||||
case QUEEN -> "Q";
|
||||
case KING -> "K";
|
||||
case ACE -> "A";
|
||||
};
|
||||
}
|
||||
|
||||
private static String suitToWire(Suit s) {
|
||||
return switch (s) {
|
||||
case HEARTS -> "H";
|
||||
case DIAMONDS -> "D";
|
||||
case CLUBS -> "C";
|
||||
case SPADES -> "S";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
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.ErrorResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Handler for the `RAISE` command.
|
||||
*
|
||||
* <p>This handler validates the request (amount >= 0), resolves the user and target lobby (by
|
||||
* `GAME_ID` when provided or by session username otherwise), checks that a game is running and then
|
||||
* forwards the action to the {@code GameController}.
|
||||
*
|
||||
* <p>On failure the handler dispatches an {@link ErrorResponse} with an appropriate error code
|
||||
* (e.g. {@code INVALID_AMOUNT}, {@code NOT_IN_LOBBY}, {@code LOBBY_NOT_FOUND}, {@code
|
||||
* GAME_NOT_STARTED}, {@code GAME_ACTION_FAILED}). On success it dispatches an {@link OkResponse}.
|
||||
*/
|
||||
public class PlayerRaiseHandler extends CommandHandler<PlayerRaiseRequest> {
|
||||
private final UserRegistry userRegistry;
|
||||
private final LobbyManager lobbyManager;
|
||||
|
||||
/**
|
||||
* Creates a new {@code PlayerRaiseHandler}.
|
||||
*
|
||||
* @param responseDispatcher dispatcher used to send responses back to the client
|
||||
* @param userRegistry registry to resolve users from session ids
|
||||
* @param lobbyManager manager used to lookup lobbies and their game controllers
|
||||
*/
|
||||
public PlayerRaiseHandler(
|
||||
ResponseDispatcher responseDispatcher,
|
||||
UserRegistry userRegistry,
|
||||
LobbyManager lobbyManager) {
|
||||
super(responseDispatcher);
|
||||
this.userRegistry = userRegistry;
|
||||
this.lobbyManager = lobbyManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the raise request: validate parameters, resolve lobby and game, and forward the raise
|
||||
* action to the game controller. Sends an {@link ErrorResponse} on failure or an {@link
|
||||
* OkResponse} on success.
|
||||
*
|
||||
* @param request the parsed {@link PlayerRaiseRequest}
|
||||
*/
|
||||
@Override
|
||||
public void execute(PlayerRaiseRequest request) {
|
||||
int amount = request.getAmount();
|
||||
|
||||
if (amount < 0) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "INVALID_AMOUNT", "Amount must be non-negative"));
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
|
||||
userRegistry.getBySessionId(request.getSessionId());
|
||||
if (opt.isEmpty()) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
|
||||
return;
|
||||
}
|
||||
|
||||
String username = opt.get().getName();
|
||||
|
||||
Integer gameId = request.getGameId();
|
||||
var lobby =
|
||||
(gameId != null)
|
||||
? lobbyManager.getLobby(LobbyId.of(gameId))
|
||||
: lobbyManager.getLobbyByUsername(username);
|
||||
|
||||
if (lobby == null) {
|
||||
if (gameId != null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||
} else {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
GameController game = lobby.getGameController();
|
||||
if (game == null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "GAME_NOT_STARTED", "Game not started"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
game.playerRaise(PlayerId.of(username), amount);
|
||||
} catch (RuntimeException e) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(request.getContext(), "GAME_ACTION_FAILED", e.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||
|
||||
/**
|
||||
* Parser for the `RAISE` command.
|
||||
*
|
||||
* <p>Parses the request parameters and builds a {@link PlayerRaiseRequest}. Expected parameters:
|
||||
*
|
||||
* <ul>
|
||||
* <li>`GAME_ID` (optional) - numeric id of the lobby/game to target
|
||||
* <li>`AMOUNT` (required) - amount to raise
|
||||
* </ul>
|
||||
*/
|
||||
public class PlayerRaiseParser implements CommandParser<PlayerRaiseRequest> {
|
||||
@Override
|
||||
public PlayerRaiseRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
RequestParameterAccessor accessor =
|
||||
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||
|
||||
Integer gameId = null;
|
||||
try {
|
||||
gameId = accessor.optional("GAME_ID", null, Integer::parseInt);
|
||||
} catch (Exception e) {
|
||||
// parse handled by framework
|
||||
}
|
||||
|
||||
int amount = accessor.require("AMOUNT", Integer::parseInt);
|
||||
|
||||
return new PlayerRaiseRequest(primitiveRequest.context(), gameId, amount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
|
||||
/**
|
||||
* Request for the `RAISE` command.
|
||||
*
|
||||
* <p>Contains the optional target `gameId` and the `amount` the player wants to raise.
|
||||
*/
|
||||
public class PlayerRaiseRequest extends Request {
|
||||
private final Integer gameId;
|
||||
private final int amount;
|
||||
|
||||
/**
|
||||
* Creates a new {@code PlayerRaiseRequest}.
|
||||
*
|
||||
* @param context the request context
|
||||
* @param gameId optional game id of the targeted lobby (may be {@code null})
|
||||
* @param amount the raise amount (non-negative)
|
||||
*/
|
||||
public PlayerRaiseRequest(RequestContext context, Integer gameId, int amount) {
|
||||
super(context);
|
||||
this.gameId = gameId;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the optional target game id.
|
||||
*
|
||||
* @return the game id or {@code null} if not provided
|
||||
*/
|
||||
public Integer getGameId() {
|
||||
return gameId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested raise amount.
|
||||
*
|
||||
* @return the raise amount
|
||||
*/
|
||||
public int getAmount() {
|
||||
return amount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list;
|
||||
|
||||
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.response.dispatcher.ResponseDispatcher;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Handler for the `GET_LOBBY_LIST` command.
|
||||
*
|
||||
* <p>Queries the {@link LobbyManager} for all available lobbies and dispatches a {@link
|
||||
* GetLobbyListResponse} containing basic metadata for each lobby (`ID`, `NAME`, `PLAYER_COUNT`).
|
||||
*/
|
||||
public class GetLobbyListHandler extends CommandHandler<GetLobbyListRequest> {
|
||||
private final LobbyManager lobbyManager;
|
||||
|
||||
public GetLobbyListHandler(ResponseDispatcher responseDispatcher, LobbyManager lobbyManager) {
|
||||
super(responseDispatcher);
|
||||
this.lobbyManager = lobbyManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(GetLobbyListRequest request) {
|
||||
Collection<ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby> l =
|
||||
lobbyManager.getAllLobbies();
|
||||
responseDispatcher.dispatch(new GetLobbyListResponse(request.getContext(), l));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||
|
||||
/**
|
||||
* Parser for the `GET_LOBBY_LIST` command.
|
||||
*
|
||||
* <p>Parses the incoming primitive request and produces a {@link GetLobbyListRequest}. This command
|
||||
* takes no parameters.
|
||||
*/
|
||||
public class GetLobbyListParser implements CommandParser<GetLobbyListRequest> {
|
||||
@Override
|
||||
public GetLobbyListRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
return new GetLobbyListRequest(primitiveRequest.context());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
|
||||
/**
|
||||
* Request object for the `GET_LOBBY_LIST` command.
|
||||
*
|
||||
* <p>Contains only the request context; this command does not require parameters.
|
||||
*/
|
||||
public class GetLobbyListRequest extends Request {
|
||||
public GetLobbyListRequest(RequestContext context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
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 java.util.Collection;
|
||||
|
||||
/**
|
||||
* Success response for `GET_LOBBY_LIST`.
|
||||
*
|
||||
* <p>Builds a `LOBBIES` collection with repeated `LOBBY` blocks containing `ID`, `NAME` and
|
||||
* `PLAYER_COUNT`.
|
||||
*/
|
||||
public class GetLobbyListResponse extends SuccessResponse {
|
||||
public GetLobbyListResponse(RequestContext context, Collection<Lobby> lobbies) {
|
||||
super(
|
||||
context,
|
||||
ResponseBody.builder()
|
||||
.block(
|
||||
"LOBBIES",
|
||||
lobbies_block -> {
|
||||
for (Lobby lobby : lobbies) {
|
||||
lobbies_block.block(
|
||||
"LOBBY",
|
||||
lb -> {
|
||||
lb.param("ID", lobby.getId().value());
|
||||
lb.param("NAME", lobby.getName());
|
||||
lb.param(
|
||||
"PLAYER_COUNT",
|
||||
lobby.getPlayerNames().size());
|
||||
});
|
||||
}
|
||||
})
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status;
|
||||
|
||||
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.ErrorResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Handler for the `GET_LOBBY_STATUS` command.
|
||||
*
|
||||
* <p>Resolves the target lobby either by the optional `ID` parameter or by `USERNAME`. If both are
|
||||
* omitted the handler requires the session to be associated with a logged-in user (see the inline
|
||||
* pre-execution check). On success a {@link GetLobbyStatusResponse} is dispatched, otherwise an
|
||||
* {@link ErrorResponse} with code `LOBBY_NOT_FOUND` is sent.
|
||||
*/
|
||||
public class GetLobbyStatusHandler extends CommandHandler<GetLobbyStatusRequest> {
|
||||
private final LobbyManager lobbyManager;
|
||||
private final UserRegistry userRegistry;
|
||||
|
||||
public GetLobbyStatusHandler(
|
||||
ResponseDispatcher responseDispatcher,
|
||||
LobbyManager lobbyManager,
|
||||
UserRegistry userRegistry) {
|
||||
super(responseDispatcher);
|
||||
this.lobbyManager = lobbyManager;
|
||||
this.userRegistry = userRegistry;
|
||||
|
||||
// Only require logged-in session if the request does not provide an explicit ID
|
||||
// or USERNAME
|
||||
addCheck(
|
||||
request -> {
|
||||
if (!(request instanceof GetLobbyStatusRequest)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
GetLobbyStatusRequest r = (GetLobbyStatusRequest) request;
|
||||
if (r.getId() != null || r.getUsername() != null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
var maybeUser = userRegistry.getBySessionId(r.getSessionId());
|
||||
if (maybeUser.isPresent()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(
|
||||
new ErrorResponse(
|
||||
r.getContext(),
|
||||
"USER_NOT_LOGGED_IN",
|
||||
"Request requires an associated logged-in user"));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(GetLobbyStatusRequest request) {
|
||||
// Resolve username from request or session when no explicit ID/USERNAME
|
||||
// provided
|
||||
String targetUsername = request.getUsername();
|
||||
if (targetUsername == null) {
|
||||
var maybeUser = userRegistry.getBySessionId(request.getSessionId());
|
||||
if (maybeUser.isPresent()) {
|
||||
targetUsername = maybeUser.get().getName();
|
||||
}
|
||||
}
|
||||
|
||||
var lobby =
|
||||
(request.getId() != null)
|
||||
? lobbyManager.getLobby(LobbyId.of(request.getId()))
|
||||
: (targetUsername != null
|
||||
? lobbyManager.getLobbyByUsername(targetUsername)
|
||||
: null);
|
||||
|
||||
if (lobby == null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
responseDispatcher.dispatch(new GetLobbyStatusResponse(request.getContext(), lobby));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||
|
||||
public class GetLobbyStatusParser implements CommandParser<GetLobbyStatusRequest> {
|
||||
/**
|
||||
* Parse the incoming primitive request into a {@link GetLobbyStatusRequest}.
|
||||
*
|
||||
* <p>Supported optional parameters:
|
||||
*
|
||||
* <ul>
|
||||
* <li>`ID` (int) — numeric lobby id to query
|
||||
* <li>`USERNAME` (String) — username to lookup the lobby for
|
||||
* </ul>
|
||||
*
|
||||
* If both are omitted the handler may require a logged-in session.
|
||||
*
|
||||
* @param primitiveRequest raw request containing parameters and context
|
||||
* @return parsed {@link GetLobbyStatusRequest}
|
||||
*/
|
||||
@Override
|
||||
public GetLobbyStatusRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
RequestParameterAccessor accessor =
|
||||
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||
Integer id = null;
|
||||
try {
|
||||
id = accessor.optional("ID", null, Integer::parseInt);
|
||||
} catch (Exception e) {
|
||||
// parse error handled elsewhere
|
||||
}
|
||||
String username = accessor.optional("USERNAME", null);
|
||||
return new GetLobbyStatusRequest(primitiveRequest.context(), id, username);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
|
||||
public class GetLobbyStatusRequest extends Request {
|
||||
private final Integer id;
|
||||
private final String username;
|
||||
|
||||
/**
|
||||
* Create a new GetLobbyStatusRequest.
|
||||
*
|
||||
* @param context request context (session id, source, etc.)
|
||||
* @param id optional numeric lobby id to query, or null
|
||||
* @param username optional username to query the lobby for, or null
|
||||
*/
|
||||
public GetLobbyStatusRequest(RequestContext context, Integer id, String username) {
|
||||
super(context);
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
/** Returns the optional lobby id. */
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/** Returns the optional username. */
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Success response for `GET_LOBBY_STATUS`.
|
||||
*
|
||||
* <p>Builds a response with a single `LOBBY` block containing `ID`, `NAME` and a nested `PLAYERS`
|
||||
* collection with repeated `PLAYER` blocks (`USERNAME`, `READY`).
|
||||
*/
|
||||
public class GetLobbyStatusResponse extends SuccessResponse {
|
||||
/**
|
||||
* Create a new response for the provided {@link Lobby}.
|
||||
*
|
||||
* @param context request context to use for the response
|
||||
* @param lobby lobby domain object containing id, name and players
|
||||
*/
|
||||
public GetLobbyStatusResponse(RequestContext context, Lobby lobby) {
|
||||
super(
|
||||
context,
|
||||
ResponseBody.builder()
|
||||
.block(
|
||||
"LOBBY",
|
||||
lb -> {
|
||||
lb.param("ID", lobby.getId().value());
|
||||
lb.param("NAME", lobby.getName());
|
||||
lb.block(
|
||||
"PLAYERS",
|
||||
players -> {
|
||||
for (String p : lobby.getPlayerNames()) {
|
||||
players.block(
|
||||
"PLAYER",
|
||||
pb -> {
|
||||
pb.param("USERNAME", p);
|
||||
pb.param("READY", "false");
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
|
||||
/**
|
||||
* Handler for the `JOIN_LOBBY` command.
|
||||
*
|
||||
* <p>Resolves the username from the session (requires {@link UserLoggedInCheck}), looks up the
|
||||
* target lobby and attempts to add the user. On success an `+OK` response is dispatched; on failure
|
||||
* an appropriate {@link ErrorResponse} with one of the error codes `LOBBY_NOT_FOUND`,
|
||||
* `USER_NOT_LOGGED_IN` or `LOBBY_FULL_OR_ALREADY_IN` is returned.
|
||||
*/
|
||||
public class JoinLobbyHandler extends CommandHandler<JoinLobbyRequest> {
|
||||
private final LobbyManager lobbyManager;
|
||||
private final UserRegistry userRegistry;
|
||||
|
||||
/**
|
||||
* Create a new {@link JoinLobbyHandler}.
|
||||
*
|
||||
* @param responseDispatcher dispatcher used to send responses back to the client
|
||||
* @param lobbyManager manager providing lobby state and operations
|
||||
* @param userRegistry registry to resolve session -> user mappings
|
||||
*/
|
||||
public JoinLobbyHandler(
|
||||
ResponseDispatcher responseDispatcher,
|
||||
LobbyManager lobbyManager,
|
||||
UserRegistry userRegistry) {
|
||||
super(responseDispatcher);
|
||||
this.lobbyManager = lobbyManager;
|
||||
this.userRegistry = userRegistry;
|
||||
addCheck(new UserLoggedInCheck(userRegistry));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the join-lobby request.
|
||||
*
|
||||
* @param request the parsed {@link JoinLobbyRequest}
|
||||
*/
|
||||
@Override
|
||||
public void execute(JoinLobbyRequest request) {
|
||||
LobbyId lid = LobbyId.of(request.getId());
|
||||
var lobby = lobbyManager.getLobby(lid);
|
||||
if (lobby == null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
var maybeUser = userRegistry.getBySessionId(request.getContext().sessionId());
|
||||
if (maybeUser.isEmpty()) {
|
||||
// UserLoggedInCheck should normally prevent this; keep safe fallback
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(),
|
||||
"USER_NOT_LOGGED_IN",
|
||||
"No user associated with session"));
|
||||
return;
|
||||
}
|
||||
|
||||
User user = maybeUser.get();
|
||||
String username = user.getName();
|
||||
|
||||
boolean ok = lobbyManager.addPlayerToLobby(username, lid);
|
||||
if (!ok) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(),
|
||||
"LOBBY_FULL_OR_ALREADY_IN",
|
||||
"Lobby full or user already in lobby"));
|
||||
return;
|
||||
}
|
||||
|
||||
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||
|
||||
/**
|
||||
* Parser for the `JOIN_LOBBY` command.
|
||||
*
|
||||
* <p>Expected request parameters:
|
||||
*
|
||||
* <ul>
|
||||
* <li>`ID` (int) — numeric lobby identifier (required)
|
||||
* </ul>
|
||||
*
|
||||
* <p>The parser builds a {@link JoinLobbyRequest} containing the parsed lobby id and the original
|
||||
* request context.
|
||||
*/
|
||||
public class JoinLobbyParser implements CommandParser<JoinLobbyRequest> {
|
||||
/**
|
||||
* Parse the incoming primitive request into a {@link JoinLobbyRequest}.
|
||||
*
|
||||
* @param primitiveRequest the raw primitive request
|
||||
* @return a {@link JoinLobbyRequest} with the parsed lobby id and context
|
||||
*/
|
||||
@Override
|
||||
public JoinLobbyRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
RequestParameterAccessor accessor =
|
||||
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||
int id = accessor.require("ID", Integer::parseInt);
|
||||
return new JoinLobbyRequest(primitiveRequest.context(), id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
|
||||
/**
|
||||
* Request data for the `JOIN_LOBBY` command.
|
||||
*
|
||||
* <p>Contains the lobby id the client wants to join and inherits the {@link Request} contextual
|
||||
* information.
|
||||
*/
|
||||
public class JoinLobbyRequest extends Request {
|
||||
private final int id;
|
||||
|
||||
/**
|
||||
* Create a new {@link JoinLobbyRequest}.
|
||||
*
|
||||
* @param context the request context
|
||||
* @param id numeric lobby id to join
|
||||
*/
|
||||
public JoinLobbyRequest(RequestContext context, int id) {
|
||||
super(context);
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the lobby id requested by the client.
|
||||
*
|
||||
* @return numeric lobby id
|
||||
*/
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.game;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.BetAction;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.BlindAction;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.CallAction;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.FoldAction;
|
||||
@@ -180,6 +181,16 @@ public class GameController {
|
||||
engine.processAction(new RaiseAction(playerId, amount));
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a player's bet action by sending a BetAction to the GameEngine.
|
||||
*
|
||||
* @param playerId The ID of the player who is betting.
|
||||
* @param amount The amount the player is betting.
|
||||
*/
|
||||
public void playerBet(PlayerId playerId, int amount) {
|
||||
engine.processAction(new BetAction(playerId, amount));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current game state from the GameEngine.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.GameEngine;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.RoundManager;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.TurnManager;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.RuleEngine;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/** Represents a single lobby: id, name, players and an optional GameController. */
|
||||
public class Lobby {
|
||||
private static final Logger LOGGER = Logger.getLogger(Lobby.class.getName());
|
||||
|
||||
public enum AddResult {
|
||||
NOT_ADDED,
|
||||
ADDED,
|
||||
ADDED_AND_STARTED
|
||||
}
|
||||
|
||||
private final LobbyId id;
|
||||
private final String name;
|
||||
private final List<String> playerNames = new CopyOnWriteArrayList<>();
|
||||
private volatile GameController gameController;
|
||||
|
||||
public Lobby(LobbyId id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public LobbyId getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public List<String> getPlayerNames() {
|
||||
return List.copyOf(playerNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to add a player to this lobby.
|
||||
*
|
||||
* @return true if added, false if already present or full
|
||||
*/
|
||||
public boolean addPlayer(String playerName, int maxPlayers) {
|
||||
if (playerName == null) {
|
||||
return false;
|
||||
}
|
||||
synchronized (playerNames) {
|
||||
if (playerNames.contains(playerName)) {
|
||||
return false;
|
||||
}
|
||||
if (playerNames.size() >= maxPlayers) {
|
||||
return false;
|
||||
}
|
||||
playerNames.add(playerName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean removePlayer(String playerName) {
|
||||
return playerNames.remove(playerName);
|
||||
}
|
||||
|
||||
public void initGame(GameController controller) {
|
||||
this.gameController = controller;
|
||||
}
|
||||
|
||||
public GameController getGameController() {
|
||||
return gameController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically add a player and, if the lobby reached {@code autoStartPlayers} and no game exists
|
||||
* yet, create and start a game. The operation is synchronized on the internal player list to
|
||||
* avoid races when multiple joins happen concurrently.
|
||||
*
|
||||
* @return {@link AddResult} indicating whether the player was added and if a game was started
|
||||
*/
|
||||
public AddResult addPlayerAndMaybeStart(
|
||||
String playerName, int maxPlayers, int autoStartPlayers, int defaultStartChips) {
|
||||
if (playerName == null) {
|
||||
return AddResult.NOT_ADDED;
|
||||
}
|
||||
synchronized (playerNames) {
|
||||
if (playerNames.contains(playerName)) {
|
||||
return AddResult.NOT_ADDED;
|
||||
}
|
||||
if (playerNames.size() >= maxPlayers) {
|
||||
return AddResult.NOT_ADDED;
|
||||
}
|
||||
playerNames.add(playerName);
|
||||
|
||||
// If threshold reached and no game running, create and start game here
|
||||
if (gameController == null && playerNames.size() == autoStartPlayers) {
|
||||
try {
|
||||
GameState state = new GameState();
|
||||
GameEngine engine =
|
||||
new GameEngine(
|
||||
state,
|
||||
new RuleEngine(new ArrayList<>()),
|
||||
new RoundManager(),
|
||||
new TurnManager());
|
||||
GameController game = new GameController(engine);
|
||||
|
||||
for (String p : playerNames) {
|
||||
game.addPlayer(PlayerId.of(p), defaultStartChips);
|
||||
}
|
||||
|
||||
game.startGame();
|
||||
this.gameController = game;
|
||||
LOGGER.info(() -> "Auto-started game in lobby " + id.value());
|
||||
return AddResult.ADDED_AND_STARTED;
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.log(Level.WARNING, "Auto-start failed for lobby " + id.value(), e);
|
||||
return AddResult.ADDED;
|
||||
}
|
||||
}
|
||||
|
||||
return AddResult.ADDED;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||
|
||||
/** Listener interface for lobby lifecycle events. */
|
||||
public interface LobbyEventListener {
|
||||
/** Called when a game is automatically started in the given lobby. */
|
||||
void onGameStarted(LobbyId lobbyId);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||
|
||||
/** Typesafe wrapper for lobby IDs (1-8). */
|
||||
public record LobbyId(int value) {
|
||||
public static LobbyId of(int value) {
|
||||
return new LobbyId(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby.AddResult;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/** Manages dynamic creation of up to 8 lobbies and maps players to lobbies. */
|
||||
public class LobbyManager {
|
||||
private static final int MAX_LOBBIES = 8;
|
||||
private static final int DEFAULT_MAX_PLAYERS = 4;
|
||||
private static final int AUTO_START_PLAYERS = 4;
|
||||
private static final int DEFAULT_START_CHIPS = 20000;
|
||||
|
||||
private final Map<LobbyId, Lobby> activeLobbies = new ConcurrentHashMap<>();
|
||||
private final Map<String, LobbyId> playerToLobby = new ConcurrentHashMap<>();
|
||||
private final List<LobbyEventListener> listeners = new CopyOnWriteArrayList<>();
|
||||
private final int maxPlayersPerLobby;
|
||||
private static final Logger LOGGER = Logger.getLogger(LobbyManager.class.getName());
|
||||
|
||||
public LobbyManager() {
|
||||
this(DEFAULT_MAX_PLAYERS);
|
||||
}
|
||||
|
||||
public LobbyManager(int maxPlayersPerLobby) {
|
||||
this.maxPlayersPerLobby = maxPlayersPerLobby;
|
||||
}
|
||||
|
||||
/** Create a new lobby with an automatic id (1..8). Returns null if none available. */
|
||||
public synchronized LobbyId createNewLobby(String name) {
|
||||
if (activeLobbies.size() >= MAX_LOBBIES) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 1; i <= MAX_LOBBIES; i++) {
|
||||
LobbyId id = LobbyId.of(i);
|
||||
if (!activeLobbies.containsKey(id)) {
|
||||
Lobby lobby = new Lobby(id, name == null ? ("Room " + i) : name);
|
||||
activeLobbies.put(id, lobby);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Lobby getLobby(LobbyId id) {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
return activeLobbies.get(id);
|
||||
}
|
||||
|
||||
public Lobby getLobbyByUsername(String username) {
|
||||
LobbyId id = playerToLobby.get(username);
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
return activeLobbies.get(id);
|
||||
}
|
||||
|
||||
public boolean addPlayerToLobby(String username, LobbyId lobbyId) {
|
||||
Lobby lobby = getLobby(lobbyId);
|
||||
if (lobby == null) {
|
||||
return false;
|
||||
}
|
||||
AddResult result =
|
||||
lobby.addPlayerAndMaybeStart(
|
||||
username, maxPlayersPerLobby, AUTO_START_PLAYERS, DEFAULT_START_CHIPS);
|
||||
if (result != AddResult.NOT_ADDED) {
|
||||
playerToLobby.put(username, lobbyId);
|
||||
if (result == AddResult.ADDED_AND_STARTED) {
|
||||
LOGGER.info(
|
||||
() ->
|
||||
"Lobby "
|
||||
+ lobbyId.value()
|
||||
+ " reached "
|
||||
+ AUTO_START_PLAYERS
|
||||
+ " players; game started.");
|
||||
notifyGameStarted(lobbyId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void addListener(LobbyEventListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeListener(LobbyEventListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
private void notifyGameStarted(LobbyId lobbyId) {
|
||||
for (LobbyEventListener l : listeners) {
|
||||
try {
|
||||
l.onGameStarted(lobbyId);
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.warning(
|
||||
() ->
|
||||
"Listener threw while handling game-start for lobby "
|
||||
+ lobbyId.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean removePlayer(String username) {
|
||||
LobbyId id = playerToLobby.remove(username);
|
||||
if (id == null) {
|
||||
return false;
|
||||
}
|
||||
Lobby lobby = activeLobbies.get(id);
|
||||
if (lobby == null) {
|
||||
return false;
|
||||
}
|
||||
boolean removed = lobby.removePlayer(username);
|
||||
// If lobby becomes empty, remove it
|
||||
if (lobby.getPlayerNames().isEmpty()) {
|
||||
activeLobbies.remove(id);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
public Collection<Lobby> getAllLobbies() {
|
||||
return activeLobbies.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given action to every player username in the lobby identified by {@code lobbyId}.
|
||||
* This is a small helper that keeps iteration logic centralized and avoids leaking internal
|
||||
* collections to callers.
|
||||
*/
|
||||
public void broadcast(LobbyId lobbyId, java.util.function.Consumer<String> action) {
|
||||
Lobby lobby = getLobby(lobbyId);
|
||||
if (lobby == null) {
|
||||
return;
|
||||
}
|
||||
for (String username : lobby.getPlayerNames()) {
|
||||
action.accept(username);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
/** Represents an authenticated user on the server. */
|
||||
public class User {
|
||||
private final UserId id;
|
||||
private final String name;
|
||||
private String name;
|
||||
private SessionId sessionId;
|
||||
private Instant disconnectedAt;
|
||||
private final Queue<Message> messages;
|
||||
@@ -47,10 +47,20 @@ public class User {
|
||||
*
|
||||
* @return the user name
|
||||
*/
|
||||
public String getName() {
|
||||
public synchronized String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new display name for this user. Thread-safe; callers must ensure the registry is
|
||||
* updated to maintain uniqueness when needed.
|
||||
*
|
||||
* @param newName the new display name
|
||||
*/
|
||||
public synchronized void setName(String newName) {
|
||||
this.name = newName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the session currently associated with this user, if any.
|
||||
*
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/** Creates new users, resolving name conflicts automatically. */
|
||||
public class UserFactory {
|
||||
private final UserRegistry registry;
|
||||
private final AtomicInteger anonymousCounter = new AtomicInteger(1);
|
||||
private static final Logger LOGGER = LogManager.getLogger(UserFactory.class);
|
||||
|
||||
/**
|
||||
* Creates a new UserFactory backed by the given registry.
|
||||
@@ -18,16 +23,40 @@ public class UserFactory {
|
||||
/**
|
||||
* Creates and registers a new user with the given name and session. If the name is already
|
||||
* taken, a numeric suffix is appended and incremented until a free name is found (e.g.
|
||||
* Lars_001, Lars_002, ...).
|
||||
* Lars_001, Lars_002, ...). If the desiredName is null or empty, an automatic name of the form
|
||||
* "playerN" is assigned, incrementing N until a free name is found.
|
||||
*
|
||||
* @param desiredName the preferred display name
|
||||
* @param sessionId the session to associate with the new user
|
||||
* @param sessionId the session to associate with the new userwas
|
||||
* @return the newly created and registered user
|
||||
*/
|
||||
public User create(String desiredName, SessionId sessionId) {
|
||||
if (desiredName == null || desiredName.isBlank()) {
|
||||
// assign playerN names for anonymous logins
|
||||
while (true) {
|
||||
String candidate = "player" + anonymousCounter.getAndIncrement();
|
||||
var result = registry.registerIfAvailable(candidate, sessionId);
|
||||
if (result.isPresent()) {
|
||||
var user = result.get();
|
||||
LOGGER.info(
|
||||
"Registered user '{}' with id {} for session {}",
|
||||
user.getName(),
|
||||
user.getId().value(),
|
||||
sessionId == null ? "null" : sessionId.value());
|
||||
return user;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result = registry.registerIfAvailable(desiredName, sessionId);
|
||||
if (result.isPresent()) {
|
||||
return result.get();
|
||||
var user = result.get();
|
||||
LOGGER.info(
|
||||
"Registered user '{}' with id {} for session {}",
|
||||
user.getName(),
|
||||
user.getId().value(),
|
||||
sessionId == null ? "null" : sessionId.value());
|
||||
return user;
|
||||
}
|
||||
|
||||
int suffix = 1;
|
||||
@@ -35,7 +64,13 @@ public class UserFactory {
|
||||
String candidate = desiredName + "_" + String.format("%03d", suffix);
|
||||
result = registry.registerIfAvailable(candidate, sessionId);
|
||||
if (result.isPresent()) {
|
||||
return result.get();
|
||||
var user = result.get();
|
||||
LOGGER.info(
|
||||
"Registered user '{}' with id {} for session {}",
|
||||
user.getName(),
|
||||
user.getId().value(),
|
||||
sessionId == null ? "null" : sessionId.value());
|
||||
return user;
|
||||
}
|
||||
suffix++;
|
||||
}
|
||||
|
||||
@@ -185,4 +185,29 @@ public class UserRegistry {
|
||||
public Collection<User> getAllUsers() {
|
||||
return byId.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to change the username of an existing user. Ensures the new name is not already
|
||||
* taken and updates internal indices atomically.
|
||||
*
|
||||
* @param userId the id of the user to rename
|
||||
* @param newName the desired new name
|
||||
* @return true if the rename succeeded, false if the name was already taken or user not found
|
||||
*/
|
||||
public synchronized boolean changeUsername(UserId userId, String newName) {
|
||||
User user = byId.get(userId);
|
||||
if (user == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (byName.containsKey(newName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// remove old mapping and put new mapping
|
||||
byName.remove(user.getName());
|
||||
user.setName(newName);
|
||||
byName.put(newName, user);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
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.sessions.SessionId;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Helper to send responses to sessions identified by usernames. */
|
||||
public class SessionDispatcher {
|
||||
private final ResponseDispatcher responseDispatcher;
|
||||
private final UserRegistry userRegistry;
|
||||
|
||||
public SessionDispatcher(ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
|
||||
this.responseDispatcher = responseDispatcher;
|
||||
this.userRegistry = userRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a success response built from {@code body} to the user with {@code username}. If the
|
||||
* user is disconnected or unknown, the call is a no-op.
|
||||
*/
|
||||
public void dispatchToUser(String username, ResponseBody body) {
|
||||
Optional<User> opt = userRegistry.getByUsername(username);
|
||||
if (opt.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
User user = opt.get();
|
||||
Optional<SessionId> sid = user.getSessionId();
|
||||
if (sid.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
RequestContext ctx = new RequestContext(sid.get(), 0);
|
||||
SuccessResponse resp = new SuccessResponse(ctx, body) {};
|
||||
responseDispatcher.dispatch(resp);
|
||||
}
|
||||
|
||||
/** Broadcast the provided {@code body} to every player in the given lobby. */
|
||||
public void broadcastToLobby(Lobby lobby, ResponseBody body) {
|
||||
for (String username : lobby.getPlayerNames()) {
|
||||
dispatchToUser(username, body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 645 KiB |
|
Before Width: | Height: | Size: 651 KiB After Width: | Height: | Size: 461 KiB |
|
Before Width: | Height: | Size: 665 KiB After Width: | Height: | Size: 486 KiB |
|
Before Width: | Height: | Size: 659 KiB After Width: | Height: | Size: 475 KiB |
|
Before Width: | Height: | Size: 660 KiB After Width: | Height: | Size: 475 KiB |
|
Before Width: | Height: | Size: 657 KiB After Width: | Height: | Size: 471 KiB |
|
Before Width: | Height: | Size: 659 KiB After Width: | Height: | Size: 474 KiB |
|
Before Width: | Height: | Size: 661 KiB After Width: | Height: | Size: 476 KiB |
|
Before Width: | Height: | Size: 658 KiB After Width: | Height: | Size: 471 KiB |
|
Before Width: | Height: | Size: 663 KiB After Width: | Height: | Size: 482 KiB |
|
Before Width: | Height: | Size: 662 KiB After Width: | Height: | Size: 477 KiB |
|
Before Width: | Height: | Size: 663 KiB After Width: | Height: | Size: 472 KiB |
|
Before Width: | Height: | Size: 657 KiB After Width: | Height: | Size: 463 KiB |
|
Before Width: | Height: | Size: 664 KiB After Width: | Height: | Size: 477 KiB |
|
Before Width: | Height: | Size: 665 KiB After Width: | Height: | Size: 481 KiB |
|
Before Width: | Height: | Size: 648 KiB After Width: | Height: | Size: 462 KiB |
|
Before Width: | Height: | Size: 661 KiB After Width: | Height: | Size: 487 KiB |
|
Before Width: | Height: | Size: 656 KiB After Width: | Height: | Size: 475 KiB |
|
Before Width: | Height: | Size: 656 KiB After Width: | Height: | Size: 476 KiB |
|
Before Width: | Height: | Size: 653 KiB After Width: | Height: | Size: 471 KiB |
|
Before Width: | Height: | Size: 655 KiB After Width: | Height: | Size: 474 KiB |
|
Before Width: | Height: | Size: 658 KiB After Width: | Height: | Size: 477 KiB |
|
Before Width: | Height: | Size: 654 KiB After Width: | Height: | Size: 471 KiB |
|
Before Width: | Height: | Size: 660 KiB After Width: | Height: | Size: 481 KiB |
|
Before Width: | Height: | Size: 658 KiB After Width: | Height: | Size: 478 KiB |
|
Before Width: | Height: | Size: 659 KiB After Width: | Height: | Size: 470 KiB |
|
Before Width: | Height: | Size: 653 KiB After Width: | Height: | Size: 462 KiB |
|
Before Width: | Height: | Size: 660 KiB After Width: | Height: | Size: 475 KiB |
|
Before Width: | Height: | Size: 660 KiB After Width: | Height: | Size: 480 KiB |
|
Before Width: | Height: | Size: 644 KiB After Width: | Height: | Size: 455 KiB |
|
Before Width: | Height: | Size: 658 KiB After Width: | Height: | Size: 479 KiB |
|
Before Width: | Height: | Size: 652 KiB After Width: | Height: | Size: 468 KiB |
|
Before Width: | Height: | Size: 653 KiB After Width: | Height: | Size: 468 KiB |
|
Before Width: | Height: | Size: 650 KiB After Width: | Height: | Size: 464 KiB |
|
Before Width: | Height: | Size: 651 KiB After Width: | Height: | Size: 467 KiB |
|
Before Width: | Height: | Size: 654 KiB After Width: | Height: | Size: 470 KiB |
|
Before Width: | Height: | Size: 650 KiB After Width: | Height: | Size: 464 KiB |
|
Before Width: | Height: | Size: 656 KiB After Width: | Height: | Size: 474 KiB |
|
Before Width: | Height: | Size: 655 KiB After Width: | Height: | Size: 470 KiB |
|
Before Width: | Height: | Size: 656 KiB After Width: | Height: | Size: 465 KiB |
|
Before Width: | Height: | Size: 650 KiB After Width: | Height: | Size: 456 KiB |
|
Before Width: | Height: | Size: 657 KiB After Width: | Height: | Size: 470 KiB |
|
Before Width: | Height: | Size: 658 KiB After Width: | Height: | Size: 474 KiB |