Making Haskell nicer for game programming
Last week I tried to create a board game programming architecture in Haskell. The idea was to base the architecture around specifying an abstract state machine for the game. As we discussed in GameDev this week, abstract state machines aren’t exactly appropriate for games, but they’re close enough that you can make a few modifications to the idea and get something cool (the biggest modification being that your set of rules is part of your state).
Haskell was a joy to work with in this sense. I made a monad which handled the transactional nature of ASMs; I differentiated between rules and actions in order to enforce the if condition then action structure which makes reasoning about ASMs easy. It was cool stuff, and it took about 100 lines of code.
Then I set out to write my first game with it. Just a simple bidding game I found somewhere; I don’t remember what it was called. And it hit me: games are stateful, definitely abstract state machines are stateful, and Haskell sucks when it comes to content programming for stateful systems. I was trying to show how easy it would be to specify a game with dynamically changing rules, and all I could think about was how to refactor the horribly ugly code that ensued.
For the first time ever, I used C macros in a Haskell program. There are a lot of good reasons not to do this. But it did make working with stateful data much easier. Here’s the idea:
First, let’s make our game state object:
data GameState
= GameState { _p1bid :: Int
, _p2bid :: Int
, _score :: Int
}
Because very few Haskellers read this blog, I’ll explain. That essentially declares a new class with three member variables, whose names you should be able to extract.
Now we make the monad in which we will perform game computations:
type Game a = StateT GameState IO a
When we perform a computation inside the Game monad, there is a “global” GameState at our disposal. But we also need to interact with the user (the computation is not “pure” in that sense), so we glob on the IO monad.
That was all fairly straightforward. Here’s the new stuff. Define an “Accessor” class, which abstracts over the ability of data inside the state to be read and written:
data Accessor a
= Accessor { readVal :: Game a
, writeVal :: a -> Game ()
}
Again, it’s just a class—actually a template class abstracted over the type a—with two members, readVal and writeVal. Except this time the members have more complex data types. readVal performs a computation given no input (namely, to return the value), and writeVal performs a computation given an a as input (namely, to store the value back into the game state). We will define an Accessor object for each data member in our state, each with different implementations of readVal and writeVal.
Here’s where the macros come in. The implementations are different, but they are regularly different, but not abstractable due to limitations which ultimately boil down to Haskell’s type system (no partial records). We define a macro to define an accessor for us:
#define ACCESSOR(NAME) \
NAME = Accessor { readVal = fmap _ ## NAME get \
, writeVal = \n -> get >>= \s -> put $ s { _ ## NAME = n } \
}
Wow, that’s ugly. Fortunately it’s the last of the ugly we will see. For non Haskellers… I’m not going to explain that in detail. Suffice to say that it defines an Accessor object called NAME which does the right thing. It assumes that the element in our state being referenced is called NAME with an underscore prepended.
Now we define a few accessors:
ACCESSOR(p1bid) ACCESSOR(p2bid) ACCESSOR(score)
And now we can start defining some familiar procedural operations, like := (which we must spell =: because operators that start with a colon have a special meaning) and +=.
a =: x = writeVal a x
a += x = do
a_ <- readVal a -- I would have called it a', but the C preprocessor doesn't like that
a =: (a_ + x)
a -= x = a += (-x)
-- etc. etc.
If we were making this into a library, we would probably define the precedences to be saner, so we wouldn’t have to, for example, parenthesize (a_ + x) above.
And then you can go on defining your game:
main :: IO ()
main = flip evalStateT (GameState 0 0 0) $ loop $ do
liftIO $ putStr "Player 1, bid: "
bid <- liftIO readLn
p1bid =: bid
-- ...
cmp <- liftM2 compare (readVal p1bid) (readVal p2bid)
case cmp of
LT -> score -= 1
EQ -> return ()
GT -> score += 1
Which feels an awful lot nicer. For comparison, score -= 1 would otherwise have been written:
get >>= (s -> put $ s { _score = _score s - 1 })
It still ain’t perfect. I don’t like that I have to read values from the state into temporary variables (well, I don’t have to, but I do if I want anything to be near readable). There’s really no way around that, except for more preprocessing!… which I’m not going to do.
8 comments.