Files
NumberSwipe_New/campaign-50/NumberSwipe-50关DEMO.html
T

900 lines
108 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#ffffff"><title>数字一笔 · 53关DEMO</title><style>/* 数字一笔 · Web 壳样式。令牌与组件规格见仓库 docs/design.md(改令牌先改文档,再同步 src/theme.ts)。 */
:root {
/* ---- 颜色 ---- */
--bg: #ffffff;
--bg-app: #f2f2f6;
--surface-2: #f3f3f7;
--cell: #e6e7ec;
--line: #e1e2ea;
--ink: #23243a;
--ink-2: #6b6d80;
--ink-3: #a3a5b5;
--primary: #6558f5;
--primary-bright: #7d72ff;
--primary-soft: #eceafe;
--on-primary: #ffffff;
--danger: #e5484d;
--danger-soft: #fdecec;
--hint: #f5a524;
--hint-soft: #fef3dd;
--clue-ring: #23243a;
--overlay: rgba(35, 36, 58, 0.92);
--shadow-primary: 0 6px 18px rgba(101, 88, 245, 0.28);
--shadow-primary-pressed: 0 3px 10px rgba(101, 88, 245, 0.22);
--shadow-frame: 0 20px 60px rgba(20, 20, 40, 0.14);
/* ---- 字体 ---- */
--font: ui-rounded, "SF Pro Rounded", "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", system-ui, -apple-system, "Segoe UI", sans-serif;
--fs-display: 34px;
--fs-title: 22px;
--fs-heading: 17px;
--fs-body: 15px;
--fs-label: 14px;
--fs-caption: 13px;
--fs-micro: 12px;
/* ---- 圆角 ---- */
--r-sm: 10px;
--r-md: 14px;
--r-lg: 18px;
--r-xl: 24px;
--r-pill: 999px;
/* ---- 间距(4pt 网格) ---- */
--sp-1: 4px;
--sp-2: 8px;
--sp-3: 12px;
--sp-4: 16px;
--sp-5: 20px;
--sp-6: 24px;
--sp-7: 28px;
/* ---- 动效 ---- */
--t-fast: 120ms;
--t-base: 200ms;
--t-slow: 320ms;
--t-hold: 500ms;
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
/* ---- 尺寸 ---- */
--tap: 44px;
--safe-top: env(safe-area-inset-top, 0px);
--safe-bottom: env(safe-area-inset-bottom, 0px);
--safe-left: env(safe-area-inset-left, 0px);
--safe-right: env(safe-area-inset-right, 0px);
}
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
html,
body {
margin: 0;
height: 100%;
background: var(--bg-app);
color: var(--ink);
font-family: var(--font);
font-size: var(--fs-body);
line-height: 1.3;
-webkit-font-smoothing: antialiased;
font-variant-numeric: tabular-nums;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
overscroll-behavior: none;
}
#app {
position: relative;
margin: 0 auto;
width: 100%;
max-width: 480px;
height: 100vh;
height: 100dvh;
background: var(--bg);
overflow: hidden;
}
.screen {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
padding: calc(var(--safe-top) + var(--sp-3)) calc(var(--safe-right) + var(--sp-5)) calc(var(--safe-bottom) + var(--sp-4))
calc(var(--safe-left) + var(--sp-5));
}
.screen.hidden {
display: none;
}
.screen.enter {
animation: screen-in var(--t-base) var(--ease-out) both;
}
button {
font: inherit;
color: inherit;
background: none;
border: 0;
padding: 0;
cursor: pointer;
-webkit-touch-callout: none;
transition: transform var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out),
box-shadow var(--t-fast) var(--ease-out), opacity var(--t-fast) var(--ease-out);
}
button:active {
transform: scale(0.97);
}
button:focus-visible {
outline: 3px solid var(--primary);
outline-offset: 2px;
}
/* ---------- 选关 ---------- */
.select-header {
padding: var(--sp-7) var(--sp-1) var(--sp-2);
}
.select-header h1 {
margin: 0;
font-size: var(--fs-display);
font-weight: 800;
letter-spacing: 0.02em;
line-height: 1.15;
}
.select-header .sub {
margin: var(--sp-2) 0 0;
color: var(--ink-2);
font-size: var(--fs-body);
font-weight: 500;
}
.overall {
display: flex;
align-items: center;
gap: var(--sp-3);
margin-top: var(--sp-4);
}
.overall-bar {
flex: 1;
height: 6px;
border-radius: var(--r-pill);
background: var(--cell);
overflow: hidden;
}
.overall-bar i {
display: block;
height: 100%;
width: 0;
border-radius: var(--r-pill);
background: var(--primary);
transition: width var(--t-slow) var(--ease-out);
}
.overall-text {
color: var(--ink-3);
font-size: var(--fs-caption);
font-weight: 600;
min-width: 52px;
text-align: right;
}
.pack-list {
flex: 1;
overflow-y: auto;
margin-top: var(--sp-3);
padding-bottom: var(--sp-5);
/* 继续按钮固定在底部,包列表自己滚;底部 20px 渐隐,避免被按钮“切断” */
min-height: 0;
-webkit-mask-image: linear-gradient(to bottom, transparent, #000 12px, #000 calc(100% - 20px), transparent);
mask-image: linear-gradient(to bottom, transparent, #000 12px, #000 calc(100% - 20px), transparent);
}
.pack {
margin-top: var(--sp-7);
}
.pack-list > .pack:first-child {
margin-top: var(--sp-2);
}
.pack-title {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 0 var(--sp-1) var(--sp-3);
font-size: var(--fs-heading);
font-weight: 700;
}
.pack-progress {
color: var(--ink-3);
font-size: var(--fs-label);
font-weight: 600;
}
.pack.done .pack-progress {
color: var(--primary);
}
.level-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: var(--sp-3);
}
.level-tile {
position: relative;
aspect-ratio: 1;
border-radius: var(--r-md);
background: var(--cell);
color: var(--ink);
font-size: 20px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.level-tile:active {
transform: scale(0.94);
}
.level-tile.cleared {
background: var(--primary);
color: var(--on-primary);
}
/* 已过关:右上角一个小勾(纯 CSS 画,不依赖字体里的 ✓) */
.level-tile.cleared::after {
content: "";
position: absolute;
right: 8px;
top: 6px;
width: 4px;
height: 8px;
border: solid rgba(255, 255, 255, 0.9);
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.level-tile.current {
box-shadow: inset 0 0 0 3px var(--primary);
background: var(--primary-soft);
color: var(--primary);
animation: breathe 2.4s var(--ease-in-out) infinite;
}
.level-tile.locked {
color: var(--ink-3);
background: var(--surface-2);
}
.level-tile.locked::before {
content: "";
width: 10px;
height: 12px;
border: 2px solid var(--ink-3);
border-radius: 3px;
border-top-width: 3px;
}
.level-tile.locked span {
display: none;
}
.level-tile.shake {
animation: shake var(--t-slow) var(--ease-out);
}
.primary {
margin-top: auto;
height: 54px;
border-radius: var(--r-lg);
background: var(--primary);
color: var(--on-primary);
font-size: var(--fs-heading);
font-weight: 700;
letter-spacing: 0.02em;
box-shadow: var(--shadow-primary);
}
.primary:active {
transform: scale(0.98);
box-shadow: var(--shadow-primary-pressed);
}
.select-footer {
display: flex;
justify-content: center;
gap: var(--sp-2);
padding-top: var(--sp-4);
}
.chip {
height: 34px;
padding: 0 var(--sp-4);
border-radius: var(--r-pill);
background: var(--surface-2);
color: var(--ink-2);
font-size: var(--fs-caption);
font-weight: 600;
}
.chip.on {
background: var(--primary-soft);
color: var(--primary);
}
.chip.danger {
color: var(--danger);
}
/* ---------- 对局 ---------- */
.topbar {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
height: 52px;
}
.topbar .icon {
justify-self: start;
width: var(--tap);
height: var(--tap);
display: flex;
align-items: center;
justify-content: center;
color: var(--ink-2);
border-radius: var(--r-pill);
margin-left: -8px;
}
.topbar .icon:active {
background: var(--surface-2);
}
.level-info {
text-align: center;
line-height: 1.1;
}
.level-title {
font-size: var(--fs-title);
font-weight: 800;
letter-spacing: 0.02em;
}
.level-sub {
margin-top: 3px;
color: var(--ink-3);
font-size: var(--fs-micro);
font-weight: 600;
letter-spacing: 0.04em;
white-space: nowrap;
}
.remaining {
justify-self: end;
display: inline-flex;
align-items: center;
gap: 5px;
height: 32px;
padding: 0 var(--sp-3) 0 10px;
border-radius: var(--r-pill);
background: var(--surface-2);
color: var(--ink-2);
font-size: var(--fs-label);
font-weight: 700;
min-width: 52px;
justify-content: center;
transition: background-color var(--t-base) var(--ease-out), color var(--t-base) var(--ease-out);
}
.remaining-icon {
color: var(--ink-3);
transition: color var(--t-base) var(--ease-out);
}
.remaining-icon.check {
display: none;
}
.remaining.bump {
animation: bump var(--t-base) var(--ease-spring);
}
.remaining.done {
background: var(--primary-soft);
color: var(--primary);
}
.remaining.done .remaining-icon {
color: var(--primary);
}
.remaining.done .remaining-icon.tile {
display: none;
}
.remaining.done .remaining-icon.check {
display: block;
}
/* 本包进度条:每关一段。灰 = 未到,淡紫 = 已过,实紫 = 当前 */
.level-progress {
display: flex;
gap: 3px;
height: 4px;
margin: var(--sp-2) var(--sp-1) 0;
}
.level-progress i {
flex: 1;
border-radius: 2px;
background: var(--cell);
transition: background-color var(--t-slow) var(--ease-out), opacity var(--t-slow) var(--ease-out);
}
.level-progress i.cleared {
background: var(--primary);
opacity: 0.4;
}
.level-progress i.current {
background: var(--primary);
opacity: 1;
}
.caption {
height: 36px;
margin-top: var(--sp-2);
display: flex;
align-items: center;
justify-content: center;
color: var(--ink-2);
font-size: var(--fs-body);
font-weight: 500;
text-align: center;
transition: opacity var(--t-base) var(--ease-out);
}
.caption:empty {
opacity: 0;
}
.board-wrap {
flex: 1;
min-height: 0;
position: relative;
}
#board {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
touch-action: none;
display: block;
}
.bottombar {
display: flex;
justify-content: center;
gap: var(--sp-3);
padding-top: var(--sp-3);
}
.gesture-help {
margin: 0;
padding-top: var(--sp-2);
color: var(--ink-2);
font-size: var(--fs-caption);
text-align: center;
}
.action {
flex: 1;
max-width: 170px;
height: 56px;
border-radius: var(--r-lg);
background: var(--surface-2);
color: var(--ink);
font-size: var(--fs-body);
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
gap: var(--sp-1);
}
.action svg {
color: var(--ink-2);
}
.action:active {
background: var(--cell);
}
.action.pulse {
animation: pulse 0.9s var(--ease-in-out) infinite;
}
.action:disabled {
opacity: 0.4;
pointer-events: none;
}
.toast {
position: absolute;
left: 50%;
bottom: calc(var(--safe-bottom) + 128px);
transform: translate(-50%, 8px);
padding: 10px var(--sp-4);
border-radius: var(--r-md);
background: var(--overlay);
color: #fff;
font-size: var(--fs-label);
font-weight: 600;
width: max-content;
max-width: calc(100% - 40px);
text-align: center;
opacity: 0;
pointer-events: none;
transition: opacity var(--t-base) var(--ease-out), transform var(--t-base) var(--ease-out);
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- 动效 ---------- */
@keyframes screen-in {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
50% {
transform: translateX(5px);
}
75% {
transform: translateX(-3px);
}
}
@keyframes pulse {
0%,
100% {
box-shadow: 0 0 0 0 rgba(245, 165, 36, 0);
}
50% {
box-shadow: 0 0 0 5px rgba(245, 165, 36, 0.35);
}
}
@keyframes bump {
0% {
transform: scale(1);
}
40% {
transform: scale(1.14);
}
100% {
transform: scale(1);
}
}
@keyframes breathe {
0%,
100% {
box-shadow: inset 0 0 0 3px var(--primary), 0 0 0 0 rgba(101, 88, 245, 0);
}
50% {
box-shadow: inset 0 0 0 3px var(--primary), 0 0 0 6px rgba(101, 88, 245, 0.16);
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation: none !important;
transition: none !important;
}
}
@media (min-width: 520px) {
body {
padding: var(--sp-6) 0;
}
#app {
height: calc(100vh - 48px);
height: calc(100dvh - 48px);
border-radius: 28px;
box-shadow: var(--shadow-frame);
}
}
/* DEMO additions: preserve the original token and component system. */
#constraints{display:flex;gap:6px;flex-wrap:wrap;justify-content:center;margin:4px 0 0;min-height:0;flex-shrink:0}#constraints .chip{height:28px;min-height:28px;padding:0 10px;font-size:12px}.level-sub{max-width:240px;overflow:hidden;text-overflow:ellipsis}.caption{flex-shrink:0;height:36px;padding:0 4px}.topbar,.level-progress,.bottombar,.gesture-help{flex-shrink:0}#board-wrap{margin-top:8px}.next-overlay{position:absolute;bottom:calc(var(--safe-bottom) + 114px);left:50%;transform:translateX(-50%);padding:0 22px;min-width:160px;height:44px;margin:0;z-index:3}.next-overlay[hidden]{display:none}.next-overlay:active{transform:translateX(-50%) scale(.98)}#status{z-index:4}.rules-button{position:absolute;right:20px;top:calc(var(--safe-top) + 68px);background:none;font-size:12px;color:var(--ink-2);padding:3px}.sheet-backdrop{position:absolute;inset:0;background:rgba(35,36,58,.36);z-index:6;display:flex;align-items:flex-end}.sheet-backdrop[hidden]{display:none}.sheet{width:100%;max-height:75%;overflow:auto;background:var(--bg);border-radius:24px 24px 0 0;padding:24px 20px calc(20px + var(--safe-bottom))}.sheet h2{font-size:22px;font-weight:800;margin:0 0 12px}.sheet p{font-size:15px;line-height:1.7;color:var(--ink-2)}.sheet textarea{width:100%;min-height:100px;padding:12px;border:1px solid var(--line);border-radius:14px;font:inherit;user-select:text}.sheet .sheet-actions{display:flex;gap:8px;margin-top:16px}.sheet .action{height:48px}.sheet .primary{height:48px;padding:0 16px;margin:0}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}.select-footer .chip{min-height:44px;height:44px}.level-tile{min-height:44px}.select-footer a{display:flex;align-items:center;text-decoration:none;color:var(--ink-2)}@media(max-height:660px){.select-header{padding-top:8px}.caption{font-size:13px;height:30px}#constraints .chip{height:24px;min-height:24px}.bottombar{padding-top:8px}.action{height:48px}.gesture-help{font-size:12px}.screen{padding-top:calc(var(--safe-top) + 8px)}}
/* Responsive campaign feedback and controls. Original tokens stay intact. */
.feedback-row{display:flex;align-items:center;gap:8px;flex-shrink:0;min-height:58px;padding:6px 0;width:100%}
#status{position:static;left:auto;bottom:auto;transform:none;flex:1;min-width:0;width:auto;max-width:none;margin:0;padding:8px 12px;font-size:13px;line-height:1.45;overflow-wrap:anywhere;transition:opacity var(--t-base) var(--ease-out)}
#status.show{transform:none}
#next{position:static;left:auto;bottom:auto;transform:none;flex:0 0 auto;min-width:108px;height:44px;margin:0;padding:0 14px}
#next:active{transform:scale(.98)}
.rules-button{position:static;right:auto;top:auto;flex:0 0 44px;width:44px;min-height:44px;border-radius:var(--r-sm);font-size:13px;padding:4px;order:3}
.topbar .remaining{order:2}.topbar .level-info{min-width:0}.level-sub{max-width:100%;white-space:nowrap}
#constraints .chip{min-height:32px;height:auto;padding:6px 10px;line-height:1.4;max-width:100%;text-align:center}
#constraints{gap:4px}.caption{height:auto;min-height:32px;line-height:1.4;margin-top:4px;padding:4px 0}
#board-wrap{margin-top:4px}#board{touch-action:none}.sheet textarea{font-size:16px}
@media(max-width:430px){.screen{padding-left:calc(var(--safe-left) + 12px);padding-right:calc(var(--safe-right) + 12px)}.topbar{gap:6px}.level-title{font-size:20px}.level-sub{font-size:11px}.gesture-help{font-size:11px;padding-top:2px}.bottombar{gap:8px;padding-top:6px}.caption{font-size:13px}.feedback-row{min-height:54px}.select-header h1{font-size:30px}.sheet{padding-left:16px;padding-right:16px}}
@media(max-height:680px){#constraints .chip{min-height:28px;padding:4px 8px}.caption{min-height:28px}.feedback-row{min-height:50px}#status{padding:6px 10px;font-size:12px}.action{height:48px}.screen{padding-top:calc(var(--safe-top) + 8px)}.select-header{padding-top:8px}}
@media(orientation:landscape) and (max-height:520px) and (min-width:560px){body{padding:0}#app{max-width:none;height:100dvh;border-radius:0;box-shadow:none}#screen-play:not(.hidden){display:grid;grid-template-columns:minmax(0,1fr) 220px;grid-template-rows:48px 10px auto auto minmax(0,1fr) auto auto;column-gap:16px;row-gap:4px}.topbar{grid-column:1/-1;grid-row:1}.level-progress{grid-column:1/-1;grid-row:2;margin:0}.caption{grid-column:2;grid-row:3}#constraints{grid-column:2;grid-row:4}#board-wrap{grid-column:1;grid-row:3/8;margin:0;min-height:0}.feedback-row{grid-column:2;grid-row:5;flex-direction:column;justify-content:flex-end;align-items:stretch}.gesture-help{grid-column:2;grid-row:6}.bottombar{grid-column:2;grid-row:7}.bottombar .action{height:44px;font-size:12px;gap:2px}.bottombar svg{width:18px;height:18px}}
@media(max-height:600px) and (orientation:portrait){.gesture-help{display:none}.level-progress{margin-top:6px;margin-bottom:4px}.caption{min-height:24px;font-size:12px;padding:2px 0}.feedback-row{min-height:48px}.bottombar{padding-top:4px}.action{height:44px}.topbar{min-height:44px}.screen{padding-bottom:calc(var(--safe-bottom) + 8px)}}
.sound-button{min-width:44px;min-height:44px;font-size:12px;color:var(--ink-2);border-radius:var(--r-sm);flex:0 0 44px;order:4}.sound-button[aria-pressed="false"]{color:var(--ink-3)}
@media(max-width:430px){.topbar{gap:3px}.rules-button,.sound-button{flex-basis:44px}.topbar .level-title{font-size:18px}.topbar .remaining{padding:6px 8px}.level-sub{font-size:10px}}
</style></head><body><div id="app">
<section id="screen-select" class="screen"><header class="select-header"><h1 id="title">数字一笔</h1><p class="sub">53关DEMO · 一笔走遍每一格</p><div class="overall"><div class="overall-bar"><i id="overall-fill"></i></div><span id="progress" class="overall-text">0 / 53</span></div></header><div id="pack-list" class="pack-list"></div><button id="btn-continue" class="primary">继续</button><footer class="select-footer"><a href="../index.html" class="chip">原型入口</a><button id="btn-feedback" class="chip">试玩反馈</button></footer></section>
<section id="screen-play" class="screen hidden"><header class="topbar"><button id="btn-back" class="icon" aria-label="返回选关"><svg viewBox="0 0 24 24" width="26" height="26" aria-hidden="true"><path d="M15 5l-7 7 7 7" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg></button><div class="level-info"><div id="position" class="level-title"></div><div id="level-sub" class="level-sub"></div></div><button id="btn-sound" class="sound-button" aria-label="切换音效" aria-pressed="true">音效开</button><button id="btn-rules" class="rules-button" aria-label="查看本关规则">规则</button><div id="remaining" class="remaining" aria-live="polite"><svg class="remaining-icon tile" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><rect x="4" y="4" width="16" height="16" rx="4.5" fill="none" stroke="currentColor" stroke-width="2.4"/></svg><svg class="remaining-icon check" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"/></svg><span id="remaining-n">0</span></div></header><div id="level-progress" class="level-progress" aria-hidden="true"></div><div id="caption" class="caption"></div><div id="constraints" aria-label="当前规则"></div><div id="board-wrap" class="board-wrap"><canvas id="board" aria-label="拖动或点按共边格画路"></canvas></div><div class="feedback-row" aria-label="操作反馈与下一关"><div id="status" class="toast" role="status" aria-live="polite"></div><button id="next" class="primary next-overlay" hidden>下一关 →</button></div><h2 id="goal" class="sr-only"></h2><p class="gesture-help">按住滑动连线 · 沿来时路逐格回退</p><footer class="bottombar"><button id="restart" class="action"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path d="M4 12a8 8 0 1 0 2.5-5.8M4 4v5h5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>重开</span></button><button id="undo" class="action"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path d="M9 5l-5 5 5 5M4 10h10a6 6 0 0 1 0 12" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>撤销</span></button><button id="hint" class="action"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path d="M5 19v-7a4 4 0 0 1 4-4h10m-4-4 4 4-4 4" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>指路</span></button></footer></section>
<div id="rules-sheet" class="sheet-backdrop" hidden><section class="sheet" role="dialog" aria-label="本关规则"><h2>本关规则</h2><p id="rule-text"></p><div class="sheet-actions"><button id="close-rules" class="action">返回游戏</button></div></section></div>
<div id="feedback-sheet" class="sheet-backdrop" hidden><section class="sheet" role="dialog" aria-label="试玩反馈"><h2>试玩反馈</h2><p>记下有趣、枯燥、太难或规则不清楚的关卡,导出记录后用于调整。</p><textarea id="reasoning" aria-label="本关反馈" placeholder="例如:第18关有趣;第24关太难……"></textarea><div class="sheet-actions"><button id="close-feedback" class="action">关闭</button><button id="export" class="primary">导出全部记录</button></div></section></div>
</div><script>const PACK={"mode":"campaign-50","version":"demo-2026-10-14-v2","title":"数字一笔 · 53关DEMO","chapters":[{"id":1,"title":"覆盖、大格与墙","introduced":["big","walls"],"focus":"结束操作教学,建立计步与区域收尾","peak":8},{"id":2,"title":"方向与双色","introduced":["direction","alternation"],"focus":"明确进出边,再规划异色续接","peak":18},{"id":3,"title":"奇偶与步进门","introduced":["parity","rhythm-door"],"focus":"真实步号与到达状态,五关单元引入","peak":28},{"id":4,"title":"六边迁移与起点推理","introduced":["hex","unknown-start"],"focus":"旧规则迁移到六向几何,适度减少起点信息","peak":38},{"id":5,"title":"组合应用与收束","introduced":[],"focus":"增加推理依赖,不继续增加规则","peak":48}],"rule":"每格一次,数字是访问步号。每关只启用显示的规则;大格整块一步,墙不能穿过,双色每步换色,奇偶看步号,通道按图示两边进出,门通过前每落一格切换,通过后保持开启。","levels":[{"id":"21ceadc1-cd0e-511b-b7d3-d227e01de8a1","contentRevision":1,"order":1,"mode":"campaign-50","geometry":"square","features":[],"title":"第一笔","chapter":1,"chapterTitle":"覆盖、大格与墙","role":"演示/热身","targetDifficulty":12,"caption":"从1开始,走遍全部格,最后到5。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":1,"h":1,"clue":5},{"id":"c3","x":0,"y":1,"w":1,"h":1},{"id":"c4","x":1,"y":1,"w":1,"h":1,"clue":1}]},{"id":"de71e06d-ed2b-5371-a3dd-7089ccff10b3","contentRevision":1,"order":2,"mode":"campaign-50","geometry":"square","features":[],"title":"先收短支路","chapter":1,"chapterTitle":"覆盖、大格与墙","role":"练习","targetDifficulty":16,"caption":"走错沿来时路逐格退回,松手也能继续画。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1,"clue":1},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":0,"y":1,"w":1,"h":1},{"id":"c4","x":1,"y":1,"w":1,"h":1},{"id":"c5","x":2,"y":1,"w":1,"h":1},{"id":"c6","x":0,"y":2,"w":1,"h":1,"clue":6},{"id":"c7","x":1,"y":2,"w":1,"h":1}]},{"id":"f521de33-0d27-54c1-9c04-8fe43955132c","contentRevision":1,"order":3,"mode":"campaign-50","geometry":"blocks","features":["big"],"title":"大块一步","chapter":1,"chapterTitle":"覆盖、大格与墙","role":"变化","targetDifficulty":20,"caption":"整块大格只算一步。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":0,"y":1,"w":1,"h":1,"clue":1},{"id":"c4","x":1,"y":1,"w":2,"h":2},{"id":"c5","x":0,"y":2,"w":1,"h":1},{"id":"c6","x":0,"y":3,"w":1,"h":1},{"id":"c7","x":1,"y":3,"w":1,"h":1,"clue":6}]},{"id":"07323e0e-de6c-5d71-8876-e3d88d008b60","contentRevision":1,"order":4,"mode":"campaign-50","geometry":"blocks","features":["big"],"title":"枢纽留后","chapter":1,"chapterTitle":"覆盖、大格与墙","role":"组合","targetDifficulty":22,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1,"clue":5},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":3,"y":0,"w":1,"h":1,"clue":1},{"id":"c4","x":0,"y":1,"w":1,"h":1},{"id":"c5","x":1,"y":1,"w":1,"h":1},{"id":"c6","x":2,"y":1,"w":2,"h":2},{"id":"c7","x":0,"y":2,"w":1,"h":1},{"id":"c8","x":1,"y":2,"w":1,"h":1},{"id":"c9","x":0,"y":3,"w":1,"h":1}]},{"id":"0b255856-d185-5314-85e3-4a95eb233b04","contentRevision":1,"order":5,"mode":"campaign-50","geometry":"blocks","features":["big"],"title":"认清异形共边","chapter":1,"chapterTitle":"覆盖、大格与墙","role":"巩固","targetDifficulty":26,"caption":"异形也是整块一步,只看真实共边。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":2,"h":2,"shape":[[0,0],[0,1],[1,0]]},{"id":"c3","x":0,"y":1,"w":1,"h":1},{"id":"c4","x":1,"y":1,"w":1,"h":1},{"id":"c5","x":3,"y":1,"w":1,"h":1,"clue":7},{"id":"c6","x":0,"y":2,"w":1,"h":1},{"id":"c7","x":1,"y":2,"w":1,"h":1},{"id":"c8","x":2,"y":2,"w":1,"h":1},{"id":"c9","x":3,"y":2,"w":1,"h":1},{"id":"c10","x":0,"y":3,"w":1,"h":1,"clue":1}]},{"id":"23914286-0aa4-5d7a-afd1-b5a31c0c1a6e","contentRevision":1,"order":6,"mode":"campaign-50","geometry":"square","features":["walls"],"title":"边上的墙","chapter":1,"chapterTitle":"覆盖、大格与墙","role":"演示/迁移","targetDifficulty":30,"caption":"黑黄墙封住这条边,整局不会打开。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1,"clue":8},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":0,"y":1,"w":1,"h":1},{"id":"c4","x":1,"y":1,"w":1,"h":1},{"id":"c5","x":2,"y":1,"w":1,"h":1},{"id":"c6","x":0,"y":2,"w":1,"h":1,"clue":1},{"id":"c7","x":1,"y":2,"w":1,"h":1},{"id":"c8","x":2,"y":2,"w":1,"h":1}],"walls":[["c3","c6"]]},{"id":"e85cd2f3-94af-5b5e-bfd5-d513e6a5ba2c","contentRevision":1,"order":7,"mode":"campaign-50","geometry":"square","features":["walls"],"title":"近路要绕","chapter":1,"chapterTitle":"覆盖、大格与墙","role":"练习/挑战","targetDifficulty":35,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1,"clue":1},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":0,"y":1,"w":1,"h":1},{"id":"c4","x":1,"y":1,"w":1,"h":1},{"id":"c5","x":2,"y":1,"w":1,"h":1,"clue":6},{"id":"c6","x":0,"y":2,"w":1,"h":1},{"id":"c7","x":1,"y":2,"w":1,"h":1},{"id":"c8","x":2,"y":2,"w":1,"h":1},{"id":"c9","x":0,"y":3,"w":1,"h":1},{"id":"c10","x":1,"y":3,"w":1,"h":1}],"walls":[["c7","c10"]]},{"id":"a2cbcb61-034a-5215-9a31-035c1c0b96f5","contentRevision":1,"order":8,"mode":"campaign-50","geometry":"blocks","features":["big","walls"],"title":"第一座桥","chapter":1,"chapterTitle":"覆盖、大格与墙","role":"章节峰值","targetDifficulty":40,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":3,"y":0,"w":1,"h":1,"clue":11},{"id":"c4","x":0,"y":1,"w":1,"h":1},{"id":"c5","x":1,"y":1,"w":2,"h":2},{"id":"c6","x":3,"y":1,"w":1,"h":1},{"id":"c7","x":0,"y":2,"w":1,"h":1},{"id":"c8","x":3,"y":2,"w":1,"h":1},{"id":"c9","x":0,"y":3,"w":1,"h":1},{"id":"c10","x":1,"y":3,"w":1,"h":1},{"id":"c11","x":2,"y":3,"w":1,"h":1},{"id":"c12","x":3,"y":3,"w":1,"h":1,"clue":1}],"walls":[["c1","c5"],["c5","c10"]]},{"id":"c27d6782-9897-56e4-9024-27a725af609a","contentRevision":1,"order":9,"mode":"campaign-50","geometry":"square","features":["walls"],"title":"绕过去就好","chapter":1,"chapterTitle":"覆盖、大格与墙","role":"释放","targetDifficulty":24,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":0,"y":1,"w":1,"h":1},{"id":"c4","x":1,"y":1,"w":1,"h":1},{"id":"c5","x":2,"y":1,"w":1,"h":1,"clue":8},{"id":"c6","x":0,"y":2,"w":1,"h":1},{"id":"c7","x":1,"y":2,"w":1,"h":1},{"id":"c8","x":2,"y":2,"w":1,"h":1,"clue":1}],"walls":[["c1","c4"]]},{"id":"2a913cf0-2d97-512f-b8d8-bf4b859ac675","contentRevision":1,"order":10,"mode":"campaign-50","geometry":"blocks","features":["big","walls"],"title":"对称小花园","chapter":1,"chapterTitle":"覆盖、大格与墙","role":"收束","targetDifficulty":28,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":2,"h":2},{"id":"c3","x":0,"y":1,"w":1,"h":1},{"id":"c4","x":1,"y":1,"w":1,"h":1,"clue":7},{"id":"c5","x":0,"y":2,"w":1,"h":1},{"id":"c6","x":1,"y":2,"w":1,"h":1},{"id":"c7","x":2,"y":2,"w":1,"h":1},{"id":"c8","x":3,"y":2,"w":1,"h":1,"clue":1},{"id":"c9","x":0,"y":3,"w":1,"h":1},{"id":"c10","x":1,"y":3,"w":1,"h":1},{"id":"c11","x":2,"y":3,"w":1,"h":1}],"walls":[["c1","c2"]]},{"id":"9edd9ff2-ed54-5fd8-a0e5-8c477b3100bc","contentRevision":1,"order":11,"mode":"campaign-50","geometry":"square","features":["direction"],"title":"左右通道","chapter":2,"chapterTitle":"方向与双色","role":"演示/热身","targetDifficulty":24,"caption":"横通道只从左右进出,也可反向走。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1,"clue":1},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":0,"y":1,"w":1,"h":1,"clue":8},{"id":"c4","x":1,"y":1,"w":1,"h":1},{"id":"c5","x":2,"y":1,"w":1,"h":1},{"id":"c6","x":0,"y":2,"w":1,"h":1},{"id":"c7","x":1,"y":2,"w":1,"h":1,"direction":"lr"},{"id":"c8","x":2,"y":2,"w":1,"h":1}]},{"id":"852f0119-285e-5ec3-9569-8e34485bcf74","contentRevision":1,"order":12,"mode":"campaign-50","geometry":"square","features":["direction"],"title":"上下通道","chapter":2,"chapterTitle":"方向与双色","role":"练习","targetDifficulty":28,"caption":"竖通道只从上下进出。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":0,"y":1,"w":1,"h":1},{"id":"c4","x":1,"y":1,"w":1,"h":1,"direction":"du"},{"id":"c5","x":2,"y":1,"w":1,"h":1},{"id":"c6","x":0,"y":2,"w":1,"h":1},{"id":"c7","x":1,"y":2,"w":1,"h":1},{"id":"c8","x":2,"y":2,"w":1,"h":1},{"id":"c9","x":0,"y":3,"w":1,"h":1,"clue":1}]},{"id":"f509046b-a9b3-5f56-9aeb-e5b772115dfe","contentRevision":1,"order":13,"mode":"campaign-50","geometry":"square","features":["direction"],"title":"认清拐角","chapter":2,"chapterTitle":"方向与双色","role":"变化","targetDifficulty":32,"caption":"拐角连接哪两边,就从哪两边进出。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":1,"h":1,"direction":"dl"},{"id":"c3","x":0,"y":1,"w":1,"h":1},{"id":"c4","x":1,"y":1,"w":1,"h":1,"clue":1},{"id":"c5","x":2,"y":1,"w":1,"h":1},{"id":"c6","x":0,"y":2,"w":1,"h":1,"clue":5},{"id":"c7","x":1,"y":2,"w":1,"h":1},{"id":"c8","x":2,"y":2,"w":1,"h":1},{"id":"c9","x":0,"y":3,"w":1,"h":1},{"id":"c10","x":1,"y":3,"w":1,"h":1}]},{"id":"44f6955d-e999-5c24-8d71-14dc2d16f8f1","contentRevision":1,"order":14,"mode":"campaign-50","geometry":"square","features":["direction","walls"],"title":"两角相接","chapter":2,"chapterTitle":"方向与双色","role":"组合","targetDifficulty":35,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":3,"y":0,"w":1,"h":1},{"id":"c4","x":0,"y":1,"w":1,"h":1},{"id":"c5","x":1,"y":1,"w":1,"h":1},{"id":"c6","x":2,"y":1,"w":1,"h":1,"direction":"dl"},{"id":"c7","x":3,"y":1,"w":1,"h":1},{"id":"c8","x":0,"y":2,"w":1,"h":1},{"id":"c9","x":1,"y":2,"w":1,"h":1},{"id":"c10","x":2,"y":2,"w":1,"h":1,"direction":"ru"},{"id":"c11","x":3,"y":2,"w":1,"h":1},{"id":"c12","x":0,"y":3,"w":1,"h":1,"clue":1}],"walls":[["c4","c8"]]},{"id":"8f11cd51-0d6d-584a-9d00-babd8e381410","contentRevision":1,"order":15,"mode":"campaign-50","geometry":"square","features":["direction","walls"],"title":"直线与拐角","chapter":2,"chapterTitle":"方向与双色","role":"巩固","targetDifficulty":38,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1,"direction":"dr"},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":3,"y":0,"w":1,"h":1},{"id":"c4","x":0,"y":1,"w":1,"h":1},{"id":"c5","x":1,"y":1,"w":1,"h":1,"direction":"lr"},{"id":"c6","x":2,"y":1,"w":1,"h":1},{"id":"c7","x":3,"y":1,"w":1,"h":1},{"id":"c8","x":0,"y":2,"w":1,"h":1},{"id":"c9","x":1,"y":2,"w":1,"h":1},{"id":"c10","x":2,"y":2,"w":1,"h":1},{"id":"c11","x":3,"y":2,"w":1,"h":1,"clue":1},{"id":"c12","x":0,"y":3,"w":1,"h":1},{"id":"c13","x":1,"y":3,"w":1,"h":1}],"walls":[["c8","c9"]]},{"id":"a9749d13-9881-5ed8-9fcd-d06589e4e7df","contentRevision":1,"order":16,"mode":"campaign-50","geometry":"blocks","features":["big","alternation"],"title":"蓝橙第一步","chapter":2,"chapterTitle":"方向与双色","role":"演示/迁移","targetDifficulty":26,"caption":"蓝→橙→蓝,每一步换颜色。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1,"category":"circle"},{"id":"c1","x":1,"y":0,"w":1,"h":1,"category":"triangle"},{"id":"c2","x":2,"y":0,"w":1,"h":1,"category":"circle"},{"id":"c3","x":0,"y":1,"w":1,"h":1,"category":"triangle"},{"id":"c4","x":1,"y":1,"w":1,"h":1,"category":"circle"},{"id":"c5","x":2,"y":1,"w":1,"h":1,"category":"triangle"},{"id":"c6","x":0,"y":2,"w":1,"h":1,"clue":1,"category":"circle"},{"id":"c7","x":1,"y":2,"w":2,"h":2,"category":"triangle"},{"id":"c8","x":0,"y":3,"w":1,"h":1,"category":"circle"}]},{"id":"a9742951-1d48-538e-8d10-f37c3492ed6f","contentRevision":1,"order":17,"mode":"campaign-50","geometry":"blocks","features":["big","alternation"],"title":"留下另一种颜色","chapter":2,"chapterTitle":"方向与双色","role":"练习/挑战","targetDifficulty":32,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1,"category":"circle"},{"id":"c1","x":1,"y":0,"w":1,"h":1,"category":"triangle"},{"id":"c2","x":2,"y":0,"w":1,"h":1,"category":"circle"},{"id":"c3","x":3,"y":0,"w":1,"h":1,"category":"triangle"},{"id":"c4","x":0,"y":1,"w":1,"h":1,"category":"triangle"},{"id":"c5","x":1,"y":1,"w":2,"h":2,"category":"triangle"},{"id":"c6","x":3,"y":1,"w":1,"h":1,"category":"circle"},{"id":"c7","x":0,"y":2,"w":1,"h":1,"category":"circle"},{"id":"c8","x":3,"y":2,"w":1,"h":1,"clue":1,"category":"circle"},{"id":"c9","x":0,"y":3,"w":1,"h":1,"category":"triangle"},{"id":"c10","x":1,"y":3,"w":1,"h":1,"category":"circle"},{"id":"c11","x":2,"y":3,"w":1,"h":1,"category":"triangle"}]},{"id":"7261dec3-e994-5326-8755-2e60168b909c","contentRevision":1,"order":18,"mode":"campaign-50","geometry":"blocks","features":["big","alternation","walls"],"title":"双色跨区","chapter":2,"chapterTitle":"方向与双色","role":"章节峰值","targetDifficulty":46,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":2,"h":2,"category":"triangle"},{"id":"c1","x":2,"y":0,"w":1,"h":1,"category":"circle"},{"id":"c2","x":3,"y":0,"w":1,"h":1,"category":"triangle"},{"id":"c3","x":2,"y":1,"w":1,"h":2,"category":"circle","clue":9},{"id":"c4","x":3,"y":1,"w":1,"h":1,"category":"circle"},{"id":"c5","x":0,"y":2,"w":1,"h":1,"category":"circle"},{"id":"c6","x":1,"y":2,"w":1,"h":1,"category":"triangle"},{"id":"c7","x":3,"y":2,"w":1,"h":1,"category":"triangle"},{"id":"c8","x":0,"y":3,"w":1,"h":1,"category":"triangle"},{"id":"c9","x":1,"y":3,"w":1,"h":1,"category":"circle"},{"id":"c10","x":2,"y":3,"w":1,"h":1,"category":"triangle"},{"id":"c11","x":3,"y":3,"w":1,"h":1,"category":"circle"},{"id":"c12","x":0,"y":4,"w":1,"h":1,"category":"circle"},{"id":"c13","x":1,"y":4,"w":1,"h":1,"category":"triangle"},{"id":"c14","x":2,"y":4,"w":1,"h":1,"clue":1,"category":"circle"}],"walls":[["c9","c10"],["c3","c6"]]},{"id":"21ae0172-eeda-51ab-a66c-2de946d2541b","contentRevision":1,"order":19,"mode":"campaign-50","geometry":"blocks","features":["big","alternation"],"title":"短短的接力","chapter":2,"chapterTitle":"方向与双色","role":"释放","targetDifficulty":32,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1,"category":"triangle"},{"id":"c1","x":1,"y":0,"w":1,"h":1,"category":"circle"},{"id":"c2","x":2,"y":0,"w":1,"h":1,"category":"circle"},{"id":"c3","x":3,"y":0,"w":1,"h":1,"category":"triangle"},{"id":"c4","x":0,"y":1,"w":1,"h":1,"category":"circle"},{"id":"c5","x":1,"y":1,"w":2,"h":2,"category":"triangle"},{"id":"c6","x":3,"y":1,"w":1,"h":1,"category":"circle"},{"id":"c7","x":0,"y":2,"w":1,"h":1,"category":"triangle"},{"id":"c8","x":3,"y":2,"w":1,"h":1,"category":"triangle"},{"id":"c9","x":0,"y":3,"w":1,"h":1,"clue":1,"category":"circle"}]},{"id":"cc92cc4f-1336-544e-81b4-bd6731ed60d3","contentRevision":1,"order":20,"mode":"campaign-50","geometry":"blocks","features":["big","alternation"],"title":"双色回环","chapter":2,"chapterTitle":"方向与双色","role":"收束","targetDifficulty":36,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1,"category":"circle"},{"id":"c1","x":1,"y":0,"w":1,"h":1,"category":"triangle"},{"id":"c2","x":2,"y":0,"w":1,"h":1,"category":"circle"},{"id":"c3","x":3,"y":0,"w":1,"h":1,"category":"triangle"},{"id":"c4","x":0,"y":1,"w":1,"h":1,"category":"triangle"},{"id":"c5","x":1,"y":1,"w":1,"h":1,"category":"circle"},{"id":"c6","x":2,"y":1,"w":1,"h":1,"category":"triangle"},{"id":"c7","x":3,"y":1,"w":1,"h":1,"category":"circle"},{"id":"c8","x":0,"y":2,"w":1,"h":1,"category":"circle"},{"id":"c9","x":1,"y":2,"w":2,"h":2,"category":"triangle"},{"id":"c10","x":3,"y":2,"w":1,"h":1,"category":"triangle"},{"id":"c11","x":0,"y":3,"w":1,"h":1,"category":"circle"},{"id":"c12","x":3,"y":3,"w":1,"h":1,"clue":1,"category":"circle"}]},{"id":"9b9c1633-aedf-51f2-a04b-3818a2c6b7db","contentRevision":1,"order":21,"mode":"campaign-50","geometry":"blocks","features":["big","parity"],"title":"偶数落脚","chapter":3,"chapterTitle":"奇偶与步进门","role":"演示/热身","targetDifficulty":30,"caption":"“偶”要求本格的访问步号是偶数。","cells":[{"id":"c0","x":0,"y":0,"w":2,"h":2},{"id":"c1","x":2,"y":0,"w":1,"h":1},{"id":"c2","x":3,"y":0,"w":1,"h":1},{"id":"c3","x":2,"y":1,"w":1,"h":1},{"id":"c4","x":3,"y":1,"w":1,"h":1,"clue":9},{"id":"c5","x":0,"y":2,"w":1,"h":1},{"id":"c6","x":1,"y":2,"w":1,"h":1,"parity":"even"},{"id":"c7","x":2,"y":2,"w":1,"h":1},{"id":"c8","x":3,"y":2,"w":1,"h":1},{"id":"c9","x":0,"y":3,"w":1,"h":1,"clue":1}]},{"id":"2174f62c-3d56-575a-8210-b1535710e9b0","contentRevision":1,"order":22,"mode":"campaign-50","geometry":"blocks","features":["big","parity"],"title":"奇数绕行","chapter":3,"chapterTitle":"奇偶与步进门","role":"练习","targetDifficulty":34,"caption":"“奇”要求本格的访问步号是奇数。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1,"parity":"odd"},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":3,"y":0,"w":1,"h":1,"clue":1},{"id":"c4","x":0,"y":1,"w":1,"h":1},{"id":"c5","x":1,"y":1,"w":1,"h":1},{"id":"c6","x":2,"y":1,"w":2,"h":2,"clue":2},{"id":"c7","x":0,"y":2,"w":1,"h":1},{"id":"c8","x":1,"y":2,"w":1,"h":1},{"id":"c9","x":0,"y":3,"w":1,"h":1},{"id":"c10","x":1,"y":3,"w":1,"h":1},{"id":"c11","x":2,"y":3,"w":1,"h":1}]},{"id":"82766c6c-a705-5340-acf7-d4d46dc113c1","contentRevision":1,"order":23,"mode":"campaign-50","geometry":"blocks","features":["big","parity"],"title":"凹口的步号","chapter":3,"chapterTitle":"奇偶与步进门","role":"变化","targetDifficulty":39,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":3,"y":0,"w":1,"h":1},{"id":"c4","x":0,"y":1,"w":1,"h":1},{"id":"c5","x":1,"y":1,"w":1,"h":1},{"id":"c6","x":2,"y":1,"w":1,"h":1,"clue":7},{"id":"c7","x":3,"y":1,"w":1,"h":1},{"id":"c8","x":0,"y":2,"w":1,"h":1},{"id":"c9","x":1,"y":2,"w":1,"h":1,"clue":1},{"id":"c10","x":2,"y":2,"w":2,"h":2,"shape":[[0,0],[1,0],[0,1]]},{"id":"c11","x":0,"y":3,"w":1,"h":1},{"id":"c12","x":1,"y":3,"w":1,"h":1,"parity":"odd"}]},{"id":"f872f251-f6cd-5cf4-a8fa-c3619f948f22","contentRevision":1,"order":24,"mode":"campaign-50","geometry":"blocks","features":["big","parity","walls"],"title":"墙后先收尾","chapter":3,"chapterTitle":"奇偶与步进门","role":"组合","targetDifficulty":43,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":2,"h":2},{"id":"c1","x":2,"y":0,"w":1,"h":1},{"id":"c2","x":3,"y":0,"w":1,"h":1},{"id":"c3","x":2,"y":1,"w":1,"h":1},{"id":"c4","x":3,"y":1,"w":1,"h":1},{"id":"c5","x":0,"y":2,"w":1,"h":1},{"id":"c6","x":1,"y":2,"w":1,"h":1,"parity":"even"},{"id":"c7","x":2,"y":2,"w":1,"h":1},{"id":"c8","x":3,"y":2,"w":1,"h":1,"clue":1},{"id":"c9","x":0,"y":3,"w":1,"h":1},{"id":"c10","x":1,"y":3,"w":1,"h":1,"parity":"odd"},{"id":"c11","x":2,"y":3,"w":1,"h":1},{"id":"c12","x":3,"y":3,"w":1,"h":1},{"id":"c13","x":0,"y":4,"w":1,"h":1},{"id":"c14","x":1,"y":4,"w":1,"h":1}],"walls":[["c7","c8"],["c7","c11"]]},{"id":"1bcbf26f-650f-5791-b227-dd5e8f0d2dd0","contentRevision":1,"order":25,"mode":"campaign-50","geometry":"blocks","features":["big","parity"],"title":"少量线索也够","chapter":3,"chapterTitle":"奇偶与步进门","role":"巩固","targetDifficulty":36,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":2,"h":2},{"id":"c1","x":2,"y":0,"w":1,"h":1},{"id":"c2","x":3,"y":0,"w":1,"h":1},{"id":"c3","x":2,"y":1,"w":1,"h":1,"clue":1},{"id":"c4","x":3,"y":1,"w":1,"h":1},{"id":"c5","x":0,"y":2,"w":1,"h":1},{"id":"c6","x":1,"y":2,"w":1,"h":1,"parity":"odd"},{"id":"c7","x":2,"y":2,"w":1,"h":1,"clue":2},{"id":"c8","x":3,"y":2,"w":1,"h":1},{"id":"c9","x":0,"y":3,"w":1,"h":1},{"id":"c10","x":1,"y":3,"w":1,"h":1},{"id":"c11","x":2,"y":3,"w":1,"h":1}]},{"id":"da394472-bef6-53dd-abb2-bc4e7a60f07e","contentRevision":1,"order":26,"mode":"campaign-50","geometry":"square","features":["rhythm-door"],"title":"门随脚步开关","chapter":3,"chapterTitle":"奇偶与步进门","role":"演示/迁移","targetDifficulty":32,"caption":"空盘门关,落下起点后门开;通过前每格切换一次,通过后保持开启。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1,"clue":8},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":0,"y":1,"w":1,"h":1},{"id":"c4","x":1,"y":1,"w":1,"h":1,"clue":1},{"id":"c5","x":2,"y":1,"w":1,"h":1},{"id":"c6","x":0,"y":2,"w":1,"h":1},{"id":"c7","x":1,"y":2,"w":1,"h":1},{"id":"c8","x":2,"y":2,"w":1,"h":1}],"doors":[{"edge":["c2","c1"],"initialOpen":false}]},{"id":"d1d36cdb-8e4f-57b9-b53a-754da1264744","contentRevision":1,"order":27,"mode":"campaign-50","geometry":"square","features":["rhythm-door"],"title":"从另一侧通过","chapter":3,"chapterTitle":"奇偶与步进门","role":"练习/挑战","targetDifficulty":38,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1,"clue":1},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":0,"y":1,"w":1,"h":1},{"id":"c4","x":1,"y":1,"w":1,"h":1},{"id":"c5","x":2,"y":1,"w":1,"h":1},{"id":"c6","x":0,"y":2,"w":1,"h":1},{"id":"c7","x":1,"y":2,"w":1,"h":1},{"id":"c8","x":2,"y":2,"w":1,"h":1},{"id":"c9","x":0,"y":3,"w":1,"h":1},{"id":"c10","x":1,"y":3,"w":1,"h":1},{"id":"c11","x":2,"y":3,"w":1,"h":1,"clue":7}],"doors":[{"edge":["c3","c0"],"initialOpen":false}]},{"id":"a3f362fb-e070-5340-804a-14cb90fef2d4","contentRevision":1,"order":28,"mode":"campaign-50","geometry":"blocks","features":["big","rhythm-door"],"title":"大块改变节拍","chapter":3,"chapterTitle":"奇偶与步进门","role":"章节峰值","targetDifficulty":50,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":3,"y":0,"w":1,"h":1},{"id":"c4","x":0,"y":1,"w":1,"h":2,"clue":13},{"id":"c5","x":1,"y":1,"w":1,"h":1},{"id":"c6","x":2,"y":1,"w":1,"h":1},{"id":"c7","x":3,"y":1,"w":1,"h":1},{"id":"c8","x":1,"y":2,"w":1,"h":1},{"id":"c9","x":2,"y":2,"w":1,"h":1,"clue":1},{"id":"c10","x":3,"y":2,"w":1,"h":1},{"id":"c11","x":0,"y":3,"w":1,"h":1},{"id":"c12","x":1,"y":3,"w":2,"h":2},{"id":"c13","x":3,"y":3,"w":1,"h":1},{"id":"c14","x":0,"y":4,"w":1,"h":1}],"doors":[{"edge":["c14","c11"],"initialOpen":false}]},{"id":"d09f715e-948a-5ddc-ac89-1e2fa8e917f3","contentRevision":1,"order":29,"mode":"campaign-50","geometry":"blocks","features":["big","rhythm-door","walls"],"title":"墙和门分开看","chapter":3,"chapterTitle":"奇偶与步进门","role":"释放","targetDifficulty":40,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1,"clue":1},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":2,"h":2},{"id":"c3","x":0,"y":1,"w":1,"h":1},{"id":"c4","x":1,"y":1,"w":1,"h":1},{"id":"c5","x":0,"y":2,"w":1,"h":1},{"id":"c6","x":1,"y":2,"w":1,"h":1},{"id":"c7","x":2,"y":2,"w":1,"h":1},{"id":"c8","x":3,"y":2,"w":1,"h":1},{"id":"c9","x":0,"y":3,"w":1,"h":1,"clue":10},{"id":"c10","x":1,"y":3,"w":1,"h":1},{"id":"c11","x":2,"y":3,"w":1,"h":1}],"doors":[{"edge":["c5","c9"],"initialOpen":false}],"walls":[["c0","c1"]]},{"id":"ea61da6f-0488-59bd-b1f5-5e4f4efa4bbd","contentRevision":1,"order":30,"mode":"campaign-50","geometry":"blocks","features":["big","rhythm-door"],"title":"顺着节拍收尾","chapter":3,"chapterTitle":"奇偶与步进门","role":"收束","targetDifficulty":42,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":3,"y":0,"w":1,"h":1},{"id":"c4","x":0,"y":1,"w":1,"h":1},{"id":"c5","x":1,"y":1,"w":1,"h":1},{"id":"c6","x":2,"y":1,"w":1,"h":1},{"id":"c7","x":3,"y":1,"w":1,"h":1},{"id":"c8","x":0,"y":2,"w":1,"h":1},{"id":"c9","x":1,"y":2,"w":1,"h":1},{"id":"c10","x":2,"y":2,"w":2,"h":2},{"id":"c11","x":0,"y":3,"w":1,"h":1},{"id":"c12","x":1,"y":3,"w":1,"h":1,"clue":11},{"id":"c13","x":0,"y":4,"w":1,"h":1,"clue":1}],"doors":[{"edge":["c7","c3"],"initialOpen":false}]},{"id":"d61b202d-a661-5a76-8b40-0cef0a79480d","contentRevision":1,"order":31,"mode":"campaign-50","geometry":"hex","features":["walls"],"title":"六向邻接","chapter":4,"chapterTitle":"六边迁移与起点推理","role":"演示/热身","targetDifficulty":34,"caption":"六边形有六个共边方向,墙仍不能穿过。","cells":[{"id":"c0","q":0,"r":0,"clue":9},{"id":"c1","q":0,"r":1},{"id":"c2","q":1,"r":0},{"id":"c3","q":1,"r":1},{"id":"c4","q":2,"r":0,"clue":5},{"id":"c5","q":2,"r":1},{"id":"c6","q":3,"r":0},{"id":"c7","q":3,"r":1},{"id":"c8","q":4,"r":0,"clue":3},{"id":"c9","q":4,"r":1,"clue":1}],"walls":[["c1","c3"]]},{"id":"2c745dac-0edc-5d14-acda-be7bd80331f9","contentRevision":1,"order":32,"mode":"campaign-50","geometry":"hex","features":["walls"],"title":"六边留出口","chapter":4,"chapterTitle":"六边迁移与起点推理","role":"练习","targetDifficulty":38,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","q":0,"r":0},{"id":"c1","q":0,"r":1},{"id":"c2","q":0,"r":2},{"id":"c3","q":1,"r":0,"clue":6},{"id":"c4","q":1,"r":1},{"id":"c5","q":1,"r":2,"clue":11},{"id":"c6","q":2,"r":0},{"id":"c7","q":2,"r":1},{"id":"c8","q":2,"r":2},{"id":"c9","q":3,"r":0},{"id":"c10","q":3,"r":1},{"id":"c11","q":3,"r":2,"clue":1},{"id":"c12","q":4,"r":0}],"walls":[["c5","c7"],["c1","c4"]]},{"id":"36adc671-1d28-5c05-96c1-c7f84dff757a","contentRevision":1,"order":33,"mode":"campaign-50","geometry":"hex","features":["alternation"],"title":"六边换颜色","chapter":4,"chapterTitle":"六边迁移与起点推理","role":"变化","targetDifficulty":43,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","q":0,"r":0,"category":"circle"},{"id":"c1","q":0,"r":1,"category":"triangle"},{"id":"c2","q":0,"r":2,"clue":1,"category":"circle"},{"id":"c3","q":1,"r":0,"category":"triangle"},{"id":"c4","q":1,"r":1,"category":"circle"},{"id":"c5","q":1,"r":2,"category":"triangle"},{"id":"c6","q":2,"r":0,"category":"triangle"},{"id":"c7","q":2,"r":1,"category":"circle"},{"id":"c8","q":2,"r":2,"category":"triangle"},{"id":"c9","q":3,"r":0,"category":"circle"},{"id":"c10","q":3,"r":1,"category":"circle"},{"id":"c11","q":3,"r":2,"category":"circle"},{"id":"c12","q":4,"r":0,"category":"triangle"}]},{"id":"a0bd742b-73c2-5dd6-9df9-a69580e5becd","contentRevision":1,"order":34,"mode":"campaign-50","geometry":"hex","features":["alternation","walls"],"title":"颜色之外还有墙","chapter":4,"chapterTitle":"六边迁移与起点推理","role":"组合","targetDifficulty":47,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","q":0,"r":0,"category":"triangle"},{"id":"c1","q":0,"r":1,"category":"circle"},{"id":"c2","q":0,"r":2,"category":"triangle"},{"id":"c3","q":1,"r":0,"category":"circle"},{"id":"c4","q":1,"r":1,"category":"circle"},{"id":"c5","q":1,"r":2,"category":"triangle"},{"id":"c6","q":2,"r":0,"category":"triangle"},{"id":"c7","q":2,"r":1,"clue":1,"category":"circle"},{"id":"c8","q":2,"r":2,"category":"circle"},{"id":"c9","q":3,"r":0,"category":"circle"},{"id":"c10","q":3,"r":1,"category":"triangle"},{"id":"c11","q":3,"r":2,"category":"triangle"},{"id":"c12","q":4,"r":0,"category":"circle"},{"id":"c13","q":4,"r":1,"category":"triangle"},{"id":"c14","q":4,"r":2,"category":"circle"}],"walls":[["c10","c12"],["c7","c10"]]},{"id":"03398b88-7ed6-5bc3-b001-7bbcb825d963","contentRevision":1,"order":35,"mode":"campaign-50","geometry":"hex","features":["alternation","walls"],"title":"六边小花园","chapter":4,"chapterTitle":"六边迁移与起点推理","role":"巩固","targetDifficulty":40,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","q":0,"r":0,"category":"circle"},{"id":"c1","q":0,"r":1,"category":"triangle"},{"id":"c2","q":1,"r":0,"category":"triangle"},{"id":"c3","q":1,"r":1,"category":"circle"},{"id":"c4","q":2,"r":0,"clue":1,"category":"circle"},{"id":"c5","q":2,"r":1,"category":"triangle"},{"id":"c6","q":3,"r":0,"category":"triangle"},{"id":"c7","q":3,"r":1,"category":"circle"},{"id":"c8","q":4,"r":0,"category":"circle"},{"id":"c9","q":4,"r":1,"category":"triangle"},{"id":"c10","q":5,"r":0,"category":"triangle"},{"id":"c11","q":5,"r":1,"category":"circle"}],"walls":[["c8","c10"]]},{"id":"1a91cff3-d0ef-5abc-b4a1-adbc4d70a208","contentRevision":1,"order":36,"mode":"campaign-50","geometry":"hex","features":["parity"],"title":"六边也有奇偶","chapter":4,"chapterTitle":"六边迁移与起点推理","role":"演示/迁移","targetDifficulty":44,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","q":0,"r":0},{"id":"c1","q":0,"r":1},{"id":"c2","q":0,"r":2},{"id":"c3","q":1,"r":0},{"id":"c4","q":1,"r":1,"clue":1},{"id":"c5","q":1,"r":2},{"id":"c6","q":2,"r":0,"clue":6},{"id":"c7","q":2,"r":1,"clue":7},{"id":"c8","q":2,"r":2,"clue":13},{"id":"c9","q":3,"r":0,"parity":"odd"},{"id":"c10","q":3,"r":1},{"id":"c11","q":3,"r":2},{"id":"c12","q":4,"r":0},{"id":"c13","q":4,"r":1}]},{"id":"2e11044e-f88b-5152-bda8-b1eed66a3888","contentRevision":1,"order":37,"mode":"campaign-50","geometry":"hex","features":["parity"],"title":"起点没有写出","chapter":4,"chapterTitle":"六边迁移与起点推理","role":"练习/挑战","targetDifficulty":49,"caption":"这关没有1,先观察起点。","cells":[{"id":"c0","q":0,"r":0},{"id":"c1","q":0,"r":1,"clue":2},{"id":"c2","q":1,"r":0},{"id":"c3","q":1,"r":1},{"id":"c4","q":2,"r":0,"clue":4},{"id":"c5","q":2,"r":1},{"id":"c6","q":3,"r":0,"clue":7},{"id":"c7","q":3,"r":1},{"id":"c8","q":4,"r":0,"parity":"odd"},{"id":"c9","q":4,"r":1,"clue":11},{"id":"c10","q":5,"r":0},{"id":"c11","q":5,"r":1}]},{"id":"349e4537-d0e7-545d-be9b-feb7ff23fa2f","contentRevision":1,"order":38,"mode":"campaign-50","geometry":"hex","features":["parity","walls"],"title":"找起点再跨区","chapter":4,"chapterTitle":"六边迁移与起点推理","role":"章节峰值","targetDifficulty":58,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","q":0,"r":0},{"id":"c1","q":0,"r":1},{"id":"c2","q":0,"r":2},{"id":"c3","q":1,"r":0,"clue":2},{"id":"c4","q":1,"r":1},{"id":"c5","q":1,"r":2},{"id":"c6","q":2,"r":0},{"id":"c7","q":2,"r":1,"clue":6},{"id":"c8","q":2,"r":2},{"id":"c9","q":3,"r":0},{"id":"c10","q":3,"r":1},{"id":"c11","q":3,"r":2},{"id":"c12","q":4,"r":0,"parity":"even"},{"id":"c13","q":4,"r":1,"clue":13},{"id":"c14","q":4,"r":2,"parity":"odd"},{"id":"c15","q":5,"r":0},{"id":"c16","q":5,"r":1}],"walls":[["c8","c10"],["c3","c6"]]},{"id":"7f895016-a15f-5d1a-b89b-dce54f3f0986","contentRevision":1,"order":39,"mode":"campaign-50","geometry":"hex","features":["parity","walls"],"title":"给回起点的短题","chapter":4,"chapterTitle":"六边迁移与起点推理","role":"释放","targetDifficulty":42,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","q":0,"r":0,"clue":1},{"id":"c1","q":0,"r":1},{"id":"c2","q":1,"r":0,"clue":2},{"id":"c3","q":1,"r":1},{"id":"c4","q":2,"r":0},{"id":"c5","q":2,"r":1},{"id":"c6","q":3,"r":0,"clue":7},{"id":"c7","q":3,"r":1},{"id":"c8","q":4,"r":0},{"id":"c9","q":4,"r":1,"clue":11},{"id":"c10","q":5,"r":0,"parity":"even"},{"id":"c11","q":5,"r":1}],"walls":[["c4","c6"]]},{"id":"15549f52-37a6-5149-b564-8caf096022fe","contentRevision":1,"order":40,"mode":"campaign-50","geometry":"hex","features":["parity","walls"],"title":"六边收束","chapter":4,"chapterTitle":"六边迁移与起点推理","role":"收束","targetDifficulty":46,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","q":0,"r":0,"clue":1},{"id":"c1","q":0,"r":1},{"id":"c2","q":0,"r":2,"clue":4},{"id":"c3","q":1,"r":0},{"id":"c4","q":1,"r":1,"clue":5},{"id":"c5","q":1,"r":2},{"id":"c6","q":2,"r":0},{"id":"c7","q":2,"r":1},{"id":"c8","q":2,"r":2},{"id":"c9","q":3,"r":0},{"id":"c10","q":3,"r":1},{"id":"c11","q":3,"r":2,"clue":13},{"id":"c12","q":4,"r":0,"parity":"even"},{"id":"c13","q":4,"r":1},{"id":"c14","q":4,"r":2,"clue":14}],"walls":[["c4","c6"]]},{"id":"034f2c11-d092-5280-b22d-dbe2b8ca26c2","contentRevision":1,"order":41,"mode":"campaign-50","geometry":"square","features":["direction","walls"],"title":"通道再接力","chapter":5,"chapterTitle":"组合应用与收束","role":"演示/热身","targetDifficulty":44,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1,"clue":13},{"id":"c2","x":2,"y":0,"w":1,"h":1,"direction":"lr"},{"id":"c3","x":3,"y":0,"w":1,"h":1},{"id":"c4","x":0,"y":1,"w":1,"h":1},{"id":"c5","x":1,"y":1,"w":1,"h":1},{"id":"c6","x":2,"y":1,"w":1,"h":1},{"id":"c7","x":3,"y":1,"w":1,"h":1},{"id":"c8","x":0,"y":2,"w":1,"h":1},{"id":"c9","x":1,"y":2,"w":1,"h":1,"direction":"lu"},{"id":"c10","x":2,"y":2,"w":1,"h":1},{"id":"c11","x":3,"y":2,"w":1,"h":1,"clue":1},{"id":"c12","x":0,"y":3,"w":1,"h":1},{"id":"c13","x":1,"y":3,"w":1,"h":1},{"id":"c14","x":2,"y":3,"w":1,"h":1}],"walls":[["c10","c11"]]},{"id":"758c22d7-8b75-5110-befe-9b4b71a749b0","contentRevision":1,"order":42,"mode":"campaign-50","geometry":"blocks","features":["big","alternation","walls"],"title":"两个双色枢纽","chapter":5,"chapterTitle":"组合应用与收束","role":"练习","targetDifficulty":48,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":2,"category":"triangle"},{"id":"c1","x":1,"y":0,"w":1,"h":1,"category":"triangle"},{"id":"c2","x":2,"y":0,"w":1,"h":1,"category":"circle"},{"id":"c3","x":3,"y":0,"w":1,"h":1,"category":"triangle"},{"id":"c4","x":1,"y":1,"w":1,"h":1,"category":"circle","clue":11},{"id":"c5","x":2,"y":1,"w":1,"h":1,"category":"triangle"},{"id":"c6","x":3,"y":1,"w":1,"h":1,"category":"circle"},{"id":"c7","x":0,"y":2,"w":1,"h":1,"category":"circle"},{"id":"c8","x":1,"y":2,"w":1,"h":1,"category":"triangle"},{"id":"c9","x":2,"y":2,"w":1,"h":1,"category":"circle"},{"id":"c10","x":3,"y":2,"w":1,"h":1,"category":"triangle"},{"id":"c11","x":0,"y":3,"w":1,"h":1,"category":"triangle"},{"id":"c12","x":1,"y":3,"w":1,"h":1,"category":"circle"},{"id":"c13","x":2,"y":3,"w":2,"h":2,"shape":[[0,0],[0,1],[1,0]],"category":"circle"},{"id":"c14","x":0,"y":4,"w":1,"h":1,"clue":1,"category":"circle"},{"id":"c15","x":1,"y":4,"w":1,"h":1,"category":"triangle"}],"walls":[["c9","c10"],["c4","c5"]]},{"id":"d430c0b6-4d1c-50f0-9365-82f40d6bdcdb","contentRevision":1,"order":43,"mode":"campaign-50","geometry":"blocks","features":["big","parity","walls"],"title":"奇偶分配区域","chapter":5,"chapterTitle":"组合应用与收束","role":"变化","targetDifficulty":52,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":3,"y":0,"w":1,"h":1},{"id":"c4","x":0,"y":1,"w":1,"h":1},{"id":"c5","x":1,"y":1,"w":1,"h":1,"parity":"even"},{"id":"c6","x":2,"y":1,"w":1,"h":1},{"id":"c7","x":3,"y":1,"w":1,"h":1,"parity":"odd"},{"id":"c8","x":0,"y":2,"w":1,"h":1},{"id":"c9","x":1,"y":2,"w":1,"h":1},{"id":"c10","x":2,"y":2,"w":2,"h":1},{"id":"c11","x":0,"y":3,"w":1,"h":1},{"id":"c12","x":1,"y":3,"w":1,"h":1},{"id":"c13","x":2,"y":3,"w":2,"h":2,"shape":[[0,0],[1,0],[0,1]]},{"id":"c14","x":0,"y":4,"w":1,"h":1},{"id":"c15","x":1,"y":4,"w":1,"h":1},{"id":"c16","x":3,"y":4,"w":1,"h":1,"clue":1}],"walls":[["c8","c9"],["c12","c15"]]},{"id":"09653cbe-4ca9-571d-b976-853ac0ecb886","contentRevision":1,"order":44,"mode":"campaign-50","geometry":"hex","features":["alternation","walls"],"title":"六边出口互锁","chapter":5,"chapterTitle":"组合应用与收束","role":"组合","targetDifficulty":56,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","q":0,"r":0,"category":"circle"},{"id":"c1","q":0,"r":1,"category":"triangle"},{"id":"c2","q":1,"r":2,"category":"triangle"},{"id":"c3","q":1,"r":0,"category":"triangle"},{"id":"c4","q":1,"r":1,"category":"circle"},{"id":"c5","q":2,"r":2,"category":"circle"},{"id":"c6","q":2,"r":0,"category":"circle"},{"id":"c7","q":2,"r":1,"category":"triangle"},{"id":"c8","q":3,"r":2,"category":"triangle"},{"id":"c9","q":3,"r":0,"category":"triangle"},{"id":"c10","q":3,"r":1,"category":"circle"},{"id":"c11","q":4,"r":2,"category":"circle","clue":3},{"id":"c12","q":4,"r":0,"category":"circle"},{"id":"c13","q":4,"r":1,"category":"triangle"},{"id":"c14","q":5,"r":2,"category":"triangle"},{"id":"c15","q":5,"r":0,"category":"triangle"},{"id":"c16","q":5,"r":1,"category":"circle"},{"id":"c17","q":6,"r":2,"clue":1,"category":"circle"}],"walls":[["c1","c4"],["c4","c7"],["c7","c10"]]},{"id":"ba80bb56-a59e-55aa-b586-46d245984ccb","contentRevision":1,"order":45,"mode":"campaign-50","geometry":"square","features":["direction","rhythm-door"],"title":"方向决定门路","chapter":5,"chapterTitle":"组合应用与收束","role":"巩固","targetDifficulty":58,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":3,"y":0,"w":1,"h":1},{"id":"c4","x":0,"y":1,"w":1,"h":1},{"id":"c5","x":1,"y":1,"w":1,"h":1,"direction":"ru"},{"id":"c6","x":2,"y":1,"w":1,"h":1},{"id":"c7","x":3,"y":1,"w":1,"h":1},{"id":"c8","x":0,"y":2,"w":1,"h":1},{"id":"c9","x":1,"y":2,"w":1,"h":1,"direction":"lr"},{"id":"c10","x":2,"y":2,"w":1,"h":1,"clue":1},{"id":"c11","x":3,"y":2,"w":1,"h":1},{"id":"c12","x":0,"y":3,"w":1,"h":1},{"id":"c13","x":1,"y":3,"w":1,"h":1},{"id":"c14","x":2,"y":3,"w":1,"h":1},{"id":"c15","x":3,"y":3,"w":1,"h":1}],"doors":[{"edge":["c2","c3"],"initialOpen":false}]},{"id":"9ceed4f0-5c12-5251-9cc9-11aafba3e8c3","contentRevision":1,"order":46,"mode":"campaign-50","geometry":"blocks","features":["big","parity","rhythm-door"],"title":"先定奇偶,再定节拍","chapter":5,"chapterTitle":"组合应用与收束","role":"演示/迁移","targetDifficulty":60,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1},{"id":"c1","x":1,"y":0,"w":1,"h":1},{"id":"c2","x":2,"y":0,"w":1,"h":1},{"id":"c3","x":3,"y":0,"w":1,"h":1},{"id":"c4","x":0,"y":1,"w":1,"h":1,"parity":"odd"},{"id":"c5","x":1,"y":1,"w":1,"h":1},{"id":"c6","x":2,"y":1,"w":1,"h":1},{"id":"c7","x":3,"y":1,"w":1,"h":1,"clue":8},{"id":"c8","x":0,"y":2,"w":1,"h":1},{"id":"c9","x":1,"y":2,"w":1,"h":1,"clue":1},{"id":"c10","x":2,"y":2,"w":1,"h":1,"parity":"even"},{"id":"c11","x":3,"y":2,"w":1,"h":1},{"id":"c12","x":0,"y":3,"w":2,"h":2,"shape":[[0,0],[1,0],[0,1]]},{"id":"c13","x":2,"y":3,"w":1,"h":1},{"id":"c14","x":3,"y":3,"w":1,"h":1},{"id":"c15","x":1,"y":4,"w":1,"h":1},{"id":"c16","x":2,"y":4,"w":2,"h":1}],"doors":[{"edge":["c3","c7"],"initialOpen":false}]},{"id":"71a70b69-ff59-51a3-a149-1dd88868c0f3","contentRevision":1,"order":47,"mode":"campaign-50","geometry":"square","features":["direction","walls","rhythm-door"],"title":"墙通道门三者联动","chapter":5,"chapterTitle":"组合应用与收束","role":"练习/挑战","targetDifficulty":65,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1,"direction":"dr"},{"id":"c1","x":0,"y":1,"w":1,"h":1},{"id":"c2","x":0,"y":2,"w":1,"h":1},{"id":"c3","x":0,"y":3,"w":1,"h":1},{"id":"c4","x":1,"y":0,"w":1,"h":1},{"id":"c5","x":1,"y":1,"w":1,"h":1},{"id":"c6","x":1,"y":2,"w":1,"h":1,"direction":"du"},{"id":"c7","x":1,"y":3,"w":1,"h":1},{"id":"c8","x":2,"y":0,"w":1,"h":1},{"id":"c9","x":2,"y":1,"w":1,"h":1},{"id":"c10","x":2,"y":2,"w":1,"h":1},{"id":"c11","x":2,"y":3,"w":1,"h":1,"clue":1},{"id":"c12","x":3,"y":0,"w":1,"h":1,"direction":"lr"},{"id":"c13","x":3,"y":1,"w":1,"h":1},{"id":"c14","x":3,"y":2,"w":1,"h":1},{"id":"c15","x":3,"y":3,"w":1,"h":1},{"id":"c16","x":4,"y":0,"w":1,"h":1},{"id":"c17","x":4,"y":1,"w":1,"h":1}],"doors":[{"edge":["c6","c7"],"initialOpen":false}],"walls":[["c1","c5"],["c10","c11"]]},{"id":"e96c918b-2f4c-5a18-b443-8ac574f77ad6","contentRevision":1,"order":48,"mode":"campaign-50","geometry":"hex","features":["parity","walls","rhythm-door"],"title":"六边综合峰值","chapter":5,"chapterTitle":"组合应用与收束","role":"章节峰值","targetDifficulty":72,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","q":0,"r":0,"clue":18},{"id":"c1","q":0,"r":1},{"id":"c2","q":0,"r":2},{"id":"c3","q":1,"r":0},{"id":"c4","q":1,"r":1},{"id":"c5","q":1,"r":2},{"id":"c6","q":2,"r":0,"clue":1},{"id":"c7","q":2,"r":1,"clue":2},{"id":"c8","q":2,"r":2},{"id":"c9","q":3,"r":0},{"id":"c10","q":3,"r":1,"parity":"even"},{"id":"c11","q":3,"r":2},{"id":"c12","q":4,"r":0},{"id":"c13","q":4,"r":1},{"id":"c14","q":4,"r":2},{"id":"c15","q":5,"r":0},{"id":"c16","q":5,"r":1},{"id":"c17","q":5,"r":2},{"id":"c18","q":6,"r":0,"parity":"even"}],"doors":[{"edge":["c3","c0"],"initialOpen":false}],"walls":[["c10","c11"],["c10","c13"]]},{"id":"a25a4829-f77f-5b4a-9163-4eef9cd3856e","contentRevision":1,"order":49,"mode":"campaign-50","geometry":"blocks","features":["big","alternation","walls"],"title":"回到双色小题","chapter":5,"chapterTitle":"组合应用与收束","role":"释放","targetDifficulty":48,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","x":0,"y":0,"w":1,"h":1,"category":"triangle"},{"id":"c1","x":1,"y":0,"w":2,"h":2,"shape":[[0,0],[0,1],[1,0]],"category":"triangle"},{"id":"c2","x":3,"y":0,"w":1,"h":1,"category":"circle"},{"id":"c3","x":0,"y":1,"w":1,"h":1,"category":"circle"},{"id":"c4","x":2,"y":1,"w":1,"h":1,"clue":1,"category":"circle"},{"id":"c5","x":3,"y":1,"w":1,"h":1,"category":"triangle"},{"id":"c6","x":0,"y":2,"w":1,"h":1,"category":"triangle"},{"id":"c7","x":1,"y":2,"w":1,"h":1,"category":"circle"},{"id":"c8","x":2,"y":2,"w":1,"h":1,"category":"triangle"},{"id":"c9","x":3,"y":2,"w":1,"h":1,"category":"circle"},{"id":"c10","x":0,"y":3,"w":1,"h":1,"category":"circle"},{"id":"c11","x":1,"y":3,"w":1,"h":1,"category":"triangle"}],"walls":[["c4","c8"]]},{"id":"f79bf403-764d-57e7-a37f-e85dce3b4505","contentRevision":1,"order":50,"mode":"campaign-50","geometry":"hex","features":["alternation","walls"],"title":"继续还有新花园","chapter":5,"chapterTitle":"组合应用与收束","role":"收束","targetDifficulty":54,"caption":"先观察,再画;松手可继续,走错可回退。","cells":[{"id":"c0","q":0,"r":0,"category":"triangle"},{"id":"c1","q":0,"r":1,"category":"circle"},{"id":"c2","q":0,"r":2,"category":"triangle"},{"id":"c3","q":1,"r":0,"category":"circle"},{"id":"c4","q":1,"r":1,"category":"triangle"},{"id":"c5","q":1,"r":2,"category":"circle"},{"id":"c6","q":2,"r":0,"clue":1,"category":"circle"},{"id":"c7","q":2,"r":1,"category":"triangle"},{"id":"c8","q":2,"r":2,"category":"triangle"},{"id":"c9","q":3,"r":0,"category":"triangle"},{"id":"c10","q":3,"r":1,"category":"circle"},{"id":"c11","q":3,"r":2,"category":"circle"},{"id":"c12","q":4,"r":0,"category":"circle"},{"id":"c13","q":4,"r":1,"category":"circle"},{"id":"c14","q":4,"r":2,"category":"triangle"},{"id":"c15","q":5,"r":0,"category":"triangle"}],"walls":[["c6","c7"],["c0","c1"]]},{"mode":"campaign-50","chapter":2,"chapterTitle":"方向与双色","contentRevision":1,"role":"引导","targetDifficulty":3,"id":"tutorial-straight","order":51,"geometry":"square","features":["direction"],"title":"引导:直线通道","caption":"横通道只连接左、右两边:从左进入后,必须向右离开。","tutorial":"straight","cells":[{"id":"t0","x":0,"y":0,"w":1,"h":1,"clue":1},{"id":"t1","x":1,"y":0,"w":1,"h":1,"direction":"lr"},{"id":"t2","x":2,"y":0,"w":1,"h":1},{"id":"t3","x":0,"y":1,"w":1,"h":1},{"id":"t4","x":1,"y":1,"w":1,"h":1},{"id":"t5","x":2,"y":1,"w":1,"h":1}]},{"mode":"campaign-50","chapter":2,"chapterTitle":"方向与双色","contentRevision":1,"role":"引导","targetDifficulty":3,"id":"tutorial-corner","order":52,"geometry":"square","features":["direction"],"title":"引导:拐角通道","caption":"这个拐角只连接左、上两边:从左进入后,必须向上离开。","tutorial":"corner","cells":[{"id":"t0","x":0,"y":0,"w":1,"h":1,"clue":1},{"id":"t1","x":1,"y":0,"w":1,"h":1},{"id":"t2","x":2,"y":0,"w":1,"h":1},{"id":"t3","x":0,"y":1,"w":1,"h":1},{"id":"t4","x":1,"y":1,"w":1,"h":1,"direction":"lu"},{"id":"t5","x":2,"y":1,"w":1,"h":1}]},{"mode":"campaign-50","chapter":2,"chapterTitle":"方向与双色","contentRevision":1,"role":"引导","targetDifficulty":3,"id":"tutorial-colour","order":53,"geometry":"blocks","features":["big","alternation"],"title":"引导:蓝橙交替","caption":"蓝 → 橙 → 蓝,每一步必须换颜色;相邻的同色格也不能走。","tutorial":"colour","cells":[{"id":"t0","x":0,"y":1,"w":1,"h":1,"clue":1,"category":"circle"},{"id":"t1","x":0,"y":0,"w":2,"h":1,"category":"triangle"},{"id":"t2","x":1,"y":1,"w":1,"h":1,"category":"circle"}]}],"playOrder":["21ceadc1-cd0e-511b-b7d3-d227e01de8a1","de71e06d-ed2b-5371-a3dd-7089ccff10b3","f521de33-0d27-54c1-9c04-8fe43955132c","07323e0e-de6c-5d71-8876-e3d88d008b60","0b255856-d185-5314-85e3-4a95eb233b04","23914286-0aa4-5d7a-afd1-b5a31c0c1a6e","e85cd2f3-94af-5b5e-bfd5-d513e6a5ba2c","a2cbcb61-034a-5215-9a31-035c1c0b96f5","c27d6782-9897-56e4-9024-27a725af609a","2a913cf0-2d97-512f-b8d8-bf4b859ac675","tutorial-straight","tutorial-corner","9edd9ff2-ed54-5fd8-a0e5-8c477b3100bc","852f0119-285e-5ec3-9569-8e34485bcf74","f509046b-a9b3-5f56-9aeb-e5b772115dfe","44f6955d-e999-5c24-8d71-14dc2d16f8f1","8f11cd51-0d6d-584a-9d00-babd8e381410","tutorial-colour","a9749d13-9881-5ed8-9fcd-d06589e4e7df","a9742951-1d48-538e-8d10-f37c3492ed6f","7261dec3-e994-5326-8755-2e60168b909c","21ae0172-eeda-51ab-a66c-2de946d2541b","cc92cc4f-1336-544e-81b4-bd6731ed60d3","9b9c1633-aedf-51f2-a04b-3818a2c6b7db","2174f62c-3d56-575a-8210-b1535710e9b0","82766c6c-a705-5340-acf7-d4d46dc113c1","f872f251-f6cd-5cf4-a8fa-c3619f948f22","1bcbf26f-650f-5791-b227-dd5e8f0d2dd0","da394472-bef6-53dd-abb2-bc4e7a60f07e","d1d36cdb-8e4f-57b9-b53a-754da1264744","a3f362fb-e070-5340-804a-14cb90fef2d4","d09f715e-948a-5ddc-ac89-1e2fa8e917f3","ea61da6f-0488-59bd-b1f5-5e4f4efa4bbd","d61b202d-a661-5a76-8b40-0cef0a79480d","2c745dac-0edc-5d14-acda-be7bd80331f9","36adc671-1d28-5c05-96c1-c7f84dff757a","a0bd742b-73c2-5dd6-9df9-a69580e5becd","03398b88-7ed6-5bc3-b001-7bbcb825d963","1a91cff3-d0ef-5abc-b4a1-adbc4d70a208","2e11044e-f88b-5152-bda8-b1eed66a3888","349e4537-d0e7-545d-be9b-feb7ff23fa2f","7f895016-a15f-5d1a-b89b-dce54f3f0986","15549f52-37a6-5149-b564-8caf096022fe","034f2c11-d092-5280-b22d-dbe2b8ca26c2","758c22d7-8b75-5110-befe-9b4b71a749b0","d430c0b6-4d1c-50f0-9365-82f40d6bdcdb","09653cbe-4ca9-571d-b976-853ac0ecb886","ba80bb56-a59e-55aa-b586-46d245984ccb","9ceed4f0-5c12-5251-9cc9-11aafba3e8c3","71a70b69-ff59-51a3-a149-1dd88868c0f3","e96c918b-2f4c-5a18-b443-8ac574f77ad6","a25a4829-f77f-5b4a-9163-4eef9cd3856e","f79bf403-764d-57e7-a37f-e85dce3b4505"]};
// Pure rules / geometry / solver. Each mode receives its own standalone copy.
function units(c){return c.shape?c.shape.map(([x,y])=>[c.x+x,c.y+y]):Array.from({length:(c.w||1)*(c.h||1)},(_,i)=>[c.x+i%(c.w||1),c.y+Math.floor(i/(c.w||1))]);}
function outlineUnits(c){
const occupied=new Set(units(c).map(p=>p.join(','))),edges=[];
for(const [x,y]of units(c))for(const [dx,dy,a,b]of [[0,-1,[x,y],[x+1,y]],[1,0,[x+1,y],[x+1,y+1]],[0,1,[x+1,y+1],[x,y+1]],[-1,0,[x,y+1],[x,y]]])if(!occupied.has((x+dx)+','+(y+dy)))edges.push([a,b]);
const next=new Map(edges.map(([a,b])=>[a.join(','),b])),start=edges[0][0],points=[start];let at=next.get(start.join(','));
while(at.join(',')!==start.join(',')){points.push(at);at=next.get(at.join(','));if(points.length>edges.length)throw Error('Invalid block contour');}
return points.filter((b,i)=>{const a=points[(i+points.length-1)%points.length],d=points[(i+1)%points.length];return (b[0]-a[0])*(d[1]-b[1])!==(b[1]-a[1])*(d[0]-b[0]);});
}
function graph(level){
const cells=level.cells, ids=new Map(cells.map((c,i)=>[c.id,i]));
if(ids.size!==cells.length)throw Error('重复格 id');
const adj=cells.map(()=>[]), occupancy=new Map();
if(level.geometry!=='hex') for(let i=0;i<cells.length;i++)for(const [x,y]of units(cells[i])){const k=x+','+y;if(occupancy.has(k))throw Error('拼块重叠');occupancy.set(k,i);}
for(let i=0;i<cells.length;i++)for(let j=i+1;j<cells.length;j++){
let connected=false;
if(level.geometry==='hex'){let dq=cells[j].q-cells[i].q,dr=cells[j].r-cells[i].r;connected=[[1,0],[-1,0],[0,1],[0,-1],[1,-1],[-1,1]].some(([a,b])=>a===dq&&b===dr);}
else connected=units(cells[i]).some(([x,y])=>[[1,0],[-1,0],[0,1],[0,-1]].some(([a,b])=>occupancy.get((x+a)+','+(y+b))===j));
if(connected){adj[i].push(j);adj[j].push(i);}
}
const clues=new Map();for(const c of cells)if(c.clue!==undefined){if(!Number.isInteger(c.clue)||c.clue<1||c.clue>cells.length||clues.has(c.clue))throw Error('数字范围或重复');clues.set(c.clue,ids.get(c.id));}
return {cells,ids,adj,clues};
}
function constraints(level){
const result=[];
for(let i=0;i<(level.relations||[]).length;i++)result.push({key:'relation:'+i,kind:'relation',...level.relations[i]});
for(let i=0;i<(level.orderChains||[]).length;i++)for(let j=0;j<level.orderChains[i].length-1;j++)result.push({key:'order:'+i+':'+j,kind:'order',from:level.orderChains[i][j],to:level.orderChains[i][j+1]});
for(let i=0;i<(level.windows||[]).length;i++)result.push({key:'window:'+i,kind:'window',...level.windows[i]});
for(const c of level.cells)if(c.parity)result.push({key:'parity:'+c.id,kind:'parity',cell:c.id,value:c.parity});
for(const c of level.cells)if(c.direction)result.push({key:'direction:'+c.id,kind:'direction',cell:c.id,value:c.direction});
if((level.features||[]).includes('alternation'))result.push({key:'alternation',kind:'alternation'});
for(let i=0;i<(level.walls||[]).length;i++)result.push({key:'wall:'+i,kind:'wall',edge:level.walls[i]});
for(let i=0;i<(level.doors||[]).length;i++)result.push({key:'door:'+i,kind:'door',...level.doors[i]});
for(let i=0;i<(level.gates||[]).length;i++)result.push({key:'gate:'+i,kind:'gate',...level.gates[i]});
return result;
}
function directionAt(level,path,k){
if(k===0||k===path.length-1)return null;
const [a,b,c]=[path[k-1],path[k],path[k+1]].map(id=>level.cells.find(x=>x.id===id));
const side=(x)=>x.x<b.x?'l':x.x>b.x?'r':x.y<b.y?'u':'d';
return [side(a),side(c)].sort().join('');
}
function doorLatched(door,path){
for(let i=1;i<path.length;i++)if(door.edge.includes(path[i-1])&&door.edge.includes(path[i])&&(Boolean(door.initialOpen)!==Boolean(i%2)))return true;
return false;
}
function doorOpen(door,path){return Array.isArray(path)?doorLatched(door,path)||(Boolean(door.initialOpen)!==Boolean(path.length%2)):Boolean(door.initialOpen)!==Boolean(path%2);}
const directionNames={lr:'左右直行',du:'上下直行',ru:'上与右拐角',dr:'右与下拐角',dl:'下与左拐角',lu:'左与上拐角'};
function edgeBlocked(level,path,a,b,{omit=null}={}){
for(let i=0;i<(level.walls||[]).length;i++)if(omit!=='wall:'+i&&level.walls[i].includes(a)&&level.walls[i].includes(b))return true;
for(let i=0;i<(level.doors||[]).length;i++)if(omit!=='door:'+i&&level.doors[i].edge.includes(a)&&level.doors[i].edge.includes(b)&&!doorOpen(level.doors[i],path))return true;
return false;
}
function violations(level,path,{omit=null,complete=path.length===level.cells.length,ignoreClues=false}={}){
const positions=new Map(path.map((id,i)=>[id,i+1])),bad=[];
if(!ignoreClues)for(const c of level.cells)if(c.clue!==undefined){const at=positions.get(c.id);if(at!==undefined&&at!==c.clue||path.length>=c.clue&&path[c.clue-1]!==c.id)bad.push({key:'clue:'+c.id,cells:[c.id,path[c.clue-1]].filter(Boolean),message:'固定数字步号不符'});}
for(const r of constraints(level)){
if(r.key===omit)continue;
let fail=false,where=[];
if(r.kind==='relation'){
const a=positions.get(r.from),b=positions.get(r.to);where=[r.from,r.to];
fail=b!==undefined&&(a===undefined||b-a!==r.delta)||a!==undefined&&b===undefined&&path.length>=a+r.delta;
}else if(r.kind==='order'){where=[r.from,r.to];fail=positions.has(r.to)&&(!positions.has(r.from)||positions.get(r.from)>positions.get(r.to));}
else if(r.kind==='window'){where=[r.cell];const p=positions.get(r.cell);fail=p!==undefined?(p<r.min||p>r.max):path.length>=r.max;}
else if(r.kind==='parity'){where=[r.cell];const p=positions.get(r.cell);fail=p!==undefined&&(p%2===1)!==(r.value==='odd');}
else if(r.kind==='direction'){where=[r.cell];const p=positions.get(r.cell);fail=p===1||p!==undefined&&p<path.length&&directionAt(level,path,p-1)!==r.value||complete&&p===path.length;}
else if(r.kind==='alternation'){for(let i=1;i<path.length;i++){const a=level.cells.find(c=>c.id===path[i-1]),b=level.cells.find(c=>c.id===path[i]);if(a.category===b.category){fail=true;where.push(a.id,b.id);}}}
else if(r.kind==='wall'||r.kind==='door')for(let i=1;i<path.length;i++){if(r.edge.includes(path[i-1])&&r.edge.includes(path[i])&&(r.kind==='wall'||!doorOpen(r,path.slice(0,i)))){fail=true;where=r.edge;}}
else if(r.kind==='gate')for(let i=1;i<path.length;i++){if(r.edge.includes(path[i-1])&&r.edge.includes(path[i])){const s=positions.get(r.switchCell);if(s===undefined||s>i){fail=true;where=r.edge;}}}
if(fail)bad.push({key:r.key,cells:where,message:r.kind==='relation'?'相对步差不符':r.kind==='order'?'标记次序不符':r.kind==='window'?'到达窗口不符':r.kind==='parity'?'该格步号奇偶不符':r.kind==='direction'?'进出方向不符':r.kind==='wall'?'永久墙不能穿过':r.kind==='door'?'这一步门是关闭的':r.kind==='gate'?'开关尚未打开此门':'蓝橙颜色必须交替'});
}
return bad;
}
function canAppend(level,path,id){
const g=graph(level);if(path.includes(id)||!g.ids.has(id))return false;
if(!path.length)return !g.clues.has(1)||g.cells[g.clues.get(1)].id===id;
const last=path.at(-1);if(edgeBlocked(level,path,last,id))return false;if(!g.adj[g.ids.get(last)].includes(g.ids.get(id)))return false;
if((level.features||[]).includes('alternation')&&g.cells[g.ids.get(last)].category===g.cells[g.ids.get(id)].category)return false;
for(const gate of level.gates||[])if(gate.edge.includes(last)&&gate.edge.includes(id)&&!path.includes(gate.switchCell))return false;
return true;
}
function solve(level,{limit=2,omit=null,ignoreClues=false,prune=true,nodeLimit=Infinity}={}){
const g=graph(level),n=g.cells.length,results=[],path=[],seen=new Set();let nodes=0;
const active=constraints(level).filter(r=>r.key!==omit),alternating=active.some(r=>r.kind==='alternation');
const adj=g.adj.map((a,i)=>a.filter(j=>(!alternating||g.cells[i].category!==g.cells[j].category)&&!active.some(r=>r.kind==='wall'&&r.edge.includes(g.cells[i].id)&&r.edge.includes(g.cells[j].id))));
const forced=ignoreClues?new Map():g.clues;
function connected(cur){const reached=new Set([cur]),queue=[cur];for(let k=0;k<queue.length;k++)for(const j of adj[queue[k]])if(!seen.has(j)&&!reached.has(j)){reached.add(j);queue.push(j);}return reached.size===n-path.length+1;}
function visit(i){if(++nodes>nodeLimit)throw Error('求解节点预算耗尽,不能视为唯一');seen.add(i);path.push(g.cells[i].id);
const k=path.length;
if(!violations(level,path,{omit,ignoreClues}).length){
if(k===n)results.push([...path]);
else if(!prune||connected(i)){
const f=forced.get(k+1);let next=adj[i].filter(j=>!seen.has(j)&&(f===undefined||j===f));
next.sort((a,b)=>adj[a].filter(x=>!seen.has(x)).length-adj[b].filter(x=>!seen.has(x)).length);
for(const j of next){if(!ignoreClues&&g.cells[j].clue!==undefined&&g.cells[j].clue!==k+1)continue;
let blocked=edgeBlocked(level,path,g.cells[i].id,g.cells[j].id,{omit});for(const r of active)if(r.kind==='gate'&&r.edge.includes(g.cells[i].id)&&r.edge.includes(g.cells[j].id)&&!path.includes(r.switchCell))blocked=true;
if(!blocked)visit(j);if(results.length>=limit)break;
}
}
}
path.pop();seen.delete(i);
}
let starts=forced.has(1)?[forced.get(1)]:g.cells.map((_,i)=>i);
for(const i of starts){if(!ignoreClues&&g.cells[i].clue!==undefined&&g.cells[i].clue!==1)continue;visit(i);if(results.length>=limit)break;}
return results;
}
function hint(solution,path){let k=0;while(k<path.length&&path[k]===solution[k])k++;if(k<path.length)return {kind:k?'backtrack':'restart',cell:k?path[k-1]:solution[0]};return k===solution.length?{kind:'none'}:{kind:k?'next':'start',cell:solution[k]};}
function assertStructure(level){
const g=graph(level),n=g.cells.length;if(level.features?.includes('direction')&&level.geometry!=='square')throw Error('通道仅适用单位方格');if(n<3||n>20)throw Error('格数须为3–20');
if(level.geometry==='hex'){const keys=new Set();for(const c of g.cells){if(!Number.isInteger(c.q)||!Number.isInteger(c.r))throw Error('六边坐标');const key=c.q+','+c.r;if(keys.has(key))throw Error('六边重叠');keys.add(key);}}
else for(const c of g.cells){if(!Number.isInteger(c.x)||!Number.isInteger(c.y))throw Error('坐标');const u=units(c);if(!u.length||new Set(u.map(p=>p.join(','))).size!==u.length)throw Error('异形重复');const seen=new Set([u[0].join(',')]);for(let changed=true;changed;){changed=false;for(const [x,y]of u)if(!seen.has(x+','+y)&&[[1,0],[-1,0],[0,1],[0,-1]].some(([a,b])=>seen.has((x+a)+','+(y+b)))){seen.add(x+','+y);changed=true;}}if(seen.size!==u.length)throw Error('异形不连通');}
const reach=new Set([0]),queue=[0];for(let k=0;k<queue.length;k++)for(const j of g.adj[queue[k]])if(!reach.has(j)){reach.add(j);queue.push(j);}if(reach.size!==n)throw Error('盘面不连通');
if((level.doors||[]).length>1)throw Error('步进门只允许一道门');
const physicalEdges=[...(level.walls||[]),...(level.doors||[]).map(d=>d.edge)].map(e=>[...e].sort().join('|'));if(new Set(physicalEdges).size!==physicalEdges.length)throw Error('墙门共边重复');
const pairs=[];
for(const r of constraints(level)){
for(const id of [r.cell,r.from,r.to,r.switchCell,...(r.edge||[])].filter(Boolean))if(!g.ids.has(id))throw Error('约束引用不存在');
if(r.kind==='relation'&&(!Number.isInteger(r.delta)||r.delta<1||r.delta>=n||r.from===r.to))throw Error('步差');
if(r.kind==='order')pairs.push([r.from,r.to]);
if(r.kind==='window'&&(!Number.isInteger(r.min)||!Number.isInteger(r.max)||r.min<1||r.max>n||r.max-r.min<2||g.cells[g.ids.get(r.cell)].clue!==undefined))throw Error('窗口需允许至少两个同奇偶步号且不与数字重叠');
if(r.kind==='parity'&&(!['odd','even'].includes(r.value)||g.cells[g.ids.get(r.cell)].clue!==undefined))throw Error('奇偶字段或同格数字冲突');
if(r.kind==='direction'&&!Object.keys(directionNames).includes(r.value))throw Error('方向');
if((r.kind==='wall'||r.kind==='door')&&(r.edge.length!==2||!g.adj[g.ids.get(r.edge[0])].includes(g.ids.get(r.edge[1]))))throw Error('墙或门不在共边');
if(r.kind==='gate'&&(r.edge.length!==2||!g.adj[g.ids.get(r.edge[0])].includes(g.ids.get(r.edge[1]))))throw Error('门不在共边');
}
if((level.gates||[]).length>3||new Set((level.gates||[]).map(r=>r.switchCell)).size!==(level.gates||[]).length)throw Error('最多三门且开关一对一');
if((level.features||[]).includes('alternation')&&g.cells.some(c=>!['circle','triangle'].includes(c.category)))throw Error('缺少类别');
function cycle(v,stack=new Set(),done=new Set()){if(stack.has(v))throw Error('先后链循环');if(done.has(v))return;stack.add(v);for(const [a,b]of pairs)if(a===v)cycle(b,stack,done);stack.delete(v);done.add(v);}for(const [a]of pairs)cycle(a);
return g;
}
// Small offline Web Audio cues: no network or audio-file dependency.
class CasualAudio {
constructor(host,storage,key){this.host=host;this.storage=storage;this.key=key;this.context=null;this.lastEvent=null;this.lastAt={};try{this.enabled=storage.getItem(key)!=='off';}catch{this.enabled=true;}}
unlock(){if(!this.enabled)return false;try{const Audio=this.host.AudioContext||this.host.webkitAudioContext;if(!Audio)return false;if(!this.context)this.context=new Audio();if(this.context.state==='suspended')this.context.resume()?.catch(()=>{});return true;}catch{return false;}}
toggle(){this.enabled=!this.enabled;try{this.storage.setItem(this.key,this.enabled?'on':'off');}catch{}if(this.enabled)this.unlock();return this.enabled;}
play(kind,delay=0){if(!this.unlock())return;const ctx=this.context,now=ctx.currentTime;const gap=kind==='move'?.045:kind==='error'?.18:0;if(this.lastAt[kind]!==undefined&&now-this.lastAt[kind]<gap)return;this.lastAt[kind]=now;this.lastEvent=kind;
const cues={move:[[523,.07,0]],correct:[[659,.09,0],[880,.14,.075]],error:[[330,.10,0],[247,.15,.09]],undo:[[587,.07,0],[440,.10,.055]],restart:[[523,.075,0],[392,.09,.065],[523,.11,.14]],complete:[[523,.12,0],[659,.12,.10],[784,.14,.20],[1046,.25,.32]]};
for(const [frequency,duration,offset]of cues[kind]||[]){try{const start=now+delay+offset,osc=ctx.createOscillator(),gain=ctx.createGain();osc.type='sine';osc.frequency.setValueAtTime(frequency,start);gain.gain.setValueAtTime(.0001,start);gain.gain.linearRampToValueAtTime(kind==='error'?.055:.075,start+.008);gain.gain.exponentialRampToValueAtTime(.0001,start+duration);osc.connect(gain);gain.connect(ctx.destination);osc.onended=()=>{osc.disconnect();gain.disconnect();};osc.start(start);osc.stop(start+duration+.015);}catch{}}
}
}
// The build embeds this module together with the local rule file and JSON.
const pack=PACK;const playLevels=(pack.playOrder||pack.levels.map(l=>l.id)).map(id=>pack.levels.find(l=>l.id===id));const total=playLevels.length;
const BOARD_FONT='ui-rounded, "SF Pro Rounded", "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", system-ui, -apple-system, "Segoe UI", sans-serif';
const UI_THEME={cell:'#E6E7EC',ink:'#23243A',primary:'#6558F5',bright:'#7D72FF',danger:'#E5484D',hint:'#F5A524',ring:'#DFE1EC'};let toastTimer=null;let channelViews=[];let cellViews=[];let inputKind="mouse";
const $=id=>document.getElementById(id),canvas=$('board'),ctx=canvas.getContext('2d');
let level,solution,path=[],index=0,selected='入门',polygons=[],centers=new Map(),unit=1,focus=[],drag=false,lastPoint=null,cleared=false;
const memoryKey='variant:campaign-50:v1';const sound=new CasualAudio(window,localStorage,memoryKey+':sound');let completed=[];try{completed=JSON.parse(localStorage.getItem(memoryKey)||'[]');}catch{}
const labels=new Map();let started=0,firstDrawAt=null,record=null;let records=[];try{records=JSON.parse(localStorage.getItem(memoryKey+':records')||'[]');}catch{}
function saveRecord(){if(!record)return;record.reasoning=$('reasoning').value;record.elapsedSeconds=Math.round((performance.now()-started)/1000);record.completed=cleared;const copy={...record,endedAt:new Date().toISOString()};const at=records.findIndex(r=>r.attemptId===copy.attemptId);if(at>=0)records[at]=copy;else records.push(copy);try{localStorage.setItem(memoryKey+':records',JSON.stringify(records.slice(-500)));}catch{}}
function setStatus(s){$('status').textContent=s;$('status').className='toast show';if(toastTimer)clearTimeout(toastTimer);toastTimer=setTimeout(()=>{$('status').className='toast';},1800);}function showPlay(){$('screen-select').className='screen hidden';$('screen-play').className='screen enter';draw();}function showSelect(){saveRecord();$('screen-select').className='screen enter';$('screen-play').className='screen hidden';renderSelectors();$('status').className='toast';}
function modeMarkers(){labels.clear();let ids=[...(level.relations||[]).flatMap(r=>[r.from,r.to]),...(level.orderChains||[]).flat()];for(const id of ids)if(!labels.has(id))labels.set(id,String.fromCharCode(65+labels.size));}
function makeRecord(){return {attemptId:Date.now().toString(36)+'-'+Math.random().toString(36).slice(2,8),mode:pack.mode,level:level.id,contentRevision:level.contentRevision,order:level.order,chapter:level.chapter,title:level.title,startedAt:new Date().toISOString(),thinkingBeforeFirstDrawSeconds:null,elapsedSeconds:0,firstError:null,backtracks:0,restarts:0,hints:0,completed:false,reasoning:''};}
function start(i,open=true){if(record)saveRecord();index=i;level=playLevels[i];solution=solve(level)[0];if(!solution)throw Error('没有合法路线');path=[];focus=[];cleared=false;started=performance.now();firstDrawAt=null;record=makeRecord();$('reasoning').value='';modeMarkers();$('goal').textContent=level.title;$('caption').textContent=level.caption||'先观察,再画;错了可以退回。';$('title').textContent='数字一笔';$('rule-text').textContent=rulesFor(level);try{localStorage.setItem(memoryKey+':current',level.id);}catch{}setStatus('从数字 1 开始;没有 1 时,可尝试任意格。');renderConstraints();renderSelectors();if(open)showPlay();draw();}
function renderSelectors(){
const list=$('pack-list');list.replaceChildren();for(const chapter of pack.chapters){const section=document.createElement('section');section.className='pack';const head=document.createElement('div');head.className='pack-title';const title=document.createElement('span');title.textContent=chapter.title;const count=document.createElement('span');count.className='pack-progress';const chapterLevels=playLevels.filter(l=>l.chapter===chapter.id);count.textContent=chapterLevels.filter(l=>completed.includes(l.id)).length+' / '+chapterLevels.length;head.append(title,count);const grid=document.createElement('div');grid.className='level-grid';for(const l of chapterLevels){const i=playLevels.indexOf(l),button=document.createElement('button');button.levelId=l.id;button.textContent=String(playLevels.indexOf(l)+1);button.title=l.title;button.className='level-tile'+(completed.includes(l.id)?' cleared':'')+(i===index?' current':'');button.onclick=()=>start(i);grid.append(button);}section.append(head,grid);list.append(section);}
$('position').textContent=level.chapter+'-'+(playLevels.filter(l=>l.chapter===level.chapter).indexOf(level)+1);$('level-sub').textContent=level.chapterTitle+' · 第'+(index+1)+'关'+(level.tutorial?' · 引导':'');$('progress').textContent=completed.filter(id=>playLevels.some(l=>l.id===id)).length+' / '+total;$('overall-fill').style.width=(completed.filter(id=>playLevels.some(l=>l.id===id)).length/total*100)+'%';$('btn-continue').textContent='继续 · '+level.chapter+'-'+(playLevels.filter(l=>l.chapter===level.chapter).indexOf(level)+1);$('level-progress').replaceChildren();for(const l of playLevels.filter(l=>l.chapter===level.chapter)){const seg=document.createElement('i');seg.className=(completed.includes(l.id)?'cleared ':'')+(l.id===level.id?'current':'');$('level-progress').append(seg);}
}
function rulesFor(l){const a=['每格恰好经过一次,共边才能走;固定数字是访问步号。'];if(l.geometry==='blocks')a.push('大格和异形整块只算一步。');if(l.geometry==='hex')a.push('六边形有六个共边方向。');if(l.features.includes('direction'))a.push('通道连接哪两边,就从哪两边进出,可反向;通道不能是端点。');if(l.features.includes('alternation'))a.push('蓝→橙→蓝,每一步换颜色;○蓝,△橙。');if(l.features.includes('parity'))a.push('奇/偶标记要求该格访问步号的奇偶,不按面积计数。');if(l.features.includes('walls'))a.push('黑黄墙永久封闭这条共边。');if(l.features.includes('rhythm-door'))a.push('门空盘关,起点也切换,每落一格切换一次;开门时通过后保持开启。回退撤销穿门那一步或重开后,恢复切换。');return a.join(' ');}
function renderConstraints(){const list=$('constraints');list.replaceChildren();const add=(text,ids)=>{const b=document.createElement('button');b.className='chip';b.textContent=text;b.onclick=()=>{focus=ids;draw();};list.append(b);};if(level.geometry==='blocks')add('大格一步',level.cells.filter(c=>c.w*c.h>1).map(c=>c.id));if(level.features.includes('direction'))add('通道 · 按图进出',level.cells.filter(c=>c.direction).map(c=>c.id));if(level.features.includes('alternation'))add('蓝 → 橙 → 蓝',[]);if(level.features.includes('parity'))add('奇/偶 · 看步号',level.cells.filter(c=>c.parity).map(c=>c.id));if(level.walls?.length)add('永久墙',level.walls.flat());for(const d of level.doors||[])add(doorLatched(d,path)?'已通过 · 门保持开启':('已走'+path.length+'格 · 未通过 · '+(doorOpen(d,path)?'门开,绕行一步关':'门关,走一步开')),d.edge);}
function localGeometry(){
const raw=[],center=new Map();for(const c of level.cells){if(level.geometry==='hex'){
// Rotate the axial strip into portrait orientation; adjacency remains axial.
const x=1.5*c.r,y=Math.sqrt(3)*(c.q+c.r/2);center.set(c.id,[x,y]);let p=[];for(let a=0;a<6;a++){const angle=a*Math.PI/3;p.push([x+Math.cos(angle),y+Math.sin(angle)]);}raw.push({id:c.id,p});
}else{const u=units(c);center.set(c.id,[u.reduce((s,p)=>s+p[0]+.5,0)/u.length,u.reduce((s,p)=>s+p[1]+.5,0)/u.length]);for(const [x,y]of u)raw.push({id:c.id,p:[[x,y],[x+1,y],[x+1,y+1],[x,y+1]]});}}
return {raw,center};
}
function draw(){if(!level)return;const width=canvas.clientWidth,height=canvas.clientHeight;if(!width||!height)return;const dpr=window.devicePixelRatio||1;canvas.width=Math.round(width*dpr);canvas.height=Math.round(height*dpr);ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,width,height);
const {raw,center}=localGeometry(),all=raw.flatMap(r=>r.p),xmin=Math.min(...all.map(p=>p[0])),xmax=Math.max(...all.map(p=>p[0])),ymin=Math.min(...all.map(p=>p[1])),ymax=Math.max(...all.map(p=>p[1]));unit=Math.min(level.cells.length<=6?96:110,(width-20)/(xmax-xmin),(height-20-height*.06)/(ymax-ymin));const ox=(width-unit*(xmax-xmin))/2-unit*xmin,oy=(height-unit*(ymax-ymin))/2-unit*ymin-height*.03;const tr=([x,y])=>[ox+x*unit,oy+y*unit];polygons=raw.map(r=>({...r,p:r.p.map(tr)}));centers=new Map([...center].map(([id,p])=>[id,tr(p)]));
// Place irregular-block labels inside the nearest occupied unit, as in the original view.
if(level.geometry!=='hex')for(const c of level.cells)if(c.shape){const middle=center.get(c.id);const candidates=units(c).map(([x,y])=>[x+.5,y+.5]);candidates.sort((a,b)=>Math.hypot(a[0]-middle[0],a[1]-middle[1])-Math.hypot(b[0]-middle[0],b[1]-middle[1]));centers.set(c.id,tr(candidates[0]));}
const bad=violations(level,path);if(level.tutorial&&!bad.length&&!cleared)focus=path.length?[solution[path.length]]:level.cells.filter(c=>c.direction).map(c=>c.id);const badIds=new Set(bad.flatMap(b=>b.cells));channelViews=[];cellViews=[];
for(let i=1;i<path.length;i++)drawBridge(path[i-1],path[i]);
for(const c of level.cells){const visited=path.includes(c.id),start=c.clue===1,ring=c.clue!==undefined||c.parity||c.direction;const alternate=c.category!==undefined;let fill=badIds.has(c.id)?UI_THEME.danger:alternate?(c.category==='circle'?'#64acf3':'#ffae55'):cleared?UI_THEME.bright:visited||start?UI_THEME.primary:UI_THEME.cell;const foreground=badIds.has(c.id)||!alternate&&(visited||start)?'#FFFFFF':UI_THEME.ink;const cellUnit=level.geometry==='hex'?Math.sqrt(3)*unit:unit;cellViews.push({cell:c.id,fill,error:badIds.has(c.id),category:c.category||null,cellUnit});if(focus.includes(c.id))paintCell(c,cellUnit*.008,UI_THEME.hint);if(ring)paintCell(c,cellUnit*.045,visited||start?UI_THEME.ring:UI_THEME.ink);paintCell(c,cellUnit*.05+(ring?Math.max(2,cellUnit*.05):0),fill);
const [x,y]=centers.get(c.id),step=path.indexOf(c.id)+1;let value=step||c.clue;const ratio=value===1?.6:value>=10?.46:.52,font=Math.max(14,Math.min(52,cellUnit*ratio));ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillStyle=foreground;
if(c.direction){const radius=Math.min(unit*.26,28),state=directionState(c,badIds);drawDirectionPath(c,x,y);drawChannel(c.direction,x,y,radius,state);channelViews.push({cell:c.id,kind:c.direction,center:[x,y],radius,state});}
if(value&&c.direction){const size=Math.max(10,Math.min(18,unit*.22));ctx.fillStyle=visited||start?'#FFFFFF':UI_THEME.cell;roundedRect(x+unit*.105,y-unit*.38,unit*.28,unit*.28,unit*.07);ctx.fill();ctx.fillStyle=UI_THEME.ink;ctx.font='800 '+size+'px '+BOARD_FONT;ctx.fillText(String(value),x+unit*.245,y-unit*.24);}
else if(value){ctx.font='800 '+font+'px '+BOARD_FONT;ctx.fillText(String(value),x,y+(c.direction?unit*.09:c.parity?-unit*.06:0)+font*.04);if(c.parity){ctx.font='700 '+Math.max(10,unit*.19)+'px '+BOARD_FONT;ctx.fillText(c.parity==='odd'?'奇':'偶',x,y+unit*.27);}if(c.category){ctx.font='700 '+Math.max(10,unit*.18)+'px '+BOARD_FONT;const mismatch=c.clue!==undefined&&step&&step!==c.clue;ctx.fillText(c.category==='circle'?'○':'△',x+(mismatch?unit*.28:0),y+(mismatch?unit*.27:-unit*.3));}if(c.clue!==undefined&&step&&step!==c.clue){ctx.font='800 '+Math.max(10,unit*.19)+'px '+BOARD_FONT;ctx.fillText('需'+c.clue,x,y-unit*.3);}}
else if(c.parity){ctx.font='700 '+Math.max(16,unit*.44)+'px '+BOARD_FONT;ctx.fillText(c.parity==='odd'?'奇':'偶',x,y+unit*.015);}else if(c.category){ctx.font='700 '+Math.max(14,unit*.36)+'px '+BOARD_FONT;ctx.fillText(c.category==='circle'?'○':'△',x,y);}
}
for(const edge of level.walls||[])drawBarrier(edge,'wall');for(const d of level.doors||[])drawBarrier(d.edge,doorLatched(d,path)?'latched':doorOpen(d,path)?'open':'closed');
updateTutorial(bad);$('remaining-n').textContent=cleared?'完成':String(level.cells.length-path.length);$('remaining').className='remaining'+(cleared?' done':'');if(level.doors?.length)renderConstraints();if(bad.length)setStatus(bad[0].message+',沿来时路逐格退回。');$('undo').disabled=!path.length;$('next').hidden=!cleared||index===playLevels.length-1;
}
function roundedRect(x,y,w,h,r){r=Math.min(r,w/2,h/2);ctx.beginPath();ctx.moveTo(x+r,y);ctx.lineTo(x+w-r,y);ctx.quadraticCurveTo(x+w,y,x+w,y+r);ctx.lineTo(x+w,y+h-r);ctx.quadraticCurveTo(x+w,y+h,x+w-r,y+h);ctx.lineTo(x+r,y+h);ctx.quadraticCurveTo(x,y+h,x,y+h-r);ctx.lineTo(x,y+r);ctx.quadraticCurveTo(x,y,x+r,y);ctx.closePath();}
function paintCell(c,inset,colour){ctx.fillStyle=colour;if(level.geometry==='hex'){const p=polygons.find(p=>p.id===c.id).p,[cx,cy]=centers.get(c.id),scale=1-inset/(unit*.86);const pts=p.map(([x,y])=>[cx+(x-cx)*scale,cy+(y-cy)*scale]);ctx.beginPath();for(let i=0;i<6;i++){const a=pts[(i+5)%6],b=pts[i],d=pts[(i+1)%6];const radius=Math.max(2,Math.sqrt(3)*unit*.18-inset*.2),tangent=Math.min(radius/Math.sqrt(3),Math.hypot(a[0]-b[0],a[1]-b[1])*.3);const toward=p=>{const length=Math.hypot(p[0]-b[0],p[1]-b[1]);return [b[0]+(p[0]-b[0])*tangent/length,b[1]+(p[1]-b[1])*tangent/length];};const before=toward(a),after=toward(d);if(i===0)ctx.moveTo(...before);else ctx.lineTo(...before);ctx.quadraticCurveTo(...b,...after);}ctx.closePath();ctx.fill();return;}
const polygonsHere=polygons.filter(p=>p.id===c.id),u=units(c),origin=[polygonsHere[0].p[0][0]-u[0][0]*unit,polygonsHere[0].p[0][1]-u[0][1]*unit],radius=Math.max(2,unit*.18-inset*.2);
if(!c.shape){roundedRect(origin[0]+c.x*unit+inset,origin[1]+c.y*unit+inset,c.w*unit-inset*2,c.h*unit-inset*2,radius);ctx.fill();return;}
const boundary=outlineUnits(c).map(([x,y])=>[origin[0]+x*unit,origin[1]+y*unit]);
const insetPoints=boundary.map((b,i)=>{const a=boundary[(i+boundary.length-1)%boundary.length],d=boundary[(i+1)%boundary.length];const n=([x,y],[xx,yy])=>{const len=Math.hypot(xx-x,yy-y);return [-(yy-y)/len,(xx-x)/len];};const n1=n(a,b),n2=n(b,d);return [b[0]+(n1[0]+n2[0])*inset,b[1]+(n1[1]+n2[1])*inset];});
ctx.beginPath();for(let i=0;i<insetPoints.length;i++){const a=insetPoints[(i+insetPoints.length-1)%insetPoints.length],b=insetPoints[i],d=insetPoints[(i+1)%insetPoints.length];const cross=(b[0]-a[0])*(d[1]-b[1])-(b[1]-a[1])*(d[0]-b[0]);const r=cross>0?Math.min(radius,Math.hypot(b[0]-a[0],b[1]-a[1])/2,Math.hypot(d[0]-b[0],d[1]-b[1])/2):0;const toward=(p)=>{const len=Math.hypot(p[0]-b[0],p[1]-b[1]);return [b[0]+(p[0]-b[0])*r/len,b[1]+(p[1]-b[1])*r/len];};const before=toward(a),after=toward(d);if(i===0)ctx.moveTo(...before);else ctx.lineTo(...before);ctx.quadraticCurveTo(...b,...after);}ctx.closePath();ctx.fill();}
function drawBridge(a,b){const segments=sharedSegments(a,b);if(!segments.length)return;const [p,q]=segments[0],cx=(p[0]+q[0])/2,cy=(p[1]+q[1])/2,len=Math.hypot(q[0]-p[0],q[1]-p[1]),nx=-(q[1]-p[1])/len,ny=(q[0]-p[0])/len;ctx.beginPath();ctx.moveTo(cx-nx*unit*.09,cy-ny*unit*.09);ctx.lineTo(cx+nx*unit*.09,cy+ny*unit*.09);ctx.lineWidth=(level.geometry==='hex'?Math.sqrt(3)*unit:unit)*.46;ctx.lineCap='butt';ctx.strokeStyle=cleared?UI_THEME.bright:UI_THEME.primary;ctx.stroke();}
function sharedSegments(a,b){const edges=[];for(const p of polygons.filter(p=>p.id===a))for(const q of polygons.filter(p=>p.id===b))for(let i=0;i<p.p.length;i++)for(let j=0;j<q.p.length;j++){const x=p.p[i],y=p.p[(i+1)%p.p.length],v=q.p[j],w=q.p[(j+1)%q.p.length];if(Math.hypot(x[0]-w[0],x[1]-w[1])<.01&&Math.hypot(y[0]-v[0],y[1]-v[1])<.01)edges.push([x,y]);}return edges;}
function sideOf(c,id){const a=level.cells.find(a=>a.id===id);return a.x<c.x?'l':a.x>c.x?'r':a.y<c.y?'u':'d';}
function directionState(c,badIds=new Set()){const k=path.indexOf(c.id);if(k<0)return 'idle';if(k===0||badIds.has(c.id))return 'error';const entry=sideOf(c,path[k-1]);if(!c.direction.includes(entry))return 'error';if(k<path.length-1){const exit=sideOf(c,path[k+1]);if([entry,exit].sort().join('')!==c.direction)return 'error';}return 'correct';}
function drawDirectionPath(c,x,y){const k=path.indexOf(c.id);if(k<0)return;const vectors={l:[-1,0],r:[1,0],u:[0,-1],d:[0,1]};ctx.beginPath();if(k>0){const [dx,dy]=vectors[sideOf(c,path[k-1])];ctx.moveTo(x+dx*unit*.44,y+dy*unit*.44);ctx.lineTo(x,y);}else ctx.moveTo(x,y);if(k<path.length-1){const [dx,dy]=vectors[sideOf(c,path[k+1])];ctx.lineTo(x+dx*unit*.44,y+dy*unit*.44);}ctx.lineCap='round';ctx.lineJoin='round';ctx.lineWidth=unit*.14;ctx.strokeStyle='#BEB8FF';ctx.stroke();}
function drawChannel(kind,x,y,r,state='idle'){const vector={l:[-1,0],r:[1,0],u:[0,-1],d:[0,1]};const [a,b]=kind.split('').map(k=>vector[k]);ctx.beginPath();ctx.moveTo(x+a[0]*r,y+a[1]*r);ctx.lineTo(x,y);ctx.lineTo(x+b[0]*r,y+b[1]*r);ctx.lineCap='round';ctx.lineJoin='round';ctx.lineWidth=10;ctx.strokeStyle=state==='correct'?'#C8C2FF':state==='error'?'#FFD5D7':'#DFE1EC';ctx.stroke();ctx.lineWidth=5;ctx.strokeStyle=state==='correct'?'#FFFFFF':state==='error'?'#E5484D':UI_THEME.ink;ctx.stroke();}
function drawBarrier(edge,kind){const isOpen=kind==='open'||kind==='latched';const left=polygons.filter(p=>p.id===edge[0]),right=polygons.filter(p=>p.id===edge[1]);let commons=[];for(const a of left)for(const b of right)for(let i=0;i<a.p.length;i++)for(let j=0;j<b.p.length;j++){const p=a.p[i],q=a.p[(i+1)%a.p.length],r=b.p[j],t=b.p[(j+1)%b.p.length];if(Math.hypot(p[0]-t[0],p[1]-t[1])<.01&&Math.hypot(q[0]-r[0],q[1]-r[1])<.01)commons.push([p,q]);}for(const common of commons){const [a,b]=common;const point=f=>[a[0]+(b[0]-a[0])*f,a[1]+(b[1]-a[1])*f];ctx.lineCap='butt';ctx.lineWidth=9;ctx.strokeStyle=isOpen?'#23895f':kind==='wall'?'#272638':'#aa4d25';ctx.beginPath();if(isOpen){ctx.moveTo(...point(.05));ctx.lineTo(...point(.26));ctx.moveTo(...point(.74));ctx.lineTo(...point(.95));}else{ctx.moveTo(...point(.05));ctx.lineTo(...point(.95));}ctx.stroke();if(kind==='wall'){ctx.lineWidth=4;ctx.strokeStyle='#ffc542';ctx.setLineDash([5,4]);ctx.beginPath();ctx.moveTo(...point(.05));ctx.lineTo(...point(.95));ctx.stroke();ctx.setLineDash([]);}if(kind!=='wall'){const [x,y]=point(.5);ctx.font='700 '+Math.max(10,Math.min(12,unit*.3))+'px '+BOARD_FONT;ctx.textAlign='center';ctx.fillStyle=isOpen?'#12623f':'#993b1e';ctx.fillText(kind==='latched'?'已过':isOpen?'开':'关',x+10,y-8);}}}
function inside(x,y,p){let hit=false;for(let i=0,j=p.length-1;i<p.length;j=i++){const [a,b]=p[i],[c,d]=p[j];if((b>y)!==(d>y)&&x<(c-a)*(y-b)/(d-b)+a)hit=!hit;}return hit;}
function hit(x,y){return polygons.find(s=>{if(level.geometry!=='hex')return inside(x,y,s.p);const [cx,cy]=centers.get(s.id),scale=inputKind==='touch'? .92:.86;return inside(x,y,s.p.map(([px,py])=>[cx+(px-cx)*scale,cy+(py-cy)*scale]));})?.id;}
function markError(id,message){sound.play('error');if(!record.firstError)record.firstError={step:path.length+1,cell:id,message};setStatus(message);}
function advance(id){if(!id||cleared)return;const at=path.indexOf(id);if(at!==-1){if(path.length>=2&&at===path.length-2){record.backtracks++;path.pop();sound.play('undo');focus=[];setStatus('已回退,继续观察剩余格。');draw();}return;}
if(!canAppend(level,path,id)){const g=graph(level),adjacent=path.length&&g.adj[g.ids.get(path.at(-1))].includes(g.ids.get(id));if(adjacent||!path.length){markError(id,path.length?(level.walls?.some(e=>e.includes(path.at(-1))&&e.includes(id))?'这是永久墙,不能穿过。':level.doors?.some(d=>d.edge.includes(path.at(-1))&&d.edge.includes(id))?'门现在关闭;绕行落格后会切换。':'同色格不能连续经过,请换另一种颜色。'):'请从数字 1 开始。');}return;}
if(firstDrawAt===null){firstDrawAt=performance.now();record.thinkingBeforeFirstDrawSeconds=Math.round((firstDrawAt-started)/1000);}path.push(id);focus=[];const bad=violations(level,path);if(bad.length)markError(id,bad[0].message);else{const cell=level.cells.find(c=>c.id===id);sound.play(cell.clue!==undefined&&cell.clue===path.length?'correct':'move');setStatus('可松手续画;退回上一格可撤销一步。');}if(path.length===level.cells.length&&!bad.length){cleared=true;record.completed=true;record.elapsedSeconds=Math.round((performance.now()-started)/1000);if(!completed.includes(level.id)){completed.push(level.id);try{localStorage.setItem(memoryKey,JSON.stringify(completed));}catch{}}setStatus(index===playLevels.length-1?total+'关完成!':'完成!');sound.play('complete',.23);saveRecord();renderSelectors();}draw();}
function point(e){const r=canvas.getBoundingClientRect();return [e.clientX-r.left,e.clientY-r.top];}
canvas.addEventListener('pointerdown',e=>{e.preventDefault();sound.unlock();inputKind=e.pointerType||'mouse';drag=true;canvas.setPointerCapture(e.pointerId);lastPoint=point(e);advance(hit(...lastPoint));});
canvas.addEventListener('pointermove',e=>{if(!drag)return;e.preventDefault();const p=point(e),a=lastPoint||p,d=Math.hypot(p[0]-a[0],p[1]-a[1]),steps=Math.ceil(d/Math.max(3,unit*.1));for(let i=1;i<=steps;i++)advance(hit(a[0]+(p[0]-a[0])*i/steps,a[1]+(p[1]-a[1])*i/steps));lastPoint=p;});
function end(){drag=false;lastPoint=null;}canvas.addEventListener('pointerup',end);canvas.addEventListener('pointercancel',end);
$('undo').onclick=()=>{sound.unlock();if(path.length){path.pop();sound.play('undo');record.backtracks++;record.completed=false;cleared=false;focus=[];setStatus('已撤销一步。');draw();}};
$('restart').onclick=()=>{sound.unlock();sound.play('restart');path=[];cleared=false;focus=[];record.restarts++;record.completed=false;setStatus('已重开。');draw();};
$('hint').onclick=()=>{record.hints++;const h=hint(solution,path);focus=h.kind==='none'?[]:[h.cell];setStatus(h.kind==='backtrack'?'请沿来时路逐格退回黄色边框格,再继续。':h.kind==='restart'?'起点需要调整,请重开并从黄色边框格开始。':h.kind==='none'?'已经完成。':'下一步在黄色边框格;提示不会自动画路。');draw();};
$('next').onclick=()=>{if(index+1<playLevels.length)start(index+1);};
$('export').onclick=()=>{saveRecord();const payload={version:pack.version,exportedAt:new Date().toISOString(),clearedIds:completed,records};const blob=new Blob([JSON.stringify(payload,null,2)],{type:'application/json'}),a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='NumberSwipe-DEMO53-试玩记录.json';a.click();setTimeout(()=>URL.revokeObjectURL(a.href),1000);setStatus('已导出全部试玩记录。');};
window.addEventListener('pagehide',saveRecord);
$('btn-back').onclick=showSelect;$('btn-continue').onclick=showPlay;$('btn-rules').onclick=()=>{$('rules-sheet').hidden=false;};$('close-rules').onclick=()=>{$('rules-sheet').hidden=true;};$('btn-feedback').onclick=()=>{$('feedback-sheet').hidden=false;};$('close-feedback').onclick=()=>{saveRecord();$('feedback-sheet').hidden=true;};
window.addEventListener('resize',draw);window.visualViewport?.addEventListener('resize',draw);window.addEventListener('orientationchange',draw);let initialId=null;try{initialId=localStorage.getItem(memoryKey+':current');}catch{}let initial=playLevels.findIndex(l=>l.id===initialId);start(initial>=0?initial:0,false);
function renderSoundButton(){$('btn-sound').textContent=sound.enabled?'音效开':'音效关';$('btn-sound').title=sound.enabled?'关闭音效':'开启音效';$('btn-sound').setAttribute?.('aria-pressed',String(sound.enabled));}
$('btn-sound').onclick=()=>{sound.toggle();renderSoundButton();};renderSoundButton();
function updateTutorial(bad){if(!level.tutorial)return;let text=level.caption;if(bad.length)text='进出方向不对,请退回上一格,按通道连接的两边走。';else if(cleared)text='学会了!接下来试试正式关卡。';else if(level.tutorial==='colour')text=path.length===1?'下一步去橙色块;同色蓝块不能连续走。':path.length===2?'再换回蓝色,完成蓝 → 橙 → 蓝。':level.caption;else{const c=level.cells.find(c=>c.direction);const at=path.indexOf(c.id);if(at>=0&&at===path.length-1)text=level.tutorial==='straight'?'已从左边进入:沿横线向右离开,不能拐弯。':'已从左边进入:沿拐角向上离开,不能直走。';} $('caption').textContent=text;}
// Read-only snapshot used for interaction verification; never supplies an answer.
window.variantSnapshot=()=>({mode:pack.mode,level:level.id,order:level.order,displayOrder:index+1,tutorial:level.tutorial||null,soundEnabled:sound.enabled,lastSound:sound.lastEvent,chapter:level.chapter,records:records.length,cellViews,geometry:level.geometry,directionMarkers:channelViews,screen:$('screen-play').className.includes('hidden')?'select':'play',path:[...path],violations:violations(level,path),cleared,centers:Object.fromEntries(centers),unit,openedGates:(level.gates||[]).map(g=>path.includes(g.switchCell)),doors:(level.doors||[]).map(d=>doorOpen(d,path)),latchedDoors:(level.doors||[]).map(d=>doorLatched(d,path))});
</script></body></html>