Mac って何?それって美味しいの?

マイコンと呼ばれた時代から現在まで、使用記から何から何でも。

ローカルIntelMac上でDiffusersを使ったAI生成モンスターバトルゲームを作成しました!

AI-Generated Monster Battle


Diffusersを使用した、古いIntelMacでも動作するAI生成モンスターバトルのゲームを作成しました。プレイヤーがAIで生成されたモンスターとバトルする、ターン制のシンプルなRPGです。このゲームでは、DiffusersのStable Diffusion PipelineとLoRA(Low-Rank Adaptation)を使用し、毎回ユニークなモンスター画像を生成しています。

使用技術

  • Diffusers & Stable Diffusion Pipeline: AI画像生成の技術を利用して、モンスターの外見をランダムに設定し、毎回新しいモンスターを表示。
  • LCM LoRA: Stable DiffusionにLCM LoRAを使用し、少ないステップで画像生成時間を短縮。
  • Pygame: Pythonでのゲーム開発に利用し、ターン制バトルや日本語フォントに対応。

ゲームの特徴

  • 多彩なモンスタータイプと属性: モンスターのタイプと属性がランダムに選ばれ、バトルごとに異なる対戦相手。
  • 戦略的な属性相性システム: プレイヤーの攻撃や魔法は属性があり、相性によってダメージが増減。
  • モンスターの弱点調査: バトル中にモンスターの弱点を調査し、有効な攻撃を学ぶことが可能。

ゲームの実装例

以下は、Pythonコードです。実行するにはDiffusers等の実行環境が必要です。Intel Macに環境を作成する手順はこちら。

yummac.hatenablog.jp

コード内で直接指定しているディレクトリやファイルは環境に合わせて変更が必要です。StableDiffusionPipelineで指定のモデルは、ローカルにダウンロードし指定しています。


# DiffusersとLCM LoRAのセットアップ
pipe = StableDiffusionPipeline.from_single_file(
    "モデルファイルのパスとファイル名",
    torch_dtype=torch.float16,
)
pipe = pipe.to("mps")  #GPUを使わない場合は"cpu"を指定
pipe.load_lora_weights("LCM Loraファイルのパスとファイル名")
pipe.scheduler = LCMScheduler.from_config(pipe.scheduler.config)

また、日本語表示のため、お好みの日本語フォントファイルの場所を指定してください。Google Fontsなどでダウンロードできます。


# 日本語対応フォントを指定
font = pygame.font.Font("Font Bookでインストールしたフォントのファイルのパスとファイル名", 18)
small_font = pygame.font.Font("Font Bookでインストールしたフォントのファイルのパスとファイル名", 14)

AI-Generated Monster Battle RPG


from diffusers import StableDiffusionPipeline, LCMScheduler
import torch
import random
import pygame
import sys

# DiffusersとLCM LoRAのセットアップ
pipe = StableDiffusionPipeline.from_single_file(
    "/Users/user/stable-diffusion-webui/models/Stable-diffusion/chilled_remix_v2.safetensors",
    torch_dtype=torch.float16,
)
pipe = pipe.to("mps")
pipe.load_lora_weights("/Users/user/stable-diffusion-webui/models/Lora/pytorch_lora_weights_SD15.safetensors")
pipe.scheduler = LCMScheduler.from_config(pipe.scheduler.config)

# Pygameの初期化
pygame.init()
screen = pygame.display.set_mode((800, 800))
pygame.display.set_caption("AI-Generated Monster Battle RPG")
#font = pygame.font.Font(None, 36)
#small_font = pygame.font.Font(None, 24)

# 日本語対応フォントを指定(ZenKakuGothicAntique)
font = pygame.font.Font("/Users/user/Library/Fonts/ZenKakuGothicAntique-Regular.ttf", 18)
small_font = pygame.font.Font("/Users/user/Library/Fonts/ZenKakuGothicAntique-Regular.ttf", 14)

# モンスター属性データ
monster_data = {
    "dragon": {"attack": 20, "defense": 5},
    "golem": {"attack": 15, "defense": 10},
    "spirit": {"attack": 10, "defense": 7},
    "beast": {"attack": 18, "defense": 4},
}
size_hp_multiplier = {"small": 0.8, "medium": 1.0, "large": 1.2}
element_effectiveness = {
    "fire": {"weak": "water", "strong": "wind"},
    "water": {"weak": "earth", "strong": "fire"},
    "wind": {"weak": "fire", "strong": "earth"},
    "earth": {"weak": "wind", "strong": "water"},
}

# モンスタータイプと属性の日本語対応辞書
monster_type_jp = {
    "dragon": "ドラゴン",
    "golem": "ゴーレム",
    "spirit": "精霊",
    "beast": "ビースト"
}

monster_attribute_kj = {
    "fire": "火",
    "water": "水",
    "wind": "風",
    "earth": "地"
}

monster_attribute_jp = {
    "fire": "ファイヤー",
    "water": "ウォーター",
    "wind": "ウィンド",
    "earth": "アース"
}

# モンスターのタイプと属性でモンスター名を日本語に変換する関数
def translate_to_japanese(type_, attribute):
    type_jp = monster_type_jp.get(type_, type_)
    if type_ == "spirit":
        attribute_jp = monster_attribute_kj.get(attribute, attribute)
        name_jp = f"{attribute_jp}の{type_jp}"
    else:
        attribute_jp = monster_attribute_jp.get(attribute, attribute)
        name_jp = f"{attribute_jp}{type_jp}"
    return name_jp

def generate_monster_image():
    # "モンスターを召喚しています..."メッセージを表示
    screen.fill((50, 50, 50))  # 背景色をリセット
    summon_text = font.render("モンスターを召喚しています...", True, (255, 255, 255))
    screen.blit(summon_text, (300, 400))
    pygame.display.flip()  # 画面を更新してメッセージを表示

    # モンスターの属性とサイズ、タイプをランダムに選択してプロンプトを作成
    attributes = ["fire", "water", "wind", "earth"]
    sizes = ["small", "medium", "large"]
    types = ["dragon", "golem", "spirit", "beast"]
    attribute = random.choice(attributes)
    size = random.choice(sizes)
    type_ = random.choice(types)
    prompt = f"realistic, detailed, A {size} {attribute} {type_} monster with unique features"
    print("Generated prompt:", prompt)

    # モンスター画像を生成
    generated_image = pipe(prompt, height=512, width=512, num_inference_steps=6, guidance_scale=1.0).images[0]
    generated_image.save("monster_generated.png")
    return pygame.image.load("monster_generated.png"), attribute, size, type_

def draw_hp_bar(x, y, hp, max_hp):
    hp_ratio = hp / max_hp
    pygame.draw.rect(screen, (255, 0, 0), (x, y, 200, 20))
    pygame.draw.rect(screen, (0, 255, 0), (x, y, 200 * hp_ratio, 20))

def calculate_damage(player_attack, monster_defense, advantage):
    base_damage = max(5, player_attack - monster_defense)
    if advantage == "strong":
        return int(base_damage * 1.5)
    elif advantage == "weak":
        return int(base_damage * 0.5)
    return base_damage

def game_loop():
    monster_img, monster_attribute, monster_size, monster_type = generate_monster_image()
    monster_name_jp = translate_to_japanese(monster_type, monster_attribute)
    player_hp = 100
    player_max_hp = 100
    player_attack = 15
    monster_max_hp = int(160 * size_hp_multiplier[monster_size])
    monster_hp = monster_max_hp
    monster_attack = monster_data[monster_type]["attack"]
    monster_defense = monster_data[monster_type]["defense"]
    player_turn = True
    running = True
    messages = ["バトル開始!", ""]

    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()

        screen.fill((50, 50, 50))

        # HPバー表示
        draw_hp_bar(50, 30, player_hp, player_max_hp)
        draw_hp_bar(550, 30, monster_hp, monster_max_hp)

        # HPテキスト表示
        player_hp_text = small_font.render(f"プレイヤー HP: {player_hp}", True, (255, 255, 255))
        monster_hp_text = small_font.render(f"モンスター HP: {monster_hp}", True, (255, 255, 255))
        screen.blit(player_hp_text, (50, 10))
        screen.blit(monster_hp_text, (550, 10))

        # モンスター画像の中央表示
        screen.blit(monster_img, (150, 100))

        if player_hp > 0 and monster_hp > 0 and player_turn:
            # 攻撃方法の選択メッセージを表示
            messages.append("攻撃方法を選択: 1-通常攻撃, 2-火魔法, 3-水魔法, 4-風魔法, 5-地魔法, 6-調べる")

        # 勝敗判定
        if player_hp <= 0 or monster_hp <= 0:
            if player_hp <= 0:
                result_text = f"あなたは {monster_name_jp} モンスターに負けてしまった..."
            else:
                result_text = f"あなたは {monster_name_jp} モンスターを倒した!"
            messages.append(result_text)
            messages.append("他のモンスターと戦いますか? (Y/N)")

        # 下部のテキスト枠表示
        pygame.draw.rect(screen, (30, 30, 30), (50, 650, 700, 120))
        for i, message in enumerate(messages[-3:]):
            message_text = font.render(message, True, (255, 255, 255))
            screen.blit(message_text, (60, 660 + i * 30))
        
        # リプレイ
        if player_hp <= 0 or monster_hp <= 0:
            pygame.display.flip()

            replay = False
            waiting_for_input = True
            while waiting_for_input:
                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        pygame.quit()
                        sys.exit()
                    elif event.type == pygame.KEYDOWN:
                        if event.key == pygame.K_y:
                            replay = True
                            waiting_for_input = False
                        elif event.key == pygame.K_n:
                            waiting_for_input = False

            if replay:
                game_loop()
            else:
                pygame.quit()
                sys.exit()

        if player_turn:
            # 攻撃方法の選択メッセージを表示してからキー入力を待機
            pygame.display.flip()

            # 攻撃方法のキー入力を待機
            waiting_for_choice = True
            attack_choice = 1
            while waiting_for_choice:
                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        pygame.quit()
                        sys.exit()
                    elif event.type == pygame.KEYDOWN:
                        if event.key in [pygame.K_1, pygame.K_2, pygame.K_3, pygame.K_4, pygame.K_5, pygame.K_6]:
                            attack_choice = int(event.unicode)
                            waiting_for_choice = False

            if attack_choice == 6:
                damage = 0
                # プレイヤーとモンスターのHPに応じて成功率を計算
                success_rate = 0
                hp_ratio = min((player_hp / player_max_hp), (monster_hp / monster_max_hp))

                if hp_ratio <= 0.5:
                    success_rate = 90  # HPが50%以下で成功率90%
                else:
                    success_rate = 100 - (hp_ratio * 100)
                
                # 成功率に基づいて調査の成功を判定
                if random.randint(1, 100) <= success_rate:
                    weak_against = element_effectiveness[monster_attribute]["weak"]
                    weak_against_jp = monster_attribute_kj.get(weak_against, weak_against)
                    messages.append(f"このモンスターは {monster_name_jp} で {weak_against_jp}魔法攻撃が有効だ!")
                else:
                    messages.append("モンスターの弱点を見つけられなかった...")

            else:
                # 攻撃選択によるダメージ計算
                if attack_choice == 1:
                    damage = calculate_damage(player_attack + random.randint(0, player_attack), monster_defense, "normal")
                    messages.append(f"プレイヤーの攻撃! モンスターに {damage} のダメージを与えた")
                else:
                    elements = ["fire", "water", "wind", "earth"]
                    player_attribute = elements[attack_choice - 2]
                    if element_effectiveness[player_attribute]["strong"] == monster_attribute:
                        advantage = "strong"
                    elif element_effectiveness[player_attribute]["weak"] == monster_attribute:
                        advantage = "weak"
                    else:
                        advantage = "normal"
                    damage = calculate_damage(player_attack  + random.randint(0, player_attack), monster_defense, advantage)
                    magic_jp = monster_attribute_kj.get(player_attribute, player_attribute)
                    messages.append(f"プレイヤーは {magic_jp}魔法を唱えた! モンスターに {damage} のダメージを与えた")

            monster_hp -= damage
            if monster_hp < 0:
                monster_hp = 0
            player_turn = False
        else:
            damage = random.randint(0, int(monster_attack * 1.5))
            player_hp -= damage
            if player_hp < 0:
                player_hp = 0
            
            if damage == 0:
                messages.append(f"プレイヤーはモンスターの攻撃をかわした!!")
            else:
                messages.append(f"モンスターの攻撃! プレイヤーは {damage} のダメージ")
            
            player_turn = True

        pygame.display.flip()
        pygame.time.delay(1000)

game_loop()

日本語対応

日本語対応フォントを使用し、日本語でのメッセージ表示が可能です。

ゲームの楽しみ方

バトルは毎回違うモンスターと戦えるので、繰り返し遊ぶことができます。AIが生成したモンスターとの戦い、属性を活用した戦略的なバトルが楽しめます!