Функционально-банановая игра путешественника - интригует и сводит с ума
Я хочу использовать реактивный банан, чтобы написать игру для путешественников, для которой люди могут писать ботов. FRP совершенно новый для меня, и мне трудно начать. Когда я начал, я создал более сложный График, но для своих целей я постарался сделать его максимально простым. Я хотел бы получить некоторые рекомендации, в основном о том, с чего начать и как разбить эту большую проблему на более мелкие проблемы, которые необходимо решить. Вот начальный дизайн.
Идея состоит в том, чтобы иметь граф LNodes (со взвешенными ребрами, которые я пока игнорирую для простоты). Эти LNode я описываю как 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
событие и обновить карту на Вулкане с этим Player
, Move позволит пройти от одного LNode
другому, при условии LNode
s связаны ребром. Бери уберу resources
от текущего Planet
Player
"на" и добавить эти resources
к Player
, Давай сделаем обратное.
Как я могу разбить эту большую проблему на более мелкие, чтобы я мог разобраться с этим?
Обновление: Оказывается, Hunt the Wumpus не является хорошим выбором, чтобы помочь в изучении FRP, см. Объяснение того, для чего предназначен FRP. Это в ответе Генриха Апфельма.
Тем не менее, я буду полностью игнорировать сетевой код сейчас. Я мог бы просто написать несколько фиктивных ботов для проверки времени и т. Д.
Обновление: Некоторые люди, кажется, заинтересованы в этой проблеме, поэтому я собираюсь отслеживать связанные вопросы здесь.
1 ответ
Это сложный проект. Игра примерно разбита на следующие части:
- входной слой (переводит входные данные в игровые события)
- движок (принимает событие и текущее состояние для создания нового игрового состояния)
- выходной слой (отображает состояние игры и сообщения, полученные при выполнении событий)
Сначала я упростил бы слои ввода и вывода, используя консольный ввод и вывод (т.е. stdin и stdout.) Позже вы можете добавить поддержку сети.
Во-вторых, я бы упростил саму игру. Например, начните с однопользовательской игры. Недавно мне было очень весело переводить игру Land the 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, и, прежде чем вы это узнаете, у вас будет настоящая интерактивная игра!