リバーシ
1.概要
オセロ風ゲームのリバーシを作ってみましょう。
- 盤面を描く(横8縦8に区切ります)
- 中央に白と黒の石を斜めに2つ並べる
- クリックした時、自分の石を表示する
- 相手の石を挟んだら、それらを自分の石の色に変える
- (応用)コンピュータ対戦にする
2.プログラム例
//------------------------------------------------------------// リバーシを作ろう// [2022.9.28版]//------------------------------------------------------------//(解説)// ・マウスで盤面のマスをクリックして、黒石、白石を交互に置いていきます。// ・コンピューター対戦ではありません// ・パスとか、置けないところのチェックとかの処理はしていません。// ・rキーを押すと再ゲームできます。//------------------------------------------------------------// 参考:だえうホームページ C言語でオセロゲームを作成// https://daeudaeu.com/othello/
//* 変数宣言 *//
// 盤面のサイズ// 全体は8行×8列で固定とする。(第0行~第7行、第0列~第7列、左上隅のマスが第0行第0列)// マスの大きさは100×100で固定とする。
// 盤面を表す二次元配列int[][] field = new int[8][8]; // 値が 0:空き 1:黒石 2:白石
// その他の変数boolean game; // game==true → ゲーム中 game==false → ゲーム終了(石を置けるマスがない)int now; // now==1 → 黒の手番 now==2 → 白の手番int win; // win==1 → 黒の勝ち win==2 → 白の勝ち win==0 → 未決着int kuro_n, shiro_n; //現在の黒石・白石の数
//* メインルーチン *//
void setup() { size(800, 900); init(); printData(); //学習&デバッグ用。配列fieldの中身をコンソールに表示}
void draw() { background(255); drawLine(); drawField(); if (game == false) { winCheck(); }}
//* 関数定義 *//
// ゲームの初期化void init() { for (int i = 0; i<8; i++) { for (int j = 0; j<8; j++) { field[i][j] = 0; } } field[3][4] = 1; field[4][3] = 1; field[3][3] = 2; field[4][4] = 2; game = true; now = 1; //リバーシでは、黒が先手。 win = 0;}
// 配列の中身をコンソールに表示。 学習&デバッグ用void printData() { println("----------------------"); for (int i = 0; i<8; i++) { for (int j = 0; j<8; j++) { print(field[i][j], " "); } println(); }}
// 盤面の描画void drawLine() { fill(0, 140, 0); rect(0, 0, 800, 800); fill(0); for (int i = 0; i<9; i++) { line(i*100, 0, i*100, 800); } for (int j = 0; j<9; j++) { line(0, j*100, 800, j*100); }}
// 黒石、白石の描画void drawField() { game = false; //空マスが一つも見つからずに(game=falseのまま)forループを終えたなら、gameは終了 kuro_n = 0; shiro_n = 0; for (int i = 0; i<8; i++) { for (int j = 0; j<8; j++) { if (field[i][j] == 1) { //黒石 fill(0); ellipse(50+(j*100), 50+(i*100), 80, 80); kuro_n++; } else if (field[i][j] == 2) { //白石 fill(255); ellipse(50+(j*100), 50+(i*100), 80, 80); shiro_n++; } else if (field[i][j] == 0) { //空マス 空マスが一つでもあるなら、gameは継続(game=trueに変更) game = true; } } } // 情報表示 textSize(50); fill(0); text("KURO ", 90, 870); text(kuro_n, 280, 870); text("SHIRO ", 490, 870); text(shiro_n, 680, 870); textSize(20); text("NEXT", 375, 825); noFill(); rect(370, 830, 60, 60); if (now == 1) { fill(0); ellipse(399, 859, 50, 50); } else { fill(255); ellipse(400, 860, 50, 50); }}
// ゲーム終了時の勝敗判定void winCheck() { //ゲーム終了(game==false)の場合、実行される if (kuro_n == shiro_n) { win = 0; } else if (kuro_n > shiro_n) { win = 1; } else if (kuro_n < shiro_n) { win = 2; } // 結果表示 textSize(80); fill(255, 255, 255, 200); noStroke(); rect(150, 550, 500, 100); stroke(0); if (win == 1) { fill(0); text("KURO win", 205, 635); } else if (win == 2) { fill(0); text("SHIRO win", 205, 635); } else if (win == 0) { fill(0); text("Draw", 205, 635); }}
// クリックして石を置き、挟んだ相手の石を自分の色にする(後述解説参照)void mousePressed() { //---- step1:マウスクリックしたところに、自分の色の石を置く ----- int j = int(mouseX/100); //x座標からj列を求める int i = int(mouseY/100); //y座標からi行を求める if (field[i][j] == 0) { //そのマスが空なら、石を置ける field[i][j] = now; // now==1 → 黒の手番 now==2 → 白の手番
//---- step2:石を置いたところの周辺の隣接するマスの状況をチェックする ----- for (int di = -1; di < 2; di++) { for (int dj = -1; dj < 2; dj++) {
//---- step3:隣接するマスの状況によって処理をする ----- // 盤面外の場合 if ((i + di < 0) || (i + di >= 8) || (j + dj < 0) || (j + dj >= 8)) { continue; // djループのcontinue } // 空、または自分の色の石の場合 if ((field[i + di][j + dj] == 0) || (field[i + di][j + dj] == now)) { continue; // djループのcontinue }
//---- step4:隣接するマスに相手の石があった場合、その先も次々とチェックしていく ----- for (int s = 2; s < 8; s++) { // 盤面外になったら if ((i + di*s < 0) || (i + di*s >= 8) || (j + dj*s < 0) || (j + dj*s >= 8)) { break; // sのループをbreak } // 空きになったら if (field[i + di*s][j + dj*s] == 0) { break; // sのループをbreak }
// 自分の石になったら if (field[i + di*s][j + dj*s] == now) {
//---- step5:挟んだ相手の石を自分の色にする ----- for (int n = 1; n < s; n++) { field[i + di*n][j + dj*n] = now; } break; // 自分の色にする処理の終了後は、sのループをbreak } } } } now = 3 - now; //手番の交替。nowが1なら2に、2ならば1になる。 } printData(); //学習&デバッグ用。配列fieldの中身をコンソールに表示}
// 再ゲーム処理void keyPressed() { if ((key == 'r')||(key == 'R')) { init(); }}
3.解説
◆ クリックして石を置き、挟んだ相手の石を自分の色にする処理(mousePressed())について
step1:マウスクリックしたところに、自分の色の石を置く
クリックした座標(mouseX,mouseY)から、マス(i行、j列)を計算します。ここで注意しなければならないのは、mouseXは横方向なので、これから横方向のj列を算出します。また、mouseYは縦方向なので、これから縦方向のi行を算出します。
このプログラムでは、クリックした場所のマスが空いていれば、その時の手番の色の石を置けるとしています。本来のオセロのルールでは、必ず相手の石をひっくり返せる位置に置かなければならない(置けるところがない場合はパス)となっていますが、このプログラムでは省略しています。
step2:石を置いたところの周辺の隣接するマスの状況をチェックする
まず、周囲の隣接するマスをチェックします。置いた場所(i行、j列)から8方向を探索します。これは、下記のような二重のfor文(diのループ、djのループ)で実現できます。
for (int di = -1; di < 2; di++) { for (int dj = -1; dj < 2; dj++) { : if (field[ i + di ][ j + dj ] == ....)
ここで field[ i+di ][ j+dj ]
の内容を確認すれば、周囲の隣接するマスの状況をチェックできます。探索する方向は、左上、上、右上、左、真ん中、右、左下、下、右下の順になります。
具体例として、1行1列([1][1]、i=1、j=1)に石を置いた場合、チェックする周辺の隣接マスは下記のようになります。(①~⑨はチェックする順番です)
di | dj | チェックするマス [ i+di ][ j+dj ] | 方向 | 具体例 |
---|---|---|---|---|
-1 | -1 | [ 1+(-1) ] [ 1+(-1) ] | ①左上 | [ 0 ] [ 0 ] |
-1 | 0 | [ 1+(-1) ] [ 1+0 ] | ② 上 | [ 0 ] [ 1 ] |
-1 | 1 | [ 1+(-1)1 ] [ 1+1 ] | ③右上 | [ 0 ] [ 2 ] |
0 | -1 | [ 1+0 ] [ 1+(-1) ] | ④ 左 | [ 1 ] [ 0 ] |
0 | 0 | [ 1+0 ] [ 1+0 ] | ⑤中央 | [ 1] [ 1 ] |
0 | 1 | [ 1+0 ] [ 1+1 ] | ⑥ 右 | [ 1] [ 2 ] |
1 | -1 | [ 1+1 ] [ 1+(-1) ] | ⑦左下 | [ 2] [ 0 ] |
1 | 0 | [ 1+1 ] [ 1+0 ] | ⑧ 下 | [ 2] [ 1 ] |
1 | 1 | [ 1+1 ] [ 1+1 ] | ⑨右下 | [ 2] [ 2 ] |
[ i+di ] [ j+dj ]
step3:隣接するマスの状況によって処理をする
石を置いたところの周囲の隣接するマスをチェックするにあたって、石を盤の端に置いた場合、周囲のマスのいくつかは盤外になる場合もあります。このような場合はfield[i+di][j+dj]を確認しようとするとエラーになってしまうので、事前に避けるようにしなければなりません。この場合はcontinueしてこれ以降の処理をせずに、次の方向(次のdj)へ移ります。
また、隣接するマスが空(field[ i+di ] [ j+dj ] == 0)であった場合、および自分の石(field[ i+di ] [ j+dj ] == now)であった場合、その方向にそれ以上探索を進める必要はありませんので、この場合もcontinueしてこれ以降の処理をせずに、次の方向(次のdj)へ移ります。
step4:隣接するマスに相手の石があった場合、その先も次々とチェックしていく
そのマス([ i+di ] [ j+dj ])が盤外でも空でも自分の色の石でもない場合は、必然的に相手の色の石、ということになります。相手の色の石だった場合、その方向(di,dj)で、次々とチェックしていく必要があります。その先も次々とチェックしていく処理は、下記のようなfor文(sのループ)で実現できます。なお、s=1のマス([ i+di ] [ j+dj ])は隣接するマスであり、既にチェックしたので、s=2からチェックを始めます。また、盤は8×8なので、sの最大値も8より小さくなります。
for (int s = 2; s < 8; s++) { : if (field[ i + di*s ][ j + dj*s ] == ....)
具体例として、5行2列( [5] [2]、i=5、j=2)に石を置いて、右上方向(di = -1、dj = 1)に相手の石があった場合、チェックするマスは下記のようになります。
s | チェックするマス [ i + di*s ][ j + dj*s ] | 具体例 |
---|---|---|
2 | [ 5+(-1)*2 ] [ 2+1*2 ] | [ 3 ] [ 4 ] |
3 | [ 5+(-1)*3 ] [ 2+1*3 ] | [ 2 ] [ 5 ] |
4 | [ 5+(-1)*4 ] [ 2+1*4 ] | [ 1 ] [ 6 ] |
: | : | : |
[ i + di*s ] [ j + dj*s ]
ただし、そこでチェックするマスが
- 盤面外になったら、何もせずにsのループをbreakします。その後は次の方向(次のdj)の確認へ移ります。
- 空になったら、何もせずにsのループをbreakします。その後は次の方向(次のdj)の確認へ移ります。
- 自分の石になったら、それまでの相手の石を自分の色にする処理(step5)をして、それが終わったらsのループをbreakします。その後は次の方向(次のdj)の確認へ移ります。
そして、それ以外(=相手の石)だったら、そのままsのループを継続します。(次のsにします)
step5:挟んだ相手の石を自分の色にする
自分の石になったら、それまでに挟んだ石の色を自分の色に変更します。これは下記のようなfor文(nのループ)で実現できます。
for (int n = 1; n < s; n++) { field[ i + di*n ][ j + dj*n ] = now;
このsは、次々とチェックしていって自分の石になるまでのカウンタでした。これをこのnのループの最終値のために使います。 なお、nowは手番であり、その手番の時の色(1:黒、2:白)なので、これを代入することで挟んだ石を自分の色にする、いわゆる石をひっくり返すことになります。 つまり、これまでsのループで次々とチェックしてきたところを、今度はnのループで出発点にもどりつつ、途中の石を自分の色にしていきます。
◆(参考)手番を切り替える処理について
このプログラムでは、黒の番を1,白の番を2として、1と2を切り替えながら変数nowに代入してその時の手番を表しています。 この切り替えのために、
now = 3 - now; //手番の交替。nowが1なら2に、2ならば1になる。
という計算をしています。
一般的に、aとbを交互に切り替えたいとき、
now = (a+b) - now;
とすれば実現できます。
なお、よく使われる「0と1の切り替え」や「-1と1の切り替え」は、以下のような形のものもよく見られます。
now = 1 - now; // 0と1の切り替えnow = -now; // -1と1の切り替えnow *= -1; // -1と1の切り替え