Our game, with a UI!
-- We use a language extensions so we can put signatures in instance declarations.
{-# LANGUAGE InstanceSigs #-}
module LectureIOGame where
import Data.List (delete, minimumBy)
import Text.Read (readMaybe)
1 A UI
Let’s imagine a transcript of playing the game of Nim to build our UI:
The state of the game looks like: 3 4 5
The computer player's move: Take 2 from the 1st pile.
The state of the game looks like: 1 4 5
Your possible moves:
1. Take 1 from the 1st pile
2. Take 1 from the 2nd pile
3. Take 2 from the 2nd pile
4. Take 3 from the 2nd pile
5. Take 4 from the 2nd pile
6. Take 1 from the 3rd pile
7. Take 2 from the 3rd pile
8. Take 3 from the 3rd pile
9. Take 4 from the 3rd pile
10. Take 5 from the 3rd pile
Enter the number of the option you want on a line of its own.
4
Your move: Take 3 from the 2nd pile
The state of the game looks like: 1 1 5
The computer player's move: Take 5 from the 3rd pile.
The state of the game looks like: 1 1 0
Your possible moves:
1. Take 1 from the 1st pile
2. Take 1 from the 2nd pile
...
You lose.
Want to play again?
1. Yes
2. No
Enter the number of the option you want on a line of its own.
1
...
You lose.
Want to play again?
1. Yes
2. No
Enter the number of the option you want on a line of its own.
2
Let’s build the pieces.
-- | Produce an IO () representing showing the (showable) state argument.
showState :: Show a => a -> IO ()
= putStrLn ("The state of the game looks like: " ++ show state)
showState state
-- | Given a Bool indicating whether it is the player's move (or the computer's),
-- return a string suitable for prefixing onto a displayed move.
whoseMove :: Bool -> String
True = "Your move: "
whoseMove False = "The computer player's move: "
whoseMove
-- | Produce an IO () representing showing the move string in a Move,
-- indicating whose move it is (based on the initial Bool argument, True
-- for the player's move and False for the computer's).
showMove :: Bool -> Move a -> IO ()
= putStrLn (whoseMove yourMove ++ name) >> putStrLn ""
showMove yourMove (name,_)
-- | Use the given function to transform each element of the given list to
-- a String and produce an IO () representing showing all of those elements
-- on lines of their own, prefixed by their (1-based) number in the list.
showAll :: (a -> String) -> [a] -> IO ()
= helper 1 as
showAll showA as where helper _ [] = return ()
:as) = putStrLn (show n ++ ". " ++ showA a) >>
helper n (a+1) as
helper (n
-- | Given a function to transform elements to Strings, a String prompt,
-- and a list of elements, produce an IO (Maybe a) representing getting
-- the result of prompting the user to select one of the choices and the
-- choice the user selected. On failure (e.g., because the user's
-- response couldn't be matched with a choice, including for the empty
-- list, where this will annoyingly prompt the user with no choices),
-- the Maybe a will be Nothing.
selectVal :: (a -> String) -> String -> [a] -> IO (Maybe a)
= putStrLn prompt >> putStrLn "" >>
selectVal showA prompt as >> putStrLn "" >>
showAll showA as putStrLn "Enter the number of the option you want on a line of its own." >>
getLine >>= processAnswer
where -- processAnswer :: String -> IO (Maybe a)
= case readMaybe answerStr of
processAnswer answerStr Nothing -> putStrLn "Unable to process your answer." >> return Nothing
Just n | n <= 0 || n > length as -> putStrLn "Your answer was out of range." >> return Nothing
| otherwise -> return (Just (as !! (n-1)))
-- | As with selectVal except this IO a represents repeatedly prompting
-- the user until they successfully select a value (which they never can
-- if the list is empty).
insistSelectVal :: (a -> String) -> String -> [a] -> IO a
= selectVal showA prompt as >>= \maybeA ->
insistSelectVal showA prompt as case maybeA of
Just a -> return a
Nothing -> insistSelectVal showA prompt as
-- | Given a Bool representing if it's the player's turn (or the
-- computer's) and the current game state, returns an IO (Move a)
-- representing the result of prompting for (if needed) and selecting
-- the next move. (On the computer's turn, this is side-effect free
-- but on the player's, it requires I/O.)
getMove :: GameState a => Bool -> a -> IO (Move a)
False a = return (pickBestMove' a)
getMove True a = insistSelectVal fst "Your possible moves:" (nextGameStates a)
getMove
-- | Given a Bool indicating whose turn it is and a game state,
-- returns an IO () representing displaying the end game status
-- to the player. Win/loss/tie is determined by the result of
-- getGameState (where a Just 0 or Nothing result is taken to
-- be a tie, a positive result a win for the current player, and
-- a negative result a loss for the current player).
showEndGame :: GameState a => Bool -> a -> IO ()
= putStrLn (message yourVal)
showEndGame yourMove state where phasingVal = maybe 0 id (getGameStateValue state)
= (if yourMove then 1 else (-1)) * phasingVal
yourVal | n < 0 = "You lose."
message n | n == 0 = "It's a tie."
| otherwise = "You win"
-- | Returns an IO () representing playing one round of a game
-- beginning at the given game state and with the player moving
-- first if the Bool argument is True and the computer otherwise.
playRound :: (GameState a, Show a) => Bool -> a -> IO ()
=
playRound yourMove state >>
showState state case getGameStateValue state of
-- Not the game end:
Nothing -> getMove yourMove state >>= \move ->
>>
showMove yourMove move not yourMove) (snd move)
playRound (
-- Game over:
-> showEndGame yourMove state
_
-- | Returns an IO () representing playing a game repeatedly
-- from the given initial state, with the player first if the
-- Bool argument is True and starting over with the other player
-- first each time the game ends and a new game begins. (Always
-- starts from the same initial state.) This continues for at
-- least one round and until the player chooses not to continue.
playGame :: (GameState a, Show a) => Bool -> a -> IO ()
=
playGame playerFirst initState undefined -- you know what would make a great exercise here?
Now you can play a round of the Magic State game where you go first with: playRound True (initGameState :: MSState)
. (With your Nim code in place, you could swap in NimState
for MSState
and play Nim instead!)
To play the game repeatedly, you’d need an implementation of playGame
. Sounds like an exercise for you!
You may find reviewing playRound
and insistSelectVal
particularly helpful.
2 The Game Itself
Of course, to play the game, we need a game to play! The implementation is here. You can pop your implementation of NimState
in as well to play it!
Unlike our original version, however, we start with the abstract pieces like GameTree
and GameState
this time, now that we know what we’re doing!
2.1 Any (2-player, alternating turns, complete information) Game
The core data types:
type Move a = (String, a)
data GameTree a = GameTree a [GameTree a]
deriving (Eq, Ord, Show, Read)
class GameState a where
-- | Produces 1 for a win, -1 for a loss, 0 for a tie,
-- and Nothing for a not-yet-complete game.
getGameStateValue :: a -> Maybe Double
initGameState :: a
-- | Produces a list of the next game states after the current one.
-- If the game is complete, the list is empty.
nextGameStates :: a -> [Move a]
The key additional functions:
constructGameTree :: GameState a => a -> GameTree a
=
constructGameTree state GameTree state (map (constructGameTree . snd) (nextGameStates state))
-- | Not actually key, but really fun:
gameTree :: GameState a => GameTree a
= constructGameTree initGameState
gameTree
getGameTreeValue :: GameState a => GameTree a -> Double
GameTree state []) =
getGameTreeValue (case getGameStateValue state of
Just v -> v
Nothing -> 0
GameTree _ nexts) =
getGameTreeValue (negate (minimum (map getGameTreeValue nexts))
pickBestMove :: GameState a => a -> Maybe (Move a)
= case nextGameStates state of
pickBestMove state -> Nothing
[] -> Just (argmin getMoveValue moves)
moves where getMoveValue move =
snd move))
getGameTreeValue (constructGameTree (
pickBestMove' :: GameState a => a -> Move a
=
pickBestMove' state maybe undefined id (pickBestMove state)
A utility function:
-- | argmin f vals produces the value in vals for which f produces
-- the smallest result. vals MUST NOT BE EMPTY.
argmin :: Ord b => (a -> b) -> [a] -> a
= fst minTuple
argmin f as where
-- Get tuples of the as and their values computed by f
-- We put these in a variable so that Haskell will cache
-- them for us (call f just once per value in as), since
-- our f is potentially very expensive!
= zip as (map f as)
tuples
-- Compare tuples by their f values
= compare (snd t1) (snd t2)
compareSnds t1 t2
-- Find the smallest tuple
= minimumBy compareSnds tuples minTuple
2.2 Magic Sum Game
-- | The state of the magic sum game, with the
-- list of available numbers to take, the list of
-- numbers I have taken, and the list of numbers
-- you have taken.
data MSState = MSState [Int] [Int] [Int]
deriving (Eq, Ord, Read, Show)
-- | Produce all sublists of exactly the given length
allSubLists :: Int -> [a] -> [[a]]
0 _ = [[]]
allSubLists = []
allSubLists _ [] :as)
allSubLists n (a| n < 0 = []
| otherwise = map (a:) (allSubLists (n-1) as) ++
allSubLists n as
all3Lists :: [a] -> [[a]]
= allSubLists 3
all3Lists
hasMSWin :: [Int] -> Bool
= not . null . filter (== 15) . map sum . all3Lists
hasMSWin
isMSTie :: MSState -> Bool
isMSWin, isMSLoss,MSState _ ns _) = hasMSWin ns
isMSWin (MSState _ _ ns) = hasMSWin ns
isMSLoss (
MSState [] _ _) = True
isMSTie (= False
isMSTie _
getMSValue :: MSState -> Maybe Double
| isMSWin ms = Just 1
getMSValue ms | isMSLoss ms = Just (-1)
| isMSTie ms = Just 0
| otherwise = Nothing
initMSState :: MSState
= MSState [1..9] [] []
initMSState
nextMSStates :: MSState -> [Move MSState]
MSState pool me you) =
nextMSStates (map (\choice -> ("Take " ++ show choice, MSState (delete choice pool) you (choice:me))) pool
getGTMSValue :: GameTree MSState -> Double
GameTree state []) =
getGTMSValue (case getMSValue state of
Just v -> v
Nothing -> 0
GameTree _ nexts) =
getGTMSValue (negate (minimum (map getGTMSValue nexts))
instance GameState MSState where
getGameStateValue :: MSState -> Maybe Double
= getMSValue
getGameStateValue
initGameState :: MSState
= initMSState
initGameState
nextGameStates :: MSState -> [Move MSState]
= nextMSStates nextGameStates