(100) 総合課題 ドラクエ風戦闘シミュレータ

Ippei Kishida

Last-modified:2018/03/29 22:55:21.

これまでに習得したプログラミング技術を使って、 簡単なゲームを作ってみよう。

Figure 1: (fig:fig100_010) CAPTION
Figure 1: (fig:fig100_010) CAPTION

1 仕様

ドラゴンクエスト(ドラクエ) は、ほとんどの人が知っているだろう。 今回はあれの戦闘を模したものを作ってみる。 ただし、本家のような複雑なものは大変なので、 最小限の仕様で作ってみよう。

キャラクターのパラメータ要素も簡略化して以下のみとする。

1回の攻撃でのダメージ計算式は、以下とする。

\[\begin{aligned} D_{\mathrm B} =& a_{\mathrm A} - d_{\mathrm B} & ( a_{\mathrm A} - d_{\mathrm B} \ge 1 \mathrm{のとき} ) \\ D_{\mathrm B} =& 1 & ( a_{\mathrm A} - d_{\mathrm B} < 1 \mathrm{のとき} ) \end{aligned}\] ここで、 \(D_{\mathrm B}\) は防御側 B のダメージ、 \(a_{\mathrm A}\) は攻撃側 A の攻撃力、 \(d_{\mathrm B}\) は防御側 B の防御力である。

この時、B の HP は以下のように変化する \[\begin{aligned} h_{\mathrm{B, new}} =& h_{\mathrm{B, old}} - D_{\mathrm{B}} & ( h_{\mathrm{B, old}} - D_{\mathrm{B}} \ge 0 \mathrm{のとき} ) \\ h_{\mathrm{B, new}} =& 0 & ( h_{\mathrm{B, old}} - D_{\mathrm{B}} < 0 \mathrm{のとき} ) \end{aligned}\] ここで、 \(h_{\mathrm{B, new}}\) はダメージ適用後の HP、 \(h_{\mathrm{B, old}}\) はダメージ適用前の HP である。

随分とシンプルだが、「ドラクエっぽい」感じにはなるだろう。 ここまでで仕様は定まった。 あとはコードに落とし込むだけなので、ここで「さあ、作ってね」で作れる筈だ。 何から手をつけて良いか分からない人も出そうだが、 これは設計のトレーニングでもあるので、 ここで5分だけ時間を取ろう。 この間はできるだけ、次のヒントを見ないようにして考えて欲しい。 特に考えるべきは、

もちろん、コーディングを開始しても構わない。

2 ヒント

2.1 構造

プログラムの基本構造はターン毎のループになる。 ループの終了条件は、以下の OR 条件となる。

どの条件でループを脱出したかによって終了メッセージが変化する。 この目的のためには、終了状態を示す変数 ( たとえば winner, loser, state といった名前のどれか) を用意しておいて、 ループの外でその変数の値によってメッセージを変えれば良い。

# loop.rb
while true

  #何か処理
  val = 0
  #val = 1
  #val = -1

  if val == 0
    winner = "A"
    break
  end

  if val == 1
    winner = "B"
    break
  end

  if val == -1
    winner = "none"
    break
  end
end

print "Winner is #{winner}.\n"

2.2 変数

キャラクターのパラメータ要素はすでに示してあるので、それに従って変数を作れば良い。 また、キャラクターの名前も変数として持っておくと、 「岸田の攻撃!」のように名前が表示されてちょっと嬉しくなる。

これらのプログラムで使われるパラメータは、 プログラムの先頭部分にまとめて定義しておくのがコツだ。 いろいろパラメータをかえてテストしたりバランス調整するときに、 プログラムコードの中に埋もれてる定義場所を探し回らずに済む。 ターン数の上限の値も変数か定数に格納し、先頭で定義するのが良いだろう。

愚直にそれぞれ別個の変数にしてしまっても良い。 しかしあとあとメソッドを作るとすれば、配列にした方が便利だったりする。

それぞれ別個の変数にしてしまう方法

# parameters1.rb
a_name    = "ロト"
a_attack  =  70
a_defence =  70
a_hp      =  60

b_name    = "岸田"
b_attack  =  70
b_defence =  70
b_hp      =  60

# プログラムの本体部分
print "本当の戦いはこれからだ!\n"

配列に入れる方法

# parameters2.rb
name    = [ "ロト", "岸田" ]
attack  = [     50,  70    ]
defence = [     50,  70    ]
hp      = [    100,  60    ] # 最大 HP と同義。
max_loop = 100

# プログラムの本体部分
print "本当の戦いはこれからだ!\n"

2.3 メソッド

以下の処理が必要だ。

これらはターン毎に、2人のキャラクターのアクションとして実行される。 すなわちループを1回回るごとに 2回処理が必要になる。 またそれぞれで同じルール、フォーマットが適用されなければならない。 これまでの授業で述べたように、 2回行う処理は2回同じようなコードを書くのではなく、 メソッドを使うなどして記述を一箇所にまとめるべきだ。

これら3つの処理を別個のメソッドとして書くこともできるが、 それぞれの機能が別個には必要ではないので、 まとめて1つのメソッドとしてしまった方がラクに書けるだろう。

2.3.1 変数のスコープ

parameters1.rb を土台に、 攻撃処理のコア部分をメソッドにしてみよう。

# attack1.rb
# それぞれ別個の変数にしてしまう方法
a_name    = "ロト"
a_attack  =  110
a_defence =  110
a_hp      =  110

b_name    = "岸田"
b_attack  =  100
b_defence =  100
b_hp      =  100

# 攻撃の処理を行う関数
# attack0 は攻撃側の攻撃力、 defence1 は防御側の防御力、 hp1 は防御側の HP 。
def attack_by_a( a_attack, b_defence, b_hp )
  damage = a_attack - b_defence
  b_hp -= damage
end

# プログラムの本体部分
print "before attack: #{b_hp}\n" #=> 100
attack_by_a( a_attack, b_defence, b_hp )
print "after  attack: #{b_hp}\n" #=> 100

コードの 20, 22 行目にコメントで示してあるように、 実行しても b_hp は変化せず 100 のままであることが分かる。 これはプログラマの意図と異なる。 attack によって防御側の HP が変化して欲しかった筈だ。 「そうなっていないのは何故だろうか?」 というような疑問を持つ人は第11回のテキストで「変数のスコープ」を もう一度確認しておこう。 スコープを考慮して修正したサンプルコードは以下のようになる。

# attack2.rb
# それぞれ別個の変数にしてしまう方法
a_name    = "ロト"
a_attack  =  110
a_defence =  110
$a_hp      =  110

b_name    = "岸田"
b_attack  =  100
b_defence =  100
$b_hp      =  100

# 攻撃の処理を行う関数
# attack0 は攻撃側の攻撃力、 defence1 は防御側の防御力、 hp1 は防御側の HP 。
def attack_by_a( a_attack, b_defence )
  damage = a_attack - b_defence
  $b_hp -= damage
end

# プログラムの本体部分
print "before attack: #{$b_hp}\n" #=> 100
attack_by_a( a_attack, b_defence )
print "after  attack: #{$b_hp}\n" #=> 90

2.3.2 何を引数にするか

前述の attack2.rb では、 キャラクターごとに別の攻撃メソッド( たとえば attack_by_b )を作る必要がある。 先に述べたように、これを1つのメソッドで扱わなければメソッド化する意義が薄い。

これまでに学んだ方法で対処するには、以下のような方法がある。

サンプルコードを以下に示す。

# attack3.rb
# それぞれ別個の変数にしてしまう方法
NAME    = [ "ロト", "岸田" ]
ATTACK  = [     50,  70    ]
DEFENCE = [     50,  70    ]
$hp     = [    100,  60    ] # 最大 HP と同義。
max_loop = 100

# 攻撃の処理を行う関数
# attacker は攻撃側の番号、defender は防御側の番号。
def attack_action( attacker_id, defender_id )
  print "攻撃側のキャラクターの名前:   #{NAME[ attacker_id ]}\n"
  print "攻撃側のキャラクターの攻撃力: #{ATTACK[ attacker_id ]}\n"
  print "防御側のキャラクターの防御力: #{DEFENCE[ defender_id ]}\n"
  print "防御側のキャラクターのHP:     #{$hp[ defender_id ]}\n"
  $hp[ defender_id ] -= 10 #なんか演算
  print "防御側のキャラクターのHP:     #{$hp[ defender_id ]}\n"
  print "\n"
end

# プログラムの本体部分
attack_action( 0, 1 )
attack_action( 1, 0 )

# 行動の結果の HP
print "最終的な、\n"
print "#{NAME[ 0 ]}のHP: #{$hp[ 0 ]}\n" #=> 90
print "#{NAME[ 1 ]}のHP: #{$hp[ 1 ]}\n" #=> 50

2.3.3 メソッドの処理の結果を利用する

メソッドの内外では変数のスコープが異なり、 切り離された世界の出来事として扱われる。 では、メソッド内での処理の結果をメソッドを呼び出した側に 伝えるにはどうしたら良いだろうか? 大きく分けて2つの方法がある。 1つは返り値(第12回)を使う方法であり、 もう1つはグローバル変数(see 080)を使う方法である。

2.4 画面出力

プログラムを実行して最初の表示が「○○は負けました」では納得がいかないだろう。 経過を必ず画面に表示するように。 たとえば、

  岸田の攻撃!ロトに 12 のダメージを与えた!
  ロトの攻撃!岸田に 23 のダメージを与えた!
  岸田の攻撃!ロトに 12 のダメージを与えた!
  ロトの攻撃!岸田に 23 のダメージを与えた!
  :
  岸田の攻撃!ロトに 12 のダメージを与えた!
  ロトの攻撃!岸田に 23 のダメージを与えた!
  岸田はたおれた。
  ロトが勝ちました。

それぞれの攻撃力や現在 HP を表示するなど、工夫の余地はいくらでもある。

2.5 プログラムに待機させる

Sec. 2.4 の出力が一瞬で全部書き出されると 速すぎて戦っている気がしない。 そこで1アクションごとにプログラムを待機させると雰囲気が出る。 待機させるには sleep を使えば良い。

# sleep.rb
print "hello, "
sleep 1.0
print "world!\n"

なお、これまでに学んだ STDIN.gets を使って ENTER を打つたびに 行動するようにする方法もある。

2.6 開発中オプション

最終的に sleep を入れて出力を待機させることを開発中に思い付いたとする。 しかしそのコードを書いてしまうと、開発中のテストも全てプログラムが待機させられる。 sleep 時間を 1秒に設定した状態で 30ターンで引き分けになることを確認するテストを行おうとすると、 最後に確認できるまでに 60秒かかる。 ちょっとコードが間違っていたらそれを修正してまた 60秒。 こんなことはやってられない。

方法はいくつかある。 以下に対処の例を挙げておくので参考にされたい。

2.7 テスト

以下の条件のときに正しく動作するかに、特に注意すること。

3 課題・レポート

3.1 課題

今回のテキストで解説した仕様に従い、 プログラムを完成させよ。 ( Sec. 3.2 にも目を通し、 コード作成を進めながら適宜記録を残すように。 )

さらに、敵(先攻側)には以下の情報を使うこと。

自分のキャラクターに 攻撃力 + 防御力 + HP の和が 200 になるようにパラメータを割り振り、 「岸田」に打ち勝つパラメータを適当に見つけ出せ。 このパラメータをソースコードに書き込むこと。 実行可能かつ、「岸田」に打ち勝てる状態のソースコードを 第14回課題として提出せよ。

3.2 レポート

Sec. 3.1 のコードを書く上で経験したことについて、 特に以下の点について自由形式でまとめ、 第14回レポート課題として提出せよ。

  1. プログラミングにおいて苦労した全ての点について、
  2. 自分のプログラムで工夫した点。
  3. 本テキストにおいて理解しにくかった点。
  4. 今回、および全体的な感想。

拡張子 .txt のテキストファイルとして作成し、 提出せよ。


  1. 勝負がなかなか決まらないことがある。 たとえば防御に特化したキャラクター同士の対戦では、 いずれの攻撃も相手の防御に阻まれてほとんどダメージが通らない。 実際のゲームでも戦闘の長期化を避けるための工夫が凝らされている。 たとえばドラクエの闘技場では、 今回作る処理と同じように規定ターン数を超過すると引き分けとして処理される。 また、「会心の一撃」システムも戦闘の長期化を抑制する効果がある。 なお今回の仕様で、 「攻撃力より防御力の方が大きかったときでもダメージが 0 ではなく 1 となる」 としているのは、 無限ループ回避が目的である。