update ktlint rules
This commit is contained in:
@@ -4,3 +4,5 @@ ktlint_standard = enabled
|
|||||||
ktlint_experimental = enabled
|
ktlint_experimental = enabled
|
||||||
ktlint_standard_string-template-indent = enabled
|
ktlint_standard_string-template-indent = enabled
|
||||||
ktlint_standard_multiline-expression-wrapping = enabled
|
ktlint_standard_multiline-expression-wrapping = enabled
|
||||||
|
ktlint_function_signature_body_expression_wrapping = always
|
||||||
|
indent_size = 2
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
@file:Suppress("PropertyName")
|
@file:Suppress("PropertyName")
|
||||||
|
|
||||||
@Suppress("ktlint:standard:property-naming")
|
|
||||||
val ktor_version: String by project
|
val ktor_version: String by project
|
||||||
val kotlin_version: String by project
|
val kotlin_version: String by project
|
||||||
val kotlin_serialization_version: String by project
|
val kotlin_serialization_version: String by project
|
||||||
@@ -10,64 +9,63 @@ val kotlin_logging_version: String by project
|
|||||||
val kotest_version: String by project
|
val kotest_version: String by project
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "2.1.10"
|
kotlin("jvm") version "2.1.10"
|
||||||
id("io.ktor.plugin") version "2.3.13"
|
id("io.ktor.plugin") version "2.3.13"
|
||||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.1.10"
|
id("org.jetbrains.kotlin.plugin.serialization") version "2.1.10"
|
||||||
id("org.jlleitschuh.gradle.ktlint") version "12.2.0"
|
id("org.jlleitschuh.gradle.ktlint") version "12.2.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "io.github.flecomte"
|
group = "io.github.flecomte"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
|
||||||
application {
|
application {
|
||||||
mainClass.set("eventDemo.ApplicationKt")
|
mainClass.set("eventDemo.ApplicationKt")
|
||||||
|
|
||||||
val isDevelopment: Boolean = project.ext.has("development")
|
val isDevelopment: Boolean = project.ext.has("development")
|
||||||
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
|
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
|
||||||
}
|
}
|
||||||
|
|
||||||
configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
|
configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
|
||||||
version.set("1.5.0")
|
version.set("1.5.0")
|
||||||
enableExperimentalRules.set(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<Test>().configureEach {
|
tasks.withType<Test>().configureEach {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("io.ktor:ktor-server-core-jvm")
|
implementation("io.ktor:ktor-server-core-jvm")
|
||||||
implementation("io.ktor:ktor-server-auth-jvm")
|
implementation("io.ktor:ktor-server-auth-jvm")
|
||||||
implementation("io.ktor:ktor-server-auth-jwt-jvm")
|
implementation("io.ktor:ktor-server-auth-jwt-jvm")
|
||||||
implementation("io.ktor:ktor-server-auto-head-response-jvm")
|
implementation("io.ktor:ktor-server-auto-head-response-jvm")
|
||||||
implementation("io.ktor:ktor-server-resources")
|
implementation("io.ktor:ktor-server-resources")
|
||||||
implementation("io.ktor:ktor-server-content-negotiation-jvm")
|
implementation("io.ktor:ktor-server-content-negotiation-jvm")
|
||||||
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
|
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
|
||||||
implementation("io.ktor:ktor-server-websockets-jvm")
|
implementation("io.ktor:ktor-server-websockets-jvm")
|
||||||
implementation("io.ktor:ktor-server-cors-jvm")
|
implementation("io.ktor:ktor-server-cors-jvm")
|
||||||
implementation("io.ktor:ktor-server-host-common-jvm")
|
implementation("io.ktor:ktor-server-host-common-jvm")
|
||||||
implementation("io.ktor:ktor-server-status-pages-jvm")
|
implementation("io.ktor:ktor-server-status-pages-jvm")
|
||||||
implementation("io.ktor:ktor-server-netty-jvm")
|
implementation("io.ktor:ktor-server-netty-jvm")
|
||||||
implementation("io.ktor:ktor-server-data-conversion")
|
implementation("io.ktor:ktor-server-data-conversion")
|
||||||
implementation("io.ktor:ktor-client-content-negotiation")
|
implementation("io.ktor:ktor-client-content-negotiation")
|
||||||
implementation("io.ktor:ktor-client-auth")
|
implementation("io.ktor:ktor-client-auth")
|
||||||
implementation("ch.qos.logback:logback-classic:$logback_version")
|
implementation("ch.qos.logback:logback-classic:$logback_version")
|
||||||
implementation("io.insert-koin:koin-ktor:$koin_version")
|
implementation("io.insert-koin:koin-ktor:$koin_version")
|
||||||
implementation("io.insert-koin:koin-logger-slf4j:$koin_version")
|
implementation("io.insert-koin:koin-logger-slf4j:$koin_version")
|
||||||
implementation("io.github.oshai:kotlin-logging-jvm:$kotlin_logging_version")
|
implementation("io.github.oshai:kotlin-logging-jvm:$kotlin_logging_version")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:$kotlin_serialization_version")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:$kotlin_serialization_version")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
|
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
|
||||||
|
|
||||||
// Force version of sub library (for security)
|
// Force version of sub library (for security)
|
||||||
implementation("commons-codec:commons-codec:1.13")
|
implementation("commons-codec:commons-codec:1.13")
|
||||||
|
|
||||||
testImplementation("io.kotest:kotest-extensions-koin:$kotest_version")
|
testImplementation("io.kotest:kotest-extensions-koin:$kotest_version")
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
|
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
|
||||||
testImplementation("io.ktor:ktor-server-test-host-jvm:2.3.11")
|
testImplementation("io.ktor:ktor-server-test-host-jvm:2.3.11")
|
||||||
testImplementation("io.kotest:kotest-runner-junit5:$kotest_version")
|
testImplementation("io.kotest:kotest-runner-junit5:$kotest_version")
|
||||||
testImplementation("io.mockk:mockk:1.13.17")
|
testImplementation("io.mockk:mockk:1.13.17")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import io.ktor.server.engine.embeddedServer
|
|||||||
import io.ktor.server.netty.Netty
|
import io.ktor.server.netty.Netty
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
embeddedServer(
|
embeddedServer(
|
||||||
factory = Netty,
|
factory = Netty,
|
||||||
port = 8080,
|
port = 8080,
|
||||||
host = "0.0.0.0",
|
host = "0.0.0.0",
|
||||||
module = Application::configure,
|
module = Application::configure,
|
||||||
watchPaths = listOf("classes"),
|
watchPaths = listOf("classes"),
|
||||||
).start(wait = true)
|
).start(wait = true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,21 +9,21 @@ import kotlinx.coroutines.channels.SendChannel
|
|||||||
typealias ErrorNotifier = suspend (String) -> Unit
|
typealias ErrorNotifier = suspend (String) -> Unit
|
||||||
|
|
||||||
fun errorNotifier(
|
fun errorNotifier(
|
||||||
command: GameCommand,
|
command: GameCommand,
|
||||||
channel: SendChannel<Notification>,
|
channel: SendChannel<Notification>,
|
||||||
): ErrorNotifier =
|
): ErrorNotifier =
|
||||||
{
|
{
|
||||||
val logger = KotlinLogging.logger { }
|
val logger = KotlinLogging.logger { }
|
||||||
ErrorNotification(message = it)
|
ErrorNotification(message = it)
|
||||||
.let { notification ->
|
.let { notification ->
|
||||||
logger.atWarn {
|
logger.atWarn {
|
||||||
message = "Notification ERROR sent: ${notification.message}"
|
message = "Notification ERROR sent: ${notification.message}"
|
||||||
payload =
|
payload =
|
||||||
mapOf(
|
mapOf(
|
||||||
"notification" to notification,
|
"notification" to notification,
|
||||||
"command" to command,
|
"command" to command,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
channel.send(notification)
|
channel.send(notification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,32 +15,33 @@ import kotlinx.coroutines.channels.SendChannel
|
|||||||
* This action can be executing an action and produce a new [GameEvent] after verification.
|
* This action can be executing an action and produce a new [GameEvent] after verification.
|
||||||
*/
|
*/
|
||||||
class GameCommandHandler(
|
class GameCommandHandler(
|
||||||
private val commandStreamChannel: CommandStreamChannelBuilder<GameCommand>,
|
private val commandStreamChannel: CommandStreamChannelBuilder<GameCommand>,
|
||||||
private val runner: GameCommandRunner,
|
private val runner: GameCommandRunner,
|
||||||
) {
|
) {
|
||||||
private val logger = KotlinLogging.logger { }
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init the handler
|
* Init the handler
|
||||||
*/
|
*/
|
||||||
suspend fun handle(
|
suspend fun handle(
|
||||||
player: Player,
|
player: Player,
|
||||||
incomingCommandChannel: ReceiveChannel<GameCommand>,
|
incomingCommandChannel: ReceiveChannel<GameCommand>,
|
||||||
outgoingErrorChannelNotification: SendChannel<Notification>,
|
outgoingErrorChannelNotification: SendChannel<Notification>,
|
||||||
) = commandStreamChannel(incomingCommandChannel)
|
) =
|
||||||
.process { command ->
|
commandStreamChannel(incomingCommandChannel)
|
||||||
if (command.payload.player.id != player.id) {
|
.process { command ->
|
||||||
logger.atWarn {
|
if (command.payload.player.id != player.id) {
|
||||||
message = "Handle command Refuse, the player of the command is not the same: $command"
|
logger.atWarn {
|
||||||
payload = mapOf("command" to command)
|
message = "Handle command Refuse, the player of the command is not the same: $command"
|
||||||
}
|
payload = mapOf("command" to command)
|
||||||
nack()
|
}
|
||||||
} else {
|
nack()
|
||||||
logger.atInfo {
|
} else {
|
||||||
message = "Handle command: $command"
|
logger.atInfo {
|
||||||
payload = mapOf("command" to command)
|
message = "Handle command: $command"
|
||||||
}
|
payload = mapOf("command" to command)
|
||||||
runner.run(command, outgoingErrorChannelNotification)
|
}
|
||||||
}
|
runner.run(command, outgoingErrorChannelNotification)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,29 +18,29 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
@DelicateCoroutinesApi
|
@DelicateCoroutinesApi
|
||||||
fun Route.gameSocket(
|
fun Route.gameSocket(
|
||||||
playerNotificationListener: PlayerNotificationEventListener,
|
playerNotificationListener: PlayerNotificationEventListener,
|
||||||
commandHandler: GameCommandHandler,
|
commandHandler: GameCommandHandler,
|
||||||
) {
|
) {
|
||||||
authenticate {
|
authenticate {
|
||||||
webSocket("/game") {
|
webSocket("/game") {
|
||||||
val currentPlayer = call.getPlayer()
|
val currentPlayer = call.getPlayer()
|
||||||
val outgoingFrameChannel: SendChannel<Notification> = fromFrameChannel(outgoing)
|
val outgoingFrameChannel: SendChannel<Notification> = fromFrameChannel(outgoing)
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
commandHandler.handle(
|
commandHandler.handle(
|
||||||
currentPlayer,
|
currentPlayer,
|
||||||
toObjectChannel(incoming),
|
toObjectChannel(incoming),
|
||||||
outgoingFrameChannel,
|
outgoingFrameChannel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
playerNotificationListener.startListening(outgoingFrameChannel, currentPlayer)
|
playerNotificationListener.startListening(outgoingFrameChannel, currentPlayer)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ApplicationCall.getPlayer() =
|
private fun ApplicationCall.getPlayer() =
|
||||||
principal<JWTPrincipal>()!!.run {
|
principal<JWTPrincipal>()!!.run {
|
||||||
Player(
|
Player(
|
||||||
id = payload.getClaim("playerid").asString(),
|
id = payload.getClaim("playerid").asString(),
|
||||||
name = payload.getClaim("username").asString(),
|
name = payload.getClaim("username").asString(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,21 +11,21 @@ import eventDemo.app.notification.Notification
|
|||||||
import kotlinx.coroutines.channels.SendChannel
|
import kotlinx.coroutines.channels.SendChannel
|
||||||
|
|
||||||
class GameCommandRunner(
|
class GameCommandRunner(
|
||||||
private val eventHandler: GameEventHandler,
|
private val eventHandler: GameEventHandler,
|
||||||
private val gameStateRepository: GameStateRepository,
|
private val gameStateRepository: GameStateRepository,
|
||||||
) {
|
) {
|
||||||
suspend fun run(
|
suspend fun run(
|
||||||
command: GameCommand,
|
command: GameCommand,
|
||||||
outgoingErrorChannelNotification: SendChannel<Notification>,
|
outgoingErrorChannelNotification: SendChannel<Notification>,
|
||||||
) {
|
) {
|
||||||
val gameState = gameStateRepository.getLast(command.payload.aggregateId)
|
val gameState = gameStateRepository.getLast(command.payload.aggregateId)
|
||||||
val errorNotifier = errorNotifier(command, outgoingErrorChannelNotification)
|
val errorNotifier = errorNotifier(command, outgoingErrorChannelNotification)
|
||||||
|
|
||||||
when (command) {
|
when (command) {
|
||||||
is IWantToPlayCardCommand -> command.run(gameState, errorNotifier, this.eventHandler)
|
is IWantToPlayCardCommand -> command.run(gameState, errorNotifier, this.eventHandler)
|
||||||
is IamReadyToPlayCommand -> command.run(gameState, errorNotifier, this.eventHandler)
|
is IamReadyToPlayCommand -> command.run(gameState, errorNotifier, this.eventHandler)
|
||||||
is IWantToJoinTheGameCommand -> command.run(gameState, errorNotifier, this.eventHandler)
|
is IWantToJoinTheGameCommand -> command.run(gameState, errorNotifier, this.eventHandler)
|
||||||
is ICantPlayCommand -> command.run(gameState, errorNotifier, this.eventHandler)
|
is ICantPlayCommand -> command.run(gameState, errorNotifier, this.eventHandler)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ import kotlinx.coroutines.channels.ReceiveChannel
|
|||||||
* A stream to publish and read the game command.
|
* A stream to publish and read the game command.
|
||||||
*/
|
*/
|
||||||
class GameCommandStream(
|
class GameCommandStream(
|
||||||
incoming: ReceiveChannel<GameCommand>,
|
incoming: ReceiveChannel<GameCommand>,
|
||||||
) : CommandStream<GameCommand> by CommandStreamChannel(incoming)
|
) : CommandStream<GameCommand> by CommandStreamChannel(incoming)
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed interface GameCommand : Command {
|
sealed interface GameCommand : Command {
|
||||||
val payload: Payload
|
val payload: Payload
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed interface Payload {
|
sealed interface Payload {
|
||||||
val aggregateId: GameId
|
val aggregateId: GameId
|
||||||
val player: Player
|
val player: Player
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,39 +14,39 @@ import kotlinx.serialization.Serializable
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ICantPlayCommand(
|
data class ICantPlayCommand(
|
||||||
override val payload: Payload,
|
override val payload: Payload,
|
||||||
) : GameCommand {
|
) : GameCommand {
|
||||||
override val id: CommandId = CommandId()
|
override val id: CommandId = CommandId()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Payload(
|
data class Payload(
|
||||||
override val aggregateId: GameId,
|
override val aggregateId: GameId,
|
||||||
override val player: Player,
|
override val player: Player,
|
||||||
) : GameCommand.Payload
|
) : GameCommand.Payload
|
||||||
|
|
||||||
suspend fun run(
|
suspend fun run(
|
||||||
state: GameState,
|
state: GameState,
|
||||||
playerErrorNotifier: ErrorNotifier,
|
playerErrorNotifier: ErrorNotifier,
|
||||||
eventHandler: GameEventHandler,
|
eventHandler: GameEventHandler,
|
||||||
) {
|
) {
|
||||||
if (state.currentPlayerTurn != payload.player) {
|
if (state.currentPlayerTurn != payload.player) {
|
||||||
playerErrorNotifier("Its not your turn!")
|
playerErrorNotifier("Its not your turn!")
|
||||||
return
|
return
|
||||||
}
|
|
||||||
val playableCards = state.playableCards(payload.player)
|
|
||||||
if (playableCards.isEmpty()) {
|
|
||||||
val takenCard = state.deck.stack.first()
|
|
||||||
|
|
||||||
eventHandler.handle(payload.aggregateId) {
|
|
||||||
PlayerHavePassEvent(
|
|
||||||
aggregateId = payload.aggregateId,
|
|
||||||
player = payload.player,
|
|
||||||
takenCard = takenCard,
|
|
||||||
version = it,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
playerErrorNotifier("You can and must play one card, like ${playableCards.first()::class.simpleName}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
val playableCards = state.playableCards(payload.player)
|
||||||
|
if (playableCards.isEmpty()) {
|
||||||
|
val takenCard = state.deck.stack.first()
|
||||||
|
|
||||||
|
eventHandler.handle(payload.aggregateId) {
|
||||||
|
PlayerHavePassEvent(
|
||||||
|
aggregateId = payload.aggregateId,
|
||||||
|
player = payload.player,
|
||||||
|
takenCard = takenCard,
|
||||||
|
version = it,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
playerErrorNotifier("You can and must play one card, like ${playableCards.first()::class.simpleName}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,31 +14,31 @@ import kotlinx.serialization.Serializable
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class IWantToJoinTheGameCommand(
|
data class IWantToJoinTheGameCommand(
|
||||||
override val payload: Payload,
|
override val payload: Payload,
|
||||||
) : GameCommand {
|
) : GameCommand {
|
||||||
override val id: CommandId = CommandId()
|
override val id: CommandId = CommandId()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Payload(
|
data class Payload(
|
||||||
override val aggregateId: GameId,
|
override val aggregateId: GameId,
|
||||||
override val player: Player,
|
override val player: Player,
|
||||||
) : GameCommand.Payload
|
) : GameCommand.Payload
|
||||||
|
|
||||||
suspend fun run(
|
suspend fun run(
|
||||||
state: GameState,
|
state: GameState,
|
||||||
playerErrorNotifier: ErrorNotifier,
|
playerErrorNotifier: ErrorNotifier,
|
||||||
eventHandler: GameEventHandler,
|
eventHandler: GameEventHandler,
|
||||||
) {
|
) {
|
||||||
if (!state.isStarted) {
|
if (!state.isStarted) {
|
||||||
eventHandler.handle(payload.aggregateId) {
|
eventHandler.handle(payload.aggregateId) {
|
||||||
NewPlayerEvent(
|
NewPlayerEvent(
|
||||||
aggregateId = payload.aggregateId,
|
aggregateId = payload.aggregateId,
|
||||||
player = payload.player,
|
player = payload.player,
|
||||||
version = it,
|
version = it,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
playerErrorNotifier("The game is already started")
|
playerErrorNotifier("The game is already started")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,42 +15,42 @@ import kotlinx.serialization.Serializable
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class IWantToPlayCardCommand(
|
data class IWantToPlayCardCommand(
|
||||||
override val payload: Payload,
|
override val payload: Payload,
|
||||||
) : GameCommand {
|
) : GameCommand {
|
||||||
override val id: CommandId = CommandId()
|
override val id: CommandId = CommandId()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Payload(
|
data class Payload(
|
||||||
override val aggregateId: GameId,
|
override val aggregateId: GameId,
|
||||||
override val player: Player,
|
override val player: Player,
|
||||||
val card: Card,
|
val card: Card,
|
||||||
) : GameCommand.Payload
|
) : GameCommand.Payload
|
||||||
|
|
||||||
suspend fun run(
|
suspend fun run(
|
||||||
state: GameState,
|
state: GameState,
|
||||||
playerErrorNotifier: ErrorNotifier,
|
playerErrorNotifier: ErrorNotifier,
|
||||||
eventHandler: GameEventHandler,
|
eventHandler: GameEventHandler,
|
||||||
) {
|
) {
|
||||||
if (!state.isStarted) {
|
if (!state.isStarted) {
|
||||||
playerErrorNotifier("The game is Not started")
|
playerErrorNotifier("The game is Not started")
|
||||||
return
|
return
|
||||||
}
|
|
||||||
if (state.currentPlayerTurn != payload.player) {
|
|
||||||
playerErrorNotifier("Its not your turn!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.canBePlayThisCard(payload.player, payload.card)) {
|
|
||||||
eventHandler.handle(payload.aggregateId) {
|
|
||||||
CardIsPlayedEvent(
|
|
||||||
aggregateId = payload.aggregateId,
|
|
||||||
card = payload.card,
|
|
||||||
player = payload.player,
|
|
||||||
version = it,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
playerErrorNotifier("You cannot play this card")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (state.currentPlayerTurn != payload.player) {
|
||||||
|
playerErrorNotifier("Its not your turn!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.canBePlayThisCard(payload.player, payload.card)) {
|
||||||
|
eventHandler.handle(payload.aggregateId) {
|
||||||
|
CardIsPlayedEvent(
|
||||||
|
aggregateId = payload.aggregateId,
|
||||||
|
card = payload.card,
|
||||||
|
player = payload.player,
|
||||||
|
version = it,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
playerErrorNotifier("You cannot play this card")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,38 +14,38 @@ import kotlinx.serialization.Serializable
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class IamReadyToPlayCommand(
|
data class IamReadyToPlayCommand(
|
||||||
override val payload: Payload,
|
override val payload: Payload,
|
||||||
) : GameCommand {
|
) : GameCommand {
|
||||||
override val id: CommandId = CommandId()
|
override val id: CommandId = CommandId()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Payload(
|
data class Payload(
|
||||||
override val aggregateId: GameId,
|
override val aggregateId: GameId,
|
||||||
override val player: Player,
|
override val player: Player,
|
||||||
) : GameCommand.Payload
|
) : GameCommand.Payload
|
||||||
|
|
||||||
suspend fun run(
|
suspend fun run(
|
||||||
state: GameState,
|
state: GameState,
|
||||||
playerErrorNotifier: ErrorNotifier,
|
playerErrorNotifier: ErrorNotifier,
|
||||||
eventHandler: GameEventHandler,
|
eventHandler: GameEventHandler,
|
||||||
) {
|
) {
|
||||||
val playerExist: Boolean = state.players.contains(payload.player)
|
val playerExist: Boolean = state.players.contains(payload.player)
|
||||||
val playerIsAlreadyReady: Boolean = state.readyPlayers.contains(payload.player)
|
val playerIsAlreadyReady: Boolean = state.readyPlayers.contains(payload.player)
|
||||||
|
|
||||||
if (state.isStarted) {
|
if (state.isStarted) {
|
||||||
playerErrorNotifier("The game is already started")
|
playerErrorNotifier("The game is already started")
|
||||||
} else if (!playerExist) {
|
} else if (!playerExist) {
|
||||||
playerErrorNotifier("You are not in the game")
|
playerErrorNotifier("You are not in the game")
|
||||||
} else if (playerIsAlreadyReady) {
|
} else if (playerIsAlreadyReady) {
|
||||||
playerErrorNotifier("You are already ready")
|
playerErrorNotifier("You are already ready")
|
||||||
} else {
|
} else {
|
||||||
eventHandler.handle(payload.aggregateId) {
|
eventHandler.handle(payload.aggregateId) {
|
||||||
PlayerReadyEvent(
|
PlayerReadyEvent(
|
||||||
aggregateId = payload.aggregateId,
|
aggregateId = payload.aggregateId,
|
||||||
player = payload.player,
|
player = payload.player,
|
||||||
version = it,
|
version = it,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,100 +10,100 @@ import java.util.UUID
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed interface Card {
|
sealed interface Card {
|
||||||
val id: UUID
|
val id: UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The color of a card
|
* The color of a card
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
enum class Color {
|
enum class Color {
|
||||||
Blue,
|
Blue,
|
||||||
Red,
|
Red,
|
||||||
Yellow,
|
Yellow,
|
||||||
Green,
|
Green,
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface ColorCard : Card {
|
sealed interface ColorCard : Card {
|
||||||
val color: Color
|
val color: Color
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A play card with color and number
|
* A play card with color and number
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("Simple")
|
@SerialName("Simple")
|
||||||
data class NumericCard(
|
data class NumericCard(
|
||||||
val number: Int,
|
val number: Int,
|
||||||
override val color: Color,
|
override val color: Color,
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
) : Card,
|
) : Card,
|
||||||
ColorCard
|
ColorCard
|
||||||
|
|
||||||
sealed interface Special : Card
|
sealed interface Special : Card
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A revert card to revert the order of the turn.
|
* A revert card to revert the order of the turn.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("Reverse")
|
@SerialName("Reverse")
|
||||||
data class ReverseCard(
|
data class ReverseCard(
|
||||||
override val color: Color,
|
override val color: Color,
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
) : Special,
|
) : Special,
|
||||||
ColorCard
|
ColorCard
|
||||||
|
|
||||||
sealed interface PassTurnCard : Card
|
sealed interface PassTurnCard : Card
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A pass card to pass the turn of the next player.
|
* A pass card to pass the turn of the next player.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("Pass")
|
@SerialName("Pass")
|
||||||
data class PassCard(
|
data class PassCard(
|
||||||
override val color: Color,
|
override val color: Color,
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
) : Special,
|
) : Special,
|
||||||
ColorCard,
|
ColorCard,
|
||||||
PassTurnCard
|
PassTurnCard
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A play card to force the next player to take 2 card and pass the turn.
|
* A play card to force the next player to take 2 card and pass the turn.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("Plus2")
|
@SerialName("Plus2")
|
||||||
data class Plus2Card(
|
data class Plus2Card(
|
||||||
override val color: Color,
|
override val color: Color,
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
) : Special,
|
) : Special,
|
||||||
ColorCard,
|
ColorCard,
|
||||||
PassTurnCard
|
PassTurnCard
|
||||||
|
|
||||||
sealed interface AllColorCard : Card
|
sealed interface AllColorCard : Card
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A play card to force the next player to take 4 card and pass the turn.
|
* A play card to force the next player to take 4 card and pass the turn.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("Plus4")
|
@SerialName("Plus4")
|
||||||
class Plus4Card(
|
class Plus4Card(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
) : Special,
|
) : Special,
|
||||||
AllColorCard,
|
AllColorCard,
|
||||||
PassTurnCard
|
PassTurnCard
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A play card to change the color.
|
* A play card to change the color.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("ChangeColor")
|
@SerialName("ChangeColor")
|
||||||
class ChangeColorCard(
|
class ChangeColorCard(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
) : Special,
|
) : Special,
|
||||||
AllColorCard
|
AllColorCard
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,119 +4,130 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Deck(
|
data class Deck(
|
||||||
val stack: Stack = Stack(),
|
val stack: Stack = Stack(),
|
||||||
val discard: Discard = Discard(),
|
val discard: Discard = Discard(),
|
||||||
val playersHands: PlayersHands = PlayersHands(),
|
val playersHands: PlayersHands = PlayersHands(),
|
||||||
) {
|
) {
|
||||||
constructor(players: Set<Player>) :
|
constructor(players: Set<Player>) :
|
||||||
this(playersHands = PlayersHands(players))
|
this(playersHands = PlayersHands(players))
|
||||||
|
|
||||||
fun shuffle(): Deck = copy(stack = stack.shuffle())
|
fun shuffle(): Deck =
|
||||||
|
copy(stack = stack.shuffle())
|
||||||
|
|
||||||
fun placeFirstCardOnDiscard(): Deck {
|
fun placeFirstCardOnDiscard(): Deck {
|
||||||
val takenCard = stack.first()
|
val takenCard = stack.first()
|
||||||
return copy(
|
return copy(
|
||||||
stack = stack - takenCard,
|
stack = stack - takenCard,
|
||||||
discard = discard + takenCard,
|
discard = discard + takenCard,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun takeOneCardFromStackTo(player: Player): Deck =
|
||||||
|
takeOne().let { (deck, newPlayerCard) ->
|
||||||
|
deck.copy(
|
||||||
|
playersHands = deck.playersHands.addCard(player, newPlayerCard),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun takeOneCardFromStackTo(player: Player): Deck =
|
fun putOneCardFromHand(
|
||||||
takeOne().let { (deck, newPlayerCard) ->
|
player: Player,
|
||||||
deck.copy(
|
card: Card,
|
||||||
playersHands = deck.playersHands.addCard(player, newPlayerCard),
|
): Deck =
|
||||||
)
|
run {
|
||||||
}
|
// Validate parameters
|
||||||
|
val playerHand =
|
||||||
|
playersHands.getHand(player)
|
||||||
|
?: error("No player on this game")
|
||||||
|
if (playerHand.none { it == card }) {
|
||||||
|
error("No card exist on the player hand")
|
||||||
|
}
|
||||||
|
}.let {
|
||||||
|
copy(
|
||||||
|
discard = discard + card,
|
||||||
|
playersHands = playersHands.removeCard(player, card),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun putOneCardFromHand(
|
fun playerHasNoCardLeft(): List<Player.PlayerId> =
|
||||||
player: Player,
|
playersHands
|
||||||
card: Card,
|
.filter { (playerId, hand) -> hand.isEmpty() }
|
||||||
): Deck =
|
.map { (playerId, hand) -> playerId }
|
||||||
run {
|
|
||||||
// Validate parameters
|
private fun take(n: Int): Pair<Deck, List<Card>> {
|
||||||
val playerHand =
|
val takenCards = stack.take(n)
|
||||||
playersHands.getHand(player)
|
val newStack = stack.filterNot { takenCards.contains(it) }.toStack()
|
||||||
?: error("No player on this game")
|
return Pair(copy(stack = newStack), takenCards)
|
||||||
if (playerHand.none { it == card }) {
|
}
|
||||||
error("No card exist on the player hand")
|
|
||||||
}
|
private fun takeOne(): Pair<Deck, Card> =
|
||||||
|
take(1).let { (deck, cards) -> Pair(deck, cards.first()) }
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newWithoutPlayers(): 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 {
|
}.let {
|
||||||
copy(
|
it + (1..4).map { Card.Plus4Card() }
|
||||||
discard = discard + card,
|
}.toStack()
|
||||||
playersHands = playersHands.removeCard(player, card),
|
.let { Deck(it) }
|
||||||
)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun playerHasNoCardLeft(): List<Player.PlayerId> =
|
|
||||||
playersHands
|
|
||||||
.filter { (playerId, hand) -> hand.isEmpty() }
|
|
||||||
.map { (playerId, hand) -> playerId }
|
|
||||||
|
|
||||||
private fun take(n: Int): Pair<Deck, List<Card>> {
|
|
||||||
val takenCards = stack.take(n)
|
|
||||||
val newStack = stack.filterNot { takenCards.contains(it) }.toStack()
|
|
||||||
return Pair(copy(stack = newStack), takenCards)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun takeOne(): Pair<Deck, Card> = take(1).let { (deck, cards) -> Pair(deck, cards.first()) }
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun newWithoutPlayers(): 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 {
|
|
||||||
it + (1..4).map { Card.Plus4Card() }
|
|
||||||
}.toStack()
|
|
||||||
.let { Deck(it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Deck.initHands(
|
fun Deck.initHands(
|
||||||
players: Set<Player>,
|
players: Set<Player>,
|
||||||
handSize: Int = 7,
|
handSize: Int = 7,
|
||||||
): Deck {
|
): Deck {
|
||||||
// Copy cards from stack to the player hands
|
// Copy cards from stack to the player hands
|
||||||
val deckWithEmptyHands = copy(playersHands = PlayersHands(players))
|
val deckWithEmptyHands = copy(playersHands = PlayersHands(players))
|
||||||
return players.fold(deckWithEmptyHands) { acc: Deck, player: Player ->
|
return players.fold(deckWithEmptyHands) { acc: Deck, player: Player ->
|
||||||
val hand = acc.stack.take(handSize)
|
val hand = acc.stack.take(handSize)
|
||||||
val newStack = acc.stack.filterNot { card: Card -> hand.contains(card) }.toStack()
|
val newStack = acc.stack.filterNot { card: Card -> hand.contains(card) }.toStack()
|
||||||
copy(
|
copy(
|
||||||
stack = newStack,
|
stack = newStack,
|
||||||
playersHands = acc.playersHands.addCards(player, hand),
|
playersHands = acc.playersHands.addCards(player, hand),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmInline
|
@JvmInline
|
||||||
@Serializable
|
@Serializable
|
||||||
value class Stack(
|
value class Stack(
|
||||||
private val cards: Set<Card> = emptySet(),
|
private val cards: Set<Card> = emptySet(),
|
||||||
) : Set<Card> by cards {
|
) : Set<Card> by cards {
|
||||||
operator fun plus(card: Card): Stack = cards.plus(card).toStack()
|
operator fun plus(card: Card): Stack =
|
||||||
|
cards.plus(card).toStack()
|
||||||
|
|
||||||
operator fun minus(card: Card): Stack = cards.minus(card).toStack()
|
operator fun minus(card: Card): Stack =
|
||||||
|
cards.minus(card).toStack()
|
||||||
|
|
||||||
fun shuffle(): Stack = shuffled().toStack()
|
fun shuffle(): Stack =
|
||||||
|
shuffled().toStack()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun List<Card>.toStack(): Stack = Stack(this.toSet())
|
fun List<Card>.toStack(): Stack =
|
||||||
|
Stack(this.toSet())
|
||||||
|
|
||||||
fun Set<Card>.toStack(): Stack = Stack(this)
|
fun Set<Card>.toStack(): Stack =
|
||||||
|
Stack(this)
|
||||||
|
|
||||||
@JvmInline
|
@JvmInline
|
||||||
@Serializable
|
@Serializable
|
||||||
value class Discard(
|
value class Discard(
|
||||||
private val cards: Set<Card> = emptySet(),
|
private val cards: Set<Card> = emptySet(),
|
||||||
) : Set<Card> by cards {
|
) : Set<Card> by cards {
|
||||||
operator fun plus(card: Card): Discard = cards.plus(card).toDiscard()
|
operator fun plus(card: Card): Discard =
|
||||||
|
cards.plus(card).toDiscard()
|
||||||
|
|
||||||
operator fun minus(card: Card): Discard = cards.minus(card).toDiscard()
|
operator fun minus(card: Card): Discard =
|
||||||
|
cards.minus(card).toDiscard()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun List<Card>.toDiscard(): Discard = Discard(this.toSet())
|
fun List<Card>.toDiscard(): Discard =
|
||||||
|
Discard(this.toSet())
|
||||||
|
|
||||||
fun Set<Card>.toDiscard(): Discard = Discard(this)
|
fun Set<Card>.toDiscard(): Discard =
|
||||||
|
Discard(this)
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import java.util.UUID
|
|||||||
@JvmInline
|
@JvmInline
|
||||||
@Serializable(with = GameIdSerializer::class)
|
@Serializable(with = GameIdSerializer::class)
|
||||||
value class GameId(
|
value class GameId(
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
) : AggregateId {
|
) : AggregateId {
|
||||||
override fun toString(): String = id.toString()
|
override fun toString(): String =
|
||||||
|
id.toString()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,21 +8,22 @@ import java.util.UUID
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Player(
|
data class Player(
|
||||||
val name: String,
|
val name: String,
|
||||||
@Serializable(with = PlayerIdSerializer::class)
|
@Serializable(with = PlayerIdSerializer::class)
|
||||||
val id: PlayerId = PlayerId(UUID.randomUUID()),
|
val id: PlayerId = PlayerId(UUID.randomUUID()),
|
||||||
) {
|
) {
|
||||||
constructor(id: String, name: String) : this(
|
constructor(id: String, name: String) : this(
|
||||||
name,
|
name,
|
||||||
PlayerId(UUID.fromString(id)),
|
PlayerId(UUID.fromString(id)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@JvmInline
|
@JvmInline
|
||||||
value class PlayerId(
|
value class PlayerId(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
) : AggregateId {
|
) : AggregateId {
|
||||||
override fun toString(): String = id.toString()
|
override fun toString(): String =
|
||||||
}
|
id.toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,43 +5,46 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
@JvmInline
|
@JvmInline
|
||||||
value class PlayersHands(
|
value class PlayersHands(
|
||||||
private val map: Map<Player.PlayerId, List<Card>> = emptyMap(),
|
private val map: Map<Player.PlayerId, List<Card>> = emptyMap(),
|
||||||
) : Map<Player.PlayerId, List<Card>> by map {
|
) : Map<Player.PlayerId, List<Card>> by map {
|
||||||
constructor(players: Set<Player>) :
|
constructor(players: Set<Player>) :
|
||||||
this(players.map { it.id }.associateWith { emptyList<Card>() }.toPlayersHands())
|
this(players.map { it.id }.associateWith { emptyList<Card>() }.toPlayersHands())
|
||||||
|
|
||||||
fun getHand(player: Player): List<Card>? = this[player.id]
|
fun getHand(player: Player): List<Card>? =
|
||||||
|
this[player.id]
|
||||||
|
|
||||||
fun removeCard(
|
fun removeCard(
|
||||||
player: Player,
|
player: Player,
|
||||||
card: Card,
|
card: Card,
|
||||||
): PlayersHands =
|
): PlayersHands =
|
||||||
mapValues { (playerId, cards) ->
|
mapValues { (playerId, cards) ->
|
||||||
if (playerId == player.id) {
|
if (playerId == player.id) {
|
||||||
if (!cards.contains(card)) error("The hand no contain the card")
|
if (!cards.contains(card)) error("The hand no contain the card")
|
||||||
cards - card
|
cards - card
|
||||||
} else {
|
} else {
|
||||||
cards
|
cards
|
||||||
}
|
}
|
||||||
}.toPlayersHands()
|
}.toPlayersHands()
|
||||||
|
|
||||||
fun addCard(
|
fun addCard(
|
||||||
player: Player,
|
player: Player,
|
||||||
newCard: Card,
|
newCard: Card,
|
||||||
): PlayersHands = addCards(player, listOf(newCard))
|
): PlayersHands =
|
||||||
|
addCards(player, listOf(newCard))
|
||||||
|
|
||||||
fun addCards(
|
fun addCards(
|
||||||
player: Player,
|
player: Player,
|
||||||
newCards: List<Card>,
|
newCards: List<Card>,
|
||||||
): PlayersHands =
|
): PlayersHands =
|
||||||
mapValues { (p, cards) ->
|
mapValues { (p, cards) ->
|
||||||
if (p == player.id) {
|
if (p == player.id) {
|
||||||
if (cards.intersect(newCards).isNotEmpty()) error("The hand already contain the card")
|
if (cards.intersect(newCards).isNotEmpty()) error("The hand already contain the card")
|
||||||
cards + newCards
|
cards + newCards
|
||||||
} else {
|
} else {
|
||||||
cards
|
cards
|
||||||
}
|
}
|
||||||
}.toPlayersHands()
|
}.toPlayersHands()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Map<Player.PlayerId, List<Card>>.toPlayersHands(): PlayersHands = PlayersHands(this)
|
fun Map<Player.PlayerId, List<Card>>.toPlayersHands(): PlayersHands =
|
||||||
|
PlayersHands(this)
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import eventDemo.libs.event.Event
|
|||||||
* A stream to publish and read the played card event.
|
* A stream to publish and read the played card event.
|
||||||
*/
|
*/
|
||||||
interface EventHandler<E : Event<ID>, ID : AggregateId> {
|
interface EventHandler<E : Event<ID>, ID : AggregateId> {
|
||||||
fun registerProjectionBuilder(builder: (E) -> Unit)
|
fun registerProjectionBuilder(builder: (E) -> Unit)
|
||||||
|
|
||||||
fun handle(
|
fun handle(
|
||||||
aggregateId: ID,
|
aggregateId: ID,
|
||||||
buildEvent: (version: Int) -> E,
|
buildEvent: (version: Int) -> E,
|
||||||
): E
|
): E
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ import eventDemo.app.event.event.GameEvent
|
|||||||
import eventDemo.libs.event.EventBus
|
import eventDemo.libs.event.EventBus
|
||||||
|
|
||||||
class GameEventBus(
|
class GameEventBus(
|
||||||
bus: EventBus<GameEvent, GameId>,
|
bus: EventBus<GameEvent, GameId>,
|
||||||
) : EventBus<GameEvent, GameId> by bus
|
) : EventBus<GameEvent, GameId> by bus
|
||||||
|
|||||||
@@ -12,30 +12,30 @@ import kotlin.concurrent.withLock
|
|||||||
* A stream to publish and read the played card event.
|
* A stream to publish and read the played card event.
|
||||||
*/
|
*/
|
||||||
class GameEventHandler(
|
class GameEventHandler(
|
||||||
private val eventBus: GameEventBus,
|
private val eventBus: GameEventBus,
|
||||||
private val eventStore: GameEventStore,
|
private val eventStore: GameEventStore,
|
||||||
private val versionBuilder: VersionBuilder,
|
private val versionBuilder: VersionBuilder,
|
||||||
) : EventHandler<GameEvent, GameId> {
|
) : EventHandler<GameEvent, GameId> {
|
||||||
private val projectionsBuilders: ConcurrentLinkedQueue<(GameEvent) -> Unit> = ConcurrentLinkedQueue()
|
private val projectionsBuilders: ConcurrentLinkedQueue<(GameEvent) -> Unit> = ConcurrentLinkedQueue()
|
||||||
private val locks: ConcurrentHashMap<GameId, ReentrantLock> = ConcurrentHashMap()
|
private val locks: ConcurrentHashMap<GameId, ReentrantLock> = ConcurrentHashMap()
|
||||||
|
|
||||||
override fun registerProjectionBuilder(builder: GameProjectionBuilder) {
|
override fun registerProjectionBuilder(builder: GameProjectionBuilder) {
|
||||||
projectionsBuilders.add(builder)
|
projectionsBuilders.add(builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(
|
override fun handle(
|
||||||
aggregateId: GameId,
|
aggregateId: GameId,
|
||||||
buildEvent: (version: Int) -> GameEvent,
|
buildEvent: (version: Int) -> GameEvent,
|
||||||
): GameEvent =
|
): GameEvent =
|
||||||
locks
|
locks
|
||||||
.computeIfAbsent(aggregateId) { ReentrantLock() }
|
.computeIfAbsent(aggregateId) { ReentrantLock() }
|
||||||
.withLock {
|
.withLock {
|
||||||
buildEvent(versionBuilder.buildNextVersion(aggregateId))
|
buildEvent(versionBuilder.buildNextVersion(aggregateId))
|
||||||
.also { eventStore.publish(it) }
|
.also { eventStore.publish(it) }
|
||||||
}.also { event ->
|
}.also { event ->
|
||||||
projectionsBuilders.forEach { it(event) }
|
projectionsBuilders.forEach { it(event) }
|
||||||
eventBus.publish(event)
|
eventBus.publish(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias GameProjectionBuilder = (GameEvent) -> Unit
|
typealias GameProjectionBuilder = (GameEvent) -> Unit
|
||||||
|
|||||||
@@ -8,5 +8,5 @@ import eventDemo.libs.event.EventStore
|
|||||||
* A stream to publish and read the played card event.
|
* A stream to publish and read the played card event.
|
||||||
*/
|
*/
|
||||||
class GameEventStore(
|
class GameEventStore(
|
||||||
private val eventStore: EventStore<GameEvent, GameId>,
|
private val eventStore: EventStore<GameEvent, GameId>,
|
||||||
) : EventStore<GameEvent, GameId> by eventStore
|
) : EventStore<GameEvent, GameId> by eventStore
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import eventDemo.libs.event.EventStream
|
|||||||
* A stream to publish and read the played card event.
|
* A stream to publish and read the played card event.
|
||||||
*/
|
*/
|
||||||
class GameEventStream(
|
class GameEventStream(
|
||||||
private val eventStream: EventStream<GameEvent>,
|
private val eventStream: EventStream<GameEvent>,
|
||||||
) : EventStream<GameEvent> by eventStream {
|
) : EventStream<GameEvent> by eventStream {
|
||||||
override fun publish(event: GameEvent) {
|
override fun publish(event: GameEvent) {
|
||||||
eventStream.publish(event)
|
eventStream.publish(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import java.util.UUID
|
|||||||
* An [GameEvent] to represent a played card.
|
* An [GameEvent] to represent a played card.
|
||||||
*/
|
*/
|
||||||
data class CardIsPlayedEvent(
|
data class CardIsPlayedEvent(
|
||||||
override val aggregateId: GameId,
|
override val aggregateId: GameId,
|
||||||
val card: Card,
|
val card: Card,
|
||||||
override val player: Player,
|
override val player: Player,
|
||||||
override val version: Int,
|
override val version: Int,
|
||||||
) : GameEvent,
|
) : GameEvent,
|
||||||
PlayerActionEvent {
|
PlayerActionEvent {
|
||||||
override val eventId: UUID = UUID.randomUUID()
|
override val eventId: UUID = UUID.randomUUID()
|
||||||
override val createdAt: Instant = Clock.System.now()
|
override val createdAt: Instant = Clock.System.now()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import java.util.UUID
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed interface GameEvent : Event<GameId> {
|
sealed interface GameEvent : Event<GameId> {
|
||||||
override val eventId: UUID
|
override val eventId: UUID
|
||||||
override val aggregateId: GameId
|
override val aggregateId: GameId
|
||||||
override val version: Int
|
override val version: Int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,37 +12,37 @@ import java.util.UUID
|
|||||||
* This [GameEvent] is sent when all players are ready.
|
* This [GameEvent] is sent when all players are ready.
|
||||||
*/
|
*/
|
||||||
data class GameStartedEvent(
|
data class GameStartedEvent(
|
||||||
override val aggregateId: GameId,
|
override val aggregateId: GameId,
|
||||||
val firstPlayer: Player,
|
val firstPlayer: Player,
|
||||||
val deck: Deck,
|
val deck: Deck,
|
||||||
override val version: Int,
|
override val version: Int,
|
||||||
) : GameEvent {
|
) : GameEvent {
|
||||||
override val eventId: UUID = UUID.randomUUID()
|
override val eventId: UUID = UUID.randomUUID()
|
||||||
override val createdAt: Instant = Clock.System.now()
|
override val createdAt: Instant = Clock.System.now()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun new(
|
fun new(
|
||||||
id: GameId,
|
id: GameId,
|
||||||
players: Set<Player>,
|
players: Set<Player>,
|
||||||
shuffleIsDisabled: Boolean = isDisabled,
|
shuffleIsDisabled: Boolean = isDisabled,
|
||||||
version: Int,
|
version: Int,
|
||||||
): GameStartedEvent =
|
): GameStartedEvent =
|
||||||
GameStartedEvent(
|
GameStartedEvent(
|
||||||
aggregateId = id,
|
aggregateId = id,
|
||||||
firstPlayer = if (shuffleIsDisabled) players.first() else players.random(),
|
firstPlayer = if (shuffleIsDisabled) players.first() else players.random(),
|
||||||
deck =
|
deck =
|
||||||
Deck
|
Deck
|
||||||
.newWithoutPlayers()
|
.newWithoutPlayers()
|
||||||
.let { if (shuffleIsDisabled) it else it.shuffle() }
|
.let { if (shuffleIsDisabled) it else it.shuffle() }
|
||||||
.initHands(players)
|
.initHands(players)
|
||||||
.placeFirstCardOnDiscard(),
|
.placeFirstCardOnDiscard(),
|
||||||
version = version,
|
version = version,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isDisabled = false
|
private var isDisabled = false
|
||||||
|
|
||||||
internal fun disableShuffleDeck() {
|
internal fun disableShuffleDeck() {
|
||||||
isDisabled = true
|
isDisabled = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import java.util.UUID
|
|||||||
* An [GameEvent] to represent a new player joining the game.
|
* An [GameEvent] to represent a new player joining the game.
|
||||||
*/
|
*/
|
||||||
data class NewPlayerEvent(
|
data class NewPlayerEvent(
|
||||||
override val aggregateId: GameId,
|
override val aggregateId: GameId,
|
||||||
val player: Player,
|
val player: Player,
|
||||||
override val version: Int,
|
override val version: Int,
|
||||||
) : GameEvent {
|
) : GameEvent {
|
||||||
override val eventId: UUID = UUID.randomUUID()
|
override val eventId: UUID = UUID.randomUUID()
|
||||||
override val createdAt: Instant = Clock.System.now()
|
override val createdAt: Instant = Clock.System.now()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ package eventDemo.app.event.event
|
|||||||
import eventDemo.app.entity.Player
|
import eventDemo.app.entity.Player
|
||||||
|
|
||||||
sealed interface PlayerActionEvent : GameEvent {
|
sealed interface PlayerActionEvent : GameEvent {
|
||||||
val player: Player
|
val player: Player
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import java.util.UUID
|
|||||||
* This [GameEvent] is sent when a player chose a color.
|
* This [GameEvent] is sent when a player chose a color.
|
||||||
*/
|
*/
|
||||||
data class PlayerChoseColorEvent(
|
data class PlayerChoseColorEvent(
|
||||||
override val aggregateId: GameId,
|
override val aggregateId: GameId,
|
||||||
override val player: Player,
|
override val player: Player,
|
||||||
val color: Card.Color,
|
val color: Card.Color,
|
||||||
override val version: Int,
|
override val version: Int,
|
||||||
) : GameEvent,
|
) : GameEvent,
|
||||||
PlayerActionEvent {
|
PlayerActionEvent {
|
||||||
override val eventId: UUID = UUID.randomUUID()
|
override val eventId: UUID = UUID.randomUUID()
|
||||||
override val createdAt: Instant = Clock.System.now()
|
override val createdAt: Instant = Clock.System.now()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import java.util.UUID
|
|||||||
* This [GameEvent] is sent when a player can play.
|
* This [GameEvent] is sent when a player can play.
|
||||||
*/
|
*/
|
||||||
data class PlayerHavePassEvent(
|
data class PlayerHavePassEvent(
|
||||||
override val aggregateId: GameId,
|
override val aggregateId: GameId,
|
||||||
override val player: Player,
|
override val player: Player,
|
||||||
val takenCard: Card,
|
val takenCard: Card,
|
||||||
override val version: Int,
|
override val version: Int,
|
||||||
) : GameEvent,
|
) : GameEvent,
|
||||||
PlayerActionEvent {
|
PlayerActionEvent {
|
||||||
override val eventId: UUID = UUID.randomUUID()
|
override val eventId: UUID = UUID.randomUUID()
|
||||||
override val createdAt: Instant = Clock.System.now()
|
override val createdAt: Instant = Clock.System.now()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import java.util.UUID
|
|||||||
* This [GameEvent] is sent when a player is ready.
|
* This [GameEvent] is sent when a player is ready.
|
||||||
*/
|
*/
|
||||||
data class PlayerReadyEvent(
|
data class PlayerReadyEvent(
|
||||||
override val aggregateId: GameId,
|
override val aggregateId: GameId,
|
||||||
val player: Player,
|
val player: Player,
|
||||||
override val version: Int,
|
override val version: Int,
|
||||||
) : GameEvent {
|
) : GameEvent {
|
||||||
override val eventId: UUID = UUID.randomUUID()
|
override val eventId: UUID = UUID.randomUUID()
|
||||||
override val createdAt: Instant = Clock.System.now()
|
override val createdAt: Instant = Clock.System.now()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import java.util.UUID
|
|||||||
* This [GameEvent] is sent when a player is ready.
|
* This [GameEvent] is sent when a player is ready.
|
||||||
*/
|
*/
|
||||||
data class PlayerWinEvent(
|
data class PlayerWinEvent(
|
||||||
override val aggregateId: GameId,
|
override val aggregateId: GameId,
|
||||||
val player: Player,
|
val player: Player,
|
||||||
override val version: Int,
|
override val version: Int,
|
||||||
) : GameEvent {
|
) : GameEvent {
|
||||||
override val eventId: UUID = UUID.randomUUID()
|
override val eventId: UUID = UUID.randomUUID()
|
||||||
override val createdAt: Instant = Clock.System.now()
|
override val createdAt: Instant = Clock.System.now()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,173 +8,174 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GameState(
|
data class GameState(
|
||||||
override val aggregateId: GameId,
|
override val aggregateId: GameId,
|
||||||
override val lastEventVersion: Int = 0,
|
override val lastEventVersion: Int = 0,
|
||||||
val players: Set<Player> = emptySet(),
|
val players: Set<Player> = emptySet(),
|
||||||
val currentPlayerTurn: Player? = null,
|
val currentPlayerTurn: Player? = null,
|
||||||
val cardOnCurrentStack: LastCard? = null,
|
val cardOnCurrentStack: LastCard? = null,
|
||||||
val colorOnCurrentStack: Card.Color? = null,
|
val colorOnCurrentStack: Card.Color? = null,
|
||||||
val direction: Direction = Direction.CLOCKWISE,
|
val direction: Direction = Direction.CLOCKWISE,
|
||||||
val readyPlayers: Set<Player> = emptySet(),
|
val readyPlayers: Set<Player> = emptySet(),
|
||||||
val deck: Deck = Deck(players),
|
val deck: Deck = Deck(players),
|
||||||
val isStarted: Boolean = false,
|
val isStarted: Boolean = false,
|
||||||
val playerWins: Set<Player> = emptySet(),
|
val playerWins: Set<Player> = emptySet(),
|
||||||
) : Projection<GameId> {
|
) : Projection<GameId> {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LastCard(
|
data class LastCard(
|
||||||
val card: Card,
|
val card: Card,
|
||||||
val player: Player,
|
val player: Player,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class Direction {
|
enum class Direction {
|
||||||
CLOCKWISE,
|
CLOCKWISE,
|
||||||
COUNTER_CLOCKWISE,
|
COUNTER_CLOCKWISE,
|
||||||
;
|
;
|
||||||
|
|
||||||
fun revert(): Direction =
|
fun revert(): Direction =
|
||||||
if (this === CLOCKWISE) {
|
if (this === CLOCKWISE) {
|
||||||
COUNTER_CLOCKWISE
|
COUNTER_CLOCKWISE
|
||||||
} else {
|
} else {
|
||||||
CLOCKWISE
|
CLOCKWISE
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isReady: Boolean get() {
|
||||||
|
return players.size == readyPlayers.size && players.all { readyPlayers.contains(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val currentPlayerIndex: Int? get() {
|
||||||
|
val i = players.indexOf(currentPlayerTurn)
|
||||||
|
return if (i == -1) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nextPlayerIndex(direction: Direction): Int {
|
||||||
|
if (players.isEmpty()) return 0
|
||||||
|
|
||||||
|
return if (direction == Direction.CLOCKWISE) {
|
||||||
|
sidePlayerIndexClockwise
|
||||||
|
} else {
|
||||||
|
sidePlayerIndexCounterClockwise
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
nextPlayer.currentIndex + this.currentIndex
|
||||||
|
} else {
|
||||||
|
nextPlayer.currentIndex - this.currentIndex
|
||||||
|
}.let { it % players.size }
|
||||||
|
|
||||||
|
val Player.cardOnBoardIsForYou: Boolean get() {
|
||||||
|
if (cardOnCurrentStack == null) error("No card")
|
||||||
|
return this.playerDiffIndex(cardOnCurrentStack.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")
|
||||||
}
|
}
|
||||||
|
|
||||||
val isReady: Boolean get() {
|
fun canBePlayThisCard(
|
||||||
return players.size == readyPlayers.size && players.all { readyPlayers.contains(it) }
|
player: Player,
|
||||||
}
|
card: Card,
|
||||||
|
): Boolean {
|
||||||
|
val cardOnBoard = cardOnCurrentStack?.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val currentPlayerIndex: Int? get() {
|
is Card.ReverseCard -> {
|
||||||
val i = players.indexOf(currentPlayerTurn)
|
when (card) {
|
||||||
return if (i == -1) {
|
is Card.ReverseCard -> true
|
||||||
null
|
is Card.AllColorCard -> true
|
||||||
|
is Card.ColorCard -> card.color == cardOnBoard.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is Card.PassCard -> {
|
||||||
|
if (player.cardOnBoardIsForYou) {
|
||||||
|
false
|
||||||
} else {
|
} else {
|
||||||
i
|
when (card) {
|
||||||
|
is Card.AllColorCard -> true
|
||||||
|
is Card.ColorCard -> card.color == cardOnBoard.color
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun nextPlayerIndex(direction: Direction): Int {
|
is Card.ChangeColorCard -> {
|
||||||
if (players.isEmpty()) return 0
|
when (card) {
|
||||||
|
is Card.AllColorCard -> true
|
||||||
|
is Card.ColorCard -> card.color == colorOnCurrentStack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return if (direction == Direction.CLOCKWISE) {
|
is Card.Plus2Card -> {
|
||||||
sidePlayerIndexClockwise
|
if (player.cardOnBoardIsForYou && card is Card.Plus2Card) {
|
||||||
|
true
|
||||||
} else {
|
} else {
|
||||||
sidePlayerIndexCounterClockwise
|
when (card) {
|
||||||
|
is Card.AllColorCard -> true
|
||||||
|
is Card.Plus2Card -> true
|
||||||
|
is Card.ColorCard -> card.color == cardOnBoard.color
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun nextPlayer(direction: Direction): Player = players.elementAt(nextPlayerIndex(direction))
|
is Card.Plus4Card -> {
|
||||||
|
if (player.cardOnBoardIsForYou && card is Card.Plus4Card) {
|
||||||
private val sidePlayerIndexClockwise: Int by lazy {
|
true
|
||||||
if (players.isEmpty()) {
|
|
||||||
0
|
|
||||||
} else {
|
} else {
|
||||||
((currentPlayerIndex ?: 0) + 1) % players.size
|
when (card) {
|
||||||
}
|
is Card.AllColorCard -> true
|
||||||
}
|
is Card.ColorCard -> card.color == colorOnCurrentStack
|
||||||
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) {
|
|
||||||
nextPlayer.currentIndex + this.currentIndex
|
|
||||||
} else {
|
|
||||||
nextPlayer.currentIndex - this.currentIndex
|
|
||||||
}.let { it % players.size }
|
|
||||||
|
|
||||||
val Player.cardOnBoardIsForYou: Boolean get() {
|
|
||||||
if (cardOnCurrentStack == null) error("No card")
|
|
||||||
return this.playerDiffIndex(cardOnCurrentStack.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 = cardOnCurrentStack?.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 == colorOnCurrentStack
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 == colorOnCurrentStack
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,103 +13,103 @@ import eventDemo.app.event.event.PlayerWinEvent
|
|||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
|
||||||
fun GameState.apply(event: GameEvent): GameState =
|
fun GameState.apply(event: GameEvent): GameState =
|
||||||
this.let { state ->
|
this.let { state ->
|
||||||
val logger = KotlinLogging.logger { }
|
val logger = KotlinLogging.logger { }
|
||||||
if (event is PlayerActionEvent) {
|
if (event is PlayerActionEvent) {
|
||||||
if (state.currentPlayerTurn != event.player) {
|
if (state.currentPlayerTurn != event.player) {
|
||||||
logger.atError {
|
logger.atError {
|
||||||
message = "Inconsistent player turn. CurrentPlayerTurn: $state.currentPlayerTurn | Player: ${event.player}"
|
message = "Inconsistent player turn. CurrentPlayerTurn: $state.currentPlayerTurn | Player: ${event.player}"
|
||||||
payload =
|
payload =
|
||||||
mapOf(
|
mapOf(
|
||||||
"CurrentPlayerTurn" to (state.currentPlayerTurn ?: "No currentPlayerTurn"),
|
"CurrentPlayerTurn" to (state.currentPlayerTurn ?: "No currentPlayerTurn"),
|
||||||
"Player" to event.player,
|
"Player" to event.player,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (event) {
|
||||||
|
is CardIsPlayedEvent -> {
|
||||||
|
val nextDirectionAfterPlay =
|
||||||
|
when (event.card) {
|
||||||
|
is Card.ReverseCard -> state.direction.revert()
|
||||||
|
else -> state.direction
|
||||||
|
}
|
||||||
|
|
||||||
|
val color =
|
||||||
|
when (event.card) {
|
||||||
|
is Card.ColorCard -> event.card.color
|
||||||
|
is Card.AllColorCard -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentPlayerAfterThePlay =
|
||||||
|
if (event.card is Card.AllColorCard) {
|
||||||
|
state.currentPlayerTurn
|
||||||
|
} else {
|
||||||
|
state.nextPlayer(nextDirectionAfterPlay)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.copy(
|
||||||
|
currentPlayerTurn = currentPlayerAfterThePlay,
|
||||||
|
direction = nextDirectionAfterPlay,
|
||||||
|
colorOnCurrentStack = color,
|
||||||
|
cardOnCurrentStack = GameState.LastCard(event.card, event.player),
|
||||||
|
deck = state.deck.putOneCardFromHand(event.player, event.card),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is NewPlayerEvent -> {
|
||||||
|
if (state.isStarted) {
|
||||||
|
logger.error { "The game is already started" }
|
||||||
}
|
}
|
||||||
|
|
||||||
when (event) {
|
state.copy(
|
||||||
is CardIsPlayedEvent -> {
|
players = state.players + event.player,
|
||||||
val nextDirectionAfterPlay =
|
|
||||||
when (event.card) {
|
|
||||||
is Card.ReverseCard -> state.direction.revert()
|
|
||||||
else -> state.direction
|
|
||||||
}
|
|
||||||
|
|
||||||
val color =
|
|
||||||
when (event.card) {
|
|
||||||
is Card.ColorCard -> event.card.color
|
|
||||||
is Card.AllColorCard -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentPlayerAfterThePlay =
|
|
||||||
if (event.card is Card.AllColorCard) {
|
|
||||||
state.currentPlayerTurn
|
|
||||||
} else {
|
|
||||||
state.nextPlayer(nextDirectionAfterPlay)
|
|
||||||
}
|
|
||||||
|
|
||||||
state.copy(
|
|
||||||
currentPlayerTurn = currentPlayerAfterThePlay,
|
|
||||||
direction = nextDirectionAfterPlay,
|
|
||||||
colorOnCurrentStack = color,
|
|
||||||
cardOnCurrentStack = GameState.LastCard(event.card, event.player),
|
|
||||||
deck = state.deck.putOneCardFromHand(event.player, event.card),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is NewPlayerEvent -> {
|
|
||||||
if (state.isStarted) {
|
|
||||||
logger.error { "The game is already started" }
|
|
||||||
}
|
|
||||||
|
|
||||||
state.copy(
|
|
||||||
players = state.players + event.player,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is PlayerReadyEvent -> {
|
|
||||||
if (state.isStarted) {
|
|
||||||
logger.error { "The game is already started" }
|
|
||||||
}
|
|
||||||
state.copy(
|
|
||||||
readyPlayers = state.readyPlayers + event.player,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is PlayerHavePassEvent -> {
|
|
||||||
if (event.takenCard != state.deck.stack.first()) {
|
|
||||||
logger.error { "taken card is not ot top of the stack: ${event.takenCard}" }
|
|
||||||
}
|
|
||||||
state.copy(
|
|
||||||
currentPlayerTurn = state.nextPlayerTurn,
|
|
||||||
deck = state.deck.takeOneCardFromStackTo(event.player),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is PlayerChoseColorEvent -> {
|
|
||||||
state.copy(
|
|
||||||
currentPlayerTurn = state.nextPlayerTurn,
|
|
||||||
colorOnCurrentStack = event.color,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is GameStartedEvent -> {
|
|
||||||
state.copy(
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is PlayerWinEvent -> {
|
|
||||||
state.copy(
|
|
||||||
playerWins = state.playerWins + event.player,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.copy(
|
|
||||||
lastEventVersion = event.version,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is PlayerReadyEvent -> {
|
||||||
|
if (state.isStarted) {
|
||||||
|
logger.error { "The game is already started" }
|
||||||
|
}
|
||||||
|
state.copy(
|
||||||
|
readyPlayers = state.readyPlayers + event.player,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerHavePassEvent -> {
|
||||||
|
if (event.takenCard != state.deck.stack.first()) {
|
||||||
|
logger.error { "taken card is not ot top of the stack: ${event.takenCard}" }
|
||||||
|
}
|
||||||
|
state.copy(
|
||||||
|
currentPlayerTurn = state.nextPlayerTurn,
|
||||||
|
deck = state.deck.takeOneCardFromStackTo(event.player),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerChoseColorEvent -> {
|
||||||
|
state.copy(
|
||||||
|
currentPlayerTurn = state.nextPlayerTurn,
|
||||||
|
colorOnCurrentStack = event.color,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is GameStartedEvent -> {
|
||||||
|
state.copy(
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerWinEvent -> {
|
||||||
|
state.copy(
|
||||||
|
playerWins = state.playerWins + event.player,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.copy(
|
||||||
|
lastEventVersion = event.version,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,36 +6,38 @@ import eventDemo.app.event.GameEventStore
|
|||||||
import eventDemo.app.event.event.GameEvent
|
import eventDemo.app.event.event.GameEvent
|
||||||
|
|
||||||
class GameStateRepository(
|
class GameStateRepository(
|
||||||
eventStore: GameEventStore,
|
eventStore: GameEventStore,
|
||||||
eventHandler: GameEventHandler,
|
eventHandler: GameEventHandler,
|
||||||
snapshotConfig: SnapshotConfig = SnapshotConfig(),
|
snapshotConfig: SnapshotConfig = SnapshotConfig(),
|
||||||
) {
|
) {
|
||||||
private val projectionsSnapshot =
|
private val projectionsSnapshot =
|
||||||
ProjectionSnapshotRepositoryInMemory(
|
ProjectionSnapshotRepositoryInMemory(
|
||||||
eventStore = eventStore,
|
eventStore = eventStore,
|
||||||
snapshotCacheConfig = snapshotConfig,
|
snapshotCacheConfig = snapshotConfig,
|
||||||
applyToProjection = GameState::apply,
|
applyToProjection = GameState::apply,
|
||||||
initialStateBuilder = { aggregateId: GameId -> GameState(aggregateId) },
|
initialStateBuilder = { aggregateId: GameId -> GameState(aggregateId) },
|
||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
eventHandler.registerProjectionBuilder { event ->
|
eventHandler.registerProjectionBuilder { event ->
|
||||||
projectionsSnapshot.applyAndPutToCache(event)
|
projectionsSnapshot.applyAndPutToCache(event)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the last version of the [GameState] from the all eventStream.
|
* Get the last version of the [GameState] from the all eventStream.
|
||||||
*
|
*
|
||||||
* It fetches it from the local cache if possible, otherwise it builds it.
|
* It fetches it from the local cache if possible, otherwise it builds it.
|
||||||
*/
|
*/
|
||||||
fun getLast(gameId: GameId): GameState = projectionsSnapshot.getLast(gameId)
|
fun getLast(gameId: GameId): GameState =
|
||||||
|
projectionsSnapshot.getLast(gameId)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the [GameState] to the specific [event][GameEvent].
|
* Get the [GameState] to the specific [event][GameEvent].
|
||||||
* It does not contain the [events][GameEvent] it after this one.
|
* It does not contain the [events][GameEvent] it after this one.
|
||||||
*
|
*
|
||||||
* It fetches it from the local cache if possible, otherwise it builds it.
|
* It fetches it from the local cache if possible, otherwise it builds it.
|
||||||
*/
|
*/
|
||||||
fun getUntil(event: GameEvent): GameState = projectionsSnapshot.getUntil(event)
|
fun getUntil(event: GameEvent): GameState =
|
||||||
|
projectionsSnapshot.getUntil(event)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ package eventDemo.app.event.projection
|
|||||||
import eventDemo.libs.event.AggregateId
|
import eventDemo.libs.event.AggregateId
|
||||||
|
|
||||||
interface Projection<ID : AggregateId> {
|
interface Projection<ID : AggregateId> {
|
||||||
val aggregateId: ID
|
val aggregateId: ID
|
||||||
val lastEventVersion: Int
|
val lastEventVersion: Int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,182 +13,183 @@ import kotlin.time.Duration
|
|||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
data class SnapshotConfig(
|
data class SnapshotConfig(
|
||||||
val maxSnapshotCacheSize: Int = 20,
|
val maxSnapshotCacheSize: Int = 20,
|
||||||
val maxSnapshotCacheTtl: Duration = 10.minutes,
|
val maxSnapshotCacheTtl: Duration = 10.minutes,
|
||||||
/**
|
/**
|
||||||
* Only create [snapshots][Projection] every [X][modulo] [events][Event]
|
* Only create [snapshots][Projection] every [X][modulo] [events][Event]
|
||||||
*/
|
*/
|
||||||
val modulo: Int = 10,
|
val modulo: Int = 10,
|
||||||
)
|
)
|
||||||
|
|
||||||
class ProjectionSnapshotRepositoryInMemory<E : Event<ID>, P : Projection<ID>, ID : AggregateId>(
|
class ProjectionSnapshotRepositoryInMemory<E : Event<ID>, P : Projection<ID>, ID : AggregateId>(
|
||||||
private val eventStore: EventStore<E, ID>,
|
private val eventStore: EventStore<E, ID>,
|
||||||
private val initialStateBuilder: (ID) -> P,
|
private val initialStateBuilder: (ID) -> P,
|
||||||
private val snapshotCacheConfig: SnapshotConfig = SnapshotConfig(),
|
private val snapshotCacheConfig: SnapshotConfig = SnapshotConfig(),
|
||||||
private val applyToProjection: P.(event: E) -> P,
|
private val applyToProjection: P.(event: E) -> P,
|
||||||
) {
|
) {
|
||||||
private val projectionsSnapshot: ConcurrentHashMap<ID, ConcurrentLinkedQueue<Pair<P, Instant>>> = ConcurrentHashMap()
|
private val projectionsSnapshot: ConcurrentHashMap<ID, ConcurrentLinkedQueue<Pair<P, Instant>>> = ConcurrentHashMap()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a snapshot for the event
|
* Create a snapshot for the event
|
||||||
*
|
*
|
||||||
* 1. get the last snapshot with a version lower than that of the event
|
* 1. get the last snapshot with a version lower than that of the event
|
||||||
* 2. get the events with a greater version of the snapshot
|
* 2. get the events with a greater version of the snapshot
|
||||||
* 3. apply the event to the snapshot
|
* 3. apply the event to the snapshot
|
||||||
* 4. apply the new event to the projection
|
* 4. apply the new event to the projection
|
||||||
* 5. save it
|
* 5. save it
|
||||||
* 6. remove old one
|
* 6. remove old one
|
||||||
*/
|
*/
|
||||||
fun applyAndPutToCache(event: E) {
|
fun applyAndPutToCache(event: E) {
|
||||||
if ((event.version % snapshotCacheConfig.modulo) == 0) {
|
if ((event.version % snapshotCacheConfig.modulo) == 0) {
|
||||||
getUntil(event)
|
getUntil(event)
|
||||||
.also {
|
.also {
|
||||||
save(it)
|
save(it)
|
||||||
removeOldSnapshot(it.aggregateId)
|
removeOldSnapshot(it.aggregateId)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the last version of the [Projection] from the cache.
|
* Build the last version of the [Projection] from the cache.
|
||||||
*
|
*
|
||||||
* 1. get the last snapshot
|
* 1. get the last snapshot
|
||||||
* 2. get the missing event to the snapshot
|
* 2. get the missing event to the snapshot
|
||||||
* 3. apply the missing events to the snapshot
|
* 3. apply the missing events to the snapshot
|
||||||
*/
|
*/
|
||||||
fun getLast(aggregateId: ID): P {
|
fun getLast(aggregateId: ID): P {
|
||||||
val lastSnapshot = getLastSnapshot(aggregateId)?.first
|
val lastSnapshot = getLastSnapshot(aggregateId)?.first
|
||||||
val missingEventOfSnapshot = getEventAfterTheSnapshot(aggregateId, lastSnapshot)
|
val missingEventOfSnapshot = getEventAfterTheSnapshot(aggregateId, lastSnapshot)
|
||||||
return lastSnapshot.applyEvents(aggregateId, missingEventOfSnapshot)
|
return lastSnapshot.applyEvents(aggregateId, missingEventOfSnapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the [Projection] to the specific [event][Event].
|
||||||
|
*
|
||||||
|
* It does not contain the [events][Event] it after this one.
|
||||||
|
*
|
||||||
|
* 1. get the last snapshot before the event
|
||||||
|
* 2. get the events with a greater version of the snapshot but lower of passed event
|
||||||
|
* 3. apply the events to the snapshot
|
||||||
|
*/
|
||||||
|
fun getUntil(event: E): P {
|
||||||
|
val lastSnapshot = getLastSnapshotBeforeOrEqualEvent(event)?.first
|
||||||
|
if (lastSnapshot?.lastEventVersion == event.version) {
|
||||||
|
return lastSnapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
val missingEventOfSnapshot =
|
||||||
* Build the [Projection] to the specific [event][Event].
|
eventStore
|
||||||
*
|
.getStream(event.aggregateId)
|
||||||
* It does not contain the [events][Event] it after this one.
|
.readVersionBetween((lastSnapshot?.lastEventVersion ?: 1)..event.version)
|
||||||
*
|
|
||||||
* 1. get the last snapshot before the event
|
|
||||||
* 2. get the events with a greater version of the snapshot but lower of passed event
|
|
||||||
* 3. apply the events to the snapshot
|
|
||||||
*/
|
|
||||||
fun getUntil(event: E): P {
|
|
||||||
val lastSnapshot = getLastSnapshotBeforeOrEqualEvent(event)?.first
|
|
||||||
if (lastSnapshot?.lastEventVersion == event.version) {
|
|
||||||
return lastSnapshot
|
|
||||||
}
|
|
||||||
|
|
||||||
val missingEventOfSnapshot =
|
return if (lastSnapshot?.lastEventVersion == event.version) {
|
||||||
eventStore
|
lastSnapshot
|
||||||
.getStream(event.aggregateId)
|
} else {
|
||||||
.readVersionBetween((lastSnapshot?.lastEventVersion ?: 1)..event.version)
|
lastSnapshot.applyEvents(event.aggregateId, missingEventOfSnapshot)
|
||||||
|
|
||||||
return if (lastSnapshot?.lastEventVersion == event.version) {
|
|
||||||
lastSnapshot
|
|
||||||
} else {
|
|
||||||
lastSnapshot.applyEvents(event.aggregateId, missingEventOfSnapshot)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the oldest snapshot.
|
* Remove the oldest snapshot.
|
||||||
*
|
*
|
||||||
* The rules are pass in the controller.
|
* The rules are pass in the controller.
|
||||||
*/
|
*/
|
||||||
private fun removeOldSnapshot(aggregateId: ID) {
|
private fun removeOldSnapshot(aggregateId: ID) {
|
||||||
projectionsSnapshot[aggregateId]?.let { queue ->
|
projectionsSnapshot[aggregateId]?.let { queue ->
|
||||||
// never remove the last one
|
// never remove the last one
|
||||||
val theLastOne = getLastSnapshot(aggregateId)
|
val theLastOne = getLastSnapshot(aggregateId)
|
||||||
removeByDate(queue, theLastOne)
|
removeByDate(queue, theLastOne)
|
||||||
removeBySize(queue, theLastOne)
|
removeBySize(queue, theLastOne)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun removeBySize(
|
private fun removeBySize(
|
||||||
queue: ConcurrentLinkedQueue<Pair<P, Instant>>,
|
queue: ConcurrentLinkedQueue<Pair<P, Instant>>,
|
||||||
theLastOne: Pair<P, Instant>?,
|
theLastOne: Pair<P, Instant>?,
|
||||||
) {
|
) {
|
||||||
// Remove if size exceeds the limit
|
// Remove if size exceeds the limit
|
||||||
val size = queue.size
|
val size = queue.size
|
||||||
if (size > snapshotCacheConfig.maxSnapshotCacheSize) {
|
if (size > snapshotCacheConfig.maxSnapshotCacheSize) {
|
||||||
val numberToRemove = size - snapshotCacheConfig.maxSnapshotCacheSize
|
val numberToRemove = size - snapshotCacheConfig.maxSnapshotCacheSize
|
||||||
if (numberToRemove > 0) {
|
if (numberToRemove > 0) {
|
||||||
queue
|
queue
|
||||||
.sortedBy { it.first.lastEventVersion }
|
.sortedBy { it.first.lastEventVersion }
|
||||||
.take(numberToRemove)
|
.take(numberToRemove)
|
||||||
.let { it - theLastOne }
|
.let { it - theLastOne }
|
||||||
.forEach { queue.remove(it) }
|
.forEach { queue.remove(it) }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun removeByDate(
|
private fun removeByDate(
|
||||||
queue: ConcurrentLinkedQueue<Pair<P, Instant>>,
|
queue: ConcurrentLinkedQueue<Pair<P, Instant>>,
|
||||||
theLastOne: Pair<P, Instant>?,
|
theLastOne: Pair<P, Instant>?,
|
||||||
) {
|
) {
|
||||||
// remove the oldest by time
|
// remove the oldest by time
|
||||||
val now = Clock.System.now()
|
val now = Clock.System.now()
|
||||||
val deadLine = now - snapshotCacheConfig.maxSnapshotCacheTtl
|
val deadLine = now - snapshotCacheConfig.maxSnapshotCacheTtl
|
||||||
val toRemove = queue.filter { deadLine > it.second }
|
val toRemove = queue.filter { deadLine > it.second }
|
||||||
(toRemove - theLastOne).forEach { queue.remove(it) }
|
(toRemove - theLastOne).forEach { queue.remove(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save the snapshot.
|
* Save the snapshot.
|
||||||
*/
|
*/
|
||||||
private fun save(projection: P) {
|
private fun save(projection: P) {
|
||||||
projectionsSnapshot
|
projectionsSnapshot
|
||||||
.computeIfAbsent(projection.aggregateId) { ConcurrentLinkedQueue() }
|
.computeIfAbsent(projection.aggregateId) { ConcurrentLinkedQueue() }
|
||||||
.add(Pair(projection, Clock.System.now()))
|
.add(Pair(projection, Clock.System.now()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the last snapshot when the version is lower of then event version
|
* Get the last snapshot when the version is lower of then event version
|
||||||
*/
|
*/
|
||||||
private fun getLastSnapshotBeforeOrEqualEvent(event: E) =
|
private fun getLastSnapshotBeforeOrEqualEvent(event: E) =
|
||||||
projectionsSnapshot[event.aggregateId]
|
projectionsSnapshot[event.aggregateId]
|
||||||
?.sortedByDescending { it.first.lastEventVersion }
|
?.sortedByDescending { it.first.lastEventVersion }
|
||||||
?.find { it.first.lastEventVersion <= event.version }
|
?.find { it.first.lastEventVersion <= event.version }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the last snapshot (with the higher version).
|
* Get the last snapshot (with the higher version).
|
||||||
*/
|
*/
|
||||||
private fun getLastSnapshot(aggregateId: ID) =
|
private fun getLastSnapshot(aggregateId: ID) =
|
||||||
projectionsSnapshot[aggregateId]
|
projectionsSnapshot[aggregateId]
|
||||||
?.maxByOrNull { it.first.lastEventVersion }
|
?.maxByOrNull { it.first.lastEventVersion }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the events from the [event stream][EventStream] when the version is higher of the snapshot.
|
* Get the events from the [event stream][EventStream] when the version is higher of the snapshot.
|
||||||
*
|
*
|
||||||
* If the snapshot is null, it takes all events from the event [event stream][EventStream]
|
* If the snapshot is null, it takes all events from the event [event stream][EventStream]
|
||||||
*/
|
*/
|
||||||
private fun getEventAfterTheSnapshot(
|
private fun getEventAfterTheSnapshot(
|
||||||
aggregateId: ID,
|
aggregateId: ID,
|
||||||
snapshot: P?,
|
snapshot: P?,
|
||||||
) = eventStore
|
) =
|
||||||
.getStream(aggregateId)
|
eventStore
|
||||||
.readGreaterOfVersion(snapshot?.lastEventVersion ?: 0)
|
.getStream(aggregateId)
|
||||||
|
.readGreaterOfVersion(snapshot?.lastEventVersion ?: 0)
|
||||||
/**
|
|
||||||
* Apply events to the projection.
|
/**
|
||||||
*/
|
* Apply events to the projection.
|
||||||
private fun P?.applyEvents(
|
*/
|
||||||
aggregateId: ID,
|
private fun P?.applyEvents(
|
||||||
eventsToApply: Set<E>,
|
aggregateId: ID,
|
||||||
): P =
|
eventsToApply: Set<E>,
|
||||||
eventsToApply
|
): P =
|
||||||
.fold(this ?: initialStateBuilder(aggregateId), applyToProjectionSecure)
|
eventsToApply
|
||||||
|
.fold(this ?: initialStateBuilder(aggregateId), applyToProjectionSecure)
|
||||||
/**
|
|
||||||
* Wrap the [applyToProjection] lambda to avoid duplicate apply of the same event.
|
/**
|
||||||
*/
|
* Wrap the [applyToProjection] lambda to avoid duplicate apply of the same event.
|
||||||
private val applyToProjectionSecure: P.(event: E) -> P = { event ->
|
*/
|
||||||
if (event.version == lastEventVersion + 1) {
|
private val applyToProjectionSecure: P.(event: E) -> P = { event ->
|
||||||
applyToProjection(event)
|
if (event.version == lastEventVersion + 1) {
|
||||||
} else if (event.version <= lastEventVersion) {
|
applyToProjection(event)
|
||||||
KotlinLogging.logger { }.warn { "Event is already is the Projection, skip apply." }
|
} else if (event.version <= lastEventVersion) {
|
||||||
this
|
KotlinLogging.logger { }.warn { "Event is already is the Projection, skip apply." }
|
||||||
} else {
|
this
|
||||||
error("The version of the event must follow directly after the version of the projection.")
|
} else {
|
||||||
}
|
error("The version of the event must follow directly after the version of the projection.")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,118 +28,118 @@ import kotlinx.coroutines.channels.SendChannel
|
|||||||
import kotlinx.coroutines.channels.trySendBlocking
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
|
|
||||||
class PlayerNotificationEventListener(
|
class PlayerNotificationEventListener(
|
||||||
private val eventBus: GameEventBus,
|
private val eventBus: GameEventBus,
|
||||||
private val gameStateRepository: GameStateRepository,
|
private val gameStateRepository: GameStateRepository,
|
||||||
) {
|
) {
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
fun startListening(
|
fun startListening(
|
||||||
outgoingNotificationChannel: SendChannel<Notification>,
|
outgoingNotificationChannel: SendChannel<Notification>,
|
||||||
currentPlayer: Player,
|
currentPlayer: Player,
|
||||||
) {
|
) {
|
||||||
eventBus.subscribe { event: GameEvent ->
|
eventBus.subscribe { event: GameEvent ->
|
||||||
val currentState = gameStateRepository.getUntil(event)
|
val currentState = gameStateRepository.getUntil(event)
|
||||||
|
|
||||||
fun Notification.send() {
|
fun Notification.send() {
|
||||||
if (currentState.players.contains(currentPlayer)) {
|
if (currentState.players.contains(currentPlayer)) {
|
||||||
// Only notify players who have already joined the game.
|
// Only notify players who have already joined the game.
|
||||||
outgoingNotificationChannel.trySendBlocking(this)
|
outgoingNotificationChannel.trySendBlocking(this)
|
||||||
logger.atInfo {
|
logger.atInfo {
|
||||||
message = "Notification for player ${currentPlayer.name} was SEND: ${this@send}"
|
message = "Notification for player ${currentPlayer.name} was SEND: ${this@send}"
|
||||||
payload = mapOf("notification" to this@send, "event" to event)
|
payload = mapOf("notification" to this@send, "event" to event)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Rare use case, when a connexion is created with the channel,
|
// Rare use case, when a connexion is created with the channel,
|
||||||
// but the player was not already join in the game
|
// but the player was not already join in the game
|
||||||
logger.atWarn {
|
logger.atWarn {
|
||||||
message = "Notification for player ${currentPlayer.name} was SKIP, No player on the game: ${this@send}"
|
message = "Notification for player ${currentPlayer.name} was SKIP, No player on the game: ${this@send}"
|
||||||
payload = mapOf("notification" to this@send, "event" to event)
|
payload = mapOf("notification" to this@send, "event" to event)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendNextTurnNotif() =
|
|
||||||
ItsTheTurnOfNotification(
|
|
||||||
player = currentState.currentPlayerTurn ?: error("No player turn defined"),
|
|
||||||
).send()
|
|
||||||
|
|
||||||
when (event) {
|
|
||||||
is NewPlayerEvent -> {
|
|
||||||
if (currentPlayer != event.player) {
|
|
||||||
PlayerAsJoinTheGameNotification(
|
|
||||||
player = event.player,
|
|
||||||
).send()
|
|
||||||
} else {
|
|
||||||
WelcomeToTheGameNotification(
|
|
||||||
players = currentState.players,
|
|
||||||
).send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is CardIsPlayedEvent -> {
|
|
||||||
if (currentPlayer != event.player) {
|
|
||||||
PlayerAsPlayACardNotification(
|
|
||||||
player = event.player,
|
|
||||||
card = event.card,
|
|
||||||
).send()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.card !is Card.AllColorCard) {
|
|
||||||
ItsTheTurnOfNotification(
|
|
||||||
player = currentState.currentPlayerTurn ?: error("No player turn defined"),
|
|
||||||
).send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is GameStartedEvent -> {
|
|
||||||
TheGameWasStartedNotification(
|
|
||||||
hand =
|
|
||||||
event.deck.playersHands.getHand(currentPlayer)
|
|
||||||
?: error("You are not in the game"),
|
|
||||||
).send()
|
|
||||||
|
|
||||||
sendNextTurnNotif()
|
|
||||||
}
|
|
||||||
|
|
||||||
is PlayerChoseColorEvent -> {
|
|
||||||
if (currentPlayer != event.player) {
|
|
||||||
PlayerWasChoseTheCardColorNotification(
|
|
||||||
player = event.player,
|
|
||||||
color = event.color,
|
|
||||||
).send()
|
|
||||||
}
|
|
||||||
|
|
||||||
sendNextTurnNotif()
|
|
||||||
}
|
|
||||||
|
|
||||||
is PlayerHavePassEvent -> {
|
|
||||||
if (currentPlayer == event.player) {
|
|
||||||
YourNewCardNotification(
|
|
||||||
card = event.takenCard,
|
|
||||||
).send()
|
|
||||||
} else {
|
|
||||||
PlayerHavePassNotification(
|
|
||||||
player = event.player,
|
|
||||||
).send()
|
|
||||||
}
|
|
||||||
|
|
||||||
sendNextTurnNotif()
|
|
||||||
}
|
|
||||||
|
|
||||||
is PlayerReadyEvent -> {
|
|
||||||
if (currentPlayer != event.player) {
|
|
||||||
PlayerWasReadyNotification(
|
|
||||||
player = event.player,
|
|
||||||
).send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is PlayerWinEvent -> {
|
|
||||||
PlayerWinNotification(
|
|
||||||
player = event.player,
|
|
||||||
).send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendNextTurnNotif() =
|
||||||
|
ItsTheTurnOfNotification(
|
||||||
|
player = currentState.currentPlayerTurn ?: error("No player turn defined"),
|
||||||
|
).send()
|
||||||
|
|
||||||
|
when (event) {
|
||||||
|
is NewPlayerEvent -> {
|
||||||
|
if (currentPlayer != event.player) {
|
||||||
|
PlayerAsJoinTheGameNotification(
|
||||||
|
player = event.player,
|
||||||
|
).send()
|
||||||
|
} else {
|
||||||
|
WelcomeToTheGameNotification(
|
||||||
|
players = currentState.players,
|
||||||
|
).send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is CardIsPlayedEvent -> {
|
||||||
|
if (currentPlayer != event.player) {
|
||||||
|
PlayerAsPlayACardNotification(
|
||||||
|
player = event.player,
|
||||||
|
card = event.card,
|
||||||
|
).send()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.card !is Card.AllColorCard) {
|
||||||
|
ItsTheTurnOfNotification(
|
||||||
|
player = currentState.currentPlayerTurn ?: error("No player turn defined"),
|
||||||
|
).send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is GameStartedEvent -> {
|
||||||
|
TheGameWasStartedNotification(
|
||||||
|
hand =
|
||||||
|
event.deck.playersHands.getHand(currentPlayer)
|
||||||
|
?: error("You are not in the game"),
|
||||||
|
).send()
|
||||||
|
|
||||||
|
sendNextTurnNotif()
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerChoseColorEvent -> {
|
||||||
|
if (currentPlayer != event.player) {
|
||||||
|
PlayerWasChoseTheCardColorNotification(
|
||||||
|
player = event.player,
|
||||||
|
color = event.color,
|
||||||
|
).send()
|
||||||
|
}
|
||||||
|
|
||||||
|
sendNextTurnNotif()
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerHavePassEvent -> {
|
||||||
|
if (currentPlayer == event.player) {
|
||||||
|
YourNewCardNotification(
|
||||||
|
card = event.takenCard,
|
||||||
|
).send()
|
||||||
|
} else {
|
||||||
|
PlayerHavePassNotification(
|
||||||
|
player = event.player,
|
||||||
|
).send()
|
||||||
|
}
|
||||||
|
|
||||||
|
sendNextTurnNotif()
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerReadyEvent -> {
|
||||||
|
if (currentPlayer != event.player) {
|
||||||
|
PlayerWasReadyNotification(
|
||||||
|
player = event.player,
|
||||||
|
).send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerWinEvent -> {
|
||||||
|
PlayerWinNotification(
|
||||||
|
player = event.player,
|
||||||
|
).send()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,76 +11,76 @@ import eventDemo.app.event.projection.GameStateRepository
|
|||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
|
||||||
class ReactionEventListener(
|
class ReactionEventListener(
|
||||||
private val eventBus: GameEventBus,
|
private val eventBus: GameEventBus,
|
||||||
private val eventHandler: GameEventHandler,
|
private val eventHandler: GameEventHandler,
|
||||||
private val gameStateRepository: GameStateRepository,
|
private val gameStateRepository: GameStateRepository,
|
||||||
private val priority: Int = DEFAULT_PRIORITY,
|
private val priority: Int = DEFAULT_PRIORITY,
|
||||||
) {
|
) {
|
||||||
companion object Config {
|
companion object Config {
|
||||||
const val DEFAULT_PRIORITY = -1000
|
const val DEFAULT_PRIORITY = -1000
|
||||||
|
}
|
||||||
|
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
fun init() {
|
||||||
|
eventBus.subscribe(priority) { event: GameEvent ->
|
||||||
|
val state = gameStateRepository.getUntil(event)
|
||||||
|
sendStartGameEvent(state, event)
|
||||||
|
sendWinnerEvent(state, event)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger { }
|
private suspend fun sendStartGameEvent(
|
||||||
|
state: GameState,
|
||||||
fun init() {
|
event: GameEvent,
|
||||||
eventBus.subscribe(priority) { event: GameEvent ->
|
) {
|
||||||
val state = gameStateRepository.getUntil(event)
|
if (state.isReady && !state.isStarted) {
|
||||||
sendStartGameEvent(state, event)
|
val reactionEvent =
|
||||||
sendWinnerEvent(state, event)
|
eventHandler.handle(state.aggregateId) {
|
||||||
|
GameStartedEvent.new(
|
||||||
|
id = state.aggregateId,
|
||||||
|
players = state.players,
|
||||||
|
version = it,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
logger.atInfo {
|
||||||
|
message = "Reaction event was Send $reactionEvent on reaction of: $event"
|
||||||
|
payload =
|
||||||
|
mapOf(
|
||||||
|
"event" to event,
|
||||||
|
"reactionEvent" to reactionEvent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (event is PlayerReadyEvent) {
|
||||||
|
logger.info { "All players was not ready ${state.readyPlayers}" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun sendStartGameEvent(
|
private fun sendWinnerEvent(
|
||||||
state: GameState,
|
state: GameState,
|
||||||
event: GameEvent,
|
event: GameEvent,
|
||||||
) {
|
) {
|
||||||
if (state.isReady && !state.isStarted) {
|
val winner = state.playerHasNoCardLeft().firstOrNull()
|
||||||
val reactionEvent =
|
if (winner != null) {
|
||||||
eventHandler.handle(state.aggregateId) {
|
val reactionEvent =
|
||||||
GameStartedEvent.new(
|
eventHandler.handle(state.aggregateId) {
|
||||||
id = state.aggregateId,
|
PlayerWinEvent(
|
||||||
players = state.players,
|
aggregateId = state.aggregateId,
|
||||||
version = it,
|
player = winner,
|
||||||
)
|
version = it,
|
||||||
}
|
)
|
||||||
logger.atInfo {
|
|
||||||
message = "Reaction event was Send $reactionEvent on reaction of: $event"
|
|
||||||
payload =
|
|
||||||
mapOf(
|
|
||||||
"event" to event,
|
|
||||||
"reactionEvent" to reactionEvent,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (event is PlayerReadyEvent) {
|
|
||||||
logger.info { "All players was not ready ${state.readyPlayers}" }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun sendWinnerEvent(
|
logger.atInfo {
|
||||||
state: GameState,
|
message = "Reaction event was Send $reactionEvent on reaction of: $event"
|
||||||
event: GameEvent,
|
payload =
|
||||||
) {
|
mapOf(
|
||||||
val winner = state.playerHasNoCardLeft().firstOrNull()
|
"event" to event,
|
||||||
if (winner != null) {
|
"reactionEvent" to reactionEvent,
|
||||||
val reactionEvent =
|
)
|
||||||
eventHandler.handle(state.aggregateId) {
|
}
|
||||||
PlayerWinEvent(
|
|
||||||
aggregateId = state.aggregateId,
|
|
||||||
player = winner,
|
|
||||||
version = it,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.atInfo {
|
|
||||||
message = "Reaction event was Send $reactionEvent on reaction of: $event"
|
|
||||||
payload =
|
|
||||||
mapOf(
|
|
||||||
"event" to event,
|
|
||||||
"reactionEvent" to reactionEvent,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ErrorNotification(
|
data class ErrorNotification(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
val message: String,
|
val message: String,
|
||||||
) : Notification
|
) : Notification
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ItsTheTurnOfNotification(
|
data class ItsTheTurnOfNotification(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
val player: Player,
|
val player: Player,
|
||||||
) : Notification
|
) : Notification
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ import java.util.UUID
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed interface Notification {
|
sealed interface Notification {
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
val id: UUID
|
val id: UUID
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PlayerAsJoinTheGameNotification(
|
data class PlayerAsJoinTheGameNotification(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
val player: Player,
|
val player: Player,
|
||||||
) : Notification
|
) : Notification
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import java.util.UUID
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PlayerAsPlayACardNotification(
|
data class PlayerAsPlayACardNotification(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
val player: Player,
|
val player: Player,
|
||||||
val card: Card,
|
val card: Card,
|
||||||
) : Notification
|
) : Notification
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PlayerHavePassNotification(
|
data class PlayerHavePassNotification(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
val player: Player,
|
val player: Player,
|
||||||
) : Notification
|
) : Notification
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import java.util.UUID
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PlayerWasChoseTheCardColorNotification(
|
data class PlayerWasChoseTheCardColorNotification(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
val player: Player,
|
val player: Player,
|
||||||
val color: Card.Color,
|
val color: Card.Color,
|
||||||
) : Notification
|
) : Notification
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PlayerWasReadyNotification(
|
data class PlayerWasReadyNotification(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
val player: Player,
|
val player: Player,
|
||||||
) : Notification
|
) : Notification
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PlayerWinNotification(
|
data class PlayerWinNotification(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
val player: Player,
|
val player: Player,
|
||||||
) : Notification
|
) : Notification
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class TheGameWasStartedNotification(
|
data class TheGameWasStartedNotification(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
val hand: List<Card>,
|
val hand: List<Card>,
|
||||||
) : Notification
|
) : Notification
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class WelcomeToTheGameNotification(
|
data class WelcomeToTheGameNotification(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
val players: Set<Player>,
|
val players: Set<Player>,
|
||||||
) : Notification
|
) : Notification
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class YourNewCardNotification(
|
data class YourNewCardNotification(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
val card: Card,
|
val card: Card,
|
||||||
) : Notification
|
) : Notification
|
||||||
|
|||||||
@@ -15,41 +15,41 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
@Resource("/game/{id}")
|
@Resource("/game/{id}")
|
||||||
class Game(
|
class Game(
|
||||||
@Serializable(with = GameIdSerializer::class)
|
@Serializable(with = GameIdSerializer::class)
|
||||||
val id: GameId,
|
val id: GameId,
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
@Resource("card/last")
|
@Resource("card/last")
|
||||||
class Card(
|
class Card(
|
||||||
val game: Game,
|
val game: Game,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@Resource("state")
|
@Resource("state")
|
||||||
class State(
|
class State(
|
||||||
val game: Game,
|
val game: Game,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API routes to read the game state.
|
* API routes to read the game state.
|
||||||
*/
|
*/
|
||||||
fun Route.readTheGameState(gameStateRepository: GameStateRepository) {
|
fun Route.readTheGameState(gameStateRepository: GameStateRepository) {
|
||||||
authenticate {
|
authenticate {
|
||||||
// Read the last played card on the game.
|
// Read the last played card on the game.
|
||||||
get<Game.Card> { body ->
|
get<Game.Card> { body ->
|
||||||
gameStateRepository
|
gameStateRepository
|
||||||
.getLast(body.game.id)
|
.getLast(body.game.id)
|
||||||
.cardOnCurrentStack
|
.cardOnCurrentStack
|
||||||
?.card
|
?.card
|
||||||
?.let { call.respond(it) }
|
?.let { call.respond(it) }
|
||||||
?: call.response.status(HttpStatusCode.BadRequest)
|
?: call.response.status(HttpStatusCode.BadRequest)
|
||||||
}
|
|
||||||
|
|
||||||
// Read the last played card on the game.
|
|
||||||
get<Game.State> { body ->
|
|
||||||
val state = gameStateRepository.getLast(body.game.id)
|
|
||||||
call.respond(state)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read the last played card on the game.
|
||||||
|
get<Game.State> { body ->
|
||||||
|
val state = gameStateRepository.getLast(body.game.id)
|
||||||
|
call.respond(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ import io.ktor.server.application.Application
|
|||||||
import org.koin.ktor.ext.get
|
import org.koin.ktor.ext.get
|
||||||
|
|
||||||
fun Application.configure() {
|
fun Application.configure() {
|
||||||
configureKoin()
|
configureKoin()
|
||||||
|
|
||||||
configureSecurity()
|
configureSecurity()
|
||||||
|
|
||||||
configureSerialization()
|
configureSerialization()
|
||||||
|
|
||||||
configureWebSockets()
|
configureWebSockets()
|
||||||
declareWebSocketsGameRoute(get(), get())
|
declareWebSocketsGameRoute(get(), get())
|
||||||
|
|
||||||
configureHttpRouting()
|
configureHttpRouting()
|
||||||
declareHttpGameRoute()
|
declareHttpGameRoute()
|
||||||
|
|
||||||
configureGameListener()
|
configureGameListener()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,43 +21,43 @@ private val jwtIssuer = "PlayCardGame"
|
|||||||
private val jwtSecret = "secret"
|
private val jwtSecret = "secret"
|
||||||
|
|
||||||
fun Application.configureSecurity() {
|
fun Application.configureSecurity() {
|
||||||
authentication {
|
authentication {
|
||||||
jwt {
|
jwt {
|
||||||
realm = jwtRealm
|
realm = jwtRealm
|
||||||
verifier(
|
verifier(
|
||||||
JWT
|
JWT
|
||||||
.require(Algorithm.HMAC256(jwtSecret))
|
.require(Algorithm.HMAC256(jwtSecret))
|
||||||
.withIssuer(jwtIssuer)
|
.withIssuer(jwtIssuer)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
validate { credential ->
|
validate { credential ->
|
||||||
if (credential.payload.getClaim("username").asString() != "") {
|
if (credential.payload.getClaim("username").asString() != "") {
|
||||||
JWTPrincipal(credential.payload)
|
JWTPrincipal(credential.payload)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
|
||||||
}
|
|
||||||
challenge { defaultScheme, realm ->
|
|
||||||
call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
challenge { defaultScheme, realm ->
|
||||||
|
call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
routing {
|
routing {
|
||||||
post("login/{username}") {
|
post("login/{username}") {
|
||||||
val username = call.parameters["username"]!!
|
val username = call.parameters["username"]!!
|
||||||
val player = Player(name = username)
|
val player = Player(name = username)
|
||||||
|
|
||||||
call.respond(hashMapOf("token" to player.makeJwt()))
|
call.respond(hashMapOf("token" to player.makeJwt()))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Player.makeJwt(): String =
|
fun Player.makeJwt(): String =
|
||||||
JWT
|
JWT
|
||||||
.create()
|
.create()
|
||||||
.withIssuer(jwtIssuer)
|
.withIssuer(jwtIssuer)
|
||||||
.withClaim("username", name)
|
.withClaim("username", name)
|
||||||
.withPayload(Json.encodeToString(this))
|
.withPayload(Json.encodeToString(this))
|
||||||
.withExpiresAt(Date(System.currentTimeMillis() + 60000))
|
.withExpiresAt(Date(System.currentTimeMillis() + 60000))
|
||||||
.sign(Algorithm.HMAC256(jwtSecret))
|
.sign(Algorithm.HMAC256(jwtSecret))
|
||||||
|
|||||||
@@ -23,30 +23,30 @@ import org.koin.ktor.plugin.Koin
|
|||||||
import org.koin.logger.slf4jLogger
|
import org.koin.logger.slf4jLogger
|
||||||
|
|
||||||
fun Application.configureKoin() {
|
fun Application.configureKoin() {
|
||||||
install(Koin) {
|
install(Koin) {
|
||||||
slf4jLogger()
|
slf4jLogger()
|
||||||
modules(appKoinModule)
|
modules(appKoinModule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val appKoinModule =
|
val appKoinModule =
|
||||||
module {
|
module {
|
||||||
single {
|
single {
|
||||||
GameEventBus(EventBusInMemory())
|
GameEventBus(EventBusInMemory())
|
||||||
}
|
|
||||||
single {
|
|
||||||
GameEventStore(EventStoreInMemory())
|
|
||||||
}
|
|
||||||
single {
|
|
||||||
GameStateRepository(get(), get(), snapshotConfig = SnapshotConfig())
|
|
||||||
}
|
|
||||||
single {
|
|
||||||
CommandStreamChannelBuilder<GameCommand>()
|
|
||||||
}
|
|
||||||
|
|
||||||
singleOf(::VersionBuilderLocal) bind VersionBuilder::class
|
|
||||||
singleOf(::GameEventHandler)
|
|
||||||
singleOf(::GameCommandRunner)
|
|
||||||
singleOf(::GameCommandHandler)
|
|
||||||
singleOf(::PlayerNotificationEventListener)
|
|
||||||
}
|
}
|
||||||
|
single {
|
||||||
|
GameEventStore(EventStoreInMemory())
|
||||||
|
}
|
||||||
|
single {
|
||||||
|
GameStateRepository(get(), get(), snapshotConfig = SnapshotConfig())
|
||||||
|
}
|
||||||
|
single {
|
||||||
|
CommandStreamChannelBuilder<GameCommand>()
|
||||||
|
}
|
||||||
|
|
||||||
|
singleOf(::VersionBuilderLocal) bind VersionBuilder::class
|
||||||
|
singleOf(::GameEventHandler)
|
||||||
|
singleOf(::GameCommandRunner)
|
||||||
|
singleOf(::GameCommandHandler)
|
||||||
|
singleOf(::PlayerNotificationEventListener)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ import io.ktor.server.application.Application
|
|||||||
import org.koin.ktor.ext.get
|
import org.koin.ktor.ext.get
|
||||||
|
|
||||||
fun Application.configureGameListener() {
|
fun Application.configureGameListener() {
|
||||||
ReactionEventListener(get(), get(), get())
|
ReactionEventListener(get(), get(), get())
|
||||||
.init()
|
.init()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,41 +12,41 @@ import io.ktor.server.resources.Resources
|
|||||||
import io.ktor.server.response.respondText
|
import io.ktor.server.response.respondText
|
||||||
|
|
||||||
fun Application.configureHttpRouting() {
|
fun Application.configureHttpRouting() {
|
||||||
install(CORS) {
|
install(CORS) {
|
||||||
allowMethod(HttpMethod.Options)
|
allowMethod(HttpMethod.Options)
|
||||||
allowMethod(HttpMethod.Put)
|
allowMethod(HttpMethod.Put)
|
||||||
allowMethod(HttpMethod.Post)
|
allowMethod(HttpMethod.Post)
|
||||||
allowMethod(HttpMethod.Delete)
|
allowMethod(HttpMethod.Delete)
|
||||||
allowMethod(HttpMethod.Patch)
|
allowMethod(HttpMethod.Patch)
|
||||||
allowHeader(HttpHeaders.Authorization)
|
allowHeader(HttpHeaders.Authorization)
|
||||||
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)
|
||||||
}
|
}
|
||||||
install(AutoHeadResponse)
|
exception<Throwable> { call, cause ->
|
||||||
install(Resources)
|
call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError)
|
||||||
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(
|
||||||
val httpError: HttpErrorBadRequest,
|
val httpError: HttpErrorBadRequest,
|
||||||
) : Exception()
|
) : Exception()
|
||||||
|
|
||||||
class HttpErrorBadRequest(
|
class HttpErrorBadRequest(
|
||||||
statusCode: HttpStatusCode,
|
statusCode: HttpStatusCode,
|
||||||
val title: String = statusCode.description,
|
val title: String = statusCode.description,
|
||||||
val invalidParams: List<InvalidParam>,
|
val invalidParams: List<InvalidParam>,
|
||||||
) {
|
) {
|
||||||
val statusCode: Int = statusCode.value
|
val statusCode: Int = statusCode.value
|
||||||
|
|
||||||
data class InvalidParam(
|
data class InvalidParam(
|
||||||
val name: String,
|
val name: String,
|
||||||
val reason: String,
|
val reason: String,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,72 +18,76 @@ import kotlinx.serialization.modules.SerializersModule
|
|||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
fun Application.configureSerialization() {
|
fun Application.configureSerialization() {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(
|
json(
|
||||||
defaultJsonSerializer(),
|
defaultJsonSerializer(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun defaultJsonSerializer(): Json =
|
fun defaultJsonSerializer(): Json =
|
||||||
Json {
|
Json {
|
||||||
serializersModule =
|
serializersModule =
|
||||||
SerializersModule {
|
SerializersModule {
|
||||||
contextual(UUID::class) { UUIDSerializer }
|
contextual(UUID::class) { UUIDSerializer }
|
||||||
contextual(GameId::class) { GameIdSerializer }
|
contextual(GameId::class) { GameIdSerializer }
|
||||||
contextual(CommandId::class) { CommandIdSerializer }
|
contextual(CommandId::class) { CommandIdSerializer }
|
||||||
contextual(Player.PlayerId::class) { PlayerIdSerializer }
|
contextual(Player.PlayerId::class) { PlayerIdSerializer }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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())
|
||||||
|
|
||||||
override fun serialize(
|
override fun serialize(
|
||||||
encoder: Encoder,
|
encoder: Encoder,
|
||||||
value: CommandId,
|
value: CommandId,
|
||||||
) {
|
) {
|
||||||
encoder.encodeString(value.toString())
|
encoder.encodeString(value.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CommandId", PrimitiveKind.STRING)
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CommandId", PrimitiveKind.STRING)
|
||||||
}
|
}
|
||||||
|
|
||||||
object PlayerIdSerializer : KSerializer<Player.PlayerId> {
|
object PlayerIdSerializer : KSerializer<Player.PlayerId> {
|
||||||
override fun deserialize(decoder: Decoder): Player.PlayerId = Player.PlayerId(UUID.fromString(decoder.decodeString()))
|
override fun deserialize(decoder: Decoder): Player.PlayerId =
|
||||||
|
Player.PlayerId(UUID.fromString(decoder.decodeString()))
|
||||||
|
|
||||||
override fun serialize(
|
override fun serialize(
|
||||||
encoder: Encoder,
|
encoder: Encoder,
|
||||||
value: Player.PlayerId,
|
value: Player.PlayerId,
|
||||||
) {
|
) {
|
||||||
encoder.encodeString(value.id.toString())
|
encoder.encodeString(value.id.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("PlayerId", PrimitiveKind.STRING)
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("PlayerId", PrimitiveKind.STRING)
|
||||||
}
|
}
|
||||||
|
|
||||||
object GameIdSerializer : KSerializer<GameId> {
|
object GameIdSerializer : KSerializer<GameId> {
|
||||||
override fun deserialize(decoder: Decoder): GameId = GameId(UUID.fromString(decoder.decodeString()))
|
override fun deserialize(decoder: Decoder): GameId =
|
||||||
|
GameId(UUID.fromString(decoder.decodeString()))
|
||||||
|
|
||||||
override fun serialize(
|
override fun serialize(
|
||||||
encoder: Encoder,
|
encoder: Encoder,
|
||||||
value: GameId,
|
value: GameId,
|
||||||
) {
|
) {
|
||||||
encoder.encodeString(value.id.toString())
|
encoder.encodeString(value.id.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("GameId", PrimitiveKind.STRING)
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("GameId", PrimitiveKind.STRING)
|
||||||
}
|
}
|
||||||
|
|
||||||
object UUIDSerializer : KSerializer<UUID> {
|
object UUIDSerializer : KSerializer<UUID> {
|
||||||
override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString())
|
override fun deserialize(decoder: Decoder): UUID =
|
||||||
|
UUID.fromString(decoder.decodeString())
|
||||||
|
|
||||||
override fun serialize(
|
override fun serialize(
|
||||||
encoder: Encoder,
|
encoder: Encoder,
|
||||||
value: UUID,
|
value: UUID,
|
||||||
) {
|
) {
|
||||||
encoder.encodeString(value.toString())
|
encoder.encodeString(value.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import io.ktor.server.websocket.timeout
|
|||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
|
||||||
fun Application.configureWebSockets() {
|
fun Application.configureWebSockets() {
|
||||||
install(WebSockets) {
|
install(WebSockets) {
|
||||||
pingPeriod = Duration.ofSeconds(15)
|
pingPeriod = Duration.ofSeconds(15)
|
||||||
timeout = Duration.ofSeconds(15)
|
timeout = Duration.ofSeconds(15)
|
||||||
maxFrameSize = Long.MAX_VALUE
|
maxFrameSize = Long.MAX_VALUE
|
||||||
masking = false
|
masking = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import kotlinx.coroutines.DelicateCoroutinesApi
|
|||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
fun Application.declareWebSocketsGameRoute(
|
fun Application.declareWebSocketsGameRoute(
|
||||||
playerNotificationListener: PlayerNotificationEventListener,
|
playerNotificationListener: PlayerNotificationEventListener,
|
||||||
commandHandler: GameCommandHandler,
|
commandHandler: GameCommandHandler,
|
||||||
) {
|
) {
|
||||||
routing {
|
routing {
|
||||||
gameSocket(playerNotificationListener, commandHandler)
|
gameSocket(playerNotificationListener, commandHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import io.ktor.server.routing.routing
|
|||||||
import org.koin.ktor.ext.get
|
import org.koin.ktor.ext.get
|
||||||
|
|
||||||
fun Application.declareHttpGameRoute() {
|
fun Application.declareHttpGameRoute() {
|
||||||
routing {
|
routing {
|
||||||
readTheGameState(get())
|
readTheGameState(get())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,28 +16,28 @@ import kotlinx.serialization.json.Json
|
|||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class, InternalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class, InternalCoroutinesApi::class)
|
||||||
inline fun <reified T> CoroutineScope.toObjectChannel(
|
inline fun <reified T> CoroutineScope.toObjectChannel(
|
||||||
frames: ReceiveChannel<Frame>,
|
frames: ReceiveChannel<Frame>,
|
||||||
bufferSize: Int = 0,
|
bufferSize: Int = 0,
|
||||||
): ReceiveChannel<T> {
|
): ReceiveChannel<T> {
|
||||||
val logger = KotlinLogging.logger { }
|
val logger = KotlinLogging.logger { }
|
||||||
return produce(capacity = bufferSize) {
|
return produce(capacity = bufferSize) {
|
||||||
frames.consumeEach { frame ->
|
frames.consumeEach { frame ->
|
||||||
if (frame is Frame.Text) {
|
if (frame is Frame.Text) {
|
||||||
logger.debug { "Conversion of the Frame: ${frame.readText()}" }
|
logger.debug { "Conversion of the Frame: ${frame.readText()}" }
|
||||||
send(Json.decodeFromString(frame.readText()))
|
send(Json.decodeFromString(frame.readText()))
|
||||||
} else {
|
} else {
|
||||||
logger.warn { "The frame is not a text frame" }
|
logger.warn { "The frame is not a text frame" }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T> CoroutineScope.fromFrameChannel(frames: SendChannel<Frame>): SendChannel<T> {
|
inline fun <reified T> CoroutineScope.fromFrameChannel(frames: SendChannel<Frame>): SendChannel<T> {
|
||||||
val channel = Channel<T>()
|
val channel = Channel<T>()
|
||||||
launch {
|
launch {
|
||||||
channel.consumeEach { obj ->
|
channel.consumeEach { obj ->
|
||||||
frames.send(Frame.Text(Json.encodeToString(obj)))
|
frames.send(Frame.Text(Json.encodeToString(obj)))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return channel
|
}
|
||||||
|
return channel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ import java.util.UUID
|
|||||||
@JvmInline
|
@JvmInline
|
||||||
@Serializable(with = CommandIdSerializer::class)
|
@Serializable(with = CommandIdSerializer::class)
|
||||||
value class CommandId(
|
value class CommandId(
|
||||||
private val id: UUID = UUID.randomUUID(),
|
private val id: UUID = UUID.randomUUID(),
|
||||||
) {
|
) {
|
||||||
constructor(id: String) : this(UUID.fromString(id))
|
constructor(id: String) : this(UUID.fromString(id))
|
||||||
|
|
||||||
override fun toString(): String = id.toString()
|
override fun toString(): String =
|
||||||
|
id.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,5 +24,5 @@ value class CommandId(
|
|||||||
* A command is a request for an action.
|
* A command is a request for an action.
|
||||||
*/
|
*/
|
||||||
interface Command {
|
interface Command {
|
||||||
val id: CommandId
|
val id: CommandId
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,26 +10,26 @@ import kotlinx.coroutines.launch
|
|||||||
* The stream contains a list of all actions yet to be executed.
|
* The stream contains a list of all actions yet to be executed.
|
||||||
*/
|
*/
|
||||||
interface CommandStream<C : Command> {
|
interface CommandStream<C : Command> {
|
||||||
/**
|
/**
|
||||||
* A class to implement success/failed action.
|
* A class to implement success/failed action.
|
||||||
*/
|
*/
|
||||||
interface ComputeStatus {
|
interface ComputeStatus {
|
||||||
suspend fun ack()
|
suspend fun ack()
|
||||||
|
|
||||||
suspend fun nack()
|
suspend fun nack()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply an action to all command income in the stream.
|
* Apply an action to all command income in the stream.
|
||||||
*/
|
*/
|
||||||
suspend fun process(action: CommandBlock<C>)
|
suspend fun process(action: CommandBlock<C>)
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
fun blockAndProcess(action: CommandBlock<C>) {
|
fun blockAndProcess(action: CommandBlock<C>) {
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
process(action)
|
process(action)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias CommandBlock<C> = suspend CommandStream.ComputeStatus.(C) -> Unit
|
typealias CommandBlock<C> = suspend CommandStream.ComputeStatus.(C) -> Unit
|
||||||
|
|||||||
@@ -9,87 +9,88 @@ import kotlin.time.Duration
|
|||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
class CommandStreamChannelBuilder<C : Command>(
|
class CommandStreamChannelBuilder<C : Command>(
|
||||||
private val maxCacheTime: Duration = 10.minutes,
|
private val maxCacheTime: Duration = 10.minutes,
|
||||||
) {
|
) {
|
||||||
operator fun invoke(incoming: ReceiveChannel<C>): CommandStreamChannel<C> = CommandStreamChannel(incoming, maxCacheTime)
|
operator fun invoke(incoming: ReceiveChannel<C>): CommandStreamChannel<C> =
|
||||||
|
CommandStreamChannel(incoming, maxCacheTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manage [Command]'s with kotlin Channel
|
* Manage [Command]'s with kotlin Channel
|
||||||
*/
|
*/
|
||||||
class CommandStreamChannel<C : Command>(
|
class CommandStreamChannel<C : Command>(
|
||||||
private val incoming: ReceiveChannel<C>,
|
private val incoming: ReceiveChannel<C>,
|
||||||
private val maxCacheTime: Duration = 10.minutes,
|
private val maxCacheTime: Duration = 10.minutes,
|
||||||
) : CommandStream<C> {
|
) : CommandStream<C> {
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
private val executedCommand: ConcurrentHashMap<CommandId, Pair<Boolean, Instant>> = ConcurrentHashMap()
|
private val executedCommand: ConcurrentHashMap<CommandId, Pair<Boolean, Instant>> = ConcurrentHashMap()
|
||||||
|
|
||||||
override suspend fun process(action: CommandBlock<C>) {
|
override suspend fun process(action: CommandBlock<C>) {
|
||||||
for (command in incoming) {
|
for (command in incoming) {
|
||||||
val now = Clock.System.now()
|
val now = Clock.System.now()
|
||||||
val (status, _) = executedCommand.computeIfAbsent(command.id) { Pair(false, now) }
|
val (status, _) = executedCommand.computeIfAbsent(command.id) { Pair(false, now) }
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
logger.atWarn {
|
|
||||||
message = "Command already executed: $command"
|
|
||||||
payload = mapOf("command" to command)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
compute(command, action)
|
|
||||||
}
|
|
||||||
executedCommand
|
|
||||||
.filterValues { (_, date) ->
|
|
||||||
(date + maxCacheTime) > now
|
|
||||||
}.keys
|
|
||||||
.forEach {
|
|
||||||
executedCommand.remove(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun compute(
|
|
||||||
command: C,
|
|
||||||
action: CommandBlock<C>,
|
|
||||||
) {
|
|
||||||
val status =
|
|
||||||
object : CommandStream.ComputeStatus {
|
|
||||||
var isSet: Boolean = false
|
|
||||||
|
|
||||||
override suspend fun ack() {
|
|
||||||
if (!isSet) markAsSuccess(command) else error("Already NACK")
|
|
||||||
isSet = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun nack() {
|
|
||||||
if (!isSet) markAsFailed(command) else error("Already ACK")
|
|
||||||
isSet = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val actionResult = runCatching { status.action(command) }
|
|
||||||
if (actionResult.isFailure) {
|
|
||||||
logger.atInfo {
|
|
||||||
message = "Error on compute the Command: $command"
|
|
||||||
payload = mapOf("command" to command)
|
|
||||||
cause = actionResult.exceptionOrNull()
|
|
||||||
}
|
|
||||||
markAsFailed(command)
|
|
||||||
} else if (!status.isSet) {
|
|
||||||
status.ack()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun markAsSuccess(command: C) {
|
|
||||||
logger.atInfo {
|
|
||||||
message = "Compute command SUCCESS: $command"
|
|
||||||
payload = mapOf("command" to command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun markAsFailed(command: C) {
|
|
||||||
logger.atWarn {
|
logger.atWarn {
|
||||||
message = "Compute command FAILED: $command"
|
message = "Command already executed: $command"
|
||||||
payload = mapOf("command" to command)
|
payload = mapOf("command" to command)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
compute(command, action)
|
||||||
|
}
|
||||||
|
executedCommand
|
||||||
|
.filterValues { (_, date) ->
|
||||||
|
(date + maxCacheTime) > now
|
||||||
|
}.keys
|
||||||
|
.forEach {
|
||||||
|
executedCommand.remove(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun compute(
|
||||||
|
command: C,
|
||||||
|
action: CommandBlock<C>,
|
||||||
|
) {
|
||||||
|
val status =
|
||||||
|
object : CommandStream.ComputeStatus {
|
||||||
|
var isSet: Boolean = false
|
||||||
|
|
||||||
|
override suspend fun ack() {
|
||||||
|
if (!isSet) markAsSuccess(command) else error("Already NACK")
|
||||||
|
isSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun nack() {
|
||||||
|
if (!isSet) markAsFailed(command) else error("Already ACK")
|
||||||
|
isSet = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val actionResult = runCatching { status.action(command) }
|
||||||
|
if (actionResult.isFailure) {
|
||||||
|
logger.atInfo {
|
||||||
|
message = "Error on compute the Command: $command"
|
||||||
|
payload = mapOf("command" to command)
|
||||||
|
cause = actionResult.exceptionOrNull()
|
||||||
|
}
|
||||||
|
markAsFailed(command)
|
||||||
|
} else if (!status.isSet) {
|
||||||
|
status.ack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun markAsSuccess(command: C) {
|
||||||
|
logger.atInfo {
|
||||||
|
message = "Compute command SUCCESS: $command"
|
||||||
|
payload = mapOf("command" to command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun markAsFailed(command: C) {
|
||||||
|
logger.atWarn {
|
||||||
|
message = "Compute command FAILED: $command"
|
||||||
|
payload = mapOf("command" to command)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import java.util.UUID
|
|||||||
* @see Event
|
* @see Event
|
||||||
*/
|
*/
|
||||||
interface AggregateId {
|
interface AggregateId {
|
||||||
val id: UUID
|
val id: UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,8 +16,8 @@ interface AggregateId {
|
|||||||
* @see EventStream
|
* @see EventStream
|
||||||
*/
|
*/
|
||||||
interface Event<ID : AggregateId> {
|
interface Event<ID : AggregateId> {
|
||||||
val eventId: UUID
|
val eventId: UUID
|
||||||
val aggregateId: ID
|
val aggregateId: ID
|
||||||
val createdAt: Instant
|
val createdAt: Instant
|
||||||
val version: Int
|
val version: Int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package eventDemo.libs.event
|
package eventDemo.libs.event
|
||||||
|
|
||||||
interface EventBus<E : Event<ID>, ID : AggregateId> {
|
interface EventBus<E : Event<ID>, ID : AggregateId> {
|
||||||
fun publish(event: E)
|
fun publish(event: E)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param priority The higher the priority, the more it will be called first
|
* @param priority The higher the priority, the more it will be called first
|
||||||
*/
|
*/
|
||||||
fun subscribe(
|
fun subscribe(
|
||||||
priority: Int = 0,
|
priority: Int = 0,
|
||||||
block: suspend (E) -> Unit,
|
block: suspend (E) -> Unit,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,22 +3,22 @@ package eventDemo.libs.event
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
class EventBusInMemory<E : Event<ID>, ID : AggregateId> : EventBus<E, ID> {
|
class EventBusInMemory<E : Event<ID>, ID : AggregateId> : EventBus<E, ID> {
|
||||||
private val subscribers: MutableList<Pair<Int, suspend (E) -> Unit>> = mutableListOf()
|
private val subscribers: MutableList<Pair<Int, suspend (E) -> Unit>> = mutableListOf()
|
||||||
|
|
||||||
override fun publish(event: E) {
|
override fun publish(event: E) {
|
||||||
subscribers
|
subscribers
|
||||||
.sortedByDescending { (priority, _) -> priority }
|
.sortedByDescending { (priority, _) -> priority }
|
||||||
.forEach { (_, block) ->
|
.forEach { (_, block) ->
|
||||||
runBlocking {
|
runBlocking {
|
||||||
block(event)
|
block(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun subscribe(
|
override fun subscribe(
|
||||||
priority: Int,
|
priority: Int,
|
||||||
block: suspend (E) -> Unit,
|
block: suspend (E) -> Unit,
|
||||||
) {
|
) {
|
||||||
subscribers.add(priority to block)
|
subscribers.add(priority to block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.libs.event
|
package eventDemo.libs.event
|
||||||
|
|
||||||
interface EventStore<E : Event<ID>, ID : AggregateId> {
|
interface EventStore<E : Event<ID>, ID : AggregateId> {
|
||||||
fun getStream(aggregateId: ID): EventStream<E>
|
fun getStream(aggregateId: ID): EventStream<E>
|
||||||
|
|
||||||
fun publish(event: E)
|
fun publish(event: E)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import java.util.concurrent.ConcurrentHashMap
|
|||||||
import java.util.concurrent.ConcurrentMap
|
import java.util.concurrent.ConcurrentMap
|
||||||
|
|
||||||
class EventStoreInMemory<E : Event<ID>, ID : AggregateId> : EventStore<E, ID> {
|
class EventStoreInMemory<E : Event<ID>, ID : AggregateId> : EventStore<E, ID> {
|
||||||
private val streams: ConcurrentMap<ID, EventStream<E>> = ConcurrentHashMap()
|
private val streams: ConcurrentMap<ID, EventStream<E>> = ConcurrentHashMap()
|
||||||
|
|
||||||
override fun getStream(aggregateId: ID): EventStream<E> = streams.computeIfAbsent(aggregateId) { EventStreamInMemory() }
|
override fun getStream(aggregateId: ID): EventStream<E> =
|
||||||
|
streams.computeIfAbsent(aggregateId) { EventStreamInMemory() }
|
||||||
|
|
||||||
override fun publish(event: E) = getStream(event.aggregateId).publish(event)
|
override fun publish(event: E) =
|
||||||
|
getStream(event.aggregateId).publish(event)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ package eventDemo.libs.event
|
|||||||
* Interface representing an event stream for publishing and reading domain events
|
* Interface representing an event stream for publishing and reading domain events
|
||||||
*/
|
*/
|
||||||
interface EventStream<E : Event<*>> {
|
interface EventStream<E : Event<*>> {
|
||||||
/** Publishes a single event to the event stream */
|
/** Publishes a single event to the event stream */
|
||||||
fun publish(event: E)
|
fun publish(event: E)
|
||||||
|
|
||||||
/** Publishes multiple events to the event stream */
|
/** Publishes multiple events to the event stream */
|
||||||
fun publish(vararg events: E)
|
fun publish(vararg events: E)
|
||||||
|
|
||||||
/** Reads all events */
|
/** Reads all events */
|
||||||
fun readAll(): Set<E>
|
fun readAll(): Set<E>
|
||||||
|
|
||||||
fun readGreaterOfVersion(version: Int): Set<E>
|
fun readGreaterOfVersion(version: Int): Set<E>
|
||||||
|
|
||||||
fun readVersionBetween(version: IntRange): Set<E>
|
fun readVersionBetween(version: IntRange): Set<E>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,32 +10,33 @@ import java.util.concurrent.ConcurrentLinkedQueue
|
|||||||
* All methods are implemented.
|
* All methods are implemented.
|
||||||
*/
|
*/
|
||||||
class EventStreamInMemory<E : Event<*>> : EventStream<E> {
|
class EventStreamInMemory<E : Event<*>> : EventStream<E> {
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
private val events: Queue<E> = ConcurrentLinkedQueue()
|
private val events: Queue<E> = ConcurrentLinkedQueue()
|
||||||
|
|
||||||
override fun publish(event: E) {
|
override fun publish(event: E) {
|
||||||
if (events.none { it.eventId == event.eventId }) {
|
if (events.none { it.eventId == event.eventId }) {
|
||||||
events.add(event)
|
events.add(event)
|
||||||
logger.atInfo {
|
logger.atInfo {
|
||||||
message = "Event published: $event"
|
message = "Event published: $event"
|
||||||
payload = mapOf("event" to event)
|
payload = mapOf("event" to event)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun publish(vararg events: E) {
|
override fun publish(vararg events: E) {
|
||||||
events.forEach { publish(it) }
|
events.forEach { publish(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun readAll(): Set<E> = events.toSet()
|
override fun readAll(): Set<E> =
|
||||||
|
events.toSet()
|
||||||
|
|
||||||
override fun readGreaterOfVersion(version: Int): Set<E> =
|
override fun readGreaterOfVersion(version: Int): Set<E> =
|
||||||
events
|
events
|
||||||
.filter { it.version > version }
|
.filter { it.version > version }
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|
||||||
override fun readVersionBetween(version: IntRange): Set<E> =
|
override fun readVersionBetween(version: IntRange): Set<E> =
|
||||||
events
|
events
|
||||||
.filter { version.contains(it.version) }
|
.filter { version.contains(it.version) }
|
||||||
.toSet()
|
.toSet()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.libs.event
|
package eventDemo.libs.event
|
||||||
|
|
||||||
interface VersionBuilder {
|
interface VersionBuilder {
|
||||||
fun buildNextVersion(aggregateId: AggregateId): Int
|
fun buildNextVersion(aggregateId: AggregateId): Int
|
||||||
|
|
||||||
fun getLastVersion(aggregateId: AggregateId): Int
|
fun getLastVersion(aggregateId: AggregateId): Int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,18 @@ import java.util.concurrent.ConcurrentHashMap
|
|||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
class VersionBuilderLocal : VersionBuilder {
|
class VersionBuilderLocal : VersionBuilder {
|
||||||
private val logger = KotlinLogging.logger { }
|
private val logger = KotlinLogging.logger { }
|
||||||
private val versions: ConcurrentHashMap<AggregateId, AtomicInteger> = ConcurrentHashMap()
|
private val versions: ConcurrentHashMap<AggregateId, AtomicInteger> = ConcurrentHashMap()
|
||||||
|
|
||||||
override fun buildNextVersion(aggregateId: AggregateId): Int =
|
override fun buildNextVersion(aggregateId: AggregateId): Int =
|
||||||
versionOfAggregate(aggregateId)
|
versionOfAggregate(aggregateId)
|
||||||
.addAndGet(1)
|
.addAndGet(1)
|
||||||
.also { logger.debug { "New version $it" } }
|
.also { logger.debug { "New version $it" } }
|
||||||
|
|
||||||
override fun getLastVersion(aggregateId: AggregateId): Int = versionOfAggregate(aggregateId).toInt()
|
override fun getLastVersion(aggregateId: AggregateId): Int =
|
||||||
|
versionOfAggregate(aggregateId).toInt()
|
||||||
|
|
||||||
private fun versionOfAggregate(aggregateId: AggregateId) =
|
private fun versionOfAggregate(aggregateId: AggregateId) =
|
||||||
versions
|
versions
|
||||||
.computeIfAbsent(aggregateId) { AtomicInteger(0) }
|
.computeIfAbsent(aggregateId) { AtomicInteger(0) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package eventDemo
|
|||||||
import eventDemo.app.entity.Card
|
import eventDemo.app.entity.Card
|
||||||
import eventDemo.app.entity.Deck
|
import eventDemo.app.entity.Deck
|
||||||
|
|
||||||
fun Deck.allCardCount(): Int = stack.size + discard.size + playersHands.values.flatten().size
|
fun Deck.allCardCount(): Int =
|
||||||
|
stack.size + discard.size + playersHands.values.flatten().size
|
||||||
|
|
||||||
fun Deck.allCards(): Set<Card> = stack + discard + playersHands.values.flatten()
|
fun Deck.allCards(): Set<Card> =
|
||||||
|
stack + discard + playersHands.values.flatten()
|
||||||
|
|
||||||
// suspend fun SendChannel<Frame>.send(command: GameCommand) = send(Frame.Text(Json.encodeToString(command)))
|
// suspend fun SendChannel<Frame>.send(command: GameCommand) = send(Frame.Text(Json.encodeToString(command)))
|
||||||
|
|||||||
@@ -20,26 +20,26 @@ import kotlin.test.assertIs
|
|||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
class GameCommandHandlerTest :
|
class GameCommandHandlerTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
test("handle a command should execute the command") {
|
test("handle a command should execute the command") {
|
||||||
koinApplication { modules(appKoinModule) }.koin.apply {
|
koinApplication { modules(appKoinModule) }.koin.apply {
|
||||||
val commandHandler by inject<GameCommandHandler>()
|
val commandHandler by inject<GameCommandHandler>()
|
||||||
val notificationListener by inject<PlayerNotificationEventListener>()
|
val notificationListener by inject<PlayerNotificationEventListener>()
|
||||||
val gameId = GameId()
|
val gameId = GameId()
|
||||||
val player = Player("Tesla")
|
val player = Player("Tesla")
|
||||||
val channelCommand = Channel<GameCommand>(Channel.BUFFERED)
|
val channelCommand = Channel<GameCommand>(Channel.BUFFERED)
|
||||||
val channelNotification = Channel<Notification>(Channel.BUFFERED)
|
val channelNotification = Channel<Notification>(Channel.BUFFERED)
|
||||||
ReactionEventListener(get(), get(), get()).init()
|
ReactionEventListener(get(), get(), get()).init()
|
||||||
notificationListener.startListening(channelNotification, player)
|
notificationListener.startListening(channelNotification, player)
|
||||||
|
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
commandHandler.handle(player, channelCommand, channelNotification)
|
commandHandler.handle(player, channelCommand, channelNotification)
|
||||||
}
|
|
||||||
|
|
||||||
channelCommand.send(IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(gameId, player)))
|
|
||||||
assertIs<WelcomeToTheGameNotification>(channelNotification.receive()).let {
|
|
||||||
it.players shouldContain player
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
channelCommand.send(IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(gameId, player)))
|
||||||
|
assertIs<WelcomeToTheGameNotification>(channelNotification.receive()).let {
|
||||||
|
it.players shouldContain player
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ package eventDemo.app.command
|
|||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
|
||||||
class GameCommandRunnerTest :
|
class GameCommandRunnerTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
test("run should run the correct command") { }
|
test("run should run the correct command") { }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package eventDemo.app.command.command
|
|||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
|
||||||
class ICantPlayCommandTest :
|
class ICantPlayCommandTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
|
|
||||||
xtest("run should publish the event") { }
|
xtest("run should publish the event") { }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package eventDemo.app.command.command
|
|||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
|
||||||
class IWantToJoinTheGameCommandTest :
|
class IWantToJoinTheGameCommandTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
|
|
||||||
xtest("run should publish the event") { }
|
xtest("run should publish the event") { }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package eventDemo.app.command.command
|
|||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
|
||||||
class IWantToPlayCardCommandTest :
|
class IWantToPlayCardCommandTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
|
|
||||||
xtest("run should publish the event") { }
|
xtest("run should publish the event") { }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package eventDemo.app.command.command
|
|||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
|
||||||
class IamReadyToPlayCommandTest :
|
class IamReadyToPlayCommandTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
|
|
||||||
xtest("run should publish the event") { }
|
xtest("run should publish the event") { }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,97 +8,97 @@ import io.kotest.matchers.ints.shouldBeExactly
|
|||||||
import kotlin.test.assertNotNull
|
import kotlin.test.assertNotNull
|
||||||
|
|
||||||
class DeckTest :
|
class DeckTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
val totalCardsNumber = 104
|
val totalCardsNumber = 104
|
||||||
test("newWithoutPlayers") {
|
test("newWithoutPlayers") {
|
||||||
// When
|
// When
|
||||||
val deck = Deck.newWithoutPlayers()
|
val deck = Deck.newWithoutPlayers()
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
deck.stack.size shouldBeExactly totalCardsNumber
|
deck.stack.size shouldBeExactly totalCardsNumber
|
||||||
deck.discard.size shouldBeExactly 0
|
deck.discard.size shouldBeExactly 0
|
||||||
deck.playersHands.size shouldBeExactly 0
|
deck.playersHands.size shouldBeExactly 0
|
||||||
|
|
||||||
deck.allCardCount() shouldBeExactly totalCardsNumber
|
deck.allCardCount() shouldBeExactly totalCardsNumber
|
||||||
deck.allCards().shouldBeUnique()
|
deck.allCards().shouldBeUnique()
|
||||||
deck.allCards().map { it.id }.shouldBeUnique()
|
deck.allCards().map { it.id }.shouldBeUnique()
|
||||||
}
|
}
|
||||||
|
|
||||||
test("initHands should be generate the hands of all players from the stack") {
|
test("initHands should be generate the hands of all players from the stack") {
|
||||||
// Given
|
// Given
|
||||||
val playerNumbers = 4
|
val playerNumbers = 4
|
||||||
val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet()
|
val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet()
|
||||||
val deck = Deck.newWithoutPlayers()
|
val deck = Deck.newWithoutPlayers()
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val initDeck = deck.initHands(players)
|
val initDeck = deck.initHands(players)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
initDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7)
|
initDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7)
|
||||||
initDeck.discard.size shouldBeExactly 0
|
initDeck.discard.size shouldBeExactly 0
|
||||||
initDeck.playersHands.size shouldBeExactly playerNumbers
|
initDeck.playersHands.size shouldBeExactly playerNumbers
|
||||||
initDeck.playersHands.forEach { (_, cards) -> cards.size shouldBeExactly 7 }
|
initDeck.playersHands.forEach { (_, cards) -> cards.size shouldBeExactly 7 }
|
||||||
initDeck.allCardCount() shouldBeExactly totalCardsNumber
|
initDeck.allCardCount() shouldBeExactly totalCardsNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
test("takeOneCardFromStackTo player") {
|
test("takeOneCardFromStackTo player") {
|
||||||
// Given
|
// Given
|
||||||
val playerNumbers = 4
|
val playerNumbers = 4
|
||||||
val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet()
|
val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet()
|
||||||
val deck = Deck.newWithoutPlayers().initHands(players)
|
val deck = Deck.newWithoutPlayers().initHands(players)
|
||||||
val firstPlayer = players.first()
|
val firstPlayer = players.first()
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val modifiedDeck = deck.takeOneCardFromStackTo(firstPlayer)
|
val modifiedDeck = deck.takeOneCardFromStackTo(firstPlayer)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
modifiedDeck.discard.size shouldBeExactly 0
|
modifiedDeck.discard.size shouldBeExactly 0
|
||||||
modifiedDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7) - 1
|
modifiedDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7) - 1
|
||||||
modifiedDeck.playersHands.size shouldBeExactly playerNumbers
|
modifiedDeck.playersHands.size shouldBeExactly playerNumbers
|
||||||
assertNotNull(modifiedDeck.playersHands.getHand(firstPlayer)).size shouldBeExactly 7 + 1
|
assertNotNull(modifiedDeck.playersHands.getHand(firstPlayer)).size shouldBeExactly 7 + 1
|
||||||
modifiedDeck.playersHands
|
modifiedDeck.playersHands
|
||||||
.filterKeys { it != firstPlayer.id }
|
.filterKeys { it != firstPlayer.id }
|
||||||
.forEach { (_, cards) -> cards.size shouldBeExactly 7 }
|
.forEach { (_, cards) -> cards.size shouldBeExactly 7 }
|
||||||
modifiedDeck.allCardCount() shouldBeExactly totalCardsNumber
|
modifiedDeck.allCardCount() shouldBeExactly totalCardsNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
test("putOneCardFromHand") {
|
test("putOneCardFromHand") {
|
||||||
// Given
|
// Given
|
||||||
val playerNumbers = 4
|
val playerNumbers = 4
|
||||||
val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet()
|
val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet()
|
||||||
val deck = Deck.newWithoutPlayers().initHands(players)
|
val deck = Deck.newWithoutPlayers().initHands(players)
|
||||||
val firstPlayer = players.first()
|
val firstPlayer = players.first()
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val card = deck.playersHands.getHand(firstPlayer)!!.first()
|
val card = deck.playersHands.getHand(firstPlayer)!!.first()
|
||||||
val modifiedDeck = deck.putOneCardFromHand(firstPlayer, card)
|
val modifiedDeck = deck.putOneCardFromHand(firstPlayer, card)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
modifiedDeck.discard.size shouldBeExactly 1
|
modifiedDeck.discard.size shouldBeExactly 1
|
||||||
modifiedDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7)
|
modifiedDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7)
|
||||||
modifiedDeck.playersHands.size shouldBeExactly playerNumbers
|
modifiedDeck.playersHands.size shouldBeExactly playerNumbers
|
||||||
assertNotNull(modifiedDeck.playersHands.getHand(firstPlayer)).size shouldBeExactly 6
|
assertNotNull(modifiedDeck.playersHands.getHand(firstPlayer)).size shouldBeExactly 6
|
||||||
modifiedDeck.playersHands
|
modifiedDeck.playersHands
|
||||||
.filterKeys { it != firstPlayer.id }
|
.filterKeys { it != firstPlayer.id }
|
||||||
.forEach { (_, cards) -> cards.size shouldBeExactly 7 }
|
.forEach { (_, cards) -> cards.size shouldBeExactly 7 }
|
||||||
modifiedDeck.allCardCount() shouldBeExactly totalCardsNumber
|
modifiedDeck.allCardCount() shouldBeExactly totalCardsNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
test("placeFirstCardOnDiscard") {
|
test("placeFirstCardOnDiscard") {
|
||||||
// Given
|
// Given
|
||||||
val playerNumbers = 4
|
val playerNumbers = 4
|
||||||
val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet()
|
val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet()
|
||||||
val deck = Deck.newWithoutPlayers().initHands(players)
|
val deck = Deck.newWithoutPlayers().initHands(players)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val modifiedDeck = deck.placeFirstCardOnDiscard()
|
val modifiedDeck = deck.placeFirstCardOnDiscard()
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
modifiedDeck.discard.size shouldBeExactly 1
|
modifiedDeck.discard.size shouldBeExactly 1
|
||||||
modifiedDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7) - 1
|
modifiedDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7) - 1
|
||||||
modifiedDeck.playersHands.size shouldBeExactly playerNumbers
|
modifiedDeck.playersHands.size shouldBeExactly playerNumbers
|
||||||
modifiedDeck.playersHands
|
modifiedDeck.playersHands
|
||||||
.forEach { (_, cards) -> cards.size shouldBeExactly 7 }
|
.forEach { (_, cards) -> cards.size shouldBeExactly 7 }
|
||||||
modifiedDeck.allCardCount() shouldBeExactly totalCardsNumber
|
modifiedDeck.allCardCount() shouldBeExactly totalCardsNumber
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,37 +5,37 @@ import io.kotest.matchers.ints.shouldBeExactly
|
|||||||
import kotlin.test.assertNotNull
|
import kotlin.test.assertNotNull
|
||||||
|
|
||||||
class PlayerHandKtTest :
|
class PlayerHandKtTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
test("addCards") {
|
test("addCards") {
|
||||||
// Given
|
// Given
|
||||||
val playerNumbers = 4
|
val playerNumbers = 4
|
||||||
val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet()
|
val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet()
|
||||||
val firstPlayer = players.first()
|
val firstPlayer = players.first()
|
||||||
val playersHands = PlayersHands(players)
|
val playersHands = PlayersHands(players)
|
||||||
val card = Card.NumericCard(0, Card.Color.Red)
|
val card = Card.NumericCard(0, Card.Color.Red)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val newHands: PlayersHands = playersHands.addCards(firstPlayer, listOf(card))
|
val newHands: PlayersHands = playersHands.addCards(firstPlayer, listOf(card))
|
||||||
|
|
||||||
assertNotNull(newHands.getHand(firstPlayer)).size shouldBeExactly 1
|
assertNotNull(newHands.getHand(firstPlayer)).size shouldBeExactly 1
|
||||||
assertNotNull(newHands.getHand(players.last())).size shouldBeExactly 0
|
assertNotNull(newHands.getHand(players.last())).size shouldBeExactly 0
|
||||||
}
|
}
|
||||||
|
|
||||||
test("removeCard") {
|
test("removeCard") {
|
||||||
// Given
|
// Given
|
||||||
val playerNumbers = 4
|
val playerNumbers = 4
|
||||||
val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet()
|
val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet()
|
||||||
val firstPlayer = players.first()
|
val firstPlayer = players.first()
|
||||||
val card1 = Card.NumericCard(1, Card.Color.Red)
|
val card1 = Card.NumericCard(1, Card.Color.Red)
|
||||||
val card2 = Card.NumericCard(2, Card.Color.Red)
|
val card2 = Card.NumericCard(2, Card.Color.Red)
|
||||||
val playersHands: PlayersHands =
|
val playersHands: PlayersHands =
|
||||||
PlayersHands(players)
|
PlayersHands(players)
|
||||||
.addCards(firstPlayer, listOf(card1, card2))
|
.addCards(firstPlayer, listOf(card1, card2))
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val newHands: PlayersHands = playersHands.removeCard(firstPlayer, card1)
|
val newHands: PlayersHands = playersHands.removeCard(firstPlayer, card1)
|
||||||
|
|
||||||
assertNotNull(newHands.getHand(firstPlayer)).size shouldBeExactly 1
|
assertNotNull(newHands.getHand(firstPlayer)).size shouldBeExactly 1
|
||||||
assertNotNull(newHands.getHand(players.last())).size shouldBeExactly 0
|
assertNotNull(newHands.getHand(players.last())).size shouldBeExactly 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ package eventDemo.app.entity
|
|||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
|
||||||
class PlayersHandsTest :
|
class PlayersHandsTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
|
|
||||||
xtest("getHand should return the hand of the player") { }
|
xtest("getHand should return the hand of the player") { }
|
||||||
|
|
||||||
xtest("removeCard should remove the card") { }
|
xtest("removeCard should remove the card") { }
|
||||||
|
|
||||||
xtest("addCard should add the card to the correct hand") { }
|
xtest("addCard should add the card to the correct hand") { }
|
||||||
|
|
||||||
xtest("toPlayersHands should build object from map") { }
|
xtest("toPlayersHands should build object from map") { }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package eventDemo.app.event
|
|||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
|
||||||
class GameEventHandlerTest :
|
class GameEventHandlerTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
xtest("handle event should publish the event to the stream") { }
|
xtest("handle event should publish the event to the stream") { }
|
||||||
xtest("handle event should build the registered projection") { }
|
xtest("handle event should build the registered projection") { }
|
||||||
xtest("handle event should publish the event to the bus") { }
|
xtest("handle event should publish the event to the bus") { }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,112 +15,112 @@ import kotlin.test.assertIs
|
|||||||
import kotlin.test.assertNotNull
|
import kotlin.test.assertNotNull
|
||||||
|
|
||||||
class GameStateBuilderTest :
|
class GameStateBuilderTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
test("apply") {
|
test("apply") {
|
||||||
disableShuffleDeck()
|
disableShuffleDeck()
|
||||||
val versionBuilder = VersionBuilderLocal()
|
val versionBuilder = VersionBuilderLocal()
|
||||||
val gameId = GameId()
|
val gameId = GameId()
|
||||||
val player1 = Player(name = "Nikola")
|
val player1 = Player(name = "Nikola")
|
||||||
val player2 = Player(name = "Einstein")
|
val player2 = Player(name = "Einstein")
|
||||||
|
|
||||||
GameState(gameId)
|
GameState(gameId)
|
||||||
.run {
|
.run {
|
||||||
val event =
|
val event =
|
||||||
NewPlayerEvent(
|
NewPlayerEvent(
|
||||||
aggregateId = gameId,
|
aggregateId = gameId,
|
||||||
player = player1,
|
player = player1,
|
||||||
version = versionBuilder.buildNextVersion(gameId),
|
version = versionBuilder.buildNextVersion(gameId),
|
||||||
)
|
)
|
||||||
apply(event).also { state ->
|
apply(event).also { state ->
|
||||||
state.aggregateId shouldBeEqual gameId
|
state.aggregateId shouldBeEqual gameId
|
||||||
state.isReady shouldBeEqual false
|
state.isReady shouldBeEqual false
|
||||||
state.isStarted shouldBeEqual false
|
state.isStarted shouldBeEqual false
|
||||||
}
|
}
|
||||||
}.run {
|
}.run {
|
||||||
val event =
|
val event =
|
||||||
NewPlayerEvent(
|
NewPlayerEvent(
|
||||||
aggregateId = gameId,
|
aggregateId = gameId,
|
||||||
player = player2,
|
player = player2,
|
||||||
version = versionBuilder.buildNextVersion(gameId),
|
version = versionBuilder.buildNextVersion(gameId),
|
||||||
)
|
)
|
||||||
apply(event).also { state ->
|
apply(event).also { state ->
|
||||||
state.aggregateId shouldBeEqual gameId
|
state.aggregateId shouldBeEqual gameId
|
||||||
state.players shouldBeEqual setOf(player1, player2)
|
state.players shouldBeEqual setOf(player1, player2)
|
||||||
}
|
}
|
||||||
}.run {
|
}.run {
|
||||||
val event =
|
val event =
|
||||||
PlayerReadyEvent(
|
PlayerReadyEvent(
|
||||||
aggregateId = gameId,
|
aggregateId = gameId,
|
||||||
player = player1,
|
player = player1,
|
||||||
version = versionBuilder.buildNextVersion(gameId),
|
version = versionBuilder.buildNextVersion(gameId),
|
||||||
)
|
)
|
||||||
apply(event).also { state ->
|
apply(event).also { state ->
|
||||||
state.aggregateId shouldBeEqual gameId
|
state.aggregateId shouldBeEqual gameId
|
||||||
state.readyPlayers shouldBeEqual setOf(player1)
|
state.readyPlayers shouldBeEqual setOf(player1)
|
||||||
}
|
}
|
||||||
}.run {
|
}.run {
|
||||||
val event =
|
val event =
|
||||||
PlayerReadyEvent(
|
PlayerReadyEvent(
|
||||||
aggregateId = gameId,
|
aggregateId = gameId,
|
||||||
player = player2,
|
player = player2,
|
||||||
version = versionBuilder.buildNextVersion(gameId),
|
version = versionBuilder.buildNextVersion(gameId),
|
||||||
)
|
)
|
||||||
apply(event).also { state ->
|
apply(event).also { state ->
|
||||||
state.aggregateId shouldBeEqual gameId
|
state.aggregateId shouldBeEqual gameId
|
||||||
state.readyPlayers shouldBeEqual setOf(player1, player2)
|
state.readyPlayers shouldBeEqual setOf(player1, player2)
|
||||||
state.isReady shouldBeEqual true
|
state.isReady shouldBeEqual true
|
||||||
state.isStarted shouldBeEqual false
|
state.isStarted shouldBeEqual false
|
||||||
}
|
}
|
||||||
}.run {
|
}.run {
|
||||||
val event =
|
val event =
|
||||||
GameStartedEvent.new(
|
GameStartedEvent.new(
|
||||||
id = gameId,
|
id = gameId,
|
||||||
players = setOf(player1, player2),
|
players = setOf(player1, player2),
|
||||||
shuffleIsDisabled = true,
|
shuffleIsDisabled = true,
|
||||||
version = versionBuilder.buildNextVersion(gameId),
|
version = versionBuilder.buildNextVersion(gameId),
|
||||||
)
|
)
|
||||||
apply(event).also { state ->
|
apply(event).also { state ->
|
||||||
state.aggregateId shouldBeEqual gameId
|
state.aggregateId shouldBeEqual gameId
|
||||||
state.isStarted shouldBeEqual true
|
state.isStarted shouldBeEqual true
|
||||||
assertIs<Card.NumericCard>(state.deck.stack.first()).let {
|
assertIs<Card.NumericCard>(state.deck.stack.first()).let {
|
||||||
it.number shouldBeEqual 6
|
it.number shouldBeEqual 6
|
||||||
it.color shouldBeEqual Card.Color.Red
|
it.color shouldBeEqual Card.Color.Red
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.run {
|
}.run {
|
||||||
val playedCard = playableCards(player1)[0]
|
val playedCard = playableCards(player1)[0]
|
||||||
val event =
|
val event =
|
||||||
CardIsPlayedEvent(
|
CardIsPlayedEvent(
|
||||||
aggregateId = gameId,
|
aggregateId = gameId,
|
||||||
card = playedCard,
|
card = playedCard,
|
||||||
player = player1,
|
player = player1,
|
||||||
version = versionBuilder.buildNextVersion(gameId),
|
version = versionBuilder.buildNextVersion(gameId),
|
||||||
)
|
)
|
||||||
apply(event).also { state ->
|
apply(event).also { state ->
|
||||||
state.aggregateId shouldBeEqual gameId
|
state.aggregateId shouldBeEqual gameId
|
||||||
assertNotNull(state.cardOnCurrentStack).card shouldBeEqual playedCard
|
assertNotNull(state.cardOnCurrentStack).card shouldBeEqual playedCard
|
||||||
assertIs<Card.NumericCard>(playedCard).let {
|
assertIs<Card.NumericCard>(playedCard).let {
|
||||||
it.number shouldBeEqual 0
|
it.number shouldBeEqual 0
|
||||||
it.color shouldBeEqual Card.Color.Red
|
it.color shouldBeEqual Card.Color.Red
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.run {
|
}.run {
|
||||||
val playedCard = playableCards(player2)[0]
|
val playedCard = playableCards(player2)[0]
|
||||||
val event =
|
val event =
|
||||||
CardIsPlayedEvent(
|
CardIsPlayedEvent(
|
||||||
aggregateId = gameId,
|
aggregateId = gameId,
|
||||||
card = playedCard,
|
card = playedCard,
|
||||||
player = player2,
|
player = player2,
|
||||||
version = versionBuilder.buildNextVersion(gameId),
|
version = versionBuilder.buildNextVersion(gameId),
|
||||||
)
|
)
|
||||||
apply(event).also { state ->
|
apply(event).also { state ->
|
||||||
state.aggregateId shouldBeEqual gameId
|
state.aggregateId shouldBeEqual gameId
|
||||||
assertNotNull(state.cardOnCurrentStack).card shouldBeEqual playedCard
|
assertNotNull(state.cardOnCurrentStack).card shouldBeEqual playedCard
|
||||||
assertIs<Card.NumericCard>(playedCard).let {
|
assertIs<Card.NumericCard>(playedCard).let {
|
||||||
it.number shouldBeEqual 7
|
it.number shouldBeEqual 7
|
||||||
it.color shouldBeEqual Card.Color.Red
|
it.color shouldBeEqual Card.Color.Red
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -18,113 +18,113 @@ import kotlin.test.assertNotNull
|
|||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
class GameStateRepositoryTest :
|
class GameStateRepositoryTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
val player1 = Player("Tesla")
|
val player1 = Player("Tesla")
|
||||||
val player2 = Player(name = "Einstein")
|
val player2 = Player(name = "Einstein")
|
||||||
|
|
||||||
test("GameStateRepository should build the projection when a new event occurs") {
|
test("GameStateRepository should build the projection when a new event occurs") {
|
||||||
val aggregateId = GameId()
|
val aggregateId = GameId()
|
||||||
koinApplication { modules(appKoinModule) }.koin.apply {
|
koinApplication { modules(appKoinModule) }.koin.apply {
|
||||||
val repo = get<GameStateRepository>()
|
val repo = get<GameStateRepository>()
|
||||||
val eventHandler = get<GameEventHandler>()
|
val eventHandler = get<GameEventHandler>()
|
||||||
eventHandler
|
eventHandler
|
||||||
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
|
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
|
||||||
.also { event ->
|
.also { event ->
|
||||||
assertNotNull(repo.getUntil(event)).also {
|
assertNotNull(repo.getUntil(event)).also {
|
||||||
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
||||||
}
|
|
||||||
assertNotNull(repo.getLast(aggregateId)).also {
|
|
||||||
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
stopKoin()
|
assertNotNull(repo.getLast(aggregateId)).also {
|
||||||
}
|
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
||||||
|
|
||||||
test("get should build the last version of the state") {
|
|
||||||
val aggregateId = GameId()
|
|
||||||
koinApplication { modules(appKoinModule) }.koin.apply {
|
|
||||||
val repo = get<GameStateRepository>()
|
|
||||||
val eventHandler = get<GameEventHandler>()
|
|
||||||
|
|
||||||
eventHandler
|
|
||||||
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
|
|
||||||
.also {
|
|
||||||
assertNotNull(repo.getLast(aggregateId)).also {
|
|
||||||
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eventHandler
|
|
||||||
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) }
|
|
||||||
.also {
|
|
||||||
assertNotNull(repo.getLast(aggregateId)).also {
|
|
||||||
assertNotNull(it.players) shouldBeEqual setOf(player1, player2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
stopKoin()
|
||||||
|
}
|
||||||
|
|
||||||
test("getUntil should build the state until the event") {
|
test("get should build the last version of the state") {
|
||||||
repeat(10) {
|
val aggregateId = GameId()
|
||||||
val aggregateId = GameId()
|
koinApplication { modules(appKoinModule) }.koin.apply {
|
||||||
koinApplication { modules(appKoinModule) }.koin.apply {
|
val repo = get<GameStateRepository>()
|
||||||
val repo = get<GameStateRepository>()
|
val eventHandler = get<GameEventHandler>()
|
||||||
val eventHandler = get<GameEventHandler>()
|
|
||||||
|
|
||||||
val event1 =
|
eventHandler
|
||||||
eventHandler
|
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
|
||||||
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
|
.also {
|
||||||
.also { event1 ->
|
assertNotNull(repo.getLast(aggregateId)).also {
|
||||||
assertNotNull(repo.getUntil(event1)).also {
|
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
||||||
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
eventHandler
|
eventHandler
|
||||||
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) }
|
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) }
|
||||||
.also { event2 ->
|
.also {
|
||||||
assertNotNull(repo.getUntil(event2)).also {
|
assertNotNull(repo.getLast(aggregateId)).also {
|
||||||
assertNotNull(it.players) shouldBeEqual setOf(player1, player2)
|
assertNotNull(it.players) shouldBeEqual setOf(player1, player2)
|
||||||
}
|
}
|
||||||
assertNotNull(repo.getUntil(event1)).also {
|
}
|
||||||
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
test("getUntil should build the state until the event") {
|
||||||
|
repeat(10) {
|
||||||
|
val aggregateId = GameId()
|
||||||
|
koinApplication { modules(appKoinModule) }.koin.apply {
|
||||||
|
val repo = get<GameStateRepository>()
|
||||||
|
val eventHandler = get<GameEventHandler>()
|
||||||
|
|
||||||
|
val event1 =
|
||||||
|
eventHandler
|
||||||
|
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
|
||||||
|
.also { event1 ->
|
||||||
|
assertNotNull(repo.getUntil(event1)).also {
|
||||||
|
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventHandler
|
||||||
|
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) }
|
||||||
|
.also { event2 ->
|
||||||
|
assertNotNull(repo.getUntil(event2)).also {
|
||||||
|
assertNotNull(it.players) shouldBeEqual setOf(player1, player2)
|
||||||
|
}
|
||||||
|
assertNotNull(repo.getUntil(event1)).also {
|
||||||
|
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test("getUntil should be concurrently secure") {
|
test("getUntil should be concurrently secure") {
|
||||||
val aggregateId = GameId()
|
val aggregateId = GameId()
|
||||||
koinApplication { modules(appKoinModule) }.koin.apply {
|
koinApplication { modules(appKoinModule) }.koin.apply {
|
||||||
val repo = get<GameStateRepository>()
|
val repo = get<GameStateRepository>()
|
||||||
val eventHandler = get<GameEventHandler>()
|
val eventHandler = get<GameEventHandler>()
|
||||||
|
|
||||||
(1..10)
|
(1..10)
|
||||||
.map { r ->
|
.map { r ->
|
||||||
GlobalScope
|
GlobalScope
|
||||||
.launch {
|
.launch {
|
||||||
repeat(100) { r2 ->
|
repeat(100) { r2 ->
|
||||||
val playerX = Player("player$r$r2")
|
val playerX = Player("player$r$r2")
|
||||||
eventHandler
|
eventHandler
|
||||||
.handle(aggregateId) {
|
.handle(aggregateId) {
|
||||||
NewPlayerEvent(
|
NewPlayerEvent(
|
||||||
aggregateId = aggregateId,
|
aggregateId = aggregateId,
|
||||||
player = playerX,
|
player = playerX,
|
||||||
version = it,
|
version = it,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}.joinAll()
|
|
||||||
|
|
||||||
repo.getLast(aggregateId).run {
|
|
||||||
lastEventVersion shouldBeEqual 1000
|
|
||||||
players shouldHaveSize 1000
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}.joinAll()
|
||||||
|
|
||||||
xtest("get should be concurrently secure") { }
|
repo.getLast(aggregateId).run {
|
||||||
})
|
lastEventVersion shouldBeEqual 1000
|
||||||
|
players shouldHaveSize 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xtest("get should be concurrently secure") { }
|
||||||
|
})
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ package eventDemo.app.event.projection
|
|||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
|
||||||
class GameStateTest :
|
class GameStateTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
xtest("isReady") { }
|
xtest("isReady") { }
|
||||||
xtest("nextPlayer") { }
|
xtest("nextPlayer") { }
|
||||||
xtest("nextPlayerTurn") { }
|
xtest("nextPlayerTurn") { }
|
||||||
xtest("playerDiffIndex") { }
|
xtest("playerDiffIndex") { }
|
||||||
xtest("cardOnBoardIsForYou") { }
|
xtest("cardOnBoardIsForYou") { }
|
||||||
xtest("playableCards") { }
|
xtest("playableCards") { }
|
||||||
xtest("playerHasNoCardLeft") { }
|
xtest("playerHasNoCardLeft") { }
|
||||||
xtest("canBePlayThisCard") { }
|
xtest("canBePlayThisCard") { }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,151 +20,151 @@ import kotlin.test.assertNotNull
|
|||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
class ProjectionSnapshotRepositoryInMemoryTest :
|
class ProjectionSnapshotRepositoryInMemoryTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
|
|
||||||
test("when call applyAndPutToCache, the getUntil method must be use the built projection cache") {
|
test("when call applyAndPutToCache, the getUntil method must be use the built projection cache") {
|
||||||
val eventStore: EventStore<TestEvents, IdTest> = EventStoreInMemory()
|
val eventStore: EventStore<TestEvents, IdTest> = EventStoreInMemory()
|
||||||
val repo = getSnapshotRepoTest(eventStore)
|
val repo = getSnapshotRepoTest(eventStore)
|
||||||
val aggregateId = IdTest()
|
val aggregateId = IdTest()
|
||||||
|
|
||||||
val eventOther = Event2Test(value2 = "valOther", version = 1, aggregateId = IdTest())
|
val eventOther = Event2Test(value2 = "valOther", version = 1, aggregateId = IdTest())
|
||||||
eventStore.publish(eventOther)
|
eventStore.publish(eventOther)
|
||||||
repo.applyAndPutToCache(eventOther)
|
repo.applyAndPutToCache(eventOther)
|
||||||
assertNotNull(repo.getUntil(eventOther)).also {
|
assertNotNull(repo.getUntil(eventOther)).also {
|
||||||
assertNotNull(it.value) shouldBeEqual "valOther"
|
assertNotNull(it.value) shouldBeEqual "valOther"
|
||||||
}
|
}
|
||||||
|
|
||||||
val event1 = Event1Test(value1 = "val1", version = 1, aggregateId = aggregateId)
|
val event1 = Event1Test(value1 = "val1", version = 1, aggregateId = aggregateId)
|
||||||
eventStore.publish(event1)
|
eventStore.publish(event1)
|
||||||
repo.applyAndPutToCache(event1)
|
repo.applyAndPutToCache(event1)
|
||||||
assertNotNull(repo.getLast(event1.aggregateId)).also {
|
assertNotNull(repo.getLast(event1.aggregateId)).also {
|
||||||
assertNotNull(it.value) shouldBeEqual "val1"
|
assertNotNull(it.value) shouldBeEqual "val1"
|
||||||
}
|
}
|
||||||
assertNotNull(repo.getUntil(event1)).also {
|
assertNotNull(repo.getUntil(event1)).also {
|
||||||
assertNotNull(it.value) shouldBeEqual "val1"
|
assertNotNull(it.value) shouldBeEqual "val1"
|
||||||
}
|
}
|
||||||
|
|
||||||
val event2 = Event2Test(value2 = "val2", version = 2, aggregateId = aggregateId)
|
val event2 = Event2Test(value2 = "val2", version = 2, aggregateId = aggregateId)
|
||||||
eventStore.publish(event2)
|
eventStore.publish(event2)
|
||||||
repo.applyAndPutToCache(event2)
|
repo.applyAndPutToCache(event2)
|
||||||
assertNotNull(repo.getLast(event2.aggregateId)).also {
|
assertNotNull(repo.getLast(event2.aggregateId)).also {
|
||||||
assertNotNull(it.value) shouldBeEqual "val1val2"
|
assertNotNull(it.value) shouldBeEqual "val1val2"
|
||||||
}
|
}
|
||||||
assertNotNull(repo.getUntil(event1)).also {
|
assertNotNull(repo.getUntil(event1)).also {
|
||||||
assertNotNull(it.value) shouldBeEqual "val1"
|
assertNotNull(it.value) shouldBeEqual "val1"
|
||||||
}
|
}
|
||||||
assertNotNull(repo.getUntil(event2)).also {
|
assertNotNull(repo.getUntil(event2)).also {
|
||||||
assertNotNull(it.value) shouldBeEqual "val1val2"
|
assertNotNull(it.value) shouldBeEqual "val1val2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("ProjectionSnapshotRepositoryInMemory should be thread safe") {
|
test("ProjectionSnapshotRepositoryInMemory should be thread safe") {
|
||||||
val eventStore: EventStore<TestEvents, IdTest> = EventStoreInMemory()
|
val eventStore: EventStore<TestEvents, IdTest> = EventStoreInMemory()
|
||||||
val repo = getSnapshotRepoTest(eventStore)
|
val repo = getSnapshotRepoTest(eventStore)
|
||||||
val aggregateId = IdTest()
|
val aggregateId = IdTest()
|
||||||
val versionBuilder = VersionBuilderLocal()
|
val versionBuilder = VersionBuilderLocal()
|
||||||
val lock = ReentrantLock()
|
val lock = ReentrantLock()
|
||||||
(0..9)
|
(0..9)
|
||||||
.map {
|
.map {
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
(1..10).map {
|
(1..10).map {
|
||||||
val eventX =
|
val eventX =
|
||||||
lock.withLock {
|
lock.withLock {
|
||||||
EventXTest(num = 1, version = versionBuilder.buildNextVersion(aggregateId), aggregateId = aggregateId)
|
EventXTest(num = 1, version = versionBuilder.buildNextVersion(aggregateId), aggregateId = aggregateId)
|
||||||
.also { eventStore.publish(it) }
|
|
||||||
}
|
|
||||||
repo.applyAndPutToCache(eventX)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.joinAll()
|
|
||||||
assertNotNull(repo.getLast(aggregateId)).num shouldBeEqual 100
|
|
||||||
}
|
|
||||||
|
|
||||||
test("removeOldSnapshot") {
|
|
||||||
val versionBuilder = VersionBuilderLocal()
|
|
||||||
val eventStore: EventStore<TestEvents, IdTest> = EventStoreInMemory()
|
|
||||||
val repo = getSnapshotRepoTest(eventStore, SnapshotConfig(2))
|
|
||||||
val aggregateId = IdTest()
|
|
||||||
|
|
||||||
fun buildEndSendEventX() {
|
|
||||||
EventXTest(num = 1, version = versionBuilder.buildNextVersion(aggregateId), aggregateId = aggregateId)
|
|
||||||
.also { eventStore.publish(it) }
|
.also { eventStore.publish(it) }
|
||||||
.also { repo.applyAndPutToCache(it) }
|
}
|
||||||
|
repo.applyAndPutToCache(eventX)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}.joinAll()
|
||||||
|
assertNotNull(repo.getLast(aggregateId)).num shouldBeEqual 100
|
||||||
|
}
|
||||||
|
|
||||||
buildEndSendEventX()
|
test("removeOldSnapshot") {
|
||||||
repo.getLast(aggregateId).num shouldBeEqual 1
|
val versionBuilder = VersionBuilderLocal()
|
||||||
buildEndSendEventX()
|
val eventStore: EventStore<TestEvents, IdTest> = EventStoreInMemory()
|
||||||
repo.getLast(aggregateId).num shouldBeEqual 2
|
val repo = getSnapshotRepoTest(eventStore, SnapshotConfig(2))
|
||||||
buildEndSendEventX()
|
val aggregateId = IdTest()
|
||||||
repo.getLast(aggregateId).num shouldBeEqual 3
|
|
||||||
buildEndSendEventX()
|
fun buildEndSendEventX() {
|
||||||
repo.getLast(aggregateId).num shouldBeEqual 4
|
EventXTest(num = 1, version = versionBuilder.buildNextVersion(aggregateId), aggregateId = aggregateId)
|
||||||
}
|
.also { eventStore.publish(it) }
|
||||||
})
|
.also { repo.applyAndPutToCache(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
buildEndSendEventX()
|
||||||
|
repo.getLast(aggregateId).num shouldBeEqual 1
|
||||||
|
buildEndSendEventX()
|
||||||
|
repo.getLast(aggregateId).num shouldBeEqual 2
|
||||||
|
buildEndSendEventX()
|
||||||
|
repo.getLast(aggregateId).num shouldBeEqual 3
|
||||||
|
buildEndSendEventX()
|
||||||
|
repo.getLast(aggregateId).num shouldBeEqual 4
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
@JvmInline
|
@JvmInline
|
||||||
private value class IdTest(
|
private value class IdTest(
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
) : AggregateId
|
) : AggregateId
|
||||||
|
|
||||||
private data class ProjectionTest(
|
private data class ProjectionTest(
|
||||||
override val aggregateId: IdTest,
|
override val aggregateId: IdTest,
|
||||||
override val lastEventVersion: Int = 0,
|
override val lastEventVersion: Int = 0,
|
||||||
var value: String? = null,
|
var value: String? = null,
|
||||||
var num: Int = 0,
|
var num: Int = 0,
|
||||||
) : Projection<IdTest>
|
) : Projection<IdTest>
|
||||||
|
|
||||||
private sealed interface TestEvents : Event<IdTest>
|
private sealed interface TestEvents : Event<IdTest>
|
||||||
|
|
||||||
private data class Event1Test(
|
private data class Event1Test(
|
||||||
override val eventId: UUID = UUID.randomUUID(),
|
override val eventId: UUID = UUID.randomUUID(),
|
||||||
override val aggregateId: IdTest,
|
override val aggregateId: IdTest,
|
||||||
override val createdAt: Instant = Clock.System.now(),
|
override val createdAt: Instant = Clock.System.now(),
|
||||||
override val version: Int,
|
override val version: Int,
|
||||||
val value1: String,
|
val value1: String,
|
||||||
) : TestEvents
|
) : TestEvents
|
||||||
|
|
||||||
private data class Event2Test(
|
private data class Event2Test(
|
||||||
override val eventId: UUID = UUID.randomUUID(),
|
override val eventId: UUID = UUID.randomUUID(),
|
||||||
override val aggregateId: IdTest,
|
override val aggregateId: IdTest,
|
||||||
override val createdAt: Instant = Clock.System.now(),
|
override val createdAt: Instant = Clock.System.now(),
|
||||||
override val version: Int,
|
override val version: Int,
|
||||||
val value2: String,
|
val value2: String,
|
||||||
) : TestEvents
|
) : TestEvents
|
||||||
|
|
||||||
private data class EventXTest(
|
private data class EventXTest(
|
||||||
override val eventId: UUID = UUID.randomUUID(),
|
override val eventId: UUID = UUID.randomUUID(),
|
||||||
override val aggregateId: IdTest,
|
override val aggregateId: IdTest,
|
||||||
override val createdAt: Instant = Clock.System.now(),
|
override val createdAt: Instant = Clock.System.now(),
|
||||||
override val version: Int,
|
override val version: Int,
|
||||||
val num: Int,
|
val num: Int,
|
||||||
) : TestEvents
|
) : TestEvents
|
||||||
|
|
||||||
private fun getSnapshotRepoTest(
|
private fun getSnapshotRepoTest(
|
||||||
eventStore: EventStore<TestEvents, IdTest>,
|
eventStore: EventStore<TestEvents, IdTest>,
|
||||||
snapshotConfig: SnapshotConfig = SnapshotConfig(2000),
|
snapshotConfig: SnapshotConfig = SnapshotConfig(2000),
|
||||||
): ProjectionSnapshotRepositoryInMemory<TestEvents, ProjectionTest, IdTest> =
|
): ProjectionSnapshotRepositoryInMemory<TestEvents, ProjectionTest, IdTest> =
|
||||||
ProjectionSnapshotRepositoryInMemory(
|
ProjectionSnapshotRepositoryInMemory(
|
||||||
eventStore = eventStore,
|
eventStore = eventStore,
|
||||||
initialStateBuilder = { aggregateId: IdTest -> ProjectionTest(aggregateId) },
|
initialStateBuilder = { aggregateId: IdTest -> ProjectionTest(aggregateId) },
|
||||||
snapshotCacheConfig = snapshotConfig,
|
snapshotCacheConfig = snapshotConfig,
|
||||||
) { event ->
|
) { event ->
|
||||||
this.let { projection ->
|
this.let { projection ->
|
||||||
when (event) {
|
when (event) {
|
||||||
is Event1Test -> {
|
is Event1Test -> {
|
||||||
projection.copy(value = (projection.value ?: "") + event.value1)
|
projection.copy(value = (projection.value ?: "") + event.value1)
|
||||||
}
|
|
||||||
|
|
||||||
is Event2Test -> {
|
|
||||||
projection.copy(value = (projection.value ?: "") + event.value2)
|
|
||||||
}
|
|
||||||
|
|
||||||
is EventXTest -> {
|
|
||||||
projection.copy(num = projection.num + event.num)
|
|
||||||
}
|
|
||||||
}.copy(
|
|
||||||
lastEventVersion = event.version,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is Event2Test -> {
|
||||||
|
projection.copy(value = (projection.value ?: "") + event.value2)
|
||||||
|
}
|
||||||
|
|
||||||
|
is EventXTest -> {
|
||||||
|
projection.copy(num = projection.num + event.num)
|
||||||
|
}
|
||||||
|
}.copy(
|
||||||
|
lastEventVersion = event.version,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,89 +33,89 @@ import kotlin.test.assertIs
|
|||||||
import kotlin.test.assertNotNull
|
import kotlin.test.assertNotNull
|
||||||
|
|
||||||
class GameStateRouteTest :
|
class GameStateRouteTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
test("/game/{id}/state on empty game") {
|
test("/game/{id}/state on empty game") {
|
||||||
testApplication {
|
testApplication {
|
||||||
val id = GameId()
|
val id = GameId()
|
||||||
val player1 = Player(name = "Nikola")
|
val player1 = Player(name = "Nikola")
|
||||||
application {
|
application {
|
||||||
stopKoin()
|
stopKoin()
|
||||||
configure()
|
configure()
|
||||||
}
|
|
||||||
|
|
||||||
httpClient()
|
|
||||||
.get("/game/$id/state") {
|
|
||||||
withAuth(player1)
|
|
||||||
accept(ContentType.Application.Json)
|
|
||||||
}.apply {
|
|
||||||
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
|
|
||||||
val state = call.body<GameState>()
|
|
||||||
id shouldBeEqual state.aggregateId
|
|
||||||
state.players shouldHaveSize 0
|
|
||||||
state.isStarted shouldBeEqual false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test("/game/{id}/card/last") {
|
httpClient()
|
||||||
testApplication {
|
.get("/game/$id/state") {
|
||||||
val gameId = GameId()
|
withAuth(player1)
|
||||||
val player1 = Player(name = "Nikola")
|
accept(ContentType.Application.Json)
|
||||||
val player2 = Player(name = "Einstein")
|
}.apply {
|
||||||
var lastPlayedCard: Card? = null
|
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
|
||||||
|
val state = call.body<GameState>()
|
||||||
|
id shouldBeEqual state.aggregateId
|
||||||
|
state.players shouldHaveSize 0
|
||||||
|
state.isStarted shouldBeEqual false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
application {
|
test("/game/{id}/card/last") {
|
||||||
stopKoin()
|
testApplication {
|
||||||
configure()
|
val gameId = GameId()
|
||||||
|
val player1 = Player(name = "Nikola")
|
||||||
|
val player2 = Player(name = "Einstein")
|
||||||
|
var lastPlayedCard: Card? = null
|
||||||
|
|
||||||
val eventHandler by inject<GameEventHandler>()
|
application {
|
||||||
val stateRepo by inject<GameStateRepository>()
|
stopKoin()
|
||||||
runBlocking {
|
configure()
|
||||||
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player1, it) }
|
|
||||||
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player2, it) }
|
|
||||||
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player1, it) }
|
|
||||||
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player2, it) }
|
|
||||||
eventHandler.handle(gameId) {
|
|
||||||
GameStartedEvent.new(
|
|
||||||
gameId,
|
|
||||||
setOf(player1, player2),
|
|
||||||
shuffleIsDisabled = true,
|
|
||||||
it,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
delay(100)
|
|
||||||
lastPlayedCard = stateRepo.getLast(gameId).playableCards(player1).first()
|
|
||||||
assertNotNull(lastPlayedCard)
|
|
||||||
.let { assertIs<Card.NumericCard>(lastPlayedCard) }
|
|
||||||
.let {
|
|
||||||
it.number shouldBeEqual 0
|
|
||||||
it.color shouldBeEqual Card.Color.Red
|
|
||||||
}
|
|
||||||
delay(100)
|
|
||||||
eventHandler.handle(gameId) {
|
|
||||||
CardIsPlayedEvent(
|
|
||||||
gameId,
|
|
||||||
assertNotNull(lastPlayedCard),
|
|
||||||
player1,
|
|
||||||
it,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
delay(100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
httpClient()
|
val eventHandler by inject<GameEventHandler>()
|
||||||
.get("/game/$gameId/card/last") {
|
val stateRepo by inject<GameStateRepository>()
|
||||||
withAuth(player1)
|
runBlocking {
|
||||||
accept(ContentType.Application.Json)
|
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player1, it) }
|
||||||
}.apply {
|
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player2, it) }
|
||||||
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
|
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player1, it) }
|
||||||
assertEquals(assertNotNull(lastPlayedCard), call.body<Card>())
|
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player2, it) }
|
||||||
}
|
eventHandler.handle(gameId) {
|
||||||
|
GameStartedEvent.new(
|
||||||
|
gameId,
|
||||||
|
setOf(player1, player2),
|
||||||
|
shuffleIsDisabled = true,
|
||||||
|
it,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
delay(100)
|
||||||
|
lastPlayedCard = stateRepo.getLast(gameId).playableCards(player1).first()
|
||||||
|
assertNotNull(lastPlayedCard)
|
||||||
|
.let { assertIs<Card.NumericCard>(lastPlayedCard) }
|
||||||
|
.let {
|
||||||
|
it.number shouldBeEqual 0
|
||||||
|
it.color shouldBeEqual Card.Color.Red
|
||||||
|
}
|
||||||
|
delay(100)
|
||||||
|
eventHandler.handle(gameId) {
|
||||||
|
CardIsPlayedEvent(
|
||||||
|
gameId,
|
||||||
|
assertNotNull(lastPlayedCard),
|
||||||
|
player1,
|
||||||
|
it,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
delay(100)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
httpClient()
|
||||||
|
.get("/game/$gameId/card/last") {
|
||||||
|
withAuth(player1)
|
||||||
|
accept(ContentType.Application.Json)
|
||||||
|
}.apply {
|
||||||
|
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
|
||||||
|
assertEquals(assertNotNull(lastPlayedCard), call.body<Card>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
private fun HttpRequestBuilder.withAuth(player: Player) {
|
private fun HttpRequestBuilder.withAuth(player: Player) {
|
||||||
header("Authorization", "Bearer ${player.makeJwt()}")
|
header("Authorization", "Bearer ${player.makeJwt()}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,127 +42,127 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
|
|
||||||
@DelicateCoroutinesApi
|
@DelicateCoroutinesApi
|
||||||
class GameStateTest :
|
class GameStateTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
test("Simulation of a game") {
|
test("Simulation of a game") {
|
||||||
withTimeout(2.seconds) {
|
withTimeout(2.seconds) {
|
||||||
disableShuffleDeck()
|
disableShuffleDeck()
|
||||||
val id = GameId()
|
val id = GameId()
|
||||||
val player1 = Player(name = "Nikola")
|
val player1 = Player(name = "Nikola")
|
||||||
val player2 = Player(name = "Einstein")
|
val player2 = Player(name = "Einstein")
|
||||||
val channelCommand1 = Channel<GameCommand>(Channel.BUFFERED)
|
val channelCommand1 = Channel<GameCommand>(Channel.BUFFERED)
|
||||||
val channelCommand2 = Channel<GameCommand>(Channel.BUFFERED)
|
val channelCommand2 = Channel<GameCommand>(Channel.BUFFERED)
|
||||||
val channelNotification1 = Channel<Notification>(Channel.BUFFERED)
|
val channelNotification1 = Channel<Notification>(Channel.BUFFERED)
|
||||||
val channelNotification2 = Channel<Notification>(Channel.BUFFERED)
|
val channelNotification2 = Channel<Notification>(Channel.BUFFERED)
|
||||||
|
|
||||||
var playedCard1: Card? = null
|
var playedCard1: Card? = null
|
||||||
var playedCard2: Card? = null
|
var playedCard2: Card? = null
|
||||||
|
|
||||||
val player1Job =
|
val player1Job =
|
||||||
launch {
|
launch {
|
||||||
channelCommand1.send(IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(id, player1)))
|
channelCommand1.send(IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(id, player1)))
|
||||||
channelNotification1.receive().let {
|
channelNotification1.receive().let {
|
||||||
assertIs<WelcomeToTheGameNotification>(it).players shouldBeEqual setOf(player1)
|
assertIs<WelcomeToTheGameNotification>(it).players shouldBeEqual setOf(player1)
|
||||||
}
|
|
||||||
channelNotification1.receive().let {
|
|
||||||
assertIs<PlayerAsJoinTheGameNotification>(it).player shouldBeEqual player2
|
|
||||||
}
|
|
||||||
channelCommand1.send(IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(id, player1)))
|
|
||||||
channelNotification1.receive().let {
|
|
||||||
assertIs<PlayerWasReadyNotification>(it).player shouldBeEqual player2
|
|
||||||
}
|
|
||||||
val player1Hand =
|
|
||||||
channelNotification1.receive().let {
|
|
||||||
assertIs<TheGameWasStartedNotification>(it).hand shouldHaveSize 7
|
|
||||||
}
|
|
||||||
playedCard1 = player1Hand.first()
|
|
||||||
channelNotification1.receive().let {
|
|
||||||
assertIs<ItsTheTurnOfNotification>(it).apply {
|
|
||||||
player shouldBeEqual player1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
channelCommand1.send(IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(id, player1, player1Hand.first())))
|
|
||||||
|
|
||||||
channelNotification1.receive().let {
|
|
||||||
assertIs<ItsTheTurnOfNotification>(it).apply {
|
|
||||||
player shouldBeEqual player2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
channelNotification1.receive().let {
|
|
||||||
assertIs<PlayerAsPlayACardNotification>(it).apply {
|
|
||||||
player shouldBeEqual player2
|
|
||||||
card shouldBeEqual assertNotNull(playedCard2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val player2Job =
|
|
||||||
launch {
|
|
||||||
delay(100)
|
|
||||||
channelCommand2.send(IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(id, player2)))
|
|
||||||
channelNotification2.receive().let {
|
|
||||||
assertIs<WelcomeToTheGameNotification>(it).players shouldBeEqual setOf(player1, player2)
|
|
||||||
}
|
|
||||||
channelNotification2.receive().let {
|
|
||||||
assertIs<PlayerWasReadyNotification>(it).player shouldBeEqual player1
|
|
||||||
}
|
|
||||||
channelCommand2.send(IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(id, player2)))
|
|
||||||
val player2Hand =
|
|
||||||
channelNotification2.receive().let {
|
|
||||||
assertIs<TheGameWasStartedNotification>(it).hand shouldHaveSize 7
|
|
||||||
}
|
|
||||||
channelNotification2.receive().let {
|
|
||||||
assertIs<ItsTheTurnOfNotification>(it).apply {
|
|
||||||
player shouldBeEqual player1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
channelNotification2.receive().let {
|
|
||||||
assertIs<PlayerAsPlayACardNotification>(it).apply {
|
|
||||||
player shouldBeEqual player1
|
|
||||||
card shouldBeEqual assertNotNull(playedCard1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
playedCard2 = player2Hand.first()
|
|
||||||
|
|
||||||
channelNotification2.receive().let {
|
|
||||||
assertIs<ItsTheTurnOfNotification>(it).apply {
|
|
||||||
player shouldBeEqual player2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
channelCommand2.send(IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(id, player2, player2Hand.first())))
|
|
||||||
}
|
|
||||||
|
|
||||||
koinApplication { modules(appKoinModule) }.koin.apply {
|
|
||||||
val commandHandler by inject<GameCommandHandler>()
|
|
||||||
val eventStore by inject<GameEventStore>()
|
|
||||||
val playerNotificationListener by inject<PlayerNotificationEventListener>()
|
|
||||||
ReactionEventListener(get(), get(), get()).init()
|
|
||||||
playerNotificationListener.startListening(channelNotification1, player1)
|
|
||||||
playerNotificationListener.startListening(channelNotification2, player2)
|
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
commandHandler.handle(player1, channelCommand1, channelNotification1)
|
|
||||||
}
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
commandHandler.handle(player2, channelCommand2, channelNotification2)
|
|
||||||
}
|
|
||||||
|
|
||||||
joinAll(player1Job, player2Job)
|
|
||||||
|
|
||||||
val state =
|
|
||||||
ProjectionSnapshotRepositoryInMemory(
|
|
||||||
eventStore = eventStore,
|
|
||||||
initialStateBuilder = { aggregateId: GameId -> GameState(aggregateId) },
|
|
||||||
applyToProjection = GameState::apply,
|
|
||||||
).getLast(id)
|
|
||||||
|
|
||||||
state.aggregateId shouldBeEqual id
|
|
||||||
assertTrue(state.isStarted)
|
|
||||||
state.players shouldBeEqual setOf(player1, player2)
|
|
||||||
state.readyPlayers shouldBeEqual setOf(player1, player2)
|
|
||||||
state.direction shouldBeEqual GameState.Direction.CLOCKWISE
|
|
||||||
assertNotNull(state.cardOnCurrentStack) shouldBeEqual GameState.LastCard(assertNotNull(playedCard2), player2)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
channelNotification1.receive().let {
|
||||||
|
assertIs<PlayerAsJoinTheGameNotification>(it).player shouldBeEqual player2
|
||||||
|
}
|
||||||
|
channelCommand1.send(IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(id, player1)))
|
||||||
|
channelNotification1.receive().let {
|
||||||
|
assertIs<PlayerWasReadyNotification>(it).player shouldBeEqual player2
|
||||||
|
}
|
||||||
|
val player1Hand =
|
||||||
|
channelNotification1.receive().let {
|
||||||
|
assertIs<TheGameWasStartedNotification>(it).hand shouldHaveSize 7
|
||||||
|
}
|
||||||
|
playedCard1 = player1Hand.first()
|
||||||
|
channelNotification1.receive().let {
|
||||||
|
assertIs<ItsTheTurnOfNotification>(it).apply {
|
||||||
|
player shouldBeEqual player1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
channelCommand1.send(IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(id, player1, player1Hand.first())))
|
||||||
|
|
||||||
|
channelNotification1.receive().let {
|
||||||
|
assertIs<ItsTheTurnOfNotification>(it).apply {
|
||||||
|
player shouldBeEqual player2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channelNotification1.receive().let {
|
||||||
|
assertIs<PlayerAsPlayACardNotification>(it).apply {
|
||||||
|
player shouldBeEqual player2
|
||||||
|
card shouldBeEqual assertNotNull(playedCard2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val player2Job =
|
||||||
|
launch {
|
||||||
|
delay(100)
|
||||||
|
channelCommand2.send(IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(id, player2)))
|
||||||
|
channelNotification2.receive().let {
|
||||||
|
assertIs<WelcomeToTheGameNotification>(it).players shouldBeEqual setOf(player1, player2)
|
||||||
|
}
|
||||||
|
channelNotification2.receive().let {
|
||||||
|
assertIs<PlayerWasReadyNotification>(it).player shouldBeEqual player1
|
||||||
|
}
|
||||||
|
channelCommand2.send(IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(id, player2)))
|
||||||
|
val player2Hand =
|
||||||
|
channelNotification2.receive().let {
|
||||||
|
assertIs<TheGameWasStartedNotification>(it).hand shouldHaveSize 7
|
||||||
|
}
|
||||||
|
channelNotification2.receive().let {
|
||||||
|
assertIs<ItsTheTurnOfNotification>(it).apply {
|
||||||
|
player shouldBeEqual player1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
channelNotification2.receive().let {
|
||||||
|
assertIs<PlayerAsPlayACardNotification>(it).apply {
|
||||||
|
player shouldBeEqual player1
|
||||||
|
card shouldBeEqual assertNotNull(playedCard1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
playedCard2 = player2Hand.first()
|
||||||
|
|
||||||
|
channelNotification2.receive().let {
|
||||||
|
assertIs<ItsTheTurnOfNotification>(it).apply {
|
||||||
|
player shouldBeEqual player2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
channelCommand2.send(IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(id, player2, player2Hand.first())))
|
||||||
|
}
|
||||||
|
|
||||||
|
koinApplication { modules(appKoinModule) }.koin.apply {
|
||||||
|
val commandHandler by inject<GameCommandHandler>()
|
||||||
|
val eventStore by inject<GameEventStore>()
|
||||||
|
val playerNotificationListener by inject<PlayerNotificationEventListener>()
|
||||||
|
ReactionEventListener(get(), get(), get()).init()
|
||||||
|
playerNotificationListener.startListening(channelNotification1, player1)
|
||||||
|
playerNotificationListener.startListening(channelNotification2, player2)
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
commandHandler.handle(player1, channelCommand1, channelNotification1)
|
||||||
|
}
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
commandHandler.handle(player2, channelCommand2, channelNotification2)
|
||||||
|
}
|
||||||
|
|
||||||
|
joinAll(player1Job, player2Job)
|
||||||
|
|
||||||
|
val state =
|
||||||
|
ProjectionSnapshotRepositoryInMemory(
|
||||||
|
eventStore = eventStore,
|
||||||
|
initialStateBuilder = { aggregateId: GameId -> GameState(aggregateId) },
|
||||||
|
applyToProjection = GameState::apply,
|
||||||
|
).getLast(id)
|
||||||
|
|
||||||
|
state.aggregateId shouldBeEqual id
|
||||||
|
assertTrue(state.isStarted)
|
||||||
|
state.players shouldBeEqual setOf(player1, player2)
|
||||||
|
state.readyPlayers shouldBeEqual setOf(player1, player2)
|
||||||
|
state.direction shouldBeEqual GameState.Direction.CLOCKWISE
|
||||||
|
assertNotNull(state.cardOnCurrentStack) shouldBeEqual GameState.LastCard(assertNotNull(playedCard2), player2)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import io.ktor.serialization.kotlinx.json.json
|
|||||||
import io.ktor.server.testing.ApplicationTestBuilder
|
import io.ktor.server.testing.ApplicationTestBuilder
|
||||||
|
|
||||||
fun ApplicationTestBuilder.httpClient(): HttpClient =
|
fun ApplicationTestBuilder.httpClient(): HttpClient =
|
||||||
createClient {
|
createClient {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(
|
json(
|
||||||
defaultJsonSerializer(),
|
defaultJsonSerializer(),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,42 +14,42 @@ import kotlin.test.assertIs
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CommandTest(
|
data class CommandTest(
|
||||||
override val id: CommandId,
|
override val id: CommandId,
|
||||||
) : Command
|
) : Command
|
||||||
|
|
||||||
class FrameChannelConverterTest :
|
class FrameChannelConverterTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
|
|
||||||
test("toObjectChannel") {
|
test("toObjectChannel") {
|
||||||
val uuid = "d737c631-76af-406e-bc29-f3e5b97226a5"
|
val uuid = "d737c631-76af-406e-bc29-f3e5b97226a5"
|
||||||
val id = CommandId(UUID.fromString(uuid))
|
val id = CommandId(UUID.fromString(uuid))
|
||||||
val jsonCommand = """{"id":"$uuid"}"""
|
val jsonCommand = """{"id":"$uuid"}"""
|
||||||
|
|
||||||
val channel = Channel<Frame>()
|
val channel = Channel<Frame>()
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
val commandChannel = toObjectChannel<CommandTest>(channel)
|
val commandChannel = toObjectChannel<CommandTest>(channel)
|
||||||
commandChannel.receive().id shouldBeEqual id
|
commandChannel.receive().id shouldBeEqual id
|
||||||
channel.close()
|
channel.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.send(Frame.Text(jsonCommand))
|
channel.send(Frame.Text(jsonCommand))
|
||||||
}
|
}
|
||||||
|
|
||||||
test("fromFrameChannel") {
|
test("fromFrameChannel") {
|
||||||
val uuid = "d737c631-76af-406e-bc29-f3e5b97226a5"
|
val uuid = "d737c631-76af-406e-bc29-f3e5b97226a5"
|
||||||
val id = CommandId(UUID.fromString(uuid))
|
val id = CommandId(UUID.fromString(uuid))
|
||||||
val command = CommandTest(id)
|
val command = CommandTest(id)
|
||||||
val jsonCommand = """{"id":"$uuid"}"""
|
val jsonCommand = """{"id":"$uuid"}"""
|
||||||
|
|
||||||
val channel = Channel<Frame>()
|
val channel = Channel<Frame>()
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
val commandChannel = fromFrameChannel<CommandTest>(channel)
|
val commandChannel = fromFrameChannel<CommandTest>(channel)
|
||||||
commandChannel.send(command)
|
commandChannel.send(command)
|
||||||
commandChannel.close()
|
commandChannel.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
assertIs<Frame.Text>(channel.receive()).readText() shouldBeEqual jsonCommand
|
assertIs<Frame.Text>(channel.receive()).readText() shouldBeEqual jsonCommand
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,26 +8,26 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class CommandTest(
|
class CommandTest(
|
||||||
override val id: CommandId,
|
override val id: CommandId,
|
||||||
) : Command
|
) : Command
|
||||||
|
|
||||||
class CommandStreamChannelTest :
|
class CommandStreamChannelTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
|
|
||||||
test("send and receive") {
|
test("send and receive") {
|
||||||
val command = CommandTest(CommandId())
|
val command = CommandTest(CommandId())
|
||||||
|
|
||||||
val channel = Channel<CommandTest>()
|
val channel = Channel<CommandTest>()
|
||||||
val stream =
|
val stream =
|
||||||
CommandStreamChannel(channel)
|
CommandStreamChannel(channel)
|
||||||
|
|
||||||
val spyCall: () -> Unit = mockk(relaxed = true)
|
val spyCall: () -> Unit = mockk(relaxed = true)
|
||||||
|
|
||||||
stream.blockAndProcess {
|
stream.blockAndProcess {
|
||||||
println("In action ${it.id}")
|
println("In action ${it.id}")
|
||||||
spyCall()
|
spyCall()
|
||||||
}
|
}
|
||||||
channel.send(command)
|
channel.send(command)
|
||||||
verify(exactly = 1) { spyCall() }
|
verify(exactly = 1) { spyCall() }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package eventDemo.libs.event
|
|||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
|
||||||
class EventBusInMemoryTest :
|
class EventBusInMemoryTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
|
|
||||||
xtest("publish should call the subscribed functions") { }
|
xtest("publish should call the subscribed functions") { }
|
||||||
xtest("publish should call the subscribed functions on the priority order") { }
|
xtest("publish should call the subscribed functions on the priority order") { }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ package eventDemo.libs.event
|
|||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
|
||||||
class EventStreamInMemoryTest :
|
class EventStreamInMemoryTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
|
|
||||||
xtest("publish should be concurrently secure") { }
|
xtest("publish should be concurrently secure") { }
|
||||||
|
|
||||||
xtest("readLast should only return the event of aggregate") { }
|
xtest("readLast should only return the event of aggregate") { }
|
||||||
xtest("readLast should return the last event of the aggregate") { }
|
xtest("readLast should return the last event of the aggregate") { }
|
||||||
|
|
||||||
xtest("readLastOf should return the last event of the aggregate of the type") { }
|
xtest("readLastOf should return the last event of the aggregate of the type") { }
|
||||||
|
|
||||||
xtest("readAll should only return the event of aggregate") { }
|
xtest("readAll should only return the event of aggregate") { }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,43 +10,43 @@ import java.util.UUID
|
|||||||
|
|
||||||
@JvmInline
|
@JvmInline
|
||||||
private value class IdTest(
|
private value class IdTest(
|
||||||
override val id: UUID = UUID.randomUUID(),
|
override val id: UUID = UUID.randomUUID(),
|
||||||
) : AggregateId
|
) : AggregateId
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
class VersionBuilderLocalTest :
|
class VersionBuilderLocalTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
|
|
||||||
test("buildNextVersion") {
|
test("buildNextVersion") {
|
||||||
VersionBuilderLocal().run {
|
VersionBuilderLocal().run {
|
||||||
val id = IdTest()
|
val id = IdTest()
|
||||||
buildNextVersion(id) shouldBeEqual 1
|
buildNextVersion(id) shouldBeEqual 1
|
||||||
buildNextVersion(id) shouldBeEqual 2
|
buildNextVersion(id) shouldBeEqual 2
|
||||||
buildNextVersion(IdTest()) shouldBeEqual 1
|
buildNextVersion(IdTest()) shouldBeEqual 1
|
||||||
buildNextVersion(id) shouldBeEqual 3
|
buildNextVersion(id) shouldBeEqual 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("buildNextVersion concurrently") {
|
||||||
|
val versionBuilder = VersionBuilderLocal()
|
||||||
|
val id = IdTest()
|
||||||
|
(1..20)
|
||||||
|
.map {
|
||||||
|
GlobalScope.launch {
|
||||||
|
(1..1000).map {
|
||||||
|
versionBuilder.buildNextVersion(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}.joinAll()
|
||||||
|
versionBuilder.getLastVersion(id) shouldBeEqual 20 * 1000
|
||||||
|
}
|
||||||
|
|
||||||
test("buildNextVersion concurrently") {
|
test("getLastVersion") {
|
||||||
val versionBuilder = VersionBuilderLocal()
|
VersionBuilderLocal().run {
|
||||||
val id = IdTest()
|
val id = IdTest()
|
||||||
(1..20)
|
getLastVersion(id) shouldBeEqual 0
|
||||||
.map {
|
getLastVersion(id) shouldBeEqual 0
|
||||||
GlobalScope.launch {
|
getLastVersion(id) shouldBeEqual 0
|
||||||
(1..1000).map {
|
}
|
||||||
versionBuilder.buildNextVersion(id)
|
}
|
||||||
}
|
})
|
||||||
}
|
|
||||||
}.joinAll()
|
|
||||||
versionBuilder.getLastVersion(id) shouldBeEqual 20 * 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
test("getLastVersion") {
|
|
||||||
VersionBuilderLocal().run {
|
|
||||||
val id = IdTest()
|
|
||||||
getLastVersion(id) shouldBeEqual 0
|
|
||||||
getLastVersion(id) shouldBeEqual 0
|
|
||||||
getLastVersion(id) shouldBeEqual 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|||||||
Reference in New Issue
Block a user