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 {
name: Login
type: http
seq: 1
seq: 2
}
post {
@@ -22,4 +22,5 @@ body:multipart-form {
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -15,7 +15,6 @@ class AuthAction @Inject()(val sessionManager: SessionManager, val parser: BodyP
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] = {
val session = request.cookies.get("sessionId")
if (session.isDefined)
@@ -23,7 +22,6 @@ class AuthAction @Inject()(val sessionManager: SessionManager, val parser: BodyP
None
}
// Transform a normal request into an AuthenticatedRequest
override def invokeBlock[A](
request: Request[A],
block: AuthenticatedRequest[A] => Future[Result]

View File

@@ -1,11 +1,14 @@
package controllers
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.mvc.*
import javax.inject.*
import scala.util.Try
/**
@@ -15,11 +18,227 @@ import javax.inject.*
@Singleton
class IngameController @Inject()(
val controllerComponents: ControllerComponents,
val authAction: AuthAction
val authAction: AuthAction,
val podManager: PodManager
) extends BaseController {
def mainMenu(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
Ok("Main Menu for user: " + request.user.name)
def game(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
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
import auth.{AuthAction, AuthenticatedRequest}
import logic.PodManager
import play.api.*
import play.api.mvc.*
@@ -14,7 +15,8 @@ import javax.inject.*
@Singleton
class MainMenuController @Inject()(
val controllerComponents: ControllerComponents,
val authAction: AuthAction
val authAction: AuthAction,
val podManager: PodManager
) extends BaseController {
// 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] =>
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] = {
Action { implicit request =>
Ok(views.html.rules())
Ok(views.html.mainmenu.rules())
}
}
}

View File

@@ -28,10 +28,10 @@ class UserController @Inject()(
if (possibleUser.isDefined) {
Redirect(routes.MainMenuController.mainMenu())
} else {
Ok(views.html.login())
Ok(views.html.login.login())
}
} 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 logic.game.GameLobby
import model.users.User
import util.GameUtil
import javax.inject.Singleton
import scala.collection.mutable
@@ -27,7 +28,7 @@ class PodManager {
): GameLobby = {
val gameLobby = GameLobby(
logic = BaseGameLogic(injector.getInstance(classOf[Configuration])),
id = java.util.UUID.randomUUID().toString,
id = GameUtil.generateCode(),
internalId = java.util.UUID.randomUUID(),
name = name,
maxPlayers = maxPlayers,

View File

@@ -2,9 +2,9 @@ package logic.game
import de.knockoutwhist.cards.{Hand, Suit}
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.events.global.SessionClosed
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}
@@ -46,9 +46,13 @@ class GameLobby private(
event match {
case event: PlayerEvent =>
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 =>
users.values.foreach(session => session.updatePlayer(event))
case event: SimpleEvent =>
users.values.foreach(session => session.updatePlayer(event))
}
@@ -70,6 +74,9 @@ class GameLobby private(
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()
}
@@ -150,7 +157,7 @@ class GameLobby private(
//-------------------
private def getUserSession(userId: UUID): UserSession = {
def getUserSession(userId: UUID): UserSession = {
val sessionOpt = users.get(userId)
if (sessionOpt.isEmpty) {
throw new NotInThisGameException("You are not in this game!")
@@ -158,6 +165,18 @@ class GameLobby private(
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 = {
if (!Thread.holdsLock(userSession.lock)) {
throw new IllegalStateException("The user session is not locked!")
@@ -165,11 +184,7 @@ class GameLobby private(
if (userSession.canInteract.isEmpty || userSession.canInteract.get != iType) {
throw new NotInteractableException("You can't play a card!")
}
val playerOption = getMatch.totalplayers.find(_.id == userSession.id)
if (playerOption.isEmpty) {
throw new NotInThisGameException("You are not in this game!")
}
playerOption.get
getPlayerBySession(userSession)
}
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") {
<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...") {
<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") {
<div id="tie" class="game-field game-field-background">

View File

@@ -33,7 +33,7 @@
</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);
background-size: cover;
background-repeat: no-repeat;

View File

@@ -12,10 +12,19 @@ GET /assets/*file controllers.Assets.versioned(path="/public",
GET /mainmenu controllers.MainMenuController.mainMenu()
GET /rules controllers.MainMenuController.rules()
POST /createGame controllers.MainMenuController.createGame()
# User authentication routes
GET /login controllers.UserController.login()
POST /login controllers.UserController.login_Post()
GET /logout controllers.UserController.logout()
# 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)