Собираем змейку на Arduino

Змей­ка — очень бла­го­дат­ная тема для про­грам­ми­стов, и вот поче­му:

  • понят­ные и про­стые пра­ви­ла,
  • мини­мум логи­ки, кото­рую нуж­но запро­грам­ми­ро­вать,
  • при­ми­тив­ная гра­фи­ка,
  • про­стое управ­ле­ние,
  • море кай­фа.

У нас уже был раз­бор того, как в неё игра­ет искус­ствен­ный интел­лект, и ста­тья о том, как за 10 минут сде­лать змей­ку для бра­у­зе­ра. Теперь шаг­нём даль­ше и не толь­ко напи­шем код, но и собе­рём желе­зо. Резуль­тат — ваша соб­ствен­ная змей­ка на мик­ро­про­цес­сор­ной пла­те, с экра­ном.

Что понадобится

Arduino Nano

https://aliexpress.ru/item/33011803738.html

Есть мно­го вари­ан­тов плат­фор­мы Adruino, но нам нуж­на одна из самых про­стых — мик­ро­схе­ма на пла­те с нож­ка­ми для мон­та­жа в спе­ци­аль­ную пла­ту. Обра­ти­те вни­ма­ние: при покуп­ке вам нуж­но ука­зать, что хоти­те пла­ту с уже при­па­ян­ны­ми нога­ми (soldered headers / soldered pins). Ина­че при­дёт­ся паять ноги на пла­ту, а смысл был в том, что­бы не паять.

Пласт­мас­со­вая пла­та для мон­та­жа (breadboard)

https://aliexpress.ru/item/32957396174.html
При­мер­но вот так мы соеди­ним меж­ду собой все эле­мен­ты на пла­те.

Све­то­ди­од­ная мат­ри­ца 8 × 8

https://aliexpress.ru/item/32888107767.html

По ней и будет бегать наша змей­ка. Мож­но, конеч­но, исполь­зо­вать ЖК-матрицы, что­бы кар­тин­ка была получ­ше, но они сто­ят доро­же и про­грам­ми­ро­вать их слож­нее.

Пере­мен­ный рези­стор (потен­цио­метр)

https://aliexpress.ru/item/32845927611.html

Обыч­ный пере­мен­ный рези­стор на 10 кОм. Суть этих устройств в том, что, вра­щая регу­ля­тор, мы можем изме­нять сопро­тив­ле­ние рези­сто­ра. В нашем про­ек­те он будет отве­чать за ско­рость дви­же­ния змей­ки. Если она дви­га­ет­ся слиш­ком быст­ро — кру­ти­те руч­ку и змей­ка замед­лит­ся.

Джой­стик

https://aliexpress.ru/item/33001603885.html

Рабо­та­ет так же, как на обыч­ной при­став­ке. Им мы будем управ­лять змей­кой.

Про­во­да

https://aliexpress.ru/item/32990573297.html

Самый кайф это­го про­ек­та в том, что нам не при­дёт­ся паять, что­бы собрать все моду­ли вме­сте. Вме­сто это­го исполь­зу­ем про­во­да с раз­ны­ми разъ­ёма­ми, поэто­му луч­ше сра­зу зака­зать пол­ный ком­плект.

Загружаем код игры

Мы сей­час не будем раз­би­рать осо­бен­но­сти про­грам­ми­ро­ва­ния под Arduino, а вме­сто это­го сра­зу дадим гото­вый скетч. Скетч — это про­грам­ма для Arduino, кото­рая напи­са­на на C++ и кото­рую сохра­ни­ли как файл с рас­ши­ре­ни­ем .ino.

КОД ИГРЫ

#include "LedControl.h" // LedControl library is used for controlling a LED matrix. Find it using Library Manager or download zip here: https://github.com/wayoda/LedControl
// --------------------------------------------------------------- //
// ------------------------- user config ------------------------- //
// --------------------------------------------------------------- //
// there are defined all the pins
struct Pin {
    static const short joystickX = A2;   // joystick X axis pin
    static const short joystickY = A3;   // joystick Y axis pin
    static const short joystickVCC = 15; // virtual VCC for the joystick (Analog 1) (to make the joystick connectable right next to the arduino nano)
    static const short joystickGND = 14; // virtual GND for the joystick (Analog 0) (to make the joystick connectable right next to the arduino nano)
    static const short potentiometer = A7; // potentiometer for snake speed control
    static const short CLK = 10;   // clock for LED matrix
    static const short CS  = 11; // chip-select for LED matrix
    static const short DIN = 12; // data-in for LED matrix
};
// LED matrix brightness: between 0(darkest) and 15(brightest)
const short intensity = 8;
// lower = faster message scrolling
const short messageSpeed = 5;
// initial snake length (1...63, recommended 3)
const short initialSnakeLength = 3;
void setup() {
    Serial.begin(115200);  // set the same baud rate on your Serial Monitor
    initialize();         // initialize pins & LED matrix
    calibrateJoystick(); // calibrate the joystick home (do not touch it)
    showSnakeMessage(); // scrolls the 'snake' message around the matrix
}
void loop() {
    generateFood();    // if there is no food, generate one
    scanJoystick();    // watches joystick movements & blinks with food
    calculateSnake();  // calculates snake parameters
    handleGameStates();
// uncomment this if you want the current game board to be printed to the serial (slows down the game a bit)
// dumpGameBoard();
}
// --------------------------------------------------------------- //
// -------------------- supporting variables --------------------- //
// --------------------------------------------------------------- //
LedControl matrix(Pin::DIN, Pin::CLK, Pin::CS, 1);
struct Point {
    int row = 0, col = 0;
    Point(int row = 0, int col = 0): row(row), col(col) {}
};
struct Coordinate {
    int x = 0, y = 0;
    Coordinate(int x = 0, int y = 0): x(x), y(y) {}
};
bool win = false;
bool gameOver = false;
// primary snake head coordinates (snake head), it will be randomly generated
Point snake;
// food is not anywhere yet
Point food(-1, -1);
// construct with default values in case the user turns off the calibration
Coordinate joystickHome(500, 500);
// snake parameters
int snakeLength = initialSnakeLength; // choosed by the user in the config section
int snakeSpeed = 1; // will be set according to potentiometer value, cannot be 0
int snakeDirection = 0; // if it is 0, the snake does not move
// direction constants
const short up     = 1;
const short right  = 2;
const short down   = 3; // 'down - 2' must be 'up'
const short left   = 4; // 'left - 2' must be 'right'
// threshold where movement of the joystick will be accepted
const int joystickThreshold = 160;
// artificial logarithmity (steepness) of the potentiometer (-1 = linear, 1 = natural, bigger = steeper (recommended 0...1))
const float logarithmity = 0.4;
// snake body segments storage
int gameboard[8][8] = {};
// --------------------------------------------------------------- //
// -------------------------- functions -------------------------- //
// --------------------------------------------------------------- //
// if there is no food, generate one, also check for victory
void generateFood() {
    if (food.row == -1 || food.col == -1) {
// self-explanatory
        if (snakeLength >= 64) {
            win = true;
            return; // prevent the food generator from running, in this case it would run forever, because it will not be able to find a pixel without a snake
        }
// generate food until it is in the right position
        do {
            food.col = random(8);
            food.row = random(8);
        } while (gameboard[food.row][food.col] > 0);
    }
}
// watches joystick movements & blinks with food
void scanJoystick() {
    int previousDirection = snakeDirection; // save the last direction
    long timestamp = millis();
    while (millis() < timestamp + snakeSpeed) {
// calculate snake speed exponentially (10...1000ms)
        float raw = mapf(analogRead(Pin::potentiometer), 0, 1023, 0, 1);
        snakeSpeed = mapf(pow(raw, 3.5), 0, 1, 10, 1000); // change the speed exponentially
        if (snakeSpeed == 0) snakeSpeed = 1; // safety: speed can not be 0
// determine the direction of the snake
        analogRead(Pin::joystickY) < joystickHome.y - joystickThreshold ? snakeDirection = up    : 0;
        analogRead(Pin::joystickY) > joystickHome.y + joystickThreshold ? snakeDirection = down  : 0;
        analogRead(Pin::joystickX) < joystickHome.x - joystickThreshold ? snakeDirection = left  : 0;
        analogRead(Pin::joystickX) > joystickHome.x + joystickThreshold ? snakeDirection = right : 0;
// ignore directional change by 180 degrees (no effect for non-moving snake)
        snakeDirection + 2 == previousDirection && previousDirection != 0 ? snakeDirection = previousDirection : 0;
        snakeDirection - 2 == previousDirection && previousDirection != 0 ? snakeDirection = previousDirection : 0;
// intelligently blink with the food
        matrix.setLed(0, food.row, food.col, millis() % 100 < 50 ? 1 : 0);
    }
}
// calculate snake movement data
void calculateSnake() {
    switch (snakeDirection) {
    case up:
        snake.row--;
        fixEdge();
        matrix.setLed(0, snake.row, snake.col, 1);
        break;
    case right:
        snake.col++;
        fixEdge();
        matrix.setLed(0, snake.row, snake.col, 1);
        break;
    case down:
        snake.row++;
        fixEdge();
        matrix.setLed(0, snake.row, snake.col, 1);
        break;
    case left:
        snake.col--;
        fixEdge();
        matrix.setLed(0, snake.row, snake.col, 1);
        break;
    default: // if the snake is not moving, exit
        return;
    }
// if there is a snake body segment, this will cause the end of the game (snake must be moving)
    if (gameboard[snake.row][snake.col] > 1 && snakeDirection != 0) {
        gameOver = true;
        return;
    }
// check if the food was eaten
    if (snake.row == food.row && snake.col == food.col) {
        food.row = -1; // reset food
        food.col = -1;
// increment snake length
        snakeLength++;
// increment all the snake body segments
        for (int row = 0; row < 8; row++) {
            for (int col = 0; col < 8; col++) {
                if (gameboard[row][col] > 0 ) {
                    gameboard[row][col]++;
                }
            }
        }
    }
// add new segment at the snake head location
    gameboard[snake.row][snake.col] = snakeLength + 1; // will be decremented in a moment
// decrement all the snake body segments, if segment is 0, turn the corresponding led off
    for (int row = 0; row < 8; row++) {
        for (int col = 0; col < 8; col++) {
// if there is a body segment, decrement it's value
            if (gameboard[row][col] > 0 ) {
                gameboard[row][col]--;
            }
// display the current pixel
            matrix.setLed(0, row, col, gameboard[row][col] == 0 ? 0 : 1);
        }
    }
}
// causes the snake to appear on the other side of the screen if it gets out of the edge
void fixEdge() {
    snake.col < 0 ? snake.col += 8 : 0;
    snake.col > 7 ? snake.col -= 8 : 0;
    snake.row < 0 ? snake.row += 8 : 0;
    snake.row > 7 ? snake.row -= 8 : 0;
}
void handleGameStates() {
    if (gameOver || win) {
        unrollSnake();
        showScoreMessage(snakeLength - initialSnakeLength);
        if (gameOver) showGameOverMessage();
        else if (win) showWinMessage();
// re-init the game
        win = false;
        gameOver = false;
        snake.row = random(8);
        snake.col = random(8);
        food.row = -1;
        food.col = -1;
        snakeLength = initialSnakeLength;
        snakeDirection = 0;
        memset(gameboard, 0, sizeof(gameboard[0][0]) * 8 * 8);
        matrix.clearDisplay(0);
    }
}
void unrollSnake() {
// switch off the food LED
    matrix.setLed(0, food.row, food.col, 0);
    delay(800);
// flash the screen 5 times
    for (int i = 0; i < 5; i++) {
// invert the screen
        for (int row = 0; row < 8; row++) {
            for (int col = 0; col < 8; col++) {
                matrix.setLed(0, row, col, gameboard[row][col] == 0 ? 1 : 0);
            }
        }
        delay(20);
// invert it back
        for (int row = 0; row < 8; row++) {
            for (int col = 0; col < 8; col++) {
                matrix.setLed(0, row, col, gameboard[row][col] == 0 ? 0 : 1);
            }
        }
        delay(50);
    }
    delay(600);
    for (int i = 1; i <= snakeLength; i++) {
        for (int row = 0; row < 8; row++) {
            for (int col = 0; col < 8; col++) {
                if (gameboard[row][col] == i) {
                    matrix.setLed(0, row, col, 0);
                    delay(100);
                }
            }
        }
    }
}
// calibrate the joystick home for 10 times
void calibrateJoystick() {
    Coordinate values;
    for (int i = 0; i < 10; i++) {
        values.x += analogRead(Pin::joystickX);
        values.y += analogRead(Pin::joystickY);
    }
    joystickHome.x = values.x / 10;
    joystickHome.y = values.y / 10;
}
void initialize() {
    pinMode(Pin::joystickVCC, OUTPUT);
    digitalWrite(Pin::joystickVCC, HIGH);
    pinMode(Pin::joystickGND, OUTPUT);
    digitalWrite(Pin::joystickGND, LOW);
    matrix.shutdown(0, false);
    matrix.setIntensity(0, intensity);
    matrix.clearDisplay(0);
    randomSeed(analogRead(A5));
    snake.row = random(8);
    snake.col = random(8);
}
void dumpGameBoard() {
    String buff = "\n\n\n";
    for (int row = 0; row < 8; row++) {
        for (int col = 0; col < 8; col++) {
            if (gameboard[row][col] < 10) buff += " ";
            if (gameboard[row][col] != 0) buff += gameboard[row][col];
            else if (col == food.col && row == food.row) buff += "@";
            else buff += "-";
            buff += " ";
        }
        buff += "\n";
    }
    Serial.println(buff);
}
// --------------------------------------------------------------- //
// -------------------------- messages --------------------------- //
// --------------------------------------------------------------- //
const PROGMEM bool snakeMessage[8][56] = {
    {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, 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, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 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, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 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, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 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, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 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, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 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, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}
};
const PROGMEM bool gameOverMessage[8][90] = {
    {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, 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, 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, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 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, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}
};
const PROGMEM bool scoreMessage[8][58] = {
    {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, 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, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 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, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 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, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 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, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
};
const PROGMEM bool digits[][8][8] = {
    {
        {0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 1, 1, 1, 1, 0, 0},
        {0, 1, 1, 0, 0, 1, 1, 0},
        {0, 1, 1, 0, 1, 1, 1, 0},
        {0, 1, 1, 1, 0, 1, 1, 0},
        {0, 1, 1, 0, 0, 1, 1, 0},
        {0, 1, 1, 0, 0, 1, 1, 0},
        {0, 0, 1, 1, 1, 1, 0, 0}
    },
    {
        {0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 0, 1, 1, 0, 0, 0},
        {0, 0, 0, 1, 1, 0, 0, 0},
        {0, 0, 1, 1, 1, 0, 0, 0},
        {0, 0, 0, 1, 1, 0, 0, 0},
        {0, 0, 0, 1, 1, 0, 0, 0},
        {0, 0, 0, 1, 1, 0, 0, 0},
        {0, 1, 1, 1, 1, 1, 1, 0}
    },
    {
        {0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 1, 1, 1, 1, 0, 0},
        {0, 1, 1, 0, 0, 1, 1, 0},
        {0, 0, 0, 0, 0, 1, 1, 0},
        {0, 0, 0, 0, 1, 1, 0, 0},
        {0, 0, 1, 1, 0, 0, 0, 0},
        {0, 1, 1, 0, 0, 0, 0, 0},
        {0, 1, 1, 1, 1, 1, 1, 0}
    },
    {
        {0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 1, 1, 1, 1, 0, 0},
        {0, 1, 1, 0, 0, 1, 1, 0},
        {0, 0, 0, 0, 0, 1, 1, 0},
        {0, 0, 0, 1, 1, 1, 0, 0},
        {0, 0, 0, 0, 0, 1, 1, 0},
        {0, 1, 1, 0, 0, 1, 1, 0},
        {0, 0, 1, 1, 1, 1, 0, 0}
    },
    {
        {0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 0, 0, 1, 1, 0, 0},
        {0, 0, 0, 1, 1, 1, 0, 0},
        {0, 0, 1, 0, 1, 1, 0, 0},
        {0, 1, 0, 0, 1, 1, 0, 0},
        {0, 1, 1, 1, 1, 1, 1, 0},
        {0, 0, 0, 0, 1, 1, 0, 0},
        {0, 0, 0, 0, 1, 1, 0, 0}
    },
    {
        {0, 0, 0, 0, 0, 0, 0, 0},
        {0, 1, 1, 1, 1, 1, 1, 0},
        {0, 1, 1, 0, 0, 0, 0, 0},
        {0, 1, 1, 1, 1, 1, 0, 0},
        {0, 0, 0, 0, 0, 1, 1, 0},
        {0, 0, 0, 0, 0, 1, 1, 0},
        {0, 1, 1, 0, 0, 1, 1, 0},
        {0, 0, 1, 1, 1, 1, 0, 0}
    },
    {
        {0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 1, 1, 1, 1, 0, 0},
        {0, 1, 1, 0, 0, 1, 1, 0},
        {0, 1, 1, 0, 0, 0, 0, 0},
        {0, 1, 1, 1, 1, 1, 0, 0},
        {0, 1, 1, 0, 0, 1, 1, 0},
        {0, 1, 1, 0, 0, 1, 1, 0},
        {0, 0, 1, 1, 1, 1, 0, 0}
    },
    {
        {0, 0, 0, 0, 0, 0, 0, 0},
        {0, 1, 1, 1, 1, 1, 1, 0},
        {0, 1, 1, 0, 0, 1, 1, 0},
        {0, 0, 0, 0, 1, 1, 0, 0},
        {0, 0, 0, 0, 1, 1, 0, 0},
        {0, 0, 0, 1, 1, 0, 0, 0},
        {0, 0, 0, 1, 1, 0, 0, 0},
        {0, 0, 0, 1, 1, 0, 0, 0}
    },
    {
        {0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 1, 1, 1, 1, 0, 0},
        {0, 1, 1, 0, 0, 1, 1, 0},
        {0, 1, 1, 0, 0, 1, 1, 0},
        {0, 0, 1, 1, 1, 1, 0, 0},
        {0, 1, 1, 0, 0, 1, 1, 0},
        {0, 1, 1, 0, 0, 1, 1, 0},
        {0, 0, 1, 1, 1, 1, 0, 0}
    },
    {
        {0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 1, 1, 1, 1, 0, 0},
        {0, 1, 1, 0, 0, 1, 1, 0},
        {0, 1, 1, 0, 0, 1, 1, 0},
        {0, 0, 1, 1, 1, 1, 1, 0},
        {0, 0, 0, 0, 0, 1, 1, 0},
        {0, 1, 1, 0, 0, 1, 1, 0},
        {0, 0, 1, 1, 1, 1, 0, 0}
    }
};
// scrolls the 'snake' message around the matrix
void showSnakeMessage() {
    [&] {
        for (int d = 0; d < sizeof(snakeMessage[0]) - 7; d++) {
            for (int col = 0; col < 8; col++) {
                delay(messageSpeed);
                for (int row = 0; row < 8; row++) {
// this reads the byte from the PROGMEM and displays it on the screen
                    matrix.setLed(0, row, col, pgm_read_byte(&(snakeMessage[row][col + d])));
                }
            }
// if the joystick is moved, exit the message
            if (analogRead(Pin::joystickY) < joystickHome.y - joystickThreshold
                    || analogRead(Pin::joystickY) > joystickHome.y + joystickThreshold
                    || analogRead(Pin::joystickX) < joystickHome.x - joystickThreshold
                    || analogRead(Pin::joystickX) > joystickHome.x + joystickThreshold) {
                return; // return the lambda function
            }
        }
    }();
    matrix.clearDisplay(0);
// wait for joystick co come back
    while (analogRead(Pin::joystickY) < joystickHome.y - joystickThreshold
            || analogRead(Pin::joystickY) > joystickHome.y + joystickThreshold
            || analogRead(Pin::joystickX) < joystickHome.x - joystickThreshold
            || analogRead(Pin::joystickX) > joystickHome.x + joystickThreshold) {}
}
// scrolls the 'game over' message around the matrix
void showGameOverMessage() {
    [&] {
        for (int d = 0; d < sizeof(gameOverMessage[0]) - 7; d++) {
            for (int col = 0; col < 8; col++) {
                delay(messageSpeed);
                for (int row = 0; row < 8; row++) {
// this reads the byte from the PROGMEM and displays it on the screen
                    matrix.setLed(0, row, col, pgm_read_byte(&(gameOverMessage[row][col + d])));
                }
            }
// if the joystick is moved, exit the message
            if (analogRead(Pin::joystickY) < joystickHome.y - joystickThreshold
                    || analogRead(Pin::joystickY) > joystickHome.y + joystickThreshold
                    || analogRead(Pin::joystickX) < joystickHome.x - joystickThreshold
                    || analogRead(Pin::joystickX) > joystickHome.x + joystickThreshold) {
                return; // return the lambda function
            }
        }
    }();
    matrix.clearDisplay(0);
// wait for joystick co come back
    while (analogRead(Pin::joystickY) < joystickHome.y - joystickThreshold
            || analogRead(Pin::joystickY) > joystickHome.y + joystickThreshold
            || analogRead(Pin::joystickX) < joystickHome.x - joystickThreshold
            || analogRead(Pin::joystickX) > joystickHome.x + joystickThreshold) {}
}
// scrolls the 'win' message around the matrix
void showWinMessage() {
// not implemented yet // TODO: implement it
}
// scrolls the 'score' message with numbers around the matrix
void showScoreMessage(int score) {
    if (score < 0 || score > 99) return;
// specify score digits
    int second = score % 10;
    int first = (score / 10) % 10;
    [&] {
        for (int d = 0; d < sizeof(scoreMessage[0]) + 2 * sizeof(digits[0][0]); d++) {
            for (int col = 0; col < 8; col++) {
                delay(messageSpeed);
                for (int row = 0; row < 8; row++) {
                    if (d <= sizeof(scoreMessage[0]) - 8) {
                        matrix.setLed(0, row, col, pgm_read_byte(&(scoreMessage[row][col + d])));
                    }
                    int c = col + d - sizeof(scoreMessage[0]) + 6; // move 6 px in front of the previous message
// if the score is < 10, shift out the first digit (zero)
                    if (score < 10) c += 8;
                    if (c >= 0 && c < 8) {
                        if (first > 0) matrix.setLed(0, row, col, pgm_read_byte(&(digits[first][row][c]))); // show only if score is >= 10 (see above)
                    } else {
                        c -= 8;
                        if (c >= 0 && c < 8) {
                            matrix.setLed(0, row, col, pgm_read_byte(&(digits[second][row][c]))); // show always
                        }
                    }
                }
            }
// if the joystick is moved, exit the message
            if (analogRead(Pin::joystickY) < joystickHome.y - joystickThreshold
                    || analogRead(Pin::joystickY) > joystickHome.y + joystickThreshold
                    || analogRead(Pin::joystickX) < joystickHome.x - joystickThreshold
                    || analogRead(Pin::joystickX) > joystickHome.x + joystickThreshold) {
                return; // return the lambda function
            }
        }
    }();
    matrix.clearDisplay(0);
// // wait for joystick co come back
// while (analogRead(Pin::joystickY) < joystickHome.y - joystickThreshold
//         || analogRead(Pin::joystickY) > joystickHome.y + joystickThreshold
//         || analogRead(Pin::joystickX) < joystickHome.x - joystickThreshold
//         || analogRead(Pin::joystickX) > joystickHome.x + joystickThreshold) {}
}
// standard map function, but with floats
float mapf(float x, float in_min, float in_max, float out_min, float out_max) {
    return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

Что­бы загру­зить код игры в мик­ро­кон­трол­лер, вам пона­до­бит­ся:

  1. Сохра­нить про­грам­му в виде .ino-файла.
  2. Под­клю­чить пла­ту к ком­пью­те­ру с помо­щью USB-кабеля.
  3. Уста­но­вить Arduino IDE — сре­ду раз­ра­бот­ки под Arduino для ком­пью­те­ра.
  4. Настро­ить ком­пью­тер для рабо­ты с пла­той. Нуж­но поста­вить драй­ве­ры для кон­трол­ле­ра и убе­дить­ся, что ком­пью­тер всё рас­по­знал вер­но. Что­бы понять, как это пра­виль­но сде­лать, поищи­те в интер­не­те «Как под­клю­чить arduino nano к ком­пью­те­ру» или почи­тай­те эту ста­тью (не нашу).
  5. Открыть в Arduino IDE наш файл с про­грам­мой.
  6. Там же нажать кноп­ку «Загру­зить».
  7. Подо­ждать сооб­ще­ния о том, что про­грам­ма загру­же­на.

Та самая кноп­ка «Загру­зить» в Arduino IDE.

Собираем и запускаем

Глав­ное на этом эта­пе — не спе­шить и делать всё по кар­тин­ке. Что­бы под­клю­чить про­во­да к Arduino, нуж­но вста­вить его в центр белой пла­сти­ко­вой мон­таж­ной пла­ты и под­клю­чить нуж­ные про­во­да рядом с нуж­ны­ми вхо­да­ми. «Рядом» — то есть на том же, гру­бо гово­ря, столб­це: дыр­ки на мон­таж­ной пла­те соеди­не­ны как бы попе­рёк пла­ты, то есть все про­во­да, кото­рые вы воткнё­те в один стол­бец, будут соеди­не­ны.

Если всё рав­но непо­нят­но, что и куда под­клю­чать, посмот­ри­те видео:

Что­бы игра зара­бо­та­ла после сбор­ки, доста­точ­но под­клю­чить пла­ту к любо­му бло­ку пита­ния или ком­пью­те­ру.

Железо — не главное.
Глав­ное — уметь напи­сать код, что­бы это желе­зо зара­бо­та­ло. При­хо­ди­те в Прак­ти­кум, научим все­му, что нуж­но любо­му про­грам­ми­сту.