タンク・チェンジ 戦車の移動とクリア判定の続きです。今回はギブアップ時に正解手順を示す処理を実装します。

解を求める方法

ユーザーがギブアップした状態からゲームクリアまでの最短手順を取得するために幅優先探索で全探索をおこないます。そのために現在の状態を数値に変換します。

現在の状態とは青と赤の戦車がそれぞれどこにあるのかとどちらの手番なのかです。同じ色であれば戦車を区別する必要はありません。このようにすることで考える必要がある場合の数を減らすことができます。また最短手順を考えている以上、同じ状態が何度も出現することはありえず、千日手のような状態になっていつまで待っても答えがでてこないということも避けられます。

まず戦車の位置は行と列で表現されていますが、これを (row + 1) + 10 * (col + 1) として2桁の整数で表現することにします。また戦車の番号を青と赤それぞれで昇順(小さい順)でソートして以下のように計算します。

動的計画法で bitDPがありますが、その劣化版です(bit じゃなく十進数そのまんまだし)。なんかデタラメやっているようですが、一応最短手順は取得できます。また JavaScript で安全に扱える整数は 2 の 53 乗 – 1 であり、9,007,199,254,740,991 です。上のコードだとその範囲内に収まっています。

Handクラスの定義

次の手に関する情報を格納するためのHandクラスを定義します。コンストラクタに渡されるのはどの位置にあった戦車がどの位置に移動するかです。

Statusクラスの定義

戦車の位置情報などの状態を保存するためにStatusクラスを定義します。最初にコンストラクタを示します。

TankPositionsToLong関数は両軍の戦車の位置とどちらに手番があるかを整数値で返すためのものです。

開始点の状態をセットする

SetStartPositions関数は最短手順を考えるときの開始点になる状態(降参ボタンをクリックしたとき)をStatusオブジェクトに反映させるためのものです。

つながっている位置を取得する

GetNextPositions関数は引数で渡された位置から移動できる位置のリストを返します。

位置を表す整数は (row + 1) + 10 * (col + 1) なのでこの値に 11 と 9 をそれぞれ増減すると斜めに移動できる位置がわかります。また増減の結果、10の位が 1 ~ 5 以外であったり、1の位が 1 ~ 7 以外になった場合はマップの範囲外に出てしまったことになります。

その位置は安全か調べる

IsSafe関数はその位置が敵から撃たれない位置かどうかを判定します。引数の位置から敵にたどりつけるのであれば敵に撃たれる位置です。そうでないなら安全な位置であると判断できます。

安全に移動できる位置を求める

GetSafeNextPositions関数は前述のGetNextPositions関数とIsSafe関数を使って移動可能で撃たれない位置のリストを取得するためのものです。

次の手の候補リストを取得する

GetHands関数は現在の状態から次の手の候補のリストを取得します。

着手後の状態を取得する

GetResult関数は着手によってどのような状態に移行するかを取得します。

BluePositions と RedPositions から hand.From と同じものがあるときは取り除き、hand.Next を追加したオブジェクトを複製すれば着手後の状態をもつオブジェクトを生成することができます。それと同時に手番の交代と手数が増えるので Turn と TurnCount の変更も反映するようにしておきます。そして最短手順の一例を示さなければならないので前の状態を保存しておきます。

クリア判定

IsFinish関数はクリアしたかどうかを調べるためのものです。

幅優先探索で最短手順を取得する

Statusクラスを用いて幅優先探索で最短手順を取得する処理を示します。

まず最初の Status オブジェクトを生成して、SetStartPositionsメンバ関数を呼び出して現在の状態を格納します。そしてこれをキューに格納します。

あとはループ処理でキューから格納されたオブジェクトを取り出して GetHands メンバ関数を呼び出して次の手の候補を取得します。手の候補からそれぞれ GetResult メンバ関数を呼び出して次の状態を表すStatus オブジェクトを取得し、それをまたキューに格納するという処理を続けます。

ただこのときに同じ状態を複数回探索してしまわないように連想配列にオブジェクトを格納します。同一の状態が出現したときは手数が少ない場合以外は無視します。これでいつまで経っても処理が終わらない状態を回避できます。連想配列で使うキーはTankPositionsToLongメンバ関数が返した整数値です。

GetResultメンバ関数が返したオブジェクトの IsFinishメンバ関数が true を返すかキューの中身が空になったら処理は終了です。IsFinishメンバ関数が true を返すことなくキューの中身が空になった場合はすでに手詰まりになっていて最短手数は存在しない場合です。

正解手順にしたがって戦車を動かす

幅優先探索の結果を取得して正解手順にしたがって戦車を動かす処理を示します。

bfs 関数の戻り値が null の場合は解は存在しません。この場合はなにもしないで false を返します。bfs 関数の戻り値が null ではない場合はこの状態は戦車を移動させた最後の状態なので、ここから逆に遡って前の状態を取得していきます。

lastStatus.PrevStatus が null であればこれが降参ボタンをクリックしたときの状態です。これまで取得したオブジェクトをリストに追加するときに最後尾ではなく先頭に追加するようにすれば反転させなくてもこれが正解手順となります。

正解手順にそって戦車を動かすには手順の前の状態との差分(なくなったもの、追加されたものはないか)を調べます。それがそのターンで動かす戦車です。どの戦車を動かすかわかったらあとはそのとおりに移動させるだけです。