Compare commits
44 Commits
archive/ma
...
5c8fd8510e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c8fd8510e | ||
|
|
6d958cdd9e | ||
|
|
b508d2f428 | ||
|
|
33989efedc | ||
|
|
c948e5e800 | ||
|
|
a71752df2f | ||
|
|
b847d3c054 | ||
| c7dd72ecc2 | |||
| 42a5adbae0 | |||
|
|
ae9a8f2af9 | ||
| 7adc8b8645 | |||
|
|
146348470f | ||
| 5e503cbc36 | |||
|
|
126e2030ae | ||
| 54e3215127 | |||
|
|
72d2845772 | ||
| 64a7a63ab3 | |||
|
|
51c36348b9 | ||
| 2e54880302 | |||
|
|
266406fe7c | ||
| 5c6d3ac436 | |||
|
|
674619dadc | ||
|
|
ef539d3eea | ||
|
|
e09463b138 | ||
| 4d6ea54771 | |||
| aa83082d09 | |||
| 42e7597e26 | |||
|
|
6befd9c722 | ||
| 51d9c0b5f6 | |||
| de565b52dc | |||
| 051e7406e3 | |||
| 44c88c8f60 | |||
| 96c38466d2 | |||
| 32d4f9c6ce | |||
|
|
5c1a5a661b | ||
|
|
7b97986df9 | ||
| 7879d1ab6e | |||
|
|
1564956107 | ||
| 2e10059a67 | |||
|
|
b8f44c07d4 | ||
| 91d7f6ca00 | |||
|
|
df90abed80 | ||
| 0d2c6f77c8 | |||
| afde6c02da |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.env
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -137,3 +137,5 @@ target
|
|||||||
/knockoutwhist/
|
/knockoutwhist/
|
||||||
/knockoutwhistweb/.g8/
|
/knockoutwhistweb/.g8/
|
||||||
/knockoutwhistweb/.bsp/
|
/knockoutwhistweb/.bsp/
|
||||||
|
/currentSnapshot.json
|
||||||
|
.env
|
||||||
2
.sbtopts
Normal file
2
.sbtopts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-J--add-opens=java.base/java.util=ALL-UNNAMED
|
||||||
|
-J--add-opens=java.base/java.lang=ALL-UNNAMED
|
||||||
113
CHANGELOG.md
Normal file
113
CHANGELOG.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
## (2025-11-07)
|
||||||
|
|
||||||
|
### ⚠ 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))
|
||||||
|
* **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))
|
||||||
|
* **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:** 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
|
||||||
|
|
||||||
|
* **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))
|
||||||
|
* 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))
|
||||||
|
* 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:** 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))
|
||||||
|
* 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
|
||||||
|
|
||||||
|
* 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 bump ([7879d1a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7879d1ab6ee8f227c19b69b467a28dd7b479ff73))
|
||||||
|
## (2025-11-07)
|
||||||
|
|
||||||
|
### ⚠ 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))
|
||||||
|
* **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))
|
||||||
|
* **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:** 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
|
||||||
|
|
||||||
|
* **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))
|
||||||
|
* 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))
|
||||||
|
* 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:** 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))
|
||||||
|
* 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
|
||||||
|
|
||||||
|
* 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 bump ([7879d1a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7879d1ab6ee8f227c19b69b467a28dd7b479ff73))
|
||||||
|
|
||||||
|
## (2025-11-07)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 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))
|
||||||
|
## (2025-11-07)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* changelog syntax ([2e54880](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e548803020c99f62644283fcf3570048261173a))
|
||||||
|
## (2025-11-07)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* removed trailing ([64a7a63](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/64a7a63ab3dff59e66f62328e3b5865bb177fcde))
|
||||||
|
## (2025-11-07)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 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))
|
||||||
41
Dockerfile
Normal file
41
Dockerfile
Normal 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"]
|
||||||
9
README.md
Normal file
9
README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## User Password Protection
|
||||||
|
|
||||||
|
All the User Passwords are encrypted using Argon2.
|
||||||
16
bruno/KnockOutWhist/Game/Create Game.bru
Normal file
16
bruno/KnockOutWhist/Game/Create Game.bru
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
meta {
|
||||||
|
name: Create Game
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{host}}/createGame
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
20
bruno/KnockOutWhist/Game/Get Game.bru
Normal file
20
bruno/KnockOutWhist/Game/Get Game.bru
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
meta {
|
||||||
|
name: Get Game
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{host}}/game/:id
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
params:path {
|
||||||
|
id: BZvtJ3
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
20
bruno/KnockOutWhist/Game/Start Game.bru
Normal file
20
bruno/KnockOutWhist/Game/Start Game.bru
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
meta {
|
||||||
|
name: Start Game
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{host}}/game/:id/start
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
params:path {
|
||||||
|
id: nR1o3n
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
8
bruno/KnockOutWhist/Game/folder.bru
Normal file
8
bruno/KnockOutWhist/Game/folder.bru
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
meta {
|
||||||
|
name: Game
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
auth {
|
||||||
|
mode: inherit
|
||||||
|
}
|
||||||
26
bruno/KnockOutWhist/Login.bru
Normal file
26
bruno/KnockOutWhist/Login.bru
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
meta {
|
||||||
|
name: Login
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{host}}/login
|
||||||
|
body: formUrlEncoded
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body:form-urlencoded {
|
||||||
|
username: Janis
|
||||||
|
password: password123
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
username: Janis
|
||||||
|
password: password123
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
9
bruno/KnockOutWhist/bruno.json
Normal file
9
bruno/KnockOutWhist/bruno.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"name": "KnockOutWhist",
|
||||||
|
"type": "collection",
|
||||||
|
"ignore": [
|
||||||
|
"node_modules",
|
||||||
|
".git"
|
||||||
|
]
|
||||||
|
}
|
||||||
0
bruno/KnockOutWhist/collection.bru
Normal file
0
bruno/KnockOutWhist/collection.bru
Normal file
3
bruno/KnockOutWhist/environments/Local.bru
Normal file
3
bruno/KnockOutWhist/environments/Local.bru
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
vars {
|
||||||
|
host: http://localhost:9000
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -37,7 +36,11 @@ lazy val knockoutwhistweb = project.in(file("knockoutwhistweb"))
|
|||||||
.dependsOn(knockoutwhist % "compile->compile;test->test")
|
.dependsOn(knockoutwhist % "compile->compile;test->test")
|
||||||
.settings(
|
.settings(
|
||||||
commonSettings,
|
commonSettings,
|
||||||
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 += "com.auth0" % "java-jwt" % "4.3.0",
|
||||||
|
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("."))
|
||||||
|
|||||||
Submodule knockoutwhist updated: fbc0ea2277...b9a7b0a2af
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
35
knockoutwhistweb/app/assets/stylesheets/login.less
Normal file
35
knockoutwhistweb/app/assets/stylesheets/login.less
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
.login-box {
|
||||||
|
position: fixed; /* changed from absolute to fixed so it centers relative to the viewport */
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%); /* center exactly */
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px; /* keeps box from stretching too wide */
|
||||||
|
padding: 1rem;
|
||||||
|
z-index: 2; /* above particles */
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||||
|
position: relative;
|
||||||
|
z-index: 3; /* ensure card sits above the particles */
|
||||||
|
}
|
||||||
|
|
||||||
|
#particles-js {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 0; /* behind content */
|
||||||
|
pointer-events: none; /* allow clicks through particles */
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
@@ -1,21 +1,84 @@
|
|||||||
@import "light-mode.less";
|
@import "light-mode.less";
|
||||||
@import "dark-mode.less";
|
@import "dark-mode.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); }
|
||||||
}
|
}
|
||||||
body {
|
|
||||||
|
.game-field-background {
|
||||||
background-image: @background-image;
|
background-image: @background-image;
|
||||||
background-size: 100vw 100vh;
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.lobby-background {
|
||||||
|
background-color: @background-color;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
.navbar-header{
|
||||||
height: 100vh;
|
text-align:center;
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar-toggle {
|
||||||
|
float: none;
|
||||||
|
margin-right:0;
|
||||||
|
}
|
||||||
|
.handcard :hover {
|
||||||
|
box-shadow: 3px 3px 3px @highlightcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
#sessions {
|
#sessions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -32,7 +95,8 @@ html, body {
|
|||||||
animation-fill-mode: backwards;
|
animation-fill-mode: backwards;
|
||||||
animation-delay: 1s;
|
animation-delay: 1s;
|
||||||
}
|
}
|
||||||
#sessions a, h1, p {
|
|
||||||
|
#sessions a, #sessions h1, #sessions p {
|
||||||
color: @color;
|
color: @color;
|
||||||
font-size: 40px;
|
font-size: 40px;
|
||||||
font-family: Arial, serif;
|
font-family: Arial, serif;
|
||||||
@@ -44,6 +108,11 @@ html, body {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
#ingame a, #ingame h1, #ingame p {
|
||||||
|
color: @color;
|
||||||
|
font-size: 40px;
|
||||||
|
font-family: Arial, serif;
|
||||||
|
}
|
||||||
#playercards {
|
#playercards {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -61,8 +130,22 @@ html, body {
|
|||||||
&:nth-child(7) { animation-delay: 3.5s; }
|
&:nth-child(7) { animation-delay: 3.5s; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#card-slide {
|
||||||
|
div {
|
||||||
|
animation: slideIn 0.5s ease-out forwards;
|
||||||
|
animation-fill-mode: backwards;
|
||||||
|
&:nth-child(1) { animation-delay: 0.5s; }
|
||||||
|
&:nth-child(2) { animation-delay: 1s; }
|
||||||
|
&:nth-child(3) { animation-delay: 1.5s; }
|
||||||
|
&:nth-child(4) { animation-delay: 2s; }
|
||||||
|
&:nth-child(5) { animation-delay: 2.5s; }
|
||||||
|
&:nth-child(6) { animation-delay: 3s; }
|
||||||
|
&:nth-child(7) { animation-delay: 3.5s; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
#cardsplayed {
|
#cardsplayed {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -102,11 +185,6 @@ html, body {
|
|||||||
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;
|
||||||
@@ -131,3 +209,19 @@ html, body {
|
|||||||
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;
|
||||||
|
}
|
||||||
37
knockoutwhistweb/app/auth/AuthAction.scala
Normal file
37
knockoutwhistweb/app/auth/AuthAction.scala
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import controllers.routes
|
||||||
|
import logic.user.SessionManager
|
||||||
|
import model.users.User
|
||||||
|
import play.api.mvc.*
|
||||||
|
|
||||||
|
import javax.inject.Inject
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
class AuthenticatedRequest[A](val user: User, request: Request[A]) extends WrappedRequest[A](request)
|
||||||
|
|
||||||
|
class AuthAction @Inject()(val sessionManager: SessionManager, val parser: BodyParsers.Default)(implicit ec: ExecutionContext)
|
||||||
|
extends ActionBuilder[AuthenticatedRequest, AnyContent] {
|
||||||
|
|
||||||
|
override def executionContext: ExecutionContext = ec
|
||||||
|
|
||||||
|
protected def getUserFromSession(request: RequestHeader): Option[User] = {
|
||||||
|
val session = request.cookies.get("sessionId")
|
||||||
|
if (session.isDefined)
|
||||||
|
return sessionManager.getUserBySession(session.get.value)
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
override def invokeBlock[A](
|
||||||
|
request: Request[A],
|
||||||
|
block: AuthenticatedRequest[A] => Future[Result]
|
||||||
|
): Future[Result] = {
|
||||||
|
getUserFromSession(request) match {
|
||||||
|
case Some(user) =>
|
||||||
|
block(new AuthenticatedRequest(user, request))
|
||||||
|
case None =>
|
||||||
|
Future.successful(Results.Redirect(routes.UserController.login()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
package components
|
package components
|
||||||
|
|
||||||
import controllers.WebUI
|
|
||||||
import de.knockoutwhist.components.DefaultConfiguration
|
import de.knockoutwhist.components.DefaultConfiguration
|
||||||
import de.knockoutwhist.ui.UI
|
import de.knockoutwhist.ui.UI
|
||||||
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] = super.uis + WebUI
|
override def uis: Set[UI] = Set()
|
||||||
override def listener: Set[EventListener] = super.listener + WebUI
|
override def listener: Set[EventListener] = Set()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import com.google.inject.{Guice, Injector}
|
|
||||||
import controllers.sessions.AdvancedSession
|
|
||||||
import de.knockoutwhist.KnockOutWhist
|
|
||||||
import de.knockoutwhist.components.Configuration
|
|
||||||
import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak}
|
|
||||||
import de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic
|
|
||||||
import di.KnockOutWebConfigurationModule
|
|
||||||
import play.api.mvc.*
|
|
||||||
import play.api.*
|
|
||||||
import play.twirl.api.Html
|
|
||||||
|
|
||||||
import java.util.UUID
|
|
||||||
import javax.inject.*
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This controller creates an `Action` to handle HTTP requests to the
|
|
||||||
* application's home page.
|
|
||||||
*/
|
|
||||||
@Singleton
|
|
||||||
class HomeController @Inject()(val controllerComponents: ControllerComponents) extends BaseController {
|
|
||||||
|
|
||||||
private var initial = false
|
|
||||||
private val injector: Injector = Guice.createInjector(KnockOutWebConfigurationModule())
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an Action to render an HTML page.
|
|
||||||
*
|
|
||||||
* The configuration in the `routes` file means that this method
|
|
||||||
* will be called when the application receives a `GET` request with
|
|
||||||
* a path of `/`.
|
|
||||||
*/
|
|
||||||
def index(): Action[AnyContent] = {
|
|
||||||
if (!initial) {
|
|
||||||
initial = true
|
|
||||||
KnockOutWhist.entry(injector.getInstance(classOf[Configuration]))
|
|
||||||
}
|
|
||||||
Action { implicit request =>
|
|
||||||
Redirect("/sessions")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
def rules(): Action[AnyContent] = {
|
|
||||||
Action { implicit request =>
|
|
||||||
Ok(views.html.rules.apply())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
def sessions(): Action[AnyContent] = {
|
|
||||||
Action { implicit request =>
|
|
||||||
Ok(views.html.sessions.apply(PodGameManager.listSessions()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def ingame(id: String): Action[AnyContent] = {
|
|
||||||
val uuid: UUID = UUID.fromString(id)
|
|
||||||
if (PodGameManager.identify(uuid).isEmpty) {
|
|
||||||
return Action { implicit request =>
|
|
||||||
NotFound(views.html.tui.apply(List(Html(s"<p>Session with id $id not found!</p>"))))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val session = PodGameManager.identify(uuid).get
|
|
||||||
val player = session.asInstanceOf[AdvancedSession].player
|
|
||||||
val logic = WebUI.logic.get.asInstanceOf[BaseGameLogic]
|
|
||||||
if (logic.getCurrentState == Lobby) {
|
|
||||||
|
|
||||||
} else if (logic.getCurrentState == InGame) {
|
|
||||||
return Action { implicit request =>
|
|
||||||
Ok(views.html.ingame.apply(player, logic))
|
|
||||||
}
|
|
||||||
} else if (logic.getCurrentState == SelectTrump) {
|
|
||||||
return Action { implicit request =>
|
|
||||||
Ok(views.html.selecttrump.apply(player, logic))
|
|
||||||
}
|
|
||||||
} else if (logic.getCurrentState == TieBreak) {
|
|
||||||
return Action { implicit request =>
|
|
||||||
Ok(views.html.tie.apply(player, logic))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Action { implicit request =>
|
|
||||||
InternalServerError("Oops")
|
|
||||||
}
|
|
||||||
//if (logic.getCurrentState == Lobby) {
|
|
||||||
//Action { implicit request =>
|
|
||||||
//Ok(views.html.tui.apply(player, logic))
|
|
||||||
//}
|
|
||||||
//} else {
|
|
||||||
//Action { implicit request =>
|
|
||||||
//Ok(views.html.tui.apply(player, logic))
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
438
knockoutwhistweb/app/controllers/IngameController.scala
Normal file
438
knockoutwhistweb/app/controllers/IngameController.scala
Normal file
@@ -0,0 +1,438 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import auth.{AuthAction, AuthenticatedRequest}
|
||||||
|
import de.knockoutwhist.cards.Hand
|
||||||
|
import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak}
|
||||||
|
import exceptions.{CantPlayCardException, GameFullException, NotEnoughPlayersException, NotHostException, NotInThisGameException}
|
||||||
|
import logic.PodManager
|
||||||
|
import logic.game.PollingEvents.CardPlayed
|
||||||
|
import logic.game.PollingEvents.GameStarted
|
||||||
|
import logic.game.{GameLobby, PollingEvents}
|
||||||
|
import model.sessions.{PlayerSession, UserSession}
|
||||||
|
import model.users.User
|
||||||
|
import play.api.*
|
||||||
|
import play.api.libs.json.{JsArray, JsValue, Json}
|
||||||
|
import play.api.mvc.*
|
||||||
|
import util.WebUIUtils
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.inject.*
|
||||||
|
import scala.concurrent.Future
|
||||||
|
import scala.util.Try
|
||||||
|
import scala.concurrent.ExecutionContext
|
||||||
|
@Singleton
|
||||||
|
class IngameController @Inject() (
|
||||||
|
val cc: ControllerComponents,
|
||||||
|
val podManager: PodManager,
|
||||||
|
val authAction: AuthAction,
|
||||||
|
implicit val ec: ExecutionContext
|
||||||
|
) extends AbstractController(cc) {
|
||||||
|
|
||||||
|
// --- Helper function (defined outside match/if for scope) ---
|
||||||
|
def buildSuccessResponse(game: GameLobby, hand: Option[Hand]): JsValue = {
|
||||||
|
// NOTE: Replace the unsafe .get calls here if game state is not guaranteed
|
||||||
|
val currentRound = game.logic.getCurrentRound.get
|
||||||
|
val currentTrick = game.logic.getCurrentTrick.get
|
||||||
|
|
||||||
|
// JSON Building Logic:
|
||||||
|
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",
|
||||||
|
"handData" -> stringHand,
|
||||||
|
"currentPlayerName" -> game.logic.getCurrentPlayer.get.name,
|
||||||
|
"trumpSuit" -> currentRound.trumpSuit.toString,
|
||||||
|
"trickCards" -> trickCardsJson,
|
||||||
|
"scoreTable" -> scoreTableJson,
|
||||||
|
"firstCardId" -> firstCardId,
|
||||||
|
"nextPlayer" -> nextPlayer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def handleEvent(event: PollingEvents, game: GameLobby, user: User): Result = {
|
||||||
|
event match {
|
||||||
|
case CardPlayed =>
|
||||||
|
val player = game.getPlayerByUser(user)
|
||||||
|
val hand = player.currentHand()
|
||||||
|
val jsonResponse = buildSuccessResponse(game, hand)
|
||||||
|
Ok(jsonResponse)
|
||||||
|
case GameStarted =>
|
||||||
|
val jsonResponse = Json.obj(
|
||||||
|
"status" -> "gameStart",
|
||||||
|
"redirectUrl" -> routes.IngameController.game(game.id).url
|
||||||
|
)
|
||||||
|
Ok(jsonResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- Main Polling Action ---
|
||||||
|
def polling(gameId: String): Action[AnyContent] = authAction.async { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
|
||||||
|
val playerId = request.user.id
|
||||||
|
|
||||||
|
// 1. Safely look up the game
|
||||||
|
podManager.getGame(gameId) match {
|
||||||
|
case Some(game) =>
|
||||||
|
|
||||||
|
// 2. Short-Poll Check (Check for missed events)
|
||||||
|
if (game.getPollingState.nonEmpty) {
|
||||||
|
val event = game.getPollingState.dequeue()
|
||||||
|
|
||||||
|
Future.successful(handleEvent(event, game, request.user))
|
||||||
|
} else {
|
||||||
|
|
||||||
|
val eventPromise = game.registerWaiter(playerId)
|
||||||
|
|
||||||
|
eventPromise.future.map { event =>
|
||||||
|
game.removeWaiter(playerId)
|
||||||
|
handleEvent(event, game, request.user)
|
||||||
|
}.recover {
|
||||||
|
case _: Throwable =>
|
||||||
|
game.removeWaiter(playerId)
|
||||||
|
NoContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case None =>
|
||||||
|
// Game not found
|
||||||
|
Future.successful(NotFound("Game not found."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def game(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
val game = podManager.getGame(gameId)
|
||||||
|
game match {
|
||||||
|
case Some(g) =>
|
||||||
|
g.logic.getCurrentState match {
|
||||||
|
case Lobby => Ok(views.html.lobby.lobby(Some(request.user), g))
|
||||||
|
case InGame =>
|
||||||
|
Ok(views.html.ingame.ingame(
|
||||||
|
g.getPlayerByUser(request.user),
|
||||||
|
g
|
||||||
|
))
|
||||||
|
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 =>
|
||||||
|
NotFound("Game not found")
|
||||||
|
}
|
||||||
|
//NotFound(s"Reached end of game method unexpectedly. GameId: $gameId")
|
||||||
|
}
|
||||||
|
def startGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
val game = podManager.getGame(gameId)
|
||||||
|
val result = Try {
|
||||||
|
game match {
|
||||||
|
case Some(g) =>
|
||||||
|
g.startGame(request.user)
|
||||||
|
case None =>
|
||||||
|
NotFound("Game not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 _: NotHostException =>
|
||||||
|
Forbidden(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
|
case _: NotEnoughPlayersException =>
|
||||||
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
|
case _ =>
|
||||||
|
InternalServerError(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 result = Try {
|
||||||
|
game.get.leaveGame(request.user.id)
|
||||||
|
}
|
||||||
|
if (result.isSuccess) {
|
||||||
|
Ok(Json.obj(
|
||||||
|
"status" -> "success",
|
||||||
|
"redirectUrl" -> routes.MainMenuController.mainMenu().url
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
InternalServerError(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> "Something went wrong."
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def joinGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
val game = podManager.getGame(gameId)
|
||||||
|
val result = Try {
|
||||||
|
game match {
|
||||||
|
case Some(g) =>
|
||||||
|
g.addUser(request.user)
|
||||||
|
case None =>
|
||||||
|
NotFound("Game not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.isSuccess) {
|
||||||
|
Redirect(routes.IngameController.game(gameId))
|
||||||
|
} else {
|
||||||
|
val throwable = result.failed.get
|
||||||
|
throwable match {
|
||||||
|
case _: GameFullException =>
|
||||||
|
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] => {
|
||||||
|
val game = podManager.getGame(gameId)
|
||||||
|
game match {
|
||||||
|
case Some(g) =>
|
||||||
|
val jsonBody = request.body.asJson
|
||||||
|
val cardIdOpt: Option[String] = jsonBody.flatMap { jsValue =>
|
||||||
|
(jsValue \ "cardID").asOpt[String]
|
||||||
|
}
|
||||||
|
cardIdOpt match {
|
||||||
|
case Some(cardId) =>
|
||||||
|
var optSession: Option[UserSession] = None
|
||||||
|
val result = Try {
|
||||||
|
val session = g.getUserSession(request.user.id)
|
||||||
|
optSession = Some(session)
|
||||||
|
session.lock.lock()
|
||||||
|
g.playCard(session, cardId.toInt)
|
||||||
|
}
|
||||||
|
optSession.foreach(_.lock.unlock())
|
||||||
|
if (result.isSuccess) {
|
||||||
|
Ok(Json.obj(
|
||||||
|
"status" -> "success",
|
||||||
|
"redirectUrl" -> routes.IngameController.game(gameId).url
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
val throwable = result.failed.get
|
||||||
|
throwable match {
|
||||||
|
case _: CantPlayCardException =>
|
||||||
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
|
case _: NotInThisGameException =>
|
||||||
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> throwable.getMessage
|
||||||
|
))
|
||||||
|
case _: IllegalArgumentException =>
|
||||||
|
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 =>
|
||||||
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> "cardId Parameter is missing"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
case None =>
|
||||||
|
NotFound(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> "Game not found"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def playDogCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => {
|
||||||
|
val game = podManager.getGame(gameId)
|
||||||
|
game match {
|
||||||
|
case Some(g) => {
|
||||||
|
val cardIdOpt = request.body.asFormUrlEncoded.flatMap(_.get("cardId").flatMap(_.headOption))
|
||||||
|
var optSession: Option[UserSession] = None
|
||||||
|
val result = Try {
|
||||||
|
cardIdOpt match {
|
||||||
|
case Some(cardId) if cardId == "skip" =>
|
||||||
|
val session = g.getUserSession(request.user.id)
|
||||||
|
optSession = Some(session)
|
||||||
|
session.lock.lock()
|
||||||
|
g.playDogCard(session, -1)
|
||||||
|
case Some(cardId) =>
|
||||||
|
val session = g.getUserSession(request.user.id)
|
||||||
|
optSession = Some(session)
|
||||||
|
session.lock.lock()
|
||||||
|
g.playDogCard(session, cardId.toInt)
|
||||||
|
case None =>
|
||||||
|
throw new IllegalArgumentException("cardId parameter is missing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
optSession.foreach(_.lock.unlock())
|
||||||
|
if (result.isSuccess) {
|
||||||
|
NoContent
|
||||||
|
} else {
|
||||||
|
val throwable = result.failed.get
|
||||||
|
throwable match {
|
||||||
|
case _: CantPlayCardException =>
|
||||||
|
BadRequest(throwable.getMessage)
|
||||||
|
case _: NotInThisGameException =>
|
||||||
|
BadRequest(throwable.getMessage)
|
||||||
|
case _: IllegalArgumentException =>
|
||||||
|
BadRequest(throwable.getMessage)
|
||||||
|
case _: IllegalStateException =>
|
||||||
|
BadRequest(throwable.getMessage)
|
||||||
|
case _ =>
|
||||||
|
InternalServerError(throwable.getMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case None =>
|
||||||
|
NotFound("Game not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def playTrump(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
val game = podManager.getGame(gameId)
|
||||||
|
game match {
|
||||||
|
case Some(g) =>
|
||||||
|
val trumpOpt = request.body.asFormUrlEncoded.flatMap(_.get("trump").flatMap(_.headOption))
|
||||||
|
trumpOpt match {
|
||||||
|
case Some(trump) =>
|
||||||
|
var optSession: Option[UserSession] = None
|
||||||
|
val result = Try {
|
||||||
|
val session = g.getUserSession(request.user.id)
|
||||||
|
optSession = Some(session)
|
||||||
|
session.lock.lock()
|
||||||
|
g.selectTrump(session, trump.toInt)
|
||||||
|
}
|
||||||
|
optSession.foreach(_.lock.unlock())
|
||||||
|
if (result.isSuccess) {
|
||||||
|
NoContent
|
||||||
|
} else {
|
||||||
|
val throwable = result.failed.get
|
||||||
|
throwable match {
|
||||||
|
case _: IllegalArgumentException =>
|
||||||
|
BadRequest(throwable.getMessage)
|
||||||
|
case _: NotInThisGameException =>
|
||||||
|
BadRequest(throwable.getMessage)
|
||||||
|
case _: IllegalStateException =>
|
||||||
|
BadRequest(throwable.getMessage)
|
||||||
|
case _ =>
|
||||||
|
InternalServerError(throwable.getMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case None =>
|
||||||
|
BadRequest("trump parameter is missing")
|
||||||
|
}
|
||||||
|
case None =>
|
||||||
|
NotFound("Game not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def playTie(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
val game = podManager.getGame(gameId)
|
||||||
|
game match {
|
||||||
|
case Some(g) =>
|
||||||
|
val tieOpt = request.body.asFormUrlEncoded.flatMap(_.get("tie").flatMap(_.headOption))
|
||||||
|
tieOpt match {
|
||||||
|
case Some(tie) =>
|
||||||
|
var optSession: Option[UserSession] = None
|
||||||
|
val result = Try {
|
||||||
|
val session = g.getUserSession(request.user.id)
|
||||||
|
optSession = Some(session)
|
||||||
|
session.lock.lock()
|
||||||
|
g.selectTie(g.getUserSession(request.user.id), tie.toInt)
|
||||||
|
}
|
||||||
|
optSession.foreach(_.lock.unlock())
|
||||||
|
if (result.isSuccess) {
|
||||||
|
NoContent
|
||||||
|
} else {
|
||||||
|
val throwable = result.failed.get
|
||||||
|
throwable match {
|
||||||
|
case _: IllegalArgumentException =>
|
||||||
|
BadRequest(throwable.getMessage)
|
||||||
|
case _: NotInThisGameException =>
|
||||||
|
BadRequest(throwable.getMessage)
|
||||||
|
case _: IllegalStateException =>
|
||||||
|
BadRequest(throwable.getMessage)
|
||||||
|
case _ =>
|
||||||
|
InternalServerError(throwable.getMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case None =>
|
||||||
|
BadRequest("tie parameter is missing")
|
||||||
|
}
|
||||||
|
case None =>
|
||||||
|
NotFound("Game not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
Ok(
|
||||||
|
JavaScriptReverseRouter("jsRoutes")(
|
||||||
|
routes.javascript.MainMenuController.createGame,
|
||||||
|
routes.javascript.IngameController.startGame,
|
||||||
|
routes.javascript.IngameController.kickPlayer,
|
||||||
|
routes.javascript.IngameController.leaveGame,
|
||||||
|
routes.javascript.IngameController.playCard,
|
||||||
|
routes.javascript.IngameController.polling
|
||||||
|
)
|
||||||
|
).as("text/javascript")
|
||||||
|
}
|
||||||
|
}
|
||||||
78
knockoutwhistweb/app/controllers/MainMenuController.scala
Normal file
78
knockoutwhistweb/app/controllers/MainMenuController.scala
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import auth.{AuthAction, AuthenticatedRequest}
|
||||||
|
import logic.PodManager
|
||||||
|
import play.api.*
|
||||||
|
import play.api.libs.json.Json
|
||||||
|
import play.api.mvc.*
|
||||||
|
|
||||||
|
import javax.inject.*
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This controller creates an `Action` to handle HTTP requests to the
|
||||||
|
* application's home page.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class MainMenuController @Inject()(
|
||||||
|
val controllerComponents: ControllerComponents,
|
||||||
|
val authAction: AuthAction,
|
||||||
|
val podManager: PodManager
|
||||||
|
) extends BaseController {
|
||||||
|
|
||||||
|
// Pass the request-handling function directly to authAction (no nested Action)
|
||||||
|
def mainMenu(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
Ok(views.html.mainmenu.creategame(Some(request.user)))
|
||||||
|
}
|
||||||
|
|
||||||
|
def index(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
Redirect(routes.MainMenuController.mainMenu())
|
||||||
|
}
|
||||||
|
|
||||||
|
def createGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
val jsonBody = request.body.asJson
|
||||||
|
if (jsonBody.isDefined) {
|
||||||
|
val gamename: String = (jsonBody.get \ "lobbyname").asOpt[String]
|
||||||
|
.getOrElse(s"${request.user.name}'s Game")
|
||||||
|
|
||||||
|
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
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
BadRequest(Json.obj(
|
||||||
|
"status" -> "failure",
|
||||||
|
"errorMessage" -> "Invalid form submission"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def joinGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
val postData = request.body.asFormUrlEncoded
|
||||||
|
if (postData.isDefined) {
|
||||||
|
val gameId = postData.get.get("gameId").flatMap(_.headOption).getOrElse("")
|
||||||
|
val game = podManager.getGame(gameId)
|
||||||
|
game match {
|
||||||
|
case Some(g) =>
|
||||||
|
Redirect(routes.IngameController.joinGame(gameId))
|
||||||
|
case None =>
|
||||||
|
NotFound("Game not found")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
BadRequest("Invalid form submission")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def rules(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
Ok(views.html.mainmenu.rules(Some(request.user)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import controllers.sessions.PlayerSession
|
|
||||||
import de.knockoutwhist.utils.events.SimpleEvent
|
|
||||||
|
|
||||||
import java.util.UUID
|
|
||||||
import scala.collection.mutable
|
|
||||||
|
|
||||||
object PodGameManager {
|
|
||||||
|
|
||||||
private val sessions: mutable.Map[UUID, PlayerSession] = mutable.Map()
|
|
||||||
|
|
||||||
def addSession(session: PlayerSession): Unit = {
|
|
||||||
sessions.put(session.id, session)
|
|
||||||
}
|
|
||||||
|
|
||||||
def clearSessions(): Unit = {
|
|
||||||
sessions.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
def identify(id: UUID): Option[PlayerSession] = {
|
|
||||||
sessions.get(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
def transmit(id: UUID, event: SimpleEvent): Unit = {
|
|
||||||
identify(id).foreach(_.updatePlayer(event))
|
|
||||||
}
|
|
||||||
|
|
||||||
def transmitAll(event: SimpleEvent): Unit = {
|
|
||||||
sessions.foreach(session => session._2.updatePlayer(event))
|
|
||||||
}
|
|
||||||
|
|
||||||
def listSessions(): List[PlayerSession] = {
|
|
||||||
sessions.values.toList
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
70
knockoutwhistweb/app/controllers/UserController.scala
Normal file
70
knockoutwhistweb/app/controllers/UserController.scala
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import auth.{AuthAction, AuthenticatedRequest}
|
||||||
|
import logic.user.{SessionManager, UserManager}
|
||||||
|
import play.api.*
|
||||||
|
import play.api.mvc.*
|
||||||
|
|
||||||
|
import javax.inject.*
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This controller creates an `Action` to handle HTTP requests to the
|
||||||
|
* application's home page.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class UserController @Inject()(
|
||||||
|
val controllerComponents: ControllerComponents,
|
||||||
|
val sessionManager: SessionManager,
|
||||||
|
val userManager: UserManager,
|
||||||
|
val authAction: AuthAction
|
||||||
|
) extends BaseController {
|
||||||
|
|
||||||
|
def login(): Action[AnyContent] = {
|
||||||
|
Action { implicit request =>
|
||||||
|
val session = request.cookies.get("sessionId")
|
||||||
|
if (session.isDefined) {
|
||||||
|
val possibleUser = sessionManager.getUserBySession(session.get.value)
|
||||||
|
if (possibleUser.isDefined) {
|
||||||
|
Redirect(routes.MainMenuController.mainMenu())
|
||||||
|
} else {
|
||||||
|
Ok(views.html.login.login())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(views.html.login.login())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def login_Post(): Action[AnyContent] = {
|
||||||
|
Action { implicit request =>
|
||||||
|
val postData = request.body.asFormUrlEncoded
|
||||||
|
if (postData.isDefined) {
|
||||||
|
// Extract username and password from form data
|
||||||
|
val username = postData.get.get("username").flatMap(_.headOption).getOrElse("")
|
||||||
|
val password = postData.get.get("password").flatMap(_.headOption).getOrElse("")
|
||||||
|
val possibleUser = userManager.authenticate(username, password)
|
||||||
|
if (possibleUser.isDefined) {
|
||||||
|
Redirect(routes.MainMenuController.mainMenu()).withCookies(
|
||||||
|
Cookie("sessionId", sessionManager.createSession(possibleUser.get))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
println("Failed login attempt for user: " + username)
|
||||||
|
Unauthorized("Invalid username or password")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
BadRequest("Invalid form submission")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass the request-handling function directly to authAction (no nested Action)
|
||||||
|
def logout(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
|
val sessionCookie = request.cookies.get("sessionId")
|
||||||
|
if (sessionCookie.isDefined) {
|
||||||
|
sessionManager.invalidateSession(sessionCookie.get.value)
|
||||||
|
}
|
||||||
|
Redirect(routes.UserController.login()).discardingCookies(DiscardingCookie("sessionId"))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import controllers.sessions.AdvancedSession
|
|
||||||
import de.knockoutwhist.cards.{Card, CardValue, Hand, Suit}
|
|
||||||
import de.knockoutwhist.control.GameLogic
|
|
||||||
import de.knockoutwhist.control.GameState.{InGame, Lobby}
|
|
||||||
import de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic
|
|
||||||
import de.knockoutwhist.events.*
|
|
||||||
import de.knockoutwhist.events.global.GameStateChangeEvent
|
|
||||||
import de.knockoutwhist.player.AbstractPlayer
|
|
||||||
import de.knockoutwhist.rounds.Match
|
|
||||||
import de.knockoutwhist.ui.UI
|
|
||||||
import de.knockoutwhist.utils.CustomThread
|
|
||||||
import de.knockoutwhist.utils.events.{EventListener, SimpleEvent}
|
|
||||||
|
|
||||||
object WebUI extends CustomThread with EventListener with UI {
|
|
||||||
|
|
||||||
setName("WebUI")
|
|
||||||
|
|
||||||
var init = false
|
|
||||||
var logic: Option[GameLogic] = None
|
|
||||||
|
|
||||||
var latestOutput: String = ""
|
|
||||||
|
|
||||||
override def instance: CustomThread = WebUI
|
|
||||||
|
|
||||||
override def listen(event: SimpleEvent): Unit = {
|
|
||||||
event match {
|
|
||||||
case event: GameStateChangeEvent =>
|
|
||||||
if (event.oldState == Lobby && event.newState == InGame) {
|
|
||||||
val match1: Option[Match] = logic.get.asInstanceOf[BaseGameLogic].getCurrentMatch
|
|
||||||
val players: List[AbstractPlayer] = match1.get.totalplayers
|
|
||||||
players.map(player => PodGameManager.addSession(AdvancedSession(player.id, player)))
|
|
||||||
}
|
|
||||||
case _ =>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override def initial(gameLogic: GameLogic): Boolean = {
|
|
||||||
if (init) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
init = true
|
|
||||||
this.logic = Some(gameLogic)
|
|
||||||
start()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package exceptions;
|
||||||
|
|
||||||
|
public class CantPlayCardException extends GameException {
|
||||||
|
public CantPlayCardException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
knockoutwhistweb/app/exceptions/GameException.java
Normal file
7
knockoutwhistweb/app/exceptions/GameException.java
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package exceptions;
|
||||||
|
|
||||||
|
public abstract class GameException extends RuntimeException {
|
||||||
|
public GameException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
knockoutwhistweb/app/exceptions/GameFullException.java
Normal file
7
knockoutwhistweb/app/exceptions/GameFullException.java
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package exceptions;
|
||||||
|
|
||||||
|
public class GameFullException extends GameException {
|
||||||
|
public GameFullException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package exceptions;
|
||||||
|
|
||||||
|
public class NotEnoughPlayersException extends GameException {
|
||||||
|
public NotEnoughPlayersException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
knockoutwhistweb/app/exceptions/NotHostException.java
Normal file
7
knockoutwhistweb/app/exceptions/NotHostException.java
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package exceptions;
|
||||||
|
|
||||||
|
public class NotHostException extends GameException {
|
||||||
|
public NotHostException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package exceptions;
|
||||||
|
|
||||||
|
public class NotInThisGameException extends GameException {
|
||||||
|
public NotInThisGameException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package exceptions;
|
||||||
|
|
||||||
|
public class NotInteractableException extends GameException {
|
||||||
|
public NotInteractableException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
knockoutwhistweb/app/logic/PodManager.scala
Normal file
49
knockoutwhistweb/app/logic/PodManager.scala
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package logic
|
||||||
|
|
||||||
|
import com.google.inject.{Guice, Injector}
|
||||||
|
import de.knockoutwhist.components.Configuration
|
||||||
|
import de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic
|
||||||
|
import di.KnockOutWebConfigurationModule
|
||||||
|
import logic.game.GameLobby
|
||||||
|
import model.users.User
|
||||||
|
import util.GameUtil
|
||||||
|
|
||||||
|
import javax.inject.Singleton
|
||||||
|
import scala.collection.mutable
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class PodManager {
|
||||||
|
|
||||||
|
val TTL: Long = System.currentTimeMillis() + 86400000L // 24 hours in milliseconds
|
||||||
|
val podIp: String = System.getenv("POD_IP")
|
||||||
|
val podName: String = System.getenv("POD_NAME")
|
||||||
|
|
||||||
|
private val sessions: mutable.Map[String, GameLobby] = mutable.Map()
|
||||||
|
private val injector: Injector = Guice.createInjector(KnockOutWebConfigurationModule())
|
||||||
|
|
||||||
|
def createGame(
|
||||||
|
host: User,
|
||||||
|
name: String,
|
||||||
|
maxPlayers: Int
|
||||||
|
): GameLobby = {
|
||||||
|
val gameLobby = GameLobby(
|
||||||
|
logic = BaseGameLogic(injector.getInstance(classOf[Configuration])),
|
||||||
|
id = GameUtil.generateCode(),
|
||||||
|
internalId = java.util.UUID.randomUUID(),
|
||||||
|
name = name,
|
||||||
|
maxPlayers = maxPlayers,
|
||||||
|
host = host
|
||||||
|
)
|
||||||
|
sessions += (gameLobby.id -> gameLobby)
|
||||||
|
gameLobby
|
||||||
|
}
|
||||||
|
|
||||||
|
def getGame(gameId: String): Option[GameLobby] = {
|
||||||
|
sessions.get(gameId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private[logic] def removeGame(gameId: String): Unit = {
|
||||||
|
sessions.remove(gameId)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
294
knockoutwhistweb/app/logic/game/GameLobby.scala
Normal file
294
knockoutwhistweb/app/logic/game/GameLobby.scala
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
package logic.game
|
||||||
|
|
||||||
|
import de.knockoutwhist.cards.{Hand, Suit}
|
||||||
|
import de.knockoutwhist.control.GameLogic
|
||||||
|
import de.knockoutwhist.control.GameState.{InGame, Lobby, MainMenu}
|
||||||
|
import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil}
|
||||||
|
import de.knockoutwhist.events.global.{CardPlayedEvent, GameStateChangeEvent, SessionClosed}
|
||||||
|
import de.knockoutwhist.events.player.PlayerEvent
|
||||||
|
import de.knockoutwhist.player.Playertype.HUMAN
|
||||||
|
import de.knockoutwhist.player.{AbstractPlayer, PlayerFactory}
|
||||||
|
import de.knockoutwhist.rounds.{Match, Round, Trick}
|
||||||
|
import de.knockoutwhist.utils.events.{EventListener, SimpleEvent}
|
||||||
|
import exceptions.*
|
||||||
|
import logic.game.PollingEvents.{CardPlayed, GameStarted}
|
||||||
|
import model.sessions.{InteractionType, UserSession}
|
||||||
|
import model.users.User
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
import scala.collection.mutable
|
||||||
|
import scala.collection.mutable.ListBuffer
|
||||||
|
import scala.concurrent.{Promise => ScalaPromise}
|
||||||
|
|
||||||
|
class GameLobby private(
|
||||||
|
val logic: GameLogic,
|
||||||
|
val id: String,
|
||||||
|
val internalId: UUID,
|
||||||
|
val name: String,
|
||||||
|
val maxPlayers: Int
|
||||||
|
) extends EventListener {
|
||||||
|
logic.addListener(this)
|
||||||
|
logic.createSession()
|
||||||
|
|
||||||
|
private val users: mutable.Map[UUID, UserSession] = mutable.Map()
|
||||||
|
private var pollingState: mutable.Queue[PollingEvents] = mutable.Queue()
|
||||||
|
private val waitingPromises: mutable.Map[UUID, ScalaPromise[PollingEvents]] = mutable.Map()
|
||||||
|
|
||||||
|
def registerWaiter(playerId: UUID): ScalaPromise[PollingEvents] = {
|
||||||
|
val promise = ScalaPromise[PollingEvents]()
|
||||||
|
waitingPromises.put(playerId, promise)
|
||||||
|
promise
|
||||||
|
}
|
||||||
|
|
||||||
|
def removeWaiter(playerId: UUID): Unit = {
|
||||||
|
waitingPromises.remove(playerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
def addUser(user: User): UserSession = {
|
||||||
|
if (users.size >= maxPlayers) throw new GameFullException("The game is full!")
|
||||||
|
if (users.contains(user.id)) throw new IllegalArgumentException("User is already in the game!")
|
||||||
|
if (logic.getCurrentState != Lobby) throw new IllegalStateException("The game has already started!")
|
||||||
|
val userSession = new UserSession(
|
||||||
|
user = user,
|
||||||
|
host = false
|
||||||
|
)
|
||||||
|
users += (user.id -> userSession)
|
||||||
|
userSession
|
||||||
|
}
|
||||||
|
|
||||||
|
override def listen(event: SimpleEvent): Unit = {
|
||||||
|
event match {
|
||||||
|
case event: CardPlayedEvent =>
|
||||||
|
val newEvent = PollingEvents.CardPlayed
|
||||||
|
if (waitingPromises.nonEmpty) {
|
||||||
|
waitingPromises.values.foreach(_.success(newEvent))
|
||||||
|
waitingPromises.clear()
|
||||||
|
} else {
|
||||||
|
pollingState.enqueue(newEvent)
|
||||||
|
}
|
||||||
|
case event: PlayerEvent =>
|
||||||
|
users.get(event.playerId).foreach(session => session.updatePlayer(event))
|
||||||
|
case event: GameStateChangeEvent =>
|
||||||
|
if (event.oldState == MainMenu && event.newState == Lobby) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.oldState == Lobby && event.newState == InGame) {
|
||||||
|
val newEvent = PollingEvents.GameStarted
|
||||||
|
if (waitingPromises.nonEmpty) {
|
||||||
|
waitingPromises.values.foreach(_.success(newEvent))
|
||||||
|
waitingPromises.clear()
|
||||||
|
} else {
|
||||||
|
pollingState.enqueue(newEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
users.values.foreach(session => session.updatePlayer(event))
|
||||||
|
case event: SessionClosed =>
|
||||||
|
users.values.foreach(session => session.updatePlayer(event))
|
||||||
|
case event: SimpleEvent =>
|
||||||
|
users.values.foreach(session => session.updatePlayer(event))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the game if the user is the host.
|
||||||
|
* @param user the user who wants to start the game.
|
||||||
|
*/
|
||||||
|
def startGame(user: User): Unit = {
|
||||||
|
val sessionOpt = users.get(user.id)
|
||||||
|
if (sessionOpt.isEmpty) {
|
||||||
|
throw new NotInThisGameException("You are not in this game!")
|
||||||
|
}
|
||||||
|
if (!sessionOpt.get.host) {
|
||||||
|
throw new NotHostException("Only the host can start the game!")
|
||||||
|
}
|
||||||
|
if (logic.getCurrentState != Lobby) {
|
||||||
|
throw new IllegalStateException("The game has already started!")
|
||||||
|
}
|
||||||
|
val playerNamesList = ListBuffer[AbstractPlayer]()
|
||||||
|
users.values.foreach { player =>
|
||||||
|
playerNamesList += PlayerFactory.createPlayer(player.name, player.id, HUMAN)
|
||||||
|
}
|
||||||
|
if (playerNamesList.size < 2) {
|
||||||
|
throw new NotEnoughPlayersException("Not enough players to start the game!")
|
||||||
|
}
|
||||||
|
logic.createMatch(playerNamesList.toList)
|
||||||
|
logic.controlMatch()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the user from the game lobby.
|
||||||
|
* @param user the user who wants to leave the game.
|
||||||
|
*/
|
||||||
|
def leaveGame(userId: UUID): Unit = {
|
||||||
|
val sessionOpt = users.get(userId)
|
||||||
|
if (sessionOpt.isEmpty) {
|
||||||
|
throw new NotInThisGameException("You are not in this game!")
|
||||||
|
}
|
||||||
|
users.remove(userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play a card from the player's hand.
|
||||||
|
* @param userSession the user session of the player.
|
||||||
|
* @param cardIndex the index of the card in the player's hand.
|
||||||
|
*/
|
||||||
|
def playCard(userSession: UserSession, cardIndex: Int): Unit = {
|
||||||
|
val player = getPlayerInteractable(userSession, InteractionType.Card)
|
||||||
|
if (player.isInDogLife) {
|
||||||
|
throw new CantPlayCardException("You are in dog life!")
|
||||||
|
}
|
||||||
|
val hand = getHand(player)
|
||||||
|
val card = hand.cards(cardIndex)
|
||||||
|
if (!PlayerUtil.canPlayCard(card, getRound, getTrick, player)) {
|
||||||
|
throw new CantPlayCardException("You can't play this card!")
|
||||||
|
}
|
||||||
|
userSession.resetCanInteract()
|
||||||
|
logic.playerInputLogic.receivedCard(card)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play a card from the player's hand while in dog life or skip the round.
|
||||||
|
* @param userSession the user session of the player.
|
||||||
|
* @param cardIndex the index of the card in the player's hand or -1 if the player wants to skip the round.
|
||||||
|
*/
|
||||||
|
def playDogCard(userSession: UserSession, cardIndex: Int): Unit = {
|
||||||
|
val player = getPlayerInteractable(userSession, InteractionType.DogCard)
|
||||||
|
if (!player.isInDogLife) {
|
||||||
|
throw new CantPlayCardException("You are not in dog life!")
|
||||||
|
}
|
||||||
|
if (cardIndex == -1) {
|
||||||
|
if (!MatchUtil.dogNeedsToPlay(getMatch, getRound)) {
|
||||||
|
throw new CantPlayCardException("You can't skip this round!")
|
||||||
|
}
|
||||||
|
logic.playerInputLogic.receivedDog(None)
|
||||||
|
}
|
||||||
|
val hand = getHand(player)
|
||||||
|
val card = hand.cards(cardIndex)
|
||||||
|
userSession.resetCanInteract()
|
||||||
|
logic.playerInputLogic.receivedDog(Some(card))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select the trump suit for the round.
|
||||||
|
* @param userSession the user session of the player.
|
||||||
|
* @param trumpIndex the index of the trump suit.
|
||||||
|
*/
|
||||||
|
def selectTrump(userSession: UserSession, trumpIndex: Int): Unit = {
|
||||||
|
val player = getPlayerInteractable(userSession, InteractionType.TrumpSuit)
|
||||||
|
val trumpSuits = Suit.values.toList
|
||||||
|
val selectedTrump = trumpSuits(trumpIndex)
|
||||||
|
userSession.resetCanInteract()
|
||||||
|
logic.playerInputLogic.receivedTrumpSuit(selectedTrump)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param userSession
|
||||||
|
* @param tieNumber
|
||||||
|
*/
|
||||||
|
def selectTie(userSession: UserSession, tieNumber: Int): Unit = {
|
||||||
|
val player = getPlayerInteractable(userSession, InteractionType.TieChoice)
|
||||||
|
userSession.resetCanInteract()
|
||||||
|
logic.playerTieLogic.receivedTieBreakerCard(tieNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//-------------------
|
||||||
|
|
||||||
|
def getUserSession(userId: UUID): UserSession = {
|
||||||
|
val sessionOpt = users.get(userId)
|
||||||
|
if (sessionOpt.isEmpty) {
|
||||||
|
throw new NotInThisGameException("You are not in this game!")
|
||||||
|
}
|
||||||
|
sessionOpt.get
|
||||||
|
}
|
||||||
|
|
||||||
|
def getPlayerByUser(user: User): AbstractPlayer = {
|
||||||
|
getPlayerBySession(getUserSession(user.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
def getPlayers: mutable.Map[UUID, UserSession] = {
|
||||||
|
users.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
def getLogic: GameLogic = {
|
||||||
|
logic
|
||||||
|
}
|
||||||
|
def getPollingState: mutable.Queue[PollingEvents] = {
|
||||||
|
pollingState
|
||||||
|
}
|
||||||
|
private def getPlayerBySession(userSession: UserSession): AbstractPlayer = {
|
||||||
|
val playerOption = getMatch.totalplayers.find(_.id == userSession.id)
|
||||||
|
if (playerOption.isEmpty) {
|
||||||
|
throw new NotInThisGameException("You are not in this game!")
|
||||||
|
}
|
||||||
|
playerOption.get
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getPlayerInteractable(userSession: UserSession, iType: InteractionType): AbstractPlayer = {
|
||||||
|
if (!userSession.lock.isHeldByCurrentThread) {
|
||||||
|
throw new IllegalStateException("The user session is not locked!")
|
||||||
|
}
|
||||||
|
if (userSession.canInteract.isEmpty || userSession.canInteract.get != iType) {
|
||||||
|
throw new NotInteractableException("You can't play a card!")
|
||||||
|
}
|
||||||
|
getPlayerBySession(userSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getHand(player: AbstractPlayer): Hand = {
|
||||||
|
val handOption = player.currentHand()
|
||||||
|
if (handOption.isEmpty) {
|
||||||
|
throw new IllegalStateException("You have no cards!")
|
||||||
|
}
|
||||||
|
handOption.get
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getMatch: Match = {
|
||||||
|
val matchOpt = logic.getCurrentMatch
|
||||||
|
if (matchOpt.isEmpty) {
|
||||||
|
throw new IllegalStateException("No match is currently running!")
|
||||||
|
}
|
||||||
|
matchOpt.get
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getRound: Round = {
|
||||||
|
val roundOpt = logic.getCurrentRound
|
||||||
|
if (roundOpt.isEmpty) {
|
||||||
|
throw new IllegalStateException("No round is currently running!")
|
||||||
|
}
|
||||||
|
roundOpt.get
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getTrick: Trick = {
|
||||||
|
val trickOpt = logic.getCurrentTrick
|
||||||
|
if (trickOpt.isEmpty) {
|
||||||
|
throw new IllegalStateException("No trick is currently running!")
|
||||||
|
}
|
||||||
|
trickOpt.get
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object GameLobby {
|
||||||
|
def apply(
|
||||||
|
logic: GameLogic,
|
||||||
|
id: String,
|
||||||
|
internalId: UUID,
|
||||||
|
name: String,
|
||||||
|
maxPlayers: Int,
|
||||||
|
host: User
|
||||||
|
): GameLobby = {
|
||||||
|
val lobby = new GameLobby(
|
||||||
|
logic = logic,
|
||||||
|
id = id,
|
||||||
|
internalId = internalId,
|
||||||
|
name = name,
|
||||||
|
maxPlayers = maxPlayers
|
||||||
|
)
|
||||||
|
lobby.users += (host.id -> new UserSession(
|
||||||
|
user = host,
|
||||||
|
host = true
|
||||||
|
))
|
||||||
|
lobby
|
||||||
|
}
|
||||||
|
}
|
||||||
6
knockoutwhistweb/app/logic/game/PollingEvents.scala
Normal file
6
knockoutwhistweb/app/logic/game/PollingEvents.scala
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package logic.game
|
||||||
|
|
||||||
|
enum PollingEvents {
|
||||||
|
case CardPlayed
|
||||||
|
case GameStarted
|
||||||
|
}
|
||||||
14
knockoutwhistweb/app/logic/user/SessionManager.scala
Normal file
14
knockoutwhistweb/app/logic/user/SessionManager.scala
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package logic.user
|
||||||
|
|
||||||
|
import com.google.inject.ImplementedBy
|
||||||
|
import logic.user.impl.BaseSessionManager
|
||||||
|
import model.users.User
|
||||||
|
|
||||||
|
@ImplementedBy(classOf[BaseSessionManager])
|
||||||
|
trait SessionManager {
|
||||||
|
|
||||||
|
def createSession(user: User): String
|
||||||
|
def getUserBySession(sessionId: String): Option[User]
|
||||||
|
def invalidateSession(sessionId: String): Unit
|
||||||
|
|
||||||
|
}
|
||||||
16
knockoutwhistweb/app/logic/user/UserManager.scala
Normal file
16
knockoutwhistweb/app/logic/user/UserManager.scala
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package logic.user
|
||||||
|
|
||||||
|
import com.google.inject.ImplementedBy
|
||||||
|
import logic.user.impl.StubUserManager
|
||||||
|
import model.users.User
|
||||||
|
|
||||||
|
@ImplementedBy(classOf[StubUserManager])
|
||||||
|
trait UserManager {
|
||||||
|
|
||||||
|
def addUser(name: String, password: String): Boolean
|
||||||
|
def authenticate(name: String, password: String): Option[User]
|
||||||
|
def userExists(name: String): Option[User]
|
||||||
|
def userExistsById(id: Long): Option[User]
|
||||||
|
def removeUser(name: String): Boolean
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package logic.user.impl
|
||||||
|
|
||||||
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
|
import com.auth0.jwt.{JWT, JWTVerifier}
|
||||||
|
import com.github.benmanes.caffeine.cache.{Cache, Caffeine}
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import logic.user.SessionManager
|
||||||
|
import model.users.User
|
||||||
|
import scalafx.util.Duration
|
||||||
|
import services.JwtKeyProvider
|
||||||
|
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.{Inject, Singleton}
|
||||||
|
import scala.util.Try
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class BaseSessionManager @Inject()(val keyProvider: JwtKeyProvider, val userManager: StubUserManager, val config: Config) extends SessionManager {
|
||||||
|
|
||||||
|
private val algorithm = Algorithm.RSA512(keyProvider.publicKey, keyProvider.privateKey)
|
||||||
|
private val verifier: JWTVerifier = JWT.require(algorithm)
|
||||||
|
.withIssuer(config.getString("auth.issuer"))
|
||||||
|
.withAudience(config.getString("auth.audience"))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
//TODO reduce cache to a minimum amount, as JWT should be self-contained
|
||||||
|
private val cache: Cache[String, User] = Caffeine.newBuilder()
|
||||||
|
.maximumSize(10_000)
|
||||||
|
.expireAfterWrite(5, TimeUnit.MINUTES).build()
|
||||||
|
|
||||||
|
override def createSession(user: User): String = {
|
||||||
|
//Write session identifier to cache and DB
|
||||||
|
val sessionId = JWT.create()
|
||||||
|
.withIssuer(config.getString("auth.issuer"))
|
||||||
|
.withAudience(config.getString("auth.audience"))
|
||||||
|
.withSubject(user.id.toString)
|
||||||
|
.withClaim("id", user.internalId)
|
||||||
|
.withExpiresAt(Instant.now.plus(7, ChronoUnit.DAYS))
|
||||||
|
.sign(algorithm)
|
||||||
|
//TODO write to Redis and DB
|
||||||
|
cache.put(sessionId, user)
|
||||||
|
|
||||||
|
sessionId
|
||||||
|
}
|
||||||
|
|
||||||
|
override def getUserBySession(sessionId: String): Option[User] = {
|
||||||
|
val cachedUser = cache.getIfPresent(sessionId)
|
||||||
|
if (cachedUser != null) {
|
||||||
|
Some(cachedUser)
|
||||||
|
} else {
|
||||||
|
val result = Try {
|
||||||
|
val decoded = verifier.verify(sessionId)
|
||||||
|
val user = userManager.userExistsById(decoded.getClaim("id").asLong())
|
||||||
|
user.foreach(u => cache.put(sessionId, u))
|
||||||
|
user
|
||||||
|
}
|
||||||
|
if (result.isSuccess) {
|
||||||
|
result.get
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def invalidateSession(sessionId: String): Unit = {
|
||||||
|
//TODO remove from Redis and DB
|
||||||
|
cache.invalidate(sessionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
57
knockoutwhistweb/app/logic/user/impl/StubUserManager.scala
Normal file
57
knockoutwhistweb/app/logic/user/impl/StubUserManager.scala
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package logic.user.impl
|
||||||
|
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import logic.user.UserManager
|
||||||
|
import model.users.User
|
||||||
|
import util.UserHash
|
||||||
|
|
||||||
|
import javax.inject.{Inject, Singleton}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class StubUserManager @Inject()(val config: Config) extends UserManager {
|
||||||
|
|
||||||
|
private val user: Map[String, User] = Map(
|
||||||
|
"Janis" -> User(
|
||||||
|
internalId = 1L,
|
||||||
|
id = java.util.UUID.fromString("123e4567-e89b-12d3-a456-426614174000"),
|
||||||
|
name = "Janis",
|
||||||
|
passwordHash = UserHash.hashPW("password123")
|
||||||
|
),
|
||||||
|
"Leon" -> User(
|
||||||
|
internalId = 2L,
|
||||||
|
id = java.util.UUID.fromString("223e4567-e89b-12d3-a456-426614174000"),
|
||||||
|
name = "Leon",
|
||||||
|
passwordHash = UserHash.hashPW("password123")
|
||||||
|
),
|
||||||
|
"Jakob" -> User(
|
||||||
|
internalId = 2L,
|
||||||
|
id = java.util.UUID.fromString("323e4567-e89b-12d3-a456-426614174000"),
|
||||||
|
name = "Jakob",
|
||||||
|
passwordHash = UserHash.hashPW("password123")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def addUser(name: String, password: String): Boolean = {
|
||||||
|
throw new NotImplementedError("StubUserManager.addUser is not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override def authenticate(name: String, password: String): Option[User] = {
|
||||||
|
user.get(name) match {
|
||||||
|
case Some(u) if UserHash.verifyUser(password, u) => Some(u)
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def userExists(name: String): Option[User] = {
|
||||||
|
user.get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def userExistsById(id: Long): Option[User] = {
|
||||||
|
user.values.find(_.internalId == id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def removeUser(name: String): Boolean = {
|
||||||
|
throw new NotImplementedError("StubUserManager.removeUser is not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
10
knockoutwhistweb/app/model/sessions/InteractionType.scala
Normal file
10
knockoutwhistweb/app/model/sessions/InteractionType.scala
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package model.sessions
|
||||||
|
|
||||||
|
enum InteractionType {
|
||||||
|
|
||||||
|
case TrumpSuit
|
||||||
|
case Card
|
||||||
|
case DogCard
|
||||||
|
case TieChoice
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package controllers.sessions
|
package model.sessions
|
||||||
|
|
||||||
import de.knockoutwhist.utils.events.SimpleEvent
|
import de.knockoutwhist.utils.events.SimpleEvent
|
||||||
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package controllers.sessions
|
package model.sessions
|
||||||
|
|
||||||
import de.knockoutwhist.player.AbstractPlayer
|
import de.knockoutwhist.player.AbstractPlayer
|
||||||
import de.knockoutwhist.utils.events.SimpleEvent
|
import de.knockoutwhist.utils.events.SimpleEvent
|
||||||
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
case class AdvancedSession(id: UUID, player: AbstractPlayer) extends PlayerSession {
|
case class SimpleSession(id: UUID, player: AbstractPlayer) extends PlayerSession {
|
||||||
|
|
||||||
def name: String = player.name
|
def name: String = player.name
|
||||||
|
|
||||||
35
knockoutwhistweb/app/model/sessions/UserSession.scala
Normal file
35
knockoutwhistweb/app/model/sessions/UserSession.scala
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package model.sessions
|
||||||
|
|
||||||
|
import de.knockoutwhist.events.player.{RequestCardEvent, RequestTieChoiceEvent, RequestTrumpSuitEvent}
|
||||||
|
import de.knockoutwhist.utils.events.SimpleEvent
|
||||||
|
import model.users.User
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.locks.{Lock, ReentrantLock}
|
||||||
|
|
||||||
|
class UserSession(user: User, val host: Boolean) extends PlayerSession {
|
||||||
|
var canInteract: Option[InteractionType] = None
|
||||||
|
val lock: ReentrantLock = ReentrantLock()
|
||||||
|
|
||||||
|
override def updatePlayer(event: SimpleEvent): Unit = {
|
||||||
|
event match {
|
||||||
|
case event: RequestTrumpSuitEvent =>
|
||||||
|
canInteract = Some(InteractionType.TrumpSuit)
|
||||||
|
case event: RequestTieChoiceEvent =>
|
||||||
|
canInteract = Some(InteractionType.TieChoice)
|
||||||
|
case event: RequestCardEvent =>
|
||||||
|
if (event.player.isInDogLife) canInteract = Some(InteractionType.DogCard)
|
||||||
|
else canInteract = Some(InteractionType.Card)
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def id: UUID = user.id
|
||||||
|
|
||||||
|
override def name: String = user.name
|
||||||
|
|
||||||
|
def resetCanInteract(): Unit = {
|
||||||
|
canInteract = None
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
20
knockoutwhistweb/app/model/users/User.scala
Normal file
20
knockoutwhistweb/app/model/users/User.scala
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package model.users
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
case class User(
|
||||||
|
internalId: Long,
|
||||||
|
id: UUID,
|
||||||
|
name: String,
|
||||||
|
passwordHash: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
def withName(newName: String): User = {
|
||||||
|
this.copy(name = newName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def withPasswordHash(newPasswordHash: String): User = {
|
||||||
|
this.copy(passwordHash = newPasswordHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
56
knockoutwhistweb/app/services/JwtKeyProvider.scala
Normal file
56
knockoutwhistweb/app/services/JwtKeyProvider.scala
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import play.api.Configuration
|
||||||
|
|
||||||
|
import java.nio.file.{Files, Paths}
|
||||||
|
import java.security.interfaces.{RSAPrivateKey, RSAPublicKey}
|
||||||
|
import java.security.spec.{PKCS8EncodedKeySpec, RSAPublicKeySpec, X509EncodedKeySpec}
|
||||||
|
import java.security.{KeyFactory, KeyPair, PrivateKey, PublicKey}
|
||||||
|
import java.util.Base64
|
||||||
|
import javax.inject.*
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class JwtKeyProvider @Inject()(config: Configuration) {
|
||||||
|
|
||||||
|
private def cleanPem(pem: String): String =
|
||||||
|
pem.replaceAll("-----BEGIN (.*)-----", "")
|
||||||
|
.replaceAll("-----END (.*)-----", "")
|
||||||
|
.replaceAll("\\s", "")
|
||||||
|
|
||||||
|
private def loadPublicKeyFromPem(pem: String): RSAPublicKey = {
|
||||||
|
val decoded = Base64.getDecoder.decode(cleanPem(pem))
|
||||||
|
val spec = new X509EncodedKeySpec(decoded)
|
||||||
|
KeyFactory.getInstance("RSA").generatePublic(spec).asInstanceOf[RSAPublicKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
private def loadPrivateKeyFromPem(pem: String): RSAPrivateKey = {
|
||||||
|
val decoded = Base64.getDecoder.decode(cleanPem(pem))
|
||||||
|
val spec = new PKCS8EncodedKeySpec(decoded)
|
||||||
|
KeyFactory.getInstance("RSA").generatePrivate(spec).asInstanceOf[RSAPrivateKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
val publicKey: RSAPublicKey = {
|
||||||
|
val pemOpt = config.getOptional[String]("auth.publicKeyPem")
|
||||||
|
val fileOpt = config.getOptional[String]("auth.publicKeyFile")
|
||||||
|
|
||||||
|
pemOpt.orElse(fileOpt.map { path =>
|
||||||
|
new String(Files.readAllBytes(Paths.get(path)))
|
||||||
|
}) match {
|
||||||
|
case Some(pem) => loadPublicKeyFromPem(pem)
|
||||||
|
case None => throw new RuntimeException("No RSA public key configured.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val privateKey: RSAPrivateKey = {
|
||||||
|
val pemOpt = config.getOptional[String]("auth.privateKeyPem")
|
||||||
|
val fileOpt = config.getOptional[String]("auth.privateKeyFile")
|
||||||
|
|
||||||
|
pemOpt.orElse(fileOpt.map { path =>
|
||||||
|
new String(Files.readAllBytes(Paths.get(path)))
|
||||||
|
}) match {
|
||||||
|
case Some(pem) => loadPrivateKeyFromPem(pem)
|
||||||
|
case None => throw new RuntimeException("No RSA private key configured.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
29
knockoutwhistweb/app/util/GameUtil.scala
Normal file
29
knockoutwhistweb/app/util/GameUtil.scala
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import scala.util.Random
|
||||||
|
|
||||||
|
object GameUtil {
|
||||||
|
|
||||||
|
private val CharPool: String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
private val CodeLength: Int = 6
|
||||||
|
private val MaxRepetition: Int = 2
|
||||||
|
private val random = new Random()
|
||||||
|
|
||||||
|
def generateCode(): String = {
|
||||||
|
val freq = Array.fill(CharPool.length)(0)
|
||||||
|
val code = new StringBuilder(CodeLength)
|
||||||
|
|
||||||
|
for (_ <- 0 until CodeLength) {
|
||||||
|
var index = random.nextInt(CharPool.length)
|
||||||
|
// Pick a new character if it's already used twice
|
||||||
|
while (freq(index) >= MaxRepetition) {
|
||||||
|
index = random.nextInt(CharPool.length)
|
||||||
|
}
|
||||||
|
freq(index) += 1
|
||||||
|
code.append(CharPool.charAt(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
code.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
23
knockoutwhistweb/app/util/UserHash.scala
Normal file
23
knockoutwhistweb/app/util/UserHash.scala
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import de.mkammerer.argon2.Argon2Factory
|
||||||
|
import de.mkammerer.argon2.Argon2Factory.Argon2Types
|
||||||
|
import model.users.User
|
||||||
|
|
||||||
|
object UserHash {
|
||||||
|
private val ITERATIONS: Int = 3
|
||||||
|
private val MEMORY: Int = 32_768
|
||||||
|
private val PARALLELISM: Int = 1
|
||||||
|
private val SALT_LENGTH: Int = 32
|
||||||
|
private val HASH_LENGTH: Int = 64
|
||||||
|
private val ARGON_2 = Argon2Factory.create(Argon2Types.ARGON2id, SALT_LENGTH, HASH_LENGTH)
|
||||||
|
|
||||||
|
def hashPW(password: String): String = {
|
||||||
|
ARGON_2.hash(ITERATIONS, MEMORY, PARALLELISM, password.toCharArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
def verifyUser(password: String, user: User): Boolean = {
|
||||||
|
ARGON_2.verify(user.passwordHash, password.toCharArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -29,6 +29,6 @@ object WebUIUtils {
|
|||||||
case Three => "3"
|
case Three => "3"
|
||||||
case Two => "2"
|
case Two => "2"
|
||||||
}
|
}
|
||||||
views.html.output.card.apply(f"images/cards/$cv$s.png")(card.toString)
|
views.html.render.card.apply(f"images/cards/$cv$s.png")(card.toString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
@main("Welcome to Play") {
|
|
||||||
<h1>Welcome to Play!</h1>
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic)
|
|
||||||
|
|
||||||
@main("Ingame") {
|
|
||||||
<div id="ingame">
|
|
||||||
<h1>Knockout Whist</h1>
|
|
||||||
<div id="nextPlayers">
|
|
||||||
<p>Next Player:</p>
|
|
||||||
<p>@logic.getPlayerQueue.get.duplicate().nextPlayer()</p>
|
|
||||||
</div>
|
|
||||||
<div id="firstCard">
|
|
||||||
<div id="trumpsuit">
|
|
||||||
<p>Trumpsuit: </p>
|
|
||||||
<p>@logic.getCurrentRound.get.trumpSuit</p>
|
|
||||||
</div>
|
|
||||||
<div id="firstCardObject">
|
|
||||||
<p>First Card</p>
|
|
||||||
@if(logic.getCurrentTrick.get.firstCard.isDefined) {
|
|
||||||
@util.WebUIUtils.cardtoImage(logic.getCurrentTrick.get.firstCard.get)
|
|
||||||
} else {
|
|
||||||
@views.html.output.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>
|
|
||||||
}
|
|
||||||
93
knockoutwhistweb/app/views/ingame/ingame.scala.html
Normal file
93
knockoutwhistweb/app/views/ingame/ingame.scala.html
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
@import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.TrickUtil
|
||||||
|
|
||||||
|
@(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
|
||||||
|
|
||||||
|
@main("Ingame") {
|
||||||
|
<div class="lobby-background vh-100">
|
||||||
|
<main class="game-field-background vh-100">
|
||||||
|
<div class="py-5 container-xxl">
|
||||||
|
|
||||||
|
<div class="row ms-4 me-4">
|
||||||
|
<div class="col-4 mt-5 text-start">
|
||||||
|
<h4 class="fw-semibold mb-1">Current Player</h4>
|
||||||
|
<p class="fs-5 text-primary" id="current-player-name">@gamelobby.getLogic.getCurrentPlayer.get.name</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" 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')">
|
||||||
|
@util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
pollForUpdates('@gamelobby.id');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic)
|
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic)
|
||||||
|
|
||||||
@main("Selecting Trumpsuit...") {
|
@main("Selecting Trumpsuit...") {
|
||||||
<div id="selecttrumpsuit">
|
<div id="selecttrumpsuit" class="game-field game-field-background">
|
||||||
@if(player.equals(logic.getCurrentMatch.get.roundlist.last.winner.get)) {
|
@if(player.equals(logic.getCurrentMatch.get.roundlist.last.winner.get)) {
|
||||||
<h1>Knockout Whist</h1>
|
<h1>Knockout Whist</h1>
|
||||||
<p>You (@player.toString) have won the last round! Select a trumpsuit for the next round!</p>
|
<p>You (@player.toString) have won the last round! Select a trumpsuit for the next round!</p>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic)
|
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic)
|
||||||
|
|
||||||
@main("Tie") {
|
@main("Tie") {
|
||||||
<div id="tie">
|
<div id="tie" class="game-field game-field-background">
|
||||||
<h1>Knockout Whist</h1>
|
<h1>Knockout Whist</h1>
|
||||||
<p>The last Round was tied between
|
<p>The last Round was tied between
|
||||||
@for(players <- logic.playerTieLogic.getTiedPlayers) {
|
@for(players <- logic.playerTieLogic.getTiedPlayers) {
|
||||||
74
knockoutwhistweb/app/views/lobby/lobby.scala.html
Normal file
74
knockoutwhistweb/app/views/lobby/lobby.scala.html
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
@(user: Option[model.users.User], gamelobby: logic.game.GameLobby)
|
||||||
|
|
||||||
|
@main("Lobby") {
|
||||||
|
<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">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)) {
|
||||||
|
@for(playersession <- gamelobby.getPlayers.values) {
|
||||||
|
<div class="col-auto my-auto">
|
||||||
|
<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>
|
||||||
|
@* <p class="card-text">Your text could be here!</p>*@
|
||||||
|
<a href="#" class="btn btn-danger disabled" aria-disabled="true" tabindex="-1">Remove</a>
|
||||||
|
} else {
|
||||||
|
<h5 class="card-title">@playersession.name</h5>
|
||||||
|
@* <p class="card-text">Your text could be here!</p>*@
|
||||||
|
<div class="btn btn-danger" onclick="removePlayer('@gamelobby.id', '@playersession.id')">Remove</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 {
|
||||||
|
@for(playersession <- gamelobby.getPlayers.values) {
|
||||||
|
<div class="col-auto my-auto"> <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 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>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
pollForUpdates('@gamelobby.id');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
47
knockoutwhistweb/app/views/login/login.scala.html
Normal file
47
knockoutwhistweb/app/views/login/login.scala.html
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
@()
|
||||||
|
|
||||||
|
@main("Login") {
|
||||||
|
|
||||||
|
<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 action="@routes.UserController.login_Post()" method="post">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label text-body">Password</label>
|
||||||
|
<input type="password" class="form-control text-body" id="password" name="password" placeholder="Enter password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<a href="#" class="text-decoration-none">Forgot password?</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary">Login</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="text-center mt-3">
|
||||||
|
Don’t have an account?
|
||||||
|
<a href="#" class="text-decoration-none">Sign up</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="@routes.Assets.versioned("/javascripts/particles.js")"></script>
|
||||||
|
<script>
|
||||||
|
particlesJS.load('particles-js', 'assets/conf/particlesjs-config.json', function() {
|
||||||
|
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>
|
||||||
|
}
|
||||||
@@ -9,17 +9,21 @@
|
|||||||
<!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">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body class="d-flex flex-column min-vh-100">
|
||||||
@* 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.JavaScriptRoutingController.javascriptRoutes()" type="text/javascript"></script>
|
||||||
<script src="@routes.Assets.versioned("javascripts/main.js")" 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>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
32
knockoutwhistweb/app/views/mainmenu/creategame.scala.html
Normal file
32
knockoutwhistweb/app/views/mainmenu/creategame.scala.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
@(user: Option[model.users.User])
|
||||||
|
|
||||||
|
@main("Create Game") {
|
||||||
|
@navbar(user)
|
||||||
|
<main class="lobby-background flex-grow-1">
|
||||||
|
<div class="w-50 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>
|
||||||
|
}
|
||||||
57
knockoutwhistweb/app/views/mainmenu/navbar.scala.html
Normal file
57
knockoutwhistweb/app/views/mainmenu/navbar.scala.html
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
@(user: Option[model.users.User])
|
||||||
|
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||||
|
<div class="container d-flex justify-content-left">
|
||||||
|
<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>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse justify-content-center" id="navBar">
|
||||||
|
<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">
|
||||||
|
<ul class="navbar-nav mb-2 mb-lg-0">
|
||||||
|
@if(user.isDefined) {
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" aria-current="page" href="@routes.MainMenuController.mainMenu()">Create Game</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link disabled" aria-disabled="true">Lobbies</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" href="@routes.MainMenuController.rules()">Rules</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<form class="navbar-nav me-auto mb-2 mb-lg-0" method="post" action="@routes.MainMenuController.joinGame()">
|
||||||
|
<input class="form-control me-2" type="text" placeholder="Enter GameCode" name="gameId" aria-label="Join Game"/>
|
||||||
|
<button class="btn btn-outline-success" type="submit">Join</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* Right side: profile dropdown if logged in, otherwise Login / Sign Up buttons *@
|
||||||
|
@if(user.isDefined) {
|
||||||
|
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle d-flex align-items-center" href="#" id="profileDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<img src="@routes.Assets.versioned("images/profile.png")" alt="Profile" class="rounded-circle" width="30" height="30" />
|
||||||
|
<span class="ms-2">@user.get.name</span>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="profileDropdown">
|
||||||
|
<li><a class="dropdown-item disabled" href="#" tabindex="-1" aria-disabled="true">Stats</a></li>
|
||||||
|
<li><a class="dropdown-item disabled" href="#" tabindex="-1" aria-disabled="true">Settings</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><a class="dropdown-item" href="@routes.UserController.logout()">Logout</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
} else {
|
||||||
|
<div class="d-flex ms-auto">
|
||||||
|
<a class="btn btn-outline-primary me-2" href="@routes.UserController.login()">Login</a>
|
||||||
|
<a class="btn btn-primary" href="@routes.UserController.login()">Sign Up</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
76
knockoutwhistweb/app/views/mainmenu/rules.scala.html
Normal file
76
knockoutwhistweb/app/views/mainmenu/rules.scala.html
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
@(user: Option[model.users.User])
|
||||||
|
|
||||||
|
@main("Rules") {
|
||||||
|
@navbar(user)
|
||||||
|
<div id="rules">
|
||||||
|
<div class="container my-4">
|
||||||
|
<div class="card shadow-sm rounded-3">
|
||||||
|
<div class="card-header text-white text-center">
|
||||||
|
<h4 class="mb-0 text-body">Game Rules Overview</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover mb-0 align-middle">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Section</th>
|
||||||
|
<th scope="col">Details</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Players</td>
|
||||||
|
<td>Two to seven players. The aim is to be the last player left in the game.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Aim</td>
|
||||||
|
<td>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.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Equipment</td>
|
||||||
|
<td>A standard 52-card pack is used.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Card Ranks</td>
|
||||||
|
<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>
|
||||||
|
<tr>
|
||||||
|
<td>Deal (First Hand)</td>
|
||||||
|
<td>The dealer deals seven cards to each player. One card is turned up to determine the trump suit for the round.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Deal (Subsequent Hands)</td>
|
||||||
|
<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, 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>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Play</td>
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Winning a Trick</td>
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Leading Trumps</td>
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Knockout</td>
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Winning the Game</td>
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Dog Life</td>
|
||||||
|
<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 the next and the dog re-enters the game properly in the next hand. If the dog fails, they are knocked out.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
@(src: String)(alt: String)
|
|
||||||
<img src="@routes.Assets.versioned(src)" alt="@alt"/>
|
|
||||||
2
knockoutwhistweb/app/views/render/card.scala.html
Normal file
2
knockoutwhistweb/app/views/render/card.scala.html
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
@(src: String)(alt: String)
|
||||||
|
<img src="@routes.Assets.versioned(src)" alt="@alt"
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
@()
|
|
||||||
|
|
||||||
@main("Rules") {
|
|
||||||
<div id="rules">
|
|
||||||
<table>
|
|
||||||
<caption>Rules Overview and Equipment</caption>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Section</th>
|
|
||||||
<th>Details</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Players</td>
|
|
||||||
<td>Two to seven players. The aim is to be the last player left in the game.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Aim</td>
|
|
||||||
<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>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Equipment</td>
|
|
||||||
<td>A standard 52-card pack is used.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Card Ranks</td>
|
|
||||||
<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>
|
|
||||||
<tr>
|
|
||||||
<td>Deal (First Hand)</td>
|
|
||||||
<td>The dealer deals seven cards to each player. One card is turned up to determine the trump suit for the round.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Deal (Subsequent Hands)</td>
|
|
||||||
<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>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Play</td>
|
|
||||||
<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>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Winning a Trick</td>
|
|
||||||
<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>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Leading Trumps</td>
|
|
||||||
<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>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Knockout</td>
|
|
||||||
<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>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Winning the Game</td>
|
|
||||||
<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>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
<td>Dog Life</td>
|
|
||||||
<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>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
@(sessions: List[controllers.sessions.PlayerSession])
|
|
||||||
|
|
||||||
@main("Sessions") {
|
|
||||||
<div id="sessions">
|
|
||||||
<h1>Knockout Whist sessions</h1>
|
|
||||||
<p id="textanimation">Please select your session to jump inside the game!</p>
|
|
||||||
@for(session <- sessions) {
|
|
||||||
<a id="textanimation" href="@routes.HomeController.ingame(session.id.toString)">@session.name</a><br>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
@(toRender: List[Html])
|
|
||||||
|
|
||||||
@main("Tui") {
|
|
||||||
<div id="tui">
|
|
||||||
@for(line <- toRender) {
|
|
||||||
@line
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1 +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.hosts.AllowedHostsFilter
|
||||||
|
|
||||||
|
play.http.secret.key="QCY?tAnfk?aZ?iwrNwnxIlR6CTf:G3gf:90Latabg@5241AB`R5W:1uDFN];Ik@n"
|
||||||
|
play.http.secret.key=${?APPLICATION_SECRET}
|
||||||
|
|
||||||
|
auth {
|
||||||
|
issuer = "knockoutwhistweb"
|
||||||
|
audience = "ui"
|
||||||
|
privateKeyFile = ${?PRIVATE_KEY_FILE}
|
||||||
|
privateKeyPem = ${?PRIVATE_KEY_PEM}
|
||||||
|
publicKeyFile = ${?PUBLIC_KEY_FILE}
|
||||||
|
publicKeyPem = ${?PUBLIC_KEY_PEM}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,12 +3,31 @@
|
|||||||
# https://www.playframework.com/documentation/latest/ScalaRouting
|
# https://www.playframework.com/documentation/latest/ScalaRouting
|
||||||
# ~~~~
|
# ~~~~
|
||||||
|
|
||||||
# An example controller showing a sample home page
|
# For the javascript routing
|
||||||
|
GET /assets/js/routes controllers.JavaScriptRoutingController.javascriptRoutes()
|
||||||
GET / controllers.HomeController.index()
|
# Primary routes
|
||||||
GET /sessions controllers.HomeController.sessions()
|
GET / controllers.MainMenuController.index()
|
||||||
GET /ingame/:id controllers.HomeController.ingame(id: String)
|
|
||||||
# Map static resources from the /public folder to the /assets URL path
|
|
||||||
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
|
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
|
||||||
|
|
||||||
GET /rules controllers.HomeController.rules()
|
# Main menu routes
|
||||||
|
GET /mainmenu controllers.MainMenuController.mainMenu()
|
||||||
|
GET /rules controllers.MainMenuController.rules()
|
||||||
|
|
||||||
|
POST /createGame controllers.MainMenuController.createGame()
|
||||||
|
POST /joinGame controllers.MainMenuController.joinGame()
|
||||||
|
|
||||||
|
# User authentication routes
|
||||||
|
GET /login controllers.UserController.login()
|
||||||
|
POST /login controllers.UserController.login_Post()
|
||||||
|
|
||||||
|
GET /logout controllers.UserController.logout()
|
||||||
|
|
||||||
|
# In-game routes
|
||||||
|
GET /game/:id controllers.IngameController.game(id: String)
|
||||||
|
GET /game/:id/join controllers.IngameController.joinGame(id: String)
|
||||||
|
GET /game/:id/start controllers.IngameController.startGame(id: String)
|
||||||
|
POST /game/:id/kickPlayer controllers.IngameController.kickPlayer(id: String, playerId: String)
|
||||||
|
GET /game/:id/leaveGame controllers.IngameController.leaveGame(id: String)
|
||||||
|
POST /game/:id/playCard controllers.IngameController.playCard(id: String)
|
||||||
|
# Polling
|
||||||
|
GET /polling controllers.IngameController.polling(gameId: String)
|
||||||
110
knockoutwhistweb/public/conf/particlesjs-config.json
Normal file
110
knockoutwhistweb/public/conf/particlesjs-config.json
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"particles": {
|
||||||
|
"number": {
|
||||||
|
"value": 80,
|
||||||
|
"density": {
|
||||||
|
"enable": true,
|
||||||
|
"value_area": 800
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"value": "#ffffff"
|
||||||
|
},
|
||||||
|
"shape": {
|
||||||
|
"type": "circle",
|
||||||
|
"stroke": {
|
||||||
|
"width": 0,
|
||||||
|
"color": "#000000"
|
||||||
|
},
|
||||||
|
"polygon": {
|
||||||
|
"nb_sides": 5
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"src": "img/github.svg",
|
||||||
|
"width": 100,
|
||||||
|
"height": 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"opacity": {
|
||||||
|
"value": 0.5,
|
||||||
|
"random": false,
|
||||||
|
"anim": {
|
||||||
|
"enable": false,
|
||||||
|
"speed": 1,
|
||||||
|
"opacity_min": 0.1,
|
||||||
|
"sync": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"value": 3,
|
||||||
|
"random": true,
|
||||||
|
"anim": {
|
||||||
|
"enable": false,
|
||||||
|
"speed": 40,
|
||||||
|
"size_min": 0.1,
|
||||||
|
"sync": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"line_linked": {
|
||||||
|
"enable": true,
|
||||||
|
"distance": 150,
|
||||||
|
"color": "#ffffff",
|
||||||
|
"opacity": 0.4,
|
||||||
|
"width": 1
|
||||||
|
},
|
||||||
|
"move": {
|
||||||
|
"enable": true,
|
||||||
|
"speed": 1,
|
||||||
|
"direction": "none",
|
||||||
|
"random": false,
|
||||||
|
"straight": false,
|
||||||
|
"out_mode": "out",
|
||||||
|
"bounce": false,
|
||||||
|
"attract": {
|
||||||
|
"enable": false,
|
||||||
|
"rotateX": 600,
|
||||||
|
"rotateY": 1200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"interactivity": {
|
||||||
|
"detect_on": "canvas",
|
||||||
|
"events": {
|
||||||
|
"onhover": {
|
||||||
|
"enable": false,
|
||||||
|
"mode": "repulse"
|
||||||
|
},
|
||||||
|
"onclick": {
|
||||||
|
"enable": false,
|
||||||
|
"mode": "push"
|
||||||
|
},
|
||||||
|
"resize": true
|
||||||
|
},
|
||||||
|
"modes": {
|
||||||
|
"grab": {
|
||||||
|
"distance": 400,
|
||||||
|
"line_linked": {
|
||||||
|
"opacity": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bubble": {
|
||||||
|
"distance": 400,
|
||||||
|
"size": 40,
|
||||||
|
"duration": 2,
|
||||||
|
"opacity": 8,
|
||||||
|
"speed": 3
|
||||||
|
},
|
||||||
|
"repulse": {
|
||||||
|
"distance": 200,
|
||||||
|
"duration": 0.4
|
||||||
|
},
|
||||||
|
"push": {
|
||||||
|
"particles_nb": 4
|
||||||
|
},
|
||||||
|
"remove": {
|
||||||
|
"particles_nb": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"retina_detect": true
|
||||||
|
}
|
||||||
BIN
knockoutwhistweb/public/images/logo.png
Normal file
BIN
knockoutwhistweb/public/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
knockoutwhistweb/public/images/profile.png
Normal file
BIN
knockoutwhistweb/public/images/profile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,432 @@
|
|||||||
|
/*!
|
||||||
|
* 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
|
||||||
|
function pollForUpdates(gameId) {
|
||||||
|
if (!gameId) {
|
||||||
|
console.error("Game ID is missing. Stopping poll.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const element = document.getElementById('card-slide');
|
||||||
|
const element2 = document.getElementById('lobbybackground');
|
||||||
|
// Safety check for the target element
|
||||||
|
if (!element && !element2) {
|
||||||
|
console.error("Polling target element not found. Stopping poll.");
|
||||||
|
// Use a timeout to retry in case the DOM loads late, passing gameId.
|
||||||
|
setTimeout(() => pollForUpdates(gameId), 5000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const route = jsRoutes.controllers.IngameController.polling(gameId);
|
||||||
|
|
||||||
|
// Call your specific controller endpoint
|
||||||
|
fetch(route.url)
|
||||||
|
.then(response => {
|
||||||
|
if (response.status === 204) {
|
||||||
|
console.log("Polling: Timeout reached. Restarting poll.");
|
||||||
|
|
||||||
|
// CRITICAL: Pass gameId in the recursive call
|
||||||
|
setTimeout(() => pollForUpdates(gameId), 5000);
|
||||||
|
} else if (response.ok && response.status === 200) {
|
||||||
|
response.json().then(data => {
|
||||||
|
|
||||||
|
if (data.status === "cardPlayed" && data.handData) {
|
||||||
|
console.log("Event received: Card played. Redrawing hand.");
|
||||||
|
|
||||||
|
const newHand = data.handData;
|
||||||
|
let newHandHTML = '';
|
||||||
|
element.innerHTML = '';
|
||||||
|
|
||||||
|
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}')">
|
||||||
|
|
||||||
|
<img src="/assets/images/cards/${cardId}.png" width="120px" style="border-radius: 6px"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
newHandHTML += cardHtml;
|
||||||
|
});
|
||||||
|
|
||||||
|
element.innerHTML = newHandHTML;
|
||||||
|
|
||||||
|
const currentPlayerElement = document.getElementById('current-player-name');
|
||||||
|
if (currentPlayerElement) {
|
||||||
|
currentPlayerElement.textContent = data.currentPlayerName;
|
||||||
|
}
|
||||||
|
const nextPlayerElement = document.getElementById('next-player-name');
|
||||||
|
if (nextPlayerElement && data.nextPlayer) {
|
||||||
|
// Use the correctly named field from the server response
|
||||||
|
nextPlayerElement.textContent = data.nextPlayer;
|
||||||
|
} else {
|
||||||
|
// Case 2: Player name is empty or null (signal to clear display).
|
||||||
|
nextPlayerElement.textContent = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const trumpElement = document.getElementById('trump-suit');
|
||||||
|
if (trumpElement) {
|
||||||
|
trumpElement.textContent = data.trumpSuit;
|
||||||
|
}
|
||||||
|
const trickContainer = document.getElementById('trick-cards-container');
|
||||||
|
if (trickContainer) {
|
||||||
|
let trickHTML = '';
|
||||||
|
|
||||||
|
// Iterate over the array of played cards received from the server
|
||||||
|
data.trickCards.forEach(trickCard => {
|
||||||
|
// Reconstruct the HTML structure from your template
|
||||||
|
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>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
trickContainer.innerHTML = trickHTML;
|
||||||
|
}
|
||||||
|
const scoreBody = document.getElementById('score-table-body');
|
||||||
|
if (scoreBody && 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>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
scoreBody.innerHTML = scoreHTML;
|
||||||
|
}
|
||||||
|
const firstCardContainer = document.getElementById('first-card-container');
|
||||||
|
const cardId = data.firstCardId; // This will be "KH", "S7", or "BLANK"
|
||||||
|
|
||||||
|
if (firstCardContainer) {
|
||||||
|
let imageSrc = '';
|
||||||
|
let altText = 'First Card';
|
||||||
|
|
||||||
|
// Check if a card was actually played or if it's the start of a trick
|
||||||
|
if (cardId === "BLANK") {
|
||||||
|
imageSrc = "/assets/images/cards/1B.png";
|
||||||
|
altText = "Blank Card";
|
||||||
|
} else {
|
||||||
|
imageSrc = `/assets/images/cards/${cardId}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconstruct the image HTML (assuming the inner element needs replacement)
|
||||||
|
const newImageHTML = `
|
||||||
|
<img src="${imageSrc}" alt="${altText}" width="80px" style="border-radius: 6px"/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Clear the container and insert the new image
|
||||||
|
firstCardContainer.innerHTML = newImageHTML;
|
||||||
|
}
|
||||||
|
} else if (data.status === "gameStart") {
|
||||||
|
window.location.href = data.redirectUrl;
|
||||||
|
}
|
||||||
|
pollForUpdates(gameId);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Handle network or server errors
|
||||||
|
console.error(`Polling error: Status ${response.status}`);
|
||||||
|
// Wait before retrying, passing gameId correctly
|
||||||
|
setTimeout(() => pollForUpdates(gameId), 5000);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Network error during polling:", error);
|
||||||
|
// Wait before retrying on network failure, passing gameId correctly
|
||||||
|
setTimeout(() => pollForUpdates(gameId), 5000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGameJS() {
|
||||||
|
let lobbyName = document.getElementById("lobbyname").value;
|
||||||
|
if (lobbyName === "") {
|
||||||
|
lobbyName = "DefaultLobby"
|
||||||
|
}
|
||||||
|
const playerAmount = document.getElementById("playeramount").value;
|
||||||
|
const jsonObj = {
|
||||||
|
lobbyname: lobbyName,
|
||||||
|
playeramount: playerAmount
|
||||||
|
}
|
||||||
|
sendGameCreationRequest(jsonObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendGameCreationRequest(dataObject) {
|
||||||
|
const route = jsRoutes.controllers.MainMenuController.createGame();
|
||||||
|
|
||||||
|
fetch(route.url, {
|
||||||
|
method: route.type,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(dataObject)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
return response.json().then(data => {
|
||||||
|
if (!response.ok) {
|
||||||
|
return Promise.reject(data);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
window.location.href = data.redirectUrl;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error && error.errorMessage) {
|
||||||
|
alert(`${error.errorMessage}`);
|
||||||
|
} else {
|
||||||
|
alert('An unexpected error occurred. Please try again.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function startGame(gameId) {
|
||||||
|
sendGameStartRequest(gameId)
|
||||||
|
}
|
||||||
|
function sendGameStartRequest(gameId) {
|
||||||
|
const route = jsRoutes.controllers.IngameController.startGame(gameId);
|
||||||
|
|
||||||
|
fetch(route.url, {
|
||||||
|
method: route.type,
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
return response.json().then(data => {
|
||||||
|
if (!response.ok) {
|
||||||
|
return Promise.reject(data);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// SUCCESS BLOCK: data is the { status: 'success', ... } object
|
||||||
|
if (data.status === 'success') {
|
||||||
|
window.location.href = data.redirectUrl;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error && error.errorMessage) {
|
||||||
|
alert(`${error.errorMessage}`);
|
||||||
|
} else {
|
||||||
|
alert('An unexpected error occurred. Please try again.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function removePlayer(gameid, playersessionId) {
|
||||||
|
sendRemovePlayerRequest(gameid, playersessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendRemovePlayerRequest(gameId, playersessionId) {
|
||||||
|
const route = jsRoutes.controllers.IngameController.kickPlayer(gameId, playersessionId);
|
||||||
|
|
||||||
|
fetch(route.url, {
|
||||||
|
method: route.type,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
return response.json().then(data => {
|
||||||
|
if (!response.ok) {
|
||||||
|
return Promise.reject(data);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// SUCCESS BLOCK: data is the { status: 'success', ... } object
|
||||||
|
if (data.status === 'success') {
|
||||||
|
window.location.href = data.redirectUrl;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error && 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);
|
||||||
|
fetch(route.url, {
|
||||||
|
method: route.type,
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
return response.json().then(data => {
|
||||||
|
if (!response.ok) {
|
||||||
|
return Promise.reject(data);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// SUCCESS BLOCK: data is the { status: 'success', ... } object
|
||||||
|
if (data.status === 'success') {
|
||||||
|
window.location.href = data.redirectUrl;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error && error.errorMessage) {
|
||||||
|
alert(`${error.errorMessage}`);
|
||||||
|
} else {
|
||||||
|
alert('An unexpected error occurred. Please try again.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePlayCard(cardobject, gameId) {
|
||||||
|
const cardId = cardobject.dataset.cardId;
|
||||||
|
const jsonObj = {
|
||||||
|
cardID: cardId
|
||||||
|
}
|
||||||
|
sendPlayCardRequest(jsonObj, gameId, cardobject)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendPlayCardRequest(jsonObj, gameId, cardobject) {
|
||||||
|
const wiggleKeyframes = [
|
||||||
|
{ transform: 'translateX(0)' },
|
||||||
|
{ transform: 'translateX(-5px)' },
|
||||||
|
{ transform: 'translateX(5px)' },
|
||||||
|
{ transform: 'translateX(-5px)' },
|
||||||
|
{ transform: 'translateX(0)' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Define the timing options
|
||||||
|
const wiggleTiming = {
|
||||||
|
duration: 400, // 0.4 seconds
|
||||||
|
iterations: 1,
|
||||||
|
easing: 'ease-in-out',
|
||||||
|
// Fill mode ensures the final state is applied until reset
|
||||||
|
fill: 'forwards'
|
||||||
|
};
|
||||||
|
const route = jsRoutes.controllers.IngameController.playCard(gameId);
|
||||||
|
|
||||||
|
fetch(route.url, {
|
||||||
|
method: route.type,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(jsonObj)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
return response.json().then(data => {
|
||||||
|
if (!response.ok) {
|
||||||
|
return Promise.reject(data);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
//window.location.href = data.redirectUrl;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error && error.errorMessage.includes("You can't play this card!")) {
|
||||||
|
cardobject.parentElement.animate(wiggleKeyframes, wiggleTiming);
|
||||||
|
} else if (error && error.errorMessage) {
|
||||||
|
alert(`${error.errorMessage}`);
|
||||||
|
} else {
|
||||||
|
alert('An unexpected error occurred. Please try again.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
1541
knockoutwhistweb/public/javascripts/particles.js
Normal file
1541
knockoutwhistweb/public/javascripts/particles.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,33 +13,33 @@ import play.api.test.Helpers.*
|
|||||||
*/
|
*/
|
||||||
class HomeControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting {
|
class HomeControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting {
|
||||||
|
|
||||||
"HomeController GET" should {
|
// "HomeController GET" should {
|
||||||
|
//
|
||||||
"render the index page from a new instance of controller" in {
|
// "render the index page from a new instance of controller" in {
|
||||||
val controller = new HomeController(stubControllerComponents())
|
// val controller = new HomeController(stubControllerComponents())
|
||||||
val home = controller.index().apply(FakeRequest(GET, "/"))
|
// val home = controller.index().apply(FakeRequest(GET, "/"))
|
||||||
|
//
|
||||||
status(home) mustBe OK
|
// status(home) mustBe OK
|
||||||
contentType(home) mustBe Some("text/html")
|
// contentType(home) mustBe Some("text/html")
|
||||||
contentAsString(home) must include ("Welcome to Play")
|
// contentAsString(home) must include ("Welcome to Play")
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
"render the index page from the application" in {
|
// "render the index page from the application" in {
|
||||||
val controller = inject[HomeController]
|
// val controller = inject[HomeController]
|
||||||
val home = controller.index().apply(FakeRequest(GET, "/"))
|
// val home = controller.index().apply(FakeRequest(GET, "/"))
|
||||||
|
//
|
||||||
status(home) mustBe OK
|
// status(home) mustBe OK
|
||||||
contentType(home) mustBe Some("text/html")
|
// contentType(home) mustBe Some("text/html")
|
||||||
contentAsString(home) must include ("Welcome to Play")
|
// contentAsString(home) must include ("Welcome to Play")
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
"render the index page from the router" in {
|
// "render the index page from the router" in {
|
||||||
val request = FakeRequest(GET, "/")
|
// val request = FakeRequest(GET, "/")
|
||||||
val home = route(app, request).get
|
// val home = route(app, request).get
|
||||||
|
//
|
||||||
status(home) mustBe OK
|
// status(home) mustBe OK
|
||||||
contentType(home) mustBe Some("text/html")
|
// contentType(home) mustBe Some("text/html")
|
||||||
contentAsString(home) must include ("Welcome to Play")
|
// contentAsString(home) must include ("Welcome to Play")
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
3
versions.env
Normal file
3
versions.env
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
MAJOR=1
|
||||||
|
MINOR=0
|
||||||
|
PATCH=9
|
||||||
Reference in New Issue
Block a user