From 9098b7c4e3193a8abed976bae42932f24dabdf24 Mon Sep 17 00:00:00 2001 From: LQ63 Date: Sun, 12 Oct 2025 15:05:40 +0200 Subject: [PATCH] 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 --- .../app/controllers/HomeController.scala | 25 +- knockoutwhistweb/app/controllers/WebUI.scala | 521 +++++++++++++++++- knockoutwhistweb/app/views/index.scala.html | 2 - knockoutwhistweb/app/views/main.scala.html | 10 + knockoutwhistweb/app/views/tui.scala.html | 6 +- knockoutwhistweb/conf/routes | 2 +- 6 files changed, 546 insertions(+), 20 deletions(-) create mode 100644 knockoutwhistweb/app/views/main.scala.html diff --git a/knockoutwhistweb/app/controllers/HomeController.scala b/knockoutwhistweb/app/controllers/HomeController.scala index 3fa3357..64be26e 100644 --- a/knockoutwhistweb/app/controllers/HomeController.scala +++ b/knockoutwhistweb/app/controllers/HomeController.scala @@ -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)) + } } \ No newline at end of file diff --git a/knockoutwhistweb/app/controllers/WebUI.scala b/knockoutwhistweb/app/controllers/WebUI.scala index 9103001..9286742 100644 --- a/knockoutwhistweb/app/controllers/WebUI.scala +++ b/knockoutwhistweb/app/controllers/WebUI.scala @@ -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 { +import java.io.{BufferedReader, InputStreamReader} +import java.util.concurrent.atomic.AtomicBoolean +import scala.annotation.tailrec +import scala.util.{Failure, Success, Try} - override def initial: Boolean = true - - - - +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") + } } + diff --git a/knockoutwhistweb/app/views/index.scala.html b/knockoutwhistweb/app/views/index.scala.html index 1f2e671..d4fdd74 100644 --- a/knockoutwhistweb/app/views/index.scala.html +++ b/knockoutwhistweb/app/views/index.scala.html @@ -1,5 +1,3 @@ -@(output: String) - @main("Welcome to Play") {

Welcome to Play!

} diff --git a/knockoutwhistweb/app/views/main.scala.html b/knockoutwhistweb/app/views/main.scala.html new file mode 100644 index 0000000..a4607a8 --- /dev/null +++ b/knockoutwhistweb/app/views/main.scala.html @@ -0,0 +1,10 @@ +@(title: String)(content: play.twirl.api.Html) + + + + @title + + + @content + + \ No newline at end of file diff --git a/knockoutwhistweb/app/views/tui.scala.html b/knockoutwhistweb/app/views/tui.scala.html index bcca00b..60af01d 100644 --- a/knockoutwhistweb/app/views/tui.scala.html +++ b/knockoutwhistweb/app/views/tui.scala.html @@ -1 +1,5 @@ -@() \ No newline at end of file +@(renderTUI: String) + +@main("Welcome to Play") { +

@renderTUI

+} diff --git a/knockoutwhistweb/conf/routes b/knockoutwhistweb/conf/routes index be56278..90c173b 100644 --- a/knockoutwhistweb/conf/routes +++ b/knockoutwhistweb/conf/routes @@ -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)