Compare commits
496 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17b4b13beb | |||
| 7d759bc34b | |||
| c79066b111 | |||
| be7b528cbc | |||
| 1563bc3fa5 | |||
| ea647b8cfc | |||
| 706738d9a7 | |||
| 7de3af0cef | |||
| 85a3af9047 | |||
| 537e8aa771 | |||
| d869f81f1c | |||
| 144b5fa70e | |||
| 8843b63281 | |||
| ecdaf20ece | |||
| 2d82dbdb14 | |||
| da65a0470c | |||
| 17ed897516 | |||
| 1402a1231a | |||
| f9eb293460 | |||
| dfd0ae6a59 | |||
| a4c138c461 | |||
| f296bf272e | |||
| 9d00b948c7 | |||
| d5b9f223f3 | |||
| d5c833fa00 | |||
| 69cac803ad | |||
| cbef7c3888 | |||
| 5df9a7ffc4 | |||
| d11fea9c5d | |||
| 086394d25e | |||
| 4020b196cb | |||
| 3f168daf78 | |||
| 4760b55673 | |||
| 35e30eec2f | |||
| aef36f8a73 | |||
| a893418934 | |||
| b6c59a92a1 | |||
| f77cf41cd5 | |||
| df2dc51acb | |||
| 3dbedbb047 | |||
| baccb15c17 | |||
| bfef83f7e7 | |||
| ea143e2a24 | |||
| 894515204c | |||
| b030948cb6 | |||
| f78e9d8a25 | |||
| 089aeb9419 | |||
| 94613626d9 | |||
| 3d0fca616a | |||
| 2f744a5558 | |||
| 46919a064d | |||
| 310cb59fe6 | |||
| d57fd08b37 | |||
| f0135ce6f4 | |||
| 09aca862ca | |||
| a2212aa91c | |||
| 121b20f24f | |||
| 8790b06277 | |||
| bca9669703 | |||
| 09cc7e39c5 | |||
| 0aae4221e2 | |||
| f15fed2243 | |||
| cea21093c4 | |||
| e9dc8f1586 | |||
| 56421b4c15 | |||
| 87023b7238 | |||
| 6bd0ed6064 | |||
| b58eb656b3 | |||
| c10dc81ef3 | |||
| 0097da6bdc | |||
| 448f2ca472 | |||
| 01831fadc0 | |||
| 1d5d608a18 | |||
| cd8aa9e709 | |||
| 9706e140ac | |||
| 4afb96ee7a | |||
| 39b068a5cf | |||
| c5b92d9ba8 | |||
| 3314060eb1 | |||
| 6aaaaa55a3 | |||
| d687ed8238 | |||
| 6981a1fc0b | |||
| d2d83edfb2 | |||
| edcee79a10 | |||
| 6246615655 | |||
| cf712a46c0 | |||
| 59f1763926 | |||
| aad00bda0b | |||
| 903624e968 | |||
| 5294615aca | |||
| d474d85b41 | |||
| baa1d51a1f | |||
| fedaa87202 | |||
| 34bac0adb6 | |||
| 0be59681e4 | |||
| 28aad75930 | |||
| eefcd10135 | |||
| b7328769a9 | |||
| c8e437f8f0 | |||
| 87971103b8 | |||
| 145d6e7d31 | |||
| 426a108b54 | |||
| dd1a9a4e23 | |||
| f60d17edfe | |||
| b30af0c212 | |||
| 3be6fb0706 | |||
| 47475dc4d3 | |||
| 351ac06aab | |||
| 88d37bca49 | |||
| 1a96d5fb23 | |||
| 2cfc40543a | |||
| 9f5625b2f6 | |||
| 2f19fff618 | |||
| b63ce096fa | |||
| b304fa04ed | |||
| 394cdf4ea1 | |||
| 3100e4008d | |||
| 4cf88a9a2f | |||
| e4707bd6ee | |||
| f843adc93b | |||
| bddd9658cf | |||
| 217209997f | |||
| 712ba913e7 | |||
| 6f8c43397c | |||
| 479fab6557 | |||
| 5a3c47660d | |||
| be8e46d356 | |||
| ba1249bf93 | |||
| cf52d293a4 | |||
| b397478337 | |||
| 0d896d6aad | |||
| ef1f3c20b6 | |||
| 61030c3528 | |||
| 4ce9021e57 | |||
| 4f1624a30b | |||
| 363a0ed895 | |||
| ecf692fbcd | |||
| 4fd036173f | |||
| d36a78eee4 | |||
| 8484f731a8 | |||
| b158be26e3 | |||
| 59af75a75c | |||
| 823b406fe6 | |||
| 71f2d3c2ce | |||
| eb2b209e91 | |||
| 5663807e29 | |||
| fe83057049 | |||
| e9b40e9d04 | |||
| b7f5a268e3 | |||
| 619f6eef84 | |||
| d8d6dbb34f | |||
| efd91fbe5f | |||
| ad2accfced | |||
| 4c35596f19 | |||
| 659e606fb4 | |||
| 5358309e1a | |||
| dc2b3e05d9 | |||
| d6ad60d6a7 | |||
| ddb3e221ac | |||
| 5d0a9b9c25 | |||
| 52ad32a100 | |||
| 822d9a0e1e | |||
| 216c15e825 | |||
| 30927d471e | |||
| 86ab82ebfd | |||
| 6a06369d94 | |||
| 48bfef5fcb | |||
| a6e98b2453 | |||
| 54a3f1e29d | |||
| 1b18affd57 | |||
| 5fc3f315fc | |||
| ac9396cfb2 | |||
| c6772d6f89 | |||
| d86b9b5838 | |||
| d91ee5b04f | |||
| 6b5d521ab9 | |||
| 4ad9ae08c5 | |||
| 1107542c54 | |||
| 2ad99d8732 | |||
| d791e9b2d1 | |||
| 3cd1e6925e | |||
| a7e4411cd0 | |||
| bf979ab391 | |||
| 588bb1f312 | |||
| fb68ecc777 | |||
| 8ca41c5ab4 | |||
| 6422e6c924 | |||
| 404bf6379c | |||
| d46bd8de7e | |||
| c55a9eace6 | |||
| b3eaa64cd7 | |||
| ab4f60a74f | |||
| 4f9e9a0e9a | |||
| c31f0048ee | |||
| 0f5ddfda0c | |||
| 09f8ddf123 | |||
| c11201479b | |||
| 5ded42abe6 | |||
| 572b66889d | |||
| 89fd6aef05 | |||
| 6e640f7fde | |||
| 7a6c876017 | |||
| 22d742d614 | |||
| 71d48d148b | |||
| 5c0f3a7c86 | |||
| 2190fccd8f | |||
| 4bfb59f234 | |||
| d49f18c3aa | |||
| d4a3ebb487 | |||
| 3cabe06fb0 | |||
| ebca653ab4 | |||
| 9df48478f3 | |||
| 9023263b01 | |||
| c04d4fca76 | |||
| d3d72bd5b4 | |||
| 4a3c885b68 | |||
| 571c616ce9 | |||
| 01ecbb0497 | |||
| 5e91cefabb | |||
| 0f4d048b76 | |||
| b1d099f102 | |||
| 40e558be18 | |||
| 277b776fec | |||
| 88b797ba1d | |||
| 07c00275fd | |||
| 1d4cf2d846 | |||
| c2d0af03b7 | |||
| 48476a67f3 | |||
| 6471749133 | |||
| 826e58644a | |||
| 67a0f237e6 | |||
| 2a03571cd1 | |||
| 7eafaba65a | |||
| 69fc00a3b2 | |||
| ca65655443 | |||
| 488e8f524b | |||
| 9488ef41c3 | |||
| 8bb0bfaf09 | |||
| 33998e4a88 | |||
| d094df16da | |||
| b9e45142c2 | |||
| 16c91c6aae | |||
| ea5f6a1083 | |||
| c5f37392ef | |||
| 66ee8bec46 | |||
| 36348d0d93 | |||
| 90b2f3bb3a | |||
| 1cddcf6657 | |||
| e358c930c5 | |||
| 95fc53ec61 | |||
| 989dc95cef | |||
| 2c343c72b3 | |||
| ffbfd2dee4 | |||
| 97808fe26d | |||
| 5b37dffa4c | |||
| 838ada39d5 | |||
| 4e675677c0 | |||
| a629136fcb | |||
| 30067bd69b | |||
| 192c8a2cb9 | |||
| 028c3adb0d | |||
| 19b555fb47 | |||
| 8a7fad11a9 | |||
| c0a7cadf7e | |||
| 2fd07bbf60 | |||
| 15c998d549 | |||
| 062e66cdf9 | |||
| 7674ae57ad | |||
| 5e79b45d90 | |||
| 45411c4087 | |||
| cc7f8a8ef3 | |||
| d3a00aebc0 | |||
| ab2adbf311 | |||
| 8099228d4f | |||
| ac83d63ea1 | |||
| 3443b5e8f3 | |||
| ad7086e5a5 | |||
| e39cbb4357 | |||
| 238a156d91 | |||
| 621843167e | |||
| 215e7e2cae | |||
| 03daf9c3b8 | |||
| d23a00e148 | |||
| a5f0735a08 | |||
| 4855e937e6 | |||
| 007e863878 | |||
| 56aa3c0215 | |||
| 417af2e8f7 | |||
| 893bcee9bf | |||
| 6d1b467116 | |||
| 4687566816 | |||
| d27a07f3e8 | |||
| b86f18a11c | |||
| 7ff274269e | |||
| ec916aa98c | |||
| 190b30672a | |||
| 11be6e2b9b | |||
| a8599cfb4c | |||
| 08b9f652be | |||
| 3252c7e534 | |||
| 7cba7ac1dd | |||
| c83a360fec | |||
| 2631791330 | |||
| 181f538038 | |||
| 497820db05 | |||
| 88c9431bf0 | |||
| 5a53a14933 | |||
| f7f66ec5db | |||
| d85b3d21ee | |||
| 382383d38b | |||
| 6f345cc51e | |||
| 585d675a8f | |||
| b25f111f48 | |||
| d82297ac46 | |||
| 8c2130d0d6 | |||
| 52b5d5b8c2 | |||
| 1f8cf35e55 | |||
| 0bf24b9e0f | |||
| a85eb968fa | |||
| 0006ed9faa | |||
| 457fa58a07 | |||
| 22b3e1764d | |||
| 44785ee77f | |||
| 31f7ee8935 | |||
| 088407b041 | |||
| 30f98b1333 | |||
| 10dac9a68e | |||
| 786b7e990d | |||
| 3053d531ee | |||
| acb082ce5d | |||
| 6aedb57473 | |||
| 66f0d734cf | |||
| 6ec743f126 | |||
| 696f9f4fa4 | |||
| 4557dc2f5a | |||
| b208f1fffd | |||
| 6216268658 | |||
| 133a79433d | |||
| 8fe96c5413 | |||
| 885a002f6d | |||
| fb593d48a6 | |||
| 9746f52965 | |||
| 06c059ce5e | |||
| 1952f720de | |||
| d7eb9701c9 | |||
| 0f8f1bb918 | |||
| 9cf6b468ad | |||
| e08b75dc7a | |||
| 761d4b1cae | |||
| e4f0c7efb8 | |||
| f91642bade | |||
| 93b6338041 | |||
| a66c91c67f | |||
| 3e1d787b69 | |||
| bc36892699 | |||
| 120a477a55 | |||
| 5d4fe4bcfb | |||
| 346a728fb9 | |||
| 63fb6e4b47 | |||
| 9044ce3660 | |||
| 4fa60ad9e0 | |||
| 3abc03e6e1 | |||
| e43732c520 | |||
| 1dcf628d2c | |||
| 4e65e2dec0 | |||
| 492901a837 | |||
| 659ea8a744 | |||
| 5e653a516c | |||
| f5ee57454d | |||
| fa8bb25b03 | |||
| e6c222388b | |||
| 7316ec80bf | |||
| b86352a298 | |||
| a7de101be2 | |||
| a2cd59e6b9 | |||
| 564ace38e4 | |||
| d16c348444 | |||
| 8f3d0ab8a9 | |||
| 77ec50e008 | |||
| 869f061d86 | |||
| 9635ee8ef9 | |||
| 65a93a8c82 | |||
| c3e0e740ed | |||
| 0ae88c9149 | |||
| 0aafe13e34 | |||
| 4798de7499 | |||
| 3359c9bc7e | |||
| 346a308176 | |||
| 8998152e8d | |||
| ef7a83c707 | |||
| d9e7c0c56d | |||
| 0227145cab | |||
| 755ea54381 | |||
| c0b149cc5f | |||
| 1669e07d3a | |||
| d0e908774b | |||
| 08e02a6135 | |||
| 6ee9c6c399 | |||
| fb5399fc18 | |||
| 6d8f1787a9 | |||
| 9b4c6d8906 | |||
| ca818d9892 | |||
| dac4dcfdbe | |||
| 0556767d40 | |||
| 5997e736cf | |||
| a4e267cb00 | |||
| 9146d92c04 | |||
| b2adeb7a56 | |||
| d77ce42795 | |||
| f5a62c6e93 | |||
| 371e6afe5c | |||
| 23debb18bc | |||
| 3ff4491fcf | |||
| 31361ffb83 | |||
| 0f93bb8b85 | |||
| 4dd6732945 | |||
| 3354bf7be1 | |||
| 3b8f7d42dd | |||
| a38b2562d1 | |||
| 54a7be074f | |||
| 1b6e8ce6d6 | |||
| 6e4e8f5150 | |||
| c7b7adaa07 | |||
| 84b889a343 | |||
| 25c92da522 | |||
| 7f6a3ada29 | |||
| 098092707c | |||
| f467d9ebe2 | |||
| 13d03637da | |||
| 50052b72ff | |||
| ab4ba0f70b | |||
| 46ccae40ad | |||
| 050beb9d98 | |||
| 517df2369d | |||
| 6219c2d660 | |||
| 57b8bca1a8 | |||
| 050d87ce57 | |||
| a930bd94b5 | |||
| 66ee791342 | |||
| 240bba9bf5 | |||
| 15d1ea4edd | |||
| 3badca0a01 | |||
| 203004fc42 | |||
| de0d0f93f8 | |||
| 7788bbc5d3 | |||
| 2c6ecdc0f9 | |||
| 15ab6faf2e | |||
| 25c20a2ea3 | |||
| 73dd00f37d | |||
| f512e6c422 | |||
| eb3ffee6fc | |||
| af9daecf46 | |||
| 48e6b51ad1 | |||
| e655530760 | |||
| 62f01d1e5d | |||
| ca4283bbe7 | |||
| 3c7773e7fb | |||
| 406567d375 | |||
| ff0f9f70d4 | |||
| 53cafd3dd2 | |||
| 30dcd8f4ab | |||
| 2e705185f4 | |||
| f6dda36c8d | |||
| 4de90e6962 | |||
| 284ff2d043 | |||
| c02680143f | |||
| cb7c284314 | |||
| a2d176f13c | |||
| 644c3e93fc | |||
| d0ab49ec57 | |||
| 8deb647e94 | |||
| 3ea2971ab5 | |||
| d10e9a72fd | |||
| ea41a3c75a | |||
| ce583062ba | |||
| 040ff7cf52 | |||
| 616f609fae | |||
| 4895af9030 | |||
| db3f34fddf | |||
| 999dd9beb9 | |||
| 3be2d3d9f4 | |||
| c094b224c3 | |||
| 3807059229 | |||
| 59cd5bca88 | |||
| a43361d606 | |||
| 1b154c685e | |||
| f12e1e4529 | |||
| a5427a232f | |||
| ad81479bcd | |||
| e374be4e31 | |||
| d939b0da67 | |||
| ea987243b0 | |||
| 8a6c573bde | |||
| 139626520b | |||
| b85e87798e | |||
| 5448f5683d |
@@ -1,3 +1,25 @@
|
||||
> [!IMPORTANT]
|
||||
> ## Programming Project (CS108)
|
||||
>
|
||||
> - **Course:** Programmierprojekt (CS108)
|
||||
> - **Semester:** FS26
|
||||
> - **Institution:** University of Basel
|
||||
> - **Duration:** 18 February 2026 – 29 June 2026
|
||||
> - **Project Type:** Group Project
|
||||
> - **Project members:** Mathis Ginkel, Julian Kropff, Jona Walpert and me (Lars Winzer)
|
||||
>
|
||||
> ### Topic
|
||||
>
|
||||
> As part of the *Programming Project* course (CS108) for the semester FS26 at the University of Basel, we developed a computer game implementation of the popular card game Poker.
|
||||
>
|
||||
> The project's focus was networking and distributed systems design. A key aspect of the implementation was the development and integration of a custom protocol specifically designed for client-server communication. This protocol handled game state synchronization, player actions, lobby and global chat functionality, and real-time updates across the network.
|
||||
>
|
||||
> ### Contribution Context
|
||||
>
|
||||
> This repository contains work created during the official duration of the course in collaboration with three other students.
|
||||
>
|
||||
> Further information regarding individual contributions, repository state, synchronization status, and project ownership may be documented separately within this repository.
|
||||
|
||||
<div align="center">
|
||||
<img src="documents/images/logo.png" alt="Game Logo" width="200"/>
|
||||
</div>
|
||||
@@ -36,7 +58,6 @@ This protocol handles game state synchronization, player actions, lobby and glob
|
||||
├── documents/ # Project resources and documentation
|
||||
│ ├── images/ # Images for README and documentation
|
||||
│ ├── diary # Diary files for each organizational meetup
|
||||
│ ├── blog # Blog files covering project-related topics
|
||||
│ ├── docs # Documentation
|
||||
│ ├── milestones/ # Milestone deliverables (6 milestones)
|
||||
│ └── (Other resources) # Additional project materials
|
||||
|
||||
@@ -4,6 +4,7 @@ plugins {
|
||||
id 'org.openjfx.javafxplugin' version '0.1.0'
|
||||
id 'checkstyle'
|
||||
id 'com.diffplug.spotless' version '7.0.2'
|
||||
id 'jacoco'
|
||||
}
|
||||
|
||||
group = 'ch.unibas.dmi.dbis'
|
||||
@@ -25,7 +26,7 @@ repositories {
|
||||
|
||||
javafx {
|
||||
version = "25.0.2"
|
||||
modules = ['javafx.controls', 'javafx.fxml', 'javafx.base', 'javafx.graphics', 'javafx.web']
|
||||
modules = ['javafx.controls', 'javafx.fxml', 'javafx.base', 'javafx.graphics', 'javafx.web', 'javafx.media']
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -41,8 +42,17 @@ dependencies {
|
||||
implementation 'org.json:json:20240303'
|
||||
}
|
||||
|
||||
jacoco {
|
||||
toolVersion = "0.8.14"
|
||||
}
|
||||
|
||||
jacocoTestReport {
|
||||
dependsOn test
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
finalizedBy jacocoTestReport
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# Blog
|
||||
|
||||
## Purpose
|
||||
This folder contains a collection of blog posts documenting the development process of our application. These blogs serve two key purposes:
|
||||
|
||||
1. **For External Audiences**
|
||||
|
||||
The blogs provide insights into the internal program flow and the interaction between components without requiring readers to dive into the source code. They offer a mixed-level overview of how different parts of the system work together.
|
||||
|
||||
2. **For Our Team**
|
||||
|
||||
The blogs serve as a record of our decision-making process. By documenting what decisions were made and why, we can track the evolution of our design choices and understand the reasoning behind them. This is invaluable for maintaining consistency across the team.
|
||||
|
||||
But keep in mind that these blogs **do not replace** the detailed source code documentation.
|
||||
|
||||
## Format
|
||||
All blog posts follow the naming convention: `<YYYY-MM-DD>_<Title>.md`
|
||||
|
||||
## Blog Posts
|
||||
|
||||
### General
|
||||
*(No posts yet)*
|
||||
|
||||
### Client
|
||||
*(No posts yet)*
|
||||
|
||||
### Server
|
||||
*(No posts yet)*
|
||||
@@ -0,0 +1,9 @@
|
||||
# Besprechung über Veränderung der Aufgabenteilung
|
||||
|
||||
## Verschieben von Aufgaben
|
||||
|
||||
Da die Implementation der client server Kommunikation länger dauerte als geplant, wurden achivements erfüllt, die erst bei späteren Meilensteinen relevant sein werden.
|
||||
|
||||
## Aufgabeverteilung
|
||||
|
||||
Um zu vermeiden, dass sich Personen erst lange Zeit in den Code anderer einarbeiten müssen um zu helfen, wurden Dokumentation und das Erstellen von Materialien für MS4 auf ein Teammitgleid (Jona) ausgelagert um das Projekt nicht zu verlangsamen
|
||||
@@ -0,0 +1,38 @@
|
||||
# Neue Aufgabenteilung für MS4 (10.04)
|
||||
|
||||
## Kurzfassung
|
||||
|
||||
Offene Meilensteine wurden überprüft und Personen zugewiesen. Ziel ist, verbleibende Funktionen (Game List, Username-Integration, Lobby/Chat-Funktionen) bis zum nächsten Sprint stabil zu machen.
|
||||
|
||||
## Offene Aufgaben
|
||||
|
||||
- Broadcast: Offen (Priorität niedrig)
|
||||
- Username-Integration: In Backend integrieren und frontend Anpasungen (wichtig)
|
||||
- Demo: Kernfunktion vorhanden, muss getestet werden
|
||||
- Game List: Backend fehlt vollständig
|
||||
- Chat: Globaler Chat und Lobby-Chat müssen eingebunden werden
|
||||
- Lounging: Support für mehrere Lobbys und Spielerlisten
|
||||
- QA: Präsentation und Dokumentation
|
||||
- Rules to Code: Vorstellung und Integration
|
||||
- "Shall we play a game": Flag zum Umschalten zwischen Demo- und Normalmodus
|
||||
- Whisper: Funktion (siehe Zuordnung)
|
||||
- Zeitplan: Verschiebung von nicht-kritischen Punkten
|
||||
|
||||
## Aufgaben & Zustänextrdigkeiten
|
||||
|
||||
- Jona
|
||||
- Command Line: Parsing, Option für Username-Eingabe (Start vs. Lobby)
|
||||
- Game List: Implementierung (Datenstruktur, Commands, Backend-Anbindung)
|
||||
- Lounging: Mehrere Lobbys, Liste aller Spieler
|
||||
|
||||
- Lars
|
||||
- QA: Präsentation und ausgeschriebenes Dokument
|
||||
|
||||
- Mathis
|
||||
- Whisper-Chat: Fertigstellung
|
||||
- Lobby-Chat: Integration
|
||||
|
||||
- Julian
|
||||
- Game Engine: Fertigstellung
|
||||
- Flag: Umschalten zwischen Demo- und echter Game Engine
|
||||
- Rules to Code: Vorstellung
|
||||
@@ -0,0 +1,5 @@
|
||||
## Besprechung vom 13.05.2026
|
||||
|
||||
### Abstimmung über die Implementierung der Auto-Discovery im lokalen Netzwerk
|
||||
Über einen vom Server ausgesendeten Multicast gäbe es die Möglichkeit das Clients in ihrem lokalen Netzwerk aktive Server automatisch finden.
|
||||
Da Julian für Prüfungen lernen muss, haben wir diese Funktion leider nicht weiter verfolgt.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Besprechung vom 26.04.2024
|
||||
|
||||
## Fortschritt für die Implementierung der Unit Tests
|
||||
Die Unit Tests für alle zentralen Komponenten des Netzwerkes wurden erfolgreich implementiert.
|
||||
Bei dieser Gelegenheit wurden auch Bugs behoben und die Codequalität verbessert.
|
||||
|
||||
In einzelnen Bereichen wurde noch nicht die vorgenommene Quote von 50% erreicht, weitere Unit-Tests sollen bis zum nächsten Meilenstein folgen. Komponenten beteiligt an den Kernbereichen sind größtenteils abgedeckt.
|
||||
|
||||
## Perspektivische Änderungen für den letzten Meilenstein
|
||||
Ein getroffener Kompromiss um den vierten Meilenstein fristgerecht zu erreichen, war das zurückstellen der Codequalität.
|
||||
In der Zeit bis zum sechsten Meilenstein sollen diese Provisorien noch einmal überarbeitet und finalisiert werden.
|
||||
@@ -1,7 +1,7 @@
|
||||
# Meeting Diary
|
||||
|
||||
This folder holds the notes and documentation of all team meetings throughout the project.
|
||||
The notes are organised by **milestones (1–6)** so that everyone can easily see what was discussed and decided at each stage. Each sub-header below links to the actual Markdown file containing the details.
|
||||
The notes are organized by **milestones (1–6)** so that everyone can easily see what was discussed and decided at each stage. Each sub-header below links to the actual Markdown file containing the details.
|
||||
|
||||
## Milestone 1
|
||||
### [24. February 2026](24-02.md)
|
||||
@@ -14,21 +14,33 @@ The notes are organised by **milestones (1–6)** so that everyone can easily se
|
||||
- Finalizing structure of network protocol
|
||||
|
||||
|
||||
## Milestone 2
|
||||
*(Meeting notes to be added here when available)*
|
||||
## Milestone 2
|
||||
### [14. March 2025](14-03.md)
|
||||
- Discussion of LobbyScreen, GameScreen, and Chatbox structure
|
||||
- Endscreen postponed to MS3
|
||||
|
||||
|
||||
## Milestone 3
|
||||
*(Meeting notes to be added here when available)*
|
||||
## Milestone 3
|
||||
None, as milestone 3 covered the assessment of individual technical understanding through evaluation.
|
||||
|
||||
|
||||
## Milestone 4
|
||||
*(Meeting notes to be added here when available)*
|
||||
## Milestone 4
|
||||
### [09. April 2026](09-04.md)
|
||||
- Task redistribution due to delays in client-server communication
|
||||
- Documentation and MS4 materials assigned to Jona
|
||||
|
||||
### [10. April 2026](10-04.md)
|
||||
- Review and assignment of open milestones
|
||||
- Focus on stabilizing Game List, Username-Integration, Lobby/Chat functions
|
||||
- Task list for remaining features and QA
|
||||
|
||||
|
||||
## Milestone 5
|
||||
*(Meeting notes to be added here when available)*
|
||||
## Milestone 5
|
||||
### [26. April 2026](26-04.md)
|
||||
- Progress on unit tests and code quality improvements
|
||||
- Discussion of compromises made for MS4 and plans for finalizing code quality by MS6
|
||||
|
||||
|
||||
## Milestone 6
|
||||
*(Meeting notes to be added here when available)*
|
||||
### [13. Mai 2026](13-05.md)
|
||||
- Discussion on possible auto-discovery feature; not implementing due to time constrains
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
- [Game Engine Architecture](game-engine-architecture.md)
|
||||
- [Casono Rules Easy Description](casono-rules-easy-description.md)
|
||||
- [Game engine implementation](game-engine-implementation.md)
|
||||
- [Casonso Manual](casono-manual.md)
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
# Casono Manual
|
||||
|
||||
<!-- vim-markdown-toc GFM -->
|
||||
|
||||
* [Start Game](#start-game)
|
||||
* [Server starten](#server-starten)
|
||||
* [Client starten](#client-starten)
|
||||
* [UI](#ui)
|
||||
* [Lobby UI](#lobby-ui)
|
||||
* [Eine Lobby erstellen](#eine-lobby-erstellen)
|
||||
* [Einer Lobby beitreten](#einer-lobby-beitreten)
|
||||
* [Den Username ändern](#den-username-ändern)
|
||||
* [Highscores](#highscores)
|
||||
* [Game UI](#game-ui)
|
||||
* [Themes](#themes)
|
||||
* [Casono Browser](#casono-browser)
|
||||
* [ChatUI](#chat ui)
|
||||
* [Casono Rules](#casono-rules)
|
||||
* [1. Spielübersicht](#1-spielübersicht)
|
||||
* [Grundregeln](#grundregeln)
|
||||
* [2. Sitzposition & Dealer-Button](#2-sitzposition--dealer-button)
|
||||
* [3. Blinds (Pflichteinsätze)](#3-blinds-pflichteinsätze)
|
||||
* [4. Spielablauf im Detail](#4-spielablauf-im-detail)
|
||||
* [4.1 Preflop (erste Setzrunde)](#41-preflop-erste-setzrunde)
|
||||
* [4.2 Flop (3 Gemeinschaftskarten)](#42-flop-3-gemeinschaftskarten)
|
||||
* [4.3 Turn (4. Gemeinschaftskarte)](#43-turn-4-gemeinschaftskarte)
|
||||
* [4.4 River (5. Gemeinschaftskarte)](#44-river-5-gemeinschaftskarte)
|
||||
* [5. Showdown (Kartenvergleich)](#5-showdown-kartenvergleich)
|
||||
* [6. Poker-Handrangfolge](#6-poker-handrangfolge)
|
||||
* [7. Wichtige Grundprinzipien](#7-wichtige-grundprinzipien)
|
||||
* [Reihenfolge beachten](#reihenfolge-beachten)
|
||||
* [Klare Aktionen](#klare-aktionen)
|
||||
* [Ein Spieler – eine Hand](#ein-spieler--eine-hand)
|
||||
* [Fehlerhafte Einsätze](#fehlerhafte-einsätze)
|
||||
* [8. Strategische Einordnung](#8-strategische-einordnung)
|
||||
* [Casono Rules Easy Description](#casono-rules-easy-description)
|
||||
* [Blinds (Small Blind & Big Blind)](#blinds-small-blind--big-blind)
|
||||
* [Erste Setzrunde (Preflop)](#erste-setzrunde-preflop)
|
||||
* [Beispiel Preflop](#beispiel-preflop)
|
||||
* [Flop (3 Gemeinschaftskarten)](#flop-3-gemeinschaftskarten)
|
||||
* [Beispiel Flop](#beispiel-flop)
|
||||
* [Turn (4. Karte)](#turn-4-karte)
|
||||
* [River (5. Karte)](#river-5-karte)
|
||||
* [Showdown (Gewinnentscheidung)](#showdown-gewinnentscheidung)
|
||||
* [Poker Hand Rankings (Gewichtung)](#poker-hand-rankings-gewichtung)
|
||||
* [Fazit](#fazit)
|
||||
|
||||
<!-- vim-markdown-toc -->
|
||||
|
||||
# Start Game
|
||||
|
||||
Es wird Java 25 benötigt
|
||||
## Server starten
|
||||
|
||||
```bash
|
||||
java -jar casono.jar server <listenport>
|
||||
```
|
||||
## Client starten
|
||||
|
||||
```bash
|
||||
java -jar casono.jar client <serverip>:<serverport> [username]
|
||||
```
|
||||
|
||||
Die Parameter serverip, serverport, listenport und username müssen ersetzt werden
|
||||
|
||||
# UI
|
||||
|
||||
## Lobby UI
|
||||
|
||||
### Eine Lobby erstellen
|
||||
|
||||

|
||||
|
||||
### Einer Lobby beitreten
|
||||
|
||||

|
||||
|
||||
### Den Username ändern
|
||||
|
||||

|
||||
|
||||
### Highscores
|
||||
|
||||

|
||||
|
||||
## Game UI
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Themes
|
||||
|
||||

|
||||
|
||||
### Black-and-White Theme
|
||||
|
||||

|
||||
|
||||
### Glass-Effect Theme
|
||||
|
||||

|
||||
|
||||
## Casono Browser
|
||||
|
||||
![[casono-browser.png]]
|
||||
|
||||

|
||||
|
||||
## Chat UI
|
||||
|
||||
### Globaler Chat
|
||||
|
||||

|
||||
|
||||
### Lobby Chat
|
||||
|
||||

|
||||
|
||||
### Whisper Chat
|
||||
|
||||

|
||||
|
||||
# Casono Rules
|
||||
|
||||
## 1. Spielübersicht
|
||||
|
||||
Texas Hold’em ist ein strategisches Kartenspiel für mehrere Spieler.
|
||||
Ziel ist es, den Pot (alle gesetzten Chips) zu gewinnen, entweder durch:
|
||||
|
||||
- die beste Kartenkombination am Ende der Runde
|
||||
- oder indem alle anderen Spieler vorher aussteigen (Fold)
|
||||
|
||||
### Grundregeln
|
||||
|
||||
- Jeder Spieler erhält 2 verdeckte Karten (Hole Cards)
|
||||
- Es werden 5 Gemeinschaftskarten offen in der Mitte ausgelegt
|
||||
- Jeder Spieler bildet die beste 5-Karten-Kombination aus:
|
||||
- eigenen Karten
|
||||
- und Gemeinschaftskarten
|
||||
|
||||
- Zu Spielbeginn erhält jeder Spieler ein Startgeld von 20000 Chips ($)
|
||||
|
||||
## 2. Sitzposition & Dealer-Button
|
||||
|
||||
Der sogenannte Dealer-Button bestimmt die Positionen am Tisch:
|
||||
|
||||
- Er zeigt an, wer als „Geber“ (Dealer) fungiert
|
||||
- Die Positionen rotieren im Uhrzeigersinn nach jeder Runde
|
||||
|
||||
Die Position ist entscheidend, da sie bestimmt:
|
||||
|
||||
- die Reihenfolge der Aktionen
|
||||
- wer die Blinds setzen muss
|
||||
|
||||
## 3. Blinds (Pflichteinsätze)
|
||||
|
||||
Vor jeder Runde werden zwei verpflichtende Einsätze geleistet:
|
||||
|
||||
- **Small Blind** (kleiner Blind): 100 Chips – gesetzt vom Spieler links neben dem Dealer
|
||||
- **Big Blind** (großer Blind): 200 Chips - gesetzt vom Spieler zwei Plätze links vom Dealer
|
||||
|
||||
Diese Einsätze sorgen dafür, dass:
|
||||
|
||||
- ein Startpot entsteht
|
||||
- jede Runde aktiv gespielt wird
|
||||
|
||||
## 4. Spielablauf im Detail
|
||||
|
||||
### 4.1 Preflop (erste Setzrunde)
|
||||
|
||||
Nach dem Austeilen der Karten beginnt die erste Setzrunde.
|
||||
|
||||
Der Spieler links vom Big Blind eröffnet die Runde.
|
||||
|
||||
Jeder Spieler hat folgende Optionen:
|
||||
|
||||
- **Fold** – Karten ablegen und aussteigen
|
||||
- **Call** – Einsatz mitgehen
|
||||
- **Raise** – Einsatz erhöhen
|
||||
|
||||
### 4.2 Flop (3 Gemeinschaftskarten)
|
||||
|
||||
- Drei Karten werden offen auf den Tisch gelegt
|
||||
- Eine neue Setzrunde beginnt
|
||||
- Die Setzrunde beginnt jetzt immer beim ersten aktiven Spieler links vom Dealer (im Uhrzeigersinn).
|
||||
- Der erste Spieler bei der Flop Runde muss keinen höheren Einsatz setzen als der letzte Spieler aus der Preflop Runde, allerdings muss er seinen eigenen Einsatz aus der Preflop Runde überbieten.
|
||||
|
||||
Ab diesem Zeitpunkt können alle Spieler ihre Strategie anhand zusätzlicher Informationen anpassen.
|
||||
|
||||
### 4.3 Turn (4. Gemeinschaftskarte)
|
||||
|
||||
- Die vierte Karte wird aufgedeckt
|
||||
- Eine weitere Setzrunde folgt
|
||||
|
||||
Die Einsätze werden oft höher, da sich stärkere Hände entwickeln.
|
||||
|
||||
### 4.4 River (5. Gemeinschaftskarte)
|
||||
|
||||
- Die letzte Karte wird aufgedeckt
|
||||
- Letzte Setzrunde
|
||||
|
||||
Dies ist die finale Entscheidungsphase:
|
||||
|
||||
- Maximierung des Gewinns
|
||||
- oder Minimierung von Verlusten
|
||||
|
||||
## 5. Showdown (Kartenvergleich)
|
||||
|
||||
Wenn nach der letzten Setzrunde mindestens zwei Spieler verbleiben:
|
||||
|
||||
- Alle verbleibenden Spieler decken ihre Karten auf
|
||||
- Die **beste 5-Karten-Kombination gewinnt**
|
||||
|
||||
Wichtig:
|
||||
|
||||
- Die Karten „sprechen für sich“ – die beste Hand zählt unabhängig von Ansagen
|
||||
|
||||
## 6. Poker-Handrangfolge
|
||||
|
||||
Die Stärke der Hände ist eindeutig festgelegt (von schwach nach stark):
|
||||
|
||||
1. High Card (höchste Einzelkarte)
|
||||
2. One Pair (ein Paar)
|
||||
3. Two Pair (zwei Paare)
|
||||
4. Three of a Kind (Drilling)
|
||||
5. Straight (Straße)
|
||||
6. Flush (Farbe)
|
||||
7. Full House
|
||||
8. Four of a Kind (Vierling)
|
||||
9. Straight Flush
|
||||
10. Royal Flush
|
||||
|
||||
Je höher die Kombination, desto stärker die Hand.
|
||||
|
||||
## 7. Wichtige Grundprinzipien
|
||||
|
||||
### Reihenfolge beachten
|
||||
|
||||
Spieler müssen immer der Reihe nach handeln.
|
||||
|
||||
### Klare Aktionen
|
||||
|
||||
Alle Aktionen müssen eindeutig sein:
|
||||
|
||||
- Einsätze klar ansagen oder eindeutig setzen
|
||||
|
||||
### Ein Spieler – eine Hand
|
||||
|
||||
- Spieler dürfen ihre Karten nicht teilen oder gemeinsam spielen
|
||||
|
||||
### Fehlerhafte Einsätze
|
||||
|
||||
- Unklare oder falsche Einsätze können korrigiert werden, abhängig von der Spielsituation (nur wenn der Einsatz
|
||||
außerhalb der gültigen Grenzen liegt; zu hohe oder unzulässige Beträge werden blockiert und nicht automatisch
|
||||
korrigiert)
|
||||
|
||||
## 8. Strategische Einordnung
|
||||
|
||||
Texas Hold’em ist kein reines Glücksspiel. Der Erfolg basiert auf:
|
||||
|
||||
- Wahrscheinlichkeiten (Mathematik)
|
||||
- Einschätzung von Gegnern (Psychologie)
|
||||
- Positionsspiel und Timing
|
||||
|
||||
# Casono Rules Easy Description
|
||||
|
||||
Der Pokertisch ist ein unglaublich faszinierender Erlebnisraum, in dem man sehr viel lernen kann: über sich selbst, über
|
||||
andere Menschen und über Fragen wie: Wie treffe ich eigentlich Entscheidungen, wie gehe ich mit Stress und Unsicherheit
|
||||
um und wie gut ich darin bin, mich in andere hineinzuversetzen und Situationen richtig einzuschätzen.
|
||||
|
||||
Damit Du in diesem Erlebnisraum starten kannst, ist es – wie bei jedem Spiel – notwendig, zuerst die Grundregeln und den
|
||||
Spielablauf zu verstehen.
|
||||
|
||||
Also los geht es:
|
||||
|
||||
Wir haben am Tisch **4 Spieler**: Julian, Mathis, Jona und Lars. Jeder Spieler startet mit **20000 Chips ($)**. Jeder
|
||||
bekommt **2 Karten auf die Hand** und es gibt zusätzlich **5 Gemeinschaftskarten**, die später in der Mitte aufgedeckt
|
||||
werden.
|
||||
|
||||

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

|
||||
|
||||
Regel: *32 Dead Button 🟡*
|
||||
|
||||
Diese Einsätze sorgen dafür, dass sofort ein Pot entsteht und das Spiel überhaupt beginnt, weil jeder schon “im Spiel”
|
||||
ist.
|
||||
|
||||
Danach werden die Karten verteilt: zuerst Small Blind, dann Big Blind und dann im Uhrzeigersinn alle anderen Spieler.
|
||||
|
||||
## Erste Setzrunde (Preflop)
|
||||
|
||||
Die erste Setzrunde beginnt immer bei dem Spieler links vom Big Blind.
|
||||
|
||||
Jetzt muss jeder Spieler entscheiden:
|
||||
|
||||
* Fold (aussteigen)
|
||||
* Call (mitgehen)
|
||||
* Raise (erhöhen)
|
||||
|
||||
Regel: *40 Methods of Betting 🟢*
|
||||
Regel: *41 Methods of Calling 🟢*
|
||||
Regel: *42 Methods of Raising 🟢*
|
||||
Regel: *50 Acting in Turn 🟢*
|
||||
|
||||
## Beispiel Preflop
|
||||
|
||||
Julian schaut seine Karten an und entscheidet sich direkt für einen Raise von **600 Chips**.
|
||||
|
||||

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

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

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

|
||||
|
||||
Damit verändert sich sofort die Situation: Julian und Jona müssen entscheiden, ob sie diesen Raise bezahlen, selbst
|
||||
erhöhen oder aussteigen.
|
||||
|
||||
## Flop (3 Gemeinschaftskarten)
|
||||
|
||||
Jetzt werden **3 Gemeinschaftskarten** in die Mitte gelegt. Ab hier verändert sich das Spiel komplett, weil alle Spieler
|
||||
zusätzliche Informationen bekommen.
|
||||
|
||||
Die Setzrunde beginnt jetzt immer beim ersten aktiven Spieler links vom Dealer (im Uhrzeigersinn).
|
||||
|
||||

|
||||
|
||||
Es beginnt eine neue Setzrunde.
|
||||
|
||||
Regel: *49 Accepted Action 🟢*
|
||||
|
||||
## Beispiel Flop
|
||||
|
||||
Jona setzt **1000 Chips** als Erstes. Lars entscheidet sich mitzugehen (Call), weil seine Karten durch die
|
||||
Gemeinschaftskarten stärker geworden sind.
|
||||
|
||||
Julian steigt aus, weil er keine gute Verbindung mehr sieht. Mathis ist bereits raus.
|
||||
|
||||

|
||||
|
||||
## Turn (4. Karte)
|
||||
|
||||
Jetzt kommt die **4. Gemeinschaftskarte**.
|
||||
|
||||
Wieder beginnt eine neue Setzrunde.
|
||||
|
||||
Jona setzt diesmal **3000 Chips**. Lars bezahlt erneut (Call), weil seine Hand weiterhin gut spielbar ist.
|
||||
|
||||

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

|
||||
|
||||
Lars muss jetzt entscheiden: Fold, Call oder Raise auf 10000 Chips.
|
||||
|
||||
Regel: *54 Pot Size Bets 🟡*
|
||||
|
||||
## Showdown (Gewinnentscheidung)
|
||||
|
||||
Wenn nach der letzten Setzrunde noch zwei Spieler übrig sind, kommt es zum Showdown.
|
||||
|
||||
Beide Spieler zeigen ihre Karten offen. Gewonnen hat die **beste 5-Karten-Kombination aus Handkarten und
|
||||
Gemeinschaftskarten**.
|
||||
|
||||

|
||||
|
||||
Regel: *12 Cards Speak at Showdown 🟢*
|
||||
Regel: *16 Face Up for All-Ins 🟢*
|
||||
Regel: *17 Non All-In Showdowns 🟢*
|
||||
|
||||
Wenn Lars den letzten Einsatz bezahlt, werden die Hände verglichen. Wenn er foldet, gewinnt Jona automatisch den
|
||||
gesamten Pot.
|
||||
|
||||
## Poker Hand Rankings (Gewichtung)
|
||||
|
||||
Die Kartenkombinationen sind klar geordnet – von schwach bis extrem stark:
|
||||
|
||||
<img src="./images/12.png" height="600">
|
||||
|
||||
Je höher die Kombination, desto stärker die Hand und desto wahrscheinlicher der Gewinn.
|
||||
|
||||
Jona: 2. Paar:
|
||||
|
||||

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

|
||||
|
||||
Da zwei Paare in der Rangfolge über einem einzelnen Paar stehen, gewinnt Jona diese Runde.
|
||||
|
||||
## Fazit
|
||||
|
||||
Poker ist kein Glücksspiel im klassischen Sinn, sondern ein Spiel aus Strategie, Psychologie und Mathematik. Jede
|
||||
Entscheidung von Julian, Mathis, Jona oder Lars verändert die komplette Dynamik am Tisch. Wer die Regeln versteht,
|
||||
versteht nicht nur Karten, sondern auch Menschen und Entscheidungen unter Druck.
|
||||
|
||||
Regel: *67 One Player One Hand 🟢*
|
||||
Regel: *52 Incorrect Bets 🟡*
|
||||
Regel: *57 Non-Standard Betting 🟡*
|
||||
@@ -0,0 +1,85 @@
|
||||
const marked = {
|
||||
parse: function (md) {
|
||||
if (!md) return "";
|
||||
|
||||
let toc = [];
|
||||
|
||||
md = md.replace(/^(#{1,3})\s+(.*)$/gim, (m, hashes, title) => {
|
||||
const level = hashes.length;
|
||||
|
||||
const id = title
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\wäöüß]+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
|
||||
toc.push({ level, title, id });
|
||||
|
||||
return `
|
||||
<h${level} id="${id}" class="cm-h${level}">
|
||||
${title}
|
||||
</h${level}>
|
||||
`;
|
||||
});
|
||||
|
||||
md = md.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
|
||||
const langClass = lang ? ` language-${lang}` : "";
|
||||
return `<pre class="cm-code${langClass}"><code>${escapeHtml(code.trim())}</code></pre>`;
|
||||
});
|
||||
|
||||
md = md.replace(/!\[(.*?)\]\((.*?)\)/gim,
|
||||
`<img alt="$1" src="$2" class="cm-img">`
|
||||
);
|
||||
|
||||
md = md.replace(/\[(.*?)\]\((.*?)\)/gim,
|
||||
`<a href="$2" target="_blank" class="cm-link">$1</a>`
|
||||
);
|
||||
|
||||
md = md.replace(/`(.*?)`/gim,
|
||||
`<code class="cm-inline">$1</code>`
|
||||
);
|
||||
|
||||
md = md.replace(/\*\*(.*?)\*\*/gim, "<b>$1</b>");
|
||||
md = md.replace(/\*(.*?)\*/gim, "<i>$1</i>");
|
||||
|
||||
md = md.replace(/^\s*-\s(.+)$/gim, "<li>$1</li>");
|
||||
md = wrapLists(md);
|
||||
|
||||
md = md
|
||||
.replace(/\n{2,}/g, "</p><p>")
|
||||
.replace(/^(?!<h|<ul|<pre|<li|<\/)(.+)$/gim, "<p>$1</p>");
|
||||
|
||||
return buildToc(toc) + md;
|
||||
}
|
||||
};
|
||||
|
||||
function buildToc(items) {
|
||||
if (!items.length) return "";
|
||||
|
||||
let html = `<div class="cm-toc">`;
|
||||
|
||||
items.forEach(i => {
|
||||
html += `
|
||||
<div class="cm-toc-item level-${i.level}">
|
||||
<a href="#${i.id}">${i.title}</a>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `</div>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
function wrapLists(html) {
|
||||
return html.replace(/(<li>.*?<\/li>)/gs, "<ul>$1</ul>");
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
window.marked = marked;
|
||||
@@ -1,56 +1,42 @@
|
||||
# Casono Rules Easy Description
|
||||
|
||||
> Source: https://www.youtube.com/watch?v=h-1WyU5Wqsw&pp=ygUZbGVybmVuIHBva2VyIHRleGFzIGhvbGRlbQ%3D%3D
|
||||
> Quelle: https://www.youtube.com/watch?v=h-1WyU5Wqsw&pp=ygUZbGVybmVuIHBva2VyIHRleGFzIGhvbGRlbQ%3D%3D
|
||||
<!-- vim-markdown-toc GFM -->
|
||||
|
||||
* [Deustch](#deustch)
|
||||
* [Blinds (Small Blind & Big Blind)](#blinds-small-blind--big-blind)
|
||||
* [Erste Setzrunde (Preflop)](#erste-setzrunde-preflop)
|
||||
* [Beispiel Preflop](#beispiel-preflop)
|
||||
* [Flop (3 Gemeinschaftskarten)](#flop-3-gemeinschaftskarten)
|
||||
* [Beispiel Flop](#beispiel-flop)
|
||||
* [Turn (4. Karte)](#turn-4-karte)
|
||||
* [River (5. Karte)](#river-5-karte)
|
||||
* [Showdown (Gewinnentscheidung)](#showdown-gewinnentscheidung)
|
||||
* [Poker Hand Rankings (Gewichtung)](#poker-hand-rankings-gewichtung)
|
||||
* [Fazit](#fazit)
|
||||
* [English](#english)
|
||||
* [Blinds (Small Blind & Big Blind)](#blinds-small-blind--big-blind-1)
|
||||
* [First Betting Round (Preflop)](#first-betting-round-preflop)
|
||||
* [Preflop Example](#preflop-example)
|
||||
* [Flop (3 Community Cards)](#flop-3-community-cards)
|
||||
* [Flop Example](#flop-example)
|
||||
* [Turn (4th Card)](#turn-4th-card)
|
||||
* [River (5th Card)](#river-5th-card)
|
||||
* [Showdown (Winning Decision)](#showdown-winning-decision)
|
||||
* [Poker Hand Rankings (Hierarchy)](#poker-hand-rankings-hierarchy)
|
||||
* [Conclusion](#conclusion)
|
||||
* [Blinds (Small Blind & Big Blind)](#blinds-small-blind--big-blind)
|
||||
* [Erste Setzrunde (Preflop)](#erste-setzrunde-preflop)
|
||||
* [Beispiel Preflop](#beispiel-preflop)
|
||||
* [Flop (3 Gemeinschaftskarten)](#flop-3-gemeinschaftskarten)
|
||||
* [Beispiel Flop](#beispiel-flop)
|
||||
* [Turn (4. Karte)](#turn-4-karte)
|
||||
* [River (5. Karte)](#river-5-karte)
|
||||
* [Showdown (Gewinnentscheidung)](#showdown-gewinnentscheidung)
|
||||
* [Poker Hand Rankings (Gewichtung)](#poker-hand-rankings-gewichtung)
|
||||
* [Fazit](#fazit)
|
||||
|
||||
<!-- vim-markdown-toc -->
|
||||
|
||||
## Deutsch
|
||||
|
||||
Der Pokertisch ist ein unglaublich faszinierender Erlebnisraum, in dem man sehr viel lernen kann: über sich selbst, über andere Menschen und über Fragen wie: wie treffe ich eigentlich Entscheidungen, wie gehe ich mit Stress und Unsicherheit um und wie gut ich darin bin, mich in andere hineinzuversetzen und Situationen richtig einzuschätzen.
|
||||
Der Pokertisch ist ein unglaublich faszinierender Erlebnisraum, in dem man sehr viel lernen kann: über sich selbst, über andere Menschen und über Fragen wie: Wie treffe ich eigentlich Entscheidungen, wie gehe ich mit Stress und Unsicherheit um und wie gut ich darin bin, mich in andere hineinzuversetzen und Situationen richtig einzuschätzen.
|
||||
|
||||
Damit Du in diesem Erlebnisraum starten kannst, ist es – wie bei jedem Spiel – notwendig, zuerst die Grundregeln und den Spielablauf zu verstehen.
|
||||
|
||||
Also los geht es:
|
||||
|
||||
Wir haben am Tisch **4 Spieler**: Julian, Mathis, Jona und Lars. Jeder Spieler startet mit **20.000 Chips**. Jeder bekommt **2 Karten auf die Hand** und es gibt zusätzlich **5 Gemeinschaftskarten**, die später in der Mitte aufgedeckt werden.
|
||||
Wir haben am Tisch **4 Spieler**: Julian, Mathis, Jona und Lars. Jeder Spieler startet mit **20000 Chips ($)**. Jeder bekommt **2 Karten auf die Hand** und es gibt zusätzlich **5 Gemeinschaftskarten**, die später in der Mitte aufgedeckt werden.
|
||||
|
||||

|
||||

|
||||
|
||||
Die Spieler sitzen in folgender Reihenfolge: Julian, Mathis, Jona und Lars. Einer davon hat den Dealer-Button, der bestimmt, wer die Karten austeilt und von wo die Runde beginnt. Dieser Button wandert nach jeder Runde im Uhrzeigersinn weiter und verändert damit die Position ständig.
|
||||
|
||||
Regel: *34 Button Placement and Movement 🟢*
|
||||
|
||||
### Blinds (Small Blind & Big Blind)
|
||||
## Blinds (Small Blind & Big Blind)
|
||||
|
||||
Bevor die Karten verteilt werden, gibt es zwei Pflicht-Einsätze:
|
||||
|
||||
Der Small Blind und der Big Blind. Der Big Blind ist immer doppelt so hoch wie der Small Blind.
|
||||
|
||||

|
||||

|
||||
|
||||
Regel: *32 Dead Button 🟡*
|
||||
|
||||
@@ -58,7 +44,7 @@ Diese Einsätze sorgen dafür, dass sofort ein Pot entsteht und das Spiel überh
|
||||
|
||||
Danach werden die Karten verteilt: zuerst Small Blind, dann Big Blind und dann im Uhrzeigersinn alle anderen Spieler.
|
||||
|
||||
### Erste Setzrunde (Preflop)
|
||||
## Erste Setzrunde (Preflop)
|
||||
|
||||
Die erste Setzrunde beginnt immer bei dem Spieler links vom Big Blind.
|
||||
|
||||
@@ -73,251 +59,108 @@ Regel: *41 Methods of Calling 🟢*
|
||||
Regel: *42 Methods of Raising 🟢*
|
||||
Regel: *50 Acting in Turn 🟢*
|
||||
|
||||
### Beispiel Preflop
|
||||
## Beispiel Preflop
|
||||
|
||||
Julian schaut seine Karten an und entscheidet sich direkt für einen Call von **600 Chips**.
|
||||
Julian schaut seine Karten an und entscheidet sich direkt für einen Raise von **600 Chips**.
|
||||
|
||||

|
||||

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

|
||||

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

|
||||

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

|
||||

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

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

|
||||
|
||||
Es beginnt eine neue Setzrunde.
|
||||
|
||||
Regel: *49 Accepted Action 🟢*
|
||||
|
||||
### Beispiel Flop
|
||||
## Beispiel Flop
|
||||
|
||||
Lars setzt **1000 Chips** als Erstes. Julian entscheidet sich mitzugehen (Call), weil seine Karten durch die Gemeinschaftskarten stärker geworden sind.
|
||||
Jona setzt **1000 Chips** als Erstes. Lars entscheidet sich mitzugehen (Call), weil seine Karten durch die Gemeinschaftskarten stärker geworden sind.
|
||||
|
||||

|
||||
Julian steigt aus, weil er keine gute Verbindung mehr sieht. Mathis ist bereits raus.
|
||||
|
||||
Jona steigt aus, weil er keine gute Verbindung mehr sieht. Mathis ist bereits raus.
|
||||

|
||||
|
||||
### Turn (4. Karte)
|
||||
## Turn (4. Karte)
|
||||
|
||||
Jetzt kommt die **4. Gemeinschaftskarte**.
|
||||
|
||||
Wieder beginnt eine neue Setzrunde.
|
||||
|
||||
Lars setzt diesmal **3000 Chips**. Julian bezahlt erneut (Call), weil seine Hand weiterhin gut spielbar ist.
|
||||
Jona setzt diesmal **3000 Chips**. Lars bezahlt erneut (Call), weil seine Hand weiterhin gut spielbar ist.
|
||||
|
||||

|
||||

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

|
||||

|
||||
|
||||
Julian muss jetzt entscheiden: Fold, Call oder Raise auf **10000 Chips**.
|
||||
Lars muss jetzt entscheiden: Fold, Call oder Raise auf 10000 Chips.
|
||||
|
||||
Regel: *54 Pot Size Bets 🟡*
|
||||
|
||||
### Showdown (Gewinnentscheidung)
|
||||
## Showdown (Gewinnentscheidung)
|
||||
|
||||
Wenn nach der letzten Setzrunde noch zwei Spieler übrig sind, kommt es zum Showdown.
|
||||
|
||||
Beide Spieler zeigen ihre Karten offen. Gewonnen hat die **beste 5-Karten-Kombination aus Handkarten und Gemeinschaftskarten**.
|
||||
|
||||

|
||||

|
||||
|
||||
Regel: *12 Cards Speak at Showdown 🟢*
|
||||
Regel: *16 Face Up for All-Ins 🟢*
|
||||
Regel: *17 Non All-In Showdowns 🟢*
|
||||
|
||||
Wenn Julian den letzten Einsatz bezahlt, werden die Hände verglichen. Wenn er foldet, gewinnt Lars automatisch den gesamten Pot.
|
||||
Wenn Lars den letzten Einsatz bezahlt, werden die Hände verglichen. Wenn er foldet, gewinnt Jona automatisch den gesamten Pot.
|
||||
|
||||
### Poker Hand Rankings (Gewichtung)
|
||||
## Poker Hand Rankings (Gewichtung)
|
||||
|
||||
Die Kartenkombinationen sind klar geordnet – von schwach bis extrem stark:
|
||||
|
||||

|
||||
<img src="./images/12.png" height="600">
|
||||
|
||||
Je höher die Kombination, desto stärker die Hand und desto wahrscheinlicher der Gewinn.
|
||||
|
||||
Julian: 2. Paar:
|
||||
Jona: 2. Paar:
|
||||
|
||||

|
||||

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

|
||||

|
||||
|
||||
Da zwei Paare in der Rangfolge über einem einzelnen Paar stehen, gewinnt Julian diese Runde.
|
||||
Da zwei Paare in der Rangfolge über einem einzelnen Paar stehen, gewinnt Jona diese Runde.
|
||||
|
||||
### Fazit
|
||||
## Fazit
|
||||
|
||||
Poker ist kein Glücksspiel im klassischen Sinn, sondern ein Spiel aus Strategie, Psychologie und Mathematik. Jede Entscheidung von Julian, Mathis, Jona oder Lars verändert die komplette Dynamik am Tisch. Wer die Regeln versteht, versteht nicht nur Karten, sondern auch Menschen und Entscheidungen unter Druck.
|
||||
|
||||
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, it’s necessary—like with any game—to first understand the basic rules and game flow.
|
||||
|
||||
So let’s begin:
|
||||
|
||||
We have **4 players** at the table: Julian, Mathis, Jona, and Lars. Each player starts with **20,000 chips**. Each receives **2 hole cards**, and there are **5 community cards** that will later be revealed in the center.
|
||||
|
||||

|
||||
|
||||
The players are seated as follows: Julian, Mathis, Jona, and Lars. One of them holds the dealer button, which determines who deals the cards and where the round begins. This button moves clockwise after each hand, constantly changing the positions.
|
||||
|
||||
Rule: *34 Button Placement and Movement 🟢*
|
||||
|
||||
### Blinds (Small Blind & Big Blind)
|
||||
|
||||
Before cards are dealt, there are two mandatory bets:
|
||||
|
||||
The Small Blind and the Big Blind. The Big Blind is always double the amount of the Small Blind.
|
||||
|
||||

|
||||
|
||||
Rule: *32 Dead Button 🟡*
|
||||
|
||||
These forced bets ensure that a pot exists from the start, making the game begin with everyone already involved.
|
||||
|
||||
After this, the cards are dealt: first to the Small Blind, then the Big Blind, and then clockwise to all other players.
|
||||
|
||||
### First Betting Round (Preflop)
|
||||
|
||||
The first betting round always begins with the player to the left of the Big Blind.
|
||||
|
||||
Now each player must decide:
|
||||
|
||||
* Fold (exit the hand)
|
||||
* Call (match the current bet)
|
||||
* Raise (increase the bet)
|
||||
|
||||
Rule: *40 Methods of Betting 🟢*
|
||||
Rule: *41 Methods of Calling 🟢*
|
||||
Rule: *42 Methods of Raising 🟢*
|
||||
Rule: *50 Acting in Turn 🟢*
|
||||
|
||||
### Preflop Example
|
||||
|
||||
Julian looks at his cards and decides to Call for **600 chips**.
|
||||
|
||||

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

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

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

|
||||
|
||||
This immediately changes the situation: Julian and Jona must now decide whether to call this raise, re-raise, or fold.
|
||||
|
||||
### Flop (3 Community Cards)
|
||||
|
||||
Now **3 community cards** are placed face-up in the center. From this point, the game changes completely, as all players gain additional information.
|
||||
|
||||

|
||||
|
||||
A new betting round begins.
|
||||
|
||||
Rule: *49 Accepted Action 🟢*
|
||||
|
||||
### Flop Example
|
||||
|
||||
Lars bets **1,000 chips** first. Julian decides to Call because his hand has improved with the community cards.
|
||||
|
||||

|
||||
|
||||
Jona folds, as he no longer sees a strong connection. Mathis is already out.
|
||||
|
||||
### Turn (4th Card)
|
||||
|
||||
Now the **4th community card** is revealed.
|
||||
|
||||
Another betting round begins.
|
||||
|
||||
Lars bets **3,000 chips** this time. Julian calls again, as his hand remains strong.
|
||||
|
||||

|
||||
|
||||
Rule: *53 Action Out of Turn 🟡*
|
||||
|
||||
### River (5th Card)
|
||||
|
||||
Now the final community card is revealed.
|
||||
|
||||
This is the last decision point in the hand.
|
||||
|
||||
Lars bets **5,000 chips**.
|
||||
|
||||

|
||||
|
||||
Julian must now decide: Fold, Call, or even Raise to **10,000 chips**.
|
||||
|
||||
Rule: *54 Pot Size Bets 🟡*
|
||||
|
||||
### Showdown (Winning Decision)
|
||||
|
||||
If two or more players remain after the final betting round, a showdown occurs.
|
||||
|
||||
All active players reveal their cards. The winner is the one with the **best 5-card combination using any combination of their hole cards and the community cards**.
|
||||
|
||||

|
||||
|
||||
Rule: *12 Cards Speak at Showdown 🟢*
|
||||
Rule: *16 Face Up for All-Ins 🟢*
|
||||
Rule: *17 Non All-In Showdowns 🟢*
|
||||
|
||||
If Julian calls the final bet, the hands are compared. If he folds, Lars wins the entire pot automatically.
|
||||
|
||||
### Poker Hand Rankings (Hierarchy)
|
||||
|
||||
The card combinations are clearly ranked—from weakest to strongest:
|
||||
|
||||

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

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

|
||||
|
||||
Since two pair ranks higher than one pair, Julian wins this round.
|
||||
|
||||
### Conclusion
|
||||
|
||||
Poker is not gambling in the traditional sense, but rather a game of strategy, psychology, and mathematics. Every decision made by Julian, Mathis, Jona, or Lars changes the entire dynamic at the table. Understanding the rules means not only understanding cards, but also people and decision-making under pressure.
|
||||
|
||||
|
After Width: | Height: | Size: 958 KiB |
|
After Width: | Height: | Size: 12 MiB |
|
After Width: | Height: | Size: 14 MiB |
|
After Width: | Height: | Size: 14 MiB |
|
After Width: | Height: | Size: 13 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 12 MiB |
|
After Width: | Height: | Size: 12 MiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 740 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 863 KiB |
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 726 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 832 KiB |
|
After Width: | Height: | Size: 922 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 729 KiB |
@@ -0,0 +1,555 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Markdown Viewer</title>
|
||||
<script src="casono-markdown-render-engine.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #0d9e3b;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: 40px;
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
color: #ffffff;
|
||||
text-shadow: 2px 2px 0 #000;
|
||||
}
|
||||
|
||||
p, li {
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: gold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
code {
|
||||
background: rgba(0,0,0,0.4);
|
||||
padding: 2px 6px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid #5c3d10;
|
||||
}
|
||||
|
||||
.cm-toc {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.cm-toc-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.cm-toc-item {
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.cm-toc-item a {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cm-toc-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.level-2 { margin-left: 12px; opacity: 0.95; }
|
||||
.level-3 { margin-left: 24px; opacity: 0.85; }
|
||||
|
||||
.cm-h1 {
|
||||
font-size: 28px;
|
||||
margin-top: 20px;
|
||||
text-shadow: 2px 2px 0 #000;
|
||||
}
|
||||
|
||||
.cm-h2 {
|
||||
font-size: 22px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.cm-h3 {
|
||||
font-size: 18px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.cm-img {
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* CODE */
|
||||
.cm-code {
|
||||
background: rgba(0,0,0,0.35);
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.cm-inline {
|
||||
background: rgba(0,0,0,0.4);
|
||||
padding: 2px 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="content"></div>
|
||||
|
||||
<script>
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
const markdown = `
|
||||
# Start Game
|
||||
|
||||
Es wird Java 25 benötigt
|
||||
## Server starten
|
||||
|
||||
\`\`\`bash
|
||||
java -jar casono.jar server <listenport>
|
||||
\`\`\`
|
||||
## Client starten
|
||||
|
||||
\`\`\`bash
|
||||
java -jar casono.jar client <serverip>:<serverport> [username]
|
||||
\`\`\`
|
||||
|
||||
Die Parameter serverip, serverport, listenport und username müssen ersetzt werden
|
||||
# UI
|
||||
|
||||
## Lobby UI
|
||||
|
||||
### Eine Lobby erstellen
|
||||
|
||||

|
||||
|
||||
### Einer Lobby beitreten
|
||||
|
||||

|
||||
|
||||
### Den Username ändern
|
||||
|
||||

|
||||
|
||||
### Highscores
|
||||
|
||||

|
||||
|
||||
## Game UI
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Themes
|
||||
|
||||

|
||||
|
||||
### Black-and-White Theme
|
||||
|
||||

|
||||
|
||||
### Glass-Effect Theme
|
||||
|
||||

|
||||
|
||||
## Casono Browser
|
||||
|
||||
![[casono-browser.png]]
|
||||
|
||||

|
||||
|
||||
## Chat UI
|
||||
|
||||
### Globaler Chat
|
||||
|
||||

|
||||
|
||||
### Lobby Chat
|
||||
|
||||

|
||||
|
||||
### Whisper Chat
|
||||
|
||||

|
||||
|
||||
# Casono Rules
|
||||
|
||||
## 1. Spielübersicht
|
||||
|
||||
Texas Hold’em ist ein strategisches Kartenspiel für mehrere Spieler.
|
||||
Ziel ist es, den Pot (alle gesetzten Chips) zu gewinnen, entweder durch:
|
||||
|
||||
- die beste Kartenkombination am Ende der Runde
|
||||
- oder indem alle anderen Spieler vorher aussteigen (Fold)
|
||||
|
||||
### Grundregeln
|
||||
|
||||
- Jeder Spieler erhält 2 verdeckte Karten (Hole Cards)
|
||||
- Es werden 5 Gemeinschaftskarten offen in der Mitte ausgelegt
|
||||
- Jeder Spieler bildet die beste 5-Karten-Kombination aus:
|
||||
- eigenen Karten
|
||||
- und Gemeinschaftskarten
|
||||
|
||||
- Zu Spielbeginn erhält jeder Spieler ein Startgeld von 20000 Chips ($)
|
||||
|
||||
## 2. Sitzposition & Dealer-Button
|
||||
|
||||
Der sogenannte Dealer-Button bestimmt die Positionen am Tisch:
|
||||
|
||||
- Er zeigt an, wer als „Geber“ (Dealer) fungiert
|
||||
- Die Positionen rotieren im Uhrzeigersinn nach jeder Runde
|
||||
|
||||
Die Position ist entscheidend, da sie bestimmt:
|
||||
|
||||
- die Reihenfolge der Aktionen
|
||||
- wer die Blinds setzen muss
|
||||
|
||||
## 3. Blinds (Pflichteinsätze)
|
||||
|
||||
Vor jeder Runde werden zwei verpflichtende Einsätze geleistet:
|
||||
|
||||
- **Small Blind** (kleiner Blind): 100 Chips – gesetzt vom Spieler links neben dem Dealer
|
||||
- **Big Blind** (großer Blind): 200 Chips - gesetzt vom Spieler zwei Plätze links vom Dealer
|
||||
|
||||
Diese Einsätze sorgen dafür, dass:
|
||||
|
||||
- ein Startpot entsteht
|
||||
- jede Runde aktiv gespielt wird
|
||||
|
||||
## 4. Spielablauf im Detail
|
||||
|
||||
### 4.1 Preflop (erste Setzrunde)
|
||||
|
||||
Nach dem Austeilen der Karten beginnt die erste Setzrunde.
|
||||
|
||||
Der Spieler links vom Big Blind eröffnet die Runde.
|
||||
|
||||
Jeder Spieler hat folgende Optionen:
|
||||
|
||||
- **Fold** – Karten ablegen und aussteigen
|
||||
- **Call** – Einsatz mitgehen
|
||||
- **Raise** – Einsatz erhöhen
|
||||
|
||||
### 4.2 Flop (3 Gemeinschaftskarten)
|
||||
|
||||
- Drei Karten werden offen auf den Tisch gelegt
|
||||
- Eine neue Setzrunde beginnt
|
||||
- Die Setzrunde beginnt jetzt immer beim ersten aktiven Spieler links vom Dealer (im Uhrzeigersinn).
|
||||
- Der erste Spieler bei der Flop Runde muss keinen höheren Einsatz setzen als der letzte Spieler aus der Preflop Runde, allerdings muss er seinen eigenen Einsatz aus der Preflop Runde überbieten.
|
||||
|
||||
Ab diesem Zeitpunkt können alle Spieler ihre Strategie anhand zusätzlicher Informationen anpassen.
|
||||
|
||||
### 4.3 Turn (4. Gemeinschaftskarte)
|
||||
|
||||
- Die vierte Karte wird aufgedeckt
|
||||
- Eine weitere Setzrunde folgt
|
||||
|
||||
Die Einsätze werden oft höher, da sich stärkere Hände entwickeln.
|
||||
|
||||
### 4.4 River (5. Gemeinschaftskarte)
|
||||
|
||||
- Die letzte Karte wird aufgedeckt
|
||||
- Letzte Setzrunde
|
||||
|
||||
Dies ist die finale Entscheidungsphase:
|
||||
|
||||
- Maximierung des Gewinns
|
||||
- oder Minimierung von Verlusten
|
||||
|
||||
## 5. Showdown (Kartenvergleich)
|
||||
|
||||
Wenn nach der letzten Setzrunde mindestens zwei Spieler verbleiben:
|
||||
|
||||
- Alle verbleibenden Spieler decken ihre Karten auf
|
||||
- Die **beste 5-Karten-Kombination gewinnt**
|
||||
|
||||
Wichtig:
|
||||
|
||||
- Die Karten „sprechen für sich“ – die beste Hand zählt unabhängig von Ansagen
|
||||
|
||||
## 6. Poker-Handrangfolge
|
||||
|
||||
Die Stärke der Hände ist eindeutig festgelegt (von schwach nach stark):
|
||||
|
||||
1. High Card (höchste Einzelkarte)
|
||||
2. One Pair (ein Paar)
|
||||
3. Two Pair (zwei Paare)
|
||||
4. Three of a Kind (Drilling)
|
||||
5. Straight (Straße)
|
||||
6. Flush (Farbe)
|
||||
7. Full House
|
||||
8. Four of a Kind (Vierling)
|
||||
9. Straight Flush
|
||||
10. Royal Flush
|
||||
|
||||
Je höher die Kombination, desto stärker die Hand.
|
||||
|
||||
## 7. Wichtige Grundprinzipien
|
||||
|
||||
### Reihenfolge beachten
|
||||
|
||||
Spieler müssen immer der Reihe nach handeln.
|
||||
|
||||
### Klare Aktionen
|
||||
|
||||
Alle Aktionen müssen eindeutig sein:
|
||||
|
||||
- Einsätze klar ansagen oder eindeutig setzen
|
||||
|
||||
### Ein Spieler – eine Hand
|
||||
|
||||
- Spieler dürfen ihre Karten nicht teilen oder gemeinsam spielen
|
||||
|
||||
### Fehlerhafte Einsätze
|
||||
|
||||
- Unklare oder falsche Einsätze können korrigiert werden, abhängig von der Spielsituation (nur wenn der Einsatz
|
||||
außerhalb der gültigen Grenzen liegt; zu hohe oder unzulässige Beträge werden blockiert und nicht automatisch
|
||||
korrigiert)
|
||||
|
||||
## 8. Strategische Einordnung
|
||||
|
||||
Texas Hold’em ist kein reines Glücksspiel. Der Erfolg basiert auf:
|
||||
|
||||
- Wahrscheinlichkeiten (Mathematik)
|
||||
- Einschätzung von Gegnern (Psychologie)
|
||||
- Positionsspiel und Timing
|
||||
|
||||
# Casono Rules Easy Description
|
||||
|
||||
Der Pokertisch ist ein unglaublich faszinierender Erlebnisraum, in dem man sehr viel lernen kann: über sich selbst, über
|
||||
andere Menschen und über Fragen wie: Wie treffe ich eigentlich Entscheidungen, wie gehe ich mit Stress und Unsicherheit
|
||||
um und wie gut ich darin bin, mich in andere hineinzuversetzen und Situationen richtig einzuschätzen.
|
||||
|
||||
Damit Du in diesem Erlebnisraum starten kannst, ist es – wie bei jedem Spiel – notwendig, zuerst die Grundregeln und den
|
||||
Spielablauf zu verstehen.
|
||||
|
||||
Also los geht es:
|
||||
|
||||
Wir haben am Tisch **4 Spieler**: Julian, Mathis, Jona und Lars. Jeder Spieler startet mit **20000 Chips ($)**. Jeder
|
||||
bekommt **2 Karten auf die Hand** und es gibt zusätzlich **5 Gemeinschaftskarten**, die später in der Mitte aufgedeckt
|
||||
werden.
|
||||
|
||||

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

|
||||
|
||||
Regel: *32 Dead Button 🟡*
|
||||
|
||||
Diese Einsätze sorgen dafür, dass sofort ein Pot entsteht und das Spiel überhaupt beginnt, weil jeder schon “im Spiel”
|
||||
ist.
|
||||
|
||||
Danach werden die Karten verteilt: zuerst Small Blind, dann Big Blind und dann im Uhrzeigersinn alle anderen Spieler.
|
||||
|
||||
## Erste Setzrunde (Preflop)
|
||||
|
||||
Die erste Setzrunde beginnt immer bei dem Spieler links vom Big Blind.
|
||||
|
||||
Jetzt muss jeder Spieler entscheiden:
|
||||
|
||||
* Fold (aussteigen)
|
||||
* Call (mitgehen)
|
||||
* Raise (erhöhen)
|
||||
|
||||
Regel: *40 Methods of Betting 🟢*
|
||||
Regel: *41 Methods of Calling 🟢*
|
||||
Regel: *42 Methods of Raising 🟢*
|
||||
Regel: *50 Acting in Turn 🟢*
|
||||
|
||||
## Beispiel Preflop
|
||||
|
||||
Julian schaut seine Karten an und entscheidet sich direkt für einen Raise von **600 Chips**.
|
||||
|
||||

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

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

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

|
||||
|
||||
Damit verändert sich sofort die Situation: Julian und Jona müssen entscheiden, ob sie diesen Raise bezahlen, selbst
|
||||
erhöhen oder aussteigen.
|
||||
|
||||
## Flop (3 Gemeinschaftskarten)
|
||||
|
||||
Jetzt werden **3 Gemeinschaftskarten** in die Mitte gelegt. Ab hier verändert sich das Spiel komplett, weil alle Spieler
|
||||
zusätzliche Informationen bekommen.
|
||||
|
||||
Die Setzrunde beginnt jetzt immer beim ersten aktiven Spieler links vom Dealer (im Uhrzeigersinn).
|
||||
|
||||

|
||||
|
||||
Es beginnt eine neue Setzrunde.
|
||||
|
||||
Regel: *49 Accepted Action 🟢*
|
||||
|
||||
## Beispiel Flop
|
||||
|
||||
Jona setzt **1000 Chips** als Erstes. Lars entscheidet sich mitzugehen (Call), weil seine Karten durch die
|
||||
Gemeinschaftskarten stärker geworden sind.
|
||||
|
||||
Julian steigt aus, weil er keine gute Verbindung mehr sieht. Mathis ist bereits raus.
|
||||
|
||||

|
||||
|
||||
## Turn (4. Karte)
|
||||
|
||||
Jetzt kommt die **4. Gemeinschaftskarte**.
|
||||
|
||||
Wieder beginnt eine neue Setzrunde.
|
||||
|
||||
Jona setzt diesmal **3000 Chips**. Lars bezahlt erneut (Call), weil seine Hand weiterhin gut spielbar ist.
|
||||
|
||||

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

|
||||
|
||||
Lars muss jetzt entscheiden: Fold, Call oder Raise auf 10000 Chips.
|
||||
|
||||
Regel: *54 Pot Size Bets 🟡*
|
||||
|
||||
## Showdown (Gewinnentscheidung)
|
||||
|
||||
Wenn nach der letzten Setzrunde noch zwei Spieler übrig sind, kommt es zum Showdown.
|
||||
|
||||
Beide Spieler zeigen ihre Karten offen. Gewonnen hat die **beste 5-Karten-Kombination aus Handkarten und
|
||||
Gemeinschaftskarten**.
|
||||
|
||||

|
||||
|
||||
Regel: *12 Cards Speak at Showdown 🟢*
|
||||
Regel: *16 Face Up for All-Ins 🟢*
|
||||
Regel: *17 Non All-In Showdowns 🟢*
|
||||
|
||||
Wenn Lars den letzten Einsatz bezahlt, werden die Hände verglichen. Wenn er foldet, gewinnt Jona automatisch den
|
||||
gesamten Pot.
|
||||
|
||||
## Poker Hand Rankings (Gewichtung)
|
||||
|
||||
Die Kartenkombinationen sind klar geordnet – von schwach bis extrem stark:
|
||||
|
||||
<img src="./images/12.png" height="600">
|
||||
|
||||
Je höher die Kombination, desto stärker die Hand und desto wahrscheinlicher der Gewinn.
|
||||
|
||||
Jona: 2. Paar:
|
||||
|
||||

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

|
||||
|
||||
Da zwei Paare in der Rangfolge über einem einzelnen Paar stehen, gewinnt Jona diese Runde.
|
||||
|
||||
## Fazit
|
||||
|
||||
Poker ist kein Glücksspiel im klassischen Sinn, sondern ein Spiel aus Strategie, Psychologie und Mathematik. Jede
|
||||
Entscheidung von Julian, Mathis, Jona oder Lars verändert die komplette Dynamik am Tisch. Wer die Regeln versteht,
|
||||
versteht nicht nur Karten, sondern auch Menschen und Entscheidungen unter Druck.
|
||||
|
||||
Regel: *67 One Player One Hand 🟢*
|
||||
Regel: *52 Incorrect Bets 🟡*
|
||||
Regel: *57 Non-Standard Betting 🟡*
|
||||
`;
|
||||
|
||||
document.getElementById("content").innerHTML =
|
||||
marked.parse(markdown);
|
||||
|
||||
document.querySelectorAll("#content a").forEach(link => {
|
||||
link.addEventListener("click", (e) => {
|
||||
const href = link.getAttribute("href");
|
||||
|
||||
if (href && href.endsWith(".md")) {
|
||||
e.preventDefault();
|
||||
alert("You cannot load an external MD file using `file:///` without a server.");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<noscript>
|
||||
<div style="
|
||||
padding: 40px;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #0d9e3b;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
">
|
||||
<h2>JavaScript is disabled</h2>
|
||||
|
||||
<p>
|
||||
The Casono Browser requires JavaScript for the <br>
|
||||
Casono Markdown Render Engine to display content correctly.<br><br>
|
||||
|
||||
For security, JavaScript is disabled by default in the Casono Browser and should only be enabled when needed.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Please enable JavaScript in your Browser settings and reload the page.
|
||||
</p>
|
||||
|
||||
<img src="images/activate-js.png" alt="JavaScript aktivieren Anleitung" style="max-width: 100%; margin-top: 20px; border-radius: 10px;">
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 211 KiB |
|
After Width: | Height: | Size: 70 KiB |
@@ -0,0 +1,144 @@
|
||||
\documentclass{article}
|
||||
\setlength{\parindent}{0pt}
|
||||
\usepackage[a4paper,margin=2.5cm]{geometry}
|
||||
\usepackage{listings}
|
||||
|
||||
% External links
|
||||
\usepackage{hyperref}
|
||||
|
||||
% Colored text
|
||||
\usepackage{xcolor}
|
||||
|
||||
% Customized table
|
||||
\usepackage{float}
|
||||
\usepackage{array}
|
||||
\newcolumntype{L}[1]{>{\raggedright\arraybackslash}p{#1}}
|
||||
\usepackage{booktabs}
|
||||
|
||||
\title{QS-Konzept für MS4}
|
||||
\author{Syntax Syndicate - Casono}
|
||||
\begin{document}
|
||||
\maketitle
|
||||
|
||||
|
||||
\section{Einleitung}
|
||||
Dieses Dokument beschreibt die Software-Qualitätssicherung für unser Mehrspieler-Spiel \textit{Casono}. Es wird als Teil der Programmierprojekt-Vorlesung (ehemals bekannt als cs108) an der Universität Basel von uns entwickelt.\\
|
||||
|
||||
Dieses Konzept basiert auf der \textit{DIN ISO 9126} und ist demzufolge in folgende Themenbereiche unterteilt:
|
||||
\begin{itemize}
|
||||
\item \textbf{Konstruktives Qualitätsmanagement} - Maßnahmen, die \emph{während} der Entwicklung ergriffen werden, um die Qualität von Anfang an sicherzustellen.
|
||||
Einschließlich technischer Standards, Werkzeuge und internen Organisation.
|
||||
\item \textbf{Analytisches Qualitätsmanagement} - Maßnahmen zur \emph{Kontrolle und Untersuchung} des bestehenden Produkts, durch Anwendung automatisierte Analysen, Metrik-Schwellenwerte und Unit-Tests.
|
||||
\end{itemize}
|
||||
|
||||
|
||||
\section{Konstruktives Qualitätsmanagement}
|
||||
Ein konstruktives Qualitätsmanagement umfasst alle Maßnahmen, die proaktiv Mängel verhindern, indem gemeinsame Standards, Prozesse und Werkzeuge festgelegt werden, die jedes Teammitglied während der Entwicklung einhält.
|
||||
|
||||
\subsection{Technische Maßnahmen}
|
||||
|
||||
\subsubsection{JavaDoc}
|
||||
Alle öffentlichen Klassen, Interfaces, Records und Methoden müssen durch JavaDoc dokumentiert werden.\\
|
||||
Als Teil der GitLab-Build-Pipeline wird JavaDoc auf Richtigkeit überprüft. Werden strukturelle Fehler wie ungültige Referenzen erkannt, wird der Merge verhindert.\\
|
||||
Unsere aktuelle Grundlage sind daher 0 Fehler. Für den kommenden Meilenstein ist aber unser Ziel, die Regelungen weiter zu verschärfen, sodass auch Warnungen einen Merge verhindern.\\
|
||||
|
||||
Unsere implizite Form für JavaDoc-Kommentare ist wie folgt:
|
||||
\begin{itemize}
|
||||
\item Eine prägnante Zusammenfassung in einem Satz in der ersten Zeile
|
||||
\item Zusätzliche Informationen oder Beispiele in weiteren Absätzen.
|
||||
\item Dokumentation aller Parameter (\texttt{@param}), Rückgabewerte (\texttt{@return}) und Fehler (\texttt{@throws}).
|
||||
\end{itemize}
|
||||
Beginnend mit dem fünften Meilenstein soll diese Form ebenfalls durch einen Job in der CI-Pipeline überprüft werden.
|
||||
|
||||
\newpage
|
||||
\subsubsection{Logging}
|
||||
Das Projekt verwendet Log4J~2 (\texttt{log4j-api} und \texttt{log4j-core} zur Laufzeit) zusammen mit Jansi für eine farbige Konsolenausgabe.
|
||||
Das Format der Ausgabe ist durch eine Konfigurationsdatei vereinheitlicht.\\
|
||||
|
||||
Die Protokollstufen werden wie folgt einheitlich verwendet:
|
||||
\begin{itemize}
|
||||
\item \texttt{DEBUG} für interne Zustandsänderungen
|
||||
\item \texttt{INFO} für Lebenszyklusereignisse wie Serverstart, Verbindungsaufbau/-trennung des Clients
|
||||
\item \texttt{WARN} für behebbare Anomalien
|
||||
\item \texttt{ERROR} für nicht behebbare Fehler
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Organisatorische Maßnahmen}
|
||||
|
||||
\subsubsection{Teamkultur und die \texttt{CONTRIBUTORS}-Datei}
|
||||
Das Stammverzeichnis des GitLab-Repositories enthält eine Datei namens \texttt{CONTRIBUTORS.md}, die die Teamkonventionen dokumentiert:
|
||||
\begin{itemize}
|
||||
\item Die Verwendung von Branches - Feature-Branches werden in \texttt{main} zusammengeführt
|
||||
\item Merge-Richtlinie - CI muss bestanden werden. Keine ausdrückliche Genehmigung durch Kollegen erforderlich \footnote{Diese Regelung wird sich voraussichtlich ändern. Siehe dazu den Abschnitt zur \texttt{CODEOWNERS}-Datei.}
|
||||
\item Namenskonventionen für Branches und Commits
|
||||
\item Code-Stil - Google-Java-Format, AOSP-Variante, Einrückung mit vier Leerzeichen
|
||||
\item Jede Änderung beginnt mit einem Issue oder Task in GitLab, bevor mit der Umsetzung begonnen wird
|
||||
\item Branch-Namen folgen dem Muster \texttt{<typ>/<kurzbeschreibung>} gemäß \href{https://conventional-branch.github.io/}{Conventional Branch} Standard
|
||||
\item Commit-Messages folgen dem \href{https://www.conventionalcommits.org/en/v1.0.0/}{Conventional Commits} Standard
|
||||
\item Code-Style wird durch Linter (Checkstyle) und Formatter (Spotless, Google/AOSP Java Style) automatisiert überprüft
|
||||
\item Merge Anfragen müssen eine Beschreibung enthalten und auf das zugehörige Issue oder den Task verweisen
|
||||
\item Zusammenarbeit und Kommunikation erfolgen bevorzugt über Issue-Kommentare, nicht über private Nachrichten
|
||||
\end{itemize}
|
||||
|
||||
\subsubsection{GitLab-Task- und Issue-Vorlagen für Fortschrittsverfolgung}
|
||||
GitLab-Vorlagen für Tasks und Issues werden für alle geplanten Arbeitselemente verwendet. Die Aufgabenvorlage sorgt für eine einheitliche Struktur, die die Fortschrittsverfolgung erleichtert und das Risiko vager oder unvollständiger Arbeitselemente reduziert.
|
||||
|
||||
Durch die Vergabe von Labels kann Tasks und Issues weiterer Kontext gegeben werden.
|
||||
|
||||
|
||||
\newpage
|
||||
\section{Analytisches Qualitätsmanagement}
|
||||
\subsection{Analytische Verfahren}
|
||||
|
||||
\subsubsection{Eigentumsrechte an Programmcode über GitLab \texttt{CODEOWNERS}}
|
||||
Beginnend mit dem fünften Meilenstein soll eine \texttt{CODEOWNERS}-Datei erstellt werden, welche Teammitgliedern die Eigentumsrechte an bestimmten Teilen des Programmcodes zuspricht.
|
||||
Wollen andere Teammitglieder Änderungen an diesen Teilen vornehmen, muss der Eigentümer sie freigeben.\\
|
||||
Aktuell ist jedoch noch unklar, ob diese Funktion genutzt werden kann, da die Instanz mindestens die \textit{Premium}-Stufe haben muss.
|
||||
|
||||
\subsubsection{Automatisiertes CI-Linting und Build-Verifizierung}
|
||||
Jeder Push und jede Merge-Anfrage löst die CI-Pipeline aus.
|
||||
|
||||
Für Pushes und Merge-Anfrage setzt sich die Pipeline aus folgenden Phasen zusammen:
|
||||
\begin{enumerate}
|
||||
\item \textbf{Linting-Phase} \textcolor{blue}{[Push, MR]} - Checkstyle und Spotless überprüfen, ob der Programmcode dem vereinbarten Stil entspricht.
|
||||
\item \textbf{Build-Phase} \textcolor{blue}{[Push, MR]} - \texttt{./gradlew assemble} prüft, ob das gesamte Projekt fehlerfrei kompiliert werden kann.
|
||||
\item \textbf{Checkstyle-Report} \textcolor{blue}{[MR]} - Ein zusätzlicher Checkstyle-Report wird für Merge-Anfragen erzeugt und als Code-Quality-Report bereitgestellt.
|
||||
\item \textbf{Javadoc-Prüfung} \textcolor{blue}{[MR]} - JavaDoc wird auf Korrektheit geprüft.
|
||||
\item \textbf{Test-Phase} \textcolor{blue}{[Push, MR]} - Automatisierte Tests werden ausgeführt und ein Testreport erzeugt.
|
||||
\end{enumerate}
|
||||
Fehlschlagende Jobs verhindern einen Push nicht, jedoch wird ein Merge so lange verhindert, bis alle Jobs fehlerfrei abgeschlossen werden können.
|
||||
|
||||
\subsection{Testverfahren}
|
||||
Unit-Tests werden mit \textit{JUnit 5} geschrieben und in der Testphase der CI-Pipeline ausgeführt. Ein fehlgeschlagener Test blockiert die Merge-Anfrage.
|
||||
|
||||
Als weiteres Werkzeug verwenden wir \textit{JaCoCo} für die Ermittlung der Testabdeckung. \\
|
||||
Die aktuelle Testabdeckung ist wie folgt:
|
||||
|
||||
\begin{table}[H]
|
||||
\centering
|
||||
\renewcommand{\arraystretch}{1.4}
|
||||
\begin{tabular}{L{7.5cm} r r}
|
||||
\toprule
|
||||
\textbf{Paketname (gekürzt)} & \textbf{Instr.\ Cov.} & \textbf{Branch Cov.} \\
|
||||
\midrule
|
||||
\texttt{client.chat} & 0\,\% & 0\,\% \\
|
||||
\texttt{client.game} & 0\,\% & 0\,\% \\
|
||||
\texttt{client.network} & 3\,\% & 1\,\% \\
|
||||
\texttt{client.ui} (von Abdeckung ausgeschlossen) & 0\,\% & 0\,\% \\
|
||||
\texttt{server.app.checks} & 0\,\% & 0\,\% \\
|
||||
\texttt{server.app.commands} & 0\,\% & 0\,\% \\
|
||||
\texttt{server.domain.game} & 94\,\% & 91\,\% \\
|
||||
\texttt{server.domain.*} & 45\,\% & 37\,\% \\
|
||||
\texttt{server.network.transport} & 0\,\% & 0\,\% \\
|
||||
\texttt{server.network.protocol} & 0\,\% & 0\,\% \\
|
||||
\texttt{server.network.*} & 2\,\% & 1\,\% \\
|
||||
\midrule
|
||||
\textbf{Summe} & \textbf{12\,\%} & \textbf{9\,\%} \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{table}
|
||||
|
||||
Unser Ziel ist es, realistische Anforderungen an unsere Testabdeckung zu stellen.
|
||||
Bis zum fünften Meilenstein wollen wir daher für wichtige Bereiche, wie die Game-Engine und interne Komponenten des Netzwerks, eine möglichst hohe Abdeckung von mindestens 50\% erzielen.
|
||||
|
||||
\end{document}
|
||||
@@ -0,0 +1,250 @@
|
||||
\documentclass{article}
|
||||
\setlength{\parindent}{0pt}
|
||||
\usepackage[a4paper,margin=2.5cm]{geometry}
|
||||
\usepackage{listings}
|
||||
|
||||
% External links
|
||||
\usepackage{hyperref}
|
||||
|
||||
% Colored text
|
||||
\usepackage{xcolor}
|
||||
% Graphics
|
||||
\usepackage{graphicx}
|
||||
|
||||
% Customized table
|
||||
\usepackage{float}
|
||||
\usepackage{array}
|
||||
\newcolumntype{L}[1]{>{\raggedright\arraybackslash}p{#1}}
|
||||
\usepackage{booktabs}
|
||||
|
||||
% Create plots
|
||||
\usepackage{pgfplots}
|
||||
\usepackage{pgf-pie}
|
||||
|
||||
\title{QS-Report für MS6}
|
||||
\author{Syntax Syndicate - Casono}
|
||||
\begin{document}
|
||||
\maketitle
|
||||
|
||||
\section{Einleitung}
|
||||
Dieses Dokument ist die Fortsetzung des QA-Konzeptes für unser Mehrspieler-Spiel \textit{Casono}.
|
||||
Es wurde von uns als Teil der Programmierprojekt-Vorlesung (ehemals bekannt als cs108) an der Universität Basel von uns entwickelt.
|
||||
|
||||
Dieser Report folgt dem selben \textit{DIN ISO 9126}, und ist wie auch das Konzept für den ersten Teil in die selben Themenbereiche unterteilt:
|
||||
\begin{itemize}
|
||||
\item \textbf{Konstruktives Qualitätsmanagement} - Maßnahmen, die \emph{während} der Entwicklung ergriffen werden, um die Qualität von Anfang an sicherzustellen.
|
||||
Einschließlich technischer Standards, Werkzeuge und internen Organisation.
|
||||
\item \textbf{Analytisches Qualitätsmanagement} - Maßnahmen zur \emph{Kontrolle und Untersuchung} des bestehenden Produkts, durch Anwendung automatisierte Analysen, Metrik-Schwellenwerte und Unit-Tests.
|
||||
\end{itemize}
|
||||
|
||||
\section{Konstruktives Qualitätsmanagement}
|
||||
Ein konstruktives Qualitätsmanagement umfasst alle Maßnahmen, die proaktiv Mängel verhindern, indem gemeinsame Standards, Prozesse und Werkzeuge festgelegt werden, die jedes Teammitglied während der Entwicklung einhält.
|
||||
|
||||
\subsection{Technische Maßnahmen}
|
||||
|
||||
\subsubsection{JavaDoc}
|
||||
JavaDoc wurde genutzt um alle Öffentlichen Klassen, Interfaces, Records und Methoden zu dokumentieren.
|
||||
Als Teil der GitLab-Build-Pipeline wird JavaDoc auf Richtigkeit überprüft. Werden strukturelle Fehler wie ungültige Referenzen erkannt, schlägt die Pipeline fehl und der Merge wird verhindert.\\
|
||||
|
||||
Unsere implizite Form für JavaDoc-Kommentare ist wie folgt:
|
||||
\begin{itemize}
|
||||
\item Eine prägnante Zusammenfassung in einem Satz in der ersten Zeile
|
||||
\item Zusätzliche Informationen oder Beispiele in weiteren Absätzen.
|
||||
\item Dokumentation aller Parameter (\texttt{@param}), Rückgabewerte (\texttt{@return}) und Fehler (\texttt{@throws}).
|
||||
\end{itemize}
|
||||
Leider konnten wir das in MS4 vorgenommene Ziel, das auch Warnungen zu einem Abbruch der Pipeline führen, aus Technischen Gründen nicht umsetzen.
|
||||
|
||||
\newpage
|
||||
\subsubsection{Logging}
|
||||
Das Projekt verwendet Log4J~2 (\texttt{log4j-api} und \texttt{log4j-core} zur Laufzeit) zusammen mit Jansi für eine farbige Konsolenausgabe.
|
||||
Das Format der Ausgabe ist durch eine Konfigurationsdatei vereinheitlicht.\\
|
||||
|
||||
Die Protokollstufen werden wie folgt einheitlich verwendet:
|
||||
\begin{itemize}
|
||||
\item \texttt{DEBUG} für interne Zustandsänderungen
|
||||
\item \texttt{INFO} für Lebenszyklusereignisse wie Serverstart, Verbindungsaufbau/-trennung des Clients
|
||||
\item \texttt{WARN} für behebbare Anomalien
|
||||
\item \texttt{ERROR} für nicht behebbare Fehler
|
||||
\end{itemize}
|
||||
|
||||
Im Gegensatz zu MS4 wurden die Namen der Logger gekürzt und nicht mehr der gesamte Classpath verwendet. Ein Beispiel für die Ausgabe sieht so aus:
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=\textwidth]{../../images/milestones/ms-6/qa-report/server_log.png}
|
||||
\caption{Konsolenausgabe des Servers während dem Verbindungsaufbau und Anmeldung eines Nutzers}
|
||||
\end{figure}
|
||||
|
||||
\subsection{Organisatorische Maßnahmen}
|
||||
|
||||
\subsubsection{Teamkultur und die \texttt{CONTRIBUTORS}-Datei}
|
||||
Das Stammverzeichnis des GitLab-Repositories enthält eine Datei namens \texttt{CONTRIBUTORS.md}, die die seit MS4 unveränderten Teamkonventionen dokumentiert:
|
||||
\begin{itemize}
|
||||
\item Die Verwendung von Branches - Feature-Branches werden in \texttt{main} zusammengeführt
|
||||
\item Merge-Richtlinie - CI muss bestanden werden. Eine Genehmigung durch Kollegen ist wünschenswert wenn Änderungen an dessen Code vorgenommen wurden.
|
||||
\item Namenskonventionen für Branches und Commits
|
||||
\item Code-Stil - Google-Java-Format, AOSP-Variante, Einrückung mit vier Leerzeichen
|
||||
\item Jede Änderung beginnt mit einem Issue oder Task in GitLab, bevor mit der Umsetzung begonnen wird
|
||||
\item Branch-Namen folgen dem Muster \texttt{<typ>/<kurzbeschreibung>} gemäß \href{https://conventional-branch.github.io/}{Conventional Branch} Standard
|
||||
\item Commit-Messages folgen dem \href{https://www.conventionalcommits.org/en/v1.0.0/}{Conventional Commits} Standard
|
||||
\item Code-Style wird durch Linter (Checkstyle) und Formatter (Spotless, Google/AOSP Java Style) automatisiert überprüft
|
||||
\item Merge Anfragen müssen eine Beschreibung enthalten und auf das zugehörige Issue oder den Task verweisen
|
||||
\item Zusammenarbeit und Kommunikation erfolgen bevorzugt über Issue-Kommentare, nicht über private Nachrichten
|
||||
\end{itemize}
|
||||
|
||||
\subsubsection{GitLab-Task- und Issue-Vorlagen für Fortschrittsverfolgung}
|
||||
GitLab-Vorlagen für Tasks und Issues werden für alle geplanten Arbeitselemente verwendet. Die Aufgabenvorlage sorgt für eine einheitliche Struktur, die die Fortschrittsverfolgung erleichtert und das Risiko vager oder unvollständiger Arbeitselemente reduziert.
|
||||
|
||||
Durch die Vergabe von Labels kann Tasks und Issues weiterer Kontext gegeben werden.
|
||||
|
||||
\newpage
|
||||
\section{Analytisches Qualitätsmanagement}
|
||||
|
||||
\subsection{Analytische Verfahren}
|
||||
|
||||
\subsubsection{Eigentumsrechte an Programmcode über GitLab \texttt{CODEOWNERS}}
|
||||
In unserem Konzept haben wir uns vorgenommen, durch eine \texttt{CODEOWNERS} Datei den Teammitgliedern die Eigentumsrechte ihres Programmcodes zuzusprechen. Leider hat sich bestätigt das die von SciCORE betriebene Instanz nicht die Premium-Stufe hat, wodurch diese Funktion für uns nicht zur Verfügung steht. \\
|
||||
|
||||
Statt auf eine Automatisierte Grundlage zu vertrauen, haben wir uns für den nächstbesten Weg der Disziplinarität entschieden, wie am Beispiel von \href{https://git.scicore.unibas.ch/cs108-fs26/Gruppe-13/-/merge_requests/166}{dieser Merge-Anfrage} gesehen werden kann.
|
||||
|
||||
\subsubsection{Automatisiertes CI-Linting und Build-Verifizierung}
|
||||
Jeder Push und jede Merge-Anfrage löst die CI-Pipeline aus.
|
||||
|
||||
Für Pushes und Merge-Anfragen setzt sich die Pipeline aus folgenden Phasen zusammen:
|
||||
\begin{enumerate}
|
||||
\item \textbf{Linting-Phase} \textcolor{blue}{[Push, MR]} - Checkstyle und Spotless überprüfen, ob der Programmcode dem vereinbarten Stil entspricht.
|
||||
\item \textbf{Build-Phase} \textcolor{blue}{[Push, MR]} - \texttt{./gradlew assemble} prüft, ob das gesamte Projekt fehlerfrei kompiliert werden kann.
|
||||
\item \textbf{Checkstyle-Report} \textcolor{blue}{[MR]} - Ein zusätzlicher Checkstyle-Report wird für Merge-Anfragen erzeugt und als Code-Quality-Report bereitgestellt.
|
||||
\item \textbf{Javadoc-Prüfung} \textcolor{blue}{[MR]} - JavaDoc wird auf Korrektheit geprüft.
|
||||
\item \textbf{Test-Phase} \textcolor{blue}{[Push, MR]} - Automatisierte Tests werden ausgeführt und ein Testreport erzeugt.
|
||||
\end{enumerate}
|
||||
Die Entscheidung das fehlgeschlagene Jobs keinen Push aber dafür eine Merge-Anfrage verhindern können, hat sich als vernünftig bewehrt.
|
||||
Über die Dauer des Projektes hat diese Pipeline mehrmals verhindert das Fehlerhafter oder sogar kaputter Programmcode in den main Branch gelangen konnte.
|
||||
|
||||
\newpage
|
||||
\subsection{Testverfahren}
|
||||
Unit-Tests wurden mit \textit{JUnit 5} geschrieben und in der Testphase der CI-Pipeline ausgeführt. Ein fehlgeschlagener Test blockiert die Merge-Anfrage.
|
||||
|
||||
Als weiteres Werkzeug verwendeten wir \textit{JaCoCo} für die Ermittlung der Testabdeckung. \\
|
||||
Die Testabdeckung für Meilenstein sechs ist wie folgt:
|
||||
|
||||
\begin{table}[H]
|
||||
\centering
|
||||
\renewcommand{\arraystretch}{1.4}
|
||||
\begin{tabular}{L{7.5cm} r r}
|
||||
\textbf{Paketname (gekürzt)} & \textbf{Instr.\ Cov.} & \textbf{Branch Cov.} \\
|
||||
\midrule
|
||||
\texttt{client.chat} & 30\,\% & 16\,\% \\
|
||||
\texttt{client.game} & 15\,\% & 2\,\% \\
|
||||
\texttt{client.network} & 2\,\% & 1\,\% \\
|
||||
\texttt{client.ui} (von Abdeckung ausgeschlossen) & 0\,\% & 0\,\% \\
|
||||
\texttt{server.app.checks} & 0\,\% & 0\,\% \\
|
||||
\texttt{server.app.commands} & 0\,\% & 0\,\% \\
|
||||
\texttt{server.domain.game} & 68\,\% & 43\,\% \\
|
||||
\texttt{server.domain.*} & 59\,\% & 44\,\% \\
|
||||
\texttt{server.network.transport} & 0\,\% & 0\,\% \\
|
||||
\texttt{server.network.protocol} & 67\,\% & 55\,\% \\
|
||||
\texttt{server.network.*} & 67\,\% & 55\,\% \\
|
||||
\midrule
|
||||
\textbf{Summe} & \textbf{18\,\%} & \textbf{11\,\%} \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{table}
|
||||
|
||||
Im vierten Meilenstein haben wir uns das Ziel gesetzt die Testabdeckung für die Game-Engine und wichtige Netzwerkkomponenten auf mindestens 50\% zu erhöhen, dieses Ziel haben wir laut JaCoCo nicht erreicht.\\
|
||||
Geht man jedoch nach der Abdeckung der "Test Runner for Java" Erweiterung in VSCode, dann beträgt die Abdeckung 51\%. Sowohl Julian (Game-Engine) als auch Lars (Serverseitiges Netzwerk) haben sich nach diesen Angaben gereichtet, weshalb wir dachten wir hätten unser Ziel erreicht.
|
||||
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=0.4\textwidth]{../../images/milestones/ms-6/qa-report/vscode_code_coverage.png}
|
||||
\caption{Abweichende Angaben bezüglich der Abdeckung von VSCode zu JaCoCo}
|
||||
\end{figure}
|
||||
|
||||
Die Verwendung zwei unterschiedlicher Tools zur Beurteilung des Fortschritts ist definitiv etwas, was es in Zukunft zu vermeiden gilt.
|
||||
|
||||
\newpage
|
||||
\section{Metriken}
|
||||
\textit{Die nachfolgenden Metriken wurden am 17. Mai 2026 um 20:58 Uhr erhoben, neuere Änderungen sind daher nicht berücksichtigt.}
|
||||
|
||||
\subsection{Lines of Code}
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\begin{tikzpicture}
|
||||
\begin{axis}[
|
||||
width=\textwidth,
|
||||
height=0.45\textwidth,
|
||||
xlabel={Meilenstein},
|
||||
ylabel={Zeilen},
|
||||
ymin=0,
|
||||
ymax=31000,
|
||||
xtick={1,2,3},
|
||||
xticklabels={MS4,MS5,MS6},
|
||||
legend pos=north west,
|
||||
grid=both,
|
||||
scaled y ticks=false,
|
||||
yticklabel style={/pgf/number format/fixed}
|
||||
]
|
||||
|
||||
% Actual Java code
|
||||
\addplot[
|
||||
color=blue,
|
||||
mark=o,
|
||||
thick
|
||||
]
|
||||
coordinates {
|
||||
(1,9760)
|
||||
(2,12972)
|
||||
(3,15319)
|
||||
};
|
||||
\addlegendentry{Java code (actual)}
|
||||
|
||||
% Total Java
|
||||
\addplot[
|
||||
color=red,
|
||||
mark=square*,
|
||||
thick
|
||||
]
|
||||
coordinates {
|
||||
(1,16396)
|
||||
(2,21203)
|
||||
(3,24319)
|
||||
};
|
||||
\addlegendentry{Java total}
|
||||
|
||||
% Total in src
|
||||
\addplot[
|
||||
color=green!60!black,
|
||||
mark=diamond*,
|
||||
thick
|
||||
]
|
||||
coordinates {
|
||||
(1,18012)
|
||||
(2,22872)
|
||||
(3,29639)
|
||||
};
|
||||
\addlegendentry{Total in src}
|
||||
|
||||
\end{axis}
|
||||
\end{tikzpicture}
|
||||
\caption{Entwicklung der Java-Zeilen (tatsächlicher Code vs. Gesamt) über die Meilensteine hinweg}
|
||||
\end{figure}
|
||||
|
||||
\subsection{Verteilung der Code-Zeilen auf verschiedene Dateitypen}
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\begin{tikzpicture}
|
||||
\pie[
|
||||
text=legend,
|
||||
radius=4
|
||||
]{
|
||||
62.26/Java,
|
||||
17.19/PostCSS,
|
||||
14.90/Markdown,
|
||||
2.87/XML,
|
||||
2.79/HTML
|
||||
}
|
||||
\end{tikzpicture}
|
||||
\caption{Verteilung der Code-Zeilen (Top 5 Dateitypen)}
|
||||
\label{fig:loc-pie}
|
||||
\end{figure}
|
||||
|
||||
\end{document}
|
||||
@@ -1,8 +1,12 @@
|
||||
game.name = Example Game
|
||||
game.command = java -jar exampleGame.jar
|
||||
game.name = Casono
|
||||
# Default client command (example)
|
||||
game.command = java -jar casono.jar client <serverip>:<serverport> [username]
|
||||
# Explicit client command property
|
||||
game.client.command = java -jar casono.jar client <serverip>:<serverport> [username]
|
||||
# Server command example
|
||||
game.server.command = java -jar casono.jar server <listenport>
|
||||
game.description.file = index.html
|
||||
game.screenshot.file = example.png
|
||||
game.server.command = java -jar exampleGame.jar
|
||||
game.screenshot.file = screenshot.png
|
||||
game.use = true
|
||||
game.year = 2020
|
||||
game.developers = Tom Cat, Jerry Mouse, William Hanna, Joseph Barbera
|
||||
game.year = 2026
|
||||
game.developers = Lars Winzer, Jona Walpert, Matthis Müller, Julian Gerber
|
||||
|
||||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 611 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 450" width="800" height="450">
|
||||
<rect width="800" height="450" rx="20" fill="#07101a" />
|
||||
<ellipse cx="400" cy="240" rx="300" ry="120" fill="#0b5a2b" />
|
||||
<g fill="#d4b26a" font-family="Cinzel, serif" font-size="32" text-anchor="middle">
|
||||
<text x="400" y="60">Casono Table Preview</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 363 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="240" viewBox="0 0 240 240">
|
||||
<rect width="240" height="240" rx="20" fill="#141212" />
|
||||
<circle cx="120" cy="84" r="44" fill="#ffffff" />
|
||||
<text x="120" y="150" font-family="Lato, sans-serif" font-size="18" fill="#fff" text-anchor="middle">Jona</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 321 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="240" viewBox="0 0 240 240">
|
||||
<rect width="240" height="240" rx="20" fill="#141212" />
|
||||
<circle cx="120" cy="84" r="44" fill="#ffffff" />
|
||||
<text x="120" y="150" font-family="Lato, sans-serif" font-size="18" fill="#fff" text-anchor="middle">Julian</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 323 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="240" viewBox="0 0 240 240">
|
||||
<rect width="240" height="240" rx="20" fill="#141212" />
|
||||
<circle cx="120" cy="84" r="44" fill="#ffffff" />
|
||||
<text x="120" y="150" font-family="Lato, sans-serif" font-size="18" fill="#fff" text-anchor="middle">Lars</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 321 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="240" viewBox="0 0 240 240">
|
||||
<rect width="240" height="240" rx="20" fill="#141212" />
|
||||
<circle cx="120" cy="84" r="44" fill="#ffffff" />
|
||||
<text x="120" y="150" font-family="Lato, sans-serif" font-size="18" fill="#fff" text-anchor="middle">Matthis</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 324 B |
@@ -1,53 +1,350 @@
|
||||
<html>
|
||||
<!-- Dies ist ein html Kommentar -->
|
||||
<!-- Ändert in diesem File sämtliche Platzhalter, wie z.B. den Spielnamen in der nächsten Linie -->
|
||||
<head><title>Example Game</title><link rel="stylesheet" href="main.css"></head>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Casono</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="icon" type="image/png" href="images/logo.png">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700;900&family=Lato:wght@300;400;700&display=swap"
|
||||
rel="stylesheet">
|
||||
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Header nicht verändern -->
|
||||
<header class="site-header"><div class="wrapper"><a class="site-title" href="/cs108/">Programmierprojekt - Computerspiele</a></div></header><div class="page-content"><div class="wrapper"><div class="home">
|
||||
<noscript>
|
||||
<div style="
|
||||
padding: 40px;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #0d9e3b;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
">
|
||||
<h2 style="
|
||||
color: #ffffff;
|
||||
text-shadow: 2px 2px 0 #000;
|
||||
">JavaScript is disabled</h2>
|
||||
|
||||
<h1>Example Game</h1>
|
||||
Hier könnt ihr einen Satz über euer Spiel sagen.
|
||||
<p>
|
||||
The Casono Browser requires JavaScript for the <br>
|
||||
Casono Markdown Render Engine to display content correctly.<br><br>
|
||||
|
||||
<!-- Dieser Block ist dafür verantwortlich, den Trailer / das Gameplay Video und euren Screenshot einzubauen. Verändert ihn nicht, sondern überschreibt einfach die Dateien -->
|
||||
<br>
|
||||
<img width="250" src="screenshot.png">
|
||||
<br>
|
||||
<br>
|
||||
<h2>Trailer</h2>
|
||||
<video controls width="250">
|
||||
<source src="video.mp4"
|
||||
type="video/mp4">
|
||||
Sorry, your browser doesn't support embedded videos.
|
||||
</video>
|
||||
<br>
|
||||
<br>
|
||||
For security, JavaScript is disabled by default in the Casono Browser and should only be enabled when needed.
|
||||
</p>
|
||||
|
||||
<!-- Entwickler anpassen -->
|
||||
<p>
|
||||
Please enable JavaScript in your Browser settings and reload the page.
|
||||
</p>
|
||||
|
||||
<h3>Entwickler:</h3>
|
||||
<ul>
|
||||
<li> Edgar Codd </li>
|
||||
<li> Lynn Conway </li>
|
||||
<li> Ada Lovelace </li>
|
||||
<li> Alan Turing </li>
|
||||
</ul>
|
||||
<img src="images/activate-js.png" alt="JavaScript aktivieren Anleitung" style="max-width: 100%; margin-top: 20px; border-radius: 10px;">
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<!-- Nachfolgend sollt ihr die Tasten angeben, welche man braucht um euer Spiel zu Spielen. Um weitere Tasten hinzuzufügen könnt Ihr einfach eine Zeile kopieren und den Buchstaben oder das Wort zwischen <i>Beispiel:</i> ändern. -->
|
||||
<header class="sticky-nav">
|
||||
<nav>
|
||||
<div class="logo" data-aos="fade-down"><img src="images/logo.png" alt="Casono" class="site-logo"></div>
|
||||
<ul id="nav-links" data-aos="fade-down" data-aos-delay="100">
|
||||
<li><a href="#hero" data-i18n="navHome">Home</a></li>
|
||||
<li><a href="#about" data-i18n="navAbout">Über</a></li>
|
||||
<li><a href="#features" data-i18n="navFeatures">Features</a></li>
|
||||
<li><a href="#table" data-i18n="navTable">Tisch</a></li>
|
||||
<li><a href="#media" data-i18n="navMedia">Trailer & Gameplay</a></li>
|
||||
<li><a href="#resources" data-i18n="navDownload">Download</a></li>
|
||||
<li><a href="#team" data-i18n="navTeam">Team</a></li>
|
||||
</ul>
|
||||
<button id="language-toggle" class="language-toggle" type="button" aria-label="Switch language">EN</button>
|
||||
<button class="burger-menu" id="burger-menu" aria-label="Toggle navigation menu">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<h2>Steuerung:</h2>
|
||||
<i>Pfeiltasten:</i> bewegen<br/>
|
||||
<i>Leertaste:</i> springen<br/>
|
||||
<i>I:</i> Inventar<br/>
|
||||
<i>Esc:</i> Spiel beenden<br/>
|
||||
<i>P:</i> Spiel pausieren
|
||||
<main>
|
||||
<section id="hero" class="hero-section">
|
||||
<div class="hero-content">
|
||||
<h1 data-aos="zoom-in">Casono</h1>
|
||||
<p class="tagline" data-i18n="heroTagline" data-aos="fade-up" data-aos-delay="300">Erlebe den
|
||||
Nervenkitzel von Texas Hold'em
|
||||
Poker</p>
|
||||
<p class="description" data-i18n="heroDescription" data-aos="fade-up" data-aos-delay="500">Casono ist
|
||||
ein stilvolles Texas Hold'em
|
||||
Poker-Erlebnis mit spannenden Matches und eleganter Optik.</p>
|
||||
<a href="#features" class="cta-button" data-i18n="heroCta" data-aos="zoom-in-up"
|
||||
data-aos-delay="700">Mehr erfahren</a>
|
||||
</div>
|
||||
<div class="hero-overlay"></div>
|
||||
</section>
|
||||
|
||||
<!-- Falls ihr noch zusätzliche Informationen wie Items oder andere Erklärungen angeben wollt, könnt Ihr das wie nachfolgend machen. -->
|
||||
<section id="about" class="content-section">
|
||||
<h2 data-i18n="aboutTitle" data-aos="fade-up">Über Casono</h2>
|
||||
<p data-i18n="aboutText" data-aos="fade-up" data-aos-delay="100">Casono kombiniert echtes Casino-Feeling mit
|
||||
leicht zugänglichem
|
||||
Gameplay. Spiele online gegen Freunde.</p>
|
||||
<!-- <div class="about-video-container" data-aos="zoom-in" data-aos-delay="200">
|
||||
<video class="about-video" controls playsinline preload="metadata" loading="lazy">
|
||||
<source src="video.mp4" type="video/mp4">
|
||||
<span data-i18n="videoFallback">Ihr Browser unterstützt das Video-Tag nicht.</span>
|
||||
</video>
|
||||
</div> -->
|
||||
</section>
|
||||
|
||||
<h2>Inventar:</h2>
|
||||
<i>Eisblock:</i> Gegner können sich nicht mehr bewegen<br/>
|
||||
<i>Schwarzes Loch:</i> Steuerung der Gegner wird invertiert
|
||||
<section id="features" class="features-section">
|
||||
<h2 data-i18n="featuresTitle" data-aos="fade-up">Key Features</h2>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card" data-accent="1" data-aos="fade-up" data-aos-delay="100">
|
||||
<h3 data-i18n="feature1Title">Sitzungen & Matchmaking</h3>
|
||||
<p data-i18n="feature1Text">Realistisches Poker-Erlebnis mit spannenden Matches.</p>
|
||||
</div>
|
||||
|
||||
</div></div></div></div></body>
|
||||
</html>
|
||||
<div class="feature-card" data-accent="2" data-aos="fade-up" data-aos-delay="300">
|
||||
<h3 data-i18n="feature2Title">Stilvolle UI</h3>
|
||||
<p data-i18n="feature2Text">Intuitive Benutzeroberfläche für ein immersives Erlebnis.
|
||||
</p>
|
||||
</div>
|
||||
<div class="feature-card" data-accent="3" data-aos="fade-up" data-aos-delay="400">
|
||||
<h3 data-i18n="feature3Title">Einfache Installation</h3>
|
||||
<p data-i18n="feature3Text">Download, entpacken, starten, fertig!</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="table" class="content-section table-section">
|
||||
<h2 data-i18n="tableTitle" data-aos="fade-up">Der Tisch</h2>
|
||||
<p data-i18n="tableText" data-aos="fade-up" data-aos-delay="100"> Spielen und chatten in einer gemeinsamen
|
||||
eleganten Ansicht.</p>
|
||||
<div class="table-preview-container" data-aos="zoom-in" data-aos-delay="200">
|
||||
<img src="images/table_preview.png" alt="Casino Tisch Vorschau" class="table-preview-image"
|
||||
loading="lazy">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="media" class="media-section">
|
||||
<h2 data-i18n="mediaTitle" data-aos="fade-up">Trailer & Gameplay</h2>
|
||||
<p data-i18n="mediaText" data-aos="fade-up" data-aos-delay="100">Erhalte einen schnellen Eindruck vom Stil
|
||||
und vom Spielablauf.
|
||||
</p>
|
||||
<div class="media-grid" data-aos="fade-up" data-aos-delay="200">
|
||||
<article class="media-card">
|
||||
<h3 data-i18n="mediaTrailer">Trailer</h3>
|
||||
<video controls playsinline preload="metadata" loading="lazy">
|
||||
<source src="casono-trailer.mp4" type="video/mp4">
|
||||
<span data-i18n="videoFallback">Ihr Browser unterstützt das Video-Tag nicht.</span>
|
||||
</video>
|
||||
</article>
|
||||
<article class="media-card">
|
||||
<h3 data-i18n="mediaGameplay">Gameplay</h3>
|
||||
<video controls playsinline preload="metadata" loading="lazy">
|
||||
<source src="gameplay.mp4" type="video/mp4">
|
||||
<span data-i18n="videoFallback">Ihr Browser unterstützt das Video-Tag nicht.</span>
|
||||
</video>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="resources" class="resources-section">
|
||||
<h2 data-i18n="resourcesTitle" data-aos="zoom-in-up">Download & Ressourcen</h2>
|
||||
<div class="download-section" data-aos="zoom-in-up" data-aos-delay="100">
|
||||
<a href="https://git.scicore.unibas.ch/cs108-fs26/Gruppe-13/-/archive/main/Gruppe-13-main.zip"
|
||||
class="download-button" data-i18n="downloadButton" target="_blank"
|
||||
rel="noopener noreferrer">Download Game
|
||||
(ZIP)</a>
|
||||
</div>
|
||||
|
||||
<div class="run-instructions" data-aos="fade-up" data-aos-delay="200">
|
||||
<div class="code-window" role="region" aria-label="Run instructions">
|
||||
<code>
|
||||
# Requires Java 25
|
||||
|
||||
# Start the Server:
|
||||
java -jar casono.jar server <listenport>
|
||||
|
||||
# Start the Client:
|
||||
java -jar casono.jar client <serverip>:<serverport> [username]
|
||||
|
||||
# Replace <listenport>, <serverip>, <serverport> and [username] with your values.
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="team" class="team-section">
|
||||
<h2 data-i18n="teamTitle" data-aos="fade-up">Das Team</h2>
|
||||
<div class="team-grid">
|
||||
<a class="team-member team-member-link" href="https://github.com/official-Cromatin" target="_blank"
|
||||
rel="noopener noreferrer" data-aos="fade-up" data-aos-delay="100">
|
||||
<img src="https://avatars.githubusercontent.com/u/92319467?v=4" alt="Lars Winzer" class="team-photo"
|
||||
loading="lazy">
|
||||
<h3>Lars Winzer</h3>
|
||||
<p>Server architecture & Server side networking</p>
|
||||
</a>
|
||||
<a class="team-member team-member-link" href="https://jona.walpert.ch/" target="_blank"
|
||||
rel="noopener noreferrer" data-aos="fade-up" data-aos-delay="200">
|
||||
<img src="https://avatars.githubusercontent.com/u/98217482?v=4" alt="Jona Walpert"
|
||||
class="team-photo" loading="lazy">
|
||||
<h3>Jona Walpert</h3>
|
||||
<p>UI & UX Design</p>
|
||||
</a>
|
||||
<a class="team-member team-member-link" href="https://github.com/ginkelmath" target="_blank"
|
||||
rel="noopener noreferrer" data-aos="fade-up" data-aos-delay="300">
|
||||
<img src="https://avatars.githubusercontent.com/u/160567160?v=4" alt="Matthis Ginkel"
|
||||
class="team-photo" loading="lazy">
|
||||
<h3>Matthis Ginkel</h3>
|
||||
<p>Client side development</p>
|
||||
</a>
|
||||
<a class="team-member team-member-link" href="https://github.com/17jk789" target="_blank"
|
||||
rel="noopener noreferrer" data-aos="fade-up" data-aos-delay="400">
|
||||
<img src="https://avatars.githubusercontent.com/u/189449499?v=4" alt="Julian Kropff"
|
||||
class="team-photo" loading="lazy">
|
||||
<h3>Julian Kropff</h3>
|
||||
<p>UI & UX Design, Game Engine</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="github-link" data-aos="fade-up" data-aos-delay="500">
|
||||
<a href="https://git.scicore.unibas.ch/cs108-fs26/Gruppe-13">
|
||||
<i class="fa-brands fa-github github-icon"></i>
|
||||
<span data-i18n="repoLink">Projekt-Repo</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
|
||||
<div id="trailer-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-button" id="close-trailer-modal">×</span>
|
||||
<video id="trailer-video" width="100%" controls autoplay muted loop playsinline>
|
||||
<source src="Casono-Trailer.webm" type="video/webm">
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="demo-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-button" id="close-demo-modal">×</span>
|
||||
<video id="demo-video" width="100%" controls autoplay muted loop playsinline>
|
||||
<source src="gameplay.mp4" type="video/mp4">
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
|
||||
<script>
|
||||
AOS.init({ duration: 800, once: true });
|
||||
|
||||
const translations = {
|
||||
de: {
|
||||
navHome: 'Home',
|
||||
navAbout: 'Über',
|
||||
navFeatures: 'Features',
|
||||
navTable: 'Tisch',
|
||||
navMedia: 'Trailer & Gameplay',
|
||||
navDownload: 'Download',
|
||||
navTeam: 'Team',
|
||||
heroTagline: "Erlebe den Nervenkitzel von Texas Hold'em Poker",
|
||||
heroDescription: "Casono ist ein stilvolles Texas Hold'em Poker-Erlebnis mit spannenden Matches und eleganter Optik.",
|
||||
heroCta: 'Mehr erfahren',
|
||||
aboutTitle: 'Über Casono',
|
||||
aboutText: 'Casono kombiniert echtes Casino-Feeling mit leicht zugänglichem Gameplay. Spiele online gegen deine Freunde.',
|
||||
featuresTitle: 'Key Features',
|
||||
feature1Title: 'Sitzungen & Matchmaking',
|
||||
feature1Text: 'Realistisches Poker-Erlebnis mit spannenden Matches.',
|
||||
feature2Title: 'Stilvolle UI',
|
||||
feature2Text: 'Intuitive Benutzeroberfläche für ein immersives Erlebnis.',
|
||||
feature3Title: 'Einfache Installation',
|
||||
feature3Text: 'Download, entpacken, starten, fertig!',
|
||||
tableTitle: 'Der Tisch',
|
||||
tableText: 'Spielen und chatten in einer gemeinsamen eleganten Ansicht.',
|
||||
mediaTitle: 'Trailer & Gameplay',
|
||||
mediaText: 'Erhalte einen Eindruck vom Stil und vom Spielablauf.',
|
||||
mediaTrailer: 'Trailer',
|
||||
mediaGameplay: 'Gameplay',
|
||||
resourcesTitle: 'Download & Ressourcen',
|
||||
downloadButton: 'Download Game (ZIP)',
|
||||
teamTitle: 'Das Team',
|
||||
repoLink: 'Projekt-Repo',
|
||||
videoFallback: 'Ihr Browser unterstützt das Video-Tag nicht.'
|
||||
},
|
||||
en: {
|
||||
navHome: 'Home',
|
||||
navAbout: 'About',
|
||||
navFeatures: 'Features',
|
||||
navTable: 'Table',
|
||||
navMedia: 'Trailer & Gameplay',
|
||||
navDownload: 'Download',
|
||||
navTeam: 'Team',
|
||||
heroTagline: "Feel the thrill of Texas Hold'em Poker",
|
||||
heroDescription: "Casono is a stylish Texas Hold'em poker experience with exciting matches and elegant visuals.",
|
||||
heroCta: 'Learn more',
|
||||
aboutTitle: 'About Casono',
|
||||
aboutText: 'Casono combines real casino flair with approachable gameplay. Play online with your friends.',
|
||||
featuresTitle: 'Key Features',
|
||||
feature1Title: 'Sessions & Matchmaking',
|
||||
feature1Text: 'Realistic poker experience with thrilling matches.',
|
||||
feature2Title: 'Stylish UI',
|
||||
feature2Text: 'Intuitive interface for an immersive experience.',
|
||||
feature3Title: 'Easy Setup',
|
||||
feature3Text: 'Download, unpack, launch, done!',
|
||||
tableTitle: 'The Table',
|
||||
tableText: 'Play and chat in one shared elegant view.',
|
||||
mediaTitle: 'Trailer & Gameplay',
|
||||
mediaText: 'Get a quick look at the style and core gameplay loop.',
|
||||
mediaTrailer: 'Trailer',
|
||||
mediaGameplay: 'Gameplay',
|
||||
resourcesTitle: 'Download & Resources',
|
||||
downloadButton: 'Download Game (ZIP)',
|
||||
teamTitle: 'The Team',
|
||||
repoLink: 'Project Repo',
|
||||
videoFallback: 'Your browser does not support the video tag.'
|
||||
}
|
||||
};
|
||||
|
||||
const languageToggle = document.getElementById('language-toggle');
|
||||
const i18nNodes = document.querySelectorAll('[data-i18n]');
|
||||
|
||||
function applyLanguage(language) {
|
||||
const dictionary = translations[language] ?? translations.de;
|
||||
i18nNodes.forEach((node) => {
|
||||
const key = node.getAttribute('data-i18n');
|
||||
if (!key || !dictionary[key]) {
|
||||
return;
|
||||
}
|
||||
node.textContent = dictionary[key];
|
||||
});
|
||||
|
||||
document.documentElement.lang = language;
|
||||
languageToggle.textContent = language === 'de' ? 'EN' : 'DE';
|
||||
localStorage.setItem('casono-language', language);
|
||||
}
|
||||
|
||||
const preferredLanguage = localStorage.getItem('casono-language') === 'en' ? 'en' : 'de';
|
||||
applyLanguage(preferredLanguage);
|
||||
|
||||
languageToggle?.addEventListener('click', () => {
|
||||
const nextLanguage = document.documentElement.lang === 'de' ? 'en' : 'de';
|
||||
applyLanguage(nextLanguage);
|
||||
});
|
||||
|
||||
document.getElementById('burger-menu')?.addEventListener('click', function () {
|
||||
const nav = document.querySelector('nav ul');
|
||||
nav.classList.toggle('nav-active');
|
||||
this.classList.toggle('active');
|
||||
});
|
||||
|
||||
// modal open/close handlers
|
||||
document.getElementById('close-trailer-modal')?.addEventListener('click', () => {
|
||||
document.getElementById('trailer-modal').classList.remove('show');
|
||||
});
|
||||
document.getElementById('close-demo-modal')?.addEventListener('click', () => {
|
||||
document.getElementById('demo-modal').classList.remove('show');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,667 +0,0 @@
|
||||
/**
|
||||
* Reset some basic elements
|
||||
*/
|
||||
body, h1, h2, h3, h4, h5, h6,
|
||||
p, blockquote, pre, hr,
|
||||
dl, dd, ol, ul, figure {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic styling
|
||||
*/
|
||||
body {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
font-weight: 300;
|
||||
color: #111;
|
||||
background-color: #fdfdfd;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set `margin-bottom` to maintain vertical rhythm
|
||||
*/
|
||||
h1, h2, h3, h4, h5, h6,
|
||||
p, blockquote, pre,
|
||||
ul, ol, dl, figure,
|
||||
.highlight {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Images
|
||||
*/
|
||||
img {
|
||||
max-width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Figures
|
||||
*/
|
||||
figure > img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists
|
||||
*/
|
||||
ul, ol {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
li > ul,
|
||||
li > ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Headings
|
||||
*/
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/**
|
||||
* Links
|
||||
*/
|
||||
a {
|
||||
color: #2a7ae2;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #1756a9;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #111;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blockquotes
|
||||
*/
|
||||
blockquote {
|
||||
color: #828282;
|
||||
border-left: 4px solid #e8e8e8;
|
||||
padding-left: 15px;
|
||||
font-size: 18px;
|
||||
letter-spacing: -1px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
blockquote > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Code formatting
|
||||
*/
|
||||
pre,
|
||||
code {
|
||||
font-size: 15px;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 3px;
|
||||
background-color: #eef;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 1px 5px;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 8px 12px;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
border: 0;
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper
|
||||
*/
|
||||
.wrapper {
|
||||
max-width: -webkit-calc(800px - (30px * 2));
|
||||
max-width: calc(800px - (30px * 2));
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
padding-right: 30px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.wrapper {
|
||||
max-width: -webkit-calc(800px - (30px));
|
||||
max-width: calc(800px - (30px));
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clearfix
|
||||
*/
|
||||
.wrapper:after, .footer-col-wrapper:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/**
|
||||
* Icons
|
||||
*/
|
||||
.icon > svg {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.icon > svg path {
|
||||
fill: #828282;
|
||||
}
|
||||
|
||||
/**
|
||||
* Site header
|
||||
*/
|
||||
.site-header {
|
||||
border-top: 5px solid #424242;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
min-height: 56px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
font-size: 26px;
|
||||
line-height: 56px;
|
||||
letter-spacing: -1px;
|
||||
margin-bottom: 0;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.site-title, .site-title:visited {
|
||||
color: #424242;
|
||||
}
|
||||
|
||||
.site-nav {
|
||||
float: right;
|
||||
line-height: 56px;
|
||||
}
|
||||
|
||||
.site-nav .menu-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.site-nav .page-link {
|
||||
color: #111;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.site-nav .page-link:not(:first-child) {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.site-nav {
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
right: 30px;
|
||||
background-color: #fdfdfd;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 5px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.site-nav .menu-icon {
|
||||
display: block;
|
||||
float: right;
|
||||
width: 36px;
|
||||
height: 26px;
|
||||
line-height: 0;
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.site-nav .menu-icon > svg {
|
||||
width: 18px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.site-nav .menu-icon > svg path {
|
||||
fill: #424242;
|
||||
}
|
||||
|
||||
.site-nav .trigger {
|
||||
clear: both;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.site-nav:hover .trigger {
|
||||
display: block;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.site-nav .page-link {
|
||||
display: block;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Site footer
|
||||
*/
|
||||
.site-footer {
|
||||
border-top: 1px solid #e8e8e8;
|
||||
padding: 30px 0;
|
||||
}
|
||||
|
||||
.footer-heading {
|
||||
font-size: 18px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.contact-list,
|
||||
.social-media-list {
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.footer-col-wrapper {
|
||||
font-size: 15px;
|
||||
color: #828282;
|
||||
margin-left: -15px;
|
||||
}
|
||||
|
||||
.footer-col {
|
||||
float: left;
|
||||
margin-bottom: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.footer-col-1 {
|
||||
width: -webkit-calc(35% - (30px / 2));
|
||||
width: calc(35% - (30px / 2));
|
||||
}
|
||||
|
||||
.footer-col-2 {
|
||||
width: -webkit-calc(20% - (30px / 2));
|
||||
width: calc(20% - (30px / 2));
|
||||
}
|
||||
|
||||
.footer-col-3 {
|
||||
width: -webkit-calc(45% - (30px / 2));
|
||||
width: calc(45% - (30px / 2));
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.footer-col-1,
|
||||
.footer-col-2 {
|
||||
width: -webkit-calc(50% - (30px / 2));
|
||||
width: calc(50% - (30px / 2));
|
||||
}
|
||||
|
||||
.footer-col-3 {
|
||||
width: -webkit-calc(100% - (30px / 2));
|
||||
width: calc(100% - (30px / 2));
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.footer-col {
|
||||
float: none;
|
||||
width: -webkit-calc(100% - (30px / 2));
|
||||
width: calc(100% - (30px / 2));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Page content
|
||||
*/
|
||||
.page-content {
|
||||
padding: 30px 0;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.post-list {
|
||||
margin-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.post-list > li {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
font-size: 14px;
|
||||
color: #828282;
|
||||
}
|
||||
|
||||
.post-link {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts
|
||||
*/
|
||||
.post-header {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
font-size: 42px;
|
||||
letter-spacing: -1px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.post-title {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.post-content {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.post-content h2 {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.post-content h2 {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.post-content h3 {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.post-content h3 {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.post-content h4 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.post-content h4 {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Syntax highlighting styles
|
||||
*/
|
||||
.highlight {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.highlight .c {
|
||||
color: #998;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.highlight .err {
|
||||
color: #a61717;
|
||||
background-color: #e3d2d2;
|
||||
}
|
||||
|
||||
.highlight .k {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .o {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .cm {
|
||||
color: #998;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.highlight .cp {
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .c1 {
|
||||
color: #998;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.highlight .cs {
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.highlight .gd {
|
||||
color: #000;
|
||||
background-color: #fdd;
|
||||
}
|
||||
|
||||
.highlight .gd .x {
|
||||
color: #000;
|
||||
background-color: #faa;
|
||||
}
|
||||
|
||||
.highlight .ge {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.highlight .gr {
|
||||
color: #a00;
|
||||
}
|
||||
|
||||
.highlight .gh {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.highlight .gi {
|
||||
color: #000;
|
||||
background-color: #dfd;
|
||||
}
|
||||
|
||||
.highlight .gi .x {
|
||||
color: #000;
|
||||
background-color: #afa;
|
||||
}
|
||||
|
||||
.highlight .go {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.highlight .gp {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.highlight .gs {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .gu {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.highlight .gt {
|
||||
color: #a00;
|
||||
}
|
||||
|
||||
.highlight .kc {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .kd {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .kp {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .kr {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .kt {
|
||||
color: #458;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .m {
|
||||
color: #099;
|
||||
}
|
||||
|
||||
.highlight .s {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .na {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.highlight .nb {
|
||||
color: #0086B3;
|
||||
}
|
||||
|
||||
.highlight .nc {
|
||||
color: #458;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .no {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.highlight .ni {
|
||||
color: #800080;
|
||||
}
|
||||
|
||||
.highlight .ne {
|
||||
color: #900;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .nf {
|
||||
color: #900;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .nn {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.highlight .nt {
|
||||
color: #000080;
|
||||
}
|
||||
|
||||
.highlight .nv {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.highlight .ow {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight .w {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.highlight .mf {
|
||||
color: #099;
|
||||
}
|
||||
|
||||
.highlight .mh {
|
||||
color: #099;
|
||||
}
|
||||
|
||||
.highlight .mi {
|
||||
color: #099;
|
||||
}
|
||||
|
||||
.highlight .mo {
|
||||
color: #099;
|
||||
}
|
||||
|
||||
.highlight .sb {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .sc {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .sd {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .s2 {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .se {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .sh {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .si {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .sx {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .sr {
|
||||
color: #009926;
|
||||
}
|
||||
|
||||
.highlight .s1 {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.highlight .ss {
|
||||
color: #990073;
|
||||
}
|
||||
|
||||
.highlight .bp {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.highlight .vc {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.highlight .vg {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.highlight .vi {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.highlight .il {
|
||||
color: #099;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,728 @@
|
||||
/* Basic Reset */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--background-color: #f6efe4;
|
||||
/* page background */
|
||||
--card-bg-color: #f6efe4;
|
||||
--nav-bg-color: rgba(69, 2, 6, 0.96);
|
||||
--primary-color: #0b0b0b;
|
||||
/* main text on light background */
|
||||
--secondary-color: #6b6b6b;
|
||||
--accent-color: #a88130;
|
||||
/* accent */
|
||||
--button-color: #a40404;
|
||||
/* primary buttons */
|
||||
--button-hover: #fbf0df;
|
||||
--section-team-bg: #a40404;
|
||||
/* meet the team background */
|
||||
--team-button-bg: #fbf0df;
|
||||
/* team buttons start */
|
||||
--team-button-hover: #450206;
|
||||
/* team button hover */
|
||||
--header-bg: #450206;
|
||||
/* header background */
|
||||
--hero-overlay-color: rgba(0, 0, 0, 0.12);
|
||||
--font-title: 'Cinzel', serif;
|
||||
--font-body: 'Lato', sans-serif;
|
||||
--section-padding: 5rem 5%;
|
||||
--card-padding: 1.6rem;
|
||||
--grid-gap: 1.6rem;
|
||||
--transition: 0.28s cubic-bezier(.2, .9, .2, 1);
|
||||
--shadow-light: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
--shadow-medium: 0 6px 14px rgba(0, 0, 0, 0.08);
|
||||
--shadow-heavy: 0 12px 28px rgba(0, 0, 0, 0.12);
|
||||
--border-radius: 12px;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
background-color: var(--background-color);
|
||||
color: var(--primary-color);
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-family: var(--font-title);
|
||||
color: var(--accent-color);
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.5rem;
|
||||
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2.5rem;
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-color);
|
||||
text-decoration: none;
|
||||
transition: color var(--transition);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.sticky-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
background-color: var(--header-bg);
|
||||
z-index: 1000;
|
||||
box-shadow: var(--shadow-medium);
|
||||
padding: 14px 0;
|
||||
/* taller header */
|
||||
transition: background-color var(--transition);
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: var(--font-title);
|
||||
font-size: 1.8rem;
|
||||
font-weight: bold;
|
||||
color: var(--accent-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 48px;
|
||||
margin-right: 48px;
|
||||
/* push logo further left and add space to the nav links */
|
||||
}
|
||||
|
||||
.site-logo {
|
||||
max-height: 56px;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
/* push links to the right */
|
||||
margin-right: 56px;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
margin-left: 1.2rem;
|
||||
}
|
||||
|
||||
nav ul li a {
|
||||
font-weight: bold;
|
||||
padding: 0.5rem 0;
|
||||
position: relative;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
nav ul li a::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
background-color: var(--accent-color);
|
||||
transition: width var(--transition);
|
||||
}
|
||||
|
||||
nav ul li a:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Header text color to contrast against dark header */
|
||||
.logo {
|
||||
color: var(--team-button-bg);
|
||||
}
|
||||
|
||||
nav ul li a {
|
||||
color: var(--team-button-bg);
|
||||
}
|
||||
|
||||
.language-toggle {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background-color: var(--team-button-bg);
|
||||
color: var(--header-bg);
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 0.35rem 0.85rem;
|
||||
font-weight: 700;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.08em;
|
||||
cursor: pointer;
|
||||
transition: transform var(--transition), box-shadow var(--transition), background-color var(--transition);
|
||||
box-shadow: var(--shadow-light);
|
||||
}
|
||||
|
||||
.language-toggle:hover {
|
||||
background-color: var(--accent-color);
|
||||
color: #fff;
|
||||
transform: translateY(-50%) translateY(-1px);
|
||||
}
|
||||
|
||||
/* Burger Menu Button (Hidden on Desktop) */
|
||||
.burger-menu {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
.burger-menu span {
|
||||
display: block;
|
||||
width: 25px;
|
||||
height: 3px;
|
||||
background-color: var(--primary-color);
|
||||
margin: 5px 0;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero-section {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: white;
|
||||
background-image: url('images/background.png');
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-color: transparent;
|
||||
overflow: hidden;
|
||||
padding: var(--section-padding);
|
||||
}
|
||||
|
||||
/* Hero CTA styled like team buttons per request */
|
||||
.hero-section .cta-button {
|
||||
background-color: var(--team-button-bg);
|
||||
color: var(--section-team-bg);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.hero-section .cta-button:hover {
|
||||
background-color: var(--team-button-hover);
|
||||
color: var(--team-button-bg);
|
||||
}
|
||||
|
||||
.hero-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: var(--hero-overlay-color);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.hero-content h1 {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.hero-content .tagline {
|
||||
font-size: 1.4rem;
|
||||
color: #f6efe4;
|
||||
margin-bottom: 1.5rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hero-content .description {
|
||||
font-size: 1.1rem;
|
||||
color: #f6efe4;
|
||||
margin: 0 auto 2.5rem;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.cta-button,
|
||||
.download-button {
|
||||
display: inline-block;
|
||||
background-color: var(--button-color);
|
||||
color: var(--button-hover);
|
||||
padding: 0.9rem 2.2rem;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
transition: all var(--transition);
|
||||
box-shadow: var(--shadow-medium);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cta-button:hover,
|
||||
.download-button:hover {
|
||||
background-color: var(--button-hover);
|
||||
color: var(--button-color);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.download-button {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* General Content Sections */
|
||||
.content-section,
|
||||
.features-section,
|
||||
.resources-section,
|
||||
.team-section {
|
||||
padding: var(--section-padding);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Keep inner grids/content centered with a max width */
|
||||
.features-grid,
|
||||
.team-grid {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Center content and grid items site-wide */
|
||||
.content-section,
|
||||
.features-section,
|
||||
.resources-section,
|
||||
.team-section {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.features-grid,
|
||||
.team-grid {
|
||||
justify-items: center;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.team-grid {
|
||||
justify-content: center;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
}
|
||||
|
||||
/* Center nav links */
|
||||
.sticky-nav nav {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
margin-left: 0;
|
||||
gap: 3.2rem;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* Make videos keep a sensible aspect ratio so they don't look flat */
|
||||
video,
|
||||
.modal-content video {
|
||||
aspect-ratio: 16/9;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
|
||||
/* Story Section */
|
||||
.content-section p {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
font-size: 1.1rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Decorative Divider */
|
||||
.features-section::before,
|
||||
.table-section::before,
|
||||
.resources-section::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 150px;
|
||||
height: 3px;
|
||||
background: linear-gradient(to right, transparent, var(--secondary-color), transparent);
|
||||
margin: 1rem auto;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
|
||||
.table-section {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.media-section {
|
||||
padding: var(--section-padding);
|
||||
background-color: var(--card-bg-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.media-grid {
|
||||
max-width: 1200px;
|
||||
margin: 2rem auto 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(280px, 1fr));
|
||||
gap: var(--grid-gap);
|
||||
}
|
||||
|
||||
.media-card {
|
||||
background-color: var(--background-color);
|
||||
border: 1px solid var(--secondary-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--card-padding);
|
||||
box-shadow: var(--shadow-light);
|
||||
}
|
||||
|
||||
.media-card h3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.media-card video {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.resources-section {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
/* Features Section */
|
||||
.features-section {
|
||||
background-color: var(--card-bg-color);
|
||||
}
|
||||
|
||||
.features-grid,
|
||||
.team-grid {
|
||||
display: grid;
|
||||
gap: var(--grid-gap);
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
.team-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
margin-top: 2.5rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.feature-card,
|
||||
.team-member {
|
||||
background-color: var(--background-color);
|
||||
padding: var(--card-padding);
|
||||
border-radius: var(--border-radius);
|
||||
text-align: center;
|
||||
border: 1px solid var(--secondary-color);
|
||||
transition: transform var(--transition), box-shadow var(--transition);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-5px) scale(1.02);
|
||||
box-shadow: var(--shadow-heavy);
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
border-top: 4px solid transparent;
|
||||
}
|
||||
|
||||
.feature-card[data-accent="1"] {
|
||||
border-top-color: var(--accent-red);
|
||||
}
|
||||
|
||||
.feature-card[data-accent="2"] {
|
||||
border-top-color: var(--accent-green);
|
||||
}
|
||||
|
||||
.feature-card[data-accent="3"] {
|
||||
border-top-color: var(--accent-navy);
|
||||
}
|
||||
|
||||
.feature-card[data-accent="4"] {
|
||||
border-top-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.team-member {
|
||||
background-color: #0f0e11;
|
||||
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||
transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
box-shadow 0.25s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
border-color 0.25s;
|
||||
box-shadow: var(--shadow-light), 0 2px 8px 0 rgba(0, 0, 0, 0.18);
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.team-member:hover {
|
||||
transform: translateY(-10px) scale(1.04);
|
||||
box-shadow: 0 0 36px 8px rgba(11, 61, 145, 0.12), 0 6px 22px 0 rgba(0, 0, 0, 0.36);
|
||||
border-color: rgba(11, 61, 145, 0.18);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.team-member-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.team-member-link:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.feature-card .icon {
|
||||
width: 100px;
|
||||
margin-bottom: 1.5rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.feature-card h3,
|
||||
.team-member h3 {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
font-size: 0.95rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.team-photo {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
margin-bottom: 1.2rem;
|
||||
border: 2px solid rgba(255, 255, 255, 0.04);
|
||||
background: #141114;
|
||||
/* darker neutral placeholder (less white) */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Resources Section */
|
||||
.resources-section {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Team section uses the red background per request */
|
||||
.team-section {
|
||||
background-color: var(--section-team-bg);
|
||||
color: var(--team-button-bg);
|
||||
padding: var(--section-padding) 0;
|
||||
/* full-bleed background, inner padding on grid */
|
||||
}
|
||||
|
||||
.team-grid {
|
||||
padding: 0 5%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.team-section .cta-button,
|
||||
.team-section .download-button {
|
||||
background-color: var(--team-button-bg);
|
||||
color: var(--section-team-bg);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.team-section .cta-button:hover,
|
||||
.team-section .download-button:hover {
|
||||
background-color: var(--team-button-hover);
|
||||
color: var(--team-button-bg);
|
||||
}
|
||||
|
||||
/* Ensure AOS animations have a starting point */
|
||||
[data-aos] {
|
||||
opacity: 0;
|
||||
transition-property: transform, opacity;
|
||||
}
|
||||
|
||||
/* Ensure global CTA/Download buttons use the user-requested button color (unless overridden in team-section) */
|
||||
.cta-button {
|
||||
background-color: var(--button-color);
|
||||
color: var(--button-hover);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cta-button:hover {
|
||||
background-color: var(--button-hover);
|
||||
color: var(--button-color);
|
||||
}
|
||||
|
||||
.download-button {
|
||||
background-color: var(--button-color);
|
||||
color: var(--button-hover);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.download-button:hover {
|
||||
background-color: var(--button-hover);
|
||||
color: var(--button-color);
|
||||
}
|
||||
|
||||
/* Table preview sizing + padding so it never touches edges */
|
||||
.table-preview-container {
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-preview-image {
|
||||
max-width: 100%;
|
||||
width: calc(100% - 32px);
|
||||
height: auto;
|
||||
padding: 8px;
|
||||
border-radius: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.media-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal and video: ensure padding and responsive scaling without distortion */
|
||||
.modal {
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: min(1000px, calc(100% - 48px));
|
||||
max-width: 100%;
|
||||
padding: 20px;
|
||||
background: var(--card-bg-color);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.modal.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content video {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
|
||||
.modal-content video,
|
||||
video {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: calc(100vh - 160px);
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Remove global side padding so sections can be full-bleed; keep inner content padded via grids */
|
||||
main,
|
||||
body {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
/* Run instructions code window */
|
||||
.run-instructions {
|
||||
max-width: 900px;
|
||||
margin: 2rem auto 0;
|
||||
/* space above code window (gap from download button) */
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.run-instructions .code-window {
|
||||
background: #0b1220;
|
||||
color: #e6eef8;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, 'Roboto Mono', 'Courier New', monospace;
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 8px 30px rgba(2, 6, 23, 0.45);
|
||||
overflow: auto;
|
||||
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.run-instructions .code-window::before {
|
||||
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: -12px;
|
||||
background: var(--header-bg);
|
||||
color: var(--team-button-bg);
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.run-instructions .code-window code {
|
||||
display: block;
|
||||
white-space: pre;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.run-instructions .note {
|
||||
margin-top: 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
@@ -7,6 +7,12 @@ import org.apache.logging.log4j.Logger;
|
||||
|
||||
/** Main entry point for Casono application. Handles client and server startup. */
|
||||
public final class Main {
|
||||
|
||||
private static final int MIN_ARGS_FOR_USERNAME = 3;
|
||||
private static final int ARGS_COUNT_SERVER = 2;
|
||||
private static final int ARGS_COUNT_CLIENT_MIN = 2;
|
||||
private static final int ARGS_COUNT_CLIENT_WITH_USER = 3;
|
||||
|
||||
/**
|
||||
* Main entry point for Casono.
|
||||
*
|
||||
@@ -17,10 +23,13 @@ public final class Main {
|
||||
printUsage();
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
switch (args[0]) {
|
||||
case "server" -> ServerApp.start(args[1]);
|
||||
case "client" -> ClientApp.start(args[1]);
|
||||
case "client" -> {
|
||||
String address = args[1];
|
||||
String username = args.length >= MIN_ARGS_FOR_USERNAME ? args[2] : null;
|
||||
ClientApp.start(address, username);
|
||||
}
|
||||
default -> {
|
||||
printUsage();
|
||||
System.exit(1);
|
||||
@@ -29,23 +38,26 @@ public final class Main {
|
||||
}
|
||||
|
||||
private static boolean isValid(String[] args) {
|
||||
if (args.length != 2) {
|
||||
if (args.length < ARGS_COUNT_SERVER) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return switch (args[0]) {
|
||||
case "server", "client" -> true;
|
||||
case "server" -> args.length == ARGS_COUNT_SERVER;
|
||||
case "client" ->
|
||||
args.length == ARGS_COUNT_CLIENT_MIN
|
||||
|| args.length == ARGS_COUNT_CLIENT_WITH_USER;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private static void printUsage() {
|
||||
Logger logger = LogManager.getLogger(Main.class);
|
||||
Logger logger = LogManager.getLogger(Main.class.getSimpleName());
|
||||
logger.fatal(
|
||||
"""
|
||||
Usage:
|
||||
java -jar xyz.jar server <listenPort>
|
||||
java -jar xyz.jar client <serverIp>:<serverPort>
|
||||
java -jar xyz.jar client <serverIp>:<serverPort> [<username>]
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +1,131 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client;
|
||||
|
||||
/**
|
||||
* Entry point for the Casono client application. Handles client startup and connection parameters.
|
||||
*/
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LoginResult;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.Launcher;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Entry point for the Casono client application. Handles client startup and connection parameters.
|
||||
* Entry point and bootstrap helper for the Casono client application.
|
||||
*
|
||||
* <p>Default constructor for the application.
|
||||
* <p>Responsibilities: - Parse CLI args (host:port, optional username) - Store username centrally
|
||||
* so UI can reuse it - Create a shared ClientService and do startup LOGIN
|
||||
*/
|
||||
public class ClientApp {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(ClientApp.class);
|
||||
private static final Logger LOGGER = LogManager.getLogger(ClientApp.class.getSimpleName());
|
||||
|
||||
/** Shared client connection used when a username is provided at startup. */
|
||||
private static volatile ClientService sharedClientService;
|
||||
|
||||
private static volatile String sharedUsername;
|
||||
|
||||
/** Default constructor. */
|
||||
public ClientApp() {
|
||||
// Default constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the client application with the given address.
|
||||
*
|
||||
* @param arg Address in the format "ip:port".
|
||||
* @throws IllegalArgumentException if the address format is invalid.
|
||||
*/
|
||||
public static void start(String arg) {
|
||||
public static ClientService getSharedClientService() {
|
||||
return sharedClientService;
|
||||
}
|
||||
|
||||
private static void setSharedClientService(ClientService cs) {
|
||||
sharedClientService = cs;
|
||||
}
|
||||
|
||||
public static String getSharedUsername() {
|
||||
return sharedUsername;
|
||||
}
|
||||
|
||||
public static void updateSharedUsername(String username) {
|
||||
setSharedUsername(normalizeUsername(username));
|
||||
}
|
||||
|
||||
private static String normalizeUsername(String username) {
|
||||
if (username == null || username.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return username.trim();
|
||||
}
|
||||
|
||||
private static void setSharedUsername(String username) {
|
||||
sharedUsername = username;
|
||||
LOGGER.info("sharedUsername set to '{}'", getSharedUsername());
|
||||
}
|
||||
|
||||
public static void start(String arg, String username) {
|
||||
String[] hostPort = parseHostAndPort(arg);
|
||||
String host = hostPort[0];
|
||||
int port = Integer.parseInt(hostPort[1]);
|
||||
String normalizedUsername = normalizeUsername(username);
|
||||
|
||||
configureServerProperties(host, port);
|
||||
setSharedUsername(normalizedUsername);
|
||||
|
||||
performStartupLogin(host, port, normalizedUsername);
|
||||
|
||||
launchUI(arg);
|
||||
}
|
||||
|
||||
private static String[] parseHostAndPort(String arg) {
|
||||
String[] parts = arg.split(":", 2);
|
||||
if (parts.length != 2) {
|
||||
throw new IllegalArgumentException("Address must be in format <ip>:<port>.");
|
||||
throw new IllegalArgumentException("The address must be in the format <ip>:<port>.");
|
||||
}
|
||||
String host = parts[0];
|
||||
int port = Integer.parseInt(parts[1]);
|
||||
return parts;
|
||||
}
|
||||
|
||||
private static void configureServerProperties(String host, int port) {
|
||||
LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host);
|
||||
// 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));
|
||||
}
|
||||
|
||||
private static void performStartupLogin(String host, int port, String username) {
|
||||
try {
|
||||
ClientService clientService = new ClientService(host, port);
|
||||
setSharedClientService(clientService);
|
||||
LobbyClient lobbyClient = new LobbyClient(clientService);
|
||||
LoginResult res = lobbyClient.login(username);
|
||||
|
||||
if (res != null && res.getUsername() != null && !res.getUsername().isBlank()) {
|
||||
setSharedUsername(res.getUsername().trim());
|
||||
}
|
||||
|
||||
String assignedUsername = null;
|
||||
String assignedId = null;
|
||||
if (res != null) {
|
||||
assignedUsername = res.getUsername();
|
||||
assignedId = res.getId();
|
||||
}
|
||||
LOGGER.info("Assigned username='{}' id={}", assignedUsername, assignedId);
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.warn(
|
||||
"Could not establish initial connection for startup login: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void launchUI(String arg) {
|
||||
String username = getSharedUsername();
|
||||
if (username == null || username.isBlank()) {
|
||||
Launcher.main(new String[] {arg});
|
||||
return;
|
||||
}
|
||||
Launcher.main(new String[] {arg, username});
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
start(args[0]);
|
||||
if (args == null || args.length == 0) {
|
||||
throw new IllegalArgumentException("Address argument required: <host:port>");
|
||||
}
|
||||
|
||||
String username = null;
|
||||
if (args.length > 1) {
|
||||
username = normalizeUsername(args[1]);
|
||||
}
|
||||
|
||||
start(args[0], username);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Interface used to create test instances of the ChatClient */
|
||||
public interface ChatClientInterface {
|
||||
|
||||
List<Message> getMessages();
|
||||
|
||||
void sendMessage(Message message);
|
||||
|
||||
List<String> getUsers();
|
||||
}
|
||||
@@ -1,13 +1,24 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ChatClient;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatViewController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.function.Consumer;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
@@ -16,17 +27,28 @@ import org.jspecify.annotations.Nullable;
|
||||
*/
|
||||
public class ChatController {
|
||||
|
||||
private final String username;
|
||||
/** Ensures one active message poller per physical ClientService connection. */
|
||||
private static final Map<ClientService, ChatController> ACTIVE_CONTROLLERS =
|
||||
new WeakHashMap<>();
|
||||
|
||||
private final ChatClient chatClient;
|
||||
private volatile String username;
|
||||
|
||||
private ClientService clientService;
|
||||
|
||||
private final ChatClientInterface chatClient;
|
||||
|
||||
public ChatBoxController getChatBoxController() {
|
||||
return chatBoxController;
|
||||
}
|
||||
|
||||
private final ChatBoxController chatBoxController;
|
||||
public void setChatBoxController(ChatBoxController chatBoxController) {
|
||||
this.chatBoxController = chatBoxController;
|
||||
}
|
||||
|
||||
private ChatBoxController chatBoxController;
|
||||
private int lobbyId = -1;
|
||||
private final Timer timer;
|
||||
private final Consumer<List<String>> serverEventListener;
|
||||
|
||||
public record ChatKey(ChatType type, @Nullable String targetUser) {
|
||||
public ChatKey(ChatType type) {
|
||||
@@ -38,10 +60,21 @@ public class ChatController {
|
||||
return chatModelMap;
|
||||
}
|
||||
|
||||
private Map<ChatKey, ChatModel> chatModelMap;
|
||||
private final Map<ChatKey, ChatModel> chatModelMap;
|
||||
|
||||
private static final long REFRESH_TIME = 1000;
|
||||
|
||||
/** List of all users connected to the server, to safe them locally on the client */
|
||||
private final List<String> localUserList;
|
||||
|
||||
public List<String> getLocalUserList() {
|
||||
return this.localUserList;
|
||||
}
|
||||
|
||||
public final Map<ChatController.ChatKey, ChatViewController> activeChatControllers;
|
||||
|
||||
private final Logger logger;
|
||||
|
||||
/**
|
||||
* Constructor, adds TimerTask to be sent to the server regularly
|
||||
*
|
||||
@@ -50,15 +83,29 @@ public class ChatController {
|
||||
*/
|
||||
public ChatController(String username, ClientService clientService) {
|
||||
this.username = username;
|
||||
this.clientService = clientService;
|
||||
chatClient = new ChatClient(clientService);
|
||||
chatModelMap = new LinkedHashMap<>();
|
||||
localUserList = new ArrayList<>();
|
||||
this.chatBoxController = new ChatBoxController(username, this);
|
||||
this.timer = new Timer();
|
||||
this.logger = LogManager.getLogger(ChatController.class.getSimpleName());
|
||||
this.serverEventListener = this::handleServerEvent;
|
||||
this.activeChatControllers = new HashMap<>();
|
||||
|
||||
registerAsActiveController(clientService);
|
||||
clientService.addEventListener(serverEventListener);
|
||||
|
||||
this.timer = new Timer(true);
|
||||
timer.schedule(
|
||||
new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
receiveMessage();
|
||||
try {
|
||||
receiveMessage();
|
||||
checkWhisperUsers();
|
||||
} catch (RuntimeException e) {
|
||||
logger.warn("Chat refresh failed: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
},
|
||||
0,
|
||||
@@ -66,20 +113,79 @@ public class ChatController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to be activated, if a lobby has be chosen. It will update the UI and add a new
|
||||
* Another constructor to give the ChatClient directly instead of the Client Service used only
|
||||
* for test purposes
|
||||
*
|
||||
* @param username
|
||||
*/
|
||||
public ChatController(String username, ChatClientInterface chatClient) {
|
||||
this.username = username;
|
||||
this.chatClient = chatClient;
|
||||
chatModelMap = new LinkedHashMap<>();
|
||||
localUserList = new ArrayList<>();
|
||||
this.chatBoxController = new ChatBoxController(username, this);
|
||||
this.logger = LogManager.getLogger(ChatController.class.getSimpleName());
|
||||
this.serverEventListener = this::handleServerEvent;
|
||||
this.activeChatControllers = new HashMap<>();
|
||||
|
||||
this.timer = new Timer(true);
|
||||
timer.schedule(
|
||||
new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
receiveMessage();
|
||||
checkWhisperUsers();
|
||||
} catch (RuntimeException e) {
|
||||
logger.warn("Chat refresh failed: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
},
|
||||
0,
|
||||
REFRESH_TIME);
|
||||
}
|
||||
|
||||
private void registerAsActiveController(ClientService clientService) {
|
||||
synchronized (ACTIVE_CONTROLLERS) {
|
||||
ChatController oldController = ACTIVE_CONTROLLERS.get(clientService);
|
||||
if (oldController != null && oldController != this) {
|
||||
oldController.shutdown();
|
||||
logger.info("Replaced previous ChatController poller for shared ClientService");
|
||||
}
|
||||
ACTIVE_CONTROLLERS.put(clientService, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to be activated, if a lobby has been chosen. It will update the UI and add a new
|
||||
* ChatModel to hold the Data for the Lobby Chat
|
||||
*
|
||||
* @param lobbyId
|
||||
*/
|
||||
public void setLobbyChat(int lobbyId) {
|
||||
this.lobbyId = lobbyId;
|
||||
ChatKey key = new ChatKey(ChatType.LOBBY);
|
||||
ChatModel existingModel = chatModelMap.get(key);
|
||||
if (existingModel != null) {
|
||||
existingModel.lobbyId = lobbyId;
|
||||
return;
|
||||
}
|
||||
ChatModel lobbyChatModel = new ChatModel(ChatType.LOBBY, username, lobbyId, null);
|
||||
chatModelMap.put(new ChatKey(ChatType.LOBBY), lobbyChatModel);
|
||||
this.chatBoxController.addChatTab("Lobby", lobbyChatModel);
|
||||
chatModelMap.put(key, lobbyChatModel);
|
||||
this.chatBoxController.addChatTab("LOBBY", lobbyChatModel, ChatType.LOBBY);
|
||||
}
|
||||
|
||||
/** method to get all messages from the server */
|
||||
/**
|
||||
* Method to process the Messages that got polled by the {@link ChatClient}.
|
||||
*
|
||||
* <p>Case distinction for every message: - Messages identified with ChatType.LOBBY -> check if
|
||||
* they belong to the users lobby. - Messages identified with ChatType.GLOBAL -> check if the
|
||||
* target user of that Message is the user, or the message got sent by the user.
|
||||
*
|
||||
* <p>All Messages get added to a particular {@link ChatModel}, if the checks passed.
|
||||
*/
|
||||
public void receiveMessage() {
|
||||
String currentUsername = getCurrentUsername();
|
||||
List<Message> newMessages = chatClient.getMessages();
|
||||
if (!newMessages.isEmpty()) {
|
||||
for (Message msg : newMessages) {
|
||||
@@ -88,28 +194,39 @@ public class ChatController {
|
||||
chatModelMap.get(new ChatKey(ChatType.GLOBAL)).addMessage(msg);
|
||||
break;
|
||||
case ChatType.LOBBY:
|
||||
if (msg.lobbyId == lobbyId) {
|
||||
if (msg.lobbyId == getActiveLobbyChatId()) {
|
||||
chatModelMap
|
||||
.computeIfAbsent(
|
||||
new ChatKey(ChatType.LOBBY),
|
||||
(_key) ->
|
||||
new ChatModel(
|
||||
ChatType.LOBBY,
|
||||
username,
|
||||
currentUsername,
|
||||
msg.lobbyId,
|
||||
null))
|
||||
.addMessage(msg);
|
||||
}
|
||||
break;
|
||||
case ChatType.WHISPER:
|
||||
if (msg.target.equals(username)) {
|
||||
if (chatModelMap.containsKey(
|
||||
new ChatKey(ChatType.WHISPER, msg.sender))) {
|
||||
chatModelMap
|
||||
.get(new ChatKey(ChatType.WHISPER, msg.sender))
|
||||
.addMessage(msg);
|
||||
if (msg.target.equals(currentUsername)
|
||||
|| msg.sender.equals(currentUsername)) {
|
||||
ChatKey key;
|
||||
if (msg.target.equals(currentUsername)) {
|
||||
key = new ChatKey(ChatType.WHISPER, msg.sender);
|
||||
} else {
|
||||
chatBoxController.addWhisperChat(msg.sender);
|
||||
key = new ChatKey(ChatType.WHISPER, msg.target);
|
||||
}
|
||||
if (chatModelMap.containsKey(key)) {
|
||||
chatModelMap.get(key).addMessage(msg);
|
||||
} else {
|
||||
ChatModel chatModel =
|
||||
new ChatModel(
|
||||
ChatType.WHISPER,
|
||||
currentUsername,
|
||||
lobbyId,
|
||||
key.targetUser());
|
||||
chatBoxController.addWhisperChat(key.targetUser(), chatModel);
|
||||
chatModel.addMessage(msg);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -118,6 +235,176 @@ public class ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the lobbyId of the currently active lobby chat, to check if incoming lobby
|
||||
* messages belong to the same lobby.
|
||||
*
|
||||
* @return the lobbyId of the currently active lobby chat.
|
||||
*/
|
||||
private int getActiveLobbyChatId() {
|
||||
ChatModel lobbyModel = chatModelMap.get(new ChatKey(ChatType.LOBBY));
|
||||
if (lobbyModel != null) {
|
||||
return lobbyModel.lobbyId;
|
||||
}
|
||||
return lobbyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to process the usernames of other users connected to the server, after they got polled
|
||||
* by the {@link ChatClient}.
|
||||
*
|
||||
* <p>Checks for every one of the usernames, if it is already in the list localUserList and if
|
||||
* not adds them. For every new User added in the List, they get added as an option to start a
|
||||
* Whisper Chat with.
|
||||
*/
|
||||
public synchronized void checkWhisperUsers() {
|
||||
List<String> users = chatClient.getUsers();
|
||||
Set<String> remoteUsers = new HashSet<>();
|
||||
String currentUsername = getCurrentUsername();
|
||||
|
||||
for (String user : users) {
|
||||
if (user == null || !user.contains("=")) {
|
||||
continue;
|
||||
}
|
||||
String value = user.split("\\=", 2)[1].trim();
|
||||
if (value.isBlank() || value.equals(currentUsername)) {
|
||||
continue;
|
||||
}
|
||||
remoteUsers.add(value);
|
||||
}
|
||||
|
||||
for (String known : new ArrayList<>(localUserList)) {
|
||||
if (!remoteUsers.contains(known)) {
|
||||
localUserList.remove(known);
|
||||
chatBoxController.removeWhisperUser(known);
|
||||
}
|
||||
}
|
||||
|
||||
for (String remote : remoteUsers) {
|
||||
addWhisperUser(remote);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a whisper target locally and updates the UI if it is a new user.
|
||||
*
|
||||
* @param user target username
|
||||
*/
|
||||
public synchronized void addWhisperUser(String user) {
|
||||
if (user == null || user.isBlank()) {
|
||||
return;
|
||||
}
|
||||
if (!(localUserList.contains(user) || user.equals(getCurrentUsername()))) {
|
||||
localUserList.add(user);
|
||||
logger.info("adding new whisper user");
|
||||
chatBoxController.addWhisperUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void updateUsername(String newUsername) {
|
||||
if (newUsername == null || newUsername.isBlank()) {
|
||||
return;
|
||||
}
|
||||
String oldUsername = this.username;
|
||||
this.username = newUsername.trim();
|
||||
chatBoxController.setUsername(this.username);
|
||||
|
||||
if (oldUsername != null && !oldUsername.equals(this.username)) {
|
||||
localUserList.remove(oldUsername);
|
||||
chatBoxController.removeWhisperUser(oldUsername);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleServerEvent(List<String> lines) {
|
||||
if (lines == null || lines.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<RequestParameter> params;
|
||||
try {
|
||||
params = ClientService.convertToRequestParameters(lines);
|
||||
} catch (RuntimeException e) {
|
||||
return;
|
||||
}
|
||||
String event = null;
|
||||
String oldUsername = null;
|
||||
String newUsername = null;
|
||||
|
||||
for (RequestParameter p : params) {
|
||||
if ("EVENT".equalsIgnoreCase(p.key())) {
|
||||
event = p.value();
|
||||
} else if ("OLD_USERNAME".equalsIgnoreCase(p.key())) {
|
||||
oldUsername = p.value();
|
||||
} else if ("NEW_USERNAME".equalsIgnoreCase(p.key())) {
|
||||
newUsername = p.value();
|
||||
}
|
||||
}
|
||||
|
||||
if (!"USERNAME_CHANGED".equalsIgnoreCase(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyUsernameMigration(oldUsername, newUsername);
|
||||
}
|
||||
|
||||
private synchronized void applyUsernameMigration(String oldUsername, String newUsername) {
|
||||
if (oldUsername == null
|
||||
|| newUsername == null
|
||||
|| oldUsername.isBlank()
|
||||
|| newUsername.isBlank()
|
||||
|| oldUsername.equals(newUsername)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String currentUsername = getCurrentUsername();
|
||||
if (oldUsername.equals(currentUsername)) {
|
||||
updateUsername(newUsername);
|
||||
}
|
||||
|
||||
ChatKey oldKey = new ChatKey(ChatType.WHISPER, oldUsername);
|
||||
ChatKey newKey = new ChatKey(ChatType.WHISPER, newUsername);
|
||||
|
||||
ChatModel oldModel = chatModelMap.remove(oldKey);
|
||||
ChatModel existingNewModel = chatModelMap.get(newKey);
|
||||
if (oldModel != null) {
|
||||
oldModel.setTarget(newUsername);
|
||||
if (existingNewModel == null) {
|
||||
chatModelMap.put(newKey, oldModel);
|
||||
} else {
|
||||
// Merge possible parallel history into the already existing new-key model.
|
||||
for (Message msg : oldModel.messages) {
|
||||
existingNewModel.addMessage(msg);
|
||||
}
|
||||
}
|
||||
chatBoxController.renameWhisperUser(oldUsername, newUsername);
|
||||
}
|
||||
|
||||
localUserList.remove(oldUsername);
|
||||
chatBoxController.removeWhisperUser(oldUsername);
|
||||
if (!newUsername.equals(getCurrentUsername())) {
|
||||
addWhisperUser(newUsername);
|
||||
}
|
||||
}
|
||||
|
||||
public String getCurrentUsername() {
|
||||
String shared = ClientApp.getSharedUsername();
|
||||
if (shared != null && !shared.isBlank()) {
|
||||
return shared.trim();
|
||||
}
|
||||
return username;
|
||||
}
|
||||
|
||||
/** Stops polling background tasks for this chat controller instance. */
|
||||
public void shutdown() {
|
||||
timer.cancel();
|
||||
clientService.removeEventListener(serverEventListener);
|
||||
synchronized (ACTIVE_CONTROLLERS) {
|
||||
if (ACTIVE_CONTROLLERS.get(clientService) == this) {
|
||||
ACTIVE_CONTROLLERS.remove(clientService);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* method to send a message to the server
|
||||
*
|
||||
|
||||
@@ -2,8 +2,6 @@ package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.function.Consumer;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
|
||||
/**
|
||||
* ChatModel, stores the data for a specific chat
|
||||
@@ -12,7 +10,7 @@ import javafx.beans.property.SimpleIntegerProperty;
|
||||
*/
|
||||
public class ChatModel {
|
||||
|
||||
private ArrayList<Consumer<Message>> listeners = new ArrayList<>();
|
||||
private final ArrayList<Consumer<Message>> listeners = new ArrayList<>();
|
||||
|
||||
public ArrayList<Message> messages;
|
||||
|
||||
@@ -22,9 +20,7 @@ public class ChatModel {
|
||||
public final String username;
|
||||
|
||||
/** The person to send the message to If the chat is a whisper chat */
|
||||
private final String target;
|
||||
|
||||
private final IntegerProperty count;
|
||||
private String target;
|
||||
|
||||
public int lobbyId;
|
||||
|
||||
@@ -37,10 +33,9 @@ public class ChatModel {
|
||||
* @param target The username of the whisper recipient, or null for other chat types.
|
||||
*/
|
||||
public ChatModel(ChatType chattype, String username, int lobbyId, String target) {
|
||||
this.messages = new ArrayList<Message>();
|
||||
this.messages = new ArrayList<>();
|
||||
this.chattype = chattype;
|
||||
this.username = username;
|
||||
this.count = new SimpleIntegerProperty(0);
|
||||
this.lobbyId = lobbyId;
|
||||
this.target = target;
|
||||
}
|
||||
@@ -62,7 +57,7 @@ public class ChatModel {
|
||||
*/
|
||||
public synchronized void addMessage(Message msg) {
|
||||
messages.add(msg);
|
||||
listeners.stream().forEach((l) -> l.accept(messages.getLast()));
|
||||
listeners.forEach((l) -> l.accept(messages.getLast()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,4 +77,8 @@ public class ChatModel {
|
||||
public String getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
public void setTarget(String target) {
|
||||
this.target = target;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ public class Message {
|
||||
private final String message;
|
||||
public String sender;
|
||||
public String timestamp;
|
||||
public int lobbyId = 0;
|
||||
public String target = null;
|
||||
public int lobbyId;
|
||||
public String target;
|
||||
|
||||
/**
|
||||
* Constructs a Message with a provided timestamp. Typically used when reconstructing messages
|
||||
@@ -100,7 +100,7 @@ public class Message {
|
||||
* @return A formatted string containing all message attributes for server transmission.
|
||||
*/
|
||||
public String toArgsString() {
|
||||
String gameIdString = "";
|
||||
String gameIdString;
|
||||
if (lobbyId >= 0) {
|
||||
gameIdString = " GAME=" + lobbyId;
|
||||
} else {
|
||||
|
||||
@@ -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,217 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.game;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.GameClient;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class GameService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(GameService.class.getName());
|
||||
|
||||
private final GameClient client;
|
||||
private GameState state;
|
||||
private final Queue<Runnable> actionQueue = new LinkedList<>();
|
||||
private volatile boolean actionInProgress = false;
|
||||
private static final long ACTION_TIMEOUT_MS = 5000;
|
||||
|
||||
/**
|
||||
* Constructs a GameService with the specified GameClient.
|
||||
*
|
||||
* @param client The GameClient used to communicate with the server. Must not be null.
|
||||
*/
|
||||
public GameService(GameClient client) {
|
||||
if (client == null) {
|
||||
throw new IllegalArgumentException("GameClient must not be null");
|
||||
}
|
||||
|
||||
this.client = client;
|
||||
LOG.info("GameService created");
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the game state by fetching the latest state from the server using the GameClient.
|
||||
*
|
||||
* @return The updated GameState after refreshing.
|
||||
*/
|
||||
public GameState refresh() {
|
||||
LOG.fine("Refreshing game state...");
|
||||
|
||||
GameState newState;
|
||||
try {
|
||||
newState = client.fetchGameState();
|
||||
} catch (Exception e) {
|
||||
// Shouldn't happen with the new GameClient, but keep service stable.
|
||||
LOG.log(Level.WARNING, "refresh() failed: " + e.getMessage(), e);
|
||||
return state;
|
||||
}
|
||||
|
||||
if (newState == null) {
|
||||
LOG.fine("No new state (null) -> keeping previous state");
|
||||
return state;
|
||||
}
|
||||
|
||||
this.state = newState;
|
||||
|
||||
LOG.info(
|
||||
() ->
|
||||
"State updated: phase="
|
||||
+ state.phase
|
||||
+ " pot="
|
||||
+ state.pot
|
||||
+ " players="
|
||||
+ (state.players != null ? state.players.size() : 0)
|
||||
+ " community="
|
||||
+ (state.communityCards != null ? state.communityCards.size() : 0));
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current phase of the game.
|
||||
*
|
||||
* @return The current game phase as a string (e.g., "Pre-Flop", "Flop", "Turn", "River").
|
||||
*/
|
||||
public int getPot() {
|
||||
ensureState();
|
||||
return state.pot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current phase of the game.
|
||||
*
|
||||
* @return The current game phase as a string (e.g., "Pre-Flop", "Flop", "Turn", "River").
|
||||
*/
|
||||
public List<Card> getCommunityCards() {
|
||||
ensureState();
|
||||
return state.communityCards != null ? state.communityCards : List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of players currently in the game.
|
||||
*
|
||||
* @return A list of Player objects representing the players in the game. A list of Player
|
||||
* objects representing the players in the game.
|
||||
*/
|
||||
public List<Player> getPlayers() {
|
||||
ensureState();
|
||||
return state.players != null ? state.players : List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the index of the current player whose turn it is.
|
||||
*
|
||||
* @return The index of the current player in the players list, or -1 if not available.
|
||||
*/
|
||||
public Player getWinner() {
|
||||
ensureState();
|
||||
if (state.winnerIndex < 0
|
||||
|| state.players == null
|
||||
|| state.winnerIndex >= state.players.size()) {
|
||||
return null;
|
||||
}
|
||||
return state.players.get(state.winnerIndex);
|
||||
}
|
||||
|
||||
/** Retrieves the current phase of the game. */
|
||||
public void call() {
|
||||
queueAction(() -> client.sendCall());
|
||||
}
|
||||
|
||||
/** Retrieves the current phase of the game. */
|
||||
public void fold() {
|
||||
queueAction(() -> client.sendFold());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current phase of the game.
|
||||
*
|
||||
* @param amount The amount to bet. Must be a positive integer.
|
||||
*/
|
||||
public void bet(int amount) {
|
||||
queueAction(() -> client.sendBet(amount));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current phase of the game.
|
||||
*
|
||||
* @param amount The amount to raise. Must be a positive integer.
|
||||
*/
|
||||
public void raise(int amount) {
|
||||
queueAction(() -> client.sendRaise(amount));
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue an action to be executed sequentially. Only one action is processed at a time to
|
||||
* prevent concurrent operations from overwhelming the server or UI.
|
||||
*
|
||||
* @param action The action to queue and execute.
|
||||
*/
|
||||
private void queueAction(Runnable action) {
|
||||
synchronized (actionQueue) {
|
||||
actionQueue.offer(action);
|
||||
if (!actionInProgress) {
|
||||
processNextAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the next queued action if one is available. This method ensures that only one action
|
||||
* is in progress at any given time.
|
||||
*/
|
||||
private void processNextAction() {
|
||||
synchronized (actionQueue) {
|
||||
if (actionQueue.isEmpty() || actionInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionInProgress = true;
|
||||
Runnable action = actionQueue.poll();
|
||||
|
||||
if (action != null) {
|
||||
try {
|
||||
LOG.fine("Processing queued action");
|
||||
action.run();
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.WARNING, "Error executing queued action: " + e.getMessage(), e);
|
||||
} finally {
|
||||
actionInProgress = false;
|
||||
if (!actionQueue.isEmpty()) {
|
||||
processNextAction();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
actionInProgress = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an action is currently in progress, preventing the UI from accepting new actions
|
||||
* while one is being processed.
|
||||
*
|
||||
* @return true if an action is in progress, false otherwise.
|
||||
*/
|
||||
public boolean isActionInProgress() {
|
||||
return actionInProgress;
|
||||
}
|
||||
|
||||
/** Ensures that the game state has been initialized before accessing it. */
|
||||
private void ensureState() {
|
||||
if (state == null) {
|
||||
throw new IllegalStateException("GameService used before any successful refresh()");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current GameState without refreshing from the server.
|
||||
*
|
||||
* @return The current GameState, or null if it has not been initialized yet.
|
||||
*/
|
||||
public GameState peekStateOrNull() {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,16 @@ import java.util.List;
|
||||
* dealer position, active player, community cards, and player information.
|
||||
*/
|
||||
public class GameState {
|
||||
public List<Player> players = new ArrayList<>();
|
||||
public List<Card> communityCards = new ArrayList<>();
|
||||
public List<String> highscoreEntries = new ArrayList<>();
|
||||
|
||||
public String phase;
|
||||
public int pot;
|
||||
public int currentBet;
|
||||
public int dealer;
|
||||
public int activePlayer;
|
||||
|
||||
public List<Card> communityCards = new ArrayList<>();
|
||||
public List<Player> players = new ArrayList<>();
|
||||
public int winnerIndex = -1;
|
||||
public List<String> winnerNames = new ArrayList<>();
|
||||
public int potPerWinner;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatClientInterface;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||
import java.util.ArrayList;
|
||||
@@ -12,7 +13,7 @@ import org.apache.logging.log4j.Logger;
|
||||
* from the server. It uses the ClientService to send commands and receive responses from the
|
||||
* server.
|
||||
*/
|
||||
public class ChatClient {
|
||||
public class ChatClient implements ChatClientInterface {
|
||||
|
||||
private final ClientService clientService;
|
||||
private final Logger logger;
|
||||
@@ -25,7 +26,7 @@ public class ChatClient {
|
||||
*/
|
||||
public ChatClient(ClientService clientService) {
|
||||
this.clientService = clientService;
|
||||
this.logger = LogManager.getLogger(ChatClient.class);
|
||||
this.logger = LogManager.getLogger(ChatClient.class.getSimpleName());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,4 +70,23 @@ public class ChatClient {
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to poll the usernames of all users currently connected to the server, by sending the
|
||||
* "LIST_USERS" command.
|
||||
*
|
||||
* @return List of all usernames retrieved from the server.
|
||||
*/
|
||||
public List<String> getUsers() {
|
||||
logger.info("Asking server for list of users");
|
||||
List<String> users = clientService.processCommand("LIST_USERS");
|
||||
List<String> parameters = new ArrayList<>();
|
||||
for (String line : users) {
|
||||
String trimmed = line == null ? "" : line.trim();
|
||||
if (trimmed.startsWith("USERNAME=")) {
|
||||
parameters.add(trimmed);
|
||||
}
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,21 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
@@ -30,10 +38,17 @@ public class ClientService {
|
||||
private final ExecutorService executor;
|
||||
private final boolean offlineMode;
|
||||
|
||||
public static ArrayList<String> response;
|
||||
private final AtomicInteger idGenerator;
|
||||
private Logger logger;
|
||||
|
||||
private final Map<Integer, ArrayBlockingQueue<ParsedResponse>> pendingResponses =
|
||||
new ConcurrentHashMap<>();
|
||||
private final CopyOnWriteArrayList<Consumer<List<String>>> eventListeners =
|
||||
new CopyOnWriteArrayList<>();
|
||||
private Thread readerThread = null;
|
||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||
private static final int READER_JOIN_TIMEOUT_MS = 500;
|
||||
|
||||
/**
|
||||
* Constructs a ClientService with the given server IP and port. It establishes a socket
|
||||
* connection to the server and initializes the TcpTransport and ExecutorService for
|
||||
@@ -43,10 +58,9 @@ public class ClientService {
|
||||
* @param port The port number of the server to connect to.
|
||||
*/
|
||||
public ClientService(String ip, int port) {
|
||||
|
||||
this.idGenerator = new AtomicInteger(0);
|
||||
|
||||
this.logger = LogManager.getLogger(ClientService.class);
|
||||
this.logger = LogManager.getLogger(ClientService.class.getSimpleName());
|
||||
|
||||
this.offlineMode = false;
|
||||
|
||||
@@ -59,6 +73,156 @@ public class ClientService {
|
||||
}
|
||||
|
||||
executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
startReaderThread();
|
||||
}
|
||||
|
||||
private void startReaderThread() {
|
||||
running.set(true);
|
||||
readerThread =
|
||||
new Thread(
|
||||
() -> {
|
||||
while (running.get()) {
|
||||
try {
|
||||
RawPacket rp = clienttcptransport.read();
|
||||
processRawPacket(rp);
|
||||
} catch (IOException e) {
|
||||
if (running.get()) {
|
||||
logger.warn("IO error on transport reader", e);
|
||||
}
|
||||
break;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
"casono-client-reader");
|
||||
readerThread.setDaemon(true);
|
||||
readerThread.start();
|
||||
}
|
||||
|
||||
private void processRawPacket(RawPacket rp) throws InterruptedException, IOException {
|
||||
int rid = rp.requestId();
|
||||
String responseText = rp.payload();
|
||||
logger.info("Raw message '{}'", responseText);
|
||||
|
||||
ParsedPacket parsed = parsePacketContent(responseText);
|
||||
|
||||
logger.debug(
|
||||
"Parsed response lines (rid={}, success={}, openBlocks={}): {}",
|
||||
rid,
|
||||
parsed.success,
|
||||
parsed.blockStack.size(),
|
||||
parsed.lines);
|
||||
|
||||
handleParsedResponse(rid, parsed.success, parsed.lines);
|
||||
}
|
||||
|
||||
private ParsedPacket parsePacketContent(String responseText) {
|
||||
boolean hasStatus = false;
|
||||
boolean success = false;
|
||||
List<String> lines = new ArrayList<>();
|
||||
Deque<String> blockStack = new ArrayDeque<>();
|
||||
|
||||
for (String rawLine : responseText.split("\\n")) {
|
||||
String line = rawLine;
|
||||
String trimmed = line.trim();
|
||||
|
||||
if (!hasStatus) {
|
||||
StatusCheckResult status = checkStatus(trimmed);
|
||||
if (status != null) {
|
||||
success = status.success;
|
||||
hasStatus = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmed.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
line = line.replaceFirst("^\\s+", "");
|
||||
trimmed = line.trim();
|
||||
|
||||
if (isContainerStart(trimmed)) {
|
||||
blockStack.push(trimmed);
|
||||
lines.add(trimmed);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("END".equals(trimmed)) {
|
||||
if (!blockStack.isEmpty()) {
|
||||
blockStack.pop();
|
||||
lines.add("END");
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
lines.add(line);
|
||||
}
|
||||
|
||||
if (!hasStatus) {
|
||||
success = inferStatus(responseText);
|
||||
}
|
||||
|
||||
return new ParsedPacket(success, lines, blockStack);
|
||||
}
|
||||
|
||||
private StatusCheckResult checkStatus(String trimmed) {
|
||||
if ("+OK".equals(trimmed)) {
|
||||
return new StatusCheckResult(true);
|
||||
}
|
||||
if (trimmed.startsWith("-ERR") || trimmed.startsWith("-ERROR")) {
|
||||
return new StatusCheckResult(false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean inferStatus(String responseText) {
|
||||
if (responseText.contains("+OK")) {
|
||||
return true;
|
||||
}
|
||||
if (responseText.contains("-ERR") || responseText.contains("-ERROR")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void handleParsedResponse(int rid, boolean success, List<String> lines)
|
||||
throws InterruptedException {
|
||||
if (rid == 0) {
|
||||
for (Consumer<List<String>> l : eventListeners) {
|
||||
try {
|
||||
l.accept(List.copyOf(lines));
|
||||
} catch (Exception e) {
|
||||
logger.warn("Event listener threw", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ArrayBlockingQueue<ParsedResponse> q = pendingResponses.get(rid);
|
||||
if (q != null) {
|
||||
q.put(new ParsedResponse(success, lines));
|
||||
} else {
|
||||
logger.warn("No pending response queue for id {}", rid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record ParsedPacket(boolean success, List<String> lines, Deque<String> blockStack) {}
|
||||
|
||||
private record StatusCheckResult(boolean success) {}
|
||||
|
||||
private boolean isContainerStart(String token) {
|
||||
return token.equals("LOBBIES")
|
||||
|| token.equals("LOBBY")
|
||||
|| token.equals("PLAYERS")
|
||||
|| token.equals("PLAYER")
|
||||
|| token.equals("USERS")
|
||||
|| token.equals("USER")
|
||||
|| token.equals("CARDS")
|
||||
|| token.equals("CARD");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,9 +244,10 @@ public class ClientService {
|
||||
return offlineMode;
|
||||
}
|
||||
|
||||
// Allow primitive values to contain hyphens (UUIDs) in addition to
|
||||
// digits/words/colons
|
||||
static Pattern responseRex =
|
||||
Pattern.compile(
|
||||
"(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[+-]?[\\d\\w:]+))");
|
||||
Pattern.compile("(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[^ ]+))");
|
||||
|
||||
/**
|
||||
* Removes escape characters from a string, specifically converting escaped single quotes (\')
|
||||
@@ -122,6 +287,19 @@ public class ClientService {
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static record ParsedResponse(boolean success, List<String> lines) {}
|
||||
|
||||
/**
|
||||
* Register an event listener that receives unsolicited event payload lines (no status prefix).
|
||||
*/
|
||||
public void addEventListener(Consumer<List<String>> listener) {
|
||||
eventListeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeEventListener(Consumer<List<String>> listener) {
|
||||
eventListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a command to the server and processes the multi-line response. It handles the protocol
|
||||
* handshake (expecting +OK), strips leading tabs from response lines, and collects them until
|
||||
@@ -133,43 +311,51 @@ public class ClientService {
|
||||
* occurs.
|
||||
*/
|
||||
protected List<String> processCommand(String message) {
|
||||
List<String> response = new ArrayList<>();
|
||||
sendRequest(
|
||||
() -> {
|
||||
try {
|
||||
writeToTransport(message);
|
||||
String responseText = null;
|
||||
if (offlineMode) {
|
||||
throw new RuntimeException("ClientService is offline");
|
||||
}
|
||||
|
||||
responseText = clienttcptransport.read().payload();
|
||||
logger.info("Raw message '" + responseText + "'");
|
||||
Boolean success = null;
|
||||
int count = 0;
|
||||
for (String line : responseText.split("\n")) {
|
||||
if (success == null) {
|
||||
if ("+OK".equals(line)) {
|
||||
success = true;
|
||||
continue;
|
||||
int reqId = idGenerator.incrementAndGet();
|
||||
ArrayBlockingQueue<ParsedResponse> q = new ArrayBlockingQueue<>(1);
|
||||
pendingResponses.put(reqId, q);
|
||||
|
||||
} else if (("-ERROR").equals(responseText)) {
|
||||
success = false;
|
||||
}
|
||||
continue;
|
||||
} else if ("END".equals(line)) {
|
||||
break;
|
||||
Future<?> writeFuture =
|
||||
executor.submit(
|
||||
() -> {
|
||||
try {
|
||||
clienttcptransport.write(new RawPacket(reqId, message));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
line = line.replaceFirst("^\t", "");
|
||||
response.add(line);
|
||||
}
|
||||
if (success != null && success) {
|
||||
return;
|
||||
} else {
|
||||
throw new RuntimeException("Error in " + message + ": " + response);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw getRuntimeException(e);
|
||||
}
|
||||
});
|
||||
return response;
|
||||
});
|
||||
|
||||
try {
|
||||
writeFuture.get();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
pendingResponses.remove(reqId);
|
||||
throw new RuntimeException(e);
|
||||
} catch (ExecutionException e) {
|
||||
pendingResponses.remove(reqId);
|
||||
throw getRuntimeException(e);
|
||||
}
|
||||
|
||||
ParsedResponse pr;
|
||||
try {
|
||||
pr = q.take();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
pendingResponses.remove(reqId);
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
pendingResponses.remove(reqId);
|
||||
}
|
||||
|
||||
if (pr.success) {
|
||||
return pr.lines;
|
||||
}
|
||||
|
||||
throw new RuntimeException("Error in " + message + ": " + pr.lines);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,15 +386,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,7 +403,17 @@ public class ClientService {
|
||||
*/
|
||||
public void closeSocket() {
|
||||
try {
|
||||
executor.shutdown();
|
||||
running.set(false);
|
||||
if (readerThread != null) {
|
||||
readerThread.interrupt();
|
||||
try {
|
||||
readerThread.join(READER_JOIN_TIMEOUT_MS);
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
executor.shutdownNow();
|
||||
clienttcptransport.close();
|
||||
socket.close();
|
||||
logger.info("Socket closed");
|
||||
@@ -227,16 +422,9 @@ public class ClientService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to write with the tcp transport to the server
|
||||
*
|
||||
* @param s - Message to be sent
|
||||
* @throws IOException
|
||||
*/
|
||||
private void writeToTransport(String s) throws IOException {
|
||||
int id = this.idGenerator.incrementAndGet();
|
||||
this.clienttcptransport.write(new RawPacket(id, s));
|
||||
}
|
||||
// removed unused helper writeToTransport; write is done via processCommand()
|
||||
// which
|
||||
// manages request ids and response matching
|
||||
|
||||
public void ping() {
|
||||
processCommand("PING");
|
||||
|
||||
@@ -3,97 +3,319 @@ package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.Card;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameState;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.Player;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* The GameClient class is responsible for communicating with the server to retrieve the current
|
||||
* game state. It sends a command to the server and parses the response into a structured GameState
|
||||
* object.
|
||||
* Fetches and parses game state from server.
|
||||
*
|
||||
* <p>Protocol notes (based on your current server implementation): - Success replies contain:
|
||||
* PHASE, POT, CURRENT_BET, DEALER, ACTIVE_PLAYER, then blocks: - CARD ... END (community cards on
|
||||
* root level) - PLAYER ... (NAME/CHIPS/BET/STATE + optional CARD blocks for requesting player) ...
|
||||
* END - Error replies contain: -ERR then CODE=..., MSG=..., END
|
||||
*
|
||||
* <p>Important: - The server does NOT wrap cards in a "CARDS" container. - CARD blocks appear
|
||||
* either: - at root level -> community cards - inside PLAYER -> hole cards for that player (usually
|
||||
* only for the requesting user)
|
||||
*/
|
||||
public class GameClient {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(GameClient.class.getName());
|
||||
|
||||
private final ClientService client;
|
||||
private final int gameId;
|
||||
|
||||
public GameClient(ClientService client, int gameId) {
|
||||
if (client == null) {
|
||||
throw new IllegalArgumentException("ClientService must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a GameClient with the given ClientService for communication.
|
||||
*
|
||||
* @param client The ClientService instance used to send commands and receive responses from the
|
||||
* server.
|
||||
*/
|
||||
public GameClient(ClientService client) {
|
||||
this.client = client;
|
||||
this.gameId = gameId;
|
||||
LOG.info(() -> "GameClient initialized for gameId=" + gameId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current game state from the server by sending a command and parsing the
|
||||
* response.
|
||||
*
|
||||
* @return A GameState object representing the current state of the game.
|
||||
*/
|
||||
public GameState getGameState() {
|
||||
List<String> response = client.processCommand("GET_GAME_STATE");
|
||||
return parseGameState(response);
|
||||
public GameState fetchGameState() {
|
||||
final String cmd = "GET_GAME_STATE GAME_ID=" + gameId;
|
||||
|
||||
List<String> lines;
|
||||
try {
|
||||
lines = client.processCommand(cmd);
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.SEVERE, "processCommand failed for " + cmd + ": " + e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lines == null || lines.isEmpty()) {
|
||||
LOG.warning("Empty server response for " + cmd);
|
||||
return null;
|
||||
}
|
||||
|
||||
LOG.info(() -> "GET_GAME_STATE raw lines count=" + lines.size() + " lines=" + lines);
|
||||
|
||||
String joined = String.join("\n", lines);
|
||||
|
||||
if (joined.contains("-ERR") || joined.contains("-ERROR")) {
|
||||
String code = extractValue(joined, "CODE");
|
||||
String msg = extractValue(joined, "MSG");
|
||||
LOG.info(() -> "GET_GAME_STATE returned -ERR code=" + code + " msg=" + msg);
|
||||
|
||||
if ("GAME_NOT_STARTED".equalsIgnoreCase(code)) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
GameState state = parseGameState(joined);
|
||||
LOG.info(
|
||||
() ->
|
||||
"Parsed state: phase="
|
||||
+ state.phase
|
||||
+ " pot="
|
||||
+ state.pot
|
||||
+ " players="
|
||||
+ (state.players != null ? state.players.size() : 0)
|
||||
+ " community="
|
||||
+ (state.communityCards != null
|
||||
? state.communityCards.size()
|
||||
: 0));
|
||||
return state;
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.SEVERE, "Failed parsing game state. Payload=\n" + joined, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the raw response from the server into a structured GameState object.
|
||||
*
|
||||
* @param input The raw response string from the server.
|
||||
* @return A GameState object representing the current state of the game.
|
||||
*/
|
||||
private GameState parseGameState(List<String> input) {
|
||||
public void sendCall() {
|
||||
try {
|
||||
client.processCommand("CALL GAME_ID=" + gameId);
|
||||
LOG.fine("CALL command sent");
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.WARNING, "Failed to send CALL: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
GameState state = new GameState();
|
||||
public void sendFold() {
|
||||
try {
|
||||
client.processCommand("FOLD GAME_ID=" + gameId);
|
||||
LOG.fine("FOLD command sent");
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.WARNING, "Failed to send FOLD: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// String[] lines = input.split("\n");
|
||||
public void sendBet(int amount) {
|
||||
try {
|
||||
client.processCommand("BET GAME_ID=" + gameId + " AMOUNT=" + amount);
|
||||
LOG.fine("BET command sent: amount=" + amount);
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.WARNING, "Failed to send BET: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
Player currentPlayer = null;
|
||||
Card currentCard = null;
|
||||
public void sendRaise(int amount) {
|
||||
try {
|
||||
client.processCommand("RAISE GAME_ID=" + gameId + " AMOUNT=" + amount);
|
||||
LOG.fine("RAISE command sent: amount=" + amount);
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.WARNING, "Failed to send RAISE: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
for (String rawLine : input) {
|
||||
private GameState parseGameState(String input) {
|
||||
GameState s = new GameState();
|
||||
s.players = new ArrayList<>();
|
||||
s.communityCards = new ArrayList<>();
|
||||
s.winnerNames = new ArrayList<>();
|
||||
|
||||
String line = rawLine.trim();
|
||||
|
||||
if (line.startsWith("+OK") || line.equals("END")) {
|
||||
ParseState state = new ParseState();
|
||||
for (String raw : input.split("\n")) {
|
||||
String line = raw.trim();
|
||||
if (line.isEmpty() || line.startsWith("+OK")) {
|
||||
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 (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 (!parseGlobalField(s, line)
|
||||
&& !parsePlayerStructure(s, state, line)
|
||||
&& !parseCardData(state, line)
|
||||
&& !parsePlayerData(state, line)) {
|
||||
LOG.fine(() -> "Ignored line: '" + line + "'");
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
return state;
|
||||
private boolean parseGlobalField(GameState s, String line) {
|
||||
return switch (line) {
|
||||
case String l when l.startsWith("PHASE=") -> {
|
||||
s.phase = value(l);
|
||||
yield true;
|
||||
}
|
||||
case String l when l.startsWith("POT=") -> {
|
||||
s.pot = intVal(l);
|
||||
yield true;
|
||||
}
|
||||
case String l when l.startsWith("CURRENT_BET=") -> {
|
||||
s.currentBet = intVal(l);
|
||||
yield true;
|
||||
}
|
||||
case String l when l.startsWith("DEALER=") -> {
|
||||
s.dealer = intVal(l);
|
||||
yield true;
|
||||
}
|
||||
case String l when l.startsWith("ACTIVE_PLAYER=") -> {
|
||||
s.activePlayer = intVal(l);
|
||||
yield true;
|
||||
}
|
||||
case String l when l.startsWith("WINNER=") -> {
|
||||
s.winnerIndex = intVal(l);
|
||||
yield true;
|
||||
}
|
||||
case String l when l.startsWith("WINNER_NAME=") -> {
|
||||
s.winnerNames.add(value(l));
|
||||
yield true;
|
||||
}
|
||||
case String l when l.startsWith("POT_PER_WINNER=") -> {
|
||||
s.potPerWinner = intVal(l);
|
||||
yield true;
|
||||
}
|
||||
case String l when l.startsWith("HIGHSCORE=") -> {
|
||||
s.highscoreEntries.add(value(l));
|
||||
yield true;
|
||||
}
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private boolean parsePlayerStructure(GameState s, ParseState state, String line) {
|
||||
if (line.equals("PLAYER")) {
|
||||
state.currentPlayer = new Player();
|
||||
s.players.add(state.currentPlayer);
|
||||
state.currentCard = null;
|
||||
state.insidePlayer = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.equals("CARDS")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.equals("CARD")) {
|
||||
state.currentCard = new Card("", "");
|
||||
if (state.insidePlayer && state.currentPlayer != null) {
|
||||
state.currentPlayer.addCard(state.currentCard);
|
||||
} else {
|
||||
s.communityCards.add(state.currentCard);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.equals("END")) {
|
||||
if (state.currentCard != null) {
|
||||
state.currentCard = null;
|
||||
} else if (state.insidePlayer) {
|
||||
state.insidePlayer = false;
|
||||
state.currentPlayer = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean parseCardData(ParseState state, String line) {
|
||||
if (state.currentCard == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (line.startsWith("VALUE=")) {
|
||||
state.currentCard.setValue(value(line));
|
||||
return true;
|
||||
}
|
||||
if (line.startsWith("SUIT=")) {
|
||||
state.currentCard.setSuit(value(line));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean parsePlayerData(ParseState state, String line) {
|
||||
if (state.currentPlayer == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (line.startsWith("USERNAME=") || line.startsWith("NAME=")) {
|
||||
state.currentPlayer.setId(PlayerId.of(value(line)));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.startsWith("ID=")) {
|
||||
if (state.currentPlayer.getId() == null) {
|
||||
state.currentPlayer.setId(PlayerId.of(value(line)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.startsWith("CHIPS=")) {
|
||||
state.currentPlayer.setChips(intVal(line));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.startsWith("BET=")) {
|
||||
state.currentPlayer.setBet(intVal(line));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.startsWith("STATE=")) {
|
||||
state.currentPlayer.setState(parseState(value(line)));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static class ParseState {
|
||||
Player currentPlayer;
|
||||
Card currentCard;
|
||||
boolean insidePlayer;
|
||||
}
|
||||
|
||||
private static String value(String line) {
|
||||
return line.split("=", 2)[1];
|
||||
}
|
||||
|
||||
private static int intVal(String line) {
|
||||
return Integer.parseInt(value(line));
|
||||
}
|
||||
|
||||
private static PlayerState parseState(String s) {
|
||||
if (s == null) {
|
||||
return PlayerState.ACTIVE;
|
||||
}
|
||||
|
||||
return switch (s.trim().toUpperCase()) {
|
||||
case "ACTIVE" -> PlayerState.ACTIVE;
|
||||
case "FOLDED" -> PlayerState.FOLDED;
|
||||
case "DEALER" -> PlayerState.DEALER;
|
||||
default -> PlayerState.ACTIVE;
|
||||
};
|
||||
}
|
||||
|
||||
private static String extractValue(String joined, String key) {
|
||||
if (joined == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (String raw : joined.split("\n")) {
|
||||
String line = raw.trim();
|
||||
if (line.startsWith(key + "=")) {
|
||||
return line.substring((key + "=").length()).replace("'", "");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The LobbyClient class is responsible for communicating with the server to manage game lobbies. It
|
||||
* provides methods to create a lobby, join a lobby, and fetch the current status of a lobby by
|
||||
@@ -29,7 +33,25 @@ public class LobbyClient {
|
||||
* @return A string representing the current status of the lobby, as returned by the server.
|
||||
*/
|
||||
public String fetchLobbyStatusString(int lobbyId) {
|
||||
return client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId).getFirst();
|
||||
List<String> lines = client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId);
|
||||
|
||||
// Prefer explicit STATUS parameter when available
|
||||
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||
for (RequestParameter p : params) {
|
||||
if ("STATUS".equalsIgnoreCase(p.key())) {
|
||||
return p.value();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: some servers may return a plain token as the first line
|
||||
if (!lines.isEmpty()) {
|
||||
String first = lines.get(0).trim();
|
||||
if ("CREATED".equalsIgnoreCase(first) || "RUNNING".equalsIgnoreCase(first)) {
|
||||
return first.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,8 +60,45 @@ public class LobbyClient {
|
||||
* @return The id of the newly created lobby, as returned by the server.
|
||||
*/
|
||||
public int createLobby() {
|
||||
String response = client.processCommand("CREATE_LOBBY").getFirst();
|
||||
return Integer.parseInt(response);
|
||||
List<String> lines = client.processCommand("CREATE_LOBBY");
|
||||
|
||||
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||
for (RequestParameter p : params) {
|
||||
if ("LOBBY_ID".equalsIgnoreCase(p.key())) {
|
||||
return Integer.parseInt(p.value());
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for simple legacy/test servers that return the id as a single plain
|
||||
// line
|
||||
if (!lines.isEmpty()) {
|
||||
try {
|
||||
return Integer.parseInt(lines.get(0));
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException("No LOBBY_ID in response: " + lines);
|
||||
}
|
||||
|
||||
/** Fetches the global highscores from the server. */
|
||||
public List<String> getHighscores() {
|
||||
List<String> lines = client.processCommand("GET_HIGHSCORES");
|
||||
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||
|
||||
List<String> entries = new ArrayList<>();
|
||||
for (RequestParameter p : params) {
|
||||
if ("HIGHSCORE".equalsIgnoreCase(p.key())) {
|
||||
entries.add(p.value());
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/** Clears all global highscores on the server. */
|
||||
public void clearHighscores() {
|
||||
client.processCommand("CLEAR_HIGHSCORES");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,8 +107,15 @@ public class LobbyClient {
|
||||
* @return The id of the lobby that the client is currently in, as returned by the server.
|
||||
*/
|
||||
public int getLobbyId() {
|
||||
String response = client.processCommand("GET_LOBBY_ID").getFirst();
|
||||
return Integer.parseInt(response);
|
||||
List<String> lines = client.processCommand("GET_LOBBY_ID");
|
||||
if (lines.isEmpty()) {
|
||||
throw new RuntimeException("GET_LOBBY_ID returned empty response");
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(lines.get(0).trim());
|
||||
} catch (NumberFormatException e) {
|
||||
throw new RuntimeException("Invalid GET_LOBBY_ID response: " + lines, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,12 +127,169 @@ public class LobbyClient {
|
||||
client.processCommand("JOIN_LOBBY ID=" + lobbyId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the server to mark the current user as absent from the given lobby.
|
||||
*
|
||||
* @param lobbyId the lobby to leave
|
||||
*/
|
||||
public void leaveLobby(int lobbyId) {
|
||||
client.processCommand("LEAVE_LOBBY ID=" + lobbyId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs in to the server with the given username by sending a "LOGIN" command.
|
||||
*
|
||||
* @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) {
|
||||
String command = buildLoginCommand(user);
|
||||
List<String> lines = client.processCommand(command);
|
||||
|
||||
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||
|
||||
String assigned = user;
|
||||
String id = null;
|
||||
for (RequestParameter p : params) {
|
||||
if ("USERNAME".equalsIgnoreCase(p.key())) {
|
||||
assigned = p.value();
|
||||
} else if ("ID".equalsIgnoreCase(p.key())) {
|
||||
id = p.value();
|
||||
}
|
||||
}
|
||||
return new LoginResult(assigned, id);
|
||||
}
|
||||
|
||||
private String buildLoginCommand(String username) {
|
||||
if (username == null || username.isBlank()) {
|
||||
return "LOGIN";
|
||||
}
|
||||
return "LOGIN USERNAME=" + username.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the username for the currently logged-in session.
|
||||
*
|
||||
* @param newUsername desired new username
|
||||
* @return a {@link LoginResult} containing assigned username and id as returned by the server
|
||||
*/
|
||||
public LoginResult changeUsername(String newUsername) {
|
||||
List<String> lines = client.processCommand("CHANGE_USERNAME USERNAME=" + newUsername);
|
||||
|
||||
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||
|
||||
String assigned = newUsername;
|
||||
String id = null;
|
||||
for (RequestParameter p : params) {
|
||||
if ("USERNAME".equalsIgnoreCase(p.key())) {
|
||||
assigned = p.value();
|
||||
} else if ("ID".equalsIgnoreCase(p.key())) {
|
||||
id = p.value();
|
||||
}
|
||||
}
|
||||
return new LoginResult(assigned, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the server for the list of available lobbies.
|
||||
*
|
||||
* @return list of LobbyInfo objects representing current lobbies
|
||||
*/
|
||||
public List<LobbyInfo> getLobbyList() {
|
||||
List<String> lines = client.processCommand("GET_LOBBY_LIST");
|
||||
|
||||
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||
|
||||
List<LobbyInfo> result = new ArrayList<>();
|
||||
|
||||
Integer currentId = null;
|
||||
String currentName = null;
|
||||
Integer currentPlayerCount = null;
|
||||
|
||||
for (RequestParameter p : params) {
|
||||
String key = p.key().toUpperCase();
|
||||
String val = p.value();
|
||||
switch (key) {
|
||||
case "ID":
|
||||
if (currentId != null) {
|
||||
result.add(
|
||||
new LobbyInfo(
|
||||
currentId,
|
||||
currentName,
|
||||
currentPlayerCount == null ? 0 : currentPlayerCount));
|
||||
currentName = null;
|
||||
currentPlayerCount = null;
|
||||
}
|
||||
try {
|
||||
currentId = Integer.parseInt(val);
|
||||
} catch (NumberFormatException e) {
|
||||
currentId = null;
|
||||
}
|
||||
break;
|
||||
case "NAME":
|
||||
currentName = val;
|
||||
break;
|
||||
case "PLAYER_COUNT":
|
||||
try {
|
||||
currentPlayerCount = Integer.parseInt(val);
|
||||
} catch (NumberFormatException e) {
|
||||
currentPlayerCount = 0;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentId != null) {
|
||||
result.add(
|
||||
new LobbyInfo(
|
||||
currentId,
|
||||
currentName,
|
||||
currentPlayerCount == null ? 0 : currentPlayerCount));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the current lobby members from GET_LOBBY_STATUS and returns their usernames in order.
|
||||
*
|
||||
* @param lobbyId id of the lobby to inspect
|
||||
* @return list of usernames currently known in the lobby
|
||||
*/
|
||||
public List<String> fetchLobbyPlayerNames(int lobbyId) {
|
||||
List<String> lines = client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId);
|
||||
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||
|
||||
List<String> names = new ArrayList<>();
|
||||
for (RequestParameter parameter : params) {
|
||||
if (!"USERNAME".equalsIgnoreCase(parameter.key())) {
|
||||
continue;
|
||||
}
|
||||
String name = parameter.value();
|
||||
if (name != null) {
|
||||
String trimmed = name.trim();
|
||||
if (!trimmed.isEmpty() && !names.contains(trimmed)) {
|
||||
names.add(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
/** Simple data holder for lobby metadata returned by the server. */
|
||||
public static final class LobbyInfo {
|
||||
public final int id;
|
||||
public final String name;
|
||||
public final int playerCount;
|
||||
|
||||
public LobbyInfo(int id, String name, int playerCount) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.playerCount = playerCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||
|
||||
/**
|
||||
* Result of a successful login request.
|
||||
*
|
||||
* <p>Holds the username assigned by the server and the server-side id as a string (UUID). The
|
||||
* client previously attempted to parse the id as an integer which failed when the server returned a
|
||||
* UUID; this class therefore stores the id as a {@link String}.
|
||||
*/
|
||||
public class LoginResult {
|
||||
private final String username;
|
||||
private final String id;
|
||||
|
||||
/**
|
||||
* Create a login result.
|
||||
*
|
||||
* @param username the assigned username
|
||||
* @param id the assigned id (UUID string) returned by the server
|
||||
*/
|
||||
public LoginResult(String username, String id) {
|
||||
this.username = username;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the assigned username
|
||||
*/
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the assigned id as returned by the server (UUID string)
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui;
|
||||
import java.net.URL;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.media.Media;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import javafx.scene.media.MediaView;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/** Plays an intro video in fullscreen and then starts the main UI. */
|
||||
public class IntroVideoPlayer extends Application {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(IntroVideoPlayer.class);
|
||||
|
||||
private Stage videoStage;
|
||||
|
||||
@Override
|
||||
public void start(Stage stage) {
|
||||
this.videoStage = stage;
|
||||
|
||||
URL videoUrl = getClass().getResource("/images/placeholder-animation.mp4");
|
||||
|
||||
if (videoUrl == null) {
|
||||
LOGGER.error("Video file not found!");
|
||||
Platform.exit();
|
||||
return;
|
||||
}
|
||||
|
||||
Media media = new Media(videoUrl.toExternalForm());
|
||||
MediaPlayer mediaPlayer = new MediaPlayer(media);
|
||||
MediaView mediaView = new MediaView(mediaPlayer);
|
||||
|
||||
StackPane root = new StackPane(mediaView);
|
||||
|
||||
// black background
|
||||
root.setStyle("-fx-background-color: black;");
|
||||
|
||||
Rectangle2D screenBounds = Screen.getPrimary().getBounds();
|
||||
|
||||
Scene scene = new Scene(root, screenBounds.getWidth(), screenBounds.getHeight());
|
||||
|
||||
// scale video automatically
|
||||
mediaView.setPreserveRatio(true);
|
||||
mediaView.setFitWidth(screenBounds.getWidth());
|
||||
mediaView.setFitHeight(screenBounds.getHeight());
|
||||
|
||||
stage.initStyle(StageStyle.UNDECORATED);
|
||||
stage.setFullScreen(true);
|
||||
stage.setFullScreenExitHint("");
|
||||
stage.setScene(scene);
|
||||
|
||||
// Add F11 fullscreen toggle
|
||||
scene.setOnKeyPressed(
|
||||
event -> {
|
||||
if (event.getCode() == KeyCode.F11) {
|
||||
stage.setFullScreen(!stage.isFullScreen());
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
|
||||
stage.show();
|
||||
|
||||
mediaPlayer.setOnEndOfMedia(this::onVideoEnd);
|
||||
mediaPlayer.play();
|
||||
}
|
||||
|
||||
private void onVideoEnd() {
|
||||
videoStage.close();
|
||||
setSystemProperties();
|
||||
startMainUI();
|
||||
}
|
||||
|
||||
private void setSystemProperties() {
|
||||
var params = getParameters().getRaw();
|
||||
if (params == null || params.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String arg = params.get(0);
|
||||
String[] parts = arg.split(":", 2);
|
||||
if (parts.length == 2) {
|
||||
System.setProperty("casono.server.host", parts[0]);
|
||||
System.setProperty("casono.server.port", parts[1]);
|
||||
}
|
||||
}
|
||||
|
||||
private void startMainUI() {
|
||||
Stage mainStage = new Stage();
|
||||
try {
|
||||
new Casinomainui().start(mainStage);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Platform.exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,8 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui;
|
||||
import javafx.application.Application;
|
||||
|
||||
/**
|
||||
* Launcher for the Casono main UI.
|
||||
*
|
||||
* <p>Default constructor for the application.
|
||||
*/
|
||||
/** Launcher for the Casono intro animation. */
|
||||
public class Launcher {
|
||||
|
||||
/** Default constructor. */
|
||||
@@ -21,6 +16,6 @@ public class Launcher {
|
||||
* @param args Command line arguments
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
Application.launch(Casinomainui.class, args);
|
||||
Application.launch(IntroVideoPlayer.class, args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController.ChatKey;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Node;
|
||||
@@ -20,23 +29,35 @@ public class ChatBoxController {
|
||||
|
||||
private String username;
|
||||
|
||||
private ChatController chatController;
|
||||
|
||||
@FXML private VBox chatBox;
|
||||
private final ChatController chatController;
|
||||
|
||||
@FXML private TabPane chatTabPane;
|
||||
|
||||
@FXML private MenuButton addWhisperChatButton;
|
||||
|
||||
private FXMLLoader fxmlLoader;
|
||||
private final List<String> activeWhisperChats;
|
||||
|
||||
@FXML private List<MenuItem> whisperUsers;
|
||||
|
||||
private String ressource = "/ui-structure/components/chatui/chattab.fxml";
|
||||
@FXML private final Map<String, Tab> usernameTabMap;
|
||||
|
||||
/**
|
||||
* Constructor for the ChatBoxController, initializes the necessary fields and data structures
|
||||
* for managing chat tabs and whisper chats.
|
||||
*
|
||||
* @param username The username of the current user.
|
||||
* @param chatController The ChatController instance responsible for handling chat logic and
|
||||
* communication with the server.
|
||||
*/
|
||||
public ChatBoxController(String username, ChatController chatController) {
|
||||
this.username = username;
|
||||
this.chatController = chatController;
|
||||
activeWhisperChats = new ArrayList<>();
|
||||
usernameTabMap = new HashMap<>();
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
if (username != null && !username.isBlank()) {
|
||||
this.username = username.trim();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,12 +67,15 @@ public class ChatBoxController {
|
||||
*/
|
||||
@FXML
|
||||
public void initialize() {
|
||||
ChatModel globalChatModel = new ChatModel(ChatType.GLOBAL, username, -1, null);
|
||||
chatController
|
||||
.getChatModelMap()
|
||||
.put(new ChatController.ChatKey(ChatType.GLOBAL), globalChatModel);
|
||||
addChatTab("GLOBAL", globalChatModel);
|
||||
// TODO: Button to add new Whisper Chat
|
||||
ChatType global = ChatType.GLOBAL;
|
||||
ChatModel globalChatModel = new ChatModel(global, username, -1, null);
|
||||
chatController.getChatModelMap().put(new ChatController.ChatKey(global), globalChatModel);
|
||||
addChatTab("GLOBAL", globalChatModel, global);
|
||||
addWhisperChatButton.setOnAction(
|
||||
_ -> {
|
||||
SoundManager.getInstance().playButtonClick();
|
||||
addWhisperChatButton.show();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,10 +85,74 @@ public class ChatBoxController {
|
||||
* @param targetUserName The username of the person to be added to the whisper list.
|
||||
*/
|
||||
public void addWhisperUser(String targetUserName) {
|
||||
MenuItem menuItem = new MenuItem(targetUserName);
|
||||
whisperUsers.add(menuItem);
|
||||
addWhisperChatButton.getItems().add(menuItem);
|
||||
menuItem.setOnAction(event -> addWhisperChat(targetUserName));
|
||||
Platform.runLater(
|
||||
() -> {
|
||||
for (MenuItem existing : addWhisperChatButton.getItems()) {
|
||||
if (targetUserName.equals(existing.getText())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
MenuItem menuItem = new MenuItem(targetUserName);
|
||||
addWhisperChatButton.getItems().add(menuItem);
|
||||
menuItem.setOnAction(
|
||||
_ -> {
|
||||
SoundManager.getInstance().playButtonClick();
|
||||
addWhisperChat(
|
||||
targetUserName,
|
||||
new ChatModel(
|
||||
ChatType.WHISPER, username, -1, targetUserName));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void removeWhisperUser(String targetUserName) {
|
||||
Platform.runLater(
|
||||
() ->
|
||||
addWhisperChatButton
|
||||
.getItems()
|
||||
.removeIf(item -> targetUserName.equals(item.getText())));
|
||||
}
|
||||
|
||||
public void renameWhisperUser(String oldUsername, String newUsername) {
|
||||
if (oldUsername == null
|
||||
|| newUsername == null
|
||||
|| oldUsername.isBlank()
|
||||
|| newUsername.isBlank()
|
||||
|| oldUsername.equals(newUsername)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Platform.runLater(
|
||||
() -> {
|
||||
for (MenuItem item : addWhisperChatButton.getItems()) {
|
||||
if (oldUsername.equals(item.getText())) {
|
||||
item.setText(newUsername);
|
||||
item.setOnAction(
|
||||
_ -> {
|
||||
SoundManager.getInstance().playButtonClick();
|
||||
addWhisperChat(
|
||||
newUsername,
|
||||
new ChatModel(
|
||||
ChatType.WHISPER,
|
||||
username,
|
||||
-1,
|
||||
newUsername));
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Tab tab = usernameTabMap.remove(oldUsername);
|
||||
if (tab != null) {
|
||||
tab.setText(newUsername);
|
||||
usernameTabMap.put(newUsername, tab);
|
||||
}
|
||||
|
||||
int idx = activeWhisperChats.indexOf(oldUsername);
|
||||
if (idx >= 0) {
|
||||
activeWhisperChats.set(idx, newUsername);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,12 +161,27 @@ public class ChatBoxController {
|
||||
*
|
||||
* @param target The username of the recipient for the private messages.
|
||||
*/
|
||||
public void addWhisperChat(String target) {
|
||||
ChatModel chatModel = new ChatModel(ChatType.WHISPER, username, -1, target);
|
||||
chatController
|
||||
.getChatModelMap()
|
||||
.put(new ChatController.ChatKey(ChatType.WHISPER, target), chatModel);
|
||||
addChatTab(target, chatModel);
|
||||
public void addWhisperChat(String target, ChatModel chatModel) {
|
||||
if (!activeWhisperChats.contains(target)) {
|
||||
activeWhisperChats.add(target);
|
||||
ChatType whisper = ChatType.WHISPER;
|
||||
chatController.getChatModelMap().put(new ChatKey(whisper, target), chatModel);
|
||||
ChatViewController chatViewController = addChatTab(target, chatModel, whisper);
|
||||
Tab chatTab = chatViewController.getChatTab();
|
||||
chatTab.setOnCloseRequest(
|
||||
_ -> {
|
||||
chatViewController.tabIsOpen = false;
|
||||
chatTabPane.getTabs().remove(chatTab);
|
||||
});
|
||||
} else {
|
||||
ChatViewController chatViewController =
|
||||
chatController.activeChatControllers.get(new ChatKey(ChatType.WHISPER, target));
|
||||
if (!chatViewController.tabIsOpen) {
|
||||
reopenChatTab(chatViewController.getChatTab());
|
||||
chatViewController.tabIsOpen = true;
|
||||
}
|
||||
chatTabPane.getSelectionModel().select(usernameTabMap.get(target));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,23 +193,105 @@ public class ChatBoxController {
|
||||
* @param chatModel The {@link ChatModel} containing the data and logic for this specific chat.
|
||||
* @throws RuntimeException If the FXML resource for the chat tab cannot be loaded.
|
||||
*/
|
||||
public void addChatTab(String title, ChatModel chatModel) {
|
||||
public ChatViewController addChatTab(String title, ChatModel chatModel, ChatType chatType) {
|
||||
String ressource = "/ui-structure/components/chatui/chattab.fxml";
|
||||
URL resource = getClass().getResource(ressource);
|
||||
FXMLLoader fxmlLoader = new FXMLLoader(resource);
|
||||
try {
|
||||
ChatViewController chatViewController =
|
||||
new ChatViewController(this.chatController, chatModel, this.username);
|
||||
fxmlLoader.setController(chatViewController);
|
||||
Node load = fxmlLoader.load();
|
||||
VBox.setVgrow(load, Priority.ALWAYS);
|
||||
VBox vbox = new VBox();
|
||||
VBox.setVgrow(vbox, Priority.ALWAYS);
|
||||
vbox.getChildren().add(load);
|
||||
chatModel.addListener((msg) -> chatViewController.showMessage(msg));
|
||||
Tab newChat = new Tab(title, vbox);
|
||||
this.chatTabPane.getTabs().add(newChat);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
return runOnPlatformSynchronized(
|
||||
() -> {
|
||||
try {
|
||||
ChatViewController chatViewController =
|
||||
new ChatViewController(this.chatController, chatModel, this);
|
||||
|
||||
ChatKey chatKey;
|
||||
if (chatType.equals(ChatType.WHISPER)) {
|
||||
chatKey = new ChatKey(chatType, title);
|
||||
} else {
|
||||
chatKey = new ChatKey(chatType);
|
||||
}
|
||||
chatController.activeChatControllers.put(chatKey, chatViewController);
|
||||
fxmlLoader.setController(chatViewController);
|
||||
Node load = fxmlLoader.load();
|
||||
VBox.setVgrow(load, Priority.ALWAYS);
|
||||
VBox vbox = new VBox();
|
||||
VBox.setVgrow(vbox, Priority.ALWAYS);
|
||||
vbox.getChildren().add(load);
|
||||
chatModel.addListener(chatViewController::showMessage);
|
||||
Tab newChat = new Tab(title, vbox);
|
||||
usernameTabMap.put(title, newChat);
|
||||
chatTabPane.getTabs().add(newChat);
|
||||
this.chatTabPane.getSelectionModel().select(newChat);
|
||||
chatViewController.setChatTab(newChat);
|
||||
if (!chatType.equals(ChatType.WHISPER)) {
|
||||
newChat.setClosable(false);
|
||||
}
|
||||
return chatViewController;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper Function to cope with a Threads problem occurring when the main JavaFX Thread performs
|
||||
* the task to add a new whisper Chat tab, and the task is specified to runLater.
|
||||
*/
|
||||
public static <T> T runOnPlatformSynchronized(Callable<T> code) {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
try {
|
||||
return code.call();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else {
|
||||
CompletableFuture<T> futureLock = new CompletableFuture<>();
|
||||
Platform.runLater(
|
||||
() -> {
|
||||
try {
|
||||
futureLock.complete(code.call());
|
||||
} catch (Exception e) {
|
||||
futureLock.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
try {
|
||||
return futureLock.get();
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void loadChats() {
|
||||
Map<ChatController.ChatKey, ChatModel> chatModelMap = chatController.getChatModelMap();
|
||||
ChatController.ChatKey key = new ChatController.ChatKey(ChatType.GLOBAL);
|
||||
ChatModel global = chatModelMap.get(key);
|
||||
ChatViewController globalController = chatController.activeChatControllers.get(key);
|
||||
|
||||
for (String user : chatController.getLocalUserList()) {
|
||||
addWhisperUser(user);
|
||||
}
|
||||
|
||||
for (Message msg : global.messages) {
|
||||
globalController.showMessage(msg);
|
||||
}
|
||||
for (String user : chatController.getLocalUserList()) {
|
||||
ChatController.ChatKey whisperUserKey =
|
||||
new ChatController.ChatKey(ChatType.WHISPER, user);
|
||||
if (chatModelMap.containsKey(whisperUserKey)) {
|
||||
ChatModel whisperChatModel = chatModelMap.get(whisperUserKey);
|
||||
addWhisperChat(user, whisperChatModel);
|
||||
for (Message msg : whisperChatModel.messages) {
|
||||
chatController.activeChatControllers.get(whisperUserKey).showMessage(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void reopenChatTab(Tab chatTab) {
|
||||
Platform.runLater(
|
||||
() -> {
|
||||
chatTabPane.getTabs().add(chatTab);
|
||||
chatTabPane.getSelectionModel().select(chatTab);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager;
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
import javafx.application.Platform;
|
||||
@@ -11,37 +12,48 @@ import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/** Responsible for the presentation of the ChatModel to the Client */
|
||||
public class ChatViewController implements Initializable {
|
||||
|
||||
private final ChatBoxController chatBoxController;
|
||||
@FXML private Button sendButton;
|
||||
|
||||
@FXML private TextField inputField;
|
||||
|
||||
@FXML private VBox chatInterfaceVBox;
|
||||
|
||||
@FXML private HBox controlBar;
|
||||
|
||||
@FXML private ScrollPane scrollPane;
|
||||
|
||||
@FXML private VBox chat;
|
||||
|
||||
private final ChatModel chatModel;
|
||||
public Tab getChatTab() {
|
||||
return chatTab;
|
||||
}
|
||||
|
||||
private final String username;
|
||||
public void setChatTab(Tab chatTab) {
|
||||
this.chatTab = chatTab;
|
||||
}
|
||||
|
||||
@FXML private Tab chatTab;
|
||||
|
||||
public Boolean tabIsOpen;
|
||||
|
||||
private final ChatModel chatModel;
|
||||
|
||||
private final ChatController controller;
|
||||
|
||||
private static final int CHAT_PADDING = 20;
|
||||
|
||||
public ChatViewController(ChatController chatController, ChatModel chatModel, String username) {
|
||||
public ChatViewController(
|
||||
ChatController chatController,
|
||||
ChatModel chatModel,
|
||||
ChatBoxController chatBoxController) {
|
||||
this.controller = chatController;
|
||||
this.username = username;
|
||||
this.chatModel = chatModel;
|
||||
this.chatBoxController = chatBoxController;
|
||||
tabIsOpen = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,8 +66,8 @@ public class ChatViewController implements Initializable {
|
||||
*/
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resourceBundle) {
|
||||
inputField.setOnAction(event -> sendMessage());
|
||||
sendButton.setOnAction(event -> sendMessage());
|
||||
inputField.setOnAction(_ -> sendMessage());
|
||||
sendButton.setOnAction(_ -> sendMessage());
|
||||
scrollPane.vvalueProperty().bind(chat.heightProperty());
|
||||
}
|
||||
|
||||
@@ -65,14 +77,19 @@ public class ChatViewController implements Initializable {
|
||||
* The input field is cleared after sending.
|
||||
*/
|
||||
public void sendMessage() {
|
||||
SoundManager.getInstance().playButtonClick();
|
||||
String message = inputField.getText().trim();
|
||||
|
||||
message = message.replace("'", " ");
|
||||
|
||||
if (!message.isEmpty()) {
|
||||
inputField.clear();
|
||||
String currentUsername = controller.getCurrentUsername();
|
||||
Message msg =
|
||||
new Message(
|
||||
chatModel.getChattype(),
|
||||
chatModel.lobbyId,
|
||||
username,
|
||||
currentUsername,
|
||||
chatModel.getTarget(),
|
||||
message);
|
||||
controller.onSendToNetwork(msg);
|
||||
@@ -87,6 +104,10 @@ public class ChatViewController implements Initializable {
|
||||
* @param msg The {@link Message} object containing the content and metadata to display.
|
||||
*/
|
||||
public void showMessage(Message msg) {
|
||||
if (!tabIsOpen) {
|
||||
tabIsOpen = true;
|
||||
chatBoxController.reopenChatTab(chatTab);
|
||||
}
|
||||
Platform.runLater(
|
||||
() -> {
|
||||
String msgText =
|
||||
|
||||
@@ -1,57 +1,200 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.GameClient;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Logger;
|
||||
import javafx.application.Application;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public class CasinoGameUI extends Application {
|
||||
|
||||
// Static field for ClientService (workaround for JavaFX Application launch)
|
||||
private static ClientService staticClientService;
|
||||
private static final Logger LOG = Logger.getLogger(CasinoGameUI.class.getName());
|
||||
|
||||
public static void setClientService(ClientService clientService) {
|
||||
staticClientService = clientService;
|
||||
}
|
||||
private static ClientService clientService;
|
||||
private static ChatController chatController;
|
||||
|
||||
public static ClientService getClientService() {
|
||||
return staticClientService;
|
||||
}
|
||||
private static String username;
|
||||
private static int lobbyId = -1;
|
||||
|
||||
/** Default no-arg constructor. */
|
||||
private static final int DEFAULT_WIDTH = 1200;
|
||||
private static final int DEFAULT_HEIGHT = 800;
|
||||
private static final int GUEST_ID_LENGTH = 8;
|
||||
|
||||
/** Default constructor for the CasinoGameUI application. */
|
||||
public CasinoGameUI() {
|
||||
// default no-arg constructor
|
||||
}
|
||||
|
||||
private static final int DEFAULT_WIDTH = 1200;
|
||||
private static final int DEFAULT_HEIGHT = 800;
|
||||
/**
|
||||
* Sets the ClientService instance to be used by the application.
|
||||
*
|
||||
* @param clientService the ClientService instance to set
|
||||
*/
|
||||
public static void setClientService(ClientService clientService) {
|
||||
CasinoGameUI.clientService = clientService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the main stage of the application.
|
||||
* Sets the username to be used by the application.
|
||||
*
|
||||
* @param stage The main stage provided by the system.
|
||||
* @throws IOException If the FXML file or resources cannot be loaded.
|
||||
* @param username the username to set
|
||||
*/
|
||||
public static void setUsername(String username) {
|
||||
CasinoGameUI.username = username;
|
||||
}
|
||||
|
||||
public static void setChatController(ChatController chatController) {
|
||||
CasinoGameUI.chatController = chatController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the lobby ID to be used by the application.
|
||||
*
|
||||
* @param lobbyId the lobby ID to set
|
||||
*/
|
||||
public static void setLobbyId(int lobbyId) {
|
||||
CasinoGameUI.lobbyId = lobbyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The main entry point for the JavaFX application. This method is called after the application
|
||||
* is
|
||||
*
|
||||
* @param stage the primary stage for this application, onto which the application scene can be
|
||||
* set. Applications may create other stages, if needed, but they will not be primary
|
||||
* stages.
|
||||
* @throws IOException
|
||||
*/
|
||||
@Override
|
||||
public void start(Stage stage) throws IOException {
|
||||
ensureClientService();
|
||||
String effectiveUsername = determineEffectiveUsername();
|
||||
logStartInfo(effectiveUsername);
|
||||
|
||||
FXMLLoader fxmlLoader =
|
||||
new FXMLLoader(CasinoGameUI.class.getResource("/ui-structure/Casinogameui.fxml"));
|
||||
Scene scene = new Scene(fxmlLoader.load(), DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
stage.setTitle("Casono (GAME)");
|
||||
Parent root = fxmlLoader.load();
|
||||
CasinoGameController controller = fxmlLoader.getController();
|
||||
|
||||
configureController(controller, effectiveUsername);
|
||||
Scene scene = new Scene(root, DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
configureStage(stage, scene, controller);
|
||||
controller.start();
|
||||
}
|
||||
|
||||
private void ensureClientService() {
|
||||
if (clientService == null) {
|
||||
clientService = ClientApp.getSharedClientService();
|
||||
}
|
||||
if (clientService == null) {
|
||||
throw new IllegalStateException(
|
||||
"CasinoGameUI: clientService is null. "
|
||||
+ "Call CasinoGameUI.setClientService(...)"
|
||||
+ " or start via ClientApp with a shared connection.");
|
||||
}
|
||||
}
|
||||
|
||||
private String determineEffectiveUsername() {
|
||||
String effectiveUsername = normalize(username);
|
||||
if (effectiveUsername == null) {
|
||||
effectiveUsername = normalize(ClientApp.getSharedUsername());
|
||||
}
|
||||
if (effectiveUsername == null) {
|
||||
effectiveUsername =
|
||||
"Guest-" + UUID.randomUUID().toString().substring(0, GUEST_ID_LENGTH);
|
||||
}
|
||||
return effectiveUsername;
|
||||
}
|
||||
|
||||
private void logStartInfo(String effectiveUsername) {
|
||||
LOG.info(
|
||||
"CasinoGameUI starting: effectiveUsername='"
|
||||
+ effectiveUsername
|
||||
+ "', injectedUsername='"
|
||||
+ username
|
||||
+ "', sharedUsername='"
|
||||
+ ClientApp.getSharedUsername()
|
||||
+ "', hasClientService="
|
||||
+ (clientService != null));
|
||||
}
|
||||
|
||||
private void configureController(CasinoGameController controller, String effectiveUsername)
|
||||
throws IOException {
|
||||
if (lobbyId <= 0) {
|
||||
throw new IllegalStateException("CasinoGameUI: lobbyId must be set before start()");
|
||||
}
|
||||
|
||||
GameClient gameClient = new GameClient(clientService, lobbyId);
|
||||
GameService gameService = new GameService(gameClient);
|
||||
controller.setGameService(gameService);
|
||||
controller.setMyPlayerId(PlayerId.of(effectiveUsername));
|
||||
controller.setChatContext(effectiveUsername, clientService, lobbyId);
|
||||
|
||||
try {
|
||||
LobbyClient lobbyClient = new LobbyClient(clientService);
|
||||
controller.setLobbyPlayerNames(lobbyClient.fetchLobbyPlayerNames(lobbyId));
|
||||
} catch (Exception e) {
|
||||
LOG.fine("Could not preload lobby player names: " + e.getMessage());
|
||||
}
|
||||
|
||||
controller.startChat(chatController);
|
||||
}
|
||||
|
||||
private void configureStage(Stage stage, Scene scene, CasinoGameController controller) {
|
||||
stage.setTitle("Casono");
|
||||
String iconPath = getClass().getResource("/images/logoinverted.png").toExternalForm();
|
||||
stage.getIcons().add(new javafx.scene.image.Image(iconPath));
|
||||
stage.setScene(scene);
|
||||
stage.setFullScreen(true);
|
||||
stage.setFullScreenExitHint("");
|
||||
stage.setOnHidden(e -> controller.stop());
|
||||
scene.setOnKeyPressed(
|
||||
event -> {
|
||||
if (event.getCode() == KeyCode.F11) {
|
||||
stage.setFullScreen(!stage.isFullScreen());
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
stage.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point of the application.
|
||||
* Normalizes a string by trimming whitespace and converting blank strings to null.
|
||||
*
|
||||
* @param args Command line arguments.
|
||||
* @param s the string to normalize
|
||||
* @return the normalized string, or null if the input is null or blank
|
||||
*/
|
||||
private static String normalize(String s) {
|
||||
if (s == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String t = s.trim();
|
||||
return t.isBlank() ? null : t;
|
||||
}
|
||||
|
||||
/**
|
||||
* The main method serves as the entry point for the application. It launches the JavaFX
|
||||
* application.
|
||||
*
|
||||
* @param args command line arguments (not used)
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
launch();
|
||||
|
||||
@@ -4,17 +4,24 @@ import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
@@ -61,7 +68,13 @@ public class CasinoBrowserController {
|
||||
private static final CookieManager COOKIE_MANAGER =
|
||||
new CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class);
|
||||
private static final Logger LOGGER =
|
||||
LogManager.getLogger(CasinoBrowserController.class.getSimpleName());
|
||||
|
||||
private static final ObservableList<String> URL_SUGGESTIONS =
|
||||
FXCollections.observableArrayList(TRUSTED_DOMAINS);
|
||||
|
||||
private static ContextMenu autoCompleteMenu = new ContextMenu();
|
||||
|
||||
private static final int LOGO_HEIGHT = 40;
|
||||
private static final int CORNER_RADIUS = 40;
|
||||
@@ -74,12 +87,15 @@ public class CasinoBrowserController {
|
||||
private static final int ALERT_HEIGHT = 300;
|
||||
private static final String LOGO_PATH = "/images/logoinverted.png";
|
||||
private static final String LOGO_PATH_MAIN = "/images/logo.png";
|
||||
private static final int MAX_AUTOCOMPLETE_RESULTS = 5;
|
||||
|
||||
static {
|
||||
CookieHandler.setDefault(COOKIE_MANAGER);
|
||||
|
||||
TRUSTED_DOMAINS.add("wikipedia.org");
|
||||
TRUSTED_DOMAINS.add("youtube.com");
|
||||
TRUSTED_DOMAINS.add("github.com");
|
||||
TRUSTED_DOMAINS.add("search.brave.com");
|
||||
TRUSTED_DOMAINS.add("oracle.com");
|
||||
TRUSTED_DOMAINS.add("stackoverflow.com");
|
||||
TRUSTED_DOMAINS.add("docs.oracle.com");
|
||||
@@ -94,8 +110,69 @@ public class CasinoBrowserController {
|
||||
TRUSTED_DOMAINS.add("metager.de");
|
||||
TRUSTED_DOMAINS.add("unibas.ch");
|
||||
TRUSTED_DOMAINS.add("mojeek.com");
|
||||
TRUSTED_DOMAINS.add("searx.be ");
|
||||
TRUSTED_DOMAINS.add("searx.be");
|
||||
TRUSTED_DOMAINS.add("startpage.com");
|
||||
TRUSTED_DOMAINS.add("vscode.dev");
|
||||
TRUSTED_DOMAINS.add("casono.netlify.app");
|
||||
}
|
||||
|
||||
/** Aliases for common websites. */
|
||||
private static final java.util.Map<String, String> ALIASES =
|
||||
Map.ofEntries(
|
||||
Map.entry("wiki", "wikipedia.org"),
|
||||
Map.entry("yt", "youtube.com"),
|
||||
Map.entry("bra", "search.brave.com"),
|
||||
Map.entry("bs", "search.brave.com"),
|
||||
Map.entry("gh", "github.com"),
|
||||
Map.entry("google", "google.com"),
|
||||
Map.entry("duck", "duckduckgo.com"),
|
||||
Map.entry("bing", "bing.com"),
|
||||
Map.entry("meta", "metager.de"),
|
||||
Map.entry("mojeek", "mojeek.com"),
|
||||
Map.entry("searx", "searx.be"),
|
||||
Map.entry("startpage", "startpage.com"),
|
||||
Map.entry("stack", "stackoverflow.com"),
|
||||
Map.entry("so", "stackoverflow.com"),
|
||||
Map.entry("oracle", "oracle.com"),
|
||||
Map.entry("docs", "docs.oracle.com"),
|
||||
Map.entry("mdn", "developer.mozilla.org"),
|
||||
Map.entry("maven", "maven.apache.org"),
|
||||
Map.entry("gradle", "gradle.org"),
|
||||
Map.entry("spring", "spring.io"),
|
||||
Map.entry("jetbrains", "jetbrains.com"),
|
||||
Map.entry("uni", "unibas.ch"),
|
||||
Map.entry("vs", "vscode.dev"),
|
||||
Map.entry("vscode", "vscode.dev"),
|
||||
Map.entry("vs code", "vscode.dev"),
|
||||
Map.entry("main", "casono.netlify.app"),
|
||||
Map.entry("cas", "casono.netlify.app"),
|
||||
Map.entry("casono", "casono.netlify.app"));
|
||||
|
||||
/**
|
||||
* Resolves user input into a valid URL.
|
||||
*
|
||||
* @param input User input from the URL field, which can be a full URL, a domain name, or an
|
||||
* alias.
|
||||
* @return A properly formatted URL string that can be loaded by the browser, or the original
|
||||
* input if it cannot be resolved.
|
||||
*/
|
||||
private static String resolveInputToUrl(String input) {
|
||||
if (input == null || input.isBlank()) {
|
||||
return input;
|
||||
}
|
||||
|
||||
input = input.trim();
|
||||
|
||||
String alias = ALIASES.get(input.toLowerCase());
|
||||
if (alias != null) {
|
||||
return "https://" + alias;
|
||||
}
|
||||
|
||||
if (input.matches("^[a-zA-Z][a-zA-Z0-9+.-]*://.*")) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return "https://" + input;
|
||||
}
|
||||
|
||||
private static boolean javascriptEnabled = false;
|
||||
@@ -124,6 +201,15 @@ public class CasinoBrowserController {
|
||||
WebView webView = new WebView();
|
||||
WebEngine engine = webView.getEngine();
|
||||
|
||||
engine.locationProperty()
|
||||
.addListener(
|
||||
(obs, oldUrl, newUrl) -> {
|
||||
if (isDownloadUrl(newUrl)) {
|
||||
LOGGER.warn("Download navigation blocked: " + newUrl);
|
||||
engine.getLoadWorker().cancel();
|
||||
}
|
||||
});
|
||||
|
||||
engine.setJavaScriptEnabled(false);
|
||||
|
||||
configurePopupBlocker(engine);
|
||||
@@ -203,9 +289,9 @@ public class CasinoBrowserController {
|
||||
engine.setCreatePopupHandler(
|
||||
config -> {
|
||||
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||
alert.setTitle("Popup blockiert");
|
||||
alert.setTitle("Popup blocked");
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText("Popup wurde aus Sicherheitsgründen blockiert.");
|
||||
alert.setContentText("Casono Browser blocked a security threat.");
|
||||
|
||||
try {
|
||||
var stream = CasinoBrowserController.class.getResourceAsStream(LOGO_PATH);
|
||||
@@ -283,9 +369,90 @@ public class CasinoBrowserController {
|
||||
urlField.getStyleClass().add("gray-input-field");
|
||||
HBox.setHgrow(urlField, Priority.ALWAYS);
|
||||
|
||||
urlField.textProperty()
|
||||
.addListener(
|
||||
(obs, oldText, newText) -> {
|
||||
if (newText == null || newText.isBlank()) {
|
||||
autoCompleteMenu.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
String input = newText.toLowerCase();
|
||||
|
||||
String aliasMatch = ALIASES.get(input);
|
||||
if (aliasMatch != null) {
|
||||
autoCompleteMenu.getItems().clear();
|
||||
|
||||
MenuItem item = new MenuItem(aliasMatch);
|
||||
item.setOnAction(
|
||||
e -> {
|
||||
urlField.setText("https://" + aliasMatch);
|
||||
autoCompleteMenu.hide();
|
||||
});
|
||||
|
||||
autoCompleteMenu.getItems().add(item);
|
||||
autoCompleteMenu.show(urlField, javafx.geometry.Side.BOTTOM, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
updateSuggestions(input, urlField);
|
||||
});
|
||||
|
||||
urlField.focusedProperty()
|
||||
.addListener(
|
||||
(obs, oldVal, newVal) -> {
|
||||
if (!newVal) {
|
||||
autoCompleteMenu.hide();
|
||||
}
|
||||
});
|
||||
|
||||
return urlField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the autocomplete suggestions based on the current input in the URL field. Suggestions
|
||||
* are filtered from the list of trusted domains and sorted to prioritize those that start with
|
||||
* the input.
|
||||
*
|
||||
* @param input The current text input from the URL field, used to filter and sort suggestions.
|
||||
* @param urlField The TextField for the URL input, used to position the autocomplete menu and
|
||||
* update its content.
|
||||
*/
|
||||
private static void updateSuggestions(String input, TextField urlField) {
|
||||
autoCompleteMenu.getItems().clear();
|
||||
|
||||
URL_SUGGESTIONS.stream()
|
||||
.filter(
|
||||
domain ->
|
||||
domain.toLowerCase().startsWith(input)
|
||||
|| domain.toLowerCase().contains(input))
|
||||
.sorted(
|
||||
(a, b) -> {
|
||||
boolean aStarts = a.startsWith(input);
|
||||
boolean bStarts = b.startsWith(input);
|
||||
return Boolean.compare(!aStarts, !bStarts);
|
||||
})
|
||||
.limit(MAX_AUTOCOMPLETE_RESULTS)
|
||||
.forEach(
|
||||
domain -> {
|
||||
MenuItem item = new MenuItem(domain);
|
||||
|
||||
item.setOnAction(
|
||||
e -> {
|
||||
urlField.setText("https://" + domain);
|
||||
autoCompleteMenu.hide();
|
||||
});
|
||||
|
||||
autoCompleteMenu.getItems().add(item);
|
||||
});
|
||||
|
||||
if (!autoCompleteMenu.getItems().isEmpty()) {
|
||||
autoCompleteMenu.show(urlField, javafx.geometry.Side.BOTTOM, 0, 0);
|
||||
} else {
|
||||
autoCompleteMenu.hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Security Label
|
||||
*
|
||||
@@ -305,7 +472,7 @@ public class CasinoBrowserController {
|
||||
* @return Toggle button
|
||||
*/
|
||||
public static Button createJsToggle(WebEngine engine) {
|
||||
Button jsToggle = new Button("JS EINSCHALTEN");
|
||||
Button jsToggle = new Button("JS TURN ON");
|
||||
jsToggle.getStyleClass().add("red-button");
|
||||
|
||||
jsToggle.setOnAction(
|
||||
@@ -319,7 +486,7 @@ public class CasinoBrowserController {
|
||||
jsToggle.getStyleClass().add("yellow-button");
|
||||
|
||||
} else {
|
||||
jsToggle.setText("JS EINSCHALTEN");
|
||||
jsToggle.setText("JS TURN ON");
|
||||
jsToggle.getStyleClass().removeAll("yellow-button");
|
||||
jsToggle.getStyleClass().add("red-button");
|
||||
}
|
||||
@@ -410,7 +577,16 @@ public class CasinoBrowserController {
|
||||
*/
|
||||
public static void configureUrlEvents(
|
||||
WebEngine engine, TextField urlField, Label securityLabel) {
|
||||
urlField.setOnAction(e -> loadUrlSafely(engine, urlField.getText(), securityLabel));
|
||||
urlField.setOnKeyPressed(
|
||||
e -> {
|
||||
if (e.getCode() == KeyCode.ENTER) {
|
||||
if (!autoCompleteMenu.isShowing()) {
|
||||
String resolved = resolveInputToUrl(urlField.getText());
|
||||
urlField.setText(resolved);
|
||||
loadUrlSafely(engine, resolved, securityLabel);
|
||||
}
|
||||
}
|
||||
});
|
||||
engine.locationProperty().addListener((obs, o, n) -> urlField.setText(n));
|
||||
}
|
||||
|
||||
@@ -468,25 +644,54 @@ public class CasinoBrowserController {
|
||||
*/
|
||||
private static void loadUrlSafely(WebEngine engine, String url, Label securityLabel) {
|
||||
try {
|
||||
if (!url.startsWith("http")) {
|
||||
if (!url.matches("^[a-zA-Z][a-zA-Z0-9+.-]*://.*")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
|
||||
URI uri = new URI(url);
|
||||
String scheme = uri.getScheme();
|
||||
|
||||
// HTTPS required
|
||||
if (!"https".equalsIgnoreCase(uri.getScheme())) {
|
||||
String fullUrl = uri.toString();
|
||||
|
||||
if (isDownloadUrl(fullUrl)) {
|
||||
securityLabel.setText("BLOCKED DOWNLOAD");
|
||||
|
||||
LOGGER.warn("Download blocked: " + fullUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("file".equalsIgnoreCase(scheme)) {
|
||||
|
||||
Path baseDir = Paths.get(System.getProperty("user.dir")).normalize();
|
||||
|
||||
Path manual = baseDir.resolve("documents/docs/game-engine/manual.html").normalize();
|
||||
|
||||
Path outreach = baseDir.resolve("outreach/index.html").normalize();
|
||||
|
||||
Path requested = Paths.get(uri).normalize();
|
||||
|
||||
if (requested.equals(manual) || requested.equals(outreach)) {
|
||||
securityLabel.setText("LOCAL OK");
|
||||
engine.load(uri.toString());
|
||||
} else {
|
||||
securityLabel.setText("UNKNOWN");
|
||||
engine.load(uri.toString());
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!"https".equalsIgnoreCase(scheme)) {
|
||||
securityLabel.setText("BLOCKED");
|
||||
return;
|
||||
}
|
||||
|
||||
String host = uri.getHost();
|
||||
if (host == null) {
|
||||
if (host == null || host.isBlank()) {
|
||||
securityLabel.setText("ERROR");
|
||||
return;
|
||||
}
|
||||
|
||||
// Secure Domain Check
|
||||
boolean trusted =
|
||||
TRUSTED_DOMAINS.stream()
|
||||
.anyMatch(domain -> host.equals(domain) || host.endsWith("." + domain));
|
||||
@@ -496,13 +701,15 @@ public class CasinoBrowserController {
|
||||
securityLabel.setText("BLOCKED");
|
||||
return;
|
||||
}
|
||||
securityLabel.setText("UNBEKANNT");
|
||||
securityLabel.setText("UNKNOWN");
|
||||
} else {
|
||||
securityLabel.setText("SAFE");
|
||||
}
|
||||
|
||||
engine.load(uri.toString());
|
||||
|
||||
} catch (Exception e) {
|
||||
securityLabel.setText("ERROR");
|
||||
securityLabel.setText("INVALID");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,7 +726,7 @@ public class CasinoBrowserController {
|
||||
alert.setHeaderText("This website is unknown");
|
||||
String content =
|
||||
host
|
||||
+ "\n\nThis site has not been verified by the Casono browser.\n"
|
||||
+ "\n\nThis site has not been verified by the Casono Browser.\n"
|
||||
+ "Do you still want to open it?";
|
||||
alert.setContentText(content);
|
||||
|
||||
@@ -545,4 +752,18 @@ public class CasinoBrowserController {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given URL points to a downloadable file based on its extension.
|
||||
*
|
||||
* @param url the URL to check
|
||||
* @return true if the URL is likely a download link, false otherwise
|
||||
*/
|
||||
private static boolean isDownloadUrl(String url) {
|
||||
if (url == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return url.matches(".*\\.(exe|zip|dmg|msi|apk|jar|pdf)(\\?.*)?$");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||
import java.util.List;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.stage.Stage;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/** Controller for the highscore popup window. */
|
||||
public class HighscoreViewController {
|
||||
|
||||
private static final Logger LOGGER =
|
||||
LogManager.getLogger(HighscoreViewController.class.getSimpleName());
|
||||
|
||||
@FXML private ListView<String> highscoreList;
|
||||
@FXML private Label statusLabel;
|
||||
@FXML private Node highscoreRoot;
|
||||
|
||||
private LobbyClient lobbyClient;
|
||||
private double dragOffsetX;
|
||||
private double dragOffsetY;
|
||||
|
||||
public void setLobbyClient(LobbyClient lobbyClient) {
|
||||
this.lobbyClient = lobbyClient;
|
||||
}
|
||||
|
||||
/** Starts dragging the window from anywhere in the popup. */
|
||||
@FXML
|
||||
public void onPopupPressed(MouseEvent event) {
|
||||
Stage stage = getStage();
|
||||
if (stage == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dragOffsetX = stage.getX() - event.getScreenX();
|
||||
dragOffsetY = stage.getY() - event.getScreenY();
|
||||
}
|
||||
|
||||
/** Moves the popup window while the mouse is dragged anywhere on the popup. */
|
||||
@FXML
|
||||
public void onPopupDragged(MouseEvent event) {
|
||||
Stage stage = getStage();
|
||||
if (stage == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
stage.setX(event.getScreenX() + dragOffsetX);
|
||||
stage.setY(event.getScreenY() + dragOffsetY);
|
||||
}
|
||||
|
||||
/** Returns the popup stage if the root is already attached to a scene. */
|
||||
private Stage getStage() {
|
||||
if (highscoreRoot == null || highscoreRoot.getScene() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (Stage) highscoreRoot.getScene().getWindow();
|
||||
}
|
||||
|
||||
/** Loads current highscores from the server and refreshes the list. */
|
||||
@FXML
|
||||
public void refreshHighscores() {
|
||||
if (highscoreList == null || statusLabel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lobbyClient == null) {
|
||||
statusLabel.setText("Lobby client not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
List<String> entries = lobbyClient.getHighscores();
|
||||
highscoreList.getItems().setAll(entries);
|
||||
statusLabel.setText(
|
||||
entries.isEmpty() ? "No highscores yet." : entries.size() + " entries loaded.");
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.error("Failed to load highscores: {}", e.getMessage());
|
||||
statusLabel.setText("Failed to load highscores.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Clears all highscores globally on the server and reloads the list. */
|
||||
@FXML
|
||||
public void clearHighscores() {
|
||||
if (lobbyClient == null || statusLabel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
lobbyClient.clearHighscores();
|
||||
refreshHighscores();
|
||||
statusLabel.setText("Highscores cleared.");
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.error("Failed to clear highscores: {}", e.getMessage());
|
||||
statusLabel.setText("Failed to clear highscores.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Closes the highscore popup window. */
|
||||
@FXML
|
||||
public void closeWindow() {
|
||||
if (highscoreList == null || highscoreList.getScene() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Stage stage = (Stage) highscoreList.getScene().getWindow();
|
||||
stage.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
|
||||
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Controller for the movable notebook (Tips) display within the Casono Game UI.
|
||||
*
|
||||
* <p>Responsible for: - Drag-and-drop movement of the notebook, - Display of general gaming tips, -
|
||||
* Toggle visibility of the notebook.
|
||||
*/
|
||||
public class NotebookController {
|
||||
|
||||
private static final Logger LOGGER =
|
||||
LogManager.getLogger(NotebookController.class.getSimpleName());
|
||||
|
||||
@FXML private VBox notebook;
|
||||
@FXML private ScrollPane tipsContent;
|
||||
@FXML private VBox tipsList;
|
||||
@FXML private Button toggleButton;
|
||||
|
||||
private double xOffset = 0;
|
||||
private double yOffset = 0;
|
||||
private static final double NOTEBOOK_SCALE = 0.95;
|
||||
private static final double SCALE_NORMAL = 1.0;
|
||||
private boolean isVisible = true;
|
||||
|
||||
/** Standard constructor. Used by FXML. */
|
||||
public NotebookController() {
|
||||
// default constructor for FXML
|
||||
}
|
||||
|
||||
/** Initialize the notebook with default tips. */
|
||||
@FXML
|
||||
private void initialize() {
|
||||
if (tipsList != null) {
|
||||
loadDefaultTips();
|
||||
}
|
||||
}
|
||||
|
||||
/** Load default poker tips into the notebook. */
|
||||
private void loadDefaultTips() {
|
||||
tipsList.getChildren().clear();
|
||||
|
||||
String[] tips = {
|
||||
"TEXAS HOLD'EM GRUNDLAGEN\n\nJeder Spieler erhält 2 verdeckte Karten. "
|
||||
+ "Zusätzlich gibt es 5 Gemeinschaftskarten.\n"
|
||||
+ "Ziel: Beste 5-Karten-Hand bilden oder alle Gegner zum Fold bringen.\n"
|
||||
+ "Startstack: 20000 Chips ($)",
|
||||
"BLINDS & DEALER\n\nSmall Blind: 100 Chips | Big Blind: 200 Chips\n"
|
||||
+ "Die Blinds erzeugen den Startpot und "
|
||||
+ "müssen vor jeder Runde gezahlt werden.\n"
|
||||
+ "Der Dealer-Button bestimmt die Positionen "
|
||||
+ "sowie die Reihenfolge der Aktionen.",
|
||||
"SPIELABLAUF\n\nPreflop → Flop (3 Karten) → Turn (4. Karte) → River (5. Karte)\n"
|
||||
+ "Preflop beginnt links vom Big Blind.\n"
|
||||
+ "Nach dem Flop beginnt die Action links vom Dealer (aktive Spieler).",
|
||||
"AKTIONEN\n\nFold = aussteigen\nCall = mitgehen\nRaise = erhöhen\n"
|
||||
+ "Alle Aktionen müssen in korrekter Reihenfolge (Acting in Turn) erfolgen.",
|
||||
"POSITION IST ENTSCHEIDEND\n\nEarly Position: nur starke Hände spielen\n"
|
||||
+ "Middle Position: etwas breiter spielen\n"
|
||||
+ "Late Position: größter Vorteil durch mehr Informationen",
|
||||
"SHOWDOWN\n\nWenn nach der letzten Setzrunde mehrere Spieler übrig sind:\n"
|
||||
+ "Beste 5-Karten-Kombination aus Handkarten + Gemeinschaftskarten gewinnt.",
|
||||
"STRATEGIE & DENKEN\n\nPoker ist Strategie + Psychologie + Mathematik\n"
|
||||
+ "Entscheidungen basieren auf Wahrscheinlichkeiten, "
|
||||
+ "Position und Gegnerverhalten.",
|
||||
"WICHTIGE REGELN\n\n- Acting in Turn ist Pflicht\n"
|
||||
+ "- Jeder Spieler spielt nur seine eigene Hand (One Player One Hand)\n"
|
||||
+ "- Ungültige Einsätze werden blockiert oder korrigiert\n"
|
||||
+ "- Reihenfolge am Tisch muss immer eingehalten werden",
|
||||
"HÄUFIGE FEHLER\n\n❌ Zu viele Hände spielen\n"
|
||||
+ "❌ Position ignorieren\n❌ Zu oft callen statt folden\n"
|
||||
+ "❌ Stärke der Gegner unterschätzen\n❌ Tilt (emotional spielen)",
|
||||
"TOP 10 POKERHÄNDE\n\n"
|
||||
+ "1. 👑 Royal Flush\n"
|
||||
+ "A♠ K♠ Q♠ J♠ 10♠\n\n"
|
||||
+ "2. 🔥 Straight Flush\n"
|
||||
+ "9♥ 8♥ 7♥ 6♥ 5♥\n\n"
|
||||
+ "3. 💥 Four of a Kind (Poker)\n"
|
||||
+ "A♣ A♦ A♥ A♠ K♠\n\n"
|
||||
+ "4. 🏠 Full House\n"
|
||||
+ "K♣ K♦ K♥ 9♠ 9♦\n\n"
|
||||
+ "5. 🌊 Flush\n"
|
||||
+ "A♥ J♥ 8♥ 5♥ 2♥\n\n"
|
||||
+ "6. ➡ Straight\n"
|
||||
+ "10♣ 9♦ 8♠ 7♥ 6♣\n\n"
|
||||
+ "7. 🎯 Three of a Kind (Drilling)\n"
|
||||
+ "Q♣ Q♦ Q♥ 7♠ 2♦\n\n"
|
||||
+ "8. 👥 Two Pair\n"
|
||||
+ "J♣ J♦ 4♠ 4♥ A♣\n\n"
|
||||
+ "9. 👍 One Pair\n"
|
||||
+ "8♣ 8♦ K♠ J♥ 3♣\n\n"
|
||||
+ "10. 🃏 High Card\n"
|
||||
+ "A♠ J♦ 9♣ 5♥ 2♠"
|
||||
};
|
||||
|
||||
for (String tip : tips) {
|
||||
TextFlow tipBox = createTipElement(tip);
|
||||
tipsList.getChildren().add(tipBox);
|
||||
}
|
||||
}
|
||||
|
||||
private static final int LINE_SPACING = 4;
|
||||
private static final int TIP_WIDTH = 280;
|
||||
|
||||
/**
|
||||
* Create a styled text element for a single tip.
|
||||
*
|
||||
* @param tipText The text of the tip to display.
|
||||
* @return A TextFlow element containing the formatted tip.
|
||||
*/
|
||||
private TextFlow createTipElement(String tipText) {
|
||||
Text text = new Text(tipText);
|
||||
text.setStyle("-fx-font-size: 12; -fx-font-family: 'Segoe UI', Arial;");
|
||||
TextFlow flow = new TextFlow(text);
|
||||
flow.setStyle(
|
||||
"-fx-padding: 10; -fx-background-color: #f5f5f5; "
|
||||
+ "-fx-border-radius: 5; -fx-margin: 5;");
|
||||
flow.setLineSpacing(LINE_SPACING);
|
||||
flow.setPrefWidth(TIP_WIDTH);
|
||||
return flow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the notebook header is pressed with the mouse. Saves the relative position for
|
||||
* later correct repositioning.
|
||||
*
|
||||
* @param event The mouse event
|
||||
*/
|
||||
@FXML
|
||||
private void onNotebookPressed(MouseEvent event) {
|
||||
if (notebook == null || event == null) {
|
||||
return;
|
||||
}
|
||||
xOffset = event.getSceneX() - notebook.getLayoutX();
|
||||
yOffset = event.getSceneY() - notebook.getLayoutY();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called while dragging the notebook with the mouse. Updates the position and slightly scales
|
||||
* the notebook for visual feedback.
|
||||
*
|
||||
* @param event The mouse event
|
||||
*/
|
||||
@FXML
|
||||
private void onNotebookDragged(MouseEvent event) {
|
||||
if (notebook == null || event == null || notebook.getScene() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
notebook.setScaleX(NOTEBOOK_SCALE);
|
||||
notebook.setScaleY(NOTEBOOK_SCALE);
|
||||
|
||||
double targetX = event.getSceneX() - xOffset;
|
||||
double targetY = event.getSceneY() - yOffset;
|
||||
|
||||
double maxX = Math.max(0, notebook.getScene().getWidth() - scaledNodeWidth());
|
||||
double maxY = Math.max(0, notebook.getScene().getHeight() - scaledNodeHeight());
|
||||
|
||||
notebook.setLayoutX(clamp(targetX, 0, maxX));
|
||||
notebook.setLayoutY(clamp(targetY, 0, maxY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the mouse cursor is released over the notebook. Resets the notebook scaling to
|
||||
* normal size.
|
||||
*
|
||||
* @param event The mouse event
|
||||
*/
|
||||
@FXML
|
||||
private void onNotebookReleased(MouseEvent event) {
|
||||
if (notebook == null) {
|
||||
return;
|
||||
}
|
||||
notebook.setScaleX(SCALE_NORMAL);
|
||||
notebook.setScaleY(SCALE_NORMAL);
|
||||
|
||||
if (notebook.getScene() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
double maxX = Math.max(0, notebook.getScene().getWidth() - scaledNodeWidth());
|
||||
double maxY = Math.max(0, notebook.getScene().getHeight() - scaledNodeHeight());
|
||||
|
||||
notebook.setLayoutX(clamp(notebook.getLayoutX(), 0, maxX));
|
||||
notebook.setLayoutY(clamp(notebook.getLayoutY(), 0, maxY));
|
||||
}
|
||||
|
||||
/** Close the notebook window from the header minus button. */
|
||||
@FXML
|
||||
private void onToggleNotebook() {
|
||||
if (notebook == null) {
|
||||
return;
|
||||
}
|
||||
isVisible = false;
|
||||
notebook.setVisible(false);
|
||||
notebook.setManaged(false);
|
||||
}
|
||||
|
||||
/** Shows the tips notebook (makes it visible if currently hidden). */
|
||||
public void showTips() {
|
||||
if (notebook == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isVisible) {
|
||||
isVisible = true;
|
||||
notebook.setVisible(true);
|
||||
notebook.setManaged(true);
|
||||
}
|
||||
|
||||
if (tipsContent != null) {
|
||||
tipsContent.setVisible(true);
|
||||
tipsContent.setManaged(true);
|
||||
}
|
||||
|
||||
if (toggleButton != null) {
|
||||
toggleButton.setText("−");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the scaled width of the notebook node based on its current bounds and scale
|
||||
* factor.
|
||||
*
|
||||
* @return The scaled width of the notebook node.
|
||||
*/
|
||||
private double scaledNodeWidth() {
|
||||
double width = notebook.getBoundsInLocal().getWidth();
|
||||
if (width <= 0) {
|
||||
width = notebook.prefWidth(-1);
|
||||
}
|
||||
return Math.max(0, width * notebook.getScaleX());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the scaled height of the notebook node based on its current bounds and scale
|
||||
* factor.
|
||||
*
|
||||
* @return The scaled height of the notebook node.
|
||||
*/
|
||||
private double scaledNodeHeight() {
|
||||
double height = notebook.getBoundsInLocal().getHeight();
|
||||
if (height <= 0) {
|
||||
height = notebook.prefHeight(-1);
|
||||
}
|
||||
return Math.max(0, height * notebook.getScaleY());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamps a value between a minimum and maximum bound.
|
||||
*
|
||||
* @param value the value to clamp
|
||||
* @param min the lower bound
|
||||
* @param max the upper bound
|
||||
* @return the clamped value in the range [min, max]
|
||||
*/
|
||||
private double clamp(double value, double min, double max) {
|
||||
return Math.max(min, Math.min(value, max));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.Player;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/**
|
||||
* Controller for displaying a player's status in the poker game, including their name, chip count
|
||||
* and whether they are the dealer.
|
||||
*/
|
||||
public class PlayerStatusController {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(PlayerStatusController.class.getName());
|
||||
|
||||
@FXML Label playerName;
|
||||
@FXML Label playerMoney;
|
||||
@FXML ImageView dealerIcon;
|
||||
@FXML ImageView playerProfileImage;
|
||||
@FXML Pane parent;
|
||||
@FXML VBox playerStatusBox;
|
||||
@FXML HBox statusInnerBoxTop;
|
||||
@FXML HBox statusInnerBoxBottom;
|
||||
|
||||
private Image dealerImage;
|
||||
// Profile images (loaded from resources/images/profile-picture)
|
||||
private Image profileUserImage;
|
||||
private Image profileDealerImage;
|
||||
private Player player;
|
||||
private boolean turnHighlighted;
|
||||
private static final String DEALER_IMAGE_PATH = "/images/chip-dealer-blue-5.png";
|
||||
private static final double DEALER_ICON_X_FACTOR = 0.8;
|
||||
private static final String TURN_HIGHLIGHT_STYLE_CLASS = "player-status-active-turn";
|
||||
|
||||
/** Initialize the controller, load dealer image, and set up bindings. */
|
||||
@FXML
|
||||
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.log(Level.SEVERE, "Error loading dealer image", e);
|
||||
}
|
||||
|
||||
try {
|
||||
var base = "/images/profile-picture/";
|
||||
var uUser = getClass().getResource(base + "poker_user_491.png");
|
||||
var uDealer = getClass().getResource(base + "poker_dealer_471.png");
|
||||
|
||||
if (uUser != null) {
|
||||
profileUserImage = new Image(uUser.toExternalForm(), true);
|
||||
}
|
||||
if (uDealer != null) {
|
||||
profileDealerImage = new Image(uDealer.toExternalForm(), true);
|
||||
}
|
||||
|
||||
if (playerProfileImage != null && profileUserImage != null) {
|
||||
playerProfileImage.setImage(profileUserImage);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.log(Level.FINE, "Could not load profile images", e);
|
||||
}
|
||||
|
||||
dealerIcon.layoutXProperty().bind(parent.widthProperty().multiply(DEALER_ICON_X_FACTOR));
|
||||
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;
|
||||
if (player == null) {
|
||||
turnHighlighted = false;
|
||||
}
|
||||
refresh();
|
||||
updateTurnHighlightStyle();
|
||||
}
|
||||
|
||||
/** Refresh UI safely */
|
||||
public void refresh() {
|
||||
|
||||
if (player == null) {
|
||||
playerName.setText("-");
|
||||
playerMoney.setText("0 $");
|
||||
dealerIcon.setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight or unhighlight this player slot as the currently active turn.
|
||||
*
|
||||
* @param highlighted true if this slot should be highlighted, false otherwise.
|
||||
*/
|
||||
public void setTurnHighlighted(boolean highlighted) {
|
||||
this.turnHighlighted = highlighted;
|
||||
updateTurnHighlightStyle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the controller is currently bound to the provided player.
|
||||
*
|
||||
* @param candidate the player to compare against.
|
||||
* @return true if both represent the same player.
|
||||
*/
|
||||
public boolean hasPlayer(Player candidate) {
|
||||
if (player == null || candidate == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String currentName = normalizeIdentifier(player.getName());
|
||||
String candidateName = normalizeIdentifier(candidate.getName());
|
||||
if (currentName != null && candidateName != null) {
|
||||
return currentName.equals(candidateName);
|
||||
}
|
||||
|
||||
return player.getId() != null
|
||||
&& candidate.getId() != null
|
||||
&& player.getId().equals(candidate.getId());
|
||||
}
|
||||
|
||||
private String normalizeIdentifier(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* External quick update
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
if (playerProfileImage != null) {
|
||||
if (isDealer && profileDealerImage != null) {
|
||||
playerProfileImage.setImage(profileDealerImage);
|
||||
} else if (profileUserImage != null) {
|
||||
playerProfileImage.setImage(profileUserImage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the visual style of the player status box to indicate whether it's currently this
|
||||
* player's turn.
|
||||
*/
|
||||
private void updateTurnHighlightStyle() {
|
||||
if (statusInnerBoxTop == null || statusInnerBoxBottom == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (turnHighlighted) {
|
||||
if (!statusInnerBoxTop.getStyleClass().contains(TURN_HIGHLIGHT_STYLE_CLASS)) {
|
||||
statusInnerBoxTop.getStyleClass().add(TURN_HIGHLIGHT_STYLE_CLASS);
|
||||
}
|
||||
if (!statusInnerBoxBottom.getStyleClass().contains(TURN_HIGHLIGHT_STYLE_CLASS)) {
|
||||
statusInnerBoxBottom.getStyleClass().add(TURN_HIGHLIGHT_STYLE_CLASS);
|
||||
}
|
||||
} else {
|
||||
statusInnerBoxTop.getStyleClass().remove(TURN_HIGHLIGHT_STYLE_CLASS);
|
||||
statusInnerBoxBottom.getStyleClass().remove(TURN_HIGHLIGHT_STYLE_CLASS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Controller for the settings display within the poker UI. Handles theme selection and drag
|
||||
* movement.
|
||||
*/
|
||||
public class SettingsController {
|
||||
|
||||
private static final Logger LOGGER =
|
||||
LogManager.getLogger(SettingsController.class.getSimpleName());
|
||||
|
||||
@FXML private VBox settingsBox;
|
||||
@FXML private RadioButton themeStandard;
|
||||
@FXML private RadioButton themeStandardParquet;
|
||||
@FXML private RadioButton themeBlackWhite;
|
||||
@FXML private RadioButton themeGlass;
|
||||
@FXML private RadioButton themeLightGlass;
|
||||
private boolean visible = true;
|
||||
|
||||
private double xOffset;
|
||||
private double yOffset;
|
||||
|
||||
private static final double SCALE_DRAG = 0.95;
|
||||
private static final double SCALE_NORMAL = 1.0;
|
||||
|
||||
private String currentTheme = "standard";
|
||||
private Consumer<String> themeChangeListener;
|
||||
|
||||
/** Initialize the settings controller, set up theme toggle group and listeners. */
|
||||
@FXML
|
||||
private void initialize() {
|
||||
setupThemeToggle();
|
||||
}
|
||||
|
||||
/** Set up the theme toggle group and listeners for the theme selection radio buttons. */
|
||||
private void setupThemeToggle() {
|
||||
if (themeStandard == null
|
||||
|| themeStandardParquet == null
|
||||
|| themeBlackWhite == null
|
||||
|| themeGlass == null
|
||||
|| themeLightGlass == null) {
|
||||
LOGGER.warn("Theme buttons not injected from FXML");
|
||||
return;
|
||||
}
|
||||
|
||||
ToggleGroup group = new ToggleGroup();
|
||||
|
||||
themeStandard.setToggleGroup(group);
|
||||
themeStandardParquet.setToggleGroup(group);
|
||||
themeBlackWhite.setToggleGroup(group);
|
||||
themeGlass.setToggleGroup(group);
|
||||
themeLightGlass.setToggleGroup(group);
|
||||
|
||||
themeStandard.setSelected(true);
|
||||
|
||||
group.selectedToggleProperty()
|
||||
.addListener(
|
||||
(obs, oldVal, newVal) -> {
|
||||
if (newVal == themeStandard) {
|
||||
setTheme("standard");
|
||||
} else if (newVal == themeStandardParquet) {
|
||||
setTheme("mainuiparquet");
|
||||
} else if (newVal == themeBlackWhite) {
|
||||
setTheme("blackwhite");
|
||||
} else if (newVal == themeGlass) {
|
||||
setTheme("glass");
|
||||
} else if (newVal == themeLightGlass) {
|
||||
setTheme("lightglass");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current theme and notify the listener of the change.
|
||||
*
|
||||
* @param theme The new theme to set (e.g., "standard", "blackwhite", "glass").
|
||||
*/
|
||||
private void setTheme(String theme) {
|
||||
currentTheme = theme;
|
||||
|
||||
if (themeChangeListener != null) {
|
||||
themeChangeListener.accept(theme);
|
||||
}
|
||||
|
||||
LOGGER.info("Theme changed to: " + theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the mouse press event on the settings box to prepare for dragging.
|
||||
*
|
||||
* @param event The MouseEvent triggered when the user presses the mouse button on the settings
|
||||
* box.
|
||||
*/
|
||||
@FXML
|
||||
private void onSettingsPressed(MouseEvent event) {
|
||||
if (settingsBox == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
xOffset = event.getSceneX() - settingsBox.getLayoutX();
|
||||
yOffset = event.getSceneY() - settingsBox.getLayoutY();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the mouse drag event on the settings box to allow dragging it around the scene.
|
||||
*
|
||||
* @param event The MouseEvent triggered when the user drags the mouse while pressing on the
|
||||
* settings box.
|
||||
*/
|
||||
@FXML
|
||||
private void onSettingsDragged(MouseEvent event) {
|
||||
if (settingsBox == null || settingsBox.getScene() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
settingsBox.setScaleX(SCALE_DRAG);
|
||||
settingsBox.setScaleY(SCALE_DRAG);
|
||||
|
||||
double x = event.getSceneX() - xOffset;
|
||||
double y = event.getSceneY() - yOffset;
|
||||
|
||||
double maxX = settingsBox.getScene().getWidth() - settingsBox.getWidth();
|
||||
double maxY = settingsBox.getScene().getHeight() - settingsBox.getHeight();
|
||||
|
||||
settingsBox.setLayoutX(clamp(x, 0, maxX));
|
||||
settingsBox.setLayoutY(clamp(y, 0, maxY));
|
||||
}
|
||||
|
||||
/** Close the settings box from the header minus button. */
|
||||
@FXML
|
||||
private void onToggleSettings() {
|
||||
if (settingsBox == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
visible = !visible;
|
||||
|
||||
settingsBox.setVisible(visible);
|
||||
settingsBox.setManaged(visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the mouse release event on the settings box to reset its scale after dragging.
|
||||
*
|
||||
* @param event The MouseEvent triggered when the user releases the mouse button after dragging
|
||||
* the settings box.
|
||||
*/
|
||||
@FXML
|
||||
private void onSettingsReleased(MouseEvent event) {
|
||||
if (settingsBox == null || settingsBox.getScene() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
settingsBox.setScaleX(SCALE_NORMAL);
|
||||
settingsBox.setScaleY(SCALE_NORMAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a listener to be notified when the theme changes. The listener will receive the new theme
|
||||
* as a string.
|
||||
*
|
||||
* @param listener A Consumer that accepts a String representing the new theme (e.g.,
|
||||
* "standard", "blackwhite", "glass").
|
||||
*/
|
||||
public void setThemeChangeListener(Consumer<String> listener) {
|
||||
this.themeChangeListener = listener;
|
||||
}
|
||||
|
||||
/** Show the settings box by setting its visibility and managed properties to true. */
|
||||
public void show() {
|
||||
if (settingsBox == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
settingsBox.setVisible(true);
|
||||
settingsBox.setManaged(true);
|
||||
}
|
||||
|
||||
/** Hide the settings box by setting its visibility and managed properties to false. */
|
||||
public void hide() {
|
||||
if (settingsBox == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
settingsBox.setVisible(false);
|
||||
settingsBox.setManaged(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to clamp a value between a minimum and maximum range.
|
||||
*
|
||||
* @param value The value to clamp.
|
||||
* @param min The minimum allowed value.
|
||||
* @param max The maximum allowed value.
|
||||
* @return The clamped value, guaranteed to be between min and max.
|
||||
*/
|
||||
private double clamp(double value, double min, double max) {
|
||||
return Math.max(min, Math.min(value, max));
|
||||
}
|
||||
}
|
||||