Merge branch 'main' into feat/93-add-serverside-create-lobby-command

This commit is contained in:
Jona Walpert
2026-04-12 11:27:11 +02:00
159 changed files with 19963 additions and 304 deletions
-1
View File
@@ -36,7 +36,6 @@ This protocol handles game state synchronization, player actions, lobby and glob
├── documents/ # Project resources and documentation
│ ├── images/ # Images for README and documentation
│ ├── diary # Diary files for each organizational meetup
│ ├── blog # Blog files covering project-related topics
│ ├── docs # Documentation
│ ├── milestones/ # Milestone deliverables (6 milestones)
│ └── (Other resources) # Additional project materials
+10
View File
@@ -4,6 +4,7 @@ plugins {
id 'org.openjfx.javafxplugin' version '0.1.0'
id 'checkstyle'
id 'com.diffplug.spotless' version '7.0.2'
id 'jacoco'
}
group = 'ch.unibas.dmi.dbis'
@@ -41,8 +42,17 @@ dependencies {
implementation 'org.json:json:20240303'
}
jacoco {
toolVersion = "0.8.14"
}
jacocoTestReport {
dependsOn test
}
test {
useJUnitPlatform()
finalizedBy jacocoTestReport
}
checkstyle {
+3
View File
@@ -21,4 +21,7 @@
<!-- Allow for compacter line seperators -->
<suppress checks="EmptyLineSeparator" files=".*Test\.java"/>
<!-- Allow longer method length in composition root -->
<suppress checks="MethodLength" files="ServerApp.java"/>
</suppressions>
-28
View File
@@ -1,28 +0,0 @@
# Blog
## Purpose
This folder contains a collection of blog posts documenting the development process of our application. These blogs serve two key purposes:
1. **For External Audiences**
The blogs provide insights into the internal program flow and the interaction between components without requiring readers to dive into the source code. They offer a mixed-level overview of how different parts of the system work together.
2. **For Our Team**
The blogs serve as a record of our decision-making process. By documenting what decisions were made and why, we can track the evolution of our design choices and understand the reasoning behind them. This is invaluable for maintaining consistency across the team.
But keep in mind that these blogs **do not replace** the detailed source code documentation.
## Format
All blog posts follow the naming convention: `<YYYY-MM-DD>_<Title>.md`
## Blog Posts
### General
*(No posts yet)*
### Client
*(No posts yet)*
### Server
*(No posts yet)*
+9
View File
@@ -0,0 +1,9 @@
# Besprechung über Veränderung der Aufgabenteilung
## Verschieben von Aufgaben
Da die Implementation der client server Kommunikation länger dauerte als geplant, wurden achivements erfüllt, die erst bei späteren Meilensteinen relevant sein werden.
## Aufgabeverteilung
Um zu vermeiden, dass sich Personen erst lange Zeit in den Code anderer einarbeiten müssen um zu helfen, wurden Dokumentation und das Erstellen von Materialien für MS4 auf ein Teammitgleid (Jona) ausgelagert um das Projekt nicht zu verlangsamen
@@ -0,0 +1,537 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
<head>
<meta charset="utf-8" />
<meta name="generator" content="pandoc" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
<title>casono-rules-easy-description</title>
<style>
html {
color: #1a1a1a;
background-color: #fdfdfd;
}
body {
margin: 0 auto;
max-width: 36em;
padding-left: 50px;
padding-right: 50px;
padding-top: 50px;
padding-bottom: 50px;
hyphens: auto;
overflow-wrap: break-word;
text-rendering: optimizeLegibility;
font-kerning: normal;
}
@media (max-width: 600px) {
body {
font-size: 0.9em;
padding: 12px;
}
h1 {
font-size: 1.8em;
}
}
@media print {
html {
background-color: white;
}
body {
background-color: transparent;
color: black;
font-size: 12pt;
}
p, h2, h3 {
orphans: 3;
widows: 3;
}
h2, h3, h4 {
page-break-after: avoid;
}
}
p {
margin: 1em 0;
}
a {
color: #1a1a1a;
}
a:visited {
color: #1a1a1a;
}
img {
max-width: 100%;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 1.4em;
}
h5, h6 {
font-size: 1em;
font-style: italic;
}
h6 {
font-weight: normal;
}
ol, ul {
padding-left: 1.7em;
margin-top: 1em;
}
li > ol, li > ul {
margin-top: 0;
}
blockquote {
margin: 1em 0 1em 1.7em;
padding-left: 1em;
border-left: 2px solid #e6e6e6;
color: #606060;
}
code {
font-family: Menlo, Monaco, Consolas, 'Lucida Console', monospace;
font-size: 85%;
margin: 0;
hyphens: manual;
}
pre {
margin: 1em 0;
overflow: auto;
}
pre code {
padding: 0;
overflow: visible;
overflow-wrap: normal;
}
.sourceCode {
background-color: transparent;
overflow: visible;
}
hr {
background-color: #1a1a1a;
border: none;
height: 1px;
margin: 1em 0;
}
table {
margin: 1em 0;
border-collapse: collapse;
width: 100%;
overflow-x: auto;
display: block;
font-variant-numeric: lining-nums tabular-nums;
}
table caption {
margin-bottom: 0.75em;
}
tbody {
margin-top: 0.5em;
border-top: 1px solid #1a1a1a;
border-bottom: 1px solid #1a1a1a;
}
th {
border-top: 1px solid #1a1a1a;
padding: 0.25em 0.5em 0.25em 0.5em;
}
td {
padding: 0.125em 0.5em 0.25em 0.5em;
}
header {
margin-bottom: 4em;
text-align: center;
}
#TOC li {
list-style: none;
}
#TOC ul {
padding-left: 1.3em;
}
#TOC > ul {
padding-left: 0;
}
#TOC a:not(:hover) {
text-decoration: none;
}
code{white-space: pre-wrap;}
span.smallcaps{font-variant: small-caps;}
div.columns{display: flex; gap: min(4vw, 1.5em);}
div.column{flex: auto; overflow-x: auto;}
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
/* The extra [class] is a hack that increases specificity enough to
override a similar rule in reveal.js */
ul.task-list[class]{list-style: none;}
ul.task-list li input[type="checkbox"] {
font-size: inherit;
width: 0.8em;
margin: 0 0.8em 0.2em -1.6em;
vertical-align: middle;
}
.display.math{display: block; text-align: center; margin: 0.5rem auto;}
</style>
</head>
<body>
<h1 id="casono-rules-easy-description">Casono Rules Easy
Description</h1>
<blockquote>
<p>Source:
https://www.youtube.com/watch?v=h-1WyU5Wqsw&amp;pp=ygUZbGVybmVuIHBva2VyIHRleGFzIGhvbGRlbQ%3D%3D
<!-- vim-markdown-toc GFM --></p>
</blockquote>
<ul>
<li><a href="#deustch">Deustch</a>
<ul>
<li><a href="#blinds-small-blind--big-blind">Blinds (Small Blind &amp;
Big Blind)</a></li>
<li><a href="#erste-setzrunde-preflop">Erste Setzrunde
(Preflop)</a></li>
<li><a href="#beispiel-preflop">Beispiel Preflop</a></li>
<li><a href="#flop-3-gemeinschaftskarten">Flop (3
Gemeinschaftskarten)</a></li>
<li><a href="#beispiel-flop">Beispiel Flop</a></li>
<li><a href="#turn-4-karte">Turn (4. Karte)</a></li>
<li><a href="#river-5-karte">River (5. Karte)</a></li>
<li><a href="#showdown-gewinnentscheidung">Showdown
(Gewinnentscheidung)</a></li>
<li><a href="#poker-hand-rankings-gewichtung">Poker Hand Rankings
(Gewichtung)</a></li>
<li><a href="#fazit">Fazit</a></li>
</ul></li>
<li><a href="#english">English</a>
<ul>
<li><a href="#blinds-small-blind--big-blind-1">Blinds (Small Blind &amp;
Big Blind)</a></li>
<li><a href="#first-betting-round-preflop">First Betting Round
(Preflop)</a></li>
<li><a href="#preflop-example">Preflop Example</a></li>
<li><a href="#flop-3-community-cards">Flop (3 Community Cards)</a></li>
<li><a href="#flop-example">Flop Example</a></li>
<li><a href="#turn-4th-card">Turn (4th Card)</a></li>
<li><a href="#river-5th-card">River (5th Card)</a></li>
<li><a href="#showdown-winning-decision">Showdown (Winning
Decision)</a></li>
<li><a href="#poker-hand-rankings-hierarchy">Poker Hand Rankings
(Hierarchy)</a></li>
<li><a href="#conclusion">Conclusion</a></li>
</ul></li>
</ul>
<!-- vim-markdown-toc -->
<h2 id="deutsch">Deutsch</h2>
<p>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.</p>
<p>Damit Du in diesem Erlebnisraum starten kannst, ist es wie bei
jedem Spiel notwendig, zuerst die Grundregeln und den Spielablauf zu
verstehen.</p>
<p>Also los geht es:</p>
<p>Wir haben am Tisch <strong>4 Spieler</strong>: Julian, Mathis, Jona
und Lars. Jeder Spieler startet mit <strong>20.000 Chips</strong>. Jeder
bekommt <strong>2 Karten auf die Hand</strong> und es gibt zusätzlich
<strong>5 Gemeinschaftskarten</strong>, die später in der Mitte
aufgedeckt werden.</p>
<figure>
<img src="./images/1.png" alt="1. image" />
<figcaption aria-hidden="true">1. image</figcaption>
</figure>
<p>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.</p>
<p>Regel: <em>34 Button Placement and Movement 🟢</em></p>
<h3 id="blinds-small-blind-big-blind">Blinds (Small Blind &amp; Big
Blind)</h3>
<p>Bevor die Karten verteilt werden, gibt es zwei Pflicht-Einsätze:</p>
<p>Der Small Blind und der Big Blind. Der Big Blind ist immer doppelt so
hoch wie der Small Blind.</p>
<figure>
<img src="./images/2.png" alt="2. image" />
<figcaption aria-hidden="true">2. image</figcaption>
</figure>
<p>Regel: <em>32 Dead Button 🟡</em></p>
<p>Diese Einsätze sorgen dafür, dass sofort ein Pot entsteht und das
Spiel überhaupt beginnt, weil jeder schon “im Spiel” ist.</p>
<p>Danach werden die Karten verteilt: zuerst Small Blind, dann Big Blind
und dann im Uhrzeigersinn alle anderen Spieler.</p>
<h3 id="erste-setzrunde-preflop">Erste Setzrunde (Preflop)</h3>
<p>Die erste Setzrunde beginnt immer bei dem Spieler links vom Big
Blind.</p>
<p>Jetzt muss jeder Spieler entscheiden:</p>
<ul>
<li>Fold (aussteigen)</li>
<li>Call (mitgehen)</li>
<li>Raise (erhöhen)</li>
</ul>
<p>Regel: <em>40 Methods of Betting 🟢</em> Regel: <em>41 Methods of
Calling 🟢</em> Regel: <em>42 Methods of Raising 🟢</em> Regel: <em>50
Acting in Turn 🟢</em></p>
<h3 id="beispiel-preflop">Beispiel Preflop</h3>
<p>Julian schaut seine Karten an und entscheidet sich direkt für einen
Call von <strong>600 Chips</strong>.</p>
<figure>
<img src="./images/3.png" alt="3. image" />
<figcaption aria-hidden="true">3. image</figcaption>
</figure>
<p>Mathis sieht seine Karten an und merkt, dass sie nicht gut sind, also
foldet er und steigt aus.</p>
<figure>
<img src="./images/4.png" alt="4. image" />
<figcaption aria-hidden="true">4. image</figcaption>
</figure>
<p>Jona ist nun dran und entscheidet sich ebenfalls für einen Call, weil
seine Hand spielbar ist.</p>
<figure>
<img src="./images/5.png" alt="5. image" />
<figcaption aria-hidden="true">5. image</figcaption>
</figure>
<p>Lars schaut seine Karten an, erkennt eine starke Hand und erhöht auf
<strong>1200 Chips</strong>.</p>
<figure>
<img src="./images/6.png" alt="6. image" />
<figcaption aria-hidden="true">6. image</figcaption>
</figure>
<p>Damit verändert sich sofort die Situation: Julian und Jona müssen
entscheiden, ob sie diesen Raise bezahlen, selbst erhöhen oder
aussteigen.</p>
<h3 id="flop-3-gemeinschaftskarten">Flop (3 Gemeinschaftskarten)</h3>
<p>Jetzt werden <strong>3 Gemeinschaftskarten</strong> in die Mitte
gelegt. Ab hier verändert sich das Spiel komplett, weil alle Spieler
zusätzliche Informationen bekommen.</p>
<figure>
<img src="./images/7.png" alt="7. image" />
<figcaption aria-hidden="true">7. image</figcaption>
</figure>
<p>Es beginnt eine neue Setzrunde.</p>
<p>Regel: <em>49 Accepted Action 🟢</em></p>
<h3 id="beispiel-flop">Beispiel Flop</h3>
<p>Lars setzt <strong>1000 Chips</strong> als Erstes. Julian entscheidet
sich mitzugehen (Call), weil seine Karten durch die Gemeinschaftskarten
stärker geworden sind.</p>
<figure>
<img src="./images/8.png" alt="8. image" />
<figcaption aria-hidden="true">8. image</figcaption>
</figure>
<p>Jona steigt aus, weil er keine gute Verbindung mehr sieht. Mathis ist
bereits raus.</p>
<h3 id="turn-4.-karte">Turn (4. Karte)</h3>
<p>Jetzt kommt die <strong>4. Gemeinschaftskarte</strong>.</p>
<p>Wieder beginnt eine neue Setzrunde.</p>
<p>Lars setzt diesmal <strong>3000 Chips</strong>. Julian bezahlt erneut
(Call), weil seine Hand weiterhin gut spielbar ist.</p>
<figure>
<img src="./images/9.png" alt="9. image" />
<figcaption aria-hidden="true">9. image</figcaption>
</figure>
<p>Regel: <em>53 Action Out of Turn 🟡</em></p>
<h3 id="river-5.-karte">River (5. Karte)</h3>
<p>Jetzt wird die letzte Gemeinschaftskarte aufgedeckt.</p>
<p>Dies ist die letzte Entscheidung im Spiel.</p>
<p>Lars setzt <strong>5000 Chips</strong>.</p>
<figure>
<img src="./images/10.png" alt="10. image" />
<figcaption aria-hidden="true">10. image</figcaption>
</figure>
<p>Julian muss jetzt entscheiden: Fold, Call oder Raise auf
<strong>10000 Chips</strong>.</p>
<p>Regel: <em>54 Pot Size Bets 🟡</em></p>
<h3 id="showdown-gewinnentscheidung">Showdown (Gewinnentscheidung)</h3>
<p>Wenn nach der letzten Setzrunde noch zwei Spieler übrig sind, kommt
es zum Showdown.</p>
<p>Beide Spieler zeigen ihre Karten offen. Gewonnen hat die
<strong>beste 5-Karten-Kombination aus Handkarten und
Gemeinschaftskarten</strong>.</p>
<figure>
<img src="./images/11.png" alt="11. image" />
<figcaption aria-hidden="true">11. image</figcaption>
</figure>
<p>Regel: <em>12 Cards Speak at Showdown 🟢</em> Regel: <em>16 Face Up
for All-Ins 🟢</em> Regel: <em>17 Non All-In Showdowns 🟢</em></p>
<p>Wenn Julian den letzten Einsatz bezahlt, werden die Hände verglichen.
Wenn er foldet, gewinnt Lars automatisch den gesamten Pot.</p>
<h3 id="poker-hand-rankings-gewichtung">Poker Hand Rankings
(Gewichtung)</h3>
<p>Die Kartenkombinationen sind klar geordnet von schwach bis extrem
stark:</p>
<figure>
<img src="./images/12.png" alt="12. image" />
<figcaption aria-hidden="true">12. image</figcaption>
</figure>
<p>Je höher die Kombination, desto stärker die Hand und desto
wahrscheinlicher der Gewinn.</p>
<p>Julian: 2. Paar:</p>
<figure>
<img src="./images/13.png" alt="13. image" />
<figcaption aria-hidden="true">13. image</figcaption>
</figure>
<p>Lars: 1. Paar:</p>
<figure>
<img src="./images/14.png" alt="14. image" />
<figcaption aria-hidden="true">14. image</figcaption>
</figure>
<p>Da zwei Paare in der Rangfolge über einem einzelnen Paar stehen,
gewinnt Julian diese Runde.</p>
<h3 id="fazit">Fazit</h3>
<p>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.</p>
<p>Regel: <em>67 One Player One Hand 🟢</em> Regel: <em>52 Incorrect
Bets 🟡</em> Regel: <em>57 Non-Standard Betting 🟡</em></p>
<h2 id="english">English</h2>
<p>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?</p>
<p>To enter this experiential space, its necessary—like with any
game—to first understand the basic rules and game flow.</p>
<p>So lets begin:</p>
<p>We have <strong>4 players</strong> at the table: Julian, Mathis,
Jona, and Lars. Each player starts with <strong>20,000 chips</strong>.
Each receives <strong>2 hole cards</strong>, and there are <strong>5
community cards</strong> that will later be revealed in the center.</p>
<figure>
<img src="./images/1.png" alt="1. image" />
<figcaption aria-hidden="true">1. image</figcaption>
</figure>
<p>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.</p>
<p>Rule: <em>34 Button Placement and Movement 🟢</em></p>
<h3 id="blinds-small-blind-big-blind-1">Blinds (Small Blind &amp; Big
Blind)</h3>
<p>Before cards are dealt, there are two mandatory bets:</p>
<p>The Small Blind and the Big Blind. The Big Blind is always double the
amount of the Small Blind.</p>
<figure>
<img src="./images/2.png" alt="2. image" />
<figcaption aria-hidden="true">2. image</figcaption>
</figure>
<p>Rule: <em>32 Dead Button 🟡</em></p>
<p>These forced bets ensure that a pot exists from the start, making the
game begin with everyone already involved.</p>
<p>After this, the cards are dealt: first to the Small Blind, then the
Big Blind, and then clockwise to all other players.</p>
<h3 id="first-betting-round-preflop">First Betting Round (Preflop)</h3>
<p>The first betting round always begins with the player to the left of
the Big Blind.</p>
<p>Now each player must decide:</p>
<ul>
<li>Fold (exit the hand)</li>
<li>Call (match the current bet)</li>
<li>Raise (increase the bet)</li>
</ul>
<p>Rule: <em>40 Methods of Betting 🟢</em><br />
Rule: <em>41 Methods of Calling 🟢</em><br />
Rule: <em>42 Methods of Raising 🟢</em><br />
Rule: <em>50 Acting in Turn 🟢</em></p>
<h3 id="preflop-example">Preflop Example</h3>
<p>Julian looks at his cards and decides to Call for <strong>600
chips</strong>.</p>
<figure>
<img src="./images/3.png" alt="3. image" />
<figcaption aria-hidden="true">3. image</figcaption>
</figure>
<p>Mathis sees his cards and realizes they arent strong, so he Folds
and exits the hand.</p>
<figure>
<img src="./images/4.png" alt="4. image" />
<figcaption aria-hidden="true">4. image</figcaption>
</figure>
<p>Jona is next and decides to Call as well, because his hand is
playable.</p>
<figure>
<img src="./images/5.png" alt="5. image" />
<figcaption aria-hidden="true">5. image</figcaption>
</figure>
<p>Lars looks at his cards, recognizes a strong hand, and Raises to
<strong>1200 chips</strong>.</p>
<figure>
<img src="./images/6.png" alt="6. image" />
<figcaption aria-hidden="true">6. image</figcaption>
</figure>
<p>This immediately changes the situation: Julian and Jona must now
decide whether to call this raise, re-raise, or fold.</p>
<h3 id="flop-3-community-cards">Flop (3 Community Cards)</h3>
<p>Now <strong>3 community cards</strong> are placed face-up in the
center. From this point, the game changes completely, as all players
gain additional information.</p>
<figure>
<img src="./images/7.png" alt="7. image" />
<figcaption aria-hidden="true">7. image</figcaption>
</figure>
<p>A new betting round begins.</p>
<p>Rule: <em>49 Accepted Action 🟢</em></p>
<h3 id="flop-example">Flop Example</h3>
<p>Lars bets <strong>1,000 chips</strong> first. Julian decides to Call
because his hand has improved with the community cards.</p>
<figure>
<img src="./images/8.png" alt="8. image" />
<figcaption aria-hidden="true">8. image</figcaption>
</figure>
<p>Jona folds, as he no longer sees a strong connection. Mathis is
already out.</p>
<h3 id="turn-4th-card">Turn (4th Card)</h3>
<p>Now the <strong>4th community card</strong> is revealed.</p>
<p>Another betting round begins.</p>
<p>Lars bets <strong>3,000 chips</strong> this time. Julian calls again,
as his hand remains strong.</p>
<figure>
<img src="./images/9.png" alt="9. image" />
<figcaption aria-hidden="true">9. image</figcaption>
</figure>
<p>Rule: <em>53 Action Out of Turn 🟡</em></p>
<h3 id="river-5th-card">River (5th Card)</h3>
<p>Now the final community card is revealed.</p>
<p>This is the last decision point in the hand.</p>
<p>Lars bets <strong>5,000 chips</strong>.</p>
<figure>
<img src="./images/10.png" alt="10. image" />
<figcaption aria-hidden="true">10. image</figcaption>
</figure>
<p>Julian must now decide: Fold, Call, or even Raise to <strong>10,000
chips</strong>.</p>
<p>Rule: <em>54 Pot Size Bets 🟡</em></p>
<h3 id="showdown-winning-decision">Showdown (Winning Decision)</h3>
<p>If two or more players remain after the final betting round, a
showdown occurs.</p>
<p>All active players reveal their cards. The winner is the one with the
<strong>best 5-card combination using any combination of their hole
cards and the community cards</strong>.</p>
<figure>
<img src="./images/11.png" alt="11. image" />
<figcaption aria-hidden="true">11. image</figcaption>
</figure>
<p>Rule: <em>12 Cards Speak at Showdown 🟢</em><br />
Rule: <em>16 Face Up for All-Ins 🟢</em><br />
Rule: <em>17 Non All-In Showdowns 🟢</em></p>
<p>If Julian calls the final bet, the hands are compared. If he folds,
Lars wins the entire pot automatically.</p>
<h3 id="poker-hand-rankings-hierarchy">Poker Hand Rankings
(Hierarchy)</h3>
<p>The card combinations are clearly ranked—from weakest to
strongest:</p>
<figure>
<img src="./images/12.png" alt="12. image" />
<figcaption aria-hidden="true">12. image</figcaption>
</figure>
<p>The higher the combination, the stronger the hand, and the greater
the chance of winning.</p>
<p>Julian: Two Pair:</p>
<figure>
<img src="./images/13.png" alt="13. image" />
<figcaption aria-hidden="true">13. image</figcaption>
</figure>
<p>Lars: One Pair:</p>
<figure>
<img src="./images/14.png" alt="14. image" />
<figcaption aria-hidden="true">14. image</figcaption>
</figure>
<p>Since two pair ranks higher than one pair, Julian wins this
round.</p>
<h3 id="conclusion">Conclusion</h3>
<p>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.</p>
</body>
</html>
@@ -50,6 +50,42 @@ This document describes the protocol for client-server communication in our appl
- [Error Response](#error-response-6)
- [Example Request](#example-request-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)
- [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 -->
@@ -455,6 +491,628 @@ 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
MESSAGE=Lobby not found
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.
@@ -0,0 +1,323 @@
# Casono Rules Easy Description
> Source: <https://www.youtube.com/watch?v=h-1WyU5Wqsw&pp=ygUZbGVybmVuIHBva2VyIHRleGFzIGhvbGRlbQ%3D%3D>
<!-- vim-markdown-toc GFM -->
* [Deustch](#deustch)
* [Blinds (Small Blind & Big Blind)](#blinds-small-blind--big-blind)
* [Erste Setzrunde (Preflop)](#erste-setzrunde-preflop)
* [Beispiel Preflop](#beispiel-preflop)
* [Flop (3 Gemeinschaftskarten)](#flop-3-gemeinschaftskarten)
* [Beispiel Flop](#beispiel-flop)
* [Turn (4. Karte)](#turn-4-karte)
* [River (5. Karte)](#river-5-karte)
* [Showdown (Gewinnentscheidung)](#showdown-gewinnentscheidung)
* [Poker Hand Rankings (Gewichtung)](#poker-hand-rankings-gewichtung)
* [Fazit](#fazit)
* [English](#english)
* [Blinds (Small Blind & Big Blind)](#blinds-small-blind--big-blind-1)
* [First Betting Round (Preflop)](#first-betting-round-preflop)
* [Preflop Example](#preflop-example)
* [Flop (3 Community Cards)](#flop-3-community-cards)
* [Flop Example](#flop-example)
* [Turn (4th Card)](#turn-4th-card)
* [River (5th Card)](#river-5th-card)
* [Showdown (Winning Decision)](#showdown-winning-decision)
* [Poker Hand Rankings (Hierarchy)](#poker-hand-rankings-hierarchy)
* [Conclusion](#conclusion)
<!-- 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.
Damit Du in diesem Erlebnisraum starten kannst, ist es wie bei jedem Spiel notwendig, zuerst die Grundregeln und den Spielablauf zu verstehen.
Also los geht es:
Wir haben am Tisch **4 Spieler**: Julian, Mathis, Jona und Lars. Jeder Spieler startet mit **20.000 Chips**. Jeder bekommt **2 Karten auf die Hand** und es gibt zusätzlich **5 Gemeinschaftskarten**, die später in der Mitte aufgedeckt werden.
![1. image](./images/1.png)
Die Spieler sitzen in folgender Reihenfolge: Julian, Mathis, Jona und Lars. Einer davon hat den Dealer-Button, der bestimmt, wer die Karten austeilt und von wo die Runde beginnt. Dieser Button wandert nach jeder Runde im Uhrzeigersinn weiter und verändert damit die Position ständig.
Regel: *34 Button Placement and Movement 🟢*
### Blinds (Small Blind & Big Blind)
Bevor die Karten verteilt werden, gibt es zwei Pflicht-Einsätze:
Der Small Blind und der Big Blind. Der Big Blind ist immer doppelt so hoch wie der Small Blind.
![2. image](./images/2.png)
Regel: *32 Dead Button 🟡*
Diese Einsätze sorgen dafür, dass sofort ein Pot entsteht und das Spiel überhaupt beginnt, weil jeder schon “im Spiel” ist.
Danach werden die Karten verteilt: zuerst Small Blind, dann Big Blind und dann im Uhrzeigersinn alle anderen Spieler.
### Erste Setzrunde (Preflop)
Die erste Setzrunde beginnt immer bei dem Spieler links vom Big Blind.
Jetzt muss jeder Spieler entscheiden:
* Fold (aussteigen)
* Call (mitgehen)
* Raise (erhöhen)
Regel: *40 Methods of Betting 🟢*
Regel: *41 Methods of Calling 🟢*
Regel: *42 Methods of Raising 🟢*
Regel: *50 Acting in Turn 🟢*
### Beispiel Preflop
Julian schaut seine Karten an und entscheidet sich direkt für einen Call von **600 Chips**.
![3. image](./images/3.png)
Mathis sieht seine Karten an und merkt, dass sie nicht gut sind, also foldet er und steigt aus.
![4. image](./images/4.png)
Jona ist nun dran und entscheidet sich ebenfalls für einen Call, weil seine Hand spielbar ist.
![5. image](./images/5.png)
Lars schaut seine Karten an, erkennt eine starke Hand und erhöht auf **1200 Chips**.
![6. image](./images/6.png)
Damit verändert sich sofort die Situation: Julian und Jona müssen entscheiden, ob sie diesen Raise bezahlen, selbst erhöhen oder aussteigen.
### Flop (3 Gemeinschaftskarten)
Jetzt werden **3 Gemeinschaftskarten** in die Mitte gelegt. Ab hier verändert sich das Spiel komplett, weil alle Spieler zusätzliche Informationen bekommen.
![7. image](./images/7.png)
Es beginnt eine neue Setzrunde.
Regel: *49 Accepted Action 🟢*
### Beispiel Flop
Lars setzt **1000 Chips** als Erstes. Julian entscheidet sich mitzugehen (Call), weil seine Karten durch die Gemeinschaftskarten stärker geworden sind.
![8. image](./images/8.png)
Jona steigt aus, weil er keine gute Verbindung mehr sieht. Mathis ist bereits raus.
### Turn (4. Karte)
Jetzt kommt die **4. Gemeinschaftskarte**.
Wieder beginnt eine neue Setzrunde.
Lars setzt diesmal **3000 Chips**. Julian bezahlt erneut (Call), weil seine Hand weiterhin gut spielbar ist.
![9. image](./images/9.png)
Regel: *53 Action Out of Turn 🟡*
### River (5. Karte)
Jetzt wird die letzte Gemeinschaftskarte aufgedeckt.
Dies ist die letzte Entscheidung im Spiel.
Lars setzt **5000 Chips**.
![10. image](./images/10.png)
Julian muss jetzt entscheiden: Fold, Call oder Raise auf **10000 Chips**.
Regel: *54 Pot Size Bets 🟡*
### Showdown (Gewinnentscheidung)
Wenn nach der letzten Setzrunde noch zwei Spieler übrig sind, kommt es zum Showdown.
Beide Spieler zeigen ihre Karten offen. Gewonnen hat die **beste 5-Karten-Kombination aus Handkarten und Gemeinschaftskarten**.
![11. image](./images/11.png)
Regel: *12 Cards Speak at Showdown 🟢*
Regel: *16 Face Up for All-Ins 🟢*
Regel: *17 Non All-In Showdowns 🟢*
Wenn Julian den letzten Einsatz bezahlt, werden die Hände verglichen. Wenn er foldet, gewinnt Lars automatisch den gesamten Pot.
### Poker Hand Rankings (Gewichtung)
Die Kartenkombinationen sind klar geordnet von schwach bis extrem stark:
![12. image](./images/12.png)
Je höher die Kombination, desto stärker die Hand und desto wahrscheinlicher der Gewinn.
Julian: 2. Paar:
![13. image](./images/13.png)
Lars: 1. Paar:
![14. image](./images/14.png)
Da zwei Paare in der Rangfolge über einem einzelnen Paar stehen, gewinnt Julian diese Runde.
### Fazit
Poker ist kein Glücksspiel im klassischen Sinn, sondern ein Spiel aus Strategie, Psychologie und Mathematik. Jede Entscheidung von Julian, Mathis, Jona oder Lars verändert die komplette Dynamik am Tisch. Wer die Regeln versteht, versteht nicht nur Karten, sondern auch Menschen und Entscheidungen unter Druck.
Regel: *67 One Player One Hand 🟢*
Regel: *52 Incorrect Bets 🟡*
Regel: *57 Non-Standard Betting 🟡*
## English
The poker table is an incredibly fascinating experiential space where you can learn a great deal: about yourself, about other people, and about questions such as: How do I actually make decisions? How do I handle stress and uncertainty? And how good am I at empathizing with others and correctly assessing situations?
To enter this experiential space, its necessary—like with any game—to first understand the basic rules and game flow.
So lets begin:
We have **4 players** at the table: Julian, Mathis, Jona, and Lars. Each player starts with **20,000 chips**. Each receives **2 hole cards**, and there are **5 community cards** that will later be revealed in the center.
![1. image](./images/1.png)
The players are seated as follows: Julian, Mathis, Jona, and Lars. One of them holds the dealer button, which determines who deals the cards and where the round begins. This button moves clockwise after each hand, constantly changing the positions.
Rule: *34 Button Placement and Movement 🟢*
### Blinds (Small Blind & Big Blind)
Before cards are dealt, there are two mandatory bets:
The Small Blind and the Big Blind. The Big Blind is always double the amount of the Small Blind.
![2. image](./images/2.png)
Rule: *32 Dead Button 🟡*
These forced bets ensure that a pot exists from the start, making the game begin with everyone already involved.
After this, the cards are dealt: first to the Small Blind, then the Big Blind, and then clockwise to all other players.
### First Betting Round (Preflop)
The first betting round always begins with the player to the left of the Big Blind.
Now each player must decide:
* Fold (exit the hand)
* Call (match the current bet)
* Raise (increase the bet)
Rule: *40 Methods of Betting 🟢*
Rule: *41 Methods of Calling 🟢*
Rule: *42 Methods of Raising 🟢*
Rule: *50 Acting in Turn 🟢*
### Preflop Example
Julian looks at his cards and decides to Call for **600 chips**.
![3. image](./images/3.png)
Mathis sees his cards and realizes they arent strong, so he Folds and exits the hand.
![4. image](./images/4.png)
Jona is next and decides to Call as well, because his hand is playable.
![5. image](./images/5.png)
Lars looks at his cards, recognizes a strong hand, and Raises to **1200 chips**.
![6. image](./images/6.png)
This immediately changes the situation: Julian and Jona must now decide whether to call this raise, re-raise, or fold.
### Flop (3 Community Cards)
Now **3 community cards** are placed face-up in the center. From this point, the game changes completely, as all players gain additional information.
![7. image](./images/7.png)
A new betting round begins.
Rule: *49 Accepted Action 🟢*
### Flop Example
Lars bets **1,000 chips** first. Julian decides to Call because his hand has improved with the community cards.
![8. image](./images/8.png)
Jona folds, as he no longer sees a strong connection. Mathis is already out.
### Turn (4th Card)
Now the **4th community card** is revealed.
Another betting round begins.
Lars bets **3,000 chips** this time. Julian calls again, as his hand remains strong.
![9. image](./images/9.png)
Rule: *53 Action Out of Turn 🟡*
### River (5th Card)
Now the final community card is revealed.
This is the last decision point in the hand.
Lars bets **5,000 chips**.
![10. image](./images/10.png)
Julian must now decide: Fold, Call, or even Raise to **10,000 chips**.
Rule: *54 Pot Size Bets 🟡*
### Showdown (Winning Decision)
If two or more players remain after the final betting round, a showdown occurs.
All active players reveal their cards. The winner is the one with the **best 5-card combination using any combination of their hole cards and the community cards**.
![11. image](./images/11.png)
Rule: *12 Cards Speak at Showdown 🟢*
Rule: *16 Face Up for All-Ins 🟢*
Rule: *17 Non All-In Showdowns 🟢*
If Julian calls the final bet, the hands are compared. If he folds, Lars wins the entire pot automatically.
### Poker Hand Rankings (Hierarchy)
The card combinations are clearly ranked—from weakest to strongest:
![12. image](./images/12.png)
The higher the combination, the stronger the hand, and the greater the chance of winning.
Julian: Two Pair:
![13. image](./images/13.png)
Lars: One Pair:
![14. image](./images/14.png)
Since two pair ranks higher than one pair, Julian wins this round.
### Conclusion
Poker is not gambling in the traditional sense, but rather a game of strategy, psychology, and mathematics. Every decision made by Julian, Mathis, Jona, or Lars changes the entire dynamic at the table. Understanding the rules means not only understanding cards, but also people and decision-making under pressure.
File diff suppressed because it is too large Load Diff
@@ -7,6 +7,12 @@ import org.apache.logging.log4j.Logger;
/** Main entry point for Casono application. Handles client and server startup. */
public final class Main {
private static final int MIN_ARGS_FOR_USERNAME = 3;
private static final int ARGS_COUNT_SERVER = 2;
private static final int ARGS_COUNT_CLIENT_MIN = 2;
private static final int ARGS_COUNT_CLIENT_WITH_USER = 3;
/**
* Main entry point for Casono.
*
@@ -17,10 +23,13 @@ public final class Main {
printUsage();
System.exit(1);
}
switch (args[0]) {
case "server" -> ServerApp.start(args[1]);
case "client" -> ClientApp.start(args[1]);
case "client" -> {
String address = args[1];
String username = args.length >= MIN_ARGS_FOR_USERNAME ? args[2] : null;
ClientApp.start(address, username);
}
default -> {
printUsage();
System.exit(1);
@@ -29,12 +38,15 @@ public final class Main {
}
private static boolean isValid(String[] args) {
if (args.length != 2) {
if (args.length < ARGS_COUNT_SERVER) {
return false;
}
return switch (args[0]) {
case "server", "client" -> true;
case "server" -> args.length == ARGS_COUNT_SERVER;
case "client" ->
args.length == ARGS_COUNT_CLIENT_MIN
|| args.length == ARGS_COUNT_CLIENT_WITH_USER;
default -> false;
};
}
@@ -45,7 +57,7 @@ public final class Main {
"""
Usage:
java -jar xyz.jar server <listenPort>
java -jar xyz.jar client <serverIp>:<serverPort>
java -jar xyz.jar client <serverIp>:<serverPort> [<username>]
""");
}
}
@@ -1,33 +1,51 @@
package ch.unibas.dmi.dbis.cs108.casono.client;
/**
* Entry point for the Casono client application. Handles client startup and connection parameters.
*/
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LoginResult;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.Launcher;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Entry point for the Casono client application. Handles client startup and connection parameters.
* Entry point and bootstrap helper for the Casono client application.
*
* <p>Default constructor for the application.
* <p>This class is responsible for two tasks: - performing an optional startup LOGIN when a
* username is supplied on the command line, and - providing a shared {@link ClientService} instance
* that the UI can reuse so the initial connection remains active.
*/
public class ClientApp {
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;
/** Default constructor. */
public ClientApp() {
// Default constructor
}
/**
* Returns the shared {@link ClientService} instance created during startup, or {@code null} if
* none exists. The UI may call this to reuse the connection that already performed the initial
* LOGIN.
*/
public static ClientService getSharedClientService() {
return sharedClientService;
}
private static void setSharedClientService(ClientService cs) {
sharedClientService = cs;
}
/**
* Starts the client application with the given address.
*
* @param arg Address in the format "ip:port".
* @throws IllegalArgumentException if the address format is invalid.
*/
public static void start(String arg) {
public static void start(String arg, String username) {
String[] parts = arg.split(":", 2);
if (parts.length != 2) {
throw new IllegalArgumentException("Address must be in format <ip>:<port>.");
@@ -36,16 +54,69 @@ public class ClientApp {
int port = Integer.parseInt(parts[1]);
LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host);
// Expose the chosen host/port to the UI via system properties so controllers
// (which read System.getProperty("casono.server.host"/"casono.server.port"))
// can obtain the correct connection information.
System.setProperty("casono.server.host", host);
System.setProperty("casono.server.port", Integer.toString(port));
// Forward the original address argument to the launcher as well.
Launcher.main(new String[] {arg});
System.setProperty("casono.server.port", String.valueOf(port));
// If a username was provided, create a shared connection and perform
// the initial LOGIN asynchronously on that connection. The connection
// is intentionally NOT closed so the UI can reuse it.
if (username != null && !username.isBlank()) {
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);
LOGGER.info(
"Assigned username='{}' id={}",
res.getUsername(),
res.getId());
} 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());
// UI will create its own connection when it initializes
}
}
if (username != null && !username.isBlank()) {
Launcher.main(new String[] {arg, username});
} else {
Launcher.main(new String[] {arg});
}
}
/**
* Main entry point. {@code <host:port>} (e.g. {@code 127.0.0.1:1234})
*
* <p>If a username is provided the application attempts a startup LOGIN on the shared
* connection; the UI will still start and will reuse the connection when possible. The username
* is not propagated via a system property for UI display.
*/
public static void main(String[] args) {
start(args[0]);
if (args == null || args.length == 0) {
throw new IllegalArgumentException("Address argument required: <host:port>");
}
// Optional username argument
String username = args.length > 1 && args[1] != null && !args[1].isBlank() ? args[1] : null;
start(args[0], username);
}
}
@@ -2,6 +2,53 @@ package ch.unibas.dmi.dbis.cs108.casono.client.game;
/** Represents a playing card with a value and suit. */
public class Card {
public String value;
public String suit;
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,107 @@
package ch.unibas.dmi.dbis.cs108.casono.client.game;
import ch.unibas.dmi.dbis.cs108.casono.client.network.GameClient;
import java.util.List;
/**
* Service class responsible for managing the game state and providing methods to interact with the
* game.
*/
public class GameService {
private final GameClient client;
private GameState state;
/**
* Constructs a GameService with the given GameClient for communication with the server.
*
* @param client The GameClient instance used to send commands and receive responses from the
* server.
*/
public GameService(GameClient client) {
this.client = client;
}
/**
* Refresh the game state by fetching the latest state from the server using the GameClient.
*
* @return The updated GameState object representing the current state of the game after
* refreshing.
*/
public GameState refresh() {
state = client.fetchGameState();
return state;
}
/**
* Get the current game state.
*
* @return The current GameState object representing the state of the game.
*/
public int getPot() {
return state.pot;
}
/**
* Get the current game state.
*
* @return The current GameState object representing the state of the game.
*/
public List<Card> getCommunityCards() {
return state.communityCards;
}
/**
* Get the list of players in the current game state.
*
* @return A list of Player objects representing the players in the current game state.
*/
public List<Player> getPlayers() {
return state.players;
}
/** Send a CALL command to the server to indicate that the player wants to call. */
public void call() {
client.sendCall();
}
/** Send a FOLD command to the server to indicate that the player wants to fold. */
public void fold() {
client.sendFold();
}
/**
* Send a BET command to the server to indicate that the player wants to bet with the specified
* amount.
*
* @param amount The amount the player wants to bet.
*/
public void bet(int amount) {
client.sendBet(amount);
}
/**
* Send a RAISE command to the server to indicate that the player wants to raise to the
* specified amount.
*
* @param amount The amount the player wants to raise to.
*/
public void raise(int amount) {
client.sendRaise(amount);
}
/**
* Get the winner of the game if there is one. If the game is still ongoing or if there is no
* winner, this method returns null.
*
* @return The Player object representing the winner of the game, or null if there is no winner
* yet.
*/
public Player getWinner() {
if (state.winnerIndex < 0) {
return null;
}
return state.players.get(state.winnerIndex);
}
}
@@ -13,6 +13,7 @@ public class GameState {
public int currentBet;
public int dealer;
public int activePlayer;
public int winnerIndex = -1;
public List<Card> communityCards = new ArrayList<>();
public List<Player> players = new ArrayList<>();
@@ -8,10 +8,186 @@ import java.util.List;
* (e.g., "active", "folded"), and their hole cards.
*/
public class Player {
public String name;
public int chips;
public int bet;
public String state;
public List<Card> cards = new ArrayList<>();
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
}
@@ -138,33 +138,64 @@ public class ClientService {
() -> {
try {
writeToTransport(message);
String responseText = null;
String responseText = clienttcptransport.read().payload();
logger.info("Raw message '{}'", responseText);
responseText = clienttcptransport.read().payload();
logger.info("Raw message '" + responseText + "'");
Boolean success = null;
int count = 0;
for (String line : responseText.split("\n")) {
if (success == null) {
boolean hasStatus = false;
boolean success = false;
for (String rawLine : responseText.split("\n")) {
String line = rawLine;
if (!hasStatus) {
if ("+OK".equals(line)) {
success = true;
hasStatus = true;
continue;
} else if (("-ERROR").equals(responseText)) {
success = false;
}
if (line.startsWith("-ERR") || line.startsWith("-ERROR")) {
success = false;
hasStatus = true;
continue;
}
// ignore any lines before the status indicator
continue;
} else if ("END".equals(line)) {
}
if ("END".equals(line)) {
break;
}
line = line.replaceFirst("^\t", "");
// strip a single leading tab if present (protocol formatting)
if (line.startsWith("\t")) {
line = line.substring(1);
}
response.add(line);
}
if (success != null && success) {
return;
} else {
throw new RuntimeException("Error in " + message + ": " + response);
if (!hasStatus) {
// Fallback for servers that do not place the status on a
// dedicated line: try to detect status markers anywhere
// in the payload to remain compatible with older servers.
if (responseText.contains("+OK")) {
success = true;
hasStatus = true;
} else if (responseText.contains("-ERR")
|| responseText.contains("-ERROR")) {
success = false;
hasStatus = true;
} else {
throw new RuntimeException(
"No status line in response for '"
+ message
+ "': "
+ responseText);
}
}
if (success) {
return;
}
throw new RuntimeException("Error in " + message + ": " + response);
} catch (Exception e) {
throw getRuntimeException(e);
}
@@ -200,15 +231,14 @@ public class ClientService {
* @return A RuntimeException representing the cause of the original exception.
*/
private static RuntimeException getRuntimeException(Exception e) {
Throwable reason = e.getCause();
RuntimeException re;
if (reason == null) {
reason = e;
} else if (reason instanceof RuntimeException rte) {
re = rte;
Throwable cause = e.getCause();
if (cause == null) {
return e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);
}
re = new RuntimeException(reason);
return re;
if (cause instanceof RuntimeException) {
return (RuntimeException) cause;
}
return new RuntimeException(cause);
}
/**
@@ -3,6 +3,8 @@ 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.List;
/**
@@ -13,6 +15,7 @@ import java.util.List;
public class GameClient {
private final ClientService client;
private final int gameId;
/**
* Constructs a GameClient with the given ClientService for communication.
@@ -20,80 +23,290 @@ public class GameClient {
* @param client The ClientService instance used to send commands and receive responses from the
* server.
*/
public GameClient(ClientService client) {
public GameClient(ClientService client, int gameId) {
this.client = client;
this.gameId = gameId;
}
/**
* Retrieves the current game state from the server by sending a command and parsing the
* response.
* Fetch the current game state from the server by sending a "GET_GAME_STATE"
*
* @return A GameState object representing the current state of the game.
* @return A GameState object representing the current state of the game, as parsed
*/
public GameState getGameState() {
List<String> response = client.processCommand("GET_GAME_STATE");
return parseGameState(response);
public GameState fetchGameState() {
List<String> responseLines = client.processCommand("GET_GAME_STATE\nGAME_ID=" + gameId);
if (responseLines == null || responseLines.isEmpty()) {
throw new RuntimeException("Empty server response");
}
String fullResponse = String.join("\n", responseLines);
return parse(fullResponse);
}
/** Send a CALL command to the server to indicate that the player wants to call */
public void sendCall() {
client.processCommand("CALL\nGAME_ID=" + gameId);
}
/** Send a FOLD command to the server to indicate that the player wants to fold */
public void sendFold() {
client.processCommand("FOLD\nGAME_ID=" + gameId);
}
/** Send a BET command to the server to indicate that the player wants to bet */
public void sendBet(int amount) {
client.processCommand("BET\nGAME_ID=" + gameId + "\nAMOUNT=" + amount);
}
/**
* Parses the raw response from the server into a structured GameState object.
* Send a RAISE command to the server to indicate that the player wants to raise
*
* @param input The raw response string from the server.
* @param amount The amount to raise to
*/
public void sendRaise(int amount) {
client.processCommand("RAISE\nGAME_ID=" + gameId + "\nAMOUNT=" + amount);
}
/**
* Parses the raw server response string into a structured GameState object.
*
* @param input The raw server response string containing the game state information.
* @return A GameState object representing the current state of the game.
*/
private GameState parseGameState(List<String> input) {
private GameState parse(String input) {
GameState state = new GameState();
// String[] lines = input.split("\n");
ParserContext ctx = new ParserContext(state);
Player currentPlayer = null;
Card currentCard = null;
for (String raw : input.split("\n")) {
String line = raw.trim();
for (String rawLine : input) {
String line = rawLine.trim();
if (line.startsWith("+OK") || line.equals("END")) {
if (shouldSkip(line)) {
continue;
}
if (line.startsWith("PHASE=")) {
state.phase = line.split("=")[1];
} else if (line.startsWith("POT=")) {
state.pot = Integer.parseInt(line.split("=")[1]);
} else if (line.startsWith("CURRENT_BET=")) {
state.currentBet = Integer.parseInt(line.split("=")[1]);
} else if (line.startsWith("DEALER=")) {
state.dealer = Integer.parseInt(line.split("=")[1]);
} else if (line.startsWith("ACTIVE_PLAYER=")) {
state.activePlayer = Integer.parseInt(line.split("=")[1]);
} else if (line.startsWith("PLAYER")) {
currentPlayer = new Player();
state.players.add(currentPlayer);
} else if (line.startsWith("NAME=") && currentPlayer != null) {
currentPlayer.name = line.split("=")[1];
} else if (line.startsWith("CHIPS=") && currentPlayer != null) {
currentPlayer.chips = Integer.parseInt(line.split("=")[1]);
} else if (line.startsWith("BET=") && currentPlayer != null) {
currentPlayer.bet = Integer.parseInt(line.split("=")[1]);
} else if (line.startsWith("STATE=") && currentPlayer != null) {
currentPlayer.state = line.split("=")[1];
} else if (line.startsWith("CARD")) {
currentCard = new Card();
if (handleGlobal(line, ctx)) {
continue;
}
if (currentPlayer != null) {
currentPlayer.cards.add(currentCard);
} else {
state.communityCards.add(currentCard);
}
} else if (line.startsWith("VALUE=") && currentCard != null) {
currentCard.value = line.split("=")[1];
} else if (line.startsWith("SUIT=") && currentCard != null) {
currentCard.suit = line.split("=")[1];
if (handleSections(line, ctx)) {
continue;
}
if (handlePlayer(line, ctx)) {
continue;
}
if (handleCards(line, ctx)) {
continue;
}
}
return state;
}
/** Holds the mutable parsing state while processing the server response. */
private static class ParserContext {
GameState state;
Player currentPlayer;
Card currentCard;
boolean inPlayers;
boolean inPlayerCards;
boolean inCommunityCards;
ParserContext(GameState state) {
this.state = state;
}
}
/**
* Checks whether a line should be ignored.
*
* @param line the current input line
* @return true if the line is empty or a status message, false otherwise
*/
private boolean shouldSkip(String line) {
return line.isEmpty() || line.startsWith("+OK");
}
/**
* Parses global game state properties (e.g., phase, pot, current bet).
*
* @param line the current input line
* @param ctx the parser context containing the game state
* @return true if the line was handled, false otherwise
*/
private boolean handleGlobal(String line, ParserContext ctx) {
GameState state = ctx.state;
if (line.startsWith("PHASE=")) {
state.phase = value(line);
} else if (line.startsWith("POT=")) {
state.pot = intVal(line);
} else if (line.startsWith("CURRENT_BET=")) {
state.currentBet = intVal(line);
} else if (line.startsWith("DEALER=")) {
state.dealer = intVal(line);
} else if (line.startsWith("ACTIVE_PLAYER=")) {
state.activePlayer = intVal(line);
} else if (line.startsWith("WINNER=")) {
state.winnerIndex = intVal(line);
} else {
return false;
}
return true;
}
/**
* Processes section markers such as PLAYERS, PLAYER, CARDS, and END. Updates the parser context
* accordingly.
*
* @param line the current input line
* @param ctx the parser context
* @return true if the line was handled, false otherwise
*/
private boolean handleSections(String line, ParserContext ctx) {
if (line.equals("END")) {
ctx.inPlayers = false;
ctx.inPlayerCards = false;
ctx.inCommunityCards = false;
ctx.currentPlayer = null;
ctx.currentCard = null;
return true;
}
if (line.equals("PLAYERS")) {
ctx.inPlayers = true;
return true;
}
if (line.equals("PLAYER")) {
ctx.currentPlayer = new Player();
ctx.state.players.add(ctx.currentPlayer);
return true;
}
if (line.equals("CARDS")) {
if (ctx.inPlayers && ctx.currentPlayer != null) {
ctx.inPlayerCards = true;
ctx.inCommunityCards = false;
} else {
ctx.inCommunityCards = true;
ctx.inPlayerCards = false;
}
return true;
}
return false;
}
/**
* Parses properties of the current player (e.g., name, chips, bet, state).
*
* @param line the current input line
* @param ctx the parser context
* @return true if the line was handled, false otherwise
*/
private boolean handlePlayer(String line, ParserContext ctx) {
Player p = ctx.currentPlayer;
if (p == null) {
return false;
}
if (line.startsWith("NAME=")) {
p.setId(PlayerId.of(value(line)));
} else if (line.startsWith("CHIPS=")) {
p.setChips(intVal(line));
} else if (line.startsWith("BET=")) {
p.setBet(intVal(line));
} else if (line.startsWith("STATE=")) {
p.setState(parseState(value(line)));
} else {
return false;
}
return true;
}
/**
* Parses card-related lines and assigns cards to the current player or the community cards.
*
* @param line the current input line
* @param ctx the parser context
* @return true if the line was handled, false otherwise
*/
private boolean handleCards(String line, ParserContext ctx) {
if (line.startsWith("CARD")) {
ctx.currentCard = new Card("", "");
if (ctx.inPlayerCards && ctx.currentPlayer != null) {
ctx.currentPlayer.addCard(ctx.currentCard);
} else if (ctx.inCommunityCards) {
ctx.state.communityCards.add(ctx.currentCard);
}
return true;
}
if (line.startsWith("VALUE=") && ctx.currentCard != null) {
ctx.currentCard.setValue(value(line));
return true;
}
if (line.startsWith("SUIT=") && ctx.currentCard != null) {
ctx.currentCard.setSuit(value(line));
return true;
}
return false;
}
/**
* Helper method to extract the value from a line in the format KEY=VALUE.
*
* @param line The input line from which to extract the value, expected to be in the format
* KEY=VALUE.
* @return The extracted value part of the input line, which is the substring after the first
* '=' character.
*/
private String value(String line) {
return line.split("=", 2)[1];
}
/**
* Helper method to extract an integer value from a line in the format KEY=VALUE.
*
* @param line The input line from which to extract the integer value, expected to be in the
* format KEY=VALUE where VALUE is an integer.
* @return The extracted integer value from the input line, parsed from the substring after the
* first '=' character.
*/
private int intVal(String line) {
return Integer.parseInt(value(line));
}
/**
* Helper method to parse a string representation of a player's state into the corresponding
* PlayerState enum value.
*
* @param s The input string representing the player's state, expected to be one of ACTIVE,
* FOLDED, or DEALER (case-insensitive).
* @return The corresponding PlayerState enum value based on the input string. If the input does
* not match any known state, it defaults to PlayerState.ACTIVE.
*/
private PlayerState parseState(String s) {
return switch (s.toUpperCase()) {
case "ACTIVE" -> PlayerState.ACTIVE;
case "FOLDED" -> PlayerState.FOLDED;
case "DEALER" -> PlayerState.DEALER;
default -> PlayerState.ACTIVE;
};
}
}
@@ -1,5 +1,8 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
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
@@ -65,8 +68,23 @@ public class LobbyClient {
* 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 void login(String user) {
client.processCommand("LOGIN USERNAME=" + user);
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);
}
}
@@ -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;
}
}
@@ -1,7 +1,19 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
import ch.unibas.dmi.dbis.cs108.casono.client.game.Card;
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameService;
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameState;
import ch.unibas.dmi.dbis.cs108.casono.client.game.Player;
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.PlayerStatusController;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.TaskbarController;
import java.util.List;
import java.util.logging.Logger;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
/**
@@ -15,24 +27,853 @@ import javafx.scene.layout.VBox;
*/
public class CasinoGameController {
private static final Logger LOGGER = Logger.getLogger(CasinoGameController.class.getName());
@FXML private Label tableText;
@FXML private VBox casinoTable;
@FXML private HBox communityCardsBox;
@FXML private VBox casinoTableInnerBox;
@FXML private HBox playerCardsBox;
@FXML private HBox potBox;
@FXML private PlayerStatusController playerStatusController;
@FXML private PlayerStatusController player1Controller;
@FXML private PlayerStatusController player2Controller;
@FXML private PlayerStatusController player3Controller;
@FXML private VBox player1;
@FXML private VBox player2;
@FXML private VBox player3;
@FXML private ImageView myDealerIcon;
private GameService gameService;
private PlayerId myPlayerId;
private TaskbarController taskbarController;
private Image dealerImage;
private static final int TOTAL_SLOTS = 5;
private static final int PLAYER_SLOTS = 2;
private int pot = 0;
private static final String BACKSIDE = "/images/card-background-3.png";
private static final String DEALER_IMAGE_PATH = "/images/chip-dealer-blue-3.png";
private static final double TABLE_OFFSET_Y_FACTOR = 0.09;
private static final double PLAYER_CARDS_OFFSET_Y_FACTOR = 0.35;
private static final double MOVE_FROM_X = -40;
private static final double MOVE_FROM_Y = -10;
private static final double MOVE_TO_X = 0;
private static final double MOVE_TO_Y = 0;
private static final double FADE_FROM = 0;
private static final double FADE_TO = 1;
private static final double SCALE_FROM = 0.95;
private static final double SCALE_TO = 1;
private static final double SETTLE_FROM_Y = 0;
private static final double SETTLE_TO_Y = 10;
private static final boolean SETTLE_AUTO_REVERSE = true;
private static final int SETTLE_CYCLE_COUNT = 2;
private static final long ANIMATION_DURATION_MS = 350;
private static final long SETTLE_DURATION_MS = 150;
private static final long STAGGER_DELAY_MS = 120;
private static final double HOVER_SCALE_ON = 1.08;
private static final double HOVER_SCALE_OFF = 1.0;
private static final double HOVER_LIFT_ON = -8;
private static final double HOVER_LIFT_OFF = 0;
private static final long HOVER_DURATION_MS = 180;
private static final long UI_UPDATE_INTERVAL_SECONDS = 1;
private static final int PLAYER_INDEX_0 = 0;
private static final int PLAYER_INDEX_1 = 1;
private static final int PLAYER_INDEX_2 = 2;
private static final int DEALER_INDEX_PLAYER_1 = 0;
private static final int DEALER_INDEX_PLAYER_2 = 1;
private static final int DEALER_INDEX_PLAYER_3 = 2;
private static final int DEALER_PLAYER_1 = 0;
private static final int DEALER_PLAYER_2 = 1;
private static final int DEALER_PLAYER_3 = 2;
private static final int DEALER_MYSELF = 3;
private static final double CARD_START_OPACITY = 0.0;
private static final double CARD_START_SCALE = 0.85;
private static final double COMMUNITY_CARD_HEIGHT_RATIO = 0.22;
private static final double COMMUNITY_CARD_WIDTH_RATIO = 0.11;
private static final double PLAYER_CARD_HEIGHT_RATIO = 0.30;
private static final double PLAYER_CARD_WIDTH_RATIO = 0.15;
private static final double PLAYER_CARDS_Y_OFFSET_FACTOR = 0.10;
private static final int[] CHIP_VALUES = {
100000, 50000, 20000, 10000,
5000, 2000, 1000, 500,
200, 100, 50, 20,
10, 5, 2, 1
};
private static final double CHIP_HEIGHT_RATIO = 0.08;
private static final double CHIP_WIDTH_RATIO = 0.04;
private static final double CHIP_OPACITY_START = 0.0;
private static final double CHIP_SCALE_START = 0.6;
private static final double CHIP_SCALE_END = 1.0;
private static final double CHIP_RANDOM_X_FACTOR = 20.0;
private static final double CHIP_RANDOM_X_CENTER = 0.5;
private static final double CHIP_RANDOM_Y_BASE = -30.0;
private static final double CHIP_RANDOM_Y_VARIATION = 20.0;
private static final double CHIP_TRANSLATE_RESET_X = 0.0;
private static final double CHIP_TRANSLATE_RESET_Y = 0.0;
private static final double CHIP_FADE_TO = 1.0;
private static final double CHIP_SCALE_TO = 1.0;
private static final long CHIP_FADE_DURATION_MS = 200;
private static final long CHIP_SCALE_DURATION_MS = 220;
private static final long CHIP_DROP_DURATION_MS = 250;
private static final long CHIP_STAGGER_DELAY_MULTIPLIER = 35L;
/** Standard constructor. Used by FXML. */
public CasinoGameController() {
// default constructor for FXML
}
@FXML private Label welcomeText;
@FXML private VBox casinoTable;
/**
* Set the GameService for this controller. This method must be called before starting the UI to
* ensure that the controller has access to the game logic and can update the interface
* accordingly.
*
* @param gameService The GameService instance that provides access to the game state and logic.
*/
public void setGameService(GameService gameService) {
this.gameService = gameService;
}
// TODO: Test logic: will be replaced by real game interactions,
// once the game engine is finished
/** Set the PlayerId of the current player. */
@FXML
public void initialize() {
LOGGER.info("INIT UI");
if (communityCardsBox == null) {
LOGGER.warning("communityCardsBox is NULL");
return;
}
try {
var url = getClass().getResource(DEALER_IMAGE_PATH);
if (url != null) {
dealerImage = new Image(url.toExternalForm(), true);
myDealerIcon.setImage(dealerImage);
}
} catch (Exception e) {
LOGGER.warning("Dealer icon load failed");
}
myDealerIcon.setVisible(false);
javafx.stage.Screen screen = javafx.stage.Screen.getPrimary();
double screenHeight = screen.getBounds().getHeight();
casinoTableInnerBox.setTranslateY(-screenHeight * TABLE_OFFSET_Y_FACTOR);
playerCardsBox.setTranslateY(screenHeight * PLAYER_CARDS_OFFSET_Y_FACTOR);
// uiTest();
// empty display only (optional)
renderCommunityCards(List.of());
renderPlayerCards(List.of());
}
// Test method to demonstrate the UI functionality with sample data.
// @FXML
// public void uiTest() {
// if (communityCardsBox == null) {
// LOGGER.info("communityCardsBox is NULL");
// return;
// }
//
// try {
// renderCommunityCards(
// List.of(
// new Card("ace", "hearts"),
// new Card("king", "spades"),
// new Card("10", "diamonds")));
//
// renderPlayerCards(List.of(new Card("10", "spades"), new Card("king", "spades")));
// } catch (Exception e) {
// e.printStackTrace();
// }
//
// Player mathis = new Player(PlayerId.of("Mathis"), 20000);
// mathis.setState(PlayerState.ACTIVE);
//
// Player jona = new Player(PlayerId.of("Jona"), 20000);
// jona.setState(PlayerState.FOLDED);
//
// Player lars = new Player(PlayerId.of("Lars"), 20000);
// lars.setState(PlayerState.ACTIVE);
//
// player1Controller.setPlayer(mathis);
// player2Controller.setPlayer(jona);
// player3Controller.setPlayer(lars);
//
// LOGGER.info(
// "Player 1: "
// + mathis.getName()
// + " | $"
// + mathis.getChips()
// + " | "
// + mathis.getState());
// LOGGER.info(
// "Player 2: "
// + jona.getName()
// + " | $"
// + jona.getChips()
// + " | "
// + jona.getState());
// LOGGER.info(
// "Player 3: "
// + lars.getName()
// + " | $"
// + lars.getChips()
// + " | "
// + lars.getState());
//
// if (playerStatusController != null) {
// playerStatusController.setPlayer(mathis);
// } else {
// LOGGER.warning("PlayerStatusController is NULL");
// }
//
// javafx.stage.Screen screen = javafx.stage.Screen.getPrimary();
// double screenHeight = screen.getBounds().getHeight();
//
// casinoTableInnerBox.setTranslateY(-screenHeight * 0.08);
// playerCardsBox.setTranslateY(screenHeight * 0.35);
//
// renderPot(500);
//
// lars.removeChips(500);
// LOGGER.info("Lars: $" + lars.getChips());
//
// player3Controller.refresh();
//
// mathis.fall();
// player1Controller.refresh();
//
// GameState s = new GameState();
// s.dealer = 3;
// highlightDealer(s);
//
// s.phase = "FLOP";
//
// updateGameInfo(s);
//
// s.winnerIndex = 1;
//
// updateGameInfo(s);
// }
/** Start the UI update loop. */
public void start() {
if (gameService == null) {
throw new IllegalStateException("GameService not set!");
}
startLoop();
}
/**
* Temporary test method that performs a placeholder action when the table is clicked.
*
* <p>In the final implementation, this will be replaced by the game logic.
* Start a loop that periodically updates the UI by fetching the latest game state from the
* GameService.
*/
@FXML
public void onTableClick() {
welcomeText.setText("Einsatz akzeptiert!");
private void startLoop() {
javafx.animation.Timeline t =
new javafx.animation.Timeline(
new javafx.animation.KeyFrame(
javafx.util.Duration.seconds(UI_UPDATE_INTERVAL_SECONDS),
e -> updateUI()));
t.setCycleCount(javafx.animation.Animation.INDEFINITE);
t.play();
}
/**
* Update the UI by fetching the latest game state from the GameService and updating all
* relevant components.
*/
private void updateUI() {
try {
GameState s = gameService.refresh();
if (s == null) {
return;
}
renderCommunityCards(s.communityCards);
renderPot(s.pot);
updatePlayers(s.players);
updatePlayerCards(s);
updateGameInfo(s);
highlightDealer(s);
updateTaskbar(s);
} catch (Exception e) {
LOGGER.severe("Error: UI Update failed: " + e.getMessage());
}
}
/**
* Update the player's hole cards based on the active player in the game state.
*
* @param s The current game state.
*/
private void updatePlayerCards(GameState s) {
int active = s.activePlayer;
if (active >= 0 && active < s.players.size()) {
Player p = s.players.get(active);
renderPlayerCards(p.getCards());
} else {
renderPlayerCards(List.of());
}
}
/**
* Update the game information display, such as the current phase and the winner if the hand has
* ended.
*
* @param s The current game state.
*/
private void updateGameInfo(GameState s) {
String text = "Phase: " + s.phase;
if (s.winnerIndex >= 0 && s.winnerIndex < s.players.size()) {
Player winner = s.players.get(s.winnerIndex);
text = "Winner: " + winner.getName();
}
tableText.setText(text);
}
/**
* Update the taskbar with the current game state and the player's ID.
*
* @param s The current game state.
*/
private void updateTaskbar(GameState s) {
taskbarController.update(s, myPlayerId);
}
/**
* Update the player status components with the latest player information from the game state.
*
* @param p The list of players in the current game state.
*/
private void updatePlayers(List<Player> p) {
if (p == null) {
return;
}
if (p.size() > PLAYER_INDEX_0) {
player1Controller.setPlayer(p.get(PLAYER_INDEX_0));
}
if (p.size() > PLAYER_INDEX_1) {
player2Controller.setPlayer(p.get(PLAYER_INDEX_1));
}
if (p.size() > PLAYER_INDEX_2) {
player3Controller.setPlayer(p.get(PLAYER_INDEX_2));
}
player1Controller.refresh();
player2Controller.refresh();
player3Controller.refresh();
}
/**
* Highlight the dealer in the UI based on the dealer index in the game state.
*
* @param s The current game state containing the dealer index.
*/
private void highlightDealer(GameState s) {
player1Controller.setDealer(false);
player2Controller.setDealer(false);
player3Controller.setDealer(false);
myDealerIcon.setVisible(false);
int dealer = s.dealer;
if (dealer == DEALER_PLAYER_1) {
player1Controller.setDealer(true);
}
if (dealer == DEALER_PLAYER_2) {
player2Controller.setDealer(true);
}
if (dealer == DEALER_PLAYER_3) {
player3Controller.setDealer(true);
}
if (dealer == DEALER_MYSELF) {
myDealerIcon.setVisible(true);
}
}
/**
* Render the community cards on the table based on the list of cards provided.
*
* @param cards The list of community cards to display.
*/
private void renderCommunityCards(List<Card> cards) {
communityCardsBox.getChildren().clear();
if (cards == null) {
cards = List.of();
}
for (int i = 0; i < TOTAL_SLOTS; i++) {
ImageView view = new ImageView();
styleCommunityCard(view);
Image image;
if (i < cards.size() && cards.get(i) != null) {
image = loadImageSafe(mapCardToImage(cards.get(i)));
} else {
image = loadImageSafe(BACKSIDE);
}
view.setImage(image);
view.setOnMouseEntered(e -> animateCardHover(view, true));
view.setOnMouseExited(e -> animateCardHover(view, false));
view.setOpacity(CARD_START_OPACITY);
view.setScaleX(CARD_START_SCALE);
view.setScaleY(CARD_START_SCALE);
communityCardsBox.getChildren().add(view);
animateCardAppear(view, i);
}
}
/**
* Render the player's hole cards based on the list of cards provided.
*
* @param cards The list of hole cards to display for the player.
*/
private void renderPlayerCards(List<Card> cards) {
playerCardsBox.getChildren().clear();
playerCardsBox.getChildren().add(myDealerIcon);
if (cards == null) {
cards = List.of();
}
for (int i = 0; i < PLAYER_SLOTS; i++) {
ImageView view = new ImageView();
stylePlayerCard(view);
Image image;
if (i < cards.size() && cards.get(i) != null) {
image = loadImageSafe(mapCardToImage(cards.get(i)));
} else {
image = loadImageSafe(BACKSIDE);
}
view.setImage(image);
view.setOnMouseEntered(e -> animateCardHover(view, true));
view.setOnMouseExited(e -> animateCardHover(view, false));
view.setOpacity(CARD_START_OPACITY);
view.setScaleX(CARD_START_SCALE);
view.setScaleY(CARD_START_SCALE);
playerCardsBox.getChildren().add(view);
animateCardAppear(view, i);
}
}
/**
* Load an image from the given path safely, falling back to a default backside image if the
* specified image cannot be found.
*
* @param path The path to the image resource to load.
* @return The loaded Image object.
*/
private Image loadImageSafe(String path) {
var stream = getClass().getResourceAsStream(path);
if (stream == null) {
LOGGER.warning("IMAGE NOT FOUND: " + path);
stream = getClass().getResourceAsStream(BACKSIDE);
}
return new Image(stream);
}
/**
* Apply styling to the given ImageView for community cards, including CSS classes and size
* bindings.
*
* @param view The ImageView to style as a community card.
*/
private void styleCommunityCard(ImageView view) {
view.getStyleClass().add("community-card");
view.setPreserveRatio(true);
view.setSmooth(true);
javafx.scene.Scene scene = communityCardsBox.getScene();
if (scene != null) {
bindCommunityCardSize(view, scene);
} else {
communityCardsBox
.sceneProperty()
.addListener(
(obs, oldScene, newScene) -> {
if (newScene != null) {
bindCommunityCardSize(view, newScene);
}
});
}
}
/**
* Apply styling to the given ImageView for player hole cards, including CSS classes and size
* bindings.
*
* @param view The ImageView to style as a player hole card.
*/
private void stylePlayerCard(ImageView view) {
view.getStyleClass().add("player-card");
view.setPreserveRatio(true);
view.setSmooth(true);
javafx.scene.Scene scene = playerCardsBox.getScene();
if (scene != null) {
bindPlayerCardSize(view, scene);
} else {
playerCardsBox
.sceneProperty()
.addListener(
(obs, oldScene, newScene) -> {
if (newScene != null) {
bindPlayerCardSize(view, newScene);
}
});
}
}
/**
* Bind the size of the given ImageView to the scene dimensions for community cards.
*
* @param view The ImageView representing a community card whose size should be bound to the
* scene dimensions.
* @param scene The JavaFX Scene to which the ImageView belongs, used for binding the size
* properties.
*/
private void bindCommunityCardSize(ImageView view, javafx.scene.Scene scene) {
view.fitHeightProperty().bind(scene.heightProperty().multiply(COMMUNITY_CARD_HEIGHT_RATIO));
view.fitWidthProperty().bind(scene.widthProperty().multiply(COMMUNITY_CARD_WIDTH_RATIO));
}
/**
* Bind the size of the given ImageView to the scene dimensions for player hole cards.
*
* @param view The ImageView representing a player hole card whose size should be bound to the
* scene dimensions.
* @param scene The JavaFX Scene to which the ImageView belongs, used for binding the size
* properties.
*/
private void bindPlayerCardSize(ImageView view, javafx.scene.Scene scene) {
view.fitHeightProperty().bind(scene.heightProperty().multiply(PLAYER_CARD_HEIGHT_RATIO));
view.fitWidthProperty().bind(scene.widthProperty().multiply(PLAYER_CARD_WIDTH_RATIO));
}
/**
* Animate the appearance of a card by applying a sequence of transitions, including movement,
* fading, scaling, and settling.
*
* @param view The ImageView representing the card to animate.
* @param index The index of the card in the display order.
*/
private void animateCardAppear(ImageView view, int index) {
javafx.animation.TranslateTransition move =
new javafx.animation.TranslateTransition(
javafx.util.Duration.millis(ANIMATION_DURATION_MS), view);
move.setFromX(MOVE_FROM_X);
move.setToX(MOVE_TO_X);
move.setFromY(MOVE_FROM_Y);
move.setToY(MOVE_TO_Y);
move.setInterpolator(javafx.animation.Interpolator.EASE_OUT);
javafx.animation.FadeTransition fade =
new javafx.animation.FadeTransition(
javafx.util.Duration.millis(ANIMATION_DURATION_MS), view);
fade.setFromValue(FADE_FROM);
fade.setToValue(FADE_TO);
javafx.animation.ScaleTransition scale =
new javafx.animation.ScaleTransition(
javafx.util.Duration.millis(ANIMATION_DURATION_MS), view);
scale.setFromX(SCALE_FROM);
scale.setFromY(SCALE_FROM);
scale.setToX(SCALE_TO);
scale.setToY(SCALE_TO);
scale.setInterpolator(javafx.animation.Interpolator.EASE_OUT);
javafx.animation.TranslateTransition settle =
new javafx.animation.TranslateTransition(
javafx.util.Duration.millis(SETTLE_DURATION_MS), view);
settle.setFromY(SETTLE_FROM_Y);
settle.setToY(SETTLE_TO_Y);
settle.setAutoReverse(SETTLE_AUTO_REVERSE);
settle.setCycleCount(SETTLE_CYCLE_COUNT);
javafx.animation.SequentialTransition st =
new javafx.animation.SequentialTransition(
new javafx.animation.ParallelTransition(move, fade, scale), settle);
st.setDelay(javafx.util.Duration.millis(index * STAGGER_DELAY_MS));
st.play();
}
/**
* Animate a card hover effect by scaling and lifting the card when hovered and resetting it
* when not hovered.
*
* @param view The ImageView representing the card to animate on hover.
* @param hover A boolean indicating whether the card is being hovered (true) or not (false).
*/
private void animateCardHover(ImageView view, boolean hover) {
double scale = hover ? HOVER_SCALE_ON : HOVER_SCALE_OFF;
double lift = hover ? HOVER_LIFT_ON : HOVER_LIFT_OFF;
javafx.animation.ScaleTransition st =
new javafx.animation.ScaleTransition(
javafx.util.Duration.millis(HOVER_DURATION_MS), view);
st.setToX(scale);
st.setToY(scale);
javafx.animation.TranslateTransition tt =
new javafx.animation.TranslateTransition(
javafx.util.Duration.millis(HOVER_DURATION_MS), view);
tt.setToY(lift);
st.play();
tt.play();
}
/**
* Position the player's hole cards box on the screen based on a predefined offset factor
* relative to the screen height.
*/
private void positionPlayerCards() {
javafx.stage.Screen screen = javafx.stage.Screen.getPrimary();
double screenHeight = screen.getBounds().getHeight();
playerCardsBox.setTranslateY(screenHeight * PLAYER_CARDS_Y_OFFSET_FACTOR);
}
/**
* Map a Card object to the corresponding image path based on its suit and value.
*
* @param card The Card object to be mapped to an image path.
* @return The string path to the image representing the given card.
*/
private String mapCardToImage(Card card) {
if (card == null) {
return BACKSIDE;
}
String suit =
switch (card.getSuit().toLowerCase()) {
case "hearts" -> "heart";
case "diamonds" -> "diamond";
case "clubs", "cross" -> "cross";
case "spades", "pik" -> "pik";
default -> card.getSuit().toLowerCase();
};
String value =
switch (card.getValue().toLowerCase()) {
case "a", "ace" -> "ace";
case "k", "king" -> "king";
case "q", "queen" -> "queen";
case "j", "jack" -> "jack";
case "10", "t" -> "10";
default -> card.getValue().toLowerCase();
};
String path = "/images/card-" + suit + "-" + value + "-3.png";
// LOGGER.info("CARD PATH: " + path);
return path;
}
/**
* Render the pot by calculating the required chips based on the pot amount and displaying them
* with animations.
*
* @param pot The total amount in the pot that needs to be represented with chips on the UI.
*/
private void renderPot(int pot) {
potBox.getChildren().clear();
int remaining = pot;
int index = 0;
for (int chipValue : CHIP_VALUES) {
while (remaining >= chipValue) {
ImageView chip =
new ImageView(loadImageSafe("/images/chip-" + chipValue + "-5.png"));
if (chip.getImage() == null) {
LOGGER.warning("Missing chip image: " + chipValue);
break;
}
styleChip(chip);
potBox.getChildren().add(chip);
animateChipAppear(chip, index);
remaining -= chipValue;
index++;
}
}
// LOGGER.info("POT RENDERED: " + pot + " -> remaining: " + remaining);
}
/**
* Apply styling to the given ImageView for chips, including CSS classes and size bindings.
*
* @param view The ImageView to style as a chip in the pot display.
*/
private void styleChip(ImageView view) {
view.getStyleClass().add("chips-icon");
view.setPreserveRatio(true);
view.setSmooth(true);
javafx.scene.Scene scene = potBox.getScene();
if (scene != null) {
bindChipSize(view, scene);
} else {
potBox.sceneProperty()
.addListener(
(obs, oldScene, newScene) -> {
if (newScene != null) {
bindChipSize(view, newScene);
}
});
}
}
/**
* Bind the size of the given ImageView to the scene dimensions for chips in the pot display.
*
* @param view The ImageView representing a chip whose size should be bound to the scene
* dimensions.
* @param scene The JavaFX Scene to which the ImageView belongs, used for binding the size
* properties.
*/
private void bindChipSize(ImageView view, javafx.scene.Scene scene) {
view.fitHeightProperty()
.bind(
scene.heightProperty().multiply(CHIP_HEIGHT_RATIO) // kleiner als Karten
);
view.fitWidthProperty().bind(scene.widthProperty().multiply(CHIP_WIDTH_RATIO));
}
/**
* Animate the appearance of a chip by applying a sequence of transitions, including random
* initial positioning, fading, scaling, and dropping into place.
*
* @param view The ImageView representing the chip to animate.
* @param index The index of the chip in the display order, used to stagger the animation timing
* for multiple chips.
*/
private void animateChipAppear(ImageView view, int index) {
view.setOpacity(CHIP_OPACITY_START);
view.setScaleX(CHIP_SCALE_START);
view.setScaleY(CHIP_SCALE_START);
double randomX = (Math.random() - CHIP_RANDOM_X_CENTER) * CHIP_RANDOM_X_FACTOR;
double randomY = CHIP_RANDOM_Y_BASE - Math.random() * CHIP_RANDOM_Y_VARIATION;
view.setTranslateX(randomX);
view.setTranslateY(randomY);
javafx.animation.FadeTransition fade =
new javafx.animation.FadeTransition(
javafx.util.Duration.millis(CHIP_FADE_DURATION_MS), view);
fade.setToValue(CHIP_FADE_TO);
javafx.animation.ScaleTransition scale =
new javafx.animation.ScaleTransition(
javafx.util.Duration.millis(CHIP_SCALE_DURATION_MS), view);
scale.setToX(CHIP_SCALE_TO);
scale.setToY(CHIP_SCALE_TO);
javafx.animation.TranslateTransition drop =
new javafx.animation.TranslateTransition(
javafx.util.Duration.millis(CHIP_DROP_DURATION_MS), view);
drop.setToX(CHIP_TRANSLATE_RESET_X);
drop.setToY(CHIP_TRANSLATE_RESET_Y);
javafx.animation.SequentialTransition st =
new javafx.animation.SequentialTransition(fade, scale, drop);
st.setDelay(javafx.util.Duration.millis(index * CHIP_STAGGER_DELAY_MULTIPLIER));
st.play();
}
}
@@ -7,26 +7,30 @@ import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
/**
* Main class for the Casono Game UI.
*
* <p>Starts the JavaFX application, loads the graphical user interface from the FXML file, and
* initializes the main stage for the game.
*
* <p>Tasks: - Loads the FXML interface "/ui-structure/Casinogameui.fxml". - Loads the application
* icon from "/images/logoinverted.png". - Starts the application in full-screen mode.
*/
public class CasinoGameUI extends Application {
// Static field for ClientService (workaround for JavaFX Application launch)
private static ClientService staticClientService;
private static ClientService clientService;
public static void setClientService(ClientService clientService) {
staticClientService = clientService;
}
private static final int DEFAULT_WIDTH = 1200;
private static final int DEFAULT_HEIGHT = 800;
public static ClientService getClientService() {
return staticClientService;
}
/** Default no-arg constructor. */
/** default constructor */
public CasinoGameUI() {
// default no-arg constructor
}
private static final int DEFAULT_WIDTH = 1200;
private static final int DEFAULT_HEIGHT = 800;
public static void setClientService(ClientService clientService) {
CasinoGameUI.clientService = clientService;
}
/**
* Starts the main stage of the application.
@@ -39,7 +43,7 @@ public class CasinoGameUI extends Application {
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)");
stage.setTitle("Casono");
String iconPath = getClass().getResource("/images/logoinverted.png").toExternalForm();
stage.getIcons().add(new javafx.scene.image.Image(iconPath));
@@ -49,7 +53,7 @@ public class CasinoGameUI extends Application {
}
/**
* Entry point of the application.
* Starting point of the application.
*
* @param args Command line arguments.
*/
@@ -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;
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 javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
@@ -18,22 +25,225 @@ import org.apache.logging.log4j.Logger;
*/
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 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. */
public TaskbarController() {
// 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;
@FXML private TextField taskbarInput;
if (isOut) {
setRedButton(betButton);
setRedButton(callButton);
setRedButton(foldButton);
setRedButton(raiseButton);
setRedInputField(taskbarInput);
return;
}
private double xOffset = 0;
private double yOffset = 0;
private static final double TASKBAR_SCALE = 0.9;
private static final int MIN_CREDITS = 5;
private static final int MAX_CREDITS = 100000;
private static final int CREDIT_STEP = 5;
if (isMyTurn) {
activateButton(betButton);
activateButton(callButton);
activateButton(foldButton);
activateButton(raiseButton);
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;
}
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,
@@ -72,8 +282,8 @@ public class TaskbarController {
*/
@FXML
private void onTaskbarReleased(MouseEvent event) {
taskbar.setScaleX(1.0);
taskbar.setScaleY(1.0);
taskbar.setScaleX(SCALE_NORMAL);
taskbar.setScaleY(SCALE_NORMAL);
}
/**
@@ -97,6 +307,76 @@ public class TaskbarController {
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() {
String input = taskbarInput.getText();
try {
int amount = Integer.parseInt(input.trim());
gameService.raise(amount);
LOGGER.info("Player RAISE {}", amount);
taskbarInput.clear();
refreshGame();
} catch (NumberFormatException e) {
LOGGER.error("Invalid raise amount");
}
}
/**
* 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
private void onExitButtonClick() {
javafx.application.Platform.runLater(
@@ -109,7 +389,7 @@ public class TaskbarController {
try {
new Casinomainui().start(new javafx.stage.Stage());
} catch (Exception e) {
LOGGER.error("Fehler beim Starten der Lobby-UI: {}", e.getMessage());
LOGGER.error("Error: starting the lobby UI: {}", e.getMessage());
}
});
}
@@ -120,19 +400,34 @@ public class TaskbarController {
* displayed on the console.
*/
private void processBet() {
if (gameService == null) {
LOGGER.error("Error: GameService not initialized");
return;
}
String input = taskbarInput.getText();
try {
int credits = Integer.parseInt(input.trim());
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();
refreshGame();
} 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) {
LOGGER.error("Error: Please enter only one number!");
LOGGER.error("Error: Invalid number entered");
}
}
@@ -1,5 +1,6 @@
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.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
import javafx.application.Platform;
@@ -36,12 +37,16 @@ public class CasinomainuiController {
private int nextButtonId = 1;
private LobbyClient lobbyClient;
/** Default constructor for dependency injection by FXMLLoader. */
/** Default constructor used by FXMLLoader. */
public CasinomainuiController() {
// 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
public void initialize() {
titleLabel.setText("Casono");
@@ -51,16 +56,18 @@ public class CasinomainuiController {
translationManager = LobbyButtonTranslationManager.getInstance();
String host = System.getProperty("casono.server.host");
int port = Integer.parseInt(System.getProperty("casono.server.port"));
ClientService clientService;
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
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 =
new LobbyButtonGridManager(
@@ -117,7 +124,11 @@ public class CasinomainuiController {
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
public void handleCreateLobbyButton() {
if (translationManager.isFull()) {
@@ -24,6 +24,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
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.network.NetworkManager;
@@ -85,7 +86,8 @@ public class ServerApp {
SESSION_DISCONNECT_JOB_PERIOD,
TimeUnit.SECONDS);
registerCommands(dispatcher, router, responseDispatcher, userRegistry);
LobbyManager lobbyManager = new LobbyManager();
registerCommands(dispatcher, router, responseDispatcher, userRegistry, lobbyManager);
NetworkManager networkManager = new NetworkManager(port, sessionManager, router);
networkManager.start();
@@ -102,7 +104,8 @@ public class ServerApp {
CommandParserDispatcher parserDispatcher,
CommandRouter commandRouter,
ResponseDispatcher responseDispatcher,
UserRegistry userRegistry) {
UserRegistry userRegistry,
LobbyManager lobbyManager) {
parserDispatcher.register("PING", new PingParser());
commandRouter.register(PingRequest.class, new PingHandler(responseDispatcher));
@@ -136,5 +139,118 @@ public class ServerApp {
parserDispatcher.register("LIST_USERS", new ListUsersParser());
commandRouter.register(
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, 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, 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, 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, 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, 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, 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, lobbyManager, userRegistry));
// 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, 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,74 @@
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 = request.getUsername();
Lobby lobby = null;
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));
}
}
@@ -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,114 @@
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;
/** Response carrying a snapshot of the current game state using the project's wire format. */
public class GetGameStateResponse extends SuccessResponse {
public GetGameStateResponse(
RequestContext context, GameController game, String requestingUsername) {
super(context, buildBody(game.getState(), requestingUsername));
}
private static ResponseBody buildBody(GameState state, String requestingUsername) {
int globalCurrentBet = computeGlobalCurrentBet(state);
ResponseBodyBuilder builder = ResponseBody.builder();
builder.param("PHASE", state.getPhase() == null ? "UNKNOWN" : state.getPhase().name());
builder.param("POT", state.getPot().getAmount());
builder.param("CURRENT_BET", globalCurrentBet);
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()) {
int b = state.getCurrentBet(p.getId());
if (b > globalCurrentBet) {
globalCurrentBet = b;
}
}
return globalCurrentBet;
}
private static void appendCommunityCards(ResponseBodyBuilder builder, GameState state) {
for (Card c : state.getCommunityCards()) {
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) {
for (Player p : state.getPlayers()) {
PlayerId pid = p.getId();
builder.block(
"PLAYER",
pblock -> {
pblock.param("NAME", pid.value());
pblock.param("CHIPS", p.getChips());
pblock.param("BET", state.getCurrentBet(pid));
pblock.param("STATE", p.isFolded() ? "FOLDED" : "ACTIVE");
if (requestingUsername != null && requestingUsername.equals(pid.value())) {
List<Card> hole = state.getHoleCards(pid);
for (Card hc : hole) {
pblock.block(
"CARD",
cblock -> {
cblock.param("VALUE", rankToWire(hc.getRank()));
cblock.param("SUIT", suitToWire(hc.getSuit()));
});
}
}
});
}
}
private static String rankToWire(Rank r) {
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) {
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,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,45 @@
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()
.block(
"LOBBY",
lb -> {
lb.param("ID", lobby.getId().value());
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,83 @@
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;
/**
* 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;
/**
* 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) {
LobbyId lid = LobbyId.of(request.getId());
var lobby = lobbyManager.getLobby(lid);
if (lobby == null) {
responseDispatcher.dispatch(
new ErrorResponse(request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
return;
}
var maybeUser = userRegistry.getBySessionId(request.getContext().sessionId());
if (maybeUser.isEmpty()) {
// UserLoggedInCheck should normally prevent this; keep safe fallback
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) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(),
"LOBBY_FULL_OR_ALREADY_IN",
"Lobby full or user already in lobby"));
return;
}
responseDispatcher.dispatch(new OkResponse(request.getContext()));
}
}
@@ -0,0 +1,33 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
/**
* Parser for the `JOIN_LOBBY` command.
*
* <p>Expected request parameters:
*
* <ul>
* <li>`ID` (int) — numeric lobby identifier (required)
* </ul>
*
* <p>The parser builds a {@link JoinLobbyRequest} containing the parsed lobby id and the original
* request context.
*/
public class JoinLobbyParser implements CommandParser<JoinLobbyRequest> {
/**
* Parse the incoming primitive request into a {@link JoinLobbyRequest}.
*
* @param primitiveRequest the raw primitive request
* @return a {@link JoinLobbyRequest} with the parsed lobby id and context
*/
@Override
public JoinLobbyRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor =
new RequestParameterAccessor(primitiveRequest.parameters());
int id = accessor.require("ID", Integer::parseInt);
return new JoinLobbyRequest(primitiveRequest.context(), id);
}
}
@@ -0,0 +1,34 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
/**
* Request data for the `JOIN_LOBBY` command.
*
* <p>Contains the lobby id the client wants to join and inherits the {@link Request} contextual
* information.
*/
public class JoinLobbyRequest extends Request {
private final int id;
/**
* Create a new {@link JoinLobbyRequest}.
*
* @param context the request context
* @param id numeric lobby id to join
*/
public JoinLobbyRequest(RequestContext context, int id) {
super(context);
this.id = id;
}
/**
* Returns the lobby id requested by the client.
*
* @return numeric lobby id
*/
public int getId() {
return id;
}
}
@@ -1,5 +1,6 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.game;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.BetAction;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.BlindAction;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.CallAction;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.FoldAction;
@@ -180,6 +181,16 @@ public class GameController {
engine.processAction(new RaiseAction(playerId, amount));
}
/**
* Processes a player's bet action by sending a BetAction to the GameEngine.
*
* @param playerId The ID of the player who is betting.
* @param amount The amount the player is betting.
*/
public void playerBet(PlayerId playerId, int amount) {
engine.processAction(new BetAction(playerId, amount));
}
/**
* Retrieves the current game state from the GameEngine.
*
@@ -0,0 +1,131 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.GameEngine;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.RoundManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.TurnManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.RuleEngine;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
/** Represents a single lobby: id, name, players and an optional GameController. */
public class Lobby {
private static final Logger LOGGER = Logger.getLogger(Lobby.class.getName());
public enum AddResult {
NOT_ADDED,
ADDED,
ADDED_AND_STARTED
}
private final LobbyId id;
private final String name;
private final List<String> playerNames = new CopyOnWriteArrayList<>();
private volatile GameController gameController;
public Lobby(LobbyId id, String name) {
this.id = id;
this.name = name;
}
public LobbyId getId() {
return id;
}
public String getName() {
return name;
}
public List<String> getPlayerNames() {
return List.copyOf(playerNames);
}
/**
* Try to add a player to this lobby.
*
* @return true if added, false if already present or full
*/
public boolean addPlayer(String playerName, int maxPlayers) {
if (playerName == null) {
return false;
}
synchronized (playerNames) {
if (playerNames.contains(playerName)) {
return false;
}
if (playerNames.size() >= maxPlayers) {
return false;
}
playerNames.add(playerName);
return true;
}
}
public boolean removePlayer(String playerName) {
return playerNames.remove(playerName);
}
public void initGame(GameController controller) {
this.gameController = controller;
}
public GameController getGameController() {
return gameController;
}
/**
* Atomically add a player and, if the lobby reached {@code autoStartPlayers} and no game exists
* yet, create and start a game. The operation is synchronized on the internal player list to
* avoid races when multiple joins happen concurrently.
*
* @return {@link AddResult} indicating whether the player was added and if a game was started
*/
public AddResult addPlayerAndMaybeStart(
String playerName, int maxPlayers, int autoStartPlayers, int defaultStartChips) {
if (playerName == null) {
return AddResult.NOT_ADDED;
}
synchronized (playerNames) {
if (playerNames.contains(playerName)) {
return AddResult.NOT_ADDED;
}
if (playerNames.size() >= maxPlayers) {
return AddResult.NOT_ADDED;
}
playerNames.add(playerName);
// If threshold reached and no game running, create and start game here
if (gameController == null && playerNames.size() == autoStartPlayers) {
try {
GameState state = new GameState();
GameEngine engine =
new GameEngine(
state,
new RuleEngine(new ArrayList<>()),
new RoundManager(),
new TurnManager());
GameController game = new GameController(engine);
for (String p : playerNames) {
game.addPlayer(PlayerId.of(p), defaultStartChips);
}
game.startGame();
this.gameController = game;
LOGGER.info(() -> "Auto-started game in lobby " + id.value());
return AddResult.ADDED_AND_STARTED;
} catch (RuntimeException e) {
LOGGER.log(Level.WARNING, "Auto-start failed for lobby " + id.value(), e);
return AddResult.ADDED;
}
}
return AddResult.ADDED;
}
}
}
@@ -0,0 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
/** Listener interface for lobby lifecycle events. */
public interface LobbyEventListener {
/** Called when a game is automatically started in the given lobby. */
void onGameStarted(LobbyId lobbyId);
}
@@ -0,0 +1,8 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
/** Typesafe wrapper for lobby IDs (1-8). */
public record LobbyId(int value) {
public static LobbyId of(int value) {
return new LobbyId(value);
}
}
@@ -0,0 +1,144 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby.AddResult;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
/** Manages dynamic creation of up to 8 lobbies and maps players to lobbies. */
public class LobbyManager {
private static final int MAX_LOBBIES = 8;
private static final int DEFAULT_MAX_PLAYERS = 4;
private static final int AUTO_START_PLAYERS = 4;
private static final int DEFAULT_START_CHIPS = 20000;
private final Map<LobbyId, Lobby> activeLobbies = new ConcurrentHashMap<>();
private final Map<String, LobbyId> playerToLobby = new ConcurrentHashMap<>();
private final List<LobbyEventListener> listeners = new CopyOnWriteArrayList<>();
private final int maxPlayersPerLobby;
private static final Logger LOGGER = Logger.getLogger(LobbyManager.class.getName());
public LobbyManager() {
this(DEFAULT_MAX_PLAYERS);
}
public LobbyManager(int maxPlayersPerLobby) {
this.maxPlayersPerLobby = maxPlayersPerLobby;
}
/** Create a new lobby with an automatic id (1..8). Returns null if none available. */
public synchronized LobbyId createNewLobby(String name) {
if (activeLobbies.size() >= MAX_LOBBIES) {
return null;
}
for (int i = 1; i <= MAX_LOBBIES; i++) {
LobbyId id = LobbyId.of(i);
if (!activeLobbies.containsKey(id)) {
Lobby lobby = new Lobby(id, name == null ? ("Room " + i) : name);
activeLobbies.put(id, lobby);
return id;
}
}
return null;
}
public Lobby getLobby(LobbyId id) {
if (id == null) {
return null;
}
return activeLobbies.get(id);
}
public Lobby getLobbyByUsername(String username) {
LobbyId id = playerToLobby.get(username);
if (id == null) {
return null;
}
return activeLobbies.get(id);
}
public boolean addPlayerToLobby(String username, LobbyId lobbyId) {
Lobby lobby = getLobby(lobbyId);
if (lobby == null) {
return false;
}
AddResult result =
lobby.addPlayerAndMaybeStart(
username, maxPlayersPerLobby, AUTO_START_PLAYERS, DEFAULT_START_CHIPS);
if (result != AddResult.NOT_ADDED) {
playerToLobby.put(username, lobbyId);
if (result == AddResult.ADDED_AND_STARTED) {
LOGGER.info(
() ->
"Lobby "
+ lobbyId.value()
+ " reached "
+ AUTO_START_PLAYERS
+ " players; game started.");
notifyGameStarted(lobbyId);
}
return true;
}
return false;
}
public void addListener(LobbyEventListener listener) {
listeners.add(listener);
}
public void removeListener(LobbyEventListener listener) {
listeners.remove(listener);
}
private void notifyGameStarted(LobbyId lobbyId) {
for (LobbyEventListener l : listeners) {
try {
l.onGameStarted(lobbyId);
} catch (RuntimeException e) {
LOGGER.warning(
() ->
"Listener threw while handling game-start for lobby "
+ lobbyId.value());
}
}
}
public boolean removePlayer(String username) {
LobbyId id = playerToLobby.remove(username);
if (id == null) {
return false;
}
Lobby lobby = activeLobbies.get(id);
if (lobby == null) {
return false;
}
boolean removed = lobby.removePlayer(username);
// If lobby becomes empty, remove it
if (lobby.getPlayerNames().isEmpty()) {
activeLobbies.remove(id);
}
return removed;
}
public Collection<Lobby> getAllLobbies() {
return activeLobbies.values();
}
/**
* Apply the given action to every player username in the lobby identified by {@code lobbyId}.
* This is a small helper that keeps iteration logic centralized and avoids leaking internal
* collections to callers.
*/
public void broadcast(LobbyId lobbyId, java.util.function.Consumer<String> action) {
Lobby lobby = getLobby(lobbyId);
if (lobby == null) {
return;
}
for (String username : lobby.getPlayerNames()) {
action.accept(username);
}
}
}
@@ -13,7 +13,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
/** Represents an authenticated user on the server. */
public class User {
private final UserId id;
private final String name;
private String name;
private SessionId sessionId;
private Instant disconnectedAt;
private final Queue<Message> messages;
@@ -47,10 +47,20 @@ public class User {
*
* @return the user name
*/
public String getName() {
public synchronized String getName() {
return name;
}
/**
* Sets a new display name for this user. Thread-safe; callers must ensure the registry is
* updated to maintain uniqueness when needed.
*
* @param newName the new display name
*/
public synchronized void setName(String newName) {
this.name = newName;
}
/**
* Returns the session currently associated with this user, if any.
*
@@ -1,10 +1,15 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** Creates new users, resolving name conflicts automatically. */
public class UserFactory {
private final UserRegistry registry;
private final AtomicInteger anonymousCounter = new AtomicInteger(1);
private static final Logger LOGGER = LogManager.getLogger(UserFactory.class);
/**
* Creates a new UserFactory backed by the given registry.
@@ -18,16 +23,40 @@ public class UserFactory {
/**
* Creates and registers a new user with the given name and session. If the name is already
* taken, a numeric suffix is appended and incremented until a free name is found (e.g.
* Lars_001, Lars_002, ...).
* Lars_001, Lars_002, ...). If the desiredName is null or empty, an automatic name of the form
* "playerN" is assigned, incrementing N until a free name is found.
*
* @param desiredName the preferred display name
* @param sessionId the session to associate with the new user
* @param sessionId the session to associate with the new userwas
* @return the newly created and registered user
*/
public User create(String desiredName, SessionId sessionId) {
if (desiredName == null || desiredName.isBlank()) {
// assign playerN names for anonymous logins
while (true) {
String candidate = "player" + anonymousCounter.getAndIncrement();
var result = registry.registerIfAvailable(candidate, sessionId);
if (result.isPresent()) {
var user = result.get();
LOGGER.info(
"Registered user '{}' with id {} for session {}",
user.getName(),
user.getId().value(),
sessionId == null ? "null" : sessionId.value());
return user;
}
}
}
var result = registry.registerIfAvailable(desiredName, sessionId);
if (result.isPresent()) {
return result.get();
var user = result.get();
LOGGER.info(
"Registered user '{}' with id {} for session {}",
user.getName(),
user.getId().value(),
sessionId == null ? "null" : sessionId.value());
return user;
}
int suffix = 1;
@@ -35,7 +64,13 @@ public class UserFactory {
String candidate = desiredName + "_" + String.format("%03d", suffix);
result = registry.registerIfAvailable(candidate, sessionId);
if (result.isPresent()) {
return result.get();
var user = result.get();
LOGGER.info(
"Registered user '{}' with id {} for session {}",
user.getName(),
user.getId().value(),
sessionId == null ? "null" : sessionId.value());
return user;
}
suffix++;
}
@@ -185,4 +185,29 @@ public class UserRegistry {
public Collection<User> getAllUsers() {
return byId.values();
}
/**
* Attempts to change the username of an existing user. Ensures the new name is not already
* taken and updates internal indices atomically.
*
* @param userId the id of the user to rename
* @param newName the desired new name
* @return true if the rename succeeded, false if the name was already taken or user not found
*/
public synchronized boolean changeUsername(UserId userId, String newName) {
User user = byId.get(userId);
if (user == null) {
return false;
}
if (byName.containsKey(newName)) {
return false;
}
// remove old mapping and put new mapping
byName.remove(user.getName());
user.setName(newName);
byName.put(newName, user);
return true;
}
}
@@ -0,0 +1,47 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
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.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.sessions.SessionId;
import java.util.Optional;
/** Helper to send responses to sessions identified by usernames. */
public class SessionDispatcher {
private final ResponseDispatcher responseDispatcher;
private final UserRegistry userRegistry;
public SessionDispatcher(ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
this.responseDispatcher = responseDispatcher;
this.userRegistry = userRegistry;
}
/**
* Dispatch a success response built from {@code body} to the user with {@code username}. If the
* user is disconnected or unknown, the call is a no-op.
*/
public void dispatchToUser(String username, ResponseBody body) {
Optional<User> opt = userRegistry.getByUsername(username);
if (opt.isEmpty()) {
return;
}
User user = opt.get();
Optional<SessionId> sid = user.getSessionId();
if (sid.isEmpty()) {
return;
}
RequestContext ctx = new RequestContext(sid.get(), 0);
SuccessResponse resp = new SuccessResponse(ctx, body) {};
responseDispatcher.dispatch(resp);
}
/** Broadcast the provided {@code body} to every player in the given lobby. */
public void broadcastToLobby(Lobby lobby, ResponseBody body) {
for (String username : lobby.getPlayerNames()) {
dispatchToUser(username, body);
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 651 KiB

After

Width:  |  Height:  |  Size: 461 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 665 KiB

After

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 KiB

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 660 KiB

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 657 KiB

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 KiB

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 661 KiB

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 658 KiB

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 663 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 662 KiB

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 663 KiB

After

Width:  |  Height:  |  Size: 472 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 657 KiB

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 664 KiB

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 665 KiB

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 648 KiB

After

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 661 KiB

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 656 KiB

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 656 KiB

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 KiB

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 655 KiB

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 658 KiB

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 654 KiB

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 660 KiB

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 658 KiB

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 KiB

After

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 KiB

After

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 660 KiB

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 660 KiB

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 644 KiB

After

Width:  |  Height:  |  Size: 455 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 658 KiB

After

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 652 KiB

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 KiB

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 650 KiB

After

Width:  |  Height:  |  Size: 464 KiB

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