These are code examples for the “Deriving state from events” article:
All gists:
-
Deriving state from events
These are code examples for the “Deriving state from events” article:
All gists:
Deriving state from events
| typealias Game = List<GameEvent> | |
| private val Game.secret: Code? | |
| get() = filterIsInstance<GameStarted>().firstOrNull()?.secret | |
| private val Game.secretLength: Int | |
| get() = secret?.length ?: 0 | |
| private val Game.secretPegs: List<Code.Peg> | |
| get() = secret?.pegs ?: emptyList() | |
| private val Game.attempts: Int | |
| get() = filterIsInstance<GuessMade>().size | |
| private val Game.totalAttempts: Int | |
| get() = filterIsInstance<GameStarted>().firstOrNull()?.totalAttempts ?: 0 | |
| private val Game.availablePegs: Set<Code.Peg> | |
| get() = filterIsInstance<GameStarted>().firstOrNull()?.availablePegs ?: emptySet() | |
| private fun Game.isWon(): Boolean = | |
| filterIsInstance<GameWon>().isNotEmpty() | |
| private fun Game.isLost(): Boolean = | |
| filterIsInstance<GameLost>().isNotEmpty() | |
| private fun Game.isStarted(): Boolean = | |
| filterIsInstance<GameStarted>().isNotEmpty() | |
| private fun Game.isGuessTooShort(guess: Code): Boolean = | |
| guess.length < secretLength | |
| private fun Game.isGuessTooLong(guess: Code): Boolean = | |
| guess.length > secretLength | |
| private fun Game.isGuessValid(guess: Code): Boolean = | |
| availablePegs.containsAll(guess.pegs) |
| data class Game( | |
| val secret: Code, | |
| val attempts: Int, | |
| val totalAttempts: Int, | |
| val availablePegs: Set<Code.Peg>, | |
| val outcome: Feedback.Outcome | |
| ) { | |
| val secretLength: Int = secret.length | |
| val secretPegs: List<Code.Peg> = secret.pegs | |
| fun isWon(): Boolean = outcome == WON | |
| fun isLost(): Boolean = outcome == LOST | |
| fun isGuessTooShort(guess: Code): Boolean = | |
| guess.length < secretLength | |
| fun isGuessTooLong(guess: Code): Boolean = | |
| guess.length > secretLength | |
| fun isGuessValid(guess: Code): Boolean = | |
| availablePegs.containsAll(guess.pegs) | |
| } | |
| fun applyEvent( | |
| game: Game?, | |
| event: GameEvent | |
| ): Game? = when (event) { | |
| is GameStarted -> Game(event.secret, 0, event.totalAttempts, event.availablePegs, IN_PROGRESS) | |
| is GuessMade -> game?.copy(attempts = game.attempts + 1) | |
| is GameWon -> game?.copy(outcome = WON) | |
| is GameLost -> game?.copy(outcome = LOST) | |
| } | |
| val events: List<GameEvent> = emptyList() | |
| val state = events.fold(null, ::applyEvent) |
| sealed interface Game | |
| data object NotStartedGame : Game | |
| data class StartedGame( | |
| val secret: Code, | |
| val attempts: Int, | |
| val totalAttempts: Int, | |
| val availablePegs: Set<Code.Peg>, | |
| val outcome: Feedback.Outcome | |
| ) : Game { | |
| val secretLength: Int = secret.length | |
| val secretPegs: List<Code.Peg> = secret.pegs | |
| fun isWon(): Boolean = outcome == WON | |
| fun isLost(): Boolean = outcome == LOST | |
| fun isGuessTooShort(guess: Code): Boolean = | |
| guess.length < secretLength | |
| fun isGuessTooLong(guess: Code): Boolean = | |
| guess.length > secretLength | |
| fun isGuessValid(guess: Code): Boolean = | |
| availablePegs.containsAll(guess.pegs) | |
| } | |
| fun applyEvent( | |
| game: Game, | |
| event: GameEvent | |
| ): Game = when (game) { | |
| is NotStartedGame -> when (event) { | |
| is GameStarted -> StartedGame(event.secret, 0, event.totalAttempts, event.availablePegs, IN_PROGRESS) | |
| else -> game | |
| } | |
| is StartedGame -> when (event) { | |
| is GameStarted -> game | |
| is GuessMade -> game.copy(attempts = game.attempts + 1) | |
| is GameWon -> game.copy(outcome = WON) | |
| is GameLost -> game.copy(outcome = LOST) | |
| } | |
| } | |
| private fun startedNotFinishedGame(command: MakeGuess, game: Game): Either<GameError, StartedGame> { | |
| if (game !is StartedGame) { | |
| return GameNotStarted(command.gameId).left() | |
| } | |
| if (game.isWon()) { | |
| return GameAlreadyWon(command.gameId).left() | |
| } | |
| if (game.isLost()) { | |
| return GameAlreadyLost(command.gameId).left() | |
| } | |
| return game.right() | |
| } | |
| private fun validGuess(command: MakeGuess, game: StartedGame): Either<GameError, Code> { | |
| if (game.isGuessTooShort(command.guess)) { | |
| return GuessTooShort(command.gameId, command.guess, game.secretLength).left() | |
| } | |
| if (game.isGuessTooLong(command.guess)) { | |
| return GuessTooLong(command.gameId, command.guess, game.secretLength).left() | |
| } | |
| if (!game.isGuessValid(command.guess)) { | |
| return InvalidPegInGuess(command.gameId, command.guess, game.availablePegs).left() | |
| } | |
| return command.guess.right() | |
| } | |
| private fun StartedGame.feedbackOn(guess: Code): Feedback = | |
| feedbackPegsOn(guess) | |
| .let { (exactHits, colourHits) -> | |
| Feedback(outcomeFor(exactHits), exactHits + colourHits) | |
| } | |
| private fun StartedGame.feedbackPegsOn(guess: Code) = | |
| exactHits(guess).map { BLACK } to colourHits(guess).map { WHITE } | |
| private fun StartedGame.outcomeFor(exactHits: List<Feedback.Peg>) = when { | |
| exactHits.size == this.secretLength -> WON | |
| this.attempts + 1 == this.totalAttempts -> LOST | |
| else -> IN_PROGRESS | |
| } | |
| private fun StartedGame.exactHits(guess: Code): List<Code.Peg> = this.secretPegs | |
| .zip(guess.pegs) | |
| .filter { (secretColour, guessColour) -> secretColour == guessColour } | |
| .unzip() | |
| .second | |
| private fun StartedGame.colourHits(guess: Code): List<Code.Peg> = this.secretPegs | |
| .zip(guess.pegs) | |
| .filter { (secretColour, guessColour) -> secretColour != guessColour } | |
| .unzip() | |
| .let { (secret, guess) -> | |
| guess.fold(secret to emptyList<Code.Peg>()) { (secretPegs, colourHits), guessPeg -> | |
| secretPegs.remove(guessPeg)?.let { it to colourHits + guessPeg } ?: (secretPegs to colourHits) | |
| }.second | |
| } | |
| fun notStartedGame(): Game = NotStartedGame |
| sealed interface Game | |
| data object NotStartedGame : Game | |
| data class StartedGame( | |
| val secret: Code, | |
| val attempts: Int, | |
| val totalAttempts: Int, | |
| val availablePegs: Set<Code.Peg> | |
| ) : Game { | |
| val secretLength: Int = secret.length | |
| val secretPegs: List<Code.Peg> = secret.pegs | |
| fun isGuessTooShort(guess: Code): Boolean = | |
| guess.length < secretLength | |
| fun isGuessTooLong(guess: Code): Boolean = | |
| guess.length > secretLength | |
| fun isGuessValid(guess: Code): Boolean = | |
| availablePegs.containsAll(guess.pegs) | |
| } | |
| data class WonGame( | |
| val secret: Code, | |
| val attempts: Int, | |
| val totalAttempts: Int, | |
| ) : Game | |
| data class LostGame( | |
| val secret: Code, | |
| val totalAttempts: Int, | |
| ) : Game | |
| fun applyEvent( | |
| game: Game, | |
| event: GameEvent | |
| ): Game = when (game) { | |
| is NotStartedGame -> when (event) { | |
| is GameStarted -> StartedGame(event.secret, 0, event.totalAttempts, event.availablePegs) | |
| else -> game | |
| } | |
| is StartedGame -> when (event) { | |
| is GameStarted -> game | |
| is GuessMade -> game.copy(attempts = game.attempts + 1) | |
| is GameWon -> WonGame(secret = game.secret, attempts = game.attempts, totalAttempts = game.totalAttempts) | |
| is GameLost -> LostGame(secret = game.secret, totalAttempts = game.totalAttempts) | |
| } | |
| is WonGame -> game | |
| is LostGame -> game | |
| } | |
| private fun startedNotFinishedGame(command: MakeGuess, game: Game): Either<GameError, StartedGame> = when (game) { | |
| is NotStartedGame -> GameNotStarted(command.gameId).left() | |
| is WonGame -> GameAlreadyWon(command.gameId).left() | |
| is LostGame -> GameAlreadyLost(command.gameId).left() | |
| is StartedGame -> game.right() | |
| } |
| fun <COMMAND : Any, EVENT : Any, ERROR : Any, STATE> handlerFor( | |
| execute: (COMMAND, STATE) -> Either<ERROR, NonEmptyList<EVENT>>, | |
| applyEvent: (STATE, EVENT) -> STATE, | |
| initialState: () -> STATE, | |
| ): (COMMAND, List<EVENT>) -> Either<ERROR, NonEmptyList<EVENT>> = | |
| { command, events -> execute(command, events.fold(initialState(), applyEvent)) } | |
| val handler = handlerFor(::execute, ::applyEvent) { null } | |
| val handler = handlerFor(::execute, ::applyEvent) { NotStartedGame } | |
| handler(command, events) |