Files
KnockOutWhist-Web/knockoutwhistweb/app/logic/game/GameLobby.scala
lq64 d57e6efa98 feat(ui): FRO-7 Endscreen (#97)
Added a nice look to the endscreen. Implemented a ranking method inside GameLobby to get an order.

Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #97
Reviewed-by: Janis <janis-e@gmx.de>
Co-authored-by: lq64 <lq@blackhole.local>
Co-committed-by: lq64 <lq@blackhole.local>
2025-12-03 09:18:11 +01:00

345 lines
11 KiB
Scala

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.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil}
import de.knockoutwhist.events.global.{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 events.{KickEvent, LeftEvent, LobbyUpdateEvent, UserEvent}
import exceptions.*
import logic.PodManager
import model.sessions.{InteractionType, UserSession}
import model.users.User
import play.api.libs.json.{JsObject, Json}
import java.util.{Timer, TimerTask, UUID}
import scala.collection.mutable
import scala.collection.mutable.ListBuffer
import scala.util.Try
class GameLobby private(
val logic: GameLogic,
val id: String,
val internalId: UUID,
val name: String,
val maxPlayers: Int
) extends EventListener {
private val users: mutable.Map[UUID, UserSession] = mutable.Map()
logic.addListener(this)
logic.createSession()
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,
gameLobby = this
)
users += (user.id -> userSession)
PodManager.registerUserToGame(user, id)
logic.invoke(LobbyUpdateEvent())
userSession
}
override def listen(event: SimpleEvent): Unit = {
event match {
case event: PlayerEvent =>
users.get(event.playerId).foreach(session => session.updatePlayer(event))
case event: UserEvent =>
users.get(event.userId).foreach(session => session.updatePlayer(event))
case event: GameStateChangeEvent =>
if (event.oldState == MainMenu && event.newState == Lobby) {
return
}
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) {
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.
* @param kicked whether the user was kicked or left voluntarily.
*/
def leaveGame(userId: UUID, kicked: Boolean): Unit = {
val sessionOpt = users.get(userId)
if (sessionOpt.isEmpty) {
throw new NotInThisGameException("You are not in this game!")
}
if (sessionOpt.get.host) {
logic.invoke(SessionClosed())
users.clear()
PodManager.removeGame(id)
return
}
if (kicked) {
logic.invoke(KickEvent(sessionOpt.get.user))
} else {
logic.invoke(LeftEvent(sessionOpt.get.user))
}
users.remove(userId)
PodManager.unregisterUserFromGame(sessionOpt.get.user)
logic.invoke(LobbyUpdateEvent())
}
/**
* 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)
}
private def getHand(player: AbstractPlayer): Hand = {
val handOption = player.currentHand()
if (handOption.isEmpty) {
throw new IllegalStateException("You have no cards!")
}
handOption.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
}
/**
* 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)
return
}
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)
}
//-------------------
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)
}
/**
*
* @param userSession
* @param tieNumber
*/
def selectTie(userSession: UserSession, tieNumber: Int): Unit = {
val player = getPlayerInteractable(userSession, InteractionType.TieChoice)
userSession.resetCanInteract()
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()
}
def getPlayerByUser(user: User): AbstractPlayer = {
getPlayerBySession(getUserSession(user.id))
}
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 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 getMatch: Match = {
val matchOpt = logic.getCurrentMatch
if (matchOpt.isEmpty) {
throw new IllegalStateException("No match is currently running!")
}
matchOpt.get
}
def getPlayers: mutable.Map[UUID, UserSession] = {
users.clone()
}
def getLogic: GameLogic = {
logic
}
def getUsers: Set[User] = {
users.values.map(d => d.user).toSet
}
def getFinalRanking: List[(String, (Int, Int))] = {
Try {
val match1 = getMatch
if (!match1.isOver) {
List.empty
} else {
val winnerName = logic.getWinner.get.name
val allPlayerNames = match1.totalplayers.map(_.name)
val roundlist = match1.roundlist
val playerMetrics: Map[String, (Int, Int)] = allPlayerNames.map { name =>
val roundsWon = roundlist.count { round =>
round.winner.exists(_.name == name)
}
val totalTricksWon = roundlist.flatMap(_.tricklist).count { trick =>
trick.winner.exists(_.name == name)
}
name -> (roundsWon, totalTricksWon)
}.toMap
val winnerMetrics = playerMetrics(winnerName)
val remainingPlayersMetrics = playerMetrics.view.filterKeys(_ != winnerName).toList
val sortedRemainingPlayers = remainingPlayersMetrics.sortBy { case (_, (rounds, tricks)) =>
(-rounds, -tricks)
}
(winnerName, winnerMetrics) :: sortedRemainingPlayers
}
}.getOrElse(List())
}
private def transmitToAll(event: JsObject): Unit = {
users.values.foreach(session => {
session.websocketActor.foreach(act => act.transmitJsonToClient(event))
})
}
}
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,
gameLobby = lobby
))
lobby
}
}