feat: BAC-30 Implement Jackson Mapping via DTOs #102

Merged
Janis merged 1 commits from feat/BAC-30 into main 2025-12-06 10:17:05 +01:00
16 changed files with 240 additions and 70 deletions
Showing only changes of commit a8ccebb504 - Show all commits

View File

@@ -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(_))
)
}
}

View File

@@ -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,
)
}
}

View File

@@ -1,19 +1,25 @@
package dto package dto
import de.knockoutwhist.control.sublogic.PlayerTieLogic import dto.subDTO.PlayerDTO
import play.api.libs.json.{Json, OFormat} 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 { object TieInfoDTO {
implicit val tieInfoFormat: OFormat[TieInfoDTO] = Json.format[TieInfoDTO] def apply(lobby: GameLobby, user: User): TieInfoDTO = {
val selfPlayer = Try {
Some(lobby.getPlayerByUser(user))
}.getOrElse(None)
def apply(tieInput: PlayerTieLogic): Unit = {
TieInfoDTO( TieInfoDTO(
currentPlayer = tieInput.currentTiePlayer().map(PlayerDTO.apply), currentPlayer = lobby.logic.playerTieLogic.currentTiePlayer().map(PlayerDTO.apply),
tiedPlayers = tieInput.getTiedPlayers.map(PlayerDTO.apply), self = selfPlayer.map(PlayerDTO.apply),
highestAmount = tieInput.highestAllowedNumber() tiedPlayers = lobby.logic.playerTieLogic.getTiedPlayers.map(PlayerDTO.apply),
highestAmount = lobby.logic.playerTieLogic.highestAllowedNumber()
) )
} }

View File

@@ -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(_))
)
}
}

View File

@@ -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
)
}
}

View File

@@ -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
)
}
}

View File

@@ -1,7 +1,6 @@
package dto package dto.subDTO
import de.knockoutwhist.cards.Card import de.knockoutwhist.cards.Card
import play.api.libs.json.{Json, OFormat}
import util.WebUIUtils import util.WebUIUtils
case class CardDTO(identifier: String, path: String, idx: Int) { case class CardDTO(identifier: String, path: String, idx: Int) {
@@ -14,8 +13,6 @@ case class CardDTO(identifier: String, path: String, idx: Int) {
object CardDTO { object CardDTO {
implicit val cardFormat: OFormat[CardDTO] = Json.format[CardDTO]
def apply(card: Card, index: Int = 0): CardDTO = { def apply(card: Card, index: Int = 0): CardDTO = {
CardDTO( CardDTO(
identifier = WebUIUtils.cardtoString(card), identifier = WebUIUtils.cardtoString(card),

View File

@@ -1,14 +1,11 @@
package dto package dto.subDTO
import de.knockoutwhist.cards.Hand import de.knockoutwhist.cards.Hand
import play.api.libs.json.{Json, OFormat}
case class HandDTO(card: List[CardDTO]) case class HandDTO(card: List[CardDTO])
object HandDTO { object HandDTO {
implicit val handFormat: OFormat[HandDTO] = Json.format[HandDTO]
def apply(hand: Hand): HandDTO = { def apply(hand: Hand): HandDTO = {
HandDTO( HandDTO(
card = hand.cards.zipWithIndex.map { case (card, idx) => CardDTO(card, idx) } card = hand.cards.zipWithIndex.map { case (card, idx) => CardDTO(card, idx) }

View File

@@ -1,14 +1,11 @@
package dto package dto.subDTO
import de.knockoutwhist.player.AbstractPlayer import de.knockoutwhist.player.AbstractPlayer
import play.api.libs.json.{Json, OFormat}
case class PlayerDTO(id: String, name: String, dogLife: Boolean) case class PlayerDTO(id: String, name: String, dogLife: Boolean)
object PlayerDTO { object PlayerDTO {
implicit val playerFormat: OFormat[PlayerDTO] = Json.format[PlayerDTO]
def apply(player: AbstractPlayer): PlayerDTO = { def apply(player: AbstractPlayer): PlayerDTO = {
PlayerDTO( PlayerDTO(
id = player.id.toString, id = player.id.toString,

View File

@@ -1,14 +1,11 @@
package dto package dto.subDTO
import de.knockoutwhist.control.GameLogic import de.knockoutwhist.control.GameLogic
import play.api.libs.json.{Json, OFormat}
case class PlayerQueueDTO(currentPlayer: Option[PlayerDTO], queue: Seq[PlayerDTO]) case class PlayerQueueDTO(currentPlayer: Option[PlayerDTO], queue: Seq[PlayerDTO])
object PlayerQueueDTO { object PlayerQueueDTO {
implicit val queueFormat: OFormat[PlayerQueueDTO] = Json.format[PlayerQueueDTO]
def apply(logic: GameLogic): PlayerQueueDTO = { def apply(logic: GameLogic): PlayerQueueDTO = {
val currentPlayerDTO = logic.getCurrentPlayer.map(PlayerDTO(_)) val currentPlayerDTO = logic.getCurrentPlayer.map(PlayerDTO(_))
val queueDTO = logic.getPlayerQueue.map(_.duplicate().flatMap(player => Some(PlayerDTO(player))).toSeq) val queueDTO = logic.getPlayerQueue.map(_.duplicate().flatMap(player => Some(PlayerDTO(player))).toSeq)

View File

@@ -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
)
}
}

View File

@@ -1,21 +1,17 @@
package dto package dto.subDTO
import de.knockoutwhist.cards.Card import de.knockoutwhist.cards.Card
import de.knockoutwhist.cards.CardValue.Ace 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 { object RoundDTO {
implicit val roundFormat: OFormat[RoundDTO] = Json.format[RoundDTO]
def apply(round: de.knockoutwhist.rounds.Round): RoundDTO = { def apply(round: de.knockoutwhist.rounds.Round): RoundDTO = {
RoundDTO( RoundDTO(
trumpSuit = CardDTO(Card(Ace, round.trumpSuit)), trumpSuit = CardDTO(Card(Ace, round.trumpSuit)),
firstRound = round.firstRound, firstRound = round.firstRound,
tricklist = round.tricklist.map(trick => TrickDTO(trick)), tricklist = round.tricklist.map(trick => TrickDTO(trick))
winner = round.winner.map(player => PlayerDTO(player))
) )
} }

View File

@@ -1,14 +1,11 @@
package dto package dto.subDTO
import de.knockoutwhist.rounds.Trick 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]) case class TrickDTO(cards: Map[PlayerDTO, CardDTO], firstCard: Option[CardDTO], winner: Option[PlayerDTO])
object TrickDTO { object TrickDTO {
implicit val trickFormat: OFormat[TrickDTO] = Json.format[TrickDTO]
def apply(trick: Trick): TrickDTO = { def apply(trick: Trick): TrickDTO = {
TrickDTO( TrickDTO(
cards = trick.cards.map { case (card, player) => PlayerDTO(player) -> CardDTO(card) }, cards = trick.cards.map { case (card, player) => PlayerDTO(player) -> CardDTO(card) },

View File

@@ -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
)
}
}

View File

@@ -1,25 +1,37 @@
package util package util
import de.knockoutwhist.control.GameState
import de.knockoutwhist.control.GameState.{FinishedMatch, InGame, Lobby, SelectTrump, TieBreak}
import de.knockoutwhist.utils.events.SimpleEvent 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 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.databind.json.JsonMapper
import tools.jackson.module.scala.ScalaModule import tools.jackson.module.scala.ScalaModule
import util.mapper.* import util.mapper.*
object WebsocketEventMapper { object WebsocketEventMapper {
private val scalaModule = ScalaModule.builder() implicit val cardFormat: OFormat[CardDTO] = Json.format[CardDTO]
.addAllBuiltinModules() implicit val handFormat: OFormat[HandDTO] = Json.format[HandDTO]
.supportScala3Classes(true) implicit val playerFormat: OFormat[PlayerDTO] = Json.format[PlayerDTO]
.build() 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() 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 customMappers: Map[String,SimpleEventMapper[SimpleEvent]] = Map() private var specialMappers: Map[String,SimpleEventMapper[SimpleEvent]] = Map()
private def registerCustomMapper[T <: SimpleEvent](mapper: SimpleEventMapper[T]): Unit = { 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 // Register all custom mappers here
@@ -37,19 +49,28 @@ object WebsocketEventMapper {
registerCustomMapper(TurnEventMapper) registerCustomMapper(TurnEventMapper)
def toJson(obj: SimpleEvent, session: UserSession): JsValue = { def toJson(obj: SimpleEvent, session: UserSession): JsValue = {
val data: Option[JsValue] = if (customMappers.contains(obj.id)) { val data: Option[JsValue] = if (specialMappers.contains(obj.id)) {
Some(customMappers(obj.id).toJson(obj, session)) Some(specialMappers(obj.id).toJson(obj, session))
}else { }else {
None None
} }
if (data.isEmpty) {
return Json.obj()
}
Json.obj( Json.obj(
"id" -> ("request-" + java.util.UUID.randomUUID().toString), "id" -> ("request-" + java.util.UUID.randomUUID().toString),
"event" -> obj.id, "event" -> obj.id,
"state" -> toJson(session),
"data" -> data "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()
}
}
} }