From 52e5033afca344ae40a644196555a9655913710a Mon Sep 17 00:00:00 2001 From: Janis Date: Mon, 24 Nov 2025 14:31:31 +0100 Subject: [PATCH] feat(api): Implement received hand event handling and UI updates (#83) #76 Reviewed-on: https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/pulls/83 --- .../app/model/sessions/UserSession.scala | 1 + .../model/sessions/UserWebsocketActor.scala | 9 ++-- knockoutwhistweb/app/util/WebUIUtils.scala | 21 +++++++++- .../app/util/WebsocketEventMapper.scala | 28 ++++++++++++- .../util/mapper/ReceivedHandEventMapper.scala | 17 ++++++++ .../app/util/mapper/SimpleEventMapper.scala | 12 ++++++ knockoutwhistweb/app/views/main.scala.html | 2 + knockoutwhistweb/public/javascripts/events.js | 42 +++++++++++++++++++ .../public/javascripts/interact.js | 7 ++++ .../public/javascripts/websocket.js | 18 ++++---- 10 files changed, 143 insertions(+), 14 deletions(-) create mode 100644 knockoutwhistweb/app/util/mapper/ReceivedHandEventMapper.scala create mode 100644 knockoutwhistweb/app/util/mapper/SimpleEventMapper.scala create mode 100644 knockoutwhistweb/public/javascripts/events.js create mode 100644 knockoutwhistweb/public/javascripts/interact.js diff --git a/knockoutwhistweb/app/model/sessions/UserSession.scala b/knockoutwhistweb/app/model/sessions/UserSession.scala index ed249c9..c98e384 100644 --- a/knockoutwhistweb/app/model/sessions/UserSession.scala +++ b/knockoutwhistweb/app/model/sessions/UserSession.scala @@ -26,6 +26,7 @@ class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) e else canInteract = Some(InteractionType.Card) case _ => } + websocketActor.foreach(_.transmitEventToClient(event)) } override def id: UUID = user.id diff --git a/knockoutwhistweb/app/model/sessions/UserWebsocketActor.scala b/knockoutwhistweb/app/model/sessions/UserWebsocketActor.scala index fba8100..d7f80f6 100644 --- a/knockoutwhistweb/app/model/sessions/UserWebsocketActor.scala +++ b/knockoutwhistweb/app/model/sessions/UserWebsocketActor.scala @@ -61,6 +61,10 @@ class UserWebsocketActor( )) return } + val statusOpt = (json \ "status").asOpt[String] + if (statusOpt.isDefined) { + return + } val event = eventOpt.get val data = (json \ "data").asOpt[JsObject].getOrElse(Json.obj()) val result = Try { @@ -83,12 +87,11 @@ class UserWebsocketActor( } def transmitJsonToClient(jsonObj: JsObject): Unit = { - out ! jsonObj.toString() + transmitTextToClient(jsonObj.toString()) } def transmitEventToClient(event: SimpleEvent): Unit = { - val jsonString = WebsocketEventMapper.toJsonString(event) - out ! jsonString + transmitJsonToClient(WebsocketEventMapper.toJson(event)) } } diff --git a/knockoutwhistweb/app/util/WebUIUtils.scala b/knockoutwhistweb/app/util/WebUIUtils.scala index f797985..625303d 100644 --- a/knockoutwhistweb/app/util/WebUIUtils.scala +++ b/knockoutwhistweb/app/util/WebUIUtils.scala @@ -1,8 +1,9 @@ package util -import de.knockoutwhist.cards.Card +import de.knockoutwhist.cards.{Card, Hand} import de.knockoutwhist.cards.CardValue.* import de.knockoutwhist.cards.Suit.{Clubs, Diamonds, Hearts, Spades} +import play.api.libs.json.{JsArray, Json} import play.twirl.api.Html import scalafx.scene.image.Image @@ -36,4 +37,22 @@ object WebUIUtils { f"$cv$s" } + /** + * Map a Hand to a JsArray of cards + * Per card it has the string and the index in the hand + * @param hand + * @return + */ + def handToJson(hand: Hand): JsArray = { + val cards = hand.cards + JsArray( + cards.zipWithIndex.map { case (card, index) => + Json.obj( + "idx" -> index, + "card" -> cardtoString(card) + ) + } + ) + } + } diff --git a/knockoutwhistweb/app/util/WebsocketEventMapper.scala b/knockoutwhistweb/app/util/WebsocketEventMapper.scala index 7e01a7f..c12ecef 100644 --- a/knockoutwhistweb/app/util/WebsocketEventMapper.scala +++ b/knockoutwhistweb/app/util/WebsocketEventMapper.scala @@ -1,8 +1,11 @@ package util import de.knockoutwhist.utils.events.SimpleEvent +import logic.game.GameLobby +import play.api.libs.json.{JsValue, Json} import tools.jackson.databind.json.JsonMapper import tools.jackson.module.scala.ScalaModule +import util.mapper.{ReceivedHandEventMapper, SimpleEventMapper} object WebsocketEventMapper { @@ -13,8 +16,29 @@ object WebsocketEventMapper { private val mapper = JsonMapper.builder().addModule(scalaModule).build() - def toJsonString(obj: SimpleEvent): String = { - mapper.writeValueAsString(obj) + private var customMappers: Map[String,SimpleEventMapper[SimpleEvent]] = Map() + + private def registerCustomMapper[T <: SimpleEvent](mapper: SimpleEventMapper[T]): Unit = { + customMappers = customMappers + (mapper.id -> mapper.asInstanceOf[SimpleEventMapper[SimpleEvent]]) + } + + // Register all custom mappers here + registerCustomMapper(ReceivedHandEventMapper) + + def toJson(obj: SimpleEvent, gameLobby: GameLobby): JsValue = { + val data: Option[JsValue] = if (customMappers.contains(obj.id)) { + Some(customMappers(obj.id).toJson(obj)) + }else { + None + } + if (data.isEmpty) { + return Json.obj() + } + Json.obj( + "id" -> ("request-" + java.util.UUID.randomUUID().toString), + "event" -> obj.id, + "data" -> data + ) } } diff --git a/knockoutwhistweb/app/util/mapper/ReceivedHandEventMapper.scala b/knockoutwhistweb/app/util/mapper/ReceivedHandEventMapper.scala new file mode 100644 index 0000000..830a761 --- /dev/null +++ b/knockoutwhistweb/app/util/mapper/ReceivedHandEventMapper.scala @@ -0,0 +1,17 @@ +package util.mapper + +import de.knockoutwhist.events.player.ReceivedHandEvent +import logic.game.GameLobby +import play.api.libs.json.{JsArray, JsObject, Json} +import util.WebUIUtils + +object ReceivedHandEventMapper extends SimpleEventMapper[ReceivedHandEvent] { + + override def id: String = "ReceivedHandEvent" + override def toJson(event: ReceivedHandEvent, gameLobby: GameLobby): JsObject = { + Json.obj( + "dog" -> event.player.isInDogLife, + "hand" -> event.player.currentHand().map(hand => WebUIUtils.handToJson(hand)) + ) + } +} diff --git a/knockoutwhistweb/app/util/mapper/SimpleEventMapper.scala b/knockoutwhistweb/app/util/mapper/SimpleEventMapper.scala new file mode 100644 index 0000000..660fcea --- /dev/null +++ b/knockoutwhistweb/app/util/mapper/SimpleEventMapper.scala @@ -0,0 +1,12 @@ +package util.mapper + +import de.knockoutwhist.utils.events.SimpleEvent +import logic.game.GameLobby +import play.api.libs.json.JsObject + +trait SimpleEventMapper[T <: SimpleEvent] { + + def id: String + def toJson(event: T, gameLobby: GameLobby): JsObject + +} diff --git a/knockoutwhistweb/app/views/main.scala.html b/knockoutwhistweb/app/views/main.scala.html index 72e8799..aea7e72 100644 --- a/knockoutwhistweb/app/views/main.scala.html +++ b/knockoutwhistweb/app/views/main.scala.html @@ -26,6 +26,8 @@ + + diff --git a/knockoutwhistweb/public/javascripts/events.js b/knockoutwhistweb/public/javascripts/events.js new file mode 100644 index 0000000..24d7860 --- /dev/null +++ b/knockoutwhistweb/public/javascripts/events.js @@ -0,0 +1,42 @@ +function receiveHandEvent(eventData) { + //Data + const dog = eventData.dog; + const hand = eventData.hand; + + const handElement = $('#card-slide'); + handElement.addClass('ingame-cards-slide') + + let newHtml = ''; + + //Build Hand Container + hand.forEach((card) => { + //Data + const idx = card.idx + const cardS = card.card; + + const cardHtml = ` +
+
+ + ${cardS} +
+
+ `; + newHtml += cardHtml; + }); + + //Build dog if needed + if (dog) { + newHtml += ` +
+ +
+ `; + } + handElement.html(newHtml); +} + +onEvent("ReceivedHandEvent", receiveHandEvent) \ No newline at end of file diff --git a/knockoutwhistweb/public/javascripts/interact.js b/knockoutwhistweb/public/javascripts/interact.js new file mode 100644 index 0000000..ad34feb --- /dev/null +++ b/knockoutwhistweb/public/javascripts/interact.js @@ -0,0 +1,7 @@ +function handlePlayCard(card, dog) { + // TODO needs implementation +} + +function handleSkipDogLife(button) { + // TODO needs implementation +} \ No newline at end of file diff --git a/knockoutwhistweb/public/javascripts/websocket.js b/knockoutwhistweb/public/javascripts/websocket.js index 744c37a..ccbf44f 100644 --- a/knockoutwhistweb/public/javascripts/websocket.js +++ b/knockoutwhistweb/public/javascripts/websocket.js @@ -1,7 +1,9 @@ +type EventHandler = (data: any) => any | Promise; + // javascript let ws = null; // will be created by connectWebSocket() -const pending = new Map(); // id -> { resolve, reject, timer } -const handlers = new Map(); // eventType -> handler(data) -> (value|Promise) +const pending: Map = new Map(); // id -> { resolve, reject, timer } +const handlers: Map = new Map(); // eventType -> handler(data) -> (value|Promise) let timer = null; @@ -39,8 +41,8 @@ function setupSocketHandlers(socket) { if (id && eventType) { const handler = handlers.get(eventType); - const sendResponse = (respData) => { - const response = {id: id, event: eventType, data: respData === undefined ? {} : respData}; + const sendResponse = (result) => { + const response = {id: id, event: eventType, status: result}; if (socket && socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify(response)); } else { @@ -56,10 +58,10 @@ function setupSocketHandlers(socket) { try { Promise.resolve(handler(data === undefined ? {} : data)) - .then(result => sendResponse(result)) - .catch(err => sendResponse({error: err?.message ? err.message : String(err)})); + .then(_ => sendResponse("success")) + .catch(_ => sendResponse("error")); } catch (err) { - sendResponse({error: err?.message ? err.message : String(err)}); + sendResponse("error"); } } }; @@ -180,7 +182,7 @@ function sendEventAndWait(eventType, eventData, timeoutMs = 10000) { return p; } -function onEvent(eventType, handler) { +function onEvent(eventType: string, handler: EventHandler) { handlers.set(eventType, handler); }