Merge branch 'feat/game-ui-optimation' into 'main'
Feat: New Game UI Features and bug fixed See merge request cs108-fs26/Gruppe-13!152
This commit was merged in pull request #308.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
const marked = {
|
||||
parse: function (md) {
|
||||
if (!md) return "";
|
||||
|
||||
let toc = [];
|
||||
|
||||
md = md.replace(/^(#{1,3})\s+(.*)$/gim, (m, hashes, title) => {
|
||||
const level = hashes.length;
|
||||
|
||||
const id = title
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\wäöüß]+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
|
||||
toc.push({ level, title, id });
|
||||
|
||||
return `
|
||||
<h${level} id="${id}" class="cm-h${level}">
|
||||
${title}
|
||||
</h${level}>
|
||||
`;
|
||||
});
|
||||
|
||||
md = md.replace(/```([\s\S]*?)```/g, (_, code) => {
|
||||
return `<pre class="cm-code"><code>${escapeHtml(code)}</code></pre>`;
|
||||
});
|
||||
|
||||
md = md.replace(/!\[(.*?)\]\((.*?)\)/gim,
|
||||
`<img alt="$1" src="$2" class="cm-img">`
|
||||
);
|
||||
|
||||
md = md.replace(/\[(.*?)\]\((.*?)\)/gim,
|
||||
`<a href="$2" target="_blank" class="cm-link">$1</a>`
|
||||
);
|
||||
|
||||
md = md.replace(/`(.*?)`/gim,
|
||||
`<code class="cm-inline">$1</code>`
|
||||
);
|
||||
|
||||
md = md.replace(/\*\*(.*?)\*\*/gim, "<b>$1</b>");
|
||||
md = md.replace(/\*(.*?)\*/gim, "<i>$1</i>");
|
||||
|
||||
md = md.replace(/^\s*-\s(.+)$/gim, "<li>$1</li>");
|
||||
md = wrapLists(md);
|
||||
|
||||
md = md
|
||||
.replace(/\n{2,}/g, "</p><p>")
|
||||
.replace(/^(?!<h|<ul|<pre|<li|<\/)(.+)$/gim, "<p>$1</p>");
|
||||
|
||||
return buildToc(toc) + md;
|
||||
}
|
||||
};
|
||||
|
||||
function buildToc(items) {
|
||||
if (!items.length) return "";
|
||||
|
||||
let html = `<div class="cm-toc">`;
|
||||
|
||||
items.forEach(i => {
|
||||
html += `
|
||||
<div class="cm-toc-item level-${i.level}">
|
||||
<a href="#${i.id}">${i.title}</a>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `</div>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
function wrapLists(html) {
|
||||
return html.replace(/(<li>.*?<\/li>)/gs, "<ul>$1</ul>");
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
window.marked = marked;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1,316 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Markdown Viewer</title>
|
||||
<script src="casono-markdown-render-engine.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #0d9e3b;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: 40px;
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
color: #ffffff;
|
||||
text-shadow: 2px 2px 0 #000;
|
||||
}
|
||||
|
||||
p, li {
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: gold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
code {
|
||||
background: rgba(0,0,0,0.4);
|
||||
padding: 2px 6px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid #5c3d10;
|
||||
}
|
||||
|
||||
.cm-toc {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.cm-toc-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.cm-toc-item {
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.cm-toc-item a {
|
||||
color: gold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cm-toc-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.level-2 { margin-left: 12px; opacity: 0.95; }
|
||||
.level-3 { margin-left: 24px; opacity: 0.85; }
|
||||
|
||||
.cm-h1 {
|
||||
font-size: 28px;
|
||||
margin-top: 20px;
|
||||
text-shadow: 2px 2px 0 #000;
|
||||
}
|
||||
|
||||
.cm-h2 {
|
||||
font-size: 22px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.cm-h3 {
|
||||
font-size: 18px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.cm-img {
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* CODE */
|
||||
.cm-code {
|
||||
background: rgba(0,0,0,0.35);
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.cm-inline {
|
||||
background: rgba(0,0,0,0.4);
|
||||
padding: 2px 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="content"></div>
|
||||
|
||||
<script>
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
const markdown = `
|
||||
# Casono Manual
|
||||
|
||||
Hinweis: Eine vereinfachte Einführung finden Sie in der Datei [Casono Rules – Easy Description](casono-rules-easy-description.md).
|
||||
Dieses Dokument beschreibt den Ablauf eines Pokerspiels anhand eines Beispiels.
|
||||
|
||||
## 1. Spielübersicht
|
||||
|
||||
Texas Hold’em ist ein strategisches Kartenspiel für mehrere Spieler.
|
||||
Ziel ist es, den Pot (alle gesetzten Chips) zu gewinnen, entweder durch:
|
||||
|
||||
- die beste Kartenkombination am Ende der Runde
|
||||
- oder indem alle anderen Spieler vorher aussteigen (Fold)
|
||||
|
||||
### Grundregeln
|
||||
|
||||
- Jeder Spieler erhält 2 verdeckte Karten (Hole Cards)
|
||||
- Es werden 5 Gemeinschaftskarten offen in der Mitte ausgelegt
|
||||
- Jeder Spieler bildet die beste 5-Karten-Kombination aus:
|
||||
- eigenen Karten
|
||||
- und Gemeinschaftskarten
|
||||
|
||||
- Zu Spielbeginn erhält jeder Spieler ein Startgeld von 20000 Chips ($)
|
||||
|
||||
## 2. Sitzposition & Dealer-Button
|
||||
|
||||
Der sogenannte Dealer-Button bestimmt die Positionen am Tisch:
|
||||
|
||||
- Er zeigt an, wer als „Geber“ (Dealer) fungiert
|
||||
- Die Positionen rotieren im Uhrzeigersinn nach jeder Runde
|
||||
|
||||
Die Position ist entscheidend, da sie bestimmt:
|
||||
|
||||
- die Reihenfolge der Aktionen
|
||||
- wer die Blinds setzen muss
|
||||
|
||||
## 3. Blinds (Pflichteinsätze)
|
||||
|
||||
Vor jeder Runde werden zwei verpflichtende Einsätze geleistet:
|
||||
|
||||
- **Small Blind** (kleiner Blind): 100 Chips – gesetzt vom Spieler links neben dem Dealer
|
||||
- **Big Blind** (großer Blind): 200 Chips - gesetzt vom Spieler zwei Plätze links vom Dealer
|
||||
|
||||
Diese Einsätze sorgen dafür, dass:
|
||||
|
||||
- ein Startpot entsteht
|
||||
- jede Runde aktiv gespielt wird
|
||||
|
||||
## 4. Spielablauf im Detail
|
||||
|
||||
### 4.1 Preflop (erste Setzrunde)
|
||||
|
||||
Nach dem Austeilen der Karten beginnt die erste Setzrunde.
|
||||
|
||||
Der Spieler links vom Big Blind eröffnet die Runde.
|
||||
|
||||
Jeder Spieler hat folgende Optionen:
|
||||
|
||||
- **Fold** – Karten ablegen und aussteigen
|
||||
- **Call** – Einsatz mitgehen
|
||||
- **Raise** – Einsatz erhöhen
|
||||
|
||||
### 4.2 Flop (3 Gemeinschaftskarten)
|
||||
|
||||
- Drei Karten werden offen auf den Tisch gelegt
|
||||
- Eine neue Setzrunde beginnt
|
||||
- Die Setzrunde beginnt jetzt immer beim ersten aktiven Spieler links vom Dealer (im Uhrzeigersinn).
|
||||
|
||||
Ab diesem Zeitpunkt können alle Spieler ihre Strategie anhand zusätzlicher Informationen anpassen.
|
||||
|
||||
### 4.3 Turn (4. Gemeinschaftskarte)
|
||||
|
||||
- Die vierte Karte wird aufgedeckt
|
||||
- Eine weitere Setzrunde folgt
|
||||
|
||||
Die Einsätze werden oft höher, da sich stärkere Hände entwickeln.
|
||||
|
||||
### 4.4 River (5. Gemeinschaftskarte)
|
||||
|
||||
- Die letzte Karte wird aufgedeckt
|
||||
- Letzte Setzrunde
|
||||
|
||||
Dies ist die finale Entscheidungsphase:
|
||||
|
||||
- Maximierung des Gewinns
|
||||
- oder Minimierung von Verlusten
|
||||
|
||||
## 5. Showdown (Kartenvergleich)
|
||||
|
||||
Wenn nach der letzten Setzrunde mindestens zwei Spieler verbleiben:
|
||||
|
||||
- Alle verbleibenden Spieler decken ihre Karten auf
|
||||
- Die **beste 5-Karten-Kombination gewinnt**
|
||||
|
||||
Wichtig:
|
||||
|
||||
- Die Karten „sprechen für sich“ – die beste Hand zählt unabhängig von Ansagen
|
||||
|
||||
## 6. Poker-Handrangfolge
|
||||
|
||||
Die Stärke der Hände ist eindeutig festgelegt (von schwach nach stark):
|
||||
|
||||
1. High Card (höchste Einzelkarte)
|
||||
2. One Pair (ein Paar)
|
||||
3. Two Pair (zwei Paare)
|
||||
4. Three of a Kind (Drilling)
|
||||
5. Straight (Straße)
|
||||
6. Flush (Farbe)
|
||||
7. Full House
|
||||
8. Four of a Kind (Vierling)
|
||||
9. Straight Flush
|
||||
10. Royal Flush
|
||||
|
||||
Je höher die Kombination, desto stärker die Hand.
|
||||
|
||||
## 7. Wichtige Grundprinzipien
|
||||
|
||||
### Reihenfolge beachten
|
||||
|
||||
Spieler müssen immer der Reihe nach handeln.
|
||||
|
||||
### Klare Aktionen
|
||||
|
||||
Alle Aktionen müssen eindeutig sein:
|
||||
|
||||
- Einsätze klar ansagen oder eindeutig setzen
|
||||
|
||||
### Ein Spieler – eine Hand
|
||||
|
||||
- Spieler dürfen ihre Karten nicht teilen oder gemeinsam spielen
|
||||
|
||||
### Fehlerhafte Einsätze
|
||||
|
||||
- Unklare oder falsche Einsätze können korrigiert werden, abhängig von der Spielsituation (nur wenn der Einsatz außerhalb der gültigen Grenzen liegt; zu hohe oder unzulässige Beträge werden blockiert und nicht automatisch korrigiert)
|
||||
|
||||
## 8. Strategische Einordnung
|
||||
|
||||
Texas Hold’em ist kein reines Glücksspiel. Der Erfolg basiert auf:
|
||||
|
||||
- Wahrscheinlichkeiten (Mathematik)
|
||||
- Einschätzung von Gegnern (Psychologie)
|
||||
- Positionsspiel und Timing
|
||||
`;
|
||||
|
||||
document.getElementById("content").innerHTML =
|
||||
marked.parse(markdown);
|
||||
|
||||
document.querySelectorAll("#content a").forEach(link => {
|
||||
link.addEventListener("click", (e) => {
|
||||
const href = link.getAttribute("href");
|
||||
|
||||
if (href && href.endsWith(".md")) {
|
||||
e.preventDefault();
|
||||
alert("You cannot load an external MD file using `file:///` without a server.");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<noscript>
|
||||
<div style="
|
||||
padding: 40px;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #0d9e3b;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
">
|
||||
<h2>JavaScript is disabled</h2>
|
||||
|
||||
<p>
|
||||
The Casono Browser requires JavaScript for the <br>
|
||||
Casono Markdown Render Engine to display content correctly.<br><br>
|
||||
|
||||
For security, JavaScript is disabled by default in the Casono Browser and should only be enabled when needed.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Please enable JavaScript in your Browser settings and reload the page.
|
||||
</p>
|
||||
|
||||
<img src="images/activate-js.png" alt="JavaScript aktivieren Anleitung" style="max-width: 100%; margin-top: 20px; border-radius: 10px;">
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+520
-21
@@ -1,11 +1,14 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||
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.game.PlayerState;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.PlayerStatusController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.TaskbarController;
|
||||
@@ -138,11 +141,21 @@ public class CasinoGameController {
|
||||
private static final long CHIP_STAGGER_DELAY_MULTIPLIER = 35L;
|
||||
private static final double CHAT_WIDTH = 400;
|
||||
private static final double CHAT_HEIGHT = 600;
|
||||
private static final String ACTIVE_CARD_BOX_STYLE_CLASS = "player-cards-active-turn";
|
||||
private static final int DEALER_OFFSET = 3;
|
||||
private static final String FIRST_PREFLOP_INFO_TEMPLATE =
|
||||
"Bet the big blind amount: %d$ or +%d$";
|
||||
|
||||
private String chatUsername;
|
||||
private ClientService chatClientService;
|
||||
private int chatLobbyId = -1;
|
||||
private boolean chatInitialized;
|
||||
private boolean gameFinished;
|
||||
private boolean localWinAnnouncementSent;
|
||||
private String lastWinnerName;
|
||||
private static final int SMALL_BLIND = 100;
|
||||
private static final int BIG_BLIND = 200;
|
||||
private static final int NO_BET = 0;
|
||||
|
||||
/** Standard constructor. Used by FXML. */
|
||||
public CasinoGameController() {
|
||||
@@ -194,11 +207,18 @@ public class CasinoGameController {
|
||||
if (controller != null && myPlayerId != null) {
|
||||
controller.setGameService(gameService, myPlayerId);
|
||||
}
|
||||
configureTaskbarLobbyActionAnnouncements();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the PlayerId of the current player.
|
||||
*
|
||||
* @param chatController The ChatController instance used for managing the chat functionality.
|
||||
*/
|
||||
public void startChat(ChatController chatController) {
|
||||
this.chatController = chatController;
|
||||
initializeChatIfPossible();
|
||||
configureTaskbarLobbyActionAnnouncements();
|
||||
}
|
||||
|
||||
/** Set the PlayerId of the current player. */
|
||||
@@ -210,6 +230,7 @@ public class CasinoGameController {
|
||||
if (taskbarController != null && gameService != null && myPlayerId != null) {
|
||||
taskbarController.setGameService(gameService, myPlayerId);
|
||||
}
|
||||
configureTaskbarLobbyActionAnnouncements();
|
||||
|
||||
if (communityCardsBox == null) {
|
||||
LOGGER.warning("communityCardsBox is NULL");
|
||||
@@ -230,6 +251,7 @@ public class CasinoGameController {
|
||||
}
|
||||
|
||||
myDealerIcon.setVisible(false);
|
||||
tableText.setWrapText(true);
|
||||
|
||||
javafx.stage.Screen screen = javafx.stage.Screen.getPrimary();
|
||||
double screenHeight = screen.getBounds().getHeight();
|
||||
@@ -262,6 +284,7 @@ public class CasinoGameController {
|
||||
this.chatUsername = username;
|
||||
this.chatClientService = clientService;
|
||||
this.chatLobbyId = lobbyId;
|
||||
configureTaskbarLobbyActionAnnouncements();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -477,7 +500,7 @@ public class CasinoGameController {
|
||||
*/
|
||||
private void applyState(GameState s) {
|
||||
|
||||
if (s == null) {
|
||||
if (s == null || gameFinished) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -490,27 +513,33 @@ public class CasinoGameController {
|
||||
+ (s.players != null ? s.players.size() : 0));
|
||||
|
||||
List<Player> players = (s.players != null) ? s.players : List.of();
|
||||
List<Card> community = (s.communityCards != null) ? s.communityCards : List.of();
|
||||
List<Card> myCards = getMyCards(players);
|
||||
|
||||
java.util.List<String> newCommunityKeys = cardKeys(community, TOTAL_SLOTS);
|
||||
if (!newCommunityKeys.equals(lastCommunityKeys)) {
|
||||
lastCommunityKeys = newCommunityKeys;
|
||||
renderCommunityCards(community);
|
||||
String winnerName = resolveWinnerName(s);
|
||||
if (winnerName != null && !winnerName.isBlank()) {
|
||||
lastWinnerName = winnerName;
|
||||
} else if (!isTerminalPhase(s.phase)) {
|
||||
lastWinnerName = null;
|
||||
}
|
||||
|
||||
java.util.List<String> newMyCardKeys = cardKeys(myCards, PLAYER_SLOTS);
|
||||
if (!newMyCardKeys.equals(lastMyCardKeys)) {
|
||||
lastMyCardKeys = newMyCardKeys;
|
||||
renderPlayerCards(myCards);
|
||||
if (isGameFinishedState(s, winnerName)) {
|
||||
updatePlayers(players, -1);
|
||||
syncWhisperTargetsFromPlayers(players);
|
||||
updateGameInfo(s);
|
||||
highlightDealer(s);
|
||||
updateTaskbar(s);
|
||||
clearActiveTurnHighlights();
|
||||
announceLocalWinIfNeeded(s);
|
||||
finishGameUiLoop();
|
||||
return;
|
||||
}
|
||||
|
||||
updateCards(s, players);
|
||||
|
||||
if (s.pot != lastPot) {
|
||||
lastPot = s.pot;
|
||||
renderPot(s.pot);
|
||||
}
|
||||
|
||||
updatePlayers(players);
|
||||
updatePlayers(players, s.activePlayer);
|
||||
syncWhisperTargetsFromPlayers(players);
|
||||
updateGameInfo(s);
|
||||
highlightDealer(s);
|
||||
@@ -533,6 +562,32 @@ public class CasinoGameController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the displayed community cards and the player's hole cards based on the current game
|
||||
* state.
|
||||
*
|
||||
* @param s The current game state containing the community cards and the list of players, used
|
||||
* to determine.
|
||||
* @param players The list of players currently in the game, used to retrieve the player's hole
|
||||
* cards for display. If null, it will be treated as an empty list.
|
||||
*/
|
||||
private void updateCards(GameState s, List<Player> players) {
|
||||
List<Card> community = (s.communityCards != null) ? s.communityCards : List.of();
|
||||
List<Card> myCards = getMyCards(players);
|
||||
|
||||
var newCommunityKeys = cardKeys(community, TOTAL_SLOTS);
|
||||
if (!newCommunityKeys.equals(lastCommunityKeys)) {
|
||||
lastCommunityKeys = newCommunityKeys;
|
||||
renderCommunityCards(community);
|
||||
}
|
||||
|
||||
var newMyCardKeys = cardKeys(myCards, PLAYER_SLOTS);
|
||||
if (!newMyCardKeys.equals(lastMyCardKeys)) {
|
||||
lastMyCardKeys = newMyCardKeys;
|
||||
renderPlayerCards(myCards);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize the whisper chat targets with the current list of players.
|
||||
*
|
||||
@@ -610,14 +665,102 @@ public class CasinoGameController {
|
||||
*/
|
||||
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();
|
||||
if (s == null) {
|
||||
tableText.setText("Phase: -");
|
||||
return;
|
||||
}
|
||||
|
||||
tableText.setText(text);
|
||||
String winnerName = resolveWinnerName(s);
|
||||
if (winnerName != null && !winnerName.isBlank()) {
|
||||
tableText.setText("Winner: " + winnerName);
|
||||
return;
|
||||
}
|
||||
|
||||
String firstPreflopInfo = resolveFirstPreflopInfo(s);
|
||||
if (firstPreflopInfo != null) {
|
||||
tableText.setText(firstPreflopInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
String phaseText = formatPhaseText(s.phase);
|
||||
StringBuilder text = new StringBuilder("Phase: ").append(phaseText);
|
||||
|
||||
tableText.setText(text.toString());
|
||||
}
|
||||
|
||||
private String formatPhaseText(String phase) {
|
||||
if (phase == null || phase.isBlank()) {
|
||||
return "-";
|
||||
}
|
||||
return phase.toUpperCase().replace("_", " ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a context-specific info message for the first player to act preflop.
|
||||
*
|
||||
* @param s The current game state.
|
||||
* @return An English hint with the big blind amount and its double, or null if the special case
|
||||
* does not apply.
|
||||
*/
|
||||
private String resolveFirstPreflopInfo(GameState s) {
|
||||
if (s == null || s.players == null || s.players.size() < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!"PREFLOP".equalsIgnoreCase(s.phase)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int firstIndex =
|
||||
(s.players.size() == 2) ? s.dealer : (s.dealer + DEALER_OFFSET) % s.players.size();
|
||||
if (s.activePlayer != firstIndex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isInitialPreflopBlindLayout(s)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int callTarget = Math.max(0, s.currentBet);
|
||||
if (callTarget <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
long raiseTarget = (long) callTarget * 2L;
|
||||
int safeRaiseTarget =
|
||||
raiseTarget > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) raiseTarget;
|
||||
return String.format(FIRST_PREFLOP_INFO_TEMPLATE, callTarget, safeRaiseTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the preflop state still matches the initial blind layout.
|
||||
*
|
||||
* @param s The current game state.
|
||||
* @return true if exactly one small blind and one big blind are present and all other players
|
||||
* have not yet committed chips.
|
||||
*/
|
||||
private boolean isInitialPreflopBlindLayout(GameState s) {
|
||||
int sbCount = 0;
|
||||
int bbCount = 0;
|
||||
int zeroCount = 0;
|
||||
|
||||
for (Player player : s.players) {
|
||||
if (player == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int bet = Math.max(0, player.getBet());
|
||||
if (bet == SMALL_BLIND) {
|
||||
sbCount++;
|
||||
} else if (bet == BIG_BLIND) {
|
||||
bbCount++;
|
||||
} else if (bet == NO_BET) {
|
||||
zeroCount++;
|
||||
}
|
||||
}
|
||||
|
||||
int playerCount = s.players.size();
|
||||
return playerCount >= 2 && sbCount == 1 && bbCount == 1 && zeroCount == playerCount - 2;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -644,8 +787,10 @@ public class CasinoGameController {
|
||||
* @param players The list of players currently in the game, used to determine which players are
|
||||
* opponents and update their display in the UI accordingly. If the list is null or empty,
|
||||
* all opponent slots will be cleared.
|
||||
* @param activePlayerIndex The index of the active player in the players list, used to
|
||||
* determine whose turn it is and update the UI highlights to reflect that.
|
||||
*/
|
||||
private void updatePlayers(List<Player> players) {
|
||||
private void updatePlayers(List<Player> players, int activePlayerIndex) {
|
||||
if (players == null || players.isEmpty()) {
|
||||
clearOpponentSlots();
|
||||
return;
|
||||
@@ -671,6 +816,7 @@ public class CasinoGameController {
|
||||
safeRefresh(player1Controller);
|
||||
safeRefresh(player2Controller);
|
||||
safeRefresh(player3Controller);
|
||||
updateActiveTurnHighlights(players, activePlayerIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -695,7 +841,7 @@ public class CasinoGameController {
|
||||
}
|
||||
|
||||
PlayerId pid = p.getId();
|
||||
boolean isMe = myPlayerId != null && pid != null && pid.equals(myPlayerId);
|
||||
boolean isMe = myPlayerId != null && myPlayerId.equals(pid);
|
||||
|
||||
if (isMe) {
|
||||
meFound = true;
|
||||
@@ -767,6 +913,318 @@ public class CasinoGameController {
|
||||
safeRefresh(player1Controller);
|
||||
safeRefresh(player2Controller);
|
||||
safeRefresh(player3Controller);
|
||||
clearActiveTurnHighlights();
|
||||
setTaskbarTurnHighlight(false);
|
||||
}
|
||||
|
||||
/** Clear all visual highlights in the UI that indicate the active player's turn. */
|
||||
private void clearActiveTurnHighlights() {
|
||||
if (player1Controller != null) {
|
||||
player1Controller.setTurnHighlighted(false);
|
||||
}
|
||||
|
||||
if (player2Controller != null) {
|
||||
player2Controller.setTurnHighlighted(false);
|
||||
}
|
||||
|
||||
if (player3Controller != null) {
|
||||
player3Controller.setTurnHighlighted(false);
|
||||
}
|
||||
|
||||
if (playerStatusController != null) {
|
||||
playerStatusController.setTurnHighlighted(false);
|
||||
}
|
||||
|
||||
setPlayerCardsTurnHighlighted(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the visual highlight state of the player's hole cards to indicate whether it is currently
|
||||
* the player's turn in the game.
|
||||
*
|
||||
* @param highlighted true to highlight the player's hole cards, indicating that it is their
|
||||
* turn, or false to remove the highlight when it is not their turn.
|
||||
*/
|
||||
private void setPlayerCardsTurnHighlighted(boolean highlighted) {
|
||||
if (playerCardsBox == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Node child : playerCardsBox.getChildren()) {
|
||||
if (child instanceof ImageView) {
|
||||
toggleStyleClass(child, ACTIVE_CARD_BOX_STYLE_CLASS, highlighted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the visual highlights in the UI to indicate which player's turn it is based on the
|
||||
* active.
|
||||
*
|
||||
* @param players The list of players currently in the game, used to identify the active player
|
||||
* and update the turn highlights accordingly.
|
||||
* @param activePlayerIndex The index of the active player in the players list, used to
|
||||
* determine whose turn it is and update the UI highlights to reflect that.
|
||||
*/
|
||||
private void updateActiveTurnHighlights(List<Player> players, int activePlayerIndex) {
|
||||
clearActiveTurnHighlights();
|
||||
|
||||
Player activePlayer = getPlayerAtIndex(players, activePlayerIndex);
|
||||
if (activePlayer == null || activePlayer.getId() == null) {
|
||||
setTaskbarTurnHighlight(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (myPlayerId != null && myPlayerId.equals(activePlayer.getId())) {
|
||||
setPlayerCardsTurnHighlighted(true);
|
||||
setTaskbarTurnHighlight(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setTaskbarTurnHighlight(false);
|
||||
|
||||
if (player1Controller != null && player1Controller.hasPlayer(activePlayer)) {
|
||||
player1Controller.setTurnHighlighted(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (player2Controller != null && player2Controller.hasPlayer(activePlayer)) {
|
||||
player2Controller.setTurnHighlighted(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (player3Controller != null && player3Controller.hasPlayer(activePlayer)) {
|
||||
player3Controller.setTurnHighlighted(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the turn highlight state in the taskbar controller, which visually indicates whether it
|
||||
* is the current player's turn in the game.
|
||||
*
|
||||
* @param highlighted true to highlight the turn in the taskbar, false to remove the highlight.
|
||||
*/
|
||||
private void setTaskbarTurnHighlight(boolean highlighted) {
|
||||
TaskbarController taskbarCtrl = resolveTaskbarController();
|
||||
if (taskbarCtrl != null) {
|
||||
taskbarCtrl.setTurnHighlighted(highlighted);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the Player object at the specified index from the list of players.
|
||||
*
|
||||
* @param players The list of players from which to retrieve the player at the specified index.
|
||||
* @param index The index of the player to retrieve from the list.
|
||||
* @return The Player object at the specified index if it exists and is valid, or null if the
|
||||
* index is out of bounds or if the players list is null or empty.
|
||||
*/
|
||||
private Player getPlayerAtIndex(List<Player> players, int index) {
|
||||
if (players == null || index < 0 || index >= players.size()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return players.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the winner of the hand based on the game state.
|
||||
*
|
||||
* @param s The current game state, which may contain information about the players, their
|
||||
* states, and the winner index.
|
||||
* @return The Player object representing the winner if it can be determined from the game
|
||||
* state, or null if the winner cannot be resolved or if the game is still in progress.
|
||||
*/
|
||||
private Player resolveWinner(GameState s) {
|
||||
if (s == null || s.players == null || s.players.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (s.winnerIndex >= 0 && s.winnerIndex < s.players.size()) {
|
||||
return s.players.get(s.winnerIndex);
|
||||
}
|
||||
|
||||
Player remaining = null;
|
||||
int remainingCount = 0;
|
||||
for (Player player : s.players) {
|
||||
if (player == null) {
|
||||
continue;
|
||||
}
|
||||
if (player.getState() == PlayerState.FOLDED) {
|
||||
continue;
|
||||
}
|
||||
if (remainingCount > 0) {
|
||||
return null;
|
||||
}
|
||||
remaining = player;
|
||||
remainingCount++;
|
||||
}
|
||||
|
||||
if (remaining != null && remainingCount == 1) {
|
||||
return remaining;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the name of the winner based on the game state.
|
||||
*
|
||||
* @param s The current game state, which may contain information about the players, their
|
||||
* states, and the winner index.
|
||||
* @return The name of the winner if it can be determined from the game state, or null if the
|
||||
* winner cannot be resolved or if the game is still in progress.
|
||||
*/
|
||||
private String resolveWinnerName(GameState s) {
|
||||
Player winner = resolveWinner(s);
|
||||
if (winner != null) {
|
||||
String name = winner.getName();
|
||||
if (name != null && !name.isBlank()) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
if (isTerminalPhase(s != null ? s.phase : null)
|
||||
&& lastWinnerName != null
|
||||
&& !lastWinnerName.isBlank()) {
|
||||
return lastWinnerName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a short lobby chat message when the local player wins, but only once per hand.
|
||||
*
|
||||
* @param s The current game state.
|
||||
*/
|
||||
private void announceLocalWinIfNeeded(GameState s) {
|
||||
if (localWinAnnouncementSent || s == null || !isTerminalPhase(s.phase)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Player winner = resolveWinner(s);
|
||||
boolean isLocalWinner =
|
||||
winner != null
|
||||
&& winner.getId() != null
|
||||
&& myPlayerId != null
|
||||
&& myPlayerId.equals(winner.getId());
|
||||
|
||||
if (!isLocalWinner && lastWinnerName != null && !lastWinnerName.isBlank()) {
|
||||
String localName =
|
||||
(chatUsername != null && !chatUsername.isBlank())
|
||||
? chatUsername
|
||||
: (winner != null ? winner.getName() : null);
|
||||
isLocalWinner = localName != null && localName.equalsIgnoreCase(lastWinnerName);
|
||||
}
|
||||
|
||||
if (!isLocalWinner) {
|
||||
return;
|
||||
}
|
||||
|
||||
localWinAnnouncementSent = true;
|
||||
sendLobbyActionMessage("I won");
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the game is in a finished state based on the game state and the presence of a
|
||||
* winner name.
|
||||
*
|
||||
* @param s The current game state, which may be null or contain information about the phase,
|
||||
* players, and winner index.
|
||||
* @param winnerName The name of the winner, which may be null or blank if the winner is not yet
|
||||
* determined or if the game is still in progress.
|
||||
* @return true if the game is considered finished based on the provided state and winner
|
||||
* information, false otherwise.
|
||||
*/
|
||||
private boolean isGameFinishedState(GameState s, String winnerName) {
|
||||
if (s == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean winnerKnown = winnerName != null && !winnerName.isBlank();
|
||||
if (!winnerKnown) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isTerminalPhase(s.phase)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (s.winnerIndex >= 0 && s.players != null && s.winnerIndex < s.players.size()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return hasSingleRemainingPlayer(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if there is only one remaining active player in the game state.
|
||||
*
|
||||
* @param s The current game state, which may be null or contain a list of players.
|
||||
* @return true if there is exactly one active player remaining (not folded), false otherwise.
|
||||
*/
|
||||
private boolean hasSingleRemainingPlayer(GameState s) {
|
||||
if (s == null || s.players == null || s.players.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int remaining = 0;
|
||||
for (Player player : s.players) {
|
||||
if (player == null || player.getState() == PlayerState.FOLDED) {
|
||||
continue;
|
||||
}
|
||||
remaining++;
|
||||
if (remaining > 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return remaining == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current phase of the game is a terminal phase, such as "FINISHED" or
|
||||
* "SHOWDOWN".
|
||||
*
|
||||
* @param phase The current phase of the game, which may be null or blank.
|
||||
* @return true if the phase is considered terminal, indicating that the hand has ended and a
|
||||
* winner can be declared, false otherwise.
|
||||
*/
|
||||
private boolean isTerminalPhase(String phase) {
|
||||
return "FINISHED".equalsIgnoreCase(phase) || "SHOWDOWN".equalsIgnoreCase(phase);
|
||||
}
|
||||
|
||||
/** Finish the game UI loop by stopping the timeline that updates the UI. */
|
||||
private void finishGameUiLoop() {
|
||||
if (gameFinished) {
|
||||
return;
|
||||
}
|
||||
gameFinished = true;
|
||||
if (timeline != null) {
|
||||
timeline.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle a CSS style class on a JavaFX Node based on the active state.
|
||||
*
|
||||
* @param node The JavaFX Node on which to toggle the style class.
|
||||
* @param styleClass The name of the CSS style class to toggle.
|
||||
* @param active A boolean indicating whether to add (true) or remove (false) the style class
|
||||
* from the node.
|
||||
*/
|
||||
private void toggleStyleClass(Node node, String styleClass, boolean active) {
|
||||
if (node == null || styleClass == null || styleClass.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (active) {
|
||||
if (!node.getStyleClass().contains(styleClass)) {
|
||||
node.getStyleClass().add(styleClass);
|
||||
}
|
||||
} else {
|
||||
node.getStyleClass().remove(styleClass);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1315,6 +1773,47 @@ public class CasinoGameController {
|
||||
if (controller != null && gameService != null && myPlayerId != null) {
|
||||
controller.setGameService(gameService, myPlayerId);
|
||||
}
|
||||
configureTaskbarLobbyActionAnnouncements();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the TaskbarController to announce lobby actions by sending chat messages when
|
||||
* certain actions occur in the game, such as winning a hand.
|
||||
*/
|
||||
private void configureTaskbarLobbyActionAnnouncements() {
|
||||
TaskbarController controller = resolveTaskbarController();
|
||||
if (controller == null) {
|
||||
return;
|
||||
}
|
||||
controller.setLobbyActionAnnouncer(this::sendLobbyActionMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a lobby action message to the chat controller with the specified action label, which can
|
||||
* be used to announce game events in the lobby chat.
|
||||
*
|
||||
* @param actionLabel The label describing the action to announce in the lobby chat, such as "I
|
||||
* won" when the local player wins a hand.
|
||||
*/
|
||||
private void sendLobbyActionMessage(String actionLabel) {
|
||||
if (chatController == null
|
||||
|| chatLobbyId < 0
|
||||
|| actionLabel == null
|
||||
|| actionLabel.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String sender =
|
||||
(chatUsername != null && !chatUsername.isBlank())
|
||||
? chatUsername
|
||||
: chatController.getCurrentUsername();
|
||||
if (sender == null || sender.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Message message =
|
||||
new Message(ChatType.LOBBY, chatLobbyId, sender, null, actionLabel.trim());
|
||||
chatController.onSendToNetwork(message);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+225
-13
@@ -4,17 +4,24 @@ import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
@@ -63,6 +70,11 @@ public class CasinoBrowserController {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class);
|
||||
|
||||
private static final ObservableList<String> URL_SUGGESTIONS =
|
||||
FXCollections.observableArrayList(TRUSTED_DOMAINS);
|
||||
|
||||
private static ContextMenu autoCompleteMenu = new ContextMenu();
|
||||
|
||||
private static final int LOGO_HEIGHT = 40;
|
||||
private static final int CORNER_RADIUS = 40;
|
||||
private static final int HBOX_SPACIN = 10;
|
||||
@@ -74,12 +86,15 @@ public class CasinoBrowserController {
|
||||
private static final int ALERT_HEIGHT = 300;
|
||||
private static final String LOGO_PATH = "/images/logoinverted.png";
|
||||
private static final String LOGO_PATH_MAIN = "/images/logo.png";
|
||||
private static final int MAX_AUTOCOMPLETE_RESULTS = 5;
|
||||
|
||||
static {
|
||||
CookieHandler.setDefault(COOKIE_MANAGER);
|
||||
|
||||
TRUSTED_DOMAINS.add("wikipedia.org");
|
||||
TRUSTED_DOMAINS.add("youtube.com");
|
||||
TRUSTED_DOMAINS.add("github.com");
|
||||
TRUSTED_DOMAINS.add("search.brave.com");
|
||||
TRUSTED_DOMAINS.add("oracle.com");
|
||||
TRUSTED_DOMAINS.add("stackoverflow.com");
|
||||
TRUSTED_DOMAINS.add("docs.oracle.com");
|
||||
@@ -94,10 +109,63 @@ public class CasinoBrowserController {
|
||||
TRUSTED_DOMAINS.add("metager.de");
|
||||
TRUSTED_DOMAINS.add("unibas.ch");
|
||||
TRUSTED_DOMAINS.add("mojeek.com");
|
||||
TRUSTED_DOMAINS.add("searx.be ");
|
||||
TRUSTED_DOMAINS.add("searx.be");
|
||||
TRUSTED_DOMAINS.add("startpage.com");
|
||||
}
|
||||
|
||||
/** Aliases for common websites. */
|
||||
private static final java.util.Map<String, String> ALIASES =
|
||||
Map.ofEntries(
|
||||
Map.entry("wiki", "wikipedia.org"),
|
||||
Map.entry("yt", "youtube.com"),
|
||||
Map.entry("bra", "search.brave.com"),
|
||||
Map.entry("bs", "search.brave.com"),
|
||||
Map.entry("gh", "github.com"),
|
||||
Map.entry("google", "google.com"),
|
||||
Map.entry("duck", "duckduckgo.com"),
|
||||
Map.entry("bing", "bing.com"),
|
||||
Map.entry("meta", "metager.de"),
|
||||
Map.entry("mojeek", "mojeek.com"),
|
||||
Map.entry("searx", "searx.be"),
|
||||
Map.entry("startpage", "startpage.com"),
|
||||
Map.entry("stack", "stackoverflow.com"),
|
||||
Map.entry("so", "stackoverflow.com"),
|
||||
Map.entry("oracle", "oracle.com"),
|
||||
Map.entry("docs", "docs.oracle.com"),
|
||||
Map.entry("mdn", "developer.mozilla.org"),
|
||||
Map.entry("maven", "maven.apache.org"),
|
||||
Map.entry("gradle", "gradle.org"),
|
||||
Map.entry("spring", "spring.io"),
|
||||
Map.entry("jetbrains", "jetbrains.com"),
|
||||
Map.entry("uni", "unibas.ch"));
|
||||
|
||||
/**
|
||||
* Resolves user input into a valid URL.
|
||||
*
|
||||
* @param input User input from the URL field, which can be a full URL, a domain name, or an
|
||||
* alias.
|
||||
* @return A properly formatted URL string that can be loaded by the browser, or the original
|
||||
* input if it cannot be resolved.
|
||||
*/
|
||||
private static String resolveInputToUrl(String input) {
|
||||
if (input == null || input.isBlank()) {
|
||||
return input;
|
||||
}
|
||||
|
||||
input = input.trim();
|
||||
|
||||
String alias = ALIASES.get(input.toLowerCase());
|
||||
if (alias != null) {
|
||||
return "https://" + alias;
|
||||
}
|
||||
|
||||
if (input.matches("^[a-zA-Z][a-zA-Z0-9+.-]*://.*")) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return "https://" + input;
|
||||
}
|
||||
|
||||
private static boolean javascriptEnabled = false;
|
||||
|
||||
/** Deletes all cookies stored during the current browser session. */
|
||||
@@ -124,6 +192,15 @@ public class CasinoBrowserController {
|
||||
WebView webView = new WebView();
|
||||
WebEngine engine = webView.getEngine();
|
||||
|
||||
engine.locationProperty()
|
||||
.addListener(
|
||||
(obs, oldUrl, newUrl) -> {
|
||||
if (isDownloadUrl(newUrl)) {
|
||||
LOGGER.warn("Download navigation blocked: " + newUrl);
|
||||
engine.getLoadWorker().cancel();
|
||||
}
|
||||
});
|
||||
|
||||
engine.setJavaScriptEnabled(false);
|
||||
|
||||
configurePopupBlocker(engine);
|
||||
@@ -203,9 +280,9 @@ public class CasinoBrowserController {
|
||||
engine.setCreatePopupHandler(
|
||||
config -> {
|
||||
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||
alert.setTitle("Popup blockiert");
|
||||
alert.setTitle("Popup blocked");
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText("Popup wurde aus Sicherheitsgründen blockiert.");
|
||||
alert.setContentText("Casono Browser blocked a security threat.");
|
||||
|
||||
try {
|
||||
var stream = CasinoBrowserController.class.getResourceAsStream(LOGO_PATH);
|
||||
@@ -283,9 +360,90 @@ public class CasinoBrowserController {
|
||||
urlField.getStyleClass().add("gray-input-field");
|
||||
HBox.setHgrow(urlField, Priority.ALWAYS);
|
||||
|
||||
urlField.textProperty()
|
||||
.addListener(
|
||||
(obs, oldText, newText) -> {
|
||||
if (newText == null || newText.isBlank()) {
|
||||
autoCompleteMenu.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
String input = newText.toLowerCase();
|
||||
|
||||
String aliasMatch = ALIASES.get(input);
|
||||
if (aliasMatch != null) {
|
||||
autoCompleteMenu.getItems().clear();
|
||||
|
||||
MenuItem item = new MenuItem(aliasMatch);
|
||||
item.setOnAction(
|
||||
e -> {
|
||||
urlField.setText("https://" + aliasMatch);
|
||||
autoCompleteMenu.hide();
|
||||
});
|
||||
|
||||
autoCompleteMenu.getItems().add(item);
|
||||
autoCompleteMenu.show(urlField, javafx.geometry.Side.BOTTOM, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
updateSuggestions(input, urlField);
|
||||
});
|
||||
|
||||
urlField.focusedProperty()
|
||||
.addListener(
|
||||
(obs, oldVal, newVal) -> {
|
||||
if (!newVal) {
|
||||
autoCompleteMenu.hide();
|
||||
}
|
||||
});
|
||||
|
||||
return urlField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the autocomplete suggestions based on the current input in the URL field. Suggestions
|
||||
* are filtered from the list of trusted domains and sorted to prioritize those that start with
|
||||
* the input.
|
||||
*
|
||||
* @param input The current text input from the URL field, used to filter and sort suggestions.
|
||||
* @param urlField The TextField for the URL input, used to position the autocomplete menu and
|
||||
* update its content.
|
||||
*/
|
||||
private static void updateSuggestions(String input, TextField urlField) {
|
||||
autoCompleteMenu.getItems().clear();
|
||||
|
||||
URL_SUGGESTIONS.stream()
|
||||
.filter(
|
||||
domain ->
|
||||
domain.toLowerCase().startsWith(input)
|
||||
|| domain.toLowerCase().contains(input))
|
||||
.sorted(
|
||||
(a, b) -> {
|
||||
boolean aStarts = a.startsWith(input);
|
||||
boolean bStarts = b.startsWith(input);
|
||||
return Boolean.compare(!aStarts, !bStarts);
|
||||
})
|
||||
.limit(MAX_AUTOCOMPLETE_RESULTS)
|
||||
.forEach(
|
||||
domain -> {
|
||||
MenuItem item = new MenuItem(domain);
|
||||
|
||||
item.setOnAction(
|
||||
e -> {
|
||||
urlField.setText("https://" + domain);
|
||||
autoCompleteMenu.hide();
|
||||
});
|
||||
|
||||
autoCompleteMenu.getItems().add(item);
|
||||
});
|
||||
|
||||
if (!autoCompleteMenu.getItems().isEmpty()) {
|
||||
autoCompleteMenu.show(urlField, javafx.geometry.Side.BOTTOM, 0, 0);
|
||||
} else {
|
||||
autoCompleteMenu.hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Security Label
|
||||
*
|
||||
@@ -305,7 +463,7 @@ public class CasinoBrowserController {
|
||||
* @return Toggle button
|
||||
*/
|
||||
public static Button createJsToggle(WebEngine engine) {
|
||||
Button jsToggle = new Button("JS EINSCHALTEN");
|
||||
Button jsToggle = new Button("JS TURN ON");
|
||||
jsToggle.getStyleClass().add("red-button");
|
||||
|
||||
jsToggle.setOnAction(
|
||||
@@ -319,7 +477,7 @@ public class CasinoBrowserController {
|
||||
jsToggle.getStyleClass().add("yellow-button");
|
||||
|
||||
} else {
|
||||
jsToggle.setText("JS EINSCHALTEN");
|
||||
jsToggle.setText("JS TURN ON");
|
||||
jsToggle.getStyleClass().removeAll("yellow-button");
|
||||
jsToggle.getStyleClass().add("red-button");
|
||||
}
|
||||
@@ -410,7 +568,16 @@ public class CasinoBrowserController {
|
||||
*/
|
||||
public static void configureUrlEvents(
|
||||
WebEngine engine, TextField urlField, Label securityLabel) {
|
||||
urlField.setOnAction(e -> loadUrlSafely(engine, urlField.getText(), securityLabel));
|
||||
urlField.setOnKeyPressed(
|
||||
e -> {
|
||||
if (e.getCode() == KeyCode.ENTER) {
|
||||
if (!autoCompleteMenu.isShowing()) {
|
||||
String resolved = resolveInputToUrl(urlField.getText());
|
||||
urlField.setText(resolved);
|
||||
loadUrlSafely(engine, resolved, securityLabel);
|
||||
}
|
||||
}
|
||||
});
|
||||
engine.locationProperty().addListener((obs, o, n) -> urlField.setText(n));
|
||||
}
|
||||
|
||||
@@ -468,25 +635,54 @@ public class CasinoBrowserController {
|
||||
*/
|
||||
private static void loadUrlSafely(WebEngine engine, String url, Label securityLabel) {
|
||||
try {
|
||||
if (!url.startsWith("http")) {
|
||||
if (!url.matches("^[a-zA-Z][a-zA-Z0-9+.-]*://.*")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
|
||||
URI uri = new URI(url);
|
||||
String scheme = uri.getScheme();
|
||||
|
||||
// HTTPS required
|
||||
if (!"https".equalsIgnoreCase(uri.getScheme())) {
|
||||
String fullUrl = uri.toString();
|
||||
|
||||
if (isDownloadUrl(fullUrl)) {
|
||||
securityLabel.setText("BLOCKED DOWNLOAD");
|
||||
|
||||
LOGGER.warn("Download blocked: " + fullUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("file".equalsIgnoreCase(scheme)) {
|
||||
Path allowed =
|
||||
Paths.get(
|
||||
System.getProperty("user.dir"),
|
||||
"documents",
|
||||
"docs",
|
||||
"game-engine",
|
||||
"manual.html")
|
||||
.normalize();
|
||||
|
||||
Path requested = Paths.get(uri).normalize();
|
||||
|
||||
if (requested.equals(allowed)) {
|
||||
securityLabel.setText("LOCAL OK");
|
||||
engine.load(uri.toString());
|
||||
} else {
|
||||
securityLabel.setText("BLOCKED");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!"https".equalsIgnoreCase(scheme)) {
|
||||
securityLabel.setText("BLOCKED");
|
||||
return;
|
||||
}
|
||||
|
||||
String host = uri.getHost();
|
||||
if (host == null) {
|
||||
if (host == null || host.isBlank()) {
|
||||
securityLabel.setText("ERROR");
|
||||
return;
|
||||
}
|
||||
|
||||
// Secure Domain Check
|
||||
boolean trusted =
|
||||
TRUSTED_DOMAINS.stream()
|
||||
.anyMatch(domain -> host.equals(domain) || host.endsWith("." + domain));
|
||||
@@ -500,9 +696,11 @@ public class CasinoBrowserController {
|
||||
} else {
|
||||
securityLabel.setText("SAFE");
|
||||
}
|
||||
|
||||
engine.load(uri.toString());
|
||||
|
||||
} catch (Exception e) {
|
||||
securityLabel.setText("ERROR");
|
||||
securityLabel.setText("INVALID");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,7 +717,7 @@ public class CasinoBrowserController {
|
||||
alert.setHeaderText("This website is unknown");
|
||||
String content =
|
||||
host
|
||||
+ "\n\nThis site has not been verified by the Casono browser.\n"
|
||||
+ "\n\nThis site has not been verified by the Casono Browser.\n"
|
||||
+ "Do you still want to open it?";
|
||||
alert.setContentText(content);
|
||||
|
||||
@@ -545,4 +743,18 @@ public class CasinoBrowserController {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given URL points to a downloadable file based on its extension.
|
||||
*
|
||||
* @param url the URL to check
|
||||
* @return true if the URL is likely a download link, false otherwise
|
||||
*/
|
||||
private static boolean isDownloadUrl(String url) {
|
||||
if (url == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return url.matches(".*\\.(exe|zip|dmg|msi|apk|jar|pdf)(\\?.*)?$");
|
||||
}
|
||||
}
|
||||
|
||||
+66
-3
@@ -2,12 +2,15 @@ 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.Level;
|
||||
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.Pane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/**
|
||||
* Controller for displaying a player's status in the poker game, including their name, chip count
|
||||
@@ -21,11 +24,16 @@ public class PlayerStatusController {
|
||||
@FXML private Label playerMoney;
|
||||
@FXML private ImageView dealerIcon;
|
||||
@FXML private Pane parent;
|
||||
@FXML private VBox playerStatusBox;
|
||||
@FXML private HBox statusInnerBoxTop;
|
||||
@FXML private HBox statusInnerBoxBottom;
|
||||
|
||||
private Image dealerImage;
|
||||
private Player player;
|
||||
private static final String DEALER_IMAGE_PATH = "/images/chip-dealer-blue-3.png";
|
||||
private boolean turnHighlighted;
|
||||
private static final String DEALER_IMAGE_PATH = "/images/chip-dealer-blue-5.png";
|
||||
private static final double DEALER_ICON_X_FACTOR = 0.8;
|
||||
private static final String TURN_HIGHLIGHT_STYLE_CLASS = "player-status-active-turn";
|
||||
|
||||
/** Initialize the controller, load dealer image, and set up bindings. */
|
||||
@FXML
|
||||
@@ -42,8 +50,7 @@ public class PlayerStatusController {
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("Error: loading dealer image:");
|
||||
e.printStackTrace();
|
||||
LOGGER.log(Level.SEVERE, "Error loading dealer image", e);
|
||||
}
|
||||
|
||||
dealerIcon.layoutXProperty().bind(parent.widthProperty().multiply(DEALER_ICON_X_FACTOR));
|
||||
@@ -57,13 +64,20 @@ public class PlayerStatusController {
|
||||
*/
|
||||
public void setPlayer(Player player) {
|
||||
this.player = player;
|
||||
if (player == null) {
|
||||
turnHighlighted = false;
|
||||
}
|
||||
refresh();
|
||||
updateTurnHighlightStyle();
|
||||
}
|
||||
|
||||
/** Refresh UI safely */
|
||||
public void refresh() {
|
||||
|
||||
if (player == null) {
|
||||
playerName.setText("-");
|
||||
playerMoney.setText("0 $");
|
||||
dealerIcon.setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,6 +98,33 @@ public class PlayerStatusController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight or unhighlight this player slot as the currently active turn.
|
||||
*
|
||||
* @param highlighted true if this slot should be highlighted, false otherwise.
|
||||
*/
|
||||
public void setTurnHighlighted(boolean highlighted) {
|
||||
this.turnHighlighted = highlighted;
|
||||
updateTurnHighlightStyle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the controller is currently bound to the provided player.
|
||||
*
|
||||
* @param candidate the player to compare against.
|
||||
* @return true if both represent the same player.
|
||||
*/
|
||||
public boolean hasPlayer(Player candidate) {
|
||||
if (player == null
|
||||
|| candidate == null
|
||||
|| player.getId() == null
|
||||
|| candidate.getId() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return player.getId().equals(candidate.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* External quick update
|
||||
*
|
||||
@@ -107,4 +148,26 @@ public class PlayerStatusController {
|
||||
dealerIcon.setImage(dealerImage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the visual style of the player status box to indicate whether it's currently this
|
||||
* player's turn.
|
||||
*/
|
||||
private void updateTurnHighlightStyle() {
|
||||
if (statusInnerBoxTop == null || statusInnerBoxBottom == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (turnHighlighted) {
|
||||
if (!statusInnerBoxTop.getStyleClass().contains(TURN_HIGHLIGHT_STYLE_CLASS)) {
|
||||
statusInnerBoxTop.getStyleClass().add(TURN_HIGHLIGHT_STYLE_CLASS);
|
||||
}
|
||||
if (!statusInnerBoxBottom.getStyleClass().contains(TURN_HIGHLIGHT_STYLE_CLASS)) {
|
||||
statusInnerBoxBottom.getStyleClass().add(TURN_HIGHLIGHT_STYLE_CLASS);
|
||||
}
|
||||
} else {
|
||||
statusInnerBoxTop.getStyleClass().remove(TURN_HIGHLIGHT_STYLE_CLASS);
|
||||
statusInnerBoxBottom.getStyleClass().remove(TURN_HIGHLIGHT_STYLE_CLASS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+553
-65
@@ -8,11 +8,22 @@ 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.network.LobbyClient;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.UnaryOperator;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
@@ -59,8 +70,16 @@ public class TaskbarController {
|
||||
private static final String STYLE_RED_INPUT = "red-input-field";
|
||||
private static final double SCALE_NORMAL = 1.0;
|
||||
private static final int DEALER_OFFSET = 3;
|
||||
private static final String LOGO_PATH = "/images/logoinverted.png";
|
||||
private static final String LOGO_PATH_MAIN = "/images/logo.png";
|
||||
private static final double ALERT_LOGO_HEIGHT = 40.0;
|
||||
private boolean inputActionAllowed;
|
||||
private int lastReferenceBet = BIG_BLIND;
|
||||
private Consumer<String> lobbyActionAnnouncer;
|
||||
private String lastPhase;
|
||||
private static final int FIRST_PLAYER_MAX_BET = 3000;
|
||||
private static final int MAX_INPUT_LENGTH = 6;
|
||||
private static final double MAX_CHIP_PERCENT = 0.30;
|
||||
|
||||
/** Standard constructor. Used by FXML. */
|
||||
public TaskbarController() {
|
||||
@@ -213,6 +232,11 @@ public class TaskbarController {
|
||||
this.myPlayerId = myPlayerId;
|
||||
}
|
||||
|
||||
/** Sets an optional callback that publishes simple action labels to lobby chat. */
|
||||
public void setLobbyActionAnnouncer(Consumer<String> lobbyActionAnnouncer) {
|
||||
this.lobbyActionAnnouncer = lobbyActionAnnouncer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the taskbar controller. This method is called automatically after the FXML
|
||||
* components have been loaded.
|
||||
@@ -220,15 +244,102 @@ public class TaskbarController {
|
||||
@FXML
|
||||
public void initialize() {
|
||||
updateBasicButtons(false, false);
|
||||
|
||||
if (taskbarInput != null) {
|
||||
|
||||
UnaryOperator<TextFormatter.Change> filter =
|
||||
change -> {
|
||||
String newText = change.getControlNewText();
|
||||
|
||||
if (newText.length() > MAX_INPUT_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!newText.matches("\\d*")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return change;
|
||||
};
|
||||
|
||||
taskbarInput.setTextFormatter(new TextFormatter<>(filter));
|
||||
|
||||
taskbarInput
|
||||
.textProperty()
|
||||
.addListener((obs, oldValue, newValue) -> refreshBetInputUi());
|
||||
}
|
||||
|
||||
Tooltip callTooltip = new Tooltip();
|
||||
callButton.setTooltip(callTooltip);
|
||||
callButton.setOnMouseEntered(e -> callTooltip.setText(previewCall()));
|
||||
|
||||
Tooltip raiseTooltip = new Tooltip();
|
||||
raiseButton.setTooltip(raiseTooltip);
|
||||
raiseButton.setOnMouseEntered(e -> raiseTooltip.setText(previewRaise()));
|
||||
|
||||
setBetButtonVisible(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a preview string for the Call action, showing the amount that would be called based
|
||||
* on the current game state and the player's bet status.
|
||||
*
|
||||
* @return A string representing the Call action preview, including the amount that would be
|
||||
* called, or an empty string if the game state is not available.
|
||||
*/
|
||||
private String previewCall() {
|
||||
if (lastState == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
int callTarget = effectiveCallTarget(lastState);
|
||||
Player me = findCurrentPlayer(lastState);
|
||||
|
||||
int alreadyInvested = (me != null) ? Math.max(0, me.getBet()) : 0;
|
||||
int amount = Math.max(0, callTarget - alreadyInvested);
|
||||
|
||||
return "CALL " + amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a preview string for the Raise action, showing the amount that would be raised.
|
||||
*
|
||||
* @return A string representing the Raise action preview, including the amount that would be
|
||||
* raised.
|
||||
*/
|
||||
private String previewRaise() {
|
||||
if (lastState == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// int callTarget = effectiveCallTarget(lastState);
|
||||
// Player me = findCurrentPlayer(lastState);
|
||||
|
||||
// int alreadyInvested = (me != null) ? Math.max(0, me.getBet()) : 0;
|
||||
|
||||
// Integer targetBet = resolveTargetBet(ActionType.RAISE_BUTTON, lastState);
|
||||
// if (targetBet == null) {
|
||||
// return "";
|
||||
// }
|
||||
|
||||
// int totalContribution = Math.max(0, targetBet);
|
||||
|
||||
GameState state = ensureLatestStateForAction(ActionType.CALL_BUTTON.name().toLowerCase());
|
||||
|
||||
// int raiseBy = Math.max(0, targetBet);
|
||||
int raiseBy = effectiveCallTarget(state);
|
||||
|
||||
int callTarget = effectiveCallTarget(lastState);
|
||||
Player me = findCurrentPlayer(lastState);
|
||||
|
||||
int alreadyInvested = (me != null) ? Math.max(0, me.getBet()) : 0;
|
||||
int amount = Math.max(0, callTarget - alreadyInvested);
|
||||
|
||||
int raiseamout = amount + raiseBy;
|
||||
|
||||
return "RAISE +" + raiseBy + " TO " + raiseamout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the taskbar based on the current game state and the player's status.
|
||||
*
|
||||
@@ -248,6 +359,7 @@ public class TaskbarController {
|
||||
}
|
||||
|
||||
this.lastState = state;
|
||||
|
||||
if (state.currentBet > 0) {
|
||||
lastReferenceBet = state.currentBet;
|
||||
}
|
||||
@@ -269,8 +381,14 @@ public class TaskbarController {
|
||||
boolean isOut = me.getState() == PlayerState.FOLDED;
|
||||
boolean isGameFinished = isHandFinished(state);
|
||||
|
||||
boolean phaseChanged = lastPhase == null || !lastPhase.equalsIgnoreCase(state.phase);
|
||||
|
||||
lastPhase = state.phase;
|
||||
|
||||
updateBasicButtons(isMyTurn, isOut || isGameFinished);
|
||||
applyActionAvailability(state, me, isMyTurn, isOut || isGameFinished);
|
||||
|
||||
applyActionAvailability(state, me, isMyTurn, isOut || isGameFinished, phaseChanged);
|
||||
|
||||
setMoney(me.getChips());
|
||||
refreshBetInputUi();
|
||||
}
|
||||
@@ -287,7 +405,14 @@ public class TaskbarController {
|
||||
* if the game is finished, which disables actions.
|
||||
*/
|
||||
private void applyActionAvailability(
|
||||
GameState state, Player me, boolean isMyTurn, boolean isOutOrFinished) {
|
||||
GameState state,
|
||||
Player me,
|
||||
boolean isMyTurn,
|
||||
boolean isOutOrFinished,
|
||||
boolean phaseChanged) {
|
||||
|
||||
// boolean firstPlayerNewRound =
|
||||
// phaseChanged && isFirstPreflopPlayerInputOnly(state, me);
|
||||
inputActionAllowed = false;
|
||||
if (!isMyTurn || isOutOrFinished || state == null || me == null) {
|
||||
setBetButtonVisible(false);
|
||||
@@ -308,7 +433,17 @@ public class TaskbarController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFirstPreflopPlayerInputOnly(state, me)) {
|
||||
// if (isFirstPreflopPlayerInputOnly(state, me)) {
|
||||
// setActionEnabled(betButton, true);
|
||||
// setActionEnabled(callButton, false);
|
||||
// setActionEnabled(foldButton, false);
|
||||
// setActionEnabled(raiseButton, false);
|
||||
// activateInputField(taskbarInput);
|
||||
// inputActionAllowed = true;
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (isFirstPlayerOfPhase(state, me)) {
|
||||
setActionEnabled(betButton, true);
|
||||
setActionEnabled(callButton, false);
|
||||
setActionEnabled(foldButton, false);
|
||||
@@ -353,7 +488,16 @@ public class TaskbarController {
|
||||
if (value > Integer.MAX_VALUE / 2) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
return value * 2;
|
||||
|
||||
int callTarget = effectiveCallTarget(lastState);
|
||||
Player me = findCurrentPlayer(lastState);
|
||||
|
||||
int alreadyInvested = (me != null) ? Math.max(0, me.getBet()) : 0;
|
||||
int amount = Math.max(0, callTarget - alreadyInvested);
|
||||
|
||||
int raisevalue = amount + value;
|
||||
|
||||
return raisevalue;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -431,10 +575,22 @@ public class TaskbarController {
|
||||
* @return An integer representing the effective call target for the current game state.
|
||||
*/
|
||||
private int effectiveCallTarget(GameState state) {
|
||||
if (state != null && state.currentBet > 0) {
|
||||
return state.currentBet;
|
||||
int currentBet = (state != null) ? Math.max(0, state.currentBet) : 0;
|
||||
int rememberedBet = Math.max(0, lastReferenceBet);
|
||||
|
||||
if (state != null && isPreflop(state.phase) && isInitialPreflopBlindLayout(state)) {
|
||||
return currentBet;
|
||||
}
|
||||
return Math.max(0, lastReferenceBet);
|
||||
|
||||
if (currentBet <= 0) {
|
||||
return rememberedBet;
|
||||
}
|
||||
|
||||
if (rememberedBet <= 0) {
|
||||
return currentBet;
|
||||
}
|
||||
|
||||
return Math.max(currentBet, rememberedBet);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -473,6 +629,46 @@ public class TaskbarController {
|
||||
return isInitialPreflopBlindLayout(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current player is the first to act in the current phase of the game, based on
|
||||
* the dealer position and the active player index.
|
||||
*
|
||||
* @param state The current GameState object representing the state of the game, which includes
|
||||
* information about
|
||||
* @param me The Player object representing the current player, used to determine their
|
||||
* position.
|
||||
* @return A boolean value indicating whether the current player is the first to act in the
|
||||
* current phase of the game.
|
||||
*/
|
||||
private boolean isFirstPlayerOfPhase(GameState state, Player me) {
|
||||
|
||||
if (state == null || me == null || state.players == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int size = state.players.size();
|
||||
if (size < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int myIndex = state.players.indexOf(me);
|
||||
if (myIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int dealer = state.dealer;
|
||||
|
||||
int firstIndex;
|
||||
|
||||
if (isPreflop(state.phase)) {
|
||||
firstIndex = (size == 2) ? dealer : (dealer + DEALER_OFFSET) % size;
|
||||
} else {
|
||||
firstIndex = (dealer + 1) % size;
|
||||
}
|
||||
|
||||
return state.activePlayer == firstIndex && myIndex == firstIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the initial blind layout is still in place during the pre-flop phase.
|
||||
*
|
||||
@@ -482,6 +678,10 @@ public class TaskbarController {
|
||||
* the pre-flop phase.
|
||||
*/
|
||||
private boolean isInitialPreflopBlindLayout(GameState state) {
|
||||
if (state == null || state.players == null || state.players.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int sbCount = 0;
|
||||
int bbCount = 0;
|
||||
int zeroCount = 0;
|
||||
@@ -618,57 +818,31 @@ public class TaskbarController {
|
||||
}
|
||||
|
||||
GameState state = ensureLatestStateForAction(actionType.name().toLowerCase());
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isHandFinished(state)) {
|
||||
LOGGER.info("Action {} ignored: hand is already finished", actionType);
|
||||
update(state, myPlayerId);
|
||||
if (state == null || isHandFinished(state)) {
|
||||
if (state != null) {
|
||||
LOGGER.info("Action {} ignored: hand finished", actionType);
|
||||
update(state, myPlayerId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Player me = findCurrentPlayer(state);
|
||||
if (me == null) {
|
||||
LOGGER.error("Action {} blocked: player missing in state", actionType);
|
||||
LOGGER.error("Action {} blocked: player missing", actionType);
|
||||
return;
|
||||
}
|
||||
|
||||
int callTarget = effectiveCallTarget(state);
|
||||
int targetBet;
|
||||
if (actionType == ActionType.CALL_BUTTON) {
|
||||
targetBet = callTarget;
|
||||
} else if (actionType == ActionType.RAISE_BUTTON) {
|
||||
targetBet = safeDouble(callTarget);
|
||||
} else {
|
||||
Integer fromInput = parseInputTarget(taskbarInput.getText());
|
||||
if (fromInput == null) {
|
||||
return;
|
||||
}
|
||||
targetBet = fromInput;
|
||||
Integer targetBet = resolveTargetBet(actionType, state);
|
||||
if (targetBet == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ValidationResult validation = validateTarget(state, me, targetBet);
|
||||
if (!validation.valid) {
|
||||
refreshBetInputUi();
|
||||
if (!handleValidation(validation, actionType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (validation.warning) {
|
||||
showWarningDialog("WARNING", validation.message);
|
||||
}
|
||||
|
||||
if (state.currentBet <= 0) {
|
||||
gameService.bet(targetBet);
|
||||
LOGGER.info("Player BET (target={})", targetBet);
|
||||
} else if (targetBet == callTarget) {
|
||||
gameService.call();
|
||||
LOGGER.info("Player CALL (target={})", targetBet);
|
||||
} else {
|
||||
int raiseBy = Math.max(0, targetBet - state.currentBet);
|
||||
gameService.raise(raiseBy);
|
||||
LOGGER.info("Player RAISE by {} (target={})", raiseBy, targetBet);
|
||||
}
|
||||
executeAction(state, targetBet, actionType);
|
||||
|
||||
if (targetBet > 0) {
|
||||
lastReferenceBet = targetBet;
|
||||
@@ -678,6 +852,148 @@ public class TaskbarController {
|
||||
refreshGame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the target bet amount based on the type of action being submitted and the current
|
||||
* game state.
|
||||
*
|
||||
* @param actionType The type of action being submitted, which determines how the target bet is
|
||||
* calculated.
|
||||
* @param state The current GameState object representing the state of the game, which is used
|
||||
* to determine the effective call target and to validate the input for the bet amount.
|
||||
* @return An Integer representing the resolved target bet amount for the action being
|
||||
* submitted, or null if the input is invalid or cannot be resolved based on the action type
|
||||
* and game state.
|
||||
*/
|
||||
private Integer resolveTargetBet(ActionType actionType, GameState state) {
|
||||
int callTarget = effectiveCallTarget(state);
|
||||
|
||||
if (actionType == ActionType.CALL_BUTTON) {
|
||||
return callTarget;
|
||||
}
|
||||
|
||||
if (actionType == ActionType.RAISE_BUTTON) {
|
||||
Integer input = parseInputTarget(taskbarInput.getText());
|
||||
return input != null ? input : callTarget + BIG_BLIND;
|
||||
}
|
||||
|
||||
return parseInputTarget(taskbarInput.getText());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the validation result of a proposed action, including displaying warnings and
|
||||
* refreshing the UI if the action is blocked or cancelled.
|
||||
*
|
||||
* @param validation The ValidationResult object representing the outcome of validating the
|
||||
* proposed action, which includes whether the action is valid, if it triggers a warning,
|
||||
* and any associated messages.
|
||||
* @param actionType The type of action being processed, used for logging purposes to indicate
|
||||
* which action is being validated and potentially blocked or cancelled.
|
||||
* @return A boolean value indicating whether the action is valid and can proceed (true) or if
|
||||
* it is blocked or cancelled due to validation failure or user cancellation after a warning
|
||||
* (false).
|
||||
*/
|
||||
private boolean handleValidation(ValidationResult validation, ActionType actionType) {
|
||||
if (!validation.valid) {
|
||||
refreshBetInputUi();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (validation.warning) {
|
||||
boolean confirmed = showWarningDialog("WARNING", validation.message);
|
||||
if (!confirmed) {
|
||||
LOGGER.info("Action {} cancelled after warning", actionType);
|
||||
refreshBetInputUi();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the player action by sending the appropriate command to the GameService based on the
|
||||
* target bet and the current game state.
|
||||
*
|
||||
* @param state The current GameState object representing the state of the game, which includes
|
||||
* information about the current bet and the player's status, used to determine how to
|
||||
* execute the action based on the target bet.
|
||||
* @param targetBet The integer value representing the target bet amount for the action being
|
||||
* executed, which is used to determine
|
||||
*/
|
||||
private void executeAction(GameState state, int targetBet, ActionType actionType) {
|
||||
int callTarget = effectiveCallTarget(state);
|
||||
String phase = (state != null ? state.phase : "UNKNOWN");
|
||||
|
||||
int alreadyInvested =
|
||||
(findCurrentPlayer(state) != null)
|
||||
? Math.max(0, findCurrentPlayer(state).getBet())
|
||||
: 0;
|
||||
|
||||
if (actionType == ActionType.CALL_BUTTON) {
|
||||
|
||||
int amount = Math.max(0, callTarget - alreadyInvested);
|
||||
|
||||
gameService.call();
|
||||
|
||||
LOGGER.info("[{}] CALL +{} (total to {})", phase, amount, callTarget);
|
||||
announceLobbyAction("CALL " + amount);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (actionType == ActionType.RAISE_BUTTON) {
|
||||
|
||||
int raiseBy = Math.max(0, targetBet - callTarget);
|
||||
|
||||
gameService.raise(raiseBy);
|
||||
|
||||
LOGGER.info("[{}] RAISE +{} (target={})", phase, raiseBy, targetBet);
|
||||
announceLobbyAction("RAISE +" + raiseBy + " TO " + targetBet);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.currentBet <= 0) {
|
||||
|
||||
gameService.bet(targetBet);
|
||||
|
||||
LOGGER.info("[{}] BET {}", phase, targetBet);
|
||||
announceLobbyAction("BET " + targetBet);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetBet == callTarget) {
|
||||
|
||||
int amount = Math.max(0, callTarget - alreadyInvested);
|
||||
|
||||
gameService.call();
|
||||
|
||||
LOGGER.info("[{}] CALL +{} (total to {})", phase, amount, callTarget);
|
||||
announceLobbyAction("CALL " + amount);
|
||||
|
||||
} else {
|
||||
|
||||
int raiseBy = Math.max(0, targetBet - callTarget);
|
||||
|
||||
gameService.raise(raiseBy);
|
||||
|
||||
LOGGER.info("[{}] RAISE +{} (target={})", phase, raiseBy, targetBet);
|
||||
announceLobbyAction("RAISE +" + raiseBy + " TO " + targetBet);
|
||||
}
|
||||
}
|
||||
|
||||
/** Publishes a simple action label to the lobby chat, if a publisher is configured. */
|
||||
private void announceLobbyAction(String actionLabel) {
|
||||
if (lobbyActionAnnouncer == null || actionLabel == null || actionLabel.isBlank()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
lobbyActionAnnouncer.accept(actionLabel.trim());
|
||||
} catch (RuntimeException ex) {
|
||||
LOGGER.warn("Could not publish lobby action '{}': {}", actionLabel, ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the latest game state is available for processing a player action. If the last
|
||||
* known state is null, it attempts to fetch the current state from the GameService.
|
||||
@@ -691,8 +1007,15 @@ public class TaskbarController {
|
||||
if (text == null || text.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Integer.parseInt(text.trim());
|
||||
int value = Integer.parseInt(text.trim());
|
||||
|
||||
if (value % SMALL_BLIND != 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value;
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
@@ -737,21 +1060,44 @@ public class TaskbarController {
|
||||
* triggers a warning, or if it is blocked due to being invalid or too risky.
|
||||
*/
|
||||
private ValidationResult validateTarget(GameState state, Player me, int targetBet) {
|
||||
|
||||
if (state == null || me == null) {
|
||||
return ValidationResult.blocked("Invalid game state");
|
||||
}
|
||||
|
||||
int callTarget = effectiveCallTarget(state);
|
||||
int raiseTarget = safeDouble(callTarget);
|
||||
if (targetBet != callTarget && targetBet != raiseTarget) {
|
||||
return ValidationResult.blocked("Only calls or exactly two raises allowed");
|
||||
final int callTarget = effectiveCallTarget(state);
|
||||
|
||||
final int alreadyInvested = Math.max(0, me.getBet());
|
||||
final int chips = Math.max(0, me.getChips());
|
||||
|
||||
final int required = Math.max(0, targetBet - alreadyInvested);
|
||||
|
||||
final int minRequiredToCall = Math.max(0, callTarget - alreadyInvested);
|
||||
|
||||
if (required < minRequiredToCall) {
|
||||
return ValidationResult.blocked("Bet must match at least the call amount");
|
||||
}
|
||||
|
||||
int alreadyInvested = Math.max(0, me.getBet());
|
||||
int required = Math.max(0, targetBet - alreadyInvested);
|
||||
int chips = Math.max(0, me.getChips());
|
||||
if (isFirstPlayerOfPhase(state, me)) {
|
||||
if (required > FIRST_PLAYER_MAX_BET) {
|
||||
return ValidationResult.blocked(
|
||||
"First player cannot bet more than " + FIRST_PLAYER_MAX_BET);
|
||||
}
|
||||
}
|
||||
|
||||
if (targetBet == callTarget && required == 0) {
|
||||
if (isPreflop(state.phase)) {
|
||||
int maxAllowed = (int) Math.floor(me.getChips() * MAX_CHIP_PERCENT);
|
||||
|
||||
if (required > maxAllowed) {
|
||||
return ValidationResult.blocked(
|
||||
"Preflop: You can only bet up to 30% of your stack (" + maxAllowed + ")");
|
||||
}
|
||||
}
|
||||
|
||||
if (required == minRequiredToCall) {
|
||||
if (required > chips) {
|
||||
return ValidationResult.blocked("Not enough chips to call");
|
||||
}
|
||||
return ValidationResult.ok();
|
||||
}
|
||||
|
||||
@@ -760,16 +1106,19 @@ public class TaskbarController {
|
||||
}
|
||||
|
||||
if (required > chips) {
|
||||
return ValidationResult.blocked("Not enough chips for the next bet");
|
||||
return ValidationResult.blocked("Not enough chips for this bet");
|
||||
}
|
||||
|
||||
BetRisk risk = evaluateBetRisk(state, required, chips);
|
||||
|
||||
if (risk == BetRisk.BLOCKED) {
|
||||
return ValidationResult.blocked("Assignment for this phase is blocked");
|
||||
return ValidationResult.blocked("Bet too risky for this phase");
|
||||
}
|
||||
|
||||
if (risk == BetRisk.WARNING) {
|
||||
return ValidationResult.warning(warningTextForPhase(state));
|
||||
}
|
||||
|
||||
return ValidationResult.ok();
|
||||
}
|
||||
|
||||
@@ -849,12 +1198,50 @@ public class TaskbarController {
|
||||
* @param content The string representing the content of the warning message, which is displayed
|
||||
* in the body of the dialog.
|
||||
*/
|
||||
private void showWarningDialog(String title, String content) {
|
||||
private boolean showWarningDialog(String title, String content) {
|
||||
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(content);
|
||||
alert.showAndWait();
|
||||
ButtonType proceedButton = new ButtonType("CONTINUE");
|
||||
ButtonType cancelButton = new ButtonType("CANCEL", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
alert.getButtonTypes().setAll(proceedButton, cancelButton);
|
||||
applyAlertBranding(alert);
|
||||
Optional<ButtonType> result = alert.showAndWait();
|
||||
return result.isPresent() && result.get() == proceedButton;
|
||||
}
|
||||
|
||||
/** Applies Casono icon and logo to alert dialogs when resources are available. */
|
||||
private void applyAlertBranding(Alert alert) {
|
||||
if (alert == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (var iconStream = TaskbarController.class.getResourceAsStream(LOGO_PATH);
|
||||
var mainLogoStream = TaskbarController.class.getResourceAsStream(LOGO_PATH_MAIN)) {
|
||||
if (iconStream != null) {
|
||||
Image icon = new Image(iconStream);
|
||||
if (!icon.isError()
|
||||
&& alert.getDialogPane() != null
|
||||
&& alert.getDialogPane().getScene() != null
|
||||
&& alert.getDialogPane().getScene().getWindow()
|
||||
instanceof javafx.stage.Stage stage) {
|
||||
stage.getIcons().add(icon);
|
||||
}
|
||||
}
|
||||
|
||||
if (mainLogoStream != null) {
|
||||
Image mainLogo = new Image(mainLogoStream);
|
||||
if (!mainLogo.isError()) {
|
||||
ImageView logoView = new ImageView(mainLogo);
|
||||
logoView.setFitHeight(ALERT_LOGO_HEIGHT);
|
||||
logoView.setPreserveRatio(true);
|
||||
alert.setGraphic(logoView);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Could not load alert logo resources: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -865,6 +1252,9 @@ public class TaskbarController {
|
||||
*/
|
||||
@FXML
|
||||
private void onTaskbarPressed(MouseEvent event) {
|
||||
if (taskbar == null || event == null) {
|
||||
return;
|
||||
}
|
||||
xOffset = event.getSceneX() - taskbar.getLayoutX();
|
||||
yOffset = event.getSceneY() - taskbar.getLayoutY();
|
||||
}
|
||||
@@ -873,17 +1263,25 @@ public class TaskbarController {
|
||||
* Called while dragging the taskbar with the mouse. Updates the position and slightly scales
|
||||
* the taskbar for visual feedback.
|
||||
*
|
||||
* <p>TODO: It still needs to be fixed that the taskbar cannot disappear out of the window.
|
||||
*
|
||||
* @param event Das Mausereignis
|
||||
*/
|
||||
@FXML
|
||||
private void onTaskbarDragged(MouseEvent event) {
|
||||
taskbar.setLayoutX(event.getSceneX() - xOffset);
|
||||
taskbar.setLayoutY(event.getSceneY() - yOffset);
|
||||
if (taskbar == null || event == null || taskbar.getScene() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
taskbar.setScaleX(TASKBAR_SCALE);
|
||||
taskbar.setScaleY(TASKBAR_SCALE);
|
||||
|
||||
double targetX = event.getSceneX() - xOffset;
|
||||
double targetY = event.getSceneY() - yOffset;
|
||||
|
||||
double maxX = Math.max(0, taskbar.getScene().getWidth() - scaledNodeWidth());
|
||||
double maxY = Math.max(0, taskbar.getScene().getHeight() - scaledNodeHeight());
|
||||
|
||||
taskbar.setLayoutX(clamp(targetX, 0, maxX));
|
||||
taskbar.setLayoutY(clamp(targetY, 0, maxY));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -894,8 +1292,62 @@ public class TaskbarController {
|
||||
*/
|
||||
@FXML
|
||||
private void onTaskbarReleased(MouseEvent event) {
|
||||
if (taskbar == null) {
|
||||
return;
|
||||
}
|
||||
taskbar.setScaleX(SCALE_NORMAL);
|
||||
taskbar.setScaleY(SCALE_NORMAL);
|
||||
|
||||
if (taskbar.getScene() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
double maxX = Math.max(0, taskbar.getScene().getWidth() - scaledNodeWidth());
|
||||
double maxY = Math.max(0, taskbar.getScene().getHeight() - scaledNodeHeight());
|
||||
|
||||
taskbar.setLayoutX(clamp(taskbar.getLayoutX(), 0, maxX));
|
||||
taskbar.setLayoutY(clamp(taskbar.getLayoutY(), 0, maxY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the scaled width of the taskbar node based on its current bounds and scale factor.
|
||||
*
|
||||
* @return The scaled width of the taskbar node, ensuring it is non-negative and accounts for
|
||||
* the current scale applied to the node.
|
||||
*/
|
||||
private double scaledNodeWidth() {
|
||||
double width = taskbar.getBoundsInLocal().getWidth();
|
||||
if (width <= 0) {
|
||||
width = taskbar.prefWidth(-1);
|
||||
}
|
||||
return Math.max(0, width * taskbar.getScaleX());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the scaled height of the taskbar node based on its current bounds and scale
|
||||
* factor.
|
||||
*
|
||||
* @return The scaled height of the taskbar node, ensuring it is non-negative and accounts for
|
||||
* the current scale applied to the node.
|
||||
*/
|
||||
private double scaledNodeHeight() {
|
||||
double height = taskbar.getBoundsInLocal().getHeight();
|
||||
if (height <= 0) {
|
||||
height = taskbar.prefHeight(-1);
|
||||
}
|
||||
return Math.max(0, height * taskbar.getScaleY());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamps a value between a minimum and maximum bound.
|
||||
*
|
||||
* @param value the value to clamp
|
||||
* @param min the lower bound
|
||||
* @param max the upper bound
|
||||
* @return the clamped value in the range [min, max]
|
||||
*/
|
||||
private double clamp(double value, double min, double max) {
|
||||
return Math.max(min, Math.min(value, max));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -928,7 +1380,7 @@ public class TaskbarController {
|
||||
/** Called when the Call button is clicked. */
|
||||
@FXML
|
||||
private void onInputPlayerCall() {
|
||||
submitPresetInputAndProcess("call");
|
||||
submitAction(ActionType.CALL_BUTTON);
|
||||
}
|
||||
|
||||
/** Called when the Fold button is clicked. */
|
||||
@@ -949,6 +1401,7 @@ public class TaskbarController {
|
||||
|
||||
gameService.fold();
|
||||
LOGGER.info("Player FOLD");
|
||||
announceLobbyAction("FOLD");
|
||||
|
||||
refreshGame();
|
||||
}
|
||||
@@ -1062,6 +1515,9 @@ public class TaskbarController {
|
||||
GameState refreshed = gameService.refresh();
|
||||
if (refreshed != null) {
|
||||
lastState = refreshed;
|
||||
if (refreshed.currentBet > 0 && refreshed.currentBet >= lastReferenceBet) {
|
||||
lastReferenceBet = refreshed.currentBet;
|
||||
}
|
||||
return refreshed;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -1074,12 +1530,24 @@ public class TaskbarController {
|
||||
/**
|
||||
* Opens the integrated Casono web browser.
|
||||
*
|
||||
* <p>TODO: Replace the start URL with the official project website (e.g., Tips & Tricks page)
|
||||
* once the content for strategies and support is available.
|
||||
* <p>once the content for strategies and support is available.
|
||||
*/
|
||||
@FXML
|
||||
private void onBrowserButtonClick() {
|
||||
CasinoBrowserController.open("wikipedia.org");
|
||||
try {
|
||||
Path path =
|
||||
Paths.get(
|
||||
System.getProperty("user.dir"),
|
||||
"documents",
|
||||
"docs",
|
||||
"game-engine",
|
||||
"manual.html");
|
||||
|
||||
CasinoBrowserController.open(path.toUri().toString());
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/** Opens the highscore popup window from the taskbar. */
|
||||
@@ -1092,6 +1560,7 @@ public class TaskbarController {
|
||||
alert.setTitle("Info");
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText("No active server connection for highscores.");
|
||||
applyAlertBranding(alert);
|
||||
alert.showAndWait();
|
||||
return;
|
||||
}
|
||||
@@ -1124,4 +1593,23 @@ public class TaskbarController {
|
||||
LOGGER.error("Could not open highscore window from taskbar: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight or unhighlight the taskbar when it is the player's turn.
|
||||
*
|
||||
* @param highlighted true to highlight, false to remove highlight
|
||||
*/
|
||||
public void setTurnHighlighted(boolean highlighted) {
|
||||
if (taskbar == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (highlighted) {
|
||||
if (!taskbar.getStyleClass().contains("player-active-turn")) {
|
||||
taskbar.getStyleClass().add("player-active-turn");
|
||||
}
|
||||
} else {
|
||||
taskbar.getStyleClass().remove("player-active-turn");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
-7
@@ -10,9 +10,16 @@ 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.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/** Handler for GET_GAME_STATE: returns pot, phase, community cards and per-player info. */
|
||||
public class GetGameStateHandler extends CommandHandler<GetGameStateRequest> {
|
||||
private static final Logger LOGGER = Logger.getLogger(GetGameStateHandler.class.getName());
|
||||
private static final int FINISHED_CLEANUP_DELAY_SECONDS = 2;
|
||||
|
||||
private final LobbyManager lobbyManager;
|
||||
private final UserRegistry userRegistry;
|
||||
|
||||
@@ -73,11 +80,12 @@ public class GetGameStateHandler extends CommandHandler<GetGameStateRequest> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (game.getState().getPhase() == GamePhase.FINISHED) {
|
||||
cleanupLobby(lobby, lobbyId);
|
||||
}
|
||||
|
||||
responseDispatcher.dispatch(new GetGameStateResponse(request.getContext(), game, username));
|
||||
|
||||
if (game.getState().getPhase() == GamePhase.FINISHED) {
|
||||
CompletableFuture.delayedExecutor(FINISHED_CLEANUP_DELAY_SECONDS, TimeUnit.SECONDS)
|
||||
.execute(() -> cleanupLobby(lobby, lobbyId));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,15 +93,20 @@ public class GetGameStateHandler extends CommandHandler<GetGameStateRequest> {
|
||||
* when the game reaches FINISHED phase.
|
||||
*/
|
||||
private void cleanupLobby(Lobby lobby, LobbyId lobbyId) {
|
||||
if (lobby == null || lobbyId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Remove all players from the lobby
|
||||
for (String playerName : lobby.getPlayerNames()) {
|
||||
lobbyManager.removePlayer(playerName);
|
||||
}
|
||||
// Reset the game controller so the lobby returns to CREATED state
|
||||
lobby.initGame(null);
|
||||
} catch (RuntimeException e) {
|
||||
// Log silently to avoid disrupting game state response
|
||||
LOGGER.log(
|
||||
Level.WARNING,
|
||||
"Failed to cleanup lobby " + lobbyId + " during FINISHED state",
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -69,6 +69,10 @@ public class GetGameStateResponse extends SuccessResponse {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.tryMarkWinnerPersistedForHand()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Player winner = findPlayerByIndex(state, winnerIndex);
|
||||
if (winner == null || winner.getId() == null || winner.getId().value() == null) {
|
||||
return;
|
||||
|
||||
@@ -35,6 +35,7 @@ public class GameState {
|
||||
private int currentPlayerIndex;
|
||||
private boolean handActive;
|
||||
private boolean allowOutOfTurn;
|
||||
private boolean winnerPersistedForHand;
|
||||
private GamePhase phase;
|
||||
private int dealerIndex;
|
||||
|
||||
@@ -551,6 +552,7 @@ public class GameState {
|
||||
*/
|
||||
public void startNewHand() {
|
||||
handActive = true;
|
||||
resetWinnerPersistedForHand();
|
||||
phase = GamePhase.PREFLOP;
|
||||
|
||||
pot = new Pot();
|
||||
@@ -575,6 +577,24 @@ public class GameState {
|
||||
setCurrentPlayerToPreflopFirstToAct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the winner as already persisted for the current hand.
|
||||
*
|
||||
* @return true exactly once per hand; false on all subsequent calls.
|
||||
*/
|
||||
public synchronized boolean tryMarkWinnerPersistedForHand() {
|
||||
if (winnerPersistedForHand) {
|
||||
return false;
|
||||
}
|
||||
winnerPersistedForHand = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Resets the winner persistence guard for a new hand. */
|
||||
public synchronized void resetWinnerPersistedForHand() {
|
||||
winnerPersistedForHand = false;
|
||||
}
|
||||
|
||||
public void foldPlayer(PlayerId playerId) {
|
||||
getPlayer(playerId).setFolded(true);
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 645 KiB After Width: | Height: | Size: 469 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 658 KiB |
@@ -48,7 +48,7 @@
|
||||
}
|
||||
|
||||
.taskbar {
|
||||
-fx-background-color: rgba(13, 158, 59);
|
||||
-fx-background-color: rgb(13, 158, 59);
|
||||
-fx-background-radius: 23;
|
||||
-fx-border-radius: 20;
|
||||
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||
@@ -56,13 +56,17 @@
|
||||
-fx-padding: 10 25;
|
||||
-fx-cursor: move;
|
||||
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.taskbar:hover {
|
||||
-fx-border-color: #ffffff;
|
||||
}
|
||||
|
||||
.taskbar.player-active-turn {
|
||||
/* Active turn should only change the shadow to avoid flicker/transparency jumps. */
|
||||
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
|
||||
}
|
||||
|
||||
.black-input-field {
|
||||
-fx-background-color: #1a1a1a;
|
||||
-fx-text-fill: #ffffff;
|
||||
@@ -229,6 +233,13 @@
|
||||
-fx-opacity: 1;
|
||||
}
|
||||
|
||||
.player-card.player-cards-active-turn {
|
||||
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
|
||||
}
|
||||
|
||||
.dealer-box.player-cards-active-turn {
|
||||
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
|
||||
}
|
||||
|
||||
.chips-icon {
|
||||
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||
@@ -243,6 +254,10 @@
|
||||
-fx-padding: 10;
|
||||
}
|
||||
|
||||
/*.player-cards-box.player-cards-active-turn {*/
|
||||
/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/
|
||||
/*}*/
|
||||
|
||||
.money-value {
|
||||
-fx-text-fill: #ffffff;
|
||||
-fx-font-size: 18px;
|
||||
@@ -297,6 +312,14 @@
|
||||
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||
}
|
||||
|
||||
.status-inner-box-top.player-status-active-turn {
|
||||
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
|
||||
}
|
||||
|
||||
.status-inner-box-bottom.player-status-active-turn {
|
||||
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
|
||||
}
|
||||
|
||||
.status-label-small {
|
||||
-fx-text-fill: #ffffff;
|
||||
-fx-font-size: 18px;
|
||||
@@ -369,3 +392,24 @@
|
||||
-fx-font-weight: bold;
|
||||
-fx-font-size: 16px;
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
-fx-background-color: #333333;
|
||||
-fx-text-fill: #ffffff;
|
||||
-fx-font-family: "Courier New";
|
||||
-fx-font-weight: bold;
|
||||
-fx-background-radius: 8;
|
||||
-fx-border-radius: 8;
|
||||
-fx-border-color: #333333;
|
||||
-fx-color-color: #cccccc;
|
||||
-fx-border-width: 2;
|
||||
-fx-padding: 5 12;
|
||||
}
|
||||
|
||||
.context-menu .menu-item {
|
||||
-fx-background-color: #333333;
|
||||
}
|
||||
|
||||
.context-menu .menu-item:focused {
|
||||
-fx-background-color: #333333;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
</Label>
|
||||
|
||||
<Label fx:id="tableText"
|
||||
text="Place your bet"
|
||||
text="Welcome! The round is about to begin."
|
||||
styleClass="table-title" />
|
||||
|
||||
</VBox>
|
||||
@@ -93,6 +93,7 @@
|
||||
|
||||
<!-- Player Cards -->
|
||||
<HBox fx:id="playerCardsBox"
|
||||
styleClass="player-cards-box"
|
||||
spacing="10"
|
||||
alignment="CENTER"
|
||||
StackPane.alignment="BOTTOM_CENTER">
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
<?import javafx.geometry.Insets?>
|
||||
|
||||
<!--
|
||||
TODO: Main placeholder for player status & input.
|
||||
|
||||
This layout currently serves as a visual structure. As soon as the game engine and
|
||||
server requests (API/requests) can be processed, the name, account balance
|
||||
and status displays will be dynamically populated with real-time data from the server.
|
||||
@@ -24,7 +22,8 @@ and status displays will be dynamically populated with real-time data from the s
|
||||
<children>
|
||||
|
||||
<!-- NAME -->
|
||||
<HBox styleClass="status-inner-box-top"
|
||||
<HBox fx:id="statusInnerBoxTop"
|
||||
styleClass="status-inner-box-top"
|
||||
alignment="CENTER_LEFT"
|
||||
prefHeight="40">
|
||||
|
||||
@@ -40,7 +39,8 @@ and status displays will be dynamically populated with real-time data from the s
|
||||
<HBox prefHeight="5"/>
|
||||
|
||||
<!-- MONEY -->
|
||||
<HBox styleClass="status-inner-box-bottom"
|
||||
<HBox fx:id="statusInnerBoxBottom"
|
||||
styleClass="status-inner-box-bottom"
|
||||
alignment="CENTER_LEFT"
|
||||
prefHeight="40">
|
||||
|
||||
|
||||
@@ -39,19 +39,19 @@
|
||||
styleClass="yellow-button"/>
|
||||
|
||||
<!-- Action buttons: Trigger Call -->
|
||||
<Button text="Call"
|
||||
<Button text="CALL"
|
||||
fx:id="callButton"
|
||||
onAction="#onInputPlayerCall"
|
||||
styleClass="yellow-button"/>
|
||||
|
||||
<!-- Action buttons: Trigger Fold -->
|
||||
<Button text="Fold"
|
||||
<Button text="FOLD"
|
||||
fx:id="foldButton"
|
||||
onAction="#onInputPlayerFold"
|
||||
styleClass="yellow-button"/>
|
||||
|
||||
<!-- Action buttons: Trigger Raise -->
|
||||
<Button text="Raise"
|
||||
<Button text="RAISE"
|
||||
fx:id="raiseButton"
|
||||
onAction="#onInputPlayerRaise"
|
||||
styleClass="yellow-button"/>
|
||||
|
||||
Reference in New Issue
Block a user