Inspired by Brent Yorgi adventure game , I wrote a small text adventure game (a la Zork) that uses the MonadPrompt library. It was pretty simple to use it to separate the IO backend from the actual function that controls the gameplay, but I'm trying to do something more complicated with that now.
Basically, I want to enable undo and redo as a function of the game. My strategy for this was to keep a zipper for games (which includes the last input was). Since I want to be able to keep the history when the game is restarted, the save file is just a list of all the inputs that the player has made, can affect the gamestate (therefore, the study of the inventory will not be included, say). The idea is to quickly play the last game from the inputs in the file save mode when loading the game (passing output to the terminal and entering data from the list in the file) and thereby create a complete game history.
Here is some code that basically shows the settings that I have (I apologize for the length, but this is very 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 ...
The next replayIO function. It is required to execute a backend function when it (usually basicIO) and a list of actions to play
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 replayIO implementation replayIO not work, because replayIO not directly recursive, you cannot actually remove actions from the list of actions transferred to replayIO . It receives an initial list of actions from a function that loads a save file, and then can peek into the first action in the list.
The only solution that came to me was to save the list of repeat actions inside the GameState . I donβt like it because it means that I cannot cleanly separate basicIO and replayIO . I would like replayIO handle its action list, and then when it transfers control to basicIO for that list so that it completely disappears.
So far I have used runPromptM from the runPromptM package to use the monad tooltip, but looking at the package, runPromptC and runRecPromptC functions look as if they are much more powerful, but I donβt understand them enough to see how (or if) they can be useful to me here.
I hope I have included enough details to explain my problem, if anyone can get me out of the forest here, I would really appreciate it.