Created
March 26, 2020 18:34
-
-
Save LordRaydenMK/b45c1f3e67a1167d5982511a81365904 to your computer and use it in GitHub Desktop.
Functional Hangman with IO<E, A> from Arrow
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package io.github.lordraydenmk.hangman | |
import arrow.fx.IO | |
import arrow.fx.extensions.fx | |
import arrow.fx.flatMap | |
import java.io.IOException | |
import kotlin.random.Random | |
import kotlin.streams.toList | |
// region utils | |
fun <E, A> IO.Companion.syncCatch(f: (Throwable) -> E, effect: () -> A): IO<E, A> = | |
defer { | |
try { | |
just(effect()) | |
} catch (t: Throwable) { | |
raiseError<E, A>(f(t)) | |
} | |
} | |
typealias UIO<A> = IO<Nothing, A> | |
// endregion | |
val ioException = { t: Throwable -> if (t is IOException) t else throw NotImplementedError() } | |
fun getStrLn(): IO<IOException, String> = IO.syncCatch(ioException) { readLine().orEmpty() } | |
fun putStrLn(line: String): IO<IOException, Unit> = | |
IO.syncCatch(ioException) { println(line) } | |
object Hangman { | |
data class State(val name: String, val guesses: Set<Char> = emptySet(), val word: String) { | |
val failures: Int = (guesses.toSet().minus(word.toSet())).size | |
val playerLost: Boolean = failures > 8 | |
val playerWon: Boolean = (word.toSet().minus(guesses)).isEmpty() | |
} | |
val dictionary: List<String> by lazy { | |
javaClass.classLoader.getResource("words.txt") | |
.openStream() | |
.bufferedReader() | |
.lines() | |
.toList() | |
} | |
fun hangman(): IO<IOException, Unit> = IO.fx<IOException, Unit> { | |
putStrLn("Welcome to purely functional hangman").bind() | |
val name = getName.bind() | |
putStrLn("Welcome $name. Let's begin!").bind() | |
val word = chooseWord.bind() | |
val state = State(name, word = word) | |
renderState(state).bind() | |
gameLoop(state).bind() | |
Unit | |
} | |
fun gameLoop(state: State): IO<IOException, State> = IO.fx<IOException, State> { | |
val guess = getChoice().bind() | |
val updatedState = state.copy(guesses = state.guesses.plus(guess)) | |
renderState(updatedState).bind() | |
val loop = when { | |
updatedState.playerWon -> putStrLn("Congratulations ${state.name} you won the game").map { false } | |
updatedState.playerLost -> putStrLn("Sorry ${state.name} you lost the game. The word was ${state.word}").map { false } | |
updatedState.word.contains(guess) -> putStrLn("You guessed correctly!").map { true } | |
else -> putStrLn("That's wrong, but keep trying").map { true } | |
}.bind() | |
if (loop) gameLoop(updatedState).bind() else updatedState | |
} | |
val getName: IO<IOException, String> = IO.fx<IOException, String> { | |
putStrLn("What is your name").bind() | |
val name = getStrLn().bind() | |
name | |
} | |
fun getChoice(): IO<IOException, Char> = IO.fx<IOException, Char> { | |
putStrLn("Pleas enter a letter: ").bind() | |
val line = getStrLn().bind() | |
val char = line.toLowerCase() | |
.trim() | |
.firstOrNull() | |
?.let { IO.just(it) } ?: putStrLn("You did not enter a character!").flatMap { getChoice() } | |
char.bind() | |
} | |
fun nextInt(max: Int): UIO<Int> = IO { Random.nextInt(max) } | |
val chooseWord: IO<IOException, String> = IO.fx<IOException, String> { | |
val rand = nextInt(dictionary.size).bind() | |
dictionary[rand] | |
} | |
fun renderState(state: State): IO<IOException, Unit> { | |
val word = state.word.toList().joinToString("") { if (state.guesses.contains(it)) " $it " else " " } | |
val line = state.word.map { " - " }.joinToString("") | |
val guesses = "Guesses: ${state.guesses.toList().sorted().joinToString("")}" | |
val text = "$word\n$line\n\n$guesses\n" | |
return putStrLn(text) | |
} | |
} | |
fun main() { | |
Hangman.hangman().unsafeRunSyncEither() | |
.fold( | |
{ it.printStackTrace() }, | |
{ println(it) } | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment