update ktlint rules

This commit is contained in:
2025-03-14 03:23:16 +01:00
parent 492981bed0
commit b4234a9b37
97 changed files with 2392 additions and 2359 deletions

View File

@@ -9,21 +9,21 @@ import kotlinx.coroutines.channels.SendChannel
typealias ErrorNotifier = suspend (String) -> Unit
fun errorNotifier(
command: GameCommand,
channel: SendChannel<Notification>,
command: GameCommand,
channel: SendChannel<Notification>,
): ErrorNotifier =
{
val logger = KotlinLogging.logger { }
ErrorNotification(message = it)
.let { notification ->
logger.atWarn {
message = "Notification ERROR sent: ${notification.message}"
payload =
mapOf(
"notification" to notification,
"command" to command,
)
}
channel.send(notification)
}
}
{
val logger = KotlinLogging.logger { }
ErrorNotification(message = it)
.let { notification ->
logger.atWarn {
message = "Notification ERROR sent: ${notification.message}"
payload =
mapOf(
"notification" to notification,
"command" to command,
)
}
channel.send(notification)
}
}

View File

@@ -15,32 +15,33 @@ import kotlinx.coroutines.channels.SendChannel
* This action can be executing an action and produce a new [GameEvent] after verification.
*/
class GameCommandHandler(
private val commandStreamChannel: CommandStreamChannelBuilder<GameCommand>,
private val runner: GameCommandRunner,
private val commandStreamChannel: CommandStreamChannelBuilder<GameCommand>,
private val runner: GameCommandRunner,
) {
private val logger = KotlinLogging.logger { }
private val logger = KotlinLogging.logger { }
/**
* Init the handler
*/
suspend fun handle(
player: Player,
incomingCommandChannel: ReceiveChannel<GameCommand>,
outgoingErrorChannelNotification: SendChannel<Notification>,
) = commandStreamChannel(incomingCommandChannel)
.process { command ->
if (command.payload.player.id != player.id) {
logger.atWarn {
message = "Handle command Refuse, the player of the command is not the same: $command"
payload = mapOf("command" to command)
}
nack()
} else {
logger.atInfo {
message = "Handle command: $command"
payload = mapOf("command" to command)
}
runner.run(command, outgoingErrorChannelNotification)
}
/**
* Init the handler
*/
suspend fun handle(
player: Player,
incomingCommandChannel: ReceiveChannel<GameCommand>,
outgoingErrorChannelNotification: SendChannel<Notification>,
) =
commandStreamChannel(incomingCommandChannel)
.process { command ->
if (command.payload.player.id != player.id) {
logger.atWarn {
message = "Handle command Refuse, the player of the command is not the same: $command"
payload = mapOf("command" to command)
}
nack()
} else {
logger.atInfo {
message = "Handle command: $command"
payload = mapOf("command" to command)
}
runner.run(command, outgoingErrorChannelNotification)
}
}
}

View File

@@ -18,29 +18,29 @@ import kotlinx.coroutines.launch
@DelicateCoroutinesApi
fun Route.gameSocket(
playerNotificationListener: PlayerNotificationEventListener,
commandHandler: GameCommandHandler,
playerNotificationListener: PlayerNotificationEventListener,
commandHandler: GameCommandHandler,
) {
authenticate {
webSocket("/game") {
val currentPlayer = call.getPlayer()
val outgoingFrameChannel: SendChannel<Notification> = fromFrameChannel(outgoing)
GlobalScope.launch {
commandHandler.handle(
currentPlayer,
toObjectChannel(incoming),
outgoingFrameChannel,
)
}
playerNotificationListener.startListening(outgoingFrameChannel, currentPlayer)
}
authenticate {
webSocket("/game") {
val currentPlayer = call.getPlayer()
val outgoingFrameChannel: SendChannel<Notification> = fromFrameChannel(outgoing)
GlobalScope.launch {
commandHandler.handle(
currentPlayer,
toObjectChannel(incoming),
outgoingFrameChannel,
)
}
playerNotificationListener.startListening(outgoingFrameChannel, currentPlayer)
}
}
}
private fun ApplicationCall.getPlayer() =
principal<JWTPrincipal>()!!.run {
Player(
id = payload.getClaim("playerid").asString(),
name = payload.getClaim("username").asString(),
)
}
principal<JWTPrincipal>()!!.run {
Player(
id = payload.getClaim("playerid").asString(),
name = payload.getClaim("username").asString(),
)
}

View File

@@ -11,21 +11,21 @@ import eventDemo.app.notification.Notification
import kotlinx.coroutines.channels.SendChannel
class GameCommandRunner(
private val eventHandler: GameEventHandler,
private val gameStateRepository: GameStateRepository,
private val eventHandler: GameEventHandler,
private val gameStateRepository: GameStateRepository,
) {
suspend fun run(
command: GameCommand,
outgoingErrorChannelNotification: SendChannel<Notification>,
) {
val gameState = gameStateRepository.getLast(command.payload.aggregateId)
val errorNotifier = errorNotifier(command, outgoingErrorChannelNotification)
suspend fun run(
command: GameCommand,
outgoingErrorChannelNotification: SendChannel<Notification>,
) {
val gameState = gameStateRepository.getLast(command.payload.aggregateId)
val errorNotifier = errorNotifier(command, outgoingErrorChannelNotification)
when (command) {
is IWantToPlayCardCommand -> command.run(gameState, errorNotifier, this.eventHandler)
is IamReadyToPlayCommand -> command.run(gameState, errorNotifier, this.eventHandler)
is IWantToJoinTheGameCommand -> command.run(gameState, errorNotifier, this.eventHandler)
is ICantPlayCommand -> command.run(gameState, errorNotifier, this.eventHandler)
}
when (command) {
is IWantToPlayCardCommand -> command.run(gameState, errorNotifier, this.eventHandler)
is IamReadyToPlayCommand -> command.run(gameState, errorNotifier, this.eventHandler)
is IWantToJoinTheGameCommand -> command.run(gameState, errorNotifier, this.eventHandler)
is ICantPlayCommand -> command.run(gameState, errorNotifier, this.eventHandler)
}
}
}

View File

@@ -9,5 +9,5 @@ import kotlinx.coroutines.channels.ReceiveChannel
* A stream to publish and read the game command.
*/
class GameCommandStream(
incoming: ReceiveChannel<GameCommand>,
incoming: ReceiveChannel<GameCommand>,
) : CommandStream<GameCommand> by CommandStreamChannel(incoming)

View File

@@ -7,11 +7,11 @@ import kotlinx.serialization.Serializable
@Serializable
sealed interface GameCommand : Command {
val payload: Payload
val payload: Payload
@Serializable
sealed interface Payload {
val aggregateId: GameId
val player: Player
}
@Serializable
sealed interface Payload {
val aggregateId: GameId
val player: Player
}
}

View File

@@ -14,39 +14,39 @@ import kotlinx.serialization.Serializable
*/
@Serializable
data class ICantPlayCommand(
override val payload: Payload,
override val payload: Payload,
) : GameCommand {
override val id: CommandId = CommandId()
override val id: CommandId = CommandId()
@Serializable
data class Payload(
override val aggregateId: GameId,
override val player: Player,
) : GameCommand.Payload
@Serializable
data class Payload(
override val aggregateId: GameId,
override val player: Player,
) : GameCommand.Payload
suspend fun run(
state: GameState,
playerErrorNotifier: ErrorNotifier,
eventHandler: GameEventHandler,
) {
if (state.currentPlayerTurn != payload.player) {
playerErrorNotifier("Its not your turn!")
return
}
val playableCards = state.playableCards(payload.player)
if (playableCards.isEmpty()) {
val takenCard = state.deck.stack.first()
eventHandler.handle(payload.aggregateId) {
PlayerHavePassEvent(
aggregateId = payload.aggregateId,
player = payload.player,
takenCard = takenCard,
version = it,
)
}
} else {
playerErrorNotifier("You can and must play one card, like ${playableCards.first()::class.simpleName}")
}
suspend fun run(
state: GameState,
playerErrorNotifier: ErrorNotifier,
eventHandler: GameEventHandler,
) {
if (state.currentPlayerTurn != payload.player) {
playerErrorNotifier("Its not your turn!")
return
}
val playableCards = state.playableCards(payload.player)
if (playableCards.isEmpty()) {
val takenCard = state.deck.stack.first()
eventHandler.handle(payload.aggregateId) {
PlayerHavePassEvent(
aggregateId = payload.aggregateId,
player = payload.player,
takenCard = takenCard,
version = it,
)
}
} else {
playerErrorNotifier("You can and must play one card, like ${playableCards.first()::class.simpleName}")
}
}
}

View File

@@ -14,31 +14,31 @@ import kotlinx.serialization.Serializable
*/
@Serializable
data class IWantToJoinTheGameCommand(
override val payload: Payload,
override val payload: Payload,
) : GameCommand {
override val id: CommandId = CommandId()
override val id: CommandId = CommandId()
@Serializable
data class Payload(
override val aggregateId: GameId,
override val player: Player,
) : GameCommand.Payload
@Serializable
data class Payload(
override val aggregateId: GameId,
override val player: Player,
) : GameCommand.Payload
suspend fun run(
state: GameState,
playerErrorNotifier: ErrorNotifier,
eventHandler: GameEventHandler,
) {
if (!state.isStarted) {
eventHandler.handle(payload.aggregateId) {
NewPlayerEvent(
aggregateId = payload.aggregateId,
player = payload.player,
version = it,
)
}
} else {
playerErrorNotifier("The game is already started")
}
suspend fun run(
state: GameState,
playerErrorNotifier: ErrorNotifier,
eventHandler: GameEventHandler,
) {
if (!state.isStarted) {
eventHandler.handle(payload.aggregateId) {
NewPlayerEvent(
aggregateId = payload.aggregateId,
player = payload.player,
version = it,
)
}
} else {
playerErrorNotifier("The game is already started")
}
}
}

View File

@@ -15,42 +15,42 @@ import kotlinx.serialization.Serializable
*/
@Serializable
data class IWantToPlayCardCommand(
override val payload: Payload,
override val payload: Payload,
) : GameCommand {
override val id: CommandId = CommandId()
override val id: CommandId = CommandId()
@Serializable
data class Payload(
override val aggregateId: GameId,
override val player: Player,
val card: Card,
) : GameCommand.Payload
@Serializable
data class Payload(
override val aggregateId: GameId,
override val player: Player,
val card: Card,
) : GameCommand.Payload
suspend fun run(
state: GameState,
playerErrorNotifier: ErrorNotifier,
eventHandler: GameEventHandler,
) {
if (!state.isStarted) {
playerErrorNotifier("The game is Not started")
return
}
if (state.currentPlayerTurn != payload.player) {
playerErrorNotifier("Its not your turn!")
return
}
if (state.canBePlayThisCard(payload.player, payload.card)) {
eventHandler.handle(payload.aggregateId) {
CardIsPlayedEvent(
aggregateId = payload.aggregateId,
card = payload.card,
player = payload.player,
version = it,
)
}
} else {
playerErrorNotifier("You cannot play this card")
}
suspend fun run(
state: GameState,
playerErrorNotifier: ErrorNotifier,
eventHandler: GameEventHandler,
) {
if (!state.isStarted) {
playerErrorNotifier("The game is Not started")
return
}
if (state.currentPlayerTurn != payload.player) {
playerErrorNotifier("Its not your turn!")
return
}
if (state.canBePlayThisCard(payload.player, payload.card)) {
eventHandler.handle(payload.aggregateId) {
CardIsPlayedEvent(
aggregateId = payload.aggregateId,
card = payload.card,
player = payload.player,
version = it,
)
}
} else {
playerErrorNotifier("You cannot play this card")
}
}
}

View File

@@ -14,38 +14,38 @@ import kotlinx.serialization.Serializable
*/
@Serializable
data class IamReadyToPlayCommand(
override val payload: Payload,
override val payload: Payload,
) : GameCommand {
override val id: CommandId = CommandId()
override val id: CommandId = CommandId()
@Serializable
data class Payload(
override val aggregateId: GameId,
override val player: Player,
) : GameCommand.Payload
@Serializable
data class Payload(
override val aggregateId: GameId,
override val player: Player,
) : GameCommand.Payload
suspend fun run(
state: GameState,
playerErrorNotifier: ErrorNotifier,
eventHandler: GameEventHandler,
) {
val playerExist: Boolean = state.players.contains(payload.player)
val playerIsAlreadyReady: Boolean = state.readyPlayers.contains(payload.player)
suspend fun run(
state: GameState,
playerErrorNotifier: ErrorNotifier,
eventHandler: GameEventHandler,
) {
val playerExist: Boolean = state.players.contains(payload.player)
val playerIsAlreadyReady: Boolean = state.readyPlayers.contains(payload.player)
if (state.isStarted) {
playerErrorNotifier("The game is already started")
} else if (!playerExist) {
playerErrorNotifier("You are not in the game")
} else if (playerIsAlreadyReady) {
playerErrorNotifier("You are already ready")
} else {
eventHandler.handle(payload.aggregateId) {
PlayerReadyEvent(
aggregateId = payload.aggregateId,
player = payload.player,
version = it,
)
}
}
if (state.isStarted) {
playerErrorNotifier("The game is already started")
} else if (!playerExist) {
playerErrorNotifier("You are not in the game")
} else if (playerIsAlreadyReady) {
playerErrorNotifier("You are already ready")
} else {
eventHandler.handle(payload.aggregateId) {
PlayerReadyEvent(
aggregateId = payload.aggregateId,
player = payload.player,
version = it,
)
}
}
}
}

View File

@@ -10,100 +10,100 @@ import java.util.UUID
*/
@Serializable
sealed interface Card {
val id: UUID
val id: UUID
/**
* The color of a card
*/
@Serializable
enum class Color {
Blue,
Red,
Yellow,
Green,
}
/**
* The color of a card
*/
@Serializable
enum class Color {
Blue,
Red,
Yellow,
Green,
}
sealed interface ColorCard : Card {
val color: Color
}
sealed interface ColorCard : Card {
val color: Color
}
/**
* A play card with color and number
*/
@Serializable
@SerialName("Simple")
data class NumericCard(
val number: Int,
override val color: Color,
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
) : Card,
ColorCard
/**
* A play card with color and number
*/
@Serializable
@SerialName("Simple")
data class NumericCard(
val number: Int,
override val color: Color,
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
) : Card,
ColorCard
sealed interface Special : Card
sealed interface Special : Card
/**
* A revert card to revert the order of the turn.
*/
@Serializable
@SerialName("Reverse")
data class ReverseCard(
override val color: Color,
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
) : Special,
ColorCard
/**
* A revert card to revert the order of the turn.
*/
@Serializable
@SerialName("Reverse")
data class ReverseCard(
override val color: Color,
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
) : Special,
ColorCard
sealed interface PassTurnCard : Card
sealed interface PassTurnCard : Card
/**
* A pass card to pass the turn of the next player.
*/
@Serializable
@SerialName("Pass")
data class PassCard(
override val color: Color,
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
) : Special,
ColorCard,
PassTurnCard
/**
* A pass card to pass the turn of the next player.
*/
@Serializable
@SerialName("Pass")
data class PassCard(
override val color: Color,
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
) : Special,
ColorCard,
PassTurnCard
/**
* A play card to force the next player to take 2 card and pass the turn.
*/
@Serializable
@SerialName("Plus2")
data class Plus2Card(
override val color: Color,
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
) : Special,
ColorCard,
PassTurnCard
/**
* A play card to force the next player to take 2 card and pass the turn.
*/
@Serializable
@SerialName("Plus2")
data class Plus2Card(
override val color: Color,
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
) : Special,
ColorCard,
PassTurnCard
sealed interface AllColorCard : Card
sealed interface AllColorCard : Card
/**
* A play card to force the next player to take 4 card and pass the turn.
*/
@Serializable
@SerialName("Plus4")
class Plus4Card(
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
) : Special,
AllColorCard,
PassTurnCard
/**
* A play card to force the next player to take 4 card and pass the turn.
*/
@Serializable
@SerialName("Plus4")
class Plus4Card(
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
) : Special,
AllColorCard,
PassTurnCard
/**
* A play card to change the color.
*/
@Serializable
@SerialName("ChangeColor")
class ChangeColorCard(
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
) : Special,
AllColorCard
/**
* A play card to change the color.
*/
@Serializable
@SerialName("ChangeColor")
class ChangeColorCard(
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
) : Special,
AllColorCard
}

View File

@@ -4,119 +4,130 @@ import kotlinx.serialization.Serializable
@Serializable
data class Deck(
val stack: Stack = Stack(),
val discard: Discard = Discard(),
val playersHands: PlayersHands = PlayersHands(),
val stack: Stack = Stack(),
val discard: Discard = Discard(),
val playersHands: PlayersHands = PlayersHands(),
) {
constructor(players: Set<Player>) :
this(playersHands = PlayersHands(players))
constructor(players: Set<Player>) :
this(playersHands = PlayersHands(players))
fun shuffle(): Deck = copy(stack = stack.shuffle())
fun shuffle(): Deck =
copy(stack = stack.shuffle())
fun placeFirstCardOnDiscard(): Deck {
val takenCard = stack.first()
return copy(
stack = stack - takenCard,
discard = discard + takenCard,
)
fun placeFirstCardOnDiscard(): Deck {
val takenCard = stack.first()
return copy(
stack = stack - takenCard,
discard = discard + takenCard,
)
}
fun takeOneCardFromStackTo(player: Player): Deck =
takeOne().let { (deck, newPlayerCard) ->
deck.copy(
playersHands = deck.playersHands.addCard(player, newPlayerCard),
)
}
fun takeOneCardFromStackTo(player: Player): Deck =
takeOne().let { (deck, newPlayerCard) ->
deck.copy(
playersHands = deck.playersHands.addCard(player, newPlayerCard),
)
}
fun putOneCardFromHand(
player: Player,
card: Card,
): Deck =
run {
// Validate parameters
val playerHand =
playersHands.getHand(player)
?: error("No player on this game")
if (playerHand.none { it == card }) {
error("No card exist on the player hand")
}
}.let {
copy(
discard = discard + card,
playersHands = playersHands.removeCard(player, card),
)
}
fun putOneCardFromHand(
player: Player,
card: Card,
): Deck =
run {
// Validate parameters
val playerHand =
playersHands.getHand(player)
?: error("No player on this game")
if (playerHand.none { it == card }) {
error("No card exist on the player hand")
}
fun playerHasNoCardLeft(): List<Player.PlayerId> =
playersHands
.filter { (playerId, hand) -> hand.isEmpty() }
.map { (playerId, hand) -> playerId }
private fun take(n: Int): Pair<Deck, List<Card>> {
val takenCards = stack.take(n)
val newStack = stack.filterNot { takenCards.contains(it) }.toStack()
return Pair(copy(stack = newStack), takenCards)
}
private fun takeOne(): Pair<Deck, Card> =
take(1).let { (deck, cards) -> Pair(deck, cards.first()) }
companion object {
fun newWithoutPlayers(): Deck =
listOf(Card.Color.Red, Card.Color.Blue, Card.Color.Yellow, Card.Color.Green)
.flatMap { color ->
((0..9) + (1..9)).map { Card.NumericCard(it, color) } +
(1..2).map { Card.Plus2Card(color) } +
(1..2).map { Card.ReverseCard(color) } +
(1..2).map { Card.PassCard(color) }
}.let {
copy(
discard = discard + card,
playersHands = playersHands.removeCard(player, card),
)
}
fun playerHasNoCardLeft(): List<Player.PlayerId> =
playersHands
.filter { (playerId, hand) -> hand.isEmpty() }
.map { (playerId, hand) -> playerId }
private fun take(n: Int): Pair<Deck, List<Card>> {
val takenCards = stack.take(n)
val newStack = stack.filterNot { takenCards.contains(it) }.toStack()
return Pair(copy(stack = newStack), takenCards)
}
private fun takeOne(): Pair<Deck, Card> = take(1).let { (deck, cards) -> Pair(deck, cards.first()) }
companion object {
fun newWithoutPlayers(): Deck =
listOf(Card.Color.Red, Card.Color.Blue, Card.Color.Yellow, Card.Color.Green)
.flatMap { color ->
((0..9) + (1..9)).map { Card.NumericCard(it, color) } +
(1..2).map { Card.Plus2Card(color) } +
(1..2).map { Card.ReverseCard(color) } +
(1..2).map { Card.PassCard(color) }
}.let {
it + (1..4).map { Card.Plus4Card() }
}.toStack()
.let { Deck(it) }
}
it + (1..4).map { Card.Plus4Card() }
}.toStack()
.let { Deck(it) }
}
}
fun Deck.initHands(
players: Set<Player>,
handSize: Int = 7,
players: Set<Player>,
handSize: Int = 7,
): Deck {
// Copy cards from stack to the player hands
val deckWithEmptyHands = copy(playersHands = PlayersHands(players))
return players.fold(deckWithEmptyHands) { acc: Deck, player: Player ->
val hand = acc.stack.take(handSize)
val newStack = acc.stack.filterNot { card: Card -> hand.contains(card) }.toStack()
copy(
stack = newStack,
playersHands = acc.playersHands.addCards(player, hand),
)
}
// Copy cards from stack to the player hands
val deckWithEmptyHands = copy(playersHands = PlayersHands(players))
return players.fold(deckWithEmptyHands) { acc: Deck, player: Player ->
val hand = acc.stack.take(handSize)
val newStack = acc.stack.filterNot { card: Card -> hand.contains(card) }.toStack()
copy(
stack = newStack,
playersHands = acc.playersHands.addCards(player, hand),
)
}
}
@JvmInline
@Serializable
value class Stack(
private val cards: Set<Card> = emptySet(),
private val cards: Set<Card> = emptySet(),
) : Set<Card> by cards {
operator fun plus(card: Card): Stack = cards.plus(card).toStack()
operator fun plus(card: Card): Stack =
cards.plus(card).toStack()
operator fun minus(card: Card): Stack = cards.minus(card).toStack()
operator fun minus(card: Card): Stack =
cards.minus(card).toStack()
fun shuffle(): Stack = shuffled().toStack()
fun shuffle(): Stack =
shuffled().toStack()
}
fun List<Card>.toStack(): Stack = Stack(this.toSet())
fun List<Card>.toStack(): Stack =
Stack(this.toSet())
fun Set<Card>.toStack(): Stack = Stack(this)
fun Set<Card>.toStack(): Stack =
Stack(this)
@JvmInline
@Serializable
value class Discard(
private val cards: Set<Card> = emptySet(),
private val cards: Set<Card> = emptySet(),
) : Set<Card> by cards {
operator fun plus(card: Card): Discard = cards.plus(card).toDiscard()
operator fun plus(card: Card): Discard =
cards.plus(card).toDiscard()
operator fun minus(card: Card): Discard = cards.minus(card).toDiscard()
operator fun minus(card: Card): Discard =
cards.minus(card).toDiscard()
}
fun List<Card>.toDiscard(): Discard = Discard(this.toSet())
fun List<Card>.toDiscard(): Discard =
Discard(this.toSet())
fun Set<Card>.toDiscard(): Discard = Discard(this)
fun Set<Card>.toDiscard(): Discard =
Discard(this)

View File

@@ -11,7 +11,8 @@ import java.util.UUID
@JvmInline
@Serializable(with = GameIdSerializer::class)
value class GameId(
override val id: UUID = UUID.randomUUID(),
override val id: UUID = UUID.randomUUID(),
) : AggregateId {
override fun toString(): String = id.toString()
override fun toString(): String =
id.toString()
}

View File

@@ -8,21 +8,22 @@ import java.util.UUID
@Serializable
data class Player(
val name: String,
@Serializable(with = PlayerIdSerializer::class)
val id: PlayerId = PlayerId(UUID.randomUUID()),
val name: String,
@Serializable(with = PlayerIdSerializer::class)
val id: PlayerId = PlayerId(UUID.randomUUID()),
) {
constructor(id: String, name: String) : this(
name,
PlayerId(UUID.fromString(id)),
)
constructor(id: String, name: String) : this(
name,
PlayerId(UUID.fromString(id)),
)
@Serializable
@JvmInline
value class PlayerId(
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
) : AggregateId {
override fun toString(): String = id.toString()
}
@Serializable
@JvmInline
value class PlayerId(
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
) : AggregateId {
override fun toString(): String =
id.toString()
}
}

View File

@@ -5,43 +5,46 @@ import kotlinx.serialization.Serializable
@Serializable
@JvmInline
value class PlayersHands(
private val map: Map<Player.PlayerId, List<Card>> = emptyMap(),
private val map: Map<Player.PlayerId, List<Card>> = emptyMap(),
) : Map<Player.PlayerId, List<Card>> by map {
constructor(players: Set<Player>) :
this(players.map { it.id }.associateWith { emptyList<Card>() }.toPlayersHands())
constructor(players: Set<Player>) :
this(players.map { it.id }.associateWith { emptyList<Card>() }.toPlayersHands())
fun getHand(player: Player): List<Card>? = this[player.id]
fun getHand(player: Player): List<Card>? =
this[player.id]
fun removeCard(
player: Player,
card: Card,
): PlayersHands =
mapValues { (playerId, cards) ->
if (playerId == player.id) {
if (!cards.contains(card)) error("The hand no contain the card")
cards - card
} else {
cards
}
}.toPlayersHands()
fun removeCard(
player: Player,
card: Card,
): PlayersHands =
mapValues { (playerId, cards) ->
if (playerId == player.id) {
if (!cards.contains(card)) error("The hand no contain the card")
cards - card
} else {
cards
}
}.toPlayersHands()
fun addCard(
player: Player,
newCard: Card,
): PlayersHands = addCards(player, listOf(newCard))
fun addCard(
player: Player,
newCard: Card,
): PlayersHands =
addCards(player, listOf(newCard))
fun addCards(
player: Player,
newCards: List<Card>,
): PlayersHands =
mapValues { (p, cards) ->
if (p == player.id) {
if (cards.intersect(newCards).isNotEmpty()) error("The hand already contain the card")
cards + newCards
} else {
cards
}
}.toPlayersHands()
fun addCards(
player: Player,
newCards: List<Card>,
): PlayersHands =
mapValues { (p, cards) ->
if (p == player.id) {
if (cards.intersect(newCards).isNotEmpty()) error("The hand already contain the card")
cards + newCards
} else {
cards
}
}.toPlayersHands()
}
fun Map<Player.PlayerId, List<Card>>.toPlayersHands(): PlayersHands = PlayersHands(this)
fun Map<Player.PlayerId, List<Card>>.toPlayersHands(): PlayersHands =
PlayersHands(this)

View File

@@ -7,10 +7,10 @@ import eventDemo.libs.event.Event
* A stream to publish and read the played card event.
*/
interface EventHandler<E : Event<ID>, ID : AggregateId> {
fun registerProjectionBuilder(builder: (E) -> Unit)
fun registerProjectionBuilder(builder: (E) -> Unit)
fun handle(
aggregateId: ID,
buildEvent: (version: Int) -> E,
): E
fun handle(
aggregateId: ID,
buildEvent: (version: Int) -> E,
): E
}

View File

@@ -5,5 +5,5 @@ import eventDemo.app.event.event.GameEvent
import eventDemo.libs.event.EventBus
class GameEventBus(
bus: EventBus<GameEvent, GameId>,
bus: EventBus<GameEvent, GameId>,
) : EventBus<GameEvent, GameId> by bus

View File

@@ -12,30 +12,30 @@ import kotlin.concurrent.withLock
* A stream to publish and read the played card event.
*/
class GameEventHandler(
private val eventBus: GameEventBus,
private val eventStore: GameEventStore,
private val versionBuilder: VersionBuilder,
private val eventBus: GameEventBus,
private val eventStore: GameEventStore,
private val versionBuilder: VersionBuilder,
) : EventHandler<GameEvent, GameId> {
private val projectionsBuilders: ConcurrentLinkedQueue<(GameEvent) -> Unit> = ConcurrentLinkedQueue()
private val locks: ConcurrentHashMap<GameId, ReentrantLock> = ConcurrentHashMap()
private val projectionsBuilders: ConcurrentLinkedQueue<(GameEvent) -> Unit> = ConcurrentLinkedQueue()
private val locks: ConcurrentHashMap<GameId, ReentrantLock> = ConcurrentHashMap()
override fun registerProjectionBuilder(builder: GameProjectionBuilder) {
projectionsBuilders.add(builder)
}
override fun registerProjectionBuilder(builder: GameProjectionBuilder) {
projectionsBuilders.add(builder)
}
override fun handle(
aggregateId: GameId,
buildEvent: (version: Int) -> GameEvent,
): GameEvent =
locks
.computeIfAbsent(aggregateId) { ReentrantLock() }
.withLock {
buildEvent(versionBuilder.buildNextVersion(aggregateId))
.also { eventStore.publish(it) }
}.also { event ->
projectionsBuilders.forEach { it(event) }
eventBus.publish(event)
}
override fun handle(
aggregateId: GameId,
buildEvent: (version: Int) -> GameEvent,
): GameEvent =
locks
.computeIfAbsent(aggregateId) { ReentrantLock() }
.withLock {
buildEvent(versionBuilder.buildNextVersion(aggregateId))
.also { eventStore.publish(it) }
}.also { event ->
projectionsBuilders.forEach { it(event) }
eventBus.publish(event)
}
}
typealias GameProjectionBuilder = (GameEvent) -> Unit

View File

@@ -8,5 +8,5 @@ import eventDemo.libs.event.EventStore
* A stream to publish and read the played card event.
*/
class GameEventStore(
private val eventStore: EventStore<GameEvent, GameId>,
private val eventStore: EventStore<GameEvent, GameId>,
) : EventStore<GameEvent, GameId> by eventStore

View File

@@ -7,9 +7,9 @@ import eventDemo.libs.event.EventStream
* A stream to publish and read the played card event.
*/
class GameEventStream(
private val eventStream: EventStream<GameEvent>,
private val eventStream: EventStream<GameEvent>,
) : EventStream<GameEvent> by eventStream {
override fun publish(event: GameEvent) {
eventStream.publish(event)
}
override fun publish(event: GameEvent) {
eventStream.publish(event)
}
}

View File

@@ -11,12 +11,12 @@ import java.util.UUID
* An [GameEvent] to represent a played card.
*/
data class CardIsPlayedEvent(
override val aggregateId: GameId,
val card: Card,
override val player: Player,
override val version: Int,
override val aggregateId: GameId,
val card: Card,
override val player: Player,
override val version: Int,
) : GameEvent,
PlayerActionEvent {
override val eventId: UUID = UUID.randomUUID()
override val createdAt: Instant = Clock.System.now()
PlayerActionEvent {
override val eventId: UUID = UUID.randomUUID()
override val createdAt: Instant = Clock.System.now()
}

View File

@@ -10,7 +10,7 @@ import java.util.UUID
*/
@Serializable
sealed interface GameEvent : Event<GameId> {
override val eventId: UUID
override val aggregateId: GameId
override val version: Int
override val eventId: UUID
override val aggregateId: GameId
override val version: Int
}

View File

@@ -12,37 +12,37 @@ import java.util.UUID
* This [GameEvent] is sent when all players are ready.
*/
data class GameStartedEvent(
override val aggregateId: GameId,
val firstPlayer: Player,
val deck: Deck,
override val version: Int,
override val aggregateId: GameId,
val firstPlayer: Player,
val deck: Deck,
override val version: Int,
) : GameEvent {
override val eventId: UUID = UUID.randomUUID()
override val createdAt: Instant = Clock.System.now()
override val eventId: UUID = UUID.randomUUID()
override val createdAt: Instant = Clock.System.now()
companion object {
fun new(
id: GameId,
players: Set<Player>,
shuffleIsDisabled: Boolean = isDisabled,
version: Int,
): GameStartedEvent =
GameStartedEvent(
aggregateId = id,
firstPlayer = if (shuffleIsDisabled) players.first() else players.random(),
deck =
Deck
.newWithoutPlayers()
.let { if (shuffleIsDisabled) it else it.shuffle() }
.initHands(players)
.placeFirstCardOnDiscard(),
version = version,
)
}
companion object {
fun new(
id: GameId,
players: Set<Player>,
shuffleIsDisabled: Boolean = isDisabled,
version: Int,
): GameStartedEvent =
GameStartedEvent(
aggregateId = id,
firstPlayer = if (shuffleIsDisabled) players.first() else players.random(),
deck =
Deck
.newWithoutPlayers()
.let { if (shuffleIsDisabled) it else it.shuffle() }
.initHands(players)
.placeFirstCardOnDiscard(),
version = version,
)
}
}
private var isDisabled = false
internal fun disableShuffleDeck() {
isDisabled = true
isDisabled = true
}

View File

@@ -10,10 +10,10 @@ import java.util.UUID
* An [GameEvent] to represent a new player joining the game.
*/
data class NewPlayerEvent(
override val aggregateId: GameId,
val player: Player,
override val version: Int,
override val aggregateId: GameId,
val player: Player,
override val version: Int,
) : GameEvent {
override val eventId: UUID = UUID.randomUUID()
override val createdAt: Instant = Clock.System.now()
override val eventId: UUID = UUID.randomUUID()
override val createdAt: Instant = Clock.System.now()
}

View File

@@ -3,5 +3,5 @@ package eventDemo.app.event.event
import eventDemo.app.entity.Player
sealed interface PlayerActionEvent : GameEvent {
val player: Player
val player: Player
}

View File

@@ -11,12 +11,12 @@ import java.util.UUID
* This [GameEvent] is sent when a player chose a color.
*/
data class PlayerChoseColorEvent(
override val aggregateId: GameId,
override val player: Player,
val color: Card.Color,
override val version: Int,
override val aggregateId: GameId,
override val player: Player,
val color: Card.Color,
override val version: Int,
) : GameEvent,
PlayerActionEvent {
override val eventId: UUID = UUID.randomUUID()
override val createdAt: Instant = Clock.System.now()
PlayerActionEvent {
override val eventId: UUID = UUID.randomUUID()
override val createdAt: Instant = Clock.System.now()
}

View File

@@ -11,12 +11,12 @@ import java.util.UUID
* This [GameEvent] is sent when a player can play.
*/
data class PlayerHavePassEvent(
override val aggregateId: GameId,
override val player: Player,
val takenCard: Card,
override val version: Int,
override val aggregateId: GameId,
override val player: Player,
val takenCard: Card,
override val version: Int,
) : GameEvent,
PlayerActionEvent {
override val eventId: UUID = UUID.randomUUID()
override val createdAt: Instant = Clock.System.now()
PlayerActionEvent {
override val eventId: UUID = UUID.randomUUID()
override val createdAt: Instant = Clock.System.now()
}

View File

@@ -10,10 +10,10 @@ import java.util.UUID
* This [GameEvent] is sent when a player is ready.
*/
data class PlayerReadyEvent(
override val aggregateId: GameId,
val player: Player,
override val version: Int,
override val aggregateId: GameId,
val player: Player,
override val version: Int,
) : GameEvent {
override val eventId: UUID = UUID.randomUUID()
override val createdAt: Instant = Clock.System.now()
override val eventId: UUID = UUID.randomUUID()
override val createdAt: Instant = Clock.System.now()
}

View File

@@ -10,10 +10,10 @@ import java.util.UUID
* This [GameEvent] is sent when a player is ready.
*/
data class PlayerWinEvent(
override val aggregateId: GameId,
val player: Player,
override val version: Int,
override val aggregateId: GameId,
val player: Player,
override val version: Int,
) : GameEvent {
override val eventId: UUID = UUID.randomUUID()
override val createdAt: Instant = Clock.System.now()
override val eventId: UUID = UUID.randomUUID()
override val createdAt: Instant = Clock.System.now()
}

View File

@@ -8,173 +8,174 @@ import kotlinx.serialization.Serializable
@Serializable
data class GameState(
override val aggregateId: GameId,
override val lastEventVersion: Int = 0,
val players: Set<Player> = emptySet(),
val currentPlayerTurn: Player? = null,
val cardOnCurrentStack: LastCard? = null,
val colorOnCurrentStack: Card.Color? = null,
val direction: Direction = Direction.CLOCKWISE,
val readyPlayers: Set<Player> = emptySet(),
val deck: Deck = Deck(players),
val isStarted: Boolean = false,
val playerWins: Set<Player> = emptySet(),
override val aggregateId: GameId,
override val lastEventVersion: Int = 0,
val players: Set<Player> = emptySet(),
val currentPlayerTurn: Player? = null,
val cardOnCurrentStack: LastCard? = null,
val colorOnCurrentStack: Card.Color? = null,
val direction: Direction = Direction.CLOCKWISE,
val readyPlayers: Set<Player> = emptySet(),
val deck: Deck = Deck(players),
val isStarted: Boolean = false,
val playerWins: Set<Player> = emptySet(),
) : Projection<GameId> {
@Serializable
data class LastCard(
val card: Card,
val player: Player,
)
@Serializable
data class LastCard(
val card: Card,
val player: Player,
)
enum class Direction {
CLOCKWISE,
COUNTER_CLOCKWISE,
;
enum class Direction {
CLOCKWISE,
COUNTER_CLOCKWISE,
;
fun revert(): Direction =
if (this === CLOCKWISE) {
COUNTER_CLOCKWISE
} else {
CLOCKWISE
}
fun revert(): Direction =
if (this === CLOCKWISE) {
COUNTER_CLOCKWISE
} else {
CLOCKWISE
}
}
val isReady: Boolean get() {
return players.size == readyPlayers.size && players.all { readyPlayers.contains(it) }
}
private val currentPlayerIndex: Int? get() {
val i = players.indexOf(currentPlayerTurn)
return if (i == -1) {
null
} else {
i
}
}
private fun nextPlayerIndex(direction: Direction): Int {
if (players.isEmpty()) return 0
return if (direction == Direction.CLOCKWISE) {
sidePlayerIndexClockwise
} else {
sidePlayerIndexCounterClockwise
}
}
fun nextPlayer(direction: Direction): Player =
players.elementAt(nextPlayerIndex(direction))
private val sidePlayerIndexClockwise: Int by lazy {
if (players.isEmpty()) {
0
} else {
((currentPlayerIndex ?: 0) + 1) % players.size
}
}
private val sidePlayerIndexCounterClockwise: Int by lazy {
if (players.isEmpty()) {
0
} else {
((currentPlayerIndex ?: 0) - 1) % players.size
}
}
val nextPlayerTurn: Player? by lazy {
if (players.isEmpty()) {
null
} else {
nextPlayer(direction)
}
}
private val Player.currentIndex: Int get() = players.indexOf(this)
fun Player.playerDiffIndex(nextPlayer: Player): Int =
if (direction == Direction.CLOCKWISE) {
nextPlayer.currentIndex + this.currentIndex
} else {
nextPlayer.currentIndex - this.currentIndex
}.let { it % players.size }
val Player.cardOnBoardIsForYou: Boolean get() {
if (cardOnCurrentStack == null) error("No card")
return this.playerDiffIndex(cardOnCurrentStack.player) == 1
}
fun playableCards(player: Player): List<Card> =
deck
.playersHands
.getHand(player)
?.filter { canBePlayThisCard(player, it) }
?: emptyList()
fun playerHasNoCardLeft(): List<Player> =
deck.playerHasNoCardLeft().map { playerId ->
players.find { it.id == playerId } ?: error("inconsistency detected between players")
}
val isReady: Boolean get() {
return players.size == readyPlayers.size && players.all { readyPlayers.contains(it) }
}
fun canBePlayThisCard(
player: Player,
card: Card,
): Boolean {
val cardOnBoard = cardOnCurrentStack?.card ?: return false
return when (cardOnBoard) {
is Card.NumericCard -> {
when (card) {
is Card.AllColorCard -> true
is Card.NumericCard -> card.number == cardOnBoard.number || card.color == cardOnBoard.color
is Card.ColorCard -> card.color == cardOnBoard.color
}
}
private val currentPlayerIndex: Int? get() {
val i = players.indexOf(currentPlayerTurn)
return if (i == -1) {
null
is Card.ReverseCard -> {
when (card) {
is Card.ReverseCard -> true
is Card.AllColorCard -> true
is Card.ColorCard -> card.color == cardOnBoard.color
}
}
is Card.PassCard -> {
if (player.cardOnBoardIsForYou) {
false
} else {
i
when (card) {
is Card.AllColorCard -> true
is Card.ColorCard -> card.color == cardOnBoard.color
}
}
}
}
private fun nextPlayerIndex(direction: Direction): Int {
if (players.isEmpty()) return 0
is Card.ChangeColorCard -> {
when (card) {
is Card.AllColorCard -> true
is Card.ColorCard -> card.color == colorOnCurrentStack
}
}
return if (direction == Direction.CLOCKWISE) {
sidePlayerIndexClockwise
is Card.Plus2Card -> {
if (player.cardOnBoardIsForYou && card is Card.Plus2Card) {
true
} else {
sidePlayerIndexCounterClockwise
when (card) {
is Card.AllColorCard -> true
is Card.Plus2Card -> true
is Card.ColorCard -> card.color == cardOnBoard.color
}
}
}
}
fun nextPlayer(direction: Direction): Player = players.elementAt(nextPlayerIndex(direction))
private val sidePlayerIndexClockwise: Int by lazy {
if (players.isEmpty()) {
0
is Card.Plus4Card -> {
if (player.cardOnBoardIsForYou && card is Card.Plus4Card) {
true
} else {
((currentPlayerIndex ?: 0) + 1) % players.size
}
}
private val sidePlayerIndexCounterClockwise: Int by lazy {
if (players.isEmpty()) {
0
} else {
((currentPlayerIndex ?: 0) - 1) % players.size
}
}
val nextPlayerTurn: Player? by lazy {
if (players.isEmpty()) {
null
} else {
nextPlayer(direction)
}
}
private val Player.currentIndex: Int get() = players.indexOf(this)
fun Player.playerDiffIndex(nextPlayer: Player): Int =
if (direction == Direction.CLOCKWISE) {
nextPlayer.currentIndex + this.currentIndex
} else {
nextPlayer.currentIndex - this.currentIndex
}.let { it % players.size }
val Player.cardOnBoardIsForYou: Boolean get() {
if (cardOnCurrentStack == null) error("No card")
return this.playerDiffIndex(cardOnCurrentStack.player) == 1
}
fun playableCards(player: Player): List<Card> =
deck
.playersHands
.getHand(player)
?.filter { canBePlayThisCard(player, it) }
?: emptyList()
fun playerHasNoCardLeft(): List<Player> =
deck.playerHasNoCardLeft().map { playerId ->
players.find { it.id == playerId } ?: error("inconsistency detected between players")
}
fun canBePlayThisCard(
player: Player,
card: Card,
): Boolean {
val cardOnBoard = cardOnCurrentStack?.card ?: return false
return when (cardOnBoard) {
is Card.NumericCard -> {
when (card) {
is Card.AllColorCard -> true
is Card.NumericCard -> card.number == cardOnBoard.number || card.color == cardOnBoard.color
is Card.ColorCard -> card.color == cardOnBoard.color
}
}
is Card.ReverseCard -> {
when (card) {
is Card.ReverseCard -> true
is Card.AllColorCard -> true
is Card.ColorCard -> card.color == cardOnBoard.color
}
}
is Card.PassCard -> {
if (player.cardOnBoardIsForYou) {
false
} else {
when (card) {
is Card.AllColorCard -> true
is Card.ColorCard -> card.color == cardOnBoard.color
}
}
}
is Card.ChangeColorCard -> {
when (card) {
is Card.AllColorCard -> true
is Card.ColorCard -> card.color == colorOnCurrentStack
}
}
is Card.Plus2Card -> {
if (player.cardOnBoardIsForYou && card is Card.Plus2Card) {
true
} else {
when (card) {
is Card.AllColorCard -> true
is Card.Plus2Card -> true
is Card.ColorCard -> card.color == cardOnBoard.color
}
}
}
is Card.Plus4Card -> {
if (player.cardOnBoardIsForYou && card is Card.Plus4Card) {
true
} else {
when (card) {
is Card.AllColorCard -> true
is Card.ColorCard -> card.color == colorOnCurrentStack
}
}
}
when (card) {
is Card.AllColorCard -> true
is Card.ColorCard -> card.color == colorOnCurrentStack
}
}
}
}
}
}

View File

@@ -13,103 +13,103 @@ import eventDemo.app.event.event.PlayerWinEvent
import io.github.oshai.kotlinlogging.KotlinLogging
fun GameState.apply(event: GameEvent): GameState =
this.let { state ->
val logger = KotlinLogging.logger { }
if (event is PlayerActionEvent) {
if (state.currentPlayerTurn != event.player) {
logger.atError {
message = "Inconsistent player turn. CurrentPlayerTurn: $state.currentPlayerTurn | Player: ${event.player}"
payload =
mapOf(
"CurrentPlayerTurn" to (state.currentPlayerTurn ?: "No currentPlayerTurn"),
"Player" to event.player,
)
}
}
this.let { state ->
val logger = KotlinLogging.logger { }
if (event is PlayerActionEvent) {
if (state.currentPlayerTurn != event.player) {
logger.atError {
message = "Inconsistent player turn. CurrentPlayerTurn: $state.currentPlayerTurn | Player: ${event.player}"
payload =
mapOf(
"CurrentPlayerTurn" to (state.currentPlayerTurn ?: "No currentPlayerTurn"),
"Player" to event.player,
)
}
}
}
when (event) {
is CardIsPlayedEvent -> {
val nextDirectionAfterPlay =
when (event.card) {
is Card.ReverseCard -> state.direction.revert()
else -> state.direction
}
val color =
when (event.card) {
is Card.ColorCard -> event.card.color
is Card.AllColorCard -> null
}
val currentPlayerAfterThePlay =
if (event.card is Card.AllColorCard) {
state.currentPlayerTurn
} else {
state.nextPlayer(nextDirectionAfterPlay)
}
state.copy(
currentPlayerTurn = currentPlayerAfterThePlay,
direction = nextDirectionAfterPlay,
colorOnCurrentStack = color,
cardOnCurrentStack = GameState.LastCard(event.card, event.player),
deck = state.deck.putOneCardFromHand(event.player, event.card),
)
}
is NewPlayerEvent -> {
if (state.isStarted) {
logger.error { "The game is already started" }
}
when (event) {
is CardIsPlayedEvent -> {
val nextDirectionAfterPlay =
when (event.card) {
is Card.ReverseCard -> state.direction.revert()
else -> state.direction
}
val color =
when (event.card) {
is Card.ColorCard -> event.card.color
is Card.AllColorCard -> null
}
val currentPlayerAfterThePlay =
if (event.card is Card.AllColorCard) {
state.currentPlayerTurn
} else {
state.nextPlayer(nextDirectionAfterPlay)
}
state.copy(
currentPlayerTurn = currentPlayerAfterThePlay,
direction = nextDirectionAfterPlay,
colorOnCurrentStack = color,
cardOnCurrentStack = GameState.LastCard(event.card, event.player),
deck = state.deck.putOneCardFromHand(event.player, event.card),
)
}
is NewPlayerEvent -> {
if (state.isStarted) {
logger.error { "The game is already started" }
}
state.copy(
players = state.players + event.player,
)
}
is PlayerReadyEvent -> {
if (state.isStarted) {
logger.error { "The game is already started" }
}
state.copy(
readyPlayers = state.readyPlayers + event.player,
)
}
is PlayerHavePassEvent -> {
if (event.takenCard != state.deck.stack.first()) {
logger.error { "taken card is not ot top of the stack: ${event.takenCard}" }
}
state.copy(
currentPlayerTurn = state.nextPlayerTurn,
deck = state.deck.takeOneCardFromStackTo(event.player),
)
}
is PlayerChoseColorEvent -> {
state.copy(
currentPlayerTurn = state.nextPlayerTurn,
colorOnCurrentStack = event.color,
)
}
is GameStartedEvent -> {
state.copy(
colorOnCurrentStack = (event.deck.discard.first() as? Card.ColorCard)?.color ?: state.colorOnCurrentStack,
cardOnCurrentStack = GameState.LastCard(event.deck.discard.first(), event.firstPlayer),
currentPlayerTurn = event.firstPlayer,
deck = event.deck,
isStarted = true,
)
}
is PlayerWinEvent -> {
state.copy(
playerWins = state.playerWins + event.player,
)
}
}.copy(
lastEventVersion = event.version,
state.copy(
players = state.players + event.player,
)
}
}
is PlayerReadyEvent -> {
if (state.isStarted) {
logger.error { "The game is already started" }
}
state.copy(
readyPlayers = state.readyPlayers + event.player,
)
}
is PlayerHavePassEvent -> {
if (event.takenCard != state.deck.stack.first()) {
logger.error { "taken card is not ot top of the stack: ${event.takenCard}" }
}
state.copy(
currentPlayerTurn = state.nextPlayerTurn,
deck = state.deck.takeOneCardFromStackTo(event.player),
)
}
is PlayerChoseColorEvent -> {
state.copy(
currentPlayerTurn = state.nextPlayerTurn,
colorOnCurrentStack = event.color,
)
}
is GameStartedEvent -> {
state.copy(
colorOnCurrentStack = (event.deck.discard.first() as? Card.ColorCard)?.color ?: state.colorOnCurrentStack,
cardOnCurrentStack = GameState.LastCard(event.deck.discard.first(), event.firstPlayer),
currentPlayerTurn = event.firstPlayer,
deck = event.deck,
isStarted = true,
)
}
is PlayerWinEvent -> {
state.copy(
playerWins = state.playerWins + event.player,
)
}
}.copy(
lastEventVersion = event.version,
)
}

View File

@@ -6,36 +6,38 @@ import eventDemo.app.event.GameEventStore
import eventDemo.app.event.event.GameEvent
class GameStateRepository(
eventStore: GameEventStore,
eventHandler: GameEventHandler,
snapshotConfig: SnapshotConfig = SnapshotConfig(),
eventStore: GameEventStore,
eventHandler: GameEventHandler,
snapshotConfig: SnapshotConfig = SnapshotConfig(),
) {
private val projectionsSnapshot =
ProjectionSnapshotRepositoryInMemory(
eventStore = eventStore,
snapshotCacheConfig = snapshotConfig,
applyToProjection = GameState::apply,
initialStateBuilder = { aggregateId: GameId -> GameState(aggregateId) },
)
private val projectionsSnapshot =
ProjectionSnapshotRepositoryInMemory(
eventStore = eventStore,
snapshotCacheConfig = snapshotConfig,
applyToProjection = GameState::apply,
initialStateBuilder = { aggregateId: GameId -> GameState(aggregateId) },
)
init {
eventHandler.registerProjectionBuilder { event ->
projectionsSnapshot.applyAndPutToCache(event)
}
init {
eventHandler.registerProjectionBuilder { event ->
projectionsSnapshot.applyAndPutToCache(event)
}
}
/**
* Get the last version of the [GameState] from the all eventStream.
*
* It fetches it from the local cache if possible, otherwise it builds it.
*/
fun getLast(gameId: GameId): GameState = projectionsSnapshot.getLast(gameId)
/**
* Get the last version of the [GameState] from the all eventStream.
*
* It fetches it from the local cache if possible, otherwise it builds it.
*/
fun getLast(gameId: GameId): GameState =
projectionsSnapshot.getLast(gameId)
/**
* Get the [GameState] to the specific [event][GameEvent].
* It does not contain the [events][GameEvent] it after this one.
*
* It fetches it from the local cache if possible, otherwise it builds it.
*/
fun getUntil(event: GameEvent): GameState = projectionsSnapshot.getUntil(event)
/**
* Get the [GameState] to the specific [event][GameEvent].
* It does not contain the [events][GameEvent] it after this one.
*
* It fetches it from the local cache if possible, otherwise it builds it.
*/
fun getUntil(event: GameEvent): GameState =
projectionsSnapshot.getUntil(event)
}

View File

@@ -3,6 +3,6 @@ package eventDemo.app.event.projection
import eventDemo.libs.event.AggregateId
interface Projection<ID : AggregateId> {
val aggregateId: ID
val lastEventVersion: Int
val aggregateId: ID
val lastEventVersion: Int
}

View File

@@ -13,182 +13,183 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
data class SnapshotConfig(
val maxSnapshotCacheSize: Int = 20,
val maxSnapshotCacheTtl: Duration = 10.minutes,
/**
* Only create [snapshots][Projection] every [X][modulo] [events][Event]
*/
val modulo: Int = 10,
val maxSnapshotCacheSize: Int = 20,
val maxSnapshotCacheTtl: Duration = 10.minutes,
/**
* Only create [snapshots][Projection] every [X][modulo] [events][Event]
*/
val modulo: Int = 10,
)
class ProjectionSnapshotRepositoryInMemory<E : Event<ID>, P : Projection<ID>, ID : AggregateId>(
private val eventStore: EventStore<E, ID>,
private val initialStateBuilder: (ID) -> P,
private val snapshotCacheConfig: SnapshotConfig = SnapshotConfig(),
private val applyToProjection: P.(event: E) -> P,
private val eventStore: EventStore<E, ID>,
private val initialStateBuilder: (ID) -> P,
private val snapshotCacheConfig: SnapshotConfig = SnapshotConfig(),
private val applyToProjection: P.(event: E) -> P,
) {
private val projectionsSnapshot: ConcurrentHashMap<ID, ConcurrentLinkedQueue<Pair<P, Instant>>> = ConcurrentHashMap()
private val projectionsSnapshot: ConcurrentHashMap<ID, ConcurrentLinkedQueue<Pair<P, Instant>>> = ConcurrentHashMap()
/**
* Create a snapshot for the event
*
* 1. get the last snapshot with a version lower than that of the event
* 2. get the events with a greater version of the snapshot
* 3. apply the event to the snapshot
* 4. apply the new event to the projection
* 5. save it
* 6. remove old one
*/
fun applyAndPutToCache(event: E) {
if ((event.version % snapshotCacheConfig.modulo) == 0) {
getUntil(event)
.also {
save(it)
removeOldSnapshot(it.aggregateId)
}
/**
* Create a snapshot for the event
*
* 1. get the last snapshot with a version lower than that of the event
* 2. get the events with a greater version of the snapshot
* 3. apply the event to the snapshot
* 4. apply the new event to the projection
* 5. save it
* 6. remove old one
*/
fun applyAndPutToCache(event: E) {
if ((event.version % snapshotCacheConfig.modulo) == 0) {
getUntil(event)
.also {
save(it)
removeOldSnapshot(it.aggregateId)
}
}
}
/**
* Build the last version of the [Projection] from the cache.
*
* 1. get the last snapshot
* 2. get the missing event to the snapshot
* 3. apply the missing events to the snapshot
*/
fun getLast(aggregateId: ID): P {
val lastSnapshot = getLastSnapshot(aggregateId)?.first
val missingEventOfSnapshot = getEventAfterTheSnapshot(aggregateId, lastSnapshot)
return lastSnapshot.applyEvents(aggregateId, missingEventOfSnapshot)
/**
* Build the last version of the [Projection] from the cache.
*
* 1. get the last snapshot
* 2. get the missing event to the snapshot
* 3. apply the missing events to the snapshot
*/
fun getLast(aggregateId: ID): P {
val lastSnapshot = getLastSnapshot(aggregateId)?.first
val missingEventOfSnapshot = getEventAfterTheSnapshot(aggregateId, lastSnapshot)
return lastSnapshot.applyEvents(aggregateId, missingEventOfSnapshot)
}
/**
* Build the [Projection] to the specific [event][Event].
*
* It does not contain the [events][Event] it after this one.
*
* 1. get the last snapshot before the event
* 2. get the events with a greater version of the snapshot but lower of passed event
* 3. apply the events to the snapshot
*/
fun getUntil(event: E): P {
val lastSnapshot = getLastSnapshotBeforeOrEqualEvent(event)?.first
if (lastSnapshot?.lastEventVersion == event.version) {
return lastSnapshot
}
/**
* Build the [Projection] to the specific [event][Event].
*
* It does not contain the [events][Event] it after this one.
*
* 1. get the last snapshot before the event
* 2. get the events with a greater version of the snapshot but lower of passed event
* 3. apply the events to the snapshot
*/
fun getUntil(event: E): P {
val lastSnapshot = getLastSnapshotBeforeOrEqualEvent(event)?.first
if (lastSnapshot?.lastEventVersion == event.version) {
return lastSnapshot
}
val missingEventOfSnapshot =
eventStore
.getStream(event.aggregateId)
.readVersionBetween((lastSnapshot?.lastEventVersion ?: 1)..event.version)
val missingEventOfSnapshot =
eventStore
.getStream(event.aggregateId)
.readVersionBetween((lastSnapshot?.lastEventVersion ?: 1)..event.version)
return if (lastSnapshot?.lastEventVersion == event.version) {
lastSnapshot
} else {
lastSnapshot.applyEvents(event.aggregateId, missingEventOfSnapshot)
}
return if (lastSnapshot?.lastEventVersion == event.version) {
lastSnapshot
} else {
lastSnapshot.applyEvents(event.aggregateId, missingEventOfSnapshot)
}
}
/**
* Remove the oldest snapshot.
*
* The rules are pass in the controller.
*/
private fun removeOldSnapshot(aggregateId: ID) {
projectionsSnapshot[aggregateId]?.let { queue ->
// never remove the last one
val theLastOne = getLastSnapshot(aggregateId)
removeByDate(queue, theLastOne)
removeBySize(queue, theLastOne)
}
/**
* Remove the oldest snapshot.
*
* The rules are pass in the controller.
*/
private fun removeOldSnapshot(aggregateId: ID) {
projectionsSnapshot[aggregateId]?.let { queue ->
// never remove the last one
val theLastOne = getLastSnapshot(aggregateId)
removeByDate(queue, theLastOne)
removeBySize(queue, theLastOne)
}
}
private fun removeBySize(
queue: ConcurrentLinkedQueue<Pair<P, Instant>>,
theLastOne: Pair<P, Instant>?,
) {
// Remove if size exceeds the limit
val size = queue.size
if (size > snapshotCacheConfig.maxSnapshotCacheSize) {
val numberToRemove = size - snapshotCacheConfig.maxSnapshotCacheSize
if (numberToRemove > 0) {
queue
.sortedBy { it.first.lastEventVersion }
.take(numberToRemove)
.let { it - theLastOne }
.forEach { queue.remove(it) }
}
}
private fun removeBySize(
queue: ConcurrentLinkedQueue<Pair<P, Instant>>,
theLastOne: Pair<P, Instant>?,
) {
// Remove if size exceeds the limit
val size = queue.size
if (size > snapshotCacheConfig.maxSnapshotCacheSize) {
val numberToRemove = size - snapshotCacheConfig.maxSnapshotCacheSize
if (numberToRemove > 0) {
queue
.sortedBy { it.first.lastEventVersion }
.take(numberToRemove)
.let { it - theLastOne }
.forEach { queue.remove(it) }
}
}
}
private fun removeByDate(
queue: ConcurrentLinkedQueue<Pair<P, Instant>>,
theLastOne: Pair<P, Instant>?,
) {
// remove the oldest by time
val now = Clock.System.now()
val deadLine = now - snapshotCacheConfig.maxSnapshotCacheTtl
val toRemove = queue.filter { deadLine > it.second }
(toRemove - theLastOne).forEach { queue.remove(it) }
}
/**
* Save the snapshot.
*/
private fun save(projection: P) {
projectionsSnapshot
.computeIfAbsent(projection.aggregateId) { ConcurrentLinkedQueue() }
.add(Pair(projection, Clock.System.now()))
}
/**
* Get the last snapshot when the version is lower of then event version
*/
private fun getLastSnapshotBeforeOrEqualEvent(event: E) =
projectionsSnapshot[event.aggregateId]
?.sortedByDescending { it.first.lastEventVersion }
?.find { it.first.lastEventVersion <= event.version }
/**
* Get the last snapshot (with the higher version).
*/
private fun getLastSnapshot(aggregateId: ID) =
projectionsSnapshot[aggregateId]
?.maxByOrNull { it.first.lastEventVersion }
/**
* Get the events from the [event stream][EventStream] when the version is higher of the snapshot.
*
* If the snapshot is null, it takes all events from the event [event stream][EventStream]
*/
private fun getEventAfterTheSnapshot(
aggregateId: ID,
snapshot: P?,
) = eventStore
.getStream(aggregateId)
.readGreaterOfVersion(snapshot?.lastEventVersion ?: 0)
/**
* Apply events to the projection.
*/
private fun P?.applyEvents(
aggregateId: ID,
eventsToApply: Set<E>,
): P =
eventsToApply
.fold(this ?: initialStateBuilder(aggregateId), applyToProjectionSecure)
/**
* Wrap the [applyToProjection] lambda to avoid duplicate apply of the same event.
*/
private val applyToProjectionSecure: P.(event: E) -> P = { event ->
if (event.version == lastEventVersion + 1) {
applyToProjection(event)
} else if (event.version <= lastEventVersion) {
KotlinLogging.logger { }.warn { "Event is already is the Projection, skip apply." }
this
} else {
error("The version of the event must follow directly after the version of the projection.")
}
private fun removeByDate(
queue: ConcurrentLinkedQueue<Pair<P, Instant>>,
theLastOne: Pair<P, Instant>?,
) {
// remove the oldest by time
val now = Clock.System.now()
val deadLine = now - snapshotCacheConfig.maxSnapshotCacheTtl
val toRemove = queue.filter { deadLine > it.second }
(toRemove - theLastOne).forEach { queue.remove(it) }
}
/**
* Save the snapshot.
*/
private fun save(projection: P) {
projectionsSnapshot
.computeIfAbsent(projection.aggregateId) { ConcurrentLinkedQueue() }
.add(Pair(projection, Clock.System.now()))
}
/**
* Get the last snapshot when the version is lower of then event version
*/
private fun getLastSnapshotBeforeOrEqualEvent(event: E) =
projectionsSnapshot[event.aggregateId]
?.sortedByDescending { it.first.lastEventVersion }
?.find { it.first.lastEventVersion <= event.version }
/**
* Get the last snapshot (with the higher version).
*/
private fun getLastSnapshot(aggregateId: ID) =
projectionsSnapshot[aggregateId]
?.maxByOrNull { it.first.lastEventVersion }
/**
* Get the events from the [event stream][EventStream] when the version is higher of the snapshot.
*
* If the snapshot is null, it takes all events from the event [event stream][EventStream]
*/
private fun getEventAfterTheSnapshot(
aggregateId: ID,
snapshot: P?,
) =
eventStore
.getStream(aggregateId)
.readGreaterOfVersion(snapshot?.lastEventVersion ?: 0)
/**
* Apply events to the projection.
*/
private fun P?.applyEvents(
aggregateId: ID,
eventsToApply: Set<E>,
): P =
eventsToApply
.fold(this ?: initialStateBuilder(aggregateId), applyToProjectionSecure)
/**
* Wrap the [applyToProjection] lambda to avoid duplicate apply of the same event.
*/
private val applyToProjectionSecure: P.(event: E) -> P = { event ->
if (event.version == lastEventVersion + 1) {
applyToProjection(event)
} else if (event.version <= lastEventVersion) {
KotlinLogging.logger { }.warn { "Event is already is the Projection, skip apply." }
this
} else {
error("The version of the event must follow directly after the version of the projection.")
}
}
}

View File

@@ -28,118 +28,118 @@ import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.trySendBlocking
class PlayerNotificationEventListener(
private val eventBus: GameEventBus,
private val gameStateRepository: GameStateRepository,
private val eventBus: GameEventBus,
private val gameStateRepository: GameStateRepository,
) {
private val logger = KotlinLogging.logger {}
private val logger = KotlinLogging.logger {}
fun startListening(
outgoingNotificationChannel: SendChannel<Notification>,
currentPlayer: Player,
) {
eventBus.subscribe { event: GameEvent ->
val currentState = gameStateRepository.getUntil(event)
fun startListening(
outgoingNotificationChannel: SendChannel<Notification>,
currentPlayer: Player,
) {
eventBus.subscribe { event: GameEvent ->
val currentState = gameStateRepository.getUntil(event)
fun Notification.send() {
if (currentState.players.contains(currentPlayer)) {
// Only notify players who have already joined the game.
outgoingNotificationChannel.trySendBlocking(this)
logger.atInfo {
message = "Notification for player ${currentPlayer.name} was SEND: ${this@send}"
payload = mapOf("notification" to this@send, "event" to event)
}
} else {
// Rare use case, when a connexion is created with the channel,
// but the player was not already join in the game
logger.atWarn {
message = "Notification for player ${currentPlayer.name} was SKIP, No player on the game: ${this@send}"
payload = mapOf("notification" to this@send, "event" to event)
}
}
}
fun sendNextTurnNotif() =
ItsTheTurnOfNotification(
player = currentState.currentPlayerTurn ?: error("No player turn defined"),
).send()
when (event) {
is NewPlayerEvent -> {
if (currentPlayer != event.player) {
PlayerAsJoinTheGameNotification(
player = event.player,
).send()
} else {
WelcomeToTheGameNotification(
players = currentState.players,
).send()
}
}
is CardIsPlayedEvent -> {
if (currentPlayer != event.player) {
PlayerAsPlayACardNotification(
player = event.player,
card = event.card,
).send()
}
if (event.card !is Card.AllColorCard) {
ItsTheTurnOfNotification(
player = currentState.currentPlayerTurn ?: error("No player turn defined"),
).send()
}
}
is GameStartedEvent -> {
TheGameWasStartedNotification(
hand =
event.deck.playersHands.getHand(currentPlayer)
?: error("You are not in the game"),
).send()
sendNextTurnNotif()
}
is PlayerChoseColorEvent -> {
if (currentPlayer != event.player) {
PlayerWasChoseTheCardColorNotification(
player = event.player,
color = event.color,
).send()
}
sendNextTurnNotif()
}
is PlayerHavePassEvent -> {
if (currentPlayer == event.player) {
YourNewCardNotification(
card = event.takenCard,
).send()
} else {
PlayerHavePassNotification(
player = event.player,
).send()
}
sendNextTurnNotif()
}
is PlayerReadyEvent -> {
if (currentPlayer != event.player) {
PlayerWasReadyNotification(
player = event.player,
).send()
}
}
is PlayerWinEvent -> {
PlayerWinNotification(
player = event.player,
).send()
}
}
fun Notification.send() {
if (currentState.players.contains(currentPlayer)) {
// Only notify players who have already joined the game.
outgoingNotificationChannel.trySendBlocking(this)
logger.atInfo {
message = "Notification for player ${currentPlayer.name} was SEND: ${this@send}"
payload = mapOf("notification" to this@send, "event" to event)
}
} else {
// Rare use case, when a connexion is created with the channel,
// but the player was not already join in the game
logger.atWarn {
message = "Notification for player ${currentPlayer.name} was SKIP, No player on the game: ${this@send}"
payload = mapOf("notification" to this@send, "event" to event)
}
}
}
fun sendNextTurnNotif() =
ItsTheTurnOfNotification(
player = currentState.currentPlayerTurn ?: error("No player turn defined"),
).send()
when (event) {
is NewPlayerEvent -> {
if (currentPlayer != event.player) {
PlayerAsJoinTheGameNotification(
player = event.player,
).send()
} else {
WelcomeToTheGameNotification(
players = currentState.players,
).send()
}
}
is CardIsPlayedEvent -> {
if (currentPlayer != event.player) {
PlayerAsPlayACardNotification(
player = event.player,
card = event.card,
).send()
}
if (event.card !is Card.AllColorCard) {
ItsTheTurnOfNotification(
player = currentState.currentPlayerTurn ?: error("No player turn defined"),
).send()
}
}
is GameStartedEvent -> {
TheGameWasStartedNotification(
hand =
event.deck.playersHands.getHand(currentPlayer)
?: error("You are not in the game"),
).send()
sendNextTurnNotif()
}
is PlayerChoseColorEvent -> {
if (currentPlayer != event.player) {
PlayerWasChoseTheCardColorNotification(
player = event.player,
color = event.color,
).send()
}
sendNextTurnNotif()
}
is PlayerHavePassEvent -> {
if (currentPlayer == event.player) {
YourNewCardNotification(
card = event.takenCard,
).send()
} else {
PlayerHavePassNotification(
player = event.player,
).send()
}
sendNextTurnNotif()
}
is PlayerReadyEvent -> {
if (currentPlayer != event.player) {
PlayerWasReadyNotification(
player = event.player,
).send()
}
}
is PlayerWinEvent -> {
PlayerWinNotification(
player = event.player,
).send()
}
}
}
}
}

View File

@@ -11,76 +11,76 @@ import eventDemo.app.event.projection.GameStateRepository
import io.github.oshai.kotlinlogging.KotlinLogging
class ReactionEventListener(
private val eventBus: GameEventBus,
private val eventHandler: GameEventHandler,
private val gameStateRepository: GameStateRepository,
private val priority: Int = DEFAULT_PRIORITY,
private val eventBus: GameEventBus,
private val eventHandler: GameEventHandler,
private val gameStateRepository: GameStateRepository,
private val priority: Int = DEFAULT_PRIORITY,
) {
companion object Config {
const val DEFAULT_PRIORITY = -1000
companion object Config {
const val DEFAULT_PRIORITY = -1000
}
private val logger = KotlinLogging.logger { }
fun init() {
eventBus.subscribe(priority) { event: GameEvent ->
val state = gameStateRepository.getUntil(event)
sendStartGameEvent(state, event)
sendWinnerEvent(state, event)
}
}
private val logger = KotlinLogging.logger { }
fun init() {
eventBus.subscribe(priority) { event: GameEvent ->
val state = gameStateRepository.getUntil(event)
sendStartGameEvent(state, event)
sendWinnerEvent(state, event)
private suspend fun sendStartGameEvent(
state: GameState,
event: GameEvent,
) {
if (state.isReady && !state.isStarted) {
val reactionEvent =
eventHandler.handle(state.aggregateId) {
GameStartedEvent.new(
id = state.aggregateId,
players = state.players,
version = it,
)
}
logger.atInfo {
message = "Reaction event was Send $reactionEvent on reaction of: $event"
payload =
mapOf(
"event" to event,
"reactionEvent" to reactionEvent,
)
}
} else {
if (event is PlayerReadyEvent) {
logger.info { "All players was not ready ${state.readyPlayers}" }
}
}
}
private suspend fun sendStartGameEvent(
state: GameState,
event: GameEvent,
) {
if (state.isReady && !state.isStarted) {
val reactionEvent =
eventHandler.handle(state.aggregateId) {
GameStartedEvent.new(
id = state.aggregateId,
players = state.players,
version = it,
)
}
logger.atInfo {
message = "Reaction event was Send $reactionEvent on reaction of: $event"
payload =
mapOf(
"event" to event,
"reactionEvent" to reactionEvent,
)
}
} else {
if (event is PlayerReadyEvent) {
logger.info { "All players was not ready ${state.readyPlayers}" }
}
private fun sendWinnerEvent(
state: GameState,
event: GameEvent,
) {
val winner = state.playerHasNoCardLeft().firstOrNull()
if (winner != null) {
val reactionEvent =
eventHandler.handle(state.aggregateId) {
PlayerWinEvent(
aggregateId = state.aggregateId,
player = winner,
version = it,
)
}
}
private fun sendWinnerEvent(
state: GameState,
event: GameEvent,
) {
val winner = state.playerHasNoCardLeft().firstOrNull()
if (winner != null) {
val reactionEvent =
eventHandler.handle(state.aggregateId) {
PlayerWinEvent(
aggregateId = state.aggregateId,
player = winner,
version = it,
)
}
logger.atInfo {
message = "Reaction event was Send $reactionEvent on reaction of: $event"
payload =
mapOf(
"event" to event,
"reactionEvent" to reactionEvent,
)
}
}
logger.atInfo {
message = "Reaction event was Send $reactionEvent on reaction of: $event"
payload =
mapOf(
"event" to event,
"reactionEvent" to reactionEvent,
)
}
}
}
}

View File

@@ -6,7 +6,7 @@ import java.util.UUID
@Serializable
data class ErrorNotification(
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val message: String,
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val message: String,
) : Notification

View File

@@ -7,7 +7,7 @@ import java.util.UUID
@Serializable
data class ItsTheTurnOfNotification(
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val player: Player,
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val player: Player,
) : Notification

View File

@@ -6,6 +6,6 @@ import java.util.UUID
@Serializable
sealed interface Notification {
@Serializable(with = UUIDSerializer::class)
val id: UUID
@Serializable(with = UUIDSerializer::class)
val id: UUID
}

View File

@@ -7,7 +7,7 @@ import java.util.UUID
@Serializable
data class PlayerAsJoinTheGameNotification(
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val player: Player,
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val player: Player,
) : Notification

View File

@@ -8,8 +8,8 @@ import java.util.UUID
@Serializable
data class PlayerAsPlayACardNotification(
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val player: Player,
val card: Card,
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val player: Player,
val card: Card,
) : Notification

View File

@@ -7,7 +7,7 @@ import java.util.UUID
@Serializable
data class PlayerHavePassNotification(
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val player: Player,
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val player: Player,
) : Notification

View File

@@ -8,8 +8,8 @@ import java.util.UUID
@Serializable
data class PlayerWasChoseTheCardColorNotification(
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val player: Player,
val color: Card.Color,
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val player: Player,
val color: Card.Color,
) : Notification

View File

@@ -7,7 +7,7 @@ import java.util.UUID
@Serializable
data class PlayerWasReadyNotification(
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val player: Player,
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val player: Player,
) : Notification

View File

@@ -7,7 +7,7 @@ import java.util.UUID
@Serializable
data class PlayerWinNotification(
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val player: Player,
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val player: Player,
) : Notification

View File

@@ -7,7 +7,7 @@ import java.util.UUID
@Serializable
data class TheGameWasStartedNotification(
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val hand: List<Card>,
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val hand: List<Card>,
) : Notification

View File

@@ -7,7 +7,7 @@ import java.util.UUID
@Serializable
data class WelcomeToTheGameNotification(
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val players: Set<Player>,
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val players: Set<Player>,
) : Notification

View File

@@ -7,7 +7,7 @@ import java.util.UUID
@Serializable
data class YourNewCardNotification(
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val card: Card,
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
val card: Card,
) : Notification

View File

@@ -15,41 +15,41 @@ import kotlinx.serialization.Serializable
@Serializable
@Resource("/game/{id}")
class Game(
@Serializable(with = GameIdSerializer::class)
val id: GameId,
@Serializable(with = GameIdSerializer::class)
val id: GameId,
) {
@Serializable
@Resource("card/last")
class Card(
val game: Game,
)
@Serializable
@Resource("card/last")
class Card(
val game: Game,
)
@Serializable
@Resource("state")
class State(
val game: Game,
)
@Serializable
@Resource("state")
class State(
val game: Game,
)
}
/**
* API routes to read the game state.
*/
fun Route.readTheGameState(gameStateRepository: GameStateRepository) {
authenticate {
// Read the last played card on the game.
get<Game.Card> { body ->
gameStateRepository
.getLast(body.game.id)
.cardOnCurrentStack
?.card
?.let { call.respond(it) }
?: call.response.status(HttpStatusCode.BadRequest)
}
// Read the last played card on the game.
get<Game.State> { body ->
val state = gameStateRepository.getLast(body.game.id)
call.respond(state)
}
authenticate {
// Read the last played card on the game.
get<Game.Card> { body ->
gameStateRepository
.getLast(body.game.id)
.cardOnCurrentStack
?.card
?.let { call.respond(it) }
?: call.response.status(HttpStatusCode.BadRequest)
}
// Read the last played card on the game.
get<Game.State> { body ->
val state = gameStateRepository.getLast(body.game.id)
call.respond(state)
}
}
}