使用MonadPrompt实现重放

deo*_*ian 7 monads haskell functional-programming

受Brent Yorgey 冒险游戏的启发 ,我一直在编写一个使用MonadPrompt 库的小型文本冒险游戏(la Zork) .使用它将IO后端与管理游戏玩法的实际功能分开是相当简单的,但我现在正试图做一些更复杂的事情.

基本上,我想启用撤消和重做作为游戏的一个功能.我的策略是保持游戏状态的拉链(包括最后输入的内容).由于我希望能够在重新加载游戏时保持历史记录,因此保存文件只是玩家执行的所有输入的列表,可以影响游戏状态(因此,检查库存不会包括在内).这个想法是在加载游戏时快速重放保存文件中输入的最后一个游戏(跳过输出到终端,并从文件中的列表中获取输入),从而建立游戏状态的完整历史记录.

这里有一些代码基本上显示了我的设置(我为长度道歉,但这从实际代码中大大简化):

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
...
Run Code Online (Sandbox Code Playgroud)

接下来是replayIO功能.它需要一个后端函数来执行它完成重放(通常是basicIO)和一系列要重放的动作

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) -}
... 
Run Code Online (Sandbox Code Playgroud)

这种实现replayIO虽然不起作用,因为replayIO不是直接递归,你实际上无法从传递给的动作列表中删除Actions replayIO.它从加载保存文件的函数中获取操作的初始列表,然后它可以查看列表中的第一个操作.

到目前为止,我所遇到的唯一解决方案是维护内部的重放操作列表GameState.因为这意味着我不能完全分开出来,我不喜欢这个basicIOreplayIO.我想要replayIO处理它的动作列表,然后当它将控制传递basicIO给该列表时完全消失.

到目前为止,我已经使用runPromptMMonadPrompt包来使用Prompt monad,但是通过查看包,runPromptC和runRecPromptC函数看起来更强大,但是我不太清楚它们是如何(或者如果)他们可能对我有用.

希望我已经包含足够的细节来解释我的问题,如果有人能带领我走出困境,我真的很感激.

ham*_*mar 3

据我所知,无法在运行操作的Prompt过程中切换提示处理程序,因此您将需要一个处理程序来处理仍然有操作需要重播的情况以及已经执行完操作的情况。恢复正常比赛。

我认为解决此问题的最佳方法是将另一个StateT变压器添加到堆栈中以存储要执行的剩余操作列表。这样,重播逻辑可以与 中的主游戏逻辑分开,并且您的重播处理程序可以在没有剩余操作时basicIO调用,否则不执行任何操作或从状态中选择操作。lift . basicIO