diff --git a/knockoutwhist b/knockoutwhist
index a5dcf3e..372f20c 160000
--- a/knockoutwhist
+++ b/knockoutwhist
@@ -1 +1 @@
-Subproject commit a5dcf3ee904ab548479e23ca7b146df14a835b80
+Subproject commit 372f20ca6c3308dee21d9fc946689a8fd77cb465
diff --git a/knockoutwhistweb/app/assets/stylesheets/main.less b/knockoutwhistweb/app/assets/stylesheets/main.less
index b726d46..b9713a8 100644
--- a/knockoutwhistweb/app/assets/stylesheets/main.less
+++ b/knockoutwhistweb/app/assets/stylesheets/main.less
@@ -49,6 +49,16 @@
box-shadow: 3px 3px 3px @highlightcolor;
}
+.inactive::after {
+ content: "";
+ position: absolute;
+ inset: 0; /* cover the whole container */
+ background: rgba(0, 0, 0, 0.50);
+ z-index: 10;
+ border-radius: 6px;
+ pointer-events: none; /* user can't click through overlay */
+}
+
.bottom-div {
position: fixed;
bottom: 0;
diff --git a/knockoutwhistweb/app/controllers/IngameController.scala b/knockoutwhistweb/app/controllers/IngameController.scala
index 83cc988..7a7afad 100644
--- a/knockoutwhistweb/app/controllers/IngameController.scala
+++ b/knockoutwhistweb/app/controllers/IngameController.scala
@@ -1,14 +1,16 @@
package controllers
import auth.{AuthAction, AuthenticatedRequest}
-import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak}
+import de.knockoutwhist.control.GameState.{FinishedMatch, InGame, Lobby, SelectTrump, TieBreak}
import exceptions.*
import logic.PodManager
import logic.game.GameLobby
import model.sessions.UserSession
+import model.users.User
import play.api.*
import play.api.libs.json.{JsValue, Json}
import play.api.mvc.*
+import play.twirl.api.Html
import java.util.UUID
import javax.inject.*
@@ -22,29 +24,47 @@ class IngameController @Inject() (
val authAction: AuthAction,
implicit val ec: ExecutionContext
) extends AbstractController(cc) {
+
+ def returnInnerHTML(gameLobby: GameLobby, user: User): Html = {
+ gameLobby.logic.getCurrentState match {
+ case Lobby => views.html.lobby.lobby(Some(user), gameLobby)
+ case InGame =>
+ views.html.ingame.ingame(
+ gameLobby.getPlayerByUser(user),
+ gameLobby
+ )
+ case SelectTrump =>
+ views.html.ingame.selecttrump(
+ gameLobby.getPlayerByUser(user),
+ gameLobby
+ )
+ case TieBreak =>
+ views.html.ingame.tie(
+ gameLobby.getPlayerByUser(user),
+ gameLobby
+ )
+ case FinishedMatch =>
+ views.html.ingame.finishedMatch(
+ Some(user),
+ gameLobby
+ )
+ case _ =>
+ throw new IllegalStateException(s"Invalid game state for in-game view. GameId: ${gameLobby.id}" + s" State: ${gameLobby.logic.getCurrentState}")
+ }
+ }
+
def game(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val game = podManager.getGame(gameId)
game match {
case Some(g) =>
- g.logic.getCurrentState match {
- case Lobby => Ok(views.html.lobby.lobby(Some(request.user), g))
- case InGame =>
- Ok(views.html.ingame.ingame(
- g.getPlayerByUser(request.user),
- g
- ))
- case SelectTrump =>
- Ok(views.html.ingame.selecttrump(
- g.getPlayerByUser(request.user),
- g
- ))
- case TieBreak =>
- Ok(views.html.ingame.tie(
- g.getPlayerByUser(request.user),
- g
- ))
- case _ =>
- InternalServerError(s"Invalid game state for in-game view. GameId: $gameId" + s" State: ${g.logic.getCurrentState}")
+ val results = Try {
+ returnInnerHTML(g, request.user)
+
+ }
+ if (results.isSuccess) {
+ Ok(views.html.main("In-Game - Knockout Whist")(results.get))
+ } else {
+ InternalServerError(results.failed.get.getMessage)
}
case None =>
NotFound("Game not found")
@@ -126,32 +146,7 @@ class IngameController @Inject() (
))
}
}
- 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 {
@@ -255,15 +250,30 @@ class IngameController @Inject() (
val throwable = result.failed.get
throwable match {
case _: CantPlayCardException =>
- BadRequest(throwable.getMessage)
+ BadRequest(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> throwable.getMessage
+ ))
case _: NotInThisGameException =>
- BadRequest(throwable.getMessage)
+ BadRequest(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> throwable.getMessage
+ ))
case _: IllegalArgumentException =>
- BadRequest(throwable.getMessage)
+ BadRequest(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> throwable.getMessage
+ ))
case _: IllegalStateException =>
- BadRequest(throwable.getMessage)
+ BadRequest(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> throwable.getMessage
+ ))
case _ =>
- InternalServerError(throwable.getMessage)
+ InternalServerError(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> throwable.getMessage
+ ))
}
}
}
@@ -276,7 +286,10 @@ class IngameController @Inject() (
val game = podManager.getGame(gameId)
game match {
case Some(g) =>
- val trumpOpt = request.body.asFormUrlEncoded.flatMap(_.get("trump").flatMap(_.headOption))
+ val jsonBody = request.body.asJson
+ val trumpOpt: Option[String] = jsonBody.flatMap { jsValue =>
+ (jsValue \ "trump").asOpt[String]
+ }
trumpOpt match {
case Some(trump) =>
var optSession: Option[UserSession] = None
@@ -293,13 +306,25 @@ class IngameController @Inject() (
val throwable = result.failed.get
throwable match {
case _: IllegalArgumentException =>
- BadRequest(throwable.getMessage)
+ BadRequest(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> throwable.getMessage
+ ))
case _: NotInThisGameException =>
- BadRequest(throwable.getMessage)
+ BadRequest(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> throwable.getMessage
+ ))
case _: IllegalStateException =>
- BadRequest(throwable.getMessage)
+ BadRequest(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> throwable.getMessage
+ ))
case _ =>
- InternalServerError(throwable.getMessage)
+ InternalServerError(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> throwable.getMessage
+ ))
}
}
case None =>
@@ -313,7 +338,10 @@ class IngameController @Inject() (
val game = podManager.getGame(gameId)
game match {
case Some(g) =>
- val tieOpt = request.body.asFormUrlEncoded.flatMap(_.get("tie").flatMap(_.headOption))
+ val jsonBody = request.body.asJson
+ val tieOpt: Option[String] = jsonBody.flatMap { jsValue =>
+ (jsValue \ "tie").asOpt[String]
+ }
tieOpt match {
case Some(tie) =>
var optSession: Option[UserSession] = None
@@ -330,13 +358,25 @@ class IngameController @Inject() (
val throwable = result.failed.get
throwable match {
case _: IllegalArgumentException =>
- BadRequest(throwable.getMessage)
+ BadRequest(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> throwable.getMessage
+ ))
case _: NotInThisGameException =>
- BadRequest(throwable.getMessage)
+ BadRequest(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> throwable.getMessage
+ ))
case _: IllegalStateException =>
- BadRequest(throwable.getMessage)
+ BadRequest(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> throwable.getMessage
+ ))
case _ =>
- InternalServerError(throwable.getMessage)
+ InternalServerError(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> throwable.getMessage
+ ))
}
}
case None =>
diff --git a/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala b/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala
index 92beeab..b2869d6 100644
--- a/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala
+++ b/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala
@@ -12,16 +12,22 @@ class JavaScriptRoutingController @Inject()(
val authAction: AuthAction,
val podManager: PodManager
) extends BaseController {
- def javascriptRoutes(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
+ def javascriptRoutes(): Action[AnyContent] =
+ Action { implicit request =>
Ok(
JavaScriptReverseRouter("jsRoutes")(
routes.javascript.MainMenuController.createGame,
routes.javascript.IngameController.startGame,
+ routes.javascript.MainMenuController.joinGame,
routes.javascript.IngameController.kickPlayer,
routes.javascript.IngameController.leaveGame,
routes.javascript.IngameController.playCard,
routes.javascript.IngameController.playDogCard,
- routes.javascript.PollingController.polling
+ routes.javascript.IngameController.playTrump,
+ routes.javascript.IngameController.playTie,
+ routes.javascript.IngameController.returnToLobby,
+ routes.javascript.PollingController.polling,
+ routes.javascript.UserController.login_Post
)
).as("text/javascript")
}
diff --git a/knockoutwhistweb/app/controllers/MainMenuController.scala b/knockoutwhistweb/app/controllers/MainMenuController.scala
index b4c0e4b..bf41033 100644
--- a/knockoutwhistweb/app/controllers/MainMenuController.scala
+++ b/knockoutwhistweb/app/controllers/MainMenuController.scala
@@ -17,12 +17,13 @@ import javax.inject.*
class MainMenuController @Inject()(
val controllerComponents: ControllerComponents,
val authAction: AuthAction,
- val podManager: PodManager
+ val podManager: PodManager,
+ val ingameController: IngameController
) extends BaseController {
// Pass the request-handling function directly to authAction (no nested Action)
def mainMenu(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
- Ok(views.html.mainmenu.creategame(Some(request.user)))
+ Ok(views.html.main("KnockOutWhist")(views.html.mainmenu.creategame(Some(request.user))))
}
def index(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
@@ -45,7 +46,8 @@ class MainMenuController @Inject()(
)
Ok(Json.obj(
"status" -> "success",
- "redirectUrl" -> routes.IngameController.game(gameLobby.id).url
+ "redirectUrl" -> routes.IngameController.game(gameLobby.id).url,
+ "content" -> ingameController.returnInnerHTML(gameLobby, request.user).toString
))
} else {
BadRequest(Json.obj(
@@ -57,18 +59,31 @@ class MainMenuController @Inject()(
}
def joinGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
- val postData = request.body.asFormUrlEncoded
- if (postData.isDefined) {
- val gameId = postData.get.get("gameId").flatMap(_.headOption).getOrElse("")
- val game = podManager.getGame(gameId)
+ val jsonBody = request.body.asJson
+ val gameId: Option[String] = jsonBody.flatMap { jsValue =>
+ (jsValue \ "gameId").asOpt[String]
+ }
+ if (gameId.isDefined) {
+ val game = podManager.getGame(gameId.get)
game match {
case Some(g) =>
- Redirect(routes.IngameController.joinGame(gameId))
+ g.addUser(request.user)
+ Ok(Json.obj(
+ "status" -> "success",
+ "redirectUrl" -> routes.IngameController.game(g.id).url,
+ "content" -> ingameController.returnInnerHTML(g, request.user).toString
+ ))
case None =>
- NotFound("Game not found")
+ NotFound(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> "No Game found"
+ ))
}
} else {
- BadRequest("Invalid form submission")
+ BadRequest(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> "Invalid form submission"
+ ))
}
}
diff --git a/knockoutwhistweb/app/controllers/PollingController.scala b/knockoutwhistweb/app/controllers/PollingController.scala
index bce96b2..360f2e7 100644
--- a/knockoutwhistweb/app/controllers/PollingController.scala
+++ b/knockoutwhistweb/app/controllers/PollingController.scala
@@ -6,7 +6,7 @@ import de.knockoutwhist.cards.Hand
import de.knockoutwhist.player.AbstractPlayer
import logic.PodManager
import logic.game.{GameLobby, PollingEvents}
-import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, ReloadEvent}
+import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, NewTrick, ReloadEvent}
import model.sessions.UserSession
import model.users.User
import play.api.libs.json.{JsArray, JsValue, Json}
@@ -26,6 +26,7 @@ class PollingController @Inject() (
val cc: ControllerComponents,
val podManager: PodManager,
val authAction: AuthAction,
+ val ingameController: IngameController,
implicit val ec: ExecutionContext
) extends AbstractController(cc) {
@@ -64,7 +65,8 @@ class PollingController @Inject() (
"trickCards" -> trickCardsJson,
"scoreTable" -> scoreTableJson,
"firstCardId" -> firstCardId,
- "nextPlayer" -> nextPlayer
+ "nextPlayer" -> nextPlayer,
+ "yourTurn" -> (game.logic.getCurrentPlayer.get == player)
)
}
@@ -88,6 +90,11 @@ class PollingController @Inject() (
val hand = player.currentHand()
val jsonResponse = buildCardPlayResponse(game, hand, player, true)
Ok(jsonResponse)
+ case NewTrick =>
+ val player = game.getPlayerByUser(userSession.user)
+ val hand = player.currentHand()
+ val jsonResponse = buildCardPlayResponse(game, hand, player, false)
+ Ok(jsonResponse)
case CardPlayed =>
val player = game.getPlayerByUser(userSession.user)
val hand = player.currentHand()
@@ -98,7 +105,8 @@ class PollingController @Inject() (
case ReloadEvent =>
val jsonResponse = Json.obj(
"status" -> "reloadEvent",
- "redirectUrl" -> routes.IngameController.game(game.id).url
+ "redirectUrl" -> routes.IngameController.game(game.id).url,
+ "content" -> ingameController.returnInnerHTML(game, userSession.user).toString
)
Ok(jsonResponse)
}
diff --git a/knockoutwhistweb/app/controllers/UserController.scala b/knockoutwhistweb/app/controllers/UserController.scala
index d3e5ebf..7cd46de 100644
--- a/knockoutwhistweb/app/controllers/UserController.scala
+++ b/knockoutwhistweb/app/controllers/UserController.scala
@@ -3,6 +3,7 @@ package controllers
import auth.{AuthAction, AuthenticatedRequest}
import logic.user.{SessionManager, UserManager}
import play.api.*
+import play.api.libs.json.Json
import play.api.mvc.*
import javax.inject.*
@@ -28,28 +29,35 @@ class UserController @Inject()(
if (possibleUser.isDefined) {
Redirect(routes.MainMenuController.mainMenu())
} else {
- Ok(views.html.login.login())
+ Ok(views.html.main("Login")(views.html.login.login()))
}
} else {
- Ok(views.html.login.login())
+ Ok(views.html.main("Login")(views.html.login.login()))
}
}
}
def login_Post(): Action[AnyContent] = {
Action { implicit request =>
- val postData = request.body.asFormUrlEncoded
- if (postData.isDefined) {
+ val jsonBody = request.body.asJson
+ val username: Option[String] = jsonBody.flatMap { jsValue =>
+ (jsValue \ "username").asOpt[String]
+ }
+ val password: Option[String] = jsonBody.flatMap { jsValue =>
+ (jsValue \ "password").asOpt[String]
+ }
+ if (username.isDefined && password.isDefined) {
// Extract username and password from form data
- val username = postData.get.get("username").flatMap(_.headOption).getOrElse("")
- val password = postData.get.get("password").flatMap(_.headOption).getOrElse("")
- val possibleUser = userManager.authenticate(username, password)
+ val possibleUser = userManager.authenticate(username.get, password.get)
if (possibleUser.isDefined) {
- Redirect(routes.MainMenuController.mainMenu()).withCookies(
+ Ok(Json.obj(
+ "status" -> "success",
+ "redirectUrl" -> routes.MainMenuController.mainMenu().url,
+ "content" -> views.html.mainmenu.creategame(possibleUser).toString
+ )).withCookies(
Cookie("sessionId", sessionManager.createSession(possibleUser.get))
)
} else {
- println("Failed login attempt for user: " + username)
Unauthorized("Invalid username or password")
}
} else {
diff --git a/knockoutwhistweb/app/logic/game/GameLobby.scala b/knockoutwhistweb/app/logic/game/GameLobby.scala
index 418f685..c894af5 100644
--- a/knockoutwhistweb/app/logic/game/GameLobby.scala
+++ b/knockoutwhistweb/app/logic/game/GameLobby.scala
@@ -4,14 +4,15 @@ import de.knockoutwhist.cards.{Hand, Suit}
import de.knockoutwhist.control.GameLogic
import de.knockoutwhist.control.GameState.{InGame, Lobby, MainMenu}
import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil}
-import de.knockoutwhist.events.global.{CardPlayedEvent, GameStateChangeEvent, SessionClosed}
+import de.knockoutwhist.events.global.tie.TieTurnEvent
+import de.knockoutwhist.events.global.{CardPlayedEvent, GameStateChangeEvent, NewTrickEvent, SessionClosed}
import de.knockoutwhist.events.player.{PlayerEvent, ReceivedHandEvent}
import de.knockoutwhist.player.Playertype.HUMAN
import de.knockoutwhist.player.{AbstractPlayer, PlayerFactory}
import de.knockoutwhist.rounds.{Match, Round, Trick}
import de.knockoutwhist.utils.events.{EventListener, SimpleEvent}
import exceptions.*
-import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, ReloadEvent}
+import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, NewTrick, ReloadEvent}
import model.sessions.{InteractionType, UserSession}
import model.users.User
@@ -82,8 +83,13 @@ class GameLobby private(
users.get(event.playerId).foreach(session => session.updatePlayer(event))
case event: CardPlayedEvent =>
addToQueue(CardPlayed)
+ case event: TieTurnEvent =>
+ addToQueue(ReloadEvent)
+ users.get(event.player.id).foreach(session => session.updatePlayer(event))
case event: PlayerEvent =>
users.get(event.playerId).foreach(session => session.updatePlayer(event))
+ case event: NewTrickEvent =>
+ addToQueue(NewTrick)
case event: GameStateChangeEvent =>
if (event.oldState == MainMenu && event.newState == Lobby) {
return
@@ -197,6 +203,7 @@ class GameLobby private(
throw new CantPlayCardException("You can't skip this round!")
}
logic.playerInputLogic.receivedDog(None)
+ return
}
val hand = getHand(player)
val card = hand.cards(cardIndex)
@@ -229,7 +236,7 @@ class GameLobby private(
}
def returnToLobby(userSession: UserSession): Unit = {
- if (users.contains(userSession.id)) {
+ if (!users.contains(userSession.id)) {
throw new NotInThisGameException("You are not in this game!")
}
val session = users(userSession.id)
diff --git a/knockoutwhistweb/app/logic/game/PollingEvents.scala b/knockoutwhistweb/app/logic/game/PollingEvents.scala
index 3fa5525..cc1117e 100644
--- a/knockoutwhistweb/app/logic/game/PollingEvents.scala
+++ b/knockoutwhistweb/app/logic/game/PollingEvents.scala
@@ -3,6 +3,7 @@ package logic.game
enum PollingEvents {
case CardPlayed
case NewRound
+ case NewTrick
case ReloadEvent
case LobbyUpdate
case LobbyCreation
diff --git a/knockoutwhistweb/app/views/ingame/finishedMatch.scala.html b/knockoutwhistweb/app/views/ingame/finishedMatch.scala.html
new file mode 100644
index 0000000..155eac6
--- /dev/null
+++ b/knockoutwhistweb/app/views/ingame/finishedMatch.scala.html
@@ -0,0 +1,37 @@
+@(user: Option[model.users.User], gamelobby: logic.game.GameLobby)
+
+
Waiting for the host to start the game...
-Waiting for the host to start the game...
+