From c77eeff12347d5415d91ae51a1687e007dc52fee Mon Sep 17 00:00:00 2001 From: Janis Date: Mon, 13 Oct 2025 15:16:41 +0200 Subject: [PATCH] Refactor match handling and session management; add player session functionality and update event handling in WebUI --- .../WebApplicationConfiguration.scala | 6 +- .../app/controllers/GameManager.scala | 8 - .../app/controllers/HomeController.scala | 21 +- .../app/controllers/PodGameManager.scala | 37 ++++ knockoutwhistweb/app/controllers/WebUI.scala | 77 ++++++++ .../controllers/sessions/PlayerSession.scala | 12 ++ .../SimpleSession.scala} | 186 ++++++------------ knockoutwhistweb/conf/routes | 4 +- 8 files changed, 209 insertions(+), 142 deletions(-) delete mode 100644 knockoutwhistweb/app/controllers/GameManager.scala create mode 100644 knockoutwhistweb/app/controllers/PodGameManager.scala create mode 100644 knockoutwhistweb/app/controllers/WebUI.scala create mode 100644 knockoutwhistweb/app/controllers/sessions/PlayerSession.scala rename knockoutwhistweb/app/controllers/{WebUIMain.scala => sessions/SimpleSession.scala} (58%) diff --git a/knockoutwhistweb/app/components/WebApplicationConfiguration.scala b/knockoutwhistweb/app/components/WebApplicationConfiguration.scala index 0747614..8226cf8 100644 --- a/knockoutwhistweb/app/components/WebApplicationConfiguration.scala +++ b/knockoutwhistweb/app/components/WebApplicationConfiguration.scala @@ -1,13 +1,13 @@ package components -import controllers.WebUIMain +import controllers.WebUI import de.knockoutwhist.components.DefaultConfiguration import de.knockoutwhist.ui.UI import de.knockoutwhist.utils.events.EventListener class WebApplicationConfiguration extends DefaultConfiguration { - override def uis: Set[UI] = super.uis + WebUIMain - override def listener: Set[EventListener] = super.listener + WebUIMain + override def uis: Set[UI] = super.uis + WebUI + override def listener: Set[EventListener] = super.listener + WebUI } diff --git a/knockoutwhistweb/app/controllers/GameManager.scala b/knockoutwhistweb/app/controllers/GameManager.scala deleted file mode 100644 index 904415b..0000000 --- a/knockoutwhistweb/app/controllers/GameManager.scala +++ /dev/null @@ -1,8 +0,0 @@ -package controllers - -object GameManager { - - - - -} diff --git a/knockoutwhistweb/app/controllers/HomeController.scala b/knockoutwhistweb/app/controllers/HomeController.scala index e8a769c..4489cd5 100644 --- a/knockoutwhistweb/app/controllers/HomeController.scala +++ b/knockoutwhistweb/app/controllers/HomeController.scala @@ -1,5 +1,6 @@ package controllers +import controllers.sessions.SimpleSession import com.google.inject.{Guice, Injector} import de.knockoutwhist.KnockOutWhist import de.knockoutwhist.components.Configuration @@ -7,8 +8,10 @@ import di.KnockOutWebConfigurationModule import play.api.* import play.api.mvc.* +import java.util.UUID import javax.inject.* + /** * This controller creates an `Action` to handle HTTP requests to the * application's home page. @@ -36,9 +39,23 @@ class HomeController @Inject()(val controllerComponents: ControllerComponents) e } } - def ingame(): Action[AnyContent] = { + def sessions(): Action[AnyContent] = { Action { implicit request => - Ok(views.html.tui.apply(WebUIMain.latestOutput)) + Ok(views.html.tui.apply(PodGameManager.listSessions().map(f => f.toString + "\n").mkString(""))) + } + } + + def ingame(id: String): Action[AnyContent] = { + val uuid: UUID = UUID.fromString(id) + if (PodGameManager.identify(uuid).isEmpty) { + Action { implicit request => + NotFound(views.html.tui.apply("Player not found")) + } + } else { + val session = PodGameManager.identify(uuid).get + Action { implicit request => + Ok(views.html.tui.apply(session.asInstanceOf[SimpleSession].get())) + } } } diff --git a/knockoutwhistweb/app/controllers/PodGameManager.scala b/knockoutwhistweb/app/controllers/PodGameManager.scala new file mode 100644 index 0000000..f1f951f --- /dev/null +++ b/knockoutwhistweb/app/controllers/PodGameManager.scala @@ -0,0 +1,37 @@ +package controllers + +import controllers.sessions.PlayerSession +import de.knockoutwhist.utils.events.SimpleEvent + +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[UUID] = { + sessions.keys.toList + } + +} diff --git a/knockoutwhistweb/app/controllers/WebUI.scala b/knockoutwhistweb/app/controllers/WebUI.scala new file mode 100644 index 0000000..06eac00 --- /dev/null +++ b/knockoutwhistweb/app/controllers/WebUI.scala @@ -0,0 +1,77 @@ +package controllers + +import controllers.sessions.SimpleSession +import de.knockoutwhist.cards.{Card, CardValue, Hand, Suit} +import de.knockoutwhist.events.* +import de.knockoutwhist.events.ERROR_STATUS.* +import de.knockoutwhist.events.GLOBAL_STATUS.* +import de.knockoutwhist.events.PLAYER_STATUS.* +import de.knockoutwhist.events.ROUND_STATUS.{PLAYERS_OUT, SHOW_START_ROUND, WON_ROUND} +import de.knockoutwhist.events.cards.{RenderHandEvent, ShowTieCardsEvent} +import de.knockoutwhist.events.round.ShowCurrentTrickEvent +import de.knockoutwhist.events.ui.GameState.{INGAME, MAIN_MENU} +import de.knockoutwhist.events.ui.{GameState, GameStateUpdateEvent} +import de.knockoutwhist.player.AbstractPlayer +import de.knockoutwhist.rounds.Match +import de.knockoutwhist.ui.UI +import de.knockoutwhist.utils.CustomThread +import de.knockoutwhist.utils.events.{EventListener, SimpleEvent} + +object WebUI extends CustomThread with EventListener with UI { + + setName("WebUI") + + var init = false + private var internState: GameState = GameState.NO_SET + + var latestOutput: String = "" + + override def instance: CustomThread = WebUI + + override def listen(event: SimpleEvent): Unit = { + runLater { + event match { + case event: RenderHandEvent => + PodGameManager.transmit(event.player.id, event) + case event: ShowTieCardsEvent => + PodGameManager.transmitAll(event) + case event: ShowGlobalStatus => + if (event.status == TECHNICAL_MATCH_STARTED) { + val matchImpl = event.objects.head.asInstanceOf[Match] + for (player <- matchImpl.totalplayers) { + PodGameManager.addSession(SimpleSession(player.id, "")) + } + } else { + PodGameManager.transmitAll(event) + } + case event: ShowPlayerStatus => + PodGameManager.transmit(event.player.id, event) + case event: ShowRoundStatus => + PodGameManager.transmitAll(event) + case event: ShowErrorStatus => + PodGameManager.transmitAll(event) + case event: ShowCurrentTrickEvent => + PodGameManager.transmitAll(event) + case event: GameStateUpdateEvent => + if (internState != event.gameState) { + internState = event.gameState + if (event.gameState == MAIN_MENU) { + PodGameManager.clearSessions() + } + Some(true) + } + case _ => None + } + } + } + + override def initial: Boolean = { + if (init) { + return false + } + init = true + start() + true + } + +} diff --git a/knockoutwhistweb/app/controllers/sessions/PlayerSession.scala b/knockoutwhistweb/app/controllers/sessions/PlayerSession.scala new file mode 100644 index 0000000..a50de7f --- /dev/null +++ b/knockoutwhistweb/app/controllers/sessions/PlayerSession.scala @@ -0,0 +1,12 @@ +package controllers.sessions + +import de.knockoutwhist.utils.events.SimpleEvent + +import java.util.UUID + +trait PlayerSession { + + def id: UUID + def updatePlayer(event: SimpleEvent): Unit + +} diff --git a/knockoutwhistweb/app/controllers/WebUIMain.scala b/knockoutwhistweb/app/controllers/sessions/SimpleSession.scala similarity index 58% rename from knockoutwhistweb/app/controllers/WebUIMain.scala rename to knockoutwhistweb/app/controllers/sessions/SimpleSession.scala index b1f211f..ec00813 100644 --- a/knockoutwhistweb/app/controllers/WebUIMain.scala +++ b/knockoutwhistweb/app/controllers/sessions/SimpleSession.scala @@ -1,136 +1,56 @@ -package controllers +package controllers.sessions -import de.knockoutwhist.cards.{Card, CardValue, Hand, Suit} -import de.knockoutwhist.events.* +import de.knockoutwhist.cards.Card import de.knockoutwhist.events.ERROR_STATUS.* import de.knockoutwhist.events.GLOBAL_STATUS.* import de.knockoutwhist.events.PLAYER_STATUS.* -import de.knockoutwhist.events.ROUND_STATUS.{PLAYERS_OUT, SHOW_START_ROUND, WON_ROUND} +import de.knockoutwhist.events.ROUND_STATUS.* +import de.knockoutwhist.events.{ShowErrorStatus, ShowGlobalStatus, ShowPlayerStatus, ShowRoundStatus} import de.knockoutwhist.events.cards.{RenderHandEvent, ShowTieCardsEvent} import de.knockoutwhist.events.round.ShowCurrentTrickEvent -import de.knockoutwhist.events.ui.{GameState, GameStateUpdateEvent} import de.knockoutwhist.player.AbstractPlayer -import de.knockoutwhist.ui.UI -import de.knockoutwhist.utils.CustomThread -import de.knockoutwhist.utils.events.{EventListener, SimpleEvent} +import de.knockoutwhist.ui.tui.TUIMain.TUICards.{renderCardAsString, renderHandEvent} +import de.knockoutwhist.utils.events.SimpleEvent -object WebUIMain extends CustomThread with EventListener with UI { +import java.util.UUID - setName("WebUI") - - var init = false - private var internState: GameState = GameState.NO_SET - - var latestOutput: String = "" - - override def instance: CustomThread = WebUIMain - - override def listen(event: SimpleEvent): Unit = { - runLater { - event match { - case event: RenderHandEvent => - renderhandmethod(event) - case event: ShowTieCardsEvent => - showtiecardseventmethod(event) - case event: ShowGlobalStatus => - showglobalstatusmethod(event) - case event: ShowPlayerStatus => - showplayerstatusmethod(event) - case event: ShowRoundStatus => - showroundstatusmethod(event) - case event: ShowErrorStatus => - showerrstatmet(event) - case event: ShowCurrentTrickEvent => - showcurtrevmet(event) - case event: GameStateUpdateEvent => - if (internState != event.gameState) { - internState = event.gameState - if (event.gameState == GameState.MAIN_MENU) { - mainMenu() - } - Some(true) - } - case _ => None - } +case class SimpleSession(id: UUID, private var output: String) extends PlayerSession { + def get(): String = { + output + } + override def updatePlayer(event: SimpleEvent): Unit = { + event match { + case event: RenderHandEvent => + renderHand(event) + case event: ShowTieCardsEvent => + showtiecardseventmethod(event) + case event: ShowGlobalStatus => + showglobalstatusmethod(event) + case event: ShowPlayerStatus => + showplayerstatusmethod(event) + case event: ShowRoundStatus => + showroundstatusmethod(event) + case event: ShowErrorStatus => + showerrstatmet(event) + case event: ShowCurrentTrickEvent => + showcurtrevmet(event) } } - - object TUICards { - def renderCardAsString(card: Card): Vector[String] = { - val lines = "│ │" - if (card.cardValue == CardValue.Ten) { - return Vector( - s"┌─────────┐", - s"│${cardColour(card.suit)}${Console.BOLD}${card.cardValue.cardType()}${Console.RESET} │", - lines, - s"│ ${cardColour(card.suit)}${Console.BOLD}${card.suit.cardType()}${Console.RESET} │", - lines, - s"│ ${cardColour(card.suit)}${Console.BOLD}${card.cardValue.cardType()}${Console.RESET}│", - s"└─────────┘" - ) - } - Vector( - s"┌─────────┐", - s"│${cardColour(card.suit)}${Console.BOLD}${card.cardValue.cardType()}${Console.RESET} │", - lines, - s"│ ${cardColour(card.suit)}${Console.BOLD}${card.suit.cardType()}${Console.RESET} │", - lines, - s"│ ${cardColour(card.suit)}${Console.BOLD}${card.cardValue.cardType()}${Console.RESET}│", - s"└─────────┘" - ) - } - - private def cardColour(suit: Suit): String = suit match { - case Suit.Hearts | Suit.Diamonds => Console.RED - case Suit.Clubs | Suit.Spades => Console.BLACK - } - - def renderHandEvent(hand: Hand, showNumbers: Boolean): Vector[String] = { - val cardStrings = hand.cards.map(TUICards.renderCardAsString) - var zipped = cardStrings.transpose - if (showNumbers) zipped = { - List.tabulate(hand.cards.length) { i => - s" ${i + 1} " - } - } :: zipped - zipped.map(_.mkString(" ")).toVector - } - } - private object TUIUtil { - def clearConsole() = { - latestOutput = "" - } + private def clear(): Unit = { + output = "" } - override def initial: Boolean = { - if (init) { - return false - } - init = true - start() - true + private def renderHand(event: RenderHandEvent): Unit = { + renderHandEvent(event.hand, event.showNumbers).foreach(addToOutput) } - private def mainMenu(): Unit = { - TUIUtil.clearConsole() - println("Welcome to Knockout Whist!") - println() - println("Please select an option:") - println("1. Start a new match") - println("2. Exit") - } - - private def renderhandmethod(event: RenderHandEvent): Option[Boolean] = { - TUICards.renderHandEvent(event.hand, event.showNumbers).foreach(println) - Some(true) - } private def showtiecardseventmethod(event: ShowTieCardsEvent): Option[Boolean] = { val a: Array[String] = Array("", "", "", "", "", "", "", "") for ((player, card) <- event.card) { val playerNameLength = player.name.length a(0) += " " + player.name + ":" + (" " * (playerNameLength - 1)) - val rendered = TUICards.renderCardAsString(card) + val rendered = renderCardAsString(card) a(1) += " " + rendered(0) a(2) += " " + rendered(1) a(3) += " " + rendered(2) @@ -139,9 +59,10 @@ object WebUIMain extends CustomThread with EventListener with UI { a(6) += " " + rendered(5) a(7) += " " + rendered(6) } - a.foreach(println) + a.foreach(addToOutput) Some(true) } + private def showglobalstatusmethod(event: ShowGlobalStatus): Option[Boolean] = { event.status match { case SHOW_TIE => @@ -158,9 +79,9 @@ object WebUIMain extends CustomThread with EventListener with UI { println("It's a tie again! Let's cut again.") Some(true) case SHOW_START_MATCH => - TUIUtil.clearConsole() + clear() println("Starting a new match...") - latestOutput += "\n\n" + output += "\n\n" Some(true) case SHOW_TYPE_PLAYERS => println("Please enter the names of the players, separated by a comma.") @@ -169,18 +90,16 @@ object WebUIMain extends CustomThread with EventListener with UI { if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) { None } else { - TUIUtil.clearConsole() + clear() println(s"The match is over. The winner is ${event.objects.head.asInstanceOf[AbstractPlayer]}") Some(true) } } } + private def showplayerstatusmethod(event: ShowPlayerStatus): Option[Boolean] = { val player = event.player event.status match { - case SHOW_TURN => - println("It's your turn, " + player.name + ".") - Some(true) case SHOW_PLAY_CARD => println("Which card do you want to play?") Some(true) @@ -218,16 +137,24 @@ object WebUIMain extends CustomThread with EventListener with UI { Some(true) case SHOW_WON_PLAYER_TRICK => println(s"${event.player.name} won the trick.") - latestOutput = "\n\n" + output = "\n\n" Some(true) } } + private def showroundstatusmethod(event: ShowRoundStatus): Option[Boolean] = { event.status match { + case SHOW_TURN => + if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) { + None + } else { + println(s"It's ${event.objects.head.asInstanceOf[AbstractPlayer].name} turn.") + Some(true) + } case SHOW_START_ROUND => - TUIUtil.clearConsole() + clear() println(s"Starting a new round. The trump suit is ${event.currentRound.trumpSuit}.") - latestOutput = "\n\n" + output = "\n\n" Some(true) case WON_ROUND => if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) { @@ -244,6 +171,7 @@ object WebUIMain extends CustomThread with EventListener with UI { Some(true) } } + private def showerrstatmet(event: ShowErrorStatus): Option[Boolean] = { event.status match { case INVALID_NUMBER => @@ -275,7 +203,7 @@ object WebUIMain extends CustomThread with EventListener with UI { } private def showcurtrevmet(event: ShowCurrentTrickEvent): Option[Boolean] = { - TUIUtil.clearConsole() + clear() val sb = new StringBuilder() sb.append("Current Trick:\n") sb.append("Trump-Suit: " + event.round.trumpSuit + "\n") @@ -289,16 +217,18 @@ object WebUIMain extends CustomThread with EventListener with UI { Some(true) } + private def addToOutput(str: String): Unit = { + output += str + "\n" + } + private def println(s: String): Unit = { - latestOutput += s + "\n" - System.out.println(s) + output += s + "\n" } private def println(): Unit = { - latestOutput += "\n" - System.out.println() + output += "\n" } - + } diff --git a/knockoutwhistweb/conf/routes b/knockoutwhistweb/conf/routes index d8d4816..fe99cc4 100644 --- a/knockoutwhistweb/conf/routes +++ b/knockoutwhistweb/conf/routes @@ -4,7 +4,9 @@ # ~~~~ # An example controller showing a sample home page + GET / controllers.HomeController.index() -GET /ingame controllers.HomeController.ingame() +GET /sessions controllers.HomeController.sessions() +GET /ingame/:id controllers.HomeController.ingame(id: String) # Map static resources from the /public folder to the /assets URL path GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)