20190218のJavaに関する記事は10件です。

ぼくのかんがえたさいきょうのおせろ【思考ルーチン編2】

1.はじめに

 前回の記事に書いた通りGitHubに挑戦していたのですが、Google Chromeの翻訳機能を駆使してもいまいち使い方が分からないまま、時間だけが過ぎていく今日この頃でした。そこで、今回からはGitHubと同じような機能をもち、日本語もサポートしてくれているbitbucketを活用していきます。また、これまでのJavaソースはEclipseを用いて作成していたので、Eclipseをbitbucketと連携させながらコーディングしていきます。すまないGitHub。

 で、オセロの石を置く場所を自動で探索する思考ルーチンを実装していこうと思った訳ですが、少しコーディングから離れていた間に、これまで自分がどのような実装をしていたのか、さっぱり分からなくなってしまいました。思考ルーチン編1の記事を投稿したときにコメントをいただいた通り、OthelloBoardクラスの行数がとんでもなく多くなってしまい(460行!)、変数やメソッドの繋がりが極めて複雑になったからでした。そのため、まずはOthelloBoardクラスをいくつかのクラスに分割し、どのクラスがどの変数・メソッドを参照しているのかがもう少し分かりやすくなるように書き直すことから始めていきます。

 しかし、もはや書いた私ですら良く分からなくなってしまっているOthelloBoardクラス。ちょっと別のクラスに変数を移したり、ちょっとコードを書き替えただけで、Eclipseが大量のエラーメッセージを出しやがります。やばい、何個エラーを潰してもちっとも終わる気配がない……!
OthelloBoardクラス「ボーっとしてるなよ、坊主。気張れよ。あとたった何万回ぐらいだ。」

 ……クラス設計の重要性が身に沁みた週末でした。

2.新しいクラス設計

 ということで、OthelloBoardクラス内の変数やメソッドを次の各クラスに分散させてみました。

  • オセロ盤のサイズ(一辺に置くことのできる石の数)やプレイヤー側の石の色など、ゲーム開始時に設定を行うためのConfigクラス。
  • 石の配置状況を管理するBoardクラス。
  • コマンドプロンプト上にオセロ盤や黒石、白石の数を表示するPrintクラス。
  • 指定されたマスに石を置いたとき、相手の石をひっくり返せるかどうか判定するFlipクラス。
  • (主に敵側が)石を置くべきマスを判定する(戦略を練る)ためのStrategyクラス。
  • (主にプレイヤー側が)石を置きたいマスを入力(選択)するためのPlayerクラス。
  • ターンの経過や終了処理については、従来通りOthelloBoardクラスに残しておきます。

 初めから色々書き替えようとすると収拾がつかなくなる(なった)ので、とりあえずこれだけにしておきます。

Configクラス(とDiscStateクラス)

 オセロを開始する際、コマンドプロンプトからオセロ盤のサイズやプレイヤー側の石の色を入力させるクラスです。今後、オセロ開始時に入力させる項目を追加する際は、このクラスに実装していく予定です。将来的には、敵側の思考ルーチンの難易度とかを選ばせるようにしたいです。

Configクラス(クリックすると開きます)
import java.util.Scanner;

public class Config {
    private final int size;             // オセロ盤の一辺(8, 10, 12, 14, 16)
    private final char playerColor;     // プレイヤーの石の色
    private final char otherColor;      // 相手の石の色

    public Config() {
        this.size = this.askBoardSize();
        this.playerColor = this.askPlayerColor();
        if (this.playerColor == DiscState.BLACK) {
            this.otherColor = DiscState.WHITE;
        } else {
            this.otherColor = DiscState.BLACK;
        }
    }

    public int getSize() {
        return this.size;
    }
    public char getPlayerColor() {
        return this.playerColor;
    }
    public char getOtherColor() {
        return this.otherColor;
    }

    // オセロ盤のサイズが決まるまで入力を受け付ける
    private int askBoardSize() {
        while (true) {
            System.out.println("\nオセロ盤の一辺の長さを決めてください。");
            System.out.print("[6, 8, 10, 12, 14, 16 のいずれか]:");
            Scanner sc = new Scanner(System.in);
            String line = sc.nextLine();
            if ("6".equals(line) || "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 char 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("あなたの石は黒です。");
                return DiscState.BLACK;
            } else if ("w".equals(line)) {
                System.out.println("あなたの石は白です。");
                return DiscState.WHITE;
            }
            System.out.println("入力が間違っています。");
        }
    }
}

 なお、以前のコードでは石の色を表す'B', 'W'、石を置いていないことを表す'N'を直接書いていましたが、これらはマジックナンバーと呼ばれよろしくない存在です(万が一、別の文字を割り当てることになったとき、コード全体で書き換える必要が出てくるため、エラーやバグの温床になりやすい)。そこで、石の色や配置の状況を表すDiscStateクラスを用意し、それぞれ文字定数BLACK、WHITE、NONEを割り当てました。

DiscStateクラス(クリックすると開きます)
// オセロ盤の石の色や配置の状態を表すための文字定数を管理する
public final class DiscState {
    public static final char BLACK = 'B';       // 黒石が置かれている
    public static final char WHITE = 'W';       // 白石が置かれている
    public static final char NONE = 'N';        // 石が置かれていない

    private DiscState() {
    }
}

 やっと、staticやfinalの使い所が実感できてきた気がしています。

Boardクラス

 オセロ盤の各マスに石が置かれているか、置かれている場合は黒石か白石かといった状態を管理するクラスです。また、ゲーム開始時におけるオセロ盤の初期化(中央4マスだけに石を置く)や、石の配置状況が更新された際は必ず黒石、白石の個数の更新もします。特に2つ目の理由により、石の配置状況の更新はputDiscメソッドとflipAllDiscsメソッドのみとし、両者のメソッド内で個数の更新を同時に行うよう実装しました。なお、Configクラスからオセロ盤のサイズを受け取る想定です。
 初めにオセロプログラムを作り始めたときは、石の位置を表す座標をint型変数x, yで表していたのですが、途中から座標を表すクラスCoordinatesを自作して利用していました。しかし今回、int型の座標を表すクラスがJavaのライブラリにあること(Pointクラス)を知ったので、今後はそちらを利用していきます。

Boardクラス(クリックすると開きます)
import java.awt.Point;
import java.util.ArrayList;

public class Board {
    private final int size;                 // オセロ盤の一辺(8, 10, 12, 14, 16)
    private char[][] squares;               // 各マスの石の有無、石がある場合は色を表す
    private int blackCounter;               // 黒石の個数
    private int whiteCounter;               // 白石の個数

    public Board(int size) {
        this.size = size;
        this.squares = new char[this.size][this.size];
        this.blackCounter = 0;
        this.whiteCounter = 0;
    }

    public int getSize() {
        return this.size;
    }
    public char[][] getSquares() {
        return this.squares;
    }
    public char getSquareState(Point p) {
        return this.squares[p.y][p.x];
    }
    public int getCounter(char color) {
        if (color == DiscState.BLACK) {
            return this.blackCounter;
        } else if (color == DiscState.WHITE) {
            return this.whiteCounter;
        } else {
            return this.size*this.size - this.blackCounter - this.whiteCounter;
        }
    }

    // オセロ盤をゲーム開始時の状態にする
    public 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(DiscState.BLACK, new Point(this.size/2 - 1, this.size/2 - 1));
        this.putDisc(DiscState.BLACK, new Point(this.size/2, this.size/2));
        this.putDisc(DiscState.WHITE, new Point(this.size/2, this.size/2 - 1));
        this.putDisc(DiscState.WHITE, new Point(this.size/2 - 1, this.size/2));
    }

    // オセロ盤の指定された座標に石を置き、同時に石の個数を更新する
    public void putDisc(char color, Point p) {
        this.squares[p.y][p.x] = color;
        if (color == DiscState.BLACK) {
            this.blackCounter ++;
        } else if (color == DiscState.WHITE) {
            this.whiteCounter ++;
        }
    }

    // オセロ盤の指定された座標の石をひっくり返し、同時に石の個数を更新する
    public void flipAllDiscs(ArrayList<Point> discs) {
        for (Point disc : discs) {
            this.flipDisc(disc);
        }
    }

    // オセロ盤の指定された座標の石をひっくり返し、同時に石の個数を更新する
    private void flipDisc(Point p) {
        if (this.squares[p.y][p.x] == DiscState.BLACK) {
            this.squares[p.y][p.x] = DiscState.WHITE;
            this.blackCounter --;
            this.whiteCounter ++;
        } else if (this.squares[p.y][p.x] == DiscState.WHITE) {
            this.squares[p.y][p.x] = DiscState.BLACK;
            this.blackCounter ++;
            this.whiteCounter --;
        }
    }
}

Printクラス

 コマンドプロンプト上に、罫線を駆使してオセロ盤を表示します。Boardクラスのメンバ変数・配列を受け取ります。

Printクラス(クリックすると開きます)
import java.awt.Point;
import java.util.ArrayList;

public class Print {
    private Board board;                                        // オセロ盤の状態
    private final String alphabets = "abcdefghijklmnop";        // 横方向の座標を示すアルファベット

    public Print(Board board) {
        this.board = board;
    }

    // オセロ盤をコンソール上に表示する
    public void printBoard() {
        this.printBoardAlphabetLine();                          // アルファベット行
        this.printBoardOtherLine("┏", "┳", "┓");                  // 上端
        for (int y = 0; y < this.board.getSize() - 1; y ++) {
            this.printBoardDiscLine(y);                         // 石を表示する行
            this.printBoardOtherLine("┣", "╋", "┫");          // 行間の枠
        }
        this.printBoardDiscLine(this.board.getSize() - 1);      // 石を表示する行
        this.printBoardOtherLine("┗", "┻", "┛");              // 下端
    }

    // プレイヤーと相手の石の数を表示する
    public void printDiscNumber(char playerColor) {
        if (playerColor == DiscState.BLACK) {
            System.out.print("あなた = " + this.board.getCounter(DiscState.BLACK) + "  ");
            System.out.println("相手 = " + this.board.getCounter(DiscState.WHITE));
        } else if (playerColor == DiscState.WHITE) {
            System.out.print("あなた = " + this.board.getCounter(DiscState.WHITE) + "  ");
            System.out.println("相手 = " + this.board.getCounter(DiscState.BLACK));
        }
    }

    // ひっくり返した石の座標をすべて表示する
    public void printAllFlippedDiscs(ArrayList<Point> discs) {
        System.out.println("次の石をひっくり返しました。");
        int count = 0;
        for (Point disc : discs) {
            System.out.print(alphabets.substring(disc.x, disc.x + 1) + (disc.y + 1) + " ");
            count ++;
            if (count == 8) {
                System.out.println("");
                count = 0;
            }
        }
        System.out.println("");
    }

    // オセロ盤の列を示すアルファベットを表示する
    private void printBoardAlphabetLine() {
        String buf = "  ";
        for (int x = 0; x < this.board.getSize(); 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.board.getSize(); x ++) {
            if (this.board.getSquareState(new Point(x, y)) == DiscState.BLACK) {
                buf += "●┃";
            } else if (this.board.getSquareState(new Point(x, y)) == DiscState.WHITE) {
                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.board.getSize() - 1; x ++) {
            buf += "━" + middle;
        }
        System.out.println(buf + "━" + right);
    }
}

Flipクラス

 指定されたマスに石を置いたと仮定し、相手の石をひっくり返せるかどうかを判定します。Boardクラスのメンバ変数・配列と、次のターンでどこにどの色の石を置くかどうかの情報を受け取ります。
 相手の石をひっくり返せるかどうかの判定については、以前のコードから大分軽量化を試みました。特に、縦・横・斜めの8方向にひっくり返せる石を探す部分については、まず各方向に1マスずつ進むためのベクトルとしてDirectionsクラスを用意しました。

Directionsクラス(クリックすると開きます)
import java.awt.Point;
import java.util.ArrayList;

public class Directions {
    public static final ArrayList<Point> directions;

    static {
        directions = new ArrayList<Point>();
        directions.add(new Point(1, 0));        //   0度
        directions.add(new Point(1, 1));        //  45度
        directions.add(new Point(0, 1));        //  90度
        directions.add(new Point(-1, 1));       // 135度
        directions.add(new Point(-1, 0));       // 180度
        directions.add(new Point(-1, -1));      // 225度
        directions.add(new Point(0, -1));       // 270度
        directions.add(new Point(1, -1));       // 315度
    }
}

 Flipクラスでは、指定されたマスに石を置くと相手の石をひっくり返せるかどうかを調べるisAvailableSquareメソッドと、ひっくり返せる石の座標の一覧を取得するgetAllFlippedDiscsメソッドを用意しています。また、Directionsクラスを活用し、同じような処理は新規メソッドに分離したため、前回のものより大分すっきりしたように思います。

Flipクラス(クリックすると開きます)
import java.awt.Point;
import java.util.ArrayList;

public class Flip {
    private Board board;                    // オセロ盤の状態
    private char nextColor;                 // 置こうとしている石の色
    private Point nextMove;                 // 石を置こうとしているマス

    public Flip(Board board, char nextColor, Point nextMove) {
        this.board = board;
        this.nextColor = nextColor;
        this.nextMove = nextMove;
    }

    // 指定されたマスが相手の石をひっくり返せるマスかどうか判定する
    public boolean isAvailableSquare() {
        // すでに石が置かれていないかどうか調べる
        if (!this.isEmptySquare(this.nextMove)) {
            return false;
        }
        // 各方向にひっくり返せる石があるかどうか調べる
        for (Point direction : Directions.directions) {
            if (this.searchFlippedDiscs(this.nextMove, direction).size() > 0) {
                // ひっくり返せる石があった場合
                return true;
            }
        }
        // どの方向にもひっくり返せる石がなかった場合
        return false;
    }

    // ひっくり返される石の座標の一覧を返す
    public ArrayList<Point> getAllFlippedDiscs() {
        ArrayList<Point> allFlippedDiscs = new ArrayList<Point>();
        for (Point direction : Directions.directions) {
            allFlippedDiscs.addAll(this.searchFlippedDiscs(this.nextMove, direction));
        }
        return allFlippedDiscs;
    }

    // 指定されたマスに石がないかどうか判定する
    private boolean isEmptySquare(Point square) {
        if (this.board.getSquareState(square) == DiscState.NONE) {
            return true;
        } else {
            return false;
        }
    }

    // 縦・横・斜めのうち1方向に対し、ひっくり返せる石の座標の一覧を取得する
    // 相手の石が連続する間は一覧に仮登録し続け、その直後に自分の石がきたらその一覧を返す
    // しかし、隣にマスがない(盤の外)または石がない場合は、一覧を全消去して返す(=ひっくり返せない)
    private ArrayList<Point> searchFlippedDiscs(Point square, Point direction) {
        Point currentSquare = new Point(square);
        ArrayList<Point> flippedDiscs = new ArrayList<Point>();

        while(true) {
            // 隣のマスの座標を求める
            Point nextSquare = this.getNextSquare(currentSquare, direction);
            // 隣のマスの状況によりループを抜ける場合
            if (!this.isSquareInRange(nextSquare)) {
                // 隣にマスがない場合
                flippedDiscs.clear();
                break;
            } else if (board.getSquareState(nextSquare) == DiscState.NONE) {
                // 隣のマスに石がない場合
                flippedDiscs.clear();
                break;
            } else if (board.getSquareState(nextSquare) == this.nextColor) {
                // 隣のマスに自分の石がある場合
                break;
            }
            // 隣のマスに相手の石がある場合は、さらに隣のマスに進む
            flippedDiscs.add(nextSquare);
            currentSquare.setLocation(nextSquare);
        }
        return flippedDiscs;
    }

    // 指定された向きに関して隣のマスの座標を求める
    private Point getNextSquare(Point currentSquare, Point direction) {
        Point nextSquare = new Point(currentSquare.x, currentSquare.y);
        nextSquare.translate(direction.x, direction.y);
        return nextSquare;
    }

    // 指定されたマスがオセロ盤の中にあるかどうか調べる
    private boolean isSquareInRange(Point square) {
        if (0 <= square.x && square.x < this.board.getSize() &&
            0 <= square.y && square.y < this.board.getSize()) {
            return true;
        } else {
            return false;
        }
    }
}

 なお、Flipクラスはあくまでひっくり返せるかどうかを判定するだけであって、実際にひっくり返す処理を行うのはBoardクラス内のflipAllDiscsメソッドです。その意味では、Flipクラスは本当はCheckFlipクラスとでも名付けた方が良かったかも知れません。

Strategyクラス

 石を置けるマスがあるかどうか判定し、ある場合はどのマスに石を置くかを選ぶクラスです。Boardクラスのメンバ変数・配列を受け取ります。
 今後検討している処理(主にログ)のため、以前のコードより変更しています。まず、次に石を置くべきマスの候補に対し、何らかの評価値を割り当てたり、何らかの条件を満たすとフラグを立てたりできるよう、Pointクラスを継承させたCandidate(候補)クラスを作成しました。今は、そのマスに石を置くとひっくり返せる石の座標を保持させていますが、たとえば角に配置できるかどうかや、ここまでのターン数に応じて石を少なく取ったり多く取ったりさせるなどの処理を追加する予定です。

Candidateクラス(クリックすると開きます)
import java.awt.Point;
import java.util.ArrayList;

public class Candidate extends Point {
    private ArrayList<Point> allFlippedDiscs;

    public Candidate(Point point) {
        super(point);
    }
    public Candidate(Point point, ArrayList<Point> allFlippedDiscs) {
        super(point);
        this.allFlippedDiscs = allFlippedDiscs;
    }
    public void setAllFlippedDiscs(ArrayList<Point> allFlippedDiscs) {
        this.allFlippedDiscs = allFlippedDiscs;
    }
    public ArrayList<Point> getAllFlippedDiscs() {
        return this.allFlippedDiscs;
    }
}

 続いて、Strategyクラスです。

Strategyクラス(クリックすると開きます)
import java.awt.Point;
import java.util.ArrayList;
import java.util.Random;

public class Strategy {
    private Config config;                      // 初期設定
    private Board board;                        // オセロ盤の状態
    private char nextColor;                     // 置こうとしている石の色
    ArrayList<Candidate> candidates;            // 石を置ける(=相手の石をひっくり返せる)マスの一覧

    public Strategy(Config config, Board board, char nextColor) {
        this.config = config;
        this.board = board;
        this.nextColor = nextColor;
        this.candidates = new ArrayList<Candidate>();
    }

    // 次に石を置けるマスがあるかどうかを判定する
    public boolean hasCandidates() {
        this.searchCandidates();
        if (this.candidates.size() > 0) {
            return true;
        } else {
            return false;
        }
    }


    // 次に石を置くべきマスを1つ選ぶ
    public Candidate getNextMove() {
        return this.getNextMoveRandom();
    }

    // 次に石を置くべきマスをランダムに1つ選ぶ
    private Candidate getNextMoveRandom() {
        return this.candidates.get(new Random().nextInt(this.candidates.size()));
    }

    // 石を置ける(=相手の石をひっくり返せる)マスを探索する
    private void searchCandidates() {
        for (int y = 0; y < this.board.getSize(); y ++) {
            for (int x = 0; x < this.board.getSize(); x ++) {
                Point currentSquare = new Point(x, y);
                Flip flip = new Flip(this.board, this.nextColor, currentSquare);
                if (flip.isAvailableSquare()) {
                    this.candidates.add(new Candidate(currentSquare, flip.getAllFlippedDiscs()));
                }
            }
        }
    }
}

 前回に引き続き、敵側の思考ルーチンは石を置ける場所からランダムに1つ選ぶだけの単純なものです。次回こそ、このクラスをもっともっと充実させていく予定です。

Playerクラス

 次に石を置く場所をプレイヤーに入力させるクラスです。入力された座標のマスに他の石がないかどうか、相手の石をひっくり返せるかどうか、そもそも入力された文字列が座標として正しいかどうかを判定します。なお、クラスのネーミングは安直だったと反省しています。

Playerクラス(クリックすると開きます)
import java.awt.Point;
import java.util.Scanner;

public class Player {
    private Board board;                                    // オセロ盤の状態
    private char nextColor;                             // 置こうとしている石の色

    private Candidate nextMove;                         // 次に石を置くマス
    private Flip flip;                                      // ひっくり返せる石の情報

    private final String alphabets = "abcdefghijklmnop";    // 横方向の座標を示すアルファベット

    public Player(Board board, char nextColor) {
        this.board = board;
        this.nextColor = nextColor;
        this.nextMove = new Candidate(new Point(0, 0));
    }

    // 次に石を置く場所が決まるまで入力を受け付ける
    public Candidate askNextMove() {
        Scanner sc = new Scanner(System.in);
        while (true) {
            // 入力
            System.out.println("\n石を置く場所を決めてください。");
            System.out.print("[x座標 y座標](例 a 1):");
            String line = sc.nextLine();
            // プレイヤーの入力した座標がオセロ盤の範囲内かどうか判定する
            if (!this.checkCoordinatesRange(line)) {
                // 座標が正しくない場合、再度入力させる
                System.out.println("入力が間違っています。");
                continue;
            }
            // 石を置ける(=相手の石をひっくり返せる)マスかどうか判定する
            this.flip = new Flip(this.board, this.nextColor, this.nextMove);
            if (!this.flip.isAvailableSquare()) {
                System.out.println("そのマスに石を置くことはできません。");
                continue;
            }
            this.nextMove.setAllFlippedDiscs(this.flip.getAllFlippedDiscs());
            return this.nextMove;
        }
    }

    // プレイヤーの入力した座標がオセロ盤の範囲内かどうか判定する
    private boolean checkCoordinatesRange(String line) {
        String[] tokens = line.split(" ");
        // 1文字目のアルファベットから横の座標を読み取る
        int x = this.alphabets.indexOf(tokens[0]);
        if (tokens[0].length() != 1 || x < 0 || x >= this.board.getSize()) {
            return false;
        }
        // 残りの文字から縦の座標を読み取る
        int y;
        try {
            y = Integer.parseInt(tokens[1]);
        } catch (NumberFormatException e) {
            return false;
        }
        if (y <= 0 || y > this.board.getSize()) {
            return false;
        }

        this.nextMove.setLocation(x, y - 1);
        return true;
    }
}

OthelloBoardクラス

 オセロのターン経過などを処理するクラスです。他の様々な処理を別クラスとして分離したことにより、大分読みやすくなったと思います。もちろん、まだ手を入れていく必要はありますが……

OthelloBoardクラス(クリックすると開きます)
public class OthelloBoard {
    private Config config;                                      // 初期設定
    private Board board;                                        // オセロ盤の状態
    private int turnCountMax;                                   // ターン数の最大値(1辺*1辺-4)

    private int turnCounter;                                    // 現在ターン数
    private int skipCounter;                                    // 連続スキップ回数(2になればオセロ終了)
    private boolean isPlayerTurn;                               // 現在ターンがプレイヤーの番ならばtrue
    private char nextColor;                                 // 現在のターンがどちらの色の手番か

    // コンストラクタ
    public OthelloBoard() {
        System.out.println("オセロを始めます。");
        // 初期設定
        this.config = new Config();
        this.board = new Board(this.config.getSize());
        this.board.initializeBoard();
        this.turnCountMax = this.config.getSize()*this.config.getSize() - 4;
        Print print = new Print(this.board);
        print.printBoard();
        print.printDiscNumber(this.config.getPlayerColor());

        // 第1ターンのみの処理
        this.turnCounter = 1;
        this.skipCounter = 0;
        this.isPlayerTurn = this.getFirstMove();
        this.nextColor = this.getNextColor();
    }

    // オセロを開始する
    public void start() {
        // 毎ターンの処理
        while (this.turnCounter <= this.turnCountMax) {
            // ターンをスキップするかどうか判定する
            Strategy strategy = new Strategy(this.config, this.board, this.nextColor);
            if (!strategy.hasCandidates()) {
                // 現在のターンを敵側にゆずる
                System.out.println("ターンがスキップされました。");
                this.skipCounter ++;
                if (this.skipCounter == 2) {
                    System.out.println("ターンが連続でスキップされたため、オセロを終了します。");
                    break;
                }
                this.isPlayerTurn = !this.isPlayerTurn;
                this.nextColor = this.getNextColor();
                continue;
            }
            // 以下、ターンをスキップしない場合
            // 次に石を置く場所を決める
            this.skipCounter = 0;
            Candidate nextMove;
            if (this.isPlayerTurn) {
                // プレイヤーのターン
                System.out.println("\nTurn " + this.turnCounter + ":あなたのターンです。");
                Player player = new Player(this.board, this.nextColor);
                nextMove = player.askNextMove();
            } else {
                // 相手のターン
                System.out.println("\nTurn " + this.turnCounter + ":相手のターンです。");
                nextMove = strategy.getNextMove();
            }
            // ひっくり返した後の盤面を表示する
            this.board.putDisc(this.nextColor, nextMove);
            this.board.flipAllDiscs(nextMove.getAllFlippedDiscs());
            Print print = new Print(this.board);
            print.printBoard();
            print.printDiscNumber(this.config.getPlayerColor());
            print.printAllFlippedDiscs(nextMove.getAllFlippedDiscs());
            // 次ターンのための処理
            this.turnCounter ++;
            this.isPlayerTurn = !this.isPlayerTurn;
            if (this.isPlayerTurn) {
                this.nextColor = this.config.getPlayerColor();
            } else {
                this.nextColor = this.config.getOtherColor();
            }
        }
        // 勝敗の判定
        this.printResult();
    }

    // ゲームの勝敗を表示する
    private void printResult() {
        if (this.board.getCounter(DiscState.BLACK) > this.board.getCounter(DiscState.WHITE)) {
            System.out.println("黒石の勝ちです。");
        } else {
            System.out.println("白石の勝ちです。");
        }
    }

    // 現在のターンがどちらの色の手番か判定する
    private char getNextColor() {
        if (this.isPlayerTurn) {
            return this.config.getPlayerColor();
        } else {
            return this.config.getOtherColor();
        }
    }

    // 先手がどちらかを決める
    // プレイヤーが黒石ならプレイヤーが先手、白石なら相手が先手となる
    private boolean getFirstMove() {
        if (this.config.getPlayerColor() == DiscState.BLACK) {
            return true;
        } else {
            return false;
        }
    }
}

OthelloBoardTestクラス

 OthelloBoardクラスを呼び出す、ただそれだけのクラスです。

OthelloBoardTestクラス(クリックすると開きます)
public class OthelloBoardTest {
    public static void main(String args[]) {
        OthelloBoard ob = new OthelloBoard();
        ob.start();
    }
}

3.(ひとまず)完成⇒コミット

 先ほどご説明したソースコードたちを、Bitbucketにコミットしました。⇒MyOthello

 しばらくはBitbucketで色々試してみて、いつかのタイミングでGitHubにもリベンジしてみたいです。なお、eclipseとの連携のやり方はGitHubもほぼ同じみたいです。

 ここまでお読みいただき、ありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javaのシリアル通信ライブラリRXTXの課題と代替調査

対象

Javaでシリアル通信したい方向けです。
私自身は、近年IoTが流行ってしまい、ソフトウェア屋さんなのに!素人なのに!電子工作始めることになり、苦労しています(笑)
本文は、簡単な備忘録気分で書いていますので、不足点や不備があるかと思いますが、よかったら読んでやってください。

環境

調査時点では、Windows10 64bit環境で動作することを第一としています。
開発環境は以下となっています。

  • Eclipse 2019-01
  • maven

RXTX

Javaでシリアル通信をしたいとき、ググると大抵RXTXライブラリがヒットします。
RXTXは、JavaのライブラリとC/C++ネイティブライブラリの2つから構成されており、以下からダウンロードできます。
Windows 32bit, Windows 64bit, Linux, mac等、プラットフォームによってネイティブライブラリを切り替える必要があります。

RXTXの課題

RXTXを使っていて、以下の課題に直面しています。
(他にもあるかも?)

  1. 10年以上メンテナンスされていない
  2. 亜種バージョン(2.2)やネイティブライブラリもいろいろなサイトで少し異なるバージョンが配布されており、管理・統一されていない
  3. ネイティブライブラリの影響で、折角Javaで書いたのにプラットフォーム依存となってしまう

代替ライブラリ

上記の課題があると、シリアル通信しようとすると尻込みしちゃいますよね。。。
「でも、RXTXで困ってる人は私だけではないはず!何とかしようとしている勇者がいるはず!」
ということで、2つほど見繕ってみました。
サンプルを実行してみると、Windows 10 64bitではすんなり動いてくれたので、ご紹介します。

purejavacomm

説明を読むと、JNA(Java Native Access)を利用して、プラットフォーム非依存を確保しつつシリアル通信を実現したそうです。
RXTXで言うところのネイティブライブラリが、JNAに置き換わったという理解です。
RXTXを意識していると書いてある通り、APIは似通っています。

2019-01に1.0.3をリリースしているので、メンテナンスはしてくれている様子です。
(記述時のmavenリポジトリは1.0.2が最新でした)

jSerialComm

こちらもRXTXの代替として作られているようで、APIは類似しています。
ネイティブライブラリはjarに内蔵されており、起動時にプラットフォーム別に利用ライブラリをロードしているようです。
Windows/Linux に加えて、ARMプロセッサにも対応しているとあり、RaspberryPiでも動くようです。
(ソースコードを眺めるとAndroidというキーワードも入っているのでAndroid上でも動作するかも?)

メンテナンスは、2018-12に最新版リリースしています。

評価

実際にサンプルを実行すると、purejavacommもjSerialCommも、mavenリポジトリから利用できました。
ネイティブライブラリを意識せずに動作しました。

他ライブラリとの連携という観点では、両者ともにnettyと連携するライブラリが公開されています。

メンテナンス・サポートといった面では、purejavacommが良さそうですが、netty-transport-purejavacomm - github.comはmavenリポジトリに無いのが惜しいところです。
netty本家サイトではNetty-Transport-jSerialComm - github.com が紹介されていますし、mavenリポジトリを利用できるので使いやすいです。

今後の課題

今後、purejavacomm と jSerialCommを使ったサンプルを投稿したいと思います。

(2019-02-19)投稿しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JAVA コンストラクタの呼び出し処理

どうもおしょうです。
以下、自分のメモ用で記事を書きます。

まずはメイン処理のMain.javaを作成

Main.java
class Main {
    public static void main(String[] args) {
        // インスタンスの生成と変数への代入
        Sample sample = new Sample();

        System.out.println("【サンプル】");
        System.out.println("名前:" + sample.name);
    }
}

次にコンストラクタを呼び出すSample.javaを作成

Sample.java
class Sample {
    // 定数の定義
    public String name;

    // コンストラクタの定義
    Sample() {
        this.name = "おしょう";
    }
}

Main.javaの4行目にある

Sample sample = new Sample();

でSample.javaのコンストラクタ(Sample.javaの6~8行目)を実行します。

その後、Main.javaの6行目にある

System.out.println("名前:" + sample.name);

この処理で、Sample.javaのコンストラクタによってセットされた

name(中身に"おしょう"がセットされている)を処理結果のコンソールに出力します。

2つのjavaファイルをコンパイルして実行した結果は...

【サンプル】
名前:おしょう

となります。

Sample.javaの定数をprivateにして呼び出せたような気がするが...
何かより良い書き方があればご指摘お願いします。

以上、失礼します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

propertiesファイルを編集すると文字化けしたみたいになる

事象

eclipseで編集し、日本語を書いて保存した際
他のエディタで開いたときやGitで差分をチェックしたいとき、以下のようになり読むことができない。
image.png
image.png
変換ツール「¥uXXXX」形式のユニコードエスケープからの逆変換

原因

eclipseで実行するとeclipseが勝手にnative2ascii(ねいてぃぶあすきー)というツールを使い自動で変換を行うため
https://www.javadrive.jp/start/encoding/index3.html
コンテンツ・タイプなどで文字コードの設定などを変えても日本語が文字化けしたようになってしまう。
実際は文字化けではなく文字コードになっていて、System.out.println();すると日本語が出る。
※ サクラエディタで編集すると回避できる。

対処方法

native2asciiを無効にする
※実行時に不具合が出た場合は、チェックを外して実行
image.png

または日本語の編集も可能にするプロパティエディター「PropertiesEditorプラグイン」を利用する
http://proengineer.internous.co.jp/content/columnfeature/9158

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javaのバージョンを上げたら実行できなくなった

プロジェクトを右クリック→「プロパティ」

※ 7→8にあげた
image.png

ライブラリを8のものに設定し直す
image.png

コンパイラー準拠を8に設定し直す
image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SpringWebFluxで使うEntityクラスのメンバをfinalにしたらエラー

もうこれ完全に自分用メモ。Twitterでもよかったレベルなんだけど残すためにこっち。

Spring Data MongoDBで使うEntityクラスをなんとなくで作ったらなんかエラー出た。
なんかSetterよこせとかっていうエラーが出る。
試しにSetterつけたら確かにうまくいく。
でもfindOneして返しただけなのになんで???

エラー
This application has no configured error view, so you are seeing this as a fallback.

Mon Feb 18 06:27:46 JST 2019
There was an unexpected error (type=Internal Server Error, status=500).
No accessor to set property @org.springframework.data.annotation.Id()private final java.lang.Integer com.example.demo.Flower.id!
java.lang.UnsupportedOperationException: No accessor to set property @org.springframework.data.annotation.Id()private final java.lang.Integer com.example.demo.Flower.id!
    at com.example.demo.Flower_Accessor_jjo0e1.setProperty(Unknown Source)
    at org.springframework.data.mapping.model.ConvertingPropertyAccessor.setProperty(ConvertingPropertyAccessor.java:61)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readAndPopulateIdentifier(MappingMongoConverter.java:326)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.populateProperties(MappingMongoConverter.java:289)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:275)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:245)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:194)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:190)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:78)
    at org.springframework.data.mongodb.core.ReactiveMongoTemplate$ReadDocumentCallback.doWith(ReactiveMongoTemplate.java:2910)
    at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:107)
    at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:353)
    at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:76)
    at com.mongodb.reactivestreams.client.internal.ObservableToPublisher$1.onNext(ObservableToPublisher.java:68)
...

ちょっとコード変えて試したりもっかいエラーをよく読んでみると・・・
あー、finalにしちゃうとConvertのところでうまくいかないのね。雰囲気で了解。

setterなくてもfinalだけ外したら動いた。裏でいろいろあるんだねえ。

@Document
public class Flower {
    @Id
    private Integer id;
    private String name;
    private String color;

    Flower(){
        id = null;
        name = "";
        color = "";
    }
    Flower(int id, String name, String color){
        this.id = id;
        this.name = name;
        this.color = color;
    }

    public Integer getId() {
        return this.id;
    }
    public String getName() {
        return this.name;
    }
    public String getColor() {
        return this.color;
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Java】Builderパターンで実装されたクラスを使う関数を実装する

注釈

何か設定してから自分自身を返すような実装全てをBuilderパターンとします。

やりたかったこと

SimpleJdbcInsertを使ってインサートするため、以下のようなコードが量産されていました。

new SimpleJdbcInsert(Jdbcテンプレート)
        .Builderの処理
        .execute(new BeanPropertySqlParameterSource(挿入するオブジェクト));

このコードでは一々SimpleJdbcInsertBeanPropertySqlParameterSourcenewする辺りが非効率で、その他の理由からもプロジェクト全体で効率化への需要があったため、関数で処理をラップすることで効率化を図ろうとしました。

問題になったこと

SimpleJdbcInsertBuilderパターンで実装されているため、関数を実装する初期化済みのインスタンスを渡すぐらいしか実装が思いつきませんでした。
しかし、これでは全く効率化していないに等しいです。

全く効率化していないコード
ラップ関数(new SimpleJdbcInsert(Jdbcテンプレート).Builderの処理, 挿入するオブジェクト);

解決方法

Fluent Builderパターン(ローンパターン)的な書き方をすることで効率的に実装できました。

//ラップ関数
public int insertExecute(UnaryOperator<SimpleJdbcInsert> builder, Object entity) {
    return builder.apply(new SimpleJdbcInsert(Jdbcテンプレート))
            .execute(new BeanPropertySqlParameterSource(entity));
}

//利用例
insertExecute(it -> it.ビルダー処理, 挿入するオブジェクト);

「ビルダーによる生成処理が邪魔ならそれを受け取ってしまえばよい」という発想がとっさに出てこなかったのは勉強不足ですね……。

参考にさせていただいた記事

Javaで書くBuilderパターンのパターン - Qiita

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Springでbeanプロパティがリスト形式のリクエストパラメータをバインドする方法

リスト形式のリクエストパラメータをバインドする方法

以下のコードでbeanプロパティがリスト形式の場合のリクエストパラメータをバインドできます。

<body>
    <form action="/sample/bean" method="post">
        <table>
            <tr>
                <td>入力1:price:</td>
                <td><input type="text" name="sampleBean.childBean[0].price"></td>
                <td>入力1:unit:</td>
                <td><input type="text" name="sampleBean.childBean[0].unit"></td>
            </tr>
            <tr>
                <td>入力2:price:</td>
                <td><input type="text" name="sampleBean.childBean[1].price"></td>
                <td>入力2:unit:</td>
                <td><input type="text" name="sampleBean.childBean[1].unit"></td>
            </tr>
        </table>
    </form>
</body>
@Controller
public class SampleController {

    @RequestMapping(value="/sample/bean", method=RequestMethod.POST)
    public String goUserCreateErrorPage(SampleBean sampleBean) {
        return "sample";
    }
}
public class SampleBean {
  private String childBeanUnit;
  private List<SampleChildBean> childBean;
}
getter,setterは略

public class SampleChildBean {
    private String price;
    private String unit;
}
getter,setterは略

ポイント

今回の場合はinputのname属性の定義の仕方にポイントがあります。
一つずつ解説していくと、
解説1:Controller側の@RequestMappingメソッドの引数で指定しているバインド対象の引数名
解説2:バインド対象引数のクラスで定義されているプロパティ名を指定
    更にリストなので、index番号も指定する。
解説3:最後に解説2で指定しているプロパティのクラスが持つプロパティを指定する(ややこしい?)

<input type="text" name="sampleBean.childBean[0].price">
                            解説1      解説2     解説3

終わりに

一旦、忘れないうちに書いてみたのは良いものの、説明がいまいちなので、
時間あるときにもう少しましな説明を考えよう、、笑

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webアプリでのtry with resources文

経緯

javaのtry with resources文を書こうとして
sqlserverを使うのでmssql-jdbc-7.0.0.jre8.jarをインポートした。

この時以下のコードがエラーに

dbConnect.java
try(Connection connection = DriverManager.getConnection(url)) {
}

と書くと「No suitable driver found for jdbc:sqlserver://〜」とエラーが出ました。

エラーを調べたところ、Class.forNameでドライバを指定するようにと出たので、
以下のコードに修正。

dbConnect.java
try{
//JDBCドライバを指定(JDBC3.0以下)
Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
Connection connection = DriverManager.getConnection(url); 
}

修正してエラーを無くしたのですが、これではtry with resources文が使用できていないので、どうにか最初のコードで動かす方法を探した。

解決策

原因はtomcatのクラスパスにjdbcが指定されていない為、Class.forNameなしではjdbcまでのパスがわからないことが原因だった。

eclipseを使用していたので、「tomcat>実行の構成>クラスパス>外部JARの選択」を選択し、libの下にインポートしたjdbcのjarにパスを通すことで正常終了した。

その後

Azureが好きなので、AppService上にデプロイしたが、tomcatのクラスパスに追加する方法がわからないので、分かったら追記する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ApacheのCommons CLIでコマンドラインオプション解析をする。

Apache Commons CLIとは

コマンドラインオプションを解析するAPIです。

例えば、hoge.jarを実行する際に3つのオプションがあるとします。
java -jar /usr/local/hoge.jar user age mail

で、このうち
・userはオプションを2つ取りたい
・age(年齢)は必須にしたい
・オプションが間違っている場合は指定するオプション情報(ヘルプ)を返したい

という要望に答えてくれるAPIがApache Commons CLIです。

導入してみる

次の環境にて導入してみました。
・java 1.8
・maven構成

pom.xml
    <dependency>
        <groupId>commons-cli</groupId>
        <artifactId>commons-cli</artifactId>
        <version>1.4</version>
    </dependency>
main.java
        // コマンドラインオプションの設定
        Options options = new Options();

        // 設定方法1
        // 引数名(-t), 引数を取得するか否か, 説明
        options.addOption("m", true, "メールアドレス");

        // 設定方法2
        // 引数毎にオプションを設定する

        Options.addOption(Option.builder("u")     // オプションの名前
        .argName("serviceid"))                    // 引数名
        .hasArg(2)                                // 引数を2つとる。
        .desc("ユーザー")                          // 説明
        .build());                                // インスタンスを生成

        Options.addOption(Option.builder("a")
        .argName("age"))
        .required()                               // 必須
        .hasArg()
        .desc("年齢")
        .build());

        // オプションのヘルプ情報を表示する。
        HelpFormatter hf = new HelpFormatter();
        hf.printHelp("[opts]", options);

        // コマンドライン解析
        CommandLineParser parser = new DefaultParser();
        CommandLine cmd = null;
        try {
            cmd = parser.parse(options, args);
        } catch (ParseException e) {
            log.error("cmd parser failed." , e);
        }

        // ユーザー
        cmd.hasOption("u");
        log.info("ユーザー["+String.join(",", cmd.getOptionValues("u"))+"]");

        // 年齢
        cmd.hasOption("a");
        log.info("年齢["+String.join(",", cmd.getOptionValues("a"))+"]");

        // メールアドレス
        cmd.hasOption("m");
        log.info("メールアドレス["+String.join(",", cmd.getOptionValues("m"))+"]");

次の引数で実行してみる。
-a 18 -u edy jeff -m hogehoge@mail.jp

console.log
usage: [opts]
 -a <age>    年齢
 -m <arg>    メールアドレス
 -u <user>   ユーザー
INFO App - ユーザー[edy,jeff] (App.java:67)
INFO App - 年齢[18] (App.java:71)
INFO App - メールアドレス[hogehoge@mail.jp] (App.java:75)

オプション情報も表示され、指定した引数分取得している事がわかる。

ちなみに、必須項目の年齢を抜いてみると

console.log
usage: [opts]
 -a <age>    年齢
 -m <arg>    メールアドレス
 -u <user>   ユーザー
ERROR App - cmd parser failed. (App.java:62)
org.apache.commons.cli.MissingOptionException: Missing required option: a
    at org.apache.commons.cli.DefaultParser.checkRequiredOptions(DefaultParser.java:199)
    at org.apache.commons.cli.DefaultParser.parse(DefaultParser.java:130)
    at org.apache.commons.cli.DefaultParser.parse(DefaultParser.java:76)
    at org.apache.commons.cli.DefaultParser.parse(DefaultParser.java:60)
    at free_dom.test.App.main(App.java:60)
Exception in thread "main" java.lang.NullPointerException
    at free_dom.test.App.main(App.java:66)

"a"オプションが無いよとOutputしてくれる。

バッチを作る際にこういうAPIが欲しいと思っていたので次から使ってみようと思います。
今更ではありますが(*´Д`)

20190218 追記

Commons CLI1.3以降ではOptionBuilderを使うことは非推奨となっておりました。
このため、ソース部分も修正致しました。

OptionBuilder.withArgName("user").
              hasArgs(2).
              withDescription("ユーザー").
              create("u");

代わりに、Option.builderを使います。

Option.builder("u")
      .argName("user"))
      .hasArgs(2)
      .desc("ユーザー")
      .build()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む