素人が RPG の開発に挑戦してみる 5
引き続きソード・ワールドの戦闘システムの実装に奮闘中。なんとかキャラクター同士の戦闘処理が形になったので、次はモンスターとの戦闘を実装しよう。お相手はゴブリン君。データは物理戦闘に関係ある項目だけ抽出。
■ゴブリン 敏捷度 = 13 攻撃点 = 武器または牙: 9(2) 打撃点 = 7 回避点 = 10(3) 防御点 = 5 生命点/抵抗値 = 12/10(3)
基本的にはキャラクター同士の戦闘と同じように扱えるはずだが、モンスターはピンゾロでも失敗しないし 6 ゾロでも自動成功ではない点に注意。そして攻撃点と回避点の比較では、同値ならキャラクター有利となることも忘れてはならない。
モンスターにはサイコロの目による自動成功も自動失敗もない。従ってキャラクターを攻撃する際に呼び出す dodged? メソッドの第二引数に Dice.roll_2d6 の戻り値をそのまま渡すわけにはいかない。モンスターの出目が 12 でもキャラクターは回避を諦める必要はないのだ。
今のところ Character#dodged? の第二引数は 12 でなければ特に何も起こらないので、攻撃点と出目の合計値を第一引数として渡し、第二引数は 0 にすることにする。この第二引数はモンスターの攻撃では常に 0 になるわけだから、呼び出し側でいちいち 0 を渡すよりも引数のデフォルト値を 0 にしてしまえば良い。
2D6 の乱数を出力するメソッドも共有化のために Dice クラスとして Character クラスの外に出す。Character#power メソッドと Character#prevent メソッドに相当する処理は、Monster クラスでは固定値なので同名のインスタンス変数で代用*1。
その他変数名などを修正して、こうなった。
RATING_SHEET = { 7 => [nil, nil, nil, 0, 1, 1, 2, 3, 4, 4 ,5, 5, 6], 14 => [nil, nil, nil, 1, 2, 3, 4, 4, 4, 5, 6 ,7 ,8], } class Dice def self.roll_2d6 (rand(6) + 1) + (rand(6) + 1) end end class Judge def resolve(a, b) offense, defense = [a, b].sort{|a, b| b.nimble <=> a.nimble} turn = 1.0 loop do puts "■第 #{sprintf('%d', turn)} ターン" if turn % 1 == 0 turn += 0.5 print " #{offense.name} の攻撃: " damage = offense.attack(defense) begin unless damage puts "攻撃は外れた" next end puts "#{damage} のダメージを与えた" if turn % 1 == 0 || offense.nimble != defense.nimble puts " #{a.name} は倒れた" unless a.active? puts " #{b.name} は倒れた" unless b.active? next if offense.active? && defense.active? break end ensure # 攻守交代 offense, defense = defense, offense end end end end class Character attr_accessor :name, :life, :deft, :nimble, :power attr_accessor :weapon, :shield, :armor attr_accessor :fighter, :thief, :ranger def attack(d) dice = Dice.roll_2d6 return nil if dice == 2 return nil if d.dodged?(bonus(@deft), dice) d.damage(power) end def dodged?(a, a_dice = 0) dice = Dice.roll_2d6 return true if dice == 12 return false if a_dice == 12 return false if dice == 2 a + a_dice <= bonus(@nimble) + dice + (@shield ? 1 : 0) end def damage(p) return nil unless p d = p - prevent d = (d > 0 ? d : 0) @life -= d d end def power result = nil loop do dice = Dice.roll_2d6 unless dice == 2 result ||= 0 result += RATING_SHEET[@weapon][dice] end break if dice < 10 end result += @fighter + bonus(@power) if result result end def prevent dice = Dice.roll_2d6 return 0 if dice == 2 RATING_SHEET[@armor][dice] + @fighter end def active? @life > 0 end def bonus(p) p / 6 end end class Monster attr_accessor :name, :life, :nimble, :ap, :dp attr_accessor :power, :prevent def attack(d) return nil if d.dodged?(@ap + Dice.roll_2d6) d.damage(@power) end def dodged?(a, a_dice) dice = Dice.roll_2d6 return false if a_dice == 12 a + a_dice < @dp + dice end def damage(p) return nil unless p d = p - @prevent d = (d > 0 ? d : 0) @life -= d d end def active? @life > 0 end end
これでキャラクターとモンスターの戦闘はキャラクター同士の戦闘と同様に扱うことが可能になった。Judge クラスの処理そのものは一切変更されていない。Monster#dodged? の第二引数にデフォルト値がないのはモンスター同士の戦闘を考慮していないため。ルールにもなかったし問題ないはずだ。
いざ、勝負。
a = Character.new a.name = 'デッカード A' a.life = 18 a.deft = 16 a.nimble = 12 a.power = 14 a.weapon = 14 a.shield = true a.armor = 7 a.fighter = 2 c = Monster.new c.name = 'ゴブリン' c.life = 12 c.nimble = 13 c.ap = 2 c.dp = 3 c.power = 7 c.prevent = 5 Judge.new.resolve(a, c)
結果。結構いい勝負をする。何十回もやっていると、ごく稀にデッカード君が第 1 ターンでクリットぶん回して、20 超のダメージでゴブリン君を瞬殺したりするのも趣き深い。そんなデッカード君も体感で二、三割程度は負けてしまう。
■第 1 ターン ゴブリン の攻撃: 5 のダメージを与えた デッカード A の攻撃: 攻撃は外れた ■第 2 ターン ゴブリン の攻撃: 3 のダメージを与えた デッカード A の攻撃: 10 のダメージを与えた ■第 3 ターン ゴブリン の攻撃: 2 のダメージを与えた デッカード A の攻撃: 0 のダメージを与えた ■第 4 ターン ゴブリン の攻撃: 攻撃は外れた デッカード A の攻撃: 攻撃は外れた ■第 5 ターン ゴブリン の攻撃: 攻撃は外れた デッカード A の攻撃: 攻撃は外れた ■第 6 ターン ゴブリン の攻撃: 攻撃は外れた デッカード A の攻撃: 1 のダメージを与えた ■第 7 ターン ゴブリン の攻撃: 攻撃は外れた デッカード A の攻撃: 3 のダメージを与えた ゴブリン は倒れた
少しだけ気になったのは、キャラクター同士の戦闘もそうだったが、タイマンだとどうもお互いに攻撃を外すことが多い。これは意図された仕様なのだろうか。とは言うものの、私自身はタイマン戦闘なんてやった記憶がないので、あまり気にすることはないのかも知れない。
さて、そろそろ面倒臭がってないでテストを書き始めた方がいいような気がしてきた。Dice.roll_2d6 が欲しい値を返すようにオーバライドすれば、乱数による結果のブレを無視してテストが書ける。しかしながら、今回のように明確な仕様を定めなずにコードを書き始めた場合、教科書通りメソッド単位でちまちまテストを積み上げていると、後から仕様変更が入って全てパーになる危険が高い。
私なら、とりあえず Judge#resolve のテストを書く。サイコロの出目を制御できるのだから、何ターン目にどちらが勝って最終的にそれぞれ生命点がどれだけ残っているのかは全て事前に分かる。Character クラスも Monster クラスも単純に戦闘手順を割り振っただけなので、Judge#resolve が期待通りの結果を出しさえすれば現状の形に拘る理由は全くないし、規模的にも気軽にごっそり書き換えられる程度なのでテストを書いてまで保守する必要はない。私の場合、外部とのインタフェース部とか、将来どのような変更が加えられるにしてもこいつだけは気軽に弄ってもらっては困る、そういうメソッドに絞ってテストを書くことにしている。あとはテストを書かないと自分で挙動が追いきれない処理だな。
次はファイター技能とシーフ技能の使い分けに挑戦する予定。シーフ技能の場合はクリティカル基準が 9 になるが、判定の度にいちいち今使用しているのはどっちの技能かなんて分岐を入れるのはみっともないので、技能そのものをオブジェクト化して Character#power を委譲する方向で。でも 2.0 が発売されたら、今考えてることが全部無駄になったりして。
- 作者: 白井操
- 出版社/メーカー: 講談社
- 発売日: 2005/12
- メディア: 単行本
- この商品を含むブログ (2件) を見る
*1:実際は attr_accessor が裏でアクセサを定義してくれているのだが、気持ち、メソッド呼び出しのオーバヘッドを避けてみた