Built-in project · Arduino Uno R3

Snake on two matrices, on an Arduino Uno

An 8x16 play field on two MAX7219 modules, steered with a thumb stick: eat the apple, grow, and do not hit a wall or yourself. The whole build, an Arduino Uno and 3 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

AdvancedRuns in your browser. Free, and no account needed.

Snake on two matrices · Arduino Uno R3live0.000 s 0.00x
Hold the button
The circuit itself, running here on the simulator. Press what can be pressed; click anything else to open it in the editor.

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Snake on two matrices: an 8x16 play field on a pair of MAX7219 modules,
// steered with a thumb stick.
//
// Wiring, board to modules:
//   DIN -> pin 11 and CLK -> pin 13, shared by both modules
//   CS  -> pin 10 on the top module, pin 9 on the bottom one
//   VCC -> 5 V, GND -> GND
//   stick VRX -> A0, VRY -> A1, SW -> pin 2, VCC -> 5 V, GND -> GND
//
// The same cabinet the `stacker` example builds on the ESP32-C3: two 8x8
// modules standing one above the other, eight columns wide and sixteen rows
// tall. They share data and clock and have a chip select each, so a packet
// goes to exactly the module whose CS is low.
//
// `stacker` cascades its pair instead, DOUT into the next DIN, which saves a
// pin. This one spends it: a cascade sends both modules' packets between one
// pair of CS edges, and a snake step touches two pixels that are usually in
// the same half, so a select each means a step writes one module's eight bytes
// and leaves the other alone.
//
// # Why this is an Uno
//
// The stick came to this board when the ESP32-C3 model had no ADC to read a
// potentiometer with; it has one now (docs/esp32c3.md section 3.5), so the
// game stays here by choice. Everything else about this circuit is the
// stacker's, pin for pin in spirit; only the board and the input differ, and
// a board apiece is what makes the two worth comparing.
//
// # Steering
//
// A stick is an absolute position and a snake has a heading, so the stick is
// read as a direction rather than a place: push it far enough off centre and
// the snake turns that way, let it spring back and the snake carries on. A
// turn into the snake's own neck is refused, which is what stops a flick of the
// wrist from ending the game.
//
// # The body
//
// A ring buffer of cell numbers, one byte each, so a snake as long as the whole
// field costs 128 bytes of the Uno's 2048. A step writes the new head and, when
// the snake has not just eaten, rubs out the old tail: two pixels a step, and
// the other 126 are left exactly as they are.

#include "mokxi_joystick.h"
#include "mokxi_max7219.h"

const uint8_t DIN_PIN = 11;
const uint8_t CLK_PIN = 13;
const uint8_t CS_TOP_PIN = 10;
const uint8_t CS_BOTTOM_PIN = 9;
const uint8_t VRX_PIN = A0;
const uint8_t VRY_PIN = A1;
const uint8_t SW_PIN = 2;

Max7219 top(DIN_PIN, CS_TOP_PIN, CLK_PIN, 1);
Max7219 bottom(DIN_PIN, CS_BOTTOM_PIN, CLK_PIN, 1);
Joystick stick(VRX_PIN, VRY_PIN, SW_PIN);

const uint8_t COLUMNS = 8;
const uint8_t ROWS = 16;
const uint8_t CELLS = COLUMNS * ROWS;

/**
 * How far off centre the stick has to go before it counts as a push, on top of
 * the header's own dead zone.
 */
const int16_t THROW = 160;

/** Milliseconds a step takes, at the start and at its quickest. */
const uint32_t SLOW_STEP = 240;
const uint32_t FAST_STEP = 90;

/** The body, oldest segment first, as cell numbers (`row * COLUMNS + col`). */
uint8_t body[CELLS];
uint8_t head = 0;
uint8_t length = 3;
int8_t dx = 0;
int8_t dy = 1;
uint8_t apple = 0;
uint32_t stepMs = SLOW_STEP;
uint32_t lastStep = 0;
bool dead = false;
/**
 * True between a new game being laid out and the player touching anything.
 *
 * Without it the snake sets off the moment the board powers up, and the field
 * is sixteen rows: thirteen steps of runway, which at a step every 240 ms is
 * three seconds from Run to a wall. Nobody gets a hand to the stick in three
 * seconds. So the snake stands still, apple and all, until the first push or
 * press, which is also what a cabinet does.
 */
bool waiting = true;
uint16_t lfsr = 0xd00du;

static uint8_t nextRandom() {
  // Taps at 16, 14, 13 and 11: the maximal-length sixteen-bit LFSR.
  uint16_t bit = (uint16_t)(((lfsr >> 0) ^ (lfsr >> 2) ^ (lfsr >> 3) ^ (lfsr >> 5)) & 1u);
  lfsr = (uint16_t)((lfsr >> 1) | (bit << 15));
  return (uint8_t)(lfsr % CELLS);
}

/** Light or clear one cell of the field, whichever module it falls on. */
static void plot(uint8_t cell, bool on) {
  uint8_t row = (uint8_t)(cell / COLUMNS);
  uint8_t col = (uint8_t)(cell % COLUMNS);
  if (row < 8) {
    top.setPixel((int16_t)col, (int16_t)row, on);
  } else {
    bottom.setPixel((int16_t)col, (int16_t)(row - 8), on);
  }
}

static void show() {
  top.show();
  bottom.show();
}

static void blank() {
  top.clear();
  bottom.clear();
  show();
}

/** Is this cell part of the snake? */
static bool occupied(uint8_t cell) {
  for (uint8_t i = 0; i < length; i++) {
    uint8_t at = (uint8_t)((head + CELLS - i) % CELLS);
    if (body[at] == cell) {
      return true;
    }
  }
  return false;
}

/** Drop an apple somewhere the snake is not. */
static void placeApple() {
  for (uint8_t tries = 0; tries < 200; tries++) {
    uint8_t cell = nextRandom();
    if (!occupied(cell)) {
      apple = cell;
      plot(apple, true);
      return;
    }
  }
}

static void newGame() {
  blank();
  length = 3;
  head = 0;
  dx = 0;
  dy = 1;
  stepMs = SLOW_STEP;
  dead = false;
  // Three segments down the middle, heading down the field.
  for (uint8_t i = 0; i < length; i++) {
    body[i] = (uint8_t)(i * COLUMNS + 3);
    plot(body[i], true);
  }
  head = (uint8_t)(length - 1);
  placeApple();
  show();
  waiting = true;
  lastStep = millis();
}

/**
 * The stick as a heading, or no change when it is near the middle.
 *
 * The offsets come from mokxi_joystick.h rather than from a bare analogRead,
 * for two reasons an ATmega328P makes unavoidable: the first conversion after
 * the mux moves to another channel is the one to throw away, so reading two
 * axes in a row without that gives the second one the first one's answer; and
 * a stick does not spring back to exactly 512, so `begin()` samples the rest
 * position and everything here is measured from that. Without either, the
 * snake turns into a wall a second after the game starts, which is what this
 * sketch did until it did this.
 */
static void steer() {
  int16_t x = stick.dx();
  int16_t y = stick.dy();
  int8_t wantX = 0;
  int8_t wantY = 0;
  // Whichever axis is pushed further wins, so a diagonal is not ambiguous.
  int16_t ax = (int16_t)(x < 0 ? -x : x);
  int16_t ay = (int16_t)(y < 0 ? -y : y);
  if (ax > ay) {
    if (ax > THROW) {
      wantX = (int8_t)(x > 0 ? 1 : -1);
    }
  } else if (ay > THROW) {
    wantY = (int8_t)(y > 0 ? 1 : -1);
  }
  if (wantX == 0 && wantY == 0) {
    return;
  }
  // The first push is also what starts the game.
  waiting = false;
  // A turn into its own neck is refused: the snake cannot reverse.
  if (wantX != 0 && dx != 0) {
    return;
  }
  if (wantY != 0 && dy != 0) {
    return;
  }
  dx = wantX;
  dy = wantY;
}

void setup() {
  Serial.begin(115200);
  Serial.println("Mokxi Uno: snake on two MAX7219 matrices, steered with a thumb stick");
  stick.begin();
  top.begin(6);
  bottom.begin(6);
  // The seed is how long the player waits before the first push, which is the
  // only entropy a board with no clock battery has.
  newGame();
}

void loop() {
  steer();

  if (dead) {
    if (stick.pressed()) {
      lfsr ^= (uint16_t)millis();
      if (lfsr == 0u) {
        lfsr = 0xd00du;
      }
      newGame();
      while (stick.pressed()) {
        delay(8);
      }
    }
    delay(16);
    return;
  }

  if (waiting) {
    // Standing still. A press starts it too, for a player who would rather
    // keep the heading it was laid out with.
    if (stick.pressed()) {
      waiting = false;
      lastStep = millis();
      while (stick.pressed()) {
        delay(8);
      }
    }
    delay(8);
    return;
  }

  if ((uint32_t)(millis() - lastStep) < stepMs) {
    // Keep reading the stick between steps: a flick that begins and ends
    // inside one step would otherwise never be seen.
    delay(4);
    return;
  }
  lastStep = millis();

  uint8_t from = body[head];
  int8_t col = (int8_t)(from % COLUMNS);
  int8_t row = (int8_t)(from / COLUMNS);
  col = (int8_t)(col + dx);
  row = (int8_t)(row + dy);

  // The walls are walls, not a wrap: this is the version you can lose.
  if (col < 0 || col >= (int8_t)COLUMNS || row < 0 || row >= (int8_t)ROWS) {
    dead = true;
  }

  uint8_t cell = 0;
  if (!dead) {
    cell = (uint8_t)(row * COLUMNS + col);
    if (occupied(cell)) {
      dead = true;
    }
  }

  if (dead) {
    Serial.println("bitten");
    // Three flashes of the whole snake, then wait for the stick to be pressed.
    for (uint8_t f = 0; f < 3; f++) {
      for (uint8_t i = 0; i < length; i++) {
        plot(body[(uint8_t)((head + CELLS - i) % CELLS)], false);
      }
      show();
      delay(160);
      for (uint8_t i = 0; i < length; i++) {
        plot(body[(uint8_t)((head + CELLS - i) % CELLS)], true);
      }
      show();
      delay(160);
    }
    return;
  }

  bool ate = cell == apple;
  head = (uint8_t)((head + 1) % CELLS);
  body[head] = cell;
  plot(cell, true);

  if (ate) {
    if (length < CELLS - 1) {
      length++;
    }
    // Every apple makes it a little quicker, down to a floor.
    if (stepMs > FAST_STEP) {
      stepMs -= 8;
    }
    placeApple();
    Serial.println("apple");
  } else {
    // Rub out the tail: the segment that just fell off the end.
    uint8_t tail = (uint8_t)((head + CELLS - length) % CELLS);
    plot(body[tail], false);
  }
  show();
}

Parts list

5 parts, plus the jumper wires. Every one is in the editor's parts bin.

How it is wired

9 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.

  • Ground: Arduino Uno R3 pin GND; LED matrix, 8x8 (1) pin GND; LED matrix, 8x8 (2) pin GND; Thumb stick pin GND
  • Arduino Uno R3 pin 13; LED matrix, 8x8 (1) pin CLK; LED matrix, 8x8 (2) pin CLK
  • Arduino Uno R3 pin 11; LED matrix, 8x8 (1) pin DIN; LED matrix, 8x8 (2) pin DIN
  • Arduino Uno R3 pin 10; LED matrix, 8x8 (1) pin CS
  • Arduino Uno R3 pin 9; LED matrix, 8x8 (2) pin CS
  • Arduino Uno R3 pin 2; Thumb stick pin SW
  • Arduino Uno R3 pin 5V; LED matrix, 8x8 (1) pin VCC; LED matrix, 8x8 (2) pin VCC; Thumb stick pin VCC
  • Arduino Uno R3 pin A0; Thumb stick pin VRX
  • Arduino Uno R3 pin A1; Thumb stick pin VRY

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.