Game of Life in C

August 16, 2025

Game of Life in C using Test Driven Development

It's been a while I didn't write C code. ANSI C by K&R on the right side of my table motivated me enough to try game of life in C. This time I will be doing TDD.

No idea, what kind of libraries are required to setup TDD.

Time to fire up Cursor and ask how to set it up.

Initial Prompt

I started with this prompt:

what are the libraries to do tdd in c? how should we go about it?? i want to write game of life in c using tdd. tell me most suitable framework for it

It gave me a list of frameworks but most suitable one is Unity + CMake and a directory structure below.

gol/
├── src/
   ├── game_of_life.c
   └── game_of_life.h
├── test/
   ├── test_game_of_life.c
   └── unity/
       ├── unity.c
       └── unity.h
├── build/
├── CMakeLists.txt
├── Makefile
└── README.md

Setting up the unity framework

Step 1: Downloading the header files

We need to download the files first.
Here is the GitHub repository link: Unity Framework

Below are the simple commands to download the unity header files.

# Create the test directory and download Unity
mkdir -p test/unity
cd test/unity
curl -O https://raw.githubusercontent.com/ThrowTheSwitch/Unity/master/src/unity.c
curl -O https://raw.githubusercontent.com/ThrowTheSwitch/Unity/master/src/unity.h
curl -O https://raw.githubusercontent.com/ThrowTheSwitch/Unity/master/src/unity_internals.h

Step 2: Create project structure

Let's create a basic project structure:

mkdir -p src build

Step 3: Create a simple Makefile

Below is a code for a Makefile:

# Makefile for Game of Life TDD project

CC = gcc
CFLAGS = -Wall -Wextra -std=c99 -g
TEST_CFLAGS = $(CFLAGS) -I./test/unity

# Directories
SRC_DIR = src
TEST_DIR = test
BUILD_DIR = build

# Source files
SRC_FILES = $(wildcard $(SRC_DIR)/*.c)
TEST_FILES = $(wildcard $(TEST_DIR)/*.c)
UNITY_FILES = $(TEST_DIR)/unity/unity.c

# Object files
SRC_OBJS = $(SRC_FILES:$(SRC_DIR)/%.c=$(BUILD_DIR)/%.o)
TEST_OBJS = $(TEST_FILES:$(TEST_DIR)/%.c=$(BUILD_DIR)/test_%.o)
UNITY_OBJS = $(BUILD_DIR)/unity.o

# Targets
.PHONY: all clean test

all: $(BUILD_DIR)/game_of_life

test: $(BUILD_DIR)/test_runner
	./$(BUILD_DIR)/test_runner

$(BUILD_DIR)/game_of_life: $(SRC_OBJS)
	$(CC) $(CFLAGS) -o $@ $^

$(BUILD_DIR)/test_runner: $(TEST_OBJS) $(UNITY_OBJS) $(SRC_OBJS)
	$(CC) $(TEST_CFLAGS) -o $@ $^

$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
	@mkdir -p $(BUILD_DIR)
	$(CC) $(CFLAGS) -c $< -o $@

$(BUILD_DIR)/test_%.o: $(TEST_DIR)/%.c
	@mkdir -p $(BUILD_DIR)
	$(CC) $(TEST_CFLAGS) -c $< -o $@

$(BUILD_DIR)/unity.o: $(TEST_DIR)/unity/unity.c
	@mkdir -p $(BUILD_DIR)
	$(CC) $(TEST_CFLAGS) -c $< -o $@

clean:
	rm -rf $(BUILD_DIR)

Step 4: The first Test

Let's create a first test that our setup is working:

// test/test_game_of_life.c
#include "unity.h"
#include "../src/game_of_life.h"

void setUp(void) {
    // Set up code that runs before each test
}

void tearDown(void) {
    // Clean up code that runs after each test
}

void test_unity_works(void) {
    TEST_ASSERT_EQUAL(1, 1);
}

void test_dead_cell_with_three_neighbors_becomes_alive(void) {
    // This will be your first real test
    // For now, just make it pass to verify setup
    TEST_ASSERT_TRUE(1);
}

int main(void) {
    UNITY_BEGIN();
    RUN_TEST(test_unity_works);
    RUN_TEST(test_dead_cell_with_three_neighbors_becomes_alive);
    return UNITY_END();
}

Step 5: Creating an Initial header file

// src/game_of_life.h
#ifndef GAME_OF_LIFE_H
#define GAME_OF_LIFE_H

#include <stdbool.h>

// Function declarations will go here
// For now, we'll add them as we write tests

#endif // GAME_OF_LIFE_H

Step 6: Create initial source file

// src/game_of_life.c
#include "game_of_life.h"

// Function implementations will go here
// For now, it's empty

Final step: Testing the setup

make test

Here is the output I got:

  gol make test
gcc -Wall -Wextra -std=c99 -g -I./test/unity -c test/test_game_of_life.c -o build/test_test_game_of_life.o
gcc -Wall -Wextra -std=c99 -g -I./test/unity -c test/unity/unity.c -o build/unity.o
gcc -Wall -Wextra -std=c99 -g -c src/game_of_life.c -o build/game_of_life.o
gcc -Wall -Wextra -std=c99 -g -I./test/unity -o build/test_runner build/test_test_game_of_life.o build/unity.o build/game_of_life.o
./build/test_runner
test/test_game_of_life.c:25:test_unity_works:PASS
test/test_game_of_life.c:26:test_dead_cell_with_three_neighbors_becomes_alive:PASS

-----------------------
2 Tests 0 Failures 0 Ignored
OK

It worked...

Writing Actual Tests

The Red phase

We will write tests only so that we can see them fail beautifully...

First, let's define function declerations in our game_of_life.h file.

First, we will start off with cell state management.

So here are rules of Game of Life:

The universe of the Game of Life is an infinite, two-dimensional orthogonal grid of square cells, each of which is in one of two possible states, live or dead (or populated and unpopulated, respectively). Every cell interacts with its eight neighbours, which are the cells that are horizontally, vertically, or diagonally adjacent. At each step in time, the following transitions occur:

  1. Any live cell with fewer than two live neighbours dies, as if by underpopulation.
  2. Any live cell with two or three live neighbours lives on to the next generation.
  3. Any live cell with more than three live neighbours dies, as if by overpopulation.
  4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.

Let's talk about cell first, then we will move on to grid...

// Data structures
typedef struct {
    bool alive;
} Cell;

typedef struct {
    Cell** cells;
    int width;
    int height;
} Grid;

// Cell functions
Cell create_cell(bool alive);
bool is_alive(Cell cell);

Now, let's write cell state management tests:

// test/game_of_life.c
void test_create_dead_cell(void) {
    Cell cell = create_cell(false);
    TEST_ASSERT_FALSE(is_alive(cell));
}

void test_create_alive_cell(void) {
    Cell cell = create_cell(true);
    TEST_ASSERT_TRUE(is_alive(cell));
}

int main(void) {
  UNITY_BEGIN();

  RUN_TEST(test_create_dead_cell);
  RUN_TEST(test_create_alive_cell);

  return UNITY_END();
}

When we run the tests by executing the command: make test

Below is the output we get:

  gol make test
gcc -Wall -Wextra -std=c99 -g -I./test/unity -c test/test_game_of_life.c -o build/test_test_game_of_life.o
gcc -Wall -Wextra -std=c99 -g -I./test/unity -o build/test_runner build/test_test_game_of_life.o build/unity.o build/game_of_life.o
Undefined symbols for architecture arm64:
  "_create_cell", referenced from:
      _test_create_dead_cell in test_test_game_of_life.o
      _test_create_alive_cell in test_test_game_of_life.o
  "_is_alive", referenced from:
      _test_create_dead_cell in test_test_game_of_life.o
      _test_create_alive_cell in test_test_game_of_life.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [build/test_runner] Error 1

The Green phase

We will add enough code to make our tests pass:

// Cell functions
Cell create_cell(bool alive) {
    Cell cell;
    cell.alive = alive;
    return cell;
}

bool is_alive(Cell cell) {
    return cell.alive;
}

Here is the output we got:

  gol make test
gcc -Wall -Wextra -std=c99 -g -c src/game_of_life.c -o build/game_of_life.o
gcc -Wall -Wextra -std=c99 -g -I./test/unity -o build/test_runner build/test_test_game_of_life.o build/unity.o build/game_of_life.o
./build/test_runner
test/test_game_of_life.c:171:test_create_dead_cell:PASS
test/test_game_of_life.c:172:test_create_alive_cell:PASS

-----------------------
2 Tests 0 Failures 0 Ignored
OK

Our green phase is completed successfully!!!

Can we add more tests on Cell?

Let's try!!!

We can think of few cases like:

Let's see what happens when we call is_alive() function multiple times on alive_cell and dead_cell.

void test_cell_consistency_after_multiple_checks(void) {
    Cell alive_cell = create_cell(true);
    Cell dead_cell = create_cell(false);

    // Multiple calls to is_alive should return same result
    TEST_ASSERT_TRUE(is_alive(alive_cell));
    TEST_ASSERT_TRUE(is_alive(alive_cell));
    TEST_ASSERT_TRUE(is_alive(alive_cell));

    TEST_ASSERT_FALSE(is_alive(dead_cell));
    TEST_ASSERT_FALSE(is_alive(dead_cell));
    TEST_ASSERT_FALSE(is_alive(dead_cell));
}

int main(void) {
  UNITY_BEGIN();

  RUN_TEST(test_create_dead_cell);
  RUN_TEST(test_create_alive_cell);
  RUN_TEST(test_cell_consistency_after_multiple_checks);
  return UNITY_END();
}

Here is the output we are getting:

  gol make test
./build/test_runner
test/test_game_of_life.c:213:test_create_dead_cell:PASS
test/test_game_of_life.c:214:test_create_alive_cell:PASS
test/test_game_of_life.c:217:test_cell_consistency_after_multiple_checks:PASS

-----------------------
3 Tests 0 Failures 0 Ignored
OK

We can see that state doesn't get mutated even after calling is_alive() function multiple times on both alive and dead cells.

It was good exploring TDD in C and how easy testing is when we do test it using Unity.

Though our Game Of Life program is far from complete but we learnt how can we do it very easily using Unity.