Pygameを使って RPGを作る（11. 敵遭遇後処理）

# 概要

RPGゲームにおいて、Mapの敵を倒したら、さらに新しい敵が湧かないと枯れてしまうため、以下の処理が必要となる

1.敵と遭遇したら、戦闘画面になり、その後、敵を消込を行う。
2.新たな敵を生成し、Mapに追加する

# ロジック処理
1.空のMapデータにモンスター配置情報を更新する
2.Playerの位置情報を配置しないエリアを更新する 
3.モンスターを１つ追加生成し、配置する
4.Map上常に同じ数の敵を維持する

# ロジック処理で必要な情報
1.Playerの位置（モンスター遭遇時）
2.モンスター配置していない空のMapデータ（Blockは必要）
3.モンスター配置情報（こちらはPlayerに倒されるため、変動情報となる）

# Playerクラス
```py
class Player(pg.sprite.Sprite):
    
    def __init__(self, pos, collision_sprites, enemy_sprites):
        super().__init__()

        self.collision_sprites = collision_sprites
        self.enemy_sprites = enemy_sprites
        # イメージの読み込み
        self.surface = pg.image.load(IMAGE_PATH).convert_alpha()

        # 矩形（rect）と位置情報
        self.rect = self.surface.get_frect(topleft=pos)
        self.hit_box_rect = self.rect.inflate(-10,-10)

        # 移動関連
        self.key_speed = 5
        self.direction = pg.Vector2(0, 0)
        self.game_stage = 'main'
        self.enemy_name = ''

    def handle_input(self, dt):
        """キーボード入力で移動処理を行う"""
        keys = pg.key.get_pressed()

        # キーが押されたときだけ移動
        self.direction.x = int(keys[pg.K_RIGHT]) - int(keys[pg.K_LEFT])
        self.direction.y = int(keys[pg.K_DOWN]) - int(keys[pg.K_UP])
        
        self.direction = self.direction.normalize() if self.direction else self.direction

    def collision_enemy(self, dt, enemy_list, mob_pos_info, base_map):

        collided_enemies = []  # 衝突した敵を記録するリスト

        # 水平方向、垂直方向の衝突判定を処理
        self.hit_box_rect.x += self.direction.x * self.key_speed * dt
        self.hit_box_rect.y += self.direction.y * self.key_speed * dt

        for enemy_sprite, enemy in zip(self.enemy_sprites, enemy_list):
            if enemy_sprite.rect.colliderect(self.hit_box_rect):
                print(f'{enemy.name}と衝突しました！')
                self.game_stage = 'battle'
                self.enemy_name = enemy.name
                collided_enemies.append((enemy_sprite, enemy))

        # 衝突した敵を削除
        for enemy_sprite, enemy in collided_enemies:
            if enemy_sprite in self.enemy_sprites:
                self.enemy_sprites.remove(enemy_sprite)  # スプライトグループから削除
            if enemy in enemy_list:
                enemy_list.remove(enemy)  # インスタンスリストから削除

                # print(f'mobpos:{enemy.mob_pos}')
                if enemy.mob_pos in mob_pos_info:
                    mob_pos_info.remove(enemy.mob_pos)

                self.general_mob(base_map, mob_pos_info, enemy_list)
                # print(f'mob_pos_info:{mob_pos_info}')
            # enemy.destroy()  # 敵のリソースを解放

        # メイン矩形をヒットボックスに同期
        self.rect.center = self.hit_box_rect.center

    def cal_mob_area(self, mob_pos_info, max_y, max_x):

        all_list = []
        for px, py in mob_pos_info:

            # NONエリアの計算
            px_min = max(0, px - MOB_AREA)
            px_max = min(max_x - 1, px + MOB_AREA)
            py_min = max(0, py - MOB_AREA)
            py_max = min(max_y - 1, py + MOB_AREA)

            mob_area = []
            for p_y in range(py_min, py_max + 1):
                for p_x in range(px_min, px_max + 1):
                    mob_area.append([p_x, p_y])  # 各座標をリストに追加
            
            all_list.extend(mob_area)

        return all_list
    
    def place_exits_mob(self, map_data, mob_pos_info, num):
        max_y = len(map_data)
        max_x = len(map_data[0])

        for x, y in mob_pos_info:
            # 範囲外アクセスを防ぐチェック
            # if 0 <= y < max_y and 0 <= x < max_x:
            if map_data[y][x] == 0:  # 配置可能な場所
                map_data[y][x] = num  # モンスターを配置
            else:
                # print(f"座標 ({x}, {y}) にモンスターを配置できません。")
                pass
            # else:
            #     print(f"座標 ({x}, {y}) はマップの範囲外です。")
        return map_data

    def get_base_map(self, current_map):
        base_map = []
        for i, row in enumerate(current_map):
            row_area = []
            for j, column in enumerate(row):
                if column == 'B':
                    row_area.append(1) 
                else:
                    row_area.append(0)
            base_map.append(row_area)

        return base_map

    def general_mob(self, map_data, mob_pos_info, enemy_list):

        max_map_y = len(map_data)
        max_map_x = len(map_data[0])

        # Mob配置禁止エリアの計算
        self.non_mob_area = self.cal_mob_area(mob_pos_info, max_map_y, max_map_x)

        # 既存のMobを配置する
        self.updated_map = self.place_exits_mob(map_data, self.non_mob_area, 9)

        # 配置禁止エリアの計算
        p = self.cal_player_area(max_map_y, max_map_x)
        self.place_result_map = self.place_exits_mob(self.updated_map, p, 3)

        for row in self.place_result_map:
            print(row)

        # 配置可能な座標を取得
        zero_positions = [(i, j) for i, row in enumerate(self.place_result_map) for j, val in enumerate(row) if val == 0]
        if zero_positions:
            new_mob = random.sample(zero_positions, 1)
            flattened_list = [item for tup in new_mob for item in tup]
            print(f'配置したモンスター: {flattened_list}')
            # ランダムでMobを生成
            id = random.randint(0, 2)
            x = flattened_list[0] * TILE_SIZE
            y = flattened_list[1] * TILE_SIZE
            enemy = Enemy((y, x), id, flattened_list, self.enemy_sprites)
            enemy_list.append(enemy)
        else:
            print("モンスターを配置できるスペースがありません。")
    
    
    def cal_player_area(self, max_y, max_x):
        # プレイヤーのタイル位置を計算
        px = int(self.rect.centerx / TILE_SIZE)
        py = int(self.rect.centery / TILE_SIZE)

        # NONエリアの計算
        px_min = max(0, px - NON_ENEMY_AREA)
        px_max = min(max_x - 1, px + NON_ENEMY_AREA)
        py_min = max(0, py - NON_ENEMY_AREA)
        py_max = min(max_y - 1, py + NON_ENEMY_AREA)

        player_area = []
        for p_y in range(py_min, py_max + 1):
            for p_x in range(px_min, px_max + 1):
                player_area.append([p_x, p_y])  # 各座標をリストに追加

        return player_area

    def collision_block(self,dt):
        """ブロックとの衝突判定と位置調整を行う"""
        # 水平方向の衝突処理
        self.hit_box_rect.x += self.direction.x * self.key_speed *dt
        for block in self.collision_sprites:
            if block.rect.colliderect(self.hit_box_rect):
                if self.direction.x > 0:  # 右に移動中
                    self.hit_box_rect.right = block.rect.left
                elif self.direction.x < 0:  # 左に移動中
                    self.hit_box_rect.left = block.rect.right
                # 衝突後、移動量をリセット
                self.direction.x = 0

        # 垂直方向の衝突処理
        self.hit_box_rect.y += self.direction.y * self.key_speed *dt
        for block in self.collision_sprites:
            if block.rect.colliderect(self.hit_box_rect):
                if self.direction.y > 0:  # 下に移動中
                    self.hit_box_rect.bottom = block.rect.top
                elif self.direction.y < 0:  # 上に移動中
                    self.hit_box_rect.top = block.rect.bottom
                # 衝突後、移動量をリセット
                self.direction.y = 0

        # メイン矩形をヒットボックスに同期
        self.rect.center = self.hit_box_rect.center

    def update(self, dt, enemy_list, mob_pos_info, current_map):
        base_map = self.get_base_map(current_map)
        self.handle_input(dt)
        self.collision_block(dt)
        self.collision_enemy(dt, enemy_list, mob_pos_info, base_map)
```

# Mapクラス
```py
class Map(pg.sprite.Sprite):
    def __init__(self, collision_sprites, enemy_sprites):

        self.block_images = {
            "B" : "../maps/tree.png"
        }
        self.name = 'map_01'
        self.current_map = TILE[self.name]

        self.collision_sprites = collision_sprites
        self.enemy_sprites = enemy_sprites

        self.map_list = []
        self.enemy_list = []
        # mob配置はキューで管理する
        self.mob_pos_info = []
        self.player = None

    def create(self):
        
        # BlockとPlayerの配置
        for i, row in enumerate(self.current_map):
            for j, column in enumerate(row):
                x = j * TILE_SIZE
                y = i * TILE_SIZE

                if column == 'B':
                    self.block = Block((x,y), self.block_images['B'], self.collision_sprites)
                    self.map_list.append(self.block)
                if column == 'P':
                    self.player = Player((x,y), self.collision_sprites, self.enemy_sprites)
        
        # 敵の配置
        self.create_enemy()

        # 
        self.base_map = self.get_base_map()
        return self.player, self.map_list, self.enemy_list, self.mob_pos_info, self.current_map
    
    def create_enemy(self):
        id = 0 #生成時作業用ID
        # 敵の配置
        self.replace_zeros_with_nines()
        for i, row in enumerate(self.grid):
            # print(row)
            for j, column in enumerate(row):
                x = j * TILE_SIZE
                y = i * TILE_SIZE
                if column == 9:
                    print(f'x:{j} y:{i}に{column}を配置')
                    self.mob_pos_info.append([j,i])

                    self.enemy = Enemy((x,y), id, [j,i], self.enemy_sprites)

                    id += 1
                    if id >2: id = 0
                    self.enemy_list.append(self.enemy)
        # print(self.mob_pos_info)

    def cal_player_area(self, max_y, max_x):
        # px 
        px = -1
        py = -1
        for i, row in enumerate(self.current_map):
            for j, column in enumerate(row):
                if column == 'P':
                    px = j
                    py = i
                    break

        # NONエリアの計算
        px_min = max(0, px - NON_ENEMY_AREA)
        px_max = min(max_x - 1, px + NON_ENEMY_AREA)
        py_min = max(0, py - NON_ENEMY_AREA)
        py_max = min(max_y - 1, py + NON_ENEMY_AREA)

        return px_min, px_max, py_min, py_max
        
    def cal_non_enemy_area(self):

        map_data = self.get_base_map()
        # mapデータの最大値を取得する
        max_map_y = len(map_data)
        max_map_x = len(map_data[0])

        # Playerエリアの計算
        px_min, px_max, py_min, py_max = self.cal_player_area(max_map_x, max_map_y)

        # 敵を配置できるエリアの取得
        self.loc_enemy_area = []
        for i, row in enumerate(self.current_map):
            row_area = []
            for j, column in enumerate(row):
                if column == '.':
                    px = j
                    py = i
                    if (px > px_min and py > py_min) and (px < px_max and py < py_max):
                        row_area.append(1)
                    else:
                        row_area.append(0) 
                else:
                    row_area.append(1)
            self.loc_enemy_area.append(row_area)

        return self.loc_enemy_area

    def replace_zeros_with_nines(self):
        """リスト内の0をランダムに選択して指定された数を9に置き換える。"""
        # 0の座標を探す
        self.grid = self.cal_non_enemy_area()

        zero_positions = [(i, j) for i, row in enumerate(self.grid) for j, val in enumerate(row) if val == 0]
        
        # 指定された数だけランダムに選択
        if MAX_ENEMY_NUM > len(zero_positions):
            raise ValueError("0の数より置き換える数が多いです。")
        
        random_positions = random.sample(zero_positions, MAX_ENEMY_NUM)
        
        # 選択された位置を9に置き換える
        for i, j in random_positions:
            self.grid[i][j] = 9

    def get_base_map(self):
        base_map = []
        for i, row in enumerate(self.current_map):
            row_area = []
            for j, column in enumerate(row):
                if column == 'B':
                    row_area.append(1) 
                else:
                    row_area.append(0)
            base_map.append(row_area)

        return base_map
```

# まとめ

今回は、モンスター配置において、Playerの周りに配置しない、モンスター同士が近くに配置しないなど、ロジックを組み込んだ。
若干バグがあるが、とりあえずは機能する

Playerクラスには、戦闘画面遷移、戦闘後のMobを消す、生成、配置
Mapクラスには、初期のMob配置を分けた。

課題：
MapクラスとPlayerが大きくなりすぎた。