使用MonadPrompt实现重播
受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
...
接下来是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
不是直接递归的,你不能真正从传递到操作列表中删除操作replayIO
。 它从加载保存文件的函数中获取动作的初始列表,然后它可以查看列表中的第一个动作。
到目前为止,我唯一的解决方案就是维护GameState
中的重放动作列表。 我不喜欢这个,因为这意味着我不能干净地将basicIO
和replayIO
分开。 我想让replayIO
处理它的动作列表,然后当它将控制权传递给basicIO
,该列表完全消失。
到目前为止,我已经使用runPromptM
包中的runPromptM来使用提示monad,但通过查看包,runPromptC和runRecPromptC函数看起来更强大,但我不能很好地理解它们,或者如果)他们可能对我有用。
希望我已经包含足够的细节来解释我的问题,如果有人能够让我走出困境,我会非常感激。
根据我所知,无法通过运行Prompt
动作中途切换提示处理程序,因此您需要一个处理程序来处理仍然存在要重播的操作的情况,以及您在“已恢复正常发挥。
我看到解决这个问题的最佳方法是将另一个StateT
转换器添加到堆栈中,以存储剩余的要执行的操作列表。 这样,重放逻辑可以与basicIO
的主要游戏逻辑basicIO
,并且重播处理程序可以调用lift . basicIO
lift . basicIO
当没有任何动作的时候,不做任何动作或者在状态之外选择动作。
上一篇: Implementing replays with MonadPrompt
下一篇: Which mobile device emulators do you use to test mobile development?