#include <SPI.h> // Подключаем библиотеку SPI для обмена данными между устройствами.
#include <Wire.h> // Подключаем библиотеку Wire для работы с интерфейсом I2C.
#include <Adafruit_GFX.h> // Подключаем библиотеку Adafruit GFX для работы с графикой.
#include <Adafruit_SSD1306.h> // Подключаем библиотеку Adafruit SSD1306 для работы с OLED-дисплеями.
#define SCREEN_WIDTH 128 // Устанавливаем ширину экрана.
#define SCREEN_HEIGHT 64 // Устанавливаем высоту экрана.
#define OLED_RESET 4 // Определяем пин для сброса дисплея.
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); // Создаем объект для работы с дисплеем.
const byte buttonPins[] = {4, 2, 5, 3}; // Массив пинов кнопок.
// Определение возможных состояний игры.
typedef enum {
START, // Начальное состояние.
RUNNING, // Игра идет.
GAMEOVER // Конец игры.
} State;
// Определение направлений движения змеи.
typedef enum {
LEFT, // Влево.
UP, // Вверх.
RIGHT, // Вправо.
DOWN // Вниз.
} Direction;
#define SNAKE_PIECE_SIZE 3 // Размер одного сегмента змеи.
#define MAX_SANKE_LENGTH 165 // Максимальная длина змеи.
#define MAP_SIZE_X 20 // Ширина игрового поля.
#define MAP_SIZE_Y 20 // Высота игрового поля.
#define STARTING_SNAKE_SIZE 5 // Начальная длина змеи.
#define SNAKE_MOVE_DELAY 30 // Задержка между движениями змеи.
State gameState; // Текущее состояние игры.
int8_t snake[MAX_SANKE_LENGTH][2]; // Массив координат сегментов змеи.
uint8_t snake_length; // Длина змеи.
Direction dir; // Направление движения змеи.
Direction newDir; // Новое направление движения змеи.
int8_t fruit[2]; // Координаты фрукта.
void setup() {
Serial.begin(9600); // Инициализируем последовательный порт.
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3D)) { // Проверяем успешность инициализации дисплея.
Serial.println(F("SSD1306 allocation failed")); // Выводим сообщение об ошибке.
for (;;) ; // Зависание программы в случае ошибки.
}
for (byte i = 0; i < 4; i++) { // Проходимся по всем пинам кнопок.
pinMode(buttonPins[i], INPUT_PULLUP); // Настраиваем их как входы с подтягивающим резистором.
}
randomSeed(analogRead(A0)); // Инициализируем генератор случайных чисел.
setupGame(); // Вызываем функцию настройки игры.
}
void setupGame() {
gameState = START; // Устанавливаем начальное состояние игры.
dir = RIGHT; // Устанавливаем начальное направление движения вправо.
newDir = RIGHT; // Устанавливаем новое направление движения вправо.
resetSnake(); // Сбрасываем змею.
generateFruit(); // Генерируем фрукт.
display.clearDisplay(); // Очищаем дисплей.
drawMap(); // Рисуем игровое поле.
drawScore(); // Рисуем счет.
drawPressToStart(); // Показываем сообщение "Нажмите кнопку для начала".
display.display(); // Обновляем дисплей.
}
void resetSnake() {
snake_length = STARTING_SNAKE_SIZE; // Устанавливаем длину змеи равной начальной длине.
for (int i = 0; i < snake_length; i++) { // Проходимся по сегментам змеи.
snake[i][0] = MAP_SIZE_X / 2 - i; // Устанавливаем координату X.
snake[i][1] = MAP_SIZE_Y / 2; // Устанавливаем координату Y.
}
}
int moveTime = 0; // Время последнего перемещения змеи.
void loop() {
switch (gameState) { // В зависимости от состояния игры выполняем разные действия.
case START: // Если игра еще не началась.
if (buttonPress()) gameState = RUNNING; // Если нажата кнопка, начинаем игру.
break;
case RUNNING: // Если игра идет.
moveTime++; // Увеличиваем время ожидания следующего хода.
readDirection(); // Читаем направление движения.
if (moveTime >= SNAKE_MOVE_DELAY) { // Если прошло достаточно времени.
dir = newDir; // Меняем направление движения змеи.
display.clearDisplay(); // Очищаем дисплей.
if (moveSnake()) { // Делаем ход змеей.
gameState = GAMEOVER; // Если произошла ошибка, заканчиваем игру.
drawGameover(); // Рисуем сообщение "Игра окончена".
delay(1000); // Ждем одну секунду.
}
drawMap(); // Рисуем игровое поле.
drawScore(); // Рисуем счет.
display.display(); // Обновляем дисплей.
checkFruit(); // Проверяем, съела ли змея фрукт.
moveTime = 0; // Сбрасываем таймер.
}
break;
case GAMEOVER: // Если игра закончилась.
if (buttonPress()) { // Если нажата кнопка.
delay(500); // Ждем полсекунды.
setupGame(); // Начинаем новую игру.
gameState = START; // Переходим в начальное состояние.
}
break;
}
delay(10); // Небольшая задержка для снижения нагрузки на процессор.
}
bool buttonPress() {
for (byte i = 0; i < 4; i++) { // Проходимся по всем кнопкам.
byte buttonPin = buttonPins[i]; // Получаем номер пина кнопки.
if (digitalRead(buttonPin) == LOW) { // Если кнопка нажата.
return true; // Возвращаем истину.
}
}
return false; // Если ни одна кнопка не нажата, возвращаем ложь.
}
void readDirection() {
for (byte i = 0; i < 4; i++) { // Проходимся по всем кнопкам.
byte buttonPin = buttonPins[i]; // Получаем номер пина кнопки.
if (digitalRead(buttonPin) == LOW && i != ((int)dir + 2) % 4) { // Если кнопка нажата и это не противоположное направление.
newDir = (Direction)i; // Устанавливаем новое направление движения.
return; // Выходим из функции.
}
}
}
bool moveSnake() {
int8_t x = snake[0][0]; // Получаем текущую координату X головы змеи.
int8_t y = snake[0][1]; // Получаем текущую координату Y головы змеи.
switch (dir) { // В зависимости от направления двигаемся в соответствующую сторону.
case LEFT:
x -= 1; // Двигаемся влево.
break;
case UP:
y -= 1; // Двигаемся вверх.
break;
case RIGHT:
x += 1; // Двигаемся вправо.
break;
case DOWN:
y += 1; // Двигаемся вниз.
break;
}
if (collisionCheck(x, y)) // Если произошло столкновение.
return true; // Возвращаем истину.
for (int i = snake_length - 1; i > 0; i--) { // Проходимся по сегментам змеи начиная с хвоста.
snake[i][0] = snake[i - 1][0]; // Передвигаем каждый сегмент на место предыдущего.
snake[i][1] = snake[i - 1][1];
}
snake[0][0] = x; // Устанавливаем новые координаты головы змеи.
snake[0][1] = y;
return false; // Возвращаем ложь, так как движение успешно выполнено.
}
void checkFruit() {
if (fruit[0] == snake[0][0] && fruit[1] == snake[0][1]) // Если змея съела фрукт.
{
if (snake_length + 1 <= MAX_SANKE_LENGTH) // Если змея может стать длиннее.
snake_length++; // Удлиняем змею.
generateFruit(); // Генерируем новый фрукт.
}
}
void generateFruit() {
bool b = false; // Флаг, указывающий на то, что фрукт находится на месте змеи.
do {
b = false; // Сбрасываем флаг.
fruit[0] = random(0, MAP_SIZE_X); // Генерируем случайную координату X.
fruit[1] = random(0, MAP_SIZE_Y); // Генерируем случайную координату Y.
for (int i = 0; i < snake_length; i++) { // Проходимся по всем сегментам змеи.
if (fruit[0] == snake[i][0] && fruit[1] == snake[i][1]) { // Если фрукт оказался на месте змеи.
b = true; // Устанавливаем флаг.
continue;
}
}
} while(b);
}
bool collisionCheck(int8_t x, int8_t y) {
for(int i = 1; i < snake_length; i++) {
if(x == snake[i][0] && y == snake[i][1]) return true;
}
if(x < 0 || y < 0 || x >= MAP_SIZE_X || y >= MAP_SIZE_Y) return true;
return false;
}
void drawMap() {
int offsetMapX = SCREEN_WIDTH - SNAKE_PIECE_SIZE * MAP_SIZE_X - 2;
int offsetMapY = 2;
display.drawRect(fruit[0] * SNAKE_PIECE_SIZE + offsetMapX, fruit[1] * SNAKE_PIECE_SIZE + offsetMapY, SNAKE_PIECE_SIZE, SNAKE_PIECE_SIZE, SSD1306_INVERSE);
display.drawRect(offsetMapX - 2, 0, SNAKE_PIECE_SIZE * MAP_SIZE_X + 4, SNAKE_PIECE_SIZE * MAP_SIZE_Y + 4, SSD1306_WHITE);
for(int i = 0; i < snake_length; i++) {
display.fillRect(snake[i][0] * SNAKE_PIECE_SIZE + offsetMapX, snake[i][1] * SNAKE_PIECE_SIZE + offsetMapY, SNAKE_PIECE_SIZE, SNAKE_PIECE_SIZE, SSD1306_WHITE);
}
}
void drawScore() {
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(2,2);
display.print(F("Score:"));
display.println(snake_length - STARTING_SNAKE_SIZE);
}
void drawPressToStart() {
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(2,20);
display.print(F("Press a\n button to\n start the\n game!"));
}
void drawGameover() {
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(2,50);
display.println(F("GAMEOVER"));
}