Hướng dẫn làm máy chơi Game xếp gạch sử dụng OLED 0.96 với Arduino

Hướng dẫn làm máy chơi Game xếp gạch sử dụng OLED 0.96 với Arduino

Không ít bạn ở đây tuổi thơ đã gắn liền với máy chơi game xếp gạch, với cách chơi đơn giản nhưng vô cùng gây nghiện. Trong hướng dẫn này mình sẽ sử dụng một màn hình OLED 0.96 I2C với Arduino Nano để tạo ra trò chơi thú vị này.

Để tạo động lực cho Team Arduino KIT ra nhiều bài viết chất lượng hơn, các bạn có thể ủng hộ mình bằng cách Donate qua MoMo, Ngân hàng, Paypal…Nhấn vào link bên dưới nhé.

Tổng quan màn hình OLED 0.96

Màn hình OLED 0.96 có nhiều kích thước (chẳng hạn như 128×64, 128×32) và màu sắc ( OLED trắng, xanh lam, vàng). OLED sử dụng giao tiếp truyền thông I2Cgiao tiếp SPI.

Điểm chung giữa hai màn hình OLED I2C và SPI là đều sử dụng IC SSD1306 có khả năng điều khiển các màn hình OLED có nhiều kích cỡ và độ phân giải, (128×64, 128×32). Nó hỗ trợ hiển thị màu sắc, độ tương phản cao và điều chỉnh được độ sáng.

IC SSD1306 xử lý tất cả bộ đệm RAM và yêu cầu rất ít tài nguyên từ Arduino.

Xem ngay: Hiển thị thời gian thực bằng màn hình OLED 0.96 sử dụng NodeMCU ESP8266

Sơ đồ chân màn hình OLED I2C

  • VCC: Chân nguồn (điện áp khuyến cáo là 5V)
  • GND: Chân nối đất
  • SDA: Chân dữ liệu
  • SCL: Chân xung Clock SCL

Linh kiện cần thiết cho dự án

TÊN LINH KIỆN SỐ LƯỢNG NƠI BÁN
Arduino Nano 1 Shopee | Cytron
Màn hình OLED 0.96 I2C 1 Shopee | Cytron
Breadboard 1 Shopee | Cytron
Nút nhấn 4 Shopee | Cytron
Loa 5V 1 Shopee | Cytron
Pin 9V 1 Shopee | Cytron
Dây nguồn Pin 9V 1 Shopee | Cytron
Dây cắm (Đực – Đực) 10 – 20 Shopee | Cytron

Sơ đồ đấu nối máy Game xếp gạch sử dụng OLED 0.96 với Arduino

Giải thích Code

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

Khai báo các thư viện cần thiết để làm việc với màn hình OLED.

#define WIDTH 64
#define HEIGHT 128

Định nghĩa các hằng số, chiều rộng của màn hình (WIDTH) là 64 pixel và chiều cao (HEIGHT) là 128 pixel.

Adafruit_SSD1306 display(128, 64, &Wire, -1);

128: Đây là giá trị chiều rộng (width) của màn hình OLED.
64: Đây là giá trị chiều cao (height) của màn hình OLED.
&Wire: Đây là đối tượng Wire được truyền vào để sử dụng cho giao tiếp I2C. Wire là một thư viện cho phép Arduino giao tiếp qua giao thức I2C.
-1: Đây là địa chỉ I2C của màn hình OLED. Giá trị -1 được sử dụng để chỉ định rằng địa chỉ I2C sẽ được xác định tự động.

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
  };

Khai báo một mảng dữ liệu mantex_logo chứa các giá trị hexa (dưới dạng unsigned char) để biểu diễn hình ảnh của logo Mantex.

const char pieces_S_l[2][2][4] = {{
                                      {0, 0, 1, 1}, {0, 1, 1, 2}
                                    },
                                    {
                                      {0, 1, 1, 2}, {1, 1, 0, 0}
                                    }};

Đoạn code trên khai báo một mảng hai chiều pieces_S_l có kiểu dữ liệu char. Mảng này được sắp xếp thành một cấu trúc dữ liệu ba chiều với kích thước 2x2x4.

Mỗi phần tử trong mảng pieces_S_l là một mảng hai chiều có kích thước 4×1, chứa các giá trị số nguyên kiểu char.

Cấu trúc dữ liệu này được sử dụng để lưu trữ thông tin về các khối hình chữ S khi xoay trái.

const char pieces_S_r[2][2][4]{{
                                    {1, 1, 0, 0}, {0, 1, 1, 2}
                                  },
                                  {
                                    {0, 1, 1, 2}, {0, 0, 1, 1}
                                  }};

Mảng này đại diện cho các khối hình chữ S khi xoay phải.

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} 
                                  }};

Mảng này đại diện cho các khối hình chữ L khi xoay trái.

const char Pieces_Sq[1][2][4] = {{ 
                                    {0, 1, 0, 1}, {0, 0, 1, 1} } 
                                  };

Mảng này đại diện cho khối hình vuông và nó chỉ quay một vòng.

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} 
                                  }};

Mảng này đại diện cho khối hình chữ T và xoay theo bốn hướng.

const char Pieces_l[2][2][4] = {{ 
                                    {0, 1, 2, 3}, {0, 0, 0, 0} } 
                                  , 
                                  { 
                                    {0, 0, 0, 0}, {0, 1, 2, 3} 
                                  }};

Mảng này có hình dạng là đường “Line” và xoay theo hai hướng (hướng ngang và hướng dọc).

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;

Khai báo các nút trên bàn phím của máy Game xếp gạch, với 4 số nguyên (LEFT, RIGHT, CHANGE, SPEED) tương ứng với các chân 11, 9, 12, 10 trên Arduino Nano.

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;
  }

Hàm checkLines() kiểm tra các dòng trong lưới trò chơi xem có dòng nào đã đầy hay không. Nếu có, hàm sẽ gọi hàm breakLine() để xóa dòng đó và cập nhật lại lưới.

Hàm breakLine(short line) được sử dụng để xóa một dòng đã đầy trong lưới trò chơi và cập nhật lại lưới sau khi xóa.

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;
  }

Hàm refresh() được sử dụng để làm mới màn hình hiển thị của trò chơi.

Hàm drawGrid() được gọi để vẽ lưới trò chơi, trong đó, các ô được tô đen nếu giá trị tương ứng trong lưới gridtrue.

Hàm nextHorizontalCollision() kiểm tra xem có va chạm ngang tiếp theo của khối hiện tại không. Với mỗi phần tử trong mảng piece, hàm tính toán vị trí mới newX của ô theo giá trị pieceX và amount. Nếu vị trí mới newX vượt quá giới hạn của lưới hoặc có va chạm với một ô trong lưới tại vị trí mới newX và pieceY, hàm trả về true, ngược lại trả về false.

Hàm nextCollision() kiểm tra xem có va chạm đứng tiếp theo của khối hiện tại không. Với mỗi phần tử trong mảng piece, hàm tính toán vị trí mới x và y của ô theo giá trị pieceX và pieceY cộng với các giá trị tương ứng trong mảng piece. Nếu vị trí mới y vượt quá giới hạn của lưới hoặc có va chạm với một ô trong lưới tại vị trí x và y, hàm trả về true, ngược lại trả về 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);
  }

Hàm generate() được sử dụng để tạo một khối mới để rơi xuống trong trò chơi.

Trước tiên, giá trị của biến currentType được gán bằng giá trị của biến nextType, và giá trị của biến nextType được tạo ngẫu nhiên từ 0 đến TYPES.

Nếu giá trị của currentType khác 5 (loại khối đặc biệt), biến pieceX được gán một giá trị ngẫu nhiên từ 0 đến 9. Nếu giá trị của currentType là 5, pieceX được gán một giá trị ngẫu nhiên từ 0 đến 7.

pieceY được gán giá trị 0 để đặt khối mới ở vị trí ban đầu trên lưới.

Biến rotation được gán giá trị 0 để đặt khối mới ở góc quay ban đầu.

Hàm copyPiece() được gọi để sao chép thông tin về khối từ mảng piece tương ứng với currentType và rotation vào mảng piece.

Hàm drawPiece() được sử dụng để vẽ khối trên màn hình. Với mỗi phần tử trong mảng piece, hàm fillRect() được gọi để vẽ một hình chữ nhật trắng tại vị trí tương ứng trên màn hình, được tính toán dựa trên vị trí x, y, và giá trị trong mảng piece.

Hàm drawNextPiece() được sử dụng để vẽ khối tiếp theo trên màn hình. Khối tiếp theo được sao chép vào mảng nPiece từ mảng piece tương ứng với nextType và góc quay 0. Với mỗi phần tử trong mảng nPiece, hàm fillRect() được gọi để vẽ một hình chữ nhật trắng nhỏ tại vị trí tương ứng trên màn hình, được tính toán dựa trên giá trị trong mảng nPiece.

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);
  }

Hàm copyPiece() được sử dụng để sao chép thông tin về khối từ các mảng pieces_L_l, pieces_S_l, pieces_S_r, pieces_Sq, pieces_T, và pieces_l vào mảng piece tương ứng với loại khối (type) và góc quay (rotation) cho trước.

Hàm getMaxRotation() trả về số lượng góc quay tối đa cho mỗi loại khối. Cụ thể, nếu type là 1, 2 hoặc 5, thì trả về 2. Nếu type là 0 hoặc 4, trả về 4. Nếu type là 3, trả về 1. Trong trường hợp còn lại, trả về 0.

Hàm canRotate() kiểm tra xem khối có thể xoay được sang góc quay (rotation) mới hay không. Đầu tiên, một mảng piece được sao chép từ mảng currentType và rotation sử dụng hàm copyPiece(). Sau đó, hàm kiểm tra xem khối có va chạm với các phần tử ngang bên cạnh không bằng cách gọi hàm nextHorizontalCollision() với piece và tham số amount bằng 0. Kết quả trả về là true nếu có va chạm và false nếu không có va chạm.v

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]);
  }

Hàm drawLayout() được sử dụng để vẽ giao diện chung của trò chơi.

Hàm getNumberLength() được sử dụng để tính toán độ dài của một số nguyên (n) trong chuỗi ký tự. Nó sử dụng một biến đếm (counter) khởi tạo bằng 1 và sau đó tăng biến này lên mỗi lần chia n cho 10 cho đến khi n không còn lớn hơn hoặc bằng 10 nữa. Cuối cùng, hàm trả về giá trị của biến đếm.

Hàm drawText() được sử dụng để vẽ chuỗi ký tự (text) lên màn hình với độ dài (length), vị trí x, y được chỉ định. Trước khi vẽ, nó thiết lập kích thước chữ (textSize), màu chữ (textColor), vị trí vẽ (setCursor), và bộ ký tự (cp437). Sau đó, nó lặp qua từng ký tự trong chuỗi text và sử dụng hàm display.write() để vẽ từng ký tự lên màn hình.

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();
  }

Hàm setup() là một hàm được gọi một lần duy nhất khi Arduino khởi động hoặc được reset. Nó được sử dụng để khởi tạo các cấu hình và thiết lập ban đầu cho các chân I/O, màn hình, giao tiếp Serial và các biến khác.

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;
    }
  }

Hàm loop() là một vòng lặp chính của chương trình Arduino, nó được thực thi lặp đi lặp lại cho đến khi Arduino tắt hoặc reset. Trong hàm loop(), chương trình thực hiện kiểm tra thời gian cập nhật và trạng thái của các nút bấm như: Left, Right, Speed, Change.

Code Game xếp gạch Arduino

#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;
  }
}

Bài viết liên quan

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments