feat(game): Implement return to lobby functionality and enhance dog life handling

This commit is contained in:
2025-11-19 19:18:41 +01:00
parent e2a5cb9614
commit b260e18223
8 changed files with 143 additions and 45 deletions

View File

@@ -227,7 +227,10 @@ class IngameController @Inject() (
val game = podManager.getGame(gameId) val game = podManager.getGame(gameId)
game match { game match {
case Some(g) => { 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 var optSession: Option[UserSession] = None
val result = Try { val result = Try {
cardIdOpt match { cardIdOpt match {
@@ -344,4 +347,46 @@ class IngameController @Inject() (
} }
} }
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"
))
}
}
} }

View File

@@ -20,6 +20,7 @@ class JavaScriptRoutingController @Inject()(
routes.javascript.IngameController.kickPlayer, routes.javascript.IngameController.kickPlayer,
routes.javascript.IngameController.leaveGame, routes.javascript.IngameController.leaveGame,
routes.javascript.IngameController.playCard, routes.javascript.IngameController.playCard,
routes.javascript.IngameController.playDogCard,
routes.javascript.PollingController.polling routes.javascript.PollingController.polling
) )
).as("text/javascript") ).as("text/javascript")

View File

@@ -3,6 +3,7 @@ package controllers
import auth.{AuthAction, AuthenticatedRequest} import auth.{AuthAction, AuthenticatedRequest}
import controllers.PollingController.{scheduler, timeoutDuration} import controllers.PollingController.{scheduler, timeoutDuration}
import de.knockoutwhist.cards.Hand import de.knockoutwhist.cards.Hand
import de.knockoutwhist.player.AbstractPlayer
import logic.PodManager import logic.PodManager
import logic.game.{GameLobby, PollingEvents} import logic.game.{GameLobby, PollingEvents}
import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, ReloadEvent} import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, ReloadEvent}
@@ -28,7 +29,7 @@ class PollingController @Inject() (
implicit val ec: ExecutionContext implicit val ec: ExecutionContext
) extends AbstractController(cc) { ) 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 currentRound = game.logic.getCurrentRound.get
val currentTrick = game.logic.getCurrentTrick.get val currentTrick = game.logic.getCurrentTrick.get
@@ -57,6 +58,7 @@ class PollingController @Inject() (
"status" -> "cardPlayed", "status" -> "cardPlayed",
"animation" -> newRound, "animation" -> newRound,
"handData" -> stringHand, "handData" -> stringHand,
"dog" -> player.isInDogLife,
"currentPlayerName" -> game.logic.getCurrentPlayer.get.name, "currentPlayerName" -> game.logic.getCurrentPlayer.get.name,
"trumpSuit" -> currentRound.trumpSuit.toString, "trumpSuit" -> currentRound.trumpSuit.toString,
"trickCards" -> trickCardsJson, "trickCards" -> trickCardsJson,
@@ -84,12 +86,12 @@ class PollingController @Inject() (
case NewRound => case NewRound =>
val player = game.getPlayerByUser(userSession.user) val player = game.getPlayerByUser(userSession.user)
val hand = player.currentHand() val hand = player.currentHand()
val jsonResponse = buildCardPlayResponse(game, hand, true) val jsonResponse = buildCardPlayResponse(game, hand, player, true)
Ok(jsonResponse) Ok(jsonResponse)
case CardPlayed => case CardPlayed =>
val player = game.getPlayerByUser(userSession.user) val player = game.getPlayerByUser(userSession.user)
val hand = player.currentHand() val hand = player.currentHand()
val jsonResponse = buildCardPlayResponse(game, hand, false) val jsonResponse = buildCardPlayResponse(game, hand, player, false)
Ok(jsonResponse) Ok(jsonResponse)
case LobbyUpdate => case LobbyUpdate =>
Ok(buildLobbyUsersResponse(game, userSession)) Ok(buildLobbyUsersResponse(game, userSession))

View File

@@ -88,12 +88,7 @@ class GameLobby private(
if (event.oldState == MainMenu && event.newState == Lobby) { if (event.oldState == MainMenu && event.newState == Lobby) {
return return
} }
if (event.oldState == Lobby && event.newState == InGame) { addToQueue(ReloadEvent)
addToQueue(ReloadEvent)
return
} else {
addToQueue(ReloadEvent)
}
users.values.foreach(session => session.updatePlayer(event)) users.values.foreach(session => session.updatePlayer(event))
case event: SessionClosed => case event: SessionClosed =>
users.values.foreach(session => session.updatePlayer(event)) users.values.foreach(session => session.updatePlayer(event))
@@ -198,7 +193,7 @@ class GameLobby private(
throw new CantPlayCardException("You are not in dog life!") throw new CantPlayCardException("You are not in dog life!")
} }
if (cardIndex == -1) { if (cardIndex == -1) {
if (!MatchUtil.dogNeedsToPlay(getMatch, getRound)) { if (MatchUtil.dogNeedsToPlay(getMatch, getRound)) {
throw new CantPlayCardException("You can't skip this round!") throw new CantPlayCardException("You can't skip this round!")
} }
logic.playerInputLogic.receivedDog(None) logic.playerInputLogic.receivedDog(None)
@@ -233,6 +228,19 @@ class GameLobby private(
logic.playerTieLogic.receivedTieBreakerCard(tieNumber) 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()
}
//------------------- //-------------------

View File

@@ -75,9 +75,14 @@
<div class="row justify-content-center ingame-cards-slide" id="card-slide"> <div class="row justify-content-center ingame-cards-slide" id="card-slide">
@for(i <- player.currentHand().get.cards.indices) { @for(i <- player.currentHand().get.cards.indices) {
<div class="col-auto handcard" style="border-radius: 6px"> <div class="col-auto handcard" style="border-radius: 6px">
<div class="btn btn-outline-light p-0 border-0 shadow-none" data-card-id="@i" style="border-radius: 6px" onclick="handlePlayCard(this, '@gamelobby.id')"> <div class="btn btn-outline-light p-0 border-0 shadow-none" data-card-id="@i" style="border-radius: 6px" onclick="handlePlayCard(this, '@gamelobby.id', @player.isInDogLife)">
@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"/>
</div> </div>
@if(player.isInDogLife) {
<div class="mt-2">
<button class="btn btn-danger" onclick="handleSkipDogLife('@gamelobby.id')">Skip Dog Life</button>
</div>
}
</div> </div>
} }
</div> </div>

View File

@@ -18,13 +18,13 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
</head> </head>
<body class="d-flex flex-column min-vh-100"> <body class="d-flex flex-column min-vh-100" id="main-body">
@* And here's where we render the `Html` object containing @* And here's where we render the `Html` object containing
* the page content. *@ * the page content. *@
@content @content
<script src="@routes.JavaScriptRoutingController.javascriptRoutes()" type="text/javascript"></script>
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</body> </body>
<script src="@routes.JavaScriptRoutingController.javascriptRoutes()" type="text/javascript"></script>
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</html> </html>

View File

@@ -4,34 +4,38 @@
# ~~~~ # ~~~~
# For the javascript routing # For the javascript routing
GET /assets/js/routes controllers.JavaScriptRoutingController.javascriptRoutes() GET /assets/js/routes controllers.JavaScriptRoutingController.javascriptRoutes()
# Primary routes # Primary routes
GET / controllers.MainMenuController.index() GET / controllers.MainMenuController.index()
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
# Main menu routes # Main menu routes
GET /mainmenu controllers.MainMenuController.mainMenu() GET /mainmenu controllers.MainMenuController.mainMenu()
GET /rules controllers.MainMenuController.rules() GET /rules controllers.MainMenuController.rules()
POST /createGame controllers.MainMenuController.createGame() POST /createGame controllers.MainMenuController.createGame()
POST /joinGame controllers.MainMenuController.joinGame() POST /joinGame controllers.MainMenuController.joinGame()
# User authentication routes # User authentication routes
GET /login controllers.UserController.login() GET /login controllers.UserController.login()
POST /login controllers.UserController.login_Post() POST /login controllers.UserController.login_Post()
GET /logout controllers.UserController.logout() GET /logout controllers.UserController.logout()
# In-game routes # In-game routes
GET /game/:id controllers.IngameController.game(id: String) GET /game/:id controllers.IngameController.game(id: String)
GET /game/:id/join controllers.IngameController.joinGame(id: String) GET /game/:id/join controllers.IngameController.joinGame(id: String)
GET /game/:id/start controllers.IngameController.startGame(id: String) GET /game/:id/start controllers.IngameController.startGame(id: String)
POST /game/:id/kickPlayer controllers.IngameController.kickPlayer(id: String, playerId: 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/trump controllers.IngameController.playTrump(id: String)
POST /game/:id/tie controllers.IngameController.playTie(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 # Polling
GET /polling controllers.PollingController.polling(gameId: String) GET /polling/:gameId controllers.PollingController.polling(gameId: String)

View File

@@ -89,7 +89,7 @@ function pollForUpdates(gameId) {
const $lobbyElement = $('#lobbybackground'); const $lobbyElement = $('#lobbybackground');
const $mainmenuElement = $('#main-menu-screen') const $mainmenuElement = $('#main-menu-screen')
if (!$handElement.length && !$lobbyElement.length && !$mainmenuElement.length) { if (!$handElement.length && !$lobbyElement.length && !$mainmenuElement.length) {
setTimeout(() => pollForUpdates(gameId), 5000); setTimeout(() => pollForUpdates(gameId), 1000);
return; return;
} }
const route = jsRoutes.controllers.PollingController.polling(gameId); const route = jsRoutes.controllers.PollingController.polling(gameId);
@@ -115,13 +115,15 @@ function pollForUpdates(gameId) {
$handElement.removeClass('ingame-cards-slide'); $handElement.removeClass('ingame-cards-slide');
} }
const dog = data.dog;
newHand.forEach((cardId, index) => { newHand.forEach((cardId, index) => {
const cardHtml = ` const cardHtml = `
<div class="col-auto handcard" style="border-radius: 6px"> <div class="col-auto handcard" style="border-radius: 6px">
<div class="btn btn-outline-light p-0 border-0 shadow-none" <div class="btn btn-outline-light p-0 border-0 shadow-none"
data-card-id="${index}" data-card-id="${index}"
style="border-radius: 6px" style="border-radius: 6px"
onclick="handlePlayCard(this, '${gameId}')"> onclick="handlePlayCard(this, '${gameId}', '${dog}')">
<img src="/assets/images/cards/${cardId}.png" width="120px" style="border-radius: 6px"/> <img src="/assets/images/cards/${cardId}.png" width="120px" style="border-radius: 6px"/>
</div> </div>
@@ -130,6 +132,14 @@ function pollForUpdates(gameId) {
newHandHTML += cardHtml; newHandHTML += cardHtml;
}); });
if (dog) {
newHandHTML += `
<div class="mt-2">
<button class="btn btn-danger" onclick="handleSkipDogLife(this, '${gameId}')">Skip Dog Life</button>
</div>
`;
}
$handElement.html(newHandHTML); $handElement.html(newHandHTML);
$('#current-player-name').text(data.currentPlayerName) $('#current-player-name').text(data.currentPlayerName)
if (data.nextPlayer) { if (data.nextPlayer) {
@@ -248,12 +258,12 @@ function pollForUpdates(gameId) {
console.error(`Something unexpected happened while polling. ${jqXHR.status}, ${errorThrown}`) console.error(`Something unexpected happened while polling. ${jqXHR.status}, ${errorThrown}`)
} }
}), }),
complete: ((jqXHR, textStatus) => { complete: (() => {
if (!window.location.href.includes("game")) { if (!window.location.href.includes("game")) {
console.log("[DEBUG] Page URL changed. Stopping poll restart."); console.log("[DEBUG] Page URL changed. Stopping poll restart.");
return; return;
} }
setTimeout(() => pollForUpdates(gameId), 500); setTimeout(() => pollForUpdates(gameId), 200);
}) })
}) })
} }
@@ -374,7 +384,7 @@ function sendLeavePlayerRequest(gameId) {
}) })
} }
function handlePlayCard(cardobject, gameId) { function handlePlayCard(cardobject, gameId, dog = false) {
const cardId = cardobject.dataset.cardId; const cardId = cardobject.dataset.cardId;
const jsonObj = { const jsonObj = {
cardID: cardId cardID: cardId
@@ -382,6 +392,33 @@ function handlePlayCard(cardobject, gameId) {
sendPlayCardRequest(jsonObj, gameId, cardobject) sendPlayCardRequest(jsonObj, gameId, cardobject)
} }
function handleSkipDogLife(cardobject, gameId) {
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);
}
if (error?.errorMessage) {
alert(`${error.errorMessage}`);
} else {
alert('An unexpected error occurred. Please try again.');
}
})
})
}
function sendPlayCardRequest(jsonObj, gameId, cardobject) { function sendPlayCardRequest(jsonObj, gameId, cardobject) {
const wiggleKeyframes = [ const wiggleKeyframes = [
{ transform: 'translateX(0)' }, { transform: 'translateX(0)' },
@@ -405,10 +442,6 @@ function sendPlayCardRequest(jsonObj, gameId, cardobject) {
contentType: 'application/json', contentType: 'application/json',
dataType: 'json', dataType: 'json',
data: JSON.stringify(jsonObj), data: JSON.stringify(jsonObj),
success: (data => {
if (data.status === 'success') {
}
}),
error: (jqXHR => { error: (jqXHR => {
try { try {
error = JSON.parse(jqXHR.responseText); error = JSON.parse(jqXHR.responseText);