Strategy。アルゴリズムの切り替え。とかいうと、難しく感じるけど、やってることはOOPの基本。
RPGの戦闘シーンで説明してみよう(ジャンケンだとパクリとか言われそうなので)。簡単のため、味方は主人公だけで。
function Battle(monsterList) {
this.turnNumber = 1;
this.hpOfHero = 500;
this.offenseOfHero = 12;
this.monsters = monsterList; // モンスターのリストを配列で保存
}
function Battle.prototype.start() {
// モンスターがいなくなるか、主人公のHPが無くなるまでループ
while(true) {
if (this.monsters.length == 0) {
print("勇者は勝利した!"); // print は適当に定義すべし
break;
} else if (this.hpOfHero > 0) {
this.nextTurn();
} else {
print("勇者は死んでしまった");
break;
}
}
}
function Battle.prototype.nextTurn() {
print(this.turnNumber + "ターン: ");
// まずは勇者の攻撃(本題とはあんまり関係ない)
var targetNum = Math.floor(Math.random() * this.monsters.length) // 勇者攻撃対象をランダムに設定
var target = this.monsters[targetNum];
target.hp -= (this.offenseOfHero - target.defence); // (勇者の攻撃力 - 敵の防御力)をHPから引く
print("勇者の攻撃: " + target.getName() + "に" + (this.offenseOfHero - target.defence) + "のダメージを与えた!");
if (target.hp <= 0) {
print(target.getName() + "を倒した!");
var newList = new Array();
for (var i = 0; i < this.monsters.length; i++) {
if (i != targetNum) {
newList.push(this.monsters[i]);
}
}
this.monsters = newList;
}
// 本題のモンスターの行動決定
for (var i = 0; i < this.monsters.length; i++) {
var damage = this.monsters[i].act() * 3; // ここがStrategy。 攻撃力 * 3 のダメージを受ける
print(this.monsters[i].getName() + "の攻撃: " + damage + "のダメージを受けた!")
this.hpOfHero -= damage;
}
this.turnNumber++;
}
function Monster() {
this.name; // abstract
this.hp; // abstract
this.defence; // abstract
function Monster.prototype.act() {
// abstract
// 攻撃力の値を return するメソッド
}
function Monster.prototype.getName() {
return this.name;
}
}
function Slime() {
Slime.prototype.name = "スライム";
Slime.prototype.hp = 20;
Slime.prototype.defence = 3;
function Slime.prototype.act() {
return 3; // 攻撃力3
}
}
Slime.prototype = new Monster(); // Monster クラスを継承
function HoimiSlime() {
HoimiSlime.prototype.name = "ホイミスライム";
HoimiSlime.prototype.hp = 25;
HoimiSlime.prototype.defence = 2;
function HoimiSlime.prototype.act() {
var rand = Math.floor(Math.random() * 3);
if (this.hp < 10 && rand == 1) {
this.hp += 30; // HP が少なかったら1/3の確率でHPを回復する
return 0; // 魔法を使ったので攻撃しない
} else {
return 2; // 攻撃力2
}
}
}
HoimiSlime.prototype = new Monster();
function KingSlime() {
KingSlime.prototype.name = "キングスライム";
KingSlime.prototype.hp = 50;
KingSlime.prototype.defence = 5;
function KingSlime.prototype.act() {
var rand = Math.floor(Math.random() * 2); // 0 or 1
if (rand == 1) {
return 8 // 会心の一撃!
} else {
return 4 // 通常攻撃
}
}
}
KingSlime.prototype = new Monster();
// デモコード
var monsters = new Array(new Slime, new Slime, new HoimiSlime, new KingSlime); // スライム * 2、ホイミスライム、キングスライムの組み合わせ
var btl = new Battle(monsters);
btl.start();
実行すると次のような感じになる。
1ターン:
勇者の攻撃: スライムに9のダメージを与えた!
スライムの攻撃: 9のダメージを受けた!
スライムの攻撃: 9のダメージを受けた!
ホイミスライムの攻撃: 6のダメージを受けた!
キングスライムの攻撃: 12のダメージを受けた!
2ターン:
勇者の攻撃: キングスライムに7のダメージを与えた!
スライムの攻撃: 9のダメージを受けた!
スライムの攻撃: 9のダメージを受けた!
ホイミスライムの攻撃: 6のダメージを受けた!
キングスライムの攻撃: 24のダメージを受けた!
3ターン:
勇者の攻撃: スライムに9のダメージを与えた!
スライムの攻撃: 9のダメージを受けた!
スライムの攻撃: 9のダメージを受けた!
ホイミスライムの攻撃: 6のダメージを受けた!
キングスライムの攻撃: 12のダメージを受けた!
4ターン:
勇者の攻撃: スライムに9のダメージを与えた!
スライムを倒した!
スライムの攻撃: 9のダメージを受けた!
ホイミスライムの攻撃: 6のダメージを受けた!
キングスライムの攻撃: 24のダメージを受けた!
5ターン:
勇者の攻撃: ホイミスライムに10のダメージを与えた!
スライムの攻撃: 9のダメージを受けた!
ホイミスライムの攻撃: 6のダメージを受けた!
キングスライムの攻撃: 12のダメージを受けた!
6ターン:
勇者の攻撃: ホイミスライムに10のダメージを与えた!
スライムの攻撃: 9のダメージを受けた!
ホイミスライムの攻撃: 6のダメージを受けた!
キングスライムの攻撃: 12のダメージを受けた!
7ターン:
勇者の攻撃: スライムに9のダメージを与えた!
スライムの攻撃: 9のダメージを受けた!
ホイミスライムの攻撃: 6のダメージを受けた!
キングスライムの攻撃: 24のダメージを受けた!
8ターン:
勇者の攻撃: ホイミスライムに10のダメージを与えた!
ホイミスライムを倒した!
スライムの攻撃: 9のダメージを受けた!
キングスライムの攻撃: 12のダメージを受けた!
9ターン:
勇者の攻撃: スライムに9のダメージを与えた!
スライムの攻撃: 9のダメージを受けた!
キングスライムの攻撃: 24のダメージを受けた!
10ターン:
勇者の攻撃: スライムに9のダメージを与えた!
スライムを倒した!
キングスライムの攻撃: 24のダメージを受けた!
11ターン:
勇者の攻撃: キングスライムに7のダメージを与えた!
キングスライムの攻撃: 24のダメージを受けた!
12ターン:
勇者の攻撃: キングスライムに7のダメージを与えた!
キングスライムの攻撃: 24のダメージを受けた!
...
17ターン:
勇者の攻撃: キングスライムに7のダメージを与えた!
キングスライムを倒した!
勇者は勝利した!
モンスターは「スライム」「ホイミスライム」「キングスライム」の3種類を用意した。そのターンに何をするかはモンスターごとに戦略が違う。スライムはただただ攻撃するだけだし、ホイミスライムはHPが少ないと一定の確率でホイミを唱える。また、キングスライムは1/2の確率で会心の一撃を放つようにしてみた。
それぞれのモンスターはMonsterクラスを継承しており、必ず(といってもAbstractが無いのでJavaと違って言語レベルでの強制はできていないのだが)actメソッドを持っている。モンスターを扱うBattleクラスはこのactメソッドを使うことのみを知っていれば良く、モンスターの種類やそのモンスターが何を考えているのかについては無関心で構わない。
このactメソッドがOOPの利点として非常に重要で、戦闘ごとに違う種類のモンスターを組み合わせたり、色々な種類のモンスターを作っても、Monsterクラスを継承したモンスターを使う限りは、actメソッドを媒体としてBattleクラスは全く変更無く使用することが出来る。
モンスターの種類による戦略(AI)の切り替えをifによる条件分岐で行うとしたらどうなるだろうか? Battle.prototype.nextTurnにはモンスターの種類が増えるゴトににどんどんifが増えていき、メンテナンス性が最悪になるだろう。
function Battle.prototype.nextTurn() {
...
for (var i = 0; i < this.monsters.length; i++) {
var monsterName = this.monsters[i];
if (monsterName == "スライム") {
...
} else if (monsterName == "ホイミスライム") {
...
} else if (...) {
...
}
}
}
Strategyパターンでは「モンスター」という「単位」でオブジェクトを分割することにより処理を分散し、メンテナンス性を高めていると言える。
Strategyパターンはオブジェクトの一部の振舞いを付け換えられるようにするパターンであり、今回のパターンのようにBattleクラスとMonsterクラスのような独立したクラス間での単なるポリモーフィズムには当てはまらないと思います。
ロボットで言うと手のパーツを換えて攻撃方法を換えるようなイメージです。
http://www.dmz.hitachi-sk.co.jp/Java/Tech/pattern/gof/strategy.html
http://www.hellohiro.com/pattern/strategy.htm
http://www.techscore.com/tech/DesignPattern/Strategy.html
そういわれてみればそうですね。戦術の違うスライムを「スライムクラス」+「戦術Aクラス」「戦術Bクラス」の組み合わせで表現するべきでした。
今度時間があるときに修正しておきます。
http://yudai.arielworks.com/memo/2005/02/24/090533.trackback
末尾に「3 + 3」の計算結果を繋げて下さい。例えば計算結果が「17」の場合、「090533.trackback17」です。これは機械的なトラックバックスパムを防止するための措置です。