Compare commits
167 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f1624a30b | |||
| 363a0ed895 | |||
| ecf692fbcd | |||
| 4fd036173f | |||
| d36a78eee4 | |||
| 8484f731a8 | |||
| b158be26e3 | |||
| 59af75a75c | |||
| 823b406fe6 | |||
| 71f2d3c2ce | |||
| eb2b209e91 | |||
| 5663807e29 | |||
| fe83057049 | |||
| e9b40e9d04 | |||
| b7f5a268e3 | |||
| 619f6eef84 | |||
| d8d6dbb34f | |||
| efd91fbe5f | |||
| ad2accfced | |||
| 4c35596f19 | |||
| 659e606fb4 | |||
| 5358309e1a | |||
| dc2b3e05d9 | |||
| d6ad60d6a7 | |||
| ddb3e221ac | |||
| 5d0a9b9c25 | |||
| 52ad32a100 | |||
| 822d9a0e1e | |||
| 216c15e825 | |||
| 30927d471e | |||
| 86ab82ebfd | |||
| 6a06369d94 | |||
| 48bfef5fcb | |||
| a6e98b2453 | |||
| 54a3f1e29d | |||
| 1b18affd57 | |||
| 5fc3f315fc | |||
| ac9396cfb2 | |||
| c6772d6f89 | |||
| d86b9b5838 | |||
| d91ee5b04f | |||
| 6b5d521ab9 | |||
| 4ad9ae08c5 | |||
| 1107542c54 | |||
| 2ad99d8732 | |||
| d791e9b2d1 | |||
| 3cd1e6925e | |||
| a7e4411cd0 | |||
| bf979ab391 | |||
| 588bb1f312 | |||
| 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 | |||
| ebca653ab4 | |||
| 9df48478f3 | |||
| 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 | |||
| 192c8a2cb9 | |||
| 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 | |||
| 190b30672a | |||
| 11be6e2b9b | |||
| a8599cfb4c | |||
| 08b9f652be | |||
| 3252c7e534 | |||
| 181f538038 |
@@ -0,0 +1,11 @@
|
||||
# Besprechung vom 26.04.2024
|
||||
|
||||
## Fortschritt für die Implementierung der Unit Tests
|
||||
Die Unit Tests für alle zentralen Komponenten des Netzwerkes wurden erfolgreich implementiert.
|
||||
Bei dieser Gelegenheit wurden auch Bugs behoben und die Codequalität verbessert.
|
||||
|
||||
In einzelnen Bereichen wurde noch nicht die vorgenommene Quote von 50% erreicht, weitere Unit-Tests sollen bis zum nächsten Meilenstein folgen. Komponenten beteiligt an den Kernbereichen sind größtenteils abgedeckt.
|
||||
|
||||
## Perspektivische Änderungen für den letzten Meilenstein
|
||||
Ein getroffener Kompromiss um den vierten Meilenstein fristgerecht zu erreichen, war das zurückstellen der Codequalität.
|
||||
In der Zeit bis zum sechsten Meilenstein sollen diese Provisorien noch einmal überarbeitet und finalisiert werden.
|
||||
@@ -36,7 +36,9 @@ None, as milestone 3 covered the assessment of individual technical understandin
|
||||
|
||||
|
||||
## Milestone 5
|
||||
*(Meeting notes to be added here when available)*
|
||||
### [26. April 2026](26-04.md)
|
||||
- Progress on unit tests and code quality improvements
|
||||
- Discussion of compromises made for MS4 and plans for finalizing code quality by MS6
|
||||
|
||||
|
||||
## Milestone 6
|
||||
|
||||
@@ -1,29 +1,94 @@
|
||||
# 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.
|
||||
<!-- vim-markdown-toc GFM -->
|
||||
|
||||
* [1. Spielübersicht](#1-spielübersicht)
|
||||
* [Grundregeln](#grundregeln)
|
||||
* [2. Sitzposition & Dealer-Button](#2-sitzposition--dealer-button)
|
||||
* [3. Blinds (Pflichteinsätze)](#3-blinds-pflichteinsätze)
|
||||
* [4. Spielablauf im Detail](#4-spielablauf-im-detail)
|
||||
* [4.1 Preflop (erste Setzrunde)](#41-preflop-erste-setzrunde)
|
||||
* [4.2 Flop (3 Gemeinschaftskarten)](#42-flop-3-gemeinschaftskarten)
|
||||
* [4.3 Turn (4. Gemeinschaftskarte)](#43-turn-4-gemeinschaftskarte)
|
||||
* [4.4 River (5. Gemeinschaftskarte)](#44-river-5-gemeinschaftskarte)
|
||||
* [5. Showdown (Kartenvergleich)](#5-showdown-kartenvergleich)
|
||||
* [6. Poker-Handrangfolge](#6-poker-handrangfolge)
|
||||
* [7. Wichtige Grundprinzipien](#7-wichtige-grundprinzipien)
|
||||
* [Reihenfolge beachten](#reihenfolge-beachten)
|
||||
* [Klare Aktionen](#klare-aktionen)
|
||||
* [Ein Spieler – eine Hand](#ein-spieler--eine-hand)
|
||||
* [Fehlerhafte Einsätze](#fehlerhafte-einsätze)
|
||||
* [8. Strategische Einordnung](#8-strategische-einordnung)
|
||||
* [Start Game](#start-game)
|
||||
* [Server starten](#server-starten)
|
||||
* [Client starten](#client-starten)
|
||||
* [UI](#ui)
|
||||
* [Lobby UI](#lobby-ui)
|
||||
* [Eine Lobby erstellen](#eine-lobby-erstellen)
|
||||
* [Einer Lobby beitreten](#einer-lobby-beitreten)
|
||||
* [Den Username ändern](#den-username-ändern)
|
||||
* [Highscores](#highscores)
|
||||
* [Game UI](#game-ui)
|
||||
* [Casono Browser](#casono-browser)
|
||||
* [Casono Rules](#casono-rules)
|
||||
* [1. Spielübersicht](#1-spielübersicht)
|
||||
* [Grundregeln](#grundregeln)
|
||||
* [2. Sitzposition & Dealer-Button](#2-sitzposition--dealer-button)
|
||||
* [3. Blinds (Pflichteinsätze)](#3-blinds-pflichteinsätze)
|
||||
* [4. Spielablauf im Detail](#4-spielablauf-im-detail)
|
||||
* [4.1 Preflop (erste Setzrunde)](#41-preflop-erste-setzrunde)
|
||||
* [4.2 Flop (3 Gemeinschaftskarten)](#42-flop-3-gemeinschaftskarten)
|
||||
* [4.3 Turn (4. Gemeinschaftskarte)](#43-turn-4-gemeinschaftskarte)
|
||||
* [4.4 River (5. Gemeinschaftskarte)](#44-river-5-gemeinschaftskarte)
|
||||
* [5. Showdown (Kartenvergleich)](#5-showdown-kartenvergleich)
|
||||
* [6. Poker-Handrangfolge](#6-poker-handrangfolge)
|
||||
* [7. Wichtige Grundprinzipien](#7-wichtige-grundprinzipien)
|
||||
* [Reihenfolge beachten](#reihenfolge-beachten)
|
||||
* [Klare Aktionen](#klare-aktionen)
|
||||
* [Ein Spieler – eine Hand](#ein-spieler--eine-hand)
|
||||
* [Fehlerhafte Einsätze](#fehlerhafte-einsätze)
|
||||
* [8. Strategische Einordnung](#8-strategische-einordnung)
|
||||
* [Casono Rules Easy Description](#casono-rules-easy-description)
|
||||
* [Blinds (Small Blind & Big Blind)](#blinds-small-blind--big-blind)
|
||||
* [Erste Setzrunde (Preflop)](#erste-setzrunde-preflop)
|
||||
* [Beispiel Preflop](#beispiel-preflop)
|
||||
* [Flop (3 Gemeinschaftskarten)](#flop-3-gemeinschaftskarten)
|
||||
* [Beispiel Flop](#beispiel-flop)
|
||||
* [Turn (4. Karte)](#turn-4-karte)
|
||||
* [River (5. Karte)](#river-5-karte)
|
||||
* [Showdown (Gewinnentscheidung)](#showdown-gewinnentscheidung)
|
||||
* [Poker Hand Rankings (Gewichtung)](#poker-hand-rankings-gewichtung)
|
||||
* [Fazit](#fazit)
|
||||
|
||||
<!-- vim-markdown-toc -->
|
||||
|
||||
# Start Game
|
||||
|
||||
## Server starten
|
||||
```bash
|
||||
./gradlew run --no-configuration-cache --args="server 1234"
|
||||
```
|
||||
|
||||
## Client starten
|
||||
```bash
|
||||
./gradlew run --no-configuration-cache --args="client 127.0.0.1:1234 DeinName"
|
||||
```
|
||||
|
||||
# UI
|
||||
|
||||
## Lobby UI
|
||||
|
||||
### Eine Lobby erstellen
|
||||
|
||||

|
||||
|
||||
### Einer Lobby beitreten
|
||||
|
||||

|
||||
|
||||
### Den Username ändern
|
||||
|
||||

|
||||
|
||||
### Highscores
|
||||
|
||||

|
||||
|
||||
## Game UI
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Casono Browser
|
||||
|
||||

|
||||
|
||||
# Casono Rules
|
||||
|
||||
## 1. Spielübersicht
|
||||
|
||||
Texas Hold’em ist ein strategisches Kartenspiel für mehrere Spieler.
|
||||
@@ -160,3 +225,154 @@ Texas Hold’em ist kein reines Glücksspiel. Der Erfolg basiert auf:
|
||||
- Wahrscheinlichkeiten (Mathematik)
|
||||
- Einschätzung von Gegnern (Psychologie)
|
||||
- Positionsspiel und Timing
|
||||
|
||||
# Casono Rules Easy Description
|
||||
|
||||
Der Pokertisch ist ein unglaublich faszinierender Erlebnisraum, in dem man sehr viel lernen kann: über sich selbst, über andere Menschen und über Fragen wie: Wie treffe ich eigentlich Entscheidungen, wie gehe ich mit Stress und Unsicherheit um und wie gut ich darin bin, mich in andere hineinzuversetzen und Situationen richtig einzuschätzen.
|
||||
|
||||
Damit Du in diesem Erlebnisraum starten kannst, ist es – wie bei jedem Spiel – notwendig, zuerst die Grundregeln und den Spielablauf zu verstehen.
|
||||
|
||||
Also los geht es:
|
||||
|
||||
Wir haben am Tisch **4 Spieler**: Julian, Mathis, Jona und Lars. Jeder Spieler startet mit **20000 Chips ($)**. Jeder bekommt **2 Karten auf die Hand** und es gibt zusätzlich **5 Gemeinschaftskarten**, die später in der Mitte aufgedeckt werden.
|
||||
|
||||

|
||||
|
||||
Die Spieler sitzen in folgender Reihenfolge: Julian, Mathis, Jona und Lars. Einer davon hat den Dealer-Button, der bestimmt, wer die Karten austeilt und von wo die Runde beginnt. Dieser Button wandert nach jeder Runde im Uhrzeigersinn weiter und verändert damit die Position ständig.
|
||||
|
||||
Regel: *34 Button Placement and Movement 🟢*
|
||||
|
||||
## Blinds (Small Blind & Big Blind)
|
||||
|
||||
Bevor die Karten verteilt werden, gibt es zwei Pflicht-Einsätze:
|
||||
|
||||
Der Small Blind und der Big Blind. Der Big Blind ist immer doppelt so hoch wie der Small Blind.
|
||||
|
||||

|
||||
|
||||
Regel: *32 Dead Button 🟡*
|
||||
|
||||
Diese Einsätze sorgen dafür, dass sofort ein Pot entsteht und das Spiel überhaupt beginnt, weil jeder schon “im Spiel” ist.
|
||||
|
||||
Danach werden die Karten verteilt: zuerst Small Blind, dann Big Blind und dann im Uhrzeigersinn alle anderen Spieler.
|
||||
|
||||
## Erste Setzrunde (Preflop)
|
||||
|
||||
Die erste Setzrunde beginnt immer bei dem Spieler links vom Big Blind.
|
||||
|
||||
Jetzt muss jeder Spieler entscheiden:
|
||||
|
||||
* Fold (aussteigen)
|
||||
* Call (mitgehen)
|
||||
* Raise (erhöhen)
|
||||
|
||||
Regel: *40 Methods of Betting 🟢*
|
||||
Regel: *41 Methods of Calling 🟢*
|
||||
Regel: *42 Methods of Raising 🟢*
|
||||
Regel: *50 Acting in Turn 🟢*
|
||||
|
||||
## Beispiel Preflop
|
||||
|
||||
Julian schaut seine Karten an und entscheidet sich direkt für einen Raise von **600 Chips**.
|
||||
|
||||

|
||||
|
||||
Mathis sieht seine Karten an und merkt, dass sie nicht gut sind, also foldet er und steigt aus.
|
||||
|
||||

|
||||
|
||||
Jona ist nun dran und entscheidet sich ebenfalls für einen Call, weil seine Hand spielbar ist.
|
||||
|
||||

|
||||
|
||||
Lars schaut seine Karten an, erkennt eine starke Hand und erhöht auf **1200 Chips**.
|
||||
|
||||

|
||||
|
||||
Damit verändert sich sofort die Situation: Julian und Jona müssen entscheiden, ob sie diesen Raise bezahlen, selbst erhöhen oder aussteigen.
|
||||
|
||||
## Flop (3 Gemeinschaftskarten)
|
||||
|
||||
Jetzt werden **3 Gemeinschaftskarten** in die Mitte gelegt. Ab hier verändert sich das Spiel komplett, weil alle Spieler zusätzliche Informationen bekommen.
|
||||
|
||||
Die Setzrunde beginnt jetzt immer beim ersten aktiven Spieler links vom Dealer (im Uhrzeigersinn).
|
||||
|
||||

|
||||
|
||||
Es beginnt eine neue Setzrunde.
|
||||
|
||||
Regel: *49 Accepted Action 🟢*
|
||||
|
||||
## Beispiel Flop
|
||||
|
||||
Jona setzt **1000 Chips** als Erstes. Lars entscheidet sich mitzugehen (Call), weil seine Karten durch die Gemeinschaftskarten stärker geworden sind.
|
||||
|
||||
Julian steigt aus, weil er keine gute Verbindung mehr sieht. Mathis ist bereits raus.
|
||||
|
||||

|
||||
|
||||
## Turn (4. Karte)
|
||||
|
||||
Jetzt kommt die **4. Gemeinschaftskarte**.
|
||||
|
||||
Wieder beginnt eine neue Setzrunde.
|
||||
|
||||
Jona setzt diesmal **3000 Chips**. Lars bezahlt erneut (Call), weil seine Hand weiterhin gut spielbar ist.
|
||||
|
||||

|
||||
|
||||
Regel: *53 Action Out of Turn 🟡*
|
||||
|
||||
## River (5. Karte)
|
||||
|
||||
Jetzt wird die letzte Gemeinschaftskarte aufgedeckt.
|
||||
|
||||
Dies ist die letzte Entscheidung im Spiel.
|
||||
|
||||
Jona setzt **5000 Chips**.
|
||||
|
||||

|
||||
|
||||
Lars muss jetzt entscheiden: Fold, Call oder Raise auf 10000 Chips.
|
||||
|
||||
Regel: *54 Pot Size Bets 🟡*
|
||||
|
||||
## Showdown (Gewinnentscheidung)
|
||||
|
||||
Wenn nach der letzten Setzrunde noch zwei Spieler übrig sind, kommt es zum Showdown.
|
||||
|
||||
Beide Spieler zeigen ihre Karten offen. Gewonnen hat die **beste 5-Karten-Kombination aus Handkarten und Gemeinschaftskarten**.
|
||||
|
||||

|
||||
|
||||
Regel: *12 Cards Speak at Showdown 🟢*
|
||||
Regel: *16 Face Up for All-Ins 🟢*
|
||||
Regel: *17 Non All-In Showdowns 🟢*
|
||||
|
||||
Wenn Lars den letzten Einsatz bezahlt, werden die Hände verglichen. Wenn er foldet, gewinnt Jona automatisch den gesamten Pot.
|
||||
|
||||
## Poker Hand Rankings (Gewichtung)
|
||||
|
||||
Die Kartenkombinationen sind klar geordnet – von schwach bis extrem stark:
|
||||
|
||||
<img src="./images/12.png" height="600">
|
||||
|
||||
Je höher die Kombination, desto stärker die Hand und desto wahrscheinlicher der Gewinn.
|
||||
|
||||
Jona: 2. Paar:
|
||||
|
||||

|
||||
|
||||
Lars: 1. Paar:
|
||||
|
||||

|
||||
|
||||
Da zwei Paare in der Rangfolge über einem einzelnen Paar stehen, gewinnt Jona diese Runde.
|
||||
|
||||
## Fazit
|
||||
|
||||
Poker ist kein Glücksspiel im klassischen Sinn, sondern ein Spiel aus Strategie, Psychologie und Mathematik. Jede Entscheidung von Julian, Mathis, Jona oder Lars verändert die komplette Dynamik am Tisch. Wer die Regeln versteht, versteht nicht nur Karten, sondern auch Menschen und Entscheidungen unter Druck.
|
||||
|
||||
Regel: *67 One Player One Hand 🟢*
|
||||
Regel: *52 Incorrect Bets 🟡*
|
||||
Regel: *57 Non-Standard Betting 🟡*
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
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(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
|
||||
const langClass = lang ? ` language-${lang}` : "";
|
||||
return `<pre class="cm-code${langClass}"><code>${escapeHtml(code.trim())}</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;
|
||||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 740 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 863 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 922 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1,508 @@
|
||||
<!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
|
||||
|
||||
# Start Game
|
||||
|
||||
## Server starten
|
||||
\`\`\`bash
|
||||
./gradlew run --no-configuration-cache --args="server 1234"
|
||||
\`\`\`
|
||||
|
||||
## Client starten
|
||||
\`\`\`bash
|
||||
./gradlew run --no-configuration-cache --args="client 127.0.0.1:1234 DeinName"
|
||||
\`\`\`
|
||||
|
||||
# UI
|
||||
|
||||
## Lobby UI
|
||||
|
||||
### Eine Lobby erstellen
|
||||
|
||||

|
||||
|
||||
### Einer Lobby beitreten
|
||||
|
||||

|
||||
|
||||
### Den Username ändern
|
||||
|
||||

|
||||
|
||||
### Highscores
|
||||
|
||||

|
||||
|
||||
## Game UI
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Casono Browser
|
||||
|
||||

|
||||
|
||||
# Casono Rules
|
||||
|
||||
## 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
|
||||
|
||||
# Casono Rules Easy Description
|
||||
|
||||
Der Pokertisch ist ein unglaublich faszinierender Erlebnisraum, in dem man sehr viel lernen kann: über sich selbst, über andere Menschen und über Fragen wie: Wie treffe ich eigentlich Entscheidungen, wie gehe ich mit Stress und Unsicherheit um und wie gut ich darin bin, mich in andere hineinzuversetzen und Situationen richtig einzuschätzen.
|
||||
|
||||
Damit Du in diesem Erlebnisraum starten kannst, ist es – wie bei jedem Spiel – notwendig, zuerst die Grundregeln und den Spielablauf zu verstehen.
|
||||
|
||||
Also los geht es:
|
||||
|
||||
Wir haben am Tisch **4 Spieler**: Julian, Mathis, Jona und Lars. Jeder Spieler startet mit **20000 Chips ($)**. Jeder bekommt **2 Karten auf die Hand** und es gibt zusätzlich **5 Gemeinschaftskarten**, die später in der Mitte aufgedeckt werden.
|
||||
|
||||

|
||||
|
||||
Die Spieler sitzen in folgender Reihenfolge: Julian, Mathis, Jona und Lars. Einer davon hat den Dealer-Button, der bestimmt, wer die Karten austeilt und von wo die Runde beginnt. Dieser Button wandert nach jeder Runde im Uhrzeigersinn weiter und verändert damit die Position ständig.
|
||||
|
||||
Regel: *34 Button Placement and Movement 🟢*
|
||||
|
||||
## Blinds (Small Blind & Big Blind)
|
||||
|
||||
Bevor die Karten verteilt werden, gibt es zwei Pflicht-Einsätze:
|
||||
|
||||
Der Small Blind und der Big Blind. Der Big Blind ist immer doppelt so hoch wie der Small Blind.
|
||||
|
||||

|
||||
|
||||
Regel: *32 Dead Button 🟡*
|
||||
|
||||
Diese Einsätze sorgen dafür, dass sofort ein Pot entsteht und das Spiel überhaupt beginnt, weil jeder schon “im Spiel” ist.
|
||||
|
||||
Danach werden die Karten verteilt: zuerst Small Blind, dann Big Blind und dann im Uhrzeigersinn alle anderen Spieler.
|
||||
|
||||
## Erste Setzrunde (Preflop)
|
||||
|
||||
Die erste Setzrunde beginnt immer bei dem Spieler links vom Big Blind.
|
||||
|
||||
Jetzt muss jeder Spieler entscheiden:
|
||||
|
||||
* Fold (aussteigen)
|
||||
* Call (mitgehen)
|
||||
* Raise (erhöhen)
|
||||
|
||||
Regel: *40 Methods of Betting 🟢*
|
||||
Regel: *41 Methods of Calling 🟢*
|
||||
Regel: *42 Methods of Raising 🟢*
|
||||
Regel: *50 Acting in Turn 🟢*
|
||||
|
||||
## Beispiel Preflop
|
||||
|
||||
Julian schaut seine Karten an und entscheidet sich direkt für einen Raise von **600 Chips**.
|
||||
|
||||

|
||||
|
||||
Mathis sieht seine Karten an und merkt, dass sie nicht gut sind, also foldet er und steigt aus.
|
||||
|
||||

|
||||
|
||||
Jona ist nun dran und entscheidet sich ebenfalls für einen Call, weil seine Hand spielbar ist.
|
||||
|
||||

|
||||
|
||||
Lars schaut seine Karten an, erkennt eine starke Hand und erhöht auf **1200 Chips**.
|
||||
|
||||

|
||||
|
||||
Damit verändert sich sofort die Situation: Julian und Jona müssen entscheiden, ob sie diesen Raise bezahlen, selbst erhöhen oder aussteigen.
|
||||
|
||||
## Flop (3 Gemeinschaftskarten)
|
||||
|
||||
Jetzt werden **3 Gemeinschaftskarten** in die Mitte gelegt. Ab hier verändert sich das Spiel komplett, weil alle Spieler zusätzliche Informationen bekommen.
|
||||
|
||||
Die Setzrunde beginnt jetzt immer beim ersten aktiven Spieler links vom Dealer (im Uhrzeigersinn).
|
||||
|
||||

|
||||
|
||||
Es beginnt eine neue Setzrunde.
|
||||
|
||||
Regel: *49 Accepted Action 🟢*
|
||||
|
||||
## Beispiel Flop
|
||||
|
||||
Jona setzt **1000 Chips** als Erstes. Lars entscheidet sich mitzugehen (Call), weil seine Karten durch die Gemeinschaftskarten stärker geworden sind.
|
||||
|
||||
Julian steigt aus, weil er keine gute Verbindung mehr sieht. Mathis ist bereits raus.
|
||||
|
||||

|
||||
|
||||
## Turn (4. Karte)
|
||||
|
||||
Jetzt kommt die **4. Gemeinschaftskarte**.
|
||||
|
||||
Wieder beginnt eine neue Setzrunde.
|
||||
|
||||
Jona setzt diesmal **3000 Chips**. Lars bezahlt erneut (Call), weil seine Hand weiterhin gut spielbar ist.
|
||||
|
||||

|
||||
|
||||
Regel: *53 Action Out of Turn 🟡*
|
||||
|
||||
## River (5. Karte)
|
||||
|
||||
Jetzt wird die letzte Gemeinschaftskarte aufgedeckt.
|
||||
|
||||
Dies ist die letzte Entscheidung im Spiel.
|
||||
|
||||
Jona setzt **5000 Chips**.
|
||||
|
||||

|
||||
|
||||
Lars muss jetzt entscheiden: Fold, Call oder Raise auf 10000 Chips.
|
||||
|
||||
Regel: *54 Pot Size Bets 🟡*
|
||||
|
||||
## Showdown (Gewinnentscheidung)
|
||||
|
||||
Wenn nach der letzten Setzrunde noch zwei Spieler übrig sind, kommt es zum Showdown.
|
||||
|
||||
Beide Spieler zeigen ihre Karten offen. Gewonnen hat die **beste 5-Karten-Kombination aus Handkarten und Gemeinschaftskarten**.
|
||||
|
||||

|
||||
|
||||
Regel: *12 Cards Speak at Showdown 🟢*
|
||||
Regel: *16 Face Up for All-Ins 🟢*
|
||||
Regel: *17 Non All-In Showdowns 🟢*
|
||||
|
||||
Wenn Lars den letzten Einsatz bezahlt, werden die Hände verglichen. Wenn er foldet, gewinnt Jona automatisch den gesamten Pot.
|
||||
|
||||
## Poker Hand Rankings (Gewichtung)
|
||||
|
||||
Die Kartenkombinationen sind klar geordnet – von schwach bis extrem stark:
|
||||
|
||||
<img src="./images/12.png" height="600">
|
||||
|
||||
Je höher die Kombination, desto stärker die Hand und desto wahrscheinlicher der Gewinn.
|
||||
|
||||
Jona: 2. Paar:
|
||||
|
||||

|
||||
|
||||
Lars: 1. Paar:
|
||||
|
||||

|
||||
|
||||
Da zwei Paare in der Rangfolge über einem einzelnen Paar stehen, gewinnt Jona diese Runde.
|
||||
|
||||
## Fazit
|
||||
|
||||
Poker ist kein Glücksspiel im klassischen Sinn, sondern ein Spiel aus Strategie, Psychologie und Mathematik. Jede Entscheidung von Julian, Mathis, Jona oder Lars verändert die komplette Dynamik am Tisch. Wer die Regeln versteht, versteht nicht nur Karten, sondern auch Menschen und Entscheidungen unter Druck.
|
||||
|
||||
Regel: *67 One Player One Hand 🟢*
|
||||
Regel: *52 Incorrect Bets 🟡*
|
||||
Regel: *57 Non-Standard Betting 🟡*
|
||||
`;
|
||||
|
||||
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>
|
||||
@@ -1,8 +1,9 @@
|
||||
# Protocol Document
|
||||
|
||||
This document describes the protocol for client-server communication in our application. It defines the structure of requests and responses, the supported commands along with their request parameters, response formats, and possible errors.
|
||||
|
||||
|
||||
# Table of Contents
|
||||
|
||||
- [Protocol Document](#protocol-document)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [General structure of requests](#general-structure-of-requests)
|
||||
@@ -36,20 +37,27 @@ This document describes the protocol for client-server communication in our appl
|
||||
- [Error Response](#error-response-4)
|
||||
- [Example Request](#example-request-2)
|
||||
- [Example Response](#example-response-2)
|
||||
- [LOGOUT command](#logout-command)
|
||||
- [CHANGE\_USERNAME command](#change_username-command)
|
||||
- [Required pre-execution checks](#required-pre-execution-checks-3)
|
||||
- [Request Parameters](#request-parameters-3)
|
||||
- [Success Response](#success-response-3)
|
||||
- [Error Response](#error-response-5)
|
||||
- [Example Request](#example-request-3)
|
||||
- [Example Response](#example-response-3)
|
||||
- [LIST\_USERS command](#list_users-command)
|
||||
- [LOGOUT command](#logout-command)
|
||||
- [Required pre-execution checks](#required-pre-execution-checks-4)
|
||||
- [Request Parameters](#request-parameters-4)
|
||||
- [Success Response](#success-response-4)
|
||||
- [Error Response](#error-response-6)
|
||||
- [Example Request](#example-request-4)
|
||||
- [Example Response](#example-response-4)
|
||||
- [LIST\_USERS command](#list_users-command)
|
||||
- [Required pre-execution checks](#required-pre-execution-checks-5)
|
||||
- [Request Parameters](#request-parameters-5)
|
||||
- [Success Response](#success-response-5)
|
||||
- [Error Response](#error-response-7)
|
||||
- [Example Request](#example-request-5)
|
||||
- [Example Response](#example-response-5)
|
||||
- [GET_LOBBY_LIST command](#get_lobby_list-command)
|
||||
- [Required pre-execution checks](#required-pre-execution-checks)
|
||||
- [Request Parameters](#request-parameters)
|
||||
@@ -62,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)
|
||||
@@ -91,50 +111,53 @@ This document describes the protocol for client-server communication in our appl
|
||||
<!-- Please see the comments for copy ‚ n' paste ready examples -->
|
||||
|
||||
# General structure of requests
|
||||
As mentioned before, our protocol is based on POP3.
|
||||
|
||||
As mentioned before, our protocol is based on POP3.
|
||||
Each command is represented as a single line of text, starting with the command name followed by parameters. The server responds with a status line indicating success or failure, followed by the body.
|
||||
|
||||
Requests can have parameters that provide additional information for the command. Parameters are key-value pairs separated by an equal sign (`=`).
|
||||
|
||||
Responses are collections of key-value pairs, containing either a value or another collection, allowing for nested structures.
|
||||
Responses are collections of key-value pairs, containing either a value or another collection, allowing for nested structures.
|
||||
Each collection is ended with the `END` keyword.
|
||||
|
||||
|
||||
|
||||
# Preconditions
|
||||
|
||||
The serverside pipeline to process incoming requests consists of multiple stages.
|
||||
Each of these stages can yield an error response if the request does not meet the requirements of that stage.
|
||||
|
||||
## Parsing
|
||||
|
||||
One of these stages is the parsing. It is responsible for parsing the raw request into a structured format that can be easily processed by the command handlers. It validates the syntax of the request as well.
|
||||
|
||||
### Error Response
|
||||
|
||||
| Code | Description |
|
||||
| :-------------- | :---------------------------------------------------------------------------------------------- |
|
||||
| `PARSING_ERROR` | The body of the request contains syntax errors (see message field of response for more details) |
|
||||
|
||||
|
||||
## Command dispatching
|
||||
|
||||
After the request has been successfully parsed, the next stage is to dispatch the `PrimitiveRequest` to the appropriate `CommandParser`. This is done by the `CommandDispatcherDispatcher`, which uses the command name to determine which parser to use.
|
||||
|
||||
### Error Response
|
||||
|
||||
| Code | Description |
|
||||
| :---------------- | :---------------------------------------------------------------- |
|
||||
| `UNKNOWN_COMMAND` | The command is unknown to this server. No parser has been defined |
|
||||
|
||||
|
||||
## Command parsing
|
||||
|
||||
Once the `PrimitiveRequest` has been dispatched to the appropriate `CommandParser`, the parser is responsible for parsing the parameters of the request and creating a `Request` that can be executed by the responsible `CommandHandler`.
|
||||
|
||||
### Error Response
|
||||
|
||||
| Code | Description |
|
||||
| :------------------ | :------------------------------------------------------------------------------------------------ |
|
||||
| `MISSING_PARAMETER` | A required parameter is missing from the request (see message field of response for more details) |
|
||||
|
||||
|
||||
|
||||
# Pre-execution checks
|
||||
Pre-execution checks are reusable validation steps that can be registered on command handlers.
|
||||
|
||||
Pre-execution checks are reusable validation steps that can be registered on command handlers.
|
||||
They are implemented as `HandlerCheck` instances and are executed by the `CommandHandlerExecutor` before the handler's main logic is invoked.
|
||||
|
||||
<!--
|
||||
@@ -148,16 +171,17 @@ Description of the check, what it does and when it should be used.
|
||||
-->
|
||||
|
||||
## UserLoggedInCheck
|
||||
|
||||
The `UserLoggedInCheck` is a common pre-execution check that verifies whether the user is logged in (i.e. has a user associated with his session).
|
||||
|
||||
### Error Response
|
||||
|
||||
| Code | Description |
|
||||
| :------------------- | :------------------------ |
|
||||
| `USER_NOT_LOGGED_IN` | The user is not logged in |
|
||||
|
||||
|
||||
|
||||
# Commands
|
||||
|
||||
Commands are the core of our protocol, representing the various actions that clients can request from the server. Each command has a unique name and may require specific parameters in addition to pre-execution checks.
|
||||
The server processes these commands and responds accordingly.
|
||||
|
||||
@@ -212,41 +236,50 @@ END
|
||||
-->
|
||||
|
||||
## PING command
|
||||
|
||||
The `PING` command is a simple command that can be used to check if the server is responsive.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
### Success Response
|
||||
|
||||
No response fields.
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
PING
|
||||
```
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
END
|
||||
```
|
||||
|
||||
|
||||
## CHECK_USERNAME command
|
||||
|
||||
The `CHECK_USERNAME` command is used to check if a username is already taken by another user. Additional users can still log in with the same username, but their name will be substituted with a suffix.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
| Parameter Name | Type | Optional | Description |
|
||||
| :------------- | :------- | :------- | :------------------------------------- |
|
||||
| `USERNAME` | `String` | no | The username to check for availability |
|
||||
|
||||
### Success Response
|
||||
|
||||
| Field | Type | Description |
|
||||
| :------- | :--------------------------- | :---------------------------------------------------------------------- |
|
||||
| `STATUS` | `Enum<UsernameAvailability>` | Member of enum indicating if the username is available or already taken |
|
||||
@@ -257,11 +290,13 @@ None.
|
||||
| `TAKEN` | Username is already in use |
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
CHECK_USERNAME USERNAME='Lars'
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
STATUS=FREE
|
||||
@@ -269,33 +304,40 @@ END
|
||||
```
|
||||
|
||||
## LOGIN command
|
||||
|
||||
The `LOGIN` command is used to log in a user with a specified username. If the username is already taken by another user, the server will append a suffix to the username to make it unique.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
| Parameter Name | Type | Optional | Description |
|
||||
| :------------- | :------- | :------- | :----------------------------------- |
|
||||
| `USERNAME` | `String` | no | The username to create the user with |
|
||||
|
||||
### Success Response
|
||||
|
||||
| Field | Type | Description |
|
||||
| :--------- | :---------------------------------------------------------------------- | :-------------------------------------------------------------------- |
|
||||
| `USERNAME` | `String` | Username of the newly created user, can differ from the requested one |
|
||||
| `ID` | [`UUID`](https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html) | The ID of the created user |
|
||||
|
||||
### Error Response
|
||||
|
||||
| Code | Description |
|
||||
| :------------------ | :----------------------------------------------------------------------------- |
|
||||
| `ALREADY_LOGGED_IN` | The session is already associated with a user, logging in again is prohibited. |
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
LOGIN USERNAME='Lars'
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
USERNAME='Lars_1234'
|
||||
@@ -303,45 +345,100 @@ LOGIN USERNAME='Lars'
|
||||
END
|
||||
```
|
||||
|
||||
## CHANGE_USERNAME command
|
||||
|
||||
## LOGOUT command
|
||||
Description of the command, what it does, and when it should be used.
|
||||
The `CHANGE_USERNAME` command is used to change the username of an already logged-in user. The request is tied to the current session and updates all affected server-side mappings.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
| Parameter Name | Type | Optional | Description |
|
||||
| :------------- | :------- | :------- | :------------------------------------ |
|
||||
| `USERNAME` | `String` | no | The new username for the active user |
|
||||
|
||||
### Success Response
|
||||
|
||||
| Field | Type | Description |
|
||||
| :--------- | :---------------------------------------------------------------------- | :-------------------------------------- |
|
||||
| `USERNAME` | `String` | Effective username after rename |
|
||||
| `ID` | [`UUID`](https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html) | The ID of the renamed user |
|
||||
|
||||
### Error Response
|
||||
|
||||
| Code | Description |
|
||||
| :------------------- | :-------------------------------------------------------------------- |
|
||||
| `USER_NOT_LOGGED_IN` | No active user is associated with this session. |
|
||||
| `INVALID_USERNAME` | Username contains disallowed characters or is empty. |
|
||||
| `USERNAME_TAKEN` | Requested username is already used by another user. |
|
||||
| `RENAME_CONFLICT` | Rename could not be propagated to all current lobby/game structures. |
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
CHANGE_USERNAME USERNAME='Lars_New'
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
USERNAME='Lars_New'
|
||||
ID=e47a671e-2b2a-42df-bb82-953fe2ebd307
|
||||
END
|
||||
```
|
||||
|
||||
## LOGOUT command
|
||||
|
||||
Description of the command, what it does, and when it should be used.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
### Success Response
|
||||
|
||||
No response fields.
|
||||
|
||||
### Error Response
|
||||
|
||||
| Code | Description |
|
||||
| :------------------- | :--------------------------------------------------------------- |
|
||||
| `NO_USER_ASSOCIATED` | The session has no user associated, logging out is not possible. |
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
LOGOUT
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
END
|
||||
```
|
||||
|
||||
## LIST_USERS command
|
||||
|
||||
The `LIST_USERS` command is used to retrieve a list of all currently logged-in users.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
### Success Response
|
||||
|
||||
| Field | Type | Description |
|
||||
| :------ | :----------------- | :--------------------------------------- |
|
||||
| `USERS` | `Collection<User>` | Collection of all users currently online |
|
||||
@@ -351,16 +448,18 @@ No parameters.
|
||||
| `USERNAME` | `String` | Username of the newly created user, can differ from the requested one |
|
||||
| `ID` | [`UUID`](https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html) | The ID of the created user |
|
||||
|
||||
|
||||
### Error Response
|
||||
|
||||
None.
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
LIST_USERS
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
USERS
|
||||
@@ -381,8 +480,11 @@ END
|
||||
```
|
||||
|
||||
## SEND_MESSAGE command
|
||||
|
||||
The `SEND_MESSAGE` command is used to transfer the chat message sent by a user to the server.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
@@ -403,28 +505,36 @@ None.
|
||||
| `WHISPER` | Message is for the whisper chat |
|
||||
|
||||
### Success Response
|
||||
|
||||
No response fields.
|
||||
|
||||
### Error Response
|
||||
|
||||
None.
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
SEND_MESSAGE TYPE=GLOBAL GAME=1 USER=player1 TARGET=null TIME='10:30' TEXT='Hello World'
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
END
|
||||
```
|
||||
|
||||
## GET_MESSAGE_COUNT command
|
||||
|
||||
The `GET_MESSAGE_COUNT` is used to get the current number of messages that are stored in the queue for a client.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
### Success Response
|
||||
@@ -434,17 +544,19 @@ No parameters.
|
||||
| `COUNT` | `int` | The current number of messages |
|
||||
|
||||
### Error Response
|
||||
|
||||
| Code | Description |
|
||||
| :------------------- |:---------------------------------------------------------------------------------|
|
||||
| `NO_USER_ASSOCIATED` | The session has no user associated, there is no queue of messages for the client |
|
||||
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
GET_MESSAGE_COUNT
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
COUNT=10
|
||||
@@ -452,14 +564,19 @@ END
|
||||
```
|
||||
|
||||
## GET_NEXT_MESSAGE command
|
||||
|
||||
The `GET_NEXT_MESSAGE` command is used to get the next message stored in a queue for the client.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
### Success Response
|
||||
|
||||
| Field | Type | Description |
|
||||
|:---------|:----------------|:-------------------------------------------------------------------|
|
||||
| `TYPE` | `Enum<ChatType` | Member of Enum, indicating with chat type is used |
|
||||
@@ -476,16 +593,19 @@ No parameters.
|
||||
| `WHISPER` | Message is for the whisper chat |
|
||||
|
||||
### Error Response
|
||||
|
||||
| Code | Description |
|
||||
| :------------------- |:---------------------------------------------------------------------------------|
|
||||
| `NO_USER_ASSOCIATED` | The session has no user associated, there is no queue of messages for the client |
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
GET_NEXT_MESSAGE
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
TYPE=GLOBAL GAME=-1 USER=player1 TARGET=null TIME=9:30 TEXT="Guten Tag"
|
||||
@@ -613,9 +733,11 @@ END
|
||||
The `GET_LOBBY_LIST` command requests the server to return a list of currently available lobbies with basic metadata.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
### Implementation notes
|
||||
@@ -667,9 +789,11 @@ END
|
||||
The `GET_GAME_STATE` command returns a snapshot of the current game for a lobby. It includes global fields (`PHASE`, `POT`, `CURRENT_BET`, `DEALER`, `ACTIVE_PLAYER`), repeated `CARD` blocks for community cards and repeated `PLAYER` blocks for per-player information. If the requester is a player in the game, their hole cards are included inside their `PLAYER` block as nested `CARD` blocks.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
- [`UserLoggedInCheck`](#userloggedincheck)
|
||||
|
||||
### Request Parameters
|
||||
|
||||
| Parameter Name | Type | Optional | Description |
|
||||
| :------------- | :----- | :------: | :---------- |
|
||||
| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to query. If omitted the server will resolve the lobby by the requesting session's associated user.
|
||||
@@ -1114,9 +1238,11 @@ END
|
||||
The `GET_LOBBY_STATUS` command requests the server to return the current state of a lobby, including the list of players and their ready state.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
| Parameter Name | Type | Optional | Description |
|
||||
| :------------- | :--- | :------: | :---------- |
|
||||
| `ID` | `int` | no | Numeric id of the target lobby |
|
||||
@@ -1151,6 +1277,7 @@ END
|
||||
```
|
||||
|
||||
### Error Response
|
||||
|
||||
| Code | Description |
|
||||
| :--- | :---------- |
|
||||
| `LOBBY_NOT_FOUND` | The specified lobby id does not exist |
|
||||
@@ -1222,4 +1349,72 @@ END
|
||||
CODE=LOBBIES_FULL
|
||||
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
|
||||
```
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Interface used to create test instances of the ChatClient */
|
||||
public interface ChatClientInterface {
|
||||
|
||||
List<Message> getMessages();
|
||||
|
||||
void sendMessage(Message message);
|
||||
|
||||
List<String> getUsers();
|
||||
}
|
||||
@@ -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,19 +31,24 @@ 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;
|
||||
private ClientService clientService;
|
||||
|
||||
private final ChatClient chatClient;
|
||||
private final ChatClientInterface chatClient;
|
||||
|
||||
public ChatBoxController getChatBoxController() {
|
||||
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,44 @@ 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(
|
||||
new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
receiveMessage();
|
||||
checkWhisperUsers();
|
||||
} catch (RuntimeException e) {
|
||||
logger.warn("Chat refresh failed: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
},
|
||||
0,
|
||||
REFRESH_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Another constructor to give the ChatClient directly instead of the Client Service used only
|
||||
* for test purposes
|
||||
*
|
||||
* @param username
|
||||
*/
|
||||
public ChatController(String username, ChatClientInterface chatClient) {
|
||||
this.username = username;
|
||||
this.chatClient = chatClient;
|
||||
chatModelMap = new LinkedHashMap<>();
|
||||
localUserList = new ArrayList<>();
|
||||
this.chatBoxController = new ChatBoxController(username, this);
|
||||
this.logger = LogManager.getLogger(ChatController.class);
|
||||
this.serverEventListener = this::handleServerEvent;
|
||||
this.activeChatControllers = new HashMap<>();
|
||||
|
||||
this.timer = new Timer(true);
|
||||
timer.schedule(
|
||||
@@ -118,7 +172,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 +185,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 +201,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 +222,7 @@ public class ChatController {
|
||||
ChatModel chatModel =
|
||||
new ChatModel(
|
||||
ChatType.WHISPER,
|
||||
username,
|
||||
currentUsername,
|
||||
lobbyId,
|
||||
key.targetUser());
|
||||
chatBoxController.addWhisperChat(key.targetUser(), chatModel);
|
||||
@@ -203,13 +259,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 +294,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);
|
||||
|
||||
@@ -2,8 +2,6 @@ package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.function.Consumer;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
|
||||
/**
|
||||
* ChatModel, stores the data for a specific chat
|
||||
@@ -12,7 +10,7 @@ import javafx.beans.property.SimpleIntegerProperty;
|
||||
*/
|
||||
public class ChatModel {
|
||||
|
||||
private ArrayList<Consumer<Message>> listeners = new ArrayList<>();
|
||||
private final ArrayList<Consumer<Message>> listeners = new ArrayList<>();
|
||||
|
||||
public ArrayList<Message> messages;
|
||||
|
||||
@@ -22,9 +20,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 final IntegerProperty count;
|
||||
private String target;
|
||||
|
||||
public int lobbyId;
|
||||
|
||||
@@ -37,10 +33,9 @@ public class ChatModel {
|
||||
* @param target The username of the whisper recipient, or null for other chat types.
|
||||
*/
|
||||
public ChatModel(ChatType chattype, String username, int lobbyId, String target) {
|
||||
this.messages = new ArrayList<Message>();
|
||||
this.messages = new ArrayList<>();
|
||||
this.chattype = chattype;
|
||||
this.username = username;
|
||||
this.count = new SimpleIntegerProperty(0);
|
||||
this.lobbyId = lobbyId;
|
||||
this.target = target;
|
||||
}
|
||||
@@ -62,7 +57,7 @@ public class ChatModel {
|
||||
*/
|
||||
public synchronized void addMessage(Message msg) {
|
||||
messages.add(msg);
|
||||
listeners.stream().forEach((l) -> l.accept(messages.getLast()));
|
||||
listeners.forEach((l) -> l.accept(messages.getLast()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,4 +77,8 @@ public class ChatModel {
|
||||
public String getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
public void setTarget(String target) {
|
||||
this.target = target;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ public class Message {
|
||||
private final String message;
|
||||
public String sender;
|
||||
public String timestamp;
|
||||
public int lobbyId = 0;
|
||||
public String target = null;
|
||||
public int lobbyId;
|
||||
public String target;
|
||||
|
||||
/**
|
||||
* Constructs a Message with a provided timestamp. Typically used when reconstructing messages
|
||||
@@ -100,7 +100,7 @@ public class Message {
|
||||
* @return A formatted string containing all message attributes for server transmission.
|
||||
*/
|
||||
public String toArgsString() {
|
||||
String gameIdString = "";
|
||||
String gameIdString;
|
||||
if (lobbyId >= 0) {
|
||||
gameIdString = " GAME=" + lobbyId;
|
||||
} else {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatClientInterface;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||
import java.util.ArrayList;
|
||||
@@ -12,7 +13,7 @@ import org.apache.logging.log4j.Logger;
|
||||
* from the server. It uses the ClientService to send commands and receive responses from the
|
||||
* server.
|
||||
*/
|
||||
public class ChatClient {
|
||||
public class ChatClient implements ChatClientInterface {
|
||||
|
||||
private final ClientService clientService;
|
||||
private final Logger logger;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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.ChatController.ChatKey;
|
||||
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;
|
||||
@@ -19,7 +21,6 @@ import javafx.scene.control.MenuButton;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TabPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
@@ -27,24 +28,16 @@ public class ChatBoxController {
|
||||
|
||||
private String username;
|
||||
|
||||
private ChatController chatController;
|
||||
|
||||
@FXML private VBox chatBox;
|
||||
private final ChatController chatController;
|
||||
|
||||
@FXML private TabPane chatTabPane;
|
||||
|
||||
@FXML private MenuButton addWhisperChatButton;
|
||||
|
||||
@FXML private HBox menuBox;
|
||||
|
||||
private FXMLLoader fxmlLoader;
|
||||
|
||||
private final List<String> activeWhisperChats;
|
||||
|
||||
@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 +53,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,12 +66,11 @@ 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);
|
||||
addWhisperChatButton.setOnAction(event -> addWhisperChatButton.show());
|
||||
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(_ -> addWhisperChatButton.show());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,10 +82,15 @@ 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(
|
||||
event ->
|
||||
_ ->
|
||||
addWhisperChat(
|
||||
targetUserName,
|
||||
new ChatModel(
|
||||
@@ -98,6 +101,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(
|
||||
_ ->
|
||||
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,11 +158,22 @@ public class ChatBoxController {
|
||||
public void addWhisperChat(String target, ChatModel chatModel) {
|
||||
if (!activeWhisperChats.contains(target)) {
|
||||
activeWhisperChats.add(target);
|
||||
chatController
|
||||
.getChatModelMap()
|
||||
.put(new ChatController.ChatKey(ChatType.WHISPER, target), chatModel);
|
||||
addChatTab(target, chatModel);
|
||||
ChatType whisper = ChatType.WHISPER;
|
||||
chatController.getChatModelMap().put(new ChatKey(whisper, target), chatModel);
|
||||
ChatViewController chatViewController = addChatTab(target, chatModel, whisper);
|
||||
Tab chatTab = chatViewController.getChatTab();
|
||||
chatTab.setOnCloseRequest(
|
||||
_ -> {
|
||||
chatViewController.tabIsOpen = false;
|
||||
chatTabPane.getTabs().remove(chatTab);
|
||||
});
|
||||
} else {
|
||||
ChatViewController chatViewController =
|
||||
chatController.activeChatControllers.get(new ChatKey(ChatType.WHISPER, target));
|
||||
if (!chatViewController.tabIsOpen) {
|
||||
reopenChatTab(chatViewController.getChatTab());
|
||||
chatViewController.tabIsOpen = true;
|
||||
}
|
||||
chatTabPane.getSelectionModel().select(usernameTabMap.get(target));
|
||||
}
|
||||
}
|
||||
@@ -125,27 +187,39 @@ 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 ChatViewController 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(
|
||||
return runOnPlatformSynchronized(
|
||||
() -> {
|
||||
try {
|
||||
ChatViewController chatViewController =
|
||||
new ChatViewController(
|
||||
this.chatController, chatModel, this.username);
|
||||
new ChatViewController(this.chatController, chatModel, this);
|
||||
|
||||
ChatKey chatKey;
|
||||
if (chatType.equals(ChatType.WHISPER)) {
|
||||
chatKey = new ChatKey(chatType, title);
|
||||
} else {
|
||||
chatKey = new ChatKey(chatType);
|
||||
}
|
||||
chatController.activeChatControllers.put(chatKey, chatViewController);
|
||||
fxmlLoader.setController(chatViewController);
|
||||
Node load = fxmlLoader.load();
|
||||
VBox.setVgrow(load, Priority.ALWAYS);
|
||||
VBox vbox = new VBox();
|
||||
VBox.setVgrow(vbox, Priority.ALWAYS);
|
||||
vbox.getChildren().add(load);
|
||||
chatModel.addListener((msg) -> chatViewController.showMessage(msg));
|
||||
chatModel.addListener(chatViewController::showMessage);
|
||||
Tab newChat = new Tab(title, vbox);
|
||||
usernameTabMap.put(title, newChat);
|
||||
this.chatTabPane.getTabs().add(newChat);
|
||||
chatTabPane.getTabs().add(newChat);
|
||||
this.chatTabPane.getSelectionModel().select(newChat);
|
||||
return newChat;
|
||||
chatViewController.setChatTab(newChat);
|
||||
if (!chatType.equals(ChatType.WHISPER)) {
|
||||
newChat.setClosable(false);
|
||||
}
|
||||
return chatViewController;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -180,4 +254,38 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void reopenChatTab(Tab chatTab) {
|
||||
Platform.runLater(
|
||||
() -> {
|
||||
chatTabPane.getTabs().add(chatTab);
|
||||
chatTabPane.getSelectionModel().select(chatTab);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,37 +11,48 @@ import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/** Responsible for the presentation of the ChatModel to the Client */
|
||||
public class ChatViewController implements Initializable {
|
||||
|
||||
private final ChatBoxController chatBoxController;
|
||||
@FXML private Button sendButton;
|
||||
|
||||
@FXML private TextField inputField;
|
||||
|
||||
@FXML private VBox chatInterfaceVBox;
|
||||
|
||||
@FXML private HBox controlBar;
|
||||
|
||||
@FXML private ScrollPane scrollPane;
|
||||
|
||||
@FXML private VBox chat;
|
||||
|
||||
private final ChatModel chatModel;
|
||||
public Tab getChatTab() {
|
||||
return chatTab;
|
||||
}
|
||||
|
||||
private final String username;
|
||||
public void setChatTab(Tab chatTab) {
|
||||
this.chatTab = chatTab;
|
||||
}
|
||||
|
||||
@FXML private Tab chatTab;
|
||||
|
||||
public Boolean tabIsOpen;
|
||||
|
||||
private final ChatModel chatModel;
|
||||
|
||||
private final ChatController controller;
|
||||
|
||||
private static final int CHAT_PADDING = 20;
|
||||
|
||||
public ChatViewController(ChatController chatController, ChatModel chatModel, String username) {
|
||||
public ChatViewController(
|
||||
ChatController chatController,
|
||||
ChatModel chatModel,
|
||||
ChatBoxController chatBoxController) {
|
||||
this.controller = chatController;
|
||||
this.username = username;
|
||||
this.chatModel = chatModel;
|
||||
this.chatBoxController = chatBoxController;
|
||||
tabIsOpen = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,8 +65,8 @@ public class ChatViewController implements Initializable {
|
||||
*/
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resourceBundle) {
|
||||
inputField.setOnAction(event -> sendMessage());
|
||||
sendButton.setOnAction(event -> sendMessage());
|
||||
inputField.setOnAction(_ -> sendMessage());
|
||||
sendButton.setOnAction(_ -> sendMessage());
|
||||
scrollPane.vvalueProperty().bind(chat.heightProperty());
|
||||
}
|
||||
|
||||
@@ -68,11 +79,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);
|
||||
@@ -87,6 +99,10 @@ public class ChatViewController implements Initializable {
|
||||
* @param msg The {@link Message} object containing the content and metadata to display.
|
||||
*/
|
||||
public void showMessage(Message msg) {
|
||||
if (!tabIsOpen) {
|
||||
tabIsOpen = true;
|
||||
chatBoxController.reopenChatTab(chatTab);
|
||||
}
|
||||
Platform.runLater(
|
||||
() -> {
|
||||
String msgText =
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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)(\\?.*)?$");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,56 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username.ChangeUsernameHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username.ChangeUsernameParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username.ChangeUsernameRequest;
|
||||
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;
|
||||
@@ -24,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;
|
||||
@@ -101,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);
|
||||
@@ -168,6 +180,15 @@ public class ServerApp {
|
||||
commandRouter.register(
|
||||
LoginRequest.class, new LoginHandler(responseDispatcher, userRegistry));
|
||||
|
||||
parserDispatcher.register("CHANGE_USERNAME", new ChangeUsernameParser());
|
||||
commandRouter.register(
|
||||
ChangeUsernameRequest.class,
|
||||
new ChangeUsernameHandler(
|
||||
responseDispatcher,
|
||||
userRegistry,
|
||||
context.lobbyManager(),
|
||||
context.sessionManager()));
|
||||
|
||||
parserDispatcher.register("LOGOUT", new LogoutParser());
|
||||
commandRouter.register(
|
||||
LogoutRequest.class, new LogoutHandler(responseDispatcher, userRegistry));
|
||||
@@ -192,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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username;
|
||||
|
||||
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.request.RequestContext;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
|
||||
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.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/** Handles CHANGE_USERNAME requests for already logged-in users. */
|
||||
public class ChangeUsernameHandler extends CommandHandler<ChangeUsernameRequest> {
|
||||
private static final Pattern VALID_USERNAME = Pattern.compile("[a-zA-Z0-9_-]+");
|
||||
private final UserRegistry userRegistry;
|
||||
private final LobbyManager lobbyManager;
|
||||
private final SessionManager sessionManager;
|
||||
|
||||
/**
|
||||
* @param responseDispatcher dispatcher used for responses
|
||||
* @param userRegistry registry containing all users
|
||||
* @param lobbyManager lobby manager used to keep lobby/game mappings in sync
|
||||
* @param sessionManager session manager used to broadcast rename events
|
||||
*/
|
||||
public ChangeUsernameHandler(
|
||||
ResponseDispatcher responseDispatcher,
|
||||
UserRegistry userRegistry,
|
||||
LobbyManager lobbyManager,
|
||||
SessionManager sessionManager) {
|
||||
super(responseDispatcher);
|
||||
this.userRegistry = userRegistry;
|
||||
this.lobbyManager = lobbyManager;
|
||||
this.sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(ChangeUsernameRequest request) {
|
||||
Optional<User> user = userRegistry.getBySessionId(request.getSessionId());
|
||||
if (user.isEmpty()) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(),
|
||||
"USER_NOT_LOGGED_IN",
|
||||
"This session is not associated with an active user."));
|
||||
return;
|
||||
}
|
||||
|
||||
String newUsername = request.getUsername() == null ? "" : request.getUsername().trim();
|
||||
if (newUsername.isEmpty() || !VALID_USERNAME.matcher(newUsername).matches()) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(),
|
||||
"INVALID_USERNAME",
|
||||
"Only letters, numbers, '_' and '-' are allowed."));
|
||||
return;
|
||||
}
|
||||
|
||||
User currentUser = user.get();
|
||||
String oldUsername = currentUser.getName();
|
||||
boolean changed = userRegistry.changeUsername(currentUser.getId(), newUsername);
|
||||
if (!changed) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(),
|
||||
"USERNAME_TAKEN",
|
||||
"The requested username is already taken."));
|
||||
return;
|
||||
}
|
||||
|
||||
boolean lobbySynced =
|
||||
lobbyManager == null || lobbyManager.renamePlayer(oldUsername, newUsername);
|
||||
if (!lobbySynced) {
|
||||
userRegistry.changeUsername(currentUser.getId(), oldUsername);
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
request.getContext(),
|
||||
"RENAME_CONFLICT",
|
||||
"Could not update username in current lobby/game state."));
|
||||
return;
|
||||
}
|
||||
|
||||
responseDispatcher.dispatch(
|
||||
new ChangeUsernameResponse(
|
||||
request.getContext(), currentUser.getName(), currentUser.getId()));
|
||||
|
||||
broadcastUsernameChanged(oldUsername, currentUser.getName());
|
||||
}
|
||||
|
||||
private void broadcastUsernameChanged(String oldUsername, String newUsername) {
|
||||
if (sessionManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Session session : sessionManager.getAllSessions()) {
|
||||
RequestContext ctx = new RequestContext(session.getId(), 0);
|
||||
SuccessResponse ev =
|
||||
new SuccessResponse(
|
||||
ctx,
|
||||
new ResponseBodyBuilder()
|
||||
.param("EVENT", "USERNAME_CHANGED")
|
||||
.param("OLD_USERNAME", oldUsername)
|
||||
.param("NEW_USERNAME", newUsername)
|
||||
.build()) {};
|
||||
responseDispatcher.dispatch(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||
|
||||
/** Parses CHANGE_USERNAME requests. */
|
||||
public class ChangeUsernameParser implements CommandParser<ChangeUsernameRequest> {
|
||||
@Override
|
||||
public ChangeUsernameRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
RequestParameterAccessor accessor =
|
||||
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||
return new ChangeUsernameRequest(primitiveRequest.context(), accessor.require("USERNAME"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username;
|
||||
|
||||
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 used to change the username of the current session user. */
|
||||
public class ChangeUsernameRequest extends Request {
|
||||
private final String username;
|
||||
|
||||
/**
|
||||
* @param context request context for responses
|
||||
* @param username desired new username
|
||||
*/
|
||||
public ChangeUsernameRequest(RequestContext context, String username) {
|
||||
super(context);
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return desired new username
|
||||
*/
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
|
||||
|
||||
/** Response for successful username changes. */
|
||||
public class ChangeUsernameResponse extends SuccessResponse {
|
||||
/**
|
||||
* @param context request context
|
||||
* @param username current username after the rename operation
|
||||
* @param id user id of renamed user
|
||||
*/
|
||||
public ChangeUsernameResponse(RequestContext context, String username, UserId id) {
|
||||
super(
|
||||
context,
|
||||
new ResponseBodyBuilder()
|
||||
.param("USERNAME", username)
|
||||
.param("ID", id.value())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
Before Width: | Height: | Size: 645 KiB After Width: | Height: | Size: 469 KiB |
|
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"
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
|
||||
<?import javafx.scene.control.skin.TabPaneSkin.TabContentRegion?>
|
||||
<VBox xmlns="http://javafx.com/javafx/21"
|
||||
xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:id="chatBox"
|
||||
VBox.vgrow="ALWAYS"
|
||||
stylesheets="@chatui.css"
|
||||
styleClass="chat-box"
|
||||
spacing="10"
|
||||
spacing="20"
|
||||
maxWidth="Infinity">
|
||||
|
||||
<HBox spacing="10"
|
||||
@@ -17,14 +18,14 @@
|
||||
|
||||
<Label text="== CHAT ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/>
|
||||
<Region HBox.hgrow="ALWAYS"/>
|
||||
<MenuButton fx:id="addWhisperChatButton" styleClass="yellow-button" text="WHISPER CHAT"
|
||||
<MenuButton fx:id="addWhisperChatButton" styleClass="menu-button" text="WHISPER CHAT"
|
||||
alignment="CENTER_RIGHT">
|
||||
</MenuButton>
|
||||
</HBox>
|
||||
|
||||
<TabPane fx:id="chatTabPane"
|
||||
VBox.vgrow="ALWAYS"
|
||||
styleClass="tab-header-area">
|
||||
styleClass="tab-pane">
|
||||
|
||||
</TabPane>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<VBox fx:id="chatInterfaceVBox"
|
||||
maxWidth="Infinity"
|
||||
spacing="10"
|
||||
styleClass="chat-box"
|
||||
styleClass="tab-chat-box"
|
||||
stylesheets="@chatui.css"
|
||||
VBox.vgrow="ALWAYS"
|
||||
xmlns="http://javafx.com/javafx/17.0.12"
|
||||
|
||||
@@ -5,7 +5,17 @@
|
||||
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||
-fx-border-width: 12;
|
||||
-fx-padding: 20;
|
||||
/*-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);*/
|
||||
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);
|
||||
}
|
||||
|
||||
.tab-chat-box {
|
||||
|
||||
-fx-background-color: #0d9e3b;
|
||||
-fx-background-radius: 58;
|
||||
-fx-border-radius: 50;
|
||||
-fx-border-color: #0d763b #0d623b #0d623b #0d763b;
|
||||
-fx-border-width: 12;
|
||||
-fx-padding: 20;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
@@ -163,25 +173,25 @@
|
||||
-fx-border-width: 5;
|
||||
-fx-padding: 10;
|
||||
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);
|
||||
-fx-max-height: 10;
|
||||
-fx-min-height: 10;
|
||||
}
|
||||
|
||||
.tab-pane .tab {
|
||||
-fx-background-color: #0d9e3b;
|
||||
-fx-background-radius: 18;
|
||||
-fx-background-color: #0d983b;
|
||||
-fx-background-radius: 28;
|
||||
-fx-border-radius: 10;
|
||||
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||
-fx-border-color: #0d873b #0d733b #0d733b #0d873b;
|
||||
-fx-border-width: 5;
|
||||
-fx-padding: 10;
|
||||
-fx-padding: 6 15;
|
||||
}
|
||||
|
||||
.tab-pane .tab:selected {
|
||||
-fx-background-color: #0d763b;
|
||||
-fx-background-color: #0d873b;
|
||||
-fx-background-radius: 18;
|
||||
-fx-border-radius: 10;
|
||||
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||
-fx-border-color: #0d763b #0d623b #0d623b #0d763b;
|
||||
-fx-border-width: 5;
|
||||
-fx-padding: 10;
|
||||
-fx-padding: 6 15;
|
||||
}
|
||||
|
||||
.tab-pane .tab .tab-close-button {
|
||||
@@ -192,7 +202,6 @@
|
||||
-fx-background-color: #0d9e3b;
|
||||
}
|
||||
|
||||
|
||||
.tab .tab-label {
|
||||
-fx-alignment: CENTER;
|
||||
-fx-text-fill: #ffffff;
|
||||
@@ -204,3 +213,36 @@
|
||||
-fx-alignment: CENTER;
|
||||
-fx-text-fill: #ffffff;
|
||||
}
|
||||
|
||||
.menu-button {
|
||||
-fx-background-color: #b8860b;
|
||||
-fx-border-color: #ffd700;
|
||||
-fx-text-fill: #ffffff;
|
||||
-fx-background-radius: 12;
|
||||
-fx-border-radius: 12;
|
||||
-fx-font-family: "Courier New";
|
||||
-fx-font-weight: bold;
|
||||
-fx-border-width: 2;
|
||||
-fx-padding: 6 15;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.menu-button .context-menu {
|
||||
-fx-background-color: #b8860b;
|
||||
-fx-background-radius: 4;
|
||||
-fx-padding: 6 15;
|
||||
-fx-cursor: hand;
|
||||
|
||||
}
|
||||
|
||||
.menu-button:hover {
|
||||
-fx-translate-y: -3;
|
||||
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||
}
|
||||
|
||||
.menu-button .menu-item {
|
||||
-fx-background-color: #b8860b;
|
||||
-fx-border-color: #ffd700;
|
||||
-fx-text-fill: #ffffff
|
||||
}
|
||||
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.*?>
|
||||
<?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.
|
||||
|
||||
Der Chat unterscheidet intern zwischen:
|
||||
- Player-to-Player (Privater 2-Personen-Chat)
|
||||
- Lobby-Chat (Aktueller Raum)
|
||||
- Globaler Chat (Gesamter Server)
|
||||
|
||||
Features, die später implementiert werden sollen:
|
||||
- Schicke Chat-Bubbles mit Namen und Uhrzeit.
|
||||
-->
|
||||
|
||||
<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"
|
||||
alignment="CENTER"
|
||||
stylesheets="@chatui.css">
|
||||
|
||||
<!-- Innenabstand: Schafft oben, rechts und unten 30 Pixel Platz, links nur 10 Pixel (asymmetrisch) -->
|
||||
<padding>
|
||||
<Insets top="30" right="30" bottom="30" left="10"/>
|
||||
</padding>
|
||||
|
||||
<children>
|
||||
<VBox VBox.vgrow="ALWAYS"
|
||||
styleClass="chat-box"
|
||||
spacing="10"
|
||||
maxWidth="Infinity">
|
||||
|
||||
<children>
|
||||
<Label text="== CHAT ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/>
|
||||
<Region minHeight="4" styleClass="chat-separator"/>
|
||||
|
||||
<!-- Chatnachrichten werden hier hinzugefügt -->
|
||||
<ScrollPane fx:id="chatScrollPane" fitToWidth="true" vbarPolicy="AS_NEEDED" hbarPolicy="NEVER" VBox.vgrow="ALWAYS" styleClass="chat-scroll-pane">
|
||||
<content>
|
||||
<VBox fx:id="chatVBox" spacing="10" styleClass="chat-VBox"/>
|
||||
</content>
|
||||
</ScrollPane>
|
||||
|
||||
<!-- Eingabebereich -->
|
||||
<HBox spacing="10">
|
||||
<!-- TODO: Größe des TextFields dynamisch anpassen -->
|
||||
<TextField fx:id="inputField" HBox.hgrow="ALWAYS" promptText="Nachricht eingeben..." styleClass="gray-input-field" onAction="#sendMessage"/>
|
||||
<Button fx:id="sendButton" text="SENDEN" onAction="#sendMessage" styleClass="yellow-button"/>
|
||||
<Button fx:id="refreshButton" text="NEU" onAction="#showMessage" styleClass="yellow-button"/>
|
||||
</HBox>
|
||||
</children>
|
||||
</VBox>
|
||||
</children>
|
||||
</VBox>
|
||||
@@ -1,155 +0,0 @@
|
||||
.chat-box {
|
||||
-fx-background-color: #0d9e3b;
|
||||
-fx-background-radius: 58;
|
||||
-fx-border-radius: 50;
|
||||
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||
-fx-border-width: 12;
|
||||
-fx-padding: 20;
|
||||
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
-fx-text-fill: #ffffff;
|
||||
-fx-font-size: 22px;
|
||||
-fx-font-weight: bold;
|
||||
-fx-padding: 0 0 10 0;
|
||||
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);
|
||||
}
|
||||
|
||||
.chat-separator {
|
||||
-fx-background-color: #ffffff;
|
||||
-fx-min-height: 4px;
|
||||
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);
|
||||
}
|
||||
|
||||
.black-input-field {
|
||||
-fx-background-color: #1a1a1a;
|
||||
-fx-text-fill: #ffffff;
|
||||
-fx-font-family: "Courier New";
|
||||
-fx-font-weight: bold;
|
||||
-fx-background-radius: 8;
|
||||
-fx-border-radius: 8;
|
||||
-fx-border-color: #444444;
|
||||
-fx-border-width: 2;
|
||||
-fx-padding: 5 12;
|
||||
}
|
||||
|
||||
.black-input-field:hover {
|
||||
-fx-translate-y: -3;
|
||||
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||
}
|
||||
|
||||
|
||||
.black-input-field:focused {
|
||||
-fx-border-color: #ffffff;
|
||||
-fx-background-color: #000000;
|
||||
}
|
||||
|
||||
.gray-input-field {
|
||||
-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: #666666;
|
||||
-fx-color-color: #cccccc;
|
||||
-fx-border-width: 2;
|
||||
-fx-padding: 5 12;
|
||||
}
|
||||
|
||||
.gray-input-field:hover {
|
||||
-fx-translate-y: -3;
|
||||
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||
}
|
||||
|
||||
.gray-input-field:focused {
|
||||
-fx-border-color: #ffffff;
|
||||
-fx-background-color: #333333;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.gray-button {
|
||||
-fx-background-color: #333333;
|
||||
-fx-border-color: #666666;
|
||||
-fx-text-fill: #cccccc;
|
||||
}
|
||||
|
||||
.gray-button, .yellow-button, .red-button {
|
||||
-fx-background-radius: 12;
|
||||
-fx-border-radius: 12;
|
||||
-fx-font-family: "Courier New";
|
||||
-fx-font-weight: bold;
|
||||
-fx-border-width: 2;
|
||||
-fx-padding: 6 15;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.gray-button:hover, .yellow-button:hover, .red-button:hover {
|
||||
-fx-translate-y: -3;
|
||||
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||
}
|
||||
|
||||
.chat-scroll-pane {
|
||||
-fx-background-color: transparent;
|
||||
-fx-background: transparent;
|
||||
-fx-border-color: transparent;
|
||||
}
|
||||
|
||||
.chat-VBox {
|
||||
-fx-background-color: transparent;
|
||||
-fx-background: transparent;
|
||||
}
|
||||
.scroll-pane {
|
||||
-fx-background-color: transparent;
|
||||
-fx-border-color: transparent;
|
||||
}
|
||||
|
||||
.scroll-pane .scroll-bar:vertical {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.scroll-pane .scroll-bar .track {
|
||||
-fx-background-color: #5c3d10;
|
||||
-fx-background-insets: 0;
|
||||
-fx-background-radius: 5;
|
||||
}
|
||||
|
||||
.scroll-pane .scroll-bar .thumb {
|
||||
-fx-background-color: #3d260a;
|
||||
-fx-background-insets: 0;
|
||||
-fx-background-radius: 5;
|
||||
-fx-pref-width: 6;
|
||||
-fx-pref-height: 6;
|
||||
-fx-padding: 0;
|
||||
}
|
||||
|
||||
.scroll-pane .scroll-bar:horizontal {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.scroll-pane .scroll-bar:horizontal .track {
|
||||
-fx-background-color: #5c3d10;
|
||||
-fx-background-radius: 5;
|
||||
}
|
||||
|
||||
.scroll-pane .scroll-bar:horizontal .thumb {
|
||||
-fx-background-color: #3d260a;
|
||||
-fx-background-radius: 5;
|
||||
-fx-pref-height: 6;
|
||||
}
|
||||
|
||||
.scroll-pane .scroll-bar:horizontal .thumb:hover {
|
||||
-fx-pref-height: 10;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
public class ChatClientTest implements ChatClientInterface {
|
||||
|
||||
private ChatClientTest otherChatClient;
|
||||
|
||||
public final Queue<Message> messageQueue;
|
||||
|
||||
public ChatClientTest() {
|
||||
this.messageQueue = new LinkedList<>();
|
||||
}
|
||||
|
||||
public ChatClientTest(ChatClientTest otherChatClient) {
|
||||
this.otherChatClient = otherChatClient;
|
||||
this.messageQueue = new LinkedList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(Message message) {
|
||||
otherChatClient.messageQueue.add(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> getMessages() {
|
||||
List<Message> messages = new LinkedList<>();
|
||||
while (!messageQueue.isEmpty()) {
|
||||
Message message = messageQueue.poll();
|
||||
messages.add(message);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getUsers() {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,151 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class ChatControllerTest {
|
||||
|
||||
ChatController chatController1;
|
||||
ChatController chatController2;
|
||||
|
||||
String name1 = "testperson1";
|
||||
String name2 = "testperson1";
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {}
|
||||
public void setup() {
|
||||
ChatClientTest receivingChatClient = new ChatClientTest();
|
||||
ChatClientTest sendingChatClient = new ChatClientTest(receivingChatClient);
|
||||
this.chatController1 = new ChatController(name1, sendingChatClient);
|
||||
this.chatController2 = new ChatController(name2, receivingChatClient);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void teardown() {}
|
||||
|
||||
/*
|
||||
* @Test
|
||||
* public void testChatController() {
|
||||
* Client client = new Client("user1");
|
||||
* client.startApplication();
|
||||
* }
|
||||
*/
|
||||
@Test
|
||||
public void sendGlobalMessage() {
|
||||
ChatModel global = new ChatModel(ChatType.GLOBAL, name2, -1, null);
|
||||
chatController2.getChatModelMap().put(new ChatController.ChatKey(ChatType.GLOBAL), global);
|
||||
|
||||
Message msg1 = new Message(ChatType.GLOBAL, -1, name1, null, "Hallo Welt");
|
||||
Message msg2 = new Message(ChatType.GLOBAL, -1, name1, null, "$$Hallo$$Welt$$");
|
||||
Message msg3 = new Message(ChatType.GLOBAL, -1, name1, null, "_*'=(&%$/§&%");
|
||||
Message msg4 = new Message(ChatType.LOBBY, -1, name1, null, "Guten Tag");
|
||||
Message msg5 = new Message(ChatType.LOBBY, -1, name1, null, "$/&)$)(&=)$=");
|
||||
Message msg6 = new Message(ChatType.GLOBAL, -1, name1, null, "NAME=ANDERER_NUTZER");
|
||||
Message msg7 = new Message(ChatType.GLOBAL, -1, name1, null, "________");
|
||||
|
||||
ArrayList<Message> messages = new ArrayList<>();
|
||||
messages.add(msg1);
|
||||
messages.add(msg2);
|
||||
messages.add(msg3);
|
||||
messages.add(msg4);
|
||||
messages.add(msg5);
|
||||
messages.add(msg6);
|
||||
messages.add(msg7);
|
||||
|
||||
chatController1.onSendToNetwork(msg1);
|
||||
chatController1.onSendToNetwork(msg2);
|
||||
chatController1.onSendToNetwork(msg3);
|
||||
chatController1.onSendToNetwork(msg4);
|
||||
chatController1.onSendToNetwork(msg5);
|
||||
chatController1.onSendToNetwork(msg6);
|
||||
chatController1.onSendToNetwork(msg7);
|
||||
|
||||
chatController2.receiveMessage();
|
||||
|
||||
assertEquals(5, global.messages.size());
|
||||
|
||||
assertEquals(messages.get(0), global.messages.get(0));
|
||||
assertEquals(messages.get(1), global.messages.get(1));
|
||||
assertEquals(messages.get(2), global.messages.get(2));
|
||||
|
||||
assertNotEquals(messages.get(3), global.messages.get(3));
|
||||
assertNotEquals(messages.get(4), global.messages.get(4));
|
||||
|
||||
assertEquals(messages.get(5), global.messages.get(3));
|
||||
assertEquals(messages.get(6), global.messages.get(4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendLobbyMessage() {
|
||||
ChatModel lobby = new ChatModel(ChatType.LOBBY, name2, 1, null);
|
||||
chatController2.getChatModelMap().put(new ChatController.ChatKey(ChatType.LOBBY), lobby);
|
||||
|
||||
Message msg1 = new Message(ChatType.LOBBY, 1, name1, null, "Hallo Welt");
|
||||
Message msg2 = new Message(ChatType.LOBBY, 1, name1, null, "$$Hallo$$Welt$$");
|
||||
Message msg3 = new Message(ChatType.LOBBY, 1, name1, null, "_*'=(&%$/§&%");
|
||||
Message msg4 = new Message(ChatType.LOBBY, 4, name1, null, "Guten Tag");
|
||||
Message msg5 = new Message(ChatType.LOBBY, 5, name1, null, "$/&)$)(&=)$=");
|
||||
Message msg6 = new Message(ChatType.LOBBY, 1, name1, null, "NAME=ANDERER_NUTZER");
|
||||
Message msg7 = new Message(ChatType.LOBBY, 1, name1, null, "________");
|
||||
|
||||
ArrayList<Message> messages = new ArrayList<>();
|
||||
messages.add(msg1);
|
||||
messages.add(msg2);
|
||||
messages.add(msg3);
|
||||
messages.add(msg4);
|
||||
messages.add(msg5);
|
||||
messages.add(msg6);
|
||||
messages.add(msg7);
|
||||
|
||||
for (Message msg : messages) {
|
||||
chatController1.onSendToNetwork(msg);
|
||||
}
|
||||
|
||||
chatController2.receiveMessage();
|
||||
|
||||
assertEquals(5, lobby.messages.size());
|
||||
|
||||
assertEquals(messages.get(0), lobby.messages.get(0));
|
||||
assertEquals(messages.get(1), lobby.messages.get(1));
|
||||
assertEquals(messages.get(2), lobby.messages.get(2));
|
||||
|
||||
assertNotEquals(messages.get(3), lobby.messages.get(3));
|
||||
assertNotEquals(messages.get(4), lobby.messages.get(4));
|
||||
|
||||
assertEquals(messages.get(5), lobby.messages.get(3));
|
||||
assertEquals(messages.get(6), lobby.messages.get(4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendWhisperMessage() {
|
||||
ChatModel whisper = new ChatModel(ChatType.WHISPER, name2, -1, name1);
|
||||
chatController2
|
||||
.getChatModelMap()
|
||||
.put(new ChatController.ChatKey(ChatType.WHISPER, name1), whisper);
|
||||
|
||||
Message msg1 = new Message(ChatType.WHISPER, -1, name1, name2, "Hallo Welt");
|
||||
Message msg2 = new Message(ChatType.WHISPER, -1, name1, name2, "$$Hallo$$Welt$$");
|
||||
Message msg3 = new Message(ChatType.WHISPER, -1, name1, name2, "_*'=(&%$/§&%");
|
||||
Message msg4 = new Message(ChatType.WHISPER, -1, name1, name2, "NAME=ANDERER_NUTZER");
|
||||
Message msg5 = new Message(ChatType.WHISPER, -1, name1, name2, "________");
|
||||
|
||||
ArrayList<Message> messages = new ArrayList<>();
|
||||
messages.add(msg1);
|
||||
messages.add(msg2);
|
||||
messages.add(msg3);
|
||||
messages.add(msg4);
|
||||
messages.add(msg5);
|
||||
|
||||
for (Message msg : messages) {
|
||||
chatController1.onSendToNetwork(msg);
|
||||
}
|
||||
|
||||
chatController2.receiveMessage();
|
||||
|
||||
assertEquals(5, whisper.messages.size());
|
||||
|
||||
assertEquals(messages.get(0), whisper.messages.get(0));
|
||||
assertEquals(messages.get(1), whisper.messages.get(1));
|
||||
assertEquals(messages.get(2), whisper.messages.get(2));
|
||||
|
||||
assertEquals(messages.get(3), whisper.messages.get(3));
|
||||
assertEquals(messages.get(4), whisper.messages.get(4));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
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 java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class TaskbarControllerInputValidationTest {
|
||||
|
||||
@Test
|
||||
void updateLogicRunsWithoutCrash() {
|
||||
GameState state = new GameState();
|
||||
|
||||
state.players = List.of(
|
||||
new Player(PlayerId.of("me"), 20000),
|
||||
new Player(PlayerId.of("other"), 20000)
|
||||
);
|
||||
|
||||
state.phase = "PREFLOP";
|
||||
state.currentBet = 200;
|
||||
state.activePlayer = 0;
|
||||
|
||||
DummyController controller = new DummyController();
|
||||
|
||||
controller.update(state, PlayerId.of("me"));
|
||||
|
||||
assertTrue(true);
|
||||
}
|
||||
|
||||
static class DummyController {
|
||||
void update(GameState state, PlayerId id) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class CommandParserDispatcherTest {
|
||||
private class DummyRequest extends Request {
|
||||
public DummyRequest(RequestContext context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccessfulParsing() {
|
||||
CommandParserDispatcher dispatcher = new CommandParserDispatcher();
|
||||
|
||||
RequestContext ctx = new RequestContext(new SessionId(UUID.randomUUID()), 7411);
|
||||
PrimitiveRequest primitiveRequest = new PrimitiveRequest(ctx, "TEST_CMD", List.of());
|
||||
|
||||
dispatcher.register(
|
||||
"TEST_CMD",
|
||||
new CommandParser<Request>() {
|
||||
@Override
|
||||
public Request parse(PrimitiveRequest primitiveRequest) {
|
||||
return new DummyRequest(primitiveRequest.context());
|
||||
}
|
||||
});
|
||||
|
||||
Request result = dispatcher.parse(primitiveRequest);
|
||||
|
||||
assertTrue(result instanceof DummyRequest);
|
||||
assertEquals(ctx, result.getContext());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnknownCommandThrows() {
|
||||
CommandParserDispatcher dispatcher = new CommandParserDispatcher();
|
||||
|
||||
RequestContext ctx = new RequestContext(new SessionId(UUID.randomUUID()), 42);
|
||||
PrimitiveRequest primitiveRequest = new PrimitiveRequest(ctx, "UNKNOWN_CMD", List.of());
|
||||
|
||||
UnknownCommandException ex =
|
||||
assertThrows(
|
||||
UnknownCommandException.class, () -> dispatcher.parse(primitiveRequest));
|
||||
|
||||
assertEquals("UNKNOWN_CMD", ex.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.network.events;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class EventBusTest {
|
||||
private EventBus eventBus;
|
||||
|
||||
private class TestEvent implements Event {}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
eventBus = new EventBus();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSingleSubscriberReceivesEvent() {
|
||||
AtomicBoolean called = new AtomicBoolean(false);
|
||||
eventBus.subscribe(TestEvent.class, e -> called.set(true));
|
||||
eventBus.publish(new TestEvent());
|
||||
assertTrue(called.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleSubscribersReceiveEvent() {
|
||||
AtomicInteger counter = new AtomicInteger(0);
|
||||
eventBus.subscribe(TestEvent.class, e -> counter.incrementAndGet());
|
||||
eventBus.subscribe(TestEvent.class, e -> counter.incrementAndGet());
|
||||
eventBus.publish(new TestEvent());
|
||||
assertEquals(2, counter.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublishNullThrowsNpe() {
|
||||
assertThrows(NullPointerException.class, () -> eventBus.publish((Event) null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubscribingToBaseEventDoesNotReceiveSubclass() {
|
||||
AtomicBoolean called = new AtomicBoolean(false);
|
||||
eventBus.subscribe(Event.class, e -> called.set(true));
|
||||
eventBus.publish(new TestEvent());
|
||||
assertFalse(called.get());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class ResponseBodyBuilderTest {
|
||||
@Test
|
||||
void testSingleParameter() {
|
||||
ResponseBody body = ResponseBody.builder().param("param1", "value1").build();
|
||||
|
||||
assertEquals(1, body.nodes().size());
|
||||
ResponseParameter parameter = (ResponseParameter) body.nodes().get(0);
|
||||
assertEquals("param1", parameter.key());
|
||||
assertEquals("value1", parameter.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSingleBlock() {
|
||||
ResponseBody body =
|
||||
ResponseBody.builder()
|
||||
.block("block1", block -> block.param("param1", "value1"))
|
||||
.build();
|
||||
|
||||
assertEquals(1, body.nodes().size());
|
||||
|
||||
ResponseBlock block = (ResponseBlock) body.nodes().get(0);
|
||||
assertEquals(1, block.children().size());
|
||||
assertEquals("block1", block.tag());
|
||||
|
||||
ResponseParameter parameter = (ResponseParameter) block.children().get(0);
|
||||
assertEquals("param1", parameter.key());
|
||||
assertEquals("value1", parameter.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleParameter() {
|
||||
ResponseBody body =
|
||||
ResponseBody.builder().param("param1", "value1").param("param2", "value2").build();
|
||||
|
||||
assertEquals(2, body.nodes().size());
|
||||
ResponseParameter parameter;
|
||||
|
||||
parameter = (ResponseParameter) body.nodes().get(0);
|
||||
assertEquals("param1", parameter.key());
|
||||
assertEquals("value1", parameter.value());
|
||||
|
||||
parameter = (ResponseParameter) body.nodes().get(1);
|
||||
assertEquals("param2", parameter.key());
|
||||
assertEquals("value2", parameter.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleBlocks() {
|
||||
ResponseBody body =
|
||||
ResponseBody.builder()
|
||||
.block("block1", block -> block.param("param1", "value1"))
|
||||
.block(
|
||||
"block2",
|
||||
block -> {
|
||||
block.param("param2", "value2");
|
||||
block.param("param3", "value3");
|
||||
})
|
||||
.build();
|
||||
|
||||
assertEquals(2, body.nodes().size());
|
||||
ResponseBlock block;
|
||||
ResponseParameter parameter;
|
||||
|
||||
block = (ResponseBlock) body.nodes().get(0);
|
||||
assertEquals("block1", block.tag());
|
||||
assertEquals(1, block.children().size());
|
||||
parameter = (ResponseParameter) block.children().get(0);
|
||||
assertEquals("param1", parameter.key());
|
||||
assertEquals("value1", parameter.value());
|
||||
|
||||
block = (ResponseBlock) body.nodes().get(1);
|
||||
assertEquals("block2", block.tag());
|
||||
assertEquals(2, block.children().size());
|
||||
parameter = (ResponseParameter) block.children().get(0);
|
||||
assertEquals("param2", parameter.key());
|
||||
assertEquals("value2", parameter.value());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.PrimitiveResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.Response;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBlock;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseParameter;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class ResponseEncoderTest {
|
||||
private class TestResponse extends Response {
|
||||
public TestResponse(RequestContext context, ResponseBody body) {
|
||||
super(context, body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String prefix() {
|
||||
return "+OK";
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEncodeParameterSimple() {
|
||||
ResponseBody body = ResponseBody.builder().param("param", "value").build();
|
||||
|
||||
RequestContext ctx = new RequestContext(new SessionId(), 42);
|
||||
Response response = new TestResponse(ctx, body);
|
||||
|
||||
PrimitiveResponse pr = ResponseEncoder.encode(response);
|
||||
|
||||
String expected = "+OK\n\tparam=value\nEND";
|
||||
assertEquals(expected, pr.payload());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEncodeParameterMasking() {
|
||||
ResponseBody body = ResponseBody.builder().param("msg", "It's a me, malario").build();
|
||||
|
||||
RequestContext ctx = new RequestContext(new SessionId(), 42);
|
||||
Response response = new TestResponse(ctx, body);
|
||||
|
||||
PrimitiveResponse pr = ResponseEncoder.encode(response);
|
||||
String expected = "+OK\n\tmsg='It\\'s a me, malario'\nEND";
|
||||
assertEquals(expected, pr.payload());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEncodeNestedBlock() {
|
||||
ResponseBody body =
|
||||
ResponseBody.builder().block("TAG", b -> b.param("param", "key")).build();
|
||||
|
||||
RequestContext ctx = new RequestContext(new SessionId(UUID.randomUUID()), 42);
|
||||
Response response = new TestResponse(ctx, body);
|
||||
|
||||
PrimitiveResponse pr = ResponseEncoder.encode(response);
|
||||
|
||||
String expected = "+OK\n\tTAG\n\t\tparam=key\n\tEND\nEND";
|
||||
assertEquals(expected, pr.payload());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPrivateEncodeParameter()
|
||||
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
|
||||
var param = new ResponseParameter("p", "v");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
Method m =
|
||||
ResponseEncoder.class.getDeclaredMethod(
|
||||
"encodeParameter", ResponseParameter.class, StringBuilder.class, int.class);
|
||||
m.setAccessible(true);
|
||||
m.invoke(null, param, sb, 1);
|
||||
|
||||
assertEquals("\tp=v", sb.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWordNoMask()
|
||||
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
|
||||
Method m = ResponseEncoder.class.getDeclaredMethod("maskIfNeeded", String.class);
|
||||
m.setAccessible(true);
|
||||
|
||||
String noMask = (String) m.invoke(null, "abc");
|
||||
assertEquals("abc", noMask);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMaskString()
|
||||
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
|
||||
Method m = ResponseEncoder.class.getDeclaredMethod("maskIfNeeded", String.class);
|
||||
m.setAccessible(true);
|
||||
|
||||
String withSpace = (String) m.invoke(null, "a b");
|
||||
assertEquals("'a b'", withSpace);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEscapeSingleQuote()
|
||||
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
|
||||
Method m = ResponseEncoder.class.getDeclaredMethod("maskIfNeeded", String.class);
|
||||
m.setAccessible(true);
|
||||
|
||||
String withSpace = (String) m.invoke(null, "a b");
|
||||
assertEquals("'a b'", withSpace);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPrivateEncodeBlock()
|
||||
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
|
||||
var child = new ResponseParameter("a", "b");
|
||||
var block = new ResponseBlock("TAG", List.of(child));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
Method m =
|
||||
ResponseEncoder.class.getDeclaredMethod(
|
||||
"encodeBlock", ResponseBlock.class, StringBuilder.class, int.class);
|
||||
m.setAccessible(true);
|
||||
m.invoke(null, block, sb, 1);
|
||||
|
||||
String expected = "\tTAG\n\t\ta=b\n\tEND";
|
||||
assertEquals(expected, sb.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
|
||||
import java.lang.reflect.Field;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class SessionDisconnectJobTest {
|
||||
private EventBus eventBus;
|
||||
private SessionManager sessionManager;
|
||||
|
||||
@BeforeEach
|
||||
void initEnvironment() {
|
||||
eventBus = new EventBus();
|
||||
sessionManager = new SessionManager(eventBus, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisconnectValidSession() {
|
||||
SessionDisconnectJob disconnectJob =
|
||||
new SessionDisconnectJob(sessionManager, eventBus, Duration.ofSeconds(30));
|
||||
|
||||
sessionManager.create(null, null);
|
||||
sessionManager.create(null, null);
|
||||
|
||||
AtomicBoolean disconnectOccurred = new AtomicBoolean(false);
|
||||
eventBus.subscribe(DisconnectEvent.class, (event) -> disconnectOccurred.set(true));
|
||||
|
||||
disconnectJob.run();
|
||||
assertFalse(disconnectOccurred.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisconnectExpiredSession()
|
||||
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
|
||||
SessionDisconnectJob disconnectJob =
|
||||
new SessionDisconnectJob(sessionManager, eventBus, Duration.ofSeconds(30));
|
||||
|
||||
sessionManager.create(null, null);
|
||||
Session expiredSession = sessionManager.create(null, null);
|
||||
Field lastActivityField = Session.class.getDeclaredField("lastActivity");
|
||||
lastActivityField.setAccessible(true);
|
||||
lastActivityField.set(expiredSession, Instant.now().minus(35, ChronoUnit.SECONDS));
|
||||
|
||||
AtomicReference<DisconnectEvent> disconnectEvent = new AtomicReference<>();
|
||||
eventBus.subscribe(DisconnectEvent.class, (event) -> disconnectEvent.set(event));
|
||||
|
||||
disconnectJob.run();
|
||||
assertEquals(disconnectEvent.get().sessionId(), expiredSession.getId());
|
||||
}
|
||||
}
|
||||