総合演習

演習問題

本章で学習したクラス構文、プライベートフィールド、ゲッター/セッター、プロトコルベースのポリモーフィズム、プロトタイプ、および this の制御を活用して、簡易的なロールプレイングゲーム(RPG)の戦闘シミュレーターを構築しましょう。 以下の仕様を満たすプログラムを作成しなさい。

  1. 戦闘に参加するプレイヤーキャラクターの共通の基底クラスとして、Character を定義すること。
    • 名前(#name)と現在の体力値(#hp)をプライベートフィールドとして定義すること。
    • コンストラクタで、名前と体力を受け取り初期化すること。
    • name ゲッターを持ち、名前を返すこと。
    • hp ゲッターを持ち、現在の体力を返すこと。
    • hp セッターを持ち、設定された値が負の値になる場合は 0 に補正し、生存判定に用いること。
    • isAlive ゲッターを持ち、体力が 0 より大きい場合は true0 の場合は false を返すこと。
  2. Character クラスを継承した Hero クラスと Mage クラスを定義すること。
    • Hero クラスはコンストラクタで名前、体力、および物理攻撃力 attackPower を受け取ること。独自の attack(target) メソッドを持ち、相手に this.attackPower 分のダメージを与え、"[名前] の攻撃! [相手の名前] に [ダメージ] のダメージを与えた。" と表示すること。
    • Mage クラスはコンストラクタで名前、体力、魔法攻撃力 magicPower、および魔法力 mp を受け取ること。attack(target) メソッドをオーバーライドし、現在の mp10 以上の場合は mp10 消費して this.magicPower * 2 の魔法ダメージを与え、"[名前] は呪文を唱えた! [相手の名前] に [ダメージ] の魔法ダメージを与えた。" と表示すること。mp が不足している場合は、通常の物理攻撃(ダメージ 5)を行い、"[名前] の杖での攻撃! [相手の名前] に 5 のダメージを与えた。" と表示すること。
  3. クラス構文を使わず、プロトタイプオブジェクトを用いて敵キャラクターを定義すること。
    • 共通のプロトタイプオブジェクトである enemyPrototype を作成すること。これには、生存判定を行う isAlive() メソッド(自身の hp プロパティが 0 より大きいかで判定)と、攻撃を行う attack(target) メソッド(自身の attackPower 分のダメージを与え、メッセージを出力する)を定義すること。
    • Object.create(enemyPrototype) メソッドを使用して、具体的な敵キャラクターである slime(名前: "スライム", 体力: 50, 攻撃力: 10)と goblin(名前: "ゴブリン", 体力: 80, 攻撃力: 15)のオブジェクトを作成すること。
  4. ターンごとのキャラクター行動(アクション)を管理する TurnManager クラスを定義すること。
    • ターンごとに実行するアクション関数を保持する配列 #actions をプライベートフィールドとして持つこと。
    • registerAction(action) メソッドを持ち、実行したいアクション関数を配列に追加すること。
    • executeTurn() メソッドを持ち、登録されたアクション関数を順番に呼び出して実行したのち、登録アクションをすべてクリアすること。
  5. 作成したクラスとオブジェクトを用いて戦闘のセットアップを行い、ターンを実行するプログラムを書きなさい。
    • HeroMage、および slimegoblin を作成すること。
    • TurnManager を作成し、それぞれのキャラクターの攻撃処理(attack メソッド)をコールバック関数として登録すること。この際、this が消失しないようにアロー関数または bind() メソッドを用いて適切なバインド処理を行うこと。
    • TurnManagerexecuteTurn() を実行し、それぞれの攻撃アクションが正しく対象キャラクターに実行されることを確認しなさい。

解答例

演習問題のプログラムの実装例は以下の通りである。

実装の方針として、まず基底クラス Character にプライベートフィールドとゲッター/セッターを定義して体力の安全な管理をカプセル化する。 次にそれを継承した HeroMage を作成して、同じ attack(target) メソッドで異なる攻撃手段を提供するポリモーフィズムを実現する。 敵キャラクター側は、クラス定義を用いずに Object.create() によるプロトタイプオブジェクトからの生成を行い、クラスベースと同様のポリモーフィズムに対応する。 最後に、コールバック処理での this 消失を考慮しながら、TurnManagerbind を用いてアクションを登録し、戦闘処理を一括で実行する。

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

results matching ""

    No results matching ""