Implementing a MUD server in haskell

Hi, I've been thinking about two things for some time now: 1. Sharpening my haskell 2. Implementing a MUD server I've been thinking about doing (1) and (2) at the same time but I've run into some problems with data structures. I need to be able to get a list of players from a room (for things like a 'look' command) and I need to be able to find which room contains a given player. I'm still looking at this problem through an imperative lens, so here's some sort-of-C: struct room{ ... struct player * players; ... }; struct player{ ... struct room * room; ... }; From what I can see, I'd use a record type for players and rooms, but I'm not sure how to replicate the pointer effects: keeping each player pointing to a single instance of a consistent world and maintaining the invariant that (given: p :: Player, r :: Room): p `elem` (Room.players r) => (Player.room p == r) This needs to stand up to concurrent modification of a shared world structure, but I think I'll set up the concurrency controls after I get my head around this. -- Jack

Jack Kelly wrote:
struct room{ ... struct player * players; ... };
struct player{ ... struct room * room; ... };
From what I can see, I'd use a record type for players and rooms, but I'm not sure how to replicate the pointer effects:
Essentially you have to choose some form of identity (relational DB fans might call it a "key") for your players and rooms. My gut feeling is that String will do fine; it's nice for debugging, gives rise to a natural way to write your admin commands and so on. However, it may not be quite good enough (100 mobs all called "an orc" need individual keys) and you may end up using an Int or Int64 for your key. Once you've chosen a key, you can do something very like the C++ structure, but instead you have Room { ... players :: [String] ... } Player { ... room :: String } ..although you may find life is made more pleasant by newtype RoomID = RoomID String newtype PlayerID = PlayerID String which improves the looks of your types, helps the compiler help you, and also makes it easier to change to another representation later. I should mention, actually, that there is also the naive solution: Room { ... players :: [Player] ... } Player { ... room :: Room ... } The main reason to dislike this is that if something in a Player structure changes, you have to remember to change all the references to it... Depending on your design, it may be that rooms themselves can't really change, so this may not matter for rooms (except for the player list, but maybe we don't need that, read on...)
keeping each player pointing to a single instance of a consistent world and maintaining the invariant that (given: p :: Player, r :: Room):
p `elem` (Room.players r) => (Player.room p == r)
There are always two techniques for maintaining invariants. One is to design your data structures so it is impossible to violate them, (our DB fan would call this normalization), and the other is to ensure that all modifications are carried out via a relatively small set of functions which are responsible to maintaining the invariant (which the DB fan would call middleware). In solution 1, you would simply not store the player list in the room structure at all. Instead, you would only store the room in the player, and whenever you wanted to answer the question "Which players are in this room" you would do a quick search through all players to find them playersInRoom :: Room -> [Player] playersInRoom r = [p | p <- allplayers, (room p) == r] -- Just like a database! In SQL we'd say: -- SELECT p FROM allplayers WHERE p.room = r Of course the disadvantage to this is the linear search through all the players. Depending on how often this is performed, this may actually cost you less that explicitly "caching" the value inside the room. In solution 2, of course, whenever you move a player from room A to room B you do it via a function which is explicitly designed to keep everythign up to date: movePlayer PlayerID -> RoomID -> RoomID -> Game () movePlayer pid raid rbid = do p <- getPlayer pid setPlayer pid (p {room = rbid}) ra <- getRoom raid rb <- getRoom rbid setRoom raid (ra {players = (players ra)\\[pid]} ) setRoom rbid (rb {players = pid : (players rb)} ) Which I've written in an imaginary Game monad, and using rather ugly low-level combinators get/set Room/Player. You could make it look much more pleasant in practice with some better chosen combinators. Still I think solution 1 (don't store redundant data which can get out of sync) has a lot to recommend it and solution 2 may be premature optimization; i.e. implement solution 2, which is a kind of caching of computed data, only once you've convinced yourself that recalculating that data every time really is slowing you down.
This needs to stand up to concurrent modification of a shared world structure, but I think I'll set up the concurrency controls after I get my head around this.t The simplest way to do this is to bundle all your big shared mutable world into a single MVar. What this amounts to is perfect brute force serialisation of the actual modification part: i.e. all world modifications share a global lock. This is easy to implement and easy to reason about.
If that turns out to be too restrictive, then you split up the MVars into smaller pieces, but then you have to think a bit harder to convince yourself it is safe. Jules

Jules Bean wrote:
Jack Kelly wrote: -snip-
Essentially you have to choose some form of identity (relational DB fans might call it a "key") for your players and rooms. My gut feeling is that String will do fine; it's nice for debugging, gives rise to a natural way to write your admin commands and so on. However, it may not be quite good enough (100 mobs all called "an orc" need individual keys) and you may end up using an Int or Int64 for your key.
Perhaps doing the keying on an Int and then having a [String] of names that entities can be called is the way to go. List comprehensions will still work: let ents = [ ent | ent <- allents, name `elem` (Entity.aliases ent) ] in -- ... but so will addressing an object uniquely when needed: let target = listToMaybe $ [ ent | ent <- allents, id == (Entity.id ent) ] in -- ...
Once you've chosen a key, you can do something very like the C++ structure: -snip-
I'm thinking of doing the following (based on some suggestions from #haskell): data Room = Room {...} type Zone = Data.Map String Room type World = Data.Map String Zone type ZoneID = String type RoomID = String data Location = Location { zone :: ZoneID, room :: RoomID} data Player = Player { location :: Location, ... } Aside: When should I use newtype vs type? From what I understand, both create type aliases that are only relevant at compile time, but newtype disallows things like: -- Bad newtype Fred = Fred String putStrLn $ Fred "this won't work" as a rule of thumb, is it `better' to newtype things so that the type system can trip you up?
I should mention, actually, that there is also the naive solution: -snip- That does seem like a bad idea.
There are always two techniques for maintaining invariants. In solution 1, you would simply not store the player list in the room structure at all. Instead, you would only store the room in the player, and whenever you wanted to answer the question "Which players are in this room" you would do a quick search through all players to find them
playersInRoom :: Room -> [Player] playersInRoom r = [p | p <- allplayers, (room p) == r] -- Just like a database! In SQL we'd say: -- SELECT p FROM allplayers WHERE p.room = r
Of course the disadvantage to this is the linear search through all the players. Depending on how often this is performed, this may actually cost you less that explicitly "caching" the value inside the room.
I'd not thought of doing it that way. Maybe because keeping a couple of pointers in sync in C is less painful then coding up a search to generate an appropriate list. Learning to stop looking through the "imperative lens" is hard.
In solution 2, of course, whenever you move a player from room A to room B you do it via a function which is explicitly designed to keep everythign up to date:
movePlayer PlayerID -> RoomID -> RoomID -> Game () movePlayer pid raid rbid = do p <- getPlayer pid setPlayer pid (p {room = rbid}) ra <- getRoom raid rb <- getRoom rbid setRoom raid (ra {players = (players ra)\\[pid]} ) setRoom rbid (rb {players = pid : (players rb)} )
Which I've written in an imaginary Game monad, and using rather ugly low-level combinators get/set Room/Player. You could make it look much more pleasant in practice with some better chosen combinators.
Game looks like it'd behave like some kind of specialisation of State, but since there'd be responses sent out to the sockets, I'm thinking of something like StateT GameState IO (). Is it worth having the StateT because IO already handles some concept of state? If so, will running something like: forkIO $ evalStateT gameLoop initialstate work or will the IO actions be queued up until the end of gameLoop? The idea of building a Game monad is attractive regardless because I'll still need functions like movePlayer regardless of which path is chosen.
Still I think solution 1 (don't store redundant data which can get out of sync) has a lot to recommend it and solution 2 may be premature optimization; i.e. implement solution 2, which is a kind of caching of computed data, only once you've convinced yourself that recalculating that data every time really is slowing you down.
I think I'll go with solution 1. It's conceptually simpler and if it's slow I can profile and optimise later, right?
The simplest way to do this is to bundle all your big shared mutable world into a single MVar. What this amounts to is perfect brute force serialisation of the actual modification part: i.e. all world modifications share a global lock. This is easy to implement and easy to reason about.
I think that my fear of doing this was again instinctive premature optimisation.
If that turns out to be too restrictive, then you split up the MVars into smaller pieces, but then you have to think a bit harder to convince yourself it is safe.
Another idea that I picked up from the "Simple TCP server" (http://sequence.complete.org/node/258) is to have a single thread that's dedicated to manipulating the world and have each socket reading thread send messages to this game logic thread through (TChan String)s or similar. Is this a better or worse idea than having clients mutate the world directly? Both amount to doing one thing at a time (if the global MVar route is chosen). Thanks Jules. It's been a great help. -- Jack

On Dec 16, 2007 1:45 PM, Jules Bean
This needs to stand up to concurrent modification of a shared world structure, but I think I'll set up the concurrency controls after I get my head around this.t The simplest way to do this is to bundle all your big shared mutable world into a single MVar. What this amounts to is perfect brute force serialisation of the actual modification part: i.e. all world modifications share a global lock. This is easy to implement and easy to reason about.
If that turns out to be too restrictive, then you split up the MVars into smaller pieces, but then you have to think a bit harder to convince yourself it is safe.
STM! Why use Haskell concurrently and not use STM? STM is beautiful. Luke

Jack Kelly
struct room{ ... struct player * players; ... };
struct player{ ... struct room * room; ... };
Extreme programming (or maybe it was some other "agile" thingy) suggests doing the simplest possible approach that could conceivably work. What about: data Room = .. data Player = .. locations :: [(Player,Room)] ? -k -- If I haven't seen further, it is by standing in the footprints of giants
participants (4)
-
Jack Kelly
-
Jules Bean
-
Ketil Malde
-
Luke Palmer