今回は、前章までの石取りゲームをもう少し複雑にします。石の山が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