
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 関数の中身(流れ)
- 敵出現演出
- ターン制ループ
- 味方の行動選択
- 敵の行動(AI)
- 勝利・敗北判定
通常戦もボス戦も、
同じ関数で処理できる設計 がとても美しいです。
マップ探索とイベント処理の連携
マップは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ファイルに詰まっています。
#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ゲーム制作編は完走です!
本当にお疲れさまでした。
