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:
@@ -14,7 +14,7 @@ data class GameState(
|
||||
val lastCard: LastCard? = null,
|
||||
val lastColor: Card.Color? = null,
|
||||
val direction: Direction = Direction.CLOCKWISE,
|
||||
val readyPlayers: List<Player> = emptyList(),
|
||||
val readyPlayers: Set<Player> = emptySet(),
|
||||
val deck: Deck = Deck(players),
|
||||
val isStarted: Boolean = false,
|
||||
) {
|
||||
@@ -51,6 +51,8 @@ data class GameState(
|
||||
}
|
||||
|
||||
private val nextPlayerIndex: Int get() {
|
||||
if (players.size == 0) return 0
|
||||
|
||||
val y =
|
||||
if (direction == Direction.CLOCKWISE) {
|
||||
+1
|
||||
@@ -61,7 +63,13 @@ data class GameState(
|
||||
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)
|
||||
|
||||
@@ -79,7 +87,8 @@ data class GameState(
|
||||
|
||||
fun playableCards(player: Player): List<Card> =
|
||||
deck
|
||||
.playersHands[player]
|
||||
.playersHands
|
||||
.getHand(player)
|
||||
?.filter { canBePlayThisCard(player, it) }
|
||||
?: emptyList()
|
||||
|
||||
|
||||
@@ -10,15 +10,13 @@ import eventDemo.app.entity.Player
|
||||
import eventDemo.app.event.GameEventStream
|
||||
import eventDemo.app.event.buildStateFromEventStream
|
||||
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 kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Listen [GameCommand] on [GameCommandStream], check the validity and execute an action.
|
||||
@@ -28,26 +26,32 @@ import kotlinx.coroutines.launch
|
||||
class GameCommandHandler(
|
||||
private val eventStream: GameEventStream,
|
||||
) {
|
||||
private val logger = KotlinLogging.logger { }
|
||||
|
||||
/**
|
||||
* Init the handler
|
||||
*/
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun handle(
|
||||
suspend fun handle(
|
||||
player: Player,
|
||||
incoming: ReceiveChannel<Frame>,
|
||||
outgoing: SendChannel<Frame>,
|
||||
): Job {
|
||||
val commandStream = GameCommandStream(incoming, outgoing)
|
||||
val playerNotifier: (String) -> Unit = { outgoing.trySendBlocking(Frame.Text(it)) }
|
||||
return GlobalScope.launch {
|
||||
init(player, commandStream, playerNotifier)
|
||||
) {
|
||||
val commandStream = GameCommandStream(incoming)
|
||||
val playerErrorNotifier: (String) -> Unit = {
|
||||
val notification = ErrorNotification(message = it)
|
||||
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(
|
||||
player: Player,
|
||||
commandStream: GameCommandStream,
|
||||
playerNotifier: (String) -> Unit,
|
||||
playerErrorNotifier: (String) -> Unit,
|
||||
) {
|
||||
commandStream.process { command ->
|
||||
if (command.payload.player.id != player.id) {
|
||||
@@ -57,12 +61,12 @@ class GameCommandHandler(
|
||||
val gameState = command.buildGameState()
|
||||
|
||||
when (command) {
|
||||
is IWantToPlayCardCommand -> command.run(gameState, playerNotifier, eventStream)
|
||||
is IamReadyToPlayCommand -> command.run(gameState, playerNotifier, eventStream)
|
||||
is IWantToJoinTheGameCommand -> command.run(gameState, playerNotifier, eventStream)
|
||||
is ICantPlayCommand -> command.run(gameState, playerNotifier, eventStream)
|
||||
is IWantToPlayCardCommand -> command.run(gameState, playerErrorNotifier, eventStream)
|
||||
is IamReadyToPlayCommand -> command.run(gameState, playerErrorNotifier, eventStream)
|
||||
is IWantToJoinTheGameCommand -> command.run(gameState, playerErrorNotifier, eventStream)
|
||||
is ICantPlayCommand -> command.run(gameState, playerErrorNotifier, eventStream)
|
||||
}
|
||||
} as CommandBlock<GameCommand>
|
||||
}
|
||||
}
|
||||
|
||||
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.routing.Route
|
||||
import io.ktor.server.websocket.webSocket
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
fun Route.gameSocket(
|
||||
playerNotificationListener: GameEventPlayerNotificationListener,
|
||||
commandHandler: GameCommandHandler,
|
||||
) {
|
||||
authenticate {
|
||||
webSocket("/game") {
|
||||
commandHandler.handle(call.getPlayer(), incoming, outgoing)
|
||||
playerNotificationListener.startListening(outgoing)
|
||||
val currentPlayer = call.getPlayer()
|
||||
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 io.ktor.websocket.Frame
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
@@ -19,10 +18,7 @@ class GameCommandStreamInMemory : CommandStreamInMemory<GameCommand>()
|
||||
*/
|
||||
class GameCommandStream(
|
||||
incoming: ReceiveChannel<Frame>,
|
||||
outgoing: SendChannel<Frame>,
|
||||
) : CommandStream<GameCommand> by CommandStreamChannel(
|
||||
incoming,
|
||||
outgoing,
|
||||
{ Json.encodeToString(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.event.PlayerHavePassEvent
|
||||
import eventDemo.libs.command.CommandId
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* A command to perform an action to play a new card
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("Pass")
|
||||
data class ICantPlayCommand(
|
||||
override val payload: Payload,
|
||||
) : GameCommand {
|
||||
@@ -27,19 +25,22 @@ data class ICantPlayCommand(
|
||||
|
||||
fun run(
|
||||
state: GameState,
|
||||
playerNotifier: (String) -> Unit,
|
||||
playerErrorNotifier: (String) -> Unit,
|
||||
eventStream: GameEventStream,
|
||||
) {
|
||||
val playableCards = state.playableCards(payload.player)
|
||||
if (playableCards.isEmpty()) {
|
||||
val takenCard = state.deck.stack.first()
|
||||
|
||||
eventStream.publish(
|
||||
PlayerHavePassEvent(
|
||||
payload.gameId,
|
||||
payload.player,
|
||||
gameId = payload.gameId,
|
||||
player = payload.player,
|
||||
takenCard = takenCard,
|
||||
),
|
||||
)
|
||||
} 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.event.NewPlayerEvent
|
||||
import eventDemo.libs.command.CommandId
|
||||
import kotlinx.serialization.SerialName
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* A command to perform an action to play a new card
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("JoinGame")
|
||||
data class IWantToJoinTheGameCommand(
|
||||
override val payload: Payload,
|
||||
) : GameCommand {
|
||||
@@ -27,9 +26,10 @@ data class IWantToJoinTheGameCommand(
|
||||
|
||||
fun run(
|
||||
state: GameState,
|
||||
playerNotifier: (String) -> Unit,
|
||||
playerErrorNotifier: (String) -> Unit,
|
||||
eventStream: GameEventStream,
|
||||
) {
|
||||
val logger = KotlinLogging.logger {}
|
||||
if (!state.isStarted) {
|
||||
eventStream.publish(
|
||||
NewPlayerEvent(
|
||||
@@ -38,7 +38,11 @@ data class IWantToJoinTheGameCommand(
|
||||
),
|
||||
)
|
||||
} 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.event.CardIsPlayedEvent
|
||||
import eventDemo.libs.command.CommandId
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* A command to perform an action to play a new card
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("PlayCard")
|
||||
data class IWantToPlayCardCommand(
|
||||
override val payload: Payload,
|
||||
) : GameCommand {
|
||||
@@ -29,11 +27,11 @@ data class IWantToPlayCardCommand(
|
||||
|
||||
fun run(
|
||||
state: GameState,
|
||||
playerNotifier: (String) -> Unit,
|
||||
playerErrorNotifier: (String) -> Unit,
|
||||
eventStream: GameEventStream,
|
||||
) {
|
||||
if (!state.isReady) {
|
||||
playerNotifier("The game is Not started")
|
||||
if (!state.isStarted) {
|
||||
playerErrorNotifier("The game is Not started")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -46,7 +44,7 @@ data class IWantToPlayCardCommand(
|
||||
),
|
||||
)
|
||||
} 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.event.PlayerReadyEvent
|
||||
import eventDemo.libs.command.CommandId
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* A command to set as ready to play
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("Ready")
|
||||
data class IamReadyToPlayCommand(
|
||||
override val payload: Payload,
|
||||
) : GameCommand {
|
||||
@@ -27,13 +25,16 @@ data class IamReadyToPlayCommand(
|
||||
|
||||
fun run(
|
||||
state: GameState,
|
||||
playerNotifier: (String) -> Unit,
|
||||
playerErrorNotifier: (String) -> Unit,
|
||||
eventStream: GameEventStream,
|
||||
) {
|
||||
val playerExist: Boolean = state.players.contains(payload.player)
|
||||
val playerIsAlreadyReady: Boolean = state.readyPlayers.contains(payload.player)
|
||||
|
||||
if (playerIsAlreadyReady) {
|
||||
playerNotifier("You are already ready")
|
||||
if (!playerExist) {
|
||||
playerErrorNotifier("You are not in the game")
|
||||
} else if (playerIsAlreadyReady) {
|
||||
playerErrorNotifier("You are already ready")
|
||||
} else {
|
||||
eventStream.publish(
|
||||
PlayerReadyEvent(
|
||||
|
||||
@@ -11,6 +11,8 @@ data class Deck(
|
||||
constructor(players: Set<Player>) :
|
||||
this(playersHands = PlayersHands(players))
|
||||
|
||||
fun shuffle(): Deck = copy(stack = stack.shuffle())
|
||||
|
||||
fun placeFirstCardOnDiscard(): Deck {
|
||||
val takenCard = stack.first()
|
||||
return copy(
|
||||
@@ -21,16 +23,9 @@ data class Deck(
|
||||
|
||||
fun takeOneCardFromStackTo(player: Player): Deck =
|
||||
takeOne().let { (deck, newPlayerCard) ->
|
||||
val newHands =
|
||||
deck.playersHands
|
||||
.mapValues { (p, cards) ->
|
||||
if (p == player) {
|
||||
cards + newPlayerCard
|
||||
} else {
|
||||
cards
|
||||
}
|
||||
}.toPlayersHands()
|
||||
deck.copy(playersHands = newHands)
|
||||
deck.copy(
|
||||
playersHands = deck.playersHands.addCard(player, newPlayerCard),
|
||||
)
|
||||
}
|
||||
|
||||
fun putOneCardFromHand(
|
||||
@@ -40,7 +35,7 @@ data class Deck(
|
||||
run {
|
||||
// Validate parameters
|
||||
val playerHand =
|
||||
playersHands[player]
|
||||
playersHands.getHand(player)
|
||||
?: error("No player on this game")
|
||||
if (playerHand.none { it == card }) {
|
||||
error("No card exist on the player hand")
|
||||
@@ -70,8 +65,7 @@ data class Deck(
|
||||
(1..2).map { Card.PassCard(color) }
|
||||
}.let {
|
||||
it + (1..4).map { Card.Plus4Card() }
|
||||
}.shuffled()
|
||||
.toStack()
|
||||
}.toStack()
|
||||
.let { Deck(it) }
|
||||
}
|
||||
}
|
||||
@@ -100,6 +94,8 @@ value class Stack(
|
||||
operator fun plus(card: Card): Stack = cards.plus(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())
|
||||
|
||||
@@ -12,4 +12,6 @@ import java.util.UUID
|
||||
@Serializable(with = GameIdSerializer::class)
|
||||
value class GameId(
|
||||
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)),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class PlayerId(
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
|
||||
@@ -5,28 +5,38 @@ import kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class PlayersHands(
|
||||
private val map: Map<Player, List<Card>> = emptyMap(),
|
||||
) : Map<Player, List<Card>> by map {
|
||||
constructor(players: Set<Player>) : this(players.associateWith { emptyList<Card>() }.toPlayersHands())
|
||||
private val map: Map<Player.PlayerId, List<Card>> = emptyMap(),
|
||||
) : Map<Player.PlayerId, List<Card>> by map {
|
||||
constructor(players: Set<Player>) :
|
||||
this(players.map { it.id }.associateWith { emptyList<Card>() }.toPlayersHands())
|
||||
|
||||
fun getHand(player: Player): List<Card>? = this[player.id]
|
||||
|
||||
fun removeCard(
|
||||
player: Player,
|
||||
card: Card,
|
||||
): PlayersHands =
|
||||
mapValues { (p, cards) ->
|
||||
if (p == player) {
|
||||
mapValues { (playerId, cards) ->
|
||||
if (playerId == player.id) {
|
||||
if (!cards.contains(card)) error("The hand no contain the card")
|
||||
cards - card
|
||||
} else {
|
||||
cards
|
||||
}
|
||||
}.toPlayersHands()
|
||||
|
||||
fun addCard(
|
||||
player: Player,
|
||||
newCard: Card,
|
||||
): PlayersHands = addCards(player, listOf(newCard))
|
||||
|
||||
fun addCards(
|
||||
player: Player,
|
||||
newCards: List<Card>,
|
||||
): PlayersHands =
|
||||
mapValues { (p, cards) ->
|
||||
if (p == player) {
|
||||
if (p == player.id) {
|
||||
if (cards.intersect(newCards).isNotEmpty()) error("The hand already contain the card")
|
||||
cards + newCards
|
||||
} else {
|
||||
cards
|
||||
@@ -34,4 +44,4 @@ value class PlayersHands(
|
||||
}.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.PlayerHavePassEvent
|
||||
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(
|
||||
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 =
|
||||
events.fold(GameState(this)) { state: GameState, event: GameEvent ->
|
||||
when (event) {
|
||||
@@ -37,12 +41,13 @@ private fun GameId.buildStateFromEvents(events: List<GameEvent>): GameState =
|
||||
lastPlayer = event.player,
|
||||
direction = direction,
|
||||
lastColor = color,
|
||||
lastCard = GameState.LastCard(event.card, event.player),
|
||||
deck = state.deck.putOneCardFromHand(event.player, event.card),
|
||||
)
|
||||
}
|
||||
|
||||
is NewPlayerEvent -> {
|
||||
if (state.isReady) error("The game is already started")
|
||||
if (state.isStarted) error("The game is already started")
|
||||
|
||||
state.copy(
|
||||
players = state.players + event.player,
|
||||
@@ -56,6 +61,7 @@ private fun GameId.buildStateFromEvents(events: List<GameEvent>): GameState =
|
||||
}
|
||||
|
||||
is PlayerHavePassEvent -> {
|
||||
if (event.takenCard != state.deck.stack.first()) error("taken card is not ot top of the stack")
|
||||
state.copy(
|
||||
lastPlayer = event.player,
|
||||
deck = state.deck.takeOneCardFromStackTo(event.player),
|
||||
@@ -70,7 +76,7 @@ private fun GameId.buildStateFromEvents(events: List<GameEvent>): GameState =
|
||||
|
||||
is GameStartedEvent -> {
|
||||
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),
|
||||
lastPlayer = event.firstPlayer,
|
||||
deck = event.deck,
|
||||
|
||||
@@ -8,7 +8,7 @@ import eventDemo.app.entity.Player
|
||||
* An [GameEvent] to represent a played card.
|
||||
*/
|
||||
data class CardIsPlayedEvent(
|
||||
override val id: GameId,
|
||||
override val gameId: GameId,
|
||||
val card: Card,
|
||||
val player: Player,
|
||||
) : GameEvent
|
||||
|
||||
@@ -9,5 +9,5 @@ import kotlinx.serialization.Serializable
|
||||
*/
|
||||
@Serializable
|
||||
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.
|
||||
*/
|
||||
data class GameStartedEvent(
|
||||
override val id: GameId,
|
||||
override val gameId: GameId,
|
||||
val firstPlayer: Player,
|
||||
val deck: Deck,
|
||||
) : GameEvent {
|
||||
@@ -19,9 +19,20 @@ data class GameStartedEvent(
|
||||
players: Set<Player>,
|
||||
): GameStartedEvent =
|
||||
GameStartedEvent(
|
||||
id = id,
|
||||
firstPlayer = players.random(),
|
||||
deck = Deck.newWithoutPlayers().initHands(players).placeFirstCardOnDiscard(),
|
||||
gameId = id,
|
||||
firstPlayer = if (isDisabled) players.first() else players.random(),
|
||||
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.
|
||||
*/
|
||||
data class NewPlayerEvent(
|
||||
override val id: GameId,
|
||||
override val gameId: GameId,
|
||||
val player: Player,
|
||||
) : GameEvent
|
||||
|
||||
@@ -8,7 +8,7 @@ import eventDemo.app.entity.Player
|
||||
* This [GameEvent] is sent when a player chose a color.
|
||||
*/
|
||||
data class PlayerChoseColorEvent(
|
||||
override val id: GameId,
|
||||
override val gameId: GameId,
|
||||
val player: Player,
|
||||
val color: Card.Color,
|
||||
) : GameEvent
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eventDemo.app.event.event
|
||||
|
||||
import eventDemo.app.entity.Card
|
||||
import eventDemo.app.entity.GameId
|
||||
import eventDemo.app.entity.Player
|
||||
|
||||
@@ -7,6 +8,7 @@ import eventDemo.app.entity.Player
|
||||
* This [GameEvent] is sent when a player can play.
|
||||
*/
|
||||
data class PlayerHavePassEvent(
|
||||
override val id: GameId,
|
||||
override val gameId: GameId,
|
||||
val player: Player,
|
||||
val takenCard: Card,
|
||||
) : GameEvent
|
||||
|
||||
@@ -7,6 +7,6 @@ import eventDemo.app.entity.Player
|
||||
* This [GameEvent] is sent when a player is ready.
|
||||
*/
|
||||
data class PlayerReadyEvent(
|
||||
override val id: GameId,
|
||||
override val gameId: GameId,
|
||||
val player: Player,
|
||||
) : GameEvent
|
||||
|
||||
@@ -1,18 +1,126 @@
|
||||
package eventDemo.app.eventListener
|
||||
|
||||
import eventDemo.app.entity.Player
|
||||
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.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 io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.ktor.websocket.Frame
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
|
||||
class GameEventPlayerNotificationListener(
|
||||
private val eventBus: GameEventBus,
|
||||
private val eventStream: GameEventStream,
|
||||
) {
|
||||
fun startListening(outgoing: SendChannel<Frame>) {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
fun startListening(
|
||||
outgoing: SendChannel<Frame>,
|
||||
currentPlayer: Player,
|
||||
) {
|
||||
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,24 +2,40 @@ package eventDemo.app.eventListener
|
||||
|
||||
import eventDemo.app.event.GameEventBus
|
||||
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.GameStartedEvent
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
|
||||
class GameEventReactionListener(
|
||||
private val eventBus: GameEventBus,
|
||||
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() {
|
||||
eventBus.subscribe { event: GameEvent ->
|
||||
val state = event.id.buildStateFromEventStream(eventStream)
|
||||
if (state.isReady) {
|
||||
eventStream.publish(
|
||||
eventBus.subscribe(priority) { event: GameEvent ->
|
||||
val state = event.buildStateFromEventStreamTo(eventStream)
|
||||
if (state.isReady && !state.isStarted) {
|
||||
val reactionEvent =
|
||||
GameStartedEvent.new(
|
||||
state.gameId,
|
||||
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
|
||||
Reference in New Issue
Block a user