idris-0.10: test/effects003/hangman.idr
module Main
import Effects
import Effect.StdIO
import Effect.Random
import Data.So
import Data.Fin
import Data.Vect
import VectMissing
-----------------------------------------------------------------------
-- GAME STATE
-----------------------------------------------------------------------
{- First, the game state, HState, where the type specifies how many guesses
are left and how many missing letters there are still to get. -}
data HState = Running Nat Nat | NotRunning
data Hangman : HState -> Type where
Init : Hangman NotRunning -- initialising, but not ready
GameWon : String -> Hangman NotRunning
GameLost : String -> Hangman NotRunning
MkH : (word : String) ->
(guesses : Nat) ->
(got : List Char) ->
(missing : Vect m Char) ->
Hangman (Running guesses m)
implementation Default (Hangman NotRunning) where
default = Init
implementation Show (Hangman s) where
show Init = "Not ready yet"
show (GameWon w) = "You won! Successfully guessed " ++ w
show (GameLost w) = "You lost! The word was " ++ w
show (MkH w guesses got missing)
= let w' = pack (map showGot (unpack w)) in
w' ++ "\n\n" ++ show guesses ++ " guesses left"
where showGot : Char -> Char
showGot ' ' = '/'
showGot c = if ((not (isAlpha c)) || (c `elem` got)) then c else '-'
{- Initialise the state with the missing letters in a word -}
total
letters : String -> List Char
letters x with (strM x)
letters "" | StrNil = []
letters (strCons y xs) | (StrCons y xs)
= let xs' = assert_total (letters xs) in
if ((not (isAlpha y)) || (y `elem` xs')) then xs' else y :: xs'
initState : (x : String) -> Hangman (Running 6 (length (letters x)))
initState w = let xs = letters w in
MkH w _ [] (fromList (letters w))
-----------------------------------------------------------------------
-- RULES
-----------------------------------------------------------------------
{- Now, the rules of the game, written as an Effect.
We can think of the rules as giving a protocol that the game player and
the machine must follow for an implementation of the game to make sense.
-}
data HangmanRules : Effect where
-- Rule:
-- Precondition: we can make a guess if we have one or more guess available
-- (S g) and one or more letters are still missing (S w)
-- Postcondition: return whether the character was in the word. If so, reduce
-- the number of missing letters, if not, reduce the number of guesses left
Guess : (x : Char) ->
sig HangmanRules Bool
(Hangman (Running (S g) (S w)))
(\inword =>
Hangman (case inword of
True => (Running (S g) w)
False => (Running g (S w))))
-- The 'Won' operation requires that there are no missing letters
Won : sig HangmanRules ()
(Hangman (Running g 0))
(Hangman NotRunning)
-- The 'Lost' operation requires that there are no guesses left
Lost : sig HangmanRules ()
(Hangman (Running 0 g))
(Hangman NotRunning)
-- Set up a new game, initialised with 6 guesses and the missing letters in
-- the given word. Note that if there are no letters in the word, we won't
-- be able to run 'Guess'!
NewWord : (w : String) ->
sig HangmanRules () h (Hangman (Running 6 (length (letters w))))
-- Finally, allow us to get the current game state
Get : sig HangmanRules h h
HANGMAN : HState -> EFFECT
HANGMAN h = MkEff (Hangman h) HangmanRules
-- Promote explicit effects to Eff programs
guess : Char -> Eff Bool
[HANGMAN (Running (S g) (S w))]
(\inword => [HANGMAN (case inword of
True => Running (S g) w
False => Running g (S w))])
guess c = call (Main.Guess c)
won : Eff () [HANGMAN (Running g 0)] [HANGMAN NotRunning]
won = call Won
lost : Eff () [HANGMAN (Running 0 g)] [HANGMAN NotRunning]
lost = call Lost
new_word : (w : String) -> Eff () [HANGMAN h]
[HANGMAN (Running 6 (length (letters w)))]
new_word w = call (NewWord w)
get : Eff (Hangman h) [HANGMAN h]
get = call Get
-----------------------------------------------------------------------
-- IMPLEMENTATION OF THE RULES
-----------------------------------------------------------------------
{- This effect handler simply updates the game state as necessary for
each operation. 'Guess' is slightly tricky, in that it needs to check
whether the letter is in the word, and branch accordingly (and if it
is in the word, update the vector of missing letters to be the right
length). -}
implementation Handler HangmanRules m where
handle (MkH w g got []) Won k = k () (GameWon w)
handle (MkH w Z got m) Lost k = k () (GameLost w)
handle st Get k = k st st
handle st (NewWord w) k = k () (initState w)
handle (MkH w (S g) got m) (Guess x) k =
case isElem x m of
No _ => k False (MkH w _ got m)
Yes p => k True (MkH w _ (x :: got) (shrink m p))
-----------------------------------------------------------------------
-- USER INTERFACE
-----------------------------------------------------------------------
{- Finally, an implementation of the game which reads user input and calls
the operations we defined above when appropriate.
The type indicates that the game must start in a running state, with some
guesses available, and get to a not running state (i.e. won or lost).
Since we picked a word at random, we can't actually make the assumption there
were valid letters in it!
-}
soRefl : So x -> (x = True)
soRefl Oh = Refl
game : Eff () [HANGMAN (Running (S g) w), STDIO]
[HANGMAN NotRunning, STDIO]
game {w=Z} = won
game {w=S _}
= do putStrLn (show !get)
putStr "Enter guess: "
let guess = trim !getStr
case choose (not (guess == "")) of
(Left p) => processGuess (strHead' guess (soRefl p))
(Right p) => do putStrLn "Invalid input!"
game
where
processGuess : Char -> Eff () [HANGMAN (Running (S g) (S w)), STDIO]
[HANGMAN NotRunning, STDIO]
processGuess {g} c {w}
= case !(guess c) of
True => do putStrLn "Good guess!"
case w of
Z => won
(S k) => game
False => do putStrLn "No, sorry"
case g of
Z => lost
(S k) => game
{- Some candidate words. We'll use programming languages. We don't want to
write the length explicitly, so infer it with a proof search. -}
words : ?wlen
words = with Vect ["idris","agda","haskell","miranda",
"java","javascript","fortran","basic","racket",
"coffeescript","rust","purescript","clean","links",
"koka","cobol"]
wlen = proof search
{- It typechecks! Ship it! -}
runGame : Eff () [HANGMAN NotRunning, RND, STDIO]
runGame = do srand 1234567890
let w = index !(rndFin _) words
new_word w
game
putStrLn (show !get)
{- I made a couple of mistakes while writing this. For example, the following
were caught by the type checker:
* Forgetting to check the 'Won' state before continuing with 'game'
* Accidentally checking the number of missing letters rather than the number
of guesses when checking if 'Lost' was callable
-}
main : IO ()
main = run runGame
-- Local Variables:
-- idris-load-packages: ("effects")
-- End: