Create GameStateRepository
Use GameState.apply() to build Projection Create GameEventHandler Add PlayerWinEvent
This commit is contained in:
27
src/main/kotlin/eventDemo/app/event/GameEventHandler.kt
Normal file
27
src/main/kotlin/eventDemo/app/event/GameEventHandler.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package eventDemo.app.event
|
||||
|
||||
import eventDemo.app.event.event.GameEvent
|
||||
|
||||
/**
|
||||
* A stream to publish and read the played card event.
|
||||
*/
|
||||
class GameEventHandler(
|
||||
private val eventBus: GameEventBus,
|
||||
private val eventStream: GameEventStream,
|
||||
) {
|
||||
private val projectionsBuilders: MutableList<(GameEvent) -> Unit> = mutableListOf()
|
||||
|
||||
fun registerProjectionBuilder(builder: GameProjectionBuilder) {
|
||||
projectionsBuilders.add(builder)
|
||||
}
|
||||
|
||||
fun handle(vararg events: GameEvent) {
|
||||
events.forEach { event ->
|
||||
eventStream.publish(event)
|
||||
projectionsBuilders.forEach { it(event) }
|
||||
eventBus.publish(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typealias GameProjectionBuilder = (GameEvent) -> Unit
|
||||
@@ -8,11 +8,9 @@ import eventDemo.libs.event.EventStream
|
||||
* A stream to publish and read the played card event.
|
||||
*/
|
||||
class GameEventStream(
|
||||
private val eventBus: GameEventBus,
|
||||
private val eventStream: EventStream<GameEvent, GameId>,
|
||||
) : EventStream<GameEvent, GameId> by eventStream {
|
||||
override fun publish(event: GameEvent) {
|
||||
eventStream.publish(event)
|
||||
eventBus.publish(event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,14 +17,15 @@ data class GameStartedEvent(
|
||||
fun new(
|
||||
id: GameId,
|
||||
players: Set<Player>,
|
||||
shuffleIsDisabled: Boolean = isDisabled,
|
||||
): GameStartedEvent =
|
||||
GameStartedEvent(
|
||||
gameId = id,
|
||||
firstPlayer = if (isDisabled) players.first() else players.random(),
|
||||
firstPlayer = if (shuffleIsDisabled) players.first() else players.random(),
|
||||
deck =
|
||||
Deck
|
||||
.newWithoutPlayers()
|
||||
.let { if (isDisabled) it else it.shuffle() }
|
||||
.let { if (shuffleIsDisabled) it else it.shuffle() }
|
||||
.initHands(players)
|
||||
.placeFirstCardOnDiscard(),
|
||||
)
|
||||
|
||||
12
src/main/kotlin/eventDemo/app/event/event/PlayerWinEvent.kt
Normal file
12
src/main/kotlin/eventDemo/app/event/event/PlayerWinEvent.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package eventDemo.app.event.event
|
||||
|
||||
import eventDemo.app.entity.GameId
|
||||
import eventDemo.app.entity.Player
|
||||
|
||||
/**
|
||||
* This [GameEvent] is sent when a player is ready.
|
||||
*/
|
||||
data class PlayerWinEvent(
|
||||
override val gameId: GameId,
|
||||
val player: Player,
|
||||
) : GameEvent
|
||||
165
src/main/kotlin/eventDemo/app/event/projection/GameState.kt
Normal file
165
src/main/kotlin/eventDemo/app/event/projection/GameState.kt
Normal file
@@ -0,0 +1,165 @@
|
||||
package eventDemo.app.event.projection
|
||||
|
||||
import eventDemo.app.entity.Card
|
||||
import eventDemo.app.entity.Deck
|
||||
import eventDemo.app.entity.GameId
|
||||
import eventDemo.app.entity.Player
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@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 direction: Direction = Direction.CLOCKWISE,
|
||||
val readyPlayers: Set<Player> = emptySet(),
|
||||
val deck: Deck = Deck(players),
|
||||
val isStarted: Boolean = false,
|
||||
val playerWins: Set<Player> = emptySet(),
|
||||
) {
|
||||
@Serializable
|
||||
data class LastCard(
|
||||
val card: Card,
|
||||
val player: Player,
|
||||
)
|
||||
|
||||
enum class Direction {
|
||||
CLOCKWISE,
|
||||
COUNTER_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 lastPlayerIndex: Int? get() {
|
||||
val i = players.indexOf(lastPlayer)
|
||||
return if (i == -1) {
|
||||
null
|
||||
} else {
|
||||
i
|
||||
}
|
||||
}
|
||||
|
||||
private val nextPlayerIndex: Int get() {
|
||||
if (players.size == 0) 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
|
||||
} else {
|
||||
players.elementAt(nextPlayerIndex)
|
||||
}
|
||||
}
|
||||
|
||||
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 (lastCard == null) error("No card")
|
||||
return this.playerDiffIndex(lastCard.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 = lastCard?.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 == lastColor
|
||||
}
|
||||
}
|
||||
|
||||
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 == lastColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package eventDemo.app.event
|
||||
package eventDemo.app.event.projection
|
||||
|
||||
import eventDemo.app.GameState
|
||||
import eventDemo.app.entity.Card
|
||||
import eventDemo.app.entity.GameId
|
||||
import eventDemo.app.event.GameEventStream
|
||||
import eventDemo.app.event.event.CardIsPlayedEvent
|
||||
import eventDemo.app.event.event.GameEvent
|
||||
import eventDemo.app.event.event.GameStartedEvent
|
||||
@@ -10,19 +10,33 @@ import eventDemo.app.event.event.NewPlayerEvent
|
||||
import eventDemo.app.event.event.PlayerChoseColorEvent
|
||||
import eventDemo.app.event.event.PlayerHavePassEvent
|
||||
import eventDemo.app.event.event.PlayerReadyEvent
|
||||
import eventDemo.app.event.event.PlayerWinEvent
|
||||
|
||||
fun GameId.buildStateFromEventStream(eventStream: GameEventStream): GameState =
|
||||
buildStateFromEvents(
|
||||
eventStream.readAll(this),
|
||||
)
|
||||
|
||||
/**
|
||||
* Build the state to the specific event
|
||||
*/
|
||||
fun GameEvent.buildStateFromEventStreamTo(eventStream: GameEventStream): GameState =
|
||||
gameId.buildStateFromEvents(
|
||||
eventStream.readAll(gameId).takeWhile { it != this } + this,
|
||||
)
|
||||
|
||||
private fun GameId.buildStateFromEvents(events: List<GameEvent>): GameState =
|
||||
events.fold(GameState(this)) { state: GameState, event: GameEvent ->
|
||||
events.fold(GameState(this)) { state, event ->
|
||||
state.apply(event)
|
||||
}
|
||||
|
||||
fun List<GameEvent>.buildStateFromEvents(): GameState =
|
||||
fold(GameState(this.first().gameId)) { state, event ->
|
||||
state.apply(event)
|
||||
}
|
||||
|
||||
fun GameState.apply(event: GameEvent): GameState =
|
||||
let { state ->
|
||||
when (event) {
|
||||
is CardIsPlayedEvent -> {
|
||||
val direction =
|
||||
@@ -83,5 +97,11 @@ private fun GameId.buildStateFromEvents(events: List<GameEvent>): GameState =
|
||||
isStarted = true,
|
||||
)
|
||||
}
|
||||
|
||||
is PlayerWinEvent -> {
|
||||
copy(
|
||||
playerWins = playerWins + event.player,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package eventDemo.app.event.projection
|
||||
|
||||
import eventDemo.app.entity.GameId
|
||||
import eventDemo.app.event.GameEventHandler
|
||||
import eventDemo.app.event.GameEventStream
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class GameStateRepository(
|
||||
private val eventStream: GameEventStream,
|
||||
eventHandler: GameEventHandler,
|
||||
) {
|
||||
private val projections: ConcurrentHashMap<GameId, GameState> = ConcurrentHashMap()
|
||||
|
||||
init {
|
||||
eventHandler.registerProjectionBuilder { event ->
|
||||
val projection = projections[event.gameId]
|
||||
if (projection == null) {
|
||||
event.gameId
|
||||
.buildStateFromEventStream(eventStream)
|
||||
.update()
|
||||
} else {
|
||||
projection
|
||||
.apply(event)
|
||||
.let { projections.put(it.gameId, it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun get(gameId: GameId): GameState = gameId.buildStateFromEventStream(eventStream)
|
||||
|
||||
private fun GameState.update() {
|
||||
projections[gameId] = this
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user