2 Commits
Author SHA1 Message Date
flecomte 3c85c344ce refactoring: Masive refactor to build the V2
Tests / lint (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / build (push) Has been cancelled
2026-07-30 22:42:56 +02:00
flecomte b313b39cf4 chore: config db in pgadmin 2026-07-30 22:42:54 +02:00
20 changed files with 703 additions and 160 deletions
+27
View File
@@ -0,0 +1,27 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="docker composeUp" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="composeUp" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<GradleProfilingDisabled>false</GradleProfilingDisabled>
<GradleCoverageDisabled>false</GradleCoverageDisabled>
<method v="2" />
</configuration>
</component>
+31
View File
@@ -4,10 +4,20 @@ services:
environment: environment:
PGADMIN_DEFAULT_EMAIL: $PGADMIN_DEFAULT_EMAIL PGADMIN_DEFAULT_EMAIL: $PGADMIN_DEFAULT_EMAIL
PGADMIN_DEFAULT_PASSWORD_FILE: /run/secrets/pgadmin_password PGADMIN_DEFAULT_PASSWORD_FILE: /run/secrets/pgadmin_password
PGADMIN_CONFIG_SERVER_MODE: 'False'
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
secrets: secrets:
- pgadmin_password - pgadmin_password
volumes: volumes:
- pgadmin_data:/var/lib/pgadmin - pgadmin_data:/var/lib/pgadmin
configs:
- source: pgpass
target: /pgpass
mode: 0600
uid: "5050"
gid: "5050"
- source: servers_json
target: /pgadmin4/servers.json
labels: labels:
- "traefik.http.routers.pgadmin.rule=Host(`pgadmin.postgresql.traefik.me`)" - "traefik.http.routers.pgadmin.rule=Host(`pgadmin.postgresql.traefik.me`)"
- "traefik.http.services.pgadmin.loadbalancer.server.port=80" - "traefik.http.services.pgadmin.loadbalancer.server.port=80"
@@ -24,6 +34,27 @@ services:
- "traefik.http.routers.rabbitmq-management.service=rabbitmq-management" - "traefik.http.routers.rabbitmq-management.service=rabbitmq-management"
- "traefik.http.services.rabbitmq-management.loadbalancer.server.port=15672" - "traefik.http.services.rabbitmq-management.loadbalancer.server.port=15672"
configs:
pgpass:
content: |
*:*:*:event-demo:changeit
servers_json:
content: |
{
"Servers": {
"1": {
"Name": "Event demo",
"Group": "Servers",
"Host": "postgresql",
"Port": 5432,
"MaintenanceDB": "event-demo",
"Username": "event-demo",
"PassFile": "/pgpass",
"SSLMode": "prefer"
}
}
}
secrets: secrets:
pgadmin_password: pgadmin_password:
file: ../pgadmin.secret file: ../pgadmin.secret
@@ -24,6 +24,10 @@ import eventDemo.contexts.game.domain.events.PlayerWinEvent
import eventDemo.contexts.game.domain.game.gameState.Game import eventDemo.contexts.game.domain.game.gameState.Game
import eventDemo.contexts.game.domain.game.gameState.GameStarted import eventDemo.contexts.game.domain.game.gameState.GameStarted
import eventDemo.sharedKernel.UserId import eventDemo.sharedKernel.UserId
import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.withLoggingContext
private val logger = KotlinLogging.logger {}
fun GameEvent.toNotification( fun GameEvent.toNotification(
game: Game, game: Game,
@@ -31,15 +35,16 @@ fun GameEvent.toNotification(
): Iterable<Notification> = ): Iterable<Notification> =
Iterable { Iterable {
iterator { iterator {
val currentPlayerId = game.players.get(currentUserId).id
context(iterator: SequenceScope<Notification>) context(iterator: SequenceScope<Notification>)
suspend fun Notification.send() { suspend fun Notification.send() {
iterator.yield(this) withLoggingContext("notification" to (this).toString()) {
logger.info { "Notification sent" }
iterator.yield(this)
}
} }
fun PlayerActionEvent.isFromCurrentUser(): Boolean = fun PlayerActionEvent.isFromCurrentUser(): Boolean =
currentPlayerId != playerId game.players.get(currentUserId).id == playerId
when (this@toNotification) { when (this@toNotification) {
is GameCreatedEvent -> { is GameCreatedEvent -> {
@@ -51,7 +56,7 @@ fun GameEvent.toNotification(
} }
is NewPlayerEvent -> { is NewPlayerEvent -> {
if (this@toNotification.isFromCurrentUser()) { if (this@toNotification.player.userId != currentUserId) {
PlayerAsJoinTheGameNotification( PlayerAsJoinTheGameNotification(
player = this@toNotification.player, player = this@toNotification.player,
).send() ).send()
@@ -85,6 +90,7 @@ fun GameEvent.toNotification(
if (game is GameStarted) { if (game is GameStarted) {
ItsTheTurnOfNotification(player = game.nextPlayer) ItsTheTurnOfNotification(player = game.nextPlayer)
.send()
} }
} }
@@ -101,21 +107,20 @@ fun GameEvent.toNotification(
if (game is GameStarted) { if (game is GameStarted) {
ItsTheTurnOfNotification(player = game.nextPlayer) ItsTheTurnOfNotification(player = game.nextPlayer)
.send()
} }
} }
is PlayerReadyEvent -> { is PlayerReadyEvent -> {
if (this@toNotification.isFromCurrentUser()) { PlayerWasReadyNotification(
PlayerWasReadyNotification( playerId = this@toNotification.playerId,
playerId = this@toNotification.playerId, ).send()
)
}
} }
is PlayerWinEvent -> { is PlayerWinEvent -> {
PlayerWinNotification( PlayerWinNotification(
playerId = this@toNotification.playerId, playerId = this@toNotification.playerId,
) ).send()
} }
} }
} }
@@ -48,7 +48,7 @@ class ReactionListener(
} }
private fun sendWinnerEvent(game: Game) { private fun sendWinnerEvent(game: Game) {
if (game is GameStarted) { if (game is GameStarted && game.lastPlayerId != null) {
val lastPlayerWin = val lastPlayerWin =
game game
.players .players
@@ -4,6 +4,7 @@ import eventDemo.contexts.game.domain.game.DiscardPile
import eventDemo.contexts.game.domain.game.DrawPile import eventDemo.contexts.game.domain.game.DrawPile
import eventDemo.contexts.game.domain.game.GameId import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.Player import eventDemo.contexts.game.domain.game.Player
import eventDemo.contexts.game.domain.game.PlayerHand
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer
import eventDemo.libs.eventSource.EventId import eventDemo.libs.eventSource.EventId
@@ -20,6 +21,7 @@ data class GameStartedEvent(
override val aggregateId: GameId, override val aggregateId: GameId,
@Serializable(with = PlayerIdSerializer::class) @Serializable(with = PlayerIdSerializer::class)
val firstPlayer: Player.PlayerId, val firstPlayer: Player.PlayerId,
val playersHans: Map<Player.PlayerId, PlayerHand>,
val drawPile: DrawPile, val drawPile: DrawPile,
val discardPile: DiscardPile, val discardPile: DiscardPile,
override val version: Int, override val version: Int,
@@ -28,9 +30,3 @@ data class GameStartedEvent(
override val eventId: EventId = EventId(UUID.randomUUID()) override val eventId: EventId = EventId(UUID.randomUUID())
override val createdAt: Instant = Clock.System.now() override val createdAt: Instant = Clock.System.now()
} }
private var isDisabled = false
internal fun disableShuffleDeck() {
isDisabled = true
}
@@ -7,6 +7,8 @@ import kotlinx.serialization.Serializable
value class DrawPile( value class DrawPile(
val cards: Set<Card> = emptySet(), val cards: Set<Card> = emptySet(),
) { ) {
val size: Int get() = cards.size
fun take(number: Int): Pair<DrawPile, Set<Card>> = fun take(number: Int): Pair<DrawPile, Set<Card>> =
cards.drop(number).toDrawPile() to cards.take(number).toSet() cards.drop(number).toDrawPile() to cards.take(number).toSet()
@@ -25,6 +25,7 @@ data class Player(
} }
} }
@Serializable
class PlayerList( class PlayerList(
val players: Set<Player> = emptySet(), val players: Set<Player> = emptySet(),
) : Set<Player> by players { ) : Set<Player> by players {
@@ -39,28 +39,36 @@ sealed interface Game {
companion object { companion object {
fun loadFromHistory(events: Set<GameEvent>): Game = fun loadFromHistory(events: Set<GameEvent>): Game =
events.fold(GameInit(events.first().aggregateId)) { game: Game, event -> events
game.run { .fold(GameInit(events.first().aggregateId)) { game: Game, event ->
when (event) { game.run {
is GameCreatedEvent if this is GameInit -> applyEvent(event) when (event) {
is GameCreatedEvent -> error("Game is already created") is GameCreatedEvent if this is GameInit -> applyEvent(event)
is NewPlayerEvent if this is GameCreated -> applyEvent(event) is GameCreatedEvent -> error("Game is already created")
is NewPlayerEvent -> error("Game is already stared") is NewPlayerEvent if this is GameCreated -> applyEvent(event)
is PlayerReadyEvent if this is GameCreated -> applyEvent(event) is NewPlayerEvent -> error("Game is already stared")
is PlayerReadyEvent -> error("Game is already stared") is PlayerReadyEvent if this is GameCreated -> applyEvent(event)
is GameStartedEvent if this is GameCreated -> applyEvent(event) is PlayerReadyEvent -> error("Game is already stared")
is GameStartedEvent -> error("Game is already started") is GameStartedEvent if this is GameCreated -> applyEvent(event)
is CardIsPlayedEvent if this is GameStarted -> applyEvent(event) is GameStartedEvent -> error("Game is already started")
is CardIsPlayedEvent -> error("Game is end") is CardIsPlayedEvent if this is GameStarted -> applyEvent(event)
is PlayerHaveDrawCardEvent if this is GameStarted -> applyEvent(event) is CardIsPlayedEvent -> error("Game is end")
is PlayerHaveDrawCardEvent -> error("Game is end") is PlayerHaveDrawCardEvent if this is GameStarted -> applyEvent(event)
is PlayerWinEvent if this is GameStarted -> applyEvent(event) is PlayerHaveDrawCardEvent -> error("Game is end")
is PlayerWinEvent -> error("Game is end") is PlayerWinEvent if this is GameStarted -> applyEvent(event)
is DrawFilledWithDiscardEvent if this is GameStarted -> applyEvent(event) is PlayerWinEvent -> error("Game is end")
is DrawFilledWithDiscardEvent -> error("Game is end") is DrawFilledWithDiscardEvent if this is GameStarted -> applyEvent(event)
is DrawFilledWithDiscardEvent -> error("Game is end")
}
}
}.let {
when (it) {
is GameInit -> it
is GameCreated -> it.copy(recordedEvents = emptySet())
is GameEnded -> it.copy(recordedEvents = emptySet())
is GameStarted -> it.copy(recordedEvents = emptySet())
} }
} }
}
} }
} }
@@ -71,18 +79,3 @@ internal fun <T : GameEvent> T.checkState(
if (!block(this)) throw exception(this) if (!block(this)) throw exception(this)
return this return this
} }
/**
* recordedEvents versions must be ordered and incremental.
*/
internal fun Game.checkRecorderEventsConsistency() {
recordedEvents
.also { if (it.size != (it.lastOrNull()?.version ?: 0)) throw InconsistentEventVersionException(recordedEvents) }
.mapIndexed { index, event ->
(index + 1) == event.version
}.run {
if (any { !it }) {
throw InconsistentEventVersionException(recordedEvents)
}
}
}
@@ -9,6 +9,7 @@ import eventDemo.contexts.game.domain.game.DiscardPile
import eventDemo.contexts.game.domain.game.DrawPile import eventDemo.contexts.game.domain.game.DrawPile
import eventDemo.contexts.game.domain.game.GameId import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.Player import eventDemo.contexts.game.domain.game.Player
import eventDemo.contexts.game.domain.game.PlayerHand
import eventDemo.contexts.game.domain.game.PlayerList import eventDemo.contexts.game.domain.game.PlayerList
import eventDemo.contexts.game.domain.game.errors.AllPlayerNotReadyException import eventDemo.contexts.game.domain.game.errors.AllPlayerNotReadyException
import eventDemo.contexts.game.domain.game.errors.DeckMissingCardsException import eventDemo.contexts.game.domain.game.errors.DeckMissingCardsException
@@ -23,24 +24,28 @@ data class GameCreated(
override val recordedEvents: Set<GameEvent>, override val recordedEvents: Set<GameEvent>,
override val version: Int, override val version: Int,
) : Game { ) : Game {
init {
checkRecorderEventsConsistency()
}
val allPlayerIsReady: Boolean val allPlayerIsReady: Boolean
get() { get() {
return playersStatus.values.all { it == PlayerStatus.Ready } return playersStatus.isNotEmpty() && playersStatus.values.all { it == PlayerStatus.Ready }
} }
fun startGame(deck: Deck = newDeck().shuffleDeck()): GameStarted { fun startGame(deck: Deck = newDeck().shuffleDeck()): GameStarted {
val (drawPile, discardPile) = initPiles(deck) val (drawPile, discardPile, playersHands) =
initPiles(deck)
.let { (drawPile, discardPile) ->
createHandsFromDrawPile(drawPile)
.let { (drawPile, playersHands) ->
Triple(drawPile, discardPile, playersHands)
}
}
return GameStartedEvent( return GameStartedEvent(
aggregateId = aggregateId, aggregateId = aggregateId,
firstPlayer = players.random().id, firstPlayer = players.randomPlayer().id,
version = version + 1, version = version + 1,
drawPile = drawPile, drawPile = drawPile,
discardPile = discardPile, discardPile = discardPile,
playersHans = playersHands,
).checkState( ).checkState(
{ players.size > 1 }, { players.size > 1 },
{ NeedMorePlayersToStartGameException(players) }, { NeedMorePlayersToStartGameException(players) },
@@ -50,7 +55,8 @@ data class GameCreated(
).checkState( ).checkState(
{ deck.size == 108 }, { deck.size == 108 },
{ DeckMissingCardsException(players, deck) }, { DeckMissingCardsException(players, deck) },
).run(::applyEvent) ).also { if (it.drawPile.size + it.discardPile.size + playersHands.values.sumOf { it.size } != 108) error("missing cards!") }
.run(::applyEvent)
} }
private fun initPiles(deck: Set<Card>): Pair<DrawPile, DiscardPile> = private fun initPiles(deck: Set<Card>): Pair<DrawPile, DiscardPile> =
@@ -61,6 +67,20 @@ data class GameCreated(
draw to DiscardPile(cards) draw to DiscardPile(cards)
} }
private fun createHandsFromDrawPile(drawPile: DrawPile) =
players
.map { it.id }
.fold(Pair(drawPile, emptyMap<Player.PlayerId, PlayerHand>())) { (drawAcc, handsAcc), playerId ->
drawAcc
.take(7)
.let { (draw, hand) ->
Pair(
draw,
handsAcc + (playerId to PlayerHand(hand)),
)
}
}
fun userJoinTheGame( fun userJoinTheGame(
userId: UserId, userId: UserId,
name: String, name: String,
@@ -107,8 +127,13 @@ data class GameCreated(
internal fun applyEvent(event: GameStartedEvent): GameStarted = internal fun applyEvent(event: GameStartedEvent): GameStarted =
GameStarted( GameStarted(
aggregateId = event.aggregateId, aggregateId = event.aggregateId,
players = players, players =
lastPlayerId = event.firstPlayer, players
.map {
it.copy(hand = event.playersHans[it.id] ?: error("Player ${it.id} not found"))
}.let { PlayerList(it.toSet()) },
lastPlayerId = null,
nextPlayerId = event.firstPlayer,
drawPile = event.drawPile, drawPile = event.drawPile,
discardPile = event.discardPile, discardPile = event.discardPile,
version = event.version + 1, version = event.version + 1,
@@ -133,7 +158,22 @@ fun newDeck(): Deck =
(1..2).map { Card.PassCard(color) } (1..2).map { Card.PassCard(color) }
}.let { }.let {
it + (1..4).map { Card.Plus4Card() } it + (1..4).map { Card.Plus4Card() }
}.let {
it + (1..4).map { Card.ChangeColorCard() }
}.toSet() }.toSet()
fun Set<Card>.shuffleDeck() = fun Set<Card>.shuffleDeck(): Set<Card> {
shuffled().toSet() if (isDisabled) return this
return shuffled().toSet()
}
private fun PlayerList.randomPlayer(): Player {
if (isDisabled) return first()
return random()
}
private var isDisabled = false
fun disableRandomForTest() {
isDisabled = true
}
@@ -16,6 +16,5 @@ data class GameEnded(
if (!players.map { it.id }.containsAll(playerWins)) { if (!players.map { it.id }.containsAll(playerWins)) {
throw IllegalArgumentException("Player ${players.map { it.id }} were not in players") throw IllegalArgumentException("Player ${players.map { it.id }} were not in players")
} }
checkRecorderEventsConsistency()
} }
} }
@@ -23,12 +23,39 @@ import eventDemo.contexts.game.domain.game.errors.ThePlayerIsNotInTheGameExcepti
import eventDemo.contexts.game.domain.game.errors.ThePlayerMustPlayACardException import eventDemo.contexts.game.domain.game.errors.ThePlayerMustPlayACardException
import eventDemo.contexts.game.domain.game.gameState.Game.Direction import eventDemo.contexts.game.domain.game.gameState.Game.Direction
fun PlayerList.nextPlayerTurn(
lastPlayerId: Player.PlayerId,
direction: Direction,
): Player.PlayerId {
val lastPlayer = get(lastPlayerId)
val playersLastTurn = filter { it.hand.cards.isNotEmpty() || it == lastPlayer }
return playersLastTurn
.indexOf(lastPlayer)
.let { lastPlayerIndex ->
if (direction == Direction.CLOCKWISE) {
if (lastPlayerIndex == playersLastTurn.size - 1) {
0
} else {
lastPlayerIndex + 1
}
} else {
if (lastPlayerIndex == 0) {
playersLastTurn.size - 1
} else {
lastPlayerIndex - 1
}
}
}.let { nextPlayerIndex -> elementAt(nextPlayerIndex).id }
}
data class GameStarted( data class GameStarted(
override val aggregateId: GameId, override val aggregateId: GameId,
override val players: PlayerList, override val players: PlayerList,
val drawPile: DrawPile, val drawPile: DrawPile,
val discardPile: DiscardPile, val discardPile: DiscardPile,
val lastPlayerId: Player.PlayerId, val lastPlayerId: Player.PlayerId?,
val nextPlayerId: Player.PlayerId,
val currentColor: Color, val currentColor: Color,
val playedTurnHistory: List<History> = emptyList(), val playedTurnHistory: List<History> = emptyList(),
val direction: Direction = Direction.CLOCKWISE, val direction: Direction = Direction.CLOCKWISE,
@@ -40,41 +67,15 @@ data class GameStarted(
val lastPlayedCard: Card? by lazy { discardPile.topCard } val lastPlayedCard: Card? by lazy { discardPile.topCard }
val lastPlayed: Player by lazy { players.get(lastPlayerId) }
init {
checkRecorderEventsConsistency()
}
data class History( data class History(
val playerId: Player.PlayerId, val playerId: Player.PlayerId,
val event: GameEvent, val event: GameEvent,
val direction: Direction, val direction: Direction,
) )
val lastPlayer by lazy { players.get(lastPlayerId) } val lastPlayer: Player? by lazy { lastPlayerId?.let { players.get(it) } }
val nextPlayer: Player by lazy { val nextPlayer: Player by lazy { players.get(nextPlayerId) }
val playersLastTurn = players.filter { it.hand.cards.isNotEmpty() || it == lastPlayer }
playersLastTurn
.indexOf(lastPlayer)
.let { lastPlayerIndex ->
if (direction == Direction.CLOCKWISE) {
if (lastPlayerIndex == playersLastTurn.size - 1) {
0
} else {
lastPlayerIndex + 1
}
} else {
if (lastPlayerIndex == 0) {
playersLastTurn.size - 1
} else {
lastPlayerIndex - 1
}
}
}.let { nextPlayerIndex -> players.elementAt(nextPlayerIndex) }
}
fun canBePlayThisCard(card: Card): Boolean { fun canBePlayThisCard(card: Card): Boolean {
val cardOnBoard = discardPile.topCard ?: return false val cardOnBoard = discardPile.topCard ?: return false
@@ -144,8 +145,12 @@ data class GameStarted(
): GameStarted = ): GameStarted =
CardIsPlayedEvent(aggregateId, card, playerId, chosenColor, version + 1) CardIsPlayedEvent(aggregateId, card, playerId, chosenColor, version + 1)
.checkPlayerTurn() .checkPlayerTurn()
.checkState({ card is Card.CardWithColor && chosenColor != null }, { TheCardIsAColorCardException(playerId) }) .checkState({
.checkState({ card is Card.CardWith4Color && chosenColor == null }, { TheCardHasNoColorException(playerId) }) (card is Card.CardWithColor && chosenColor == null) || card is Card.CardWith4Color
}, { TheCardIsAColorCardException(playerId) })
.checkState({
(card is Card.CardWith4Color && chosenColor != null) || card is Card.CardWithColor
}, { TheCardHasNoColorException(playerId) })
.run(::applyEvent) .run(::applyEvent)
internal fun applyEvent(event: CardIsPlayedEvent): GameStarted = internal fun applyEvent(event: CardIsPlayedEvent): GameStarted =
@@ -167,6 +172,7 @@ data class GameStarted(
discardPile = discardPile.withNewCard(card = event.card), discardPile = discardPile.withNewCard(card = event.card),
currentColor = color, currentColor = color,
lastPlayerId = event.playerId, lastPlayerId = event.playerId,
nextPlayerId = players.nextPlayerTurn(event.playerId, nextDirectionAfterPlay),
playedTurnHistory = playedTurnHistory - History(event.playerId, event, direction), playedTurnHistory = playedTurnHistory - History(event.playerId, event, direction),
direction = nextDirectionAfterPlay, direction = nextDirectionAfterPlay,
version = event.version, version = event.version,
@@ -202,6 +208,8 @@ data class GameStarted(
copy( copy(
players = players.withNewCardOnPlayerHand(event.playerId, event.takenCards), players = players.withNewCardOnPlayerHand(event.playerId, event.takenCards),
drawPile = drawPile.take(event.takenCards.size).first, drawPile = drawPile.take(event.takenCards.size).first,
lastPlayerId = event.playerId,
nextPlayerId = players.nextPlayerTurn(event.playerId, direction),
version = event.version, version = event.version,
recordedEvents = recordedEvents + event, recordedEvents = recordedEvents + event,
) )
@@ -16,7 +16,7 @@ fun Route.gameWebSocket(channelSubscriber: GameChannelsSubscriber) {
authenticate { authenticate {
webSocket("/games/{id}") { webSocket("/games/{id}") {
channelSubscriber.subscribePlayerToGameChannels( channelSubscriber.subscribePlayerToGameChannels(
gameId = GameId(UUID.nameUUIDFromBytes(call.parameters["id"]?.encodeToByteArray()!!)), gameId = GameId(UUID.fromString(call.parameters["id"]!!)),
userId = call.currentUserId, userId = call.currentUserId,
incomingCommandChannel = toObjectChannel(incoming), incomingCommandChannel = toObjectChannel(incoming),
sendNotificationChannel = fromFrameChannel(outgoing), sendNotificationChannel = fromFrameChannel(outgoing),
@@ -78,7 +78,11 @@ class BusInRabbitMQ<E>(
body: ByteArray, body: ByteArray,
) { ) {
runBlocking { runBlocking {
block(stringToObject(body.toString(Charsets.UTF_8))) val obj = stringToObject(body.toString(Charsets.UTF_8))
withLoggingContext("item" to obj.toString()) {
logger.info { "Received delivery of $exchangeName" }
}
block(obj)
} }
channel.basicAck(envelope.deliveryTag, false) channel.basicAck(envelope.deliveryTag, false)
} }
@@ -1,6 +1,8 @@
package eventDemo.contexts.game.application package eventDemo.contexts.game.application
import eventDemo.Tag import eventDemo.Tag
import eventDemo.contexts.auth.application.eventStores.UserRepository
import eventDemo.contexts.auth.domain.User
import eventDemo.contexts.game.application.channels.GameChannelsSubscriber import eventDemo.contexts.game.application.channels.GameChannelsSubscriber
import eventDemo.contexts.game.application.command.models.GameCommand import eventDemo.contexts.game.application.command.models.GameCommand
import eventDemo.contexts.game.application.eventStores.GameRepository import eventDemo.contexts.game.application.eventStores.GameRepository
@@ -11,11 +13,11 @@ import eventDemo.contexts.game.application.notification.models.PlayerAsPlayACard
import eventDemo.contexts.game.application.notification.models.PlayerWasReadyNotification import eventDemo.contexts.game.application.notification.models.PlayerWasReadyNotification
import eventDemo.contexts.game.application.notification.models.TheGameWasStartedNotification import eventDemo.contexts.game.application.notification.models.TheGameWasStartedNotification
import eventDemo.contexts.game.application.notification.models.WelcomeToTheGameNotification import eventDemo.contexts.game.application.notification.models.WelcomeToTheGameNotification
import eventDemo.contexts.game.domain.events.disableShuffleDeck
import eventDemo.contexts.game.domain.game.Card import eventDemo.contexts.game.domain.game.Card
import eventDemo.contexts.game.domain.game.GameId import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.gameState.Game import eventDemo.contexts.game.domain.game.gameState.Game
import eventDemo.contexts.game.domain.game.gameState.GameStarted import eventDemo.contexts.game.domain.game.gameState.GameStarted
import eventDemo.contexts.game.domain.game.gameState.disableRandomForTest
import eventDemo.testHelpers.CreateGameWithCommandsInChannelsHelpers.createGameWithCommandsInChannels import eventDemo.testHelpers.CreateGameWithCommandsInChannelsHelpers.createGameWithCommandsInChannels
import eventDemo.testHelpers.CreateGameWithCommandsInChannelsHelpers.joinTheGame import eventDemo.testHelpers.CreateGameWithCommandsInChannelsHelpers.joinTheGame
import eventDemo.testHelpers.CreateGameWithCommandsInChannelsHelpers.playCard import eventDemo.testHelpers.CreateGameWithCommandsInChannelsHelpers.playCard
@@ -25,7 +27,11 @@ import eventDemo.testHelpers.testKoinApplicationWithConfig
import io.kotest.assertions.nondeterministic.eventually import io.kotest.assertions.nondeterministic.eventually
import io.kotest.assertions.nondeterministic.until import io.kotest.assertions.nondeterministic.until
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.equals.shouldBeEqual import io.kotest.matchers.equals.shouldBeEqual
import io.kotest.matchers.equals.shouldEqual
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.types.shouldBeInstanceOf
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@@ -44,7 +50,7 @@ class GameSimulationTest :
test("Simulation of a game") { test("Simulation of a game") {
withTimeout(10.seconds) { withTimeout(10.seconds) {
disableShuffleDeck() disableRandomForTest()
val gameId = GameId() val gameId = GameId()
val user1 = createNewUser("user1") val user1 = createNewUser("user1")
val user2 = createNewUser("user2") val user2 = createNewUser("user2")
@@ -61,6 +67,11 @@ class GameSimulationTest :
testKoinApplicationWithConfig { testKoinApplicationWithConfig {
val gameRepository = get<GameRepository>() val gameRepository = get<GameRepository>()
val userRepository = get<UserRepository>()
userRepository.run {
save(user1)
save(user2)
}
gameRepository.create(gameId) gameRepository.create(gameId)
@@ -104,8 +115,9 @@ class GameSimulationTest :
// Player 1 actions // Player 1 actions
val player1Job = val player1Job =
launch { launch {
createGameWithCommandsInChannels(channelCommand1) { getPlayer -> createGameWithCommandsInChannels(channelCommand1, gameId, user1) {
user1.joinTheGame()
joinTheGame()
player1Notifications.waitNotification<WelcomeToTheGameNotification> { player1Notifications.waitNotification<WelcomeToTheGameNotification> {
players.map { it.userId }.contains(user1.id) players.map { it.userId }.contains(user1.id)
} }
@@ -116,20 +128,38 @@ class GameSimulationTest :
player.userId == user2.id player.userId == user2.id
} }
getPlayer(user1).readyToPlay() readyToPlay()
player1Notifications.waitNotification<PlayerWasReadyNotification> { player1Notifications.waitNotification<PlayerWasReadyNotification> {
playerId == getPlayer(user2).id playerId == getPlayer(user2).id
} }
val player1Hand = playedCard1 =
player1Notifications.waitNotification<TheGameWasStartedNotification> { hand.size == 7 }.hand player1Notifications
.waitNotification<TheGameWasStartedNotification> { hand.size == 7 }
.hand
.first()
.apply {
this.shouldBeInstanceOf<Card.NumericCard>()
number shouldEqual 1
color shouldEqual Card.Color.Red
}
playedCard1 = player1Hand.first()
player1Notifications.waitNotification<ItsTheTurnOfNotification> { player1Notifications.waitNotification<ItsTheTurnOfNotification> {
if (player.userId == user2.id) error("WRONG PLAYER TURN")
player.userId == user1.id player.userId == user1.id
} }
getPlayer(user1).playCard(playedCard1!!, null) game
.shouldBeInstanceOf<GameStarted>()
.discardPile
.topCard
.shouldNotBeNull()
.shouldBeInstanceOf<Card.NumericCard> {
it.number shouldEqual 0
it.color shouldEqual Card.Color.Red
}
playCard(playedCard1!!)
player1Notifications.waitNotification<ItsTheTurnOfNotification> { player1Notifications.waitNotification<ItsTheTurnOfNotification> {
player == getPlayer(user2) player == getPlayer(user2)
@@ -139,8 +169,18 @@ class GameSimulationTest :
playerId == getPlayer(user2).id && card == playedCard2 playerId == getPlayer(user2).id && card == playedCard2
} }
playedCard1 = player1Hand.elementAt(1) playedCard1 =
getPlayer(user1).playCard(playedCard1) assertInstanceOf<GameStarted>(game)
.playableCards(currentPlayer.id)
.first()
playedCard1.run {
this.shouldBeInstanceOf<Card.NumericCard>()
number shouldEqual 2
color shouldEqual Card.Color.Red
}
playCard(playedCard1)
player1Notifications.waitNotification<ItsTheTurnOfNotification> { player1Notifications.waitNotification<ItsTheTurnOfNotification> {
player == getPlayer(user2) player == getPlayer(user2)
@@ -151,11 +191,11 @@ class GameSimulationTest :
// Player 2 actions // Player 2 actions
val player2Job = val player2Job =
launch { launch {
createGameWithCommandsInChannels(channelCommand2) { getPlayer -> createGameWithCommandsInChannels(channelCommand2, gameId, user2) {
// wait player 1 has joined the game // wait player 1 has joined the game
until(3.seconds) { player1HasJoin } until(3.seconds) { player1HasJoin }
user2.joinTheGame() joinTheGame()
player2Notifications.waitNotification<WelcomeToTheGameNotification> { player2Notifications.waitNotification<WelcomeToTheGameNotification> {
players.map { it.userId }.contains(user1.id) && players.map { it.userId }.contains(user1.id) &&
@@ -163,30 +203,48 @@ class GameSimulationTest :
} }
player2Notifications.waitNotification<PlayerWasReadyNotification> { playerId == getPlayer(user1).id } player2Notifications.waitNotification<PlayerWasReadyNotification> { playerId == getPlayer(user1).id }
getPlayer(user2).readyToPlay() readyToPlay()
val player2Hand = playedCard2 =
player2Notifications.waitNotification<TheGameWasStartedNotification> { hand.size == 7 }.hand player2Notifications
.waitNotification<TheGameWasStartedNotification> { hand.size == 7 }
.hand
.first()
.apply {
this.shouldBeInstanceOf<Card.NumericCard>()
number shouldEqual 8
color shouldEqual Card.Color.Red
}
player2Notifications.waitNotification<ItsTheTurnOfNotification> { player2Notifications.waitNotification<ItsTheTurnOfNotification> {
if (player.userId == user2.id) error("WRONG PLAYER TURN")
player.userId == user1.id player.userId == user1.id
} }
player2Notifications.waitNotification<PlayerAsPlayACardNotification> { player2Notifications.waitNotification<PlayerAsPlayACardNotification> {
playerId == getPlayer(user1).id && card == playedCard1 playerId == getPlayer(user1).id && card == playedCard1
} }
playedCard2 = player2Hand.first()
player2Notifications.waitNotification<ItsTheTurnOfNotification> { player2Notifications.waitNotification<ItsTheTurnOfNotification> {
player == getPlayer(user2) player == currentPlayer
} }
getPlayer(user2).playCard(playedCard2) game
.shouldBeInstanceOf<GameStarted>()
.discardPile
.topCard
.shouldNotBeNull()
.shouldBeInstanceOf<Card.NumericCard> {
it.number shouldEqual 1
it.color shouldEqual Card.Color.Red
}
playCard(playedCard2)
player2Notifications.waitNotification<ItsTheTurnOfNotification> { player2Notifications.waitNotification<ItsTheTurnOfNotification> {
player.userId == user1.id player.userId == user1.id
} }
player2Notifications.waitNotification<PlayerAsPlayACardNotification> { player2Notifications.waitNotification<PlayerAsPlayACardNotification> {
playerId == getPlayer(user2).id && card == playedCard2 playerId == currentPlayer.id && card == playedCard2
} }
} }
} }
@@ -200,20 +258,25 @@ class GameSimulationTest :
// Check if the state is correct // Check if the state is correct
game.aggregateId shouldBeEqual gameId game.aggregateId shouldBeEqual gameId
game.players.map { it.id } shouldBeEqual setOf(user1.id, user2.id) game.players.map { it.userId } shouldContainExactly setOf(user1.id, user2.id)
assertNotNull(game.players.find { it.userId == user1.id }) assertNotNull(game.players.find { it.userId == user1.id })
.hand.size shouldBeEqual 5 .hand.size shouldBeEqual 5
assertNotNull(game.players.find { it.userId == user2.id }) assertNotNull(game.players.find { it.userId == user2.id })
.hand.size shouldBeEqual 6 .hand.size shouldBeEqual 6
game.direction shouldBeEqual Game.Direction.CLOCKWISE game.direction shouldBeEqual Game.Direction.CLOCKWISE
assertNotNull(game.lastPlayed.userId) shouldBeEqual user1 assertNotNull(game.lastPlayer?.userId) shouldBeEqual user1.id
assertNotNull(game.discardPile.topCard) shouldBeEqual assertNotNull(playedCard1) assertNotNull(game.discardPile.topCard) shouldBeEqual assertNotNull(playedCard1)
} }
} }
} }
}) })
private suspend inline fun <reified T : Notification> MutableList<Notification>.waitNotification(crossinline block: T.() -> Boolean): T = context(user: User)
eventually(3.seconds) { private suspend inline fun <reified T : Notification> MutableList<Notification>.waitNotification(crossinline block: T.() -> Boolean): T {
filterIsInstance<T>().first { block(it) } println("NOTIFICATION WAITING: ${T::class.simpleName} for user: ${user.username}")
} return eventually(3.seconds) {
filterIsInstance<T>()
.first { block(it) }
.also { remove(it) }
}.also { println("NOTIFICATION RECEIVED: ${T::class.simpleName} for user: ${user.username}") }
}
@@ -1,6 +1,7 @@
package eventDemo.contexts.game.application.notification package eventDemo.contexts.game.application.notification
import eventDemo.contexts.auth.application.eventStores.UserEventStoreRepository import eventDemo.contexts.auth.application.eventStores.UserEventStoreRepository
import eventDemo.contexts.auth.application.eventStores.UserRepository
import eventDemo.contexts.auth.infrastructure.persistence.eventStore.UserEventStoreInMemory import eventDemo.contexts.auth.infrastructure.persistence.eventStore.UserEventStoreInMemory
import eventDemo.contexts.game.application.command.handlers.GameCommandHandlerDispatcher import eventDemo.contexts.game.application.command.handlers.GameCommandHandlerDispatcher
import eventDemo.contexts.game.application.command.handlers.JoinTheGameHandler import eventDemo.contexts.game.application.command.handlers.JoinTheGameHandler
@@ -14,6 +15,8 @@ import eventDemo.contexts.game.application.notification.models.WelcomeToTheGameN
import eventDemo.contexts.game.infrastructure.persistence.eventBus.GameEventBusInMemory import eventDemo.contexts.game.infrastructure.persistence.eventBus.GameEventBusInMemory
import eventDemo.contexts.game.infrastructure.persistence.eventStore.GameEventStoreInMemory import eventDemo.contexts.game.infrastructure.persistence.eventStore.GameEventStoreInMemory
import eventDemo.sharedKernel.UserId import eventDemo.sharedKernel.UserId
import eventDemo.testHelpers.createNewUser
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.equals.shouldEqual import io.kotest.matchers.equals.shouldEqual
@@ -21,6 +24,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.junit.jupiter.api.assertInstanceOf import org.junit.jupiter.api.assertInstanceOf
import kotlin.time.Duration.Companion.seconds
class EventToNotificationSubscriberTest : class EventToNotificationSubscriberTest :
FunSpec({ FunSpec({
@@ -46,8 +50,12 @@ class EventToNotificationSubscriberTest :
val game = gameRepository.create() val game = gameRepository.create()
val user1 = UserId() val user1 = createNewUser("user1")
val user2 = UserId() val user2 = createNewUser("user2")
userRepository.run {
save(user1)
save(user2)
}
val player1Notifications = mutableListOf<Notification>() val player1Notifications = mutableListOf<Notification>()
GlobalScope.launch { GlobalScope.launch {
@@ -56,20 +64,22 @@ class EventToNotificationSubscriberTest :
} }
} }
commentDispatcher.dispatch(JoinTheGameCommand(user1, JoinTheGameCommand.Payload(game.aggregateId))) commentDispatcher.dispatch(JoinTheGameCommand(user1.id, JoinTheGameCommand.Payload(game.aggregateId)))
subscriber subscriber
.subscribeToEventsAndSendNotification( .subscribeToEventsAndSendNotification(
game.aggregateId, game.aggregateId,
user1, user2.id,
notificationChannel, notificationChannel,
).use { ).use {
commentDispatcher.dispatch(JoinTheGameCommand(user2, JoinTheGameCommand.Payload(game.aggregateId))) commentDispatcher.dispatch(JoinTheGameCommand(user2.id, JoinTheGameCommand.Payload(game.aggregateId)))
} }
player1Notifications.size shouldEqual 2 eventually(duration = 1.seconds) {
player1Notifications.size shouldEqual 1
}
player1Notifications.first().let { notification -> player1Notifications.first().let { notification ->
assertInstanceOf<WelcomeToTheGameNotification>(notification) assertInstanceOf<WelcomeToTheGameNotification>(notification)
notification.players.map { it.userId } shouldContain user1 notification.players.map { it.userId } shouldContain user2.id
} }
} }
}) })
@@ -0,0 +1,301 @@
package eventDemo.contexts.game.application.notification
import eventDemo.contexts.game.application.notification.models.ItsTheTurnOfNotification
import eventDemo.contexts.game.application.notification.models.PilesShuffledNotification
import eventDemo.contexts.game.application.notification.models.PlayerAsPlayACardNotification
import eventDemo.contexts.game.application.notification.models.PlayerHavePassNotification
import eventDemo.contexts.game.application.notification.models.PlayerWasReadyNotification
import eventDemo.contexts.game.application.notification.models.TheGameWasStartedNotification
import eventDemo.contexts.game.application.notification.models.WelcomeToTheGameNotification
import eventDemo.contexts.game.application.notification.models.YourNewCardNotification
import eventDemo.contexts.game.domain.events.CardIsPlayedEvent
import eventDemo.contexts.game.domain.events.DrawFilledWithDiscardEvent
import eventDemo.contexts.game.domain.events.GameStartedEvent
import eventDemo.contexts.game.domain.events.NewPlayerEvent
import eventDemo.contexts.game.domain.events.PlayerHaveDrawCardEvent
import eventDemo.contexts.game.domain.events.PlayerReadyEvent
import eventDemo.contexts.game.domain.game.Card
import eventDemo.contexts.game.domain.game.DiscardPile
import eventDemo.contexts.game.domain.game.DrawPile
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.Player
import eventDemo.contexts.game.domain.game.PlayerHand
import eventDemo.contexts.game.domain.game.PlayerList
import eventDemo.contexts.game.domain.game.gameState.GameCreated
import eventDemo.contexts.game.domain.game.gameState.GameStarted
import eventDemo.sharedKernel.UserId
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.assertInstanceOf
class ToNotificationTest :
FunSpec({
val player1 =
Player(
name = "Bob",
userId = UserId(),
hand = PlayerHand(setOf(Card.NumericCard(1, Card.Color.Red))),
id = Player.PlayerId(),
)
val player2 =
Player(
name = "John",
userId = UserId(),
hand = PlayerHand(setOf(Card.NumericCard(1, Card.Color.Red))),
id = Player.PlayerId(),
)
test("NewPlayerEvent") {
val game =
GameCreated(
aggregateId = GameId(),
version = 1,
players = PlayerList(setOf(player1)),
recordedEvents = setOf(),
)
NewPlayerEvent(
game.aggregateId,
version = 2,
player = player1,
).toNotification(
game = game,
currentUserId = player1.userId,
).let {
it.toList().size shouldBe 1
// Check if the user is
assertInstanceOf<WelcomeToTheGameNotification>(it.first()).run {
players.size shouldBe 1
players.first().name shouldBe "Bob"
}
}
}
test("PlayerReadyEvent") {
val game =
GameCreated(
aggregateId = GameId(),
version = 1,
players = PlayerList(setOf(player1)),
recordedEvents = setOf(),
)
PlayerReadyEvent(
game.aggregateId,
version = 2,
playerId = player1.id,
).toNotification(
game = game,
currentUserId = player1.userId,
).let {
it.toList().size shouldBe 1
assertInstanceOf<PlayerWasReadyNotification>(it.first()).let {
it.playerId shouldBe player1.id
}
}
}
test("PlayerHaveDrawCardEvent on current player") {
val game =
GameStarted(
aggregateId = GameId(),
players = PlayerList(setOf(player1, player2)),
drawPile = DrawPile(),
discardPile = DiscardPile(),
lastPlayerId = player1.id,
nextPlayerId = player2.id,
currentColor = Card.Color.Red,
version = 1,
recordedEvents = setOf(),
)
val card = Card.NumericCard(1, Card.Color.Blue)
PlayerHaveDrawCardEvent(
game.aggregateId,
version = 2,
playerId = player1.id,
takenCards = setOf(card),
).toNotification(
game = game,
currentUserId = player1.userId,
).let {
it.toList().size shouldBe 2
it.toList().let { notifications ->
assertInstanceOf<YourNewCardNotification>(notifications.first()).let {
it.cards.first() shouldBe card
}
assertInstanceOf<ItsTheTurnOfNotification>(notifications[1]).let {
it.player.id shouldBe player2.id
}
}
}
}
test("PlayerHaveDrawCardEvent on other player") {
val game =
GameStarted(
aggregateId = GameId(),
players = PlayerList(setOf(player1, player2)),
drawPile = DrawPile(),
discardPile = DiscardPile(),
lastPlayerId = player1.id,
nextPlayerId = player2.id,
currentColor = Card.Color.Red,
version = 1,
recordedEvents = setOf(),
)
val card = Card.NumericCard(1, Card.Color.Blue)
PlayerHaveDrawCardEvent(
game.aggregateId,
version = 2,
playerId = player1.id,
takenCards = setOf(card),
).toNotification(
game = game,
currentUserId = player2.userId,
).let {
it.toList().size shouldBe 2
it.toList().let { notifications ->
assertInstanceOf<PlayerHavePassNotification>(notifications.first()).let {
it.playerId shouldBe player1.id
}
assertInstanceOf<ItsTheTurnOfNotification>(notifications[1]).let {
it.player.id shouldBe player2.id
}
}
}
}
test("CardIsPlayedEvent on current player") {
val game =
GameStarted(
aggregateId = GameId(),
players = PlayerList(setOf(player1, player2)),
drawPile = DrawPile(),
discardPile = DiscardPile(),
lastPlayerId = player2.id,
nextPlayerId = player1.id,
currentColor = Card.Color.Red,
version = 1,
recordedEvents = setOf(),
)
val card = Card.NumericCard(1, Card.Color.Blue)
CardIsPlayedEvent(
game.aggregateId,
version = 2,
playerId = player1.id,
card = card,
).toNotification(
game = game,
currentUserId = player1.userId,
).toList()
.let { notifications ->
notifications.size shouldBe 2
assertInstanceOf<PlayerAsPlayACardNotification>(notifications.first()).let {
it.playerId shouldBe player1.id
it.card shouldBe card
}
assertInstanceOf<ItsTheTurnOfNotification>(notifications[1]).let {
it.player.id shouldBe player1.id
}
}
}
test("CardIsPlayedEvent on other player") {
val game =
GameStarted(
aggregateId = GameId(),
players = PlayerList(setOf(player1, player2)),
drawPile = DrawPile(),
discardPile = DiscardPile(),
lastPlayerId = player1.id,
nextPlayerId = player2.id,
currentColor = Card.Color.Red,
version = 1,
recordedEvents = setOf(),
)
val card = Card.NumericCard(1, Card.Color.Blue)
CardIsPlayedEvent(
game.aggregateId,
version = 2,
playerId = player2.id,
card = card,
).toNotification(
game = game,
currentUserId = player1.userId,
).let {
it.toList().size shouldBe 2
it.toList().let { notifications ->
assertInstanceOf<PlayerAsPlayACardNotification>(notifications.first()).let {
it.playerId shouldBe player2.id
it.card shouldBe card
}
assertInstanceOf<ItsTheTurnOfNotification>(notifications[1]).let {
it.player.id shouldBe player2.id
}
}
}
}
test("DrawFilledWithDiscardEvent") {
val game =
GameStarted(
aggregateId = GameId(),
players = PlayerList(setOf(player1, player2)),
drawPile = DrawPile(),
discardPile = DiscardPile(),
lastPlayerId = player1.id,
nextPlayerId = player2.id,
currentColor = Card.Color.Red,
version = 1,
recordedEvents = setOf(),
)
DrawFilledWithDiscardEvent(
game.aggregateId,
version = 2,
newDrawPile = DrawPile(),
newDiscardPile = DiscardPile(),
).toNotification(
game = game,
currentUserId = player1.userId,
).let {
it.toList().size shouldBe 1
assertInstanceOf<PilesShuffledNotification>(it.first())
}
}
test("GameStartedEvent") {
val game =
GameStarted(
aggregateId = GameId(),
players = PlayerList(setOf(player1, player2)),
drawPile = DrawPile(),
discardPile = DiscardPile(),
lastPlayerId = player1.id,
nextPlayerId = player2.id,
currentColor = Card.Color.Red,
version = 1,
recordedEvents = setOf(),
)
GameStartedEvent(
game.aggregateId,
version = 2,
firstPlayer = player1.id,
playersHans =
mapOf(
player1.id to player1.hand,
player2.id to player2.hand,
),
drawPile = DrawPile(),
discardPile = DiscardPile(),
).toNotification(
game = game,
currentUserId = player1.userId,
).toList()
.let { notifications ->
notifications.size shouldBe 2
assertInstanceOf<TheGameWasStartedNotification>(notifications.first()).let {
it.hand.size shouldBe 1
it.hand.first() shouldBe player1.hand.cards.first()
}
assertInstanceOf<ItsTheTurnOfNotification>(notifications[1]).let {
it.player.id shouldBe player2.id
}
}
}
})
@@ -11,6 +11,7 @@ import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.Player import eventDemo.contexts.game.domain.game.Player
import eventDemo.contexts.game.domain.game.PlayerHand import eventDemo.contexts.game.domain.game.PlayerHand
import eventDemo.contexts.game.domain.game.PlayerList import eventDemo.contexts.game.domain.game.PlayerList
import eventDemo.contexts.game.domain.game.gameState.Game.Direction
import eventDemo.sharedKernel.UserId import eventDemo.sharedKernel.UserId
import eventDemo.testHelpers.act import eventDemo.testHelpers.act
import eventDemo.testHelpers.assert import eventDemo.testHelpers.assert
@@ -195,10 +196,12 @@ class GameStartedTest :
val card3 = Card.NumericCard(3, Card.Color.Red) val card3 = Card.NumericCard(3, Card.Color.Red)
assert { assert {
val player1 = Player("Player 1", UserId()) val player1 = Player("Player 1", UserId())
val player2 = Player("Player 2", UserId())
GameStarted( GameStarted(
aggregateId = GameId(), aggregateId = GameId(),
players = PlayerList(setOf(player1)), players = PlayerList(setOf(player1)),
lastPlayerId = player1.id, lastPlayerId = player1.id,
nextPlayerId = player2.id,
drawPile = DrawPile(), drawPile = DrawPile(),
discardPile = discardPile =
DiscardPile( DiscardPile(
@@ -244,17 +247,14 @@ class GameStartedTest :
val card3 = Card.NumericCard(3, Card.Color.Red) val card3 = Card.NumericCard(3, Card.Color.Red)
val card4 = Card.NumericCard(4, Card.Color.Red) val card4 = Card.NumericCard(4, Card.Color.Red)
val player1 = val player1 = Player("Jo", UserId())
Player( val player2 = Player("Bob", UserId())
"Jo",
UserId(),
hand = PlayerHand(cards = setOf()),
)
assert { assert {
GameStarted( GameStarted(
aggregateId = GameId(), aggregateId = GameId(),
players = PlayerList(setOf(player1)), players = PlayerList(setOf(player1)),
lastPlayerId = player1.id, lastPlayerId = player1.id,
nextPlayerId = player2.id,
drawPile = drawPile =
DrawPile( DrawPile(
setOf( setOf(
@@ -303,6 +303,7 @@ class GameStartedTest :
aggregateId = GameId(), aggregateId = GameId(),
players = PlayerList(setOf(player1, player2)), players = PlayerList(setOf(player1, player2)),
lastPlayerId = player1.id, lastPlayerId = player1.id,
nextPlayerId = player2.id,
drawPile = DrawPile(), drawPile = DrawPile(),
discardPile = DiscardPile(), discardPile = DiscardPile(),
currentColor = Card.Color.Yellow, currentColor = Card.Color.Yellow,
@@ -373,10 +374,12 @@ private fun gameWithCard(
val player1 = Player("Tesla", UserId(), hand = played1Hand) val player1 = Player("Tesla", UserId(), hand = played1Hand)
val player2 = Player("Einstein", UserId(), hand = played2Hand) val player2 = Player("Einstein", UserId(), hand = played2Hand)
val player3 = Player("Curie", UserId(), hand = PlayerHand(setOf(Card.NumericCard(8, Card.Color.Yellow)))) val player3 = Player("Curie", UserId(), hand = PlayerHand(setOf(Card.NumericCard(8, Card.Color.Yellow))))
val players = PlayerList(setOf(player1, player2, player3))
return GameStarted( return GameStarted(
aggregateId = GameId(), aggregateId = GameId(),
players = PlayerList(setOf(player1, player2, player3)), players = players,
lastPlayerId = player3.id, lastPlayerId = player3.id,
nextPlayerId = players.nextPlayerTurn(player3.id, Direction.CLOCKWISE),
discardPile = DiscardPile(setOf(onTheDiscardPile)), discardPile = DiscardPile(setOf(onTheDiscardPile)),
drawPile = DrawPile(), drawPile = DrawPile(),
currentColor = (onTheDiscardPile as? Card.CardWithColor)?.color ?: chosenColor ?: error("no color"), currentColor = (onTheDiscardPile as? Card.CardWithColor)?.color ?: chosenColor ?: error("no color"),
@@ -0,0 +1,41 @@
package eventDemo.contexts.game.domain.game.gameState
import eventDemo.contexts.game.domain.game.Card
import io.kotest.assertions.retry
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import kotlin.time.Duration.Companion.seconds
class NewDeckTest :
FunSpec({
test("newDeck") {
newDeck().let {
it shouldNotBe null
it.filterIsInstance<Card.NumericCard>() shouldHaveSize 76
it.filterIsInstance<Card.Plus2Card>() shouldHaveSize 8
it.filterIsInstance<Card.ReverseCard>() shouldHaveSize 8
it.filterIsInstance<Card.PassCard>() shouldHaveSize 8
it.filterIsInstance<Card.Plus4Card>() shouldHaveSize 4
it.filterIsInstance<Card.ChangeColorCard>() shouldHaveSize 4
it shouldHaveSize 108
}
}
test("shuffleDeck") {
val deck = (0..9).map { Card.NumericCard(it, Card.Color.Red) }
deck.run {
this[3].number shouldBe 3
}
should {
retry(maxRetry = 4, timeout = 1.seconds) {
deck.shuffled().run {
this[3].number shouldNotBe 3
}
}
}
}
})
@@ -117,8 +117,8 @@ class GameListRouteTest :
call.body<List<GameList>>().first().let { call.body<List<GameList>>().first().let {
it.status shouldBeEqual GameList.Status.IS_STARTED it.status shouldBeEqual GameList.Status.IS_STARTED
it.players shouldHaveSize 2 it.players shouldHaveSize 2
it.players.map { it.userId } shouldContain user1 it.players.map { it.userId } shouldContain user1.id
it.players.map { it.userId } shouldContain user2 it.players.map { it.userId } shouldContain user2.id
it.winners shouldHaveSize 0 it.winners shouldHaveSize 0
} }
} }
@@ -9,51 +9,70 @@ import eventDemo.contexts.game.application.eventStores.GameRepository
import eventDemo.contexts.game.domain.game.Card import eventDemo.contexts.game.domain.game.Card
import eventDemo.contexts.game.domain.game.GameId import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.Player import eventDemo.contexts.game.domain.game.Player
import eventDemo.contexts.game.domain.game.gameState.Game
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import org.koin.core.Koin import org.koin.core.Koin
import java.util.UUID
object CreateGameWithCommandsInChannelsHelpers { object CreateGameWithCommandsInChannelsHelpers {
class Data(
private val repo: GameRepository,
val gameId: GameId,
val currentUser: User,
) {
fun getPlayer(user: User): Player =
game.players.get(user.id)
val currentPlayer: Player get() = getPlayer(currentUser)
val game: Game
get() = repo.get(gameId)!!
}
context(koin: Koin) context(koin: Koin)
suspend fun <T> createGameWithCommandsInChannels( suspend fun <T> createGameWithCommandsInChannels(
channelCommand: Channel<GameCommand>, channelCommand: Channel<GameCommand>,
gameName: String = "testGame${UUID.randomUUID()}", gameId: GameId,
block: suspend context(Channel<GameCommand>, GameId) ((User) -> Player) -> T, user: User,
block:
suspend context(
CreateGameWithCommandsInChannelsHelpers,
Channel<GameCommand>,
User,
) Data.() -> T,
): T { ): T {
val gameId = GameId(UUID.nameUUIDFromBytes(gameName.encodeToByteArray()))
val repo = koin.get<GameRepository>() val repo = koin.get<GameRepository>()
repo.getOrCreate(gameId) repo.getOrCreate(gameId)
return with(gameId) { return with(channelCommand) {
with(channelCommand) { with(user) {
with(CreateGameWithCommandsInChannelsHelpers) { with(CreateGameWithCommandsInChannelsHelpers) {
block { user -> repo.get(gameId)!!.players.get(user.id) } Data(repo, gameId, user).block()
} }
} }
} }
} }
context(channelCommand: Channel<GameCommand>, gameId: GameId) context(channelCommand: Channel<GameCommand>, data: Data)
suspend fun User.joinTheGame(): JoinTheGameCommand = suspend fun joinTheGame(): JoinTheGameCommand =
JoinTheGameCommand( JoinTheGameCommand(
id, data.currentUser.id,
JoinTheGameCommand.Payload(gameId), JoinTheGameCommand.Payload(data.gameId),
).also { channelCommand.send(it) } ).also { channelCommand.send(it) }
context(channelCommand: Channel<GameCommand>, gameId: GameId) context(channelCommand: Channel<GameCommand>, data: Data)
suspend fun Player.readyToPlay(): ReadyToPlayCommand = suspend fun readyToPlay(): ReadyToPlayCommand =
ReadyToPlayCommand( ReadyToPlayCommand(
userId, data.currentUser.id,
ReadyToPlayCommand.Payload(gameId, id), ReadyToPlayCommand.Payload(data.gameId, data.currentPlayer.id),
).also { channelCommand.send(it) } ).also { channelCommand.send(it) }
context(channelCommand: Channel<GameCommand>, gameId: GameId) context(channelCommand: Channel<GameCommand>, data: Data)
suspend fun Player.playCard( suspend fun playCard(
card: Card, card: Card,
chosenColor: Card.Color? = null, chosenColor: Card.Color? = null,
): PlayCardCommand = ): PlayCardCommand =
PlayCardCommand( PlayCardCommand(
userId, data.currentUser.id,
PlayCardCommand.Payload(gameId, id, card, chosenColor), PlayCardCommand.Payload(data.gameId, data.currentPlayer.id, card, chosenColor),
).also { channelCommand.send(it) } ).also { channelCommand.send(it) }
} }