Python プログラミング教室

2025年9月13日の実践コースはPython(Pygame)の続き

前回に続いてPythonでゲームを作っていきます。
前回までに作ったプログラムをきれいにしましたので、以下のプログラムからスタートしたいと思います。

こちらのコードを活用してテトリスのような落ちもの系パズルゲームを作っていきます。

"""落ちものゲームの土台:グリッド表示+1マスのスプライト
中学生向けに、読みやすいコメントと構成にしています。
- 画面左側に白い盤面(10x20マス)を描画
- キー操作で1マス(スプライト)を上下左右に移動
"""

from __future__ import annotations

import sys
import pygame

# ============================================================================
# 定数(ゲーム全体で使う値)
# ============================================================================

FPS = 60                      # 1秒あたりの描画回数(フレームレート)
CELL = 30                     # 1マスの大きさ(ピクセル)
COLS, ROWS = 10, 20           # 盤面の列数(横)、行数(縦)
PANEL_WIDTH = 240             # 右側の情報パネルの幅(ピクセル)

# 画面サイズ(横幅は「盤面の横幅 + パネル幅」)
WIDTH = CELL * COLS + PANEL_WIDTH
HEIGHT = CELL * ROWS

# 色(RGB)
WHITE = (255, 255, 255)
GRAY = (200, 200, 200)
BLACK = (0, 0, 0)
BLUE = (0, 120, 255)


# ============================================================================
# クラス定義
# ============================================================================

class CellBlock(pygame.sprite.Sprite):
    """盤面上の『1マス』を表すスプライト。

    grid_x, grid_y は「マスの座標」(ピクセルではない点に注意)。
    画面上のピクセル位置は update_rect() で計算して rect に反映します。
    """
    def __init__(self, grid_x: int, grid_y: int, color: tuple[int, int, int], cell_size: int) -> None:
        super().__init__()
        # -1 するのは、グリッド線と重なって見えづらくならないための微調整
        self.image = pygame.Surface((cell_size - 1, cell_size - 1))
        self.image.fill(color)

        self.rect = self.image.get_rect()
        self.grid_x = grid_x
        self.grid_y = grid_y
        self.cell_size = cell_size

        self.update_rect()

    def update_rect(self) -> None:
        """グリッド座標(マス)→ 画面上のピクセル座標に変換して配置する。"""
        self.rect.topleft = (self.grid_x * self.cell_size, self.grid_y * self.cell_size)


# ============================================================================
# 描画用関数
# ============================================================================

def draw_board(screen: pygame.Surface) -> None:
    """左側の盤面(白背景+マス目の線)を描画する。"""
    # 盤面の白い背景(左側だけを塗る)
    screen.fill(WHITE, (0, 0, CELL * COLS, HEIGHT))

    # 薄いグリッド線(縦線)
    for x in range(COLS + 1):
        pygame.draw.line(screen, GRAY, (x * CELL, 0), (x * CELL, HEIGHT))

    # 薄いグリッド線(横線)
    for y in range(ROWS + 1):
        pygame.draw.line(screen, GRAY, (0, y * CELL), (CELL * COLS, y * CELL))


# ============================================================================
# メイン処理
# ============================================================================

def main() -> None:
    """ゲームの初期化・イベント処理・描画を行うメインループ。"""
    pygame.init()
    pygame.display.set_caption("落ちゲー")
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    clock = pygame.time.Clock()

    # スプライトのグループ(描画や更新をまとめて行える)
    all_blocks = pygame.sprite.Group()

    # 例:中央付近(x=4, y=2)に青いブロックを1つ置く
    block = CellBlock(grid_x=4, grid_y=2, color=BLUE, cell_size=CELL)
    all_blocks.add(block)

    running = True
    while running:
        dt = clock.tick(FPS)  # 経過ミリ秒(将来、落下速度などに使える)
        for event in pygame.event.get():
            # ウィンドウ右上の「×」ボタンで終了
            if event.type == pygame.QUIT:
                running = False

            # キーが押されたとき:1マスずつ動かす(盤面の外に出ないように制限)
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_LEFT and block.grid_x > 0:
                    block.grid_x -= 1
                    block.update_rect()
                elif event.key == pygame.K_RIGHT and block.grid_x < COLS - 1:
                    block.grid_x += 1
                    block.update_rect()
                elif event.key == pygame.K_UP and block.grid_y > 0:
                    block.grid_y -= 1
                    block.update_rect()
                elif event.key == pygame.K_DOWN and block.grid_y < ROWS - 1:
                    block.grid_y += 1
                    block.update_rect()

        # 背景を濃いグレーで塗り、盤面(白+グリッド)を描く
        screen.fill((30, 30, 30))
        draw_board(screen)

        # ブロック(スプライト)を描画
        all_blocks.draw(screen)

        # 画面に反映
        pygame.display.flip()

    # 後片付け
    pygame.quit()
    sys.exit()


# スクリプトとして直接実行されたときだけ main() を呼ぶ
if __name__ == "__main__":
    main()

ステップ1 1マスのスプライトを登場させる

ステップ1はこちらの記事を参照してください。

ステップ2 4マスのピースを登場させる

1) 形データと色の候補を追加

(ファイル先頭の定数付近に追記。import random も忘れずに)

# ★先頭のインポート部分に以下のコードを追加
import random

# ★定数部分に以下のコードを追加
COLORS = [
    (0,180,255), (255,200,0), (170,0,255),
    (0,200,80), (255,100,0), (255,0,100), (0,120,255)
]

# 4つの相対座標で形を表現(テトリスの7種)
SHAPES = {
    "I": [(-2,0),(-1,0),(0,0),(1,0)],
    "O": [(-1,0),(0,0),(-1,1),(0,1)],
    "T": [(-1,0),(0,0),(1,0),(0,1)],
    "S": [(0,0),(1,0),(-1,1),(0,1)],
    "Z": [(-1,0),(0,0),(0,1),(1,1)],
    "J": [(-1,0),(-1,1),(0,0),(1,0)],
    "L": [(-1,0),(0,0),(1,0),(1,1)],
}

2) ピース(4マスの束)クラスを追加

CellBlock を4つ内包し、移動回転(簡易壁キック付き)ができます。

class Piece:
    def __init__(self, kind=None, color=None):
        self.kind = kind or random.choice(list(SHAPES.keys()))
        self.color = color or random.choice(COLORS)
        self.x = COLS // 2   # 盤面中央から出現
        self.y = 1
        self.offsets = [list(p) for p in SHAPES[self.kind]]  # 形の相対座標
        # 4つのCellBlockを作ってグループに入れる
        self.blocks = []
        self.group = pygame.sprite.Group()
        for (ox, oy) in self.offsets:
            b = CellBlock(self.x + ox, self.y + oy, self.color, CELL)
            self.blocks.append(b)
            self.group.add(b)

    def world_cells(self):
        return [(self.x + ox, self.y + oy) for (ox, oy) in self.offsets]

    def _hits_wall(self):
        # 盤面の外に出ていないか(まだ固定ブロックとの衝突は見ない)
        for (gx, gy) in self.world_cells():
            if not (0 <= gx < COLS and 0 <= gy < ROWS):
                return True
        return False

    def update_sprites(self):
        # grid座標 → 画面座標に反映
        for i, (ox, oy) in enumerate(self.offsets):
            self.blocks[i].grid_x = self.x + ox
            self.blocks[i].grid_y = self.y + oy
            self.blocks[i].update_rect()

    def move(self, dx, dy):
        old_x, old_y = self.x, self.y
        self.x += dx; self.y += dy
        if self._hits_wall():
            self.x, self.y = old_x, old_y
            return False
        self.update_sprites()
        return True

    def rotate_cw(self):
        # 時計回り (x,y)->(-y,x)、壁キック(左右に少しずらして試す)
        return self._rotate([[-oy, ox] for (ox, oy) in self.offsets])

    def rotate_ccw(self):
        # 反時計回り (x,y)->(y,-x)
        return self._rotate([[oy, -ox] for (ox, oy) in self.offsets])

    def _rotate(self, new_offsets):
        old_offsets, old_x = self.offsets, self.x
        self.offsets = new_offsets
        for dx in [0, -1, 1, -2, 2]:
            self.x = old_x + dx
            if not self._hits_wall():
                self.update_sprites()
                return True
        # 無理なら元に戻す
        self.offsets, self.x = old_offsets, old_x
        return False

    def draw(self, screen):
        self.group.draw(screen)

3) main() を少し変更

  • これまでの「単体ブロック」は不要にして、ピースを1つ出す
  • 矢印キーで移動Z回転
def main():
    pygame.init()
    pygame.display.set_caption("落ちゲー")
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    clock = pygame.time.Clock()

    # いずれ固定ブロックを入れるためのグループ(今は空)
    fixed_blocks = pygame.sprite.Group()

    # 今落ちているピースを1つ生成
    current = Piece()  # 種類はランダム

    running = True
    while running:
        dt = clock.tick(FPS)
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_LEFT:
                    current.move(-1, 0)
                elif event.key == pygame.K_RIGHT:
                    current.move(1, 0)
                elif event.key == pygame.K_DOWN:
                    current.move(0, 1)
                elif event.key == pygame.K_UP:
                    current.rotate_cw()
                elif event.key == pygame.K_z:
                    current.rotate_ccw()

        screen.fill((30, 30, 30))
        draw_board(screen)

        # (将来用)固定ブロックを描画
        fixed_blocks.draw(screen)
        # 現在のピースを描画
        current.draw(screen)

        pygame.display.flip()

    pygame.quit()
    sys.exit()

動作チェック

  • 4マスのピースが中央に出現
  • ←→↓ で動く/↑とZで回転(端っこで回転しても簡単な壁キックで入る)

次は「自動で落ちる(重力)→止まったら固定する → 新しいピースが出る」を実装します。

-Python, プログラミング教室