PythonのArcade による六角形ゲームの終了処理実装をしてみた！

## 概要
このファイルは六角形マップ上でユニットを動かすゲームの終了処理を実装したものです。主に以下の2つのクラスで構成されています：

1. **GameEndPopup クラス**
   - ゲーム終了時のポップアップウィンドウを管理
   - 半透明の背景、枠線、テキストメッセージを表示

2. **MyHexMap クラス**
   - メインのゲームウィンドウを管理
   - 六角形マップの描画とユニットの操作を制御
   - ゲーム終了条件の判定と終了処理を実装

## 実装のポイント

### 1. ゲーム終了の判定と状態管理
```python
class MyHexMap:
    def __init__(self):
        # ゲーム終了状態を管理するフラグ
        self.is_game_ended = False

    def on_mouse_release(self):
        # ユニットが(0,0)に到達したらゲーム終了
        if self.select_unit.position == (0,0):
            self.is_game_ended = True
```

### 2. ポップアップの表示制御
```python
class GameEndPopup:
    def __init__(self, width, height):
        # ポップアップを画面中央に配置
        self.box_width = 400
        self.box_height = 200
        self.box_x = width // 2 - self.box_width // 2
        self.box_y = height // 2 - self.box_height // 2
```

### 3. ゲーム終了後の操作制限
```python
def on_mouse_release(self, x, y, button, modifiers):
    # ゲーム終了時はユニット移動を無効化
    if self.is_game_ended:
        return
```

### 4. 視覚的フィードバック
- 半透明の背景（RGBA: 200, 200, 200, 200）でゲーム画面を薄暗く
- 中央配置された白いポップアップボックス
- 黒い枠線とテキストで勝利メッセージを表示
- ゲーム終了時はユニットの選択枠を非表示に

### 5. 連携ポイント
- `Hexmap` クラスと連携して六角形マップを管理
- `MyUnit` クラスと連携してユニットの位置情報を管理
- `arcade` ライブラリを使用して描画処理を実装

このような構造により、ゲーム終了時の状態管理と視覚的なフィードバックを効果的に実現しています。

# コードの全体像
**main.py**
```python
import arcade
from components.hexmap import Hexmap
from components.myunit import MyUnit

WIDTH = 800
HEIGHT = 600
RADIUS = 30 # 半径
GRID_SIZE = 10 # hexの個数
TITLE = "test_hexmap"
UNIT_FILEPATH = "hogehoge.png" # 好きな画像のpath
UNIT_SCALE = 0.3
UNIT_SPEED = 100

class GameEndPopup:
    def __init__(self, width, height):
        # ポップアップの位置とサイズ
        self.box_width = 400
        self.box_height = 200
        self.box_x = width // 2 - self.box_width // 2
        self.box_y = height // 2 - self.box_height // 2
        
    def draw(self):
        # 半透明の背景
        arcade.draw_rectangle_filled(
            center_x=self.box_x + self.box_width // 2,
            center_y=self.box_y + self.box_height // 2,
            width=self.box_width,
            height=self.box_height,
            color=(200, 200, 200, 200)
        )
        
        # ボックスの枠線
        arcade.draw_rectangle_outline(
            center_x=self.box_x + self.box_width // 2,
            center_y=self.box_y + self.box_height // 2,
            width=self.box_width,
            height=self.box_height,
            color=arcade.color.BLACK,
            border_width=2
        )
        
        # テキストの表示
        arcade.draw_text(
            text = "勝利条件を達成しました。\nプレイヤーAの勝利",
            start_x = self.box_x + self.box_width // 2,
            start_y = self.box_y + self.box_height // 2,
            color = arcade.color.BLACK,
            font_size=15,
            font_name=("calibri", "Hiragino Sans"),
            anchor_x="center",
            anchor_y="center",
            align="center",
            width=self.box_width
        )

class MyHexMap(arcade.Window):
    def __init__(self, width, height, title):
        super().__init__(width, height, title)
        self.hex_map = Hexmap(radius=RADIUS, grid_size=GRID_SIZE)
        self.unit_list = arcade.SpriteList()
        self.select_unit = None
        self.target_hex = None
        self.mouse_point = None
        self.hex_point = None
        self.game_end_popup = None
        self.is_game_ended = False  # 追加：ゲーム終了フラグ
        
    def setup(self):
        arcade.set_background_color(arcade.color.AMAZON)
        
        # ユニットを複数配置
        unit1 = MyUnit(filename=UNIT_FILEPATH, scale=UNIT_SCALE
                    , speed=UNIT_SPEED, name="unit1"
                    , attack=10, defence=5)
        unit1.center_x = self.width-200
        unit1.center_y = self.height-200
        self.unit_list.append(unit1)
        self.game_end_popup = GameEndPopup(self.width, self.height)
    
    def on_draw(self):
        arcade.start_render()
        self.hex_map.draw_hexagon(on_mouse_coordinates=self.mouse_point)
        self.unit_list.draw()
        
        # 選択されたユニットの周りに枠を描画
        if self.select_unit and not self.is_game_ended:  # ゲーム終了時は選択枠を表示しない
            arcade.draw_rectangle_outline(
                center_x=self.select_unit.center_x,
                center_y=self.select_unit.center_y,
                width=self.select_unit.width + 10,
                height=self.select_unit.height + 10,
                color=arcade.color.RED,
                border_width=3
            )
        
        # ゲーム終了フラグが立っている場合、ポップアップを表示
        if self.is_game_ended:
            self.game_end_popup.draw()
    
    def on_key_press(self, symbol, modifiers):
        if symbol == arcade.key.Q:
            print("Q key press")
            arcade.close_window()
        
    
    def on_mouse_press(self, x, y, button, modifiers):
        print(f"star func on_mouse_press")
        # マウスを押したら、hex座標の位置を取得して表示
        self.hex_point = self.hex_map.get_hex_center_position(mouse_x=x, mouse_y=y)
        if self.hex_point:
            print(f"Clicked hex point {self.hex_point}")
        
        # マウスを押したら、特定のポイントにあるスプライトを取得
        unit_hit = arcade.get_sprites_at_point(point=(x, y), sprite_list=self.unit_list)
        if unit_hit:
            self.select_unit = unit_hit[0] # [0]はスプライトのため、MyUnitのメソッドが使える⇨従って、この時current_hexメソッドは有効
            self.select_unit.alpha = 128 # ユニットが半透明
            print(f"選択したユニットの位置：{self.select_unit.position}")
    
    def on_mouse_release(self, x, y, button, modifiers):
        if self.is_game_ended:  # ゲーム終了時はユニット移動を無効化
            return
            
        print(f"star func on_mouse_release")
        if self.select_unit:
            target_hex = self.hex_map.get_hex_center_position(mouse_x=x, mouse_y=y)
            if target_hex:
                self.select_unit.center_x = target_hex[0]
                self.select_unit.center_y = target_hex[1]
                self.select_unit.current_hex = target_hex
                self.select_unit.alpha = 255
                
                # ユニットが(0,0)に到達したらゲーム終了フラグを立てる
                if self.select_unit.position == (0,0):
                    self.is_game_ended = True
                    
            self.select_unit = None
    
    def on_mouse_motion(self, x, y, dx, dy):
        self.mouse_point = (x, y)
        if self.select_unit:
            self.select_unit.center_x = x
            self.select_unit.center_y = y

def main():
    window = MyHexMap(WIDTH, HEIGHT, TITLE)
    window.setup()
    arcade.run()

if __name__ == "__main__":
    main()
```

**hexmap.py**
```python
import arcade
import math
from utils.helper import calcutate_distance

class Hexmap:
    """
    六角形グリッドマップを生成、描画するクラス。
    ハイライト表示は、3hex分

    Attributes:
        radius (float): 六角形の半径。
        grid_size (int): グリッドのサイズ (行と列の数)。
        hex_centers (list): 各六角形の中心座標を格納するリスト。
    """
    def __init__(self, radius, grid_size):
        """
        Hexmapクラスのコンストラクタ。

        Args:
            radius (float): 六角形の半径。
            grid_size (int): グリッドのサイズ。
        """
        self.radius = radius
        self.grid_size = grid_size
        self.hex_centers = []

    # 頂点の計算
    def hexagon_vertices(self, x, y):
        """
        六角形の頂点座標を計算する。

        Args:
            x (float): 六角形の中心x座標。
            y (float): 六角形の中心y座標。

        Returns:
            vertices: 六角形の頂点座標のリスト。
        """
        vertices = []
        for i in range(6):
            angle = i * math.pi / 3
            vx = x + self.radius * math.cos(angle)
            vy = y + self.radius * math.sin(angle)
            vertices.append((vx, vy))
        return vertices

    # ヘックスマップの描画
    def draw_hexagon(self
                    , highlight_coordinates=None
                    , attack_coordinates=None
                    , on_mouse_coordinates=None):
        """
        六角形グリッドマップを描画する。

        Args:
            highlight_coordinates (tuple): ハイライトを表示するために必要なX,Y座標 例）クリックした位置から3hex分の周囲をハイライト表示したい場合は、クリックした位置のX,Y座標
            on_mouse_coordinates (tuple):  マウスのX,Y座標
        """
        root_3 = math.sqrt(3)
        horizontal_spacing = 3 * self.radius / 2
        vertical_spacing = root_3 * self.radius

        for row in range(self.grid_size):
            for col in range(self.grid_size):
                x = col * horizontal_spacing
                y = row * vertical_spacing

                if col % 2 == 1:
                    y += vertical_spacing / 2
                self.hex_centers.append((x, y))
                hex_vertices_list = self.hexagon_vertices(x, y)

                self.draw_single_hexagon(x, y, hex_vertices_list, highlight_coordinates, attack_coordinates, on_mouse_coordinates)

    # ハイライトの表示
    def draw_single_hexagon(self, x, y, hex_vertices_list, highlight_coordinates, attack_coordinates, on_mouse_coordinates):
        """
        個々の六角形を描画する。ハイライトの有無を判断。

        Args:
            x (float): 六角形の中心x座標。
            y (float): 六角形の中心y座標。
            hex_vertices_list (list): 六角形の頂点座標リスト。
            clicked_hex (tuple): クリックされた六角形の中心座標。
            highlight_trigger (bool): ハイライトを有効にするかどうか。
            on_mouse_point(): マウスの位置座標
        """
        
        if highlight_coordinates:
            distance = calcutate_distance(x, y, highlight_coordinates)
            if distance <= self.radius * 5.5:  # ハイライト範囲内
                self.draw_highlighted_hexagon(hex_vertices_list)
            else:
                self.draw_normal_hexagon(hex_vertices_list)
        else:
            self.draw_normal_hexagon(hex_vertices_list)
        
        if attack_coordinates:
            distance = calcutate_distance(x, y, attack_coordinates)
            if distance <= self.radius*2:
                self.attack_highlighted_draw(hex_vertices_list)
            else:
                self.draw_normal_hexagon(hex_vertices_list)
        else:
            self.draw_normal_hexagon(hex_vertices_list)
        
        if on_mouse_coordinates:
            distance = calcutate_distance(x, y, on_mouse_coordinates)
            if distance <= self.radius:
                self.draw_hex_on_mouse(hex_vertices_list)
            else:
                self.draw_normal_hexagon(hex_vertices_list)
        else:
            self.draw_normal_hexagon(hex_vertices_list)

    # ハイライトヘックスを表示
    def draw_highlighted_hexagon(self, hex_vertices_list):
        """
        移動範囲のハイライトされた六角形を描画する。

        Args:
            hex_vertices_list (list): 六角形の頂点座標リスト。
        """
        arcade.draw_polygon_filled(point_list=hex_vertices_list, color=(0, 0, 255, 50))
        arcade.draw_polygon_outline(point_list=hex_vertices_list, color=arcade.color.BLACK, line_width=2)
    
    def attack_highlighted_draw(self, hex_vertices_list):
        """
        攻撃範囲のハイライトされた六角形を描画する。

        Args:
            hex_vertices_list (list): 六角形の頂点座標リスト。
        """
        arcade.draw_polygon_filled(point_list=hex_vertices_list, color=(255, 0, 0, 50))
        arcade.draw_polygon_outline(point_list=hex_vertices_list, color=arcade.color.BLACK, line_width=2)
    
    # マウスがどこのhexにいるかを表示
    def draw_hex_on_mouse(self, hex_vertices_list):
        """
        hexmap上のhexにマウスが置かれた時に白色に描画する
        Args:
            hex_vertices_list (list): 六角形の頂点座標リスト。
        """
        arcade.draw_polygon_filled(point_list=hex_vertices_list, color=(255, 255, 255, 100))
        arcade.draw_polygon_outline(point_list=hex_vertices_list, color=arcade.color.BLACK, line_width=2)

    # 通常のヘックスを表示
    def draw_normal_hexagon(self, hex_vertices_list):
        """
        通常の六角形を描画する。

        Args:
            hex_vertices_list (list): 六角形の頂点座標リスト。
        """
        arcade.draw_polygon_outline(point_list=hex_vertices_list, color=arcade.color.BLACK, line_width=2)

    def get_hex_center_position(self, mouse_x, mouse_y):
        """
        マウス座標から最も近い六角形の中心座標を取得する。

        Args:
            mouse_x (float): マウスのx座標。
            mouse_y (float): マウスのy座標。

        Returns:
            tuple: 最も近い六角形の中心座標。見つからない場合はNone。
        """
        for center in self.hex_centers:
            distance = calcutate_distance(mouse_x, mouse_y, center)
            if distance <= self.radius:
                return center
        return None
    
    # def calcutate_distance(self, hex_center_x, hex_center_y, point):
    #     """
    #     現在地の座標と、ヘックスの中心座標との距離を算出する
        
    #     Args:
    #         hex_center_x (float): hexmapの中心x座標
    #         hex_center_y (float): hexmapの中心y座標
    #         hex_center_point(tuple): tuple形式のx, y座標

    #     Returns:
    #         tuple: 最も近い六角形の中心座標。見つからない場合はNone。
    #     """
    #     # 距離の計算ロジックを入れる
    #     dx = hex_center_x - point[0]
    #     dy = hex_center_y - point[1]
    #     distance =  math.sqrt(dx**2 + dy**2)
    #     return distance
```

**myunit.py**
```python
import arcade
import math
from utils.helper import calcutate_distance

# ユニットのサイズ
UNIT_WIDTH = 70
UNIT_HEIGHT = 50

# 赤い長方形のオフセット
RED_OFFSET = 5

# 円の半径
CIRCLE_RADIUS = 10

# Unitクラス
class MyUnit(arcade.Sprite):
    def __init__(self, filename, scale, speed, name:str, attack:int, defence:int):
        super().__init__(filename, scale)
        self.speed = speed
        self.destination_x = None # destinationは方向という意味。
        self.destination_y = None
        self.center_x = 0
        self.center_y = 0
        
        self.name = name
        self.attack = attack
        self.defence = defence
    
    # 方向先が指定されたら、ユニットを移動
    def update(self):
        if self.destination_x is not None and self.destination_y is not None:
            """
            現在地とマウスクリック時の距離を計算し、移動するときに反映
            
            # Geminiさんに移動の正規化をするようにした理由を説明
            ユニットがクリックされた地点へ一定の速度で滑らかに移動するようにするためです。
            正規化によって、移動ベクトル (dx, dy) の長さを1に保ち
            方向のみを表す単位ベクトルに変換しています。
            """
            dx = self.destination_x - self.center_x
            dy = self.destination_y - self.center_y
            distance = math.sqrt(dx**2 + dy**2)
            
            if distance > self.speed:
                # 移動方向を正規化
                # 移動のスケールをmax1にしている
                dx = dx/distance
                dy = dy/distance
                
                # ユニットの移動
                self.center_x = self.center_x + dx
                self.center_y = self.center_y + dy
            else:
                self.center_x = self.destination_x
                self.center_y = self.destination_y
            # 初期値に戻す
            self.destination_x = None
            self.destination_y = None
    
    # ユニットの攻撃処理
    def attack_unit(self, target, radius):
        # 攻撃ユニットと防御ユニットとの距離判定
        distance_unit = calcutate_distance(self.center_x, self.center_y, target.position)
        if distance_unit <= radius*2:
            print(f"{self.name}が、{target.name}を攻撃しました。")
            if self.attack > target.defence:
                print(f"{self.name}が勝利！ {target.name}を撃破！")
                target.remove_from_sprite_lists()
            else:
                print(f"{self.name}の攻撃失敗・・・{target.name}生存")
        else:
            print(f"攻撃範囲内のユニットを選択してください。")
            pass
```

helper.py
```python
import math

def calcutate_distance(x, y, point:tuple):
    """
    現在地の座標と、pointとの距離を算出する
    
    Args:
        x (float): 現在地x座標
        y (float): 現在地y座標
        hex_center_point(tuple): tuple形式のx, y座標

    Returns:
        現在地の(x, y)座標とpointとの距離
    """
    # 距離の計算ロジックを入れる
    dx = x - point[0]
    dy = y - point[1]
    distance =  math.sqrt(dx**2 + dy**2)
    return distance

def calcutate_dis_hex(radius: int):
    """
    ヘックスのグリッドサイズから、ヘックスとその隣のヘックスとの距離を算出する
    
    Args:
        radius (float): 六角形の半径
    """
    h = math.sqrt(3)*radius
    w = 2*radius
    dis_hex = h # 上下のヘックス top bottomの頭文字
    print(dis_hex)
```

# ほかにもこんなのやってます
- [pythonのarcadeでhexmapを作成してみた！
](https://qiita.com/nami_fumi/items/12904d2572d7f5f3fe23)
- [Python&arcadeで簡単！レトロ風シューティングゲームを作ろう！🚀](https://qiita.com/nami_fumi/items/dc679b47f57fa5a18f68)
- [pythonのarcadeを使って、ユニット選択とボタンの表示をやってみた](https://qiita.com/nami_fumi/items/c819bd71dac6123f942d)
- [PythonのArcadeでHexmap上のユニットを移動させる](https://qiita.com/nami_fumi/items/296adbdce9ef94a30852)

# 参考文献
- [The Python Arcade Library](https://api.arcade.academy/en/latest/index.html)
    - arcade公式サイト
- [Hexagonal Grids](https://www.redblobgames.com/grids/hexagons/)
    - Hexmapの計算方法や配置を理解するの参考になった良記事
- Gemini
    - コードをよりよくするときにいいです
- [perplexity](https://www.perplexity.ai/)
    - 初期コードを書かせる時にいいです
- [pythonのarcadeでhexmapを作成してみた！](https://qiita.com/nami_fumi/items/12904d2572d7f5f3fe23)