Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce6fc1bf57 | |||
| 05b3324cbd | |||
| d6ad60d6a7 | |||
| ddb3e221ac | |||
| 52ad32a100 | |||
| 822d9a0e1e | |||
| 30927d471e | |||
| 86ab82ebfd | |||
| 6a06369d94 | |||
| 1107542c54 | |||
| 2ad99d8732 | |||
| d791e9b2d1 | |||
| 3cd1e6925e | |||
| a7e4411cd0 | |||
| bf979ab391 | |||
| fb68ecc777 | |||
| 8ca41c5ab4 | |||
| 6422e6c924 | |||
| 404bf6379c | |||
| d46bd8de7e | |||
| c55a9eace6 | |||
| b3eaa64cd7 | |||
| ab4f60a74f | |||
| 4f9e9a0e9a | |||
| c31f0048ee | |||
| 0f5ddfda0c | |||
| 09f8ddf123 | |||
| c11201479b | |||
| 5ded42abe6 | |||
| 572b66889d | |||
| 89fd6aef05 | |||
| 6e640f7fde | |||
| 7a6c876017 | |||
| 22d742d614 | |||
| 71d48d148b | |||
| 5c0f3a7c86 | |||
| 2190fccd8f | |||
| 4bfb59f234 | |||
| d49f18c3aa | |||
| d4a3ebb487 | |||
| 3cabe06fb0 | |||
| 9023263b01 | |||
| c04d4fca76 | |||
| d3d72bd5b4 | |||
| 4a3c885b68 | |||
| 571c616ce9 | |||
| 01ecbb0497 | |||
| 5e91cefabb | |||
| 0f4d048b76 | |||
| b1d099f102 | |||
| 40e558be18 | |||
| 277b776fec | |||
| 88b797ba1d | |||
| 07c00275fd | |||
| 1d4cf2d846 | |||
| c2d0af03b7 | |||
| 48476a67f3 | |||
| 6471749133 | |||
| 826e58644a | |||
| 67a0f237e6 | |||
| 2a03571cd1 | |||
| 7eafaba65a | |||
| 69fc00a3b2 | |||
| ca65655443 | |||
| 488e8f524b | |||
| 9488ef41c3 | |||
| 8bb0bfaf09 | |||
| 33998e4a88 | |||
| d094df16da | |||
| b9e45142c2 | |||
| 16c91c6aae | |||
| ea5f6a1083 | |||
| c5f37392ef | |||
| 66ee8bec46 | |||
| 36348d0d93 | |||
| 90b2f3bb3a | |||
| 1cddcf6657 | |||
| e358c930c5 | |||
| 95fc53ec61 | |||
| 989dc95cef | |||
| 2c343c72b3 | |||
| ffbfd2dee4 | |||
| 97808fe26d | |||
| 5b37dffa4c | |||
| 838ada39d5 | |||
| 4e675677c0 | |||
| a629136fcb | |||
| 30067bd69b |
@@ -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>
|
||||
@@ -11,7 +11,7 @@ import org.apache.logging.log4j.Logger;
|
||||
* Entry point and bootstrap helper for the Casono client application.
|
||||
*
|
||||
* <p>Responsibilities: - Parse CLI args (host:port, optional username) - Store username centrally
|
||||
* so UI can reuse it - Create a shared ClientService and do startup LOGIN (optional)
|
||||
* so UI can reuse it - Create a shared ClientService and do startup LOGIN
|
||||
*/
|
||||
public class ClientApp {
|
||||
|
||||
@@ -40,7 +40,14 @@ public class ClientApp {
|
||||
}
|
||||
|
||||
public static void updateSharedUsername(String username) {
|
||||
setSharedUsername(username != null && !username.isBlank() ? username.trim() : null);
|
||||
setSharedUsername(normalizeUsername(username));
|
||||
}
|
||||
|
||||
private static String normalizeUsername(String username) {
|
||||
if (username == null || username.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return username.trim();
|
||||
}
|
||||
|
||||
private static void setSharedUsername(String username) {
|
||||
@@ -52,15 +59,14 @@ public class ClientApp {
|
||||
String[] hostPort = parseHostAndPort(arg);
|
||||
String host = hostPort[0];
|
||||
int port = Integer.parseInt(hostPort[1]);
|
||||
String normalizedUsername = normalizeUsername(username);
|
||||
|
||||
configureServerProperties(host, port);
|
||||
setSharedUsername(username != null && !username.isBlank() ? username.trim() : null);
|
||||
setSharedUsername(normalizedUsername);
|
||||
|
||||
if (getSharedUsername() != null) {
|
||||
performStartupLogin(host, port, username);
|
||||
}
|
||||
performStartupLogin(host, port, normalizedUsername);
|
||||
|
||||
launchUI(arg, username);
|
||||
launchUI(arg);
|
||||
}
|
||||
|
||||
private static String[] parseHostAndPort(String arg) {
|
||||
@@ -81,52 +87,33 @@ public class ClientApp {
|
||||
try {
|
||||
ClientService clientService = new ClientService(host, port);
|
||||
setSharedClientService(clientService);
|
||||
LobbyClient lobbyClient = new LobbyClient(clientService);
|
||||
LoginResult res = lobbyClient.login(username);
|
||||
|
||||
java.util.concurrent.ExecutorService bg =
|
||||
java.util.concurrent.Executors.newSingleThreadExecutor(
|
||||
r -> {
|
||||
Thread t = new Thread(r);
|
||||
t.setDaemon(true);
|
||||
t.setName("casono-startup-login");
|
||||
return t;
|
||||
});
|
||||
if (res != null && res.getUsername() != null && !res.getUsername().isBlank()) {
|
||||
setSharedUsername(res.getUsername().trim());
|
||||
}
|
||||
|
||||
bg.submit(
|
||||
() -> {
|
||||
try {
|
||||
LobbyClient lobbyClient = new LobbyClient(clientService);
|
||||
LoginResult res = lobbyClient.login(username);
|
||||
|
||||
if (res != null
|
||||
&& res.getUsername() != null
|
||||
&& !res.getUsername().isBlank()) {
|
||||
setSharedUsername(res.getUsername().trim());
|
||||
}
|
||||
|
||||
LOGGER.info(
|
||||
"Assigned username='{}' id={}",
|
||||
res != null ? res.getUsername() : null,
|
||||
res != null ? res.getId() : null);
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.warn("Startup login failed: {}", e.getMessage());
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Unexpected error during startup login", e);
|
||||
} finally {
|
||||
bg.shutdown();
|
||||
}
|
||||
});
|
||||
String assignedUsername = null;
|
||||
String assignedId = null;
|
||||
if (res != null) {
|
||||
assignedUsername = res.getUsername();
|
||||
assignedId = res.getId();
|
||||
}
|
||||
LOGGER.info("Assigned username='{}' id={}", assignedUsername, assignedId);
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.warn(
|
||||
"Could not establish initial connection for startup login: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void launchUI(String arg, String username) {
|
||||
if (username != null && !username.isBlank()) {
|
||||
Launcher.main(new String[] {arg, username});
|
||||
} else {
|
||||
private static void launchUI(String arg) {
|
||||
String username = getSharedUsername();
|
||||
if (username == null || username.isBlank()) {
|
||||
Launcher.main(new String[] {arg});
|
||||
return;
|
||||
}
|
||||
Launcher.main(new String[] {arg, username});
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
@@ -134,7 +121,10 @@ public class ClientApp {
|
||||
throw new IllegalArgumentException("Address argument required: <host:port>");
|
||||
}
|
||||
|
||||
String username = args.length > 1 && args[1] != null && !args[1].isBlank() ? args[1] : null;
|
||||
String username = null;
|
||||
if (args.length > 1) {
|
||||
username = normalizeUsername(args[1]);
|
||||
}
|
||||
|
||||
start(args[0], username);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ChatClient;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatViewController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -39,7 +41,11 @@ public class ChatController {
|
||||
return chatBoxController;
|
||||
}
|
||||
|
||||
private final ChatBoxController chatBoxController;
|
||||
public void setChatBoxController(ChatBoxController chatBoxController) {
|
||||
this.chatBoxController = chatBoxController;
|
||||
}
|
||||
|
||||
private ChatBoxController chatBoxController;
|
||||
private int lobbyId = -1;
|
||||
private final Timer timer;
|
||||
private final Consumer<List<String>> serverEventListener;
|
||||
@@ -61,6 +67,12 @@ public class ChatController {
|
||||
/** List of all users connected to the server, to safe them locally on the client */
|
||||
private final List<String> localUserList;
|
||||
|
||||
public List<String> getLocalUserList() {
|
||||
return this.localUserList;
|
||||
}
|
||||
|
||||
public final Map<ChatController.ChatKey, ChatViewController> activeChatControllers;
|
||||
|
||||
private final Logger logger;
|
||||
|
||||
/**
|
||||
@@ -78,6 +90,7 @@ public class ChatController {
|
||||
this.chatBoxController = new ChatBoxController(username, this);
|
||||
this.logger = LogManager.getLogger(ChatController.class);
|
||||
this.serverEventListener = this::handleServerEvent;
|
||||
this.activeChatControllers = new HashMap<>();
|
||||
|
||||
registerAsActiveController(clientService);
|
||||
clientService.addEventListener(serverEventListener);
|
||||
@@ -126,7 +139,7 @@ public class ChatController {
|
||||
}
|
||||
ChatModel lobbyChatModel = new ChatModel(ChatType.LOBBY, username, lobbyId, null);
|
||||
chatModelMap.put(key, lobbyChatModel);
|
||||
this.chatBoxController.addChatTab("Lobby", lobbyChatModel);
|
||||
this.chatBoxController.addChatTab("LOBBY", lobbyChatModel, ChatType.LOBBY);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -81,11 +81,7 @@ public class LobbyClient {
|
||||
throw new RuntimeException("No LOBBY_ID in response: " + lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the global highscores from the server.
|
||||
*
|
||||
* @return list of formatted highscore entries, newest entries at the end
|
||||
*/
|
||||
/** Fetches the global highscores from the server. */
|
||||
public List<String> getHighscores() {
|
||||
List<String> lines = client.processCommand("GET_HIGHSCORES");
|
||||
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||
@@ -139,7 +135,8 @@ public class LobbyClient {
|
||||
* server
|
||||
*/
|
||||
public LoginResult login(String user) {
|
||||
List<String> lines = client.processCommand("LOGIN USERNAME=" + user);
|
||||
String command = buildLoginCommand(user);
|
||||
List<String> lines = client.processCommand(command);
|
||||
|
||||
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||
|
||||
@@ -155,6 +152,13 @@ public class LobbyClient {
|
||||
return new LoginResult(assigned, id);
|
||||
}
|
||||
|
||||
private String buildLoginCommand(String username) {
|
||||
if (username == null || username.isBlank()) {
|
||||
return "LOGIN";
|
||||
}
|
||||
return "LOGIN USERNAME=" + username.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the username for the currently logged-in session.
|
||||
*
|
||||
@@ -199,7 +203,6 @@ public class LobbyClient {
|
||||
String val = p.value();
|
||||
switch (key) {
|
||||
case "ID":
|
||||
// If we were collecting a lobby, flush it
|
||||
if (currentId != null) {
|
||||
result.add(
|
||||
new LobbyInfo(
|
||||
|
||||
+39
-11
@@ -3,6 +3,7 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
@@ -27,7 +28,7 @@ public class ChatBoxController {
|
||||
|
||||
private String username;
|
||||
|
||||
private ChatController chatController;
|
||||
private final ChatController chatController;
|
||||
|
||||
@FXML private VBox chatBox;
|
||||
|
||||
@@ -43,8 +44,6 @@ public class ChatBoxController {
|
||||
|
||||
@FXML private final Map<String, Tab> usernameTabMap;
|
||||
|
||||
private String ressource = "/ui-structure/components/chatui/chattab.fxml";
|
||||
|
||||
/**
|
||||
* Constructor for the ChatBoxController, initializes the necessary fields and data structures
|
||||
* for managing chat tabs and whisper chats.
|
||||
@@ -73,11 +72,10 @@ public class ChatBoxController {
|
||||
*/
|
||||
@FXML
|
||||
public void initialize() {
|
||||
ChatModel globalChatModel = new ChatModel(ChatType.GLOBAL, username, -1, null);
|
||||
chatController
|
||||
.getChatModelMap()
|
||||
.put(new ChatController.ChatKey(ChatType.GLOBAL), globalChatModel);
|
||||
addChatTab("GLOBAL", globalChatModel);
|
||||
ChatType global = ChatType.GLOBAL;
|
||||
ChatModel globalChatModel = new ChatModel(global, username, -1, null);
|
||||
chatController.getChatModelMap().put(new ChatController.ChatKey(global), globalChatModel);
|
||||
addChatTab("GLOBAL", globalChatModel, global);
|
||||
addWhisperChatButton.setOnAction(event -> addWhisperChatButton.show());
|
||||
}
|
||||
|
||||
@@ -166,10 +164,11 @@ public class ChatBoxController {
|
||||
public void addWhisperChat(String target, ChatModel chatModel) {
|
||||
if (!activeWhisperChats.contains(target)) {
|
||||
activeWhisperChats.add(target);
|
||||
ChatType whisper = ChatType.WHISPER;
|
||||
chatController
|
||||
.getChatModelMap()
|
||||
.put(new ChatController.ChatKey(ChatType.WHISPER, target), chatModel);
|
||||
addChatTab(target, chatModel);
|
||||
.put(new ChatController.ChatKey(whisper, target), chatModel);
|
||||
addChatTab(target, chatModel, whisper);
|
||||
} else {
|
||||
chatTabPane.getSelectionModel().select(usernameTabMap.get(target));
|
||||
}
|
||||
@@ -184,7 +183,8 @@ public class ChatBoxController {
|
||||
* @param chatModel The {@link ChatModel} containing the data and logic for this specific chat.
|
||||
* @throws RuntimeException If the FXML resource for the chat tab cannot be loaded.
|
||||
*/
|
||||
public void addChatTab(String title, ChatModel chatModel) {
|
||||
public void addChatTab(String title, ChatModel chatModel, ChatType chatType) {
|
||||
String ressource = "/ui-structure/components/chatui/chattab.fxml";
|
||||
URL resource = getClass().getResource(ressource);
|
||||
FXMLLoader fxmlLoader = new FXMLLoader(resource);
|
||||
runOnPlatformSynchronized(
|
||||
@@ -193,6 +193,8 @@ public class ChatBoxController {
|
||||
ChatViewController chatViewController =
|
||||
new ChatViewController(
|
||||
this.chatController, chatModel, this.username);
|
||||
chatController.activeChatControllers.put(
|
||||
new ChatController.ChatKey(chatType), chatViewController);
|
||||
fxmlLoader.setController(chatViewController);
|
||||
Node load = fxmlLoader.load();
|
||||
VBox.setVgrow(load, Priority.ALWAYS);
|
||||
@@ -239,4 +241,30 @@ public class ChatBoxController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void loadChats() {
|
||||
Map<ChatController.ChatKey, ChatModel> chatModelMap = chatController.getChatModelMap();
|
||||
ChatController.ChatKey key = new ChatController.ChatKey(ChatType.GLOBAL);
|
||||
ChatModel global = chatModelMap.get(key);
|
||||
ChatViewController globalController = chatController.activeChatControllers.get(key);
|
||||
|
||||
for (String user : chatController.getLocalUserList()) {
|
||||
addWhisperUser(user);
|
||||
}
|
||||
|
||||
for (Message msg : global.messages) {
|
||||
globalController.showMessage(msg);
|
||||
}
|
||||
for (String user : chatController.getLocalUserList()) {
|
||||
ChatController.ChatKey whisperUserKey =
|
||||
new ChatController.ChatKey(ChatType.WHISPER, user);
|
||||
if (chatModelMap.containsKey(whisperUserKey)) {
|
||||
ChatModel whisperChatModel = chatModelMap.get(whisperUserKey);
|
||||
addWhisperChat(user, whisperChatModel);
|
||||
for (Message msg : whisperChatModel.messages) {
|
||||
chatController.activeChatControllers.get(whisperUserKey).showMessage(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+543
-50
@@ -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;
|
||||
@@ -18,15 +21,13 @@ import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Logger;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
/**
|
||||
* Controller for the casino gaming area.
|
||||
@@ -67,7 +68,9 @@ public class CasinoGameController {
|
||||
private java.util.List<String> lastCommunityKeys = java.util.List.of();
|
||||
private java.util.List<String> lastMyCardKeys = java.util.List.of();
|
||||
private int lastPot = Integer.MIN_VALUE;
|
||||
private ChatController chatController;
|
||||
|
||||
public static final double CHAT_CONTAINER_CONSTANT = 6.0;
|
||||
private static final int TOTAL_SLOTS = 5;
|
||||
private static final int PLAYER_SLOTS = 2;
|
||||
private int pot = 0;
|
||||
@@ -138,12 +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 ChatController chatController;
|
||||
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() {
|
||||
@@ -195,6 +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. */
|
||||
@@ -206,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");
|
||||
@@ -226,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();
|
||||
@@ -239,7 +265,8 @@ public class CasinoGameController {
|
||||
// empty display only (optional)
|
||||
renderCommunityCards(List.of());
|
||||
renderPlayerCards(List.of());
|
||||
initializeChatIfPossible();
|
||||
|
||||
chatContainer.toFront();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -257,7 +284,7 @@ public class CasinoGameController {
|
||||
this.chatUsername = username;
|
||||
this.chatClientService = clientService;
|
||||
this.chatLobbyId = lobbyId;
|
||||
initializeChatIfPossible();
|
||||
configureTaskbarLobbyActionAnnouncements();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,42 +292,32 @@ public class CasinoGameController {
|
||||
* is available and the chat has not already been initialized.
|
||||
*/
|
||||
private void initializeChatIfPossible() {
|
||||
if (chatInitialized || chatClientService == null || chatUsername == null) {
|
||||
if (chatInitialized
|
||||
|| chatClientService == null
|
||||
|| chatUsername == null
|
||||
|| chatController == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
chatController = new ChatController(chatUsername, chatClientService);
|
||||
chatController.updateUsername(chatUsername);
|
||||
|
||||
URL resource = getClass().getResource("/ui-structure/components/chatui/chatbox.fxml");
|
||||
FXMLLoader loader = new FXMLLoader(resource);
|
||||
loader.setController(chatController.getChatBoxController());
|
||||
Node chatNode = loader.load();
|
||||
chatContainer.getChildren().setAll(chatNode);
|
||||
AnchorPane.setTopAnchor(chatNode, CHAT_CONTAINER_CONSTANT);
|
||||
AnchorPane.setBottomAnchor(chatNode, CHAT_CONTAINER_CONSTANT);
|
||||
AnchorPane.setLeftAnchor(chatNode, 0.0);
|
||||
AnchorPane.setRightAnchor(chatNode, CHAT_CONTAINER_CONSTANT);
|
||||
|
||||
Parent root = loader.load();
|
||||
|
||||
Stage chatStage = new Stage();
|
||||
|
||||
chatStage.setTitle("Casono");
|
||||
|
||||
String iconPath = getClass().getResource("/images/logoinverted.png").toExternalForm();
|
||||
chatStage.getIcons().add(new Image(iconPath));
|
||||
|
||||
Scene scene = new Scene(root);
|
||||
chatStage.setScene(scene);
|
||||
|
||||
chatStage.setWidth(CHAT_WIDTH);
|
||||
chatStage.setHeight(CHAT_HEIGHT);
|
||||
|
||||
chatStage.setOnCloseRequest(event -> chatInitialized = false);
|
||||
|
||||
chatStage.show();
|
||||
chatController.getChatBoxController().loadChats();
|
||||
chatInitialized = true;
|
||||
|
||||
if (chatLobbyId >= 0) {
|
||||
chatController.setLobbyChat(chatLobbyId);
|
||||
}
|
||||
|
||||
chatInitialized = true;
|
||||
|
||||
} catch (IOException e) {
|
||||
LOGGER.warning("Could not initialize game chat UI: " + e.getMessage());
|
||||
}
|
||||
@@ -483,7 +500,7 @@ public class CasinoGameController {
|
||||
*/
|
||||
private void applyState(GameState s) {
|
||||
|
||||
if (s == null) {
|
||||
if (s == null || gameFinished) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -496,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);
|
||||
@@ -539,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.
|
||||
*
|
||||
@@ -616,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -650,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;
|
||||
@@ -677,6 +816,7 @@ public class CasinoGameController {
|
||||
safeRefresh(player1Controller);
|
||||
safeRefresh(player2Controller);
|
||||
safeRefresh(player3Controller);
|
||||
updateActiveTurnHighlights(players, activePlayerIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -701,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;
|
||||
@@ -773,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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1321,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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
@@ -24,6 +26,7 @@ public class CasinoGameUI extends Application {
|
||||
private static final Logger LOG = Logger.getLogger(CasinoGameUI.class.getName());
|
||||
|
||||
private static ClientService clientService;
|
||||
private static ChatController chatController;
|
||||
|
||||
private static String username;
|
||||
private static int lobbyId = -1;
|
||||
@@ -55,6 +58,10 @@ public class CasinoGameUI extends Application {
|
||||
CasinoGameUI.username = username;
|
||||
}
|
||||
|
||||
public static void setChatController(ChatController chatController) {
|
||||
CasinoGameUI.chatController = chatController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the lobby ID to be used by the application.
|
||||
*
|
||||
@@ -77,8 +84,7 @@ public class CasinoGameUI extends Application {
|
||||
public void start(Stage stage) throws IOException {
|
||||
|
||||
if (clientService == null) {
|
||||
clientService =
|
||||
ch.unibas.dmi.dbis.cs108.casono.client.ClientApp.getSharedClientService();
|
||||
clientService = ClientApp.getSharedClientService();
|
||||
}
|
||||
if (clientService == null) {
|
||||
throw new IllegalStateException(
|
||||
@@ -90,8 +96,7 @@ public class CasinoGameUI extends Application {
|
||||
String effectiveUsername = normalize(username);
|
||||
|
||||
if (effectiveUsername == null) {
|
||||
effectiveUsername =
|
||||
normalize(ch.unibas.dmi.dbis.cs108.casono.client.ClientApp.getSharedUsername());
|
||||
effectiveUsername = normalize(ClientApp.getSharedUsername());
|
||||
}
|
||||
|
||||
if (effectiveUsername == null) {
|
||||
@@ -105,7 +110,7 @@ public class CasinoGameUI extends Application {
|
||||
+ "', injectedUsername='"
|
||||
+ username
|
||||
+ "', sharedUsername='"
|
||||
+ ch.unibas.dmi.dbis.cs108.casono.client.ClientApp.getSharedUsername()
|
||||
+ ClientApp.getSharedUsername()
|
||||
+ "', hasClientService="
|
||||
+ (clientService != null));
|
||||
|
||||
@@ -125,6 +130,8 @@ public class CasinoGameUI extends Application {
|
||||
controller.setMyPlayerId(PlayerId.of(effectiveUsername));
|
||||
controller.setChatContext(effectiveUsername, clientService, lobbyId);
|
||||
|
||||
controller.startChat(chatController);
|
||||
|
||||
Scene scene = new Scene(root, DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
stage.setTitle("Casono");
|
||||
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+554
-66
@@ -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 && isFirstPreflopPlayer(state, me);
|
||||
inputActionAllowed = false;
|
||||
if (!isMyTurn || isOutOrFinished || state == null || me == null) {
|
||||
setBetButtonVisible(false);
|
||||
@@ -308,7 +433,7 @@ public class TaskbarController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFirstPreflopPlayerInputOnly(state, me)) {
|
||||
if (isFirstPreflopPlayer(state, me)) {
|
||||
setActionEnabled(betButton, true);
|
||||
setActionEnabled(callButton, false);
|
||||
setActionEnabled(foldButton, false);
|
||||
@@ -318,6 +443,16 @@ public class TaskbarController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFirstPlayerOfPhase(state, me)) {
|
||||
setActionEnabled(betButton, true);
|
||||
setActionEnabled(callButton, false);
|
||||
// setActionEnabled(foldButton, false);
|
||||
setActionEnabled(raiseButton, false);
|
||||
activateInputField(taskbarInput);
|
||||
inputActionAllowed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
int callTarget = effectiveCallTarget(state);
|
||||
int raiseTarget = safeDouble(callTarget);
|
||||
ValidationResult callValidation = validateTarget(state, me, callTarget);
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -449,7 +605,7 @@ public class TaskbarController {
|
||||
* @return A boolean value indicating whether the current player is the first to act in the
|
||||
* pre-flop phase with only the initial blind layout in place.
|
||||
*/
|
||||
private boolean isFirstPreflopPlayerInputOnly(GameState state, Player me) {
|
||||
private boolean isFirstPreflopPlayer(GameState state, Player me) {
|
||||
if (state == null || me == null || !isPreflop(state.phase) || state.players == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -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 (isFirstPreflopPlayer(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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -55,9 +55,9 @@ public class CasinomainuiController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the UI components and sets default values. If a shared {@link
|
||||
* ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService} exists (created at application
|
||||
* start), the controller reuses it so the connection remains open and already-logged-in.
|
||||
* Initializes the UI components and sets default values. If a shared {@link ClientService}
|
||||
* exists (created at application start), the controller reuses it so the connection remains
|
||||
* open and already-logged-in.
|
||||
*/
|
||||
@FXML
|
||||
public void initialize() {
|
||||
@@ -141,6 +141,7 @@ public class CasinomainuiController {
|
||||
AnchorPane.setBottomAnchor(chatNode, 0.0);
|
||||
AnchorPane.setLeftAnchor(chatNode, 0.0);
|
||||
AnchorPane.setRightAnchor(chatNode, 0.0);
|
||||
gridManager.setChatController(chatController);
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("Could not initialize lobby chat UI: {}", e.getMessage());
|
||||
}
|
||||
|
||||
+15
-11
@@ -1,7 +1,10 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -41,6 +44,8 @@ public class LobbyButtonGridManager {
|
||||
private final LobbyButtonTranslationManager translationManager;
|
||||
private final LobbyClient lobbyClient;
|
||||
|
||||
private ChatController chatController;
|
||||
|
||||
private final ConcurrentHashMap<String, Image> imageCache = new ConcurrentHashMap<>();
|
||||
|
||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
@@ -553,14 +558,11 @@ public class LobbyButtonGridManager {
|
||||
try {
|
||||
var cs = lobbyClient.getClientService();
|
||||
|
||||
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI
|
||||
.setClientService(cs);
|
||||
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI.setLobbyId(
|
||||
lobbyId);
|
||||
CasinoGameUI.setClientService(cs);
|
||||
CasinoGameUI.setLobbyId(lobbyId);
|
||||
CasinoGameUI.setChatController(chatController);
|
||||
|
||||
String username =
|
||||
ch.unibas.dmi.dbis.cs108.casono.client.ClientApp
|
||||
.getSharedUsername();
|
||||
String username = ClientApp.getSharedUsername();
|
||||
if (username == null || username.isBlank()) {
|
||||
username =
|
||||
"Guest-"
|
||||
@@ -570,11 +572,9 @@ public class LobbyButtonGridManager {
|
||||
.toString()
|
||||
.substring(0, GUEST_ID_LENGTH);
|
||||
}
|
||||
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI.setUsername(
|
||||
username);
|
||||
CasinoGameUI.setUsername(username);
|
||||
|
||||
new ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI()
|
||||
.start(gameStage);
|
||||
new CasinoGameUI().start(gameStage);
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Game UI failed: {}", e.getMessage());
|
||||
@@ -590,4 +590,8 @@ public class LobbyButtonGridManager {
|
||||
public LobbyClient getLobbyClient() {
|
||||
return lobbyClient;
|
||||
}
|
||||
|
||||
public void setChatController(ChatController chatController) {
|
||||
this.chatController = chatController;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyCleanupJob;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||
@@ -73,11 +74,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandR
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionDisconnectJob;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
|
||||
import java.time.Duration;
|
||||
@@ -141,37 +138,12 @@ public class ServerApp {
|
||||
userRegistry,
|
||||
new CommandContext(lobbyManager, sessionManager));
|
||||
|
||||
// Periodic cleanup: remove empty lobbies older than 30s and notify affected
|
||||
// users
|
||||
scheduler.scheduleAtFixedRate(
|
||||
() -> {
|
||||
try {
|
||||
var expired =
|
||||
lobbyManager.findEmptyLobbiesOlderThan(
|
||||
Duration.ofSeconds(LOBBY_EXPIRY_SECONDS));
|
||||
for (var lid : expired) {
|
||||
// remove lobby from manager first
|
||||
lobbyManager.removeLobby(lid);
|
||||
|
||||
// broadcast LOBBY_CLOSED event to all connected sessions
|
||||
// (requestId=0)
|
||||
for (Session s : sessionManager.getAllSessions()) {
|
||||
RequestContext ctx = new RequestContext(s.getId(), 0);
|
||||
SuccessResponse ev =
|
||||
new SuccessResponse(
|
||||
ctx,
|
||||
ResponseBody.builder()
|
||||
.param("EVENT", "LOBBY_CLOSED")
|
||||
.param("LOBBY_ID", lid.value())
|
||||
.build()) {};
|
||||
|
||||
responseDispatcher.dispatch(ev);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Lobby expiry job failed", e);
|
||||
}
|
||||
},
|
||||
new LobbyCleanupJob(
|
||||
lobbyManager,
|
||||
sessionManager,
|
||||
responseDispatcher,
|
||||
Duration.ofSeconds(LOBBY_EXPIRY_SECONDS)),
|
||||
LOBBY_CLEANUP_INITIAL_DELAY_SECONDS,
|
||||
LOBBY_CLEANUP_PERIOD_SECONDS,
|
||||
TimeUnit.SECONDS);
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.checks;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call.PlayerCallRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold.PlayerFoldRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.checks.HandlerCheck;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.Response;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Checks whether a target lobby exists for game action commands.
|
||||
*
|
||||
* <p>Supported commands are BET, CALL, FOLD and RAISE. If a request specifies a GAME_ID, the check
|
||||
* validates that this lobby exists. Otherwise, it validates that the logged-in user is in a lobby.
|
||||
*/
|
||||
public class GameLobbyExistsCheck implements HandlerCheck {
|
||||
private final UserRegistry userRegistry;
|
||||
private final LobbyManager lobbyManager;
|
||||
|
||||
public GameLobbyExistsCheck(UserRegistry userRegistry, LobbyManager lobbyManager) {
|
||||
this.userRegistry = userRegistry;
|
||||
this.lobbyManager = lobbyManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Response> check(Request request) {
|
||||
Integer gameId = extractGameId(request);
|
||||
User user = userRegistry.getBySessionId(request.getSessionId()).get();
|
||||
|
||||
Lobby lobby;
|
||||
if (gameId != null) {
|
||||
lobby = lobbyManager.getLobby(LobbyId.of(gameId));
|
||||
} else {
|
||||
lobby = lobbyManager.getLobbyByUsername(user.getName());
|
||||
}
|
||||
|
||||
if (lobby != null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (gameId != null) {
|
||||
return Optional.of(
|
||||
new ErrorResponse(request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||
}
|
||||
|
||||
return Optional.of(
|
||||
new ErrorResponse(request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
|
||||
}
|
||||
|
||||
private Integer extractGameId(Request request) {
|
||||
if (request instanceof PlayerBetRequest) {
|
||||
return ((PlayerBetRequest) request).getGameId();
|
||||
}
|
||||
if (request instanceof PlayerCallRequest) {
|
||||
return ((PlayerCallRequest) request).getGameId();
|
||||
}
|
||||
if (request instanceof PlayerFoldRequest) {
|
||||
return ((PlayerFoldRequest) request).getGameId();
|
||||
}
|
||||
if (request instanceof PlayerRaiseRequest) {
|
||||
return ((PlayerRaiseRequest) request).getGameId();
|
||||
}
|
||||
|
||||
throw new IllegalStateException(
|
||||
"GameLobbyExistsCheck is only supported for BET, CALL, FOLD and RAISE requests");
|
||||
}
|
||||
}
|
||||
+12
-18
@@ -1,9 +1,13 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.GameLobbyExistsCheck;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||
@@ -40,6 +44,8 @@ public class PlayerBetHandler extends CommandHandler<PlayerBetRequest> {
|
||||
super(responseDispatcher);
|
||||
this.userRegistry = userRegistry;
|
||||
this.lobbyManager = lobbyManager;
|
||||
addCheck(new UserLoggedInCheck(userRegistry));
|
||||
addCheck(new GameLobbyExistsCheck(userRegistry, lobbyManager));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,8 +64,7 @@ public class PlayerBetHandler extends CommandHandler<PlayerBetRequest> {
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
|
||||
userRegistry.getBySessionId(request.getSessionId());
|
||||
Optional<User> opt = userRegistry.getBySessionId(request.getSessionId());
|
||||
if (opt.isEmpty()) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
|
||||
@@ -69,22 +74,11 @@ public class PlayerBetHandler extends CommandHandler<PlayerBetRequest> {
|
||||
String username = opt.get().getName();
|
||||
|
||||
Integer gameId = request.getGameId();
|
||||
var lobby =
|
||||
(gameId != null)
|
||||
? lobbyManager.getLobby(LobbyId.of(gameId))
|
||||
: lobbyManager.getLobbyByUsername(username);
|
||||
|
||||
if (lobby == null) {
|
||||
if (gameId != null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||
} else {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
|
||||
}
|
||||
return;
|
||||
Lobby lobby;
|
||||
if (gameId != null) {
|
||||
lobby = lobbyManager.getLobby(LobbyId.of(gameId));
|
||||
} else {
|
||||
lobby = lobbyManager.getLobbyByUsername(username);
|
||||
}
|
||||
|
||||
GameController game = lobby.getGameController();
|
||||
|
||||
+12
-18
@@ -1,9 +1,13 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.GameLobbyExistsCheck;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||
@@ -36,6 +40,8 @@ public class PlayerCallHandler extends CommandHandler<PlayerCallRequest> {
|
||||
super(responseDispatcher);
|
||||
this.userRegistry = userRegistry;
|
||||
this.lobbyManager = lobbyManager;
|
||||
addCheck(new UserLoggedInCheck(userRegistry));
|
||||
addCheck(new GameLobbyExistsCheck(userRegistry, lobbyManager));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,8 +52,7 @@ public class PlayerCallHandler extends CommandHandler<PlayerCallRequest> {
|
||||
*/
|
||||
@Override
|
||||
public void execute(PlayerCallRequest request) {
|
||||
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
|
||||
userRegistry.getBySessionId(request.getSessionId());
|
||||
Optional<User> opt = userRegistry.getBySessionId(request.getSessionId());
|
||||
if (opt.isEmpty()) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
|
||||
@@ -57,22 +62,11 @@ public class PlayerCallHandler extends CommandHandler<PlayerCallRequest> {
|
||||
String username = opt.get().getName();
|
||||
|
||||
Integer gameId = request.getGameId();
|
||||
var lobby =
|
||||
(gameId != null)
|
||||
? lobbyManager.getLobby(LobbyId.of(gameId))
|
||||
: lobbyManager.getLobbyByUsername(username);
|
||||
|
||||
if (lobby == null) {
|
||||
if (gameId != null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||
} else {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
|
||||
}
|
||||
return;
|
||||
Lobby lobby;
|
||||
if (gameId != null) {
|
||||
lobby = lobbyManager.getLobby(LobbyId.of(gameId));
|
||||
} else {
|
||||
lobby = lobbyManager.getLobbyByUsername(username);
|
||||
}
|
||||
|
||||
GameController game = lobby.getGameController();
|
||||
|
||||
+12
-18
@@ -1,9 +1,13 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.GameLobbyExistsCheck;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||
@@ -36,6 +40,8 @@ public class PlayerFoldHandler extends CommandHandler<PlayerFoldRequest> {
|
||||
super(responseDispatcher);
|
||||
this.userRegistry = userRegistry;
|
||||
this.lobbyManager = lobbyManager;
|
||||
addCheck(new UserLoggedInCheck(userRegistry));
|
||||
addCheck(new GameLobbyExistsCheck(userRegistry, lobbyManager));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,8 +52,7 @@ public class PlayerFoldHandler extends CommandHandler<PlayerFoldRequest> {
|
||||
*/
|
||||
@Override
|
||||
public void execute(PlayerFoldRequest request) {
|
||||
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
|
||||
userRegistry.getBySessionId(request.getSessionId());
|
||||
Optional<User> opt = userRegistry.getBySessionId(request.getSessionId());
|
||||
if (opt.isEmpty()) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
|
||||
@@ -57,22 +62,11 @@ public class PlayerFoldHandler extends CommandHandler<PlayerFoldRequest> {
|
||||
String username = opt.get().getName();
|
||||
|
||||
Integer gameId = request.getGameId();
|
||||
var lobby =
|
||||
(gameId != null)
|
||||
? lobbyManager.getLobby(LobbyId.of(gameId))
|
||||
: lobbyManager.getLobbyByUsername(username);
|
||||
|
||||
if (lobby == null) {
|
||||
if (gameId != null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||
} else {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
|
||||
}
|
||||
return;
|
||||
Lobby lobby;
|
||||
if (gameId != null) {
|
||||
lobby = lobbyManager.getLobby(LobbyId.of(gameId));
|
||||
} else {
|
||||
lobby = lobbyManager.getLobbyByUsername(username);
|
||||
}
|
||||
|
||||
GameController game = lobby.getGameController();
|
||||
|
||||
+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;
|
||||
|
||||
+12
-18
@@ -1,9 +1,13 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.GameLobbyExistsCheck;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||
@@ -40,6 +44,8 @@ public class PlayerRaiseHandler extends CommandHandler<PlayerRaiseRequest> {
|
||||
super(responseDispatcher);
|
||||
this.userRegistry = userRegistry;
|
||||
this.lobbyManager = lobbyManager;
|
||||
addCheck(new UserLoggedInCheck(userRegistry));
|
||||
addCheck(new GameLobbyExistsCheck(userRegistry, lobbyManager));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,8 +66,7 @@ public class PlayerRaiseHandler extends CommandHandler<PlayerRaiseRequest> {
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
|
||||
userRegistry.getBySessionId(request.getSessionId());
|
||||
Optional<User> opt = userRegistry.getBySessionId(request.getSessionId());
|
||||
if (opt.isEmpty()) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
|
||||
@@ -71,22 +76,11 @@ public class PlayerRaiseHandler extends CommandHandler<PlayerRaiseRequest> {
|
||||
String username = opt.get().getName();
|
||||
|
||||
Integer gameId = request.getGameId();
|
||||
var lobby =
|
||||
(gameId != null)
|
||||
? lobbyManager.getLobby(LobbyId.of(gameId))
|
||||
: lobbyManager.getLobbyByUsername(username);
|
||||
|
||||
if (lobby == null) {
|
||||
if (gameId != null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||
} else {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
|
||||
}
|
||||
return;
|
||||
Lobby lobby;
|
||||
if (gameId != null) {
|
||||
lobby = lobbyManager.getLobby(LobbyId.of(gameId));
|
||||
} else {
|
||||
lobby = lobbyManager.getLobbyByUsername(username);
|
||||
}
|
||||
|
||||
GameController game = lobby.getGameController();
|
||||
|
||||
+2
-2
@@ -1,5 +1,6 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
@@ -21,8 +22,7 @@ public class GetLobbyListHandler extends CommandHandler<GetLobbyListRequest> {
|
||||
|
||||
@Override
|
||||
public void execute(GetLobbyListRequest request) {
|
||||
Collection<ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby> l =
|
||||
lobbyManager.getAllLobbies();
|
||||
Collection<Lobby> l = lobbyManager.getAllLobbies();
|
||||
responseDispatcher.dispatch(new GetLobbyListResponse(request.getContext(), l));
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -11,6 +11,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||
@@ -38,8 +39,7 @@ public class StartGameHandler extends CommandHandler<StartGameRequest> {
|
||||
|
||||
@Override
|
||||
public void execute(StartGameRequest request) {
|
||||
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
|
||||
userRegistry.getBySessionId(request.getSessionId());
|
||||
Optional<User> opt = userRegistry.getBySessionId(request.getSessionId());
|
||||
if (opt.isEmpty()) {
|
||||
// Guard: UserLoggedInCheck should normally handle this
|
||||
responseDispatcher.dispatch(
|
||||
|
||||
+1
-1
@@ -16,6 +16,6 @@ public class LoginParser implements CommandParser<LoginRequest> {
|
||||
public LoginRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
RequestParameterAccessor accessor =
|
||||
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||
return new LoginRequest(primitiveRequest.context(), accessor.require("USERNAME"));
|
||||
return new LoginRequest(primitiveRequest.context(), accessor.optional("USERNAME", null));
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -5,6 +5,7 @@ import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
|
||||
@@ -40,8 +41,7 @@ public class SendMessageHandler extends CommandHandler<SendMessageRequest> {
|
||||
@Override
|
||||
public void execute(SendMessageRequest request) {
|
||||
Message message = request.getMessage();
|
||||
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> senderUser =
|
||||
userRegistry.getBySessionId(request.getSessionId());
|
||||
Optional<User> senderUser = userRegistry.getBySessionId(request.getSessionId());
|
||||
if (senderUser.isPresent()) {
|
||||
message.sender = senderUser.get().getName();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/** Periodically removes expired empty lobbies and notifies connected sessions. */
|
||||
public class LobbyCleanupJob implements Runnable {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(LobbyCleanupJob.class);
|
||||
|
||||
private final LobbyManager lobbyManager;
|
||||
private final SessionManager sessionManager;
|
||||
private final ResponseDispatcher responseDispatcher;
|
||||
private final Duration expiryThreshold;
|
||||
|
||||
/**
|
||||
* Creates a new cleanup job for expired empty lobbies.
|
||||
*
|
||||
* @param lobbyManager manager used to find and remove expired lobbies
|
||||
* @param sessionManager manager used to resolve connected sessions
|
||||
* @param responseDispatcher dispatcher used to notify clients about closed lobbies
|
||||
* @param expiryThreshold age threshold that marks an empty lobby as expired
|
||||
*/
|
||||
public LobbyCleanupJob(
|
||||
LobbyManager lobbyManager,
|
||||
SessionManager sessionManager,
|
||||
ResponseDispatcher responseDispatcher,
|
||||
Duration expiryThreshold) {
|
||||
this.lobbyManager = lobbyManager;
|
||||
this.sessionManager = sessionManager;
|
||||
this.responseDispatcher = responseDispatcher;
|
||||
this.expiryThreshold = expiryThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one cleanup cycle.
|
||||
*
|
||||
* <p>The method first fetches all expired empty lobbies. Each lobby is then processed
|
||||
* independently so that a failure for one lobby does not stop the remaining cleanups.
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
LOGGER.debug("Job started.");
|
||||
try {
|
||||
List<LobbyId> expired;
|
||||
try {
|
||||
expired = lobbyManager.findEmptyLobbiesOlderThan(expiryThreshold);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Lobby expiry job failed: could not fetch expired lobbies", e);
|
||||
return;
|
||||
}
|
||||
|
||||
for (LobbyId lobbyId : expired) {
|
||||
try {
|
||||
lobbyManager.removeLobby(lobbyId);
|
||||
broadcastLobbyClosed(lobbyId);
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.warn("Failed to process expired lobby {}", lobbyId.value(), e);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
LOGGER.debug("Job finished.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a lobby-closed event to all currently connected sessions.
|
||||
*
|
||||
* @param lobbyId id of the lobby that was closed
|
||||
*/
|
||||
private void broadcastLobbyClosed(LobbyId lobbyId) {
|
||||
for (Session session : sessionManager.getAllSessions()) {
|
||||
try {
|
||||
RequestContext ctx = new RequestContext(session.getId(), 0);
|
||||
SuccessResponse event =
|
||||
new SuccessResponse(
|
||||
ctx,
|
||||
ResponseBody.builder()
|
||||
.param("EVENT", "LOBBY_CLOSED")
|
||||
.param("LOBBY_ID", lobbyId.value())
|
||||
.build()) {};
|
||||
responseDispatcher.dispatch(event);
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.warn(
|
||||
"Failed to notify session {} about closed lobby {}",
|
||||
session.getId().value(),
|
||||
lobbyId.value(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -48,7 +48,7 @@ public class TokenClassifier {
|
||||
}
|
||||
|
||||
private static void validateSeparator(List<RawToken> rawTokens, int index) {
|
||||
boolean missingKey = index == 0 || rawTokens.get(index - 1).type() != RawTokenType.WORD;
|
||||
boolean missingKey = index <= 1 || rawTokens.get(index - 1).type() != RawTokenType.WORD;
|
||||
|
||||
boolean missingValue =
|
||||
index + 1 >= rawTokens.size()
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.image.*?>
|
||||
|
||||
|
||||
<!-- Main container: Links the UI to the CasinoGameController and loads Casinogameui.css -->
|
||||
<AnchorPane xmlns="http://javafx.com/javafx/21"
|
||||
xmlns:fx="http://javafx.com/fxml/1"
|
||||
@@ -66,7 +67,7 @@
|
||||
</Label>
|
||||
|
||||
<Label fx:id="tableText"
|
||||
text="Place your bet"
|
||||
text="Welcome! The round is about to begin."
|
||||
styleClass="table-title" />
|
||||
|
||||
</VBox>
|
||||
@@ -75,12 +76,14 @@
|
||||
<HBox fx:id="communityCardsBox"
|
||||
styleClass="community-cards-box"
|
||||
alignment="CENTER"
|
||||
GridPane.columnIndex="2"
|
||||
spacing="10"/>
|
||||
|
||||
<!-- Pot Box -->
|
||||
<HBox fx:id="potBox"
|
||||
alignment="CENTER"
|
||||
spacing="4"
|
||||
GridPane.columnIndex="2"
|
||||
maxWidth="Infinity"
|
||||
maxHeight="Infinity"/>
|
||||
|
||||
@@ -90,6 +93,7 @@
|
||||
|
||||
<!-- Player Cards -->
|
||||
<HBox fx:id="playerCardsBox"
|
||||
styleClass="player-cards-box"
|
||||
spacing="10"
|
||||
alignment="CENTER"
|
||||
StackPane.alignment="BOTTOM_CENTER">
|
||||
@@ -98,6 +102,7 @@
|
||||
styleClass="dealer-box"
|
||||
fitWidth="100"
|
||||
fitHeight="100"
|
||||
GridPane.columnIndex="2"
|
||||
preserveRatio="true"
|
||||
visible="false"/>
|
||||
|
||||
@@ -116,56 +121,48 @@
|
||||
<!-- RIGHT SIDE (Chat Box) -->
|
||||
<AnchorPane fx:id="chatContainer" GridPane.columnIndex="2" />
|
||||
|
||||
<HBox AnchorPane.topAnchor="45"
|
||||
AnchorPane.leftAnchor="0"
|
||||
AnchorPane.rightAnchor="0"
|
||||
alignment="CENTER"
|
||||
spacing="300">
|
||||
|
||||
<VBox HBox.hgrow="ALWAYS"
|
||||
alignment="TOP_LEFT"
|
||||
translateX="50"
|
||||
translateY="50">
|
||||
|
||||
<fx:include fx:id="player1"
|
||||
source="gameuicomponents/Playerstatus.fxml"/>
|
||||
|
||||
</VBox>
|
||||
|
||||
<VBox HBox.hgrow="ALWAYS"
|
||||
alignment="TOP_CENTER"
|
||||
translateY="20">
|
||||
|
||||
<fx:include fx:id="player2"
|
||||
source="gameuicomponents/Playerstatus.fxml"/>
|
||||
|
||||
</VBox>
|
||||
|
||||
<VBox HBox.hgrow="ALWAYS"
|
||||
alignment="TOP_RIGHT"
|
||||
translateX="-50"
|
||||
translateY="50">
|
||||
|
||||
<fx:include fx:id="player3"
|
||||
source="gameuicomponents/Playerstatus.fxml"/>
|
||||
|
||||
</VBox>
|
||||
|
||||
</HBox>
|
||||
</children>
|
||||
</GridPane>
|
||||
|
||||
<!-- movable taskbar -->
|
||||
<AnchorPane>
|
||||
<fx:include fx:id="taskbarInclude" source="gameuicomponents/Taskbar.fxml"/>
|
||||
<fx:include fx:id="taskbarInclude" source="gameuicomponents/Taskbar.fxml" GridPane.columnIndex="1"/>
|
||||
</AnchorPane>
|
||||
|
||||
<!-- Opponent Status 1: One of three panels displaying the opponent's icon, name, and account balance -->
|
||||
<GridPane AnchorPane.leftAnchor="0.0"
|
||||
AnchorPane.rightAnchor="0.0"
|
||||
AnchorPane.topAnchor="50">
|
||||
|
||||
<columnConstraints>
|
||||
<ColumnConstraints percentWidth="5"/>
|
||||
<ColumnConstraints percentWidth="16"/>
|
||||
</columnConstraints>
|
||||
|
||||
<children>
|
||||
<fx:include fx:id="player1" source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/>
|
||||
</children>
|
||||
</GridPane>
|
||||
|
||||
<!-- Opponent Status 2: One of three panels displaying the opponent's icon, name, and account balance -->
|
||||
<GridPane AnchorPane.leftAnchor="0.0"
|
||||
AnchorPane.rightAnchor="0.0"
|
||||
AnchorPane.topAnchor="20">
|
||||
|
||||
<columnConstraints>
|
||||
<ColumnConstraints percentWidth="30"/>
|
||||
<ColumnConstraints percentWidth="16"/>
|
||||
</columnConstraints>
|
||||
|
||||
<children>
|
||||
<fx:include fx:id="player2" source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/>
|
||||
</children>
|
||||
</GridPane>
|
||||
|
||||
<!-- Opponent Status 3: One of three panels that displays the opponent's icon, name, and account balance -->
|
||||
<GridPane AnchorPane.leftAnchor="0.0"
|
||||
AnchorPane.rightAnchor="0.0"
|
||||
AnchorPane.topAnchor="50">
|
||||
|
||||
<columnConstraints>
|
||||
<ColumnConstraints percentWidth="55"/>
|
||||
<ColumnConstraints percentWidth="16"/>
|
||||
</columnConstraints>
|
||||
|
||||
<children>
|
||||
<fx:include fx:id="player3" source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/>
|
||||
</children>
|
||||
</GridPane>
|
||||
</AnchorPane>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
|
||||
<VBox fx:id="highscoreRoot"
|
||||
xmlns="http://javafx.com/javafx/21"
|
||||
xmlns:fx="http://javafx.com/fxml/1"
|
||||
|
||||
@@ -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.
|
||||
@@ -16,7 +14,7 @@ and status displays will be dynamically populated with real-time data from the s
|
||||
<VBox fx:id="playerStatusBox"
|
||||
xmlns="http://javafx.com/javafx/21"
|
||||
xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.PlayerStatusController"
|
||||
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.PlayerStatusController"
|
||||
styleClass="player-status-pane"
|
||||
prefWidth="200"
|
||||
spacing="5">
|
||||
@@ -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">
|
||||
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
|
||||
<!-- movable taskbar -->
|
||||
<HBox xmlns="http://javafx.com/javafx/21"
|
||||
xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.TaskbarController"
|
||||
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.TaskbarController"
|
||||
fx:id="taskbar"
|
||||
styleClass="taskbar"
|
||||
spacing="15"
|
||||
@@ -40,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"/>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
|
||||
|
||||
<!--
|
||||
Dieses Fenster wird später mit den Serveranfragen verknüpft,
|
||||
um Nachrichten von Mitspielern und Systemmeldungen in Echtzeit anzuzeigen.
|
||||
@@ -19,7 +20,7 @@
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx/21"
|
||||
xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatViewController"
|
||||
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatViewController"
|
||||
alignment="CENTER"
|
||||
stylesheets="@chatui.css">
|
||||
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.TaskbarController;
|
||||
import java.net.URL;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Smoke test for Taskbar UI. Ensures FXML loads and controller initializes without errors. */
|
||||
class CasinoGameControllerUITest {
|
||||
|
||||
private static boolean fxStarted = false;
|
||||
|
||||
/** Initializes JavaFX once before all tests. */
|
||||
@BeforeAll
|
||||
static void startJavaFX() throws Exception {
|
||||
if (!fxStarted) {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
try {
|
||||
Platform.startup(
|
||||
() -> {
|
||||
Platform.setImplicitExit(false);
|
||||
latch.countDown();
|
||||
});
|
||||
} catch (IllegalStateException ignored) {
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
assertTrue(latch.await(10, TimeUnit.SECONDS), "JavaFX did not start");
|
||||
fxStarted = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Ensures FXML loads and controller initializes without errors. */
|
||||
@Test
|
||||
void taskbarLoadsWithoutErrors() throws Exception {
|
||||
URL url = getClass().getResource("/ui-structure/gameuicomponents/Taskbar.fxml");
|
||||
assertNotNull(url);
|
||||
|
||||
FXMLLoader loader = new FXMLLoader(url);
|
||||
Parent root = loader.load();
|
||||
|
||||
TaskbarController controller = loader.getController();
|
||||
|
||||
assertNotNull(root);
|
||||
assertNotNull(controller);
|
||||
}
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.*;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.TextField;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* TODO: Refactor UI tests to avoid reflection and test behavior via public APIs or real UI actions
|
||||
* (preferably using TestFX) for better maintainability and robustness.
|
||||
*
|
||||
* <p>These tests verify: - input validation behavior - UI state updates based on game state -
|
||||
* correct enabling/disabling of action buttons
|
||||
*/
|
||||
class TaskbarControllerInputValidationTest {
|
||||
|
||||
private static boolean fxStarted = false;
|
||||
|
||||
/** Initializes JavaFX once before all tests. */
|
||||
@BeforeAll
|
||||
static void initJavaFX() throws Exception {
|
||||
if (fxStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
try {
|
||||
Platform.startup(
|
||||
() -> {
|
||||
Platform.setImplicitExit(false);
|
||||
latch.countDown();
|
||||
});
|
||||
} catch (IllegalStateException ignored) {
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
latch.await(10, TimeUnit.SECONDS);
|
||||
fxStarted = true;
|
||||
}
|
||||
|
||||
/** Ensures invalid input values do not enable the BET button. */
|
||||
@Test
|
||||
void invalidInputShouldBeRejected() throws Exception {
|
||||
Fixture f = load();
|
||||
|
||||
String[] invalidInputs = {"", "abc", "-1", "999999999999"};
|
||||
|
||||
for (String input : invalidInputs) {
|
||||
setText(f.input, input);
|
||||
|
||||
invokePrivate(f.controller, "refreshBetInputUi");
|
||||
|
||||
Button bet = get(f.controller, "betButton", Button.class);
|
||||
|
||||
assertFalse(bet.isVisible(), "Invalid input must not enable BET button: " + input);
|
||||
}
|
||||
}
|
||||
|
||||
/** Ensures valid input enables the BET button. */
|
||||
@Test
|
||||
void validInputShouldEnableBet() throws Exception {
|
||||
Fixture f = load();
|
||||
|
||||
GameState state = createGameState();
|
||||
|
||||
setField(f.controller, "myPlayerId", PlayerId.of("me"));
|
||||
setField(f.controller, "lastState", state);
|
||||
setField(f.controller, "inputActionAllowed", true);
|
||||
|
||||
f.controller.update(state, PlayerId.of("me"));
|
||||
|
||||
setText(f.input, "200");
|
||||
|
||||
invokePrivate(f.controller, "refreshBetInputUi");
|
||||
|
||||
Button bet = get(f.controller, "betButton", Button.class);
|
||||
|
||||
assertTrue(bet.isVisible(), "BET button should be enabled for valid input");
|
||||
}
|
||||
|
||||
/** Ensures all UI actions are disabled when the game is finished. */
|
||||
@Test
|
||||
void finishedStateShouldDisableUi() throws Exception {
|
||||
Fixture f = load();
|
||||
|
||||
GameState state = createGameState();
|
||||
state.phase = "FINISHED";
|
||||
|
||||
f.controller.update(state, PlayerId.of("me"));
|
||||
|
||||
assertTrue(f.input.isDisabled());
|
||||
assertTrue(get(f.controller, "callButton", Button.class).isDisabled());
|
||||
assertTrue(get(f.controller, "foldButton", Button.class).isDisabled());
|
||||
assertTrue(get(f.controller, "raiseButton", Button.class).isDisabled());
|
||||
}
|
||||
|
||||
/** Creates a minimal game state for testing. */
|
||||
private GameState createGameState() {
|
||||
GameState state = new GameState();
|
||||
|
||||
Player me = new Player(PlayerId.of("me"), 20000);
|
||||
Player other = new Player(PlayerId.of("other"), 20000);
|
||||
|
||||
state.players = List.of(me, other);
|
||||
state.phase = "PREFLOP";
|
||||
state.currentBet = 200;
|
||||
state.activePlayer = 0;
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/** Loads the JavaFX controller and its UI components. */
|
||||
private Fixture load() throws Exception {
|
||||
URL url = getClass().getResource("/ui-structure/gameuicomponents/Taskbar.fxml");
|
||||
assertNotNull(url);
|
||||
|
||||
FXMLLoader loader = new FXMLLoader(url);
|
||||
Parent root = loader.load();
|
||||
|
||||
root.applyCss();
|
||||
root.layout();
|
||||
|
||||
TaskbarController controller = loader.getController();
|
||||
|
||||
TextField input = get(controller, "taskbarInput", TextField.class);
|
||||
Button bet = get(controller, "betButton", Button.class);
|
||||
|
||||
return new Fixture(controller, input, bet);
|
||||
}
|
||||
|
||||
private record Fixture(TaskbarController controller, TextField input, Button betButton) {}
|
||||
|
||||
private static void setText(TextField field, String value) {
|
||||
field.setText(value);
|
||||
}
|
||||
|
||||
private static void invokePrivate(Object obj, String method) throws Exception {
|
||||
Method m = obj.getClass().getDeclaredMethod(method);
|
||||
m.setAccessible(true);
|
||||
m.invoke(obj);
|
||||
}
|
||||
|
||||
private static <T> T get(Object obj, String field, Class<T> type) throws Exception {
|
||||
Field f = obj.getClass().getDeclaredField(field);
|
||||
f.setAccessible(true);
|
||||
return type.cast(f.get(obj));
|
||||
}
|
||||
|
||||
/** Sets a private field value using reflection. */
|
||||
private static void setField(Object obj, String fieldName, Object value) throws Exception {
|
||||
Field field = obj.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(obj, value);
|
||||
}
|
||||
|
||||
/** Gets a private field value using reflection. */
|
||||
// private static <T> T getField(Object obj, String fieldName, Class<T> type) throws Exception {
|
||||
// Field field = obj.getClass().getDeclaredField(fieldName);
|
||||
// field.setAccessible(true);
|
||||
// return type.cast(field.get(obj));
|
||||
// }
|
||||
}
|
||||
+3
-3
@@ -1,6 +1,7 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.TestServer;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
@@ -8,15 +9,14 @@ class LobbyButtonGridManagerTest {
|
||||
LobbyButtonGridManager gridManager;
|
||||
LobbyButtonTranslationManager translationManager;
|
||||
GridPane gridPane;
|
||||
ch.unibas.dmi.dbis.cs108.casono.client.network.TestServer testServer;
|
||||
TestServer testServer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
gridPane = new GridPane();
|
||||
translationManager = LobbyButtonTranslationManager.getInstance();
|
||||
translationManager.getButtonIdToLobbyId().clear();
|
||||
// Start an in-process test server and connect a real ClientService to it.
|
||||
testServer = new ch.unibas.dmi.dbis.cs108.casono.client.network.TestServer();
|
||||
testServer = new TestServer();
|
||||
String host = "127.0.0.1";
|
||||
int port = testServer.getPort();
|
||||
ClientService clientService = new ClientService(host, port);
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class TokenClassifierTest {
|
||||
@Test
|
||||
void testClassifySimpleCommand() {
|
||||
List<RawToken> raw = Tokenizer.tokenize("PING");
|
||||
List<Token> tokens = TokenClassifier.classify(raw);
|
||||
|
||||
assertEquals(2, tokens.size());
|
||||
assertEquals(TokenType.COMMAND, tokens.get(0).type());
|
||||
assertEquals("PING", tokens.get(0).value());
|
||||
assertEquals(TokenType.EOF, tokens.get(1).type());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCommandWithParameter() {
|
||||
List<RawToken> raw = Tokenizer.tokenize("ANSWER VALUE=42");
|
||||
List<Token> tokens = TokenClassifier.classify(raw);
|
||||
|
||||
assertEquals(5, tokens.size());
|
||||
assertEquals(TokenType.COMMAND, tokens.get(0).type());
|
||||
assertEquals(TokenType.KEY, tokens.get(1).type());
|
||||
assertEquals(TokenType.SEPARATOR, tokens.get(2).type());
|
||||
assertEquals(TokenType.VALUE, tokens.get(3).type());
|
||||
assertEquals("42", tokens.get(3).value());
|
||||
assertEquals(TokenType.EOF, tokens.get(4).type());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringValue() {
|
||||
List<RawToken> raw = Tokenizer.tokenize("GREET MSG='Hello World'");
|
||||
List<Token> tokens = TokenClassifier.classify(raw);
|
||||
|
||||
assertEquals(5, tokens.size());
|
||||
assertEquals(TokenType.COMMAND, tokens.get(0).type());
|
||||
assertEquals(TokenType.KEY, tokens.get(1).type());
|
||||
assertEquals(TokenType.SEPARATOR, tokens.get(2).type());
|
||||
assertEquals(TokenType.VALUE, tokens.get(3).type());
|
||||
assertEquals("Hello World", tokens.get(3).value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnexpectedStringLiteralThrows() {
|
||||
List<RawToken> raw = Tokenizer.tokenize("CMD 'oops'");
|
||||
|
||||
TokenizerException ex =
|
||||
assertThrows(TokenizerException.class, () -> TokenClassifier.classify(raw));
|
||||
assertTrue(ex.getMessage().contains("Unexpected string literal"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMissingValueAfterSeparatorThrows() {
|
||||
List<RawToken> raw = Tokenizer.tokenize("CMD KEY=");
|
||||
|
||||
TokenizerException ex =
|
||||
assertThrows(TokenizerException.class, () -> TokenClassifier.classify(raw));
|
||||
assertEquals("Expected VALUE after '='", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMissingKeyBeforeSeparatorThrows() {
|
||||
List<RawToken> raw = Tokenizer.tokenize("CMD =VALUE");
|
||||
|
||||
TokenizerException ex =
|
||||
assertThrows(TokenizerException.class, () -> TokenClassifier.classify(raw));
|
||||
assertEquals("Expected KEY before '='", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNextWordIsKeyThrows() {
|
||||
List<RawToken> raw = Tokenizer.tokenize("CMD KEY1=KEY2=42");
|
||||
|
||||
TokenizerException ex =
|
||||
assertThrows(TokenizerException.class, () -> TokenClassifier.classify(raw));
|
||||
assertEquals("Expected VALUE after '='", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyRawTokensThrows() {
|
||||
List<RawToken> raw = new ArrayList<>();
|
||||
|
||||
TokenizerException ex =
|
||||
assertThrows(TokenizerException.class, () -> TokenClassifier.classify(raw));
|
||||
assertEquals("Expected COMMAND as first token", ex.getMessage());
|
||||
assertEquals(1, ex.getLine());
|
||||
assertEquals(1, ex.getColumn());
|
||||
}
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.parser.tokenizer;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class TokenizerTest {
|
||||
@Test
|
||||
void testEofAlwaysPresent() {
|
||||
List<RawToken> tokens = Tokenizer.tokenize("");
|
||||
|
||||
assertEquals(1, tokens.size());
|
||||
assertEquals(RawTokenType.EOF, tokens.get(0).type());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSimpleCommand() {
|
||||
List<RawToken> tokens = Tokenizer.tokenize("PING");
|
||||
assertEquals(2, tokens.size());
|
||||
assertEquals(RawTokenType.WORD, tokens.get(0).type());
|
||||
assertEquals("PING", tokens.get(0).value());
|
||||
assertEquals(RawTokenType.EOF, tokens.get(1).type());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSimpleCommandWithUnderscore() {
|
||||
List<RawToken> tokens = Tokenizer.tokenize("JOIN_GAME");
|
||||
assertEquals(2, tokens.size());
|
||||
assertEquals(RawTokenType.WORD, tokens.get(0).type());
|
||||
assertEquals("JOIN_GAME", tokens.get(0).value());
|
||||
assertEquals(RawTokenType.EOF, tokens.get(1).type());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCommandWithOneParameter() {
|
||||
List<RawToken> tokens = Tokenizer.tokenize("GET VALUE=42");
|
||||
assertEquals(5, tokens.size());
|
||||
|
||||
assertEquals(RawTokenType.WORD, tokens.get(0).type());
|
||||
assertEquals("GET", tokens.get(0).value());
|
||||
|
||||
// Parameter
|
||||
assertEquals(RawTokenType.WORD, tokens.get(1).type());
|
||||
assertEquals("VALUE", tokens.get(1).value());
|
||||
|
||||
assertEquals(RawTokenType.SEPARATOR, tokens.get(2).type());
|
||||
assertEquals("=", tokens.get(2).value());
|
||||
|
||||
assertEquals(RawTokenType.WORD, tokens.get(3).type());
|
||||
assertEquals("42", tokens.get(3).value());
|
||||
|
||||
assertEquals(RawTokenType.EOF, tokens.get(4).type());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCommandWithOneParameterWhitespacesBetweenSeperator() {
|
||||
List<RawToken> tokens = Tokenizer.tokenize("GET VALUE = 42");
|
||||
assertEquals(5, tokens.size());
|
||||
|
||||
assertEquals(RawTokenType.WORD, tokens.get(0).type());
|
||||
assertEquals("GET", tokens.get(0).value());
|
||||
|
||||
// Parameter
|
||||
assertEquals(RawTokenType.WORD, tokens.get(1).type());
|
||||
assertEquals("VALUE", tokens.get(1).value());
|
||||
|
||||
assertEquals(RawTokenType.SEPARATOR, tokens.get(2).type());
|
||||
assertEquals("=", tokens.get(2).value());
|
||||
|
||||
assertEquals(RawTokenType.WORD, tokens.get(3).type());
|
||||
assertEquals("42", tokens.get(3).value());
|
||||
|
||||
assertEquals(RawTokenType.EOF, tokens.get(4).type());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCommandWithOneParameterAndNewline() {
|
||||
List<RawToken> tokens = Tokenizer.tokenize("GET\nVALUE=42");
|
||||
assertEquals(6, tokens.size());
|
||||
|
||||
assertEquals(RawTokenType.WORD, tokens.get(0).type());
|
||||
assertEquals("GET", tokens.get(0).value());
|
||||
|
||||
// Parameter
|
||||
assertEquals(RawTokenType.WORD, tokens.get(2).type());
|
||||
assertEquals("VALUE", tokens.get(2).value());
|
||||
|
||||
assertEquals(RawTokenType.SEPARATOR, tokens.get(3).type());
|
||||
assertEquals("=", tokens.get(3).value());
|
||||
|
||||
assertEquals(RawTokenType.WORD, tokens.get(4).type());
|
||||
assertEquals("42", tokens.get(4).value());
|
||||
|
||||
assertEquals(RawTokenType.EOF, tokens.get(5).type());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCommandWithStringParameter() {
|
||||
List<RawToken> tokens = Tokenizer.tokenize("GREET MSG='Hello World'");
|
||||
assertEquals(5, tokens.size());
|
||||
|
||||
assertEquals(RawTokenType.WORD, tokens.get(0).type());
|
||||
assertEquals("GREET", tokens.get(0).value());
|
||||
|
||||
// First parameter
|
||||
assertEquals(RawTokenType.WORD, tokens.get(1).type());
|
||||
assertEquals("MSG", tokens.get(1).value());
|
||||
|
||||
assertEquals(RawTokenType.SEPARATOR, tokens.get(2).type());
|
||||
assertEquals("=", tokens.get(2).value());
|
||||
|
||||
assertEquals(RawTokenType.STRING, tokens.get(3).type());
|
||||
assertEquals("Hello World", tokens.get(3).value());
|
||||
|
||||
assertEquals(RawTokenType.EOF, tokens.get(4).type());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCommandWithMultilineStringParameter() {
|
||||
List<RawToken> tokens = Tokenizer.tokenize("GREET MSG='Hello\nWorld'");
|
||||
assertEquals(5, tokens.size());
|
||||
|
||||
assertEquals(RawTokenType.WORD, tokens.get(0).type());
|
||||
assertEquals("GREET", tokens.get(0).value());
|
||||
|
||||
// First parameter
|
||||
assertEquals(RawTokenType.WORD, tokens.get(1).type());
|
||||
assertEquals("MSG", tokens.get(1).value());
|
||||
|
||||
assertEquals(RawTokenType.SEPARATOR, tokens.get(2).type());
|
||||
assertEquals("=", tokens.get(2).value());
|
||||
|
||||
assertEquals(RawTokenType.STRING, tokens.get(3).type());
|
||||
assertEquals("Hello\nWorld", tokens.get(3).value());
|
||||
|
||||
assertEquals(RawTokenType.EOF, tokens.get(4).type());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWhitespace() {
|
||||
List<RawToken> tokens = Tokenizer.tokenize(" \t PING");
|
||||
assertEquals(2, tokens.size());
|
||||
assertEquals(RawTokenType.WORD, tokens.get(0).type());
|
||||
assertEquals("PING", tokens.get(0).value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringWithEscapedQuote() {
|
||||
List<RawToken> tokens =
|
||||
Tokenizer.tokenize("WONDERFUL_GREETING MSG='it\\'s a wonderful day'");
|
||||
assertEquals(5, tokens.size());
|
||||
assertEquals(RawTokenType.STRING, tokens.get(3).type());
|
||||
assertEquals("it's a wonderful day", tokens.get(3).value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringWithWronglyEscapedQuote() {
|
||||
TokenizerException ex =
|
||||
assertThrows(
|
||||
TokenizerException.class,
|
||||
() -> Tokenizer.tokenize("WONDERFUL_GREETING MSG='it\'s a wonderful day'"));
|
||||
assertEquals("Unterminated string literal", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringWithEscapedBackslash() {
|
||||
List<RawToken> tokens = Tokenizer.tokenize("EXECUTE_COMMAND CMD='\\whoami'");
|
||||
assertEquals(5, tokens.size());
|
||||
assertEquals(RawTokenType.STRING, tokens.get(3).type());
|
||||
assertEquals("\\whoami", tokens.get(3).value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testColumnTracking() {
|
||||
List<RawToken> tokens = Tokenizer.tokenize("GET VAL=1");
|
||||
assertEquals(5, tokens.size());
|
||||
assertEquals(1, tokens.get(0).column());
|
||||
assertEquals(5, tokens.get(1).column());
|
||||
assertEquals(8, tokens.get(2).column());
|
||||
assertEquals(9, tokens.get(3).column());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnterminatedStringThrows() {
|
||||
TokenizerException ex =
|
||||
assertThrows(
|
||||
TokenizerException.class, () -> Tokenizer.tokenize("GREETING = 'unclosed"));
|
||||
assertEquals(1, ex.getLine());
|
||||
assertEquals(12, ex.getColumn());
|
||||
assertTrue(ex.getMessage().contains("Unterminated string literal"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnexpectedCharacterThrows() {
|
||||
TokenizerException ex =
|
||||
assertThrows(TokenizerException.class, () -> Tokenizer.tokenize("CMD @ KEY=VALUE"));
|
||||
assertTrue(ex.getMessage().contains("Unexpected character '@'"));
|
||||
assertEquals(1, ex.getLine());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyStringValue() {
|
||||
List<RawToken> tokens = Tokenizer.tokenize("cmd = ''");
|
||||
assertEquals(4, tokens.size());
|
||||
assertEquals(RawTokenType.STRING, tokens.get(2).type());
|
||||
assertEquals("", tokens.get(2).value());
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class RequestParameterAccessorTest {
|
||||
@Test
|
||||
void testRequiredParameterPresent() {
|
||||
List<RequestParameter> parameters = List.of(new RequestParameter("ARG1", "VAL1"));
|
||||
RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
|
||||
|
||||
assertEquals("VAL1", accessor.require("ARG1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMissingRequiredParameterThrows() {
|
||||
List<RequestParameter> parameters = List.of();
|
||||
RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
|
||||
|
||||
MissingParameterException ex =
|
||||
assertThrows(MissingParameterException.class, () -> accessor.require("ARG1"));
|
||||
assertEquals("Required parameter with key 'ARG1' is missing.", ex.getMessage());
|
||||
assertEquals("ARG1", ex.getParameterKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRequiredParameterSuccessfulParsing() {
|
||||
List<RequestParameter> parameters = List.of(new RequestParameter("ARG1", "42"));
|
||||
RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
|
||||
|
||||
assertEquals(42, (int) accessor.require("ARG1", Integer::valueOf));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRequiredParameterInvalidParsingThrows() {
|
||||
List<RequestParameter> parameters =
|
||||
List.of(new RequestParameter("ARG1", "The answer is: 42"));
|
||||
RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
|
||||
|
||||
ParameterParseException ex =
|
||||
assertThrows(
|
||||
ParameterParseException.class,
|
||||
() -> accessor.require("ARG1", Integer::valueOf));
|
||||
assertEquals("Error while parsing 'ARG1' with specified parser", ex.getMessage());
|
||||
assertEquals("ARG1", ex.getParameterKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOptionalParameterPresent() {
|
||||
List<RequestParameter> parameters = List.of(new RequestParameter("ARG1", "VAL1"));
|
||||
RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
|
||||
|
||||
assertEquals("VAL1", accessor.optional("ARG1", "DEFAULT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOptionalParameterMissing() {
|
||||
List<RequestParameter> parameters = List.of();
|
||||
RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
|
||||
|
||||
assertEquals("DEFAULT", accessor.optional("ARG1", "DEFAULT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOptionalParameterSuccessfulParsing() {
|
||||
List<RequestParameter> parameters = List.of(new RequestParameter("ARG1", "42"));
|
||||
RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
|
||||
|
||||
assertEquals(42, (int) accessor.optional("ARG1", 7411, Integer::valueOf));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMissingOptionalParameterSuccessfulParsing() {
|
||||
List<RequestParameter> parameters = List.of();
|
||||
RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
|
||||
|
||||
assertEquals(7411, (int) accessor.optional("ARG1", 7411, Integer::valueOf));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOptionalParameterInvalidParsingThrows() {
|
||||
List<RequestParameter> parameters =
|
||||
List.of(new RequestParameter("ARG1", "The answer is: 42"));
|
||||
RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
|
||||
|
||||
ParameterParseException ex =
|
||||
assertThrows(
|
||||
ParameterParseException.class,
|
||||
() -> accessor.optional("ARG1", 7411, Integer::valueOf));
|
||||
assertEquals("Error while parsing 'ARG1' with specified parser", ex.getMessage());
|
||||
assertEquals("ARG1", ex.getParameterKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testContainsExistingKey() {
|
||||
List<RequestParameter> parameters = List.of(new RequestParameter("ARG1", "VAL1"));
|
||||
RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
|
||||
|
||||
assertTrue(accessor.containsKey("ARG1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testContainsMissingKey() {
|
||||
List<RequestParameter> parameters = List.of();
|
||||
RequestParameterAccessor accessor = new RequestParameterAccessor(parameters);
|
||||
|
||||
assertFalse(accessor.containsKey("ARG1"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user