From 8df3491757fd241ff0542023828ddbcba71fa29b Mon Sep 17 00:00:00 2001 From: Janis Date: Sat, 1 Nov 2025 10:30:37 +0100 Subject: [PATCH] feat(user-sessions): enhance game lobby management with user session handling and game full exception --- knockoutwhist | 2 +- .../app/controllers/HomeController.scala | 6 +- .../app/controllers/UserController.scala | 2 +- .../app/exceptions/GameFullException.java | 7 ++ .../app/logic/PodGameManager.scala | 37 -------- knockoutwhistweb/app/logic/PodManager.scala | 48 ++++++++++ .../app/logic/game/GameLobby.scala | 88 +++++++++++++++++-- 7 files changed, 140 insertions(+), 50 deletions(-) create mode 100644 knockoutwhistweb/app/exceptions/GameFullException.java delete mode 100644 knockoutwhistweb/app/logic/PodGameManager.scala create mode 100644 knockoutwhistweb/app/logic/PodManager.scala diff --git a/knockoutwhist b/knockoutwhist index fbc0ea2..e0e45c4 160000 --- a/knockoutwhist +++ b/knockoutwhist @@ -1 +1 @@ -Subproject commit fbc0ea2277596e2a2d29125b5f9a84213336dc18 +Subproject commit e0e45c4b431fff6740e38a59906f5e217fcd801f diff --git a/knockoutwhistweb/app/controllers/HomeController.scala b/knockoutwhistweb/app/controllers/HomeController.scala index b8435f6..a84e529 100644 --- a/knockoutwhistweb/app/controllers/HomeController.scala +++ b/knockoutwhistweb/app/controllers/HomeController.scala @@ -6,7 +6,7 @@ import de.knockoutwhist.components.Configuration import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak} import de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic import di.KnockOutWebConfigurationModule -import logic.PodGameManager +import logic.PodManager import model.sessions.SimpleSession import play.api.mvc.* import play.api.* @@ -44,12 +44,12 @@ class HomeController @Inject()(val controllerComponents: ControllerComponents) e } def rules(): Action[AnyContent] = { Action { implicit request => - Ok(views.html.rules.apply()) + Ok(views.html.rules()) } } def sessions(): Action[AnyContent] = { Action { implicit request => - Ok(views.html.sessions.apply(PodGameManager.listSessions())) + Ok(views.html.rules()) } } diff --git a/knockoutwhistweb/app/controllers/UserController.scala b/knockoutwhistweb/app/controllers/UserController.scala index ce31739..1c7352c 100644 --- a/knockoutwhistweb/app/controllers/UserController.scala +++ b/knockoutwhistweb/app/controllers/UserController.scala @@ -6,7 +6,7 @@ import de.knockoutwhist.components.Configuration import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak} import de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic import di.KnockOutWebConfigurationModule -import logic.PodGameManager +import logic.PodManager import logic.user.{SessionManager, UserManager} import model.sessions.SimpleSession import play.api.* diff --git a/knockoutwhistweb/app/exceptions/GameFullException.java b/knockoutwhistweb/app/exceptions/GameFullException.java new file mode 100644 index 0000000..f0db380 --- /dev/null +++ b/knockoutwhistweb/app/exceptions/GameFullException.java @@ -0,0 +1,7 @@ +package exceptions; + +public class GameFullException extends GameException { + public GameFullException(String message) { + super(message); + } +} diff --git a/knockoutwhistweb/app/logic/PodGameManager.scala b/knockoutwhistweb/app/logic/PodGameManager.scala deleted file mode 100644 index 891e046..0000000 --- a/knockoutwhistweb/app/logic/PodGameManager.scala +++ /dev/null @@ -1,37 +0,0 @@ -package logic - -import de.knockoutwhist.utils.events.SimpleEvent -import model.sessions.PlayerSession - -import java.util.UUID -import scala.collection.mutable - -object PodGameManager { - - private val sessions: mutable.Map[UUID, PlayerSession] = mutable.Map() - - def addSession(session: PlayerSession): Unit = { - sessions.put(session.id, session) - } - - def clearSessions(): Unit = { - sessions.clear() - } - - def identify(id: UUID): Option[PlayerSession] = { - sessions.get(id) - } - - def transmit(id: UUID, event: SimpleEvent): Unit = { - identify(id).foreach(_.updatePlayer(event)) - } - - def transmitAll(event: SimpleEvent): Unit = { - sessions.foreach(session => session._2.updatePlayer(event)) - } - - def listSessions(): List[PlayerSession] = { - sessions.values.toList - } - -} diff --git a/knockoutwhistweb/app/logic/PodManager.scala b/knockoutwhistweb/app/logic/PodManager.scala new file mode 100644 index 0000000..ebf046d --- /dev/null +++ b/knockoutwhistweb/app/logic/PodManager.scala @@ -0,0 +1,48 @@ +package logic + +import com.google.inject.{Guice, Injector} +import de.knockoutwhist.components.Configuration +import de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic +import di.KnockOutWebConfigurationModule +import logic.game.GameLobby +import model.users.User + +import javax.inject.Singleton +import scala.collection.mutable + +@Singleton +class PodManager { + + val TTL: Long = System.currentTimeMillis() + 86400000L // 24 hours in milliseconds + val podIp: String = System.getenv("POD_IP") + val podName: String = System.getenv("POD_NAME") + + private val sessions: mutable.Map[String, GameLobby] = mutable.Map() + private val injector: Injector = Guice.createInjector(KnockOutWebConfigurationModule()) + + def createGame( + host: User, + name: String, + maxPlayers: Int + ): GameLobby = { + val gameLobby = GameLobby( + logic = BaseGameLogic(injector.getInstance(classOf[Configuration])), + id = java.util.UUID.randomUUID().toString, + internalId = java.util.UUID.randomUUID(), + name = name, + maxPlayers = maxPlayers, + host = host + ) + sessions += (gameLobby.id -> gameLobby) + gameLobby + } + + def getGame(gameId: String): Option[GameLobby] = { + sessions.get(gameId) + } + + private[logic] def removeGame(gameId: String): Unit = { + sessions.remove(gameId) + } + +} diff --git a/knockoutwhistweb/app/logic/game/GameLobby.scala b/knockoutwhistweb/app/logic/game/GameLobby.scala index a22392d..1549f8c 100644 --- a/knockoutwhistweb/app/logic/game/GameLobby.scala +++ b/knockoutwhistweb/app/logic/game/GameLobby.scala @@ -2,34 +2,62 @@ package logic.game import de.knockoutwhist.cards.{Hand, Suit} import de.knockoutwhist.control.GameLogic +import de.knockoutwhist.control.GameState.Lobby import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil, RoundUtil} +import de.knockoutwhist.events.global.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.{CantPlayCardException, NotHostException, NotInThisGameException, NotInteractableException} +import exceptions.{CantPlayCardException, GameFullException, NotHostException, NotInThisGameException, NotInteractableException} import model.sessions.{InteractionType, UserSession} import model.users.User import java.util.UUID +import scala.collection.mutable import scala.collection.mutable.ListBuffer -class GameLobby(val logic: GameLogic, val id: String, internalId: UUID) extends EventListener{ +class GameLobby private( + val logic: GameLogic, + val id: String, + val internalId: UUID, + val name: String, + val maxPlayers: Int + ) extends EventListener { logic.addListener(this) logic.createSession() - val users: Map[UUID, UserSession] = Map() + private val users: mutable.Map[UUID, UserSession] = mutable.Map() + + 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!") + if (logic.getCurrentState != Lobby) throw new IllegalStateException("The game has already started!") + val userSession = new UserSession( + user = user, + host = false + ) + users += (user.id -> userSession) + userSession + } override def listen(event: SimpleEvent): Unit = { event match { case event: PlayerEvent => users.get(event.playerId).foreach(session => session.updatePlayer(event)) + case event: SessionClosed => + users.values.foreach(session => session.updatePlayer(event)) + case event: SimpleEvent => users.values.foreach(session => session.updatePlayer(event)) } } + /** + * Start the game if the user is the host. + * @param user the user who wants to start the game. + */ def startGame(user: User): Unit = { val sessionOpt = users.get(user.id) if (sessionOpt.isEmpty) { @@ -46,13 +74,25 @@ class GameLobby(val logic: GameLogic, val id: String, internalId: UUID) extends logic.controlMatch() } + /** + * Remove the user from the game lobby. + * @param user the user who wants to leave the game. + */ + def leaveGame(user: User): Unit = { + val sessionOpt = users.get(user.id) + if (sessionOpt.isEmpty) { + throw new NotInThisGameException("You are not in this game!") + } + users.remove(user.id) + } + /** * Play a card from the player's hand. * @param userSession the user session of the player. * @param cardIndex the index of the card in the player's hand. */ def playCard(userSession: UserSession, cardIndex: Int): Unit = { - val player = getPlayer(userSession, InteractionType.Card) + val player = getPlayerInteractable(userSession, InteractionType.Card) if (player.isInDogLife) { throw new CantPlayCardException("You are in dog life!") } @@ -70,7 +110,7 @@ class GameLobby(val logic: GameLogic, val id: String, internalId: UUID) extends * @param cardIndex the index of the card in the player's hand or -1 if the player wants to skip the round. */ def playDogCard(userSession: UserSession, cardIndex: Int): Unit = { - val player = getPlayer(userSession, InteractionType.DogCard) + val player = getPlayerInteractable(userSession, InteractionType.DogCard) if (!player.isInDogLife) { throw new CantPlayCardException("You are not in dog life!") } @@ -91,7 +131,7 @@ class GameLobby(val logic: GameLogic, val id: String, internalId: UUID) extends * @param trumpIndex the index of the trump suit. */ def selectTrump(userSession: UserSession, trumpIndex: Int): Unit = { - val player = getPlayer(userSession, InteractionType.TrumpSuit) + val player = getPlayerInteractable(userSession, InteractionType.TrumpSuit) val trumpSuits = Suit.values.toList val selectedTrump = trumpSuits(trumpIndex) logic.playerInputLogic.receivedTrumpSuit(selectedTrump) @@ -103,14 +143,22 @@ class GameLobby(val logic: GameLogic, val id: String, internalId: UUID) extends * @param tieNumber */ def selectTie(userSession: UserSession, tieNumber: Int): Unit = { - val player = getPlayer(userSession, InteractionType.TieChoice) + val player = getPlayerInteractable(userSession, InteractionType.TieChoice) logic.playerTieLogic.receivedTieBreakerCard(tieNumber) } //------------------- - private def getPlayer(userSession: UserSession, iType: InteractionType): AbstractPlayer = { + private def getUserSession(userId: UUID): UserSession = { + val sessionOpt = users.get(userId) + if (sessionOpt.isEmpty) { + throw new NotInThisGameException("You are not in this game!") + } + sessionOpt.get + } + + private def getPlayerInteractable(userSession: UserSession, iType: InteractionType): AbstractPlayer = { if (!Thread.holdsLock(userSession.lock)) { throw new IllegalStateException("The user session is not locked!") } @@ -157,3 +205,27 @@ class GameLobby(val logic: GameLogic, val id: String, internalId: UUID) extends } } + +object GameLobby { + def apply( + logic: GameLogic, + id: String, + internalId: UUID, + name: String, + maxPlayers: Int, + host: User + ): GameLobby = { + val lobby = new GameLobby( + logic = logic, + id = id, + internalId = internalId, + name = name, + maxPlayers = maxPlayers + ) + lobby.users += (host.id -> new UserSession( + user = host, + host = true + )) + lobby + } +}