From 1ef5e8a72fdf8a3d1ae624c8c3d7c6595017bc6f Mon Sep 17 00:00:00 2001 From: Janis Date: Wed, 26 Nov 2025 18:41:25 +0100 Subject: [PATCH] feat(api): Implemented session closed and kick event via websocket (#87) Reviewed-on: https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/pulls/87 Reviewed-by: lq64 Co-authored-by: Janis Co-committed-by: Janis --- .../app/controllers/IngameController.scala | 4 +- knockoutwhistweb/app/events/KickEvent.scala | 9 +++ knockoutwhistweb/app/events/LeftEvent.scala | 9 +++ .../app/events/LobbyUpdateEvent.scala | 9 +++ knockoutwhistweb/app/events/UserEvent.scala | 12 +++ .../app/logic/game/GameLobby.scala | 24 +++--- .../app/util/WebsocketEventMapper.scala | 6 +- .../app/util/mapper/KickEventMapper.scala | 19 +++++ .../app/util/mapper/LeftEventMapper.scala | 19 +++++ .../util/mapper/LobbyUpdateEventMapper.scala | 25 ++++++ .../app/util/mapper/SessionClosedMapper.scala | 19 +++++ .../app/views/lobby/lobby.scala.html | 31 +++++++- knockoutwhistweb/public/javascripts/events.js | 78 ++++++++++++++++++- .../public/javascripts/interact.js | 9 ++- 14 files changed, 253 insertions(+), 20 deletions(-) create mode 100644 knockoutwhistweb/app/events/KickEvent.scala create mode 100644 knockoutwhistweb/app/events/LeftEvent.scala create mode 100644 knockoutwhistweb/app/events/LobbyUpdateEvent.scala create mode 100644 knockoutwhistweb/app/events/UserEvent.scala create mode 100644 knockoutwhistweb/app/util/mapper/KickEventMapper.scala create mode 100644 knockoutwhistweb/app/util/mapper/LeftEventMapper.scala create mode 100644 knockoutwhistweb/app/util/mapper/LobbyUpdateEventMapper.scala create mode 100644 knockoutwhistweb/app/util/mapper/SessionClosedMapper.scala diff --git a/knockoutwhistweb/app/controllers/IngameController.scala b/knockoutwhistweb/app/controllers/IngameController.scala index 3929169..9c92bcc 100644 --- a/knockoutwhistweb/app/controllers/IngameController.scala +++ b/knockoutwhistweb/app/controllers/IngameController.scala @@ -89,7 +89,7 @@ class IngameController @Inject()( val game = PodManager.getGame(gameId) val playerToKickUUID = UUID.fromString(playerToKick) val result = Try { - game.get.leaveGame(playerToKickUUID) + game.get.leaveGame(playerToKickUUID, true) } if (result.isSuccess) { Ok(Json.obj( @@ -107,7 +107,7 @@ class IngameController @Inject()( def leaveGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = PodManager.getGame(gameId) val result = Try { - game.get.leaveGame(request.user.id) + game.get.leaveGame(request.user.id, false) } if (result.isSuccess) { Ok(Json.obj( diff --git a/knockoutwhistweb/app/events/KickEvent.scala b/knockoutwhistweb/app/events/KickEvent.scala new file mode 100644 index 0000000..844448b --- /dev/null +++ b/knockoutwhistweb/app/events/KickEvent.scala @@ -0,0 +1,9 @@ +package events + +import model.users.User + +case class KickEvent(user: User) extends UserEvent(user) { + + override def id: String = "KickEvent" + +} diff --git a/knockoutwhistweb/app/events/LeftEvent.scala b/knockoutwhistweb/app/events/LeftEvent.scala new file mode 100644 index 0000000..765bf7b --- /dev/null +++ b/knockoutwhistweb/app/events/LeftEvent.scala @@ -0,0 +1,9 @@ +package events + +import model.users.User + +case class LeftEvent(user: User) extends UserEvent(user) { + + override def id: String = "LeftEvent" + +} diff --git a/knockoutwhistweb/app/events/LobbyUpdateEvent.scala b/knockoutwhistweb/app/events/LobbyUpdateEvent.scala new file mode 100644 index 0000000..7625f13 --- /dev/null +++ b/knockoutwhistweb/app/events/LobbyUpdateEvent.scala @@ -0,0 +1,9 @@ +package events + +import de.knockoutwhist.utils.events.SimpleEvent + +case class LobbyUpdateEvent() extends SimpleEvent { + + override def id: String = "LobbyUpdateEvent" + +} diff --git a/knockoutwhistweb/app/events/UserEvent.scala b/knockoutwhistweb/app/events/UserEvent.scala new file mode 100644 index 0000000..3d64eae --- /dev/null +++ b/knockoutwhistweb/app/events/UserEvent.scala @@ -0,0 +1,12 @@ +package events + +import de.knockoutwhist.utils.events.SimpleEvent +import model.users.User + +import java.util.UUID + +abstract class UserEvent(user: User) extends SimpleEvent { + + def userId: UUID = user.id + +} diff --git a/knockoutwhistweb/app/logic/game/GameLobby.scala b/knockoutwhistweb/app/logic/game/GameLobby.scala index 2f96d1f..0da9376 100644 --- a/knockoutwhistweb/app/logic/game/GameLobby.scala +++ b/knockoutwhistweb/app/logic/game/GameLobby.scala @@ -10,13 +10,14 @@ import de.knockoutwhist.player.Playertype.HUMAN import de.knockoutwhist.player.{AbstractPlayer, PlayerFactory} import de.knockoutwhist.rounds.{Match, Round, Trick} import de.knockoutwhist.utils.events.{EventListener, SimpleEvent} +import events.{KickEvent, LeftEvent, LobbyUpdateEvent, UserEvent} import exceptions.* import logic.PodManager import model.sessions.{InteractionType, UserSession} import model.users.User import play.api.libs.json.{JsObject, Json} -import java.util.UUID +import java.util.{Timer, TimerTask, UUID} import scala.collection.mutable import scala.collection.mutable.ListBuffer @@ -44,7 +45,7 @@ class GameLobby private( ) users += (user.id -> userSession) PodManager.registerUserToGame(user, id) - //TODO : transmit Lobby Update transmitToAll() + logic.invoke(LobbyUpdateEvent()) userSession } @@ -52,6 +53,8 @@ class GameLobby private( event match { case event: PlayerEvent => users.get(event.playerId).foreach(session => session.updatePlayer(event)) + case event: UserEvent => + users.get(event.userId).foreach(session => session.updatePlayer(event)) case event: GameStateChangeEvent => if (event.oldState == MainMenu && event.newState == Lobby) { return @@ -93,8 +96,9 @@ class GameLobby private( * Remove the user from the game lobby. * * @param user the user who wants to leave the game. + * @param kicked whether the user was kicked or left voluntarily. */ - def leaveGame(userId: UUID): Unit = { + def leaveGame(userId: UUID, kicked: Boolean): Unit = { val sessionOpt = users.get(userId) if (sessionOpt.isEmpty) { throw new NotInThisGameException("You are not in this game!") @@ -105,16 +109,14 @@ class GameLobby private( PodManager.removeGame(id) return } - sessionOpt.get.websocketActor.foreach(act => act.transmitJsonToClient(Json.obj( - "id" -> "-1", - "event" -> "SessionClosed", - "data" -> Json.obj( - "reason" -> "You left the game (or got kicked)." - ) - ))) + if (kicked) { + logic.invoke(KickEvent(sessionOpt.get.user)) + } else { + logic.invoke(LeftEvent(sessionOpt.get.user)) + } users.remove(userId) PodManager.unregisterUserFromGame(sessionOpt.get.user) - //TODO: transmit Lobby Update transmitToAll() + logic.invoke(LobbyUpdateEvent()) } /** diff --git a/knockoutwhistweb/app/util/WebsocketEventMapper.scala b/knockoutwhistweb/app/util/WebsocketEventMapper.scala index 45030d9..aa237ed 100644 --- a/knockoutwhistweb/app/util/WebsocketEventMapper.scala +++ b/knockoutwhistweb/app/util/WebsocketEventMapper.scala @@ -6,7 +6,7 @@ import model.sessions.UserSession import play.api.libs.json.{JsValue, Json} import tools.jackson.databind.json.JsonMapper import tools.jackson.module.scala.ScalaModule -import util.mapper.{CardPlayedEventMapper, GameStateEventMapper, ReceivedHandEventMapper, SimpleEventMapper} +import util.mapper.{CardPlayedEventMapper, GameStateEventMapper, KickEventMapper, LeftEventMapper, LobbyUpdateEventMapper, ReceivedHandEventMapper, SessionClosedMapper, SimpleEventMapper} object WebsocketEventMapper { @@ -27,6 +27,10 @@ object WebsocketEventMapper { registerCustomMapper(ReceivedHandEventMapper) registerCustomMapper(GameStateEventMapper) registerCustomMapper(CardPlayedEventMapper) + registerCustomMapper(LobbyUpdateEventMapper) + registerCustomMapper(LeftEventMapper) + registerCustomMapper(KickEventMapper) + registerCustomMapper(SessionClosedMapper) def toJson(obj: SimpleEvent, session: UserSession): JsValue = { val data: Option[JsValue] = if (customMappers.contains(obj.id)) { diff --git a/knockoutwhistweb/app/util/mapper/KickEventMapper.scala b/knockoutwhistweb/app/util/mapper/KickEventMapper.scala new file mode 100644 index 0000000..7f73047 --- /dev/null +++ b/knockoutwhistweb/app/util/mapper/KickEventMapper.scala @@ -0,0 +1,19 @@ +package util.mapper + +import controllers.routes +import events.KickEvent +import model.sessions.UserSession +import play.api.libs.json.{JsObject, Json} + +object KickEventMapper extends SimpleEventMapper[KickEvent] { + + override def id: String = "KickEvent" + + override def toJson(event: KickEvent, session: UserSession): JsObject = { + Json.obj( + "url" -> routes.MainMenuController.mainMenu().url, + "content" -> views.html.mainmenu.creategame(Some(session.user)).toString, + ) + } + +} diff --git a/knockoutwhistweb/app/util/mapper/LeftEventMapper.scala b/knockoutwhistweb/app/util/mapper/LeftEventMapper.scala new file mode 100644 index 0000000..3455dda --- /dev/null +++ b/knockoutwhistweb/app/util/mapper/LeftEventMapper.scala @@ -0,0 +1,19 @@ +package util.mapper + +import controllers.routes +import events.{KickEvent, LeftEvent} +import model.sessions.UserSession +import play.api.libs.json.{JsObject, Json} + +object LeftEventMapper extends SimpleEventMapper[LeftEvent] { + + override def id: String = "LeftEvent" + + override def toJson(event: LeftEvent, session: UserSession): JsObject = { + Json.obj( + "url" -> routes.MainMenuController.mainMenu().url, + "content" -> views.html.mainmenu.creategame(Some(session.user)).toString + ) + } + +} diff --git a/knockoutwhistweb/app/util/mapper/LobbyUpdateEventMapper.scala b/knockoutwhistweb/app/util/mapper/LobbyUpdateEventMapper.scala new file mode 100644 index 0000000..cb878d9 --- /dev/null +++ b/knockoutwhistweb/app/util/mapper/LobbyUpdateEventMapper.scala @@ -0,0 +1,25 @@ +package util.mapper + +import events.LobbyUpdateEvent +import model.sessions.UserSession +import play.api.libs.json.{JsArray, JsObject, Json} + +object LobbyUpdateEventMapper extends SimpleEventMapper[LobbyUpdateEvent] { + + override def id: String = "LobbyUpdateEvent" + + override def toJson(event: LobbyUpdateEvent, session: UserSession): JsObject = { + Json.obj( + "host" -> session.host, + "maxPlayers" -> session.gameLobby.maxPlayers, + "players" -> JsArray(session.gameLobby.getPlayers.values.map(player => { + Json.obj( + "id" -> player.id, + "name" -> player.name, + "self" -> (player.id == session.user.id) + ) + }).toList) + ) + } + +} diff --git a/knockoutwhistweb/app/util/mapper/SessionClosedMapper.scala b/knockoutwhistweb/app/util/mapper/SessionClosedMapper.scala new file mode 100644 index 0000000..a247200 --- /dev/null +++ b/knockoutwhistweb/app/util/mapper/SessionClosedMapper.scala @@ -0,0 +1,19 @@ +package util.mapper + +import controllers.routes +import de.knockoutwhist.events.global.SessionClosed +import model.sessions.UserSession +import play.api.libs.json.{JsObject, Json} + +object SessionClosedMapper extends SimpleEventMapper[SessionClosed] { + + override def id: String = "SessionClosed" + + override def toJson(event: SessionClosed, session: UserSession): JsObject = { + Json.obj( + "url" -> routes.MainMenuController.mainMenu().url, + "content" -> views.html.mainmenu.creategame(Some(session.user)).toString, + ) + } + +} diff --git a/knockoutwhistweb/app/views/lobby/lobby.scala.html b/knockoutwhistweb/app/views/lobby/lobby.scala.html index f3f6346..66c4390 100644 --- a/knockoutwhistweb/app/views/lobby/lobby.scala.html +++ b/knockoutwhistweb/app/views/lobby/lobby.scala.html @@ -1,6 +1,35 @@ @(user: Option[model.users.User], gamelobby: logic.game.GameLobby)
+ + + + + + +
@@ -15,7 +44,7 @@
- Playeramount: @gamelobby.getPlayers.size / @gamelobby.maxPlayers
+ Players: @gamelobby.getPlayers.size / @gamelobby.maxPlayers
diff --git a/knockoutwhistweb/public/javascripts/events.js b/knockoutwhistweb/public/javascripts/events.js index 1ca2df2..ad4d2f7 100644 --- a/knockoutwhistweb/public/javascripts/events.js +++ b/knockoutwhistweb/public/javascripts/events.js @@ -42,8 +42,9 @@ function receiveHandEvent(eventData) { function receiveGameStateChange(eventData) { const content = eventData.content; const title = eventData.title || 'Knockout Whist'; + const url = eventData.url || null; - exchangeBody(content, title); + exchangeBody(content, title, url); } function receiveCardPlayedEvent(eventData) { const firstCard = eventData.firstCard; @@ -84,6 +85,79 @@ function receiveCardPlayedEvent(eventData) { `; firstCardContainer.html(newFirstCardHTML); } +function receiveLobbyUpdateEvent(eventData) { + const host = eventData.host; + const maxPlayers = eventData.maxPlayers; + const players = eventData.players; + + const lobbyPlayersContainer = $('#players'); + const playerAmountBox = $('#playerAmount'); + + let newHtml = '' + + if (host) { + players.forEach(user => { + + const inner = user.self ? `
${user.name} (You)
+ Remove` + : `
${user.name}
+
Remove
` + + newHtml += `
+
+ Profile +
+ ${inner} +
+
+
` + }) + } else { + players.forEach(user => { + + const inner = user.self ? `
${user.name} (You)
` : `
${user.name}
` + + newHtml += `
+
+ Profile +
+ ${inner} +
+
+
` + }) + } + + lobbyPlayersContainer.html(newHtml); + playerAmountBox.text(`Players: ${players.length} / ${maxPlayers}`); + +} +function receiveKickEvent(eventData) { + $('#kickedModal').modal({ + backdrop: 'static', + keyboard: false + }).modal('show'); + + setTimeout(() => { + receiveGameStateChange(eventData) + }, 5000); +} +function receiveSessionClosedEvent(eventData) { + $('#sessionClosed').modal({ + backdrop: 'static', + keyboard: false + }).modal('show'); + + setTimeout(() => { + receiveGameStateChange(eventData) + }, 5000); +} + + onEvent("ReceivedHandEvent", receiveHandEvent) onEvent("GameStateChangeEvent", receiveGameStateChange) -onEvent("CardPlayedEvent", receiveCardPlayedEvent) \ No newline at end of file +onEvent("CardPlayedEvent", receiveCardPlayedEvent) +onEvent("LobbyUpdateEvent", receiveLobbyUpdateEvent) +onEvent("LeftEvent", receiveGameStateChange) +onEvent("KickEvent", receiveKickEvent) +onEvent("SessionClosed", receiveSessionClosedEvent) diff --git a/knockoutwhistweb/public/javascripts/interact.js b/knockoutwhistweb/public/javascripts/interact.js index ad34feb..555634e 100644 --- a/knockoutwhistweb/public/javascripts/interact.js +++ b/knockoutwhistweb/public/javascripts/interact.js @@ -1,7 +1,10 @@ function handlePlayCard(card, dog) { - // TODO needs implementation + // TODO needs implementation } function handleSkipDogLife(button) { - // TODO needs implementation -} \ No newline at end of file + // TODO needs implementation +} +function handleKickPlayer(playerId) { + // TODO needs implementation +}