patternMinor
Textbased User Interface for user and program taking turns
Viewed 0 times
userturnsprograminterfacetextbasedforandtaking
Problem
I wrote this program to model interactions between a user and an artificial player, both playing by the same rules (not enforced here for simplicity).
The game played is here is "your next word has to start with the last letter of mine"
I am especially interested how to formulate this better structurally-
The game played is here is "your next word has to start with the last letter of mine"
module Main where
import Lib
import System.Random
import System.Exit
import Control.Monad
vocab = ["alpha","beta","gamma"]
blacklist = []
pick:: [a] -> IO a --picks random element. copy pasted, not understood
pick x = Control.Monad.liftM (x !!) (randomRIO (0, length x - 1))
main :: IO ()
main = do
userInput [String] -> [String] -> IO a
processUser input vocab blacklist = if input == "quit" then exitSuccess
else do
successor [String] -> [String] -> IO a
processPC Nothing v b = do putStrLn "I give up"
exitSuccess
processPC (Just ioWord) v b = do word [String] -> [String] -> IO (Maybe (IO String))
getNext lastWord vocab blacklist = do let chooseFrom = filter (`notElem` blacklist) vocab
let matches = filter (\x -> head x == last lastWord) chooseFrom
if null matches then return Nothing
else return (Just (pick matches) )I am especially interested how to formulate this better structurally-
Solution
The very first thing that jumps out at me is the indirection of maintaining both a list of possible words and a seen list. Picking a better representation for your state will make bookkeeping easier.
Now fill in the logically necessary functions needed to work with that state. We need to be able to construct a state blob—
Remove played words from one—
And choose a new word from one given the constraint imposed by the last play.
Note the separation from any logic in taking turns, or which player is currently up. Testing these operations will be much easier then (load them up in GHCi and try it out), and integrating them into our monadic game playing code should be pretty straightforward, just follow the types.
Note also how I have moved the logic for making a player move into the function where it makes sense to do so. User player code shouldn't drive computer player code, and vice versa. As an exercise, try making the necessary modifications and stylistic tweaks to
You should make a best effort attempt at understanding all of the functions you write into your source. Copy and pasting
Spoilers—Here are the commit-level changes and final runnable code I produced while working through this.
type Word = String
type Words = [Word]
type Moves = Map.Map Char WordsNow fill in the logically necessary functions needed to work with that state. We need to be able to construct a state blob—
makeMoves :: Words -> Moves
makeMoves = Map.fromListWith (++) . catMaybes . map tag
where
tag :: Word -> Maybe (Char, Words)
tag [] = Nothing
tag w@(c:_) = Just (c, [w])Remove played words from one—
remove :: Word -> Moves -> Moves
remove [] = id
remove w@(c:_) = Map.adjust (delete w) cAnd choose a new word from one given the constraint imposed by the last play.
move :: Char -> Moves -> IO (Maybe (Word, Moves))
move c ms =
let possible = Map.lookup c ms
in case possible of
Nothing -> return Nothing
Just [] -> return Nothing
Just ws -> do
w <- pick ws
return $ Just (w, remove w ms)Note the separation from any logic in taking turns, or which player is currently up. Testing these operations will be much easier then (load them up in GHCi and try it out), and integrating them into our monadic game playing code should be pretty straightforward, just follow the types.
processUser :: Moves -> IO ()
processUser moves = do
input return ()
"" -> do putStrLn "You must enter a word."
processUser moves
w -> processPC (last w) (remove w moves)Note also how I have moved the logic for making a player move into the function where it makes sense to do so. User player code shouldn't drive computer player code, and vice versa. As an exercise, try making the necessary modifications and stylistic tweaks to
processPC yourself.You should make a best effort attempt at understanding all of the functions you write into your source. Copy and pasting
pick in when you don't understand it is poor form, where'd you even find that definition? At least use a library. If you don't know what liftM is doing, just write the function out using do-notation as you would any other operation in the IO monad.pick :: [a] -> IO a
pick xs = do
i <- randomRIO (0, length xs - 1)
return (xs !! i)Spoilers—Here are the commit-level changes and final runnable code I produced while working through this.
Code Snippets
type Word = String
type Words = [Word]
type Moves = Map.Map Char WordsmakeMoves :: Words -> Moves
makeMoves = Map.fromListWith (++) . catMaybes . map tag
where
tag :: Word -> Maybe (Char, Words)
tag [] = Nothing
tag w@(c:_) = Just (c, [w])remove :: Word -> Moves -> Moves
remove [] = id
remove w@(c:_) = Map.adjust (delete w) cmove :: Char -> Moves -> IO (Maybe (Word, Moves))
move c ms =
let possible = Map.lookup c ms
in case possible of
Nothing -> return Nothing
Just [] -> return Nothing
Just ws -> do
w <- pick ws
return $ Just (w, remove w ms)processUser :: Moves -> IO ()
processUser moves = do
input <- getLine
case input of
"quit" -> return ()
"" -> do putStrLn "You must enter a word."
processUser moves
w -> processPC (last w) (remove w moves)Context
StackExchange Code Review Q#138374, answer score: 4
Revisions (0)
No revisions yet.