総合演習
演習問題
本章で学習したクラス構文、プライベートフィールド、ゲッター/セッター、プロトコルベースのポリモーフィズム、プロトタイプ、および this の制御を活用して、簡易的なロールプレイングゲーム(RPG)の戦闘シミュレーターを構築しましょう。
以下の仕様を満たすプログラムを作成しなさい。
- 戦闘に参加するプレイヤーキャラクターの共通の基底クラスとして、
Characterを定義すること。- 名前(
#name)と現在の体力値(#hp)をプライベートフィールドとして定義すること。 - コンストラクタで、名前と体力を受け取り初期化すること。
nameゲッターを持ち、名前を返すこと。hpゲッターを持ち、現在の体力を返すこと。hpセッターを持ち、設定された値が負の値になる場合は0に補正し、生存判定に用いること。isAliveゲッターを持ち、体力が0より大きい場合はtrue、0の場合はfalseを返すこと。
- 名前(
Characterクラスを継承したHeroクラスとMageクラスを定義すること。Heroクラスはコンストラクタで名前、体力、および物理攻撃力attackPowerを受け取ること。独自のattack(target)メソッドを持ち、相手にthis.attackPower分のダメージを与え、"[名前] の攻撃! [相手の名前] に [ダメージ] のダメージを与えた。"と表示すること。Mageクラスはコンストラクタで名前、体力、魔法攻撃力magicPower、および魔法力mpを受け取ること。attack(target)メソッドをオーバーライドし、現在のmpが10以上の場合はmpを10消費してthis.magicPower * 2の魔法ダメージを与え、"[名前] は呪文を唱えた! [相手の名前] に [ダメージ] の魔法ダメージを与えた。"と表示すること。mpが不足している場合は、通常の物理攻撃(ダメージ5)を行い、"[名前] の杖での攻撃! [相手の名前] に 5 のダメージを与えた。"と表示すること。
- クラス構文を使わず、プロトタイプオブジェクトを用いて敵キャラクターを定義すること。
- 共通のプロトタイプオブジェクトである
enemyPrototypeを作成すること。これには、生存判定を行うisAlive()メソッド(自身のhpプロパティが0より大きいかで判定)と、攻撃を行うattack(target)メソッド(自身のattackPower分のダメージを与え、メッセージを出力する)を定義すること。 Object.create(enemyPrototype)メソッドを使用して、具体的な敵キャラクターであるslime(名前: "スライム", 体力: 50, 攻撃力: 10)とgoblin(名前: "ゴブリン", 体力: 80, 攻撃力: 15)のオブジェクトを作成すること。
- 共通のプロトタイプオブジェクトである
- ターンごとのキャラクター行動(アクション)を管理する
TurnManagerクラスを定義すること。- ターンごとに実行するアクション関数を保持する配列
#actionsをプライベートフィールドとして持つこと。 registerAction(action)メソッドを持ち、実行したいアクション関数を配列に追加すること。executeTurn()メソッドを持ち、登録されたアクション関数を順番に呼び出して実行したのち、登録アクションをすべてクリアすること。
- ターンごとに実行するアクション関数を保持する配列
- 作成したクラスとオブジェクトを用いて戦闘のセットアップを行い、ターンを実行するプログラムを書きなさい。
HeroとMage、およびslimeとgoblinを作成すること。TurnManagerを作成し、それぞれのキャラクターの攻撃処理(attackメソッド)をコールバック関数として登録すること。この際、thisが消失しないようにアロー関数またはbind()メソッドを用いて適切なバインド処理を行うこと。TurnManagerのexecuteTurn()を実行し、それぞれの攻撃アクションが正しく対象キャラクターに実行されることを確認しなさい。
解答例
演習問題のプログラムの実装例は以下の通りである。
実装の方針として、まず基底クラス Character にプライベートフィールドとゲッター/セッターを定義して体力の安全な管理をカプセル化する。
次にそれを継承した Hero と Mage を作成して、同じ attack(target) メソッドで異なる攻撃手段を提供するポリモーフィズムを実現する。
敵キャラクター側は、クラス定義を用いずに Object.create() によるプロトタイプオブジェクトからの生成を行い、クラスベースと同様のポリモーフィズムに対応する。
最後に、コールバック処理での this 消失を考慮しながら、TurnManager に bind を用いてアクションを登録し、戦闘処理を一括で実行する。
rpg_battle.js
// 1. 基底クラスの定義
class Character {
#name;
#hp;
constructor(name, hp) {
this.#name = name;
this.#hp = hp;
}
get name() {
return this.#name;
}
get hp() {
return this.#hp;
}
set hp(value) {
if (value < 0) {
this.#hp = 0;
} else {
this.#hp = value;
}
}
get isAlive() {
return this.#hp > 0;
}
}
// 2. プレイヤーキャラクターの定義(クラス継承)
class Hero extends Character {
constructor(name, hp, attackPower) {
super(name, hp);
this.attackPower = attackPower;
}
attack(target) {
if (!this.isAlive) return;
target.hp -= this.attackPower;
console.log(`${this.name} の攻撃! ${target.name} に ${this.attackPower} のダメージを与えた。`);
}
}
class Mage extends Character {
constructor(name, hp, magicPower, mp) {
super(name, hp);
this.magicPower = magicPower;
this.mp = mp;
}
attack(target) {
if (!this.isAlive) return;
if (this.mp >= 10) {
this.mp -= 10;
const damage = this.magicPower * 2;
target.hp -= damage;
console.log(`${this.name} は呪文を唱えた! ${target.name} に ${damage} の魔法ダメージを与えた。`);
} else {
const damage = 5;
target.hp -= damage;
console.log(`${this.name} の杖での攻撃! ${target.name} に ${damage} のダメージを与えた。`);
}
}
}
// 3. 敵キャラクターの定義(プロトタイプベース)
const enemyPrototype = {
isAlive() {
return this.hp > 0;
},
attack(target) {
if (!this.isAlive()) return;
target.hp -= this.attackPower;
console.log(`${this.name} の反撃! ${target.name} に ${this.attackPower} のダメージを与えた。`);
}
};
const slime = Object.create(enemyPrototype);
slime.name = "スライム";
slime.hp = 50;
slime.attackPower = 10;
const goblin = Object.create(enemyPrototype);
goblin.name = "ゴブリン";
goblin.hp = 80;
goblin.attackPower = 15;
// 4. ターンマネージャーの定義(thisの消失対策を要するコールバック管理)
class TurnManager {
#actions = [];
registerAction(action) {
this.#actions.push(action);
}
executeTurn() {
console.log("--- ターンの実行 ---");
for (const action of this.#actions) {
action();
}
this.#actions = [];
console.log("------------------");
}
}
// 5. 戦闘の実行
const hero = new Hero("勇者", 100, 20);
const mage = new Mage("魔法使い", 60, 15, 15);
const manager = new TurnManager();
// メソッドをそのまま渡すとthisが消失するため、bindでインスタンスをバインドする
manager.registerAction(hero.attack.bind(hero, slime));
manager.registerAction(mage.attack.bind(mage, goblin));
// 敵キャラクターの攻撃も同様にバインドして登録する
manager.registerAction(slime.attack.bind(slime, hero));
manager.registerAction(goblin.attack.bind(goblin, mage));
// ターンを実行
manager.executeTurn();
// 実行後のHPステータス確認
console.log(`${hero.name} の残りHP: ${hero.hp}`);
console.log(`${mage.name} の残りHP: ${mage.hp}`);
console.log(`${slime.name} の残りHP: ${slime.hp}`);
console.log(`${goblin.name} の残りHP: ${goblin.hp}`);
実行結果
--- ターンの実行 ---
勇者 の攻撃! スライム に 20 のダメージを与えた。
魔法使い は呪文を唱えた! ゴブリン に 30 の魔法ダメージを与えた。
スライム の反撃! 勇者 に 10 のダメージを与えた。
ゴブリン の反撃! 魔法使い に 15 のダメージを与えた。
------------------
勇者 の残りHP: 90
魔法使い の残りHP: 45
スライム の残りHP: 30
ゴブリン の残りHP: 50