Compare commits
131 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 | |||
| 028c3adb0d | |||
| 19b555fb47 | |||
| 8a7fad11a9 | |||
| c0a7cadf7e | |||
| 2fd07bbf60 | |||
| 15c998d549 | |||
| 062e66cdf9 | |||
| 7674ae57ad | |||
| 5e79b45d90 | |||
| 45411c4087 | |||
| cc7f8a8ef3 | |||
| d3a00aebc0 | |||
| ab2adbf311 | |||
| 8099228d4f | |||
| ac83d63ea1 | |||
| 3443b5e8f3 | |||
| ad7086e5a5 | |||
| e39cbb4357 | |||
| 238a156d91 | |||
| 621843167e | |||
| 215e7e2cae | |||
| 03daf9c3b8 | |||
| d23a00e148 | |||
| a5f0735a08 | |||
| 4855e937e6 | |||
| 007e863878 | |||
| 56aa3c0215 | |||
| 417af2e8f7 | |||
| 893bcee9bf | |||
| 6d1b467116 | |||
| 4687566816 | |||
| d27a07f3e8 | |||
| b86f18a11c | |||
| 7ff274269e | |||
| ec916aa98c | |||
| 11be6e2b9b | |||
| a8599cfb4c | |||
| 08b9f652be | |||
| 3252c7e534 | |||
| 7cba7ac1dd | |||
| c83a360fec | |||
| 2631791330 | |||
| 181f538038 |
@@ -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>
|
||||
@@ -70,6 +70,18 @@ This document describes the protocol for client-server communication in our appl
|
||||
- [Success Response](#success-response)
|
||||
- [Example Request](#example-request)
|
||||
- [Example Response](#example-response)
|
||||
- [GET_HIGHSCORES command](#get_highscores-command)
|
||||
- [Required pre-execution checks](#required-pre-execution-checks)
|
||||
- [Request Parameters](#request-parameters)
|
||||
- [Success Response](#success-response)
|
||||
- [Example Request](#example-request)
|
||||
- [Example Response](#example-response)
|
||||
- [CLEAR_HIGHSCORES command](#clear_highscores-command)
|
||||
- [Required pre-execution checks](#required-pre-execution-checks)
|
||||
- [Request Parameters](#request-parameters)
|
||||
- [Success Response](#success-response)
|
||||
- [Example Request](#example-request)
|
||||
- [Example Response](#example-response)
|
||||
- [RAISE command](#raise-command)
|
||||
- [CALL command](#call-command)
|
||||
- [FOLD command](#fold-command)
|
||||
@@ -1338,3 +1350,71 @@ END
|
||||
MESSAGE=Maximum number of 8 lobbies reached
|
||||
END
|
||||
```
|
||||
|
||||
## GET_HIGHSCORES command
|
||||
|
||||
The `GET_HIGHSCORES` command returns the stored highscore list. The server sends a `HIGHSCORES` block containing repeated `HIGHSCORE` fields. Each entry is already formatted as `timestamp | winner name`.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
### Success Response
|
||||
|
||||
| Field | Type | Description |
|
||||
| :----------- | :---------------------- | :--------------------------------------- |
|
||||
| `HIGHSCORES` | `Collection<Highscore>` | Collection of formatted highscore lines. |
|
||||
|
||||
| Fields of `Highscore` | Type | Description |
|
||||
| :-------------------- | :------- | :-------------------------------------------------- |
|
||||
| `HIGHSCORE` | `String` | One formatted entry in `yyyy-MM-dd HH:mm:ss | name` |
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
GET_HIGHSCORES
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
HIGHSCORES
|
||||
HIGHSCORE='2026-04-21 15:19:23 | Jona'
|
||||
HIGHSCORE='2026-04-21 16:02:11 | Lars'
|
||||
END
|
||||
END
|
||||
```
|
||||
|
||||
## CLEAR_HIGHSCORES command
|
||||
|
||||
The `CLEAR_HIGHSCORES` command deletes all stored highscore entries on the server.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
### Success Response
|
||||
|
||||
No response fields.
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
CLEAR_HIGHSCORES
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
END
|
||||
```
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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 {
|
||||
|
||||
@@ -39,6 +39,17 @@ public class ClientApp {
|
||||
return sharedUsername;
|
||||
}
|
||||
|
||||
public static void updateSharedUsername(String username) {
|
||||
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) {
|
||||
sharedUsername = username;
|
||||
LOGGER.info("sharedUsername set to '{}'", getSharedUsername());
|
||||
@@ -48,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) {
|
||||
@@ -77,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) {
|
||||
@@ -130,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);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||
|
||||
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;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.function.Consumer;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
@@ -24,7 +31,7 @@ public class ChatController {
|
||||
private static final Map<ClientService, ChatController> ACTIVE_CONTROLLERS =
|
||||
new WeakHashMap<>();
|
||||
|
||||
private final String username;
|
||||
private volatile String username;
|
||||
|
||||
private final ClientService clientService;
|
||||
|
||||
@@ -34,9 +41,14 @@ 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;
|
||||
|
||||
public record ChatKey(ChatType type, @Nullable String targetUser) {
|
||||
public ChatKey(ChatType type) {
|
||||
@@ -55,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;
|
||||
|
||||
/**
|
||||
@@ -71,8 +89,11 @@ public class ChatController {
|
||||
localUserList = new ArrayList<>();
|
||||
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);
|
||||
|
||||
this.timer = new Timer(true);
|
||||
timer.schedule(
|
||||
@@ -118,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,6 +152,7 @@ public class ChatController {
|
||||
* <p>All Messages get added to a particular {@link ChatModel}, if the checks passed.
|
||||
*/
|
||||
public void receiveMessage() {
|
||||
String currentUsername = getCurrentUsername();
|
||||
List<Message> newMessages = chatClient.getMessages();
|
||||
if (!newMessages.isEmpty()) {
|
||||
for (Message msg : newMessages) {
|
||||
@@ -146,16 +168,17 @@ public class ChatController {
|
||||
(_key) ->
|
||||
new ChatModel(
|
||||
ChatType.LOBBY,
|
||||
username,
|
||||
currentUsername,
|
||||
msg.lobbyId,
|
||||
null))
|
||||
.addMessage(msg);
|
||||
}
|
||||
break;
|
||||
case ChatType.WHISPER:
|
||||
if (msg.target.equals(username) || msg.sender.equals(username)) {
|
||||
if (msg.target.equals(currentUsername)
|
||||
|| msg.sender.equals(currentUsername)) {
|
||||
ChatKey key;
|
||||
if (msg.target.equals(username)) {
|
||||
if (msg.target.equals(currentUsername)) {
|
||||
key = new ChatKey(ChatType.WHISPER, msg.sender);
|
||||
} else {
|
||||
key = new ChatKey(ChatType.WHISPER, msg.target);
|
||||
@@ -166,7 +189,7 @@ public class ChatController {
|
||||
ChatModel chatModel =
|
||||
new ChatModel(
|
||||
ChatType.WHISPER,
|
||||
username,
|
||||
currentUsername,
|
||||
lobbyId,
|
||||
key.targetUser());
|
||||
chatBoxController.addWhisperChat(key.targetUser(), chatModel);
|
||||
@@ -203,13 +226,29 @@ public class ChatController {
|
||||
*/
|
||||
public synchronized void checkWhisperUsers() {
|
||||
List<String> users = chatClient.getUsers();
|
||||
logger.info(users);
|
||||
if (!users.isEmpty()) {
|
||||
for (String user : users) {
|
||||
String value = user.split("\\=")[1];
|
||||
logger.info(value);
|
||||
addWhisperUser(value);
|
||||
Set<String> remoteUsers = new HashSet<>();
|
||||
String currentUsername = getCurrentUsername();
|
||||
|
||||
for (String user : users) {
|
||||
if (user == null || !user.contains("=")) {
|
||||
continue;
|
||||
}
|
||||
String value = user.split("\\=", 2)[1].trim();
|
||||
if (value.isBlank() || value.equals(currentUsername)) {
|
||||
continue;
|
||||
}
|
||||
remoteUsers.add(value);
|
||||
}
|
||||
|
||||
for (String known : new ArrayList<>(localUserList)) {
|
||||
if (!remoteUsers.contains(known)) {
|
||||
localUserList.remove(known);
|
||||
chatBoxController.removeWhisperUser(known);
|
||||
}
|
||||
}
|
||||
|
||||
for (String remote : remoteUsers) {
|
||||
addWhisperUser(remote);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,16 +261,110 @@ public class ChatController {
|
||||
if (user == null || user.isBlank()) {
|
||||
return;
|
||||
}
|
||||
if (!(localUserList.contains(user) || user.equals(username))) {
|
||||
if (!(localUserList.contains(user) || user.equals(getCurrentUsername()))) {
|
||||
localUserList.add(user);
|
||||
logger.info("adding new whisper user");
|
||||
chatBoxController.addWhisperUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void updateUsername(String newUsername) {
|
||||
if (newUsername == null || newUsername.isBlank()) {
|
||||
return;
|
||||
}
|
||||
String oldUsername = this.username;
|
||||
this.username = newUsername.trim();
|
||||
chatBoxController.setUsername(this.username);
|
||||
|
||||
if (oldUsername != null && !oldUsername.equals(this.username)) {
|
||||
localUserList.remove(oldUsername);
|
||||
chatBoxController.removeWhisperUser(oldUsername);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleServerEvent(List<String> lines) {
|
||||
if (lines == null || lines.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<RequestParameter> params;
|
||||
try {
|
||||
params = ClientService.convertToRequestParameters(lines);
|
||||
} catch (RuntimeException e) {
|
||||
return;
|
||||
}
|
||||
String event = null;
|
||||
String oldUsername = null;
|
||||
String newUsername = null;
|
||||
|
||||
for (RequestParameter p : params) {
|
||||
if ("EVENT".equalsIgnoreCase(p.key())) {
|
||||
event = p.value();
|
||||
} else if ("OLD_USERNAME".equalsIgnoreCase(p.key())) {
|
||||
oldUsername = p.value();
|
||||
} else if ("NEW_USERNAME".equalsIgnoreCase(p.key())) {
|
||||
newUsername = p.value();
|
||||
}
|
||||
}
|
||||
|
||||
if (!"USERNAME_CHANGED".equalsIgnoreCase(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyUsernameMigration(oldUsername, newUsername);
|
||||
}
|
||||
|
||||
private synchronized void applyUsernameMigration(String oldUsername, String newUsername) {
|
||||
if (oldUsername == null
|
||||
|| newUsername == null
|
||||
|| oldUsername.isBlank()
|
||||
|| newUsername.isBlank()
|
||||
|| oldUsername.equals(newUsername)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String currentUsername = getCurrentUsername();
|
||||
if (oldUsername.equals(currentUsername)) {
|
||||
updateUsername(newUsername);
|
||||
}
|
||||
|
||||
ChatKey oldKey = new ChatKey(ChatType.WHISPER, oldUsername);
|
||||
ChatKey newKey = new ChatKey(ChatType.WHISPER, newUsername);
|
||||
|
||||
ChatModel oldModel = chatModelMap.remove(oldKey);
|
||||
ChatModel existingNewModel = chatModelMap.get(newKey);
|
||||
if (oldModel != null) {
|
||||
oldModel.setTarget(newUsername);
|
||||
if (existingNewModel == null) {
|
||||
chatModelMap.put(newKey, oldModel);
|
||||
} else {
|
||||
// Merge possible parallel history into the already existing new-key model.
|
||||
for (Message msg : oldModel.messages) {
|
||||
existingNewModel.addMessage(msg);
|
||||
}
|
||||
}
|
||||
chatBoxController.renameWhisperUser(oldUsername, newUsername);
|
||||
}
|
||||
|
||||
localUserList.remove(oldUsername);
|
||||
chatBoxController.removeWhisperUser(oldUsername);
|
||||
if (!newUsername.equals(getCurrentUsername())) {
|
||||
addWhisperUser(newUsername);
|
||||
}
|
||||
}
|
||||
|
||||
public String getCurrentUsername() {
|
||||
String shared = ClientApp.getSharedUsername();
|
||||
if (shared != null && !shared.isBlank()) {
|
||||
return shared.trim();
|
||||
}
|
||||
return username;
|
||||
}
|
||||
|
||||
/** Stops polling background tasks for this chat controller instance. */
|
||||
public void shutdown() {
|
||||
timer.cancel();
|
||||
clientService.removeEventListener(serverEventListener);
|
||||
synchronized (ACTIVE_CONTROLLERS) {
|
||||
if (ACTIVE_CONTROLLERS.get(clientService) == this) {
|
||||
ACTIVE_CONTROLLERS.remove(clientService);
|
||||
|
||||
@@ -22,7 +22,7 @@ public class ChatModel {
|
||||
public final String username;
|
||||
|
||||
/** The person to send the message to If the chat is a whisper chat */
|
||||
private final String target;
|
||||
private String target;
|
||||
|
||||
private final IntegerProperty count;
|
||||
|
||||
@@ -82,4 +82,8 @@ public class ChatModel {
|
||||
public String getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
public void setTarget(String target) {
|
||||
this.target = target;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.util.List;
|
||||
public class GameState {
|
||||
public List<Player> players = new ArrayList<>();
|
||||
public List<Card> communityCards = new ArrayList<>();
|
||||
public List<String> highscoreEntries = new ArrayList<>();
|
||||
|
||||
public String phase;
|
||||
public int pot;
|
||||
|
||||
@@ -155,6 +155,10 @@ public class GameClient {
|
||||
s.winnerIndex = intVal(l);
|
||||
yield true;
|
||||
}
|
||||
case String l when l.startsWith("HIGHSCORE=") -> {
|
||||
s.highscoreEntries.add(value(l));
|
||||
yield true;
|
||||
}
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -81,6 +81,26 @@ public class LobbyClient {
|
||||
throw new RuntimeException("No LOBBY_ID in response: " + lines);
|
||||
}
|
||||
|
||||
/** Fetches the global highscores from the server. */
|
||||
public List<String> getHighscores() {
|
||||
List<String> lines = client.processCommand("GET_HIGHSCORES");
|
||||
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||
|
||||
List<String> entries = new ArrayList<>();
|
||||
for (RequestParameter p : params) {
|
||||
if ("HIGHSCORE".equalsIgnoreCase(p.key())) {
|
||||
entries.add(p.value());
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/** Clears all global highscores on the server. */
|
||||
public void clearHighscores() {
|
||||
client.processCommand("CLEAR_HIGHSCORES");
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the server to return the id of the lobby that the client is currently in.
|
||||
*
|
||||
@@ -115,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);
|
||||
|
||||
@@ -131,6 +152,36 @@ 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.
|
||||
*
|
||||
* @param newUsername desired new username
|
||||
* @return a {@link LoginResult} containing assigned username and id as returned by the server
|
||||
*/
|
||||
public LoginResult changeUsername(String newUsername) {
|
||||
List<String> lines = client.processCommand("CHANGE_USERNAME USERNAME=" + newUsername);
|
||||
|
||||
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||
|
||||
String assigned = newUsername;
|
||||
String id = null;
|
||||
for (RequestParameter p : params) {
|
||||
if ("USERNAME".equalsIgnoreCase(p.key())) {
|
||||
assigned = p.value();
|
||||
} else if ("ID".equalsIgnoreCase(p.key())) {
|
||||
id = p.value();
|
||||
}
|
||||
}
|
||||
return new LoginResult(assigned, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the server for the list of available lobbies.
|
||||
*
|
||||
@@ -152,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(
|
||||
|
||||
+98
-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.
|
||||
@@ -60,6 +59,12 @@ public class ChatBoxController {
|
||||
usernameTabMap = new HashMap<>();
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
if (username != null && !username.isBlank()) {
|
||||
this.username = username.trim();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the chat interface by creating the global chat model and adding the corresponding
|
||||
* "GLOBAL" tab to the interface. It also registers the global chat in the {@link
|
||||
@@ -67,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());
|
||||
}
|
||||
|
||||
@@ -84,6 +88,11 @@ public class ChatBoxController {
|
||||
public void addWhisperUser(String targetUserName) {
|
||||
Platform.runLater(
|
||||
() -> {
|
||||
for (MenuItem existing : addWhisperChatButton.getItems()) {
|
||||
if (targetUserName.equals(existing.getText())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
MenuItem menuItem = new MenuItem(targetUserName);
|
||||
addWhisperChatButton.getItems().add(menuItem);
|
||||
menuItem.setOnAction(
|
||||
@@ -98,6 +107,54 @@ public class ChatBoxController {
|
||||
});
|
||||
}
|
||||
|
||||
public void removeWhisperUser(String targetUserName) {
|
||||
Platform.runLater(
|
||||
() ->
|
||||
addWhisperChatButton
|
||||
.getItems()
|
||||
.removeIf(item -> targetUserName.equals(item.getText())));
|
||||
}
|
||||
|
||||
public void renameWhisperUser(String oldUsername, String newUsername) {
|
||||
if (oldUsername == null
|
||||
|| newUsername == null
|
||||
|| oldUsername.isBlank()
|
||||
|| newUsername.isBlank()
|
||||
|| oldUsername.equals(newUsername)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Platform.runLater(
|
||||
() -> {
|
||||
for (MenuItem item : addWhisperChatButton.getItems()) {
|
||||
if (oldUsername.equals(item.getText())) {
|
||||
item.setText(newUsername);
|
||||
item.setOnAction(
|
||||
event ->
|
||||
addWhisperChat(
|
||||
newUsername,
|
||||
new ChatModel(
|
||||
ChatType.WHISPER,
|
||||
username,
|
||||
-1,
|
||||
newUsername)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Tab tab = usernameTabMap.remove(oldUsername);
|
||||
if (tab != null) {
|
||||
tab.setText(newUsername);
|
||||
usernameTabMap.put(newUsername, tab);
|
||||
}
|
||||
|
||||
int idx = activeWhisperChats.indexOf(oldUsername);
|
||||
if (idx >= 0) {
|
||||
activeWhisperChats.set(idx, newUsername);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new private (whisper) chat model for a specific target user, registers it within
|
||||
* the chat system, and opens a new chat tab.
|
||||
@@ -107,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));
|
||||
}
|
||||
@@ -125,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(
|
||||
@@ -134,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);
|
||||
@@ -180,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -68,11 +68,12 @@ public class ChatViewController implements Initializable {
|
||||
String message = inputField.getText().trim();
|
||||
if (!message.isEmpty()) {
|
||||
inputField.clear();
|
||||
String currentUsername = controller.getCurrentUsername();
|
||||
Message msg =
|
||||
new Message(
|
||||
chatModel.getChattype(),
|
||||
chatModel.lobbyId,
|
||||
username,
|
||||
currentUsername,
|
||||
chatModel.getTarget(),
|
||||
message);
|
||||
controller.onSendToNetwork(msg);
|
||||
|
||||
+549
-55
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -633,12 +770,13 @@ public class CasinoGameController {
|
||||
*/
|
||||
private void updateTaskbar(GameState s) {
|
||||
TaskbarController controller = resolveTaskbarController();
|
||||
if (controller != null) {
|
||||
if (gameService != null && myPlayerId != null) {
|
||||
controller.setGameService(gameService, myPlayerId);
|
||||
}
|
||||
controller.update(s, myPlayerId);
|
||||
if (controller == null || myPlayerId == null) {
|
||||
return;
|
||||
}
|
||||
if (gameService != null) {
|
||||
controller.setGameService(gameService, myPlayerId);
|
||||
}
|
||||
controller.update(s, myPlayerId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -649,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;
|
||||
@@ -676,6 +816,7 @@ public class CasinoGameController {
|
||||
safeRefresh(player1Controller);
|
||||
safeRefresh(player2Controller);
|
||||
safeRefresh(player3Controller);
|
||||
updateActiveTurnHighlights(players, activePlayerIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -700,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;
|
||||
@@ -772,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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1320,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)(\\?.*)?$");
|
||||
}
|
||||
}
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||
import java.util.List;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.stage.Stage;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/** Controller for the highscore popup window. */
|
||||
public class HighscoreViewController {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(HighscoreViewController.class);
|
||||
|
||||
@FXML private ListView<String> highscoreList;
|
||||
@FXML private Label statusLabel;
|
||||
@FXML private Node highscoreRoot;
|
||||
|
||||
private LobbyClient lobbyClient;
|
||||
private double dragOffsetX;
|
||||
private double dragOffsetY;
|
||||
|
||||
public void setLobbyClient(LobbyClient lobbyClient) {
|
||||
this.lobbyClient = lobbyClient;
|
||||
}
|
||||
|
||||
/** Starts dragging the window from anywhere in the popup. */
|
||||
@FXML
|
||||
public void onPopupPressed(MouseEvent event) {
|
||||
Stage stage = getStage();
|
||||
if (stage == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dragOffsetX = stage.getX() - event.getScreenX();
|
||||
dragOffsetY = stage.getY() - event.getScreenY();
|
||||
}
|
||||
|
||||
/** Moves the popup window while the mouse is dragged anywhere on the popup. */
|
||||
@FXML
|
||||
public void onPopupDragged(MouseEvent event) {
|
||||
Stage stage = getStage();
|
||||
if (stage == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
stage.setX(event.getScreenX() + dragOffsetX);
|
||||
stage.setY(event.getScreenY() + dragOffsetY);
|
||||
}
|
||||
|
||||
/** Returns the popup stage if the root is already attached to a scene. */
|
||||
private Stage getStage() {
|
||||
if (highscoreRoot == null || highscoreRoot.getScene() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (Stage) highscoreRoot.getScene().getWindow();
|
||||
}
|
||||
|
||||
/** Loads current highscores from the server and refreshes the list. */
|
||||
@FXML
|
||||
public void refreshHighscores() {
|
||||
if (highscoreList == null || statusLabel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lobbyClient == null) {
|
||||
statusLabel.setText("Lobby client not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
List<String> entries = lobbyClient.getHighscores();
|
||||
highscoreList.getItems().setAll(entries);
|
||||
statusLabel.setText(
|
||||
entries.isEmpty() ? "No highscores yet." : entries.size() + " entries loaded.");
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.error("Failed to load highscores: {}", e.getMessage());
|
||||
statusLabel.setText("Failed to load highscores.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Clears all highscores globally on the server and reloads the list. */
|
||||
@FXML
|
||||
public void clearHighscores() {
|
||||
if (lobbyClient == null || statusLabel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
lobbyClient.clearHighscores();
|
||||
refreshHighscores();
|
||||
statusLabel.setText("Highscores cleared.");
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.error("Failed to clear highscores: {}", e.getMessage());
|
||||
statusLabel.setText("Failed to clear highscores.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Closes the highscore popup window. */
|
||||
@FXML
|
||||
public void closeWindow() {
|
||||
if (highscoreList == null || highscoreList.getScene() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Stage stage = (Stage) highscoreList.getScene().getWindow();
|
||||
stage.close();
|
||||
}
|
||||
}
|
||||
+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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1226
-88
File diff suppressed because it is too large
Load Diff
+104
-8
@@ -4,12 +4,15 @@ 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.gameuicomponents.HighscoreViewController;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.Button;
|
||||
@@ -20,6 +23,8 @@ import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
@@ -50,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() {
|
||||
@@ -104,6 +109,13 @@ public class CasinomainuiController {
|
||||
casinoTable.getChildren().add(gridManager.getGridPane());
|
||||
gridManager.renderLobbyButtons();
|
||||
|
||||
String sharedUsername = ClientApp.getSharedUsername();
|
||||
updateUsernameFieldPresentation(sharedUsername);
|
||||
if (loginButton != null) {
|
||||
loginButton.setText(
|
||||
sharedUsername != null && !sharedUsername.isBlank() ? "Change Name" : "Login");
|
||||
}
|
||||
|
||||
initializeChat(clientService);
|
||||
}
|
||||
|
||||
@@ -129,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());
|
||||
}
|
||||
@@ -156,7 +169,7 @@ public class CasinomainuiController {
|
||||
/** Handles the login button action. Validates input and calls LobbyClient.login(). */
|
||||
@FXML
|
||||
public void handleLoginButton() {
|
||||
String username = usernameField.getText();
|
||||
String username = usernameField == null ? null : usernameField.getText();
|
||||
if (username == null || username.isBlank()) {
|
||||
showAlert("Please enter a username.");
|
||||
return;
|
||||
@@ -170,12 +183,63 @@ public class CasinomainuiController {
|
||||
showAlert("Offline mode: cannot send login to server.");
|
||||
return;
|
||||
}
|
||||
|
||||
String trimmed = username.trim();
|
||||
try {
|
||||
lobbyClient.login(username);
|
||||
showAlert("Login sent: " + username);
|
||||
var result = lobbyClient.login(trimmed);
|
||||
String assigned = result != null ? result.getUsername() : trimmed;
|
||||
ClientApp.updateSharedUsername(assigned);
|
||||
if (chatController != null) {
|
||||
chatController.updateUsername(assigned);
|
||||
}
|
||||
updateUsernameFieldPresentation(assigned);
|
||||
if (loginButton != null) {
|
||||
loginButton.setText("Change Name");
|
||||
}
|
||||
return;
|
||||
} catch (RuntimeException loginError) {
|
||||
if (!containsProtocolError(loginError, "ALREADY_LOGGED_IN")) {
|
||||
LOGGER.error("Login failed: {}", loginError.getMessage());
|
||||
showAlert("Login failed: " + loginError.getMessage());
|
||||
return;
|
||||
}
|
||||
LOGGER.info("Session already logged in, trying username change");
|
||||
}
|
||||
|
||||
try {
|
||||
var result = lobbyClient.changeUsername(trimmed);
|
||||
String assigned = result != null ? result.getUsername() : trimmed;
|
||||
ClientApp.updateSharedUsername(assigned);
|
||||
if (chatController != null) {
|
||||
chatController.updateUsername(assigned);
|
||||
}
|
||||
updateUsernameFieldPresentation(assigned);
|
||||
if (loginButton != null) {
|
||||
loginButton.setText("Change Name");
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.error("Login failed: {}", e.getMessage());
|
||||
showAlert("Login failed: " + e.getMessage());
|
||||
LOGGER.error("Change username failed: {}", e.getMessage());
|
||||
showAlert("Change username failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean containsProtocolError(RuntimeException error, String code) {
|
||||
String msg = error.getMessage();
|
||||
return msg != null && msg.contains(code);
|
||||
}
|
||||
|
||||
private void updateUsernameFieldPresentation(String username) {
|
||||
if (usernameField == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (username != null && !username.isBlank()) {
|
||||
usernameField.setText(username.trim());
|
||||
usernameField.positionCaret(usernameField.getText().length());
|
||||
usernameField.setPromptText("Username");
|
||||
} else {
|
||||
usernameField.clear();
|
||||
usernameField.setPromptText("Username");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,4 +293,36 @@ public class CasinomainuiController {
|
||||
LOGGER.error("Failed to create or add lobby: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** Opens the highscore popup from the lobby UI. */
|
||||
@FXML
|
||||
public void handleOpenHighscores() {
|
||||
try {
|
||||
FXMLLoader loader =
|
||||
new FXMLLoader(
|
||||
getClass()
|
||||
.getResource(
|
||||
"/ui-structure/gameuicomponents/HighscoreView.fxml"));
|
||||
Parent root = loader.load();
|
||||
|
||||
HighscoreViewController controller = loader.getController();
|
||||
controller.setLobbyClient(lobbyClient);
|
||||
controller.refreshHighscores();
|
||||
|
||||
Stage stage = new Stage();
|
||||
stage.initStyle(StageStyle.TRANSPARENT);
|
||||
stage.setTitle("Casono Highscores");
|
||||
String iconPath = getClass().getResource("/images/logoinverted.png").toExternalForm();
|
||||
stage.getIcons().add(new Image(iconPath));
|
||||
Scene scene = new Scene(root);
|
||||
scene.setFill(javafx.scene.paint.Color.TRANSPARENT);
|
||||
stage.setScene(scene);
|
||||
stage.show();
|
||||
stage.setAlwaysOnTop(true);
|
||||
stage.toFront();
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("Could not open highscore window: {}", e.getMessage());
|
||||
showAlert("Could not open highscore window.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,51 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username.Chang
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetParser;
|
||||
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.PlayerCallHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call.PlayerCallParser;
|
||||
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.PlayerFoldHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold.PlayerFoldParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold.PlayerFoldRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state.GetGameStateHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state.GetGameStateParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state.GetGameStateRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores.ClearHighscoresHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores.ClearHighscoresParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores.ClearHighscoresRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores.GetHighscoresHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores.GetHighscoresParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores.GetHighscoresRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby.CreateLobbyHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby.CreateLobbyParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby.CreateLobbyRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list.GetLobbyListHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list.GetLobbyListParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list.GetLobbyListRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status.GetLobbyStatusHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status.GetLobbyStatusParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status.GetLobbyStatusRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby.JoinLobbyHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby.JoinLobbyParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby.JoinLobbyRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.login.LoginHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.login.LoginParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.login.LoginRequest;
|
||||
@@ -27,20 +63,18 @@ 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;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandlerExecutor;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandRouter;
|
||||
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;
|
||||
@@ -104,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);
|
||||
@@ -204,152 +213,96 @@ public class ServerApp {
|
||||
ListUsersRequest.class, new ListUsersHandler(responseDispatcher, userRegistry));
|
||||
|
||||
// GET_LOBBY_LIST registration
|
||||
parserDispatcher.register(
|
||||
"GET_LOBBY_LIST",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list
|
||||
.GetLobbyListParser());
|
||||
parserDispatcher.register("GET_LOBBY_LIST", new GetLobbyListParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list
|
||||
.GetLobbyListRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
|
||||
.get_lobby_list.GetLobbyListRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list
|
||||
.GetLobbyListHandler(responseDispatcher, context.lobbyManager()));
|
||||
GetLobbyListRequest.class,
|
||||
(CommandHandler<GetLobbyListRequest>)
|
||||
new GetLobbyListHandler(responseDispatcher, context.lobbyManager()));
|
||||
|
||||
// GET_GAME_STATE registration
|
||||
parserDispatcher.register(
|
||||
"GET_GAME_STATE",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state
|
||||
.GetGameStateParser());
|
||||
parserDispatcher.register("GET_GAME_STATE", new GetGameStateParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state
|
||||
.GetGameStateRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game
|
||||
.get_game_state.GetGameStateRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state
|
||||
.GetGameStateHandler(
|
||||
GetGameStateRequest.class,
|
||||
(CommandHandler<GetGameStateRequest>)
|
||||
new GetGameStateHandler(
|
||||
responseDispatcher, context.lobbyManager(), userRegistry));
|
||||
|
||||
// BET registration
|
||||
parserDispatcher.register(
|
||||
"BET",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetParser());
|
||||
parserDispatcher.register("GET_HIGHSCORES", new GetHighscoresParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet
|
||||
.PlayerBetRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet
|
||||
.PlayerBetHandler(
|
||||
GetHighscoresRequest.class,
|
||||
(CommandHandler<GetHighscoresRequest>)
|
||||
new GetHighscoresHandler(responseDispatcher));
|
||||
|
||||
parserDispatcher.register("CLEAR_HIGHSCORES", new ClearHighscoresParser());
|
||||
commandRouter.register(
|
||||
ClearHighscoresRequest.class,
|
||||
(CommandHandler<ClearHighscoresRequest>)
|
||||
new ClearHighscoresHandler(responseDispatcher));
|
||||
|
||||
// BET registration
|
||||
parserDispatcher.register("BET", new PlayerBetParser());
|
||||
commandRouter.register(
|
||||
PlayerBetRequest.class,
|
||||
(CommandHandler<PlayerBetRequest>)
|
||||
new PlayerBetHandler(
|
||||
responseDispatcher, userRegistry, context.lobbyManager()));
|
||||
|
||||
// RAISE registration
|
||||
parserDispatcher.register(
|
||||
"RAISE",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise
|
||||
.PlayerRaiseParser());
|
||||
parserDispatcher.register("RAISE", new PlayerRaiseParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseRequest
|
||||
.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise
|
||||
.PlayerRaiseRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise
|
||||
.PlayerRaiseHandler(
|
||||
PlayerRaiseRequest.class,
|
||||
(CommandHandler<PlayerRaiseRequest>)
|
||||
new PlayerRaiseHandler(
|
||||
responseDispatcher, userRegistry, context.lobbyManager()));
|
||||
|
||||
// CALL registration
|
||||
parserDispatcher.register(
|
||||
"CALL",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call
|
||||
.PlayerCallParser());
|
||||
parserDispatcher.register("CALL", new PlayerCallParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call.PlayerCallRequest
|
||||
.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call
|
||||
.PlayerCallRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call
|
||||
.PlayerCallHandler(
|
||||
PlayerCallRequest.class,
|
||||
(CommandHandler<PlayerCallRequest>)
|
||||
new PlayerCallHandler(
|
||||
responseDispatcher, userRegistry, context.lobbyManager()));
|
||||
|
||||
// FOLD registration
|
||||
parserDispatcher.register(
|
||||
"FOLD",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold
|
||||
.PlayerFoldParser());
|
||||
parserDispatcher.register("FOLD", new PlayerFoldParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold.PlayerFoldRequest
|
||||
.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold
|
||||
.PlayerFoldRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold
|
||||
.PlayerFoldHandler(
|
||||
PlayerFoldRequest.class,
|
||||
(CommandHandler<PlayerFoldRequest>)
|
||||
new PlayerFoldHandler(
|
||||
responseDispatcher, userRegistry, context.lobbyManager()));
|
||||
|
||||
// GET_LOBBY_STATUS registration
|
||||
parserDispatcher.register(
|
||||
"GET_LOBBY_STATUS",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status
|
||||
.GetLobbyStatusParser());
|
||||
parserDispatcher.register("GET_LOBBY_STATUS", new GetLobbyStatusParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status
|
||||
.GetLobbyStatusRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
|
||||
.get_lobby_status.GetLobbyStatusRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
|
||||
.get_lobby_status.GetLobbyStatusHandler(
|
||||
GetLobbyStatusRequest.class,
|
||||
(CommandHandler<GetLobbyStatusRequest>)
|
||||
new GetLobbyStatusHandler(
|
||||
responseDispatcher, context.lobbyManager(), userRegistry));
|
||||
|
||||
// CREATE_LOBBY registration
|
||||
parserDispatcher.register(
|
||||
"CREATE_LOBBY",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby
|
||||
.CreateLobbyParser());
|
||||
parserDispatcher.register("CREATE_LOBBY", new CreateLobbyParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby
|
||||
.CreateLobbyRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
|
||||
.create_lobby.CreateLobbyRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby
|
||||
.CreateLobbyHandler(
|
||||
CreateLobbyRequest.class,
|
||||
(CommandHandler<CreateLobbyRequest>)
|
||||
new CreateLobbyHandler(
|
||||
responseDispatcher,
|
||||
context.lobbyManager(),
|
||||
context.sessionManager()));
|
||||
|
||||
// JOIN_LOBBY registration
|
||||
parserDispatcher.register(
|
||||
"JOIN_LOBBY",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||
.JoinLobbyParser());
|
||||
parserDispatcher.register("JOIN_LOBBY", new JoinLobbyParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||
.JoinLobbyRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||
.JoinLobbyRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||
.JoinLobbyHandler(
|
||||
JoinLobbyRequest.class,
|
||||
(CommandHandler<JoinLobbyRequest>)
|
||||
new JoinLobbyHandler(
|
||||
responseDispatcher, context.lobbyManager(), userRegistry));
|
||||
|
||||
// START_GAME registration
|
||||
parserDispatcher.register(
|
||||
"START_GAME",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
|
||||
.StartGameParser());
|
||||
parserDispatcher.register("START_GAME", new StartGameParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
|
||||
.StartGameRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
|
||||
.StartGameRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
|
||||
.StartGameHandler(
|
||||
StartGameRequest.class,
|
||||
(CommandHandler<StartGameRequest>)
|
||||
new StartGameHandler(
|
||||
responseDispatcher, context.lobbyManager(), userRegistry));
|
||||
}
|
||||
}
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+40
-1
@@ -8,6 +8,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.Player;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GamePhase;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.highscore.HighscoreService;
|
||||
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;
|
||||
@@ -50,7 +51,11 @@ public class GetGameStateResponse extends SuccessResponse {
|
||||
builder.param("CURRENT_BET", computeGlobalCurrentBet(state));
|
||||
builder.param("DEALER", state.getDealerIndex());
|
||||
builder.param("ACTIVE_PLAYER", state.getCurrentPlayerIndex());
|
||||
builder.param("WINNER", computeWinnerIndex(state, game));
|
||||
int winnerIndex = computeWinnerIndex(state, game);
|
||||
builder.param("WINNER", winnerIndex);
|
||||
|
||||
appendWinnerToHighscoresIfFinished(state, winnerIndex);
|
||||
appendHighscoreEntries(builder);
|
||||
|
||||
appendCommunityCards(builder, state);
|
||||
|
||||
@@ -59,6 +64,40 @@ public class GetGameStateResponse extends SuccessResponse {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static void appendWinnerToHighscoresIfFinished(GameState state, int winnerIndex) {
|
||||
if (winnerIndex < 0 || state.getPhase() != GamePhase.FINISHED) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.tryMarkWinnerPersistedForHand()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Player winner = findPlayerByIndex(state, winnerIndex);
|
||||
if (winner == null || winner.getId() == null || winner.getId().value() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
HighscoreService.getInstance().appendWinner(winner.getId().value());
|
||||
}
|
||||
|
||||
private static Player findPlayerByIndex(GameState state, int winnerIndex) {
|
||||
int currentIndex = 0;
|
||||
for (Player player : state.getPlayers()) {
|
||||
if (currentIndex == winnerIndex) {
|
||||
return player;
|
||||
}
|
||||
currentIndex++;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void appendHighscoreEntries(ResponseBodyBuilder builder) {
|
||||
for (String entry : HighscoreService.getInstance().readFormattedEntries()) {
|
||||
builder.param("HIGHSCORE", entry);
|
||||
}
|
||||
}
|
||||
|
||||
private static int computeWinnerIndex(GameState state, GameController game) {
|
||||
GamePhase phase = state.getPhase();
|
||||
if (phase != GamePhase.SHOWDOWN && phase != GamePhase.FINISHED) {
|
||||
|
||||
+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();
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.highscore.HighscoreService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
|
||||
/** Handler for CLEAR_HIGHSCORES. */
|
||||
public class ClearHighscoresHandler extends CommandHandler<ClearHighscoresRequest> {
|
||||
private final HighscoreService highscoreService;
|
||||
|
||||
public ClearHighscoresHandler(ResponseDispatcher responseDispatcher) {
|
||||
super(responseDispatcher);
|
||||
this.highscoreService = HighscoreService.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(ClearHighscoresRequest request) {
|
||||
highscoreService.clearAll();
|
||||
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||
|
||||
/** Parser for CLEAR_HIGHSCORES. */
|
||||
public class ClearHighscoresParser implements CommandParser<ClearHighscoresRequest> {
|
||||
@Override
|
||||
public ClearHighscoresRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
return new ClearHighscoresRequest(primitiveRequest.context());
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
|
||||
/** Request object for CLEAR_HIGHSCORES. */
|
||||
public class ClearHighscoresRequest extends Request {
|
||||
public ClearHighscoresRequest(RequestContext context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.highscore.HighscoreService;
|
||||
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;
|
||||
|
||||
/** Handler for GET_HIGHSCORES. */
|
||||
public class GetHighscoresHandler extends CommandHandler<GetHighscoresRequest> {
|
||||
private final HighscoreService highscoreService;
|
||||
|
||||
public GetHighscoresHandler(ResponseDispatcher responseDispatcher) {
|
||||
super(responseDispatcher);
|
||||
this.highscoreService = HighscoreService.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(GetHighscoresRequest request) {
|
||||
responseDispatcher.dispatch(
|
||||
new GetHighscoresResponse(
|
||||
request.getContext(), highscoreService.readFormattedEntries()));
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||
|
||||
/** Parser for GET_HIGHSCORES. */
|
||||
public class GetHighscoresParser implements CommandParser<GetHighscoresRequest> {
|
||||
@Override
|
||||
public GetHighscoresRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
return new GetHighscoresRequest(primitiveRequest.context());
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
|
||||
/** Request object for GET_HIGHSCORES. */
|
||||
public class GetHighscoresRequest extends Request {
|
||||
public GetHighscoresRequest(RequestContext context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
|
||||
import java.util.List;
|
||||
|
||||
/** Success response containing repeated HIGHSCORE parameters. */
|
||||
public class GetHighscoresResponse extends SuccessResponse {
|
||||
public GetHighscoresResponse(RequestContext context, List<String> entries) {
|
||||
super(
|
||||
context,
|
||||
ResponseBody.builder()
|
||||
.block(
|
||||
"HIGHSCORES",
|
||||
block -> {
|
||||
for (String entry : entries) {
|
||||
block.param("HIGHSCORE", entry);
|
||||
}
|
||||
})
|
||||
.build());
|
||||
}
|
||||
}
|
||||
+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));
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -5,10 +5,12 @@ 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;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
import java.util.Optional;
|
||||
|
||||
public class SendMessageHandler extends CommandHandler<SendMessageRequest> {
|
||||
private final UserRegistry userRegistry;
|
||||
@@ -39,6 +41,10 @@ public class SendMessageHandler extends CommandHandler<SendMessageRequest> {
|
||||
@Override
|
||||
public void execute(SendMessageRequest request) {
|
||||
Message message = request.getMessage();
|
||||
Optional<User> senderUser = userRegistry.getBySessionId(request.getSessionId());
|
||||
if (senderUser.isPresent()) {
|
||||
message.sender = senderUser.get().getName();
|
||||
}
|
||||
broadcast(request, message);
|
||||
OkResponse response = new OkResponse(request.getContext());
|
||||
responseDispatcher.dispatch(response);
|
||||
|
||||
+45
-2
@@ -10,6 +10,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Deck;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.GameEngine;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.evaluator.HandEvaluator;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.evaluator.HandRank;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.Player;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GamePhase;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
|
||||
@@ -61,6 +62,35 @@ public class GameController {
|
||||
engine.getState().addPlayer(name, chips);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a player id in the controller list and underlying game state.
|
||||
*
|
||||
* @param oldId old player id
|
||||
* @param newId new player id
|
||||
* @return true if rename succeeded
|
||||
*/
|
||||
public boolean renamePlayer(PlayerId oldId, PlayerId newId) {
|
||||
if (oldId == null || newId == null) {
|
||||
return false;
|
||||
}
|
||||
if (oldId.equals(newId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int idx = players.indexOf(oldId);
|
||||
if (idx < 0 || players.contains(newId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean stateRenamed = engine.getState().renamePlayerId(oldId, newId);
|
||||
if (!stateRenamed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
players.set(idx, newId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new hand by preparing the deck, setting the phase to PREFLOP, rotating the
|
||||
* dealer, dealing hole cards, posting blinds, and setting the first active player.
|
||||
@@ -154,9 +184,22 @@ public class GameController {
|
||||
*
|
||||
* @param playerId The ID of the player who is folding.
|
||||
*/
|
||||
public void playerFold(PlayerId playerId) {
|
||||
// engine.processAction(new FoldAction(PlayerId.of(playerId)));
|
||||
public PlayerId playerFold(PlayerId playerId) {
|
||||
|
||||
engine.processAction(new FoldAction(playerId));
|
||||
|
||||
GameState state = engine.getState();
|
||||
|
||||
if (state.countNonFoldedPlayers() == 1) {
|
||||
|
||||
for (Player p : state.getPlayers()) {
|
||||
if (!p.isFolded()) {
|
||||
return p.getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+29
@@ -1,6 +1,9 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.Action;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.ActionType;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.Player;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.RuleEngine;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
|
||||
|
||||
@@ -80,12 +83,38 @@ public class GameEngine {
|
||||
*/
|
||||
public void handleAction(GameState state, Action action) {
|
||||
|
||||
if (action.getType() != ActionType.BLIND) {
|
||||
|
||||
Player currentPlayer = state.getCurrentPlayer();
|
||||
PlayerId actingPlayerId = action.getPlayerId();
|
||||
|
||||
if (currentPlayer == null || actingPlayerId == null) {
|
||||
throw new RuntimeException("Invalid turn state");
|
||||
}
|
||||
|
||||
if (!state.isAllowOutOfTurn() && !currentPlayer.getId().equals(actingPlayerId)) {
|
||||
throw new RuntimeException("Not your turn");
|
||||
}
|
||||
|
||||
Player actingPlayer = state.getPlayer(actingPlayerId);
|
||||
if (actingPlayer.isFolded()) {
|
||||
throw new RuntimeException("Player already folded");
|
||||
}
|
||||
if (actingPlayer.isAllIn()) {
|
||||
throw new RuntimeException("Player is all-in");
|
||||
}
|
||||
}
|
||||
|
||||
// 1.Check the rules
|
||||
ruleEngine.validate(state, action);
|
||||
|
||||
// 2. Perform action
|
||||
action.execute(state);
|
||||
|
||||
if (action.getType() != ActionType.BLIND) {
|
||||
state.markActedThisRound(action.getPlayerId());
|
||||
}
|
||||
|
||||
// 3. Next turn
|
||||
turnManager.nextPlayer(state);
|
||||
|
||||
|
||||
+90
-10
@@ -4,7 +4,6 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Card;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Deck;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.Player;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerStatus;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GamePhase;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
|
||||
import java.util.ArrayList;
|
||||
@@ -27,6 +26,13 @@ public class RoundManager {
|
||||
public static final int SMALL_BLIND = 100;
|
||||
public static final int BIG_BLIND = 200;
|
||||
|
||||
/**
|
||||
* Starts a new hand by resetting the game state, ensuring a deck is available, dealing hole
|
||||
* cards to players, posting blinds, and setting the first player to act for the preflop phase.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which will be
|
||||
* modified to
|
||||
*/
|
||||
public void startNewHand(GameState state) {
|
||||
// Use GameState's canonical reset
|
||||
state.startNewHand();
|
||||
@@ -44,12 +50,19 @@ public class RoundManager {
|
||||
state.setCurrentPlayerToPreflopFirstToAct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current betting round is finished and advances the game phase if necessary.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which may be
|
||||
* modified to advance the phase or end the hand.
|
||||
*/
|
||||
public void progressIfNeeded(GameState state) {
|
||||
if (state.getPhase() == null) {
|
||||
state.setPhase(GamePhase.PREFLOP);
|
||||
}
|
||||
|
||||
if (state.countNonFoldedPlayers() == 1) {
|
||||
state.setHandActive(false);
|
||||
state.setPhase(GamePhase.FINISHED);
|
||||
return;
|
||||
}
|
||||
@@ -59,27 +72,51 @@ public class RoundManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current betting round is finished by checking if all active players have
|
||||
* met the current bet or are all-in and by handling special cases for preflop betting rounds.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which is used
|
||||
* to evaluate the betting round status.
|
||||
* @return true if the betting round is finished and the game can progress to the next phase,
|
||||
* false otherwise.
|
||||
*/
|
||||
private boolean isBettingRoundFinished(GameState state) {
|
||||
int target = state.getTableState().getCurrentBet();
|
||||
// A betting round only ends after every active player had at least one chance to act.
|
||||
return allActivePlayersActedThisRound(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to check if all active players have acted in the current betting round. This is
|
||||
* used to handle the special case of preflop rounds where no bets have been made yet, but
|
||||
* players still need to have the opportunity to act.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which is used
|
||||
* to check if all active players have acted in the current round.
|
||||
* @return true if all active players have acted in the current round, false if there are still
|
||||
* active players who have not acted yet.
|
||||
*/
|
||||
private boolean allActivePlayersActedThisRound(GameState state) {
|
||||
for (Player p : state.getPlayers()) {
|
||||
if (p == null) {
|
||||
if (p == null || p.isFolded() || p.isAllIn()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// be tolerant if codebase mixes status + boolean flags
|
||||
if (p.getStatus() == PlayerStatus.FOLDED || p.isFolded()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int bet = state.getCurrentBet(p.getId());
|
||||
if (bet < target && !p.isAllIn()) {
|
||||
if (!state.hasActedThisRound(p.getId())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the game phase to the next stage (FLOP, TURN, RIVER, SHOWDOWN) based on the current
|
||||
* phase.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which will be
|
||||
* modified to advance the phase and deal community cards as needed when progressing to the
|
||||
* next stage of the hand.
|
||||
*/
|
||||
private void advancePhase(GameState state) {
|
||||
switch (state.getPhase()) {
|
||||
case PREFLOP -> dealFlop(state);
|
||||
@@ -93,6 +130,12 @@ public class RoundManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that a deck of cards is available in the game state.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which will be
|
||||
* modified to include a new shuffled deck if one does not already exist.
|
||||
*/
|
||||
private void ensureDeck(GameState state) {
|
||||
Deck deck = state.getDeck();
|
||||
if (deck == null) {
|
||||
@@ -102,6 +145,13 @@ public class RoundManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deals hole cards to each player in the game state by drawing two cards from the deck for each
|
||||
* player and assigning them as their hole cards.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which will be
|
||||
* modified to assign hole cards to each active player.
|
||||
*/
|
||||
private void dealHoleCards(GameState state) {
|
||||
Deck deck = state.getDeck();
|
||||
|
||||
@@ -165,6 +215,14 @@ public class RoundManager {
|
||||
state.getTableState().setCurrentBet(BIG_BLIND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deals the flop by drawing three community cards from the deck and adding them to the game
|
||||
* state, then setting the game phase to FLOP and resetting bets for the new betting round.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which will be
|
||||
* modified to add three community cards for the flop, set the phase to FLOP, and reset bets
|
||||
* for the new betting round.
|
||||
*/
|
||||
private void dealFlop(GameState state) {
|
||||
ensureDeck(state);
|
||||
Deck deck = state.getDeck();
|
||||
@@ -180,6 +238,14 @@ public class RoundManager {
|
||||
state.setCurrentPlayerToPostflopFirstToAct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deals the turn by drawing one community card from the deck and adding it to the game state,
|
||||
* then setting the game phase to TURN and resetting bets for the new betting round.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which will be
|
||||
* modified to add one community card for the turn, set the phase to TURN, and reset bets
|
||||
* for the new betting round.
|
||||
*/
|
||||
private void dealTurn(GameState state) {
|
||||
ensureDeck(state);
|
||||
Deck deck = state.getDeck();
|
||||
@@ -193,6 +259,14 @@ public class RoundManager {
|
||||
state.setCurrentPlayerToPostflopFirstToAct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deals the river by drawing one community card from the deck and adding it to the game state,
|
||||
* then setting the game phase to RIVER and resetting bets for the new betting round.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which will be
|
||||
* modified to add one community card for the river, set the phase to RIVER, and reset bets
|
||||
* for the new betting round.
|
||||
*/
|
||||
private void dealRiver(GameState state) {
|
||||
ensureDeck(state);
|
||||
Deck deck = state.getDeck();
|
||||
@@ -206,6 +280,12 @@ public class RoundManager {
|
||||
state.setCurrentPlayerToPostflopFirstToAct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the showdown phase by setting the game phase to SHOWDOWN.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which will be
|
||||
* modified to set the phase to SHOWDOWN.
|
||||
*/
|
||||
private void showdown(GameState state) {
|
||||
state.setPhase(GamePhase.SHOWDOWN);
|
||||
|
||||
|
||||
@@ -49,6 +49,16 @@ public class Player {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the player's id. This is used when a username change is propagated into an already
|
||||
* running game.
|
||||
*
|
||||
* @param id new player id
|
||||
*/
|
||||
public void setId(PlayerId id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the display name of the player. Currently identical to the player ID.
|
||||
*
|
||||
|
||||
+292
-12
@@ -7,8 +7,10 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* The GameState class encapsulates the entire state of a poker game at any given moment.
|
||||
@@ -33,11 +35,13 @@ public class GameState {
|
||||
private int currentPlayerIndex;
|
||||
private boolean handActive;
|
||||
private boolean allowOutOfTurn;
|
||||
private boolean winnerPersistedForHand;
|
||||
private GamePhase phase;
|
||||
private int dealerIndex;
|
||||
|
||||
private final Map<PlayerId, Integer> currentBets = new HashMap<>();
|
||||
private final Map<PlayerId, Integer> playerBetCommitments = new HashMap<>();
|
||||
private final Set<PlayerId> actedThisRound = new HashSet<>();
|
||||
|
||||
private final Map<PlayerId, List<Card>> holeCards = new HashMap<>();
|
||||
private final List<Card> communityCards = new ArrayList<>();
|
||||
@@ -46,7 +50,11 @@ public class GameState {
|
||||
|
||||
private static final int DEALER_OFFSET = 3;
|
||||
|
||||
// Getters
|
||||
/**
|
||||
* Returns the players in seating/turn order.
|
||||
*
|
||||
* @return a collection of players in the order they are seated/act.
|
||||
*/
|
||||
public Collection<Player> getPlayers() {
|
||||
List<Player> ordered = new ArrayList<>(playerOrder.size());
|
||||
for (PlayerId id : playerOrder) {
|
||||
@@ -58,57 +66,123 @@ public class GameState {
|
||||
return ordered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current player whose turn it is to act.
|
||||
*
|
||||
* @return the current player, or null if there are no players or the index is out of bounds.
|
||||
*/
|
||||
public Player getCurrentPlayer() {
|
||||
PlayerId id = playerOrder.get(currentPlayerIndex);
|
||||
return players.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the current player in the player order list.
|
||||
*
|
||||
* @return the index of the current player, or 0 if there are no players.
|
||||
*/
|
||||
public int getCurrentPlayerIndex() {
|
||||
return currentPlayerIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a hand is currently active (i.e., in progress).
|
||||
*
|
||||
* @return true if a hand is active, false otherwise.
|
||||
*/
|
||||
public boolean isHandActive() {
|
||||
return handActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current phase of the game (e.g., PREFLOP, FLOP, TURN, RIVER).
|
||||
*
|
||||
* @return the current game phase.
|
||||
*/
|
||||
public GamePhase getPhase() {
|
||||
return phase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current state of the table, including information such as the current bet and pot
|
||||
* size.
|
||||
*
|
||||
* @return the current table state.
|
||||
*/
|
||||
public TableState getTableState() {
|
||||
return tableState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current pot, which includes the total amount of chips in the pot and the
|
||||
* contributions from each player.
|
||||
*
|
||||
* @return the current pot.
|
||||
*/
|
||||
public Pot getPot() {
|
||||
return pot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current deck of cards being used in the game.
|
||||
*
|
||||
* @return the current deck of cards.
|
||||
*/
|
||||
public Deck getDeck() {
|
||||
return deck;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of community cards currently on the table.
|
||||
*
|
||||
* @return a list of community cards, which may be empty if no cards have been dealt yet.
|
||||
*/
|
||||
public List<Card> getCommunityCards() {
|
||||
return communityCards;
|
||||
}
|
||||
|
||||
/** Always returns a mutable list (never null). */
|
||||
/**
|
||||
* Returns the hole cards for the specified player. If the player does not have hole cards yet,
|
||||
* an empty list is returned and stored in the map for future reference.
|
||||
*
|
||||
* @param playerId the ID of the player whose hole cards are being requested.
|
||||
* @return a list of hole cards for the specified player, which may be empty if the player has
|
||||
* not been dealt cards yet.
|
||||
*/
|
||||
public List<Card> getHoleCards(PlayerId playerId) {
|
||||
return holeCards.computeIfAbsent(playerId, k -> new ArrayList<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the dealer in the player order list.
|
||||
*
|
||||
* @return the index of the dealer, or 0 if there are no players.
|
||||
*/
|
||||
public int getDealerIndex() {
|
||||
return dealerIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map of player IDs to their respective hole cards. Each player's hole cards are
|
||||
* represented as a list of Card objects. If a player does not have hole cards yet, they will be
|
||||
* associated with an empty list.
|
||||
*
|
||||
* @return a map where the key is the player's ID and the value is a list of their hole cards.
|
||||
*/
|
||||
public Map<PlayerId, List<Card>> getPlayerCards() {
|
||||
return holeCards;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of players currently in the game, based on the size of the player order
|
||||
* list.
|
||||
*
|
||||
* @return the number of players in the game.
|
||||
*/
|
||||
public int getPlayerCount() {
|
||||
return players.size();
|
||||
}
|
||||
|
||||
// Setters / Mutators
|
||||
public void setCurrentPlayerIndex(int index) {
|
||||
if (playerOrder.isEmpty()) {
|
||||
this.currentPlayerIndex = 0;
|
||||
@@ -117,6 +191,10 @@ public class GameState {
|
||||
this.currentPlayerIndex = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current player index to the first player who should act preflop, which is determined
|
||||
* based on the dealer index and the number of players.
|
||||
*/
|
||||
public void setCurrentPlayerToPreflopFirstToAct() {
|
||||
int size = playerOrder.size();
|
||||
if (size == 0) {
|
||||
@@ -127,11 +205,16 @@ public class GameState {
|
||||
// Heads-up: dealer (small blind) acts first preflop.
|
||||
int first = (size == 2) ? dealerIndex : (dealerIndex + DEALER_OFFSET) % size;
|
||||
currentPlayerIndex = first;
|
||||
resetRoundActions();
|
||||
if (!canPlayerAct(currentPlayerIndex)) {
|
||||
nextPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current player index to the first player who should act postflop, which is the
|
||||
* player immediately to the left of the dealer (i.e., dealerIndex + 1).
|
||||
*/
|
||||
public void setCurrentPlayerToPostflopFirstToAct() {
|
||||
int size = playerOrder.size();
|
||||
if (size == 0) {
|
||||
@@ -141,27 +224,86 @@ public class GameState {
|
||||
|
||||
int first = (dealerIndex + 1) % size;
|
||||
currentPlayerIndex = first;
|
||||
resetRoundActions();
|
||||
if (!canPlayerAct(currentPlayerIndex)) {
|
||||
nextPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the specified player as having acted in the current round.
|
||||
*
|
||||
* @param playerId the ID of the player to mark as having acted this round.
|
||||
*/
|
||||
public void markActedThisRound(PlayerId playerId) {
|
||||
if (playerId != null) {
|
||||
actedThisRound.add(playerId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the specified player has already acted in the current round by checking if their ID
|
||||
* is present in the set of players who have acted this round.
|
||||
*
|
||||
* @param playerId the ID of the player to check for having acted this round.
|
||||
* @return true if the player has acted this round, false otherwise.
|
||||
*/
|
||||
public boolean hasActedThisRound(PlayerId playerId) {
|
||||
return playerId != null && actedThisRound.contains(playerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the tracking of which players have acted this round by clearing the set of player IDs.
|
||||
*/
|
||||
public void resetRoundActions() {
|
||||
actedThisRound.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether a hand is currently active (i.e., in progress).
|
||||
*
|
||||
* @param handActive a boolean value indicating whether a hand is active (true) or not (false).
|
||||
*/
|
||||
public void setHandActive(boolean handActive) {
|
||||
this.handActive = handActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current phase of the game (e.g., PREFLOP, FLOP, TURN, RIVER).
|
||||
*
|
||||
* @param phase the GamePhase to set as the current phase of the game.
|
||||
*/
|
||||
public void setPhase(GamePhase phase) {
|
||||
this.phase = phase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the index of the dealer in the player order list.
|
||||
*
|
||||
* @param dealerIndex the index to set as the dealer index, which should be within the bounds of
|
||||
* the player order list if it is not empty.
|
||||
*/
|
||||
public void setDealerIndex(int dealerIndex) {
|
||||
this.dealerIndex = dealerIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current deck of cards being used in the game.
|
||||
*
|
||||
* @param deck the Deck object to set as the current deck of cards for the game. This should be
|
||||
* a valid Deck instance that can be used for dealing cards during the game.
|
||||
*/
|
||||
public void setDeck(Deck deck) {
|
||||
this.deck = deck;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new player to the game with the specified ID and initial chip count.
|
||||
*
|
||||
* @param id the PlayerId of the new player to add to the game.
|
||||
* @param chips the initial number of chips the player has, which can be used for betting during
|
||||
* the game.
|
||||
*/
|
||||
public void addPlayer(PlayerId id, int chips) {
|
||||
Player player = new Player(id, chips);
|
||||
|
||||
@@ -174,18 +316,87 @@ public class GameState {
|
||||
holeCards.computeIfAbsent(id, k -> new ArrayList<>());
|
||||
}
|
||||
|
||||
// Betting
|
||||
/**
|
||||
* Renames a player id across all game-state structures.
|
||||
*
|
||||
* @param oldId existing player id
|
||||
* @param newId new player id
|
||||
* @return true if the rename was applied, false otherwise
|
||||
*/
|
||||
public synchronized boolean renamePlayerId(PlayerId oldId, PlayerId newId) {
|
||||
if (oldId == null || newId == null) {
|
||||
return false;
|
||||
}
|
||||
if (!players.containsKey(oldId)) {
|
||||
return false;
|
||||
}
|
||||
if (oldId.equals(newId)) {
|
||||
return true;
|
||||
}
|
||||
if (players.containsKey(newId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Player player = players.remove(oldId);
|
||||
if (player == null) {
|
||||
return false;
|
||||
}
|
||||
player.setId(newId);
|
||||
players.put(newId, player);
|
||||
|
||||
int idx = playerOrder.indexOf(oldId);
|
||||
if (idx >= 0) {
|
||||
playerOrder.set(idx, newId);
|
||||
}
|
||||
|
||||
moveMapEntry(currentBets, oldId, newId, 0);
|
||||
moveMapEntry(playerBetCommitments, oldId, newId, 0);
|
||||
moveMapEntry(holeCards, oldId, newId, new ArrayList<>());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to move an entry in a map from an old key to a new key, with a fallback value
|
||||
* if the old key is not present.
|
||||
*
|
||||
* @param map the map in which to move the entry, where the key is a PlayerId and the value is
|
||||
* of type T.
|
||||
* @param oldId the existing PlayerId key that is being renamed.
|
||||
* @param newId the new PlayerId key to which the entry should be moved.
|
||||
* @param fallback the value to use if the oldId is not present in the map.
|
||||
* @param <T> the type of the values in the map, which can be any type that is used for
|
||||
* player-related data in the game state.
|
||||
*/
|
||||
private <T> void moveMapEntry(
|
||||
Map<PlayerId, T> map, PlayerId oldId, PlayerId newId, T fallback) {
|
||||
T value = map.remove(oldId);
|
||||
map.put(newId, value != null ? value : fallback);
|
||||
}
|
||||
|
||||
// BETTING
|
||||
|
||||
/**
|
||||
* Returns the current bet amount for the specified player.
|
||||
*
|
||||
* @param playerId the ID of the player whose current bet is being requested.
|
||||
* @return the current bet amount for the specified player, or 0 if the player does not have a
|
||||
* current bet.
|
||||
*/
|
||||
public int getCurrentBet(PlayerId playerId) {
|
||||
return currentBets.getOrDefault(playerId, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current bet amount for the specified player.
|
||||
*
|
||||
* @param playerId the ID of the player whose current bet is being set.
|
||||
* @param amount the new bet amount to set for the specified player.
|
||||
*/
|
||||
public void setCurrentBet(PlayerId playerId, int amount) {
|
||||
currentBets.put(playerId, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset bets to 0 for ALL players (do not clear the map). Also resets table current bet to 0.
|
||||
*/
|
||||
/** Reset bets to 0 for ALL players (do not clear the map). */
|
||||
public void resetBets() {
|
||||
for (PlayerId id : playerOrder) {
|
||||
currentBets.put(id, 0);
|
||||
@@ -193,10 +404,20 @@ public class GameState {
|
||||
tableState.setCurrentBet(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the specified amount to the pot.
|
||||
*
|
||||
* @param amount the amount to add to the pot.
|
||||
*/
|
||||
public void addToPot(int amount) {
|
||||
pot.add(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether out-of-turn actions are allowed in the current game state.
|
||||
*
|
||||
* @return true if out-of-turn actions are allowed, false otherwise.
|
||||
*/
|
||||
public boolean isAllowOutOfTurn() {
|
||||
return allowOutOfTurn;
|
||||
}
|
||||
@@ -205,7 +426,13 @@ public class GameState {
|
||||
this.allowOutOfTurn = allowOutOfTurn;
|
||||
}
|
||||
|
||||
// Player helpers
|
||||
/**
|
||||
* Retrieves the Player object associated with the given PlayerId.
|
||||
*
|
||||
* @param id the PlayerId of the player to retrieve.
|
||||
* @return the Player object associated with the specified PlayerId, or a RuntimeException if
|
||||
* the player is not found.
|
||||
*/
|
||||
public Player getPlayer(PlayerId id) {
|
||||
Player player = players.get(id);
|
||||
if (player == null) {
|
||||
@@ -215,7 +442,13 @@ public class GameState {
|
||||
return player;
|
||||
}
|
||||
|
||||
// Bet commitments
|
||||
/**
|
||||
* Retrieves the current bet commitment for the specified player.
|
||||
*
|
||||
* @param playerId the ID of the player whose current bet commitment is being requested.
|
||||
* @return the current bet commitment for the specified player, or 0 if the player does not have
|
||||
* a bet commitment.
|
||||
*/
|
||||
public int getCurrentBetCommitment(PlayerId playerId) {
|
||||
return playerBetCommitments.getOrDefault(playerId, 0);
|
||||
}
|
||||
@@ -224,7 +457,7 @@ public class GameState {
|
||||
playerBetCommitments.put(playerId, amount);
|
||||
}
|
||||
|
||||
// Cards
|
||||
// CARDS
|
||||
|
||||
/**
|
||||
* Gives (overwrites) the two hole cards for the given player. Ensures stable list identity
|
||||
@@ -237,15 +470,21 @@ public class GameState {
|
||||
cards.add(c2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a community card to the list of community cards on the table.
|
||||
*
|
||||
* @param card the Card object representing the community card to be added to the table.
|
||||
*/
|
||||
public void addCommunityCard(Card card) {
|
||||
communityCards.add(card);
|
||||
}
|
||||
|
||||
/** Resets the community cards by clearing the list of community cards on the table. */
|
||||
public void resetCommunityCards() {
|
||||
communityCards.clear();
|
||||
}
|
||||
|
||||
// Turn / dealer management
|
||||
/** Advances the current player index to the next player who can act. */
|
||||
public void nextPlayer() {
|
||||
if (playerOrder.isEmpty()) {
|
||||
currentPlayerIndex = 0;
|
||||
@@ -263,6 +502,14 @@ public class GameState {
|
||||
} while (currentPlayerIndex != start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the player at the specified index in the player order list is able to take an
|
||||
* action.
|
||||
*
|
||||
* @param index the index of the player in the player order list to check for actionability.
|
||||
* @return true if the player at the specified index can act (i.e., is not folded and not
|
||||
* all-in), false otherwise.
|
||||
*/
|
||||
private boolean canPlayerAct(int index) {
|
||||
if (index < 0 || index >= playerOrder.size()) {
|
||||
return false;
|
||||
@@ -277,12 +524,19 @@ public class GameState {
|
||||
dealerIndex = (dealerIndex + 1) % playerOrder.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Player object representing the current dealer based on the dealer index in the
|
||||
* player order list.
|
||||
*
|
||||
* @return the Player object representing the current dealer, or null if there are no players or
|
||||
* the dealer index is out of bounds.
|
||||
*/
|
||||
public Player getDealer() {
|
||||
PlayerId id = playerOrder.get(dealerIndex);
|
||||
return players.get(id);
|
||||
}
|
||||
|
||||
// Hand reset / lifecycle
|
||||
// HAND RESET / LIFECYCLE
|
||||
|
||||
/**
|
||||
* Starts a new hand by resetting the game state for the next round of poker.
|
||||
@@ -298,6 +552,7 @@ public class GameState {
|
||||
*/
|
||||
public void startNewHand() {
|
||||
handActive = true;
|
||||
resetWinnerPersistedForHand();
|
||||
phase = GamePhase.PREFLOP;
|
||||
|
||||
pot = new Pot();
|
||||
@@ -322,10 +577,35 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the specified player has folded by retrieving their Player object and checking
|
||||
* their folded status.
|
||||
*
|
||||
* @param playerId the ID of the player to check for folded status.
|
||||
* @return true if the player has folded, false otherwise.
|
||||
*/
|
||||
public boolean isFolded(PlayerId playerId) {
|
||||
return getPlayer(playerId).isFolded();
|
||||
}
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.highscore;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Stores and reads persistent highscore entries in timestamp + winner format. */
|
||||
public final class HighscoreService {
|
||||
|
||||
private static final HighscoreService INSTANCE = new HighscoreService();
|
||||
private static final DateTimeFormatter DISPLAY_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
|
||||
private static final int MAX_RETURNED_ENTRIES = 100;
|
||||
private static final String LINE_SEPARATOR = "\t";
|
||||
|
||||
private final Path storagePath;
|
||||
|
||||
private HighscoreService() {
|
||||
this.storagePath =
|
||||
Path.of(System.getProperty("user.home"), ".casono", "highscores", "winners.log");
|
||||
}
|
||||
|
||||
public static HighscoreService getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public synchronized void appendWinner(String winnerName) {
|
||||
if (winnerName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String sanitized = sanitizeName(winnerName);
|
||||
if (sanitized.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String line = Instant.now() + LINE_SEPARATOR + sanitized + System.lineSeparator();
|
||||
|
||||
try {
|
||||
Files.createDirectories(storagePath.getParent());
|
||||
Files.writeString(
|
||||
storagePath,
|
||||
line,
|
||||
StandardCharsets.UTF_8,
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.APPEND);
|
||||
} catch (IOException ignored) {
|
||||
// Highscore persistence must never break the game state response path.
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized List<String> readFormattedEntries() {
|
||||
if (!Files.exists(storagePath)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<String> lines;
|
||||
try {
|
||||
lines = Files.readAllLines(storagePath, StandardCharsets.UTF_8);
|
||||
} catch (IOException ignored) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<String> formatted = new ArrayList<>();
|
||||
|
||||
for (String raw : lines) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] parts = raw.split(LINE_SEPARATOR, 2);
|
||||
if (parts.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
Instant ts = Instant.parse(parts[0].trim());
|
||||
String display = DISPLAY_FORMATTER.format(ts) + " | " + parts[1].trim();
|
||||
formatted.add(display);
|
||||
} catch (Exception ignored) {
|
||||
// Skip malformed lines and keep all valid entries.
|
||||
}
|
||||
}
|
||||
|
||||
int from = Math.max(0, formatted.size() - MAX_RETURNED_ENTRIES);
|
||||
return new ArrayList<>(formatted.subList(from, formatted.size()));
|
||||
}
|
||||
|
||||
public synchronized void clearAll() {
|
||||
try {
|
||||
Files.deleteIfExists(storagePath);
|
||||
} catch (IOException ignored) {
|
||||
// Clearing highscores must not break request handling.
|
||||
}
|
||||
}
|
||||
|
||||
private static String sanitizeName(String winnerName) {
|
||||
return winnerName.trim().replace("\n", " ").replace("\r", " ").replace("\t", " ");
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,30 @@ public class Lobby {
|
||||
return playerNames.remove(playerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a player in this lobby's player list.
|
||||
*
|
||||
* @param oldName old username
|
||||
* @param newName new username
|
||||
* @return true if renamed successfully
|
||||
*/
|
||||
public boolean renamePlayer(String oldName, String newName) {
|
||||
if (oldName == null || newName == null) {
|
||||
return false;
|
||||
}
|
||||
synchronized (playerNames) {
|
||||
if (oldName.equals(newName)) {
|
||||
return playerNames.contains(oldName);
|
||||
}
|
||||
int idx = playerNames.indexOf(oldName);
|
||||
if (idx < 0 || playerNames.contains(newName)) {
|
||||
return false;
|
||||
}
|
||||
playerNames.set(idx, newName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void initGame(GameController controller) {
|
||||
this.gameController = controller;
|
||||
}
|
||||
|
||||
+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,5 +1,6 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby.AddResult;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
@@ -170,6 +171,56 @@ public class LobbyManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a player across lobby mapping, lobby player list and running game ids.
|
||||
*
|
||||
* @param oldUsername old username
|
||||
* @param newUsername new username
|
||||
* @return true if rename was applied
|
||||
*/
|
||||
public synchronized boolean renamePlayer(String oldUsername, String newUsername) {
|
||||
if (oldUsername == null || newUsername == null) {
|
||||
return false;
|
||||
}
|
||||
if (oldUsername.equals(newUsername)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
LobbyId lobbyId = playerToLobby.get(oldUsername);
|
||||
if (lobbyId == null) {
|
||||
return true;
|
||||
}
|
||||
if (playerToLobby.containsKey(newUsername)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Lobby lobby = activeLobbies.get(lobbyId);
|
||||
if (lobby == null) {
|
||||
playerToLobby.remove(oldUsername);
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean lobbyRenamed = lobby.renamePlayer(oldUsername, newUsername);
|
||||
if (!lobbyRenamed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (lobby.getGameController() != null) {
|
||||
boolean gameRenamed =
|
||||
lobby.getGameController()
|
||||
.renamePlayer(PlayerId.of(oldUsername), PlayerId.of(newUsername));
|
||||
if (!gameRenamed) {
|
||||
// Best-effort rollback to keep structures consistent.
|
||||
lobby.renamePlayer(newUsername, oldUsername);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
playerToLobby.remove(oldUsername);
|
||||
playerToLobby.put(newUsername, lobbyId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given action to every player username in the lobby identified by {@code lobbyId}.
|
||||
* This is a small helper that keeps iteration logic centralized and avoids leaking internal
|
||||
|
||||
@@ -200,6 +200,10 @@ public class UserRegistry {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.getName().equals(newName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (byName.containsKey(newName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
+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>
|
||||
|
||||
@@ -81,25 +81,32 @@
|
||||
</GridPane.margin>
|
||||
</Button>
|
||||
|
||||
<!-- Username input and login button -->
|
||||
<!-- <HBox alignment="CENTER_LEFT" spacing="10"-->
|
||||
<!-- GridPane.rowIndex="0"-->
|
||||
<!-- GridPane.columnIndex="0"-->
|
||||
<!-- GridPane.halignment="LEFT"-->
|
||||
<!-- GridPane.valignment="TOP">-->
|
||||
<!-- <GridPane.margin>-->
|
||||
<!-- <!– place below the 'CREATE A LOBBY' button –>-->
|
||||
<!-- <Insets top="100" left="20" />-->
|
||||
<!-- </GridPane.margin>-->
|
||||
<!-- <TextField fx:id="usernameField"-->
|
||||
<!-- promptText="Username"-->
|
||||
<!-- styleClass="gray-input-field"-->
|
||||
<!-- maxWidth="180" />-->
|
||||
<!-- <Button text="Login"-->
|
||||
<!-- fx:id="loginButton"-->
|
||||
<!-- onAction="#handleLoginButton"-->
|
||||
<!-- styleClass="button-create-lobby" />-->
|
||||
<!-- </HBox>-->
|
||||
<!-- Username input + change name, with highscores directly below -->
|
||||
<VBox alignment="TOP_LEFT" spacing="10"
|
||||
GridPane.rowIndex="0"
|
||||
GridPane.columnIndex="0"
|
||||
GridPane.halignment="LEFT"
|
||||
GridPane.valignment="TOP">
|
||||
<GridPane.margin>
|
||||
<Insets top="140" left="20" />
|
||||
</GridPane.margin>
|
||||
|
||||
<HBox alignment="CENTER_LEFT" spacing="10">
|
||||
<TextField fx:id="usernameField"
|
||||
promptText="Username"
|
||||
styleClass="gray-input-field"
|
||||
maxWidth="180" />
|
||||
<Button text="Apply Name"
|
||||
fx:id="loginButton"
|
||||
onAction="#handleLoginButton"
|
||||
styleClass="button-create-lobby" />
|
||||
</HBox>
|
||||
|
||||
<Button text="HIGHSCORES"
|
||||
styleClass="button-create-lobby"
|
||||
fx:id="highscoreButton"
|
||||
onAction="#handleOpenHighscores" />
|
||||
</VBox>
|
||||
|
||||
<VBox fx:id="casinoTable"
|
||||
alignment="CENTER"
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
.highscore-root {
|
||||
-fx-background-color: rgba(13, 158, 59, 0.96);
|
||||
-fx-background-radius: 16;
|
||||
-fx-border-radius: 16;
|
||||
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||
-fx-border-width: 3;
|
||||
}
|
||||
|
||||
.highscore-title {
|
||||
-fx-text-fill: #ffffff;
|
||||
-fx-font-size: 24px;
|
||||
-fx-font-weight: bold;
|
||||
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);
|
||||
}
|
||||
|
||||
.highscore-status {
|
||||
-fx-text-fill: #ffffff;
|
||||
-fx-font-size: 13px;
|
||||
}
|
||||
|
||||
.list-view {
|
||||
-fx-background-color: #114d26;
|
||||
-fx-control-inner-background: #114d26;
|
||||
-fx-background-insets: 0;
|
||||
-fx-background-radius: 12;
|
||||
-fx-border-radius: 12;
|
||||
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||
-fx-border-width: 2;
|
||||
}
|
||||
|
||||
.list-view .list-cell {
|
||||
-fx-background-color: transparent;
|
||||
-fx-text-fill: #ffe89a;
|
||||
-fx-font-family: "Courier New";
|
||||
-fx-font-size: 13px;
|
||||
}
|
||||
|
||||
.list-view .list-cell:filled {
|
||||
-fx-background-color: rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
|
||||
.list-view .list-cell:filled:hover {
|
||||
-fx-background-color: rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.list-view .list-cell:filled:selected,
|
||||
.list-view .list-cell:filled:selected:focused {
|
||||
-fx-background-color: rgba(0, 0, 0, 0.35);
|
||||
-fx-text-fill: #ffffff;
|
||||
}
|
||||
|
||||
.gray-button,
|
||||
.yellow-button,
|
||||
.red-button {
|
||||
-fx-background-radius: 10;
|
||||
-fx-border-radius: 10;
|
||||
-fx-font-family: "Courier New";
|
||||
-fx-font-weight: bold;
|
||||
-fx-border-width: 2;
|
||||
-fx-padding: 6 12;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.gray-button {
|
||||
-fx-background-color: #333333;
|
||||
-fx-border-color: #666666;
|
||||
-fx-text-fill: #cccccc;
|
||||
}
|
||||
|
||||
.yellow-button {
|
||||
-fx-background-color: #b8860b;
|
||||
-fx-border-color: #ffd700;
|
||||
-fx-text-fill: #ffffff;
|
||||
}
|
||||
|
||||
.red-button {
|
||||
-fx-background-color: #8b0000;
|
||||
-fx-border-color: #ff4444;
|
||||
-fx-text-fill: #ffffff;
|
||||
}
|
||||
|
||||
.highscore-root {
|
||||
-fx-background-color: #0d9e3b;
|
||||
-fx-background-radius: 16;
|
||||
-fx-border-radius: 16;
|
||||
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||
-fx-border-width: 3;
|
||||
}
|
||||
|
||||
.highscore-title {
|
||||
-fx-text-fill: #ffffff;
|
||||
-fx-font-size: 22px;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.highscore-subtitle {
|
||||
-fx-text-fill: #f2f2f2;
|
||||
-fx-font-size: 13px;
|
||||
}
|
||||
|
||||
.highscore-text-area {
|
||||
-fx-control-inner-background: #1b1b1b;
|
||||
-fx-text-fill: #ffffff;
|
||||
-fx-font-family: "Courier New";
|
||||
-fx-font-size: 14px;
|
||||
-fx-border-color: #666666;
|
||||
-fx-border-width: 2;
|
||||
-fx-border-radius: 10;
|
||||
-fx-background-radius: 10;
|
||||
}
|
||||
|
||||
.gray-button,
|
||||
.green-button,
|
||||
.red-button {
|
||||
-fx-background-radius: 10;
|
||||
-fx-border-radius: 10;
|
||||
-fx-font-family: "Courier New";
|
||||
-fx-font-weight: bold;
|
||||
-fx-border-width: 2;
|
||||
-fx-padding: 6 14;
|
||||
}
|
||||
|
||||
.green-button {
|
||||
-fx-background-color: #1f7d33;
|
||||
-fx-border-color: #35c759;
|
||||
-fx-text-fill: #ffffff;
|
||||
}
|
||||
|
||||
.red-button {
|
||||
-fx-background-color: #8b0000;
|
||||
-fx-border-color: #ff4444;
|
||||
-fx-text-fill: #ffffff;
|
||||
}
|
||||
|
||||
.gray-button {
|
||||
-fx-background-color: #333333;
|
||||
-fx-border-color: #666666;
|
||||
-fx-text-fill: #cccccc;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ListView?>
|
||||
<?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"
|
||||
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.HighscoreViewController"
|
||||
spacing="12"
|
||||
styleClass="highscore-root"
|
||||
onMousePressed="#onPopupPressed"
|
||||
onMouseDragged="#onPopupDragged"
|
||||
stylesheets="@HighscoreView.css">
|
||||
|
||||
<padding>
|
||||
<Insets top="14" right="14" bottom="14" left="14"/>
|
||||
</padding>
|
||||
|
||||
<Label text="Highscores" styleClass="highscore-title"/>
|
||||
|
||||
<ListView fx:id="highscoreList" prefHeight="320" VBox.vgrow="ALWAYS"/>
|
||||
|
||||
<Label fx:id="statusLabel" text="Ready." styleClass="highscore-status"/>
|
||||
|
||||
<HBox spacing="10">
|
||||
<Button text="Refresh" onAction="#refreshHighscores" styleClass="gray-button" HBox.hgrow="ALWAYS" maxWidth="Infinity"/>
|
||||
<Button text="Clear" onAction="#clearHighscores" styleClass="yellow-button" HBox.hgrow="ALWAYS" maxWidth="Infinity"/>
|
||||
<Button text="Close" onAction="#closeWindow" styleClass="red-button" HBox.hgrow="ALWAYS" maxWidth="Infinity"/>
|
||||
</HBox>
|
||||
|
||||
</VBox>
|
||||
@@ -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"
|
||||
@@ -19,7 +18,7 @@
|
||||
|
||||
<!-- Placeholder buttons -->
|
||||
<Button text="SETTINGS" styleClass="gray-button" />
|
||||
<Button text="INFO" styleClass="gray-button" />
|
||||
<Button text="HIGHSCORE" onAction="#onHighscoreButtonClick" styleClass="gray-button" />
|
||||
<!-- Buten for the Casono Browser -->
|
||||
<Button text="HELP" onAction="#onBrowserButtonClick" styleClass="gray-button" />
|
||||
|
||||
@@ -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
-5
@@ -1,8 +1,7 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
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.*;
|
||||
|
||||
@@ -10,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);
|
||||
|
||||
+152
-21
@@ -37,6 +37,127 @@ public class GameControllerTest {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(GameControllerTest.class.getName());
|
||||
|
||||
/**
|
||||
* Helper method to retrieve the current player's ID from the game state.
|
||||
*
|
||||
* @param game The GameController instance from which to retrieve the current player's ID.
|
||||
* @return The PlayerId of the current player in the game.
|
||||
*/
|
||||
private static PlayerId currentPlayerId(GameController game) {
|
||||
return game.getState().getCurrentPlayer().getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to simulate a call action for the current player in the game.
|
||||
*
|
||||
* @param game The GameController instance on which to perform the call action for the current
|
||||
* player.
|
||||
*/
|
||||
private static void callCurrent(GameController game) {
|
||||
game.playerCall(currentPlayerId(game));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to simulate a fold action for the current player in the game.
|
||||
*
|
||||
* @param game The GameController instance on which to perform the fold action for the current
|
||||
* player.
|
||||
*/
|
||||
private static void foldCurrent(GameController game) {
|
||||
game.playerFold(currentPlayerId(game));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the preflop phase ends once all active (non-folded) players have completed
|
||||
* their required actions for the current betting round.
|
||||
*/
|
||||
@Test
|
||||
public void testPreflopEndsAfterOneActionPerActivePlayer() {
|
||||
|
||||
GameState state = new GameState();
|
||||
GameEngine engine =
|
||||
new GameEngine(
|
||||
state,
|
||||
new RuleEngine(new ArrayList<>()),
|
||||
new RoundManager(),
|
||||
new TurnManager());
|
||||
|
||||
GameController game = new GameController(engine);
|
||||
game.addPlayer(PlayerId.of("P1"), 10000);
|
||||
game.addPlayer(PlayerId.of("P2"), 10000);
|
||||
game.addPlayer(PlayerId.of("P3"), 10000);
|
||||
game.addPlayer(PlayerId.of("P4"), 10000);
|
||||
|
||||
game.startGame();
|
||||
|
||||
assertEquals(GamePhase.PREFLOP, game.getState().getPhase());
|
||||
assertEquals(0, game.getCommunityCards().size());
|
||||
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
|
||||
assertEquals(GamePhase.PREFLOP, game.getState().getPhase());
|
||||
assertEquals(0, game.getCommunityCards().size());
|
||||
|
||||
callCurrent(game);
|
||||
|
||||
assertEquals(GamePhase.FLOP, game.getState().getPhase());
|
||||
assertEquals(3, game.getCommunityCards().size());
|
||||
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
|
||||
assertEquals(GamePhase.TURN, game.getState().getPhase());
|
||||
assertEquals(4, game.getCommunityCards().size());
|
||||
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
|
||||
assertEquals(GamePhase.RIVER, game.getState().getPhase());
|
||||
assertEquals(5, game.getCommunityCards().size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that during the preflop phase, folded players are excluded from progression checks
|
||||
* and do not block phase advancement.
|
||||
*/
|
||||
@Test
|
||||
public void testPreflopCountsOnlyNonFoldedPlayersForProgress() {
|
||||
|
||||
GameState state = new GameState();
|
||||
GameEngine engine =
|
||||
new GameEngine(
|
||||
state,
|
||||
new RuleEngine(new ArrayList<>()),
|
||||
new RoundManager(),
|
||||
new TurnManager());
|
||||
|
||||
GameController game = new GameController(engine);
|
||||
game.addPlayer(PlayerId.of("A"), 10000);
|
||||
game.addPlayer(PlayerId.of("B"), 10000);
|
||||
game.addPlayer(PlayerId.of("C"), 10000);
|
||||
game.addPlayer(PlayerId.of("D"), 10000);
|
||||
|
||||
game.startGame();
|
||||
assertEquals(GamePhase.PREFLOP, game.getState().getPhase());
|
||||
|
||||
callCurrent(game);
|
||||
foldCurrent(game);
|
||||
callCurrent(game);
|
||||
|
||||
assertEquals(GamePhase.PREFLOP, game.getState().getPhase());
|
||||
|
||||
callCurrent(game);
|
||||
|
||||
assertEquals(GamePhase.FLOP, game.getState().getPhase());
|
||||
assertEquals(3, game.getCommunityCards().size());
|
||||
}
|
||||
|
||||
/**
|
||||
* This test simulates a specific game scenario where Julian is expected to win with a Two Pair
|
||||
* hand. It sets up a fixed deck to ensure that the desired cards are dealt to the players and
|
||||
@@ -83,11 +204,14 @@ public class GameControllerTest {
|
||||
}
|
||||
}
|
||||
|
||||
game.playerCall(PlayerId.of("Julian"));
|
||||
callCurrent(game);
|
||||
game.playerFold(PlayerId.of("Mathis"));
|
||||
game.playerCall(PlayerId.of("Jona"));
|
||||
callCurrent(game);
|
||||
game.playerRaise(PlayerId.of("Lars"), 1200);
|
||||
|
||||
// New street model: once all non-folded players acted, preflop is complete.
|
||||
assertEquals(GamePhase.FLOP, game.getState().getPhase());
|
||||
|
||||
List<Card> board = game.getCommunityCards();
|
||||
assertTrue(board.size() >= 3 && board.size() <= 5);
|
||||
|
||||
@@ -170,10 +294,10 @@ public class GameControllerTest {
|
||||
|
||||
int potBefore = game.getState().getPot().getAmount();
|
||||
|
||||
game.playerRaise(PlayerId.of("Lars"), 1200);
|
||||
game.playerCall(PlayerId.of("Julian"));
|
||||
game.playerFold(PlayerId.of("Mathis"));
|
||||
game.playerCall(PlayerId.of("Jona"));
|
||||
game.playerRaise(currentPlayerId(game), 1200);
|
||||
callCurrent(game);
|
||||
foldCurrent(game);
|
||||
callCurrent(game);
|
||||
|
||||
System.out.println("Pot after actions: " + game.getState().getPot().getAmount());
|
||||
|
||||
@@ -183,11 +307,15 @@ public class GameControllerTest {
|
||||
|
||||
assertEquals(3, game.getCommunityCards().size(), "The flop must have 3 cards");
|
||||
|
||||
game.playerCall(PlayerId.of("Julian"));
|
||||
for (int i = 0; i < 10 && game.getCommunityCards().size() < 4; i++) {
|
||||
callCurrent(game);
|
||||
}
|
||||
|
||||
assertEquals(4, game.getCommunityCards().size(), "A turn must result in 4 cards");
|
||||
|
||||
game.playerCall(PlayerId.of("Jona"));
|
||||
for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) {
|
||||
callCurrent(game);
|
||||
}
|
||||
|
||||
assertEquals(5, game.getCommunityCards().size(), "The river must consist of 5 cards");
|
||||
|
||||
@@ -278,13 +406,12 @@ public class GameControllerTest {
|
||||
game.playerCall(PlayerId.of("Jona"));
|
||||
game.playerRaise(PlayerId.of("Lars"), 1200);
|
||||
|
||||
game.playerCall(PlayerId.of("Julian"));
|
||||
game.playerCall(PlayerId.of("Jona"));
|
||||
assertEquals(GamePhase.FLOP, game.getState().getPhase());
|
||||
|
||||
assertTrue(game.getCommunityCards().size() >= 3);
|
||||
|
||||
for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) {
|
||||
game.playerCall(PlayerId.of("Julian"));
|
||||
callCurrent(game);
|
||||
}
|
||||
|
||||
assertEquals(5, game.getCommunityCards().size());
|
||||
@@ -367,10 +494,14 @@ public class GameControllerTest {
|
||||
|
||||
assertEquals(3, game.getCommunityCards().size());
|
||||
|
||||
game.playerCall(PlayerId.of("Julian"));
|
||||
for (int i = 0; i < 10 && game.getCommunityCards().size() < 4; i++) {
|
||||
callCurrent(game);
|
||||
}
|
||||
assertEquals(4, game.getCommunityCards().size());
|
||||
|
||||
game.playerCall(PlayerId.of("Mathis"));
|
||||
for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) {
|
||||
callCurrent(game);
|
||||
}
|
||||
assertEquals(5, game.getCommunityCards().size());
|
||||
|
||||
Set<String> boardSeen = new HashSet<>();
|
||||
@@ -424,10 +555,10 @@ public class GameControllerTest {
|
||||
|
||||
game.startGame();
|
||||
|
||||
game.playerRaise(PlayerId.of("BigStack"), 1000);
|
||||
game.playerCall(PlayerId.of("Caller"));
|
||||
game.playerCall(PlayerId.of("MidStack"));
|
||||
game.playerCall(PlayerId.of("ShortStack"));
|
||||
game.playerRaise(currentPlayerId(game), 1000);
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
|
||||
for (Player p : game.getState().getPlayers()) {
|
||||
assertTrue(p.getChips() >= 0, "Negative chips detected for " + p.getId());
|
||||
@@ -435,13 +566,13 @@ public class GameControllerTest {
|
||||
|
||||
assertTrue(game.getState().getPot().getAmount() > 0);
|
||||
|
||||
game.playerRaise(PlayerId.of("BigStack"), 100);
|
||||
game.playerRaise(currentPlayerId(game), 100);
|
||||
|
||||
int pot = game.getState().getPot().getAmount();
|
||||
assertTrue(pot > 0, "Pot must remain valid after re-raises");
|
||||
|
||||
game.playerFold(PlayerId.of("Caller"));
|
||||
game.playerFold(PlayerId.of("MidStack"));
|
||||
foldCurrent(game);
|
||||
foldCurrent(game);
|
||||
|
||||
long activePlayers =
|
||||
game.getState().getPlayers().stream().filter(p -> p.getChips() > 0).count();
|
||||
@@ -451,7 +582,7 @@ public class GameControllerTest {
|
||||
assertTrue(game.getCommunityCards().size() >= 3);
|
||||
|
||||
for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) {
|
||||
game.playerCall(PlayerId.of("BigStack"));
|
||||
callCurrent(game);
|
||||
}
|
||||
|
||||
assertEquals(5, game.getCommunityCards().size());
|
||||
|
||||
+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