refactoring
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
package eventDemo.app.actions
|
||||
|
||||
import eventDemo.app.actions.playNewCard.PlayCardCommand
|
||||
import eventDemo.shared.command.GameCommandStream
|
||||
import eventDemo.shared.entity.Player
|
||||
import eventDemo.shared.event.CardIsPlayedEvent
|
||||
import eventDemo.shared.event.GameEvent
|
||||
import eventDemo.shared.event.GameEventStream
|
||||
import eventDemo.shared.event.GameState
|
||||
import eventDemo.shared.event.buildStateFromEventStream
|
||||
import eventDemo.app.GameState
|
||||
import eventDemo.app.command.GameCommand
|
||||
import eventDemo.app.command.GameCommandStream
|
||||
import eventDemo.app.command.PlayCardCommand
|
||||
import eventDemo.app.entity.Player
|
||||
import eventDemo.app.event.CardIsPlayedEvent
|
||||
import eventDemo.app.event.GameEvent
|
||||
import eventDemo.app.event.GameEventStream
|
||||
import eventDemo.app.event.buildStateFromEventStream
|
||||
import io.ktor.websocket.Frame
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -17,7 +18,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Listen [PlayCardCommand] on [GameCommandStream], check the validity and execute the action.
|
||||
* Listen [GameCommand] on [GameCommandStream], check the validity and execute an action.
|
||||
*
|
||||
* This action can be executing an action and produce a new [GameEvent] after verification.
|
||||
*/
|
||||
@@ -64,10 +65,10 @@ class GameCommandHandler(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun GameState.commandCardCanBeExecuted(command: PlayCardCommand): Boolean =
|
||||
canBePlayThisCard(
|
||||
command.payload.player,
|
||||
command.payload.card,
|
||||
)
|
||||
private fun GameState.commandCardCanBeExecuted(command: PlayCardCommand): Boolean =
|
||||
canBePlayThisCard(
|
||||
command.payload.player,
|
||||
command.payload.card,
|
||||
)
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
package eventDemo.app.actions
|
||||
package eventDemo.app
|
||||
|
||||
import eventDemo.app.event.GameEvent
|
||||
import eventDemo.libs.event.EventBus
|
||||
import eventDemo.shared.GameId
|
||||
import eventDemo.shared.event.GameEvent
|
||||
import eventDemo.shared.toFrame
|
||||
import io.ktor.websocket.Frame
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class GameEventPlayerNotificationSubscriber(
|
||||
class GameEventPlayerNotificationListener(
|
||||
private val eventBus: EventBus<GameEvent, GameId>,
|
||||
private val outgoing: SendChannel<Frame>,
|
||||
) {
|
||||
@@ -1,13 +1,12 @@
|
||||
package eventDemo.app.actions
|
||||
package eventDemo.app
|
||||
|
||||
import eventDemo.app.event.GameEvent
|
||||
import eventDemo.app.event.GameStartedEvent
|
||||
import eventDemo.app.event.buildStateFromEventStream
|
||||
import eventDemo.libs.event.EventBus
|
||||
import eventDemo.libs.event.EventStream
|
||||
import eventDemo.shared.GameId
|
||||
import eventDemo.shared.event.GameEvent
|
||||
import eventDemo.shared.event.GameStartedEvent
|
||||
import eventDemo.shared.event.buildStateFromEventStream
|
||||
|
||||
class GameEventReactionSubscriber(
|
||||
class GameEventReactionListener(
|
||||
private val eventBus: EventBus<GameEvent, GameId>,
|
||||
private val eventStream: EventStream<GameEvent, GameId>,
|
||||
) {
|
||||
15
src/main/kotlin/eventDemo/app/GameId.kt
Normal file
15
src/main/kotlin/eventDemo/app/GameId.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
package eventDemo.app
|
||||
|
||||
import eventDemo.configuration.GameIdSerializer
|
||||
import eventDemo.libs.event.AggregateId
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* An [AggregateId] for a game.
|
||||
*/
|
||||
@JvmInline
|
||||
@Serializable(with = GameIdSerializer::class)
|
||||
value class GameId(
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
) : AggregateId
|
||||
145
src/main/kotlin/eventDemo/app/GameState.kt
Normal file
145
src/main/kotlin/eventDemo/app/GameState.kt
Normal file
@@ -0,0 +1,145 @@
|
||||
package eventDemo.app
|
||||
|
||||
import eventDemo.app.entity.Card
|
||||
import eventDemo.app.entity.Deck
|
||||
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: List<Player> = emptyList(),
|
||||
val deck: Deck = Deck(players.toList()),
|
||||
val isStarted: Boolean = false,
|
||||
) {
|
||||
@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) }
|
||||
}
|
||||
|
||||
fun canBePlayThisCard(
|
||||
player: Player,
|
||||
card: Card,
|
||||
): Boolean {
|
||||
if (!isReady) return false
|
||||
val cardOnGame = lastCard?.card ?: return false
|
||||
|
||||
return when (cardOnGame) {
|
||||
is Card.NumericCard -> {
|
||||
when (card) {
|
||||
is Card.AllColorCard -> true
|
||||
is Card.NumericCard -> card.number == cardOnGame.number || card.color == cardOnGame.color
|
||||
is Card.ColorCard -> card.color == cardOnGame.color
|
||||
}
|
||||
}
|
||||
|
||||
is Card.ReverseCard -> {
|
||||
when (card) {
|
||||
is Card.ReverseCard -> true
|
||||
is Card.AllColorCard -> true
|
||||
is Card.ColorCard -> card.color == cardOnGame.color
|
||||
}
|
||||
}
|
||||
|
||||
is Card.PassCard -> {
|
||||
if (player.cardOnBoardIsForYou) {
|
||||
false
|
||||
} else {
|
||||
when (card) {
|
||||
is Card.AllColorCard -> true
|
||||
is Card.ColorCard -> card.color == cardOnGame.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 == cardOnGame.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val lastPlayerIndex: Int? get() {
|
||||
val i = players.indexOf(lastPlayer)
|
||||
return if (i == -1) {
|
||||
null
|
||||
} else {
|
||||
i
|
||||
}
|
||||
}
|
||||
|
||||
private val nextPlayerIndex: Int get() {
|
||||
val y =
|
||||
if (direction == Direction.CLOCKWISE) {
|
||||
+1
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
|
||||
return ((lastPlayerIndex ?: 0) + y) % players.size
|
||||
}
|
||||
|
||||
val nextPlayer: Player = players.elementAt(nextPlayerIndex)
|
||||
|
||||
private val Player.currentIndex: Int get() = players.indexOf(this)
|
||||
|
||||
private 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
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package eventDemo.app.actions.readLastPlayedCard
|
||||
package eventDemo.app.actions
|
||||
|
||||
import eventDemo.app.GameId
|
||||
import eventDemo.app.event.CardIsPlayedEvent
|
||||
import eventDemo.app.event.GameEventStream
|
||||
import eventDemo.app.event.buildStateFromEventStream
|
||||
import eventDemo.configuration.GameIdSerializer
|
||||
import eventDemo.libs.event.readLastOf
|
||||
import eventDemo.plugins.GameIdSerializer
|
||||
import eventDemo.shared.GameId
|
||||
import eventDemo.shared.event.CardIsPlayedEvent
|
||||
import eventDemo.shared.event.GameEventStream
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.resources.Resource
|
||||
import io.ktor.server.application.call
|
||||
@@ -25,6 +26,12 @@ class Game(
|
||||
class Card(
|
||||
val game: Game,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@Resource("state")
|
||||
class State(
|
||||
val game: Game,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,10 +43,25 @@ fun Routing.readLastPlayedCard() {
|
||||
/*
|
||||
* Read the last played card on the game.
|
||||
*/
|
||||
get<Game.Card> { card ->
|
||||
get<Game.Card> { body ->
|
||||
eventStream
|
||||
.readLastOf<CardIsPlayedEvent, _, _>(card.game.id)
|
||||
.readLastOf<CardIsPlayedEvent, _, _>(body.game.id)
|
||||
?.let { call.respond(it.card) }
|
||||
?: call.response.status(HttpStatusCode.BadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API route to read the last card played.
|
||||
*/
|
||||
fun Routing.readGameState() {
|
||||
val eventStream by inject<GameEventStream>()
|
||||
|
||||
/*
|
||||
* Read the last played card on the game.
|
||||
*/
|
||||
get<Game.State> { body ->
|
||||
val state = body.game.id.buildStateFromEventStream(eventStream)
|
||||
call.respond(state)
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package eventDemo.app.actions.playNewCard
|
||||
|
||||
import eventDemo.libs.command.send
|
||||
import eventDemo.shared.GameId
|
||||
import eventDemo.shared.command.GameCommandStreamInMemory
|
||||
import eventDemo.shared.entity.Card
|
||||
import eventDemo.shared.entity.Player
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.resources.Resource
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.auth.authenticate
|
||||
import io.ktor.server.auth.principal
|
||||
import io.ktor.server.request.receive
|
||||
import io.ktor.server.resources.post
|
||||
import io.ktor.server.response.respondNullable
|
||||
import io.ktor.server.routing.Routing
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Resource("/game/{id}")
|
||||
class GameRoute(
|
||||
// @Serializable(with = GameIdSerializer::class)
|
||||
val id: GameId,
|
||||
) {
|
||||
@Serializable
|
||||
@Resource("card")
|
||||
class Card(
|
||||
val game: GameRoute,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* API route to send a request to play card.
|
||||
*/
|
||||
fun Routing.playNewCard() {
|
||||
val commandStream = GameCommandStreamInMemory()
|
||||
authenticate {
|
||||
/*
|
||||
* A player request to play a new card.
|
||||
*
|
||||
* It always returns [HttpStatusCode.OK], but it is not mean that card is already played!
|
||||
*/
|
||||
post<GameRoute.Card> {
|
||||
val card = call.receive<Card>()
|
||||
val name = call.principal<Player>()!!
|
||||
launch(Dispatchers.Default) {
|
||||
commandStream.send(
|
||||
PlayCardCommand(
|
||||
it.game.id,
|
||||
name,
|
||||
card,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
call.respondNullable<Any?>(HttpStatusCode.OK, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/main/kotlin/eventDemo/app/command/GameCommandStream.kt
Normal file
27
src/main/kotlin/eventDemo/app/command/GameCommandStream.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package eventDemo.app.command
|
||||
|
||||
import eventDemo.libs.command.CommandStream
|
||||
import eventDemo.libs.command.CommandStreamChannel
|
||||
import eventDemo.libs.command.CommandStreamInMemory
|
||||
import io.ktor.websocket.Frame
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* A stream to publish and read the game command.
|
||||
*/
|
||||
class GameCommandStreamInMemory : CommandStreamInMemory<GameCommand>()
|
||||
|
||||
/**
|
||||
* A stream to publish and read the game command.
|
||||
*/
|
||||
class GameCommandStream(
|
||||
incoming: ReceiveChannel<Frame>,
|
||||
outgoing: SendChannel<Frame>,
|
||||
) : CommandStream<GameCommand> by CommandStreamChannel(
|
||||
incoming,
|
||||
outgoing,
|
||||
{ Json.encodeToString(GameCommand.serializer(), it) },
|
||||
{ Json.decodeFromString(GameCommand.serializer(), it) },
|
||||
)
|
||||
@@ -1,10 +1,10 @@
|
||||
package eventDemo.app.actions.playNewCard
|
||||
package eventDemo.app.command
|
||||
|
||||
import eventDemo.app.GameId
|
||||
import eventDemo.app.entity.Card
|
||||
import eventDemo.app.entity.Player
|
||||
import eventDemo.libs.command.Command
|
||||
import eventDemo.libs.command.CommandId
|
||||
import eventDemo.shared.GameId
|
||||
import eventDemo.shared.entity.Card
|
||||
import eventDemo.shared.entity.Player
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
109
src/main/kotlin/eventDemo/app/entity/Card.kt
Normal file
109
src/main/kotlin/eventDemo/app/entity/Card.kt
Normal file
@@ -0,0 +1,109 @@
|
||||
package eventDemo.app.entity
|
||||
|
||||
import eventDemo.configuration.UUIDSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* A Play card
|
||||
*/
|
||||
@Serializable
|
||||
sealed interface Card {
|
||||
val id: UUID
|
||||
|
||||
/**
|
||||
* The color of a card
|
||||
*/
|
||||
@Serializable
|
||||
enum class Color {
|
||||
Blue,
|
||||
Red,
|
||||
Yellow,
|
||||
Green,
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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 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
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
53
src/main/kotlin/eventDemo/app/entity/Deck.kt
Normal file
53
src/main/kotlin/eventDemo/app/entity/Deck.kt
Normal file
@@ -0,0 +1,53 @@
|
||||
package eventDemo.app.entity
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Deck(
|
||||
val stack: Set<Card> = emptySet(),
|
||||
val discard: Set<Card> = emptySet(),
|
||||
val playersHands: List<PlayerHand> = emptyList(),
|
||||
) {
|
||||
constructor(players: List<Player>) : this(playersHands = players.map { PlayerHand(it) })
|
||||
|
||||
fun putOneCardOnDiscard(): Deck {
|
||||
val takenCard = stack.first()
|
||||
val newStack = stack.filterNot { it != takenCard }.toSet()
|
||||
return copy(stack = newStack)
|
||||
}
|
||||
|
||||
fun take(n: Int): Pair<Deck, List<Card>> {
|
||||
val takenCards = stack.take(n)
|
||||
val newStack = stack.filterNot { takenCards.contains(it) }.toSet()
|
||||
return Pair(copy(stack = newStack), takenCards)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun initHands(
|
||||
players: Set<Player>,
|
||||
handSize: Int = 7,
|
||||
): Deck {
|
||||
val deck = new()
|
||||
val playersHands = players.map { PlayerHand(it, deck.stack.take(handSize)) }
|
||||
val allTakenCards = playersHands.flatMap { it.cards }
|
||||
val newStack = deck.stack.filterNot { allTakenCards.contains(it) }.toSet()
|
||||
return deck.copy(
|
||||
stack = newStack,
|
||||
playersHands = playersHands,
|
||||
)
|
||||
}
|
||||
|
||||
private fun new(): 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 {
|
||||
(1..4).map { Card.Plus4Card() }
|
||||
}.shuffled()
|
||||
.toSet()
|
||||
.let { Deck(it) }
|
||||
}
|
||||
}
|
||||
36
src/main/kotlin/eventDemo/app/entity/Player.kt
Normal file
36
src/main/kotlin/eventDemo/app/entity/Player.kt
Normal file
@@ -0,0 +1,36 @@
|
||||
package eventDemo.app.entity
|
||||
|
||||
import eventDemo.configuration.PlayerIdSerializer
|
||||
import eventDemo.configuration.UUIDSerializer
|
||||
import eventDemo.libs.event.AggregateId
|
||||
import io.ktor.server.auth.Principal
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class Player(
|
||||
val name: String,
|
||||
@Serializable(with = PlayerIdSerializer::class)
|
||||
val id: PlayerId = PlayerId(UUID.randomUUID()),
|
||||
) : Principal {
|
||||
constructor(id: String, name: String) : this(
|
||||
name,
|
||||
PlayerId(UUID.fromString(id)),
|
||||
)
|
||||
|
||||
@JvmInline
|
||||
value class PlayerId(
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
) : AggregateId {
|
||||
override fun toString(): String = id.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PlayerHand(
|
||||
val player: Player,
|
||||
val cards: List<Card> = emptyList(),
|
||||
) {
|
||||
val count = lazy { cards.count() }
|
||||
}
|
||||
79
src/main/kotlin/eventDemo/app/event/CardIsPlayedEvent.kt
Normal file
79
src/main/kotlin/eventDemo/app/event/CardIsPlayedEvent.kt
Normal file
@@ -0,0 +1,79 @@
|
||||
package eventDemo.app.event
|
||||
|
||||
import eventDemo.app.GameId
|
||||
import eventDemo.app.entity.Card
|
||||
import eventDemo.app.entity.Deck
|
||||
import eventDemo.app.entity.Player
|
||||
import eventDemo.libs.event.Event
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* An [Event] of a Game.
|
||||
*/
|
||||
@Serializable
|
||||
sealed interface GameEvent : Event<GameId> {
|
||||
override val id: GameId
|
||||
}
|
||||
|
||||
/**
|
||||
* An [Event] to represent a played card.
|
||||
*/
|
||||
data class CardIsPlayedEvent(
|
||||
override val id: GameId,
|
||||
val card: Card,
|
||||
val player: Player,
|
||||
) : GameEvent
|
||||
|
||||
/**
|
||||
* An [Event] to represent a new player joining the game.
|
||||
*/
|
||||
data class NewPlayerEvent(
|
||||
override val id: GameId,
|
||||
val player: Player,
|
||||
) : GameEvent
|
||||
|
||||
/**
|
||||
* This [Event] is sent when a player is ready.
|
||||
*/
|
||||
data class PlayerReadyEvent(
|
||||
override val id: GameId,
|
||||
val player: Player,
|
||||
) : GameEvent
|
||||
|
||||
/**
|
||||
* This [Event] is sent when a player is ready.
|
||||
*/
|
||||
data class GameStartedEvent(
|
||||
override val id: GameId,
|
||||
val firstPlayer: Player,
|
||||
val deck: Deck,
|
||||
) : GameEvent {
|
||||
companion object {
|
||||
fun new(
|
||||
id: GameId,
|
||||
players: Set<Player>,
|
||||
): GameStartedEvent =
|
||||
GameStartedEvent(
|
||||
id = id,
|
||||
firstPlayer = players.random(),
|
||||
deck = Deck.initHands(players).putOneCardOnDiscard(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This [Event] is sent when a player can play.
|
||||
*/
|
||||
data class PlayerHavePassEvent(
|
||||
override val id: GameId,
|
||||
val player: Player,
|
||||
) : GameEvent
|
||||
|
||||
/**
|
||||
* This [Event] is sent when a player chose a color.
|
||||
*/
|
||||
data class PlayerChoseColorEvent(
|
||||
override val id: GameId,
|
||||
val player: Player,
|
||||
val color: Card.Color,
|
||||
) : GameEvent
|
||||
8
src/main/kotlin/eventDemo/app/event/GameEventBus.kt
Normal file
8
src/main/kotlin/eventDemo/app/event/GameEventBus.kt
Normal file
@@ -0,0 +1,8 @@
|
||||
package eventDemo.app.event
|
||||
|
||||
import eventDemo.app.GameId
|
||||
import eventDemo.libs.event.EventBus
|
||||
|
||||
class GameEventBus(
|
||||
bus: EventBus<GameEvent, GameId>,
|
||||
) : EventBus<GameEvent, GameId> by bus
|
||||
18
src/main/kotlin/eventDemo/app/event/GameEventStream.kt
Normal file
18
src/main/kotlin/eventDemo/app/event/GameEventStream.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
package eventDemo.app.event
|
||||
|
||||
import eventDemo.app.GameId
|
||||
import eventDemo.libs.event.EventBus
|
||||
import eventDemo.libs.event.EventStream
|
||||
|
||||
/**
|
||||
* A stream to publish and read the played card event.
|
||||
*/
|
||||
class GameEventStream(
|
||||
private val eventBus: EventBus<GameEvent, GameId>,
|
||||
private val m: EventStream<GameEvent, GameId>,
|
||||
) : EventStream<GameEvent, GameId> by m {
|
||||
override fun publish(event: GameEvent) {
|
||||
m.publish(event)
|
||||
eventBus.publish(event)
|
||||
}
|
||||
}
|
||||
72
src/main/kotlin/eventDemo/app/event/GameStateBuilder.kt
Normal file
72
src/main/kotlin/eventDemo/app/event/GameStateBuilder.kt
Normal file
@@ -0,0 +1,72 @@
|
||||
package eventDemo.app.event
|
||||
|
||||
import eventDemo.app.GameId
|
||||
import eventDemo.app.GameState
|
||||
import eventDemo.app.entity.Card
|
||||
import eventDemo.libs.event.EventStream
|
||||
|
||||
fun GameId.buildStateFromEventStream(eventStream: EventStream<GameEvent, GameId>): GameState =
|
||||
buildStateFromEvents(
|
||||
eventStream.readAll(this),
|
||||
)
|
||||
|
||||
private fun GameId.buildStateFromEvents(events: List<GameEvent>): GameState =
|
||||
events.fold(GameState(this)) { state: GameState, event: GameEvent ->
|
||||
when (event) {
|
||||
is CardIsPlayedEvent -> {
|
||||
val direction =
|
||||
when (event.card) {
|
||||
is Card.ReverseCard -> state.direction.revert()
|
||||
else -> state.direction
|
||||
}
|
||||
|
||||
val color =
|
||||
when (event.card) {
|
||||
is Card.ColorCard -> event.card.color
|
||||
else -> state.lastColor
|
||||
}
|
||||
|
||||
state.copy(
|
||||
lastPlayer = event.player,
|
||||
direction = direction,
|
||||
lastColor = color,
|
||||
)
|
||||
}
|
||||
|
||||
is NewPlayerEvent -> {
|
||||
if (state.isReady) error("The game is already started")
|
||||
|
||||
state.copy(
|
||||
players = state.players + event.player,
|
||||
)
|
||||
}
|
||||
|
||||
is PlayerReadyEvent -> {
|
||||
state.copy(
|
||||
readyPlayers = state.readyPlayers + event.player,
|
||||
)
|
||||
}
|
||||
|
||||
is PlayerHavePassEvent -> {
|
||||
state.copy(
|
||||
lastPlayer = event.player,
|
||||
)
|
||||
}
|
||||
|
||||
is PlayerChoseColorEvent -> {
|
||||
state.copy(
|
||||
lastColor = event.color,
|
||||
)
|
||||
}
|
||||
|
||||
is GameStartedEvent -> {
|
||||
state.copy(
|
||||
lastColor = (event.deck.discard.first() as? Card.ColorCard)?.color,
|
||||
lastCard = eventDemo.app.GameState.LastCard(event.deck.discard.first(), event.firstPlayer),
|
||||
lastPlayer = event.firstPlayer,
|
||||
deck = event.deck,
|
||||
isStarted = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user