Compare commits

..

126 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 d6ad60d6a7 Fix: radical overhaul of Game UI tests to fix CI pipeline failures 2026-04-26 23:27:53 +02:00
Julian Kropff ddb3e221ac Fix: radical overhaul of Game UI tests to fix CI pipeline failures 2026-04-26 23:04:45 +02: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
Julian Kropff 52ad32a100 Fix: radical overhaul of Game UI tests to fix CI pipeline failures 2026-04-26 22:35:45 +02:00
Julian Kropff 822d9a0e1e Fix: Checkstyle 2026-04-26 21:57:14 +02:00
Mathis Ginkel 216c15e825 Merge branch 'refs/heads/main' into feat/chat-visual-improvement 2026-04-26 21:46:52 +02:00
Julian Kropff 30927d471e Add: Game UI test: Ensures FXML loads and controller initializes without errors. 2026-04-26 21:42:47 +02:00
Julian Kropff 86ab82ebfd Add: Game UI Taskbar tests:
- input validation behavior
- UI state updates based on game state
- correct enabling/disabling of action buttons
2026-04-26 21:34:24 +02:00
Julian Kropff 6a06369d94 Fix: Checkstyle 2026-04-26 21:26:45 +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
Julian Kropff 1107542c54 Fix: Bug fix so that the 3000 cap does not apply to every Phase 2026-04-26 18:09:03 +02:00
Julian Kropff 2ad99d8732 Merge branch 'feat/game-ui-optimation' into 'main'
Feat: New Game UI Features and bug fixed

See merge request cs108-fs26/Gruppe-13!152
2026-04-26 15:52:40 +00:00
Julian Kropff d791e9b2d1 Merge branch 'main' into feat/game-ui-optimation 2026-04-26 17:43:37 +02:00
Lars Simon Winzer 3cd1e6925e Merge branch 'feat/128-unit-tests-for-requestparameteraccessor' into 'main'
Unit tests for RequestParameterAccessor

Closes #128

See merge request cs108-fs26/Gruppe-13!151
2026-04-26 17:19:30 +02:00
Lars Simon Winzer a7e4411cd0 Test: Add unit tests for RequestParameterAccessor 2026-04-26 17:13:11 +02:00
Julian Kropff bf979ab391 Fix: rename button names 2026-04-26 17:05:05 +02:00
Mathis Ginkel 588bb1f312 Merge branch 'main' into feat/chat-visual-improvement 2026-04-26 16:58:45 +02:00
Julian Kropff fb68ecc777 Feat: New Game UI Features:
- Call, Raise, Fold, Bet and Winner action are displayed in the Lobby Chat.
- More security features in the Game UI
- The Game UI is now even more Texas Hold'em friendly
2026-04-26 16:58:23 +02:00
Julian Kropff 8ca41c5ab4 Fix: Checkstyle 2026-04-26 16:51:43 +02:00
Julian Kropff 6422e6c924 Add: new welcome message in the Game UI 2026-04-26 16:21:26 +02:00
Julian Kropff 404bf6379c Add: the new Dealer Icon in PlayerStatusController 2026-04-26 16:20:26 +02:00
Julian Kropff d46bd8de7e Add: New Dealer Icon 2026-04-26 16:17:58 +02:00
Julian Kropff c55a9eace6 Add: Block file downloads in Casono Browser 2026-04-26 16:16:28 +02:00
Julian Kropff b3eaa64cd7 Add: Casono Logo to popups 2026-04-25 18:36:35 +02:00
Julian Kropff ab4f60a74f Add: hint for first player to act in preflop 2026-04-25 18:34:18 +02:00
Julian Kropff 4f9e9a0e9a Fix: Checkstyle 2026-04-25 16:26:54 +02:00
Julian Kropff c31f0048ee Style: fixes in FXML and CSS for Game UI 2026-04-25 15:02:02 +02:00
Julian Kropff 0f5ddfda0c Add: taskbar image for Casono manual 2026-04-25 14:58:51 +02:00
Julian Kropff 09f8ddf123 Add: Casono manual with integrated Markdown renderer 2026-04-25 14:57:05 +02:00
Julian Kropff c11201479b Add: visual indicator for current player in Game UI 2026-04-25 14:54:46 +02:00
Lars Simon Winzer 5ded42abe6 Merge branch 'fix/126-missing-key-is-not-detected-by-tokenclassifier' into 'main'
Detect missing keys while classifying tokens

Closes #126

See merge request cs108-fs26/Gruppe-13!150
2026-04-25 14:33:12 +02:00
Lars Simon Winzer 572b66889d Test: Add test for missing key before separator in TokenClassifier 2026-04-25 14:27:42 +02:00
Lars Simon Winzer 89fd6aef05 Fix: Detect missing key when validating separator 2026-04-25 14:26:22 +02:00
Julian Kropff 6e640f7fde Add: Casono manual via help button and taskbar dragging with screen boundary clamping and scaling 2026-04-25 14:19:47 +02:00
Julian Kropff 7a6c876017 Add: secure local HTML file loading in Casono Browser and website autocompletion 2026-04-25 14:05:31 +02:00
Lars Simon Winzer 22d742d614 Merge branch 'feat/4-create-unit-tests-for-server-side-tokenizer' into 'main'
Unit tests for Tokenizer and TokenClassifier

Closes #4

See merge request cs108-fs26/Gruppe-13!149
2026-04-25 13:58:52 +02:00
Julian Kropff 71d48d148b Fix: delay lobby cleanup to ensure winner is displayed on all Clients
Refs #125
2026-04-25 13:50:18 +02:00
Julian Kropff 5c0f3a7c86 Add: persistence guard for hand winner to prevent double saving
Refs #125
2026-04-25 13:43:18 +02:00
Lars Simon Winzer 2190fccd8f Style: Apply Spotless 2026-04-25 13:28:34 +02:00
Lars Simon Winzer 4bfb59f234 Test: Unit tests for TokenClassifier
Co-authored-by: Copilot <copilot@github.com>
2026-04-25 13:28:10 +02:00
Lars Simon Winzer d49f18c3aa Test: Unit tests for Tokenizer 2026-04-25 10:44:09 +02:00
Jona Walpert d4a3ebb487 Merge branch 'refactor/124-lobby-cleanup-job' into 'main'
Refactor: Add LobbyCleanupJob for periodic expired lobby cleanup

Closes #124

See merge request cs108-fs26/Gruppe-13!148
2026-04-24 13:20:45 +00:00
Jona Walpert 3cabe06fb0 Merge branch 'main' into 'refactor/124-lobby-cleanup-job' to prevent merge conflicts 2026-04-24 15:15:29 +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
Jona Walpert 9023263b01 Refacor: Schedule LobbyCleanupJob instead of cleanup lambda
Refs #124

Co-authored-by: Copilot <copilot@github.com>
2026-04-24 14:16:12 +02:00
Jona Walpert c04d4fca76 Refactor: Add LobbyCleanupJob for periodic expired lobby cleanup
- isolate failures per lobby during cleanup
- notify connected sessions about closed lobbies
Refs #124

Co-authored-by: Copilot <copilot@github.com>
2026-04-24 14:14:59 +02:00
Mathis Ginkel d3d72bd5b4 Merge branch 'feat/chat-integration' into 'main'
Feat: Add chat box into the game screen

See merge request cs108-fs26/Gruppe-13!147
2026-04-24 12:14:16 +00:00
Mathis Ginkel 4a3c885b68 Style: Apply Spotless 2026-04-24 14:04:12 +02:00
Mathis Ginkel 571c616ce9 Merge remote-tracking branch 'origin/feat/chat-integration' into feat/chat-integration 2026-04-24 13:45:15 +02:00
Mathis Ginkel 01ecbb0497 Style: Apply Spotless 2026-04-24 13:45:02 +02:00
Julian Kropff 5e91cefabb Fix: whisper chat not working in game UI 2026-04-24 13:44:38 +02:00
Julian Kropff 0f4d048b76 Fix: Chat UI initialization improved in Game UI 2026-04-24 13:44:18 +02:00
Mathis Ginkel b1d099f102 Feat: Add load method to restore all chat message of a chat
Refs #107
2026-04-24 13:43:40 +02:00
Mathis Ginkel 40e558be18 Feat: Add chat box into the game screen
Refs #107
2026-04-24 13:40:44 +02:00
Mathis Ginkel 277b776fec Feat: Add chat box into game ui 2026-04-24 13:38:52 +02:00
Mathis Ginkel 88b797ba1d Feat: Add field ChatController to LobbyButtonGridManager 2026-04-24 13:37:12 +02:00
Mathis Ginkel 07c00275fd Merge remote-tracking branch 'origin/feat/chat-integration' into feat/chat-integration 2026-04-24 13:27:17 +02:00
Mathis Ginkel 1d4cf2d846 Style: Apply Spotless 2026-04-24 13:26:57 +02:00
Julian Kropff c2d0af03b7 Fix: whisper chat not working in game UI 2026-04-24 13:26:57 +02:00
Julian Kropff 48476a67f3 Fix: Chat UI initialization improved in Game UI 2026-04-24 13:26:57 +02:00
Julian Kropff 6471749133 Fix: correct layout layering in Game UI 2026-04-24 13:26:57 +02:00
Mathis Ginkel 826e58644a Feat: Add load method to restore all chat message of a chat
Refs #107
2026-04-24 13:26:57 +02:00
Mathis Ginkel 67a0f237e6 Feat: Forward ChatController to all instances where its needed
Refs #107
2026-04-24 13:26:57 +02:00
Mathis Ginkel 2a03571cd1 Feat: Add chat box into the game screen
Refs #107
2026-04-24 13:26:57 +02:00
Mathis Ginkel 7eafaba65a Feat: Add chat box into game ui 2026-04-24 13:26:57 +02:00
Mathis Ginkel 69fc00a3b2 Feat: Add field ChatController to LobbyButtonGridManager 2026-04-24 13:26:57 +02:00
Jona Walpert ca65655443 Merge branch 'refactor/121-duplicate-check-lobby-code' into 'main'
Refactor/121 duplicate check lobby code

See merge request cs108-fs26/Gruppe-13!146
2026-04-24 11:26:08 +00:00
Jona Walpert 488e8f524b Merge branch 'fix/123-server-assigned-username-send-to-client' into 'main'
Fix: Readd propagation of server assigned username. Login suername changed to...

Closes #123

See merge request cs108-fs26/Gruppe-13!145
2026-04-24 11:14:23 +00:00
Mathis Ginkel 9488ef41c3 Style: Apply Spotless 2026-04-24 11:46:32 +02:00
Julian Kropff 8bb0bfaf09 Fix: whisper chat not working in game UI 2026-04-24 11:16:34 +02:00
Julian Kropff 33998e4a88 Fix: Chat UI initialization improved in Game UI 2026-04-23 19:59:55 +02:00
Julian Kropff d094df16da Fix: correct layout layering in Game UI 2026-04-23 19:55:57 +02:00
Jona Walpert b9e45142c2 Refactor: changed duplicate lobyb existence checks to the new preexecution check 2026-04-23 15:17:30 +02:00
Jona Walpert 16c91c6aae Feat: Add lobby check to repalce duplicate code 2026-04-23 15:16:45 +02:00
Jona Walpert ea5f6a1083 Fix: Readd propagation of server assigned username. Login suername changed to optional instead of required 2026-04-23 14:46:24 +02:00
Mathis Ginkel c5f37392ef Feat: Add load method to restore all chat message of a chat
Refs #107
2026-04-23 12:06:43 +02:00
Mathis Ginkel 66ee8bec46 Feat: Forward ChatController to all instances where its needed
Refs #107
2026-04-23 12:06:43 +02:00
Mathis Ginkel 36348d0d93 Feat: Add chat box into the game screen
Refs #107
2026-04-23 12:06:43 +02:00
Mathis Ginkel 90b2f3bb3a Feat: Add chat box into game ui 2026-04-23 12:06:38 +02:00
Mathis Ginkel 1cddcf6657 Feat: Add field ChatController to LobbyButtonGridManager 2026-04-23 12:02:49 +02:00
Jona Walpert e358c930c5 Merge branch 'chore/122-fix-long-file-imports' into 'main'
Fix: Correct fxml imports and restore branch state

See merge request cs108-fs26/Gruppe-13!144
2026-04-22 16:33:47 +00:00
Jona Walpert 95fc53ec61 Fix: Wire correct controllers to fxml files
Co-authored-by: Copilot <copilot@github.com>
2026-04-22 18:29:54 +02:00
Jona Walpert 989dc95cef Fix: Final code restoration and import cleanup
Co-authored-by: Copilot <copilot@github.com>
2026-04-22 18:19:21 +02:00
Jona Walpert 2c343c72b3 Merge remote-tracking branch 'origin/main' into chore/122-fix-long-file-imports 2026-04-22 18:07:49 +02:00
Jona Walpert ffbfd2dee4 Fix: Correct fxml imports and restore branch state"
Co-authored-by: Copilot <copilot@github.com>
2026-04-22 17:51:35 +02:00
Jona Walpert 97808fe26d Merge branch 'fix/revert-failed-imports' into 'main'
Revert "Merge branch 'chore/122-fix-long-file-imports' into 'main'"

See merge request cs108-fs26/Gruppe-13!142
2026-04-22 15:43:11 +00:00
Jona Walpert 5b37dffa4c Revert "Merge branch 'chore/122-fix-long-file-imports' into 'main'"
This reverts commit 838ada39d5, reversing
changes made to 028c3adb0d.
2026-04-22 17:39:13 +02:00
Jona Walpert 838ada39d5 Merge branch 'chore/122-fix-long-file-imports' into 'main'
Chore: fix long line class paths in fxml files by using imports

Closes #122

See merge request cs108-fs26/Gruppe-13!141
2026-04-22 12:30:48 +00:00
Jona Walpert 4e675677c0 Chore: fix long line class paths on server side by using imports
Refs #122
2026-04-22 14:20:38 +02:00
Jona Walpert a629136fcb Chore: fix long line class paths on client side by using imports
Refs #122
2026-04-22 14:19:53 +02:00
Jona Walpert 30067bd69b Chore: fix long line class paths in fxml files by using imports
Refs #122
2026-04-22 14:18:50 +02:00
Mathis Ginkel 192c8a2cb9 Feat: Change look of chat box 2026-04-21 22:18:06 +02:00
68 changed files with 3934 additions and 695 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
*(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
+218 -2
View File
@@ -1,9 +1,19 @@
# 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 -->
* [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)
@@ -21,9 +31,64 @@
* [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
![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
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)
- 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.
![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 🟡*
@@ -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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
window.marked = marked;
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

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

+508
View File
@@ -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
![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
Texas Holdem 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 Holdem 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.
![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 =
marked.parse(markdown);
document.querySelectorAll("#content a").forEach(link => {
link.addEventListener("click", (e) => {
const href = link.getAttribute("href");
if (href && href.endsWith(".md")) {
e.preventDefault();
alert("You cannot load an external MD file using `file:///` without a server.");
}
});
});
});
</script>
<noscript>
<div style="
padding: 40px;
font-family: Arial, sans-serif;
background-color: #0d9e3b;
color: #ffffff;
text-align: center;
">
<h2>JavaScript is disabled</h2>
<p>
The Casono Browser requires JavaScript for the <br>
Casono Markdown Render Engine to display content correctly.<br><br>
For security, JavaScript is disabled by default in the Casono Browser and should only be enabled when needed.
</p>
<p>
Please enable JavaScript in your Browser settings and reload the page.
</p>
<img src="images/activate-js.png" alt="JavaScript aktivieren Anleitung" style="max-width: 100%; margin-top: 20px; border-radius: 10px;">
</div>
</noscript>
</body>
</html>
@@ -11,7 +11,7 @@ import org.apache.logging.log4j.Logger;
* Entry point and bootstrap helper for the Casono client application.
*
* <p>Responsibilities: - Parse CLI args (host:port, optional username) - Store username centrally
* so UI can reuse it - Create a shared ClientService and do startup LOGIN (optional)
* so UI can reuse it - Create a shared ClientService and do startup LOGIN
*/
public class ClientApp {
@@ -40,7 +40,14 @@ public class ClientApp {
}
public static void updateSharedUsername(String username) {
setSharedUsername(username != null && !username.isBlank() ? username.trim() : null);
setSharedUsername(normalizeUsername(username));
}
private static String normalizeUsername(String username) {
if (username == null || username.isBlank()) {
return null;
}
return username.trim();
}
private static void setSharedUsername(String username) {
@@ -52,15 +59,14 @@ public class ClientApp {
String[] hostPort = parseHostAndPort(arg);
String host = hostPort[0];
int port = Integer.parseInt(hostPort[1]);
String normalizedUsername = normalizeUsername(username);
configureServerProperties(host, port);
setSharedUsername(username != null && !username.isBlank() ? username.trim() : null);
setSharedUsername(normalizedUsername);
if (getSharedUsername() != null) {
performStartupLogin(host, port, username);
}
performStartupLogin(host, port, normalizedUsername);
launchUI(arg, username);
launchUI(arg);
}
private static String[] parseHostAndPort(String arg) {
@@ -81,52 +87,33 @@ public class ClientApp {
try {
ClientService clientService = new ClientService(host, port);
setSharedClientService(clientService);
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;
});
bg.submit(
() -> {
try {
LobbyClient lobbyClient = new LobbyClient(clientService);
LoginResult res = lobbyClient.login(username);
if (res != null
&& res.getUsername() != null
&& !res.getUsername().isBlank()) {
if (res != null && res.getUsername() != null && !res.getUsername().isBlank()) {
setSharedUsername(res.getUsername().trim());
}
LOGGER.info(
"Assigned username='{}' id={}",
res != null ? res.getUsername() : null,
res != null ? res.getId() : null);
} catch (RuntimeException e) {
LOGGER.warn("Startup login failed: {}", e.getMessage());
} catch (Exception e) {
LOGGER.warn("Unexpected error during startup login", e);
} finally {
bg.shutdown();
String assignedUsername = null;
String assignedId = null;
if (res != null) {
assignedUsername = res.getUsername();
assignedId = res.getId();
}
});
LOGGER.info("Assigned username='{}' id={}", assignedUsername, assignedId);
} catch (RuntimeException e) {
LOGGER.warn(
"Could not establish initial connection for startup login: {}", e.getMessage());
}
}
private static void launchUI(String arg, String username) {
if (username != null && !username.isBlank()) {
Launcher.main(new String[] {arg, username});
} else {
private static void launchUI(String arg) {
String username = getSharedUsername();
if (username == null || username.isBlank()) {
Launcher.main(new String[] {arg});
return;
}
Launcher.main(new String[] {arg, username});
}
public static void main(String[] args) {
@@ -134,7 +121,10 @@ public class ClientApp {
throw new IllegalArgumentException("Address argument required: <host:port>");
}
String username = args.length > 1 && args[1] != null && !args[1].isBlank() ? args[1] : null;
String username = null;
if (args.length > 1) {
username = normalizeUsername(args[1]);
}
start(args[0], username);
}
@@ -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();
}
@@ -4,8 +4,10 @@ import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ChatClient;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatViewController;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -31,15 +33,19 @@ public class ChatController {
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;
@@ -61,6 +67,12 @@ public class ChatController {
/** List of all users connected to the server, to safe them locally on the client */
private final List<String> localUserList;
public List<String> getLocalUserList() {
return this.localUserList;
}
public final Map<ChatController.ChatKey, ChatViewController> activeChatControllers;
private final Logger logger;
/**
@@ -78,6 +90,7 @@ public class ChatController {
this.chatBoxController = new ChatBoxController(username, this);
this.logger = LogManager.getLogger(ChatController.class);
this.serverEventListener = this::handleServerEvent;
this.activeChatControllers = new HashMap<>();
registerAsActiveController(clientService);
clientService.addEventListener(serverEventListener);
@@ -99,6 +112,39 @@ public class ChatController {
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) {
synchronized (ACTIVE_CONTROLLERS) {
ChatController oldController = ACTIVE_CONTROLLERS.get(clientService);
@@ -126,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);
}
/**
@@ -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;
@@ -24,8 +22,6 @@ public class ChatModel {
/** The person to send the message to If the chat is a whisper chat */
private String target;
private final IntegerProperty count;
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()));
}
/**
@@ -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 {
@@ -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;
@@ -81,11 +81,7 @@ public class LobbyClient {
throw new RuntimeException("No LOBBY_ID in response: " + lines);
}
/**
* Fetches the global highscores from the server.
*
* @return list of formatted highscore entries, newest entries at the end
*/
/** Fetches the global highscores from the server. */
public List<String> getHighscores() {
List<String> lines = client.processCommand("GET_HIGHSCORES");
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
@@ -139,7 +135,8 @@ public class LobbyClient {
* server
*/
public LoginResult login(String user) {
List<String> lines = client.processCommand("LOGIN USERNAME=" + user);
String command = buildLoginCommand(user);
List<String> lines = client.processCommand(command);
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
@@ -155,6 +152,13 @@ public class LobbyClient {
return new LoginResult(assigned, id);
}
private String buildLoginCommand(String username) {
if (username == null || username.isBlank()) {
return "LOGIN";
}
return "LOGIN USERNAME=" + username.trim();
}
/**
* Changes the username for the currently logged-in session.
*
@@ -199,7 +203,6 @@ public class LobbyClient {
String val = p.value();
switch (key) {
case "ID":
// If we were collecting a lobby, flush it
if (currentId != null) {
result.add(
new LobbyInfo(
@@ -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.
@@ -73,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());
}
/**
@@ -98,7 +90,7 @@ public class ChatBoxController {
MenuItem menuItem = new MenuItem(targetUserName);
addWhisperChatButton.getItems().add(menuItem);
menuItem.setOnAction(
event ->
_ ->
addWhisperChat(
targetUserName,
new ChatModel(
@@ -132,7 +124,7 @@ public class ChatBoxController {
if (oldUsername.equals(item.getText())) {
item.setText(newUsername);
item.setOnAction(
event ->
_ ->
addWhisperChat(
newUsername,
new ChatModel(
@@ -166,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));
}
}
@@ -184,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);
}
@@ -239,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());
}
@@ -88,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;
}
/**
@@ -650,8 +787,10 @@ public class CasinoGameController {
* @param players The list of players currently in the game, used to determine which players are
* opponents and update their display in the UI accordingly. If the list is null or empty,
* all opponent slots will be cleared.
* @param activePlayerIndex The index of the active player in the players list, used to
* determine whose turn it is and update the UI highlights to reflect that.
*/
private void updatePlayers(List<Player> players) {
private void updatePlayers(List<Player> players, int activePlayerIndex) {
if (players == null || players.isEmpty()) {
clearOpponentSlots();
return;
@@ -677,6 +816,7 @@ public class CasinoGameController {
safeRefresh(player1Controller);
safeRefresh(player2Controller);
safeRefresh(player3Controller);
updateActiveTurnHighlights(players, activePlayerIndex);
}
/**
@@ -701,7 +841,7 @@ public class CasinoGameController {
}
PlayerId pid = p.getId();
boolean isMe = myPlayerId != null && pid != null && pid.equals(myPlayerId);
boolean isMe = myPlayerId != null && myPlayerId.equals(pid);
if (isMe) {
meFound = true;
@@ -773,6 +913,318 @@ public class CasinoGameController {
safeRefresh(player1Controller);
safeRefresh(player2Controller);
safeRefresh(player3Controller);
clearActiveTurnHighlights();
setTaskbarTurnHighlight(false);
}
/** Clear all visual highlights in the UI that indicate the active player's turn. */
private void clearActiveTurnHighlights() {
if (player1Controller != null) {
player1Controller.setTurnHighlighted(false);
}
if (player2Controller != null) {
player2Controller.setTurnHighlighted(false);
}
if (player3Controller != null) {
player3Controller.setTurnHighlighted(false);
}
if (playerStatusController != null) {
playerStatusController.setTurnHighlighted(false);
}
setPlayerCardsTurnHighlighted(false);
}
/**
* Set the visual highlight state of the player's hole cards to indicate whether it is currently
* the player's turn in the game.
*
* @param highlighted true to highlight the player's hole cards, indicating that it is their
* turn, or false to remove the highlight when it is not their turn.
*/
private void setPlayerCardsTurnHighlighted(boolean highlighted) {
if (playerCardsBox == null) {
return;
}
for (Node child : playerCardsBox.getChildren()) {
if (child instanceof ImageView) {
toggleStyleClass(child, ACTIVE_CARD_BOX_STYLE_CLASS, highlighted);
}
}
}
/**
* Update the visual highlights in the UI to indicate which player's turn it is based on the
* active.
*
* @param players The list of players currently in the game, used to identify the active player
* and update the turn highlights accordingly.
* @param activePlayerIndex The index of the active player in the players list, used to
* determine whose turn it is and update the UI highlights to reflect that.
*/
private void updateActiveTurnHighlights(List<Player> players, int activePlayerIndex) {
clearActiveTurnHighlights();
Player activePlayer = getPlayerAtIndex(players, activePlayerIndex);
if (activePlayer == null || activePlayer.getId() == null) {
setTaskbarTurnHighlight(false);
return;
}
if (myPlayerId != null && myPlayerId.equals(activePlayer.getId())) {
setPlayerCardsTurnHighlighted(true);
setTaskbarTurnHighlight(true);
return;
}
setTaskbarTurnHighlight(false);
if (player1Controller != null && player1Controller.hasPlayer(activePlayer)) {
player1Controller.setTurnHighlighted(true);
return;
}
if (player2Controller != null && player2Controller.hasPlayer(activePlayer)) {
player2Controller.setTurnHighlighted(true);
return;
}
if (player3Controller != null && player3Controller.hasPlayer(activePlayer)) {
player3Controller.setTurnHighlighted(true);
}
}
/**
* Set the turn highlight state in the taskbar controller, which visually indicates whether it
* is the current player's turn in the game.
*
* @param highlighted true to highlight the turn in the taskbar, false to remove the highlight.
*/
private void setTaskbarTurnHighlight(boolean highlighted) {
TaskbarController taskbarCtrl = resolveTaskbarController();
if (taskbarCtrl != null) {
taskbarCtrl.setTurnHighlighted(highlighted);
}
}
/**
* Retrieve the Player object at the specified index from the list of players.
*
* @param players The list of players from which to retrieve the player at the specified index.
* @param index The index of the player to retrieve from the list.
* @return The Player object at the specified index if it exists and is valid, or null if the
* index is out of bounds or if the players list is null or empty.
*/
private Player getPlayerAtIndex(List<Player> players, int index) {
if (players == null || index < 0 || index >= players.size()) {
return null;
}
return players.get(index);
}
/**
* Resolve the winner of the hand based on the game state.
*
* @param s The current game state, which may contain information about the players, their
* states, and the winner index.
* @return The Player object representing the winner if it can be determined from the game
* state, or null if the winner cannot be resolved or if the game is still in progress.
*/
private Player resolveWinner(GameState s) {
if (s == null || s.players == null || s.players.isEmpty()) {
return null;
}
if (s.winnerIndex >= 0 && s.winnerIndex < s.players.size()) {
return s.players.get(s.winnerIndex);
}
Player remaining = null;
int remainingCount = 0;
for (Player player : s.players) {
if (player == null) {
continue;
}
if (player.getState() == PlayerState.FOLDED) {
continue;
}
if (remainingCount > 0) {
return null;
}
remaining = player;
remainingCount++;
}
if (remaining != null && remainingCount == 1) {
return remaining;
}
return null;
}
/**
* Resolve the name of the winner based on the game state.
*
* @param s The current game state, which may contain information about the players, their
* states, and the winner index.
* @return The name of the winner if it can be determined from the game state, or null if the
* winner cannot be resolved or if the game is still in progress.
*/
private String resolveWinnerName(GameState s) {
Player winner = resolveWinner(s);
if (winner != null) {
String name = winner.getName();
if (name != null && !name.isBlank()) {
return name;
}
}
if (isTerminalPhase(s != null ? s.phase : null)
&& lastWinnerName != null
&& !lastWinnerName.isBlank()) {
return lastWinnerName;
}
return null;
}
/**
* Sends a short lobby chat message when the local player wins, but only once per hand.
*
* @param s The current game state.
*/
private void announceLocalWinIfNeeded(GameState s) {
if (localWinAnnouncementSent || s == null || !isTerminalPhase(s.phase)) {
return;
}
Player winner = resolveWinner(s);
boolean isLocalWinner =
winner != null
&& winner.getId() != null
&& myPlayerId != null
&& myPlayerId.equals(winner.getId());
if (!isLocalWinner && lastWinnerName != null && !lastWinnerName.isBlank()) {
String localName =
(chatUsername != null && !chatUsername.isBlank())
? chatUsername
: (winner != null ? winner.getName() : null);
isLocalWinner = localName != null && localName.equalsIgnoreCase(lastWinnerName);
}
if (!isLocalWinner) {
return;
}
localWinAnnouncementSent = true;
sendLobbyActionMessage("I won");
}
/**
* Determine if the game is in a finished state based on the game state and the presence of a
* winner name.
*
* @param s The current game state, which may be null or contain information about the phase,
* players, and winner index.
* @param winnerName The name of the winner, which may be null or blank if the winner is not yet
* determined or if the game is still in progress.
* @return true if the game is considered finished based on the provided state and winner
* information, false otherwise.
*/
private boolean isGameFinishedState(GameState s, String winnerName) {
if (s == null) {
return false;
}
boolean winnerKnown = winnerName != null && !winnerName.isBlank();
if (!winnerKnown) {
return false;
}
if (isTerminalPhase(s.phase)) {
return true;
}
if (s.winnerIndex >= 0 && s.players != null && s.winnerIndex < s.players.size()) {
return true;
}
return hasSingleRemainingPlayer(s);
}
/**
* Determine if there is only one remaining active player in the game state.
*
* @param s The current game state, which may be null or contain a list of players.
* @return true if there is exactly one active player remaining (not folded), false otherwise.
*/
private boolean hasSingleRemainingPlayer(GameState s) {
if (s == null || s.players == null || s.players.isEmpty()) {
return false;
}
int remaining = 0;
for (Player player : s.players) {
if (player == null || player.getState() == PlayerState.FOLDED) {
continue;
}
remaining++;
if (remaining > 1) {
return false;
}
}
return remaining == 1;
}
/**
* Determine if the current phase of the game is a terminal phase, such as "FINISHED" or
* "SHOWDOWN".
*
* @param phase The current phase of the game, which may be null or blank.
* @return true if the phase is considered terminal, indicating that the hand has ended and a
* winner can be declared, false otherwise.
*/
private boolean isTerminalPhase(String phase) {
return "FINISHED".equalsIgnoreCase(phase) || "SHOWDOWN".equalsIgnoreCase(phase);
}
/** Finish the game UI loop by stopping the timeline that updates the UI. */
private void finishGameUiLoop() {
if (gameFinished) {
return;
}
gameFinished = true;
if (timeline != null) {
timeline.stop();
}
}
/**
* Toggle a CSS style class on a JavaFX Node based on the active state.
*
* @param node The JavaFX Node on which to toggle the style class.
* @param styleClass The name of the CSS style class to toggle.
* @param active A boolean indicating whether to add (true) or remove (false) the style class
* from the node.
*/
private void toggleStyleClass(Node node, String styleClass, boolean active) {
if (node == null || styleClass == null || styleClass.isBlank()) {
return;
}
if (active) {
if (!node.getStyleClass().contains(styleClass)) {
node.getStyleClass().add(styleClass);
}
} else {
node.getStyleClass().remove(styleClass);
}
}
/**
@@ -1321,6 +1773,47 @@ public class CasinoGameController {
if (controller != null && gameService != null && myPlayerId != null) {
controller.setGameService(gameService, myPlayerId);
}
configureTaskbarLobbyActionAnnouncements();
}
/**
* Configure the TaskbarController to announce lobby actions by sending chat messages when
* certain actions occur in the game, such as winning a hand.
*/
private void configureTaskbarLobbyActionAnnouncements() {
TaskbarController controller = resolveTaskbarController();
if (controller == null) {
return;
}
controller.setLobbyActionAnnouncer(this::sendLobbyActionMessage);
}
/**
* Send a lobby action message to the chat controller with the specified action label, which can
* be used to announce game events in the lobby chat.
*
* @param actionLabel The label describing the action to announce in the lobby chat, such as "I
* won" when the local player wins a hand.
*/
private void sendLobbyActionMessage(String actionLabel) {
if (chatController == null
|| chatLobbyId < 0
|| actionLabel == null
|| actionLabel.isBlank()) {
return;
}
String sender =
(chatUsername != null && !chatUsername.isBlank())
? chatUsername
: chatController.getCurrentUsername();
if (sender == null || sender.isBlank()) {
return;
}
Message message =
new Message(ChatType.LOBBY, chatLobbyId, sender, null, actionLabel.trim());
chatController.onSendToNetwork(message);
}
/**
@@ -1,5 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameService;
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
@@ -24,6 +26,7 @@ public class CasinoGameUI extends Application {
private static final Logger LOG = Logger.getLogger(CasinoGameUI.class.getName());
private static ClientService clientService;
private static ChatController chatController;
private static String username;
private static int lobbyId = -1;
@@ -55,6 +58,10 @@ public class CasinoGameUI extends Application {
CasinoGameUI.username = username;
}
public static void setChatController(ChatController chatController) {
CasinoGameUI.chatController = chatController;
}
/**
* Sets the lobby ID to be used by the application.
*
@@ -77,8 +84,7 @@ public class CasinoGameUI extends Application {
public void start(Stage stage) throws IOException {
if (clientService == null) {
clientService =
ch.unibas.dmi.dbis.cs108.casono.client.ClientApp.getSharedClientService();
clientService = ClientApp.getSharedClientService();
}
if (clientService == null) {
throw new IllegalStateException(
@@ -90,8 +96,7 @@ public class CasinoGameUI extends Application {
String effectiveUsername = normalize(username);
if (effectiveUsername == null) {
effectiveUsername =
normalize(ch.unibas.dmi.dbis.cs108.casono.client.ClientApp.getSharedUsername());
effectiveUsername = normalize(ClientApp.getSharedUsername());
}
if (effectiveUsername == null) {
@@ -105,7 +110,7 @@ public class CasinoGameUI extends Application {
+ "', injectedUsername='"
+ username
+ "', sharedUsername='"
+ ch.unibas.dmi.dbis.cs108.casono.client.ClientApp.getSharedUsername()
+ ClientApp.getSharedUsername()
+ "', hasClientService="
+ (clientService != null));
@@ -125,6 +130,8 @@ public class CasinoGameUI extends Application {
controller.setMyPlayerId(PlayerId.of(effectiveUsername));
controller.setChatContext(effectiveUsername, clientService, lobbyId);
controller.startChat(chatController);
Scene scene = new Scene(root, DEFAULT_WIDTH, DEFAULT_HEIGHT);
stage.setTitle("Casono");
@@ -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");
@@ -98,6 +113,59 @@ public class CasinoBrowserController {
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)(\\?.*)?$");
}
}
@@ -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);
}
}
}
@@ -8,11 +8,22 @@ import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
@@ -59,8 +70,16 @@ public class TaskbarController {
private static final String STYLE_RED_INPUT = "red-input-field";
private static final double SCALE_NORMAL = 1.0;
private static final int DEALER_OFFSET = 3;
private static final String LOGO_PATH = "/images/logoinverted.png";
private static final String LOGO_PATH_MAIN = "/images/logo.png";
private static final double ALERT_LOGO_HEIGHT = 40.0;
private boolean inputActionAllowed;
private int lastReferenceBet = BIG_BLIND;
private Consumer<String> lobbyActionAnnouncer;
private String lastPhase;
private static final int FIRST_PLAYER_MAX_BET = 3000;
private static final int MAX_INPUT_LENGTH = 6;
private static final double MAX_CHIP_PERCENT = 0.30;
/** Standard constructor. Used by FXML. */
public TaskbarController() {
@@ -213,6 +232,11 @@ public class TaskbarController {
this.myPlayerId = myPlayerId;
}
/** Sets an optional callback that publishes simple action labels to lobby chat. */
public void setLobbyActionAnnouncer(Consumer<String> lobbyActionAnnouncer) {
this.lobbyActionAnnouncer = lobbyActionAnnouncer;
}
/**
* Initializes the taskbar controller. This method is called automatically after the FXML
* components have been loaded.
@@ -220,15 +244,102 @@ public class TaskbarController {
@FXML
public void initialize() {
updateBasicButtons(false, false);
if (taskbarInput != null) {
UnaryOperator<TextFormatter.Change> filter =
change -> {
String newText = change.getControlNewText();
if (newText.length() > MAX_INPUT_LENGTH) {
return null;
}
if (!newText.matches("\\d*")) {
return null;
}
return change;
};
taskbarInput.setTextFormatter(new TextFormatter<>(filter));
taskbarInput
.textProperty()
.addListener((obs, oldValue, newValue) -> refreshBetInputUi());
}
Tooltip callTooltip = new Tooltip();
callButton.setTooltip(callTooltip);
callButton.setOnMouseEntered(e -> callTooltip.setText(previewCall()));
Tooltip raiseTooltip = new Tooltip();
raiseButton.setTooltip(raiseTooltip);
raiseButton.setOnMouseEntered(e -> raiseTooltip.setText(previewRaise()));
setBetButtonVisible(false);
}
/**
* Generates a preview string for the Call action, showing the amount that would be called based
* on the current game state and the player's bet status.
*
* @return A string representing the Call action preview, including the amount that would be
* called, or an empty string if the game state is not available.
*/
private String previewCall() {
if (lastState == null) {
return "";
}
int callTarget = effectiveCallTarget(lastState);
Player me = findCurrentPlayer(lastState);
int alreadyInvested = (me != null) ? Math.max(0, me.getBet()) : 0;
int amount = Math.max(0, callTarget - alreadyInvested);
return "CALL " + amount;
}
/**
* Generates a preview string for the Raise action, showing the amount that would be raised.
*
* @return A string representing the Raise action preview, including the amount that would be
* raised.
*/
private String previewRaise() {
if (lastState == null) {
return "";
}
// int callTarget = effectiveCallTarget(lastState);
// Player me = findCurrentPlayer(lastState);
// int alreadyInvested = (me != null) ? Math.max(0, me.getBet()) : 0;
// Integer targetBet = resolveTargetBet(ActionType.RAISE_BUTTON, lastState);
// if (targetBet == null) {
// return "";
// }
// int totalContribution = Math.max(0, targetBet);
GameState state = ensureLatestStateForAction(ActionType.CALL_BUTTON.name().toLowerCase());
// int raiseBy = Math.max(0, targetBet);
int raiseBy = effectiveCallTarget(state);
int callTarget = effectiveCallTarget(lastState);
Player me = findCurrentPlayer(lastState);
int alreadyInvested = (me != null) ? Math.max(0, me.getBet()) : 0;
int amount = Math.max(0, callTarget - alreadyInvested);
int raiseamout = amount + raiseBy;
return "RAISE +" + raiseBy + " TO " + raiseamout;
}
/**
* Updates the taskbar based on the current game state and the player's status.
*
@@ -248,6 +359,7 @@ public class TaskbarController {
}
this.lastState = state;
if (state.currentBet > 0) {
lastReferenceBet = state.currentBet;
}
@@ -269,8 +381,14 @@ public class TaskbarController {
boolean isOut = me.getState() == PlayerState.FOLDED;
boolean isGameFinished = isHandFinished(state);
boolean phaseChanged = lastPhase == null || !lastPhase.equalsIgnoreCase(state.phase);
lastPhase = state.phase;
updateBasicButtons(isMyTurn, isOut || isGameFinished);
applyActionAvailability(state, me, isMyTurn, isOut || isGameFinished);
applyActionAvailability(state, me, isMyTurn, isOut || isGameFinished, phaseChanged);
setMoney(me.getChips());
refreshBetInputUi();
}
@@ -287,7 +405,14 @@ public class TaskbarController {
* if the game is finished, which disables actions.
*/
private void applyActionAvailability(
GameState state, Player me, boolean isMyTurn, boolean isOutOrFinished) {
GameState state,
Player me,
boolean isMyTurn,
boolean isOutOrFinished,
boolean phaseChanged) {
// boolean firstPlayerNewRound =
// phaseChanged && isFirstPreflopPlayer(state, me);
inputActionAllowed = false;
if (!isMyTurn || isOutOrFinished || state == null || me == null) {
setBetButtonVisible(false);
@@ -308,7 +433,7 @@ public class TaskbarController {
return;
}
if (isFirstPreflopPlayerInputOnly(state, me)) {
if (isFirstPreflopPlayer(state, me)) {
setActionEnabled(betButton, true);
setActionEnabled(callButton, false);
setActionEnabled(foldButton, false);
@@ -318,6 +443,16 @@ public class TaskbarController {
return;
}
if (isFirstPlayerOfPhase(state, me)) {
setActionEnabled(betButton, true);
setActionEnabled(callButton, false);
// setActionEnabled(foldButton, false);
setActionEnabled(raiseButton, false);
activateInputField(taskbarInput);
inputActionAllowed = true;
return;
}
int callTarget = effectiveCallTarget(state);
int raiseTarget = safeDouble(callTarget);
ValidationResult callValidation = validateTarget(state, me, callTarget);
@@ -353,7 +488,16 @@ public class TaskbarController {
if (value > Integer.MAX_VALUE / 2) {
return Integer.MAX_VALUE;
}
return value * 2;
int callTarget = effectiveCallTarget(lastState);
Player me = findCurrentPlayer(lastState);
int alreadyInvested = (me != null) ? Math.max(0, me.getBet()) : 0;
int amount = Math.max(0, callTarget - alreadyInvested);
int raisevalue = amount + value;
return raisevalue;
}
/**
@@ -431,10 +575,22 @@ public class TaskbarController {
* @return An integer representing the effective call target for the current game state.
*/
private int effectiveCallTarget(GameState state) {
if (state != null && state.currentBet > 0) {
return state.currentBet;
int currentBet = (state != null) ? Math.max(0, state.currentBet) : 0;
int rememberedBet = Math.max(0, lastReferenceBet);
if (state != null && isPreflop(state.phase) && isInitialPreflopBlindLayout(state)) {
return currentBet;
}
return Math.max(0, lastReferenceBet);
if (currentBet <= 0) {
return rememberedBet;
}
if (rememberedBet <= 0) {
return currentBet;
}
return Math.max(currentBet, rememberedBet);
}
/**
@@ -449,7 +605,7 @@ public class TaskbarController {
* @return A boolean value indicating whether the current player is the first to act in the
* pre-flop phase with only the initial blind layout in place.
*/
private boolean isFirstPreflopPlayerInputOnly(GameState state, Player me) {
private boolean isFirstPreflopPlayer(GameState state, Player me) {
if (state == null || me == null || !isPreflop(state.phase) || state.players == null) {
return false;
}
@@ -473,6 +629,46 @@ public class TaskbarController {
return isInitialPreflopBlindLayout(state);
}
/**
* Checks if the current player is the first to act in the current phase of the game, based on
* the dealer position and the active player index.
*
* @param state The current GameState object representing the state of the game, which includes
* information about
* @param me The Player object representing the current player, used to determine their
* position.
* @return A boolean value indicating whether the current player is the first to act in the
* current phase of the game.
*/
private boolean isFirstPlayerOfPhase(GameState state, Player me) {
if (state == null || me == null || state.players == null) {
return false;
}
int size = state.players.size();
if (size < 2) {
return false;
}
int myIndex = state.players.indexOf(me);
if (myIndex < 0) {
return false;
}
int dealer = state.dealer;
int firstIndex;
if (isPreflop(state.phase)) {
firstIndex = (size == 2) ? dealer : (dealer + DEALER_OFFSET) % size;
} else {
firstIndex = (dealer + 1) % size;
}
return state.activePlayer == firstIndex && myIndex == firstIndex;
}
/**
* Checks if the initial blind layout is still in place during the pre-flop phase.
*
@@ -482,6 +678,10 @@ public class TaskbarController {
* the pre-flop phase.
*/
private boolean isInitialPreflopBlindLayout(GameState state) {
if (state == null || state.players == null || state.players.isEmpty()) {
return false;
}
int sbCount = 0;
int bbCount = 0;
int zeroCount = 0;
@@ -618,57 +818,31 @@ public class TaskbarController {
}
GameState state = ensureLatestStateForAction(actionType.name().toLowerCase());
if (state == null) {
return;
}
if (isHandFinished(state)) {
LOGGER.info("Action {} ignored: hand is already finished", actionType);
if (state == null || isHandFinished(state)) {
if (state != null) {
LOGGER.info("Action {} ignored: hand finished", actionType);
update(state, myPlayerId);
}
return;
}
Player me = findCurrentPlayer(state);
if (me == null) {
LOGGER.error("Action {} blocked: player missing in state", actionType);
LOGGER.error("Action {} blocked: player missing", actionType);
return;
}
int callTarget = effectiveCallTarget(state);
int targetBet;
if (actionType == ActionType.CALL_BUTTON) {
targetBet = callTarget;
} else if (actionType == ActionType.RAISE_BUTTON) {
targetBet = safeDouble(callTarget);
} else {
Integer fromInput = parseInputTarget(taskbarInput.getText());
if (fromInput == null) {
Integer targetBet = resolveTargetBet(actionType, state);
if (targetBet == null) {
return;
}
targetBet = fromInput;
}
ValidationResult validation = validateTarget(state, me, targetBet);
if (!validation.valid) {
refreshBetInputUi();
if (!handleValidation(validation, actionType)) {
return;
}
if (validation.warning) {
showWarningDialog("WARNING", validation.message);
}
if (state.currentBet <= 0) {
gameService.bet(targetBet);
LOGGER.info("Player BET (target={})", targetBet);
} else if (targetBet == callTarget) {
gameService.call();
LOGGER.info("Player CALL (target={})", targetBet);
} else {
int raiseBy = Math.max(0, targetBet - state.currentBet);
gameService.raise(raiseBy);
LOGGER.info("Player RAISE by {} (target={})", raiseBy, targetBet);
}
executeAction(state, targetBet, actionType);
if (targetBet > 0) {
lastReferenceBet = targetBet;
@@ -678,6 +852,148 @@ public class TaskbarController {
refreshGame();
}
/**
* Resolves the target bet amount based on the type of action being submitted and the current
* game state.
*
* @param actionType The type of action being submitted, which determines how the target bet is
* calculated.
* @param state The current GameState object representing the state of the game, which is used
* to determine the effective call target and to validate the input for the bet amount.
* @return An Integer representing the resolved target bet amount for the action being
* submitted, or null if the input is invalid or cannot be resolved based on the action type
* and game state.
*/
private Integer resolveTargetBet(ActionType actionType, GameState state) {
int callTarget = effectiveCallTarget(state);
if (actionType == ActionType.CALL_BUTTON) {
return callTarget;
}
if (actionType == ActionType.RAISE_BUTTON) {
Integer input = parseInputTarget(taskbarInput.getText());
return input != null ? input : callTarget + BIG_BLIND;
}
return parseInputTarget(taskbarInput.getText());
}
/**
* Handles the validation result of a proposed action, including displaying warnings and
* refreshing the UI if the action is blocked or cancelled.
*
* @param validation The ValidationResult object representing the outcome of validating the
* proposed action, which includes whether the action is valid, if it triggers a warning,
* and any associated messages.
* @param actionType The type of action being processed, used for logging purposes to indicate
* which action is being validated and potentially blocked or cancelled.
* @return A boolean value indicating whether the action is valid and can proceed (true) or if
* it is blocked or cancelled due to validation failure or user cancellation after a warning
* (false).
*/
private boolean handleValidation(ValidationResult validation, ActionType actionType) {
if (!validation.valid) {
refreshBetInputUi();
return false;
}
if (validation.warning) {
boolean confirmed = showWarningDialog("WARNING", validation.message);
if (!confirmed) {
LOGGER.info("Action {} cancelled after warning", actionType);
refreshBetInputUi();
return false;
}
}
return true;
}
/**
* Executes the player action by sending the appropriate command to the GameService based on the
* target bet and the current game state.
*
* @param state The current GameState object representing the state of the game, which includes
* information about the current bet and the player's status, used to determine how to
* execute the action based on the target bet.
* @param targetBet The integer value representing the target bet amount for the action being
* executed, which is used to determine
*/
private void executeAction(GameState state, int targetBet, ActionType actionType) {
int callTarget = effectiveCallTarget(state);
String phase = (state != null ? state.phase : "UNKNOWN");
int alreadyInvested =
(findCurrentPlayer(state) != null)
? Math.max(0, findCurrentPlayer(state).getBet())
: 0;
if (actionType == ActionType.CALL_BUTTON) {
int amount = Math.max(0, callTarget - alreadyInvested);
gameService.call();
LOGGER.info("[{}] CALL +{} (total to {})", phase, amount, callTarget);
announceLobbyAction("CALL " + amount);
return;
}
if (actionType == ActionType.RAISE_BUTTON) {
int raiseBy = Math.max(0, targetBet - callTarget);
gameService.raise(raiseBy);
LOGGER.info("[{}] RAISE +{} (target={})", phase, raiseBy, targetBet);
announceLobbyAction("RAISE +" + raiseBy + " TO " + targetBet);
return;
}
if (state.currentBet <= 0) {
gameService.bet(targetBet);
LOGGER.info("[{}] BET {}", phase, targetBet);
announceLobbyAction("BET " + targetBet);
return;
}
if (targetBet == callTarget) {
int amount = Math.max(0, callTarget - alreadyInvested);
gameService.call();
LOGGER.info("[{}] CALL +{} (total to {})", phase, amount, callTarget);
announceLobbyAction("CALL " + amount);
} else {
int raiseBy = Math.max(0, targetBet - callTarget);
gameService.raise(raiseBy);
LOGGER.info("[{}] RAISE +{} (target={})", phase, raiseBy, targetBet);
announceLobbyAction("RAISE +" + raiseBy + " TO " + targetBet);
}
}
/** Publishes a simple action label to the lobby chat, if a publisher is configured. */
private void announceLobbyAction(String actionLabel) {
if (lobbyActionAnnouncer == null || actionLabel == null || actionLabel.isBlank()) {
return;
}
try {
lobbyActionAnnouncer.accept(actionLabel.trim());
} catch (RuntimeException ex) {
LOGGER.warn("Could not publish lobby action '{}': {}", actionLabel, ex.getMessage());
}
}
/**
* Ensures that the latest game state is available for processing a player action. If the last
* known state is null, it attempts to fetch the current state from the GameService.
@@ -691,8 +1007,15 @@ public class TaskbarController {
if (text == null || text.trim().isEmpty()) {
return null;
}
try {
return Integer.parseInt(text.trim());
int value = Integer.parseInt(text.trim());
if (value % SMALL_BLIND != 0) {
return null;
}
return value;
} catch (NumberFormatException e) {
return null;
}
@@ -737,21 +1060,44 @@ public class TaskbarController {
* triggers a warning, or if it is blocked due to being invalid or too risky.
*/
private ValidationResult validateTarget(GameState state, Player me, int targetBet) {
if (state == null || me == null) {
return ValidationResult.blocked("Invalid game state");
}
int callTarget = effectiveCallTarget(state);
int raiseTarget = safeDouble(callTarget);
if (targetBet != callTarget && targetBet != raiseTarget) {
return ValidationResult.blocked("Only calls or exactly two raises allowed");
final int callTarget = effectiveCallTarget(state);
final int alreadyInvested = Math.max(0, me.getBet());
final int chips = Math.max(0, me.getChips());
final int required = Math.max(0, targetBet - alreadyInvested);
final int minRequiredToCall = Math.max(0, callTarget - alreadyInvested);
if (required < minRequiredToCall) {
return ValidationResult.blocked("Bet must match at least the call amount");
}
int alreadyInvested = Math.max(0, me.getBet());
int required = Math.max(0, targetBet - alreadyInvested);
int chips = Math.max(0, me.getChips());
if (isFirstPreflopPlayer(state, me)) {
if (required > FIRST_PLAYER_MAX_BET) {
return ValidationResult.blocked(
"First player cannot bet more than " + FIRST_PLAYER_MAX_BET);
}
}
if (targetBet == callTarget && required == 0) {
if (isPreflop(state.phase)) {
int maxAllowed = (int) Math.floor(me.getChips() * MAX_CHIP_PERCENT);
if (required > maxAllowed) {
return ValidationResult.blocked(
"Preflop: You can only bet up to 30% of your stack (" + maxAllowed + ")");
}
}
if (required == minRequiredToCall) {
if (required > chips) {
return ValidationResult.blocked("Not enough chips to call");
}
return ValidationResult.ok();
}
@@ -760,16 +1106,19 @@ public class TaskbarController {
}
if (required > chips) {
return ValidationResult.blocked("Not enough chips for the next bet");
return ValidationResult.blocked("Not enough chips for this bet");
}
BetRisk risk = evaluateBetRisk(state, required, chips);
if (risk == BetRisk.BLOCKED) {
return ValidationResult.blocked("Assignment for this phase is blocked");
return ValidationResult.blocked("Bet too risky for this phase");
}
if (risk == BetRisk.WARNING) {
return ValidationResult.warning(warningTextForPhase(state));
}
return ValidationResult.ok();
}
@@ -849,12 +1198,50 @@ public class TaskbarController {
* @param content The string representing the content of the warning message, which is displayed
* in the body of the dialog.
*/
private void showWarningDialog(String title, String content) {
private boolean showWarningDialog(String title, String content) {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(content);
alert.showAndWait();
ButtonType proceedButton = new ButtonType("CONTINUE");
ButtonType cancelButton = new ButtonType("CANCEL", ButtonBar.ButtonData.CANCEL_CLOSE);
alert.getButtonTypes().setAll(proceedButton, cancelButton);
applyAlertBranding(alert);
Optional<ButtonType> result = alert.showAndWait();
return result.isPresent() && result.get() == proceedButton;
}
/** Applies Casono icon and logo to alert dialogs when resources are available. */
private void applyAlertBranding(Alert alert) {
if (alert == null) {
return;
}
try (var iconStream = TaskbarController.class.getResourceAsStream(LOGO_PATH);
var mainLogoStream = TaskbarController.class.getResourceAsStream(LOGO_PATH_MAIN)) {
if (iconStream != null) {
Image icon = new Image(iconStream);
if (!icon.isError()
&& alert.getDialogPane() != null
&& alert.getDialogPane().getScene() != null
&& alert.getDialogPane().getScene().getWindow()
instanceof javafx.stage.Stage stage) {
stage.getIcons().add(icon);
}
}
if (mainLogoStream != null) {
Image mainLogo = new Image(mainLogoStream);
if (!mainLogo.isError()) {
ImageView logoView = new ImageView(mainLogo);
logoView.setFitHeight(ALERT_LOGO_HEIGHT);
logoView.setPreserveRatio(true);
alert.setGraphic(logoView);
}
}
} catch (Exception e) {
LOGGER.warn("Could not load alert logo resources: {}", e.getMessage());
}
}
/**
@@ -865,6 +1252,9 @@ public class TaskbarController {
*/
@FXML
private void onTaskbarPressed(MouseEvent event) {
if (taskbar == null || event == null) {
return;
}
xOffset = event.getSceneX() - taskbar.getLayoutX();
yOffset = event.getSceneY() - taskbar.getLayoutY();
}
@@ -873,17 +1263,25 @@ public class TaskbarController {
* Called while dragging the taskbar with the mouse. Updates the position and slightly scales
* the taskbar for visual feedback.
*
* <p>TODO: It still needs to be fixed that the taskbar cannot disappear out of the window.
*
* @param event Das Mausereignis
*/
@FXML
private void onTaskbarDragged(MouseEvent event) {
taskbar.setLayoutX(event.getSceneX() - xOffset);
taskbar.setLayoutY(event.getSceneY() - yOffset);
if (taskbar == null || event == null || taskbar.getScene() == null) {
return;
}
taskbar.setScaleX(TASKBAR_SCALE);
taskbar.setScaleY(TASKBAR_SCALE);
double targetX = event.getSceneX() - xOffset;
double targetY = event.getSceneY() - yOffset;
double maxX = Math.max(0, taskbar.getScene().getWidth() - scaledNodeWidth());
double maxY = Math.max(0, taskbar.getScene().getHeight() - scaledNodeHeight());
taskbar.setLayoutX(clamp(targetX, 0, maxX));
taskbar.setLayoutY(clamp(targetY, 0, maxY));
}
/**
@@ -894,8 +1292,62 @@ public class TaskbarController {
*/
@FXML
private void onTaskbarReleased(MouseEvent event) {
if (taskbar == null) {
return;
}
taskbar.setScaleX(SCALE_NORMAL);
taskbar.setScaleY(SCALE_NORMAL);
if (taskbar.getScene() == null) {
return;
}
double maxX = Math.max(0, taskbar.getScene().getWidth() - scaledNodeWidth());
double maxY = Math.max(0, taskbar.getScene().getHeight() - scaledNodeHeight());
taskbar.setLayoutX(clamp(taskbar.getLayoutX(), 0, maxX));
taskbar.setLayoutY(clamp(taskbar.getLayoutY(), 0, maxY));
}
/**
* Calculates the scaled width of the taskbar node based on its current bounds and scale factor.
*
* @return The scaled width of the taskbar node, ensuring it is non-negative and accounts for
* the current scale applied to the node.
*/
private double scaledNodeWidth() {
double width = taskbar.getBoundsInLocal().getWidth();
if (width <= 0) {
width = taskbar.prefWidth(-1);
}
return Math.max(0, width * taskbar.getScaleX());
}
/**
* Calculates the scaled height of the taskbar node based on its current bounds and scale
* factor.
*
* @return The scaled height of the taskbar node, ensuring it is non-negative and accounts for
* the current scale applied to the node.
*/
private double scaledNodeHeight() {
double height = taskbar.getBoundsInLocal().getHeight();
if (height <= 0) {
height = taskbar.prefHeight(-1);
}
return Math.max(0, height * taskbar.getScaleY());
}
/**
* Clamps a value between a minimum and maximum bound.
*
* @param value the value to clamp
* @param min the lower bound
* @param max the upper bound
* @return the clamped value in the range [min, max]
*/
private double clamp(double value, double min, double max) {
return Math.max(min, Math.min(value, max));
}
/**
@@ -928,7 +1380,7 @@ public class TaskbarController {
/** Called when the Call button is clicked. */
@FXML
private void onInputPlayerCall() {
submitPresetInputAndProcess("call");
submitAction(ActionType.CALL_BUTTON);
}
/** Called when the Fold button is clicked. */
@@ -949,6 +1401,7 @@ public class TaskbarController {
gameService.fold();
LOGGER.info("Player FOLD");
announceLobbyAction("FOLD");
refreshGame();
}
@@ -1062,6 +1515,9 @@ public class TaskbarController {
GameState refreshed = gameService.refresh();
if (refreshed != null) {
lastState = refreshed;
if (refreshed.currentBet > 0 && refreshed.currentBet >= lastReferenceBet) {
lastReferenceBet = refreshed.currentBet;
}
return refreshed;
}
} catch (Exception e) {
@@ -1074,12 +1530,24 @@ public class TaskbarController {
/**
* Opens the integrated Casono web browser.
*
* <p>TODO: Replace the start URL with the official project website (e.g., Tips & Tricks page)
* once the content for strategies and support is available.
* <p>once the content for strategies and support is available.
*/
@FXML
private void onBrowserButtonClick() {
CasinoBrowserController.open("wikipedia.org");
try {
Path path =
Paths.get(
System.getProperty("user.dir"),
"documents",
"docs",
"game-engine",
"manual.html");
CasinoBrowserController.open(path.toUri().toString());
} catch (Exception e) {
e.printStackTrace();
}
}
/** Opens the highscore popup window from the taskbar. */
@@ -1092,6 +1560,7 @@ public class TaskbarController {
alert.setTitle("Info");
alert.setHeaderText(null);
alert.setContentText("No active server connection for highscores.");
applyAlertBranding(alert);
alert.showAndWait();
return;
}
@@ -1124,4 +1593,23 @@ public class TaskbarController {
LOGGER.error("Could not open highscore window from taskbar: {}", e.getMessage());
}
}
/**
* Highlight or unhighlight the taskbar when it is the player's turn.
*
* @param highlighted true to highlight, false to remove highlight
*/
public void setTurnHighlighted(boolean highlighted) {
if (taskbar == null) {
return;
}
if (highlighted) {
if (!taskbar.getStyleClass().contains("player-active-turn")) {
taskbar.getStyleClass().add("player-active-turn");
}
} else {
taskbar.getStyleClass().remove("player-active-turn");
}
}
}
@@ -55,9 +55,9 @@ public class CasinomainuiController {
}
/**
* Initializes the UI components and sets default values. If a shared {@link
* ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService} exists (created at application
* start), the controller reuses it so the connection remains open and already-logged-in.
* Initializes the UI components and sets default values. If a shared {@link ClientService}
* exists (created at application start), the controller reuses it so the connection remains
* open and already-logged-in.
*/
@FXML
public void initialize() {
@@ -141,6 +141,7 @@ public class CasinomainuiController {
AnchorPane.setBottomAnchor(chatNode, 0.0);
AnchorPane.setLeftAnchor(chatNode, 0.0);
AnchorPane.setRightAnchor(chatNode, 0.0);
gridManager.setChatController(chatController);
} catch (IOException e) {
LOGGER.warn("Could not initialize lobby chat UI: {}", e.getMessage());
}
@@ -1,7 +1,10 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import java.util.ArrayList;
import java.util.Collections;
@@ -41,6 +44,8 @@ public class LobbyButtonGridManager {
private final LobbyButtonTranslationManager translationManager;
private final LobbyClient lobbyClient;
private ChatController chatController;
private final ConcurrentHashMap<String, Image> imageCache = new ConcurrentHashMap<>();
private final ExecutorService executor = Executors.newCachedThreadPool();
@@ -553,14 +558,11 @@ public class LobbyButtonGridManager {
try {
var cs = lobbyClient.getClientService();
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI
.setClientService(cs);
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI.setLobbyId(
lobbyId);
CasinoGameUI.setClientService(cs);
CasinoGameUI.setLobbyId(lobbyId);
CasinoGameUI.setChatController(chatController);
String username =
ch.unibas.dmi.dbis.cs108.casono.client.ClientApp
.getSharedUsername();
String username = ClientApp.getSharedUsername();
if (username == null || username.isBlank()) {
username =
"Guest-"
@@ -570,11 +572,9 @@ public class LobbyButtonGridManager {
.toString()
.substring(0, GUEST_ID_LENGTH);
}
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI.setUsername(
username);
CasinoGameUI.setUsername(username);
new ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI()
.start(gameStage);
new CasinoGameUI().start(gameStage);
} catch (Exception e) {
LOGGER.error("Game UI failed: {}", e.getMessage());
@@ -590,4 +590,8 @@ public class LobbyButtonGridManager {
public LobbyClient getLobbyClient() {
return lobbyClient;
}
public void setChatController(ChatController chatController) {
this.chatController = chatController;
}
}
@@ -63,6 +63,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyCleanupJob;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
@@ -73,11 +74,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandR
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionDisconnectJob;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
import java.time.Duration;
@@ -141,37 +138,12 @@ public class ServerApp {
userRegistry,
new CommandContext(lobbyManager, sessionManager));
// Periodic cleanup: remove empty lobbies older than 30s and notify affected
// users
scheduler.scheduleAtFixedRate(
() -> {
try {
var expired =
lobbyManager.findEmptyLobbiesOlderThan(
Duration.ofSeconds(LOBBY_EXPIRY_SECONDS));
for (var lid : expired) {
// remove lobby from manager first
lobbyManager.removeLobby(lid);
// broadcast LOBBY_CLOSED event to all connected sessions
// (requestId=0)
for (Session s : sessionManager.getAllSessions()) {
RequestContext ctx = new RequestContext(s.getId(), 0);
SuccessResponse ev =
new SuccessResponse(
ctx,
ResponseBody.builder()
.param("EVENT", "LOBBY_CLOSED")
.param("LOBBY_ID", lid.value())
.build()) {};
responseDispatcher.dispatch(ev);
}
}
} catch (Exception e) {
logger.warn("Lobby expiry job failed", e);
}
},
new LobbyCleanupJob(
lobbyManager,
sessionManager,
responseDispatcher,
Duration.ofSeconds(LOBBY_EXPIRY_SECONDS)),
LOBBY_CLEANUP_INITIAL_DELAY_SECONDS,
LOBBY_CLEANUP_PERIOD_SECONDS,
TimeUnit.SECONDS);
@@ -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");
}
}
@@ -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) {
Lobby lobby;
if (gameId != null) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
lobby = lobbyManager.getLobby(LobbyId.of(gameId));
} else {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
}
return;
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) {
Lobby lobby;
if (gameId != null) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
lobby = lobbyManager.getLobby(LobbyId.of(gameId));
} else {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
}
return;
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) {
Lobby lobby;
if (gameId != null) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
lobby = lobbyManager.getLobby(LobbyId.of(gameId));
} else {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
}
return;
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);
}
}
@@ -69,6 +69,10 @@ public class GetGameStateResponse extends SuccessResponse {
return;
}
if (!state.tryMarkWinnerPersistedForHand()) {
return;
}
Player winner = findPlayerByIndex(state, winnerIndex);
if (winner == null || winner.getId() == null || winner.getId().value() == null) {
return;
@@ -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) {
Lobby lobby;
if (gameId != null) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
lobby = lobbyManager.getLobby(LobbyId.of(gameId));
} else {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
}
return;
lobby = lobbyManager.getLobbyByUsername(username);
}
GameController game = lobby.getGameController();
@@ -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,6 +5,7 @@ import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
@@ -40,8 +41,7 @@ public class SendMessageHandler extends CommandHandler<SendMessageRequest> {
@Override
public void execute(SendMessageRequest request) {
Message message = request.getMessage();
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> senderUser =
userRegistry.getBySessionId(request.getSessionId());
Optional<User> senderUser = userRegistry.getBySessionId(request.getSessionId());
if (senderUser.isPresent()) {
message.sender = senderUser.get().getName();
}
@@ -35,6 +35,7 @@ public class GameState {
private int currentPlayerIndex;
private boolean handActive;
private boolean allowOutOfTurn;
private boolean winnerPersistedForHand;
private GamePhase phase;
private int dealerIndex;
@@ -551,6 +552,7 @@ public class GameState {
*/
public void startNewHand() {
handActive = true;
resetWinnerPersistedForHand();
phase = GamePhase.PREFLOP;
pot = new Pot();
@@ -575,6 +577,24 @@ public class GameState {
setCurrentPlayerToPreflopFirstToAct();
}
/**
* Marks the winner as already persisted for the current hand.
*
* @return true exactly once per hand; false on all subsequent calls.
*/
public synchronized boolean tryMarkWinnerPersistedForHand() {
if (winnerPersistedForHand) {
return false;
}
winnerPersistedForHand = true;
return true;
}
/** Resets the winner persistence guard for a new hand. */
public synchronized void resetWinnerPersistedForHand() {
winnerPersistedForHand = false;
}
public void foldPlayer(PlayerId playerId) {
getPlayer(playerId).setFolded(true);
}
@@ -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);
}
}
}
}
@@ -48,7 +48,7 @@ public class TokenClassifier {
}
private static void validateSeparator(List<RawToken> rawTokens, int index) {
boolean missingKey = index == 0 || rawTokens.get(index - 1).type() != RawTokenType.WORD;
boolean missingKey = index <= 1 || rawTokens.get(index - 1).type() != RawTokenType.WORD;
boolean missingValue =
index + 1 >= rawTokens.size()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 645 KiB

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

@@ -48,7 +48,7 @@
}
.taskbar {
-fx-background-color: rgba(13, 158, 59);
-fx-background-color: rgb(13, 158, 59);
-fx-background-radius: 23;
-fx-border-radius: 20;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
@@ -56,13 +56,17 @@
-fx-padding: 10 25;
-fx-cursor: move;
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5);
transition: all 0.2s ease-in-out;
}
.taskbar:hover {
-fx-border-color: #ffffff;
}
.taskbar.player-active-turn {
/* Active turn should only change the shadow to avoid flicker/transparency jumps. */
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
}
.black-input-field {
-fx-background-color: #1a1a1a;
-fx-text-fill: #ffffff;
@@ -229,6 +233,13 @@
-fx-opacity: 1;
}
.player-card.player-cards-active-turn {
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
}
.dealer-box.player-cards-active-turn {
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
}
.chips-icon {
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
@@ -243,6 +254,10 @@
-fx-padding: 10;
}
/*.player-cards-box.player-cards-active-turn {*/
/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/
/*}*/
.money-value {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
@@ -297,6 +312,14 @@
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.status-inner-box-top.player-status-active-turn {
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
}
.status-inner-box-bottom.player-status-active-turn {
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
}
.status-label-small {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
@@ -369,3 +392,24 @@
-fx-font-weight: bold;
-fx-font-size: 16px;
}
.context-menu {
-fx-background-color: #333333;
-fx-text-fill: #ffffff;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-background-radius: 8;
-fx-border-radius: 8;
-fx-border-color: #333333;
-fx-color-color: #cccccc;
-fx-border-width: 2;
-fx-padding: 5 12;
}
.context-menu .menu-item {
-fx-background-color: #333333;
}
.context-menu .menu-item:focused {
-fx-background-color: #333333;
}
@@ -5,6 +5,7 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.image.*?>
<!-- Main container: Links the UI to the CasinoGameController and loads Casinogameui.css -->
<AnchorPane xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1"
@@ -66,7 +67,7 @@
</Label>
<Label fx:id="tableText"
text="Place your bet"
text="Welcome! The round is about to begin."
styleClass="table-title" />
</VBox>
@@ -75,12 +76,14 @@
<HBox fx:id="communityCardsBox"
styleClass="community-cards-box"
alignment="CENTER"
GridPane.columnIndex="2"
spacing="10"/>
<!-- Pot Box -->
<HBox fx:id="potBox"
alignment="CENTER"
spacing="4"
GridPane.columnIndex="2"
maxWidth="Infinity"
maxHeight="Infinity"/>
@@ -90,6 +93,7 @@
<!-- Player Cards -->
<HBox fx:id="playerCardsBox"
styleClass="player-cards-box"
spacing="10"
alignment="CENTER"
StackPane.alignment="BOTTOM_CENTER">
@@ -98,6 +102,7 @@
styleClass="dealer-box"
fitWidth="100"
fitHeight="100"
GridPane.columnIndex="2"
preserveRatio="true"
visible="false"/>
@@ -116,56 +121,48 @@
<!-- RIGHT SIDE (Chat Box) -->
<AnchorPane fx:id="chatContainer" GridPane.columnIndex="2" />
<HBox AnchorPane.topAnchor="45"
AnchorPane.leftAnchor="0"
AnchorPane.rightAnchor="0"
alignment="CENTER"
spacing="300">
<VBox HBox.hgrow="ALWAYS"
alignment="TOP_LEFT"
translateX="50"
translateY="50">
<fx:include fx:id="player1"
source="gameuicomponents/Playerstatus.fxml"/>
</VBox>
<VBox HBox.hgrow="ALWAYS"
alignment="TOP_CENTER"
translateY="20">
<fx:include fx:id="player2"
source="gameuicomponents/Playerstatus.fxml"/>
</VBox>
<VBox HBox.hgrow="ALWAYS"
alignment="TOP_RIGHT"
translateX="-50"
translateY="50">
<fx:include fx:id="player3"
source="gameuicomponents/Playerstatus.fxml"/>
</VBox>
</HBox>
</children>
</GridPane>
<!-- movable taskbar -->
<AnchorPane>
<fx:include fx:id="taskbarInclude" source="gameuicomponents/Taskbar.fxml"/>
<fx:include fx:id="taskbarInclude" source="gameuicomponents/Taskbar.fxml" GridPane.columnIndex="1"/>
</AnchorPane>
<!-- Opponent Status 1: One of three panels displaying the opponent's icon, name, and account balance -->
<GridPane AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0"
AnchorPane.topAnchor="50">
<columnConstraints>
<ColumnConstraints percentWidth="5"/>
<ColumnConstraints percentWidth="16"/>
</columnConstraints>
<children>
<fx:include fx:id="player1" source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/>
</children>
</GridPane>
<!-- Opponent Status 2: One of three panels displaying the opponent's icon, name, and account balance -->
<GridPane AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0"
AnchorPane.topAnchor="20">
<columnConstraints>
<ColumnConstraints percentWidth="30"/>
<ColumnConstraints percentWidth="16"/>
</columnConstraints>
<children>
<fx:include fx:id="player2" source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/>
</children>
</GridPane>
<!-- Opponent Status 3: One of three panels that displays the opponent's icon, name, and account balance -->
<GridPane AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0"
AnchorPane.topAnchor="50">
<columnConstraints>
<ColumnConstraints percentWidth="55"/>
<ColumnConstraints percentWidth="16"/>
</columnConstraints>
<children>
<fx:include fx:id="player3" source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/>
</children>
</GridPane>
</AnchorPane>
@@ -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
}
@@ -7,6 +7,7 @@
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<VBox fx:id="highscoreRoot"
xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1"
@@ -6,8 +6,6 @@
<?import javafx.geometry.Insets?>
<!--
TODO: Main placeholder for player status & input.
This layout currently serves as a visual structure. As soon as the game engine and
server requests (API/requests) can be processed, the name, account balance
and status displays will be dynamically populated with real-time data from the server.
@@ -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,7 +3,6 @@
<?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"
@@ -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,6 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.network.TestServer;
import javafx.scene.layout.GridPane;
import org.junit.jupiter.api.*;
@@ -8,15 +9,14 @@ class LobbyButtonGridManagerTest {
LobbyButtonGridManager gridManager;
LobbyButtonTranslationManager translationManager;
GridPane gridPane;
ch.unibas.dmi.dbis.cs108.casono.client.network.TestServer testServer;
TestServer testServer;
@BeforeEach
void setUp() throws Exception {
gridPane = new GridPane();
translationManager = LobbyButtonTranslationManager.getInstance();
translationManager.getButtonIdToLobbyId().clear();
// Start an in-process test server and connect a real ClientService to it.
testServer = new ch.unibas.dmi.dbis.cs108.casono.client.network.TestServer();
testServer = new TestServer();
String host = "127.0.0.1";
int port = testServer.getPort();
ClientService clientService = new ClientService(host, port);
@@ -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());
}
}