C言語入門|STEP21:完成版 rpg1.c 全コード解説-バトルRPG全体の流れを総復習

はじめに:ここまで本当によく積み上げてきました

STEP1からSTEP20まで、
あなたは少しずつバトルRPGを作り上げてきました。

  • キャラクターと職業
  • 成長とレベルアップ
  • 魔法・敵AI・バトル
  • マップ探索とイベント
  • セーブ・ロード
  • ラスボスとエンディング

STEP21では、これらが 1つのファイル rpg1.c の中でどう結びついているのか を確認します。

「この行は何のためにあるのか」
「なぜこの順番なのか」

という視点で読むと、
C言語でのゲーム構築力が一段上がります 👍

rpg1.c 全体構成の見取り図

まずは全体を大きく整理します。

ブロック役割
include / define環境設定・定数
構造体・列挙型データ設計
初期化関数キャラ・マップ・魔法
成長・計算関数ATK・MAG・EXP
バトル関連戦闘ループ
マップ関連探索・イベント
セーブ・ロード状態保存
main 関数ゲーム全体制御

上から下へ読むだけで、ゲームの全体像が見える
というのが、このプログラムの良い点です。

main 関数は「司令塔」

ゲーム全体を動かしているのは main 関数です。

int main(void) {
    srand((unsigned)time(NULL));

    GameState gs;
    memset(&gs, 0, sizeof(gs));

    show_title();
    load_map_template(gs.map);
    init_characters(gs.party);

    while (1) {
        draw_map(&gs);
        入力処理
        移動処理
        イベント処理
    }
}

main の役割

  • ゲーム開始準備
  • 無限ループでゲーム進行
  • 状況に応じて処理を振り分け

細かい処理はすべて関数に任せる
これが読みやすさの秘訣です。

GameState がすべてを握っている

このRPGの核となるのが GameState 構造体です。

typedef struct {
    int px, py;
    char map[MAP_H][MAP_W];
    Character party[PARTY_SIZE];
    int inv[ITEM_COUNT];
    int game_clear;
} GameState;

なぜこれが重要なのか

  • セーブ・ロードが簡単
  • 関数に渡しやすい。
  • 状態管理が一元化される。

ゲーム全体=GameState
という考え方を身につけられたのは大きな収穫です。

データ設計がRPGの土台

STEP12〜15で学んだ通り、
このRPGはデータ設計がとても丁寧です。

  • Character 構造体
  • Enemy 構造体
  • Spell / EnemySpell
  • ItemDef

これらがあるからこそ、

  • 職業ごとの差
  • 魔法の多様性
  • 敵AIの分岐

if地獄にならずに実現 できています。

バトル処理は1つの関数に集約

戦闘は battle 関数にすべて集約されています。

static int battle(GameState* gs, Enemy enemy)

battle 関数の中身(流れ)

  1. 敵出現演出
  2. ターン制ループ
  3. 味方の行動選択
  4. 敵の行動(AI)
  5. 勝利・敗北判定

通常戦もボス戦も、
同じ関数で処理できる設計 がとても美しいです。

マップ探索とイベント処理の連携

マップは2次元配列で管理されています。

char map[MAP_H][MAP_W];

移動後に、

  • T → 宝箱
  • E → 戦闘
  • I → 宿
  • C → 教会
  • B → ラスボス

という形で 1文字=1イベント が実行されます。

これはRPGの基本であり、
とても応用しやすい設計です。

セーブ・ロードで完成度が一段上がる

GameState を丸ごと保存する方式により、

  • 途中中断できる。
  • マップ状態も保持される。
  • 学習用として分かりやすい。

というメリットが生まれました。

fwrite(gs, sizeof(GameState), 1, fp);

構造体を信じて丸ごと保存
これはC言語ならではの強みです。

ラスボスとエンディングで「完結」する

STEP20で実装したラスボス戦により、

  • 物語に区切りができ
  • ゲームとしての達成感が生まれ
  • プログラムが自然に終了

します。

エンディング処理があることで、
「作ったRPG」から「完成したRPG」へ進化しました。

完成版 rpg1.c(全文)

以下が、省略なしの完成版 rpg1.c です。
※ここまで学んだすべてが、この1ファイルに詰まっています。

プロジェクト名:rpg1 ソースファイル名: rpg1.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#ifdef _WIN32
#include <windows.h>
#endif

/* =========================================================
   画面制御(簡易)
   - Windows: cls
   - Linux/Mac: clear
   ========================================================= */
void clear_screen(void) {
#ifdef _WIN32
    system("cls");
#else
    system("clear");
#endif
}

/* =========================================================
   効果音(ビープ)
   kind: 0=決定 1=攻撃 2=回復 3=勝利 4=警告
   ========================================================= */
static void sfx_beep(int kind) {
#ifdef _WIN32
    switch (kind) {
    case 0: Beep(880, 60); break;
    case 1: Beep(660, 60); break;
    case 2: Beep(988, 60); break;
    case 3: Beep(523, 80); Beep(659, 80); Beep(784, 120); break;
    default: Beep(200, 120); break;
    }
#else
    /* 端末ベル(環境によって無効なこともあります) */
    (void)kind;
    printf("\a");
    fflush(stdout);
#endif
}

/* =========================================================
   ANSIカラー(対応していない端末ではそのまま文字列になります)
   ========================================================= */
#define RESET  "\x1b[0m"
#define RED    "\x1b[31m"
#define GREEN  "\x1b[32m"
#define YELLOW "\x1b[33m"
#define BLUE   "\x1b[34m"
#define MAGENTA "\x1b[35m"
#define CYAN   "\x1b[36m"
#define WHITE  "\x1b[37m"

/* =========================================================
   基本定義
   ========================================================= */
#define MAP_H 25
#define MAP_W 25

#define PARTY_SIZE 3
#define MAX_LEVEL 20

#define SPELLS_PER_CHAR 4
#define SPELLS_PER_ENEMY 4

#define SAVE_FILE "save.dat"

/* =========================================================
   職業
   ========================================================= */
typedef enum { HERO = 0, WARRIOR = 1, MAGE = 2, JOB_COUNT = 3 } Job;

/* =========================================================
   アイテムID(全8種)
   - 宝箱8個 = 8アイテムを必ず1回ずつ取れる設計にする
   ========================================================= */
typedef enum {
    ITEM_POTION = 0,
    ITEM_HIPOTION,
    ITEM_ETHER,
    ITEM_ELIXIR,
    ITEM_BRONZE_SWORD,
    ITEM_STEEL_SWORD,
    ITEM_APPRENTICE_STAFF,
    ITEM_SAGE_STAFF,
    ITEM_COUNT
} ItemID;

/* =========================================================
   味方側の魔法タイプ
   ========================================================= */
typedef enum { SPELL_DMG = 0, SPELL_HEAL = 1, SPELL_BUFF = 2 } SpellType;

/* =========================================================
   味方側の魔法定義(キャラが持つ4つ)
   ========================================================= */
typedef struct {
    char name[24];
    SpellType type;
    int mp_cost;
    int power;
} Spell;

/* =========================================================
   キャラクター(味方)
   ========================================================= */
typedef struct {
    char name[20];
    Job job;
    int level;

    int hp, max_hp;
    int mp, max_mp;

    int base_atk;
    int base_magic;

    int weapon_atk;
    int staff_magic;

    int alive;
    int exp;

    int defending; /* 1なら被ダメ軽減(次の攻撃で半減) */

    Spell spells[SPELLS_PER_CHAR];
} Character;

/* =========================================================
   敵側の固有魔法(4種)
   ========================================================= */
typedef enum {
    EN_SPELL_DMG = 0,
    EN_SPELL_HEAL = 1,
    EN_SPELL_ATKUP = 2,
    EN_SPELL_DEFUP = 3
} EnemySpellKind;

typedef struct {
    char name[24];
    EnemySpellKind kind;
    int mp_cost;
    int power;
} EnemySpell;

/* =========================================================
   敵
   ========================================================= */
typedef struct {
    char name[24];

    int hp, max_hp;
    int mp, max_mp;

    int atk;
    int magic;

    int exp;
    int is_boss;

    int defending;
    EnemySpell spells[SPELLS_PER_ENEMY];
} Enemy;

/* =========================================================
   ゲーム全体状態(セーブ対象)
   ========================================================= */
typedef struct {
    int px, py;
    char map[MAP_H][MAP_W];
    Character party[PARTY_SIZE];
    int inv[ITEM_COUNT];
    int game_clear;
} GameState;

/* =========================================================
   入力ヘルパ
   ========================================================= */
static void read_line(char* buf, size_t n) {
    if (fgets(buf, (int)n, stdin) == NULL) {
        buf[0] = '\0';
        return;
    }
    size_t len = strlen(buf);
    if (len && buf[len - 1] == '\n') buf[len - 1] = '\0';
}

static int read_int_safely(int* out) {
    char buf[64];
    read_line(buf, sizeof(buf));
    if (buf[0] == '\0') return 0;
    char* end = NULL;
    long v = strtol(buf, &end, 10);
    if (end == buf) return 0;
    *out = (int)v;
    return 1;
}

/* =========================================================
   アイテム定義
   ========================================================= */
typedef enum { IT_CONSUME = 0, IT_WEAPON = 1, IT_STAFF = 2 } ItemType;
typedef struct {
    char name[32];
    ItemType type;
    int value;
} ItemDef;

static const ItemDef g_items[ITEM_COUNT] = {
    {"ポーション", IT_CONSUME, 30},
    {"ハイポーション", IT_CONSUME, 70},
    {"エーテル", IT_CONSUME, 15},
    {"エリクサー", IT_CONSUME, 999},
    {"ブロンズソード", IT_WEAPON, 3},
    {"スチールソード", IT_WEAPON, 6},
    {"見習いの杖", IT_STAFF, 3},
    {"賢者の杖", IT_STAFF, 6},
};

/* =========================================================
   成長テーブル(職業ごと)
   ========================================================= */
typedef struct { int hp, mp, atk, mag; } Growth;

static const Growth g_growth[JOB_COUNT][MAX_LEVEL + 1] = {
    /* HERO */
    {
        {0,0,0,0},{0,0,0,0},
        {6,2,2,1},{6,2,2,1},{7,2,2,1},{7,2,2,1},
        {8,3,2,1},{8,3,2,1},{9,3,2,1},{9,3,2,2},
        {10,3,3,2},{10,3,3,2},{10,4,3,2},{11,4,3,2},
        {11,4,3,2},{12,4,3,2},{12,5,3,2},{13,5,3,2},
        {13,5,3,2}
    },
    /* WARRIOR */
    {
        {0,0,0,0},{0,0,0,0},
        {9,1,3,0},{9,1,3,0},{10,1,3,0},{10,1,3,0},
        {11,2,3,0},{11,2,3,0},{12,2,3,0},{12,2,4,0},
        {13,2,4,0},{13,2,4,0},{14,2,4,0},{14,3,4,0},
        {15,3,4,0},{15,3,4,0},{16,3,4,0},{16,3,5,0},
        {17,3,5,0}
    },
    /* MAGE */
    {
        {0,0,0,0},{0,0,0,0},
        {4,5,1,3},{4,5,1,3},{5,6,1,3},{5,6,1,3},
        {6,6,1,4},{6,7,1,4},{7,7,1,4},{7,8,1,5},
        {8,8,2,5},{8,9,2,5},{9,9,2,6},{9,10,2,6},
        {10,10,2,6},{10,11,2,7},{11,11,2,7},{11,12,2,7},
        {12,12,2,8}
    }
};

/* =========================================================
   味方の魔法(職業別に4種)
   ========================================================= */
static void init_spells(Character* c) {
    if (c->job == HERO) {
        c->spells[0] = (Spell){ "ヒール", SPELL_HEAL, 4, 25 };
        c->spells[1] = (Spell){ "ホーリーブレード", SPELL_DMG, 5, 2 };
        c->spells[2] = (Spell){ "ガードオーラ", SPELL_BUFF, 4, 1 };
        c->spells[3] = (Spell){ "スマイト", SPELL_DMG, 7, 3 };
    }
    else if (c->job == WARRIOR) {
        c->spells[0] = (Spell){ "パワーストライク", SPELL_DMG, 4, 2 };
        c->spells[1] = (Spell){ "ウォークライ", SPELL_BUFF, 4, 2 };
        c->spells[2] = (Spell){ "アイアンウォール", SPELL_BUFF, 5, 2 };
        c->spells[3] = (Spell){ "スラッシュウェブ", SPELL_DMG, 7, 3 };
    }
    else {
        c->spells[0] = (Spell){ "ファイア", SPELL_DMG, 4, 2 };
        c->spells[1] = (Spell){ "アイス", SPELL_DMG, 4, 2 };
        c->spells[2] = (Spell){ "サンダー", SPELL_DMG, 6, 3 };
        c->spells[3] = (Spell){ "リカバー", SPELL_HEAL, 5, 35 };
    }
}

/* =========================================================
   キャラ初期化
   ========================================================= */
static void init_characters(Character p[]) {
    p[0] = (Character){ "勇者", HERO, 1, 55,55, 12,12, 8, 6, 0,0, 1, 0, 0,0, {0} };
    p[1] = (Character){ "戦士", WARRIOR,1, 65,65,  8, 8, 12,3, 0,0, 1, 0, 0,0, {0} };
    p[2] = (Character){ "魔法使い", MAGE,1, 45,45, 22,22,  5,12,0,0, 1, 0, 0,0, {0} };

    for (int i = 0; i < PARTY_SIZE; i++) {
        p[i].defending = 0;
        init_spells(&p[i]);
    }
}

static int char_atk(const Character* c) { return c->base_atk + c->weapon_atk; }
static int char_mag(const Character* c) { return c->base_magic + c->staff_magic; }

/* =========================================================
   経験値・レベルアップ
   ========================================================= */
static int need_exp(int level) { return level * 20; }

static void level_up(Character* c) {
    if (c->level >= MAX_LEVEL) return;

    c->level++;
    Growth g = g_growth[c->job][c->level];

    c->max_hp += g.hp; c->hp += g.hp;
    c->max_mp += g.mp; c->mp += g.mp;
    c->base_atk += g.atk;
    c->base_magic += g.mag;

    sfx_beep(0);
    printf(YELLOW ">> %s はレベル%dになった!(HP+%d MP+%d ATK+%d MAG+%d)\n" RESET,
        c->name, c->level, g.hp, g.mp, g.atk, g.mag);
}

static void add_exp_party(Character p[], int exp) {
    for (int i = 0; i < PARTY_SIZE; i++) {
        if (!p[i].alive) continue;

        p[i].exp += exp;
        while (p[i].level < MAX_LEVEL && p[i].exp >= need_exp(p[i].level)) {
            p[i].exp -= need_exp(p[i].level);
            level_up(&p[i]);
        }
    }
}

/* =========================================================
   タイトル&ストーリー
   ========================================================= */
static void show_title(void) {
    clear_screen();
    printf(CYAN);
    printf("#########################################\n");
    printf("#                                       #\n");
    printf("#              BATTLE RPG               #\n");
    printf("#                                       #\n");
    printf("#########################################\n");
    printf(RESET);

    printf("\n【物語】\n");
    printf("  世界は闇に覆われ、各地に魔物があふれた。\n");
    printf("  勇者・戦士・魔法使いの3人は、世界を救うため旅立つ。\n");
    printf("  伝説の魔竜「マスタードラゴン」を倒せば、平和が戻ると言われている…\n");

    printf("\n【操作】\n");
    printf("  4← 8↑ 6→ 2↓ で移動\n");
    printf("  s : セーブ  l : ロード  q : 終了\n");

    printf("\n【マップ記号】\n");
    printf("  @ : プレイヤー\n");
    printf("  E : 敵\n");
    printf("  T : 宝箱\n");
    printf("  I : 宿(HP/MP回復)\n");
    printf("  C : 教会(復活)\n");
    printf("  B : ラストボス\n");

    printf("\nEnterキーで開始...\n");
    char tmp[8];
    read_line(tmp, sizeof(tmp));
}

static void show_ending(void) {
    clear_screen();
    printf(CYAN);
    printf("#########################################\n");
    printf("#              THE END                  #\n");
    printf("#########################################\n");
    printf(RESET);

    printf("\n【物語】\n");
    printf("  激戦の末、マスタードラゴンは崩れ落ちた。\n");
    printf("  闇は晴れ、町に笑い声が戻る。\n");
    printf("  しかし旅は続く…新たな物語が、また始まる。\n");

    printf("\nEnterキーで終了\n");
    char tmp[8];
    read_line(tmp, sizeof(tmp));
}

/* =========================================================
   マップ初期化(25x25)
   ========================================================= */
static void load_map_template(char map[MAP_H][MAP_W]) {
    static const char* tmpl[MAP_H] = {
        "#########################",
        "#    E      T     I     #",
        "# ##### ##### ##### ##  #",
        "#     E           C     #",
        "# ### ###### ### ###    #",
        "#   #    T   #     E    #",
        "# ### ###### # ##### ####",
        "#     E      #   T      #",
        "### ##### ####### ###   #",
        "#   #   #     E   #     #",
        "# ### # ######### # ##  #",
        "#     #     T     #     #",
        "# ##### ### ##### ###   #",
        "#   I   #   #     E     #",
        "# ##### # ### ###########",
        "#     T #     #         #",
        "### ##### ##### ######  #",
        "#     E      T     C    #",
        "# ##### ##### ##### ##  #",
        "#   T             E     #",
        "# ### ###### ### ###    #",
        "#     E                 #",
        "# ##### ##### ##### ## B#",
        "#    T      E     I     #",
        "#########################"
    };

    for (int i = 0; i < MAP_H; i++) {
        for (int j = 0; j < MAP_W; j++) {
            map[i][j] = tmpl[i][j];
        }
    }
}

static void draw_map(const GameState* gs) {
    for (int i = 0; i < MAP_H; i++) {
        for (int j = 0; j < MAP_W; j++) {
            if (i == gs->px && j == gs->py) {
                printf(YELLOW "@");
                continue;
            }

            char c = gs->map[i][j];
            switch (c) {
            case '#': printf(WHITE "#"); break;
            case 'E': printf(RED "E"); break;
            case 'T': printf(YELLOW "T"); break;
            case 'I': printf(GREEN "I"); break;
            case 'C': printf(CYAN "C"); break;
            case 'B': printf(MAGENTA "B"); break;
            case ' ': printf(" "); break;
            default:  printf(" "); break;
            }
        }
        printf("\n");
    }
    printf(RESET);
}

static void respawn_enemy(GameState* gs) {
    for (int tries = 0; tries < 5000; tries++) {
        int x = rand() % MAP_H;
        int y = rand() % MAP_W;

        if (x == gs->px && y == gs->py) continue;

        if (gs->map[x][y] == ' ') {
            gs->map[x][y] = 'E';
            return;
        }
    }
}

/* =========================================================
   宝箱(位置固定)
   ========================================================= */
typedef struct { int x, y; ItemID item; } TreasureSpot;

static const TreasureSpot g_treasures[ITEM_COUNT] = {
    { 1, 12, ITEM_POTION },
    { 5, 10, ITEM_HIPOTION },
    { 7, 18, ITEM_ETHER },
    { 11, 12, ITEM_ELIXIR },
    { 15, 6, ITEM_BRONZE_SWORD },
    { 17, 13, ITEM_STEEL_SWORD },
    { 19, 4, ITEM_APPRENTICE_STAFF },
    { 23, 5, ITEM_SAGE_STAFF },
};

static int treasure_index_at(int x, int y) {
    for (int i = 0; i < ITEM_COUNT; i++) {
        if (g_treasures[i].x == x && g_treasures[i].y == y) return i;
    }
    return -1;
}

static void open_treasure_fixed(GameState* gs, int x, int y) {
    int idx = treasure_index_at(x, y);

    if (idx < 0) {
        int item = rand() % ITEM_COUNT;
        gs->inv[item]++;
        sfx_beep(0);
        printf(YELLOW "宝箱を開けた!「%s」を手に入れた!(所持:%d)\n" RESET,
            g_items[item].name, gs->inv[item]);
        return;
    }

    ItemID item_id = g_treasures[idx].item;
    gs->inv[item_id]++;

    sfx_beep(0);
    printf(YELLOW "宝箱を開けた!「%s」を手に入れた!(所持:%d)\n" RESET,
        g_items[item_id].name, gs->inv[item_id]);
}

/* =========================================================
   宿・教会
   ========================================================= */
static void inn_rest(GameState* gs) {
    sfx_beep(2);
    printf(GREEN "宿に泊まった! HP/MPが全回復!\n" RESET);

    for (int i = 0; i < PARTY_SIZE; i++) {
        if (!gs->party[i].alive) continue;
        gs->party[i].hp = gs->party[i].max_hp;
        gs->party[i].mp = gs->party[i].max_mp;
    }
}

static void church_revive(GameState* gs) {
    sfx_beep(2);
    printf(CYAN "教会で祈った…倒れた仲間が復活した!\n" RESET);

    for (int i = 0; i < PARTY_SIZE; i++) {
        if (gs->party[i].alive) continue;
        gs->party[i].alive = 1;
        gs->party[i].hp = gs->party[i].max_hp / 2;
        gs->party[i].mp = 0;
    }
}

/* =========================================================
   セーブ/ロード
   ========================================================= */
static int save_game(const GameState* gs) {
    FILE* fp = fopen(SAVE_FILE, "wb");
    if (!fp) return 0;
    size_t w = fwrite(gs, sizeof(GameState), 1, fp);
    fclose(fp);
    return (w == 1);
}

static int load_game(GameState* gs) {
    FILE* fp = fopen(SAVE_FILE, "rb");
    if (!fp) return 0;
    size_t r = fread(gs, sizeof(GameState), 1, fp);
    fclose(fp);
    return (r == 1);
}

/* =========================================================
   ステータス表示(バトル用)
   ========================================================= */
static void print_party_status(const Character p[]) {
    printf(BLUE "==== PARTY ====\n" RESET);
    for (int i = 0; i < PARTY_SIZE; i++) {
        printf("%d) %-10s Lv%-2d  HP %3d/%3d  MP %3d/%3d  ATK %2d  MAG %2d  %s\n",
            i + 1,
            p[i].name,
            p[i].level,
            p[i].hp, p[i].max_hp,
            p[i].mp, p[i].max_mp,
            char_atk(&p[i]),
            char_mag(&p[i]),
            p[i].alive ? "" : RED "(DOWN)" RESET
        );
    }
}

static void print_enemy_status(const Enemy* e) {
    printf(RED "==== ENEMY ====\n" RESET);
    printf("  %-14s  HP %3d/%3d  MP %3d/%3d  ATK %2d  MAG %2d  %s\n",
        e->name, e->hp, e->max_hp, e->mp, e->max_mp, e->atk, e->magic,
        e->defending ? CYAN "[DEF]" RESET : ""
    );
}

/* =========================================================
   アイテム使用
   ========================================================= */
static int any_item_available(const int inv[]) {
    for (int i = 0; i < ITEM_COUNT; i++) if (inv[i] > 0) return 1;
    return 0;
}

static void show_inventory(const int inv[]) {
    printf(YELLOW "---- 所持アイテム ----\n" RESET);
    for (int i = 0; i < ITEM_COUNT; i++) {
        if (inv[i] > 0) printf("%d) %-16s x%d\n", i + 1, g_items[i].name, inv[i]);
    }
}

static void use_consumable_on(Character* c, ItemID id) {
    if (id == ITEM_POTION || id == ITEM_HIPOTION) {
        c->hp += g_items[id].value;
        if (c->hp > c->max_hp) c->hp = c->max_hp;
        sfx_beep(2);
    }
    else if (id == ITEM_ETHER) {
        c->mp += g_items[id].value;
        if (c->mp > c->max_mp) c->mp = c->max_mp;
        sfx_beep(2);
    }
    else if (id == ITEM_ELIXIR) {
        c->hp = c->max_hp;
        c->mp = c->max_mp;
        sfx_beep(2);
    }
}

static int equip_item(Character* c, ItemID id) {
    if (g_items[id].type == IT_WEAPON) {
        if (c->job == HERO || c->job == WARRIOR) {
            c->weapon_atk = g_items[id].value;
            sfx_beep(0);
            printf(YELLOW "%s は「%s」を装備した!(武器ATK+%d)\n" RESET,
                c->name, g_items[id].name, g_items[id].value);
            return 1;
        }
        return 0;
    }
    else if (g_items[id].type == IT_STAFF) {
        if (c->job == MAGE) {
            c->staff_magic = g_items[id].value;
            sfx_beep(0);
            printf(YELLOW "%s は「%s」を装備した!(杖MAG+%d)\n" RESET,
                c->name, g_items[id].name, g_items[id].value);
            return 1;
        }
        return 0;
    }
    return 0;
}

/* =========================================================
   味方魔法処理
   ========================================================= */
static int cast_spell(Character* caster, Character party[], Enemy* enemy, int spell_index) {
    if (spell_index < 0 || spell_index >= SPELLS_PER_CHAR) return 0;

    Spell sp = caster->spells[spell_index];

    if (caster->mp < sp.mp_cost) {
        printf(RED "MPが足りない!\n" RESET);
        sfx_beep(4);
        return 0;
    }

    caster->mp -= sp.mp_cost;

    if (sp.type == SPELL_DMG) {
        int dmg = char_mag(caster) * sp.power;

        if (enemy->defending) {
            dmg /= 2;
            enemy->defending = 0;
        }

        enemy->hp -= dmg;
        if (enemy->hp < 0) enemy->hp = 0;

        sfx_beep(1);
        printf(YELLOW "%s は %s! %d ダメージ!\n" RESET, caster->name, sp.name, dmg);
        return 1;
    }
    else if (sp.type == SPELL_HEAL) {
        printf("回復対象を選んでください (1-%d): ", PARTY_SIZE);
        int t = 0;
        if (!read_int_safely(&t)) return 0;
        if (t < 1 || t > PARTY_SIZE) return 0;

        Character* target = &party[t - 1];
        if (!target->alive) {
            printf(RED "その仲間は倒れている!\n" RESET);
            return 0;
        }

        target->hp += sp.power;
        if (target->hp > target->max_hp) target->hp = target->max_hp;

        sfx_beep(2);
        printf(GREEN "%s は %s! %s のHPが回復した!(+%d)\n" RESET,
            caster->name, sp.name, target->name, sp.power);
        return 1;
    }
    else {
        sfx_beep(0);

        if (strcmp(sp.name, "ウォークライ") == 0) {
            caster->base_atk += sp.power;
            printf(YELLOW "%s は %s! ATKが上がった!(+%d)\n" RESET, caster->name, sp.name, sp.power);
        }
        else {
            caster->defending = 1;
            printf(YELLOW "%s は %s! 次の被ダメージを軽減!\n" RESET, caster->name, sp.name);
        }
        return 1;
    }
}

/* =========================================================
   敵タイプ(テンプレート)
   ========================================================= */
typedef struct {
    char name[24];
    int hp, mp, atk, magic, exp;
    int can_cast;
    EnemySpell spells[SPELLS_PER_ENEMY];
} EnemyTemplate;

static const EnemyTemplate g_enemy_templates[] = {
    {
        "スライム", 28, 10, 6, 2, 8, 1,
        {
            {"ぬめり攻撃", EN_SPELL_DMG,   3, 2},
            {"スライム再生", EN_SPELL_HEAL, 4, 12},
            {"ぷるぷる強化", EN_SPELL_ATKUP,4, 1},
            {"体を縮める", EN_SPELL_DEFUP,  3, 0},
        }
    },
    {
        "ゴブリン", 40, 14, 8, 4, 12, 1,
        {
            {"ダガー投げ", EN_SPELL_DMG,   4, 2},
            {"応急手当", EN_SPELL_HEAL,     5, 16},
            {"激昂", EN_SPELL_ATKUP,        5, 2},
            {"身構える", EN_SPELL_DEFUP,    4, 0},
        }
    },
    {
        "スケルトン", 55, 16, 10, 5, 18, 1,
        {
            {"ボーンスピア", EN_SPELL_DMG,  5, 2},
            {"骨の修復", EN_SPELL_HEAL,     6, 18},
            {"戦意高揚", EN_SPELL_ATKUP,     6, 2},
            {"骨の盾", EN_SPELL_DEFUP,      5, 0},
        }
    },
    {
        "オーク", 75, 18, 13, 5, 25, 1,
        {
            {"ぶん回し", EN_SPELL_DMG,      6, 2},
            {"肉体再生", EN_SPELL_HEAL,     7, 20},
            {"猛進", EN_SPELL_ATKUP,        7, 3},
            {"鉄皮", EN_SPELL_DEFUP,        6, 0},
        }
    },
    {
        "ダークメイジ", 60, 26, 7, 12, 30, 1,
        {
            {"ダークボルト", EN_SPELL_DMG,  6, 3},
            {"ダークヒール", EN_SPELL_HEAL, 7, 22},
            {"呪力増幅", EN_SPELL_ATKUP,     6, 1},
            {"バリア", EN_SPELL_DEFUP,      6, 0},
        }
    },
    {
        "ワイバーン", 95, 22, 15, 7, 40, 1,
        {
            {"ウィンドカッター", EN_SPELL_DMG, 7, 2},
            {"翼の休息", EN_SPELL_HEAL,         7, 20},
            {"飛翔の咆哮", EN_SPELL_ATKUP,       7, 2},
            {"回避姿勢", EN_SPELL_DEFUP,         6, 0},
        }
    },
};

static Enemy make_random_enemy(void) {
    int n = (int)(sizeof(g_enemy_templates) / sizeof(g_enemy_templates[0]));
    const EnemyTemplate* t = &g_enemy_templates[rand() % n];

    Enemy e;
    memset(&e, 0, sizeof(e));

    snprintf(e.name, sizeof(e.name), "%s", t->name);

    e.max_hp = t->hp; e.hp = t->hp;
    e.max_mp = t->mp; e.mp = t->mp;

    e.atk = t->atk;
    e.magic = t->magic;

    e.exp = t->exp;
    e.is_boss = 0;

    e.defending = 0;

    for (int i = 0; i < SPELLS_PER_ENEMY; i++) e.spells[i] = t->spells[i];

    return e;
}

/* =========================================================
   敵AI(固有魔法4種)
   ========================================================= */
static int enemy_try_cast_spell(Enemy* e, Character party[]) {
    if (e->mp <= 0) return 0;
    if ((rand() % 100) >= 50) return 0;

    int usable_idx[SPELLS_PER_ENEMY];
    int usable_cnt = 0;

    for (int i = 0; i < SPELLS_PER_ENEMY; i++) {
        if (e->mp >= e->spells[i].mp_cost) usable_idx[usable_cnt++] = i;
    }
    if (usable_cnt == 0) return 0;

    int hp_rate = (e->hp * 100) / (e->max_hp > 0 ? e->max_hp : 1);

    int chosen = -1;

    if (hp_rate <= 40) {
        for (int k = 0; k < usable_cnt; k++) {
            EnemySpell sp = e->spells[usable_idx[k]];
            if (sp.kind == EN_SPELL_HEAL) { chosen = usable_idx[k]; break; }
        }
    }

    if (chosen < 0) chosen = usable_idx[rand() % usable_cnt];

    EnemySpell sp = e->spells[chosen];
    e->mp -= sp.mp_cost;

    if (sp.kind == EN_SPELL_DMG) {
        int t = -1;
        for (int tries = 0; tries < 100; tries++) {
            int idx = rand() % PARTY_SIZE;
            if (party[idx].alive) { t = idx; break; }
        }
        if (t < 0) return 0;

        int dmg = e->magic * sp.power + (rand() % 5);
        if (party[t].defending) dmg /= 2;

        party[t].hp -= dmg;

        sfx_beep(1);
        printf(RED "敵は %s! %s に %d ダメージ!\n" RESET, sp.name, party[t].name, dmg);

        if (party[t].hp <= 0) {
            party[t].hp = 0;
            party[t].alive = 0;
            sfx_beep(4);
            printf(RED "%s は倒れた…\n" RESET, party[t].name);
        }
        return 1;
    }
    else if (sp.kind == EN_SPELL_HEAL) {
        int heal = sp.power + (rand() % 8);
        e->hp += heal;
        if (e->hp > e->max_hp) e->hp = e->max_hp;

        sfx_beep(2);
        printf(RED "敵は %s! HPが回復した!(+%d)\n" RESET, sp.name, heal);
        return 1;
    }
    else if (sp.kind == EN_SPELL_ATKUP) {
        e->atk += sp.power;

        sfx_beep(0);
        printf(RED "敵は %s! ATKが上がった!(+%d)\n" RESET, sp.name, sp.power);
        return 1;
    }
    else {
        e->defending = 1;

        sfx_beep(0);
        printf(RED "敵は %s! 次の被ダメージを軽減!\n" RESET, sp.name);
        return 1;
    }
}

/* =========================================================
   バトル終了待ち(Enterでマップ復帰)
   ========================================================= */
static void wait_enter_to_return(void) {
    printf("\n" CYAN "バトル終了!Enterキーでマップ画面に戻ります。\n" RESET);
    char buf[64];

    /* Enterのみ(空行)を待つ */
    while (1) {
        read_line(buf, sizeof(buf));
        if (buf[0] == '\0') return;
    }
}

/* =========================================================
   ボス撃破後待ち(Enterでエンディングへ)
   ========================================================= */
static void wait_enter_to_ending(void) {
    printf("\n" MAGENTA "ボス撃破!Enterキーでエンディングへ進みます。\n" RESET);
    char buf[64];

    /* Enterのみ(空行)を待つ */
    while (1) {
        read_line(buf, sizeof(buf));
        if (buf[0] == '\0') return;
    }
}

/* =========================================================
   バトル
   ========================================================= */
static int battle(GameState* gs, Enemy enemy) {
    clear_screen();
    printf(RED "%s が現れた!\n" RESET, enemy.name);
    sfx_beep(4);

    while (enemy.hp > 0) {
        int alive_cnt = 0;
        for (int i = 0; i < PARTY_SIZE; i++) if (gs->party[i].alive) alive_cnt++;
        if (alive_cnt == 0) {
            printf(RED "\nパーティは全滅した…\n" RESET);
            wait_enter_to_return();
            return 0;
        }

        printf("\n");
        print_party_status(gs->party);
        print_enemy_status(&enemy);
        printf("\n");

        for (int i = 0; i < PARTY_SIZE; i++) {
            Character* c = &gs->party[i];
            if (!c->alive) continue;

            c->defending = 0;

            if (enemy.hp <= 0) break;

            printf("\n%s の行動を選んでください\n", c->name);
            printf("  1:戦う  2:魔法  3:アイテム  4:防御\n");
            printf("選択: ");

            int cmd = 0;
            if (!read_int_safely(&cmd)) cmd = 0;

            if (cmd == 1) {
                int dmg = char_atk(c) + (rand() % 3);
                if (enemy.defending) { dmg /= 2; enemy.defending = 0; }

                enemy.hp -= dmg;
                if (enemy.hp < 0) enemy.hp = 0;

                sfx_beep(1);
                printf(YELLOW "%s の攻撃! %d ダメージ!\n" RESET, c->name, dmg);
            }
            else if (cmd == 2) {
                printf("魔法を選択してください\n");
                for (int s = 0; s < SPELLS_PER_CHAR; s++) {
                    printf("  %d:%s (MP%d)\n", s + 1, c->spells[s].name, c->spells[s].mp_cost);
                }
                printf("選択: ");
                int si = 0;
                if (!read_int_safely(&si)) si = 0;
                si--;

                if (!cast_spell(c, gs->party, &enemy, si)) {
                    printf("行動失敗。通常攻撃に切り替えます。\n");
                    int dmg = char_atk(c);
                    if (enemy.defending) { dmg /= 2; enemy.defending = 0; }

                    enemy.hp -= dmg;
                    if (enemy.hp < 0) enemy.hp = 0;

                    sfx_beep(1);
                    printf(YELLOW "%s の攻撃! %d ダメージ!\n" RESET, c->name, dmg);
                }
            }
            else if (cmd == 3) {
                if (!any_item_available(gs->inv)) {
                    printf(RED "アイテムがない!\n" RESET);
                    continue;
                }

                show_inventory(gs->inv);
                printf("使用/装備するアイテム番号: ");
                int id = 0;
                if (!read_int_safely(&id)) continue;
                id--;

                if (id < 0 || id >= ITEM_COUNT || gs->inv[id] <= 0) {
                    printf(RED "選択が不正です。\n" RESET);
                    continue;
                }

                ItemID item_id = (ItemID)id;

                if (g_items[item_id].type == IT_CONSUME) {
                    printf("対象を選んでください (1-%d): ", PARTY_SIZE);
                    int t = 0;
                    if (!read_int_safely(&t)) continue;
                    if (t < 1 || t > PARTY_SIZE) continue;

                    Character* target = &gs->party[t - 1];
                    if (!target->alive) {
                        printf(RED "その仲間は倒れている!\n" RESET);
                        continue;
                    }

                    gs->inv[item_id]--;
                    use_consumable_on(target, item_id);
                    printf(GREEN "%s は %s を使った!\n" RESET, c->name, g_items[item_id].name);
                }
                else {
                    if (!equip_item(c, item_id)) {
                        printf(RED "その職業では装備できない!\n" RESET);
                        sfx_beep(4);
                    }
                }
            }
            else if (cmd == 4) {
                c->defending = 1;
                sfx_beep(0);
                printf(CYAN "%s は身を守っている…(次の被ダメージ軽減)\n" RESET, c->name);
            }
            else {
                printf("迷っている…(通常攻撃)\n");
                int dmg = char_atk(c);
                if (enemy.defending) { dmg /= 2; enemy.defending = 0; }

                enemy.hp -= dmg;
                if (enemy.hp < 0) enemy.hp = 0;

                sfx_beep(1);
                printf(YELLOW "%s の攻撃! %d ダメージ!\n" RESET, c->name, dmg);
            }
        }

        if (enemy.hp <= 0) break;

        printf("\n");
        if (enemy_try_cast_spell(&enemy, gs->party)) {
            /* 魔法が通った */
        }
        else {
            int t = -1;
            for (int tries = 0; tries < 100; tries++) {
                int idx = rand() % PARTY_SIZE;
                if (gs->party[idx].alive) { t = idx; break; }
            }

            if (t >= 0) {
                int dmg = enemy.atk + (rand() % 4);
                if (gs->party[t].defending) dmg /= 2;

                gs->party[t].hp -= dmg;

                sfx_beep(1);
                printf(RED "敵の攻撃! %s に %d ダメージ!\n" RESET, gs->party[t].name, dmg);

                if (gs->party[t].hp <= 0) {
                    gs->party[t].hp = 0;
                    gs->party[t].alive = 0;
                    sfx_beep(4);
                    printf(RED "%s は倒れた…\n" RESET, gs->party[t].name);
                }
            }
        }
    }

    sfx_beep(3);
    printf(GREEN "\n勝利!\n" RESET);

    if (!enemy.is_boss) {
        printf("経験値 %d を獲得!\n", enemy.exp);
        add_exp_party(gs->party, enemy.exp);

        /* 通常戦:Enterでマップへ */
        wait_enter_to_return();
    }
    else {
        printf(YELLOW "ボスを倒した!\n" RESET);

        /* ボス戦:Enterでエンディングへ */
        wait_enter_to_ending();
    }

    return 1;
}

/* =========================================================
   メイン
   ========================================================= */
int main(void) {
    srand((unsigned)time(NULL));

    GameState gs;
    memset(&gs, 0, sizeof(gs));

    show_title();

    load_map_template(gs.map);
    gs.px = 1; gs.py = 1;
    gs.game_clear = 0;
    init_characters(gs.party);

    gs.inv[ITEM_POTION] = 2;
    gs.inv[ITEM_ETHER] = 1;

    while (1) {
        clear_screen();
        draw_map(&gs);

        printf("\nコマンド: 4/6/8/2=移動  s=セーブ  l=ロード  q=終了\n");
        printf("入力: ");

        char buf[64];
        read_line(buf, sizeof(buf));
        if (buf[0] == '\0') continue;

        if (buf[0] == 'q') {
            printf("終了します。\n");
            break;
        }

        if (buf[0] == 's') {
            if (save_game(&gs)) {
                sfx_beep(0);
                printf(GREEN "セーブしました。(%s)\n" RESET, SAVE_FILE);
            }
            else {
                sfx_beep(4);
                printf(RED "セーブに失敗しました。\n" RESET);
            }
            printf("Enterで続行...");
            read_line(buf, sizeof(buf));
            continue;
        }

        if (buf[0] == 'l') {
            GameState tmp;
            if (load_game(&tmp)) {
                gs = tmp;
                sfx_beep(0);
                printf(GREEN "ロードしました。(%s)\n" RESET, SAVE_FILE);
            }
            else {
                sfx_beep(4);
                printf(RED "ロードに失敗しました。(セーブデータがない可能性)\n" RESET);
            }
            printf("Enterで続行...");
            read_line(buf, sizeof(buf));
            continue;
        }

        int key = atoi(buf);
        int nx = gs.px, ny = gs.py;

        if (key == 4) ny--;
        else if (key == 6) ny++;
        else if (key == 8) nx--;
        else if (key == 2) nx++;
        else continue;

        if (nx < 0 || nx >= MAP_H || ny < 0 || ny >= MAP_W) continue;

        if (gs.map[nx][ny] != '#') {
            gs.px = nx; gs.py = ny;
        }

        char cell = gs.map[gs.px][gs.py];

        if (cell == 'T') {
            clear_screen();
            open_treasure_fixed(&gs, gs.px, gs.py);
            gs.map[gs.px][gs.py] = ' ';
            printf("Enterで続行...");
            read_line(buf, sizeof(buf));
        }
        else if (cell == 'I') {
            clear_screen();
            inn_rest(&gs);
            printf("Enterで続行...");
            read_line(buf, sizeof(buf));
        }
        else if (cell == 'C') {
            clear_screen();
            church_revive(&gs);
            printf("Enterで続行...");
            read_line(buf, sizeof(buf));
        }
        else if (cell == 'E') {
            Enemy e = make_random_enemy();

            int win = battle(&gs, e);
            if (win) {
                gs.map[gs.px][gs.py] = ' ';
                respawn_enemy(&gs);
            }
            else {
                clear_screen();
                printf(RED "ゲームオーバー…\n" RESET);
                printf("Enterでタイトルへ...");
                read_line(buf, sizeof(buf));

                show_title();
                load_map_template(gs.map);
                gs.px = 1; gs.py = 1;
                init_characters(gs.party);
                memset(gs.inv, 0, sizeof(gs.inv));
                gs.inv[ITEM_POTION] = 2;
                gs.inv[ITEM_ETHER] = 1;
            }
        }
        else if (cell == 'B') {
            Enemy boss;
            memset(&boss, 0, sizeof(boss));

            snprintf(boss.name, sizeof(boss.name), "マスタードラゴン");

            boss.max_hp = 220; boss.hp = 220;
            boss.max_mp = 35;  boss.mp = 35;

            boss.atk = 18;
            boss.magic = 14;

            boss.exp = 0;
            boss.is_boss = 1;

            boss.defending = 0;

            boss.spells[0] = (EnemySpell){ "ドラゴンブレス", EN_SPELL_DMG,   8, 3 };
            boss.spells[1] = (EnemySpell){ "深紅の再生",     EN_SPELL_HEAL, 10, 28 };
            boss.spells[2] = (EnemySpell){ "覇王の咆哮",     EN_SPELL_ATKUP, 8,  3 };
            boss.spells[3] = (EnemySpell){ "鱗の要塞",       EN_SPELL_DEFUP, 7,  0 };

            int win = battle(&gs, boss);
            if (win) {
                gs.game_clear = 1;
                show_ending();
                break;
            }
            else {
                clear_screen();
                printf(RED "マスタードラゴンに敗北した…\n" RESET);
                printf("Enterでタイトルへ...");
                read_line(buf, sizeof(buf));

                show_title();
                load_map_template(gs.map);
                gs.px = 1; gs.py = 1;
                init_characters(gs.party);
                memset(gs.inv, 0, sizeof(gs.inv));
                gs.inv[ITEM_POTION] = 2;
                gs.inv[ITEM_ETHER] = 1;
            }
        }
    }

    return 0;
}

このRPGを作り切ったあなたへ

このプログラムを最後まで理解し、
実装を追いかけたあなたは、

  • 構造体設計
  • 状態管理
  • 関数分割
  • ゲームロジック

実践レベルで身につけています

次のステップとしては、

  • ファイル分割(15章)
  • 機能追加(スキル・状態異常)
  • UI改善
  • データ駆動設計

など、いくらでも発展させられます。

STEP21まとめ

  • rpg1.c は「設計 → 実装 → 完結」まで揃った教材
  • main は司令塔、処理は関数に分離
  • GameState がゲーム全体を管理
  • 戦闘・探索・成長が1本の流れでつながっている。

🎉 これで、バトルRPGゲーム制作編は完走です!
本当にお疲れさまでした。