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 346bb83..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 {
@@ -172,8 +167,7 @@ class IngameController @Inject() (
optSession.foreach(_.lock.unlock())
if (result.isSuccess) {
Ok(Json.obj(
- "status" -> "success",
- "redirectUrl" -> routes.IngameController.game(gameId).url
+ "status" -> "success"
))
} else {
val throwable = result.failed.get
@@ -198,6 +192,11 @@ class IngameController @Inject() (
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
+ case _: NotInteractableException =>
+ BadRequest(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> throwable.getMessage
+ ))
case _ =>
InternalServerError(Json.obj(
"status" -> "failure",
@@ -223,7 +222,10 @@ class IngameController @Inject() (
val game = podManager.getGame(gameId)
game match {
case Some(g) => {
- val cardIdOpt = request.body.asFormUrlEncoded.flatMap(_.get("cardId").flatMap(_.headOption))
+ val jsonBody = request.body.asJson
+ val cardIdOpt: Option[String] = jsonBody.flatMap { jsValue =>
+ (jsValue \ "cardID").asOpt[String]
+ }
var optSession: Option[UserSession] = None
val result = Try {
cardIdOpt match {
@@ -248,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
+ ))
}
}
}
@@ -269,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
@@ -286,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 =>
@@ -306,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
@@ -323,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 =>
@@ -339,5 +386,47 @@ class IngameController @Inject() (
NotFound("Game not found")
}
}
+
+
+ def returnToLobby(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
+ val game = podManager.getGame(gameId)
+ game match {
+ case Some(g) =>
+ val result = Try {
+ val session = g.getUserSession(request.user.id)
+ g.returnToLobby(session)
+ }
+ if (result.isSuccess) {
+ Ok(Json.obj(
+ "status" -> "success",
+ "redirectUrl" -> routes.IngameController.game(gameId).url
+ ))
+ } else {
+ val throwable = result.failed.get
+ throwable match {
+ case _: NotInThisGameException =>
+ BadRequest(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> throwable.getMessage
+ ))
+ case _: IllegalStateException =>
+ BadRequest(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> throwable.getMessage
+ ))
+ case _ =>
+ InternalServerError(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> throwable.getMessage
+ ))
+ }
+ }
+ case None =>
+ NotFound(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> "Game not found"
+ ))
+ }
+ }
}
\ No newline at end of file
diff --git a/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala b/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala
index eb7e82a..b625e13 100644
--- a/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala
+++ b/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala
@@ -12,15 +12,23 @@ 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.MainMenuController.joinGame,
+ routes.javascript.MainMenuController.navSPA,
routes.javascript.IngameController.startGame,
routes.javascript.IngameController.kickPlayer,
routes.javascript.IngameController.leaveGame,
routes.javascript.IngameController.playCard,
- routes.javascript.PollingController.polling
+ routes.javascript.IngameController.playDogCard,
+ 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..63d03ab 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("Knockout Whist - Create Game")(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,22 +59,58 @@ 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"
+ ))
}
}
def rules(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
- Ok(views.html.mainmenu.rules(Some(request.user)))
+ Ok(views.html.main("Knockout Whist - Rules")(views.html.mainmenu.rules(Some(request.user))))
}
+
+ def navSPA(location: String) : Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
+ location match {
+ case "0" => // Main Menu
+ Ok(Json.obj(
+ "status" -> "success",
+ "redirectUrl" -> routes.MainMenuController.mainMenu().url,
+ "content" -> views.html.mainmenu.creategame(Some(request.user)).toString
+ ))
+ case "1" => // Rules
+ Ok(Json.obj(
+ "status" -> "success",
+ "redirectUrl" -> routes.MainMenuController.rules().url,
+ "content" -> views.html.mainmenu.rules(Some(request.user)).toString
+ ))
+ case _ =>
+ BadRequest(Json.obj(
+ "status" -> "failure",
+ "errorMessage" -> "Invalid form submission"
+ ))
+ }
+ }
+
}
\ No newline at end of file
diff --git a/knockoutwhistweb/app/controllers/PollingController.scala b/knockoutwhistweb/app/controllers/PollingController.scala
index 73d4b11..360f2e7 100644
--- a/knockoutwhistweb/app/controllers/PollingController.scala
+++ b/knockoutwhistweb/app/controllers/PollingController.scala
@@ -1,10 +1,12 @@
package controllers
import auth.{AuthAction, AuthenticatedRequest}
+import controllers.PollingController.{scheduler, timeoutDuration}
import de.knockoutwhist.cards.Hand
+import de.knockoutwhist.player.AbstractPlayer
import logic.PodManager
import logic.game.{GameLobby, PollingEvents}
-import logic.game.PollingEvents.{CardPlayed, 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}
@@ -13,16 +15,22 @@ import util.WebUIUtils
import javax.inject.{Inject, Singleton}
import scala.concurrent.{ExecutionContext, Future}
-
+import java.util.concurrent.{Executors, ScheduledExecutorService, TimeUnit}
+import scala.concurrent.duration.*
+object PollingController {
+ private val scheduler: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
+ private val timeoutDuration = 25.seconds
+}
@Singleton
class PollingController @Inject() (
val cc: ControllerComponents,
val podManager: PodManager,
val authAction: AuthAction,
+ val ingameController: IngameController,
implicit val ec: ExecutionContext
) extends AbstractController(cc) {
- private def buildCardPlayResponse(game: GameLobby, hand: Option[Hand], newRound: Boolean): JsValue = {
+ private def buildCardPlayResponse(game: GameLobby, hand: Option[Hand], player: AbstractPlayer, newRound: Boolean): JsValue = {
val currentRound = game.logic.getCurrentRound.get
val currentTrick = game.logic.getCurrentTrick.get
@@ -51,12 +59,14 @@ class PollingController @Inject() (
"status" -> "cardPlayed",
"animation" -> newRound,
"handData" -> stringHand,
+ "dog" -> player.isInDogLife,
"currentPlayerName" -> game.logic.getCurrentPlayer.get.name,
"trumpSuit" -> currentRound.trumpSuit.toString,
"trickCards" -> trickCardsJson,
"scoreTable" -> scoreTableJson,
"firstCardId" -> firstCardId,
- "nextPlayer" -> nextPlayer
+ "nextPlayer" -> nextPlayer,
+ "yourTurn" -> (game.logic.getCurrentPlayer.get == player)
)
}
@@ -78,43 +88,52 @@ class PollingController @Inject() (
case NewRound =>
val player = game.getPlayerByUser(userSession.user)
val hand = player.currentHand()
- val jsonResponse = buildCardPlayResponse(game, hand, true)
+ 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()
- val jsonResponse = buildCardPlayResponse(game, hand, false)
+ val jsonResponse = buildCardPlayResponse(game, hand, player, false)
Ok(jsonResponse)
case LobbyUpdate =>
Ok(buildLobbyUsersResponse(game, userSession))
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)
}
}
- // --- Main Polling Action ---
def polling(gameId: String): Action[AnyContent] = authAction.async { implicit request: AuthenticatedRequest[AnyContent] =>
val playerId = request.user.id
- // 1. Safely look up the game
podManager.getGame(gameId) match {
case Some(game) =>
-
- // 2. Short-Poll Check (Check for missed events)
- if (game.getPollingState.nonEmpty) {
- val event = game.getPollingState.dequeue()
-
+ val playerEventQueue = game.getEventsOfPlayer(playerId)
+ if (playerEventQueue.nonEmpty) {
+ val event = playerEventQueue.dequeue()
Future.successful(handleEvent(event, game, game.getUserSession(playerId)))
} else {
-
val eventPromise = game.registerWaiter(playerId)
-
+ val scheduledFuture = scheduler.schedule(
+ new Runnable {
+ override def run(): Unit =
+ eventPromise.tryFailure(new java.util.concurrent.TimeoutException("Polling Timeout"))
+ },
+ timeoutDuration.toMillis,
+ TimeUnit.MILLISECONDS
+ )
eventPromise.future.map { event =>
+ scheduledFuture.cancel(false)
game.removeWaiter(playerId)
handleEvent(event, game, game.getUserSession(playerId))
}.recover {
@@ -125,7 +144,6 @@ class PollingController @Inject() (
}
case None =>
- // Game not found
Future.successful(NotFound("Game not found."))
}
}
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 1cf7dfa..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, LobbyUpdate, NewRound, ReloadEvent}
+import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, NewTrick, ReloadEvent}
import model.sessions.{InteractionType, UserSession}
import model.users.User
@@ -27,21 +28,39 @@ class GameLobby private(
val name: String,
val maxPlayers: Int
) extends EventListener {
- logic.addListener(this)
- logic.createSession()
+
+
private val users: mutable.Map[UUID, UserSession] = mutable.Map()
- private val pollingState: mutable.Queue[PollingEvents] = mutable.Queue()
+ private val eventsPerPlayer: mutable.Map[UUID, mutable.Queue[PollingEvents]] = mutable.Map()
private val waitingPromises: mutable.Map[UUID, ScalaPromise[PollingEvents]] = mutable.Map()
+ private val lock = new Object
+ lock.synchronized {
+ logic.addListener(this)
+ logic.createSession()
+ }
+
def registerWaiter(playerId: UUID): ScalaPromise[PollingEvents] = {
val promise = ScalaPromise[PollingEvents]()
- waitingPromises.put(playerId, promise)
- promise
+ lock.synchronized {
+ val queue = eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
+
+ if (queue.nonEmpty) {
+ val evt = queue.dequeue()
+ promise.success(evt)
+ promise
+ } else {
+ waitingPromises.put(playerId, promise)
+ promise
+ }
+ }
}
def removeWaiter(playerId: UUID): Unit = {
- waitingPromises.remove(playerId)
+ lock.synchronized {
+ waitingPromises.remove(playerId)
+ }
}
def addUser(user: User): UserSession = {
@@ -64,17 +83,18 @@ 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
}
- if (event.oldState == Lobby && event.newState == InGame) {
- addToQueue(ReloadEvent)
- }else {
- addToQueue(ReloadEvent)
- }
+ addToQueue(ReloadEvent)
users.values.foreach(session => session.updatePlayer(event))
case event: SessionClosed =>
users.values.foreach(session => session.updatePlayer(event))
@@ -84,11 +104,29 @@ class GameLobby private(
}
private def addToQueue(event: PollingEvents): Unit = {
- if (waitingPromises.nonEmpty) {
- waitingPromises.values.foreach(_.success(event))
- waitingPromises.clear()
- } else {
- pollingState.enqueue(event)
+ lock.synchronized {
+ users.keys.foreach { playerId =>
+ val q = eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
+ q.enqueue(event)
+ }
+ val waiterIds = waitingPromises.keys.toList
+ waiterIds.foreach { playerId =>
+ val q = eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
+ if (q.nonEmpty) {
+ val evt = q.dequeue()
+ val p = waitingPromises.remove(playerId)
+ p.foreach(_.success(evt))
+ }
+ }
+ }
+
+ waitingPromises.keys.foreach { playerId =>
+ val queue = eventsPerPlayer(playerId)
+ if (queue.nonEmpty) {
+ val promise = waitingPromises(playerId)
+ promise.success(queue.dequeue())
+ waitingPromises.remove(playerId)
+ }
}
}
@@ -161,10 +199,11 @@ class GameLobby private(
throw new CantPlayCardException("You are not in dog life!")
}
if (cardIndex == -1) {
- if (!MatchUtil.dogNeedsToPlay(getMatch, getRound)) {
+ if (MatchUtil.dogNeedsToPlay(getMatch, getRound)) {
throw new CantPlayCardException("You can't skip this round!")
}
logic.playerInputLogic.receivedDog(None)
+ return
}
val hand = getHand(player)
val card = hand.cards(cardIndex)
@@ -196,6 +235,19 @@ class GameLobby private(
logic.playerTieLogic.receivedTieBreakerCard(tieNumber)
}
+ def returnToLobby(userSession: UserSession): Unit = {
+ if (!users.contains(userSession.id)) {
+ throw new NotInThisGameException("You are not in this game!")
+ }
+ val session = users(userSession.id)
+ if (session != userSession) {
+ throw new IllegalArgumentException("User session does not match!")
+ }
+ if (!session.host)
+ throw new NotHostException("Only the host can return to the lobby!")
+ logic.createSession()
+ }
+
//-------------------
@@ -218,8 +270,8 @@ class GameLobby private(
def getLogic: GameLogic = {
logic
}
- def getPollingState: mutable.Queue[PollingEvents] = {
- pollingState
+ def getEventsOfPlayer(playerId: UUID): mutable.Queue[PollingEvents] = {
+ eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
}
private def getPlayerBySession(userSession: UserSession): AbstractPlayer = {
val playerOption = getMatch.totalplayers.find(_.id == userSession.id)
diff --git a/knockoutwhistweb/app/logic/game/PollingEvents.scala b/knockoutwhistweb/app/logic/game/PollingEvents.scala
index 5e5bc8a..cc1117e 100644
--- a/knockoutwhistweb/app/logic/game/PollingEvents.scala
+++ b/knockoutwhistweb/app/logic/game/PollingEvents.scala
@@ -3,6 +3,8 @@ package logic.game
enum PollingEvents {
case CardPlayed
case NewRound
+ case NewTrick
case ReloadEvent
case LobbyUpdate
+ case LobbyCreation
}
\ No newline at end of file
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...
+
+
+ $handElement.html(newHandHTML);
+
+ if (data.yourTurn) {
+ $handElement.removeClass('inactive');
+ } else {
+ $handElement.addClass('inactive');
+ }
+
+ $('#current-player-name').text(data.currentPlayerName)
+ if (data.nextPlayer) {
+ $('#next-player-name').text(data.nextPlayer);
+ } else if (nextPlayerElement) {
+ $('#next-player-name').text("");
+ } else {
+ console.warn("[DEBUG] 'current-player-name' element missing in DOM");
+ }
+ $('#trump-suit').text(data.trumpSuit);
+ if ($('#trick-cards-container').length) {
+ let trickHTML = '';
+
+ data.trickCards.forEach(trickCard => {
+ trickHTML += `
+
+
-
-
-
+
+