#1.はじめに(という名の自分語り)
私が高校生だった頃、学校の囲碁将棋部に少し在籍していたことがあります。囲碁や将棋を以前からやっていた訳ではなく、漫画などに影響されたことによる興味本位でした。そのため実力はまったくと言っていいほどなく、部の最弱の座を欲しいままにしていました。
で、なぜか囲碁将棋部の部室にはオセロ盤もあって、他の部員に将棋でボコボコにされた後は気分転換にオセロもやっていました。オセロは囲碁や将棋に比べるとルールがシンプルであり、それ故の難しさも当然ある訳ですが、将棋で取った駒の使いどころが分からなかったり、囲碁はいつになったら終わりと見なされるのか分からなかったりした私にとっては、実に取っつきやすい種目だったのです。まぁ、それでもやっぱりボコボコにされていましたが。
さて、世間では7行$×$79文字の範囲内でオセロのコーディングをやってのける猛者たちがいるようなのですが、私には10倍の70行はおろか、700行ですら無理なんじゃないかという体たらくですので、今回は文字数制限なしでオセロの実装に挑むことにしました。本当は、どの場所に石を置くのが最適かを判断するアルゴリズムを練って、タイトル通り最強のオセロプログラムを目指していたのですが、薄々予想していた通り、そもそも普通にオセロができる状況にするまでで精一杯でした。そこで、今回は【準備編】として、とりあえずオセロが遊べるようになるまでの経緯を書いていきます。
#2.問題設定(という名の言い訳)
今回は、とにかくオセロができれば良し!ということにします。ただし、今の私にはブラウザ上で動くようなものをつくることはできませんので、毎度お馴染みコンソール上で動くものをつくることにします。ですから、皆さんが実行して遊べます、というような代物では__ないです__がご容赦ください。
#3.実装① オセロ盤の表示
これから実装していくオセロの処理は、基本的にすべてOthelloBoardというクラスの中に書いていきます。OthelloBoardは次の変数および配列をメンバとしてもっています。
class OthelloBoard {
private int size; // オセロ盤の一辺(8, 10, 12, 14, 16)
private char[][] squares; // 各マスの状態(B:黒, W:白, N:石なし)
private char playerColor; // プレイヤーの石の色
private char otherColor; // 相手の石の色
private int turnCounter; // ターン数を数える
private final String alphabets = "abcdefghijklmnop";
}
int型変数sizeは、オセロ盤の一辺に置くことのできる石の数を表しています。後述するコンストラクタでは一般的な8が代入されますが、それをいじくると16マス$×$16マスの__狂気__のサイズで遊ぶこともできます。一応私もやってみましたが、早々に挫折しました。頭おかしなるでホンマ
う~ん、この絶望感。
……話を戻します。char型の2次元配列squaresは、各マスの状態を表す配列です。そのマスに石があるかどうか、あれば黒石なのか白石なのかを参照したり、更新したりします。次のchar型変数playerColor、otherColorには、ゲーム開始時にプレイヤーが黒石か白石のどちらかを選択したとき、'B'または'W'のどちらか一方が入ります。
最後の文字列alphabetsには、オセロ盤を表示するときに使う横方向の座標を表すアルファベットが入っています。$x$個目のアルファベットを参照したいときはthis.alphabets.charAt(x)、逆に、あるアルファベットyが何番目の文字なのか知りたい(横方向の座標を数値として知りたい)ときはthis.alphabets.indexOf("y")などとすれば良いです。また、この文字列は今後変更する予定がないためfinal修飾子をつけています。この辺りは、前回までの記事でいただいたコメントを参考にしました。コメントをくださった方々、誠にありがとうございました。
さて、ここからは、オセロ盤を表示する部分について説明します。
// オセロ盤をコンソール上に表示する
private void printBoard() {
this.printBoardAlphabetLine(); // アルファベット行
this.printBoardOtherLine("┏", "┳", "┓"); // 上端
for (int y = 0; y < this.size - 1; y ++) {
this.printBoardDiscLine(y); // 石を表示する行
this.printBoardOtherLine("┣", "╋", "┫"); // 行間の枠
}
this.printBoardDiscLine(this.size - 1); // 石を表示する行
this.printBoardOtherLine("┗", "┻", "┛"); // 下端
}
// オセロ盤の列を示すアルファベットを表示する
private void printBoardAlphabetLine() {
String buf = " ";
for (int x = 0; x < this.size; x ++) {
buf += " " + this.alphabets.charAt(x);
}
System.out.println(buf);
}
// オセロ盤の石がある行を1行分表示する
private void printBoardDiscLine(int y) {
String buf = String.format("%2d┃", y+1);
for (int x = 0; x < this.size; x ++) {
if (this.squares[y][x] == 'B') {
buf += "●┃";
} else if (this.squares[y][x] == 'W') {
buf += "○┃";
} else {
buf += " ┃";
}
}
System.out.println(buf);
}
// オセロ盤の枠を表す罫線を1行分表示する
private void printBoardOtherLine(String left, String middle, String right) {
String buf = " " + left;
for (int x = 0; x < this.size - 1; x ++) {
buf += "━" + middle;
}
System.out.println(buf + "━" + right);
}
オセロ盤をコンソール上に表示したいときは、最初のprintBoardメソッドを呼び出します。オセロ盤を表示するには、石を表す黒丸や白丸、盤の枠を表す罫線などを表示する必要があるのですが、同じような処理がたくさん出てくるため、パーツが違うだけで処理の流れが同じ部分については別のメソッドprintBoardAlphabetLine、printBoardDiscLine、printBoardOtherLineにまとめておいて、それらを呼び出す形にしています。
ちなみに、上記のメソッドを実行するとこんなのが表示されます(サイズは8マス$×$8マス)。
オセロ盤の上と左に座標を表すアルファベットまたは数字を出力しています。プレイヤーが石を置く場所を入力するときは、たとえば「a 1」のようにアルファベット+半角スペース+数字を入力することとします。
#4.実装② プレイヤーの入力
次に、プレイヤーの入力をどのように処理するかを説明します。先ほど書いたように、石を置く場所は「アルファベット+半角スペース+数字」で入力することになるのですが、その入力が正しいものであるかどうかは必ず確認する必要があります。確認が必要な内容は、とりあえず次の通りとします。
- 入力された座標の場所が実際に存在するかどうか。たとえば「z 15」のようなあり得ない場所や、「syoukou 1234」のように座標が読み取れないものは、入力として受け付けない。
- 入力された座標にマスに他の石がないかどうか。
- 入力された座標のマスに他の石はないが、相手側の石を1つもひっくり返せない状況でないかどうか。
これらを確認する処理を実装したものが次のコードとなります。
// 石を置く場所が決まるまで入力を受け付ける
private void askNewCoordinates(char myColor, char enemyColor) {
while (true) {
// 入力
System.out.println("\n石を置く場所を決めてください。");
System.out.println("[x座標 y座標](例 a 1):");
Scanner sc = new Scanner(System.in);
// オセロ盤の範囲内かどうか判定する
Coordinates newDisc = this.checkCoordinatesRange(sc.nextLine());
if (newDisc.equals(-1, -1)) {
// 座標が正しくない場合、再度入力させる
System.out.println("入力が間違っています。");
continue;
}
if (this.squares[newDisc.y][newDisc.x] != 'N') {
// すでに石が置かれている場合、再度入力させる
System.out.println("すでに石があります。");
continue;
}
// 相手の石をひっくり返せるかどうか判定する
ArrayList<Coordinates> discs = this.checkDiscsTurnedOverAllLine(
myColor, enemyColor, newDisc, this.size*this.size);
if (! discs.isEmpty()) {
// ひっくり返せる石がある場合、実際に石をひっくり返す
this.putDisc(myColor, newDisc);
this.turnOverDiscs(discs);
this.printDiscsTurnedOver(discs);
return;
}
System.out.println("相手の石をひっくり返せません。");
}
}
// プレイヤーの入力した座標がオセロ盤の範囲内かどうか判定する
// 判定に成功すればその座標を、失敗すれば(-1, -1)を返す
private Coordinates checkCoordinatesRange(String line) {
String[] tokens = line.split(" ");
// 1文字目のアルファベットから横の座標を読み取る
int x = this.alphabets.indexOf(tokens[0]);
if (tokens[0].length() != 1 || x < 0 || this.size <= x) {
return new Coordinates(-1, -1);
}
// 残りの文字から縦の座標を読み取る
int y;
try {
y = Integer.parseInt(tokens[1]);
if (y <= 0 || this.size < y) {
return new Coordinates(-1, -1);
}
} catch (NumberFormatException e) {
return new Coordinates(-1, -1);
}
return new Coordinates(x, y - 1);
}
プレイヤー側に置きたい石の場所を入力させたいときは、最初のaskNewCoordinatesメソッドを呼び出します。while文を用いることにより、正しい入力として処理できるまでプレイヤーに何度も何度も入力させ続けるようになっています(って言葉にすると少し怖いですね)。
先ほどの箇条書きの1つ目については、checkCoordinatesRangeメソッドで判定しています。プレイヤーは横方向の座標のアルファベットと縦方向の座標の数字を半角スペースで連結しているため、まずはsplitメソッドで分割します。縦の座標については、parseIntメソッドを用いて文字列を数値に変換しているのですが、実はこのメソッド、__変換に失敗するとNumberFormatExceptionという例外を出してプログラム全体が強制終了してしまう__のです。そこで、そのような例外が発生しても強制終了しないように、最近勉強したばかりの__例外処理__を使います。上記のコードのように、例外が発生するかも知れない部分を__tryブロック__で囲み、tryブロックの後にある__catchブロック__において、例外を捕まえた後の処理を書きます。tryブロックは例外処理が起こりうる場所に網を張るようなイメージで、その網にかかった例外に施す処理がcatchブロックに書かれている、という感じです(間違ってたらコメントしていただけると助かります)。
箇条書きの2つ目については、char型配列squaresを参照することにより、簡単に判定できます。
箇条書きの3つ目ですが、これがやっかいです。相手の石をひっくり返せるかどうか、というのはまさにオセロの根幹とも言える処理です。上記のコードで呼び出しているcheckDiscsTurnedOverAllLineメソッドについて、次節で詳しく説明します。
#5.実装③ 相手の石をひっくり返せるかどうかの判定
初めに、今後の処理で良く出てくるCoordinatesクラスについて説明します。このCoordinatesクラスは単に、オセロ盤のx座標、y座標を数値として格納したクラスです。コードを書く手間を省くため、メンバ変数x, yはpublicにしています。厳密にはprivateにして参照や代入には専用のpublicメソッドを用意すべきでしょうが、今回は楽することを優先しました。すみません。なお、他の座標をコピーするcopyメソッド、与えられた座標と等しいかどうかを判定するequalsメソッドも用意しています。
class Coordinates {
public int x;
public int y;
Coordinates(int x, int y) {
this.x = x;
this.y = y;
}
Coordinates(Coordinates c) {
this.x = c.x;
this.y = c.y;
}
public void copy(Coordinates c) {
this.x = c.x;
this.y = c.y;
}
public boolean equals(int x, int y) {
if (this.x == x && this.y == y) {
return true;
} else {
return false;
}
}
}
さて、本プログラムの最もえげつない部分である、相手の石をひっくり返せるかどうかの判定についてです。まず、__我々が実際にオセロで遊んでいるとき__に相手の石をひっくり返せるかどうかをどのように判定しているか、整理してみましょう。
- まだ石がなく、かつ、まだ注目していないマスに注目する。
- 注目しているマスから右、右上、上、左上、左、左下、下、右下の__8方向__を見る。各方向において、注目しているマスの__1つ隣から相手の石が1個以上続き、その後に自分の石が1個ある__とき、3. に進む。どの方向でもそのような状況でなければ、1. に戻る。
- 8方向すべてについて、注目しているマスと2. で見つけた自分の石にはさまれた相手の石をひっくり返して、自分の石にする。
分かりづらくて申し訳ありませんが、言葉にするとこのような感じです。で、こんな処理を実装してみたのが次のコードになります。
// 入力された座標の石が相手の石をひっくり返せるかどうか判定する
// ひっくり返せる石の座標をArraylistにして返す
// 引数countMaxでひっくり返せる個数の最大値を決められるので、
// その座標に石を置けるかどうかだけの判定なら1で良い
// ひっくり返せる石の座標をすべて返すときはsize*sizeにする
private ArrayList<Coordinates> checkDiscsTurnedOverAllLine(
char myColor, char enemyColor, Coordinates myCoordinates, int countMax)
{
ArrayList<Coordinates> discs = new ArrayList<Coordinates>();
// 各方向をスキャンする
for (int d = 0; d < 8; d ++) {
discs.addAll(this.checkDiscsTurnedOverOneLine(myColor, enemyColor, myCoordinates, d));
// ひっくり返せる石の最大値を超えた場合は、処理を中止する
if (discs.size() > countMax) {
break;
}
}
return discs;
}
// 入力された座標の石が相手の石をひっくり返せるかどうか判定する
// 引数directionによりスキャンする向きが変わる
// 0:0度, 1:45度, 2:90度, 3:135度, 4:180度, 5:225度, 6:270度, 7:315度
private ArrayList<Coordinates> checkDiscsTurnedOverOneLine(
char myColor, char enemyColor, Coordinates myCoordinates, int direction)
{
// ひっくり返せる石をスキャンする
Coordinates currentCoordinates = new Coordinates(myCoordinates);
ArrayList<Coordinates> discs = new ArrayList<Coordinates>();
// 相手の石が続く間、隣をスキャンし続ける
while (true) {
// 隣の石の座標を求める
Coordinates nextDisc = this.getNextDiscCoordinates(currentCoordinates, direction);
if (nextDisc.equals(-1, -1)) {
// ひっくり返せる石がない場合、空のリストを返す
discs.clear();
break;
}
if (this.squares[nextDisc.y][nextDisc.x] == enemyColor) {
// 隣に相手の石があれば、ひっくり返すリストに仮登録する
discs.add(nextDisc);
} else if (this.squares[nextDisc.y][nextDisc.x] == myColor) {
// 隣に自分の石があれば、リストを返す
break;
} else {
// 隣に石がなければ、空のリストを返す
discs.clear();
break;
}
// 隣の石に進む
currentCoordinates.copy(nextDisc);
}
return discs;
}
// 隣(方向により異なる)にある石の座標を返す
// 座標が範囲外であれば(-1, -1)を返す
private Coordinates getNextDiscCoordinates(Coordinates myDisc, int direction) {
// x座標
int x = myDisc.x;
if (direction == 0 || direction == 1 || direction == 7) {
x ++; // 0度, 45度, 315度
} else if (direction == 3 || direction == 4 || direction == 5) {
x --; // 135度, 180度, 225度
}
// y座標
int y = myDisc.y;
if (direction == 1 || direction == 2 || direction == 3) {
y --; // 45度, 90度, 135度
} else if (direction == 5 || direction == 6 || direction == 7) {
y ++; // 225度, 270度, 315度
}
if (x < 0 || this.size <= x || y < 0 || this.size <= y) {
// 座標が範囲外の場合
return new Coordinates(-1, -1);
}
return new Coordinates(x, y);
}
相手の石をひっくり返せるかどうかを調べたいときは、まずcheckDiscsTurnedOverAllLineメソッドを呼び出します。AllLineとあるように8方向すべてを見ていくのですが、良く似た部分の処理をまとめたかったので、各方向については更にcheckDiscsTurnedOverOneLineメソッドを呼び出すことにしました。
このcheckDiscsTurnedOverOneLineメソッドは、OneLineとあるように引数directionで指定された1方向のみの処理を行います。ある方向に対し、隣の石の座標をgetNextDiscCoordinatesメソッド(方向を指定することにより、x座標とy座標が1増えたり、1減ったり、変化しなかったりします)から取得し続けます。while文の中において、注目しているマスの隣に相手の石が連続している場合は、それらをひっくり返せる石のリストdiscsに__仮登録__していきます。もし、ひっくり返せるかも知れない相手の石の後に自分の石がない、またはオセロ盤の範囲から出てしまうような場合は、仮登録したリストをすべて消去します。
最終的に、最初にご紹介したcheckDiscsTurnedOverAllLineメソッドが返すリストに含まれる座標こそが、本当にひっくり返せる石のリスト、ということになります。
#6.実装④ オセロ全体の流れ
最後に、オセロゲームの流れを記述した部分を簡単にご紹介します。
// オセロを開始する
public void start() {
// プレイヤーの石を決める
this.askPlayerColor();
// オセロ盤を開始直後の状態にする
this.initializeBoard();
this.printBoard();
this.turnCounter = 1;
int turnCounterMax = this.size*this.size - 4;
int skipCounter = 0;
// 先手がどちらかを決める
boolean isPlayerTurn = true;
if (this.playerColor == 'W') {
isPlayerTurn = false;
}
// 各ターンの処理
System.out.println("オセロを始めます。");
int playerDiscNum;
int otherDiscNum;
while (this.turnCounter <= turnCounterMax) {
// 現時点での石の数を表示する
playerDiscNum = this.countDisc(this.playerColor);
otherDiscNum = this.countDisc(this.otherColor);
System.out.print("あなた = " + playerDiscNum + " ");
System.out.println("相手 = " + otherDiscNum);
if (isPlayerTurn) {
// プレイヤーのターン
// プレイヤーが石をおけるかどうか判定する
if (! this.checkSquaresForNewDisc(this.playerColor, this.otherColor)) {
// プレイヤーのターンはスキップされる
System.out.println("あなたのターンはスキップされました。");
if (skipCounter == 1) {
// すでに相手のターンもスキップされていた場合、ゲーム終了
break;
}
isPlayerTurn = !isPlayerTurn;
skipCounter ++;
continue;
}
System.out.println("Turn " + turnCounter + ":あなたのターンです。");
skipCounter = 0;
this.askNewCoordinates(this.playerColor, this.otherColor);
} else {
// 相手のターン
// 相手が石をおけるかどうか判定する
if (! this.checkSquaresForNewDisc(this.otherColor, this.playerColor)) {
// プレイヤーのターンはスキップされる
System.out.println("相手のターンはスキップされました。");
if (skipCounter == 1) {
// すでにプレイヤーのターンもスキップされていた場合、ゲーム終了
break;
}
isPlayerTurn = !isPlayerTurn;
skipCounter ++;
continue;
}
// 相手のターン
System.out.println("Turn " + turnCounter + ":相手のターンです。");
skipCounter = 0;
this.askNewCoordinates(this.otherColor, this.playerColor);
}
this.printBoard();
// 次ターンに向けての処理
this.turnCounter ++;
isPlayerTurn = !isPlayerTurn;
}
// 勝敗の判定
playerDiscNum = this.countDisc(this.playerColor);
otherDiscNum = this.countDisc(this.otherColor);
System.out.print("あなた = " + playerDiscNum + " ");
System.out.println("相手 = " + otherDiscNum);
if (playerDiscNum > otherDiscNum) {
System.out.println("あなたの勝ちです。");
} else if (playerDiscNum == otherDiscNum) {
System.out.println("引き分けです。");
} else {
System.out.println("あなたの負けです。");
}
}
余談ですが、上記のstartメソッドのように、1メソッドの中に80行も書いてしまうのは良くありません。同様の処理は別のメソッドにまとめるとか、いくつかの部分を別のメソッドに分けるなどして、読みやすさを意識したコードを書かなければなりません。が、今回は時間が足りませんでした。次回お見せするときには改善しておきますので、どうかご容赦ください。
さて、ゲームの流れを制御するにあたり、1つ大事なことがありまして、それは__自分の石がまったく置けない状況だとパスになり、相手の番になる__ことなのです。さらに付け加えると、__自分と相手のどちらもが石を置けない状況になると、そこでゲーム終了__になることも忘れてはなりません。そこで上記のコードでは、int型変数skipCounterをつくり、自分と相手のどちらかがパスの状況になれば1加算し、石を置ける状況であれば0にリセットする処理を実装しています。そして、自分と相手が連続してパスになる状況(skipCounter == 2)になればゲーム終了となります。
#7.実装まとめ
残りの処理は、ここまでの内容に比べると簡単なものであるため、説明は割愛させていただきます。下記に、今回実装したプログラム全体を記載しておきます。
オセロプログラム(クリックすると開きます)
import java.util.ArrayList;
import java.util.Scanner;
class OthelloBoardTest {
public static void main(String args[]) {
OthelloBoard ob = new OthelloBoard();
ob.start();
}
}
class OthelloBoard {
private int size; // オセロ盤の一辺(8, 10, 12, 14, 16)
private char[][] squares; // 各マスの状態(B:黒, W:白, N:石なし)
private char playerColor; // プレイヤーの石の色
private char otherColor; // 相手の石の色
private int turnCounter; // ターン数を数える
private final String alphabets = "abcdefghijklmnop";
// 横方向の座標を示すアルファベット
// コンストラクタ
public OthelloBoard() {
this.size = 8;
// this.size = askBoardSize();
this.squares = new char[this.size][this.size];
}
// オセロを開始する
public void start() {
// プレイヤーの石を決める
this.askPlayerColor();
// オセロ盤を開始直後の状態にする
this.initializeBoard();
this.printBoard();
this.turnCounter = 1;
int turnCounterMax = this.size*this.size - 4;
int skipCounter = 0;
// 先手がどちらかを決める
boolean isPlayerTurn = true;
if (this.playerColor == 'W') {
isPlayerTurn = false;
}
// 各ターンの処理
System.out.println("オセロを始めます。");
int playerDiscNum;
int otherDiscNum;
while (this.turnCounter <= turnCounterMax) {
// 現時点での石の数を表示する
playerDiscNum = this.countDisc(this.playerColor);
otherDiscNum = this.countDisc(this.otherColor);
System.out.print("あなた = " + playerDiscNum + " ");
System.out.println("相手 = " + otherDiscNum);
if (isPlayerTurn) {
// プレイヤーのターン
// プレイヤーが石をおけるかどうか判定する
if (! this.checkSquaresForNewDisc(this.playerColor, this.otherColor)) {
// プレイヤーのターンはスキップされる
System.out.println("あなたのターンはスキップされました。");
if (skipCounter == 1) {
// すでに相手のターンもスキップされていた場合、ゲーム終了
break;
}
isPlayerTurn = !isPlayerTurn;
skipCounter ++;
continue;
}
System.out.println("Turn " + turnCounter + ":あなたのターンです。");
skipCounter = 0;
this.askNewCoordinates(this.playerColor, this.otherColor);
} else {
// 相手のターン
// 相手が石をおけるかどうか判定する
if (! this.checkSquaresForNewDisc(this.otherColor, this.playerColor)) {
// プレイヤーのターンはスキップされる
System.out.println("相手のターンはスキップされました。");
if (skipCounter == 1) {
// すでにプレイヤーのターンもスキップされていた場合、ゲーム終了
break;
}
isPlayerTurn = !isPlayerTurn;
skipCounter ++;
continue;
}
// 相手のターン
System.out.println("Turn " + turnCounter + ":相手のターンです。");
skipCounter = 0;
this.askNewCoordinates(this.otherColor, this.playerColor);
}
this.printBoard();
// 次ターンに向けての処理
this.turnCounter ++;
isPlayerTurn = !isPlayerTurn;
}
// 勝敗の判定
playerDiscNum = this.countDisc(this.playerColor);
otherDiscNum = this.countDisc(this.otherColor);
System.out.print("あなた = " + playerDiscNum + " ");
System.out.println("相手 = " + otherDiscNum);
if (playerDiscNum > otherDiscNum) {
System.out.println("あなたの勝ちです。");
} else if (playerDiscNum == otherDiscNum) {
System.out.println("引き分けです。");
} else {
System.out.println("あなたの負けです。");
}
}
// 石をひっくり返す
public void turnOverDiscs(ArrayList<Coordinates> discs) {
for (int i = 0; i < discs.size(); i ++) {
int x = discs.get(i).x;
int y = discs.get(i).y;
if (this.squares[y][x] == 'B') {
this.squares[y][x] = 'W';
} else if (this.squares[y][x] == 'W') {
this.squares[y][x] = 'B';
}
}
}
// 石を置ける場所(他の石をひっくり返せる場所)があるかどうか判定する
private boolean checkSquaresForNewDisc(char myColor, char enemyColor) {
for (int y = 0; y < this.size; y ++) {
for (int x = 0; x < this.size; x ++) {
if (this.squares[y][x] != 'N') {
continue;
}
ArrayList<Coordinates> discs = this.checkDiscsTurnedOverAllLine(
myColor, enemyColor, new Coordinates(x, y), 1);
if (discs.size() >= 1) {
return true;
}
}
}
return false;
}
// 石を置く場所が決まるまで入力を受け付ける
private void askNewCoordinates(char myColor, char enemyColor) {
while (true) {
// 入力
System.out.println("\n石を置く場所を決めてください。");
System.out.println("[x座標 y座標](例 a 1):");
Scanner sc = new Scanner(System.in);
// オセロ盤の範囲内かどうか判定する
Coordinates newDisc = this.checkCoordinatesRange(sc.nextLine());
if (newDisc.equals(-1, -1)) {
// 座標が正しくない場合、再度入力させる
System.out.println("入力が間違っています。");
continue;
}
if (this.squares[newDisc.y][newDisc.x] != 'N') {
// すでに石が置かれている場合、再度入力させる
System.out.println("すでに石があります。");
continue;
}
// 相手の石をひっくり返せるかどうか判定する
ArrayList<Coordinates> discs = this.checkDiscsTurnedOverAllLine(
myColor, enemyColor, newDisc, this.size*this.size);
if (! discs.isEmpty()) {
// ひっくり返せる石がある場合、実際に石をひっくり返す
this.putDisc(myColor, newDisc);
this.turnOverDiscs(discs);
this.printDiscsTurnedOver(discs);
return;
}
System.out.println("相手の石をひっくり返せません。");
}
}
// プレイヤーの入力した座標がオセロ盤の範囲内かどうか判定する
// 判定に成功すればその座標を、失敗すれば(-1, -1)を返す
private Coordinates checkCoordinatesRange(String line) {
String[] tokens = line.split(" ");
// 1文字目のアルファベットから横の座標を読み取る
int x = this.alphabets.indexOf(tokens[0]);
if (tokens[0].length() != 1 || x < 0 || this.size <= x) {
return new Coordinates(-1, -1);
}
// 残りの文字から縦の座標を読み取る
int y;
try {
y = Integer.parseInt(tokens[1]);
if (y <= 0 || this.size < y) {
return new Coordinates(-1, -1);
}
} catch (NumberFormatException e) {
return new Coordinates(-1, -1);
}
return new Coordinates(x, y - 1);
}
// 入力された座標の石が相手の石をひっくり返せるかどうか判定する
// ひっくり返せる石の座標をArraylistにして返す
// 引数countMaxでひっくり返せる個数の最大値を決められるので、
// その座標に石を置けるかどうかだけの判定なら1で良い
// ひっくり返せる石の座標をすべて返すときはsize*sizeにする
private ArrayList<Coordinates> checkDiscsTurnedOverAllLine(
char myColor, char enemyColor, Coordinates myCoordinates, int countMax)
{
ArrayList<Coordinates> discs = new ArrayList<Coordinates>();
// 各方向をスキャンする
for (int d = 0; d < 8; d ++) {
discs.addAll(this.checkDiscsTurnedOverOneLine(myColor, enemyColor, myCoordinates, d));
// ひっくり返せる石の最大値を超えた場合は、処理を中止する
if (discs.size() > countMax) {
break;
}
}
return discs;
}
// 入力された座標の石が相手の石をひっくり返せるかどうか判定する
// 引数directionによりスキャンする向きが変わる
// 0:0度, 1:45度, 2:90度, 3:135度, 4:180度, 5:225度, 6:270度, 7:315度
private ArrayList<Coordinates> checkDiscsTurnedOverOneLine(
char myColor, char enemyColor, Coordinates myCoordinates, int direction)
{
// ひっくり返せる石をスキャンする
Coordinates currentCoordinates = new Coordinates(myCoordinates);
ArrayList<Coordinates> discs = new ArrayList<Coordinates>();
// 相手の石が続く間、隣をスキャンし続ける
while (true) {
// 隣の石の座標を求める
Coordinates nextDisc = this.getNextDiscCoordinates(currentCoordinates, direction);
if (nextDisc.equals(-1, -1)) {
// ひっくり返せる石がない場合、空のリストを返す
discs.clear();
break;
}
if (this.squares[nextDisc.y][nextDisc.x] == enemyColor) {
// 隣に相手の石があれば、ひっくり返すリストに仮登録する
discs.add(nextDisc);
} else if (this.squares[nextDisc.y][nextDisc.x] == myColor) {
// 隣に自分の石があれば、リストを返す
break;
} else {
// 隣に石がなければ、空のリストを返す
discs.clear();
break;
}
// 隣の石に進む
currentCoordinates.copy(nextDisc);
}
return discs;
}
// 隣(方向により異なる)にある石の座標を返す
// 座標が範囲外であれば(-1, -1)を返す
private Coordinates getNextDiscCoordinates(Coordinates myDisc, int direction) {
// x座標
int x = myDisc.x;
if (direction == 0 || direction == 1 || direction == 7) {
x ++; // 0度, 45度, 315度
} else if (direction == 3 || direction == 4 || direction == 5) {
x --; // 135度, 180度, 225度
}
// y座標
int y = myDisc.y;
if (direction == 1 || direction == 2 || direction == 3) {
y --; // 45度, 90度, 135度
} else if (direction == 5 || direction == 6 || direction == 7) {
y ++; // 225度, 270度, 315度
}
if (x < 0 || this.size <= x || y < 0 || this.size <= y) {
// 座標が範囲外の場合
return new Coordinates(-1, -1);
}
return new Coordinates(x, y);
}
// オセロ盤のサイズが決まるまで入力を受け付ける
// このメソッドをコンストラクタのthis.sizeの右辺に貼り付けると、
// オセロ盤のサイズを入力する処理を追加できる
private int askBoardSize() {
while (true) {
System.out.println("");
System.out.println("オセロ盤の一辺の長さを決めてください。");
System.out.print("[8, 10, 12, 14, 16 のいずれか]:");
Scanner sc = new Scanner(System.in);
String line = sc.nextLine();
if ("8".equals(line) || "10".equals(line) || "12".equals(line) ||
"14".equals(line) || "16".equals(line)) {
System.out.println("オセロ盤の一辺の長さは" + line + "です。");
return Integer.parseInt(line);
}
System.out.println("入力が間違っています。");
}
}
// プレイヤーの石の色が決まるまで入力を受け付ける
private void askPlayerColor() {
while (true) {
System.out.println("\nあなたの石を決めてください。");
System.out.println("[b (黒), w (白) のいずれか]:");
Scanner sc = new Scanner(System.in);
String line = sc.nextLine();
if ("b".equals(line)) {
System.out.println("あなたの石は黒です。");
this.playerColor = 'B';
this.otherColor = 'W';
return;
} else if ("w".equals(line)) {
System.out.println("あなたの石は白です。");
this.playerColor = 'W';
this.otherColor = 'B';
return;
}
System.out.println("入力が間違っています。");
}
}
// 指定された色の石を数える
private int countDisc(char myColor) {
int count = 0;
for (int y = 0; y < this.size; y ++) {
for (int x = 0; x < this.size; x ++) {
if (this.squares[y][x] == myColor) {
count ++;
}
}
}
return count;
}
// オセロ盤を開始直後の状態にする
private void initializeBoard() {
for (int y = 0; y < this.size; y ++) {
for (int x = 0; x < this.size; x ++) {
squares[y][x] = 'N';
}
}
// 中央4マスだけに石を置く
this.putDisc('B', this.size/2 - 1, this.size/2 - 1);
this.putDisc('B', this.size/2, this.size/2);
this.putDisc('W', this.size/2, this.size/2 - 1);
this.putDisc('W', this.size/2 - 1, this.size/2);
}
// オセロ盤の指定された座標に石を置く
private void putDisc(char discColor, int x, int y) {
this.squares[y][x] = discColor;
}
private void putDisc(char discColor, Coordinates c) {
this.putDisc(discColor, c.x, c.y);
}
// ひっくり返した石の座標をすべて表示する
private void printDiscsTurnedOver(ArrayList<Coordinates> discs) {
System.out.println("次の石をひっくり返しました。");
int count = 0;
for (int i = 0; i < discs.size(); i ++) {
System.out.print(this.alphabets.substring(discs.get(i).x, discs.get(i).x + 1) +
(discs.get(i).y + 1) + " ");
count ++;
if (count == 8) {
System.out.println("");
count = 0;
}
}
System.out.println("");
}
// オセロ盤をコンソール上に表示する
private void printBoard() {
this.printBoardAlphabetLine(); // アルファベット行
this.printBoardOtherLine("┏", "┳", "┓"); // 上端
for (int y = 0; y < this.size - 1; y ++) {
this.printBoardDiscLine(y); // 石を表示する行
this.printBoardOtherLine("┣", "╋", "┫"); // 行間の枠
}
this.printBoardDiscLine(this.size - 1); // 石を表示する行
this.printBoardOtherLine("┗", "┻", "┛"); // 下端
}
// オセロ盤の列を示すアルファベットを表示する
private void printBoardAlphabetLine() {
String buf = " ";
for (int x = 0; x < this.size; x ++) {
buf += " " + this.alphabets.charAt(x);
}
System.out.println(buf);
}
// オセロ盤の石がある行を1行分表示する
private void printBoardDiscLine(int y) {
String buf = String.format("%2d┃", y+1);
for (int x = 0; x < this.size; x ++) {
if (this.squares[y][x] == 'B') {
buf += "●┃";
} else if (this.squares[y][x] == 'W') {
buf += "○┃";
} else {
buf += " ┃";
}
}
System.out.println(buf);
}
// オセロ盤の枠を表す罫線を1行分表示する
private void printBoardOtherLine(String left, String middle, String right) {
String buf = " " + left;
for (int x = 0; x < this.size - 1; x ++) {
buf += "━" + middle;
}
System.out.println(buf + "━" + right);
}
}
class Coordinates {
public int x;
public int y;
Coordinates(int x, int y) {
this.x = x;
this.y = y;
}
Coordinates(Coordinates c) {
this.x = c.x;
this.y = c.y;
}
public void copy(Coordinates c) {
this.x = c.x;
this.y = c.y;
}
public boolean equals(int x, int y) {
if (this.x == x && this.y == y) {
return true;
} else {
return false;
}
}
}
#8.実行してみる
1つ座標を入力する度にオセロ盤が表示されるため、ゲーム全体をだらだらお見せする代わりに1画面だけご紹介します。
最後の最後に自分も相手もパスになったため、ゲームが終了した場面です。何が辛いかって、まだ相手側(PC)の処理を実装していないため、現状では__相手の石まで座標を入力しなければならない__んですよね。この悲しい__自作自演感__。私は日曜日の昼間から何をやっているんだ………
#9.今後の課題
とりあえず、最低限オセロができる状態にはなったので、今後は相手側の思考ルーチンを実装していきます。初めは石を置ける場所からランダムに選ぶ感じになりますが、たとえばゲームの序盤ではあえて石を取りすぎず、中盤ではねちっこく角を狙い、終盤では演算速度に物を言わせて何手も先まで先読みさせるなど、__序盤、中盤、終盤、隙がない__アルゴリズムをつくってみたいと考えています。
今回はソースコードが長いせいもあり、説明が冗長気味になってしまって申し訳ございませんでした。それでもここまで読んでくださった方、誠にありがとうございました。