使用MonadPrompt实现重放

时间:2011-11-20 18:42:36

标签: haskell functional-programming monads

受到布伦特·尤里尼的启发 adventure game, 我一直在写一个小型的基于文本的冒险游戏(la Zork),它使用了 MonadPrompt 图书馆。使用它来分离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
...

接下来是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) -}
... 

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

到目前为止,我所遇到的唯一解决方案是维护列表 GameState内的重播操作。我不喜欢这个,因为这意味着我不能 彻底分开basicIOreplayIO。我希望replayIO能够处理它 动作列表,然后将控制权传递给basicIO以获取该列表 完全消失。

到目前为止,我已使用MonadPrompt包中的runPromptM来使用提示 monad,但查看包,runPromptC和runRecPromptC 函数看起来更强大,但我不理解它们 足以看出他们在这里如何(或者如果)对我有用。

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

1 个答案:

答案 0 :(得分:3)

据我所知,在运行Prompt操作的过程中无法切换提示处理程序,因此您需要一个处理器,可以处理仍有动作重放的情况,以及你恢复正常比赛的情况。

我看到解决此问题的最佳方法是在堆栈中添加另一个StateT转换器,以存储要执行的剩余操作列表。这样,重放逻辑可以与basicIO中的主游戏逻辑分开,并且当没有任何操作时,您的重播处理程序可以只调用lift . basicIO,并执行无操作或选择操作否则就是国家。