Compare commits
29 Commits
6c57abb1db
...
archive/ma
| Author | SHA1 | Date | |
|---|---|---|---|
| bef96ba7e3 | |||
| c0dadf8927 | |||
| 1f377de0f4 | |||
| 6c31fa0538 | |||
| 729a4eec6b | |||
| 72fcf783b8 | |||
| 1517d0c006 | |||
| 7f765b4514 | |||
| 03f1811ab4 | |||
| 63689b7a26 | |||
| 92e4851219 | |||
| c168ae7dc0 | |||
| ccf44ede41 | |||
| d71809d6f4 | |||
| 82245d6bcc | |||
| d8576f669a | |||
|
|
cfe27f1e78 | ||
|
|
b33ab184d2 | ||
|
|
b17c5160e9 | ||
| 7458464dd6 | |||
| f8c337fad1 | |||
| 3357fb7310 | |||
| dad604186e | |||
| 8e415390e7 | |||
| fc751af1ef | |||
|
|
9aa447f2f6 | ||
| 2b89f3d161 | |||
| c77eeff123 | |||
| 7cbb6e6ab7 |
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
@@ -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
@@ -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`
|
||||
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[submodule "knockoutwhist"]
|
||||
path = knockoutwhist
|
||||
branch = main
|
||||
url = https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist.git
|
||||
@@ -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"))
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
knockoutwhist
Submodule
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
@@ -0,0 +1,4 @@
|
||||
:root {
|
||||
--background-image: url('/assets/images/img.png');
|
||||
--color: black;
|
||||
}
|
||||
133
knockoutwhistweb/app/assets/stylesheets/main.less
Normal file
@@ -0,0 +1,133 @@
|
||||
@import "light-mode.less";
|
||||
@import "dark-mode.less";
|
||||
|
||||
@background-image: var(--background-image);
|
||||
@color: var(--color);
|
||||
@keyframes slideIn {
|
||||
0% { transform: translateX(-100vw); }
|
||||
100% { transform: translateX(0); }
|
||||
}
|
||||
body {
|
||||
background-image: @background-image;
|
||||
background-size: 100vw 100vh;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
#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, h1, 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%;
|
||||
}
|
||||
#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;
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
package components
|
||||
|
||||
import controllers.WebUI
|
||||
import de.knockoutwhist.components.DefaultConfiguration
|
||||
import de.knockoutwhist.ui.UI
|
||||
import de.knockoutwhist.utils.events.EventListener
|
||||
|
||||
//class WebApplicationConfiguration extends DefaultConfiguration {
|
||||
class WebApplicationConfiguration extends DefaultConfiguration {
|
||||
|
||||
override def uis: Set[UI] = super.uis + WebUI
|
||||
override def listener: Set[EventListener] = super.listener + WebUI
|
||||
|
||||
|
||||
//}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
package controllers
|
||||
|
||||
import javax.inject.*
|
||||
import play.api.*
|
||||
import play.api.mvc.*
|
||||
import com.google.inject.{Guice, Injector}
|
||||
import controllers.sessions.AdvancedSession
|
||||
import de.knockoutwhist.KnockOutWhist
|
||||
import de.knockoutwhist.control.ControlHandler
|
||||
import de.knockoutwhist.ui.tui.TUIMain
|
||||
import de.knockoutwhist.components.Configuration
|
||||
import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak}
|
||||
import de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic
|
||||
import di.KnockOutWebConfigurationModule
|
||||
import play.api.mvc.*
|
||||
import play.api.*
|
||||
import play.twirl.api.Html
|
||||
|
||||
import java.util.UUID
|
||||
import javax.inject.*
|
||||
|
||||
|
||||
/**
|
||||
* This controller creates an `Action` to handle HTTP requests to the
|
||||
@@ -15,6 +23,7 @@ import de.knockoutwhist.ui.tui.TUIMain
|
||||
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.
|
||||
@@ -26,21 +35,59 @@ class HomeController @Inject()(val controllerComponents: ControllerComponents) e
|
||||
def index(): Action[AnyContent] = {
|
||||
if (!initial) {
|
||||
initial = true
|
||||
ControlHandler.addListener(WebUI)
|
||||
KnockOutWhist.main(new Array[String](_length = 0))
|
||||
KnockOutWhist.entry(injector.getInstance(classOf[Configuration]))
|
||||
}
|
||||
Action { implicit request =>
|
||||
Ok(views.html.index.apply())
|
||||
Redirect("/sessions")
|
||||
}
|
||||
}
|
||||
def rules(): Action[AnyContent] = {
|
||||
Action { implicit request =>
|
||||
Ok(views.html.rules.apply())
|
||||
}
|
||||
}
|
||||
def sessions(): Action[AnyContent] = {
|
||||
Action { implicit request =>
|
||||
Ok(views.html.sessions.apply(PodGameManager.listSessions()))
|
||||
}
|
||||
}
|
||||
|
||||
def ingame(): Action[AnyContent] = {
|
||||
Action { implicit request =>
|
||||
Ok(views.html.tui.apply(WebUI.latestOutput))
|
||||
}
|
||||
def ingame(id: String): Action[AnyContent] = {
|
||||
val uuid: UUID = UUID.fromString(id)
|
||||
if (PodGameManager.identify(uuid).isEmpty) {
|
||||
return Action { implicit request =>
|
||||
NotFound(views.html.tui.apply(List(Html(s"<p>Session with id $id not found!</p>"))))
|
||||
}
|
||||
} else {
|
||||
val session = PodGameManager.identify(uuid).get
|
||||
val player = session.asInstanceOf[AdvancedSession].player
|
||||
val logic = WebUI.logic.get.asInstanceOf[BaseGameLogic]
|
||||
if (logic.getCurrentState == Lobby) {
|
||||
|
||||
def showTUI(): Action[AnyContent] = Action { implicit request =>
|
||||
Ok(views.html.tui.render(WebUI.latestOutput))
|
||||
} else if (logic.getCurrentState == InGame) {
|
||||
return Action { implicit request =>
|
||||
Ok(views.html.ingame.apply(player, logic))
|
||||
}
|
||||
} else if (logic.getCurrentState == SelectTrump) {
|
||||
return Action { implicit request =>
|
||||
Ok(views.html.selecttrump.apply(player, logic))
|
||||
}
|
||||
} else if (logic.getCurrentState == TieBreak) {
|
||||
return Action { implicit request =>
|
||||
Ok(views.html.tie.apply(player, logic))
|
||||
}
|
||||
}
|
||||
}
|
||||
Action { implicit request =>
|
||||
InternalServerError("Oops")
|
||||
}
|
||||
//if (logic.getCurrentState == Lobby) {
|
||||
//Action { implicit request =>
|
||||
//Ok(views.html.tui.apply(player, logic))
|
||||
//}
|
||||
//} else {
|
||||
//Action { implicit request =>
|
||||
//Ok(views.html.tui.apply(player, logic))
|
||||
//}
|
||||
}
|
||||
}
|
||||
37
knockoutwhistweb/app/controllers/PodGameManager.scala
Normal file
@@ -0,0 +1,37 @@
|
||||
package controllers
|
||||
|
||||
import controllers.sessions.PlayerSession
|
||||
import de.knockoutwhist.utils.events.SimpleEvent
|
||||
|
||||
import java.util.UUID
|
||||
import scala.collection.mutable
|
||||
|
||||
object PodGameManager {
|
||||
|
||||
private val sessions: mutable.Map[UUID, PlayerSession] = mutable.Map()
|
||||
|
||||
def addSession(session: PlayerSession): Unit = {
|
||||
sessions.put(session.id, session)
|
||||
}
|
||||
|
||||
def clearSessions(): Unit = {
|
||||
sessions.clear()
|
||||
}
|
||||
|
||||
def identify(id: UUID): Option[PlayerSession] = {
|
||||
sessions.get(id)
|
||||
}
|
||||
|
||||
def transmit(id: UUID, event: SimpleEvent): Unit = {
|
||||
identify(id).foreach(_.updatePlayer(event))
|
||||
}
|
||||
|
||||
def transmitAll(event: SimpleEvent): Unit = {
|
||||
sessions.foreach(session => session._2.updatePlayer(event))
|
||||
}
|
||||
|
||||
def listSessions(): List[PlayerSession] = {
|
||||
sessions.values.toList
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,521 +1,49 @@
|
||||
package controllers
|
||||
|
||||
import de.knockoutwhist.events.directional.RequestPickTrumpsuitEvent
|
||||
import de.knockoutwhist.events.round.ShowCurrentTrickEvent
|
||||
import de.knockoutwhist.ui.UI
|
||||
import de.knockoutwhist.ui.tui.TUIMain.{init, runLater, start}
|
||||
import de.knockoutwhist.utils.events.{EventListener, SimpleEvent}
|
||||
import de.knockoutwhist.KnockOutWhist
|
||||
import controllers.sessions.AdvancedSession
|
||||
import de.knockoutwhist.cards.{Card, CardValue, Hand, Suit}
|
||||
import de.knockoutwhist.control.controllerBaseImpl.{PlayerLogic, TrickLogic}
|
||||
import de.knockoutwhist.control.{ControlHandler, ControlThread}
|
||||
import de.knockoutwhist.control.GameLogic
|
||||
import de.knockoutwhist.control.GameState.{InGame, Lobby}
|
||||
import de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic
|
||||
import de.knockoutwhist.events.*
|
||||
import de.knockoutwhist.events.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.directional.*
|
||||
import de.knockoutwhist.events.round.ShowCurrentTrickEvent
|
||||
import de.knockoutwhist.events.ui.GameState.MAIN_MENU
|
||||
import de.knockoutwhist.events.ui.{GameState, GameStateUpdateEvent}
|
||||
import de.knockoutwhist.events.util.DelayEvent
|
||||
import de.knockoutwhist.player.Playertype.HUMAN
|
||||
import de.knockoutwhist.player.{AbstractPlayer, PlayerFactory}
|
||||
import de.knockoutwhist.events.global.GameStateChangeEvent
|
||||
import de.knockoutwhist.player.AbstractPlayer
|
||||
import de.knockoutwhist.rounds.Match
|
||||
import de.knockoutwhist.ui.UI
|
||||
import de.knockoutwhist.undo.{UndoManager, UndoneException}
|
||||
import de.knockoutwhist.utils.CustomThread
|
||||
import de.knockoutwhist.utils.events.{EventListener, SimpleEvent}
|
||||
|
||||
import java.io.{BufferedReader, InputStreamReader}
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import scala.annotation.tailrec
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
object WebUI extends CustomThread with EventListener with UI {
|
||||
|
||||
override def initial: Boolean = {
|
||||
setName("WebUI")
|
||||
|
||||
var init = false
|
||||
var logic: Option[GameLogic] = None
|
||||
|
||||
var latestOutput: String = ""
|
||||
|
||||
override def instance: CustomThread = WebUI
|
||||
|
||||
override def listen(event: SimpleEvent): Unit = {
|
||||
event match {
|
||||
case event: GameStateChangeEvent =>
|
||||
if (event.oldState == Lobby && event.newState == InGame) {
|
||||
val match1: Option[Match] = logic.get.asInstanceOf[BaseGameLogic].getCurrentMatch
|
||||
val players: List[AbstractPlayer] = match1.get.totalplayers
|
||||
players.map(player => PodGameManager.addSession(AdvancedSession(player.id, player)))
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
override def initial(gameLogic: GameLogic): Boolean = {
|
||||
if (init) {
|
||||
return false
|
||||
}
|
||||
init = true
|
||||
this.logic = Some(gameLogic)
|
||||
start()
|
||||
true
|
||||
}
|
||||
setName("WebUI")
|
||||
|
||||
override def instance: CustomThread = WebUI
|
||||
|
||||
var init = false
|
||||
override def runLater[R](op: => R): Unit = {
|
||||
interrupted.set(true)
|
||||
super.runLater(op)
|
||||
}
|
||||
var latestOutput: String = ""
|
||||
private var internState: GameState = GameState.NO_SET
|
||||
override def listen(event: SimpleEvent): Unit = {
|
||||
runLater {
|
||||
event match {
|
||||
case event: RenderHandEvent =>
|
||||
renderhandmethod(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: RequestTieNumberEvent =>
|
||||
reqnumbereventmet(event)
|
||||
case event: RequestCardEvent =>
|
||||
reqcardeventmet(event)
|
||||
case event: RequestDogPlayCardEvent =>
|
||||
reqdogeventmet(event)
|
||||
case event: RequestPickTrumpsuitEvent =>
|
||||
reqpicktevmet(event)
|
||||
case event: ShowCurrentTrickEvent =>
|
||||
showcurtrevmet(event)
|
||||
case event: GameStateUpdateEvent =>
|
||||
if (internState != event.gameState) {
|
||||
internState = event.gameState
|
||||
if (event.gameState == GameState.MAIN_MENU) {
|
||||
mainMenu()
|
||||
} else if (event.gameState == GameState.PLAYERS) {
|
||||
reqplayersevent()
|
||||
}
|
||||
Some(true)
|
||||
}
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
object WebUICards {
|
||||
def renderCardAsString(card: Card): Vector[String] = {
|
||||
val lines = "│ │"
|
||||
if (card.cardValue == CardValue.Ten) {
|
||||
latestOutput += Vector(
|
||||
s"┌─────────┐",
|
||||
s"│${cardColour(card.suit)}${Console.BOLD}${card.cardValue.cardType()}${Console.RESET} │",
|
||||
lines,
|
||||
s"│ ${cardColour(card.suit)}${Console.BOLD}${card.suit.cardType()}${Console.RESET} │",
|
||||
lines,
|
||||
s"│ ${cardColour(card.suit)}${Console.BOLD}${card.cardValue.cardType()}${Console.RESET}│",
|
||||
s"└─────────┘"
|
||||
)
|
||||
return Vector(
|
||||
s"┌─────────┐",
|
||||
s"│${cardColour(card.suit)}${Console.BOLD}${card.cardValue.cardType()}${Console.RESET} │",
|
||||
lines,
|
||||
s"│ ${cardColour(card.suit)}${Console.BOLD}${card.suit.cardType()}${Console.RESET} │",
|
||||
lines,
|
||||
s"│ ${cardColour(card.suit)}${Console.BOLD}${card.cardValue.cardType()}${Console.RESET}│",
|
||||
s"└─────────┘"
|
||||
)
|
||||
}
|
||||
latestOutput += Vector(
|
||||
s"┌─────────┐",
|
||||
s"│${cardColour(card.suit)}${Console.BOLD}${card.cardValue.cardType()}${Console.RESET} │",
|
||||
lines,
|
||||
s"│ ${cardColour(card.suit)}${Console.BOLD}${card.suit.cardType()}${Console.RESET} │",
|
||||
lines,
|
||||
s"│ ${cardColour(card.suit)}${Console.BOLD}${card.cardValue.cardType()}${Console.RESET}│",
|
||||
s"└─────────┘"
|
||||
)
|
||||
return Vector(
|
||||
s"┌─────────┐",
|
||||
s"│${cardColour(card.suit)}${Console.BOLD}${card.cardValue.cardType()}${Console.RESET} │",
|
||||
lines,
|
||||
s"│ ${cardColour(card.suit)}${Console.BOLD}${card.suit.cardType()}${Console.RESET} │",
|
||||
lines,
|
||||
s"│ ${cardColour(card.suit)}${Console.BOLD}${card.cardValue.cardType()}${Console.RESET}│",
|
||||
s"└─────────┘"
|
||||
)
|
||||
}
|
||||
|
||||
private def cardColour(suit: Suit): String = suit match {
|
||||
case Suit.Hearts | Suit.Diamonds => Console.RED
|
||||
case Suit.Clubs | Suit.Spades => Console.BLACK
|
||||
}
|
||||
|
||||
def renderHandEvent(hand: Hand, showNumbers: Boolean): Vector[String] = {
|
||||
val cardStrings = hand.cards.map(WebUICards.renderCardAsString)
|
||||
var zipped = cardStrings.transpose
|
||||
if (showNumbers) zipped = {
|
||||
List.tabulate(hand.cards.length) { i =>
|
||||
s" ${i + 1} "
|
||||
}
|
||||
} :: zipped
|
||||
latestOutput += zipped.map(_.mkString(" ")).toVector
|
||||
zipped.map(_.mkString(" ")).toVector
|
||||
}
|
||||
}
|
||||
|
||||
//override def initial: Boolean = {
|
||||
//if (init) {
|
||||
//return false
|
||||
//}
|
||||
//init = true
|
||||
//start()
|
||||
//true
|
||||
//}
|
||||
|
||||
@tailrec
|
||||
private def mainMenu(): Unit = {
|
||||
latestOutput = ""
|
||||
latestOutput += "Welcome to Knockout Whist\n"
|
||||
latestOutput += "Please select an option:\n"
|
||||
latestOutput += "1. Start a new match\n"
|
||||
latestOutput += "2. Exit\n"
|
||||
Try {
|
||||
input().toInt
|
||||
} match {
|
||||
case Success(value) =>
|
||||
value match {
|
||||
case 1 =>
|
||||
ControlThread.runLater {
|
||||
KnockOutWhist.config.maincomponent.startMatch()
|
||||
}
|
||||
case 2 =>
|
||||
println("Exiting the game.")
|
||||
System.exit(0)
|
||||
case _ =>
|
||||
showerrstatmet(ShowErrorStatus(INVALID_NUMBER))
|
||||
ControlThread.runLater {
|
||||
ControlHandler.invoke(DelayEvent(500))
|
||||
ControlHandler.invoke(GameStateUpdateEvent(MAIN_MENU))
|
||||
}
|
||||
mainMenu()
|
||||
}
|
||||
case Failure(exception) =>
|
||||
exception match {
|
||||
case undo: UndoneException =>
|
||||
case _ =>
|
||||
showerrstatmet(ShowErrorStatus(NOT_A_NUMBER))
|
||||
ControlThread.runLater {
|
||||
ControlHandler.invoke(DelayEvent(500))
|
||||
ControlHandler.invoke(GameStateUpdateEvent(MAIN_MENU))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def renderhandmethod(event: RenderHandEvent): Option[Boolean] = {
|
||||
WebUICards.renderHandEvent(event.hand, event.showNumbers).foreach(println)
|
||||
Some(true)
|
||||
}
|
||||
|
||||
private def showtiecardseventmethod(event: ShowTieCardsEvent): Option[Boolean] = {
|
||||
val a: Array[String] = Array("", "", "", "", "", "", "", "")
|
||||
for ((player, card) <- event.card) {
|
||||
val playerNameLength = player.name.length
|
||||
a(0) += " " + player.name + ":" + (" " * (playerNameLength - 1))
|
||||
val rendered = WebUICards.renderCardAsString(card)
|
||||
a(1) += " " + rendered(0)
|
||||
a(2) += " " + rendered(1)
|
||||
a(3) += " " + rendered(2)
|
||||
a(4) += " " + rendered(3)
|
||||
a(5) += " " + rendered(4)
|
||||
a(6) += " " + rendered(5)
|
||||
a(7) += " " + rendered(6)
|
||||
}
|
||||
a.foreach(println)
|
||||
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 =>
|
||||
latestOutput = ""
|
||||
println("Starting a new match...")
|
||||
wait(1000)
|
||||
latestOutput = ""
|
||||
Some(true)
|
||||
case SHOW_TYPE_PLAYERS =>
|
||||
latestOutput += "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 {
|
||||
latestOutput = ""
|
||||
latestOutput += 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_TURN =>
|
||||
println("It's your turn, " + player.name + ".")
|
||||
Some(true)
|
||||
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.")
|
||||
wait(2000)
|
||||
latestOutput = ""
|
||||
Some(true)
|
||||
}
|
||||
}
|
||||
|
||||
private def showroundstatusmethod(event: ShowRoundStatus): Option[Boolean] = {
|
||||
event.status match {
|
||||
case SHOW_START_ROUND =>
|
||||
latestOutput = ""
|
||||
println(s"Starting a new round. The trump suit is ${event.currentRound.trumpSuit}.")
|
||||
wait(2000)
|
||||
latestOutput = ""
|
||||
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 =>
|
||||
latestOutput += "Please enter a valid input"
|
||||
Some(true)
|
||||
case INVALID_NUMBER_OF_PLAYERS =>
|
||||
latestOutput += "Please enter at least two names."
|
||||
Some(true)
|
||||
case IDENTICAL_NAMES =>
|
||||
latestOutput += "Please enter unique names."
|
||||
Some(true)
|
||||
case INVALID_NAME_FORMAT =>
|
||||
latestOutput += "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 {
|
||||
latestOutput += f"You have to play a card of suit: ${event.objects.head.asInstanceOf[Card].suit}\n"
|
||||
Some(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def reqnumbereventmet(event: RequestTieNumberEvent): Option[Boolean] = {
|
||||
val tryTie = Try {
|
||||
val number = input().toInt
|
||||
if (number < 1 || number > event.remaining) {
|
||||
throw new IllegalArgumentException(s"Number must be between 1 and ${event.remaining}")
|
||||
}
|
||||
number
|
||||
}
|
||||
if (tryTie.isFailure && tryTie.failed.get.isInstanceOf[UndoneException]) {
|
||||
return Some(true)
|
||||
}
|
||||
ControlThread.runLater {
|
||||
KnockOutWhist.config.playerlogcomponent.selectedTie(event.winner, event.matchImpl, event.round, event.playersout, event.cut, tryTie, event.currentStep, event.remaining, event.currentIndex)
|
||||
}
|
||||
Some(true)
|
||||
}
|
||||
|
||||
private def reqcardeventmet(event: RequestCardEvent): Option[Boolean] = {
|
||||
val tryCard = Try {
|
||||
val card = input().toInt - 1
|
||||
if (card < 0 || card >= event.hand.cards.length) {
|
||||
throw new IllegalArgumentException(s"Number has to be between 1 and ${event.hand.cards.length}")
|
||||
} else {
|
||||
event.hand.cards(card)
|
||||
}
|
||||
}
|
||||
if (tryCard.isFailure && tryCard.failed.get.isInstanceOf[UndoneException]) {
|
||||
return Some(true)
|
||||
}
|
||||
ControlThread.runLater {
|
||||
KnockOutWhist.config.trickcomponent.controlSuitplayed(tryCard, event.matchImpl, event.round, event.trick, event.currentIndex, event.player)
|
||||
}
|
||||
Some(true)
|
||||
}
|
||||
|
||||
private def reqdogeventmet(event: RequestDogPlayCardEvent): Option[Boolean] = {
|
||||
val tryDogCard = Try {
|
||||
val card = input()
|
||||
if (card.equalsIgnoreCase("y")) {
|
||||
Some(event.hand.cards.head)
|
||||
} else if (card.equalsIgnoreCase("n") && !event.needstoplay) {
|
||||
None
|
||||
} else {
|
||||
throw new IllegalArgumentException("Didn't want to play card but had to")
|
||||
}
|
||||
}
|
||||
if (tryDogCard.isFailure && tryDogCard.failed.get.isInstanceOf[UndoneException]) {
|
||||
return Some(true)
|
||||
}
|
||||
ControlThread.runLater {
|
||||
KnockOutWhist.config.trickcomponent.controlDogPlayed(tryDogCard, event.matchImpl, event.round, event.trick, event.currentIndex, event.player)
|
||||
}
|
||||
Some(true)
|
||||
}
|
||||
|
||||
private def reqplayersevent(): Option[Boolean] = {
|
||||
showglobalstatusmethod(ShowGlobalStatus(SHOW_TYPE_PLAYERS))
|
||||
val names = Try {
|
||||
input().split(",")
|
||||
}
|
||||
if (names.isFailure && names.failed.get.isInstanceOf[UndoneException]) {
|
||||
return Some(true)
|
||||
}
|
||||
if (names.get.length < 2) {
|
||||
showerrstatmet(ShowErrorStatus(INVALID_NUMBER_OF_PLAYERS))
|
||||
return reqplayersevent()
|
||||
}
|
||||
if (names.get.distinct.length != names.get.length) {
|
||||
showerrstatmet(ShowErrorStatus(IDENTICAL_NAMES))
|
||||
return reqplayersevent()
|
||||
}
|
||||
if (names.get.count(_.trim.isBlank) > 0
|
||||
|| names.get.count(_.trim.length <= 2) > 0
|
||||
|| names.get.count(_.trim.length > 10) > 0) {
|
||||
showerrstatmet(ShowErrorStatus(INVALID_NAME_FORMAT))
|
||||
return reqplayersevent()
|
||||
}
|
||||
ControlThread.runLater {
|
||||
KnockOutWhist.config
|
||||
.maincomponent
|
||||
.enteredPlayers(names.get
|
||||
.map(s => PlayerFactory.createPlayer(s, playertype = HUMAN))
|
||||
.toList)
|
||||
}
|
||||
Some(true)
|
||||
}
|
||||
|
||||
private def reqpicktevmet(event: RequestPickTrumpsuitEvent): Option[Boolean] = {
|
||||
val trySuit = Try {
|
||||
val suit = input().toInt
|
||||
suit match {
|
||||
case 1 => Suit.Hearts
|
||||
case 2 => Suit.Diamonds
|
||||
case 3 => Suit.Clubs
|
||||
case 4 => Suit.Spades
|
||||
case _ => throw IllegalArgumentException("Didn't enter a number between 1 and 4")
|
||||
}
|
||||
}
|
||||
if (trySuit.isFailure && trySuit.failed.get.isInstanceOf[UndoneException]) {
|
||||
return Some(true)
|
||||
}
|
||||
ControlThread.runLater {
|
||||
KnockOutWhist.config.playerlogcomponent.trumpSuitSelected(event.matchImpl, trySuit, event.remaining_players, event.firstRound, event.player)
|
||||
}
|
||||
Some(true)
|
||||
}
|
||||
|
||||
private def showcurtrevmet(event: ShowCurrentTrickEvent): Option[Boolean] = {
|
||||
latestOutput = ""
|
||||
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")
|
||||
}
|
||||
latestOutput += sb.toString()
|
||||
//println(sb.toString())
|
||||
Some(true)
|
||||
}
|
||||
|
||||
private val isInIO: AtomicBoolean = new AtomicBoolean(false)
|
||||
private val interrupted: AtomicBoolean = new AtomicBoolean(false)
|
||||
|
||||
private def input(): String = {
|
||||
interrupted.set(false)
|
||||
val reader = new BufferedReader(new InputStreamReader(System.in))
|
||||
|
||||
while (!interrupted.get()) {
|
||||
if (reader.ready()) {
|
||||
val in = reader.readLine()
|
||||
if (in.equals("undo")) {
|
||||
UndoManager.undoStep()
|
||||
throw new UndoneException("Undo")
|
||||
} else if (in.equals("redo")) {
|
||||
UndoManager.redoStep()
|
||||
throw new UndoneException("Redo")
|
||||
} else if (in.equals("load")
|
||||
&& KnockOutWhist.config.persistenceManager.canLoadfile("currentSnapshot")) {
|
||||
KnockOutWhist.config.persistenceManager.loadFile("currentSnapshot.json")
|
||||
throw new UndoneException("Load")
|
||||
} else if (in.equals("save")) {
|
||||
KnockOutWhist.config.persistenceManager.saveFile("currentSnapshot.json")
|
||||
}
|
||||
return in
|
||||
}
|
||||
Thread.sleep(50)
|
||||
}
|
||||
throw new UndoneException("Skipped")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package controllers.sessions
|
||||
|
||||
import de.knockoutwhist.player.AbstractPlayer
|
||||
import de.knockoutwhist.utils.events.SimpleEvent
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
case class AdvancedSession(id: UUID, player: AbstractPlayer) extends PlayerSession {
|
||||
|
||||
def name: String = player.name
|
||||
|
||||
override def updatePlayer(event: SimpleEvent): Unit = {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package controllers.sessions
|
||||
|
||||
import de.knockoutwhist.utils.events.SimpleEvent
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
trait PlayerSession {
|
||||
|
||||
def id: UUID
|
||||
def name: String
|
||||
def updatePlayer(event: SimpleEvent): Unit
|
||||
|
||||
}
|
||||
13
knockoutwhistweb/app/di/KnockOutWebConfigurationModule.scala
Normal file
@@ -0,0 +1,13 @@
|
||||
package di
|
||||
|
||||
import com.google.inject.AbstractModule
|
||||
import components.WebApplicationConfiguration
|
||||
import de.knockoutwhist.components.Configuration
|
||||
|
||||
class KnockOutWebConfigurationModule extends AbstractModule {
|
||||
|
||||
override def configure(): Unit = {
|
||||
bind(classOf[Configuration]).to(classOf[WebApplicationConfiguration])
|
||||
}
|
||||
|
||||
}
|
||||
34
knockoutwhistweb/app/util/WebUIUtils.scala
Normal file
@@ -0,0 +1,34 @@
|
||||
package util
|
||||
|
||||
import de.knockoutwhist.cards.Card
|
||||
import de.knockoutwhist.cards.CardValue.*
|
||||
import de.knockoutwhist.cards.Suit.{Clubs, Diamonds, Hearts, Spades}
|
||||
import play.twirl.api.Html
|
||||
import scalafx.scene.image.Image
|
||||
|
||||
object WebUIUtils {
|
||||
def cardtoImage(card: Card): Html = {
|
||||
val s = card.suit match {
|
||||
case Spades => "S"
|
||||
case Hearts => "H"
|
||||
case Clubs => "C"
|
||||
case Diamonds => "D"
|
||||
}
|
||||
val cv = card.cardValue match {
|
||||
case Ace => "A"
|
||||
case King => "K"
|
||||
case Queen => "Q"
|
||||
case Jack => "J"
|
||||
case Ten => "T"
|
||||
case Nine => "9"
|
||||
case Eight => "8"
|
||||
case Seven => "7"
|
||||
case Six => "6"
|
||||
case Five => "5"
|
||||
case Four => "4"
|
||||
case Three => "3"
|
||||
case Two => "2"
|
||||
}
|
||||
views.html.output.card.apply(f"images/cards/$cv$s.png")(card.toString)
|
||||
}
|
||||
}
|
||||
50
knockoutwhistweb/app/views/ingame.scala.html
Normal file
@@ -0,0 +1,50 @@
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic)
|
||||
|
||||
@main("Ingame") {
|
||||
<div id="ingame">
|
||||
<h1>Knockout Whist</h1>
|
||||
<div id="nextPlayers">
|
||||
<p>Next Player:</p>
|
||||
<p>@logic.getPlayerQueue.get.duplicate().nextPlayer()</p>
|
||||
</div>
|
||||
<div id="firstCard">
|
||||
<div id="trumpsuit">
|
||||
<p>Trumpsuit: </p>
|
||||
<p>@logic.getCurrentRound.get.trumpSuit</p>
|
||||
</div>
|
||||
<div id="firstCardObject">
|
||||
<p>First Card</p>
|
||||
@if(logic.getCurrentTrick.get.firstCard.isDefined) {
|
||||
@util.WebUIUtils.cardtoImage(logic.getCurrentTrick.get.firstCard.get)
|
||||
} else {
|
||||
@views.html.output.card.apply("images/cards/1B.png")("Blank Card")
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>@logic.getCurrentPlayer.get has to play a card!</p>
|
||||
@if(logic.getCurrentTrick.get.cards.nonEmpty) {
|
||||
<p>Cards played</p>
|
||||
} else {
|
||||
<p id="invisible">Cards played</p>
|
||||
}
|
||||
|
||||
<div id="cardsplayed">
|
||||
@for((cardplayed, player) <- logic.getCurrentTrick.get.cards) {
|
||||
<div id="playedcardplayer">
|
||||
<p>@player</p>
|
||||
@util.WebUIUtils.cardtoImage(cardplayed)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<p>Your cards</p>
|
||||
<div id="playercards">
|
||||
@for(card <- player.currentHand().get.cards) {
|
||||
@util.WebUIUtils.cardtoImage(card)
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
}
|
||||
@@ -1,10 +1,25 @@
|
||||
@(title: String)(content: play.twirl.api.Html)
|
||||
@*
|
||||
* This template is called from the `index` template. This template
|
||||
* handles the rendering of the page header and body tags. It takes
|
||||
* two arguments, a `String` for the title of the page and an `Html`
|
||||
* object to insert into the body of the page.
|
||||
*@
|
||||
@(title: String)(content: Html)
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@* Here's where we render the page title `String`. *@
|
||||
<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")">
|
||||
</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>
|
||||
</body>
|
||||
</html>
|
||||
2
knockoutwhistweb/app/views/output/card.scala.html
Normal file
@@ -0,0 +1,2 @@
|
||||
@(src: String)(alt: String)
|
||||
<img src="@routes.Assets.versioned(src)" alt="@alt"/>
|
||||
3
knockoutwhistweb/app/views/output/text.scala.html
Normal file
@@ -0,0 +1,3 @@
|
||||
@(text: String)
|
||||
<p>@text</p>
|
||||
|
||||
63
knockoutwhistweb/app/views/rules.scala.html
Normal file
@@ -0,0 +1,63 @@
|
||||
@()
|
||||
|
||||
@main("Rules") {
|
||||
<div id="rules">
|
||||
<table>
|
||||
<caption>Rules Overview and Equipment</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Section</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Players</td>
|
||||
<td>Two to seven players. The aim is to be the last player left in the game.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Aim</td>
|
||||
<td>To be the last player left in at the end of the game, with the object in each hand being to win a majority of tricks.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Equipment</td>
|
||||
<td>A standard 52-card pack is used.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Card Ranks</td>
|
||||
<td>In each suit, cards rank from highest to lowest: A K Q J 10 9 8 7 6 5 4 3 2.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Deal (First Hand)</td>
|
||||
<td>The dealer deals seven cards to each player. One card is turned up to determine the trump suit for the round.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Deal (Subsequent Hands)</td>
|
||||
<td>The deal rotates clockwise. The player who took the most tricks in the previous hand selects the trump suit. If there's a tie for the highest number of tricks, players cut cards to decide who calls trumps. One fewer card is dealt in each successive hand until the final hand consists of one card each.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Play</td>
|
||||
<td>The player to the dealer's left (eldest hand) leads the first trick. Any card can be led. Other players must follow suit if possible. A player with no cards of the suit led may play any card.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Winning a Trick</td>
|
||||
<td>The highest card of the suit led wins, unless a trump is played, in which case the highest trump wins. The winner of the trick leads the next.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Leading Trumps</td>
|
||||
<td>Some rules disallow leading trumps before the suit has been 'broken' (a trump has been played to the lead of another suit). Leading trumps is always permissible if a player holds only trumps.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Knockout</td>
|
||||
<td>At the end of each hand, any player who took no tricks is knocked out and takes no further part in the game.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Winning the Game</td>
|
||||
<td>The game is won when a player takes all the tricks in a round, as all other players are knocked out, leaving only one player remaining.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<td>Dog Life</td>
|
||||
<td>The first player who takes no tricks is awarded a "dog's life". In the next hand, that player is dealt one card and can decide which trick to play it to. Each time a trick is played the "dog" may either play the card or knock on the table and wait to play it later. If the dog wins a trick, the player to the left leads to the next and the dog re-enters the game properly in the next hand. If the dog fails, they are knocked out.</td>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
27
knockoutwhistweb/app/views/selecttrump.scala.html
Normal file
@@ -0,0 +1,27 @@
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic)
|
||||
|
||||
@main("Selecting Trumpsuit...") {
|
||||
<div id="selecttrumpsuit">
|
||||
@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>
|
||||
}
|
||||
12
knockoutwhistweb/app/views/sessions.scala.html
Normal file
@@ -0,0 +1,12 @@
|
||||
@(sessions: List[controllers.sessions.PlayerSession])
|
||||
|
||||
@main("Sessions") {
|
||||
<div id="sessions">
|
||||
<h1>Knockout Whist sessions</h1>
|
||||
<p id="textanimation">Please select your session to jump inside the game!</p>
|
||||
@for(session <- sessions) {
|
||||
<a id="textanimation" href="@routes.HomeController.ingame(session.id.toString)">@session.name</a><br>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
27
knockoutwhistweb/app/views/tie.scala.html
Normal file
@@ -0,0 +1,27 @@
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic)
|
||||
|
||||
@main("Tie") {
|
||||
<div id="tie">
|
||||
<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>
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
@(renderTUI: String)
|
||||
@(toRender: List[Html])
|
||||
|
||||
@main("Welcome to Play") {
|
||||
<h1>@renderTUI</h1>
|
||||
@main("Tui") {
|
||||
<div id="tui">
|
||||
@for(line <- toRender) {
|
||||
@line
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
# ~~~~
|
||||
|
||||
# An example controller showing a sample home page
|
||||
|
||||
GET / controllers.HomeController.index()
|
||||
GET /ingame controllers.HomeController.ingame()
|
||||
GET /showTUI controllers.HomeController.showTUI()
|
||||
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
|
||||
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
|
||||
|
||||
GET /rules controllers.HomeController.rules()
|
||||
|
||||
BIN
knockoutwhistweb/public/images/background.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
knockoutwhistweb/public/images/cards/1B.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
knockoutwhistweb/public/images/cards/1J.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
knockoutwhistweb/public/images/cards/2B.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
knockoutwhistweb/public/images/cards/2C.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
knockoutwhistweb/public/images/cards/2D.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
knockoutwhistweb/public/images/cards/2H.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
knockoutwhistweb/public/images/cards/2J.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
knockoutwhistweb/public/images/cards/2S.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
knockoutwhistweb/public/images/cards/3C.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
knockoutwhistweb/public/images/cards/3D.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
knockoutwhistweb/public/images/cards/3H.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
knockoutwhistweb/public/images/cards/3S.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
knockoutwhistweb/public/images/cards/4C.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
knockoutwhistweb/public/images/cards/4D.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
knockoutwhistweb/public/images/cards/4H.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
knockoutwhistweb/public/images/cards/4S.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
knockoutwhistweb/public/images/cards/5C.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
knockoutwhistweb/public/images/cards/5D.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
knockoutwhistweb/public/images/cards/5H.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
knockoutwhistweb/public/images/cards/5S.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
knockoutwhistweb/public/images/cards/6C.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
knockoutwhistweb/public/images/cards/6D.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
knockoutwhistweb/public/images/cards/6H.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
knockoutwhistweb/public/images/cards/6S.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
knockoutwhistweb/public/images/cards/7C.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
knockoutwhistweb/public/images/cards/7D.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
knockoutwhistweb/public/images/cards/7H.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
knockoutwhistweb/public/images/cards/7S.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
knockoutwhistweb/public/images/cards/8C.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
knockoutwhistweb/public/images/cards/8D.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
knockoutwhistweb/public/images/cards/8H.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
knockoutwhistweb/public/images/cards/8S.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
knockoutwhistweb/public/images/cards/9C.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
knockoutwhistweb/public/images/cards/9D.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
knockoutwhistweb/public/images/cards/9H.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
knockoutwhistweb/public/images/cards/9S.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
knockoutwhistweb/public/images/cards/AC.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
knockoutwhistweb/public/images/cards/ACB.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
knockoutwhistweb/public/images/cards/AD.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
knockoutwhistweb/public/images/cards/ADB.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
knockoutwhistweb/public/images/cards/AH.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
knockoutwhistweb/public/images/cards/AHB.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
knockoutwhistweb/public/images/cards/AS.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
knockoutwhistweb/public/images/cards/ASB.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
knockoutwhistweb/public/images/cards/JC.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
knockoutwhistweb/public/images/cards/JD.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
knockoutwhistweb/public/images/cards/JH.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
knockoutwhistweb/public/images/cards/JS.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
knockoutwhistweb/public/images/cards/KC.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
knockoutwhistweb/public/images/cards/KD.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
knockoutwhistweb/public/images/cards/KH.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
knockoutwhistweb/public/images/cards/KS.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
knockoutwhistweb/public/images/cards/QC.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
knockoutwhistweb/public/images/cards/QD.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
knockoutwhistweb/public/images/cards/QH.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
knockoutwhistweb/public/images/cards/QS.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
knockoutwhistweb/public/images/cards/TC.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
knockoutwhistweb/public/images/cards/TD.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
knockoutwhistweb/public/images/cards/TH.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
knockoutwhistweb/public/images/cards/TS.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
knockoutwhistweb/public/images/img.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
@@ -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.
|
||||
|
||||
@@ -4,3 +4,5 @@ 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("com.typesafe.sbt" % "sbt-less" % "1.1.2")
|
||||
|
||||