Реализация повторов с MonadPrompt - PullRequest
7 голосов
/ 20 ноября 2011

Вдохновленный приключенческой игрой Брента Йорги , я писал небольшую текстовую приключенческую игру (по-ла-зоркски), в которой используется библиотека MonadPrompt .Было довольно просто использовать его для отделения бэкенда ввода-вывода от реальной функции, которая управляет игровым процессом, но сейчас я пытаюсь сделать что-то более сложное с ним.

По сути, я хочу включить отмену ипеределывать как особенность игры.Моя стратегия для этого состояла в том, чтобы держать молнию игровых состояний (которая включает то, что было последним вводом).Поскольку я хочу иметь возможность сохранять историю при перезагрузке игры, файл сохранения - это просто список всех входных данных, выполненных игроком, которые могут повлиять на состояние игры (так что, скажем, проверка инвентаря не будет включена).Идея состоит в том, чтобы быстро воспроизвести последнюю игру из входных данных в файле сохранения при загрузке игры (пропуск вывода в терминал и получение ввода из списка в файле) и, таким образом, создать полную историю состояний игры.

Вот некоторый код, который в основном показывает мои настройки (я извиняюсь за длину, но это значительно упрощает фактический код):

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 из MonadPromptдля использования монады Prompt, но, просматривая пакет, функции runPromptC и runRecPromptC выглядят гораздо более мощными, но я недостаточно хорошо их понимаю, чтобы понять, как (или если) они могут быть полезны для меня здесь.

Надеюсь, я включил достаточно подробностей, чтобы объяснить мою проблему, если кто-то может вывести меня из леса сюда, я был бы очень признателен.

1 Ответ

3 голосов
/ 20 ноября 2011

Из того, что я могу сказать, нет способа переключить обработчики приглашений на полпути при выполнении действия Prompt, поэтому вам понадобится один обработчик, который может справиться как со случаем, когда для воспроизведения еще есть действия, так ислучай, когда вы возобновили нормальную игру.

Лучший способ решения этой проблемы - добавить еще один преобразователь StateT в свой стек для хранения оставшегося списка действий, которые нужно выполнить.Таким образом, логика воспроизведения может храниться отдельно от логики основной игры в basicIO, и ваш обработчик воспроизведения может просто вызвать lift . basicIO, когда не осталось никаких действий, и не выполнять никаких операций или выбирать действия из состояния в противном случае..

...