+
+ 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 @@
@gamelobby.getLogic.getCurrentPlayer.get.name
+@gamelobby.getLogic.getCurrentPlayer.get.name
@if(!TrickUtil.isOver(gamelobby.getLogic.getCurrentMatch.get, gamelobby.getLogic.getPlayerQueue.get)) {@nextplayer
+@nextplayer
} }@gamelobby.getLogic.getCurrentRound.get.trumpSuit
+@gamelobby.getLogic.getCurrentRound.get.trumpSuit
+
+