Added WebUI observer, added route to see latestOutput. For some reason the WebUI Observer doesn't execute it's methods for writing stuff into latestOutput. This has something to do with how the threads work

This commit is contained in:
LQ63
2025-10-12 15:05:40 +02:00
parent 30fb50c3b8
commit 9098b7c4e3
6 changed files with 546 additions and 20 deletions

View File

@@ -1,9 +1,11 @@
package controllers
import javax.inject._
import play.api._
import play.api.mvc._
import javax.inject.*
import play.api.*
import play.api.mvc.*
import de.knockoutwhist.KnockOutWhist
import de.knockoutwhist.control.ControlHandler
import de.knockoutwhist.ui.tui.TUIMain
/**
* This controller creates an `Action` to handle HTTP requests to the
@@ -24,18 +26,21 @@ class HomeController @Inject()(val controllerComponents: ControllerComponents) e
def index(): Action[AnyContent] = {
if (!initial) {
initial = true
ControlHandler.addListener(WebUI)
KnockOutWhist.main(new Array[String](_length = 0))
}
Action { implicit request: Request[AnyContent] => {
Ok(views.html.main.apply("KnockoutWhist")(views.html.))
}
Action { implicit request =>
Ok(views.html.index.apply())
}
}
def ingame(): Action[AnyContent] = {
Action { implicit request: Request[AnyContent] => {
Ok(views.html.tui.apply())
Action { implicit request =>
Ok(views.html.tui.apply(WebUI.latestOutput))
}
}
def showTUI(): Action[AnyContent] = Action { implicit request =>
Ok(views.html.tui.render(WebUI.latestOutput))
}
}

View File

@@ -1,12 +1,521 @@
package controllers
import de.knockoutwhist.events.directional.RequestPickTrumpsuitEvent
import de.knockoutwhist.events.round.ShowCurrentTrickEvent
import de.knockoutwhist.ui.UI
import de.knockoutwhist.ui.tui.TUIMain.{init, runLater, start}
import de.knockoutwhist.utils.events.{EventListener, SimpleEvent}
import de.knockoutwhist.KnockOutWhist
import de.knockoutwhist.cards.{Card, CardValue, Hand, Suit}
import de.knockoutwhist.control.controllerBaseImpl.{PlayerLogic, TrickLogic}
import de.knockoutwhist.control.{ControlHandler, ControlThread}
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.directional.*
import de.knockoutwhist.events.round.ShowCurrentTrickEvent
import de.knockoutwhist.events.ui.GameState.MAIN_MENU
import de.knockoutwhist.events.ui.{GameState, GameStateUpdateEvent}
import de.knockoutwhist.events.util.DelayEvent
import de.knockoutwhist.player.Playertype.HUMAN
import de.knockoutwhist.player.{AbstractPlayer, PlayerFactory}
import de.knockoutwhist.ui.UI
import de.knockoutwhist.undo.{UndoManager, UndoneException}
import de.knockoutwhist.utils.CustomThread
import de.knockoutwhist.utils.events.{EventListener, SimpleEvent}
object WebUI extends UI {
override def initial: Boolean = true
import java.io.{BufferedReader, InputStreamReader}
import java.util.concurrent.atomic.AtomicBoolean
import scala.annotation.tailrec
import scala.util.{Failure, Success, Try}
object WebUI extends CustomThread with EventListener with UI {
override def initial: Boolean = {
if (init) {
return false
}
init = true
start()
true
}
setName("WebUI")
override def instance: CustomThread = WebUI
var init = false
override def runLater[R](op: => R): Unit = {
interrupted.set(true)
super.runLater(op)
}
var latestOutput: String = ""
private var internState: GameState = GameState.NO_SET
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: RequestTieNumberEvent =>
reqnumbereventmet(event)
case event: RequestCardEvent =>
reqcardeventmet(event)
case event: RequestDogPlayCardEvent =>
reqdogeventmet(event)
case event: RequestPickTrumpsuitEvent =>
reqpicktevmet(event)
case event: ShowCurrentTrickEvent =>
showcurtrevmet(event)
case event: GameStateUpdateEvent =>
if (internState != event.gameState) {
internState = event.gameState
if (event.gameState == GameState.MAIN_MENU) {
mainMenu()
} else if (event.gameState == GameState.PLAYERS) {
reqplayersevent()
}
Some(true)
}
case _ => None
}
}
}
object WebUICards {
def renderCardAsString(card: Card): Vector[String] = {
val lines = "│ │"
if (card.cardValue == CardValue.Ten) {
latestOutput += 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"└─────────┘"
)
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"└─────────┘"
)
}
latestOutput += 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"└─────────┘"
)
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"└─────────┘"
)
}
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(WebUICards.renderCardAsString)
var zipped = cardStrings.transpose
if (showNumbers) zipped = {
List.tabulate(hand.cards.length) { i =>
s" ${i + 1} "
}
} :: zipped
latestOutput += zipped.map(_.mkString(" ")).toVector
zipped.map(_.mkString(" ")).toVector
}
}
//override def initial: Boolean = {
//if (init) {
//return false
//}
//init = true
//start()
//true
//}
@tailrec
private def mainMenu(): Unit = {
latestOutput = ""
latestOutput += "Welcome to Knockout Whist\n"
latestOutput += "Please select an option:\n"
latestOutput += "1. Start a new match\n"
latestOutput += "2. Exit\n"
Try {
input().toInt
} match {
case Success(value) =>
value match {
case 1 =>
ControlThread.runLater {
KnockOutWhist.config.maincomponent.startMatch()
}
case 2 =>
println("Exiting the game.")
System.exit(0)
case _ =>
showerrstatmet(ShowErrorStatus(INVALID_NUMBER))
ControlThread.runLater {
ControlHandler.invoke(DelayEvent(500))
ControlHandler.invoke(GameStateUpdateEvent(MAIN_MENU))
}
mainMenu()
}
case Failure(exception) =>
exception match {
case undo: UndoneException =>
case _ =>
showerrstatmet(ShowErrorStatus(NOT_A_NUMBER))
ControlThread.runLater {
ControlHandler.invoke(DelayEvent(500))
ControlHandler.invoke(GameStateUpdateEvent(MAIN_MENU))
}
}
}
}
private def renderhandmethod(event: RenderHandEvent): Option[Boolean] = {
WebUICards.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 = WebUICards.renderCardAsString(card)
a(1) += " " + rendered(0)
a(2) += " " + rendered(1)
a(3) += " " + rendered(2)
a(4) += " " + rendered(3)
a(5) += " " + rendered(4)
a(6) += " " + rendered(5)
a(7) += " " + rendered(6)
}
a.foreach(println)
Some(true)
}
private def showglobalstatusmethod(event: ShowGlobalStatus): Option[Boolean] = {
event.status match {
case SHOW_TIE =>
println("It's a tie! Let's cut to determine the winner.")
Some(true)
case SHOW_TIE_WINNER =>
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) {
None
} else {
println(s"${event.objects.head.asInstanceOf[AbstractPlayer].name} wins the cut!")
Some(true)
}
case SHOW_TIE_TIE =>
println("It's a tie again! Let's cut again.")
Some(true)
case SHOW_START_MATCH =>
latestOutput = ""
println("Starting a new match...")
wait(1000)
latestOutput = ""
Some(true)
case SHOW_TYPE_PLAYERS =>
latestOutput += "Please enter the names of the players, separated by a comma."
Some(true)
case SHOW_FINISHED_MATCH =>
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) {
None
} else {
latestOutput = ""
latestOutput += 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)
case SHOW_DOG_PLAY_CARD =>
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[Boolean]) {
None
} else {
println("You are using your dog life. Do you want to play your final card now?")
if (event.objects.head.asInstanceOf[Boolean]) {
println("You have to play your final card this round!")
println("Please enter y to play your final card.")
Some(true)
} else {
println("Please enter y/n to play your final card.")
Some(true)
}
}
case SHOW_TIE_NUMBERS =>
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[Int]) {
None
} else {
println(s"${player.name} enter a number between 1 and ${event.objects.head.asInstanceOf[Int]}.")
Some(true)
}
case SHOW_TRUMPSUIT_OPTIONS =>
println("Which suit do you want to pick as the next trump suit?")
println("1: Hearts")
println("2: Diamonds")
println("3: Clubs")
println("4: Spades")
println()
Some(true)
case SHOW_NOT_PLAYED =>
println(s"Player ${event.player} decided to not play his card")
Some(true)
case SHOW_WON_PLAYER_TRICK =>
println(s"${event.player.name} won the trick.")
wait(2000)
latestOutput = ""
Some(true)
}
}
private def showroundstatusmethod(event: ShowRoundStatus): Option[Boolean] = {
event.status match {
case SHOW_START_ROUND =>
latestOutput = ""
println(s"Starting a new round. The trump suit is ${event.currentRound.trumpSuit}.")
wait(2000)
latestOutput = ""
Some(true)
case WON_ROUND =>
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[AbstractPlayer]) {
None
} else {
println(s"${event.objects.head.asInstanceOf[AbstractPlayer].name} won the round.")
Some(true)
}
case PLAYERS_OUT =>
println("The following players are out of the game:")
event.currentRound.playersout.foreach(p => {
println(p.name)
})
Some(true)
}
}
private def showerrstatmet(event: ShowErrorStatus): Option[Boolean] = {
event.status match {
case INVALID_NUMBER =>
println("Please enter a valid number.")
Some(true)
case NOT_A_NUMBER =>
println("Please enter a number.")
Some(true)
case INVALID_INPUT =>
latestOutput += "Please enter a valid input"
Some(true)
case INVALID_NUMBER_OF_PLAYERS =>
latestOutput += "Please enter at least two names."
Some(true)
case IDENTICAL_NAMES =>
latestOutput += "Please enter unique names."
Some(true)
case INVALID_NAME_FORMAT =>
latestOutput += "Please enter valid names. Those can not be empty, shorter than 2 or longer then 10 characters."
Some(true)
case WRONG_CARD =>
if (event.objects.length != 1 || !event.objects.head.isInstanceOf[Card]) {
None
} else {
latestOutput += f"You have to play a card of suit: ${event.objects.head.asInstanceOf[Card].suit}\n"
Some(true)
}
}
}
private def reqnumbereventmet(event: RequestTieNumberEvent): Option[Boolean] = {
val tryTie = Try {
val number = input().toInt
if (number < 1 || number > event.remaining) {
throw new IllegalArgumentException(s"Number must be between 1 and ${event.remaining}")
}
number
}
if (tryTie.isFailure && tryTie.failed.get.isInstanceOf[UndoneException]) {
return Some(true)
}
ControlThread.runLater {
KnockOutWhist.config.playerlogcomponent.selectedTie(event.winner, event.matchImpl, event.round, event.playersout, event.cut, tryTie, event.currentStep, event.remaining, event.currentIndex)
}
Some(true)
}
private def reqcardeventmet(event: RequestCardEvent): Option[Boolean] = {
val tryCard = Try {
val card = input().toInt - 1
if (card < 0 || card >= event.hand.cards.length) {
throw new IllegalArgumentException(s"Number has to be between 1 and ${event.hand.cards.length}")
} else {
event.hand.cards(card)
}
}
if (tryCard.isFailure && tryCard.failed.get.isInstanceOf[UndoneException]) {
return Some(true)
}
ControlThread.runLater {
KnockOutWhist.config.trickcomponent.controlSuitplayed(tryCard, event.matchImpl, event.round, event.trick, event.currentIndex, event.player)
}
Some(true)
}
private def reqdogeventmet(event: RequestDogPlayCardEvent): Option[Boolean] = {
val tryDogCard = Try {
val card = input()
if (card.equalsIgnoreCase("y")) {
Some(event.hand.cards.head)
} else if (card.equalsIgnoreCase("n") && !event.needstoplay) {
None
} else {
throw new IllegalArgumentException("Didn't want to play card but had to")
}
}
if (tryDogCard.isFailure && tryDogCard.failed.get.isInstanceOf[UndoneException]) {
return Some(true)
}
ControlThread.runLater {
KnockOutWhist.config.trickcomponent.controlDogPlayed(tryDogCard, event.matchImpl, event.round, event.trick, event.currentIndex, event.player)
}
Some(true)
}
private def reqplayersevent(): Option[Boolean] = {
showglobalstatusmethod(ShowGlobalStatus(SHOW_TYPE_PLAYERS))
val names = Try {
input().split(",")
}
if (names.isFailure && names.failed.get.isInstanceOf[UndoneException]) {
return Some(true)
}
if (names.get.length < 2) {
showerrstatmet(ShowErrorStatus(INVALID_NUMBER_OF_PLAYERS))
return reqplayersevent()
}
if (names.get.distinct.length != names.get.length) {
showerrstatmet(ShowErrorStatus(IDENTICAL_NAMES))
return reqplayersevent()
}
if (names.get.count(_.trim.isBlank) > 0
|| names.get.count(_.trim.length <= 2) > 0
|| names.get.count(_.trim.length > 10) > 0) {
showerrstatmet(ShowErrorStatus(INVALID_NAME_FORMAT))
return reqplayersevent()
}
ControlThread.runLater {
KnockOutWhist.config
.maincomponent
.enteredPlayers(names.get
.map(s => PlayerFactory.createPlayer(s, playertype = HUMAN))
.toList)
}
Some(true)
}
private def reqpicktevmet(event: RequestPickTrumpsuitEvent): Option[Boolean] = {
val trySuit = Try {
val suit = input().toInt
suit match {
case 1 => Suit.Hearts
case 2 => Suit.Diamonds
case 3 => Suit.Clubs
case 4 => Suit.Spades
case _ => throw IllegalArgumentException("Didn't enter a number between 1 and 4")
}
}
if (trySuit.isFailure && trySuit.failed.get.isInstanceOf[UndoneException]) {
return Some(true)
}
ControlThread.runLater {
KnockOutWhist.config.playerlogcomponent.trumpSuitSelected(event.matchImpl, trySuit, event.remaining_players, event.firstRound, event.player)
}
Some(true)
}
private def showcurtrevmet(event: ShowCurrentTrickEvent): Option[Boolean] = {
latestOutput = ""
val sb = new StringBuilder()
sb.append("Current Trick:\n")
sb.append("Trump-Suit: " + event.round.trumpSuit + "\n")
if (event.trick.firstCard.isDefined) {
sb.append(s"Suit to play: ${event.trick.firstCard.get.suit}\n")
}
for ((card, player) <- event.trick.cards) {
sb.append(s"${player.name} played ${card.toString}\n")
}
latestOutput += sb.toString()
//println(sb.toString())
Some(true)
}
private val isInIO: AtomicBoolean = new AtomicBoolean(false)
private val interrupted: AtomicBoolean = new AtomicBoolean(false)
private def input(): String = {
interrupted.set(false)
val reader = new BufferedReader(new InputStreamReader(System.in))
while (!interrupted.get()) {
if (reader.ready()) {
val in = reader.readLine()
if (in.equals("undo")) {
UndoManager.undoStep()
throw new UndoneException("Undo")
} else if (in.equals("redo")) {
UndoManager.redoStep()
throw new UndoneException("Redo")
} else if (in.equals("load")
&& KnockOutWhist.config.persistenceManager.canLoadfile("currentSnapshot")) {
KnockOutWhist.config.persistenceManager.loadFile("currentSnapshot.json")
throw new UndoneException("Load")
} else if (in.equals("save")) {
KnockOutWhist.config.persistenceManager.saveFile("currentSnapshot.json")
}
return in
}
Thread.sleep(50)
}
throw new UndoneException("Skipped")
}
}

View File

@@ -1,5 +1,3 @@
@(output: String)
@main("Welcome to Play") {
<h1>Welcome to Play!</h1>
}

View File

@@ -0,0 +1,10 @@
@(title: String)(content: play.twirl.api.Html)
<!DOCTYPE html>
<html>
<head>
<title>@title</title>
</head>
<body>
@content
</body>
</html>

View File

@@ -1 +1,5 @@
@()
@(renderTUI: String)
@main("Welcome to Play") {
<h1>@renderTUI</h1>
}

View File

@@ -6,6 +6,6 @@
# An example controller showing a sample home page
GET / controllers.HomeController.index()
GET /ingame controllers.HomeController.ingame()
GET /showTUI controllers.HomeController.showTUI()
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)