0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Arduino nano TETRIS Wokwi

0
Posted at

スクリーンショット 2026-07-20 120053.png

diagram.json
{
  "version": 1,
  "author": "Anonymous maker",
  "editor": "wokwi",
  "parts": [
    { "type": "wokwi-arduino-nano", "id": "nano", "top": -283.2, "left": -163.7, "attrs": {} },
    {
      "type": "board-ssd1306",
      "id": "oled1",
      "top": -294.46,
      "left": -278.17,
      "attrs": { "i2cAddress": "0x3c" }
    },
    {
      "type": "wokwi-pushbutton-6mm",
      "id": "btn1",
      "top": -184.6,
      "left": -268.8,
      "attrs": { "color": "green", "xray": "1", "key": "ArrowLeft" }
    },
    {
      "type": "wokwi-pushbutton-6mm",
      "id": "btn2",
      "top": -184.6,
      "left": -230.4,
      "attrs": { "color": "green", "xray": "1", "key": "ArrowRight" }
    },
    {
      "type": "wokwi-pushbutton-6mm",
      "id": "btn3",
      "top": -184.6,
      "left": -182.4,
      "attrs": { "color": "green", "xray": "1", "key": "ArrowDown" }
    },
    {
      "type": "wokwi-pushbutton-6mm",
      "id": "btn4",
      "top": -184.6,
      "left": -144,
      "attrs": { "color": "green", "xray": "1", "key": "ArrowUp" }
    }
  ],
  "connections": [
    [ "oled1:GND", "nano:GND.2", "black", [ "v-9.6", "h86.4" ] ],
    [ "oled1:VCC", "nano:5V", "red", [ "v-19.2", "h172.95", "h19.2" ] ],
    [ "oled1:SCL", "nano:A5", "green", [ "v-28.8", "h173.1", "h-19.2" ] ],
    [ "oled1:SDA", "nano:A4", "green", [ "v-38.4", "h134.47" ] ],
    [ "btn1:1.r", "nano:5", "green", [ "v-9.6", "h250.4", "v-96", "h-96" ] ],
    [ "btn1:2.r", "nano:GND.1", "green", [ "h0" ] ],
    [ "btn2:1.r", "nano:4", "green", [ "v0", "h135.2" ] ],
    [ "btn2:2.r", "nano:GND.1", "green", [ "h0" ] ],
    [ "btn3:1.r", "nano:3", "green", [ "v0", "h96.8" ] ],
    [ "btn3:2.r", "nano:GND.1", "green", [ "h135.2", "v0.4" ] ],
    [ "btn4:1.r", "nano:2", "green", [ "v0", "h68" ] ],
    [ "btn4:2.r", "nano:GND.1", "green", [ "h96.8", "v-9.2" ] ]
  ],
  "dependencies": {}
}
sketch.ino
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// ディスプレイの設定
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// ボタンのピン設定 (デジタルピン 2, 3, 4, 5 を使用)
const int BTN_LEFT  = 5;
const int BTN_RIGHT = 4;
const int BTN_DOWN  = 3;
const int BTN_ROT   = 2;

// ゲームボードの設定
#define BOARD_WIDTH  10
#define BOARD_HEIGHT 20
#define BLOCK_SIZE   3 // 1ブロックを3x3ピクセルで描画
#define BOARD_X      ((SCREEN_WIDTH - (BOARD_WIDTH * BLOCK_SIZE)) / 2)
#define BOARD_Y      2

// ボードの状態を保持する配列 (0: 空, 1: ブロックあり)
uint8_t board[BOARD_HEIGHT][BOARD_WIDTH] = {0};

// ゲーム状態の変数
int currentType, currentRot, currentX, currentY;
int nextType;
unsigned long lastDropTime = 0;
int dropInterval = 500; // 初期落下速度(ミリ秒)
int score = 0;
bool gameOver = false;

// 7種類のテトリミノ (I, J, L, O, S, T, Z) x 4方向の回転状態
// 4x4のグリッドを16ビット(0x0000〜0xFFFF)のビットマップで表現
const uint16_t TETROMINOS[7][4] PROGMEM = {
  // I型
  {0x0F00, 0x2222, 0x00F0, 0x4444},
  // J型
  {0x44C0, 0x8E00, 0x6440, 0x0E20},
  // L型
  {0x4460, 0x0E80, 0xC440, 0x2E00},
  // O型 (回転しても形は同じ)
  {0xCC00, 0xCC00, 0xCC00, 0xCC00},
  // S型
  {0x06C0, 0x8C40, 0x06C0, 0x8C40},
  // T型
  {0x0E40, 0x4C40, 0x4E00, 0x4640},
  // Z型
  {0x0C60, 0x4C80, 0x0C60, 0x4C80}
};

void setup() {
  Serial.begin(9600);
  
  // ボタンの初期化(内部プルアップ抵抗を使用)
  pinMode(BTN_LEFT, INPUT_PULLUP);
  pinMode(BTN_RIGHT, INPUT_PULLUP);
  pinMode(BTN_ROT, INPUT_PULLUP);
  pinMode(BTN_DOWN, INPUT_PULLUP);

  // ディスプレイの初期化
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { 
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // 初期化失敗時は停止
  }
  
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(47, 26);
  display.println(F("TETRIS"));
  display.display();
  delay(1500);

  // 乱数のシードを設定(未接続のアナログピンのノイズを利用)
  randomSeed(analogRead(0));
  
  // 最初のブロックを生成
  nextType = random(7);
  spawnBlock();
}

void loop() {
  if (gameOver) {
    drawGameOver();
    // 任意のボタンを押してリスタート
    if (digitalRead(BTN_ROT) == LOW || digitalRead(BTN_DOWN) == LOW) {
      resetGame();
    }
    return;
  }

  unsigned long currentTime = millis();
  
  // 入力処理
  handleInput();

  // ブロックの自然落下処理
  if (currentTime - lastDropTime > dropInterval) {
    if (!checkCollision(currentType, currentRot, currentX, currentY + 1)) {
      currentY++; // 下へ移動可能なら移動
    } else {
      lockBlock(); // 衝突したら固定
    }
    lastDropTime = currentTime;
  }

  // 画面描画
  drawGame();
}

void handleInput() {
  // 左移動
  if (digitalRead(BTN_LEFT) == LOW) {
    if (!checkCollision(currentType, currentRot, currentX - 1, currentY)) {
      currentX--;
    }
    delay(120); // チャタリング防止と移動速度調整
  }
  // 右移動
  if (digitalRead(BTN_RIGHT) == LOW) {
    if (!checkCollision(currentType, currentRot, currentX + 1, currentY)) {
      currentX++;
    }
    delay(120);
  }
  // 回転
  if (digitalRead(BTN_ROT) == LOW) {
    int nextRot = (currentRot + 1) % 4;
    if (!checkCollision(currentType, nextRot, currentX, currentY)) {
      currentRot = nextRot;
    }
    delay(200); // 回転は少し長めにウェイトを入れる
  }
  // 下降(ソフトドロップ)
  if (digitalRead(BTN_DOWN) == LOW) {
    if (!checkCollision(currentType, currentRot, currentX, currentY + 1)) {
      currentY++;
      score += 1; // 落下させたボーナススコア
    }
    delay(50); // 高速落下
  }
}

// 指定した位置・回転状態でブロックが壁や他のブロックに衝突しないか確認
bool checkCollision(int type, int rot, int x, int y) {
  // PROGMEMからブロックの形状データを読み込む
  uint16_t shape = pgm_read_word(&TETROMINOS[type][rot]);
  
  for (int i = 0; i < 16; i++) {
    // 16ビットのデータから各マスの状態を確認 (1番上の左端から順に確認)
    if (shape & (0x8000 >> i)) {
      int blockX = x + (i % 4);
      int blockY = y + (i / 4);
      
      // 左右の壁、または底に衝突
      if (blockX < 0 || blockX >= BOARD_WIDTH || blockY >= BOARD_HEIGHT) {
        return true; 
      }
      // 既に固定されているブロックに衝突
      if (blockY >= 0 && board[blockY][blockX] != 0) {
        return true;
      }
    }
  }
  return false;
}

// ブロックをボードに固定し、新しいブロックを生成する
void lockBlock() {
  uint16_t shape = pgm_read_word(&TETROMINOS[currentType][currentRot]);
  
  for (int i = 0; i < 16; i++) {
    if (shape & (0x8000 >> i)) {
      int blockX = currentX + (i % 4);
      int blockY = currentY + (i / 4);
      if (blockY >= 0 && blockY < BOARD_HEIGHT && blockX >= 0 && blockX < BOARD_WIDTH) {
        board[blockY][blockX] = 1; // ボードにブロックを書き込む
      }
    }
  }
  
  clearLines(); // 揃ったラインを消去
  spawnBlock(); // 次のブロックを出現
}

void spawnBlock() {
  currentType = nextType;
  nextType = random(7);
  currentRot = 0;
  currentX = BOARD_WIDTH / 2 - 2; // 中央上部へ配置
  currentY = -2; // 画面外(少し上)から出現させる
  
  // 出現した瞬間に衝突する場合はゲームオーバー
  if (checkCollision(currentType, currentRot, currentX, currentY)) {
    gameOver = true;
  }
}

// 横一列が揃っているか確認し、消去する
void clearLines() {
  int linesCleared = 0;
  
  for (int y = BOARD_HEIGHT - 1; y >= 0; y--) {
    bool isFull = true;
    for (int x = 0; x < BOARD_WIDTH; x++) {
      if (board[y][x] == 0) {
        isFull = false;
        break;
      }
    }
    
    if (isFull) {
      linesCleared++;
      // 消去した行より上の行をすべて1段下げる
      for (int moveY = y; moveY > 0; moveY--) {
        for (int x = 0; x < BOARD_WIDTH; x++) {
          board[moveY][x] = board[moveY - 1][x];
        }
      }
      // 1番上の行を空にする
      for (int x = 0; x < BOARD_WIDTH; x++) {
        board[0][x] = 0;
      }
      y++; // 下げた行も再度チェックするため y をインクリメント
    }
  }
  
  // スコアの加算 (複数ライン同時消しでボーナス)
  if (linesCleared == 1) score += 100;
  else if (linesCleared == 2) score += 300;
  else if (linesCleared == 3) score += 500;
  else if (linesCleared >= 4) score += 800;
  
  // スコアに応じて落下速度を上げる (MAX速度 150ms)
  dropInterval = max(150, 500 - (score / 1000) * 50);
}

// 画面全体の描画
void drawGame() {
  display.clearDisplay();

  // ボードの枠を描画
  display.drawRect(BOARD_X - 1, BOARD_Y - 1, 
                   (BOARD_WIDTH * BLOCK_SIZE) + 2, 
                   (BOARD_HEIGHT * BLOCK_SIZE) + 2, 
                   SSD1306_WHITE);

  // 固定されたブロックの描画
  for (int y = 0; y < BOARD_HEIGHT; y++) {
    for (int x = 0; x < BOARD_WIDTH; x++) {
      if (board[y][x] != 0) {
        display.fillRect(BOARD_X + x * BLOCK_SIZE, BOARD_Y + y * BLOCK_SIZE, 
                         BLOCK_SIZE - 1, BLOCK_SIZE - 1, SSD1306_WHITE); // -1 でブロック間の隙間を作る
      }
    }
  }

  // 落下中のブロックの描画
  uint16_t shape = pgm_read_word(&TETROMINOS[currentType][currentRot]);
  for (int i = 0; i < 16; i++) {
    if (shape & (0x8000 >> i)) {
      int blockX = currentX + (i % 4);
      int blockY = currentY + (i / 4);
      if (blockY >= 0) { // 画面外(上部)のブロックは描画しない
        display.fillRect(BOARD_X + blockX * BLOCK_SIZE, BOARD_Y + blockY * BLOCK_SIZE, 
                         BLOCK_SIZE - 1, BLOCK_SIZE - 1, SSD1306_WHITE);
      }
    }
  }

  // UI (スコアと次のブロック) の描画
  display.setCursor(BOARD_X + (BOARD_WIDTH * BLOCK_SIZE) + 10, BOARD_Y);
  display.print(F("SCORE"));
  display.setCursor(BOARD_X + (BOARD_WIDTH * BLOCK_SIZE) + 10, BOARD_Y + 10);
  display.print(score);
  
  display.setCursor(BOARD_X + (BOARD_WIDTH * BLOCK_SIZE) + 10, BOARD_Y + 30);
  display.print(F("NEXT"));
  drawNextPiece(BOARD_X + (BOARD_WIDTH * BLOCK_SIZE) + 10, BOARD_Y + 45);

  display.display();
}

// 次のブロックを描画する関数
void drawNextPiece(int x, int y) {
  uint16_t shape = pgm_read_word(&TETROMINOS[nextType][0]);
  for (int i = 0; i < 16; i++) {
    if (shape & (0x8000 >> i)) {
      int blockX = (i % 4);
      int blockY = (i / 4);
      display.fillRect(x + blockX * BLOCK_SIZE, y + blockY * BLOCK_SIZE, 
                       BLOCK_SIZE - 1, BLOCK_SIZE - 1, SSD1306_WHITE);
    }
  }
}

// ゲームオーバー画面
void drawGameOver() {
  display.clearDisplay();
  display.setCursor(30, 20);
  display.println(F("GAME OVER"));
  display.setCursor(20, 40);
  display.print(F("Score: "));
  display.println(score);
  display.display();
}

// ゲームのリセット処理
void resetGame() {
  // ボードのクリア
  for (int y = 0; y < BOARD_HEIGHT; y++) {
    for (int x = 0; x < BOARD_WIDTH; x++) {
      board[y][x] = 0;
    }
  }
  score = 0;
  dropInterval = 500;
  gameOver = false;
  nextType = random(7);
  spawnBlock();
  delay(500); // 少し待機してから再開
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?