Compare commits

..

33 Commits

Author SHA1 Message Date
TeamCity
2bc50664e0 ci: bump version to v3.0.0 2025-11-20 15:31:03 +00:00
3e3a062a06 ci: bump version to v2.0.0 2025-11-20 16:27:39 +01:00
641c892981 fix(polling): Improve polling mechanism and delay handling (#60)
Reviewed-on: #60
2025-11-20 10:51:39 +01:00
a58b2e03b1 feat(game)!: Fixed polling, SPA, Gameplayloop etc. (#59)
Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #59
Co-authored-by: Janis <janis.e.20@gmx.de>
Co-committed-by: Janis <janis.e.20@gmx.de>
2025-11-19 22:54:20 +01:00
e60fe7c98d feat(ci): Polling Added polling for when the game starts and a card gets played (#58)
Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #58
2025-11-14 09:11:32 +01:00
370de175db feat(ci): Polling
Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #53
2025-11-13 11:07:08 +01:00
5d245d0011 feat(ui): implement tie & trump menu, fixed some critical bugs (#52)
Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #52
Co-authored-by: Janis <janis.e.20@gmx.de>
Co-committed-by: Janis <janis.e.20@gmx.de>
2025-11-13 08:20:30 +01:00
c220e54bb8 feat(ui): added js routing, updated ingame ui, added tricktable (#50)
This merge request has full JS routing for calling specific endpoints. Game is fully playable but doesn't have polling yet. This version already has the UI changes adressed in MR #43 so first merge MR #43 and then this one or only merge this one because it already has the UI changes :)

Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #50
Reviewed-by: Janis <janis-e@gmx.de>
2025-11-12 11:44:21 +01:00
TeamCity
b847d3c054 ci: bump version to v1.0.9 2025-11-07 16:54:36 +00:00
c7dd72ecc2 fix: removed trailing 2025-11-07 17:52:12 +01:00
42a5adbae0 fix: removed trailing 2025-11-07 17:46:55 +01:00
TeamCity
ae9a8f2af9 ci: bump version to v1.0.8 2025-11-07 16:28:11 +00:00
7adc8b8645 fix: trailing 2025-11-07 17:25:35 +01:00
TeamCity
146348470f ci: bump version to v1.0.7 2025-11-07 15:52:23 +00:00
5e503cbc36 fix: removed trailing 2025-11-07 16:49:51 +01:00
TeamCity
126e2030ae ci: bump version to v1.0.6 2025-11-07 15:45:22 +00:00
54e3215127 fix: traling 2025-11-07 16:42:50 +01:00
TeamCity
72d2845772 ci: bump version to v1.0.5 2025-11-07 15:00:33 +00:00
64a7a63ab3 fix: removed trailing 2025-11-07 15:56:50 +01:00
TeamCity
51c36348b9 ci: bump version to v1.0.4 2025-11-07 14:47:49 +00:00
2e54880302 fix: changelog syntax 2025-11-07 15:45:31 +01:00
TeamCity
266406fe7c ci: bump version to v1.0.3 2025-11-07 14:38:07 +00:00
5c6d3ac436 fix: ensure proper CMD syntax in Dockerfile (#48)
Reviewed-on: #48
Co-authored-by: Janis <janis.e.20@gmx.de>
Co-committed-by: Janis <janis.e.20@gmx.de>
2025-11-07 15:24:08 +01:00
TeamCity
674619dadc ci: bump version to v1.0.2 2025-11-07 14:09:00 +00:00
TeamCity
ef539d3eea ci: bump version to v1.0.1 2025-11-07 14:02:39 +00:00
TeamCity
e09463b138 ci: bump version to v1.0.0 2025-11-07 13:45:15 +00:00
4d6ea54771 feat(docker): added docker container support (#47)
Reviewed-on: #47
Co-authored-by: Janis <janis.e.20@gmx.de>
Co-committed-by: Janis <janis.e.20@gmx.de>
2025-11-07 14:22:23 +01:00
aa83082d09 fix: update allowed hosts filter and adjust background color in login page (#45)
Reviewed-on: #45
2025-11-07 11:13:59 +01:00
42e7597e26 ci: revert setup (#44)
Reviewed-on: #44
2025-11-07 11:05:49 +01:00
TeamCity
6befd9c722 ci: bump version to v.. [skip ci] 2025-11-07 09:58:59 +00:00
51d9c0b5f6 fix: disabled external node 2025-11-07 10:18:11 +01:00
de565b52dc fix: update file paths and improve session handling in user interactions (#39)
Reviewed-on: #39
2025-11-06 10:03:28 +01:00
051e7406e3 feat(ui): add Lobby and Main Menu Body (#38)
Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #38
Co-authored-by: Janis <janis.e.20@gmx.de>
Co-committed-by: Janis <janis.e.20@gmx.de>
2025-11-06 09:03:09 +01:00
41 changed files with 2300 additions and 472 deletions

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
.env

1
.gitignore vendored
View File

@@ -138,3 +138,4 @@ target
/knockoutwhistweb/.g8/ /knockoutwhistweb/.g8/
/knockoutwhistweb/.bsp/ /knockoutwhistweb/.bsp/
/currentSnapshot.json /currentSnapshot.json
.env

2
.sbtopts Normal file
View File

@@ -0,0 +1,2 @@
-J--add-opens=java.base/java.util=ALL-UNNAMED
-J--add-opens=java.base/java.lang=ALL-UNNAMED

View File

@@ -1,4 +1,4 @@
## (2025-11-03) ## (2025-11-07)
### ⚠ BREAKING CHANGES ### ⚠ BREAKING CHANGES
@@ -7,7 +7,10 @@
### Features ### Features
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2)) * **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
* **docker:** added docker container support ([#47](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/47)) ([4d6ea54](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/4d6ea54771c284d5ea0bb798fedaf0423a6cdd58))
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb)) * implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
* **ui:** add Lobby and Main Menu Body ([#38](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/38)) ([051e740](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/051e7406e30385d3260631a783f507b90d9f94d1))
* **ui:** add main menu navbar and join game functionality ([#35](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/35)) ([32d4f9c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/32d4f9c6cefa20f10816dad9f33548a146d4c2c3))
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f)) * **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186)) * **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17) * **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
@@ -18,17 +21,22 @@
* **base:** fixed persistence logic ([#21](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/21)) ([7f765b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7f765b4514459202df3768c64a1df383fecb66e2)) * **base:** fixed persistence logic ([#21](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/21)) ([7f765b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7f765b4514459202df3768c64a1df383fecb66e2))
* **config:** modified git module to use the main branch ([#10](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/10)) ([ccf44ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf44ede41e10df4b973c9cecc09d1e7105143f5)) * **config:** modified git module to use the main branch ([#10](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/10)) ([ccf44ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf44ede41e10df4b973c9cecc09d1e7105143f5))
* disabled external node ([51d9c0b](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/51d9c0b5f687d951f42480e333f37b3155d58dbd))
* ensure proper unlocking of user session locks in game actions ([#37](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/37)) ([44c88c8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/44c88c8f60abf4a54e721f8e7f6de44a6d28b258))
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa)) * **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
* improve lock handling in user session interactions ([#36](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/36)) ([96c3846](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/96c38466d20203a561a5bc3bba560c9d9fd6d754))
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f)) * **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782)) * **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c)) * **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
* update allowed hosts filter and adjust background color in login page ([#45](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/45)) ([aa83082](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/aa83082d095b30fd3eae336ad5f1aa5d409fbdab))
* update file paths and improve session handling in user interactions ([#39](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/39)) ([de565b5](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/de565b52dca6b49b903eceaecd47cc3a5429608b))
### Reverts ### Reverts
* ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe)) * ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe))
* version bumb ([2e10059](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e10059a6756befe6f8e70c94fd34b865693efb8)) * version bumb ([2e10059](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e10059a6756befe6f8e70c94fd34b865693efb8))
* version bump ([7879d1a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7879d1ab6ee8f227c19b69b467a28dd7b479ff73)) * version bump ([7879d1a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7879d1ab6ee8f227c19b69b467a28dd7b479ff73))
## (2025-11-03) ## (2025-11-07)
### ⚠ BREAKING CHANGES ### ⚠ BREAKING CHANGES
@@ -37,7 +45,10 @@
### Features ### Features
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2)) * **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
* **docker:** added docker container support ([#47](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/47)) ([4d6ea54](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/4d6ea54771c284d5ea0bb798fedaf0423a6cdd58))
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb)) * implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
* **ui:** add Lobby and Main Menu Body ([#38](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/38)) ([051e740](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/051e7406e30385d3260631a783f507b90d9f94d1))
* **ui:** add main menu navbar and join game functionality ([#35](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/35)) ([32d4f9c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/32d4f9c6cefa20f10816dad9f33548a146d4c2c3))
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f)) * **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186)) * **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17) * **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
@@ -48,94 +59,72 @@
* **base:** fixed persistence logic ([#21](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/21)) ([7f765b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7f765b4514459202df3768c64a1df383fecb66e2)) * **base:** fixed persistence logic ([#21](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/21)) ([7f765b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7f765b4514459202df3768c64a1df383fecb66e2))
* **config:** modified git module to use the main branch ([#10](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/10)) ([ccf44ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf44ede41e10df4b973c9cecc09d1e7105143f5)) * **config:** modified git module to use the main branch ([#10](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/10)) ([ccf44ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf44ede41e10df4b973c9cecc09d1e7105143f5))
* disabled external node ([51d9c0b](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/51d9c0b5f687d951f42480e333f37b3155d58dbd))
* ensure proper unlocking of user session locks in game actions ([#37](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/37)) ([44c88c8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/44c88c8f60abf4a54e721f8e7f6de44a6d28b258))
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa)) * **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
* improve lock handling in user session interactions ([#36](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/36)) ([96c3846](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/96c38466d20203a561a5bc3bba560c9d9fd6d754))
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f)) * **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782)) * **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c)) * **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
* update allowed hosts filter and adjust background color in login page ([#45](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/45)) ([aa83082](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/aa83082d095b30fd3eae336ad5f1aa5d409fbdab))
* update file paths and improve session handling in user interactions ([#39](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/39)) ([de565b5](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/de565b52dca6b49b903eceaecd47cc3a5429608b))
### Reverts ### Reverts
* ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe)) * ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe))
* version bumb ([2e10059](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e10059a6756befe6f8e70c94fd34b865693efb8)) * version bumb ([2e10059](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e10059a6756befe6f8e70c94fd34b865693efb8))
* version bump ([7879d1a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7879d1ab6ee8f227c19b69b467a28dd7b479ff73)) * version bump ([7879d1a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7879d1ab6ee8f227c19b69b467a28dd7b479ff73))
## (2025-11-03)
### ⚠ BREAKING CHANGES ## (2025-11-07)
* implemented multigame support (#34)
### Features
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
* **ui:** LESS Integration ([72fcf78](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/72fcf783b8ca5ce260e4b152016353e7dc1edb69)), closes [#15-Create-a-default-theme-with-Less-](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/15-Create-a-default-theme-with-Less-) [#23](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/23)
* **ui:** UI now shows player names instead of their id ([#11](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/11)) ([c168ae7](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c168ae7dc05e99c1890661aa3b63f4d4d12779fb))
### Bug Fixes ### Bug Fixes
* **base:** fixed persistence logic ([#21](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/21)) ([7f765b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7f765b4514459202df3768c64a1df383fecb66e2)) * ensure proper CMD syntax in Dockerfile ([#48](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/48)) ([5c6d3ac](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/5c6d3ac436f6d23a36f58b6835c9bd50feddc789))
* **config:** modified git module to use the main branch ([#10](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/10)) ([ccf44ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf44ede41e10df4b973c9cecc09d1e7105143f5)) ## (2025-11-07)
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
### Reverts
* ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe))
* version bumb ([2e10059](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e10059a6756befe6f8e70c94fd34b865693efb8))
## (2025-11-03)
### ⚠ BREAKING CHANGES
* implemented multigame support (#34)
### Features
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
* **ui:** LESS Integration ([72fcf78](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/72fcf783b8ca5ce260e4b152016353e7dc1edb69)), closes [#15-Create-a-default-theme-with-Less-](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/15-Create-a-default-theme-with-Less-) [#23](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/23)
* **ui:** UI now shows player names instead of their id ([#11](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/11)) ([c168ae7](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c168ae7dc05e99c1890661aa3b63f4d4d12779fb))
### Bug Fixes ### Bug Fixes
* **base:** fixed persistence logic ([#21](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/21)) ([7f765b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7f765b4514459202df3768c64a1df383fecb66e2)) * changelog syntax ([2e54880](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e548803020c99f62644283fcf3570048261173a))
* **config:** modified git module to use the main branch ([#10](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/10)) ([ccf44ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf44ede41e10df4b973c9cecc09d1e7105143f5)) ## (2025-11-07)
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
### Reverts
* ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe))
## (2025-11-03)
### ⚠ BREAKING CHANGES
* implemented multigame support (#34)
### Features
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
* **ui:** LESS Integration ([72fcf78](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/72fcf783b8ca5ce260e4b152016353e7dc1edb69)), closes [#15-Create-a-default-theme-with-Less-](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/15-Create-a-default-theme-with-Less-) [#23](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/23)
* **ui:** UI now shows player names instead of their id ([#11](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/11)) ([c168ae7](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c168ae7dc05e99c1890661aa3b63f4d4d12779fb))
### Bug Fixes ### Bug Fixes
* **base:** fixed persistence logic ([#21](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/21)) ([7f765b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7f765b4514459202df3768c64a1df383fecb66e2)) * removed trailing ([64a7a63](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/64a7a63ab3dff59e66f62328e3b5865bb177fcde))
* **config:** modified git module to use the main branch ([#10](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/10)) ([ccf44ed](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/ccf44ede41e10df4b973c9cecc09d1e7105143f5)) ## (2025-11-07)
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f)) ### Bug Fixes
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c)) * traling ([54e3215](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/54e321512777f6722864694eb677eab0e8418a9f))
## (2025-11-07)
### Bug Fixes
* removed trailing ([5e503cb](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/5e503cbc364f7cb23926976acc6cee575eadd9d6))
## (2025-11-07)
### Bug Fixes
* trailing ([7adc8b8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7adc8b8645390cd18d63b4eee6db8ef448b7a46a))
## (2025-11-07)
### Bug Fixes
* removed trailing ([c7dd72e](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c7dd72ecc2786ef63daf2b4288093025a8e22bfd))
* removed trailing ([42a5adb](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/42a5adbae01802587a48a9de15bad44b5ef014cf))
## (2025-11-20)
### ⚠ BREAKING CHANGES
* **game:** Fixed polling, SPA, Gameplayloop etc. (#59)
### Features
* **ci:** Polling ([370de17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/370de175db65edf87e7ab211190977b540a39d85))
* **ci:** Polling Added polling for when the game starts and a card gets played ([#58](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/58)) ([e60fe7c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/e60fe7c98dcab05949140a8a54ed6e4e2fbbc022))
* **game:** Fixed polling, SPA, Gameplayloop etc. ([#59](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/59)) ([a58b2e0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/a58b2e03b11a54667d63ba6604f579a8e328c9d1))
* **ui:** added js routing, updated ingame ui, added tricktable ([#50](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/50)) ([c220e54](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c220e54bb8d87f4f0f37a089bcd993e8df806123)), closes [#43](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/43) [#43](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/43)
* **ui:** implement tie & trump menu, fixed some critical bugs ([#52](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/52)) ([5d245d0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/5d245d0011a5fb03193514303b45702cd8329224))
### Bug Fixes
* **polling:** Improve polling mechanism and delay handling ([#60](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/60)) ([641c892](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/641c892981649eb85640527cc0fe325ff683fa77))

41
Dockerfile Normal file
View File

@@ -0,0 +1,41 @@
# === Stage 1: Build the Play application ===
FROM sbtscala/scala-sbt:eclipse-temurin-alpine-22_36_1.10.3_3.5.1 AS builder
WORKDIR /app
# Install Node.js and Less CSS preprocessor
USER root
RUN apk add --no-cache nodejs npm && \
npm install -g less
# Cache dependencies first
COPY project ./project
COPY build.sbt ./
RUN sbt -Dscoverage.skip=true update
# Copy the rest of the code
COPY . .
# Build the app and stage it
RUN sbt -Dscoverage.skip=true clean stage
# === Stage 2: Runtime image ===
FROM eclipse-temurin:21-jre-alpine
# Install Argon2 CLI and libraries
RUN apk add --no-cache bash argon2 argon2-libs
WORKDIR /opt/playapp
# Copy staged Play build
COPY --from=builder /app/knockoutwhistweb/target/universal/stage /opt/playapp
# Expose the default Play port
EXPOSE 9000
# Set environment variables
ENV PLAY_HTTP_PORT=9000
# Run the Play app
ENTRYPOINT ["./bin/knockoutwhistweb"]
CMD ["-Dplay.server.pidfile.path=/dev/null"]

View File

@@ -19,7 +19,6 @@ lazy val commonSettings = Seq(
.map(m => "org.openjfx" % s"javafx-$m" % "21" classifier osName) .map(m => "org.openjfx" % s"javafx-$m" % "21" classifier osName)
}, },
libraryDependencies += guice, libraryDependencies += guice,
coverageEnabled := true,
coverageFailOnMinimum := true, coverageFailOnMinimum := true,
coverageMinimumStmtTotal := 85, coverageMinimumStmtTotal := 85,
coverageMinimumBranchTotal := 100 coverageMinimumBranchTotal := 100
@@ -40,7 +39,8 @@ lazy val knockoutwhistweb = project.in(file("knockoutwhistweb"))
libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "7.0.2" % Test, libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "7.0.2" % Test,
libraryDependencies += "de.mkammerer" % "argon2-jvm" % "2.12", libraryDependencies += "de.mkammerer" % "argon2-jvm" % "2.12",
libraryDependencies += "com.auth0" % "java-jwt" % "4.3.0", libraryDependencies += "com.auth0" % "java-jwt" % "4.3.0",
libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.2.2" libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.2.2",
JsEngineKeys.engineType := JsEngineKeys.EngineType.Node
) )
lazy val root = (project in file(".")) lazy val root = (project in file("."))

View File

@@ -1,6 +1,14 @@
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
--background-image: url('/assets/images/background.png'); --background-image: url('/assets/images/background.png') !important;
--color: white; --color: #f8f9fa !important; /* Light text on dark bg */
--highlightscolor: rgba(131, 131, 131, 0.75) !important;
--background-color: #192734;
/* Bootstrap variable overrides for dark mode */
--bs-body-color: var(--color);
--bs-link-color: #66b2ff;
--bs-link-hover-color: #99ccff;
--bs-border-color: rgba(255, 255, 255, 0.2);
--bs-heading-color: var(--color);
} }
} }

View File

@@ -1,4 +1,6 @@
:root { :root {
--background-image: url('/assets/images/img.png'); --background-image: url('/assets/images/img.png');
--color: black; --color: black;
--highlightscolor: rgba(0, 0, 0, 0.75);
--background-color: rgba(228, 232, 237, 1);
} }

View File

@@ -2,16 +2,87 @@
@import "dark-mode.less"; @import "dark-mode.less";
@import "login.less"; @import "login.less";
/* Provide default (light) variables so the site works even if light-mode.less fails */
:root {
--background-image: url('/assets/images/img.png');
--color: #212529; /* Bootstrap body text default */
/* Bootstrap variable overrides for light mode */
--bs-body-color: var(--color) !important;
--bs-link-color: #0d6efd !important;
--bs-link-hover-color: #0a58ca !important;
--bs-border-color: rgba(0, 0, 0, 0.125) !important;
--bs-heading-color: var(--color) !important;
}
@background-color: var(--background-color);
@highlightcolor: var(--highlightscolor);
@background-image: var(--background-image); @background-image: var(--background-image);
@color: var(--color); @color: var(--color);
@keyframes slideIn { @keyframes slideIn {
0% { transform: translateX(-100vw); } 0% { transform: translateX(-100vw); }
100% { transform: translateX(0); } 100% { transform: translateX(0); }
} }
.game-field-background { .game-field-background {
background-image: @background-image; background-image: @background-image;
background-size: 100vw 100vh;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover;
max-width: 1400px;
margin: 0 auto;
min-height: 100vh;
}
.lobby-background {
background-color: @background-color;
width: 100%;
height: 100vh;
}
.navbar-header{
text-align:center;
}
.navbar-toggle {
float: none;
margin-right:0;
}
.handcard :hover {
box-shadow: 3px 3px 3px @highlightcolor;
}
.inactive::after {
content: "";
position: absolute;
inset: 0; /* cover the whole container */
background: rgba(0, 0, 0, 0.50);
z-index: 10;
border-radius: 6px;
pointer-events: none; /* user can't click through overlay */
}
.bottom-div {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
max-width: 1400px;
width: 100%;
margin: 0;
text-align: center;
padding: 10px;
}
/* Ensure body text color follows theme variable and works with Bootstrap */
body {
color: @color;
}
.footer {
width: 100%;
text-align: center;
font-size: 12px;
color: @color;
padding: 0.5rem 0;
flex-grow: 1; /* fill remaining vertical space as visual footer background */
} }
.game-field { .game-field {
@@ -19,6 +90,15 @@
inset: 0; inset: 0;
overflow: auto; overflow: auto;
} }
.navbar-drop-shadow {
box-shadow: 0 1px 15px 0 #000000
}
.ingame-side-shadow {
box-shadow: 0 1px 15px 0 #000000
}
#sessions { #sessions {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -53,31 +133,21 @@
font-size: 40px; font-size: 40px;
font-family: Arial, serif; font-family: Arial, serif;
} }
#playercards {
display: flex; .ingame-cards-slide {
flex-direction: row; div {
justify-content: center; animation: slideIn 0.5s ease-out forwards;
height: 20%; animation-fill-mode: backwards;
img { &:nth-child(1) { animation-delay: 0.5s; }
animation: slideIn 0.5s ease-out forwards; &:nth-child(2) { animation-delay: 1s; }
animation-fill-mode: backwards; &:nth-child(3) { animation-delay: 1.5s; }
&:nth-child(1) { animation-delay: 0.5s; } &:nth-child(4) { animation-delay: 2s; }
&:nth-child(2) { animation-delay: 1s; } &:nth-child(5) { animation-delay: 2.5s; }
&:nth-child(3) { animation-delay: 1.5s; } &:nth-child(6) { animation-delay: 3s; }
&:nth-child(4) { animation-delay: 2s; } &:nth-child(7) { animation-delay: 3.5s; }
&:nth-child(5) { animation-delay: 2.5s; } }
&:nth-child(6) { animation-delay: 3s; }
&:nth-child(7) { animation-delay: 3.5s; }
}
} }
#cardsplayed {
display: flex;
flex-direction: row;
height: 10%;
min-height: 10%
}
#playedcardplayer { #playedcardplayer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -111,11 +181,6 @@
font-size: 20px; font-size: 20px;
} }
#trumpsuit {
display: flex;
flex-direction: row;
margin-left: 4%;
}
#nextPlayers { #nextPlayers {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -140,3 +205,51 @@
font-size: 1.5em; font-size: 1.5em;
font-family: Arial, serif; font-family: Arial, serif;
} }
.score-table {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 10px;
margin-bottom: 20px;
backdrop-filter: blur(8px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.score-header {
font-weight: bold;
color: #000000;
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
.score-row {
color: #000000;
}
/* In-game centered stage and blurred sides overlay */
.ingame-stage {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem 1rem;
}
/* Wrapper that adds a backdrop blur to the background outside the centered card */
.blur-sides {
position: relative;
}
/* Create an overlay that blurs everything behind it, except the central content area */
.blur-sides::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
/* fallback: subtle vignette if backdrop-filter unsupported */
background: radial-gradient(ellipse at center, rgba(0,0,0,0) 30%, rgba(0,0,0,0.35) 100%);
}
@supports ((-webkit-backdrop-filter: blur(8px)) or (backdrop-filter: blur(8px))) {
.blur-sides::before {
background: rgba(0,0,0,0.08);
-webkit-backdrop-filter: blur(10px) saturate(110%);
backdrop-filter: blur(10px) saturate(110%);
}
}

View File

@@ -15,7 +15,7 @@ class AuthAction @Inject()(val sessionManager: SessionManager, val parser: BodyP
override def executionContext: ExecutionContext = ec override def executionContext: ExecutionContext = ec
private def getUserFromSession(request: RequestHeader): Option[User] = { protected def getUserFromSession(request: RequestHeader): Option[User] = {
val session = request.cookies.get("sessionId") val session = request.cookies.get("sessionId")
if (session.isDefined) if (session.isDefined)
return sessionManager.getUserBySession(session.get.value) return sessionManager.getUserBySession(session.get.value)

View File

@@ -2,11 +2,12 @@ package components
import de.knockoutwhist.components.DefaultConfiguration import de.knockoutwhist.components.DefaultConfiguration
import de.knockoutwhist.ui.UI import de.knockoutwhist.ui.UI
import de.knockoutwhist.utils.DelayHandler
import de.knockoutwhist.utils.events.EventListener import de.knockoutwhist.utils.events.EventListener
class WebApplicationConfiguration extends DefaultConfiguration { class WebApplicationConfiguration extends DefaultConfiguration {
override def uis: Set[UI] = Set() override def uis: Set[UI] = Set()
override def listener: Set[EventListener] = Set() override def listener: Set[EventListener] = Set(DelayHandler)
} }

View File

@@ -1,56 +1,73 @@
package controllers package controllers
import auth.{AuthAction, AuthenticatedRequest} import auth.{AuthAction, AuthenticatedRequest}
import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak} import de.knockoutwhist.control.GameState.{FinishedMatch, InGame, Lobby, SelectTrump, TieBreak}
import exceptions.{CantPlayCardException, GameFullException, NotEnoughPlayersException, NotHostException, NotInThisGameException} import exceptions.*
import logic.PodManager import logic.PodManager
import model.sessions.{PlayerSession, UserSession} import logic.game.GameLobby
import model.sessions.UserSession
import model.users.User
import play.api.* import play.api.*
import play.api.libs.json.{JsValue, Json}
import play.api.mvc.* import play.api.mvc.*
import play.twirl.api.Html
import java.util.UUID
import javax.inject.* import javax.inject.*
import scala.concurrent.ExecutionContext
import scala.util.Try import scala.util.Try
/**
* This controller creates an `Action` to handle HTTP requests to the
* application's home page.
*/
@Singleton @Singleton
class IngameController @Inject()( class IngameController @Inject() (
val controllerComponents: ControllerComponents, val cc: ControllerComponents,
val authAction: AuthAction, val podManager: PodManager,
val podManager: PodManager val authAction: AuthAction,
) extends BaseController { implicit val ec: ExecutionContext
) extends AbstractController(cc) {
def returnInnerHTML(gameLobby: GameLobby, user: User): Html = {
gameLobby.logic.getCurrentState match {
case Lobby => views.html.lobby.lobby(Some(user), gameLobby)
case InGame =>
views.html.ingame.ingame(
gameLobby.getPlayerByUser(user),
gameLobby
)
case SelectTrump =>
views.html.ingame.selecttrump(
gameLobby.getPlayerByUser(user),
gameLobby
)
case TieBreak =>
views.html.ingame.tie(
gameLobby.getPlayerByUser(user),
gameLobby
)
case FinishedMatch =>
views.html.ingame.finishedMatch(
Some(user),
gameLobby
)
case _ =>
throw new IllegalStateException(s"Invalid game state for in-game view. GameId: ${gameLobby.id}" + s" State: ${gameLobby.logic.getCurrentState}")
}
}
def game(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => def game(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val game = podManager.getGame(gameId) val game = podManager.getGame(gameId)
game match { game match {
case Some(g) => case Some(g) =>
g.logic.getCurrentState match { val results = Try {
case Lobby => Ok("Lobby: " + gameId) returnInnerHTML(g, request.user)
case InGame => }
Ok(views.html.ingame.ingame( if (results.isSuccess) {
g.getPlayerByUser(request.user), Ok(views.html.main("In-Game - Knockout Whist")(results.get))
g.logic } else {
)) InternalServerError(results.failed.get.getMessage)
case SelectTrump =>
Ok(views.html.ingame.selecttrump(
g.getPlayerByUser(request.user),
g.logic
))
case TieBreak =>
Ok(views.html.ingame.tie(
g.getPlayerByUser(request.user),
g.logic
))
case _ =>
InternalServerError(s"Invalid game state for in-game view. GameId: $gameId" + s" State: ${g.logic.getCurrentState}")
} }
case None => case None =>
NotFound("Game not found") Redirect(routes.MainMenuController.mainMenu())
} }
//NotFound(s"Reached end of game method unexpectedly. GameId: $gameId")
} }
def startGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => def startGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val game = podManager.getGame(gameId) val game = podManager.getGame(gameId)
@@ -63,52 +80,80 @@ class IngameController @Inject()(
} }
} }
if (result.isSuccess) { if (result.isSuccess) {
NoContent Ok(Json.obj(
"status" -> "success",
"redirectUrl" -> routes.IngameController.game(gameId).url
))
} else { } else {
val throwable = result.failed.get val throwable = result.failed.get
throwable match { throwable match {
case _: NotInThisGameException => case _: NotInThisGameException =>
BadRequest(throwable.getMessage) BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _: NotHostException => case _: NotHostException =>
Forbidden(throwable.getMessage) Forbidden(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _: NotEnoughPlayersException => case _: NotEnoughPlayersException =>
BadRequest(throwable.getMessage) BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _ => case _ =>
InternalServerError(throwable.getMessage) InternalServerError(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
} }
} }
} }
def joinGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => def kickPlayer(gameId: String, playerToKick: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val game = podManager.getGame(gameId)
val playerToKickUUID = UUID.fromString(playerToKick)
val result = Try {
game.get.leaveGame(playerToKickUUID)
}
if(result.isSuccess) {
Ok(Json.obj(
"status" -> "success",
"redirectUrl" -> routes.IngameController.game(gameId).url
))
} else {
InternalServerError(Json.obj(
"status" -> "failure",
"errorMessage" -> "Something went wrong."
))
}
}
def leaveGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val game = podManager.getGame(gameId) val game = podManager.getGame(gameId)
val result = Try { val result = Try {
game match { game.get.leaveGame(request.user.id)
case Some(g) =>
g.addUser(request.user)
case None =>
NotFound("Game not found")
}
} }
if (result.isSuccess) { if (result.isSuccess) {
Redirect(routes.IngameController.game(gameId)) Ok(Json.obj(
"status" -> "success",
"redirectUrl" -> routes.MainMenuController.mainMenu().url
))
} else { } else {
val throwable = result.failed.get InternalServerError(Json.obj(
throwable match { "status" -> "failure",
case _: GameFullException => "errorMessage" -> "Something went wrong."
BadRequest(throwable.getMessage) ))
case _: IllegalArgumentException =>
BadRequest(throwable.getMessage)
case _: IllegalStateException =>
BadRequest(throwable.getMessage)
case _ =>
InternalServerError(throwable.getMessage)
}
} }
} }
def playCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => { def playCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => {
val game = podManager.getGame(gameId) val game = podManager.getGame(gameId)
game match { game match {
case Some(g) => case Some(g) =>
val cardIdOpt = request.body.asFormUrlEncoded.flatMap(_.get("cardId").flatMap(_.headOption)) val jsonBody = request.body.asJson
val cardIdOpt: Option[String] = jsonBody.flatMap { jsValue =>
(jsValue \ "cardID").asOpt[String]
}
cardIdOpt match { cardIdOpt match {
case Some(cardId) => case Some(cardId) =>
var optSession: Option[UserSession] = None var optSession: Option[UserSession] = None
@@ -120,27 +165,55 @@ class IngameController @Inject()(
} }
optSession.foreach(_.lock.unlock()) optSession.foreach(_.lock.unlock())
if (result.isSuccess) { if (result.isSuccess) {
NoContent Ok(Json.obj(
"status" -> "success"
))
} else { } else {
val throwable = result.failed.get val throwable = result.failed.get
throwable match { throwable match {
case _: CantPlayCardException => case _: CantPlayCardException =>
BadRequest(throwable.getMessage) BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _: NotInThisGameException => case _: NotInThisGameException =>
BadRequest(throwable.getMessage) BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _: IllegalArgumentException => case _: IllegalArgumentException =>
BadRequest(throwable.getMessage) BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _: IllegalStateException => case _: IllegalStateException =>
BadRequest(throwable.getMessage) BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _: NotInteractableException =>
BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _ => case _ =>
InternalServerError(throwable.getMessage) InternalServerError(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
} }
} }
case None => case None =>
BadRequest("cardId parameter is missing") BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> "cardId Parameter is missing"
))
} }
case None => case None =>
NotFound("Game not found") NotFound(Json.obj(
"status" -> "failure",
"errorMessage" -> "Game not found"
))
} }
} }
} }
@@ -148,7 +221,10 @@ class IngameController @Inject()(
val game = podManager.getGame(gameId) val game = podManager.getGame(gameId)
game match { game match {
case Some(g) => { case Some(g) => {
val cardIdOpt = request.body.asFormUrlEncoded.flatMap(_.get("cardId").flatMap(_.headOption)) val jsonBody = request.body.asJson
val cardIdOpt: Option[String] = jsonBody.flatMap { jsValue =>
(jsValue \ "cardID").asOpt[String]
}
var optSession: Option[UserSession] = None var optSession: Option[UserSession] = None
val result = Try { val result = Try {
cardIdOpt match { cardIdOpt match {
@@ -173,15 +249,30 @@ class IngameController @Inject()(
val throwable = result.failed.get val throwable = result.failed.get
throwable match { throwable match {
case _: CantPlayCardException => case _: CantPlayCardException =>
BadRequest(throwable.getMessage) BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _: NotInThisGameException => case _: NotInThisGameException =>
BadRequest(throwable.getMessage) BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _: IllegalArgumentException => case _: IllegalArgumentException =>
BadRequest(throwable.getMessage) BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _: IllegalStateException => case _: IllegalStateException =>
BadRequest(throwable.getMessage) BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _ => case _ =>
InternalServerError(throwable.getMessage) InternalServerError(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
} }
} }
} }
@@ -194,7 +285,10 @@ class IngameController @Inject()(
val game = podManager.getGame(gameId) val game = podManager.getGame(gameId)
game match { game match {
case Some(g) => case Some(g) =>
val trumpOpt = request.body.asFormUrlEncoded.flatMap(_.get("trump").flatMap(_.headOption)) val jsonBody = request.body.asJson
val trumpOpt: Option[String] = jsonBody.flatMap { jsValue =>
(jsValue \ "trump").asOpt[String]
}
trumpOpt match { trumpOpt match {
case Some(trump) => case Some(trump) =>
var optSession: Option[UserSession] = None var optSession: Option[UserSession] = None
@@ -211,13 +305,25 @@ class IngameController @Inject()(
val throwable = result.failed.get val throwable = result.failed.get
throwable match { throwable match {
case _: IllegalArgumentException => case _: IllegalArgumentException =>
BadRequest(throwable.getMessage) BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _: NotInThisGameException => case _: NotInThisGameException =>
BadRequest(throwable.getMessage) BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _: IllegalStateException => case _: IllegalStateException =>
BadRequest(throwable.getMessage) BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _ => case _ =>
InternalServerError(throwable.getMessage) InternalServerError(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
} }
} }
case None => case None =>
@@ -231,7 +337,10 @@ class IngameController @Inject()(
val game = podManager.getGame(gameId) val game = podManager.getGame(gameId)
game match { game match {
case Some(g) => case Some(g) =>
val tieOpt = request.body.asFormUrlEncoded.flatMap(_.get("tie").flatMap(_.headOption)) val jsonBody = request.body.asJson
val tieOpt: Option[String] = jsonBody.flatMap { jsValue =>
(jsValue \ "tie").asOpt[String]
}
tieOpt match { tieOpt match {
case Some(tie) => case Some(tie) =>
var optSession: Option[UserSession] = None var optSession: Option[UserSession] = None
@@ -248,13 +357,25 @@ class IngameController @Inject()(
val throwable = result.failed.get val throwable = result.failed.get
throwable match { throwable match {
case _: IllegalArgumentException => case _: IllegalArgumentException =>
BadRequest(throwable.getMessage) BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _: NotInThisGameException => case _: NotInThisGameException =>
BadRequest(throwable.getMessage) BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _: IllegalStateException => case _: IllegalStateException =>
BadRequest(throwable.getMessage) BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _ => case _ =>
InternalServerError(throwable.getMessage) InternalServerError(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
} }
} }
case None => case None =>
@@ -265,4 +386,46 @@ class IngameController @Inject()(
} }
} }
def returnToLobby(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val game = podManager.getGame(gameId)
game match {
case Some(g) =>
val result = Try {
val session = g.getUserSession(request.user.id)
g.returnToLobby(session)
}
if (result.isSuccess) {
Ok(Json.obj(
"status" -> "success",
"redirectUrl" -> routes.IngameController.game(gameId).url
))
} else {
val throwable = result.failed.get
throwable match {
case _: NotInThisGameException =>
BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _: IllegalStateException =>
BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _ =>
InternalServerError(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
}
}
case None =>
NotFound(Json.obj(
"status" -> "failure",
"errorMessage" -> "Game not found"
))
}
}
} }

View File

@@ -0,0 +1,35 @@
package controllers
import auth.{AuthAction, AuthenticatedRequest}
import logic.PodManager
import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents}
import play.api.routing.JavaScriptReverseRouter
import javax.inject.Inject
class JavaScriptRoutingController @Inject()(
val controllerComponents: ControllerComponents,
val authAction: AuthAction,
val podManager: PodManager
) extends BaseController {
def javascriptRoutes(): Action[AnyContent] =
Action { implicit request =>
Ok(
JavaScriptReverseRouter("jsRoutes")(
routes.javascript.MainMenuController.createGame,
routes.javascript.MainMenuController.joinGame,
routes.javascript.MainMenuController.navSPA,
routes.javascript.IngameController.startGame,
routes.javascript.IngameController.kickPlayer,
routes.javascript.IngameController.leaveGame,
routes.javascript.IngameController.playCard,
routes.javascript.IngameController.playDogCard,
routes.javascript.IngameController.playTrump,
routes.javascript.IngameController.playTie,
routes.javascript.IngameController.returnToLobby,
routes.javascript.PollingController.polling,
routes.javascript.UserController.login_Post
)
).as("text/javascript")
}
}

View File

@@ -3,6 +3,7 @@ package controllers
import auth.{AuthAction, AuthenticatedRequest} import auth.{AuthAction, AuthenticatedRequest}
import logic.PodManager import logic.PodManager
import play.api.* import play.api.*
import play.api.libs.json.Json
import play.api.mvc.* import play.api.mvc.*
import javax.inject.* import javax.inject.*
@@ -16,12 +17,13 @@ import javax.inject.*
class MainMenuController @Inject()( class MainMenuController @Inject()(
val controllerComponents: ControllerComponents, val controllerComponents: ControllerComponents,
val authAction: AuthAction, val authAction: AuthAction,
val podManager: PodManager val podManager: PodManager,
val ingameController: IngameController
) extends BaseController { ) extends BaseController {
// Pass the request-handling function directly to authAction (no nested Action) // Pass the request-handling function directly to authAction (no nested Action)
def mainMenu(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => def mainMenu(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
Ok(views.html.mainmenu.navbar(Some(request.user))) Ok(views.html.main("Knockout Whist - Create Game")(views.html.mainmenu.creategame(Some(request.user))))
} }
def index(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => def index(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
@@ -29,33 +31,86 @@ class MainMenuController @Inject()(
} }
def createGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => def createGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val gameLobby = podManager.createGame( val jsonBody = request.body.asJson
host = request.user, if (jsonBody.isDefined) {
name = s"${request.user.name}'s Game", val gamename: String = (jsonBody.get \ "lobbyname").asOpt[String]
maxPlayers = 4 .getOrElse(s"${request.user.name}'s Game")
)
Redirect(routes.IngameController.game(gameLobby.id)) val playeramount: String = (jsonBody.get \ "playeramount").asOpt[String]
.getOrElse(throw new IllegalArgumentException("Player amount is required."))
val gameLobby = podManager.createGame(
host = request.user,
name = gamename,
maxPlayers = playeramount.toInt
)
Ok(Json.obj(
"status" -> "success",
"redirectUrl" -> routes.IngameController.game(gameLobby.id).url,
"content" -> ingameController.returnInnerHTML(gameLobby, request.user).toString
))
} else {
BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> "Invalid form submission"
))
}
} }
def joinGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => def joinGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val postData = request.body.asFormUrlEncoded val jsonBody = request.body.asJson
if (postData.isDefined) { val gameId: Option[String] = jsonBody.flatMap { jsValue =>
val gameId = postData.get.get("gameId").flatMap(_.headOption).getOrElse("") (jsValue \ "gameId").asOpt[String]
val game = podManager.getGame(gameId) }
if (gameId.isDefined) {
val game = podManager.getGame(gameId.get)
game match { game match {
case Some(g) => case Some(g) =>
Redirect(routes.IngameController.joinGame(gameId)) g.addUser(request.user)
Ok(Json.obj(
"status" -> "success",
"redirectUrl" -> routes.IngameController.game(g.id).url,
"content" -> ingameController.returnInnerHTML(g, request.user).toString
))
case None => case None =>
NotFound("Game not found") NotFound(Json.obj(
"status" -> "failure",
"errorMessage" -> "No Game found"
))
} }
} else { } else {
BadRequest("Invalid form submission") BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> "Invalid form submission"
))
} }
} }
def rules(): Action[AnyContent] = { def rules(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
Action { implicit request => Ok(views.html.main("Knockout Whist - Rules")(views.html.mainmenu.rules(Some(request.user))))
Ok(views.html.mainmenu.rules()) }
def navSPA(location: String) : Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
location match {
case "0" => // Main Menu
Ok(Json.obj(
"status" -> "success",
"redirectUrl" -> routes.MainMenuController.mainMenu().url,
"content" -> views.html.mainmenu.creategame(Some(request.user)).toString
))
case "1" => // Rules
Ok(Json.obj(
"status" -> "success",
"redirectUrl" -> routes.MainMenuController.rules().url,
"content" -> views.html.mainmenu.rules(Some(request.user)).toString
))
case _ =>
BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> "Invalid form submission"
))
} }
} }
} }

View File

@@ -0,0 +1,153 @@
package controllers
import auth.{AuthAction, AuthenticatedRequest}
import controllers.PollingController.{scheduler, timeoutDuration}
import de.knockoutwhist.cards.Hand
import de.knockoutwhist.player.AbstractPlayer
import logic.PodManager
import logic.game.{GameLobby, PollingEvents}
import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, NewTrick, ReloadEvent}
import model.sessions.UserSession
import model.users.User
import play.api.libs.json.{JsArray, JsValue, Json}
import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents, Result}
import util.WebUIUtils
import javax.inject.{Inject, Singleton}
import scala.concurrent.{ExecutionContext, Future}
import java.util.concurrent.{Executors, ScheduledExecutorService, TimeUnit}
import scala.concurrent.duration.*
object PollingController {
private val scheduler: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
private val timeoutDuration = 25.seconds
}
@Singleton
class PollingController @Inject() (
val cc: ControllerComponents,
val podManager: PodManager,
val authAction: AuthAction,
val ingameController: IngameController,
implicit val ec: ExecutionContext
) extends AbstractController(cc) {
private def buildCardPlayResponse(game: GameLobby, hand: Option[Hand], player: AbstractPlayer, newRound: Boolean): JsValue = {
val currentRound = game.logic.getCurrentRound.get
val currentTrick = game.logic.getCurrentTrick.get
val trickCardsJson = Json.toJson(
currentTrick.cards.map { case (card, player) =>
Json.obj("cardId" -> WebUIUtils.cardtoString(card), "player" -> player.name)
}
)
val scoreTableJson = Json.toJson(
game.getLogic.getPlayerQueue.get.toList.map { player =>
Json.obj(
"name" -> player.name,
"tricks" -> currentRound.tricklist.count(_.winner.contains(player))
)
}
)
val stringHand = hand.map { h =>
val cardStrings = h.cards.map(WebUIUtils.cardtoString)
Json.toJson(cardStrings).as[JsArray]
}.getOrElse(Json.arr())
val firstCardId = currentTrick.firstCard.map(WebUIUtils.cardtoString).getOrElse("BLANK")
val nextPlayer = game.getLogic.getPlayerQueue.get.duplicate().nextPlayer().name
Json.obj(
"status" -> "cardPlayed",
"animation" -> newRound,
"handData" -> stringHand,
"dog" -> player.isInDogLife,
"currentPlayerName" -> game.logic.getCurrentPlayer.get.name,
"trumpSuit" -> currentRound.trumpSuit.toString,
"trickCards" -> trickCardsJson,
"scoreTable" -> scoreTableJson,
"firstCardId" -> firstCardId,
"nextPlayer" -> nextPlayer,
"yourTurn" -> (game.logic.getCurrentPlayer.get == player)
)
}
private def buildLobbyUsersResponse(game: GameLobby, userSession: UserSession): JsValue = {
Json.obj(
"status" -> "lobbyUpdate",
"host" -> userSession.host,
"users" -> game.getUsers.map(u => Json.obj(
"name" -> u.name,
"id" -> u.id,
"self" -> (u.id == userSession.id)
)),
"maxPlayers" -> game.maxPlayers
)
}
def handleEvent(event: PollingEvents, game: GameLobby, userSession: UserSession): Result = {
event match {
case NewRound =>
val player = game.getPlayerByUser(userSession.user)
val hand = player.currentHand()
val jsonResponse = buildCardPlayResponse(game, hand, player, true)
Ok(jsonResponse)
case NewTrick =>
val player = game.getPlayerByUser(userSession.user)
val hand = player.currentHand()
val jsonResponse = buildCardPlayResponse(game, hand, player, false)
Ok(jsonResponse)
case CardPlayed =>
val player = game.getPlayerByUser(userSession.user)
val hand = player.currentHand()
val jsonResponse = buildCardPlayResponse(game, hand, player, false)
Ok(jsonResponse)
case LobbyUpdate =>
Ok(buildLobbyUsersResponse(game, userSession))
case ReloadEvent =>
val jsonResponse = Json.obj(
"status" -> "reloadEvent",
"redirectUrl" -> routes.IngameController.game(game.id).url,
"content" -> ingameController.returnInnerHTML(game, userSession.user).toString
)
Ok(jsonResponse)
}
}
def polling(gameId: String): Action[AnyContent] = authAction.async { implicit request: AuthenticatedRequest[AnyContent] =>
val playerId = request.user.id
podManager.getGame(gameId) match {
case Some(game) =>
val playerEventQueue = game.getEventsOfPlayer(playerId)
if (playerEventQueue.nonEmpty) {
val event = playerEventQueue.dequeue()
Future.successful(handleEvent(event, game, game.getUserSession(playerId)))
} else {
val eventPromise = game.registerWaiter(playerId)
val scheduledFuture = scheduler.schedule(
new Runnable {
override def run(): Unit =
eventPromise.tryFailure(new java.util.concurrent.TimeoutException("Polling Timeout"))
},
timeoutDuration.toMillis,
TimeUnit.MILLISECONDS
)
eventPromise.future.map { event =>
scheduledFuture.cancel(false)
game.removeWaiter(playerId)
handleEvent(event, game, game.getUserSession(playerId))
}.recover {
case _: Throwable =>
game.removeWaiter(playerId)
NoContent
}
}
case None =>
Future.successful(NotFound("Game not found."))
}
}
}

View File

@@ -3,6 +3,7 @@ package controllers
import auth.{AuthAction, AuthenticatedRequest} import auth.{AuthAction, AuthenticatedRequest}
import logic.user.{SessionManager, UserManager} import logic.user.{SessionManager, UserManager}
import play.api.* import play.api.*
import play.api.libs.json.Json
import play.api.mvc.* import play.api.mvc.*
import javax.inject.* import javax.inject.*
@@ -28,28 +29,35 @@ class UserController @Inject()(
if (possibleUser.isDefined) { if (possibleUser.isDefined) {
Redirect(routes.MainMenuController.mainMenu()) Redirect(routes.MainMenuController.mainMenu())
} else { } else {
Ok(views.html.login.login()) Ok(views.html.main("Login")(views.html.login.login()))
} }
} else { } else {
Ok(views.html.login.login()) Ok(views.html.main("Login")(views.html.login.login()))
} }
} }
} }
def login_Post(): Action[AnyContent] = { def login_Post(): Action[AnyContent] = {
Action { implicit request => Action { implicit request =>
val postData = request.body.asFormUrlEncoded val jsonBody = request.body.asJson
if (postData.isDefined) { val username: Option[String] = jsonBody.flatMap { jsValue =>
(jsValue \ "username").asOpt[String]
}
val password: Option[String] = jsonBody.flatMap { jsValue =>
(jsValue \ "password").asOpt[String]
}
if (username.isDefined && password.isDefined) {
// Extract username and password from form data // Extract username and password from form data
val username = postData.get.get("username").flatMap(_.headOption).getOrElse("") val possibleUser = userManager.authenticate(username.get, password.get)
val password = postData.get.get("password").flatMap(_.headOption).getOrElse("")
val possibleUser = userManager.authenticate(username, password)
if (possibleUser.isDefined) { if (possibleUser.isDefined) {
Redirect(routes.MainMenuController.mainMenu()).withCookies( Ok(Json.obj(
"status" -> "success",
"redirectUrl" -> routes.MainMenuController.mainMenu().url,
"content" -> views.html.mainmenu.creategame(possibleUser).toString
)).withCookies(
Cookie("sessionId", sessionManager.createSession(possibleUser.get)) Cookie("sessionId", sessionManager.createSession(possibleUser.get))
) )
} else { } else {
println("Failed login attempt for user: " + username)
Unauthorized("Invalid username or password") Unauthorized("Invalid username or password")
} }
} else { } else {

View File

@@ -2,21 +2,24 @@ package logic.game
import de.knockoutwhist.cards.{Hand, Suit} import de.knockoutwhist.cards.{Hand, Suit}
import de.knockoutwhist.control.GameLogic import de.knockoutwhist.control.GameLogic
import de.knockoutwhist.control.GameState.{Lobby, MainMenu} import de.knockoutwhist.control.GameState.{InGame, Lobby, MainMenu}
import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil} import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil}
import de.knockoutwhist.events.global.{GameStateChangeEvent, SessionClosed} import de.knockoutwhist.events.global.tie.TieTurnEvent
import de.knockoutwhist.events.player.PlayerEvent import de.knockoutwhist.events.global.{CardPlayedEvent, GameStateChangeEvent, NewTrickEvent, SessionClosed}
import de.knockoutwhist.events.player.{PlayerEvent, ReceivedHandEvent}
import de.knockoutwhist.player.Playertype.HUMAN import de.knockoutwhist.player.Playertype.HUMAN
import de.knockoutwhist.player.{AbstractPlayer, PlayerFactory} import de.knockoutwhist.player.{AbstractPlayer, PlayerFactory}
import de.knockoutwhist.rounds.{Match, Round, Trick} import de.knockoutwhist.rounds.{Match, Round, Trick}
import de.knockoutwhist.utils.events.{EventListener, SimpleEvent} import de.knockoutwhist.utils.events.{EventListener, SimpleEvent}
import exceptions.* import exceptions.*
import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, NewTrick, ReloadEvent}
import model.sessions.{InteractionType, UserSession} import model.sessions.{InteractionType, UserSession}
import model.users.User import model.users.User
import java.util.UUID import java.util.UUID
import scala.collection.mutable import scala.collection.mutable
import scala.collection.mutable.ListBuffer import scala.collection.mutable.ListBuffer
import scala.concurrent.Promise as ScalaPromise
class GameLobby private( class GameLobby private(
val logic: GameLogic, val logic: GameLogic,
@@ -25,10 +28,40 @@ class GameLobby private(
val name: String, val name: String,
val maxPlayers: Int val maxPlayers: Int
) extends EventListener { ) extends EventListener {
logic.addListener(this)
logic.createSession()
private val users: mutable.Map[UUID, UserSession] = mutable.Map() private val users: mutable.Map[UUID, UserSession] = mutable.Map()
private val eventsPerPlayer: mutable.Map[UUID, mutable.Queue[PollingEvents]] = mutable.Map()
private val waitingPromises: mutable.Map[UUID, ScalaPromise[PollingEvents]] = mutable.Map()
private val lock = new Object
lock.synchronized {
logic.addListener(this)
logic.createSession()
}
def registerWaiter(playerId: UUID): ScalaPromise[PollingEvents] = {
val promise = ScalaPromise[PollingEvents]()
lock.synchronized {
val queue = eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
if (queue.nonEmpty) {
val evt = queue.dequeue()
promise.success(evt)
promise
} else {
waitingPromises.put(playerId, promise)
promise
}
}
}
def removeWaiter(playerId: UUID): Unit = {
lock.synchronized {
waitingPromises.remove(playerId)
}
}
def addUser(user: User): UserSession = { def addUser(user: User): UserSession = {
if (users.size >= maxPlayers) throw new GameFullException("The game is full!") if (users.size >= maxPlayers) throw new GameFullException("The game is full!")
@@ -39,17 +72,29 @@ class GameLobby private(
host = false host = false
) )
users += (user.id -> userSession) users += (user.id -> userSession)
addToQueue(LobbyUpdate)
userSession userSession
} }
override def listen(event: SimpleEvent): Unit = { override def listen(event: SimpleEvent): Unit = {
event match { event match {
case event: ReceivedHandEvent =>
addToQueue(NewRound)
users.get(event.playerId).foreach(session => session.updatePlayer(event))
case event: CardPlayedEvent =>
addToQueue(CardPlayed)
case event: TieTurnEvent =>
addToQueue(ReloadEvent)
users.get(event.player.id).foreach(session => session.updatePlayer(event))
case event: PlayerEvent => case event: PlayerEvent =>
users.get(event.playerId).foreach(session => session.updatePlayer(event)) users.get(event.playerId).foreach(session => session.updatePlayer(event))
case event: NewTrickEvent =>
addToQueue(NewTrick)
case event: GameStateChangeEvent => case event: GameStateChangeEvent =>
if (event.oldState == MainMenu && event.newState == Lobby) { if (event.oldState == MainMenu && event.newState == Lobby) {
return return
} }
addToQueue(ReloadEvent)
users.values.foreach(session => session.updatePlayer(event)) users.values.foreach(session => session.updatePlayer(event))
case event: SessionClosed => case event: SessionClosed =>
users.values.foreach(session => session.updatePlayer(event)) users.values.foreach(session => session.updatePlayer(event))
@@ -58,6 +103,24 @@ class GameLobby private(
} }
} }
private def addToQueue(event: PollingEvents): Unit = {
lock.synchronized {
users.keys.foreach { playerId =>
val q = eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
q.enqueue(event)
}
val waiterIds = waitingPromises.keys.toList
waiterIds.foreach { playerId =>
val q = eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
if (q.nonEmpty) {
val evt = q.dequeue()
val p = waitingPromises.remove(playerId)
p.foreach(_.success(evt))
}
}
}
}
/** /**
* Start the game if the user is the host. * Start the game if the user is the host.
* @param user the user who wants to start the game. * @param user the user who wants to start the game.
@@ -88,12 +151,13 @@ class GameLobby private(
* Remove the user from the game lobby. * Remove the user from the game lobby.
* @param user the user who wants to leave the game. * @param user the user who wants to leave the game.
*/ */
def leaveGame(user: User): Unit = { def leaveGame(userId: UUID): Unit = {
val sessionOpt = users.get(user.id) val sessionOpt = users.get(userId)
if (sessionOpt.isEmpty) { if (sessionOpt.isEmpty) {
throw new NotInThisGameException("You are not in this game!") throw new NotInThisGameException("You are not in this game!")
} }
users.remove(user.id) users.remove(userId)
addToQueue(LobbyUpdate)
} }
/** /**
@@ -126,10 +190,11 @@ class GameLobby private(
throw new CantPlayCardException("You are not in dog life!") throw new CantPlayCardException("You are not in dog life!")
} }
if (cardIndex == -1) { if (cardIndex == -1) {
if (!MatchUtil.dogNeedsToPlay(getMatch, getRound)) { if (MatchUtil.dogNeedsToPlay(getMatch, getRound)) {
throw new CantPlayCardException("You can't skip this round!") throw new CantPlayCardException("You can't skip this round!")
} }
logic.playerInputLogic.receivedDog(None) logic.playerInputLogic.receivedDog(None)
return
} }
val hand = getHand(player) val hand = getHand(player)
val card = hand.cards(cardIndex) val card = hand.cards(cardIndex)
@@ -161,6 +226,19 @@ class GameLobby private(
logic.playerTieLogic.receivedTieBreakerCard(tieNumber) logic.playerTieLogic.receivedTieBreakerCard(tieNumber)
} }
def returnToLobby(userSession: UserSession): Unit = {
if (!users.contains(userSession.id)) {
throw new NotInThisGameException("You are not in this game!")
}
val session = users(userSession.id)
if (session != userSession) {
throw new IllegalArgumentException("User session does not match!")
}
if (!session.host)
throw new NotHostException("Only the host can return to the lobby!")
logic.createSession()
}
//------------------- //-------------------
@@ -176,6 +254,16 @@ class GameLobby private(
getPlayerBySession(getUserSession(user.id)) getPlayerBySession(getUserSession(user.id))
} }
def getPlayers: mutable.Map[UUID, UserSession] = {
users.clone()
}
def getLogic: GameLogic = {
logic
}
def getEventsOfPlayer(playerId: UUID): mutable.Queue[PollingEvents] = {
eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
}
private def getPlayerBySession(userSession: UserSession): AbstractPlayer = { private def getPlayerBySession(userSession: UserSession): AbstractPlayer = {
val playerOption = getMatch.totalplayers.find(_.id == userSession.id) val playerOption = getMatch.totalplayers.find(_.id == userSession.id)
if (playerOption.isEmpty) { if (playerOption.isEmpty) {
@@ -226,6 +314,10 @@ class GameLobby private(
trickOpt.get trickOpt.get
} }
def getUsers: Set[User] = {
users.values.map(d => d.user).toSet
}
} }
object GameLobby { object GameLobby {

View File

@@ -0,0 +1,10 @@
package logic.game
enum PollingEvents {
case CardPlayed
case NewRound
case NewTrick
case ReloadEvent
case LobbyUpdate
case LobbyCreation
}

View File

@@ -13,6 +13,7 @@ import java.time.Instant
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.{Inject, Singleton} import javax.inject.{Inject, Singleton}
import scala.util.Try
@Singleton @Singleton
class BaseSessionManager @Inject()(val keyProvider: JwtKeyProvider, val userManager: StubUserManager, val config: Config) extends SessionManager { class BaseSessionManager @Inject()(val keyProvider: JwtKeyProvider, val userManager: StubUserManager, val config: Config) extends SessionManager {
@@ -44,15 +45,21 @@ class BaseSessionManager @Inject()(val keyProvider: JwtKeyProvider, val userMana
} }
override def getUserBySession(sessionId: String): Option[User] = { override def getUserBySession(sessionId: String): Option[User] = {
//TODO verify JWT token instead of looking up in cache
val cachedUser = cache.getIfPresent(sessionId) val cachedUser = cache.getIfPresent(sessionId)
if (cachedUser != null) { if (cachedUser != null) {
Some(cachedUser) Some(cachedUser)
} else { } else {
val decoded = verifier.verify(sessionId) val result = Try {
val user = userManager.userExistsById(decoded.getClaim("id").asLong()) val decoded = verifier.verify(sessionId)
user.foreach(u => cache.put(sessionId, u)) val user = userManager.userExistsById(decoded.getClaim("id").asLong())
user user.foreach(u => cache.put(sessionId, u))
user
}
if (result.isSuccess) {
result.get
} else {
None
}
} }
} }

View File

@@ -22,6 +22,12 @@ class StubUserManager @Inject()(val config: Config) extends UserManager {
id = java.util.UUID.fromString("223e4567-e89b-12d3-a456-426614174000"), id = java.util.UUID.fromString("223e4567-e89b-12d3-a456-426614174000"),
name = "Leon", name = "Leon",
passwordHash = UserHash.hashPW("password123") passwordHash = UserHash.hashPW("password123")
),
"Jakob" -> User(
internalId = 2L,
id = java.util.UUID.fromString("323e4567-e89b-12d3-a456-426614174000"),
name = "Jakob",
passwordHash = UserHash.hashPW("password123")
) )
) )

View File

@@ -7,7 +7,7 @@ import model.users.User
import java.util.UUID import java.util.UUID
import java.util.concurrent.locks.{Lock, ReentrantLock} import java.util.concurrent.locks.{Lock, ReentrantLock}
class UserSession(user: User, val host: Boolean) extends PlayerSession { class UserSession(val user: User, val host: Boolean) extends PlayerSession {
var canInteract: Option[InteractionType] = None var canInteract: Option[InteractionType] = None
val lock: ReentrantLock = ReentrantLock() val lock: ReentrantLock = ReentrantLock()

View File

@@ -8,6 +8,10 @@ import scalafx.scene.image.Image
object WebUIUtils { object WebUIUtils {
def cardtoImage(card: Card): Html = { def cardtoImage(card: Card): Html = {
views.html.render.card.apply(f"images/cards/${cardtoString(card)}.png")(card.toString)
}
def cardtoString(card: Card) = {
val s = card.suit match { val s = card.suit match {
case Spades => "S" case Spades => "S"
case Hearts => "H" case Hearts => "H"
@@ -29,6 +33,7 @@ object WebUIUtils {
case Three => "3" case Three => "3"
case Two => "2" case Two => "2"
} }
views.html.render.card.apply(f"images/cards/$cv$s.png")(card.toString) f"$cv$s"
} }
} }

View File

@@ -0,0 +1,37 @@
@(user: Option[model.users.User], gamelobby: logic.game.GameLobby)
<main class="lobby-background vh-100" id="lobbybackground">
<div class="container d-flex flex-column" style="height: calc(100vh - 1rem);">
<div class="row">
<div class="col">
<div class="p-3 text-center fs-4">Winner: @gamelobby.getLogic.getWinner</div>
</div>
</div>
<div class="row justify-content-center align-items-center flex-grow-1">
@if((gamelobby.getUserSession(user.get.id).host)) {
<div class="col-12 text-center mb-5">
<div class="btn btn-success" onclick="backToLobby('@gamelobby.id')">Return to lobby</div>
</div>
} else {
<div class="col-12 text-center mt-3">
<div class="spinner-border mt-1" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
}
</div>
</div>
</main>
<script>
function waitForFunction(name, checkInterval = 100) {
return new Promise(resolve => {
const timer = setInterval(() => {
if (typeof window[name] === "function") {
clearInterval(timer);
resolve(window[name]);
}
}, checkInterval);
});
}
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
</script>

View File

@@ -1,50 +1,107 @@
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic) @import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.TrickUtil
@import de.knockoutwhist.utils.Implicits.*
@main("Ingame") { @(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
<div id="ingame" class="game-field game-field-background">
<h1>Knockout Whist</h1> <div class="lobby-background vh-100">
<div id="nextPlayers"> <main class="game-field-background vh-100 ingame-side-shadow">
<p>Next Player:</p> <div class="py-5 container-xxl">
<p>@logic.getPlayerQueue.get.duplicate().nextPlayer()</p>
</div> <div class="row ms-4 me-4">
<div id="firstCard"> <div class="col-4 mt-5 text-start">
<div id="trumpsuit"> <h4 class="fw-semibold mb-1">Current Player</h4>
<p>Trumpsuit: </p> <p class="fs-5 text-primary" id="current-player-name">@gamelobby.getLogic.getCurrentPlayer.get.name</p>
<p>@logic.getCurrentRound.get.trumpSuit</p> @if(!TrickUtil.isOver(gamelobby.getLogic.getCurrentMatch.get, gamelobby.getLogic.getPlayerQueue.get)) {
<h4 class="fw-semibold mb-1">Next Player</h4>
@for(nextplayer <- gamelobby.getLogic.getPlayerQueue.get.duplicate()) {
<p class="fs-5 text-primary" id="next-player-name">@nextplayer</p>
}
}
</div>
<div class="col-4 text-center">
<div class="score-table mt-5" id="score-table-body">
<h4 class="fw-bold mb-3 text-black">Tricks Won</h4>
<div class="d-flex justify-content-between score-header pb-1">
<div style="width: 50%">PLAYER</div>
<div style="width: 50%">TRICKS</div>
</div>
@for(player <- gamelobby.getLogic.getPlayerQueue.get.toList.sortBy { p =>
-(gamelobby.getLogic.getCurrentRound.get.tricklist.filter { trick => trick.winner.contains(p) }.size)
}) {
<div class="d-flex justify-content-between score-row pt-1">
<div style="width: 50%" class="text-truncate">@player.name</div>
<div style="width: 50%">
@(gamelobby.getLogic.getCurrentRound.get.tricklist.filter { trick => trick.winner.contains(player) }.size)
</div>
</div>
}
</div>
<div class="d-flex justify-content-center g-3 mb-5" id="trick-cards-container">
@for((cardplayed, player) <- gamelobby.getLogic.getCurrentTrick.get.cards) {
<div class="col-auto">
<div class="card text-center shadow-sm border-0 bg-transparent" style="width: 7rem; backdrop-filter: blur(4px);">
<div class="p-2">
@util.WebUIUtils.cardtoImage(cardplayed) width="100%"/>
</div>
<div class="card-body p-2 bg-transparent">
<small class="fw-semibold text-secondary">@player</small>
</div>
</div>
</div>
}
</div>
</div>
<div class="col-4 mt-5 text-end">
<h4 class="fw-semibold mb-1">Trumpsuit</h4>
<p class="fs-5 text-primary" id="trumpsuit">@gamelobby.getLogic.getCurrentRound.get.trumpSuit</p>
<h5 class="fw-semibold mt-4 mb-1">First Card</h5>
<div class="d-inline-block border rounded shadow-sm p-1 bg-light" id="first-card-container">
@if(gamelobby.getLogic.getCurrentTrick.get.firstCard.isDefined) {
@util.WebUIUtils.cardtoImage(gamelobby.getLogic.getCurrentTrick.get.firstCard.get) width="80px"/>
} else {
@views.html.render.card.apply("images/cards/1B.png")("Blank Card") width="80px"/>
}
</div>
</div>
</div>
<div class="row justify-content-center g-2 mt-4 bottom-div" style="backdrop-filter: blur(4px); margin-left: 0; margin-right: 0;">
<div class="row justify-content-center ingame-cards-slide @{
!gamelobby.logic.getCurrentPlayer.contains(player) ? "inactive" |: ""
}" id="card-slide">
@for(i <- player.currentHand().get.cards.indices) {
<div class="col-auto handcard" style="border-radius: 6px">
<div class="btn btn-outline-light p-0 border-0 shadow-none" data-card-id="@i" style="border-radius: 6px" onclick="handlePlayCard(this, '@gamelobby.id', '@player.isInDogLife')">
@util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/>
</div>
@if(player.isInDogLife) {
<div class="mt-2">
<button class="btn btn-danger" onclick="handleSkipDogLife(this, '@gamelobby.id')">Skip Dog Life</button>
</div>
}
</div>
}
</div>
</div>
</div> </div>
<div id="firstCardObject"> </main>
<p>First Card</p>
@if(logic.getCurrentTrick.get.firstCard.isDefined) {
@util.WebUIUtils.cardtoImage(logic.getCurrentTrick.get.firstCard.get)
} else {
@views.html.render.card.apply("images/cards/1B.png")("Blank Card")
}
</div>
</div>
<p>@logic.getCurrentPlayer.get has to play a card!</p>
@if(logic.getCurrentTrick.get.cards.nonEmpty) {
<p>Cards played</p>
} else {
<p id="invisible">Cards played</p>
}
<div id="cardsplayed">
@for((cardplayed, player) <- logic.getCurrentTrick.get.cards) {
<div id="playedcardplayer">
<p>@player</p>
@util.WebUIUtils.cardtoImage(cardplayed)
</div>
}
</div>
<p>Your cards</p>
<div id="playercards">
@for(card <- player.currentHand().get.cards) {
@util.WebUIUtils.cardtoImage(card)
}
</div>
</div> </div>
} <script>
function waitForFunction(name, checkInterval = 100) {
return new Promise(resolve => {
const timer = setInterval(() => {
if (typeof window[name] === "function") {
clearInterval(timer);
resolve(window[name]);
}
}, checkInterval);
});
}
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
</script>

View File

@@ -1,27 +1,71 @@
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic) @(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
@main("Selecting Trumpsuit...") {
<div id="selecttrumpsuit" class="game-field game-field-background"> <div id="selecttrumpsuit" class="game-field game-field-background">
@if(player.equals(logic.getCurrentMatch.get.roundlist.last.winner.get)) { <div class="ingame-stage blur-sides">
<h1>Knockout Whist</h1> <div class="container py-4">
<p>You (@player.toString) have won the last round! Select a trumpsuit for the next round!</p> <div class="row justify-content-center">
<p>Available trumpsuits are displayed below:</p> <div class="col-12">
<div id="playercards"> <div class="card shadow-sm">
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Spades)) <div class="card-header text-center">
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Clubs)) <h3 class="mb-0">Select Trump Suit</h3>
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Hearts)) </div>
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Diamonds)) <div class="card-body">
</div> @if(player.equals(gamelobby.logic.getCurrentMatch.get.roundlist.last.winner.get)) {
<p>Your cards</p> <div class="alert alert-info" role="alert" aria-live="polite">
You (@player.toString) won the last round. Choose the trump suit for the next round.
</div>
<div id="playercards"> <div class="row justify-content-center col-auto mb-5">
@for(card <- player.currentHand().get.cards) { <div class="col-auto handcard">
@util.WebUIUtils.cardtoImage(card) <div class="btn btn-outline-light p-0 border-0 shadow-none" data-trump="0" style="border-radius: 6px" onclick="handleTrumpSelection(this, '@gamelobby.id')">
} @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Spades)) width="120px" style="border-radius: 6px"/>
</div>
</div>
<div class="col-auto handcard">
<div class="btn btn-outline-light p-0 border-0 shadow-none" data-trump="1" style="border-radius: 6px" onclick="handleTrumpSelection(this, '@gamelobby.id')">
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Hearts)) width="120px" style="border-radius: 6px"/>
</div>
</div>
<div class="col-auto handcard">
<div class="btn btn-outline-light p-0 border-0 shadow-none" data-trump="2" style="border-radius: 6px" onclick="handleTrumpSelection(this, '@gamelobby.id')">
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Diamonds)) width="120px" style="border-radius: 6px"/>
</div>
</div>
<div class="col-auto handcard">
<div class="btn btn-outline-light p-0 border-0 shadow-none" data-trump="3" style="border-radius: 6px" onclick="handleTrumpSelection(this, '@gamelobby.id')">
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Clubs)) width="120px" style="border-radius: 6px"/>
</div>
</div>
</div>
<div class="row justify-content-center ingame-cards-slide" id="card-slide">
@for(i <- player.currentHand().get.cards.indices) {
<div class="col-auto" style="border-radius: 6px">
@util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/>
</div>
}
</div>
} else {
<div class="alert alert-warning" role="alert" aria-live="polite">
@gamelobby.logic.getCurrentMatch.get.roundlist.last.winner.get.name is choosing a trumpsuit. The new round will start once a suit is picked.
</div>
}
</div>
</div>
</div>
</div>
</div> </div>
} else { </div>
<h1>Knockout Whist</h1>
<p>@player.toString is choosing a trumpsuit. Starting new round when @player.toString picked a trumpsuit...</p>
}
</div> </div>
} <script>
function waitForFunction(name, checkInterval = 100) {
return new Promise(resolve => {
const timer = setInterval(() => {
if (typeof window[name] === "function") {
clearInterval(timer);
resolve(window[name]);
}
}, checkInterval);
});
}
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
</script>

View File

@@ -1,27 +1,117 @@
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic) @(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
@main("Tie") {
<div id="tie" class="game-field game-field-background"> <div id="tie" class="game-field game-field-background">
<h1>Knockout Whist</h1> <div class="ingame-stage blur-sides">
<p>The last Round was tied between <div class="container py-4">
@for(players <- logic.playerTieLogic.getTiedPlayers) { <div class="row justify-content-center">
@players <div class="col-12 col-md-10 col-lg-8">
} <div class="card shadow-sm">
</p> <div class="card-header text-center">
@if(player.equals(logic.playerTieLogic.currentTiePlayer())) { <h3 class="mb-0">Tie Break</h3>
<p>Pick a card between 1 and @logic.playerTieLogic.highestAllowedNumber()! The resulting card will be your card for the cut.</p> </div>
} else { <div class="card-body">
<p>@logic.playerTieLogic.currentTiePlayer() is currently picking his number for the cut.</p> <div class="mb-3">
<p>Currently picked Cards:</p> <p class="card-text">
<div id="cardsplayed"> The last round was tied between:
@for((player, card) <- logic.playerTieLogic.getSelectedCard) { <span class="ms-1">
<div id="playedcardplayer"> @for(players <- gamelobby.logic.playerTieLogic.getTiedPlayers) {
<p>@player</p> <span class="badge text-bg-secondary me-1">@players</span>
@util.WebUIUtils.cardtoImage(card) }
</div> </span>
} </p>
</div> </div>
}
@if(player.equals(gamelobby.logic.playerTieLogic.currentTiePlayer())) {
@defining(gamelobby.logic.playerTieLogic.highestAllowedNumber()) { maxNum =>
<div class="alert alert-info" role="alert" aria-live="polite">
Pick a number between 1 and @{maxNum + 1}. The resulting card will be your card for the cut.
</div>
<div class="row g-2 align-items-center">
<div class="col-auto">
<label for="tieNumber" class="col-form-label">Your number</label>
</div>
<div class="col-auto">
<input type="number" id="tieNumber" class="form-control" name="tie" min="1" max="@{maxNum + 1}" placeholder="1" required>
</div>
<div class="col-auto">
<button onclick="selectTie('@gamelobby.id')" class="btn btn-primary">Confirm</button>
</div>
</div>
<h6 class="mt-4 mb-3">Currently Picked Cards</h6>
<div id="cardsplayed" class="row g-3 justify-content-center">
@if(gamelobby.logic.playerTieLogic.getSelectedCard.nonEmpty) {
@for((player, card) <- gamelobby.logic.playerTieLogic.getSelectedCard) {
<div class="col-6">
<div class="card shadow-sm border-0 h-100 text-center">
<div class="card-body d-flex flex-column justify-content-between">
<p class="card-text fw-semibold mb-2 text-primary">@player</p>
<div class="card-img-top">
@util.WebUIUtils.cardtoImage(card)
</div>
</div>
</div>
</div>
}
} else {
<div class="col-12">
<div class="alert alert-info text-center" role="alert">
<i class="bi bi-info-circle me-2"></i>
No cards have been selected yet.
</div>
</div>
}
</div>
}
} else {
<div class="alert alert-warning" role="alert" aria-live="polite">
<strong>@gamelobby.logic.playerTieLogic.currentTiePlayer()</strong> is currently picking a number for the cut.
</div>
<h6 class="mt-4 mb-3">Currently Picked Cards</h6>
<div id="cardsplayed" class="row g-3 justify-content-center">
@if(gamelobby.logic.playerTieLogic.getSelectedCard.nonEmpty) {
@for((player, card) <- gamelobby.logic.playerTieLogic.getSelectedCard) {
<div class="col-6 col-sm-4 col-md-3 col-lg-2">
<div class="card shadow-sm border-0 h-100 text-center">
<div class="card-body d-flex flex-column justify-content-between">
<p class="card-text fw-semibold mb-2 text-primary">@player</p>
<div class="card-img-top">
@util.WebUIUtils.cardtoImage(card)
</div>
</div>
</div>
</div>
}
} else {
<div class="col-12">
<div class="alert alert-info text-center" role="alert">
<i class="bi bi-info-circle me-2"></i>
No cards have been selected yet.
</div>
</div>
}
</div>
}
</div>
</div>
</div>
</div>
</div>
</div>
</div> </div>
} <script>
function waitForFunction(name, checkInterval = 100) {
return new Promise(resolve => {
const timer = setInterval(() => {
if (typeof window[name] === "function") {
clearInterval(timer);
resolve(window[name]);
}
}, checkInterval);
});
}
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
</script>

View File

@@ -0,0 +1,81 @@
@(user: Option[model.users.User], gamelobby: logic.game.GameLobby)
<main class="lobby-background vh-100" id="lobbybackground">
<div class="container d-flex flex-column" style="height: calc(100vh - 1rem);">
<div class="row">
<div class="col">
<div class="p-3 fs-1 d-flex align-items-center">
<div class="text-center" style="flex-grow: 1;">
Lobby-Name: @gamelobby.name
</div>
<div class="btn btn-danger ms-auto" onclick="leaveGame('@gamelobby.id')">Exit</div>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="p-3 text-center fs-4" id="playerAmount">Playeramount: @gamelobby.getPlayers.size / @gamelobby.maxPlayers</div>
</div>
</div>
<div class="row justify-content-center align-items-center flex-grow-1">
@if((gamelobby.getUserSession(user.get.id).host)) {
<div id="players" class="justify-content-center align-items-center d-flex">
@for(playersession <- gamelobby.getPlayers.values) {
<div class="col-auto my-auto m-3">
<div class="card" style="width: 18rem;">
<img src="@routes.Assets.versioned("images/profile.png")" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
<div class="card-body">
@if(playersession.id == user.get.id) {
<h5 class="card-title">@playersession.name (You)</h5>
<a href="#" class="btn btn-danger disabled" aria-disabled="true" tabindex="-1">Remove</a>
} else {
<h5 class="card-title">@playersession.name</h5>
<div class="btn btn-danger" onclick="removePlayer('@gamelobby.id', '@playersession.id')">Remove</div>
}
</div>
</div>
</div>
}
</div>
<div class="col-12 text-center mb-5">
<div class="btn btn-success" onclick="startGame('@gamelobby.id')">Start Game</div>
</div>
} else {
<div id="players" class="justify-content-center align-items-center d-flex">
@for(playersession <- gamelobby.getPlayers.values) {
<div class="col-auto my-auto m-3"> <div class="card" style="width: 18rem;">
<img src="@routes.Assets.versioned("images/profile.png")" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
<div class="card-body">
@if(playersession.id == user.get.id) {
<h5 class="card-title">@playersession.name (You)</h5>
} else {
<h5 class="card-title">@playersession.name</h5>
}
</div>
</div>
</div>
}
</div>
<div class="col-12 text-center mt-3">
<p class="fs-4">Waiting for the host to start the game...</p>
<div class="spinner-border mt-1" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
}
</div>
</div>
</main>
<script>
function waitForFunction(name, checkInterval = 100) {
return new Promise(resolve => {
const timer = setInterval(() => {
if (typeof window[name] === "function") {
clearInterval(timer);
resolve(window[name]);
}
}, checkInterval);
});
}
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
</script>

View File

@@ -1,41 +1,42 @@
@() @()
<div class="login-box">
<div class="card login-card p-4">
<div class="card-body">
<h3 class="text-center mb-4 text-body">Login</h3>
<form onsubmit="login(); return false;">
<div class="mb-3">
<label for="username" class="form-label text-body">Username</label>
<input type="text" class="form-control text-body" id="username" name="username" placeholder="Enter Username" required>
</div>
@main("Login") { <div class="mb-3">
<div class="login-box"> <label for="password" class="form-label text-body">Password</label>
<div class="card login-card p-4"> <input type="password" class="form-control text-body" id="password" name="password" placeholder="Enter password" required>
<div class="card-body"> </div>
<h3 class="text-center mb-4">Login</h3>
<form action="@routes.UserController.login_Post()" method="post"> <div class="d-flex justify-content-between align-items-center mb-3">
<div class="mb-3"> <a href="#" class="text-decoration-none">Forgot password?</a>
<label for="username" class="form-label">Username</label> </div>
<input type="text" class="form-control" id="username" name="username" placeholder="Enter Username" required>
</div>
<div class="mb-3"> <div class="d-grid" >
<label for="password" class="form-label">Password</label> <button type="submit" class="btn btn-primary">Login</button>
<input type="password" class="form-control" id="password" name="password" placeholder="Enter password" required> </div>
</div> </form>
<div class="d-flex justify-content-between align-items-center mb-3"> <p class="text-center mt-3">
<a href="#" class="text-decoration-none">Forgot password?</a> Dont have an account?
</div> <a href="#" class="text-decoration-none">Sign up</a>
</p>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Login</button>
</div>
</form>
<p class="text-center mt-3">
Dont have an account?
<a href="#" class="text-decoration-none">Sign up</a>
</p>
</div>
</div> </div>
</div> </div>
<script src="@routes.Assets.versioned("/javascripts/particles.js")"></script> </div>
<div id="particles-js" style="background-color: rgb(182, 25, 36); <script src="@routes.Assets.versioned("/javascripts/particles.js")"></script>
background-size: cover; <script>
background-repeat: no-repeat; particlesJS.load('particles-js', 'assets/conf/particlesjs-config.json', function() {
background-position: 50% 50%;"></div> console.log('callback - particles.js config loaded');
} });
</script>
<div id="particles-js" style="background-color: rgb(11, 8, 8);
background-size: cover;
background-repeat: no-repeat;
background-position: 50% 50%;"></div>

View File

@@ -3,26 +3,28 @@
* handles the rendering of the page header and body tags. It takes * handles the rendering of the page header and body tags. It takes
* two arguments, a `String` for the title of the page and an `Html` * two arguments, a `String` for the title of the page and an `Html`
* object to insert into the body of the page. * object to insert into the body of the page.
*@ *@
@(title: String)(content: Html) @(title: String)(content: Html)
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@* Here's where we render the page title `String`. *@ @* Here's where we render the page title `String`. *@
<title>@title</title> <title>@title</title>
<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")"> <link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")"> <link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
</head> </head>
<body> <body class="d-flex flex-column min-vh-100" id="main-body">
@* And here's where we render the `Html` object containing @* And here's where we render the `Html` object containing
* the page content. *@ * the page content. *@
@content @content
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
</body> </body>
<script src="@routes.JavaScriptRoutingController.javascriptRoutes()" type="text/javascript"></script>
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</html> </html>

View File

@@ -0,0 +1,30 @@
@(user: Option[model.users.User])
@navbar(user)
<main class="lobby-background flex-grow-1">
<div class="w-25 mx-auto">
<div class="mt-3">
<label for="lobbyname" class="form-label">Lobby-Name</label>
<input type="text" class="form-control" id="lobbyname" name="lobbyname" placeholder="Lobby 1" required>
</div>
<div class="form-check form-switch mt-3">
<input class="form-check-input" type="checkbox" id="visibilityswitch" disabled>
<label class="form-check-label" for="visibilityswitch">public/private</label>
</div>
<div class="mt-3">
<label for="playeramount" class="form-label">Playeramount:</label>
<input type="range" class="form-range text-body" min="2" max="7" value="2" id="playeramount" name="playeramount">
<div class="d-flex justify-content-between">
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
</div>
</div>
<div class="mt-3 text-center">
<div class="btn btn-success" onclick="createGameJS()">Create Game</div>
</div>
</div>
</main>

View File

@@ -1,28 +1,30 @@
@(user: Option[model.users.User]) @(user: Option[model.users.User])
@main("Knockout Whist - Main Menu") { <nav class="navbar navbar-expand-lg bg-body-tertiary navbar-drop-shadow">
<nav class="navbar navbar-expand-lg bg-body-tertiary"> <div class="container d-flex justify-content-start">
<div class="container-fluid">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navBar" aria-controls="navBar" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navBar" aria-controls="navBar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navBar"> <div class="collapse navbar-collapse justify-content-center" id="navBar">
<a class="navbar-brand" href="@routes.MainMenuController.mainMenu()">KnockOutWhist</a> <a class="navbar-brand ms-auto" href="@routes.MainMenuController.mainMenu()">
<img src="@routes.Assets.versioned("images/logo.png")" alt="Logo" width="30" height="20" class="d-inline-block align-text-top">
KnockOutWhist
</a>
<div class="navbar-nav me-auto mb-2 mb-lg-0"> <div class="navbar-nav me-auto mb-2 mb-lg-0">
<ul class="navbar-nav mb-2 mb-lg-0"> <ul class="navbar-nav mb-2 mb-lg-0">
@if(user.isDefined) { @if(user.isDefined) {
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Create Game</a> <a class="nav-link active" aria-current="page" onclick="navSpa('0', 'Knockout Whist - Create Game'); return false;" href="@routes.MainMenuController.mainMenu()">Create Game</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link disabled" aria-disabled="true">Lobbies</a> <a class="nav-link disabled" aria-disabled="true">Lobbies</a>
</li> </li>
} }
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" href="@routes.MainMenuController.rules()">Rules</a> <a class="nav-link active" onclick="navSpa('1', 'Knockout Whist - Rules'); return false;" href="@routes.MainMenuController.rules()">Rules</a>
</li> </li>
</ul> </ul>
<form class="navbar-nav me-auto mb-2 mb-lg-0" method="post" action="@routes.MainMenuController.joinGame()"> <form class="navbar-nav me-auto mb-2 mb-lg-0" onsubmit="joinGame(); return false;">
<input class="form-control me-2" type="text" placeholder="Enter GameCode" name="gameId" aria-label="Join Game"/> <input class="form-control me-2" type="text" placeholder="Enter GameCode" id="gameId" aria-label="Join Game"/>
<button class="btn btn-outline-success" type="submit">Join</button> <button class="btn btn-outline-success" type="submit">Join</button>
</form> </form>
</div> </div>
@@ -53,4 +55,3 @@
</div> </div>
</div> </div>
</nav> </nav>
}

View File

@@ -1,63 +1,177 @@
@() @(user: Option[model.users.User])
@navbar(user)
@main("Rules") { <main class="lobby-background flex-grow-1">
<div id="rules" class="game-field game-field-background"> <div class="container my-4" style="max-width:980px;">
<table> <div class="card rules-card shadow-sm rounded-3 overflow-hidden">
<caption>Rules Overview and Equipment</caption> <div class="card-header text-center py-3 border-0">
<thead> <h3 class="mb-0 rules-title">Game Rules Overview</h3>
<tr> </div>
<th>Section</th>
<th>Details</th> <div class="card-body p-0">
</tr> <style>
</thead>
<tbody> </style>
<tr>
<td>Players</td> <div class="accordion rules-accordion" id="rulesAccordion">
<td>Two to seven players. The aim is to be the last player left in the game.</td> <div class="accordion-item">
</tr> <h2 class="accordion-header" id="headingPlayers">
<tr> <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapsePlayers" aria-expanded="true" aria-controls="collapsePlayers">
<td>Aim</td> Players
<td>To be the last player left in at the end of the game, with the object in each hand being to win a majority of tricks.</td> </button>
</tr> </h2>
<tr> <div id="collapsePlayers" class="accordion-collapse collapse show" aria-labelledby="headingPlayers" data-bs-parent="#rulesAccordion">
<td>Equipment</td> <div class="accordion-body">
<td>A standard 52-card pack is used.</td> Two to seven players. The aim is to be the last player left in the game.
</tr> </div>
<tr> </div>
<td>Card Ranks</td> </div>
<td>In each suit, cards rank from highest to lowest: A K Q J 10 9 8 7 6 5 4 3 2.</td>
</tr> <div class="accordion-item">
<tr> <h2 class="accordion-header" id="headingAim">
<td>Deal (First Hand)</td> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseAim" aria-expanded="false" aria-controls="collapseAim">
<td>The dealer deals seven cards to each player. One card is turned up to determine the trump suit for the round.</td> Aim
</tr> </button>
<tr> </h2>
<td>Deal (Subsequent Hands)</td> <div id="collapseAim" class="accordion-collapse collapse" aria-labelledby="headingAim" data-bs-parent="#rulesAccordion">
<td>The deal rotates clockwise. The player who took the most tricks in the previous hand selects the trump suit. If there's a tie for the highest number of tricks, players cut cards to decide who calls trumps. One fewer card is dealt in each successive hand until the final hand consists of one card each.</td> <div class="accordion-body">
</tr> To be the last player left at the end of the game, with the object in each hand being to win a majority of tricks.
<tr> </div>
<td>Play</td> </div>
<td>The player to the dealer's left (eldest hand) leads the first trick. Any card can be led. Other players must follow suit if possible. A player with no cards of the suit led may play any card.</td> </div>
</tr>
<tr> <div class="accordion-item">
<td>Winning a Trick</td> <h2 class="accordion-header" id="headingEquipment">
<td>The highest card of the suit led wins, unless a trump is played, in which case the highest trump wins. The winner of the trick leads the next.</td> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseEquipment" aria-expanded="false" aria-controls="collapseEquipment">
</tr> Equipment
<tr> </button>
<td>Leading Trumps</td> </h2>
<td>Some rules disallow leading trumps before the suit has been 'broken' (a trump has been played to the lead of another suit). Leading trumps is always permissible if a player holds only trumps.</td> <div id="collapseEquipment" class="accordion-collapse collapse" aria-labelledby="headingEquipment" data-bs-parent="#rulesAccordion">
</tr> <div class="accordion-body">
<tr> A standard 52-card pack is used.
<td>Knockout</td> </div>
<td>At the end of each hand, any player who took no tricks is knocked out and takes no further part in the game.</td> </div>
</tr> </div>
<tr>
<td>Winning the Game</td> <div class="accordion-item">
<td>The game is won when a player takes all the tricks in a round, as all other players are knocked out, leaving only one player remaining.</td> <h2 class="accordion-header" id="headingRanks">
</tr> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseRanks" aria-expanded="false" aria-controls="collapseRanks">
</tbody> Card Ranks
<td>Dog Life</td> </button>
<td>The first player who takes no tricks is awarded a "dog's life". In the next hand, that player is dealt one card and can decide which trick to play it to. Each time a trick is played the "dog" may either play the card or knock on the table and wait to play it later. If the dog wins a trick, the player to the left leads to the next and the dog re-enters the game properly in the next hand. If the dog fails, they are knocked out.</td> </h2>
</table> <div id="collapseRanks" class="accordion-collapse collapse" aria-labelledby="headingRanks" data-bs-parent="#rulesAccordion">
</div> <div class="accordion-body">
} In each suit, cards rank from highest to lowest: A K Q J 10 9 8 7 6 5 4 3 2.
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingDealFirst">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDealFirst" aria-expanded="false" aria-controls="collapseDealFirst">
Deal (First Hand)
</button>
</h2>
<div id="collapseDealFirst" class="accordion-collapse collapse" aria-labelledby="headingDealFirst" data-bs-parent="#rulesAccordion">
<div class="accordion-body">
The dealer deals seven cards to each player. One card is turned up to determine the trump suit for the round.
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingDealSubsequent">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDealSubsequent" aria-expanded="false" aria-controls="collapseDealSubsequent">
Deal (Subsequent Hands)
</button>
</h2>
<div id="collapseDealSubsequent" class="accordion-collapse collapse" aria-labelledby="headingDealSubsequent" data-bs-parent="#rulesAccordion">
<div class="accordion-body">
The deal rotates clockwise. The player who took the most tricks in the previous hand selects the trump suit. If there's a tie, players cut cards to decide who calls trumps. One fewer card is dealt in each successive hand until the final hand consists of one card each.
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingPlay">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsePlay" aria-expanded="false" aria-controls="collapsePlay">
Play
</button>
</h2>
<div id="collapsePlay" class="accordion-collapse collapse" aria-labelledby="headingPlay" data-bs-parent="#rulesAccordion">
<div class="accordion-body">
The player to the dealer's left (eldest hand) leads the first trick. Any card can be led. Other players must follow suit if possible. A player with no cards of the suit led may play any card.
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingWinningTrick">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseWinningTrick" aria-expanded="false" aria-controls="collapseWinningTrick">
Winning a Trick
</button>
</h2>
<div id="collapseWinningTrick" class="accordion-collapse collapse" aria-labelledby="headingWinningTrick" data-bs-parent="#rulesAccordion">
<div class="accordion-body">
The highest card of the suit led wins, unless a trump is played, in which case the highest trump wins. The winner of the trick leads the next.
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingLeadingTrumps">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseLeadingTrumps" aria-expanded="false" aria-controls="collapseLeadingTrumps">
Leading Trumps
</button>
</h2>
<div id="collapseLeadingTrumps" class="accordion-collapse collapse" aria-labelledby="headingLeadingTrumps" data-bs-parent="#rulesAccordion">
<div class="accordion-body">
Some rules disallow leading trumps before the suit has been 'broken' (a trump has been played to the lead of another suit). Leading trumps is always permissible if a player holds only trumps.
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingKnockout">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseKnockout" aria-expanded="false" aria-controls="collapseKnockout">
Knockout
</button>
</h2>
<div id="collapseKnockout" class="accordion-collapse collapse" aria-labelledby="headingKnockout" data-bs-parent="#rulesAccordion">
<div class="accordion-body">
At the end of each hand, any player who took no tricks is knocked out and takes no further part in the game.
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingWinningGame">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseWinningGame" aria-expanded="false" aria-controls="collapseWinningGame">
Winning the Game
</button>
</h2>
<div id="collapseWinningGame" class="accordion-collapse collapse" aria-labelledby="headingWinningGame" data-bs-parent="#rulesAccordion">
<div class="accordion-body">
The game is won when a player takes all the tricks in a round, as all other players are knocked out, leaving only one player remaining.
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingDogLife">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDogLife" aria-expanded="false" aria-controls="collapseDogLife">
Dog Life
</button>
</h2>
<div id="collapseDogLife" class="accordion-collapse collapse" aria-labelledby="headingDogLife" data-bs-parent="#rulesAccordion">
<div class="accordion-body">
The first player who takes no tricks is awarded a \"dog's life\". In the next hand, that player is dealt one card and can decide which trick to play it to. Each time a trick is played the dog may either play the card or knock on the table and wait to play it later. If the dog wins a trick, the player to the left leads the next and the dog re-enters the game properly in the next hand. If the dog fails, they are knocked out.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>

View File

@@ -1,2 +1,2 @@
@(src: String)(alt: String) @(src: String)(alt: String)
<img src="@routes.Assets.versioned(src)" alt="@alt"/> <img src="@routes.Assets.versioned(src)" alt="@alt"

View File

@@ -1,14 +1,15 @@
# https://www.playframework.com/documentation/latest/Configuration # https://www.playframework.com/documentation/latest/Configuration
play.filters.disabled += play.filters.csrf.CSRFFilter play.filters.disabled += play.filters.csrf.CSRFFilter
play.filters.disabled += play.filters.hosts.AllowedHostsFilter
play.http.secret.key="QCY?tAnfk?aZ?iwrNwnxIlR6CTf:G3gf:90Latabg@5241AB`R5W:1uDFN];Ik@n"
play.http.secret.key=${?APPLICATION_SECRET}
auth { auth {
issuer = "knockoutwhistweb" issuer = "knockoutwhistweb"
audience = "ui" audience = "ui"
# ${?PUBLIC_KEY_FILE} privateKeyFile = ${?PRIVATE_KEY_FILE}
privateKeyFile = "D:\\Workspaces\\Gitops\\rsa512-private.pem" privateKeyPem = ${?PRIVATE_KEY_PEM}
privateKeyPem = ${?PUBLIC_KEY_PEM} publicKeyFile = ${?PUBLIC_KEY_FILE}
#${?PUBLIC_KEY_FILE}
publicKeyFile = "D:\\Workspaces\\Gitops\\rsa512-public.pem"
publicKeyPem = ${?PUBLIC_KEY_PEM} publicKeyPem = ${?PUBLIC_KEY_PEM}
} }

View File

@@ -3,27 +3,39 @@
# https://www.playframework.com/documentation/latest/ScalaRouting # https://www.playframework.com/documentation/latest/ScalaRouting
# ~~~~ # ~~~~
# For the javascript routing
GET /assets/js/routes controllers.JavaScriptRoutingController.javascriptRoutes()
# Primary routes # Primary routes
GET / controllers.MainMenuController.index() GET / controllers.MainMenuController.index()
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
# Main menu routes # Main menu routes
GET /mainmenu controllers.MainMenuController.mainMenu() GET /mainmenu controllers.MainMenuController.mainMenu()
GET /rules controllers.MainMenuController.rules() GET /rules controllers.MainMenuController.rules()
GET /navSPA/:pType controllers.MainMenuController.navSPA(pType)
POST /createGame controllers.MainMenuController.createGame() POST /createGame controllers.MainMenuController.createGame()
POST /joinGame controllers.MainMenuController.joinGame() POST /joinGame controllers.MainMenuController.joinGame()
# User authentication routes # User authentication routes
GET /login controllers.UserController.login() GET /login controllers.UserController.login()
POST /login controllers.UserController.login_Post() POST /login controllers.UserController.login_Post()
GET /logout controllers.UserController.logout() GET /logout controllers.UserController.logout()
# In-game routes # In-game routes
GET /game/:id controllers.IngameController.game(id: String) GET /game/:id controllers.IngameController.game(id: String)
GET /game/:id/join controllers.IngameController.joinGame(id: String) POST /game/:id/start controllers.IngameController.startGame(id: String)
POST /game/:id/start controllers.IngameController.startGame(id: String) POST /game/:id/kickPlayer/:playerToKick controllers.IngameController.kickPlayer(id: String, playerToKick: String)
POST /game/:id/playCard controllers.IngameController.playCard(id: String) POST /game/:id/trump controllers.IngameController.playTrump(id: String)
POST /game/:id/tie controllers.IngameController.playTie(id: String)
GET /game/:id/leaveGame controllers.IngameController.leaveGame(id: String)
POST /game/:id/playCard controllers.IngameController.playCard(id: String)
POST /game/:id/dogPlayCard controllers.IngameController.playDogCard(id: String)
POST /game/:id/returnToLobby controllers.IngameController.returnToLobby(id: String)
# Polling
GET /polling/:gameId controllers.PollingController.polling(gameId: String)

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1,3 +1,667 @@
particlesJS.load('particles-js', 'assets/conf/particlesjs-config.json', function() { /*!
console.log('callback - particles.js config loaded'); * Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
}); * Copyright 2011-2025 The Bootstrap Authors
* Licensed under the Creative Commons Attribution 3.0 Unported License.
*/
(() => {
'use strict'
const getStoredTheme = () => localStorage.getItem('theme')
const setStoredTheme = theme => localStorage.setItem('theme', theme)
const getPreferredTheme = () => {
const storedTheme = getStoredTheme()
if (storedTheme) {
return storedTheme
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
const setTheme = theme => {
if (theme === 'auto') {
document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'))
} else {
document.documentElement.setAttribute('data-bs-theme', theme)
}
}
setTheme(getPreferredTheme())
const showActiveTheme = (theme, focus = false) => {
const themeSwitcher = document.querySelector('#bd-theme')
if (!themeSwitcher) {
return
}
const themeSwitcherText = document.querySelector('#bd-theme-text')
const activeThemeIcon = document.querySelector('.theme-icon-active use')
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href')
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
element.classList.remove('active')
element.setAttribute('aria-pressed', 'false')
})
btnToActive.classList.add('active')
btnToActive.setAttribute('aria-pressed', 'true')
activeThemeIcon.setAttribute('href', svgOfActiveBtn)
const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`
themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)
if (focus) {
themeSwitcher.focus()
}
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const storedTheme = getStoredTheme()
if (storedTheme !== 'light' && storedTheme !== 'dark') {
setTheme(getPreferredTheme())
}
})
window.addEventListener('DOMContentLoaded', () => {
showActiveTheme(getPreferredTheme())
document.querySelectorAll('[data-bs-theme-value]')
.forEach(toggle => {
toggle.addEventListener('click', () => {
const theme = toggle.getAttribute('data-bs-theme-value')
setStoredTheme(theme)
setTheme(theme)
showActiveTheme(theme, true)
})
})
})
})()
let polling = false;
function pollForUpdates(gameId) {
if (polling) {
console.log("[DEBUG] Polling already in progress. Skipping this cycle.");
return;
}
polling = true;
console.log(`[DEBUG] Starting poll cycle for Game ID: ${gameId} at ${new Date().toISOString()}`);
if (!gameId) {
console.error("[DEBUG] Game ID is missing. Stopping poll.");
return;
}
const $handElement = $('#card-slide');
const $lobbyElement = $('#lobbybackground');
const $mainmenuElement = $('#main-menu-screen')
const $mainbody = $('#main-body')
if (!$handElement.length && !$lobbyElement.length && !$mainmenuElement.length && !$mainbody.length) {
setTimeout(() => { polling = false; pollForUpdates(gameId) }, 1000);
return;
}
const route = jsRoutes.controllers.PollingController.polling(gameId);
$.ajax({
url: route.url,
type: 'GET',
dataType: 'json',
success: (data => {
if (!data) {
console.log("[DEBUG] Received 204 No Content (Timeout). Restarting poll.");
return;
}
if (data.status === "cardPlayed" && data.handData) {
console.log("Event received: Card played. Redrawing hand.");
const newHand = data.handData;
let newHandHTML = '';
$handElement.empty();
if(data.animation) {
$handElement.addClass('ingame-cards-slide');
} else {
$handElement.removeClass('ingame-cards-slide');
}
const dog = data.dog;
newHand.forEach((cardId, index) => {
const cardHtml = `
<div class="col-auto handcard" style="border-radius: 6px">
<div class="btn btn-outline-light p-0 border-0 shadow-none"
data-card-id="${index}"
style="border-radius: 6px"
onclick="handlePlayCard(this, '${gameId}', '${dog}')">
<img src="/assets/images/cards/${cardId}.png" width="120px" style="border-radius: 6px"/>
</div>
</div>
`;
newHandHTML += cardHtml;
});
if (dog) {
newHandHTML += `
<div class="mt-2">
<button class="btn btn-danger" onclick="handleSkipDogLife(this, '${gameId}')">Skip Dog Life</button>
</div>
`;
}
$handElement.html(newHandHTML);
if (data.yourTurn) {
$handElement.removeClass('inactive');
} else {
$handElement.addClass('inactive');
}
$('#current-player-name').text(data.currentPlayerName)
if (data.nextPlayer) {
$('#next-player-name').text(data.nextPlayer);
} else if (nextPlayerElement) {
$('#next-player-name').text("");
} else {
console.warn("[DEBUG] 'current-player-name' element missing in DOM");
}
$('#trump-suit').text(data.trumpSuit);
if ($('#trick-cards-container').length) {
let trickHTML = '';
data.trickCards.forEach(trickCard => {
trickHTML += `
<div class="col-auto">
<div class="card text-center shadow-sm border-0 bg-transparent" style="width: 7rem; backdrop-filter: blur(4px);">
<div class="p-2">
<img src="/assets/images/cards/${trickCard.cardId}.png" width="100%"/>
</div>
<div class="card-body p-2 bg-transparent">
<small class="fw-semibold text-secondary">${trickCard.player}</small>
</div>
</div>
</div>
`;
});
$('#trick-cards-container').html(trickHTML);
}
if ($('#score-table-body').length && data.scoreTable) {
let scoreHTML = '';
scoreHTML += `<h4 class="fw-bold mb-3 text-black">Tricks Won</h4>
<div class="d-flex justify-content-between score-header pb-1">
<div style="width: 50%">PLAYER</div>
<div style="width: 50%">TRICKS</div>
</div>`
data.scoreTable.forEach(score => {
scoreHTML += `
<div class="d-flex justify-content-between score-row pt-1">
<div style="width: 50%" class="text-truncate">${score.name}</div>
<div style="width: 50%">${score.tricks}</div>
</div>
`;
});
$('#score-table-body').html(scoreHTML);
}
const cardId = data.firstCardId;
if ($('#first-card-container').length) {
let imageSrc = '';
let altText = 'First Card';
if (cardId === "BLANK") {
imageSrc = "/assets/images/cards/1B.png";
altText = "Blank Card";
} else {
imageSrc = `/assets/images/cards/${cardId}.png`;
}
const newImageHTML = `
<img src="${imageSrc}" alt="${altText}" width="80px" style="border-radius: 6px"/>
`;
$('#first-card-container').html(newImageHTML);
}
} else if (data.status === "reloadEvent") {
console.log("[DEBUG] Reload event received. Redirecting...");
exchangeBody(data.content, "Knockout Whist - Ingame", data.redirectUrl);
}
else if (data.status === "lobbyUpdate") {
console.log("[DEBUG] Entering 'lobbyUpdate' logic.");
let newHtml = ''
if (data.host) {
data.users.forEach(user => {
const inner = user.self ? `<h5 class="card-title">${user.name} (You)</h5>
<a href="#" class="btn btn-danger disabled" aria-disabled="true" tabindex="-1">Remove</a>`
: ` <h5 class="card-title">${user.name}</h5>
<div class="btn btn-danger" onclick="removePlayer('${gameId}', '${user.id}')">Remove</div>`
newHtml += `<div class="col-auto my-auto m-3">
<div class="card" style="width: 18rem;">
<img src="/assets/images/profile.png" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
<div class="card-body">
${inner}
</div>
</div>
</div>`
})
} else {
data.users.forEach(user => {
const inner = user.self ? `<h5 class="card-title">${user.name} (You)</h5>` : ` <h5 class="card-title">${user.name}</h5>`
newHtml += `<div class="col-auto my-auto m-3">
<div class="card" style="width: 18rem;">
<img src="/assets/images/profile.png" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
<div class="card-body">
${inner}
</div>
</div>
</div>`
})
}
$("#players").html(newHtml);
$('#playerAmount').text(`Playeramount: ${data.users.length} / ${data.maxPlayers}`);
} else {
console.warn(`[DEBUG] Received unknown status: ${data.status}`);
}
}),
error: ((jqXHR, textStatus, errorThrown) => {
if (jqXHR.status >= 400) {
console.error(`Server error: ${jqXHR.status}, ${errorThrown}`);
}
else {
console.error(`Something unexpected happened while polling. ${jqXHR.status}, ${errorThrown}`)
}
}),
complete: (() => {
setTimeout(() => { polling = false; pollForUpdates(gameId) }, 200);
})
})
}
function createGameJS() {
let lobbyName = $('#lobbyname').val();
if ($.trim(lobbyName) === "") {
lobbyName = "DefaultLobby"
}
const jsonObj = {
lobbyname: lobbyName,
playeramount: $("#playeramount").val()
}
sendGameCreationRequest(jsonObj);
}
function backToLobby(gameId) {
const route = jsRoutes.controllers.IngameController.returnToLobby(gameId);
$.ajax({
url: route.url,
type: route.type,
contentType: 'application/json',
dataType: 'json',
error: ((jqXHR) => {
const errorData = JSON.parse(jqXHR.responseText);
if (errorData && errorData.errorMessage) {
alert(`${errorData.errorMessage}`);
} else {
alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
}
})
})
}
function sendGameCreationRequest(dataObject) {
const route = jsRoutes.controllers.MainMenuController.createGame();
$.ajax({
url: route.url,
type: route.type,
contentType: 'application/json',
data: JSON.stringify(dataObject),
dataType: 'json',
success: (data => {
if (data.status === 'success') {
exchangeBody(data.content, "Knockout Whist - Lobby", data.redirectUrl);
}
}),
error: ((jqXHR) => {
const errorData = JSON.parse(jqXHR.responseText);
if (errorData && errorData.errorMessage) {
alert(`${errorData.errorMessage}`);
} else {
alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
}
})
})
}
function exchangeBody(content, title = "Knockout Whist", url = null) {
if (url) {
window.history.pushState({}, title, url);
}
$("#main-body").html(content);
document.title = title;
}
function startGame(gameId) {
sendGameStartRequest(gameId)
}
function sendGameStartRequest(gameId) {
const route = jsRoutes.controllers.IngameController.startGame(gameId);
$.ajax({
url: route.url,
type: route.type,
dataType: 'json',
success: (data => {
if (data.status === 'success') {
window.location.href = data.redirectUrl;
}
}),
error: ((jqXHR) => {
const errorData = JSON.parse(jqXHR.responseText);
if (errorData && errorData.errorMessage) {
alert(`${errorData.errorMessage}`);
} else {
alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
}
})
})
}
function removePlayer(gameid, playersessionId) {
sendRemovePlayerRequest(gameid, playersessionId)
}
function sendRemovePlayerRequest(gameId, playersessionId) {
const route = jsRoutes.controllers.IngameController.kickPlayer(gameId, playersessionId);
$.ajax({
url: route.url,
type: route.type,
contentType: 'application/json',
dataType: 'json',
success: (data => {
if (data.status === 'success') {
window.location.href = data.redirectUrl;
}
}),
error: ((jqXHR) => {
const errorData = JSON.parse(jqXHR.responseText);
if (errorData && errorData.errorMessage) {
alert(`${errorData.errorMessage}`);
} else {
alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
}
})
})
}
function login() {
const username = $('#username').val();
const password = $('#password').val();
const jsonObj = {
username: username,
password: password
};
const route = jsRoutes.controllers.UserController.login_Post();
$.ajax({
url: route.url,
type: route.type,
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify(jsonObj),
success: (data => {
if (data.status === 'success') {
exchangeBody(data.content, 'Knockout Whist - Create Game', data.redirectUrl);
return
}
alert('Login failed. Please check your credentials and try again.');
}),
error: ((jqXHR) => {
const errorData = JSON.parse(jqXHR.responseText);
if (errorData && errorData.errorMessage) {
alert(`${errorData.errorMessage}`);
} else {
alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
}
})
});
}
function joinGame() {
const gameId = $('#gameId').val();
const jsonObj = {
gameId: gameId
};
const route = jsRoutes.controllers.MainMenuController.joinGame();
$.ajax({
url: route.url,
type: route.type,
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify(jsonObj),
success: (data => {
if (data.status === 'success') {
exchangeBody(data.content, "Knockout Whist - Lobby", data.redirectUrl);
return
}
alert('Could not join the game. Please check the Game ID and try again.');
}),
error: ((jqXHR) => {
const errorData = JSON.parse(jqXHR.responseText);
if (errorData && errorData.errorMessage) {
alert(`${errorData.errorMessage}`);
} else {
alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
}
})
});
return false
}
function navSpa(page, title) {
const route = jsRoutes.controllers.MainMenuController.navSPA(page);
$.ajax({
url: route.url,
type: route.type,
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify(jsonObj),
success: (data => {
if (data.status === 'success') {
exchangeBody(data.content, title, data.redirectUrl);
return
}
alert('Could not join the game. Please check the Game ID and try again.');
}),
error: ((jqXHR) => {
const errorData = JSON.parse(jqXHR.responseText);
if (errorData && errorData.errorMessage) {
alert(`${errorData.errorMessage}`);
} else {
alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
}
})
});
return false
}
function selectTie(gameId) {
const route = jsRoutes.controllers.IngameController.playTie(gameId);
const jsonObj = {
tie: $('#tieNumber').val()
};
$.ajax({
url: route.url,
type: route.type,
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify(jsonObj),
error: (jqXHR => {
let error;
try {
error = JSON.parse(jqXHR.responseText);
} catch (e) {
console.error("Failed to parse error response:", e);
}
if (error?.errorMessage) {
alert(`${error.errorMessage}`);
} else {
alert('An unexpected error occurred. Please try again.');
}
})
})
}
function leaveGame(gameId) {
sendLeavePlayerRequest(gameId)
}
function sendLeavePlayerRequest(gameId) {
const route = jsRoutes.controllers.IngameController.leaveGame(gameId);
$.ajax({
url: route.url,
type: route.type,
dataType: 'json',
error: ((jqXHR) => {
const errorData = JSON.parse(jqXHR.responseText);
if (errorData && errorData.errorMessage) {
alert(`${errorData.errorMessage}`);
} else {
alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
}
})
})
}
function handleTrumpSelection(cardobject, gameId) {
const trumpId = cardobject.dataset.trump;
const jsonObj = {
trump: trumpId
}
const route = jsRoutes.controllers.IngameController.playTrump(gameId);
$.ajax({
url: route.url,
type: route.type,
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify(jsonObj),
error: (jqXHR => {
let error;
try {
error = JSON.parse(jqXHR.responseText);
} catch (e) {
console.error("Failed to parse error response:", e);
}
if (error?.errorMessage) {
alert(`${error.errorMessage}`);
} else {
alert('An unexpected error occurred. Please try again.');
}
})
})
}
function handlePlayCard(cardobject, gameId, dog = false) {
const cardId = cardobject.dataset.cardId;
const jsonObj = {
cardID: cardId
}
sendPlayCardRequest(jsonObj, gameId, cardobject, dog)
}
function handleSkipDogLife(cardobject, gameId) {
const wiggleKeyframes = [
{ transform: 'translateX(0)' },
{ transform: 'translateX(-5px)' },
{ transform: 'translateX(5px)' },
{ transform: 'translateX(-5px)' },
{ transform: 'translateX(0)' }
];
const wiggleTiming = {
duration: 400,
iterations: 1,
easing: 'ease-in-out',
fill: 'forwards'
};
const route = jsRoutes.controllers.IngameController.playDogCard(gameId);
$.ajax({
url: route.url,
type: route.type,
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify({
cardID: 'skip'
}),
error: (jqXHR => {
let error;
try {
error = JSON.parse(jqXHR.responseText);
} catch (e) {
console.error("Failed to parse error response:", e);
}
if (error?.errorMessage.includes("You can't skip this round!")) {
cardobject.parentElement.animate(wiggleKeyframes, wiggleTiming);
} else if (error?.errorMessage) {
alert(`${error.errorMessage}`);
} else {
alert('An unexpected error occurred. Please try again.');
}
})
})
}
function sendPlayCardRequest(jsonObj, gameId, cardobject, dog) {
const wiggleKeyframes = [
{ transform: 'translateX(0)' },
{ transform: 'translateX(-5px)' },
{ transform: 'translateX(5px)' },
{ transform: 'translateX(-5px)' },
{ transform: 'translateX(0)' }
];
const wiggleTiming = {
duration: 400,
iterations: 1,
easing: 'ease-in-out',
fill: 'forwards'
};
const route = dog === "true" ? jsRoutes.controllers.IngameController.playDogCard(gameId) : jsRoutes.controllers.IngameController.playCard(gameId);
$.ajax({
url: route.url,
type: route.type,
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify(jsonObj),
error: (jqXHR => {
try {
error = JSON.parse(jqXHR.responseText);
} catch (e) {
console.error("Failed to parse error response:", e);
}
if (error?.errorMessage.includes("You can't play this card!")) {
cardobject.parentElement.animate(wiggleKeyframes, wiggleTiming);
} else if (error?.errorMessage) {
alert(`${error.errorMessage}`);
} else {
alert('An unexpected error occurred. Please try again.');
}
})
})
}

View File

@@ -6,3 +6,5 @@ addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.0")
addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.1") addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.1")
addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.1.2") addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.1.2")
addSbtPlugin("nl.gn0s1s" % "sbt-dotenv" % "3.2.0")

View File

@@ -1,3 +1,3 @@
MAJOR=2 MAJOR=3
MINOR=0 MINOR=0
PATCH=0 PATCH=0