add IamReadyToPlayCommand & refactoring
This commit is contained in:
@@ -2,6 +2,7 @@ package eventDemo.app
|
|||||||
|
|
||||||
import eventDemo.app.entity.Card
|
import eventDemo.app.entity.Card
|
||||||
import eventDemo.app.entity.Deck
|
import eventDemo.app.entity.Deck
|
||||||
|
import eventDemo.app.entity.GameId
|
||||||
import eventDemo.app.entity.Player
|
import eventDemo.app.entity.Player
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
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 io.ktor.http.HttpStatusCode
|
|
||||||
import io.ktor.resources.Resource
|
|
||||||
import io.ktor.server.application.call
|
|
||||||
import io.ktor.server.resources.get
|
|
||||||
import io.ktor.server.response.respond
|
|
||||||
import io.ktor.server.routing.Routing
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import org.koin.ktor.ext.inject
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@Resource("/game/{id}")
|
|
||||||
class Game(
|
|
||||||
@Serializable(with = GameIdSerializer::class)
|
|
||||||
val id: GameId,
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
@Resource("card/last")
|
|
||||||
class Card(
|
|
||||||
val game: Game,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@Resource("state")
|
|
||||||
class State(
|
|
||||||
val game: Game,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API route to read the last card played.
|
|
||||||
*/
|
|
||||||
fun Routing.readLastPlayedCard() {
|
|
||||||
val eventStream by inject<GameEventStream>()
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Read the last played card on the game.
|
|
||||||
*/
|
|
||||||
get<Game.Card> { body ->
|
|
||||||
eventStream
|
|
||||||
.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,14 +1,15 @@
|
|||||||
package eventDemo.app.actions
|
package eventDemo.app.command
|
||||||
|
|
||||||
import eventDemo.app.GameState
|
import eventDemo.app.GameState
|
||||||
import eventDemo.app.command.GameCommand
|
import eventDemo.app.command.command.GameCommand
|
||||||
import eventDemo.app.command.GameCommandStream
|
import eventDemo.app.command.command.IamReadyToPlayCommand
|
||||||
import eventDemo.app.command.PlayCardCommand
|
import eventDemo.app.command.command.IwantToPlayCardCommand
|
||||||
import eventDemo.app.entity.Player
|
import eventDemo.app.entity.Player
|
||||||
import eventDemo.app.event.CardIsPlayedEvent
|
|
||||||
import eventDemo.app.event.GameEvent
|
|
||||||
import eventDemo.app.event.GameEventStream
|
import eventDemo.app.event.GameEventStream
|
||||||
import eventDemo.app.event.buildStateFromEventStream
|
import eventDemo.app.event.buildStateFromEventStream
|
||||||
|
import eventDemo.app.event.event.CardIsPlayedEvent
|
||||||
|
import eventDemo.app.event.event.GameEvent
|
||||||
|
import eventDemo.app.event.event.PlayerReadyEvent
|
||||||
import io.ktor.websocket.Frame
|
import io.ktor.websocket.Frame
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -35,24 +36,22 @@ class GameCommandHandler(
|
|||||||
*/
|
*/
|
||||||
fun init(player: Player) {
|
fun init(player: Player) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
commandStream.process {
|
commandStream.process { command ->
|
||||||
if (it.payload.player.id != player.id) {
|
if (command.payload.player.id != player.id) {
|
||||||
nack()
|
nack()
|
||||||
}
|
}
|
||||||
when (it) {
|
|
||||||
is PlayCardCommand -> {
|
|
||||||
// Check the command can be executed
|
|
||||||
val canBeExecuted =
|
|
||||||
it.payload.gameId
|
|
||||||
.buildStateFromEventStream(eventStream)
|
|
||||||
.commandCardCanBeExecuted(it)
|
|
||||||
|
|
||||||
if (canBeExecuted) {
|
val state = command.buildState()
|
||||||
|
|
||||||
|
when (command) {
|
||||||
|
is IwantToPlayCardCommand -> {
|
||||||
|
// Check the command can be executed
|
||||||
|
if (state.commandCardCanBeExecuted(command)) {
|
||||||
eventStream.publish(
|
eventStream.publish(
|
||||||
CardIsPlayedEvent(
|
CardIsPlayedEvent(
|
||||||
it.payload.gameId,
|
command.payload.gameId,
|
||||||
it.payload.card,
|
command.payload.card,
|
||||||
it.payload.player,
|
command.payload.player,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -61,14 +60,29 @@ class GameCommandHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is IamReadyToPlayCommand -> {
|
||||||
|
if (state.playerIsAlreadyReady(command.payload.player)) {
|
||||||
|
nack()
|
||||||
|
} else {
|
||||||
|
PlayerReadyEvent(
|
||||||
|
command.payload.gameId,
|
||||||
|
command.payload.player,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun GameState.commandCardCanBeExecuted(command: PlayCardCommand): Boolean =
|
private fun GameState.playerIsAlreadyReady(player: Player): Boolean = readyPlayers.contains(player)
|
||||||
|
|
||||||
|
private fun GameState.commandCardCanBeExecuted(command: IwantToPlayCardCommand): Boolean =
|
||||||
canBePlayThisCard(
|
canBePlayThisCard(
|
||||||
command.payload.player,
|
command.payload.player,
|
||||||
command.payload.card,
|
command.payload.card,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun GameCommand.buildState(): GameState = payload.gameId.buildStateFromEventStream(eventStream)
|
||||||
}
|
}
|
||||||
32
src/main/kotlin/eventDemo/app/command/GameCommandRoute.kt
Normal file
32
src/main/kotlin/eventDemo/app/command/GameCommandRoute.kt
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package eventDemo.app.command
|
||||||
|
|
||||||
|
import eventDemo.app.entity.Player
|
||||||
|
import eventDemo.app.event.GameEventBus
|
||||||
|
import eventDemo.app.event.GameEventStream
|
||||||
|
import eventDemo.app.eventListener.GameEventPlayerNotificationListener
|
||||||
|
import io.ktor.server.application.ApplicationCall
|
||||||
|
import io.ktor.server.auth.authenticate
|
||||||
|
import io.ktor.server.auth.jwt.JWTPrincipal
|
||||||
|
import io.ktor.server.auth.principal
|
||||||
|
import io.ktor.server.routing.Route
|
||||||
|
import io.ktor.server.websocket.webSocket
|
||||||
|
|
||||||
|
fun Route.gameSocket(
|
||||||
|
eventStream: GameEventStream,
|
||||||
|
eventBus: GameEventBus,
|
||||||
|
) {
|
||||||
|
authenticate {
|
||||||
|
webSocket("/game") {
|
||||||
|
GameCommandHandler(eventStream, incoming, outgoing).init(call.getPlayer())
|
||||||
|
GameEventPlayerNotificationListener(eventBus, outgoing).init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ApplicationCall.getPlayer() =
|
||||||
|
principal<JWTPrincipal>()!!.run {
|
||||||
|
Player(
|
||||||
|
id = payload.getClaim("playerid").asString(),
|
||||||
|
name = payload.getClaim("username").asString(),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package eventDemo.app.command
|
package eventDemo.app.command
|
||||||
|
|
||||||
|
import eventDemo.app.command.command.GameCommand
|
||||||
import eventDemo.libs.command.CommandStream
|
import eventDemo.libs.command.CommandStream
|
||||||
import eventDemo.libs.command.CommandStreamChannel
|
import eventDemo.libs.command.CommandStreamChannel
|
||||||
import eventDemo.libs.command.CommandStreamInMemory
|
import eventDemo.libs.command.CommandStreamInMemory
|
||||||
|
|||||||
17
src/main/kotlin/eventDemo/app/command/command/GameCommand.kt
Normal file
17
src/main/kotlin/eventDemo/app/command/command/GameCommand.kt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package eventDemo.app.command.command
|
||||||
|
|
||||||
|
import eventDemo.app.entity.GameId
|
||||||
|
import eventDemo.app.entity.Player
|
||||||
|
import eventDemo.libs.command.Command
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed interface GameCommand : Command {
|
||||||
|
val payload: Payload
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed interface Payload {
|
||||||
|
val gameId: GameId
|
||||||
|
val player: Player
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package eventDemo.app.command.command
|
||||||
|
|
||||||
|
import eventDemo.app.entity.GameId
|
||||||
|
import eventDemo.app.entity.Player
|
||||||
|
import eventDemo.libs.command.CommandId
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A command to set as ready to play
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
@SerialName("Ready")
|
||||||
|
data class IamReadyToPlayCommand(
|
||||||
|
override val payload: Payload,
|
||||||
|
) : GameCommand {
|
||||||
|
override val name: String = "Ready"
|
||||||
|
override val id: CommandId = CommandId()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Payload(
|
||||||
|
override val gameId: GameId,
|
||||||
|
override val player: Player,
|
||||||
|
) : GameCommand.Payload
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
package eventDemo.app.command
|
package eventDemo.app.command.command
|
||||||
|
|
||||||
import eventDemo.app.GameId
|
|
||||||
import eventDemo.app.entity.Card
|
import eventDemo.app.entity.Card
|
||||||
|
import eventDemo.app.entity.GameId
|
||||||
import eventDemo.app.entity.Player
|
import eventDemo.app.entity.Player
|
||||||
import eventDemo.libs.command.Command
|
|
||||||
import eventDemo.libs.command.CommandId
|
import eventDemo.libs.command.CommandId
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@@ -13,15 +12,9 @@ import kotlinx.serialization.Serializable
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("PlayCard")
|
@SerialName("PlayCard")
|
||||||
data class PlayCardCommand(
|
data class IwantToPlayCardCommand(
|
||||||
override val payload: Payload,
|
override val payload: Payload,
|
||||||
) : GameCommand {
|
) : GameCommand {
|
||||||
constructor(
|
|
||||||
gameId: GameId,
|
|
||||||
player: Player,
|
|
||||||
card: Card,
|
|
||||||
) : this(Payload(gameId, player, card))
|
|
||||||
|
|
||||||
override val name: String = "PlayCard"
|
override val name: String = "PlayCard"
|
||||||
override val id: CommandId = CommandId()
|
override val id: CommandId = CommandId()
|
||||||
|
|
||||||
@@ -32,14 +25,3 @@ data class PlayCardCommand(
|
|||||||
val card: Card,
|
val card: Card,
|
||||||
) : GameCommand.Payload
|
) : GameCommand.Payload
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
|
||||||
sealed interface GameCommand : Command {
|
|
||||||
val payload: Payload
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
sealed interface Payload {
|
|
||||||
val gameId: GameId
|
|
||||||
val player: Player
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package eventDemo.app.entity
|
package eventDemo.app.entity
|
||||||
|
|
||||||
import eventDemo.configuration.UUIDSerializer
|
import eventDemo.shared.UUIDSerializer
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.app
|
package eventDemo.app.entity
|
||||||
|
|
||||||
import eventDemo.configuration.GameIdSerializer
|
|
||||||
import eventDemo.libs.event.AggregateId
|
import eventDemo.libs.event.AggregateId
|
||||||
|
import eventDemo.shared.GameIdSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package eventDemo.app.entity
|
package eventDemo.app.entity
|
||||||
|
|
||||||
import eventDemo.configuration.PlayerIdSerializer
|
|
||||||
import eventDemo.configuration.UUIDSerializer
|
|
||||||
import eventDemo.libs.event.AggregateId
|
import eventDemo.libs.event.AggregateId
|
||||||
|
import eventDemo.shared.PlayerIdSerializer
|
||||||
|
import eventDemo.shared.UUIDSerializer
|
||||||
import io.ktor.server.auth.Principal
|
import io.ktor.server.auth.Principal
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -26,11 +26,3 @@ data class Player(
|
|||||||
override fun toString(): String = id.toString()
|
override fun toString(): String = id.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class PlayerHand(
|
|
||||||
val player: Player,
|
|
||||||
val cards: List<Card> = emptyList(),
|
|
||||||
) {
|
|
||||||
val count = lazy { cards.count() }
|
|
||||||
}
|
|
||||||
|
|||||||
11
src/main/kotlin/eventDemo/app/entity/PlayerHand.kt
Normal file
11
src/main/kotlin/eventDemo/app/entity/PlayerHand.kt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package eventDemo.app.entity
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PlayerHand(
|
||||||
|
val player: Player,
|
||||||
|
val cards: List<Card> = emptyList(),
|
||||||
|
) {
|
||||||
|
val count = lazy { cards.count() }
|
||||||
|
}
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package eventDemo.app.event
|
package eventDemo.app.event
|
||||||
|
|
||||||
import eventDemo.app.GameId
|
import eventDemo.app.entity.GameId
|
||||||
|
import eventDemo.app.event.event.GameEvent
|
||||||
import eventDemo.libs.event.EventBus
|
import eventDemo.libs.event.EventBus
|
||||||
|
|
||||||
class GameEventBus(
|
class GameEventBus(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package eventDemo.app.event
|
package eventDemo.app.event
|
||||||
|
|
||||||
import eventDemo.app.GameId
|
import eventDemo.app.entity.GameId
|
||||||
|
import eventDemo.app.event.event.GameEvent
|
||||||
import eventDemo.libs.event.EventBus
|
import eventDemo.libs.event.EventBus
|
||||||
import eventDemo.libs.event.EventStream
|
import eventDemo.libs.event.EventStream
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
package eventDemo.app.event
|
package eventDemo.app.event
|
||||||
|
|
||||||
import eventDemo.app.GameId
|
|
||||||
import eventDemo.app.GameState
|
import eventDemo.app.GameState
|
||||||
import eventDemo.app.entity.Card
|
import eventDemo.app.entity.Card
|
||||||
|
import eventDemo.app.entity.GameId
|
||||||
|
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.PlayerChoseColorEvent
|
||||||
|
import eventDemo.app.event.event.PlayerHavePassEvent
|
||||||
|
import eventDemo.app.event.event.PlayerReadyEvent
|
||||||
import eventDemo.libs.event.EventStream
|
import eventDemo.libs.event.EventStream
|
||||||
|
|
||||||
fun GameId.buildStateFromEventStream(eventStream: EventStream<GameEvent, GameId>): GameState =
|
fun GameId.buildStateFromEventStream(eventStream: EventStream<GameEvent, GameId>): GameState =
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package eventDemo.app.event.event
|
||||||
|
|
||||||
|
import eventDemo.app.entity.Card
|
||||||
|
import eventDemo.app.entity.GameId
|
||||||
|
import eventDemo.app.entity.Player
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An [GameEvent] to represent a played card.
|
||||||
|
*/
|
||||||
|
data class CardIsPlayedEvent(
|
||||||
|
override val id: GameId,
|
||||||
|
val card: Card,
|
||||||
|
val player: Player,
|
||||||
|
) : GameEvent
|
||||||
13
src/main/kotlin/eventDemo/app/event/event/GameEvent.kt
Normal file
13
src/main/kotlin/eventDemo/app/event/event/GameEvent.kt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package eventDemo.app.event.event
|
||||||
|
|
||||||
|
import eventDemo.app.entity.GameId
|
||||||
|
import eventDemo.libs.event.Event
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An [Event] of a Game.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
sealed interface GameEvent : Event<GameId> {
|
||||||
|
override val id: GameId
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package eventDemo.app.event.event
|
||||||
|
|
||||||
|
import eventDemo.app.entity.Deck
|
||||||
|
import eventDemo.app.entity.GameId
|
||||||
|
import eventDemo.app.entity.Player
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This [GameEvent] is sent when all players 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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/main/kotlin/eventDemo/app/event/event/NewPlayerEvent.kt
Normal file
12
src/main/kotlin/eventDemo/app/event/event/NewPlayerEvent.kt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package eventDemo.app.event.event
|
||||||
|
|
||||||
|
import eventDemo.app.entity.GameId
|
||||||
|
import eventDemo.app.entity.Player
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An [GameEvent] to represent a new player joining the game.
|
||||||
|
*/
|
||||||
|
data class NewPlayerEvent(
|
||||||
|
override val id: GameId,
|
||||||
|
val player: Player,
|
||||||
|
) : GameEvent
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package eventDemo.app.event.event
|
||||||
|
|
||||||
|
import eventDemo.app.entity.Card
|
||||||
|
import eventDemo.app.entity.GameId
|
||||||
|
import eventDemo.app.entity.Player
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This [GameEvent] is sent when a player chose a color.
|
||||||
|
*/
|
||||||
|
data class PlayerChoseColorEvent(
|
||||||
|
override val id: GameId,
|
||||||
|
val player: Player,
|
||||||
|
val color: Card.Color,
|
||||||
|
) : GameEvent
|
||||||
@@ -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 can play.
|
||||||
|
*/
|
||||||
|
data class PlayerHavePassEvent(
|
||||||
|
override val id: GameId,
|
||||||
|
val player: Player,
|
||||||
|
) : GameEvent
|
||||||
@@ -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 PlayerReadyEvent(
|
||||||
|
override val id: GameId,
|
||||||
|
val player: Player,
|
||||||
|
) : GameEvent
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package eventDemo.app
|
package eventDemo.app.eventListener
|
||||||
|
|
||||||
import eventDemo.app.event.GameEvent
|
import eventDemo.app.entity.GameId
|
||||||
|
import eventDemo.app.event.event.GameEvent
|
||||||
import eventDemo.libs.event.EventBus
|
import eventDemo.libs.event.EventBus
|
||||||
import eventDemo.shared.toFrame
|
import eventDemo.shared.toFrame
|
||||||
import io.ktor.websocket.Frame
|
import io.ktor.websocket.Frame
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package eventDemo.app
|
package eventDemo.app.eventListener
|
||||||
|
|
||||||
import eventDemo.app.event.GameEvent
|
import eventDemo.app.entity.GameId
|
||||||
import eventDemo.app.event.GameStartedEvent
|
|
||||||
import eventDemo.app.event.buildStateFromEventStream
|
import eventDemo.app.event.buildStateFromEventStream
|
||||||
|
import eventDemo.app.event.event.GameEvent
|
||||||
|
import eventDemo.app.event.event.GameStartedEvent
|
||||||
import eventDemo.libs.event.EventBus
|
import eventDemo.libs.event.EventBus
|
||||||
import eventDemo.libs.event.EventStream
|
import eventDemo.libs.event.EventStream
|
||||||
|
|
||||||
56
src/main/kotlin/eventDemo/app/query/ReadTheGameState.kt
Normal file
56
src/main/kotlin/eventDemo/app/query/ReadTheGameState.kt
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package eventDemo.app.query
|
||||||
|
|
||||||
|
import eventDemo.app.entity.GameId
|
||||||
|
import eventDemo.app.event.GameEventStream
|
||||||
|
import eventDemo.app.event.buildStateFromEventStream
|
||||||
|
import eventDemo.app.event.event.CardIsPlayedEvent
|
||||||
|
import eventDemo.libs.event.readLastOf
|
||||||
|
import eventDemo.shared.GameIdSerializer
|
||||||
|
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.resources.get
|
||||||
|
import io.ktor.server.response.respond
|
||||||
|
import io.ktor.server.routing.Route
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@Resource("/game/{id}")
|
||||||
|
class Game(
|
||||||
|
@Serializable(with = GameIdSerializer::class)
|
||||||
|
val id: GameId,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
@Resource("card/last")
|
||||||
|
class Card(
|
||||||
|
val game: Game,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@Resource("state")
|
||||||
|
class State(
|
||||||
|
val game: Game,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API routes to read the game state.
|
||||||
|
*/
|
||||||
|
fun Route.readTheGameState(eventStream: GameEventStream) {
|
||||||
|
authenticate {
|
||||||
|
// Read the last played card on the game.
|
||||||
|
get<Game.Card> { body ->
|
||||||
|
eventStream
|
||||||
|
.readLastOf<CardIsPlayedEvent, _, _>(body.game.id)
|
||||||
|
?.let { call.respond(it.card) }
|
||||||
|
?: call.response.status(HttpStatusCode.BadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the last played card on the game.
|
||||||
|
get<Game.State> { body ->
|
||||||
|
val state = body.game.id.buildStateFromEventStream(eventStream)
|
||||||
|
call.respond(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package eventDemo.configuration
|
package eventDemo.configuration
|
||||||
|
|
||||||
import eventDemo.app.GameEventReactionListener
|
import eventDemo.app.eventListener.GameEventReactionListener
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import org.koin.ktor.ext.get
|
import org.koin.ktor.ext.get
|
||||||
|
|
||||||
@@ -11,11 +11,11 @@ fun Application.configure() {
|
|||||||
|
|
||||||
configureSerialization()
|
configureSerialization()
|
||||||
|
|
||||||
configureSockets()
|
configureWebSockets()
|
||||||
configureWebSocketsGameRoute(get(), get())
|
declareWebSocketsGameRoute(get(), get())
|
||||||
|
|
||||||
configureHttp()
|
|
||||||
configureHttpRouting()
|
configureHttpRouting()
|
||||||
|
declareHttpGameRoute()
|
||||||
|
|
||||||
GameEventReactionListener(get(), get())
|
GameEventReactionListener(get(), get())
|
||||||
.init()
|
.init()
|
||||||
|
|||||||
@@ -5,9 +5,13 @@ import io.ktor.http.HttpMethod
|
|||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.application.install
|
import io.ktor.server.application.install
|
||||||
|
import io.ktor.server.plugins.autohead.AutoHeadResponse
|
||||||
import io.ktor.server.plugins.cors.routing.CORS
|
import io.ktor.server.plugins.cors.routing.CORS
|
||||||
|
import io.ktor.server.plugins.statuspages.StatusPages
|
||||||
|
import io.ktor.server.resources.Resources
|
||||||
|
import io.ktor.server.response.respondText
|
||||||
|
|
||||||
fun Application.configureHttp() {
|
fun Application.configureHttpRouting() {
|
||||||
install(CORS) {
|
install(CORS) {
|
||||||
allowMethod(HttpMethod.Options)
|
allowMethod(HttpMethod.Options)
|
||||||
allowMethod(HttpMethod.Put)
|
allowMethod(HttpMethod.Put)
|
||||||
@@ -18,6 +22,16 @@ fun Application.configureHttp() {
|
|||||||
allowHeader("MyCustomHeader")
|
allowHeader("MyCustomHeader")
|
||||||
anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
|
anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
|
||||||
}
|
}
|
||||||
|
install(AutoHeadResponse)
|
||||||
|
install(Resources)
|
||||||
|
install(StatusPages) {
|
||||||
|
exception<BadRequestException> { call, cause ->
|
||||||
|
call.respondText(text = "400: $cause", status = HttpStatusCode.BadRequest)
|
||||||
|
}
|
||||||
|
exception<Throwable> { call, cause ->
|
||||||
|
call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BadRequestException(
|
class BadRequestException(
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package eventDemo.configuration
|
||||||
|
|
||||||
|
import eventDemo.shared.UUIDSerializer
|
||||||
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
|
import io.ktor.server.application.Application
|
||||||
|
import io.ktor.server.application.install
|
||||||
|
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
fun Application.configureSerialization() {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(
|
||||||
|
Json {
|
||||||
|
serializersModule =
|
||||||
|
SerializersModule {
|
||||||
|
contextual(UUID::class) { UUIDSerializer }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package eventDemo.configuration
|
||||||
|
|
||||||
|
import io.ktor.server.application.Application
|
||||||
|
import io.ktor.server.application.install
|
||||||
|
import io.ktor.server.websocket.WebSockets
|
||||||
|
import io.ktor.server.websocket.pingPeriod
|
||||||
|
import io.ktor.server.websocket.timeout
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
fun Application.configureWebSockets() {
|
||||||
|
install(WebSockets) {
|
||||||
|
pingPeriod = Duration.ofSeconds(15)
|
||||||
|
timeout = Duration.ofSeconds(15)
|
||||||
|
maxFrameSize = Long.MAX_VALUE
|
||||||
|
masking = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package eventDemo.configuration
|
||||||
|
|
||||||
|
import eventDemo.app.command.gameSocket
|
||||||
|
import eventDemo.app.event.GameEventBus
|
||||||
|
import eventDemo.app.event.GameEventStream
|
||||||
|
import io.ktor.server.application.Application
|
||||||
|
import io.ktor.server.routing.routing
|
||||||
|
|
||||||
|
fun Application.declareWebSocketsGameRoute(
|
||||||
|
eventStream: GameEventStream,
|
||||||
|
eventBus: GameEventBus,
|
||||||
|
) {
|
||||||
|
routing {
|
||||||
|
gameSocket(eventStream, eventBus)
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/main/kotlin/eventDemo/configuration/DeclareHttpRoutes.kt
Normal file
12
src/main/kotlin/eventDemo/configuration/DeclareHttpRoutes.kt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package eventDemo.configuration
|
||||||
|
|
||||||
|
import eventDemo.app.query.readTheGameState
|
||||||
|
import io.ktor.server.application.Application
|
||||||
|
import io.ktor.server.routing.routing
|
||||||
|
import org.koin.ktor.ext.get
|
||||||
|
|
||||||
|
fun Application.declareHttpGameRoute() {
|
||||||
|
routing {
|
||||||
|
readTheGameState(get())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package eventDemo.configuration
|
|
||||||
|
|
||||||
import eventDemo.app.actions.readGameState
|
|
||||||
import eventDemo.app.actions.readLastPlayedCard
|
|
||||||
import io.ktor.http.HttpStatusCode
|
|
||||||
import io.ktor.server.application.Application
|
|
||||||
import io.ktor.server.application.install
|
|
||||||
import io.ktor.server.plugins.autohead.AutoHeadResponse
|
|
||||||
import io.ktor.server.plugins.statuspages.StatusPages
|
|
||||||
import io.ktor.server.resources.Resources
|
|
||||||
import io.ktor.server.response.respondText
|
|
||||||
import io.ktor.server.routing.routing
|
|
||||||
|
|
||||||
fun Application.configureHttpRouting() {
|
|
||||||
install(AutoHeadResponse)
|
|
||||||
install(Resources)
|
|
||||||
install(StatusPages) {
|
|
||||||
exception<BadRequestException> { call, cause ->
|
|
||||||
call.respondText(text = "400: $cause", status = HttpStatusCode.BadRequest)
|
|
||||||
}
|
|
||||||
exception<Throwable> { call, cause ->
|
|
||||||
call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
routing {
|
|
||||||
readLastPlayedCard()
|
|
||||||
readGameState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package eventDemo.configuration
|
|
||||||
|
|
||||||
import eventDemo.app.GameEventPlayerNotificationListener
|
|
||||||
import eventDemo.app.actions.GameCommandHandler
|
|
||||||
import eventDemo.app.entity.Player
|
|
||||||
import eventDemo.app.event.GameEventBus
|
|
||||||
import eventDemo.app.event.GameEventStream
|
|
||||||
import io.ktor.server.application.Application
|
|
||||||
import io.ktor.server.application.ApplicationCall
|
|
||||||
import io.ktor.server.application.install
|
|
||||||
import io.ktor.server.auth.authenticate
|
|
||||||
import io.ktor.server.auth.jwt.JWTPrincipal
|
|
||||||
import io.ktor.server.auth.principal
|
|
||||||
import io.ktor.server.routing.routing
|
|
||||||
import io.ktor.server.websocket.WebSockets
|
|
||||||
import io.ktor.server.websocket.pingPeriod
|
|
||||||
import io.ktor.server.websocket.timeout
|
|
||||||
import io.ktor.server.websocket.webSocket
|
|
||||||
import java.time.Duration
|
|
||||||
|
|
||||||
fun Application.configureSockets() {
|
|
||||||
install(WebSockets) {
|
|
||||||
pingPeriod = Duration.ofSeconds(15)
|
|
||||||
timeout = Duration.ofSeconds(15)
|
|
||||||
maxFrameSize = Long.MAX_VALUE
|
|
||||||
masking = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Application.configureWebSocketsGameRoute(
|
|
||||||
eventStream: GameEventStream,
|
|
||||||
eventBus: GameEventBus,
|
|
||||||
) {
|
|
||||||
routing {
|
|
||||||
authenticate {
|
|
||||||
webSocket("/game") {
|
|
||||||
GameCommandHandler(eventStream, incoming, outgoing).init(call.getPlayer())
|
|
||||||
GameEventPlayerNotificationListener(eventBus, outgoing).init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ApplicationCall.getPlayer() =
|
|
||||||
principal<JWTPrincipal>()!!.run {
|
|
||||||
Player(
|
|
||||||
id = payload.getClaim("playerid").asString(),
|
|
||||||
name = payload.getClaim("username").asString(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package eventDemo.libs.command
|
package eventDemo.libs.command
|
||||||
|
|
||||||
import eventDemo.configuration.CommandIdSerializer
|
import eventDemo.shared.CommandIdSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.shared
|
package eventDemo.shared
|
||||||
|
|
||||||
import eventDemo.app.command.GameCommand
|
import eventDemo.app.command.command.GameCommand
|
||||||
import eventDemo.app.event.GameEvent
|
import eventDemo.app.event.event.GameEvent
|
||||||
import io.ktor.websocket.Frame
|
import io.ktor.websocket.Frame
|
||||||
import io.ktor.websocket.readText
|
import io.ktor.websocket.readText
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|||||||
@@ -1,35 +1,16 @@
|
|||||||
package eventDemo.configuration
|
package eventDemo.shared
|
||||||
|
|
||||||
import eventDemo.app.GameId
|
import eventDemo.app.entity.GameId
|
||||||
import eventDemo.app.entity.Player.PlayerId
|
import eventDemo.app.entity.Player.PlayerId
|
||||||
import eventDemo.libs.command.CommandId
|
import eventDemo.libs.command.CommandId
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
|
||||||
import io.ktor.server.application.Application
|
|
||||||
import io.ktor.server.application.install
|
|
||||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
import kotlinx.serialization.encoding.Decoder
|
import kotlinx.serialization.encoding.Decoder
|
||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.modules.SerializersModule
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
fun Application.configureSerialization() {
|
|
||||||
install(ContentNegotiation) {
|
|
||||||
json(
|
|
||||||
Json {
|
|
||||||
serializersModule =
|
|
||||||
SerializersModule {
|
|
||||||
contextual(UUID::class) { UUIDSerializer }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object CommandIdSerializer : KSerializer<CommandId> {
|
object CommandIdSerializer : KSerializer<CommandId> {
|
||||||
override fun deserialize(decoder: Decoder): CommandId = CommandId(decoder.decodeString())
|
override fun deserialize(decoder: Decoder): CommandId = CommandId(decoder.decodeString())
|
||||||
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package eventDemo.app.actions
|
package eventDemo.app.query
|
||||||
|
|
||||||
import eventDemo.app.GameId
|
|
||||||
import eventDemo.app.entity.Card
|
import eventDemo.app.entity.Card
|
||||||
|
import eventDemo.app.entity.GameId
|
||||||
import eventDemo.app.entity.Player
|
import eventDemo.app.entity.Player
|
||||||
import eventDemo.app.event.CardIsPlayedEvent
|
|
||||||
import eventDemo.app.event.GameEventStream
|
import eventDemo.app.event.GameEventStream
|
||||||
|
import eventDemo.app.event.event.CardIsPlayedEvent
|
||||||
import eventDemo.configuration.configure
|
import eventDemo.configuration.configure
|
||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.body
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package eventDemo.app.actions
|
package eventDemo.app.query
|
||||||
|
|
||||||
import eventDemo.configuration.UUIDSerializer
|
import eventDemo.shared.UUIDSerializer
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
Reference in New Issue
Block a user