Skip to content

Instantly share code, notes, and snippets.

@LordRaydenMK
Created March 26, 2020 18:34
Show Gist options
  • Save LordRaydenMK/b45c1f3e67a1167d5982511a81365904 to your computer and use it in GitHub Desktop.
Save LordRaydenMK/b45c1f3e67a1167d5982511a81365904 to your computer and use it in GitHub Desktop.
Functional Hangman with IO<E, A> from Arrow
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