将棋ソフト開発 Week1 ~ 将棋のコア部分を作る ~
プログラミングの勉強がてらに、将棋ソフト開発をしています。0週目は作るプログラムを方針を作成しましたが、1週目が終わり進捗報告と、やったこと、これから取り組むことについて記載していきたいと思います。 1週目ではリポジトリやパッケージ構成の準備と将棋のコア部分である各駒や盤の実装や、ルールの実装を取り組みました。
目次
進捗報告
一旦ランダムに動くところまで作りました。将棋のプレイを管理するクラスはまだ書いてないのでテストコードで無理やり実装して動かしているだけです。
将棋プログラムできました
— 凪なぎさ (@nagi_nagisachan) October 16, 2020
ランダムで打って王手放置によって大体終わります(。・_・。) pic.twitter.com/g0P0iO0SVs
ちょろっと書いてあるプログラム自体は色々この後いじってるので、後述の解説?とは違うところがあります。
1週目でやったこと
- 将棋のコア部分の作成
- パッケージ、リポジトリ構成決め
- 駒の作成
- 盤の作成
- 移動のルールの作成
駒や盤といってもプログラム的に作成してるだけなので、実際に目に見えるものは作っていません。 一応Debugようにコンソール上で表示するようにはしました。
方針の変更
若干方針を変更しまして、拡張性の高いプログラムを作ると言っていましたが、一旦汎用的なものではなく普通の9x9の将棋をわかりやすく作ることにします。 あまりに汎用的なものを作るともはやボードゲームエンジンになってしまうので、ソレは非常に作るのは大変です。まずは動くものを作ることを優先!
パッケージ、リポジトリ構成決め
gradleのマルチプロジェクトにします。 ちなみに言語はJavaを使って書き始めてしまいました。今覚えばKotlinにしておけばよかったと思ってます。 時間があればKotlinで書き直すかもしれません。
3つのプロジェクトを包含していて、それぞれ役割毎に別リポジトリにしています。
- shogi-presentation-cli
- プレゼンテーション層
- 末尾にcliとしているのは一旦コマンドラインでの入出力を作成するため.
- 今後cliではなくWebAPIのインタフェースなども作る可能性もあるが、このプレゼンテーション層のみ変更すればよいです。
- shogi-application // アプリケーション(ユースケース)層
- shogi-core // ドメイン層
この分け方はクリーンアーキテクチャ(Clean Architecture)やオニオンアーキテクチャを意識した分け方にしています。
画像はClean Coder Blog より引用
今の所インフラ層はないですが、今後作るアプリケーションで棋譜保存が必要になるとか、外部との通信が発生するとかとなれば作ればよいです。
現状作るものでcliのアプリを想定して下記のような構成を予定しています。
.
|-- gradle
| `-- wrapper
|-- shogi-application // submoduleで別リポジトリ, アプリケーション層のコードを格納する
| |-- gradle
| | `-- wrapper
| `-- src
| |-- main // アプリケーション層の実装コード
| | |-- java
| | | `-- com
| | | `-- naginagisa
| | | `-- shogi
| | | `-- application
| | | |-- manager
| | | `-- service
| | `-- resources
| `-- test // アプリケーション層のテストコード
| `-- java
| `-- com
| `-- naginagisa
| `-- shogi
| `-- shogiapplication
|-- shogi-core // submoduleで別リポジトリ, ドメイン層のコードを格納する
| |-- gradle
| | `-- wrapper
| `-- src // ドメイン層の実装コード
| |-- main
| | `-- java
| | `-- com
| | `-- naginagisa
| | `-- shogi
| | `-- core
| | `-- domain
| | |-- model // ドメインモデル
| | `-- service // ドメインサービス 必要ない場合もある
| `-- test // テストコード
| `-- java
| `-- com
| `-- naginagisa
| `-- shogi
| `-- core
| `-- domain
| |-- model
| `-- service
`-- shogi-presentation-cli // submoduleで別リポジトリ, プレゼンテーション層のコードを格納する
|-- gradle
| `-- wrapper
`-- src
|-- main // プレゼンテーション層の実装コード
| |-- java
| | `-- com
| | `-- naginagisa
| | `-- shogi
| | `-- shogipresentationcli
| | |-- presenter
| | `-- controller
| `-- resources
`-- test // プレゼンテーション層のテストコード
`-- java
`-- com
`-- naginagisa
`-- shogi
`-- shogipresentationcli
一旦仮おきで、細かくはもっと分けるかもしれませんし、若干の構成変更はするかもしれません。 ただこの層で分けた、マルチプロジェクトの構成は変わらない予定です。
week1で今作ってるのはshogi-core(ドメイン層)の部分です。 submoduleにしてあるので、実際にはshogi-coreのリポジトリ単体で開発をしていけます。
駒の作成
本プログラムは高速なものを作るつもりは一切ないので、bitboardとかは使いません。コンピュータ将棋は勉強したてであまり良くわかっていませんし。 余談ですが、相当速度は犠牲にしているので、探索や学習で性能が出なくて使い物になないかだけが不安です。
domain.model.piece以下に駒関連のクラスを生成します。
pieceType側(enum)に全てロジック持つこともできそうですが、クラスで分けたほうが素直そうなので今回はクラスで分けてます。
各駒クラスはイミュータブルなものとしています。スレッドセーフになりますし、意図していないところで変更の影響を受けないようにするためです。 毎回オブジェクトを生成するのはコストではありますが、 将棋は81マスで、先手後手と駒の種類があるとして1つのコマあたりたかだか 81*2 で162のオブジェクトの生成で済みます。この程度であれば事前に生成しておく、もしくはキャッシュしておくなどすればそれほどコストはかからないかと思います。
pieceTypeはこんな感じ。 javaの場合はenumの中に振る舞いをかけてしまうのでそっちに各駒のルールなどのロジックを書いてもよいですね。
public enum PieceType {
FU,
KYO,
KEIMA,
GIN,
KIN,
KAKU,
HISYA,
TOKIN,
NARI_KYO,
NARI_KEI,
NARI_GIN,
UMA,
RYU,
GYOKU
}
今はクラス内で自分自身が動ける箇所を求めるメソッドを持っています(legalMoveList). 中でさらにメソッドに分けたりはしていますがやっていることは下記のことです
- 自分の持つ効き(Effect)方向に探索して動ける箇所(Position)を取得
- 歩は1段目(後手の場合は9段目)には存在できないなどのルール上での動ける箇所(動けない箇所)の取得
最終的に動けるマスは、これらで求めたPositionの集合の積集合や差集合などの集合演算で求めています。 ルールとかも別クラスに切り出したいですが一旦ベタ書きです Effectも将棋はたまたま効きと駒の動きが一緒ではありますが、本来動きと効きは別物だとも考えられるので分けておきたくもあります。チェスとかだと実際に効きと動きが違う駒もありますしね。 とはいえそういう変則将棋をもし作るとなったときに考えれば良いので、一旦効きと移動箇所は一緒のものにしておきます。
効きに関しては次のように定義しています。 enumですが、Javaの場合は振る舞いを持てるので盤上の他の駒を考慮しない効きのPositionのリストを返すpositionListというメソッドをつけています。
各駒クラスの、legalMoveListの実装で1つ気になっているのは王手放置などの反則手を含めるかどうか。 ここでピンや、王手放置などの手を省くもしくは、後述の盤(Board)側で省くのも良いのですが、探索時に次の局面を作ってその局面が合法な局面かどうかを判断するほうが簡単そうな気がします。 っというわけで一旦ここは反則手ありの手を生成するようにしています。 その場合メソッド名は要検討ではありますが.
盤の作成
盤関連のクラス図は次のようにしています
BoardもImmutableにしたいですが、盤には駒を追加していったりするので、一発で生成するのは引数が多くなってしまったり、例えば駒を追加するときに生成側で駒のコレクションを生成したりと大変になってしまいます。 なのでBuilderクラスを用意して生成することにしています。
マス目の表現
Squareはマス目を表します。将棋盤であれば9x9の配列で表しています。 この二次元配列も別クラスに切り分けたほうがよさそうですが一旦、このままで。後々リファクタリングするかも。
持ち駒の表現
CapturedPiecesは持ち駒を表します。 単純Mapで歩が○枚、金が▲枚みたいな感じで持っているだけですね。 持ち駒は打つことができるので、uchiteListメソッドで打つ指し手のリストを返すように実装しています。
ボード上でのすべての指して
すべての指してはlegalMoveListで求めます。 やっていることは、各コマのlegalMoveListメソッドの返り値と、持ち駒のuchiteListをすべてマージしています。
public List<Move> legalMoveList() {
List<Move> legalMoveList = new ArrayList<>();
legalMoveList.addAll(pieceSet(teban).stream().flatMap(p-> p.legalMoveList(this).stream()).collect(Collectors.toList()));
legalMoveList.addAll(currentCapturedPieces().uchiteList(this));
return legalMoveList;
}
pieceSetメソッドは書いてないですが、現在の手番の駒をすべて取得するようなメソッドです。 各駒クラスや、持ち駒クラスにそれぞれの領域での手の生成は任せているので、簡潔にかけます。
moveメソッド
指し手(Move)を作用させた後の盤を返します。 Move自体の説明は後述の指し手の表現を参照してください。 つまり1手指した後の局面を作るメソッドです。
指し手の表現
一旦こうしています。※ダメな実装です
from: 移動元の位置、 to: 移動先の位置, isPut: 駒打ちかどうか putPieceType: 打つ駒の種類 isPromotion: 成るかどうか
駒を打つときはfromはnullになって、 isPutフラグで駒打ちかどうか判断できる。 一見なんとかなりそうですが、 さすがにこのクラスはいろいろ問題点が多いので流石に後で直します。
問題点
- 呼び出し側でisPutフラグを見て判断しないといけない
- 駒を打つときはfromがnullになっていることを意識しないといけない。 Optionalなどでチェックを強制できるがあまりよいとは思えない
解決策 駒の移動のアクションと、駒を打つアクションを一緒くたにしてるのが問題な気がします。
といわけで、例えばですが指し手インタフェースを作って、それを実装した駒打ちクラス、駒移動クラスなどと言った形で各アクションごとの実装クラスを分けたほうが良さそうです。
week2で直しておきたい。
やったことのまとめ
全体のクラス図としてはこんな感じになります。 細かい関係の線は複雑になるのであえて省いています。
だいぶ部品は揃ってきたかなーという感じです。
これから取り組むこと
これからweek2で取り組むことについて
- week1で残した課題の解消
- 最低限のusiプロトコルの実装
先に探索部分を作ろうかとも思いましたが、さすがにコンソール画面上で将棋動かしているだけではつまらないですし、自分で対局しようにも、コマンド打ちながらというのも大変なので先にUSIプロトコルの実装をすることにしました。
USI(Universal Shogi Interface)プロトコルとは、将棋GUIソフトと思考エンジンが通信をするために、Tord Romstad氏によって考案された通信プロトコルです。USIの原案は、以下で読むことができます。
将棋所:USIプロトコルとは より引用
要は、USIプロトコルを実装すれば、自分で作った将棋ソフトも、将棋所や将棋GUIでエンジンとして登録できるようになるということです。 将棋GUIや将棋所が使えれば、マウスなどGUI操作で自分のソフトを試すことができるのでテストプレイが非常にやりやすく成りますね。
というわけでこちらを優先してweek2は取り組もうと思います。