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