コンテンツにスキップ

三目並べ

1.課題

〇×ゲームと同じです。

基礎

  1. 3行3列の2次元配列を用意してください。内容は、例えば0,1,2の数字のどれかを仮に入れてください
  2. 盤面を描いてください(横3縦3で区切ります)
  3. 先程の2次元配列の内容をもとに、盤面に○、×を描いてください。例えば、配列sanmoku[0][0]の内容が1なら盤面の左上のマスに○、sanmoku[1][1]が2なら盤面の真ん中のマスに×、sanmoku[0][1]が0ならば最上段の中央列のマスは空白となるようにしてください。
  4. 盤面をマウスでクリックして、そのクリックしたマスに対応する配列の内容を1または2にしてください。その後、変更した配列の内容をもとに盤面を再表示してください。
  5. ゲームらしくしてください。最初の盤面はすべて空白で、マウスでマスをクリックするたびに○と×が交互に置かれるようにしてください。
  6. 縦・横・斜めのどれかに同じマークが3つ揃ったら、終了するようにしてください
  7. コンピュータ対戦にしてください

応用

  1. 五目並べを作ってください(盤面は横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:バツwin
int[] 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();
}
}

(備考)

この課題は以前、別のページにありましたが、そこで用意されたサンプルコードは少々複雑なアルゴリズムを使っていて、学習しづらい面がありましたので、改訂版を作成しました。ただし、この複雑なアルゴリズムは、応用の五目並べを作るときには参考になる可能性があるので、下記にそのサンプルプログラムを示します。

SANMOKU_NARABE.zip