Compare commits

...

40 Commits

Author SHA1 Message Date
Lars Simon Winzer 4f1624a30b Merge branch 'feat/136-write-unit-tests-for-commandparserdispatcher' into 'main'
Unit tests for CommandParserDispatcher

Closes #136

See merge request cs108-fs26/Gruppe-13!163
2026-04-27 01:09:29 +02:00
Lars Simon Winzer 363a0ed895 Test: Add unit tests for CommandParserDispatcher 2026-04-27 01:04:45 +02:00
Lars Simon Winzer ecf692fbcd Merge branch 'feat/135-write-unit-tests-for-eventbus' into 'main'
Unit tests for EventBus

Closes #135

See merge request cs108-fs26/Gruppe-13!162
2026-04-27 00:55:43 +02:00
Mathis Ginkel 4fd036173f Merge branch 'feat/chat-tests' into 'main'
Feat/chat tests

See merge request cs108-fs26/Gruppe-13!161
2026-04-26 22:52:30 +00:00
Lars Simon Winzer d36a78eee4 Style: Change order and visibility of nested class to private 2026-04-27 00:50:44 +02:00
Mathis Ginkel 8484f731a8 Style: Apply Spotless 2026-04-27 00:46:31 +02:00
Lars Simon Winzer b158be26e3 Style: Add 'test' prefix to methods 2026-04-27 00:45:53 +02:00
Mathis Ginkel 59af75a75c Merge branch 'main' into feat/chat-tests 2026-04-27 00:42:44 +02:00
Lars Simon Winzer 823b406fe6 Test: Add unit tests for EventBus 2026-04-27 00:41:17 +02:00
Lars Simon Winzer 71f2d3c2ce Merge branch 'feat/134-write-unit-tests-for-sessiondisconnectjob' into 'main'
Unit tests for SessionDisconnectJob

Closes #134

See merge request cs108-fs26/Gruppe-13!160
2026-04-27 00:38:34 +02:00
Lars Simon Winzer eb2b209e91 Test: Add unit tests for SessionDisconnectJob 2026-04-27 00:20:54 +02:00
Mathis Ginkel 5663807e29 Style: Fixing some linter warnings in ChatModel and Message classes 2026-04-27 00:17:34 +02:00
Mathis Ginkel fe83057049 Merge branch 'main' into feat/chat-tests
# Conflicts:
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatViewController.java
2026-04-27 00:07:26 +02:00
Mathis Ginkel e9b40e9d04 Style: Apply Spotless 2026-04-26 23:54:15 +02:00
Mathis Ginkel b7f5a268e3 Feat: Remove chatui directory in gameuicomponents 2026-04-26 23:51:45 +02:00
Mathis Ginkel 619f6eef84 Merge branch 'feat/chat-visual-improvement' into 'main'
Feat/chat visual improvement

Closes #107

See merge request cs108-fs26/Gruppe-13!159
2026-04-26 21:50:55 +00:00
Mathis Ginkel d8d6dbb34f Style: fixing some linter warnings in chat ui classes 2026-04-26 21:45:25 +00:00
Mathis Ginkel efd91fbe5f Style: Apply Spotless 2026-04-26 21:45:25 +00:00
Mathis Ginkel ad2accfced Fix: Reopen whisper chat when previously closed 2026-04-26 21:45:25 +00:00
Mathis Ginkel 4c35596f19 Feat: Change look of chat box 2026-04-26 21:45:25 +00:00
Mathis Ginkel 659e606fb4 Merge branch 'main' into feat/chat-tests 2026-04-26 23:37:36 +02:00
Mathis Ginkel 5358309e1a Test: Add test for ChatController 2026-04-26 23:36:50 +02:00
Julian Kropff dc2b3e05d9 Merge branch 'feat/game-ui-tests' into 'main'
Add: Game UI Taskbar tests

See merge request cs108-fs26/Gruppe-13!158
2026-04-26 21:36:17 +00:00
Julian Kropff 5d0a9b9c25 Merge branch 'feat/manual' into 'main'
Feat/manual

Closes #130

See merge request cs108-fs26/Gruppe-13!157
2026-04-26 20:45:05 +00:00
Mathis Ginkel 216c15e825 Merge branch 'refs/heads/main' into feat/chat-visual-improvement 2026-04-26 21:46:52 +02:00
Lars Simon Winzer 48bfef5fcb Merge branch 'chore/133-write-diary' into 'main'
Diary entry covering current state of unit tests and planned structural changes approaching MS6

Closes #132 and #133

See merge request cs108-fs26/Gruppe-13!155
2026-04-26 21:22:16 +02:00
Lars Simon Winzer a6e98b2453 Docs: Extend diary readme by newly added diary entry
Co-authored-by: Copilot <copilot@github.com>
2026-04-26 21:16:59 +02:00
Lars Simon Winzer 54a3f1e29d Docs: Diary entry covering current state of unit tests and prospective changes
Co-authored-by: Copilot <copilot@github.com>
2026-04-26 21:14:37 +02:00
Mathis Ginkel 1b18affd57 Fix: Reopen whisper chat when previously closed 2026-04-26 20:25:31 +02:00
Julian Kropff 5fc3f315fc Docs: update Casono Manual 2026-04-26 20:08:43 +02:00
Julian Kropff ac9396cfb2 Fix: improve code block rendering in Casono Markdown Render Engine 2026-04-26 20:07:07 +02:00
Julian Kropff c6772d6f89 Docs: add images for manual 2026-04-26 20:04:01 +02:00
Lars Simon Winzer d86b9b5838 Merge branch 'feat/131-write-unit-tests-for-responseencoder' into 'main'
Unit tests for ResponseEncoder

Closes #131

See merge request cs108-fs26/Gruppe-13!154
2026-04-26 19:16:26 +02:00
Lars Simon Winzer d91ee5b04f Test: Add unit tests for ResponseEncoder 2026-04-26 19:10:11 +02:00
Lars Simon Winzer 6b5d521ab9 Merge branch 'feat/129-write-unit-tests-for-responsebody-and-responsebodybuilder' into 'main'
Unit tests for ResponseBodyBuilder

Closes #129

See merge request cs108-fs26/Gruppe-13!153
2026-04-26 18:35:48 +02:00
Lars Simon Winzer 4ad9ae08c5 Test: Add unit tests for ResponseBodyBuilder 2026-04-26 18:15:53 +02:00
Mathis Ginkel 588bb1f312 Merge branch 'main' into feat/chat-visual-improvement 2026-04-26 16:58:45 +02:00
Mathis Ginkel ebca653ab4 Merge remote-tracking branch 'origin/feat/chat-visual-improvement' into feat/chat-visual-improvement 2026-04-24 14:17:04 +02:00
Mathis Ginkel 9df48478f3 Feat: Change look of chat box 2026-04-24 14:16:55 +02:00
Mathis Ginkel 192c8a2cb9 Feat: Change look of chat box 2026-04-21 22:18:06 +02:00
31 changed files with 1186 additions and 308 deletions
+11
View File
@@ -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.
+3 -1
View File
@@ -36,7 +36,9 @@ None, as milestone 3 covered the assessment of individual technical understandin
## Milestone 5 ## 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 ## Milestone 6
+218 -2
View File
@@ -1,9 +1,19 @@
# Casono Manual # 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 --> <!-- vim-markdown-toc GFM -->
* [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) * [1. Spielübersicht](#1-spielübersicht)
* [Grundregeln](#grundregeln) * [Grundregeln](#grundregeln)
* [2. Sitzposition & Dealer-Button](#2-sitzposition--dealer-button) * [2. Sitzposition & Dealer-Button](#2-sitzposition--dealer-button)
@@ -21,9 +31,64 @@
* [Ein Spieler eine Hand](#ein-spieler--eine-hand) * [Ein Spieler eine Hand](#ein-spieler--eine-hand)
* [Fehlerhafte Einsätze](#fehlerhafte-einsätze) * [Fehlerhafte Einsätze](#fehlerhafte-einsätze)
* [8. Strategische Einordnung](#8-strategische-einordnung) * [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 --> <!-- 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
![Create a Lobby](./images/create-a-lobby.png)
### Einer Lobby beitreten
![Join a Lobby](./images/join-a-lobby.png)
### Den Username ändern
![Change Username](./images/change-username.png)
### Highscores
![View Highscores](./images/view-high-scores.png)
## Game UI
![Game UI](./images/game-ui.png)
![Taskbar](./images/taskbar.png)
## Casono Browser
![Casono Browser](./images/casono-browser.png)
# Casono Rules
## 1. Spielübersicht ## 1. Spielübersicht
Texas Holdem ist ein strategisches Kartenspiel für mehrere Spieler. Texas Holdem ist ein strategisches Kartenspiel für mehrere Spieler.
@@ -160,3 +225,154 @@ Texas Holdem ist kein reines Glücksspiel. Der Erfolg basiert auf:
- Wahrscheinlichkeiten (Mathematik) - Wahrscheinlichkeiten (Mathematik)
- Einschätzung von Gegnern (Psychologie) - Einschätzung von Gegnern (Psychologie)
- Positionsspiel und Timing - 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.
![1. image](./images/1-1.svg)
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.
![2. image](./images/2-1.svg)
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**.
![3. image](./images/3-1.svg)
Mathis sieht seine Karten an und merkt, dass sie nicht gut sind, also foldet er und steigt aus.
![4. image](./images/4-1.svg)
Jona ist nun dran und entscheidet sich ebenfalls für einen Call, weil seine Hand spielbar ist.
![5. image](./images/5-1.svg)
Lars schaut seine Karten an, erkennt eine starke Hand und erhöht auf **1200 Chips**.
![6. image](./images/6-1.svg)
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).
![7. image](./images/7-1.svg)
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.
![8. image](./images/8-1.svg)
## 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.
![9. image](./images/9-1.png)
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**.
![10. image](./images/10-1.png)
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**.
![11. image](./images/11-1.png)
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:
![13. image](./images/13-1.png)
Lars: 1. Paar:
![14. image](./images/14-1.png)
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 🟡*
@@ -23,8 +23,9 @@ const marked = {
`; `;
}); });
md = md.replace(/```([\s\S]*?)```/g, (_, code) => { md = md.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
return `<pre class="cm-code"><code>${escapeHtml(code)}</code></pre>`; const langClass = lang ? ` language-${lang}` : "";
return `<pre class="cm-code${langClass}"><code>${escapeHtml(code.trim())}</code></pre>`;
}); });
md = md.replace(/!\[(.*?)\]\((.*?)\)/gim, md = md.replace(/!\[(.*?)\]\((.*?)\)/gim,
Binary file not shown.

After

Width:  |  Height:  |  Size: 740 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

+194 -2
View File
@@ -130,8 +130,49 @@
const markdown = ` const markdown = `
# Casono Manual # Casono Manual
Hinweis: Eine vereinfachte Einführung finden Sie in der Datei [Casono Rules Easy Description](casono-rules-easy-description.md). # Start Game
Dieses Dokument beschreibt den Ablauf eines Pokerspiels anhand eines Beispiels.
## 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
![Create a Lobby](./images/create-a-lobby.png)
### Einer Lobby beitreten
![Join a Lobby](./images/join-a-lobby.png)
### Den Username ändern
![Change Username](./images/change-username.png)
### Highscores
![View Highscores](./images/view-high-scores.png)
## Game UI
![Game UI](./images/game-ui.png)
![Taskbar](./images/taskbar.png)
## Casono Browser
![Casono Browser](./images/casono-browser.png)
# Casono Rules
## 1. Spielübersicht ## 1. Spielübersicht
@@ -269,6 +310,157 @@ Texas Holdem ist kein reines Glücksspiel. Der Erfolg basiert auf:
- Wahrscheinlichkeiten (Mathematik) - Wahrscheinlichkeiten (Mathematik)
- Einschätzung von Gegnern (Psychologie) - Einschätzung von Gegnern (Psychologie)
- Positionsspiel und Timing - 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.
![1. image](./images/1-1.svg)
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.
![2. image](./images/2-1.svg)
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**.
![3. image](./images/3-1.svg)
Mathis sieht seine Karten an und merkt, dass sie nicht gut sind, also foldet er und steigt aus.
![4. image](./images/4-1.svg)
Jona ist nun dran und entscheidet sich ebenfalls für einen Call, weil seine Hand spielbar ist.
![5. image](./images/5-1.svg)
Lars schaut seine Karten an, erkennt eine starke Hand und erhöht auf **1200 Chips**.
![6. image](./images/6-1.svg)
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).
![7. image](./images/7-1.svg)
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.
![8. image](./images/8-1.svg)
## 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.
![9. image](./images/9-1.png)
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**.
![10. image](./images/10-1.png)
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**.
![11. image](./images/11-1.png)
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:
![13. image](./images/13-1.png)
Lars: 1. Paar:
![14. image](./images/14-1.png)
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 = document.getElementById("content").innerHTML =
@@ -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();
}
@@ -33,9 +33,9 @@ public class ChatController {
private volatile String username; private volatile String username;
private final ClientService clientService; private ClientService clientService;
private final ChatClient chatClient; private final ChatClientInterface chatClient;
public ChatBoxController getChatBoxController() { public ChatBoxController getChatBoxController() {
return chatBoxController; return chatBoxController;
@@ -112,6 +112,39 @@ public class ChatController {
REFRESH_TIME); 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(
new TimerTask() {
@Override
public void run() {
try {
receiveMessage();
checkWhisperUsers();
} catch (RuntimeException e) {
logger.warn("Chat refresh failed: {}", e.getMessage());
}
}
},
0,
REFRESH_TIME);
}
private void registerAsActiveController(ClientService clientService) { private void registerAsActiveController(ClientService clientService) {
synchronized (ACTIVE_CONTROLLERS) { synchronized (ACTIVE_CONTROLLERS) {
ChatController oldController = ACTIVE_CONTROLLERS.get(clientService); ChatController oldController = ACTIVE_CONTROLLERS.get(clientService);
@@ -2,8 +2,6 @@ package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.function.Consumer; import java.util.function.Consumer;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
/** /**
* ChatModel, stores the data for a specific chat * ChatModel, stores the data for a specific chat
@@ -12,7 +10,7 @@ import javafx.beans.property.SimpleIntegerProperty;
*/ */
public class ChatModel { public class ChatModel {
private ArrayList<Consumer<Message>> listeners = new ArrayList<>(); private final ArrayList<Consumer<Message>> listeners = new ArrayList<>();
public ArrayList<Message> messages; public ArrayList<Message> messages;
@@ -24,8 +22,6 @@ public class ChatModel {
/** The person to send the message to If the chat is a whisper chat */ /** The person to send the message to If the chat is a whisper chat */
private String target; private String target;
private final IntegerProperty count;
public int lobbyId; public int lobbyId;
/** /**
@@ -37,10 +33,9 @@ public class ChatModel {
* @param target The username of the whisper recipient, or null for other chat types. * @param target The username of the whisper recipient, or null for other chat types.
*/ */
public ChatModel(ChatType chattype, String username, int lobbyId, String target) { public ChatModel(ChatType chattype, String username, int lobbyId, String target) {
this.messages = new ArrayList<Message>(); this.messages = new ArrayList<>();
this.chattype = chattype; this.chattype = chattype;
this.username = username; this.username = username;
this.count = new SimpleIntegerProperty(0);
this.lobbyId = lobbyId; this.lobbyId = lobbyId;
this.target = target; this.target = target;
} }
@@ -62,7 +57,7 @@ public class ChatModel {
*/ */
public synchronized void addMessage(Message msg) { public synchronized void addMessage(Message msg) {
messages.add(msg); messages.add(msg);
listeners.stream().forEach((l) -> l.accept(messages.getLast())); listeners.forEach((l) -> l.accept(messages.getLast()));
} }
/** /**
@@ -15,8 +15,8 @@ public class Message {
private final String message; private final String message;
public String sender; public String sender;
public String timestamp; public String timestamp;
public int lobbyId = 0; public int lobbyId;
public String target = null; public String target;
/** /**
* Constructs a Message with a provided timestamp. Typically used when reconstructing messages * 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. * @return A formatted string containing all message attributes for server transmission.
*/ */
public String toArgsString() { public String toArgsString() {
String gameIdString = ""; String gameIdString;
if (lobbyId >= 0) { if (lobbyId >= 0) {
gameIdString = " GAME=" + lobbyId; gameIdString = " GAME=" + lobbyId;
} else { } else {
@@ -1,5 +1,6 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network; 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.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import java.util.ArrayList; 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 * from the server. It uses the ClientService to send commands and receive responses from the
* server. * server.
*/ */
public class ChatClient { public class ChatClient implements ChatClientInterface {
private final ClientService clientService; private final ClientService clientService;
private final Logger logger; private final Logger logger;
@@ -1,6 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui; 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;
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.ChatModel;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType; 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.chat.Message;
@@ -20,7 +21,6 @@ import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem; import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab; import javafx.scene.control.Tab;
import javafx.scene.control.TabPane; import javafx.scene.control.TabPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
@@ -30,16 +30,10 @@ public class ChatBoxController {
private final ChatController chatController; private final ChatController chatController;
@FXML private VBox chatBox;
@FXML private TabPane chatTabPane; @FXML private TabPane chatTabPane;
@FXML private MenuButton addWhisperChatButton; @FXML private MenuButton addWhisperChatButton;
@FXML private HBox menuBox;
private FXMLLoader fxmlLoader;
private final List<String> activeWhisperChats; private final List<String> activeWhisperChats;
@FXML private final Map<String, Tab> usernameTabMap; @FXML private final Map<String, Tab> usernameTabMap;
@@ -76,7 +70,7 @@ public class ChatBoxController {
ChatModel globalChatModel = new ChatModel(global, username, -1, null); ChatModel globalChatModel = new ChatModel(global, username, -1, null);
chatController.getChatModelMap().put(new ChatController.ChatKey(global), globalChatModel); chatController.getChatModelMap().put(new ChatController.ChatKey(global), globalChatModel);
addChatTab("GLOBAL", globalChatModel, global); addChatTab("GLOBAL", globalChatModel, global);
addWhisperChatButton.setOnAction(event -> addWhisperChatButton.show()); addWhisperChatButton.setOnAction(_ -> addWhisperChatButton.show());
} }
/** /**
@@ -96,7 +90,7 @@ public class ChatBoxController {
MenuItem menuItem = new MenuItem(targetUserName); MenuItem menuItem = new MenuItem(targetUserName);
addWhisperChatButton.getItems().add(menuItem); addWhisperChatButton.getItems().add(menuItem);
menuItem.setOnAction( menuItem.setOnAction(
event -> _ ->
addWhisperChat( addWhisperChat(
targetUserName, targetUserName,
new ChatModel( new ChatModel(
@@ -130,7 +124,7 @@ public class ChatBoxController {
if (oldUsername.equals(item.getText())) { if (oldUsername.equals(item.getText())) {
item.setText(newUsername); item.setText(newUsername);
item.setOnAction( item.setOnAction(
event -> _ ->
addWhisperChat( addWhisperChat(
newUsername, newUsername,
new ChatModel( new ChatModel(
@@ -165,11 +159,21 @@ public class ChatBoxController {
if (!activeWhisperChats.contains(target)) { if (!activeWhisperChats.contains(target)) {
activeWhisperChats.add(target); activeWhisperChats.add(target);
ChatType whisper = ChatType.WHISPER; ChatType whisper = ChatType.WHISPER;
chatController chatController.getChatModelMap().put(new ChatKey(whisper, target), chatModel);
.getChatModelMap() ChatViewController chatViewController = addChatTab(target, chatModel, whisper);
.put(new ChatController.ChatKey(whisper, target), chatModel); Tab chatTab = chatViewController.getChatTab();
addChatTab(target, chatModel, whisper); chatTab.setOnCloseRequest(
_ -> {
chatViewController.tabIsOpen = false;
chatTabPane.getTabs().remove(chatTab);
});
} else { } 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)); chatTabPane.getSelectionModel().select(usernameTabMap.get(target));
} }
} }
@@ -183,30 +187,39 @@ public class ChatBoxController {
* @param chatModel The {@link ChatModel} containing the data and logic for this specific chat. * @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. * @throws RuntimeException If the FXML resource for the chat tab cannot be loaded.
*/ */
public void addChatTab(String title, ChatModel chatModel, ChatType chatType) { public ChatViewController addChatTab(String title, ChatModel chatModel, ChatType chatType) {
String ressource = "/ui-structure/components/chatui/chattab.fxml"; String ressource = "/ui-structure/components/chatui/chattab.fxml";
URL resource = getClass().getResource(ressource); URL resource = getClass().getResource(ressource);
FXMLLoader fxmlLoader = new FXMLLoader(resource); FXMLLoader fxmlLoader = new FXMLLoader(resource);
runOnPlatformSynchronized( return runOnPlatformSynchronized(
() -> { () -> {
try { try {
ChatViewController chatViewController = ChatViewController chatViewController =
new ChatViewController( new ChatViewController(this.chatController, chatModel, this);
this.chatController, chatModel, this.username);
chatController.activeChatControllers.put( ChatKey chatKey;
new ChatController.ChatKey(chatType), chatViewController); if (chatType.equals(ChatType.WHISPER)) {
chatKey = new ChatKey(chatType, title);
} else {
chatKey = new ChatKey(chatType);
}
chatController.activeChatControllers.put(chatKey, chatViewController);
fxmlLoader.setController(chatViewController); fxmlLoader.setController(chatViewController);
Node load = fxmlLoader.load(); Node load = fxmlLoader.load();
VBox.setVgrow(load, Priority.ALWAYS); VBox.setVgrow(load, Priority.ALWAYS);
VBox vbox = new VBox(); VBox vbox = new VBox();
VBox.setVgrow(vbox, Priority.ALWAYS); VBox.setVgrow(vbox, Priority.ALWAYS);
vbox.getChildren().add(load); vbox.getChildren().add(load);
chatModel.addListener((msg) -> chatViewController.showMessage(msg)); chatModel.addListener(chatViewController::showMessage);
Tab newChat = new Tab(title, vbox); Tab newChat = new Tab(title, vbox);
usernameTabMap.put(title, newChat); usernameTabMap.put(title, newChat);
this.chatTabPane.getTabs().add(newChat); chatTabPane.getTabs().add(newChat);
this.chatTabPane.getSelectionModel().select(newChat); this.chatTabPane.getSelectionModel().select(newChat);
return newChat; chatViewController.setChatTab(newChat);
if (!chatType.equals(ChatType.WHISPER)) {
newChat.setClosable(false);
}
return chatViewController;
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@@ -267,4 +280,12 @@ public class ChatBoxController {
} }
} }
} }
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.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
/** Responsible for the presentation of the ChatModel to the Client */ /** Responsible for the presentation of the ChatModel to the Client */
public class ChatViewController implements Initializable { public class ChatViewController implements Initializable {
private final ChatBoxController chatBoxController;
@FXML private Button sendButton; @FXML private Button sendButton;
@FXML private TextField inputField; @FXML private TextField inputField;
@FXML private VBox chatInterfaceVBox;
@FXML private HBox controlBar;
@FXML private ScrollPane scrollPane; @FXML private ScrollPane scrollPane;
@FXML private VBox chat; @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 final ChatController controller;
private static final int CHAT_PADDING = 20; 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.controller = chatController;
this.username = username;
this.chatModel = chatModel; this.chatModel = chatModel;
this.chatBoxController = chatBoxController;
tabIsOpen = true;
} }
/** /**
@@ -54,8 +65,8 @@ public class ChatViewController implements Initializable {
*/ */
@Override @Override
public void initialize(URL location, ResourceBundle resourceBundle) { public void initialize(URL location, ResourceBundle resourceBundle) {
inputField.setOnAction(event -> sendMessage()); inputField.setOnAction(_ -> sendMessage());
sendButton.setOnAction(event -> sendMessage()); sendButton.setOnAction(_ -> sendMessage());
scrollPane.vvalueProperty().bind(chat.heightProperty()); scrollPane.vvalueProperty().bind(chat.heightProperty());
} }
@@ -88,6 +99,10 @@ public class ChatViewController implements Initializable {
* @param msg The {@link Message} object containing the content and metadata to display. * @param msg The {@link Message} object containing the content and metadata to display.
*/ */
public void showMessage(Message msg) { public void showMessage(Message msg) {
if (!tabIsOpen) {
tabIsOpen = true;
chatBoxController.reopenChatTab(chatTab);
}
Platform.runLater( Platform.runLater(
() -> { () -> {
String msgText = String msgText =
@@ -3,13 +3,14 @@
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import javafx.scene.control.skin.TabPaneSkin.TabContentRegion?>
<VBox xmlns="http://javafx.com/javafx/21" <VBox xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1" xmlns:fx="http://javafx.com/fxml/1"
fx:id="chatBox" fx:id="chatBox"
VBox.vgrow="ALWAYS" VBox.vgrow="ALWAYS"
stylesheets="@chatui.css" stylesheets="@chatui.css"
styleClass="chat-box" styleClass="chat-box"
spacing="10" spacing="20"
maxWidth="Infinity"> maxWidth="Infinity">
<HBox spacing="10" <HBox spacing="10"
@@ -17,14 +18,14 @@
<Label text="== CHAT ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/> <Label text="== CHAT ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/>
<Region HBox.hgrow="ALWAYS"/> <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"> alignment="CENTER_RIGHT">
</MenuButton> </MenuButton>
</HBox> </HBox>
<TabPane fx:id="chatTabPane" <TabPane fx:id="chatTabPane"
VBox.vgrow="ALWAYS" VBox.vgrow="ALWAYS"
styleClass="tab-header-area"> styleClass="tab-pane">
</TabPane> </TabPane>
@@ -8,7 +8,7 @@
<VBox fx:id="chatInterfaceVBox" <VBox fx:id="chatInterfaceVBox"
maxWidth="Infinity" maxWidth="Infinity"
spacing="10" spacing="10"
styleClass="chat-box" styleClass="tab-chat-box"
stylesheets="@chatui.css" stylesheets="@chatui.css"
VBox.vgrow="ALWAYS" VBox.vgrow="ALWAYS"
xmlns="http://javafx.com/javafx/17.0.12" xmlns="http://javafx.com/javafx/17.0.12"
@@ -5,7 +5,17 @@
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 12; -fx-border-width: 12;
-fx-padding: 20; -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 { .chat-header {
@@ -163,25 +173,25 @@
-fx-border-width: 5; -fx-border-width: 5;
-fx-padding: 10; -fx-padding: 10;
-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);
-fx-max-height: 10; -fx-min-height: 10;
} }
.tab-pane .tab { .tab-pane .tab {
-fx-background-color: #0d9e3b; -fx-background-color: #0d983b;
-fx-background-radius: 18; -fx-background-radius: 28;
-fx-border-radius: 10; -fx-border-radius: 10;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; -fx-border-color: #0d873b #0d733b #0d733b #0d873b;
-fx-border-width: 5; -fx-border-width: 5;
-fx-padding: 10; -fx-padding: 6 15;
} }
.tab-pane .tab:selected { .tab-pane .tab:selected {
-fx-background-color: #0d763b; -fx-background-color: #0d873b;
-fx-background-radius: 18; -fx-background-radius: 18;
-fx-border-radius: 10; -fx-border-radius: 10;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10; -fx-border-color: #0d763b #0d623b #0d623b #0d763b;
-fx-border-width: 5; -fx-border-width: 5;
-fx-padding: 10; -fx-padding: 6 15;
} }
.tab-pane .tab .tab-close-button { .tab-pane .tab .tab-close-button {
@@ -192,7 +202,6 @@
-fx-background-color: #0d9e3b; -fx-background-color: #0d9e3b;
} }
.tab .tab-label { .tab .tab-label {
-fx-alignment: CENTER; -fx-alignment: CENTER;
-fx-text-fill: #ffffff; -fx-text-fill: #ffffff;
@@ -204,3 +213,36 @@
-fx-alignment: CENTER; -fx-alignment: CENTER;
-fx-text-fill: #ffffff; -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
}
@@ -1,59 +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; 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.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class ChatControllerTest { public class ChatControllerTest {
ChatController chatController1;
ChatController chatController2;
String name1 = "testperson1";
String name2 = "testperson1";
@BeforeEach @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 @AfterEach
public void teardown() {} public void teardown() {}
/* @Test
* @Test public void sendGlobalMessage() {
* public void testChatController() { ChatModel global = new ChatModel(ChatType.GLOBAL, name2, -1, null);
* Client client = new Client("user1"); chatController2.getChatModelMap().put(new ChatController.ChatKey(ChatType.GLOBAL), global);
* client.startApplication();
* } 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,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,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());
}
}