Javaでシンプルなオセロを作る
オセロを作ったので、ソースコードを淡々と紹介していきます。
対戦画面はこんな感じです。
コンソールに石を置きたい位置を打ち込むことで対戦を進めていきます。
対戦相手となるAIの強さは以下の2種類から選ぶことができます。
・よわい(裏返す個数がなるべく少なくなるように石を置く)
・つよい(裏返す個数がなるべく多くなるように石を置く)
マスの評価点(角が高いなど)を採用しようと思ったのですが、思い出した時点でほとんど完成してしまっていたので、採用は見送ることにしました。また暇ができたらアルゴリズム含めて改良することにしましょう。
では、今回作成したオセロのプログラムを構成する4つのクラスを順に紹介していきます。
Pointクラス
本ブログには何度も登場している Point クラスです。
今回、オセロを実装するにあたって、石を置いた位置の周囲八方向を探索する必要があったので、座標をそれぞれの方向に「ずらす」メソッドとして move を用意しました。
なお、以後紹介するクラスも含めて、テストをろくにしていないためバグが大量に潜んでいる可能性があります。
コピペして使うような方がいればご注意ください。
public class Point { private int y, x; Point (int a, int b) { y = a; x = b; } public int getY () {return y;} public int getX () {return x;} public void move (Point p) { y += p.getY(); x += p.getX(); } }
RevListクラス
center に石を置いたとき、revPoints を裏返すことができる……という情報を保持するクラスです。
今回作成したプログラムでは、毎ターン各色の RevList を全探索し、その中から石を置く位置を選ぶという手順でゲームを進めています。
public class RevList { private Point center; private ArrayList<Point> revPoints = new ArrayList<>(); RevList (Point p) { center = new Point(p.getY(), p.getX()); } public Point getCenterPoint () { return new Point(center.getY(), center.getX()); } public void addRevPoint (ArrayList<Point> list) { if (list.size() > 0) revPoints.addAll(list); } public ArrayList<Point> getRevPoints () {return revPoints;} public int getCenterY() {return center.getY();} public int getCenterX() {return center.getX();} public int getRevNum () {return revPoints.size();} }
Boardクラス
本プログラムのメイン部分となるクラスで、かなり長いです。
findSetPoints というメソッドを毎ターン呼び出して、石を置ける位置を全探索し、2つのリスト(blackList, whiteList)に詰めています。
基本的に車輪のなんとやらなので工夫できる部分もオリジナリティもありませんが、8つの方向を表す定数 D8 の書き方に関しては、個人的になかなか上手く書けたんじゃないかなと思ってます。
直感的でわかりやすいので、競プロのスニペットに登録してあるほうもこの書き方に変えようと思います。
public class Board { private String[][] board = { {" ","a","b","c","d","e","f","g","h"," "}, {"1","・","・","・","・","・","・","・","・"," "}, {"2","・","・","・","・","・","・","・","・"," "}, {"3","・","・","・","・","・","・","・","・"," "}, {"4","・","・","・","○" ,"●" ,"・","・","・"," "}, {"5","・","・","・","●" ,"○" ,"・","・","・"," "}, {"6","・","・","・","・","・","・","・","・"," "}, {"7","・","・","・","・","・","・","・","・"," "}, {"8","・","・","・","・","・","・","・","・"," "}, {" "," "," "," "," "," "," "," "," "," "} }; private Point[] D8 = { new Point(-1, -1), new Point(-1, 0), new Point(-1, 1), new Point( 0, -1), new Point( 0, 1), new Point( 1, -1), new Point( 1, 0), new Point( 1, 1) }; ArrayList<RevList> whiteList = new ArrayList<>(); ArrayList<RevList> blackList = new ArrayList<>(); //相手の石の色を取得するメソッド public String getEnemyStone (String stone) { if (stone.equals("○")) return "●"; else return "○"; } public void findSetPoints (String stone) { if (stone.equals("●")) { blackList.clear(); } else { whiteList.clear(); } for (int i=1; i<=8; i++) { for (int j=1; j<=8; j++) { //石がすでに置いてある場合はcontinue if (board[i][j].equals("・") == false) continue; //まず周囲八方向を見る。相手の石が1個もなければcontinue boolean around = false; for (int k=0; k<8; k++) { if (board[i+D8[k].getY()][j+D8[k].getX()].equals(getEnemyStone(stone))) { around = true; } } if (around == false) continue; //1個以上相手の石があるので、探索を開始する RevList revList = new RevList(new Point(i, j)); for (int k=0; k<8; k++) { Point nowP = revList.getCenterPoint(); ArrayList<Point> tempPoints = new ArrayList<>(); boolean canCapture = false; //八方向に伸ばしていって、相手の石を挟めるかどうか探索する while (true) { nowP.move(D8[k]); if (board[nowP.getY()][nowP.getX()].equals(getEnemyStone(stone)) == true) { tempPoints.add(new Point(nowP.getY(), nowP.getX())); } else if (board[nowP.getY()][nowP.getX()].equals(stone) == true) { if (tempPoints.size() > 0) { canCapture = true; break; } else { canCapture = false; break; } } else { canCapture = false; break; } } //自分の石がなければcontinue if (canCapture == false) continue; revList.addRevPoint(tempPoints); } //1個以上の相手の石を裏返せる場合のみ、リストに追加 if (revList.getRevNum() > 0) { if (stone.equals("●")) { blackList.add(revList); } else { whiteList.add(revList); } } } } } //プレイヤーが選んだ位置がリストに含まれているかを判定するメソッド public boolean containSetPoints (int y, int x) { for (int i=0; i<blackList.size(); i++) { if (blackList.get(i).getCenterY()==y && blackList.get(i).getCenterX()==x) { return true; } } return false; } //石を裏返すメソッド(Mapを使えば外側のループを外せそう) public void revStone (String stone, int y, int x) { if (stone.equals("○")) { for (int i=0; i<whiteList.size(); i++) { if (whiteList.get(i).getCenterY()==y && whiteList.get(i).getCenterX()==x) { ArrayList<Point> list = whiteList.get(i).getRevPoints(); for (int j=0; j<list.size(); j++) { board[list.get(j).getY()][list.get(j).getX()] = stone; } return; } } } else { for (int i=0; i<blackList.size(); i++) { if (blackList.get(i).getCenterY()==y && blackList.get(i).getCenterX()==x) { ArrayList<Point> list = blackList.get(i).getRevPoints(); for (int j=0; j<list.size(); j++) { board[list.get(j).getY()][list.get(j).getX()] = stone; } return; } } } } //石を置くメソッド(上のrevStoneと一緒にしてしまえそう) public void setStone (String stone, Point p) { board[p.getY()][p.getX()] = stone; } public void print () { for (int i=0; i<=9; i++) { for (int j=0; j<=9; j++) { System.out.print(board[i][j]); } System.out.println(); } } public void result () { System.out.println(); System.out.println("ゲームが終了しました。結果は……"); print(); int blackNum = 0, whiteNum = 0; for (int i=1; i<=8; i++) { for (int j=1; j<=8; j++) { if (board[i][j].equals("○")) whiteNum++; else if (board[i][j].equals("●")) blackNum++; } } if (blackNum == whiteNum) { System.out.println(blackNum+"対"+whiteNum+"で引き分けです!"); } else if (blackNum > whiteNum) { System.out.println(blackNum+"対"+whiteNum+"でプレイヤーの勝利です!"); } else { System.out.println(blackNum+"対"+whiteNum+"でAIの勝利です!"); } } }
Gameクラス
以下の4つの処理を順におこないます。
・盤を初期化
・AIを初期化
・対戦(終わるまでループ)
・対戦結果表示
public class Game { public static Scanner sc = new Scanner(System.in); public static void main (String[] args) { //盤の生成 Board board = new Board(); //AIの生成 System.out.println("AIの強さを選択してください 0:よわい 1:つよい"); int AI = -1; while (true) { String temp = sc.next(); if (!temp.equals("0") && !temp.equals("1")) { System.out.println("ただしい強さを入力してください"); } else { AI = Integer.parseInt(temp); System.out.println((AI==0?"よわい":"つよい")+"AIが生成されました"); System.out.println(); break; } } System.out.println("---------------------------------------------------"); //メインループ while (true) { boolean playerCanSet = true; boolean enemyCanSet = true; //プレイヤーの処理---------- board.print(); System.out.println("あなたの番です"); board.findSetPoints("●"); if (board.blackList.size() != 0) { Point set = null; while (true) { String temp = sc.next(); char c0 = temp.charAt(0), c1 = temp.charAt(1); if ((c1<'a'&&'h'<c1)) { System.out.println("入力が正しくありません"); } if (temp.length()!=2 || (c0<'1'||'8'<c0) || (c1<'a'||'h'<c1)){ System.out.println("入力が正しくありません"); } else { int y = temp.charAt(0) - '0'; int x = temp.charAt(1) - 'a' + 1; if (board.containSetPoints(y, x) == true) { set = new Point(y, x); board.revStone("●", y, x); System.out.println("---------------------------------------------------"); System.out.println("あなたは"+temp+"に石を置きました"); break; } else { System.out.println("その場所には石を置くことができません"); } } } board.setStone("●", set); } else { System.out.println("あなたは石を置く場所がありませんでした"); playerCanSet = false; } //AIの処理---------- board.print(); System.out.println("---------------------------------------------------"); board.findSetPoints("○"); if (board.whiteList.size() != 0) { Collections.sort(board.whiteList, Comparator.comparing(RevList::getRevNum)); Point p = null; if (AI == 0) { p = board.whiteList.get(0).getCenterPoint(); } else if (AI == 1) { p = board.whiteList.get(board.whiteList.size()-1).getCenterPoint(); } System.out.println("AIは"+p.getY()+(char)(p.getX()+96)+"に石を置きました"); board.setStone("○", p); board.revStone("○", p.getY(), p.getX()); } else { System.out.println("AIは石を置く場所がありませんでした"); enemyCanSet = false; } //ゲーム終了判定 if (playerCanSet==false && enemyCanSet==false) { break; } } //対戦結果表示 board.result(); } }
ソースコードはここまでになります。
では最後に、今後の課題・改良余地などを記して、記事を終わりたいと思います。
・評価点の導入
・機械学習の導入
・64ビット盤の導入(オセロ盤64マスを64bit整数で管理するらしい、難しそう)
・GUIプログラミング(JavaFX?)