Create test for complete game
create notifications for reply the players implement notifications on GameEventPlayerNotificationListener add priority to the eventbus.subscribe() improve JWT creation update libs koin + ktor remove output of GameCommandStream improve logs create a function disableShuffleDeck to disable the shuffle of the deck (for tests)
This commit is contained in:
@@ -54,11 +54,15 @@ dependencies {
|
|||||||
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("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")
|
||||||
|
|
||||||
|
testImplementation("io.kotest:kotest-extensions-koin:6.0.0.M2")
|
||||||
testImplementation("io.ktor:ktor-server-tests-jvm")
|
testImplementation("io.ktor:ktor-server-tests-jvm")
|
||||||
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")
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
ktor_version=2.3.13
|
ktor_version=3.0.3
|
||||||
#ktor_version=3.0.3
|
|
||||||
kotlin_version=2.1.10
|
kotlin_version=2.1.10
|
||||||
kotlin_serialization_version=1.8.0
|
kotlin_serialization_version=1.8.0
|
||||||
logback_version=1.5.16
|
logback_version=1.5.16
|
||||||
koin_version=3.5.6
|
koin_version=4.0.2
|
||||||
# koin_version=4.0.2
|
|
||||||
kotlin_logging_version=5.1.0
|
kotlin_logging_version=5.1.0
|
||||||
#kotlin_logging_version=7.0.4
|
#kotlin_logging_version=7.0.4
|
||||||
kotest_version=5.9.1
|
kotest_version=5.9.1
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ data class GameState(
|
|||||||
val lastCard: LastCard? = null,
|
val lastCard: LastCard? = null,
|
||||||
val lastColor: Card.Color? = null,
|
val lastColor: Card.Color? = null,
|
||||||
val direction: Direction = Direction.CLOCKWISE,
|
val direction: Direction = Direction.CLOCKWISE,
|
||||||
val readyPlayers: List<Player> = emptyList(),
|
val readyPlayers: Set<Player> = emptySet(),
|
||||||
val deck: Deck = Deck(players),
|
val deck: Deck = Deck(players),
|
||||||
val isStarted: Boolean = false,
|
val isStarted: Boolean = false,
|
||||||
) {
|
) {
|
||||||
@@ -51,6 +51,8 @@ data class GameState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val nextPlayerIndex: Int get() {
|
private val nextPlayerIndex: Int get() {
|
||||||
|
if (players.size == 0) return 0
|
||||||
|
|
||||||
val y =
|
val y =
|
||||||
if (direction == Direction.CLOCKWISE) {
|
if (direction == Direction.CLOCKWISE) {
|
||||||
+1
|
+1
|
||||||
@@ -61,7 +63,13 @@ data class GameState(
|
|||||||
return ((lastPlayerIndex ?: 0) + y) % players.size
|
return ((lastPlayerIndex ?: 0) + y) % players.size
|
||||||
}
|
}
|
||||||
|
|
||||||
val nextPlayer: Player = players.elementAt(nextPlayerIndex)
|
val nextPlayer: Player? by lazy {
|
||||||
|
if (players.isEmpty()) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
players.elementAt(nextPlayerIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val Player.currentIndex: Int get() = players.indexOf(this)
|
val Player.currentIndex: Int get() = players.indexOf(this)
|
||||||
|
|
||||||
@@ -79,7 +87,8 @@ data class GameState(
|
|||||||
|
|
||||||
fun playableCards(player: Player): List<Card> =
|
fun playableCards(player: Player): List<Card> =
|
||||||
deck
|
deck
|
||||||
.playersHands[player]
|
.playersHands
|
||||||
|
.getHand(player)
|
||||||
?.filter { canBePlayThisCard(player, it) }
|
?.filter { canBePlayThisCard(player, it) }
|
||||||
?: emptyList()
|
?: emptyList()
|
||||||
|
|
||||||
|
|||||||
@@ -10,15 +10,13 @@ import eventDemo.app.entity.Player
|
|||||||
import eventDemo.app.event.GameEventStream
|
import eventDemo.app.event.GameEventStream
|
||||||
import eventDemo.app.event.buildStateFromEventStream
|
import eventDemo.app.event.buildStateFromEventStream
|
||||||
import eventDemo.app.event.event.GameEvent
|
import eventDemo.app.event.event.GameEvent
|
||||||
import eventDemo.libs.command.CommandBlock
|
import eventDemo.app.notification.ErrorNotification
|
||||||
|
import eventDemo.shared.toFrame
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.ktor.websocket.Frame
|
import io.ktor.websocket.Frame
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.channels.ReceiveChannel
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
import kotlinx.coroutines.channels.SendChannel
|
import kotlinx.coroutines.channels.SendChannel
|
||||||
import kotlinx.coroutines.channels.trySendBlocking
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listen [GameCommand] on [GameCommandStream], check the validity and execute an action.
|
* Listen [GameCommand] on [GameCommandStream], check the validity and execute an action.
|
||||||
@@ -28,26 +26,32 @@ import kotlinx.coroutines.launch
|
|||||||
class GameCommandHandler(
|
class GameCommandHandler(
|
||||||
private val eventStream: GameEventStream,
|
private val eventStream: GameEventStream,
|
||||||
) {
|
) {
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init the handler
|
* Init the handler
|
||||||
*/
|
*/
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
suspend fun handle(
|
||||||
fun handle(
|
|
||||||
player: Player,
|
player: Player,
|
||||||
incoming: ReceiveChannel<Frame>,
|
incoming: ReceiveChannel<Frame>,
|
||||||
outgoing: SendChannel<Frame>,
|
outgoing: SendChannel<Frame>,
|
||||||
): Job {
|
) {
|
||||||
val commandStream = GameCommandStream(incoming, outgoing)
|
val commandStream = GameCommandStream(incoming)
|
||||||
val playerNotifier: (String) -> Unit = { outgoing.trySendBlocking(Frame.Text(it)) }
|
val playerErrorNotifier: (String) -> Unit = {
|
||||||
return GlobalScope.launch {
|
val notification = ErrorNotification(message = it)
|
||||||
init(player, commandStream, playerNotifier)
|
logger.atInfo {
|
||||||
|
message = "Notification send ERROR: ${notification.message}"
|
||||||
|
payload = mapOf("notification" to notification)
|
||||||
}
|
}
|
||||||
|
outgoing.trySendBlocking(notification.toFrame())
|
||||||
|
}
|
||||||
|
return init(player, commandStream, playerErrorNotifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun init(
|
private suspend fun init(
|
||||||
player: Player,
|
player: Player,
|
||||||
commandStream: GameCommandStream,
|
commandStream: GameCommandStream,
|
||||||
playerNotifier: (String) -> Unit,
|
playerErrorNotifier: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
commandStream.process { command ->
|
commandStream.process { command ->
|
||||||
if (command.payload.player.id != player.id) {
|
if (command.payload.player.id != player.id) {
|
||||||
@@ -57,12 +61,12 @@ class GameCommandHandler(
|
|||||||
val gameState = command.buildGameState()
|
val gameState = command.buildGameState()
|
||||||
|
|
||||||
when (command) {
|
when (command) {
|
||||||
is IWantToPlayCardCommand -> command.run(gameState, playerNotifier, eventStream)
|
is IWantToPlayCardCommand -> command.run(gameState, playerErrorNotifier, eventStream)
|
||||||
is IamReadyToPlayCommand -> command.run(gameState, playerNotifier, eventStream)
|
is IamReadyToPlayCommand -> command.run(gameState, playerErrorNotifier, eventStream)
|
||||||
is IWantToJoinTheGameCommand -> command.run(gameState, playerNotifier, eventStream)
|
is IWantToJoinTheGameCommand -> command.run(gameState, playerErrorNotifier, eventStream)
|
||||||
is ICantPlayCommand -> command.run(gameState, playerNotifier, eventStream)
|
is ICantPlayCommand -> command.run(gameState, playerErrorNotifier, eventStream)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} as CommandBlock<GameCommand>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun GameCommand.buildGameState(): GameState = payload.gameId.buildStateFromEventStream(eventStream)
|
private fun GameCommand.buildGameState(): GameState = payload.gameId.buildStateFromEventStream(eventStream)
|
||||||
|
|||||||
@@ -8,15 +8,22 @@ import io.ktor.server.auth.jwt.JWTPrincipal
|
|||||||
import io.ktor.server.auth.principal
|
import io.ktor.server.auth.principal
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.server.routing.Route
|
||||||
import io.ktor.server.websocket.webSocket
|
import io.ktor.server.websocket.webSocket
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@DelicateCoroutinesApi
|
||||||
fun Route.gameSocket(
|
fun Route.gameSocket(
|
||||||
playerNotificationListener: GameEventPlayerNotificationListener,
|
playerNotificationListener: GameEventPlayerNotificationListener,
|
||||||
commandHandler: GameCommandHandler,
|
commandHandler: GameCommandHandler,
|
||||||
) {
|
) {
|
||||||
authenticate {
|
authenticate {
|
||||||
webSocket("/game") {
|
webSocket("/game") {
|
||||||
commandHandler.handle(call.getPlayer(), incoming, outgoing)
|
val currentPlayer = call.getPlayer()
|
||||||
playerNotificationListener.startListening(outgoing)
|
GlobalScope.launch {
|
||||||
|
commandHandler.handle(currentPlayer, incoming, outgoing)
|
||||||
|
}
|
||||||
|
playerNotificationListener.startListening(outgoing, currentPlayer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import eventDemo.libs.command.CommandStreamChannel
|
|||||||
import eventDemo.libs.command.CommandStreamInMemory
|
import eventDemo.libs.command.CommandStreamInMemory
|
||||||
import io.ktor.websocket.Frame
|
import io.ktor.websocket.Frame
|
||||||
import kotlinx.coroutines.channels.ReceiveChannel
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
import kotlinx.coroutines.channels.SendChannel
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,10 +18,7 @@ class GameCommandStreamInMemory : CommandStreamInMemory<GameCommand>()
|
|||||||
*/
|
*/
|
||||||
class GameCommandStream(
|
class GameCommandStream(
|
||||||
incoming: ReceiveChannel<Frame>,
|
incoming: ReceiveChannel<Frame>,
|
||||||
outgoing: SendChannel<Frame>,
|
|
||||||
) : CommandStream<GameCommand> by CommandStreamChannel(
|
) : CommandStream<GameCommand> by CommandStreamChannel(
|
||||||
incoming,
|
incoming,
|
||||||
outgoing,
|
|
||||||
{ Json.encodeToString(GameCommand.serializer(), it) },
|
|
||||||
{ Json.decodeFromString(GameCommand.serializer(), it) },
|
{ Json.decodeFromString(GameCommand.serializer(), it) },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,14 +6,12 @@ import eventDemo.app.entity.Player
|
|||||||
import eventDemo.app.event.GameEventStream
|
import eventDemo.app.event.GameEventStream
|
||||||
import eventDemo.app.event.event.PlayerHavePassEvent
|
import eventDemo.app.event.event.PlayerHavePassEvent
|
||||||
import eventDemo.libs.command.CommandId
|
import eventDemo.libs.command.CommandId
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command to perform an action to play a new card
|
* A command to perform an action to play a new card
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("Pass")
|
|
||||||
data class ICantPlayCommand(
|
data class ICantPlayCommand(
|
||||||
override val payload: Payload,
|
override val payload: Payload,
|
||||||
) : GameCommand {
|
) : GameCommand {
|
||||||
@@ -27,19 +25,22 @@ data class ICantPlayCommand(
|
|||||||
|
|
||||||
fun run(
|
fun run(
|
||||||
state: GameState,
|
state: GameState,
|
||||||
playerNotifier: (String) -> Unit,
|
playerErrorNotifier: (String) -> Unit,
|
||||||
eventStream: GameEventStream,
|
eventStream: GameEventStream,
|
||||||
) {
|
) {
|
||||||
val playableCards = state.playableCards(payload.player)
|
val playableCards = state.playableCards(payload.player)
|
||||||
if (playableCards.isEmpty()) {
|
if (playableCards.isEmpty()) {
|
||||||
|
val takenCard = state.deck.stack.first()
|
||||||
|
|
||||||
eventStream.publish(
|
eventStream.publish(
|
||||||
PlayerHavePassEvent(
|
PlayerHavePassEvent(
|
||||||
payload.gameId,
|
gameId = payload.gameId,
|
||||||
payload.player,
|
player = payload.player,
|
||||||
|
takenCard = takenCard,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
playerNotifier("You can and must play one card, like ${playableCards.first()::class.simpleName}")
|
playerErrorNotifier("You can and must play one card, like ${playableCards.first()::class.simpleName}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ import eventDemo.app.entity.Player
|
|||||||
import eventDemo.app.event.GameEventStream
|
import eventDemo.app.event.GameEventStream
|
||||||
import eventDemo.app.event.event.NewPlayerEvent
|
import eventDemo.app.event.event.NewPlayerEvent
|
||||||
import eventDemo.libs.command.CommandId
|
import eventDemo.libs.command.CommandId
|
||||||
import kotlinx.serialization.SerialName
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command to perform an action to play a new card
|
* A command to perform an action to play a new card
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("JoinGame")
|
|
||||||
data class IWantToJoinTheGameCommand(
|
data class IWantToJoinTheGameCommand(
|
||||||
override val payload: Payload,
|
override val payload: Payload,
|
||||||
) : GameCommand {
|
) : GameCommand {
|
||||||
@@ -27,9 +26,10 @@ data class IWantToJoinTheGameCommand(
|
|||||||
|
|
||||||
fun run(
|
fun run(
|
||||||
state: GameState,
|
state: GameState,
|
||||||
playerNotifier: (String) -> Unit,
|
playerErrorNotifier: (String) -> Unit,
|
||||||
eventStream: GameEventStream,
|
eventStream: GameEventStream,
|
||||||
) {
|
) {
|
||||||
|
val logger = KotlinLogging.logger {}
|
||||||
if (!state.isStarted) {
|
if (!state.isStarted) {
|
||||||
eventStream.publish(
|
eventStream.publish(
|
||||||
NewPlayerEvent(
|
NewPlayerEvent(
|
||||||
@@ -38,7 +38,11 @@ data class IWantToJoinTheGameCommand(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
playerNotifier("The game is already started")
|
logger.atWarn {
|
||||||
|
message = "The game is already started"
|
||||||
|
payload = mapOf("player" to this@IWantToJoinTheGameCommand.payload.player)
|
||||||
|
}
|
||||||
|
playerErrorNotifier("The game is already started")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,12 @@ import eventDemo.app.entity.Player
|
|||||||
import eventDemo.app.event.GameEventStream
|
import eventDemo.app.event.GameEventStream
|
||||||
import eventDemo.app.event.event.CardIsPlayedEvent
|
import eventDemo.app.event.event.CardIsPlayedEvent
|
||||||
import eventDemo.libs.command.CommandId
|
import eventDemo.libs.command.CommandId
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command to perform an action to play a new card
|
* A command to perform an action to play a new card
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("PlayCard")
|
|
||||||
data class IWantToPlayCardCommand(
|
data class IWantToPlayCardCommand(
|
||||||
override val payload: Payload,
|
override val payload: Payload,
|
||||||
) : GameCommand {
|
) : GameCommand {
|
||||||
@@ -29,11 +27,11 @@ data class IWantToPlayCardCommand(
|
|||||||
|
|
||||||
fun run(
|
fun run(
|
||||||
state: GameState,
|
state: GameState,
|
||||||
playerNotifier: (String) -> Unit,
|
playerErrorNotifier: (String) -> Unit,
|
||||||
eventStream: GameEventStream,
|
eventStream: GameEventStream,
|
||||||
) {
|
) {
|
||||||
if (!state.isReady) {
|
if (!state.isStarted) {
|
||||||
playerNotifier("The game is Not started")
|
playerErrorNotifier("The game is Not started")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +44,7 @@ data class IWantToPlayCardCommand(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
playerNotifier("You cannot play this card")
|
playerErrorNotifier("You cannot play this card")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,12 @@ import eventDemo.app.entity.Player
|
|||||||
import eventDemo.app.event.GameEventStream
|
import eventDemo.app.event.GameEventStream
|
||||||
import eventDemo.app.event.event.PlayerReadyEvent
|
import eventDemo.app.event.event.PlayerReadyEvent
|
||||||
import eventDemo.libs.command.CommandId
|
import eventDemo.libs.command.CommandId
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command to set as ready to play
|
* A command to set as ready to play
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("Ready")
|
|
||||||
data class IamReadyToPlayCommand(
|
data class IamReadyToPlayCommand(
|
||||||
override val payload: Payload,
|
override val payload: Payload,
|
||||||
) : GameCommand {
|
) : GameCommand {
|
||||||
@@ -27,13 +25,16 @@ data class IamReadyToPlayCommand(
|
|||||||
|
|
||||||
fun run(
|
fun run(
|
||||||
state: GameState,
|
state: GameState,
|
||||||
playerNotifier: (String) -> Unit,
|
playerErrorNotifier: (String) -> Unit,
|
||||||
eventStream: GameEventStream,
|
eventStream: GameEventStream,
|
||||||
) {
|
) {
|
||||||
|
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 (playerIsAlreadyReady) {
|
if (!playerExist) {
|
||||||
playerNotifier("You are already ready")
|
playerErrorNotifier("You are not in the game")
|
||||||
|
} else if (playerIsAlreadyReady) {
|
||||||
|
playerErrorNotifier("You are already ready")
|
||||||
} else {
|
} else {
|
||||||
eventStream.publish(
|
eventStream.publish(
|
||||||
PlayerReadyEvent(
|
PlayerReadyEvent(
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ data class Deck(
|
|||||||
constructor(players: Set<Player>) :
|
constructor(players: Set<Player>) :
|
||||||
this(playersHands = PlayersHands(players))
|
this(playersHands = PlayersHands(players))
|
||||||
|
|
||||||
|
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(
|
||||||
@@ -21,16 +23,9 @@ data class Deck(
|
|||||||
|
|
||||||
fun takeOneCardFromStackTo(player: Player): Deck =
|
fun takeOneCardFromStackTo(player: Player): Deck =
|
||||||
takeOne().let { (deck, newPlayerCard) ->
|
takeOne().let { (deck, newPlayerCard) ->
|
||||||
val newHands =
|
deck.copy(
|
||||||
deck.playersHands
|
playersHands = deck.playersHands.addCard(player, newPlayerCard),
|
||||||
.mapValues { (p, cards) ->
|
)
|
||||||
if (p == player) {
|
|
||||||
cards + newPlayerCard
|
|
||||||
} else {
|
|
||||||
cards
|
|
||||||
}
|
|
||||||
}.toPlayersHands()
|
|
||||||
deck.copy(playersHands = newHands)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putOneCardFromHand(
|
fun putOneCardFromHand(
|
||||||
@@ -40,7 +35,7 @@ data class Deck(
|
|||||||
run {
|
run {
|
||||||
// Validate parameters
|
// Validate parameters
|
||||||
val playerHand =
|
val playerHand =
|
||||||
playersHands[player]
|
playersHands.getHand(player)
|
||||||
?: error("No player on this game")
|
?: error("No player on this game")
|
||||||
if (playerHand.none { it == card }) {
|
if (playerHand.none { it == card }) {
|
||||||
error("No card exist on the player hand")
|
error("No card exist on the player hand")
|
||||||
@@ -70,8 +65,7 @@ data class Deck(
|
|||||||
(1..2).map { Card.PassCard(color) }
|
(1..2).map { Card.PassCard(color) }
|
||||||
}.let {
|
}.let {
|
||||||
it + (1..4).map { Card.Plus4Card() }
|
it + (1..4).map { Card.Plus4Card() }
|
||||||
}.shuffled()
|
}.toStack()
|
||||||
.toStack()
|
|
||||||
.let { Deck(it) }
|
.let { Deck(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,6 +94,8 @@ value class Stack(
|
|||||||
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 List<Card>.toStack(): Stack = Stack(this.toSet())
|
fun List<Card>.toStack(): Stack = Stack(this.toSet())
|
||||||
|
|||||||
@@ -12,4 +12,6 @@ import java.util.UUID
|
|||||||
@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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ data class Player(
|
|||||||
PlayerId(UUID.fromString(id)),
|
PlayerId(UUID.fromString(id)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
@JvmInline
|
@JvmInline
|
||||||
value class PlayerId(
|
value class PlayerId(
|
||||||
@Serializable(with = UUIDSerializer::class)
|
@Serializable(with = UUIDSerializer::class)
|
||||||
|
|||||||
@@ -5,28 +5,38 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
@JvmInline
|
@JvmInline
|
||||||
value class PlayersHands(
|
value class PlayersHands(
|
||||||
private val map: Map<Player, List<Card>> = emptyMap(),
|
private val map: Map<Player.PlayerId, List<Card>> = emptyMap(),
|
||||||
) : Map<Player, List<Card>> by map {
|
) : Map<Player.PlayerId, List<Card>> by map {
|
||||||
constructor(players: Set<Player>) : this(players.associateWith { emptyList<Card>() }.toPlayersHands())
|
constructor(players: Set<Player>) :
|
||||||
|
this(players.map { it.id }.associateWith { emptyList<Card>() }.toPlayersHands())
|
||||||
|
|
||||||
|
fun getHand(player: Player): List<Card>? = this[player.id]
|
||||||
|
|
||||||
fun removeCard(
|
fun removeCard(
|
||||||
player: Player,
|
player: Player,
|
||||||
card: Card,
|
card: Card,
|
||||||
): PlayersHands =
|
): PlayersHands =
|
||||||
mapValues { (p, cards) ->
|
mapValues { (playerId, cards) ->
|
||||||
if (p == player) {
|
if (playerId == player.id) {
|
||||||
|
if (!cards.contains(card)) error("The hand no contain the card")
|
||||||
cards - card
|
cards - card
|
||||||
} else {
|
} else {
|
||||||
cards
|
cards
|
||||||
}
|
}
|
||||||
}.toPlayersHands()
|
}.toPlayersHands()
|
||||||
|
|
||||||
|
fun addCard(
|
||||||
|
player: Player,
|
||||||
|
newCard: Card,
|
||||||
|
): 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) {
|
if (p == player.id) {
|
||||||
|
if (cards.intersect(newCards).isNotEmpty()) error("The hand already contain the card")
|
||||||
cards + newCards
|
cards + newCards
|
||||||
} else {
|
} else {
|
||||||
cards
|
cards
|
||||||
@@ -34,4 +44,4 @@ value class PlayersHands(
|
|||||||
}.toPlayersHands()
|
}.toPlayersHands()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Map<Player, List<Card>>.toPlayersHands(): PlayersHands = PlayersHands(this)
|
fun Map<Player.PlayerId, List<Card>>.toPlayersHands(): PlayersHands = PlayersHands(this)
|
||||||
|
|||||||
@@ -10,13 +10,17 @@ import eventDemo.app.event.event.NewPlayerEvent
|
|||||||
import eventDemo.app.event.event.PlayerChoseColorEvent
|
import eventDemo.app.event.event.PlayerChoseColorEvent
|
||||||
import eventDemo.app.event.event.PlayerHavePassEvent
|
import eventDemo.app.event.event.PlayerHavePassEvent
|
||||||
import eventDemo.app.event.event.PlayerReadyEvent
|
import eventDemo.app.event.event.PlayerReadyEvent
|
||||||
import eventDemo.libs.event.EventStream
|
|
||||||
|
|
||||||
fun GameId.buildStateFromEventStream(eventStream: EventStream<GameEvent, GameId>): GameState =
|
fun GameId.buildStateFromEventStream(eventStream: GameEventStream): GameState =
|
||||||
buildStateFromEvents(
|
buildStateFromEvents(
|
||||||
eventStream.readAll(this),
|
eventStream.readAll(this),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun GameEvent.buildStateFromEventStreamTo(eventStream: GameEventStream): GameState =
|
||||||
|
gameId.buildStateFromEvents(
|
||||||
|
eventStream.readAll(gameId).takeWhile { it != this } + this,
|
||||||
|
)
|
||||||
|
|
||||||
private fun GameId.buildStateFromEvents(events: List<GameEvent>): GameState =
|
private fun GameId.buildStateFromEvents(events: List<GameEvent>): GameState =
|
||||||
events.fold(GameState(this)) { state: GameState, event: GameEvent ->
|
events.fold(GameState(this)) { state: GameState, event: GameEvent ->
|
||||||
when (event) {
|
when (event) {
|
||||||
@@ -37,12 +41,13 @@ private fun GameId.buildStateFromEvents(events: List<GameEvent>): GameState =
|
|||||||
lastPlayer = event.player,
|
lastPlayer = event.player,
|
||||||
direction = direction,
|
direction = direction,
|
||||||
lastColor = color,
|
lastColor = color,
|
||||||
|
lastCard = GameState.LastCard(event.card, event.player),
|
||||||
deck = state.deck.putOneCardFromHand(event.player, event.card),
|
deck = state.deck.putOneCardFromHand(event.player, event.card),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NewPlayerEvent -> {
|
is NewPlayerEvent -> {
|
||||||
if (state.isReady) error("The game is already started")
|
if (state.isStarted) error("The game is already started")
|
||||||
|
|
||||||
state.copy(
|
state.copy(
|
||||||
players = state.players + event.player,
|
players = state.players + event.player,
|
||||||
@@ -56,6 +61,7 @@ private fun GameId.buildStateFromEvents(events: List<GameEvent>): GameState =
|
|||||||
}
|
}
|
||||||
|
|
||||||
is PlayerHavePassEvent -> {
|
is PlayerHavePassEvent -> {
|
||||||
|
if (event.takenCard != state.deck.stack.first()) error("taken card is not ot top of the stack")
|
||||||
state.copy(
|
state.copy(
|
||||||
lastPlayer = event.player,
|
lastPlayer = event.player,
|
||||||
deck = state.deck.takeOneCardFromStackTo(event.player),
|
deck = state.deck.takeOneCardFromStackTo(event.player),
|
||||||
@@ -70,7 +76,7 @@ private fun GameId.buildStateFromEvents(events: List<GameEvent>): GameState =
|
|||||||
|
|
||||||
is GameStartedEvent -> {
|
is GameStartedEvent -> {
|
||||||
state.copy(
|
state.copy(
|
||||||
lastColor = (event.deck.discard.first() as? Card.ColorCard)?.color,
|
lastColor = (event.deck.discard.first() as? Card.ColorCard)?.color ?: state.lastColor,
|
||||||
lastCard = GameState.LastCard(event.deck.discard.first(), event.firstPlayer),
|
lastCard = GameState.LastCard(event.deck.discard.first(), event.firstPlayer),
|
||||||
lastPlayer = event.firstPlayer,
|
lastPlayer = event.firstPlayer,
|
||||||
deck = event.deck,
|
deck = event.deck,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import eventDemo.app.entity.Player
|
|||||||
* An [GameEvent] to represent a played card.
|
* An [GameEvent] to represent a played card.
|
||||||
*/
|
*/
|
||||||
data class CardIsPlayedEvent(
|
data class CardIsPlayedEvent(
|
||||||
override val id: GameId,
|
override val gameId: GameId,
|
||||||
val card: Card,
|
val card: Card,
|
||||||
val player: Player,
|
val player: Player,
|
||||||
) : GameEvent
|
) : GameEvent
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ import kotlinx.serialization.Serializable
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed interface GameEvent : Event<GameId> {
|
sealed interface GameEvent : Event<GameId> {
|
||||||
override val id: GameId
|
override val gameId: GameId
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import eventDemo.app.entity.initHands
|
|||||||
* 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 id: GameId,
|
override val gameId: GameId,
|
||||||
val firstPlayer: Player,
|
val firstPlayer: Player,
|
||||||
val deck: Deck,
|
val deck: Deck,
|
||||||
) : GameEvent {
|
) : GameEvent {
|
||||||
@@ -19,9 +19,20 @@ data class GameStartedEvent(
|
|||||||
players: Set<Player>,
|
players: Set<Player>,
|
||||||
): GameStartedEvent =
|
): GameStartedEvent =
|
||||||
GameStartedEvent(
|
GameStartedEvent(
|
||||||
id = id,
|
gameId = id,
|
||||||
firstPlayer = players.random(),
|
firstPlayer = if (isDisabled) players.first() else players.random(),
|
||||||
deck = Deck.newWithoutPlayers().initHands(players).placeFirstCardOnDiscard(),
|
deck =
|
||||||
|
Deck
|
||||||
|
.newWithoutPlayers()
|
||||||
|
.let { if (isDisabled) it else it.shuffle() }
|
||||||
|
.initHands(players)
|
||||||
|
.placeFirstCardOnDiscard(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var isDisabled = false
|
||||||
|
|
||||||
|
internal fun disableShuffleDeck() {
|
||||||
|
isDisabled = true
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ import eventDemo.app.entity.Player
|
|||||||
* 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 id: GameId,
|
override val gameId: GameId,
|
||||||
val player: Player,
|
val player: Player,
|
||||||
) : GameEvent
|
) : GameEvent
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import eventDemo.app.entity.Player
|
|||||||
* 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 id: GameId,
|
override val gameId: GameId,
|
||||||
val player: Player,
|
val player: Player,
|
||||||
val color: Card.Color,
|
val color: Card.Color,
|
||||||
) : GameEvent
|
) : GameEvent
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eventDemo.app.event.event
|
package eventDemo.app.event.event
|
||||||
|
|
||||||
|
import eventDemo.app.entity.Card
|
||||||
import eventDemo.app.entity.GameId
|
import eventDemo.app.entity.GameId
|
||||||
import eventDemo.app.entity.Player
|
import eventDemo.app.entity.Player
|
||||||
|
|
||||||
@@ -7,6 +8,7 @@ import eventDemo.app.entity.Player
|
|||||||
* 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 id: GameId,
|
override val gameId: GameId,
|
||||||
val player: Player,
|
val player: Player,
|
||||||
|
val takenCard: Card,
|
||||||
) : GameEvent
|
) : GameEvent
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ import eventDemo.app.entity.Player
|
|||||||
* 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 id: GameId,
|
override val gameId: GameId,
|
||||||
val player: Player,
|
val player: Player,
|
||||||
) : GameEvent
|
) : GameEvent
|
||||||
|
|||||||
@@ -1,18 +1,126 @@
|
|||||||
package eventDemo.app.eventListener
|
package eventDemo.app.eventListener
|
||||||
|
|
||||||
|
import eventDemo.app.entity.Player
|
||||||
import eventDemo.app.event.GameEventBus
|
import eventDemo.app.event.GameEventBus
|
||||||
|
import eventDemo.app.event.GameEventStream
|
||||||
|
import eventDemo.app.event.buildStateFromEventStreamTo
|
||||||
|
import eventDemo.app.event.event.CardIsPlayedEvent
|
||||||
import eventDemo.app.event.event.GameEvent
|
import eventDemo.app.event.event.GameEvent
|
||||||
|
import eventDemo.app.event.event.GameStartedEvent
|
||||||
|
import eventDemo.app.event.event.NewPlayerEvent
|
||||||
|
import eventDemo.app.event.event.PlayerChoseColorEvent
|
||||||
|
import eventDemo.app.event.event.PlayerHavePassEvent
|
||||||
|
import eventDemo.app.event.event.PlayerReadyEvent
|
||||||
|
import eventDemo.app.notification.PlayerAsJoinTheGameNotification
|
||||||
|
import eventDemo.app.notification.PlayerAsPlayACardNotification
|
||||||
|
import eventDemo.app.notification.PlayerHavePassNotification
|
||||||
|
import eventDemo.app.notification.PlayerWasChoseTheCardColorNotification
|
||||||
|
import eventDemo.app.notification.PlayerWasReadyNotification
|
||||||
|
import eventDemo.app.notification.TheGameWasStartedNotification
|
||||||
|
import eventDemo.app.notification.WelcomeToTheGameNotification
|
||||||
|
import eventDemo.app.notification.YourNewCardNotification
|
||||||
import eventDemo.shared.toFrame
|
import eventDemo.shared.toFrame
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.ktor.websocket.Frame
|
import io.ktor.websocket.Frame
|
||||||
import kotlinx.coroutines.channels.SendChannel
|
import kotlinx.coroutines.channels.SendChannel
|
||||||
import kotlinx.coroutines.channels.trySendBlocking
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
|
|
||||||
class GameEventPlayerNotificationListener(
|
class GameEventPlayerNotificationListener(
|
||||||
private val eventBus: GameEventBus,
|
private val eventBus: GameEventBus,
|
||||||
|
private val eventStream: GameEventStream,
|
||||||
|
) {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
fun startListening(
|
||||||
|
outgoing: SendChannel<Frame>,
|
||||||
|
currentPlayer: Player,
|
||||||
) {
|
) {
|
||||||
fun startListening(outgoing: SendChannel<Frame>) {
|
|
||||||
eventBus.subscribe { event: GameEvent ->
|
eventBus.subscribe { event: GameEvent ->
|
||||||
outgoing.trySendBlocking(event.toFrame())
|
val currentState = event.buildStateFromEventStreamTo(eventStream)
|
||||||
|
val notification =
|
||||||
|
when (event) {
|
||||||
|
is NewPlayerEvent -> {
|
||||||
|
if (currentPlayer != event.player) {
|
||||||
|
PlayerAsJoinTheGameNotification(
|
||||||
|
player = event.player,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
WelcomeToTheGameNotification(
|
||||||
|
players = currentState.players,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is CardIsPlayedEvent -> {
|
||||||
|
if (currentPlayer != event.player) {
|
||||||
|
PlayerAsPlayACardNotification(
|
||||||
|
player = event.player,
|
||||||
|
card = event.card,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is GameStartedEvent -> {
|
||||||
|
TheGameWasStartedNotification(
|
||||||
|
hand =
|
||||||
|
event.deck.playersHands.getHand(currentPlayer)
|
||||||
|
?: error("You are not in the game"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerChoseColorEvent -> {
|
||||||
|
if (currentPlayer != event.player) {
|
||||||
|
PlayerWasChoseTheCardColorNotification(
|
||||||
|
player = event.player,
|
||||||
|
color = event.color,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerHavePassEvent -> {
|
||||||
|
if (currentPlayer == event.player) {
|
||||||
|
YourNewCardNotification(
|
||||||
|
card = event.takenCard,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
PlayerHavePassNotification(
|
||||||
|
player = event.player,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerReadyEvent -> {
|
||||||
|
if (currentPlayer != event.player) {
|
||||||
|
PlayerWasReadyNotification(
|
||||||
|
player = event.player,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (notification == null) {
|
||||||
|
logger.atInfo {
|
||||||
|
message = "Notification Ignore: $event"
|
||||||
|
payload = mapOf("event" to event)
|
||||||
|
}
|
||||||
|
} else if (currentState.players.contains(currentPlayer)) {
|
||||||
|
// Only notify players who have already joined the game.
|
||||||
|
outgoing.trySendBlocking(notification.toFrame())
|
||||||
|
logger.atInfo {
|
||||||
|
message = "Notification SEND: $notification"
|
||||||
|
payload = mapOf("notification" to notification, "event" to event)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.atInfo {
|
||||||
|
message = "Notification SKIP: $notification"
|
||||||
|
payload = mapOf("notification" to notification, "event" to event)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,25 +2,41 @@ package eventDemo.app.eventListener
|
|||||||
|
|
||||||
import eventDemo.app.event.GameEventBus
|
import eventDemo.app.event.GameEventBus
|
||||||
import eventDemo.app.event.GameEventStream
|
import eventDemo.app.event.GameEventStream
|
||||||
import eventDemo.app.event.buildStateFromEventStream
|
import eventDemo.app.event.buildStateFromEventStreamTo
|
||||||
import eventDemo.app.event.event.GameEvent
|
import eventDemo.app.event.event.GameEvent
|
||||||
import eventDemo.app.event.event.GameStartedEvent
|
import eventDemo.app.event.event.GameStartedEvent
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
|
||||||
class GameEventReactionListener(
|
class GameEventReactionListener(
|
||||||
private val eventBus: GameEventBus,
|
private val eventBus: GameEventBus,
|
||||||
private val eventStream: GameEventStream,
|
private val eventStream: GameEventStream,
|
||||||
|
private val priority: Int = DEFAULT_PRIORITY,
|
||||||
) {
|
) {
|
||||||
|
companion object Config {
|
||||||
|
const val DEFAULT_PRIORITY = -1000
|
||||||
|
}
|
||||||
|
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
eventBus.subscribe { event: GameEvent ->
|
eventBus.subscribe(priority) { event: GameEvent ->
|
||||||
val state = event.id.buildStateFromEventStream(eventStream)
|
val state = event.buildStateFromEventStreamTo(eventStream)
|
||||||
if (state.isReady) {
|
if (state.isReady && !state.isStarted) {
|
||||||
eventStream.publish(
|
val reactionEvent =
|
||||||
GameStartedEvent.new(
|
GameStartedEvent.new(
|
||||||
state.gameId,
|
state.gameId,
|
||||||
state.players,
|
state.players,
|
||||||
),
|
)
|
||||||
|
logger.atInfo {
|
||||||
|
message = "Event Send on reaction of: $event"
|
||||||
|
payload =
|
||||||
|
mapOf(
|
||||||
|
"event" to event,
|
||||||
|
"reactionEvent" to reactionEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
eventStream.publish(reactionEvent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package eventDemo.app.notification
|
||||||
|
|
||||||
|
import eventDemo.shared.UUIDSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ErrorNotification(
|
||||||
|
@Serializable(with = UUIDSerializer::class)
|
||||||
|
override val id: UUID = UUID.randomUUID(),
|
||||||
|
val message: String,
|
||||||
|
) : Notification
|
||||||
11
src/main/kotlin/eventDemo/app/notification/Notification.kt
Normal file
11
src/main/kotlin/eventDemo/app/notification/Notification.kt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package eventDemo.app.notification
|
||||||
|
|
||||||
|
import eventDemo.shared.UUIDSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed interface Notification {
|
||||||
|
@Serializable(with = UUIDSerializer::class)
|
||||||
|
val id: UUID
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package eventDemo.app.notification
|
||||||
|
|
||||||
|
import eventDemo.app.entity.Player
|
||||||
|
import eventDemo.shared.UUIDSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PlayerAsJoinTheGameNotification(
|
||||||
|
@Serializable(with = UUIDSerializer::class)
|
||||||
|
override val id: UUID = UUID.randomUUID(),
|
||||||
|
val player: Player,
|
||||||
|
) : Notification
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package eventDemo.app.notification
|
||||||
|
|
||||||
|
import eventDemo.app.entity.Card
|
||||||
|
import eventDemo.app.entity.Player
|
||||||
|
import eventDemo.shared.UUIDSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PlayerAsPlayACardNotification(
|
||||||
|
@Serializable(with = UUIDSerializer::class)
|
||||||
|
override val id: UUID = UUID.randomUUID(),
|
||||||
|
val player: Player,
|
||||||
|
val card: Card,
|
||||||
|
) : Notification
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package eventDemo.app.notification
|
||||||
|
|
||||||
|
import eventDemo.app.entity.Player
|
||||||
|
import eventDemo.shared.UUIDSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PlayerHavePassNotification(
|
||||||
|
@Serializable(with = UUIDSerializer::class)
|
||||||
|
override val id: UUID = UUID.randomUUID(),
|
||||||
|
val player: Player,
|
||||||
|
) : Notification
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package eventDemo.app.notification
|
||||||
|
|
||||||
|
import eventDemo.app.entity.Card
|
||||||
|
import eventDemo.app.entity.Player
|
||||||
|
import eventDemo.shared.UUIDSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PlayerWasChoseTheCardColorNotification(
|
||||||
|
@Serializable(with = UUIDSerializer::class)
|
||||||
|
override val id: UUID = UUID.randomUUID(),
|
||||||
|
val player: Player,
|
||||||
|
val color: Card.Color,
|
||||||
|
) : Notification
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package eventDemo.app.notification
|
||||||
|
|
||||||
|
import eventDemo.app.entity.Player
|
||||||
|
import eventDemo.shared.UUIDSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PlayerWasReadyNotification(
|
||||||
|
@Serializable(with = UUIDSerializer::class)
|
||||||
|
override val id: UUID = UUID.randomUUID(),
|
||||||
|
val player: Player,
|
||||||
|
) : Notification
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package eventDemo.app.notification
|
||||||
|
|
||||||
|
import eventDemo.app.entity.Card
|
||||||
|
import eventDemo.shared.UUIDSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TheGameWasStartedNotification(
|
||||||
|
@Serializable(with = UUIDSerializer::class)
|
||||||
|
override val id: UUID = UUID.randomUUID(),
|
||||||
|
val hand: List<Card>,
|
||||||
|
) : Notification
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package eventDemo.app.notification
|
||||||
|
|
||||||
|
import eventDemo.app.entity.Player
|
||||||
|
import eventDemo.shared.UUIDSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class WelcomeToTheGameNotification(
|
||||||
|
@Serializable(with = UUIDSerializer::class)
|
||||||
|
override val id: UUID = UUID.randomUUID(),
|
||||||
|
val players: Set<Player>,
|
||||||
|
) : Notification
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package eventDemo.app.notification
|
||||||
|
|
||||||
|
import eventDemo.app.entity.Card
|
||||||
|
import eventDemo.shared.UUIDSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class YourNewCardNotification(
|
||||||
|
@Serializable(with = UUIDSerializer::class)
|
||||||
|
override val id: UUID = UUID.randomUUID(),
|
||||||
|
val card: Card,
|
||||||
|
) : Notification
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package eventDemo.configuration
|
package eventDemo.configuration
|
||||||
|
|
||||||
import eventDemo.app.eventListener.GameEventReactionListener
|
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import org.koin.ktor.ext.get
|
import org.koin.ktor.ext.get
|
||||||
|
|
||||||
@@ -17,6 +16,5 @@ fun Application.configure() {
|
|||||||
configureHttpRouting()
|
configureHttpRouting()
|
||||||
declareHttpGameRoute()
|
declareHttpGameRoute()
|
||||||
|
|
||||||
GameEventReactionListener(get(), get())
|
configureGameListener()
|
||||||
.init()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package eventDemo.configuration
|
|||||||
|
|
||||||
import com.auth0.jwt.JWT
|
import com.auth0.jwt.JWT
|
||||||
import com.auth0.jwt.algorithms.Algorithm
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
|
import eventDemo.app.entity.Player
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.application.call
|
import io.ktor.server.application.call
|
||||||
@@ -11,13 +12,15 @@ import io.ktor.server.auth.jwt.jwt
|
|||||||
import io.ktor.server.response.respond
|
import io.ktor.server.response.respond
|
||||||
import io.ktor.server.routing.post
|
import io.ktor.server.routing.post
|
||||||
import io.ktor.server.routing.routing
|
import io.ktor.server.routing.routing
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
fun Application.configureSecurity() {
|
|
||||||
// TODO: read the jwt property from the config file
|
// TODO: read the jwt property from the config file
|
||||||
val jwtRealm = "Play card game"
|
private val jwtRealm = "Play card game"
|
||||||
val jwtIssuer = "PlayCardGame"
|
private val jwtIssuer = "PlayCardGame"
|
||||||
val jwtSecret = "secret"
|
private val jwtSecret = "secret"
|
||||||
|
|
||||||
|
fun Application.configureSecurity() {
|
||||||
authentication {
|
authentication {
|
||||||
jwt {
|
jwt {
|
||||||
realm = jwtRealm
|
realm = jwtRealm
|
||||||
@@ -42,17 +45,19 @@ fun Application.configureSecurity() {
|
|||||||
|
|
||||||
routing {
|
routing {
|
||||||
post("login/{username}") {
|
post("login/{username}") {
|
||||||
val username = call.parameters["username"]
|
val username = call.parameters["username"]!!
|
||||||
|
val player = Player(name = username)
|
||||||
|
|
||||||
val token =
|
call.respond(hashMapOf("token" to player.makeJwt()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Player.makeJwt(): String =
|
||||||
JWT
|
JWT
|
||||||
.create()
|
.create()
|
||||||
.withIssuer(jwtIssuer)
|
.withIssuer(jwtIssuer)
|
||||||
.withClaim("username", username)
|
.withClaim("username", name)
|
||||||
|
.withPayload(Json.encodeToString(this))
|
||||||
.withExpiresAt(Date(System.currentTimeMillis() + 60000))
|
.withExpiresAt(Date(System.currentTimeMillis() + 60000))
|
||||||
.sign(Algorithm.HMAC256(jwtSecret))
|
.sign(Algorithm.HMAC256(jwtSecret))
|
||||||
|
|
||||||
call.respond(hashMapOf("token" to token))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package eventDemo.configuration
|
||||||
|
|
||||||
|
import eventDemo.app.eventListener.GameEventReactionListener
|
||||||
|
import io.ktor.server.application.Application
|
||||||
|
import org.koin.ktor.ext.get
|
||||||
|
|
||||||
|
fun Application.configureGameListener() {
|
||||||
|
GameEventReactionListener(get(), get())
|
||||||
|
.init()
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package eventDemo.configuration
|
package eventDemo.configuration
|
||||||
|
|
||||||
|
import eventDemo.app.entity.GameId
|
||||||
|
import eventDemo.shared.GameIdSerializer
|
||||||
import eventDemo.shared.UUIDSerializer
|
import eventDemo.shared.UUIDSerializer
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
@@ -16,6 +18,7 @@ fun Application.configureSerialization() {
|
|||||||
serializersModule =
|
serializersModule =
|
||||||
SerializersModule {
|
SerializersModule {
|
||||||
contextual(UUID::class) { UUIDSerializer }
|
contextual(UUID::class) { UUIDSerializer }
|
||||||
|
contextual(GameId::class) { GameIdSerializer }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import eventDemo.app.command.gameSocket
|
|||||||
import eventDemo.app.eventListener.GameEventPlayerNotificationListener
|
import eventDemo.app.eventListener.GameEventPlayerNotificationListener
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.routing.routing
|
import io.ktor.server.routing.routing
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
fun Application.declareWebSocketsGameRoute(
|
fun Application.declareWebSocketsGameRoute(
|
||||||
playerNotificationListener: GameEventPlayerNotificationListener,
|
playerNotificationListener: GameEventPlayerNotificationListener,
|
||||||
commandHandler: GameCommandHandler,
|
commandHandler: GameCommandHandler,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package eventDemo.libs.command
|
|||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represent a Command stream.
|
* Represent a Command stream.
|
||||||
@@ -11,24 +10,6 @@ import kotlin.reflect.KClass
|
|||||||
* 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> {
|
||||||
/**
|
|
||||||
* Send a new [Command] to the queue.
|
|
||||||
*/
|
|
||||||
fun send(
|
|
||||||
type: KClass<C>,
|
|
||||||
command: C,
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send multiple [Command] to the queue.
|
|
||||||
*/
|
|
||||||
fun send(
|
|
||||||
type: KClass<C>,
|
|
||||||
vararg commands: C,
|
|
||||||
) {
|
|
||||||
commands.forEach { send(type, it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class to implement success/failed action.
|
* A class to implement success/failed action.
|
||||||
*/
|
*/
|
||||||
@@ -50,5 +31,3 @@ interface CommandStream<C : Command> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend inline fun <reified C : Command> CommandStream<C>.send(vararg command: C) = send(C::class, *command)
|
|
||||||
|
|||||||
@@ -4,45 +4,16 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
|||||||
import io.ktor.websocket.Frame
|
import io.ktor.websocket.Frame
|
||||||
import io.ktor.websocket.readText
|
import io.ktor.websocket.readText
|
||||||
import kotlinx.coroutines.channels.ReceiveChannel
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
import kotlinx.coroutines.channels.SendChannel
|
|
||||||
import kotlinx.coroutines.channels.onFailure
|
|
||||||
import kotlinx.coroutines.channels.onSuccess
|
|
||||||
import kotlinx.coroutines.channels.trySendBlocking
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manage [Command]'s with kotlin Channel
|
* Manage [Command]'s with kotlin Channel
|
||||||
*/
|
*/
|
||||||
class CommandStreamChannel<C : Command>(
|
class CommandStreamChannel<C : Command>(
|
||||||
private val incoming: ReceiveChannel<Frame>,
|
private val incoming: ReceiveChannel<Frame>,
|
||||||
private val outgoing: SendChannel<Frame>,
|
|
||||||
private val serializer: (C) -> String,
|
|
||||||
private val deserializer: (String) -> C,
|
private val deserializer: (String) -> C,
|
||||||
) : CommandStream<C> {
|
) : CommandStream<C> {
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a new [Command] to the queue.
|
|
||||||
*/
|
|
||||||
override fun send(
|
|
||||||
type: KClass<C>,
|
|
||||||
command: C,
|
|
||||||
) {
|
|
||||||
outgoing
|
|
||||||
.trySendBlocking(Frame.Text(serializer(command)))
|
|
||||||
.onSuccess {
|
|
||||||
logger.atInfo {
|
|
||||||
message = "Command published: $command"
|
|
||||||
payload = mapOf("command" to command)
|
|
||||||
}
|
|
||||||
}.onFailure {
|
|
||||||
logger.atError {
|
|
||||||
message = "Command FAILED: $command"
|
|
||||||
payload = mapOf("command" to command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun process(action: CommandBlock<C>) {
|
override suspend fun process(action: CommandBlock<C>) {
|
||||||
// incoming.consumeEach { commandAsFrame ->
|
// incoming.consumeEach { commandAsFrame ->
|
||||||
// if (commandAsFrame is Frame.Text) {
|
// if (commandAsFrame is Frame.Text) {
|
||||||
@@ -90,17 +61,15 @@ class CommandStreamChannel<C : Command>(
|
|||||||
|
|
||||||
private suspend fun markAsSuccess(command: C) {
|
private suspend fun markAsSuccess(command: C) {
|
||||||
logger.atInfo {
|
logger.atInfo {
|
||||||
message = "Compute command SUCCESS and it removed of the stack"
|
message = "Compute command SUCCESS: $command"
|
||||||
payload = mapOf("command" to command)
|
payload = mapOf("command" to command)
|
||||||
}
|
}
|
||||||
// outgoing.trySendBlocking(Frame.Text("Command executed successfully"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun markAsFailed(command: C) {
|
private suspend fun markAsFailed(command: C) {
|
||||||
logger.atWarn {
|
logger.atWarn {
|
||||||
message = "Compute command FAILED"
|
message = "Compute command FAILED: $command"
|
||||||
payload = mapOf("command" to command)
|
payload = mapOf("command" to command)
|
||||||
}
|
}
|
||||||
// outgoing.trySendBlocking(Frame.Text("Command execution failed"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package eventDemo.libs.command
|
|||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.channels.consumeEach
|
import kotlinx.coroutines.channels.consumeEach
|
||||||
import kotlinx.coroutines.channels.trySendBlocking
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
typealias CommandBlock<C> = suspend CommandStream.ComputeStatus.(C) -> Unit
|
typealias CommandBlock<C> = suspend CommandStream.ComputeStatus.(C) -> Unit
|
||||||
|
|
||||||
@@ -20,20 +18,6 @@ abstract class CommandStreamInMemory<C : Command> : CommandStream<C> {
|
|||||||
logger.atWarn { "${it::class.simpleName} command not send" }
|
logger.atWarn { "${it::class.simpleName} command not send" }
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a new [Command] to the queue.
|
|
||||||
*/
|
|
||||||
override fun send(
|
|
||||||
type: KClass<C>,
|
|
||||||
command: C,
|
|
||||||
) {
|
|
||||||
logger.atInfo {
|
|
||||||
message = "Command published: $command"
|
|
||||||
payload = mapOf("command" to command)
|
|
||||||
}
|
|
||||||
queue.trySendBlocking(command)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun process(action: CommandBlock<C>) {
|
override suspend fun process(action: CommandBlock<C>) {
|
||||||
queue.consumeEach { command ->
|
queue.consumeEach { command ->
|
||||||
compute(command, action)
|
compute(command, action)
|
||||||
@@ -71,14 +55,14 @@ abstract class CommandStreamInMemory<C : Command> : CommandStream<C> {
|
|||||||
|
|
||||||
private fun <C : Command> markAsSuccess(command: C) {
|
private fun <C : Command> markAsSuccess(command: C) {
|
||||||
logger.atInfo {
|
logger.atInfo {
|
||||||
message = "Compute command SUCCESS and it removed of the stack : $command"
|
message = "Compute command SUCCESS : $command"
|
||||||
payload = mapOf("command" to command)
|
payload = mapOf("command" to command)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <C : Command> markAsFailed(command: C) {
|
private fun <C : Command> markAsFailed(command: C) {
|
||||||
logger.atWarn {
|
logger.atWarn {
|
||||||
message = "Compute command FAILED and it put it ot the top of the stack : $command"
|
message = "Compute command FAILED : $command"
|
||||||
payload = mapOf("command" to command)
|
payload = mapOf("command" to command)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ interface AggregateId {
|
|||||||
* @see EventStream
|
* @see EventStream
|
||||||
*/
|
*/
|
||||||
interface Event<ID : AggregateId> {
|
interface Event<ID : AggregateId> {
|
||||||
val id: ID
|
val gameId: ID
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,11 @@ 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)
|
||||||
|
|
||||||
fun subscribe(block: (E) -> Unit)
|
/**
|
||||||
|
* @param priority The higher the priority, the more it will be called first
|
||||||
|
*/
|
||||||
|
fun subscribe(
|
||||||
|
priority: Int = 0,
|
||||||
|
block: (E) -> Unit,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
package eventDemo.libs.event
|
package eventDemo.libs.event
|
||||||
|
|
||||||
class EventBusInMemory<E : Event<ID>, ID : AggregateId> : EventBus<E, ID> {
|
class EventBusInMemory<E : Event<ID>, ID : AggregateId> : EventBus<E, ID> {
|
||||||
private val subscribers: MutableList<(E) -> Unit> = mutableListOf()
|
private val subscribers: MutableList<Pair<Int, (E) -> Unit>> = mutableListOf()
|
||||||
|
|
||||||
override fun publish(event: E) {
|
override fun publish(event: E) {
|
||||||
subscribers.forEach {
|
subscribers
|
||||||
it(event)
|
.sortedByDescending { (priority, block) -> priority }
|
||||||
|
.forEach { (_, block) ->
|
||||||
|
block(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun subscribe(block: (E) -> Unit) {
|
override fun subscribe(
|
||||||
subscribers.add(block)
|
priority: Int,
|
||||||
|
block: (E) -> Unit,
|
||||||
|
) {
|
||||||
|
subscribers.add(priority to block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class EventStreamInMemory<E : Event<ID>, ID : AggregateId> : EventStream<E, ID>
|
|||||||
): R? =
|
): R? =
|
||||||
events
|
events
|
||||||
.filterIsInstance(eventType.java)
|
.filterIsInstance(eventType.java)
|
||||||
.lastOrNull { it.id == aggregateId }
|
.lastOrNull { it.gameId == aggregateId }
|
||||||
|
|
||||||
override fun readAll(aggregateId: ID): List<E> = events
|
override fun readAll(aggregateId: ID): List<E> = events
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package eventDemo.shared
|
|||||||
|
|
||||||
import eventDemo.app.command.command.GameCommand
|
import eventDemo.app.command.command.GameCommand
|
||||||
import eventDemo.app.event.event.GameEvent
|
import eventDemo.app.event.event.GameEvent
|
||||||
|
import eventDemo.app.notification.Notification
|
||||||
import io.ktor.websocket.Frame
|
import io.ktor.websocket.Frame
|
||||||
import io.ktor.websocket.readText
|
import io.ktor.websocket.readText
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@@ -13,3 +14,11 @@ fun GameEvent.toFrame(): Frame.Text = Frame.Text(Json.encodeToString(GameEvent.s
|
|||||||
fun Frame.Text.toCommand(): GameCommand = Json.decodeFromString(GameCommand.serializer(), readText())
|
fun Frame.Text.toCommand(): GameCommand = Json.decodeFromString(GameCommand.serializer(), readText())
|
||||||
|
|
||||||
fun GameCommand.toFrame(): Frame.Text = Frame.Text(Json.encodeToString(GameCommand.serializer(), this))
|
fun GameCommand.toFrame(): Frame.Text = Frame.Text(Json.encodeToString(GameCommand.serializer(), this))
|
||||||
|
|
||||||
|
fun Frame.toNotification(): Notification =
|
||||||
|
Json.decodeFromString(
|
||||||
|
Notification.serializer(),
|
||||||
|
(this as Frame.Text).readText(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Notification.toFrame(): Frame.Text = Frame.Text(Json.encodeToString(Notification.serializer(), this))
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ object PlayerIdSerializer : KSerializer<PlayerId> {
|
|||||||
encoder.encodeString(value.id.toString())
|
encoder.encodeString(value.id.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("GameId", PrimitiveKind.STRING)
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("PlayerId", PrimitiveKind.STRING)
|
||||||
}
|
}
|
||||||
|
|
||||||
object GameIdSerializer : KSerializer<GameId> {
|
object GameIdSerializer : KSerializer<GameId> {
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
package eventDemo
|
package eventDemo
|
||||||
|
|
||||||
|
import eventDemo.app.command.command.GameCommand
|
||||||
import eventDemo.app.entity.Card
|
import eventDemo.app.entity.Card
|
||||||
import eventDemo.app.entity.Deck
|
import eventDemo.app.entity.Deck
|
||||||
|
import io.ktor.websocket.Frame
|
||||||
|
import kotlinx.coroutines.channels.SendChannel
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
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)))
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ class DeckTest :
|
|||||||
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[firstPlayer]).size shouldBeExactly 7 + 1
|
assertNotNull(modifiedDeck.playersHands.getHand(firstPlayer)).size shouldBeExactly 7 + 1
|
||||||
modifiedDeck.playersHands
|
modifiedDeck.playersHands
|
||||||
.filterKeys { it != firstPlayer }
|
.filterKeys { it != firstPlayer.id }
|
||||||
.forEach { (_, cards) -> cards.size shouldBeExactly 7 }
|
.forEach { (_, cards) -> cards.size shouldBeExactly 7 }
|
||||||
modifiedDeck.allCardCount() shouldBeExactly totalCardsNumber
|
modifiedDeck.allCardCount() shouldBeExactly totalCardsNumber
|
||||||
}
|
}
|
||||||
@@ -70,16 +70,16 @@ class DeckTest :
|
|||||||
val firstPlayer = players.first()
|
val firstPlayer = players.first()
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val card = deck.playersHands[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[firstPlayer]).size shouldBeExactly 6
|
assertNotNull(modifiedDeck.playersHands.getHand(firstPlayer)).size shouldBeExactly 6
|
||||||
modifiedDeck.playersHands
|
modifiedDeck.playersHands
|
||||||
.filterKeys { it != firstPlayer }
|
.filterKeys { it != firstPlayer.id }
|
||||||
.forEach { (_, cards) -> cards.size shouldBeExactly 7 }
|
.forEach { (_, cards) -> cards.size shouldBeExactly 7 }
|
||||||
modifiedDeck.allCardCount() shouldBeExactly totalCardsNumber
|
modifiedDeck.allCardCount() shouldBeExactly totalCardsNumber
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ class PlayerHandKtTest :
|
|||||||
// When
|
// When
|
||||||
val newHands: PlayersHands = playersHands.addCards(firstPlayer, listOf(card))
|
val newHands: PlayersHands = playersHands.addCards(firstPlayer, listOf(card))
|
||||||
|
|
||||||
assertNotNull(newHands[firstPlayer]).size shouldBeExactly 1
|
assertNotNull(newHands.getHand(firstPlayer)).size shouldBeExactly 1
|
||||||
assertNotNull(newHands[players.last()]).size shouldBeExactly 0
|
assertNotNull(newHands.getHand(players.last())).size shouldBeExactly 0
|
||||||
}
|
}
|
||||||
|
|
||||||
test("removeCard") {
|
test("removeCard") {
|
||||||
@@ -35,7 +35,7 @@ class PlayerHandKtTest :
|
|||||||
// When
|
// When
|
||||||
val newHands: PlayersHands = playersHands.removeCard(firstPlayer, card1)
|
val newHands: PlayersHands = playersHands.removeCard(firstPlayer, card1)
|
||||||
|
|
||||||
assertNotNull(newHands[firstPlayer]).size shouldBeExactly 1
|
assertNotNull(newHands.getHand(firstPlayer)).size shouldBeExactly 1
|
||||||
assertNotNull(newHands[players.last()]).size shouldBeExactly 0
|
assertNotNull(newHands.getHand(players.last())).size shouldBeExactly 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,49 +1,50 @@
|
|||||||
package eventDemo.app.query
|
package eventDemo.app.query
|
||||||
|
|
||||||
|
import eventDemo.app.GameState
|
||||||
import eventDemo.app.entity.Card
|
import eventDemo.app.entity.Card
|
||||||
import eventDemo.app.entity.GameId
|
import eventDemo.app.entity.GameId
|
||||||
import eventDemo.app.entity.Player
|
import eventDemo.app.entity.Player
|
||||||
import eventDemo.app.event.GameEventStream
|
import eventDemo.app.event.GameEventStream
|
||||||
import eventDemo.app.event.event.CardIsPlayedEvent
|
import eventDemo.app.event.event.CardIsPlayedEvent
|
||||||
import eventDemo.configuration.configure
|
import eventDemo.configuration.configure
|
||||||
|
import eventDemo.configuration.makeJwt
|
||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.collections.shouldHaveSize
|
||||||
|
import io.kotest.matchers.equals.shouldBeEqual
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.request.HttpRequestBuilder
|
||||||
import io.ktor.client.request.accept
|
import io.ktor.client.request.accept
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.request.post
|
import io.ktor.client.request.header
|
||||||
import io.ktor.client.request.setBody
|
|
||||||
import io.ktor.client.statement.bodyAsText
|
import io.ktor.client.statement.bodyAsText
|
||||||
import io.ktor.http.ContentType.Application.Json
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.http.contentType
|
|
||||||
import io.ktor.server.testing.testApplication
|
import io.ktor.server.testing.testApplication
|
||||||
import org.koin.core.context.stopKoin
|
import org.koin.core.context.stopKoin
|
||||||
import org.koin.java.KoinJavaComponent.getKoin
|
|
||||||
import org.koin.ktor.ext.inject
|
import org.koin.ktor.ext.inject
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class CardTest :
|
class GameStateRouteTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
test("/game/{id}/card") {
|
test("/game/{id}/state on empty game") {
|
||||||
testApplication {
|
testApplication {
|
||||||
|
val id = GameId()
|
||||||
|
val player1 = Player(name = "Nikola")
|
||||||
application {
|
application {
|
||||||
stopKoin()
|
stopKoin()
|
||||||
configure()
|
configure()
|
||||||
}
|
}
|
||||||
|
|
||||||
val id = GameId()
|
|
||||||
val card: Card = Card.NumericCard(1, Card.Color.Blue)
|
|
||||||
val player = Player(name = "Nikola")
|
|
||||||
httpClient()
|
httpClient()
|
||||||
.post("/game/$id/card") {
|
.get("/game/$id/state") {
|
||||||
contentType(Json)
|
withAuth(player1)
|
||||||
accept(Json)
|
accept(ContentType.Application.Json)
|
||||||
setBody(card)
|
|
||||||
}.apply {
|
}.apply {
|
||||||
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
|
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
|
||||||
|
val state = call.body<GameState>()
|
||||||
val eventStream = getKoin().get<GameEventStream>()
|
assertEquals(id, state.gameId)
|
||||||
assertEquals(CardIsPlayedEvent(id, card, player), eventStream.readLast(id))
|
state.players shouldHaveSize 0
|
||||||
|
state.isStarted shouldBeEqual false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,12 +53,13 @@ class CardTest :
|
|||||||
testApplication {
|
testApplication {
|
||||||
val id = GameId()
|
val id = GameId()
|
||||||
val card: Card = Card.NumericCard(1, Card.Color.Blue)
|
val card: Card = Card.NumericCard(1, Card.Color.Blue)
|
||||||
|
val player = Player(name = "Nikola")
|
||||||
|
|
||||||
application {
|
application {
|
||||||
stopKoin()
|
stopKoin()
|
||||||
configure()
|
configure()
|
||||||
|
|
||||||
val eventStream by inject<GameEventStream>()
|
val eventStream by inject<GameEventStream>()
|
||||||
val player = Player(name = "Nikola")
|
|
||||||
eventStream.publish(
|
eventStream.publish(
|
||||||
CardIsPlayedEvent(id, Card.NumericCard(2, Card.Color.Yellow), player),
|
CardIsPlayedEvent(id, Card.NumericCard(2, Card.Color.Yellow), player),
|
||||||
CardIsPlayedEvent(id, card, player),
|
CardIsPlayedEvent(id, card, player),
|
||||||
@@ -66,10 +68,18 @@ class CardTest :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
httpClient().get("/game/$id/card/last").apply {
|
httpClient()
|
||||||
|
.get("/game/$id/card/last") {
|
||||||
|
withAuth(player)
|
||||||
|
accept(ContentType.Application.Json)
|
||||||
|
}.apply {
|
||||||
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
|
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
|
||||||
assertEquals(card, call.body<Card>())
|
assertEquals(card, call.body<Card>())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
private fun HttpRequestBuilder.withAuth(player: Player) {
|
||||||
|
header("Authorization", "Bearer ${player.makeJwt()}")
|
||||||
|
}
|
||||||
133
src/test/kotlin/eventDemo/app/query/GameStateTest.kt
Normal file
133
src/test/kotlin/eventDemo/app/query/GameStateTest.kt
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package eventDemo.app.query
|
||||||
|
|
||||||
|
import eventDemo.app.GameState
|
||||||
|
import eventDemo.app.command.GameCommandHandler
|
||||||
|
import eventDemo.app.command.command.IWantToJoinTheGameCommand
|
||||||
|
import eventDemo.app.command.command.IWantToPlayCardCommand
|
||||||
|
import eventDemo.app.command.command.IamReadyToPlayCommand
|
||||||
|
import eventDemo.app.entity.GameId
|
||||||
|
import eventDemo.app.entity.Player
|
||||||
|
import eventDemo.app.event.GameEventStream
|
||||||
|
import eventDemo.app.event.buildStateFromEventStream
|
||||||
|
import eventDemo.app.event.event.disableShuffleDeck
|
||||||
|
import eventDemo.app.eventListener.GameEventPlayerNotificationListener
|
||||||
|
import eventDemo.app.eventListener.GameEventReactionListener
|
||||||
|
import eventDemo.app.notification.PlayerAsJoinTheGameNotification
|
||||||
|
import eventDemo.app.notification.PlayerAsPlayACardNotification
|
||||||
|
import eventDemo.app.notification.PlayerWasReadyNotification
|
||||||
|
import eventDemo.app.notification.TheGameWasStartedNotification
|
||||||
|
import eventDemo.app.notification.WelcomeToTheGameNotification
|
||||||
|
import eventDemo.configuration.appKoinModule
|
||||||
|
import eventDemo.send
|
||||||
|
import eventDemo.shared.toNotification
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.collections.shouldHaveSize
|
||||||
|
import io.kotest.matchers.equals.shouldBeEqual
|
||||||
|
import io.ktor.websocket.Frame
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.dsl.koinApplication
|
||||||
|
import kotlin.test.assertIs
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
@DelicateCoroutinesApi
|
||||||
|
class GameStateTest :
|
||||||
|
FunSpec({
|
||||||
|
test("Simulation of a game") {
|
||||||
|
disableShuffleDeck()
|
||||||
|
val id = GameId()
|
||||||
|
val player1 = Player(name = "Nikola")
|
||||||
|
val player2 = Player(name = "Einstein")
|
||||||
|
val channelIn1 = Channel<Frame>(Channel.BUFFERED)
|
||||||
|
val channelIn2 = Channel<Frame>(Channel.BUFFERED)
|
||||||
|
val channelOut1 = Channel<Frame>(Channel.BUFFERED)
|
||||||
|
val channelOut2 = Channel<Frame>(Channel.BUFFERED)
|
||||||
|
|
||||||
|
koinApplication { modules(appKoinModule) }.koin.apply {
|
||||||
|
val commandHandler by inject<GameCommandHandler>()
|
||||||
|
val playerNotificationListener by inject<GameEventPlayerNotificationListener>()
|
||||||
|
val eventStream by inject<GameEventStream>()
|
||||||
|
GameEventReactionListener(get(), get()).init()
|
||||||
|
playerNotificationListener.startListening(channelOut1, player1)
|
||||||
|
playerNotificationListener.startListening(channelOut2, player2)
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
commandHandler.handle(player1, channelIn1, channelOut1)
|
||||||
|
}
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
commandHandler.handle(player2, channelIn2, channelOut2)
|
||||||
|
}
|
||||||
|
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
channelIn1.send(IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(id, player1)))
|
||||||
|
}
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
delay(200)
|
||||||
|
channelIn2.send(IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(id, player2)))
|
||||||
|
}
|
||||||
|
channelOut1.receive().toNotification().let {
|
||||||
|
assertIs<WelcomeToTheGameNotification>(it).players shouldBeEqual setOf(player1)
|
||||||
|
}
|
||||||
|
|
||||||
|
channelOut2.receive().toNotification().let {
|
||||||
|
assertIs<WelcomeToTheGameNotification>(it).players shouldBeEqual setOf(player1, player2)
|
||||||
|
}
|
||||||
|
channelOut1.receive().toNotification().let {
|
||||||
|
assertIs<PlayerAsJoinTheGameNotification>(it).player shouldBeEqual player2
|
||||||
|
}
|
||||||
|
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
channelIn1.send(IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(id, player1)))
|
||||||
|
}
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
channelIn2.send(IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(id, player2)))
|
||||||
|
}
|
||||||
|
|
||||||
|
channelOut1.receive().toNotification().let {
|
||||||
|
assertIs<PlayerWasReadyNotification>(it).player shouldBeEqual player2
|
||||||
|
}
|
||||||
|
channelOut2.receive().toNotification().let {
|
||||||
|
assertIs<PlayerWasReadyNotification>(it).player shouldBeEqual player1
|
||||||
|
}
|
||||||
|
|
||||||
|
val player1Hand =
|
||||||
|
channelOut1.receive().toNotification().let {
|
||||||
|
assertIs<TheGameWasStartedNotification>(it).hand shouldHaveSize 7
|
||||||
|
}
|
||||||
|
val player2Hand =
|
||||||
|
channelOut2.receive().toNotification().let {
|
||||||
|
assertIs<TheGameWasStartedNotification>(it).hand shouldHaveSize 7
|
||||||
|
}
|
||||||
|
|
||||||
|
launch {
|
||||||
|
channelIn1.send(IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(id, player1, player1Hand.first())))
|
||||||
|
}
|
||||||
|
channelOut2.receive().toNotification().let {
|
||||||
|
assertIs<PlayerAsPlayACardNotification>(it).player shouldBeEqual player1
|
||||||
|
assertIs<PlayerAsPlayACardNotification>(it).card shouldBeEqual player1Hand.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
launch {
|
||||||
|
channelIn2.send(IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(id, player2, player2Hand.first())))
|
||||||
|
}
|
||||||
|
channelOut1.receive().toNotification().let {
|
||||||
|
assertIs<PlayerAsPlayACardNotification>(it).player shouldBeEqual player2
|
||||||
|
assertIs<PlayerAsPlayACardNotification>(it).card shouldBeEqual player2Hand.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
val state = id.buildStateFromEventStream(eventStream)
|
||||||
|
|
||||||
|
state.gameId 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.lastCard) shouldBeEqual GameState.LastCard(player2Hand.first(), player2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package eventDemo.app.query
|
package eventDemo.app.query
|
||||||
|
import eventDemo.app.entity.GameId
|
||||||
|
import eventDemo.shared.GameIdSerializer
|
||||||
import eventDemo.shared.UUIDSerializer
|
import eventDemo.shared.UUIDSerializer
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
@@ -17,6 +18,7 @@ fun ApplicationTestBuilder.httpClient(): HttpClient =
|
|||||||
serializersModule =
|
serializersModule =
|
||||||
SerializersModule {
|
SerializersModule {
|
||||||
contextual(UUID::class) { UUIDSerializer }
|
contextual(UUID::class) { UUIDSerializer }
|
||||||
|
contextual(GameId::class) { GameIdSerializer }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import io.ktor.websocket.Frame
|
|||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
@Serializable
|
||||||
class CommandTest(
|
class CommandTest(
|
||||||
override val id: CommandId,
|
override val id: CommandId,
|
||||||
) : Command
|
) : Command
|
||||||
@@ -15,16 +18,12 @@ class CommandStreamChannelTest :
|
|||||||
|
|
||||||
test("send and receive") {
|
test("send and receive") {
|
||||||
val command = CommandTest(CommandId())
|
val command = CommandTest(CommandId())
|
||||||
val command2 = CommandTest(CommandId())
|
|
||||||
val command3 = CommandTest(CommandId())
|
|
||||||
|
|
||||||
val channel = Channel<Frame>()
|
val channel = Channel<Frame>()
|
||||||
val stream =
|
val stream =
|
||||||
CommandStreamChannel<CommandTest>(
|
CommandStreamChannel<CommandTest>(
|
||||||
incoming = channel,
|
incoming = channel,
|
||||||
outgoing = channel,
|
deserializer = { Json.decodeFromString(it) },
|
||||||
serializer = { it.id.toString() },
|
|
||||||
deserializer = { CommandTest(CommandId(it)) },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val spyCall: () -> Unit = mockk(relaxed = true)
|
val spyCall: () -> Unit = mockk(relaxed = true)
|
||||||
@@ -33,8 +32,7 @@ class CommandStreamChannelTest :
|
|||||||
println("In action ${it.id}")
|
println("In action ${it.id}")
|
||||||
spyCall()
|
spyCall()
|
||||||
}
|
}
|
||||||
stream.send(command, command2)
|
channel.send(Frame.Text(Json.encodeToString(command)))
|
||||||
stream.send(command3)
|
verify(exactly = 1) { spyCall() }
|
||||||
verify(exactly = 3) { spyCall() }
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user