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.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())
}
}

View File

@@ -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.*

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