Merge branch 'main' into feat/change-username-at-start-or-in-game
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ChatClient;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* responsible for the transferring of messages from the server to the ChatModel or from the
|
||||
* ChatViewController to the server
|
||||
*/
|
||||
public class ChatController {
|
||||
|
||||
private final String username;
|
||||
|
||||
private final ChatClient chatClient;
|
||||
|
||||
public ChatBoxController getChatBoxController() {
|
||||
return chatBoxController;
|
||||
}
|
||||
|
||||
private final ChatBoxController chatBoxController;
|
||||
private int lobbyId = -1;
|
||||
private final Timer timer;
|
||||
|
||||
public record ChatKey(ChatType type, @Nullable String targetUser) {
|
||||
public ChatKey(ChatType type) {
|
||||
this(type, null);
|
||||
}
|
||||
}
|
||||
|
||||
public Map<ChatKey, ChatModel> getChatModelMap() {
|
||||
return chatModelMap;
|
||||
}
|
||||
|
||||
private Map<ChatKey, ChatModel> chatModelMap;
|
||||
|
||||
private static final long REFRESH_TIME = 1000;
|
||||
|
||||
/**
|
||||
* Constructor, adds TimerTask to be sent to the server regularly
|
||||
*
|
||||
* @param username
|
||||
* @param clientService
|
||||
*/
|
||||
public ChatController(String username, ClientService clientService) {
|
||||
this.username = username;
|
||||
chatClient = new ChatClient(clientService);
|
||||
chatModelMap = new LinkedHashMap<>();
|
||||
this.chatBoxController = new ChatBoxController(username, this);
|
||||
this.timer = new Timer();
|
||||
timer.schedule(
|
||||
new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
receiveMessage();
|
||||
}
|
||||
},
|
||||
0,
|
||||
REFRESH_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to be activated, if a lobby has be chosen. It will update the UI and add a new
|
||||
* ChatModel to hold the Data for the Lobby Chat
|
||||
*
|
||||
* @param lobbyId
|
||||
*/
|
||||
public void setLobbyChat(int lobbyId) {
|
||||
this.lobbyId = lobbyId;
|
||||
ChatModel lobbyChatModel = new ChatModel(ChatType.LOBBY, username, lobbyId, null);
|
||||
chatModelMap.put(new ChatKey(ChatType.LOBBY), lobbyChatModel);
|
||||
this.chatBoxController.addChatTab("Lobby", lobbyChatModel);
|
||||
}
|
||||
|
||||
/** method to get all messages from the server */
|
||||
public void receiveMessage() {
|
||||
List<Message> newMessages = chatClient.getMessages();
|
||||
if (!newMessages.isEmpty()) {
|
||||
for (Message msg : newMessages) {
|
||||
switch (msg.getMessageType()) {
|
||||
case ChatType.GLOBAL:
|
||||
chatModelMap.get(new ChatKey(ChatType.GLOBAL)).addMessage(msg);
|
||||
break;
|
||||
case ChatType.LOBBY:
|
||||
if (msg.lobbyId == lobbyId) {
|
||||
chatModelMap
|
||||
.computeIfAbsent(
|
||||
new ChatKey(ChatType.LOBBY),
|
||||
(_key) ->
|
||||
new ChatModel(
|
||||
ChatType.LOBBY,
|
||||
username,
|
||||
msg.lobbyId,
|
||||
null))
|
||||
.addMessage(msg);
|
||||
}
|
||||
break;
|
||||
case ChatType.WHISPER:
|
||||
if (msg.target.equals(username)) {
|
||||
if (chatModelMap.containsKey(
|
||||
new ChatKey(ChatType.WHISPER, msg.sender))) {
|
||||
chatModelMap
|
||||
.get(new ChatKey(ChatType.WHISPER, msg.sender))
|
||||
.addMessage(msg);
|
||||
} else {
|
||||
chatBoxController.addWhisperChat(msg.sender);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* method to send a message to the server
|
||||
*
|
||||
* @param message
|
||||
*/
|
||||
public void onSendToNetwork(Message message) {
|
||||
chatClient.sendMessage(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.function.Consumer;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
|
||||
/**
|
||||
* ChatModel, stores the data for a specific chat
|
||||
*
|
||||
* <p>Holds the current state of a chat
|
||||
*/
|
||||
public class ChatModel {
|
||||
|
||||
private ArrayList<Consumer<Message>> listeners = new ArrayList<>();
|
||||
|
||||
public ArrayList<Message> messages;
|
||||
|
||||
private final ChatType chattype;
|
||||
|
||||
/** The person currently using this client */
|
||||
public final String username;
|
||||
|
||||
/** The person to send the message to If the chat is a whisper chat */
|
||||
private final String target;
|
||||
|
||||
private final IntegerProperty count;
|
||||
|
||||
public int lobbyId;
|
||||
|
||||
/**
|
||||
* Constructs a new ChatModel for a specific chat type.
|
||||
*
|
||||
* @param chattype The type of chat (e.g., GLOBAL, LOBBY, or WHISPER).
|
||||
* @param username The username of the current user.
|
||||
* @param lobbyId The ID of the lobby, or -1 if not applicable.
|
||||
* @param target The username of the whisper recipient, or null for other chat types.
|
||||
*/
|
||||
public ChatModel(ChatType chattype, String username, int lobbyId, String target) {
|
||||
this.messages = new ArrayList<Message>();
|
||||
this.chattype = chattype;
|
||||
this.username = username;
|
||||
this.count = new SimpleIntegerProperty(0);
|
||||
this.lobbyId = lobbyId;
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of chat this model represents.
|
||||
*
|
||||
* @return The {@link ChatType}.
|
||||
*/
|
||||
public ChatType getChattype() {
|
||||
return chattype;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new message to the history and notifies all registered listeners. This method is
|
||||
* synchronized to ensure thread safety when updating the message list.
|
||||
*
|
||||
* @param msg The {@link Message} to be added.
|
||||
*/
|
||||
public synchronized void addMessage(Message msg) {
|
||||
messages.add(msg);
|
||||
listeners.stream().forEach((l) -> l.accept(messages.getLast()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a listener to be notified whenever a new message is added to this model.
|
||||
*
|
||||
* @param listener A {@link Consumer} that processes the new {@link Message}.
|
||||
*/
|
||||
public void addListener(Consumer<Message> listener) {
|
||||
this.listeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the target user for this chat, primarily used for whispers.
|
||||
*
|
||||
* @return The target username or null.
|
||||
*/
|
||||
public String getTarget() {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||
|
||||
/** Describing the Type of the Chat */
|
||||
public enum ChatType {
|
||||
GLOBAL,
|
||||
LOBBY,
|
||||
WHISPER
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||
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.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/** Message Object for internal handling of Chat-Messages */
|
||||
public class Message {
|
||||
private final ChatType type;
|
||||
private final String message;
|
||||
public String sender;
|
||||
public String timestamp;
|
||||
public int lobbyId = 0;
|
||||
public String target = null;
|
||||
|
||||
/**
|
||||
* Constructs a Message with a provided timestamp. Typically used when reconstructing messages
|
||||
* received from the server. Used for Messages in the Lobby Chat.
|
||||
*
|
||||
* @param lobbyId The ID of the lobby, or -1 if not applicable.
|
||||
* @param sender The username of the message creator.
|
||||
* @param timestamp The formatted time string (e.g., "HH:mm").
|
||||
* @param message The actual text content of the message.
|
||||
*/
|
||||
private Message(int lobbyId, String sender, String timestamp, String message) {
|
||||
this.type = ChatType.LOBBY;
|
||||
this.lobbyId = lobbyId;
|
||||
this.sender = sender;
|
||||
this.target = null;
|
||||
this.timestamp = timestamp;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Message with a provided timestamp. Typically used when reconstructing messages
|
||||
* received from the server. Used for Messages in the WHISPER and GLOBAL Chat.
|
||||
*
|
||||
* @param type The chat category (e.g., GLOBAL or WHISPER).
|
||||
* @param sender The username of the message creator.
|
||||
* @param timestamp The formatted time string (e.g., "HH:mm").
|
||||
* @param message The actual text content of the message.
|
||||
*/
|
||||
private Message(ChatType type, String sender, String target, String timestamp, String message) {
|
||||
this.type = type;
|
||||
this.lobbyId = -1;
|
||||
this.sender = sender;
|
||||
this.target = target;
|
||||
this.timestamp = timestamp;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new Message for the current user. Automatically generates a timestamp based on
|
||||
* the local system time ("HH:mm").
|
||||
*
|
||||
* @param type The chat category (e.g., GLOBAL, LOBBY, or WHISPER).
|
||||
* @param lobbyId The ID of the lobby, or -1 if not applicable.
|
||||
* @param sender The username of the current user.
|
||||
* @param target The username of the recipient (for whispers).
|
||||
* @param message The actual text content to be sent.
|
||||
*/
|
||||
public Message(ChatType type, int lobbyId, String sender, String target, String message) {
|
||||
this.type = type;
|
||||
this.lobbyId = lobbyId;
|
||||
this.sender = sender;
|
||||
this.target = target;
|
||||
this.message = message;
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");
|
||||
this.timestamp = now.format(formatter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text content of the message.
|
||||
*
|
||||
* @return The message string.
|
||||
*/
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of chat this message belongs to.
|
||||
*
|
||||
* @return The {@link ChatType}.
|
||||
*/
|
||||
public ChatType getMessageType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the message object into a string representation compatible with the network protocol
|
||||
* arguments.
|
||||
*
|
||||
* @return A formatted string containing all message attributes for server transmission.
|
||||
*/
|
||||
public String toArgsString() {
|
||||
String gameIdString = "";
|
||||
if (lobbyId >= 0) {
|
||||
gameIdString = " GAME=" + lobbyId;
|
||||
} else {
|
||||
gameIdString = " GAME='-1'";
|
||||
}
|
||||
return String.format(
|
||||
"TYPE=%s%s USER='%s' TARGET='%s' TIME='%s' TEXT='%s'",
|
||||
this.type.toString(),
|
||||
gameIdString,
|
||||
this.sender,
|
||||
this.target,
|
||||
this.timestamp,
|
||||
this.message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a list of network request parameters to reconstruct a Message object. Handles
|
||||
* different chat types (GLOBAL, LOBBY, WHISPER) and their specific requirements.
|
||||
*
|
||||
* @param parameters A list of {@link RequestParameter} received from the network.
|
||||
* @return A new {@link Message} instance populated with the parsed data.
|
||||
*/
|
||||
public static Message toMessageReqPars(List<RequestParameter> parameters) {
|
||||
String typeString = getParString(parameters, "TYPE");
|
||||
ChatType type = ChatType.valueOf(typeString);
|
||||
return switch (type) {
|
||||
case GLOBAL ->
|
||||
new Message(
|
||||
ChatType.GLOBAL,
|
||||
getParString(parameters, "USER"),
|
||||
null,
|
||||
getParString(parameters, "TIME"),
|
||||
getParString(parameters, "TEXT"));
|
||||
case LOBBY ->
|
||||
new Message(
|
||||
Integer.parseInt(getParString(parameters, "GAME")),
|
||||
getParString(parameters, "USER"),
|
||||
getParString(parameters, "TIME"),
|
||||
getParString(parameters, "TEXT"));
|
||||
case WHISPER ->
|
||||
new Message(
|
||||
ChatType.WHISPER,
|
||||
getParString(parameters, "USER"),
|
||||
getParString(parameters, "TARGET"),
|
||||
getParString(parameters, "TIME"),
|
||||
getParString(parameters, "TEXT"));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to extract a specific parameter value by its key.
|
||||
*
|
||||
* @param parameters The list of parameters to search.
|
||||
* @param keyString The key to look for.
|
||||
* @return The value associated with the key.
|
||||
* @throws RuntimeException if the key is not found.
|
||||
*/
|
||||
private static @NonNull String getParString(
|
||||
List<RequestParameter> parameters, String keyString) {
|
||||
return getParString(parameters, keyString, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to extract a specific parameter value by its key, with a fallback default
|
||||
* value.
|
||||
*
|
||||
* @param parameters The list of parameters to search.
|
||||
* @param keyString The key to look for.
|
||||
* @param defaultVal The value to return if the key is missing.
|
||||
* @return The found value or the default value.
|
||||
*/
|
||||
private static @NonNull String getParString(
|
||||
List<RequestParameter> parameters, String keyString, String defaultVal) {
|
||||
Optional<String> parOption =
|
||||
parameters.stream()
|
||||
.filter((p) -> keyString.equals(p.key()))
|
||||
.findFirst()
|
||||
.map(RequestParameter::value);
|
||||
if (parOption.isEmpty()) {
|
||||
if (defaultVal == null) {
|
||||
throw new RuntimeException("No " + keyString + " found");
|
||||
} else {
|
||||
return defaultVal;
|
||||
}
|
||||
}
|
||||
return parOption.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the message object into a network response body using the provided builder.
|
||||
*
|
||||
* @param builder The {@link ResponseBodyBuilder} used to construct the response.
|
||||
* @return The built {@link ResponseBody} containing the message data.
|
||||
*/
|
||||
public ResponseBody toResponse(ResponseBodyBuilder builder) {
|
||||
builder.param("TYPE", type.name());
|
||||
builder.param("GAME", lobbyId);
|
||||
builder.param("USER", this.sender);
|
||||
if (target != null) {
|
||||
builder.param("TARGET", target);
|
||||
}
|
||||
builder.param("TIME", this.timestamp);
|
||||
builder.param("TEXT", this.message);
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.game;
|
||||
|
||||
/** Represents a playing card with a value and suit. */
|
||||
public class Card {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.game;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents the current state of the poker game, including the phase, pot size, current bet,
|
||||
* dealer position, active player, community cards, and player information.
|
||||
*/
|
||||
public class GameState {
|
||||
public String phase;
|
||||
public int pot;
|
||||
public int currentBet;
|
||||
public int dealer;
|
||||
public int activePlayer;
|
||||
public int winnerIndex = -1;
|
||||
|
||||
public List<Card> communityCards = new ArrayList<>();
|
||||
public List<Player> players = new ArrayList<>();
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.game;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a player in the poker game, including their name, chip count, current bet, state
|
||||
* (e.g., "active", "folded"), and their hole cards.
|
||||
*/
|
||||
public class Player {
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* The ChatClient class is responsible for sending messages to the server and retrieving messages
|
||||
* from the server. It uses the ClientService to send commands and receive responses from the
|
||||
* server.
|
||||
*/
|
||||
public class ChatClient {
|
||||
|
||||
private final ClientService clientService;
|
||||
private final Logger logger;
|
||||
|
||||
/**
|
||||
* Constructs a ChatClient with the given ClientService for communication.
|
||||
*
|
||||
* @param clientService The ClientService instance used to send commands and receive responses
|
||||
* from the server.
|
||||
*/
|
||||
public ChatClient(ClientService clientService) {
|
||||
this.clientService = clientService;
|
||||
this.logger = LogManager.getLogger(ChatClient.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a Message to the server by converting it to a string format and sending a "SEND_MESSAGE"
|
||||
* command with the message content as arguments.
|
||||
*
|
||||
* @param message The Message object to be sent to the server.
|
||||
*/
|
||||
public void sendMessage(Message message) {
|
||||
String request = "SEND_MESSAGE " + message.toArgsString();
|
||||
logger.info("Writing to server: " + request);
|
||||
clientService.processCommand(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve messages from the server by first sending a "GET_MESSAGE_COUNT" command to determine
|
||||
* how many messages are available and then sending "GET_NEXT_MESSAGE" commands in a loop to
|
||||
* retrieve each message. The retrieved messages are parsed into Message objects and returned as
|
||||
* a list.
|
||||
*
|
||||
* @return A list of Message objects representing the messages retrieved from the server.
|
||||
*/
|
||||
public List<Message> getMessages() {
|
||||
logger.info("Asking server for new messages");
|
||||
List<RequestParameter> countStr =
|
||||
ClientService.convertToRequestParameters(
|
||||
clientService.processCommand("GET_MESSAGE_COUNT"));
|
||||
RequestParameter countRes = countStr.getFirst();
|
||||
if (!countRes.key().equals("COUNT")) {
|
||||
logger.error("Not the right response from server");
|
||||
}
|
||||
int count = Integer.parseInt(countRes.value());
|
||||
logger.info("Got " + count + " messages");
|
||||
ArrayList<Message> messages = new ArrayList<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
List<RequestParameter> msgRes =
|
||||
ClientService.convertToRequestParameters(
|
||||
clientService.processCommand("GET_NEXT_MESSAGE"));
|
||||
Message msg = Message.toMessageReqPars(msgRes);
|
||||
messages.add(msg);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* The ClientService class is responsible for managing the connection to the server, sending
|
||||
* commands, and receiving responses. It uses a TcpTransport to communicate with the server and an
|
||||
* ExecutorService to handle asynchronous requests.
|
||||
*/
|
||||
public class ClientService {
|
||||
|
||||
private final TcpTransport clienttcptransport;
|
||||
private final Socket socket;
|
||||
|
||||
private final ExecutorService executor;
|
||||
private final boolean offlineMode;
|
||||
|
||||
public static ArrayList<String> response;
|
||||
private final AtomicInteger idGenerator;
|
||||
private Logger logger;
|
||||
|
||||
/**
|
||||
* Constructs a ClientService with the given server IP and port. It establishes a socket
|
||||
* connection to the server and initializes the TcpTransport and ExecutorService for
|
||||
* communication.
|
||||
*
|
||||
* @param ip The IP address of the server to connect to.
|
||||
* @param port The port number of the server to connect to.
|
||||
*/
|
||||
public ClientService(String ip, int port) {
|
||||
|
||||
this.idGenerator = new AtomicInteger(0);
|
||||
|
||||
this.logger = LogManager.getLogger(ClientService.class);
|
||||
|
||||
this.offlineMode = false;
|
||||
|
||||
try {
|
||||
socket = new Socket(ip, port);
|
||||
clienttcptransport = new TcpTransport(socket);
|
||||
logger.info("Connected to server at " + ip);
|
||||
} catch (IOException i) {
|
||||
throw new RuntimeException(i);
|
||||
}
|
||||
|
||||
executor = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a ClientService in offline mode. No network connection will be attempted and calls
|
||||
* to processCommand will throw a RuntimeException.
|
||||
*
|
||||
* @param offline true to create an offline (no-network) client service
|
||||
*/
|
||||
public ClientService(boolean offline) {
|
||||
this.idGenerator = new AtomicInteger(0);
|
||||
this.offlineMode = offline;
|
||||
this.socket = null;
|
||||
this.clienttcptransport = null;
|
||||
this.executor = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
|
||||
/** Returns true if this ClientService is running in offline mode (no network). */
|
||||
public boolean isOffline() {
|
||||
return offlineMode;
|
||||
}
|
||||
|
||||
static Pattern responseRex =
|
||||
Pattern.compile(
|
||||
"(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[+-]?[\\d\\w:]+))");
|
||||
|
||||
/**
|
||||
* Removes escape characters from a string, specifically converting escaped single quotes (\')
|
||||
* back to regular single quotes (').
|
||||
*
|
||||
* @param input The escaped string to process.
|
||||
* @return The unescaped string.
|
||||
*/
|
||||
private static String unescape(String input) {
|
||||
return input.replaceAll("\\\\'", "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of raw string parameters into a list of {@link RequestParameter} objects. It
|
||||
* uses a regex matcher to distinguish between quoted strings (which are unescaped) and
|
||||
* primitive values.
|
||||
*
|
||||
* @param input A list of raw strings to be parsed.
|
||||
* @return A list of parsed {@link RequestParameter} objects.
|
||||
* @throws RuntimeException if a parameter does not match the expected format.
|
||||
*/
|
||||
public static List<RequestParameter> convertToRequestParameters(List<String> input) {
|
||||
return input.stream()
|
||||
.map((String parString) -> responseRex.matcher(parString))
|
||||
.filter(Matcher::matches)
|
||||
.map(
|
||||
(m) -> {
|
||||
if (!(m.group("string") == null)) {
|
||||
return new RequestParameter(
|
||||
m.group("key"), unescape(m.group("string")));
|
||||
} else if (!(m.group("primVal") == null)) {
|
||||
return new RequestParameter(m.group("key"), m.group("primVal"));
|
||||
} else {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a command to the server and processes the multi-line response. It handles the protocol
|
||||
* handshake (expecting +OK), strips leading tabs from response lines, and collects them until
|
||||
* the "END" marker is reached.
|
||||
*
|
||||
* @param message The raw command string to be sent to the transport layer.
|
||||
* @return A list of response lines received from the server (excluding protocol markers).
|
||||
* @throws RuntimeException if the server responds with an error or if a communication failure
|
||||
* occurs.
|
||||
*/
|
||||
protected List<String> processCommand(String message) {
|
||||
List<String> response = new ArrayList<>();
|
||||
sendRequest(
|
||||
() -> {
|
||||
try {
|
||||
writeToTransport(message);
|
||||
String responseText = null;
|
||||
|
||||
responseText = clienttcptransport.read().payload();
|
||||
logger.info("Raw message '" + responseText + "'");
|
||||
Boolean success = null;
|
||||
int count = 0;
|
||||
for (String line : responseText.split("\n")) {
|
||||
if (success == null) {
|
||||
if ("+OK".equals(line)) {
|
||||
success = true;
|
||||
continue;
|
||||
|
||||
} else if (("-ERROR").equals(responseText)) {
|
||||
success = false;
|
||||
}
|
||||
continue;
|
||||
} else if ("END".equals(line)) {
|
||||
break;
|
||||
}
|
||||
line = line.replaceFirst("^\t", "");
|
||||
response.add(line);
|
||||
}
|
||||
if (success != null && success) {
|
||||
return;
|
||||
} else {
|
||||
throw new RuntimeException("Error in " + message + ": " + response);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw getRuntimeException(e);
|
||||
}
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to send a request to the server using the ExecutorService. It submits the
|
||||
* request as a Runnable task and waits for its completion. If the task is interrupted or
|
||||
* encounters an execution exception, it throws a RuntimeException with the appropriate cause.
|
||||
*
|
||||
* @param request The Runnable task representing the request to be sent to the server.
|
||||
*/
|
||||
private void sendRequest(Runnable request) {
|
||||
Future<?> future = executor.submit(request);
|
||||
try {
|
||||
future.get();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (ExecutionException e) {
|
||||
throw getRuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to extract the cause of an exception and return it as a RuntimeException. If
|
||||
* the cause is null, it returns the original exception as a RuntimeException. If the cause is
|
||||
* already a RuntimeException, it returns it directly. Otherwise, it wraps the cause in a new
|
||||
* RuntimeException and returns it.
|
||||
*
|
||||
* @param e The exception from which to extract the cause.
|
||||
* @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;
|
||||
}
|
||||
re = new RuntimeException(reason);
|
||||
return re;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the socket connection to the server and shuts down the ExecutorService. It also closes
|
||||
* the TcpTransport used for communication. If any IOException occurs during this process, it
|
||||
* prints the exception to the console.
|
||||
*/
|
||||
public void closeSocket() {
|
||||
try {
|
||||
executor.shutdown();
|
||||
clienttcptransport.close();
|
||||
socket.close();
|
||||
logger.info("Socket closed");
|
||||
} catch (IOException j) {
|
||||
logger.debug(j);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to write with the tcp transport to the server
|
||||
*
|
||||
* @param s - Message to be sent
|
||||
* @throws IOException
|
||||
*/
|
||||
private void writeToTransport(String s) throws IOException {
|
||||
int id = this.idGenerator.incrementAndGet();
|
||||
this.clienttcptransport.write(new RawPacket(id, s));
|
||||
}
|
||||
|
||||
public void ping() {
|
||||
processCommand("PING");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||
|
||||
/**
|
||||
* The CoreClient class provides basic functionalities for communicating with the server, such as
|
||||
* sending a ping command to check connectivity and logging in with a username. It uses the
|
||||
* ClientService to send commands and receive responses from the server.
|
||||
*/
|
||||
public class CoreClient {
|
||||
private final ClientService clientService;
|
||||
|
||||
/**
|
||||
* Constructs a CoreClient with the given ClientService for communication.
|
||||
*
|
||||
* @param clientservice The ClientService instance used to send commands and receive responses
|
||||
* from the server.
|
||||
*/
|
||||
public CoreClient(ClientService clientservice) {
|
||||
this.clientService = clientservice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a "PING" command to the server to check connectivity. The server should respond with a
|
||||
* "PONG" message if the connection is successful.
|
||||
*/
|
||||
public void ping() {
|
||||
clientService.processCommand("PING");
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs in to the server with the given username by sending a "LOGIN" command.
|
||||
*
|
||||
* @param user The username to log in with.
|
||||
*/
|
||||
public void login(String user) {
|
||||
clientService.processCommand("LOGIN USERNAME=" + user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* The GameClient class is responsible for communicating with the server to retrieve the current
|
||||
* game state. It sends a command to the server and parses the response into a structured GameState
|
||||
* object.
|
||||
*/
|
||||
public class GameClient {
|
||||
|
||||
private final ClientService client;
|
||||
private final int gameId;
|
||||
|
||||
/**
|
||||
* Constructs a GameClient with the given ClientService for communication.
|
||||
*
|
||||
* @param client The ClientService instance used to send commands and receive responses from the
|
||||
* server.
|
||||
*/
|
||||
public GameClient(ClientService client, int gameId) {
|
||||
this.client = client;
|
||||
this.gameId = gameId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the current game state from the server by sending a "GET_GAME_STATE"
|
||||
*
|
||||
* @return A GameState object representing the current state of the game, as parsed
|
||||
*/
|
||||
public GameState fetchGameState() {
|
||||
List<String> responseLines = client.processCommand("GET_GAME_STATE\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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a RAISE command to the server to indicate that the player wants to raise
|
||||
*
|
||||
* @param amount The amount to raise to
|
||||
*/
|
||||
public void sendRaise(int amount) {
|
||||
client.processCommand("RAISE\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 parse(String input) {
|
||||
GameState state = new GameState();
|
||||
|
||||
ParserContext ctx = new ParserContext(state);
|
||||
|
||||
for (String raw : input.split("\n")) {
|
||||
String line = raw.trim();
|
||||
|
||||
if (shouldSkip(line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handleGlobal(line, ctx)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* sending appropriate commands to the server and processing the responses.
|
||||
*/
|
||||
public class LobbyClient {
|
||||
private final ClientService client;
|
||||
|
||||
/**
|
||||
* Constructs a LobbyClient with the given ClientService for communication.
|
||||
*
|
||||
* @param client The ClientService instance used to send commands and receive responses from the
|
||||
* server.
|
||||
*/
|
||||
public LobbyClient(ClientService client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public ClientService getClientService() {
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the current status of the lobby with the given id from the server.
|
||||
*
|
||||
* @param lobbyId The id of the lobby to fetch the status for.
|
||||
* @return A string representing the current status of the lobby, as returned by the server.
|
||||
*/
|
||||
public String fetchLobbyStatusString(int lobbyId) {
|
||||
return client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId).getFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the server to create a new lobby and return the id of the newly created lobby.
|
||||
*
|
||||
* @return The id of the newly created lobby, as returned by the server.
|
||||
*/
|
||||
public int createLobby() {
|
||||
String response = client.processCommand("CREATE_LOBBY").getFirst();
|
||||
return Integer.parseInt(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the server to return the id of the lobby that the client is currently in.
|
||||
*
|
||||
* @return The id of the lobby that the client is currently in, as returned by the server.
|
||||
*/
|
||||
public int getLobbyId() {
|
||||
String response = client.processCommand("GET_LOBBY_ID").getFirst();
|
||||
return Integer.parseInt(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the server to join the lobby with the given id.
|
||||
*
|
||||
* @param lobbyId The id of the lobby to join.
|
||||
*/
|
||||
public void joinLobby(int lobbyId) {
|
||||
client.processCommand("JOIN_LOBBY ID=" + lobbyId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs in to the server with the given username by sending a "LOGIN" command.
|
||||
*
|
||||
* @param user The username to log in with.
|
||||
*/
|
||||
public void login(String user) {
|
||||
client.processCommand("LOGIN USERNAME=" + user);
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.MenuButton;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TabPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
public class ChatBoxController {
|
||||
|
||||
private String username;
|
||||
|
||||
private ChatController chatController;
|
||||
|
||||
@FXML private VBox chatBox;
|
||||
|
||||
@FXML private TabPane chatTabPane;
|
||||
|
||||
@FXML private MenuButton addWhisperChatButton;
|
||||
|
||||
private FXMLLoader fxmlLoader;
|
||||
|
||||
@FXML private List<MenuItem> whisperUsers;
|
||||
|
||||
private String ressource = "/ui-structure/components/chatui/chattab.fxml";
|
||||
|
||||
public ChatBoxController(String username, ChatController chatController) {
|
||||
this.username = username;
|
||||
this.chatController = chatController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the chat interface by creating the global chat model and adding the corresponding
|
||||
* "GLOBAL" tab to the interface. It also registers the global chat in the {@link
|
||||
* ChatController}'s model map.
|
||||
*/
|
||||
@FXML
|
||||
public void initialize() {
|
||||
ChatModel globalChatModel = new ChatModel(ChatType.GLOBAL, username, -1, null);
|
||||
chatController
|
||||
.getChatModelMap()
|
||||
.put(new ChatController.ChatKey(ChatType.GLOBAL), globalChatModel);
|
||||
addChatTab("GLOBAL", globalChatModel);
|
||||
// TODO: Button to add new Whisper Chat
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a specific user to the list of available whisper targets. Creates a new menu item for
|
||||
* the user and defines the action to open a private chat tab when selected.
|
||||
*
|
||||
* @param targetUserName The username of the person to be added to the whisper list.
|
||||
*/
|
||||
public void addWhisperUser(String targetUserName) {
|
||||
MenuItem menuItem = new MenuItem(targetUserName);
|
||||
whisperUsers.add(menuItem);
|
||||
addWhisperChatButton.getItems().add(menuItem);
|
||||
menuItem.setOnAction(event -> addWhisperChat(targetUserName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new private (whisper) chat model for a specific target user, registers it within
|
||||
* the chat system, and opens a new chat tab.
|
||||
*
|
||||
* @param target The username of the recipient for the private messages.
|
||||
*/
|
||||
public void addWhisperChat(String target) {
|
||||
ChatModel chatModel = new ChatModel(ChatType.WHISPER, username, -1, target);
|
||||
chatController
|
||||
.getChatModelMap()
|
||||
.put(new ChatController.ChatKey(ChatType.WHISPER, target), chatModel);
|
||||
addChatTab(target, chatModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically loads a new chat tab from an FXML resource and attaches it to the TabPane. It
|
||||
* initializes a {@link ChatViewController} for the tab and sets up a listener to display
|
||||
* incoming messages in real-time.
|
||||
*
|
||||
* @param title The title to be displayed on the tab header.
|
||||
* @param chatModel The {@link ChatModel} containing the data and logic for this specific chat.
|
||||
* @throws RuntimeException If the FXML resource for the chat tab cannot be loaded.
|
||||
*/
|
||||
public void addChatTab(String title, ChatModel chatModel) {
|
||||
URL resource = getClass().getResource(ressource);
|
||||
FXMLLoader fxmlLoader = new FXMLLoader(resource);
|
||||
try {
|
||||
ChatViewController chatViewController =
|
||||
new ChatViewController(this.chatController, chatModel, this.username);
|
||||
fxmlLoader.setController(chatViewController);
|
||||
Node load = fxmlLoader.load();
|
||||
VBox.setVgrow(load, Priority.ALWAYS);
|
||||
VBox vbox = new VBox();
|
||||
VBox.setVgrow(vbox, Priority.ALWAYS);
|
||||
vbox.getChildren().add(load);
|
||||
chatModel.addListener((msg) -> chatViewController.showMessage(msg));
|
||||
Tab newChat = new Tab(title, vbox);
|
||||
this.chatTabPane.getTabs().add(newChat);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui;
|
||||
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/**
|
||||
* Controller-Klasse für das Chat-System innerhalb der Spieloberfläche.
|
||||
*
|
||||
* <p>Verwaltet das Anzeigen von Chatnachrichten, das Eingabefeld für eigene Nachrichten sowie den
|
||||
* Senden-Button. Unterstützt drei Arten von Nachrichten: - Player-to-Player (Privat) - Lobby-Chat
|
||||
* (Raum) - Globaler Chat (Serverweit)
|
||||
*
|
||||
* <p>Nachrichten werden in einem {@link VBox}-Container als {@link Label} angezeigt. Eigene
|
||||
* Nachrichten werden über {@link #onSendToNetwork(String)} an das Netzwerkprotokoll weitergeleitet,
|
||||
* während eingehende Nachrichten über {@link #receiveMessage(String, String)} verarbeitet und
|
||||
* angezeigt werden.
|
||||
*
|
||||
* <p>Hinweis: Einige TODOs stehen in der zugehörigen FXML-Datei
|
||||
*/
|
||||
public class ChatController {
|
||||
|
||||
@FXML private VBox chatVBox;
|
||||
|
||||
@FXML private TextField inputField;
|
||||
|
||||
@FXML private Button sendButton;
|
||||
|
||||
@FXML private ScrollPane chatScrollPane;
|
||||
|
||||
private static final int CHAT_PADDING = 20;
|
||||
|
||||
/** Initialisiert den ChatController nach dem Laden der FXML. */
|
||||
public void initialize() {
|
||||
inputField.setOnAction(event -> sendMessage());
|
||||
chatScrollPane.vvalueProperty().bind(chatVBox.heightProperty());
|
||||
}
|
||||
|
||||
/** Standardkonstruktor. Wird von FXML verwendet. */
|
||||
public ChatController() {
|
||||
// default constructor for FXML
|
||||
}
|
||||
|
||||
/**
|
||||
* Diese Methode wird vom Senden-Button oder Enter ausgelöst. Sie gibt die eigene Nachricht an
|
||||
* das Netzwerkprotokoll weiter.
|
||||
*/
|
||||
@FXML
|
||||
private void sendMessage() {
|
||||
String message = inputField.getText().trim();
|
||||
if (!message.isEmpty()) {
|
||||
inputField.clear();
|
||||
|
||||
// Hier wird die eigene Nachricht ans Netzwerkprotokoll übergeben
|
||||
onSendToNetwork(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Diese Funktion muss vom Netzwerkprotokoll aufgerufen werden, wenn eine neue Nachricht von
|
||||
* einem anderen Spieler kommt.
|
||||
*
|
||||
* @param player Name des Spielers
|
||||
* @param message Nachricht des Spielers
|
||||
*/
|
||||
public void receiveMessage(String player, String message) {
|
||||
String time = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm"));
|
||||
Label label = new Label("[" + time + "] " + player + ": " + message);
|
||||
label.getStyleClass().add("info-text");
|
||||
label.setWrapText(true); // Zeilenumbruch aktivieren
|
||||
label.maxWidthProperty().bind(chatVBox.widthProperty().subtract(CHAT_PADDING));
|
||||
chatVBox.getChildren().add(label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schnittstelle zum Netzwerkprotokoll. Diese Funktion wird automatisch aufgerufen, wenn der
|
||||
* Benutzer eine eigene Nachricht sendet.
|
||||
*
|
||||
* @param message Nachricht, die der Benutzer abgeschickt hat
|
||||
*/
|
||||
public void onSendToNetwork(String message) {
|
||||
// TODO: Netzwerkcode einfügen
|
||||
receiveMessage("Du", message);
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/** Responsible for the presentation of the ChatModel to the Client */
|
||||
public class ChatViewController implements Initializable {
|
||||
|
||||
@FXML private Button sendButton;
|
||||
|
||||
@FXML private TextField inputField;
|
||||
|
||||
@FXML private VBox chatInterfaceVBox;
|
||||
|
||||
@FXML private HBox controlBar;
|
||||
|
||||
@FXML private ScrollPane scrollPane;
|
||||
|
||||
@FXML private VBox chat;
|
||||
|
||||
private final ChatModel chatModel;
|
||||
|
||||
private final String username;
|
||||
|
||||
private final ChatController controller;
|
||||
|
||||
private static final int CHAT_PADDING = 20;
|
||||
|
||||
public ChatViewController(ChatController chatController, ChatModel chatModel, String username) {
|
||||
this.controller = chatController;
|
||||
this.username = username;
|
||||
this.chatModel = chatModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller after the FXML root element has been processed. Sets up event
|
||||
* handlers for sending messages via the input field or button and ensures the ScrollPane
|
||||
* automatically scrolls to the bottom when new messages are added.
|
||||
*
|
||||
* @param location The location used to resolve relative paths for the root object.
|
||||
* @param resourceBundle The resources used to localize the root object.
|
||||
*/
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resourceBundle) {
|
||||
inputField.setOnAction(event -> sendMessage());
|
||||
sendButton.setOnAction(event -> sendMessage());
|
||||
scrollPane.vvalueProperty().bind(chat.heightProperty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the text from the input field, creates a new {@link Message} object using the
|
||||
* current model state, and passes it to the {@link ChatController} for network transmission.
|
||||
* The input field is cleared after sending.
|
||||
*/
|
||||
public void sendMessage() {
|
||||
String message = inputField.getText().trim();
|
||||
if (!message.isEmpty()) {
|
||||
inputField.clear();
|
||||
Message msg =
|
||||
new Message(
|
||||
chatModel.getChattype(),
|
||||
chatModel.lobbyId,
|
||||
username,
|
||||
chatModel.getTarget(),
|
||||
message);
|
||||
controller.onSendToNetwork(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a message in the chat interface. This method creates a new Label for the message
|
||||
* text and adds it to the message container. It uses {@link Platform#runLater(Runnable)} to
|
||||
* ensure the UI update happens on the JavaFX Application Thread.
|
||||
*
|
||||
* @param msg The {@link Message} object containing the content and metadata to display.
|
||||
*/
|
||||
public void showMessage(Message msg) {
|
||||
Platform.runLater(
|
||||
() -> {
|
||||
String msgText =
|
||||
String.format(
|
||||
"[%s] %s: %s", msg.timestamp, msg.sender, msg.getMessage());
|
||||
Label label = new Label(msgText);
|
||||
label.getStyleClass().add("info-text");
|
||||
label.setWrapText(true);
|
||||
label.maxWidthProperty().bind(chat.widthProperty().subtract(CHAT_PADDING));
|
||||
chat.getChildren().add(label);
|
||||
});
|
||||
}
|
||||
}
|
||||
+851
-10
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import java.io.IOException;
|
||||
import javafx.application.Application;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
@@ -17,13 +18,19 @@ import javafx.stage.Stage;
|
||||
*/
|
||||
public class CasinoGameUI extends Application {
|
||||
|
||||
private static ClientService clientService;
|
||||
|
||||
private static final int DEFAULT_WIDTH = 1200;
|
||||
private static final int DEFAULT_HEIGHT = 800;
|
||||
|
||||
/** 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.
|
||||
@@ -36,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));
|
||||
|
||||
+110
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+311
-16
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,17 @@ public class Casinomainui extends Application {
|
||||
* @throws IOException If loading the FXML fails.
|
||||
*/
|
||||
public void start(Stage stage) throws IOException {
|
||||
// If the launcher passed an address argument (ip:port), expose it as
|
||||
// system properties so controllers can read it without embedding defaults.
|
||||
var raw = getParameters().getRaw();
|
||||
if (raw != null && raw.size() > 0) {
|
||||
String arg = raw.get(0);
|
||||
String[] parts = arg.split(":", 2);
|
||||
if (parts.length == 2) {
|
||||
System.setProperty("casono.server.host", parts[0]);
|
||||
System.setProperty("casono.server.port", parts[1]);
|
||||
}
|
||||
}
|
||||
FXMLLoader fxmlLoader =
|
||||
new FXMLLoader(getClass().getResource("/ui-structure/Casinomainui.fxml"));
|
||||
Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT);
|
||||
|
||||
+74
-3
@@ -1,9 +1,14 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
@@ -23,10 +28,13 @@ public class CasinomainuiController {
|
||||
@FXML private Rectangle greenBox;
|
||||
@FXML private Button exitbutton;
|
||||
@FXML private VBox casinoTable;
|
||||
@FXML private TextField usernameField;
|
||||
@FXML private Button loginButton;
|
||||
|
||||
private LobbyButtonTranslationManager translationManager;
|
||||
private LobbyButtonGridManager gridManager;
|
||||
private int nextButtonId = 1;
|
||||
private LobbyClient lobbyClient;
|
||||
|
||||
/** Default constructor for dependency injection by FXMLLoader. */
|
||||
public CasinomainuiController() {
|
||||
@@ -41,8 +49,63 @@ public class CasinomainuiController {
|
||||
logoView.setImage(new Image(getClass().getResource("/images/logo.png").toExternalForm()));
|
||||
|
||||
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
|
||||
}
|
||||
gridManager =
|
||||
new LobbyButtonGridManager(new javafx.scene.layout.GridPane(), translationManager);
|
||||
new LobbyButtonGridManager(
|
||||
new javafx.scene.layout.GridPane(), translationManager, clientService);
|
||||
// LobbyClient will use the provided ClientService; in offline mode calls will
|
||||
// fail with RuntimeException
|
||||
lobbyClient = new LobbyClient(clientService);
|
||||
casinoTable.getChildren().clear();
|
||||
casinoTable.getChildren().add(gridManager.getGridPane());
|
||||
gridManager.renderLobbyButtons();
|
||||
}
|
||||
|
||||
/** Handles the login button action. Validates input and calls LobbyClient.login(). */
|
||||
@FXML
|
||||
public void handleLoginButton() {
|
||||
String username = usernameField.getText();
|
||||
if (username == null || username.isBlank()) {
|
||||
showAlert("Please enter a username.");
|
||||
return;
|
||||
}
|
||||
// Only allow alphanumeric, _ and -
|
||||
if (!username.matches("[a-zA-Z0-9_-]+")) {
|
||||
showAlert("Only letters, numbers, '_' and '-' are allowed!");
|
||||
return;
|
||||
}
|
||||
if (lobbyClient.getClientService().isOffline()) {
|
||||
showAlert("Offline mode: cannot send login to server.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
lobbyClient.login(username);
|
||||
showAlert("Login sent: " + username);
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.error("Login failed: {}", e.getMessage());
|
||||
showAlert("Login failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows an alert dialog with the given message. */
|
||||
private void showAlert(String message) {
|
||||
Alert alert = new Alert(AlertType.INFORMATION);
|
||||
alert.setTitle("Info");
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
casinoTable.getChildren().clear();
|
||||
casinoTable.getChildren().add(gridManager.getGridPane());
|
||||
gridManager.renderLobbyButtons();
|
||||
@@ -62,13 +125,21 @@ public class CasinomainuiController {
|
||||
return;
|
||||
}
|
||||
int buttonId = nextButtonId++;
|
||||
int lobbyId = gridManager.createLobby();
|
||||
try {
|
||||
String username = usernameField != null ? usernameField.getText() : "<unknown>";
|
||||
LOGGER.info("Creating lobby for user: {}", username);
|
||||
// avoid attempting to create a lobby when offline
|
||||
if (lobbyClient.getClientService().isOffline()) {
|
||||
LOGGER.warn("Cannot create lobby while offline");
|
||||
showAlert("Offline mode: cannot create lobby.");
|
||||
return;
|
||||
}
|
||||
int lobbyId = gridManager.createLobby();
|
||||
translationManager.addLobbyButton(buttonId, lobbyId);
|
||||
LOGGER.info("ButtonID: {}, LobbyID: {}", buttonId, lobbyId);
|
||||
gridManager.renderLobbyButtons();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Error while adding lobby button: {}", e.getMessage());
|
||||
LOGGER.error("Failed to create or add lobby: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+323
-99
@@ -1,10 +1,18 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||
|
||||
/**
|
||||
* Manages the grid for lobby buttons and rendering. Uses LobbyButtonTranslationManager for mapping
|
||||
* ButtonID to LobbyID.
|
||||
*/
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
@@ -12,133 +20,349 @@ import javafx.scene.layout.GridPane;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Manages the grid for lobby buttons and rendering. Uses LobbyButtonTranslationManager for mapping
|
||||
* ButtonID to LobbyID.
|
||||
*/
|
||||
public class LobbyButtonGridManager {
|
||||
private static final double BUTTON_WIDTH_MARGIN = 20.0;
|
||||
|
||||
private static final double BTN_WIDTH_MRG = 20.0;
|
||||
private static final double BUTTON_MIN_SIZE = 10.0;
|
||||
private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
|
||||
|
||||
/** GridPane for the button grid. */
|
||||
private final GridPane gridPane;
|
||||
|
||||
/** Manager for mapping ButtonID to LobbyID. */
|
||||
private final LobbyButtonTranslationManager translationManager;
|
||||
|
||||
/** Number of rows in the grid. */
|
||||
private static final int ROWS = 2;
|
||||
|
||||
/** Number of columns in the grid. */
|
||||
private static final int REFRESH_INTERVAL_SECONDS = 5;
|
||||
private static final int INITIAL_DELAY_SECONDS = 5;
|
||||
private static final int COLS = 4;
|
||||
|
||||
/** Path to the button image. */
|
||||
private static final String BUTTON_IMAGE_PATH = "/images/logo.png";
|
||||
private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
|
||||
|
||||
/** Max random lobby id. */
|
||||
private static final int MAX_RANDOM_LOBBY_ID = 10000;
|
||||
private static final String BUTTON_FALLBACK_IMAGE = "/images/lobbypictures/error.png";
|
||||
|
||||
private static final String BUTTON_IMAGE_TEMPLATE = "/images/lobbypictures/lobby_%d_%s.png";
|
||||
|
||||
private final GridPane gridPane;
|
||||
private final LobbyButtonTranslationManager translationManager;
|
||||
private final LobbyClient lobbyClient;
|
||||
|
||||
private final ConcurrentHashMap<String, Image> imageCache = new ConcurrentHashMap<>();
|
||||
|
||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
|
||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
/**
|
||||
* Constructor for the GridManager.
|
||||
*
|
||||
* @param gridPane the GridPane for rendering
|
||||
* @param translationManager the manager for mapping ButtonID to LobbyID
|
||||
*/
|
||||
public LobbyButtonGridManager(
|
||||
GridPane gridPane, LobbyButtonTranslationManager translationManager) {
|
||||
GridPane gridPane,
|
||||
LobbyButtonTranslationManager translationManager,
|
||||
LobbyClient lobbyClient) {
|
||||
|
||||
this.gridPane = gridPane;
|
||||
// Singleton immer verwenden
|
||||
this.translationManager = LobbyButtonTranslationManager.getInstance();
|
||||
this.lobbyClient = lobbyClient;
|
||||
|
||||
startPeriodicRefresh(REFRESH_INTERVAL_SECONDS, INITIAL_DELAY_SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders all lobby buttons in the grid. Creates a button for each mapping with image and event
|
||||
* handler.
|
||||
*/
|
||||
public void renderLobbyButtons() {
|
||||
gridPane.getChildren().clear();
|
||||
int index = 0;
|
||||
public LobbyButtonGridManager(
|
||||
GridPane gridPane,
|
||||
LobbyButtonTranslationManager translationManager,
|
||||
ClientService clientService) {
|
||||
|
||||
this(gridPane, translationManager, new LobbyClient(clientService));
|
||||
}
|
||||
|
||||
private void startPeriodicRefresh(long initialDelay, long period) {
|
||||
scheduler.scheduleAtFixedRate(
|
||||
this::refreshMappings, initialDelay, period, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void refreshMappings() {
|
||||
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||
|
||||
if (mapping.isEmpty()) {
|
||||
// No buttons to render
|
||||
return;
|
||||
}
|
||||
for (Map.Entry<Integer, Integer> entry : mapping.entrySet()) {
|
||||
int buttonId = entry.getKey();
|
||||
Button btn = new Button();
|
||||
btn.setId("lobbyBtn-" + buttonId);
|
||||
ImageView imageView =
|
||||
new ImageView(new Image(getClass().getResourceAsStream(BUTTON_IMAGE_PATH)));
|
||||
imageView.setPreserveRatio(true);
|
||||
// Dynamische Breite: Bindung an die Zellengröße
|
||||
imageView
|
||||
.fitWidthProperty()
|
||||
.bind(gridPane.widthProperty().divide(COLS).subtract(BUTTON_WIDTH_MARGIN));
|
||||
imageView.setSmooth(true);
|
||||
btn.setGraphic(imageView);
|
||||
btn.setMaxWidth(Double.MAX_VALUE);
|
||||
btn.setMaxHeight(Double.MAX_VALUE);
|
||||
btn.setMinWidth(BUTTON_MIN_SIZE);
|
||||
btn.setMinHeight(BUTTON_MIN_SIZE);
|
||||
GridPane.setHgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
||||
GridPane.setVgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
||||
btn.setOnAction(
|
||||
e -> {
|
||||
Integer lobbyId = translationManager.getLobbyIdForButton(buttonId);
|
||||
if (lobbyId != null) {
|
||||
joinLobby(lobbyId);
|
||||
}
|
||||
});
|
||||
int row = index / COLS;
|
||||
int col = index % COLS;
|
||||
gridPane.add(btn, col, row);
|
||||
index++;
|
||||
|
||||
List<Map.Entry<Integer, Integer>> entries = new ArrayList<>(mapping.entrySet());
|
||||
|
||||
for (Map.Entry<Integer, Integer> e : entries) {
|
||||
int buttonId = e.getKey();
|
||||
int lobbyId = e.getValue();
|
||||
|
||||
CompletableFuture.supplyAsync(
|
||||
() -> {
|
||||
try {
|
||||
return lobbyClient.fetchLobbyStatusString(lobbyId);
|
||||
} catch (Exception ex) {
|
||||
LOGGER.info("Lobby {} missing: {}", lobbyId, ex.getMessage());
|
||||
return null;
|
||||
}
|
||||
},
|
||||
executor)
|
||||
.thenAccept(
|
||||
status -> {
|
||||
if (status == null) {
|
||||
translationManager.removeLobbyButton(buttonId);
|
||||
|
||||
javafx.application.Platform.runLater(
|
||||
this::updateLobbyButtonImages);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder for lobby creation logic. Returns a generated lobbyId.
|
||||
*
|
||||
* @return The generated lobbyId
|
||||
*/
|
||||
public int createLobby() {
|
||||
// TODO: Replace with actual lobby creation logic
|
||||
int lobbyId = (int) (Math.random() * MAX_RANDOM_LOBBY_ID + 1);
|
||||
LOGGER.info("Lobby created: {}", lobbyId);
|
||||
return lobbyId;
|
||||
public void renderLobbyButtons() {
|
||||
gridPane.getChildren().clear();
|
||||
|
||||
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||
|
||||
if (mapping.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Integer> buttonIds = new ArrayList<>(mapping.keySet());
|
||||
Collections.sort(buttonIds);
|
||||
|
||||
for (int index = 0; index < buttonIds.size(); index++) {
|
||||
Integer buttonId = buttonIds.get(index);
|
||||
int lobbyId = mapping.get(buttonId);
|
||||
|
||||
Button btn = createLobbyButton(buttonId, lobbyId);
|
||||
|
||||
int row = index / COLS;
|
||||
int col = index % COLS;
|
||||
|
||||
gridPane.add(btn, col, row);
|
||||
}
|
||||
}
|
||||
|
||||
private Button createLobbyButton(int buttonId, int lobbyId) {
|
||||
Button btn = new Button();
|
||||
btn.setId("lobbyBtn-" + buttonId);
|
||||
|
||||
ImageView imageView = new ImageView(safeLoadImage(BUTTON_FALLBACK_IMAGE));
|
||||
|
||||
imageView.setPreserveRatio(true);
|
||||
imageView
|
||||
.fitWidthProperty()
|
||||
.bind(gridPane.widthProperty().divide(COLS).subtract(BTN_WIDTH_MRG));
|
||||
|
||||
btn.setGraphic(imageView);
|
||||
btn.setMaxWidth(Double.MAX_VALUE);
|
||||
btn.setMaxHeight(Double.MAX_VALUE);
|
||||
btn.setMinWidth(BUTTON_MIN_SIZE);
|
||||
btn.setMinHeight(BUTTON_MIN_SIZE);
|
||||
|
||||
GridPane.setHgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
||||
GridPane.setVgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
||||
|
||||
btn.setOnAction(
|
||||
e -> {
|
||||
Integer targetLobbyId = translationManager.getLobbyIdForButton(buttonId);
|
||||
|
||||
if (targetLobbyId != null) {
|
||||
joinLobby(targetLobbyId);
|
||||
}
|
||||
});
|
||||
|
||||
loadLobbyImageAsync(btn, buttonId, lobbyId);
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
private void loadLobbyImageAsync(Button btn, int buttonId, int lobbyId) {
|
||||
|
||||
CompletableFuture.supplyAsync(() -> lobbyClient.fetchLobbyStatusString(lobbyId), executor)
|
||||
.thenAccept(
|
||||
statusStr -> {
|
||||
LobbyStatus status = parseLobbyStatus(statusStr);
|
||||
|
||||
String path =
|
||||
getImagePathForButton(
|
||||
buttonId,
|
||||
status == null ? LobbyStatus.CREATED : status);
|
||||
|
||||
Image img = safeLoadImage(path);
|
||||
|
||||
javafx.application.Platform.runLater(
|
||||
() -> {
|
||||
ImageView iv = new ImageView(img);
|
||||
iv.setPreserveRatio(true);
|
||||
iv.fitWidthProperty()
|
||||
.bind(
|
||||
gridPane.widthProperty()
|
||||
.divide(COLS)
|
||||
.subtract(BTN_WIDTH_MRG));
|
||||
btn.setGraphic(iv);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private enum LobbyStatus {
|
||||
CREATED,
|
||||
RUNNING
|
||||
}
|
||||
|
||||
private LobbyStatus parseLobbyStatus(String statusStr) {
|
||||
if (statusStr == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return LobbyStatus.valueOf(statusStr.trim().toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String getImagePathForButton(int buttonId, LobbyStatus status) {
|
||||
|
||||
String statusStr = status == LobbyStatus.CREATED ? "created" : "running";
|
||||
|
||||
return String.format(BUTTON_IMAGE_TEMPLATE, buttonId, statusStr);
|
||||
}
|
||||
|
||||
private Image safeLoadImage(String path) {
|
||||
Image cached = imageCache.get(path);
|
||||
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
java.io.InputStream is = getClass().getResourceAsStream(path);
|
||||
|
||||
if (is == null) {
|
||||
is = getClass().getResourceAsStream(BUTTON_FALLBACK_IMAGE);
|
||||
}
|
||||
|
||||
Image loaded = null;
|
||||
|
||||
try {
|
||||
if (is != null) {
|
||||
loaded = new Image(is);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Image load failed: {}", path, e);
|
||||
}
|
||||
|
||||
if (loaded != null) {
|
||||
imageCache.put(path, loaded);
|
||||
}
|
||||
|
||||
return loaded;
|
||||
}
|
||||
|
||||
public void updateLobbyButtonImages() {
|
||||
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||
|
||||
if (mapping.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Integer buttonId : mapping.keySet()) {
|
||||
int lobbyId = mapping.get(buttonId);
|
||||
|
||||
CompletableFuture.supplyAsync(
|
||||
() -> {
|
||||
String statusStr = lobbyClient.fetchLobbyStatusString(lobbyId);
|
||||
|
||||
LobbyStatus status = parseLobbyStatus(statusStr);
|
||||
|
||||
return status == null ? LobbyStatus.CREATED : status;
|
||||
},
|
||||
executor)
|
||||
.thenAccept(
|
||||
status -> {
|
||||
String path = getImagePathForButton(buttonId, status);
|
||||
|
||||
javafx.application.Platform.runLater(
|
||||
() -> {
|
||||
for (Node node : gridPane.getChildren()) {
|
||||
|
||||
boolean isButton = node instanceof Button;
|
||||
boolean idMatches =
|
||||
("lobbyBtn-" + buttonId)
|
||||
.equals(node.getId());
|
||||
|
||||
if (isButton && idMatches) {
|
||||
Button btn = (Button) node;
|
||||
|
||||
ImageView iv =
|
||||
new ImageView(safeLoadImage(path));
|
||||
|
||||
iv.setPreserveRatio(true);
|
||||
iv.fitWidthProperty()
|
||||
.bind(
|
||||
gridPane.widthProperty()
|
||||
.divide(COLS)
|
||||
.subtract(
|
||||
BTN_WIDTH_MRG));
|
||||
|
||||
btn.setGraphic(iv);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public int createLobby() {
|
||||
try {
|
||||
int lobbyId = lobbyClient.createLobby();
|
||||
|
||||
if (lobbyId <= 0) {
|
||||
throw new RuntimeException("Invalid lobby id: " + lobbyId);
|
||||
}
|
||||
|
||||
return lobbyId;
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Create lobby failed: {}", e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder for joining a lobby.
|
||||
*
|
||||
* @param lobbyId The lobbyId to join
|
||||
*/
|
||||
public void joinLobby(int lobbyId) {
|
||||
// Game-UI starten und Lobby-UI schließen
|
||||
LOGGER.info("Joining lobby: {}", lobbyId);
|
||||
try {
|
||||
lobbyClient.joinLobby(lobbyId);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Join failed: {}", e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
javafx.application.Platform.runLater(
|
||||
() -> {
|
||||
// Lobby-Stage schließen
|
||||
javafx.stage.Stage currentStage =
|
||||
(javafx.stage.Stage) gridPane.getScene().getWindow();
|
||||
currentStage.close();
|
||||
// Game-UI starten
|
||||
|
||||
currentStage.hide();
|
||||
|
||||
javafx.stage.Stage gameStage = new javafx.stage.Stage();
|
||||
|
||||
gameStage.setOnHidden(
|
||||
ev -> {
|
||||
currentStage.show();
|
||||
refreshMappings();
|
||||
updateLobbyButtonImages();
|
||||
});
|
||||
|
||||
try {
|
||||
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI
|
||||
.setClientService(lobbyClient.getClientService());
|
||||
|
||||
new ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI()
|
||||
.start(new javafx.stage.Stage());
|
||||
.start(gameStage);
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Fehler beim Starten der Game-UI: {}", e.getMessage());
|
||||
LOGGER.error("Game UI failed: {}", e.getMessage());
|
||||
currentStage.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the GridPane.
|
||||
*
|
||||
* @return The GridPane for the button grid
|
||||
*/
|
||||
public javafx.scene.layout.GridPane getGridPane() {
|
||||
public GridPane getGridPane() {
|
||||
return gridPane;
|
||||
}
|
||||
|
||||
public LobbyClient getLobbyClient() {
|
||||
return lobbyClient;
|
||||
}
|
||||
|
||||
public void refreshNow() {
|
||||
refreshMappings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@ package ch.unibas.dmi.dbis.cs108.casono.server;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersRequest;
|
||||
@@ -15,6 +21,10 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.logout.LogoutRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingParser;
|
||||
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;
|
||||
@@ -76,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();
|
||||
@@ -93,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));
|
||||
|
||||
@@ -110,8 +122,135 @@ public class ServerApp {
|
||||
commandRouter.register(
|
||||
LogoutRequest.class, new LogoutHandler(responseDispatcher, userRegistry));
|
||||
|
||||
parserDispatcher.register("SEND_MESSAGE", new SendMessageParser());
|
||||
commandRouter.register(
|
||||
SendMessageRequest.class, new SendMessageHandler(responseDispatcher, userRegistry));
|
||||
|
||||
parserDispatcher.register("GET_MESSAGE_COUNT", new GetMessageCountParser());
|
||||
commandRouter.register(
|
||||
GetMessageCountRequest.class,
|
||||
new GetMessageCountHandler(responseDispatcher, userRegistry));
|
||||
|
||||
parserDispatcher.register("GET_NEXT_MESSAGE", new GetNextMessageParser());
|
||||
commandRouter.register(
|
||||
GetNextMessageRequest.class,
|
||||
new GetNextMessageHandler(responseDispatcher, userRegistry));
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
+33
@@ -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()));
|
||||
}
|
||||
}
|
||||
+20
@@ -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);
|
||||
}
|
||||
}
|
||||
+24
@@ -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;
|
||||
}
|
||||
}
|
||||
+108
@@ -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()));
|
||||
}
|
||||
}
|
||||
+40
@@ -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);
|
||||
}
|
||||
}
|
||||
+45
@@ -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;
|
||||
}
|
||||
}
|
||||
+96
@@ -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()));
|
||||
}
|
||||
}
|
||||
+28
@@ -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);
|
||||
}
|
||||
}
|
||||
+34
@@ -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;
|
||||
}
|
||||
}
|
||||
+96
@@ -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()));
|
||||
}
|
||||
}
|
||||
+28
@@ -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);
|
||||
}
|
||||
}
|
||||
+34
@@ -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;
|
||||
}
|
||||
}
|
||||
+74
@@ -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));
|
||||
}
|
||||
}
|
||||
+16
@@ -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);
|
||||
}
|
||||
}
|
||||
+31
@@ -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;
|
||||
}
|
||||
}
|
||||
+114
@@ -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";
|
||||
};
|
||||
}
|
||||
}
|
||||
+110
@@ -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()));
|
||||
}
|
||||
}
|
||||
+34
@@ -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);
|
||||
}
|
||||
}
|
||||
+45
@@ -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;
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
|
||||
|
||||
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;
|
||||
import java.util.Optional;
|
||||
|
||||
public class GetMessageCountHandler extends CommandHandler<GetMessageCountRequest> {
|
||||
private final UserRegistry userRegistry;
|
||||
|
||||
/**
|
||||
* Constructs a new GetMessageCountHandler with the necessary response dispatcher and user
|
||||
* registry.
|
||||
*
|
||||
* @param responseDispatcher The dispatcher used to send the count or error back to the client.
|
||||
* @param userRegistry The registry used to identify the user and access their message queue.
|
||||
*/
|
||||
public GetMessageCountHandler(
|
||||
ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
|
||||
super(responseDispatcher);
|
||||
this.userRegistry = userRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a request to retrieve the number of pending messages for a user. It looks up the
|
||||
* user by their session ID; if found, it dispatches a {@link GetMessageCountResponse}
|
||||
* containing the current count. Otherwise, it dispatches an {@link ErrorResponse}.
|
||||
*
|
||||
* @param request The {@link GetMessageCountRequest} containing the session details.
|
||||
*/
|
||||
@Override
|
||||
public void execute(GetMessageCountRequest request) {
|
||||
Optional<User> user = userRegistry.getBySessionId(request.getSessionId());
|
||||
if (user.isPresent()) {
|
||||
int count = user.get().getMessageCount();
|
||||
GetMessageCountResponse response =
|
||||
new GetMessageCountResponse(request.getContext(), count);
|
||||
responseDispatcher.dispatch(response);
|
||||
} else {
|
||||
ErrorResponse response =
|
||||
new ErrorResponse(
|
||||
request.getContext(), "", "user could not be identified by SessionId");
|
||||
|
||||
responseDispatcher.dispatch(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
|
||||
|
||||
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 GetMessageCountParser implements CommandParser<GetMessageCountRequest> {
|
||||
/**
|
||||
* Parses a raw {@link PrimitiveRequest} into a {@link GetMessageCountRequest}. This method
|
||||
* wraps the request context from the network layer into a structured message count request
|
||||
* object.
|
||||
*
|
||||
* @param primitiveRequest The raw request containing parameters and context from the network.
|
||||
* @return A new {@link GetMessageCountRequest} instance.
|
||||
*/
|
||||
@Override
|
||||
public GetMessageCountRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
RequestParameterAccessor accessor =
|
||||
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||
return new GetMessageCountRequest(primitiveRequest.context());
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
|
||||
|
||||
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 GetMessageCountRequest extends Request {
|
||||
/**
|
||||
* Constructs a new GetMessageCountRequest with the specified request context. This request is
|
||||
* used by a client to query the number of pending messages currently waiting in their
|
||||
* server-side queue.
|
||||
*
|
||||
* @param context The {@link RequestContext} associated with this request.
|
||||
*/
|
||||
public GetMessageCountRequest(RequestContext context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
|
||||
|
||||
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.ResponseBodyBuilder;
|
||||
|
||||
public class GetMessageCountResponse extends SuccessResponse {
|
||||
/**
|
||||
* Constructs a new GetMessageCountResponse. It creates a response body containing the "COUNT"
|
||||
* parameter and associates it with the original request context.
|
||||
*
|
||||
* @param context The {@link RequestContext} of the original request.
|
||||
* @param count The number of pending messages to be returned to the client.
|
||||
*/
|
||||
public GetMessageCountResponse(RequestContext context, int count) {
|
||||
super(context, new ResponseBodyBuilder().param("COUNT", count).build());
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||
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;
|
||||
import java.util.Optional;
|
||||
|
||||
public class GetNextMessageHandler extends CommandHandler<GetNextMessageRequest> {
|
||||
private final UserRegistry userRegistry;
|
||||
|
||||
/**
|
||||
* Constructs a new GetNextMessageHandler with the required dispatcher and user registry.
|
||||
*
|
||||
* @param responseDispatcher The dispatcher used to send responses back to clients.
|
||||
* @param userRegistry The registry used to look up users by their session information.
|
||||
*/
|
||||
public GetNextMessageHandler(ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
|
||||
super(responseDispatcher);
|
||||
this.userRegistry = userRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the request to retrieve the next message for a specific user. It identifies the user
|
||||
* via their session ID, dequeues the next available message, and dispatches a {@link
|
||||
* GetNextMessageResponse}. If the user cannot be identified, an {@link ErrorResponse} is sent
|
||||
* instead.
|
||||
*
|
||||
* @param request The {@link GetNextMessageRequest} containing the session and context.
|
||||
*/
|
||||
@Override
|
||||
public void execute(GetNextMessageRequest request) {
|
||||
Optional<User> user = userRegistry.getBySessionId(request.getSessionId());
|
||||
if (user.isPresent()) {
|
||||
Message msg = user.get().dequeMessage();
|
||||
GetNextMessageResponse response = new GetNextMessageResponse(request.getContext(), msg);
|
||||
responseDispatcher.dispatch(response);
|
||||
} else {
|
||||
ErrorResponse response =
|
||||
new ErrorResponse(
|
||||
request.getContext(),
|
||||
"NO_USER_ASSOCIATED",
|
||||
"user could not be identified by SessionId");
|
||||
responseDispatcher.dispatch(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
|
||||
|
||||
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 GetNextMessageParser implements CommandParser<GetNextMessageRequest> {
|
||||
/**
|
||||
* Parses a raw {@link PrimitiveRequest} into a {@link GetNextMessageRequest}. This method
|
||||
* initializes a parameter accessor (though not currently used for extraction) and returns a
|
||||
* structured request object containing the original request context.
|
||||
*
|
||||
* @param primitiveRequest The raw request containing parameters and context from the network.
|
||||
* @return A new {@link GetNextMessageRequest} instance.
|
||||
*/
|
||||
@Override
|
||||
public GetNextMessageRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
RequestParameterAccessor accessor =
|
||||
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||
return new GetNextMessageRequest(primitiveRequest.context());
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
|
||||
|
||||
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 GetNextMessageRequest extends Request {
|
||||
/**
|
||||
* Constructs a new GetNextMessageRequest with the specified request context. This request is
|
||||
* typically used by a client to poll or retrieve the next available message from the server's
|
||||
* queue.
|
||||
*
|
||||
* @param context The {@link RequestContext} associated with this request.
|
||||
*/
|
||||
public GetNextMessageRequest(RequestContext context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||
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;
|
||||
|
||||
public class GetNextMessageResponse extends SuccessResponse {
|
||||
/**
|
||||
* Constructs a new GetNextMessageResponse. It converts the provided {@link Message} into a
|
||||
* network-compatible response body and associates it with the original request context.
|
||||
*
|
||||
* @param context The {@link RequestContext} of the request being answered.
|
||||
* @param msg The {@link Message} to be sent back to the client.
|
||||
*/
|
||||
public GetNextMessageResponse(RequestContext context, Message msg) {
|
||||
super(context, msg.toResponse(ResponseBody.builder()));
|
||||
}
|
||||
}
|
||||
+28
@@ -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));
|
||||
}
|
||||
}
|
||||
+17
@@ -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());
|
||||
}
|
||||
}
|
||||
+15
@@ -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);
|
||||
}
|
||||
}
|
||||
+37
@@ -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());
|
||||
}
|
||||
}
|
||||
+81
@@ -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));
|
||||
}
|
||||
}
|
||||
+36
@@ -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);
|
||||
}
|
||||
}
|
||||
+32
@@ -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;
|
||||
}
|
||||
}
|
||||
+45
@@ -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());
|
||||
}
|
||||
}
|
||||
+83
@@ -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()));
|
||||
}
|
||||
}
|
||||
+33
@@ -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);
|
||||
}
|
||||
}
|
||||
+34
@@ -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;
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
|
||||
public class SendMessageHandler extends CommandHandler<SendMessageRequest> {
|
||||
private final UserRegistry userRegistry;
|
||||
|
||||
/**
|
||||
* Constructs a new SendMessageHandler with the required dispatcher and user registry.
|
||||
*
|
||||
* @param responseDispatcher The dispatcher used to send responses back to clients.
|
||||
* @param userRegistry The registry containing all currently connected users.
|
||||
*/
|
||||
public SendMessageHandler(ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
|
||||
super(responseDispatcher);
|
||||
this.userRegistry = userRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a message send request. This method extracts the message from the request,
|
||||
* broadcasts it to all connected users, and dispatches a success response (OK) back to the
|
||||
* sender.
|
||||
*
|
||||
* @param request The {@link SendMessageRequest} containing the message and context.
|
||||
*/
|
||||
@Override
|
||||
public void execute(SendMessageRequest request) {
|
||||
Message message = request.getMessage();
|
||||
broadcast(message);
|
||||
OkResponse response = new OkResponse(request.getContext());
|
||||
responseDispatcher.dispatch(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Distributes a message to every user currently registered in the system. Each user's message
|
||||
* queue is updated with the new message.
|
||||
*
|
||||
* @param message The {@link Message} object to be broadcast.
|
||||
*/
|
||||
public void broadcast(Message message) {
|
||||
userRegistry.getAllUsers().forEach(user -> user.enqueueMessage(message));
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||
|
||||
public class SendMessageParser implements CommandParser<SendMessageRequest> {
|
||||
|
||||
/**
|
||||
* Parses a raw {@link PrimitiveRequest} into a specific {@link SendMessageRequest}. This method
|
||||
* extracts the message details from the request parameters and wraps them along with the
|
||||
* request context into a structured request object.
|
||||
*
|
||||
* @param primitiveRequest The raw request containing parameters and context from the network.
|
||||
* @return A structured {@link SendMessageRequest} containing the parsed {@link Message}.
|
||||
*/
|
||||
@Override
|
||||
public SendMessageRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
Message msg = Message.toMessageReqPars(primitiveRequest.parameters());
|
||||
return new SendMessageRequest(primitiveRequest.context(), msg);
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
|
||||
public class SendMessageRequest extends Request {
|
||||
|
||||
private final Message msg;
|
||||
|
||||
/**
|
||||
* Constructs a new SendMessageRequest with the given context and message.
|
||||
*
|
||||
* @param context The {@link RequestContext} associated with this request.
|
||||
* @param msg The {@link Message} object to be processed.
|
||||
*/
|
||||
public SendMessageRequest(RequestContext context, Message msg) {
|
||||
super(context);
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the message contained within this request.
|
||||
*
|
||||
* @return The {@link Message} instance.
|
||||
*/
|
||||
public Message getMessage() {
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||
|
||||
/** Listener interface for lobby lifecycle events. */
|
||||
public interface LobbyEventListener {
|
||||
/** Called when a game is automatically started in the given lobby. */
|
||||
void onGameStarted(LobbyId lobbyId);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||
|
||||
/** Typesafe wrapper for lobby IDs (1-8). */
|
||||
public record LobbyId(int value) {
|
||||
public static LobbyId of(int value) {
|
||||
return new LobbyId(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby.AddResult;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/** Manages dynamic creation of up to 8 lobbies and maps players to lobbies. */
|
||||
public class LobbyManager {
|
||||
private static final int MAX_LOBBIES = 8;
|
||||
private static final int DEFAULT_MAX_PLAYERS = 4;
|
||||
private static final int AUTO_START_PLAYERS = 4;
|
||||
private static final int DEFAULT_START_CHIPS = 20000;
|
||||
|
||||
private final Map<LobbyId, Lobby> activeLobbies = new ConcurrentHashMap<>();
|
||||
private final Map<String, LobbyId> playerToLobby = new ConcurrentHashMap<>();
|
||||
private final List<LobbyEventListener> listeners = new CopyOnWriteArrayList<>();
|
||||
private final int maxPlayersPerLobby;
|
||||
private static final Logger LOGGER = Logger.getLogger(LobbyManager.class.getName());
|
||||
|
||||
public LobbyManager() {
|
||||
this(DEFAULT_MAX_PLAYERS);
|
||||
}
|
||||
|
||||
public LobbyManager(int maxPlayersPerLobby) {
|
||||
this.maxPlayersPerLobby = maxPlayersPerLobby;
|
||||
}
|
||||
|
||||
/** Create a new lobby with an automatic id (1..8). Returns null if none available. */
|
||||
public synchronized LobbyId createNewLobby(String name) {
|
||||
if (activeLobbies.size() >= MAX_LOBBIES) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 1; i <= MAX_LOBBIES; i++) {
|
||||
LobbyId id = LobbyId.of(i);
|
||||
if (!activeLobbies.containsKey(id)) {
|
||||
Lobby lobby = new Lobby(id, name == null ? ("Room " + i) : name);
|
||||
activeLobbies.put(id, lobby);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Lobby getLobby(LobbyId id) {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
return activeLobbies.get(id);
|
||||
}
|
||||
|
||||
public Lobby getLobbyByUsername(String username) {
|
||||
LobbyId id = playerToLobby.get(username);
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
return activeLobbies.get(id);
|
||||
}
|
||||
|
||||
public boolean addPlayerToLobby(String username, LobbyId lobbyId) {
|
||||
Lobby lobby = getLobby(lobbyId);
|
||||
if (lobby == null) {
|
||||
return false;
|
||||
}
|
||||
AddResult result =
|
||||
lobby.addPlayerAndMaybeStart(
|
||||
username, maxPlayersPerLobby, AUTO_START_PLAYERS, DEFAULT_START_CHIPS);
|
||||
if (result != AddResult.NOT_ADDED) {
|
||||
playerToLobby.put(username, lobbyId);
|
||||
if (result == AddResult.ADDED_AND_STARTED) {
|
||||
LOGGER.info(
|
||||
() ->
|
||||
"Lobby "
|
||||
+ lobbyId.value()
|
||||
+ " reached "
|
||||
+ AUTO_START_PLAYERS
|
||||
+ " players; game started.");
|
||||
notifyGameStarted(lobbyId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void addListener(LobbyEventListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeListener(LobbyEventListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
private void notifyGameStarted(LobbyId lobbyId) {
|
||||
for (LobbyEventListener l : listeners) {
|
||||
try {
|
||||
l.onGameStarted(lobbyId);
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.warning(
|
||||
() ->
|
||||
"Listener threw while handling game-start for lobby "
|
||||
+ lobbyId.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean removePlayer(String username) {
|
||||
LobbyId id = playerToLobby.remove(username);
|
||||
if (id == null) {
|
||||
return false;
|
||||
}
|
||||
Lobby lobby = activeLobbies.get(id);
|
||||
if (lobby == null) {
|
||||
return false;
|
||||
}
|
||||
boolean removed = lobby.removePlayer(username);
|
||||
// If lobby becomes empty, remove it
|
||||
if (lobby.getPlayerNames().isEmpty()) {
|
||||
activeLobbies.remove(id);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
public Collection<Lobby> getAllLobbies() {
|
||||
return activeLobbies.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given action to every player username in the lobby identified by {@code lobbyId}.
|
||||
* This is a small helper that keeps iteration logic centralized and avoids leaking internal
|
||||
* collections to callers.
|
||||
*/
|
||||
public void broadcast(LobbyId lobbyId, java.util.function.Consumer<String> action) {
|
||||
Lobby lobby = getLobby(lobbyId);
|
||||
if (lobby == null) {
|
||||
return;
|
||||
}
|
||||
for (String username : lobby.getPlayerNames()) {
|
||||
action.accept(username);
|
||||
}
|
||||
}
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.message;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||
|
||||
public class MessageManager {
|
||||
private final UserRegistry userRegistry;
|
||||
|
||||
public MessageManager(UserRegistry userRegistry) {
|
||||
this.userRegistry = userRegistry;
|
||||
}
|
||||
|
||||
public void broadcast(Message message) {
|
||||
userRegistry.getAllUsers().forEach(user -> user.enqueueMessage(message));
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.message.Message;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
@@ -98,6 +99,14 @@ public class User {
|
||||
messages.add(message);
|
||||
}
|
||||
|
||||
public synchronized int getMessageCount() {
|
||||
return messages.size();
|
||||
}
|
||||
|
||||
public synchronized Message dequeMessage() throws NoSuchElementException {
|
||||
return messages.remove();
|
||||
}
|
||||
|
||||
public synchronized List<Message> dequeueAllMessages(Message message) {
|
||||
List<Message> allMessages = new ArrayDeque<>(messages).stream().toList();
|
||||
messages.clear();
|
||||
|
||||
Reference in New Issue
Block a user