第57章 357の山 その1


今回は、前章までの石取りゲームをもう少し複雑にします。

石の山が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メンバ関数をコンストラクタで呼び出せばよいのですが、 これでは何がなんだかわからなくなるので、この程度にしておきました。

どうやったらこのゲームに勝てるのか、考えてみてください。


[C++Index] [総合Index] [Previous Chapter] [Next Chapter]

Update Mar/22/2002 By Y.Kumei
当ホーム・ページの一部または全部を無断で複写、複製、 転載あるいはコンピュータ等のファイルに保存することを禁じます。