Functional-Banana Traveller Game - Intriguing and Maddening
Asked Answered
M

1

12

I want to use reactive-banana to write a traveller game that people can write bots for. FRP is completely new to me, and I'm having trouble making a start. I created a more elaborate Graph, when I began, but for my purposes here, I have tried to make it as simple as possible. I'd like some guidance, mostly on where to begin, and how to break this bigger problem down into smaller problems to solve. Here's the initial design.

The idea is to have a Graph of LNodes (with weighted edges I am ignoring for now for simplicity's sake). These LNodes I describe as Planets. The Planets have a name, a Map of the Players on the Planet and Resources. Players have an id, resources and a Planet.Here's the Data Structues and some associated functions, followed by more discussion.

-- 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

Here's what I know so far, my Events will involve types Planet and Player. Behaviors will involve move,join,take,and give. When a player joins, it will create a new Player event and update the Map on Vulcan with that Player. Move will allow a traversal from one LNode to another,provided the LNodes are connected by an edge. Take will remove resources from the current Planet the Player is "on" and add those resources to the Player. Give will do the opposite.

How can I break this big problem into smaller problems so I can get my head around this stuff?

Update: Turns out Hunt the Wumpus is not a good choice to help learn FRP, See an explaination of what FRP is for here. It's in a response by Heinrich Apfelmus.

That said, I will totally ignore networking code for now. I could just write some dummy bots to test timing etc.

Update: Some people seem to be interested in this problem, so I am going to track related questions here.

building a timer

input handling

Microcurie answered 6/9, 2012 at 4:20 Comment(0)
T
10

This is a complex project. A game roughly decomposes into the following pieces:

  • an input layer (translates input to game events)
  • an engine (takes an event and the current state to produce a new game state)
  • an output layer (displays game state and messages produced by performing events)

I would first simplify the input and output layers by using console input and output (i.e. stdin and stdout.) Later you can add network support.

Secondly, I would simplify the game itself. For instance, start with a single-player game. Recently I had a lot of fun translating the Land of Lisp game "Grand Theft Wumpus" to Haskell.

Thirdly, I would start with the game engine. This means you have to think about:

  • What is the game state?
  • What are the game events?

Define Haskell data structures for the game state and each event. Note that the game state needs to record everything that is relevant about the game: the map, player locations, state of players and even random number seeds.

The game state will typically be a product type:

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

The game events should be defined as sum type:

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

After you've defined these data structures have been defined, write the performEvent function:

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

The reason that the result of performEvent is IO(GameState) is that you will probably need to inform players about what happened, and using the IO monad is going to be the simplest way to do this at this stage in the game (no pun intended.) There are ways to purify a function like performEvent, but that's a whole other topic.

Example:

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

Once you have tested performEvent, you can add a front end to translate a line of text to a GameEvent:

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

Then add an input loop, write a function to create the initial GameState, and before you know it you'll have a real interactive game!

Transact answered 6/9, 2012 at 18:12 Comment(1)
Ah hunt the wumpus! Yeah starting smaller sounds like a good move.Microcurie

© 2022 - 2024 — McMap. All rights reserved.