Compare commits

...

569 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 028c3adb0d Merge branch 'chore/remove-unused-imports-lobby-ui' into 'main'
Chore: Remove unused import from lobby ui code

See merge request cs108-fs26/Gruppe-13!139
2026-04-21 18:50:38 +00:00
Jona Walpert 19b555fb47 Chore: Remove unused import from lobby ui code 2026-04-21 20:48:03 +02:00
Jona Walpert 8a7fad11a9 Merge branch 'docs/highscore-protocol' into 'main'
docs: add highscore protocol commands (Issue #110)

Closes #110

See merge request cs108-fs26/Gruppe-13!138
2026-04-21 18:43:34 +00:00
Jona Walpert c0a7cadf7e docs: add highscore protocol commands (Issue #110) 2026-04-21 20:40:52 +02:00
Jona Walpert 2fd07bbf60 Merge branch 'feat/highscore-window-ui' into 'main'
Feat: Add highscore popup UI and lobby display

Closes #110

See merge request cs108-fs26/Gruppe-13!137
2026-04-21 18:35:40 +00:00
Jona Walpert 15c998d549 Feat: Delete window header and white background. Add mouse dragging ability to Highscore popup 2026-04-21 20:31:52 +02:00
Jona Walpert 062e66cdf9 Feat: Add highscore popup UI and lobby display
Highscore button in GameUI replaces "Info" Button wich was unused
(Issue #110)
2026-04-21 20:13:13 +02:00
Jona Walpert 7674ae57ad Merge branch 'feat/highscore-client-protocol' into 'main'
Feat: Add highscore client protocol integration (Issue #110)

See merge request cs108-fs26/Gruppe-13!136
2026-04-21 16:04:04 +00:00
Jona Walpert 5e79b45d90 Feat: Add highscore client protocol integration (Issue #110) 2026-04-21 17:58:49 +02:00
Jona Walpert 45411c4087 Merge branch 'feat/highscore-server-storage' into 'main'
Feat: Add GET_HIGHSCORES and CLEAR_HIGHSCORES server commands (Issue #110)

See merge request cs108-fs26/Gruppe-13!135
2026-04-21 15:52:06 +00:00
Jona Walpert cc7f8a8ef3 Feat: Add GET_HIGHSCORES and CLEAR_HIGHSCORES server commands (Issue #110) 2026-04-21 17:42:38 +02:00
Julian Kropff d3a00aebc0 Merge branch 'fix/game-turn-and-action-bugs' into 'main'
Fix: improve round management, betting validation and game engine test coverage

Closes #118

See merge request cs108-fs26/Gruppe-13!134
2026-04-19 18:31:45 +00:00
Julian Kropff ab2adbf311 Fix: resolve checkstyle magic number and apply spotless formatting 2026-04-19 20:20:57 +02:00
Julian Kropff 8099228d4f Test: improve GameEngine tests and add testPreflopEndsAfterOneActionPerActivePlayer and
testPreflopCountsOnlyNonFoldedPlayersForProgress
Refs #118
2026-04-19 20:11:21 +02:00
Julian Kropff ac83d63ea1 Fix: align RoundManager with Round logic fixes
Refs #118
2026-04-19 20:02:43 +02:00
Julian Kropff 3443b5e8f3 Fix: align TaskbarController with Round logic fixes
Refs #118
2026-04-19 20:02:02 +02:00
Julian Kropff ad7086e5a5 Fix: implement folding in GameController
Refs #118
2026-04-19 19:38:37 +02:00
Julian Kropff e39cbb4357 Fix: adjust GameState logic
Refs #118
2026-04-19 19:35:57 +02:00
Julian Kropff 238a156d91 Fix: round transition logic in RoundManager
Refs #118
2026-04-19 19:28:13 +02:00
Julian Kropff 621843167e Fix: Improve handleAction logic in GameEngine
Refs #118
2026-04-19 19:21:44 +02:00
Julian Kropff 215e7e2cae Fix: Improved validation for betting, calling, raising and folding in TaskbarController.
Prevents incorrect user input during gameplay.
Refs #118
2026-04-19 19:19:11 +02:00
Julian Kropff 03daf9c3b8 Fix: updateTaskbar in CasinoGameController
Refs #118
2026-04-19 18:51:25 +02:00
Julian Kropff d23a00e148 Fix: disable context-dependent buttons in taskbar 2026-04-16 21:18:42 +02:00
JK a5f0735a08 Merge branch 'main' into fix/game-turn-and-action-bugs 2026-04-16 13:41:46 +02:00
Jona Walpert 4855e937e6 Merge branch 'chore/clean-up-command-registry-imports' into 'main'
Chore: fix long imports to make code more readable

See merge request cs108-fs26/Gruppe-13!133
2026-04-16 08:34:39 +00:00
Jona Walpert 007e863878 Chore: fix long imports to make code more readable 2026-04-16 10:25:57 +02:00
Jona Walpert 56aa3c0215 Merge branch 'integration/116-change-username' into 'main'
Feat: Integrate username change flow across lobby, server and chat

See merge request cs108-fs26/Gruppe-13!132
2026-04-15 19:14:23 +00:00
Jona Walpert 417af2e8f7 Merge branch 'feat/116-change-username-in-lobby' into integration/116-change-username 2026-04-15 21:09:49 +02:00
Jona Walpert 893bcee9bf Merge branch 'feat/change-username-command-on-server-side' into integration/116-change-username 2026-04-15 21:09:26 +02:00
Jona Walpert 6d1b467116 Fix: Sent chat messages with current username at send time 2026-04-15 21:06:13 +02:00
Jona Walpert 4687566816 Feat: Added client-side USERNAME_CHANGED event handling with whisper migration 2026-04-15 21:06:05 +02:00
Jona Walpert d27a07f3e8 Fix: Canonicalized outgoing chat sender name from authenticated session user 2026-04-15 21:05:40 +02:00
Jona Walpert b86f18a11c Feat: Added in-game player-id rename propagation for active games 2026-04-15 21:05:25 +02:00
Jona Walpert 7ff274269e Feat: Added lobby-level player rename handling and mapping synchronization 2026-04-15 21:04:57 +02:00
Jona Walpert ec916aa98c Fix: Allowed no-op username changes when new name equals current name 2026-04-15 21:04:38 +02:00
Jona Walpert 190b30672a Feat: Added CHANGE_USERNAME server command, registration and documentation 2026-04-15 20:49:29 +02:00
Jona Walpert 11be6e2b9b Feat: Wired lobby controller login flow to support in-session username changes 2026-04-15 20:43:15 +02:00
Jona Walpert a8599cfb4c Feat: Added CHANGE_USERNAME request support in LobbyClient 2026-04-15 20:42:50 +02:00
Jona Walpert 08b9f652be Feat: Added runtime shared-username update helper in client bootstrap 2026-04-15 20:42:28 +02:00
Jona Walpert 3252c7e534 Feat: Enabled username input section in lobby UI 2026-04-15 20:41:31 +02:00
Lars Simon Winzer 7cba7ac1dd Merge branch 'chore/113-finalize-presentation-for-fourth-milestone' into 'main'
Finalize presentation for fourth milestone

Closes #113, #83, #82, #78, #76, #70, and #67

See merge request cs108-fs26/Gruppe-13!130
2026-04-15 16:15:06 +02:00
Lars Simon Winzer c83a360fec Docs: Remove images containing code from slides 2026-04-15 16:11:58 +02:00
Lars Simon Winzer 2631791330 Docs: Finalize presentation with exported pdf 2026-04-15 13:54:49 +02:00
Jona Walpert 181f538038 fix: disable post-game buttons
Disable action buttons when game reaches FINISHED phase in UI
2026-04-15 00:11:23 +02:00
Jona Walpert 497820db05 Merge branch 'fix/lobby-auto-cleanup' into 'main'
Feat: Auto-cleanup lobbies when game finishes

See merge request cs108-fs26/Gruppe-13!129
2026-04-14 21:35:43 +00:00
Jona Walpert 88c9431bf0 Feat: Auto-cleanup lobbies when game finishes
- Add countNonFoldedPlayers() to GameState for fold-only victory detection
- Auto-transition to FINISHED phase when betting ends
- Clean up lobby (remove players, reset gameController) when game finishes
- Lobby now switches from RUNNING to CREATED and disappears immediately

Fixes: lobby not disappearing after game ends
2026-04-14 23:33:01 +02:00
Jona Walpert 5a53a14933 Merge branch 'feat/use-real-lobby-id' into 'main'
Feat: uses real lobby id and not hardcoded id

See merge request cs108-fs26/Gruppe-13!128
2026-04-14 21:31:12 +00:00
Jona Walpert f7f66ec5db Feat: uses real lobby id and not hardcoded id 2026-04-14 23:15:39 +02:00
Jona Walpert d85b3d21ee Merge branch 'feat/117-send-winner-with-get-gamestate-command' into 'main'
Feat: Winner is now sent to the frontend with get_gamestate

Closes #117

See merge request cs108-fs26/Gruppe-13!127
2026-04-14 21:13:57 +00:00
Jona Walpert 382383d38b Feat: Winenr is now sent to the frontend with get_gamestate 2026-04-14 23:09:48 +02:00
Jona Walpert 6f345cc51e Merge branch 'feat/ms-4-integration' into 'main'
Feat/ms 4 integration

Closes #59, #45, #52, #72, #73, #68, #71, and #36

See merge request cs108-fs26/Gruppe-13!126
2026-04-13 01:07:52 +00:00
Jona Walpert 585d675a8f fix: javadoc for MR 2026-04-13 03:04:54 +02:00
Jona Walpert b25f111f48 Merge branch 'feat/ms-4-integration' of https://git.scicore.unibas.ch/cs108-fs26/Gruppe-13 into feat/ms-4-integration 2026-04-13 02:58:20 +02:00
Jona Walpert d82297ac46 Fix: lobyb chat race condition 2026-04-13 02:57:07 +02:00
Julian Kropff 8c2130d0d6 Fix: checkstyle 2026-04-13 02:40:20 +02:00
Jona Walpert 52b5d5b8c2 fix: checkstyle methode length 2026-04-13 02:24:29 +02:00
Julian Kropff 1f8cf35e55 Docs: add JavaDocs 2026-04-13 02:16:26 +02:00
Julian Kropff 0bf24b9e0f Fix: Chat implementation completed 2026-04-13 01:44:18 +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
Julian Kropff 44785ee77f Fix: Issues with the chat UI display have been resolved 2026-04-13 01:15:21 +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
Julian Kropff 3053d531ee Fix: A bug with the Raise Button in the Game UI 2026-04-13 00:45:57 +02:00
Julian Kropff acb082ce5d Fic: Chat Implementation in the Lobby UI and Game UI 2026-04-13 00:35:08 +02:00
Julian Kropff 6aedb57473 Fix: Casono Rules Easy Description has been revised 2026-04-13 00:14:55 +02:00
Jona Walpert 66f0d734cf fix: fix broken UnitTest after game integration 2026-04-12 23:45:40 +02:00
Julian Kropff 6ec743f126 Fix: revision of chatContainer in the Casinomainui and Casinogameui styles 2026-04-12 22:56:40 +02:00
Julian Kropff 696f9f4fa4 Merge branch 'main' into feat/ms-4-integration 2026-04-12 22:16:58 +02:00
Julian Kropff 4557dc2f5a Fix: comment out unused input box and button 2026-04-12 21:52:40 +02:00
Julian Kropff b208f1fffd Fix: Dealer button display issue 2026-04-12 21:47:13 +02:00
Julian Kropff 6216268658 Fix: Resolve blocking issues to enable full game loop 2026-04-12 21:39:24 +02:00
Lars Simon Winzer 133a79433d Merge branch 'feat/documents-ms4' into 'main'
Add: first part of the powerpoint presentation for MS4

Closes #62, #75, and #69

See merge request cs108-fs26/Gruppe-13!125
2026-04-12 20:57:24 +02:00
Lars Simon Winzer 8fe96c5413 Merge branch 'chore/95-write-document-for-software-quality' into 'main'
Write QA concept document covering software quality

Closes #79, #80, and #95

See merge request cs108-fs26/Gruppe-13!124
2026-04-12 20:44:17 +02:00
Lars Simon Winzer 885a002f6d Docs: Updated qa concept document based on external input 2026-04-12 20:41:12 +02:00
Julian Kropff fb593d48a6 Fix: loop Animation in Game-UI 2026-04-12 20:11:10 +02:00
Lars Simon Winzer 9746f52965 Docs: Finalize QA concept document with data from JaCoCo
Note: The data covered the state of the repository at 1952f720de
2026-04-12 19:44:46 +02:00
Julian Kropff 06c059ce5e Fix: fast Game UI communication fix: display the opponent 2026-04-12 19:31:30 +02:00
Lars Simon Winzer 1952f720de Merge branch 'main' into 'chore/95-write-document-for-software-quality' 2026-04-12 19:27:42 +02:00
Mathis Ginkel d7eb9701c9 Merge branch 'feat/84-whisper-chat' into 'main'
Add whisper chat option

Closes #84

See merge request cs108-fs26/Gruppe-13!123
2026-04-12 16:47:09 +00:00
Jona Walpert 0f8f1bb918 fix: checkstyle and spotless 2026-04-12 18:36:47 +02:00
Jona Walpert 9cf6b468ad fix: correctly disaply lobbei and update them in Lobyb Screen 2026-04-12 16:02:50 +02:00
Mathis Ginkel e08b75dc7a Style: Apply Spotless 2026-04-12 15:31:59 +02:00
Lars Simon Winzer 761d4b1cae Docs: Add QA concept document for software quality assurance (lacking values) 2026-04-12 15:21:00 +02:00
Jona Walpert e4f0c7efb8 fix: Treat SocketException as client disconnect in SessionReader 2026-04-12 15:16:35 +02:00
Jona Walpert f91642bade Fix: fetch lobbies on client startup 2026-04-12 15:13:33 +02:00
Jona Walpert 93b6338041 Merge branch 'feat/lobby-expiry-broadcast' into 'main'
Feat: Expiry broadcast + client event handling and robustness

See merge request cs108-fs26/Gruppe-13!122
2026-04-12 12:57:42 +00:00
Jona Walpert a66c91c67f Merge branch 'main' into feat/lobby-expiry-broadcast 2026-04-12 14:54:09 +02:00
Jona Walpert 3e1d787b69 Feat: Add findEmptyLobbiesOlderThan to find and remove stale empty lobbies 2026-04-12 14:52:30 +02:00
Jona Walpert bc36892699 Feat: Expiry broadcast + client event handling and robustness 2026-04-12 14:40:17 +02:00
Julian Kropff 120a477a55 Merge branch 'feat/game-ui-communication' into 'main'
Fix: Game UI to support server commands

See merge request cs108-fs26/Gruppe-13!121
2026-04-12 12:07:37 +00:00
Jona Walpert 5d4fe4bcfb Merge branch 'feat/lobby-status' into 'main'
Feat: Add STATUS param to GET_LOBBY_STATUS and client parsing

See merge request cs108-fs26/Gruppe-13!120
2026-04-12 11:55:57 +00:00
Jona Walpert 346a728fb9 Feat): Add STATUS param to GET_LOBBY_STATUS and client parsing 2026-04-12 13:53:23 +02:00
Jona Walpert 63fb6e4b47 Merge branch 'feat/lobby-join-logging' into 'main'
Feat/lobby join logging

See merge request cs108-fs26/Gruppe-13!119
2026-04-12 11:33:41 +00:00
Jona Walpert 9044ce3660 Fix: Allow primitive values to contain hyphens (UUIDs) in addition to digits/words/colons 2026-04-12 13:31:00 +02:00
Jona Walpert 4fa60ad9e0 Fix: Apply checkstyle fixes 2026-04-12 13:30:11 +02:00
Jona Walpert 3abc03e6e1 Fix: fix error when switching back to mainLobby when exiting gameUI 2026-04-12 13:25:59 +02:00
Jona Walpert e43732c520 Feat: Log JOIN_LOBBY requests and joins 2026-04-12 13:12:55 +02:00
Jona Walpert 1dcf628d2c Feat: Log player joins in LobbyManager 2026-04-12 13:08:57 +02:00
Julian Kropff 4e65e2dec0 Merge branch 'feat/lobby-button-styling' into 'main'
Style: update Lobby Button styles in Casino Main UI

See merge request cs108-fs26/Gruppe-13!118
2026-04-12 10:59:53 +00:00
Jona Walpert 492901a837 Merge branch 'feat/100-add-start-game-command-on-server-side' into 'main'
Feat/100 add start game command on server side

Closes #100

See merge request cs108-fs26/Gruppe-13!109
2026-04-12 10:53:44 +00:00
Julian Kropff 659ea8a744 Fix: update Lobby Button styles in Casino Main UI 2026-04-12 12:53:44 +02:00
Jona Walpert 5e653a516c Merge branch 'main' into feat/100-add-start-game-command-on-server-side 2026-04-12 12:45:24 +02:00
Jona Walpert f5ee57454d Merge branch 'feat/93-add-serverside-create-lobby-command' into 'main'
Feat: Add createLobby command on server side

Closes #93

See merge request cs108-fs26/Gruppe-13!117
2026-04-12 10:10:41 +00:00
Jona Walpert fa8bb25b03 Add: Register create_lobby command 2026-04-12 12:03:41 +02:00
Jona Walpert e6c222388b Fix: add correct parsing on client side for create_lobyb command 2026-04-12 12:03:07 +02:00
Julian Kropff 7316ec80bf Fix: cleanup CasinoGameController and fix lifecycle issues
Refs #52
2026-04-12 11:42:20 +02:00
Jona Walpert b86352a298 Merge branch 'main' into feat/93-add-serverside-create-lobby-command 2026-04-12 11:27:11 +02:00
Jona Walpert a7de101be2 Merge branch 'feat/session-dispatcher' into 'main'
Feat: Add SessionDispatcher to send responses by username

See merge request cs108-fs26/Gruppe-13!116
2026-04-12 09:23:07 +00:00
Jona Walpert a2cd59e6b9 Feat: Add SessionDispatcher to send responses by username 2026-04-12 11:20:18 +02:00
Jona Walpert 564ace38e4 Merge branch 'feat/change-username-at-start-or-in-game' into 'main'
Feat: accept optional client username and forward to ClientApp

See merge request cs108-fs26/Gruppe-13!115
2026-04-12 09:17:38 +00:00
Jona Walpert d16c348444 Fix: fix javadoc for MR 2026-04-12 11:14:30 +02:00
Julian Kropff 8f3d0ab8a9 Fix: update GameService to support server commands
Refs #52
2026-04-12 10:58:54 +02:00
Jona Walpert 77ec50e008 Merge branch 'main' into feat/change-username-at-start-or-in-game 2026-04-12 10:58:19 +02:00
Julian Kropff 869f061d86 Fix: update GameClient to support server commands
Refs #52
2026-04-12 10:58:16 +02:00
Jona Walpert 9635ee8ef9 Fix: robust response parsing and clearer errors in ClientService Detect +OK / -ERR / -ERROR, strip leading tabs, collect response lines reliably and improve exception unwrapping. 2026-04-12 10:56:22 +02:00
Jona Walpert 65a93a8c82 Feat: Reuse shared ClientService in CasinomainuiController
Make the controller use ClientApp.getSharedClientService() if available; otherwise fall back to creating a new ClientService. Add/cleanup Javadoc.
2026-04-12 10:52:55 +02:00
Jona Walpert c3e0e740ed Feat: Keep startup LOGIN connection and expose shared ClientService
Introduce ClientApp.sharedClientService, create it on startup when a username is provided, run the startup LOGIN asynchronously on that connection and keep the socket open so the UI reuses it; add Javadoc and adjust logging.
2026-04-12 10:52:22 +02:00
Jona Walpert 0ae88c9149 Fix: Parse LOGIN response id as string in LobbyClient
Parse the server ID parameter as a string and return it in LoginResult (no integer parsing).
2026-04-12 10:45:03 +02:00
Jona Walpert 0aafe13e34 Fix: Treat login id as string (UUID) in LoginResult
Change LoginResult.id from int to String and add Javadoc explaining the server returns a UUID.
BREAKING CHANGE: LoginResult.getId() now returns String (UUID).
2026-04-12 10:43:43 +02:00
Jona Walpert 4798de7499 Feat: Log assigned username and id on registration
Add INFO log in UserFactory.create to record assigned username, user UUID and session id when a new user is registered.
2026-04-12 10:42:56 +02:00
Lars Simon Winzer 3359c9bc7e Docs: Add missing diary entries to diary README 2026-04-12 10:37:00 +02:00
Jona Walpert 346a308176 Merge branch 'feat/change-username-at-start-or-in-game' of https://git.scicore.unibas.ch/cs108-fs26/Gruppe-13 into feat/change-username-at-start-or-in-game 2026-04-12 09:36:48 +02:00
Julian Kropff 8998152e8d Fix: checkstyle 2026-04-12 09:31:25 +02:00
Julian Kropff ef7a83c707 Merge branch 'feat/lobby-ui-input-field-style' into 'main'
Style: Lobby UI input field

See merge request cs108-fs26/Gruppe-13!114
2026-04-12 07:18:43 +00:00
Jona Walpert d9e7c0c56d Merge branch 'main' into feat/change-username-at-start-or-in-game 2026-04-12 09:12:46 +02:00
Julian Kropff 0227145cab Fix: translations in Casino Main UI 2026-04-12 09:11:34 +02:00
Julian Kropff 755ea54381 Style: add gray-input-field class to Casino Main UI
Refs #106
2026-04-12 09:05:10 +02:00
jk c0b149cc5f Fix: add lobby ui input field style and fix translations
Refs #106
2026-04-12 09:00:41 +02:00
Jona Walpert 1669e07d3a Merge branch 'feat/105-add-fold-command-on-server-side' into 'main'
Feat/105 add fold command on server side

Closes #105

See merge request cs108-fs26/Gruppe-13!113
2026-04-11 23:21:05 +00:00
Jona Walpert d0e908774b Docs: Add documentation for fold command 2026-04-12 01:18:24 +02:00
Jona Walpert 08e02a6135 Feat: add Fold command on server side 2026-04-12 01:17:14 +02:00
Jona Walpert 6ee9c6c399 Merge branch 'feat/104-add-call-command-on-server-side' into 'main'
Feat/104 add call command on server side

Closes #104

See merge request cs108-fs26/Gruppe-13!112
2026-04-11 23:12:51 +00:00
Jona Walpert fb5399fc18 Docs: add documentation for call command 2026-04-12 01:06:41 +02:00
Jona Walpert 6d8f1787a9 Feat: add Call command to server side 2026-04-12 01:06:17 +02:00
Jona Walpert 9b4c6d8906 Merge branch 'feat/103-add-raise-command-on-server-side' into 'main'
Feat/103 add raise command on server side

Closes #103

See merge request cs108-fs26/Gruppe-13!111
2026-04-11 22:57:11 +00:00
Jona Walpert ca818d9892 Fix: Checkstyle Line length for MR Pipeline 2026-04-12 00:54:10 +02:00
Jona Walpert dac4dcfdbe Docs: Add documentation for Raise command 2026-04-12 00:47:41 +02:00
Jona Walpert 0556767d40 Feat: Add Raise command on backend side 2026-04-12 00:47:24 +02:00
Jona Walpert 5997e736cf Merge branch 'feat/102-add-bet-command-on-server-side' into 'main'
Feat/102 add bet command on server side

Closes #102

See merge request cs108-fs26/Gruppe-13!110
2026-04-11 22:37:32 +00:00
Jona Walpert a4e267cb00 Fix: add missing file for MR pipeline 2026-04-12 00:34:32 +02:00
Jona Walpert 9146d92c04 Fix: fix MR Pipeline 2026-04-12 00:27:45 +02:00
Mathis Ginkel b2adeb7a56 Feat: Add whisper chat functionality
Introduce recording of users connected to server, to show usernames as options for whisper chat.
2026-04-12 00:19:34 +02:00
Jona Walpert d77ce42795 Docs: add documentation of bet command 2026-04-12 00:11:30 +02:00
Jona Walpert f5a62c6e93 Feat: add bet command on server side 2026-04-12 00:11:11 +02:00
Jona Walpert 371e6afe5c Docs: add documentation of new start_game command 2026-04-11 23:18:22 +02:00
Jona Walpert 23debb18bc Feat: Add start_Game command to server 2026-04-11 23:17:39 +02:00
Lars Simon Winzer 3ff4491fcf Merge branch 'chore/101-remove-unused-blog-folder' into 'main'
Remove blog in documents

Closes #101

See merge request cs108-fs26/Gruppe-13!108
2026-04-11 23:09:07 +02:00
Lars Simon Winzer 31361ffb83 Docs: Remove blog folder from main readme in repository structure 2026-04-11 23:06:34 +02:00
Lars Simon Winzer 0f93bb8b85 Docs: Removed README of blog 2026-04-11 23:03:21 +02:00
Julian Kropff 4dd6732945 Docs: add Casono Manual pdf -> MS-4
Refs #75
2026-04-11 22:37:33 +02:00
Julian Kropff 3354bf7be1 Docs: add Casono Rules Easy Description pdf -> MS-4
Refs #75
2026-04-11 22:34:41 +02:00
Jona Walpert 3b8f7d42dd Merge branch 'feat/99-add-get-gamestate-command-on-server-side' into 'main'
Feat/99 add get gamestate command on server side

Closes #99

See merge request cs108-fs26/Gruppe-13!107
2026-04-11 20:08:19 +00:00
Julian Kropff a38b2562d1 Docs: add Casono Manual
Refs #75
2026-04-11 22:05:33 +02:00
Jona Walpert 54a7be074f Feat: Add get_game_state_command on server side 2026-04-11 22:04:47 +02:00
Jona Walpert 1b6e8ce6d6 Docs: add documentation for get_gamestate command 2026-04-11 22:02:09 +02:00
Julian Kropff 6e4e8f5150 Docs: revise Casono Rules Easy Description 2026-04-11 21:44:58 +02:00
Jona Walpert c7b7adaa07 Merge branch 'docs/fix-broken-javadoc-for-get-lobby-status-command' into 'main'
Docs: add get-lobby-status javadoc

Closes #96

See merge request cs108-fs26/Gruppe-13!106
2026-04-11 19:40:09 +00:00
Julian Kropff 84b889a343 Add: new images for Casono Rules Easy Description 2026-04-11 21:40:04 +02:00
Jona Walpert 25c92da522 Docs: add get-lobby-status javadoc 2026-04-11 21:36:44 +02:00
Jona Walpert 7f6a3ada29 Merge branch 'feat/94-add-get-lobby-list-command-on-server-side' into 'main'
Feat/94 add get lobby list command on server side

Closes #94

See merge request cs108-fs26/Gruppe-13!105
2026-04-11 19:29:06 +00:00
Jona Walpert 098092707c Merge branch 'main' into feat/94-add-get-lobby-list-command-on-server-side 2026-04-11 21:20:04 +02:00
Jona Walpert f467d9ebe2 Docs: Add new get_lobby_list command 2026-04-11 21:12:30 +02:00
Jona Walpert 13d03637da Add: register get_lobby_list command 2026-04-11 21:12:03 +02:00
Jona Walpert 50052b72ff Feat: Add get_lobby_listcommand to server 2026-04-11 21:11:40 +02:00
Jona Walpert ab4ba0f70b Merge branch 'feat/96-add-getlobbystatus-command-on-server-side' into 'main'
Feat/96 add getlobbystatus command on server side

See merge request cs108-fs26/Gruppe-13!104
2026-04-11 19:09:04 +00:00
Jona Walpert 46ccae40ad Style: apply spotless 2026-04-11 20:51:49 +02:00
Jona Walpert 050beb9d98 Docs: update command documentation 2026-04-11 20:50:19 +02:00
Jona Walpert 517df2369d Add: register get_lobby_statsu command 2026-04-11 20:49:23 +02:00
Jona Walpert 6219c2d660 Feat: Add get_Lobby_Status command on server side 2026-04-11 20:48:58 +02:00
Jona Walpert 57b8bca1a8 Merge branch 'feat/96-add-getlobbystatus-command-on-server-side' into 'main'
Feat/96 add getlobbystatus command on server side

Closes #96

See merge request cs108-fs26/Gruppe-13!103
2026-04-11 18:35:56 +00:00
Jona Walpert 050d87ce57 Docs: Add command documentation for get_lobby_status 2026-04-11 20:32:38 +02:00
Jona Walpert a930bd94b5 Feat: Add getlobbbystatus command 2026-04-11 20:31:25 +02:00
Jona Walpert 66ee791342 Merge branch 'feat/97-add-joinlobby-command-on-server-side' into 'main'
Feat: Add addPlayer command on sever side

Closes #97

See merge request cs108-fs26/Gruppe-13!102
2026-04-11 17:49:56 +00:00
Jona Walpert 240bba9bf5 Style: apply Spotless for Serverapp 2026-04-11 19:46:48 +02:00
Jona Walpert 15d1ea4edd Add: Register JoinLobby command 2026-04-11 19:45:55 +02:00
Jona Walpert 3badca0a01 Merge branch 'feat/97-add-joinlobby-command-on-server-side' of https://git.scicore.unibas.ch/cs108-fs26/Gruppe-13 into feat/97-add-joinlobby-command-on-server-side 2026-04-11 19:42:09 +02:00
Jona Walpert 203004fc42 Add: register JoinLobby command 2026-04-11 19:31:33 +02:00
Jona Walpert de0d0f93f8 Docs: Applied Spotless to JoinLobby command 2026-04-11 19:30:54 +02:00
Jona Walpert 7788bbc5d3 Style: Spotless apllied to JoinLobby command 2026-04-11 19:28:17 +02:00
Jona Walpert 2c6ecdc0f9 Docs: Add javadoc for JoinLobby command 2026-04-11 19:20:04 +02:00
Lars Simon Winzer 15ab6faf2e Merge branch 'feat/98-checkstyle-methodlength-in-serverapp' into 'main'
Allow longer method length in ServerApp

Closes #98

See merge request cs108-fs26/Gruppe-13!101
2026-04-11 18:49:31 +02:00
Lars Simon Winzer 25c20a2ea3 Fix: Allow longer method length in ServerApp 2026-04-11 18:46:50 +02:00
Jona Walpert 73dd00f37d Feat: Add JoinLobby Command to server side
This command adds the possibility to join a created lobby
2026-04-11 18:29:21 +02:00
Jona Walpert f512e6c422 Merge branch 'feat/73-support-for-multiple-lobbies' into feat/97-add-joinlobby-command-on-server-side 2026-04-11 18:15:32 +02:00
Jona Walpert eb3ffee6fc Merge branch 'feat/92-add-serverside-add-player-command' into 'feat/97-add-joinlobby-command-on-server-side'
Feat: Add addPlayer command on sever side

See merge request cs108-fs26/Gruppe-13!100
2026-04-11 16:08:48 +00:00
Jona Walpert af9daecf46 Docs: Add documentation for Join_lobby command 2026-04-11 18:03:18 +02:00
Jona Walpert 2ab27ac3ed Feat: Add lobby class and Manager with conenction to Game engine. The gameengine will be satrte when 4 players joined the lobby. This is made possible by the callback-driven-Listener
Refs #73
2026-04-11 17:41:45 +02:00
Julian Kropff 48e6b51ad1 Merge branch 'feat/game-ui' into 'main'
Add Game UI logic

See merge request cs108-fs26/Gruppe-13!99
2026-04-11 14:59:56 +00:00
Jona Walpert e655530760 Feat: Add addPlayer command on sever side
Rafs #92
2026-04-11 16:48:58 +02:00
Jona Walpert 62f01d1e5d Docs: add documentation of create_lobby commad 2026-04-11 16:44:19 +02:00
Jona Walpert ca4283bbe7 Feat: Add createLobby command on server side
Refs #93
2026-04-11 16:38:57 +02:00
Julian Kropff 3c7773e7fb Fix: checkstyle violations 2026-04-11 16:34:24 +02:00
Julian Kropff 406567d375 Fix: Integration of chatbox.fxml into Casinogameui 2026-04-11 16:29:07 +02:00
Lars Simon Winzer ff0f9f70d4 Merge branch 'chore/91-add-jacoco-as-gradle-dependency' into 'main'
Build: Add JaCoCo as gradle plugin

Closes #91

See merge request cs108-fs26/Gruppe-13!98
2026-04-11 15:56:28 +02:00
Lars Simon Winzer 53cafd3dd2 Build: Add JaCoCo as gradle plugin 2026-04-11 15:53:23 +02:00
Julian Kropff 30dcd8f4ab Merge branch 'main' into feat/game-ui
# Conflicts:
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameUI.java
#	src/main/resources/ui-structure/Casinogameui.fxml
2026-04-11 15:16:46 +02:00
Julian Kropff f54182fde6 Merge branch 'chore/ui-checkstyle-fixes' into 'main'
Fix: Resolving Git Merge Conflicts

Closes #88

See merge request cs108-fs26/Gruppe-13!97
2026-04-11 13:10:57 +00:00
Mathis Ginkel 9e7a80c17d Docs: add description of enum ChatType
Refs #88
2026-04-11 15:07:55 +02:00
Mathis Ginkel 9df3042978 Style: fix line breaks in UsernameAvailability
Refs #88
2026-04-11 14:57:23 +02:00
Mathis Ginkel 69333ede7a Fix: change visibility of enum UsernameAvailability
Refs #88
2026-04-11 14:48:46 +02:00
Julian Kropff cdb6d0c6b0 Fix: revert wrongfully deleted line breaks
Refs #88
2026-04-11 14:43:48 +02:00
Mathis Ginkel a65341b648 Style: Manually revert changes made to SuccessResponse 2026-04-11 14:37:13 +02:00
Julian Kropff b4b2e3b067 Fix: Resolving Git Merge Conflicts and Checkstyle
Refs #88
2026-04-11 14:01:21 +02:00
Julian Kropff beeba04a2c Merge branch 'feat/client-chat' into chore/ui-checkstyle-fixes
# Conflicts:
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatModel.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/Message.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/CoreClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatViewController.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java
#	src/main/resources/ui-structure/components/Chatbox.fxml
2026-04-11 12:56:01 +02:00
Julian Kropff edb98c8d21 Merge branch 'feat/client-communication' into chore/ui-checkstyle-fixes
# Conflicts:
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameUI.java
2026-04-11 12:20:27 +02:00
Mathis Ginkel 590a9aa658 Style: Manually revert changes made to UsernameAvailability enum 2026-04-11 11:56:06 +02:00
Lars Simon Winzer 7d74ed6263 Merge branch 'chore/87-write-documentation-to-explain-our-protocol' into 'main'
Documentation describing the structure and conventions of our network protocol

Closes #77 and #87

See merge request cs108-fs26/Gruppe-13!96
2026-04-11 11:32:49 +02:00
Lars Simon Winzer 2d7b15e593 Docs: Documentation describing the structure and conventions of our network protocol 2026-04-11 11:21:00 +02:00
Mathis Ginkel 66d3826509 Docs: Delete outdated network protocol document 2026-04-11 11:16:21 +02:00
Julian Kropff 2e705185f4 Fix: checkstyle 2026-04-11 08:33:02 +02:00
Mathis Ginkel 9c09927d09 Docs: Adding Server Commands for Client Chat to the protocol document manually reverting changes 2026-04-10 23:25:05 +02:00
Mathis Ginkel 97e5a55042 Style: Apply Spotless 2026-04-10 22:43:23 +02:00
Julian Kropff f6dda36c8d Merge branch 'main' into feat/game-ui 2026-04-10 21:05:53 +02:00
Julian Kropff 4de90e6962 Add: PlayerStatusController class -> player status (name, chips, dealer, fold state)
Refs #52
2026-04-10 20:59:07 +02:00
Julian Kropff 284ff2d043 Refactor: improve TaskbarController class -> Action system (fold/call/raise) implemented
Refs #52
2026-04-10 20:52:21 +02:00
Julian Kropff c02680143f Refactor: improve CasinoGameController class -> create UI layout for poker table
Refs #52
2026-04-10 20:46:50 +02:00
Julian Kropff cb7c284314 Fix: small style fixes 2026-04-10 20:41:12 +02:00
Julian Kropff a2d176f13c Refactor: improve CasinoGameUI structure
Refs #52
2026-04-10 20:34:49 +02:00
Julian Kropff 644c3e93fc Refactor: improve Card class
Refs #52
2026-04-10 20:31:15 +02:00
Julian Kropff d0ab49ec57 Add: GameService class
Refs #52
2026-04-10 20:29:26 +02:00
Julian Kropff 8deb647e94 Refactor: improve GameState class
Refs #52
2026-04-10 20:25:54 +02:00
Julian Kropff 3ea2971ab5 Refactor: improve Player class
Refs #52
2026-04-10 20:23:42 +02:00
Julian Kropff d10e9a72fd Add: PlayerId class
Refs #52
2026-04-10 20:20:45 +02:00
Julian Kropff ea41a3c75a Add: PlayerState class
Refs #52
2026-04-10 20:18:20 +02:00
Julian Kropff ce583062ba Refactor: improve GameClient structure
Refs #52
2026-04-10 20:16:08 +02:00
Julian Kropff 040ff7cf52 Fix: correct CasinoGame UI
Refs #52
2026-04-10 20:12:14 +02:00
Julian Kropff 616f609fae Fix: update Casinogame UI styles
Refs #52
2026-04-10 20:09:04 +02:00
Julian Kropff 4895af9030 Fix: correct Taskbar UI
Refs #52
2026-04-10 20:05:49 +02:00
Julian Kropff db3f34fddf Fix: correct Player status UI layout
Refs #52
2026-04-10 20:02:33 +02:00
Jona Walpert 999dd9beb9 Style: Apply Spotless 2026-04-10 14:59:27 +02:00
Jona Walpert 3be2d3d9f4 Fix: Chekstyle naming convention 2026-04-10 14:58:00 +02:00
Jona Walpert c094b224c3 Feat: Auto-assign playerN and add server-side rename support
Make username mutable, assign playerN for anonymous logins, and add atomic changeUsername in UserRegistry.

Refs #68
2026-04-10 14:20:21 +02:00
Jona Walpert 3807059229 Feat: accept optional client username and forward to ClientApp
Pass host/port and optional username via system properties for the UI.

Refs #68
2026-04-10 14:12:24 +02:00
Mathis Ginkel 31d3947175 Add: CSS-Style changes to the ChatBox 2026-04-10 13:21:34 +02:00
Jona Walpert 59cd5bca88 Docs: Add teammeating document with new division of tasks for milestone 4 2026-04-10 10:07:29 +02:00
Mathis Ginkel 1914aa2c7f Merge branch 'main' into 'feat/client-chat'
# Conflicts:
#   src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java
2026-04-09 19:34:38 +00:00
Mathis Ginkel 13df8db33e Docs: Insert JavaDocs 2026-04-09 21:25:46 +02:00
Mathis Ginkel 566c9375ab Style: Apply Spotless 2026-04-09 20:36:18 +02:00
jk a43361d606 Fix: Brightened the images 2026-04-09 19:21:07 +02:00
Mathis Ginkel 84e68d8b20 Merge remote-tracking branch 'origin/feat/client-chat' into feat/client-chat
# Conflicts:
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatModel.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/Message.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java
#	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
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/get_next_message/GetNextMessageHandler.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/get_next_message/GetNextMessageResponse.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/send_message/SendMessageParser.java
#	src/main/resources/ui-structure/Casinogameui.fxml
#	src/main/resources/ui-structure/components/chatui/chatbox.fxml
#	src/main/resources/ui-structure/components/chatui/chattab.fxml
#	src/main/resources/ui-structure/gameuicomponents/chatui/chatbox.fxml
#	src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatApplication.java
#	src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatTest.java
2026-04-09 18:13:29 +02:00
Mathis Ginkel e8401a976a Add: First implementation of Button, to create Whisper Chat 2026-04-09 18:04:57 +02:00
Mathis Ginkel c0c9225d42 Fix: Parsing of Server Responses for Client Chat 2026-04-09 18:04:57 +02:00
Mathis Ginkel 4e7b0ab67a Add: Implement Commands for Chat Protocol 2026-04-09 18:04:57 +02:00
Mathis Ginkel 39f60c9113 Fix: small changes to ChatUI 2026-04-09 18:04:57 +02:00
Jona Walpert 847d816935 Add: first (non functioning) modified version of lobby button controlelr with abckend conenction 2026-04-09 18:04:57 +02:00
Julian Kropff c8a9393fb6 Add: documentation for client network architecture 2026-04-09 18:04:57 +02:00
Julian Kropff 79bd33ad75 Add: initial client structure for game and network 2026-04-09 18:04:57 +02:00
Mathis Ginkel f5138503cb Add: CoreClient and ChatClient 2026-04-09 18:04:57 +02:00
Julian Kropff 93e39c3962 Add: Client Network Architecture Documentation 2026-04-09 18:04:57 +02:00
Mathis Ginkel 7d19e65428 Add: client chat architecture for lobby and global chat 2026-04-09 18:04:50 +02:00
Mathis Ginkel 87e9305e82 Fix: small changes in ClientService 2026-04-09 18:03:26 +02:00
Mathis Ginkel de66757837 Resolve Conflicts 2026-04-09 18:03:26 +02:00
Mathis Ginkel 65304cbb66 Merge remote-tracking branch 'origin/main' 2026-04-09 18: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
jk a5427a232f Merge branch 'main' into feat/game-ui
# Conflicts:
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameUI.java
2026-04-09 14:37:59 +02:00
Jona Walpert ad81479bcd Add: first part of the powerpoint presentation for MS4
Refs #62
2026-04-09 14:24:23 +02:00
Julian Kropff e374be4e31 Add: dealer chip images 2026-04-09 14:07:33 +02:00
Julian Kropff d939b0da67 Add: background card image 2026-04-09 13:57:24 +02:00
Julian Kropff ea987243b0 Merge branch 'feat/client-communication' into feat/game-ui 2026-04-09 13:35:55 +02:00
Jona Walpert 8a6c573bde Docs: 9.4 Small Teammeating 2026-04-09 12:15:19 +02:00
Jona Walpert 139626520b Docs: game rules description added for MS-4 2026-04-09 12:08:51 +02:00
Jona Walpert b205b25b48 Feat: Adds functionality to periodically refresh lobbies to update lobby GUI screen 2026-04-09 10:43:25 +02:00
Jona Walpert df31c307fe Fix: Comment out faulty Unit Test 2026-04-09 10:42:45 +02:00
Jona Walpert f3ecbd12f5 Feat: Add username text field to main Lobby 2026-04-08 13:34:26 +02:00
Jona Walpert 5d4d2655ef Add: Supply gameUI with client service at game startup 2026-04-08 13:33:31 +02:00
Jona Walpert d855f2d568 Feat: add offline mode to prevent errors when trying to send requests to not connected server 2026-04-08 13:29:39 +02:00
Jona Walpert ca782b389e Docs: Translate Javadoc and comments to English 2026-04-08 13:26:36 +02:00
Jona Walpert 04bf7ba545 Refactor: move login function from coreClient to LobbyClient 2026-04-08 13:22:24 +02:00
Mathis Ginkel 509aa71d9f Add: Implement Commands for Chat Protocol 2026-04-07 22:15:23 +02:00
Mathis Ginkel d1d2fb30e8 Fix: small changes to ChatUI 2026-04-07 22:11:20 +02:00
Jona Walpert aca61cbd52 Add: first (non functioning) modified version of lobby button controlelr with abckend conenction 2026-04-07 22:11:20 +02:00
Julian Kropff 73756b7d24 Add: documentation for client network architecture 2026-04-07 22:11:20 +02:00
Julian Kropff 60ed1ced91 Add: initial client structure for game and network 2026-04-07 22:11:20 +02:00
Mathis Ginkel eda51f8ac5 Add: CoreClient and ChatClient 2026-04-07 22:11:20 +02:00
Julian Kropff ce71992e17 Add: Client Network Architecture Documentation 2026-04-07 22:11:20 +02:00
Mathis Ginkel a9dcd79b38 Add: client chat architecture for lobby and global chat 2026-04-07 22:11:20 +02:00
Mathis Ginkel 79831b046c Fix: small changes in ClientService 2026-04-07 22:11:20 +02:00
Mathis Ginkel 61e54f282e Resolve Conflicts 2026-04-07 22:11:20 +02:00
Mathis Ginkel 8fb2657cdd Merge remote-tracking branch 'origin/main' 2026-04-07 22:10:52 +02:00
Mathis Ginkel 55a6d4beed Merge remote-tracking branch 'origin/main' 2026-04-05 13:12:39 +02:00
Jona Walpert 42b5344059 Test: Add in-process TestServer and update LobbyButtonGridManagerTest
Add TestServer that speaks the RawPacket/TcpTransport protocol for CREATE_LOBBY,
GET_LOBBY_STATUS and JOIN_LOBBY. Update LobbyButtonGridManagerTest to start the
TestServer and exercise a real `ClientService(host, port)`, enabling integration-style
tests of the client->transport->server roundtrip without an external server.

Refs #36
2026-04-02 22:28:12 +02:00
Jona Walpert 76e62b3cff Feat: Accept ClientService and delegate to LobbyClient
Add constructor overload that accepts ClientService and constructs a LobbyClient.
Avoids implicit host/port lookup in UI code; caller controls network config.
Keeps rendering/status behavior unchanged (async updates, deterministic ordering).
2026-04-02 22:26:14 +02:00
Jona Walpert 1070b5d4b9 Chore(startup): Propagate server ip:port to UI via system properties
ClientApp.start parses <ip>:<port>, sets `casono.server.host` / `casono.server.port` and forwards the original launch arg to the JavaFX launcher.
Casinomainui reads JavaFX launch args and exposes the same system properties.
CasinomainuiController reads those properties and constructs `ClientService(host, port)` (no hardcoded defaults)."
2026-04-02 22:12:48 +02:00
Jona Walpert 4daf6ff4a2 Fix: Sort button IDs before iterating map for deterministic rendering
Copy mapping.keySet() to a List and call Collections.sort(...) before iterating.
Ensures stable button placement and reproducible image updates (render + update)
2026-04-02 22:00:18 +02:00
Mathis Ginkel 7b1b35698c Merge remote-tracking branch 'origin/main' 2026-04-02 22:00:06 +02:00
Jona Walpert 3153f7027d fix: temporyy fix for NPE and wrong naming 2026-04-02 18:50:20 +02:00
Jona Walpert b5d19d915b Add: first (non functioning) modified version of lobby button controlelr with abckend conenction 2026-04-02 16:31:27 +02:00
Julian Kropff 09c19c2602 Add: documentation for client network architecture 2026-04-02 16:10:33 +02:00
Julian Kropff 77c7f7c0a5 Add: initial client structure for game and network 2026-04-02 16:08:24 +02:00
Mathis Ginkel 6f80501ea2 Add: CoreClient and ChatClient 2026-04-02 11:16:40 +02:00
Julian Kropff 45cd54b981 Add: Client Network Architecture Documentation 2026-04-02 10:54:47 +02:00
Mathis Ginkel 3427c661cf Merge branch 'feat/client-chat' into feat/client-communication
# Conflicts:
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatController.java
2026-04-02 10:31:28 +02:00
Mathis Ginkel ff07843e0a Fix: small changes in ClientService 2026-04-02 10:22:51 +02:00
Mathis Ginkel cc33411911 Add: basic chat functionality 2026-04-02 10:20:03 +02:00
Mathis Ginkel f721a26e53 Merge remote-tracking branch 'origin/main' 2026-04-02 10:14:23 +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
Mathis Ginkel dd721bda3f Merge remote-tracking branch 'origin/main' 2026-03-28 12:45:53 +01:00
Mathis Ginkel 68c7003c5d Merge remote-tracking branch 'origin/main' 2026-03-23 21:06:41 +01:00
Mathis Ginkel b4f34e00f2 Merge remote-tracking branch 'origin/main' 2026-03-21 10:01:34 +01:00
Mathis Ginkel 26a1ba945a Merge remote-tracking branch 'origin/main' 2026-03-20 13:23:01 +01:00
Mathis Ginkel 5401ead198 add: Network Protocol docs 2026-03-15 22:31:10 +01:00
337 changed files with 25267 additions and 1882 deletions
+22 -1
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>
@@ -36,7 +58,6 @@ This protocol handles game state synchronization, player actions, lobby and glob
├── documents/ # Project resources and documentation
│ ├── images/ # Images for README and documentation
│ ├── diary # Diary files for each organizational meetup
│ ├── blog # Blog files covering project-related topics
│ ├── docs # Documentation
│ ├── milestones/ # Milestone deliverables (6 milestones)
│ └── (Other resources) # Additional project materials
+12 -2
View File
@@ -4,6 +4,7 @@ plugins {
id 'org.openjfx.javafxplugin' version '0.1.0'
id 'checkstyle'
id 'com.diffplug.spotless' version '7.0.2'
id 'jacoco'
}
group = 'ch.unibas.dmi.dbis'
@@ -25,14 +26,14 @@ 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 {
// Source: https://mvnrepository.com/artifact/org.apache.logging.log4j
implementation("org.apache.logging.log4j:log4j-api:2.25.3")
runtimeOnly("org.apache.logging.log4j:log4j-core:2.25.3")
implementation("org.jspecify:jspecify:1.0.0")
// Source: https://mvnrepository.com/artifact/org.fusesource.jansi/jansi
runtimeOnly("org.fusesource.jansi:jansi:2.4.2")
@@ -41,8 +42,17 @@ dependencies {
implementation 'org.json:json:20240303'
}
jacoco {
toolVersion = "0.8.14"
}
jacocoTestReport {
dependsOn test
}
test {
useJUnitPlatform()
finalizedBy jacocoTestReport
}
checkstyle {
+3
View File
@@ -21,4 +21,7 @@
<!-- Allow for compacter line seperators -->
<suppress checks="EmptyLineSeparator" files=".*Test\.java"/>
<!-- Allow longer method length in composition root -->
<suppress checks="MethodLength" files="ServerApp.java"/>
</suppressions>
-28
View File
@@ -1,28 +0,0 @@
# Blog
## Purpose
This folder contains a collection of blog posts documenting the development process of our application. These blogs serve two key purposes:
1. **For External Audiences**
The blogs provide insights into the internal program flow and the interaction between components without requiring readers to dive into the source code. They offer a mixed-level overview of how different parts of the system work together.
2. **For Our Team**
The blogs serve as a record of our decision-making process. By documenting what decisions were made and why, we can track the evolution of our design choices and understand the reasoning behind them. This is invaluable for maintaining consistency across the team.
But keep in mind that these blogs **do not replace** the detailed source code documentation.
## Format
All blog posts follow the naming convention: `<YYYY-MM-DD>_<Title>.md`
## Blog Posts
### General
*(No posts yet)*
### Client
*(No posts yet)*
### Server
*(No posts yet)*
+9
View File
@@ -0,0 +1,9 @@
# Besprechung über Veränderung der Aufgabenteilung
## Verschieben von Aufgaben
Da die Implementation der client server Kommunikation länger dauerte als geplant, wurden achivements erfüllt, die erst bei späteren Meilensteinen relevant sein werden.
## Aufgabeverteilung
Um zu vermeiden, dass sich Personen erst lange Zeit in den Code anderer einarbeiten müssen um zu helfen, wurden Dokumentation und das Erstellen von Materialien für MS4 auf ein Teammitgleid (Jona) ausgelagert um das Projekt nicht zu verlangsamen
+38
View File
@@ -0,0 +1,38 @@
# Neue Aufgabenteilung für MS4 (10.04)
## Kurzfassung
Offene Meilensteine wurden überprüft und Personen zugewiesen. Ziel ist, verbleibende Funktionen (Game List, Username-Integration, Lobby/Chat-Funktionen) bis zum nächsten Sprint stabil zu machen.
## Offene Aufgaben
- Broadcast: Offen (Priorität niedrig)
- Username-Integration: In Backend integrieren und frontend Anpasungen (wichtig)
- Demo: Kernfunktion vorhanden, muss getestet werden
- Game List: Backend fehlt vollständig
- Chat: Globaler Chat und Lobby-Chat müssen eingebunden werden
- Lounging: Support für mehrere Lobbys und Spielerlisten
- QA: Präsentation und Dokumentation
- Rules to Code: Vorstellung und Integration
- "Shall we play a game": Flag zum Umschalten zwischen Demo- und Normalmodus
- Whisper: Funktion (siehe Zuordnung)
- Zeitplan: Verschiebung von nicht-kritischen Punkten
## Aufgaben & Zustänextrdigkeiten
- Jona
- Command Line: Parsing, Option für Username-Eingabe (Start vs. Lobby)
- Game List: Implementierung (Datenstruktur, Commands, Backend-Anbindung)
- Lounging: Mehrere Lobbys, Liste aller Spieler
- Lars
- QA: Präsentation und ausgeschriebenes Dokument
- Mathis
- Whisper-Chat: Fertigstellung
- Lobby-Chat: Integration
- Julian
- Game Engine: Fertigstellung
- Flag: Umschalten zwischen Demo- und echter Game Engine
- Rules to Code: Vorstellung
+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.
+22 -10
View File
@@ -1,7 +1,7 @@
# Meeting Diary
This folder holds the notes and documentation of all team meetings throughout the project.
The notes are organised by **milestones (16)** so that everyone can easily see what was discussed and decided at each stage. Each sub-header below links to the actual Markdown file containing the details.
The notes are organized by **milestones (16)** so that everyone can easily see what was discussed and decided at each stage. Each sub-header below links to the actual Markdown file containing the details.
## Milestone 1
### [24. February 2026](24-02.md)
@@ -14,21 +14,33 @@ The notes are organised by **milestones (16)** so that everyone can easily se
- Finalizing structure of network protocol
## Milestone 2
*(Meeting notes to be added here when available)*
## Milestone 2
### [14. March 2025](14-03.md)
- Discussion of LobbyScreen, GameScreen, and Chatbox structure
- Endscreen postponed to MS3
## Milestone 3
*(Meeting notes to be added here when available)*
## Milestone 3
None, as milestone 3 covered the assessment of individual technical understanding through evaluation.
## Milestone 4
*(Meeting notes to be added here when available)*
## Milestone 4
### [09. April 2026](09-04.md)
- Task redistribution due to delays in client-server communication
- Documentation and MS4 materials assigned to Jona
### [10. April 2026](10-04.md)
- Review and assignment of open milestones
- Focus on stabilizing Game List, Username-Integration, Lobby/Chat functions
- Task list for remaining features and QA
## Milestone 5
*(Meeting notes to be added here when available)*
## Milestone 5
### [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
+1
View File
@@ -4,3 +4,4 @@
- [Game Engine Architecture](game-engine-architecture.md)
- [Casono Rules Easy Description](casono-rules-easy-description.md)
- [Game engine implementation](game-engine-implementation.md)
- [Casonso Manual](casono-manual.md)
+430
View File
@@ -0,0 +1,430 @@
# Casono Manual
<!-- vim-markdown-toc GFM -->
* [Start Game](#start-game)
* [Server starten](#server-starten)
* [Client starten](#client-starten)
* [UI](#ui)
* [Lobby UI](#lobby-ui)
* [Eine Lobby erstellen](#eine-lobby-erstellen)
* [Einer Lobby beitreten](#einer-lobby-beitreten)
* [Den Username ändern](#den-username-ändern)
* [Highscores](#highscores)
* [Game UI](#game-ui)
* [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.
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 🟡*
@@ -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;
@@ -1,56 +1,42 @@
# Casono Rules Easy Description
> Source: https://www.youtube.com/watch?v=h-1WyU5Wqsw&pp=ygUZbGVybmVuIHBva2VyIHRleGFzIGhvbGRlbQ%3D%3D
> Quelle: https://www.youtube.com/watch?v=h-1WyU5Wqsw&pp=ygUZbGVybmVuIHBva2VyIHRleGFzIGhvbGRlbQ%3D%3D
<!-- vim-markdown-toc GFM -->
* [Deustch](#deustch)
* [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)
* [English](#english)
* [Blinds (Small Blind & Big Blind)](#blinds-small-blind--big-blind-1)
* [First Betting Round (Preflop)](#first-betting-round-preflop)
* [Preflop Example](#preflop-example)
* [Flop (3 Community Cards)](#flop-3-community-cards)
* [Flop Example](#flop-example)
* [Turn (4th Card)](#turn-4th-card)
* [River (5th Card)](#river-5th-card)
* [Showdown (Winning Decision)](#showdown-winning-decision)
* [Poker Hand Rankings (Hierarchy)](#poker-hand-rankings-hierarchy)
* [Conclusion](#conclusion)
* [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 -->
## Deutsch
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.
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 **20.000 Chips**. Jeder bekommt **2 Karten auf die Hand** und es gibt zusätzlich **5 Gemeinschaftskarten**, die später in der Mitte aufgedeckt werden.
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.png)
![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)
## 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.png)
![2. image](./images/2-1.svg)
Regel: *32 Dead Button 🟡*
@@ -58,7 +44,7 @@ Diese Einsätze sorgen dafür, dass sofort ein Pot entsteht und das Spiel überh
Danach werden die Karten verteilt: zuerst Small Blind, dann Big Blind und dann im Uhrzeigersinn alle anderen Spieler.
### Erste Setzrunde (Preflop)
## Erste Setzrunde (Preflop)
Die erste Setzrunde beginnt immer bei dem Spieler links vom Big Blind.
@@ -73,251 +59,108 @@ Regel: *41 Methods of Calling 🟢*
Regel: *42 Methods of Raising 🟢*
Regel: *50 Acting in Turn 🟢*
### Beispiel Preflop
## Beispiel Preflop
Julian schaut seine Karten an und entscheidet sich direkt für einen Call von **600 Chips**.
Julian schaut seine Karten an und entscheidet sich direkt für einen Raise von **600 Chips**.
![3. image](./images/3.png)
![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.png)
![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.png)
![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.png)
![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)
## 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.
![7. image](./images/7.png)
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
## Beispiel Flop
Lars setzt **1000 Chips** als Erstes. Julian entscheidet sich mitzugehen (Call), weil seine Karten durch die Gemeinschaftskarten stärker geworden sind.
Jona setzt **1000 Chips** als Erstes. Lars entscheidet sich mitzugehen (Call), weil seine Karten durch die Gemeinschaftskarten stärker geworden sind.
![8. image](./images/8.png)
Julian steigt aus, weil er keine gute Verbindung mehr sieht. Mathis ist bereits raus.
Jona steigt aus, weil er keine gute Verbindung mehr sieht. Mathis ist bereits raus.
![8. image](./images/8-1.svg)
### Turn (4. Karte)
## Turn (4. Karte)
Jetzt kommt die **4. Gemeinschaftskarte**.
Wieder beginnt eine neue Setzrunde.
Lars setzt diesmal **3000 Chips**. Julian bezahlt erneut (Call), weil seine Hand weiterhin gut spielbar ist.
Jona setzt diesmal **3000 Chips**. Lars bezahlt erneut (Call), weil seine Hand weiterhin gut spielbar ist.
![9. image](./images/9.png)
![9. image](./images/9-1.png)
Regel: *53 Action Out of Turn 🟡*
### River (5. Karte)
## River (5. Karte)
Jetzt wird die letzte Gemeinschaftskarte aufgedeckt.
Dies ist die letzte Entscheidung im Spiel.
Lars setzt **5000 Chips**.
Jona setzt **5000 Chips**.
![10. image](./images/10.png)
![10. image](./images/10-1.png)
Julian muss jetzt entscheiden: Fold, Call oder Raise auf **10000 Chips**.
Lars muss jetzt entscheiden: Fold, Call oder Raise auf 10000 Chips.
Regel: *54 Pot Size Bets 🟡*
### Showdown (Gewinnentscheidung)
## 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.png)
![11. image](./images/11-1.png)
Regel: *12 Cards Speak at Showdown 🟢*
Regel: *16 Face Up for All-Ins 🟢*
Regel: *17 Non All-In Showdowns 🟢*
Wenn Julian den letzten Einsatz bezahlt, werden die Hände verglichen. Wenn er foldet, gewinnt Lars automatisch den gesamten Pot.
Wenn Lars den letzten Einsatz bezahlt, werden die Hände verglichen. Wenn er foldet, gewinnt Jona automatisch den gesamten Pot.
### Poker Hand Rankings (Gewichtung)
## Poker Hand Rankings (Gewichtung)
Die Kartenkombinationen sind klar geordnet von schwach bis extrem stark:
![12. image](./images/12.png)
<img src="./images/12.png" height="600">
Je höher die Kombination, desto stärker die Hand und desto wahrscheinlicher der Gewinn.
Julian: 2. Paar:
Jona: 2. Paar:
![13. image](./images/13.png)
![13. image](./images/13-1.png)
Lars: 1. Paar:
![14. image](./images/14.png)
![14. image](./images/14-1.png)
Da zwei Paare in der Rangfolge über einem einzelnen Paar stehen, gewinnt Julian diese Runde.
Da zwei Paare in der Rangfolge über einem einzelnen Paar stehen, gewinnt Jona diese Runde.
### Fazit
## 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 🟡*
## English
The poker table is an incredibly fascinating experiential space where you can learn a great deal: about yourself, about other people, and about questions such as: How do I actually make decisions? How do I handle stress and uncertainty? And how good am I at empathizing with others and correctly assessing situations?
To enter this experiential space, its necessary—like with any game—to first understand the basic rules and game flow.
So lets begin:
We have **4 players** at the table: Julian, Mathis, Jona, and Lars. Each player starts with **20,000 chips**. Each receives **2 hole cards**, and there are **5 community cards** that will later be revealed in the center.
![1. image](./images/1.png)
The players are seated as follows: Julian, Mathis, Jona, and Lars. One of them holds the dealer button, which determines who deals the cards and where the round begins. This button moves clockwise after each hand, constantly changing the positions.
Rule: *34 Button Placement and Movement 🟢*
### Blinds (Small Blind & Big Blind)
Before cards are dealt, there are two mandatory bets:
The Small Blind and the Big Blind. The Big Blind is always double the amount of the Small Blind.
![2. image](./images/2.png)
Rule: *32 Dead Button 🟡*
These forced bets ensure that a pot exists from the start, making the game begin with everyone already involved.
After this, the cards are dealt: first to the Small Blind, then the Big Blind, and then clockwise to all other players.
### First Betting Round (Preflop)
The first betting round always begins with the player to the left of the Big Blind.
Now each player must decide:
* Fold (exit the hand)
* Call (match the current bet)
* Raise (increase the bet)
Rule: *40 Methods of Betting 🟢*
Rule: *41 Methods of Calling 🟢*
Rule: *42 Methods of Raising 🟢*
Rule: *50 Acting in Turn 🟢*
### Preflop Example
Julian looks at his cards and decides to Call for **600 chips**.
![3. image](./images/3.png)
Mathis sees his cards and realizes they arent strong, so he Folds and exits the hand.
![4. image](./images/4.png)
Jona is next and decides to Call as well, because his hand is playable.
![5. image](./images/5.png)
Lars looks at his cards, recognizes a strong hand, and Raises to **1200 chips**.
![6. image](./images/6.png)
This immediately changes the situation: Julian and Jona must now decide whether to call this raise, re-raise, or fold.
### Flop (3 Community Cards)
Now **3 community cards** are placed face-up in the center. From this point, the game changes completely, as all players gain additional information.
![7. image](./images/7.png)
A new betting round begins.
Rule: *49 Accepted Action 🟢*
### Flop Example
Lars bets **1,000 chips** first. Julian decides to Call because his hand has improved with the community cards.
![8. image](./images/8.png)
Jona folds, as he no longer sees a strong connection. Mathis is already out.
### Turn (4th Card)
Now the **4th community card** is revealed.
Another betting round begins.
Lars bets **3,000 chips** this time. Julian calls again, as his hand remains strong.
![9. image](./images/9.png)
Rule: *53 Action Out of Turn 🟡*
### River (5th Card)
Now the final community card is revealed.
This is the last decision point in the hand.
Lars bets **5,000 chips**.
![10. image](./images/10.png)
Julian must now decide: Fold, Call, or even Raise to **10,000 chips**.
Rule: *54 Pot Size Bets 🟡*
### Showdown (Winning Decision)
If two or more players remain after the final betting round, a showdown occurs.
All active players reveal their cards. The winner is the one with the **best 5-card combination using any combination of their hole cards and the community cards**.
![11. image](./images/11.png)
Rule: *12 Cards Speak at Showdown 🟢*
Rule: *16 Face Up for All-Ins 🟢*
Rule: *17 Non All-In Showdowns 🟢*
If Julian calls the final bet, the hands are compared. If he folds, Lars wins the entire pot automatically.
### Poker Hand Rankings (Hierarchy)
The card combinations are clearly ranked—from weakest to strongest:
![12. image](./images/12.png)
The higher the combination, the stronger the hand, and the greater the chance of winning.
Julian: Two Pair:
![13. image](./images/13.png)
Lars: One Pair:
![14. image](./images/14.png)
Since two pair ranks higher than one pair, Julian wins this round.
### Conclusion
Poker is not gambling in the traditional sense, but rather a game of strategy, psychology, and mathematics. Every decision made by Julian, Mathis, Jona, or Lars changes the entire dynamic at the table. Understanding the rules means not only understanding cards, but also people and decision-making under pressure.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 958 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.2 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.2 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.6 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.7 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.7 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 3.9 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 MiB

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>
@@ -0,0 +1,249 @@
# Client Nework Architecture
<!-- vim-markdown-toc GFM -->
* [Architecture Overview](#architecture-overview)
* [network/Card.java](#networkcardjava)
* [network/GameState.java](#networkgamestatejava)
* [network/Player.java](#networkplayerjava)
* [network/ChatClient.java](#networkchatclientjava)
* [ChatClient(ClientService clientService)](#chatclientclientservice-clientservice)
* [sendMessage(Message message)](#sendmessagemessage-message)
* [getMessages()](#getmessages)
* [network/ClientService.java](#networkclientservicejava)
* [ClientService(String ip, int port)](#clientservicestring-ip-int-port)
* [processCommand(String message)](#processcommandstring-message)
* [sendRequest(Runnable request)](#sendrequestrunnable-request)
* [getRuntimeException(Exception e)](#getruntimeexceptionexception-e)
* [closeSocket()](#closesocket)
* [writeToTransport(String s) throws IOException](#writetotransportstring-s-throws-ioexception)
* [network/CoreClient.java](#networkcoreclientjava)
* [CoreClient(ClientService clientservice)](#coreclientclientservice-clientservice)
* [ping()](#ping)
* [login(String user)](#loginstring-user)
* [network/GameClient.java](#networkgameclientjava)
* [GameClient(ClientService client)](#gameclientclientservice-client)
* [getGameState()](#getgamestate)
* [parseGameState(String input)](#parsegamestatestring-input)
* [Example server response](#example-server-response)
* [network/LobbyClient.java](#networklobbyclientjava)
* [LobbyClient(ClientService client)](#lobbyclientclientservice-client)
* [fetchLobbyStatusString(int lobbyId)](#fetchlobbystatusstringint-lobbyid)
* [createLobby()](#createlobby)
* [getLobbyId()](#getlobbyid)
* [joinLobby(int lobbyId)](#joinlobbyint-lobbyid)
<!-- vim-markdown-toc -->
## Architecture Overview
```text
client/
├── game/
│ ├── Card.java
│ ├── GameState.java
│ └── Player.java
└── network/
├── ChatClient.java
├── ClientService.java
├── CoreClient.java
├── GameClient.java
└── LobbyClient.java
```
### game/Card.java
Represents a playing card with a value and suit.
### game/GameState.java
Represents the current state of the poker game, including the phase, pot size, current bet, dealer position, active player, community cards, and player information.
### game/Player.java
Represents a player in the poker game, including their name, chip count, current bet, state (e.g., `active`, `folded`), and their hole cards.
### network/ChatClient.java
The ChatClient class is responsible for sending messages to the server and retrieving messages from the server. It uses the ClientService to send commands and receive responses from the server.
#### ChatClient(ClientService clientService)
Constructs a ChatClient with the given ClientService for communication.
- **Parameter (`clientService`)**: The ClientService instance used to send commands and receive responses from the server.
#### sendMessage(Message message)
Send a Message to the server by converting it to a string format and sending a `SEND_MESSAGE` command with the message content as arguments.
- **Parameter (`message`)**: message The Message object to be sent to the server.
#### getMessages()
Retrieve messages from the server by first sending a `GET_MESSAGE_COUNT` command to determine how many messages are available and then sending `GET_NEXT_MESSAGE` commands in a loop to retrieve each message. The retrieved messages are parsed into Message objects and returned as a list.
- **Parameter (`A`)**: list of Message objects representing the messages retrieved from the server.
### network/ClientService.java
The ClientService class is responsible for managing the connection to the server, sending commands, and receiving responses. It uses a TcpTransport to
communicate with the server and an ExecutorService to handle asynchronous requests.
#### ClientService(String ip, int port)
Constructs a ClientService with the given server IP and port. It establishes a socket connection to the server and initializes the TcpTransport and ExecutorService for communication.
- **Parameter (`ip`)**: The IP address of the server to connect to.
- **Parameter (`port`)**: The port number of the server to connect to.
#### processCommand(String message)
Sends a command to the server and waits for the response. The command is sent using the TcpTransport, and the response is read in a loop until a valid response is received. The method handles `+OK` and `-ERROR` responses from the server and returns the actual response content.
- **Parameter (`message`)**: The command message to be sent to the server.
- **Return**: The response from the server as a string.
#### sendRequest(Runnable request)
Helper method to send a request to the server using the ExecutorService. It submits the request as a Runnable task and waits for its completion. If the task is interrupted or encounters an execution exception, it throws a
RuntimeException with the appropriate cause.
- **Parameter (`request`)**: The Runnable task representing the request to be sent to the server.
#### getRuntimeException(Exception e)
Helper method to extract the cause of an exception and return it as a RuntimeException. If the cause is null, it returns the original exception as a RuntimeException. If the cause is already a RuntimeException, it returns it directly. Otherwise, it wraps the cause in a new RuntimeException and returns it.
- **Parameter (`e`)**: The exception from which to extract the cause.
- **Return**: A RuntimeException representing the cause of the original exception.
#### closeSocket()
Closes the socket connection to the server and shuts down the ExecutorService. It also closes the TcpTransport used for communication. If any IOException occurs during this process, it prints the exception to the console.
#### writeToTransport(String s) throws IOException
Helper method to write a command string to the TcpTransport. It generates a unique ID for the command using the idGenerator and sends a RawPacket containing the ID and the command string to the server. If an IOException occurs during this process, it throws a RuntimeException with the cause.
- **Parameter (`s`)**: The command string to be sent to the server.
- **Throws IOException**: If an I/O error occurs while writing to the transport.
### network/CoreClient.java
The CoreClient class provides basic functionalities for communicating with the server, such as sending a ping command to check connectivity and logging in with a username. It uses the ClientService to send commands and receive responses from the server.
#### CoreClient(ClientService clientservice)
Constructs a CoreClient with the given ClientService for communication.
- **Parameter (`clientservice`)**: The ClientService instance used to send commands and receive responses from the server.
#### ping()
Sends a `PING` command to the server to check connectivity. The server should respond with a `PONG` message if the connection is successful.
#### login(String user)
Logs in to the server with the given username by sending a `LOGIN` command.
- **Parameter (`user`)**: The username to log in with.
### network/GameClient.java
The GameClient class is responsible for communicating with the server to retrieve the current game state. It sends a command to the server and parses the response into a structured GameState object.
#### GameClient(ClientService client)
Constructs a GameClient with the given ClientService for communication.
- **Parameter (`client`)**: The ClientService instance used to send commands and receive responses from the server.
#### getGameState()
Retrieves the current game state from the server by sending a command and parsing the response.
- **Return**: A GameState object representing the current state of the game.
#### parseGameState(String input)
Parses the raw response from the server into a structured GameState object.
- **Parameter (`input`)**: The raw response string from the server.
- **Return**: A GameState object representing the current state of the game.
#### Example server response
```text
+OK
PHASE=FLO P
POT=150
CURRENT_BET=50
DEALER=0
ACTIVE_PLAYER=1
CARDS
CARD
VALUE=10
SUIT=H
CARD
VALUE=7
SUIT=S
CARD
VALUE=A
SUIT=D
PLAYERS
PLAYER
NAME=Max
CHIPS=1200
BET=50
STATE=ACTIVE
CARDS
CARD
VALUE=K
SUIT=H
CARD
VALUE=3
SUIT=C
PLAYER
NAME=Anna
CHIPS=800
BET=0
STATE=FOLDED
CARDS
END
```
### network/LobbyClient.java
The LobbyClient class is responsible for communicating with the server to manage game lobbies. It provides methods to create a lobby, join a lobby, and fetch the current status of a lobby by sending appropriate commands to the server and processing the responses.
#### LobbyClient(ClientService client)
Constructs a LobbyClient with the given ClientService for communication.
- **Parameter (`client`)**: The ClientService instance used to send commands and receive responses from the server.
#### fetchLobbyStatusString(int lobbyId)
Fetch the current status of the lobby with the given id from the server.
- **Parameter (`lobbyId`)**: The id of the lobby to fetch the status for.
- **Return**: A string representing the current status of the lobby, as returned by the server.
#### createLobby()
Request the server to create a new lobby and return the id of the newly created lobby.
- **Return**: The id of the newly created lobby, as returned by the server.
#### getLobbyId()
Request the server to return the id of the lobby that the client is currently in.
- **Return**: The id of the lobby that the client is currently in, as returned by the server.
#### joinLobby(int lobbyId)
Request the server to join the lobby with the given id.
- **Parameter (`lobbyId`)**: The id of the lobby to join.
File diff suppressed because it is too large Load Diff
+72
View File
@@ -0,0 +1,72 @@
# Our Network Protocol Documentation
## Overview
Our protocol is inspired by *POP3*, but has been highly customized to fit our specific needs.
It is a text-based protocol operating over raw TCP sockets, designed for human readability and strict structure.
## Packet Structure
Each network packet consists of:
- **4-byte header**: Specifies the size of the payload (big-endian integer).
- **4-byte request ID**: Generated by the client, used to match requests and responses.
- **Payload**: The actual data, its size as specified by the header.
This structure is used for both requests and responses.
## Conventions
- All keys (in both requests and responses) use UPPER_SNAKE_CASE.
- Only a-z, A-Z, 0-9 are allowed in keys.
- Only human-readable strings are transmitted.
- Binary data is not allowed.
## Request Format
A request consists of a single line:
```
COMMAND KEY1=ARG1 KEY2=ARG2
```
- **COMMAND**: The action to perform.
- **KEY=VALUE pairs**: Optional parameters. There may be zero or more.
- **Whitespace**: Extra spaces between key, separator, and value are ignored. Any other characters between them are an error.
- **Standalone values**: Not allowed. Every value must have a key.
### String Values
- Strings with spaces must be enclosed in single quotes: `'example string'`.
- Inside quoted strings, line breaks are allowed.
- To include a single quote inside a string, escape it (e.g., `'It\'s fine'`).
If these rules are violated, the request is considered invalid and will be rejected.
## Response Format
Responses are more complex and can represent nested collections.
- **Success**: Starts with `+OK`
- **Error**: Starts with `-ERR`
- After the status, a newline follows, then fields in the format `KEY=VALUE`.
- Collections and elements are ended with the `END` keyword.
- A collection starts with a key, and its elements are indented.
- A element starts with a key, and its fields are indented.
### Example: Nested Collection
```
+OK
KEY1=VALUE1
FIELDS
FIELD
NESTED_KEY=NESTED_VALUE
END
END
KEY2=VALUE2
END
```
## Error Handling
Any violation of the format (invalid characters, unescaped quotes, binary data, etc.) results in the request being rejected with an error response.
### Example Error Response When Violating Syntax Rules:
```
-ERR
CODE=PARSING_ERROR
MSG='Error occured during parsing. Likely due to malformed payload.'
END
```
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.
Binary file not shown.
Binary file not shown.
+144
View File
@@ -0,0 +1,144 @@
\documentclass{article}
\setlength{\parindent}{0pt}
\usepackage[a4paper,margin=2.5cm]{geometry}
\usepackage{listings}
% External links
\usepackage{hyperref}
% Colored text
\usepackage{xcolor}
% Customized table
\usepackage{float}
\usepackage{array}
\newcolumntype{L}[1]{>{\raggedright\arraybackslash}p{#1}}
\usepackage{booktabs}
\title{QS-Konzept für MS4}
\author{Syntax Syndicate - Casono}
\begin{document}
\maketitle
\section{Einleitung}
Dieses Dokument beschreibt die Software-Qualitätssicherung für unser Mehrspieler-Spiel \textit{Casono}. Es wird als Teil der Programmierprojekt-Vorlesung (ehemals bekannt als cs108) an der Universität Basel von uns entwickelt.\\
Dieses Konzept basiert auf der \textit{DIN ISO 9126} und ist demzufolge in folgende 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}
Alle öffentlichen Klassen, Interfaces, Records und Methoden müssen durch JavaDoc dokumentiert werden.\\
Als Teil der GitLab-Build-Pipeline wird JavaDoc auf Richtigkeit überprüft. Werden strukturelle Fehler wie ungültige Referenzen erkannt, wird der Merge verhindert.\\
Unsere aktuelle Grundlage sind daher 0 Fehler. Für den kommenden Meilenstein ist aber unser Ziel, die Regelungen weiter zu verschärfen, sodass auch Warnungen einen Merge verhindern.\\
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}
Beginnend mit dem fünften Meilenstein soll diese Form ebenfalls durch einen Job in der CI-Pipeline überprüft werden.
\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}
\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 Teamkonventionen dokumentiert:
\begin{itemize}
\item Die Verwendung von Branches - Feature-Branches werden in \texttt{main} zusammengeführt
\item Merge-Richtlinie - CI muss bestanden werden. Keine ausdrückliche Genehmigung durch Kollegen erforderlich \footnote{Diese Regelung wird sich voraussichtlich ändern. Siehe dazu den Abschnitt zur \texttt{CODEOWNERS}-Datei.}
\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}}
Beginnend mit dem fünften Meilenstein soll eine \texttt{CODEOWNERS}-Datei erstellt werden, welche Teammitgliedern die Eigentumsrechte an bestimmten Teilen des Programmcodes zuspricht.
Wollen andere Teammitglieder Änderungen an diesen Teilen vornehmen, muss der Eigentümer sie freigeben.\\
Aktuell ist jedoch noch unklar, ob diese Funktion genutzt werden kann, da die Instanz mindestens die \textit{Premium}-Stufe haben muss.
\subsubsection{Automatisiertes CI-Linting und Build-Verifizierung}
Jeder Push und jede Merge-Anfrage löst die CI-Pipeline aus.
Für Pushes und Merge-Anfrage 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}
Fehlschlagende Jobs verhindern einen Push nicht, jedoch wird ein Merge so lange verhindert, bis alle Jobs fehlerfrei abgeschlossen werden können.
\subsection{Testverfahren}
Unit-Tests werden mit \textit{JUnit 5} geschrieben und in der Testphase der CI-Pipeline ausgeführt. Ein fehlgeschlagener Test blockiert die Merge-Anfrage.
Als weiteres Werkzeug verwenden wir \textit{JaCoCo} für die Ermittlung der Testabdeckung. \\
Die aktuelle Testabdeckung ist wie folgt:
\begin{table}[H]
\centering
\renewcommand{\arraystretch}{1.4}
\begin{tabular}{L{7.5cm} r r}
\toprule
\textbf{Paketname (gekürzt)} & \textbf{Instr.\ Cov.} & \textbf{Branch Cov.} \\
\midrule
\texttt{client.chat} & 0\,\% & 0\,\% \\
\texttt{client.game} & 0\,\% & 0\,\% \\
\texttt{client.network} & 3\,\% & 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} & 94\,\% & 91\,\% \\
\texttt{server.domain.*} & 45\,\% & 37\,\% \\
\texttt{server.network.transport} & 0\,\% & 0\,\% \\
\texttt{server.network.protocol} & 0\,\% & 0\,\% \\
\texttt{server.network.*} & 2\,\% & 1\,\% \\
\midrule
\textbf{Summe} & \textbf{12\,\%} & \textbf{9\,\%} \\
\bottomrule
\end{tabular}
\end{table}
Unser Ziel ist es, realistische Anforderungen an unsere Testabdeckung zu stellen.
Bis zum fünften Meilenstein wollen wir daher für wichtige Bereiche, wie die Game-Engine und interne Komponenten des Netzwerks, eine möglichst hohe Abdeckung von mindestens 50\% erzielen.
\end{document}
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.
@@ -7,6 +7,12 @@ import org.apache.logging.log4j.Logger;
/** Main entry point for Casono application. Handles client and server startup. */
public final class Main {
private static final int MIN_ARGS_FOR_USERNAME = 3;
private static final int ARGS_COUNT_SERVER = 2;
private static final int ARGS_COUNT_CLIENT_MIN = 2;
private static final int ARGS_COUNT_CLIENT_WITH_USER = 3;
/**
* Main entry point for Casono.
*
@@ -17,10 +23,13 @@ public final class Main {
printUsage();
System.exit(1);
}
switch (args[0]) {
case "server" -> ServerApp.start(args[1]);
case "client" -> ClientApp.start(args[1]);
case "client" -> {
String address = args[1];
String username = args.length >= MIN_ARGS_FOR_USERNAME ? args[2] : null;
ClientApp.start(address, username);
}
default -> {
printUsage();
System.exit(1);
@@ -29,23 +38,26 @@ public final class Main {
}
private static boolean isValid(String[] args) {
if (args.length != 2) {
if (args.length < ARGS_COUNT_SERVER) {
return false;
}
return switch (args[0]) {
case "server", "client" -> true;
case "server" -> args.length == ARGS_COUNT_SERVER;
case "client" ->
args.length == ARGS_COUNT_CLIENT_MIN
|| args.length == ARGS_COUNT_CLIENT_WITH_USER;
default -> false;
};
}
private static void printUsage() {
Logger logger = LogManager.getLogger(Main.class);
Logger logger = LogManager.getLogger(Main.class.getSimpleName());
logger.fatal(
"""
Usage:
java -jar xyz.jar server <listenPort>
java -jar xyz.jar client <serverIp>:<serverPort>
java -jar xyz.jar client <serverIp>:<serverPort> [<username>]
""");
}
}
@@ -1,41 +1,131 @@
package ch.unibas.dmi.dbis.cs108.casono.client;
/**
* Entry point for the Casono client application. Handles client startup and connection parameters.
*/
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.network.LoginResult;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.Launcher;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Entry point for the Casono client application. Handles client startup and connection parameters.
* Entry point and bootstrap helper for the Casono client application.
*
* <p>Default constructor for the 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
*/
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;
private static volatile String sharedUsername;
/** Default constructor. */
public ClientApp() {
// Default constructor
}
/**
* Starts the client application with the given address.
*
* @param arg Address in the format "ip:port".
* @throws IllegalArgumentException if the address format is invalid.
*/
public static void start(String arg) {
public static ClientService getSharedClientService() {
return sharedClientService;
}
private static void setSharedClientService(ClientService cs) {
sharedClientService = cs;
}
public static String getSharedUsername() {
return sharedUsername;
}
public static void updateSharedUsername(String username) {
setSharedUsername(normalizeUsername(username));
}
private static String normalizeUsername(String username) {
if (username == null || username.isBlank()) {
return null;
}
return username.trim();
}
private static void setSharedUsername(String username) {
sharedUsername = username;
LOGGER.info("sharedUsername set to '{}'", getSharedUsername());
}
public static void start(String arg, String username) {
String[] hostPort = parseHostAndPort(arg);
String host = hostPort[0];
int port = Integer.parseInt(hostPort[1]);
String normalizedUsername = normalizeUsername(username);
configureServerProperties(host, port);
setSharedUsername(normalizedUsername);
performStartupLogin(host, port, normalizedUsername);
launchUI(arg);
}
private static String[] parseHostAndPort(String arg) {
String[] parts = arg.split(":", 2);
if (parts.length != 2) {
throw new IllegalArgumentException("Address must be in format <ip>:<port>.");
throw new IllegalArgumentException("The address must be in the format <ip>:<port>.");
}
String host = parts[0];
int port = Integer.parseInt(parts[1]);
return parts;
}
private static void configureServerProperties(String host, int port) {
LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host);
Launcher.main(new String[] {});
System.setProperty("casono.server.host", host);
System.setProperty("casono.server.port", String.valueOf(port));
}
private static void performStartupLogin(String host, int port, String username) {
try {
ClientService clientService = new ClientService(host, port);
setSharedClientService(clientService);
LobbyClient lobbyClient = new LobbyClient(clientService);
LoginResult res = lobbyClient.login(username);
if (res != null && res.getUsername() != null && !res.getUsername().isBlank()) {
setSharedUsername(res.getUsername().trim());
}
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 = getSharedUsername();
if (username == null || username.isBlank()) {
Launcher.main(new String[] {arg});
return;
}
Launcher.main(new String[] {arg, username});
}
public static void main(String[] args) {
if (args == null || args.length == 0) {
throw new IllegalArgumentException("Address argument required: <host:port>");
}
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();
}
@@ -0,0 +1,416 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ChatClient;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatViewController;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.WeakHashMap;
import java.util.function.Consumer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jspecify.annotations.Nullable;
/**
* responsible for the transferring of messages from the server to the ChatModel or from the
* ChatViewController to the server
*/
public class ChatController {
/** Ensures one active message poller per physical ClientService connection. */
private static final Map<ClientService, ChatController> ACTIVE_CONTROLLERS =
new WeakHashMap<>();
private volatile String username;
private ClientService clientService;
private final ChatClientInterface chatClient;
public ChatBoxController getChatBoxController() {
return chatBoxController;
}
public void setChatBoxController(ChatBoxController chatBoxController) {
this.chatBoxController = chatBoxController;
}
private ChatBoxController chatBoxController;
private int lobbyId = -1;
private final Timer timer;
private final Consumer<List<String>> serverEventListener;
public record ChatKey(ChatType type, @Nullable String targetUser) {
public ChatKey(ChatType type) {
this(type, null);
}
}
public Map<ChatKey, ChatModel> getChatModelMap() {
return chatModelMap;
}
private final Map<ChatKey, ChatModel> chatModelMap;
private static final long REFRESH_TIME = 1000;
/** 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;
/**
* Constructor, adds TimerTask to be sent to the server regularly
*
* @param username
* @param clientService
*/
public ChatController(String username, ClientService clientService) {
this.username = username;
this.clientService = clientService;
chatClient = new ChatClient(clientService);
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<>();
registerAsActiveController(clientService);
clientService.addEventListener(serverEventListener);
this.timer = new Timer(true);
timer.schedule(
new TimerTask() {
@Override
public void run() {
try {
receiveMessage();
checkWhisperUsers();
} catch (RuntimeException e) {
logger.warn("Chat refresh failed: {}", e.getMessage());
}
}
},
0,
REFRESH_TIME);
}
/**
* Another constructor to give the ChatClient directly instead of the Client Service used only
* for test purposes
*
* @param username
*/
public ChatController(String username, ChatClientInterface chatClient) {
this.username = username;
this.chatClient = chatClient;
chatModelMap = new LinkedHashMap<>();
localUserList = new ArrayList<>();
this.chatBoxController = new ChatBoxController(username, this);
this.logger = LogManager.getLogger(ChatController.class.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);
if (oldController != null && oldController != this) {
oldController.shutdown();
logger.info("Replaced previous ChatController poller for shared ClientService");
}
ACTIVE_CONTROLLERS.put(clientService, this);
}
}
/**
* Method to be activated, if a lobby has been chosen. It will update the UI and add a new
* ChatModel to hold the Data for the Lobby Chat
*
* @param lobbyId
*/
public void setLobbyChat(int lobbyId) {
this.lobbyId = lobbyId;
ChatKey key = new ChatKey(ChatType.LOBBY);
ChatModel existingModel = chatModelMap.get(key);
if (existingModel != null) {
existingModel.lobbyId = lobbyId;
return;
}
ChatModel lobbyChatModel = new ChatModel(ChatType.LOBBY, username, lobbyId, null);
chatModelMap.put(key, lobbyChatModel);
this.chatBoxController.addChatTab("LOBBY", lobbyChatModel, ChatType.LOBBY);
}
/**
* Method to process the Messages that got polled by the {@link ChatClient}.
*
* <p>Case distinction for every message: - Messages identified with ChatType.LOBBY -> check if
* they belong to the users lobby. - Messages identified with ChatType.GLOBAL -> check if the
* target user of that Message is the user, or the message got sent by the user.
*
* <p>All Messages get added to a particular {@link ChatModel}, if the checks passed.
*/
public void receiveMessage() {
String currentUsername = getCurrentUsername();
List<Message> newMessages = chatClient.getMessages();
if (!newMessages.isEmpty()) {
for (Message msg : newMessages) {
switch (msg.getMessageType()) {
case ChatType.GLOBAL:
chatModelMap.get(new ChatKey(ChatType.GLOBAL)).addMessage(msg);
break;
case ChatType.LOBBY:
if (msg.lobbyId == getActiveLobbyChatId()) {
chatModelMap
.computeIfAbsent(
new ChatKey(ChatType.LOBBY),
(_key) ->
new ChatModel(
ChatType.LOBBY,
currentUsername,
msg.lobbyId,
null))
.addMessage(msg);
}
break;
case ChatType.WHISPER:
if (msg.target.equals(currentUsername)
|| msg.sender.equals(currentUsername)) {
ChatKey key;
if (msg.target.equals(currentUsername)) {
key = new ChatKey(ChatType.WHISPER, msg.sender);
} else {
key = new ChatKey(ChatType.WHISPER, msg.target);
}
if (chatModelMap.containsKey(key)) {
chatModelMap.get(key).addMessage(msg);
} else {
ChatModel chatModel =
new ChatModel(
ChatType.WHISPER,
currentUsername,
lobbyId,
key.targetUser());
chatBoxController.addWhisperChat(key.targetUser(), chatModel);
chatModel.addMessage(msg);
}
}
break;
}
}
}
}
/**
* Method to get the lobbyId of the currently active lobby chat, to check if incoming lobby
* messages belong to the same lobby.
*
* @return the lobbyId of the currently active lobby chat.
*/
private int getActiveLobbyChatId() {
ChatModel lobbyModel = chatModelMap.get(new ChatKey(ChatType.LOBBY));
if (lobbyModel != null) {
return lobbyModel.lobbyId;
}
return lobbyId;
}
/**
* Method to process the usernames of other users connected to the server, after they got polled
* by the {@link ChatClient}.
*
* <p>Checks for every one of the usernames, if it is already in the list localUserList and if
* not adds them. For every new User added in the List, they get added as an option to start a
* Whisper Chat with.
*/
public synchronized void checkWhisperUsers() {
List<String> users = chatClient.getUsers();
Set<String> remoteUsers = new HashSet<>();
String currentUsername = getCurrentUsername();
for (String user : users) {
if (user == null || !user.contains("=")) {
continue;
}
String value = user.split("\\=", 2)[1].trim();
if (value.isBlank() || value.equals(currentUsername)) {
continue;
}
remoteUsers.add(value);
}
for (String known : new ArrayList<>(localUserList)) {
if (!remoteUsers.contains(known)) {
localUserList.remove(known);
chatBoxController.removeWhisperUser(known);
}
}
for (String remote : remoteUsers) {
addWhisperUser(remote);
}
}
/**
* Registers a whisper target locally and updates the UI if it is a new user.
*
* @param user target username
*/
public synchronized void addWhisperUser(String user) {
if (user == null || user.isBlank()) {
return;
}
if (!(localUserList.contains(user) || user.equals(getCurrentUsername()))) {
localUserList.add(user);
logger.info("adding new whisper user");
chatBoxController.addWhisperUser(user);
}
}
public synchronized void updateUsername(String newUsername) {
if (newUsername == null || newUsername.isBlank()) {
return;
}
String oldUsername = this.username;
this.username = newUsername.trim();
chatBoxController.setUsername(this.username);
if (oldUsername != null && !oldUsername.equals(this.username)) {
localUserList.remove(oldUsername);
chatBoxController.removeWhisperUser(oldUsername);
}
}
private void handleServerEvent(List<String> lines) {
if (lines == null || lines.isEmpty()) {
return;
}
List<RequestParameter> params;
try {
params = ClientService.convertToRequestParameters(lines);
} catch (RuntimeException e) {
return;
}
String event = null;
String oldUsername = null;
String newUsername = null;
for (RequestParameter p : params) {
if ("EVENT".equalsIgnoreCase(p.key())) {
event = p.value();
} else if ("OLD_USERNAME".equalsIgnoreCase(p.key())) {
oldUsername = p.value();
} else if ("NEW_USERNAME".equalsIgnoreCase(p.key())) {
newUsername = p.value();
}
}
if (!"USERNAME_CHANGED".equalsIgnoreCase(event)) {
return;
}
applyUsernameMigration(oldUsername, newUsername);
}
private synchronized void applyUsernameMigration(String oldUsername, String newUsername) {
if (oldUsername == null
|| newUsername == null
|| oldUsername.isBlank()
|| newUsername.isBlank()
|| oldUsername.equals(newUsername)) {
return;
}
String currentUsername = getCurrentUsername();
if (oldUsername.equals(currentUsername)) {
updateUsername(newUsername);
}
ChatKey oldKey = new ChatKey(ChatType.WHISPER, oldUsername);
ChatKey newKey = new ChatKey(ChatType.WHISPER, newUsername);
ChatModel oldModel = chatModelMap.remove(oldKey);
ChatModel existingNewModel = chatModelMap.get(newKey);
if (oldModel != null) {
oldModel.setTarget(newUsername);
if (existingNewModel == null) {
chatModelMap.put(newKey, oldModel);
} else {
// Merge possible parallel history into the already existing new-key model.
for (Message msg : oldModel.messages) {
existingNewModel.addMessage(msg);
}
}
chatBoxController.renameWhisperUser(oldUsername, newUsername);
}
localUserList.remove(oldUsername);
chatBoxController.removeWhisperUser(oldUsername);
if (!newUsername.equals(getCurrentUsername())) {
addWhisperUser(newUsername);
}
}
public String getCurrentUsername() {
String shared = ClientApp.getSharedUsername();
if (shared != null && !shared.isBlank()) {
return shared.trim();
}
return username;
}
/** Stops polling background tasks for this chat controller instance. */
public void shutdown() {
timer.cancel();
clientService.removeEventListener(serverEventListener);
synchronized (ACTIVE_CONTROLLERS) {
if (ACTIVE_CONTROLLERS.get(clientService) == this) {
ACTIVE_CONTROLLERS.remove(clientService);
}
}
}
/**
* method to send a message to the server
*
* @param message
*/
public void onSendToNetwork(Message message) {
chatClient.sendMessage(message);
}
}
@@ -0,0 +1,84 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import java.util.ArrayList;
import java.util.function.Consumer;
/**
* ChatModel, stores the data for a specific chat
*
* <p>Holds the current state of a chat
*/
public class ChatModel {
private final ArrayList<Consumer<Message>> listeners = new ArrayList<>();
public ArrayList<Message> messages;
private final ChatType chattype;
/** The person currently using this client */
public final String username;
/** The person to send the message to If the chat is a whisper chat */
private String target;
public int lobbyId;
/**
* Constructs a new ChatModel for a specific chat type.
*
* @param chattype The type of chat (e.g., GLOBAL, LOBBY, or WHISPER).
* @param username The username of the current user.
* @param lobbyId The ID of the lobby, or -1 if not applicable.
* @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<>();
this.chattype = chattype;
this.username = username;
this.lobbyId = lobbyId;
this.target = target;
}
/**
* Returns the type of chat this model represents.
*
* @return The {@link ChatType}.
*/
public ChatType getChattype() {
return chattype;
}
/**
* Adds a new message to the history and notifies all registered listeners. This method is
* synchronized to ensure thread safety when updating the message list.
*
* @param msg The {@link Message} to be added.
*/
public synchronized void addMessage(Message msg) {
messages.add(msg);
listeners.forEach((l) -> l.accept(messages.getLast()));
}
/**
* Registers a listener to be notified whenever a new message is added to this model.
*
* @param listener A {@link Consumer} that processes the new {@link Message}.
*/
public void addListener(Consumer<Message> listener) {
this.listeners.add(listener);
}
/**
* Returns the target user for this chat, primarily used for whispers.
*
* @return The target username or null.
*/
public String getTarget() {
return target;
}
public void setTarget(String target) {
this.target = target;
}
}
@@ -0,0 +1,8 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
/** Describing the Type of the Chat */
public enum ChatType {
GLOBAL,
LOBBY,
WHISPER
}
@@ -0,0 +1,209 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
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.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Optional;
import org.jspecify.annotations.NonNull;
/** Message Object for internal handling of Chat-Messages */
public class Message {
private final ChatType type;
private final String message;
public String sender;
public String timestamp;
public int lobbyId;
public String target;
/**
* Constructs a Message with a provided timestamp. Typically used when reconstructing messages
* received from the server. Used for Messages in the Lobby Chat.
*
* @param lobbyId The ID of the lobby, or -1 if not applicable.
* @param sender The username of the message creator.
* @param timestamp The formatted time string (e.g., "HH:mm").
* @param message The actual text content of the message.
*/
private Message(int lobbyId, String sender, String timestamp, String message) {
this.type = ChatType.LOBBY;
this.lobbyId = lobbyId;
this.sender = sender;
this.target = null;
this.timestamp = timestamp;
this.message = message;
}
/**
* Constructs a Message with a provided timestamp. Typically used when reconstructing messages
* received from the server. Used for Messages in the WHISPER and GLOBAL Chat.
*
* @param type The chat category (e.g., GLOBAL or WHISPER).
* @param sender The username of the message creator.
* @param timestamp The formatted time string (e.g., "HH:mm").
* @param message The actual text content of the message.
*/
private Message(ChatType type, String sender, String target, String timestamp, String message) {
this.type = type;
this.lobbyId = -1;
this.sender = sender;
this.target = target;
this.timestamp = timestamp;
this.message = message;
}
/**
* Constructs a new Message for the current user. Automatically generates a timestamp based on
* the local system time ("HH:mm").
*
* @param type The chat category (e.g., GLOBAL, LOBBY, or WHISPER).
* @param lobbyId The ID of the lobby, or -1 if not applicable.
* @param sender The username of the current user.
* @param target The username of the recipient (for whispers).
* @param message The actual text content to be sent.
*/
public Message(ChatType type, int lobbyId, String sender, String target, String message) {
this.type = type;
this.lobbyId = lobbyId;
this.sender = sender;
this.target = target;
this.message = message;
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");
this.timestamp = now.format(formatter);
}
/**
* Returns the text content of the message.
*
* @return The message string.
*/
public String getMessage() {
return message;
}
/**
* Returns the type of chat this message belongs to.
*
* @return The {@link ChatType}.
*/
public ChatType getMessageType() {
return type;
}
/**
* Formats the message object into a string representation compatible with the network protocol
* arguments.
*
* @return A formatted string containing all message attributes for server transmission.
*/
public String toArgsString() {
String gameIdString;
if (lobbyId >= 0) {
gameIdString = " GAME=" + lobbyId;
} else {
gameIdString = " GAME='-1'";
}
return String.format(
"TYPE=%s%s USER='%s' TARGET='%s' TIME='%s' TEXT='%s'",
this.type.toString(),
gameIdString,
this.sender,
this.target,
this.timestamp,
this.message);
}
/**
* Parses a list of network request parameters to reconstruct a Message object. Handles
* different chat types (GLOBAL, LOBBY, WHISPER) and their specific requirements.
*
* @param parameters A list of {@link RequestParameter} received from the network.
* @return A new {@link Message} instance populated with the parsed data.
*/
public static Message toMessageReqPars(List<RequestParameter> parameters) {
String typeString = getParString(parameters, "TYPE");
ChatType type = ChatType.valueOf(typeString);
return switch (type) {
case GLOBAL ->
new Message(
ChatType.GLOBAL,
getParString(parameters, "USER"),
null,
getParString(parameters, "TIME"),
getParString(parameters, "TEXT"));
case LOBBY ->
new Message(
Integer.parseInt(getParString(parameters, "GAME")),
getParString(parameters, "USER"),
getParString(parameters, "TIME"),
getParString(parameters, "TEXT"));
case WHISPER ->
new Message(
ChatType.WHISPER,
getParString(parameters, "USER"),
getParString(parameters, "TARGET"),
getParString(parameters, "TIME"),
getParString(parameters, "TEXT"));
};
}
/**
* Helper method to extract a specific parameter value by its key.
*
* @param parameters The list of parameters to search.
* @param keyString The key to look for.
* @return The value associated with the key.
* @throws RuntimeException if the key is not found.
*/
private static @NonNull String getParString(
List<RequestParameter> parameters, String keyString) {
return getParString(parameters, keyString, null);
}
/**
* Helper method to extract a specific parameter value by its key, with a fallback default
* value.
*
* @param parameters The list of parameters to search.
* @param keyString The key to look for.
* @param defaultVal The value to return if the key is missing.
* @return The found value or the default value.
*/
private static @NonNull String getParString(
List<RequestParameter> parameters, String keyString, String defaultVal) {
Optional<String> parOption =
parameters.stream()
.filter((p) -> keyString.equals(p.key()))
.findFirst()
.map(RequestParameter::value);
if (parOption.isEmpty()) {
if (defaultVal == null) {
throw new RuntimeException("No " + keyString + " found");
} else {
return defaultVal;
}
}
return parOption.get();
}
/**
* Converts the message object into a network response body using the provided builder.
*
* @param builder The {@link ResponseBodyBuilder} used to construct the response.
* @return The built {@link ResponseBody} containing the message data.
*/
public ResponseBody toResponse(ResponseBodyBuilder builder) {
builder.param("TYPE", type.name());
builder.param("GAME", lobbyId);
builder.param("USER", this.sender);
if (target != null) {
builder.param("TARGET", target);
}
builder.param("TIME", this.timestamp);
builder.param("TEXT", this.message);
return builder.build();
}
}
@@ -0,0 +1,54 @@
package ch.unibas.dmi.dbis.cs108.casono.client.game;
/** Represents a playing card with a value and suit. */
public class Card {
private String value;
private String suit;
/**
* Constructs a Card with the specified value and suit.
*
* @param value The value of the card (e.g., 2, 3, ..., 10, J, Q, K, A).
* @param suit The suit of the card (e.g., Hearts, Diamonds, Clubs, Spades).
*/
public Card(String value, String suit) {
this.value = value;
this.suit = suit;
}
/**
* Retrieves the value of the card.
*
* @return The value of the card.
*/
public String getValue() {
return value;
}
/**
* Retrieves the suit of the card.
*
* @return The suit of the card.
*/
public String getSuit() {
return suit;
}
/**
* Sets the value of the card.
*
* @param value The value to set for the card.
*/
public void setValue(String value) {
this.value = value;
}
/**
* Sets the suit of the card.
*
* @param suit The suit to set for the card.
*/
public void setSuit(String suit) {
this.suit = suit;
}
}
@@ -0,0 +1,217 @@
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;
public class GameService {
private static final Logger LOG = Logger.getLogger(GameService.class.getName());
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.
*
* @param client The GameClient used to communicate with the server. Must not be null.
*/
public GameService(GameClient client) {
if (client == null) {
throw new IllegalArgumentException("GameClient must not be null");
}
this.client = client;
LOG.info("GameService created");
}
/**
* Refreshes the game state by fetching the latest state from the server using the GameClient.
*
* @return The updated GameState after refreshing.
*/
public GameState refresh() {
LOG.fine("Refreshing game state...");
GameState newState;
try {
newState = client.fetchGameState();
} catch (Exception e) {
// Shouldn't happen with the new GameClient, but keep service stable.
LOG.log(Level.WARNING, "refresh() failed: " + e.getMessage(), e);
return state;
}
if (newState == null) {
LOG.fine("No new state (null) -> keeping previous state");
return state;
}
this.state = newState;
LOG.info(
() ->
"State updated: phase="
+ state.phase
+ " pot="
+ state.pot
+ " players="
+ (state.players != null ? state.players.size() : 0)
+ " community="
+ (state.communityCards != null ? state.communityCards.size() : 0));
return state;
}
/**
* Retrieves the current phase of the game.
*
* @return The current game phase as a string (e.g., "Pre-Flop", "Flop", "Turn", "River").
*/
public int getPot() {
ensureState();
return state.pot;
}
/**
* Retrieves the current phase of the game.
*
* @return The current game phase as a string (e.g., "Pre-Flop", "Flop", "Turn", "River").
*/
public List<Card> getCommunityCards() {
ensureState();
return state.communityCards != null ? state.communityCards : List.of();
}
/**
* Retrieves the list of players currently in the game.
*
* @return A list of Player objects representing the players in the game. A list of Player
* objects representing the players in the game.
*/
public List<Player> getPlayers() {
ensureState();
return state.players != null ? state.players : List.of();
}
/**
* Retrieves the index of the current player whose turn it is.
*
* @return The index of the current player in the players list, or -1 if not available.
*/
public Player getWinner() {
ensureState();
if (state.winnerIndex < 0
|| state.players == null
|| state.winnerIndex >= state.players.size()) {
return null;
}
return state.players.get(state.winnerIndex);
}
/** Retrieves the current phase of the game. */
public void call() {
queueAction(() -> client.sendCall());
}
/** Retrieves the current phase of the game. */
public void fold() {
queueAction(() -> client.sendFold());
}
/**
* Retrieves the current phase of the game.
*
* @param amount The amount to bet. Must be a positive integer.
*/
public void bet(int amount) {
queueAction(() -> client.sendBet(amount));
}
/**
* Retrieves the current phase of the game.
*
* @param amount The amount to raise. Must be a positive integer.
*/
public void raise(int 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. */
private void ensureState() {
if (state == null) {
throw new IllegalStateException("GameService used before any successful refresh()");
}
}
/**
* Returns the current GameState without refreshing from the server.
*
* @return The current GameState, or null if it has not been initialized yet.
*/
public GameState peekStateOrNull() {
return state;
}
}
@@ -0,0 +1,23 @@
package ch.unibas.dmi.dbis.cs108.casono.client.game;
import java.util.ArrayList;
import java.util.List;
/**
* Represents the current state of the poker game, including the phase, pot size, current bet,
* dealer position, active player, community cards, and player information.
*/
public class GameState {
public List<Player> players = new ArrayList<>();
public List<Card> communityCards = new ArrayList<>();
public List<String> highscoreEntries = new ArrayList<>();
public String phase;
public int pot;
public int currentBet;
public int dealer;
public int activePlayer;
public int winnerIndex = -1;
public List<String> winnerNames = new ArrayList<>();
public int potPerWinner;
}
@@ -0,0 +1,193 @@
package ch.unibas.dmi.dbis.cs108.casono.client.game;
import java.util.ArrayList;
import java.util.List;
/**
* Represents a player in the poker game, including their name, chip count, current bet, state
* (e.g., "active", "folded"), and their hole cards.
*/
public class Player {
private PlayerId id;
private int chips;
private int bet;
private PlayerState state;
private List<Card> cards = new ArrayList<>();
/**
* Constructs a new Player with default values. The player's state is set to ACTIVE by default.
*/
public Player() {
this.state = PlayerState.ACTIVE;
}
/**
* Constructs a new Player with the specified ID and initial chip count. The player's state is
* set to ACTIVE by default.
*
* @param id The unique identifier for the player.
* @param chips The initial number of chips the player has.
*/
public Player(PlayerId id, int chips) {
this.id = id;
this.chips = chips;
this.bet = 0;
this.state = PlayerState.ACTIVE;
}
/**
* Retrieves the unique identifier of the player.
*
* @return The player's ID.
*/
public PlayerId getId() {
return id;
}
/**
* Sets the unique identifier of the player.
*
* @param id The player's ID to set.
*/
public void setId(PlayerId id) {
this.id = id;
}
/**
* Returns the display name of the player. If the player's ID is not set, it returns an empty
* string.
*
* @return The player's name.
*/
public String getName() {
return id != null ? id.value() : "";
}
/**
* Retrieves the current chip count of the player.
*
* @return The number of chips the player has.
*/
public int getChips() {
return chips;
}
/**
* Sets the current chip count of the player.
*
* @param chips The number of chips to set for the player.
*/
public void setChips(int chips) {
this.chips = chips;
}
/**
* Retrieves the current bet amount of the player.
*
* @return The amount the player has currently bet in the ongoing hand.
*/
public int getBet() {
return bet;
}
/**
* Sets the current bet amount of the player.
*
* @param bet The amount the player has currently bet in the ongoing hand to set.
*/
public void setBet(int bet) {
this.bet = bet;
}
/**
* Retrieves the current state of the player (e.g., ACTIVE, FOLDED, ALL_IN).
*
* @return The player's current state in the game.
*/
public PlayerState getState() {
return state;
}
/**
* Sets the current state of the player (e.g., ACTIVE, FOLDED, ALL_IN).
*
* @param state The player's current state in the game to set.
*/
public void setState(PlayerState state) {
this.state = state;
}
/**
* Checks if the player is currently the dealer.
*
* @return True if the player's state is DEALER, false otherwise.
*/
public boolean isDealer() {
return state == PlayerState.DEALER;
}
/**
* Retrieves the list of hole cards currently held by the player.
*
* @return A list of Card objects representing the player's hole cards.
*/
public List<Card> getCards() {
return cards;
}
/**
* Sets the list of hole cards currently held by the player.
*
* @param cards A list of Card objects representing the player's hole cards to set.
*/
public void setCards(List<Card> cards) {
this.cards = cards;
}
/**
* Adds a card to the player's list of hole cards.
*
* @param card The Card object to be added to the player's hole cards.
*/
public void addCard(Card card) {
this.cards.add(card);
}
/**
* Adds the specified amount of chips to the player's current chip count.
*
* @param amount The number of chips to add to the player's current chip count.
*/
public void addChips(int amount) {
if (amount < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
this.chips += amount;
}
/**
* Removes the specified amount of chips from the player's current chip count.
*
* @param amount The number of chips to remove from the player's current chip count.
*/
public void removeChips(int amount) {
if (amount < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
if (amount > this.chips) {
throw new IllegalArgumentException(
"Not enough chips. Available: " + this.chips + ", Requested: " + amount);
}
this.chips -= amount;
}
/** Sets the player's state to FOLDED, indicating that they have folded in the current hand. */
public void fall() {
this.state = PlayerState.FOLDED;
if (this.id != null) {
this.id = PlayerId.of(this.id.value() + " (Fall)");
}
}
}
@@ -0,0 +1,66 @@
package ch.unibas.dmi.dbis.cs108.casono.client.game;
import java.util.Objects;
/** Represents a unique identifier for a player in the client domain. */
public record PlayerId(String value) {
/**
* Constructs a PlayerId with the given string value, ensuring it is not null or empty.
*
* @param value the string value representing the player's unique identifier.
*/
public PlayerId {
Objects.requireNonNull(value, "PlayerId cannot be null");
value = value.trim();
if (value.isEmpty()) {
throw new IllegalArgumentException("PlayerId cannot be empty");
}
}
/** Factory method to create a PlayerId. */
public static PlayerId of(String value) {
return new PlayerId(value);
}
/**
* Returns the string representation of the PlayerId, which is the encapsulated value.
*
* @return the string value of the PlayerId.
*/
@Override
public String toString() {
return value;
}
/**
* Compares this PlayerId with another object for equality.
*
* @param o the object to compare to.
* @return true if this PlayerId is equal to the given object, false otherwise.
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PlayerId playerId = (PlayerId) o;
return Objects.equals(value, playerId.value);
}
/**
* Returns the hash code for this PlayerId.
*
* @return the hash code of the PlayerId.
*/
@Override
public int hashCode() {
return Objects.hash(value);
}
}
@@ -0,0 +1,8 @@
package ch.unibas.dmi.dbis.cs108.casono.client.game;
/** Enumeration representing the possible states of a player in the poker game. */
public enum PlayerState {
ACTIVE,
FOLDED,
DEALER
}
@@ -0,0 +1,92 @@
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;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* The ChatClient class is responsible for sending messages to the server and retrieving messages
* from the server. It uses the ClientService to send commands and receive responses from the
* server.
*/
public class ChatClient implements ChatClientInterface {
private final ClientService clientService;
private final Logger logger;
/**
* Constructs a ChatClient with the given ClientService for communication.
*
* @param clientService The ClientService instance used to send commands and receive responses
* from the server.
*/
public ChatClient(ClientService clientService) {
this.clientService = clientService;
this.logger = LogManager.getLogger(ChatClient.class.getSimpleName());
}
/**
* Send a Message to the server by converting it to a string format and sending a "SEND_MESSAGE"
* command with the message content as arguments.
*
* @param message The Message object to be sent to the server.
*/
public void sendMessage(Message message) {
String request = "SEND_MESSAGE " + message.toArgsString();
logger.info("Writing to server: " + request);
clientService.processCommand(request);
}
/**
* Retrieve messages from the server by first sending a "GET_MESSAGE_COUNT" command to determine
* how many messages are available and then sending "GET_NEXT_MESSAGE" commands in a loop to
* retrieve each message. The retrieved messages are parsed into Message objects and returned as
* a list.
*
* @return A list of Message objects representing the messages retrieved from the server.
*/
public List<Message> getMessages() {
logger.info("Asking server for new messages");
List<RequestParameter> countStr =
ClientService.convertToRequestParameters(
clientService.processCommand("GET_MESSAGE_COUNT"));
RequestParameter countRes = countStr.getFirst();
if (!countRes.key().equals("COUNT")) {
logger.error("Not the right response from server");
}
int count = Integer.parseInt(countRes.value());
logger.info("Got " + count + " messages");
ArrayList<Message> messages = new ArrayList<>();
for (int i = 0; i < count; i++) {
List<RequestParameter> msgRes =
ClientService.convertToRequestParameters(
clientService.processCommand("GET_NEXT_MESSAGE"));
Message msg = Message.toMessageReqPars(msgRes);
messages.add(msg);
}
return messages;
}
/**
* Method to poll the usernames of all users currently connected to the server, by sending the
* "LIST_USERS" command.
*
* @return List of all usernames retrieved from the server.
*/
public List<String> getUsers() {
logger.info("Asking server for list of users");
List<String> users = clientService.processCommand("LIST_USERS");
List<String> parameters = new ArrayList<>();
for (String line : users) {
String trimmed = line == null ? "" : line.trim();
if (trimmed.startsWith("USERNAME=")) {
parameters.add(trimmed);
}
}
return parameters;
}
}
@@ -0,0 +1,432 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* The ClientService class is responsible for managing the connection to the server, sending
* commands, and receiving responses. It uses a TcpTransport to communicate with the server and an
* ExecutorService to handle asynchronous requests.
*/
public class ClientService {
private final TcpTransport clienttcptransport;
private final Socket socket;
private final ExecutorService executor;
private final boolean offlineMode;
private final AtomicInteger idGenerator;
private Logger logger;
private final Map<Integer, ArrayBlockingQueue<ParsedResponse>> pendingResponses =
new ConcurrentHashMap<>();
private final CopyOnWriteArrayList<Consumer<List<String>>> eventListeners =
new CopyOnWriteArrayList<>();
private Thread readerThread = null;
private final AtomicBoolean running = new AtomicBoolean(false);
private static final int READER_JOIN_TIMEOUT_MS = 500;
/**
* Constructs a ClientService with the given server IP and port. It establishes a socket
* connection to the server and initializes the TcpTransport and ExecutorService for
* communication.
*
* @param ip The IP address of the server to connect to.
* @param port The port number of the server to connect to.
*/
public ClientService(String ip, int port) {
this.idGenerator = new AtomicInteger(0);
this.logger = LogManager.getLogger(ClientService.class.getSimpleName());
this.offlineMode = false;
try {
socket = new Socket(ip, port);
clienttcptransport = new TcpTransport(socket);
logger.info("Connected to server at " + ip);
} catch (IOException i) {
throw new RuntimeException(i);
}
executor = Executors.newSingleThreadExecutor();
startReaderThread();
}
private void startReaderThread() {
running.set(true);
readerThread =
new Thread(
() -> {
while (running.get()) {
try {
RawPacket rp = clienttcptransport.read();
processRawPacket(rp);
} catch (IOException e) {
if (running.get()) {
logger.warn("IO error on transport reader", e);
}
break;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
},
"casono-client-reader");
readerThread.setDaemon(true);
readerThread.start();
}
private void processRawPacket(RawPacket rp) throws InterruptedException, IOException {
int rid = rp.requestId();
String responseText = rp.payload();
logger.info("Raw message '{}'", responseText);
ParsedPacket parsed = parsePacketContent(responseText);
logger.debug(
"Parsed response lines (rid={}, success={}, openBlocks={}): {}",
rid,
parsed.success,
parsed.blockStack.size(),
parsed.lines);
handleParsedResponse(rid, parsed.success, parsed.lines);
}
private ParsedPacket parsePacketContent(String responseText) {
boolean hasStatus = false;
boolean success = false;
List<String> lines = new ArrayList<>();
Deque<String> blockStack = new ArrayDeque<>();
for (String rawLine : responseText.split("\\n")) {
String line = rawLine;
String trimmed = line.trim();
if (!hasStatus) {
StatusCheckResult status = checkStatus(trimmed);
if (status != null) {
success = status.success;
hasStatus = true;
}
continue;
}
if (trimmed.isEmpty()) {
continue;
}
line = line.replaceFirst("^\\s+", "");
trimmed = line.trim();
if (isContainerStart(trimmed)) {
blockStack.push(trimmed);
lines.add(trimmed);
continue;
}
if ("END".equals(trimmed)) {
if (!blockStack.isEmpty()) {
blockStack.pop();
lines.add("END");
continue;
}
break;
}
lines.add(line);
}
if (!hasStatus) {
success = inferStatus(responseText);
}
return new ParsedPacket(success, lines, blockStack);
}
private StatusCheckResult checkStatus(String trimmed) {
if ("+OK".equals(trimmed)) {
return new StatusCheckResult(true);
}
if (trimmed.startsWith("-ERR") || trimmed.startsWith("-ERROR")) {
return new StatusCheckResult(false);
}
return null;
}
private boolean inferStatus(String responseText) {
if (responseText.contains("+OK")) {
return true;
}
if (responseText.contains("-ERR") || responseText.contains("-ERROR")) {
return false;
}
return true;
}
private void handleParsedResponse(int rid, boolean success, List<String> lines)
throws InterruptedException {
if (rid == 0) {
for (Consumer<List<String>> l : eventListeners) {
try {
l.accept(List.copyOf(lines));
} catch (Exception e) {
logger.warn("Event listener threw", e);
}
}
} else {
ArrayBlockingQueue<ParsedResponse> q = pendingResponses.get(rid);
if (q != null) {
q.put(new ParsedResponse(success, lines));
} else {
logger.warn("No pending response queue for id {}", rid);
}
}
}
private record ParsedPacket(boolean success, List<String> lines, Deque<String> blockStack) {}
private record StatusCheckResult(boolean success) {}
private boolean isContainerStart(String token) {
return token.equals("LOBBIES")
|| token.equals("LOBBY")
|| token.equals("PLAYERS")
|| token.equals("PLAYER")
|| token.equals("USERS")
|| token.equals("USER")
|| token.equals("CARDS")
|| token.equals("CARD");
}
/**
* Constructs a ClientService in offline mode. No network connection will be attempted and calls
* to processCommand will throw a RuntimeException.
*
* @param offline true to create an offline (no-network) client service
*/
public ClientService(boolean offline) {
this.idGenerator = new AtomicInteger(0);
this.offlineMode = offline;
this.socket = null;
this.clienttcptransport = null;
this.executor = Executors.newSingleThreadExecutor();
}
/** Returns true if this ClientService is running in offline mode (no network). */
public boolean isOffline() {
return offlineMode;
}
// Allow primitive values to contain hyphens (UUIDs) in addition to
// digits/words/colons
static Pattern responseRex =
Pattern.compile("(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[^ ]+))");
/**
* Removes escape characters from a string, specifically converting escaped single quotes (\')
* back to regular single quotes (').
*
* @param input The escaped string to process.
* @return The unescaped string.
*/
private static String unescape(String input) {
return input.replaceAll("\\\\'", "'");
}
/**
* Converts a list of raw string parameters into a list of {@link RequestParameter} objects. It
* uses a regex matcher to distinguish between quoted strings (which are unescaped) and
* primitive values.
*
* @param input A list of raw strings to be parsed.
* @return A list of parsed {@link RequestParameter} objects.
* @throws RuntimeException if a parameter does not match the expected format.
*/
public static List<RequestParameter> convertToRequestParameters(List<String> input) {
return input.stream()
.map((String parString) -> responseRex.matcher(parString))
.filter(Matcher::matches)
.map(
(m) -> {
if (!(m.group("string") == null)) {
return new RequestParameter(
m.group("key"), unescape(m.group("string")));
} else if (!(m.group("primVal") == null)) {
return new RequestParameter(m.group("key"), m.group("primVal"));
} else {
throw new RuntimeException();
}
})
.toList();
}
private static record ParsedResponse(boolean success, List<String> lines) {}
/**
* Register an event listener that receives unsolicited event payload lines (no status prefix).
*/
public void addEventListener(Consumer<List<String>> listener) {
eventListeners.add(listener);
}
public void removeEventListener(Consumer<List<String>> listener) {
eventListeners.remove(listener);
}
/**
* Sends a command to the server and processes the multi-line response. It handles the protocol
* handshake (expecting +OK), strips leading tabs from response lines, and collects them until
* the "END" marker is reached.
*
* @param message The raw command string to be sent to the transport layer.
* @return A list of response lines received from the server (excluding protocol markers).
* @throws RuntimeException if the server responds with an error or if a communication failure
* occurs.
*/
protected List<String> processCommand(String message) {
if (offlineMode) {
throw new RuntimeException("ClientService is offline");
}
int reqId = idGenerator.incrementAndGet();
ArrayBlockingQueue<ParsedResponse> q = new ArrayBlockingQueue<>(1);
pendingResponses.put(reqId, q);
Future<?> writeFuture =
executor.submit(
() -> {
try {
clienttcptransport.write(new RawPacket(reqId, message));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
try {
writeFuture.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
pendingResponses.remove(reqId);
throw new RuntimeException(e);
} catch (ExecutionException e) {
pendingResponses.remove(reqId);
throw getRuntimeException(e);
}
ParsedResponse pr;
try {
pr = q.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
pendingResponses.remove(reqId);
throw new RuntimeException(e);
} finally {
pendingResponses.remove(reqId);
}
if (pr.success) {
return pr.lines;
}
throw new RuntimeException("Error in " + message + ": " + pr.lines);
}
/**
* Helper method to send a request to the server using the ExecutorService. It submits the
* request as a Runnable task and waits for its completion. If the task is interrupted or
* encounters an execution exception, it throws a RuntimeException with the appropriate cause.
*
* @param request The Runnable task representing the request to be sent to the server.
*/
private void sendRequest(Runnable request) {
Future<?> future = executor.submit(request);
try {
future.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw getRuntimeException(e);
}
}
/**
* Helper method to extract the cause of an exception and return it as a RuntimeException. If
* the cause is null, it returns the original exception as a RuntimeException. If the cause is
* already a RuntimeException, it returns it directly. Otherwise, it wraps the cause in a new
* RuntimeException and returns it.
*
* @param e The exception from which to extract the cause.
* @return A RuntimeException representing the cause of the original exception.
*/
private static RuntimeException getRuntimeException(Exception e) {
Throwable cause = e.getCause();
if (cause == null) {
return e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);
}
if (cause instanceof RuntimeException) {
return (RuntimeException) cause;
}
return new RuntimeException(cause);
}
/**
* Closes the socket connection to the server and shuts down the ExecutorService. It also closes
* the TcpTransport used for communication. If any IOException occurs during this process, it
* prints the exception to the console.
*/
public void closeSocket() {
try {
running.set(false);
if (readerThread != null) {
readerThread.interrupt();
try {
readerThread.join(READER_JOIN_TIMEOUT_MS);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
executor.shutdownNow();
clienttcptransport.close();
socket.close();
logger.info("Socket closed");
} catch (IOException j) {
logger.debug(j);
}
}
// removed unused helper writeToTransport; write is done via processCommand()
// which
// manages request ids and response matching
public void ping() {
processCommand("PING");
}
}
@@ -0,0 +1,37 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
/**
* The CoreClient class provides basic functionalities for communicating with the server, such as
* sending a ping command to check connectivity and logging in with a username. It uses the
* ClientService to send commands and receive responses from the server.
*/
public class CoreClient {
private final ClientService clientService;
/**
* Constructs a CoreClient with the given ClientService for communication.
*
* @param clientservice The ClientService instance used to send commands and receive responses
* from the server.
*/
public CoreClient(ClientService clientservice) {
this.clientService = clientservice;
}
/**
* Sends a "PING" command to the server to check connectivity. The server should respond with a
* "PONG" message if the connection is successful.
*/
public void ping() {
clientService.processCommand("PING");
}
/**
* Logs in to the server with the given username by sending a "LOGIN" command.
*
* @param user The username to log in with.
*/
public void login(String user) {
clientService.processCommand("LOGIN USERNAME=" + user);
}
}
@@ -0,0 +1,321 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
import ch.unibas.dmi.dbis.cs108.casono.client.game.Card;
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameState;
import ch.unibas.dmi.dbis.cs108.casono.client.game.Player;
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Fetches and parses game state from server.
*
* <p>Protocol notes (based on your current server implementation): - Success replies contain:
* PHASE, POT, CURRENT_BET, DEALER, ACTIVE_PLAYER, then blocks: - CARD ... END (community cards on
* root level) - PLAYER ... (NAME/CHIPS/BET/STATE + optional CARD blocks for requesting player) ...
* END - Error replies contain: -ERR then CODE=..., MSG=..., END
*
* <p>Important: - The server does NOT wrap cards in a "CARDS" container. - CARD blocks appear
* either: - at root level -> community cards - inside PLAYER -> hole cards for that player (usually
* only for the requesting user)
*/
public class GameClient {
private static final Logger LOG = Logger.getLogger(GameClient.class.getName());
private final ClientService client;
private final int gameId;
public GameClient(ClientService client, int gameId) {
if (client == null) {
throw new IllegalArgumentException("ClientService must not be null");
}
this.client = client;
this.gameId = gameId;
LOG.info(() -> "GameClient initialized for gameId=" + gameId);
}
public GameState fetchGameState() {
final String cmd = "GET_GAME_STATE GAME_ID=" + gameId;
List<String> lines;
try {
lines = client.processCommand(cmd);
} catch (Exception e) {
LOG.log(Level.SEVERE, "processCommand failed for " + cmd + ": " + e.getMessage(), e);
return null;
}
if (lines == null || lines.isEmpty()) {
LOG.warning("Empty server response for " + cmd);
return null;
}
LOG.info(() -> "GET_GAME_STATE raw lines count=" + lines.size() + " lines=" + lines);
String joined = String.join("\n", lines);
if (joined.contains("-ERR") || joined.contains("-ERROR")) {
String code = extractValue(joined, "CODE");
String msg = extractValue(joined, "MSG");
LOG.info(() -> "GET_GAME_STATE returned -ERR code=" + code + " msg=" + msg);
if ("GAME_NOT_STARTED".equalsIgnoreCase(code)) {
return null;
}
return null;
}
try {
GameState state = parseGameState(joined);
LOG.info(
() ->
"Parsed state: phase="
+ state.phase
+ " pot="
+ state.pot
+ " players="
+ (state.players != null ? state.players.size() : 0)
+ " community="
+ (state.communityCards != null
? state.communityCards.size()
: 0));
return state;
} catch (Exception e) {
LOG.log(Level.SEVERE, "Failed parsing game state. Payload=\n" + joined, e);
return null;
}
}
public void sendCall() {
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() {
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) {
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) {
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")) {
String line = raw.trim();
if (line.isEmpty() || line.startsWith("+OK")) {
continue;
}
if (!parseGlobalField(s, line)
&& !parsePlayerStructure(s, state, line)
&& !parseCardData(state, line)
&& !parsePlayerData(state, line)) {
LOG.fine(() -> "Ignored line: '" + line + "'");
}
}
return s;
}
private boolean parseGlobalField(GameState s, String line) {
return switch (line) {
case String l when l.startsWith("PHASE=") -> {
s.phase = value(l);
yield true;
}
case String l when l.startsWith("POT=") -> {
s.pot = intVal(l);
yield true;
}
case String l when l.startsWith("CURRENT_BET=") -> {
s.currentBet = intVal(l);
yield true;
}
case String l when l.startsWith("DEALER=") -> {
s.dealer = intVal(l);
yield true;
}
case String l when l.startsWith("ACTIVE_PLAYER=") -> {
s.activePlayer = intVal(l);
yield true;
}
case String l when l.startsWith("WINNER=") -> {
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;
}
default -> false;
};
}
private boolean parsePlayerStructure(GameState s, ParseState state, String line) {
if (line.equals("PLAYER")) {
state.currentPlayer = new Player();
s.players.add(state.currentPlayer);
state.currentCard = null;
state.insidePlayer = true;
return true;
}
if (line.equals("CARDS")) {
return true;
}
if (line.equals("CARD")) {
state.currentCard = new Card("", "");
if (state.insidePlayer && state.currentPlayer != null) {
state.currentPlayer.addCard(state.currentCard);
} else {
s.communityCards.add(state.currentCard);
}
return true;
}
if (line.equals("END")) {
if (state.currentCard != null) {
state.currentCard = null;
} else if (state.insidePlayer) {
state.insidePlayer = false;
state.currentPlayer = null;
}
return true;
}
return false;
}
private boolean parseCardData(ParseState state, String line) {
if (state.currentCard == null) {
return false;
}
if (line.startsWith("VALUE=")) {
state.currentCard.setValue(value(line));
return true;
}
if (line.startsWith("SUIT=")) {
state.currentCard.setSuit(value(line));
return true;
}
return false;
}
private boolean parsePlayerData(ParseState state, String line) {
if (state.currentPlayer == null) {
return false;
}
if (line.startsWith("USERNAME=") || line.startsWith("NAME=")) {
state.currentPlayer.setId(PlayerId.of(value(line)));
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;
}
if (line.startsWith("BET=")) {
state.currentPlayer.setBet(intVal(line));
return true;
}
if (line.startsWith("STATE=")) {
state.currentPlayer.setState(parseState(value(line)));
return true;
}
return false;
}
private static class ParseState {
Player currentPlayer;
Card currentCard;
boolean insidePlayer;
}
private static String value(String line) {
return line.split("=", 2)[1];
}
private static int intVal(String line) {
return Integer.parseInt(value(line));
}
private static PlayerState parseState(String s) {
if (s == null) {
return PlayerState.ACTIVE;
}
return switch (s.trim().toUpperCase()) {
case "ACTIVE" -> PlayerState.ACTIVE;
case "FOLDED" -> PlayerState.FOLDED;
case "DEALER" -> PlayerState.DEALER;
default -> PlayerState.ACTIVE;
};
}
private static String extractValue(String joined, String key) {
if (joined == null) {
return null;
}
for (String raw : joined.split("\n")) {
String line = raw.trim();
if (line.startsWith(key + "=")) {
return line.substring((key + "=").length()).replace("'", "");
}
}
return null;
}
}
@@ -0,0 +1,295 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import java.util.ArrayList;
import java.util.List;
/**
* The LobbyClient class is responsible for communicating with the server to manage game lobbies. It
* provides methods to create a lobby, join a lobby, and fetch the current status of a lobby by
* sending appropriate commands to the server and processing the responses.
*/
public class LobbyClient {
private final ClientService client;
/**
* Constructs a LobbyClient with the given ClientService for communication.
*
* @param client The ClientService instance used to send commands and receive responses from the
* server.
*/
public LobbyClient(ClientService client) {
this.client = client;
}
public ClientService getClientService() {
return client;
}
/**
* Fetch the current status of the lobby with the given id from the server.
*
* @param lobbyId The id of the lobby to fetch the status for.
* @return A string representing the current status of the lobby, as returned by the server.
*/
public String fetchLobbyStatusString(int lobbyId) {
List<String> lines = client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId);
// Prefer explicit STATUS parameter when available
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
for (RequestParameter p : params) {
if ("STATUS".equalsIgnoreCase(p.key())) {
return p.value();
}
}
// Fallback: some servers may return a plain token as the first line
if (!lines.isEmpty()) {
String first = lines.get(0).trim();
if ("CREATED".equalsIgnoreCase(first) || "RUNNING".equalsIgnoreCase(first)) {
return first.toUpperCase();
}
}
return null;
}
/**
* Request the server to create a new lobby and return the id of the newly created lobby.
*
* @return The id of the newly created lobby, as returned by the server.
*/
public int createLobby() {
List<String> lines = client.processCommand("CREATE_LOBBY");
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
for (RequestParameter p : params) {
if ("LOBBY_ID".equalsIgnoreCase(p.key())) {
return Integer.parseInt(p.value());
}
}
// Fallback for simple legacy/test servers that return the id as a single plain
// line
if (!lines.isEmpty()) {
try {
return Integer.parseInt(lines.get(0));
} catch (NumberFormatException ignored) {
}
}
throw new RuntimeException("No LOBBY_ID in response: " + lines);
}
/** Fetches the global highscores from the server. */
public List<String> getHighscores() {
List<String> lines = client.processCommand("GET_HIGHSCORES");
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
List<String> entries = new ArrayList<>();
for (RequestParameter p : params) {
if ("HIGHSCORE".equalsIgnoreCase(p.key())) {
entries.add(p.value());
}
}
return entries;
}
/** Clears all global highscores on the server. */
public void clearHighscores() {
client.processCommand("CLEAR_HIGHSCORES");
}
/**
* Request the server to return the id of the lobby that the client is currently in.
*
* @return The id of the lobby that the client is currently in, as returned by the server.
*/
public int getLobbyId() {
List<String> lines = client.processCommand("GET_LOBBY_ID");
if (lines.isEmpty()) {
throw new RuntimeException("GET_LOBBY_ID returned empty response");
}
try {
return Integer.parseInt(lines.get(0).trim());
} catch (NumberFormatException e) {
throw new RuntimeException("Invalid GET_LOBBY_ID response: " + lines, e);
}
}
/**
* Request the server to join the lobby with the given id.
*
* @param lobbyId The id of the lobby to join.
*/
public void joinLobby(int lobbyId) {
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.
*
* @param user The username to log in with.
* @return a {@link LoginResult} containing the assigned username and id as returned by the
* server
*/
public LoginResult login(String user) {
String command = buildLoginCommand(user);
List<String> lines = client.processCommand(command);
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
String assigned = user;
String id = null;
for (RequestParameter p : params) {
if ("USERNAME".equalsIgnoreCase(p.key())) {
assigned = p.value();
} else if ("ID".equalsIgnoreCase(p.key())) {
id = p.value();
}
}
return new LoginResult(assigned, id);
}
private String buildLoginCommand(String username) {
if (username == null || username.isBlank()) {
return "LOGIN";
}
return "LOGIN USERNAME=" + username.trim();
}
/**
* Changes the username for the currently logged-in session.
*
* @param newUsername desired new username
* @return a {@link LoginResult} containing assigned username and id as returned by the server
*/
public LoginResult changeUsername(String newUsername) {
List<String> lines = client.processCommand("CHANGE_USERNAME USERNAME=" + newUsername);
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
String assigned = newUsername;
String id = null;
for (RequestParameter p : params) {
if ("USERNAME".equalsIgnoreCase(p.key())) {
assigned = p.value();
} else if ("ID".equalsIgnoreCase(p.key())) {
id = p.value();
}
}
return new LoginResult(assigned, id);
}
/**
* Request the server for the list of available lobbies.
*
* @return list of LobbyInfo objects representing current lobbies
*/
public List<LobbyInfo> getLobbyList() {
List<String> lines = client.processCommand("GET_LOBBY_LIST");
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
List<LobbyInfo> result = new ArrayList<>();
Integer currentId = null;
String currentName = null;
Integer currentPlayerCount = null;
for (RequestParameter p : params) {
String key = p.key().toUpperCase();
String val = p.value();
switch (key) {
case "ID":
if (currentId != null) {
result.add(
new LobbyInfo(
currentId,
currentName,
currentPlayerCount == null ? 0 : currentPlayerCount));
currentName = null;
currentPlayerCount = null;
}
try {
currentId = Integer.parseInt(val);
} catch (NumberFormatException e) {
currentId = null;
}
break;
case "NAME":
currentName = val;
break;
case "PLAYER_COUNT":
try {
currentPlayerCount = Integer.parseInt(val);
} catch (NumberFormatException e) {
currentPlayerCount = 0;
}
break;
default:
break;
}
}
if (currentId != null) {
result.add(
new LobbyInfo(
currentId,
currentName,
currentPlayerCount == null ? 0 : currentPlayerCount));
}
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;
public final String name;
public final int playerCount;
public LobbyInfo(int id, String name, int playerCount) {
this.id = id;
this.name = name;
this.playerCount = playerCount;
}
}
}
@@ -0,0 +1,38 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
/**
* Result of a successful login request.
*
* <p>Holds the username assigned by the server and the server-side id as a string (UUID). The
* client previously attempted to parse the id as an integer which failed when the server returned a
* UUID; this class therefore stores the id as a {@link String}.
*/
public class LoginResult {
private final String username;
private final String id;
/**
* Create a login result.
*
* @param username the assigned username
* @param id the assigned id (UUID string) returned by the server
*/
public LoginResult(String username, String id) {
this.username = username;
this.id = id;
}
/**
* @return the assigned username
*/
public String getUsername() {
return username;
}
/**
* @return the assigned id as returned by the server (UUID string)
*/
public String getId() {
return 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);
}
}
@@ -0,0 +1,297 @@
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;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
public class ChatBoxController {
private String username;
private final ChatController chatController;
@FXML private TabPane chatTabPane;
@FXML private MenuButton addWhisperChatButton;
private final List<String> activeWhisperChats;
@FXML private final Map<String, Tab> usernameTabMap;
/**
* Constructor for the ChatBoxController, initializes the necessary fields and data structures
* for managing chat tabs and whisper chats.
*
* @param username The username of the current user.
* @param chatController The ChatController instance responsible for handling chat logic and
* communication with the server.
*/
public ChatBoxController(String username, ChatController chatController) {
this.username = username;
this.chatController = chatController;
activeWhisperChats = new ArrayList<>();
usernameTabMap = new HashMap<>();
}
public void setUsername(String username) {
if (username != null && !username.isBlank()) {
this.username = username.trim();
}
}
/**
* Initializes the chat interface by creating the global chat model and adding the corresponding
* "GLOBAL" tab to the interface. It also registers the global chat in the {@link
* ChatController}'s model map.
*/
@FXML
public void initialize() {
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();
});
}
/**
* Adds a specific user to the list of available whisper targets. Creates a new menu item for
* the user and defines the action to open a private chat tab when selected.
*
* @param targetUserName The username of the person to be added to the whisper list.
*/
public void addWhisperUser(String targetUserName) {
Platform.runLater(
() -> {
for (MenuItem existing : addWhisperChatButton.getItems()) {
if (targetUserName.equals(existing.getText())) {
return;
}
}
MenuItem menuItem = new MenuItem(targetUserName);
addWhisperChatButton.getItems().add(menuItem);
menuItem.setOnAction(
_ -> {
SoundManager.getInstance().playButtonClick();
addWhisperChat(
targetUserName,
new ChatModel(
ChatType.WHISPER, username, -1, targetUserName));
});
});
}
public void removeWhisperUser(String targetUserName) {
Platform.runLater(
() ->
addWhisperChatButton
.getItems()
.removeIf(item -> targetUserName.equals(item.getText())));
}
public void renameWhisperUser(String oldUsername, String newUsername) {
if (oldUsername == null
|| newUsername == null
|| oldUsername.isBlank()
|| newUsername.isBlank()
|| oldUsername.equals(newUsername)) {
return;
}
Platform.runLater(
() -> {
for (MenuItem item : addWhisperChatButton.getItems()) {
if (oldUsername.equals(item.getText())) {
item.setText(newUsername);
item.setOnAction(
_ -> {
SoundManager.getInstance().playButtonClick();
addWhisperChat(
newUsername,
new ChatModel(
ChatType.WHISPER,
username,
-1,
newUsername));
});
break;
}
}
Tab tab = usernameTabMap.remove(oldUsername);
if (tab != null) {
tab.setText(newUsername);
usernameTabMap.put(newUsername, tab);
}
int idx = activeWhisperChats.indexOf(oldUsername);
if (idx >= 0) {
activeWhisperChats.set(idx, newUsername);
}
});
}
/**
* Creates a new private (whisper) chat model for a specific target user, registers it within
* the chat system, and opens a new chat tab.
*
* @param target The username of the recipient for the private messages.
*/
public void addWhisperChat(String target, ChatModel chatModel) {
if (!activeWhisperChats.contains(target)) {
activeWhisperChats.add(target);
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));
}
}
/**
* Dynamically loads a new chat tab from an FXML resource and attaches it to the TabPane. It
* initializes a {@link ChatViewController} for the tab and sets up a listener to display
* incoming messages in real-time.
*
* @param title The title to be displayed on the tab header.
* @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 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);
return runOnPlatformSynchronized(
() -> {
try {
ChatViewController chatViewController =
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(chatViewController::showMessage);
Tab newChat = new Tab(title, vbox);
usernameTabMap.put(title, newChat);
chatTabPane.getTabs().add(newChat);
this.chatTabPane.getSelectionModel().select(newChat);
chatViewController.setChatTab(newChat);
if (!chatType.equals(ChatType.WHISPER)) {
newChat.setClosable(false);
}
return chatViewController;
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
/**
* Helper Function to cope with a Threads problem occurring when the main JavaFX Thread performs
* the task to add a new whisper Chat tab, and the task is specified to runLater.
*/
public static <T> T runOnPlatformSynchronized(Callable<T> code) {
if (Platform.isFxApplicationThread()) {
try {
return code.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
CompletableFuture<T> futureLock = new CompletableFuture<>();
Platform.runLater(
() -> {
try {
futureLock.complete(code.call());
} catch (Exception e) {
futureLock.completeExceptionally(e);
}
});
try {
return futureLock.get();
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
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);
});
}
}
@@ -1,90 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
/**
* Controller-Klasse für das Chat-System innerhalb der Spieloberfläche.
*
* <p>Verwaltet das Anzeigen von Chatnachrichten, das Eingabefeld für eigene Nachrichten sowie den
* Senden-Button. Unterstützt drei Arten von Nachrichten: - Player-to-Player (Privat) - Lobby-Chat
* (Raum) - Globaler Chat (Serverweit)
*
* <p>Nachrichten werden in einem {@link VBox}-Container als {@link Label} angezeigt. Eigene
* Nachrichten werden über {@link #onSendToNetwork(String)} an das Netzwerkprotokoll weitergeleitet,
* während eingehende Nachrichten über {@link #receiveMessage(String, String)} verarbeitet und
* angezeigt werden.
*
* <p>Hinweis: Einige TODOs stehen in der zugehörigen FXML-Datei
*/
public class ChatController {
@FXML private VBox chatVBox;
@FXML private TextField inputField;
@FXML private Button sendButton;
@FXML private ScrollPane chatScrollPane;
private static final int CHAT_PADDING = 20;
/** Initialisiert den ChatController nach dem Laden der FXML. */
public void initialize() {
inputField.setOnAction(event -> sendMessage());
chatScrollPane.vvalueProperty().bind(chatVBox.heightProperty());
}
/** Standardkonstruktor. Wird von FXML verwendet. */
public ChatController() {
// default constructor for FXML
}
/**
* Diese Methode wird vom Senden-Button oder Enter ausgelöst. Sie gibt die eigene Nachricht an
* das Netzwerkprotokoll weiter.
*/
@FXML
private void sendMessage() {
String message = inputField.getText().trim();
if (!message.isEmpty()) {
inputField.clear();
// Hier wird die eigene Nachricht ans Netzwerkprotokoll übergeben
onSendToNetwork(message);
}
}
/**
* Diese Funktion muss vom Netzwerkprotokoll aufgerufen werden, wenn eine neue Nachricht von
* einem anderen Spieler kommt.
*
* @param player Name des Spielers
* @param message Nachricht des Spielers
*/
public void receiveMessage(String player, String message) {
String time = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm"));
Label label = new Label("[" + time + "] " + player + ": " + message);
label.getStyleClass().add("info-text");
label.setWrapText(true); // Zeilenumbruch aktivieren
label.maxWidthProperty().bind(chatVBox.widthProperty().subtract(CHAT_PADDING));
chatVBox.getChildren().add(label);
}
/**
* Schnittstelle zum Netzwerkprotokoll. Diese Funktion wird automatisch aufgerufen, wenn der
* Benutzer eine eigene Nachricht sendet.
*
* @param message Nachricht, die der Benutzer abgeschickt hat
*/
public void onSendToNetwork(String message) {
// TODO: Netzwerkcode einfügen
receiveMessage("Du", message);
}
}
@@ -0,0 +1,123 @@
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;
import javafx.fxml.FXML;
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.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 ScrollPane scrollPane;
@FXML private VBox chat;
public Tab getChatTab() {
return chatTab;
}
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,
ChatBoxController chatBoxController) {
this.controller = chatController;
this.chatModel = chatModel;
this.chatBoxController = chatBoxController;
tabIsOpen = true;
}
/**
* Initializes the controller after the FXML root element has been processed. Sets up event
* handlers for sending messages via the input field or button and ensures the ScrollPane
* automatically scrolls to the bottom when new messages are added.
*
* @param location The location used to resolve relative paths for the root object.
* @param resourceBundle The resources used to localize the root object.
*/
@Override
public void initialize(URL location, ResourceBundle resourceBundle) {
inputField.setOnAction(_ -> sendMessage());
sendButton.setOnAction(_ -> sendMessage());
scrollPane.vvalueProperty().bind(chat.heightProperty());
}
/**
* Retrieves the text from the input field, creates a new {@link Message} object using the
* current model state, and passes it to the {@link ChatController} for network transmission.
* 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();
Message msg =
new Message(
chatModel.getChattype(),
chatModel.lobbyId,
currentUsername,
chatModel.getTarget(),
message);
controller.onSendToNetwork(msg);
}
}
/**
* Displays a message in the chat interface. This method creates a new Label for the message
* text and adds it to the message container. It uses {@link Platform#runLater(Runnable)} to
* ensure the UI update happens on the JavaFX Application Thread.
*
* @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 =
String.format(
"[%s] %s: %s", msg.timestamp, msg.sender, msg.getMessage());
Label label = new Label(msgText);
label.getStyleClass().add("info-text");
label.setWrapText(true);
label.maxWidthProperty().bind(chat.widthProperty().subtract(CHAT_PADDING));
chat.getChildren().add(label);
});
}
}
@@ -1,9 +1,20 @@
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;
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;
/**
@@ -11,44 +22,179 @@ import javafx.stage.Stage;
*
* <p>Starts the JavaFX application, loads the graphical user interface from the FXML file, and
* initializes the main stage for the game.
*
* <p>Tasks: - Loads the FXML interface "/ui-structure/Casinogameui.fxml". - Loads the application
* icon from "/images/logoinverted.png". - Starts the application in full-screen mode.
*/
public class CasinoGameUI extends Application {
/** default constructor */
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;
private static final int DEFAULT_WIDTH = 1200;
private static final int DEFAULT_HEIGHT = 800;
private static final int GUEST_ID_LENGTH = 8;
/** Default constructor for the CasinoGameUI application. */
public CasinoGameUI() {
// default no-arg constructor
}
private static final int DEFAULT_WIDTH = 1200;
private static final int DEFAULT_HEIGHT = 800;
/**
* Sets the ClientService instance to be used by the application.
*
* @param clientService the ClientService instance to set
*/
public static void setClientService(ClientService clientService) {
CasinoGameUI.clientService = clientService;
}
/**
* Starts the main stage of the application.
* Sets the username to be used by the application.
*
* @param stage The main stage provided by the system.
* @throws IOException If the FXML file or resources cannot be loaded.
* @param username the username to set
*/
public static void setUsername(String username) {
CasinoGameUI.username = username;
}
public static void setChatController(ChatController chatController) {
CasinoGameUI.chatController = chatController;
}
/**
* Sets the lobby ID to be used by the application.
*
* @param lobbyId the lobby ID to set
*/
public static void setLobbyId(int lobbyId) {
CasinoGameUI.lobbyId = lobbyId;
}
/**
* The main entry point for the JavaFX application. This method is called after the application
* is
*
* @param stage the primary stage for this application, onto which the application scene can be
* set. Applications may create other stages, if needed, but they will not be primary
* stages.
* @throws IOException
*/
@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"));
Scene scene = new Scene(fxmlLoader.load(), DEFAULT_WIDTH, DEFAULT_HEIGHT);
stage.setTitle("Casono (GAME)");
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 = ClientApp.getSharedClientService();
}
if (clientService == null) {
throw new IllegalStateException(
"CasinoGameUI: clientService is null. "
+ "Call CasinoGameUI.setClientService(...)"
+ " or start via ClientApp with a shared connection.");
}
}
private String determineEffectiveUsername() {
String effectiveUsername = normalize(username);
if (effectiveUsername == null) {
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='"
+ ClientApp.getSharedUsername()
+ "', hasClientService="
+ (clientService != null));
}
private void configureController(CasinoGameController controller, String effectiveUsername)
throws IOException {
if (lobbyId <= 0) {
throw new IllegalStateException("CasinoGameUI: lobbyId must be set before start()");
}
GameClient gameClient = new GameClient(clientService, lobbyId);
GameService gameService = new GameService(gameClient);
controller.setGameService(gameService);
controller.setMyPlayerId(PlayerId.of(effectiveUsername));
controller.setChatContext(effectiveUsername, clientService, lobbyId);
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();
}
/**
* Starting point of the application.
* Normalizes a string by trimming whitespace and converting blank strings to null.
*
* @param args Command line arguments.
* @param s the string to normalize
* @return the normalized string, or null if the input is null or blank
*/
private static String normalize(String s) {
if (s == null) {
return null;
}
String t = s.trim();
return t.isBlank() ? null : t;
}
/**
* The main method serves as the entry point for the application. It launches the JavaFX
* application.
*
* @param args command line arguments (not used)
*/
public static void main(String[] args) {
launch();
@@ -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)(\\?.*)?$");
}
}

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