将棋盤とその上に駒が載って動かせるところまでモデル化しました。
次はその駒が「どこへ動けるか」をモデル化しましょう。
名前が長くなりすぎたのでここからは単に「盤」と「駒」と書きます。
一度、操作をする人も書いたほうが分かりやすいかな?
人は駒を動かします。どこに動きたいか考えるために盤も見るでしょうが、駒の動きを超えて盤上を動かすことはできないので盤を操作することは禁止するべきです。
駒は盤を見て自分の動ける範囲を判定し、その範囲内でどこに動くかを決めて盤に申請します。
盤は駒がどこへ動けるかは知りませんので、動いた先が盤内であり他の駒が無いかだけ判定して駒を動かすか拒否するかします。
前回の終わりに書いたように駒に移動能力を持たせましょう。
成るときは能力を「歩」から「と金」に取り換えればいいわけですね。
ところで将棋は桂馬を除いて他の駒は飛び越せませんし自分の駒のある場所には動かせませんが、相手の駒があるところへは動かすことができます。一方、FEHは他の駒があるところへは動かせませんが、自分の駒を通過して動くことができます。つまりゲームによって対象の駒を通過できるか・止まれるかは変わってくるわけですね。駒の全体的な動きのルールがあるという事はそれは共通化できるという事です。共通化してしまいましょう。ここまできてようやく共通化が出てきました!
そのゲームにおいて全ての能力に共通のルールは盤にあるべきか?
さて、盤の説明をしたときに例外のないルールは盤にあるべきという話をしました。では共通のルールも盤にあるべきでしょうか?
まず盤にゲームのルールを適用するという事は「あるゲーム用の盤」ができるということであり、複雑になるのでお勧めできません。ゲームに限らず将棋盤は将棋盤なのでそのままのほうが使いやすいです。例えば将棋盤の縦横一列を削ってチェスに使ったりしたいですからね。
また、実のところこの共通のルールは抜け道があるケースが多いので盤に持たせたくないというのもあります。チェスでは入城する際に普段と違う動きをするとか、FEHでは普段は敵をすり抜けることができませんが敵をすり抜けるスキルが存在するとか。
このときに「全ての能力に共通のルール」を「個々の能力」が上書きできるようにしておくと、将来駒の種類が増えたときにも安心です。
駒の移動範囲は自分の位置からどこへ動けるかで保持しているので、実際に移動先を指定する際には現在の盤面で移動できる範囲が必要になります。
例えば将棋のスタート盤面から王が動けるのは前方3方向だけです。他の形式もあるでしょうが9x9の升目それぞれに移動可能/不可能を判定するのが分かりやすいんじゃないでしょうか。
移動可能な盤面を作るという事は、現在の盤面を見て動かそうとしている駒の移動可能経路を探索するという事です。つまり、現在の盤面に依存した存在です。移動可能な盤面は現在の盤をもとに作成され、一手進めば現在の盤面ではなくなるため破棄されます。つまり移動可能な盤面は盤そのものではないということで、これは盤を参照する存在となります。
盤面を見て経路を探索するロジックは駒に直接メソッドを加えてもいいのですが、動かすのとは別の責務なので便宜上別にして継承して表記してみます。
さて、この「経路探索駒」の責務とは何でしょうか?いや「経路を探索すること」なのはわかってますが具体的にはどのようにするものでしょうか?経路は移動可能領域のことです。つまり移動が分かれば経路もわかるはずです。
私が知る限り駒の移動にはいくつかあります。
・将棋のように、駒が特定の方向に一歩または直線的または飛び越えて移動する(桂馬)
・FEHのように、駒が移動力を持ち向きは持たない。地形によって移動力を消費して移動する。なお駒に向きがあっても移動に関係が無ければ移動時に考慮する必要はない(タクティクスオウガやその派生)
・駒が移動力と向きを持つ。向きと相性が悪い方向への移動にはペナルティが課せられる。将棋盤には少ないが六角系では割と多い(バトルテックや戦車戦など射界があるゲーム)
とりあえず「駒が盤面を見て自分の位置から移動範囲を探索する」という作りにしてみましょう。
このときに経路探索アルゴリズムを経路探索駒側に持たせることもできますが、経路探索ということは盤面の全ての情報にアクセスするという事で盤側に強く依存します。そのうえ駒側は自由にアルゴリズムを決定できるので駒ごとに別々の記述をすることが多く再利用性が悪くなりがちです。例えば、将棋の角とチェスのビショップはほぼ同じ動きをしますが駒が同じようなコードで記述されてるとは限らなくなるでしょう。駒ごとにアルゴリズムを作成できる自由さを完全に殺して辛さだけが残ります。
再利用性を上げるために共通のルールを盤側に作ります。繰り返しになりますが盤が駒を管理している都合上、駒へ強制するルールは盤側にあるべきなのです。
そのルールは「盤面は駒の現在位置から一歩移動して移動できるか判定しまたそこから一歩移動できるか…を再帰する」というものです。一般的なアルゴリズムなので探せばどこかに開設があるかと思います。
・盤は駒の移動できる向きを得る
・盤は駒の現在位置から移動できる方向のうちいずれかに一歩移動する
・盤は移動先にある駒・移動時の向き・歩数を駒に伝える
・駒はそれをもとに移動できるかを判定する
・移動できたらさらにそこから一歩進むように再帰する。移動できなかったら別の方向へ一歩進む
例えば駒が0,0にあるとして、上下左右に2歩動けるとします。
盤は駒に対して0,1に動けるかを問い合わせ、動けるようならさらに一歩進めて0,2に進めるか問い合わせ、一歩戻ってから別方向に進んで1,1に進めるか問い合わせ、2歩戻ってから別方向に進んで1,0に進めるか問い合わせ…というふうに盤が経路を探索し、都度その駒が進めるかを問い合わせるようにします。
移動先の判定をするには判定の材料が要ります。
駒が移動するとは具体的にどのようなことでしょうか?
・ある枡にある駒が別の枡に移動する
・別の枡には駒があるかもしれないし無いかもしれない
・別の枡に駒があったときそこで止まれるかもしれないし止まれないかもしれない
・経路は途中の枡を通る、または桂馬のように飛び越える
・経路の枡には駒があるかもしれないし無いかもしれない
・経路は歩数を数えるかもしれないし数えないかもしれない。歩数を数えるとき地形によって消費歩数が違うかもしれない
・経路は自由に選べるかもしれないし向きによるかもしれない。例えば飛車は上下左右になにかに突き当たるまで動けるが曲がれない。
おっと地形の存在を忘れてました!将棋などではどの枡も同じですが、SLGの多くは地形が存在して地形により移動や戦闘に影響が出ます。
これは盤を継承して「地形のある盤」にしてもいいのですが将来的に「天候のある盤」とかも出てくるかもしれませんし、「盤」を「駒だけの盤面」「地形だけの盤面」に分割することはさほど不自然ではないでしょう。地形だけ保存して別の戦いで再利用するとか普通にありますからね。というわけで必要に応じて盤に地形を関連させます。
話を元に戻して、駒に「その枡へ移動できるか?」を問い合わせるときに引き渡す情報の話です。上記で気になったものを全て渡してしまいましょう。なに、不要なら駒側が無視すればいいだけの話です。逆に言えば最初は最小限であとから追加しても同じです。例えば将棋は地形とかないので
fun 移動可能か?(対象の場所にある駒, 現在の歩数, 侵入角) : Boolean {}
で一度作ってから、同じコードを地形のあるゲームで再利用する際には将棋に使わない引数を追加しても別に困らないでしょう。関係ないコードを変更するなと怒られるかもしれませんが!
fun 移動可能か?(対象の場所にある駒, 現在の歩数, 侵入角, 地形 = null) : Boolean {
//例えば敵がいなくて自分の移動力 - 現在の歩数が地形の必要移動力以上あればtrue
//地形は使わないのでnullでいいしnullが嫌ならPlainとか適当に作る
}
もちろん歩数を計算することも同時に必要になります。
fun 何歩使ったか?(対象の場所にある駒, 現在の歩数, 侵入角, 地形 ) : Int {
//例えば地形の必要移動力。地形が無ければ 1 でいい
}
ゲームによってはそこで止まれるかも必要になるでしょう。
fun 停止可能か?(対象の場所にある駒, 現在の歩数, 侵入角, 地形 ) : Boolean {
//例えば誰かがいる枡には止まれない。通過できるかは「移動可能か?」で判定しているはず
}
これもう引数は侵入状況でいい気がしてきましたね。映画とかで侵入チーム!状況を報告せよ!と問い合わせたら「現在A地点に南側から進入中。敵の見張りを発見。」とか返ってきそうなそんなノリで。
data class 侵入状況(対象 : 駒?, 現在の歩数, 侵入角, 地形 : 地形?=null )
fun 移動可能か?(侵入状況) : Boolean{}
fun 何歩使ったか?(侵入状況) : Int {}
fun 停止可能か?(侵入状況) : Boolean {}
盤面からは駒へ問い合わせますが、これらは全て能力に移譲するべきでしょう。なんせそれらを決めてるのは能力なんですから。とはいえUMLで移譲を書くのはあまり意味がありませんしこのまま進めます。
これで、「人は駒を盤の「特定の座標」に動かす」「駒は盤面から「移動可能な盤面」を作成し、そこへ動けそうならそこへ動くことを盤に伝える」までできました。
これで盤上を駒を(能力次第ですが)自由に動かせそうですね!
改めて人を追加すると同時に、人は盤を見たり駒を探したりもできるでしょうから追加しときましょう。
駒を見る/探すは駒が盤上にある限り存在するべき手順であり、分離…「駒を動かすことはできるがどこにあるかはわからない」盤を作ることは無意味なのは前に述べたとおりです。これは追加というよりは今まで書き忘れていたのを補足したといえるでしょう。まだSRPは維持できているはずです。
ところで、今回設計したクラスたちはエンティティでしょうか?グローバルなIDは持たないけど状態は持つオブジェクトでしょうか?それとも状態も持たない値オブジェクトでしょうか?
人から見て駒を盤上で動かす以外の操作は追加されていません。強いて言えば、移動の際に「成る」ことができて駒の能力は変えられるでしょう。ので状態は「駒の状態」しかないままです。移動可能な盤面は一見状態っぽく見えますが、盤上のそれぞれの駒の位置から一意に計算されるので状態じゃありません。
つまり今回書いたクラスはほぼすべて「値オブジェクト」です。
値オブジェクトを重視するDDD者も大満足ですね!
ともかくこれで盤上で駒を動かせるようになったわけですが、将棋やチェスなら二人の人が交互に駒を動かすだけなのでこれで終わりです。移動先の駒を取り除いたりそれを手駒に加えたりもあるでしょうがそれは人が盤を操作していいでしょう(駒に「駒を取り除く」手順があるわけではないので)。
お疲れさまでした!
…でも私がモデリングしようとしているのはFEHなので駒は移動だけではなく戦闘やアシスト行動も行います。ていうか駒が移動するだけのSLGなんてそうあるもんじゃないですし色々な行動オプションを作りたいところです。というわけで続けて「移動以外の行動もとれるモデル」を作成します。