三目並べ
1.課題
〇×ゲームと同じです。
基礎
- 3行3列の2次元配列を用意してください。内容は、例えば0,1,2の数字のどれかを仮に入れてください
- 盤面を描いてください(横3縦3で区切ります)
- 先程の2次元配列の内容をもとに、盤面に○、×を描いてください。例えば、配列sanmoku[0][0]の内容が1なら盤面の左上のマスに○、sanmoku[1][1]が2なら盤面の真ん中のマスに×、sanmoku[0][1]が0ならば最上段の中央列のマスは空白となるようにしてください。
- 盤面をマウスでクリックして、そのクリックしたマスに対応する配列の内容を1または2にしてください。その後、変更した配列の内容をもとに盤面を再表示してください。
- ゲームらしくしてください。最初の盤面はすべて空白で、マウスでマスをクリックするたびに○と×が交互に置かれるようにしてください。
- 縦・横・斜めのどれかに同じマークが3つ揃ったら、終了するようにしてください
- コンピュータ対戦にしてください
応用
- 五目並べを作ってください(盤面は横19縦19で区切ります)
サンプル例
2.一人操作版の考え方
ここでのポイントは、クリックしたマスに○または×を表示することです。クリックした場所はx、yのグラフィック座標データで、マスは2次元配列の行・列のデータですし、○×表示はグラフィック座標データです。したがって、グラフィック座標から配列の行・列へ、配列の行・列からグラフィック座標へといった変換が必要になります。
また、クリックするごとに○と×の手番を切り替える処理も必要になります。
サンプルプログラム(クリックすると表示されます)
// 三目並べ// 一人操作版
int[][] field;boolean maru;boolean game; // 縦/横そろい、右下がり/左下がりそろい、全マス埋まり、のどれかになったら、falseになるint win; // 0:未決着 1:〇win 2:×win
void setup() { size(600, 700); init();}
void draw() { background(255); dispHairetsu(); //学習&デバッグ用。変数game(ゲーム中か否か)と変数maru(マルの番/バツの番)と変数win(勝者)、および配列fieldの中身をコンソールに表示 drawLine(); drawField(); check();}
void init() { field = new int[3][3]; for (int i = 0; i<3; i++) { for (int j = 0; j<3; j++) { field[i][j] = 0; } } game = true; maru = true; // 先手は〇 (maru = trueの時、〇の番。falseなら×の番。) win = 0;}
void dispHairetsu() { println( "maru =", maru, " game =", game, " win =", win); for (int i = 0; i<3; i++) { for (int j = 0; j<3; j++) { print(field[i][j]); } println(); } println("----------------------------------------");}
void drawLine() { strokeWeight(1); for (int x = 0; x<4; x++) { line(x*200, 0, x*200, 600); } for (int y = 0; y<4; y++) { line(0, y*200, 600, y*200); }}
void drawField() { for (int i = 0; i<3; i++) { for (int j = 0; j<3; j++) { if (field[i][j] == 1) { stroke(255, 0, 0); strokeWeight(5); noFill(); ellipse(100+(j*200), 100+(i*200), 180, 180); //添字jでx座標を、添字iでy座標を算出する所がミソ。 } else if (field[i][j] == 2) { stroke(0, 0, 255); strokeWeight(5); noFill(); line(10+(j*200), 10+(i*200), ((j+1)*200)-10, ((i+1)*200)-10); line(((j+1)*200)-10, 10+(i*200), 10+(j*200), ((i+1)*200)-10); } } }}
void check() { //マルが勝ったらwin=1で、ゲームは終り(game=false)にする。 //バツが勝ったらwin=2で、ゲームは終り(game=false)にする。
//横の判定 if (game) { for (int i = 0; i<3; i++) { //横一列、丸でそろっているかどうか if ((field[i][0] == 1) && (field[i][1]== 1) && (field[i][2] == 1)) { win = 1; game = false; break; } //横一列、バツでそろっているかどうか if ((field[i][0] == 2) && (field[i][1]== 2) && (field[i][2] == 2)) { win = 2; game = false; break; } } }
//縦の判定 if (game) { for (int j = 0; j<3; j++) { //縦一列、丸でそろっているかどうか if ((field[0][j] == 1) && (field[1][j]== 1) && (field[2][j] == 1)) { win = 1; game = false; break; } //縦一列、バツでそろっているかどうか if ((field[0][j] == 2) && (field[1][j]== 2) && (field[2][j] == 2)) { win = 2; game = false; break; } } }
//右肩下がりの判定 if (game) { //右肩下がり一列、丸でそろっているかどうか if ((field[0][0] == 1) && (field[1][1]== 1) && (field[2][2] == 1)) { win = 1; game = false; } //右肩下がり一列、バツでそろっているかどうか if ((field[0][0] == 2) && (field[1][1]== 2) && (field[2][2] == 2)) { win = 2; game = false; } }
//左肩下がりの判定 if (game) { //左肩下がり一列、丸でそろっているかどうか if ((field[0][2] == 1) && (field[1][1]== 1) && (field[2][0] == 1)) { win = 1; game = false; } //左肩下がり一列、バツでそろっているかどうか if ((field[0][2] == 2) && (field[1][1]== 2) && (field[2][0] == 2)) { win = 2; game = false; } }
//マスが全て埋まっているかの判定 if (game) { boolean full = true; //最初、仮に、全て埋まっている(full=true)とする。 for (int i = 0; i<3; i++) { for (int j = 0; j<3; j++) { if (field[i][j] == 0) { full = false; //空きマスが一つでもあれば、埋まっていない(full=false)とする } } } if (full) { //全てが埋まっていた(full=trueのまま)ら、 game=false; //ゲームは終り(game=false)にする。 } }
if (!game) { //game == false の場合、つまり勝負がついたか、または全マス埋まった場合。 fill(0); textSize(80); textAlign(CENTER, CENTER); if (win == 0) { text("Draw", 300, 650); } else if (win == 1) { text("MARU win", 300, 650); } else if (win == 2) { text("BATSU win", 300, 650); } textSize(20); text("Retry : 'r' key", 300, 610); }}
void mousePressed() { if (game) { //左クリックで丸かバツを置く if (mouseButton == LEFT) { if (mouseY < 600) { int j = int(mouseX/200); //x座標で添字jを、y座標で添字iを算出するところがミソ。 int i = int(mouseY/200); if (field[i][j] == 0) { if (maru) { field[i][j] = 1; } else { field[i][j] = 2; } //順番が交互というのは下記の1行で実現。 //つまり、maru=true(マルの番)だったら、この後はバツの番(maru=false)に、 //maru=false(バツの番)だったら、この後はマルの番(maru=true)になる。 maru = !maru; } } } }}
void keyPressed() { if (key == 'r') { init(); }}
3.コンピュータ対戦版の考え方
コンピュータは、空のマスから1つを選んで打ってきます。この選び方を工夫すれば、強いコンピュータを作ることができるでしょう。下記サンプルでは単純に乱数で選んでいますが、皆さんは工夫してみてください。例えば、
- 自分のマークが2つ続いていたら、その先に打つ
- 相手のマークが2つ続いていたら、その先に打つ
- ダブルリーチになるように打つ
- ダブルリーチになるように打っているのをプレーヤーに気付かれないように打つ
などが考えられるでしょう。挑戦してみてください。
サンプルプログラム(クリックすると表示されます)
// 三目並べ// コンピュータ対戦版// <コンピュータは、空マスのひとつをランダムに選んで打つ>
int[][] field;boolean maru; // maru=trueならマルの順番、maru=falseならバツの順番。プレーヤーがマル。コンピュータはバツ。boolean game; // ゲーム中はtrue。縦/横そろい、右下がり/左下がりそろい、全マス埋まり、のどれかになったら、falseになるint win; // 0:未決着 1:マルwin 2:バツwinint[] akiList; // 空マスのマス番号のリスト マス番号:左上が0,そこから右方向(列方向)へ1,2で、2行目左端が3。最後は右下で、8。int akiSuu; // 空マスの数
void setup() { size(600, 700); init();}
void draw() { background(255); dispHairetsu(); //学習&デバッグ用。変数game(ゲーム中か否か)と変数maru(マルの番/バツの番)と変数win(勝者)、および配列fieldの中身をコンソールに表示 drawLine(); compPut(); drawField(); check();}
void init() { field = new int[3][3]; for (int i = 0; i<3; i++) { for (int j = 0; j<3; j++) { field[i][j] = 0; } } game = true; maru = true; // 先手はマル (maru = trueの時、マルの番。falseならバツの番。) win = 0;}
void dispHairetsu() { println("----------------------------------------"); println( "maru =", maru, " game =", game, " win =", win); for (int i = 0; i<3; i++) { for (int j = 0; j<3; j++) { print(field[i][j]); } println(); }}
void drawLine() { strokeWeight(1); for (int x = 0; x<4; x++) { line(x*200, 0, x*200, 600); } for (int y = 0; y<4; y++) { line(0, y*200, 600, y*200); }}
void compPut() { //コンピューターが打つ akiList = new int[8]; akiSuu = 0; int compMasu, comp_i, comp_j;
if (game) { if (maru == false) { if (frameCount % 120 == 0) { //これが無いと、プレーヤとほぼ同時にコンピュータが置いてしまうので。 akiSuu = makeAkiList(); //まず、空マスをピックアップする if (akiSuu > 0) { compMasu = selectMasu(); //空マスのうちから、どれか一つを選ぶ comp_i = int(compMasu/3); //選んだマスの番号から行・列を算出 comp_j = compMasu % 3; println("-----空マスをランダム選択した結果-----"); println("マス番号,comp_i,comp_j = ", compMasu, comp_i, comp_j); field[comp_i][comp_j] = 2; //選んだマスにバツを置く println("\n☆コンピューターの一手:", comp_i, "行", comp_j, "列を", field[comp_i][comp_j], "(バツ)に\n"); maru = !maru; } } } }}
int makeAkiList() { //空マスをピックアップし、空マス番号を配列akiList[]に順次格納する println("-----空マスピックアップ-----"); int n = 0; for (int i = 0; i<3; i++) { for (int j = 0; j<3; j++) { if (field[i][j] == 0) { akiList[n] = (i * 3) + j; println("空あり i,j,空マス番号 = ", i, j, (i*3)+j, " akiList[", n, "]=", akiList[n]); n++; } } } println("----空マスリスト結果----"); println("n=", n); for (int k=0; k<n; k++) { println("akiList[", k, "]=", akiList[k]); } return n;}
int selectMasu() { return akiList[int(random(akiSuu))]; //ここを改良すれば、ゲームに強いコンピュータも可能。}
void drawField() { for (int i = 0; i<3; i++) { for (int j = 0; j<3; j++) { if (field[i][j] == 1) { stroke(255, 0, 0); strokeWeight(5); noFill(); ellipse(100+(j*200), 100+(i*200), 180, 180); //添字jでx座標を、添字iでy座標を算出する所がミソ。 } else if (field[i][j] == 2) { stroke(0, 0, 255); strokeWeight(5); noFill(); line(10+(j*200), 10+(i*200), ((j+1)*200)-10, ((i+1)*200)-10); line(((j+1)*200)-10, 10+(i*200), 10+(j*200), ((i+1)*200)-10); } } }}
void check() { //マルが勝ったらwin=1で、ゲームは終り(game=false)にする。 //バツが勝ったらwin=2で、ゲームは終り(game=false)にする。
//横の判定 if (game) { for (int i = 0; i<3; i++) { //横一列、丸でそろっているかどうか if ((field[i][0] == 1) && (field[i][1]== 1) && (field[i][2] == 1)) { win = 1; game = false; break; } //横一列、バツでそろっているかどうか if ((field[i][0] == 2) && (field[i][1]== 2) && (field[i][2] == 2)) { win = 2; game = false; break; } } }
//縦の判定 if (game) { for (int j = 0; j<3; j++) { //縦一列、丸でそろっているかどうか if ((field[0][j] == 1) && (field[1][j]== 1) && (field[2][j] == 1)) { win = 1; game = false; break; } //縦一列、バツでそろっているかどうか if ((field[0][j] == 2) && (field[1][j]== 2) && (field[2][j] == 2)) { win = 2; game = false; break; } } }
//右肩下がりの判定 if (game) { //右肩下がり一列、丸でそろっているかどうか if ((field[0][0] == 1) && (field[1][1]== 1) && (field[2][2] == 1)) { win = 1; game = false; } //右肩下がり一列、バツでそろっているかどうか if ((field[0][0] == 2) && (field[1][1]== 2) && (field[2][2] == 2)) { win = 2; game = false; } }
//左肩下がりの判定 if (game) { //左肩下がり一列、丸でそろっているかどうか if ((field[0][2] == 1) && (field[1][1]== 1) && (field[2][0] == 1)) { win = 1; game = false; } //左肩下がり一列、バツでそろっているかどうか if ((field[0][2] == 2) && (field[1][1]== 2) && (field[2][0] == 2)) { win = 2; game = false; } }
//マスが全て埋まっているかの判定 if (game) { boolean full = true; //最初、仮に、全て埋まっている(full=true)とする。 for (int i = 0; i<3; i++) { for (int j = 0; j<3; j++) { if (field[i][j] == 0) { full = false; //空きマスが一つでもあれば、埋まっていない(full=false)とする } } } if (full) { //全てが埋まっていた(full=trueのまま)ら、 game=false; //ゲームは終り(game=false)にする。 } }
if (!game) { //game == false の場合、つまり勝負がついたか、または全マス埋まった場合。 fill(0); textSize(80); textAlign(CENTER, CENTER); if (win == 0) { text("Draw", 300, 650); } else if (win == 1) { text("MARU win", 300, 650); } else if (win == 2) { text("BATSU win", 300, 650); } textSize(20); text("Retry : 'r' key", 300, 610); }}
void mousePressed() { if (game) { //左クリックで丸かバツを置く if (mouseButton == LEFT) { if (mouseY < 600) { int j = int(mouseX/200); //x座標で添字jを、y座標で添字iを算出するところがミソ。 int i = int(mouseY/200); if (field[i][j] == 0) { if (maru) { field[i][j] = 1; println("\n★プレーヤーの一手:", i, "行", j, "列を", field[i][j], "(マル)に\n"); maru = !maru; } } } } }}
void keyPressed() { if (key == 'r') { init(); }}
(備考)
この課題は以前、別のページにありましたが、そこで用意されたサンプルコードは少々複雑なアルゴリズムを使っていて、学習しづらい面がありましたので、改訂版を作成しました。ただし、この複雑なアルゴリズムは、応用の五目並べを作るときには参考になる可能性があるので、下記にそのサンプルプログラムを示します。