В данной статье мы рассмотрим создание всемирно известной игры «Тетрис» с помощью платы Arduino и OLED дисплее. На первый взгляд эта задача может показаться невообразимо сложной, но не волнуйтесь, я думаю с помощью нашего руководства ее решение не доставит вам особых хлопот.
Кратко принцип работы нашего проекта можно посмотреть в следующем видео.
Необходимые компоненты
- Плата Arduino Nano (купить на AliExpress).
- OLED дисплей с интерфейсом I2C и диагональю экрана 1.3 дюйма (1.3 inch I2C OLED display) (купить на AliExpress — для данного проекта выбирайте вариант дисплея с 4 контактами).
- Кнопки — 4 шт.
- Зуммер (купить на AliExpress).
- Батарейка на 9 В.
- Макетная плата.
- Соединительные провода.
Распиновка OLED дисплея
Назначение контактов (распиновка) OLED дисплея с интерфейсом I2C приведена на следующем рисунке.
GND (Ground) — общий провод (земля).
VCC — контакт подачи питания на дисплей.
SCL (Serial Clock) — контакт для передачи синхросигналов в интерфейсе I2C.
SDA (Serial Data) — контакт для передачи данных в интерфейсе I2C.
Схема проекта
Схема проекта игры Тетрис на Arduino и OLED дисплее приведена на следующем рисунке.
OLED-дисплей в нашем проекте подключается к плате Arduino Nano с помощью 4-контактного интерфейса I2C. Что касается кнопок, то вам необходимо подключить их к четырем цифровым входам на Arduino Nano. Этот процесс достаточно прост — просто подключите один контакт каждой кнопки к цифровому входу платы, а другой контакт — к контакту GND платы.
Затем подключите зуммер к цифровому выходу плате Arduino Nano. Наконец, чтобы подать питание на проект, подключите батарею 9 В к контакту Vin платы Arduino Nano (положительный провод) и контакту GND платы (отрицательный провод).
Объяснение кода программы для Arduino
Полный код программы приведен в конце статьи, здесь же мы кратко рассмотрим его основные фрагменты.
Первым делом в коде программы подключим необходимые библиотеки для работы с OLED дисплеем.
1 2 3 |
#include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> |
Затем зададим ширину и высоту OLED дисплея в пикселях.
1 2 |
#define WIDTH 64 #define HEIGHT 128 |
После этого инициализируем объект для работы с OLED дисплеем с помощью библиотеки Wire, подавая на контакт сброса (reset pin) -1, что значит что он не будет использоваться.
1 |
Adafruit_SSD1306 display(128, 64, &Wire, -1); |
Шестнадцатеричное представление лого сайта circuit digest (откуда взят оригинал данной статьи):
1 2 3 4 |
static const unsigned char PROGMEM mantex_logo [] = { 0x00, 0x00, 0x18, 0x06, 0x01, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, ………. ……… 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; |
3D массив, задающий форму деталей тетриса. Он представляет собой форму буквы «S» если ее повернуть влево. Его первый индекс (2) задает количество вращений фигуры, второй индекс (2) задает количество строк, а третий индекс (4) представляет собой количество столбцов. Значения в этом массиве представляют положение блоков внутри фрагмента.
1 2 3 4 5 6 |
const char pieces_S_l[2][2][4] = {{ {0, 0, 1, 1}, {0, 1, 1, 2} }, { {0, 1, 1, 2}, {1, 1, 0, 0} }}; |
3D массив, который задает форму буквы «S» повернутой вправо.
1 2 3 4 5 6 |
const char pieces_S_r[2][2][4]{{ {1, 1, 0, 0}, {0, 1, 1, 2} }, { {0, 1, 1, 2}, {0, 0, 1, 1} }}; |
3D массив, который задает форму буквы «L» повернутой влево.
1 2 3 4 5 6 7 8 9 10 11 12 |
const char pieces_L_l[4][2][4] = {{ {0, 0, 0, 1}, {0, 1, 2, 2} }, { {0, 1, 2, 2}, {1, 1, 1, 0} }, { {0, 1, 1, 1}, {0, 0, 1, 2} }, { {0, 0, 1, 2}, {1, 0, 0, 0} }}; |
3D массив задающий квадрат 2×2, ему необходимо только одно вращение.
1 2 3 |
const char pieces_Sq[1][2][4] = {{ {0, 1, 0, 1}, {0, 0, 1, 1} }}; |
3D массив, который задает форму буквы «T» повернутой во всех возможных 4-х направлениях.
1 2 3 4 5 6 7 8 9 10 11 12 |
const char pieces_T[4][2][4] = {{ {0, 0, 1, 0},{0, 1, 1, 2} }, { {0, 1, 1, 2},{1, 0, 1, 1} }, { {1, 0, 1, 1},{0, 1, 1, 2} }, { {0, 1, 1, 2},{0, 0, 1, 0} }}; |
3D массив, который задает линию («line») размером 1×4. Он включает две ротации — одну горизонтальную и одну вертикальную.
1 2 3 4 5 6 |
const char pieces_l[2][2][4] = {{ {0, 1, 2, 3}, {0, 0, 0, 0} }, { {0, 0, 0, 0}, {0, 1, 2, 3} }}; |
Далее инициализируем необходимые переменные, среди которых четыре переменных целого типа: для движения влево, вправо, изменения и скорости. Зададим им начальные значения 11, 9, 12 и 10 соответственно.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
const short MARGIN_TOP = 19; - declares a constant variable MARGIN_TOP with a value of 19 of type short. const short MARGIN_LEFT = 3; - declares a constant variable MARGIN_LEFT with a value of 3 of type short. const short SIZE = 5; - declares a constant variable SIZE with a value of 5 of type short. const short TYPES = 6; - declares a constant variable TYPES with a value of 6 of type short. #define SPEAKER_PIN 3 - creates a macro with the name SPEAKER_PIN and a value of 3. This allows the code to refer to SPEAKER_PIN throughout the rest of the code and substitute it with the value 3. const int MELODY_LENGTH = 10; - declares a constant variable MELODY_LENGTH with a value of 10 of type int. const int MELODY_NOTES[MELODY_LENGTH] = {262, 294, 330, 262}; - declares an array MELODY_NOTES of size MELODY_LENGTH (which is 10), and initializes it with four integer values. const int MELODY_DURATIONS[MELODY_LENGTH] = {500, 500, 500, 500}; - declares an array MELODY_DURATIONS of size MELODY_LENGTH (which is 10), and initializes it with four integer values. int click[] = { 1047 }; - declares an array click of size 1, and initializes it with one integer value. int click_duration[] = { 100 }; - declares an array click_duration of size 1, and initializes it with one integer value. int erase[] = { 2093 }; - declares an array erase of size 1, and initializes it with one integer value. int erase_duration[] = { 100 }; - declares an array erase_duration of size 1, and initializes it with one integer value. word currentType, nextType, rotation; - declares three variables of type word, named currentType, nextType, and rotation. short pieceX, pieceY; - declares two variables of type short, named pieceX and pieceY. short piece[2][4]; - declares a two-dimensional array piece of size 2x4 with elements of type short. int interval = 20, score; - declares two variables, interval and score, of type int. interval is initialized with a value of 20. long timer, delayer; - declares two variables, timer and delayer, of type long. boolean grid[10][18]; - declares a two-dimensional array grid of size 10x18 with elements of type boolean. boolean b1, b2, b3; - declares three variables of type boolean, named b1, b2, and b3. int left=11; int right=9; int change=12; int speed=10; |
Следующий фрагмент кода реализует функции checkLines() и breakLine(short line) для очистки заполненных строк в игре.
checkLines() перебирает каждую строку сетки снизу вверх, проверяя, заполнена ли строка (все ячейки заняты) или нет. Если это так, она вызывает функцию BreakLine(), передавая номер строки в качестве параметра, чтобы очистить эту строку.
breakLine(short line) сначала воспроизводит звук «стирания», используя функцию tone() на контакте SPEAKER_PIN, который будет указывать на то, что линия очищена. Затем он сдвигает все строки выше очищенной строки на одну ячейку и очищает верхнюю строку. Это также добавляет 10 к счету за очистку линии. Наконец, функция инвертирует отображение светодиодной матрицы на 50 мс, чтобы создать визуальный эффект очищенной линии, а затем возвращает отображение в нормальное состояние.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
void checkLines(){ boolean full; for(short y = 17; y >= 0; y--){ full = true; for(short x = 0; x < 10; x++){ full = full && grid[x][y]; } if(full){ breakLine(y); y++; } } } void breakLine(short line){ tone(SPEAKER_PIN, erase[0], 1000 / erase_duration[0]); delay(100); noTone(SPEAKER_PIN); for(short y = line; y >= 0; y--){ for(short x = 0; x < 10; x++){ grid[x][y] = grid[x][y-1]; } } for(short x = 0; x < 10; x++){ grid[x][0] = 0; } display.invertDisplay(true); delay(50); display.invertDisplay(false); score += 10; } |
refresh(): эта функция очищает дисплей, рисует макет, рисует сетку и, наконец, рисует текущую фигуру на дисплее.
drawGrid(): эта функция перебирает всю сетку и, если ячейка заполнена, рисует белый прямоугольник размера ячейки.
nextHorizontalCollision(short piece[2][4], int amount): эта функция проверяет, есть ли какое-либо столкновение с сеткой в горизонтальном направлении. Для этого он проверяет каждую ячейку текущей фигуры и добавляет сумму к ее позиции x. Если новая позиция x находится за пределами сетки или уже занята, происходит столкновение, и функция возвращает true.
nextCollision(): эта функция проверяет, есть ли какие-либо столкновения с сеткой в вертикальном направлении. Она делает это, проверяя каждую ячейку в текущей фигуре и добавляя единицу к ее позиции y. Если новая позиция y находится за пределами сетки или уже занята, происходит столкновение, и функция возвращает true.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
void refresh(){ display.clearDisplay(); drawLayout(); drawGrid(); drawPiece(currentType, 0, pieceX, pieceY); display.display(); } void drawGrid(){ for(short x = 0; x < 10; x++) for(short y = 0; y < 18; y++) if(grid[x][y]) display.fillRect(MARGIN_LEFT + (SIZE + 1)*x, MARGIN_TOP + (SIZE + 1)*y, SIZE, SIZE, WHITE); } boolean nextHorizontalCollision(short piece[2][4], int amount){ for(short i = 0; i < 4; i++){ short newX = pieceX + piece[0][i] + amount; if(newX > 9 || newX < 0 || grid[newX][pieceY + piece[1][i]]) return true; } return false; } boolean nextCollision(){ for(short i = 0; i < 4; i++){ short y = pieceY + piece[1][i] + 1; short x = pieceX + piece[0][i]; if(y > 17 || grid[x][y]) return true; } return false; } |
generate() — эта функция устанавливает переменные для следующей фигуры тетриса, которая будет введена на игровое поле. Она устанавливает currentType таким же, как nextType, а затем генерирует новый nextType с помощью функции random().
Если currentType не является частью «O» (представленной значением типа 5), то для значения PieceX устанавливается случайное число от 0 до 8 (включительно), поскольку часть «O» всегда центрируется. В противном случае для фрагмента «O» значение PieceX устанавливается в случайное число от 0 до 6 (включительно).
Значение PieceY установлавивается равным 0, что указывает на то, что фигура будет начинаться с верха игрового поля. Значение поворота (rotation value) устанавливается в 0, что указывает на то, что фигура не была повернута.
Наконец, вызывается функция copyPiece() для копирования соответствующего фрагмента в массив фрагментов.
Функция drawPiece() принимает тип фигуры тетриса, ее вращение и текущие координаты x и y на игровом поле. Затем она использует цикл for для перебора четырех блоков фигуры и рисования каждого блока на игровом поле с помощью функции display.fillRect().
Функция drawNextPiece() рисует следующий фрагмент тетриса в поле предварительного просмотра в правой части игрового поля. Сначала она копирует следующий фрагмент в массив nPiece с помощью функции copyPiece(). Затем она проходит через четыре блока фрагмента и рисует каждый блок в поле предварительного просмотра с помощью функции display.fillRect().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
void generate(){ currentType = nextType; nextType = random(TYPES); if(currentType != 5) pieceX = random(9); else pieceX = random(7); pieceY = 0; rotation = 0; copyPiece(piece, currentType, rotation); } void drawPiece(short type, short rotation, short x, short y){ for(short i = 0; i < 4; i++) display.fillRect(MARGIN_LEFT + (SIZE + 1)*(x + piece[0][i]), MARGIN_TOP + (SIZE + 1)*(y + piece[1][i]), SIZE, SIZE, WHITE); } void drawNextPiece(){ short nPiece[2][4]; copyPiece(nPiece, nextType, 0); for(short i = 0; i < 4; i++) display.fillRect(50 + 3*nPiece[0][i], 4 + 3*nPiece[1][i], 2, 2, WHITE); } |
refresh(): эта функция вызывается для обновления изображения на дисплее. Сначала она очищает дисплей, а затем рисует макет, сетку и текущию фигуру на экране.
drawGrid(): эта функция вызывается функцией Refresh() для рисования сетки на экране, представляющей уже упавшие блоки.
nextHorizontalCollision(): This function checks if the current piece will collide with any block in the next horizontal move. It does this by checking each of the 4 blocks that make up the piece, and determining whether moving it by amount spaces would make it overlap with any blocks that have already fallen.
nextCollision(): эта функция проверяет, столкнется ли текущая фигура с каким-либо блоком при следующем горизонтальном движении (то есть когда фигура опустится на одну строчку вниз). Она делает это, проверяя каждый из 4 блоков, составляющих фигуру, и определяя, приведет ли перемещение ее на количество ячеек к перекрытию с любыми уже упавшими блоками.
generate(): эта функция генерирует новую фигуру для управления игроком. Она устанавливает для currentType тип следующей фигуры, выбирает случайную позицию для начала фигуры (pieceX), устанавливает для PieceY значение 0 (верхняя часть игровой области), устанавливает вращение на 0 и вызывает copyPiece() для заполнения в массиве частей с соответствующими блоками для новой фигуры.
drawPiece(): эта функция вызывается для рисования текущей фигуры на экране. Для этого она перебирает каждый из 4 блоков, составляющих фигуру, и рисует для каждого из них белый квадрат в соответствующей позиции на экране.
drawNextPiece(): эта функция вызывается для рисования следующей фигуры на экране. Для этого она копирует позиции блоков для следующей фигуры в новый массив nPiece, а затем рисует небольшой предварительный просмотр фигуры в правом верхнем углу экрана.
copyPiece(): эта функция заполняет массив фигур соответствующими блоками для данного типа фигуры и направления вращения. Для этого она включает тип, чтобы определить, какой набор блоков использовать, а затем копирует их по частям на основе текущего вращения.
getMaxRotation(): эта функция возвращает максимальное количество вращений, которое может иметь данный тип фигуры. Она возвращает 2 для типов 1, 2 и 5, 4 для типов 0 и 4, 1 для типа 3 и 0 для любого другого типа.
canRotate(): эта функция проверяет, можно ли повернуть текущую фигуру на величину поворота. Это делается путем вызова функции copyPiece() для создания нового массива частей с повернутыми блоками, а затем вызова nextHorizontalCollision(), чтобы проверить, будет ли повернутая часть перекрываться с какими-либо упавшими блоками.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
void copyPiece(short piece[2][4], short type, short rotation){ switch(type){ case 0: //L_l for(short i = 0; i < 4; i++){ piece[0][i] = pieces_L_l[rotation][0][i]; piece[1][i] = pieces_L_l[rotation][1][i]; } break; case 1: //S_l for(short i = 0; i < 4; i++){ piece[0][i] = pieces_S_l[rotation][0][i]; piece[1][i] = pieces_S_l[rotation][1][i]; } break; case 2: //S_r for(short i = 0; i < 4; i++){ piece[0][i] = pieces_S_r[rotation][0][i]; piece[1][i] = pieces_S_r[rotation][1][i]; } break; case 3: //Sq for(short i = 0; i < 4; i++){ piece[0][i] = pieces_Sq[0][0][i]; piece[1][i] = pieces_Sq[0][1][i]; } break; case 4: //T for(short i = 0; i < 4; i++){ piece[0][i] = pieces_T[rotation][0][i]; piece[1][i] = pieces_T[rotation][1][i]; } break; case 5: //l for(short i = 0; i < 4; i++){ piece[0][i] = pieces_l[rotation][0][i]; piece[1][i] = pieces_l[rotation][1][i]; } break; } } short getMaxRotation(short type){ if(type == 1 || type == 2 || type == 5) return 2; else if(type == 0 || type == 4) return 4; else if(type == 3) return 1; else return 0; } boolean canRotate(short rotation){ short piece[2][4]; copyPiece(piece, currentType, rotation); return !nextHorizontalCollision(piece, 0); } |
drawLayout() — эта функция отвечает за отрисовку основного макета игрового экрана, включая границу, разделительную линию, счет и следующую фигуру. Она вызывает функции drawNextPiece() и drawText() для рисования следующей фигуры и счета соответственно.
getNumberLength(int n) — вспомогательная функция, которая принимает на вход целое число и возвращает количество его цифр. Это полезно для определения длины счета, который необходимо вывести на экран.
drawText(char text[], short length, int x, int y) — рисует строку текста на экране по заданным координатам x и y. В качестве параметров она принимает текст, который нужно отрисовать, его длину и координаты x и y. Эта функция устанавливает размер, цвет, положение курсора и шрифт текста перед рисованием текста.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
void drawLayout(){ display.drawLine(0, 15, WIDTH, 15, WHITE); display.drawRect(0, 0, WIDTH, HEIGHT, WHITE); drawNextPiece(); char text[6]; itoa(score, text, 10); drawText(text, getNumberLength(score), 7, 4); } short getNumberLength(int n){ short counter = 1; while(n >= 10){ n /= 10; counter++; } return counter; } void drawText(char text[], short length, int x, int y){ display.setTextSize(1); // Normal 1:1 pixel scale display.setTextColor(WHITE); // Draw white text display.setCursor(x, y); // Start at top-left corner display.cp437(true); // Use full 256 char 'Code Page 437' font for(short i = 0; i < length; i++) display.write(text[i]); } |
В функции setup() мы инициализируем входные и выходные контакты, устанавливаем связь через последовательный порт, инициализируем и очищаем OLED-дисплей и устанавливаем вращение дисплея. Затем на две секунды отображается логотип, а затем очищается дисплей и вызывается функция drawLayout() для рисования макета игры. Наконец, устанавливается случайное начальное число, генерируется первая фигура и запускается таймер.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
void setup() { pinMode(left, INPUT_PULLUP); pinMode(right, INPUT_PULLUP); pinMode(change, INPUT_PULLUP); pinMode(speed, INPUT_PULLUP); pinMode(SPEAKER_PIN, OUTPUT); Serial.begin(9600); // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3D for 128x64 Serial.println(F("SSD1306 allocation failed")); for(;;); // Don't proceed, loop forever } display.setRotation(1); display.clearDisplay(); display.drawBitmap(3, 23, mantex_logo, 64, 82, WHITE); display.display(); delay(2000); display.clearDisplay(); drawLayout(); display.display(); randomSeed(analogRead(0)); nextType = random(TYPES); generate(); timer = millis(); } |
В функции loop() мы проверяем, прошел ли заданный интервал времени, и выполняем необходимые действия, такие как проверка и очистка строк или генерация новой фигуры. Также мы считываем состояния кнопок и обрабатываем движение текущей фигуры, включая вращение и горизонтальное перемещение. Наконец, мы регулируем скорость игры в зависимости от нажатия определенной кнопки. Также мы генерируем звуковые эффекты с помощью функций tone() и noTone().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
void loop() { if(millis() - timer > interval){ checkLines(); refresh(); if(nextCollision()){ for(short i = 0; i < 4; i++) grid[pieceX + piece[0][i]][pieceY + piece[1][i]] = 1; generate(); }else pieceY++; timer = millis(); } if(!digitalRead(left)){ tone(SPEAKER_PIN, click[0], 1000 / click_duration[0]); delay(100); noTone(SPEAKER_PIN); if(b1){ if(!nextHorizontalCollision(piece, -1)){ pieceX--; refresh(); } b1 = false; } }else{ b1 = true; } if(!digitalRead(right)){ tone(SPEAKER_PIN, click[0], 1000 / click_duration[0]); delay(100); noTone(SPEAKER_PIN); if(b2){ if(!nextHorizontalCollision(piece, 1)){ pieceX++; refresh(); } b2 = false; } }else{ b2 = true; } if(!digitalRead(speed)){ interval = 20; } else{ interval = 400; } if(!digitalRead(change)){ tone(SPEAKER_PIN, click[0], 1000 / click_duration[0]); delay(100); noTone(SPEAKER_PIN); if(b3){ if(rotation == getMaxRotation(currentType) - 1 && canRotate(0)){ rotation = 0; }else if(canRotate(rotation + 1)){ rotation++; } copyPiece(piece, currentType, rotation); refresh(); b3 = false; delayer = millis(); } }else if(millis() - delayer > 50){ b3 = true; } } |
Заключение
В этом проекте мы рассмотрели как создать игру «Тетрис», используя плату Arduino и OLED-дисплей 128×64. Для программирования игры мы использовали язык C++ и Arduino IDE.
Мы начали с настройки оборудования, включая дисплей, кнопки и зуммер, а затем определили игровую логику. Мы использовали сетку для представления игрового поля и задали семь фигур с помощью массивов. Мы также создали функции для создания и вращения фигур, проверки столкновений и обновления игрового поля.
Далее мы определили игровой цикл и реализовали кнопки управления. Мы также добавили звуковые эффекты с помощью функции tone().
Наконец, мы создали простой пользовательский интерфейс игры с использованием OLED-дисплея. Мы отображали текущий счет и следующую фигуру, а также логотип Тетриса в начале игры.
В целом, этот проект демонстрирует, как создать базовую аркадную игру с использованием Arduino и некоторых простых компонентов. Код можно расширять и модифицировать для создания более сложных игр или добавления новых функций.
Код программы (скетча)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 |
#include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #define WIDTH 64 // OLED display width, in pixels #define HEIGHT 128 // OLED display height, in pixels Adafruit_SSD1306 display(128, 64, &Wire, -1); static const unsigned char PROGMEM mantex_logo [] = { 0x00, 0x00, 0x18, 0x06, 0x01, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3c, 0x0f, 0x03, 0xe0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x3f, 0x80, 0x00, 0x00, 0x0f, 0xc0, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x07, 0xe0, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x01, 0xe0, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x78, 0x7f, 0xff, 0xff, 0xe1, 0xf0, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xf8, 0xff, 0xff, 0xff, 0xf0, 0xf0, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xf1, 0xff, 0xff, 0xff, 0xf8, 0xf0, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xf1, 0xff, 0xff, 0xff, 0xfc, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xf1, 0xff, 0x0f, 0xff, 0xfc, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xfc, 0x01, 0xff, 0xfc, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xf0, 0x00, 0xff, 0xfc, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xf1, 0xf0, 0x00, 0x7f, 0xfc, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0xf1, 0xe0, 0x70, 0x3f, 0xfc, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xf1, 0xe1, 0xf8, 0x3f, 0xfc, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xf1, 0xe1, 0xff, 0xff, 0xfc, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xf1, 0xe1, 0xff, 0xff, 0xfc, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xf1, 0xc1, 0xff, 0xff, 0xfc, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xf1, 0xe1, 0xe0, 0x07, 0xfc, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xf1, 0xe1, 0xe0, 0x01, 0xfc, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xe1, 0xe0, 0x00, 0xfc, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xe0, 0xe3, 0xf8, 0x7c, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xf1, 0xe0, 0x63, 0xfc, 0x7c, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xf1, 0xf0, 0x23, 0xfc, 0x3c, 0xff, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xf1, 0xf8, 0x23, 0xfc, 0x3c, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xf1, 0xff, 0x63, 0xfc, 0x3c, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xf1, 0xff, 0xe3, 0xfc, 0x3c, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xf1, 0xff, 0xe3, 0xfc, 0x7c, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xf1, 0xff, 0xe3, 0xf8, 0x7c, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xf1, 0xff, 0xe1, 0xf0, 0x7c, 0xff, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xff, 0xe0, 0x00, 0xfc, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xff, 0xe0, 0x03, 0xfc, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xff, 0xe0, 0x1f, 0xfc, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xf1, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xf1, 0xff, 0xff, 0xff, 0xf8, 0xf0, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xf0, 0xff, 0xff, 0xff, 0xf8, 0xf0, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x78, 0x7f, 0xff, 0xff, 0xf0, 0xf0, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x7c, 0x1f, 0xff, 0xff, 0xc1, 0xf0, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x03, 0xe0, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x0f, 0xc0, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x07, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x01, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3e, 0x1f, 0x83, 0xe0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3e, 0x0f, 0x83, 0xe0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3c, 0x0f, 0x83, 0xe0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3c, 0x0f, 0x01, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; const char pieces_S_l[2][2][4] = {{ {0, 0, 1, 1}, {0, 1, 1, 2} }, { {0, 1, 1, 2}, {1, 1, 0, 0} }}; const char pieces_S_r[2][2][4]{{ {1, 1, 0, 0}, {0, 1, 1, 2} }, { {0, 1, 1, 2}, {0, 0, 1, 1} }}; const char pieces_L_l[4][2][4] = {{ {0, 0, 0, 1}, {0, 1, 2, 2} }, { {0, 1, 2, 2}, {1, 1, 1, 0} }, { {0, 1, 1, 1}, {0, 0, 1, 2} }, { {0, 0, 1, 2}, {1, 0, 0, 0} }}; const char pieces_Sq[1][2][4] = {{ {0, 1, 0, 1}, {0, 0, 1, 1} }}; const char pieces_T[4][2][4] = {{ {0, 0, 1, 0},{0, 1, 1, 2} }, { {0, 1, 1, 2},{1, 0, 1, 1} }, { {1, 0, 1, 1},{0, 1, 1, 2} }, { {0, 1, 1, 2},{0, 0, 1, 0} }}; const char pieces_l[2][2][4] = {{ {0, 1, 2, 3}, {0, 0, 0, 0} }, { {0, 0, 0, 0}, {0, 1, 2, 3} }}; const short MARGIN_TOP = 19; const short MARGIN_LEFT = 3; const short SIZE = 5; const short TYPES = 6; #define SPEAKER_PIN 3 const int MELODY_LENGTH = 10; const int MELODY_NOTES[MELODY_LENGTH] = {262, 294, 330, 262}; const int MELODY_DURATIONS[MELODY_LENGTH] = {500, 500, 500, 500}; int click[] = { 1047 }; int click_duration[] = { 100 }; int erase[] = { 2093 }; int erase_duration[] = { 100 }; word currentType, nextType, rotation; short pieceX, pieceY; short piece[2][4]; int interval = 20, score; long timer, delayer; boolean grid[10][18]; boolean b1, b2, b3; int left=11; int right=9; int change=12; int speed=10; void checkLines(){ boolean full; for(short y = 17; y >= 0; y--){ full = true; for(short x = 0; x < 10; x++){ full = full && grid[x][y]; } if(full){ breakLine(y); y++; } } } void breakLine(short line){ tone(SPEAKER_PIN, erase[0], 1000 / erase_duration[0]); delay(100); noTone(SPEAKER_PIN); for(short y = line; y >= 0; y--){ for(short x = 0; x < 10; x++){ grid[x][y] = grid[x][y-1]; } } for(short x = 0; x < 10; x++){ grid[x][0] = 0; } display.invertDisplay(true); delay(50); display.invertDisplay(false); score += 10; } void refresh(){ display.clearDisplay(); drawLayout(); drawGrid(); drawPiece(currentType, 0, pieceX, pieceY); display.display(); } void drawGrid(){ for(short x = 0; x < 10; x++) for(short y = 0; y < 18; y++) if(grid[x][y]) display.fillRect(MARGIN_LEFT + (SIZE + 1)*x, MARGIN_TOP + (SIZE + 1)*y, SIZE, SIZE, WHITE); } boolean nextHorizontalCollision(short piece[2][4], int amount){ for(short i = 0; i < 4; i++){ short newX = pieceX + piece[0][i] + amount; if(newX > 9 || newX < 0 || grid[newX][pieceY + piece[1][i]]) return true; } return false; } boolean nextCollision(){ for(short i = 0; i < 4; i++){ short y = pieceY + piece[1][i] + 1; short x = pieceX + piece[0][i]; if(y > 17 || grid[x][y]) return true; } return false; } void generate(){ currentType = nextType; nextType = random(TYPES); if(currentType != 5) pieceX = random(9); else pieceX = random(7); pieceY = 0; rotation = 0; copyPiece(piece, currentType, rotation); } void drawPiece(short type, short rotation, short x, short y){ for(short i = 0; i < 4; i++) display.fillRect(MARGIN_LEFT + (SIZE + 1)*(x + piece[0][i]), MARGIN_TOP + (SIZE + 1)*(y + piece[1][i]), SIZE, SIZE, WHITE); } void drawNextPiece(){ short nPiece[2][4]; copyPiece(nPiece, nextType, 0); for(short i = 0; i < 4; i++) display.fillRect(50 + 3*nPiece[0][i], 4 + 3*nPiece[1][i], 2, 2, WHITE); } void copyPiece(short piece[2][4], short type, short rotation){ switch(type){ case 0: //L_l for(short i = 0; i < 4; i++){ piece[0][i] = pieces_L_l[rotation][0][i]; piece[1][i] = pieces_L_l[rotation][1][i]; } break; case 1: //S_l for(short i = 0; i < 4; i++){ piece[0][i] = pieces_S_l[rotation][0][i]; piece[1][i] = pieces_S_l[rotation][1][i]; } break; case 2: //S_r for(short i = 0; i < 4; i++){ piece[0][i] = pieces_S_r[rotation][0][i]; piece[1][i] = pieces_S_r[rotation][1][i]; } break; case 3: //Sq for(short i = 0; i < 4; i++){ piece[0][i] = pieces_Sq[0][0][i]; piece[1][i] = pieces_Sq[0][1][i]; } break; case 4: //T for(short i = 0; i < 4; i++){ piece[0][i] = pieces_T[rotation][0][i]; piece[1][i] = pieces_T[rotation][1][i]; } break; case 5: //l for(short i = 0; i < 4; i++){ piece[0][i] = pieces_l[rotation][0][i]; piece[1][i] = pieces_l[rotation][1][i]; } break; } } short getMaxRotation(short type){ if(type == 1 || type == 2 || type == 5) return 2; else if(type == 0 || type == 4) return 4; else if(type == 3) return 1; else return 0; } boolean canRotate(short rotation){ short piece[2][4]; copyPiece(piece, currentType, rotation); return !nextHorizontalCollision(piece, 0); } void drawLayout(){ display.drawLine(0, 15, WIDTH, 15, WHITE); display.drawRect(0, 0, WIDTH, HEIGHT, WHITE); drawNextPiece(); char text[6]; itoa(score, text, 10); drawText(text, getNumberLength(score), 7, 4); } short getNumberLength(int n){ short counter = 1; while(n >= 10){ n /= 10; counter++; } return counter; } void drawText(char text[], short length, int x, int y){ display.setTextSize(1); // Normal 1:1 pixel scale display.setTextColor(WHITE); // Draw white text display.setCursor(x, y); // Start at top-left corner display.cp437(true); // Use full 256 char 'Code Page 437' font for(short i = 0; i < length; i++) display.write(text[i]); } void setup() { pinMode(left, INPUT_PULLUP); pinMode(right, INPUT_PULLUP); pinMode(change, INPUT_PULLUP); pinMode(speed, INPUT_PULLUP); pinMode(SPEAKER_PIN, OUTPUT); Serial.begin(9600); // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3D for 128x64 Serial.println(F("SSD1306 allocation failed")); for(;;); // Don't proceed, loop forever } display.setRotation(1); display.clearDisplay(); display.drawBitmap(3, 23, mantex_logo, 64, 82, WHITE); display.display(); delay(2000); display.clearDisplay(); drawLayout(); display.display(); randomSeed(analogRead(0)); nextType = random(TYPES); generate(); timer = millis(); } void loop() { if(millis() - timer > interval){ checkLines(); refresh(); if(nextCollision()){ for(short i = 0; i < 4; i++) grid[pieceX + piece[0][i]][pieceY + piece[1][i]] = 1; generate(); }else pieceY++; timer = millis(); } if(!digitalRead(left)){ tone(SPEAKER_PIN, click[0], 1000 / click_duration[0]); delay(100); noTone(SPEAKER_PIN); if(b1){ if(!nextHorizontalCollision(piece, -1)){ pieceX--; refresh(); } b1 = false; } }else{ b1 = true; } if(!digitalRead(right)){ tone(SPEAKER_PIN, click[0], 1000 / click_duration[0]); delay(100); noTone(SPEAKER_PIN); if(b2){ if(!nextHorizontalCollision(piece, 1)){ pieceX++; refresh(); } b2 = false; } }else{ b2 = true; } if(!digitalRead(speed)){ interval = 20; }else{ interval = 400; } if(!digitalRead(change)){ tone(SPEAKER_PIN, click[0], 1000 / click_duration[0]); delay(100); noTone(SPEAKER_PIN); if(b3){ if(rotation == getMaxRotation(currentType) - 1 && canRotate(0)){ rotation = 0; }else if(canRotate(rotation + 1)){ rotation++; } copyPiece(piece, currentType, rotation); refresh(); b3 = false; delayer = millis(); } } else if(millis() - delayer > 50){ b3 = true; } } |