Search code examples
haskellmonadsstate-monad

State Monad to save board in game


I have a module Game that defines a method play like this play :: Board -> Move - > Board.

I want to use State Monad in another module called Playing that imports the Game module so that I can call play from there on a loop until the Board reaches a certain state.

I want to call the method with play with the Board that I get from the State Monad and then update the State Monad value with the Board returned by play.

So while the loop is going on I want to receive moves to apply to the play method and my current state.

But I'm quite lost as in how achieving this in a way that the module Game has no idea that I'm using a State Monad.

I have been looking at quite a few tutorials and examples (like this, this, this, etc) and I feel like I understand the way in which State Monad it's applied there but apparently not well enough as to abstract it to this particular implementation.

playing :: IO ()
playing = do
            putStr $ "The board looks like:"
            board <- get
            putStr $ showBoard  board 
            putStr $ "Indicate a move:"
            move <- getLine
            if validMove move then do
                newBoard <- play board (getMove move)
                if gameEnded newBoard then do
                    putStr $ "You win!" --stop the execution
                else do
                    put newBoard
            else do
               putStr $ "Invalid move"

I want playing to be on a loop until it gets a particular Board that means the game ended. And use the State Monad to send the current Board to play and to the other methods in the Game module like gameEnded :: Board -> Bool, showBoard :: Board -> String and 'getMove :: String -> Move'.

Any help is welcome


Solution

  • If you swap the order of arguments to play, you have a function of type:

    Move -> Board -> Board
    

    Which you can partially apply with a Move to get one of type:

    Board -> Board
    

    You can convert this to an action on State using modify :: (s -> s) -> State s () to modify the board:

    playing :: Move -> State Board ()
    playing move = modify (play move)
    

    One solution here is monad transformers—sounds scarier than it is. You could use StateT over IO, the StateT to store the game state and the IO to prompt the user for moves. For example:

    import Control.Monad.Trans.Class (lift)
    import Control.Monad.Trans.State (evalStateT, gets, modify)
    
    -- Get a move from the user.
    getMove :: IO Move
    getMove = do
      line <- getLine
      -- (Your implementation of parsing moves here.)
    
    -- The initial state of the board.
    initialBoard :: Board
    initialBoard = -- ...
    
    -- Whether the board represents a completed game.
    boardDone :: Board -> Bool
    boardDone board = -- ...
    
    -- Main game loop.
    gameLoop :: IO ()
    gameLoop = evalStateT loop initialBoard
      where
        loop = do
          move <- lift getMove
          modify (play move)
          done <- gets boardDone
          if done then pure () else loop
    

    You use lift to convert a normal IO action into a StateT Board IO action, modify :: (Monad m) => (s -> s) -> StateT s m () to modify the state, and gets :: (Monad m) => (s -> a) -> StateT s m a to read properties of the current state. loop either tail-calls itself to continue playing, or else returns.

    Using the structure and names in your edited question:

    playing :: IO ()
    playing = evalStateT loop initialBoard
      where
    
        loop :: StateT Board IO ()
        loop = do
          printBoard
          move <- lift promptMove
          modify (play move)
          ended <- gets gameEnded
          if ended
            then lift $ putStrLn "You win!"
            else loop
    
        printBoard :: StateT Board IO ()
        printBoard = do
          lift $ putStrLn $ "The board looks like:"
          board <- get
          lift $ putStrLn $ showBoard board
    
        promptMove :: IO Move
        promptMove = do
          putStr "Indicate a move: "
          move <- getLine
          if validMove move
            then pure $ getMove move
            else do
              putStrLn "Invalid move."
              promptMove