From a71752df2fe145e9b7eb332f9cfcf11048c45ffa Mon Sep 17 00:00:00 2001 From: LQ63 Date: Sun, 9 Nov 2025 16:34:47 +0100 Subject: [PATCH 1/7] feat(ui): changed Background color, centered Lobby Added a background color for the mainmenu and the lobby + centered lobby --- .../app/assets/stylesheets/dark-mode.less | 2 +- .../app/assets/stylesheets/light-mode.less | 1 + .../app/assets/stylesheets/main.less | 6 +- .../app/views/ingame/ingame.scala.html | 2 + .../app/views/lobby/lobby.scala.html | 119 ++++++++---------- knockoutwhistweb/app/views/main.scala.html | 7 +- .../app/views/mainmenu/creategame.scala.html | 50 ++++---- 7 files changed, 91 insertions(+), 96 deletions(-) diff --git a/knockoutwhistweb/app/assets/stylesheets/dark-mode.less b/knockoutwhistweb/app/assets/stylesheets/dark-mode.less index 02d2efa..c34aab1 100644 --- a/knockoutwhistweb/app/assets/stylesheets/dark-mode.less +++ b/knockoutwhistweb/app/assets/stylesheets/dark-mode.less @@ -3,7 +3,7 @@ --background-image: url('/assets/images/background.png') !important; --color: #f8f9fa !important; /* Light text on dark bg */ --highlightscolor: rgba(131, 131, 131, 0.75) !important; - + --background-color: #192734; /* Bootstrap variable overrides for dark mode */ --bs-body-color: var(--color); --bs-link-color: #66b2ff; diff --git a/knockoutwhistweb/app/assets/stylesheets/light-mode.less b/knockoutwhistweb/app/assets/stylesheets/light-mode.less index 5975dcc..3054bc2 100644 --- a/knockoutwhistweb/app/assets/stylesheets/light-mode.less +++ b/knockoutwhistweb/app/assets/stylesheets/light-mode.less @@ -2,4 +2,5 @@ --background-image: url('/assets/images/img.png'); --color: black; --highlightscolor: rgba(0, 0, 0, 0.75); + --background-color: rgba(228, 232, 237, 1); } diff --git a/knockoutwhistweb/app/assets/stylesheets/main.less b/knockoutwhistweb/app/assets/stylesheets/main.less index d41bf29..8daeda1 100644 --- a/knockoutwhistweb/app/assets/stylesheets/main.less +++ b/knockoutwhistweb/app/assets/stylesheets/main.less @@ -14,7 +14,7 @@ --bs-border-color: rgba(0, 0, 0, 0.125) !important; --bs-heading-color: var(--color) !important; } - +@background-color: var(--background-color); @highlightcolor: var(--highlightscolor); @background-image: var(--background-image); @color: var(--color); @@ -29,6 +29,10 @@ background-repeat: no-repeat; background-attachment: fixed; } +.lobby-background { + background-color: @background-color; + +} .navbar-header{ text-align:center; diff --git a/knockoutwhistweb/app/views/ingame/ingame.scala.html b/knockoutwhistweb/app/views/ingame/ingame.scala.html index db56c27..ba204cb 100644 --- a/knockoutwhistweb/app/views/ingame/ingame.scala.html +++ b/knockoutwhistweb/app/views/ingame/ingame.scala.html @@ -3,6 +3,7 @@ @(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby) @main("Ingame") { +
@@ -67,4 +68,5 @@
+
} diff --git a/knockoutwhistweb/app/views/lobby/lobby.scala.html b/knockoutwhistweb/app/views/lobby/lobby.scala.html index 46eabb6..a8832e9 100644 --- a/knockoutwhistweb/app/views/lobby/lobby.scala.html +++ b/knockoutwhistweb/app/views/lobby/lobby.scala.html @@ -1,82 +1,73 @@ @(user: Option[model.users.User], gamelobby: logic.game.GameLobby) @main("Lobby") { -
-
-
-
-
- Lobby-Name: @gamelobby.name +
+
+
+
+
+
+ Lobby-Name: @gamelobby.name +
+
+ +
-
- -
-
-
-
-
Playeramount: @gamelobby.getPlayers.size / @gamelobby.maxPlayers
+
+
+
Playeramount: @gamelobby.getPlayers.size / @gamelobby.maxPlayers
+
-
-
- - @if((gamelobby.getUserSession(user.get.id).host)) { - @for(playersession <- gamelobby.getPlayers.values) { -
-
+
+ @if((gamelobby.getUserSession(user.get.id).host)) { + @for(playersession <- gamelobby.getPlayers.values) { +
+
+ Profile +
+ @if(playersession.id == user.get.id) { +
@playersession.name (You)
+ @*

Your text could be here!

*@ + Remove + } else { +
@playersession.name
+ @*

Your text could be here!

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

Your text could be here!

*@ - Remove +
@playersession.name (You)
} else { -
@playersession.name
-@*

Your text could be here!

*@ -
- -
+
@playersession.name
}
-
-
- } -
- -
- } 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...
+ } + +
+

Waiting for the host to start the game...

+
+ Loading... +
-
- } + } +
-
+
} \ No newline at end of file diff --git a/knockoutwhistweb/app/views/main.scala.html b/knockoutwhistweb/app/views/main.scala.html index daccc3a..12d9c2c 100644 --- a/knockoutwhistweb/app/views/main.scala.html +++ b/knockoutwhistweb/app/views/main.scala.html @@ -18,15 +18,10 @@ - -
+ @* And here's where we render the `Html` object containing * the page content. *@ @content -
- -
-
diff --git a/knockoutwhistweb/app/views/mainmenu/creategame.scala.html b/knockoutwhistweb/app/views/mainmenu/creategame.scala.html index 1e5905c..4872ddc 100644 --- a/knockoutwhistweb/app/views/mainmenu/creategame.scala.html +++ b/knockoutwhistweb/app/views/mainmenu/creategame.scala.html @@ -2,31 +2,33 @@ @main("Create Game") { @navbar(user) -
-
-
- - -
-
- - -
+
+ +
- - -
- 2 - 3 - 4 - 5 - 6 - 7 -
+ + +
+
+ + +
+
+ + +
+ 2 + 3 + 4 + 5 + 6 + 7 +
+
+
+
-
-
-
- + +
} \ No newline at end of file -- 2.52.0 From c948e5e800bb5cc90b81ed9d474a288490437ef1 Mon Sep 17 00:00:00 2001 From: LQ63 Date: Sun, 9 Nov 2025 17:19:17 +0100 Subject: [PATCH 2/7] feat(ui): Tricktable Added a trick-table displaying the players with their won tricks sorted by the person with the most tricks --- .../app/assets/stylesheets/main.less | 16 +++++ .../app/views/ingame/ingame.scala.html | 60 +++++++++++++------ 2 files changed, 57 insertions(+), 19 deletions(-) diff --git a/knockoutwhistweb/app/assets/stylesheets/main.less b/knockoutwhistweb/app/assets/stylesheets/main.less index 8daeda1..d449d2b 100644 --- a/knockoutwhistweb/app/assets/stylesheets/main.less +++ b/knockoutwhistweb/app/assets/stylesheets/main.less @@ -209,4 +209,20 @@ body { color: @color; font-size: 1.5em; font-family: Arial, serif; +} +.score-table { + background-color: rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 10px; + margin-bottom: 20px; + backdrop-filter: blur(8px); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} +.score-header { + font-weight: bold; + color: #ffffff; + border-bottom: 1px solid rgba(255, 255, 255, 0.3); +} +.score-row { + color: #ffffff; } \ 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 ba204cb..c4640e2 100644 --- a/knockoutwhistweb/app/views/ingame/ingame.scala.html +++ b/knockoutwhistweb/app/views/ingame/ingame.scala.html @@ -11,15 +11,37 @@

Current Player

@gamelobby.getLogic.getCurrentPlayer.get.name

@if(!TrickUtil.isOver(gamelobby.getLogic.getCurrentMatch.get, gamelobby.getLogic.getPlayerQueue.get)) { -

Next Player

- @for(nextplayer <- gamelobby.getLogic.getPlayerQueue.get.duplicate()) { -

@nextplayer

- } +

Next Player

+ @for(nextplayer <- gamelobby.getLogic.getPlayerQueue.get.duplicate()) { +

@nextplayer

+ } }
-
- @for((cardplayed, player) <- gamelobby.getLogic.getCurrentTrick.get.cards) { +
+ +
+

Tricks Won

+ +
+
PLAYER
+
TRICKS
+
+ + @for(player <- gamelobby.getLogic.getPlayerQueue.get.toList.sortBy { p => + -(gamelobby.getLogic.getCurrentRound.get.tricklist.filter { trick => trick.winner.contains(p) }.size) + }) { +
+
@player.name
+
+ @(gamelobby.getLogic.getCurrentRound.get.tricklist.filter { trick => trick.winner.contains(player) }.size) +
+
+ } + +
+
+ @for((cardplayed, player) <- gamelobby.getLogic.getCurrentTrick.get.cards) {
@@ -30,20 +52,20 @@
- } + } +
-

Trumpsuit

@gamelobby.getLogic.getCurrentRound.get.trumpSuit

First Card
- @if(gamelobby.getLogic.getCurrentTrick.get.firstCard.isDefined) { + @if(gamelobby.getLogic.getCurrentTrick.get.firstCard.isDefined) { @util.WebUIUtils.cardtoImage(gamelobby.getLogic.getCurrentTrick.get.firstCard.get) width="80px"/> - } else { + } else { @views.html.render.card.apply("images/cards/1B.png")("Blank Card") width="80px"/> - } + }
@@ -56,14 +78,14 @@
@for(i <- player.currentHand().get.cards.indices) { -
-
- - -
-
+
+
+ + +
+
}
-- 2.52.0 From 33989efedc793aa4f278383d4dcfb10952dc07f1 Mon Sep 17 00:00:00 2001 From: LQ63 Date: Tue, 11 Nov 2025 12:02:40 +0100 Subject: [PATCH 3/7] feat(ui): Ingame layout Added a horizontal end for the container to not stretch endlessly to the sides. Covered sides with lobby background and adjusted the blur behind the cards to match the playing area. --- .../app/assets/stylesheets/main.less | 19 ++- .../app/views/ingame/ingame.scala.html | 153 +++++++++--------- 2 files changed, 86 insertions(+), 86 deletions(-) diff --git a/knockoutwhistweb/app/assets/stylesheets/main.less b/knockoutwhistweb/app/assets/stylesheets/main.less index d449d2b..145789b 100644 --- a/knockoutwhistweb/app/assets/stylesheets/main.less +++ b/knockoutwhistweb/app/assets/stylesheets/main.less @@ -24,14 +24,14 @@ } .game-field-background { background-image: @background-image; - background-size: cover; - background-position: center center; - background-repeat: no-repeat; - background-attachment: fixed; + max-width: 1400px; + margin: 0 auto; + min-height: 100vh; } .lobby-background { background-color: @background-color; - + width: 100%; + height: 100vh; } .navbar-header{ @@ -49,8 +49,11 @@ .bottom-div { position: fixed; bottom: 0; - left: 0; + left: 50%; + transform: translateX(-50%); + max-width: 1400px; width: 100%; + margin: 0; text-align: center; padding: 10px; } @@ -220,9 +223,9 @@ body { } .score-header { font-weight: bold; - color: #ffffff; + color: #000000; border-bottom: 1px solid rgba(255, 255, 255, 0.3); } .score-row { - color: #ffffff; + color: #000000; } \ 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 c4640e2..e18bb49 100644 --- a/knockoutwhistweb/app/views/ingame/ingame.scala.html +++ b/knockoutwhistweb/app/views/ingame/ingame.scala.html @@ -3,92 +3,89 @@ @(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby) @main("Ingame") { -
-
+
+
+
-
-
-

Current Player

-

@gamelobby.getLogic.getCurrentPlayer.get.name

- @if(!TrickUtil.isOver(gamelobby.getLogic.getCurrentMatch.get, gamelobby.getLogic.getPlayerQueue.get)) { -

Next Player

- @for(nextplayer <- gamelobby.getLogic.getPlayerQueue.get.duplicate()) { -

@nextplayer

- } - } -
- -
- -
-

Tricks Won

- -
-
PLAYER
-
TRICKS
-
- - @for(player <- gamelobby.getLogic.getPlayerQueue.get.toList.sortBy { p => - -(gamelobby.getLogic.getCurrentRound.get.tricklist.filter { trick => trick.winner.contains(p) }.size) - }) { -
-
@player.name
-
- @(gamelobby.getLogic.getCurrentRound.get.tricklist.filter { trick => trick.winner.contains(player) }.size) -
-
+
+
+

Current Player

+

@gamelobby.getLogic.getCurrentPlayer.get.name

+ @if(!TrickUtil.isOver(gamelobby.getLogic.getCurrentMatch.get, gamelobby.getLogic.getPlayerQueue.get)) { +

Next Player

+ @for(nextplayer <- gamelobby.getLogic.getPlayerQueue.get.duplicate()) { +

@nextplayer

+ } } -
-
- @for((cardplayed, player) <- gamelobby.getLogic.getCurrentTrick.get.cards) { -
-
-
- @util.WebUIUtils.cardtoImage(cardplayed) width="100%"/> -
-
- @player + +
+ +
+

Tricks Won

+ +
+
PLAYER
+
TRICKS
+
+ + @for(player <- gamelobby.getLogic.getPlayerQueue.get.toList.sortBy { p => + -(gamelobby.getLogic.getCurrentRound.get.tricklist.filter { trick => trick.winner.contains(p) }.size) + }) { +
+
@player.name
+
+ @(gamelobby.getLogic.getCurrentRound.get.tricklist.filter { trick => trick.winner.contains(player) }.size)
+ } + +
+
+ @for((cardplayed, player) <- gamelobby.getLogic.getCurrentTrick.get.cards) { +
+
+
+ @util.WebUIUtils.cardtoImage(cardplayed) width="100%"/> +
+
+ @player +
+
+
+ } +
+
+
+

Trumpsuit

+

@gamelobby.getLogic.getCurrentRound.get.trumpSuit

+ +
First Card
+
+ @if(gamelobby.getLogic.getCurrentTrick.get.firstCard.isDefined) { + @util.WebUIUtils.cardtoImage(gamelobby.getLogic.getCurrentTrick.get.firstCard.get) width="80px"/> + } else { + @views.html.render.card.apply("images/cards/1B.png")("Blank Card") width="80px"/> + } +
+
+
+ +
+
+ @for(i <- player.currentHand().get.cards.indices) { +
+
+ + +
}
-
-

Trumpsuit

-

@gamelobby.getLogic.getCurrentRound.get.trumpSuit

- -
First Card
-
- @if(gamelobby.getLogic.getCurrentTrick.get.firstCard.isDefined) { - @util.WebUIUtils.cardtoImage(gamelobby.getLogic.getCurrentTrick.get.firstCard.get) width="80px"/> - } else { - @views.html.render.card.apply("images/cards/1B.png")("Blank Card") width="80px"/> - } -
-
- - -
- -
- -
-
- @for(i <- player.currentHand().get.cards.indices) { -
-
- - -
-
- } -
-
-
-
+
+
} -- 2.52.0 From b508d2f42865bfdd83b041ca563e744e8066b276 Mon Sep 17 00:00:00 2001 From: LQ63 Date: Tue, 11 Nov 2025 14:10:37 +0100 Subject: [PATCH 4/7] feat(ui): added js routing for create game added js routing for create game, removed the form and button --- .../JavaScriptRoutingController.scala | 23 +++++++++ .../app/controllers/MainMenuController.scala | 18 +++++-- knockoutwhistweb/app/views/main.scala.html | 2 +- .../app/views/mainmenu/creategame.scala.html | 50 +++++++++---------- knockoutwhistweb/conf/routes | 3 +- knockoutwhistweb/public/javascripts/main.js | 45 ++++++++++++++++- 6 files changed, 107 insertions(+), 34 deletions(-) create mode 100644 knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala diff --git a/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala b/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala new file mode 100644 index 0000000..b25f417 --- /dev/null +++ b/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala @@ -0,0 +1,23 @@ +package controllers + +import auth.{AuthAction, AuthenticatedRequest} +import logic.PodManager +import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents} +import play.api.routing.JavaScriptReverseRouter + +import javax.inject.Inject + +class JavaScriptRoutingController @Inject()( + val controllerComponents: ControllerComponents, + val authAction: AuthAction, + val podManager: PodManager + ) extends BaseController { + def javascriptRoutes(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => + Ok( + JavaScriptReverseRouter("jsRoutes")( + routes.javascript.MainMenuController.createGame, + routes.javascript.IngameController.startGame + ) + ).as("text/javascript") + } +} diff --git a/knockoutwhistweb/app/controllers/MainMenuController.scala b/knockoutwhistweb/app/controllers/MainMenuController.scala index 55c4a1b..413df1d 100644 --- a/knockoutwhistweb/app/controllers/MainMenuController.scala +++ b/knockoutwhistweb/app/controllers/MainMenuController.scala @@ -3,6 +3,7 @@ package controllers import auth.{AuthAction, AuthenticatedRequest} import logic.PodManager import play.api.* +import play.api.libs.json.Json import play.api.mvc.* import javax.inject.* @@ -29,16 +30,23 @@ class MainMenuController @Inject()( } def createGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => - val postData = request.body.asFormUrlEncoded - if (postData.isDefined) { - val gamename = postData.get.get("lobbyname").flatMap(_.headOption).getOrElse(s"${request.user.name}'s Game") - val playeramount = postData.get.get("playeramount").flatMap(_.headOption).getOrElse("") + val jsonBody = request.body.asJson + if (jsonBody.isDefined) { + val gamename: String = (jsonBody.get \ "lobbyname").asOpt[String] + .getOrElse(s"${request.user.name}'s Game") + + val playeramount: String = (jsonBody.get \ "playeramount").asOpt[String] + .getOrElse(throw new IllegalArgumentException("Player amount is required and must be an integer.")) + val gameLobby = podManager.createGame( host = request.user, name = gamename, maxPlayers = playeramount.toInt ) - Redirect(routes.IngameController.game(gameLobby.id)) + Ok(Json.obj( + "status" -> "success", + "redirectUrl" -> routes.IngameController.game(gameLobby.id).url + )) } else { BadRequest("Invalid form submission") } diff --git a/knockoutwhistweb/app/views/main.scala.html b/knockoutwhistweb/app/views/main.scala.html index 12d9c2c..e1cc5d1 100644 --- a/knockoutwhistweb/app/views/main.scala.html +++ b/knockoutwhistweb/app/views/main.scala.html @@ -22,7 +22,7 @@ @* And here's where we render the `Html` object containing * the page content. *@ @content - + diff --git a/knockoutwhistweb/app/views/mainmenu/creategame.scala.html b/knockoutwhistweb/app/views/mainmenu/creategame.scala.html index 4872ddc..fd680fd 100644 --- a/knockoutwhistweb/app/views/mainmenu/creategame.scala.html +++ b/knockoutwhistweb/app/views/mainmenu/creategame.scala.html @@ -3,32 +3,30 @@ @main("Create Game") { @navbar(user)
-
-
-
- - -
-
- - -
-
- - -
- 2 - 3 - 4 - 5 - 6 - 7 -
-
-
- -
+
+
+ +
- +
+ + +
+
+ + +
+ 2 + 3 + 4 + 5 + 6 + 7 +
+
+
+
Create Game
+
+
} \ No newline at end of file diff --git a/knockoutwhistweb/conf/routes b/knockoutwhistweb/conf/routes index bba5dda..c8f312e 100644 --- a/knockoutwhistweb/conf/routes +++ b/knockoutwhistweb/conf/routes @@ -3,7 +3,8 @@ # https://www.playframework.com/documentation/latest/ScalaRouting # ~~~~ - +# For the javascript routing +GET /assets/js/routes controllers.JavaScriptRoutingController.javascriptRoutes() # Primary routes GET / controllers.MainMenuController.index() GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) diff --git a/knockoutwhistweb/public/javascripts/main.js b/knockoutwhistweb/public/javascripts/main.js index 36ec495..08ec3bc 100644 --- a/knockoutwhistweb/public/javascripts/main.js +++ b/knockoutwhistweb/public/javascripts/main.js @@ -77,4 +77,47 @@ }) }) }) -})() \ No newline at end of file +})() + +function createGameJS() { + let lobbyName = document.getElementById("lobbyname").value; + if (lobbyName === "") { + lobbyName = "DefaultLobby" + } + const playerAmount = document.getElementById("playeramount").value; + const jsonObj = { + lobbyname: lobbyName, + playeramount: playerAmount + } + sendGameCreationRequest(jsonObj); +} + +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 => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .then(data => { + if (data.status === 'success') { + // Redirect the user + window.location.href = data.redirectUrl; + } else { + console.error("Game creation failed:", data.message); + } + }) + .catch(error => { + console.error('Fetch error:', error); + alert('Could not create game. Please try again.'); + }); +} \ No newline at end of file -- 2.52.0 From 6d958cdd9e7e42e64cbd2b7352ba1621be5364a2 Mon Sep 17 00:00:00 2001 From: LQ63 Date: Tue, 11 Nov 2025 16:46:06 +0100 Subject: [PATCH 5/7] feat(ui): added complete js routing for create game added complete js routing for each button. Removed every form and replaced buttons with divs --- .../app/controllers/IngameController.scala | 106 +++++++++--- .../JavaScriptRoutingController.scala | 5 +- .../app/controllers/MainMenuController.scala | 7 +- .../app/views/ingame/ingame.scala.html | 7 +- .../app/views/lobby/lobby.scala.html | 10 +- knockoutwhistweb/conf/routes | 2 +- knockoutwhistweb/public/javascripts/main.js | 160 ++++++++++++++++-- 7 files changed, 252 insertions(+), 45 deletions(-) diff --git a/knockoutwhistweb/app/controllers/IngameController.scala b/knockoutwhistweb/app/controllers/IngameController.scala index 373701d..d808f05 100644 --- a/knockoutwhistweb/app/controllers/IngameController.scala +++ b/knockoutwhistweb/app/controllers/IngameController.scala @@ -6,6 +6,7 @@ import exceptions.{CantPlayCardException, GameFullException, NotEnoughPlayersExc import logic.PodManager import model.sessions.{PlayerSession, UserSession} import play.api.* +import play.api.libs.json.Json import play.api.mvc.* import java.util.UUID @@ -64,30 +65,70 @@ class IngameController @Inject()( } } if (result.isSuccess) { - Redirect(routes.IngameController.game(gameId)) + Ok(Json.obj( + "status" -> "success", + "redirectUrl" -> routes.IngameController.game(gameId).url + )) } else { val throwable = result.failed.get throwable match { case _: NotInThisGameException => - BadRequest(throwable.getMessage) + BadRequest(Json.obj( + "status" -> "failure", + "errorMessage" -> throwable.getMessage + )) case _: NotHostException => - Forbidden(throwable.getMessage) + Forbidden(Json.obj( + "status" -> "failure", + "errorMessage" -> throwable.getMessage + )) case _: NotEnoughPlayersException => - BadRequest(throwable.getMessage) + BadRequest(Json.obj( + "status" -> "failure", + "errorMessage" -> throwable.getMessage + )) case _ => - InternalServerError(throwable.getMessage) + InternalServerError(Json.obj( + "status" -> "failure", + "errorMessage" -> throwable.getMessage + )) } } } - def kickPlayer(gameId: String, playerToKick: UUID): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => + def kickPlayer(gameId: String, playerToKick: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = podManager.getGame(gameId) - game.get.leaveGame(playerToKick) - Redirect(routes.IngameController.game(gameId)) + val playerToKickUUID = UUID.fromString(playerToKick) + val result = Try { + game.get.leaveGame(playerToKickUUID) + } + if(result.isSuccess) { + Ok(Json.obj( + "status" -> "success", + "redirectUrl" -> routes.IngameController.game(gameId).url + )) + } else { + InternalServerError(Json.obj( + "status" -> "failure", + "errorMessage" -> "Something went wrong." + )) + } } def leaveGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = podManager.getGame(gameId) - game.get.leaveGame(request.user.id) - Redirect(routes.MainMenuController.mainMenu()) + val result = Try { + game.get.leaveGame(request.user.id) + } + if (result.isSuccess) { + Ok(Json.obj( + "status" -> "success", + "redirectUrl" -> routes.MainMenuController.mainMenu().url + )) + } else { + InternalServerError(Json.obj( + "status" -> "failure", + "errorMessage" -> "Something went wrong." + )) + } } def joinGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = podManager.getGame(gameId) @@ -119,7 +160,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] + } cardIdOpt match { case Some(cardId) => var optSession: Option[UserSession] = None @@ -131,27 +175,51 @@ class IngameController @Inject()( } optSession.foreach(_.lock.unlock()) if (result.isSuccess) { - NoContent + Ok(Json.obj( + "status" -> "success", + "redirectUrl" -> routes.IngameController.game(gameId).url + )) } else { 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 + )) } } case None => - BadRequest("cardId parameter is missing") + BadRequest(Json.obj( + "status" -> "failure", + "errorMessage" -> "cardId Parameter is missing" + )) } case None => - NotFound("Game not found") + NotFound(Json.obj( + "status" -> "failure", + "errorMessage" -> "Game not found" + )) } } } diff --git a/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala b/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala index b25f417..2cd5f06 100644 --- a/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala +++ b/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala @@ -16,7 +16,10 @@ class JavaScriptRoutingController @Inject()( Ok( JavaScriptReverseRouter("jsRoutes")( routes.javascript.MainMenuController.createGame, - routes.javascript.IngameController.startGame + routes.javascript.IngameController.startGame, + routes.javascript.IngameController.kickPlayer, + routes.javascript.IngameController.leaveGame, + routes.javascript.IngameController.playCard ) ).as("text/javascript") } diff --git a/knockoutwhistweb/app/controllers/MainMenuController.scala b/knockoutwhistweb/app/controllers/MainMenuController.scala index 413df1d..b4c0e4b 100644 --- a/knockoutwhistweb/app/controllers/MainMenuController.scala +++ b/knockoutwhistweb/app/controllers/MainMenuController.scala @@ -36,7 +36,7 @@ class MainMenuController @Inject()( .getOrElse(s"${request.user.name}'s Game") val playeramount: String = (jsonBody.get \ "playeramount").asOpt[String] - .getOrElse(throw new IllegalArgumentException("Player amount is required and must be an integer.")) + .getOrElse(throw new IllegalArgumentException("Player amount is required.")) val gameLobby = podManager.createGame( host = request.user, @@ -48,7 +48,10 @@ class MainMenuController @Inject()( "redirectUrl" -> routes.IngameController.game(gameLobby.id).url )) } else { - BadRequest("Invalid form submission") + BadRequest(Json.obj( + "status" -> "failure", + "errorMessage" -> "Invalid form submission" + )) } } diff --git a/knockoutwhistweb/app/views/ingame/ingame.scala.html b/knockoutwhistweb/app/views/ingame/ingame.scala.html index e18bb49..271fc28 100644 --- a/knockoutwhistweb/app/views/ingame/ingame.scala.html +++ b/knockoutwhistweb/app/views/ingame/ingame.scala.html @@ -75,12 +75,9 @@
@for(i <- player.currentHand().get.cards.indices) {
-
- - -
+
}
diff --git a/knockoutwhistweb/app/views/lobby/lobby.scala.html b/knockoutwhistweb/app/views/lobby/lobby.scala.html index a8832e9..75ba8d4 100644 --- a/knockoutwhistweb/app/views/lobby/lobby.scala.html +++ b/knockoutwhistweb/app/views/lobby/lobby.scala.html @@ -9,9 +9,7 @@
Lobby-Name: @gamelobby.name
-
- -
+
Exit
@@ -34,16 +32,14 @@ } else {
@playersession.name
@*

Your text could be here!

*@ -
- -
+
Remove
} }
- Start Game +
Start Game
} else { @for(playersession <- gamelobby.getPlayers.values) { diff --git a/knockoutwhistweb/conf/routes b/knockoutwhistweb/conf/routes index c8f312e..9a1a4a6 100644 --- a/knockoutwhistweb/conf/routes +++ b/knockoutwhistweb/conf/routes @@ -26,6 +26,6 @@ GET /logout controllers.UserController.logout() 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: java.util.UUID) +POST /game/:id/kickPlayer controllers.IngameController.kickPlayer(id: String, playerId: String) GET /game/:id/leaveGame controllers.IngameController.leaveGame(id: String) POST /game/:id/playCard controllers.IngameController.playCard(id: String) \ No newline at end of file diff --git a/knockoutwhistweb/public/javascripts/main.js b/knockoutwhistweb/public/javascripts/main.js index 08ec3bc..aa8ec29 100644 --- a/knockoutwhistweb/public/javascripts/main.js +++ b/knockoutwhistweb/public/javascripts/main.js @@ -103,21 +103,161 @@ function sendGameCreationRequest(dataObject) { body: JSON.stringify(dataObject) }) .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return response.json(); + return response.json().then(data => { + if (!response.ok) { + return Promise.reject(data); + } + return data; + }); }) .then(data => { if (data.status === 'success') { - // Redirect the user window.location.href = data.redirectUrl; - } else { - console.error("Game creation failed:", data.message); } }) .catch(error => { - console.error('Fetch error:', error); - alert('Could not create game. Please try again.'); + if (error && error.errorMessage) { + alert(`${error.errorMessage}`); + } else { + alert('An unexpected error occurred. Please try again.'); + } }); -} \ No newline at end of file +} +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 + if (data.status === 'success') { + window.location.href = data.redirectUrl; + } + }) + .catch(error => { + if (error && error.errorMessage) { + alert(`${error.errorMessage}`); + } else { + alert('An unexpected error occurred. Please try again.'); + } + }); +} +function removePlayer(gameid, playersessionId) { + sendRemovePlayerRequest(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 + if (data.status === 'success') { + window.location.href = data.redirectUrl; + } + }) + .catch(error => { + if (error && error.errorMessage) { + alert(`${error.errorMessage}`); + } else { + alert('An unexpected error occurred. Please try again.'); + } + }); +} +function leaveGame(gameId) { + sendLeavePlayerRequest(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; + } + }) + .catch(error => { + if (error && error.errorMessage) { + alert(`${error.errorMessage}`); + } else { + alert('An unexpected error occurred. Please try again.'); + } + }); +} + +function handlePlayCard(cardobject, gameId) { + const cardId = cardobject.dataset.cardId; + const jsonObj = { + cardID: cardId + } + sendPlayCardRequest(jsonObj, gameId) +} + +function sendPlayCardRequest(jsonObj, gameId) { + 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; + } + }) + .catch(error => { + if (error && error.errorMessage) { + alert(`${error.errorMessage}`); + } else { + alert('An unexpected error occurred. Please try again.'); + } + }); +} + -- 2.52.0 From 5c8fd8510ee10a09bc4b67117a5460a5e760d200 Mon Sep 17 00:00:00 2001 From: LQ63 Date: Wed, 12 Nov 2025 11:46:21 +0100 Subject: [PATCH 6/7] feat(ci): Polling Added polling for when the game starts and a card gets played --- .../app/assets/stylesheets/main.less | 6 +- .../app/controllers/IngameController.scala | 115 ++++++++++-- .../JavaScriptRoutingController.scala | 3 +- .../app/logic/game/GameLobby.scala | 41 +++- .../app/logic/game/PollingEvents.scala | 6 + .../app/views/ingame/ingame.scala.html | 17 +- .../app/views/lobby/lobby.scala.html | 7 +- knockoutwhistweb/conf/routes | 4 +- knockoutwhistweb/public/javascripts/main.js | 177 +++++++++++++++++- 9 files changed, 342 insertions(+), 34 deletions(-) create mode 100644 knockoutwhistweb/app/logic/game/PollingEvents.scala diff --git a/knockoutwhistweb/app/assets/stylesheets/main.less b/knockoutwhistweb/app/assets/stylesheets/main.less index 145789b..343b498 100644 --- a/knockoutwhistweb/app/assets/stylesheets/main.less +++ b/knockoutwhistweb/app/assets/stylesheets/main.less @@ -22,6 +22,7 @@ 0% { transform: translateX(-100vw); } 100% { transform: translateX(0); } } + .game-field-background { background-image: @background-image; max-width: 1400px; @@ -184,11 +185,6 @@ body { font-size: 20px; } -#trumpsuit { - display: flex; - flex-direction: row; - margin-left: 4%; -} #nextPlayers { display: flex; flex-direction: column; diff --git a/knockoutwhistweb/app/controllers/IngameController.scala b/knockoutwhistweb/app/controllers/IngameController.scala index d808f05..fcfa2f6 100644 --- a/knockoutwhistweb/app/controllers/IngameController.scala +++ b/knockoutwhistweb/app/controllers/IngameController.scala @@ -1,30 +1,121 @@ package controllers import auth.{AuthAction, AuthenticatedRequest} +import de.knockoutwhist.cards.Hand import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak} import exceptions.{CantPlayCardException, GameFullException, NotEnoughPlayersException, NotHostException, NotInThisGameException} import logic.PodManager +import logic.game.PollingEvents.CardPlayed +import logic.game.PollingEvents.GameStarted +import logic.game.{GameLobby, PollingEvents} import model.sessions.{PlayerSession, UserSession} +import model.users.User import play.api.* -import play.api.libs.json.Json +import play.api.libs.json.{JsArray, JsValue, Json} import play.api.mvc.* +import util.WebUIUtils import java.util.UUID import javax.inject.* +import scala.concurrent.Future import scala.util.Try - - -/** - * This controller creates an `Action` to handle HTTP requests to the - * application's home page. - */ +import scala.concurrent.ExecutionContext @Singleton -class IngameController @Inject()( - val controllerComponents: ControllerComponents, - val authAction: AuthAction, - val podManager: PodManager - ) extends BaseController { +class IngameController @Inject() ( + val cc: ControllerComponents, + val podManager: PodManager, + val authAction: AuthAction, + implicit val ec: ExecutionContext + ) extends AbstractController(cc) { + // --- Helper function (defined outside match/if for scope) --- + def buildSuccessResponse(game: GameLobby, hand: Option[Hand]): JsValue = { + // NOTE: Replace the unsafe .get calls here if game state is not guaranteed + val currentRound = game.logic.getCurrentRound.get + val currentTrick = game.logic.getCurrentTrick.get + + // JSON Building Logic: + val trickCardsJson = Json.toJson( + currentTrick.cards.map { case (card, player) => + Json.obj("cardId" -> WebUIUtils.cardtoString(card), "player" -> player.name) + } + ) + val scoreTableJson = Json.toJson( + game.getLogic.getPlayerQueue.get.toList.map { player => + Json.obj( + "name" -> player.name, + "tricks" -> currentRound.tricklist.count(_.winner.contains(player)) + ) + } + ) + + val stringHand = hand.map { h => + val cardStrings = h.cards.map(WebUIUtils.cardtoString(_)) + Json.toJson(cardStrings).as[JsArray] + }.getOrElse(Json.arr()) + + val firstCardId = currentTrick.firstCard.map(WebUIUtils.cardtoString).getOrElse("BLANK") + val nextPlayer = game.getLogic.getPlayerQueue.get.duplicate().nextPlayer().name + Json.obj( + "status" -> "cardPlayed", + "handData" -> stringHand, + "currentPlayerName" -> game.logic.getCurrentPlayer.get.name, + "trumpSuit" -> currentRound.trumpSuit.toString, + "trickCards" -> trickCardsJson, + "scoreTable" -> scoreTableJson, + "firstCardId" -> firstCardId, + "nextPlayer" -> nextPlayer + ) + } + + def handleEvent(event: PollingEvents, game: GameLobby, user: User): Result = { + event match { + case CardPlayed => + val player = game.getPlayerByUser(user) + val hand = player.currentHand() + val jsonResponse = buildSuccessResponse(game, hand) + Ok(jsonResponse) + case GameStarted => + val jsonResponse = Json.obj( + "status" -> "gameStart", + "redirectUrl" -> routes.IngameController.game(game.id).url + ) + 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() + + Future.successful(handleEvent(event, game, request.user)) + } else { + + val eventPromise = game.registerWaiter(playerId) + + eventPromise.future.map { event => + game.removeWaiter(playerId) + handleEvent(event, game, request.user) + }.recover { + case _: Throwable => + game.removeWaiter(playerId) + NoContent + } + } + + case None => + // Game not found + Future.successful(NotFound("Game not found.")) + } + } def game(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = podManager.getGame(gameId) game match { diff --git a/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala b/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala index 2cd5f06..48c2f9f 100644 --- a/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala +++ b/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala @@ -19,7 +19,8 @@ class JavaScriptRoutingController @Inject()( routes.javascript.IngameController.startGame, routes.javascript.IngameController.kickPlayer, routes.javascript.IngameController.leaveGame, - routes.javascript.IngameController.playCard + routes.javascript.IngameController.playCard, + routes.javascript.IngameController.polling ) ).as("text/javascript") } diff --git a/knockoutwhistweb/app/logic/game/GameLobby.scala b/knockoutwhistweb/app/logic/game/GameLobby.scala index 486bf4f..a897368 100644 --- a/knockoutwhistweb/app/logic/game/GameLobby.scala +++ b/knockoutwhistweb/app/logic/game/GameLobby.scala @@ -2,21 +2,23 @@ package logic.game import de.knockoutwhist.cards.{Hand, Suit} import de.knockoutwhist.control.GameLogic -import de.knockoutwhist.control.GameState.{Lobby, MainMenu} +import de.knockoutwhist.control.GameState.{InGame, Lobby, MainMenu} import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil} -import de.knockoutwhist.events.global.{GameStateChangeEvent, SessionClosed} +import de.knockoutwhist.events.global.{CardPlayedEvent, GameStateChangeEvent, SessionClosed} import de.knockoutwhist.events.player.PlayerEvent import de.knockoutwhist.player.Playertype.HUMAN import de.knockoutwhist.player.{AbstractPlayer, PlayerFactory} import de.knockoutwhist.rounds.{Match, Round, Trick} import de.knockoutwhist.utils.events.{EventListener, SimpleEvent} import exceptions.* +import logic.game.PollingEvents.{CardPlayed, GameStarted} import model.sessions.{InteractionType, UserSession} import model.users.User import java.util.UUID import scala.collection.mutable import scala.collection.mutable.ListBuffer +import scala.concurrent.{Promise => ScalaPromise} class GameLobby private( val logic: GameLogic, @@ -29,7 +31,19 @@ class GameLobby private( logic.createSession() private val users: mutable.Map[UUID, UserSession] = mutable.Map() - + private var pollingState: mutable.Queue[PollingEvents] = mutable.Queue() + private val waitingPromises: mutable.Map[UUID, ScalaPromise[PollingEvents]] = mutable.Map() + + def registerWaiter(playerId: UUID): ScalaPromise[PollingEvents] = { + val promise = ScalaPromise[PollingEvents]() + waitingPromises.put(playerId, promise) + promise + } + + def removeWaiter(playerId: UUID): Unit = { + waitingPromises.remove(playerId) + } + def addUser(user: User): UserSession = { if (users.size >= maxPlayers) throw new GameFullException("The game is full!") if (users.contains(user.id)) throw new IllegalArgumentException("User is already in the game!") @@ -44,12 +58,29 @@ class GameLobby private( override def listen(event: SimpleEvent): Unit = { event match { + case event: CardPlayedEvent => + val newEvent = PollingEvents.CardPlayed + if (waitingPromises.nonEmpty) { + waitingPromises.values.foreach(_.success(newEvent)) + waitingPromises.clear() + } else { + pollingState.enqueue(newEvent) + } case event: PlayerEvent => users.get(event.playerId).foreach(session => session.updatePlayer(event)) case event: GameStateChangeEvent => if (event.oldState == MainMenu && event.newState == Lobby) { return } + if (event.oldState == Lobby && event.newState == InGame) { + val newEvent = PollingEvents.GameStarted + if (waitingPromises.nonEmpty) { + waitingPromises.values.foreach(_.success(newEvent)) + waitingPromises.clear() + } else { + pollingState.enqueue(newEvent) + } + } users.values.foreach(session => session.updatePlayer(event)) case event: SessionClosed => users.values.foreach(session => session.updatePlayer(event)) @@ -183,7 +214,9 @@ class GameLobby private( def getLogic: GameLogic = { logic } - + def getPollingState: mutable.Queue[PollingEvents] = { + pollingState + } private def getPlayerBySession(userSession: UserSession): AbstractPlayer = { val playerOption = getMatch.totalplayers.find(_.id == userSession.id) if (playerOption.isEmpty) { diff --git a/knockoutwhistweb/app/logic/game/PollingEvents.scala b/knockoutwhistweb/app/logic/game/PollingEvents.scala new file mode 100644 index 0000000..933cf82 --- /dev/null +++ b/knockoutwhistweb/app/logic/game/PollingEvents.scala @@ -0,0 +1,6 @@ +package logic.game + +enum PollingEvents { + case CardPlayed + case GameStarted +} \ 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 271fc28..50f45f6 100644 --- a/knockoutwhistweb/app/views/ingame/ingame.scala.html +++ b/knockoutwhistweb/app/views/ingame/ingame.scala.html @@ -10,18 +10,18 @@

Current Player

-

@gamelobby.getLogic.getCurrentPlayer.get.name

+

@gamelobby.getLogic.getCurrentPlayer.get.name

@if(!TrickUtil.isOver(gamelobby.getLogic.getCurrentMatch.get, gamelobby.getLogic.getPlayerQueue.get)) {

Next Player

@for(nextplayer <- gamelobby.getLogic.getPlayerQueue.get.duplicate()) { -

@nextplayer

+

@nextplayer

} }
-
+

Tricks Won

@@ -41,7 +41,7 @@ }
-
+
@for((cardplayed, player) <- gamelobby.getLogic.getCurrentTrick.get.cards) {
@@ -58,10 +58,10 @@

Trumpsuit

-

@gamelobby.getLogic.getCurrentRound.get.trumpSuit

+

@gamelobby.getLogic.getCurrentRound.get.trumpSuit

First Card
-
+
@if(gamelobby.getLogic.getCurrentTrick.get.firstCard.isDefined) { @util.WebUIUtils.cardtoImage(gamelobby.getLogic.getCurrentTrick.get.firstCard.get) width="80px"/> } else { @@ -85,4 +85,9 @@
+ } diff --git a/knockoutwhistweb/app/views/lobby/lobby.scala.html b/knockoutwhistweb/app/views/lobby/lobby.scala.html index 75ba8d4..1ac678f 100644 --- a/knockoutwhistweb/app/views/lobby/lobby.scala.html +++ b/knockoutwhistweb/app/views/lobby/lobby.scala.html @@ -1,7 +1,7 @@ @(user: Option[model.users.User], gamelobby: logic.game.GameLobby) @main("Lobby") { -
+
@@ -66,4 +66,9 @@
+ } \ No newline at end of file diff --git a/knockoutwhistweb/conf/routes b/knockoutwhistweb/conf/routes index 9a1a4a6..8267bdc 100644 --- a/knockoutwhistweb/conf/routes +++ b/knockoutwhistweb/conf/routes @@ -28,4 +28,6 @@ GET /game/:id/join controllers.IngameController.joinGame(id: St GET /game/:id/start controllers.IngameController.startGame(id: String) POST /game/:id/kickPlayer controllers.IngameController.kickPlayer(id: String, playerId: String) GET /game/:id/leaveGame controllers.IngameController.leaveGame(id: String) -POST /game/:id/playCard controllers.IngameController.playCard(id: String) \ No newline at end of file +POST /game/:id/playCard controllers.IngameController.playCard(id: String) +# Polling +GET /polling controllers.IngameController.polling(gameId: String) \ No newline at end of file diff --git a/knockoutwhistweb/public/javascripts/main.js b/knockoutwhistweb/public/javascripts/main.js index aa8ec29..d48ba74 100644 --- a/knockoutwhistweb/public/javascripts/main.js +++ b/knockoutwhistweb/public/javascripts/main.js @@ -79,6 +79,157 @@ }) })() +function pollForUpdates(gameId) { + if (!gameId) { + console.error("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); + return; + } + const route = jsRoutes.controllers.IngameController.polling(gameId); + + // Call your specific controller endpoint + fetch(route.url) + .then(response => { + if (response.status === 204) { + console.log("Polling: Timeout reached. Restarting poll."); + + // CRITICAL: Pass gameId in the recursive call + setTimeout(() => pollForUpdates(gameId), 5000); + } else if (response.ok && response.status === 200) { + response.json().then(data => { + + if (data.status === "cardPlayed" && data.handData) { + console.log("Event received: Card played. Redrawing hand."); + + const newHand = data.handData; + let newHandHTML = ''; + element.innerHTML = ''; + + newHand.forEach((cardId, index) => { + const cardHtml = ` +
+
+ + +
+
+ `; + newHandHTML += cardHtml; + }); + + element.innerHTML = newHandHTML; + + 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 === "gameStart") { + window.location.href = data.redirectUrl; + } + 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); + } + }) + .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 === "") { @@ -226,10 +377,26 @@ function handlePlayCard(cardobject, gameId) { const jsonObj = { cardID: cardId } - sendPlayCardRequest(jsonObj, gameId) + sendPlayCardRequest(jsonObj, gameId, cardobject) } -function sendPlayCardRequest(jsonObj, gameId) { +function sendPlayCardRequest(jsonObj, gameId, cardobject) { + const wiggleKeyframes = [ + { transform: 'translateX(0)' }, + { transform: 'translateX(-5px)' }, + { transform: 'translateX(5px)' }, + { transform: 'translateX(-5px)' }, + { transform: 'translateX(0)' } + ]; + + // Define the timing options + const wiggleTiming = { + duration: 400, // 0.4 seconds + 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, { @@ -249,11 +416,13 @@ function sendPlayCardRequest(jsonObj, gameId) { }) .then(data => { if (data.status === 'success') { - window.location.href = data.redirectUrl; + //window.location.href = data.redirectUrl; } }) .catch(error => { - if (error && error.errorMessage) { + if (error && error.errorMessage.includes("You can't play this card!")) { + cardobject.parentElement.animate(wiggleKeyframes, wiggleTiming); + } else if (error && error.errorMessage) { alert(`${error.errorMessage}`); } else { alert('An unexpected error occurred. Please try again.'); -- 2.52.0 From d5fa9bd2e4e708ac0c8b71f8d91a16146091274d Mon Sep 17 00:00:00 2001 From: Janis Date: Thu, 13 Nov 2025 08:57:55 +0100 Subject: [PATCH 7/7] fix: update hash in knockoutwhist file --- knockoutwhist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/knockoutwhist b/knockoutwhist index 5aa1cef..a5dcf3e 160000 --- a/knockoutwhist +++ b/knockoutwhist @@ -1 +1 @@ -Subproject commit 5aa1cef35689d2df8a89e2d8864fc5fcf9c30e33 +Subproject commit a5dcf3ee904ab548479e23ca7b146df14a835b80 -- 2.52.0