HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Textbased User Interface for user and program taking turns

Submitted by: @import:stackexchange-codereview··
0
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"

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.

type Word = String
type Words = [Word]
type Moves = Map.Map Char Words


Now 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) c


And 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 Words
makeMoves :: 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) c
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)
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.