Compare commits

...

273 Commits

Author SHA1 Message Date
lars-winzer 17b4b13beb Add header project readme containing metadata about this project 2026-05-29 18:42:05 +02:00
Jona Walpert 7d759bc34b Merge branch 'chore/remove-sound-from-game-trailer' into 'main'
Legal: Remove sound from trailer as we don't want microsoft to sue us :)

See merge request cs108-fs26/Gruppe-13!194
2026-05-21 12:01:23 +00:00
Jona Walpert c79066b111 Legal: Remove sound from trailer as we don't want microsoft to sue us :) 2026-05-21 13:58:33 +02:00
Jona Walpert be7b528cbc Merge branch 'docs/MS6' into 'main'
Add: MS6 powerpoint presentation

See merge request cs108-fs26/Gruppe-13!193
2026-05-20 14:27:57 +00:00
Jona Walpert 1563bc3fa5 Add: MS6 powerpoint presentation 2026-05-20 16:18:23 +02:00
Lars Simon Winzer ea647b8cfc Merge branch 'chore/156-diary-entry-of-13th-of-may' into 'main'
Write diary entry of 13th of may

Closes #147 and #156

See merge request cs108-fs26/Gruppe-13!192
2026-05-17 23:01:26 +02:00
Lars Simon Winzer 706738d9a7 Docs: Write diary entry 2026-05-17 22:52:58 +02:00
Lars Simon Winzer 7de3af0cef Merge branch 'chore/155-write-qa-report-for-ms6' into 'main'
Add QA report with exported pdf and images

Closes #152, #151, and #155

See merge request cs108-fs26/Gruppe-13!191
2026-05-17 22:45:11 +02:00
Lars Simon Winzer 85a3af9047 Docs: Add QA report with exported pdf and images 2026-05-17 22:38:31 +02:00
Mathis Ginkel 537e8aa771 Merge branch 'feat/manual-fixup' into 'main'
Feat: Replacement of syntax that could not be displayed in the casono manual

See merge request cs108-fs26/Gruppe-13!190
2026-05-17 18:47:15 +00:00
Mathis Ginkel d869f81f1c Feat: markdown syntax that the html version could not display was replaced 2026-05-17 20:36:23 +02:00
Julian Kropff 144b5fa70e Merge branch 'feat/add-casono-domain' into 'main'
Add: casono website domain into Casono Browser

See merge request cs108-fs26/Gruppe-13!189
2026-05-17 15:44:59 +00:00
Julian Kropff 8843b63281 Add: casono website domain into Casono Browser 2026-05-17 17:40:40 +02:00
Julian Kropff ecdaf20ece Merge branch 'feat/casono-browser-file-search' into 'main'
Feat: Casono Browser file search

See merge request cs108-fs26/Gruppe-13!188
2026-05-17 15:10:48 +00:00
Julian Kropff 2d82dbdb14 Fix: search to find index.html. 2026-05-17 16:35:09 +02:00
Julian Kropff da65a0470c Fix: the Casono Browsers search algorithm for searching the manual and website files. 2026-05-17 16:08:11 +02:00
Mathis Ginkel 17ed897516 Merge branch 'feat/manual-improvement' into 'main'
Feat: Update casono-manual

See merge request cs108-fs26/Gruppe-13!187
2026-05-17 12:55:44 +00:00
Mathis Ginkel 1402a1231a Feat: update manual.html 2026-05-17 14:46:43 +02:00
Mathis Ginkel f9eb293460 Merge branch 'main' into feat/manual-improvement 2026-05-17 14:43:16 +02:00
Mathis Ginkel dfd0ae6a59 Merge remote-tracking branch 'origin/feat/manual-improvement' into feat/manual-improvement 2026-05-17 14:32:16 +02:00
Mathis Ginkel a4c138c461 Feat: replace gradle commands to start the game in the manual with commands to start the jar file 2026-05-17 14:30:00 +02:00
Mathis Ginkel f296bf272e Add: brings markdown file changes to manual.html 2026-05-17 14:30:00 +02:00
Mathis Ginkel 9d00b948c7 Add: description to change themes of the GameUI and change the description of the ChatUI 2026-05-17 14:30:00 +02:00
Mathis Ginkel d5b9f223f3 Add: manual section for ChatUI 2026-05-17 14:30:00 +02:00
Julian Kropff d5c833fa00 Fix: Casono Manual: avg -> svg 2026-05-17 14:30:00 +02:00
Julian Kropff 69cac803ad Docs: update Casono Manual 2026-05-17 14:30:00 +02:00
Mathis Ginkel cbef7c3888 Feat: replace gradle commands to start the game in the manual with commands to start the jar file 2026-05-17 14:26:45 +02:00
Julian Kropff 5df9a7ffc4 Merge branch 'feat/game-ui-optimization' into 'main'
Feat: Game UI optimization

See merge request cs108-fs26/Gruppe-13!186
2026-05-16 20:52:37 +00:00
Julian Kropff d11fea9c5d Fix: Apply Spotless 2026-05-16 22:47:30 +02:00
Julian Kropff 086394d25e Fix: Updating the pot when the winner is known. 2026-05-16 22:38:14 +02:00
Julian Kropff 4020b196cb Fix: A Better Method for Identifying the first Player of the Round. 2026-05-16 22:36:13 +02:00
Jona Walpert 3f168daf78 Merge branch 'feat/prohibit-name-change-text-instead-of-popup' into 'main'
Feat: Instead of a popup saying you are not allowed to change username while...

See merge request cs108-fs26/Gruppe-13!185
2026-05-16 20:33:50 +00:00
Jona Walpert 4760b55673 Feat: Instead of a popup saying you are not allowed to change username while the lobyb is still active, this commit changes the "Casono" Title to more or less the same message for 2 seconds and then switches back. This improves the quality of the UI drastically 2026-05-16 22:31:03 +02:00
Julian Kropff 35e30eec2f Fix: Call problem in Turn/River - the pot is no longer being updated. 2026-05-16 22:09:22 +02:00
Jona Walpert aef36f8a73 Merge branch 'fix/lobby-cleanup-blocked' into 'main'
Fix: Lobby cleanup got blocked by previous feature for rejoining lobby while game is still going

See merge request cs108-fs26/Gruppe-13!184
2026-05-15 13:40:45 +00:00
Jona Walpert a893418934 Fix: Lobby cleanup got blocked by previous feature for rejoining lobby while game is still going. 2026-05-15 15:37:55 +02:00
Julian Kropff b6c59a92a1 Merge remote-tracking branch 'origin/main' into feat/game-ui-optimization 2026-05-14 20:22:36 +02:00
Julian Kropff f77cf41cd5 Merge branch 'feat/outreach-landing-page-no-script' into 'main'
Add: noscript banner for Casono Browser with disabled JavaScript

See merge request cs108-fs26/Gruppe-13!183
2026-05-14 18:21:36 +00:00
Julian Kropff df2dc51acb Add: noscript banner for Casono Browser with disabled JavaScript 2026-05-14 20:05:13 +02:00
Julian Kropff 3dbedbb047 Fix: Casono Browser URL allowlist 2026-05-14 20:00:20 +02:00
Julian Kropff baccb15c17 Add: Casono Website and VS Code dev buttons in TaskbarController 2026-05-14 19:58:11 +02:00
Julian Kropff bfef83f7e7 Add: Casono Website and VS Code dev buttons in Taskbar fxml 2026-05-14 19:57:54 +02:00
Julian Kropff ea143e2a24 Fix: Apply Spotless 2026-05-14 19:24:59 +02:00
Julian Kropff 894515204c Merge remote-tracking branch 'origin/main' into feat/game-ui-optimization 2026-05-14 19:18:58 +02:00
Julian Kropff b030948cb6 Merge branch 'feat/outreach-landing-page' into 'main'
Add: Casono Website

See merge request cs108-fs26/Gruppe-13!182
2026-05-14 17:04:58 +00:00
Julian Kropff f78e9d8a25 Fix: table preview image 2026-05-14 18:51:04 +02:00
Julian Kropff 089aeb9419 Merge branch 'feat/chat-ui-style-optimization' into 'main'
Style: Update Chat UI themes

See merge request cs108-fs26/Gruppe-13!181
2026-05-14 16:42:30 +00:00
Julian Kropff 94613626d9 Style: Update Chat UI themes 2026-05-14 18:30:40 +02:00
Julian Kropff 3d0fca616a Fix: Harden GameClient command execution with exception handling 2026-05-14 18:05:05 +02:00
Julian Kropff 2f744a5558 Fix: Improve GameService action queue and concurrency handling 2026-05-14 18:02:07 +02:00
Julian Kropff 46919a064d Fix: Game UI action locking and improve taskbar controller responsiveness 2026-05-14 18:00:11 +02:00
Julian Kropff 310cb59fe6 Fix: Optimized Game UI rendering and controller responsiveness 2026-05-14 17:58:36 +02:00
Julian Kropff d57fd08b37 Fix: Improve Game UI update loop timing and prevent overlapping updates 2026-05-14 16:15:47 +02:00
Mathis Ginkel f0135ce6f4 Merge branch 'fix/140-chat-messages' into 'main'
Fix: special character only chat messages

Closes #140

See merge request cs108-fs26/Gruppe-13!180
2026-05-14 11:51:45 +00:00
Jona Walpert 09aca862ca Merge branch 'feat/f11-fullscreen-toggle' into 'main'
Feat: Add F11 fullscreen toggle

See merge request cs108-fs26/Gruppe-13!179
2026-05-14 11:48:01 +00:00
Jona Walpert a2212aa91c Merge branch 'feat/black-background-video-player' into 'main'
Feat: add black background to intro video player

See merge request cs108-fs26/Gruppe-13!178
2026-05-14 11:45:42 +00:00
Mathis Ginkel 121b20f24f Merge branch 'main' into feat/140-chat-messages 2026-05-14 11:46:43 +02:00
Mathis Ginkel 8790b06277 Fix: apostrophe-only messages are now prohibited 2026-05-14 11:44:41 +02:00
Jona Walpert bca9669703 Feat: Add F11 fullscreen toggle 2026-05-14 11:43:04 +02:00
Julian Kropff 09cc7e39c5 Feat: add black background to intro video player 2026-05-14 11:26:11 +02:00
Jona Walpert 0aae4221e2 Deleted placeholder images 2026-05-14 11:19:55 +02:00
Jona Walpert f15fed2243 Merge branch 'feat/disable-esc-to-leave-message' into 'main'
Feat: Disable "ESC to leave fullscreen" message

See merge request cs108-fs26/Gruppe-13!177
2026-05-13 21:59:03 +00:00
Jona Walpert cea21093c4 Feat: Disable "ESC to leave fullscreen" message 2026-05-13 23:56:19 +02:00
Jona Walpert e9dc8f1586 Merge branch 'feat/141-intro-video-fullscreen-startup' into 'main'
Feat: Add intro video

See merge request cs108-fs26/Gruppe-13!176
2026-05-13 21:49:08 +00:00
Jona Walpert 56421b4c15 Feat: Add intro video 2026-05-13 23:46:26 +02:00
Jona Walpert 87023b7238 Merge branch 'fix/143-client-rejoin-flow' into 'main'
fix: add client leave-lobby rejoin flow

Closes #143

See merge request cs108-fs26/Gruppe-13!175
2026-05-13 21:33:11 +00:00
Jona Walpert 6bd0ed6064 Merge branch 'fix/143-block-username-during-active-game' into 'main'
fix: block username changes during active games

See merge request cs108-fs26/Gruppe-13!174
2026-05-13 21:29:48 +00:00
Jona Walpert b58eb656b3 fix: add client leave-lobby rejoin flow 2026-05-13 23:25:57 +02:00
Jona Walpert c10dc81ef3 fix: block username changes during active games 2026-05-13 23:25:11 +02:00
Jona Walpert 0097da6bdc Merge branch 'feat/143-leave-lobby-command' into 'main'
Feat: Add leave-lobby command

See merge request cs108-fs26/Gruppe-13!172
2026-05-13 21:17:57 +00:00
Jona Walpert 448f2ca472 Merge branch 'main' into 'feat/143-leave-lobby-command' 2026-05-13 23:12:44 +02:00
Jona Walpert 01831fadc0 Merge branch 'feat/143-lobby-absent-player-tracking' into 'main'
Feat: Track absent players in lobbies

See merge request cs108-fs26/Gruppe-13!173
2026-05-13 21:11:28 +00:00
Jona Walpert 1d5d608a18 Feat: Track absent players in lobbies
- Add absent player tracking to Lobby with leave/restore methods
- Implement rejoin logic in LobbyManager to restore absent players
- Add game-end callback mechanism to GameController and Lobby
- Extend LobbyEventListener with onGameEnded event
2026-05-13 23:08:38 +02:00
Jona Walpert cd8aa9e709 Feat: Add leave-lobby command 2026-05-13 23:00:06 +02:00
Jona Walpert 9706e140ac Merge branch 'feat/133-add-sounds-to-game' into 'main'
Feat: Adds sounds to game

Closes #138

See merge request cs108-fs26/Gruppe-13!171
2026-05-13 19:38:53 +00:00
Jona Walpert 4afb96ee7a Merge branch 'main' into 'feat/133-add-sounds-to-game' 2026-05-13 21:21:38 +02:00
Julian Kropff 39b068a5cf Merge branch 'feat/game-ui-profile-picture' into 'main'
Add: Profile Pictures

See merge request cs108-fs26/Gruppe-13!170
2026-05-13 19:18:22 +00:00
Jona Walpert c5b92d9ba8 Feat: Add gmaeplay video 2026-05-13 21:15:21 +02:00
Jona Walpert 3314060eb1 Feat: Add team member pcitures and profile links 2026-05-13 21:12:56 +02:00
Jona Walpert 6aaaaa55a3 Add: Trailer video 2026-05-13 21:09:11 +02:00
Julian Kropff d687ed8238 Merge branch 'feat/ui-new-rug' into 'main'
Add: mew wallpapers

See merge request cs108-fs26/Gruppe-13!169
2026-05-13 19:04:38 +00:00
Julian Kropff 6981a1fc0b Add: Standard Wood theme 2026-05-13 20:58:05 +02:00
Jona Walpert d2d83edfb2 Merge branch 'ci/add-javafx-media-module' into 'main'
Ci: Add javafx.media module to support video playback

See merge request cs108-fs26/Gruppe-13!168
2026-05-13 17:25:50 +00:00
Julian Kropff edcee79a10 Style: The new wallpapers have been added to the UI 2026-05-13 18:57:26 +02:00
Julian Kropff 6246615655 Fix: The background image has been made brighter 2026-05-13 18:55:55 +02:00
Julian Kropff cf712a46c0 Add: new background images 2026-05-13 18:32:44 +02:00
Julian Kropff 59f1763926 Add: profile picture loading in PlayerStatusController 2026-05-13 17:55:37 +02:00
Julian Kropff aad00bda0b Style: add playerProfileImage ImageView to Playerstatus fxml 2026-05-13 17:52:18 +02:00
Julian Kropff 903624e968 Add: Profile Pictures 2026-05-13 17:48:40 +02:00
Jona Walpert 5294615aca Ci: Add javafx.media module to support video playback 2026-05-13 15:49:42 +02:00
Jona Walpert d474d85b41 Merge branch 'feat/137-Save-money-in-highscore-list' into 'main'
Feat: Money is now saved in the Higshcore list.

Closes #137

See merge request cs108-fs26/Gruppe-13!167
2026-05-13 11:06:31 +00:00
Lars Simon Winzer baa1d51a1f Merge branch 'feat/60-shorten-logger-name' into 'main'
Short logger names to reduce visual clutter in log output

Closes #60

See merge request cs108-fs26/Gruppe-13!166
2026-05-12 19:06:56 +02:00
Jona Walpert fedaa87202 Style: Fixed spotless error 2026-05-12 16:02:39 +02:00
Jona Walpert 34bac0adb6 Feat: Money is now saved in the Higshcore list.
Furthermore if two people win the pot both will get added to the highscore list
2026-05-12 16:00:38 +02:00
Lars Simon Winzer 0be59681e4 Refactor: Short logger name for application entry point 2026-05-12 12:59:52 +02:00
Lars Simon Winzer 28aad75930 Refactor: Short logger names for classes in client-side session package 2026-05-12 12:59:32 +02:00
Lars Simon Winzer eefcd10135 Refactor: Short logger names for classes in server-side packages 2026-05-12 12:59:13 +02:00
Julian Kropff b7328769a9 Merge branch 'feat/game-ui-glass-theme' into 'main'
Add: light glass theme for Game UI

See merge request cs108-fs26/Gruppe-13!165
2026-05-11 16:49:53 +00:00
Julian Kropff c8e437f8f0 Fix: checkstyle 2026-05-11 18:45:29 +02:00
Julian Kropff 87971103b8 Add: light glass theme for Game UI 2026-05-11 18:38:49 +02:00
Mathis Ginkel 145d6e7d31 Add: brings markdown file changes to manual.html 2026-05-10 22:14:09 +02:00
Mathis Ginkel 426a108b54 Add: description to change themes of the GameUI and change the description of the ChatUI 2026-05-10 21:56:27 +02:00
Julian Kropff dd1a9a4e23 Merge branch 'feat/game-ui-optimization' into 'main'
Game UI Optimisation

See merge request cs108-fs26/Gruppe-13!164
2026-05-10 18:54:22 +00:00
Julian Kropff f60d17edfe Fix: typo in Taskbar fxml 2026-05-10 20:42:46 +02:00
Mathis Ginkel b30af0c212 Fix: changed descriptive text of ChatUI elements 2026-05-10 17:33:42 +02:00
Mathis Ginkel 3be6fb0706 Add: manual section for ChatUI 2026-05-10 17:28:41 +02:00
Mathis Ginkel 47475dc4d3 Style: Apply Spotless 2026-05-10 16:49:15 +02:00
Mathis Ginkel 351ac06aab Test: Make sure that the conversion of response lines into parameters is executed correctly 2026-05-10 16:45:46 +02:00
Mathis Ginkel 88d37bca49 Fix: Messages with only special characters can be sent in the chat 2026-05-10 16:43:41 +02:00
Julian Kropff 1a96d5fb23 Fix: You should only be able to bet the minimum from your last bet on the Flop, not on the Turn or River from the second-to-last player. 2026-05-09 17:27:55 +02:00
Julian Kropff 2cfc40543a Fix: Apply Spotless 2026-05-08 16:39:02 +02:00
Julian Kropff 9f5625b2f6 Add: Notebook (Tips) box for help 2026-05-08 16:34:17 +02:00
Julian Kropff 2f19fff618 Feat: Improving the Game UI and implementing new Features:
- Improve dealer chip Rendering.
- Improve pot Rendering.
- Uses names instead of IDs as identifiers.
- Users are displayed one after another as they enter the Game UI.
- Correct order of opponents <- B <-> S <-> D <-> O -> in the Game UI.
- The boxes for the opponents should have a working hover effect.
- Improve the rendering of the table text.
- Remove hover animations from cards and the pot for improved handling and a more stable Game UI.
2026-05-08 16:27:42 +02:00
Julian Kropff b63ce096fa Style: the boxes for the opponents should have a working hover effect. 2026-05-08 15:08:13 +02:00
Julian Kropff b304fa04ed Fix: correct kicker logic and split pot evaluation
- Fixed kicker calculation for ties.
- Refactored game engine to return a list of winners.
- Enabled support for multiple winners in split pot scenarios.
2026-05-08 15:05:39 +02:00
Julian Kropff 394cdf4ea1 Remove animations from cards and the pot to improve usability, make the game interface more stable, and enhance the glass themes 2026-05-08 12:57:32 +02:00
Julian Kropff 3100e4008d Add: settings with theme options: Default, Dark, and Glass 2026-05-08 12:54:31 +02:00
Julian Kropff 4cf88a9a2f Add: tips and settings boxes to Game Ui fxml 2026-05-07 16:28:33 +02:00
Julian Kropff e4707bd6ee Style: improve help button in taskbar fxml 2026-05-07 16:25:45 +02:00
Julian Kropff f843adc93b Add: settings fxml for theme switching 2026-05-07 16:23:25 +02:00
Julian Kropff bddd9658cf Add: notebook fxml for tips 2026-05-07 16:20:53 +02:00
Julian Kropff 217209997f Style: fix Game Ui default theme 2026-05-07 16:15:54 +02:00
Julian Kropff 712ba913e7 Style: add glass theme 2026-05-07 16:14:37 +02:00
Julian Kropff 6f8c43397c Style: add dark theme 2026-05-07 16:13:34 +02:00
jk 479fab6557 Add: new wallpaper for the glass theme 2026-05-07 16:09:45 +02:00
Jona Walpert 5a3c47660d Refactor: rewrite lamdba execution to more elegant version
Co-authored-by: Copilot <copilot@github.com>
2026-04-30 11:44:57 +02:00
Jona Walpert be8e46d356 Style: Fix spotless checkStyle
Co-authored-by: Copilot <copilot@github.com>
2026-04-30 11:41:04 +02:00
Jona Walpert ba1249bf93 Feat: Add button cklick and card draw sounds to gameUI 2026-04-30 11:37:22 +02:00
Jona Walpert cf52d293a4 Feat: Add sounds to chat UI 2026-04-30 11:35:34 +02:00
Jona Walpert b397478337 Feat: Add button click sounds to Lobby UI 2026-04-30 11:34:49 +02:00
Jona Walpert 0d896d6aad Add: Add sound manager to play sounds 2026-04-30 11:33:39 +02:00
Jona Walpert ef1f3c20b6 Feat: Add button cklick and card draw sound 2026-04-30 11:31:51 +02:00
Julian Kropff 61030c3528 Fix: Casono Manual: avg -> svg 2026-04-27 09:45:44 +02:00
Julian Kropff 4ce9021e57 Docs: update Casono Manual 2026-04-27 09:41:17 +02:00
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
Jona Walpert a85eb968fa Add: round edges for Table image 2026-04-13 01:25:05 +02:00
Jona Walpert 0006ed9faa Add: change wording 2026-04-13 01:24:26 +02:00
Jona Walpert 457fa58a07 Add: remove footer 2026-04-13 01:18:29 +02:00
Jona Walpert 22b3e1764d Feat: Add downlaod button functionality to download project from gitlab 2026-04-13 01:16:49 +02:00
Jona Walpert 31f7ee8935 Add: change placehodler link in developer Card of "Jona Walpert" to his homepage 2026-04-13 01:13:13 +02:00
Jona Walpert 088407b041 Feat: Add clickable Developer Cards with placehodler links 2026-04-13 01:12:16 +02:00
Jona Walpert 30f98b1333 Add: Shortened website title 2026-04-13 01:07:23 +02:00
Jona Walpert 10dac9a68e Add: remove unnecessary placholder Video 2026-04-13 01:05:30 +02:00
Jona Walpert 786b7e990d Feat: Add English - German Switch, Add Trailer & Gameplay section 2026-04-13 01:02:35 +02:00
Jona Walpert 1b154c685e Add: change table preview to more detailled image 2026-04-09 16:56:16 +02:00
Jona Walpert f12e1e4529 Fix: typo in website 2026-04-09 16:55:39 +02:00
Jona Walpert b85e87798e Fix: Update team member names in outreach website
Replaced placeholder names with correct names for Matthis and Julian in outreach/index.html.
2026-03-31 13:27:00 +02:00
Jona Walpert 5448f5683d Add: Prototype outreach website with placeholders
Initial version of the outreach landing page.
Includes placeholder and stock images and placeholder developer names for team section.
2026-03-30 23:55:12 +02:00
152 changed files with 12553 additions and 1739 deletions
+22
View File
@@ -1,3 +1,25 @@
> [!IMPORTANT]
> ## Programming Project (CS108)
>
> - **Course:** Programmierprojekt (CS108)
> - **Semester:** FS26
> - **Institution:** University of Basel
> - **Duration:** 18 February 2026 29 June 2026
> - **Project Type:** Group Project
> - **Project members:** Mathis Ginkel, Julian Kropff, Jona Walpert and me (Lars Winzer)
>
> ### Topic
>
> As part of the *Programming Project* course (CS108) for the semester FS26 at the University of Basel, we developed a computer game implementation of the popular card game Poker.
>
> The project's focus was networking and distributed systems design. A key aspect of the implementation was the development and integration of a custom protocol specifically designed for client-server communication. This protocol handled game state synchronization, player actions, lobby and global chat functionality, and real-time updates across the network.
>
> ### Contribution Context
>
> This repository contains work created during the official duration of the course in collaboration with three other students.
>
> Further information regarding individual contributions, repository state, synchronization status, and project ownership may be documented separately within this repository.
<div align="center">
<img src="documents/images/logo.png" alt="Game Logo" width="200"/>
</div>
+1 -1
View File
@@ -26,7 +26,7 @@ repositories {
javafx {
version = "25.0.2"
modules = ['javafx.controls', 'javafx.fxml', 'javafx.base', 'javafx.graphics', 'javafx.web']
modules = ['javafx.controls', 'javafx.fxml', 'javafx.base', 'javafx.graphics', 'javafx.web', 'javafx.media']
}
dependencies {
+5
View File
@@ -0,0 +1,5 @@
## Besprechung vom 13.05.2026
### Abstimmung über die Implementierung der Auto-Discovery im lokalen Netzwerk
Über einen vom Server ausgesendeten Multicast gäbe es die Möglichkeit das Clients in ihrem lokalen Netzwerk aktive Server automatisch finden.
Da Julian für Prüfungen lernen muss, haben wir diese Funktion leider nicht weiter verfolgt.
+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.
+5 -2
View File
@@ -36,8 +36,11 @@ 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
*(Meeting notes to be added here when available)*
### [13. Mai 2026](13-05.md)
- Discussion on possible auto-discovery feature; not implementing due to time constrains
+291 -23
View File
@@ -1,32 +1,131 @@
# Casono Manual
> Hinweis: Eine vereinfachte Einführung finden Sie in der Datei [Casono Rules Easy Description](casono-rules-easy-description.md).
> Dieses Dokument beschreibt den Ablauf eines Pokerspiels anhand eines Beispiels.
<!-- vim-markdown-toc GFM -->
* [1. Spielübersicht](#1-spielübersicht)
* [Grundregeln](#grundregeln)
* [2. Sitzposition & Dealer-Button](#2-sitzposition--dealer-button)
* [3. Blinds (Pflichteinsätze)](#3-blinds-pflichteinsätze)
* [4. Spielablauf im Detail](#4-spielablauf-im-detail)
* [4.1 Preflop (erste Setzrunde)](#41-preflop-erste-setzrunde)
* [4.2 Flop (3 Gemeinschaftskarten)](#42-flop-3-gemeinschaftskarten)
* [4.3 Turn (4. Gemeinschaftskarte)](#43-turn-4-gemeinschaftskarte)
* [4.4 River (5. Gemeinschaftskarte)](#44-river-5-gemeinschaftskarte)
* [5. Showdown (Kartenvergleich)](#5-showdown-kartenvergleich)
* [6. Poker-Handrangfolge](#6-poker-handrangfolge)
* [7. Wichtige Grundprinzipien](#7-wichtige-grundprinzipien)
* [Reihenfolge beachten](#reihenfolge-beachten)
* [Klare Aktionen](#klare-aktionen)
* [Ein Spieler eine Hand](#ein-spieler--eine-hand)
* [Fehlerhafte Einsätze](#fehlerhafte-einsätze)
* [8. Strategische Einordnung](#8-strategische-einordnung)
* [Start Game](#start-game)
* [Server starten](#server-starten)
* [Client starten](#client-starten)
* [UI](#ui)
* [Lobby UI](#lobby-ui)
* [Eine Lobby erstellen](#eine-lobby-erstellen)
* [Einer Lobby beitreten](#einer-lobby-beitreten)
* [Den Username ändern](#den-username-ändern)
* [Highscores](#highscores)
* [Game UI](#game-ui)
* [Themes](#themes)
* [Casono Browser](#casono-browser)
* [ChatUI](#chat ui)
* [Casono Rules](#casono-rules)
* [1. Spielübersicht](#1-spielübersicht)
* [Grundregeln](#grundregeln)
* [2. Sitzposition & Dealer-Button](#2-sitzposition--dealer-button)
* [3. Blinds (Pflichteinsätze)](#3-blinds-pflichteinsätze)
* [4. Spielablauf im Detail](#4-spielablauf-im-detail)
* [4.1 Preflop (erste Setzrunde)](#41-preflop-erste-setzrunde)
* [4.2 Flop (3 Gemeinschaftskarten)](#42-flop-3-gemeinschaftskarten)
* [4.3 Turn (4. Gemeinschaftskarte)](#43-turn-4-gemeinschaftskarte)
* [4.4 River (5. Gemeinschaftskarte)](#44-river-5-gemeinschaftskarte)
* [5. Showdown (Kartenvergleich)](#5-showdown-kartenvergleich)
* [6. Poker-Handrangfolge](#6-poker-handrangfolge)
* [7. Wichtige Grundprinzipien](#7-wichtige-grundprinzipien)
* [Reihenfolge beachten](#reihenfolge-beachten)
* [Klare Aktionen](#klare-aktionen)
* [Ein Spieler eine Hand](#ein-spieler--eine-hand)
* [Fehlerhafte Einsätze](#fehlerhafte-einsätze)
* [8. Strategische Einordnung](#8-strategische-einordnung)
* [Casono Rules Easy Description](#casono-rules-easy-description)
* [Blinds (Small Blind & Big Blind)](#blinds-small-blind--big-blind)
* [Erste Setzrunde (Preflop)](#erste-setzrunde-preflop)
* [Beispiel Preflop](#beispiel-preflop)
* [Flop (3 Gemeinschaftskarten)](#flop-3-gemeinschaftskarten)
* [Beispiel Flop](#beispiel-flop)
* [Turn (4. Karte)](#turn-4-karte)
* [River (5. Karte)](#river-5-karte)
* [Showdown (Gewinnentscheidung)](#showdown-gewinnentscheidung)
* [Poker Hand Rankings (Gewichtung)](#poker-hand-rankings-gewichtung)
* [Fazit](#fazit)
<!-- vim-markdown-toc -->
# Start Game
Es wird Java 25 benötigt
## Server starten
```bash
java -jar casono.jar server <listenport>
```
## Client starten
```bash
java -jar casono.jar client <serverip>:<serverport> [username]
```
Die Parameter serverip, serverport, listenport und username müssen ersetzt werden
# 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)
## Themes
![Set the theme](./images/game_ui_normal_theme.png)
### Black-and-White Theme
![Black and White Theme](./images/game_ui_black_white_theme.png)
### Glass-Effect Theme
![Glass Effect Theme](./images/game_ui_glass_theme.png)
## Casono Browser
![[casono-browser.png]]
![Casono Browser](./images/casono-browser.png)
## Chat UI
### Globaler Chat
![Globaler Chat](./images/global_chat.png)
### Lobby Chat
![Lobby Chat](./images/lobby_chat.png)
### Whisper Chat
![Whisper Chat](./images/whisper_chat.png)
# Casono Rules
## 1. Spielübersicht
Texas Holdem ist ein strategisches Kartenspiel für mehrere Spieler.
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
@@ -37,8 +136,8 @@ Ziel ist es, den Pot (alle gesetzten Chips) zu gewinnen, entweder durch:
- 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
- eigenen Karten
- und Gemeinschaftskarten
- Zu Spielbeginn erhält jeder Spieler ein Startgeld von 20000 Chips ($)
@@ -85,6 +184,7 @@ Jeder Spieler hat folgende Optionen:
- 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).
- Der erste Spieler bei der Flop Runde muss keinen höheren Einsatz setzen als der letzte Spieler aus der Preflop Runde, allerdings muss er seinen eigenen Einsatz aus der Preflop Runde überbieten.
Ab diesem Zeitpunkt können alle Spieler ihre Strategie anhand zusätzlicher Informationen anpassen.
@@ -151,7 +251,9 @@ Alle Aktionen müssen eindeutig sein:
### 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)
- 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
@@ -160,3 +262,169 @@ 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.svg)
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.svg)
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.svg)
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.svg)
Lars: 1. Paar:
![14. image](./images/14-1.svg)
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: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 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: 832 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 KiB

+555
View File
@@ -0,0 +1,555 @@
<!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: black;
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 = `
# Start Game
Es wird Java 25 benötigt
## Server starten
\`\`\`bash
java -jar casono.jar server <listenport>
\`\`\`
## Client starten
\`\`\`bash
java -jar casono.jar client <serverip>:<serverport> [username]
\`\`\`
Die Parameter serverip, serverport, listenport und username müssen ersetzt werden
# 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)
## Themes
![Set the theme](./images/game_ui_normal_theme.png)
### Black-and-White Theme
![Black and White Theme](./images/game_ui_black_white_theme.png)
### Glass-Effect Theme
![Glass Effect Theme](./images/game_ui_glass_theme.png)
## Casono Browser
![[casono-browser.png]]
![Casono Browser](./images/casono-browser.png)
## Chat UI
### Globaler Chat
![Globaler Chat](./images/global_chat.png)
### Lobby Chat
![Lobby Chat](./images/lobby_chat.png)
### Whisper Chat
![Whisper Chat](./images/whisper_chat.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).
- Der erste Spieler bei der Flop Runde muss keinen höheren Einsatz setzen als der letzte Spieler aus der Preflop Runde, allerdings muss er seinen eigenen Einsatz aus der Preflop Runde überbieten.
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.svg)
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.svg)
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.svg)
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.svg)
Lars: 1. Paar:
![14. image](./images/14-1.svg)
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>
@@ -102,6 +102,14 @@ This document describes the protocol for client-server communication in our appl
- [Required pre-execution checks](#required-pre-execution-checks)
- [Request Parameters](#request-parameters)
- [Success Response](#success-response)
- [LEAVE_LOBBY command](#leave_lobby-command)
- [Required pre-execution checks](#required-pre-execution-checks-1)
- [Request Parameters](#request-parameters-1)
- [Implementation notes](#implementation-notes)
- [Success Response](#success-response-1)
- [Error Response](#error-response-1)
- [Example Request](#example-request-1)
- [Example Response](#example-response-1)
- [START_GAME command](#start_game-command)
- [GET_LOBBY_STATUS command](#get_lobby_status-command)
- [Required pre-execution checks](#required-pre-execution-checks)
@@ -671,6 +679,56 @@ END
MSG=Lobby not found
END
## LEAVE_LOBBY command
The `LEAVE_LOBBY` command marks the currently logged-in user as absent in the specified lobby while a game is running. The lobby slot is kept reserved so the player can rejoin later.
### Required pre-execution checks
- [`UserLoggedInCheck`](#userloggedincheck)
### Request Parameters
| Parameter Name | Type | Optional | Description |
| :------------- | :--- | :------: | :---------- |
| `ID` | `int` | no | Numeric id of the target lobby |
### Implementation notes
- Parser: `LeaveLobbyParser` — reads the required `ID` parameter and builds `LeaveLobbyRequest`.
- Handler: `LeaveLobbyHandler` — resolves the lobby by id, verifies that a game is currently running, resolves the user from the session and marks the player as absent via `LobbyManager.leavePlayerFromLobby(...)`.
### Success Response
No additional response fields. The server replies with a simple `+OK` on success.
### Error Response
| Code | Description |
| :--- | :---------- |
| `MISSING_PARAMETER` | Required parameter `ID` missing (parser) |
| `USER_NOT_LOGGED_IN` | No user associated with the session (pre-execution check failed) |
| `LOBBY_NOT_FOUND` | The specified lobby id does not exist |
| `GAME_NOT_RUNNING` | The lobby does not currently have a running game |
| `NOT_IN_LOBBY` | The user is not actively present in the lobby |
### Example Request
```
LEAVE_LOBBY ID=1
```
### Example Response (success)
```
+OK
END
```
## START_GAME command
The `START_GAME` command requests the server to start a new game in the specified lobby. The
Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.
Binary file not shown.
+250
View File
@@ -0,0 +1,250 @@
\documentclass{article}
\setlength{\parindent}{0pt}
\usepackage[a4paper,margin=2.5cm]{geometry}
\usepackage{listings}
% External links
\usepackage{hyperref}
% Colored text
\usepackage{xcolor}
% Graphics
\usepackage{graphicx}
% Customized table
\usepackage{float}
\usepackage{array}
\newcolumntype{L}[1]{>{\raggedright\arraybackslash}p{#1}}
\usepackage{booktabs}
% Create plots
\usepackage{pgfplots}
\usepackage{pgf-pie}
\title{QS-Report für MS6}
\author{Syntax Syndicate - Casono}
\begin{document}
\maketitle
\section{Einleitung}
Dieses Dokument ist die Fortsetzung des QA-Konzeptes für unser Mehrspieler-Spiel \textit{Casono}.
Es wurde von uns als Teil der Programmierprojekt-Vorlesung (ehemals bekannt als cs108) an der Universität Basel von uns entwickelt.
Dieser Report folgt dem selben \textit{DIN ISO 9126}, und ist wie auch das Konzept für den ersten Teil in die selben Themenbereiche unterteilt:
\begin{itemize}
\item \textbf{Konstruktives Qualitätsmanagement} - Maßnahmen, die \emph{während} der Entwicklung ergriffen werden, um die Qualität von Anfang an sicherzustellen.
Einschließlich technischer Standards, Werkzeuge und internen Organisation.
\item \textbf{Analytisches Qualitätsmanagement} - Maßnahmen zur \emph{Kontrolle und Untersuchung} des bestehenden Produkts, durch Anwendung automatisierte Analysen, Metrik-Schwellenwerte und Unit-Tests.
\end{itemize}
\section{Konstruktives Qualitätsmanagement}
Ein konstruktives Qualitätsmanagement umfasst alle Maßnahmen, die proaktiv Mängel verhindern, indem gemeinsame Standards, Prozesse und Werkzeuge festgelegt werden, die jedes Teammitglied während der Entwicklung einhält.
\subsection{Technische Maßnahmen}
\subsubsection{JavaDoc}
JavaDoc wurde genutzt um alle Öffentlichen Klassen, Interfaces, Records und Methoden zu dokumentieren.
Als Teil der GitLab-Build-Pipeline wird JavaDoc auf Richtigkeit überprüft. Werden strukturelle Fehler wie ungültige Referenzen erkannt, schlägt die Pipeline fehl und der Merge wird verhindert.\\
Unsere implizite Form für JavaDoc-Kommentare ist wie folgt:
\begin{itemize}
\item Eine prägnante Zusammenfassung in einem Satz in der ersten Zeile
\item Zusätzliche Informationen oder Beispiele in weiteren Absätzen.
\item Dokumentation aller Parameter (\texttt{@param}), Rückgabewerte (\texttt{@return}) und Fehler (\texttt{@throws}).
\end{itemize}
Leider konnten wir das in MS4 vorgenommene Ziel, das auch Warnungen zu einem Abbruch der Pipeline führen, aus Technischen Gründen nicht umsetzen.
\newpage
\subsubsection{Logging}
Das Projekt verwendet Log4J~2 (\texttt{log4j-api} und \texttt{log4j-core} zur Laufzeit) zusammen mit Jansi für eine farbige Konsolenausgabe.
Das Format der Ausgabe ist durch eine Konfigurationsdatei vereinheitlicht.\\
Die Protokollstufen werden wie folgt einheitlich verwendet:
\begin{itemize}
\item \texttt{DEBUG} für interne Zustandsänderungen
\item \texttt{INFO} für Lebenszyklusereignisse wie Serverstart, Verbindungsaufbau/-trennung des Clients
\item \texttt{WARN} für behebbare Anomalien
\item \texttt{ERROR} für nicht behebbare Fehler
\end{itemize}
Im Gegensatz zu MS4 wurden die Namen der Logger gekürzt und nicht mehr der gesamte Classpath verwendet. Ein Beispiel für die Ausgabe sieht so aus:
\begin{figure}[H]
\centering
\includegraphics[width=\textwidth]{../../images/milestones/ms-6/qa-report/server_log.png}
\caption{Konsolenausgabe des Servers während dem Verbindungsaufbau und Anmeldung eines Nutzers}
\end{figure}
\subsection{Organisatorische Maßnahmen}
\subsubsection{Teamkultur und die \texttt{CONTRIBUTORS}-Datei}
Das Stammverzeichnis des GitLab-Repositories enthält eine Datei namens \texttt{CONTRIBUTORS.md}, die die seit MS4 unveränderten Teamkonventionen dokumentiert:
\begin{itemize}
\item Die Verwendung von Branches - Feature-Branches werden in \texttt{main} zusammengeführt
\item Merge-Richtlinie - CI muss bestanden werden. Eine Genehmigung durch Kollegen ist wünschenswert wenn Änderungen an dessen Code vorgenommen wurden.
\item Namenskonventionen für Branches und Commits
\item Code-Stil - Google-Java-Format, AOSP-Variante, Einrückung mit vier Leerzeichen
\item Jede Änderung beginnt mit einem Issue oder Task in GitLab, bevor mit der Umsetzung begonnen wird
\item Branch-Namen folgen dem Muster \texttt{<typ>/<kurzbeschreibung>} gemäß \href{https://conventional-branch.github.io/}{Conventional Branch} Standard
\item Commit-Messages folgen dem \href{https://www.conventionalcommits.org/en/v1.0.0/}{Conventional Commits} Standard
\item Code-Style wird durch Linter (Checkstyle) und Formatter (Spotless, Google/AOSP Java Style) automatisiert überprüft
\item Merge Anfragen müssen eine Beschreibung enthalten und auf das zugehörige Issue oder den Task verweisen
\item Zusammenarbeit und Kommunikation erfolgen bevorzugt über Issue-Kommentare, nicht über private Nachrichten
\end{itemize}
\subsubsection{GitLab-Task- und Issue-Vorlagen für Fortschrittsverfolgung}
GitLab-Vorlagen für Tasks und Issues werden für alle geplanten Arbeitselemente verwendet. Die Aufgabenvorlage sorgt für eine einheitliche Struktur, die die Fortschrittsverfolgung erleichtert und das Risiko vager oder unvollständiger Arbeitselemente reduziert.
Durch die Vergabe von Labels kann Tasks und Issues weiterer Kontext gegeben werden.
\newpage
\section{Analytisches Qualitätsmanagement}
\subsection{Analytische Verfahren}
\subsubsection{Eigentumsrechte an Programmcode über GitLab \texttt{CODEOWNERS}}
In unserem Konzept haben wir uns vorgenommen, durch eine \texttt{CODEOWNERS} Datei den Teammitgliedern die Eigentumsrechte ihres Programmcodes zuzusprechen. Leider hat sich bestätigt das die von SciCORE betriebene Instanz nicht die Premium-Stufe hat, wodurch diese Funktion für uns nicht zur Verfügung steht. \\
Statt auf eine Automatisierte Grundlage zu vertrauen, haben wir uns für den nächstbesten Weg der Disziplinarität entschieden, wie am Beispiel von \href{https://git.scicore.unibas.ch/cs108-fs26/Gruppe-13/-/merge_requests/166}{dieser Merge-Anfrage} gesehen werden kann.
\subsubsection{Automatisiertes CI-Linting und Build-Verifizierung}
Jeder Push und jede Merge-Anfrage löst die CI-Pipeline aus.
Für Pushes und Merge-Anfragen setzt sich die Pipeline aus folgenden Phasen zusammen:
\begin{enumerate}
\item \textbf{Linting-Phase} \textcolor{blue}{[Push, MR]} - Checkstyle und Spotless überprüfen, ob der Programmcode dem vereinbarten Stil entspricht.
\item \textbf{Build-Phase} \textcolor{blue}{[Push, MR]} - \texttt{./gradlew assemble} prüft, ob das gesamte Projekt fehlerfrei kompiliert werden kann.
\item \textbf{Checkstyle-Report} \textcolor{blue}{[MR]} - Ein zusätzlicher Checkstyle-Report wird für Merge-Anfragen erzeugt und als Code-Quality-Report bereitgestellt.
\item \textbf{Javadoc-Prüfung} \textcolor{blue}{[MR]} - JavaDoc wird auf Korrektheit geprüft.
\item \textbf{Test-Phase} \textcolor{blue}{[Push, MR]} - Automatisierte Tests werden ausgeführt und ein Testreport erzeugt.
\end{enumerate}
Die Entscheidung das fehlgeschlagene Jobs keinen Push aber dafür eine Merge-Anfrage verhindern können, hat sich als vernünftig bewehrt.
Über die Dauer des Projektes hat diese Pipeline mehrmals verhindert das Fehlerhafter oder sogar kaputter Programmcode in den main Branch gelangen konnte.
\newpage
\subsection{Testverfahren}
Unit-Tests wurden mit \textit{JUnit 5} geschrieben und in der Testphase der CI-Pipeline ausgeführt. Ein fehlgeschlagener Test blockiert die Merge-Anfrage.
Als weiteres Werkzeug verwendeten wir \textit{JaCoCo} für die Ermittlung der Testabdeckung. \\
Die Testabdeckung für Meilenstein sechs ist wie folgt:
\begin{table}[H]
\centering
\renewcommand{\arraystretch}{1.4}
\begin{tabular}{L{7.5cm} r r}
\textbf{Paketname (gekürzt)} & \textbf{Instr.\ Cov.} & \textbf{Branch Cov.} \\
\midrule
\texttt{client.chat} & 30\,\% & 16\,\% \\
\texttt{client.game} & 15\,\% & 2\,\% \\
\texttt{client.network} & 2\,\% & 1\,\% \\
\texttt{client.ui} (von Abdeckung ausgeschlossen) & 0\,\% & 0\,\% \\
\texttt{server.app.checks} & 0\,\% & 0\,\% \\
\texttt{server.app.commands} & 0\,\% & 0\,\% \\
\texttt{server.domain.game} & 68\,\% & 43\,\% \\
\texttt{server.domain.*} & 59\,\% & 44\,\% \\
\texttt{server.network.transport} & 0\,\% & 0\,\% \\
\texttt{server.network.protocol} & 67\,\% & 55\,\% \\
\texttt{server.network.*} & 67\,\% & 55\,\% \\
\midrule
\textbf{Summe} & \textbf{18\,\%} & \textbf{11\,\%} \\
\bottomrule
\end{tabular}
\end{table}
Im vierten Meilenstein haben wir uns das Ziel gesetzt die Testabdeckung für die Game-Engine und wichtige Netzwerkkomponenten auf mindestens 50\% zu erhöhen, dieses Ziel haben wir laut JaCoCo nicht erreicht.\\
Geht man jedoch nach der Abdeckung der "Test Runner for Java" Erweiterung in VSCode, dann beträgt die Abdeckung 51\%. Sowohl Julian (Game-Engine) als auch Lars (Serverseitiges Netzwerk) haben sich nach diesen Angaben gereichtet, weshalb wir dachten wir hätten unser Ziel erreicht.
\begin{figure}[H]
\centering
\includegraphics[width=0.4\textwidth]{../../images/milestones/ms-6/qa-report/vscode_code_coverage.png}
\caption{Abweichende Angaben bezüglich der Abdeckung von VSCode zu JaCoCo}
\end{figure}
Die Verwendung zwei unterschiedlicher Tools zur Beurteilung des Fortschritts ist definitiv etwas, was es in Zukunft zu vermeiden gilt.
\newpage
\section{Metriken}
\textit{Die nachfolgenden Metriken wurden am 17. Mai 2026 um 20:58 Uhr erhoben, neuere Änderungen sind daher nicht berücksichtigt.}
\subsection{Lines of Code}
\begin{figure}[H]
\centering
\begin{tikzpicture}
\begin{axis}[
width=\textwidth,
height=0.45\textwidth,
xlabel={Meilenstein},
ylabel={Zeilen},
ymin=0,
ymax=31000,
xtick={1,2,3},
xticklabels={MS4,MS5,MS6},
legend pos=north west,
grid=both,
scaled y ticks=false,
yticklabel style={/pgf/number format/fixed}
]
% Actual Java code
\addplot[
color=blue,
mark=o,
thick
]
coordinates {
(1,9760)
(2,12972)
(3,15319)
};
\addlegendentry{Java code (actual)}
% Total Java
\addplot[
color=red,
mark=square*,
thick
]
coordinates {
(1,16396)
(2,21203)
(3,24319)
};
\addlegendentry{Java total}
% Total in src
\addplot[
color=green!60!black,
mark=diamond*,
thick
]
coordinates {
(1,18012)
(2,22872)
(3,29639)
};
\addlegendentry{Total in src}
\end{axis}
\end{tikzpicture}
\caption{Entwicklung der Java-Zeilen (tatsächlicher Code vs. Gesamt) über die Meilensteine hinweg}
\end{figure}
\subsection{Verteilung der Code-Zeilen auf verschiedene Dateitypen}
\begin{figure}[H]
\centering
\begin{tikzpicture}
\pie[
text=legend,
radius=4
]{
62.26/Java,
17.19/PostCSS,
14.90/Markdown,
2.87/XML,
2.79/HTML
}
\end{tikzpicture}
\caption{Verteilung der Code-Zeilen (Top 5 Dateitypen)}
\label{fig:loc-pie}
\end{figure}
\end{document}
Binary file not shown.
+10 -6
View File
@@ -1,8 +1,12 @@
game.name = Example Game
game.command = java -jar exampleGame.jar
game.name = Casono
# Default client command (example)
game.command = java -jar casono.jar client <serverip>:<serverport> [username]
# Explicit client command property
game.client.command = java -jar casono.jar client <serverip>:<serverport> [username]
# Server command example
game.server.command = java -jar casono.jar server <listenport>
game.description.file = index.html
game.screenshot.file = example.png
game.server.command = java -jar exampleGame.jar
game.screenshot.file = screenshot.png
game.use = true
game.year = 2020
game.developers = Tom Cat, Jerry Mouse, William Hanna, Joseph Barbera
game.year = 2026
game.developers = Lars Winzer, Jona Walpert, Matthis Müller, Julian Gerber
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 450" width="800" height="450">
<rect width="800" height="450" rx="20" fill="#07101a" />
<ellipse cx="400" cy="240" rx="300" ry="120" fill="#0b5a2b" />
<g fill="#d4b26a" font-family="Cinzel, serif" font-size="32" text-anchor="middle">
<text x="400" y="60">Casono Table Preview</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 363 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="240" viewBox="0 0 240 240">
<rect width="240" height="240" rx="20" fill="#141212" />
<circle cx="120" cy="84" r="44" fill="#ffffff" />
<text x="120" y="150" font-family="Lato, sans-serif" font-size="18" fill="#fff" text-anchor="middle">Jona</text>
</svg>

After

Width:  |  Height:  |  Size: 321 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="240" viewBox="0 0 240 240">
<rect width="240" height="240" rx="20" fill="#141212" />
<circle cx="120" cy="84" r="44" fill="#ffffff" />
<text x="120" y="150" font-family="Lato, sans-serif" font-size="18" fill="#fff" text-anchor="middle">Julian</text>
</svg>

After

Width:  |  Height:  |  Size: 323 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="240" viewBox="0 0 240 240">
<rect width="240" height="240" rx="20" fill="#141212" />
<circle cx="120" cy="84" r="44" fill="#ffffff" />
<text x="120" y="150" font-family="Lato, sans-serif" font-size="18" fill="#fff" text-anchor="middle">Lars</text>
</svg>

After

Width:  |  Height:  |  Size: 321 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="240" viewBox="0 0 240 240">
<rect width="240" height="240" rx="20" fill="#141212" />
<circle cx="120" cy="84" r="44" fill="#ffffff" />
<text x="120" y="150" font-family="Lato, sans-serif" font-size="18" fill="#fff" text-anchor="middle">Matthis</text>
</svg>

After

Width:  |  Height:  |  Size: 324 B

+339 -42
View File
@@ -1,53 +1,350 @@
<html>
<!-- Dies ist ein html Kommentar -->
<!-- Ändert in diesem File sämtliche Platzhalter, wie z.B. den Spielnamen in der nächsten Linie -->
<head><title>Example Game</title><link rel="stylesheet" href="main.css"></head>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Casono</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="images/logo.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700;900&family=Lato:wght@300;400;700&display=swap"
rel="stylesheet">
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
</head>
<body>
<!-- Header nicht verändern -->
<header class="site-header"><div class="wrapper"><a class="site-title" href="/cs108/">Programmierprojekt - Computerspiele</a></div></header><div class="page-content"><div class="wrapper"><div class="home">
<noscript>
<div style="
padding: 40px;
font-family: Arial, sans-serif;
background-color: #0d9e3b;
color: #ffffff;
text-align: center;
">
<h2 style="
color: #ffffff;
text-shadow: 2px 2px 0 #000;
">JavaScript is disabled</h2>
<h1>Example Game</h1>
Hier k&ouml;nnt ihr einen Satz &uuml;ber euer Spiel sagen.
<p>
The Casono Browser requires JavaScript for the <br>
Casono Markdown Render Engine to display content correctly.<br><br>
<!-- Dieser Block ist dafür verantwortlich, den Trailer / das Gameplay Video und euren Screenshot einzubauen. Verändert ihn nicht, sondern überschreibt einfach die Dateien -->
<br>
<img width="250" src="screenshot.png">
<br>
<br>
<h2>Trailer</h2>
<video controls width="250">
<source src="video.mp4"
type="video/mp4">
Sorry, your browser doesn't support embedded videos.
</video>
<br>
<br>
For security, JavaScript is disabled by default in the Casono Browser and should only be enabled when needed.
</p>
<!-- Entwickler anpassen -->
<p>
Please enable JavaScript in your Browser settings and reload the page.
</p>
<h3>Entwickler:</h3>
<ul>
<li> Edgar Codd </li>
<li> Lynn Conway </li>
<li> Ada Lovelace </li>
<li> Alan Turing </li>
</ul>
<img src="images/activate-js.png" alt="JavaScript aktivieren Anleitung" style="max-width: 100%; margin-top: 20px; border-radius: 10px;">
</div>
</noscript>
<!-- Nachfolgend sollt ihr die Tasten angeben, welche man braucht um euer Spiel zu Spielen. Um weitere Tasten hinzuzufügen könnt Ihr einfach eine Zeile kopieren und den Buchstaben oder das Wort zwischen <i>Beispiel:</i> ändern. -->
<header class="sticky-nav">
<nav>
<div class="logo" data-aos="fade-down"><img src="images/logo.png" alt="Casono" class="site-logo"></div>
<ul id="nav-links" data-aos="fade-down" data-aos-delay="100">
<li><a href="#hero" data-i18n="navHome">Home</a></li>
<li><a href="#about" data-i18n="navAbout">Über</a></li>
<li><a href="#features" data-i18n="navFeatures">Features</a></li>
<li><a href="#table" data-i18n="navTable">Tisch</a></li>
<li><a href="#media" data-i18n="navMedia">Trailer & Gameplay</a></li>
<li><a href="#resources" data-i18n="navDownload">Download</a></li>
<li><a href="#team" data-i18n="navTeam">Team</a></li>
</ul>
<button id="language-toggle" class="language-toggle" type="button" aria-label="Switch language">EN</button>
<button class="burger-menu" id="burger-menu" aria-label="Toggle navigation menu">
<span></span>
<span></span>
<span></span>
</button>
</nav>
</header>
<h2>Steuerung:</h2>
<i>Pfeiltasten:</i> bewegen<br/>
<i>Leertaste:</i> springen<br/>
<i>I:</i> Inventar<br/>
<i>Esc:</i> Spiel beenden<br/>
<i>P:</i> Spiel pausieren
<main>
<section id="hero" class="hero-section">
<div class="hero-content">
<h1 data-aos="zoom-in">Casono</h1>
<p class="tagline" data-i18n="heroTagline" data-aos="fade-up" data-aos-delay="300">Erlebe den
Nervenkitzel von Texas Hold'em
Poker</p>
<p class="description" data-i18n="heroDescription" data-aos="fade-up" data-aos-delay="500">Casono ist
ein stilvolles Texas Hold'em
Poker-Erlebnis mit spannenden Matches und eleganter Optik.</p>
<a href="#features" class="cta-button" data-i18n="heroCta" data-aos="zoom-in-up"
data-aos-delay="700">Mehr erfahren</a>
</div>
<div class="hero-overlay"></div>
</section>
<!-- Falls ihr noch zusätzliche Informationen wie Items oder andere Erklärungen angeben wollt, könnt Ihr das wie nachfolgend machen. -->
<section id="about" class="content-section">
<h2 data-i18n="aboutTitle" data-aos="fade-up">Über Casono</h2>
<p data-i18n="aboutText" data-aos="fade-up" data-aos-delay="100">Casono kombiniert echtes Casino-Feeling mit
leicht zugänglichem
Gameplay. Spiele online gegen Freunde.</p>
<!-- <div class="about-video-container" data-aos="zoom-in" data-aos-delay="200">
<video class="about-video" controls playsinline preload="metadata" loading="lazy">
<source src="video.mp4" type="video/mp4">
<span data-i18n="videoFallback">Ihr Browser unterstützt das Video-Tag nicht.</span>
</video>
</div> -->
</section>
<h2>Inventar:</h2>
<i>Eisblock:</i> Gegner k&ouml;nnen sich nicht mehr bewegen<br/>
<i>Schwarzes Loch:</i> Steuerung der Gegner wird invertiert
<section id="features" class="features-section">
<h2 data-i18n="featuresTitle" data-aos="fade-up">Key Features</h2>
<div class="features-grid">
<div class="feature-card" data-accent="1" data-aos="fade-up" data-aos-delay="100">
<h3 data-i18n="feature1Title">Sitzungen & Matchmaking</h3>
<p data-i18n="feature1Text">Realistisches Poker-Erlebnis mit spannenden Matches.</p>
</div>
</div></div></div></div></body>
</html>
<div class="feature-card" data-accent="2" data-aos="fade-up" data-aos-delay="300">
<h3 data-i18n="feature2Title">Stilvolle UI</h3>
<p data-i18n="feature2Text">Intuitive Benutzeroberfläche für ein immersives Erlebnis.
</p>
</div>
<div class="feature-card" data-accent="3" data-aos="fade-up" data-aos-delay="400">
<h3 data-i18n="feature3Title">Einfache Installation</h3>
<p data-i18n="feature3Text">Download, entpacken, starten, fertig!</p>
</div>
</div>
</section>
<section id="table" class="content-section table-section">
<h2 data-i18n="tableTitle" data-aos="fade-up">Der Tisch</h2>
<p data-i18n="tableText" data-aos="fade-up" data-aos-delay="100"> Spielen und chatten in einer gemeinsamen
eleganten Ansicht.</p>
<div class="table-preview-container" data-aos="zoom-in" data-aos-delay="200">
<img src="images/table_preview.png" alt="Casino Tisch Vorschau" class="table-preview-image"
loading="lazy">
</div>
</section>
<section id="media" class="media-section">
<h2 data-i18n="mediaTitle" data-aos="fade-up">Trailer & Gameplay</h2>
<p data-i18n="mediaText" data-aos="fade-up" data-aos-delay="100">Erhalte einen schnellen Eindruck vom Stil
und vom Spielablauf.
</p>
<div class="media-grid" data-aos="fade-up" data-aos-delay="200">
<article class="media-card">
<h3 data-i18n="mediaTrailer">Trailer</h3>
<video controls playsinline preload="metadata" loading="lazy">
<source src="casono-trailer.mp4" type="video/mp4">
<span data-i18n="videoFallback">Ihr Browser unterstützt das Video-Tag nicht.</span>
</video>
</article>
<article class="media-card">
<h3 data-i18n="mediaGameplay">Gameplay</h3>
<video controls playsinline preload="metadata" loading="lazy">
<source src="gameplay.mp4" type="video/mp4">
<span data-i18n="videoFallback">Ihr Browser unterstützt das Video-Tag nicht.</span>
</video>
</article>
</div>
</section>
<section id="resources" class="resources-section">
<h2 data-i18n="resourcesTitle" data-aos="zoom-in-up">Download & Ressourcen</h2>
<div class="download-section" data-aos="zoom-in-up" data-aos-delay="100">
<a href="https://git.scicore.unibas.ch/cs108-fs26/Gruppe-13/-/archive/main/Gruppe-13-main.zip"
class="download-button" data-i18n="downloadButton" target="_blank"
rel="noopener noreferrer">Download Game
(ZIP)</a>
</div>
<div class="run-instructions" data-aos="fade-up" data-aos-delay="200">
<div class="code-window" role="region" aria-label="Run instructions">
<code>
# Requires Java 25
# Start the Server:
java -jar casono.jar server &lt;listenport&gt;
# Start the Client:
java -jar casono.jar client &lt;serverip&gt;:&lt;serverport&gt; [username]
# Replace &lt;listenport&gt;, &lt;serverip&gt;, &lt;serverport&gt; and [username] with your values.
</code>
</div>
</div>
</section>
<section id="team" class="team-section">
<h2 data-i18n="teamTitle" data-aos="fade-up">Das Team</h2>
<div class="team-grid">
<a class="team-member team-member-link" href="https://github.com/official-Cromatin" target="_blank"
rel="noopener noreferrer" data-aos="fade-up" data-aos-delay="100">
<img src="https://avatars.githubusercontent.com/u/92319467?v=4" alt="Lars Winzer" class="team-photo"
loading="lazy">
<h3>Lars Winzer</h3>
<p>Server architecture & Server side networking</p>
</a>
<a class="team-member team-member-link" href="https://jona.walpert.ch/" target="_blank"
rel="noopener noreferrer" data-aos="fade-up" data-aos-delay="200">
<img src="https://avatars.githubusercontent.com/u/98217482?v=4" alt="Jona Walpert"
class="team-photo" loading="lazy">
<h3>Jona Walpert</h3>
<p>UI & UX Design</p>
</a>
<a class="team-member team-member-link" href="https://github.com/ginkelmath" target="_blank"
rel="noopener noreferrer" data-aos="fade-up" data-aos-delay="300">
<img src="https://avatars.githubusercontent.com/u/160567160?v=4" alt="Matthis Ginkel"
class="team-photo" loading="lazy">
<h3>Matthis Ginkel</h3>
<p>Client side development</p>
</a>
<a class="team-member team-member-link" href="https://github.com/17jk789" target="_blank"
rel="noopener noreferrer" data-aos="fade-up" data-aos-delay="400">
<img src="https://avatars.githubusercontent.com/u/189449499?v=4" alt="Julian Kropff"
class="team-photo" loading="lazy">
<h3>Julian Kropff</h3>
<p>UI & UX Design, Game Engine</p>
</a>
</div>
<div class="github-link" data-aos="fade-up" data-aos-delay="500">
<a href="https://git.scicore.unibas.ch/cs108-fs26/Gruppe-13">
<i class="fa-brands fa-github github-icon"></i>
<span data-i18n="repoLink">Projekt-Repo</span>
</a>
</div>
</section>
</main>
<div id="trailer-modal" class="modal">
<div class="modal-content">
<span class="close-button" id="close-trailer-modal">&times;</span>
<video id="trailer-video" width="100%" controls autoplay muted loop playsinline>
<source src="Casono-Trailer.webm" type="video/webm">
</video>
</div>
</div>
<div id="demo-modal" class="modal">
<div class="modal-content">
<span class="close-button" id="close-demo-modal">&times;</span>
<video id="demo-video" width="100%" controls autoplay muted loop playsinline>
<source src="gameplay.mp4" type="video/mp4">
</video>
</div>
</div>
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
<script>
AOS.init({ duration: 800, once: true });
const translations = {
de: {
navHome: 'Home',
navAbout: 'Über',
navFeatures: 'Features',
navTable: 'Tisch',
navMedia: 'Trailer & Gameplay',
navDownload: 'Download',
navTeam: 'Team',
heroTagline: "Erlebe den Nervenkitzel von Texas Hold'em Poker",
heroDescription: "Casono ist ein stilvolles Texas Hold'em Poker-Erlebnis mit spannenden Matches und eleganter Optik.",
heroCta: 'Mehr erfahren',
aboutTitle: 'Über Casono',
aboutText: 'Casono kombiniert echtes Casino-Feeling mit leicht zugänglichem Gameplay. Spiele online gegen deine Freunde.',
featuresTitle: 'Key Features',
feature1Title: 'Sitzungen & Matchmaking',
feature1Text: 'Realistisches Poker-Erlebnis mit spannenden Matches.',
feature2Title: 'Stilvolle UI',
feature2Text: 'Intuitive Benutzeroberfläche für ein immersives Erlebnis.',
feature3Title: 'Einfache Installation',
feature3Text: 'Download, entpacken, starten, fertig!',
tableTitle: 'Der Tisch',
tableText: 'Spielen und chatten in einer gemeinsamen eleganten Ansicht.',
mediaTitle: 'Trailer & Gameplay',
mediaText: 'Erhalte einen Eindruck vom Stil und vom Spielablauf.',
mediaTrailer: 'Trailer',
mediaGameplay: 'Gameplay',
resourcesTitle: 'Download & Ressourcen',
downloadButton: 'Download Game (ZIP)',
teamTitle: 'Das Team',
repoLink: 'Projekt-Repo',
videoFallback: 'Ihr Browser unterstützt das Video-Tag nicht.'
},
en: {
navHome: 'Home',
navAbout: 'About',
navFeatures: 'Features',
navTable: 'Table',
navMedia: 'Trailer & Gameplay',
navDownload: 'Download',
navTeam: 'Team',
heroTagline: "Feel the thrill of Texas Hold'em Poker",
heroDescription: "Casono is a stylish Texas Hold'em poker experience with exciting matches and elegant visuals.",
heroCta: 'Learn more',
aboutTitle: 'About Casono',
aboutText: 'Casono combines real casino flair with approachable gameplay. Play online with your friends.',
featuresTitle: 'Key Features',
feature1Title: 'Sessions & Matchmaking',
feature1Text: 'Realistic poker experience with thrilling matches.',
feature2Title: 'Stylish UI',
feature2Text: 'Intuitive interface for an immersive experience.',
feature3Title: 'Easy Setup',
feature3Text: 'Download, unpack, launch, done!',
tableTitle: 'The Table',
tableText: 'Play and chat in one shared elegant view.',
mediaTitle: 'Trailer & Gameplay',
mediaText: 'Get a quick look at the style and core gameplay loop.',
mediaTrailer: 'Trailer',
mediaGameplay: 'Gameplay',
resourcesTitle: 'Download & Resources',
downloadButton: 'Download Game (ZIP)',
teamTitle: 'The Team',
repoLink: 'Project Repo',
videoFallback: 'Your browser does not support the video tag.'
}
};
const languageToggle = document.getElementById('language-toggle');
const i18nNodes = document.querySelectorAll('[data-i18n]');
function applyLanguage(language) {
const dictionary = translations[language] ?? translations.de;
i18nNodes.forEach((node) => {
const key = node.getAttribute('data-i18n');
if (!key || !dictionary[key]) {
return;
}
node.textContent = dictionary[key];
});
document.documentElement.lang = language;
languageToggle.textContent = language === 'de' ? 'EN' : 'DE';
localStorage.setItem('casono-language', language);
}
const preferredLanguage = localStorage.getItem('casono-language') === 'en' ? 'en' : 'de';
applyLanguage(preferredLanguage);
languageToggle?.addEventListener('click', () => {
const nextLanguage = document.documentElement.lang === 'de' ? 'en' : 'de';
applyLanguage(nextLanguage);
});
document.getElementById('burger-menu')?.addEventListener('click', function () {
const nav = document.querySelector('nav ul');
nav.classList.toggle('nav-active');
this.classList.toggle('active');
});
// modal open/close handlers
document.getElementById('close-trailer-modal')?.addEventListener('click', () => {
document.getElementById('trailer-modal').classList.remove('show');
});
document.getElementById('close-demo-modal')?.addEventListener('click', () => {
document.getElementById('demo-modal').classList.remove('show');
});
</script>
</body>
</html>
-667
View File
@@ -1,667 +0,0 @@
/**
* Reset some basic elements
*/
body, h1, h2, h3, h4, h5, h6,
p, blockquote, pre, hr,
dl, dd, ol, ul, figure {
margin: 0;
padding: 0;
}
/**
* Basic styling
*/
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 1.5;
font-weight: 300;
color: #111;
background-color: #fdfdfd;
-webkit-text-size-adjust: 100%;
}
/**
* Set `margin-bottom` to maintain vertical rhythm
*/
h1, h2, h3, h4, h5, h6,
p, blockquote, pre,
ul, ol, dl, figure,
.highlight {
margin-bottom: 15px;
}
/**
* Images
*/
img {
max-width: 100%;
vertical-align: middle;
}
/**
* Figures
*/
figure > img {
display: block;
}
figcaption {
font-size: 14px;
}
/**
* Lists
*/
ul, ol {
margin-left: 30px;
}
li > ul,
li > ol {
margin-bottom: 0;
}
/**
* Headings
*/
h1, h2, h3, h4, h5, h6 {
font-weight: 300;
}
/**
* Links
*/
a {
color: #2a7ae2;
text-decoration: none;
}
a:visited {
color: #1756a9;
}
a:hover {
color: #111;
text-decoration: underline;
}
/**
* Blockquotes
*/
blockquote {
color: #828282;
border-left: 4px solid #e8e8e8;
padding-left: 15px;
font-size: 18px;
letter-spacing: -1px;
font-style: italic;
}
blockquote > :last-child {
margin-bottom: 0;
}
/**
* Code formatting
*/
pre,
code {
font-size: 15px;
border: 1px solid #e8e8e8;
border-radius: 3px;
background-color: #eef;
}
code {
padding: 1px 5px;
}
pre {
padding: 8px 12px;
overflow-x: scroll;
}
pre > code {
border: 0;
padding-right: 0;
padding-left: 0;
}
/**
* Wrapper
*/
.wrapper {
max-width: -webkit-calc(800px - (30px * 2));
max-width: calc(800px - (30px * 2));
margin-right: auto;
margin-left: auto;
padding-right: 30px;
padding-left: 30px;
}
@media screen and (max-width: 800px) {
.wrapper {
max-width: -webkit-calc(800px - (30px));
max-width: calc(800px - (30px));
padding-right: 15px;
padding-left: 15px;
}
}
/**
* Clearfix
*/
.wrapper:after, .footer-col-wrapper:after {
content: "";
display: table;
clear: both;
}
/**
* Icons
*/
.icon > svg {
display: inline-block;
width: 16px;
height: 16px;
vertical-align: middle;
}
.icon > svg path {
fill: #828282;
}
/**
* Site header
*/
.site-header {
border-top: 5px solid #424242;
border-bottom: 1px solid #e8e8e8;
min-height: 56px;
position: relative;
}
.site-title {
font-size: 26px;
line-height: 56px;
letter-spacing: -1px;
margin-bottom: 0;
float: left;
}
.site-title, .site-title:visited {
color: #424242;
}
.site-nav {
float: right;
line-height: 56px;
}
.site-nav .menu-icon {
display: none;
}
.site-nav .page-link {
color: #111;
line-height: 1.5;
}
.site-nav .page-link:not(:first-child) {
margin-left: 20px;
}
@media screen and (max-width: 600px) {
.site-nav {
position: absolute;
top: 9px;
right: 30px;
background-color: #fdfdfd;
border: 1px solid #e8e8e8;
border-radius: 5px;
text-align: right;
}
.site-nav .menu-icon {
display: block;
float: right;
width: 36px;
height: 26px;
line-height: 0;
padding-top: 10px;
text-align: center;
}
.site-nav .menu-icon > svg {
width: 18px;
height: 15px;
}
.site-nav .menu-icon > svg path {
fill: #424242;
}
.site-nav .trigger {
clear: both;
display: none;
}
.site-nav:hover .trigger {
display: block;
padding-bottom: 5px;
}
.site-nav .page-link {
display: block;
padding: 5px 10px;
}
}
/**
* Site footer
*/
.site-footer {
border-top: 1px solid #e8e8e8;
padding: 30px 0;
}
.footer-heading {
font-size: 18px;
margin-bottom: 15px;
}
.contact-list,
.social-media-list {
list-style: none;
margin-left: 0;
}
.footer-col-wrapper {
font-size: 15px;
color: #828282;
margin-left: -15px;
}
.footer-col {
float: left;
margin-bottom: 15px;
padding-left: 15px;
}
.footer-col-1 {
width: -webkit-calc(35% - (30px / 2));
width: calc(35% - (30px / 2));
}
.footer-col-2 {
width: -webkit-calc(20% - (30px / 2));
width: calc(20% - (30px / 2));
}
.footer-col-3 {
width: -webkit-calc(45% - (30px / 2));
width: calc(45% - (30px / 2));
}
@media screen and (max-width: 800px) {
.footer-col-1,
.footer-col-2 {
width: -webkit-calc(50% - (30px / 2));
width: calc(50% - (30px / 2));
}
.footer-col-3 {
width: -webkit-calc(100% - (30px / 2));
width: calc(100% - (30px / 2));
}
}
@media screen and (max-width: 600px) {
.footer-col {
float: none;
width: -webkit-calc(100% - (30px / 2));
width: calc(100% - (30px / 2));
}
}
/**
* Page content
*/
.page-content {
padding: 30px 0;
}
.page-heading {
font-size: 20px;
}
.post-list {
margin-left: 0;
list-style: none;
}
.post-list > li {
margin-bottom: 30px;
}
.post-meta {
font-size: 14px;
color: #828282;
}
.post-link {
display: block;
font-size: 24px;
}
/**
* Posts
*/
.post-header {
margin-bottom: 30px;
}
.post-title {
font-size: 42px;
letter-spacing: -1px;
line-height: 1;
}
@media screen and (max-width: 800px) {
.post-title {
font-size: 36px;
}
}
.post-content {
margin-bottom: 30px;
}
.post-content h2 {
font-size: 32px;
}
@media screen and (max-width: 800px) {
.post-content h2 {
font-size: 28px;
}
}
.post-content h3 {
font-size: 26px;
}
@media screen and (max-width: 800px) {
.post-content h3 {
font-size: 22px;
}
}
.post-content h4 {
font-size: 20px;
}
@media screen and (max-width: 800px) {
.post-content h4 {
font-size: 18px;
}
}
/**
* Syntax highlighting styles
*/
.highlight {
background: #fff;
}
.highlight .c {
color: #998;
font-style: italic;
}
.highlight .err {
color: #a61717;
background-color: #e3d2d2;
}
.highlight .k {
font-weight: bold;
}
.highlight .o {
font-weight: bold;
}
.highlight .cm {
color: #998;
font-style: italic;
}
.highlight .cp {
color: #999;
font-weight: bold;
}
.highlight .c1 {
color: #998;
font-style: italic;
}
.highlight .cs {
color: #999;
font-weight: bold;
font-style: italic;
}
.highlight .gd {
color: #000;
background-color: #fdd;
}
.highlight .gd .x {
color: #000;
background-color: #faa;
}
.highlight .ge {
font-style: italic;
}
.highlight .gr {
color: #a00;
}
.highlight .gh {
color: #999;
}
.highlight .gi {
color: #000;
background-color: #dfd;
}
.highlight .gi .x {
color: #000;
background-color: #afa;
}
.highlight .go {
color: #888;
}
.highlight .gp {
color: #555;
}
.highlight .gs {
font-weight: bold;
}
.highlight .gu {
color: #aaa;
}
.highlight .gt {
color: #a00;
}
.highlight .kc {
font-weight: bold;
}
.highlight .kd {
font-weight: bold;
}
.highlight .kp {
font-weight: bold;
}
.highlight .kr {
font-weight: bold;
}
.highlight .kt {
color: #458;
font-weight: bold;
}
.highlight .m {
color: #099;
}
.highlight .s {
color: #d14;
}
.highlight .na {
color: #008080;
}
.highlight .nb {
color: #0086B3;
}
.highlight .nc {
color: #458;
font-weight: bold;
}
.highlight .no {
color: #008080;
}
.highlight .ni {
color: #800080;
}
.highlight .ne {
color: #900;
font-weight: bold;
}
.highlight .nf {
color: #900;
font-weight: bold;
}
.highlight .nn {
color: #555;
}
.highlight .nt {
color: #000080;
}
.highlight .nv {
color: #008080;
}
.highlight .ow {
font-weight: bold;
}
.highlight .w {
color: #bbb;
}
.highlight .mf {
color: #099;
}
.highlight .mh {
color: #099;
}
.highlight .mi {
color: #099;
}
.highlight .mo {
color: #099;
}
.highlight .sb {
color: #d14;
}
.highlight .sc {
color: #d14;
}
.highlight .sd {
color: #d14;
}
.highlight .s2 {
color: #d14;
}
.highlight .se {
color: #d14;
}
.highlight .sh {
color: #d14;
}
.highlight .si {
color: #d14;
}
.highlight .sx {
color: #d14;
}
.highlight .sr {
color: #009926;
}
.highlight .s1 {
color: #d14;
}
.highlight .ss {
color: #990073;
}
.highlight .bp {
color: #999;
}
.highlight .vc {
color: #008080;
}
.highlight .vg {
color: #008080;
}
.highlight .vi {
color: #008080;
}
.highlight .il {
color: #099;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

+728
View File
@@ -0,0 +1,728 @@
/* Basic Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--background-color: #f6efe4;
/* page background */
--card-bg-color: #f6efe4;
--nav-bg-color: rgba(69, 2, 6, 0.96);
--primary-color: #0b0b0b;
/* main text on light background */
--secondary-color: #6b6b6b;
--accent-color: #a88130;
/* accent */
--button-color: #a40404;
/* primary buttons */
--button-hover: #fbf0df;
--section-team-bg: #a40404;
/* meet the team background */
--team-button-bg: #fbf0df;
/* team buttons start */
--team-button-hover: #450206;
/* team button hover */
--header-bg: #450206;
/* header background */
--hero-overlay-color: rgba(0, 0, 0, 0.12);
--font-title: 'Cinzel', serif;
--font-body: 'Lato', sans-serif;
--section-padding: 5rem 5%;
--card-padding: 1.6rem;
--grid-gap: 1.6rem;
--transition: 0.28s cubic-bezier(.2, .9, .2, 1);
--shadow-light: 0 2px 8px rgba(0, 0, 0, 0.06);
--shadow-medium: 0 6px 14px rgba(0, 0, 0, 0.08);
--shadow-heavy: 0 12px 28px rgba(0, 0, 0, 0.12);
--border-radius: 12px;
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font-body);
background-color: var(--background-color);
color: var(--primary-color);
line-height: 1.6;
overflow-x: hidden;
}
/* Typography */
h1,
h2,
h3 {
font-family: var(--font-title);
color: var(--accent-color);
margin-bottom: 1rem;
font-weight: 700;
line-height: 1.2;
}
h1 {
font-size: 3.5rem;
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.7);
}
h2 {
font-size: 2.5rem;
text-align: center;
margin-bottom: 3rem;
}
h3 {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
p {
margin-bottom: 1rem;
font-size: 1rem;
}
a {
color: var(--accent-color);
text-decoration: none;
transition: color var(--transition);
}
a:hover {
color: var(--primary-color);
}
/* Navigation */
.sticky-nav {
position: sticky;
top: 0;
width: 100%;
background-color: var(--header-bg);
z-index: 1000;
box-shadow: var(--shadow-medium);
padding: 14px 0;
/* taller header */
transition: background-color var(--transition);
}
nav {
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
position: relative;
}
.logo {
font-family: var(--font-title);
font-size: 1.8rem;
font-weight: bold;
color: var(--accent-color);
display: flex;
align-items: center;
margin-left: 48px;
margin-right: 48px;
/* push logo further left and add space to the nav links */
}
.site-logo {
max-height: 56px;
height: auto;
display: block;
}
nav ul {
list-style: none;
display: flex;
margin-left: auto;
/* push links to the right */
margin-right: 56px;
}
nav ul li {
margin-left: 1.2rem;
}
nav ul li a {
font-weight: bold;
padding: 0.5rem 0;
position: relative;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 0.9rem;
}
nav ul li a::after {
content: '';
position: absolute;
width: 0;
height: 2px;
bottom: -2px;
left: 0;
background-color: var(--accent-color);
transition: width var(--transition);
}
nav ul li a:hover::after {
width: 100%;
}
/* Header text color to contrast against dark header */
.logo {
color: var(--team-button-bg);
}
nav ul li a {
color: var(--team-button-bg);
}
.language-toggle {
position: absolute;
right: 24px;
top: 50%;
transform: translateY(-50%);
background-color: var(--team-button-bg);
color: var(--header-bg);
border: none;
border-radius: 999px;
padding: 0.35rem 0.85rem;
font-weight: 700;
font-size: 0.78rem;
letter-spacing: 0.08em;
cursor: pointer;
transition: transform var(--transition), box-shadow var(--transition), background-color var(--transition);
box-shadow: var(--shadow-light);
}
.language-toggle:hover {
background-color: var(--accent-color);
color: #fff;
transform: translateY(-50%) translateY(-1px);
}
/* Burger Menu Button (Hidden on Desktop) */
.burger-menu {
display: none;
background: none;
border: none;
cursor: pointer;
padding: 0.5rem;
z-index: 1100;
}
.burger-menu span {
display: block;
width: 25px;
height: 3px;
background-color: var(--primary-color);
margin: 5px 0;
transition: all var(--transition);
}
/* Hero Section */
.hero-section {
position: relative;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
color: white;
background-image: url('images/background.png');
background-size: cover;
background-position: center center;
background-repeat: no-repeat;
background-color: transparent;
overflow: hidden;
padding: var(--section-padding);
}
/* Hero CTA styled like team buttons per request */
.hero-section .cta-button {
background-color: var(--team-button-bg);
color: var(--section-team-bg);
border: none;
}
.hero-section .cta-button:hover {
background-color: var(--team-button-hover);
color: var(--team-button-bg);
}
.hero-overlay {
position: absolute;
inset: 0;
background-color: var(--hero-overlay-color);
z-index: 1;
}
.hero-content {
position: relative;
z-index: 2;
max-width: 900px;
}
.hero-content h1 {
color: var(--accent-color);
}
.hero-content .tagline {
font-size: 1.4rem;
color: #f6efe4;
margin-bottom: 1.5rem;
font-style: italic;
}
.hero-content .description {
font-size: 1.1rem;
color: #f6efe4;
margin: 0 auto 2.5rem;
max-width: 600px;
}
.cta-button,
.download-button {
display: inline-block;
background-color: var(--button-color);
color: var(--button-hover);
padding: 0.9rem 2.2rem;
border-radius: 5px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 1px;
transition: all var(--transition);
box-shadow: var(--shadow-medium);
border: none;
}
.cta-button:hover,
.download-button:hover {
background-color: var(--button-hover);
color: var(--button-color);
transform: translateY(-3px);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.08);
}
.download-button {
font-size: 1.1rem;
}
/* General Content Sections */
.content-section,
.features-section,
.resources-section,
.team-section {
padding: var(--section-padding);
width: 100%;
}
/* Keep inner grids/content centered with a max width */
.features-grid,
.team-grid {
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
/* Center content and grid items site-wide */
.content-section,
.features-section,
.resources-section,
.team-section {
text-align: center;
}
.features-grid,
.team-grid {
justify-items: center;
align-items: start;
}
.features-grid {
justify-content: center;
}
.team-grid {
justify-content: center;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
/* Center nav links */
.sticky-nav nav {
justify-content: center;
}
nav ul {
margin-left: 0;
gap: 3.2rem;
}
nav ul li {
margin-left: 0;
}
/* Make videos keep a sensible aspect ratio so they don't look flat */
video,
.modal-content video {
aspect-ratio: 16/9;
height: auto;
}
/* Story Section */
.content-section p {
max-width: 800px;
margin: 0 auto;
text-align: center;
font-size: 1.1rem;
color: var(--primary-color);
}
/* Decorative Divider */
.features-section::before,
.table-section::before,
.resources-section::before {
content: '';
display: block;
width: 150px;
height: 3px;
background: linear-gradient(to right, transparent, var(--secondary-color), transparent);
margin: 1rem auto;
opacity: 0.6;
}
.table-section {
padding-bottom: 1rem;
}
.media-section {
padding: var(--section-padding);
background-color: var(--card-bg-color);
text-align: center;
}
.media-grid {
max-width: 1200px;
margin: 2rem auto 0;
display: grid;
grid-template-columns: repeat(2, minmax(280px, 1fr));
gap: var(--grid-gap);
}
.media-card {
background-color: var(--background-color);
border: 1px solid var(--secondary-color);
border-radius: var(--border-radius);
padding: var(--card-padding);
box-shadow: var(--shadow-light);
}
.media-card h3 {
margin-bottom: 1rem;
}
.media-card video {
width: 100%;
border-radius: 10px;
display: block;
}
.resources-section {
padding-top: 1rem;
}
/* Features Section */
.features-section {
background-color: var(--card-bg-color);
}
.features-grid,
.team-grid {
display: grid;
gap: var(--grid-gap);
margin-top: 2rem;
}
.features-grid {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
.team-grid {
grid-template-columns: repeat(4, 1fr);
margin-top: 2.5rem;
align-items: stretch;
}
.feature-card,
.team-member {
background-color: var(--background-color);
padding: var(--card-padding);
border-radius: var(--border-radius);
text-align: center;
border: 1px solid var(--secondary-color);
transition: transform var(--transition), box-shadow var(--transition);
display: flex;
flex-direction: column;
align-items: center;
}
.feature-card:hover {
transform: translateY(-5px) scale(1.02);
box-shadow: var(--shadow-heavy);
}
.feature-card {
border-top: 4px solid transparent;
}
.feature-card[data-accent="1"] {
border-top-color: var(--accent-red);
}
.feature-card[data-accent="2"] {
border-top-color: var(--accent-green);
}
.feature-card[data-accent="3"] {
border-top-color: var(--accent-navy);
}
.feature-card[data-accent="4"] {
border-top-color: var(--accent-cyan);
}
.team-member {
background-color: #0f0e11;
border: 1px solid rgba(255, 255, 255, 0.04);
transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.25s cubic-bezier(0.22, 1, 0.36, 1),
border-color 0.25s;
box-shadow: var(--shadow-light), 0 2px 8px 0 rgba(0, 0, 0, 0.18);
height: 100%;
min-width: 0;
}
.team-member:hover {
transform: translateY(-10px) scale(1.04);
box-shadow: 0 0 36px 8px rgba(11, 61, 145, 0.12), 0 6px 22px 0 rgba(0, 0, 0, 0.36);
border-color: rgba(11, 61, 145, 0.18);
z-index: 2;
}
.team-member-link {
text-decoration: none;
color: inherit;
width: 100%;
}
.team-member-link:hover {
color: inherit;
}
.feature-card .icon {
width: 100px;
margin-bottom: 1.5rem;
object-fit: contain;
}
.feature-card h3,
.team-member h3 {
color: var(--accent-color);
}
.feature-card p {
font-size: 0.95rem;
color: var(--primary-color);
}
.team-photo {
width: 90px;
height: 90px;
border-radius: 50%;
object-fit: cover;
margin-bottom: 1.2rem;
border: 2px solid rgba(255, 255, 255, 0.04);
background: #141114;
/* darker neutral placeholder (less white) */
pointer-events: none;
}
/* Resources Section */
.resources-section {
text-align: center;
}
/* Team section uses the red background per request */
.team-section {
background-color: var(--section-team-bg);
color: var(--team-button-bg);
padding: var(--section-padding) 0;
/* full-bleed background, inner padding on grid */
}
.team-grid {
padding: 0 5%;
max-width: 1200px;
margin: 0 auto;
}
.team-section .cta-button,
.team-section .download-button {
background-color: var(--team-button-bg);
color: var(--section-team-bg);
border: none;
}
.team-section .cta-button:hover,
.team-section .download-button:hover {
background-color: var(--team-button-hover);
color: var(--team-button-bg);
}
/* Ensure AOS animations have a starting point */
[data-aos] {
opacity: 0;
transition-property: transform, opacity;
}
/* Ensure global CTA/Download buttons use the user-requested button color (unless overridden in team-section) */
.cta-button {
background-color: var(--button-color);
color: var(--button-hover);
border: none;
}
.cta-button:hover {
background-color: var(--button-hover);
color: var(--button-color);
}
.download-button {
background-color: var(--button-color);
color: var(--button-hover);
border: none;
}
.download-button:hover {
background-color: var(--button-hover);
color: var(--button-color);
}
/* Table preview sizing + padding so it never touches edges */
.table-preview-container {
padding: 8px;
box-sizing: border-box;
border-radius: 24px;
overflow: hidden;
}
.table-preview-image {
max-width: 100%;
width: calc(100% - 32px);
height: auto;
padding: 8px;
border-radius: 24px;
display: block;
}
@media (max-width: 900px) {
.media-grid {
grid-template-columns: 1fr;
}
}
/* Modal and video: ensure padding and responsive scaling without distortion */
.modal {
padding: 24px;
box-sizing: border-box;
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.6);
z-index: 2000;
}
.modal-content {
width: min(1000px, calc(100% - 48px));
max-width: 100%;
padding: 20px;
background: var(--card-bg-color);
border-radius: 12px;
}
.modal.show {
display: flex;
}
.modal-content video {
display: block;
width: 100%;
height: auto;
aspect-ratio: 16/9;
}
.modal-content video,
video {
width: 100%;
height: auto;
max-height: calc(100vh - 160px);
object-fit: contain;
display: block;
}
/* Remove global side padding so sections can be full-bleed; keep inner content padded via grids */
main,
body {
padding-left: 0;
padding-right: 0;
}
/* Run instructions code window */
.run-instructions {
max-width: 900px;
margin: 2rem auto 0;
/* space above code window (gap from download button) */
text-align: left;
}
.run-instructions .code-window {
background: #0b1220;
color: #e6eef8;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, 'Roboto Mono', 'Courier New', monospace;
padding: 1rem 1.25rem;
border-radius: 10px;
box-shadow: 0 8px 30px rgba(2, 6, 23, 0.45);
overflow: auto;
border: 1px solid rgba(255, 255, 255, 0.04);
position: relative;
}
.run-instructions .code-window::before {
position: absolute;
left: 12px;
top: -12px;
background: var(--header-bg);
color: var(--team-button-bg);
padding: 4px 8px;
border-radius: 8px;
font-size: 0.75rem;
font-weight: 700;
}
.run-instructions .code-window code {
display: block;
white-space: pre;
font-size: 0.95rem;
line-height: 1.6;
}
.run-instructions .note {
margin-top: 0.75rem;
font-size: 0.9rem;
color: var(--secondary-color);
}
Binary file not shown.
@@ -52,7 +52,7 @@ public final class Main {
}
private static void printUsage() {
Logger logger = LogManager.getLogger(Main.class);
Logger logger = LogManager.getLogger(Main.class.getSimpleName());
logger.fatal(
"""
Usage:
@@ -11,11 +11,11 @@ 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 {
private static final Logger LOGGER = LogManager.getLogger(ClientApp.class);
private static final Logger LOGGER = LogManager.getLogger(ClientApp.class.getSimpleName());
/** Shared client connection used when a username is provided at startup. */
private static volatile ClientService sharedClientService;
@@ -40,7 +40,14 @@ public class ClientApp {
}
public static void updateSharedUsername(String username) {
setSharedUsername(username != null && !username.isBlank() ? username.trim() : null);
setSharedUsername(normalizeUsername(username));
}
private static String normalizeUsername(String username) {
if (username == null || username.isBlank()) {
return null;
}
return username.trim();
}
private static void setSharedUsername(String username) {
@@ -52,15 +59,14 @@ public class ClientApp {
String[] hostPort = parseHostAndPort(arg);
String host = hostPort[0];
int port = Integer.parseInt(hostPort[1]);
String normalizedUsername = normalizeUsername(username);
configureServerProperties(host, port);
setSharedUsername(username != null && !username.isBlank() ? username.trim() : null);
setSharedUsername(normalizedUsername);
if (getSharedUsername() != null) {
performStartupLogin(host, port, username);
}
performStartupLogin(host, port, normalizedUsername);
launchUI(arg, username);
launchUI(arg);
}
private static String[] parseHostAndPort(String arg) {
@@ -81,52 +87,33 @@ public class ClientApp {
try {
ClientService clientService = new ClientService(host, port);
setSharedClientService(clientService);
LobbyClient lobbyClient = new LobbyClient(clientService);
LoginResult res = lobbyClient.login(username);
java.util.concurrent.ExecutorService bg =
java.util.concurrent.Executors.newSingleThreadExecutor(
r -> {
Thread t = new Thread(r);
t.setDaemon(true);
t.setName("casono-startup-login");
return t;
});
if (res != null && res.getUsername() != null && !res.getUsername().isBlank()) {
setSharedUsername(res.getUsername().trim());
}
bg.submit(
() -> {
try {
LobbyClient lobbyClient = new LobbyClient(clientService);
LoginResult res = lobbyClient.login(username);
if (res != null
&& res.getUsername() != null
&& !res.getUsername().isBlank()) {
setSharedUsername(res.getUsername().trim());
}
LOGGER.info(
"Assigned username='{}' id={}",
res != null ? res.getUsername() : null,
res != null ? res.getId() : null);
} catch (RuntimeException e) {
LOGGER.warn("Startup login failed: {}", e.getMessage());
} catch (Exception e) {
LOGGER.warn("Unexpected error during startup login", e);
} finally {
bg.shutdown();
}
});
String assignedUsername = null;
String assignedId = null;
if (res != null) {
assignedUsername = res.getUsername();
assignedId = res.getId();
}
LOGGER.info("Assigned username='{}' id={}", assignedUsername, assignedId);
} catch (RuntimeException e) {
LOGGER.warn(
"Could not establish initial connection for startup login: {}", e.getMessage());
}
}
private static void launchUI(String arg, String username) {
if (username != null && !username.isBlank()) {
Launcher.main(new String[] {arg, username});
} else {
private static void launchUI(String arg) {
String username = getSharedUsername();
if (username == null || username.isBlank()) {
Launcher.main(new String[] {arg});
return;
}
Launcher.main(new String[] {arg, username});
}
public static void main(String[] args) {
@@ -134,7 +121,10 @@ public class ClientApp {
throw new IllegalArgumentException("Address argument required: <host:port>");
}
String username = args.length > 1 && args[1] != null && !args[1].isBlank() ? args[1] : null;
String username = null;
if (args.length > 1) {
username = normalizeUsername(args[1]);
}
start(args[0], username);
}
@@ -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;
/**
@@ -76,8 +88,9 @@ public class ChatController {
chatModelMap = new LinkedHashMap<>();
localUserList = new ArrayList<>();
this.chatBoxController = new ChatBoxController(username, this);
this.logger = LogManager.getLogger(ChatController.class);
this.logger = LogManager.getLogger(ChatController.class.getSimpleName());
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.getSimpleName());
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,7 +1,9 @@
package ch.unibas.dmi.dbis.cs108.casono.client.game;
import ch.unibas.dmi.dbis.cs108.casono.client.network.GameClient;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -11,6 +13,9 @@ public class GameService {
private final GameClient client;
private GameState state;
private final Queue<Runnable> actionQueue = new LinkedList<>();
private volatile boolean actionInProgress = false;
private static final long ACTION_TIMEOUT_MS = 5000;
/**
* Constructs a GameService with the specified GameClient.
@@ -112,12 +117,12 @@ public class GameService {
/** Retrieves the current phase of the game. */
public void call() {
client.sendCall();
queueAction(() -> client.sendCall());
}
/** Retrieves the current phase of the game. */
public void fold() {
client.sendFold();
queueAction(() -> client.sendFold());
}
/**
@@ -126,7 +131,7 @@ public class GameService {
* @param amount The amount to bet. Must be a positive integer.
*/
public void bet(int amount) {
client.sendBet(amount);
queueAction(() -> client.sendBet(amount));
}
/**
@@ -135,7 +140,63 @@ public class GameService {
* @param amount The amount to raise. Must be a positive integer.
*/
public void raise(int amount) {
client.sendRaise(amount);
queueAction(() -> client.sendRaise(amount));
}
/**
* Queue an action to be executed sequentially. Only one action is processed at a time to
* prevent concurrent operations from overwhelming the server or UI.
*
* @param action The action to queue and execute.
*/
private void queueAction(Runnable action) {
synchronized (actionQueue) {
actionQueue.offer(action);
if (!actionInProgress) {
processNextAction();
}
}
}
/**
* Process the next queued action if one is available. This method ensures that only one action
* is in progress at any given time.
*/
private void processNextAction() {
synchronized (actionQueue) {
if (actionQueue.isEmpty() || actionInProgress) {
return;
}
actionInProgress = true;
Runnable action = actionQueue.poll();
if (action != null) {
try {
LOG.fine("Processing queued action");
action.run();
} catch (Exception e) {
LOG.log(Level.WARNING, "Error executing queued action: " + e.getMessage(), e);
} finally {
actionInProgress = false;
if (!actionQueue.isEmpty()) {
processNextAction();
}
}
} else {
actionInProgress = false;
}
}
}
/**
* Check if an action is currently in progress, preventing the UI from accepting new actions
* while one is being processed.
*
* @return true if an action is in progress, false otherwise.
*/
public boolean isActionInProgress() {
return actionInProgress;
}
/** Ensures that the game state has been initialized before accessing it. */
@@ -18,4 +18,6 @@ public class GameState {
public int dealer;
public int activePlayer;
public int winnerIndex = -1;
public List<String> winnerNames = new ArrayList<>();
public int potPerWinner;
}
@@ -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;
@@ -25,7 +26,7 @@ public class ChatClient {
*/
public ChatClient(ClientService clientService) {
this.clientService = clientService;
this.logger = LogManager.getLogger(ChatClient.class);
this.logger = LogManager.getLogger(ChatClient.class.getSimpleName());
}
/**
@@ -60,7 +60,7 @@ public class ClientService {
public ClientService(String ip, int port) {
this.idGenerator = new AtomicInteger(0);
this.logger = LogManager.getLogger(ClientService.class);
this.logger = LogManager.getLogger(ClientService.class.getSimpleName());
this.offlineMode = false;
@@ -247,8 +247,7 @@ public class ClientService {
// Allow primitive values to contain hyphens (UUIDs) in addition to
// digits/words/colons
static Pattern responseRex =
Pattern.compile(
"(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[+-]?[-\\d\\w:]+))");
Pattern.compile("(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[^ ]+))");
/**
* Removes escape characters from a string, specifically converting escaped single quotes (\')
@@ -92,25 +92,46 @@ public class GameClient {
}
public void sendCall() {
client.processCommand("CALL GAME_ID=" + gameId);
try {
client.processCommand("CALL GAME_ID=" + gameId);
LOG.fine("CALL command sent");
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to send CALL: " + e.getMessage(), e);
}
}
public void sendFold() {
client.processCommand("FOLD GAME_ID=" + gameId);
try {
client.processCommand("FOLD GAME_ID=" + gameId);
LOG.fine("FOLD command sent");
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to send FOLD: " + e.getMessage(), e);
}
}
public void sendBet(int amount) {
client.processCommand("BET GAME_ID=" + gameId + " AMOUNT=" + amount);
try {
client.processCommand("BET GAME_ID=" + gameId + " AMOUNT=" + amount);
LOG.fine("BET command sent: amount=" + amount);
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to send BET: " + e.getMessage(), e);
}
}
public void sendRaise(int amount) {
client.processCommand("RAISE GAME_ID=" + gameId + " AMOUNT=" + amount);
try {
client.processCommand("RAISE GAME_ID=" + gameId + " AMOUNT=" + amount);
LOG.fine("RAISE command sent: amount=" + amount);
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to send RAISE: " + e.getMessage(), e);
}
}
private GameState parseGameState(String input) {
GameState s = new GameState();
s.players = new ArrayList<>();
s.communityCards = new ArrayList<>();
s.winnerNames = new ArrayList<>();
ParseState state = new ParseState();
for (String raw : input.split("\n")) {
@@ -155,6 +176,14 @@ public class GameClient {
s.winnerIndex = intVal(l);
yield true;
}
case String l when l.startsWith("WINNER_NAME=") -> {
s.winnerNames.add(value(l));
yield true;
}
case String l when l.startsWith("POT_PER_WINNER=") -> {
s.potPerWinner = intVal(l);
yield true;
}
case String l when l.startsWith("HIGHSCORE=") -> {
s.highscoreEntries.add(value(l));
yield true;
@@ -224,6 +253,13 @@ public class GameClient {
return true;
}
if (line.startsWith("ID=")) {
if (state.currentPlayer.getId() == null) {
state.currentPlayer.setId(PlayerId.of(value(line)));
}
return true;
}
if (line.startsWith("CHIPS=")) {
state.currentPlayer.setChips(intVal(line));
return true;
@@ -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);
@@ -131,6 +127,15 @@ public class LobbyClient {
client.processCommand("JOIN_LOBBY ID=" + lobbyId);
}
/**
* Request the server to mark the current user as absent from the given lobby.
*
* @param lobbyId the lobby to leave
*/
public void leaveLobby(int lobbyId) {
client.processCommand("LEAVE_LOBBY ID=" + lobbyId);
}
/**
* Logs in to the server with the given username by sending a "LOGIN" command.
*
@@ -139,7 +144,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 +161,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 +212,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(
@@ -241,6 +253,33 @@ public class LobbyClient {
return result;
}
/**
* Fetches the current lobby members from GET_LOBBY_STATUS and returns their usernames in order.
*
* @param lobbyId id of the lobby to inspect
* @return list of usernames currently known in the lobby
*/
public List<String> fetchLobbyPlayerNames(int lobbyId) {
List<String> lines = client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId);
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
List<String> names = new ArrayList<>();
for (RequestParameter parameter : params) {
if (!"USERNAME".equalsIgnoreCase(parameter.key())) {
continue;
}
String name = parameter.value();
if (name != null) {
String trimmed = name.trim();
if (!trimmed.isEmpty() && !names.contains(trimmed)) {
names.add(trimmed);
}
}
}
return names;
}
/** Simple data holder for lobby metadata returned by the server. */
public static final class LobbyInfo {
public final int id;
@@ -0,0 +1,106 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui;
import java.net.URL;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.StackPane;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** Plays an intro video in fullscreen and then starts the main UI. */
public class IntroVideoPlayer extends Application {
private static final Logger LOGGER = LogManager.getLogger(IntroVideoPlayer.class);
private Stage videoStage;
@Override
public void start(Stage stage) {
this.videoStage = stage;
URL videoUrl = getClass().getResource("/images/placeholder-animation.mp4");
if (videoUrl == null) {
LOGGER.error("Video file not found!");
Platform.exit();
return;
}
Media media = new Media(videoUrl.toExternalForm());
MediaPlayer mediaPlayer = new MediaPlayer(media);
MediaView mediaView = new MediaView(mediaPlayer);
StackPane root = new StackPane(mediaView);
// black background
root.setStyle("-fx-background-color: black;");
Rectangle2D screenBounds = Screen.getPrimary().getBounds();
Scene scene = new Scene(root, screenBounds.getWidth(), screenBounds.getHeight());
// scale video automatically
mediaView.setPreserveRatio(true);
mediaView.setFitWidth(screenBounds.getWidth());
mediaView.setFitHeight(screenBounds.getHeight());
stage.initStyle(StageStyle.UNDECORATED);
stage.setFullScreen(true);
stage.setFullScreenExitHint("");
stage.setScene(scene);
// Add F11 fullscreen toggle
scene.setOnKeyPressed(
event -> {
if (event.getCode() == KeyCode.F11) {
stage.setFullScreen(!stage.isFullScreen());
event.consume();
}
});
stage.show();
mediaPlayer.setOnEndOfMedia(this::onVideoEnd);
mediaPlayer.play();
}
private void onVideoEnd() {
videoStage.close();
setSystemProperties();
startMainUI();
}
private void setSystemProperties() {
var params = getParameters().getRaw();
if (params == null || params.isEmpty()) {
return;
}
String arg = params.get(0);
String[] parts = arg.split(":", 2);
if (parts.length == 2) {
System.setProperty("casono.server.host", parts[0]);
System.setProperty("casono.server.port", parts[1]);
}
}
private void startMainUI() {
Stage mainStage = new Stage();
try {
new Casinomainui().start(mainStage);
} catch (Exception e) {
e.printStackTrace();
Platform.exit();
}
}
}
@@ -1,13 +1,8 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui;
import javafx.application.Application;
/**
* Launcher for the Casono main UI.
*
* <p>Default constructor for the application.
*/
/** Launcher for the Casono intro animation. */
public class Launcher {
/** Default constructor. */
@@ -21,6 +16,6 @@ public class Launcher {
* @param args Command line arguments
*/
public static void main(String[] args) {
Application.launch(Casinomainui.class, args);
Application.launch(IntroVideoPlayer.class, args);
}
}
@@ -1,8 +1,11 @@
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 ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
@@ -19,7 +22,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 +29,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 +67,15 @@ 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(
_ -> {
SoundManager.getInstance().playButtonClick();
addWhisperChatButton.show();
});
}
/**
@@ -98,14 +95,13 @@ public class ChatBoxController {
MenuItem menuItem = new MenuItem(targetUserName);
addWhisperChatButton.getItems().add(menuItem);
menuItem.setOnAction(
event ->
addWhisperChat(
targetUserName,
new ChatModel(
ChatType.WHISPER,
username,
-1,
targetUserName)));
_ -> {
SoundManager.getInstance().playButtonClick();
addWhisperChat(
targetUserName,
new ChatModel(
ChatType.WHISPER, username, -1, targetUserName));
});
});
}
@@ -132,14 +128,16 @@ public class ChatBoxController {
if (oldUsername.equals(item.getText())) {
item.setText(newUsername);
item.setOnAction(
event ->
addWhisperChat(
newUsername,
new ChatModel(
ChatType.WHISPER,
username,
-1,
newUsername)));
_ -> {
SoundManager.getInstance().playButtonClick();
addWhisperChat(
newUsername,
new ChatModel(
ChatType.WHISPER,
username,
-1,
newUsername));
});
break;
}
}
@@ -166,11 +164,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 +193,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 +260,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);
});
}
}
@@ -3,6 +3,7 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.application.Platform;
@@ -11,37 +12,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 +66,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());
}
@@ -65,7 +77,11 @@ public class ChatViewController implements Initializable {
* The input field is cleared after sending.
*/
public void sendMessage() {
SoundManager.getInstance().playButtonClick();
String message = inputField.getText().trim();
message = message.replace("'", " ");
if (!message.isEmpty()) {
inputField.clear();
String currentUsername = controller.getCurrentUsername();
@@ -88,6 +104,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 =
File diff suppressed because it is too large Load Diff
@@ -1,9 +1,12 @@
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;
import ch.unibas.dmi.dbis.cs108.casono.client.network.GameClient;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
import java.io.IOException;
import java.util.UUID;
import java.util.logging.Logger;
@@ -11,6 +14,7 @@ import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.stage.Stage;
/**
@@ -24,6 +28,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 +60,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.
*
@@ -75,10 +84,24 @@ public class CasinoGameUI extends Application {
*/
@Override
public void start(Stage stage) throws IOException {
ensureClientService();
String effectiveUsername = determineEffectiveUsername();
logStartInfo(effectiveUsername);
FXMLLoader fxmlLoader =
new FXMLLoader(CasinoGameUI.class.getResource("/ui-structure/Casinogameui.fxml"));
Parent root = fxmlLoader.load();
CasinoGameController controller = fxmlLoader.getController();
configureController(controller, effectiveUsername);
Scene scene = new Scene(root, DEFAULT_WIDTH, DEFAULT_HEIGHT);
configureStage(stage, scene, controller);
controller.start();
}
private void ensureClientService() {
if (clientService == null) {
clientService =
ch.unibas.dmi.dbis.cs108.casono.client.ClientApp.getSharedClientService();
clientService = ClientApp.getSharedClientService();
}
if (clientService == null) {
throw new IllegalStateException(
@@ -86,34 +109,34 @@ public class CasinoGameUI extends Application {
+ "Call CasinoGameUI.setClientService(...)"
+ " or start via ClientApp with a shared connection.");
}
}
private String determineEffectiveUsername() {
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) {
effectiveUsername =
"Guest-" + UUID.randomUUID().toString().substring(0, GUEST_ID_LENGTH);
}
return effectiveUsername;
}
private void logStartInfo(String effectiveUsername) {
LOG.info(
"CasinoGameUI starting: effectiveUsername='"
+ effectiveUsername
+ "', injectedUsername='"
+ username
+ "', sharedUsername='"
+ ch.unibas.dmi.dbis.cs108.casono.client.ClientApp.getSharedUsername()
+ ClientApp.getSharedUsername()
+ "', hasClientService="
+ (clientService != null));
}
FXMLLoader fxmlLoader =
new FXMLLoader(CasinoGameUI.class.getResource("/ui-structure/Casinogameui.fxml"));
Parent root = fxmlLoader.load();
CasinoGameController controller = fxmlLoader.getController();
private void configureController(CasinoGameController controller, String effectiveUsername)
throws IOException {
if (lobbyId <= 0) {
throw new IllegalStateException("CasinoGameUI: lobbyId must be set before start()");
}
@@ -121,22 +144,35 @@ public class CasinoGameUI extends Application {
GameClient gameClient = new GameClient(clientService, lobbyId);
GameService gameService = new GameService(gameClient);
controller.setGameService(gameService);
controller.setMyPlayerId(PlayerId.of(effectiveUsername));
controller.setChatContext(effectiveUsername, clientService, lobbyId);
Scene scene = new Scene(root, DEFAULT_WIDTH, DEFAULT_HEIGHT);
stage.setTitle("Casono");
try {
LobbyClient lobbyClient = new LobbyClient(clientService);
controller.setLobbyPlayerNames(lobbyClient.fetchLobbyPlayerNames(lobbyId));
} catch (Exception e) {
LOG.fine("Could not preload lobby player names: " + e.getMessage());
}
controller.startChat(chatController);
}
private void configureStage(Stage stage, Scene scene, CasinoGameController controller) {
stage.setTitle("Casono");
String iconPath = getClass().getResource("/images/logoinverted.png").toExternalForm();
stage.getIcons().add(new javafx.scene.image.Image(iconPath));
stage.setScene(scene);
stage.setFullScreen(true);
stage.setFullScreenExitHint("");
stage.setOnHidden(e -> controller.stop());
scene.setOnKeyPressed(
event -> {
if (event.getCode() == KeyCode.F11) {
stage.setFullScreen(!stage.isFullScreen());
event.consume();
}
});
stage.show();
controller.start();
}
/**
@@ -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;
@@ -61,7 +68,13 @@ public class CasinoBrowserController {
private static final CookieManager COOKIE_MANAGER =
new CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER);
private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class);
private static final Logger LOGGER =
LogManager.getLogger(CasinoBrowserController.class.getSimpleName());
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;
@@ -74,12 +87,15 @@ public class CasinoBrowserController {
private static final int ALERT_HEIGHT = 300;
private static final String LOGO_PATH = "/images/logoinverted.png";
private static final String LOGO_PATH_MAIN = "/images/logo.png";
private static final int MAX_AUTOCOMPLETE_RESULTS = 5;
static {
CookieHandler.setDefault(COOKIE_MANAGER);
TRUSTED_DOMAINS.add("wikipedia.org");
TRUSTED_DOMAINS.add("youtube.com");
TRUSTED_DOMAINS.add("github.com");
TRUSTED_DOMAINS.add("search.brave.com");
TRUSTED_DOMAINS.add("oracle.com");
TRUSTED_DOMAINS.add("stackoverflow.com");
TRUSTED_DOMAINS.add("docs.oracle.com");
@@ -94,8 +110,69 @@ public class CasinoBrowserController {
TRUSTED_DOMAINS.add("metager.de");
TRUSTED_DOMAINS.add("unibas.ch");
TRUSTED_DOMAINS.add("mojeek.com");
TRUSTED_DOMAINS.add("searx.be ");
TRUSTED_DOMAINS.add("searx.be");
TRUSTED_DOMAINS.add("startpage.com");
TRUSTED_DOMAINS.add("vscode.dev");
TRUSTED_DOMAINS.add("casono.netlify.app");
}
/** 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"),
Map.entry("vs", "vscode.dev"),
Map.entry("vscode", "vscode.dev"),
Map.entry("vs code", "vscode.dev"),
Map.entry("main", "casono.netlify.app"),
Map.entry("cas", "casono.netlify.app"),
Map.entry("casono", "casono.netlify.app"));
/**
* 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;
@@ -124,6 +201,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 +289,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 +369,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 +472,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 +486,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 +577,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 +644,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 baseDir = Paths.get(System.getProperty("user.dir")).normalize();
Path manual = baseDir.resolve("documents/docs/game-engine/manual.html").normalize();
Path outreach = baseDir.resolve("outreach/index.html").normalize();
Path requested = Paths.get(uri).normalize();
if (requested.equals(manual) || requested.equals(outreach)) {
securityLabel.setText("LOCAL OK");
engine.load(uri.toString());
} else {
securityLabel.setText("UNKNOWN");
engine.load(uri.toString());
}
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));
@@ -496,13 +701,15 @@ public class CasinoBrowserController {
securityLabel.setText("BLOCKED");
return;
}
securityLabel.setText("UNBEKANNT");
securityLabel.setText("UNKNOWN");
} else {
securityLabel.setText("SAFE");
}
engine.load(uri.toString());
} catch (Exception e) {
securityLabel.setText("ERROR");
securityLabel.setText("INVALID");
}
}
@@ -519,7 +726,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 +752,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)(\\?.*)?$");
}
}
@@ -14,7 +14,8 @@ import org.apache.logging.log4j.Logger;
/** Controller for the highscore popup window. */
public class HighscoreViewController {
private static final Logger LOGGER = LogManager.getLogger(HighscoreViewController.class);
private static final Logger LOGGER =
LogManager.getLogger(HighscoreViewController.class.getSimpleName());
@FXML private ListView<String> highscoreList;
@FXML private Label statusLabel;
@@ -0,0 +1,269 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Controller for the movable notebook (Tips) display within the Casono Game UI.
*
* <p>Responsible for: - Drag-and-drop movement of the notebook, - Display of general gaming tips, -
* Toggle visibility of the notebook.
*/
public class NotebookController {
private static final Logger LOGGER =
LogManager.getLogger(NotebookController.class.getSimpleName());
@FXML private VBox notebook;
@FXML private ScrollPane tipsContent;
@FXML private VBox tipsList;
@FXML private Button toggleButton;
private double xOffset = 0;
private double yOffset = 0;
private static final double NOTEBOOK_SCALE = 0.95;
private static final double SCALE_NORMAL = 1.0;
private boolean isVisible = true;
/** Standard constructor. Used by FXML. */
public NotebookController() {
// default constructor for FXML
}
/** Initialize the notebook with default tips. */
@FXML
private void initialize() {
if (tipsList != null) {
loadDefaultTips();
}
}
/** Load default poker tips into the notebook. */
private void loadDefaultTips() {
tipsList.getChildren().clear();
String[] tips = {
"TEXAS HOLD'EM GRUNDLAGEN\n\nJeder Spieler erhält 2 verdeckte Karten. "
+ "Zusätzlich gibt es 5 Gemeinschaftskarten.\n"
+ "Ziel: Beste 5-Karten-Hand bilden oder alle Gegner zum Fold bringen.\n"
+ "Startstack: 20000 Chips ($)",
"BLINDS & DEALER\n\nSmall Blind: 100 Chips | Big Blind: 200 Chips\n"
+ "Die Blinds erzeugen den Startpot und "
+ "müssen vor jeder Runde gezahlt werden.\n"
+ "Der Dealer-Button bestimmt die Positionen "
+ "sowie die Reihenfolge der Aktionen.",
"SPIELABLAUF\n\nPreflop → Flop (3 Karten) → Turn (4. Karte) → River (5. Karte)\n"
+ "Preflop beginnt links vom Big Blind.\n"
+ "Nach dem Flop beginnt die Action links vom Dealer (aktive Spieler).",
"AKTIONEN\n\nFold = aussteigen\nCall = mitgehen\nRaise = erhöhen\n"
+ "Alle Aktionen müssen in korrekter Reihenfolge (Acting in Turn) erfolgen.",
"POSITION IST ENTSCHEIDEND\n\nEarly Position: nur starke Hände spielen\n"
+ "Middle Position: etwas breiter spielen\n"
+ "Late Position: größter Vorteil durch mehr Informationen",
"SHOWDOWN\n\nWenn nach der letzten Setzrunde mehrere Spieler übrig sind:\n"
+ "Beste 5-Karten-Kombination aus Handkarten + Gemeinschaftskarten gewinnt.",
"STRATEGIE & DENKEN\n\nPoker ist Strategie + Psychologie + Mathematik\n"
+ "Entscheidungen basieren auf Wahrscheinlichkeiten, "
+ "Position und Gegnerverhalten.",
"WICHTIGE REGELN\n\n- Acting in Turn ist Pflicht\n"
+ "- Jeder Spieler spielt nur seine eigene Hand (One Player One Hand)\n"
+ "- Ungültige Einsätze werden blockiert oder korrigiert\n"
+ "- Reihenfolge am Tisch muss immer eingehalten werden",
"HÄUFIGE FEHLER\n\n❌ Zu viele Hände spielen\n"
+ "❌ Position ignorieren\n❌ Zu oft callen statt folden\n"
+ "❌ Stärke der Gegner unterschätzen\n❌ Tilt (emotional spielen)",
"TOP 10 POKERHÄNDE\n\n"
+ "1. 👑 Royal Flush\n"
+ "A♠ K♠ Q♠ J♠ 10♠\n\n"
+ "2. 🔥 Straight Flush\n"
+ "9♥ 8♥ 7♥ 6♥ 5♥\n\n"
+ "3. 💥 Four of a Kind (Poker)\n"
+ "A♣ A♦ A♥ A♠ K♠\n\n"
+ "4. 🏠 Full House\n"
+ "K♣ K♦ K♥ 9♠ 9♦\n\n"
+ "5. 🌊 Flush\n"
+ "A♥ J♥ 8♥ 5♥ 2♥\n\n"
+ "6. ➡ Straight\n"
+ "10♣ 9♦ 8♠ 7♥ 6♣\n\n"
+ "7. 🎯 Three of a Kind (Drilling)\n"
+ "Q♣ Q♦ Q♥ 7♠ 2♦\n\n"
+ "8. 👥 Two Pair\n"
+ "J♣ J♦ 4♠ 4♥ A♣\n\n"
+ "9. 👍 One Pair\n"
+ "8♣ 8♦ K♠ J♥ 3♣\n\n"
+ "10. 🃏 High Card\n"
+ "A♠ J♦ 9♣ 5♥ 2♠"
};
for (String tip : tips) {
TextFlow tipBox = createTipElement(tip);
tipsList.getChildren().add(tipBox);
}
}
private static final int LINE_SPACING = 4;
private static final int TIP_WIDTH = 280;
/**
* Create a styled text element for a single tip.
*
* @param tipText The text of the tip to display.
* @return A TextFlow element containing the formatted tip.
*/
private TextFlow createTipElement(String tipText) {
Text text = new Text(tipText);
text.setStyle("-fx-font-size: 12; -fx-font-family: 'Segoe UI', Arial;");
TextFlow flow = new TextFlow(text);
flow.setStyle(
"-fx-padding: 10; -fx-background-color: #f5f5f5; "
+ "-fx-border-radius: 5; -fx-margin: 5;");
flow.setLineSpacing(LINE_SPACING);
flow.setPrefWidth(TIP_WIDTH);
return flow;
}
/**
* Called when the notebook header is pressed with the mouse. Saves the relative position for
* later correct repositioning.
*
* @param event The mouse event
*/
@FXML
private void onNotebookPressed(MouseEvent event) {
if (notebook == null || event == null) {
return;
}
xOffset = event.getSceneX() - notebook.getLayoutX();
yOffset = event.getSceneY() - notebook.getLayoutY();
}
/**
* Called while dragging the notebook with the mouse. Updates the position and slightly scales
* the notebook for visual feedback.
*
* @param event The mouse event
*/
@FXML
private void onNotebookDragged(MouseEvent event) {
if (notebook == null || event == null || notebook.getScene() == null) {
return;
}
notebook.setScaleX(NOTEBOOK_SCALE);
notebook.setScaleY(NOTEBOOK_SCALE);
double targetX = event.getSceneX() - xOffset;
double targetY = event.getSceneY() - yOffset;
double maxX = Math.max(0, notebook.getScene().getWidth() - scaledNodeWidth());
double maxY = Math.max(0, notebook.getScene().getHeight() - scaledNodeHeight());
notebook.setLayoutX(clamp(targetX, 0, maxX));
notebook.setLayoutY(clamp(targetY, 0, maxY));
}
/**
* Called when the mouse cursor is released over the notebook. Resets the notebook scaling to
* normal size.
*
* @param event The mouse event
*/
@FXML
private void onNotebookReleased(MouseEvent event) {
if (notebook == null) {
return;
}
notebook.setScaleX(SCALE_NORMAL);
notebook.setScaleY(SCALE_NORMAL);
if (notebook.getScene() == null) {
return;
}
double maxX = Math.max(0, notebook.getScene().getWidth() - scaledNodeWidth());
double maxY = Math.max(0, notebook.getScene().getHeight() - scaledNodeHeight());
notebook.setLayoutX(clamp(notebook.getLayoutX(), 0, maxX));
notebook.setLayoutY(clamp(notebook.getLayoutY(), 0, maxY));
}
/** Close the notebook window from the header minus button. */
@FXML
private void onToggleNotebook() {
if (notebook == null) {
return;
}
isVisible = false;
notebook.setVisible(false);
notebook.setManaged(false);
}
/** Shows the tips notebook (makes it visible if currently hidden). */
public void showTips() {
if (notebook == null) {
return;
}
if (!isVisible) {
isVisible = true;
notebook.setVisible(true);
notebook.setManaged(true);
}
if (tipsContent != null) {
tipsContent.setVisible(true);
tipsContent.setManaged(true);
}
if (toggleButton != null) {
toggleButton.setText("");
}
}
/**
* Calculates the scaled width of the notebook node based on its current bounds and scale
* factor.
*
* @return The scaled width of the notebook node.
*/
private double scaledNodeWidth() {
double width = notebook.getBoundsInLocal().getWidth();
if (width <= 0) {
width = notebook.prefWidth(-1);
}
return Math.max(0, width * notebook.getScaleX());
}
/**
* Calculates the scaled height of the notebook node based on its current bounds and scale
* factor.
*
* @return The scaled height of the notebook node.
*/
private double scaledNodeHeight() {
double height = notebook.getBoundsInLocal().getHeight();
if (height <= 0) {
height = notebook.prefHeight(-1);
}
return Math.max(0, height * notebook.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));
}
}
@@ -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
@@ -17,15 +20,24 @@ public class PlayerStatusController {
private static final Logger LOGGER = Logger.getLogger(PlayerStatusController.class.getName());
@FXML private Label playerName;
@FXML private Label playerMoney;
@FXML private ImageView dealerIcon;
@FXML private Pane parent;
@FXML Label playerName;
@FXML Label playerMoney;
@FXML ImageView dealerIcon;
@FXML ImageView playerProfileImage;
@FXML Pane parent;
@FXML VBox playerStatusBox;
@FXML HBox statusInnerBoxTop;
@FXML HBox statusInnerBoxBottom;
private Image dealerImage;
// Profile images (loaded from resources/images/profile-picture)
private Image profileUserImage;
private Image profileDealerImage;
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 +54,27 @@ public class PlayerStatusController {
}
} catch (Exception e) {
LOGGER.severe("Error: loading dealer image:");
e.printStackTrace();
LOGGER.log(Level.SEVERE, "Error loading dealer image", e);
}
try {
var base = "/images/profile-picture/";
var uUser = getClass().getResource(base + "poker_user_491.png");
var uDealer = getClass().getResource(base + "poker_dealer_471.png");
if (uUser != null) {
profileUserImage = new Image(uUser.toExternalForm(), true);
}
if (uDealer != null) {
profileDealerImage = new Image(uDealer.toExternalForm(), true);
}
if (playerProfileImage != null && profileUserImage != null) {
playerProfileImage.setImage(profileUserImage);
}
} catch (Exception e) {
LOGGER.log(Level.FINE, "Could not load profile images", e);
}
dealerIcon.layoutXProperty().bind(parent.widthProperty().multiply(DEALER_ICON_X_FACTOR));
@@ -57,13 +88,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 +122,46 @@ 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) {
return false;
}
String currentName = normalizeIdentifier(player.getName());
String candidateName = normalizeIdentifier(candidate.getName());
if (currentName != null && candidateName != null) {
return currentName.equals(candidateName);
}
return player.getId() != null
&& candidate.getId() != null
&& player.getId().equals(candidate.getId());
}
private String normalizeIdentifier(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed.toLowerCase();
}
/**
* External quick update
*
@@ -106,5 +184,35 @@ public class PlayerStatusController {
if (isDealer && dealerImage != null) {
dealerIcon.setImage(dealerImage);
}
if (playerProfileImage != null) {
if (isDealer && profileDealerImage != null) {
playerProfileImage.setImage(profileDealerImage);
} else if (profileUserImage != null) {
playerProfileImage.setImage(profileUserImage);
}
}
}
/**
* 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);
}
}
}
@@ -0,0 +1,209 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
import java.util.function.Consumer;
import javafx.fxml.FXML;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Controller for the settings display within the poker UI. Handles theme selection and drag
* movement.
*/
public class SettingsController {
private static final Logger LOGGER =
LogManager.getLogger(SettingsController.class.getSimpleName());
@FXML private VBox settingsBox;
@FXML private RadioButton themeStandard;
@FXML private RadioButton themeStandardParquet;
@FXML private RadioButton themeBlackWhite;
@FXML private RadioButton themeGlass;
@FXML private RadioButton themeLightGlass;
private boolean visible = true;
private double xOffset;
private double yOffset;
private static final double SCALE_DRAG = 0.95;
private static final double SCALE_NORMAL = 1.0;
private String currentTheme = "standard";
private Consumer<String> themeChangeListener;
/** Initialize the settings controller, set up theme toggle group and listeners. */
@FXML
private void initialize() {
setupThemeToggle();
}
/** Set up the theme toggle group and listeners for the theme selection radio buttons. */
private void setupThemeToggle() {
if (themeStandard == null
|| themeStandardParquet == null
|| themeBlackWhite == null
|| themeGlass == null
|| themeLightGlass == null) {
LOGGER.warn("Theme buttons not injected from FXML");
return;
}
ToggleGroup group = new ToggleGroup();
themeStandard.setToggleGroup(group);
themeStandardParquet.setToggleGroup(group);
themeBlackWhite.setToggleGroup(group);
themeGlass.setToggleGroup(group);
themeLightGlass.setToggleGroup(group);
themeStandard.setSelected(true);
group.selectedToggleProperty()
.addListener(
(obs, oldVal, newVal) -> {
if (newVal == themeStandard) {
setTheme("standard");
} else if (newVal == themeStandardParquet) {
setTheme("mainuiparquet");
} else if (newVal == themeBlackWhite) {
setTheme("blackwhite");
} else if (newVal == themeGlass) {
setTheme("glass");
} else if (newVal == themeLightGlass) {
setTheme("lightglass");
}
});
}
/**
* Set the current theme and notify the listener of the change.
*
* @param theme The new theme to set (e.g., "standard", "blackwhite", "glass").
*/
private void setTheme(String theme) {
currentTheme = theme;
if (themeChangeListener != null) {
themeChangeListener.accept(theme);
}
LOGGER.info("Theme changed to: " + theme);
}
/**
* Handle the mouse press event on the settings box to prepare for dragging.
*
* @param event The MouseEvent triggered when the user presses the mouse button on the settings
* box.
*/
@FXML
private void onSettingsPressed(MouseEvent event) {
if (settingsBox == null) {
return;
}
xOffset = event.getSceneX() - settingsBox.getLayoutX();
yOffset = event.getSceneY() - settingsBox.getLayoutY();
}
/**
* Handle the mouse drag event on the settings box to allow dragging it around the scene.
*
* @param event The MouseEvent triggered when the user drags the mouse while pressing on the
* settings box.
*/
@FXML
private void onSettingsDragged(MouseEvent event) {
if (settingsBox == null || settingsBox.getScene() == null) {
return;
}
settingsBox.setScaleX(SCALE_DRAG);
settingsBox.setScaleY(SCALE_DRAG);
double x = event.getSceneX() - xOffset;
double y = event.getSceneY() - yOffset;
double maxX = settingsBox.getScene().getWidth() - settingsBox.getWidth();
double maxY = settingsBox.getScene().getHeight() - settingsBox.getHeight();
settingsBox.setLayoutX(clamp(x, 0, maxX));
settingsBox.setLayoutY(clamp(y, 0, maxY));
}
/** Close the settings box from the header minus button. */
@FXML
private void onToggleSettings() {
if (settingsBox == null) {
return;
}
visible = !visible;
settingsBox.setVisible(visible);
settingsBox.setManaged(visible);
}
/**
* Handle the mouse release event on the settings box to reset its scale after dragging.
*
* @param event The MouseEvent triggered when the user releases the mouse button after dragging
* the settings box.
*/
@FXML
private void onSettingsReleased(MouseEvent event) {
if (settingsBox == null || settingsBox.getScene() == null) {
return;
}
settingsBox.setScaleX(SCALE_NORMAL);
settingsBox.setScaleY(SCALE_NORMAL);
}
/**
* Set a listener to be notified when the theme changes. The listener will receive the new theme
* as a string.
*
* @param listener A Consumer that accepts a String representing the new theme (e.g.,
* "standard", "blackwhite", "glass").
*/
public void setThemeChangeListener(Consumer<String> listener) {
this.themeChangeListener = listener;
}
/** Show the settings box by setting its visibility and managed properties to true. */
public void show() {
if (settingsBox == null) {
return;
}
settingsBox.setVisible(true);
settingsBox.setManaged(true);
}
/** Hide the settings box by setting its visibility and managed properties to false. */
public void hide() {
if (settingsBox == null) {
return;
}
settingsBox.setVisible(false);
settingsBox.setManaged(false);
}
/**
* Utility method to clamp a value between a minimum and maximum range.
*
* @param value The value to clamp.
* @param min The minimum allowed value.
* @param max The maximum allowed value.
* @return The clamped value, guaranteed to be between min and max.
*/
private double clamp(double value, double min, double max) {
return Math.max(min, Math.min(value, max));
}
}
@@ -1,10 +1,12 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
/** Main UI application for Casono. Loads the main FXML layout and sets up the stage. */
import ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager;
import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.stage.Stage;
/**
@@ -30,17 +32,14 @@ public class Casinomainui extends Application {
* @throws IOException If loading the FXML fails.
*/
public void start(Stage stage) throws IOException {
// Pre-load sounds to avoid delays on first play
SoundManager.getInstance().preloadSounds();
// If the launcher passed an address argument (ip:port), expose it as
// system properties so controllers can read it without embedding defaults.
var raw = getParameters().getRaw();
if (raw != null && raw.size() > 0) {
String arg = raw.get(0);
String[] parts = arg.split(":", 2);
if (parts.length == 2) {
System.setProperty("casono.server.host", parts[0]);
System.setProperty("casono.server.port", parts[1]);
}
}
var params = getParameters();
processServerParameters(params);
FXMLLoader fxmlLoader =
new FXMLLoader(getClass().getResource("/ui-structure/Casinomainui.fxml"));
Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT);
@@ -51,9 +50,38 @@ public class Casinomainui extends Application {
stage.getIcons().add(icon);
stage.setScene(scene);
stage.setFullScreen(true);
stage.setFullScreenExitHint("");
// Add F11 fullscreen toggle
scene.setOnKeyPressed(
event -> {
if (event.getCode() == KeyCode.F11) {
stage.setFullScreen(!stage.isFullScreen());
event.consume();
}
});
stage.show();
}
private void processServerParameters(Application.Parameters params) {
if (params == null) {
return;
}
var raw = params.getRaw();
if (raw == null || raw.isEmpty()) {
return;
}
String arg = raw.get(0);
String[] parts = arg.split(":", 2);
if (parts.length == 2) {
System.setProperty("casono.server.host", parts[0]);
System.setProperty("casono.server.port", parts[1]);
}
}
/**
* Main entry point for launching the application.
*
@@ -5,8 +5,10 @@ import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.HighscoreViewController;
import ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager;
import java.io.IOException;
import java.net.URL;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
@@ -25,12 +27,14 @@ import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** Controller for the Casono main UI lobby. Handles UI initialization and user actions. */
public class CasinomainuiController {
private static final Logger LOGGER = LogManager.getLogger(CasinomainuiController.class);
private static final Logger LOGGER =
LogManager.getLogger(CasinomainuiController.class.getSimpleName());
@FXML private AnchorPane rootPane;
@FXML private Label titleLabel;
@@ -48,6 +52,11 @@ public class CasinomainuiController {
private int nextButtonId = 1;
private LobbyClient lobbyClient;
private ChatController chatController;
private PauseTransition titleResetTransition;
private static final String DEFAULT_TITLE = "Casono";
private static final String BLOCKED_USERNAME_TITLE =
"Name change blocked while lobby is active";
/** Default constructor used by FXMLLoader. */
public CasinomainuiController() {
@@ -55,13 +64,13 @@ 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() {
titleLabel.setText("Casono");
titleLabel.setText(DEFAULT_TITLE);
subtitleLabel.setText("Texas Hold'em Poker");
logoView.setImage(new Image(getClass().getResource("/images/logo.png").toExternalForm()));
@@ -141,6 +150,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());
}
@@ -168,6 +178,7 @@ public class CasinomainuiController {
/** Handles the login button action. Validates input and calls LobbyClient.login(). */
@FXML
public void handleLoginButton() {
SoundManager.getInstance().playButtonClick();
String username = usernameField == null ? null : usernameField.getText();
if (username == null || username.isBlank()) {
showAlert("Please enter a username.");
@@ -217,6 +228,11 @@ public class CasinomainuiController {
loginButton.setText("Change Name");
}
} catch (RuntimeException e) {
if (containsProtocolError(e, "CANNOT_CHANGE_USERNAME_DURING_LOBBY")
|| containsProtocolError(e, "RENAME_CONFLICT")) {
showTemporaryTitleMessage(BLOCKED_USERNAME_TITLE);
return;
}
LOGGER.error("Change username failed: {}", e.getMessage());
showAlert("Change username failed: " + e.getMessage());
}
@@ -242,6 +258,20 @@ public class CasinomainuiController {
}
}
private void showTemporaryTitleMessage(String message) {
if (titleLabel == null) {
return;
}
titleLabel.setText(message);
if (titleResetTransition != null) {
titleResetTransition.stop();
}
titleResetTransition = new PauseTransition(Duration.seconds(2));
titleResetTransition.setOnFinished(event -> titleLabel.setText(DEFAULT_TITLE));
titleResetTransition.playFromStart();
}
/** Shows an alert dialog with the given message. */
private void showAlert(String message) {
Alert alert = new Alert(AlertType.INFORMATION);
@@ -257,6 +287,7 @@ public class CasinomainuiController {
/** Handles the exit button action to close the application. */
@FXML
public void handleexitbutton() {
SoundManager.getInstance().playButtonClick();
if (chatController != null) {
chatController.shutdown();
}
@@ -270,6 +301,7 @@ public class CasinomainuiController {
*/
@FXML
public void handleCreateLobbyButton() {
SoundManager.getInstance().playButtonClick();
if (translationManager.isFull()) {
LOGGER.warn("Grid is full! No more lobbies available.");
return;
@@ -1,8 +1,12 @@
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 ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -31,7 +35,8 @@ public class LobbyButtonGridManager {
private static final int MAX_BUTTONS = 8;
private static final int GUEST_ID_LENGTH = 8;
private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
private static final Logger LOGGER =
LogManager.getLogger(LobbyButtonGridManager.class.getSimpleName());
private static final String BUTTON_FALLBACK_IMAGE = "/images/lobbypictures/error.png";
@@ -41,6 +46,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();
@@ -358,6 +365,7 @@ public class LobbyButtonGridManager {
btn.setOnAction(
e -> {
SoundManager.getInstance().playButtonClick();
Integer targetLobbyId = translationManager.getLobbyIdForButton(buttonId);
if (targetLobbyId != null) {
@@ -553,14 +561,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 +575,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 +593,8 @@ public class LobbyButtonGridManager {
public LobbyClient getLobbyClient() {
return lobbyClient;
}
public void setChatController(ChatController chatController) {
this.chatController = chatController;
}
}
@@ -48,6 +48,9 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_statu
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby.JoinLobbyHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby.JoinLobbyParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby.JoinLobbyRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.leave_lobby.LeaveLobbyHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.leave_lobby.LeaveLobbyParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.leave_lobby.LeaveLobbyRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameRequest;
@@ -63,6 +66,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 +77,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;
@@ -102,7 +102,7 @@ public class ServerApp {
public static void start(String arg) {
int port = Integer.parseInt(arg);
Logger logger = LogManager.getLogger(ServerApp.class);
Logger logger = LogManager.getLogger(ServerApp.class.getSimpleName());
logger.info("Starting server at port {}", port);
EventBus eventBus = new EventBus();
@@ -141,37 +141,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);
@@ -325,6 +300,14 @@ public class ServerApp {
new JoinLobbyHandler(
responseDispatcher, context.lobbyManager(), userRegistry));
// LEAVE_LOBBY registration
parserDispatcher.register("LEAVE_LOBBY", new LeaveLobbyParser());
commandRouter.register(
LeaveLobbyRequest.class,
(CommandHandler<LeaveLobbyRequest>)
new LeaveLobbyHandler(
responseDispatcher, context.lobbyManager(), userRegistry));
// START_GAME registration
parserDispatcher.register("START_GAME", new StartGameParser());
commandRouter.register(
@@ -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,5 +1,6 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GamePhase;
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;
@@ -42,33 +43,31 @@ public class ChangeUsernameHandler extends CommandHandler<ChangeUsernameRequest>
public void execute(ChangeUsernameRequest request) {
Optional<User> user = userRegistry.getBySessionId(request.getSessionId());
if (user.isEmpty()) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(),
"USER_NOT_LOGGED_IN",
"This session is not associated with an active user."));
dispatchError(
request.getContext(),
"USER_NOT_LOGGED_IN",
"This session is not associated with an active user.");
return;
}
String newUsername = request.getUsername() == null ? "" : request.getUsername().trim();
if (newUsername.isEmpty() || !VALID_USERNAME.matcher(newUsername).matches()) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(),
"INVALID_USERNAME",
"Only letters, numbers, '_' and '-' are allowed."));
String newUsername = validateUsername(request);
if (newUsername == null) {
return;
}
User currentUser = user.get();
String oldUsername = currentUser.getName();
if (isUsernameChangeBlocked(request.getContext(), oldUsername)) {
return;
}
boolean changed = userRegistry.changeUsername(currentUser.getId(), newUsername);
if (!changed) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(),
"USERNAME_TAKEN",
"The requested username is already taken."));
dispatchError(
request.getContext(),
"USERNAME_TAKEN",
"The requested username is already taken.");
return;
}
@@ -76,11 +75,10 @@ public class ChangeUsernameHandler extends CommandHandler<ChangeUsernameRequest>
lobbyManager == null || lobbyManager.renamePlayer(oldUsername, newUsername);
if (!lobbySynced) {
userRegistry.changeUsername(currentUser.getId(), oldUsername);
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(),
"RENAME_CONFLICT",
"Could not update username in current lobby/game state."));
dispatchError(
request.getContext(),
"RENAME_CONFLICT",
"Could not update username in current lobby/game state.");
return;
}
@@ -109,4 +107,40 @@ public class ChangeUsernameHandler extends CommandHandler<ChangeUsernameRequest>
responseDispatcher.dispatch(ev);
}
}
private String validateUsername(ChangeUsernameRequest request) {
String newUsername = request.getUsername() == null ? "" : request.getUsername().trim();
if (newUsername.isEmpty() || !VALID_USERNAME.matcher(newUsername).matches()) {
dispatchError(
request.getContext(),
"INVALID_USERNAME",
"Only letters, numbers, '_' and '-' are allowed.");
return null;
}
return newUsername;
}
private boolean isUsernameChangeBlocked(RequestContext context, String oldUsername) {
if (lobbyManager == null) {
return false;
}
var lobby = lobbyManager.getLobbyByUsername(oldUsername);
boolean gameRunning =
lobby != null
&& lobby.getGameController() != null
&& lobby.getGameController().getState().getPhase() != GamePhase.FINISHED;
if (gameRunning && lobby.isPlayerActive(oldUsername)) {
dispatchError(
context,
"CANNOT_CHANGE_USERNAME_DURING_LOBBY",
"Name change blocked while lobby is active.");
return true;
}
return false;
}
private void dispatchError(RequestContext context, String code, String message) {
responseDispatcher.dispatch(new ErrorResponse(context, code, message));
}
}
@@ -1,9 +1,13 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet;
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.GameLobbyExistsCheck;
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
@@ -40,6 +44,8 @@ public class PlayerBetHandler extends CommandHandler<PlayerBetRequest> {
super(responseDispatcher);
this.userRegistry = userRegistry;
this.lobbyManager = lobbyManager;
addCheck(new UserLoggedInCheck(userRegistry));
addCheck(new GameLobbyExistsCheck(userRegistry, lobbyManager));
}
/**
@@ -58,8 +64,7 @@ public class PlayerBetHandler extends CommandHandler<PlayerBetRequest> {
return;
}
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
userRegistry.getBySessionId(request.getSessionId());
Optional<User> opt = userRegistry.getBySessionId(request.getSessionId());
if (opt.isEmpty()) {
responseDispatcher.dispatch(
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
@@ -69,22 +74,11 @@ public class PlayerBetHandler extends CommandHandler<PlayerBetRequest> {
String username = opt.get().getName();
Integer gameId = request.getGameId();
var lobby =
(gameId != null)
? lobbyManager.getLobby(LobbyId.of(gameId))
: lobbyManager.getLobbyByUsername(username);
if (lobby == null) {
if (gameId != null) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
} else {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
}
return;
Lobby lobby;
if (gameId != null) {
lobby = lobbyManager.getLobby(LobbyId.of(gameId));
} else {
lobby = lobbyManager.getLobbyByUsername(username);
}
GameController game = lobby.getGameController();
@@ -1,9 +1,13 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call;
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.GameLobbyExistsCheck;
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
@@ -36,6 +40,8 @@ public class PlayerCallHandler extends CommandHandler<PlayerCallRequest> {
super(responseDispatcher);
this.userRegistry = userRegistry;
this.lobbyManager = lobbyManager;
addCheck(new UserLoggedInCheck(userRegistry));
addCheck(new GameLobbyExistsCheck(userRegistry, lobbyManager));
}
/**
@@ -46,8 +52,7 @@ public class PlayerCallHandler extends CommandHandler<PlayerCallRequest> {
*/
@Override
public void execute(PlayerCallRequest request) {
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
userRegistry.getBySessionId(request.getSessionId());
Optional<User> opt = userRegistry.getBySessionId(request.getSessionId());
if (opt.isEmpty()) {
responseDispatcher.dispatch(
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
@@ -57,22 +62,11 @@ public class PlayerCallHandler extends CommandHandler<PlayerCallRequest> {
String username = opt.get().getName();
Integer gameId = request.getGameId();
var lobby =
(gameId != null)
? lobbyManager.getLobby(LobbyId.of(gameId))
: lobbyManager.getLobbyByUsername(username);
if (lobby == null) {
if (gameId != null) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
} else {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
}
return;
Lobby lobby;
if (gameId != null) {
lobby = lobbyManager.getLobby(LobbyId.of(gameId));
} else {
lobby = lobbyManager.getLobbyByUsername(username);
}
GameController game = lobby.getGameController();
@@ -1,9 +1,13 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold;
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.GameLobbyExistsCheck;
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
@@ -36,6 +40,8 @@ public class PlayerFoldHandler extends CommandHandler<PlayerFoldRequest> {
super(responseDispatcher);
this.userRegistry = userRegistry;
this.lobbyManager = lobbyManager;
addCheck(new UserLoggedInCheck(userRegistry));
addCheck(new GameLobbyExistsCheck(userRegistry, lobbyManager));
}
/**
@@ -46,8 +52,7 @@ public class PlayerFoldHandler extends CommandHandler<PlayerFoldRequest> {
*/
@Override
public void execute(PlayerFoldRequest request) {
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
userRegistry.getBySessionId(request.getSessionId());
Optional<User> opt = userRegistry.getBySessionId(request.getSessionId());
if (opt.isEmpty()) {
responseDispatcher.dispatch(
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
@@ -57,22 +62,11 @@ public class PlayerFoldHandler extends CommandHandler<PlayerFoldRequest> {
String username = opt.get().getName();
Integer gameId = request.getGameId();
var lobby =
(gameId != null)
? lobbyManager.getLobby(LobbyId.of(gameId))
: lobbyManager.getLobbyByUsername(username);
if (lobby == null) {
if (gameId != null) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
} else {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
}
return;
Lobby lobby;
if (gameId != null) {
lobby = lobbyManager.getLobby(LobbyId.of(gameId));
} else {
lobby = lobbyManager.getLobbyByUsername(username);
}
GameController game = lobby.getGameController();
@@ -10,9 +10,16 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
/** Handler for GET_GAME_STATE: returns pot, phase, community cards and per-player info. */
public class GetGameStateHandler extends CommandHandler<GetGameStateRequest> {
private static final Logger LOGGER = Logger.getLogger(GetGameStateHandler.class.getName());
private static final int FINISHED_CLEANUP_DELAY_SECONDS = 2;
private final LobbyManager lobbyManager;
private final UserRegistry userRegistry;
@@ -73,11 +80,12 @@ public class GetGameStateHandler extends CommandHandler<GetGameStateRequest> {
return;
}
if (game.getState().getPhase() == GamePhase.FINISHED) {
cleanupLobby(lobby, lobbyId);
}
responseDispatcher.dispatch(new GetGameStateResponse(request.getContext(), game, username));
if (game.getState().getPhase() == GamePhase.FINISHED) {
CompletableFuture.delayedExecutor(FINISHED_CLEANUP_DELAY_SECONDS, TimeUnit.SECONDS)
.execute(() -> cleanupLobby(lobby, lobbyId));
}
}
/**
@@ -85,15 +93,25 @@ 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
// Remove all active players
for (String playerName : lobby.getPlayerNames()) {
lobbyManager.removePlayer(playerName);
}
// Reset the game controller so the lobby returns to CREATED state
// Also remove all absent players so the lobby can be deleted
for (String playerName : lobby.getAbsentPlayers()) {
lobbyManager.removePlayer(playerName);
}
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);
}
}
@@ -6,6 +6,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Rank;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Suit;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.Player;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.showdown.CardsSpeakRule;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GamePhase;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.highscore.HighscoreService;
@@ -13,8 +14,11 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestCo
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.builder.ResponseBodyBuilder;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* Response carrying a snapshot of the current game state using the project's wire format.
@@ -51,10 +55,16 @@ public class GetGameStateResponse extends SuccessResponse {
builder.param("CURRENT_BET", computeGlobalCurrentBet(state));
builder.param("DEALER", state.getDealerIndex());
builder.param("ACTIVE_PLAYER", state.getCurrentPlayerIndex());
int winnerIndex = computeWinnerIndex(state, game);
builder.param("WINNER", winnerIndex);
appendWinnerToHighscoresIfFinished(state, winnerIndex);
List<String> winnerNames = computeWinnerNames(state, game);
for (String name : winnerNames) {
builder.param("WINNER_NAME", name);
}
appendWinnerToHighscoresIfFinished(state, winnerNames);
appendHighscoreEntries(builder);
appendCommunityCards(builder, state);
@@ -64,17 +74,37 @@ public class GetGameStateResponse extends SuccessResponse {
return builder.build();
}
private static void appendWinnerToHighscoresIfFinished(GameState state, int winnerIndex) {
if (winnerIndex < 0 || state.getPhase() != GamePhase.FINISHED) {
private static void appendWinnerToHighscoresIfFinished(
GameState state, List<String> winnerNames) {
if (winnerNames == null || winnerNames.isEmpty()) {
return;
}
Player winner = findPlayerByIndex(state, winnerIndex);
if (winner == null || winner.getId() == null || winner.getId().value() == null) {
GamePhase phase = state.getPhase();
if (phase != GamePhase.SHOWDOWN && phase != GamePhase.FINISHED) {
return;
}
HighscoreService.getInstance().appendWinner(winner.getId().value());
if (!state.tryMarkWinnerPersistedForHand()) {
return;
}
// Keep ordering stable and persist tied winners as a single shared highscore
// entry. Compute the per-winner share here (pot is split equally among
// winners)
int potPerWinner = computePotPerWinner(state, winnerNames.size());
Set<String> uniqueWinners = new LinkedHashSet<>(winnerNames);
List<String> formattedWinners = new ArrayList<>();
for (String name : uniqueWinners) {
if (name != null && !name.isBlank()) {
formattedWinners.add(name + " (" + Math.max(0, potPerWinner) + ")");
}
}
if (!formattedWinners.isEmpty()) {
HighscoreService.getInstance()
.appendFormattedEntry(String.join(", ", formattedWinners));
}
}
private static Player findPlayerByIndex(GameState state, int winnerIndex) {
@@ -116,6 +146,33 @@ public class GetGameStateResponse extends SuccessResponse {
return -1;
}
/** Computes the list of all winner names at showdown/finished phase. */
private static List<String> computeWinnerNames(GameState state, GameController game) {
GamePhase phase = state.getPhase();
if (phase != GamePhase.SHOWDOWN && phase != GamePhase.FINISHED) {
return new ArrayList<>();
}
CardsSpeakRule showdown = new CardsSpeakRule();
List<Player> winners = showdown.determineWinners(state);
List<String> winnerNames = new ArrayList<>();
for (Player winner : winners) {
if (winner != null && winner.getId() != null && winner.getId().value() != null) {
winnerNames.add(winner.getId().value());
}
}
return winnerNames;
}
/** Computes the pot share per winner. */
private static int computePotPerWinner(GameState state, int numWinners) {
if (numWinners <= 0 || state.getPot() == null) {
return 0;
}
return state.getPot().getAmount() / numWinners;
}
private static int computeGlobalCurrentBet(GameState state) {
int globalCurrentBet = 0;
for (Player p : state.getPlayers()) {
@@ -1,9 +1,13 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise;
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.GameLobbyExistsCheck;
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
@@ -40,6 +44,8 @@ public class PlayerRaiseHandler extends CommandHandler<PlayerRaiseRequest> {
super(responseDispatcher);
this.userRegistry = userRegistry;
this.lobbyManager = lobbyManager;
addCheck(new UserLoggedInCheck(userRegistry));
addCheck(new GameLobbyExistsCheck(userRegistry, lobbyManager));
}
/**
@@ -60,8 +66,7 @@ public class PlayerRaiseHandler extends CommandHandler<PlayerRaiseRequest> {
return;
}
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
userRegistry.getBySessionId(request.getSessionId());
Optional<User> opt = userRegistry.getBySessionId(request.getSessionId());
if (opt.isEmpty()) {
responseDispatcher.dispatch(
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
@@ -71,22 +76,11 @@ public class PlayerRaiseHandler extends CommandHandler<PlayerRaiseRequest> {
String username = opt.get().getName();
Integer gameId = request.getGameId();
var lobby =
(gameId != null)
? lobbyManager.getLobby(LobbyId.of(gameId))
: lobbyManager.getLobbyByUsername(username);
if (lobby == null) {
if (gameId != null) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
} else {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
}
return;
Lobby lobby;
if (gameId != null) {
lobby = lobbyManager.getLobby(LobbyId.of(gameId));
} else {
lobby = lobbyManager.getLobbyByUsername(username);
}
GameController game = lobby.getGameController();
@@ -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));
}
}
@@ -23,7 +23,8 @@ import org.apache.logging.log4j.Logger;
public class JoinLobbyHandler extends CommandHandler<JoinLobbyRequest> {
private final LobbyManager lobbyManager;
private final UserRegistry userRegistry;
private static final Logger LOGGER = LogManager.getLogger(JoinLobbyHandler.class);
private static final Logger LOGGER =
LogManager.getLogger(JoinLobbyHandler.class.getSimpleName());
/**
* Create a new {@link JoinLobbyHandler}.
@@ -0,0 +1,116 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.leave_lobby;
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck;
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;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Handler for the `LEAVE_LOBBY` command.
*
* <p>Resolves the username from the session (requires {@link UserLoggedInCheck}), looks up the
* target lobby and marks the user as absent. The user can rejoin later without losing their slot.
* On success an `+OK` response is dispatched; on failure an appropriate {@link ErrorResponse} is
* returned.
*/
public class LeaveLobbyHandler extends CommandHandler<LeaveLobbyRequest> {
private final LobbyManager lobbyManager;
private final UserRegistry userRegistry;
private static final Logger LOGGER =
LogManager.getLogger(LeaveLobbyHandler.class.getSimpleName());
/**
* Create a new {@link LeaveLobbyHandler}.
*
* @param responseDispatcher dispatcher used to send responses back to the client
* @param lobbyManager manager providing lobby state and operations
* @param userRegistry registry to resolve session -> user mappings
*/
public LeaveLobbyHandler(
ResponseDispatcher responseDispatcher,
LobbyManager lobbyManager,
UserRegistry userRegistry) {
super(responseDispatcher);
this.lobbyManager = lobbyManager;
this.userRegistry = userRegistry;
addCheck(new UserLoggedInCheck(userRegistry));
}
/**
* Execute the leave-lobby request. Marks the player as absent in the lobby.
*
* @param request the parsed {@link LeaveLobbyRequest}
*/
@Override
public void execute(LeaveLobbyRequest request) {
LOGGER.info(
"LEAVE_LOBBY request: session={}, lobbyId={}",
request.getContext().sessionId(),
request.getId());
LobbyId lid = LobbyId.of(request.getId());
var lobby = lobbyManager.getLobby(lid);
if (lobby == null) {
LOGGER.warn(
"LEAVE_LOBBY: Lobby {} not found (session={})",
request.getId(),
request.getContext().sessionId());
responseDispatcher.dispatch(
new ErrorResponse(request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
return;
}
// Only allow leaving if game is running
if (lobby.getGameController() == null) {
LOGGER.warn(
"LEAVE_LOBBY: Game not running in lobby {} (session={})",
request.getId(),
request.getContext().sessionId());
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(),
"GAME_NOT_RUNNING",
"Can only leave a lobby while a game is running"));
return;
}
var maybeUser = userRegistry.getBySessionId(request.getContext().sessionId());
if (maybeUser.isEmpty()) {
LOGGER.warn(
"LEAVE_LOBBY: No user associated with session {}",
request.getContext().sessionId());
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(),
"USER_NOT_LOGGED_IN",
"No user associated with session"));
return;
}
User user = maybeUser.get();
String username = user.getName();
boolean ok = lobbyManager.leavePlayerFromLobby(username, lid);
if (!ok) {
LOGGER.warn(
"LEAVE_LOBBY: User '{}' failed to leave lobby {} (not in lobby or not active)",
username,
request.getId());
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(),
"NOT_IN_LOBBY",
"You are not in this lobby or not actively playing"));
return;
}
LOGGER.info("User '{}' left lobby {} (marked absent)", username, request.getId());
responseDispatcher.dispatch(new OkResponse(request.getContext()));
}
}
@@ -0,0 +1,33 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.leave_lobby;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
/**
* Parser for the `LEAVE_LOBBY` command.
*
* <p>Expected request parameters:
*
* <ul>
* <li>`ID` (int) — numeric lobby identifier (required)
* </ul>
*
* <p>The parser builds a {@link LeaveLobbyRequest} containing the parsed lobby id and the original
* request context.
*/
public class LeaveLobbyParser implements CommandParser<LeaveLobbyRequest> {
/**
* Parse the incoming primitive request into a {@link LeaveLobbyRequest}.
*
* @param primitiveRequest the raw primitive request
* @return a {@link LeaveLobbyRequest} with the parsed lobby id and context
*/
@Override
public LeaveLobbyRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor =
new RequestParameterAccessor(primitiveRequest.parameters());
int id = accessor.require("ID", Integer::parseInt);
return new LeaveLobbyRequest(primitiveRequest.context(), id);
}
}
@@ -0,0 +1,34 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.leave_lobby;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
/**
* Request data for the `LEAVE_LOBBY` command.
*
* <p>Contains the lobby id the client wants to leave and inherits the {@link Request} contextual
* information.
*/
public class LeaveLobbyRequest extends Request {
private final int id;
/**
* Create a new {@link LeaveLobbyRequest}.
*
* @param context the request context
* @param id numeric lobby id to leave
*/
public LeaveLobbyRequest(RequestContext context, int id) {
super(context);
this.id = id;
}
/**
* Returns the lobby id requested by the client.
*
* @return numeric lobby id
*/
public int getId() {
return id;
}
}
@@ -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();
}
@@ -30,6 +30,8 @@ public class GameController {
private int dealerIndex = 0;
private volatile Runnable onGameEndedCallback;
private static final int DEALER_OFFSET = 1;
private static final int SMALL_BLIND_OFFSET = 1;
private static final int BIG_BLIND_OFFSET = 2;
@@ -45,6 +47,11 @@ public class GameController {
this.engine = engine;
}
/** Set a callback to be invoked when the game ends. */
public void setOnGameEndedCallback(Runnable callback) {
this.onGameEndedCallback = callback;
}
/**
* Adds a player to the game with the specified name and initial chip count.
*
@@ -304,5 +311,12 @@ public class GameController {
*/
public void endGame() {
engine.getState().setPhase(GamePhase.FINISHED);
if (onGameEndedCallback != null) {
try {
onGameEndedCallback.run();
} catch (RuntimeException e) {
// Log or handle callback errors
}
}
}
}
@@ -2,6 +2,7 @@ package ch.unibas.dmi.dbis.cs108.casono.server.domain.game.evaluator;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Card;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Suit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
@@ -40,6 +41,9 @@ public class HandEvaluator {
private static final int START_INDEX = 1;
private static final int CARD_DIFFERENCE = 1;
private static final int PREVIOUS_INDEX_OFFSET = -1;
private static final int TWO_KICKERS = 2;
private static final int ONE_KICKER = 1;
private static final int THREE_KICKERS = 3;
/**
* Evaluates a list of cards and determines the best possible hand rank.
@@ -62,7 +66,7 @@ public class HandEvaluator {
return straightFlush;
}
HandRank fourKind = checkFourOfAKind(rankCount);
HandRank fourKind = checkFourOfAKind(rankCount, ranks);
if (fourKind != null) {
return fourKind;
}
@@ -80,17 +84,17 @@ public class HandEvaluator {
return new HandRank(HandRank.Type.STRAIGHT, ranks);
}
HandRank threeKind = checkThreeOfAKind(rankCount);
HandRank threeKind = checkThreeOfAKind(rankCount, ranks);
if (threeKind != null) {
return threeKind;
}
HandRank twoPair = checkTwoPair(rankCount);
HandRank twoPair = checkTwoPair(rankCount, ranks);
if (twoPair != null) {
return twoPair;
}
HandRank onePair = checkOnePair(rankCount);
HandRank onePair = checkOnePair(rankCount, ranks);
if (onePair != null) {
return onePair;
}
@@ -200,20 +204,26 @@ public class HandEvaluator {
}
/**
* Helper method to check for a four of a kind hand rank.
* Helper method to check for a four of a kind hand rank. Stores the quad rank and 1 kicker for
* tie-breaking.
*
* @param rankCount A map where the key is the card rank and the value is the count of
* occurrences.
* @param allRanks A sorted list of all card ranks in the hand.
* @return A HandRank object representing the four of a kind hand rank, or null if not found.
*/
private static HandRank checkFourOfAKind(Map<Integer, Long> rankCount) {
private static HandRank checkFourOfAKind(Map<Integer, Long> rankCount, List<Integer> allRanks) {
if (!rankCount.containsValue(FOUR_OF_A_KIND_COUNT)) {
return null;
}
int quad = getRank(rankCount, FOUR_OF_A_KIND);
List<Integer> kickers = getKickers(allRanks, List.of(quad), ONE_KICKER);
return new HandRank(HandRank.Type.FOUR_OF_A_KIND, List.of(quad));
List<Integer> result = new ArrayList<>(List.of(quad));
result.addAll(kickers);
return new HandRank(HandRank.Type.FOUR_OF_A_KIND, result);
}
/**
@@ -255,30 +265,39 @@ public class HandEvaluator {
}
/**
* Helper method to check for a three of a kind hand rank.
* Helper method to check for a three of a kind hand rank. Stores the trips rank and 2 kickers
* for tie-breaking.
*
* @param rankCount A map where the key is the card rank and the value is the count of
* occurrences.
* @param allRanks A sorted list of all card ranks in the hand.
* @return A HandRank object representing the three of a kind hand rank, or null if not found.
*/
private static HandRank checkThreeOfAKind(Map<Integer, Long> rankCount) {
private static HandRank checkThreeOfAKind(
Map<Integer, Long> rankCount, List<Integer> allRanks) {
if (!rankCount.containsValue(THREE_OF_A_KIND_COUNT)) {
return null;
}
int tripsRank = getRank(rankCount, THREE_OF_A_KIND_RANK);
List<Integer> kickers = getKickers(allRanks, List.of(tripsRank), TWO_KICKERS);
return new HandRank(HandRank.Type.THREE_OF_A_KIND, List.of(tripsRank));
List<Integer> result = new ArrayList<>(List.of(tripsRank));
result.addAll(kickers);
return new HandRank(HandRank.Type.THREE_OF_A_KIND, result);
}
/**
* Helper method to check for a two pair hand rank.
* Helper method to check for a two pair hand rank. Stores the two pair ranks and 1 kicker for
* tie-breaking. The kicker is the highest remaining card that is not part of either pair.
*
* @param rankCount A map where the key is the card rank and the value is the count of
* occurrences.
* @param allRanks A sorted list of all card ranks in the hand.
* @return A HandRank object representing the two pair hand rank, or null if not found.
*/
private static HandRank checkTwoPair(Map<Integer, Long> rankCount) {
private static HandRank checkTwoPair(Map<Integer, Long> rankCount, List<Integer> allRanks) {
long pairCount = rankCount.values().stream().filter(v -> v == PAIR).count();
@@ -294,17 +313,24 @@ public class HandEvaluator {
.limit(PAIR)
.toList();
return new HandRank(HandRank.Type.TWO_PAIR, pairRanks);
List<Integer> kickers = getKickers(allRanks, pairRanks, ONE_KICKER);
List<Integer> result = new ArrayList<>(pairRanks);
result.addAll(kickers);
return new HandRank(HandRank.Type.TWO_PAIR, result);
}
/**
* Helper method to check for a one pair hand rank.
* Helper method to check for a one pair hand rank. Stores the pair rank and 3 kickers for
* tie-breaking.
*
* @param rankCount A map where the key is the card rank and the value is the count of
* occurrences.
* @param allRanks A sorted list of all card ranks in the hand.
* @return A HandRank object representing the one pair hand rank, or null if not found.
*/
private static HandRank checkOnePair(Map<Integer, Long> rankCount) {
private static HandRank checkOnePair(Map<Integer, Long> rankCount, List<Integer> allRanks) {
long pairCount = rankCount.values().stream().filter(v -> v == PAIR).count();
@@ -313,8 +339,12 @@ public class HandEvaluator {
}
int pair = getRank(rankCount, PAIR);
List<Integer> kickers = getKickers(allRanks, List.of(pair), THREE_KICKERS);
return new HandRank(HandRank.Type.ONE_PAIR, List.of(pair));
List<Integer> result = new ArrayList<>(List.of(pair));
result.addAll(kickers);
return new HandRank(HandRank.Type.ONE_PAIR, result);
}
/**
@@ -333,6 +363,26 @@ public class HandEvaluator {
.orElse(DEFAULT_VALUE);
}
/**
* Helper method to extract kickers from the list of all card ranks. Excludes the ranks already
* used in the main hand combination.
*
* @param allRanks A sorted list of all card ranks in the hand.
* @param excludeRanks A list of ranks to exclude (e.g., ranks used in pairs, trips, etc.).
* @param numKickers The number of kickers to extract.
* @return A list of kicker ranks, sorted in descending order.
*/
private static List<Integer> getKickers(
List<Integer> allRanks, List<Integer> excludeRanks, int numKickers) {
Set<Integer> excludeSet = new HashSet<>(excludeRanks);
return allRanks.stream()
.filter(rank -> !excludeSet.contains(rank))
.limit(numKickers)
.toList();
}
/**
* Helper method to determine if a list of card ranks forms a straight.
*
@@ -5,7 +5,6 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Card;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.evaluator.HandEvaluator;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.evaluator.HandRank;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.Player;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerStatus;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.Rule;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
import java.util.ArrayList;
@@ -43,14 +42,28 @@ public class CardsSpeakRule implements Rule {
*/
public Player determineWinner(GameState state) {
List<Player> winners = determineWinners(state);
return winners.isEmpty() ? null : winners.get(0);
}
/**
* Determines all players that share the best hand rank.
*
* @param state The current state of the game.
* @return The list of winners, ordered by appearance in the state.
*/
public List<Player> determineWinners(GameState state) {
List<Player> players = new ArrayList<>(state.getPlayers());
Player bestPlayer = null;
HandRank bestRank = null;
List<Player> winners = new ArrayList<>();
for (Player player : players) {
if (player.getStatus() == PlayerStatus.FOLDED) {
// Use folded flag from game flow to stay consistent with winner calculation in
// controller.
if (player.isFolded()) {
continue;
}
@@ -62,13 +75,15 @@ public class CardsSpeakRule implements Rule {
HandRank rank = HandEvaluator.evaluate(cards);
if (bestRank == null || rank.compareTo(bestRank) > 0) {
bestRank = rank;
bestPlayer = player;
winners.clear();
winners.add(player);
} else if (rank.compareTo(bestRank) == 0) {
winners.add(player);
}
}
return bestPlayer;
return winners;
}
/**
@@ -81,15 +96,23 @@ public class CardsSpeakRule implements Rule {
*/
public void awardPot(GameState state) {
Player winner = determineWinner(state);
List<Player> winners = determineWinners(state);
if (winner == null) {
if (winners.isEmpty()) {
return;
}
int pot = state.getPot().getAmount();
int share = winners.isEmpty() ? 0 : pot / winners.size();
int remainder = winners.isEmpty() ? 0 : pot % winners.size();
winner.addChips(pot);
for (int i = 0; i < winners.size(); i++) {
Player winner = winners.get(i);
if (winner == null) {
continue;
}
winner.addChips(share + (i < remainder ? 1 : 0));
}
state.getPot().reset();
}
@@ -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);
}
@@ -18,6 +18,7 @@ public final class HighscoreService {
private static final DateTimeFormatter DISPLAY_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
private static final int MAX_RETURNED_ENTRIES = 100;
private static final int MIN_MONEY_COLUMN_COUNT = 3;
private static final String LINE_SEPARATOR = "\t";
private final Path storagePath;
@@ -32,6 +33,11 @@ public final class HighscoreService {
}
public synchronized void appendWinner(String winnerName) {
appendWinner(winnerName, 0);
}
/** Append a winner with an optional money value (money may be 0). */
public synchronized void appendWinner(String winnerName, int money) {
if (winnerName == null) {
return;
}
@@ -41,6 +47,44 @@ public final class HighscoreService {
return;
}
String line;
if (money > 0) {
line =
Instant.now()
+ LINE_SEPARATOR
+ sanitized
+ LINE_SEPARATOR
+ money
+ System.lineSeparator();
} else {
// Keep legacy two-column format for backward compatibility
line = Instant.now() + LINE_SEPARATOR + sanitized + System.lineSeparator();
}
try {
Files.createDirectories(storagePath.getParent());
Files.writeString(
storagePath,
line,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
} catch (IOException ignored) {
// Highscore persistence must never break the game state response path.
}
}
/** Append a preformatted highscore entry such as "Lars (1000), Jona (1000)". */
public synchronized void appendFormattedEntry(String entryText) {
if (entryText == null) {
return;
}
String sanitized = sanitizeName(entryText);
if (sanitized.isEmpty()) {
return;
}
String line = Instant.now() + LINE_SEPARATOR + sanitized + System.lineSeparator();
try {
@@ -74,15 +118,25 @@ public final class HighscoreService {
if (raw == null || raw.isBlank()) {
continue;
}
String[] parts = raw.split(LINE_SEPARATOR, 2);
// Support both legacy format (TIMESTAMP \t NAME) and new format (TIMESTAMP \t
// NAME \t MONEY)
String[] parts = raw.split(LINE_SEPARATOR);
if (parts.length < 2) {
continue;
}
try {
Instant ts = Instant.parse(parts[0].trim());
String display = DISPLAY_FORMATTER.format(ts) + " | " + parts[1].trim();
String name = parts[1].trim();
String display = DISPLAY_FORMATTER.format(ts) + " | " + name;
if (parts.length >= MIN_MONEY_COLUMN_COUNT) {
try {
int money = Integer.parseInt(parts[2].trim());
display = display + " | $" + money;
} catch (NumberFormatException nfe) {
// ignore malformed money
}
}
formatted.add(display);
} catch (Exception ignored) {
// Skip malformed lines and keep all valid entries.
@@ -9,6 +9,8 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.RuleEngine;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -26,7 +28,9 @@ public class Lobby {
private final LobbyId id;
private final String name;
private final List<String> playerNames = new CopyOnWriteArrayList<>();
private final Set<String> absentPlayers = ConcurrentHashMap.newKeySet();
private volatile GameController gameController;
private volatile Runnable onGameEndedCallback;
public Lobby(LobbyId id, String name) {
this.id = id;
@@ -96,12 +100,91 @@ public class Lobby {
public void initGame(GameController controller) {
this.gameController = controller;
if (controller != null && onGameEndedCallback != null) {
controller.setOnGameEndedCallback(onGameEndedCallback);
}
}
/**
* Set a callback to be invoked when the game in this lobby ends. This will be propagated to the
* game controller when a game is started.
*/
public void setOnGameEndedCallback(Runnable callback) {
this.onGameEndedCallback = callback;
if (gameController != null) {
gameController.setOnGameEndedCallback(callback);
}
}
public GameController getGameController() {
return gameController;
}
/**
* Check if a player is currently active (not absent) in the lobby.
*
* @param playerName the player to check
* @return true if player is in the active player list
*/
public boolean isPlayerActive(String playerName) {
return playerNames.contains(playerName);
}
/**
* Mark a player as absent (left the lobby) while the game is still running. The player remains
* in the lobby's mappings but is moved to the absent set.
*
* @param playerName the player to mark as absent
* @return true if the player was active and is now absent
*/
public boolean leavePlayer(String playerName) {
if (playerName == null) {
return false;
}
boolean removed = playerNames.remove(playerName);
if (removed) {
absentPlayers.add(playerName);
}
return removed;
}
/**
* Try to restore an absent player back to active. Returns true if player was absent and is now
* active again.
*
* @param playerName the player to restore
* @return true if player was restored from absent
*/
public boolean restoreAbsentPlayer(String playerName) {
if (playerName == null) {
return false;
}
boolean wasAbsent = absentPlayers.remove(playerName);
if (wasAbsent && !playerNames.contains(playerName)) {
playerNames.add(playerName);
return true;
}
return false;
}
/**
* Get the set of absent (gone but not removed) players in this lobby.
*
* @return copy of absent players set
*/
public Set<String> getAbsentPlayers() {
return Set.copyOf(absentPlayers);
}
/**
* Check if this lobby has any players (active or absent).
*
* @return true if playerNames or absentPlayers is non-empty
*/
public boolean hasAnyPlayers() {
return !playerNames.isEmpty() || !absentPlayers.isEmpty();
}
/**
* Atomically add a player and, if the lobby reached {@code autoStartPlayers} and no game exists
* yet, create and start a game. The operation is synchronized on the internal player list to
@@ -139,6 +222,11 @@ public class Lobby {
game.addPlayer(PlayerId.of(p), defaultStartChips);
}
// Set the callback before starting the game
if (onGameEndedCallback != null) {
game.setOnGameEndedCallback(onGameEndedCallback);
}
game.startGame();
this.gameController = game;
LOGGER.info(() -> "Auto-started game in lobby " + id.value());
@@ -0,0 +1,101 @@
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.getSimpleName());
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);
}
}
}
}
@@ -4,4 +4,7 @@ package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
public interface LobbyEventListener {
/** Called when a game is automatically started in the given lobby. */
void onGameStarted(LobbyId lobbyId);
/** Called when a game in the given lobby ends (phase set to FINISHED). */
void onGameEnded(LobbyId lobbyId);
}
@@ -71,6 +71,22 @@ public class LobbyManager {
if (lobby == null) {
return false;
}
// Check if player was absent and try to restore
if (lobby.getAbsentPlayers().contains(username)) {
boolean restored = lobby.restoreAbsentPlayer(username);
if (restored) {
LOGGER.info(
() ->
"User '"
+ username
+ "' rejoined absent slot in lobby "
+ lobbyId.value());
playerToLobby.put(username, lobbyId);
return true;
}
}
AddResult result =
lobby.addPlayerAndMaybeStart(
username, maxPlayersPerLobby, AUTO_START_PLAYERS, DEFAULT_START_CHIPS);
@@ -94,6 +110,8 @@ public class LobbyManager {
+ AUTO_START_PLAYERS
+ " players; game started.");
notifyGameStarted(lobbyId);
// Set the game-ended callback
notifySetGameEndedCallback(lobbyId);
}
return true;
}
@@ -121,6 +139,29 @@ public class LobbyManager {
}
}
private void notifySetGameEndedCallback(LobbyId lobbyId) {
Lobby lobby = activeLobbies.get(lobbyId);
if (lobby != null) {
lobby.setOnGameEndedCallback(
() -> {
notifyGameEnded(lobbyId);
});
}
}
private void notifyGameEnded(LobbyId lobbyId) {
for (LobbyEventListener l : listeners) {
try {
l.onGameEnded(lobbyId);
} catch (RuntimeException e) {
LOGGER.warning(
() ->
"Listener threw while handling game-end for lobby "
+ lobbyId.value());
}
}
}
public boolean removePlayer(String username) {
LobbyId id = playerToLobby.remove(username);
if (id == null) {
@@ -138,6 +179,33 @@ public class LobbyManager {
return removed;
}
/**
* Mark a player as absent (left the lobby during a running game). The player is not removed but
* moved to the absent set, allowing them to rejoin later without losing their slot.
*
* @param username the player to mark as absent
* @param lobbyId the lobby they're leaving
* @return true if the player was marked absent successfully
*/
public boolean leavePlayerFromLobby(String username, LobbyId lobbyId) {
Lobby lobby = getLobby(lobbyId);
if (lobby == null) {
return false;
}
boolean left = lobby.leavePlayer(username);
if (left) {
LOGGER.info(
() ->
"User '"
+ username
+ "' left lobby "
+ lobbyId.value()
+ " (marked absent)");
// Keep the playerToLobby mapping intact for rejoin
}
return left;
}
public Collection<Lobby> getAllLobbies() {
return activeLobbies.values();
}
@@ -149,7 +217,7 @@ public class LobbyManager {
for (Map.Entry<LobbyId, Lobby> e : activeLobbies.entrySet()) {
LobbyId id = e.getKey();
Lobby l = e.getValue();
if (l.getPlayerNames().isEmpty()) {
if (!l.hasAnyPlayers()) {
Instant created = creationTimes.get(id);
if (created != null && created.isBefore(cutoff)) {
result.add(id);
@@ -169,6 +237,9 @@ public class LobbyManager {
for (String username : removed.getPlayerNames()) {
playerToLobby.remove(username);
}
for (String username : removed.getAbsentPlayers()) {
playerToLobby.remove(username);
}
}
/**
@@ -15,7 +15,7 @@ public class UserCleanupJob implements Runnable {
private final Duration reconnectThreshold;
public UserCleanupJob(UserRegistry registry, Duration reconnectThreshold) {
this.logger = LogManager.getLogger(UserCleanupJob.class);
this.logger = LogManager.getLogger(UserCleanupJob.class.getSimpleName());
this.registry = registry;
this.reconnectThreshold = reconnectThreshold;
}

Some files were not shown because too many files have changed in this diff Show More