Improve concurrency
Fix GameState.currentPlayerTurn and nextPlayer Add ItsTheTurnOfNotification Improve test
This commit is contained in:
@@ -11,6 +11,7 @@ import java.util.UUID
|
||||
data class CardIsPlayedEvent(
|
||||
override val gameId: GameId,
|
||||
val card: Card,
|
||||
val player: Player,
|
||||
override val player: Player,
|
||||
override val eventId: UUID = UUID.randomUUID(),
|
||||
) : GameEvent
|
||||
) : GameEvent,
|
||||
PlayerActionEvent
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package eventDemo.app.event.event
|
||||
|
||||
import eventDemo.app.entity.Player
|
||||
|
||||
sealed interface PlayerActionEvent {
|
||||
val player: Player
|
||||
}
|
||||
@@ -10,8 +10,9 @@ import java.util.UUID
|
||||
*/
|
||||
data class PlayerChoseColorEvent(
|
||||
override val gameId: GameId,
|
||||
val player: Player,
|
||||
override val player: Player,
|
||||
val color: Card.Color,
|
||||
) : GameEvent {
|
||||
) : GameEvent,
|
||||
PlayerActionEvent {
|
||||
override val eventId: UUID = UUID.randomUUID()
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@ import java.util.UUID
|
||||
*/
|
||||
data class PlayerHavePassEvent(
|
||||
override val gameId: GameId,
|
||||
val player: Player,
|
||||
override val player: Player,
|
||||
val takenCard: Card,
|
||||
) : GameEvent {
|
||||
) : GameEvent,
|
||||
PlayerActionEvent {
|
||||
override val eventId: UUID = UUID.randomUUID()
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import kotlinx.serialization.Serializable
|
||||
data class GameState(
|
||||
val gameId: GameId,
|
||||
val players: Set<Player> = emptySet(),
|
||||
val lastPlayer: Player? = null,
|
||||
val lastCard: LastCard? = null,
|
||||
val lastColor: Card.Color? = null,
|
||||
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),
|
||||
@@ -42,8 +42,8 @@ data class GameState(
|
||||
return players.size == readyPlayers.size && players.all { readyPlayers.contains(it) }
|
||||
}
|
||||
|
||||
private val lastPlayerIndex: Int? get() {
|
||||
val i = players.indexOf(lastPlayer)
|
||||
private val currentPlayerIndex: Int? get() {
|
||||
val i = players.indexOf(currentPlayerTurn)
|
||||
return if (i == -1) {
|
||||
null
|
||||
} else {
|
||||
@@ -51,28 +51,42 @@ data class GameState(
|
||||
}
|
||||
}
|
||||
|
||||
private val nextPlayerIndex: Int get() {
|
||||
if (players.size == 0) return 0
|
||||
private fun nextPlayerIndex(direction: Direction): Int {
|
||||
if (players.isEmpty()) return 0
|
||||
|
||||
val y =
|
||||
if (direction == Direction.CLOCKWISE) {
|
||||
+1
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
|
||||
return ((lastPlayerIndex ?: 0) + y) % players.size
|
||||
}
|
||||
|
||||
val nextPlayer: Player? by lazy {
|
||||
if (players.isEmpty()) {
|
||||
null
|
||||
return if (direction == Direction.CLOCKWISE) {
|
||||
sidePlayerIndexClockwise
|
||||
} else {
|
||||
players.elementAt(nextPlayerIndex)
|
||||
sidePlayerIndexCounterClockwise
|
||||
}
|
||||
}
|
||||
|
||||
val Player.currentIndex: Int get() = players.indexOf(this)
|
||||
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) {
|
||||
@@ -82,8 +96,8 @@ data class GameState(
|
||||
}.let { it % players.size }
|
||||
|
||||
val Player.cardOnBoardIsForYou: Boolean get() {
|
||||
if (lastCard == null) error("No card")
|
||||
return this.playerDiffIndex(lastCard.player) == 1
|
||||
if (cardOnCurrentStack == null) error("No card")
|
||||
return this.playerDiffIndex(cardOnCurrentStack.player) == 1
|
||||
}
|
||||
|
||||
fun playableCards(player: Player): List<Card> =
|
||||
@@ -102,7 +116,7 @@ data class GameState(
|
||||
player: Player,
|
||||
card: Card,
|
||||
): Boolean {
|
||||
val cardOnBoard = lastCard?.card ?: return false
|
||||
val cardOnBoard = cardOnCurrentStack?.card ?: return false
|
||||
return when (cardOnBoard) {
|
||||
is Card.NumericCard -> {
|
||||
when (card) {
|
||||
@@ -134,7 +148,7 @@ data class GameState(
|
||||
is Card.ChangeColorCard -> {
|
||||
when (card) {
|
||||
is Card.AllColorCard -> true
|
||||
is Card.ColorCard -> card.color == lastColor
|
||||
is Card.ColorCard -> card.color == colorOnCurrentStack
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +170,7 @@ data class GameState(
|
||||
} else {
|
||||
when (card) {
|
||||
is Card.AllColorCard -> true
|
||||
is Card.ColorCard -> card.color == lastColor
|
||||
is Card.ColorCard -> card.color == colorOnCurrentStack
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,18 +7,22 @@ import eventDemo.app.event.event.CardIsPlayedEvent
|
||||
import eventDemo.app.event.event.GameEvent
|
||||
import eventDemo.app.event.event.GameStartedEvent
|
||||
import eventDemo.app.event.event.NewPlayerEvent
|
||||
import eventDemo.app.event.event.PlayerActionEvent
|
||||
import eventDemo.app.event.event.PlayerChoseColorEvent
|
||||
import eventDemo.app.event.event.PlayerHavePassEvent
|
||||
import eventDemo.app.event.event.PlayerReadyEvent
|
||||
import eventDemo.app.event.event.PlayerWinEvent
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
|
||||
fun GameId.buildStateFromEventStream(eventStream: GameEventStream): GameState {
|
||||
val events = eventStream.readAll(this)
|
||||
if (events.isEmpty()) return GameState(this)
|
||||
return events.buildStateFromEvents()
|
||||
return events.buildStateFromEvents().also {
|
||||
KotlinLogging.logger {}.warn { "state is build from scratch for game: $this " }
|
||||
}
|
||||
}
|
||||
|
||||
fun List<GameEvent>.buildStateFromEvents(): GameState {
|
||||
fun Collection<GameEvent>.buildStateFromEvents(): GameState {
|
||||
val gameId = this.firstOrNull()?.gameId ?: error("Cannot build GameState from an empty list")
|
||||
return fold(GameState(gameId)) { state, event ->
|
||||
state.apply(event)
|
||||
@@ -27,9 +31,14 @@ fun List<GameEvent>.buildStateFromEvents(): GameState {
|
||||
|
||||
fun GameState.apply(event: GameEvent): GameState =
|
||||
let { state ->
|
||||
if (event is PlayerActionEvent) {
|
||||
if (state.currentPlayerTurn != event.player) {
|
||||
error("inconsistent player turn. currentPlayerTurn: $currentPlayerTurn | player: ${event.player}")
|
||||
}
|
||||
}
|
||||
when (event) {
|
||||
is CardIsPlayedEvent -> {
|
||||
val direction =
|
||||
val nextDirectionAfterPlay =
|
||||
when (event.card) {
|
||||
is Card.ReverseCard -> state.direction.revert()
|
||||
else -> state.direction
|
||||
@@ -38,14 +47,21 @@ fun GameState.apply(event: GameEvent): GameState =
|
||||
val color =
|
||||
when (event.card) {
|
||||
is Card.ColorCard -> event.card.color
|
||||
else -> state.lastColor
|
||||
is Card.AllColorCard -> null
|
||||
}
|
||||
|
||||
val currentPlayerAfterThePlay =
|
||||
if (event.card is Card.AllColorCard) {
|
||||
currentPlayerTurn
|
||||
} else {
|
||||
nextPlayer(nextDirectionAfterPlay)
|
||||
}
|
||||
|
||||
state.copy(
|
||||
lastPlayer = event.player,
|
||||
direction = direction,
|
||||
lastColor = color,
|
||||
lastCard = GameState.LastCard(event.card, event.player),
|
||||
currentPlayerTurn = currentPlayerAfterThePlay,
|
||||
direction = nextDirectionAfterPlay,
|
||||
colorOnCurrentStack = color,
|
||||
cardOnCurrentStack = GameState.LastCard(event.card, event.player),
|
||||
deck = state.deck.putOneCardFromHand(event.player, event.card),
|
||||
)
|
||||
}
|
||||
@@ -59,6 +75,7 @@ fun GameState.apply(event: GameEvent): GameState =
|
||||
}
|
||||
|
||||
is PlayerReadyEvent -> {
|
||||
if (state.isStarted) error("The game is already started")
|
||||
state.copy(
|
||||
readyPlayers = state.readyPlayers + event.player,
|
||||
)
|
||||
@@ -67,22 +84,23 @@ fun GameState.apply(event: GameEvent): GameState =
|
||||
is PlayerHavePassEvent -> {
|
||||
if (event.takenCard != state.deck.stack.first()) error("taken card is not ot top of the stack")
|
||||
state.copy(
|
||||
lastPlayer = event.player,
|
||||
currentPlayerTurn = nextPlayerTurn,
|
||||
deck = state.deck.takeOneCardFromStackTo(event.player),
|
||||
)
|
||||
}
|
||||
|
||||
is PlayerChoseColorEvent -> {
|
||||
state.copy(
|
||||
lastColor = event.color,
|
||||
currentPlayerTurn = nextPlayerTurn,
|
||||
colorOnCurrentStack = event.color,
|
||||
)
|
||||
}
|
||||
|
||||
is GameStartedEvent -> {
|
||||
state.copy(
|
||||
lastColor = (event.deck.discard.first() as? Card.ColorCard)?.color ?: state.lastColor,
|
||||
lastCard = GameState.LastCard(event.deck.discard.first(), event.firstPlayer),
|
||||
lastPlayer = event.firstPlayer,
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -22,8 +22,7 @@ class GameStateRepository(
|
||||
val projection = projections[event.gameId]
|
||||
if (projection == null) {
|
||||
event
|
||||
.gameId
|
||||
.buildStateFromEventStream(eventStream)
|
||||
.buildStateFromEventStreamTo(eventStream)
|
||||
.update()
|
||||
} else {
|
||||
projection
|
||||
|
||||
Reference in New Issue
Block a user