Skip to content

Instantly share code, notes, and snippets.

@LordRaydenMK
Last active November 17, 2018 20:09
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 LordRaydenMK/0be8f70f860a862e69daf262b4a83e17 to your computer and use it in GitHub Desktop.
Save LordRaydenMK/0be8f70f860a862e69daf262b4a83e17 to your computer and use it in GitHub Desktop.
Functional Hangman abstracted over the container type
package io.github.lordraydenmk.tf
import arrow.Kind
import arrow.core.getOrElse
import arrow.core.right
import arrow.core.toOption
import arrow.effects.IO
import arrow.effects.SingleK
import arrow.effects.fix
import arrow.effects.instances.io.monad.flatMap
import arrow.effects.instances.io.monad.monad
import arrow.effects.instances.io.monadDefer.monadDefer
import arrow.effects.singlek.async.async
import arrow.effects.singlek.monadDefer.monadDefer
import arrow.effects.typeclasses.Async
import arrow.effects.typeclasses.MonadDefer
import arrow.typeclasses.binding
import java.io.IOException
import kotlin.random.Random
import kotlin.streams.toList
fun <F> putStrLn(A: MonadDefer<F>, line: String): Kind<F, Unit> = A {
println(line)
}
fun <F> getStrLn(A: MonadDefer<F>): Kind<F, String> = A {
readLine() ?: throw IOException("Failed to read input!")
}
class Hangman<F>(val A: MonadDefer<F>) {
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()
}
val hangman: Kind<F, Unit> = A.binding {
putStrLn(A, "Welcome to purely functional hangman").bind()
val name = getName.bind()
putStrLn(A, "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): Kind<F, State> = A.binding {
val guess = getChoice().bind()
val updatedState = state.copy(guesses = state.guesses.plus(guess))
renderState(updatedState).bind()
val loop = when {
updatedState.playerWon -> putStrLn(A, "Congratulations ${state.name} you won the game").map { false }
updatedState.playerLost -> putStrLn(A, "Sorry ${state.name} you lost the game. The word was ${state.word}").map { false }
updatedState.word.contains(guess) -> putStrLn(A, "You guessed correctly!").map { true }
else -> putStrLn(A, "That's wrong, but keep trying").map { true }
}.bind()
if (loop) gameLoop(updatedState).bind() else updatedState
}
val getName: Kind<F, String> = A.binding {
putStrLn(A, "What is your name: ").bind()
getStrLn(A).bind()
}
fun getChoice(): Kind<F, Char> = A.binding {
putStrLn(A, "Please enter a letter").bind()
val line = getStrLn(A).bind()
val char = line.toLowerCase().first().toOption().fold(
{
putStrLn(A, "Please enter a letter")
.flatMap { getChoice() }
},
{ char ->
A { char }
}
).bind()
char
}
fun nextInt(max: Int): Kind<F, Int> = A { Random.nextInt(max) }
val chooseWord: Kind<F, String> = A.binding {
val rand = nextInt(dictionary.size).bind()
dictionary[rand]
}
fun renderState(state: State): Kind<F, Unit> {
val word = state.word.toList().map { if (state.guesses.contains(it)) " $it " else " " }
.joinToString("")
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(A, text)
}
}
fun main() {
Hangman(SingleK.monadDefer()).hangman.fix().single.subscribe()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment