MicroPythonとRP2040-zeroで作るテトリス風ゲーム

blocks-fallのハード構成
blocks-fallのハード構成

本記事では、ChatGPTとRP2040-zeroと SSD1306 OLED ディスプレイを使い、MicroPython で実装したテトリス風ゲームのプログラムを紹介します。
プログラムは、各ブロックの落下、左右移動、回転(ウォールキック付き)、ラインクリア、そしてゲームオーバー判定などの機能を実現しています。
以下、各関数ごとに実装内容と工夫点を解説していきます。

https://github.com/bomura/blocks-fall


目次

ゲーム全体の構成と設定

ハードウェアとディスプレイ設定

  • I2C通信でSSD1306ディスプレイを接続。
  • 画面の向きを合わせるために、ディスプレイの論理サイズを DISPLAY_WIDTH = 32DISPLAY_HEIGHT = 128 として、UIエリアとゲームエリアに分割しています。
  • UIエリアは左側(x座標 0~7)にスコアや次ブロックを表示し、残りの部分をゲームプレイエリアとしています。
  • 今回のディスプレイは、UIエリア部分がオレンジ、ゲームプレイエリアはブルーとなっています。
blocks-fallのハード構成
blocks-fallのハード構成

ゲームフィールドの設定

  • ブロックのサイズを BLOCK_SIZE = 3 として設定。
  • ゲームフィールドは論理的に FIELD_ROWS × FIELD_COLS のグリッドとなり、各セルは 3×3 ピクセルで描画されます。

ブロックの定義

BLOCKS 辞書では、各ブロック(I, U, T, S, Z, J, L)の形状を、回転状態ごとの座標リストとして定義しています。
これにより、回転ごとに異なるブロックの形状を簡単に扱えるようにしています。

ブロックの形状定義(各回転状態)
BLOCKS = {
    'I': [
        [(0, 1), (1, 1), (2, 1), (3, 1)],
        [(2, 0), (2, 1), (2, 2), (2, 3)],
        [(0, 2), (1, 2), (2, 2), (3, 2)],
        [(1, 0), (1, 1), (1, 2), (1, 3)]
    ],
    'U': [
        [(0, 0), (0, 1), (0, 2), (1, 0), (1, 2)],
        [(0, 0), (0, 1), (1, 0), (2, 0), (2, 1)],
        [(0, 0), (0, 2), (1, 0), (1, 1), (1, 2)],
        [(0, 0), (0, 1), (1, 1), (2, 0), (2, 1)]
    ],
    'T': [
        [(0, 1), (1, 0), (1, 1), (1, 2)],
        [(0, 1), (1, 0), (1, 1), (2, 1)],
        [(0, 0), (0, 1), (0, 2), (1, 1)],
        [(0, 1), (1, 1), (1, 2), (2, 1)]
    ],
    'S': [
        [(0, 1), (0, 2), (1, 0), (1, 1)],
        [(0, 0), (1, 0), (1, 1), (2, 1)]
    ],
    'Z': [
        [(0, 0), (0, 1), (1, 1), (1, 2)],
        [(0, 1), (1, 0), (1, 1), (2, 0)]
    ],
    'J': [
        [(0, 0), (1, 0), (1, 1), (1, 2)],
        [(0, 1), (1, 1), (2, 0), (2, 1)],
        [(0, 0), (0, 1), (0, 2), (1, 2)],
        [(0, 0), (0, 1), (1, 0), (2, 0)]
    ],
    'L': [
        [(0, 0), (0, 1), (1, 0), (2, 0)],
        [(0, 0), (1, 0), (1, 1), (1, 2)],
        [(0, 1), (1, 1), (2, 0), (2, 1)],
        [(0, 0), (0, 1), (0, 2), (1, 2)]
    ]
}

各関数の解説

can_move(piece, drow, dcol)

この関数は、落下中のブロックが指定された移動量(drow, dcol)を加えたとき、

  • ゲームフィールドの範囲外になっていないか
  • 既に固定されたブロックと衝突していないか
    をチェックします。
    移動可能なら True、不可能なら False を返すため、ブロックの移動や回転処理で必ず呼び出されます。
落下中のブロックが、指定の移動量 (drow, dcol) で移動可能かチェックする
def can_move(piece, drow, dcol):
    for (dr, dc) in piece['shape']:
        new_row = piece['row'] + dr + drow
        new_col = piece['col'] + dc + dcol
        if new_col < 0 or new_col >= FIELD_COLS or new_row < 0 or new_row >= FIELD_ROWS:
            return False
        if game_field[new_row][new_col]:
            return False
    return True

3.2. fix_piece(piece)

ブロックがこれ以上下に落下できない場合、現在の位置でブロックをゲームフィールドに固定します。
固定後、ゲームフィールドの該当セルに「1」をセットすることで、以降は動かないブロックとして扱います。

落下中のブロックをゲームフィールドに固定する
def fix_piece(piece):
    for (dr, dc) in piece['shape']:
        r = piece['row'] + dr
        c = piece['col'] + dc
        if 0 <= r < FIELD_ROWS and 0 <= c < FIELD_COLS:
            game_field[r] = 1

3.3. spawn_piece()

新たなブロックを生成する関数です。

  • BLOCKS 辞書からランダムにブロックの種類と回転状態を選択
  • 選ばれた形状の横幅に合わせ、初期位置(ここではフィールド中央付近)を決定
    この関数によって、ゲーム開始時やブロック固定後に次のブロックが用意されます。
ブロックを生成
def spawn_piece():
    word, shapes = random.choice(list(BLOCKS.items()))
    rotation = random.randint(0, len(shapes) - 1)
    shape = shapes[rotation]                               
    width = max(dc for (_, dc) in shape) + 1
    col = FIELD_COLS // 2
    return {
        'row': 0,
        'col': col,
        'word': word,
        'rotation': rotation,
        'shape': shape
    }

3.4. rotate_piece(piece)

ブロックの回転処理を行います。

  • 回転後の形状を取得し、現在の回転状態から1つ先の状態に更新
  • しかし、端付近などで回転すると壁にめり込んでしまう場合があります。そこでウォールキック処理を実装。
    • オフセット候補(例:左右に1~2セルずらす)を順に試し、can_move で衝突がなくなる位置を探します。
    • 有効な位置が見つかればその位置と回転状態を確定し、そうでなければ元の状態に戻します。
ブロックを回転させ、壁にめり込む場合はオフセットを試みるウォールキック処理を行う
def rotate_piece(piece):
    shapes = BLOCKS[piece['word']]
    new_rotation = (piece['rotation'] + 1) % len(shapes)
    new_shape = shapes[new_rotation]
    # オフセット候補(例として左右に1,2セルずつずらす)
    offsets = [(0, 0), (0, -1), (0, 1), (0, -2), (0, 2)]
    original_row = piece['row']
    original_col = piece['col']
    for drow, dcol in offsets:
        # 一時的に新形状とオフセットを適用
        piece['row'] = original_row + drow
        piece['col'] = original_col + dcol
        piece['shape'] = new_shape
        if can_move(piece, 0, 0):
            piece['rotation'] = new_rotation
            return  # 有効な位置が見つかったので確定
    # オフセット候補のどれも有効でなければ、元の位置と形状に戻す
    piece['row'] = original_row
    piece['col'] = original_col

3.5. clear_lines()

ゲームフィールド内の各行をチェックし、全セルが埋まっている行を削除する関数です。

  • 削除した行数分、フィールドの上部に空行を挿入することで、ブロックが上にシフトします。
  • また、消去した行数に応じてスコアを加算します。
ラインを削除する関数
def clear_lines():
    """
    各行をチェックし、全セルが埋まっている行を削除する
    削除した行数に応じてスコアを加算し、上部に空行を挿入する
    """
    global game_field, score
    cleared_lines = 0
    new_field = []
    for row in game_field:
        if all(cell == 1 for cell in row):
            cleared_lines += 1
        else:
            new_field.append(row)
    for _ in range(cleared_lines):
        new_field.insert(0, [0] * FIELD_COLS)
    game_field = new_field
    score += 100 * cleared_lines

3.6. draw_ui(piece)

UIエリア(画面左側)にスコアと次ブロックのプレビューを描画する関数です。

  • display.text を使ってスコアを表示し、次ブロックは小さな形状でプレビューするように描画しています。
  • 次ブロックは、実際には next_piece で生成されたものを渡すようにしています。
UIエリアの描画(スコア、次ブロックのプレースホルダー)
def draw_ui(piece):
    display.fill_rect(0, 0, DISPLAY_HEIGHT, UI_WIDTH, 0)
    display.text("Score:%d" % score, UI_WIDTH+CELL_H, 0, 1)
    # 次ブロックのプレースホルダー(必要に応じて実際の次ブロック表示に拡張)
    for (dr, dc) in piece['shape']:
        x = dr * CELL_H
        y = dc * CELL_W
        display.fill_rect(x, y, CELL_W, CELL_H, 1)

3.7. draw_game_field(piece)

ゲームエリアに、固定されたブロックと現在落下中のブロックを描画する関数です。

  • ゲームフィールド内の各セルを、指定されたセルサイズに合わせてディスプレイに描画します。
  • 落下中のブロックは、現在の rowcol に各ブロックの形状のオフセットを加えた位置に描画されます。
ゲームエリアの描画(固定ブロックと落下中ブロック)
def draw_game_field(piece):
    """
    ゲームエリアの描画(固定ブロックと落下中ブロック)
    マッピング: ゲームフィールド (r, c) → 表示座標
              x = r * CELL_H
              y = UI_WIDTH + c * CELL_W
    """
    display.fill_rect(0, UI_WIDTH, GAME_AREA_HEIGHT, GAME_AREA_WIDTH, 0)
    for r in range(FIELD_ROWS):
        for c in range(FIELD_COLS):
            if game_field[r]:
                x = r * CELL_H
                y = UI_WIDTH + c * CELL_W
                display.fill_rect(x, y, CELL_W, CELL_H, 1)
    for (dr, dc) in piece['shape']:
        r = piece['row'] + dr
        c = piece['col'] + dc
        x = r * CELL_H
        y = UI_WIDTH + c * CELL_W
        display.fill_rect(x, y, CELL_W, CELL_H, 1)

ゲームループと全体の流れ

  • 入力処理:左右移動ボタンと回転ボタンの入力を監視し、適宜 can_moverotate_piece を呼び出してブロックの移動や回転を行います。
  • 落下処理:一定時間ごとにブロックを1行下に落とし、移動できなければ fix_piece で固定。
  • ラインクリアと次ブロック生成:ブロック固定後、clear_lines でラインをチェックし、次ブロックを spawn_piece で生成します。
  • ゲームオーバー判定:固定後にフィールド最上段にブロックがある場合はゲームオーバーとし、最終スコアと「GAME OVER」を表示します。

参考

【sky】テトリスのテーマ BGM コロブチカ ドレミ楽譜 – co sky

BEEP音の鳴らし方 | arduinoロボットプログラミング | クムクムまなびのひろば

まとめ

今回のプログラムでは、MicroPython と SSD1306 を利用して、テトリス風の主要機能(ブロックの落下、移動、回転、ライン消去、ゲームオーバー判定)を実装しました。
特に、回転処理でのウォールキックは、端付近でも自然な動作を実現するための重要な工夫です。
このプログラムを基に、さらなる機能(ブロックの高速落下、回転アルゴリズムの強化、スコアボーナスなど)を追加して、オリジナルのテトリス風ゲームを完成させてください!

2025-04-05DIY&修理

Posted by 納戸 工房