【C言語】マルバツゲーム(三目並べ)の作り方

C言語でのマルバツゲームの作り方解説ページアイキャッチ

このページでは、C言語での「マルバツゲーム(別名:三目並べ)」の作り方について解説していきます。

マルバツゲームとは、2人のプレイヤーに「○」「×」のどちらかの印が割り当てられ、さらに下の図のような3x3のマスの中に2人のプレイヤーが交互に自身の印を記入していくゲームになります。

マルバツゲームがどのようなゲームであるかを示す図

自身の印(「○」「×」)を3つ並べることができれば、そのプレイヤーの勝ちとなります。3つ並ぶ前に3x3の全てのマスが埋まった場合は引き分けとなります。

マルバツゲームで勝敗が決定した様子

皆さんも一度はプレイしたことのあるゲームではないでしょうか?

マルバツゲームは文字の入力・出力だけで実現できるため、特にC言語入門者の方にとっては復習や力だめしをするのに良い題材のゲームだと思います。

このページでは、そんなマルバツゲームのC言語での作り方について解説していきます。

マルバツゲームの作り方

では、マルバツゲームの作り方を解説していきます。

今回は、プレイヤー対コンピューターの1人プレイ用のマルバツゲームを開発していきたいと思います。

さらに、プレイヤーが「○」を、コンピューターが「×」を記入していくことを前提に解説していきたいと思います。

各マスに記入された ○× を管理する

マルバツゲームを実現する上で一番のポイントになるのが「3 x 3 の各マスに記入された印の管理」になると思います。

マルバツゲームを実現するために必要な情報の管理

マルバツゲームにおいては、○ or × のどちらか一方が横方向 or 縦方向 or 斜め方向に3つ並んだ際に勝敗が決定します。

そのため、各位置に記入済みの印を管理しておき(覚えておき)、それに応じて勝敗を決定するような処理が必要となります。

また、マルバツゲームにおいては既に印が記入済みの位置には印を記入することができません

○× を記入する位置を決める(decidePosition 関数) でプレイヤーやコンピューターが印を記入する位置の決め方について解説しますが、その位置に既に印が記入されている場合、再度位置決めをやり直すような処理が必要となります。

そして、これを行うためにも、各位置に記入済みの印を管理しておき、新たに印を記入しようとしている位置に既に印が記入されていないかどうかを判断するような処理が必要となります。

印記入時に既に記入済みのマスが指定された時に警告を出す様子

つまり、マルバツゲームを実現していくためには、3 x 3 のマスの各位置に記入済みの印を適切に管理していくことが必要となります。

そのため、今回は 3 x 3 の2次元の配列 board を用意し、この配列を利用して各位置に記入済みの印を管理するようにしていきたいと思います。

そして、マルバツゲームの 3 x 3 のマスにおける位置 (x, y) に記入済みの印を、2次元配列 board の要素 board[x][y] で管理するようにしていきます(x:横方向の位置・y:縦方向の位置)。

各マスの状態を2次元配列boardが管理する様子

具体的には、位置 (x, y) に記入された ○×(or 未記入)に対し、board[x][y] に下記の文字を格納するようにすることで、各位置に記入済みの印を管理していきます。例えば上の図では、位置 (1, 0) には ○ が記入されているわけですから、そのことを管理できるように board[1][0]'o' を格納しておきます。

  • ○:'o'
  • ×:'x'
  • 未記入:' '

そして、board の参照を行ないながらマルバツゲームを開発する上で必要になる処理を実現していきます。

例えば、プレイヤーやコンピューターから印を記入する位置 (x, y) を指定された際には、board[x][y]' ' であるかどうかを判断し、YES であれば記入する印に応じて 'o' or 'x'board[x][y] に格納するようにし、NO であれば印を記入する位置を決め直すような処理を行います。

印記入のやり直しを求める際にboardを参照する様子

このように各マスの状態を考慮しながら処理を行なっていくことで、マルバツゲームを実現することができるようになります。

また、各位置の状態の管理は、マルバツゲームだけでなくオセロや囲碁などのゲームをプログラミングする際にも必要になりますので、こういった状態の管理の仕方は是非覚えておくと良いと思います!

スポンサーリンク

処理の流れを制御する(main 関数)

2次元配列 board を導入することで、3 x 3 のマスの各位置に記入済みの印を管理できるようになりました。

これによってプログラム内で管理すべきデータの管理方法が決まったことになりますので、ここからはプログラム内で行う「処理」について説明していきたいと思います。

まずは、マルバツゲームの流れを思い出しながら、マルバツゲームを実現する上で必要になる処理を洗い出していきましょう!

これらの処理を関数として切り出し、さらにそれらの関数を main 関数から実行するようにすることで、マルバツゲームの処理の流れを実現していきたいと思います。

まず、マルバツゲームにおいては、プレイヤー or コンピューターが「○× を記入する位置を決める」処理が必要になります。

そして、その決めた位置に対して「○× を記入する」処理が必要になります。

また、現実世界のマルバツゲームではプレイヤーが 3 x 3 のマスを目で確認しながらプレイすることになります。そのため、プログラムでマルバツゲームを実現する際にも、マスに ○× が記入された際に 3 x 3 のマスを表示してプレイヤーが状態を確認できるようにしてあげたほうが便利です。

そのため、「3 x 3 のマスを表示する」処理もあったほうが良いでしょう。

さらに、その ○× の記入を行うことで ○ や × が3つ並ぶ可能性やマスが全て埋まる可能性がありますので、○× の記入後には「勝負の結果を判断する」処理を行う必要があります。

もし勝負の結果が決まっていないのであれば、次は先ほど ○× を記入したのとは他方側の相手に ○× を記入してもらって同様の処理を進めていくことになります。この際には、次の相手に「ターンを進める」処理が必要となります。

勝負の結果が決まったのであれば(プレイヤーの勝ち or プレイヤーの負け or 引き分け)、その時点でゲームは終了となります。この場合は、勝負の結果をプレイヤーに伝えるために「勝負の結果を表示する」処理が必要となります。

これらをまとめると、マルバツゲームのプログラムの処理の流れは下記のようなものになることになります(括弧内には各処理を実行する関数の名前を示しています)。

  1. ○× を記入する位置を決める(decidePosition
  2. ○× を記入する(writeMark
  3. 3 x 3のマスを表示する(printBoard
  4. 勝負の結果を判断する(judgeResult
  5. ターンを進める(nextTurn
  6. 勝負の結果が決まっていない場合は 1. に戻る
  7. 勝負の結果を表示する(printResult

この処理の流れを実現するために、下記のような main 関数を用意したいと思います。

main関数および各種定義

#include <stdio.h> // scanf/printf
#include <stdlib.h> // rand/srand
#include <time.h> // time

#define NUM 3 // マス数
#define TARGET NUM // 並べたら勝ちとなる印の数
#define true 1
#define false 0
typedef int bool;

typedef enum {
    RESULT_WIN, // プレイヤーの勝ち
    RESULT_LOSE, // プレイヤーの負け
    RESULT_DRAW, // 引き分け
    RESULT_NONE // 結果が決まっていない
} RESULT;

typedef enum {
    TURN_PLAYER, // プレイヤーのターン
    TURN_COM, // コンピューターのターン
} TURN;

char board[NUM][NUM]; // マスに記入された印を管理する配列

int main(void) {

    int x, y;
    RESULT result = RESULT_NONE;
    TURN turn = TURN_PLAYER;

    init();
    printBoard();

    do {

        // 1.○×を記入する位置を決める
        decidePosition(&x, &y, turn);

        // 2.○×を記入する
        writeMark(x, y , turn);

        // 3.3x3のマスを表示する
        printBoard();

        // 4.勝負の結果を判断する
        result = judgeResult(turn);
        
        // 5.ターンを進める
        turn = nextTurn(turn);

    // 6.勝負の結果が決まっていない場合は1.に戻る
    } while (result == RESULT_NONE);

    // 7.勝負の結果を表示する
    printResult(result);
}

main 関数では、前述の 1. 〜 6. の処理を行う関数を勝負の結果が決まるまで繰り返し実行するようにしています。基本はこの 1. 〜 6. を繰り返すことでマルバツゲームを実現していくことになりますが、最後に 7. の結果の表示(printResult)や、ループに入る前に初期化処理(init)を行うようにもしています。

さらに、turn はプレイヤー or コンピューターのどちらのターンであるかを示す変数であり、最初に turn = TURN_PLAYER としているため、ゲームはプレイヤーのターンから始まることになります。

また、ゲームの継続条件(繰り返しの継続条件)は result == RESULT_NONE であり、この式は勝負の結果が決まっていないかどうかを判断するための条件式となっています。具体的には、RESULT_NONE は「勝負が決まっていない」ことを示す列挙子、resultjudgeResult という勝負の結果を判断する関数から返却された値を格納する変数であり、これらを比較することでゲームを継続するかどうかを判断しています。

ここまでの説明の中にも出てきた TURN_PLAYERRESULT_NONEenum で定義した列挙子であり、この enum に関しては下記ページで解説していますので、必要に応じてこちらを参考にしていただければと思います。

列挙型・enum の解説ページアイキャッチ【C言語】列挙型(enum)について解説

また、上記では bool 型や true / false の定義を行なっていますが、これらを定義する代わりに stdbool.hinclude するのでも良いです。

stdbool.h をインクルードする際の bool 型の使い方は下記ページで解説していますので、必要に応じてこちらを参考にしていただければと思います。

C言語でbool型を扱う方法の解説ページアイキャッチC言語でのbool型の使い方

さらに、1方向のマス数は NUM、並べたら勝ちとなる印の数は TARGET という定義名を利用してマルバツゲームを実現していきます。マルバツゲームは 3 x 3 のマスを利用し、同じ印を 3 並べたら勝ちとなるため、NUMTARGET は両方とも 3 となります。

さて、上記の main 関数によりマルバツゲームにおけるゲームの大まかな流れを実現できたことになります。

ただし、まだ実現できたのはゲームの大まかな流れだけであり、ゲームに必要な詳細な処理が実現できていません。具体的には、main 関数から呼び出されている関数がまだ定義されていません。

ということで、ここからはその詳細な処理を関数として定義していきたいと思います。

初期化を行う(init 関数)

まずは初期化を行う init 関数を定義していきたいと思います。

マルバツゲームで必要になる初期化処理の1つは2次元配列 board の初期化です。

各マスに記入された ○× を管理する で解説したように、board3 x 3 の各マスに記入された印を管理する2次元配列です。

当然ですが、ゲーム開始時には 3 x 3 のマスは真っ白、つまり印が1つも記入されていない状態になりますので、これに合わせて 2次元配列 board の全要素に ' ' を格納しておく必要があります。

また、ゲームでは乱数を利用する機会が多く、このマルバツゲームにおいてもコンピューターが印を記入する位置を決める際に乱数を利用しようと思います。

C言語においては、乱数は rand 関数から生成することができるのですが、プログラム実行時に毎回異なる乱数を生成するようにするためには srand 関数を実行しておく必要があります。

そのため、その srand 関数の実行と前述の board への ' ' の格納を、マルバツゲームのプログラムにおける初期化処理としたいと思います。

この初期化処理は、下記のように init 関数を定義しておき、この init 関数を main 関数から呼び出すことで実現することができます。

init

void init(void) {
    srand(time(NULL));

    for (int x = 0; x < NUM; x++) {
        for (int y = 0; y < NUM; y++) {
            board[x][y] = ' ';
        }
    }
}

処理の流れを制御する(main 関数) のソースコードで示したように、NUM3 を示す定数マクロとなります。また boardはグローバル変数として宣言しているため引数としては受け取らないようになっています。

init 関数で使用している srand 関数と、後述で使用する rand 関数については下記ページで解説しているため、詳しく知りたい方はこちらを参照していただければと思います。

C言語で乱数を扱う方法の解説ページアイキャッチC言語で乱数を扱う方法(rand関数とsrand関数)

○× を記入する位置を決める(decidePosition 関数)

続いて、○× を記入する位置を決める decidePosition 関数を定義していきます。

○× を記入する位置の決め方はプレイヤーの場合とコンピューターの場合とで異なるため、それぞれについて個別に説明していきたいと思います。

プレイヤーの ○× を記入する位置を決める

印を記入するのがプレイヤーの場合、印を記入する位置はプレイヤー自身に決めてもらい、その決めた位置を scanf 等を利用して入力して貰えば良いです。

ただし、マルバツゲームの場合、2次元空間における位置を決める必要があるため、横方向の位置と縦方向の位置の2つの位置を入力してもらう必要があります。

横方向と縦方向の2つの位置を指定する様子

今回は単純に2回 scanf 関数を実行することで、プレイヤーから横方向と縦方向の位置を個別に入力してもらえるようにしたいと思います。

したがって、プレイヤーの ○× を記入する位置を決める関数を decidePlayerPosition とすれば、この関数は下記のように定義することができます。

decidePlayerPosition

void decidePlayerPosition(int *x, int *y) {
    printf("どこに印を記入しますか?\n");
    printf("xの入力(0 - %d):", NUM - 1);
    scanf("%d", x);
    printf("yの入力(0 - %d):", NUM - 1);
    scanf("%d", y);
}

引数がポインタであることに注意してください。decidePlayerPosition 関数は横方向の位置と縦方向の位置の2つの値を返却する必要がありますが、return では1つの値の返却しか行えません。

そのため、引数をポインタとし、そのポインタの指す先に値を格納することで2つの値の返却を実現するようにしています。

ポインタに関しては下記ページで解説していますので、詳しく知りたい方はこちらを参照していただければと思います。

ポインタの解説ページアイキャッチ【C言語】ポインタを初心者向けに分かりやすく解説

コンピューターの ○× を記入する位置を決める

プレイヤーの印を記入する位置をプレイヤーからの入力によって決定するのに対し、コンピューターの場合、印を記入する位置はプログラムの中で決めてやる必要があります。

このコンピューターが印を記入する位置の決め方を工夫することにより、強いコンピューターを実現するようなことも可能です。ただし、今回は簡単のため、単純にランダムに記入する位置を決めるようにしたいと思います。

ランダムに位置を決める際には、下記ページで紹介している rand 関数を利用すれば良いです。

C言語で乱数を扱う方法の解説ページアイキャッチC言語で乱数を扱う方法(rand関数とsrand関数)

マルバツゲームにおいてマス数は横方向・縦方向共に 3 なので、マスの位置は 0 〜 2 の値で示すことができます。そのため、0 〜 2 の値を2回分(横方向と縦方向)ランダムに決定するようにすれば、印を記入する位置をランダムに決めることができることになります。

そして、0 〜 2 の値をランダムに決定するためには rand() % 3 を実行する必要があります。つまり、rand() % 3 を2回実行することで印を記入する位置をランダムに決定することができることになります。

コンピューターの ○× を記入する位置を決める関数を decideComPosition とすれば、この関数は上記を踏まえれば次のように定義することができます。

decideComPosition

void decideComPosition(int *x, int *y) {
    *x = rand() % NUM;
    *y = rand() % NUM;
}

NUM3 を示す定数マクロとなります。また、decideComPosition 同様に引数がポインタである点に注意してください。

また、前述の通り、今回は完全にランダムに ○× を記入する位置を決定するようにしていますが、この ○× を記入する位置の決め方を工夫することによってコンピューターを強くすることが可能です。

例えば、プレイヤーの記入する印が2つ連続した際に3つ連続して印が記入されることを阻止するように位置を決定するようにすれば、完全ランダムに位置を決めるよりもコンピューターは強くなると思います。

指定された位置に ○× を記入可能かどうかを判断する

上記のような処理を行うことで、プレイヤー・コンピューターの場合の両方で ○× を記入する位置を決めることができるようになりました。

ただし、その決めた位置に ○× が記入できるとは限りません。

例えば、プレイヤーが scanf 関数に対して 02 の値以外の整数を入力した場合は、指定された位置が不正ということになります。

プログラムが指定された位置に記入可能であるかどうかを判断する様子1

また、その決めた位置に既に ○× が記入されている場合は、その位置に新たに ○× を記入することができないことになります。

プログラムが指定された位置に記入可能であるかどうかを判断する様子2

こういった場合には、○× を記入する位置の決定をやり直すような制御が必要となります。そして、この制御を行うためには、決定された ○× を記入する位置に実際に ○× が記入可能かどうかを判断する処理が必要となります。

その判断を行う関数 isMarkable とすれば、この関数は下記のように定義することができます。

isMarkable

bool isMarkable(int x, int y) {
    if (x < 0 || x > NUM - 1 || y < 0 || y > NUM - 1) {
        printf("入力した値が不正です...\n");
        return false;
    }

    if (board[x][y] != ' ') {
        printf("そのマスはすでに埋まってマス...\n");
        return false;
    }

    return true;
}

isMarkable 関数は、引数で指定された位置 (x, y) に ○× を記入可能であるかどうかを判断し、記入可能である場合は true を、記入不可の場合は false を返却する関数となります。

isMarkable 関数の1つ目の if 文では、位置 (x, y) が 3 x 3 のマスの外側を示す位置であるかどうかの判断を行なっています。

また、2つ目の if 文では、位置 (x, y) に既に ○× が記入済みであるかどうかの判断を行なっています(未記入の場合のみ board[x][y] には ' ' が格納されている)。

これら1つの if 文でも YES となった場合、位置 (x, y) には ○× は記入不可であるため false を返却し、2つの if文両方が NO の場合のみ true を返却するようにしています。

decidePosition 関数の定義

上記で定義した decidePlayerPositiondecideComPositionisMarkableを利用した場合、○× を記入する位置を決定する decidePosition 関数は下記のように定義することができます。

decidePosition

void decidePosition(int *x, int *y, TURN turn) {
    while (true) {
        if (turn == TURN_PLAYER) {
            decidePlayerPosition(x, y);
        } else {
            decideComPosition(x, y);
        }

        if (isMarkable(*x, *y)) {
            break;
        }
    }
}

decidePosition は、決定した位置を引数で指定されたアドレス xy に格納する関数となります。

さらに、次に ○× を記入するのがプレイヤー or コンピューターのどちらであるかを判断できるよう、どちらのターンであるかを示す引数として turn を受け取るようにしています。

そして、turn に応じて decidePlayerPositiondecideComPosition のいずれかを実行して ○× を記入する位置を決定し、その位置に ○× が記入可能であるかを isMarkable 関数で判断しています。

記入可能であれば、breakwhile ループを抜け出して関数を終了し、記入不可の場合はループの最初に戻って再度 decidePlayerPosition or decideComPosition を実行するようにしています。

これによって、○× が記入可能な位置がアドレス xy に格納されるまでループが継続するようになっています(アドレス xy への値の格納は decidePlayerPosition or decideComPosition の関数内で行われます)。

スポンサーリンク

○× を記入する(writeMark 関数)

続いて、decidePosition 関数によって決定された位置に ○× の記入を行う writeMark 関数を定義していきたいと思います。

といっても、writeMark 関数は実際に紙などに ○× を記入するわけではありません。指定された位置のマスに ○× が記入されたことをプログラム内で管理できるようにするための処理を行う関数となります。

具体的には、各マスに記入された ○× を管理するために2次元配列 board を用意していますので、指定された位置に対応する board の要素に 'o' or 'x' を格納することが writeMark 関数の仕事となります。

その writeMark 関数の定義は下記のようなものになります。

writeMark

void writeMark(int x, int y, TURN turn) {

    if (turn == TURN_PLAYER) {
        board[x][y] = 'o';
    } else {
        board[x][y] = 'x';
    }
}

見ていただければ分かる通り、turnTURN_PLAYER の場合、すなわち現在がプレイヤーが ○× を記入するターンである場合、引数で指定された xy を添字とする board[x][y]'o' を格納し、それ以外の場合、すなわち現在がコンピューターが ○× を記入するターンである場合、board[x][y]'x' を格納するようにしているだけです。

記入者に応じてboardに格納する文字を変更する様子

このように board[x][y]'o' or 'x' を格納しておけば、次回 ○× を記入する位置を決める際に、もし同じ位置 (x, y) が指定されたとしても、board[x][y] には ' ' が格納されていないため ○× を記入する位置を決める(decidePosition 関数) で紹介した decidePosition 関数では ○× を記入する位置の決定のやり直しが行われるようになります。

こんな感じで、writeMark 関数で board を更新してやることで、次回以降の ○× を記入する位置を適切に決定することができるようになりますし、後述で説明する勝負の結果の判断を行う処理も適切に行うことができるようになります。

3 x 3のマスを表示する(printBoard 関数)

続いて 3 x 3 のマスを表示する関数 printBoard を定義していきたいと思います。

より具体的には、3 x 3 のマスの状態、すなわち各マスに記入された印(○ or ×)の表示を行なっていきます。

これは単純に board の各要素を printf 関数等で出力してやれば良いだけなのですが、マルバツゲームらしさを出すためには 3 x 3 のマスとして認識できるように適切に改行を入れてあげた方が良いと思います。また、行番号や列番号を表示をしてやれば、プレイヤーが ○× を記入する位置を指定しやすくなります。

そのため、今回は下の図のように 3 x 3 のマスを表示するようにしていきたいと思います。

boardを3x3のマスのように表示する様子

このような表示は、printBoard 関数を下記のように定義することで実現できます。

printBoard

void printBoard(void) {
    printf(" ");
    for (int x = 0; x < NUM; x++) {
        printf("%2d", x);
    }
    printf("\n");
    for (int y = 0; y < NUM; y++) {
        printf("%d", y);
        for (int x = 0; x < NUM; x++) {
            printf("|%c", board[x][y]);
        }
        printf("|\n");
    }
    printf("\n");
}

勝負の結果を判断する(judgeResult 関数)

次は勝負の結果を判断する judgeResult 関数を定義していきたいと思います。

マルバツゲームを実現する上で一番難しいのは、この勝負の結果を判断する処理になると思います。

ですが、それでも単純な処理で勝負の結果の判断も実現することが可能です。

プレイヤーの勝ち or プレイヤーの負けの判断

マルバツゲームでは横方向・縦方向・斜め方向のいずれかの方向に ○ or × の同一の印が3つ連続して並んだ際に勝敗が決定することになります。

横方向・縦方向・斜め方向とは、3 x 3 のマスの場合は下の図で示す8つの方向となります。

3つ同じ印が並んでいるかどうかをチェックする必要のある8つの方向を示す図

また、勝敗が決定するタイミングは「○ が記入された際に ○ が3つ連続して並んだ時」or「× が記入された際に × が3つ連続して並んだ時」のいずれかとなります。要は、ある印が記入された際に、他方の印がいきなり3つ連続して並ぶことはないということです。

ですので、○ が記入されたのであれば、前述の8方向のいずれかの方向で ○ が3つ並んだかどうかを判断してやり、また × が記入されたのであれば、前述の8方向のいずれかの方向で × が3つ並んだかどうかを判断してやれば良いことになります。

そして、今回はプレイヤーが ○ を、コンピューターが × をそれぞれ記入することになっていますので、○ が3つ並んだ場合はプレイヤーの勝ち、× が3つ並んだ場合はプレイヤーの負けとして勝敗を決定することになります。

引き分けの判断

ただし、結果としては引き分けの場合もあります。マルバツゲームにおける引き分けとは、プレイヤーの勝ちでもプレイヤーの負けでもない状態、すなわち ○× のどちらの印も3つ連続して並んでいない状態で、3 x 3 のマスが全て埋まってしまった状態となります。

引き分けの状態を示す図

この引き分けになったかどうかの判断も必要となります。

judgeResult 関数の定義

ここまで解説してきたような判断は、judgeResult 関数を下記のように定義することで実現できます。

judgeResult

RESULT judgeResult(TURN turn) {

    int count;
    char mark;

    // 記入された印を取得
    if (turn == TURN_PLAYER) {
        mark = 'o';    
    } else {
        mark = 'x';
    }

    for (int y = 0; y < NUM; y++) {

        // 記入された印が横方向に3つ並んでいるかを確認
        count = 0;
        for (int x = 0; x < NUM; x++) {
            if (board[x][y] == mark) {
                count++;
            }
        }
        if (count == TARGET) {
            return turn == TURN_PLAYER ? RESULT_WIN : RESULT_LOSE;
        }

    }

    
    for (int x = 0; x < NUM; x++) {

        // 記入された印が縦方向に3つ並んでいるかを確認
        count = 0;
        for (int y = 0; y < NUM; y++) {
            if (board[x][y] == mark) {
                count++;
            }
        }
        if (count == TARGET) {
            return turn == TURN_PLAYER ? RESULT_WIN : RESULT_LOSE;
        }

    }

    // 記入された印が右下方向に3つ並んでいるかを確認
    count = 0;
    for (int k = 0; k < NUM; k++) {
        if (board[k][k] == mark) {
            count++;
        }
    }
    if (count == TARGET) {
        return turn == TURN_PLAYER ? RESULT_WIN : RESULT_LOSE;
    }

    // 記入された印が左下方向に3つ並んでいるかを確認
    count = 0;
    for (int k = 0; k < NUM; k++) {
        if (board[NUM - 1 - k][NUM - 1 - k] == mark) {
            count++;
        }
    }
    if (count == TARGET) {
        return turn == TURN_PLAYER ? RESULT_WIN : RESULT_LOSE;
    }

    // マスが全て埋まったかどうかを確認
    if (judgeFull()) {
        return RESULT_DRAW;
    }

    // まだ勝敗が決定していない
    return RESULT_NONE;
}

また、judgeResult 関数から呼び出している judgeFull 関数は下記のように定義した関数となります。

judgeFull

bool judgeFull(void) {
    for (int x = 0; x < NUM; x++) {
        for (int y = 0; y < NUM; y++) {
            if (board[x][y] == ' ') {
                return false;
            }
        }
    }
    
    return true;
}

judgeResult 関数の返却値と引数

このページで作成する他の関数に比べて judgeResult 関数はちょっと複雑なので、少し丁寧に関数の解説をしておきたいと思います。

まず、judgeResult 関数は 3 x 3 のマスに記入済みの ○× の状態から勝負の勝敗を判断する関数であり、プレイヤーにおける勝負の勝敗に応じて下記を返却します。

  • RESULT_WIN:勝ち
  • RESULT_LOSE:負け
  • RESULT_DRAW:引き分け
  • RESULT_NONE:勝敗がまだ決定していない

前述の通り、勝敗が決定するタイミングは「○ が記入された際に ○ が3つ連続して並んだ時」or「× が記入された際に × が3つ連続して並んだ時」のいずれかとなります。

そのため、直前に記入された印が ○ or × のどちらであるかどうかを判断できるよう、引数として turn を受け取るようにしています。

そして、turnTURN_PLAYER の場合は直前に記入された印が ○ であると判断し、 3 x 3 のマスの中に3つの ○ が連続して並んでいるかどうかのチェックを行います。逆に turnTURN_COM の場合は直前に記入された印が × であると判断し、3 x 3 のマスの中に3つの × が連続して並んでいるかどうかのチェックを行います。

横方向のチェック

で、そのチェックは、前述の通り横方向・縦方向・斜め方向に対して行う必要があります。

まず、横方向のチェックは下記で行っています。

横方向のチェック

for (int y = 0; y < NUM; y++) {

    // 記入された印が横方向に3つ並んでいるかを確認
    count = 0;
    for (int x = 0; x < NUM; x++) {
        if (board[x][y] == mark) {
            count++;
        }
    }
    if (count == TARGET) {
        return turn == TURN_PLAYER ? RESULT_WIN : RESULT_LOSE;
    }

}

上記では yx に対する2重ループを行っています。

順を追って動作を説明していくと、まず y0 の状態で x に対する for ループが実行され、その中で board[x][y] == mark を満たす回数をカウントしています。mark は、直前に記入された印を記憶している変数になります。

つまり、3 x 3 のマスの図で考えると、y0 の状態で x に対する for ループが実行された際には、下の図の青色のマスに直前に記入された印がいくつあるかをカウントすることになります。

y=0の行に3つ同じ印が並んでいるかどうかをチェックする様子

そのカウント数が 3TARGET) である場合は勝敗が決まったことになり、turnTURN_PLAYER の場合はプレイヤーが ○ を記入することで勝敗が決定したことになるので「プレイヤーの勝ち」、それ以外の場合は、コンピューターが × を記入することで勝敗が決定したことになるので「プレイヤーの負け」ということになります。

そのため、その勝敗結果に応じた返却値を return をして関数を終了します。

カウント数が 3 でない場合は y に対する for ループの最初に戻り、次は y1 の状態で x に対する for ループが実行されることになります。つまり、下の図の青色のマスに対して先ほどと同様のカウントが行われることになり、縦方向の位置のみを移動して横方向のチェックを行うことができます。

y=1の行に3つ同じ印が並んでいるかどうかをチェックする様子

ここでも、カウント数が 3 の場合は先ほどと同様にして関数が終了しますが、カウント数が 3 でない場合は、次は y2 の状態で x に対する for ループが実行され、今度は下の図の青色のマスに対して同様の処理が行われることになります。

y=2の行に3つ同じ印が並んでいるかどうかをチェックする様子

こんな感じで、board[x][y] のように1つ目の添字に x、2つ目の添字に y を指定して2次元配列にアクセスする場合、y に対するループの中で x に対するループを実行することで、横方向に対して ○ や × が連続して3つ並んでいるかどうかのチェックを行うことができます。

縦方向のチェック

逆に、x に対するループの中で y に対するループを実行することで、”縦方向” に対して ○ や × が連続して3つ並んでいるかどうかのチェックを行うことができます。

実際に縦方向に対するチェックを行なっているのが下記部分になります。横方向に対するチェック時とほぼ同じ処理ですが、ループの組み方が異なり、x に対するループの中で y に対するループを実行するようにしています。

縦方向のチェック

for (int x = 0; x < NUM; x++) {

    // 記入された印が縦方向に3つ並んでいるかを確認
    count = 0;
    for (int y = 0; y < NUM; y++) {
        if (board[x][y] == mark) {
            count++;
        }
    }
    if (count == TARGET) {
        return turn == TURN_PLAYER ? RESULT_WIN : RESULT_LOSE;
    }

}

この場合は、下の図の矢印のように左の列から順番に縦方向に ○ や × が連続して3つ並んでいるかどうかの判断が行われていくことになります。

縦方向に3つ同じ印が並んでいるかどうかをチェックする様子

こんな感じで、ループの組み方を変更することで、2次元配列に対してアクセスしていく方向を変化させることができます。

斜め方向のチェック

また、斜め方向のチェックに関しては下記部分で行っています。

斜め方向のチェック

// 記入された印が右下方向に3つ並んでいるかを確認
count = 0;
for (int k = 0; k < NUM; k++) {
    if (board[k][k] == mark) {
        count++;
    }
}
if (count == TARGET) {
    return turn == TURN_PLAYER ? RESULT_WIN : RESULT_LOSE;
}

// 記入された印が左下方向に3つ並んでいるかを確認
count = 0;
for (int k = 0; k < NUM; k++) {
    if (board[NUM - 1 - k][NUM - 1 - k] == mark) {
        count++;
    }
}
if (count == TARGET) {
    return turn == TURN_PLAYER ? RESULT_WIN : RESULT_LOSE;
}

前半部分では右下方向に対して ○ や × が連続して3つ並んでいるかどうかをチェックしています。

この前半部分の処理のように、2次元配列にアクセスする際に指定する添字を “同じ数だけ増やしていく” と右下方向に対してアクセスする要素が変化していくことになります。

右下斜め方向に3つ同じ印が並んでいるかどうかをチェックする様子

それに対し、後半部分では左下方向に対して ○ や × が連続して3つ並んでいるかどうかをチェックしています。

この後半部分の処理のように、2次元配列にアクセスする際に指定する添字を “同じ数だけ減らしていく” と左下方向に対してアクセスする要素が変化していくことになります。

この場合、添字に指定する数を減らしていくので添字が負の値にならないように注意してください。

左下斜め方向に3つ同じ印が並んでいるかどうかをチェックする様子

こんな感じで、2次元配列を扱う際には、どの方向の要素に対してアクセスしていくかを添字の指定の仕方やループの組み方によってうまく制御していく必要があります。

今回紹介した横方向・縦方向・斜め方向に対するアクセスの仕方に関しては、2次元配列利用時のテクニックとして覚えておくと良いと思います!

引き分けの判断

さて、ここまで行った横方向・縦方向・斜め方向に対するチェックにより、プレイヤーの勝ち or プレイヤーの負けが決定している場合は、それに応じた返却値が return されて関数が終了することになります。

したがって、横方向・縦方向・斜め方向に対するチェック時に return が実行されず関数がまだ終了していないということは、「引き分け」or「まだ勝負の勝敗が決定していない」のいずれかの状態であると考えることができます。

「引き分け」or「まだ勝負の勝敗が決定していない」のどちらの状態であるかを判断するため、judgeResult 関数では横方向・縦方向・斜め方向に対するチェックを実行した後に、下記の処理を行うようにしています。

引き分けの判断

// マスが全て埋まったかどうかを確認
if (judgeFull()) {
    return RESULT_DRAW;
}

// まだ勝敗が決定していない
return RESULT_NONE;

前述で示した judgeFull 関数は、board の要素の中に1つでも ' ' が存在する場合に false を、board の要素の中に1つも ' ' が存在しない場合は true を返却する関数となります。

board の要素の中に1つも ' ' が存在しないということは、board の全要素は 'o' or 'x' のいずれかであり、つまりは 3 x 3 のマス全てに ○× が記入された状態であると判断することができます。

つまり、上記の位置から実行される judgeFull 関数が true を返却したということは、プレイヤーの勝ちでもプレイヤーの負けでもない状態でマスが全て埋まったということになるため、この時点で勝負は「引き分け」であると判断することができます。そのため、この場合は RESULT_DRAW を返却して関数を終了しています。

逆に、judgeFull 関数が false を返却したのであれば、プレイヤーの勝ち・プレイヤーの負け・引き分けのいずれの状態でもないことになるため、まだ勝負の勝敗は決まっていないことになります。

そのため、judgeFull 関数が false を返却した場合は、最後の行で RESULT_NONE が返却されるようにしています。

スポンサーリンク

ターンを進める(nextTurn 関数)

関数の定義もいよいよ終盤になってきました!

次はターンを進める nextTurn 関数を定義していきたいと思います。

今回開発するマルバツゲームはプレイヤーとコンピューターでプレイするゲームであり、さらにマルバツゲームにはターンのスキップという概念がありません。

そのため、現在がプレイヤーのターンであれば次はコンピューターのターンに、現在がコンピューターのターンであれば次はプレイヤーのターンに進めてやれば良いだけになります。

ターンを進める様子

したがって、ターンを進める nextTurn 関数は下記のように定義することができます。

nextTurn

TURN nextTurn(TURN now) {
    return now == TURN_PLAYER ? TURN_COM : TURN_PLAYER;
}

上記の nextTurn 関数は、引数で受け取った現在のターン now から次のターンを決定して返却する関数となります。

三項演算子に慣れていない方はちょっと分かりにくいかもしれませんが、要は now == TURN_PLAYER が成立する際には : の前側の値である TURN_COM を、成立しない場合は : の後ろ側の値である TURN_PLAYER を返却しています。

勝負の結果を表示する(printResult 関数)

最後に勝負の結果を表示する printResult 関数を定義していきます。

printResult 関数の定義例は下記のようになります。

printResult

void printResult(RESULT result) {

    if (result == RESULT_WIN) {
        printf("あなたの勝ちです!!!\n");
    } else if (result == RESULT_LOSE) {
        printf("あなたの負けです!!!\n");
    } else if (result == RESULT_DRAW) {
        printf("引き分けです\n");
    }
}

見ていただければ分かる通り、result に応じて出力内容を切り替えているだけです。

以上で、各種関数の定義についての解説は終了となります。

マルバツゲームのサンプルプログラム

最後にマルバツゲームのサンプルプログラムのソースコードを紹介しておきます。

といっても、ここまで紹介してきた各種関数を1つのソースコードにまとめただけです。各関数の詳細な説明に関しては マルバツゲームの作り方 を参照していただければと思います。

スポンサーリンク

ソースコード

マルバツゲームのサンプルのソースコードは下記のようになります。

マルバツゲームのサンプル

#include <stdio.h> // scanf/printf
#include <stdlib.h> // rand/srand
#include <time.h> // time

#define NUM 3 // マス数
#define TARGET NUM // 並べたら勝ちとなる印の数
#define true 1
#define false 0
typedef int bool;

typedef enum {
    RESULT_WIN, // プレイヤーの勝ち
    RESULT_LOSE, // プレイヤーの負け
    RESULT_DRAW, // 引き分け
    RESULT_NONE // 結果が決まっていない
} RESULT;

typedef enum {
    TURN_PLAYER, // プレイヤーのターン
    TURN_COM, // コンピューターのターン
} TURN;

char board[NUM][NUM]; // マスに記入された印を管理する配列

void init(void) {
    srand(time(NULL));

    for (int x = 0; x < NUM; x++) {
        for (int y = 0; y < NUM; y++) {
            board[x][y] = ' ';
        }
    }
}

void decidePlayerPosition(int *x, int *y) {
    printf("どこに印を記入しますか?\n");
    printf("xの入力(0 - %d):", NUM - 1);
    scanf("%d", x);
    printf("yの入力(0 - %d):", NUM - 1);
    scanf("%d", y);
}

void decideComPosition(int *x, int *y) {
    *x = rand() % NUM;
    *y = rand() % NUM;
}

bool isMarkable(int x, int y) {
    if (x < 0 || x > NUM - 1 || y < 0 || y > NUM - 1) {
        printf("入力した値が不正です...\n");
        return false;
    }

    if (board[x][y] != ' ') {
        printf("そのマスはすでに埋まってマス...\n");
        return false;
    }

    return true;
}

void decidePosition(int *x, int *y, TURN turn) {
    while (true) {
        if (turn == TURN_PLAYER) {
            decidePlayerPosition(x, y);
        } else {
            decideComPosition(x, y);
        }

        if (isMarkable(*x, *y)) {
            break;
        }
    }
}

void writeMark(int x, int y, TURN turn) {

    if (turn == TURN_PLAYER) {
        board[x][y] = 'o';
    } else {
        board[x][y] = 'x';
    }
}

void printBoard(void) {
    printf(" ");
    for (int x = 0; x < NUM; x++) {
        printf("%2d", x);
    }
    printf("\n");
    for (int y = 0; y < NUM; y++) {
        printf("%d", y);
        for (int x = 0; x < NUM; x++) {
            printf("|%c", board[x][y]);
        }
        printf("|\n");
    }
    printf("\n");
}

bool judgeFull(void) {
    for (int x = 0; x < NUM; x++) {
        for (int y = 0; y < NUM; y++) {
            if (board[x][y] == ' ') {
                return false;
            }
        }
    }
    
    return true;
}

RESULT judgeResult(TURN turn) {

    int count;
    char mark;

    // 記入された印を取得
    if (turn == TURN_PLAYER) {
        mark = 'o';    
    } else {
        mark = 'x';
    }

    for (int y = 0; y < NUM; y++) {

        // 記入された印が横方向に3つ並んでいるかを確認
        count = 0;
        for (int x = 0; x < NUM; x++) {
            if (board[x][y] == mark) {
                count++;
            }
        }
        if (count == TARGET) {
            return turn == TURN_PLAYER ? RESULT_WIN : RESULT_LOSE;
        }

    }

    
    for (int x = 0; x < NUM; x++) {

        // 記入された印が縦方向に3つ並んでいるかを確認
        count = 0;
        for (int y = 0; y < NUM; y++) {
            if (board[x][y] == mark) {
                count++;
            }
        }
        if (count == TARGET) {
            return turn == TURN_PLAYER ? RESULT_WIN : RESULT_LOSE;
        }

    }

    // 記入された印が右下方向に3つ並んでいるかを確認
    count = 0;
    for (int k = 0; k < NUM; k++) {
        if (board[k][k] == mark) {
            count++;
        }
    }
    if (count == TARGET) {
        return turn == TURN_PLAYER ? RESULT_WIN : RESULT_LOSE;
    }

    // 記入された印が左下方向に3つ並んでいるかを確認
    count = 0;
    for (int k = 0; k < NUM; k++) {
        if (board[NUM - 1 - k][NUM - 1 - k] == mark) {
            count++;
        }
    }
    if (count == TARGET) {
        return turn == TURN_PLAYER ? RESULT_WIN : RESULT_LOSE;
    }

    // マスが全て埋まったかどうかを確認
    if (judgeFull()) {
        return RESULT_DRAW;
    }

    // まだ勝敗が決定していない
    return RESULT_NONE;
}

TURN nextTurn(TURN now) {
    return now == TURN_PLAYER ? TURN_COM : TURN_PLAYER;
}

void printResult(RESULT result) {

    if (result == RESULT_WIN) {
        printf("あなたの勝ちです!!!\n");
    } else if (result == RESULT_LOSE) {
        printf("あなたの負けです!!!\n");
    } else if (result == RESULT_DRAW) {
        printf("引き分けです\n");
    }
}

int main(void) {

    int x, y;
    RESULT result = RESULT_NONE;
    TURN turn = TURN_PLAYER;

    init();
    printBoard();

    do {

        // 1.○×を記入する位置を決める
        decidePosition(&x, &y, turn);

        // 2.○×を記入する
        writeMark(x, y , turn);

        // 3.3x3のマスを表示する
        printBoard();

        // 4.勝負の結果を判断する
        result = judgeResult(turn);
        
        // 5.ターンを進める
        turn = nextTurn(turn);

    // 6.勝負の結果が決まっていない場合は1.に戻る
    } while (result == RESULT_NONE);

    // 7.勝負の結果を表示する
    printResult(result);
}

動作確認

最後にサンプルプログラムの動作確認を行なっておきましょう!

上記ソースコードをコンパイルして実行すれば、次のようなメッセージが表示されます。

  0 1 2
0| | | |
1| | | |
2| | | |

どこに印を記入しますか?
xの入力(0 - 2):

ここで、○ を記入したい位置を入力すれば、その入力した位置に ○ が記入されることになります。位置は横方向と縦方向の2つを入力する必要があるので注意してください。

どこに印を記入しますか?
xの入力(0 - 2):0
yの入力(0 - 2):1
  0 1 2
0| | | |
1|o| | |
2| | | |

○ が記入されれば、次はコンピューターが自動的に位置を決定して × を記入します。記入後は、再度プレイヤーからの位置の入力の受付が行われることになります。

  0 1 2
0|x| | |
1|o| | |
2| | | |

どこに印を記入しますか?
xの入力(0 - 2):

これらを繰り返していけば、いずれプレイヤーの勝ち or プレイヤーの負け or 引き分けが決定し、その結果が表示されてゲームが終了することになります。

どこに印を記入しますか?
xの入力(0 - 2):1
yの入力(0 - 2):1
  0 1 2
0|x|o|x|
1|o|o|x|
2|x|o|o|

あなたの勝ちです!!!

コンピューターの × を記入する位置が完全にランダムに決まるため、正直対戦相手としては弱いのですが、上記のような動作からマルバツゲームが実現できていることは確認していただけると思います。

まとめ

このページでは、C言語での「マルバツゲーム(別名:三目並べ)」の作り方について解説しました!

マルバツゲームは文字の入力・出力だけで実現できるため、特にC言語入門者の方にとっては復習や力だめしをするのに良い題材のゲームだと思います。

また、今回使用した board のように、ボードゲーム等を作成する際には各マスの状態を管理するために2次元配列を利用することが多いです。例えばオセロや囲碁などを実現する際にも2次元配列を利用して各マスの状態を管理することになるので、この2次元配列の扱い方もしっかり理解しておくと良いと思います!

オセロの作り方については下記ページで解説していますので、是非こちらも読んでみてください!

オセロゲームのC言語ソースコード紹介ページのアイキャッチC言語でオセロゲームを作成

さらに、マルバツゲームを拡張することで「五目並べ」も簡単に実現することができます。この「五目並べ」の作り方については下記ページで解説していますので、こちらも参考にしていただければと思います!

C言語での五目並べの作り方の解説ページアイキャッチ【C言語】五目並べゲームの作り方

同じカテゴリのページ一覧を表示

コメントを残す

メールアドレスが公開されることはありません。