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.

About these ads

9 thoughts on “Making Haskell nicer for game programming

  1. I may have missed a wider point being made by the article, but I think the game described can be implemented in Haskell as follows:


    getBid name = do
    putStr $ name ++ ", bid: "
    bid <- readLn
    return bid

    updateScore s b1 b2 | b1 < b2 = s - 1
    | b1 == b2 = s
    | b1 > b2 = s + 1

    biddingRound score = do
    b1 <- getBid "Player 1"
    b2 <- getBid "Player 2"
    new_score = updateScore score b1 b2
    putStrLn $ "Score: " ++ (show new_score)
    biddingRound new_score

    main = biddingRound 0

    Note that I don’t have access to a compiler so there may be some errors. If there a problems let me know, and I’ll fix ‘em.

  2. Hello,

    I have a small comment in regards to you using StateT. StateT by default is lazy and therefore you may accrue a big thunk that needs to be evaluated to a GameState. Since you’re in IO anyways, why not simply use an IORef? If you do so, make sure that you seq contents before writing them away. For the ICFP contest, we used the following idiom (that Cale added).

    modifyIORef’ :: IORef a -> (a -> a) -> IO ()
    modifyIORef’ r f =
    v <- readIORef r
    let v’ = f v
    in v’ `seq` writeIORef r v’

  3. Levi, Anon: This was in reference to a more general game architecture, which was meant not for handling this simple bidding game, but for handling a game like Twilight Imperium. But the article didn’t focus on that. It’s more about dealing with gigantic unweildy states. The accessor thing is particularly useful when you’re dealing with states with nested (not recursive, just shallow nesting, like a list of players with attributes) data structures, because it composes cleanly. Those are usually a major pain.

    Christophe: I had a good reason not to use an IORef. It’s because the ASM architecture I was writing updates the state in parallel and then merges the changes to see if there are conflicts. It’s an STM-like thing, except you just die and say “hey game designer, you messed up” on retrys. Thanks for the advice on strictness; I’m slowly learning about Haskell’s execution model, but most of the time I keep it way in the abstract and can’t think about what the computer’s actually doing. Maybe I should try to write a performance-critical application in Haskell to force me to learn about that.

  4. I would suggest Drift, Data.Derive or Template Haskell for automagical generation of your accessor functions. All three are slightly different approaches that take a data type, like your GameState and generate “boilerplate” Haskell code for it.

    An example would be:

    > data GameState =
    > GameState{
    > score::Int,
    > player::String
    > }

    – Generated:
    > modifyScore f = modify $ \g -> g{score=f score}
    > modifyPlayer f = modify $ \g -> g{player=f player}
    > setScore x = modify $ \g -> g{score=x}
    > setPlayer x = modify $ \g -> g{player=x}
    > getScore = gets score
    > getPlayer = gets player

    So inside your monads actions you would write:

    > — Adds 1 to score
    > playerHasScored = do
    > modifyScore (+1)

    I wouldnt go to far with the accessors like writing extra functions for +=. They just clutter up your syntax. Just use these generators to do the tedious work of writing simple accessors for you.

    DrIFT (maybe a little outdated now)
    derive (state of the art)data type, like your GameState and generate “boilerplate” Haskell code for it.

    An example would be:

    > data GameState =
    > GameState{
    > score::Int,
    > player::String
    > }

    – Generated:
    > modifyScore f = modify $ \g -> g{score=f score}
    > modifyPlayer f = modify $ \g -> g{player=f player}
    > setScore x = modify $ \g -> g{score=x}
    > setPlayer x = modify $ \g -> g{player=x}
    > getScore = gets score
    > getPlayer = gets player

    So inside your monads actions you would write:

    > — Adds 1 to score
    > playerHasScored = do
    > modifyScore (+1)

    I wouldnt go to far with the accessors like writing extra functions for +=. They just clutter up your syntax. Just use these generators to do the tedious work of writing simple accessors for you.

    DrIFT (maybe a little outdated now)
    Template Haskell Tutorial (do it your self, its like a macro language on drugs)

  5. I think an accessor version of modify would actually suit my needs nicely. I don’t know why I didn’t think of that and chose to implement += and -= explicitly instead.

    modifyA x f = writeVal =<< fmap f (readVal x)
    
  6. As you said, the problem is that you can’t abstract over the fields of a record.

    This could be solved in one of these ways:
    1. Using a Map, and then you can abstract over its keys. (ugly)
    2. Macros, especially Template Haskell, which would allow you to write the equivalent of ACCESSOR(GameState) instead of the three invocations in your example. (ugly)
    3. A more advanced record system, with this feature. (unimplemented)

    So I would say macros are the best. GHC even has a -cpp flag.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s