From a8ccebb504f0dcb7f6d86a5fec43ebf6fa64e3a7 Mon Sep 17 00:00:00 2001 From: Janis Date: Sat, 6 Dec 2025 10:15:59 +0100 Subject: [PATCH] feat: BAC-30 Implement Jackson Mapping via DTOs --- knockoutwhist | 2 +- knockoutwhistweb/app/dto/GameInfoDTO.scala | 35 +++++++++++++ knockoutwhistweb/app/dto/LobbyInfoDTO.scala | 21 ++++++++ knockoutwhistweb/app/dto/TieInfoDTO.scala | 24 +++++---- knockoutwhistweb/app/dto/TrumpInfoDTO.scala | 29 +++++++++++ knockoutwhistweb/app/dto/UserDTO.scala | 19 ------- knockoutwhistweb/app/dto/WonInfoDTO.scala | 32 ++++++++++++ .../app/dto/{ => subDTO}/CardDTO.scala | 5 +- .../app/dto/{ => subDTO}/HandDTO.scala | 5 +- .../app/dto/{ => subDTO}/PlayerDTO.scala | 5 +- .../app/dto/{ => subDTO}/PlayerQueueDTO.scala | 5 +- .../app/dto/subDTO/PodiumPlayerDTO.scala | 47 ++++++++++++++++++ .../app/dto/{ => subDTO}/RoundDTO.scala | 10 ++-- .../app/dto/{ => subDTO}/TrickDTO.scala | 5 +- knockoutwhistweb/app/dto/subDTO/UserDTO.scala | 17 +++++++ .../app/util/WebsocketEventMapper.scala | 49 +++++++++++++------ 16 files changed, 240 insertions(+), 70 deletions(-) create mode 100644 knockoutwhistweb/app/dto/GameInfoDTO.scala create mode 100644 knockoutwhistweb/app/dto/LobbyInfoDTO.scala create mode 100644 knockoutwhistweb/app/dto/TrumpInfoDTO.scala delete mode 100644 knockoutwhistweb/app/dto/UserDTO.scala create mode 100644 knockoutwhistweb/app/dto/WonInfoDTO.scala rename knockoutwhistweb/app/dto/{ => subDTO}/CardDTO.scala (76%) rename knockoutwhistweb/app/dto/{ => subDTO}/HandDTO.scala (66%) rename knockoutwhistweb/app/dto/{ => subDTO}/PlayerDTO.scala (70%) rename knockoutwhistweb/app/dto/{ => subDTO}/PlayerQueueDTO.scala (79%) create mode 100644 knockoutwhistweb/app/dto/subDTO/PodiumPlayerDTO.scala rename knockoutwhistweb/app/dto/{ => subDTO}/RoundDTO.scala (53%) rename knockoutwhistweb/app/dto/{ => subDTO}/TrickDTO.scala (78%) create mode 100644 knockoutwhistweb/app/dto/subDTO/UserDTO.scala diff --git a/knockoutwhist b/knockoutwhist index e77aef9..10fa4ba 160000 --- a/knockoutwhist +++ b/knockoutwhist @@ -1 +1 @@ -Subproject commit e77aef9e313838ca799d4210a4affb31d3948211 +Subproject commit 10fa4badf09b2df0105581197d83a97555bbf6b1 diff --git a/knockoutwhistweb/app/dto/GameInfoDTO.scala b/knockoutwhistweb/app/dto/GameInfoDTO.scala new file mode 100644 index 0000000..6dbb3e0 --- /dev/null +++ b/knockoutwhistweb/app/dto/GameInfoDTO.scala @@ -0,0 +1,35 @@ +package dto + +import dto.subDTO.* +import logic.game.GameLobby +import model.users.User + +import scala.util.Try + +case class GameInfoDTO( + gameId: String, + self: Option[PlayerDTO], + hand: Option[HandDTO], + playerQueue: PlayerQueueDTO, + currentTrick: Option[TrickDTO], + currentRound: Option[RoundDTO] + ) + +object GameInfoDTO { + + def apply(lobby: GameLobby, user: User): GameInfoDTO = { + val selfPlayer = Try { + Some(lobby.getPlayerByUser(user)) + }.getOrElse(None) + + GameInfoDTO( + gameId = lobby.id, + self = selfPlayer.map(PlayerDTO(_)), + hand = selfPlayer.flatMap(_.currentHand()).map(HandDTO(_)), + playerQueue = PlayerQueueDTO(lobby.logic), + currentTrick = lobby.logic.getCurrentTrick.map(TrickDTO(_)), + currentRound = lobby.logic.getCurrentRound.map(RoundDTO(_)) + ) + } + +} \ No newline at end of file diff --git a/knockoutwhistweb/app/dto/LobbyInfoDTO.scala b/knockoutwhistweb/app/dto/LobbyInfoDTO.scala new file mode 100644 index 0000000..c6e78a9 --- /dev/null +++ b/knockoutwhistweb/app/dto/LobbyInfoDTO.scala @@ -0,0 +1,21 @@ +package dto + +import dto.subDTO.UserDTO +import logic.game.GameLobby +import model.users.User + +case class LobbyInfoDTO(users: List[UserDTO], self: UserDTO, maxPlayers: Int) + +object LobbyInfoDTO { + + def apply(lobby: GameLobby, user: User): LobbyInfoDTO = { + val session = lobby.getUserSession(user.id) + + LobbyInfoDTO( + users = lobby.getPlayers.values.map(user => UserDTO(user)).toList, + self = UserDTO(session), + maxPlayers = lobby.maxPlayers, + ) + } + +} diff --git a/knockoutwhistweb/app/dto/TieInfoDTO.scala b/knockoutwhistweb/app/dto/TieInfoDTO.scala index ce895fb..4642028 100644 --- a/knockoutwhistweb/app/dto/TieInfoDTO.scala +++ b/knockoutwhistweb/app/dto/TieInfoDTO.scala @@ -1,19 +1,25 @@ package dto -import de.knockoutwhist.control.sublogic.PlayerTieLogic -import play.api.libs.json.{Json, OFormat} +import dto.subDTO.PlayerDTO +import logic.game.GameLobby +import model.users.User -case class TieInfoDTO(currentPlayer: Option[PlayerDTO], tiedPlayers: Seq[PlayerDTO], highestAmount: Int) +import scala.util.Try + +case class TieInfoDTO(currentPlayer: Option[PlayerDTO], self: Option[PlayerDTO], tiedPlayers: Seq[PlayerDTO], highestAmount: Int) object TieInfoDTO { - implicit val tieInfoFormat: OFormat[TieInfoDTO] = Json.format[TieInfoDTO] - - def apply(tieInput: PlayerTieLogic): Unit = { + def apply(lobby: GameLobby, user: User): TieInfoDTO = { + val selfPlayer = Try { + Some(lobby.getPlayerByUser(user)) + }.getOrElse(None) + TieInfoDTO( - currentPlayer = tieInput.currentTiePlayer().map(PlayerDTO.apply), - tiedPlayers = tieInput.getTiedPlayers.map(PlayerDTO.apply), - highestAmount = tieInput.highestAllowedNumber() + currentPlayer = lobby.logic.playerTieLogic.currentTiePlayer().map(PlayerDTO.apply), + self = selfPlayer.map(PlayerDTO.apply), + tiedPlayers = lobby.logic.playerTieLogic.getTiedPlayers.map(PlayerDTO.apply), + highestAmount = lobby.logic.playerTieLogic.highestAllowedNumber() ) } diff --git a/knockoutwhistweb/app/dto/TrumpInfoDTO.scala b/knockoutwhistweb/app/dto/TrumpInfoDTO.scala new file mode 100644 index 0000000..c560a44 --- /dev/null +++ b/knockoutwhistweb/app/dto/TrumpInfoDTO.scala @@ -0,0 +1,29 @@ +package dto + +import dto.subDTO.{HandDTO, PlayerDTO} +import logic.game.GameLobby +import model.users.User + +import scala.util.Try + +case class TrumpInfoDTO( + chooser: Option[PlayerDTO], + self: Option[PlayerDTO], + selfHand: Option[HandDTO], + ) + +object TrumpInfoDTO { + + def apply(lobby: GameLobby, user: User): TrumpInfoDTO = { + val selfPlayer = Try { + Some(lobby.getPlayerByUser(user)) + }.getOrElse(None) + + TrumpInfoDTO( + chooser = lobby.logic.getTrumpPlayer.map(PlayerDTO(_)), + self = selfPlayer.map(PlayerDTO(_)), + selfHand = selfPlayer.flatMap(_.currentHand()).map(HandDTO(_)) + ) + } + +} diff --git a/knockoutwhistweb/app/dto/UserDTO.scala b/knockoutwhistweb/app/dto/UserDTO.scala deleted file mode 100644 index 5e18fd1..0000000 --- a/knockoutwhistweb/app/dto/UserDTO.scala +++ /dev/null @@ -1,19 +0,0 @@ -package dto - -import model.users.User -import play.api.libs.json.{Json, OFormat} - -case class UserDTO(id: String, username: String) - -object UserDTO { - - implicit val userFormat: OFormat[UserDTO] = Json.format[UserDTO] - - def apply(user: User): UserDTO = { - UserDTO( - id = user.id.toString, - username = user.name - ) - } - -} \ No newline at end of file diff --git a/knockoutwhistweb/app/dto/WonInfoDTO.scala b/knockoutwhistweb/app/dto/WonInfoDTO.scala new file mode 100644 index 0000000..edd39f1 --- /dev/null +++ b/knockoutwhistweb/app/dto/WonInfoDTO.scala @@ -0,0 +1,32 @@ +package dto + +import dto.subDTO.PodiumPlayerDTO +import logic.game.GameLobby +import model.users.User + +case class WonInfoDTO( + winner: Option[PodiumPlayerDTO], + allPlayers: Seq[PodiumPlayerDTO] + ) + +object WonInfoDTO { + + def apply(lobby: GameLobby, user: User): WonInfoDTO = { + val matchImpl = lobby.logic.getCurrentMatch + if (matchImpl.isEmpty) { + throw new IllegalStateException("No current match available in game logic") + } + val allPlayersDTO: Seq[PodiumPlayerDTO] = matchImpl.get.totalplayers.map { player => + PodiumPlayerDTO(lobby.logic, player) + } + + val selfPlayerDTO = lobby.getPlayerByUser(user) + val winnerDTO = lobby.logic.getWinner + + WonInfoDTO( + winner = winnerDTO.map(player => PodiumPlayerDTO(lobby.logic, player)), + allPlayers = allPlayersDTO + ) + } + +} \ No newline at end of file diff --git a/knockoutwhistweb/app/dto/CardDTO.scala b/knockoutwhistweb/app/dto/subDTO/CardDTO.scala similarity index 76% rename from knockoutwhistweb/app/dto/CardDTO.scala rename to knockoutwhistweb/app/dto/subDTO/CardDTO.scala index 827930e..60373c7 100644 --- a/knockoutwhistweb/app/dto/CardDTO.scala +++ b/knockoutwhistweb/app/dto/subDTO/CardDTO.scala @@ -1,7 +1,6 @@ -package dto +package dto.subDTO import de.knockoutwhist.cards.Card -import play.api.libs.json.{Json, OFormat} import util.WebUIUtils case class CardDTO(identifier: String, path: String, idx: Int) { @@ -14,8 +13,6 @@ case class CardDTO(identifier: String, path: String, idx: Int) { object CardDTO { - implicit val cardFormat: OFormat[CardDTO] = Json.format[CardDTO] - def apply(card: Card, index: Int = 0): CardDTO = { CardDTO( identifier = WebUIUtils.cardtoString(card), diff --git a/knockoutwhistweb/app/dto/HandDTO.scala b/knockoutwhistweb/app/dto/subDTO/HandDTO.scala similarity index 66% rename from knockoutwhistweb/app/dto/HandDTO.scala rename to knockoutwhistweb/app/dto/subDTO/HandDTO.scala index 7353fef..e954b2a 100644 --- a/knockoutwhistweb/app/dto/HandDTO.scala +++ b/knockoutwhistweb/app/dto/subDTO/HandDTO.scala @@ -1,14 +1,11 @@ -package dto +package dto.subDTO import de.knockoutwhist.cards.Hand -import play.api.libs.json.{Json, OFormat} case class HandDTO(card: List[CardDTO]) object HandDTO { - implicit val handFormat: OFormat[HandDTO] = Json.format[HandDTO] - def apply(hand: Hand): HandDTO = { HandDTO( card = hand.cards.zipWithIndex.map { case (card, idx) => CardDTO(card, idx) } diff --git a/knockoutwhistweb/app/dto/PlayerDTO.scala b/knockoutwhistweb/app/dto/subDTO/PlayerDTO.scala similarity index 70% rename from knockoutwhistweb/app/dto/PlayerDTO.scala rename to knockoutwhistweb/app/dto/subDTO/PlayerDTO.scala index c3b9307..bc2656a 100644 --- a/knockoutwhistweb/app/dto/PlayerDTO.scala +++ b/knockoutwhistweb/app/dto/subDTO/PlayerDTO.scala @@ -1,13 +1,10 @@ -package dto +package dto.subDTO import de.knockoutwhist.player.AbstractPlayer -import play.api.libs.json.{Json, OFormat} case class PlayerDTO(id: String, name: String, dogLife: Boolean) object PlayerDTO { - - implicit val playerFormat: OFormat[PlayerDTO] = Json.format[PlayerDTO] def apply(player: AbstractPlayer): PlayerDTO = { PlayerDTO( diff --git a/knockoutwhistweb/app/dto/PlayerQueueDTO.scala b/knockoutwhistweb/app/dto/subDTO/PlayerQueueDTO.scala similarity index 79% rename from knockoutwhistweb/app/dto/PlayerQueueDTO.scala rename to knockoutwhistweb/app/dto/subDTO/PlayerQueueDTO.scala index 43a815b..f1f49a8 100644 --- a/knockoutwhistweb/app/dto/PlayerQueueDTO.scala +++ b/knockoutwhistweb/app/dto/subDTO/PlayerQueueDTO.scala @@ -1,14 +1,11 @@ -package dto +package dto.subDTO import de.knockoutwhist.control.GameLogic -import play.api.libs.json.{Json, OFormat} case class PlayerQueueDTO(currentPlayer: Option[PlayerDTO], queue: Seq[PlayerDTO]) object PlayerQueueDTO { - implicit val queueFormat: OFormat[PlayerQueueDTO] = Json.format[PlayerQueueDTO] - def apply(logic: GameLogic): PlayerQueueDTO = { val currentPlayerDTO = logic.getCurrentPlayer.map(PlayerDTO(_)) val queueDTO = logic.getPlayerQueue.map(_.duplicate().flatMap(player => Some(PlayerDTO(player))).toSeq) diff --git a/knockoutwhistweb/app/dto/subDTO/PodiumPlayerDTO.scala b/knockoutwhistweb/app/dto/subDTO/PodiumPlayerDTO.scala new file mode 100644 index 0000000..41e54ad --- /dev/null +++ b/knockoutwhistweb/app/dto/subDTO/PodiumPlayerDTO.scala @@ -0,0 +1,47 @@ +package dto.subDTO + +import de.knockoutwhist.control.GameLogic +import de.knockoutwhist.player.AbstractPlayer +import de.knockoutwhist.rounds.Match + +case class PodiumPlayerDTO( + player: PlayerDTO, + position: Int, + roundsWon: Int, + tricksWon: Int + ) + +object PodiumPlayerDTO { + + def apply(gameLogic: GameLogic, player: AbstractPlayer): PodiumPlayerDTO = { + val matchImplOpt = gameLogic.getCurrentMatch + if (matchImplOpt.isEmpty) { + throw new IllegalStateException("No current match available in game logic") + } + val matchImpl: Match = matchImplOpt.get + var roundsWon = 0 + var tricksWon = 0 + for (round <- matchImpl.roundlist) { + if (round.winner.contains(player)) { + roundsWon += 1 + } + for (trick <- round.tricklist) { + if (trick.winner.contains(player)) { + tricksWon += 1 + } + } + } + + PodiumPlayerDTO( + player = PlayerDTO(player), + position = if (gameLogic.getWinner.contains(player)) { + 1 + } else { + 2 + }, + roundsWon = roundsWon, + tricksWon = tricksWon + ) + } + +} \ No newline at end of file diff --git a/knockoutwhistweb/app/dto/RoundDTO.scala b/knockoutwhistweb/app/dto/subDTO/RoundDTO.scala similarity index 53% rename from knockoutwhistweb/app/dto/RoundDTO.scala rename to knockoutwhistweb/app/dto/subDTO/RoundDTO.scala index ab5d91b..60ef40f 100644 --- a/knockoutwhistweb/app/dto/RoundDTO.scala +++ b/knockoutwhistweb/app/dto/subDTO/RoundDTO.scala @@ -1,21 +1,17 @@ -package dto +package dto.subDTO import de.knockoutwhist.cards.Card import de.knockoutwhist.cards.CardValue.Ace -import play.api.libs.json.{Json, OFormat} -case class RoundDTO(trumpSuit: CardDTO, firstRound: Boolean, tricklist: List[TrickDTO], winner: Option[PlayerDTO]) +case class RoundDTO(trumpSuit: CardDTO, firstRound: Boolean, tricklist: List[TrickDTO]) object RoundDTO { - implicit val roundFormat: OFormat[RoundDTO] = Json.format[RoundDTO] - def apply(round: de.knockoutwhist.rounds.Round): RoundDTO = { RoundDTO( trumpSuit = CardDTO(Card(Ace, round.trumpSuit)), firstRound = round.firstRound, - tricklist = round.tricklist.map(trick => TrickDTO(trick)), - winner = round.winner.map(player => PlayerDTO(player)) + tricklist = round.tricklist.map(trick => TrickDTO(trick)) ) } diff --git a/knockoutwhistweb/app/dto/TrickDTO.scala b/knockoutwhistweb/app/dto/subDTO/TrickDTO.scala similarity index 78% rename from knockoutwhistweb/app/dto/TrickDTO.scala rename to knockoutwhistweb/app/dto/subDTO/TrickDTO.scala index f63ac64..7e68d1e 100644 --- a/knockoutwhistweb/app/dto/TrickDTO.scala +++ b/knockoutwhistweb/app/dto/subDTO/TrickDTO.scala @@ -1,14 +1,11 @@ -package dto +package dto.subDTO import de.knockoutwhist.rounds.Trick -import play.api.libs.json.{Json, OFormat} case class TrickDTO(cards: Map[PlayerDTO, CardDTO], firstCard: Option[CardDTO], winner: Option[PlayerDTO]) object TrickDTO { - implicit val trickFormat: OFormat[TrickDTO] = Json.format[TrickDTO] - def apply(trick: Trick): TrickDTO = { TrickDTO( cards = trick.cards.map { case (card, player) => PlayerDTO(player) -> CardDTO(card) }, diff --git a/knockoutwhistweb/app/dto/subDTO/UserDTO.scala b/knockoutwhistweb/app/dto/subDTO/UserDTO.scala new file mode 100644 index 0000000..a77e234 --- /dev/null +++ b/knockoutwhistweb/app/dto/subDTO/UserDTO.scala @@ -0,0 +1,17 @@ +package dto.subDTO + +import model.sessions.UserSession + +case class UserDTO(id: String, username: String, host: Boolean = false) + +object UserDTO { + + def apply(user: UserSession): UserDTO = { + UserDTO( + id = user.id.toString, + username = user.name, + host = user.host + ) + } + +} \ No newline at end of file diff --git a/knockoutwhistweb/app/util/WebsocketEventMapper.scala b/knockoutwhistweb/app/util/WebsocketEventMapper.scala index e8883bd..2a2f011 100644 --- a/knockoutwhistweb/app/util/WebsocketEventMapper.scala +++ b/knockoutwhistweb/app/util/WebsocketEventMapper.scala @@ -1,25 +1,37 @@ package util +import de.knockoutwhist.control.GameState +import de.knockoutwhist.control.GameState.{FinishedMatch, InGame, Lobby, SelectTrump, TieBreak} import de.knockoutwhist.utils.events.SimpleEvent +import dto.subDTO.{CardDTO, HandDTO, PlayerDTO, PlayerQueueDTO, PodiumPlayerDTO, RoundDTO, TrickDTO, UserDTO} +import dto.{GameInfoDTO, LobbyInfoDTO, TieInfoDTO, TrumpInfoDTO, WonInfoDTO} import model.sessions.UserSession -import play.api.libs.json.{JsValue, Json} +import play.api.libs.json.{JsValue, Json, OFormat} import tools.jackson.databind.json.JsonMapper import tools.jackson.module.scala.ScalaModule import util.mapper.* object WebsocketEventMapper { - private val scalaModule = ScalaModule.builder() - .addAllBuiltinModules() - .supportScala3Classes(true) - .build() + implicit val cardFormat: OFormat[CardDTO] = Json.format[CardDTO] + implicit val handFormat: OFormat[HandDTO] = Json.format[HandDTO] + implicit val playerFormat: OFormat[PlayerDTO] = Json.format[PlayerDTO] + implicit val queueFormat: OFormat[PlayerQueueDTO] = Json.format[PlayerQueueDTO] + implicit val podiumPlayerFormat: OFormat[PodiumPlayerDTO] = Json.format[PodiumPlayerDTO] + implicit val roundFormat: OFormat[RoundDTO] = Json.format[RoundDTO] + implicit val trickFormat: OFormat[TrickDTO] = Json.format[TrickDTO] + implicit val userFormat: OFormat[UserDTO] = Json.format[UserDTO] - private val mapper = JsonMapper.builder().addModule(scalaModule).build() - - private var customMappers: Map[String,SimpleEventMapper[SimpleEvent]] = Map() + implicit val gameInfoDTOFormat: OFormat[GameInfoDTO] = Json.format[GameInfoDTO] + implicit val lobbyFormat: OFormat[LobbyInfoDTO] = Json.format[LobbyInfoDTO] + implicit val tieInfoFormat: OFormat[TieInfoDTO] = Json.format[TieInfoDTO] + implicit val trumpInfoFormat: OFormat[TrumpInfoDTO] = Json.format[TrumpInfoDTO] + implicit val wonInfoDTOFormat: OFormat[WonInfoDTO] = Json.format[WonInfoDTO] + + private var specialMappers: Map[String,SimpleEventMapper[SimpleEvent]] = Map() private def registerCustomMapper[T <: SimpleEvent](mapper: SimpleEventMapper[T]): Unit = { - customMappers = customMappers + (mapper.id -> mapper.asInstanceOf[SimpleEventMapper[SimpleEvent]]) + specialMappers = specialMappers + (mapper.id -> mapper.asInstanceOf[SimpleEventMapper[SimpleEvent]]) } // Register all custom mappers here @@ -37,19 +49,28 @@ object WebsocketEventMapper { registerCustomMapper(TurnEventMapper) def toJson(obj: SimpleEvent, session: UserSession): JsValue = { - val data: Option[JsValue] = if (customMappers.contains(obj.id)) { - Some(customMappers(obj.id).toJson(obj, session)) + val data: Option[JsValue] = if (specialMappers.contains(obj.id)) { + Some(specialMappers(obj.id).toJson(obj, session)) }else { None } - if (data.isEmpty) { - return Json.obj() - } Json.obj( "id" -> ("request-" + java.util.UUID.randomUUID().toString), "event" -> obj.id, + "state" -> toJson(session), "data" -> data ) } + def toJson(session: UserSession): JsValue = { + session.gameLobby.getLogic.getCurrentState match { + case Lobby => Json.toJson(LobbyInfoDTO(session.gameLobby, session.user)) + case InGame => Json.toJson(GameInfoDTO(session.gameLobby, session.user)) + case SelectTrump => Json.toJson(TrumpInfoDTO(session.gameLobby, session.user)) + case TieBreak => Json.toJson(TieInfoDTO(session.gameLobby, session.user)) + case FinishedMatch => Json.toJson(WonInfoDTO(session.gameLobby, session.user)) + case _ => Json.obj() + } + } + }