Note
この記事でいう「初級・中級・上級」は、この解説記事を3段階に分けた読み物としての順番です。Color Sort本体のスタート画面には「初級・中級・上級・超上級」という4段階の選択肢があり(colorsort/index.htmlの実際の表記: 初級 7本(空2)・4層/中級 10本(空2)・5層/上級 15本(空3)・5層/超上級 14本(空2)・5層)、これはcolorsort/app.jsのLEVEL_PRESETS(色数・容量・同色を分ける数・空き筒数の組み合わせ)そのものです。この記事の初級・中級・上級は、あくまでこの記事が選んだ実例の解きやすさを指し、アプリ側の4段階とは対応しません(初級編の実例はアプリの「初級」と同じパラメータ(5色・4層・空2)ですが、中級編・上級編の実例はアプリの難度名とは異なる組み合わせを使います)。
結論。Color Sortでは、注ぎ先の一番上の色が注ぎ元の一番上の色と一致する注ぎは、その場では常に安全(盤面を壊さない)。 この記事の実例(5色・4層・7本、うち空筒2本)は、「同じ色が一番上に重なっている筒同士を見つけたら、迷わずまとめる」だけを繰り返すと、途中で一度も後戻りせずに最後まで解けるケースで、実際に15手すべてを実行し、隠れていた「?」も含めて完成(クリア判定)まで到達することを確認しました。
盤面の見方: 筒・色・容量、そして下2層の「?」
Color Sortの筒は、容量ぶんの色つき単位を積んだ縦長のスロットです。この記事の実例は容量4層・5色・空の筒2本、合計7本の盤面(Color Sort本体の「初級」設定と同じパラメータ)です。画面には各筒が縦に並び、下から積まれた色が見えますが、既定では各筒の一番下から2層ぶんが「?」として隠されています。隠れた色は、注ぎ出してその位置が筒の一番上に来ると初めて表示されます(colorsort/index.htmlの遊び方説明: 「下の方は色が隠れていますが、注ぎ出すと見えます」)。
隠すかどうかは表示だけの仕組みで、生成される問題そのもの・注ぎの合法性には一切影響しません。「全色表示(隠れた色もすべて見る)」というチェックボックスをオンにすると、隠さずに同じ問題を比較しながら遊べます。この記事の実例では、colorsort/engine.jsを直接呼び出して隠れているはずの色もあらかじめ確認したうえで手順を説明します(実際のプレイでは、これから示す色は注ぎが進むにつれて少しずつ「?」から確定色に変わっていきます)。
画面下には次の情報が常に表示されます。
◯ / ◯ 筒そろい: 色ごとに1本、容量ぴったり単色で埋まった筒の本数(空筒を除いた本数のうち何本が完成しているか)。- 経過時間、
◯ 回操作(実際に行った注ぎの回数)。
使える操作(合法手)
- 筒をタップして選ぶ→もう一度別の筒をタップして注ぐ(
colorsort/index.htmlの遊び方説明どおり)。選んだ筒をもう一度タップすると選択解除になります。 - 注げる条件(
colorsort/engine.jsのpourInfo): 注ぎ元が空でないこと、注ぎ先が満杯でないこと、注ぎ先が空でなければ注ぎ先の一番上の色が注ぎ元の一番上の色と一致していること。実際に移る枚数は、注ぎ元の一番上から続く同色のかたまりの枚数と、注ぎ先の残り容量の小さい方です(一部だけを選んで注ぐことはできません)。 - 「一手戻す」: 直前の1回の注ぎを取り消します。ただしその注ぎで筒を1本完成させていた場合は取り消せません(
colorsort/app.jsのコード中コメントより: 完成させた筒の中身を覗いてから元に戻す、という抜け道を防ぐため)。単に隠れた色を露出させただけの注ぎ(完成を伴わない)には、この制限はかかりません。 - 「1手のヒント」・「答えを見る(一手ずつ)」: どちらも内部では同じ処理を1回だけ呼び出します(
colorsort/app.jsのコード中コメント: 「ヒントも答えも、内部では今の盤面から見た次の一手を1回だけ進める同じ処理」)。「答えを見る」という名前ですが、最終盤面へ自動的に飛ぶわけではなく、押すたびに1手ずつ進みます。 - 「最初から」: その問題の初期配置に戻ります(問題自体は変わりません)。
回転や筒の追加・削除はありません。「全色分かれている」だけでは完成にならず、隠れていた「?」がすべて一度は露出していることも必要です(colorsort/engine.jsのisClearedはisSolvedとisFullyRevealedの両方をANDで判定)。
実例: 5色・4層・7本(空2)の盤面を、常に安全な注ぎだけで解く
numColors=5・容量4・空筒2・シード"notes-colorsort-easy-16"で生成した盤面です(E.generate({ numColors: 5, capacity: 4, duplicateColors: 0, emptyTubes: 2, seed: 'notes-colorsort-easy-16' })、5回目の試行でこの条件を満たしました)。色をA〜Eの記号で表します(Color Sort本体のパレットで色ごとに割り当てられている記号と同じです)。下から積まれた順(左が下・右が上)で、括弧内は既定で隠れる下2層です。
| 筒 | 内容(下→上) |
|---|---|
| 1 | (A, C), C, E |
| 2 | (B, D), B, A |
| 3 | (D, B), E, E |
| 4 | (E, A), A, D |
| 5 | (D, C), C, B |
| 6 | 空 |
| 7 | 空 |
この盤面はnodeLimitなしの探索でも一切の後戻りが要らないという性質を持ちます(E.solveをNode.js上で実行すると、探索したノード数(15)と実際の解の手数(15)が完全に一致し、探索が最初に試した候補が一度も外れませんでした)。つまり、各段階で「今すぐ注いでも損をしない」注ぎを選び続けるだけで、最後まで迷わず進めます。
手順
以下は実際にE.solveが返した15手を、E.pourGameで1手ずつ適用し、隠れた「?」の露出・完成判定まで含めて最後まで再現した結果です(実行して確認済み。「新たに露出した?」欄は、この注ぎによって初めて色が判明したidの色を示します)。
| 手 | 注ぎ | 移る色・枚数 | 新たに露出した? | 注ぎ後の筒の状態(下→上) |
|---|---|---|---|---|
| 1 | 筒3→筒6 | Eを2枚 | 筒3側でBが1個 | 1:ACCE 2:BDBA 3:DB 4:EAAD 5:DCCB 6:EE 7:(空) |
| 2 | 筒1→筒6 | Eを1枚 | なし | 1:ACC 2:BDBA 3:DB 4:EAAD 5:DCCB 6:EEE 7:(空) |
| 3 | 筒1→筒7 | Cを2枚 | 筒1側でAが2個 | 1:A 2:BDBA 3:DB 4:EAAD 5:DCCB 6:EEE 7:CC |
| 4 | 筒2→筒1 | Aを1枚 | なし | 1:AA 2:BDB 3:DB 4:EAAD 5:DCCB 6:EEE 7:CC |
| 5 | 筒2→筒3 | Bを1枚 | 筒2側でDが1個 | 1:AA 2:BD 3:DBB 4:EAAD 5:DCCB 6:EEE 7:CC |
| 6 | 筒4→筒2 | Dを1枚 | なし | 1:AA 2:BDD 3:DBB 4:EAA 5:DCCB 6:EEE 7:CC |
| 7 | 筒4→筒1 | Aを2枚 | 筒4側でE・Aが1個ずつ | 1:AAAA(完成) 2:BDD 3:DBB 4:E 5:DCCB 6:EEE 7:CC |
| 8 | 筒6→筒4 | Eを3枚 | なし | 1:AAAA 2:BDD 3:DBB 4:EEEE(完成) 5:DCCB 6:(空) 7:CC |
| 9 | 筒2→筒6 | Dを2枚 | 筒2側でBが1個 | 1:AAAA 2:B 3:DBB 4:EEEE 5:DCCB 6:DD 7:CC |
| 10 | 筒3→筒2 | Bを2枚 | 筒3側でDが1個 | 1:AAAA 2:BBB 3:D 4:EEEE 5:DCCB 6:DD 7:CC |
| 11 | 筒6→筒3 | Dを2枚 | なし | 1:AAAA 2:BBB 3:DDD 4:EEEE 5:DCCB 6:(空) 7:CC |
| 12 | 筒5→筒2 | Bを1枚 | なし | 1:AAAA 2:BBBB(完成) 3:DDD 4:EEEE 5:DCC 6:(空) 7:CC |
| 13 | 筒5→筒6 | Cを2枚 | 筒5側でC・Dが1個ずつ | 1:AAAA 2:BBBB 3:DDD 4:EEEE 5:D 6:CC 7:CC |
| 14 | 筒3→筒5 | Dを3枚 | なし | 1:AAAA 2:BBBB 3:(空) 4:EEEE 5:DDDD(完成) 6:CC 7:CC |
| 15 | 筒6→筒7 | Cを2枚 | なし | 1:AAAA 2:BBBB 3:(空) 4:EEEE 5:DDDD 6:(空) 7:CCCC(完成) |
15手を終えた時点で、E.isSolved(全筒が色ごとに分かれている)・E.isFullyRevealed(隠れていた10個の「?」がすべて露出済み)・E.isCleared(両方を満たす)のすべてがtrueになることを確認しました。最終盤面は、筒1=A4枚・筒2=B4枚・筒3=空・筒4=E4枚・筒5=D4枚・筒6=空・筒7=C4枚です。
この盤面で「安全な注ぎ」がなぜ毎回1つに絞れたか
この盤面では、各手の時点で同じ色が一番上に重なっている筒の組が実際には複数存在する場面もありますが、そのどれを選んでも、最終的な色の集まり方自体は変わりません(同色を合流させる注ぎは、どちらを先にやっても色の行き先を変えないため)。これは次の中級編で扱う「一見同じに見えて、片方だけが手詰まりへ向かう」ケースとは異なります。この記事の実例で常に安全だったのは、たまたま色の配置がそうなっていたからであり、Color Sortの注ぎ全般が常に安全という意味ではありません(中級編・上級編で反例を示します)。
誤りやすい判断
- 「答えを見る」を押しても、最後まで自動で解いてくれるわけではない。 名前とは裏腹に、1回押すごとに1手だけ進みます(
colorsort/app.jsの実装どおり)。最後まで進めるには、クリアするまで押し続ける必要があります。 - 「一手戻す」が使えないのは、直前の注ぎが筒を1本完成させていたときだけ。 隠れた色を露出させただけの注ぎ(完成を伴わない)はいつでも戻せます。「戻せない=何かを間違えた」ではありません。
- 色を筒ごとに分けただけではクリアにならない。 隠れていた「?」が一度も露出しないまま残っていると、
isClearedはfalseのままです(この記事の実例では、15手のうちの各所で自然に全「?」が露出しましたが、常にそうなるとは限りません。隠れた色が残った状態で色だけ揃うと、colorsort/engine.jsのfindRevealMoveが代わりに露出用の一手を提案する仕組みがあります)。
読者の検証方法
- 同じ盤面は、色数5・容量4・空筒2・シード
notes-colorsort-easy-16で誰でも再現できます。Color Sort本体の「同じ問題を再現する」欄に該当パラメータのシードを入力するか(現在の起動画面は色数固定の難易度プリセットのみですが)、colorsort/engine.jsをrequireしてE.generate({ numColors: 5, capacity: 4, duplicateColors: 0, emptyTubes: 2, seed: 'notes-colorsort-easy-16' })を実行すれば、この記事と同じtubes配列が得られます。 - 手順が合っているか不安な場合は、1手ごとに「1手のヒント」で答え合わせしながら進められます。
- 自分で試す場合は、
E.solve(tubes, 4, 20000).movesで全15手を、E.pourGameで1手ずつ適用しながらE.isCleared(state, 4)が最後にtrueになることを確認できます。
この記事が確認していないこと
- 「常に安全な注ぎだけで最後まで解ける」という展開は、この記事の実例(このシード・このパラメータ)固有のものです。Color Sortには盤面ごとの解きやすさを事前に判定する仕組み(
analyzeDifficultyのような関数)が実装されていないため、同じパラメータの別シードで同じ展開になることは保証されません(実際、この記事を書く過程で試した同パラメータの他のシードの多くは、nodesがsolutionLengthより大きく、探索の途中で後戻りが必要でした。中級編・上級編はそのようなケースを扱います)。 - 「同じ色が一番上に重なっている筒同士を優先してまとめる」という進め方が、あらゆる盤面で安全だとは主張していません。中級編(その2)で、この進め方が通用しない反例を扱います。上級編(その3)は色数の多い盤面でさらに深い場所で判断が必要になる場面を扱います。
この結果が言っていないこと
- 検証は
colorsort/engine.js・colorsort/app.js(いずれも記事執筆時点、生成結果のversion: 2で確認)を対象にしています。将来の版で表示・判定基準が変わる可能性があります。 - 表中の「気づき」の説明は、筆者が
E.pourGameの実行結果(tubes・everRevealedの変化)を読んで書き下したものです。Color Sort本体の画面上にはこのような手ごとの解説は表示されません。