pygameでランダム迷路生成

## pygameでランダム迷路生成

### 概要
今回の記事では、Pygameを使用して、ランダムな迷路を生成するアプリを作成する方法について説明します。迷路生成には幅優先探索アルゴリズムを使用し、生成された迷路はアニメーションとして視覚化されます。

### 機能の概要
1. **Pygameの初期化:** Pygameライブラリを初期化し、ウィンドウを作成します。
2. **迷路の生成:** ランダムな迷路を生成する関数が実装されています。迷路内にはスタートとゴールが配置され、幅優先探索アルゴリズムを使用してスタートからゴールへのパスが確保されます。
3. **迷路の描画:** 生成された迷路は、Pygameを使用してウィンドウ上に描画されます。壁は黒色で、スタートとゴールはそれぞれ緑色と赤色で表示されます。
4. **アニメーション表示:** スタートからゴールまでのパスがアニメーションとして表示されます。各セルはオレンジ色で塗りつぶされ、スタートからゴールまでの経路が示されます。
5. **「ランダム作成」ボタン:** ボタンをクリックすると、新しいランダムな迷路が生成され、アニメーションが再生されます。
<iframe width="560" height="315" src="https://www.youtube.com/embed/5mYLakbJzgs?si=wV8sXX9SYZ4hO9WY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

### 以下コード

```python
import pygame
import random
from collections import deque

# Pygameの初期化
pygame.init()

# ウィンドウの設定
WIDTH, HEIGHT = 800, 600
window = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Random Maze Generator")

# 色の定義
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
ORANGE = (255, 165, 0)  # オレンジ色

# 定数の定義
CELL_SIZE = 20
GRID_WIDTH = WIDTH // CELL_SIZE
GRID_HEIGHT = HEIGHT // CELL_SIZE

# ボタンのrectを保持するグローバル変数
button_rect = None


# スタートからゴールへのパスがあるようにランダムな迷路を生成する関数
def generate_maze():
    """
    ランダムな迷路を生成します。

    Returns:
        maze (list): 生成された迷路のマトリックス。
        start (tuple): スタート位置の座標 (x, y)。
        goal (tuple): ゴール位置の座標 (x, y)。
    """
    maze = [
        [random.choice([0, 1]) for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)
    ]

    # スタートとゴールの座標を設定
    start_x, start_y = random.randint(1, GRID_WIDTH - 2), random.randint(
        1, GRID_HEIGHT - 2
    )
    goal_x, goal_y = random.randint(1, GRID_WIDTH - 2), random.randint(
        1, GRID_HEIGHT - 2
    )

    # スタートとゴールが壁でないようにする
    maze[start_y][start_x] = 0
    maze[goal_y][goal_x] = 0

    # スタートからゴールへのパスが存在することを確認する
    while not bfs(maze, (start_x, start_y), (goal_x, goal_y)):
        maze = [
            [random.choice([0, 1]) for _ in range(GRID_WIDTH)]
            for _ in range(GRID_HEIGHT)
        ]
        maze[start_y][start_x] = 0
        maze[goal_y][goal_x] = 0

    return maze, (start_x, start_y), (goal_x, goal_y)


# 幅優先探索アルゴリズムを使用して、スタートからゴールへのパスを見つける関数
def bfs(maze, start, goal):
    """
    幅優先探索アルゴリズムを使用して、迷路内のスタートからゴールへのパスを見つけます。

    Args:
        maze (list): 迷路のマトリックス。
        start (tuple): スタート位置の座標 (x, y)。
        goal (tuple): ゴール位置の座標 (x, y)。

    Returns:
        path (list): スタートからゴールまでのパスが含まれるセルの座標リスト。
    """
    visited = set()
    queue = deque([start])
    parent = {}  # ノードごとの親ノードを記録する辞書

    while queue:
        current = queue.popleft()
        if current == goal:
            # ゴールに到達したら、経路を返す
            path = []
            while current != start:
                path.append(current)
                current = parent[current]
            path.append(start)
            path.reverse()
            return path
        visited.add(current)
        neighbors = [
            (current[0] + dx, current[1] + dy)
            for dx, dy in [(1, 0), (-1, 0), (0, 1), (0, -1)]
        ]
        for neighbor in neighbors:
            if (
                0 <= neighbor[0] < GRID_WIDTH
                and 0 <= neighbor[1] < GRID_HEIGHT
                and maze[neighbor[1]][neighbor[0]] == 0
                and neighbor not in visited
            ):
                queue.append(neighbor)
                parent[neighbor] = current
    return False


# アニメーションとしてパスを描画する関数
def animate_path(maze, path, start, goal):
    """
    パスをアニメーションとして描画します。

    Args:
        maze (list): 迷路のマトリックス。
        path (list): スタートからゴールまでのパスが含まれるセルの座標リスト。
        start (tuple): スタート位置の座標 (x, y)。
        goal (tuple): ゴール位置の座標 (x, y)。
    """
    draw_maze(maze)  # 迷路を描画
    draw_start_goal(start, goal)  # スタートとゴールの点を描画

    for cell in path[1:-1]:  # スタートとゴールを除く経路の各セルについて
        pygame.draw.rect(
            window,
            ORANGE,
            (
                cell[0] * CELL_SIZE,
                cell[1] * CELL_SIZE,
                CELL_SIZE,
                CELL_SIZE,
            ),
        )
        pygame.time.wait(50)  # アニメーションの速度調整
        pygame.display.flip()

        # イベント処理
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                quit()

    # スタートとゴールを結ぶ線を描画
    pygame.draw.line(
        window,
        ORANGE,
        (start[0] * CELL_SIZE + CELL_SIZE // 2, start[1] * CELL_SIZE + CELL_SIZE // 2),
        (goal[0] * CELL_SIZE + CELL_SIZE // 2, goal[1] * CELL_SIZE + CELL_SIZE // 2),
        width=5,
    )
    pygame.display.flip()


# 迷路を描画する関数
def draw_maze(maze):
    """
    迷路を描画します。

    Args:
        maze (list): 迷路のマトリックス。
    """
    window.fill(WHITE)  # 背景を白で塗りつぶす
    for y in range(GRID_HEIGHT):
        for x in range(GRID_WIDTH):
            if maze[y][x] == 1:
                pygame.draw.rect(
                    window, BLACK, (x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE)
                )


# スタートとゴールの点を描画する関数
def draw_start_goal(start, goal):
    """
    スタートとゴールの点を描画します。

    Args:
        start (tuple): スタート位置の座標 (x, y)。
        goal (tuple): ゴール位置の座標 (x, y)。
    """
    start_x, start_y = start
    goal_x, goal_y = goal
    pygame.draw.circle(
        window,
        (0, 255, 0),
        (start_x * CELL_SIZE + CELL_SIZE // 2, start_y * CELL_SIZE + CELL_SIZE // 2),
        CELL_SIZE // 4,
    )
    pygame.draw.circle(
        window,
        (255, 0, 0),
        (goal_x * CELL_SIZE + CELL_SIZE // 2, goal_y * CELL_SIZE + CELL_SIZE // 2),
        CELL_SIZE // 4,
    )


# 「ランダム作成」ボタンを描画する関数
def draw_button():
    """
    「ランダム作成」ボタンを描画します。
    """
    global button_rect  # グローバル変数を更新するために必要
    button_font = pygame.font.SysFont(None, 30)
    button_text = button_font.render("random create", True, BLACK)
    button_rect = button_text.get_rect(center=(WIDTH // 2, HEIGHT - 50))
    pygame.draw.rect(window, WHITE, button_rect)
    window.blit(button_text, button_rect)


# メインのゲームループ
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.MOUSEBUTTONDOWN:
            if button_rect and button_rect.collidepoint(
                event.pos
            ):  # ボタンが存在し、クリックされた場合
                button_rect = None  # ボタンを消去
                maze, start, goal = generate_maze()  # 迷路を生成
                path = bfs(maze, start, goal)  # 幅優先探索を実行
                if path:
                    animate_path(maze, path, start, goal)  # 経路をアニメーション表示

    draw_button()  # ボタンを描画
    pygame.display.flip()

# Pygameを終了する
pygame.quit()
```
### 最後まで見てくれてありがとうございます。