今回は、前章までの石取りゲームをもう少し複雑にします。石の山が3つあります。それぞれに3,5,7個の石があります。
プレーヤーは交互に石を取っていきます。最後の1個の石を取った人が負けです。
ルールは
1.一度に何個の石をとっても良いが、最低でも1個の石を取らなくてはいけない。
2.一度に1つの山からしか石はとれない。
となっています。
これは、C言語編第76章のC++版です。
では、プログラムを見てみましょう。
// game35701.cpp
#include <iostream.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
class Mountain
{
int mt[3];
int sente, order;
int judge();
int show_stone();
int init();
int take_stone();
int comp_take();
public:
int play();
};
まずは、Mountainクラスを作ってみました。このクラスのメンバには、mtという配列があります。この配列に現在の山の石の数を 格納することにします。
山の名前を仮にA,B,Cとするとそれぞれの山にある石の数は
mt[0],mt[1],mt[2]
を調べれば良いことになります。
senteメンバは、人間が先手なら1、コンピュータが先手なら0を入れておきます。
orderは現在の取り手の番を示します。1なら先手の番、0なら後手の番というようにしておきます。
後のメンバ関数は、名前から想像がつくと思います。
judgeメンバ関数は、勝敗がついたかどうかを判定します。
show_stoneメンバ関数は、現在の山の石の数を表示します。
initメンバ関数は、ゲームを始めるときの初期化を行います。コンストラクタで 行わずに、メンバ関数を作ったのは、繰り返しゲームを行えるようにするためです。
comp_takeメンバ関数は、コンピュータがどの山から、いくつ石を取るかを決めます。
playメンバ関数は、このゲームを取り仕切ります(?)。
int Mountain::show_stone()
{
cout << "[A]---" << mt[0] << ", [B]---" << mt[1] << ", [C]---" << mt[2] << endl;
return 0;
}
現在の、石山の数を表示します。これは、簡単ですね。
int Mountain::judge()
{
if (mt[0] + mt[1] + mt[2] == 1)
return 0;
else
return 1;
}
勝敗がついたかどうかを判定します。プレーヤーが、石を取った後、すべての山の石の合計が1であれば、そのプレーヤーの勝ちです。
勝負がついたら0を返し、その他の時は1を返します。
もし、山Aに3個、B,Cに石が0個の時、Aから3個の石を取ったらどうでしょうか。 プレーヤーは、Aから2個石を取れば勝つのに、あえて3個の石を取るという行為をしない、という 暗黙のルールを作らなくてはいけません。
そうしなければ勝負がつきません。コンピュータが石を取るときも、このことを念頭に置きます。 人間が取る場合、間違って3個石を取ろうとした場合は、注意する仕組みも作る必要がありそうです。
int Mountain::take_stone()
{
char ans[8];
int stone, mtorder;
if (sente != order) {
comp_take();
if (judge() == 0) {
cout << "コンピュータの勝ちです" << endl;
return 1;
}
if (order == 1)
order = 0;
else
order = 1;
return 0;
}
while (1) {
cout << "どの山からとりますか(A,B,C)---";
cin >> ans;
if (strcmp(ans, "A") != 0 && strcmp(ans, "B") != 0 && strcmp(ans, "C") != 0 &&
strcmp(ans, "a") != 0 && strcmp(ans, "b") != 0 && strcmp(ans, "c") != 0) {
cout << "山の指定が正しくありません" << endl;
continue;
} else {
ans[0] = toupper(ans[0]);
mtorder = ans[0] - 'A';
if (mt[mtorder] == 0) {
cout << "その山にはもう石はありません" << endl;
continue;
}
break;
}
}
while (1) {
cout << "いくつとりますか---";
cin >> ans;
stone = atoi(ans);
mt[mtorder] -= stone;
if (mt[0] + mt[1] + mt[2] == 0) {
cout << "その取り方では山の石が全部0になります" << endl;
mt[mtorder] += stone;
continue;
}
mt[mtorder] += stone;
if (mt[mtorder] - stone < 0 || stone <= 0) {
cout << "取る石の数が不正です" << endl;
continue;
} else {
break;
}
}
mt[mtorder] -= stone;
show_stone();
if (judge() == 0) {
cout << "あなたの勝ちです" << endl;
return 1;
}
if (order == 1)
order = 0;
else
order = 1;
return 0;
}
石を取る関数です。senteとorderの値が異なる場合は、コンピュータの取る番です。 そのような時は、comp_take関数を呼んで、コンピュータに石を取らせます。 コンピュータが石を取った後で、judgeメンバ関数を呼んで、勝敗がついたかどうかを調べます。 勝敗がついたのなら、コンピュータの勝ちなのでその旨表示してreturnします。
勝敗がつかない場合は、orderを反転して(1なら0に、0なら1にして)returnします。
senteとorderが一致する場合は、人間の番なので、まず、どの山から取るかをたずねます。
この時、プレーヤーは「A,B,C,a,b,c」のいずれかを指定しなくてはいけません。
これ以外の文字を指定すると、continueでループの最初に戻されます。
ただし、「aZ」などと2文字以上で答えることも出来ますが、この場合は先頭文字のみが 比較の対象となります。
さて、山を指定された場合、Aまたはaの山の石の個数はmt[0]です。Aを0に関連づけるには どうすればよいのでしょうか。これは、簡単です。プログラムを見てください。小文字を 指定されたときのためにtoupper関数で大文字にしておいてから
mtorder = ans[0] - 'A'
としています。ans[0]が'A'ならmtorderは0,'B'なら1、'C'なら2となります。
次に、その山からいくつ取るかをたずねます。もし、その通取って、すべての山が0になってしまっては 勝負がつかないので、そのチェックをします。
また、取る石の数が0または負であってはいけません。さらに、山の数より多い石はとれません。 これらのチェックもします。
その後、石山の数を最新のものに変更します。
次に、show_stoneメンバ関数関数を呼んで、各山の石の個数を表示します。
その後、judgeメンバ関数で勝敗の判定をします。
勝敗がつかなければ、orderを反転してreturnします。
int Mountain::init()
{
char ans[8];
mt[0] = 3;
mt[1] = 5;
mt[2] = 7;
cout << "あなたが先手になりますか(Y/N)---";
cin >> ans;
if (ans[0] == 'y' || ans[0] == 'Y') {
sente = 1;
} else {
sente = 0;
}
order = 1;
return 0;
}
ゲームを初期化するメンバ関数です。山の石の数をそれぞれ、3,5,7にします。
先攻、後攻を決めます。
orderを1に設定します。
int Mountain::comp_take()
{
int mtorder, stone;
char mtname;
srand((unsigned)time(NULL));
while (1) {
mtorder = rand() % 3;
if (mt[mtorder] == 0)
continue;
else
break;
}
while (1) {
stone = rand() % (mt[mtorder]) + 1;
mt[mtorder] -= stone;
if (mt[0] + mt[1] + mt[2] == 0) {
mt[mtorder] += stone;
continue;
} else {
break;
}
}
mtname = 'A' + mtorder;
cout << "コンピュータは" << mtname << "から" << stone << "個取りました" << endl;
show_stone();
return 0;
}
コンピュータが、石を取ります。必勝法は使わずに、乱数で適当に石を取ります。
まず、srand関数で、乱数の種をまきます。 srand関数についてはC言語編第24章で 出てきています。
最初にどの山から取るかを乱数で決めます。 rand関数は、0からRAND_MAXまでのint型整数を返すので、これを3で割り余りを 調べます。あまりは、0,1,2のいずれかですので、山の指定になります。
もし、その山にすでに石がないときは、continueでループの最初に戻り、やり直します。
山が決定したら、取る石の個数を決めます。 やは、rand関数で乱数を発生させて、これをmt[x]でわります。余りは0からmt[x]-1のいずれかなので これに1を加えた数を取る石の個数とします。
この時、すべての山に石がなくなってしまっては勝負にならないので、これをチェックします。 もし、引っかかるようならcontinueでループの最初に戻りやり直します。
取る石の個数が決定したら、show_stoneメンバ関数で各山の石の数を表示します。
int Mountain::play()
{
int ret;
char ans[8];
while (1) {
init();
show_stone();
while (1) {
ret = take_stone();
if (ret == 1)
break;
}
cout << endl << "もう一度やりますか(Y/N)";
cin >> ans;
if (ans[0] == 'y' || ans[0] == 'Y')
continue;
else
break;
}
return 0;
}
ゲームを取り仕切る(?)関数です。無限ループに入ります。
ゲームを初期化して、石の数を表示したら、次の無限ループに入ります。
take_stoneメンバ関数を呼び、石を取ります。 勝敗がつくまで、take_stoneメンバ関数を呼びます。
勝敗がついたら、再度ゲームをするかどうかたずね、再度ゲームをするなら 最初のinitメンバ関数の所に戻ります。やめるなら、breakでループを抜けて プログラムを終了します。
int main()
{
Mountain m;
m.play();
return 0;
}
メイン関数です。これは、たったこれだけです。
もっと短くしたい場合は、playメンバ関数をコンストラクタで呼び出せばよいのですが、
これでは何がなんだかわからなくなるので、この程度にしておきました。どうやったらこのゲームに勝てるのか、考えてみてください。
Update Mar/22/2002 By Y.Kumei