Compare commits

...

2 Commits
4.8.1 ... 4.9.0

Author SHA1 Message Date
TeamCity
33efc4e107 ci: bump version to v4.9.0 2025-12-06 09:19:38 +00:00
8d697fd311 feat: BAC-30 Implement Jackson Mapping via DTOs (#102)
Reviewed-on: #102
Co-authored-by: Janis <janis.e.20@gmx.de>
Co-committed-by: Janis <janis.e.20@gmx.de>
2025-12-06 10:17:04 +01:00
18 changed files with 247 additions and 72 deletions

View File

@@ -214,3 +214,8 @@
### Bug Fixes
* BAC-29 Implement Mappers for Common Classes ([#101](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/101)) ([270f44c](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/270f44cc1f3447ffcc33fb19a47c52391c69972b))
## (2025-12-06)
### Features
* BAC-30 Implement Jackson Mapping via DTOs ([#102](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/102)) ([8d697fd](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/8d697fd311478cf792b4631377de4522ecbda9f7))

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

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

View File

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

View File

@@ -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(

View File

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

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

View File

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

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

View File

@@ -1,3 +1,3 @@
MAJOR=4
MINOR=8
PATCH=1
MINOR=9
PATCH=0