
STEP12:敵AIの実装(HP割合で回復優先・ランダム性・防御行動)
STEP12では、戦闘が「作業」にならないための 핵(コア)――敵AI を入れます。
前STEPまでの敵は「殴るだけ」や「たまに魔法」だったと思うので、
- HPが減ったら回復を優先する。
- 行動にランダム性を混ぜて読めなくする。
- 防御(Defend)を選ぶこともある。
この3点を実装して、「それっぽい敵」へ進化させます。
敵AIの設計方針(enemies層 vs battle層の役割)
敵AIは 「何をするかを決める」 だけに寄せると保守しやすいです。
実際のダメージ適用・回復適用は battle層が担当します。
| 役割 | enemies層 | battle層 |
|---|---|---|
| 行動決定(攻撃/回復/防御/魔法) | ✅ | |
| 対象決定(誰を狙うか) | ✅ (提案) | ✅ (最終) |
| HP/MPの増減、ダメージ適用 | ✅ | |
| defending状態の更新 | ✅ (敵も味方も統一) |
このSTEPでは enemies層に 「行動を返す関数」 を追加します。
行動を表すデータ構造を作る(EnemyAction)
enemies.h に追加(例)
#ifndef ENEMIES_H
#define ENEMIES_H
#include "common.h"
typedef enum {
EA_ATTACK = 0,
EA_CAST_DAMAGE = 1,
EA_CAST_HEAL = 2,
EA_DEFEND = 3
} EnemyActionType;
typedef struct {
EnemyActionType type;
int spell_index; /* 魔法を使う場合だけ使用(-1なら未使用) */
int target_index; /* 攻撃対象(partyの添字)。-1ならbattle側が決める */
} EnemyAction;
EnemyAction enemies_decide_action(const Enemy* e);
/* 既存:生成、名前、魔法などがあるならそのまま */
#endif
ポイント
- AIは 「type(何する)」 と 「補助情報(spell/target)」 を返すだけ
- battle層はこの結果を見て適用する
敵AIの材料(HP割合・乱数・性格)
敵が意思決定するために最低限必要なのは HP割合 と 乱数 です。
- hp_ratio = e->hp * 100 / e->max_hp(整数でOK)
- ランダム:rand()%100
また、敵タイプによってAIが変わると面白いので、テンプレートに「性格(AI傾向)」を持たせるのがオススメです。
例:Enemyに追加したい項目(common.h/敵構造体側)
- int ai_heal_threshold;(回復優先に切り替えるHP%)
- int ai_defend_rate;(防御を選ぶ確率%)
- int ai_random_rate;(ランダム行動の混ぜ率%)
今回は既存構造体を壊したくない想定で、固定ルールで作ります(後から拡張しやすい形)。
enemies_decide_action の実装(HP割合で回復優先)
基本ルール(今回の仕様)
- HPが 35%以下 → 回復魔法があるなら 回復優先(ただし80%)
- それ以外 → 通常は攻撃(60%)+ 攻撃魔法(25%)+ 防御(15%)
- さらに「ランダム性」を入れる:一定確率で重みを崩す(フェイント)
enemies.c(例)
#include "enemies.h"
#include <stdlib.h>
static int hp_ratio_percent(const Enemy* e) {
if (e->max_hp <= 0) return 0;
return (e->hp * 100) / e->max_hp;
}
static int has_heal_spell(const Enemy* e, int* out_index) {
for (int i = 0; i < ENEMY_SPELL_MAX; i++) {
if (e->spells[i].mp_cost <= 0) continue;
if (e->spells[i].type == SPELL_HEAL) {
if (out_index) *out_index = i;
return 1;
}
}
return 0;
}
static int has_damage_spell(const Enemy* e, int* out_index) {
/* 複数あるならランダムで1つ選ぶ(簡易) */
int list[ENEMY_SPELL_MAX];
int n = 0;
for (int i = 0; i < ENEMY_SPELL_MAX; i++) {
if (e->spells[i].mp_cost <= 0) continue;
if (e->spells[i].type == SPELL_DMG) {
list[n++] = i;
}
}
if (n == 0) return 0;
if (out_index) *out_index = list[rand() % n];
return 1;
}
/* 重み付き抽選:0..sum-1 を引いて決める */
static int pick_weighted3(int w0, int w1, int w2) {
int sum = w0 + w1 + w2;
if (sum <= 0) return 0;
int r = rand() % sum;
if (r < w0) return 0;
r -= w0;
if (r < w1) return 1;
return 2;
}
EnemyAction enemies_decide_action(const Enemy* e) {
EnemyAction a;
a.type = EA_ATTACK;
a.spell_index = -1;
a.target_index = -1;
int hp = hp_ratio_percent(e);
/* 1) 低HPなら回復優先 */
if (hp <= 35) {
int heal_i = -1;
if (has_heal_spell(e, &heal_i) && e->mp >= e->spells[heal_i].mp_cost) {
int r = rand() % 100;
/* 80%で回復、20%でフェイント(防御or攻撃) */
if (r < 80) {
a.type = EA_CAST_HEAL;
a.spell_index = heal_i;
return a;
}
/* フェイント:半々で防御/攻撃 */
if (r < 90) {
a.type = EA_DEFEND;
return a;
}
a.type = EA_ATTACK;
return a;
}
/* 回復できないなら防御寄りにする */
if ((rand() % 100) < 35) {
a.type = EA_DEFEND;
return a;
}
}
/* 2) 通常時:攻撃/攻撃魔法/防御を重みで選ぶ */
int dmg_i = -1;
int can_dmg_spell = has_damage_spell(e, &dmg_i) && (e->mp >= e->spells[dmg_i].mp_cost);
int w_attack = 60;
int w_spell = can_dmg_spell ? 25 : 0;
int w_defend = 15;
/* 3) ランダム性:15%で重みを崩す(予測不能にする) */
if ((rand() % 100) < 15) {
w_attack = 45;
w_spell = can_dmg_spell ? 35 : 0;
w_defend = 20;
}
int pick = pick_weighted3(w_attack, w_spell, w_defend);
if (pick == 1 && can_dmg_spell) {
a.type = EA_CAST_DAMAGE;
a.spell_index = dmg_i;
return a;
}
if (pick == 2) {
a.type = EA_DEFEND;
return a;
}
a.type = EA_ATTACK;
return a;
}battle層に接続する(敵ターンの置き換え)
STEP11では敵ターンで
- 魔法を打つか判定 → 失敗なら通常攻撃
みたいな処理になっていたと思います。
これを EnemyAction を受け取って実行する形に置き換えます。
battle.c の敵ターン例
static void enemy_take_turn(Enemy* enemy, Character party[PARTY_SIZE]) {
EnemyAction act = enemies_decide_action(enemy);
if (act.type == EA_DEFEND) {
enemy->defending = 1;
printf("%s is defending!\n", enemy->name);
sfx_beep(0);
return;
}
if (act.type == EA_CAST_HEAL) {
Spell sp = enemy->spells[act.spell_index];
if (enemy->mp >= sp.mp_cost) {
enemy->mp -= sp.mp_cost;
int before = enemy->hp;
enemy->hp += sp.power;
if (enemy->hp > enemy->max_hp) enemy->hp = enemy->max_hp;
printf("%s casts %s! HP +%d\n", enemy->name, sp.name, enemy->hp - before);
sfx_beep(2);
return;
}
/* MP足りないなら通常攻撃にフォールバック */
}
if (act.type == EA_CAST_DAMAGE) {
Spell sp = enemy->spells[act.spell_index];
if (enemy->mp >= sp.mp_cost) {
enemy->mp -= sp.mp_cost;
int t = pick_random_alive_index(party);
if (t >= 0) {
int base = enemy->base_mag * sp.power - party[t].def;
base = clamp_min(base, 1);
int dmg = apply_variance(base);
printf("%s casts %s!\n", enemy->name, sp.name);
apply_damage_to_char(&party[t], dmg);
return;
}
}
/* 失敗なら通常攻撃へ */
}
enemy_attack_party(enemy, party);
}そして battle_run の敵ターン部分を
printf("\nEnemy turn\n");
enemy_take_turn(enemy, party);に置き換えればOKです。
動作確認(AIっぽさをチェック)
| 状況 | 期待する動き |
|---|---|
| 敵HPが35%以下 | 回復魔法があれば高確率で回復 |
| 敵HPが少ないが回復できない | 防御が混ざる(粘る) |
| 通常時 | 攻撃中心だが、たまに防御・たまに攻撃魔法 |
| 毎ターン同じにならない | ランダム性で偏りが崩れる。 |
演習問題+模範解答例
演習:敵の「瀕死モード」を追加する
問題
敵HPが15%以下になったら、行動比率を
防御 40% / 回復 40% / 攻撃 20% に変更してください(回復魔法がある場合)。
模範解答例(enemies_decide_action の低HP分岐の中に追加)
if (hp <= 15) {
int heal_i = -1;
int can_heal = has_heal_spell(e, &heal_i) && e->mp >= e->spells[heal_i].mp_cost;
int r = rand() % 100;
if (can_heal && r < 40) {
a.type = EA_CAST_HEAL;
a.spell_index = heal_i;
return a;
}
if (r < 80) {
a.type = EA_DEFEND;
return a;
}
a.type = EA_ATTACK;
return a;
}STEP12のまとめ
- enemies層に「行動決定関数(EnemyAction)」を追加すると、AI拡張が簡単
- HP割合で回復優先を入れるだけで敵が“生き物っぽく”なる。
- 防御行動を混ぜると、戦闘が単調になりにくい。
- ランダム性は「少しだけ」入れるのがコツ(入れすぎると理不尽)
