feat(ui): Websocket

Started implementing functionality to Websocket.
This commit is contained in:
LQ63
2025-11-26 01:35:46 +01:00
parent 52e5033afc
commit 0c2901bce0
17 changed files with 295 additions and 30 deletions

View File

@@ -40,8 +40,8 @@ lazy val knockoutwhistweb = project.in(file("knockoutwhistweb"))
libraryDependencies += "de.mkammerer" % "argon2-jvm" % "2.12",
libraryDependencies += "com.auth0" % "java-jwt" % "4.5.0",
libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.2.3",
libraryDependencies += "tools.jackson.module" %% "jackson-module-scala" % "3.0.2",
JsEngineKeys.engineType := JsEngineKeys.EngineType.Node
libraryDependencies += "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.16.1",
//JsEngineKeys.engineType := JsEngineKeys.EngineType.Node
)
lazy val root = (project in file("."))

View File

@@ -2,7 +2,7 @@ package logic.game
import de.knockoutwhist.cards.{Hand, Suit}
import de.knockoutwhist.control.GameLogic
import de.knockoutwhist.control.GameState.{Lobby, MainMenu}
import de.knockoutwhist.control.GameState.{InGame, Lobby, MainMenu}
import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil}
import de.knockoutwhist.events.global.{GameStateChangeEvent, SessionClosed}
import de.knockoutwhist.events.player.PlayerEvent
@@ -56,6 +56,9 @@ class GameLobby private(
if (event.oldState == MainMenu && event.newState == Lobby) {
return
}
if (event.oldState == Lobby && event.newState == InGame) {
println("RECEIVED GAMESTATEEVENT")
}
users.values.foreach(session => session.updatePlayer(event))
case event: SimpleEvent =>
users.values.foreach(session => session.updatePlayer(event))
@@ -68,6 +71,7 @@ class GameLobby private(
* @param user the user who wants to start the game.
*/
def startGame(user: User): Unit = {
println("STARTED GAME IN LOGIC")
val sessionOpt = users.get(user.id)
if (sessionOpt.isEmpty) {
throw new NotInThisGameException("You are not in this game!")

View File

@@ -2,9 +2,11 @@ package model.sessions
import de.knockoutwhist.events.player.{RequestCardEvent, RequestTieChoiceEvent, RequestTrumpSuitEvent}
import de.knockoutwhist.utils.events.SimpleEvent
import logic.PodManager
import logic.game.GameLobby
import model.users.User
import play.api.libs.json.JsObject
import play.api.libs.json.Format.GenericFormat
import play.api.libs.json.{JsError, JsObject, JsResult, JsSuccess, JsValue}
import java.util.UUID
import java.util.concurrent.locks.ReentrantLock
@@ -26,7 +28,7 @@ class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) e
else canInteract = Some(InteractionType.Card)
case _ =>
}
websocketActor.foreach(_.transmitEventToClient(event))
websocketActor.foreach(_.transmitEventToClient(event, gameLobby))
}
override def id: UUID = user.id
@@ -44,6 +46,21 @@ class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) e
case "Ping" =>
// No action needed for Ping
()
case "Start Game" =>
println("INSIDE HANDLE WEB RESPONSE" + data)
val gameId: String = (data \ "gameId").get.toString
val cleanGameId: String = gameId.replaceAll("^[\"']|[\"']$", "")
val user: JsObject = (data \ "user").asOpt[JsObject].get
val gameLobby: GameLobby = PodManager.getGame(cleanGameId).get
val realUser: JsResult[User] = user.validate[User]
val uu: User = realUser match {
case JsSuccess(extractedUser, _) =>
extractedUser
case e: JsError =>
println("FAILED" + JsError.toJson(e).toString())
throw new Exception("Failed to deserialize User object: " + JsError.toJson(e).toString())
}
gameLobby.startGame(uu)
}
}
lock.unlock()

View File

@@ -1,6 +1,7 @@
package model.sessions
import de.knockoutwhist.utils.events.SimpleEvent
import logic.game.GameLobby
import org.apache.pekko.actor.{Actor, ActorRef}
import play.api.libs.json.{JsObject, JsValue, Json}
import util.WebsocketEventMapper
@@ -27,6 +28,7 @@ class UserWebsocketActor(
Try {
jsonObject match {
case Success(value) =>
println("SUCCESS, RECEIVED SUCCESSFUL MESSAGE..." + jsonObject)
handle(value)
case Failure(exception) =>
transmitTextToClient(s"Error parsing JSON: ${exception.getMessage}")
@@ -86,12 +88,12 @@ class UserWebsocketActor(
}
}
def transmitJsonToClient(jsonObj: JsObject): Unit = {
def transmitJsonToClient(jsonObj: JsValue): Unit = {
transmitTextToClient(jsonObj.toString())
}
def transmitEventToClient(event: SimpleEvent): Unit = {
transmitJsonToClient(WebsocketEventMapper.toJson(event))
def transmitEventToClient(event: SimpleEvent, gameLobby: GameLobby): Unit = {
transmitJsonToClient(WebsocketEventMapper.toJson(event, gameLobby))
}
}

View File

@@ -1,5 +1,7 @@
package model.users
import play.api.libs.json.{Format, Json}
import java.util.UUID
case class User(
@@ -16,5 +18,7 @@ case class User(
private def withPasswordHash(newPasswordHash: String): User = {
this.copy(passwordHash = newPasswordHash)
}
}
object User {
implicit val userFormat: Format[User] = Json.format[User]
}

View File

@@ -1,17 +1,16 @@
package util
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.scala.ScalaModule
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}
import util.mapper.{GameStateChangeEventMapper, NewRoundEventMapper, NewTrickEventMapper, ReceivedHandEventMapper, RequestCardEventMapper, SimpleEventMapper, TrickEndEventMapper}
object WebsocketEventMapper {
private val scalaModule = ScalaModule.builder()
.addAllBuiltinModules()
.supportScala3Classes(true)
//.supportScala3Classes(true)
.build()
private val mapper = JsonMapper.builder().addModule(scalaModule).build()
@@ -24,10 +23,15 @@ object WebsocketEventMapper {
// Register all custom mappers here
registerCustomMapper(ReceivedHandEventMapper)
registerCustomMapper(NewRoundEventMapper)
registerCustomMapper(NewTrickEventMapper)
registerCustomMapper(TrickEndEventMapper)
registerCustomMapper(RequestCardEventMapper)
registerCustomMapper(GameStateChangeEventMapper)
def toJson(obj: SimpleEvent, gameLobby: GameLobby): JsValue = {
val data: Option[JsValue] = if (customMappers.contains(obj.id)) {
Some(customMappers(obj.id).toJson(obj))
Some(customMappers(obj.id).toJson(obj, gameLobby))
}else {
None
}

View File

@@ -0,0 +1,18 @@
package util.mapper
import de.knockoutwhist.events.global.GameStateChangeEvent
import logic.game.GameLobby
import play.api.libs.json.{JsObject, Json}
object GameStateChangeEventMapper extends SimpleEventMapper[GameStateChangeEvent]{
override def id: String = "GameStateChangeEvent"
override def toJson(event: GameStateChangeEvent, gameLobby: GameLobby): JsObject = {
println("CALLED toJSON FOR GAMESTATECHANGE")
Json.obj(
"oldState" -> event.oldState.toString,
"newState" -> event.newState.toString,
"gameLobby" -> gameLobby.id
)
}
}

View File

@@ -0,0 +1,16 @@
package util.mapper
import de.knockoutwhist.events.global.NewRoundEvent
import logic.game.GameLobby
import play.api.libs.json.{JsObject, Json}
object NewRoundEventMapper extends SimpleEventMapper[NewRoundEvent]{
override def id: String = "NewRoundEvent"
override def toJson(event: NewRoundEvent, gameLobby: GameLobby): JsObject = {
Json.obj(
"trumpsuit" -> gameLobby.getLogic.getCurrentRound.get.trumpSuit.toString,
"players" -> gameLobby.getLogic.getCurrentMatch.get.playersIn.map(player => player.toString)
)
}
}

View File

@@ -0,0 +1,13 @@
package util.mapper
import de.knockoutwhist.events.global.NewTrickEvent
import logic.game.GameLobby
import play.api.libs.json.{JsObject, Json}
object NewTrickEventMapper extends SimpleEventMapper[NewTrickEvent]{
override def id: String = "NewTrickEvent"
override def toJson(event: NewTrickEvent, gameLobby: GameLobby): JsObject = {
Json.obj()
}
}

View File

@@ -0,0 +1,15 @@
package util.mapper
import de.knockoutwhist.events.player.RequestCardEvent
import logic.game.GameLobby
import play.api.libs.json.{JsObject, Json}
object RequestCardEventMapper extends SimpleEventMapper[RequestCardEvent]{
override def id: String = "RequestCardEvent"
override def toJson(event: RequestCardEvent, gameLobby: GameLobby): JsObject = {
Json.obj(
"player" -> event.player.name
)
}
}

View File

@@ -0,0 +1,18 @@
package util.mapper
import de.knockoutwhist.events.global.TrickEndEvent
import de.knockoutwhist.rounds.Trick
import logic.game.GameLobby
import play.api.libs.json.{JsObject, Json}
object TrickEndEventMapper extends SimpleEventMapper[TrickEndEvent]{
override def id: String = "TrickEndEvent"
override def toJson(event: TrickEndEvent, gameLobby: GameLobby): JsObject = {
Json.obj(
"playerwon" -> event.winner.name,
"playersin" -> gameLobby.getLogic.getCurrentMatch.get.playersIn.map(player => player.name),
"tricklist" -> gameLobby.getLogic.getCurrentRound.get.tricklist.map(trick => trick.winner.map(player => player.name).getOrElse("Trick in Progress"))
)
}
}

View File

@@ -40,7 +40,7 @@
}
</div>
<div class="col-12 text-center mb-5">
<div class="btn btn-success" onclick="startGame('@gamelobby.id')">Start Game</div>
<div class="btn btn-success" onclick="startGame('@gamelobby.id', '@user.get.id', '@user.get.name','@user.get.passwordHash', @user.get.internalId)">Start Game</div>
</div>
} else {
<div id="players" class="justify-content-center align-items-center d-flex">
@@ -69,6 +69,7 @@
</div>
</main>
<script>
/*
function waitForFunction(name, checkInterval = 100) {
return new Promise(resolve => {
const timer = setInterval(() => {
@@ -80,5 +81,6 @@
});
}
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
*/
connectWebSocket()
</script>

View File

@@ -23,11 +23,11 @@
* the page content. *@
@content
</body>
<script src="@routes.JavaScriptRoutingController.javascriptRoutes()" type="text/javascript"></script>
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
<script src="@routes.Assets.versioned("../../public/javascripts/websocket.js")" type="text/javascript"></script>
<script src="@routes.Assets.versioned("../../public/javascripts/events.js")" type="text/javascript"></script>
<script src="@routes.Assets.versioned("../../public/javascripts/interact.js")" type="text/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="@routes.JavaScriptRoutingController.javascriptRoutes()" type="text/javascript"></script>
<script src="@routes.Assets.versioned("javascripts/websocket.js")" type="text/javascript"></script>
<script src="@routes.Assets.versioned("javascripts/events.js")" type="text/javascript"></script>
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
<script src="@routes.Assets.versioned("javascripts/interact.js")" type="text/javascript"></script>
</html>

View File

@@ -30,4 +30,4 @@
</main>
<script>
disconnectWebSocket();
</script>
</script>

View File

@@ -1,3 +1,30 @@
function alertMessage(message) {
let newHtml = '';
const alertId = `alert-${Date.now()}`;
const fadeTime = 500;
const duration = 5000;
newHtml += `
<div class="fixed-top d-flex justify-content-center mt-3" style="z-index: 1050;">
<div
id="${alertId}" class="alert alert-primary d-flex align-items-center p-2 mb-0 w-auto" role="alert" style="display: none;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="bi flex-shrink-0 me-2" viewBox="0 0 16 16" role="img" aria-label="Warning:">
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
</svg>
<div class="small">
<small>${message}</small>
</div>
</div>
</div>
`;
$('#main-body').prepend(newHtml);
const $notice = $(`#${alertId}`);
$notice.fadeIn(fadeTime);
setTimeout(function() {
$notice.fadeOut(fadeTime, function() {
$(this).parent().remove();
});
}, duration);
}
function receiveHandEvent(eventData) {
//Data
const dog = eventData.dog;
@@ -38,5 +65,113 @@ function receiveHandEvent(eventData) {
}
handElement.html(newHtml);
}
function newRoundEvent(eventData) {
const trumpsuit = eventData.trumpsuit;
const players = eventData.players;
onEvent("ReceivedHandEvent", receiveHandEvent)
const tableElement = $('#score-table-body');
let tablehtml = `
<h4 class="fw-bold mb-3 text-black">Tricks Won</h4>
<div class="d-flex justify-content-between score-header pb-1">
<div style="width: 50%">PLAYER</div>
<div style="width: 50%">TRICKS</div>
</div>
`;
players.forEach(
tablehtml += `
<div class="d-flex justify-content-between score-row pt-1">
<div style="width: 50%" class="text-truncate">'${players}'</div>
<div style="width: 50%">
0
</div>
</div>
`
);
tableElement.html(tablehtml);
const trumpsuitClass = $('#trumpsuit');
trumpsuitClass.html(trumpsuit);
}
function trickEndEvent(eventData) {
const winner = eventData.playerwon;
const players = eventData.playersin;
const tricklist = eventData.tricklist;
let newHtml = '';
let tricktable = $('#score-table-body');
newHtml += `
<h4 class="fw-bold mb-3 text-black">Tricks Won</h4>
<div class="d-flex justify-content-between score-header pb-1">
<div style="width: 50%">PLAYER</div>
<div style="width: 50%">TRICKS</div>
</div>
`;
let playercounts = new Map();
players.forEach( player => {
playercounts.set(player, 0)
});
tricklist.forEach( player => {
if ( player !== "Trick in Progress" && playercounts.has(player)) {
playercounts.set(player, playercounts.get(player) + 1)
}
}
)
const playerorder = players.sort((playerA, playerB) => {
const countA = playercounts.get(playerA.name) || 0;
const countB = playercounts.get(playerB.name) || 0;
return countB - countA;
});
playerorder.forEach( player => {
newHtml += `
<div class="d-flex justify-content-between score-row pt-1">
<div style="width: 50%" class="text-truncate">'${player}'</div>
<div style="width: 50%">
'${playercounts.get(player)}'
</div>
</div>
`
});
tricktable.html(newHtml);
}
function newTrickEvent() {
const firstCardContainer = $('first-card-container');
let newHtml = '';
newHtml += `
<img src="images/cards/1B.png" alt="Blank Card" width="80px"/>
`;
firstCardContainer.html(newHtml);
}
function requestCardEvent(eventData) {
const player = eventData.player;
const handElement = $('#card-slide')
handElement.removeClass('inactive');
}
//alertMessage("It worked!")
function gameStateChangeEvent(eventData) {
console.log("INSIDE EVENTHANDLER IN JAVASCRIPT")
const gameId = eventData.gameId
const newHtml = jsRoutes.controllers.IngameController.returnInnerHTML(gameId, eventData.oldState.toString());
console.log("HTML:" + newHtml)
const body = $('#main-body')
body.html(newHtml)
}
onEvent("ReceivedHandEvent", receiveHandEvent)
onEvent("NewRoundEvent", newRoundEvent)
onEvent("TrickEndEvent", trickEndEvent)
onEvent("NewTrickEvent", newTrickEvent)
onEvent("RequestCardEvent", requestCardEvent)
onEvent("GameStateChangeEvent", gameStateChangeEvent)

View File

@@ -4,4 +4,18 @@ function handlePlayCard(card, dog) {
function handleSkipDogLife(button) {
// TODO needs implementation
}
function startGame(gameId, userId, username, userpasswordhash, userinternalid) {
const userpayload = {
internalId: userinternalid,
id: userId,
name: username,
passwordHash: userpasswordhash
}
const payload = {
gameId: gameId,
user: userpayload
};
console.log("CLICKED START GAME, SENDING EVENT..." + payload)
sendEvent("Start Game", payload)
}

View File

@@ -1,13 +1,14 @@
type EventHandler = (data: any) => any | Promise<any>;
// javascript
let ws = null; // will be created by connectWebSocket()
const pending: Map<string, any> = new Map(); // id -> { resolve, reject, timer }
const handlers: Map<string, EventHandler> = new Map(); // eventType -> handler(data) -> (value|Promise)
const pending = new Map(); // id -> { resolve, reject, timer }
const handlers = new Map(); // eventType -> handler(data) -> (value|Promise)
let timer = null;
function onEvent(eventType, handler) {
handlers.set(eventType, handler);
}
// helper to attach message/error/close handlers to a socket
function setupSocketHandlers(socket) {
socket.onmessage = (event) => {
@@ -15,7 +16,9 @@ function setupSocketHandlers(socket) {
let msg;
try {
msg = JSON.parse(event.data);
console.log("1 " + msg.event)
} catch (e) {
console.log("2 " + msg.event)
console.debug("Non-JSON message from server:", event.data, e);
return;
}
@@ -40,6 +43,7 @@ function setupSocketHandlers(socket) {
}
if (id && eventType) {
console.log("3 " + eventType)
const handler = handlers.get(eventType);
const sendResponse = (result) => {
const response = {id: id, event: eventType, status: result};
@@ -57,6 +61,7 @@ function setupSocketHandlers(socket) {
}
try {
console.log("4 " + msg.event)
Promise.resolve(handler(data === undefined ? {} : data))
.then(_ => sendResponse("success"))
.catch(_ => sendResponse("error"));
@@ -182,9 +187,7 @@ function sendEventAndWait(eventType, eventData, timeoutMs = 10000) {
return p;
}
function onEvent(eventType: string, handler: EventHandler) {
handlers.set(eventType, handler);
}
globalThis.sendEvent = sendEvent;
globalThis.sendEventAndWait = sendEventAndWait;