feat!: implemented multigame support #34

Merged
Janis merged 16 commits from feat/5-create-user-sessions into main 2025-11-01 20:53:23 +01:00
46 changed files with 2619 additions and 171 deletions
Showing only changes of commit 8df3491757 - Show all commits

View File

@@ -6,7 +6,7 @@ import de.knockoutwhist.components.Configuration
import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak} import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak}
import de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic import de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic
import di.KnockOutWebConfigurationModule import di.KnockOutWebConfigurationModule
import logic.PodGameManager import logic.PodManager
import model.sessions.SimpleSession import model.sessions.SimpleSession
import play.api.mvc.* import play.api.mvc.*
import play.api.* import play.api.*
@@ -44,12 +44,12 @@ class HomeController @Inject()(val controllerComponents: ControllerComponents) e
} }
def rules(): Action[AnyContent] = { def rules(): Action[AnyContent] = {
Action { implicit request => Action { implicit request =>
Ok(views.html.rules.apply()) Ok(views.html.rules())
} }
} }
def sessions(): Action[AnyContent] = { def sessions(): Action[AnyContent] = {
Action { implicit request => Action { implicit request =>
Ok(views.html.sessions.apply(PodGameManager.listSessions())) Ok(views.html.rules())
} }
} }

View File

@@ -6,7 +6,7 @@ import de.knockoutwhist.components.Configuration
import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak} import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak}
import de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic import de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic
import di.KnockOutWebConfigurationModule import di.KnockOutWebConfigurationModule
import logic.PodGameManager import logic.PodManager
import logic.user.{SessionManager, UserManager} import logic.user.{SessionManager, UserManager}
import model.sessions.SimpleSession import model.sessions.SimpleSession
import play.api.* import play.api.*

View File

@@ -0,0 +1,7 @@
package exceptions;
public class GameFullException extends GameException {
public GameFullException(String message) {
super(message);
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -2,34 +2,62 @@ package logic.game
import de.knockoutwhist.cards.{Hand, Suit} import de.knockoutwhist.cards.{Hand, Suit}
import de.knockoutwhist.control.GameLogic import de.knockoutwhist.control.GameLogic
import de.knockoutwhist.control.GameState.Lobby
import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil, RoundUtil} 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.events.player.PlayerEvent
import de.knockoutwhist.player.Playertype.HUMAN import de.knockoutwhist.player.Playertype.HUMAN
import de.knockoutwhist.player.{AbstractPlayer, PlayerFactory} import de.knockoutwhist.player.{AbstractPlayer, PlayerFactory}
import de.knockoutwhist.rounds.{Match, Round, Trick} import de.knockoutwhist.rounds.{Match, Round, Trick}
import de.knockoutwhist.utils.events.{EventListener, SimpleEvent} 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.sessions.{InteractionType, UserSession}
import model.users.User import model.users.User
import java.util.UUID import java.util.UUID
import scala.collection.mutable
import scala.collection.mutable.ListBuffer 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.addListener(this)
logic.createSession() 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 = { override def listen(event: SimpleEvent): Unit = {
event match { event match {
case event: PlayerEvent => case event: PlayerEvent =>
users.get(event.playerId).foreach(session => session.updatePlayer(event)) users.get(event.playerId).foreach(session => session.updatePlayer(event))
case event: SessionClosed =>
users.values.foreach(session => session.updatePlayer(event))
case event: SimpleEvent => case event: SimpleEvent =>
users.values.foreach(session => session.updatePlayer(event)) 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 = { def startGame(user: User): Unit = {
val sessionOpt = users.get(user.id) val sessionOpt = users.get(user.id)
if (sessionOpt.isEmpty) { if (sessionOpt.isEmpty) {
@@ -46,13 +74,25 @@ class GameLobby(val logic: GameLogic, val id: String, internalId: UUID) extends
logic.controlMatch() 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. * Play a card from the player's hand.
* @param userSession the user session of the player. * @param userSession the user session of the player.
* @param cardIndex the index of the card in the player's hand. * @param cardIndex the index of the card in the player's hand.
*/ */
def playCard(userSession: UserSession, cardIndex: Int): Unit = { def playCard(userSession: UserSession, cardIndex: Int): Unit = {
val player = getPlayer(userSession, InteractionType.Card) val player = getPlayerInteractable(userSession, InteractionType.Card)
if (player.isInDogLife) { if (player.isInDogLife) {
throw new CantPlayCardException("You are in dog life!") 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. * @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 = { def playDogCard(userSession: UserSession, cardIndex: Int): Unit = {
val player = getPlayer(userSession, InteractionType.DogCard) val player = getPlayerInteractable(userSession, InteractionType.DogCard)
if (!player.isInDogLife) { if (!player.isInDogLife) {
throw new CantPlayCardException("You are not in dog life!") 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. * @param trumpIndex the index of the trump suit.
*/ */
def selectTrump(userSession: UserSession, trumpIndex: Int): Unit = { 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 trumpSuits = Suit.values.toList
val selectedTrump = trumpSuits(trumpIndex) val selectedTrump = trumpSuits(trumpIndex)
logic.playerInputLogic.receivedTrumpSuit(selectedTrump) logic.playerInputLogic.receivedTrumpSuit(selectedTrump)
@@ -103,14 +143,22 @@ class GameLobby(val logic: GameLogic, val id: String, internalId: UUID) extends
* @param tieNumber * @param tieNumber
*/ */
def selectTie(userSession: UserSession, tieNumber: Int): Unit = { def selectTie(userSession: UserSession, tieNumber: Int): Unit = {
val player = getPlayer(userSession, InteractionType.TieChoice) val player = getPlayerInteractable(userSession, InteractionType.TieChoice)
logic.playerTieLogic.receivedTieBreakerCard(tieNumber) 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)) { if (!Thread.holdsLock(userSession.lock)) {
throw new IllegalStateException("The user session is not locked!") 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
}
}