🍍

Ruby で創る RPG 第七章 RPGを創り続ける

2024/01/04に公開


第七章 RPGを創り続ける

繰り返しと条件判断

「スライムが現れて、それを勇者が倒すのね。アスキーアートで可愛いスライムの絵も出て、素敵だわ。作ってみて、どう、感想は。」
「うん、スライムを倒せるようになったのだけれど、死ぬまで攻撃し続けるところがうまくできなくて。。。一応、3ポイントずつダメージを与えるから、四回で死ぬから、四回コピーしたんだ。何かいい方法はないのかな〜」
「そうね。プログラムを作る三つの要素があるわ。」
「どういうもの?」
「一つは、逐次。プログラムは上から下に実行されていく。みっくんの書いたのもその通りよね。」
「うん。そうだよ。」
「あとは、繰り返しと条件判断。この三つが揃うとどんなプログラムでもできるって、証明されているそうよ。」
「へ〜 そうなんだ。凄いや」
「イギリスのチューリングさんという人なの。コンピュータ科学の父、人工知能の父などとも言われているわ。」
「そうなんだね。僕もなりたいな〜」
「そうね。いっぱい学んで、素敵な未来を築いてね。」
「で、プログラミングなんだけれど、Rubyで繰り返しを書く方法はいろいろあるわ。loop do ... end が一番簡単かしら。」
「... のところに、繰り返したい処理を書くの。何回でも何十回でも繰り返してくれるわ。」
「そうなんだね。じゃあ、スライムを倒すところをこんなふうに書けばいいのかな。」

# 倒すまで繰り返す
loop do
  puts " / \"
  puts "/・Д・\"
  puts "〜〜〜〜〜"
  puts "スライム HP:#{hp}"
  gets
  puts "勇者の攻撃"
  gets
  puts "スライムに3ポイントのダメージを与えた"
  hp = hp - 3             # スライムのHPを3減らす
  gets
  system "clear"
end

「ええ、そうよ。これで何度でも繰り返してくれるわ。やって見て。」
「うん、やって見るよ。」
お姉さまに繰り返しを教えられ、やって見る僕。出来上がったコードを動かして見る。

「やってみたよ〜。もう十回くらい死んでるはずだけれど、いつまでたっても終わらないよ〜」
「うん、そうね。何十回斬っても倒れない不死身のスライムができたわね(笑)」
「そうだけれど、ちゃんと死んで欲しいよ〜。」
「そうね。繰り返しを終了するための条件が書かれていないので、ずっと繰り返すしているわ。いわゆる無限ループね。無限ループになった時にはキーボードの Ctrl + C を押すと終了するわ。」
「え〜と、どのキーを押すの?」
「コントロールキーとCを一緒に押すことを、Ctrl + C と書くわ。止まったかしら?」
「うん、止まったよ。」
「良かったわね。それでは、スライムが死んだら終了するって言う条件を書きましょう。この場合はスライムのヒットポイントが0以下になったら、終了したいので、次のように書くと良いわ。」

if hp <= 0
  break # 繰り返しを抜ける
end

ifは、もしという意味だよ。そして次に条件を書くの。hp <= 0 はスライムのヒットポイントが0以下だったらという意味よ。不等号で大小判定を行っているの。数学と一緒ね。違う点はキーボードには≦や≧と言うキーがないので、<=を組み合わせて書くの。2つ併せて以下という意味になるわ。」
「そうなんだね。ちょうど0でも良い気がするんだけれど。」
「そうね。ヒットポイントは0以上の数値だものね。ただ、ちょうど0になってくれたらいいけれども、ダメージの計算中に、ヒットポイントがマイナスになっちゃうこともあるわ。そうした時にも死んだと判定できるように、0以下にしているの。ちなみに、0丁度だったらと言うのは==と書くわ。=一つなら代入、イコールが二つで==なら、等しいという意味よ。」
「うん、わかった。」
「そして、次の行からは、条件を満たしたときの処理を書くの。ここでは繰り返しを抜けたいので、breakと書いているわ。そして、条件を書き終わったらendで終わるの。」

「ありがとう。じゃぁ書き直してみるね。」
僕は、繰り返しと条件を使って戦闘シーンを書き直した。

https://github.com/Atelier-Mirai/Ruby_RPG/blob/master/rpg/rpg05.rb

「できたよ。」
「どう?ちゃんと動いてる。」
「うん、動いてるよ。」
「そう、よかったわね。元のプログラムと比べてどうかしら?」
「うん。やってる事は一緒だけれど、ずいぶん見やすくなったよ。」
「ええ、よかったわ。元の動作はそのままで、より見やすく書き直すことをリファクタリングと言うわ。プログラムの書き方はいろいろあるのだけれど、もっと見やすくスッキリした良い書き方がないか、いろいろやってみるといいわ。」

後置if文

「ちなみに、if文 で行いたい処理が一行だけなら次のようにも書けるわ」

# 繰り返しを抜ける
break if hp <= 0

「すごい。一行だけになった!」
「ええ、もともとは三行あったものね。簡潔にかけるから使ってみて。」

自己代入演算子

「そして、簡潔に書くと言えば、自己代入演算子と呼ばれるものがあるわ。そうね〜。例えば、スライムの体力を減らすときに、hp = hp - 3と書いていたじゃない。」
「うん、書いたよ。」
「もっと簡単に hp -= 3 とも書けるの。」
「そうなんだー。自分自身の値を更新したい場面はよくあるわ。なので簡潔に書けるように用意されているの。3減らすということが分かりやすいわよね。」
「うん、そうだね。」
「ええ、これも使ってみて。」

もう少し条件分岐

「あと、そうね。スライムに表情をつけるのはどうかしら? 元気な時はピンピンしていて、死にそうな時は弱っているってするの。」
「それ、面白そう。どうやってやるの?」
「さっき紹介したif文の続きになるんだけれど、さっきは、この条件が成り立つ場合にはループ脱出しておしまいって、条件一つですぐに終わったわ。」
「うん、そうだった。」
「もっといくつもの条件を作ることもできるの。例えばこんな風ね。」

# スライムの活気度
if hp >= max_hp * 0.7
  puts "スライムはピンピンしている"
elsif hp >= max_hp * 0.3
  puts "スライムは弱ってきている"
else
  puts "スライムは死にそうだ"
end

「max_hpは最大HPなんだけれど、七割以上だったらピンピンしている、三割以上だったら弱ってきている、そうでなかったら死にそうだって感じよ。アスキーアートを書き換えるのはちょっと大変だから、文字でスライムの表情を描写するの。どうかしら?」

「わ〜 とってもいいです。」
「そう、喜んでもらえて嬉しいわ。if文は最初の条件を書くの。さっきは条件が一つだけだったからそれだけで終わったけれど、elsif文は最初の条件が満たさなかった時に書くの、ちなみにelsif文は幾つでもかけるわ。そして一番最後に else文を書くの。どの条件にも満たさなかった時に行いたい処理を書くわ。」

「わかった。じゃあちょっと書いてみるね。」
僕はかたかたタイプする。毎日練習していて、指が思い通りのキーを押してくれるのが嬉しい。
そして、数分後、プログラムが出来上がった。

https://github.com/Atelier-Mirai/Ruby_RPG/blob/master/rpg/rpg06.rb

「できたよ〜」
「どれどれ。お〜 いい感じね。みっくん素敵よ。」

乱数 と 範囲オブジェクト

「じゃぁ、みっくん。次はどんなふうにバージョンアップしていこうか。」
「うん、いつも勇者が3ポイントっていう一緒のダメージじゃなくて、変化させたいな〜。」
「あ〜。そうね。ちょうどサイコロを振ったみたいに違う感じに変えてみようか。」
「どうやってやるの。」
「乱数って言うんだけれど、Ruby では簡単に乱数が作れるようになっているわ。こんな感じで書くの。」

damage = rand(1..6)
puts "スライムに#{damage}ポイントのダメージを与えた"

randっていうのが、乱数が得られる関数よ。みっくんは関数は習ったわよね。」
「うん、習ったよ。y = ax + b だったり、y = f(x)みたいなやつだよね。」
「ええ、そうよ。Rubyにも様々な関数が用意されていて、fのことを関数名、xのことを引数(ひきすう)と言うわね。Rubyの世界では、関数と言うよりは、メソッドって呼ぶわ。」
「そうなんだね。じゃあその後に続いている。1..6っていうのは何?」
「これは範囲オブジェクトって言うふうに言うんだけれど、範囲を表すオブジェクトなの。そのまんまね。」
「範囲はわかるけれど、オブジェクトって何?」
「オブジェクトっていうのはね。まぁ物とか対象物って言うふうな意味なんだ。ちょっと漠然としているわね。」
「世界にはいろんな物があるわ。そして、そういった物たちをプログラムの世界で表現したかったの。そこで考えられたのがオブジェクト指向なの。Rubyは純粋オブジェクト指向言語なのよ。」
「スライムは物よね」
「うん、そうだね。見たことないけれど。」
「ええ、そうね。ゼリー状でぬるぬるしている物みたいだけれど、空想の世界で実際に見たことのある人はきっといないと思うわ。」
「うん、そうだね。」
「ええ、そして見たことはないけれど、スライムっていう物は存在するわよね。」
「うん、そうだね。」
「ええ、それと同じで、見たことはないけれど、1から6って言う範囲を表わす、そういう物があるって思って欲しいの。」
「そういう物があるんだね。」
「ええ、オブジェクト指向についてはまた話す機会もあると思うわ。」
「うん、わかった。」
「ええ、randっていう関数(メソッド)があって、これに1..6という範囲オブジェクトを渡すと、1から6までの乱数が返ってくるの。そしてdamageって言う変数に代入しているわ。」
「うん、わかった。じゃあ、これを実行すると1になったり3になったり6になったりするんだね。」
「ええ、そうよ。そしたらdamageの値を式展開させたら出来上がりね。」
「うん、了解。」

case文と範囲オブジェクト

「範囲オブジェクトが登場したついでに紹介すると、case文を書くことができるわ。」
case文ってな〜に?」
if文は、if節elsif節にそれぞれ条件を書くことができたわよね。それの糖衣構文、シンタックスシュガーね。」
「とういこうぶん? しんたっくすしゅが〜?」
「みっくんは苦いお薬は好きかしら。」
「あんまり好きじゃない。」
「そうよね。良薬口に苦しとは言うけれど、苦いよりは甘いほうが好きだものね。糖衣構文、英語にするとシンタックスシュガーと言うけれど、プログラミングも一緒で、お砂糖の衣をかけて、食べやすくしているドーナツのようなものなの。」
「つまり、もう少し簡単に書けるようにした構文ということね。スライムの活気度をif文で書いたわ。HPの値に着目して、いくつ以上だったらピンピン、いくつ以上だったら死にかけと言う風に、いろいろ条件を書いて行ったけれど、HPの値に注目しているのはみんな共通しているわよね。」
「うん、そうだね。」
「そうすると、毎回 HPの値 が幾つ以上だったらと書くのは冗長だわ。HPの値に着目しているのは共通なのだから、その部分を括り出したコードを書きたくなるの。」
「うん、そうだね。毎回HPがこれ以上って言うふうに書くのも大変だもんね。そのケース文というのを使うと簡単にかけるんだね。」
「ええ、もともとの例が簡単だから、あんまり実感できないかもしれないけれど、case文と範囲オブジェクトを使って書いて見ると次のようになるわ。」

# スライムの活気度
liveliness = (hp.to_r / max_hp.to_r) * 100
case liveliness
when 70..100
  puts "スライムはピンピンしている"
when 30..70
  puts "スライムは弱ってきている"
else
  puts "スライムは死にそうだ"
end

「あ、この70..100というのが、さっき登場した範囲オブジェクトなんだね。70から100までという意味だよね。」
「ええ、そうよ。しっかり理解できているわね」
「liveliness 活気度っていう英単語なんだけれど、これの値が70〜100ならピンピン、30〜70なら弱っている、それ以外なら死にそうって表示されるわ。」
「うん、簡単だね。」
「活気度の計算は、今のスライムの体力が最大HPの何パーセントかを求めているんだね。」
「ええ、その通り。」
「で、この .to_rっていうのは何?」
Rubyの世界では、全てがオブジェクトって少し触れたけれど、hpっていうオブジェクトがあるの。」
「うん、スライムの体力を表している物なんだね。」
「ええ、その通りよ。この体力オブジェクトhpなんだけれど、HPが7で最大HPが10の時は活気度はいくつになるかしら。」
「7 ÷ 10だから0.7だよ。そして100を掛けてパーセントにしているから70になるんだよね。」
「ええ、そうよ、数学上はそれで正解だわ。」
「えっ、そうするとプログラムの時は違うの?」
「ええ、小数点を扱う時に少し違ってくる場合があるの。みっくん、コンピュータは二進数で動いているっていう話をしたんだけれど覚えているかしら。」
「うん、覚えてるよ。」
「普段、あたしたちが使ってるのは、十進数よね。10の約数は何と何かしら?」
「約数って割り切れる数のことだから、2と5だよね。」
「ええ、そうよ。10は2と5で割り切れるわ。だから割り算をしたときに2で割ったり5で割ったり1もちろん10で割ったりしても正確に表すことができる。」
「うん、そうだよ。」
「じゃあ、例えば3で割ったりしたらどうかしら、0.33333ってずっと続いていくわよね。」
「うん、そうだね。ずっと続いていくから10進数ではないんだね。」
「ええ、その通り。コンピュータは二進数の世界だから、2でちょうど割り切れる数だったら正確に表せるんだけれど、0.1とか0.2って言う小数は、正確に表せないのよ。浮動小数点数って言うのだけれど、表せる桁数の制約もあるの。だから微妙に誤差が出る時があるの。」
「そうなんだね。そうすると、この.to_rというのをつけるといいことがあるの?」
「ええ、そうよ。.to_rって言うメソッドなんだけれど、これは自分自身を有理数に変換した結果を返してくれるメソッドなの。」
「有理数って?」
「あ〜、ピタゴラスの定理のお話をしたわよね。」
「ええ、小数が登場したのはずっと最近のことなの。古代ギリシャの昔から数と数の比、有理数はあったわ。分数のことね。」
「うん。」
「でね。0.7と言う小数の代わりに、7/10という有理数、分数を返してくれるようになるわ。分数なら十進数でも二進数でも整数同士の比だから正確な値なの。」
「うん、そうだね。」
「このRPGだとあまり気にしなくてもいいのだけれど、知っておいて欲しかったので紹介したわ。」

0.7 を二進数に

「ちなみに二進数だと、0.7 はどうやって表せるの? みっくん、分かるかな?」
「え〜っと。7だったら、2^{2}=4が1つと2^{1}=2が1つと2^{0}=1が1つだから、111って表せるんだったよね。小数は逆になるから、1/2の位、1/4の位、1/8になるんだよね。」
「ええ、そうよ。みっくん賢いね。」
「うん。0.7は{1 \over 2^{1}} =0.5より大きいから、{1\over 2^{1}}の位は1
残りは0.2だから、{1 \over 2^{2}} =0.25より小さいから、{1\over 2^{2}}の位は0
残りは0.2だから、{1 \over 2^{3}} =0.125より大きいから、{1\over 2^{3}}の位は1
残りは0.075だから、{1 \over 2^{4}} =0.0625より大きいから、{1\over 2^{4}}の位は1
残りは0.0125だから、{1 \over 2^{5}} =0.03125より小さいから、{1\over 2^{5}}の位は0
残りは0.0125だから、{1 \over 2^{6}} =0.015625より小さいから、{1\over 2^{6}}の位は0
残りは0.0125だから、{1 \over 2^{7}} =0.0078125より大きいから、{1\over 2^{7}}の位は1
え〜、終わらないよ〜」
「0.7 = 0.1011001まで求めたのね。」
「そうね。ずっと続きそうな気がするわね。」
「二の冪乗が幾つ入っているか調べているだけなんだけれど、どんどん小数が小さくなっていくし、桁数が増えるから計算が大変よね。」
「うん、とっても大変だった。引き算間違っていないかな〜って確かめたりして・・・」
「ええ、なのでもっと簡単な方法を教えてあげる。」
「うん、教えて教えて。」
「まず0.7よね。これを二倍するの。すると幾つ?」
「1.4だよ。」
「ええ、二倍して1より大きくなったのだから、{1 \over 2^{1}} =0.5 より大きいってことよね。」
「うん。そうだね。」
「ええ、だから、{1 \over 2^{1}}の位は1」
「で、今度は残った0.4を二倍するの。幾つになる?」
「0.8だよ。」
「ええ、さらに二倍して1より小さいのだから、{1 \over 2^{2}} =0.25 より小さいってことよね。」
「うん。そうだね。」
「ええ、だから、{1 \over 2^{2}}の位は0」
「それで、残った0.8をさらに二倍するの。幾つになる?」
「1.6だよ。」
「ええ、さらに二倍して1より大きくなったのだから、{1 \over 2^{3}} =0.125 より大きいってことよね。」
「うん。そうだね。」
「ええ、だから、{1 \over 2^{3}}の位は1」
「やり方、掴めてきたかしら。要は二倍して整数部に1が出てきたら1を出力する。そして整数部から1を引く。そうでなかったら0を出力する。の繰り返しよ。」
「うん。やってみるね。」
「         0.7
 小数部を二倍して 1.4
 小数部を二倍して 0.8
 小数部を二倍して 1.6
 小数部を二倍して 1.2
 小数部を二倍して 0.4
 小数部を二倍して 0.8
 小数部を二倍して 1.6
 小数部を二倍して 1.2
 小数部を二倍して 0.4」
「うん、簡単だね。しかも同じ繰り返しが出ているよ。」
「ええ、二進数では正確に表せなくて、ずっと続く循環小数になることもこれで分かるわね。」

「これで、二進数への変換方法はバッチリね。計算手順のことをアルゴリズム、算法って言うわ。どういう方法で計算するかによって労力がずいぶん違うわよね。」
「本当だ。遙香さんに教えてもらった方法はすごく簡単だったよ。」
「良かったわ。じゃあ、これをRubyのプログラムにしてみようか。」
「うん。やって見る。」

今まで習った変数や四則演算、繰り返しや条件を使って書いて見る。

https://github.com/Atelier-Mirai/Ruby_RPG/blob/master/rpg/binary.rb

「遙香さん、できたよ〜」
「どう、動かしてみて〜」
「お〜、素敵、ちゃんと計算しているわね。桁数も表示するようになってるし、十桁だしたら終了するのね。ばっちりね。」

ヒアドキュメント

「じゃあ、もう一つ。ヒアドキュメント、教えてあげる。」
「ヒアドキュメントってなに?」
「行指向文字列リテラルって言ったりすることもあるけれど、ヒアはここ、ドキュメントは文章ね。ここに文章そのままの意味よ。」
「うん、どういうものなの?」
「そうね。アスキーアートでスライムの絵を描いたじゃない。」
「うん、書いたよ。」
「その時に、puts命令を使って一行ずつ表示させていたわよね。あれ、全部まとめて出せたら簡単でいいと思わない。それが実現できる機能なの。」
「そうなんだね。ダブルコーテーションで囲って一行ずつ書かなくちゃいけないの、面倒だなと思ってたんだ。まとめて出せるんだ〜」
「ええ、そうよ。」
「続けてできないのかな〜って思って、途中で改行してみたんだけれど、実はうまく表示できなかったんだ」

puts " / \
     /・Д・\
     〜〜〜〜〜"

「こうやって書いてみたんだけれど、隙間が入ってスライムがずれてしまうんだ。」

「そうね。ヒアドキュメントを使って、次のように書くと解決できるわ。」

puts <<~SLIME
   / \
  /・Д・\
  〜〜〜〜〜
  HP:#{hp}
SLIME

「ダブルコーテーションを使わなくていいから簡単だと思うわ。ここではスライムの絵だから、SLIMEって言う文字列にしたけれど、End Of String の略で、EOSなんかもよく使われるわね。最初と最後が同じであれば何でもいいわ。」
「じゃあ今までやったことを使ってバージョンアップさせてみて。楽しみにしているわ。」

僕はカタコトとキーボードをタイプする。そして完成した。

https://github.com/Atelier-Mirai/Ruby_RPG/blob/master/rpg/rpg07.rb

Discussion