[Haskell-cafe] Implementing a MUD server in haskell

Jules Bean jules at jellybean.co.uk
Sun Dec 16 08:45:03 EST 2007


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



More information about the Haskell-Cafe mailing list