144牌の麻雀ソリティアを作り、終了後に「どのペア選択で勝ち筋を失ったか」を振り返れるようにしようとしました。しかし、最初の技術検証では、負けで終了したTurtleの20履歴のうち、全局面の判定が終わったのは8履歴だけでした。
profilerで調べ、1回の重点最適化を行うと14履歴まで増えました。それでも20履歴中14、70%です。別seedのholdoutでも、全履歴完了95%という基準には届きませんでした。
それでも現在は、実際に動く「こむの牌路」Mahjong Solitaireを公開しています。理由は、未達を達成に読み替えたからではありません。全履歴を必ず確定する製品の約束から、確認できた事実と未確定を分けて返すbest-effort Reviewへ、受け入れる体験を変えたからです。
公開したのはClassic 144牌とShort 72牌
ClassicはTurtle 144牌、ShortはBridge 72牌です。配置座標はこのプロジェクト独自のものです。Classicは通常34種を4枚ずつ、花4枚と季節4枚。Shortは通常16種を4枚ずつと同じ8枚の特殊牌を使います。
牌は上に重なりがなく、同じ段の左右どちらかが空いていれば選べます。通常牌は同じ絵柄同士、花は花同士、季節は季節同士を取り除けます。花と季節は別グループです。
初期配置は全消しrouteを先に構成し、牌を割り当て、独立checkerで全routeを再生してから採用します。開始時に勝ち筋があることと、プレイヤーが別のペアを取ったあとの局面を短時間で解けることは別問題でした。
研究用のPyramidや浮庭などもリポジトリにはありますが、現在の公開モードではありません。この記事の性能比較はTurtleとBridgeに限定します。
自前solverが証明すること
solverはBigIntのoccupancyとblocker masksで状態を表し、合法なペア除去をDFSで探索します。探索順序には牌の解放を使い、完全に探索し尽くしたdead-stateをmemoへ保存します。
全消しrouteを発見してcheckerで再生できればWINNABLE。全子分岐を調べ尽くしても全消しできなければUNSOLVABLE。上限に達した状態はUNKNOWNです。途中まで調べた親を「負け」としてmemoへ入れません。
Mahjongのこのsolverは、牌配置を知る完全情報の解析です。見えている数字などから推論するMinesweeperの情報条件とは違います。また、通常Hintは取れるペアを示すだけですが、勝ち筋Hintはcheckerを通った全消しrouteの最初のペアだけを示します。
計測条件と初回Gate:144牌の負け履歴が残った
2026年9月21日の技術Gateでは、各layoutについて12 seed×4種類の分岐履歴、48履歴を測りました。生成時routeをそのまま進むだけでなく、最初からrandom、早い別分岐、遅い別分岐、Undo後の別分岐を含みます。
| 初回Gate・off-route履歴 | Bridge 72 | Turtle 144 |
|---|---|---|
| 全履歴の全Turn判定完了 | 47/48(97.92%) | 34/48(70.83%) |
| 負け終了履歴の判定完了 | 15/16 | 8/20(40%) |
| Hint UNKNOWN | 0/144 | 29/144(20.14%) |
| 全履歴Review p95 | 124.8ms | 4,166.0ms |
環境はApple M4、macOS arm64、32GiB、Chromium 153.0.8010.12 headlessの専用Workerです。時間はWorker内の処理で、生成・起動・IPCを含むゲーム画面全体の待ち時間ではありません。seedとpolicyには相関があり、一般ユーザーの成功率を推定する標本とは扱いません。
全体4.5秒のReview予算内に応答しても、未確定を返しただけなら全履歴解析は完了していません。負けた手順を振り返りたい場面で40%しか完了しないことは、製品の狙いに直結する制約でした。このTechnical GateでPARKしたのは、Turtle 144の任意分岐を含む履歴を必ず完全解析できる機能であり、144牌ゲームそのものではありません。Bridge限定なら候補でしたが、Turtleで全履歴の判定完了を約束する機能は保留しました。
profilerが示したのは、候補を並べるための反復だった
Turtleの負け20履歴をNode CPU profilerと呼出しカウンタで調べると、730 solve callsのうち428がUNKNOWN、free()は23,325,954回、unlocked()は7,489,530回呼ばれていました。Review累計51.79秒に対し、CPU self samplesでfree()が39.31秒を占めています。
原因は候補sortの比較関数でした。比較のたびに両候補のunlockedを計算し、その内部で変更前・合法性確認・変更後のfree牌列挙を繰り返していました。144牌をBigIntで走査する仕事が増え、短いsolveの時間予算を消費していたのです。
同じdealのdead memoをsolveごとに捨てていたことも、難しい境界付近の再探索につながっていました。ここでのprofiler値は原因を調べるためのinstrumented runです。次のbefore/afterには、計測用wrapperを外したChromium Workerの結果を使います。
one-passで変えたのは評価・証拠共有・予算配分
1回の重点最適化では、まず候補ごとの評価を1回に集約しました。合法に除去する2牌はどちらもfreeです。除去で他の牌が新たに塞がることはないため、子状態のfree枚数による順位は、新たにfreeになった枚数による順位と一致します。元の同率順序を保ち、新しい枝刈りは追加しませんでした。
次にdeal内のproof sessionを作り、完全に証明済みのdead-stateと、検証済みWIN routeのsuffixを共有しました。UNKNOWNで終わった探索でも、その途中で完全に探索できた部分木は再利用できます。ただし、未完了の親はLOSSとして扱いません。別dealの証拠が混ざらないよう、sessionの結び付けと牌データのcopy・freezeも行います。
Reviewの順序も変えました。checkerで確認した合法remove edgeの区間について、未確定区間の中点を調べ、UNKNOWNなら勝ち負けの方向を決めず別の未試行局面へ進みます。その後、120ms・15,000 visitsから予算を3倍ずつ増やし、1回最大1,440ms、全体4,500msを予算として打ち切ります。時間は一定の探索間隔で確認するsoft limitなので、実測が予算をわずかに超える場合があります。
勝ちの証拠は合法な実履歴をrouteへ前置して祖先へ伝えられます。負けの証拠は合法removeの子へ伝えられます。Undo自体をその伝播edgeにはしません。履歴という見た目が連続していても、証拠が使える関係を区別する必要があります。
なお、これは同じメモリ条件の比較ではありません。元の1状態10,000 memoから、deal内共有200,000 dead entriesと4,096 WIN suffixへ枠を広げ、時間配分も変更しています。
比較結果:改善したが、厳しいGateには届かなかった
同じ履歴をbefore/afterで実行し、実行順を交互に反転して比較しました。p95はUNKNOWN応答も含むnearest-rankです。初回Gateの値と、比較実験のbefore値には実行時の差があるため、混ぜずに示します。
| Turtle・既存48履歴の比較実験 | Before | After | Gate |
|---|---|---|---|
| 全履歴完了 | 34/48(70.83%) | 42/48(87.50%) | 95%以上 |
| 負け履歴完了 | 8/20(40.00%) | 14/20(70.00%) | 90%以上 |
| Review p95 | 4,231.0ms | 4,500.7ms | 5,000ms以下 |
| Hint p95 | 901.5ms | 239.1ms | 1,000ms以下 |
| Hint UNKNOWN | 29/144(20.14%) | 14/144(9.72%) | 5%以下 |
Hintのp95は短くなりましたが、Reviewのp95は短くなっていません。予算を使って以前は確定しなかった局面へ取り組むためです。速度だけを見て最適化の成否を判断できない例でした。
結果を見てから都合のよい履歴を選ばないよう、別seedで各layout208履歴のholdoutも固定しました。各layout54 distinct dealsで、random/early/late/undoに加え、同種4枚から違うペアを選ぶ履歴を含みます。
| Turtle・holdout208履歴 | Before | After |
|---|---|---|
| 全履歴完了 | 158/208(75.96%) | 184/208(88.46%) |
| 負け履歴完了 | 37/83(44.58%) | 60/83(72.29%) |
| Review p95 | 4,128.6ms | 4,500.9ms |
| Hint p95 | 901.1ms | 231.5ms |
| Hint UNKNOWN | 93/624(14.90%) | 62/624(9.94%) |
afterで残ったHint UNKNOWNは既存集合14件・holdout62件ともnode上限でした。1visitあたりの仕事は軽くなっても、100,000 visitsで証明に届かない入力が残りました。全履歴95%、負け履歴90%、Hint UNKNOWN 5%以下のGateは、いずれも未達です。
Bridgeのholdout全履歴完了は205/208から208/208、Review p95は115.3msから14.6msへ改善しました。一方、Review中央値は3.2msから7.8msへ増えています。小さい探索では共有管理などの固定費が目立ち、すべての速度指標が改善したわけではありません。
正しさの検証と限界:未確定を混ぜない
最適化後も、元の独立oracle/checkerを変更せず照合しました。240小盤面の全subset、38,400状態では誤WINNABLE・誤UNSOLVABLEの観測は0です。paired結果の52,739 routeと25,116 remove edgesを独立再生し、両実装が確定した24,410 Turn statusesは一致しました。
この数字は有限の検証集合の証拠です。144牌のすべての局面に対して独立全探索を行ったわけではありません。大きい局面のspot checkではchecker側もUNKNOWNになる入力があり、それを一致確認済みへ足していません。
ablationでも、共有memoだけで完了数が改善したとはいえませんでした。負け20履歴の境界・適応探索は、共有なし・ありの両方で14履歴完了です。共有ありではUNKNOWN framesやvisits、中央値が減りましたが、複数の変更が重なる比較から個別要素の普遍的な高速化率は主張できません。
分かったこと:PARKの実験からbest-effortの製品へ
最適化実験の結論は、Turtle 144の完全解析を保証する機能をPARKのまま維持することでした。その後の実装では、solverの証明条件や予算を緩めず、製品が約束するReviewの範囲を変えています。初期配置に勝ち筋があるゲームを提供しつつ、途中履歴の判定はbest-effortと明示し、得られた証拠によって表示を3段階に分けました。
| Review | 得られた証拠 | 画面で約束すること |
|---|---|---|
| Level A | 実履歴の全局面が確定 | 証明された最初のWINNABLE→UNSOLVABLEの除去があれば、その境界と確認済み代替を示す |
| Level B | 一部UNKNOWN、かつWINNABLEの証拠あり | 最後の確認済み局面と未確定区間。最初の敗着は断定しない |
| Level C | 十分な解析証拠がない、Worker中止・不調など | 履歴、取得pair、新たに選べる牌、合法pair、通常Retryを残す |
画面が言えるのは「このペアで新たに2枚が選べるようになった」「別のペアには検証済み全消しrouteがある」といった事実です。解放枚数が多いから必ず勝てる、あるいは解放が少ないことが負けの原因だ、と飛躍させません。
Retryは完全な局面snapshotから新しいbranchを作り、元の履歴を残します。Level Bなら最後に勝ち筋を確認できた局面へ戻れますし、Level Cでも通常Retryは使えます。証明不足によってゲーム体験全体を失わせず、断定する説明だけを控える構成です。
待てることと、待っていると分かること
製品の最終受入では、Pixel実機でGive Up直後の待機表示が盤面下に隠れる問題も見つかっています。solverを速くする変更ではなく、操作付近のnoticeへ解析中の案内を出す修正を行いました。
記録された同一手順のWorker Reviewは修正前4,390ms、修正後4,361msで、いずれもLevel Bでした。この2サンプルを高速化の根拠にはしません。改善したのは、処理中であることがその場で分かる点です。
実装・受入・公開の記録を分けて読むと、「高い完了率を満たす全履歴解析」は未達のままでも、「未確定を明示して再挑戦できるゲーム」は成立しています。性能基準を黙って下げるのではなく、何を保証できる製品なのかを、画面と仕様の両方で変える判断でした。
4作品のまとめで紹介したReview→Retryの体験は、この制約を含んでいます。FreeCellも、勝利証明とUNKNOWNを別の状態空間で扱った例です。
高速化だけでは、144牌の途中局面をすべて確定する問題は解決しませんでした。そこでUNKNOWNをUNKNOWNのまま扱い、確認できた勝ち筋と未確定の区間を分けました。それでも履歴と局面を残せば、解析が完了しない場合にもRetryできる製品体験は作れます。完全解析の未達を隠さず、得られた証拠の範囲で再挑戦を支えることが、この実装の結論です。