Функционально-банановая игра для путешественников - интригующая и безумная

Я хочу использовать реактивный банан, чтобы написать путевую игру, в которой люди могут писать ботов. FRP совершенно для меня совершенно, и у меня возникли проблемы с запуском. Когда я начал, я создал более сложный график, но для моих целей здесь я попытался сделать его максимально простым. Я хотел бы получить некоторые рекомендации, в основном о том, с чего начать, и как разбить эту большую проблему на более мелкие проблемы. Здесь начальный дизайн.

Идея состоит в том, чтобы иметь график LNodes (с взвешенными ребрами, на которые я сейчас игнорирую для простоты). Эти LNodes я описываю как Planet s. Planet имеют имя, карту игроков на Planet и Resource s. Игроки имеют идентификатор, ресурсы и планету. Здесь строятся данные и некоторые связанные с ними функции, за которыми следует больше обсуждений.

-- Graph-building records and functions

data PlanetName = Vulcan
                | Mongo
                | Arakis
                | Dantooine
                | Tatooine
                     deriving (Enum,Bounded,Show)

data Planet = Planet {pName :: PlanetName
                     ,player :: IntMap Player
                     ,resources :: Int
                     } deriving Show

data Player = Player {pid :: Int
                    ,resources :: Int
                    } deriving Show



makePlanetNodes :: PlanetName -> [LNode Planet]
makePlanetNodes planet = Prelude.map makePlanetNodes' $
                         zip [planet ..] [0 ..]
  where makePlanetNodes' (planet,i) =
          (i,Planet {pName = planet, players = IM.empty})

makePlanetGraph p = mkGraph p [(0,1,1),(1,2,2),(2,3,4),(3,4,3),(4,0,2)]

-- Networking and command functions

prepareSocket :: PortNumber -> IO Socket
prepareSocket port = do
   sock' <- socket AF_INET Stream defaultProtocol
   let socketAddress = SockAddrInet port 0000
   bindSocket sock' socketAddress
   listen sock' 1
   putStrLn $ "Listening on " ++ (show port)
   return sock'

acceptConnections :: Socket -> IO ()
acceptConnections sock' = do
   forever $ do
       (sock, sockAddr) <- Network.Socket.accept sock'
       handle <- socketToHandle sock ReadWriteMode
       sockHandler sock handle

sockHandler :: Socket -> Handle -> IO ()
sockHandler sock' handle = do
    hSetBuffering handle LineBuffering
    forkIO  $ commandProcessor handle
    return ()

commandProcessor :: Handle -> IO ()
commandProcessor handle = untilM (hIsEOF handle) handleCommand >> hClose handle
  where
    handleCommand = do
        line <- hGetLine handle
        let (cmd:arg) = words line
        case cmd of
            "echo" -> echoCommand handle arg
            "move" -> moveCommand handle arg
            "join" -> joinCommand handle arg
            "take" -> takeCommand handle arg
            "give" -> giveCommand handle arg
            _ -> do hPutStrLn handle "Unknown command"


echoCommand :: Handle -> [String] -> IO ()
echoCommand handle arg = do
    hPutStrLn handle (unwords arg)

moveCommand = undefined

joinCommand = undefined

takeCommand = undefined

giveCommand = undefined

Здесь, что я знаю до сих пор, мои события будут включать типы Planet и Player. поведения будет включать в себя перемещение, соединение, принятие и предоставление. Когда игрок присоединяется, он создаст новое событие Player и обновит карту на Vulcan с помощью Player. Перемещение позволит пройти от одного LNode к другому, если LNode соединены ребром. Take удалит resources из текущего Planet Player "on" и добавит те resources в Player. Дайте будет делать обратное.

Как я могу разбить эту большую проблему на более мелкие проблемы, чтобы я мог разглядеть эту тему?

Обновление: Оказывается, Hunt Wumpus не является хорошим выбором, чтобы помочь узнать FRP, см. объяснение того, что FRP для здесь. Это в ответ Генрих Апфельмус.

Тем не менее, я полностью проигнорирую сетевой код. Я мог бы просто написать несколько фиктивных ботов для проверки времени и т.д.

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

создание таймера

обработка ввода

Ответ 1

Это сложный проект. Игра грубо разлагается на следующие части:

  • входной уровень (преобразует входные данные в игровые события)
  • движок (принимает событие и текущее состояние для создания нового состояния игры)
  • уровень вывода (отображает состояние игры и сообщения, созданные посредством выполнения событий)

Сначала я упростил бы уровни ввода и вывода, используя входные и выходные данные консоли (т.е. stdin и stdout). Позже вы можете добавить поддержку сети.

Во-вторых, я бы упростил саму игру. Например, начните с однопользовательской игры. Недавно мне было очень весело переводить Землю из игры Lisp "Grand Theft Wumpus" в Haskell.

В-третьих, я бы начал с игрового движка. Это означает, что вам нужно подумать:

  • Что такое состояние игры?
  • Каковы игровые события?

Определите структуры данных Haskell для состояния игры и каждого события. Обратите внимание, что игровое состояние должно записывать все, что имеет отношение к игре: карта, расположение игроков, состояние игроков и даже случайное число семян.

Состояние игры обычно будет типом продукта:

data GameState = {  _mapNodes :: [Node]
                   ,_mapEdges  :: [ (Node,Node) ]
                   ,_player   :: Node
                   , ...
                 }

Игровые события должны быть определены как тип суммы:

data GameEvent =
  | MovePlayer Node
  | Look
  | ...

После того, как вы определили эти структуры данных, задайте функцию performEvent:

performEvent :: GameEvent -> GameState -> IO(GameState)

Причиной того, что результат performEvent является IO(GameState), является то, что вам, вероятно, потребуется сообщить игрокам о том, что произошло, и использование монады IO будет самым простым способом сделать это на этом этапе в игра (каламбур не предназначен.) Есть способы очистить функцию типа performEvent, но это целая другая тема.

Пример:

performEvent :: GameEvent -> GameState -> IO(GameState)
performEvent (Move p n) s =
  do putStrLn "Moving from " ++ (show $ s _player) ++ " to " ++ (show n)
     return s { _player = n }
performEvent Look       s =
  do putStrLn "You are at " ++ (show $ s _player)
     return s

Как только вы протестировали performEvent, вы можете добавить передний конец для перевода строки текста в GameEvent:

parseInput :: Text -> Maybe GameEvent
parseInput t = case Text.words t of
                 ("look":_)    -> Just Look
                 ("move":n:_)  -> Move <$> (parseNode n)
                 otherwise     -> Nothing

Затем добавьте входной цикл, напишите функцию для создания исходного GameState, и, прежде чем вы это узнаете, у вас будет настоящая интерактивная игра!