feat!: implemented multigame support (#34)
Reviewed-on: #34 Co-authored-by: Janis <janis.e.20@gmx.de> Co-committed-by: Janis <janis.e.20@gmx.de>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -137,3 +137,4 @@ target
|
||||
/knockoutwhist/
|
||||
/knockoutwhistweb/.g8/
|
||||
/knockoutwhistweb/.bsp/
|
||||
/currentSnapshot.json
|
||||
|
||||
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: uZDNZA
|
||||
}
|
||||
|
||||
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: uZDNZA
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
8
bruno/KnockOutWhist/Game/folder.bru
Normal file
8
bruno/KnockOutWhist/Game/folder.bru
Normal file
@@ -0,0 +1,8 @@
|
||||
meta {
|
||||
name: Game
|
||||
seq: 3
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
26
bruno/KnockOutWhist/Login.bru
Normal file
26
bruno/KnockOutWhist/Login.bru
Normal file
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Login
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/login
|
||||
body: formUrlEncoded
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:form-urlencoded {
|
||||
username: Janis
|
||||
password: password123
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
username: Janis
|
||||
password: password123
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
}
|
||||
9
bruno/KnockOutWhist/bruno.json
Normal file
9
bruno/KnockOutWhist/bruno.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "KnockOutWhist",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
]
|
||||
}
|
||||
0
bruno/KnockOutWhist/collection.bru
Normal file
0
bruno/KnockOutWhist/collection.bru
Normal file
3
bruno/KnockOutWhist/environments/Local.bru
Normal file
3
bruno/KnockOutWhist/environments/Local.bru
Normal file
@@ -0,0 +1,3 @@
|
||||
vars {
|
||||
host: http://localhost:9000
|
||||
}
|
||||
@@ -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("."))
|
||||
|
||||
Submodule knockoutwhist updated: fbc0ea2277...e0e45c4b43
35
knockoutwhistweb/app/assets/stylesheets/login.less
Normal file
35
knockoutwhistweb/app/assets/stylesheets/login.less
Normal file
@@ -0,0 +1,35 @@
|
||||
.login-box {
|
||||
position: fixed; /* changed from absolute to fixed so it centers relative to the viewport */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%); /* center exactly */
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 420px; /* keeps box from stretching too wide */
|
||||
padding: 1rem;
|
||||
z-index: 2; /* above particles */
|
||||
}
|
||||
|
||||
.login-card {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||
position: relative;
|
||||
z-index: 3; /* ensure card sits above the particles */
|
||||
}
|
||||
|
||||
#particles-js {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0; /* behind content */
|
||||
pointer-events: none; /* allow clicks through particles */
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
@import "light-mode.less";
|
||||
@import "dark-mode.less";
|
||||
@import "login.less";
|
||||
|
||||
@background-image: var(--background-image);
|
||||
@color: var(--color);
|
||||
@@ -7,14 +8,16 @@
|
||||
0% { transform: translateX(-100vw); }
|
||||
100% { transform: translateX(0); }
|
||||
}
|
||||
body {
|
||||
.game-field-background {
|
||||
background-image: @background-image;
|
||||
background-size: 100vw 100vh;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
.game-field {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
#sessions {
|
||||
display: flex;
|
||||
@@ -31,8 +34,9 @@ html, body {
|
||||
animation: slideIn 0.5s ease-out forwards;
|
||||
animation-fill-mode: backwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
#sessions a, h1, p {
|
||||
}
|
||||
|
||||
#sessions a, #sessions h1, #sessions p {
|
||||
color: @color;
|
||||
font-size: 40px;
|
||||
font-family: Arial, serif;
|
||||
@@ -44,6 +48,11 @@ html, body {
|
||||
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;
|
||||
|
||||
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 controllers.WebUI
|
||||
import de.knockoutwhist.components.DefaultConfiguration
|
||||
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,93 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import com.google.inject.{Guice, Injector}
|
||||
import controllers.sessions.AdvancedSession
|
||||
import de.knockoutwhist.KnockOutWhist
|
||||
import de.knockoutwhist.components.Configuration
|
||||
import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak}
|
||||
import de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic
|
||||
import di.KnockOutWebConfigurationModule
|
||||
import play.api.mvc.*
|
||||
import play.api.*
|
||||
import play.twirl.api.Html
|
||||
|
||||
import java.util.UUID
|
||||
import javax.inject.*
|
||||
|
||||
|
||||
/**
|
||||
* This controller creates an `Action` to handle HTTP requests to the
|
||||
* application's home page.
|
||||
*/
|
||||
@Singleton
|
||||
class HomeController @Inject()(val controllerComponents: ControllerComponents) extends BaseController {
|
||||
|
||||
private var initial = false
|
||||
private val injector: Injector = Guice.createInjector(KnockOutWebConfigurationModule())
|
||||
|
||||
/**
|
||||
* Create an Action to render an HTML page.
|
||||
*
|
||||
* The configuration in the `routes` file means that this method
|
||||
* will be called when the application receives a `GET` request with
|
||||
* a path of `/`.
|
||||
*/
|
||||
def index(): Action[AnyContent] = {
|
||||
if (!initial) {
|
||||
initial = true
|
||||
KnockOutWhist.entry(injector.getInstance(classOf[Configuration]))
|
||||
}
|
||||
Action { implicit request =>
|
||||
Redirect("/sessions")
|
||||
}
|
||||
}
|
||||
def rules(): Action[AnyContent] = {
|
||||
Action { implicit request =>
|
||||
Ok(views.html.rules.apply())
|
||||
}
|
||||
}
|
||||
def sessions(): Action[AnyContent] = {
|
||||
Action { implicit request =>
|
||||
Ok(views.html.sessions.apply(PodGameManager.listSessions()))
|
||||
}
|
||||
}
|
||||
|
||||
def ingame(id: String): Action[AnyContent] = {
|
||||
val uuid: UUID = UUID.fromString(id)
|
||||
if (PodGameManager.identify(uuid).isEmpty) {
|
||||
return Action { implicit request =>
|
||||
NotFound(views.html.tui.apply(List(Html(s"<p>Session with id $id not found!</p>"))))
|
||||
}
|
||||
} else {
|
||||
val session = PodGameManager.identify(uuid).get
|
||||
val player = session.asInstanceOf[AdvancedSession].player
|
||||
val logic = WebUI.logic.get.asInstanceOf[BaseGameLogic]
|
||||
if (logic.getCurrentState == Lobby) {
|
||||
|
||||
} else if (logic.getCurrentState == InGame) {
|
||||
return Action { implicit request =>
|
||||
Ok(views.html.ingame.apply(player, logic))
|
||||
}
|
||||
} else if (logic.getCurrentState == SelectTrump) {
|
||||
return Action { implicit request =>
|
||||
Ok(views.html.selecttrump.apply(player, logic))
|
||||
}
|
||||
} else if (logic.getCurrentState == TieBreak) {
|
||||
return Action { implicit request =>
|
||||
Ok(views.html.tie.apply(player, logic))
|
||||
}
|
||||
}
|
||||
}
|
||||
Action { implicit request =>
|
||||
InternalServerError("Oops")
|
||||
}
|
||||
//if (logic.getCurrentState == Lobby) {
|
||||
//Action { implicit request =>
|
||||
//Ok(views.html.tui.apply(player, logic))
|
||||
//}
|
||||
//} else {
|
||||
//Action { implicit request =>
|
||||
//Ok(views.html.tui.apply(player, logic))
|
||||
//}
|
||||
}
|
||||
}
|
||||
244
knockoutwhistweb/app/controllers/IngameController.scala
Normal file
244
knockoutwhistweb/app/controllers/IngameController.scala
Normal file
@@ -0,0 +1,244 @@
|
||||
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 play.api.*
|
||||
import play.api.mvc.*
|
||||
|
||||
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("Lobby: " + gameId)
|
||||
case InGame =>
|
||||
Ok(views.html.ingame.ingame(
|
||||
g.getPlayerByUser(request.user),
|
||||
g.logic
|
||||
))
|
||||
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) {
|
||||
NoContent
|
||||
} 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 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) =>
|
||||
val result = Try {
|
||||
g.playCard(g.getUserSession(request.user.id), cardId.toInt)
|
||||
}
|
||||
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))
|
||||
val result = Try {
|
||||
cardIdOpt match {
|
||||
case Some(cardId) if cardId == "skip" =>
|
||||
g.playDogCard(g.getUserSession(request.user.id), -1)
|
||||
case Some(cardId) =>
|
||||
g.playDogCard(g.getUserSession(request.user.id), cardId.toInt)
|
||||
case None =>
|
||||
throw new IllegalArgumentException("cardId parameter is missing")
|
||||
}
|
||||
}
|
||||
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) =>
|
||||
val result = Try {
|
||||
g.selectTrump(g.getUserSession(request.user.id), trump.toInt)
|
||||
}
|
||||
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) =>
|
||||
val result = Try {
|
||||
g.selectTie(g.getUserSession(request.user.id), tie.toInt)
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
45
knockoutwhistweb/app/controllers/MainMenuController.scala
Normal file
45
knockoutwhistweb/app/controllers/MainMenuController.scala
Normal file
@@ -0,0 +1,45 @@
|
||||
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("Main Menu for user: " + request.user.name)
|
||||
}
|
||||
|
||||
def index(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
Redirect(routes.MainMenuController.mainMenu())
|
||||
}
|
||||
|
||||
def createGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val gameLobby = podManager.createGame(
|
||||
host = request.user,
|
||||
name = s"${request.user.name}'s Game",
|
||||
maxPlayers = 4
|
||||
)
|
||||
Redirect(routes.IngameController.game(gameLobby.id))
|
||||
}
|
||||
|
||||
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[PlayerSession] = {
|
||||
sessions.values.toList
|
||||
}
|
||||
|
||||
}
|
||||
70
knockoutwhistweb/app/controllers/UserController.scala
Normal file
70
knockoutwhistweb/app/controllers/UserController.scala
Normal file
@@ -0,0 +1,70 @@
|
||||
package controllers
|
||||
|
||||
import auth.{AuthAction, AuthenticatedRequest}
|
||||
import logic.user.{SessionManager, UserManager}
|
||||
import play.api.*
|
||||
import play.api.mvc.*
|
||||
|
||||
import javax.inject.*
|
||||
|
||||
|
||||
/**
|
||||
* This controller creates an `Action` to handle HTTP requests to the
|
||||
* application's home page.
|
||||
*/
|
||||
@Singleton
|
||||
class UserController @Inject()(
|
||||
val controllerComponents: ControllerComponents,
|
||||
val sessionManager: SessionManager,
|
||||
val userManager: UserManager,
|
||||
val authAction: AuthAction
|
||||
) extends BaseController {
|
||||
|
||||
def login(): Action[AnyContent] = {
|
||||
Action { implicit request =>
|
||||
val session = request.cookies.get("sessionId")
|
||||
if (session.isDefined) {
|
||||
val possibleUser = sessionManager.getUserBySession(session.get.value)
|
||||
if (possibleUser.isDefined) {
|
||||
Redirect(routes.MainMenuController.mainMenu())
|
||||
} else {
|
||||
Ok(views.html.login.login())
|
||||
}
|
||||
} else {
|
||||
Ok(views.html.login.login())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def login_Post(): Action[AnyContent] = {
|
||||
Action { implicit request =>
|
||||
val postData = request.body.asFormUrlEncoded
|
||||
if (postData.isDefined) {
|
||||
// Extract username and password from form data
|
||||
val username = postData.get.get("username").flatMap(_.headOption).getOrElse("")
|
||||
val password = postData.get.get("password").flatMap(_.headOption).getOrElse("")
|
||||
val possibleUser = userManager.authenticate(username, password)
|
||||
if (possibleUser.isDefined) {
|
||||
Redirect(routes.MainMenuController.mainMenu()).withCookies(
|
||||
Cookie("sessionId", sessionManager.createSession(possibleUser.get))
|
||||
)
|
||||
} else {
|
||||
println("Failed login attempt for user: " + username)
|
||||
Unauthorized("Invalid username or password")
|
||||
}
|
||||
} else {
|
||||
BadRequest("Invalid form submission")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass the request-handling function directly to authAction (no nested Action)
|
||||
def logout(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val sessionCookie = request.cookies.get("sessionId")
|
||||
if (sessionCookie.isDefined) {
|
||||
sessionManager.invalidateSession(sessionCookie.get.value)
|
||||
}
|
||||
NoContent.discardingCookies(DiscardingCookie("sessionId"))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import controllers.sessions.AdvancedSession
|
||||
import de.knockoutwhist.cards.{Card, CardValue, Hand, Suit}
|
||||
import de.knockoutwhist.control.GameLogic
|
||||
import de.knockoutwhist.control.GameState.{InGame, Lobby}
|
||||
import de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic
|
||||
import de.knockoutwhist.events.*
|
||||
import de.knockoutwhist.events.global.GameStateChangeEvent
|
||||
import de.knockoutwhist.player.AbstractPlayer
|
||||
import de.knockoutwhist.rounds.Match
|
||||
import de.knockoutwhist.ui.UI
|
||||
import de.knockoutwhist.utils.CustomThread
|
||||
import de.knockoutwhist.utils.events.{EventListener, SimpleEvent}
|
||||
|
||||
object WebUI extends CustomThread with EventListener with UI {
|
||||
|
||||
setName("WebUI")
|
||||
|
||||
var init = false
|
||||
var logic: Option[GameLogic] = None
|
||||
|
||||
var latestOutput: String = ""
|
||||
|
||||
override def instance: CustomThread = WebUI
|
||||
|
||||
override def listen(event: SimpleEvent): Unit = {
|
||||
event match {
|
||||
case event: GameStateChangeEvent =>
|
||||
if (event.oldState == Lobby && event.newState == InGame) {
|
||||
val match1: Option[Match] = logic.get.asInstanceOf[BaseGameLogic].getCurrentMatch
|
||||
val players: List[AbstractPlayer] = match1.get.totalplayers
|
||||
players.map(player => PodGameManager.addSession(AdvancedSession(player.id, player)))
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
override def initial(gameLogic: GameLogic): Boolean = {
|
||||
if (init) {
|
||||
return false
|
||||
}
|
||||
init = true
|
||||
this.logic = Some(gameLogic)
|
||||
start()
|
||||
true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package exceptions;
|
||||
|
||||
public class CantPlayCardException extends GameException {
|
||||
public CantPlayCardException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
7
knockoutwhistweb/app/exceptions/GameException.java
Normal file
7
knockoutwhistweb/app/exceptions/GameException.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package exceptions;
|
||||
|
||||
public abstract class GameException extends RuntimeException {
|
||||
public GameException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
7
knockoutwhistweb/app/exceptions/GameFullException.java
Normal file
7
knockoutwhistweb/app/exceptions/GameFullException.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package exceptions;
|
||||
|
||||
public class GameFullException extends GameException {
|
||||
public GameFullException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package exceptions;
|
||||
|
||||
public class NotEnoughPlayersException extends GameException {
|
||||
public NotEnoughPlayersException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
7
knockoutwhistweb/app/exceptions/NotHostException.java
Normal file
7
knockoutwhistweb/app/exceptions/NotHostException.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package exceptions;
|
||||
|
||||
public class NotHostException extends GameException {
|
||||
public NotHostException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package exceptions;
|
||||
|
||||
public class NotInThisGameException extends GameException {
|
||||
public NotInThisGameException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package exceptions;
|
||||
|
||||
public class NotInteractableException extends GameException {
|
||||
public NotInteractableException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
49
knockoutwhistweb/app/logic/PodManager.scala
Normal file
49
knockoutwhistweb/app/logic/PodManager.scala
Normal file
@@ -0,0 +1,49 @@
|
||||
package logic
|
||||
|
||||
import com.google.inject.{Guice, Injector}
|
||||
import de.knockoutwhist.components.Configuration
|
||||
import de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic
|
||||
import di.KnockOutWebConfigurationModule
|
||||
import logic.game.GameLobby
|
||||
import model.users.User
|
||||
import util.GameUtil
|
||||
|
||||
import javax.inject.Singleton
|
||||
import scala.collection.mutable
|
||||
|
||||
@Singleton
|
||||
class PodManager {
|
||||
|
||||
val TTL: Long = System.currentTimeMillis() + 86400000L // 24 hours in milliseconds
|
||||
val podIp: String = System.getenv("POD_IP")
|
||||
val podName: String = System.getenv("POD_NAME")
|
||||
|
||||
private val sessions: mutable.Map[String, GameLobby] = mutable.Map()
|
||||
private val injector: Injector = Guice.createInjector(KnockOutWebConfigurationModule())
|
||||
|
||||
def createGame(
|
||||
host: User,
|
||||
name: String,
|
||||
maxPlayers: Int
|
||||
): GameLobby = {
|
||||
val gameLobby = GameLobby(
|
||||
logic = BaseGameLogic(injector.getInstance(classOf[Configuration])),
|
||||
id = GameUtil.generateCode(),
|
||||
internalId = java.util.UUID.randomUUID(),
|
||||
name = name,
|
||||
maxPlayers = maxPlayers,
|
||||
host = host
|
||||
)
|
||||
sessions += (gameLobby.id -> gameLobby)
|
||||
gameLobby
|
||||
}
|
||||
|
||||
def getGame(gameId: String): Option[GameLobby] = {
|
||||
sessions.get(gameId)
|
||||
}
|
||||
|
||||
private[logic] def removeGame(gameId: String): Unit = {
|
||||
sessions.remove(gameId)
|
||||
}
|
||||
|
||||
}
|
||||
246
knockoutwhistweb/app/logic/game/GameLobby.scala
Normal file
246
knockoutwhistweb/app/logic/game/GameLobby.scala
Normal file
@@ -0,0 +1,246 @@
|
||||
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!")
|
||||
}
|
||||
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(user: User): Unit = {
|
||||
val sessionOpt = users.get(user.id)
|
||||
if (sessionOpt.isEmpty) {
|
||||
throw new NotInThisGameException("You are not in this game!")
|
||||
}
|
||||
users.remove(user.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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!")
|
||||
}
|
||||
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)
|
||||
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)
|
||||
logic.playerInputLogic.receivedTrumpSuit(selectedTrump)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param userSession
|
||||
* @param tieNumber
|
||||
*/
|
||||
def selectTie(userSession: UserSession, tieNumber: Int): Unit = {
|
||||
val player = getPlayerInteractable(userSession, InteractionType.TieChoice)
|
||||
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))
|
||||
}
|
||||
|
||||
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 (!Thread.holdsLock(userSession.lock)) {
|
||||
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
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package controllers.sessions
|
||||
package model.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 {
|
||||
case class SimpleSession(id: UUID, player: AbstractPlayer) extends PlayerSession {
|
||||
|
||||
def name: String = player.name
|
||||
|
||||
31
knockoutwhistweb/app/model/sessions/UserSession.scala
Normal file
31
knockoutwhistweb/app/model/sessions/UserSession.scala
Normal file
@@ -0,0 +1,31 @@
|
||||
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: Lock = 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
|
||||
|
||||
}
|
||||
20
knockoutwhistweb/app/model/users/User.scala
Normal file
20
knockoutwhistweb/app/model/users/User.scala
Normal file
@@ -0,0 +1,20 @@
|
||||
package model.users
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
case class User(
|
||||
internalId: Long,
|
||||
id: UUID,
|
||||
name: String,
|
||||
passwordHash: String
|
||||
) {
|
||||
|
||||
def withName(newName: String): User = {
|
||||
this.copy(name = newName)
|
||||
}
|
||||
|
||||
private def withPasswordHash(newPasswordHash: String): User = {
|
||||
this.copy(passwordHash = newPasswordHash)
|
||||
}
|
||||
|
||||
}
|
||||
56
knockoutwhistweb/app/services/JwtKeyProvider.scala
Normal file
56
knockoutwhistweb/app/services/JwtKeyProvider.scala
Normal file
@@ -0,0 +1,56 @@
|
||||
package services
|
||||
|
||||
import play.api.Configuration
|
||||
|
||||
import java.nio.file.{Files, Paths}
|
||||
import java.security.interfaces.{RSAPrivateKey, RSAPublicKey}
|
||||
import java.security.spec.{PKCS8EncodedKeySpec, RSAPublicKeySpec, X509EncodedKeySpec}
|
||||
import java.security.{KeyFactory, KeyPair, PrivateKey, PublicKey}
|
||||
import java.util.Base64
|
||||
import javax.inject.*
|
||||
|
||||
@Singleton
|
||||
class JwtKeyProvider @Inject()(config: Configuration) {
|
||||
|
||||
private def cleanPem(pem: String): String =
|
||||
pem.replaceAll("-----BEGIN (.*)-----", "")
|
||||
.replaceAll("-----END (.*)-----", "")
|
||||
.replaceAll("\\s", "")
|
||||
|
||||
private def loadPublicKeyFromPem(pem: String): RSAPublicKey = {
|
||||
val decoded = Base64.getDecoder.decode(cleanPem(pem))
|
||||
val spec = new X509EncodedKeySpec(decoded)
|
||||
KeyFactory.getInstance("RSA").generatePublic(spec).asInstanceOf[RSAPublicKey]
|
||||
}
|
||||
|
||||
private def loadPrivateKeyFromPem(pem: String): RSAPrivateKey = {
|
||||
val decoded = Base64.getDecoder.decode(cleanPem(pem))
|
||||
val spec = new PKCS8EncodedKeySpec(decoded)
|
||||
KeyFactory.getInstance("RSA").generatePrivate(spec).asInstanceOf[RSAPrivateKey]
|
||||
}
|
||||
|
||||
val publicKey: RSAPublicKey = {
|
||||
val pemOpt = config.getOptional[String]("auth.publicKeyPem")
|
||||
val fileOpt = config.getOptional[String]("auth.publicKeyFile")
|
||||
|
||||
pemOpt.orElse(fileOpt.map { path =>
|
||||
new String(Files.readAllBytes(Paths.get(path)))
|
||||
}) match {
|
||||
case Some(pem) => loadPublicKeyFromPem(pem)
|
||||
case None => throw new RuntimeException("No RSA public key configured.")
|
||||
}
|
||||
}
|
||||
|
||||
val privateKey: RSAPrivateKey = {
|
||||
val pemOpt = config.getOptional[String]("auth.privateKeyPem")
|
||||
val fileOpt = config.getOptional[String]("auth.privateKeyFile")
|
||||
|
||||
pemOpt.orElse(fileOpt.map { path =>
|
||||
new String(Files.readAllBytes(Paths.get(path)))
|
||||
}) match {
|
||||
case Some(pem) => loadPrivateKeyFromPem(pem)
|
||||
case None => throw new RuntimeException("No RSA private key configured.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
29
knockoutwhistweb/app/util/GameUtil.scala
Normal file
29
knockoutwhistweb/app/util/GameUtil.scala
Normal file
@@ -0,0 +1,29 @@
|
||||
package util
|
||||
|
||||
import scala.util.Random
|
||||
|
||||
object GameUtil {
|
||||
|
||||
private val CharPool: String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
private val CodeLength: Int = 6
|
||||
private val MaxRepetition: Int = 2
|
||||
private val random = new Random()
|
||||
|
||||
def generateCode(): String = {
|
||||
val freq = Array.fill(CharPool.length)(0)
|
||||
val code = new StringBuilder(CodeLength)
|
||||
|
||||
for (_ <- 0 until CodeLength) {
|
||||
var index = random.nextInt(CharPool.length)
|
||||
// Pick a new character if it's already used twice
|
||||
while (freq(index) >= MaxRepetition) {
|
||||
index = random.nextInt(CharPool.length)
|
||||
}
|
||||
freq(index) += 1
|
||||
code.append(CharPool.charAt(index))
|
||||
}
|
||||
|
||||
code.toString()
|
||||
}
|
||||
|
||||
}
|
||||
23
knockoutwhistweb/app/util/UserHash.scala
Normal file
23
knockoutwhistweb/app/util/UserHash.scala
Normal file
@@ -0,0 +1,23 @@
|
||||
package util
|
||||
|
||||
import de.mkammerer.argon2.Argon2Factory
|
||||
import de.mkammerer.argon2.Argon2Factory.Argon2Types
|
||||
import model.users.User
|
||||
|
||||
object UserHash {
|
||||
private val ITERATIONS: Int = 3
|
||||
private val MEMORY: Int = 32_768
|
||||
private val PARALLELISM: Int = 1
|
||||
private val SALT_LENGTH: Int = 32
|
||||
private val HASH_LENGTH: Int = 64
|
||||
private val ARGON_2 = Argon2Factory.create(Argon2Types.ARGON2id, SALT_LENGTH, HASH_LENGTH)
|
||||
|
||||
def hashPW(password: String): String = {
|
||||
ARGON_2.hash(ITERATIONS, MEMORY, PARALLELISM, password.toCharArray)
|
||||
}
|
||||
|
||||
def verifyUser(password: String, user: User): Boolean = {
|
||||
ARGON_2.verify(user.passwordHash, password.toCharArray)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,6 +29,6 @@ object WebUIUtils {
|
||||
case Three => "3"
|
||||
case Two => "2"
|
||||
}
|
||||
views.html.output.card.apply(f"images/cards/$cv$s.png")(card.toString)
|
||||
views.html.render.card.apply(f"images/cards/$cv$s.png")(card.toString)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
@main("Welcome to Play") {
|
||||
<h1>Welcome to Play!</h1>
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic)
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic)
|
||||
|
||||
@main("Ingame") {
|
||||
<div id="ingame">
|
||||
<div id="ingame" class="game-field game-field-background">
|
||||
<h1>Knockout Whist</h1>
|
||||
<div id="nextPlayers">
|
||||
<p>Next Player:</p>
|
||||
@@ -17,7 +17,7 @@
|
||||
@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")
|
||||
@views.html.render.card.apply("../../../public/images/cards/1B.png")("Blank Card")
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,7 +1,7 @@
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic)
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic)
|
||||
|
||||
@main("Selecting Trumpsuit...") {
|
||||
<div id="selecttrumpsuit">
|
||||
<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>
|
||||
@@ -1,7 +1,7 @@
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic)
|
||||
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic)
|
||||
|
||||
@main("Tie") {
|
||||
<div id="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) {
|
||||
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,6 +13,8 @@
|
||||
<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>
|
||||
@@ -21,5 +23,6 @@
|
||||
@content
|
||||
|
||||
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
@(sessions: List[controllers.sessions.PlayerSession])
|
||||
|
||||
@main("Sessions") {
|
||||
<div id="sessions">
|
||||
<h1>Knockout Whist sessions</h1>
|
||||
<p id="textanimation">Please select your session to jump inside the game!</p>
|
||||
@for(session <- sessions) {
|
||||
<a id="textanimation" href="@routes.HomeController.ingame(session.id.toString)">@session.name</a><br>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
@(toRender: List[Html])
|
||||
|
||||
@main("Tui") {
|
||||
<div id="tui">
|
||||
@for(line <- toRender) {
|
||||
@line
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1 +1,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,12 +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)
|
||||
|
||||
GET /rules controllers.HomeController.rules()
|
||||
# Main menu routes
|
||||
GET /mainmenu controllers.MainMenuController.mainMenu()
|
||||
GET /rules controllers.MainMenuController.rules()
|
||||
|
||||
POST /createGame controllers.MainMenuController.createGame()
|
||||
|
||||
# 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)
|
||||
POST /game/:id/join controllers.IngameController.joinGame(id: String)
|
||||
|
||||
POST /game/:id/start controllers.IngameController.startGame(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
|
||||
}
|
||||
@@ -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
@@ -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")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user