Atlantis TDD (4) Working with Legacy Code

After all this prep-work, it’s time to actually work on the game. Which game? I’m going to use Atlantis 1, the original game that started it all. The movement code in it is really gross, and the code only compiles under DOS, so the first order of business is to clean it up a bit. The result compiles under Linux without warnings, and even if nothing else comes out of this project of mine, at least there is now an Atlantis 1 code base you can just use.

This is classic legacy code: It’s not very modular at all, and there are absolutely no tests. Before I make any changes, I want to make sure that I won’t break anything, so I am going to put the movement code under testing. For this, I’m extracting the movement code into a process_movement function. Next, I’m going to make sure all code paths in that function are tested at least once.

static void test_movement(CuTest * tc)
{
  unit * u;
  region *r1, *r2;

  resetgame();
  r1 = createregion(0, 0);
  r1->terrain = T_PLAIN;
  r2 = createregion(1, 0);
  r2->terrain = T_PLAIN;
  createregion(2, 0)->terrain = T_PLAIN;
  u = createunit(r1);
  strcpy(u->thisorder, "move east");
  process_movement();
  CuAssertPtrEquals(tc, u, r2->units);
}

Since this test is only protecting the original Atlantis game against regressions, I can write it in terms of the Atlantis code base and its objects (and use r2->units directly, etc). As long as this test passes, I can be certain that regular unit movement works. This kind of protection gives enormous peace of mind to a programmer who is working with a code base that isn’t his own (or is son old he doesn’t remember writing it). A couple more tests around modifications in the Atlantis source needed to be written, you can find them in the atlantis_test.c source on github.

Next up, I’m going to slap together an interface to stitch the Atlantis 1 implementation into my interfaces! And maybe then I can write my movement logic?

Atlantis TDD (3) CMake is awesome

I started this project with a simple Makefile. Eventually, I’d gotten to 30+ source files, and the Makefiles were getting cumbersome to update. I mean, judge for yourself: How easy is this?


cmake_minimum_required(VERSION 2.4)
project (logic C)

include_directories (..)

set (MOCK_SRCS ../mock/region.c ../mock/unit.c
../mock/keyvalue.c ../mock/mock_svc.c) add_executable ( movement_test movement.c movement_test.c ../cutest/CuTest.c ${MOCK_SRCS} ) add_test(movement_test movement_test)

This is the rule to build the tests for my movement code and link them with the mock implementation of the game structures. It’s platform-agnostic, too: On Windows, I can use this to build Visual Studio project files. On Linux, cmake generates Makefiles for me that are far cleverer than any I ever wrote by hand.

This isn’t my first project to use CMake. I’ve looked at a lot of build systems over the years, and this is the one I’ve stuck with. There are still things that are hard to do in it, but they are the kind of things that are huge pains in every other build system, too. But the easy stuff, the 95% of the work, is actually easy.

This is the first time I’ve used CTest, though, which ships with CMake. You can add all your tests with the ADD_TEST command, and CMake will generate the input for CTest from that, so you can run all the different tests in your project with a single command.

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!