Skip to content

Instantly share code, notes, and snippets.

@jakzal
Last active November 30, 2023 14:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jakzal/83a3d282214c704a649fdc2781a06f54 to your computer and use it in GitHub Desktop.
Save jakzal/83a3d282214c704a649fdc2781a06f54 to your computer and use it in GitHub Desktop.
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment