3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Hex(ヘックス)— 引き分けのない陣取りで「どの初手なら勝てるか」を 5 × 5 まで解き、2003 年の論文の図と全セル照合した

3
Posted at

Hex(ヘックス)をブラウザに実装した。六角形のマスでできたひし形の盤に交互に石を置き、赤(先手)は上と下の辺を、青は左と右の辺を自分の石の鎖でつなげば勝ち。盤が埋まると必ずどちらか一方だけがつながっているので、引き分けは起こらない。「先手必勝」は戦略泥棒の論法で分かるが、どこに打てば勝てるかは教えてくれない。そこで完全読みのソルバーを書き、ヒントを付けると空いた全部のマスに「ここに打てば勝ち(緑)/負け(赤)」が出るようにした。1 × 1〜5 × 5 の盤では全部の初手の勝敗を計算し、Hayward ら(2003 年の論文 Solving 7×7 Hex)の図 2 と 1 マスずつ照合して、51 マス全部が一致した。5 × 5 の勝ちの初手は 25 マス中 13 マス。枝刈りの規則は 2 つだけで、4 × 4 の全初手の合計ノード数を 1,613,807 から 6,077 に減らした。一方で、よく効くはずの history heuristic は 4 × 4 で 3.0 倍、5 × 5 の中央で 1.64 倍ノードを増やした。「つないだら負け」の misère Hex(リバース Hex)も付けた。テスト 11 件。ソルバー内蔵パズル第 91 弾。

デモ: https://sen.ltd/portfolio/hex-game/
リポジトリ: https://github.com/sen-ltd/hex-game

Hex

ルール

  • n × n のひし形の盤。各マスは 6 つの隣を持つ(端は少ない)。
  • 赤と青が交互に、空いたマスに自分の色の石を 1 つ置く。赤が先手。
  • 赤は上の辺と下の辺を、青は左の辺と右の辺を、自分の石の鎖でつないだら勝ち。

1942 年に Piet Hein が考案し、1948 年に John Nash が独立に再発見した。Nash はプリンストンでこのゲームを広め、「先手必勝」の証明を与えたとされる。

なぜ引き分けがないのか

盤が全部埋まったとき、赤が上下をつないでいないなら、赤の石の塊は上の辺から下の辺まで届いていない。そうなると、赤の塊の「下側の縁」をなぞる青の石の列が左から右まで続いている。六角形の盤では 1 マスが 6 つの隣を持ち、4 マスが 1 点で接することがないので、「斜めにすり抜ける」ことができない。正方形の盤だと、斜めに接する 2 組の石が互いに交差したことになって、両方つながっていない局面が作れてしまう。

テストではこれを、2 × 2〜7 × 7 のランダムに埋めた盤 300 枚ずつで「ちょうど片方だけがつながっている」と確かめている(never draws)。

なぜ先手必勝なのか(でも勝ち手は分からない)

戦略泥棒の論法。後手に必勝法があったとする。先手はまず適当な 1 手を打ち、あとは「自分が後手だったら」としてその必勝法に従う。必勝法が最初の余分な石の場所に打てと言ったら、別の適当な場所に打つ。Hex では余分な石は損にならない(自分の石が多くて困ることはない)ので、先手はこれで勝ててしまう。必勝法が両方にあることはないので、矛盾。引き分けもないので、先手必勝。

この論法は「先手に必勝法がある」とは言うが、それが何かは言わない。どの初手なら勝てるかは、盤を実際に解かないと分からない。

ソルバー

局面の値は「手番の人が勝てるか」の 1 ビットだけ(引き分けがないので)。探索は置換表付きの素朴な深さ優先探索で、1 ビットの答えを返す。

行を整数で持つ

色ごとに、各行を 1 つの小さな整数(ビット c が c 列目)で持つ。マスの集合の「隣」は、この隣接((r, c±1)、(r−1, c)、(r−1, c+1)、(r+1, c)、(r+1, c−1))に合わせた数回のシフトで出る。

private dilate(src: Int32Array, dst: Int32Array) {
  const n = this.n, full = this.full;
  for (let r = 0; r < n; r++) {
    const m = src[r];
    let d = m | ((m << 1) & full) | (m >> 1);
    if (r > 0) d |= src[r - 1] | (src[r - 1] >> 1);
    if (r < n - 1) d |= src[r + 1] | ((src[r + 1] << 1) & full);
    dst[r] = d;
  }
}

「置けば鎖が完成するマス」は、片方の辺から届く自分の石(塗りつぶし)の隣と、もう片方の辺から届く石の隣の、共通部分の空きマスになる。塗りつぶしは dilate して自分の石と AND を取るのを、変化がなくなるまで繰り返すだけ。

置換表

キーは赤と青の石のビット列を double に入れたもの(53 マスまで正確なので 7 × 7 まで)。盤を 180° 回すと同じゲームになる(赤の上下・青の左右がそれぞれ入れ替わるだけ)ので、回した盤とキーの小さい方を使い、1 エントリを共有する。

枝刈り 1: 即勝ち・一本の脅し・二本の脅し

  • 自分に「置けば完成するマス」があれば勝ち。
  • 相手に完成するマスが 2 つあれば負け(1 つしかふさげない)。
  • 相手に 1 つなら、そこが唯一の手。

枝刈り 2: 「二本の脅しを作るマス」への対応だけを候補にする

もう一歩先を読む。

  • 自分に「置くと完成するマスが 2 つできるマス」があれば勝ち(相手は 1 つしかふさげない)。
  • 相手が x に置くと完成するマスが 2 つ以上(集合 Y)できるなら、それは相手の脅しで、自分は {x} ∪ Y の中に打たなければ負ける。|Y| ≥ 3 なら Y のどの 2 つでも脅しは成立するので、必ず打つべきなのは x そのものだけになる。
  • 候補手は、全部の脅しの「打つべき範囲」の共通部分。

なぜ範囲の外に打っても無駄なのか。相手の脅しは「相手が 1 手先に打てば、その範囲(キャリア)の中だけで必ずつながる」ものなので、範囲の外の手は範囲の中に何の影響も与えない。では範囲の外の手で自分が先につなげばよいかというと、それもできない。自分の鎖は相手の鎖を横切らなければならないので、相手が必ずつながれる局面で自分がつながることはない。

private candidates(p: Player, o: Player, list: Int8Array, empties: number): number {
  // p first: a cell that leaves two completing cells wins outright
  this.analyze(p);
  if (this.doubleThreats(null) > 0) return -1;
  this.analyze(o);
  const must = this.mustRows[empties];
  must.fill(this.full);
  const threats = this.doubleThreats(must);
  return this.fillOrder(p, list, threats ? must : null, empties);
}

この判定は 1 ノードあたりの計算が重い。最初の実装は空きマスごとに石を仮に置いて「完成するマス」を数え直していて、5 × 5 の (0, 1) は 4,209,953 ノードに 27.2 秒、1 秒あたり約 15 万ノードだった(枝刈りの無い misère の探索は 4 × 4 で 1 秒あたり約 140 万ノード)。そこで差分で数える形に直した。相手が x に置いて新しくできる完成マスは、

  • x がどちらの辺の「届く範囲」にも触れていなければ、ない(いまの完成マスは 0 個だと分かっている)。
  • x が上の辺側の範囲に触れるなら、「下の辺側から届く範囲に接するマス」のうち、x と x につながる石の塊に隣接するマスだけ。

石の塊ごとに「塊+隣」の行マスクを先に作っておけば、x ごとの計算は数行の AND で済む。枝刈りの中身は同じなので、ノード数は前の版とまったく同じになる(5 × 5 の (0, 1) が両方とも 4,209,953 ノード。表の 5 × 5 の数字は前の版で回したもの)。

結果: どの初手なら勝てるか

記録は全部 runs/ に 1 初手 1 行の JSON で残してある(tools/solve-openings.mts)。初手ごとに置換表を新しくするので、ノード数は実行順に左右されない。

盤 勝ちの初手 図 2 と一致 全初手の合計ノード
1 × 1 1 / 1 1 / 1 0
2 × 2 2 / 4(鈍角の角) 4 / 4 6
3 × 3 5 / 9 9 / 9 54
4 × 4 4 / 16(短い対角線ちょうど) 16 / 16 6,077
5 × 5 13 / 25 25 / 25 229,135,972
6 × 6 — — 18 初手とも 2,000 万ノード以内に決着せず

5 × 5 は 13 マスを解き、残る 12 マスは 180° 回した盤と同じゲームなので回したマスの結果を使った(盤も図 2 も 180° 対称であることはテストで確かめている)。

4 × 4 の結果がきれいで、勝てる初手は短い対角線(鈍角の角から鈍角の角へ)の 4 マスだけ。

. . . W
 . . W .
  . W . .
   W . . .

5 × 5(W が勝ちの初手、行 0 が上の辺):

. . . . W
 . W W W W
  . W W W .
   W W W W .
    W . . . .

論文の図 2 との照合

Hayward, Björnsson, Johanson, Kan, Po, van Rijswijck の Solving 7×7 Hex: Virtual Connections and Game-State Reduction(Advances in Computer Games, 2004)の図 2 には、1 × 1〜6 × 6 の全初手について「そこに初手を打ったら、最善同士でどちらが勝つか」が石の色で描いてある。6 × 6 は van Rijswijck が 2002 年に計算機で出したもの。

照合には 2 つ注意がいる。

  • 論文は White(白)が先手で、盤をダイヤ形(左右に鋭角の角が来る向き)に描いている。先手の辺は右上と左下。
  • 図は画像なので、ページを 400 dpi で描き出し、目で読んで転記した。src/literature.ts に、ダイヤの段ごとに「F(先手勝ち)/S(後手勝ち)」の文字列で置いてある。

こちらの盤の鋭角の角 (0, 0) をダイヤの右端、(n−1, n−1) を左端、(0, n−1) を上端、(n−1, 0) を下端に置くと、こちらの先手の辺(行 0 と行 n−1)がちょうど右上と左下に来る。ダイヤの段 k は r − c = k − (n−1) のマスで、段の中は r + c の大きい順に左から並ぶ。

export function diamondCells(n: number): [number, number][][] {
  const levels: [number, number][][] = [];
  for (let k = 0; k < 2 * n - 1; k++) {
    const d = k - (n - 1); // r - c
    const level: [number, number][] = [];
    for (let r = 0; r < n; r++) {
      const c = r - d;
      if (c >= 0 && c < n) level.push([r, c]);
    }
    level.sort((a, b) => b[0] + b[1] - (a[0] + a[1])); // left to right: r + c descending
    levels.push(level);
  }
  return levels;
}

テストは「各段のマス数が図の文字数と同じ」「全マスがちょうど 1 回ずつ出てくる」ことを確かめてから比べる。結果は 1 × 1〜5 × 5 の 51 マス全部が一致。鋭角と鈍角を取り違えていたら 2 × 2(鈍角が勝ち・鋭角が負け)の 4 マスが全部食い違うので、その種の向きの誤りは最初の盤で見つかる。

文献にある規則性

図からも計算からも、論文やその後の文献が挙げている規則性が読み取れる。テストで 2 × 2〜5 × 5(こちらの計算)と 6 × 6(図)について確かめた。

  • 鋭角の角への初手は負け(Beck, 1969。n ≥ 2 の全部の盤で証明されている)。
  • 鋭角の角の隣で、先手自身の辺にあるマスも負け(Beck, n ≥ 3)。
  • 短い対角線のマスは全部勝ち、先手自身の辺のマスは(短い対角線の 1 マスを除いて)全部負け。論文が 7 × 7 以下で観察して「一般に成り立つかは興味深い問題」と書いているもの。

5 × 5 で重いのはどの初手か

初手 結果 ノード
(1, 4) 勝ち 34,663,791
(0, 4)(鈍角の角) 勝ち 31,409,592
(1, 1) 勝ち 31,323,533
(2, 0) 負け 20,498,236
(2, 2)(中央) 勝ち 9,941,706
(0, 1) 負け 4,209,953

勝ちの初手が重い。勝ちを示すには、青のすべての応手(24 通り)に対して赤の勝ち手を見つけなければならない。負けの初手は青の勝ち手を 1 つ見つければ済む。例外は中央で、勝ちの初手の中で一番軽い(ほかの勝ちの初手は 2,143 万〜3,466 万ノードで、その半分以下)。理由までは確かめていない。

枝刈りの効き目

全初手の合計ノード数(初手ごとに置換表を新しくした)。

盤 素朴な探索 枝刈り 1 枝刈り 1 + 2 1 + 2 + history
3 × 3 2,535 601 54 63
4 × 4 1,613,807 209,118 6,077 18,359

4 × 4 では、枝刈り 1 で 7.7 分の 1、枝刈り 2 を足すとさらに 34 分の 1、合計で 266 分の 1 になる。5 × 5 では差がもっと開き、(0, 1) は枝刈り 1 だけだと 2 億ノードの上限でも決着しなかった(枝刈り 2 なら 4,209,953 ノード)。

意外だったのは history heuristic(過去に枝を切った手を先に試す、チェスなどでよく効く手順序)で、Hex ではノード数を増やした。4 × 4 で 6,077 → 18,359(3.0 倍)、5 × 5 の (0, 1) で 4,209,953 → 5,159,438(1.23 倍)、中央で 9,941,706 → 16,327,774(1.64 倍)。既定の手順序は「中央から近い順」で、Hex の勝ち手は中央寄りにあることが多い。history は「前に枝を切ったマス」を優先するので、端の方の「たまたま一度効いたマス」に探索を引っ張っていく。

6 × 6 は届かなかった

6 × 6 の 18 初手(残りは 180° 回転)を、1 初手あたり 2,000 万ノード(1 本 65〜118 秒)の上限で回したが、1 つも決着しなかった。鋭角の角 (0, 0) のように理論的に負けと分かっている初手でもだめだった。負けを示すには、青の勝ち手を見つけたうえで、その後の赤の全部の手に対して青の勝ちを示す必要がある。

Hayward らが 7 × 7 を解いたのは、ここで使った「二本の脅し」を一般化した**仮想連結(virtual connection)**を、AND・OR の合成規則で盤全体について計算する方法(H-search)による。二本の脅しは、その一番浅い場合(キャリアが 3 マス以下の半連結)にあたる。5 × 5 まではこれで足りるが、6 × 6 ではブリッジ(共通の空きマスを 2 つ持つ 2 つの石)や辺のテンプレートを鎖としてつなぐ必要がある。ページの対戦も 5 × 5 までにした。

misère Hex: つないだら負け

「自分の辺をつないだ方が負け」にした変種。盤が埋まると必ずどちらかがつながるので、これも引き分けがない。Lagarias と Sleator(1999)が「n が偶数なら先手、奇数なら後手が勝つ」ことを示している(負ける側は最後の 1 マスまで負けを引き延ばせる、という論法)。また、偶数の盤では鋭角の角が勝ちの初手になる。

盤 先手必勝? 勝ちの初手 ノード
2 × 2 はい 2 / 4(鋭角の角) 24
3 × 3 いいえ 0 / 9 2,279
4 × 4 はい 14 / 16((1, 2) と (2, 1) 以外) 16,468,500

どちらの定理も成り立っている。通常の Hex では負けの鋭角の角が、misère では勝ちに変わる。4 × 4 で負けの 2 マスは、通常の Hex で勝ちだった短い対角線の内側の 2 マス。misère の探索には「二本の脅し」が使えない(脅しは「つながってしまう危険」なので逆向きになる)ので素朴な探索のままで、4 × 4 で 1,647 万ノードかかった。

ページの作り

  • 探索は Web Worker で、空きマスを中央から順に 1 マスずつ解いては結果を送る。1 マスあたり 15 万ノードの上限を付け、超えたマスは「?」で出す。
  • 5 × 5 の空の盤は計算済みの表から即座に出す。1 手目の後も、その初手が勝ちなら青のどの応手も負けなので、表から全マス赤の点を出せる(厳密)。
  • それ以降は Worker の探索。スクリーンショットの局面(赤が中央、青が (1, 3)、赤が (3, 1)、青が (2, 1))は 110,374 ノード・約 0.5 秒で全 21 マスが決まり、赤の勝ち手は 3 つ。
  • コンピュータは、勝ちのマスが見つかった時点でそこに打つ。勝ちが無ければ上限超えのマス、それも無ければ一番中央の負けのマスに打つ。

テスト(11 件)

  • 盤の形(鋭角の角の隣は 2 つ、鈍角の角は 3 つ、内側は 6 つ)
  • 引き分けがないこと(2 × 2〜7 × 7 のランダムに埋めた盤 300 枚ずつ)
  • 行マスクで求めた「完成するマス」が、1 マスずつ石を置いて確かめた結果と一致すること(3 × 3〜6 × 6)
  • 枝刈り 0・1・2 と history ありの全部が、置換表も枝刈りも無い素朴なミニマックスと、ランダムな 3 × 3・4 × 4 の局面で一致すること
  • misère Hex も素朴なミニマックスと一致すること
  • ノード上限で打ち切っても盤が元のままであること
  • 図 2 の転記がダイヤの段の形に合い、全部のマスを 1 回ずつ通り、180° 対称であること
  • 図 2 と、n ≤ 4 をその場で解き直した結果、openings.json の全部が一致すること
  • 文献の規則性(Beck の 2 つ、短い対角線、先手の辺)
  • misère の偶奇の定理

まとめ

  1. Hex には引き分けがなく、戦略泥棒の論法で先手必勝だが、勝ち手は解かないと分からない。1 × 1〜5 × 5 の全初手を解いて、Hayward ら(2004)の図 2 と 51 マス全部が一致した。5 × 5 の勝ちの初手は 13 / 25、4 × 4 は短い対角線の 4 マスだけ。
  2. 「相手の二本の脅しのキャリアに打つ」という 1 つの規則で 4 × 4 の全初手の合計ノードが 34 分の 1 になった(素朴な探索から通算で 266 分の 1)。一方 history heuristic は 4 × 4 で 3.0 倍、5 × 5 の中央で 1.64 倍ノードを増やした。
  3. 6 × 6 は 1 初手 2,000 万ノードでは 1 つも決着しなかった。ここから先は仮想連結(H-search)が要る。misère Hex は 2 × 2〜4 × 4 で Lagarias–Sleator の偶奇の定理どおり。

参考文献:

  • R. Hayward, Y. Björnsson, M. Johanson, M. Kan, N. Po, J. van Rijswijck, "Solving 7×7 Hex: Virtual Connections and Game-State Reduction", H. J. van den Herik, H. Iida (eds.), Advances in Computer Games, IFIP vol. 263, Kluwer, 2004(図 2 は p. 263。2026 年 10 月 21 日参照、図は目で転記)
  • P. Henderson, Playing and Solving the Game of Hex, PhD thesis, University of Alberta, 2010(§2.7: Beck の鋭角の角の結果、misère Hex)
  • J. Lagarias, D. Sleator, "Who wins misère Hex?", E. Berlekamp, T. Rodgers (eds.), The Mathemagician and Pied Puzzler, A K Peters, 1999, pp. 237–240

デモ: https://sen.ltd/portfolio/hex-game/
リポジトリ: https://github.com/sen-ltd/hex-game

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?