このページでは、C言語での「マルバツゲーム(別名:三目並べ)」の作り方について解説していきます。
マルバツゲームとは、2人のプレイヤーに「○」「×」のどちらかの印が割り当てられ、さらに下の図のような3x3のマスの中に2人のプレイヤーが交互に自身の印を記入していくゲームになります。
自身の印(「○」「×」)を3つ並べることができれば、そのプレイヤーの勝ちとなります。3つ並ぶ前に3x3の全てのマスが埋まった場合は引き分けとなります。
皆さんも一度はプレイしたことのあるゲームではないでしょうか?
マルバツゲームは文字の入力・出力だけで実現できるため、特にC言語入門者の方にとっては復習や力だめしをするのに良い題材のゲームだと思います。
このページでは、そんなマルバツゲームのC言語での作り方について解説していきます。
Contents
マルバツゲームの作り方
では、マルバツゲームの作り方を解説していきます。
今回は、プレイヤー対コンピューターの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
:縦方向の位置)。
具体的には、位置 (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 であれば印を記入する位置を決め直すような処理を行います。
このように各マスの状態を考慮しながら処理を行なっていくことで、マルバツゲームを実現することができるようになります。
また、各位置の状態の管理は、マルバツゲームだけでなくオセロや囲碁などのゲームをプログラミングする際にも必要になりますので、こういった状態の管理の仕方は是非覚えておくと良いと思います!
スポンサーリンク
処理の流れを制御する(main
関数)
2次元配列 board
を導入することで、3
x 3
のマスの各位置に記入済みの印を管理できるようになりました。
これによってプログラム内で管理すべきデータの管理方法が決まったことになりますので、ここからはプログラム内で行う「処理」について説明していきたいと思います。
まずは、マルバツゲームの流れを思い出しながら、マルバツゲームを実現する上で必要になる処理を洗い出していきましょう!
これらの処理を関数として切り出し、さらにそれらの関数を main
関数から実行するようにすることで、マルバツゲームの処理の流れを実現していきたいと思います。
まず、マルバツゲームにおいては、プレイヤー or コンピューターが「○× を記入する位置を決める」処理が必要になります。
そして、その決めた位置に対して「○× を記入する」処理が必要になります。
また、現実世界のマルバツゲームではプレイヤーが 3
x 3
のマスを目で確認しながらプレイすることになります。そのため、プログラムでマルバツゲームを実現する際にも、マスに ○× が記入された際に 3
x 3
のマスを表示してプレイヤーが状態を確認できるようにしてあげたほうが便利です。
そのため、「3
x 3
のマスを表示する」処理もあったほうが良いでしょう。
さらに、その ○× の記入を行うことで ○ や × が3つ並ぶ可能性やマスが全て埋まる可能性がありますので、○× の記入後には「勝負の結果を判断する」処理を行う必要があります。
もし勝負の結果が決まっていないのであれば、次は先ほど ○× を記入したのとは他方側の相手に ○× を記入してもらって同様の処理を進めていくことになります。この際には、次の相手に「ターンを進める」処理が必要となります。
勝負の結果が決まったのであれば(プレイヤーの勝ち or プレイヤーの負け or 引き分け)、その時点でゲームは終了となります。この場合は、勝負の結果をプレイヤーに伝えるために「勝負の結果を表示する」処理が必要となります。
これらをまとめると、マルバツゲームのプログラムの処理の流れは下記のようなものになることになります(括弧内には各処理を実行する関数の名前を示しています)。
- ○× を記入する位置を決める(
decidePosition
) - ○× を記入する(
writeMark
) 3
x3
のマスを表示する(printBoard
)- 勝負の結果を判断する(
judgeResult
) - ターンを進める(
nextTurn
) - 勝負の結果が決まっていない場合は 1. に戻る
- 勝負の結果を表示する(
printResult
)
この処理の流れを実現するために、下記のような 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
は「勝負が決まっていない」ことを示す列挙子、result
は judgeResult
という勝負の結果を判断する関数から返却された値を格納する変数であり、これらを比較することでゲームを継続するかどうかを判断しています。
ここまでの説明の中にも出てきた TURN_PLAYER
や RESULT_NONE
は enum
で定義した列挙子であり、この enum
に関しては下記ページで解説していますので、必要に応じてこちらを参考にしていただければと思います。
また、上記では bool
型や true
/ false
の定義を行なっていますが、これらを定義する代わりに stdbool.h
を include
するのでも良いです。
stdbool.h
をインクルードする際の bool
型の使い方は下記ページで解説していますので、必要に応じてこちらを参考にしていただければと思います。
さらに、1方向のマス数は NUM
、並べたら勝ちとなる印の数は TARGET
という定義名を利用してマルバツゲームを実現していきます。マルバツゲームは 3
x 3
のマスを利用し、同じ印を 3
並べたら勝ちとなるため、NUM
と TARGET
は両方とも 3
となります。
さて、上記の main
関数によりマルバツゲームにおけるゲームの大まかな流れを実現できたことになります。
ただし、まだ実現できたのはゲームの大まかな流れだけであり、ゲームに必要な詳細な処理が実現できていません。具体的には、main
関数から呼び出されている関数がまだ定義されていません。
ということで、ここからはその詳細な処理を関数として定義していきたいと思います。
初期化を行う(init
関数)
まずは初期化を行う init
関数を定義していきたいと思います。
マルバツゲームで必要になる初期化処理の1つは2次元配列 board
の初期化です。
各マスに記入された ○× を管理する で解説したように、board
は 3
x 3
の各マスに記入された印を管理する2次元配列です。
当然ですが、ゲーム開始時には 3
x 3
のマスは真っ白、つまり印が1つも記入されていない状態になりますので、これに合わせて 2次元配列 board
の全要素に ' '
を格納しておく必要があります。
また、ゲームでは乱数を利用する機会が多く、このマルバツゲームにおいてもコンピューターが印を記入する位置を決める際に乱数を利用しようと思います。
C言語においては、乱数は rand
関数から生成することができるのですが、プログラム実行時に毎回異なる乱数を生成するようにするためには srand
関数を実行しておく必要があります。
そのため、その srand
関数の実行と前述の board
への ' '
の格納を、マルバツゲームのプログラムにおける初期化処理としたいと思います。
この初期化処理は、下記のように init
関数を定義しておき、この init
関数を main
関数から呼び出すことで実現することができます。
void init(void) {
srand(time(NULL));
for (int x = 0; x < NUM; x++) {
for (int y = 0; y < NUM; y++) {
board[x][y] = ' ';
}
}
}
処理の流れを制御する(main 関数) のソースコードで示したように、NUM
は 3
を示す定数マクロとなります。また board
はグローバル変数として宣言しているため引数としては受け取らないようになっています。
init
関数で使用している srand
関数と、後述で使用する rand
関数については下記ページで解説しているため、詳しく知りたい方はこちらを参照していただければと思います。
○× を記入する位置を決める(decidePosition
関数)
続いて、○× を記入する位置を決める decidePosition
関数を定義していきます。
○× を記入する位置の決め方はプレイヤーの場合とコンピューターの場合とで異なるため、それぞれについて個別に説明していきたいと思います。
プレイヤーの ○× を記入する位置を決める
印を記入するのがプレイヤーの場合、印を記入する位置はプレイヤー自身に決めてもらい、その決めた位置を scanf
等を利用して入力して貰えば良いです。
ただし、マルバツゲームの場合、2次元空間における位置を決める必要があるため、横方向の位置と縦方向の位置の2つの位置を入力してもらう必要があります。
今回は単純に2回 scanf
関数を実行することで、プレイヤーから横方向と縦方向の位置を個別に入力してもらえるようにしたいと思います。
したがって、プレイヤーの ○× を記入する位置を決める関数を 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
関数を利用すれば良いです。
マルバツゲームにおいてマス数は横方向・縦方向共に 3
なので、マスの位置は 0
〜 2
の値で示すことができます。そのため、0
〜 2
の値を2回分(横方向と縦方向)ランダムに決定するようにすれば、印を記入する位置をランダムに決めることができることになります。
そして、0
〜 2
の値をランダムに決定するためには rand() % 3
を実行する必要があります。つまり、rand() % 3
を2回実行することで印を記入する位置をランダムに決定することができることになります。
コンピューターの ○× を記入する位置を決める関数を decideComPosition
とすれば、この関数は上記を踏まえれば次のように定義することができます。
void decideComPosition(int *x, int *y) {
*x = rand() % NUM;
*y = rand() % NUM;
}
NUM
は 3
を示す定数マクロとなります。また、decideComPosition
同様に引数がポインタである点に注意してください。
また、前述の通り、今回は完全にランダムに ○× を記入する位置を決定するようにしていますが、この ○× を記入する位置の決め方を工夫することによってコンピューターを強くすることが可能です。
例えば、プレイヤーの記入する印が2つ連続した際に3つ連続して印が記入されることを阻止するように位置を決定するようにすれば、完全ランダムに位置を決めるよりもコンピューターは強くなると思います。
指定された位置に ○× を記入可能かどうかを判断する
上記のような処理を行うことで、プレイヤー・コンピューターの場合の両方で ○× を記入する位置を決めることができるようになりました。
ただし、その決めた位置に ○× が記入できるとは限りません。
例えば、プレイヤーが scanf
関数に対して 0
〜 2
の値以外の整数を入力した場合は、指定された位置が不正ということになります。
また、その決めた位置に既に ○× が記入されている場合は、その位置に新たに ○× を記入することができないことになります。
こういった場合には、○× を記入する位置の決定をやり直すような制御が必要となります。そして、この制御を行うためには、決定された ○× を記入する位置に実際に ○× が記入可能かどうかを判断する処理が必要となります。
その判断を行う関数 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
関数の定義
上記で定義した decidePlayerPosition
・decideComPosition
・ isMarkable
を利用した場合、○× を記入する位置を決定する 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
は、決定した位置を引数で指定されたアドレス x
と y
に格納する関数となります。
さらに、次に ○× を記入するのがプレイヤー or コンピューターのどちらであるかを判断できるよう、どちらのターンであるかを示す引数として turn
を受け取るようにしています。
そして、turn
に応じて decidePlayerPosition
と decideComPosition
のいずれかを実行して ○× を記入する位置を決定し、その位置に ○× が記入可能であるかを isMarkable
関数で判断しています。
記入可能であれば、break
で while
ループを抜け出して関数を終了し、記入不可の場合はループの最初に戻って再度 decidePlayerPosition
or decideComPosition
を実行するようにしています。
これによって、○× が記入可能な位置がアドレス x
と y
に格納されるまでループが継続するようになっています(アドレス x
と y
への値の格納は decidePlayerPosition
or decideComPosition
の関数内で行われます)。
スポンサーリンク
○× を記入する(writeMark
関数)
続いて、decidePosition
関数によって決定された位置に ○× の記入を行う writeMark
関数を定義していきたいと思います。
といっても、writeMark
関数は実際に紙などに ○× を記入するわけではありません。指定された位置のマスに ○× が記入されたことをプログラム内で管理できるようにするための処理を行う関数となります。
具体的には、各マスに記入された ○× を管理するために2次元配列 board
を用意していますので、指定された位置に対応する board
の要素に 'o'
or 'x'
を格納することが writeMark
関数の仕事となります。
その writeMark
関数の定義は下記のようなものになります。
void writeMark(int x, int y, TURN turn) {
if (turn == TURN_PLAYER) {
board[x][y] = 'o';
} else {
board[x][y] = 'x';
}
}
見ていただければ分かる通り、turn
が TURN_PLAYER
の場合、すなわち現在がプレイヤーが ○× を記入するターンである場合、引数で指定された x
と y
を添字とする board[x][y]
に 'o'
を格納し、それ以外の場合、すなわち現在がコンピューターが ○× を記入するターンである場合、board[x][y]
に 'x'
を格納するようにしているだけです。
このように 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
のマスを表示するようにしていきたいと思います。
このような表示は、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つ連続して並んだ時」or「× が記入された際に × が3つ連続して並んだ時」のいずれかとなります。要は、ある印が記入された際に、他方の印がいきなり3つ連続して並ぶことはないということです。
ですので、○ が記入されたのであれば、前述の8方向のいずれかの方向で ○ が3つ並んだかどうかを判断してやり、また × が記入されたのであれば、前述の8方向のいずれかの方向で × が3つ並んだかどうかを判断してやれば良いことになります。
そして、今回はプレイヤーが ○ を、コンピューターが × をそれぞれ記入することになっていますので、○ が3つ並んだ場合はプレイヤーの勝ち、× が3つ並んだ場合はプレイヤーの負けとして勝敗を決定することになります。
引き分けの判断
ただし、結果としては引き分けの場合もあります。マルバツゲームにおける引き分けとは、プレイヤーの勝ちでもプレイヤーの負けでもない状態、すなわち ○× のどちらの印も3つ連続して並んでいない状態で、3
x 3
のマスが全て埋まってしまった状態となります。
この引き分けになったかどうかの判断も必要となります。
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
関数は下記のように定義した関数となります。
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
を受け取るようにしています。
そして、turn
が TURN_PLAYER
の場合は直前に記入された印が ○ であると判断し、 3
x 3
のマスの中に3つの ○ が連続して並んでいるかどうかのチェックを行います。逆に turn
が TURN_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;
}
}
上記では y
と x
に対する2重ループを行っています。
順を追って動作を説明していくと、まず y
が 0
の状態で x
に対する for
ループが実行され、その中で board[x][y] == mark
を満たす回数をカウントしています。mark
は、直前に記入された印を記憶している変数になります。
つまり、3
x 3
のマスの図で考えると、y
が 0
の状態で x
に対する for
ループが実行された際には、下の図の青色のマスに直前に記入された印がいくつあるかをカウントすることになります。
そのカウント数が 3
(TARGET
) である場合は勝敗が決まったことになり、turn
が TURN_PLAYER
の場合はプレイヤーが ○ を記入することで勝敗が決定したことになるので「プレイヤーの勝ち」、それ以外の場合は、コンピューターが × を記入することで勝敗が決定したことになるので「プレイヤーの負け」ということになります。
そのため、その勝敗結果に応じた返却値を return
をして関数を終了します。
カウント数が 3
でない場合は y
に対する for
ループの最初に戻り、次は y
が 1
の状態で x
に対する for
ループが実行されることになります。つまり、下の図の青色のマスに対して先ほどと同様のカウントが行われることになり、縦方向の位置のみを移動して横方向のチェックを行うことができます。
ここでも、カウント数が 3
の場合は先ほどと同様にして関数が終了しますが、カウント数が 3
でない場合は、次は y
が 2
の状態で x
に対する for
ループが実行され、今度は下の図の青色のマスに対して同様の処理が行われることになります。
こんな感じで、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つ並んでいるかどうかの判断が行われていくことになります。
こんな感じで、ループの組み方を変更することで、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つ並んでいるかどうかをチェックしています。
この後半部分の処理のように、2次元配列にアクセスする際に指定する添字を “同じ数だけ減らしていく” と左下方向に対してアクセスする要素が変化していくことになります。
この場合、添字に指定する数を減らしていくので添字が負の値にならないように注意してください。
こんな感じで、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
関数は下記のように定義することができます。
TURN nextTurn(TURN now) {
return now == TURN_PLAYER ? TURN_COM : TURN_PLAYER;
}
上記の nextTurn
関数は、引数で受け取った現在のターン now
から次のターンを決定して返却する関数となります。
三項演算子に慣れていない方はちょっと分かりにくいかもしれませんが、要は now == TURN_PLAYER
が成立する際には :
の前側の値である TURN_COM
を、成立しない場合は :
の後ろ側の値である TURN_PLAYER
を返却しています。
勝負の結果を表示する(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言語】五目並べゲームの作り方