feat(levels): two-sided endgame release, and a board to track the 50-level run
The chain built its own worst moment: hero and tail deadlock at orientations 0
and +2 and open only at +1, with +3/+4/+5 left entirely to chance. That is why
some levels released at exactly one of six orientations, and why every model
kept writing "I don't know which way to turn" — at +2 one direction was right
and the other five were dead ends.
createDeepLock now traces candidates at all six orientations and requires the
tail to also release at +3, certified with the real swept tracer. A player
stuck at +2 turns either way and something moves. Measured by the rule bot
over the regenerated set: blind rotations, wrong-way turns and extra rotations
all go to zero, from 1.17 / 0.44 / 1.22 per level. Endgame orientation counts
shift from {1:4, 2:10, 3:4} to {2:3, 3:13, 4:1}.
It costs yield: switchback r3 and crossing r3 now produce nothing, and the set
comes out at 17 levels rather than 18. The test witnesses moved with it — a
witness is whichever seed still satisfies the construction, so they are
re-derived from the sweep rather than pinned.
docs/level-board.html is the task board for getting to 50 levels. Task state is
in bench/board.json; every number on the page — candidate count, rotation
quota, blind turns, monkey actions-per-piece, the agent batches — is measured
from the level set and the run archive at build time.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
{
|
||||
"goal": "调出 50 关难度合适、好玩的关卡,交付人工审核",
|
||||
"updated": "09-14 16:21",
|
||||
"round": 0,
|
||||
"definition": {
|
||||
"title": "「难度合适且好玩」在这套度量下的操作定义",
|
||||
"why": "评分里只有 difficulty(复测 r=0.93)和 stuck(r=0.85)可信;rotation / clarity / fun 的复测 r 只有 0.32 / 0.19 / 0.56,是模型噪声,不能拿来调参。所以「好玩」必须换成机械代理。",
|
||||
"rules": [
|
||||
{
|
||||
"k": "难度",
|
||||
"v": "agent 的 difficulty 评分沿关号上升,末档逼近 3.0(3 = 刚刚好),全程不超过 3.2",
|
||||
"how": "grok 抽样评分"
|
||||
},
|
||||
{
|
||||
"k": "不挫败",
|
||||
"v": "stuck ≤ 2.0,盲转 = 0,转错方向 = 0",
|
||||
"how": "规则机器人全量扫,12 秒"
|
||||
},
|
||||
{
|
||||
"k": "不无脑",
|
||||
"v": "猴子玩家 动作÷棋子 ≥ 3.0(v0.5 全库 1.67 = 乱点就能过)",
|
||||
"how": "生成时算"
|
||||
},
|
||||
{
|
||||
"k": "旋转是主线",
|
||||
"v": "0 旋转关 ≤ 5 关(10%),且随关号推进旋转数递增",
|
||||
"how": "求解器"
|
||||
},
|
||||
{
|
||||
"k": "不重复",
|
||||
"v": "50 关盘面签名互不相同,路线家族分布不偏科",
|
||||
"how": "deepLevelKey 去重"
|
||||
}
|
||||
]
|
||||
},
|
||||
"targets": {
|
||||
"levels": 50,
|
||||
"rotationQuota": {
|
||||
"0": 5,
|
||||
"1": 12,
|
||||
"2": 15,
|
||||
"3": 18
|
||||
},
|
||||
"difficultyCurve": [
|
||||
{
|
||||
"band": "L1–L12",
|
||||
"rot": "0–1 转",
|
||||
"difficulty": "1.5 – 2.0"
|
||||
},
|
||||
{
|
||||
"band": "L13–L32",
|
||||
"rot": "1–2 转",
|
||||
"difficulty": "2.0 – 2.6"
|
||||
},
|
||||
{
|
||||
"band": "L33–L50",
|
||||
"rot": "2–3 转",
|
||||
"difficulty": "2.6 – 3.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"phases": [
|
||||
{
|
||||
"id": "P0",
|
||||
"title": "判据:先说清楚什么叫「好」",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "T0.1",
|
||||
"title": "难度曲线与旋转配额定义",
|
||||
"status": "done",
|
||||
"note": "50 关分三档,0 转关卡死 10% 上限(用户硬要求)"
|
||||
},
|
||||
{
|
||||
"id": "T0.2",
|
||||
"title": "机械验收门槛",
|
||||
"status": "done",
|
||||
"note": "盲转=0、转错=0、终局可走朝向≥2 —— 前两项已由改锁结构达成"
|
||||
},
|
||||
{
|
||||
"id": "T0.3",
|
||||
"title": "「好玩」的机械代理指标",
|
||||
"status": "done",
|
||||
"note": "猴子 APT ≥3.0。fun 评分复测 r=0.56 不可用,改用「乱点过不了」当代理"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "P1",
|
||||
"title": "产能:先得有足够多的候选",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "T1.1",
|
||||
"title": "深锁关(3 转)扩产到 ≥60 个候选",
|
||||
"status": "todo",
|
||||
"note": "当前 14 种子出 17 关,需扩大种子与半径扫描"
|
||||
},
|
||||
{
|
||||
"id": "T1.2",
|
||||
"title": "中档(2 转)生成路径",
|
||||
"status": "todo",
|
||||
"note": "deep-lock 固定 3 转,v0.5 最多 2 转 —— 中档要么放宽锁链,要么从 v0.5 筛"
|
||||
},
|
||||
{
|
||||
"id": "T1.3",
|
||||
"title": "简单档(0–1 转)候选池",
|
||||
"status": "todo",
|
||||
"note": "从 v0.5 筛,但必须过猴子 APT 门槛(v0.5 全库中位 1.67,大部分会被刷掉)"
|
||||
},
|
||||
{
|
||||
"id": "T1.4",
|
||||
"title": "去重与家族均衡",
|
||||
"status": "todo",
|
||||
"note": "6 个路线家族不能偏科;v0.5 曾出现 1000 关只有 30 个模板"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "P2",
|
||||
"title": "标定:把机械指标映射到真实难度",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "T2.1",
|
||||
"title": "grok 抽样跑候选池,收集 difficulty / stuck",
|
||||
"status": "todo",
|
||||
"note": "分层抽样,覆盖各旋转档与各家族"
|
||||
},
|
||||
{
|
||||
"id": "T2.2",
|
||||
"title": "拟合 difficulty ~ 机械特征",
|
||||
"status": "todo",
|
||||
"note": "候选特征:旋转数、锁住率、猴子 APT、棋子数、路线长度、终局路口数"
|
||||
},
|
||||
{
|
||||
"id": "T2.3",
|
||||
"title": "用模型给全部候选打分并按曲线选 50 关",
|
||||
"status": "todo"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "P3",
|
||||
"title": "迭代:跑几轮把曲线调平",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "T3.1",
|
||||
"title": "第 1 轮:grok 全量跑 50 关",
|
||||
"status": "todo"
|
||||
},
|
||||
{
|
||||
"id": "T3.2",
|
||||
"title": "按实测曲线调整选关,重选",
|
||||
"status": "todo"
|
||||
},
|
||||
{
|
||||
"id": "T3.3",
|
||||
"title": "第 2 轮验证",
|
||||
"status": "todo"
|
||||
},
|
||||
{
|
||||
"id": "T3.4",
|
||||
"title": "收敛判定(难度曲线单调、stuck 达标)",
|
||||
"status": "todo"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "P4",
|
||||
"title": "交付",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "T4.1",
|
||||
"title": "50 关关卡集",
|
||||
"status": "todo"
|
||||
},
|
||||
{
|
||||
"id": "T4.2",
|
||||
"title": "人工审核页(可直接试玩每一关)",
|
||||
"status": "todo"
|
||||
},
|
||||
{
|
||||
"id": "T4.3",
|
||||
"title": "后续关卡制作流程的优化总结",
|
||||
"status": "todo"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"rounds": [],
|
||||
"log": [
|
||||
{
|
||||
"at": "起点",
|
||||
"text": "已完成前置工作:擦碰漏报修复(113/581 例)、终局单出口门槛、锁结构改为双向可开 —— 规则机器人盲转/转错/多转已全部归零"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta http-equiv="refresh" content="60">
|
||||
<title>50 关任务看板</title>
|
||||
<style>
|
||||
:root{
|
||||
--ground:#EEF1F4; --surface:#FFFFFF; --inset:#E3E8ED; --panel:#F7F9FA;
|
||||
--ink:#141E28; --ink2:#48596A; --ink3:#6B7A88;
|
||||
--rule:#D3DAE1; --rule2:#BFC9D2;
|
||||
--rail:#435F78; --rail-soft:#8FA6BA;
|
||||
--bad:#B23A26; --bad-soft:#F2DDD8;
|
||||
--warn:#8A5C0F; --warn-soft:#F4E9D4;
|
||||
--good:#236E64; --good-soft:#D9EDEA;
|
||||
--live:#1F6FB2; --live-soft:#DCE9F4;
|
||||
--shadow:0 1px 2px rgba(20,30,40,.06),0 6px 18px rgba(20,30,40,.05);
|
||||
color-scheme:light;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){:root:not([data-theme=light]){
|
||||
--ground:#0F161C; --surface:#141D25; --inset:#0A1015; --panel:#141D25;
|
||||
--ink:#E4EAEF; --ink2:#AEBDC9; --ink3:#8494A2;
|
||||
--rule:#28343F; --rule2:#35434F;
|
||||
--rail:#8CB0CC; --rail-soft:#4E6577;
|
||||
--bad:#E4836C; --bad-soft:#3A211B;
|
||||
--warn:#E0AE5E; --warn-soft:#33280F;
|
||||
--good:#4FC7B7; --good-soft:#11302C;
|
||||
--live:#6BB3EE; --live-soft:#12283A;
|
||||
--shadow:0 1px 2px rgba(0,0,0,.3),0 6px 18px rgba(0,0,0,.28);
|
||||
color-scheme:dark;
|
||||
}}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--ground);color:var(--ink);
|
||||
font-family:"PingFang SC","Hiragino Sans GB","Microsoft YaHei",ui-sans-serif,system-ui,-apple-system,sans-serif;
|
||||
line-height:1.7;-webkit-font-smoothing:antialiased}
|
||||
.wrap{max-width:1080px;margin:0 auto;padding:0 24px 80px}
|
||||
code,.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-variant-numeric:tabular-nums}
|
||||
header.head{padding:48px 0 26px;border-bottom:1px solid var(--rule);margin-bottom:30px}
|
||||
.eyebrow{font-size:11.5px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink3);margin-bottom:12px}
|
||||
h1{font-size:clamp(26px,4vw,38px);line-height:1.2;margin:0 0 14px;letter-spacing:-.02em;text-wrap:balance}
|
||||
.meta{font-size:12.5px;color:var(--ink3)}
|
||||
.progress{margin:22px 0 0}
|
||||
.progress .bar{height:10px;background:var(--inset);border-radius:6px;overflow:hidden}
|
||||
.progress .bar i{display:block;height:100%;background:var(--good);border-radius:6px;transition:width .4s}
|
||||
.progress .lbl{display:flex;justify-content:space-between;font-size:12.5px;color:var(--ink2);margin-top:7px}
|
||||
h2{font-size:21px;margin:44px 0 6px;letter-spacing:-.01em}
|
||||
h2 .num{color:var(--rail-soft);font-family:ui-monospace,monospace;font-size:14px;margin-right:9px}
|
||||
p{margin:0 0 12px;max-width:74ch}
|
||||
.sub{color:var(--ink2);font-size:14px}
|
||||
.card{background:var(--surface);border:1px solid var(--rule);border-radius:12px;padding:18px 20px;box-shadow:var(--shadow);margin:16px 0}
|
||||
.callout{border-left:3px solid var(--rail);background:var(--panel);border-radius:0 10px 10px 0;padding:14px 18px;margin:16px 0}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(176px,1fr));margin:16px 0}
|
||||
.tile{background:var(--surface);border:1px solid var(--rule);border-radius:12px;padding:14px 16px}
|
||||
.tile .k{font-size:11px;letter-spacing:.06em;color:var(--ink3);text-transform:uppercase}
|
||||
.tile .v{font-family:ui-monospace,monospace;font-size:25px;font-weight:600;line-height:1.25;margin-top:3px}
|
||||
.tile .n{font-size:12px;color:var(--ink2);margin-top:2px}
|
||||
.tile.ok .v{color:var(--good)} .tile.miss .v{color:var(--bad)} .tile.warn .v{color:var(--warn)}
|
||||
table{width:100%;border-collapse:collapse;font-size:13.5px;margin:14px 0}
|
||||
th,td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--rule)}
|
||||
th{font-size:11px;letter-spacing:.06em;color:var(--ink3);text-transform:uppercase;font-weight:600;white-space:nowrap}
|
||||
td.n,th.n{text-align:right;font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums;white-space:nowrap}
|
||||
.scroll{overflow-x:auto}
|
||||
.tag{display:inline-block;font-size:11px;padding:2px 9px;border-radius:20px;font-weight:600;white-space:nowrap}
|
||||
.tag.good{background:var(--good-soft);color:var(--good)}
|
||||
.tag.bad{background:var(--bad-soft);color:var(--bad)}
|
||||
.tag.live{background:var(--live-soft);color:var(--live)}
|
||||
.tag.idle{background:var(--inset);color:var(--ink3)}
|
||||
.phase{margin:24px 0}
|
||||
.phase h3{font-size:15.5px;margin:0 0 4px;display:flex;align-items:center;gap:10px}
|
||||
.phase h3 .pid{font-family:ui-monospace,monospace;font-size:11.5px;color:var(--surface);background:var(--rail);border-radius:5px;padding:2px 7px}
|
||||
.tasks{list-style:none;margin:10px 0 0;padding:0;border-left:2px solid var(--rule);margin-left:6px}
|
||||
.tasks li{position:relative;padding:9px 0 9px 22px}
|
||||
.tasks li::before{content:'';position:absolute;left:-6px;top:17px;width:10px;height:10px;border-radius:50%;
|
||||
background:var(--ground);border:2px solid var(--rule2)}
|
||||
.tasks li.done::before{background:var(--good);border-color:var(--good)}
|
||||
.tasks li.doing::before{background:var(--live);border-color:var(--live);box-shadow:0 0 0 4px var(--live-soft)}
|
||||
.tasks li.blocked::before{background:var(--bad);border-color:var(--bad)}
|
||||
.tasks .t{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
|
||||
.tasks .tid{font-family:ui-monospace,monospace;font-size:11.5px;color:var(--ink3)}
|
||||
.tasks .tt{font-size:14.5px;font-weight:500}
|
||||
.tasks li.done .tt{color:var(--ink2)}
|
||||
.tasks .note{font-size:12.5px;color:var(--ink3);margin-top:1px}
|
||||
.rules li{margin-bottom:8px}
|
||||
.rules b{font-weight:650}
|
||||
.rules .how{font-size:12px;color:var(--ink3);margin-left:6px}
|
||||
.log{list-style:none;padding:0;margin:12px 0}
|
||||
.log li{padding:8px 0 8px 16px;border-left:2px solid var(--rule);font-size:13.5px;color:var(--ink2)}
|
||||
.log li b{color:var(--ink);font-weight:600;font-family:ui-monospace,monospace;font-size:12px;margin-right:8px}
|
||||
footer{margin-top:56px;padding-top:20px;border-top:1px solid var(--rule);color:var(--ink3);font-size:12px}
|
||||
@media(max-width:640px){.wrap{padding:0 16px 60px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<header class="head">
|
||||
<div class="eyebrow">HEX TRAILS · 关卡调优</div>
|
||||
<h1>调出 50 关难度合适、好玩的关卡,交付人工审核</h1>
|
||||
<p class="meta mono">第 0 轮 · 更新于 09-14 16:21 · 本页每 60 秒自动刷新</p>
|
||||
<div class="progress">
|
||||
<div class="bar"><i style="width:18%"></i></div>
|
||||
<div class="lbl"><span>3 / 17 项完成</span><span class="mono">18%</span></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<h2><span class="num">A</span>现在的状态</h2>
|
||||
<div class="grid">
|
||||
<div class="tile "><div class="k">候选关卡</div><div class="v">17 <span style="font-size:14px;color:var(--ink3)">/ 50</span></div><div class="n">当前关卡集</div></div>
|
||||
<div class="tile ok"><div class="k">盲转 / 关</div><div class="v">0.00</div><div class="n">目标 0</div></div>
|
||||
<div class="tile ok"><div class="k">转错方向 / 关</div><div class="v">0.00</div><div class="n">目标 0</div></div>
|
||||
<div class="tile ok"><div class="k">猴子 动作÷棋子</div><div class="v">5.07</div><div class="n">目标 ≥3.0(不无脑)</div></div>
|
||||
</div>
|
||||
|
||||
<div class="scroll"><table>
|
||||
<thead><tr><th>旋转数</th><th class="n">目标关数</th><th class="n">现有</th><th>缺口</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>0 转</td><td class="n">5</td><td class="n">0</td><td><span class="tag idle">还差 5</span></td></tr>
|
||||
<tr><td>1 转</td><td class="n">12</td><td class="n">0</td><td><span class="tag idle">还差 12</span></td></tr>
|
||||
<tr><td>2 转</td><td class="n">15</td><td class="n">0</td><td><span class="tag idle">还差 15</span></td></tr>
|
||||
<tr><td>3 转</td><td class="n">18</td><td class="n">17</td><td><span class="tag live">还差 1</span></td></tr>
|
||||
</tbody></table></div>
|
||||
<p class="sub">路线家族分布:weave ×4 · switchback ×1 · petals ×4 · orbits ×4 · crossing ×2 · spiral ×2 终局可走朝向分布:{"2":3,"3":13,"4":1}</p>
|
||||
|
||||
<h2><span class="num">B</span>什么叫「难度合适且好玩」</h2>
|
||||
<div class="callout"><p class="sub">评分里只有 difficulty(复测 r=0.93)和 stuck(r=0.85)可信;rotation / clarity / fun 的复测 r 只有 0.32 / 0.19 / 0.56,是模型噪声,不能拿来调参。所以「好玩」必须换成机械代理。</p></div>
|
||||
<ul class="rules sub">
|
||||
<li><b>难度</b>:agent 的 difficulty 评分沿关号上升,末档逼近 3.0(3 = 刚刚好),全程不超过 3.2<span class="how">(grok 抽样评分)</span></li>
|
||||
<li><b>不挫败</b>:stuck ≤ 2.0,盲转 = 0,转错方向 = 0<span class="how">(规则机器人全量扫,12 秒)</span></li>
|
||||
<li><b>不无脑</b>:猴子玩家 动作÷棋子 ≥ 3.0(v0.5 全库 1.67 = 乱点就能过)<span class="how">(生成时算)</span></li>
|
||||
<li><b>旋转是主线</b>:0 旋转关 ≤ 5 关(10%),且随关号推进旋转数递增<span class="how">(求解器)</span></li>
|
||||
<li><b>不重复</b>:50 关盘面签名互不相同,路线家族分布不偏科<span class="how">(deepLevelKey 去重)</span></li>
|
||||
</ul>
|
||||
<div class="scroll"><table>
|
||||
<thead><tr><th>关号区间</th><th>旋转数</th><th>目标 difficulty 评分</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>L1–L12</td><td>0–1 转</td><td class="mono">1.5 – 2.0</td></tr>
|
||||
<tr><td>L13–L32</td><td>1–2 转</td><td class="mono">2.0 – 2.6</td></tr>
|
||||
<tr><td>L33–L50</td><td>2–3 转</td><td class="mono">2.6 – 3.0</td></tr>
|
||||
</tbody></table></div>
|
||||
|
||||
<h2><span class="num">C</span>任务</h2>
|
||||
|
||||
<div class="phase">
|
||||
<h3><span class="pid">P0</span>判据:先说清楚什么叫「好」</h3>
|
||||
<ul class="tasks">
|
||||
<li class="done"><div class="t"><span class="tid">T0.1</span><span class="tt">难度曲线与旋转配额定义</span><span class="tag good">完成</span></div><div class="note">50 关分三档,0 转关卡死 10% 上限(用户硬要求)</div></li>
|
||||
<li class="done"><div class="t"><span class="tid">T0.2</span><span class="tt">机械验收门槛</span><span class="tag good">完成</span></div><div class="note">盲转=0、转错=0、终局可走朝向≥2 —— 前两项已由改锁结构达成</div></li>
|
||||
<li class="done"><div class="t"><span class="tid">T0.3</span><span class="tt">「好玩」的机械代理指标</span><span class="tag good">完成</span></div><div class="note">猴子 APT ≥3.0。fun 评分复测 r=0.56 不可用,改用「乱点过不了」当代理</div></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="phase">
|
||||
<h3><span class="pid">P1</span>产能:先得有足够多的候选</h3>
|
||||
<ul class="tasks">
|
||||
<li class="todo"><div class="t"><span class="tid">T1.1</span><span class="tt">深锁关(3 转)扩产到 ≥60 个候选</span><span class="tag idle">待办</span></div><div class="note">当前 14 种子出 17 关,需扩大种子与半径扫描</div></li>
|
||||
<li class="todo"><div class="t"><span class="tid">T1.2</span><span class="tt">中档(2 转)生成路径</span><span class="tag idle">待办</span></div><div class="note">deep-lock 固定 3 转,v0.5 最多 2 转 —— 中档要么放宽锁链,要么从 v0.5 筛</div></li>
|
||||
<li class="todo"><div class="t"><span class="tid">T1.3</span><span class="tt">简单档(0–1 转)候选池</span><span class="tag idle">待办</span></div><div class="note">从 v0.5 筛,但必须过猴子 APT 门槛(v0.5 全库中位 1.67,大部分会被刷掉)</div></li>
|
||||
<li class="todo"><div class="t"><span class="tid">T1.4</span><span class="tt">去重与家族均衡</span><span class="tag idle">待办</span></div><div class="note">6 个路线家族不能偏科;v0.5 曾出现 1000 关只有 30 个模板</div></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="phase">
|
||||
<h3><span class="pid">P2</span>标定:把机械指标映射到真实难度</h3>
|
||||
<ul class="tasks">
|
||||
<li class="todo"><div class="t"><span class="tid">T2.1</span><span class="tt">grok 抽样跑候选池,收集 difficulty / stuck</span><span class="tag idle">待办</span></div><div class="note">分层抽样,覆盖各旋转档与各家族</div></li>
|
||||
<li class="todo"><div class="t"><span class="tid">T2.2</span><span class="tt">拟合 difficulty ~ 机械特征</span><span class="tag idle">待办</span></div><div class="note">候选特征:旋转数、锁住率、猴子 APT、棋子数、路线长度、终局路口数</div></li>
|
||||
<li class="todo"><div class="t"><span class="tid">T2.3</span><span class="tt">用模型给全部候选打分并按曲线选 50 关</span><span class="tag idle">待办</span></div></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="phase">
|
||||
<h3><span class="pid">P3</span>迭代:跑几轮把曲线调平</h3>
|
||||
<ul class="tasks">
|
||||
<li class="todo"><div class="t"><span class="tid">T3.1</span><span class="tt">第 1 轮:grok 全量跑 50 关</span><span class="tag idle">待办</span></div></li>
|
||||
<li class="todo"><div class="t"><span class="tid">T3.2</span><span class="tt">按实测曲线调整选关,重选</span><span class="tag idle">待办</span></div></li>
|
||||
<li class="todo"><div class="t"><span class="tid">T3.3</span><span class="tt">第 2 轮验证</span><span class="tag idle">待办</span></div></li>
|
||||
<li class="todo"><div class="t"><span class="tid">T3.4</span><span class="tt">收敛判定(难度曲线单调、stuck 达标)</span><span class="tag idle">待办</span></div></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="phase">
|
||||
<h3><span class="pid">P4</span>交付</h3>
|
||||
<ul class="tasks">
|
||||
<li class="todo"><div class="t"><span class="tid">T4.1</span><span class="tt">50 关关卡集</span><span class="tag idle">待办</span></div></li>
|
||||
<li class="todo"><div class="t"><span class="tid">T4.2</span><span class="tt">人工审核页(可直接试玩每一关)</span><span class="tag idle">待办</span></div></li>
|
||||
<li class="todo"><div class="t"><span class="tid">T4.3</span><span class="tt">后续关卡制作流程的优化总结</span><span class="tag idle">待办</span></div></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<h2><span class="num">E</span>已有的 agent 实测</h2>
|
||||
<div class="scroll"><table>
|
||||
<thead><tr><th>批次</th><th class="n">关数</th><th class="n">difficulty</th><th class="n">stuck</th><th class="n">盲转/关</th><th class="n">多转/关</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Codex CLI · gpt-6-astra low</td><td class="n">18</td><td class="n">2.44</td><td class="n">1.44</td><td class="n">1.17</td><td class="n">1.22</td></tr>
|
||||
<tr><td>Codex CLI · spark xhigh</td><td class="n">18</td><td class="n">3.11</td><td class="n">2.28</td><td class="n">1.17</td><td class="n">1.17</td></tr>
|
||||
<tr><td>Grok Build CLI · xhigh</td><td class="n">18</td><td class="n">2.39</td><td class="n">1.83</td><td class="n">1.17</td><td class="n">1.22</td></tr>
|
||||
<tr><td>pi · Grok 4.6 xhigh</td><td class="n">18</td><td class="n">2.39</td><td class="n">1.89</td><td class="n">1.17</td><td class="n">1.22</td></tr>
|
||||
<tr><td>pi · Grok 4.6 xhigh · 复测</td><td class="n">18</td><td class="n">2.44</td><td class="n">1.78</td><td class="n">1.39</td><td class="n">1.56</td></tr>
|
||||
<tr><td>pi · Grok 4.6 xhigh · 新门槛</td><td class="n">18</td><td class="n">2.11</td><td class="n">1.39</td><td class="n">0.28</td><td class="n">0.33</td></tr>
|
||||
<tr><td>Kimi CLI · k3 high</td><td class="n">18</td><td class="n">2.94</td><td class="n">1.83</td><td class="n">1.17</td><td class="n">1.22</td></tr>
|
||||
<tr><td>Codex · high reasoning</td><td class="n">10</td><td class="n">3.00</td><td class="n">1.90</td><td class="n">0.40</td><td class="n">0.50</td></tr>
|
||||
<tr><td>Codex CLI · xhigh</td><td class="n">6</td><td class="n">3.00</td><td class="n">2.17</td><td class="n">0.33</td><td class="n">0.33</td></tr>
|
||||
</tbody></table></div>
|
||||
<p class="sub">difficulty 3 = 刚刚好;stuck 越低越好。这两项是评分里唯二复测可信的(r = 0.93 / 0.85)。</p>
|
||||
|
||||
<h2><span class="num">F</span>日志</h2>
|
||||
<ul class="log">
|
||||
<li><b>起点</b>已完成前置工作:擦碰漏报修复(113/581 例)、终局单出口门槛、锁结构改为双向可开 —— 规则机器人盲转/转错/多转已全部归零</li>
|
||||
</ul>
|
||||
|
||||
<footer>
|
||||
<p class="mono">看板状态:bench/board.json · 生成:node scripts/build-board.mjs · 页面数字(候选数、盲转、猴子 APT、agent 实测)由脚本从关卡集与 bench/runs 直接测得,不是手写</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+4411
-7733
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,255 @@
|
||||
#!/usr/bin/env node
|
||||
// Build docs/level-board.html — the task board for "50 good levels".
|
||||
//
|
||||
// node scripts/build-board.mjs
|
||||
//
|
||||
// Task state lives in bench/board.json and is edited by hand as work moves.
|
||||
// Everything else on the page is measured live from the level set and the run
|
||||
// archive, so the board cannot claim progress the artifacts do not show.
|
||||
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { solveRotations } from '../src/core.js';
|
||||
import { metrics } from '../src/bench.js';
|
||||
import { deadTurnAudit } from '../src/deep-lock.js';
|
||||
|
||||
const board = JSON.parse(readFileSync(resolve('bench/board.json'), 'utf8'));
|
||||
const OUT = resolve('docs/level-board.html');
|
||||
const esc = s => String(s ?? '').replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
||||
const f1 = x => x == null || Number.isNaN(x) ? '—' : (Math.round(x * 10) / 10).toFixed(1);
|
||||
const f2 = x => x == null || Number.isNaN(x) ? '—' : (Math.round(x * 100) / 100).toFixed(2);
|
||||
const mean = a => a.length ? a.reduce((x, y) => x + y, 0) / a.length : null;
|
||||
|
||||
// ---------- live measurement ----------
|
||||
const SET = resolve('levels/prototypes/d5.json');
|
||||
let pool = { n: 0, rot: {}, apt: null, families: {}, blind: null, wrong: null, endgame: {} };
|
||||
if (existsSync(SET)) {
|
||||
const raw = JSON.parse(readFileSync(SET, 'utf8'));
|
||||
const levels = Array.isArray(raw) ? raw : raw.levels;
|
||||
pool.n = levels.length;
|
||||
for (const l of levels) {
|
||||
const r = l.prototype?.stats?.rotations ?? solveRotations(l).rotations;
|
||||
pool.rot[r] = (pool.rot[r] || 0) + 1;
|
||||
const fam = l.id.replace('proto-d5-', '').replace(/-r\d+-s\d+$/, '');
|
||||
pool.families[fam] = (pool.families[fam] || 0) + 1;
|
||||
const a = l.prototype?.audit ?? deadTurnAudit(l);
|
||||
pool.endgame[a.endgameMinOpen] = (pool.endgame[a.endgameMinOpen] || 0) + 1;
|
||||
}
|
||||
pool.apt = mean(levels.map(l => l.prototype?.monkeyActionsPerPiece).filter(x => typeof x === 'number'));
|
||||
}
|
||||
|
||||
// Latest rule-bot sweep over whatever the current set is.
|
||||
const RUNS = resolve('bench/runs');
|
||||
const runsBy = tagTest => {
|
||||
if (!existsSync(RUNS)) return [];
|
||||
const out = [];
|
||||
for (const f of readdirSync(RUNS)) {
|
||||
if (!f.endsWith('.json') || f === 'index.json') continue;
|
||||
try { const d = JSON.parse(readFileSync(resolve(RUNS, f), 'utf8')); if (tagTest(d, f)) out.push(metrics(d)); } catch { /* torn read */ }
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const bot = runsBy((d, f) => f.startsWith('restruct-'));
|
||||
if (bot.length) {
|
||||
pool.blind = mean(bot.map(m => m.blindRotations));
|
||||
pool.wrong = mean(bot.map(m => m.wrongWayRotations));
|
||||
}
|
||||
// Agent batches, newest label wins.
|
||||
const agentBatches = {};
|
||||
for (const f of (existsSync(RUNS) ? readdirSync(RUNS) : [])) {
|
||||
if (!f.endsWith('.json') || f === 'index.json') continue;
|
||||
try {
|
||||
const d = JSON.parse(readFileSync(resolve(RUNS, f), 'utf8'));
|
||||
if (!d.agent || !d.rating) continue;
|
||||
(agentBatches[d.agent] ||= []).push(metrics(d));
|
||||
} catch { /* torn read */ }
|
||||
}
|
||||
|
||||
const TASKS = board.phases.flatMap(p => p.tasks);
|
||||
const counts = TASKS.reduce((m, t) => (m[t.status] = (m[t.status] || 0) + 1, m), {});
|
||||
const doneN = counts.done || 0;
|
||||
const pct = Math.round(doneN / TASKS.length * 100);
|
||||
const STATUS = {
|
||||
done: ['完成', 'good'], doing: ['进行中', 'live'], todo: ['待办', 'idle'], blocked: ['受阻', 'bad'],
|
||||
};
|
||||
|
||||
const quota = board.targets.rotationQuota;
|
||||
const quotaRows = Object.entries(quota).map(([r, want]) => ({ r, want, have: pool.rot[r] || 0 }));
|
||||
|
||||
const html = `<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta http-equiv="refresh" content="60">
|
||||
<title>50 关任务看板</title>
|
||||
<style>
|
||||
:root{
|
||||
--ground:#EEF1F4; --surface:#FFFFFF; --inset:#E3E8ED; --panel:#F7F9FA;
|
||||
--ink:#141E28; --ink2:#48596A; --ink3:#6B7A88;
|
||||
--rule:#D3DAE1; --rule2:#BFC9D2;
|
||||
--rail:#435F78; --rail-soft:#8FA6BA;
|
||||
--bad:#B23A26; --bad-soft:#F2DDD8;
|
||||
--warn:#8A5C0F; --warn-soft:#F4E9D4;
|
||||
--good:#236E64; --good-soft:#D9EDEA;
|
||||
--live:#1F6FB2; --live-soft:#DCE9F4;
|
||||
--shadow:0 1px 2px rgba(20,30,40,.06),0 6px 18px rgba(20,30,40,.05);
|
||||
color-scheme:light;
|
||||
}
|
||||
@media (prefers-color-scheme:dark){:root:not([data-theme=light]){
|
||||
--ground:#0F161C; --surface:#141D25; --inset:#0A1015; --panel:#141D25;
|
||||
--ink:#E4EAEF; --ink2:#AEBDC9; --ink3:#8494A2;
|
||||
--rule:#28343F; --rule2:#35434F;
|
||||
--rail:#8CB0CC; --rail-soft:#4E6577;
|
||||
--bad:#E4836C; --bad-soft:#3A211B;
|
||||
--warn:#E0AE5E; --warn-soft:#33280F;
|
||||
--good:#4FC7B7; --good-soft:#11302C;
|
||||
--live:#6BB3EE; --live-soft:#12283A;
|
||||
--shadow:0 1px 2px rgba(0,0,0,.3),0 6px 18px rgba(0,0,0,.28);
|
||||
color-scheme:dark;
|
||||
}}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--ground);color:var(--ink);
|
||||
font-family:"PingFang SC","Hiragino Sans GB","Microsoft YaHei",ui-sans-serif,system-ui,-apple-system,sans-serif;
|
||||
line-height:1.7;-webkit-font-smoothing:antialiased}
|
||||
.wrap{max-width:1080px;margin:0 auto;padding:0 24px 80px}
|
||||
code,.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-variant-numeric:tabular-nums}
|
||||
header.head{padding:48px 0 26px;border-bottom:1px solid var(--rule);margin-bottom:30px}
|
||||
.eyebrow{font-size:11.5px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink3);margin-bottom:12px}
|
||||
h1{font-size:clamp(26px,4vw,38px);line-height:1.2;margin:0 0 14px;letter-spacing:-.02em;text-wrap:balance}
|
||||
.meta{font-size:12.5px;color:var(--ink3)}
|
||||
.progress{margin:22px 0 0}
|
||||
.progress .bar{height:10px;background:var(--inset);border-radius:6px;overflow:hidden}
|
||||
.progress .bar i{display:block;height:100%;background:var(--good);border-radius:6px;transition:width .4s}
|
||||
.progress .lbl{display:flex;justify-content:space-between;font-size:12.5px;color:var(--ink2);margin-top:7px}
|
||||
h2{font-size:21px;margin:44px 0 6px;letter-spacing:-.01em}
|
||||
h2 .num{color:var(--rail-soft);font-family:ui-monospace,monospace;font-size:14px;margin-right:9px}
|
||||
p{margin:0 0 12px;max-width:74ch}
|
||||
.sub{color:var(--ink2);font-size:14px}
|
||||
.card{background:var(--surface);border:1px solid var(--rule);border-radius:12px;padding:18px 20px;box-shadow:var(--shadow);margin:16px 0}
|
||||
.callout{border-left:3px solid var(--rail);background:var(--panel);border-radius:0 10px 10px 0;padding:14px 18px;margin:16px 0}
|
||||
.callout p:last-child{margin-bottom:0}
|
||||
.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(176px,1fr));margin:16px 0}
|
||||
.tile{background:var(--surface);border:1px solid var(--rule);border-radius:12px;padding:14px 16px}
|
||||
.tile .k{font-size:11px;letter-spacing:.06em;color:var(--ink3);text-transform:uppercase}
|
||||
.tile .v{font-family:ui-monospace,monospace;font-size:25px;font-weight:600;line-height:1.25;margin-top:3px}
|
||||
.tile .n{font-size:12px;color:var(--ink2);margin-top:2px}
|
||||
.tile.ok .v{color:var(--good)} .tile.miss .v{color:var(--bad)} .tile.warn .v{color:var(--warn)}
|
||||
table{width:100%;border-collapse:collapse;font-size:13.5px;margin:14px 0}
|
||||
th,td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--rule)}
|
||||
th{font-size:11px;letter-spacing:.06em;color:var(--ink3);text-transform:uppercase;font-weight:600;white-space:nowrap}
|
||||
td.n,th.n{text-align:right;font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums;white-space:nowrap}
|
||||
.scroll{overflow-x:auto}
|
||||
.tag{display:inline-block;font-size:11px;padding:2px 9px;border-radius:20px;font-weight:600;white-space:nowrap}
|
||||
.tag.good{background:var(--good-soft);color:var(--good)}
|
||||
.tag.bad{background:var(--bad-soft);color:var(--bad)}
|
||||
.tag.live{background:var(--live-soft);color:var(--live)}
|
||||
.tag.idle{background:var(--inset);color:var(--ink3)}
|
||||
.phase{margin:24px 0}
|
||||
.phase h3{font-size:15.5px;margin:0 0 4px;display:flex;align-items:center;gap:10px}
|
||||
.phase h3 .pid{font-family:ui-monospace,monospace;font-size:11.5px;color:var(--surface);background:var(--rail);border-radius:5px;padding:2px 7px}
|
||||
.tasks{list-style:none;margin:10px 0 0;padding:0;border-left:2px solid var(--rule);margin-left:6px}
|
||||
.tasks li{position:relative;padding:9px 0 9px 22px}
|
||||
.tasks li::before{content:'';position:absolute;left:-6px;top:17px;width:10px;height:10px;border-radius:50%;
|
||||
background:var(--ground);border:2px solid var(--rule2)}
|
||||
.tasks li.done::before{background:var(--good);border-color:var(--good)}
|
||||
.tasks li.doing::before{background:var(--live);border-color:var(--live);box-shadow:0 0 0 4px var(--live-soft)}
|
||||
.tasks li.blocked::before{background:var(--bad);border-color:var(--bad)}
|
||||
.tasks .t{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
|
||||
.tasks .tid{font-family:ui-monospace,monospace;font-size:11.5px;color:var(--ink3)}
|
||||
.tasks .tt{font-size:14.5px;font-weight:500}
|
||||
.tasks li.done .tt{color:var(--ink2)}
|
||||
.tasks .note{font-size:12.5px;color:var(--ink3);margin-top:1px}
|
||||
.rules li{margin-bottom:8px}
|
||||
.rules b{font-weight:650}
|
||||
.rules .how{font-size:12px;color:var(--ink3);margin-left:6px}
|
||||
.log{list-style:none;padding:0;margin:12px 0}
|
||||
.log li{padding:8px 0 8px 16px;border-left:2px solid var(--rule);font-size:13.5px;color:var(--ink2)}
|
||||
.log li b{color:var(--ink);font-weight:600;font-family:ui-monospace,monospace;font-size:12px;margin-right:8px}
|
||||
footer{margin-top:56px;padding-top:20px;border-top:1px solid var(--rule);color:var(--ink3);font-size:12px}
|
||||
@media(max-width:640px){.wrap{padding:0 16px 60px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<header class="head">
|
||||
<div class="eyebrow">HEX TRAILS · 关卡调优</div>
|
||||
<h1>${esc(board.goal)}</h1>
|
||||
<p class="meta mono">第 ${board.round} 轮 · 更新于 ${esc(board.updated)} · 本页每 60 秒自动刷新</p>
|
||||
<div class="progress">
|
||||
<div class="bar"><i style="width:${pct}%"></i></div>
|
||||
<div class="lbl"><span>${doneN} / ${TASKS.length} 项完成</span><span class="mono">${pct}%</span></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<h2><span class="num">A</span>现在的状态</h2>
|
||||
<div class="grid">
|
||||
<div class="tile ${pool.n >= board.targets.levels ? 'ok' : ''}"><div class="k">候选关卡</div><div class="v">${pool.n} <span style="font-size:14px;color:var(--ink3)">/ ${board.targets.levels}</span></div><div class="n">当前关卡集</div></div>
|
||||
<div class="tile ${pool.blind === 0 ? 'ok' : pool.blind == null ? '' : 'miss'}"><div class="k">盲转 / 关</div><div class="v">${f2(pool.blind)}</div><div class="n">目标 0</div></div>
|
||||
<div class="tile ${pool.wrong === 0 ? 'ok' : pool.wrong == null ? '' : 'miss'}"><div class="k">转错方向 / 关</div><div class="v">${f2(pool.wrong)}</div><div class="n">目标 0</div></div>
|
||||
<div class="tile ${pool.apt >= 3 ? 'ok' : 'warn'}"><div class="k">猴子 动作÷棋子</div><div class="v">${f2(pool.apt)}</div><div class="n">目标 ≥3.0(不无脑)</div></div>
|
||||
</div>
|
||||
|
||||
<div class="scroll"><table>
|
||||
<thead><tr><th>旋转数</th><th class="n">目标关数</th><th class="n">现有</th><th>缺口</th></tr></thead>
|
||||
<tbody>
|
||||
${quotaRows.map(q => `<tr><td>${q.r} 转</td><td class="n">${q.want}</td><td class="n">${q.have}</td><td>${q.have >= q.want ? '<span class="tag good">已满足</span>' : `<span class="tag ${q.have ? 'live' : 'idle'}">还差 ${q.want - q.have}</span>`}</td></tr>`).join('\n')}
|
||||
</tbody></table></div>
|
||||
<p class="sub">路线家族分布:${Object.entries(pool.families).map(([k, v]) => `${esc(k)} ×${v}`).join(' · ') || '—'} 终局可走朝向分布:${JSON.stringify(pool.endgame)}</p>
|
||||
|
||||
<h2><span class="num">B</span>什么叫「难度合适且好玩」</h2>
|
||||
<div class="callout"><p class="sub">${esc(board.definition.why)}</p></div>
|
||||
<ul class="rules sub">
|
||||
${board.definition.rules.map(r => `<li><b>${esc(r.k)}</b>:${esc(r.v)}<span class="how">(${esc(r.how)})</span></li>`).join('\n')}
|
||||
</ul>
|
||||
<div class="scroll"><table>
|
||||
<thead><tr><th>关号区间</th><th>旋转数</th><th>目标 difficulty 评分</th></tr></thead>
|
||||
<tbody>
|
||||
${board.targets.difficultyCurve.map(b => `<tr><td>${esc(b.band)}</td><td>${esc(b.rot)}</td><td class="mono">${esc(b.difficulty)}</td></tr>`).join('\n')}
|
||||
</tbody></table></div>
|
||||
|
||||
<h2><span class="num">C</span>任务</h2>
|
||||
${board.phases.map(p => `
|
||||
<div class="phase">
|
||||
<h3><span class="pid">${esc(p.id)}</span>${esc(p.title)}</h3>
|
||||
<ul class="tasks">
|
||||
${p.tasks.map(t => `<li class="${esc(t.status)}"><div class="t"><span class="tid">${esc(t.id)}</span><span class="tt">${esc(t.title)}</span><span class="tag ${STATUS[t.status][1]}">${STATUS[t.status][0]}</span></div>${t.note ? `<div class="note">${esc(t.note)}</div>` : ''}</li>`).join('\n')}
|
||||
</ul>
|
||||
</div>`).join('\n')}
|
||||
|
||||
${board.rounds.length ? `
|
||||
<h2><span class="num">D</span>迭代记录</h2>
|
||||
<div class="scroll"><table>
|
||||
<thead><tr><th class="n">轮</th><th>改了什么</th><th>测出什么</th></tr></thead>
|
||||
<tbody>
|
||||
${board.rounds.map(r => `<tr><td class="n">${r.n}</td><td>${esc(r.change)}</td><td class="sub">${esc(r.result)}</td></tr>`).join('\n')}
|
||||
</tbody></table></div>` : ''}
|
||||
|
||||
${Object.keys(agentBatches).length ? `
|
||||
<h2><span class="num">E</span>已有的 agent 实测</h2>
|
||||
<div class="scroll"><table>
|
||||
<thead><tr><th>批次</th><th class="n">关数</th><th class="n">difficulty</th><th class="n">stuck</th><th class="n">盲转/关</th><th class="n">多转/关</th></tr></thead>
|
||||
<tbody>
|
||||
${Object.entries(agentBatches).sort((a, b) => b[1].length - a[1].length).map(([tag, ms]) => {
|
||||
const g = k => mean(ms.map(m => Number(m.rating?.[k])).filter(x => !Number.isNaN(x)));
|
||||
return `<tr><td>${esc(tag)}</td><td class="n">${ms.length}</td><td class="n">${f2(g('difficulty'))}</td><td class="n">${f2(g('stuck'))}</td><td class="n">${f2(mean(ms.map(m => m.blindRotations)))}</td><td class="n">${f2(mean(ms.map(m => m.rotations - m.minimumRotations)))}</td></tr>`;
|
||||
}).join('\n')}
|
||||
</tbody></table></div>
|
||||
<p class="sub">difficulty 3 = 刚刚好;stuck 越低越好。这两项是评分里唯二复测可信的(r = 0.93 / 0.85)。</p>` : ''}
|
||||
|
||||
<h2><span class="num">F</span>日志</h2>
|
||||
<ul class="log">
|
||||
${[...board.log].reverse().map(l => `<li><b>${esc(l.at)}</b>${esc(l.text)}</li>`).join('\n')}
|
||||
</ul>
|
||||
|
||||
<footer>
|
||||
<p class="mono">看板状态:bench/board.json · 生成:node scripts/build-board.mjs · 页面数字(候选数、盲转、猴子 APT、agent 实测)由脚本从关卡集与 bench/runs 直接测得,不是手写</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
writeFileSync(OUT, html);
|
||||
console.log(`看板已生成:${OUT}`);
|
||||
console.log(` ${doneN}/${TASKS.length} 项完成(${pct}%)· 候选 ${pool.n}/${board.targets.levels} 关`);
|
||||
+17
-5
@@ -18,12 +18,16 @@ function candidatePool(base, boards, maxBends) {
|
||||
const pool = [];
|
||||
for (const cell of base.cells) for (let dir = 0; dir < 6; dir++) {
|
||||
const tile = { q: cell.q, r: cell.r, dir };
|
||||
const routes = [0, 1, 2].map(turn => trace(boards[turn], tile, [], true));
|
||||
if (routes.some(route => route.status !== 'exit')) continue;
|
||||
const bends = routes.map(countBends);
|
||||
// All six orientations: the chain only constrains 0/1/2, but the endgame
|
||||
// is played at 3/4/5 too, and leaving those to chance is what produced
|
||||
// locks that open at exactly one of the six.
|
||||
const routes = boards.map(board => trace(board, tile, [], true));
|
||||
if (routes.slice(0, 3).some(route => route.status !== 'exit')) continue;
|
||||
const bends = routes.map(r => r.status === 'exit' ? countBends(r) : Infinity);
|
||||
if (bends[1] > maxBends) continue;
|
||||
pool.push({
|
||||
tile, position: positionKey(tile), bends, cells: routes.map(route => route.steps.length),
|
||||
exits: routes.map(route => route.status === 'exit'),
|
||||
at: routes.map(route => new Set(route.steps.slice(1).map(positionKey))),
|
||||
});
|
||||
}
|
||||
@@ -38,17 +42,22 @@ export function createDeepLock(base, {
|
||||
seed = 0, minHeroBends = 6, maxHeroBends = 70, maxChecks = 2000000,
|
||||
} = {}) {
|
||||
const rng = random(seed);
|
||||
const boards = [0, 1, 2].map(turn => rotateBoard(base, turn));
|
||||
const boards = [0, 1, 2, 3, 4, 5].map(turn => rotateBoard(base, turn));
|
||||
const pool = candidatePool(base, boards, maxHeroBends);
|
||||
const candidates = shuffled(pool, rng);
|
||||
const heroes = candidates.filter(c => c.bends[1] >= minHeroBends);
|
||||
let checks = 0, formalChecks = 0;
|
||||
for (const a of heroes) {
|
||||
// D holds A at 0 and at +2, and never stands on A's +1 departure.
|
||||
// A and D deadlock at 0 and at +2 and open at +1 — that is the chain. The
|
||||
// added requirement is that +3 opens too: a player stuck at +2 turns one
|
||||
// way or the other and something moves either way, instead of one
|
||||
// direction being right and the other five a dead end.
|
||||
const tails = candidates.filter(d => d.position !== a.position
|
||||
&& a.at[0].has(d.position) && d.at[0].has(a.position)
|
||||
&& a.at[2].has(d.position) && d.at[2].has(a.position)
|
||||
&& !a.at[1].has(d.position));
|
||||
&& !a.at[1].has(d.position)
|
||||
&& ((a.exits[3] && !a.at[3].has(d.position)) || (d.exits[3] && !d.at[3].has(a.position))));
|
||||
for (const d of tails) {
|
||||
// C holds A at +1 and leaves alone at +2 once B is gone.
|
||||
const seconds = candidates.filter(c => c.position !== a.position && c.position !== d.position
|
||||
@@ -73,6 +82,9 @@ export function createDeepLock(base, {
|
||||
if (afterSecond.some(tile => trace(boards[2], tile, afterSecond).status === 'exit')) continue;
|
||||
if (trace(boards[1], hero, afterSecond).status !== 'exit') continue;
|
||||
if (trace(boards[1], tail, [tail]).status !== 'exit') continue;
|
||||
// Certify the two-sided release with the real swept tracer, not the
|
||||
// cell-occupancy approximation the filter above uses.
|
||||
if (![hero, tail].some(t => trace(boards[3], t, afterSecond).status === 'exit')) continue;
|
||||
const level = { ...base, tiles, moveLimit: tiles.length };
|
||||
const solution = solveRotations(level);
|
||||
if (!solution.solvable || solution.exhausted || solution.rotations !== 3) continue;
|
||||
|
||||
@@ -4,12 +4,15 @@ import { Game, rotateBoard, solveRotations, trace, validateLevel } from '../src/
|
||||
import { createRouteBoard } from '../src/route-grammar.js';
|
||||
import { createDeepLock, growDeepLevel, deepLevelStats, deadTurnAudit } from '../src/deep-lock.js';
|
||||
|
||||
// Seeds known to yield a chain lock; the sweep script records the full yield table.
|
||||
// Seeds known to yield a chain lock; the sweep script records the full yield
|
||||
// table. These moved when the tail gained its two-sided release requirement —
|
||||
// a witness is whichever seed still satisfies the current construction, not a
|
||||
// fixed number, so re-derive them from the sweep after changing the lock.
|
||||
const WITNESSES = [
|
||||
{ family: 'orbits', radius: 3, seed: 0 },
|
||||
{ family: 'weave', radius: 4, seed: 0 },
|
||||
{ family: 'spiral', radius: 4, seed: 1 },
|
||||
{ family: 'switchback', radius: 4, seed: 0 },
|
||||
{ family: 'spiral', radius: 4, seed: 2 },
|
||||
{ family: 'switchback', radius: 4, seed: 6 },
|
||||
];
|
||||
|
||||
for (const { family, radius, seed } of WITNESSES) {
|
||||
|
||||
Reference in New Issue
Block a user