package logic.game import de.knockoutwhist.cards.{Hand, Suit} import de.knockoutwhist.control.GameLogic import de.knockoutwhist.control.GameState.{InGame, Lobby, MainMenu} import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil} import de.knockoutwhist.events.global.{CardPlayedEvent, GameStateChangeEvent, SessionClosed} import de.knockoutwhist.events.player.{PlayerEvent, ReceivedHandEvent} 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, LobbyUpdate, NewRound, ReloadEvent} 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 as ScalaPromise 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() private val users: mutable.Map[UUID, UserSession] = mutable.Map() private val 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!") if (logic.getCurrentState != Lobby) throw new IllegalStateException("The game has already started!") val userSession = new UserSession( user = user, host = false ) users += (user.id -> userSession) addToQueue(LobbyUpdate) userSession } override def listen(event: SimpleEvent): Unit = { event match { case event: ReceivedHandEvent => addToQueue(NewRound) users.get(event.playerId).foreach(session => session.updatePlayer(event)) case event: CardPlayedEvent => addToQueue(CardPlayed) 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) { addToQueue(ReloadEvent) }else { addToQueue(ReloadEvent) } users.values.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)) } } private def addToQueue(event: PollingEvents): Unit = { if (waitingPromises.nonEmpty) { waitingPromises.values.foreach(_.success(event)) waitingPromises.clear() } else { pollingState.enqueue(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) { throw new NotInThisGameException("You are not in this game!") } if (!sessionOpt.get.host) { throw new NotHostException("Only the host can start the game!") } if (logic.getCurrentState != Lobby) { throw new IllegalStateException("The game has already started!") } val playerNamesList = ListBuffer[AbstractPlayer]() users.values.foreach { player => playerNamesList += PlayerFactory.createPlayer(player.name, player.id, HUMAN) } if (playerNamesList.size < 2) { throw new NotEnoughPlayersException("Not enough players to start the game!") } logic.createMatch(playerNamesList.toList) logic.controlMatch() } /** * Remove the user from the game lobby. * @param user the user who wants to leave the game. */ def leaveGame(userId: UUID): Unit = { val sessionOpt = users.get(userId) if (sessionOpt.isEmpty) { throw new NotInThisGameException("You are not in this game!") } users.remove(userId) addToQueue(LobbyUpdate) } /** * 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 = getPlayerInteractable(userSession, InteractionType.Card) if (player.isInDogLife) { throw new CantPlayCardException("You are in dog life!") } val hand = getHand(player) val card = hand.cards(cardIndex) if (!PlayerUtil.canPlayCard(card, getRound, getTrick, player)) { throw new CantPlayCardException("You can't play this card!") } userSession.resetCanInteract() logic.playerInputLogic.receivedCard(card) } /** * Play a card from the player's hand while in dog life or skip the round. * @param userSession the user session of the player. * @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 = getPlayerInteractable(userSession, InteractionType.DogCard) if (!player.isInDogLife) { throw new CantPlayCardException("You are not in dog life!") } if (cardIndex == -1) { if (!MatchUtil.dogNeedsToPlay(getMatch, getRound)) { throw new CantPlayCardException("You can't skip this round!") } logic.playerInputLogic.receivedDog(None) } val hand = getHand(player) val card = hand.cards(cardIndex) userSession.resetCanInteract() logic.playerInputLogic.receivedDog(Some(card)) } /** * Select the trump suit for the round. * @param userSession the user session of the player. * @param trumpIndex the index of the trump suit. */ def selectTrump(userSession: UserSession, trumpIndex: Int): Unit = { val player = getPlayerInteractable(userSession, InteractionType.TrumpSuit) val trumpSuits = Suit.values.toList val selectedTrump = trumpSuits(trumpIndex) userSession.resetCanInteract() logic.playerInputLogic.receivedTrumpSuit(selectedTrump) } /** * * @param userSession * @param tieNumber */ def selectTie(userSession: UserSession, tieNumber: Int): Unit = { val player = getPlayerInteractable(userSession, InteractionType.TieChoice) userSession.resetCanInteract() logic.playerTieLogic.receivedTieBreakerCard(tieNumber) } //------------------- def getUserSession(userId: UUID): UserSession = { val sessionOpt = users.get(userId) if (sessionOpt.isEmpty) { throw new NotInThisGameException("You are not in this game!") } sessionOpt.get } def getPlayerByUser(user: User): AbstractPlayer = { getPlayerBySession(getUserSession(user.id)) } def getPlayers: mutable.Map[UUID, UserSession] = { users.clone() } 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) { throw new NotInThisGameException("You are not in this game!") } playerOption.get } private def getPlayerInteractable(userSession: UserSession, iType: InteractionType): AbstractPlayer = { if (!userSession.lock.isHeldByCurrentThread) { throw new IllegalStateException("The user session is not locked!") } if (userSession.canInteract.isEmpty || userSession.canInteract.get != iType) { throw new NotInteractableException("You can't play a card!") } getPlayerBySession(userSession) } private def getHand(player: AbstractPlayer): Hand = { val handOption = player.currentHand() if (handOption.isEmpty) { throw new IllegalStateException("You have no cards!") } handOption.get } private def getMatch: Match = { val matchOpt = logic.getCurrentMatch if (matchOpt.isEmpty) { throw new IllegalStateException("No match is currently running!") } matchOpt.get } private def getRound: Round = { val roundOpt = logic.getCurrentRound if (roundOpt.isEmpty) { throw new IllegalStateException("No round is currently running!") } roundOpt.get } private def getTrick: Trick = { val trickOpt = logic.getCurrentTrick if (trickOpt.isEmpty) { throw new IllegalStateException("No trick is currently running!") } trickOpt.get } def getUsers: Set[User] = { users.values.map(d => d.user).toSet } } 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 } }