プログラミングの実戦課題 - タロット占いを創る ①「Random クラスと switch-case 文と throw 文」
こんばんは。九条です。
今回は前回に引き続きプログラミングの話をします。
今回は少し課題のテーマを変えます。
本格的なゲームを創るなら、コンピュータにプログラマが望む面数のダイス(サイコロ)を振らせる処理をする必要があります。
本記事では、コンピュータにサイコロを振らせる方法と、振って出た目の扱い方を勉強します。
仕様
今回のお題はタロット占いです。
1.プログラムを起動したら、タロットカード1枚をコンピュータがランダムに選んで、その結果(正位置、逆位置を含む。)を表示する。
たったこれだけです。
占いに慣れた人であれば、タロットカードの大アルカナには0番から21番の番号が打たれているので、番号を見ただけで絵柄まで暗記していて想起できる方もいると思いますが、今回は番号だけでなく名前まで表示するものとします。
タロットカードの絵柄には意味があり、絵柄を解釈して、占いをするのですが、今回は解釈には踏み込みません。コンピュータが選んだ番号が何だったのかだけを表示します。
タロットカードには、大アルカナと小アルカナがあります。占いの素人の方がタロットと聞いてイメージするのは大アルカナであることが多いです。大アルカナと言えば、「死神」のカードなんかが良く知られていますね。
小アルカナはトランプのような構成になっていて、占いの上級者であれば、これらも使って占いをするのですが、初心者がやる場合は大アルカナだけで占いをする場合もあります。
今回の課題は、大アルカナに絞って話を進めます。小アルカナを入れた占いについては、enum や foreach のような文法事項が絡むので、それらの解説が必要になったところで作ってみようと思います。
また、タロットには様々な占い方があり、カードを複数枚使う例えばケルト十字展開のような占い方もあります。その場合、プログラムが複雑となってくるので、今回は扱いませんが、これは配列の勉強になるので、後程扱おうと思います。
今回の課題で学べること
今回は次のクラスライブラリと文法事項の使い方を勉強します。
・Random クラス
・swtch-case 文
・throw 文
解説1
コンピュータにサイコロを振らせるには、Random クラスを使います。
例えば、次のようなコードを書くと1~6の整数を画面に表示することができます。表示される数は、実行するたびに毎回変わります。
using System;
namespace Dice6
{
class Program
{
static void Main(string[] args)
{
Random random = new Random();
int randomValue = random.Next(1, 7);
Console.Write(randomValue);
}
}
}
このコードの説明としては、まず、乱数生成器を作る必要があるので、
Random random = new Random();
の1行で作っています。
Random とは、乱数生成器の設計図で、new によって設計図から現物を作り出すことができます。
次に、
int randomValue = random.Next(1, 7);
の部分で、Next() は乱数を実際に生成する関数(メソッド)なのですが、1と7を渡していることに違和感を感じる人がいるかもしれません。
Next() メソッドは、第一引数の値以上かつ、第二引数の値より少ない乱数(ランダムな値)生成します。
要するに、1≦ randamValue < 7 となるわけです。
乱数が追加で幾つも欲しい場合でも乱数生成器を作り直してはいけません。Next() メソッドは呼び出すたびに毎回違う値を生成します。(1/6の確率で、偶然に前と同じ値を生成することも、もちろんあります。)
この、Next() メソッドに0から21番の値を生成させ、それを元に swtich-case 文で場合分けすれば、21枚の大アルカナのカードから1枚を選択することができます。
さて、まずはここまでを作り込んでみましょう。
回答(途中)
using System;
namespace Tarot1
{
class Program
{
static void Main(string[] args)
{
Random random = new Random();
int randomValue = random.Next(0, 22);
string cardName;
switch (randomValue)
{
case 0:
cardName = "愚者";
break;
case 1:
cardName = "魔術師";
break;
case 2:
cardName = "女司祭";
break;
case 3:
cardName = "女帝";
break;
case 4:
cardName = "皇帝";
break;
case 5:
cardName = "司祭";
break;
case 6:
cardName = "恋人";
break;
case 7:
cardName = "戦車";
break;
case 8:
cardName = "力";
break;
case 9:
cardName = "隠者";
break;
case 10:
cardName = "運命の輪";
break;
case 11:
cardName = "正義";
break;
case 12:
cardName = "吊るし人";
break;
case 13:
cardName = "死";
break;
case 14:
cardName = "節制";
break;
case 15:
cardName = "悪魔";
break;
case 16:
cardName = "塔";
break;
case 17:
cardName = "星";
break;
case 18:
cardName = "月";
break;
case 19:
cardName = "太陽";
break;
case 20:
cardName = "審判";
break;
case 21:
cardName = "世界";
break;
default:
throw new ApplicationException("存在しないカードが指定されました。");
}
Console.WriteLine(randomValue.ToString() + "." + cardName);
}
}
}
いくつか解説を補足します。
まず、このプログラムは if-else if-else 文を使って書くことも可能です。しかし、かなり読みづらく複雑なプログラムになって今いますので、そんなことはしません。switch-case 文を使うと非常に見やすいプログラムになります。
default:
throw new ApplicationException("存在しないカードが指定されました。");
この部分を取り除くとコンパイルエラーになり、実行できません。
何故エラーになるかと言えば、randamValue の値が0から21のいずれでもない場合に、cardName の値が不明となるからです。言語によっては不明な値を認めていたり、変数の値に何らかの初期値が設定されている場合もあるのですが、C#では値が不明となるような変数が登場すると、コンパイルエラーで弾かれてしまいます。
int randomValue = random.Next(0, 22);
と書いたことで、randamValue の値が0から21以外にはなり得ないのではないかと思う方もいるかもしれませんが、C#ではそのようなことは考慮してくれず、値が不明かどうかはもっと機械的に判断されます。
default:
throw new ApplicationException("存在しないカードが指定されました。");
これは、どういう意味かというと、あり得ない事態ですが、万が一にも、0から21以外の値が switch-case 文に渡された場合は、エラー(例外)として実行を打ち切ることを表しています。
エラー(例外)は try-catch 文で捕捉すれば、処理を続行できるのですが、ここではそのような箇所はないため、このあり得ない事態が起きた場合は、そこでプログラムがクラッシュします。
throw に来た時点で、その場で処理が打ち切られるので、この後に break; を付ける必要はありません。
Console.WriteLine(randomValue.ToString() + "." + cardName);
は、カード名だけでなく番号を付けて表示しています。
例えば、「1.魔術師」のように表示されます。
ToString() メソッドは . の左側にあるオブジェクトを文字列に変換する関数(メソッド)です。JavaScript のような言語では、数値と文字列の足し算は、数値が文字列に自動的に変換されるのですが、C#にはそのような文法はありません。数値と文字列を足し算する(繋げる)場合は、数値の方を文字列に変換する必要があります。
なお、この部分は次のように書くこともでき、こちらの方が直感的かもしれません。
Console.Write("{0}.{1}", randomValue, cardName);
{0}{1} は、後続の引数の値で置換されて出力されるというわけです。{0}{1} のように、後で置換されることを想定した箇所のことをプレースホルダと呼ぶことがあります。
プレースホルダのインデックス(番号)は1ではなく0から始まることに注意してください。
解説2
タロット占いの場合、正位置と逆位置で意味が変わってきます。正位置と逆位置を区別しないという占いのやり方もありますが、正位置と逆位置も画面に表示させてみましょう。
正位置と逆位置のように、2択の場合は、ダイスを振るというよりはコイン投げのイメージですが、プログラミングではどちらも同じように書くことができます。Next() 関数の呼び出しを次のように書けば良いです。
int randomValue2 = random.Next(0, 2);
以上を踏まえた上での最終的なプログラムは次のようになります。
※randomValue を randomValue1 に名前変更しております。
回答
using System;
namespace Tarot1
{
class Program
{
static void Main(string[] args)
{
Random random = new Random();
int randomValue1 = random.Next(0, 22);
string cardName;
switch (randomValue1)
{
case 0:
cardName = "愚者";
break;
case 1:
cardName = "魔術師";
break;
case 2:
cardName = "女司祭";
break;
case 3:
cardName = "女帝";
break;
case 4:
cardName = "皇帝";
break;
case 5:
cardName = "司祭";
break;
case 6:
cardName = "恋人";
break;
case 7:
cardName = "戦車";
break;
case 8:
cardName = "力";
break;
case 9:
cardName = "隠者";
break;
case 10:
cardName = "運命の輪";
break;
case 11:
cardName = "正義";
break;
case 12:
cardName = "吊るし人";
break;
case 13:
cardName = "死";
break;
case 14:
cardName = "節制";
break;
case 15:
cardName = "悪魔";
break;
case 16:
cardName = "塔";
break;
case 17:
cardName = "星";
break;
case 18:
cardName = "月";
break;
case 19:
cardName = "太陽";
break;
case 20:
cardName = "審判";
break;
case 21:
cardName = "世界";
break;
default:
throw new ApplicationException("存在しないカードが指定されました。");
}
int randomValue2 = random.Next(0, 2);
string seigyaku;
if (randomValue2 == 0)
{
seigyaku = "正位置";
}
else
{
seigyaku = "逆位置";
}
Console.WriteLine("{0}.{1}({2})", randomValue1, cardName, seigyaku);
}
}
}
さてプログラムを作成した後は、テストが必要ですが、この事例のテストは結構大変です。テストをするためだけのソフトを開発する必要があります。
具体的には1万回程度試行して、愚者から世界まで、それぞれ正位置と逆位置が出現することと、それ以外のものが出現しないこと(throw new ApplicationException(...); の実行によってクラッシュしないこと)を確認する必要があります。
今回はそこまで踏み込みませんが、いずれ記事にしようと思います。
以上です。