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
61 changed files with 2996 additions and 262 deletions
Showing only changes of commit 3e6cbe7d2d - Show all commits

View File

@@ -1,7 +1,7 @@
meta { meta {
name: Login name: Login
type: http type: http
seq: 1 seq: 2
} }
post { post {
@@ -22,4 +22,5 @@ body:multipart-form {
settings { settings {
encodeUrl: true encodeUrl: true
timeout: 0
} }

View File

@@ -15,7 +15,6 @@ class AuthAction @Inject()(val sessionManager: SessionManager, val parser: BodyP
override def executionContext: ExecutionContext = ec override def executionContext: ExecutionContext = ec
// This simulates checking if a user is logged in (e.g. via session)
private def getUserFromSession(request: RequestHeader): Option[User] = { private def getUserFromSession(request: RequestHeader): Option[User] = {
val session = request.cookies.get("sessionId") val session = request.cookies.get("sessionId")
if (session.isDefined) if (session.isDefined)
@@ -23,7 +22,6 @@ class AuthAction @Inject()(val sessionManager: SessionManager, val parser: BodyP
None None
} }
// Transform a normal request into an AuthenticatedRequest
override def invokeBlock[A]( override def invokeBlock[A](
request: Request[A], request: Request[A],
block: AuthenticatedRequest[A] => Future[Result] block: AuthenticatedRequest[A] => Future[Result]

View File

@@ -1,11 +1,14 @@
package controllers package controllers
import auth.{AuthAction, AuthenticatedRequest} import auth.{AuthAction, AuthenticatedRequest}
import logic.user.{SessionManager, UserManager} import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak}
import exceptions.{CantPlayCardException, GameFullException, NotEnoughPlayersException, NotHostException, NotInThisGameException}
import logic.PodManager
import play.api.* import play.api.*
import play.api.mvc.* import play.api.mvc.*
import javax.inject.* import javax.inject.*
import scala.util.Try
/** /**
@@ -15,11 +18,227 @@ import javax.inject.*
@Singleton @Singleton
class IngameController @Inject()( class IngameController @Inject()(
val controllerComponents: ControllerComponents, val controllerComponents: ControllerComponents,
val authAction: AuthAction val authAction: AuthAction,
val podManager: PodManager
) extends BaseController { ) extends BaseController {
def mainMenu(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => def game(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
Ok("Main Menu for user: " + request.user.name) val game = podManager.getGame(gameId)
game match {
case Some(g) =>
g.logic.getCurrentState match {
case Lobby => Ok("Lobby: " + gameId)
case InGame =>
Ok(views.html.ingame.ingame(
g.getPlayerByUser(request.user),
g.logic
))
case SelectTrump =>
Ok(views.html.ingame.selecttrump(
g.getPlayerByUser(request.user),
g.logic
))
case TieBreak =>
Ok(views.html.ingame.tie(
g.getPlayerByUser(request.user),
g.logic
))
case _ =>
InternalServerError(s"Invalid game state for in-game view. GameId: $gameId" + s" State: ${g.logic.getCurrentState}")
}
case None =>
NotFound("Game not found")
}
//NotFound(s"Reached end of game method unexpectedly. GameId: $gameId")
}
def startGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val game = podManager.getGame(gameId)
val result = Try {
game match {
case Some(g) =>
g.startGame(request.user)
case None =>
NotFound("Game not found")
}
}
if (result.isSuccess) {
NoContent
} else {
val throwable = result.failed.get
throwable match {
case _: NotInThisGameException =>
BadRequest(throwable.getMessage)
case _: NotHostException =>
Forbidden(throwable.getMessage)
case _: NotEnoughPlayersException =>
BadRequest(throwable.getMessage)
case _ =>
InternalServerError(throwable.getMessage)
}
}
}
def joinGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val game = podManager.getGame(gameId)
val result = Try {
game match {
case Some(g) =>
g.addUser(request.user)
case None =>
NotFound("Game not found")
}
}
if (result.isSuccess) {
Redirect(routes.IngameController.game(gameId))
} else {
val throwable = result.failed.get
throwable match {
case _: GameFullException =>
BadRequest(throwable.getMessage)
case _: IllegalArgumentException =>
BadRequest(throwable.getMessage)
case _: IllegalStateException =>
BadRequest(throwable.getMessage)
case _ =>
InternalServerError(throwable.getMessage)
}
}
}
def playCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => {
val game = podManager.getGame(gameId)
game match {
case Some(g) =>
val cardIdOpt = request.body.asFormUrlEncoded.flatMap(_.get("cardId").flatMap(_.headOption))
cardIdOpt match {
case Some(cardId) =>
val result = Try {
g.playCard(g.getUserSession(request.user.id), cardId.toInt)
}
if (result.isSuccess) {
NoContent
} else {
val throwable = result.failed.get
throwable match {
case _: CantPlayCardException =>
BadRequest(throwable.getMessage)
case _: NotInThisGameException =>
BadRequest(throwable.getMessage)
case _: IllegalArgumentException =>
BadRequest(throwable.getMessage)
case _: IllegalStateException =>
BadRequest(throwable.getMessage)
case _ =>
InternalServerError(throwable.getMessage)
}
}
case None =>
BadRequest("cardId parameter is missing")
}
case None =>
NotFound("Game not found")
}
}
}
def playDogCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => {
val game = podManager.getGame(gameId)
game match {
case Some(g) => {
val cardIdOpt = request.body.asFormUrlEncoded.flatMap(_.get("cardId").flatMap(_.headOption))
val result = Try {
cardIdOpt match {
case Some(cardId) if cardId == "skip" =>
g.playDogCard(g.getUserSession(request.user.id), -1)
case Some(cardId) =>
g.playDogCard(g.getUserSession(request.user.id), cardId.toInt)
case None =>
throw new IllegalArgumentException("cardId parameter is missing")
}
}
if (result.isSuccess) {
NoContent
} else {
val throwable = result.failed.get
throwable match {
case _: CantPlayCardException =>
BadRequest(throwable.getMessage)
case _: NotInThisGameException =>
BadRequest(throwable.getMessage)
case _: IllegalArgumentException =>
BadRequest(throwable.getMessage)
case _: IllegalStateException =>
BadRequest(throwable.getMessage)
case _ =>
InternalServerError(throwable.getMessage)
}
}
}
case None =>
NotFound("Game not found")
}
}
}
def playTrump(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val game = podManager.getGame(gameId)
game match {
case Some(g) =>
val trumpOpt = request.body.asFormUrlEncoded.flatMap(_.get("trump").flatMap(_.headOption))
trumpOpt match {
case Some(trump) =>
val result = Try {
g.selectTrump(g.getUserSession(request.user.id), trump.toInt)
}
if (result.isSuccess) {
NoContent
} else {
val throwable = result.failed.get
throwable match {
case _: IllegalArgumentException =>
BadRequest(throwable.getMessage)
case _: NotInThisGameException =>
BadRequest(throwable.getMessage)
case _: IllegalStateException =>
BadRequest(throwable.getMessage)
case _ =>
InternalServerError(throwable.getMessage)
}
}
case None =>
BadRequest("trump parameter is missing")
}
case None =>
NotFound("Game not found")
}
}
def playTie(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val game = podManager.getGame(gameId)
game match {
case Some(g) =>
val tieOpt = request.body.asFormUrlEncoded.flatMap(_.get("tie").flatMap(_.headOption))
tieOpt match {
case Some(tie) =>
val result = Try {
g.selectTie(g.getUserSession(request.user.id), tie.toInt)
}
if (result.isSuccess) {
NoContent
} else {
val throwable = result.failed.get
throwable match {
case _: IllegalArgumentException =>
BadRequest(throwable.getMessage)
case _: NotInThisGameException =>
BadRequest(throwable.getMessage)
case _: IllegalStateException =>
BadRequest(throwable.getMessage)
case _ =>
InternalServerError(throwable.getMessage)
}
}
case None =>
BadRequest("tie parameter is missing")
}
case None =>
NotFound("Game not found")
}
} }
} }

View File

@@ -1,6 +1,7 @@
package controllers package controllers
import auth.{AuthAction, AuthenticatedRequest} import auth.{AuthAction, AuthenticatedRequest}
import logic.PodManager
import play.api.* import play.api.*
import play.api.mvc.* import play.api.mvc.*
@@ -14,7 +15,8 @@ import javax.inject.*
@Singleton @Singleton
class MainMenuController @Inject()( class MainMenuController @Inject()(
val controllerComponents: ControllerComponents, val controllerComponents: ControllerComponents,
val authAction: AuthAction val authAction: AuthAction,
val podManager: PodManager
) extends BaseController { ) extends BaseController {
// Pass the request-handling function directly to authAction (no nested Action) // Pass the request-handling function directly to authAction (no nested Action)
@@ -23,13 +25,21 @@ class MainMenuController @Inject()(
} }
def index(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => def index(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
Redirect("/mainmenu") Redirect(routes.MainMenuController.mainMenu())
}
def createGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val gameLobby = podManager.createGame(
host = request.user,
name = s"${request.user.name}'s Game",
maxPlayers = 4
)
Redirect(routes.IngameController.game(gameLobby.id))
} }
def rules(): Action[AnyContent] = { def rules(): Action[AnyContent] = {
Action { implicit request => Action { implicit request =>
Ok(views.html.rules()) Ok(views.html.mainmenu.rules())
} }
} }
} }

View File

@@ -28,10 +28,10 @@ class UserController @Inject()(
if (possibleUser.isDefined) { if (possibleUser.isDefined) {
Redirect(routes.MainMenuController.mainMenu()) Redirect(routes.MainMenuController.mainMenu())
} else { } else {
Ok(views.html.login()) Ok(views.html.login.login())
} }
} else { } else {
Ok(views.html.login()) Ok(views.html.login.login())
} }
} }
} }

View File

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

View File

@@ -6,6 +6,7 @@ import de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic
import di.KnockOutWebConfigurationModule import di.KnockOutWebConfigurationModule
import logic.game.GameLobby import logic.game.GameLobby
import model.users.User import model.users.User
import util.GameUtil
import javax.inject.Singleton import javax.inject.Singleton
import scala.collection.mutable import scala.collection.mutable
@@ -27,7 +28,7 @@ class PodManager {
): GameLobby = { ): GameLobby = {
val gameLobby = GameLobby( val gameLobby = GameLobby(
logic = BaseGameLogic(injector.getInstance(classOf[Configuration])), logic = BaseGameLogic(injector.getInstance(classOf[Configuration])),
id = java.util.UUID.randomUUID().toString, id = GameUtil.generateCode(),
internalId = java.util.UUID.randomUUID(), internalId = java.util.UUID.randomUUID(),
name = name, name = name,
maxPlayers = maxPlayers, maxPlayers = maxPlayers,

View File

@@ -2,9 +2,9 @@ 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.GameState.{Lobby, MainMenu}
import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil} import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil}
import de.knockoutwhist.events.global.SessionClosed import de.knockoutwhist.events.global.{GameStateChangeEvent, 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}
@@ -46,9 +46,13 @@ class GameLobby private(
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: GameStateChangeEvent =>
if (event.oldState == MainMenu && event.newState == Lobby) {
return
}
users.values.foreach(session => session.updatePlayer(event))
case event: SessionClosed => case event: SessionClosed =>
users.values.foreach(session => session.updatePlayer(event)) 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))
} }
@@ -70,6 +74,9 @@ class GameLobby private(
users.values.foreach { player => users.values.foreach { player =>
playerNamesList += PlayerFactory.createPlayer(player.name, player.id, HUMAN) 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.createMatch(playerNamesList.toList)
logic.controlMatch() logic.controlMatch()
} }
@@ -150,7 +157,7 @@ class GameLobby private(
//------------------- //-------------------
private def getUserSession(userId: UUID): UserSession = { def getUserSession(userId: UUID): UserSession = {
val sessionOpt = users.get(userId) val sessionOpt = users.get(userId)
if (sessionOpt.isEmpty) { if (sessionOpt.isEmpty) {
throw new NotInThisGameException("You are not in this game!") throw new NotInThisGameException("You are not in this game!")
@@ -158,6 +165,18 @@ class GameLobby private(
sessionOpt.get sessionOpt.get
} }
def getPlayerByUser(user: User): AbstractPlayer = {
getPlayerBySession(getUserSession(user.id))
}
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 = { 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!")
@@ -165,11 +184,7 @@ class GameLobby private(
if (userSession.canInteract.isEmpty || userSession.canInteract.get != iType) { if (userSession.canInteract.isEmpty || userSession.canInteract.get != iType) {
throw new NotInteractableException("You can't play a card!") throw new NotInteractableException("You can't play a card!")
} }
val playerOption = getMatch.totalplayers.find(_.id == userSession.id) getPlayerBySession(userSession)
if (playerOption.isEmpty) {
throw new NotInThisGameException("You are not in this game!")
}
playerOption.get
} }
private def getHand(player: AbstractPlayer): Hand = { private def getHand(player: AbstractPlayer): Hand = {

View File

@@ -1,4 +1,4 @@
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic) @(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic)
@main("Ingame") { @main("Ingame") {
<div id="ingame" class="game-field game-field-background"> <div id="ingame" class="game-field game-field-background">

View File

@@ -1,4 +1,4 @@
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic) @(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic)
@main("Selecting Trumpsuit...") { @main("Selecting Trumpsuit...") {
<div id="selecttrumpsuit" class="game-field game-field-background"> <div id="selecttrumpsuit" class="game-field game-field-background">

View File

@@ -1,4 +1,4 @@
@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.controllerBaseImpl.BaseGameLogic) @(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic)
@main("Tie") { @main("Tie") {
<div id="tie" class="game-field game-field-background"> <div id="tie" class="game-field game-field-background">

View File

@@ -33,7 +33,7 @@
</div> </div>
</div> </div>
</div> </div>
<script src="@routes.Assets.versioned("javascripts/particles.js")"></script> <script src="@routes.Assets.versioned("/javascripts/particles.js")"></script>
<div id="particles-js" style="background-color: rgb(182, 25, 36); <div id="particles-js" style="background-color: rgb(182, 25, 36);
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;

View File

@@ -12,10 +12,19 @@ GET /assets/*file controllers.Assets.versioned(path="/public",
GET /mainmenu controllers.MainMenuController.mainMenu() GET /mainmenu controllers.MainMenuController.mainMenu()
GET /rules controllers.MainMenuController.rules() GET /rules controllers.MainMenuController.rules()
POST /createGame controllers.MainMenuController.createGame()
# User authentication routes # User authentication routes
GET /login controllers.UserController.login() GET /login controllers.UserController.login()
POST /login controllers.UserController.login_Post() POST /login controllers.UserController.login_Post()
GET /logout controllers.UserController.logout() GET /logout controllers.UserController.logout()
# In-game routes # In-game routes
# GET /ingame/:id controllers.MainMenuController.ingame(id: String) GET /game/:id controllers.IngameController.game(id: String)
POST /game/:id/join controllers.IngameController.joinGame(id: String)
POST /game/:id/start controllers.IngameController.startGame(id: String)
POST /game/:id/playCard controllers.IngameController.playCard(id: String)