Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing replays with MonadPrompt

Inspired by Brent Yorgey's adventure game, I've been writing a small text based adventure game (a la Zork) that uses the MonadPrompt library. It's been fairly straightforward to use it to separate the IO backend from the actual function that governs gameplay, but I'm now trying to do something a bit more complicated with it.

Basically, I want to enable undo and redo as a feature of the game. My strategy for this has been to keep a zipper of gamestates (which includes what the last input was). Since I want to be able to maintain history when reloading the game, The save file is just a list of all inputs the player performed that can affect the gamestate (so examining the inventory would not be included, say). The idea is to quickly replay the last game from the inputs in the save file when loading a game (skipping output to the terminal, and taking input from the list in the file), and thereby build up the full history of gamestates.

Here is some code that shows basically the setup I have (I apologize for the length, but this is much simplified from the actual code):

data Action = UndoAction | RedoAction | Go Direction -- etc ...
-- Actions are what we parse user input into, there is also error handling
-- that I left out of this example
data RPGPrompt a where
    Say :: String -> RPGPrompt ()
    QueryUser :: String -> RPGPrompt Action
    Undo :: RPGPrompt ( Prompt RPGPrompt ())
    Redo :: RPGPrompt ( Prompt RPGPrompt ())
    {-
    ... More prompts like save, quit etc. Also a prompt for the play function 
        to query the underlying gamestate (but not the GameZipper directly)
    -}

data GameState = GameState { {- hp, location etc -} }
data GameZipper = GameZipper { past :: [GameState],
                               present :: GameState, 
                               future :: [GameState]}

play :: Prompt RPGPrompt ()
play = do
  a <- prompt (QueryUser "What do you want to do?")
  case a of
    Go dir -> {- modify gamestate to change location ... -} >> play
    UndoAction -> prompt (Say "Undo!") >> join (prompt Undo)
    ... 

parseAction :: String -> Action
...

undo :: GameZipper -> GameZipper
-- shifts the last state to the present state and the current state to the future

basicIO :: RPGPrompt a -> StateT GameZipper IO a
basicIO (Say x) = putStrLn x
basicIO (QueryUser query) = do
  putStrLn query
  r <- parseAction <$> getLine
  case r of
     UndoAction -> {- ... check if undo is possible etc -}
     Go dir -> {- ... push old gamestate into past in gamezipper, 
                   create fresh gamestate for present ... -} >> return r
     ...
basicIO (Undo) = modify undo >> return play
...

Next is the replayIO function. It takes a backend function to execute when it's done replaying (usually basicIO ) and a list of actions to replay

replayIO :: (RPGPrompt a -> StateT GameZipper IO a) -> 
            [Action] ->
            RPGPrompt a ->
            StateT GameZipper IO a
replayIO _ _ (Say _) = return () -- don't output anything
replayIO resume [] (QueryUser t) = resume (QueryUser t)
replayIO _ (action:actions) (Query _) =
   case action of
      ... {- similar to basicIO here, but any non-gamestate-affecting 
             actions are no-ops (though the save file shouldn't record them 
             technically) -}
... 

This implementation of replayIO doesn't work though, because replayIO isn't directly recursive, you can't actually remove Actions from the list of actions passed to replayIO. It gets an initial list of actions from the function that loads the save file, and then it can peek at the first action in the list.

There only solution that has occurred to me so far is to maintain the list of replay actions inside the GameState. I don't like this because it means I can't cleanly separate basicIO and replayIO. I'd like for replayIO to handle its action list, and then when it passes control to basicIO for that list to disappear entirely.

So far, I've used runPromptM from the MonadPrompt package to use the Prompt monad, but looking through the package, the runPromptC and runRecPromptC functions look like they are much more powerful, but I don't understand them well enough to see how (or if) they might be useful to me here.

Hopefully I've included sufficient detail to explain my problem, if anyone can lead me out of the woods here, I'd really appreciate it.

like image 610
deontologician Avatar asked Nov 20 '11 18:11

deontologician


1 Answers

From what I can tell, there is no way to switch prompt handlers halfway through running a Prompt action, so you will need a single handler that can deal with both the case where there are still actions left to replay, and the case when you've resumed normal play.

The best way I see of solving this would be to add another StateT transformer to your stack to store the remaining list of actions to perform. That way, the replay logic can be kept separate from the main game logic in basicIO, and your replay handler can just call lift . basicIO when there are no actions left, and do no-ops or pick actions out of the state otherwise.

like image 160
hammar Avatar answered Nov 06 '22 19:11

hammar