Atlantis TDD (2) Unit Testing

Before I get to write any more implementation, it’s time to write some tests for my interfaces. I like to use CuTest for this, because it’s small, pure C, doesn’t try to be fancy, and there’s only a single file to link to. And now I’m writing tests for every function in the interface and its edge cases:

static void test_get_regions(CuTest * tc)
{
  struct region * results[4];
  void * cur;
  icursor * icur;
  int i, n = 0;

  svc.reset();

  for (i=0;i!=3;++i) {
    svc.regions->create(0, i);
  }
  cur = svc.get_regions(&icur);
  CuAssertPtrNotNull(tc, cur);
  CuAssertPtrNotNull(tc, icur);
  CuAssertIntEquals(tc, 3, icur->get(cur, 4, (void**)results));
  for (i=0;i!=3;++i) {
    int x, y;
    svc.regions->get_xy(results[i], &x, &y);
    CuAssertIntEquals(tc, 0, x);
    n |= (1<<y);
  }
  CuAssertIntEquals(tc, 7, n);

  CuAssertIntEquals(tc, 3, icur->advance(&cur, 4));
  CuAssertIntEquals(tc, 0, icur->advance(&cur, 4));
  if (icur->destroy) icur->destroy(cur);
}

This function tests the cursor returned by the get_regions function that I talked about in my previous post. As tests go, it’s pretty big: after first creating 3 regions, it asserts that the cursor will return those regions, will only return three even if asked for four, and that it’s able to advance until it reaches the end. Notice how, once again, the code is entirely written in terms of the new interface. I tend to write “struct region” instead of “region” for the type here, to remind myself that I don’t have access to the members of my objects, and apart from knowing it’s a struct, I really don’t know anything about it.

Now, I can’t run this test yet, because to don that, I would need an actual implementation of the interface to test. Since I haven’t broken out the Atlantis source yet, I’m going to write my own implementation of the data structures that does the bare minimum, and hook it up to the interfaces. You can see the result of that in the mock directory of my github project. This reference implementation will also allow me to test my game logic later without having to link against Atlantis or Eressea.

Atlantis TDD (1) Service Provider and Interfaces

The first thing I’m going to need is a service provider that serves the interface to the game. I’m a little bit spoiled by testing in PHP, where it’s easy to use an associative array to build an interface on the fly, but I obviously won’t get this here. Here’s what my interface roughly looks like:

typedef struct iunit {
  struct unit * (*create)(void);
  void (*destroy)(struct unit *);
  struct unit * (*get)(int);

  int (*get_uid)(const struct unit *);
  struct region * (*get_region)(const struct unit *);
  void (*set_region)(struct unit *, struct region *);
  ... /* more of the same */
} iunit;

/* similar struct for iregion, iship, ibuilding TBD */

typedef struct igame {
  struct iunit * units;
  struct iregion * regions;

  int max_directions;

  void (*reset)(void);
  void * (*get_regions)(struct icursor **);
  ... /* more of the same */
} igame;

That’s a lot of function pointers! As I said in my last post, the idea is to write code that is independent of implementation, so at no point can I use the actual unit structure that’s in Atlantis, or even access u->no directly. Some other game might have an entirely different way of storing ids, after all. Instead, I will be using svc.units->get_uid(u), which is more to type, but easy to plug into with an implementation-dependent function. You can see the rest of the interface classes on github, if you are curious.

I’m pretty pleased with the icursor interface. The global list of the game’s regions in Atlantis is a next-pointer chained linked list. In Eressea, it is an unrolled linked list, and I assume that in A5, it is a std::list. Or maybe it’s a hashtable? I will be iterating over all regions a lot in the game logic, and similar issues will arise for all units in a region, etc. So here’s the icursor interface:

typedef struct icursor {
  void (*destroy)(void * cursor);
  int (*get)(void * cursor, int n, void * results[]);
  int (*advance)(void ** cursor, int n);
} icursor;

A function like svc.get_regions returns a void * typed data pointer (the cursor), and an icursor interface that can be used to iterate the particular type of container it represents. You can either get one or more elements from a cursor, or advance it. When you’re done, release the cursor by calling destroy. Here’s an example:

  icursor *icursor;
  void *cur = svc.get_regions(&icursor);
  region *r;
  while (icur->get(cur, 1, &r)) {
    printf("region %d,%dn", r->x, r->y);
    icur->advance(&cur, 1);
  }
  icur->destroy(cur);

That’s the first game logic I’ve written since I started! If you think I’m off to a good start and will be writing that movement code next, think again: It’s time to write tests first!

Atlantis TDD

Some days ago, there was a discussion about movement rules on the atlantisdev mailing list that made me want to experiment with a few different mechanics. Now, I don’t know the Atlantis5 code very well, and I didn’t feel like making the change for Eressea only, because then nobody on the mailing list would benefit. That all made me wonder if I couldn’t just write movement logic independent from the game’s other implementation.

So now I’m writing the logic for an idealized interface instead, and implementing said interface for each game individually. The result should be the first truly modular code I’ve written in C, and there will be tests for everything, unlike most C code I’ve written in the past. In addition to putting it on github, I’ve chosen this tumblr to document my progress.

Now, how do I add syntax highlighting to my tumblr?


#include <stdio.h>
int main(int argc, char **argv) {
  fputs(stdin, "Hello, Worldn");
  return 0;
}

That looks presentable! I have to change the < and > to &lt; and &gt;, but apart from that, Google Prettify works as advertised. Time to write about some code!

Sumatra PDF is awesome

Things I do not want in a PDF reader:

  • an annoying auto-updater that crashes every other time
  • a 40 MB installer
  • multiple security patches per week
  • up-sells for other adobe products
  • advertising

Things I want in a PDF reader:

  • a crisp image
  • an open source rendering engine (muPDF)
  • text search

Sumatra PDF fills all my needs.