Compare commits
246 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f345cc51e | |||
| 585d675a8f | |||
| b25f111f48 | |||
| d82297ac46 | |||
| 8c2130d0d6 | |||
| 52b5d5b8c2 | |||
| 1f8cf35e55 | |||
| 0bf24b9e0f | |||
| 44785ee77f | |||
| 3053d531ee | |||
| acb082ce5d | |||
| 6aedb57473 | |||
| 66f0d734cf | |||
| 6ec743f126 | |||
| 696f9f4fa4 | |||
| 4557dc2f5a | |||
| b208f1fffd | |||
| 6216268658 | |||
| 133a79433d | |||
| 8fe96c5413 | |||
| 885a002f6d | |||
| fb593d48a6 | |||
| 9746f52965 | |||
| 06c059ce5e | |||
| 1952f720de | |||
| d7eb9701c9 | |||
| 0f8f1bb918 | |||
| 9cf6b468ad | |||
| e08b75dc7a | |||
| 761d4b1cae | |||
| e4f0c7efb8 | |||
| f91642bade | |||
| 93b6338041 | |||
| a66c91c67f | |||
| 3e1d787b69 | |||
| bc36892699 | |||
| 120a477a55 | |||
| 5d4fe4bcfb | |||
| 346a728fb9 | |||
| 63fb6e4b47 | |||
| 9044ce3660 | |||
| 4fa60ad9e0 | |||
| 3abc03e6e1 | |||
| e43732c520 | |||
| 1dcf628d2c | |||
| 4e65e2dec0 | |||
| 492901a837 | |||
| 659ea8a744 | |||
| 5e653a516c | |||
| f5ee57454d | |||
| fa8bb25b03 | |||
| e6c222388b | |||
| 7316ec80bf | |||
| b86352a298 | |||
| a7de101be2 | |||
| a2cd59e6b9 | |||
| 564ace38e4 | |||
| d16c348444 | |||
| 8f3d0ab8a9 | |||
| 77ec50e008 | |||
| 869f061d86 | |||
| 9635ee8ef9 | |||
| 65a93a8c82 | |||
| c3e0e740ed | |||
| 0ae88c9149 | |||
| 0aafe13e34 | |||
| 4798de7499 | |||
| 3359c9bc7e | |||
| 346a308176 | |||
| 8998152e8d | |||
| ef7a83c707 | |||
| d9e7c0c56d | |||
| 0227145cab | |||
| 755ea54381 | |||
| c0b149cc5f | |||
| 1669e07d3a | |||
| d0e908774b | |||
| 08e02a6135 | |||
| 6ee9c6c399 | |||
| fb5399fc18 | |||
| 6d8f1787a9 | |||
| 9b4c6d8906 | |||
| ca818d9892 | |||
| dac4dcfdbe | |||
| 0556767d40 | |||
| 5997e736cf | |||
| a4e267cb00 | |||
| 9146d92c04 | |||
| b2adeb7a56 | |||
| d77ce42795 | |||
| f5a62c6e93 | |||
| 371e6afe5c | |||
| 23debb18bc | |||
| 3ff4491fcf | |||
| 31361ffb83 | |||
| 0f93bb8b85 | |||
| 4dd6732945 | |||
| 3354bf7be1 | |||
| 3b8f7d42dd | |||
| a38b2562d1 | |||
| 54a7be074f | |||
| 1b6e8ce6d6 | |||
| 6e4e8f5150 | |||
| c7b7adaa07 | |||
| 84b889a343 | |||
| 25c92da522 | |||
| 7f6a3ada29 | |||
| 098092707c | |||
| f467d9ebe2 | |||
| 13d03637da | |||
| 50052b72ff | |||
| ab4ba0f70b | |||
| 46ccae40ad | |||
| 050beb9d98 | |||
| 517df2369d | |||
| 6219c2d660 | |||
| 57b8bca1a8 | |||
| 050d87ce57 | |||
| a930bd94b5 | |||
| 66ee791342 | |||
| 240bba9bf5 | |||
| 15d1ea4edd | |||
| 3badca0a01 | |||
| 203004fc42 | |||
| de0d0f93f8 | |||
| 7788bbc5d3 | |||
| 2c6ecdc0f9 | |||
| 15ab6faf2e | |||
| 25c20a2ea3 | |||
| 73dd00f37d | |||
| f512e6c422 | |||
| eb3ffee6fc | |||
| af9daecf46 | |||
| 2ab27ac3ed | |||
| 48e6b51ad1 | |||
| e655530760 | |||
| 62f01d1e5d | |||
| ca4283bbe7 | |||
| 3c7773e7fb | |||
| 406567d375 | |||
| ff0f9f70d4 | |||
| 53cafd3dd2 | |||
| 30dcd8f4ab | |||
| f54182fde6 | |||
| 9e7a80c17d | |||
| 9df3042978 | |||
| 69333ede7a | |||
| cdb6d0c6b0 | |||
| a65341b648 | |||
| b4b2e3b067 | |||
| beeba04a2c | |||
| edb98c8d21 | |||
| 590a9aa658 | |||
| 7d74ed6263 | |||
| 2d7b15e593 | |||
| 66d3826509 | |||
| 2e705185f4 | |||
| 9c09927d09 | |||
| 97e5a55042 | |||
| f6dda36c8d | |||
| 4de90e6962 | |||
| 284ff2d043 | |||
| c02680143f | |||
| cb7c284314 | |||
| a2d176f13c | |||
| 644c3e93fc | |||
| d0ab49ec57 | |||
| 8deb647e94 | |||
| 3ea2971ab5 | |||
| d10e9a72fd | |||
| ea41a3c75a | |||
| ce583062ba | |||
| 040ff7cf52 | |||
| 616f609fae | |||
| 4895af9030 | |||
| db3f34fddf | |||
| 999dd9beb9 | |||
| 3be2d3d9f4 | |||
| c094b224c3 | |||
| 3807059229 | |||
| 31d3947175 | |||
| 59cd5bca88 | |||
| 1914aa2c7f | |||
| 13df8db33e | |||
| 566c9375ab | |||
| a43361d606 | |||
| 84e68d8b20 | |||
| e8401a976a | |||
| c0c9225d42 | |||
| 4e7b0ab67a | |||
| 39f60c9113 | |||
| 847d816935 | |||
| c8a9393fb6 | |||
| 79bd33ad75 | |||
| f5138503cb | |||
| 93e39c3962 | |||
| 7d19e65428 | |||
| 87e9305e82 | |||
| de66757837 | |||
| 65304cbb66 | |||
| a5427a232f | |||
| ad81479bcd | |||
| e374be4e31 | |||
| d939b0da67 | |||
| ea987243b0 | |||
| 8a6c573bde | |||
| 139626520b | |||
| b205b25b48 | |||
| df31c307fe | |||
| f3ecbd12f5 | |||
| 5d4d2655ef | |||
| d855f2d568 | |||
| ca782b389e | |||
| 04bf7ba545 | |||
| 509aa71d9f | |||
| d1d2fb30e8 | |||
| aca61cbd52 | |||
| 73756b7d24 | |||
| 60ed1ced91 | |||
| eda51f8ac5 | |||
| ce71992e17 | |||
| a9dcd79b38 | |||
| 79831b046c | |||
| 61e54f282e | |||
| 8fb2657cdd | |||
| 55a6d4beed | |||
| 42b5344059 | |||
| 76e62b3cff | |||
| 1070b5d4b9 | |||
| 4daf6ff4a2 | |||
| 7b1b35698c | |||
| 3153f7027d | |||
| b5d19d915b | |||
| 09c19c2602 | |||
| 77c7f7c0a5 | |||
| 6f80501ea2 | |||
| 45cd54b981 | |||
| 3427c661cf | |||
| ff07843e0a | |||
| cc33411911 | |||
| f721a26e53 | |||
| dd721bda3f | |||
| 68c7003c5d | |||
| b4f34e00f2 | |||
| 26a1ba945a | |||
| 5401ead198 |
@@ -36,7 +36,6 @@ This protocol handles game state synchronization, player actions, lobby and glob
|
|||||||
├── documents/ # Project resources and documentation
|
├── documents/ # Project resources and documentation
|
||||||
│ ├── images/ # Images for README and documentation
|
│ ├── images/ # Images for README and documentation
|
||||||
│ ├── diary # Diary files for each organizational meetup
|
│ ├── diary # Diary files for each organizational meetup
|
||||||
│ ├── blog # Blog files covering project-related topics
|
|
||||||
│ ├── docs # Documentation
|
│ ├── docs # Documentation
|
||||||
│ ├── milestones/ # Milestone deliverables (6 milestones)
|
│ ├── milestones/ # Milestone deliverables (6 milestones)
|
||||||
│ └── (Other resources) # Additional project materials
|
│ └── (Other resources) # Additional project materials
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ plugins {
|
|||||||
id 'org.openjfx.javafxplugin' version '0.1.0'
|
id 'org.openjfx.javafxplugin' version '0.1.0'
|
||||||
id 'checkstyle'
|
id 'checkstyle'
|
||||||
id 'com.diffplug.spotless' version '7.0.2'
|
id 'com.diffplug.spotless' version '7.0.2'
|
||||||
|
id 'jacoco'
|
||||||
}
|
}
|
||||||
|
|
||||||
group = 'ch.unibas.dmi.dbis'
|
group = 'ch.unibas.dmi.dbis'
|
||||||
@@ -32,7 +33,7 @@ dependencies {
|
|||||||
// Source: https://mvnrepository.com/artifact/org.apache.logging.log4j
|
// Source: https://mvnrepository.com/artifact/org.apache.logging.log4j
|
||||||
implementation("org.apache.logging.log4j:log4j-api:2.25.3")
|
implementation("org.apache.logging.log4j:log4j-api:2.25.3")
|
||||||
runtimeOnly("org.apache.logging.log4j:log4j-core: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
|
// Source: https://mvnrepository.com/artifact/org.fusesource.jansi/jansi
|
||||||
runtimeOnly("org.fusesource.jansi:jansi:2.4.2")
|
runtimeOnly("org.fusesource.jansi:jansi:2.4.2")
|
||||||
|
|
||||||
@@ -41,8 +42,17 @@ dependencies {
|
|||||||
implementation 'org.json:json:20240303'
|
implementation 'org.json:json:20240303'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jacoco {
|
||||||
|
toolVersion = "0.8.14"
|
||||||
|
}
|
||||||
|
|
||||||
|
jacocoTestReport {
|
||||||
|
dependsOn test
|
||||||
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
|
finalizedBy jacocoTestReport
|
||||||
}
|
}
|
||||||
|
|
||||||
checkstyle {
|
checkstyle {
|
||||||
|
|||||||
@@ -21,4 +21,7 @@
|
|||||||
|
|
||||||
<!-- Allow for compacter line seperators -->
|
<!-- Allow for compacter line seperators -->
|
||||||
<suppress checks="EmptyLineSeparator" files=".*Test\.java"/>
|
<suppress checks="EmptyLineSeparator" files=".*Test\.java"/>
|
||||||
|
|
||||||
|
<!-- Allow longer method length in composition root -->
|
||||||
|
<suppress checks="MethodLength" files="ServerApp.java"/>
|
||||||
</suppressions>
|
</suppressions>
|
||||||
|
|||||||
@@ -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)*
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Meeting Diary
|
# Meeting Diary
|
||||||
|
|
||||||
This folder holds the notes and documentation of all team meetings throughout the project.
|
This folder holds the notes and documentation of all team meetings throughout the project.
|
||||||
The notes are organised by **milestones (1–6)** 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 (1–6)** 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
|
## Milestone 1
|
||||||
### [24. February 2026](24-02.md)
|
### [24. February 2026](24-02.md)
|
||||||
@@ -14,19 +14,28 @@ The notes are organised by **milestones (1–6)** so that everyone can easily se
|
|||||||
- Finalizing structure of network protocol
|
- Finalizing structure of network protocol
|
||||||
|
|
||||||
|
|
||||||
## Milestone 2
|
## Milestone 2
|
||||||
*(Meeting notes to be added here when available)*
|
### [14. March 2025](14-03.md)
|
||||||
|
- Discussion of LobbyScreen, GameScreen, and Chatbox structure
|
||||||
|
- Endscreen postponed to MS3
|
||||||
|
|
||||||
|
|
||||||
## Milestone 3
|
## Milestone 3
|
||||||
*(Meeting notes to be added here when available)*
|
None, as milestone 3 covered the assessment of individual technical understanding through evaluation.
|
||||||
|
|
||||||
|
|
||||||
## Milestone 4
|
## Milestone 4
|
||||||
*(Meeting notes to be added here when available)*
|
### [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
|
## Milestone 5
|
||||||
*(Meeting notes to be added here when available)*
|
*(Meeting notes to be added here when available)*
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,4 @@
|
|||||||
- [Game Engine Architecture](game-engine-architecture.md)
|
- [Game Engine Architecture](game-engine-architecture.md)
|
||||||
- [Casono Rules Easy Description](casono-rules-easy-description.md)
|
- [Casono Rules Easy Description](casono-rules-easy-description.md)
|
||||||
- [Game engine implementation](game-engine-implementation.md)
|
- [Game engine implementation](game-engine-implementation.md)
|
||||||
|
- [Casonso Manual](casono-manual.md)
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
# Casono Manual
|
||||||
|
|
||||||
|
> Hinweis: Eine vereinfachte Einführung finden Sie in der Datei [Casono Rules – Easy Description](casono-rules-easy-description.md).
|
||||||
|
> Dieses Dokument beschreibt den Ablauf eines Pokerspiels anhand eines Beispiels.
|
||||||
|
<!-- vim-markdown-toc GFM -->
|
||||||
|
|
||||||
|
* [1. Spielübersicht](#1-spielübersicht)
|
||||||
|
* [Grundregeln](#grundregeln)
|
||||||
|
* [2. Sitzposition & Dealer-Button](#2-sitzposition--dealer-button)
|
||||||
|
* [3. Blinds (Pflichteinsätze)](#3-blinds-pflichteinsätze)
|
||||||
|
* [4. Spielablauf im Detail](#4-spielablauf-im-detail)
|
||||||
|
* [4.1 Preflop (erste Setzrunde)](#41-preflop-erste-setzrunde)
|
||||||
|
* [4.2 Flop (3 Gemeinschaftskarten)](#42-flop-3-gemeinschaftskarten)
|
||||||
|
* [4.3 Turn (4. Gemeinschaftskarte)](#43-turn-4-gemeinschaftskarte)
|
||||||
|
* [4.4 River (5. Gemeinschaftskarte)](#44-river-5-gemeinschaftskarte)
|
||||||
|
* [5. Showdown (Kartenvergleich)](#5-showdown-kartenvergleich)
|
||||||
|
* [6. Poker-Handrangfolge](#6-poker-handrangfolge)
|
||||||
|
* [7. Wichtige Grundprinzipien](#7-wichtige-grundprinzipien)
|
||||||
|
* [Reihenfolge beachten](#reihenfolge-beachten)
|
||||||
|
* [Klare Aktionen](#klare-aktionen)
|
||||||
|
* [Ein Spieler – eine Hand](#ein-spieler--eine-hand)
|
||||||
|
* [Fehlerhafte Einsätze](#fehlerhafte-einsätze)
|
||||||
|
* [8. Strategische Einordnung](#8-strategische-einordnung)
|
||||||
|
|
||||||
|
<!-- vim-markdown-toc -->
|
||||||
|
|
||||||
|
## 1. Spielübersicht
|
||||||
|
|
||||||
|
Texas Hold’em ist ein strategisches Kartenspiel für mehrere Spieler.
|
||||||
|
Ziel ist es, den Pot (alle gesetzten Chips) zu gewinnen, entweder durch:
|
||||||
|
|
||||||
|
- die beste Kartenkombination am Ende der Runde
|
||||||
|
- oder indem alle anderen Spieler vorher aussteigen (Fold)
|
||||||
|
|
||||||
|
### Grundregeln
|
||||||
|
|
||||||
|
- Jeder Spieler erhält 2 verdeckte Karten (Hole Cards)
|
||||||
|
- Es werden 5 Gemeinschaftskarten offen in der Mitte ausgelegt
|
||||||
|
- Jeder Spieler bildet die beste 5-Karten-Kombination aus:
|
||||||
|
- eigenen Karten
|
||||||
|
- und Gemeinschaftskarten
|
||||||
|
|
||||||
|
- Zu Spielbeginn erhält jeder Spieler ein Startgeld von 20000 Chips ($)
|
||||||
|
|
||||||
|
## 2. Sitzposition & Dealer-Button
|
||||||
|
|
||||||
|
Der sogenannte Dealer-Button bestimmt die Positionen am Tisch:
|
||||||
|
|
||||||
|
- Er zeigt an, wer als „Geber“ (Dealer) fungiert
|
||||||
|
- Die Positionen rotieren im Uhrzeigersinn nach jeder Runde
|
||||||
|
|
||||||
|
Die Position ist entscheidend, da sie bestimmt:
|
||||||
|
|
||||||
|
- die Reihenfolge der Aktionen
|
||||||
|
- wer die Blinds setzen muss
|
||||||
|
|
||||||
|
## 3. Blinds (Pflichteinsätze)
|
||||||
|
|
||||||
|
Vor jeder Runde werden zwei verpflichtende Einsätze geleistet:
|
||||||
|
|
||||||
|
- **Small Blind** (kleiner Blind): 100 Chips – gesetzt vom Spieler links neben dem Dealer
|
||||||
|
- **Big Blind** (großer Blind): 200 Chips - gesetzt vom Spieler zwei Plätze links vom Dealer
|
||||||
|
|
||||||
|
Diese Einsätze sorgen dafür, dass:
|
||||||
|
|
||||||
|
- ein Startpot entsteht
|
||||||
|
- jede Runde aktiv gespielt wird
|
||||||
|
|
||||||
|
## 4. Spielablauf im Detail
|
||||||
|
|
||||||
|
### 4.1 Preflop (erste Setzrunde)
|
||||||
|
|
||||||
|
Nach dem Austeilen der Karten beginnt die erste Setzrunde.
|
||||||
|
|
||||||
|
Der Spieler links vom Big Blind eröffnet die Runde.
|
||||||
|
|
||||||
|
Jeder Spieler hat folgende Optionen:
|
||||||
|
|
||||||
|
- **Fold** – Karten ablegen und aussteigen
|
||||||
|
- **Call** – Einsatz mitgehen
|
||||||
|
- **Raise** – Einsatz erhöhen
|
||||||
|
|
||||||
|
### 4.2 Flop (3 Gemeinschaftskarten)
|
||||||
|
|
||||||
|
- Drei Karten werden offen auf den Tisch gelegt
|
||||||
|
- Eine neue Setzrunde beginnt
|
||||||
|
- Die Setzrunde beginnt jetzt immer beim ersten aktiven Spieler links vom Dealer (im Uhrzeigersinn).
|
||||||
|
|
||||||
|
Ab diesem Zeitpunkt können alle Spieler ihre Strategie anhand zusätzlicher Informationen anpassen.
|
||||||
|
|
||||||
|
### 4.3 Turn (4. Gemeinschaftskarte)
|
||||||
|
|
||||||
|
- Die vierte Karte wird aufgedeckt
|
||||||
|
- Eine weitere Setzrunde folgt
|
||||||
|
|
||||||
|
Die Einsätze werden oft höher, da sich stärkere Hände entwickeln.
|
||||||
|
|
||||||
|
### 4.4 River (5. Gemeinschaftskarte)
|
||||||
|
|
||||||
|
- Die letzte Karte wird aufgedeckt
|
||||||
|
- Letzte Setzrunde
|
||||||
|
|
||||||
|
Dies ist die finale Entscheidungsphase:
|
||||||
|
|
||||||
|
- Maximierung des Gewinns
|
||||||
|
- oder Minimierung von Verlusten
|
||||||
|
|
||||||
|
## 5. Showdown (Kartenvergleich)
|
||||||
|
|
||||||
|
Wenn nach der letzten Setzrunde mindestens zwei Spieler verbleiben:
|
||||||
|
|
||||||
|
- Alle verbleibenden Spieler decken ihre Karten auf
|
||||||
|
- Die **beste 5-Karten-Kombination gewinnt**
|
||||||
|
|
||||||
|
Wichtig:
|
||||||
|
|
||||||
|
- Die Karten „sprechen für sich“ – die beste Hand zählt unabhängig von Ansagen
|
||||||
|
|
||||||
|
## 6. Poker-Handrangfolge
|
||||||
|
|
||||||
|
Die Stärke der Hände ist eindeutig festgelegt (von schwach nach stark):
|
||||||
|
|
||||||
|
1. High Card (höchste Einzelkarte)
|
||||||
|
2. One Pair (ein Paar)
|
||||||
|
3. Two Pair (zwei Paare)
|
||||||
|
4. Three of a Kind (Drilling)
|
||||||
|
5. Straight (Straße)
|
||||||
|
6. Flush (Farbe)
|
||||||
|
7. Full House
|
||||||
|
8. Four of a Kind (Vierling)
|
||||||
|
9. Straight Flush
|
||||||
|
10. Royal Flush
|
||||||
|
|
||||||
|
Je höher die Kombination, desto stärker die Hand.
|
||||||
|
|
||||||
|
## 7. Wichtige Grundprinzipien
|
||||||
|
|
||||||
|
### Reihenfolge beachten
|
||||||
|
|
||||||
|
Spieler müssen immer der Reihe nach handeln.
|
||||||
|
|
||||||
|
### Klare Aktionen
|
||||||
|
|
||||||
|
Alle Aktionen müssen eindeutig sein:
|
||||||
|
|
||||||
|
- Einsätze klar ansagen oder eindeutig setzen
|
||||||
|
|
||||||
|
### Ein Spieler – eine Hand
|
||||||
|
|
||||||
|
- Spieler dürfen ihre Karten nicht teilen oder gemeinsam spielen
|
||||||
|
|
||||||
|
### Fehlerhafte Einsätze
|
||||||
|
|
||||||
|
- Unklare oder falsche Einsätze können korrigiert werden, abhängig von der Spielsituation (nur wenn der Einsatz außerhalb der gültigen Grenzen liegt; zu hohe oder unzulässige Beträge werden blockiert und nicht automatisch korrigiert)
|
||||||
|
|
||||||
|
## 8. Strategische Einordnung
|
||||||
|
|
||||||
|
Texas Hold’em ist kein reines Glücksspiel. Der Erfolg basiert auf:
|
||||||
|
|
||||||
|
- Wahrscheinlichkeiten (Mathematik)
|
||||||
|
- Einschätzung von Gegnern (Psychologie)
|
||||||
|
- Positionsspiel und Timing
|
||||||
@@ -1,56 +1,42 @@
|
|||||||
# Casono Rules Easy Description
|
# 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 -->
|
<!-- vim-markdown-toc GFM -->
|
||||||
|
|
||||||
* [Deustch](#deustch)
|
* [Blinds (Small Blind & Big Blind)](#blinds-small-blind--big-blind)
|
||||||
* [Blinds (Small Blind & Big Blind)](#blinds-small-blind--big-blind)
|
* [Erste Setzrunde (Preflop)](#erste-setzrunde-preflop)
|
||||||
* [Erste Setzrunde (Preflop)](#erste-setzrunde-preflop)
|
* [Beispiel Preflop](#beispiel-preflop)
|
||||||
* [Beispiel Preflop](#beispiel-preflop)
|
* [Flop (3 Gemeinschaftskarten)](#flop-3-gemeinschaftskarten)
|
||||||
* [Flop (3 Gemeinschaftskarten)](#flop-3-gemeinschaftskarten)
|
* [Beispiel Flop](#beispiel-flop)
|
||||||
* [Beispiel Flop](#beispiel-flop)
|
* [Turn (4. Karte)](#turn-4-karte)
|
||||||
* [Turn (4. Karte)](#turn-4-karte)
|
* [River (5. Karte)](#river-5-karte)
|
||||||
* [River (5. Karte)](#river-5-karte)
|
* [Showdown (Gewinnentscheidung)](#showdown-gewinnentscheidung)
|
||||||
* [Showdown (Gewinnentscheidung)](#showdown-gewinnentscheidung)
|
* [Poker Hand Rankings (Gewichtung)](#poker-hand-rankings-gewichtung)
|
||||||
* [Poker Hand Rankings (Gewichtung)](#poker-hand-rankings-gewichtung)
|
* [Fazit](#fazit)
|
||||||
* [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)
|
|
||||||
|
|
||||||
<!-- vim-markdown-toc -->
|
<!-- 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.
|
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:
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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.
|
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 🟢*
|
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:
|
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.
|
Der Small Blind und der Big Blind. Der Big Blind ist immer doppelt so hoch wie der Small Blind.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Regel: *32 Dead Button 🟡*
|
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.
|
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.
|
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: *42 Methods of Raising 🟢*
|
||||||
Regel: *50 Acting in Turn 🟢*
|
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**.
|
||||||
|
|
||||||

|

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

|

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

|

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

|

|
||||||
|
|
||||||
Damit verändert sich sofort die Situation: Julian und Jona müssen entscheiden, ob sie diesen Raise bezahlen, selbst erhöhen oder aussteigen.
|
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.
|
Jetzt werden **3 Gemeinschaftskarten** in die Mitte gelegt. Ab hier verändert sich das Spiel komplett, weil alle Spieler zusätzliche Informationen bekommen.
|
||||||
|
|
||||||

|
Die Setzrunde beginnt jetzt immer beim ersten aktiven Spieler links vom Dealer (im Uhrzeigersinn).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Es beginnt eine neue Setzrunde.
|
Es beginnt eine neue Setzrunde.
|
||||||
|
|
||||||
Regel: *49 Accepted Action 🟢*
|
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.
|
||||||
|
|
||||||

|
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.
|

|
||||||
|
|
||||||
### Turn (4. Karte)
|
## Turn (4. Karte)
|
||||||
|
|
||||||
Jetzt kommt die **4. Gemeinschaftskarte**.
|
Jetzt kommt die **4. Gemeinschaftskarte**.
|
||||||
|
|
||||||
Wieder beginnt eine neue Setzrunde.
|
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.
|
||||||
|
|
||||||

|

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

|

|
||||||
|
|
||||||
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 🟡*
|
Regel: *54 Pot Size Bets 🟡*
|
||||||
|
|
||||||
### Showdown (Gewinnentscheidung)
|
## Showdown (Gewinnentscheidung)
|
||||||
|
|
||||||
Wenn nach der letzten Setzrunde noch zwei Spieler übrig sind, kommt es zum Showdown.
|
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**.
|
Beide Spieler zeigen ihre Karten offen. Gewonnen hat die **beste 5-Karten-Kombination aus Handkarten und Gemeinschaftskarten**.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Regel: *12 Cards Speak at Showdown 🟢*
|
Regel: *12 Cards Speak at Showdown 🟢*
|
||||||
Regel: *16 Face Up for All-Ins 🟢*
|
Regel: *16 Face Up for All-Ins 🟢*
|
||||||
Regel: *17 Non All-In Showdowns 🟢*
|
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:
|
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.
|
Je höher die Kombination, desto stärker die Hand und desto wahrscheinlicher der Gewinn.
|
||||||
|
|
||||||
Julian: 2. Paar:
|
Jona: 2. Paar:
|
||||||
|
|
||||||

|

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

|

|
||||||
|
|
||||||
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.
|
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: *67 One Player One Hand 🟢*
|
||||||
Regel: *52 Incorrect Bets 🟡*
|
Regel: *52 Incorrect Bets 🟡*
|
||||||
Regel: *57 Non-Standard Betting 🟡*
|
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, it’s necessary—like with any game—to first understand the basic rules and game flow.
|
|
||||||
|
|
||||||
So let’s 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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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**.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Mathis sees his cards and realizes they aren’t strong, so he Folds and exits the hand.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Jona is next and decides to Call as well, because his hand is playable.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Lars looks at his cards, recognizes a strong hand, and Raises to **1200 chips**.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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**.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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**.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
The higher the combination, the stronger the hand, and the greater the chance of winning.
|
|
||||||
|
|
||||||
Julian: Two Pair:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Lars: One Pair:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 958 KiB |
|
After Width: | Height: | Size: 12 MiB |
|
After Width: | Height: | Size: 14 MiB |
|
After Width: | Height: | Size: 14 MiB |
|
After Width: | Height: | Size: 13 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 12 MiB |
|
After Width: | Height: | Size: 12 MiB |
@@ -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.
|
||||||
@@ -50,6 +50,43 @@ This document describes the protocol for client-server communication in our appl
|
|||||||
- [Error Response](#error-response-6)
|
- [Error Response](#error-response-6)
|
||||||
- [Example Request](#example-request-4)
|
- [Example Request](#example-request-4)
|
||||||
- [Example Response](#example-response-4)
|
- [Example Response](#example-response-4)
|
||||||
|
- [GET_LOBBY_LIST command](#get_lobby_list-command)
|
||||||
|
- [Required pre-execution checks](#required-pre-execution-checks)
|
||||||
|
- [Request Parameters](#request-parameters)
|
||||||
|
- [Success Response](#success-response)
|
||||||
|
- [Example Request](#example-request)
|
||||||
|
- [Example Response](#example-response)
|
||||||
|
- [GET_GAME_STATE command](#get_game_state-command)
|
||||||
|
- [Required pre-execution checks](#required-pre-execution-checks)
|
||||||
|
- [Request Parameters](#request-parameters)
|
||||||
|
- [Success Response](#success-response)
|
||||||
|
- [Example Request](#example-request)
|
||||||
|
- [Example Response](#example-response)
|
||||||
|
- [RAISE command](#raise-command)
|
||||||
|
- [CALL command](#call-command)
|
||||||
|
- [FOLD command](#fold-command)
|
||||||
|
- [BET command](#bet-command)
|
||||||
|
- [SEND_MESSAGE command](#send_message-command)
|
||||||
|
- [Required pre-execution checks](#required-pre-execution-checks)
|
||||||
|
- [Request Parameters](#request-parameters)
|
||||||
|
- [Success Response](#success-response)
|
||||||
|
- [GET_MESSAGE_COUNT command](#get_message_count-command)
|
||||||
|
- [Required pre-execution checks](#required-pre-execution-checks)
|
||||||
|
- [Request Parameters](#request-parameters)
|
||||||
|
- [Success Response](#success-response)
|
||||||
|
- [GET_NEXT_MESSAGE command](#get_next_message-command)
|
||||||
|
- [Required pre-execution checks](#required-pre-execution-checks)
|
||||||
|
- [Request Parameters](#request-parameters)
|
||||||
|
- [Success Response](#success-response)
|
||||||
|
- [JOIN_LOBBY command](#join_lobby-command)
|
||||||
|
- [Required pre-execution checks](#required-pre-execution-checks)
|
||||||
|
- [Request Parameters](#request-parameters)
|
||||||
|
- [Success Response](#success-response)
|
||||||
|
- [START_GAME command](#start_game-command)
|
||||||
|
- [GET_LOBBY_STATUS command](#get_lobby_status-command)
|
||||||
|
- [Required pre-execution checks](#required-pre-execution-checks)
|
||||||
|
- [Request Parameters](#request-parameters)
|
||||||
|
- [Success Response](#success-response)
|
||||||
|
|
||||||
<!-- Please see the comments for copy ‚ n' paste ready examples -->
|
<!-- Please see the comments for copy ‚ n' paste ready examples -->
|
||||||
|
|
||||||
@@ -342,3 +379,847 @@ LIST_USERS
|
|||||||
END
|
END
|
||||||
END
|
END
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## SEND_MESSAGE command
|
||||||
|
The `SEND_MESSAGE` command is used to transfer the chat message sent by a user to the server.
|
||||||
|
### Required pre-execution checks
|
||||||
|
None.
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|:---------|:----------------|:-------------------------------------------------------------------|
|
||||||
|
| `TYPE` | `Enum<ChatType` | Member of Enum, indicating with chat type is used |
|
||||||
|
| `GAME` | `int` | ID for identifying the Lobby Chat |
|
||||||
|
| `USER` | `String` | Username of the player that sent the message |
|
||||||
|
| `TARGET` | `String` | Username of to which the message is sent to (only in Whisper Chat) |
|
||||||
|
| `TIME` | `String` | Timestamp, when the message was sent |
|
||||||
|
| `TEXT` | `String` | Content of the message |
|
||||||
|
|
||||||
|
| Members of `ChatType` | Description |
|
||||||
|
|:----------------------|:--------------------------------|
|
||||||
|
| `GLOBAL` | Message is for the global chat |
|
||||||
|
| `LOBBY` | Message is for the lobby chat |
|
||||||
|
| `WHISPER` | Message is for the whisper chat |
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
No response fields.
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
None.
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
```
|
||||||
|
SEND_MESSAGE TYPE=GLOBAL GAME=1 USER=player1 TARGET=null TIME='10:30' TEXT='Hello World'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
## GET_MESSAGE_COUNT command
|
||||||
|
The `GET_MESSAGE_COUNT` is used to get the current number of messages that are stored in the queue for a client.
|
||||||
|
### Required pre-execution checks
|
||||||
|
None.
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
No parameters.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|:--------|:------|:-------------------------------|
|
||||||
|
| `COUNT` | `int` | The current number of messages |
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
| Code | Description |
|
||||||
|
| :------------------- |:---------------------------------------------------------------------------------|
|
||||||
|
| `NO_USER_ASSOCIATED` | The session has no user associated, there is no queue of messages for the client |
|
||||||
|
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
```
|
||||||
|
GET_MESSAGE_COUNT
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
COUNT=10
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
## GET_NEXT_MESSAGE command
|
||||||
|
The `GET_NEXT_MESSAGE` command is used to get the next message stored in a queue for the client.
|
||||||
|
### Required pre-execution checks
|
||||||
|
None.
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
No parameters.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
| Field | Type | Description |
|
||||||
|
|:---------|:----------------|:-------------------------------------------------------------------|
|
||||||
|
| `TYPE` | `Enum<ChatType` | Member of Enum, indicating with chat type is used |
|
||||||
|
| `GAME` | `int` | ID for identifying the Lobby Chat |
|
||||||
|
| `USER` | `String` | Username of the player that sent the message |
|
||||||
|
| `TARGET` | `String` | Username of to which the message is sent to (only in Whisper Chat) |
|
||||||
|
| `TIME` | `String` | Timestamp, when the message was sent |
|
||||||
|
| `TEXT` | `String` | Content of the message |
|
||||||
|
|
||||||
|
| Members of `ChatType` | Description |
|
||||||
|
|:----------------------|:--------------------------------|
|
||||||
|
| `GLOBAL` | Message is for the global chat |
|
||||||
|
| `LOBBY` | Message is for the lobby chat |
|
||||||
|
| `WHISPER` | Message is for the whisper chat |
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
| Code | Description |
|
||||||
|
| :------------------- |:---------------------------------------------------------------------------------|
|
||||||
|
| `NO_USER_ASSOCIATED` | The session has no user associated, there is no queue of messages for the client |
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
```
|
||||||
|
GET_NEXT_MESSAGE
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
TYPE=GLOBAL GAME=-1 USER=player1 TARGET=null TIME=9:30 TEXT="Guten Tag"
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (success)
|
||||||
|
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
## JOIN_LOBBY command
|
||||||
|
|
||||||
|
The `JOIN_LOBBY` command requests the server to add the currently logged-in user to the lobby with the given identifier. The server resolves the username from the session; clients must only provide the `ID` parameter.
|
||||||
|
|
||||||
|
### Required pre-execution checks
|
||||||
|
|
||||||
|
- [`UserLoggedInCheck`](#userloggedincheck)
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
|
||||||
|
| Parameter Name | Type | Optional | Description |
|
||||||
|
| :------------- | :--- | :------: | :---------- |
|
||||||
|
| `ID` | `int` | no | Numeric id of the target lobby |
|
||||||
|
|
||||||
|
### Implementation notes
|
||||||
|
|
||||||
|
- Parser: `JoinLobbyParser` — reads the `ID` parameter and builds `JoinLobbyRequest`.
|
||||||
|
- Handler: `JoinLobbyHandler` — resolves the username from `UserRegistry` using the session id, attempts to add the user to the lobby via `LobbyManager.addPlayerToLobby(...)` and returns appropriate responses.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
|
||||||
|
No additional response fields. The server replies with a simple `+OK` on success.
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
| :--- | :---------- |
|
||||||
|
| `USER_NOT_LOGGED_IN` | No user associated with the session (pre-execution check failed) |
|
||||||
|
| `LOBBY_NOT_FOUND` | The specified lobby id does not exist |
|
||||||
|
| `LOBBY_FULL_OR_ALREADY_IN` | Lobby is full or the user is already in the lobby |
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```
|
||||||
|
JOIN_LOBBY ID=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (success)
|
||||||
|
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (error)
|
||||||
|
|
||||||
|
```
|
||||||
|
-ERR
|
||||||
|
CODE=LOBBY_NOT_FOUND
|
||||||
|
MSG=Lobby not found
|
||||||
|
END
|
||||||
|
|
||||||
|
## START_GAME command
|
||||||
|
|
||||||
|
The `START_GAME` command requests the server to start a new game in the specified lobby. The
|
||||||
|
server requires an explicit numeric `ID` parameter identifying the target lobby.
|
||||||
|
|
||||||
|
### Required pre-execution checks
|
||||||
|
- [`UserLoggedInCheck`](#userloggedincheck)
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
| Parameter Name | Type | Optional | Description |
|
||||||
|
| :------------- | :--- | :------: | :---------- |
|
||||||
|
| `ID` | `int` | no | Numeric id of the target lobby |
|
||||||
|
|
||||||
|
### Implementation notes
|
||||||
|
|
||||||
|
- Parser: `StartGameParser` — reads the required `ID` parameter and builds `StartGameRequest`.
|
||||||
|
- Handler: `StartGameHandler` — resolves the lobby by id, checks the requester is a member of the
|
||||||
|
lobby, verifies a game is not already running and that enough players are present, initializes
|
||||||
|
`GameController` and attaches it to the lobby. On success a structured `START_GAME` success
|
||||||
|
response containing `GAME` and `MESSAGE` fields is returned.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
| Field | Type | Description |
|
||||||
|
| :------- | :------ | :-------------------------------- |
|
||||||
|
| `GAME` | `int` | Numeric id of the started game (uses lobby id) |
|
||||||
|
| `MESSAGE`| `String`| Human-readable status message |
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
| Code | Description |
|
||||||
|
| :--------------- | :---------------------------------------- |
|
||||||
|
| `MISSING_PARAMETER` | Required parameter `ID` missing (parser) |
|
||||||
|
| `LOBBY_NOT_FOUND` | The specified lobby id does not exist |
|
||||||
|
| `NOT_IN_LOBBY` | Requesting user is not a member of the lobby |
|
||||||
|
| `NOT_ENOUGH_PLAYERS` | Not enough players to start the game |
|
||||||
|
| `ALREADY_STARTED` | A game is already running in the lobby |
|
||||||
|
| `USER_NOT_LOGGED_IN` | No user associated with the session |
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
START_GAME ID=1
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (success)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
+OK
|
||||||
|
GAME=1
|
||||||
|
MESSAGE=Game started successfully
|
||||||
|
END
|
||||||
|
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
## GET_LOBBY_LIST command
|
||||||
|
|
||||||
|
The `GET_LOBBY_LIST` command requests the server to return a list of currently available lobbies with basic metadata.
|
||||||
|
|
||||||
|
### Required pre-execution checks
|
||||||
|
None.
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
No parameters.
|
||||||
|
|
||||||
|
### Implementation notes
|
||||||
|
|
||||||
|
- Parser: `GetLobbyListParser` — creates `GetLobbyListRequest` (no parameters).
|
||||||
|
- Handler: `GetLobbyListHandler` — queries `LobbyManager#getAllLobbies()` and returns `GetLobbyListResponse`.
|
||||||
|
- Response: `GetLobbyListResponse` — builds a `LOBBIES` collection with repeated `LOBBY` blocks containing `ID`, `NAME`, `PLAYER_COUNT`.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
|
||||||
|
The response contains a `LOBBIES` collection with nested `LOBBY` entries.
|
||||||
|
|
||||||
|
Example response structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
LOBBIES
|
||||||
|
LOBBY
|
||||||
|
ID=1
|
||||||
|
NAME=Room 1
|
||||||
|
PLAYER_COUNT=2
|
||||||
|
END
|
||||||
|
LOBBY
|
||||||
|
ID=2
|
||||||
|
NAME=Room 2
|
||||||
|
PLAYER_COUNT=3
|
||||||
|
END
|
||||||
|
END
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```
|
||||||
|
GET_LOBBY_LIST
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (error)
|
||||||
|
|
||||||
|
```
|
||||||
|
-ERR
|
||||||
|
CODE=NO_LOBBIES_AVAILABLE
|
||||||
|
MESSAGE=No lobbies available
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
## GET_GAME_STATE command
|
||||||
|
|
||||||
|
The `GET_GAME_STATE` command returns a snapshot of the current game for a lobby. It includes global fields (`PHASE`, `POT`, `CURRENT_BET`, `DEALER`, `ACTIVE_PLAYER`), repeated `CARD` blocks for community cards and repeated `PLAYER` blocks for per-player information. If the requester is a player in the game, their hole cards are included inside their `PLAYER` block as nested `CARD` blocks.
|
||||||
|
|
||||||
|
### Required pre-execution checks
|
||||||
|
- [`UserLoggedInCheck`](#userloggedincheck)
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
| Parameter Name | Type | Optional | Description |
|
||||||
|
| :------------- | :----- | :------: | :---------- |
|
||||||
|
| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to query. If omitted the server will resolve the lobby by the requesting session's associated user.
|
||||||
|
| `USERNAME` | `String` | yes | Optional username to query the game state for (server will also accept session resolution).
|
||||||
|
|
||||||
|
### Implementation notes
|
||||||
|
|
||||||
|
- Parser: `GetGameStateParser` — accepts optional `GAME_ID` and `USERNAME` and builds `GetGameStateRequest`.
|
||||||
|
- Handler: `GetGameStateHandler` — resolves the target lobby by `GAME_ID` when provided, otherwise by `USERNAME` or session user; if a game is running a `GetGameStateResponse` is returned.
|
||||||
|
- Response: `GetGameStateResponse` — uses repeated `CARD` and `PLAYER` blocks to match the client parser expectations.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
|
||||||
|
Top-level fields and collections (example):
|
||||||
|
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
PHASE=PRE_FLOP
|
||||||
|
POT=150
|
||||||
|
CURRENT_BET=50
|
||||||
|
DEALER=2
|
||||||
|
ACTIVE_PLAYER=1
|
||||||
|
CARD
|
||||||
|
VALUE=K
|
||||||
|
SUIT=H
|
||||||
|
END
|
||||||
|
PLAYER
|
||||||
|
NAME=player1
|
||||||
|
CHIPS=1000
|
||||||
|
BET=50
|
||||||
|
STATE=ACTIVE
|
||||||
|
CARD
|
||||||
|
VALUE=A
|
||||||
|
SUIT=S
|
||||||
|
END
|
||||||
|
END
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```
|
||||||
|
GET_GAME_STATE GAME_ID=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (error)
|
||||||
|
|
||||||
|
```
|
||||||
|
-ERR
|
||||||
|
CODE=GAME_NOT_STARTED
|
||||||
|
MESSAGE=Game not started
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
## BET command
|
||||||
|
|
||||||
|
The `BET` command lets the currently logged-in player place a bet in the ongoing game for their lobby.
|
||||||
|
|
||||||
|
### Required pre-execution checks
|
||||||
|
|
||||||
|
- [`UserLoggedInCheck`](#userloggedincheck)
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
|
||||||
|
| Parameter Name | Type | Optional | Description |
|
||||||
|
| :------------- | :--- | :------: | :---------- |
|
||||||
|
| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to target. If omitted the server resolves the lobby by the requesting session's user. |
|
||||||
|
| `AMOUNT` | `int` | no | Amount the player wants to bet |
|
||||||
|
|
||||||
|
### Implementation notes
|
||||||
|
|
||||||
|
- Parser: `PlayerBetParser` — reads `AMOUNT` and optionally `GAME_ID`.
|
||||||
|
- Handler: `PlayerBetHandler` — validates the session, lobby (by id when provided or else by session), player's turn and balance and forwards to `GameController`.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
|
||||||
|
No additional response fields. Server replies with `+OK` on success.
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
| :--- | :---------- |
|
||||||
|
| `NOT_YOUR_TURN` | The player attempted to bet when not their turn |
|
||||||
|
| `INSUFFICIENT_FUNDS` | Player does not have enough chips |
|
||||||
|
| `INVALID_AMOUNT` | Amount parameter is invalid |
|
||||||
|
| `GAME_NOT_STARTED` | No game is running in the lobby |
|
||||||
|
| `NOT_IN_LOBBY` | Requesting user is not a member of the lobby |
|
||||||
|
| `LOBBY_NOT_FOUND` | The specified `GAME_ID` does not exist |
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```
|
||||||
|
BET GAME_ID=1 AMOUNT=50
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (success)
|
||||||
|
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (error)
|
||||||
|
|
||||||
|
```
|
||||||
|
-ERR
|
||||||
|
CODE=INSUFFICIENT_FUNDS
|
||||||
|
MSG=Not enough chips
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
## RAISE command
|
||||||
|
|
||||||
|
The `RAISE` command lets the currently logged-in player increase the current bet in the ongoing game for their lobby.
|
||||||
|
|
||||||
|
### Required pre-execution checks
|
||||||
|
|
||||||
|
- [`UserLoggedInCheck`](#userloggedincheck)
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
|
||||||
|
| Parameter Name | Type | Optional | Description |
|
||||||
|
| :------------- | :--- | :------: | :---------- |
|
||||||
|
| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to target. If omitted the server resolves the lobby by the requesting session's user. |
|
||||||
|
| `AMOUNT` | `int` | no | Amount the player wants to raise |
|
||||||
|
|
||||||
|
### Implementation notes
|
||||||
|
|
||||||
|
- Parser: `PlayerRaiseParser` — reads `AMOUNT` and optionally `GAME_ID`.
|
||||||
|
- Handler: `PlayerRaiseHandler` — validates the session, lobby (by id when provided or else by session), player's turn and balance and forwards to `GameController`.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
|
||||||
|
No additional response fields. Server replies with `+OK` on success.
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
| :--- | :---------- |
|
||||||
|
| `NOT_YOUR_TURN` | The player attempted to raise when not their turn |
|
||||||
|
| `INSUFFICIENT_FUNDS` | Player does not have enough chips |
|
||||||
|
| `INVALID_AMOUNT` | Amount parameter is invalid |
|
||||||
|
| `GAME_NOT_STARTED` | No game is running in the lobby |
|
||||||
|
| `NOT_IN_LOBBY` | Requesting user is not a member of the lobby |
|
||||||
|
| `LOBBY_NOT_FOUND` | The specified `GAME_ID` does not exist |
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```
|
||||||
|
RAISE GAME_ID=1 AMOUNT=100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (success)
|
||||||
|
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (error)
|
||||||
|
|
||||||
|
```
|
||||||
|
-ERR
|
||||||
|
CODE=INSUFFICIENT_FUNDS
|
||||||
|
MSG=Not enough chips
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
## CALL command
|
||||||
|
|
||||||
|
The `CALL` command lets the currently logged-in player match the current bet (call) in the ongoing game for their lobby.
|
||||||
|
|
||||||
|
### Required pre-execution checks
|
||||||
|
|
||||||
|
- [`UserLoggedInCheck`](#userloggedincheck)
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
|
||||||
|
| Parameter Name | Type | Optional | Description |
|
||||||
|
| :------------- | :--- | :------: | :---------- |
|
||||||
|
| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to target. If omitted the server resolves the lobby by the requesting session's user. |
|
||||||
|
|
||||||
|
### Implementation notes
|
||||||
|
|
||||||
|
- Parser: `PlayerCallParser` — accepts optional `GAME_ID`.
|
||||||
|
- Handler: `PlayerCallHandler` — validates the session, resolves the lobby by id when provided or by session otherwise and forwards to `GameController`.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
|
||||||
|
No additional response fields. Server replies with `+OK` on success.
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
| :--- | :---------- |
|
||||||
|
| `NOT_YOUR_TURN` | The player attempted to call when not their turn |
|
||||||
|
| `INSUFFICIENT_FUNDS` | Player does not have enough chips |
|
||||||
|
| `GAME_NOT_STARTED` | No game is running in the lobby |
|
||||||
|
| `NOT_IN_LOBBY` | Requesting user is not a member of the lobby |
|
||||||
|
| `LOBBY_NOT_FOUND` | The specified `GAME_ID` does not exist |
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```
|
||||||
|
CALL GAME_ID=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (success)
|
||||||
|
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (error)
|
||||||
|
|
||||||
|
```
|
||||||
|
-ERR
|
||||||
|
CODE=NOT_YOUR_TURN
|
||||||
|
MSG=It is not your turn
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
## FOLD command
|
||||||
|
|
||||||
|
The `FOLD` command lets the currently logged-in player leave the current hand (fold) in the ongoing game for their lobby.
|
||||||
|
|
||||||
|
### Required pre-execution checks
|
||||||
|
|
||||||
|
- [`UserLoggedInCheck`](#userloggedincheck)
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
|
||||||
|
| Parameter Name | Type | Optional | Description |
|
||||||
|
| :------------- | :--- | :------: | :---------- |
|
||||||
|
| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to target. If omitted the server resolves the lobby by the requesting session's user. |
|
||||||
|
|
||||||
|
### Implementation notes
|
||||||
|
|
||||||
|
- Parser: `PlayerFoldParser` — accepts optional `GAME_ID`.
|
||||||
|
- Handler: `PlayerFoldHandler` — validates the session, resolves the lobby by id when provided or by session otherwise and forwards to `GameController`.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
|
||||||
|
No additional response fields. Server replies with `+OK` on success.
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
| :--- | :---------- |
|
||||||
|
| `NOT_YOUR_TURN` | The player attempted to fold when not their turn |
|
||||||
|
| `GAME_NOT_STARTED` | No game is running in the lobby |
|
||||||
|
| `NOT_IN_LOBBY` | Requesting user is not a member of the lobby |
|
||||||
|
| `LOBBY_NOT_FOUND` | The specified `GAME_ID` does not exist |
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```
|
||||||
|
FOLD GAME_ID=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (success)
|
||||||
|
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (error)
|
||||||
|
|
||||||
|
```
|
||||||
|
-ERR
|
||||||
|
CODE=NOT_YOUR_TURN
|
||||||
|
MSG=It is not your turn
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
## FOLD command
|
||||||
|
|
||||||
|
The `FOLD` command lets the currently logged-in player leave the current hand (fold) in the ongoing game for their lobby.
|
||||||
|
|
||||||
|
### Required pre-execution checks
|
||||||
|
|
||||||
|
- [`UserLoggedInCheck`](#userloggedincheck)
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
|
||||||
|
| Parameter Name | Type | Optional | Description |
|
||||||
|
| :------------- | :--- | :------: | :---------- |
|
||||||
|
| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to target. If omitted the server resolves the lobby by the requesting session's user. |
|
||||||
|
|
||||||
|
### Implementation notes
|
||||||
|
|
||||||
|
- Parser: `PlayerFoldParser` — accepts optional `GAME_ID`.
|
||||||
|
- Handler: `PlayerFoldHandler` — validates the session, resolves the lobby by id when provided or by session otherwise and forwards to `GameController`.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
|
||||||
|
No additional response fields. Server replies with `+OK` on success.
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
| :--- | :---------- |
|
||||||
|
| `NOT_YOUR_TURN` | The player attempted to fold when not their turn |
|
||||||
|
| `GAME_NOT_STARTED` | No game is running in the lobby |
|
||||||
|
| `NOT_IN_LOBBY` | Requesting user is not a member of the lobby |
|
||||||
|
| `LOBBY_NOT_FOUND` | The specified `GAME_ID` does not exist |
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```
|
||||||
|
FOLD GAME_ID=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (success)
|
||||||
|
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (error)
|
||||||
|
|
||||||
|
```
|
||||||
|
-ERR
|
||||||
|
CODE=NOT_YOUR_TURN
|
||||||
|
MSG=It is not your turn
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
## FOLD command
|
||||||
|
|
||||||
|
The `FOLD` command lets the currently logged-in player leave the current hand (fold) in the ongoing game for their lobby.
|
||||||
|
|
||||||
|
### Required pre-execution checks
|
||||||
|
|
||||||
|
- [`UserLoggedInCheck`](#userloggedincheck)
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
|
||||||
|
| Parameter Name | Type | Optional | Description |
|
||||||
|
| :------------- | :--- | :------: | :---------- |
|
||||||
|
| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to target. If omitted the server resolves the lobby by the requesting session's user. |
|
||||||
|
|
||||||
|
### Implementation notes
|
||||||
|
|
||||||
|
- Parser: `PlayerFoldParser` — accepts optional `GAME_ID`.
|
||||||
|
- Handler: `PlayerFoldHandler` — validates the session, resolves the lobby by id when provided or by session otherwise and forwards to `GameController`.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
|
||||||
|
No additional response fields. Server replies with `+OK` on success.
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
| :--- | :---------- |
|
||||||
|
| `NOT_YOUR_TURN` | The player attempted to fold when not their turn |
|
||||||
|
| `GAME_NOT_STARTED` | No game is running in the lobby |
|
||||||
|
| `NOT_IN_LOBBY` | Requesting user is not a member of the lobby |
|
||||||
|
| `LOBBY_NOT_FOUND` | The specified `GAME_ID` does not exist |
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```
|
||||||
|
FOLD GAME_ID=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (success)
|
||||||
|
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (error)
|
||||||
|
|
||||||
|
```
|
||||||
|
-ERR
|
||||||
|
CODE=NOT_YOUR_TURN
|
||||||
|
MSG=It is not your turn
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
## FOLD command
|
||||||
|
|
||||||
|
The `FOLD` command lets the currently logged-in player leave the current hand (fold) in the ongoing game for their lobby.
|
||||||
|
|
||||||
|
### Required pre-execution checks
|
||||||
|
|
||||||
|
- [`UserLoggedInCheck`](#userloggedincheck)
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
|
||||||
|
| Parameter Name | Type | Optional | Description |
|
||||||
|
| :------------- | :--- | :------: | :---------- |
|
||||||
|
| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to target. If omitted the server resolves the lobby by the requesting session's user. |
|
||||||
|
|
||||||
|
### Implementation notes
|
||||||
|
|
||||||
|
- Parser: `PlayerFoldParser` — accepts optional `GAME_ID`.
|
||||||
|
- Handler: `PlayerFoldHandler` — validates the session, resolves the lobby by id when provided or by session otherwise and forwards to `GameController`.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
|
||||||
|
No additional response fields. Server replies with `+OK` on success.
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
| :--- | :---------- |
|
||||||
|
| `NOT_YOUR_TURN` | The player attempted to fold when not their turn |
|
||||||
|
| `GAME_NOT_STARTED` | No game is running in the lobby |
|
||||||
|
| `NOT_IN_LOBBY` | Requesting user is not a member of the lobby |
|
||||||
|
| `LOBBY_NOT_FOUND` | The specified `GAME_ID` does not exist |
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```
|
||||||
|
FOLD GAME_ID=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (success)
|
||||||
|
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (error)
|
||||||
|
|
||||||
|
```
|
||||||
|
-ERR
|
||||||
|
CODE=NOT_YOUR_TURN
|
||||||
|
MSG=It is not your turn
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
## GET_LOBBY_STATUS command
|
||||||
|
|
||||||
|
The `GET_LOBBY_STATUS` command requests the server to return the current state of a lobby, including the list of players and their ready state.
|
||||||
|
|
||||||
|
### Required pre-execution checks
|
||||||
|
None.
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
| Parameter Name | Type | Optional | Description |
|
||||||
|
| :------------- | :--- | :------: | :---------- |
|
||||||
|
| `ID` | `int` | no | Numeric id of the target lobby |
|
||||||
|
|
||||||
|
### Implementation notes
|
||||||
|
|
||||||
|
- Parser: `GetLobbyStatusParser` — reads the `ID` parameter and builds `GetLobbyStatusRequest`.
|
||||||
|
- Handler: `GetLobbyStatusHandler` — queries `LobbyManager` for the lobby and builds a `GetLobbyStatusResponse` containing player entries.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
|
||||||
|
The response contains a `LOBBY` collection with nested `PLAYERS` and one or more `PLAYER` entries. Each `PLAYER` entry contains `USERNAME` and `READY` fields.
|
||||||
|
|
||||||
|
Example response structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
LOBBY
|
||||||
|
ID=1
|
||||||
|
PLAYERS
|
||||||
|
PLAYER
|
||||||
|
USERNAME=Lars_001
|
||||||
|
READY=false
|
||||||
|
END
|
||||||
|
PLAYER
|
||||||
|
USERNAME=Anna
|
||||||
|
READY=true
|
||||||
|
END
|
||||||
|
END
|
||||||
|
END
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
| Code | Description |
|
||||||
|
| :--- | :---------- |
|
||||||
|
| `LOBBY_NOT_FOUND` | The specified lobby id does not exist |
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```
|
||||||
|
GET_LOBBY_STATUS ID=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (error)
|
||||||
|
|
||||||
|
```
|
||||||
|
-ERR
|
||||||
|
CODE=LOBBY_NOT_FOUND
|
||||||
|
MESSAGE=Lobby not found
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
## CREATE_LOBBY command
|
||||||
|
|
||||||
|
The `CREATE_LOBBY` command requests the server to create a new lobby and return its identifier.
|
||||||
|
|
||||||
|
### Required pre-execution checks
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
|
||||||
|
No parameters.
|
||||||
|
|
||||||
|
### Implementation notes
|
||||||
|
|
||||||
|
- Parser: CreateLobbyParser — constructs a CreateLobbyRequest from the incoming PrimitiveRequest context (no parameters are read).
|
||||||
|
- Handler: CreateLobbyHandler — calls LobbyManager.createNewLobby(null).
|
||||||
|
- If the returned LobbyId is null, the handler dispatches an ErrorResponse with code `LOBBIES_FULL` and message "Maximum number of 8 lobbies reached".
|
||||||
|
- On success, the handler dispatches a CreateLobbyResponse containing the new lobby id.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
| :-------- | :--- | :---------- |
|
||||||
|
| `LOBBY_ID`| `int`| Numeric id of the newly created lobby |
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
| :---------- | :--------------------------------------------------------------------- |
|
||||||
|
| `LOBBIES_FULL` | The server cannot create a new lobby because the maximum number of lobbies has been reached |
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```
|
||||||
|
CREATE_LOBBY
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Success Response
|
||||||
|
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
LOBBY_ID=1
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Error Response
|
||||||
|
|
||||||
|
```
|
||||||
|
-ERR
|
||||||
|
CODE=LOBBIES_FULL
|
||||||
|
MESSAGE=Maximum number of 8 lobbies reached
|
||||||
|
END
|
||||||
|
```
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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}
|
||||||
@@ -7,6 +7,12 @@ import org.apache.logging.log4j.Logger;
|
|||||||
|
|
||||||
/** Main entry point for Casono application. Handles client and server startup. */
|
/** Main entry point for Casono application. Handles client and server startup. */
|
||||||
public final class Main {
|
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.
|
* Main entry point for Casono.
|
||||||
*
|
*
|
||||||
@@ -17,10 +23,13 @@ public final class Main {
|
|||||||
printUsage();
|
printUsage();
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (args[0]) {
|
switch (args[0]) {
|
||||||
case "server" -> ServerApp.start(args[1]);
|
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 -> {
|
default -> {
|
||||||
printUsage();
|
printUsage();
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
@@ -29,12 +38,15 @@ public final class Main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isValid(String[] args) {
|
private static boolean isValid(String[] args) {
|
||||||
if (args.length != 2) {
|
if (args.length < ARGS_COUNT_SERVER) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return switch (args[0]) {
|
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;
|
default -> false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -45,7 +57,7 @@ public final class Main {
|
|||||||
"""
|
"""
|
||||||
Usage:
|
Usage:
|
||||||
java -jar xyz.jar server <listenPort>
|
java -jar xyz.jar server <listenPort>
|
||||||
java -jar xyz.jar client <serverIp>:<serverPort>
|
java -jar xyz.jar client <serverIp>:<serverPort> [<username>]
|
||||||
""");
|
""");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,137 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client;
|
package ch.unibas.dmi.dbis.cs108.casono.client;
|
||||||
|
|
||||||
/**
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||||
* Entry point for the Casono client application. Handles client startup and connection parameters.
|
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 ch.unibas.dmi.dbis.cs108.casono.client.ui.Launcher;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
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 (optional)
|
||||||
*/
|
*/
|
||||||
public class ClientApp {
|
public class ClientApp {
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(ClientApp.class);
|
private static final Logger LOGGER = LogManager.getLogger(ClientApp.class);
|
||||||
|
|
||||||
|
/** Shared client connection used when a username is provided at startup. */
|
||||||
|
private static volatile ClientService sharedClientService;
|
||||||
|
|
||||||
|
private static volatile String sharedUsername;
|
||||||
|
|
||||||
/** Default constructor. */
|
/** Default constructor. */
|
||||||
public ClientApp() {
|
public ClientApp() {
|
||||||
// Default constructor
|
// Default constructor
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static ClientService getSharedClientService() {
|
||||||
* Starts the client application with the given address.
|
return sharedClientService;
|
||||||
*
|
}
|
||||||
* @param arg Address in the format "ip:port".
|
|
||||||
* @throws IllegalArgumentException if the address format is invalid.
|
private static void setSharedClientService(ClientService cs) {
|
||||||
*/
|
sharedClientService = cs;
|
||||||
public static void start(String arg) {
|
}
|
||||||
|
|
||||||
|
public static String getSharedUsername() {
|
||||||
|
return sharedUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
configureServerProperties(host, port);
|
||||||
|
setSharedUsername(username != null && !username.isBlank() ? username.trim() : null);
|
||||||
|
|
||||||
|
if (getSharedUsername() != null) {
|
||||||
|
performStartupLogin(host, port, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
launchUI(arg, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String[] parseHostAndPort(String arg) {
|
||||||
String[] parts = arg.split(":", 2);
|
String[] parts = arg.split(":", 2);
|
||||||
if (parts.length != 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>.");
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
}
|
}
|
||||||
String host = parts[0];
|
|
||||||
int port = Integer.parseInt(parts[1]);
|
|
||||||
|
|
||||||
|
private static void configureServerProperties(String host, int port) {
|
||||||
LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host);
|
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);
|
||||||
|
|
||||||
|
java.util.concurrent.ExecutorService bg =
|
||||||
|
java.util.concurrent.Executors.newSingleThreadExecutor(
|
||||||
|
r -> {
|
||||||
|
Thread t = new Thread(r);
|
||||||
|
t.setDaemon(true);
|
||||||
|
t.setName("casono-startup-login");
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
|
||||||
|
bg.submit(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
LobbyClient lobbyClient = new LobbyClient(clientService);
|
||||||
|
LoginResult res = lobbyClient.login(username);
|
||||||
|
|
||||||
|
if (res != null
|
||||||
|
&& res.getUsername() != null
|
||||||
|
&& !res.getUsername().isBlank()) {
|
||||||
|
setSharedUsername(res.getUsername().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info(
|
||||||
|
"Assigned username='{}' id={}",
|
||||||
|
res != null ? res.getUsername() : null,
|
||||||
|
res != null ? res.getId() : null);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOGGER.warn("Startup login failed: {}", e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("Unexpected error during startup login", e);
|
||||||
|
} finally {
|
||||||
|
bg.shutdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOGGER.warn(
|
||||||
|
"Could not establish initial connection for startup login: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void launchUI(String arg, String username) {
|
||||||
|
if (username != null && !username.isBlank()) {
|
||||||
|
Launcher.main(new String[] {arg, username});
|
||||||
|
} else {
|
||||||
|
Launcher.main(new String[] {arg});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
if (args == null || args.length == 0) {
|
||||||
|
throw new IllegalArgumentException("Address argument required: <host:port>");
|
||||||
|
}
|
||||||
|
|
||||||
|
String username = args.length > 1 && args[1] != null && !args[1].isBlank() ? args[1] : null;
|
||||||
|
|
||||||
|
start(args[0], username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||||
|
|
||||||
|
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 java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
import java.util.WeakHashMap;
|
||||||
|
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 final String username;
|
||||||
|
|
||||||
|
private final ClientService clientService;
|
||||||
|
|
||||||
|
private final ChatClient chatClient;
|
||||||
|
|
||||||
|
public ChatBoxController getChatBoxController() {
|
||||||
|
return chatBoxController;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ChatBoxController chatBoxController;
|
||||||
|
private int lobbyId = -1;
|
||||||
|
private final Timer timer;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
registerAsActiveController(clientService);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
||||||
|
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,
|
||||||
|
username,
|
||||||
|
msg.lobbyId,
|
||||||
|
null))
|
||||||
|
.addMessage(msg);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ChatType.WHISPER:
|
||||||
|
if (msg.target.equals(username) || msg.sender.equals(username)) {
|
||||||
|
ChatKey key;
|
||||||
|
if (msg.target.equals(username)) {
|
||||||
|
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,
|
||||||
|
username,
|
||||||
|
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();
|
||||||
|
logger.info(users);
|
||||||
|
if (!users.isEmpty()) {
|
||||||
|
for (String user : users) {
|
||||||
|
String value = user.split("\\=")[1];
|
||||||
|
logger.info(value);
|
||||||
|
addWhisperUser(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(username))) {
|
||||||
|
localUserList.add(user);
|
||||||
|
logger.info("adding new whisper user");
|
||||||
|
chatBoxController.addWhisperUser(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stops polling background tasks for this chat controller instance. */
|
||||||
|
public void shutdown() {
|
||||||
|
timer.cancel();
|
||||||
|
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,85 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import javafx.beans.property.IntegerProperty;
|
||||||
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChatModel, stores the data for a specific chat
|
||||||
|
*
|
||||||
|
* <p>Holds the current state of a chat
|
||||||
|
*/
|
||||||
|
public class ChatModel {
|
||||||
|
|
||||||
|
private 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 final String target;
|
||||||
|
|
||||||
|
private final IntegerProperty count;
|
||||||
|
|
||||||
|
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<Message>();
|
||||||
|
this.chattype = chattype;
|
||||||
|
this.username = username;
|
||||||
|
this.count = new SimpleIntegerProperty(0);
|
||||||
|
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.stream().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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 = 0;
|
||||||
|
public String target = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,156 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.game;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.GameClient;
|
||||||
|
import java.util.List;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
||||||
|
client.sendCall();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retrieves the current phase of the game. */
|
||||||
|
public void fold() {
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
client.sendRaise(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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,20 @@
|
|||||||
|
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 String phase;
|
||||||
|
public int pot;
|
||||||
|
public int currentBet;
|
||||||
|
public int dealer;
|
||||||
|
public int activePlayer;
|
||||||
|
public int winnerIndex = -1;
|
||||||
|
}
|
||||||
@@ -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,91 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,433 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
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>[+-]?[-\\d\\w:]+))");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,281 @@
|
|||||||
|
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() {
|
||||||
|
client.processCommand("CALL GAME_ID=" + gameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendFold() {
|
||||||
|
client.processCommand("FOLD GAME_ID=" + gameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendBet(int amount) {
|
||||||
|
client.processCommand("BET GAME_ID=" + gameId + " AMOUNT=" + amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendRaise(int amount) {
|
||||||
|
client.processCommand("RAISE GAME_ID=" + gameId + " AMOUNT=" + amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GameState parseGameState(String input) {
|
||||||
|
GameState s = new GameState();
|
||||||
|
s.players = new ArrayList<>();
|
||||||
|
s.communityCards = 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;
|
||||||
|
}
|
||||||
|
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("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,209 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
List<String> lines = client.processCommand("LOGIN USERNAME=" + user);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 we were collecting a lobby, flush it
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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,183 @@
|
|||||||
|
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.ChatType;
|
||||||
|
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.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
public class ChatBoxController {
|
||||||
|
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
private ChatController chatController;
|
||||||
|
|
||||||
|
@FXML private VBox chatBox;
|
||||||
|
|
||||||
|
@FXML private TabPane chatTabPane;
|
||||||
|
|
||||||
|
@FXML private MenuButton addWhisperChatButton;
|
||||||
|
|
||||||
|
@FXML private HBox menuBox;
|
||||||
|
|
||||||
|
private FXMLLoader fxmlLoader;
|
||||||
|
|
||||||
|
private final List<String> activeWhisperChats;
|
||||||
|
|
||||||
|
@FXML private final Map<String, Tab> usernameTabMap;
|
||||||
|
|
||||||
|
private String ressource = "/ui-structure/components/chatui/chattab.fxml";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for the ChatBoxController, initializes the necessary fields and data structures
|
||||||
|
* for managing chat tabs and whisper chats.
|
||||||
|
*
|
||||||
|
* @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<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
||||||
|
ChatModel globalChatModel = new ChatModel(ChatType.GLOBAL, username, -1, null);
|
||||||
|
chatController
|
||||||
|
.getChatModelMap()
|
||||||
|
.put(new ChatController.ChatKey(ChatType.GLOBAL), globalChatModel);
|
||||||
|
addChatTab("GLOBAL", globalChatModel);
|
||||||
|
addWhisperChatButton.setOnAction(event -> addWhisperChatButton.show());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(
|
||||||
|
() -> {
|
||||||
|
MenuItem menuItem = new MenuItem(targetUserName);
|
||||||
|
addWhisperChatButton.getItems().add(menuItem);
|
||||||
|
menuItem.setOnAction(
|
||||||
|
event ->
|
||||||
|
addWhisperChat(
|
||||||
|
targetUserName,
|
||||||
|
new ChatModel(
|
||||||
|
ChatType.WHISPER,
|
||||||
|
username,
|
||||||
|
-1,
|
||||||
|
targetUserName)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
chatController
|
||||||
|
.getChatModelMap()
|
||||||
|
.put(new ChatController.ChatKey(ChatType.WHISPER, target), chatModel);
|
||||||
|
addChatTab(target, chatModel);
|
||||||
|
} else {
|
||||||
|
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 void addChatTab(String title, ChatModel chatModel) {
|
||||||
|
URL resource = getClass().getResource(ressource);
|
||||||
|
FXMLLoader fxmlLoader = new FXMLLoader(resource);
|
||||||
|
runOnPlatformSynchronized(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
ChatViewController chatViewController =
|
||||||
|
new ChatViewController(
|
||||||
|
this.chatController, chatModel, this.username);
|
||||||
|
fxmlLoader.setController(chatViewController);
|
||||||
|
Node load = fxmlLoader.load();
|
||||||
|
VBox.setVgrow(load, Priority.ALWAYS);
|
||||||
|
VBox vbox = new VBox();
|
||||||
|
VBox.setVgrow(vbox, Priority.ALWAYS);
|
||||||
|
vbox.getChildren().add(load);
|
||||||
|
chatModel.addListener((msg) -> chatViewController.showMessage(msg));
|
||||||
|
Tab newChat = new Tab(title, vbox);
|
||||||
|
usernameTabMap.put(title, newChat);
|
||||||
|
this.chatTabPane.getTabs().add(newChat);
|
||||||
|
this.chatTabPane.getSelectionModel().select(newChat);
|
||||||
|
return newChat;
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,102 @@
|
|||||||
|
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 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.TextField;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
/** Responsible for the presentation of the ChatModel to the Client */
|
||||||
|
public class ChatViewController implements Initializable {
|
||||||
|
|
||||||
|
@FXML private Button sendButton;
|
||||||
|
|
||||||
|
@FXML private TextField inputField;
|
||||||
|
|
||||||
|
@FXML private VBox chatInterfaceVBox;
|
||||||
|
|
||||||
|
@FXML private HBox controlBar;
|
||||||
|
|
||||||
|
@FXML private ScrollPane scrollPane;
|
||||||
|
|
||||||
|
@FXML private VBox chat;
|
||||||
|
|
||||||
|
private final ChatModel chatModel;
|
||||||
|
|
||||||
|
private final String username;
|
||||||
|
|
||||||
|
private final ChatController controller;
|
||||||
|
|
||||||
|
private static final int CHAT_PADDING = 20;
|
||||||
|
|
||||||
|
public ChatViewController(ChatController chatController, ChatModel chatModel, String username) {
|
||||||
|
this.controller = chatController;
|
||||||
|
this.username = username;
|
||||||
|
this.chatModel = chatModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(event -> sendMessage());
|
||||||
|
sendButton.setOnAction(event -> 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() {
|
||||||
|
String message = inputField.getText().trim();
|
||||||
|
if (!message.isEmpty()) {
|
||||||
|
inputField.clear();
|
||||||
|
Message msg =
|
||||||
|
new Message(
|
||||||
|
chatModel.getChattype(),
|
||||||
|
chatModel.lobbyId,
|
||||||
|
username,
|
||||||
|
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) {
|
||||||
|
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,8 +1,15 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
|
||||||
|
|
||||||
|
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 java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.logging.Logger;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.scene.Parent;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
@@ -11,44 +18,144 @@ import javafx.stage.Stage;
|
|||||||
*
|
*
|
||||||
* <p>Starts the JavaFX application, loads the graphical user interface from the FXML file, and
|
* <p>Starts the JavaFX application, loads the graphical user interface from the FXML file, and
|
||||||
* initializes the main stage for the game.
|
* 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 {
|
public class CasinoGameUI extends Application {
|
||||||
|
|
||||||
/** default constructor */
|
private static final Logger LOG = Logger.getLogger(CasinoGameUI.class.getName());
|
||||||
|
|
||||||
|
private static ClientService clientService;
|
||||||
|
|
||||||
|
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() {
|
public CasinoGameUI() {
|
||||||
// default no-arg constructor
|
// default no-arg constructor
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final int DEFAULT_WIDTH = 1200;
|
|
||||||
private static final int DEFAULT_HEIGHT = 800;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the main stage of the application.
|
* Sets the ClientService instance to be used by the application.
|
||||||
*
|
*
|
||||||
* @param stage The main stage provided by the system.
|
* @param clientService the ClientService instance to set
|
||||||
* @throws IOException If the FXML file or resources cannot be loaded.
|
|
||||||
*/
|
*/
|
||||||
@Override
|
public static void setClientService(ClientService clientService) {
|
||||||
public void start(Stage stage) throws IOException {
|
CasinoGameUI.clientService = clientService;
|
||||||
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)");
|
|
||||||
|
|
||||||
String iconPath = getClass().getResource("/images/logoinverted.png").toExternalForm();
|
|
||||||
stage.getIcons().add(new javafx.scene.image.Image(iconPath));
|
|
||||||
stage.setScene(scene);
|
|
||||||
stage.setFullScreen(true);
|
|
||||||
stage.show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starting point of the application.
|
* Sets the username to be used by the application.
|
||||||
*
|
*
|
||||||
* @param args Command line arguments.
|
* @param username the username to set
|
||||||
|
*/
|
||||||
|
public static void setUsername(String username) {
|
||||||
|
CasinoGameUI.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
|
||||||
|
if (clientService == null) {
|
||||||
|
clientService =
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.client.ClientApp.getSharedClientService();
|
||||||
|
}
|
||||||
|
if (clientService == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"CasinoGameUI: clientService is null. "
|
||||||
|
+ "Call CasinoGameUI.setClientService(...)"
|
||||||
|
+ " or start via ClientApp with a shared connection.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String effectiveUsername = normalize(username);
|
||||||
|
|
||||||
|
if (effectiveUsername == null) {
|
||||||
|
effectiveUsername =
|
||||||
|
normalize(ch.unibas.dmi.dbis.cs108.casono.client.ClientApp.getSharedUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effectiveUsername == null) {
|
||||||
|
effectiveUsername =
|
||||||
|
"Guest-" + UUID.randomUUID().toString().substring(0, GUEST_ID_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.info(
|
||||||
|
"CasinoGameUI starting: effectiveUsername='"
|
||||||
|
+ effectiveUsername
|
||||||
|
+ "', injectedUsername='"
|
||||||
|
+ username
|
||||||
|
+ "', sharedUsername='"
|
||||||
|
+ ch.unibas.dmi.dbis.cs108.casono.client.ClientApp.getSharedUsername()
|
||||||
|
+ "', hasClientService="
|
||||||
|
+ (clientService != null));
|
||||||
|
|
||||||
|
FXMLLoader fxmlLoader =
|
||||||
|
new FXMLLoader(CasinoGameUI.class.getResource("/ui-structure/Casinogameui.fxml"));
|
||||||
|
Parent root = fxmlLoader.load();
|
||||||
|
CasinoGameController controller = fxmlLoader.getController();
|
||||||
|
|
||||||
|
int gameId = 1; // TODO echte gameId einsetzen
|
||||||
|
GameClient gameClient = new GameClient(clientService, gameId);
|
||||||
|
GameService gameService = new GameService(gameClient);
|
||||||
|
controller.setGameService(gameService);
|
||||||
|
|
||||||
|
controller.setMyPlayerId(PlayerId.of(effectiveUsername));
|
||||||
|
controller.setChatContext(effectiveUsername, clientService, lobbyId);
|
||||||
|
|
||||||
|
Scene scene = new Scene(root, DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||||
|
stage.setTitle("Casono");
|
||||||
|
|
||||||
|
String iconPath = getClass().getResource("/images/logoinverted.png").toExternalForm();
|
||||||
|
stage.getIcons().add(new javafx.scene.image.Image(iconPath));
|
||||||
|
|
||||||
|
stage.setScene(scene);
|
||||||
|
stage.setFullScreen(true);
|
||||||
|
stage.setOnHidden(e -> controller.stop());
|
||||||
|
stage.show();
|
||||||
|
|
||||||
|
controller.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a string by trimming whitespace and converting blank strings to null.
|
||||||
|
*
|
||||||
|
* @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) {
|
public static void main(String[] args) {
|
||||||
launch();
|
launch();
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.game.Player;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.Pane;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for displaying a player's status in the poker game, including their name, chip count
|
||||||
|
* and whether they are the dealer.
|
||||||
|
*/
|
||||||
|
public class PlayerStatusController {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(PlayerStatusController.class.getName());
|
||||||
|
|
||||||
|
@FXML private Label playerName;
|
||||||
|
@FXML private Label playerMoney;
|
||||||
|
@FXML private ImageView dealerIcon;
|
||||||
|
@FXML private Pane parent;
|
||||||
|
|
||||||
|
private Image dealerImage;
|
||||||
|
private Player player;
|
||||||
|
private static final String DEALER_IMAGE_PATH = "/images/chip-dealer-blue-3.png";
|
||||||
|
private static final double DEALER_ICON_X_FACTOR = 0.8;
|
||||||
|
|
||||||
|
/** Initialize the controller, load dealer image, and set up bindings. */
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
var url = getClass().getResource(DEALER_IMAGE_PATH);
|
||||||
|
|
||||||
|
if (url != null) {
|
||||||
|
dealerImage = new Image(url.toExternalForm(), true);
|
||||||
|
dealerIcon.setImage(dealerImage);
|
||||||
|
} else {
|
||||||
|
LOGGER.warning("Dealer image not found in resources!");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.severe("Error: loading dealer image:");
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
dealerIcon.layoutXProperty().bind(parent.widthProperty().multiply(DEALER_ICON_X_FACTOR));
|
||||||
|
dealerIcon.setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind player to UI
|
||||||
|
*
|
||||||
|
* @param player The player whose status is to be displayed.
|
||||||
|
*/
|
||||||
|
public void setPlayer(Player player) {
|
||||||
|
this.player = player;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Refresh UI safely */
|
||||||
|
public void refresh() {
|
||||||
|
|
||||||
|
if (player == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NAME (safe)
|
||||||
|
String name = player.getName();
|
||||||
|
playerName.setText(name != null ? name : "-");
|
||||||
|
|
||||||
|
// MONEY
|
||||||
|
playerMoney.setText(player.getChips() + " $");
|
||||||
|
|
||||||
|
// DEALER
|
||||||
|
boolean isDealer = player.getState() == PlayerState.DEALER;
|
||||||
|
|
||||||
|
dealerIcon.setVisible(isDealer);
|
||||||
|
|
||||||
|
if (isDealer && dealerImage != null) {
|
||||||
|
dealerIcon.setImage(dealerImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* External quick update
|
||||||
|
*
|
||||||
|
* @param name The player's name to display.
|
||||||
|
* @param chips The player's chip count to display.
|
||||||
|
*/
|
||||||
|
public void updatePlayer(String name, int chips) {
|
||||||
|
playerName.setText(name != null ? name : "-");
|
||||||
|
playerMoney.setText(chips + " $");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force dealer state
|
||||||
|
*
|
||||||
|
* @param isDealer Whether the player is the dealer or not.
|
||||||
|
*/
|
||||||
|
public void setDealer(boolean isDealer) {
|
||||||
|
dealerIcon.setVisible(isDealer);
|
||||||
|
|
||||||
|
if (isDealer && dealerImage != null) {
|
||||||
|
dealerIcon.setImage(dealerImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameService;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameState;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.game.Player;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui;
|
import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.input.KeyCode;
|
import javafx.scene.input.KeyCode;
|
||||||
import javafx.scene.input.KeyEvent;
|
import javafx.scene.input.KeyEvent;
|
||||||
@@ -18,22 +25,228 @@ import org.apache.logging.log4j.Logger;
|
|||||||
*/
|
*/
|
||||||
public class TaskbarController {
|
public class TaskbarController {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class);
|
||||||
|
|
||||||
|
@FXML private HBox taskbar;
|
||||||
|
@FXML private TextField taskbarInput;
|
||||||
|
@FXML private Button betButton;
|
||||||
|
@FXML private Button callButton;
|
||||||
|
@FXML private Button foldButton;
|
||||||
|
@FXML private Button raiseButton;
|
||||||
|
@FXML private Label moneyLabel;
|
||||||
|
|
||||||
|
private GameService gameService;
|
||||||
|
private PlayerId myPlayerId;
|
||||||
|
private GameState lastState;
|
||||||
|
private double xOffset = 0;
|
||||||
|
private double yOffset = 0;
|
||||||
|
private static final double TASKBAR_SCALE = 0.95;
|
||||||
|
private static final int MIN_CREDITS = 5;
|
||||||
|
private static final int MAX_CREDITS = 100000;
|
||||||
|
private static final int CREDIT_STEP = 5;
|
||||||
|
private static final String STYLE_YELLOW_BUTTON = "yellow-button";
|
||||||
|
private static final String STYLE_GRAY_BUTTON = "gray-button";
|
||||||
|
private static final String STYLE_RED_BUTTON = "red-button";
|
||||||
|
private static final String STYLE_YELLOW_INPUT = "yellow-input-field";
|
||||||
|
private static final String STYLE_GRAY_INPUT = "gray-input-field";
|
||||||
|
private static final String STYLE_RED_INPUT = "red-input-field";
|
||||||
|
private static final double SCALE_NORMAL = 1.0;
|
||||||
|
|
||||||
/** Standard constructor. Used by FXML. */
|
/** Standard constructor. Used by FXML. */
|
||||||
public TaskbarController() {
|
public TaskbarController() {
|
||||||
// default constructor for FXML
|
// default constructor for FXML
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class);
|
/**
|
||||||
|
* Updates the state of the basic action buttons (Bet, Call, Fold, Raise) and the input field
|
||||||
|
*
|
||||||
|
* @param isMyTurn Indicates whether it is currently the player's turn.
|
||||||
|
* @param isOut Indicates whether the player is currently out of the game (folded or all-in).
|
||||||
|
*/
|
||||||
|
private void updateBasicButtons(boolean isMyTurn, boolean isOut) {
|
||||||
|
|
||||||
@FXML private HBox taskbar;
|
if (isOut) {
|
||||||
@FXML private TextField taskbarInput;
|
setRedButton(betButton);
|
||||||
|
setRedButton(callButton);
|
||||||
|
setRedButton(foldButton);
|
||||||
|
setRedButton(raiseButton);
|
||||||
|
setRedInputField(taskbarInput);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
private double xOffset = 0;
|
if (isMyTurn) {
|
||||||
private double yOffset = 0;
|
activateButton(betButton);
|
||||||
private static final double TASKBAR_SCALE = 0.9;
|
activateButton(callButton);
|
||||||
private static final int MIN_CREDITS = 5;
|
activateButton(foldButton);
|
||||||
private static final int MAX_CREDITS = 100000;
|
activateButton(raiseButton);
|
||||||
private static final int CREDIT_STEP = 5;
|
activateInputField(taskbarInput);
|
||||||
|
} else {
|
||||||
|
deactivateButton(betButton);
|
||||||
|
deactivateButton(callButton);
|
||||||
|
deactivateButton(foldButton);
|
||||||
|
deactivateButton(raiseButton);
|
||||||
|
deactivateInputField(taskbarInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the given button to a disabled state with red styling, indicating that the player is out
|
||||||
|
*
|
||||||
|
* @param b The button to be styled as red and disabled
|
||||||
|
*/
|
||||||
|
private void setRedButton(Button b) {
|
||||||
|
b.setDisable(true);
|
||||||
|
|
||||||
|
b.getStyleClass().remove(STYLE_YELLOW_BUTTON);
|
||||||
|
b.getStyleClass().remove(STYLE_GRAY_BUTTON);
|
||||||
|
|
||||||
|
if (!b.getStyleClass().contains(STYLE_RED_BUTTON)) {
|
||||||
|
b.getStyleClass().add(STYLE_RED_BUTTON);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the given input field to a disabled state with red styling, indicating that the player
|
||||||
|
* is out
|
||||||
|
*
|
||||||
|
* @param t The text field to be styled as red and disabled
|
||||||
|
*/
|
||||||
|
private void setRedInputField(TextField t) {
|
||||||
|
t.setDisable(true);
|
||||||
|
|
||||||
|
t.getStyleClass().remove(STYLE_YELLOW_INPUT);
|
||||||
|
t.getStyleClass().remove(STYLE_GRAY_INPUT);
|
||||||
|
|
||||||
|
if (!t.getStyleClass().contains(STYLE_RED_INPUT)) {
|
||||||
|
t.getStyleClass().add(STYLE_RED_INPUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activates the given button by enabling it and applying yellow styling, indicating that it is
|
||||||
|
* the player's turn
|
||||||
|
*
|
||||||
|
* @param b The button to be activated and styled for the player's turn
|
||||||
|
*/
|
||||||
|
private void activateButton(Button b) {
|
||||||
|
b.setDisable(false);
|
||||||
|
|
||||||
|
b.getStyleClass().remove(STYLE_GRAY_BUTTON);
|
||||||
|
|
||||||
|
if (!b.getStyleClass().contains(STYLE_YELLOW_BUTTON)) {
|
||||||
|
b.getStyleClass().add(STYLE_YELLOW_BUTTON);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activates the given input field by enabling it and applying yellow styling, indicating that
|
||||||
|
* it is the player's turn
|
||||||
|
*
|
||||||
|
* @param t The text field to be activated and styled for the player's turn
|
||||||
|
*/
|
||||||
|
private void activateInputField(TextField t) {
|
||||||
|
t.setDisable(false);
|
||||||
|
|
||||||
|
t.getStyleClass().remove(STYLE_GRAY_INPUT);
|
||||||
|
|
||||||
|
if (!t.getStyleClass().contains(STYLE_YELLOW_INPUT)) {
|
||||||
|
t.getStyleClass().add(STYLE_YELLOW_INPUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deactivates the given button by disabling it and applying gray styling, indicating that it is
|
||||||
|
* not the player's turn
|
||||||
|
*
|
||||||
|
* @param b The button to be deactivated and styled for non-active state
|
||||||
|
*/
|
||||||
|
private void deactivateButton(Button b) {
|
||||||
|
b.setDisable(true);
|
||||||
|
|
||||||
|
b.getStyleClass().remove(STYLE_YELLOW_BUTTON);
|
||||||
|
b.getStyleClass().remove(STYLE_RED_BUTTON);
|
||||||
|
|
||||||
|
if (!b.getStyleClass().contains(STYLE_GRAY_BUTTON)) {
|
||||||
|
b.getStyleClass().add(STYLE_GRAY_BUTTON);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deactivates the given input field by disabling it and applying gray styling, indicating that
|
||||||
|
* it is not the player's turn
|
||||||
|
*
|
||||||
|
* @param t The text field to be deactivated and styled for non-active state
|
||||||
|
*/
|
||||||
|
private void deactivateInputField(TextField t) {
|
||||||
|
t.setDisable(true);
|
||||||
|
|
||||||
|
t.getStyleClass().remove(STYLE_YELLOW_INPUT);
|
||||||
|
t.getStyleClass().remove(STYLE_RED_INPUT);
|
||||||
|
|
||||||
|
if (!t.getStyleClass().contains(STYLE_GRAY_INPUT)) {
|
||||||
|
t.getStyleClass().add(STYLE_GRAY_INPUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the displayed amount of money the player has. The amount is shown in the format "X$".
|
||||||
|
*
|
||||||
|
* @param amount The amount of money to display for the player.
|
||||||
|
*/
|
||||||
|
public void setMoney(int amount) {
|
||||||
|
moneyLabel.setText(amount + "$");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGameService(GameService gameService, PlayerId myPlayerId) {
|
||||||
|
this.gameService = gameService;
|
||||||
|
this.myPlayerId = myPlayerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the taskbar controller. This method is called automatically after the FXML
|
||||||
|
* components have been loaded.
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
updateBasicButtons(false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the taskbar based on the current game state and the player's status. It checks if
|
||||||
|
* it's the player's turn and whether they are out of the game (folded or all-in) to adjust the
|
||||||
|
* button states and displayed money accordingly.
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* @param myPlayerId
|
||||||
|
*/
|
||||||
|
public void update(GameState state, PlayerId myPlayerId) {
|
||||||
|
|
||||||
|
if (state == null || state.players == null || myPlayerId == null) {
|
||||||
|
LOGGER.warn("Cannot update taskbar: invalid input");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastState = state;
|
||||||
|
|
||||||
|
Player me =
|
||||||
|
state.players.stream()
|
||||||
|
.filter(p -> myPlayerId.equals(p.getId()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (me == null) {
|
||||||
|
LOGGER.error("Player not found in GameState!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int myIndex = state.players.indexOf(me);
|
||||||
|
|
||||||
|
boolean isMyTurn = state.activePlayer == myIndex;
|
||||||
|
boolean isOut = me.getState() == PlayerState.FOLDED;
|
||||||
|
|
||||||
|
updateBasicButtons(isMyTurn, isOut);
|
||||||
|
setMoney(me.getChips());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the taskbar is clicked with the mouse. Saves the relative position for later,
|
* Called when the taskbar is clicked with the mouse. Saves the relative position for later,
|
||||||
@@ -72,8 +285,8 @@ public class TaskbarController {
|
|||||||
*/
|
*/
|
||||||
@FXML
|
@FXML
|
||||||
private void onTaskbarReleased(MouseEvent event) {
|
private void onTaskbarReleased(MouseEvent event) {
|
||||||
taskbar.setScaleX(1.0);
|
taskbar.setScaleX(SCALE_NORMAL);
|
||||||
taskbar.setScaleY(1.0);
|
taskbar.setScaleY(SCALE_NORMAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,6 +310,89 @@ public class TaskbarController {
|
|||||||
processBet();
|
processBet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Called when the Call button is clicked. */
|
||||||
|
@FXML
|
||||||
|
private void onInputPlayerCall() {
|
||||||
|
|
||||||
|
if (gameService == null) {
|
||||||
|
LOGGER.error("GameService not initialized");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gameService.call();
|
||||||
|
LOGGER.info("Player CALL");
|
||||||
|
|
||||||
|
refreshGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called when the Fold button is clicked. */
|
||||||
|
@FXML
|
||||||
|
private void onInputPlayerFold() {
|
||||||
|
|
||||||
|
if (gameService == null) {
|
||||||
|
LOGGER.error("GameService not initialized");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gameService.fold();
|
||||||
|
LOGGER.info("Player FOLD");
|
||||||
|
|
||||||
|
refreshGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called when the Raise button is clicked. */
|
||||||
|
@FXML
|
||||||
|
private void onInputPlayerRaise() {
|
||||||
|
|
||||||
|
if (gameService == null) {
|
||||||
|
LOGGER.error("GameService not initialized");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState state = lastState;
|
||||||
|
if (state == null) {
|
||||||
|
try {
|
||||||
|
state = gameService.refresh();
|
||||||
|
lastState = state;
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Failed to refresh game state for raise: {}", e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == null || state.currentBet <= 0) {
|
||||||
|
LOGGER.warn(
|
||||||
|
"Raise not possible: current bet is {}",
|
||||||
|
state != null ? state.currentBet : null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int raiseAmount = state.currentBet;
|
||||||
|
gameService.raise(raiseAmount);
|
||||||
|
LOGGER.info("Player RAISE by {} (target: double table bet)", raiseAmount);
|
||||||
|
|
||||||
|
taskbarInput.clear();
|
||||||
|
refreshGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the game state by fetching the latest state from the GameService and updating the
|
||||||
|
* taskbar accordingly.
|
||||||
|
*/
|
||||||
|
private void refreshGame() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
GameState newState = gameService.refresh();
|
||||||
|
update(newState, myPlayerId);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Failed to refresh game state: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the Exit button is clicked. Closes the current game stage and opens the lobby UI.
|
||||||
|
*/
|
||||||
@FXML
|
@FXML
|
||||||
private void onExitButtonClick() {
|
private void onExitButtonClick() {
|
||||||
javafx.application.Platform.runLater(
|
javafx.application.Platform.runLater(
|
||||||
@@ -107,9 +403,26 @@ public class TaskbarController {
|
|||||||
currentStage.close();
|
currentStage.close();
|
||||||
// Start lobby UI
|
// Start lobby UI
|
||||||
try {
|
try {
|
||||||
new Casinomainui().start(new javafx.stage.Stage());
|
javafx.stage.Stage newStage = new javafx.stage.Stage();
|
||||||
|
javafx.fxml.FXMLLoader fxmlLoader =
|
||||||
|
new javafx.fxml.FXMLLoader(
|
||||||
|
getClass().getResource("/ui-structure/Casinomainui.fxml"));
|
||||||
|
javafx.scene.Parent root = fxmlLoader.load();
|
||||||
|
javafx.scene.Scene scene =
|
||||||
|
new javafx.scene.Scene(
|
||||||
|
root, Casinomainui.SCENE_WIDTH, Casinomainui.SCENE_HEIGHT);
|
||||||
|
newStage.setTitle("Casono");
|
||||||
|
javafx.scene.image.Image icon =
|
||||||
|
new javafx.scene.image.Image(
|
||||||
|
getClass()
|
||||||
|
.getResource("/images/logoinverted.png")
|
||||||
|
.toExternalForm());
|
||||||
|
newStage.getIcons().add(icon);
|
||||||
|
newStage.setScene(scene);
|
||||||
|
newStage.setFullScreen(true);
|
||||||
|
newStage.show();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.error("Fehler beim Starten der Lobby-UI: {}", e.getMessage());
|
LOGGER.error("Error: starting the lobby UI: {}", e.getMessage(), e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -120,19 +433,34 @@ public class TaskbarController {
|
|||||||
* displayed on the console.
|
* displayed on the console.
|
||||||
*/
|
*/
|
||||||
private void processBet() {
|
private void processBet() {
|
||||||
|
|
||||||
|
if (gameService == null) {
|
||||||
|
LOGGER.error("Error: GameService not initialized");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String input = taskbarInput.getText();
|
String input = taskbarInput.getText();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
int credits = Integer.parseInt(input.trim());
|
int credits = Integer.parseInt(input.trim());
|
||||||
|
|
||||||
if (credits >= MIN_CREDITS && credits <= MAX_CREDITS && credits % CREDIT_STEP == 0) {
|
if (credits >= MIN_CREDITS && credits <= MAX_CREDITS && credits % CREDIT_STEP == 0) {
|
||||||
// TODO: Credits must be sent to the GameEngine
|
|
||||||
LOGGER.info("Bet set: {} Casono Credits", credits);
|
gameService.bet(credits);
|
||||||
|
|
||||||
|
LOGGER.info("Bet placed: {}", credits);
|
||||||
|
|
||||||
taskbarInput.clear();
|
taskbarInput.clear();
|
||||||
|
|
||||||
|
refreshGame();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
LOGGER.error("Error: Only increments of 5 (5, 10, ... 100,000) are allowed!");
|
LOGGER.error("Error: Bet must be between 5 and 100000 and multiple of 5");
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
LOGGER.error("Error: Please enter only one number!");
|
LOGGER.error("Error: Invalid number entered");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ public class Casinomainui extends Application {
|
|||||||
// Default constructor
|
// Default constructor
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final int SCENE_WIDTH = 1200;
|
public static final int SCENE_WIDTH = 1200;
|
||||||
private static final int SCENE_HEIGHT = 800;
|
public static final int SCENE_HEIGHT = 800;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
/**
|
/**
|
||||||
@@ -30,6 +30,17 @@ public class Casinomainui extends Application {
|
|||||||
* @throws IOException If loading the FXML fails.
|
* @throws IOException If loading the FXML fails.
|
||||||
*/
|
*/
|
||||||
public void start(Stage stage) throws IOException {
|
public void start(Stage stage) throws IOException {
|
||||||
|
// If the launcher passed an address argument (ip:port), expose it as
|
||||||
|
// system properties so controllers can read it without embedding defaults.
|
||||||
|
var raw = getParameters().getRaw();
|
||||||
|
if (raw != null && raw.size() > 0) {
|
||||||
|
String arg = raw.get(0);
|
||||||
|
String[] parts = arg.split(":", 2);
|
||||||
|
if (parts.length == 2) {
|
||||||
|
System.setProperty("casono.server.host", parts[0]);
|
||||||
|
System.setProperty("casono.server.port", parts[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
FXMLLoader fxmlLoader =
|
FXMLLoader fxmlLoader =
|
||||||
new FXMLLoader(getClass().getResource("/ui-structure/Casinomainui.fxml"));
|
new FXMLLoader(getClass().getResource("/ui-structure/Casinomainui.fxml"));
|
||||||
Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT);
|
Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT);
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import javafx.scene.control.Alert.AlertType;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.layout.AnchorPane;
|
import javafx.scene.layout.AnchorPane;
|
||||||
@@ -23,17 +34,26 @@ public class CasinomainuiController {
|
|||||||
@FXML private Rectangle greenBox;
|
@FXML private Rectangle greenBox;
|
||||||
@FXML private Button exitbutton;
|
@FXML private Button exitbutton;
|
||||||
@FXML private VBox casinoTable;
|
@FXML private VBox casinoTable;
|
||||||
|
@FXML private TextField usernameField;
|
||||||
|
@FXML private Button loginButton;
|
||||||
|
@FXML private AnchorPane chatContainer;
|
||||||
|
|
||||||
private LobbyButtonTranslationManager translationManager;
|
private LobbyButtonTranslationManager translationManager;
|
||||||
private LobbyButtonGridManager gridManager;
|
private LobbyButtonGridManager gridManager;
|
||||||
private int nextButtonId = 1;
|
private int nextButtonId = 1;
|
||||||
|
private LobbyClient lobbyClient;
|
||||||
|
private ChatController chatController;
|
||||||
|
|
||||||
/** Default constructor for dependency injection by FXMLLoader. */
|
/** Default constructor used by FXMLLoader. */
|
||||||
public CasinomainuiController() {
|
public CasinomainuiController() {
|
||||||
// Default constructor
|
// Default constructor
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Initializes the UI components and sets default values. */
|
/**
|
||||||
|
* Initializes the UI components and sets default values. If a shared {@link
|
||||||
|
* ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService} exists (created at application
|
||||||
|
* start), the controller reuses it so the connection remains open and already-logged-in.
|
||||||
|
*/
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
titleLabel.setText("Casono");
|
titleLabel.setText("Casono");
|
||||||
@@ -41,8 +61,131 @@ public class CasinomainuiController {
|
|||||||
logoView.setImage(new Image(getClass().getResource("/images/logo.png").toExternalForm()));
|
logoView.setImage(new Image(getClass().getResource("/images/logo.png").toExternalForm()));
|
||||||
|
|
||||||
translationManager = LobbyButtonTranslationManager.getInstance();
|
translationManager = LobbyButtonTranslationManager.getInstance();
|
||||||
|
String host = System.getProperty("casono.server.host");
|
||||||
|
int port = Integer.parseInt(System.getProperty("casono.server.port"));
|
||||||
|
ClientService clientService = ClientApp.getSharedClientService();
|
||||||
|
if (clientService == null) {
|
||||||
|
try {
|
||||||
|
clientService = new ClientService(host, port);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOGGER.warn(
|
||||||
|
"Could not connect to server {}:{} — starting in offline mode: {}",
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
e.getMessage());
|
||||||
|
clientService = new ClientService(true); // offline mode
|
||||||
|
}
|
||||||
|
}
|
||||||
gridManager =
|
gridManager =
|
||||||
new LobbyButtonGridManager(new javafx.scene.layout.GridPane(), translationManager);
|
new LobbyButtonGridManager(
|
||||||
|
new javafx.scene.layout.GridPane(), translationManager, clientService);
|
||||||
|
// LobbyClient will use the provided ClientService; in offline mode calls will
|
||||||
|
// fail with RuntimeException
|
||||||
|
lobbyClient = new LobbyClient(clientService);
|
||||||
|
// Fetch existing lobbies from server on startup so newly-created lobbies
|
||||||
|
// by other clients are immediately visible.
|
||||||
|
try {
|
||||||
|
if (!lobbyClient.getClientService().isOffline()) {
|
||||||
|
var lobbies = lobbyClient.getLobbyList();
|
||||||
|
int bid = nextButtonId;
|
||||||
|
for (var li : lobbies) {
|
||||||
|
try {
|
||||||
|
translationManager.addLobbyButton(bid++, li.id);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("Could not add lobby button: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nextButtonId = bid;
|
||||||
|
}
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOGGER.warn("Failed to fetch lobby list at startup: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
casinoTable.getChildren().clear();
|
||||||
|
casinoTable.getChildren().add(gridManager.getGridPane());
|
||||||
|
gridManager.renderLobbyButtons();
|
||||||
|
|
||||||
|
initializeChat(clientService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the chat UI if a valid ClientService is available. If the client is offline or
|
||||||
|
* the
|
||||||
|
*
|
||||||
|
* @param clientService
|
||||||
|
*/
|
||||||
|
private void initializeChat(ClientService clientService) {
|
||||||
|
if (clientService == null || clientService.isOffline() || chatContainer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String username = resolveChatUsername();
|
||||||
|
chatController = new ChatController(username, clientService);
|
||||||
|
URL resource = getClass().getResource("/ui-structure/components/chatui/chatbox.fxml");
|
||||||
|
FXMLLoader loader = new FXMLLoader(resource);
|
||||||
|
loader.setController(chatController.getChatBoxController());
|
||||||
|
Node chatNode = loader.load();
|
||||||
|
chatContainer.getChildren().setAll(chatNode);
|
||||||
|
AnchorPane.setTopAnchor(chatNode, 0.0);
|
||||||
|
AnchorPane.setBottomAnchor(chatNode, 0.0);
|
||||||
|
AnchorPane.setLeftAnchor(chatNode, 0.0);
|
||||||
|
AnchorPane.setRightAnchor(chatNode, 0.0);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.warn("Could not initialize lobby chat UI: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the username to be used in the chat. It first checks for a shared username set at
|
||||||
|
* the
|
||||||
|
*
|
||||||
|
* @return The resolved username, or "Guest" if no valid username is found.
|
||||||
|
*/
|
||||||
|
private String resolveChatUsername() {
|
||||||
|
String shared = ClientApp.getSharedUsername();
|
||||||
|
if (shared != null && !shared.isBlank()) {
|
||||||
|
return shared.trim();
|
||||||
|
}
|
||||||
|
if (usernameField != null
|
||||||
|
&& usernameField.getText() != null
|
||||||
|
&& !usernameField.getText().isBlank()) {
|
||||||
|
return usernameField.getText().trim();
|
||||||
|
}
|
||||||
|
return "Guest";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handles the login button action. Validates input and calls LobbyClient.login(). */
|
||||||
|
@FXML
|
||||||
|
public void handleLoginButton() {
|
||||||
|
String username = usernameField.getText();
|
||||||
|
if (username == null || username.isBlank()) {
|
||||||
|
showAlert("Please enter a username.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Only allow alphanumeric, _ and -
|
||||||
|
if (!username.matches("[a-zA-Z0-9_-]+")) {
|
||||||
|
showAlert("Only letters, numbers, '_' and '-' are allowed!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (lobbyClient.getClientService().isOffline()) {
|
||||||
|
showAlert("Offline mode: cannot send login to server.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
lobbyClient.login(username);
|
||||||
|
showAlert("Login sent: " + username);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOGGER.error("Login failed: {}", e.getMessage());
|
||||||
|
showAlert("Login failed: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shows an alert dialog with the given message. */
|
||||||
|
private void showAlert(String message) {
|
||||||
|
Alert alert = new Alert(AlertType.INFORMATION);
|
||||||
|
alert.setTitle("Info");
|
||||||
|
alert.setHeaderText(null);
|
||||||
|
alert.setContentText(message);
|
||||||
|
alert.showAndWait();
|
||||||
casinoTable.getChildren().clear();
|
casinoTable.getChildren().clear();
|
||||||
casinoTable.getChildren().add(gridManager.getGridPane());
|
casinoTable.getChildren().add(gridManager.getGridPane());
|
||||||
gridManager.renderLobbyButtons();
|
gridManager.renderLobbyButtons();
|
||||||
@@ -51,10 +194,17 @@ public class CasinomainuiController {
|
|||||||
/** Handles the exit button action to close the application. */
|
/** Handles the exit button action to close the application. */
|
||||||
@FXML
|
@FXML
|
||||||
public void handleexitbutton() {
|
public void handleexitbutton() {
|
||||||
|
if (chatController != null) {
|
||||||
|
chatController.shutdown();
|
||||||
|
}
|
||||||
Platform.exit();
|
Platform.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Handles creation of a new lobby button. */
|
/**
|
||||||
|
* Handles creation of a new lobby button. Attempts to create a lobby on the server via the
|
||||||
|
* {@link LobbyButtonGridManager} and registers the new button in the local translation manager.
|
||||||
|
* Errors are logged and displayed as an informational alert.
|
||||||
|
*/
|
||||||
@FXML
|
@FXML
|
||||||
public void handleCreateLobbyButton() {
|
public void handleCreateLobbyButton() {
|
||||||
if (translationManager.isFull()) {
|
if (translationManager.isFull()) {
|
||||||
@@ -62,13 +212,21 @@ public class CasinomainuiController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int buttonId = nextButtonId++;
|
int buttonId = nextButtonId++;
|
||||||
int lobbyId = gridManager.createLobby();
|
|
||||||
try {
|
try {
|
||||||
|
String username = usernameField != null ? usernameField.getText() : "<unknown>";
|
||||||
|
LOGGER.info("Creating lobby for user: {}", username);
|
||||||
|
// avoid attempting to create a lobby when offline
|
||||||
|
if (lobbyClient.getClientService().isOffline()) {
|
||||||
|
LOGGER.warn("Cannot create lobby while offline");
|
||||||
|
showAlert("Offline mode: cannot create lobby.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int lobbyId = gridManager.createLobby();
|
||||||
translationManager.addLobbyButton(buttonId, lobbyId);
|
translationManager.addLobbyButton(buttonId, lobbyId);
|
||||||
LOGGER.info("ButtonID: {}, LobbyID: {}", buttonId, lobbyId);
|
LOGGER.info("ButtonID: {}, LobbyID: {}", buttonId, lobbyId);
|
||||||
gridManager.renderLobbyButtons();
|
gridManager.renderLobbyButtons();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.error("Error while adding lobby button: {}", e.getMessage());
|
LOGGER.error("Failed to create or add lobby: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||||
|
|
||||||
/**
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||||
* Manages the grid for lobby buttons and rendering. Uses LobbyButtonTranslationManager for mapping
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||||
* ButtonID to LobbyID.
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||||
*/
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
@@ -12,133 +21,573 @@ import javafx.scene.layout.GridPane;
|
|||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages the grid for lobby buttons and rendering. Uses LobbyButtonTranslationManager for mapping
|
|
||||||
* ButtonID to LobbyID.
|
|
||||||
*/
|
|
||||||
public class LobbyButtonGridManager {
|
public class LobbyButtonGridManager {
|
||||||
private static final double BUTTON_WIDTH_MARGIN = 20.0;
|
|
||||||
|
private static final double BTN_WIDTH_MRG = 20.0;
|
||||||
private static final double BUTTON_MIN_SIZE = 10.0;
|
private static final double BUTTON_MIN_SIZE = 10.0;
|
||||||
|
private static final int REFRESH_INTERVAL_SECONDS = 5;
|
||||||
|
private static final int INITIAL_DELAY_SECONDS = 5;
|
||||||
|
private static final int COLS = 4;
|
||||||
|
private static final int MAX_BUTTONS = 8;
|
||||||
|
private static final int GUEST_ID_LENGTH = 8;
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
|
private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
|
||||||
|
|
||||||
/** GridPane for the button grid. */
|
private static final String BUTTON_FALLBACK_IMAGE = "/images/lobbypictures/error.png";
|
||||||
|
|
||||||
|
private static final String BUTTON_IMAGE_TEMPLATE = "/images/lobbypictures/lobby_%d_%s.png";
|
||||||
|
|
||||||
private final GridPane gridPane;
|
private final GridPane gridPane;
|
||||||
|
|
||||||
/** Manager for mapping ButtonID to LobbyID. */
|
|
||||||
private final LobbyButtonTranslationManager translationManager;
|
private final LobbyButtonTranslationManager translationManager;
|
||||||
|
private final LobbyClient lobbyClient;
|
||||||
|
|
||||||
/** Number of rows in the grid. */
|
private final ConcurrentHashMap<String, Image> imageCache = new ConcurrentHashMap<>();
|
||||||
private static final int ROWS = 2;
|
|
||||||
|
|
||||||
/** Number of columns in the grid. */
|
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||||
private static final int COLS = 4;
|
|
||||||
|
|
||||||
/** Path to the button image. */
|
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||||
private static final String BUTTON_IMAGE_PATH = "/images/logo.png";
|
|
||||||
|
|
||||||
/** Max random lobby id. */
|
|
||||||
private static final int MAX_RANDOM_LOBBY_ID = 10000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for the GridManager.
|
|
||||||
*
|
|
||||||
* @param gridPane the GridPane for rendering
|
|
||||||
* @param translationManager the manager for mapping ButtonID to LobbyID
|
|
||||||
*/
|
|
||||||
public LobbyButtonGridManager(
|
public LobbyButtonGridManager(
|
||||||
GridPane gridPane, LobbyButtonTranslationManager translationManager) {
|
GridPane gridPane,
|
||||||
|
LobbyButtonTranslationManager translationManager,
|
||||||
|
LobbyClient lobbyClient) {
|
||||||
|
|
||||||
this.gridPane = gridPane;
|
this.gridPane = gridPane;
|
||||||
// Singleton immer verwenden
|
|
||||||
this.translationManager = LobbyButtonTranslationManager.getInstance();
|
this.translationManager = LobbyButtonTranslationManager.getInstance();
|
||||||
|
this.lobbyClient = lobbyClient;
|
||||||
|
|
||||||
|
startPeriodicRefresh(INITIAL_DELAY_SECONDS, REFRESH_INTERVAL_SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public LobbyButtonGridManager(
|
||||||
* Renders all lobby buttons in the grid. Creates a button for each mapping with image and event
|
GridPane gridPane,
|
||||||
* handler.
|
LobbyButtonTranslationManager translationManager,
|
||||||
*/
|
ClientService clientService) {
|
||||||
public void renderLobbyButtons() {
|
|
||||||
gridPane.getChildren().clear();
|
this(gridPane, translationManager, new LobbyClient(clientService));
|
||||||
int index = 0;
|
|
||||||
|
// Subscribe to server-initiated events so closed lobbies are removed
|
||||||
|
// immediately
|
||||||
|
clientService.addEventListener(this::handleClientServiceEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static record EventInfo(String event, String lobbyId) {}
|
||||||
|
|
||||||
|
private EventInfo extractEventInfo(List<RequestParameter> params) {
|
||||||
|
String evt = null;
|
||||||
|
String lidStr = null;
|
||||||
|
for (RequestParameter p : params) {
|
||||||
|
if ("EVENT".equalsIgnoreCase(p.key())) {
|
||||||
|
evt = p.value();
|
||||||
|
} else if ("LOBBY_ID".equalsIgnoreCase(p.key())) {
|
||||||
|
lidStr = p.value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new EventInfo(evt, lidStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer tryParseInt(String s) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(s);
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleLobbyClosed(int lid) {
|
||||||
|
LOGGER.info(
|
||||||
|
"LOBBY_CLOSED event for lobby {} — mapping before: {}",
|
||||||
|
lid,
|
||||||
|
translationManager.getButtonIdToLobbyId());
|
||||||
|
// find button id(s) for this lobby and remove mapping
|
||||||
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||||
if (mapping.isEmpty()) {
|
Integer toRemove = null;
|
||||||
// No buttons to render
|
for (Map.Entry<Integer, Integer> e : mapping.entrySet()) {
|
||||||
|
if (e.getValue() != null && e.getValue().intValue() == lid) {
|
||||||
|
toRemove = e.getKey();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toRemove != null) {
|
||||||
|
LOGGER.info(
|
||||||
|
"Removing mapping for button {} -> lobby {} (LOBBY_CLOSED event)",
|
||||||
|
toRemove,
|
||||||
|
lid);
|
||||||
|
translationManager.removeLobbyButton(toRemove);
|
||||||
|
LOGGER.debug("Mapping after removal: {}", translationManager.getButtonIdToLobbyId());
|
||||||
|
javafx.application.Platform.runLater(this::renderLobbyButtons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleLobbyCreated(int lid) {
|
||||||
|
LOGGER.info(
|
||||||
|
"LOBBY_CREATED event for lobby {} — mapping before: {}",
|
||||||
|
lid,
|
||||||
|
translationManager.getButtonIdToLobbyId());
|
||||||
|
|
||||||
|
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||||
|
|
||||||
|
// If this lobby is already known, nothing to do
|
||||||
|
if (mapping.containsValue(lid)) {
|
||||||
|
LOGGER.debug("Lobby {} already mapped, skipping", lid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (Map.Entry<Integer, Integer> entry : mapping.entrySet()) {
|
|
||||||
int buttonId = entry.getKey();
|
// find next free button id (1..MAX_BUTTONS)
|
||||||
|
for (int candidate = 1; candidate <= MAX_BUTTONS; candidate++) {
|
||||||
|
if (!mapping.containsKey(candidate)) {
|
||||||
|
try {
|
||||||
|
translationManager.addLobbyButton(candidate, lid);
|
||||||
|
LOGGER.info("Mapped new lobby {} to button {}", lid, candidate);
|
||||||
|
LOGGER.debug(
|
||||||
|
"Mapping after add: {}", translationManager.getButtonIdToLobbyId());
|
||||||
|
javafx.application.Platform.runLater(this::renderLobbyButtons);
|
||||||
|
break;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// grid full? try next candidate
|
||||||
|
LOGGER.debug("Could not map created lobby {}: {}", lid, ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleClientServiceEvent(List<String> lines) {
|
||||||
|
EventInfo info = extractEventInfo(ClientService.convertToRequestParameters(lines));
|
||||||
|
|
||||||
|
if (info.event() == null || info.lobbyId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer lid = tryParseInt(info.lobbyId());
|
||||||
|
if (lid == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("LOBBY_CLOSED".equalsIgnoreCase(info.event())) {
|
||||||
|
handleLobbyClosed(lid);
|
||||||
|
} else if ("LOBBY_CREATED".equalsIgnoreCase(info.event())) {
|
||||||
|
handleLobbyCreated(lid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startPeriodicRefresh(long initialDelay, long period) {
|
||||||
|
scheduler.scheduleAtFixedRate(
|
||||||
|
this::refreshMappings, initialDelay, period, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshMappings() {
|
||||||
|
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||||
|
|
||||||
|
// Always attempt to sync with server-side lobby list so clients that start
|
||||||
|
// after a lobby was created will discover it. This keeps the local mapping
|
||||||
|
// in sync with the server without requiring a restart.
|
||||||
|
List<LobbyClient.LobbyInfo> serverLobbies = null;
|
||||||
|
java.util.Set<Integer> serverIds = new java.util.HashSet<>();
|
||||||
|
try {
|
||||||
|
serverLobbies = lobbyClient.getLobbyList();
|
||||||
|
if (serverLobbies != null) {
|
||||||
|
for (var li : serverLobbies) {
|
||||||
|
serverIds.add(li.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reconcileWithServerLobbies(serverLobbies);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LOGGER.debug("Could not fetch lobby list: {}", ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.isEmpty()) {
|
||||||
|
// mapping may still be empty after reconciliation
|
||||||
|
if (translationManager.getButtonIdToLobbyId().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mapping = translationManager.getButtonIdToLobbyId();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map.Entry<Integer, Integer>> entries = new ArrayList<>(mapping.entrySet());
|
||||||
|
|
||||||
|
for (Map.Entry<Integer, Integer> e : entries) {
|
||||||
|
int buttonId = e.getKey();
|
||||||
|
Integer lobbyIdObj = e.getValue();
|
||||||
|
if (lobbyIdObj == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int lobbyId = lobbyIdObj.intValue();
|
||||||
|
|
||||||
|
CompletableFuture.supplyAsync(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
return lobbyClient.fetchLobbyStatusString(lobbyId);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LOGGER.info("Lobby {} missing: {}", lobbyId, ex.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
executor)
|
||||||
|
.thenAccept(
|
||||||
|
status -> {
|
||||||
|
// Do not remove a mapping just because the status fetch
|
||||||
|
// temporarily returned null. Only remove mappings when
|
||||||
|
// the server no longer lists the lobby (reconcileWithServerLobbies
|
||||||
|
// handles removals based on authoritative server list).
|
||||||
|
if (status == null) {
|
||||||
|
if (!serverIds.contains(lobbyId)) {
|
||||||
|
translationManager.removeLobbyButton(buttonId);
|
||||||
|
javafx.application.Platform.runLater(
|
||||||
|
this::renderLobbyButtons);
|
||||||
|
} else {
|
||||||
|
// treat as created/running later; keep mapping
|
||||||
|
LOGGER.debug(
|
||||||
|
"Missing status for lobby {} - keeping mapping",
|
||||||
|
lobbyId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reconcileWithServerLobbies(List<LobbyClient.LobbyInfo> serverLobbies) {
|
||||||
|
if (serverLobbies == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||||
|
|
||||||
|
// Build set of lobby ids returned by server
|
||||||
|
java.util.Set<Integer> serverIds = new java.util.HashSet<>();
|
||||||
|
for (var li : serverLobbies) {
|
||||||
|
serverIds.add(li.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.debug("Reconciling mappings. serverIds={} mapping={}", serverIds, mapping);
|
||||||
|
|
||||||
|
// Remove mappings for lobbies that no longer exist
|
||||||
|
java.util.List<Integer> toRemove = new java.util.ArrayList<>();
|
||||||
|
for (var e : mapping.entrySet()) {
|
||||||
|
Integer lid = e.getValue();
|
||||||
|
if (lid == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!serverIds.contains(lid)) {
|
||||||
|
toRemove.add(e.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Integer bid : toRemove) {
|
||||||
|
LOGGER.info(
|
||||||
|
"Reconciling: removing mapping for button {} (server no longer lists lobby)",
|
||||||
|
bid);
|
||||||
|
translationManager.removeLobbyButton(bid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add server lobbies that are not yet mapped
|
||||||
|
for (var li : serverLobbies) {
|
||||||
|
if (mapping.containsValue(li.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// find next free button id (1..MAX_BUTTONS)
|
||||||
|
for (int candidate = 1; candidate <= MAX_BUTTONS; candidate++) {
|
||||||
|
if (!mapping.containsKey(candidate)) {
|
||||||
|
try {
|
||||||
|
translationManager.addLobbyButton(candidate, li.id);
|
||||||
|
LOGGER.info(
|
||||||
|
"Reconciling: added mapping button {} -> lobby {}",
|
||||||
|
candidate,
|
||||||
|
li.id);
|
||||||
|
break;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// grid full? try next candidate
|
||||||
|
LOGGER.debug(
|
||||||
|
"Could not add mapping for lobby {}: {}", li.id, ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!toRemove.isEmpty() || !serverLobbies.isEmpty()) {
|
||||||
|
javafx.application.Platform.runLater(this::renderLobbyButtons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void renderLobbyButtons() {
|
||||||
|
gridPane.getChildren().clear();
|
||||||
|
|
||||||
|
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||||
|
|
||||||
|
if (mapping.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Integer> buttonIds = new ArrayList<>(mapping.keySet());
|
||||||
|
Collections.sort(buttonIds);
|
||||||
|
|
||||||
|
for (int index = 0; index < buttonIds.size(); index++) {
|
||||||
|
Integer buttonId = buttonIds.get(index);
|
||||||
|
Integer lobbyIdObj = mapping.get(buttonId);
|
||||||
|
if (lobbyIdObj == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int lobbyId = lobbyIdObj.intValue();
|
||||||
|
|
||||||
|
Button btn = createLobbyButton(buttonId, lobbyId);
|
||||||
|
|
||||||
|
int row = index / COLS;
|
||||||
|
int col = index % COLS;
|
||||||
|
|
||||||
|
gridPane.add(btn, col, row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button createLobbyButton(int buttonId, int lobbyId) {
|
||||||
Button btn = new Button();
|
Button btn = new Button();
|
||||||
btn.setId("lobbyBtn-" + buttonId);
|
btn.setId("lobbyBtn-" + buttonId);
|
||||||
ImageView imageView =
|
|
||||||
new ImageView(new Image(getClass().getResourceAsStream(BUTTON_IMAGE_PATH)));
|
ImageView imageView = new ImageView(safeLoadImage(BUTTON_FALLBACK_IMAGE));
|
||||||
|
|
||||||
imageView.setPreserveRatio(true);
|
imageView.setPreserveRatio(true);
|
||||||
// Dynamische Breite: Bindung an die Zellengröße
|
|
||||||
imageView
|
imageView
|
||||||
.fitWidthProperty()
|
.fitWidthProperty()
|
||||||
.bind(gridPane.widthProperty().divide(COLS).subtract(BUTTON_WIDTH_MARGIN));
|
.bind(gridPane.widthProperty().divide(COLS).subtract(BTN_WIDTH_MRG));
|
||||||
imageView.setSmooth(true);
|
|
||||||
btn.setGraphic(imageView);
|
btn.setGraphic(imageView);
|
||||||
btn.setMaxWidth(Double.MAX_VALUE);
|
btn.setMaxWidth(Double.MAX_VALUE);
|
||||||
btn.setMaxHeight(Double.MAX_VALUE);
|
btn.setMaxHeight(Double.MAX_VALUE);
|
||||||
btn.setMinWidth(BUTTON_MIN_SIZE);
|
btn.setMinWidth(BUTTON_MIN_SIZE);
|
||||||
btn.setMinHeight(BUTTON_MIN_SIZE);
|
btn.setMinHeight(BUTTON_MIN_SIZE);
|
||||||
|
|
||||||
GridPane.setHgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
GridPane.setHgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
||||||
GridPane.setVgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
GridPane.setVgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
||||||
|
|
||||||
btn.setOnAction(
|
btn.setOnAction(
|
||||||
e -> {
|
e -> {
|
||||||
Integer lobbyId = translationManager.getLobbyIdForButton(buttonId);
|
Integer targetLobbyId = translationManager.getLobbyIdForButton(buttonId);
|
||||||
if (lobbyId != null) {
|
|
||||||
joinLobby(lobbyId);
|
if (targetLobbyId != null) {
|
||||||
|
joinLobby(targetLobbyId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
int row = index / COLS;
|
|
||||||
int col = index % COLS;
|
loadLobbyImageAsync(btn, buttonId, lobbyId);
|
||||||
gridPane.add(btn, col, row);
|
|
||||||
index++;
|
return btn;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void loadLobbyImageAsync(Button btn, int buttonId, int lobbyId) {
|
||||||
* Placeholder for lobby creation logic. Returns a generated lobbyId.
|
|
||||||
*
|
CompletableFuture.supplyAsync(() -> lobbyClient.fetchLobbyStatusString(lobbyId), executor)
|
||||||
* @return The generated lobbyId
|
.thenAccept(
|
||||||
*/
|
statusStr -> {
|
||||||
public int createLobby() {
|
LobbyStatus status = parseLobbyStatus(statusStr);
|
||||||
// TODO: Replace with actual lobby creation logic
|
|
||||||
int lobbyId = (int) (Math.random() * MAX_RANDOM_LOBBY_ID + 1);
|
String path =
|
||||||
LOGGER.info("Lobby created: {}", lobbyId);
|
getImagePathForLobby(
|
||||||
return lobbyId;
|
lobbyId, status == null ? LobbyStatus.CREATED : status);
|
||||||
}
|
|
||||||
|
Image img = safeLoadImage(path);
|
||||||
|
|
||||||
|
javafx.application.Platform.runLater(
|
||||||
|
() -> {
|
||||||
|
ImageView iv = new ImageView(img);
|
||||||
|
iv.setPreserveRatio(true);
|
||||||
|
iv.fitWidthProperty()
|
||||||
|
.bind(
|
||||||
|
gridPane.widthProperty()
|
||||||
|
.divide(COLS)
|
||||||
|
.subtract(BTN_WIDTH_MRG));
|
||||||
|
btn.setGraphic(iv);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum LobbyStatus {
|
||||||
|
CREATED,
|
||||||
|
RUNNING
|
||||||
|
}
|
||||||
|
|
||||||
|
private LobbyStatus parseLobbyStatus(String statusStr) {
|
||||||
|
if (statusStr == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return LobbyStatus.valueOf(statusStr.trim().toUpperCase());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getImagePathForLobby(int lobbyId, LobbyStatus status) {
|
||||||
|
|
||||||
|
String statusStr = status == LobbyStatus.CREATED ? "created" : "running";
|
||||||
|
|
||||||
|
return String.format(BUTTON_IMAGE_TEMPLATE, lobbyId, statusStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Image safeLoadImage(String path) {
|
||||||
|
Image cached = imageCache.get(path);
|
||||||
|
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
java.io.InputStream is = getClass().getResourceAsStream(path);
|
||||||
|
|
||||||
|
if (is == null) {
|
||||||
|
is = getClass().getResourceAsStream(BUTTON_FALLBACK_IMAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
Image loaded = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (is != null) {
|
||||||
|
loaded = new Image(is);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Image load failed: {}", path, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loaded != null) {
|
||||||
|
imageCache.put(path, loaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateLobbyButtonImages() {
|
||||||
|
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||||
|
|
||||||
|
if (mapping.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Integer buttonId : mapping.keySet()) {
|
||||||
|
Integer lobbyIdObj = mapping.get(buttonId);
|
||||||
|
if (lobbyIdObj == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int lobbyId = lobbyIdObj.intValue();
|
||||||
|
|
||||||
|
CompletableFuture.supplyAsync(
|
||||||
|
() -> {
|
||||||
|
String statusStr = lobbyClient.fetchLobbyStatusString(lobbyId);
|
||||||
|
|
||||||
|
LobbyStatus status = parseLobbyStatus(statusStr);
|
||||||
|
|
||||||
|
return status == null ? LobbyStatus.CREATED : status;
|
||||||
|
},
|
||||||
|
executor)
|
||||||
|
.thenAccept(
|
||||||
|
status -> {
|
||||||
|
String path = getImagePathForLobby(lobbyId, status);
|
||||||
|
|
||||||
|
javafx.application.Platform.runLater(
|
||||||
|
() -> {
|
||||||
|
for (Node node : gridPane.getChildren()) {
|
||||||
|
|
||||||
|
boolean isButton = node instanceof Button;
|
||||||
|
boolean idMatches =
|
||||||
|
("lobbyBtn-" + buttonId)
|
||||||
|
.equals(node.getId());
|
||||||
|
|
||||||
|
if (isButton && idMatches) {
|
||||||
|
Button btn = (Button) node;
|
||||||
|
|
||||||
|
ImageView iv =
|
||||||
|
new ImageView(safeLoadImage(path));
|
||||||
|
|
||||||
|
iv.setPreserveRatio(true);
|
||||||
|
iv.fitWidthProperty()
|
||||||
|
.bind(
|
||||||
|
gridPane.widthProperty()
|
||||||
|
.divide(COLS)
|
||||||
|
.subtract(
|
||||||
|
BTN_WIDTH_MRG));
|
||||||
|
|
||||||
|
btn.setGraphic(iv);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int createLobby() {
|
||||||
|
try {
|
||||||
|
int lobbyId = lobbyClient.createLobby();
|
||||||
|
|
||||||
|
if (lobbyId <= 0) {
|
||||||
|
throw new RuntimeException("Invalid lobby id: " + lobbyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lobbyId;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Create lobby failed: {}", e.getMessage());
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void joinLobby(int lobbyId) {
|
||||||
|
try {
|
||||||
|
lobbyClient.joinLobby(lobbyId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Join failed: {}", e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Placeholder for joining a lobby.
|
|
||||||
*
|
|
||||||
* @param lobbyId The lobbyId to join
|
|
||||||
*/
|
|
||||||
public void joinLobby(int lobbyId) {
|
|
||||||
// Game-UI starten und Lobby-UI schließen
|
|
||||||
LOGGER.info("Joining lobby: {}", lobbyId);
|
|
||||||
javafx.application.Platform.runLater(
|
javafx.application.Platform.runLater(
|
||||||
() -> {
|
() -> {
|
||||||
// Lobby-Stage schließen
|
|
||||||
javafx.stage.Stage currentStage =
|
javafx.stage.Stage currentStage =
|
||||||
(javafx.stage.Stage) gridPane.getScene().getWindow();
|
(javafx.stage.Stage) gridPane.getScene().getWindow();
|
||||||
currentStage.close();
|
|
||||||
// Game-UI starten
|
currentStage.hide();
|
||||||
|
|
||||||
|
javafx.stage.Stage gameStage = new javafx.stage.Stage();
|
||||||
|
|
||||||
|
gameStage.setOnHidden(
|
||||||
|
ev -> {
|
||||||
|
currentStage.show();
|
||||||
|
refreshMappings();
|
||||||
|
updateLobbyButtonImages();
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
var cs = lobbyClient.getClientService();
|
||||||
|
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI
|
||||||
|
.setClientService(cs);
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI.setLobbyId(
|
||||||
|
lobbyId);
|
||||||
|
|
||||||
|
String username =
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.client.ClientApp
|
||||||
|
.getSharedUsername();
|
||||||
|
if (username == null || username.isBlank()) {
|
||||||
|
username =
|
||||||
|
"Guest-"
|
||||||
|
+ java.util
|
||||||
|
.UUID
|
||||||
|
.randomUUID()
|
||||||
|
.toString()
|
||||||
|
.substring(0, GUEST_ID_LENGTH);
|
||||||
|
}
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI.setUsername(
|
||||||
|
username);
|
||||||
|
|
||||||
new ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI()
|
new ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI()
|
||||||
.start(new javafx.stage.Stage());
|
.start(gameStage);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.error("Fehler beim Starten der Game-UI: {}", e.getMessage());
|
LOGGER.error("Game UI failed: {}", e.getMessage());
|
||||||
|
currentStage.show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public GridPane getGridPane() {
|
||||||
* Getter for the GridPane.
|
|
||||||
*
|
|
||||||
* @return The GridPane for the button grid
|
|
||||||
*/
|
|
||||||
public javafx.scene.layout.GridPane getGridPane() {
|
|
||||||
return gridPane;
|
return gridPane;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LobbyClient getLobbyClient() {
|
||||||
|
return lobbyClient;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ package ch.unibas.dmi.dbis.cs108.casono.server;
|
|||||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameHandler;
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameHandler;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameParser;
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameParser;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameRequest;
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameRequest;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountRequest;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageRequest;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersHandler;
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersHandler;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersParser;
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersParser;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersRequest;
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersRequest;
|
||||||
@@ -15,6 +21,10 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.logout.LogoutRequest;
|
|||||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingHandler;
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingHandler;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingParser;
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingParser;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingRequest;
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingRequest;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageRequest;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob;
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager;
|
||||||
@@ -23,7 +33,11 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandR
|
|||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionDisconnectJob;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionDisconnectJob;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
@@ -40,7 +54,10 @@ public class ServerApp {
|
|||||||
private static final int USER_CLEANUP_JOB_RECONNECT_THRESHOLD = 10;
|
private static final int USER_CLEANUP_JOB_RECONNECT_THRESHOLD = 10;
|
||||||
private static final int SESSION_DISCONNECT_JOB_DELAY = 0;
|
private static final int SESSION_DISCONNECT_JOB_DELAY = 0;
|
||||||
private static final int SESSION_DISCONNECT_JOB_PERIOD = 2;
|
private static final int SESSION_DISCONNECT_JOB_PERIOD = 2;
|
||||||
private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 5;
|
private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 30;
|
||||||
|
private static final int LOBBY_EXPIRY_SECONDS = 30;
|
||||||
|
private static final int LOBBY_CLEANUP_INITIAL_DELAY_SECONDS = 5;
|
||||||
|
private static final int LOBBY_CLEANUP_PERIOD_SECONDS = 5;
|
||||||
|
|
||||||
public static void start(String arg) {
|
public static void start(String arg) {
|
||||||
int port = Integer.parseInt(arg);
|
int port = Integer.parseInt(arg);
|
||||||
@@ -76,12 +93,56 @@ public class ServerApp {
|
|||||||
SESSION_DISCONNECT_JOB_PERIOD,
|
SESSION_DISCONNECT_JOB_PERIOD,
|
||||||
TimeUnit.SECONDS);
|
TimeUnit.SECONDS);
|
||||||
|
|
||||||
registerCommands(dispatcher, router, responseDispatcher, userRegistry);
|
LobbyManager lobbyManager = new LobbyManager();
|
||||||
|
registerCommands(
|
||||||
|
dispatcher,
|
||||||
|
router,
|
||||||
|
responseDispatcher,
|
||||||
|
userRegistry,
|
||||||
|
new CommandContext(lobbyManager, sessionManager));
|
||||||
|
|
||||||
|
// Periodic cleanup: remove empty lobbies older than 30s and notify affected
|
||||||
|
// users
|
||||||
|
scheduler.scheduleAtFixedRate(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
var expired =
|
||||||
|
lobbyManager.findEmptyLobbiesOlderThan(
|
||||||
|
Duration.ofSeconds(LOBBY_EXPIRY_SECONDS));
|
||||||
|
for (var lid : expired) {
|
||||||
|
// remove lobby from manager first
|
||||||
|
lobbyManager.removeLobby(lid);
|
||||||
|
|
||||||
|
// broadcast LOBBY_CLOSED event to all connected sessions
|
||||||
|
// (requestId=0)
|
||||||
|
for (Session s : sessionManager.getAllSessions()) {
|
||||||
|
RequestContext ctx = new RequestContext(s.getId(), 0);
|
||||||
|
SuccessResponse ev =
|
||||||
|
new SuccessResponse(
|
||||||
|
ctx,
|
||||||
|
ResponseBody.builder()
|
||||||
|
.param("EVENT", "LOBBY_CLOSED")
|
||||||
|
.param("LOBBY_ID", lid.value())
|
||||||
|
.build()) {};
|
||||||
|
|
||||||
|
responseDispatcher.dispatch(ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Lobby expiry job failed", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LOBBY_CLEANUP_INITIAL_DELAY_SECONDS,
|
||||||
|
LOBBY_CLEANUP_PERIOD_SECONDS,
|
||||||
|
TimeUnit.SECONDS);
|
||||||
|
|
||||||
NetworkManager networkManager = new NetworkManager(port, sessionManager, router);
|
NetworkManager networkManager = new NetworkManager(port, sessionManager, router);
|
||||||
networkManager.start();
|
networkManager.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static record CommandContext(
|
||||||
|
LobbyManager lobbyManager, SessionManager sessionManager) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers command parsers and handlers.
|
* Registers command parsers and handlers.
|
||||||
*
|
*
|
||||||
@@ -93,7 +154,8 @@ public class ServerApp {
|
|||||||
CommandParserDispatcher parserDispatcher,
|
CommandParserDispatcher parserDispatcher,
|
||||||
CommandRouter commandRouter,
|
CommandRouter commandRouter,
|
||||||
ResponseDispatcher responseDispatcher,
|
ResponseDispatcher responseDispatcher,
|
||||||
UserRegistry userRegistry) {
|
UserRegistry userRegistry,
|
||||||
|
CommandContext context) {
|
||||||
parserDispatcher.register("PING", new PingParser());
|
parserDispatcher.register("PING", new PingParser());
|
||||||
commandRouter.register(PingRequest.class, new PingHandler(responseDispatcher));
|
commandRouter.register(PingRequest.class, new PingHandler(responseDispatcher));
|
||||||
|
|
||||||
@@ -110,8 +172,172 @@ public class ServerApp {
|
|||||||
commandRouter.register(
|
commandRouter.register(
|
||||||
LogoutRequest.class, new LogoutHandler(responseDispatcher, userRegistry));
|
LogoutRequest.class, new LogoutHandler(responseDispatcher, userRegistry));
|
||||||
|
|
||||||
|
parserDispatcher.register("SEND_MESSAGE", new SendMessageParser());
|
||||||
|
commandRouter.register(
|
||||||
|
SendMessageRequest.class,
|
||||||
|
new SendMessageHandler(responseDispatcher, userRegistry, context.lobbyManager()));
|
||||||
|
|
||||||
|
parserDispatcher.register("GET_MESSAGE_COUNT", new GetMessageCountParser());
|
||||||
|
commandRouter.register(
|
||||||
|
GetMessageCountRequest.class,
|
||||||
|
new GetMessageCountHandler(responseDispatcher, userRegistry));
|
||||||
|
|
||||||
|
parserDispatcher.register("GET_NEXT_MESSAGE", new GetNextMessageParser());
|
||||||
|
commandRouter.register(
|
||||||
|
GetNextMessageRequest.class,
|
||||||
|
new GetNextMessageHandler(responseDispatcher, userRegistry));
|
||||||
|
|
||||||
parserDispatcher.register("LIST_USERS", new ListUsersParser());
|
parserDispatcher.register("LIST_USERS", new ListUsersParser());
|
||||||
commandRouter.register(
|
commandRouter.register(
|
||||||
ListUsersRequest.class, new ListUsersHandler(responseDispatcher, userRegistry));
|
ListUsersRequest.class, new ListUsersHandler(responseDispatcher, userRegistry));
|
||||||
|
|
||||||
|
// GET_LOBBY_LIST registration
|
||||||
|
parserDispatcher.register(
|
||||||
|
"GET_LOBBY_LIST",
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list
|
||||||
|
.GetLobbyListParser());
|
||||||
|
commandRouter.register(
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list
|
||||||
|
.GetLobbyListRequest.class,
|
||||||
|
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
|
||||||
|
.get_lobby_list.GetLobbyListRequest>)
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list
|
||||||
|
.GetLobbyListHandler(responseDispatcher, context.lobbyManager()));
|
||||||
|
|
||||||
|
// GET_GAME_STATE registration
|
||||||
|
parserDispatcher.register(
|
||||||
|
"GET_GAME_STATE",
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state
|
||||||
|
.GetGameStateParser());
|
||||||
|
commandRouter.register(
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state
|
||||||
|
.GetGameStateRequest.class,
|
||||||
|
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game
|
||||||
|
.get_game_state.GetGameStateRequest>)
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state
|
||||||
|
.GetGameStateHandler(
|
||||||
|
responseDispatcher, context.lobbyManager(), userRegistry));
|
||||||
|
|
||||||
|
// BET registration
|
||||||
|
parserDispatcher.register(
|
||||||
|
"BET",
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetParser());
|
||||||
|
commandRouter.register(
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetRequest.class,
|
||||||
|
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet
|
||||||
|
.PlayerBetRequest>)
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet
|
||||||
|
.PlayerBetHandler(
|
||||||
|
responseDispatcher, userRegistry, context.lobbyManager()));
|
||||||
|
|
||||||
|
// RAISE registration
|
||||||
|
parserDispatcher.register(
|
||||||
|
"RAISE",
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise
|
||||||
|
.PlayerRaiseParser());
|
||||||
|
commandRouter.register(
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseRequest
|
||||||
|
.class,
|
||||||
|
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise
|
||||||
|
.PlayerRaiseRequest>)
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise
|
||||||
|
.PlayerRaiseHandler(
|
||||||
|
responseDispatcher, userRegistry, context.lobbyManager()));
|
||||||
|
|
||||||
|
// CALL registration
|
||||||
|
parserDispatcher.register(
|
||||||
|
"CALL",
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call
|
||||||
|
.PlayerCallParser());
|
||||||
|
commandRouter.register(
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call.PlayerCallRequest
|
||||||
|
.class,
|
||||||
|
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call
|
||||||
|
.PlayerCallRequest>)
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call
|
||||||
|
.PlayerCallHandler(
|
||||||
|
responseDispatcher, userRegistry, context.lobbyManager()));
|
||||||
|
|
||||||
|
// FOLD registration
|
||||||
|
parserDispatcher.register(
|
||||||
|
"FOLD",
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold
|
||||||
|
.PlayerFoldParser());
|
||||||
|
commandRouter.register(
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold.PlayerFoldRequest
|
||||||
|
.class,
|
||||||
|
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold
|
||||||
|
.PlayerFoldRequest>)
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold
|
||||||
|
.PlayerFoldHandler(
|
||||||
|
responseDispatcher, userRegistry, context.lobbyManager()));
|
||||||
|
|
||||||
|
// GET_LOBBY_STATUS registration
|
||||||
|
parserDispatcher.register(
|
||||||
|
"GET_LOBBY_STATUS",
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status
|
||||||
|
.GetLobbyStatusParser());
|
||||||
|
commandRouter.register(
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status
|
||||||
|
.GetLobbyStatusRequest.class,
|
||||||
|
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
|
||||||
|
.get_lobby_status.GetLobbyStatusRequest>)
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
|
||||||
|
.get_lobby_status.GetLobbyStatusHandler(
|
||||||
|
responseDispatcher, context.lobbyManager(), userRegistry));
|
||||||
|
|
||||||
|
// CREATE_LOBBY registration
|
||||||
|
parserDispatcher.register(
|
||||||
|
"CREATE_LOBBY",
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby
|
||||||
|
.CreateLobbyParser());
|
||||||
|
commandRouter.register(
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby
|
||||||
|
.CreateLobbyRequest.class,
|
||||||
|
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
|
||||||
|
.create_lobby.CreateLobbyRequest>)
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby
|
||||||
|
.CreateLobbyHandler(
|
||||||
|
responseDispatcher,
|
||||||
|
context.lobbyManager(),
|
||||||
|
context.sessionManager()));
|
||||||
|
|
||||||
|
// JOIN_LOBBY registration
|
||||||
|
parserDispatcher.register(
|
||||||
|
"JOIN_LOBBY",
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||||
|
.JoinLobbyParser());
|
||||||
|
commandRouter.register(
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||||
|
.JoinLobbyRequest.class,
|
||||||
|
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||||
|
.JoinLobbyRequest>)
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||||
|
.JoinLobbyHandler(
|
||||||
|
responseDispatcher, context.lobbyManager(), userRegistry));
|
||||||
|
|
||||||
|
// START_GAME registration
|
||||||
|
parserDispatcher.register(
|
||||||
|
"START_GAME",
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
|
||||||
|
.StartGameParser());
|
||||||
|
commandRouter.register(
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
|
||||||
|
.StartGameRequest.class,
|
||||||
|
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
|
||||||
|
.StartGameRequest>)
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
|
||||||
|
.StartGameHandler(
|
||||||
|
responseDispatcher, context.lobbyManager(), userRegistry));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.add_player;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/** Handler for the ADD_PLAYER command. */
|
||||||
|
public class AddPlayerHandler extends CommandHandler<AddPlayerRequest> {
|
||||||
|
private final UserRegistry userRegistry;
|
||||||
|
|
||||||
|
public AddPlayerHandler(UserRegistry userRegistry, ResponseDispatcher responseDispatcher) {
|
||||||
|
super(responseDispatcher);
|
||||||
|
this.userRegistry = userRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(AddPlayerRequest request) {
|
||||||
|
Optional<User> created =
|
||||||
|
userRegistry.registerIfAvailable(
|
||||||
|
request.getName(), request.getContext().sessionId());
|
||||||
|
if (created.isEmpty()) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(request.getContext(), "NAME_TAKEN", "Name already taken"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.add_player;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||||
|
|
||||||
|
/** Parser for the ADD_PLAYER command. */
|
||||||
|
public class AddPlayerParser implements CommandParser<AddPlayerRequest> {
|
||||||
|
@Override
|
||||||
|
public AddPlayerRequest parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
RequestParameterAccessor accessor =
|
||||||
|
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||||
|
|
||||||
|
String name = accessor.require("NAME");
|
||||||
|
|
||||||
|
int chips = accessor.require("CHIPS", Integer::parseInt);
|
||||||
|
|
||||||
|
return new AddPlayerRequest(primitiveRequest.context(), name, chips);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.add_player;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
|
||||||
|
/** Request data for the ADD_PLAYER command. */
|
||||||
|
public class AddPlayerRequest extends Request {
|
||||||
|
private final String name;
|
||||||
|
private final int chips;
|
||||||
|
|
||||||
|
public AddPlayerRequest(RequestContext context, String name, int chips) {
|
||||||
|
super(context);
|
||||||
|
this.name = name;
|
||||||
|
this.chips = chips;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getChips() {
|
||||||
|
return chips;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the `BET` command.
|
||||||
|
*
|
||||||
|
* <p>This handler validates the request (amount >= 0), resolves the user and target lobby (by
|
||||||
|
* `GAME_ID` when provided or by session username otherwise), checks that a game is running and then
|
||||||
|
* forwards the action to the {@code GameController}.
|
||||||
|
*
|
||||||
|
* <p>On failure the handler dispatches an {@link ErrorResponse} with an appropriate error code
|
||||||
|
* (e.g. {@code INVALID_AMOUNT}, {@code NOT_IN_LOBBY}, {@code LOBBY_NOT_FOUND}, {@code
|
||||||
|
* GAME_NOT_STARTED}, {@code GAME_ACTION_FAILED}). On success it dispatches an {@link OkResponse}.
|
||||||
|
*/
|
||||||
|
public class PlayerBetHandler extends CommandHandler<PlayerBetRequest> {
|
||||||
|
private final UserRegistry userRegistry;
|
||||||
|
private final LobbyManager lobbyManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code PlayerBetHandler}.
|
||||||
|
*
|
||||||
|
* @param responseDispatcher dispatcher used to send responses back to the client
|
||||||
|
* @param userRegistry registry to resolve users from session ids
|
||||||
|
* @param lobbyManager manager used to lookup lobbies and their game controllers
|
||||||
|
*/
|
||||||
|
public PlayerBetHandler(
|
||||||
|
ResponseDispatcher responseDispatcher,
|
||||||
|
UserRegistry userRegistry,
|
||||||
|
LobbyManager lobbyManager) {
|
||||||
|
super(responseDispatcher);
|
||||||
|
this.userRegistry = userRegistry;
|
||||||
|
this.lobbyManager = lobbyManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the bet request: validate parameters, resolve lobby and game, and forward the bet
|
||||||
|
* action to the game controller. Sends an {@link ErrorResponse} on failure or an {@link
|
||||||
|
* OkResponse} on success.
|
||||||
|
*
|
||||||
|
* @param request the parsed {@link PlayerBetRequest}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void execute(PlayerBetRequest request) {
|
||||||
|
if (request.getAmount() < 0) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "INVALID_AMOUNT", "Amount must be non-negative"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
|
||||||
|
userRegistry.getBySessionId(request.getSessionId());
|
||||||
|
if (opt.isEmpty()) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String username = opt.get().getName();
|
||||||
|
|
||||||
|
Integer gameId = request.getGameId();
|
||||||
|
var lobby =
|
||||||
|
(gameId != null)
|
||||||
|
? lobbyManager.getLobby(LobbyId.of(gameId))
|
||||||
|
: lobbyManager.getLobbyByUsername(username);
|
||||||
|
|
||||||
|
if (lobby == null) {
|
||||||
|
if (gameId != null) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||||
|
} else {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameController game = lobby.getGameController();
|
||||||
|
if (game == null) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "GAME_NOT_STARTED", "Game not started"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
game.playerBet(PlayerId.of(username), request.getAmount());
|
||||||
|
} catch (Exception e) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(request.getContext(), "GAME_ACTION_FAILED", e.getMessage()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser for the `BET` command.
|
||||||
|
*
|
||||||
|
* <p>Parses the request parameters and builds a {@link PlayerBetRequest}. Expected parameters:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>`GAME_ID` (optional) - numeric id of the lobby/game to target
|
||||||
|
* <li>`AMOUNT` (required) - amount to bet
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class PlayerBetParser implements CommandParser<PlayerBetRequest> {
|
||||||
|
/**
|
||||||
|
* Parse a primitive request into a {@link PlayerBetRequest}.
|
||||||
|
*
|
||||||
|
* @param primitiveRequest the raw parsed request containing parameters and context
|
||||||
|
* @return a {@link PlayerBetRequest} with the parsed values (gameId may be null)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public PlayerBetRequest parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
RequestParameterAccessor accessor =
|
||||||
|
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||||
|
|
||||||
|
Integer gameId = null;
|
||||||
|
try {
|
||||||
|
gameId = accessor.optional("GAME_ID", null, Integer::parseInt);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// parse handled by framework
|
||||||
|
}
|
||||||
|
|
||||||
|
int amount = accessor.require("AMOUNT", Integer::parseInt);
|
||||||
|
|
||||||
|
return new PlayerBetRequest(primitiveRequest.context(), gameId, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request for the `BET` command.
|
||||||
|
*
|
||||||
|
* <p>Contains the optional target `gameId` and the `amount` the player wants to bet.
|
||||||
|
*/
|
||||||
|
public class PlayerBetRequest extends Request {
|
||||||
|
private final Integer gameId;
|
||||||
|
private final int amount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code PlayerBetRequest}.
|
||||||
|
*
|
||||||
|
* @param context the request context
|
||||||
|
* @param gameId optional game id of the targeted lobby (may be {@code null})
|
||||||
|
* @param amount the bet amount (non-negative)
|
||||||
|
*/
|
||||||
|
public PlayerBetRequest(RequestContext context, Integer gameId, int amount) {
|
||||||
|
super(context);
|
||||||
|
this.gameId = gameId;
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the optional target game id.
|
||||||
|
*
|
||||||
|
* @return the game id or {@code null} if not provided
|
||||||
|
*/
|
||||||
|
public Integer getGameId() {
|
||||||
|
return gameId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the requested bet amount.
|
||||||
|
*
|
||||||
|
* @return the bet amount
|
||||||
|
*/
|
||||||
|
public int getAmount() {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the `CALL` command.
|
||||||
|
*
|
||||||
|
* <p>This handler validates the session, resolves the lobby (by `GAME_ID` when provided or by
|
||||||
|
* session username otherwise), checks for a running game and forwards the call action to the {@code
|
||||||
|
* GameController}.
|
||||||
|
*/
|
||||||
|
public class PlayerCallHandler extends CommandHandler<PlayerCallRequest> {
|
||||||
|
private final UserRegistry userRegistry;
|
||||||
|
private final LobbyManager lobbyManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code PlayerCallHandler}.
|
||||||
|
*
|
||||||
|
* @param responseDispatcher dispatcher used to send responses back to the client
|
||||||
|
* @param userRegistry registry to resolve users from session ids
|
||||||
|
* @param lobbyManager manager used to lookup lobbies and their game controllers
|
||||||
|
*/
|
||||||
|
public PlayerCallHandler(
|
||||||
|
ResponseDispatcher responseDispatcher,
|
||||||
|
UserRegistry userRegistry,
|
||||||
|
LobbyManager lobbyManager) {
|
||||||
|
super(responseDispatcher);
|
||||||
|
this.userRegistry = userRegistry;
|
||||||
|
this.lobbyManager = lobbyManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the call request: resolve lobby by `GAME_ID` if present, otherwise by session user,
|
||||||
|
* verify game is running, and forward the call to the game controller.
|
||||||
|
*
|
||||||
|
* @param request the parsed {@link PlayerCallRequest}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void execute(PlayerCallRequest request) {
|
||||||
|
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
|
||||||
|
userRegistry.getBySessionId(request.getSessionId());
|
||||||
|
if (opt.isEmpty()) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String username = opt.get().getName();
|
||||||
|
|
||||||
|
Integer gameId = request.getGameId();
|
||||||
|
var lobby =
|
||||||
|
(gameId != null)
|
||||||
|
? lobbyManager.getLobby(LobbyId.of(gameId))
|
||||||
|
: lobbyManager.getLobbyByUsername(username);
|
||||||
|
|
||||||
|
if (lobby == null) {
|
||||||
|
if (gameId != null) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||||
|
} else {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameController game = lobby.getGameController();
|
||||||
|
if (game == null) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "GAME_NOT_STARTED", "Game not started"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
game.playerCall(PlayerId.of(username));
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(request.getContext(), "GAME_ACTION_FAILED", e.getMessage()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser for the `CALL` command.
|
||||||
|
*
|
||||||
|
* <p>Accepts an optional `GAME_ID` parameter. If provided, the handler will target the specified
|
||||||
|
* lobby; otherwise the server resolves the lobby by the requesting session's user.
|
||||||
|
*/
|
||||||
|
public class PlayerCallParser implements CommandParser<PlayerCallRequest> {
|
||||||
|
@Override
|
||||||
|
public PlayerCallRequest parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
RequestParameterAccessor accessor =
|
||||||
|
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||||
|
|
||||||
|
Integer gameId = null;
|
||||||
|
try {
|
||||||
|
gameId = accessor.optional("GAME_ID", null, Integer::parseInt);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// parse errors handled by framework
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PlayerCallRequest(primitiveRequest.context(), gameId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request for the `CALL` command.
|
||||||
|
*
|
||||||
|
* <p>Contains an optional `gameId` when the client targets a specific lobby, otherwise the server
|
||||||
|
* will resolve the lobby from the requesting session's user.
|
||||||
|
*/
|
||||||
|
public class PlayerCallRequest extends Request {
|
||||||
|
private final Integer gameId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code PlayerCallRequest}.
|
||||||
|
*
|
||||||
|
* @param context the request context
|
||||||
|
* @param gameId optional game id of the targeted lobby (may be {@code null})
|
||||||
|
*/
|
||||||
|
public PlayerCallRequest(RequestContext context, Integer gameId) {
|
||||||
|
super(context);
|
||||||
|
this.gameId = gameId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the optional target game id.
|
||||||
|
*
|
||||||
|
* @return the game id or {@code null} if not provided
|
||||||
|
*/
|
||||||
|
public Integer getGameId() {
|
||||||
|
return gameId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the `FOLD` command.
|
||||||
|
*
|
||||||
|
* <p>This handler validates the session, resolves the lobby (by `GAME_ID` when provided or by
|
||||||
|
* session username otherwise), checks for a running game and forwards the fold action to the {@code
|
||||||
|
* GameController}.
|
||||||
|
*/
|
||||||
|
public class PlayerFoldHandler extends CommandHandler<PlayerFoldRequest> {
|
||||||
|
private final UserRegistry userRegistry;
|
||||||
|
private final LobbyManager lobbyManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code PlayerFoldHandler}.
|
||||||
|
*
|
||||||
|
* @param responseDispatcher dispatcher used to send responses back to the client
|
||||||
|
* @param userRegistry registry to resolve users from session ids
|
||||||
|
* @param lobbyManager manager used to lookup lobbies and their game controllers
|
||||||
|
*/
|
||||||
|
public PlayerFoldHandler(
|
||||||
|
ResponseDispatcher responseDispatcher,
|
||||||
|
UserRegistry userRegistry,
|
||||||
|
LobbyManager lobbyManager) {
|
||||||
|
super(responseDispatcher);
|
||||||
|
this.userRegistry = userRegistry;
|
||||||
|
this.lobbyManager = lobbyManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the fold request: resolve lobby by `GAME_ID` if present, otherwise by session user,
|
||||||
|
* verify game is running, and forward the fold to the game controller.
|
||||||
|
*
|
||||||
|
* @param request the parsed {@link PlayerFoldRequest}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void execute(PlayerFoldRequest request) {
|
||||||
|
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
|
||||||
|
userRegistry.getBySessionId(request.getSessionId());
|
||||||
|
if (opt.isEmpty()) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String username = opt.get().getName();
|
||||||
|
|
||||||
|
Integer gameId = request.getGameId();
|
||||||
|
var lobby =
|
||||||
|
(gameId != null)
|
||||||
|
? lobbyManager.getLobby(LobbyId.of(gameId))
|
||||||
|
: lobbyManager.getLobbyByUsername(username);
|
||||||
|
|
||||||
|
if (lobby == null) {
|
||||||
|
if (gameId != null) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||||
|
} else {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameController game = lobby.getGameController();
|
||||||
|
if (game == null) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "GAME_NOT_STARTED", "Game not started"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
game.playerFold(PlayerId.of(username));
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(request.getContext(), "GAME_ACTION_FAILED", e.getMessage()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser for the `FOLD` command.
|
||||||
|
*
|
||||||
|
* <p>Accepts an optional `GAME_ID` parameter. If provided, the handler will target the specified
|
||||||
|
* lobby; otherwise the server resolves the lobby by the requesting session's user.
|
||||||
|
*/
|
||||||
|
public class PlayerFoldParser implements CommandParser<PlayerFoldRequest> {
|
||||||
|
@Override
|
||||||
|
public PlayerFoldRequest parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
RequestParameterAccessor accessor =
|
||||||
|
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||||
|
|
||||||
|
Integer gameId = null;
|
||||||
|
try {
|
||||||
|
gameId = accessor.optional("GAME_ID", null, Integer::parseInt);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// parse handled by framework
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PlayerFoldRequest(primitiveRequest.context(), gameId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request for the `FOLD` command.
|
||||||
|
*
|
||||||
|
* <p>Contains an optional `gameId` when the client targets a specific lobby, otherwise the server
|
||||||
|
* will resolve the lobby from the requesting session's user.
|
||||||
|
*/
|
||||||
|
public class PlayerFoldRequest extends Request {
|
||||||
|
private final Integer gameId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code PlayerFoldRequest}.
|
||||||
|
*
|
||||||
|
* @param context the request context
|
||||||
|
* @param gameId optional game id of the targeted lobby (may be {@code null})
|
||||||
|
*/
|
||||||
|
public PlayerFoldRequest(RequestContext context, Integer gameId) {
|
||||||
|
super(context);
|
||||||
|
this.gameId = gameId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the optional target game id.
|
||||||
|
*
|
||||||
|
* @return the game id or {@code null} if not provided
|
||||||
|
*/
|
||||||
|
public Integer getGameId() {
|
||||||
|
return gameId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
|
||||||
|
/** Handler for GET_GAME_STATE: returns pot, phase, community cards and per-player info. */
|
||||||
|
public class GetGameStateHandler extends CommandHandler<GetGameStateRequest> {
|
||||||
|
private final LobbyManager lobbyManager;
|
||||||
|
private final UserRegistry userRegistry;
|
||||||
|
|
||||||
|
public GetGameStateHandler(
|
||||||
|
ResponseDispatcher responseDispatcher,
|
||||||
|
LobbyManager lobbyManager,
|
||||||
|
UserRegistry userRegistry) {
|
||||||
|
super(responseDispatcher);
|
||||||
|
this.lobbyManager = lobbyManager;
|
||||||
|
this.userRegistry = userRegistry;
|
||||||
|
addCheck(new UserLoggedInCheck(userRegistry));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(GetGameStateRequest request) {
|
||||||
|
Integer gameId = request.getGameId();
|
||||||
|
String username = resolveUsername(request);
|
||||||
|
|
||||||
|
Lobby lobby;
|
||||||
|
if (gameId != null) {
|
||||||
|
lobby = lobbyManager.getLobby(LobbyId.of(gameId));
|
||||||
|
if (lobby == null) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (username == null) {
|
||||||
|
var maybeUser = userRegistry.getBySessionId(request.getSessionId());
|
||||||
|
if (maybeUser.isEmpty()) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
User u = maybeUser.get();
|
||||||
|
username = u.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
lobby = lobbyManager.getLobbyByUsername(username);
|
||||||
|
if (lobby == null) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var game = lobby.getGameController();
|
||||||
|
if (game == null) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "GAME_NOT_STARTED", "Game not started"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseDispatcher.dispatch(new GetGameStateResponse(request.getContext(), game, username));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveUsername(GetGameStateRequest request) {
|
||||||
|
String username = request.getUsername();
|
||||||
|
if (username != null && !username.isBlank()) {
|
||||||
|
return username.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return userRegistry
|
||||||
|
.getBySessionId(request.getSessionId())
|
||||||
|
.map(User::getName)
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(name -> !name.isEmpty())
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||||
|
|
||||||
|
public class GetGameStateParser implements CommandParser<GetGameStateRequest> {
|
||||||
|
@Override
|
||||||
|
public GetGameStateRequest parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
RequestParameterAccessor accessor =
|
||||||
|
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||||
|
Integer gameId = accessor.optional("GAME_ID", null, Integer::parseInt);
|
||||||
|
String username = accessor.optional("USERNAME", null);
|
||||||
|
return new GetGameStateRequest(primitiveRequest.context(), username, gameId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request object for `GET_GAME_STATE`.
|
||||||
|
*
|
||||||
|
* <p>Either `USERNAME` or `GAME_ID` may be provided by the client. If both are omitted the handler
|
||||||
|
* will resolve the username from the session.
|
||||||
|
*/
|
||||||
|
public class GetGameStateRequest extends Request {
|
||||||
|
private final String username;
|
||||||
|
private final Integer gameId;
|
||||||
|
|
||||||
|
public GetGameStateRequest(RequestContext context, String username, Integer gameId) {
|
||||||
|
super(context);
|
||||||
|
this.username = username;
|
||||||
|
this.gameId = gameId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the optional username provided in the request. */
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the optional game id provided in the request. */
|
||||||
|
public Integer getGameId() {
|
||||||
|
return gameId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Card;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Rank;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Suit;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.Player;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response carrying a snapshot of the current game state using the project's wire format.
|
||||||
|
*
|
||||||
|
* <p>Wire format (relevant parts):
|
||||||
|
*
|
||||||
|
* <p>+OK PHASE=... POT=... CURRENT_BET=... DEALER=... ACTIVE_PLAYER=... CARD VALUE=... SUIT=... END
|
||||||
|
* PLAYER NAME=... CHIPS=... BET=... STATE=... CARD (only for requesting user) VALUE=... SUIT=...
|
||||||
|
* END END END
|
||||||
|
*
|
||||||
|
* <p>Notes: - Community cards are always emitted at top-level (0..5). If none exist (e.g. PREFLOP),
|
||||||
|
* none are emitted. - Hole cards are only emitted for the requesting player (privacy).
|
||||||
|
*/
|
||||||
|
public class GetGameStateResponse extends SuccessResponse {
|
||||||
|
|
||||||
|
public GetGameStateResponse(
|
||||||
|
RequestContext context, GameController game, String requestingUsername) {
|
||||||
|
super(
|
||||||
|
context,
|
||||||
|
buildBody(
|
||||||
|
Objects.requireNonNull(game, "game must not be null").getState(),
|
||||||
|
requestingUsername));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResponseBody buildBody(GameState state, String requestingUsername) {
|
||||||
|
Objects.requireNonNull(state, "state must not be null");
|
||||||
|
|
||||||
|
ResponseBodyBuilder builder = ResponseBody.builder();
|
||||||
|
|
||||||
|
builder.param("PHASE", state.getPhase() == null ? "UNKNOWN" : state.getPhase().name());
|
||||||
|
builder.param("POT", state.getPot() == null ? 0 : state.getPot().getAmount());
|
||||||
|
builder.param("CURRENT_BET", computeGlobalCurrentBet(state));
|
||||||
|
builder.param("DEALER", state.getDealerIndex());
|
||||||
|
builder.param("ACTIVE_PLAYER", state.getCurrentPlayerIndex());
|
||||||
|
|
||||||
|
appendCommunityCards(builder, state);
|
||||||
|
|
||||||
|
appendPlayers(builder, state, requestingUsername);
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int computeGlobalCurrentBet(GameState state) {
|
||||||
|
int globalCurrentBet = 0;
|
||||||
|
for (Player p : state.getPlayers()) {
|
||||||
|
if (p == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int b = state.getCurrentBet(p.getId());
|
||||||
|
if (b > globalCurrentBet) {
|
||||||
|
globalCurrentBet = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return globalCurrentBet;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendCommunityCards(ResponseBodyBuilder builder, GameState state) {
|
||||||
|
List<Card> community = state.getCommunityCards();
|
||||||
|
|
||||||
|
if (community == null || community.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Card c : community) {
|
||||||
|
if (c == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.block(
|
||||||
|
"CARD",
|
||||||
|
card -> {
|
||||||
|
card.param("VALUE", rankToWire(c.getRank()));
|
||||||
|
card.param("SUIT", suitToWire(c.getSuit()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendPlayers(
|
||||||
|
ResponseBodyBuilder builder, GameState state, String requestingUsername) {
|
||||||
|
String req = (requestingUsername == null) ? null : requestingUsername.trim();
|
||||||
|
|
||||||
|
for (Player p : state.getPlayers()) {
|
||||||
|
if (p == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerId pid = p.getId();
|
||||||
|
String name = (pid == null || pid.value() == null) ? "" : pid.value().trim();
|
||||||
|
|
||||||
|
builder.block(
|
||||||
|
"PLAYER",
|
||||||
|
pblock -> {
|
||||||
|
pblock.param("NAME", name);
|
||||||
|
pblock.param("CHIPS", p.getChips());
|
||||||
|
pblock.param("BET", pid == null ? 0 : state.getCurrentBet(pid));
|
||||||
|
pblock.param("STATE", p.isFolded() ? "FOLDED" : "ACTIVE");
|
||||||
|
|
||||||
|
if (req != null && pid != null && req.equals(name)) {
|
||||||
|
List<Card> hole = state.getHoleCards(pid);
|
||||||
|
|
||||||
|
if (hole != null) {
|
||||||
|
for (Card hc : hole) {
|
||||||
|
if (hc == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
pblock.block(
|
||||||
|
"CARD",
|
||||||
|
cblock -> {
|
||||||
|
cblock.param("VALUE", rankToWire(hc.getRank()));
|
||||||
|
cblock.param("SUIT", suitToWire(hc.getSuit()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String rankToWire(Rank r) {
|
||||||
|
if (r == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (r) {
|
||||||
|
case TWO -> "2";
|
||||||
|
case THREE -> "3";
|
||||||
|
case FOUR -> "4";
|
||||||
|
case FIVE -> "5";
|
||||||
|
case SIX -> "6";
|
||||||
|
case SEVEN -> "7";
|
||||||
|
case EIGHT -> "8";
|
||||||
|
case NINE -> "9";
|
||||||
|
case TEN -> "10";
|
||||||
|
case JACK -> "J";
|
||||||
|
case QUEEN -> "Q";
|
||||||
|
case KING -> "K";
|
||||||
|
case ACE -> "A";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String suitToWire(Suit s) {
|
||||||
|
if (s == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (s) {
|
||||||
|
case HEARTS -> "H";
|
||||||
|
case DIAMONDS -> "D";
|
||||||
|
case CLUBS -> "C";
|
||||||
|
case SPADES -> "S";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the `RAISE` command.
|
||||||
|
*
|
||||||
|
* <p>This handler validates the request (amount >= 0), resolves the user and target lobby (by
|
||||||
|
* `GAME_ID` when provided or by session username otherwise), checks that a game is running and then
|
||||||
|
* forwards the action to the {@code GameController}.
|
||||||
|
*
|
||||||
|
* <p>On failure the handler dispatches an {@link ErrorResponse} with an appropriate error code
|
||||||
|
* (e.g. {@code INVALID_AMOUNT}, {@code NOT_IN_LOBBY}, {@code LOBBY_NOT_FOUND}, {@code
|
||||||
|
* GAME_NOT_STARTED}, {@code GAME_ACTION_FAILED}). On success it dispatches an {@link OkResponse}.
|
||||||
|
*/
|
||||||
|
public class PlayerRaiseHandler extends CommandHandler<PlayerRaiseRequest> {
|
||||||
|
private final UserRegistry userRegistry;
|
||||||
|
private final LobbyManager lobbyManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code PlayerRaiseHandler}.
|
||||||
|
*
|
||||||
|
* @param responseDispatcher dispatcher used to send responses back to the client
|
||||||
|
* @param userRegistry registry to resolve users from session ids
|
||||||
|
* @param lobbyManager manager used to lookup lobbies and their game controllers
|
||||||
|
*/
|
||||||
|
public PlayerRaiseHandler(
|
||||||
|
ResponseDispatcher responseDispatcher,
|
||||||
|
UserRegistry userRegistry,
|
||||||
|
LobbyManager lobbyManager) {
|
||||||
|
super(responseDispatcher);
|
||||||
|
this.userRegistry = userRegistry;
|
||||||
|
this.lobbyManager = lobbyManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the raise request: validate parameters, resolve lobby and game, and forward the raise
|
||||||
|
* action to the game controller. Sends an {@link ErrorResponse} on failure or an {@link
|
||||||
|
* OkResponse} on success.
|
||||||
|
*
|
||||||
|
* @param request the parsed {@link PlayerRaiseRequest}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void execute(PlayerRaiseRequest request) {
|
||||||
|
int amount = request.getAmount();
|
||||||
|
|
||||||
|
if (amount < 0) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "INVALID_AMOUNT", "Amount must be non-negative"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
|
||||||
|
userRegistry.getBySessionId(request.getSessionId());
|
||||||
|
if (opt.isEmpty()) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String username = opt.get().getName();
|
||||||
|
|
||||||
|
Integer gameId = request.getGameId();
|
||||||
|
var lobby =
|
||||||
|
(gameId != null)
|
||||||
|
? lobbyManager.getLobby(LobbyId.of(gameId))
|
||||||
|
: lobbyManager.getLobbyByUsername(username);
|
||||||
|
|
||||||
|
if (lobby == null) {
|
||||||
|
if (gameId != null) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||||
|
} else {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameController game = lobby.getGameController();
|
||||||
|
if (game == null) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "GAME_NOT_STARTED", "Game not started"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
game.playerRaise(PlayerId.of(username), amount);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(request.getContext(), "GAME_ACTION_FAILED", e.getMessage()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser for the `RAISE` command.
|
||||||
|
*
|
||||||
|
* <p>Parses the request parameters and builds a {@link PlayerRaiseRequest}. Expected parameters:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>`GAME_ID` (optional) - numeric id of the lobby/game to target
|
||||||
|
* <li>`AMOUNT` (required) - amount to raise
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class PlayerRaiseParser implements CommandParser<PlayerRaiseRequest> {
|
||||||
|
@Override
|
||||||
|
public PlayerRaiseRequest parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
RequestParameterAccessor accessor =
|
||||||
|
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||||
|
|
||||||
|
Integer gameId = null;
|
||||||
|
try {
|
||||||
|
gameId = accessor.optional("GAME_ID", null, Integer::parseInt);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// parse handled by framework
|
||||||
|
}
|
||||||
|
|
||||||
|
int amount = accessor.require("AMOUNT", Integer::parseInt);
|
||||||
|
|
||||||
|
return new PlayerRaiseRequest(primitiveRequest.context(), gameId, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request for the `RAISE` command.
|
||||||
|
*
|
||||||
|
* <p>Contains the optional target `gameId` and the `amount` the player wants to raise.
|
||||||
|
*/
|
||||||
|
public class PlayerRaiseRequest extends Request {
|
||||||
|
private final Integer gameId;
|
||||||
|
private final int amount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code PlayerRaiseRequest}.
|
||||||
|
*
|
||||||
|
* @param context the request context
|
||||||
|
* @param gameId optional game id of the targeted lobby (may be {@code null})
|
||||||
|
* @param amount the raise amount (non-negative)
|
||||||
|
*/
|
||||||
|
public PlayerRaiseRequest(RequestContext context, Integer gameId, int amount) {
|
||||||
|
super(context);
|
||||||
|
this.gameId = gameId;
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the optional target game id.
|
||||||
|
*
|
||||||
|
* @return the game id or {@code null} if not provided
|
||||||
|
*/
|
||||||
|
public Integer getGameId() {
|
||||||
|
return gameId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the requested raise amount.
|
||||||
|
*
|
||||||
|
* @return the raise amount
|
||||||
|
*/
|
||||||
|
public int getAmount() {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class GetMessageCountHandler extends CommandHandler<GetMessageCountRequest> {
|
||||||
|
private final UserRegistry userRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new GetMessageCountHandler with the necessary response dispatcher and user
|
||||||
|
* registry.
|
||||||
|
*
|
||||||
|
* @param responseDispatcher The dispatcher used to send the count or error back to the client.
|
||||||
|
* @param userRegistry The registry used to identify the user and access their message queue.
|
||||||
|
*/
|
||||||
|
public GetMessageCountHandler(
|
||||||
|
ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
|
||||||
|
super(responseDispatcher);
|
||||||
|
this.userRegistry = userRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a request to retrieve the number of pending messages for a user. It looks up the
|
||||||
|
* user by their session ID; if found, it dispatches a {@link GetMessageCountResponse}
|
||||||
|
* containing the current count. Otherwise, it dispatches an {@link ErrorResponse}.
|
||||||
|
*
|
||||||
|
* @param request The {@link GetMessageCountRequest} containing the session details.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void execute(GetMessageCountRequest request) {
|
||||||
|
Optional<User> user = userRegistry.getBySessionId(request.getSessionId());
|
||||||
|
if (user.isPresent()) {
|
||||||
|
int count = user.get().getMessageCount();
|
||||||
|
GetMessageCountResponse response =
|
||||||
|
new GetMessageCountResponse(request.getContext(), count);
|
||||||
|
responseDispatcher.dispatch(response);
|
||||||
|
} else {
|
||||||
|
ErrorResponse response =
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "", "user could not be identified by SessionId");
|
||||||
|
|
||||||
|
responseDispatcher.dispatch(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||||
|
|
||||||
|
public class GetMessageCountParser implements CommandParser<GetMessageCountRequest> {
|
||||||
|
/**
|
||||||
|
* Parses a raw {@link PrimitiveRequest} into a {@link GetMessageCountRequest}. This method
|
||||||
|
* wraps the request context from the network layer into a structured message count request
|
||||||
|
* object.
|
||||||
|
*
|
||||||
|
* @param primitiveRequest The raw request containing parameters and context from the network.
|
||||||
|
* @return A new {@link GetMessageCountRequest} instance.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public GetMessageCountRequest parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
RequestParameterAccessor accessor =
|
||||||
|
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||||
|
return new GetMessageCountRequest(primitiveRequest.context());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
|
||||||
|
public class GetMessageCountRequest extends Request {
|
||||||
|
/**
|
||||||
|
* Constructs a new GetMessageCountRequest with the specified request context. This request is
|
||||||
|
* used by a client to query the number of pending messages currently waiting in their
|
||||||
|
* server-side queue.
|
||||||
|
*
|
||||||
|
* @param context The {@link RequestContext} associated with this request.
|
||||||
|
*/
|
||||||
|
public GetMessageCountRequest(RequestContext context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
|
||||||
|
|
||||||
|
public class GetMessageCountResponse extends SuccessResponse {
|
||||||
|
/**
|
||||||
|
* Constructs a new GetMessageCountResponse. It creates a response body containing the "COUNT"
|
||||||
|
* parameter and associates it with the original request context.
|
||||||
|
*
|
||||||
|
* @param context The {@link RequestContext} of the original request.
|
||||||
|
* @param count The number of pending messages to be returned to the client.
|
||||||
|
*/
|
||||||
|
public GetMessageCountResponse(RequestContext context, int count) {
|
||||||
|
super(context, new ResponseBodyBuilder().param("COUNT", count).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class GetNextMessageHandler extends CommandHandler<GetNextMessageRequest> {
|
||||||
|
private final UserRegistry userRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new GetNextMessageHandler with the required dispatcher and user registry.
|
||||||
|
*
|
||||||
|
* @param responseDispatcher The dispatcher used to send responses back to clients.
|
||||||
|
* @param userRegistry The registry used to look up users by their session information.
|
||||||
|
*/
|
||||||
|
public GetNextMessageHandler(ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
|
||||||
|
super(responseDispatcher);
|
||||||
|
this.userRegistry = userRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the request to retrieve the next message for a specific user. It identifies the user
|
||||||
|
* via their session ID, dequeues the next available message, and dispatches a {@link
|
||||||
|
* GetNextMessageResponse}. If the user cannot be identified, an {@link ErrorResponse} is sent
|
||||||
|
* instead.
|
||||||
|
*
|
||||||
|
* @param request The {@link GetNextMessageRequest} containing the session and context.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void execute(GetNextMessageRequest request) {
|
||||||
|
Optional<User> user = userRegistry.getBySessionId(request.getSessionId());
|
||||||
|
if (user.isPresent()) {
|
||||||
|
Message msg = user.get().dequeMessage();
|
||||||
|
GetNextMessageResponse response = new GetNextMessageResponse(request.getContext(), msg);
|
||||||
|
responseDispatcher.dispatch(response);
|
||||||
|
} else {
|
||||||
|
ErrorResponse response =
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(),
|
||||||
|
"NO_USER_ASSOCIATED",
|
||||||
|
"user could not be identified by SessionId");
|
||||||
|
responseDispatcher.dispatch(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||||
|
|
||||||
|
public class GetNextMessageParser implements CommandParser<GetNextMessageRequest> {
|
||||||
|
/**
|
||||||
|
* Parses a raw {@link PrimitiveRequest} into a {@link GetNextMessageRequest}. This method
|
||||||
|
* initializes a parameter accessor (though not currently used for extraction) and returns a
|
||||||
|
* structured request object containing the original request context.
|
||||||
|
*
|
||||||
|
* @param primitiveRequest The raw request containing parameters and context from the network.
|
||||||
|
* @return A new {@link GetNextMessageRequest} instance.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public GetNextMessageRequest parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
RequestParameterAccessor accessor =
|
||||||
|
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||||
|
return new GetNextMessageRequest(primitiveRequest.context());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
|
||||||
|
public class GetNextMessageRequest extends Request {
|
||||||
|
/**
|
||||||
|
* Constructs a new GetNextMessageRequest with the specified request context. This request is
|
||||||
|
* typically used by a client to poll or retrieve the next available message from the server's
|
||||||
|
* queue.
|
||||||
|
*
|
||||||
|
* @param context The {@link RequestContext} associated with this request.
|
||||||
|
*/
|
||||||
|
public GetNextMessageRequest(RequestContext context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
|
||||||
|
|
||||||
|
public class GetNextMessageResponse extends SuccessResponse {
|
||||||
|
/**
|
||||||
|
* Constructs a new GetNextMessageResponse. It converts the provided {@link Message} into a
|
||||||
|
* network-compatible response body and associates it with the original request context.
|
||||||
|
*
|
||||||
|
* @param context The {@link RequestContext} of the request being answered.
|
||||||
|
* @param msg The {@link Message} to be sent back to the client.
|
||||||
|
*/
|
||||||
|
public GetNextMessageResponse(RequestContext context, Message msg) {
|
||||||
|
super(context, msg.toResponse(ResponseBody.builder()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
|
||||||
|
|
||||||
|
public class CreateLobbyHandler extends CommandHandler<CreateLobbyRequest> {
|
||||||
|
private final LobbyManager lobbyManager;
|
||||||
|
private final SessionManager sessionManager;
|
||||||
|
|
||||||
|
public CreateLobbyHandler(
|
||||||
|
ResponseDispatcher responseDispatcher,
|
||||||
|
LobbyManager lobbyManager,
|
||||||
|
SessionManager sessionManager) {
|
||||||
|
super(responseDispatcher);
|
||||||
|
this.lobbyManager = lobbyManager;
|
||||||
|
this.sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CreateLobbyRequest request) {
|
||||||
|
LobbyId id = lobbyManager.createNewLobby(null);
|
||||||
|
|
||||||
|
if (id == null) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(),
|
||||||
|
"LOBBIES_FULL",
|
||||||
|
"Maximum number of 8 lobbies reached"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseDispatcher.dispatch(new CreateLobbyResponse(request.getContext(), id.value()));
|
||||||
|
|
||||||
|
// broadcast LOBBY_CREATED event to all connected sessions (requestId=0)
|
||||||
|
for (Session s : sessionManager.getAllSessions()) {
|
||||||
|
RequestContext ctx = new RequestContext(s.getId(), 0);
|
||||||
|
SuccessResponse ev =
|
||||||
|
new SuccessResponse(
|
||||||
|
ctx,
|
||||||
|
ResponseBody.builder()
|
||||||
|
.param("EVENT", "LOBBY_CREATED")
|
||||||
|
.param("LOBBY_ID", id.value())
|
||||||
|
.build()) {};
|
||||||
|
|
||||||
|
responseDispatcher.dispatch(ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||||
|
|
||||||
|
public class CreateLobbyParser implements CommandParser<CreateLobbyRequest> {
|
||||||
|
@Override
|
||||||
|
public CreateLobbyRequest parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
return new CreateLobbyRequest(primitiveRequest.context());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
|
||||||
|
public class CreateLobbyRequest extends Request {
|
||||||
|
public CreateLobbyRequest(RequestContext context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
|
||||||
|
|
||||||
|
/** Sends ID of newly created lobby back to client. */
|
||||||
|
public class CreateLobbyResponse extends SuccessResponse {
|
||||||
|
public CreateLobbyResponse(RequestContext context, int lobbyId) {
|
||||||
|
super(
|
||||||
|
context,
|
||||||
|
new ResponseBodyBuilder().param("LOBBY_ID", String.valueOf(lobbyId)).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the `GET_LOBBY_LIST` command.
|
||||||
|
*
|
||||||
|
* <p>Queries the {@link LobbyManager} for all available lobbies and dispatches a {@link
|
||||||
|
* GetLobbyListResponse} containing basic metadata for each lobby (`ID`, `NAME`, `PLAYER_COUNT`).
|
||||||
|
*/
|
||||||
|
public class GetLobbyListHandler extends CommandHandler<GetLobbyListRequest> {
|
||||||
|
private final LobbyManager lobbyManager;
|
||||||
|
|
||||||
|
public GetLobbyListHandler(ResponseDispatcher responseDispatcher, LobbyManager lobbyManager) {
|
||||||
|
super(responseDispatcher);
|
||||||
|
this.lobbyManager = lobbyManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(GetLobbyListRequest request) {
|
||||||
|
Collection<ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby> l =
|
||||||
|
lobbyManager.getAllLobbies();
|
||||||
|
responseDispatcher.dispatch(new GetLobbyListResponse(request.getContext(), l));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser for the `GET_LOBBY_LIST` command.
|
||||||
|
*
|
||||||
|
* <p>Parses the incoming primitive request and produces a {@link GetLobbyListRequest}. This command
|
||||||
|
* takes no parameters.
|
||||||
|
*/
|
||||||
|
public class GetLobbyListParser implements CommandParser<GetLobbyListRequest> {
|
||||||
|
@Override
|
||||||
|
public GetLobbyListRequest parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
return new GetLobbyListRequest(primitiveRequest.context());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request object for the `GET_LOBBY_LIST` command.
|
||||||
|
*
|
||||||
|
* <p>Contains only the request context; this command does not require parameters.
|
||||||
|
*/
|
||||||
|
public class GetLobbyListRequest extends Request {
|
||||||
|
public GetLobbyListRequest(RequestContext context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Success response for `GET_LOBBY_LIST`.
|
||||||
|
*
|
||||||
|
* <p>Builds a `LOBBIES` collection with repeated `LOBBY` blocks containing `ID`, `NAME` and
|
||||||
|
* `PLAYER_COUNT`.
|
||||||
|
*/
|
||||||
|
public class GetLobbyListResponse extends SuccessResponse {
|
||||||
|
public GetLobbyListResponse(RequestContext context, Collection<Lobby> lobbies) {
|
||||||
|
super(
|
||||||
|
context,
|
||||||
|
ResponseBody.builder()
|
||||||
|
.block(
|
||||||
|
"LOBBIES",
|
||||||
|
lobbies_block -> {
|
||||||
|
for (Lobby lobby : lobbies) {
|
||||||
|
lobbies_block.block(
|
||||||
|
"LOBBY",
|
||||||
|
lb -> {
|
||||||
|
lb.param("ID", lobby.getId().value());
|
||||||
|
lb.param("NAME", lobby.getName());
|
||||||
|
lb.param(
|
||||||
|
"PLAYER_COUNT",
|
||||||
|
lobby.getPlayerNames().size());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the `GET_LOBBY_STATUS` command.
|
||||||
|
*
|
||||||
|
* <p>Resolves the target lobby either by the optional `ID` parameter or by `USERNAME`. If both are
|
||||||
|
* omitted the handler requires the session to be associated with a logged-in user (see the inline
|
||||||
|
* pre-execution check). On success a {@link GetLobbyStatusResponse} is dispatched, otherwise an
|
||||||
|
* {@link ErrorResponse} with code `LOBBY_NOT_FOUND` is sent.
|
||||||
|
*/
|
||||||
|
public class GetLobbyStatusHandler extends CommandHandler<GetLobbyStatusRequest> {
|
||||||
|
private final LobbyManager lobbyManager;
|
||||||
|
private final UserRegistry userRegistry;
|
||||||
|
|
||||||
|
public GetLobbyStatusHandler(
|
||||||
|
ResponseDispatcher responseDispatcher,
|
||||||
|
LobbyManager lobbyManager,
|
||||||
|
UserRegistry userRegistry) {
|
||||||
|
super(responseDispatcher);
|
||||||
|
this.lobbyManager = lobbyManager;
|
||||||
|
this.userRegistry = userRegistry;
|
||||||
|
|
||||||
|
// Only require logged-in session if the request does not provide an explicit ID
|
||||||
|
// or USERNAME
|
||||||
|
addCheck(
|
||||||
|
request -> {
|
||||||
|
if (!(request instanceof GetLobbyStatusRequest)) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
GetLobbyStatusRequest r = (GetLobbyStatusRequest) request;
|
||||||
|
if (r.getId() != null || r.getUsername() != null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
var maybeUser = userRegistry.getBySessionId(r.getSessionId());
|
||||||
|
if (maybeUser.isPresent()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(
|
||||||
|
new ErrorResponse(
|
||||||
|
r.getContext(),
|
||||||
|
"USER_NOT_LOGGED_IN",
|
||||||
|
"Request requires an associated logged-in user"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(GetLobbyStatusRequest request) {
|
||||||
|
// Resolve username from request or session when no explicit ID/USERNAME
|
||||||
|
// provided
|
||||||
|
String targetUsername = request.getUsername();
|
||||||
|
if (targetUsername == null) {
|
||||||
|
var maybeUser = userRegistry.getBySessionId(request.getSessionId());
|
||||||
|
if (maybeUser.isPresent()) {
|
||||||
|
targetUsername = maybeUser.get().getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lobby =
|
||||||
|
(request.getId() != null)
|
||||||
|
? lobbyManager.getLobby(LobbyId.of(request.getId()))
|
||||||
|
: (targetUsername != null
|
||||||
|
? lobbyManager.getLobbyByUsername(targetUsername)
|
||||||
|
: null);
|
||||||
|
|
||||||
|
if (lobby == null) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseDispatcher.dispatch(new GetLobbyStatusResponse(request.getContext(), lobby));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||||
|
|
||||||
|
public class GetLobbyStatusParser implements CommandParser<GetLobbyStatusRequest> {
|
||||||
|
/**
|
||||||
|
* Parse the incoming primitive request into a {@link GetLobbyStatusRequest}.
|
||||||
|
*
|
||||||
|
* <p>Supported optional parameters:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>`ID` (int) — numeric lobby id to query
|
||||||
|
* <li>`USERNAME` (String) — username to lookup the lobby for
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* If both are omitted the handler may require a logged-in session.
|
||||||
|
*
|
||||||
|
* @param primitiveRequest raw request containing parameters and context
|
||||||
|
* @return parsed {@link GetLobbyStatusRequest}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public GetLobbyStatusRequest parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
RequestParameterAccessor accessor =
|
||||||
|
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||||
|
Integer id = null;
|
||||||
|
try {
|
||||||
|
id = accessor.optional("ID", null, Integer::parseInt);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// parse error handled elsewhere
|
||||||
|
}
|
||||||
|
String username = accessor.optional("USERNAME", null);
|
||||||
|
return new GetLobbyStatusRequest(primitiveRequest.context(), id, username);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
|
||||||
|
public class GetLobbyStatusRequest extends Request {
|
||||||
|
private final Integer id;
|
||||||
|
private final String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new GetLobbyStatusRequest.
|
||||||
|
*
|
||||||
|
* @param context request context (session id, source, etc.)
|
||||||
|
* @param id optional numeric lobby id to query, or null
|
||||||
|
* @param username optional username to query the lobby for, or null
|
||||||
|
*/
|
||||||
|
public GetLobbyStatusRequest(RequestContext context, Integer id, String username) {
|
||||||
|
super(context);
|
||||||
|
this.id = id;
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the optional lobby id. */
|
||||||
|
public Integer getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the optional username. */
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Success response for `GET_LOBBY_STATUS`.
|
||||||
|
*
|
||||||
|
* <p>Builds a response with a single `LOBBY` block containing `ID`, `NAME` and a nested `PLAYERS`
|
||||||
|
* collection with repeated `PLAYER` blocks (`USERNAME`, `READY`).
|
||||||
|
*/
|
||||||
|
public class GetLobbyStatusResponse extends SuccessResponse {
|
||||||
|
/**
|
||||||
|
* Create a new response for the provided {@link Lobby}.
|
||||||
|
*
|
||||||
|
* @param context request context to use for the response
|
||||||
|
* @param lobby lobby domain object containing id, name and players
|
||||||
|
*/
|
||||||
|
public GetLobbyStatusResponse(RequestContext context, Lobby lobby) {
|
||||||
|
super(
|
||||||
|
context,
|
||||||
|
ResponseBody.builder()
|
||||||
|
.param("STATUS", lobby.getGameController() != null ? "RUNNING" : "CREATED")
|
||||||
|
.block(
|
||||||
|
"LOBBY",
|
||||||
|
lb -> {
|
||||||
|
lb.param("ID", lobby.getId().value());
|
||||||
|
// STATUS indicates whether a game has been started in this
|
||||||
|
// lobby
|
||||||
|
lb.param(
|
||||||
|
"STATUS",
|
||||||
|
lobby.getGameController() == null
|
||||||
|
? "CREATED"
|
||||||
|
: "RUNNING");
|
||||||
|
lb.param("NAME", lobby.getName());
|
||||||
|
lb.block(
|
||||||
|
"PLAYERS",
|
||||||
|
players -> {
|
||||||
|
for (String p : lobby.getPlayerNames()) {
|
||||||
|
players.block(
|
||||||
|
"PLAYER",
|
||||||
|
pb -> {
|
||||||
|
pb.param("USERNAME", p);
|
||||||
|
pb.param("READY", "false");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the `JOIN_LOBBY` command.
|
||||||
|
*
|
||||||
|
* <p>Resolves the username from the session (requires {@link UserLoggedInCheck}), looks up the
|
||||||
|
* target lobby and attempts to add the user. On success an `+OK` response is dispatched; on failure
|
||||||
|
* an appropriate {@link ErrorResponse} with one of the error codes `LOBBY_NOT_FOUND`,
|
||||||
|
* `USER_NOT_LOGGED_IN` or `LOBBY_FULL_OR_ALREADY_IN` is returned.
|
||||||
|
*/
|
||||||
|
public class JoinLobbyHandler extends CommandHandler<JoinLobbyRequest> {
|
||||||
|
private final LobbyManager lobbyManager;
|
||||||
|
private final UserRegistry userRegistry;
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(JoinLobbyHandler.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link JoinLobbyHandler}.
|
||||||
|
*
|
||||||
|
* @param responseDispatcher dispatcher used to send responses back to the client
|
||||||
|
* @param lobbyManager manager providing lobby state and operations
|
||||||
|
* @param userRegistry registry to resolve session -> user mappings
|
||||||
|
*/
|
||||||
|
public JoinLobbyHandler(
|
||||||
|
ResponseDispatcher responseDispatcher,
|
||||||
|
LobbyManager lobbyManager,
|
||||||
|
UserRegistry userRegistry) {
|
||||||
|
super(responseDispatcher);
|
||||||
|
this.lobbyManager = lobbyManager;
|
||||||
|
this.userRegistry = userRegistry;
|
||||||
|
addCheck(new UserLoggedInCheck(userRegistry));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the join-lobby request.
|
||||||
|
*
|
||||||
|
* @param request the parsed {@link JoinLobbyRequest}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void execute(JoinLobbyRequest request) {
|
||||||
|
LOGGER.info(
|
||||||
|
"JOIN_LOBBY request: session={}, lobbyId={}",
|
||||||
|
request.getContext().sessionId(),
|
||||||
|
request.getId());
|
||||||
|
LobbyId lid = LobbyId.of(request.getId());
|
||||||
|
var lobby = lobbyManager.getLobby(lid);
|
||||||
|
if (lobby == null) {
|
||||||
|
LOGGER.warn(
|
||||||
|
"JOIN_LOBBY: Lobby {} not found (session={})",
|
||||||
|
request.getId(),
|
||||||
|
request.getContext().sessionId());
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var maybeUser = userRegistry.getBySessionId(request.getContext().sessionId());
|
||||||
|
if (maybeUser.isEmpty()) {
|
||||||
|
LOGGER.warn(
|
||||||
|
"JOIN_LOBBY: No user associated with session {}",
|
||||||
|
request.getContext().sessionId());
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(),
|
||||||
|
"USER_NOT_LOGGED_IN",
|
||||||
|
"No user associated with session"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = maybeUser.get();
|
||||||
|
String username = user.getName();
|
||||||
|
|
||||||
|
boolean ok = lobbyManager.addPlayerToLobby(username, lid);
|
||||||
|
if (!ok) {
|
||||||
|
LOGGER.warn(
|
||||||
|
"JOIN_LOBBY: User '{}' failed to join lobby {} (full or already in)",
|
||||||
|
username,
|
||||||
|
request.getId());
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(),
|
||||||
|
"LOBBY_FULL_OR_ALREADY_IN",
|
||||||
|
"Lobby full or user already in lobby"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info("User '{}' joined lobby {}", username, request.getId());
|
||||||
|
|
||||||
|
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
||||||
|
}
|
||||||
|
}
|
||||||