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) |