Last-modified:2018/03/29 22:55:21.
これまでに習得したプログラミング技術を使って、 簡単なゲームを作ってみよう。
ドラゴンクエスト(ドラクエ) は、ほとんどの人が知っているだろう。 今回はあれの戦闘を模したものを作ってみる。 ただし、本家のような複雑なものは大変なので、 最小限の仕様で作ってみよう。
キャラクターのパラメータ要素も簡略化して以下のみとする。
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分だけ時間を取ろう。 この間はできるだけ、次のヒントを見ないようにして考えて欲しい。 特に考えるべきは、
もちろん、コーディングを開始しても構わない。
プログラムの基本構造はターン毎のループになる。 ループの終了条件は、以下の 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"
キャラクターのパラメータ要素はすでに示してあるので、それに従って変数を作れば良い。 また、キャラクターの名前も変数として持っておくと、 「岸田の攻撃!」のように名前が表示されてちょっと嬉しくなる。
これらのプログラムで使われるパラメータは、 プログラムの先頭部分にまとめて定義しておくのがコツだ。 いろいろパラメータをかえてテストしたりバランス調整するときに、 プログラムコードの中に埋もれてる定義場所を探し回らずに済む。 ターン数の上限の値も変数か定数に格納し、先頭で定義するのが良いだろう。
愚直にそれぞれ別個の変数にしてしまっても良い。 しかしあとあとメソッドを作るとすれば、配列にした方が便利だったりする。
それぞれ別個の変数にしてしまう方法
# 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人のキャラクターのアクションとして実行される。 すなわちループを1回回るごとに 2回処理が必要になる。 またそれぞれで同じルール、フォーマットが適用されなければならない。 これまでの授業で述べたように、 2回行う処理は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
前述の attack2.rb
では、 キャラクターごとに別の攻撃メソッド( たとえば attack_by_b
)を作る必要がある。 先に述べたように、これを1つのメソッドで扱わなければメソッド化する意義が薄い。
これまでに学んだ方法で対処するには、以下のような方法がある。
プログラム内でキャラクターを表す符号を 0, 1 とする。
全てのパラメータは配列にし、 0番めの要素に 0番目のキャラクターのパラメータを、 1番めの要素に 1番目のキャラクターのパラメータを入れる。 ( parameters2.rb
参照。)
メソッドにはキャラクターを表す符号を渡す。 たとえば第1引数に攻撃側のキャラクターの符号、 第2引数に防御側のキャラクターの符号など。
サンプルコードを以下に示す。
# 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つの方法がある。 1つは返り値(第12回)を使う方法であり、 もう1つはグローバル変数(see 080)を使う方法である。
プログラムを実行して最初の表示が「○○は負けました」では納得がいかないだろう。 経過を必ず画面に表示するように。 たとえば、
岸田の攻撃!ロトに 12 のダメージを与えた!
ロトの攻撃!岸田に 23 のダメージを与えた!
岸田の攻撃!ロトに 12 のダメージを与えた!
ロトの攻撃!岸田に 23 のダメージを与えた!
:
岸田の攻撃!ロトに 12 のダメージを与えた!
ロトの攻撃!岸田に 23 のダメージを与えた!
岸田はたおれた。
ロトが勝ちました。
それぞれの攻撃力や現在 HP を表示するなど、工夫の余地はいくらでもある。
Sec. 2.4 の出力が一瞬で全部書き出されると 速すぎて戦っている気がしない。 そこで1アクションごとにプログラムを待機させると雰囲気が出る。 待機させるには sleep
を使えば良い。
# sleep.rb
print "hello, "
sleep 1.0
print "world!\n"
なお、これまでに学んだ STDIN.gets
を使って ENTER を打つたびに 行動するようにする方法もある。
最終的に sleep
を入れて出力を待機させることを開発中に思い付いたとする。 しかしそのコードを書いてしまうと、開発中のテストも全てプログラムが待機させられる。 sleep 時間を 1秒に設定した状態で 30ターンで引き分けになることを確認するテストを行おうとすると、 最後に確認できるまでに 60秒かかる。 ちょっとコードが間違っていたらそれを修正してまた 60秒。 こんなことはやってられない。
方法はいくつかある。 以下に対処の例を挙げておくので参考にされたい。
#
でコメントアウトしておく。 開発終了したらコメントアウトを外す。SLEEP_TIME = 0.0
のように sleep
する時間を保持する。 開発中は 0.0 に設定し、 開発終了したら 1.0 などの値に変更する。DEBUG = true
のように開発中であることを示す情報を保持しておき、 sleep
や STDIN.gets
を条件分岐でスルーする。 開発終了したら false に変更する。ARGV
で sleep
する時間を渡す。 指定しなかった時のデフォルト値も定めておく。ARGV
で --debug
オプションが渡されたときは sleep
する時間を 0 にする。以下の条件のときに正しく動作するかに、特に注意すること。
今回のテキストで解説した仕様に従い、 プログラムを完成させよ。 ( Sec. 3.2 にも目を通し、 コード作成を進めながら適宜記録を残すように。 )
さらに、敵(先攻側)には以下の情報を使うこと。
自分のキャラクターに 攻撃力 + 防御力 + HP の和が 200 になるようにパラメータを割り振り、 「岸田」に打ち勝つパラメータを適当に見つけ出せ。 このパラメータをソースコードに書き込むこと。 実行可能かつ、「岸田」に打ち勝てる状態のソースコードを 第14回課題として提出せよ。
Sec. 3.1 のコードを書く上で経験したことについて、 特に以下の点について自由形式でまとめ、 第14回レポート課題として提出せよ。
拡張子 .txt
のテキストファイルとして作成し、 提出せよ。
勝負がなかなか決まらないことがある。 たとえば防御に特化したキャラクター同士の対戦では、 いずれの攻撃も相手の防御に阻まれてほとんどダメージが通らない。 実際のゲームでも戦闘の長期化を避けるための工夫が凝らされている。 たとえばドラクエの闘技場では、 今回作る処理と同じように規定ターン数を超過すると引き分けとして処理される。 また、「会心の一撃」システムも戦闘の長期化を抑制する効果がある。 なお今回の仕様で、 「攻撃力より防御力の方が大きかったときでもダメージが 0 ではなく 1 となる」 としているのは、 無限ループ回避が目的である。↩