From a58b2e03b11a54667d63ba6604f579a8e328c9d1 Mon Sep 17 00:00:00 2001 From: Janis Date: Wed, 19 Nov 2025 22:54:20 +0100 Subject: [PATCH] feat(game)!: Fixed polling, SPA, Gameplayloop etc. (#59) Co-authored-by: LQ63 Reviewed-on: https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/pulls/59 Co-authored-by: Janis Co-committed-by: Janis --- knockoutwhist | 2 +- .../app/assets/stylesheets/main.less | 10 + .../app/controllers/IngameController.scala | 217 +++-- .../JavaScriptRoutingController.scala | 12 +- .../app/controllers/MainMenuController.scala | 60 +- .../app/controllers/PollingController.scala | 52 +- .../app/controllers/UserController.scala | 26 +- .../app/logic/game/GameLobby.scala | 94 ++- .../app/logic/game/PollingEvents.scala | 2 + .../app/views/ingame/finishedMatch.scala.html | 37 + .../app/views/ingame/ingame.scala.html | 30 +- .../app/views/ingame/selecttrump.scala.html | 51 +- .../app/views/ingame/tie.scala.html | 22 +- .../app/views/lobby/lobby.scala.html | 102 +-- .../app/views/login/login.scala.html | 75 +- knockoutwhistweb/app/views/main.scala.html | 9 +- .../app/views/mainmenu/creategame.scala.html | 4 +- .../app/views/mainmenu/navbar.scala.html | 8 +- .../app/views/mainmenu/rules.scala.html | 3 - knockoutwhistweb/conf/routes | 42 +- knockoutwhistweb/public/javascripts/main.js | 763 +++++++++++------- 21 files changed, 1046 insertions(+), 575 deletions(-) create mode 100644 knockoutwhistweb/app/views/ingame/finishedMatch.scala.html 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) + +
+
+
+
+
Winner: @gamelobby.getLogic.getWinner
+
+
+
+ @if((gamelobby.getUserSession(user.get.id).host)) { +
+
Return to lobby
+
+ } else { +
+
+ Loading... +
+
+ } +
+
+
+ \ No newline at end of file diff --git a/knockoutwhistweb/app/views/ingame/ingame.scala.html b/knockoutwhistweb/app/views/ingame/ingame.scala.html index 4ab4f17..b357fc4 100644 --- a/knockoutwhistweb/app/views/ingame/ingame.scala.html +++ b/knockoutwhistweb/app/views/ingame/ingame.scala.html @@ -1,8 +1,8 @@ @import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.TrickUtil +@import de.knockoutwhist.utils.Implicits.* @(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby) -@main("Ingame") {
@@ -72,12 +72,19 @@
-
+
@for(i <- player.currentHand().get.cards.indices) {
-
- @util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/> +
+ @util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/> +
+ @if(player.isInDogLife) { +
+
+ }
}
@@ -86,8 +93,15 @@
-} diff --git a/knockoutwhistweb/app/views/ingame/selecttrump.scala.html b/knockoutwhistweb/app/views/ingame/selecttrump.scala.html index f9969c0..d6041fa 100644 --- a/knockoutwhistweb/app/views/ingame/selecttrump.scala.html +++ b/knockoutwhistweb/app/views/ingame/selecttrump.scala.html @@ -1,6 +1,5 @@ @(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby) -@main("Selecting Trumpsuit...") {
@@ -18,36 +17,24 @@
-
- - -
+
+ @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Spades)) width="120px" style="border-radius: 6px"/> +
-
- - -
+
+ @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Hearts)) width="120px" style="border-radius: 6px"/> +
-
- - -
+
+ @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Diamonds)) width="120px" style="border-radius: 6px"/> +
-
- - -
+
+ @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Clubs)) width="120px" style="border-radius: 6px"/> +
@@ -69,4 +56,16 @@
-} \ No newline at end of file + \ No newline at end of file diff --git a/knockoutwhistweb/app/views/ingame/tie.scala.html b/knockoutwhistweb/app/views/ingame/tie.scala.html index 134f234..1d8c801 100644 --- a/knockoutwhistweb/app/views/ingame/tie.scala.html +++ b/knockoutwhistweb/app/views/ingame/tie.scala.html @@ -1,6 +1,4 @@ @(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby) - -@main("Tie") {
@@ -28,7 +26,7 @@ Pick a number between 1 and @{maxNum + 1}. The resulting card will be your card for the cut.
-
+
@@ -36,9 +34,9 @@
- +
-
+
Currently Picked Cards
@@ -104,4 +102,16 @@
-} \ No newline at end of file + \ No newline at end of file diff --git a/knockoutwhistweb/app/views/lobby/lobby.scala.html b/knockoutwhistweb/app/views/lobby/lobby.scala.html index b7c53ba..b4d304d 100644 --- a/knockoutwhistweb/app/views/lobby/lobby.scala.html +++ b/knockoutwhistweb/app/views/lobby/lobby.scala.html @@ -1,6 +1,5 @@ @(user: Option[model.users.User], gamelobby: logic.game.GameLobby) -@main("Lobby") {
@@ -9,7 +8,7 @@
Lobby-Name: @gamelobby.name
-
Exit
+
Exit
@@ -19,57 +18,64 @@
- @if((gamelobby.getUserSession(user.get.id).host)) { -
- @for(playersession <- gamelobby.getPlayers.values) { -
-
- Profile -
- @if(playersession.id == user.get.id) { -
@playersession.name (You)
- Remove - } else { -
@playersession.name
-
Remove
- } -
-
+ @if((gamelobby.getUserSession(user.get.id).host)) { +
+ @for(playersession <- gamelobby.getPlayers.values) { +
+
+ Profile +
+ @if(playersession.id == user.get.id) { +
@playersession.name (You)
+ Remove + } else { +
@playersession.name
+
Remove
+ }
- } -
-
-
Start Game
-
- } else { -
- @for(playersession <- gamelobby.getPlayers.values) { -
- Profile -
- @if(playersession.id == user.get.id) { -
@playersession.name (You)
- } else { -
@playersession.name
- } -
-
-
- } -
-
-

Waiting for the host to start the game...

-
- Loading...
} +
+
+
Start Game
+
+ } else { +
+ @for(playersession <- gamelobby.getPlayers.values) { +
+ Profile +
+ @if(playersession.id == user.get.id) { +
@playersession.name (You)
+ } else { +
@playersession.name
+ } +
+
+
+ } +
+
+

Waiting for the host to start the game...

+
+ Loading... +
+
+ }
-} \ No newline at end of file + function waitForFunction(name, checkInterval = 100) { + return new Promise(resolve => { + const timer = setInterval(() => { + if (typeof window[name] === "function") { + clearInterval(timer); + resolve(window[name]); + } + }, checkInterval); + }); + } + waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id')); + \ No newline at end of file diff --git a/knockoutwhistweb/app/views/login/login.scala.html b/knockoutwhistweb/app/views/login/login.scala.html index f81630e..cb36915 100644 --- a/knockoutwhistweb/app/views/login/login.scala.html +++ b/knockoutwhistweb/app/views/login/login.scala.html @@ -1,47 +1,42 @@ @() + diff --git a/knockoutwhistweb/app/views/mainmenu/rules.scala.html b/knockoutwhistweb/app/views/mainmenu/rules.scala.html index ac29169..111189e 100644 --- a/knockoutwhistweb/app/views/mainmenu/rules.scala.html +++ b/knockoutwhistweb/app/views/mainmenu/rules.scala.html @@ -1,6 +1,4 @@ @(user: Option[model.users.User]) - -@main("Rules") { @navbar(user)
@@ -177,4 +175,3 @@
-} diff --git a/knockoutwhistweb/conf/routes b/knockoutwhistweb/conf/routes index 3c2a6e6..267bab3 100644 --- a/knockoutwhistweb/conf/routes +++ b/knockoutwhistweb/conf/routes @@ -4,34 +4,38 @@ # ~~~~ # For the javascript routing -GET /assets/js/routes controllers.JavaScriptRoutingController.javascriptRoutes() +GET /assets/js/routes controllers.JavaScriptRoutingController.javascriptRoutes() # Primary routes -GET / controllers.MainMenuController.index() -GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) +GET / controllers.MainMenuController.index() +GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) # Main menu routes -GET /mainmenu controllers.MainMenuController.mainMenu() -GET /rules controllers.MainMenuController.rules() +GET /mainmenu controllers.MainMenuController.mainMenu() +GET /rules controllers.MainMenuController.rules() +GET /navSPA/:pType controllers.MainMenuController.navSPA(pType) -POST /createGame controllers.MainMenuController.createGame() -POST /joinGame controllers.MainMenuController.joinGame() +POST /createGame controllers.MainMenuController.createGame() +POST /joinGame controllers.MainMenuController.joinGame() # User authentication routes -GET /login controllers.UserController.login() -POST /login controllers.UserController.login_Post() +GET /login controllers.UserController.login() +POST /login controllers.UserController.login_Post() -GET /logout controllers.UserController.logout() +GET /logout controllers.UserController.logout() # In-game routes -GET /game/:id controllers.IngameController.game(id: String) -GET /game/:id/join controllers.IngameController.joinGame(id: String) -GET /game/:id/start controllers.IngameController.startGame(id: String) -POST /game/:id/kickPlayer controllers.IngameController.kickPlayer(id: String, playerId: String) +GET /game/:id controllers.IngameController.game(id: String) +POST /game/:id/start controllers.IngameController.startGame(id: String) +POST /game/:id/kickPlayer/:playerToKick controllers.IngameController.kickPlayer(id: String, playerToKick: String) -POST /game/:id/trump controllers.IngameController.playTrump(id: String) -POST /game/:id/tie controllers.IngameController.playTie(id: String) +POST /game/:id/trump controllers.IngameController.playTrump(id: String) +POST /game/:id/tie controllers.IngameController.playTie(id: String) + +GET /game/:id/leaveGame controllers.IngameController.leaveGame(id: String) +POST /game/:id/playCard controllers.IngameController.playCard(id: String) +POST /game/:id/dogPlayCard controllers.IngameController.playDogCard(id: String) + +POST /game/:id/returnToLobby controllers.IngameController.returnToLobby(id: String) -GET /game/:id/leaveGame controllers.IngameController.leaveGame(id: String) -POST /game/:id/playCard controllers.IngameController.playCard(id: String) # Polling -GET /polling controllers.PollingController.polling(gameId: String) \ No newline at end of file +GET /polling/:gameId controllers.PollingController.polling(gameId: String) \ No newline at end of file diff --git a/knockoutwhistweb/public/javascripts/main.js b/knockoutwhistweb/public/javascripts/main.js index 38c15d1..c4cb416 100644 --- a/knockoutwhistweb/public/javascripts/main.js +++ b/knockoutwhistweb/public/javascripts/main.js @@ -80,275 +80,292 @@ })() function pollForUpdates(gameId) { + console.log(`[DEBUG] Starting poll cycle for Game ID: ${gameId} at ${new Date().toISOString()}`); if (!gameId) { - console.error("Game ID is missing. Stopping poll."); + console.error("[DEBUG] Game ID is missing. Stopping poll."); return; } - const element = document.getElementById('card-slide'); - const element2 = document.getElementById('lobbybackground'); - // Safety check for the target element - if (!element && !element2) { - console.error("Polling target element not found. Stopping poll."); - // Use a timeout to retry in case the DOM loads late, passing gameId. - setTimeout(() => pollForUpdates(gameId), 5000); + const $handElement = $('#card-slide'); + const $lobbyElement = $('#lobbybackground'); + const $mainmenuElement = $('#main-menu-screen') + const $mainbody = $('#main-body') + if (!$handElement.length && !$lobbyElement.length && !$mainmenuElement.length && !$mainbody.length) { + setTimeout(() => pollForUpdates(gameId), 1000); return; } const route = jsRoutes.controllers.PollingController.polling(gameId); + $.ajax({ + url: route.url, + type: 'GET', + dataType: 'json', - // Call your specific controller endpoint - fetch(route.url) - .then(response => { - if (response.status === 204) { - console.log("Polling: Timeout reached. Restarting poll."); + success: (data => { + if (!data) { + console.log("[DEBUG] Received 204 No Content (Timeout). Restarting poll."); + return; + } + if (data.status === "cardPlayed" && data.handData) { + console.log("Event received: Card played. Redrawing hand."); + const newHand = data.handData; + let newHandHTML = ''; + $handElement.empty(); - // CRITICAL: Pass gameId in the recursive call - setTimeout(() => pollForUpdates(gameId), 5000); - } else if (response.ok && response.status === 200) { - response.json().then(data => { + if(data.animation) { + $handElement.addClass('ingame-cards-slide'); + } else { + $handElement.removeClass('ingame-cards-slide'); + } - if (data.status === "cardPlayed" && data.handData && element) { - console.log("Event received: Card played. Redrawing hand."); + const dog = data.dog; - const newHand = data.handData; - let newHandHTML = ''; - element.innerHTML = ''; + newHand.forEach((cardId, index) => { + const cardHtml = ` +
+
+ + +
+
+ `; + newHandHTML += cardHtml; + }); - if(data.animation) { - if (!element.classList.contains('ingame-cards-slide')) { - element.classList.add('ingame-cards-slide'); - } - } else { - element.classList.remove('ingame-cards-slide'); - } + if (dog) { + newHandHTML += ` +
+ +
+ `; + } - newHand.forEach((cardId, index) => { - const cardHtml = ` -
-
- - + $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 += ` +
+
+
+ +
+
+ ${trickCard.player}
- `; - newHandHTML += cardHtml; - }); +
+ `; + }); + $('#trick-cards-container').html(trickHTML); + } + if ($('#score-table-body').length && data.scoreTable) { + let scoreHTML = ''; + scoreHTML += `

Tricks Won

- element.innerHTML = newHandHTML; +
+
PLAYER
+
TRICKS
+
` + data.scoreTable.forEach(score => { + scoreHTML += ` +
+
${score.name}
+
${score.tricks}
+
+ `; + }); + $('#score-table-body').html(scoreHTML); + } + const cardId = data.firstCardId; + if ($('#first-card-container').length) { + let imageSrc = ''; + let altText = 'First Card'; - const currentPlayerElement = document.getElementById('current-player-name'); - if (currentPlayerElement) { - currentPlayerElement.textContent = data.currentPlayerName; - } - const nextPlayerElement = document.getElementById('next-player-name'); - if (nextPlayerElement && data.nextPlayer) { - // Use the correctly named field from the server response - nextPlayerElement.textContent = data.nextPlayer; - } else { - // Case 2: Player name is empty or null (signal to clear display). - nextPlayerElement.textContent = ""; - } - - const trumpElement = document.getElementById('trump-suit'); - if (trumpElement) { - trumpElement.textContent = data.trumpSuit; - } - const trickContainer = document.getElementById('trick-cards-container'); - if (trickContainer) { - let trickHTML = ''; - - // Iterate over the array of played cards received from the server - data.trickCards.forEach(trickCard => { - // Reconstruct the HTML structure from your template - trickHTML += ` -
-
-
- -
-
- ${trickCard.player} -
-
-
- `; - }); - trickContainer.innerHTML = trickHTML; - } - const scoreBody = document.getElementById('score-table-body'); - if (scoreBody && data.scoreTable) { - let scoreHTML = ''; - scoreHTML += `

Tricks Won

- -
-
PLAYER
-
TRICKS
-
` - data.scoreTable.forEach(score => { - scoreHTML += ` -
-
${score.name}
-
${score.tricks}
-
- `; - }); - scoreBody.innerHTML = scoreHTML; - } - const firstCardContainer = document.getElementById('first-card-container'); - const cardId = data.firstCardId; // This will be "KH", "S7", or "BLANK" - - if (firstCardContainer) { - let imageSrc = ''; - let altText = 'First Card'; - - // Check if a card was actually played or if it's the start of a trick - if (cardId === "BLANK") { - imageSrc = "/assets/images/cards/1B.png"; - altText = "Blank Card"; - } else { - imageSrc = `/assets/images/cards/${cardId}.png`; - } - - // Reconstruct the image HTML (assuming the inner element needs replacement) - const newImageHTML = ` - ${altText} - `; - - // Clear the container and insert the new image - firstCardContainer.innerHTML = newImageHTML; - } - } else if (data.status === "reloadEvent") { - window.location.href = data.redirectUrl; - } else if (data.status === "lobbyUpdate") { - const players = document.getElementById("players"); - let newHtml = '' - - if (data.host) { - data.users.forEach(user => { - - const inner = user.self ? `
${user.name} (You)
- Remove` - : `
${user.name}
-
Remove
` - - newHtml += `
-
- Profile -
- ${inner} -
-
-
` - }) - } else { - data.users.forEach(user => { - - const inner = user.self ? `
${user.name} (You)
` : `
${user.name}
` - - newHtml += `
-
- Profile -
- ${inner} -
-
-
` - }) - } - players.innerHTML = newHtml; + if (cardId === "BLANK") { + imageSrc = "/assets/images/cards/1B.png"; + altText = "Blank Card"; + } else { + imageSrc = `/assets/images/cards/${cardId}.png`; } - pollForUpdates(gameId); - }); - } else { - // Handle network or server errors - console.error(`Polling error: Status ${response.status}`); - // Wait before retrying, passing gameId correctly - setTimeout(() => pollForUpdates(gameId), 5000); + + const newImageHTML = ` + ${altText} + `; + + $('#first-card-container').html(newImageHTML); + } + } else if (data.status === "reloadEvent") { + console.log("[DEBUG] Reload event received. Redirecting..."); + exchangeBody(data.content, "Knockout Whist - Ingame", data.redirectUrl); } + else if (data.status === "lobbyUpdate") { + console.log("[DEBUG] Entering 'lobbyUpdate' logic."); + let newHtml = '' + + if (data.host) { + data.users.forEach(user => { + + const inner = user.self ? `
${user.name} (You)
+ Remove` + : `
${user.name}
+
Remove
` + + newHtml += `
+
+ Profile +
+ ${inner} +
+
+
` + }) + } else { + data.users.forEach(user => { + + const inner = user.self ? `
${user.name} (You)
` : `
${user.name}
` + + newHtml += `
+
+ Profile +
+ ${inner} +
+
+
` + }) + } + $("#players").html(newHtml); + } else { + console.warn(`[DEBUG] Received unknown status: ${data.status}`); + } + }), + error: ((jqXHR, textStatus, errorThrown) => { + if (jqXHR.status >= 400) { + console.error(`Server error: ${jqXHR.status}, ${errorThrown}`); + } + else { + console.error(`Something unexpected happened while polling. ${jqXHR.status}, ${errorThrown}`) + } + }), + complete: (() => { + if (!window.location.href.includes("game")) { + console.log("[DEBUG] Page URL changed. Stopping poll restart."); + return; + } + setTimeout(() => pollForUpdates(gameId), 200); }) - .catch(error => { - console.error("Network error during polling:", error); - // Wait before retrying on network failure, passing gameId correctly - setTimeout(() => pollForUpdates(gameId), 5000); - }); + }) } function createGameJS() { - let lobbyName = document.getElementById("lobbyname").value; - if (lobbyName === "") { + let lobbyName = $('#lobbyname').val(); + if ($.trim(lobbyName) === "") { lobbyName = "DefaultLobby" } - const playerAmount = document.getElementById("playeramount").value; const jsonObj = { lobbyname: lobbyName, - playeramount: playerAmount + playeramount: $("#playeramount").val() } sendGameCreationRequest(jsonObj); } +function backToLobby(gameId) { + const route = jsRoutes.controllers.IngameController.returnToLobby(gameId); + + $.ajax({ + url: route.url, + type: route.type, + contentType: 'application/json', + dataType: 'json', + error: ((jqXHR) => { + const errorData = JSON.parse(jqXHR.responseText); + if (errorData && errorData.errorMessage) { + alert(`${errorData.errorMessage}`); + } else { + alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`); + } + }) + }) + +} + function sendGameCreationRequest(dataObject) { const route = jsRoutes.controllers.MainMenuController.createGame(); - fetch(route.url, { - method: route.type, - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(dataObject) - }) - .then(response => { - return response.json().then(data => { - if (!response.ok) { - return Promise.reject(data); - } - return data; - }); - }) - .then(data => { + $.ajax({ + url: route.url, + type: route.type, + contentType: 'application/json', + data: JSON.stringify(dataObject), + dataType: 'json', + success: (data => { if (data.status === 'success') { - window.location.href = data.redirectUrl; + exchangeBody(data.content, "Knockout Whist - Lobby", data.redirectUrl); + } + }), + error: ((jqXHR) => { + const errorData = JSON.parse(jqXHR.responseText); + if (errorData && errorData.errorMessage) { + alert(`${errorData.errorMessage}`); + } else { + alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`); } }) - .catch(error => { - if (error && error.errorMessage) { - alert(`${error.errorMessage}`); - } else { - alert('An unexpected error occurred. Please try again.'); - } - }); + }) } + +function exchangeBody(content, title = "Knockout Whist", url = null) { + if (url) { + window.history.pushState({}, title, url); + } + $("#main-body").html(content); + document.title = title; +} + function startGame(gameId) { sendGameStartRequest(gameId) } + function sendGameStartRequest(gameId) { const route = jsRoutes.controllers.IngameController.startGame(gameId); - fetch(route.url, { - method: route.type, - }) - .then(response => { - return response.json().then(data => { - if (!response.ok) { - return Promise.reject(data); - } - return data; - }); - }) - .then(data => { - // SUCCESS BLOCK: data is the { status: 'success', ... } object + $.ajax({ + url: route.url, + type: route.type, + dataType: 'json', + success: (data => { if (data.status === 'success') { window.location.href = data.redirectUrl; } - }) - .catch(error => { - if (error && error.errorMessage) { - alert(`${error.errorMessage}`); + }), + error: ((jqXHR) => { + const errorData = JSON.parse(jqXHR.responseText); + if (errorData && errorData.errorMessage) { + alert(`${errorData.errorMessage}`); } else { - alert('An unexpected error occurred. Please try again.'); + alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`); } - }); + }) + }) } function removePlayer(gameid, playersessionId) { sendRemovePlayerRequest(gameid, playersessionId) @@ -357,33 +374,148 @@ function removePlayer(gameid, playersessionId) { function sendRemovePlayerRequest(gameId, playersessionId) { const route = jsRoutes.controllers.IngameController.kickPlayer(gameId, playersessionId); - fetch(route.url, { - method: route.type, - headers: { - 'Content-Type': 'application/json', - } - }) - .then(response => { - return response.json().then(data => { - if (!response.ok) { - return Promise.reject(data); - } - return data; - }); - }) - .then(data => { - // SUCCESS BLOCK: data is the { status: 'success', ... } object + $.ajax({ + url: route.url, + type: route.type, + contentType: 'application/json', + dataType: 'json', + success: (data => { if (data.status === 'success') { window.location.href = data.redirectUrl; } + }), + error: ((jqXHR) => { + const errorData = JSON.parse(jqXHR.responseText); + if (errorData && errorData.errorMessage) { + alert(`${errorData.errorMessage}`); + } else { + alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`); + } }) - .catch(error => { - if (error && error.errorMessage) { + }) +} + +function login() { + const username = $('#username').val(); + const password = $('#password').val(); + + const jsonObj = { + username: username, + password: password + }; + + const route = jsRoutes.controllers.UserController.login_Post(); + $.ajax({ + url: route.url, + type: route.type, + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify(jsonObj), + success: (data => { + if (data.status === 'success') { + exchangeBody(data.content, 'Knockout Whist - Create Game', data.redirectUrl); + return + } + alert('Login failed. Please check your credentials and try again.'); + }), + error: ((jqXHR) => { + const errorData = JSON.parse(jqXHR.responseText); + if (errorData && errorData.errorMessage) { + alert(`${errorData.errorMessage}`); + } else { + alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`); + } + }) + }); +} + +function joinGame() { + const gameId = $('#gameId').val(); + + const jsonObj = { + gameId: gameId + }; + + const route = jsRoutes.controllers.MainMenuController.joinGame(); + $.ajax({ + url: route.url, + type: route.type, + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify(jsonObj), + success: (data => { + if (data.status === 'success') { + exchangeBody(data.content, "Knockout Whist - Lobby", data.redirectUrl); + return + } + alert('Could not join the game. Please check the Game ID and try again.'); + }), + error: ((jqXHR) => { + const errorData = JSON.parse(jqXHR.responseText); + if (errorData && errorData.errorMessage) { + alert(`${errorData.errorMessage}`); + } else { + alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`); + } + }) + }); + return false +} + +function navSpa(page, title) { + const route = jsRoutes.controllers.MainMenuController.navSPA(page); + $.ajax({ + url: route.url, + type: route.type, + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify(jsonObj), + success: (data => { + if (data.status === 'success') { + exchangeBody(data.content, title, data.redirectUrl); + return + } + alert('Could not join the game. Please check the Game ID and try again.'); + }), + error: ((jqXHR) => { + const errorData = JSON.parse(jqXHR.responseText); + if (errorData && errorData.errorMessage) { + alert(`${errorData.errorMessage}`); + } else { + alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`); + } + }) + }); + return false +} + + +function selectTie(gameId) { + const route = jsRoutes.controllers.IngameController.playTie(gameId); + const jsonObj = { + tie: $('#tieNumber').val() + }; + + $.ajax({ + url: route.url, + type: route.type, + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify(jsonObj), + error: (jqXHR => { + let error; + try { + error = JSON.parse(jqXHR.responseText); + } catch (e) { + console.error("Failed to parse error response:", e); + } + if (error?.errorMessage) { alert(`${error.errorMessage}`); } else { alert('An unexpected error occurred. Please try again.'); } - }); + }) + }) } function leaveGame(gameId) { @@ -391,43 +523,62 @@ function leaveGame(gameId) { } function sendLeavePlayerRequest(gameId) { - const route = jsRoutes.controllers.IngameController.leaveGame(gameId); - fetch(route.url, { - method: route.type, - }) - .then(response => { - return response.json().then(data => { - if (!response.ok) { - return Promise.reject(data); - } - return data; - }); - }) - .then(data => { - // SUCCESS BLOCK: data is the { status: 'success', ... } object - if (data.status === 'success') { - window.location.href = data.redirectUrl; + $.ajax({ + url: route.url, + type: route.type, + dataType: 'json', + error: ((jqXHR) => { + const errorData = JSON.parse(jqXHR.responseText); + if (errorData && errorData.errorMessage) { + alert(`${errorData.errorMessage}`); + } else { + alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`); } }) - .catch(error => { - if (error && error.errorMessage) { + }) +} + +function handleTrumpSelection(cardobject, gameId) { + const trumpId = cardobject.dataset.trump; + const jsonObj = { + trump: trumpId + } + + const route = jsRoutes.controllers.IngameController.playTrump(gameId); + + $.ajax({ + url: route.url, + type: route.type, + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify(jsonObj), + error: (jqXHR => { + let error; + try { + error = JSON.parse(jqXHR.responseText); + } catch (e) { + console.error("Failed to parse error response:", e); + } + if (error?.errorMessage) { alert(`${error.errorMessage}`); } else { alert('An unexpected error occurred. Please try again.'); } - }); + }) + }) } -function handlePlayCard(cardobject, gameId) { +function handlePlayCard(cardobject, gameId, dog = false) { const cardId = cardobject.dataset.cardId; const jsonObj = { cardID: cardId } - sendPlayCardRequest(jsonObj, gameId, cardobject) + sendPlayCardRequest(jsonObj, gameId, cardobject, dog) } -function sendPlayCardRequest(jsonObj, gameId, cardobject) { +function handleSkipDogLife(cardobject, gameId) { + const wiggleKeyframes = [ { transform: 'translateX(0)' }, { transform: 'translateX(-5px)' }, @@ -436,44 +587,78 @@ function sendPlayCardRequest(jsonObj, gameId, cardobject) { { transform: 'translateX(0)' } ]; - // Define the timing options const wiggleTiming = { - duration: 400, // 0.4 seconds + duration: 400, iterations: 1, easing: 'ease-in-out', - // Fill mode ensures the final state is applied until reset fill: 'forwards' }; - const route = jsRoutes.controllers.IngameController.playCard(gameId); - fetch(route.url, { - method: route.type, - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(jsonObj) - }) - .then(response => { - return response.json().then(data => { - if (!response.ok) { - return Promise.reject(data); - } - return data; - }); - }) - .then(data => { - if (data.status === 'success') { - //window.location.href = data.redirectUrl; + const route = jsRoutes.controllers.IngameController.playDogCard(gameId); + + $.ajax({ + url: route.url, + type: route.type, + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify({ + cardID: 'skip' + }), + error: (jqXHR => { + let error; + try { + error = JSON.parse(jqXHR.responseText); + } catch (e) { + console.error("Failed to parse error response:", e); } - }) - .catch(error => { - if (error && error.errorMessage.includes("You can't play this card!")) { + if (error?.errorMessage.includes("You can't skip this round!")) { cardobject.parentElement.animate(wiggleKeyframes, wiggleTiming); - } else if (error && error.errorMessage) { + } else if (error?.errorMessage) { alert(`${error.errorMessage}`); } else { alert('An unexpected error occurred. Please try again.'); } - }); + }) + }) +} + +function sendPlayCardRequest(jsonObj, gameId, cardobject, dog) { + const wiggleKeyframes = [ + { transform: 'translateX(0)' }, + { transform: 'translateX(-5px)' }, + { transform: 'translateX(5px)' }, + { transform: 'translateX(-5px)' }, + { transform: 'translateX(0)' } + ]; + + const wiggleTiming = { + duration: 400, + iterations: 1, + easing: 'ease-in-out', + fill: 'forwards' + }; + const route = dog === "true" ? jsRoutes.controllers.IngameController.playDogCard(gameId) : jsRoutes.controllers.IngameController.playCard(gameId); + + $.ajax({ + url: route.url, + type: route.type, + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify(jsonObj), + error: (jqXHR => { + try { + error = JSON.parse(jqXHR.responseText); + } catch (e) { + console.error("Failed to parse error response:", e); + } + if (error?.errorMessage.includes("You can't play this card!")) { + cardobject.parentElement.animate(wiggleKeyframes, wiggleTiming); + } else if (error?.errorMessage) { + alert(`${error.errorMessage}`); + } else { + alert('An unexpected error occurred. Please try again.'); + } + }) + }) }