Built-in project · Arduino Uno R3

Snake on an Arduino Uno

Snake on a 240x320 TFT with a thumb stick: eat the apple, grow, and repaint only the squares that moved. 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 · 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: a 2.4 inch ILI9341, a thumb stick and a piezo, on an Arduino Uno.
//
// Wiring, panel to board:
//   CS   -> pin 10      SCK  -> pin 13
//   DC   -> pin 9       MOSI -> pin 11
//   RST  -> 5 V         LED  -> 5 V      VCC -> 5 V, GND -> GND
// Thumb stick:
//   VRX  -> A0          VRY  -> A1       SW  -> pin 2, VCC -> 5 V, GND -> GND
// Piezo:
//   one leg -> pin 6, the other -> GND
//
// Push the stick to turn. Eat the apple, grow by one, and do not run into the
// wall or into yourself. Press the stick to start the next game.
//
// # Why the board is small squares
//
// There is no SPI peripheral on an ATmega328P in Mokxi (docs/uno.md section 3),
// so every one of the sixteen bits of a pixel is a pin change made by hand and
// the board manages some fifty thousand pixels a second. The `pong` example
// answers that by playing in a window; this one answers it by never repainting
// anything. A step of the snake is two squares (the new head and the old
// tail), which is 2 x 12 x 12 pixels, under three hundred, and the rest of
// the picture is left exactly as it was. That is a frame in six milliseconds,
// and the game runs at whatever speed the rules want rather than whatever
// speed the panel allows.
//
// The body is a ring buffer of cell numbers rather than coordinates: one byte
// a segment, so a hundred and twenty segments cost 120 of the Uno's 2048 bytes
// of SRAM. The head and the length are the only state a step changes.

#include "mokxi_ili9341.h"
#include "mokxi_joystick.h"
#include "mokxi_tone.h"

const uint8_t TFT_CS = 10;
const uint8_t TFT_DC = 9;
const uint8_t TFT_SCK = 13;
const uint8_t TFT_MOSI = 11;
const uint8_t PIEZO_PIN = 6;

SoftSpi spi(TFT_SCK, TFT_MOSI, TFT_CS);
Ili9341 tft(spi, TFT_DC);
Joystick stick(A0, A1, 2);

// The board, in cells. 16 x 20 of 12 pixels is 192 x 240 on the panel.
const uint8_t COLS = 16;
const uint8_t ROWS = 20;
const int16_t CELL = 12;
const int16_t OX = 24;
const int16_t OY = 56;

const uint16_t BG = 0x0841;
const uint16_t EDGE = ILI9341_CYAN;
const uint16_t SNAKE = ILI9341_GREEN;
const uint16_t HEAD = 0x07ff;
const uint16_t APPLE = ILI9341_RED;

const uint8_t MAX_LEN = 120;
const int STEP_MS = 150;

uint8_t body[MAX_LEN];  // cell numbers, oldest at `tail`
uint8_t head, tail, length;
int8_t dx, dy;
uint8_t apple;
int score;
bool playing;

static uint8_t cellOf(uint8_t cx, uint8_t cy) { return (uint8_t)(cy * COLS + cx); }
static uint8_t colOf(uint8_t cell) { return (uint8_t)(cell % COLS); }
static uint8_t rowOf(uint8_t cell) { return (uint8_t)(cell / COLS); }

static void paint(uint8_t cell, uint16_t colour) {
  tft.fillRect((int16_t)(OX + colOf(cell) * CELL), (int16_t)(OY + rowOf(cell) * CELL), CELL, CELL,
               colour);
}

static bool onSnake(uint8_t cell) {
  for (uint8_t i = 0; i < length; i++) {
    uint8_t at = (uint8_t)((tail + i) % MAX_LEN);
    if (body[at] == cell) {
      return true;
    }
  }
  return false;
}

// A cheap xorshift rather than the runtime's random(): the apple only has to
// land somewhere unpredictable, and this example is already most of a quarter
// of the Uno's flash.
static uint16_t seed = 0xace1;

static uint8_t nextCell() {
  seed ^= (uint16_t)(seed << 7);
  seed ^= (uint16_t)(seed >> 9);
  seed ^= (uint16_t)(seed << 8);
  return (uint8_t)(seed % (COLS * ROWS));
}

static void dropApple() {
  for (int tries = 0; tries < 200; tries++) {
    uint8_t cell = nextCell();
    if (!onSnake(cell)) {
      apple = cell;
      paint(apple, APPLE);
      return;
    }
  }
}

// The border, and nothing written on the panel: the 5x7 font costs about four
// hundred bytes of flash and this sketch is already a quarter of the Uno's.
// The border is the thing that changes colour when the game ends, and the
// serial monitor carries the words.
static void border(uint16_t colour) {
  tft.rect(OX - 2, OY - 2, (int16_t)(COLS * CELL + 4), (int16_t)(ROWS * CELL + 4), colour, 2);
}

static void drawFurniture() {
  tft.fillScreen(ILI9341_BLACK);
  border(EDGE);
}

// The score is a row of pips under the board rather than a number on the
// panel: the digit formatter is four hundred bytes of flash this sketch does
// not have, and the serial monitor prints the figure anyway.
static void drawScore() {
  int16_t y = (int16_t)(OY + ROWS * CELL + 10);
  tft.fillRect(OX, y, (int16_t)(COLS * CELL), 8, ILI9341_BLACK);
  int pips = score > COLS * 2 ? COLS * 2 : score;
  for (int i = 0; i < pips; i++) {
    tft.fillRect((int16_t)(OX + (i % COLS) * CELL), (int16_t)(y + (i / COLS) * 5), 8, 4,
                 ILI9341_YELLOW);
  }
}

static void startGame() {
  border(EDGE);
  tft.fillRect(OX, OY, (int16_t)(COLS * CELL), (int16_t)(ROWS * CELL), BG);
  length = 3;
  tail = 0;
  head = 2;
  for (uint8_t i = 0; i < length; i++) {
    body[i] = cellOf((uint8_t)(4 + i), (uint8_t)(ROWS / 2));
    paint(body[i], i + 1 == length ? HEAD : SNAKE);
  }
  dx = 1;
  dy = 0;
  score = 0;
  playing = true;
  drawScore();
  dropApple();
}

static void gameOver() {
  playing = false;
  mokxiTone(PIEZO_PIN, 220, 120);
  mokxiTone(PIEZO_PIN, 160, 220);
  border(ILI9341_RED);
  Serial.print("game over, score ");
  Serial.println(score);
}

// Read the stick and turn, refusing a reversal onto the snake's own neck.
static void steer() {
  int8_t sx = stick.stepX();
  int8_t sy = stick.stepY();
  if (sx != 0 && dx == 0) {
    dx = sx;
    dy = 0;
  } else if (sy != 0 && dy == 0) {
    dy = sy;
    dx = 0;
  }
}

static void step() {
  uint8_t from = body[head];
  int8_t cx = (int8_t)(colOf(from) + dx);
  int8_t cy = (int8_t)(rowOf(from) + dy);
  if (cx < 0 || cy < 0 || cx >= (int8_t)COLS || cy >= (int8_t)ROWS) {
    gameOver();
    return;
  }

  uint8_t next = cellOf((uint8_t)cx, (uint8_t)cy);
  bool eating = next == apple;
  // The tail moves out of the way this step, so running onto it is allowed.
  if (onSnake(next) && !(next == body[tail] && !eating)) {
    gameOver();
    return;
  }

  paint(from, SNAKE);
  head = (uint8_t)((head + 1) % MAX_LEN);
  body[head] = next;
  paint(next, HEAD);

  if (eating) {
    score++;
    drawScore();
    mokxiTone(PIEZO_PIN, NOTE_C6, 30);
    if (length < MAX_LEN - 1) {
      length++;
    }
    dropApple();
  } else {
    paint(body[tail], BG);
    tail = (uint8_t)((tail + 1) % MAX_LEN);
  }
}

void setup() {
  Serial.begin(115200);
  Serial.println("Mokxi Uno: snake on an ILI9341");
  Serial.println("push the stick to turn, press it to start again");
  stick.begin();
  mokxiNoTone(PIEZO_PIN);
  tft.begin(0);
  drawFurniture();
  startGame();
}

void loop() {
  if (!playing) {
    if (stick.clicked()) {
      startGame();
    }
    delay(20);
    return;
  }

  // Steer all the way through the step so a flick is never missed.
  unsigned long start = millis();
  while (millis() - start < (unsigned long)STEP_MS) {
    steer();
    delay(5);
  }
  step();
}

Parts list

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

How it is wired

10 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.

  • Arduino Uno R3 pin 13; TFT display, 240x320 pin SCK
  • Arduino Uno R3 pin 11; TFT display, 240x320 pin MOSI
  • Arduino Uno R3 pin 10; TFT display, 240x320 pin CS
  • Arduino Uno R3 pin 9; TFT display, 240x320 pin DC
  • Arduino Uno R3 pin 6; Piezo speaker pin 1
  • Arduino Uno R3 pin 2; Thumb stick pin SW
  • Arduino Uno R3 pin 5V; TFT display, 240x320 pin VCC; TFT display, 240x320 pin RST; TFT display, 240x320 pin LED; Thumb stick pin VCC
  • Ground: Arduino Uno R3 pin GND; TFT display, 240x320 pin GND; Thumb stick pin GND; Piezo speaker pin 2
  • 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.