art with code

2009-07-03

Tomtegebra - a small Haskell puzzle game


Tomtegebra, my project for our summer Haskell lab course, is a small puzzle game that masks abstract algebra with cutesy animals. Your goal is to apply transformations to an equation tree to either make both sides equal or, in some levels, to find a binding for the wanted variable.

The levels are Abelian group axioms (no closure though, I don't know how to do that.) The animals are binary operators, the fruits are variables and the rock is a constant.

Implementation follows: the rendering is done with OpenGL, event handling with GLUT, image loading done with Gdk.Pixbuf, textures created with Cairo and text with Pango. There's a shared AppState record IORef that the GLUT callbacks edit and draw. Draw calls, event handling, image loading and all other icky side-effecty stuff happens in the IO monad, while the actual gameplay core is functional.

I'm using vertex buffer objects for drawing and a wholly Haskell-side transform matrix system, mostly for the heck of it. My matrix math lib is probably somewhere between pretty slow and really slow, as it uses lists of lists to represent the matrices (oh the humanity.)

The whole thing weighs in at around 1200 lines of source code, which sounds about OK for a small project like this. The algebra module is the largest individual part, followed by game rendering and the game logic. The drawing helper libraries are scattered in smaller files, in total they'd be about the same size as the algebra bit.

Haskell-land


Overall, Haskell is nice. I like ghc --make and typeclasses. I don't like fromIntegral. GHC doesn't error on partial pattern matches at compile time, which makes it error-prone (at least if you don't use -Wall.) I had some mysterious run-time crash bugs, and they didn't show where the error happened, which was flummoxing. Debugging by randomly changing things did conquer in the end, though.

And Haskell has the usual statically typed functional language good bits.

I used Haddock and Cabal for documentation and compiling, respectively. In the beginning (read: until yesterday) I was using a build script which was more or less ghc --make *.hs, but now it's all Cabalized and everything. Yay!

Tales of woe


There are odd pauses in the game every couple of seconds (old-generation GC run?) Gtk2Hs doesn't have -prof on Debian, so I haven't been able to profile the thing. The game uses 5-10% CPU to draw at 60fps on my Core 2. Which sounds like pretty much for such a trivial drawing load.

The Haskell OpenGL library is not really OpenGL. It's more a new graphics library that calls OpenGL internally, so you get to relearn the names and syntax of everything. And yet it uses Ptrs as its preferred data structure and the GL numeric types, so you can't just pass it lists of Doubles or other easy Haskell data structures. It's a bit worst of both worlds in terms of user friendliness.

GLUT's callback procedures are exactly the wrong way to go for a functional language. They basically necessitate having shared mutable state. A recursive event loop, Xlib/SDL-style, is probably a better way. At least there you wouldn't have to fool around with IORefs.

eventLoop state = do
ev <- nextEvent
state' <- handleEvent state ev
if quit state'
then return ()
else eventLoop state'


I used only lists and tuples, because all the other Haskell data structures are weird (more like, I don't know them, so better not use them, otherwise I might be forced to learn something *shock, horror*) And that means that I sometimes get to O(n) the O(1) and O(nm) the O(m). Oh well, maybe I go back and try to optimize things with Data.Map and UArray and friends if I ever get the profiler working.

I didn't like Haddock's parser. Writing code examples was a pain because it complained about them not parsing. And it's using ', ", and ` as special characters, so using the less whiny @code example@ requires backslashing like a demented backswordsman. Haddock only shows the type signature for a function, which means that you get documentation like frustumMatrix :: GLfloat -> GLfloat -> GLfloat -> GLfloat -> GLfloat -> GLfloat -> Matrix4x4, which is not very helpful.

Cabal was a bit of a pain to get going, but not all that bad. The documentation didn't really answer my questions and the examples were kinda useless as they focused on a one-file codebase. You get to poke around blind a good bit. Took me around an hour from mkcabal to a working build with data files found at runtime. The problems I had were: 1) Module name and file name must be the same. 2) If you have user-installed libs, you need to do Setup.hs configure --user to add them to the search path. 3) To get the paths for installed data files, you need to import getDataFilename :: FilePath -> IO FilePath from Paths_mypackage.

Blog Archive