はじめに
前回は、ゲームフリークのバトルシステムを題材に、Section・Event・EventHandler の役割を整理しました。
ポケモンバトルの仕様が複雑すぎるので、ゲームフリークのイベント駆動設計を学んでみる
今回はまだイベント駆動にはしません。Section に分ける前に、バトル開始から1ターン終了までを普通の関数呼び出しで動かします。
題材はガブリアス対カバルドンの1対1、1ターンです。両者の初期HPは100、素早さは120と60、技は両方「じしん」、ダメージは固定20です。特性、持ち物、ダメージ計算、ひんし判定はありません。数値は学習用の仮値です。
シーケンス
main.ts が起動と結果表示、battle.ts がバトル開始、turn.ts がターンの順番、actions.ts が行動選択・行動順・攻撃です。状態は BattleState を渡して直接更新します。
コード
状態はターン番号、ポケモン、実行ログだけです。
export type Pokemon = {
id: PokemonId;
name: string;
currentHp: number;
maxHp: number;
speed: number;
};
export type Action = {
actorId: PokemonId;
targetId: PokemonId;
move: string;
};
export type BattleState = {
turn: number;
pokemon: Pokemon[];
logs: string[];
};
バトル開始で状態を作り、2体を登場させてからターンへ進みます。
export function runBattle(): BattleState {
const state: BattleState = {
turn: 0,
pokemon: [],
logs: [],
};
state.logs.push("=== バトル開始 ===");
const garchomp = createGarchomp();
state.pokemon.push(garchomp);
state.logs.push(`${garchomp.name}が登場`);
const hippowdon = createHippowdon();
state.pokemon.push(hippowdon);
state.logs.push(`${hippowdon.name}が登場`);
return runTurn(state);
}
ターンは、番号を1にしてから選択、順序、実行、終了です。
export function runTurn(state: BattleState): BattleState {
state.turn += 1;
state.logs.push(`=== ターン${state.turn} 開始 ===`);
const actions = selectActions(state);
const orderedActions = decideActionOrder(actions, state);
for (const action of orderedActions) {
executeAction(state, action);
}
state.logs.push(`=== ターン${state.turn} 終了 ===`);
return state;
}
行動選択では両者とも相手に「じしん」を選びます。HPはここでは変えません。
export function selectActions(state: BattleState): Action[] {
return state.pokemon.map((pokemon) => {
const target = state.pokemon.find((candidate) => candidate.id !== pokemon.id);
if (target === undefined) {
throw new Error("対象ポケモンが見つかりません");
}
const action: Action = {
actorId: pokemon.id,
targetId: target.id,
move: MOVE_EARTHQUAKE,
};
state.logs.push(`${pokemon.name}:${action.move}`);
return action;
});
}
行動順は素早さが高い順です。ガブリアスが先になります。
export function decideActionOrder(actions: Action[], state: BattleState): Action[] {
const ordered = [...actions].sort((left, right) => {
return findPokemon(state, right.actorId).speed - findPokemon(state, left.actorId).speed;
});
state.logs.push("行動順:");
for (const [index, action] of ordered.entries()) {
state.logs.push(`${index + 1}. ${findPokemon(state, action.actorId).name}`);
}
return ordered;
}
攻撃はログを残し、相手のHPから20を引きます。
export function executeAction(state: BattleState, action: Action): BattleState {
const actor = findPokemon(state, action.actorId);
const target = findPokemon(state, action.targetId);
state.logs.push(`${actor.name}の${action.move}!`);
target.currentHp -= DAMAGE;
state.logs.push(`${target.name}に${DAMAGE}ダメージ!`);
return state;
}
runBattle() ごとに新しい状態とログを作ります。ログは配列へ追記し、空行や最終HPの表示は main.ts が最後に行います。
実行結果
=== バトル開始 ===
ガブリアスが登場
カバルドンが登場
=== ターン1 開始 ===
ガブリアス:じしん
カバルドン:じしん
行動順:
1. ガブリアス
2. カバルドン
ガブリアスのじしん!
カバルドンに20ダメージ!
カバルドンのじしん!
ガブリアスに20ダメージ!
=== ターン1 終了 ===
ガブリアス HP:80 / 100
カバルドン HP:80 / 100
ターン番号は1、両者のHPは80です。ガブリアスが先に攻撃しています。
次回は、この実行結果を変えずに処理を Section へ分けます。