Files
KnockOutWhist-Web/knockoutwhistweb/app/controllers/IngameController.scala
LQ63 5c8fd8510e feat(ci): Polling
Added polling for when the game starts and a card gets played
2025-11-12 11:46:21 +01:00

438 lines
15 KiB
Scala

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.{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
import scala.concurrent.ExecutionContext
@Singleton
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 {
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.logic
))
case TieBreak =>
Ok(views.html.ingame.tie(
g.getPlayerByUser(request.user),
g.logic
))
case _ =>
InternalServerError(s"Invalid game state for in-game view. GameId: $gameId" + s" State: ${g.logic.getCurrentState}")
}
case None =>
NotFound("Game not found")
}
//NotFound(s"Reached end of game method unexpectedly. GameId: $gameId")
}
def startGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val game = podManager.getGame(gameId)
val result = Try {
game match {
case Some(g) =>
g.startGame(request.user)
case None =>
NotFound("Game not found")
}
}
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 _: NotHostException =>
Forbidden(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _: NotEnoughPlayersException =>
BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _ =>
InternalServerError(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
}
}
}
def kickPlayer(gameId: String, playerToKick: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val game = podManager.getGame(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)
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)
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 {
case Some(g) =>
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
val result = Try {
val session = g.getUserSession(request.user.id)
optSession = Some(session)
session.lock.lock()
g.playCard(session, cardId.toInt)
}
optSession.foreach(_.lock.unlock())
if (result.isSuccess) {
Ok(Json.obj(
"status" -> "success",
"redirectUrl" -> routes.IngameController.game(gameId).url
))
} else {
val throwable = result.failed.get
throwable match {
case _: CantPlayCardException =>
BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _: NotInThisGameException =>
BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _: IllegalArgumentException =>
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 =>
BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> "cardId Parameter is missing"
))
}
case None =>
NotFound(Json.obj(
"status" -> "failure",
"errorMessage" -> "Game not found"
))
}
}
}
def playDogCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => {
val game = podManager.getGame(gameId)
game match {
case Some(g) => {
val cardIdOpt = request.body.asFormUrlEncoded.flatMap(_.get("cardId").flatMap(_.headOption))
var optSession: Option[UserSession] = None
val result = Try {
cardIdOpt match {
case Some(cardId) if cardId == "skip" =>
val session = g.getUserSession(request.user.id)
optSession = Some(session)
session.lock.lock()
g.playDogCard(session, -1)
case Some(cardId) =>
val session = g.getUserSession(request.user.id)
optSession = Some(session)
session.lock.lock()
g.playDogCard(session, cardId.toInt)
case None =>
throw new IllegalArgumentException("cardId parameter is missing")
}
}
optSession.foreach(_.lock.unlock())
if (result.isSuccess) {
NoContent
} else {
val throwable = result.failed.get
throwable match {
case _: CantPlayCardException =>
BadRequest(throwable.getMessage)
case _: NotInThisGameException =>
BadRequest(throwable.getMessage)
case _: IllegalArgumentException =>
BadRequest(throwable.getMessage)
case _: IllegalStateException =>
BadRequest(throwable.getMessage)
case _ =>
InternalServerError(throwable.getMessage)
}
}
}
case None =>
NotFound("Game not found")
}
}
}
def playTrump(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val game = podManager.getGame(gameId)
game match {
case Some(g) =>
val trumpOpt = request.body.asFormUrlEncoded.flatMap(_.get("trump").flatMap(_.headOption))
trumpOpt match {
case Some(trump) =>
var optSession: Option[UserSession] = None
val result = Try {
val session = g.getUserSession(request.user.id)
optSession = Some(session)
session.lock.lock()
g.selectTrump(session, trump.toInt)
}
optSession.foreach(_.lock.unlock())
if (result.isSuccess) {
NoContent
} else {
val throwable = result.failed.get
throwable match {
case _: IllegalArgumentException =>
BadRequest(throwable.getMessage)
case _: NotInThisGameException =>
BadRequest(throwable.getMessage)
case _: IllegalStateException =>
BadRequest(throwable.getMessage)
case _ =>
InternalServerError(throwable.getMessage)
}
}
case None =>
BadRequest("trump parameter is missing")
}
case None =>
NotFound("Game not found")
}
}
def playTie(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val game = podManager.getGame(gameId)
game match {
case Some(g) =>
val tieOpt = request.body.asFormUrlEncoded.flatMap(_.get("tie").flatMap(_.headOption))
tieOpt match {
case Some(tie) =>
var optSession: Option[UserSession] = None
val result = Try {
val session = g.getUserSession(request.user.id)
optSession = Some(session)
session.lock.lock()
g.selectTie(g.getUserSession(request.user.id), tie.toInt)
}
optSession.foreach(_.lock.unlock())
if (result.isSuccess) {
NoContent
} else {
val throwable = result.failed.get
throwable match {
case _: IllegalArgumentException =>
BadRequest(throwable.getMessage)
case _: NotInThisGameException =>
BadRequest(throwable.getMessage)
case _: IllegalStateException =>
BadRequest(throwable.getMessage)
case _ =>
InternalServerError(throwable.getMessage)
}
}
case None =>
BadRequest("tie parameter is missing")
}
case None =>
NotFound("Game not found")
}
}
}