Compare commits
39 Commits
webapplica
...
edfba93f83
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edfba93f83 | ||
|
|
4f7eed9ac4 | ||
|
|
ee3f65efd9 | ||
|
|
89d1626bb2 | ||
| 44c88c8f60 | |||
| 96c38466d2 | |||
| 32d4f9c6ce | |||
|
|
5c1a5a661b | ||
|
|
7b97986df9 | ||
| 7879d1ab6e | |||
|
|
1564956107 | ||
| 2e10059a67 | |||
|
|
b8f44c07d4 | ||
| 91d7f6ca00 | |||
|
|
df90abed80 | ||
| 0d2c6f77c8 | |||
| afde6c02da | |||
| bef96ba7e3 | |||
| c0dadf8927 | |||
| 1f377de0f4 | |||
| 6c31fa0538 | |||
| 729a4eec6b | |||
| 72fcf783b8 | |||
| 1517d0c006 | |||
| 7f765b4514 | |||
| 03f1811ab4 | |||
| 63689b7a26 | |||
| 92e4851219 | |||
| c168ae7dc0 | |||
| ccf44ede41 | |||
| d71809d6f4 | |||
| 82245d6bcc | |||
| d8576f669a | |||
|
|
cfe27f1e78 | ||
|
|
b33ab184d2 | ||
|
|
b17c5160e9 | ||
| 7458464dd6 | |||
| f8c337fad1 | |||
| 3357fb7310 |
46
.gitea/ISSUE_TEMPLATE/epic.md
Normal file
46
.gitea/ISSUE_TEMPLATE/epic.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: "Epic"
|
||||
about: "A large initiative or feature that consists of multiple User Stories or Subtasks"
|
||||
title: "[Epic] <Epic title>"
|
||||
labels: ["Type/Epic"]
|
||||
---
|
||||
|
||||
## 🧩 Epic Summary
|
||||
Provide a clear summary of what this Epic aims to achieve.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Goals
|
||||
- [ ] Goal 1
|
||||
- [ ] Goal 2
|
||||
- [ ] Goal 3
|
||||
|
||||
---
|
||||
|
||||
## 📋 Description
|
||||
Describe the high-level context, purpose, and expected outcome of this Epic.
|
||||
|
||||
---
|
||||
|
||||
## 🧵 Related User Stories / Subtasks
|
||||
Link to related issues here:
|
||||
- [ ] #<user-story-1>
|
||||
- [ ] #<user-story-2>
|
||||
- [ ] #<subtask-1>
|
||||
|
||||
---
|
||||
|
||||
## 📅 Milestone / Timeline
|
||||
If applicable, note any key dates or milestones:
|
||||
- Target Start:
|
||||
- Target Completion:
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Risks / Dependencies
|
||||
List any major risks or dependencies that could affect delivery.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Acceptance Criteria
|
||||
Define the success metrics or completion definition for the Epic.
|
||||
35
.gitea/ISSUE_TEMPLATE/subtask.md
Normal file
35
.gitea/ISSUE_TEMPLATE/subtask.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: "Subtask"
|
||||
about: "A smaller task that contributes to a User Story or Epic"
|
||||
title: "[Subtask] <Task title>"
|
||||
labels: ["Type/Subtask"]
|
||||
---
|
||||
|
||||
## 🧾 Description
|
||||
Briefly describe what needs to be done for this subtask.
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Linked Issues
|
||||
- Parent Story: #<user-story-number>
|
||||
- Related Epic: #<epic-number>
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Steps / Tasks
|
||||
- [ ] Task 1
|
||||
- [ ] Task 2
|
||||
- [ ] Task 3
|
||||
|
||||
---
|
||||
|
||||
## ✅ Definition of Done
|
||||
What must be true for this subtask to be considered complete:
|
||||
- [ ] Code implemented
|
||||
- [ ] Tests passed
|
||||
- [ ] Reviewed and merged
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Estimated Size
|
||||
Use label: `Size/XS` | `Size/S` | `Size/M` | `Size/L` | `Size/XL`
|
||||
42
.gitea/ISSUE_TEMPLATE/user_story.md
Normal file
42
.gitea/ISSUE_TEMPLATE/user_story.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: "User Story"
|
||||
about: "A feature or requirement from the user's perspective"
|
||||
title: "[Story] <User story title>"
|
||||
labels: ["Type/User Story"]
|
||||
---
|
||||
|
||||
## 🧍♂️ User Story
|
||||
**As a** [type of user]
|
||||
**I want to** [perform an action]
|
||||
**So that** [achieve a goal or value]
|
||||
|
||||
---
|
||||
|
||||
## 📋 Description
|
||||
Provide additional context or business logic for this story.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Acceptance Criteria
|
||||
List the specific, measurable criteria that define when this story is done:
|
||||
- [ ] Criterion 1
|
||||
- [ ] Criterion 2
|
||||
- [ ] Criterion 3
|
||||
|
||||
---
|
||||
|
||||
## 🧱 Implementation Notes
|
||||
Include technical notes, design references, or constraints.
|
||||
|
||||
---
|
||||
|
||||
## 🧵 Linked Issues
|
||||
- Parent Epic: #<epic-number>
|
||||
- Related Subtasks:
|
||||
- [ ] #<subtask-1>
|
||||
- [ ] #<subtask-2>
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Estimated Size
|
||||
Use label: `Size/XS` | `Size/S` | `Size/M` | `Size/L` | `Size/XL`
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -137,3 +137,4 @@ target
|
||||
/knockoutwhist/
|
||||
/knockoutwhistweb/.g8/
|
||||
/knockoutwhistweb/.bsp/
|
||||
/currentSnapshot.json
|
||||
|
||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,4 +1,4 @@
|
||||
[submodule "knockoutwhist"]
|
||||
path = knockoutwhist
|
||||
branch = webapplication
|
||||
branch = main
|
||||
url = https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist.git
|
||||
|
||||
141
CHANGELOG.md
Normal file
141
CHANGELOG.md
Normal file
@@ -0,0 +1,141 @@
|
||||
## (2025-11-03)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* implemented multigame support (#34)
|
||||
|
||||
### Features
|
||||
|
||||
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
|
||||
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
|
||||
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
|
||||
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
|
||||
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
|
||||
* **ui:** LESS Integration ([72fcf78](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/72fcf783b8ca5ce260e4b152016353e7dc1edb69)), closes [#15-Create-a-default-theme-with-Less-](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/15-Create-a-default-theme-with-Less-) [#23](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/23)
|
||||
* **ui:** UI now shows player names instead of their id ([#11](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/11)) ([c168ae7](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c168ae7dc05e99c1890661aa3b63f4d4d12779fb))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **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))
|
||||
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
|
||||
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
|
||||
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
|
||||
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
|
||||
|
||||
### Reverts
|
||||
|
||||
* ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe))
|
||||
* version bumb ([2e10059](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e10059a6756befe6f8e70c94fd34b865693efb8))
|
||||
* version bump ([7879d1a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7879d1ab6ee8f227c19b69b467a28dd7b479ff73))
|
||||
## (2025-11-03)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* implemented multigame support (#34)
|
||||
|
||||
### Features
|
||||
|
||||
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
|
||||
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
|
||||
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
|
||||
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
|
||||
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
|
||||
* **ui:** LESS Integration ([72fcf78](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/72fcf783b8ca5ce260e4b152016353e7dc1edb69)), closes [#15-Create-a-default-theme-with-Less-](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/15-Create-a-default-theme-with-Less-) [#23](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/23)
|
||||
* **ui:** UI now shows player names instead of their id ([#11](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/11)) ([c168ae7](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c168ae7dc05e99c1890661aa3b63f4d4d12779fb))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **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))
|
||||
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
|
||||
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
|
||||
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
|
||||
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
|
||||
|
||||
### Reverts
|
||||
|
||||
* ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe))
|
||||
* version bumb ([2e10059](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e10059a6756befe6f8e70c94fd34b865693efb8))
|
||||
* version bump ([7879d1a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/7879d1ab6ee8f227c19b69b467a28dd7b479ff73))
|
||||
## (2025-11-03)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* implemented multigame support (#34)
|
||||
|
||||
### Features
|
||||
|
||||
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
|
||||
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
|
||||
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
|
||||
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
|
||||
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
|
||||
* **ui:** LESS Integration ([72fcf78](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/72fcf783b8ca5ce260e4b152016353e7dc1edb69)), closes [#15-Create-a-default-theme-with-Less-](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/15-Create-a-default-theme-with-Less-) [#23](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/23)
|
||||
* **ui:** UI now shows player names instead of their id ([#11](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/11)) ([c168ae7](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c168ae7dc05e99c1890661aa3b63f4d4d12779fb))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **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))
|
||||
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
|
||||
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
|
||||
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
|
||||
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
|
||||
|
||||
### Reverts
|
||||
|
||||
* ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe))
|
||||
* version bumb ([2e10059](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/2e10059a6756befe6f8e70c94fd34b865693efb8))
|
||||
## (2025-11-03)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* implemented multigame support (#34)
|
||||
|
||||
### Features
|
||||
|
||||
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
|
||||
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
|
||||
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
|
||||
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
|
||||
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
|
||||
* **ui:** LESS Integration ([72fcf78](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/72fcf783b8ca5ce260e4b152016353e7dc1edb69)), closes [#15-Create-a-default-theme-with-Less-](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/15-Create-a-default-theme-with-Less-) [#23](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/23)
|
||||
* **ui:** UI now shows player names instead of their id ([#11](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/11)) ([c168ae7](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c168ae7dc05e99c1890661aa3b63f4d4d12779fb))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **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))
|
||||
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
|
||||
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
|
||||
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
|
||||
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
|
||||
|
||||
### Reverts
|
||||
|
||||
* ci: bump version to v1.0.0 [skip ci] ([91d7f6c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/91d7f6ca003edb368aba76b7a82c8b42d22bdbfe))
|
||||
## (2025-11-03)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* implemented multigame support (#34)
|
||||
|
||||
### Features
|
||||
|
||||
* **config:** add issue templates for Epics, User Stories, and Subtasks ([#4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/4)) ([d71809d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d71809d6f4389b03ecc8ee9b857e58a4da413fa2))
|
||||
* implemented multigame support ([#34](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/34)) ([afde6c0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/afde6c02da2c3055084ae90ab6b11b63b0e11cfb))
|
||||
* **ui:** added rules ([#12](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/12)) ([92e4851](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/92e4851219e64ad919d7f8bc7c30232ef7d6570f))
|
||||
* **ui:** CSS Animations [#18](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/18) ([#27](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/27)) ([c0dadf8](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c0dadf89274169b396089c2e7b3d69b043097186))
|
||||
* **ui:** implement CSS variables for theme support ([#26](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/26)) ([1f377de](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1f377de0f43aaf17271d29678a1accf11cfe121c)), closes [#17](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/17)
|
||||
* **ui:** LESS Integration ([72fcf78](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/72fcf783b8ca5ce260e4b152016353e7dc1edb69)), closes [#15-Create-a-default-theme-with-Less-](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/15-Create-a-default-theme-with-Less-) [#23](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/23)
|
||||
* **ui:** UI now shows player names instead of their id ([#11](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/11)) ([c168ae7](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/c168ae7dc05e99c1890661aa3b63f4d4d12779fb))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **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))
|
||||
* **imports:** reorganized import statements for clarity and consistency ([#22](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/22)) ([1517d0c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/1517d0c006247a95b02f66fa5bc53cc78b9d89fa))
|
||||
* **ui:** add light mode styles, update font families and fixed ui ([#24](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/24)) ([729a4ee](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/729a4eec6b721f8118d0feffa44721262ccd595f))
|
||||
* **ui:** added dark mode ([#25](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/25)) ([6c31fa0](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/6c31fa0538e5d71c8be9a728eebf7576cb108782))
|
||||
* **ui:** changed backgrounds ([#33](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/33)) ([bef96ba](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/bef96ba7e39a633c1972b01b1c1ebab9d1d3ee1c))
|
||||
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"
|
||||
]
|
||||
}
|
||||
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)
|
||||
},
|
||||
libraryDependencies += guice,
|
||||
Test / testOptions += Tests.Filter(_.equals("de.knockoutwhist.TestSequence")),
|
||||
coverageEnabled := true,
|
||||
coverageFailOnMinimum := true,
|
||||
coverageMinimumStmtTotal := 85,
|
||||
@@ -29,7 +28,8 @@ lazy val commonSettings = Seq(
|
||||
lazy val knockoutwhist = project.in(file("knockoutwhist"))
|
||||
.settings(
|
||||
commonSettings,
|
||||
mainClass := Some("de.knockoutwhist.KnockOutWhist")
|
||||
mainClass := Some("de.knockoutwhist.KnockOutWhist"),
|
||||
coverageExcludedPackages := "de.knockoutwhist.ui.*;de.knockoutwhist.utils.gui.*"
|
||||
)
|
||||
|
||||
lazy val knockoutwhistweb = project.in(file("knockoutwhistweb"))
|
||||
@@ -37,7 +37,10 @@ lazy val knockoutwhistweb = project.in(file("knockoutwhistweb"))
|
||||
.dependsOn(knockoutwhist % "compile->compile;test->test")
|
||||
.settings(
|
||||
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"
|
||||
)
|
||||
|
||||
lazy val root = (project in file("."))
|
||||
|
||||
116
conventionalcommit.json
Normal file
116
conventionalcommit.json
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"$schema": "https://github.com/lppedd/idea-conventional-commit/raw/master/src/main/resources/defaults/conventionalcommit.schema.json",
|
||||
"types": {
|
||||
"refactor": {
|
||||
"description": "Changes which neither fix a bug nor add a feature"
|
||||
},
|
||||
"fix": {
|
||||
"description": "Changes which patch a bug"
|
||||
},
|
||||
"feat": {
|
||||
"description": "Changes which introduce a new feature"
|
||||
},
|
||||
"build": {
|
||||
"description": "Changes which affect the build system or external dependencies.<br>Example scopes: gulp, broccoli, npm",
|
||||
"scopes": {
|
||||
"npm": {},
|
||||
"gulp": {},
|
||||
"broccoli": {}
|
||||
}
|
||||
},
|
||||
"chore": {
|
||||
"description": "Changes which are not user-facing"
|
||||
},
|
||||
"style": {
|
||||
"description": "Changes which do not affect code logic, such as whitespaces, formatting, missing semicolons"
|
||||
},
|
||||
"test": {
|
||||
"description": "Changes which add missing tests or fix existing ones"
|
||||
},
|
||||
"docs": {
|
||||
"description": "Changes which affect documentation"
|
||||
},
|
||||
"perf": {
|
||||
"description": "Changes which improve performance"
|
||||
},
|
||||
"ci": {
|
||||
"description": "Changes which affect CI configuration files and scripts.<br>Example scopes: travis, circle, browser-stack, sauce-labs"
|
||||
},
|
||||
"revert": {
|
||||
"description": "Changes which revert a previous commit"
|
||||
}
|
||||
},
|
||||
"commonScopes": {
|
||||
"api": {
|
||||
"description": "Changes related to the API"
|
||||
},
|
||||
"auth": {
|
||||
"description": "Changes related to authentication"
|
||||
},
|
||||
"config": {
|
||||
"description": "Changes related to configuration"
|
||||
},
|
||||
"db": {
|
||||
"description": "Changes related to the database"
|
||||
},
|
||||
"docs": {
|
||||
"description": "Changes related to documentation"
|
||||
},
|
||||
"ui": {
|
||||
"description": "Changes related to the user interface"
|
||||
},
|
||||
"ux": {
|
||||
"description": "Changes related to the user experience"
|
||||
},
|
||||
"build": {
|
||||
"description": "Changes related to the build system"
|
||||
},
|
||||
"ci": {
|
||||
"description": "Changes related to continuous integration"
|
||||
},
|
||||
"deps": {
|
||||
"description": "Changes related to dependencies"
|
||||
},
|
||||
"promotion": {
|
||||
"description": "Kargo promotion changes"
|
||||
},
|
||||
"base": {
|
||||
"description": "Changes related to the core functionality"
|
||||
},
|
||||
"release": {
|
||||
"description": "A release commit"
|
||||
}
|
||||
},
|
||||
"footerTypes": {
|
||||
"BREAKING-CHANGE": {
|
||||
"description": "The commit introduces breaking changes"
|
||||
},
|
||||
"Closes": {
|
||||
"description": "The commit closes issues or pull requests"
|
||||
},
|
||||
"Implements": {
|
||||
"description": "The commit implements new features"
|
||||
},
|
||||
"Author": {
|
||||
"description": "The commit's author"
|
||||
},
|
||||
"Co-authored-by": {
|
||||
"description": "The specified person co-authored the commit's changes"
|
||||
},
|
||||
"Signed-off-by": {
|
||||
"description": "Certifies the committer has the rights to submit the work under the project's license or agrees to some contributor representation, such as a Developer Certificate of Origin"
|
||||
},
|
||||
"Reviewed-by": {
|
||||
"description": "The specified person reviewed and is completely satisfied with the commit's changes"
|
||||
},
|
||||
"Tested-by": {
|
||||
"description": "The specified person applied the commit's changes and found them to have the desired effect"
|
||||
},
|
||||
"Acked-by": {
|
||||
"description": "The specified person liked the commit's changes"
|
||||
},
|
||||
"Refs": {
|
||||
"description": "The commit references another commit by its hash ID.<br>For multiple hash IDs, use a comma as separator"
|
||||
}
|
||||
}
|
||||
}
|
||||
Submodule knockoutwhist updated: 48cd4d3956...b9a7b0a2af
6
knockoutwhistweb/app/assets/stylesheets/dark-mode.less
Normal file
6
knockoutwhistweb/app/assets/stylesheets/dark-mode.less
Normal file
@@ -0,0 +1,6 @@
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background-image: url('/assets/images/background.png');
|
||||
--color: white;
|
||||
}
|
||||
}
|
||||
4
knockoutwhistweb/app/assets/stylesheets/light-mode.less
Normal file
4
knockoutwhistweb/app/assets/stylesheets/light-mode.less
Normal file
@@ -0,0 +1,4 @@
|
||||
:root {
|
||||
--background-image: url('/assets/images/img.png');
|
||||
--color: black;
|
||||
}
|
||||
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;
|
||||
}
|
||||
142
knockoutwhistweb/app/assets/stylesheets/main.less
Normal file
142
knockoutwhistweb/app/assets/stylesheets/main.less
Normal file
@@ -0,0 +1,142 @@
|
||||
@import "light-mode.less";
|
||||
@import "dark-mode.less";
|
||||
@import "login.less";
|
||||
|
||||
@background-image: var(--background-image);
|
||||
@color: var(--color);
|
||||
@keyframes slideIn {
|
||||
0% { transform: translateX(-100vw); }
|
||||
100% { transform: translateX(0); }
|
||||
}
|
||||
.game-field-background {
|
||||
background-image: @background-image;
|
||||
background-size: 100vw 100vh;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.game-field {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
#sessions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
h1 {
|
||||
animation: slideIn 0.5s ease-out forwards;
|
||||
animation-fill-mode: backwards;
|
||||
}
|
||||
}
|
||||
#textanimation {
|
||||
animation: slideIn 0.5s ease-out forwards;
|
||||
animation-fill-mode: backwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
#sessions a, #sessions h1, #sessions p {
|
||||
color: @color;
|
||||
font-size: 40px;
|
||||
font-family: Arial, serif;
|
||||
}
|
||||
#ingame {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: 100%;
|
||||
}
|
||||
#ingame a, #ingame h1, #ingame p {
|
||||
color: @color;
|
||||
font-size: 40px;
|
||||
font-family: Arial, serif;
|
||||
}
|
||||
#playercards {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
height: 20%;
|
||||
img {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 10%;
|
||||
min-height: 10%
|
||||
}
|
||||
#playedcardplayer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
#playedcardplayer p {
|
||||
font-size: 12px;
|
||||
height: 4%;
|
||||
}
|
||||
#playedcardplayer img {
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
#firstCard {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 20%;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
#firstCardObject {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 4%;
|
||||
}
|
||||
#firstCardObject img{
|
||||
height: 90%;
|
||||
}
|
||||
#firstCardObject p{
|
||||
height: 10%;
|
||||
font-size: 20px;
|
||||
|
||||
}
|
||||
#trumpsuit {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: 4%;
|
||||
}
|
||||
#nextPlayers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 0;
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
#invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
#selecttrumpsuit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
#rules {
|
||||
color: @color;
|
||||
font-size: 1.5em;
|
||||
font-family: Arial, serif;
|
||||
}
|
||||
37
knockoutwhistweb/app/auth/Auth.scala
Normal file
37
knockoutwhistweb/app/auth/Auth.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
|
||||
|
||||
private 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
|
||||
|
||||
import de.knockoutwhist.components.DefaultConfiguration
|
||||
import controllers.WebUI
|
||||
import de.knockoutwhist.ui.UI
|
||||
import de.knockoutwhist.utils.events.EventListener
|
||||
|
||||
class WebApplicationConfiguration extends DefaultConfiguration {
|
||||
|
||||
override def uis: Set[UI] = super.uis + WebUI
|
||||
override def listener: Set[EventListener] = super.listener + WebUI
|
||||
override def uis: Set[UI] = Set()
|
||||
override def listener: Set[EventListener] = Set()
|
||||
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import controllers.sessions.SimpleSession
|
||||
import com.google.inject.{Guice, Injector}
|
||||
import de.knockoutwhist.KnockOutWhist
|
||||
import de.knockoutwhist.components.Configuration
|
||||
import di.KnockOutWebConfigurationModule
|
||||
import play.api.*
|
||||
import play.api.mvc.*
|
||||
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 sessions(): Action[AnyContent] = {
|
||||
Action { implicit request =>
|
||||
Ok(views.html.sessions.apply(PodGameManager.listSessions().map(f => f.toString)))
|
||||
}
|
||||
}
|
||||
|
||||
def ingame(id: String): Action[AnyContent] = {
|
||||
val uuid: UUID = UUID.fromString(id)
|
||||
if (PodGameManager.identify(uuid).isEmpty) {
|
||||
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
|
||||
Action { implicit request =>
|
||||
Ok(views.html.tui.apply(session.asInstanceOf[SimpleSession].get()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
279
knockoutwhistweb/app/controllers/IngameController.scala
Normal file
279
knockoutwhistweb/app/controllers/IngameController.scala
Normal file
@@ -0,0 +1,279 @@
|
||||
package controllers
|
||||
|
||||
import auth.{AuthAction, AuthenticatedRequest}
|
||||
import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak}
|
||||
import exceptions.{CantPlayCardException, GameFullException, NotEnoughPlayersException, NotHostException, NotInThisGameException}
|
||||
import logic.PodManager
|
||||
import model.sessions.{PlayerSession, UserSession}
|
||||
import play.api.*
|
||||
import play.api.mvc.*
|
||||
|
||||
import java.util.UUID
|
||||
import javax.inject.*
|
||||
import scala.util.Try
|
||||
|
||||
|
||||
/**
|
||||
* This controller creates an `Action` to handle HTTP requests to the
|
||||
* application's home page.
|
||||
*/
|
||||
@Singleton
|
||||
class IngameController @Inject()(
|
||||
val controllerComponents: ControllerComponents,
|
||||
val authAction: AuthAction,
|
||||
val podManager: PodManager
|
||||
) extends BaseController {
|
||||
|
||||
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) {
|
||||
Redirect(routes.IngameController.game(gameId))
|
||||
} else {
|
||||
val throwable = result.failed.get
|
||||
throwable match {
|
||||
case _: NotInThisGameException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _: NotHostException =>
|
||||
Forbidden(throwable.getMessage)
|
||||
case _: NotEnoughPlayersException =>
|
||||
BadRequest(throwable.getMessage)
|
||||
case _ =>
|
||||
InternalServerError(throwable.getMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
def kickPlayer(gameId: String, playerToKick: UUID): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = podManager.getGame(gameId)
|
||||
game.get.leaveGame(playerToKick)
|
||||
Redirect(routes.IngameController.game(gameId))
|
||||
}
|
||||
def leaveGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = podManager.getGame(gameId)
|
||||
game.get.leaveGame(request.user.id)
|
||||
Redirect(routes.MainMenuController.mainMenu())
|
||||
}
|
||||
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 cardIdOpt = request.body.asFormUrlEncoded.flatMap(_.get("cardId").flatMap(_.headOption))
|
||||
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) {
|
||||
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 =>
|
||||
BadRequest("cardId parameter is missing")
|
||||
}
|
||||
case None =>
|
||||
NotFound("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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
69
knockoutwhistweb/app/controllers/MainMenuController.scala
Normal file
69
knockoutwhistweb/app/controllers/MainMenuController.scala
Normal file
@@ -0,0 +1,69 @@
|
||||
package controllers
|
||||
|
||||
import auth.{AuthAction, AuthenticatedRequest}
|
||||
import logic.PodManager
|
||||
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 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 postData = request.body.asFormUrlEncoded
|
||||
if (postData.isDefined) {
|
||||
val gamename = postData.get.get("lobbyname").flatMap(_.headOption).getOrElse(s"${request.user.name}'s Game")
|
||||
val playeramount = postData.get.get("playeramount").flatMap(_.headOption).getOrElse("")
|
||||
val gameLobby = podManager.createGame(
|
||||
host = request.user,
|
||||
name = gamename,
|
||||
maxPlayers = playeramount.toInt
|
||||
)
|
||||
Redirect(routes.IngameController.game(gameLobby.id))
|
||||
} else {
|
||||
BadRequest("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] = {
|
||||
Action { implicit request =>
|
||||
Ok(views.html.mainmenu.rules())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[UUID] = {
|
||||
sessions.keys.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,77 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import controllers.sessions.SimpleSession
|
||||
import de.knockoutwhist.cards.{Card, CardValue, Hand, Suit}
|
||||
import de.knockoutwhist.events.*
|
||||
import de.knockoutwhist.events.ERROR_STATUS.*
|
||||
import de.knockoutwhist.events.GLOBAL_STATUS.*
|
||||
import de.knockoutwhist.events.PLAYER_STATUS.*
|
||||
import de.knockoutwhist.events.ROUND_STATUS.{PLAYERS_OUT, SHOW_START_ROUND, WON_ROUND}
|
||||
import de.knockoutwhist.events.cards.{RenderHandEvent, ShowTieCardsEvent}
|
||||
import de.knockoutwhist.events.round.ShowCurrentTrickEvent
|
||||
import de.knockoutwhist.events.ui.GameState.{INGAME, MAIN_MENU}
|
||||
import de.knockoutwhist.events.ui.{GameState, GameStateUpdateEvent}
|
||||
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
|
||||
private var internState: GameState = GameState.NO_SET
|
||||
|
||||
var latestOutput: String = ""
|
||||
|
||||
override def instance: CustomThread = WebUI
|
||||
|
||||
override def listen(event: SimpleEvent): Unit = {
|
||||
runLater {
|
||||
event match {
|
||||
case event: RenderHandEvent =>
|
||||
PodGameManager.transmit(event.player.id, event)
|
||||
case event: ShowTieCardsEvent =>
|
||||
PodGameManager.transmitAll(event)
|
||||
case event: ShowGlobalStatus =>
|
||||
if (event.status == TECHNICAL_MATCH_STARTED) {
|
||||
val matchImpl = event.objects.head.asInstanceOf[Match]
|
||||
for (player <- matchImpl.totalplayers) {
|
||||
PodGameManager.addSession(SimpleSession(player.id, List()))
|
||||
}
|
||||
} else {
|
||||
PodGameManager.transmitAll(event)
|
||||
}
|
||||
case event: ShowPlayerStatus =>
|
||||
PodGameManager.transmit(event.player.id, event)
|
||||
case event: ShowRoundStatus =>
|
||||
PodGameManager.transmitAll(event)
|
||||
case event: ShowErrorStatus =>
|
||||
PodGameManager.transmitAll(event)
|
||||
case event: ShowCurrentTrickEvent =>
|
||||
PodGameManager.transmitAll(event)
|
||||
case event: GameStateUpdateEvent =>
|
||||
if (internState != event.gameState) {
|
||||
internState = event.gameState
|
||||
if (event.gameState == MAIN_MENU) {
|
||||
PodGameManager.clearSessions()
|
||||
}
|
||||
Some(true)
|
||||
}
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def initial: Boolean = {
|
||||
if (init) {
|
||||
return false
|
||||
}
|
||||
init = true
|
||||
start()
|
||||
true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
package controllers.sessions
|
||||
|
||||
import de.knockoutwhist.cards.{Card, CardValue, Hand}
|
||||
import de.knockoutwhist.events.ERROR_STATUS.*
|
||||
import de.knockoutwhist.events.GLOBAL_STATUS.*
|
||||
import de.knockoutwhist.events.PLAYER_STATUS.*
|
||||
import de.knockoutwhist.events.ROUND_STATUS.*
|
||||
import de.knockoutwhist.events.{ShowErrorStatus, ShowGlobalStatus, ShowPlayerStatus, ShowRoundStatus}
|
||||
import de.knockoutwhist.events.cards.{RenderHandEvent, ShowTieCardsEvent}
|
||||
import de.knockoutwhist.events.round.ShowCurrentTrickEvent
|
||||
import de.knockoutwhist.player.AbstractPlayer
|
||||
import de.knockoutwhist.utils.events.SimpleEvent
|
||||
import play.twirl.api.Html
|
||||
import scalafx.scene.image.Image
|
||||
import util.WebUIUtils
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
case class SimpleSession(id: UUID, private var output: List[Html]) extends PlayerSession {
|
||||
def get(): List[Html] = {
|
||||
output
|
||||
}
|
||||
|
||||
override def updatePlayer(event: SimpleEvent): Unit = {
|
||||
event match {
|
||||
case event: RenderHandEvent =>
|
||||
renderHand(event)
|
||||
case event: ShowTieCardsEvent =>
|
||||
showtiecardseventmethod(event)
|
||||
case event: ShowGlobalStatus =>
|
||||
showglobalstatusmethod(event)
|
||||
case event: ShowPlayerStatus =>
|
||||
showplayerstatusmethod(event)
|
||||
case event: ShowRoundStatus =>
|
||||
showroundstatusmethod(event)
|
||||
case event: ShowErrorStatus =>
|
||||
showerrstatmet(event)
|
||||
case event: ShowCurrentTrickEvent =>
|
||||
showcurtrevmet(event)
|
||||
}
|
||||
}
|
||||
|
||||
private def clear(): Unit = {
|
||||
output = List()
|
||||
}
|
||||
|
||||
private def renderHand(event: RenderHandEvent): Unit = {
|
||||
output = output :++ WebUICards.renderHandEvent(event.hand)
|
||||
output = output :+ Html("<br>")
|
||||
}
|
||||
|
||||
private def showtiecardseventmethod(event: ShowTieCardsEvent): Option[Boolean] = {
|
||||
var l = List[Html]()
|
||||
for ((player, card) <- event.card) {
|
||||
l = l :+ Html(s"<p>${player.name}:</p>")
|
||||
l = l :+ WebUIUtils.cardtoImage(card)
|
||||
l = l :+ Html("<br>")
|
||||
}
|
||||
output = output :++ l
|
||||
output = output :+ Html("<br>")
|
||||
Some(true)
|
||||
}
|
||||
|
||||
private def showglobalstatusmethod(event: ShowGlobalStatus): Option[Boolean] = {
|
||||
event.status match {
|
||||
case SHOW_TIE =>
|
||||
println("It's a tie! Let's cut to determine the winner.")
|
||||
Some(true)
|
||||
case SHOW_TIE_WINNER =>
|
||||
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) {
|
||||
None
|
||||
} else {
|
||||
println(s"${event.objects.head.asInstanceOf[AbstractPlayer].name} wins the cut!")
|
||||
Some(true)
|
||||
}
|
||||
case SHOW_TIE_TIE =>
|
||||
println("It's a tie again! Let's cut again.")
|
||||
Some(true)
|
||||
case SHOW_START_MATCH =>
|
||||
clear()
|
||||
println("Starting a new match...")
|
||||
output = output :+ Html("<br><br>")
|
||||
Some(true)
|
||||
case SHOW_TYPE_PLAYERS =>
|
||||
println("Please enter the names of the players, separated by a comma.")
|
||||
Some(true)
|
||||
case SHOW_FINISHED_MATCH =>
|
||||
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) {
|
||||
None
|
||||
} else {
|
||||
clear()
|
||||
println(s"The match is over. The winner is ${event.objects.head.asInstanceOf[AbstractPlayer]}")
|
||||
Some(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def showplayerstatusmethod(event: ShowPlayerStatus): Option[Boolean] = {
|
||||
val player = event.player
|
||||
event.status match {
|
||||
case SHOW_PLAY_CARD =>
|
||||
println("Which card do you want to play?")
|
||||
Some(true)
|
||||
case SHOW_DOG_PLAY_CARD =>
|
||||
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[Boolean]) {
|
||||
None
|
||||
} else {
|
||||
println("You are using your dog life. Do you want to play your final card now?")
|
||||
if (event.objects.head.asInstanceOf[Boolean]) {
|
||||
println("You have to play your final card this round!")
|
||||
println("Please enter y to play your final card.")
|
||||
Some(true)
|
||||
} else {
|
||||
println("Please enter y/n to play your final card.")
|
||||
Some(true)
|
||||
}
|
||||
}
|
||||
case SHOW_TIE_NUMBERS =>
|
||||
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[Int]) {
|
||||
None
|
||||
} else {
|
||||
println(s"${player.name} enter a number between 1 and ${event.objects.head.asInstanceOf[Int]}.")
|
||||
Some(true)
|
||||
}
|
||||
case SHOW_TRUMPSUIT_OPTIONS =>
|
||||
println("Which suit do you want to pick as the next trump suit?")
|
||||
println("1: Hearts")
|
||||
println("2: Diamonds")
|
||||
println("3: Clubs")
|
||||
println("4: Spades")
|
||||
println()
|
||||
Some(true)
|
||||
case SHOW_NOT_PLAYED =>
|
||||
println(s"Player ${event.player} decided to not play his card")
|
||||
Some(true)
|
||||
case SHOW_WON_PLAYER_TRICK =>
|
||||
println(s"${event.player.name} won the trick.")
|
||||
output = output :+ Html("<br><br>")
|
||||
Some(true)
|
||||
}
|
||||
}
|
||||
|
||||
private def showroundstatusmethod(event: ShowRoundStatus): Option[Boolean] = {
|
||||
event.status match {
|
||||
case SHOW_TURN =>
|
||||
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) {
|
||||
None
|
||||
} else {
|
||||
println(s"It's ${event.objects.head.asInstanceOf[AbstractPlayer].name} turn.")
|
||||
Some(true)
|
||||
}
|
||||
case SHOW_START_ROUND =>
|
||||
clear()
|
||||
println(s"Starting a new round. The trump suit is ${event.currentRound.trumpSuit}.")
|
||||
output = output :+ Html("<br><br>")
|
||||
Some(true)
|
||||
case WON_ROUND =>
|
||||
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) {
|
||||
None
|
||||
} else {
|
||||
println(s"${event.objects.head.asInstanceOf[AbstractPlayer].name} won the round.")
|
||||
Some(true)
|
||||
}
|
||||
case PLAYERS_OUT =>
|
||||
println("The following players are out of the game:")
|
||||
event.currentRound.playersout.foreach(p => {
|
||||
println(p.name)
|
||||
})
|
||||
Some(true)
|
||||
}
|
||||
}
|
||||
|
||||
private def showerrstatmet(event: ShowErrorStatus): Option[Boolean] = {
|
||||
event.status match {
|
||||
case INVALID_NUMBER =>
|
||||
println("Please enter a valid number.")
|
||||
Some(true)
|
||||
case NOT_A_NUMBER =>
|
||||
println("Please enter a number.")
|
||||
Some(true)
|
||||
case INVALID_INPUT =>
|
||||
println("Please enter a valid input")
|
||||
Some(true)
|
||||
case INVALID_NUMBER_OF_PLAYERS =>
|
||||
println("Please enter at least two names.")
|
||||
Some(true)
|
||||
case IDENTICAL_NAMES =>
|
||||
println("Please enter unique names.")
|
||||
Some(true)
|
||||
case INVALID_NAME_FORMAT =>
|
||||
println("Please enter valid names. Those can not be empty, shorter than 2 or longer then 10 characters.")
|
||||
Some(true)
|
||||
case WRONG_CARD =>
|
||||
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[Card]) {
|
||||
None
|
||||
} else {
|
||||
println(f"You have to play a card of suit: ${event.objects.head.asInstanceOf[Card].suit}\n")
|
||||
Some(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def showcurtrevmet(event: ShowCurrentTrickEvent): Option[Boolean] = {
|
||||
clear()
|
||||
val sb = new StringBuilder()
|
||||
sb.append("Current Trick:\n")
|
||||
sb.append("Trump-Suit: " + event.round.trumpSuit + "\n")
|
||||
if (event.trick.firstCard.isDefined) {
|
||||
sb.append(s"Suit to play: ${event.trick.firstCard.get.suit}\n")
|
||||
}
|
||||
for ((card, player) <- event.trick.cards) {
|
||||
sb.append(s"${player.name} played ${card.toString}\n")
|
||||
}
|
||||
println(sb.toString())
|
||||
Some(true)
|
||||
}
|
||||
|
||||
private def println(s: String): Unit = {
|
||||
var html = List[Html]()
|
||||
for (line <- s.split("\n")) {
|
||||
html = html :+ Html(line)
|
||||
html = html :+ Html("<br>")
|
||||
}
|
||||
output = output :++ html
|
||||
}
|
||||
|
||||
private def println(): Unit = {
|
||||
output = output :+ Html("<br>")
|
||||
}
|
||||
|
||||
object WebUICards {
|
||||
def renderCardAsString(card: Card): Vector[String] = {
|
||||
val lines = "│ │"
|
||||
if (card.cardValue == CardValue.Ten) {
|
||||
return Vector(
|
||||
s"┌─────────┐",
|
||||
s"│${card.cardValue.cardType()} │",
|
||||
lines,
|
||||
s"│ ${card.suit.cardType()} │",
|
||||
lines,
|
||||
s"│ ${card.cardValue.cardType()}│",
|
||||
s"└─────────┘"
|
||||
)
|
||||
}
|
||||
Vector(
|
||||
s"┌─────────┐",
|
||||
s"│${card.cardValue.cardType()} │",
|
||||
lines,
|
||||
s"│ ${card.suit.cardType()} │",
|
||||
lines,
|
||||
s"│ ${card.cardValue.cardType()}│",
|
||||
s"└─────────┘"
|
||||
)
|
||||
}
|
||||
|
||||
def renderHandEvent(hand: Hand): List[Html] = {
|
||||
hand.cards.map(WebUIUtils.cardtoImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
261
knockoutwhistweb/app/logic/game/GameLobby.scala
Normal file
261
knockoutwhistweb/app/logic/game/GameLobby.scala
Normal file
@@ -0,0 +1,261 @@
|
||||
package logic.game
|
||||
|
||||
import de.knockoutwhist.cards.{Hand, Suit}
|
||||
import de.knockoutwhist.control.GameLogic
|
||||
import de.knockoutwhist.control.GameState.{Lobby, MainMenu}
|
||||
import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil}
|
||||
import de.knockoutwhist.events.global.{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 model.sessions.{InteractionType, UserSession}
|
||||
import model.users.User
|
||||
|
||||
import java.util.UUID
|
||||
import scala.collection.mutable
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
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()
|
||||
|
||||
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: PlayerEvent =>
|
||||
users.get(event.playerId).foreach(session => session.updatePlayer(event))
|
||||
case event: GameStateChangeEvent =>
|
||||
if (event.oldState == MainMenu && event.newState == Lobby) {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
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,63 @@
|
||||
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}
|
||||
|
||||
@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] = {
|
||||
//TODO verify JWT token instead of looking up in cache
|
||||
val cachedUser = cache.getIfPresent(sessionId)
|
||||
if (cachedUser != null) {
|
||||
Some(cachedUser)
|
||||
} else {
|
||||
val decoded = verifier.verify(sessionId)
|
||||
val user = userManager.userExistsById(decoded.getClaim("id").asLong())
|
||||
user.foreach(u => cache.put(sessionId, u))
|
||||
user
|
||||
}
|
||||
}
|
||||
|
||||
override def invalidateSession(sessionId: String): Unit = {
|
||||
//TODO remove from Redis and DB
|
||||
cache.invalidate(sessionId)
|
||||
}
|
||||
}
|
||||
51
knockoutwhistweb/app/logic/user/impl/StubUserManager.scala
Normal file
51
knockoutwhistweb/app/logic/user/impl/StubUserManager.scala
Normal file
@@ -0,0 +1,51 @@
|
||||
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")
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.util.UUID
|
||||
trait PlayerSession {
|
||||
|
||||
def id: UUID
|
||||
def name: String
|
||||
def updatePlayer(event: SimpleEvent): Unit
|
||||
|
||||
}
|
||||
14
knockoutwhistweb/app/model/sessions/SimpleSession.scala
Normal file
14
knockoutwhistweb/app/model/sessions/SimpleSession.scala
Normal file
@@ -0,0 +1,14 @@
|
||||
package model.sessions
|
||||
|
||||
import de.knockoutwhist.player.AbstractPlayer
|
||||
import de.knockoutwhist.utils.events.SimpleEvent
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
case class SimpleSession(id: UUID, player: AbstractPlayer) extends PlayerSession {
|
||||
|
||||
def name: String = player.name
|
||||
|
||||
override def updatePlayer(event: SimpleEvent): Unit = {
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package util
|
||||
|
||||
import de.knockoutwhist.cards.Card
|
||||
import de.knockoutwhist.cards.CardValue.{Ace, Eight, Five, Four, Jack, King, Nine, Queen, Seven, Six, Ten, Three, Two}
|
||||
import de.knockoutwhist.cards.CardValue.*
|
||||
import de.knockoutwhist.cards.Suit.{Clubs, Diamonds, Hearts, Spades}
|
||||
import play.twirl.api.Html
|
||||
import scalafx.scene.image.Image
|
||||
@@ -29,6 +29,6 @@ object WebUIUtils {
|
||||
case Three => "3"
|
||||
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>
|
||||
}
|
||||
51
knockoutwhistweb/app/views/ingame/ingame.scala.html
Normal file
51
knockoutwhistweb/app/views/ingame/ingame.scala.html
Normal file
@@ -0,0 +1,51 @@
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
|
||||
|
||||
@main("Ingame") {
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-4 mt-5 text-start">
|
||||
<p class="fs-4">Next Player</p>
|
||||
<p class="fs-4">@gamelobby.getLogic.getPlayerQueue.get.duplicate().nextPlayer()</p>
|
||||
</div>
|
||||
<div class="col-4 mt-5 text-center">
|
||||
<p class="fs-3">Cards played</p>
|
||||
</div>
|
||||
<div class="col-4 mt-5 text-end">
|
||||
<p class="fs-4">Trumpsuit:</p>
|
||||
<p class="fs-4">@gamelobby.getLogic.getCurrentRound.get.trumpSuit</p>
|
||||
<p class="fs-4 mt-5">First Card:</p>
|
||||
@if(gamelobby.getLogic.getCurrentTrick.get.firstCard.isDefined) {
|
||||
@util.WebUIUtils.cardtoImage(gamelobby.getLogic.getCurrentTrick.get.firstCard.get) width="30%"/>
|
||||
} else {
|
||||
@views.html.render.card.apply("images/cards/1B.png")("Blank Card") width="30%"/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 mt-5">
|
||||
@for((cardplayed, player) <- gamelobby.getLogic.getCurrentTrick.get.cards) {
|
||||
<div class="col-auto">
|
||||
<div class="card" style="max-width: 8rem;">
|
||||
@util.WebUIUtils.cardtoImage(cardplayed) />
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">@player</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row gx-0">
|
||||
@for(i <- 0 until player.currentHand().get.cards.size) {
|
||||
<div class="col-auto">
|
||||
<form action="@(routes.IngameController.playCard(gamelobby.id))" class="d-flex" method="post">
|
||||
<input type="hidden" name="cardId" value="@i" />
|
||||
<button type="submit" class="btn bg-transparent p-0 m-0">
|
||||
@util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="40%" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
27
knockoutwhistweb/app/views/ingame/selecttrump.scala.html
Normal file
27
knockoutwhistweb/app/views/ingame/selecttrump.scala.html
Normal file
@@ -0,0 +1,27 @@
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic)
|
||||
|
||||
@main("Selecting Trumpsuit...") {
|
||||
<div id="selecttrumpsuit" class="game-field game-field-background">
|
||||
@if(player.equals(logic.getCurrentMatch.get.roundlist.last.winner.get)) {
|
||||
<h1>Knockout Whist</h1>
|
||||
<p>You (@player.toString) have won the last round! Select a trumpsuit for the next round!</p>
|
||||
<p>Available trumpsuits are displayed below:</p>
|
||||
<div id="playercards">
|
||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Spades))
|
||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Clubs))
|
||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Hearts))
|
||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Diamonds))
|
||||
</div>
|
||||
<p>Your cards</p>
|
||||
|
||||
<div id="playercards">
|
||||
@for(card <- player.currentHand().get.cards) {
|
||||
@util.WebUIUtils.cardtoImage(card)
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
<h1>Knockout Whist</h1>
|
||||
<p>@player.toString is choosing a trumpsuit. Starting new round when @player.toString picked a trumpsuit...</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
27
knockoutwhistweb/app/views/ingame/tie.scala.html
Normal file
27
knockoutwhistweb/app/views/ingame/tie.scala.html
Normal file
@@ -0,0 +1,27 @@
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic)
|
||||
|
||||
@main("Tie") {
|
||||
<div id="tie" class="game-field game-field-background">
|
||||
<h1>Knockout Whist</h1>
|
||||
<p>The last Round was tied between
|
||||
@for(players <- logic.playerTieLogic.getTiedPlayers) {
|
||||
@players
|
||||
}
|
||||
</p>
|
||||
@if(player.equals(logic.playerTieLogic.currentTiePlayer())) {
|
||||
<p>Pick a card between 1 and @logic.playerTieLogic.highestAllowedNumber()! The resulting card will be your card for the cut.</p>
|
||||
} else {
|
||||
<p>@logic.playerTieLogic.currentTiePlayer() is currently picking his number for the cut.</p>
|
||||
<p>Currently picked Cards:</p>
|
||||
<div id="cardsplayed">
|
||||
@for((player, card) <- logic.playerTieLogic.getSelectedCard) {
|
||||
<div id="playedcardplayer">
|
||||
<p>@player</p>
|
||||
@util.WebUIUtils.cardtoImage(card)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
}
|
||||
84
knockoutwhistweb/app/views/lobby/lobby.scala.html
Normal file
84
knockoutwhistweb/app/views/lobby/lobby.scala.html
Normal file
@@ -0,0 +1,84 @@
|
||||
@(user: Option[model.users.User], gamelobby: logic.game.GameLobby)
|
||||
|
||||
@main("Lobby") {
|
||||
<div class="container">
|
||||
<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>
|
||||
<form action="@(routes.IngameController.leaveGame(gamelobby.id))">
|
||||
<button type="submit" class="btn btn-danger ms-auto">Exit</button>
|
||||
</form>
|
||||
</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">
|
||||
|
||||
@if((gamelobby.getUserSession(user.get.id).host)) {
|
||||
@for(playersession <- gamelobby.getPlayers.values) {
|
||||
<div class="col-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>
|
||||
<form action="@(routes.IngameController.kickPlayer(gamelobby.id, playersession.id))" method="post">
|
||||
<button type="submit" class="btn btn-danger">Remove</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col text-center mt-3">
|
||||
<a href="@(routes.IngameController.startGame(gamelobby.id))" class="btn btn-success">Start Game</a>
|
||||
</div>
|
||||
</div>
|
||||
} else {
|
||||
@for(playersession <- gamelobby.getPlayers.values) {
|
||||
<div class="col-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>
|
||||
} else {
|
||||
<h5 class="card-title">@playersession.name</h5>
|
||||
<p class="card-text">Your text could be here!</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col mt-3">
|
||||
<p class="text-center fs-4">Waiting for the host to start the game...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col mt-1">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
41
knockoutwhistweb/app/views/login/login.scala.html
Normal file
41
knockoutwhistweb/app/views/login/login.scala.html
Normal file
@@ -0,0 +1,41 @@
|
||||
@()
|
||||
|
||||
@main("Login") {
|
||||
<div class="login-box">
|
||||
<div class="card login-card p-4">
|
||||
<div class="card-body">
|
||||
<h3 class="text-center mb-4">Login</h3>
|
||||
|
||||
<form action="@routes.UserController.login_Post()" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="username" name="username" placeholder="Enter Username" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" 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>
|
||||
<div id="particles-js" style="background-color: rgb(182, 25, 36);
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;"></div>
|
||||
}
|
||||
@@ -13,13 +13,16 @@
|
||||
<title>@title</title>
|
||||
<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 href="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@* And here's where we render the `Html` object containing
|
||||
* the page content. *@
|
||||
@content
|
||||
|
||||
<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>
|
||||
</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)
|
||||
<form action="@routes.MainMenuController.createGame()" method="post">
|
||||
<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" 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">
|
||||
<button type="submit" class="btn btn-success">Create Game</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
54
knockoutwhistweb/app/views/mainmenu/navbar.scala.html
Normal file
54
knockoutwhistweb/app/views/mainmenu/navbar.scala.html
Normal file
@@ -0,0 +1,54 @@
|
||||
@(user: Option[model.users.User])
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navBar" aria-controls="navBar" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navBar">
|
||||
<a class="navbar-brand" href="@routes.MainMenuController.mainMenu()">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="#">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>
|
||||
63
knockoutwhistweb/app/views/mainmenu/rules.scala.html
Normal file
63
knockoutwhistweb/app/views/mainmenu/rules.scala.html
Normal file
@@ -0,0 +1,63 @@
|
||||
@()
|
||||
|
||||
@main("Rules") {
|
||||
<div id="rules" class="game-field game-field-background">
|
||||
<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,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,10 +0,0 @@
|
||||
@(toRender: List[String])
|
||||
|
||||
@main("Sessions") {
|
||||
<div id="sessions">
|
||||
@for(line <- toRender) {
|
||||
<a href="@routes.HomeController.ingame(line)">@line</a><br>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
@(toRender: List[Html])
|
||||
|
||||
@main("Tui") {
|
||||
<div id="tui">
|
||||
@for(line <- toRender) {
|
||||
@line
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1 +1,14 @@
|
||||
# https://www.playframework.com/documentation/latest/Configuration
|
||||
play.filters.disabled += play.filters.csrf.CSRFFilter
|
||||
|
||||
|
||||
auth {
|
||||
issuer = "knockoutwhistweb"
|
||||
audience = "ui"
|
||||
# ${?PUBLIC_KEY_FILE}
|
||||
privateKeyFile = "D:\\Workspaces\\Gitops\\rsa512-private.pem"
|
||||
privateKeyPem = ${?PUBLIC_KEY_PEM}
|
||||
#${?PUBLIC_KEY_FILE}
|
||||
publicKeyFile = "D:\\Workspaces\\Gitops\\rsa512-public.pem"
|
||||
publicKeyPem = ${?PUBLIC_KEY_PEM}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,28 @@
|
||||
# https://www.playframework.com/documentation/latest/ScalaRouting
|
||||
# ~~~~
|
||||
|
||||
# An example controller showing a sample home page
|
||||
|
||||
GET / controllers.HomeController.index()
|
||||
GET /sessions controllers.HomeController.sessions()
|
||||
GET /ingame/:id controllers.HomeController.ingame(id: String)
|
||||
# Map static resources from the /public folder to the /assets URL path
|
||||
# Primary routes
|
||||
GET / controllers.MainMenuController.index()
|
||||
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
|
||||
|
||||
# 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: java.util.UUID)
|
||||
GET /game/:id/leaveGame controllers.IngameController.leaveGame(id: String)
|
||||
POST /game/:id/playCard controllers.IngameController.playCard(id: 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/background.png
Normal file
BIN
knockoutwhistweb/public/images/background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
BIN
knockoutwhistweb/public/images/img.png
Normal file
BIN
knockoutwhistweb/public/images/img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 MiB |
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,3 @@
|
||||
particlesJS.load('particles-js', 'assets/conf/particlesjs-config.json', function() {
|
||||
console.log('callback - particles.js config loaded');
|
||||
});
|
||||
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
@@ -1,9 +1,9 @@
|
||||
package controllers
|
||||
|
||||
import org.scalatestplus.play._
|
||||
import org.scalatestplus.play.guice._
|
||||
import play.api.test._
|
||||
import play.api.test.Helpers._
|
||||
import org.scalatestplus.play.*
|
||||
import org.scalatestplus.play.guice.*
|
||||
import play.api.test.*
|
||||
import play.api.test.Helpers.*
|
||||
|
||||
/**
|
||||
* Add your spec here.
|
||||
@@ -13,33 +13,33 @@ import play.api.test.Helpers._
|
||||
*/
|
||||
class HomeControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting {
|
||||
|
||||
"HomeController GET" should {
|
||||
|
||||
"render the index page from a new instance of controller" in {
|
||||
val controller = new HomeController(stubControllerComponents())
|
||||
val home = controller.index().apply(FakeRequest(GET, "/"))
|
||||
|
||||
status(home) mustBe OK
|
||||
contentType(home) mustBe Some("text/html")
|
||||
contentAsString(home) must include ("Welcome to Play")
|
||||
}
|
||||
|
||||
"render the index page from the application" in {
|
||||
val controller = inject[HomeController]
|
||||
val home = controller.index().apply(FakeRequest(GET, "/"))
|
||||
|
||||
status(home) mustBe OK
|
||||
contentType(home) mustBe Some("text/html")
|
||||
contentAsString(home) must include ("Welcome to Play")
|
||||
}
|
||||
|
||||
"render the index page from the router" in {
|
||||
val request = FakeRequest(GET, "/")
|
||||
val home = route(app, request).get
|
||||
|
||||
status(home) mustBe OK
|
||||
contentType(home) mustBe Some("text/html")
|
||||
contentAsString(home) must include ("Welcome to Play")
|
||||
}
|
||||
}
|
||||
// "HomeController GET" should {
|
||||
//
|
||||
// "render the index page from a new instance of controller" in {
|
||||
// val controller = new HomeController(stubControllerComponents())
|
||||
// val home = controller.index().apply(FakeRequest(GET, "/"))
|
||||
//
|
||||
// status(home) mustBe OK
|
||||
// contentType(home) mustBe Some("text/html")
|
||||
// contentAsString(home) must include ("Welcome to Play")
|
||||
// }
|
||||
//
|
||||
// "render the index page from the application" in {
|
||||
// val controller = inject[HomeController]
|
||||
// val home = controller.index().apply(FakeRequest(GET, "/"))
|
||||
//
|
||||
// status(home) mustBe OK
|
||||
// contentType(home) mustBe Some("text/html")
|
||||
// contentAsString(home) must include ("Welcome to Play")
|
||||
// }
|
||||
//
|
||||
// "render the index page from the router" in {
|
||||
// val request = FakeRequest(GET, "/")
|
||||
// val home = route(app, request).get
|
||||
//
|
||||
// status(home) mustBe OK
|
||||
// contentType(home) mustBe Some("text/html")
|
||||
// contentAsString(home) must include ("Welcome to Play")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -3,4 +3,6 @@ addSbtPlugin("org.foundweekends.giter8" % "sbt-giter8-scaffold" % "0.18.0")
|
||||
|
||||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.2.1")
|
||||
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")
|
||||
|
||||
3
versions.env
Normal file
3
versions.env
Normal file
@@ -0,0 +1,3 @@
|
||||
MAJOR=2
|
||||
MINOR=0
|
||||
PATCH=0
|
||||
Reference in New Issue
Block a user