John Conway's Game of Life - NES

game of life nes game development

I recently purchased the EverDrive N8 Pro 72pin which allows me to play NES ROMs on my NES opposed to using an emulator. At the time of this writing, I have yet to receive the cartridge but I am preparing for when it arrives!

For this project I decided to try out the NES Lib which are in C. Given my introduction to programming was MS-DOS 16 bit x86 assembly, I find the idea of learning NES's assembly to be a fun challenge.

Screenshots running on the FCEUX emulator

Pinwheel

Pulsar

Glider

Random

Random

Source Code

#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include "lib/neslib/neslib.h"

// general vars
static uint8_t spr;
static uint8_t input;

// map vars
#define WIDTH       256
#define HEIGHT      240
#define TILE_SIZE   9
#define TILES_X     15
#define TILES_Y     15

static const uint8_t ALIVE = 1;
static const uint8_t DEAD = 0;
static const uint8_t DX = (WIDTH - (TILES_X * TILE_SIZE)) / 2;
static const uint8_t DY = (HEIGHT - (TILES_Y * TILE_SIZE)) / 2;

static uint8_t i, frame = 0, paused = 0;
static uint8_t x, y;

static uint8_t fg_layer = 0;
static uint8_t bg_layer = 1;
static uint8_t neighbors;
static uint8_t next_state;
static uint8_t layers[2][TILES_Y][TILES_X] = {
        { // https://conwaylife.com/wiki/Pinwheel
                {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                {0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0},
                {0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0},
                {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                {0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0},
                {0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0},
                {0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0},
                {0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0},
                {0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0},
                {0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0},
                {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                {0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0},
                {0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0},
                {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
        },
        { // https://conwaylife.com/wiki/Pulsar
                {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                {0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0},
                {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                {0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0},
                {0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0},
                {0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0},
                {0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0},
                {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                {0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0},
                {0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0},
                {0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0},
                {0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0},
                {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                {0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0},
                {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
        }
};

/**
 * Handle wrap-around.
 * e.g.
 * TILES_X = 4
 * x = 0 -> (0 + 4) % 4 -> 4 % 4 -> 0
 * x = 1 -> (1 + 4) % 4 -> 5 % 4 -> 1
 * x = 2 -> (2 + 4) % 4 -> 6 % 4 -> 2
 * x = 3 -> (3 + 4) % 4 -> 7 % 4 -> 3
 * x = 4 -> (4 + 4) % 4 -> 8 % 4 -> 0
 */
uint8_t get_wrap(uint8_t x, uint8_t y) {
    return layers[fg_layer][(y + TILES_Y) % TILES_Y][(x + TILES_X) % TILES_X];
}

uint8_t get(uint8_t x, uint8_t y) {
    if (x < 0 || x >= TILES_X || y < 0 || y >= TILES_Y) {
        return 0;
    }
    return layers[fg_layer][y][x];
}

uint8_t count_alive_neighbors(uint8_t x, uint8_t y) {
    return get(x - 1, y - 1) + get(x, y - 1) + get(x + 1, y - 1) +
           get(x - 1, y) + get(x + 1, y) +
           get(x - 1, y + 1) + get(x, y + 1) + get(x + 1, y + 1);
}

static void randomize_map() {
    for (y = 0; y < TILES_Y; y++) {
        for (x = 0; x < TILES_X; x++) {
            if (rand8() > 207) {
                layers[fg_layer][y][x] = ALIVE;
            } else {
                layers[fg_layer][y][x] = DEAD;
            }
            layers[bg_layer][y][x] = DEAD;
        }
    }
}

static void clear_map() {
    for (y = 0; y < TILES_Y; y++) {
        for (x = 0; x < TILES_X; x++) {
            layers[fg_layer][y][x] = DEAD;
            layers[bg_layer][y][x] = DEAD;
        }
    }
}

/**
 *  @
 *   @
 * @@@
 * [y, x]
 */
void build_glider() {
    clear_map();
    x = 3;
    y = 3;
    layers[fg_layer][y + 2][x + 1] = ALIVE; // top
    layers[fg_layer][y + 1][x + 2] = ALIVE; // middle
    layers[fg_layer][y + 0][x + 0] = ALIVE; // ------
    layers[fg_layer][y + 0][x + 1] = ALIVE; // bottom
    layers[fg_layer][y + 0][x + 2] = ALIVE; // ------
}

void start_pinwheel() {
    fg_layer = 0;
    bg_layer = 1;
}
void start_pulsar() {
    fg_layer = 1;
    bg_layer = 0;
}

const uint8_t sprites_pal[32] = {
        0x0f, 0x17, 0x27, 0x37,
        0x0f, 0x29, 0x21, 0x31,
        0x0f, 0x15, 0x25, 0x35,
        0x0f, 0x19, 0x29, 0x39,
        0x0f, 0x05, 0x10, 0x10,
        0x0f, 0x07, 0x12, 0x14,
        0x0f, 0x30, 0x8f, 0x33,
};

static void swap_layers() {
    bg_layer = !bg_layer;
    fg_layer = !fg_layer;
}

static void handle_input() {
    input = pad_trigger(0);

    if (input & PAD_A + input & PAD_B) {
        randomize_map();
    }
    if (input & PAD_START) {
        paused = !paused;
    }
}

static compute_next_state() {
    for (y = 0; y < TILES_Y; y++) {
        for (x = 0; x < TILES_X; x++) {
            neighbors = count_alive_neighbors(x, y);
            next_state = layers[fg_layer][y][x]; // default to carry over state
            if (layers[fg_layer][y][x] == ALIVE) {
                // 1. Any live cell with fewer than two live neighbors dies, as if caused by underpopulation.
                // 2. Any live cell with more than three live neighbors dies, as if by overcrowding.
                if (neighbors < 2 || neighbors > 3) {
                    next_state = DEAD;

                }
                    // 3. Any live cell with two or three live neighbors lives on to the next generation.
                else {
                    next_state = ALIVE;
                }
            } else {
                // 4. Any dead cell with exactly three live neighbors becomes a live cell.
                if (neighbors == 3) {
                    next_state = ALIVE;
                }
                    // 5. Else cell remains dead.
                else {
                    next_state = DEAD;
                }
            }

            layers[bg_layer][y][x] = next_state;
        }
    }
}

static render() {
    oam_clear();
    spr = 0;
    for (y = 0; y < TILES_Y; ++y) {
        for (x = 0; x < TILES_X; ++x) {
            if (layers[fg_layer][y][x] == ALIVE) {
                neighbors = count_alive_neighbors(x, y);
                spr = oam_spr(DX + x * TILE_SIZE, DY + y * TILE_SIZE + 8, layers[fg_layer][y][x], neighbors, spr);
            }
        }
    }
}

void main(void) {
   //randomize_map();
   //build_glider();
   start_pinwheel();
   //start_pulsar();

    pal_spr(sprites_pal);   // set palette for sprites
    ppu_on_all();           // enable rendering

    while (1) {
        ppu_wait_frame();   // wait for next TV frame

        handle_input();

        if (!paused && frame % 2 == 0) {
            compute_next_state();
        }

        render();

        if (!paused && frame % 2 == 0) {
            swap_layers();
        }

        ++frame;
    }

}

NES ROMs

Tool Chain + Build NES File

  • cc65
  • chr2png
  • png2chr
  • nsf2data
  • test2data

Build Toolchain + NES File

git clone git@github.com:kennycason/game_of_life_nes.git
cd game_of_life_nes

Checkout the cc65 submodule

git submodule update --init --recursive

Build cc65

cd tools/cc65
make

Build the other tools

cd tools
make

Build the NES ROM

make

Open NES ROM with FCEUX NES Emulator

fceux game_of_life.nes

Resources + Referenced Projects

  • https://github.com/slembcke/neslib-template
  • https://github.com/sebastiandine/openNES-Snake
  • http://shiru.untergrund.net/articles/programming_nes_games_in_c.htm
  • https://nesdoug.com/

Notes:

  • NES limits the number of tiles that can be rendered to the screen. This causes some tiles to disappear between renders. The state is still preserved.
  • V2 will use VRAM for better graphics performance.


Projects

Site

Tags