Refactor match handling and session management; add player session functionality and update event handling in WebUI

This commit is contained in:
2025-10-13 15:16:41 +02:00
parent 7cbb6e6ab7
commit c77eeff123
8 changed files with 209 additions and 142 deletions

View File

@@ -1,13 +1,13 @@
package components package components
import controllers.WebUIMain import controllers.WebUI
import de.knockoutwhist.components.DefaultConfiguration import de.knockoutwhist.components.DefaultConfiguration
import de.knockoutwhist.ui.UI import de.knockoutwhist.ui.UI
import de.knockoutwhist.utils.events.EventListener import de.knockoutwhist.utils.events.EventListener
class WebApplicationConfiguration extends DefaultConfiguration { class WebApplicationConfiguration extends DefaultConfiguration {
override def uis: Set[UI] = super.uis + WebUIMain override def uis: Set[UI] = super.uis + WebUI
override def listener: Set[EventListener] = super.listener + WebUIMain override def listener: Set[EventListener] = super.listener + WebUI
} }

View File

@@ -1,8 +0,0 @@
package controllers
object GameManager {
}

View File

@@ -1,5 +1,6 @@
package controllers package controllers
import controllers.sessions.SimpleSession
import com.google.inject.{Guice, Injector} import com.google.inject.{Guice, Injector}
import de.knockoutwhist.KnockOutWhist import de.knockoutwhist.KnockOutWhist
import de.knockoutwhist.components.Configuration import de.knockoutwhist.components.Configuration
@@ -7,8 +8,10 @@ import di.KnockOutWebConfigurationModule
import play.api.* import play.api.*
import play.api.mvc.* import play.api.mvc.*
import java.util.UUID
import javax.inject.* import javax.inject.*
/** /**
* This controller creates an `Action` to handle HTTP requests to the * This controller creates an `Action` to handle HTTP requests to the
* application's home page. * 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 => 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()))
}
} }
} }

View File

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

View File

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

View File

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

View File

@@ -1,136 +1,56 @@
package controllers package controllers.sessions
import de.knockoutwhist.cards.{Card, CardValue, Hand, Suit} import de.knockoutwhist.cards.Card
import de.knockoutwhist.events.*
import de.knockoutwhist.events.ERROR_STATUS.* import de.knockoutwhist.events.ERROR_STATUS.*
import de.knockoutwhist.events.GLOBAL_STATUS.* import de.knockoutwhist.events.GLOBAL_STATUS.*
import de.knockoutwhist.events.PLAYER_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.cards.{RenderHandEvent, ShowTieCardsEvent}
import de.knockoutwhist.events.round.ShowCurrentTrickEvent import de.knockoutwhist.events.round.ShowCurrentTrickEvent
import de.knockoutwhist.events.ui.{GameState, GameStateUpdateEvent}
import de.knockoutwhist.player.AbstractPlayer import de.knockoutwhist.player.AbstractPlayer
import de.knockoutwhist.ui.UI import de.knockoutwhist.ui.tui.TUIMain.TUICards.{renderCardAsString, renderHandEvent}
import de.knockoutwhist.utils.CustomThread import de.knockoutwhist.utils.events.SimpleEvent
import de.knockoutwhist.utils.events.{EventListener, SimpleEvent}
object WebUIMain extends CustomThread with EventListener with UI { import java.util.UUID
setName("WebUI") case class SimpleSession(id: UUID, private var output: String) extends PlayerSession {
def get(): String = {
var init = false output
private var internState: GameState = GameState.NO_SET }
override def updatePlayer(event: SimpleEvent): Unit = {
var latestOutput: String = "" event match {
case event: RenderHandEvent =>
override def instance: CustomThread = WebUIMain renderHand(event)
case event: ShowTieCardsEvent =>
override def listen(event: SimpleEvent): Unit = { showtiecardseventmethod(event)
runLater { case event: ShowGlobalStatus =>
event match { showglobalstatusmethod(event)
case event: RenderHandEvent => case event: ShowPlayerStatus =>
renderhandmethod(event) showplayerstatusmethod(event)
case event: ShowTieCardsEvent => case event: ShowRoundStatus =>
showtiecardseventmethod(event) showroundstatusmethod(event)
case event: ShowGlobalStatus => case event: ShowErrorStatus =>
showglobalstatusmethod(event) showerrstatmet(event)
case event: ShowPlayerStatus => case event: ShowCurrentTrickEvent =>
showplayerstatusmethod(event) showcurtrevmet(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
}
} }
} }
private def clear(): Unit = {
object TUICards { output = ""
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 = ""
}
} }
override def initial: Boolean = { private def renderHand(event: RenderHandEvent): Unit = {
if (init) { renderHandEvent(event.hand, event.showNumbers).foreach(addToOutput)
return false
}
init = true
start()
true
} }
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] = { private def showtiecardseventmethod(event: ShowTieCardsEvent): Option[Boolean] = {
val a: Array[String] = Array("", "", "", "", "", "", "", "") val a: Array[String] = Array("", "", "", "", "", "", "", "")
for ((player, card) <- event.card) { for ((player, card) <- event.card) {
val playerNameLength = player.name.length val playerNameLength = player.name.length
a(0) += " " + player.name + ":" + (" " * (playerNameLength - 1)) a(0) += " " + player.name + ":" + (" " * (playerNameLength - 1))
val rendered = TUICards.renderCardAsString(card) val rendered = renderCardAsString(card)
a(1) += " " + rendered(0) a(1) += " " + rendered(0)
a(2) += " " + rendered(1) a(2) += " " + rendered(1)
a(3) += " " + rendered(2) a(3) += " " + rendered(2)
@@ -139,9 +59,10 @@ object WebUIMain extends CustomThread with EventListener with UI {
a(6) += " " + rendered(5) a(6) += " " + rendered(5)
a(7) += " " + rendered(6) a(7) += " " + rendered(6)
} }
a.foreach(println) a.foreach(addToOutput)
Some(true) Some(true)
} }
private def showglobalstatusmethod(event: ShowGlobalStatus): Option[Boolean] = { private def showglobalstatusmethod(event: ShowGlobalStatus): Option[Boolean] = {
event.status match { event.status match {
case SHOW_TIE => 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.") println("It's a tie again! Let's cut again.")
Some(true) Some(true)
case SHOW_START_MATCH => case SHOW_START_MATCH =>
TUIUtil.clearConsole() clear()
println("Starting a new match...") println("Starting a new match...")
latestOutput += "\n\n" output += "\n\n"
Some(true) Some(true)
case SHOW_TYPE_PLAYERS => case SHOW_TYPE_PLAYERS =>
println("Please enter the names of the players, separated by a comma.") 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]) { if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) {
None None
} else { } else {
TUIUtil.clearConsole() clear()
println(s"The match is over. The winner is ${event.objects.head.asInstanceOf[AbstractPlayer]}") println(s"The match is over. The winner is ${event.objects.head.asInstanceOf[AbstractPlayer]}")
Some(true) Some(true)
} }
} }
} }
private def showplayerstatusmethod(event: ShowPlayerStatus): Option[Boolean] = { private def showplayerstatusmethod(event: ShowPlayerStatus): Option[Boolean] = {
val player = event.player val player = event.player
event.status match { event.status match {
case SHOW_TURN =>
println("It's your turn, " + player.name + ".")
Some(true)
case SHOW_PLAY_CARD => case SHOW_PLAY_CARD =>
println("Which card do you want to play?") println("Which card do you want to play?")
Some(true) Some(true)
@@ -218,16 +137,24 @@ object WebUIMain extends CustomThread with EventListener with UI {
Some(true) Some(true)
case SHOW_WON_PLAYER_TRICK => case SHOW_WON_PLAYER_TRICK =>
println(s"${event.player.name} won the trick.") println(s"${event.player.name} won the trick.")
latestOutput = "\n\n" output = "\n\n"
Some(true) Some(true)
} }
} }
private def showroundstatusmethod(event: ShowRoundStatus): Option[Boolean] = { private def showroundstatusmethod(event: ShowRoundStatus): Option[Boolean] = {
event.status match { 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 => case SHOW_START_ROUND =>
TUIUtil.clearConsole() clear()
println(s"Starting a new round. The trump suit is ${event.currentRound.trumpSuit}.") println(s"Starting a new round. The trump suit is ${event.currentRound.trumpSuit}.")
latestOutput = "\n\n" output = "\n\n"
Some(true) Some(true)
case WON_ROUND => case WON_ROUND =>
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) { if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) {
@@ -244,6 +171,7 @@ object WebUIMain extends CustomThread with EventListener with UI {
Some(true) Some(true)
} }
} }
private def showerrstatmet(event: ShowErrorStatus): Option[Boolean] = { private def showerrstatmet(event: ShowErrorStatus): Option[Boolean] = {
event.status match { event.status match {
case INVALID_NUMBER => case INVALID_NUMBER =>
@@ -275,7 +203,7 @@ object WebUIMain extends CustomThread with EventListener with UI {
} }
private def showcurtrevmet(event: ShowCurrentTrickEvent): Option[Boolean] = { private def showcurtrevmet(event: ShowCurrentTrickEvent): Option[Boolean] = {
TUIUtil.clearConsole() clear()
val sb = new StringBuilder() val sb = new StringBuilder()
sb.append("Current Trick:\n") sb.append("Current Trick:\n")
sb.append("Trump-Suit: " + event.round.trumpSuit + "\n") sb.append("Trump-Suit: " + event.round.trumpSuit + "\n")
@@ -289,14 +217,16 @@ object WebUIMain extends CustomThread with EventListener with UI {
Some(true) Some(true)
} }
private def addToOutput(str: String): Unit = {
output += str + "\n"
}
private def println(s: String): Unit = { private def println(s: String): Unit = {
latestOutput += s + "\n" output += s + "\n"
System.out.println(s)
} }
private def println(): Unit = { private def println(): Unit = {
latestOutput += "\n" output += "\n"
System.out.println()
} }

View File

@@ -4,7 +4,9 @@
# ~~~~ # ~~~~
# An example controller showing a sample home page # An example controller showing a sample home page
GET / controllers.HomeController.index() 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 # Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)