纠正仓库内容为衍生项目,标明 campaign-200-r2 最新版
This commit is contained in:
@@ -1,75 +0,0 @@
|
||||
# 产品结构
|
||||
|
||||
这是一个**多玩法**休闲 puzzle,不是单一机制的游戏。两个玩法共用一个壳,规则、盘面模型和关卡彼此独立。
|
||||
|
||||
工作名沿用目录 `number-swipe`。上架名未定。两个玩法各自有对外名称。
|
||||
|
||||
目录编号保留 `01` / `03`:曾有玩法二(连连气球),2026-09-16 放弃并移出仓库。编号同时是存档桶名和 Cocos 资源路径,不要重排。
|
||||
|
||||
## 目录
|
||||
|
||||
```
|
||||
number-swipe/
|
||||
docs/PRODUCT.md 本文件:壳、隔离规则、玩法清单
|
||||
docs/design.md UI 设计规范:令牌、组件、动效(玩法一为参考实现)
|
||||
docs/design.html 同上的设计师版:色板、样张、可回放动效
|
||||
modes/
|
||||
01-number-stroke/ 玩法一 · 数字一笔
|
||||
docs/GDD.md
|
||||
levels/
|
||||
03-hex-walls/ 玩法三 · 六边一笔
|
||||
docs/GDD.md
|
||||
levels/
|
||||
shared/ 壳:选玩法、设置、存档索引、广告位
|
||||
```
|
||||
|
||||
## 隔离规则
|
||||
|
||||
1. **玩法之间禁止互相引用。** `modes/*` 之间不得互相 import。
|
||||
2. **规则、求解器、关卡、对局视图属于玩法。** 盘面模型不默认共用。玩法一是矩形格,玩法三是六边形 + 边墙,即使都是“顺序一笔画”也各写一份。
|
||||
3. **`shared/` 只放壳。** 允许:玩法注册表、首页选玩法、设置、存档索引、广告位、安全区、通用按钮皮肤。禁止:过关判定、邻接、提示算法、关卡 JSON。
|
||||
4. **存档按玩法分桶。** `mode:01-number-stroke` / `mode:03-hex-walls`。
|
||||
5. **原型按玩法分开跑。** 各玩法的 Web 原型进自己的 `web/`。不要求绑在同一个 index.html。
|
||||
6. **Cocos 按玩法切场景。** 资源与脚本放 `assets/modes/<id>/`。
|
||||
|
||||
壳看到的入口:
|
||||
|
||||
```ts
|
||||
interface PuzzleMode {
|
||||
id: string;
|
||||
title: string;
|
||||
mount(root: HTMLElement, ctx: ShellContext): void;
|
||||
unmount(): void;
|
||||
}
|
||||
```
|
||||
|
||||
`ShellContext` 只提供存档读写、音效、振动、返回首页。
|
||||
|
||||
## 玩法清单
|
||||
|
||||
| id | 目录 | 类型 | 状态 | 规格 |
|
||||
|---|---|---|---|---|
|
||||
| `01-number-stroke` | `modes/01-number-stroke` | 关卡逻辑:矩形一笔画 + 序号 | M1/M2:Web 原型可玩,26 关(教学 10 + 进阶 10 + 奇偶 6) | [GDD](../modes/01-number-stroke/docs/GDD.md) |
|
||||
| `03-hex-walls` | `modes/03-hex-walls` | 关卡逻辑:六边形一笔画 + 边墙 | M1/M2:Web 原型可玩,10 关 + 转墙实验包 9 关 | [GDD](../modes/03-hex-walls/docs/GDD.md) |
|
||||
|
||||
一和三同属路径推理,但几何和障碍不同,**不是 DLC 关系**。
|
||||
|
||||
新玩法开工顺序:写该玩法 GDD → 锁规则 → 再写该玩法的 `src/`。
|
||||
|
||||
## 共享决策(全产品)
|
||||
|
||||
- 手机竖屏,单机,离线可玩
|
||||
- 默认无体力、无计时、无分数
|
||||
- 竖直切面无广告;上线后的去广告 / 激励视频由壳统一接
|
||||
- 先 Web 原型验证该玩法,再进 Cocos
|
||||
- 关卡、美术全部原创,不使用第三方题库和素材
|
||||
|
||||
## 里程碑(产品级)
|
||||
|
||||
| 阶段 | 内容 |
|
||||
|---|---|
|
||||
| 现在 | 两个玩法独立可运行,均为关卡原型(M1/M2),未进 Cocos |
|
||||
| 各玩法 M1 | 只在自己的 `modes/<id>` 里做可玩原型,互不阻塞 |
|
||||
| 壳 | 两个玩法原型都已可玩,选玩法首页可以开工 |
|
||||
|
||||
任意一个玩法都可以单独推进,不必互相等待。
|
||||
@@ -1,543 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>数字一笔 · UI 设计规范 v1.5</title>
|
||||
<style>
|
||||
/* ====== 令牌:与 modes/01-number-stroke/web/style.css :root 一致 ====== */
|
||||
: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; --clue-ring-active: #dfe1ec; --overlay: rgba(35, 36, 58, 0.92);
|
||||
--shadow-primary: 0 6px 18px rgba(101, 88, 245, 0.28); --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;
|
||||
--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);
|
||||
--u: 64px; /* 样张里的单位格 */
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; background: var(--bg-app); color: var(--ink); font-family: var(--font); font-size: 15px; line-height: 1.5; font-variant-numeric: tabular-nums; -webkit-font-smoothing: antialiased; }
|
||||
a { color: var(--primary); text-decoration: none; }
|
||||
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; background: var(--surface-2); padding: 1px 5px; border-radius: 5px; }
|
||||
/* ====== 文档骨架 ====== */
|
||||
.doc { display: grid; grid-template-columns: 220px 1fr; min-height: 100vh; }
|
||||
nav { position: sticky; top: 0; height: 100vh; padding: 28px 20px; border-right: 1px solid var(--line); background: var(--bg); overflow: auto; }
|
||||
nav h1 { font-size: 18px; font-weight: 800; margin: 0 0 4px; }
|
||||
nav .ver { color: var(--ink-3); font-size: 12px; font-weight: 600; margin-bottom: 20px; }
|
||||
nav a { display: block; padding: 6px 8px; border-radius: 8px; color: var(--ink-2); font-size: 14px; font-weight: 600; }
|
||||
nav a:hover { background: var(--surface-2); color: var(--ink); }
|
||||
main { padding: 36px 48px 96px; max-width: 1180px; }
|
||||
section { margin-bottom: 64px; }
|
||||
h2 { font-size: 26px; font-weight: 800; margin: 0 0 6px; letter-spacing: 0.01em; }
|
||||
h3 { font-size: 17px; font-weight: 700; margin: 28px 0 10px; }
|
||||
.lead { color: var(--ink-2); margin: 0 0 20px; max-width: 720px; }
|
||||
.card { background: var(--bg); border: 1px solid var(--line); border-radius: 16px; padding: 20px; }
|
||||
.grid { display: grid; gap: 14px; }
|
||||
.g2 { grid-template-columns: repeat(2, 1fr); } .g3 { grid-template-columns: repeat(3, 1fr); } .g4 { grid-template-columns: repeat(4, 1fr); }
|
||||
table { width: 100%; border-collapse: collapse; background: var(--bg); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; font-size: 13.5px; }
|
||||
th, td { text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--line); vertical-align: middle; }
|
||||
th { background: var(--surface-2); color: var(--ink-2); font-weight: 700; font-size: 12.5px; }
|
||||
tr:last-child td { border-bottom: 0; }
|
||||
.note { font-size: 13px; color: var(--ink-2); }
|
||||
.tag { display: inline-block; padding: 2px 8px; border-radius: 999px; background: var(--surface-2); color: var(--ink-2); font-size: 12px; font-weight: 700; }
|
||||
.sync { background: var(--primary-soft); color: var(--primary); border-radius: 12px; padding: 12px 16px; font-size: 13.5px; font-weight: 600; margin-bottom: 28px; }
|
||||
/* ====== 色板 ====== */
|
||||
.swatches { display: grid; grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); gap: 12px; }
|
||||
.sw { background: var(--bg); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
|
||||
.sw .chip { height: 64px; border-bottom: 1px solid var(--line); }
|
||||
.sw .meta { padding: 10px 12px; }
|
||||
.sw b { display: block; font-size: 13.5px; }
|
||||
.sw code { display: inline-block; margin-top: 3px; }
|
||||
.sw small { display: block; color: var(--ink-2); font-size: 12px; margin-top: 4px; }
|
||||
/* ====== 字阶 ====== */
|
||||
.type-row { display: grid; grid-template-columns: 150px 1fr 220px; align-items: baseline; gap: 16px; padding: 12px 0; border-bottom: 1px solid var(--line); }
|
||||
.type-row:last-child { border-bottom: 0; }
|
||||
.type-row .lab { color: var(--ink-3); font-size: 12px; font-weight: 700; }
|
||||
.type-row .use { color: var(--ink-2); font-size: 13px; }
|
||||
/* ====== 盘面样张 ====== */
|
||||
.board { position: relative; display: inline-grid; grid-auto-rows: var(--u); grid-auto-columns: var(--u); background: var(--bg); padding: 14px; border-radius: 16px; border: 1px solid var(--line); }
|
||||
.bc { position: relative; width: var(--u); height: var(--u); padding: calc(var(--u) * 0.05); }
|
||||
.bc > i { position: absolute; inset: calc(var(--u) * 0.05); border-radius: calc(var(--u) * 0.18); background: var(--cell); display: flex; align-items: center; justify-content: center; font-style: normal; font-weight: 800; font-size: calc(var(--u) * 0.52); color: var(--ink); }
|
||||
.bc.two > i { font-size: calc(var(--u) * 0.46); }
|
||||
.bc.clue > i { box-shadow: inset 0 0 0 calc(var(--u) * 0.05) var(--clue-ring); }
|
||||
.bc.start > i, .bc.visited > i { background: var(--primary); color: var(--on-primary); }
|
||||
.bc.start > i { font-size: calc(var(--u) * 0.6); }
|
||||
.bc.start > i, .bc.visited.clue > i { box-shadow: inset 0 0 0 calc(var(--u) * 0.05) var(--clue-ring-active); }
|
||||
.bc.bright > i { background: var(--primary-bright); }
|
||||
.bc.conflict > i { background: var(--danger); color: var(--on-primary); }
|
||||
.bc.parity > i { font-size: calc(var(--u) * 0.44); font-weight: 700; }
|
||||
.bc.visited.parity > i { flex-direction: column; font-size: calc(var(--u) * 0.52); font-weight: 800; line-height: 1; }
|
||||
.bc.visited.parity > i small { font-size: calc(var(--u) * 0.19); font-weight: 700; margin-top: calc(var(--u) * 0.04); }
|
||||
.bc .badge { position: absolute; right: calc(var(--u) * 0.08); top: calc(var(--u) * 0.08); width: calc(var(--u) * 0.3); height: calc(var(--u) * 0.3); border-radius: 50%; background: var(--ink); color: #fff; font-size: calc(var(--u) * 0.19); font-weight: 800; display: flex; align-items: center; justify-content: center; }
|
||||
.bc.hint > i { box-shadow: 0 0 0 calc(var(--u) * 0.08) var(--hint); }
|
||||
.bc.dim > i::after { content: ""; position: absolute; inset: 0; border-radius: inherit; background: rgba(255,255,255,.45); }
|
||||
.bc.big { width: calc(var(--u) * 2); height: calc(var(--u) * 2); grid-column: span 2; grid-row: span 2; }
|
||||
.bridge { position: absolute; background: var(--primary); z-index: 1; }
|
||||
.bridge.h { width: calc(var(--u) * 0.1 + 2px); height: calc(var(--u) * 0.46); }
|
||||
.bridge.v { height: calc(var(--u) * 0.1 + 2px); width: calc(var(--u) * 0.46); }
|
||||
.bridge.red { background: var(--danger); }
|
||||
.legend { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 14px; }
|
||||
.legend .bc { --u: 72px; }
|
||||
.legend figure { margin: 0; text-align: center; }
|
||||
.legend figcaption { font-size: 12.5px; color: var(--ink-2); margin-top: 6px; font-weight: 600; }
|
||||
/* ====== 组件(与 style.css 同规格) ====== */
|
||||
.topbar { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; height: 52px; }
|
||||
.icon-btn { justify-self: start; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; color: var(--ink-2); border-radius: 999px; margin-left: -8px; background: none; border: 0; }
|
||||
.level-info { text-align: center; line-height: 1.1; }
|
||||
.level-title { font-size: var(--fs-title); font-weight: 800; letter-spacing: .02em; }
|
||||
.level-sub { margin-top: 3px; color: var(--ink-3); font-size: var(--fs-micro); font-weight: 600; letter-spacing: .04em; white-space: nowrap; }
|
||||
.remaining { justify-self: end; display: inline-flex; align-items: center; gap: 5px; height: 32px; padding: 0 12px 0 10px; border-radius: 999px; background: var(--surface-2); color: var(--ink-2); font-size: 14px; font-weight: 700; min-width: 52px; justify-content: center; }
|
||||
.remaining svg { color: var(--ink-3); }
|
||||
.remaining.done { background: var(--primary-soft); color: var(--primary); } .remaining.done svg { color: var(--primary); }
|
||||
.remaining.bump { animation: bump var(--t-base) var(--ease-spring); }
|
||||
.level-progress { display: flex; gap: 3px; height: 4px; margin: 8px 4px 0; }
|
||||
.level-progress i { flex: 1; border-radius: 2px; background: var(--cell); }
|
||||
.level-progress i.cleared { background: var(--primary); opacity: .4; } .level-progress i.current { background: var(--primary); }
|
||||
.caption { height: 36px; margin-top: 8px; display: flex; align-items: center; justify-content: center; color: var(--ink-2); font-size: 15px; font-weight: 500; }
|
||||
.gesture-help { margin: 0; padding-top: 8px; color: var(--ink-2); font-size: 13px; text-align: center; }
|
||||
.bottombar { display: flex; justify-content: center; gap: 12px; padding-top: 12px; }
|
||||
.action { flex: 1; max-width: 170px; height: 56px; border-radius: 18px; background: var(--surface-2); color: var(--ink); font: inherit; font-size: 15px; font-weight: 700; display: flex; align-items: center; justify-content: center; gap: 4px; border: 0; cursor: pointer; transition: transform var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out); }
|
||||
.action svg { color: var(--ink-2); } .action:active { transform: scale(.97); background: var(--cell); }
|
||||
.action.pulse { animation: pulse .9s var(--ease-in-out) infinite; } .action:disabled { opacity: .4; }
|
||||
.toast { display: inline-block; padding: 10px 16px; border-radius: 14px; background: var(--overlay); color: #fff; font-size: 14px; font-weight: 600; max-width: 100%; text-align: center; }
|
||||
.toast.anim { animation: toast-in var(--t-base) var(--ease-out) both; }
|
||||
.primary-btn { height: 54px; border-radius: 18px; background: var(--primary); color: #fff; font: inherit; font-size: 17px; font-weight: 700; letter-spacing: .02em; box-shadow: var(--shadow-primary); border: 0; width: 100%; cursor: pointer; transition: transform var(--t-fast) var(--ease-out), box-shadow var(--t-fast) var(--ease-out); }
|
||||
.primary-btn:active { transform: scale(.98); box-shadow: 0 3px 10px rgba(101,88,245,.22); }
|
||||
.chip-btn { height: 34px; padding: 0 16px; border-radius: 999px; background: var(--surface-2); color: var(--ink-2); font: inherit; font-size: 13px; font-weight: 600; border: 0; cursor: pointer; }
|
||||
.chip-btn.on { background: var(--primary-soft); color: var(--primary); } .chip-btn.danger { color: var(--danger); }
|
||||
.level-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; }
|
||||
.tile { position: relative; aspect-ratio: 1; border-radius: 14px; background: var(--cell); color: var(--ink); font: inherit; font-size: 20px; font-weight: 700; display: flex; align-items: center; justify-content: center; border: 0; cursor: pointer; transition: transform var(--t-fast) var(--ease-out); }
|
||||
.tile:active { transform: scale(.94); }
|
||||
.tile.cleared { background: var(--primary); color: #fff; }
|
||||
.tile.cleared::after { content: ""; position: absolute; right: 8px; top: 6px; width: 4px; height: 8px; border: solid rgba(255,255,255,.9); border-width: 0 2px 2px 0; transform: rotate(45deg); }
|
||||
.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; }
|
||||
.tile.locked { background: var(--surface-2); color: var(--ink-3); } .tile.locked::before { content: ""; width: 10px; height: 12px; border: 2px solid var(--ink-3); border-radius: 3px; border-top-width: 3px; } .tile.locked span { display: none; }
|
||||
.tile.shake { animation: shake var(--t-slow) var(--ease-out); }
|
||||
.overall { display: flex; align-items: center; gap: 12px; margin-top: 16px; }
|
||||
.overall-bar { flex: 1; height: 6px; border-radius: 999px; background: var(--cell); overflow: hidden; } .overall-bar i { display: block; height: 100%; border-radius: 999px; background: var(--primary); width: 62%; }
|
||||
.overall-text { color: var(--ink-3); font-size: 13px; font-weight: 600; }
|
||||
/* ====== 手机样机 ====== */
|
||||
.phones { display: flex; gap: 32px; flex-wrap: wrap; }
|
||||
.phone { width: 390px; height: 760px; background: var(--bg); border-radius: 28px; box-shadow: var(--shadow-frame); padding: 12px 20px 16px; display: flex; flex-direction: column; overflow: hidden; }
|
||||
.phone h1 { margin: 0; font-size: 34px; font-weight: 800; letter-spacing: .02em; line-height: 1.15; }
|
||||
.phone .sub { margin: 8px 0 0; color: var(--ink-2); }
|
||||
.pack { margin-top: 28px; } .pack-title { display: flex; justify-content: space-between; align-items: baseline; padding: 0 4px 12px; font-size: 17px; font-weight: 700; } .pack-progress { color: var(--ink-3); font-size: 14px; font-weight: 600; } .pack.done .pack-progress { color: var(--primary); }
|
||||
.phone .list { flex: 1; overflow: hidden; margin-top: 12px; -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); }
|
||||
.phone .footer { display: flex; justify-content: center; gap: 8px; padding-top: 16px; }
|
||||
.phone .board-area { flex: 1; display: flex; align-items: center; justify-content: center; padding-bottom: 6%; }
|
||||
.phone .board { --u: 78px; border: 0; padding: 0; }
|
||||
/* ====== 动效样张 ====== */
|
||||
.demo { background: var(--bg); border: 1px solid var(--line); border-radius: 16px; padding: 18px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.demo header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.demo header b { font-size: 14px; }
|
||||
.demo header span { color: var(--ink-3); font-size: 12px; font-weight: 700; }
|
||||
.demo .stage { height: 120px; display: flex; align-items: center; justify-content: center; gap: 8px; background: var(--surface-2); border-radius: 12px; position: relative; overflow: hidden; }
|
||||
.demo .stage .bc { --u: 64px; }
|
||||
.demo p { margin: 0; font-size: 12.5px; color: var(--ink-2); }
|
||||
.replay { height: 30px; padding: 0 12px; border-radius: 999px; background: var(--primary-soft); color: var(--primary); font: inherit; font-size: 12.5px; font-weight: 700; border: 0; cursor: pointer; }
|
||||
.a-pop > i { animation: pop 220ms var(--ease-out) both; }
|
||||
.a-fade > i span { animation: fade var(--t-fast) var(--ease-out) both; }
|
||||
.a-flash > i::before { content: ""; position: absolute; inset: 0; border-radius: inherit; box-shadow: inset 0 0 0 calc(var(--u) * 0.05) #fff; animation: flash var(--t-slow) linear both; }
|
||||
.a-shake > i { animation: cell-shake 280ms linear both; }
|
||||
.a-bridge .bridge { transform-origin: center; animation: bridge-grow var(--t-fast) var(--ease-out) both; }
|
||||
.a-sweep > i::before { content: ""; position: absolute; inset: 0; border-radius: inherit; background: #fff; opacity: 0; animation: sweep 250ms linear both; animation-delay: calc(var(--i) * 64ms); }
|
||||
.a-enter > i { animation: enter var(--t-base) var(--ease-out) both; animation-delay: calc(var(--i) * 14ms); }
|
||||
.a-screen { animation: screen-in var(--t-base) var(--ease-out) both; }
|
||||
.a-hint > i { animation: hint-pulse 2.4s linear both; }
|
||||
.timeline { display: grid; grid-template-columns: 320fr 500fr 160fr 400fr; gap: 3px; margin-top: 10px; }
|
||||
.timeline div { padding: 10px 10px; border-radius: 8px; font-size: 12.5px; font-weight: 700; color: var(--ink-2); background: var(--surface-2); }
|
||||
.timeline div small { display: block; font-weight: 600; color: var(--ink-3); }
|
||||
.timeline .p1 { background: var(--primary-soft); color: var(--primary); } .timeline .p3 { background: var(--cell); }
|
||||
@keyframes pop { 0% { transform: scale(1); } 35% { transform: scale(1.12); } 70% { transform: scale(.985); } 100% { transform: scale(1); } }
|
||||
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
|
||||
@keyframes flash { from { opacity: 1; } to { opacity: 0; } }
|
||||
@keyframes cell-shake { 0%,100% { transform: translateX(0); } 8% { transform: translateX(6%); } 25% { transform: translateX(-5%); } 42% { transform: translateX(4%); } 58% { transform: translateX(-3%); } 75% { transform: translateX(2%); } 92% { transform: translateX(-1%); } }
|
||||
@keyframes bridge-grow { from { transform: scaleY(0); } to { transform: scaleY(1); } }
|
||||
@keyframes sweep { 0% { opacity: 0; } 50% { opacity: .55; } 100% { opacity: 0; } }
|
||||
@keyframes enter { from { opacity: 0; transform: scale(.92); } to { opacity: 1; transform: scale(1); } }
|
||||
@keyframes screen-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@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,.16); } }
|
||||
@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,.35); } }
|
||||
@keyframes shake { 0%,100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 50% { transform: translateX(5px); } 75% { transform: translateX(-3px); } }
|
||||
@keyframes hint-pulse { 0% { box-shadow: 0 0 0 calc(var(--u) * 0.08) rgba(245,165,36,.55); } 15% { box-shadow: 0 0 0 calc(var(--u) * 0.08) rgba(245,165,36,1); } 30% { box-shadow: 0 0 0 calc(var(--u) * 0.08) rgba(245,165,36,.2); } 45% { box-shadow: 0 0 0 calc(var(--u) * 0.08) rgba(245,165,36,1); } 60% { box-shadow: 0 0 0 calc(var(--u) * 0.08) rgba(245,165,36,.2); } 75% { box-shadow: 0 0 0 calc(var(--u) * 0.08) rgba(245,165,36,1); } 100% { box-shadow: 0 0 0 calc(var(--u) * 0.08) rgba(245,165,36,0); } }
|
||||
@media (max-width: 900px) { .doc { grid-template-columns: 1fr; } nav { position: static; height: auto; border-right: 0; border-bottom: 1px solid var(--line); } main { padding: 24px 20px 64px; } .g2, .g3, .g4 { grid-template-columns: 1fr; } .type-row { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="doc">
|
||||
<nav>
|
||||
<h1>数字一笔 · UI 规范</h1>
|
||||
<div class="ver">v1.5 · 2026-09-16</div>
|
||||
<a href="#principles">1 设计原则</a>
|
||||
<a href="#color">2 色彩</a>
|
||||
<a href="#type">3 字体与字阶</a>
|
||||
<a href="#space">4 间距 · 圆角 · 尺寸</a>
|
||||
<a href="#board">5 盘面</a>
|
||||
<a href="#components">6 组件</a>
|
||||
<a href="#screens">7 页面</a>
|
||||
<a href="#motion">8 动效</a>
|
||||
<a href="#sound">9 声音与振动</a>
|
||||
<a href="#sync">10 同步规则</a>
|
||||
</nav>
|
||||
<main>
|
||||
<div class="sync">本页与 <code>docs/design.md</code> 同步维护。令牌真值在 <code>web/style.css :root</code> 与 <code>src/theme.ts</code>,由 <code>tests/theme.test.ts</code> 守门。改设计:先改 design.md → 改代码令牌 → 更新本页样张。</div>
|
||||
|
||||
<section id="principles">
|
||||
<h2>1 · 设计原则</h2>
|
||||
<p class="lead">逻辑 puzzle 的盘面只回答三个问题:我走到哪了、哪错了、过没过。</p>
|
||||
<div class="grid g3">
|
||||
<div class="card"><b>板要干净</b><p class="note">盘面无阴影、无纹理、无装饰。灰阶承担一切非路径信息。</p></div>
|
||||
<div class="card"><b>一个主色</b><p class="note">蓝紫只表示「已走 / 当前 / 可继续」。红只表示冲突,琥珀只表示提示。不每关换色。</p></div>
|
||||
<div class="card"><b>数字优先</b><p class="note">系统圆体、800 字重、等宽数字。其余文字都比数字轻。</p></div>
|
||||
<div class="card"><b>动效四档</b><p class="note">只用 120 / 200 / 320 / 500 ms 与四条缓动。新动效先归档再写。</p></div>
|
||||
<div class="card"><b>反馈贴手指</b><p class="note">落子、踩对、冲突、死路都发生在手指所在格子上。不弹窗、不撒纸片。</p></div>
|
||||
<div class="card"><b>信息随手可见</b><p class="note">关号、包名、本包进度、剩余格数常驻顶栏。</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="color">
|
||||
<h2>2 · 色彩</h2>
|
||||
<p class="lead">浅色单主题。DOM 用 CSS 变量,Canvas 用 <code>THEME</code>,同名同值。</p>
|
||||
<div class="swatches" id="swatches"></div>
|
||||
<h3>语义</h3>
|
||||
<table>
|
||||
<tr><th style="width:120px">颜色</th><th>只能用于</th><th>不能用于</th></tr>
|
||||
<tr><td><span class="tag" style="background:var(--primary);color:#fff">主色</span></td><td>已走格、桥、起点 1、当前关、主按钮、进度</td><td>装饰、背景、标题文字</td></tr>
|
||||
<tr><td><span class="tag" style="background:var(--danger);color:#fff">冲突</span></td><td>冲突格、「重置进度」</td><td>任何常驻元素、警示以外的强调</td></tr>
|
||||
<tr><td><span class="tag" style="background:var(--hint);color:#fff">提示</span></td><td>提示描边、提示按钮脉冲</td><td>任何常驻元素</td></tr>
|
||||
<tr><td><span class="tag" style="background:var(--primary-bright);color:#fff">主色·亮</span></td><td>过关后的全盘提亮(唯一允许的换色)</td><td>过关以外</td></tr>
|
||||
</table>
|
||||
<h3>对比度</h3>
|
||||
<table>
|
||||
<tr><th>组合</th><th>对比度</th><th>用途</th></tr>
|
||||
<tr><td>白 on 主色</td><td>5.2 : 1</td><td>已走格数字、主按钮</td></tr>
|
||||
<tr><td>墨 on 未走格</td><td>12.6 : 1</td><td>未走线索数字</td></tr>
|
||||
<tr><td>白 on 冲突红</td><td>4.5 : 1</td><td>冲突格数字(角标另有墨底白字)</td></tr>
|
||||
<tr><td>墨 2 on 白</td><td>5.1 : 1</td><td>副文案</td></tr>
|
||||
<tr><td>墨 on 未走格</td><td>12.6 : 1</td><td>未走线索描边(与数字同色)</td></tr>
|
||||
<tr><td>墨 3 on 白</td><td>2.5 : 1</td><td>仅非关键弱文案</td></tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section id="type">
|
||||
<h2>3 · 字体与字阶</h2>
|
||||
<p class="lead">字体栈:<code>ui-rounded, "SF Pro Rounded", "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", system-ui, -apple-system, "Segoe UI", sans-serif</code>。全局等宽数字。</p>
|
||||
<div class="card">
|
||||
<div class="type-row"><div class="lab">display · 34 / 800</div><div style="font-size:34px;font-weight:800;line-height:1.15">数字一笔 0123</div><div class="use">选关页标题</div></div>
|
||||
<div class="type-row"><div class="lab">title · 22 / 800</div><div style="font-size:22px;font-weight:800;letter-spacing:.02em">1-7 · 2-10 · 3-6</div><div class="use">顶栏关号</div></div>
|
||||
<div class="type-row"><div class="lab">heading · 17 / 700</div><div style="font-size:17px;font-weight:700">入门 · 进阶 · 奇与偶 · 继续 · 2-1</div><div class="use">包标题、主按钮</div></div>
|
||||
<div class="type-row"><div class="lab">body · 15 / 500</div><div style="font-size:15px;font-weight:500;color:var(--ink-2)">从 1 开始,滑到旁边的格</div><div class="use">副标题、字幕</div></div>
|
||||
<div class="type-row"><div class="lab">label · 14 / 700</div><div style="font-size:14px;font-weight:700;color:var(--ink-2)">6 · 完成 · 10 / 10 · 下一步在这里</div><div class="use">胶囊、包进度、toast</div></div>
|
||||
<div class="type-row"><div class="lab">caption · 13 / 600</div><div style="font-size:13px;font-weight:600;color:var(--ink-2)">音效 · 振动 · 16 / 26</div><div class="use">chip、总进度数字</div></div>
|
||||
<div class="type-row"><div class="lab">micro · 12 / 600</div><div style="font-size:12px;font-weight:600;color:var(--ink-3);letter-spacing:.04em">入门 · 第 7 关</div><div class="use">顶栏副标题</div></div>
|
||||
</div>
|
||||
<h3>盘面字阶(相对单位格 u)</h3>
|
||||
<div class="card" style="display:flex;gap:28px;align-items:flex-end;flex-wrap:wrap">
|
||||
<figure style="margin:0;text-align:center"><div class="bc start" style="--u:96px"><i>1</i></div><figcaption class="note">起点 0.60 u · 800</figcaption></figure>
|
||||
<figure style="margin:0;text-align:center"><div class="bc visited" style="--u:96px"><i>7</i></div><figcaption class="note">单位数 0.52 u · 800</figcaption></figure>
|
||||
<figure style="margin:0;text-align:center"><div class="bc visited two" style="--u:96px"><i>14</i></div><figcaption class="note">两位数 0.46 u · 800</figcaption></figure>
|
||||
<figure style="margin:0;text-align:center"><div class="bc clue parity" style="--u:96px"><i>奇</i></div><figcaption class="note">奇偶 0.44 u · 700</figcaption></figure>
|
||||
<figure style="margin:0;text-align:center"><div class="bc visited clue parity" style="--u:96px"><i>6<small>偶</small></i></div><figcaption class="note">小字 0.19 u · 700</figcaption></figure>
|
||||
<figure style="margin:0;text-align:center"><div class="bc visited conflict" style="--u:96px"><i>6</i><b class="badge">8</b></div><figcaption class="note">角标 r=0.15 u · 字 1.25 r</figcaption></figure>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="space">
|
||||
<h2>4 · 间距 · 圆角 · 尺寸</h2>
|
||||
<div class="grid g3">
|
||||
<div class="card"><b>间距 · 4pt 网格</b><p class="note">4 / 8 / 12 / 16 / 20 / 24 / 28。页面左右 20,顶 12 + 安全区,底 16 + 安全区。</p></div>
|
||||
<div class="card"><b>圆角</b><p class="note">sm 10 · md 14(关卡格、toast)· lg 18(主按钮、底栏按钮)· xl 24 · 胶囊 999 · 桌面框 28。盘面格 0.18 u。</p></div>
|
||||
<div class="card"><b>尺寸</b><p class="note">点击区 ≥ 44。顶栏 52,胶囊 32,底栏按钮 56,主按钮 54,chip 34,关卡格 5 列间距 12。</p></div>
|
||||
</div>
|
||||
<div class="card" style="margin-top:14px;display:flex;gap:20px;align-items:center;flex-wrap:wrap">
|
||||
<div style="width:10px;height:10px;border-radius:10px;background:var(--cell)"></div><span class="note">10</span>
|
||||
<div style="width:14px;height:14px;border-radius:14px;background:var(--cell)"></div><span class="note">14</span>
|
||||
<div style="width:18px;height:18px;border-radius:18px;background:var(--cell)"></div><span class="note">18</span>
|
||||
<div style="width:24px;height:24px;border-radius:24px;background:var(--cell)"></div><span class="note">24</span>
|
||||
<div style="width:60px;height:60px;border-radius:14px;background:var(--cell)"></div><span class="note">关卡格 14</span>
|
||||
<div style="width:120px;height:56px;border-radius:18px;background:var(--surface-2)"></div><span class="note">底栏按钮 18</span>
|
||||
<div style="width:64px;height:64px;border-radius:11.5px;background:var(--cell)"></div><span class="note">盘面格 0.18 u</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="board">
|
||||
<h2>5 · 盘面</h2>
|
||||
<p class="lead">格圆角 0.18 u、缝 0.10 u、桥厚 0.46 u。整盘垂直居中后上提 3%。四周死区:触屏 8%,鼠标 14%。点任意已走格直接回退,可接着滑动。</p>
|
||||
<h3>格的状态</h3>
|
||||
<div class="legend card">
|
||||
<figure><div class="bc"><i></i></div><figcaption>未走</figcaption></figure>
|
||||
<figure><div class="bc clue"><i>8</i></div><figcaption>未走线索</figcaption></figure>
|
||||
<figure><div class="bc clue parity"><i>偶</i></div><figcaption>未走奇偶</figcaption></figure>
|
||||
<figure><div class="bc start"><i>1</i></div><figcaption>起点 1(未按下)</figcaption></figure>
|
||||
<figure><div class="bc visited"><i>5</i></div><figcaption>已走</figcaption></figure>
|
||||
<figure><div class="bc visited clue"><i>8</i></div><figcaption>已走线索</figcaption></figure>
|
||||
<figure><div class="bc visited clue parity"><i>6<small>偶</small></i></div><figcaption>已走奇偶</figcaption></figure>
|
||||
<figure><div class="bc visited conflict"><i>6</i><b class="badge">8</b></div><figcaption>冲突 + 角标</figcaption></figure>
|
||||
<figure><div class="bc visited conflict"><i>5</i></div><figcaption>错位:红格 + 轻晃</figcaption></figure>
|
||||
<figure><div class="bc clue hint"><i>5</i></div><figcaption>补数字:1~9 补 5</figcaption></figure>
|
||||
<figure><div class="bc clue hint"><i>4</i></div><figcaption>提示目标</figcaption></figure>
|
||||
<figure><div class="bc visited dim"><i>9</i></div><figcaption>退回压暗</figcaption></figure>
|
||||
<figure><div class="bc visited bright"><i>12</i></div><figcaption>过关提亮</figcaption></figure>
|
||||
</div>
|
||||
<h3>样张:1-7 半途(含大格、紫色连线、红色错格)</h3>
|
||||
<div class="card" style="display:flex;gap:32px;flex-wrap:wrap;align-items:flex-start">
|
||||
<div class="board" id="board-sample"></div>
|
||||
<div class="board" id="board-sample-2"></div>
|
||||
<p class="note" style="max-width:280px">左:路径 1→2→3→4→5,6 踩到线索 8 冲突,只让冲突格变红,连接线始终紫色。右:2×2 大格只计一步,异形格由单位矩形 + 接缝同色拼成。桥保持路径色,与出错格的颜色分开。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="components">
|
||||
<h2>6 · 组件</h2>
|
||||
<h3>顶栏 · 本包进度条 · 字幕</h3>
|
||||
<div class="card" style="max-width:440px">
|
||||
<div class="topbar">
|
||||
<button class="icon-btn" aria-label="返回"><svg viewBox="0 0 24 24" width="26" height="26"><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 class="level-title">1-7</div><div class="level-sub">入门 · 第 7 关</div></div>
|
||||
<div class="remaining" id="pill-demo"><svg viewBox="0 0 24 24" width="16" height="16"><rect x="4" y="4" width="16" height="16" rx="4.5" fill="none" stroke="currentColor" stroke-width="2.4"/></svg><span>6</span></div>
|
||||
</div>
|
||||
<div class="level-progress"><i class="cleared"></i><i class="cleared"></i><i class="cleared"></i><i class="cleared"></i><i class="cleared"></i><i class="cleared"></i><i class="current"></i><i></i><i></i><i></i></div>
|
||||
<div class="caption">走错了,沿原路滑回去</div>
|
||||
<div style="display:flex;gap:10px;align-items:center;margin-top:10px;flex-wrap:wrap">
|
||||
<div class="remaining"><svg viewBox="0 0 24 24" width="16" height="16"><rect x="4" y="4" width="16" height="16" rx="4.5" fill="none" stroke="currentColor" stroke-width="2.4"/></svg><span>12</span></div>
|
||||
<div class="remaining done"><svg viewBox="0 0 24 24" width="16" height="16"><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>完成</span></div>
|
||||
<button class="replay" onclick="replay('#pill-demo','bump')">回放 bump</button>
|
||||
<span class="note">胶囊:默认 / 完成态</span>
|
||||
</div>
|
||||
</div>
|
||||
<h3>底栏按钮 · Toast</h3>
|
||||
<p class="note">三个等宽按钮,高 56px,15px 字号;补数字保留在本局盘面,指路沿用下一步 / 回退高亮。Toast 距页面底边 128px + 安全区,长句换行,避开手势说明。</p>
|
||||
<div class="card" style="max-width:440px">
|
||||
<p class="gesture-help">按住滑动连线 · 点已走格回退</p>
|
||||
<div class="bottombar">
|
||||
<button class="action"><svg viewBox="0 0 24 24" width="22" height="22"><path d="M4 12a8 8 0 1 0 2.5-5.8" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M4 4v5h5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>重开</span></button>
|
||||
<button class="action"><svg viewBox="0 0 24 24" width="22" height="22"><path d="M9 18h6M10 21h4M12 3a6 6 0 0 0-3.6 10.8c.6.5 1 1.2 1.1 2.2h5c.1-1 .5-1.7 1.1-2.2A6 6 0 0 0 12 3z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>补数字</span></button>
|
||||
<button class="action"><svg viewBox="0 0 24 24" width="22" height="22"><path d="M5 19v-7a4 4 0 0 1 4-4h10m-4-4 4 4-4 4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>指路</span></button>
|
||||
</div>
|
||||
<div class="bottombar">
|
||||
<button class="action pulse"><span>重开(脉冲)</span></button>
|
||||
<button class="action" disabled><span>禁用</span></button>
|
||||
</div>
|
||||
<div style="margin-top:14px;display:flex;gap:10px;align-items:center"><span class="toast" id="toast-demo">下一步在这里</span><button class="replay" onclick="replay('#toast-demo','anim')">回放</button></div>
|
||||
</div>
|
||||
<h3>选关:总进度 · 关卡格四态 · 主按钮 · chip</h3>
|
||||
<div class="card" style="max-width:440px">
|
||||
<div class="overall"><div class="overall-bar"><i></i></div><span class="overall-text">16 / 26</span></div>
|
||||
<div class="pack"><div class="pack-title"><span>进阶</span><span class="pack-progress">2 / 10</span></div>
|
||||
<div class="level-grid">
|
||||
<button class="tile cleared"><span>1</span></button><button class="tile cleared"><span>2</span></button><button class="tile current"><span>3</span></button><button class="tile"><span>4</span></button><button class="tile locked" id="tile-locked" onclick="replay('#tile-locked','shake')"><span>5</span></button>
|
||||
</div>
|
||||
<p class="note" style="margin:8px 0 0">已过关 / 已过关 / 当前(呼吸)/ 未过关 / 锁定(点一下抖动)</p>
|
||||
</div>
|
||||
<div style="margin-top:18px"><button class="primary-btn">继续 · 2-3</button></div>
|
||||
<div class="phone footer" style="display:flex;gap:8px;justify-content:center;padding-top:14px"><button class="chip-btn on">音效</button><button class="chip-btn">振动</button><button class="chip-btn danger">重置进度</button></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="screens">
|
||||
<h2>7 · 页面</h2>
|
||||
<p class="lead">390 × 760 样机,桌面浏览器上以 480px 手机框居中,圆角 28 + 框阴影。</p>
|
||||
<div class="phones">
|
||||
<div class="phone">
|
||||
<div style="padding:28px 4px 8px"><h1>数字一笔</h1><p class="sub">按数字顺序,一笔走遍每一格</p>
|
||||
<div class="overall"><div class="overall-bar"><i></i></div><span class="overall-text">16 / 26</span></div></div>
|
||||
<div class="list" id="phone-packs"></div>
|
||||
<button class="primary-btn" style="margin-top:auto">继续 · 2-1</button>
|
||||
<div class="footer"><button class="chip-btn on">音效</button><button class="chip-btn on">振动</button><button class="chip-btn danger">重置进度</button></div>
|
||||
</div>
|
||||
<div class="phone">
|
||||
<div class="topbar">
|
||||
<button class="icon-btn"><svg viewBox="0 0 24 24" width="26" height="26"><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 class="level-title">1-7</div><div class="level-sub">入门 · 第 7 关</div></div>
|
||||
<div class="remaining"><svg viewBox="0 0 24 24" width="16" height="16"><rect x="4" y="4" width="16" height="16" rx="4.5" fill="none" stroke="currentColor" stroke-width="2.4"/></svg><span>6</span></div>
|
||||
</div>
|
||||
<div class="level-progress"><i class="cleared"></i><i class="cleared"></i><i class="cleared"></i><i class="cleared"></i><i class="cleared"></i><i class="cleared"></i><i class="current"></i><i></i><i></i><i></i></div>
|
||||
<div class="caption"></div>
|
||||
<div class="board-area"><div class="board" id="phone-board"></div></div>
|
||||
<p class="gesture-help">按住滑动连线 · 点已走格回退</p>
|
||||
<div class="bottombar">
|
||||
<button class="action"><svg viewBox="0 0 24 24" width="22" height="22"><path d="M4 12a8 8 0 1 0 2.5-5.8" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M4 4v5h5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>重开</span></button>
|
||||
<button class="action"><svg viewBox="0 0 24 24" width="22" height="22"><path d="M9 18h6M10 21h4M12 3a6 6 0 0 0-3.6 10.8c.6.5 1 1.2 1.1 2.2h5c.1-1 .5-1.7 1.1-2.2A6 6 0 0 0 12 3z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>补数字</span></button>
|
||||
<button class="action"><svg viewBox="0 0 24 24" width="22" height="22"><path d="M5 19v-7a4 4 0 0 1 4-4h10m-4-4 4 4-4 4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>指路</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="motion">
|
||||
<h2>8 · 动效</h2>
|
||||
<p class="lead">四档时长、四条缓动。Canvas 侧的弹簧与扫光在这里用 CSS 近似回放,参数以 <code>src/theme.ts MOTION</code> 为准。</p>
|
||||
<table>
|
||||
<tr><th>档</th><th>时长</th><th>用途</th><th>缓动</th></tr>
|
||||
<tr><td>fast</td><td>120ms</td><td>按下反馈、步号淡入、桥生长</td><td>ease-out <code>cubic-bezier(.22,1,.36,1)</code></td></tr>
|
||||
<tr><td>base</td><td>200ms</td><td>整屏切换、toast、胶囊 bump、进场单格</td><td>ease-out / spring <code>cubic-bezier(.34,1.56,.64,1)</code></td></tr>
|
||||
<tr><td>slow</td><td>320ms</td><td>闪白衰减、过关扫光、抖动、进度条</td><td>linear(衰减)/ ease-out</td></tr>
|
||||
<tr><td>hold</td><td>500ms</td><td>过关停留</td><td>–</td></tr>
|
||||
<tr><td>循环</td><td>0.9s / 2.4s</td><td>提示按钮脉冲 / 当前关呼吸、提示描边</td><td>ease-in-out <code>cubic-bezier(.65,0,.35,1)</code></td></tr>
|
||||
</table>
|
||||
<h3>逐个回放</h3>
|
||||
<div class="grid g3" id="demos">
|
||||
<div class="demo"><header><b>开局进场</b><span>base · 每格 +14ms</span></header><div class="stage" id="d-enter"></div><p>缩放 0.92 → 1 + 淡入,按阅读顺序错落,延迟封顶 200ms。</p><button class="replay" onclick="replayStage('#d-enter','a-enter')">回放</button></div>
|
||||
<div class="demo"><header><b>落子弹簧</b><span>≈200ms · k=260 c=18</span></header><div class="stage" id="d-pop"></div><p>当前格放大回 1,步号同时 120ms 淡入。</p><button class="replay" onclick="replayStage('#d-pop','a-pop a-fade')">回放</button></div>
|
||||
<div class="demo"><header><b>桥生长</b><span>fast · ease-out</span></header><div class="stage" id="d-bridge"></div><p>只长厚度,跨缝方向第一帧就盖满,不露白缝。</p><button class="replay" onclick="replayStage('#d-bridge','a-bridge')">回放</button></div>
|
||||
<div class="demo"><header><b>踩对线索闪白</b><span>slow · 线性衰减</span></header><div class="stage" id="d-flash"></div><p>描边闪白 1 → 0。这是「推对了」的主奖励。</p><button class="replay" onclick="replayStage('#d-flash','a-flash')">回放</button></div>
|
||||
<div class="demo"><header><b>死路 / 非法起手抖动</b><span>280ms · 6 个来回</span></header><div class="stage" id="d-shake"></div><p>幅度 0.06 u 递减,不弹「失败」。</p><button class="replay" onclick="replayStage('#d-shake','a-shake')">回放</button></div>
|
||||
<div class="demo"><header><b>过关扫光</b><span>slow · 每格 250ms</span></header><div class="stage" id="d-sweep"></div><p>全盘切亮色,按步序扫白光,320ms 内起完。</p><button class="replay" onclick="replayStage('#d-sweep','a-sweep')">回放</button></div>
|
||||
<div class="demo"><header><b>提示描边</b><span>2.4s · ≈1.1Hz</span></header><div class="stage" id="d-hint"></div><p>琥珀外描边正弦脉冲,最后 0.4s 淡出。</p><button class="replay" onclick="replayStage('#d-hint','a-hint')">回放</button></div>
|
||||
<div class="demo"><header><b>整屏切换</b><span>base · ease-out</span></header><div class="stage" id="d-screen"><div style="width:180px;height:80px;border-radius:14px;background:var(--bg);border:1px solid var(--line);display:flex;align-items:center;justify-content:center;font-weight:700">选关 → 对局</div></div><p>淡入 + 上移 8px。关与关之间不切屏,只有盘面进退场。</p><button class="replay" onclick="replayStage('#d-screen','a-screen', true)">回放</button></div>
|
||||
<div class="demo"><header><b>冲突</b><span>280ms · 单格轻晃</span></header><div class="stage" id="d-conflict"><div class="bc visited conflict a-shake"><i>5</i></div></div><p>立刻红底 + 单格轻晃 + 闷音 + 40ms 振动。连接线始终紫色;步号错位不加叉号。撤销立即褪回并停止晃动。</p><button class="replay" onclick="replayStage('#d-conflict','a-shake')">回放</button></div>
|
||||
</div>
|
||||
<h3>过关时间线</h3>
|
||||
<div class="card">
|
||||
<div class="timeline">
|
||||
<div class="p1">扫光<small>0 – 320ms · 和弦起、胶囊「完成」、按钮禁用</small></div>
|
||||
<div>停留<small>320 – 820ms</small></div>
|
||||
<div class="p3">淡出<small>820 – 980ms</small></div>
|
||||
<div class="p1">下一关进场<small>980ms 起 · 顶栏就地更新,盘面错落进场 ≤ 400ms</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<h3>减弱动态</h3>
|
||||
<p class="note">系统开启「减弱动态效果」时:CSS 动画与过渡全部关闭;Canvas 去掉进场错落、弹簧、扫光与淡出,只保留状态切换与提示描边。</p>
|
||||
</section>
|
||||
|
||||
<section id="sound">
|
||||
<h2>9 · 声音与振动</h2>
|
||||
<table>
|
||||
<tr><th>事件</th><th>音效</th><th>振动</th></tr>
|
||||
<tr><td>落子</td><td>90ms 正弦短音,C5 起每步升半音,12 步循环</td><td>8ms(踩对线索 18ms)</td></tr>
|
||||
<tr><td>冲突</td><td>160ms 方波 130Hz 闷音</td><td>40ms</td></tr>
|
||||
<tr><td>非法起手</td><td>60ms 三角波 220Hz</td><td>15ms</td></tr>
|
||||
<tr><td>死路</td><td>–</td><td>10 · 30 · 10</td></tr>
|
||||
<tr><td>过关</td><td>C-E-G-C 三角波和弦,每音 220ms,间隔 70ms</td><td>20 · 40 · 30</td></tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section id="sync">
|
||||
<h2>10 · 同步规则</h2>
|
||||
<div class="card">
|
||||
<ol class="note" style="margin:0;padding-left:18px;line-height:1.9">
|
||||
<li>先改 <code>docs/design.md</code> 的对应表格,并在「变更记录」加一行。</li>
|
||||
<li>改 <code>modes/01-number-stroke/web/style.css</code> 的 <code>:root</code> 与 <code>src/theme.ts</code>。</li>
|
||||
<li>更新本页顶部 <code>:root</code> 与相应样张。</li>
|
||||
<li><code>npm run check</code>:<code>tests/theme.test.ts</code> 会比对 CSS 与 theme.ts 的颜色、动效时长与字体栈。</li>
|
||||
<li><code>npm run build:standalone</code> 重新生成试玩版。</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
<script>
|
||||
// ---- 色板 ----
|
||||
const TOKENS = [
|
||||
['背景', '--bg', '#FFFFFF', '页面与盘面背景 · THEME.bg'],
|
||||
['桌面底', '--bg-app', '#F2F2F6', '手机框外的底色'],
|
||||
['二级表面', '--surface-2', '#F3F3F7', '按钮、胶囊、chip、锁定格'],
|
||||
['未走格', '--cell', '#E6E7EC', '盘面未走格、未过关格、进度条底 · THEME.cell'],
|
||||
['分隔线', '--line', '#E1E2EA', '细线(预留)'],
|
||||
['墨', '--ink', '#23243A', '标题、正文、未走线索数字、角标底 · THEME.clueInk'],
|
||||
['墨 2', '--ink-2', '#6B6D80', '副文案、字幕、胶囊、图标'],
|
||||
['墨 3', '--ink-3', '#A3A5B5', '弱文案、进度数字、锁图标'],
|
||||
['主色', '--primary', '#6558F5', '已走格、桥、当前关、主按钮 · THEME.visited'],
|
||||
['主色·亮', '--primary-bright', '#7D72FF', '过关提亮 · THEME.visitedBright'],
|
||||
['主色·淡', '--primary-soft', '#ECEAFE', '当前关格底、开启态 chip、完成态胶囊'],
|
||||
['主色上的字', '--on-primary', '#FFFFFF', '已走格数字、主按钮文字 · THEME.visitedInk'],
|
||||
['冲突', '--danger', '#E5484D', '冲突格、重置进度 · THEME.conflict'],
|
||||
['冲突·淡', '--danger-soft', '#FDECEC', '预留(高对比模式)'],
|
||||
['提示', '--hint', '#F5A524', '提示描边、提示按钮脉冲 · THEME.hint'],
|
||||
['提示·淡', '--hint-soft', '#FEF3DD', '预留'],
|
||||
['线索描边', '--clue-ring', '#23243A', '未走线索 / 奇偶格描边(= 墨,与数字同色)· THEME.clueRing'],
|
||||
['已走线索描边', '(canvas)', '#DFE1EC', '起点与已走线索外圈 · THEME.activeClueRing'],
|
||||
['遮罩', '--overlay', 'rgba(35,36,58,.92)', 'toast 底'],
|
||||
];
|
||||
document.getElementById('swatches').innerHTML = TOKENS.map(([n, v, hex, use]) =>
|
||||
`<div class="sw"><div class="chip" style="background:${hex}"></div><div class="meta"><b>${n}</b><code>${hex}</code> <code>${v}</code><small>${use}</small></div></div>`).join('');
|
||||
|
||||
// ---- 盘面样张 ----
|
||||
function cell(cls, text, opts = {}) {
|
||||
const badge = opts.badge ? `<b class="badge">${opts.badge}</b>` : '';
|
||||
const sub = opts.sub ? `<small>${opts.sub}</small>` : '';
|
||||
const style = (opts.col ? `grid-column:${opts.col};` : '') + (opts.row ? `grid-row:${opts.row};` : '') + (opts.i !== undefined ? `--i:${opts.i};` : '');
|
||||
return `<div class="bc ${cls}" style="${style}"><i>${text}${sub}</i>${badge}</div>`;
|
||||
}
|
||||
// 桥:放在两格之间。unit 用 CSS 变量,位置按格坐标算
|
||||
function bridge(dir, x, y, red = false) {
|
||||
// dir h:竖向共享边(左右相邻),x 为边的列号,y 为格行号;dir v:横向共享边(上下相邻)
|
||||
const u = 'var(--u)';
|
||||
const pad = '14px';
|
||||
if (dir === 'h') return `<div class="bridge h ${red ? 'red' : ''}" style="left:calc(${pad} + ${u} * ${x} - (${u} * 0.05 + 1px)); top:calc(${pad} + ${u} * ${y} + ${u} * 0.27)"></div>`;
|
||||
return `<div class="bridge v ${red ? 'red' : ''}" style="top:calc(${pad} + ${u} * ${y} - (${u} * 0.05 + 1px)); left:calc(${pad} + ${u} * ${x} + ${u} * 0.27)"></div>`;
|
||||
}
|
||||
const b1 = document.getElementById('board-sample');
|
||||
b1.style.gridTemplateColumns = 'repeat(4, var(--u))';
|
||||
b1.innerHTML =
|
||||
cell('start visited clue', '1') + cell('visited', '2') + cell('visited', '3') + cell('visited', '4') +
|
||||
cell('clue', '4') + cell('', '') + cell('visited conflict', '6', { badge: '8' }) + cell('visited', '5') +
|
||||
cell('', '') + cell('', '') + cell('', '') + cell('', '') +
|
||||
bridge('h', 1, 0) + bridge('h', 2, 0) + bridge('h', 3, 0) + bridge('v', 3, 1) + bridge('h', 3, 1);
|
||||
const b2 = document.getElementById('board-sample-2');
|
||||
b2.style.gridTemplateColumns = 'repeat(4, var(--u))';
|
||||
b2.innerHTML =
|
||||
cell('start visited clue', '1') + cell('visited', '2') + cell('visited big clue', '3', { col: '3 / span 2', row: '1 / span 2' }) +
|
||||
cell('', '') + cell('clue', '7') +
|
||||
cell('', '') + cell('', '') + cell('clue two', '12') + cell('', '') +
|
||||
bridge('h', 1, 0) + bridge('h', 2, 0);
|
||||
const pb = document.getElementById('phone-board');
|
||||
pb.style.gridTemplateColumns = 'repeat(4, var(--u))';
|
||||
pb.innerHTML =
|
||||
cell('start visited clue', '1') + cell('visited', '2') + cell('', '') + cell('', '') +
|
||||
cell('visited clue', '4') + cell('visited', '3') + cell('clue', '8') + cell('', '') +
|
||||
cell('visited', '5') + cell('visited', '6') + cell('', '') + cell('', '') +
|
||||
bridge('h', 1, 0).replace('14px', '0px').replace('14px', '0px') + bridge('v', 1, 1).replace('14px', '0px').replace('14px', '0px') +
|
||||
bridge('h', 1, 1).replace('14px', '0px').replace('14px', '0px') + bridge('v', 0, 2).replace('14px', '0px').replace('14px', '0px') +
|
||||
bridge('h', 1, 2).replace('14px', '0px').replace('14px', '0px');
|
||||
|
||||
// ---- 选关样机 ----
|
||||
function packHtml(title, n, cleared, current, locked) {
|
||||
let tiles = '';
|
||||
for (let i = 1; i <= n; i++) {
|
||||
const cls = i <= cleared ? 'cleared' : i === current ? 'current' : locked && i > current ? 'locked' : '';
|
||||
tiles += `<button class="tile ${cls}"><span>${i}</span></button>`;
|
||||
}
|
||||
return `<div class="pack ${cleared === n ? 'done' : ''}"><div class="pack-title"><span>${title}</span><span class="pack-progress">${cleared} / ${n}</span></div><div class="level-grid">${tiles}</div></div>`;
|
||||
}
|
||||
document.getElementById('phone-packs').innerHTML = packHtml('入门', 10, 10, 0, false) + packHtml('进阶', 10, 0, 1, true) + packHtml('奇与偶', 6, 6, 0, false);
|
||||
|
||||
// ---- 动效样张 ----
|
||||
const stages = {
|
||||
'd-enter': () => [0, 1, 2, 3].map((i) => cell(i === 0 ? 'start visited clue' : i === 2 ? 'clue' : '', i === 0 ? '1' : i === 2 ? '8' : '', { i })).join(''),
|
||||
'd-pop': () => cell('visited', '4') + cell('visited', '<span>5</span>'),
|
||||
'd-bridge': () => `<div style="position:relative;display:flex">${cell('visited', '4')}${cell('visited', '5')}<div class="bridge h" style="left:calc(64px - (64px * 0.05 + 1px));top:calc(64px * 0.27)"></div></div>`,
|
||||
'd-flash': () => cell('visited clue', '8'),
|
||||
'd-shake': () => cell('visited', '7'),
|
||||
'd-sweep': () => [1, 2, 3, 4, 5].map((n, i) => cell('visited bright', String(n), { i })).join(''),
|
||||
'd-hint': () => cell('clue', '4'),
|
||||
};
|
||||
for (const [id, html] of Object.entries(stages)) document.getElementById(id).innerHTML = html();
|
||||
function replay(sel, cls) {
|
||||
const el = document.querySelector(sel);
|
||||
cls.split(' ').forEach((c) => el.classList.remove(c));
|
||||
void el.offsetWidth;
|
||||
cls.split(' ').forEach((c) => el.classList.add(c));
|
||||
}
|
||||
function replayStage(sel, cls, self = false) {
|
||||
const stage = document.querySelector(sel);
|
||||
const targets = self ? [stage.firstElementChild] : [...stage.querySelectorAll('.bc, .bridge')].concat(stage.querySelector('.bridge') ? [] : []);
|
||||
const list = self ? targets : (cls.includes('a-bridge') ? [stage] : targets);
|
||||
list.forEach((el) => { cls.split(' ').forEach((c) => el.classList.remove(c)); });
|
||||
void stage.offsetWidth;
|
||||
list.forEach((el) => { cls.split(' ').forEach((c) => el.classList.add(c)); });
|
||||
}
|
||||
// 首屏自动播一遍进场
|
||||
setTimeout(() => replayStage('#d-enter', 'a-enter'), 300);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
-328
@@ -1,328 +0,0 @@
|
||||
# UI 设计规范 · design.md
|
||||
|
||||
版本 1.5 · 2026-09-16 · 参考实现:玩法一「数字一笔」(`modes/01-number-stroke`)。设计师看图请打开同目录的 [`design.html`](design.html)(可离线双击,含色板、字阶、组件样张与可回放的动效)。
|
||||
|
||||
本文是产品级的视觉与动效规范。规则、关卡、交互语义仍以各玩法的 GDD 为准;本文只管「长什么样、怎么动」。
|
||||
|
||||
## 0. 怎么用这份文档
|
||||
|
||||
三处必须一致,改任何一处都要同步另外两处:
|
||||
|
||||
| 位置 | 内容 | 谁在用 |
|
||||
|---|---|---|
|
||||
| `docs/design.md` + `docs/design.html` | 规范与样张 | 设计师、接手人 |
|
||||
| `modes/01-number-stroke/web/style.css` 的 `:root` | CSS 令牌 | DOM 壳(选关、顶栏、底栏、toast) |
|
||||
| `modes/01-number-stroke/src/theme.ts` | `THEME` / `BOARD_TYPE` / `MOTION` | Canvas 盘面(Web 与 Cocos 共用) |
|
||||
|
||||
`tests/theme.test.ts` 会校验 `style.css` 与 `theme.ts` 的颜色、动效时长、字体栈一致,改了一边没改另一边 `npm run check` 会红。
|
||||
|
||||
**改设计的流程**:先改本文对应表格 → 改 `style.css` / `theme.ts` → 改 `design.html` 样张 → 在 §12 变更记录追加一行。
|
||||
|
||||
## 1. 设计原则
|
||||
|
||||
1. **板要干净。** 逻辑 puzzle 的盘面只回答三个问题:我走到哪了、哪错了、过没过。任何装饰都不能和这三件事抢注意力。
|
||||
2. **一个主色。** 蓝紫 `#6558F5` 只表示「已走 / 当前 / 可继续」。红只表示冲突,琥珀只表示提示。不要每关换主题色。
|
||||
3. **数字优先。** 盘面上的数字是内容本身。数字用系统圆体、800 字重、等宽数字;其余文字都比数字轻。
|
||||
4. **动效四档。** 所有动效只用 120 / 200 / 320 / 500 ms 四档时长和四条缓动曲线。新动效先归档,再写代码。
|
||||
5. **反馈紧贴手指。** 落子、踩对、冲突、死路的反馈都发生在手指所在的格子上,不弹窗、不撒纸片。
|
||||
6. **信息随手可见。** 关号、包名、本包进度、剩余格数常驻顶栏,一眼看清「我在哪、还差多少」。
|
||||
|
||||
## 2. 色彩
|
||||
|
||||
浅色单主题。所有颜色取自下表,DOM 用 CSS 变量,Canvas 用 `THEME`。
|
||||
|
||||
### 2.1 令牌
|
||||
|
||||
| 令牌 | 值 | 用途 | CSS | theme.ts |
|
||||
|---|---|---|---|---|
|
||||
| 背景 | `#FFFFFF` | 页面与盘面背景 | `--bg` | `bg` |
|
||||
| 桌面底 | `#F2F2F6` | 桌面浏览器上手机框外的底色 | `--bg-app` | – |
|
||||
| 二级表面 | `#F3F3F7` | 按钮、胶囊、chip、锁定关卡格 | `--surface-2` | – |
|
||||
| 未走格 | `#E6E7EC` | 盘面未走格、选关未过关格、进度条底 | `--cell` | `cell` |
|
||||
| 分隔线 | `#E1E2EA` | 细线(预留) | `--line` | – |
|
||||
| 墨 | `#23243A` | 标题、正文、未走线索数字、冲突角标底 | `--ink` | `clueInk` |
|
||||
| 墨 2 | `#6B6D80` | 副文案、字幕、胶囊文字、图标 | `--ink-2` | – |
|
||||
| 墨 3 | `#A3A5B5` | 弱文案、进度数字、锁图标 | `--ink-3` | – |
|
||||
| 主色 | `#6558F5` | 已走格、桥、当前关、主按钮、进度 | `--primary` | `visited` |
|
||||
| 主色·亮 | `#7D72FF` | 过关后全盘提亮 | `--primary-bright` | `visitedBright` |
|
||||
| 主色·淡 | `#ECEAFE` | 当前关格底、开启态 chip、完成态胶囊 | `--primary-soft` | – |
|
||||
| 主色上的字 | `#FFFFFF` | 已走格数字、主按钮文字 | `--on-primary` | `visitedInk` |
|
||||
| 冲突 | `#E5484D` | 冲突格、重置进度 | `--danger` | `conflict` |
|
||||
| 冲突·淡 | `#FDECEC` | 预留(高对比模式) | `--danger-soft` | – |
|
||||
| 提示 | `#F5A524` | 提示描边、提示按钮脉冲 | `--hint` | `hint` |
|
||||
| 提示·淡 | `#FEF3DD` | 预留 | `--hint-soft` | – |
|
||||
| 线索描边 | `#23243A` | 未走线索 / 奇偶格的描边(= 墨,与线索数字同色) | `--clue-ring` | `clueRing` |
|
||||
| 已走线索描边 | `#DFE1EC` | 起点与已走线索格外圈的灰白描边 | – | `activeClueRing` |
|
||||
| 闪色 | `#FFFFFF` | 踩对线索闪白、过关扫光 | – | `flash` |
|
||||
| 遮罩 | `rgba(35,36,58,.92)` | toast 底 | `--overlay` | – |
|
||||
|
||||
阴影:主按钮 `0 6px 18px rgba(101,88,245,.28)`,按下时 `0 3px 10px rgba(101,88,245,.22)`;桌面手机框 `0 20px 60px rgba(20,20,40,.14)`。盘面无阴影。
|
||||
|
||||
### 2.2 语义规则
|
||||
|
||||
- 紫 = 路径。已走格、桥、起点 `1`、当前关、主按钮、进度条都是同一个紫。
|
||||
- 红只出现在冲突格。错误发生时该格轻晃 280ms,进出连线保持紫色;步号占用别处固定数字时不显示「×」。本格数字 / 奇偶要求不符时仍保留原要求角标。
|
||||
- 琥珀只出现在提示描边和提示按钮脉冲,不用于任何常驻元素。
|
||||
- 灰阶承担一切非路径信息:未走格 `cell`、文字三档墨。未走线索 / 奇偶格的描边与线索数字同用墨色,像印在盘上的一圈,不引入第二种彩色。
|
||||
- 过关后全盘从 `primary` 切到 `primary-bright`,这是唯一允许的「换色」。
|
||||
|
||||
### 2.3 对比度
|
||||
|
||||
| 组合 | 对比度 | 用途 |
|
||||
|---|---|---|
|
||||
| `#FFFFFF` on `#6558F5` | 5.2 : 1 | 已走格数字、主按钮 |
|
||||
| `#23243A` on `#E6E7EC` | 12.6 : 1 | 未走线索数字 |
|
||||
| `#FFFFFF` on `#E5484D` | 4.5 : 1 | 冲突格数字(角标另有墨底白字) |
|
||||
| `#6B6D80` on `#FFFFFF` | 5.1 : 1 | 副文案 |
|
||||
| `#23243A` on `#E6E7EC` | 12.6 : 1 | 未走线索描边(与数字同色) |
|
||||
| `#A3A5B5` on `#FFFFFF` | 2.5 : 1 | 仅用于非关键弱文案 |
|
||||
|
||||
## 3. 字体与字阶
|
||||
|
||||
### 3.1 字体栈
|
||||
|
||||
```
|
||||
ui-rounded, "SF Pro Rounded", "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", system-ui, -apple-system, "Segoe UI", sans-serif
|
||||
```
|
||||
|
||||
`ui-rounded` 让 iOS / macOS 直接拿到系统圆体(数字圆润、与圆角格子同气质);Android 回落到 Noto Sans SC;Windows 回落到 Segoe UI。DOM 与 Canvas 用同一串(`--font` 与 `BOARD_FONT_FAMILY`)。
|
||||
|
||||
全局 `font-variant-numeric: tabular-nums`,所有数字等宽,计数变化时不跳动。
|
||||
|
||||
### 3.2 UI 字阶
|
||||
|
||||
| 名称 | 字号 / 字重 | 用途 | CSS |
|
||||
|---|---|---|---|
|
||||
| display | 34 / 800 | 选关页标题 | `--fs-display` |
|
||||
| title | 22 / 800 | 顶栏关号 `1-7` | `--fs-title` |
|
||||
| heading | 17 / 700 | 包标题、主按钮 | `--fs-heading` |
|
||||
| body | 15 / 500 | 副标题、字幕 | `--fs-body` |
|
||||
| label | 14 / 600–700 | 剩余格胶囊、包进度、toast | `--fs-label` |
|
||||
| caption | 13 / 600 | chip、总进度数字 | `--fs-caption` |
|
||||
| micro | 12 / 600 | 顶栏副标题「入门 · 第 7 关」(字距 0.04em) | `--fs-micro` |
|
||||
|
||||
选关关卡格数字 20 / 700;底栏按钮 15 / 700。
|
||||
|
||||
### 3.3 盘面字阶(相对单位格边长 u)
|
||||
|
||||
| 用途 | 比例 | 字重 | theme.ts |
|
||||
|---|---|---|---|
|
||||
| 起点 `1`(未按下的提示态与已走第 1 步) | 0.60 u | 800 | `BOARD_TYPE.start` |
|
||||
| 单位数步号 / 线索 | 0.52 u | 800 | `single` |
|
||||
| 两位数步号 / 线索 | 0.46 u | 800 | `double` |
|
||||
| 未走奇偶格「奇 / 偶」 | 0.44 u | 700 | `parity` |
|
||||
| 已走奇偶格下方小字「奇 / 偶」 | 0.19 u | 700 | `paritySub` |
|
||||
| 冲突角标半径 / 角标字号 | 0.15 u / 半径 × 1.25 | 800 | `badge` / `badgeText` |
|
||||
| 线索描边线宽 | 0.05 u(≥ 2px) | – | `ring` |
|
||||
| 提示描边线宽 | 0.08 u(≥ 3px) | – | `hintRing` |
|
||||
|
||||
数字一律 800,汉字 700,文字基线下沉 4% 字号做视觉居中。大格 / 异形格的数字放在锚点单位(形心最近的单位格)中心。
|
||||
|
||||
## 4. 间距、圆角、尺寸
|
||||
|
||||
- **间距**:4pt 网格。`--sp-1..7` = 4 / 8 / 12 / 16 / 20 / 24 / 28。页面左右内边距 20,顶部 12 + 安全区,底部 16 + 安全区。
|
||||
- **圆角**:`--r-sm` 10(预留)、`--r-md` 14(关卡格、toast)、`--r-lg` 18(主按钮、底栏按钮)、`--r-xl` 24(预留)、`--r-pill` 胶囊。桌面手机框 28。
|
||||
- **盘面**:格圆角 0.18 u,格间缝 0.10 u,桥厚 0.46 u。
|
||||
- **点击区**:所有可点元素 ≥ 44 × 44(`--tap`)。返回按钮 44,chip 34 高但左右各留 16 内边距。
|
||||
- **桌面**:≥ 520px 宽时以 480px 手机框居中,四周 24 留白,圆角 28 + 框阴影。
|
||||
|
||||
## 5. 盘面(Canvas)
|
||||
|
||||
### 5.1 布局
|
||||
|
||||
- `fitBoard`:按短边缩放,四周留 10px,单位格封顶 96px(≤ 6 格的教学盘)或 110px。
|
||||
- 整盘垂直居中后**上提容器高度的 3%**:视觉中心略高于几何中心,小盘不显得下坠。
|
||||
- 命中测试:触屏每格四周 8% 死区,鼠标 14%。点按或滑到任意历史格可直接回退;触点进入历史格时优先处理回退。
|
||||
|
||||
### 5.2 格的状态
|
||||
|
||||
| 状态 | 底 | 描边 | 数字 | 备注 |
|
||||
|---|---|---|---|---|
|
||||
| 未走 | `cell` | 无 | 无 | – |
|
||||
| 未走线索 | `cell` | `clue-ring`(墨)0.05 u | `ink` 0.52 / 0.46 u | 描边用「外层剪影 + 内层填充」画,矩形与异形同一画法 |
|
||||
| 未走奇偶 | `cell` | `clue-ring`(墨)0.05 u | 「奇 / 偶」`ink` 0.44 u | – |
|
||||
| 起点 `1`(未按下) | `primary` | `activeClueRing` | `on-primary` 0.60 u | 只是视觉提示,按下才计入路径 |
|
||||
| 已走 | `primary` | 无 | 步号 `on-primary` | 与相邻已走格之间画桥 |
|
||||
| 已走线索 / 起点 | `primary` | `activeClueRing` | 步号 | 描边保留,说明它曾是线索 |
|
||||
| 已走奇偶 | `primary` | `activeClueRing` | 步号上移 0.09 u,下方 0.19 u 小字「奇 / 偶」 | – |
|
||||
| 冲突 | `danger` | 线索有描边则 `activeClueRing` | 步号 `on-primary` | 单格轻晃 280ms,连接线仍紫色;步号错位无叉号。本格要求不符时保留数字 / 奇偶角标,半径 0.15 u |
|
||||
| 补数字 | 同普通线索 | 先叠加琥珀外圈 2.4s,之后恢复普通线索描边 | 新数字使用普通数字字阶;奇偶格保留下方小字 | 本局重开仍保留数字 |
|
||||
| 提示目标 | 不变 | 外圈 `hint` 描边 0.08 u,向外扩一线宽 | 不变 | 2.4s 脉冲后消失 |
|
||||
| 退回压暗 | 覆 `bg` 45% | – | – | 提示「已偏离」时,分叉点之后的段压暗 |
|
||||
| 过关 | `primary-bright` | – | – | 按步序扫白光 |
|
||||
|
||||
大格(2×2、3×3、1×2…)是一个圆角矩形;异形格由单位矩形 + 接缝矩形同色拼成一整块,凸角圆、凹角直。
|
||||
|
||||
### 5.3 桥
|
||||
|
||||
两个相邻已走格之间的连接:沿共享边居中,厚 0.46 u,跨缝方向盖满缝再多 1px。始终使用路径色;任一侧冲突也不变红、不随格子晃动。**生长动画只长厚度**,跨缝方向从第一帧起就盖满,不露白缝。
|
||||
|
||||
## 6. 组件
|
||||
|
||||
### 6.1 顶栏(对局)
|
||||
|
||||
```
|
||||
[←] 1-7 [▢ 6]
|
||||
入门 · 第 7 关
|
||||
▬▬ ▬▬ ▬▬ ▬▬ ▬▬ ▬▬ ▬▬ ▬▬ ▬▬ ▬▬ ← 本包进度条
|
||||
```
|
||||
|
||||
- 三列网格 `1fr auto 1fr`,中间永远居中。高 52。
|
||||
- 返回:44 圆形点击区,墨 2 色 26px 箭头,按下时底 `surface-2`。
|
||||
- 关号:title 22 / 800;副标题 micro 12 / 600 墨 3,内容 `{包名} · 第 {n} 关`。
|
||||
- 剩余格胶囊:高 32,`surface-2` 底,墨 2 字 14 / 700,左侧 16px 圆角方框图标(墨 3)。数字变化时 `bump`(200ms spring 缩放 1 → 1.14 → 1)。过关后切到 `done`:底 `primary-soft`、字与图标 `primary`、图标换勾、文字「完成」。
|
||||
- 本包进度条:每关一段,高 4,段间 3,圆角 2。灰 = 未到,紫 40% = 已过,紫 100% = 当前。
|
||||
|
||||
### 6.2 字幕
|
||||
|
||||
顶栏下方 36 高一行,body 15 / 500 墨 2 居中。只有教学关有内容,空时 200ms 淡出。
|
||||
|
||||
### 6.3 底栏
|
||||
|
||||
三个等宽按钮「重开 / 补数字 / 指路」(最大 170),高 56,圆角 18,`surface-2` 底,15 / 700 墨色,图标 22px 墨 2,图标与文字间距 4px。按下底色变 `cell` + 缩放 0.97。禁用 40% 透明。「指路」发现起手错时,重开按钮 `pulse`:0.9s 琥珀外圈呼吸。
|
||||
|
||||
底栏上方是常驻手势说明「按住滑动连线 · 点已走格回退」,caption 13px 墨 2,顶部间距 8px,居中。
|
||||
|
||||
### 6.4 Toast
|
||||
|
||||
距页面底边 128px + 底部安全区居中,避开手势说明。圆角 14,`overlay` 底白字 14 / 600,最大宽度为页面宽减 40px,长句自动换行;进出 200ms(透明 + 上移 8px),停留 1.8s。
|
||||
|
||||
### 6.5 选关页
|
||||
|
||||
- 标题 display 34 / 800,副标题 body 墨 2;下方**总进度**:6px 胶囊条(`cell` 底、`primary` 填充,320ms 过渡)+ 右侧 `16 / 26` caption 墨 3。
|
||||
- 包标题 heading 17 / 700,右侧 `x / n` label 墨 3;整包完成时变 `primary`。
|
||||
- 关卡格:5 列,间距 12,正方形,圆角 14,数字 20 / 700。
|
||||
- 未过关:`cell` 底墨字。
|
||||
- 已过关:`primary` 底白字,右上角 CSS 画的小勾。
|
||||
- 当前:`primary-soft` 底 + 3px 内描边 `primary` + 紫字,2.4s 呼吸外圈。
|
||||
- 锁定:`surface-2` 底,墨 3 锁图标,无数字;点击 320ms 抖动。
|
||||
- 按下缩放 0.94。
|
||||
- 列表可滚,顶部 12px、底部 20px 渐隐遮罩,不被按钮切断。
|
||||
- 继续按钮:高 54,圆角 18,`primary` 底白字 17 / 700,紫色投影;文案 `继续 · 2-1`。
|
||||
- 设置 chip:高 34 胶囊,`surface-2` 底墨 2 字 13 / 600;开启态 `primary-soft` 底紫字;「重置进度」红字。
|
||||
|
||||
## 7. 页面
|
||||
|
||||
### 7.1 对局页(自上而下)
|
||||
|
||||
| 区块 | 高度 |
|
||||
|---|---|
|
||||
| 安全区 + 12 | – |
|
||||
| 顶栏 | 52 |
|
||||
| 本包进度条 | 8 上边距 + 4 |
|
||||
| 字幕 | 8 上边距 + 36 |
|
||||
| 盘面 | 剩余全部(flex: 1) |
|
||||
| 手势说明 | 8 上边距 + caption 行高 |
|
||||
| 底栏 | 12 上边距 + 56 |
|
||||
| 16 + 安全区 | – |
|
||||
|
||||
### 7.2 状态流
|
||||
|
||||
```
|
||||
选关 ──点关卡 / 继续──▶ 对局(整屏淡入 200ms,盘面错落进场)
|
||||
对局 ──过关──▶ 扫光 320ms → 停留 500ms → 盘面淡出 160ms → 下一关盘面进场(顶栏就地更新)
|
||||
对局 ──最后一关过关──▶ 选关(toast「已通关全部 26 关」)
|
||||
对局 ──返回──▶ 选关(整屏淡入)
|
||||
```
|
||||
|
||||
## 8. 动效
|
||||
|
||||
### 8.1 时长与缓动
|
||||
|
||||
| 档 | 时长 | 用途 | CSS | theme.ts |
|
||||
|---|---|---|---|---|
|
||||
| fast | 120ms | 按下反馈、步号淡入、桥生长 | `--t-fast` | `MOTION.fast` |
|
||||
| base | 200ms | 整屏切换、toast、胶囊 bump、进场单格 | `--t-base` | `MOTION.base` |
|
||||
| slow | 320ms | 闪白衰减、过关扫光、抖动、进度条 | `--t-slow` | `MOTION.slow` |
|
||||
| hold | 500ms | 过关停留 | `--t-hold` | `MOTION.hold` |
|
||||
|
||||
| 缓动 | 曲线 | 用途 |
|
||||
|---|---|---|
|
||||
| ease-out | `cubic-bezier(.22, 1, .36, 1)` | 进场、淡入、桥生长(Canvas 用 quart ease-out) |
|
||||
| ease-in | `cubic-bezier(.4, 0, 1, 1)` | 退场 |
|
||||
| ease-in-out | `cubic-bezier(.65, 0, .35, 1)` | 呼吸、脉冲 |
|
||||
| spring | `cubic-bezier(.34, 1.56, .64, 1)` | bump;Canvas 弹簧 k=260、c=18、初速 2.4 |
|
||||
|
||||
### 8.2 事件 → 动效
|
||||
|
||||
| 事件 | 对象 | 动效 | 时长 | 音效 | 振动 |
|
||||
|---|---|---|---|---|---|
|
||||
| 开局 / 切关 | 每个格 | 缩放 0.92 → 1 + 淡入,按阅读顺序每格延迟 14ms,延迟封顶 200ms | base | – | – |
|
||||
| 追加一步 | 当前格 | 弹簧放大回 1(初速 2.4) | ≈ 200 | step(音高随步号) | 8ms |
|
||||
| 追加一步 | 步号 | 淡入 | fast | – | – |
|
||||
| 追加一步 | 桥 | 厚度 0 → 1(ease-out),跨缝方向始终盖满 | fast | – | – |
|
||||
| 踩对线索 | 该格描边 | 闪白 1 → 0 线性衰减 | slow | step | 18ms |
|
||||
| 冲突 | 该格 | 立即红底 + 横向轻晃,幅度 0.06 u 递减;进出连线保持紫色 | 280ms | conflict(闷音) | 40ms |
|
||||
| 撤销 | 该格 / 桥 | 立即褪回,无动画 | 0 | – | – |
|
||||
| 多步回退 | 目标格 | 后缀立即撤回,目标格轻弹;toast 说明回到第几步 | ≈ 200 | – | 12ms |
|
||||
| 补数字 | 目标格 | 数字成为固定线索,目标轻弹并显示琥珀外圈 | 2.4s | – | 18ms |
|
||||
| 死路 | 当前格 | 横向抖动,6 个来回,幅度 0.06 u 递减 | 280ms | – | 10-30-10 |
|
||||
| 非法起手 | 线索 `1` 格 | 同上抖动 | 280ms | reject | 15ms |
|
||||
| 剩余格变化 | 顶栏胶囊 | bump 1 → 1.14 → 1(spring) | base | – | – |
|
||||
| 提示 | 目标格 | 琥珀外描边,≈ 1.1Hz 正弦脉冲,最后 0.4s 淡出 | 2.4s | – | – |
|
||||
| 提示·已偏离 | 分叉后的段 | 覆白 45% 随脉冲 | 2.4s | – | – |
|
||||
| 提示·起手错 | 重开按钮 | 琥珀外圈脉冲 | 0.9s 循环 | – | – |
|
||||
| 过关 | 全盘 | 底色切 `primary-bright`;按步序扫白光,每格 250ms 正弦,整盘 320ms 内起完 | slow | cleared(和弦) | 20-40-30 |
|
||||
| 过关 | 胶囊 | 切 `done`(完成 + 勾) | base | – | – |
|
||||
| 过关后 | 全盘 | 停留 500ms → 淡出 160ms → 下一关进场 | hold + 160 | – | – |
|
||||
| 切屏 | 整屏 | 淡入 + 上移 8px | base | – | – |
|
||||
| Toast | toast | 淡入 + 上移 8px,停 1.8s | base | – | – |
|
||||
| 按钮按下 | 按钮 | 缩放 0.97(关卡格 0.94、主按钮 0.98) | fast | – | – |
|
||||
| 锁定关卡被点 | 关卡格 | 横向抖动 ±5px | slow | – | – |
|
||||
| 当前关 | 关卡格 | 外圈呼吸 0 → 6px 紫 16% | 2.4s 循环 | – | – |
|
||||
|
||||
### 8.3 过关时间线
|
||||
|
||||
```
|
||||
0ms cleared 事件:全盘切亮色,和弦起,胶囊变「完成」,按钮禁用
|
||||
0–320ms 按步序扫光(每格 250ms 正弦,起点先亮)
|
||||
320–820 停留
|
||||
820–980 整盘淡出(160ms)
|
||||
980 下一关:顶栏 / 进度条就地更新,新盘面错落进场(≤ 400ms)
|
||||
```
|
||||
|
||||
### 8.4 减弱动态
|
||||
|
||||
系统开启「减弱动态效果」时:CSS 全部动画与过渡关闭;Canvas 去掉进场错落、弹簧、扫光与淡出,只保留状态切换与提示描边(静态显示),错误格变红但不晃动。`BoardView` 通过 `reducedMotion` 选项接收。
|
||||
|
||||
### 8.5 性能约束
|
||||
|
||||
`BoardView.update()` 返回 `isAnimating`,宿主只在有动画时重绘;静止时 0 帧。所有动效都是无状态插值(`dt` 封顶 50ms),掉帧不会漏掉状态。
|
||||
|
||||
## 9. 声音与振动
|
||||
|
||||
| 事件 | 音效 | 振动 |
|
||||
|---|---|---|
|
||||
| 落子 | 90ms 正弦短音,C5 起每步升半音,12 步循环 | 8ms(踩对线索 18ms) |
|
||||
| 冲突 | 160ms 方波 130Hz 闷音 | 40ms |
|
||||
| 非法起手 | 60ms 三角波 220Hz | 15ms |
|
||||
| 死路 | – | 10-30-10 |
|
||||
| 过关 | C-E-G-C 三角波和弦,每音 220ms 间隔 70ms | 20-40-30 |
|
||||
|
||||
均可在选关页 chip 关闭。Cocos 侧换 4 段短音,接口同 `Sfx`。
|
||||
|
||||
## 10. 可访问性
|
||||
|
||||
- 关键文字对比度 ≥ 4.5 : 1(§2.3)。
|
||||
- 冲突发生时单格晃动;本格要求不符时有角标。过关胶囊有勾和文字,已过关卡格有勾,锁定有锁图标。
|
||||
- 所有可点元素 ≥ 44px;键盘焦点有 3px 紫描边。
|
||||
- 剩余格胶囊 `aria-live="polite"`;装饰性 SVG `aria-hidden`。
|
||||
- 遵守 `prefers-reduced-motion`(§8.4)。
|
||||
|
||||
## 11. 与其他玩法的关系
|
||||
|
||||
- **玩法三「六边一笔」**:结构与本规范一致(同一套壳、同一套四档动效),但 `theme.ts` 目前是靛蓝 `#4F5BD5` + 黄黑墙。待办:把 `--primary` 统一到 `#6558F5`(需先确认与墙黄 `#FFC826` 的对比),其余令牌直接复用本文。
|
||||
- **壳(`shared/`)**:首页选玩法、设置、通用按钮皮肤按本文 §4 / §6 的按钮、chip、toast 规格实现。
|
||||
|
||||
## 12. 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
|---|---|
|
||||
| 2026-09-16 | v1.5:去掉步号错位的 ×,错误格单独变红并晃动 280ms,进出连接线保持紫色;撤销 / 重开立即停止晃动。独立试玩版全部关卡直接可选。 |
|
||||
| 2026-09-16 | v1.4:第 n 步走到预定数字 n 以外的位置立即标红,角标显示「×」;本格要求不符仍显示原要求。补数字的约束同步生效。 |
|
||||
| 2026-09-16 | v1.3:触屏命中死区缩至 8%,任意历史格可直接回退;增加补数字提示,保留指路;底栏改为三个 15px 按钮,加常驻手势说明,toast 上移并支持换行。颜色、盘面字阶与动效令牌保持 v1.2。 |
|
||||
| 2026-09-16 | v1.2:未走线索 / 奇偶格描边改为墨色 `#23243A`(与线索数字同色),线宽 0.05 u。试玩反馈:主色描边跳脱、不简洁。 |
|
||||
| 2026-09-16 | v1.1:未走线索 / 奇偶格描边由灰 `#B4B8CC` 改为主色 `#6558F5`,线宽 0.05 u → 0.06 u(试玩反馈:灰描边不显眼)。已被 v1.2 替代。 |
|
||||
| 2026-09-15 | v1.0:建立令牌体系(色彩 / 字阶 / 间距 / 圆角 / 动效四档),CSS 与 `theme.ts` 由测试守门;字体栈加 `ui-rounded`;线索描边加深到 `#B4B8CC`;顶栏加副标题「包名 · 第 n 关」、本包进度条、剩余格胶囊(bump / 完成态);选关页加总进度条、已过关勾、当前关呼吸、列表渐隐;盘面加错落进场与过关后淡出;桥生长改为只长厚度;支持减弱动态。 |
|
||||
|
||||
待办:高对比模式(冲突改红底 + 斜纹)、暗色主题、玩法三令牌统一、Cocos 侧 Label 池化后复核字阶。
|
||||
@@ -1,108 +0,0 @@
|
||||
// Research-only audit. Uses the project's actual boards and solvers; does not edit levels.
|
||||
// Run from the repository root: node docs/research/audit-level-structure.mjs
|
||||
// Requires Node.js 24 with native TypeScript stripping and registerHooks.
|
||||
import { registerHooks } from 'node:module';
|
||||
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
const root = new URL('../../', import.meta.url);
|
||||
registerHooks({
|
||||
resolve(specifier, context, nextResolve) {
|
||||
if (specifier.startsWith('.') && context.parentURL?.endsWith('.ts') && !/\.[a-z]+$/i.test(specifier)) {
|
||||
const candidate = new URL(specifier + '.ts', context.parentURL);
|
||||
if (existsSync(candidate)) return nextResolve(candidate.href, context);
|
||||
}
|
||||
return nextResolve(specifier, context);
|
||||
},
|
||||
});
|
||||
|
||||
function bipartite(adj) {
|
||||
const colors = Array(adj.length).fill(-1);
|
||||
for (let i = 0; i < adj.length; i++) {
|
||||
if (colors[i] !== -1) continue;
|
||||
colors[i] = 0;
|
||||
const queue = [i];
|
||||
for (let k = 0; k < queue.length; k++) {
|
||||
const u = queue[k];
|
||||
for (const v of adj[u]) {
|
||||
if (colors[v] === colors[u]) return false;
|
||||
if (colors[v] === -1) { colors[v] = 1 - colors[u]; queue.push(v); }
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const levels = [];
|
||||
for (const mode of ['01-number-stroke', '03-hex-walls']) {
|
||||
const numeric = mode === '01-number-stroke';
|
||||
const loader = await import(new URL(`modes/${mode}/src/levels.ts`, root));
|
||||
const solver = await import(new URL(`modes/${mode}/src/solver.ts`, root));
|
||||
const folder = new URL(`modes/${mode}/levels/`, root);
|
||||
for (const file of readdirSync(folder).filter(f => f.endsWith('.json')).sort()) {
|
||||
const pack = loader.parsePack(JSON.parse(readFileSync(new URL(file, folder), 'utf8')));
|
||||
for (const { def, board: b } of loader.loadPack(pack)) {
|
||||
const solutions = solver.solve(b, { limit: 2 });
|
||||
const path = solutions.length === 1 ? solutions[0] : null;
|
||||
const clues = b.cells.filter(c => c.clue !== null).map(c => c.clue).sort((a,z) => a-z);
|
||||
const gaps = clues.slice(1).map((c,i) => c-clues[i]);
|
||||
if (clues.length) gaps.push(b.n-clues.at(-1));
|
||||
const branches = [];
|
||||
if (path) {
|
||||
const visited = new Set();
|
||||
for (let t = 1; t < path.length; t++) {
|
||||
visited.add(path[t-1]);
|
||||
const adj = numeric ? b.adj[path[t-1]] : b.adjAt(path[t-1],t);
|
||||
const physical = adj.filter(v => !visited.has(v));
|
||||
const immediate = physical.filter(v => {
|
||||
const c = b.cells[v], nextStep=t+1;
|
||||
if (b.clueCellOfStep.has(nextStep) && b.clueCellOfStep.get(nextStep) !== v) return false;
|
||||
if (c.clue !== null && c.clue !== nextStep) return false;
|
||||
if (numeric && c.parity && (nextStep%2===1 ? 'odd' : 'even') !== c.parity) return false;
|
||||
return true;
|
||||
});
|
||||
if (immediate.length > 1) branches.push({step:t, choices:immediate.map(v=>b.cells[v].id)});
|
||||
}
|
||||
}
|
||||
const geometry = numeric ? b.cells.map(c=>c.units.map(([x,y])=>`${x},${y}`).sort().join(';')).sort() : (()=> {
|
||||
const minQ=Math.min(...b.cells.map(c=>c.q)), minR=Math.min(...b.cells.map(c=>c.r));
|
||||
return b.cells.map(c=>`${c.q-minQ},${c.r-minR}`).sort();
|
||||
})();
|
||||
levels.push({
|
||||
mode, pack:pack.pack, title:pack.title, id:def.id, cells:b.n, clues,
|
||||
maxClueGap:gaps.length ? Math.max(...gaps) : null, hasStart:clues.includes(1),
|
||||
hasEnd:clues.includes(b.n), parityCount:numeric ? b.cells.filter(c=>c.parity).length : 0,
|
||||
bigCells:numeric ? b.cells.filter(c=>c.units.length>1).length : 0,
|
||||
shapeFields:numeric ? def.cells.filter(c=>c.shape).length : 0,
|
||||
staticWalls:numeric ? 0 : b.walls.length, spinners:numeric ? 0 : b.spinners.length,
|
||||
graphBipartite:bipartite(b.adj),
|
||||
solutionCountCappedAtTwo:solutions.length,
|
||||
prefixBranchPoints:branches.length, maxImmediateChoices:Math.max(1,...branches.map(b=>b.choices.length)),
|
||||
branchDetails:branches, solution:path?.map(v=>b.cells[v].id) ?? null,
|
||||
geometryKey:createHash('sha256').update(JSON.stringify(geometry)).digest('hex').slice(0,16),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const range=(items,key)=>[Math.min(...items.map(x=>x[key])),Math.max(...items.map(x=>x[key]))];
|
||||
const groups=[];
|
||||
for (const mode of ['01-number-stroke', '03-hex-walls']) {
|
||||
for (const pack of [...new Set(levels.filter(x=>x.mode===mode).map(x=>x.pack))]) {
|
||||
const list=levels.filter(x=>x.mode===mode && x.pack===pack);
|
||||
groups.push({mode,pack,title:list[0].title,count:list.length,cellsRange:range(list,'cells'),
|
||||
clueCountRange:[Math.min(...list.map(x=>x.clues.length)),Math.max(...list.map(x=>x.clues.length))],
|
||||
maxClueGapRange:range(list,'maxClueGap'),branchRange:range(list,'prefixBranchPoints'),
|
||||
unknownStart:list.filter(x=>!x.hasStart).map(x=>x.id),nonBipartite:list.filter(x=>!x.graphBipartite).length,
|
||||
spinnerRange:range(list,'spinners'),unique:list.filter(x=>x.solutionCountCappedAtTwo===1).length});
|
||||
}
|
||||
}
|
||||
const reused=[];
|
||||
for (const key of new Set(levels.map(x=>x.mode+':'+x.geometryKey))) {
|
||||
const list=levels.filter(x=>x.mode+':'+x.geometryKey===key);
|
||||
if (list.length>1) reused.push({mode:list[0].mode,levels:list.map(x=>x.id)});
|
||||
}
|
||||
const output={auditDate:'2026-10-14',nodeVersion:process.version,
|
||||
method:'Original board models and solve(limit=2). Branch count follows unique solution prefix, checks physical reachability and immediate exact/parity clues only; excludes unknown-start choices. Not a human difficulty score. Hex bipartite uses static-wall graph with rotating walls ignored. Geometry reuse ignores clues/walls/phases, compares translation only.',
|
||||
total:levels.length, unique:levels.filter(x=>x.solutionCountCappedAtTwo===1).length,groups,reusedGeometries:reused,levels};
|
||||
writeFileSync(new URL('level-structure-audit.json',import.meta.url),JSON.stringify(output,null,2)+'\n','utf8');
|
||||
console.log(JSON.stringify({total:output.total,unique:output.unique,groups,reusedGeometries:reused},null,2));
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,289 +0,0 @@
|
||||
# 一笔画深度调研:制作方法、难度证据与项目课程
|
||||
|
||||
日期:2026-10-14。承接 [首轮竞品调研](一笔画竞品调研与关卡编排方案.md)。本轮重点是“好关如何制作、难度如何校准、衍生玩法如何编排”,不重复扩大下载榜单。
|
||||
|
||||
研究采用三层证据:产品/创作者公开资料、原始研究、工程数据。以下明确区分**事实、推论、建议**。没有安装并逐关试玩竞品,也没有真人试玩本项目;下载规模沿用首轮 2026-10-13 的官方页面快照,不作为实时排名。
|
||||
|
||||
## 1. 决策摘要
|
||||
|
||||
1. **数字约束参照应补入 Hidato,交互与墙参照保留 Zip。** Hidato 的给定数字就是连续数链中的数值,能参考从中段、终点开始的推理;其斜邻接与非连续落子交互不直接移植。
|
||||
2. **Zip 存在人参与设计/编辑的公开证据。** 设计方明确用墙承载逻辑和视觉主题;但不能进一步断言每道题都纯手工、都遵循同一模板。
|
||||
3. **量产流程必须拆成“有解候选→唯一性→推理审题→真人校准→排关”。** 快速生成或找到答案不等于证明唯一,更不等于题好玩。
|
||||
4. **本项目当前风险更像课程跨度,而非内容太少。** 45/45 关经原有求解器重新验证唯一;结构统计提示进阶开局、奇偶组合阶段值得补桥。
|
||||
5. **下一批优先制作少量桥接与母题样题,再决定扩到 30 关。** 首轮的 30 关是候选内容规模;本轮增加先验收 6 关的前置步骤,避免一次生成整包后才发现教学断层。
|
||||
|
||||
## 2. 研究问题与取证结果
|
||||
|
||||
| 问题 | 本轮取得的证据 | 仍不知道什么 |
|
||||
|---|---|---|
|
||||
| Zip 谁在制作内容? | GMPuzzles 创作者说明参与设计/编辑,并给出作者、主题 | LinkedIn 全部题的制作方式、工具、人员分工、内部验收 |
|
||||
| 精确数字链怎样教? | Hidato 官方从可确定的中段和倒推位置切入 | 本项目滑动交互下玩家是否同样容易在脑中倒推 |
|
||||
| 怎样连续产出新题? | Linelight 与 Patrick Traynor 的公开制作方法;LYNE 每日题更新日志 | 这些方法在本项目上的产出率、审题成本 |
|
||||
| 数字题难度怎样评估? | Utrecht 难度研究使用玩家评分训练特征模型 | 本项目特征权重与不同玩家群的难度分布 |
|
||||
| 免费生成器是否可直接验收? | Ahle Numberlink README 明确不适合检查唯一性 | 外部代码与本项目规则的完整兼容性,本次不接入 |
|
||||
| 国内小游戏是否有当前热度证据? | 找到历史创作者案例与 2018 年媒体榜单线索 | 可验证的 2026 微信/抖音榜单、活跃、收入与具体同名产品身份 |
|
||||
| 当前 45 关哪里可能跨度大? | 原模型与求解器审计、正确解前缀结构统计 | 真人难度、误读、实际提示需求 |
|
||||
|
||||
## 3. 补充竞品与制作方事实
|
||||
|
||||
### 3.1 Hidato:比 Zip 更接近精确数字链
|
||||
|
||||
**事实:** 官方目标是在给定数字之间补出连续相邻数字链;说明每题唯一、可通过逻辑解决,建议不必从第一个数入手,可以倒推。官方示例先寻找中段可确定的位置,再回到其他数段。其发行方说明允许水平、垂直和对角邻接。[官方介绍](https://www.hidato.com/introduction)、[发行方玩法说明](https://images.amuniversal.com/ups/features/hidato/Hidato_HowtoPlay.pdf)
|
||||
|
||||
**分析:** 本项目数字 `c` 是第 `c` 步;Hidato 同样把数字放在连续链的精确位置。二者都可利用数值差限定区间长度。Zip 主要限定检查点经过顺序,不能提供同样的精确区间长度。
|
||||
|
||||
**迁移建议:** 教玩家先观察一个短而受限的中段,再从 `1` 实际画出整条路线。先在脑中反推,不要求新增“从中段画多条路径”的操作。当前玩法只接受从起点逐步连接,不能直接照搬 Hidato 的任意位置填数。
|
||||
|
||||
没有取得 Hidato 当前用户规模,因此它是规则和解题教学样本,不补称为下载爆款。
|
||||
|
||||
### 3.2 Zip:人工编辑、主题与墙的关系
|
||||
|
||||
**事实:** GMPuzzles 在 2025-03-18 的发布文说明帮助设计/编辑 LinkedIn 的 Zip 谜题;公开样例列有 Thomas Snyder 与主题 Trail of Logic,并说明墙可承载逻辑和视觉主题。完整路径从 `1` 出发,按数字顺序,最后落在最高数字。[制作方发布文](https://www.gmpuzzles.com/blog/2025/03/introducing-zip/)
|
||||
|
||||
**可以得出的结论:** 存在人参与设计与编辑的明确证据;美观主题与逻辑约束可一起设计。
|
||||
|
||||
**不能得出的结论:** 所有 Zip 关全手工制作、使用某个固定生成器、逐项删线索到最小、固定每周难度曲线。粉丝求解器的自述也不等于官方验收规范。
|
||||
|
||||
**项目建议:** “双翼”“三瓣”“回廊”等外观主题可以作为构图起点,但正式卡片仍要写逻辑主题,例如“先保留中心出口”。只有图像变化、没有新推理的题不作为新主题章节的核心。
|
||||
|
||||
### 3.3 LYNE:主动减少每日数量,增加类型变化
|
||||
|
||||
**事实:** Thomas Bowker 在 2024 年十周年更新中说,每日生成题的变化种类增加近四倍,减少每天题数,较难集合题数更少,并加入只在周末出现的特别集合。[开发者原始更新](https://thomasbowker.itch.io/lyne/devlog/783374/lyne-v150-tenth-anniversary-update)
|
||||
|
||||
**分析:** 这是“数量服务于内容体验”的实际产品迭代。它证明该作者做过这样的取舍,不证明减少题数普遍提高留存。
|
||||
|
||||
**项目建议:** 每日题优先一题清晰母题或少量分档题。主线负责逐步教学,每日负责回访与变化;不要每日自动投放大量未审过的题。
|
||||
|
||||
### 3.4 Nikoli Numberlink:同名品类也要核对规则
|
||||
|
||||
**事实:** Nikoli 官方说明同数成对连接、水平/垂直过格、不得交叉/分叉/重复通过同一格;该页面没有单列“覆盖全盘”的要求。[官方规则](https://www.nikoli.co.jp/en/puzzles/numberlink/)
|
||||
|
||||
**结论边界:** 不能只凭页面省略就断言传统 Numberlink 一律允许留空。可以确定的是,研究和工具存在不同规则变体,接入前需要完整对齐。
|
||||
|
||||
本项目的数字是唯一的精确步号;Numberlink 的重复数字是端点配对标签。题库与答案判定无法互换。
|
||||
|
||||
## 4. 三种可执行的制作模式
|
||||
|
||||
### 4.1 解法意图驱动:教学与章末题
|
||||
|
||||
Linelight 作者的制作分享强调解法句、少量机制组合、删去不服务谜题的元素及明确关卡用途;首轮已列出对应页码。[原始 GDC 讲稿](https://media.gdcvault.com/gdc2019/presentations/Taylor_Brett_PuzzleGameMagic.pdf)
|
||||
|
||||
建议流程:
|
||||
|
||||
`一句话解法 → 最小图骨架 → 作者路径 → 数字/条件 → 唯一性 → 推理解释 → 触摸验证`
|
||||
|
||||
本项目示例意图:“为了在第 8 步进入大格,先吃掉左侧小区域,不能把大格当作提前跨区的桥。”这里仅规定意图,没有声称具体布局已制作或唯一。
|
||||
|
||||
适用:第一大格关、第一奇偶关、第一转墙关、章末顿悟题。不要先随机生成大盘,再强行给它贴教学标签。
|
||||
|
||||
### 4.2 交互矩阵驱动:系统深挖
|
||||
|
||||
Patrick Traynor 的公开方法包括强制一种有趣交互、枚举机制对、围绕关键推断造题、探索不变量,以及在简单/复杂题之间造桥接题;发现替代解时,可以另做一题让替代解成为被迫策略。[作者原文](https://cwpat.me/misc/puzzle-level-idea-strategies/)
|
||||
|
||||
以下矩阵是本项目建议,**每项是要寻找的推理交互,不是增加新机制**:
|
||||
|
||||
| 已有要素组合 | 寻找的交互 | 作者应证明什么 |
|
||||
|---|---|---|
|
||||
| 大格 × 精确中段数 | 面积大而一步,邻边多但到达时机固定 | 错误出口会导致数段过短/过长或区域遗漏 |
|
||||
| 大格 × 空洞/区域接口 | 一次访问的枢纽需要放在正确区域顺序中 | 过早占用枢纽后,剩余覆盖确实不可能 |
|
||||
| 异形 × 精确区间 | 凹口改变邻接,视觉近路不是满足长度的路 | 共边解释清楚;不靠误读边界 |
|
||||
| 奇偶 × 大格 | 合并节点改变路线中的计步关系 | 奇偶去掉后多解;不是天然黑白属性装饰 |
|
||||
| 无 `1` × 中段数 | 中段约束反推唯一始端 | 不是“只有穷举起点才知道” |
|
||||
| 静态墙 × 中段数 | 近路被封,数字限定绕行 | 去墙后不再唯一,数字同样有作用 |
|
||||
| 转墙 × 数字区间 | 到达时刻与墙位一致 | 真实跨边使用出发前 `t`,不能偷换成落格后的墙位 |
|
||||
| 转墙 × 转墙 | 两处通路约束同一条路线 | 两个转墙各有作用,而非一个变成视觉干扰 |
|
||||
|
||||
矩阵按玩法各自制作。数字的大格/奇偶与六边的墙不在当前规则中组合。
|
||||
|
||||
### 4.3 生成候选驱动:已学技能的练习题
|
||||
|
||||
开源 Ahle Numberlink 作者明确写道:其工具不能用于检查谜题唯一性,因为求解过程建立在特定唯一解/路径条件假设上。[原始 README](https://github.com/thomasahle/numberlink#what-numberlink-is-not)
|
||||
|
||||
**迁移结论:** 即使一个工具能够快速生成或求解复杂盘面,也要另行确认它是否枚举符合本项目全部规则的第二条解。这里是在研究工具边界,没有复用外部源码或第三方题库。
|
||||
|
||||
本项目建议继续使用自己的管线:
|
||||
|
||||
`原创图形候选 → 作者解 → 线索选择 → solve(limit≥2) → 条件消融 → 母题标签 → 重复检查 → 人工审题 → 难度校准`
|
||||
|
||||
生成用于扩大候选空间;唯一性用于排除数学问题;人工审题负责判断是否能想明白。三个阶段相互不能替代。
|
||||
|
||||
### 4.4 为什么不只用图大小预测制作成本
|
||||
|
||||
2015 年研究证明,覆盖全部网格顶点的多端点 Zig-Zag Numberlink 判定问题为 NP-complete,属于 Flow Free 常见规则形式。[原始论文摘要](https://www.jstage.jst.go.jp/article/ipsjjip/23/3/23_239/_article)
|
||||
|
||||
该结论针对其特定问题族,并未证明本项目带大格/精确步号/转墙版本具有相同复杂度,也不说明某个 15 格关必然很难。工程上只宜把它作为提醒:扩规模、批量删条件和全枚举可能有最坏情况,离线制作需记录验证耗时和搜索上限。
|
||||
|
||||
超时不能标成“恰好一解”,也不能自动标成“高质量难关”;应该记为未确定,等待重新验证或重做。
|
||||
|
||||
## 5. 难度评估:研究证据如何落地
|
||||
|
||||
### 5.1 已验证研究方法,尚未验证本项目权重
|
||||
|
||||
Utrecht 的 2015 年研究在 Flow、Lazors、Move 中,将关卡特征与玩家主观难度评分结合,训练难度函数。研究摘要报告 Flow 在 1–10 难度量表中的平均预测误差低于半分;配套演示说明每个游戏需要重新建立模型。不能把其误差直接当作 NumberSwipe 的预期效果。[研究机构论文记录](https://research-portal.uu.nl/en/publications/automated-puzzle-difficulty-estimation/)
|
||||
|
||||
演示还说明,难度评分的预测结果比耗时/交互次数预测更可靠;这意味着不能把“评分可能可预测”换写成“可以精确预言玩家解题秒数”。本轮读取了演示的文本,PDF 截图服务未成功返回页面,因此不从其中图表读取额外数值。[作者所在大学演示,第 24–27 页](https://ics-websites.science.uu.nl/docs/vakken/mscip/assignments/puzzle-game-difficulty.pdf)
|
||||
|
||||
**建议:** 先使用特征档案和人工分档;取得足够重复试玩记录后再拟合简单模型。数据少时不做复杂机器学习,也不编造权重。
|
||||
|
||||
### 5.2 本项目应分开记录的四种负担
|
||||
|
||||
| 负担 | 定义 | 典型信号 | 应对 |
|
||||
|---|---|---|---|
|
||||
| 规则学习 | 不明白一步是什么、墙何时转 | 口述规则错误,重复犯同一种错误 | 小盘单概念演示与练习 |
|
||||
| 逻辑推断 | 懂规则但要安排路线与区间 | 长停顿、在关键分叉回退 | 调整线索、保留合理挑战 |
|
||||
| 读盘 | 看不清共边、数字、转轴 | 描述的邻接与真实模型不同 | 改布局/显示,不用提示补救 |
|
||||
| 操作 | 想法对但手势经常落错 | 口述路线正确、触摸执行错误 | 改触点、恢复手势、过密盘面 |
|
||||
|
||||
适合收集的关卡特征:格数、精确数字间隔、早期引导、未知起点、关键出口、即时无冲突选择点、推理依赖深度、异形/墙密度。首轮生成器分数继续只做筛选,不当难度真值。
|
||||
|
||||
### 5.3 逻辑审题的分层
|
||||
|
||||
建议先人工标记,未来如需开发再实现独立分析工具:
|
||||
|
||||
- L0:直接一步邻接、指定步号或明显端点。
|
||||
- L1:一个局部判断,例如错方向立刻不能按步数到达数字。
|
||||
- L2:联合两个局部事实,例如入口保留与一个中段数。
|
||||
- L3:跨区域/跨数段依赖,或数字步号与一处转墙联合推理。
|
||||
- L4:多处远端约束联合、多个相位,或明显依赖深试探。
|
||||
|
||||
此分层是策划语言,不是已有算法标准。同一关可以在不同玩家眼中属于不同层。正式发布不要求所有题零试探,但教学与常规题不应主要依赖漫长枚举。
|
||||
|
||||
## 6. 对现有 45 关的可复现结构审计
|
||||
|
||||
脚本:[audit-level-structure.mjs](research/audit-level-structure.mjs)。完整逐关数据:[level-structure-audit.json](research/level-structure-audit.json)。
|
||||
|
||||
执行:`node docs/research/audit-level-structure.mjs`,本次 Node v24.21.0。脚本直接加载两玩法原有 Board 与 solver,以 `limit:2` 检查解数量;只写研究 JSON,不写正式关卡或运行时代码。
|
||||
|
||||
### 6.1 指标定义与限制
|
||||
|
||||
**无即时冲突选择点 B:** 沿唯一正确解的每个前缀,在当前未走邻居中,排除下一步精确数字冲突、保留步号冲突与奇偶冲突后,仍有两个及以上候选出口的前缀位置数量。六边玩法按当前已走格数 `t` 使用 `adjAt` 判断转墙。
|
||||
|
||||
- B 不是所有玩家可能走到的分叉数;只沿正确解前缀统计。
|
||||
- B 不包含未知起点的候选数量,也不包含玩家尝试已知冲突出口的负担。
|
||||
- B 没有做未来距离、区域可达性和相位推理,因此多个候选可能很容易排除。
|
||||
- 游戏允许画错;“无即时冲突”不是“引擎禁止画的出口”。
|
||||
- B=0 不代表没有教学、读盘或操作价值,更不代表该关必然没有难度。
|
||||
|
||||
最大数字间隔 G:按数值排序的相邻精确线索差,并计入最后线索到 `N` 的尾段;不计起点到第一个线索的未知段,不计奇偶信息。奇偶包不能只凭 G 排序。
|
||||
|
||||
### 6.2 包级结果
|
||||
|
||||
| 包 | 关数 | 格数 | 数字数 | G 范围 | B 范围 | 唯一性复验 |
|
||||
|---|---:|---|---|---|---|---|
|
||||
| 数字·入门 | 10 | 3–14 | 1–3 | 2–8 | 0–3 | 10/10 |
|
||||
| 数字·进阶 | 10 | 14–21 | 3–4 | 7–16 | 4–10 | 10/10 |
|
||||
| 数字·奇偶 | 6 | 4–18 | 1–2 | 3–17 | 0–8 | 6/6 |
|
||||
| 六边·教学 | 10 | 3–19 | 1–3 | 2–10 | 0–9 | 10/10 |
|
||||
| 六边·转墙 | 9 | 7–19 | 2–3 | 4–11 | 1–10 | 9/9 |
|
||||
|
||||
本次 **45/45 恰好一解**。只重跑求解与结构统计;没有重新执行完整 `npm run check`,也没有在本脚本中重新执行逐条奇偶、墙和转墙消融。
|
||||
|
||||
### 6.3 值得优先观察的四个衔接
|
||||
|
||||
| 衔接 | 结构变化 | 推论 | 下一步 |
|
||||
|---|---|---|---|
|
||||
| 数字 1-10→2-1 | 14→16 格;B 3→8;2→3 大格;开始含 2 个 shape 字段 | 课程跨度不只是增加 2 格,还新增异形读盘与更多无即时冲突选择 | 制作“异形单独辨认”“多出口与区间结合”两道桥接样题 |
|
||||
| 奇偶 3-3→3-4 | 11→16 格;B 1→7;1→2 奇偶;加入异形组合 | 新条件与路线选择同时增长,值得独立检查 | 补一题中盘单奇偶,再补双奇偶协作题 |
|
||||
| 六边 1-9→1-10 | 15→19 格;B 6→9 | 章末题明显扩展覆盖与选择,可能是合理小考 | 测是否能口述区域顺序,保留挑战定位 |
|
||||
| 转墙 2-3→2-4 | 12→7 格;B 5→2;转墙 1→2 | 小盘降低部分结构负担,但相位对象增加,不能只凭格数称简单 | 观察两转墙是否各自理解、视觉是否干扰 |
|
||||
|
||||
上述只是假设定位,不是认定具体关卡有缺陷。也不对正式关 id 重排,避免存档映射受影响。
|
||||
|
||||
### 6.4 奇偶迁移的实证边界
|
||||
|
||||
数字进阶 **10/10**、奇偶 **6/6** 的实际拼块邻接图都不是二分图;数字教学也有 4 关不是二分图。大格合并能产生与普通黑白单格图不同的邻接关系。
|
||||
|
||||
因此,普通四邻接单格上“每步黑白交替”的结论不能原封不动迁移。奇偶必须在真实拼块图上按路径步数验证。六边图也经常含奇环;转墙的二分性统计使用固定墙后、忽略转墙阻挡的邻接图,不是在宣称每个相位图有相同性质。
|
||||
|
||||
### 6.5 复用几何不等于重复谜题
|
||||
|
||||
审计找到五组数字几何复用:`1-4/3-1`、`1-8/3-3`、`2-1/3-4`、`2-3/3-5`、`2-6/3-6`。六边也复用了若干聚落形状。
|
||||
|
||||
本次几何键只比较平移规范化后的格形/坐标,不比较数字、墙、相位,也没有旋转/镜像去重或图同构判断。因此这里只能证明盘形相同,不能判定题目等价。
|
||||
|
||||
**建议的四层去重:** 外观几何、实际邻接图、约束配置、关键解法。两题若几何相同而推理不同,可以用于教玩家摆脱“记图形答案”;若四层都等价,只能按明确的强化用途少量保留。
|
||||
|
||||
## 7. 新关制作与编排的具体方案
|
||||
|
||||
### 7.1 先制作 6 关验证样题
|
||||
|
||||
每关仍是设计意图,未制作坐标或证明唯一。
|
||||
|
||||
| 候选 | 玩法 | 前置技能 | 单一目标 | 建议盘面 | 应避免 |
|
||||
|---|---|---|---|---|---|
|
||||
| N-A | 数字 | 大格一步、共边 | 首次清楚读懂 L 形共边 | 8–12 格;一个异形、早期引导数字 | 同时无 `1` 或引入多个复杂异形 |
|
||||
| N-B | 数字 | N-A、精确区间 | 异形出口影响中段到达步数 | 10–14 格;一个主要关键分叉 | 用长蛇形通路代替推理 |
|
||||
| P-A | 奇偶 | 单奇偶、大格计步 | 中盘里凭一条奇偶排除方向 | 10–13 格;一个奇偶、少量数字 | 奇偶成为天然属性而不起作用 |
|
||||
| P-B | 奇偶 | P-A | 两个奇偶约束不同数段 | 12–15 格;两条件各必要 | 同时扩大到 18 格、叠多个异形 |
|
||||
| H-A | 六边 | 静态墙、中段数 | 留住一片未走区域入口 | 10–13 格;有效静态墙 | 靠过密墙强制成一条走廊 |
|
||||
| S-A | 转墙 | 已理解单墙旋转与撤销 | 两转墙分别约束前后两段 | 9–12 格;两转墙相对分开 | 无数字锚点、紧贴成视觉团块 |
|
||||
|
||||
这六关先验证教学方法,之后再推进首轮的数字 12、静态六边 8、转墙 10 关候选池。不能把桥接关加到总数之外而不更新内容预算;建议它们计入对应候选池。
|
||||
|
||||
### 7.2 用技能依赖图确定章序
|
||||
|
||||
数字课程:
|
||||
|
||||
`邻接与续画 → 全覆盖/纠错 → 大格一步 → 中段精确数 → 大格出口/区域顺序 → 异形读盘 → 异形×区间 → 奇偶单条件 → 奇偶协作`
|
||||
|
||||
“未知起点”单独依赖覆盖、精确中段数和端点推理,不必强制放到所有奇偶之后。先小盘专项,再决定放入主线或选修。
|
||||
|
||||
六边课程:
|
||||
|
||||
`六向共边 → 墙在边上 → 中段数与绕行 → 区域入口 → 单转墙更新顺序 → 撤销恢复 → 数字锚定相位 → 两转墙联合`
|
||||
|
||||
主线章序应匹配依赖,章内再用“展示→应用→误区辨析→调剂→组合→收束”。不把每个维度都同步递增,也不保证每关比上一关难。
|
||||
|
||||
### 7.3 学习测试与难度校准要用不同关序
|
||||
|
||||
**教学测试:** 按正式课程顺序播放,观察理解何时形成。重点在第一次大格、异形、奇偶与转墙。
|
||||
|
||||
**难度校准:** 先让玩家掌握对应规则,再测试候选题。部分题顺序随机/交错,避免把学习熟练误判为题更简单;相同盘形题尽量不紧挨,避免记忆污染。
|
||||
|
||||
小规模 3–5 人用于发现问题;下一阶段可把 24–30 道代表题分给更多已学会规则的玩家,让每道题获得多次独立观察。人数与重复次数是项目采样建议,没有宣称达到统计精度保证。
|
||||
|
||||
同时收集主观难度(1–5 或 1–10,二选一固定)、思考时间、画线时间、回退、提示类型、首次错误位置、规则误读和口述理由。使用中位数与离散范围看整体,不将提示后的短时间混成无辅助解题时间,也不只保留通关者。
|
||||
|
||||
### 7.4 正式入库的四道门
|
||||
|
||||
| 门 | 必须确认 | 不合格时怎样做 |
|
||||
|---|---|---|
|
||||
| 数学正确 | 原规则求解,恰好一解;未知起点检查所有起点 | 重配盘面/线索,超时记未确定 |
|
||||
| 机制有效 | 奇偶、墙、转墙按现有要求消融;数字注明保留理由 | 删除装饰条件或重造关键作用 |
|
||||
| 推理可解释 | 解法句、关键分叉、错误路线排除理由 | 简化骨架/补教学,不用广告提示兜底 |
|
||||
| 课程和触摸合格 | 前置技能、实际读盘、错误恢复、章内节奏 | 补桥接、改显示、换关位 |
|
||||
|
||||
转墙静态替代检查要准确表述:当前代码拒绝“替换后仍恰好唯一”的替代,替换后无解也无法作为有效替代;不是宣称每种冻结位置都一定产生两解以上。静态冗余墙目前是报告项,不能说成现有校验器必然拒绝。
|
||||
|
||||
## 8. 国内小游戏证据:历史可用,当前排名空缺
|
||||
|
||||
**第一方历史案例:** 创作者花叔的文章介绍“一笔一连”,采用随机走出一条路线形成有解布局,并坦言仅凭格数、起终点距离没有得到满意的自动难度标准,最终有人为选难题。页面注明原始发表 2018-11-27,更新显示 2019-08-07。[创作者记录,发布于腾讯云社区](https://developer.cloud.tencent.com/article/1373799)
|
||||
|
||||
该案例支持“先有解路线、再筛难度”的历史实践,不能证明唯一性,也不能作为腾讯官方研发方案或现行平台接口能力。
|
||||
|
||||
**二手历史线索:** 2018-11-05 的 GameRes/游戏陀螺报道转述阿拉丁榜单,把《最强连一连》《一笔画完》列入历史增长讨论;页面月度表述还存在不一致,本轮不据此给出精确名次或活跃。[历史报道](https://www.gameres.com/826873.html)
|
||||
|
||||
没有查到可验证的 2026 年同类微信/抖音排名。本轮不把搜索聚合页的标签热度、同名下载站或旧报道变成“当前爆款”证据。研究得到的是历史传播与制作线索,近期策略仍以当前玩法测试为依据。
|
||||
|
||||
若后续另做国内市场专题,应取得具体产品入口/主体、带日期的同平台榜单、真实试玩与短视频素材,再按覆盖格/覆盖边/多路径分类。否则很容易把不同版本的“一笔画完”当作一个产品。
|
||||
|
||||
## 9. 来源与结论核验表
|
||||
|
||||
| 来源 | 日期/性质 | 支持的结论 | 不支持的结论 |
|
||||
|---|---|---|---|
|
||||
| [GMPuzzles Zip 发布文](https://www.gmpuzzles.com/blog/2025/03/introducing-zip/) | 2025-03-18,设计参与方 | 有人参与设计编辑;墙承载主题;起终点规则 | 所有题纯手工、完整内部标准 |
|
||||
| [Hidato 官方介绍](https://www.hidato.com/introduction) | 页面无明确发布日期,官方说明 | 精确连续数链、中段与倒推技巧 | 当前活跃/下载规模 |
|
||||
| [Hidato 发行方玩法](https://images.amuniversal.com/ups/features/hidato/Hidato_HowtoPlay.pdf) | 发行资料 | 含对角邻接 | 当前项目也应加斜走 |
|
||||
| [LYNE 十周年更新](https://thomasbowker.itch.io/lyne/devlog/783374/lyne-v150-tenth-anniversary-update) | 2024-08,开发者 | 增加每日类型变化、减少每日数量 | 已证明提高留存 |
|
||||
| [Patrick 的关卡创意方法](https://cwpat.me/misc/puzzle-level-idea-strategies/) | 2022-07-14,创作者 | 交互、推断、不变量与桥接等制作方法 | 一笔画量产算法或难度模型 |
|
||||
| [Linelight GDC 分享](https://media.gdcvault.com/gdc2019/presentations/Taylor_Brett_PuzzleGameMagic.pdf) | 2019,创作者演示 | 解法句、精简与关卡用途 | 本项目特定参数 |
|
||||
| [Utrecht 论文记录](https://research-portal.uu.nl/en/publications/automated-puzzle-difficulty-estimation/) | 2015,同行评审研究记录 | 玩家评分校准特征模型 | 本项目预测误差 |
|
||||
| [Utrecht 配套演示](https://ics-websites.science.uu.nl/docs/vakken/mscip/assignments/puzzle-game-difficulty.pdf) | 研究者演示 | 评级方法和适用限制;已读文本 | 未成功截图的图表数值 |
|
||||
| [Ahle Numberlink](https://github.com/thomasahle/numberlink#what-numberlink-is-not) | 作者维护的源码说明 | 求解工具并非通用唯一性证明器 | 所有 Numberlink 生成器都不可靠 |
|
||||
| [Zig-Zag Numberlink 论文](https://www.jstage.jst.go.jp/article/ipsjjip/23/3/23_239/_article) | 2015,原始学术论文 | 特定覆盖型多路径问题 NP-complete | 所有变体同样复杂、某具体题必难 |
|
||||
| [Nikoli Numberlink](https://www.nikoli.co.jp/en/puzzles/numberlink/) | 原出版方规则 | 配对、横竖路径与禁止重复/交叉 | 页面省略即证明所有版本不要求覆盖 |
|
||||
| [花叔创作者记录](https://developer.cloud.tencent.com/article/1373799) | 2018 原文/2019 更新,历史第一方经验 | 有解路线生成与人工难度筛选 | 2026 平台现状、用户规模、唯一性 |
|
||||
| [GameRes 历史报道](https://www.gameres.com/826873.html) | 2018-11-05,二手媒体 | 当时曾有相关小游戏增长报道 | 当前榜单、同名产品身份、精确市场规模 |
|
||||
| [本次原模型审计](research/level-structure-audit.json) | 2026-10-14,可复现计算 | 45/45 唯一与结构指标 | 真人难度、完整测试、条件消融全部通过 |
|
||||
|
||||
本轮交付为深度研究文档、可复现审计脚本与逐关 JSON;正式关卡和运行时代码保持原样。六道样题与三十关候选计划均尚待制作、验收、试玩。
|
||||
@@ -1,298 +0,0 @@
|
||||
# 一笔画益智游戏:竞品调研与关卡制作编排方案
|
||||
|
||||
调研日期:2026-10-13。对象:NumberSwipe 当前数字一笔、六边一笔与转墙实验玩法。
|
||||
|
||||
2026-10-14 补充:[深度调研:制作与难度证据](一笔画深度调研-制作与难度证据.md)加入 Hidato 精确数字链、Zip 设计参与方、LYNE 迭代、研究方法及本项目 45 关的可复现结构审计。数学约束对照补看 Hidato,交互对照保留 Zip;首轮 30 关候选计划先以其中 6 关样题验证课程衔接。
|
||||
|
||||
本文用于内容策划决策,不修改玩法规则或正式关卡。先核对工程设计与关卡数据,再查阅官方商店、开发者网站与公开设计分享。**竞品公开事实、分析判断、本项目建议分别标明;未实际安装并逐关试玩竞品,不能据此宣称完成其全关卡难度测评。**
|
||||
|
||||
## 1. 核心判断
|
||||
|
||||
本项目最有价值的内容方向是:**精确步数约束下的大格出口推理,以及六边形上由墙和转墙形成的通路推理。** 两者可以继续在现有规则内产出不同的顿悟,近期优先扩充原创推理母题,而不是不断追加机构。
|
||||
|
||||
优先参考顺序为:
|
||||
|
||||
1. **Zip**:覆盖全盘、数字路径、墙、撤回提示,最接近当前体验;必须区分数字语义。
|
||||
2. **Fill**:单路径填满盘面,适合参考基础读盘和碎片时间体验。
|
||||
3. **Flow Free / Hexes**:成熟的内容分包与几何衍生路线,适合参考产品内容组织。
|
||||
4. **1LINE 两款**:单手操作、分级关卡与每日挑战;边覆盖类规则不直接移植。
|
||||
5. **Bridges / Warps / LYNE**:了解路径谜题如何扩展可能性;用于独立玩法储备。
|
||||
6. **Linelight 的开发者分享**:参考手工谜题制作方法,不作为同规则竞品。
|
||||
|
||||
不能从累计下载推断当前活跃、收入、留存或在中国小游戏平台的热度。以下样本以可核验的国际产品为主;未取得微信、抖音小游戏榜单和平台后台数据。
|
||||
|
||||
## 2. 当前项目基线
|
||||
|
||||
依据 [产品设计](PRODUCT.md)、[现有关卡规范](关卡设计规范.md)、两玩法 README/GDD、类型定义、生成器与校验器,并直接统计五个关卡 JSON:
|
||||
|
||||
| 模块 | 当前内容 | 玩家推理对象 | 现有边界 |
|
||||
|---|---:|---|---|
|
||||
| 数字一笔·入门 | 10 关,3–14 格 | 四向邻接、覆盖全盘、中段数字、大格一步、空洞 | 每块只走一次;数字是精确步号 |
|
||||
| 数字一笔·进阶 | 10 关,14–21 格 | 多出口大格、长条、异形、数字区间、未知起点 | 首个无 `1` 关为 2-8 |
|
||||
| 数字一笔·奇与偶 | 6 关,4–18 格 | 奇偶与数字共同筛选路线 | 每条奇偶条件必须参与唯一性 |
|
||||
| 六边一笔·教学 | 10 关,3–19 格 | 六向邻接、边墙、区域与入口 | 无矩形大格;墙在边上,不占格 |
|
||||
| 六边一笔·转墙 | 9 关,7–19 格 | 通路随步数变化,路线与相位耦合 | 实验机制;最多 4 转墙是当前规模 |
|
||||
|
||||
合计 **45 关**。两个独立玩法分别 26 关与 19 关;不是 45 关必须混排的统一课程。
|
||||
|
||||
共同目标:每格恰好一次、完整解唯一、可随时改错、手机竖屏、离线、无计时/体力/分数。路径可以松手续画,“一笔”约束的是路径结构。旧 GDD 的 44/32 关规划不是实际内容量。
|
||||
|
||||
转墙按步数转动:已落格 `t` 次时,位置为 `(phase+t) mod 3`;跨边先判断当前墙位,落格再转,起手也转,撤销恢复。这是可停下来思考的离散状态,不是等待现实时间的机关。
|
||||
|
||||
## 3. 代表性产品与受欢迎依据
|
||||
|
||||
下载档位与评分为调研时官方页面所显示的累计数据;评分可能受地区、设备和抓取时点影响。Flow 家族不因共享品牌而自动拥有相同用户规模。
|
||||
|
||||
| 产品 / 开发者 | 公开规模或口碑信号 | 玩法与公开内容结构 | 对项目的参考价值 |
|
||||
|---|---|---|---|
|
||||
| **1LINE – One Line with One Touch / X-FLOW** | Google Play 5,000 万+下载,4.4 分 | 一次触摸连接点;关卡包、每日挑战、提示 | 单手入口与常规包+每日内容;具体内部排关算法未公开。[官方商店](https://play.google.com/store/apps/details?hl=en_US&id=com.one1line.onetouch.onestroke.dotgame) |
|
||||
| **1LINE – one-stroke puzzle game / Gamincat** | Google Play 1,000 万+下载,4.6 分 | 官方列 10 个等级、500 道题;从简单形状增加线条,加入单向线、重叠线 | 看基础题如何为特殊边规则铺垫;与 X-FLOW 是不同产品。[官方商店](https://play.google.com/store/apps/details?hl=en_US&id=com.gamestart.oneline) |
|
||||
| **Fill – one-line puzzle game / Gamincat** | Google Play 500 万+下载,3.8 分 | 用一条线填满方块;4 档难度、2,000 题、提示 | 最直接的覆盖型参照;大量题库不能自动保证内容变化。[官方商店](https://play.google.com/store/apps/details/?hl=en&id=com.gamestart.fill) |
|
||||
| **Zip / LinkedIn** | 官方持续每日供题;本次没有独立用户规模数据 | 一条线覆盖每格,依次经过数字,不能穿墙;每日一题、撤回、提示、连续记录 | 数字路径与纠错体验最接近;列入相关性样本,不列作已验证下载爆款。[官方规则](https://www.linkedin.com/help/linkedin/answer/a7445030) |
|
||||
| **Flow Free / Big Duck Games** | Google Play 1 亿+下载,4.7 分 | 多条线连接同色端点并填满盘;商店列 2,500+免费题、自由与计时模式 | 分包、规模与主题内容参照;多路径区域分配不能直接当单路径规则。[官方商店](https://play.google.com/store/apps/details?hl=en_US&id=com.bigduckgames.flow) |
|
||||
| **Flow Free: Hexes** | Google Play 500 万+下载,页面顶部 4.7 分 | 同色连接覆盖全盘,改为六边形;2,500+免费题、每日题 | 更换几何可独立形成内容系列;适合比较六向读盘。[官方商店](https://play.google.com/store/apps/details?id=com.bigduckgames.flowhexes) |
|
||||
| **Flow Free: Bridges** | Google Play 1,000 万+下载,4.7 分 | 桥允许两条管线交叉;2,500+免费题 | 一个明确原语改变路径分配;需另建模型,不能直接加进当前 V1。[官方商店](https://play.google.com/store/apps/details?id=com.bigduckgames.flowbridges) |
|
||||
| **Flow Free: Warps** | Google Play 100 万+下载,4.7 分 | 通过 warp 跨盘连接;2,500+免费题、每日题 | 非局部通路的衍生方式;与本项目“笔尖跟随手指”的边界冲突。[官方商店](https://play.google.com/store/apps/details?id=com.bigduckgames.flowwarps) |
|
||||
| **LYNE / Thomas Bowker** | Steam 英文评价页显示 95% 好评、3,544 条评价 | 连接形状、填满盘面;数百预制题与每日程序生成题 | 固定课程与生成题分开供给的参考;不是单路径精确步号谜题。[官方商店](https://store.steampowered.com/app/266010?curator_clanid=11538571) |
|
||||
|
||||
辅助样本 **Linelight**:沿线移动解谜,官方列 6 个世界、200+谜题;不属于“一次画线覆盖所有格”的直接竞品,但其作者公开了制作方法,适合研究关卡编排。[发行商介绍](https://www.nintendo.com/US/store/products/linelight-switch/)
|
||||
|
||||
### 3.1 必须分清的四种玩法
|
||||
|
||||
| 类型 | 核心目标 | 典型推理 | 本项目关系 |
|
||||
|---|---|---|---|
|
||||
| 单路径覆盖格子 | 一条路径访问所有格 | 端点、出口、剩余区域连通 | 本项目、Fill;Zip 加顺序检查点 |
|
||||
| 沿已有线条的一笔画 | 在图形网络中完成连接/描线任务 | 起点选择、边的方向与可重复次数 | 1LINE 图形题的参考方向;规则以各产品具体题型为准 |
|
||||
| 多路径覆盖 | 同色端点配对且共同占满盘 | 不同路径争夺通道、区域分配 | Flow 系列,属于相邻品类 |
|
||||
| 沿线移动解机关 | 在通路上移动并改变状态 | 状态、开关、运动关系 | Linelight,借鉴制作方法 |
|
||||
|
||||
不要把“点”“边”“格”当成同一个覆盖对象。即使两款游戏都被称为一笔画,也不能共用关卡合格标准。
|
||||
|
||||
### 3.2 Zip 与数字一笔的关键差异
|
||||
|
||||
**公开事实:** Zip 要求按数字顺序经过检查点并填满全盘;规则没有要求标有 `4` 的格就是整条路线的第四格。[Zip 官方规则](https://www.linkedin.com/help/linkedin/answer/a7445030)
|
||||
|
||||
**项目事实:** 数字一笔中 `4` 必须在第 4 步进入。假设两个数字是 `4` 和 `9`,中间恰好有 **5 次移动、4 个中间格**;整个大拼块也只占其中一步。
|
||||
|
||||
**分析判断:** 精确步号提供了额外的路线长度信息,因此“大格在视觉上很大,但只耗一步”“线索靠得近,却必须远绕”能成为本项目的辨识点。Zip 更适合参考覆盖、墙与撤回体验,不能把其检查点分布原样转成精确步号关。
|
||||
|
||||
## 4. 已公开的内容组织与制作经验
|
||||
|
||||
下面收集的是公开结构或开发者陈述。除明确注明外,**没有证据证明这些游戏采用本项目的唯一解验收、难度公式或关卡生成算法。**
|
||||
|
||||
### 4.1 分级题库:1LINE 与 Fill
|
||||
|
||||
Gamincat 的 1LINE 公开了“简单形状→更多线条→特殊线”的渐进方向;Fill 明确分为四档难度。可借鉴的是分级入口与基础技能铺垫,不是照搬 500/2,000 的题量目标。[1LINE 说明](https://play.google.com/store/apps/details?hl=en_US&id=com.gamestart.oneline)、[Fill 说明](https://play.google.com/store/apps/details/?hl=en&id=com.gamestart.fill)
|
||||
|
||||
**项目建议:** 数字玩法按“出口、精确步数、起点、奇偶”组织技能课题,每包提供可识别的推理目标;只把格数变大容易形成同质题。
|
||||
|
||||
### 4.2 尺寸包与同尺寸深挖:Flow Free
|
||||
|
||||
开发者早期公开过按 5×5–9×9、10×10–14×14 分组,以及同尺寸的 8×8 Mania 包;这说明同一规则可同时提供渐进尺寸与固定尺寸的题库。它是历史案例,不能当作当前全部包结构。[开发者历史更新](https://www.bigduckgames.com/blog)
|
||||
|
||||
**项目建议:** 保留当前较小盘面,在相似格数下更换关键推理;例如同为 14 格,一题考大格出口,一题考剩余区域入口,一题考中段步数。内部按格数筛题,对玩家按推理主题命名。
|
||||
|
||||
### 4.3 每个衍生产品改变一个主要维度:Flow 家族
|
||||
|
||||
公开产品分别突出六边形、桥交叉、跨盘 warp,并保留同色连接与覆盖目标。[Hexes](https://www.bigduckgames.com/hexes)、[Bridges](https://www.bigduckgames.com/bridges)、[Warps](https://www.bigduckgames.com/warps)
|
||||
|
||||
**分析判断:** 共同操作与单个核心变化有利于形成清晰产品认知;本项目的矩形大格和六边边墙独立成玩法符合这种思路。近期应把各自机制做深。
|
||||
|
||||
### 4.4 每日内容:Zip 与 LYNE
|
||||
|
||||
Zip 官方每天提供新题,旧题在更新后不可继续玩;LYNE 官方列出了每日程序生成谜题。这是两种不同供题方式,不能由“每日题”反推出自动生成或人工制作。[Zip 帮助](https://www.linkedin.com/help/linkedin/answer/a7445030)、[LYNE 商店](https://store.steampowered.com/app/266010?curator_clanid=11538571)
|
||||
|
||||
**项目建议:** 先完善主线,日后以离线预置、已经审题的题库做每日题;跨玩家同步题、日期处理、历史题和存档需要另行设计。每日题不强制计时,也不作为教学关的替代。
|
||||
|
||||
### 4.5 手工谜题制作:Linelight 作者的公开方法
|
||||
|
||||
Brett Taylor 的 GDC 分享强调:先探索少量机制的组合;用一句话描述关键解法,再构造体现它的简洁谜题;删掉不服务解法的元素;每关必须有教学、强化、独特发现或节奏调剂等作用;作者熟练度不能代表新玩家水平。见 PDF **第 30–33、37、41–45、73–77 页**。[《Puzzle Game Magic Secrets》原始讲稿](https://media.gdcvault.com/gdc2019/presentations/Taylor_Brett_PuzzleGameMagic.pdf)
|
||||
|
||||
**项目建议:** 每关先写“解法句”。例如:“先绕过 2×2,从另一侧回来,才会在第 8 步踩中它。”必须能画出并验证具体盘面后才能入库。没有新认知、没有强化作用、也不承担节奏调剂的题,不靠换皮保留。
|
||||
|
||||
## 5. 适合当前规则的原创关卡母题
|
||||
|
||||
以下是**策划蓝图**,尚未制作坐标与线索,也未证明具体关卡唯一。不是竞品题库提取结果。
|
||||
|
||||
| 母题 | 玩家需要发现什么 | 制作方式 | 验收重点 |
|
||||
|---|---|---|---|
|
||||
| 近在眼前、必须远绕 | 空间邻近不等于步号相近 | 两个线索在图上靠近,但步号差明显大于最短路;大格/区域决定绕行 | 错路可用步数排除;不要只留下长蛇形通道 |
|
||||
| 枢纽留到后面 | 大格一次访问,不能把它当反复经过的交叉口 | 大格连接多片区域,过早用掉会遗留未走格 | 真正有竞争出口;大格不是装饰 |
|
||||
| 长条从哪边进 | 一步大格的进出方向影响后续覆盖 | 长条连接上下或左右的邻居,某组进出造成封区 | 共边清楚;不依赖细小触点 |
|
||||
| 凹口不是捷径 | 必须识别异形拼块真实共边 | L/T/S 的凹口制造可见但有约束的邻接 | 碰角与共边可辨;不靠误读图形制造难度 |
|
||||
| 最后一口先留好 | 先保留未走区域的唯一入口 | 在静态图里设置狭窄接口,过早消耗节点/通道导致剩余区域不可达 | 关键判断可在分叉处解释 |
|
||||
| 从终点反推 | 开局从末段倒推比从 `1` 试更有效 | 用终点线索与低度数邻接形成必经尾段,再约束前段 | 提供明确终点信息;不是题后才知道终点 |
|
||||
| 找起点 | 起点由覆盖和中段线索共同约束 | 不给 `1`;先用较小盘与清楚端部做教学,再加入大格 | 检查所有起点;不能靠逐个试起点 |
|
||||
| 奇偶二选一 | 两条直觉可行路线只有一条满足步号奇偶 | 用大格/异形改变图上的步数关系,奇偶筛掉另一条路线 | 删除该奇偶条件后确实多解 |
|
||||
| 墙封假近路 | 共边存在,但某条边不能跨 | 在线索邻边或区域边界放有效墙 | 去墙后不再唯一;不以墙数量替代质量 |
|
||||
| 六向出口管理 | 六边邻居多,路线仍必须安排区域先后 | 在边墙与中段数字约束下保留多个可见选择 | 有可解释的排除;不要用大量墙退化成单走廊 |
|
||||
| 到达拍子 | 经过相同位置的时机影响墙是否挡路 | 单转墙靠近关键出口,用早期数字锚定到达步数 | 用出发前相位计算;无需快滑或等待 |
|
||||
| 两处相位约束 | 同一条路线必须同时满足两个门口的时机 | 两个相距较开的转墙,一处约束前段,一处约束后段 | 两者各有必要性;不能改为静态墙替代 |
|
||||
|
||||
注意三处数学边界:
|
||||
|
||||
- “线索区间”按数字从小到大排列,不是指空间相邻。`4→9` 的路径长度固定;数字语义与 Zip 不同。
|
||||
- 普通四邻接单格图的黑白奇偶规律,**不能直接套到合并大格、异形或六边形图**。先检查实际邻接图是否为二分图,再决定是否能用交替颜色推理。
|
||||
- 静态墙关的连通/入口推理适用于固定图;转墙是时间随步号变化的图,不能用某一拍的静态连通性直接判定后续必死。未来墙位与路线步数都要考虑。
|
||||
|
||||
## 6. 关卡编排方案
|
||||
|
||||
以下关数、难度和时长为**待试玩验证的项目建议**,不代表竞品实际排关参数。
|
||||
|
||||
### 6.1 通用 10 关章节节拍
|
||||
|
||||
| 包内关位 | 作用 | 认知负担与变化 |
|
||||
|---|---|---|
|
||||
| 1 | 看懂主题 | 小盘、明显线索,只展示主题原理 |
|
||||
| 2 | 独立用一次 | 改布局,仍只有一个关键选择 |
|
||||
| 3 | 识别直觉误区 | 相似外观但错误策略能明确排除 |
|
||||
| 4 | 第二种应用 | 同主题改变入口或出口,避免只记形状 |
|
||||
| 5 | 阶段练习 | 两个已学判断串联 |
|
||||
| 6 | 短关调剂 | 缩小盘面,给一次快速完成的熟练感 |
|
||||
| 7 | 与旧机制组合 | 只组合已教学内容,增加一个主要判断 |
|
||||
| 8 | 换推理方向 | 从终点、区域或另一个线索出发 |
|
||||
| 9 | 综合挑战 | 较长区间或两处关键约束,仍能逐步解释 |
|
||||
| 10 | 章末顿悟 | 一个突出解法收束主题,难度不必最高 |
|
||||
|
||||
原则:**章间总体抬升,章内允许回落;新规则第一关回到小盘。** 小盘可以有深推理,格子多也可以很容易。调剂关承担明确节奏用途,不能连续堆放等价题。
|
||||
|
||||
### 6.2 数字一笔:现有课程评审建议
|
||||
|
||||
| 现状 | 建议处理 | 需要验证的证据 |
|
||||
|---|---|---|
|
||||
| 1-4 首次教大格 | 保留为关键教学关,1-8 再教出口 | 玩家能说出“整块一步”,不是把块内面积计成步数 |
|
||||
| 1-6 空洞后,1-7 进入多处分叉 | 先测梯度;若断层,用更小的新题补桥 | 首次错误位置、是否理解空洞邻接、是否需要盲试 |
|
||||
| 2-1 起引入异形,2-2 已出现 3×3 | 检查是否同时增加读盘与步数负担;必要时先单独熟悉异形 | 玩家是否知道凹口边界,还是规则懂但触控误走 |
|
||||
| 2-8 第一次无 `1`,同时已有较复杂盘面 | 若试玩困难,先提供一题小盘起点推理,再进入现关 | 起点能否口述推出,而非逐个点试 |
|
||||
| 奇偶包从小盘开始,后段已有组合 | 保留回落开局;增加中等规模桥接题 | 会否将“奇偶”误读为数字本身或占地面积 |
|
||||
|
||||
以上是文档与数据结构分析,不是认定这些关已经有体验问题。未经试玩不直接重排正式关卡 id,避免进度记录错位。
|
||||
|
||||
### 6.3 六边一笔:静态墙扩展包,8 关蓝图
|
||||
|
||||
目标:从“看到墙就绕路”提升为“借墙判断区域顺序”。保留已有教学 10 关。
|
||||
|
||||
| 新包关位 | 母题 | 建议格数 | 主要变量 |
|
||||
|---|---|---:|---|
|
||||
| 1 | 墙封假近路复习 | 7–9 | 1 堵墙、起点与早期数字 |
|
||||
| 2 | 中段数字决定绕向 | 9–11 | 调整线索位置,不同时扩大盘面 |
|
||||
| 3 | 留住区域入口 | 10–12 | 一个关键接口,墙参与封区 |
|
||||
| 4 | 从终点倒推尾段 | 10–12 | 明确终点,起点仍给 `1` |
|
||||
| 5 | 短盘顿悟 | 7–10 | 六向分叉与单个关键约束 |
|
||||
| 6 | 空洞与墙分工 | 12–15 | 空洞改变盘形,墙控制跨边 |
|
||||
| 7 | 两个区域的先后 | 14–17 | 两个可解释的判断 |
|
||||
| 8 | 综合收束 | 15–19 | 回用区域入口+中段数字 |
|
||||
|
||||
不要求墙数逐关递增。允许一堵关键墙比四堵普通墙更难。无起点属于另一次教学,不在本包同时引入。
|
||||
|
||||
### 6.4 转墙:正式教学候选包,10 关蓝图
|
||||
|
||||
现有 9 关实验包用于观察机制潜力;正式教学候选先控制在 **1–2 个转墙**,再决定是否保留 3–4 个转墙挑战。
|
||||
|
||||
| 关位 | 教学/推理重点 | 建议规模 |
|
||||
|---|---|---|
|
||||
| 1 | 起手也转,先跨边判断再落格转动 | 5–7 格,1 转墙 |
|
||||
| 2 | 撤销会复原相位 | 6–8 格,1 转墙;演示后撤销重走 |
|
||||
| 3 | 到达第 4 步时看当前墙位 | 7–9 格,1 转墙、早期数字锚点 |
|
||||
| 4 | 近路到达时被挡,另一条路线的到达步数不同 | 8–10 格,1 转墙 |
|
||||
| 5 | 同一主题换布局 | 9–12 格,1 转墙 |
|
||||
| 6 | 短盘巩固 | 6–9 格,1 转墙 |
|
||||
| 7 | 静态墙控制路线,转墙控制时机 | 10–12 格,1 静态墙+1 转墙 |
|
||||
| 8 | 两个转墙分别约束两段路 | 10–13 格,2 转墙,线索明确 |
|
||||
| 9 | 两处相位与较长区间组合 | 12–15 格,2 转墙 |
|
||||
| 10 | 能口述的综合解法 | 13–17 格,2 转墙 |
|
||||
|
||||
“另一条路线”必须对应合法的未重复访问路径,不能回到已走格消耗步数。固定到达步号的数字线索不会随绕路变更;作者要安排整个路线满足它。
|
||||
|
||||
每个转墙都要通过现有删除与静态替代检查。撤销教学不以“玩家必须先画错才能通关”为数据条件。若现有验收铁律无法在建议规模产出好题,可调整规模或布局,不为凑关数放宽唯一性。
|
||||
|
||||
## 7. 从竞品经验转成制作流水线
|
||||
|
||||
### 7.1 一关一张策划卡
|
||||
|
||||
建议另建制作记录,保持 `LevelDef` 与发布 JSON 格式不变。推荐字段:
|
||||
|
||||
| 字段 | 示例 / 要求 |
|
||||
|---|---|
|
||||
| 所属玩法、候选编号 | 数字 / 六边 / 转墙;候选不占用正式 id |
|
||||
| 前置技能 | 整块一步、共边、数字区间;不写“玩家已会所有规则” |
|
||||
| 关卡作用 | 教新知 / 强化 / 误区辨析 / 综合 / 节奏调剂 |
|
||||
| 解法句 | “先走左侧区域,再经过大格,才保得住右侧入口。” |
|
||||
| 作者完整路径 | 格 id 序列,用于和求解器核对 |
|
||||
| 关键分叉 | 所在步号、候选出口、为什么排除错出口 |
|
||||
| 条件有效性 | 删除某数字/奇偶/墙后怎样变化;教学保留冗余要说明 |
|
||||
| 操作风险 | 小共边、异形凹口、密集墙、窄屏数值遮挡 |
|
||||
| 难度预估 | 容易/常规/挑战+具体原因;与实测分开 |
|
||||
| 试玩记录 | 思考时间、回退、提示、误读、口述理由 |
|
||||
|
||||
### 7.2 制作顺序
|
||||
|
||||
1. **选母题与课程位置。** 先写玩家要学会的判断和解法句。
|
||||
2. **构造最小骨架。** 只留足以形成该判断的拼块、共边与区域。
|
||||
3. **确定作者解。** 按实际图上的节点计数;转墙逐步核对相位。
|
||||
4. **配线索。** 教学先保证可理解;后期优先用少而有效的条件固定路线。
|
||||
5. **求解与消融。** 检查至少到第二条解;一次删一项再复验,不能把多个“单独可删”项一并删掉。
|
||||
6. **人工推理解题。** 写出关键选择的依据;只有唯一解但依赖大范围穷举的题继续改。
|
||||
7. **检查内容重复。** 比较图结构、数字位置、关键分叉与解法,不只比较图片。
|
||||
8. **手机试玩。** 读盘与手势问题分别记录,修正后复验。
|
||||
9. **排关。** 用真实难度、主题与节奏组合,而非只按生成器得分排序。
|
||||
10. **发布验收。** 最后登记正式包、保持 id/进度稳定,执行该玩法既有检查。
|
||||
|
||||
几何可复用,但重新配线索后应形成不同推理任务。只旋转、镜像或移动起点,仍属于同一母题的变体;可用来强化技能,不作为内容多样性的主要来源。
|
||||
|
||||
### 7.3 难度怎么衡量
|
||||
|
||||
当前数字生成器实际分数:
|
||||
|
||||
`最大线索间隔×7 + 格数×1.2 + log10(无线索路径数+1)×5 - 数字数×3 + 大格数×1.5 + 未知起点加12`
|
||||
|
||||
这适合筛选候选,不能代表真人耗时。来自 [现有生成器](../modes/01-number-stroke/tools/generate.ts),不是竞品公式,也不能直接给奇偶或转墙评级。
|
||||
|
||||
建议同时记录以下维度,先不用未经验证的权重合成总分:
|
||||
|
||||
| 维度 | 数字玩法 | 六边/转墙玩法 |
|
||||
|---|---|---|
|
||||
| 初始方向是否明确 | `1` 与早期引导数字、端点 | 被墙排除后的可行出口、起手相位 |
|
||||
| 关键判断数量 | 多出口选择、区间安排 | 区域入口、到达拍子 |
|
||||
| 判断需要看多远 | 当前出口是否马上可排除 | 需否联合远端数字与另一转墙 |
|
||||
| 错路何时被发现 | 指定步号冲突、死路、封区 | 撞墙、未来相位冲突、区域遗漏 |
|
||||
| 读盘负担 | 大格/异形边界、线索数量 | 六向邻接、墙与转轴密度 |
|
||||
| 执行负担 | 窄触点、长滑动 | 误撞墙、转动显示是否清楚 |
|
||||
|
||||
**唯一解是正确性门槛;可解释推理是内容门槛;真实玩家观察是关序依据。** 求解器搜索节点数受算法和剪枝影响,只能辅助比较。
|
||||
|
||||
### 7.4 试玩与淘汰
|
||||
|
||||
先找 3–5 位未接触项目的玩家做观察,验证教学与主要断层;这不足以估计市场留存或准确通关率。
|
||||
|
||||
- 数字玩法参考现有目标:思考 15–90 秒、绘制 3–8 秒;六边参考思考 20–120 秒、绘制 5–12 秒。新规则教学可更短,这些都仍是目标值。
|
||||
- 记录看到盘面到首次落笔的时间、总思考时间、第一次错误位置、回退次数、补数/指路使用、是否能说出理由。
|
||||
- 若玩家理解规则但反复误触,先修显示或布局;若操作正常但只会枚举,改线索与骨架;若会做却觉得重复,换母题或课程位置。
|
||||
- 回退和提示本来就是允许的玩法,不将“零提示通关”变成进度门槛。
|
||||
|
||||
候选若出现以下情况,暂不入正式包:无解/多解;新条件不起作用;边界不清;主题无法一句话解释;与邻近题推理等价且不承担练习/调剂作用;转墙主要靠快速试滑而非步数推断。
|
||||
|
||||
## 8. 建议的下一批内容与优先级
|
||||
|
||||
建议先做 **30 关候选**:数字 12、静态六边 8、转墙 10。只发布验收通过者,不要求填满数量。现有 45 关全部保留时,理论内容上限为 75 关;候选数量不是已完成交付或正式上线承诺。
|
||||
|
||||
| 阶段 | 内容 | 作用 |
|
||||
|---|---|---|
|
||||
| P0 | 复测关键教学关:数字 1-4、1-7、进阶首次异形、2-8、奇偶开局;六边首墙;转墙开局 | 找到真正需要桥接的技能点 |
|
||||
| P1 | 数字 12 关:大格出口/区域顺序 4、精确区间 4、奇偶桥接 3、小盘找起点 1 | 最符合当前规则与工具能力,优先主线内容 |
|
||||
| P1 | 静态六边 8 关,采用第 6.3 节蓝图 | 延长无动态状态的六边课程 |
|
||||
| P2 | 转墙 10 关教学候选,采用第 6.4 节蓝图 | 验证可读性与相位学习,之后再扩高密度挑战 |
|
||||
| P3 | 主题选关、离线每日题 | 在主线与存档需求明确后扩展壳功能 |
|
||||
| 储备 | 桥、跨盘、单向线、多路径等 | 另立 GDD、模型与校验器后再评估 |
|
||||
|
||||
数字 12 关的初稿节拍:出口入门→换方向应用→精确区间入门→区间强化→出口与区域组合→短关调剂→奇偶桥接一→奇偶桥接二→奇偶桥接三→小盘找起点→区间综合→主题收束。试玩后可以拆成两个主题包,并按前置技能调整次序。
|
||||
|
||||
近期不把外部热门机制直接塞进数字 V1:单向边、斜走、传送等仍超出当前设计;六边墙也不并入矩形玩法。尤其 warp 会改变笔尖位置关系,需要独立产品判断。
|
||||
|
||||
## 9. 本次交付与证据边界
|
||||
|
||||
本次完成:九个代表性产品的玩法/公开内容结构采集、一个开发者制作方法参照、当前 45 关数据统计、母题库、章节节拍、下一批 30 关候选蓝图及制作验收建议。
|
||||
|
||||
未完成且不宣称完成:竞品逐关实玩、竞品内部生成算法还原、最新活跃/收入排名、中国小游戏榜单分析、30 道新关的具体布局制作、真人试玩。本次未修改代码或关卡,也未重新执行现有游戏的自动测试与唯一性校验。
|
||||
|
||||
主要项目来源:[仓库说明](../README.md)、[关卡规范](关卡设计规范.md)、[数字 GDD](../modes/01-number-stroke/docs/GDD.md)、[六边 GDD](../modes/03-hex-walls/docs/GDD.md)、[数字关卡类型](../modes/01-number-stroke/src/types.ts)、[六边关卡类型](../modes/03-hex-walls/src/types.ts)、[数字生成器](../modes/01-number-stroke/tools/generate.ts)、[六边校验器](../modes/03-hex-walls/tools/validate.ts)。外部事实的官方链接均在相应条目旁,便于复核。
|
||||
-237
@@ -1,237 +0,0 @@
|
||||
# 数字一笔画:关卡设计规范
|
||||
|
||||
依据当前工程的 GDD、实际关卡 JSON、规则代码、生成器和发布校验器整理。核对日期:2026-10-12。
|
||||
|
||||
主规范适用于 `modes/01-number-stroke`(数字一笔);`modes/03-hex-walls`(六边一笔)是独立玩法,其差异见第 9 节。
|
||||
|
||||
本文区分三种要求:**硬规则**是规则代码或发布校验要求;**设计原则**来自 GDD 和现有内容策略;**策划建议**是用于制作、评审与试玩的补充,不代表程序已经强制执行。
|
||||
|
||||
## 1. 设计目标
|
||||
|
||||
让玩家通过观察、排除和步数推算,找出唯一的一条走遍所有格的路线。爽点是“只能这样走”的顿悟,以及推理完成后一笔画完的顺畅感。
|
||||
|
||||
- 单机、离线、手机竖屏;无计时、无体力、无分数。
|
||||
- 数字玩法的 GDD 单局目标为思考 15–90 秒、绘制 3–8 秒。这是体验目标,不是已通过试玩验证的指标。
|
||||
- 大格的核心价值是“面积与步数分离”:占地更大、邻接更多,但只计一步。
|
||||
- 所有关卡、命名和资源原创,不使用第三方题库。
|
||||
|
||||
## 2. 盘面与路径的硬规则
|
||||
|
||||
| 项目 | 规范 |
|
||||
|---|---|
|
||||
| 格数 | `N = cells.length`,按拼块数量计数,不按占地面积计数 |
|
||||
| 坐标 | 使用整数单位网格,原点左上;制作时建议统一从非负坐标开始 |
|
||||
| 矩形格 | 允许 `1×1`、`1×2`、`2×1`、`2×2`、`3×3` |
|
||||
| 异形格 | `shape` 定义 2–4 个四向连通单位格;I/L/S/T/O 目录,允许旋转、镜像 |
|
||||
| 异形尺寸 | `w/h` 必须等于形状包围盒尺寸;偏移非负、无重复。`I4` 可通过 `shape` 表达,不能据此把 `4×1` 当作允许的普通矩形 |
|
||||
| 占地 | 拼块之间不能重叠;未被任何拼块占据的位置为空洞 |
|
||||
| 邻接 | 共享至少一条单位边才相邻;碰角、隔洞不相邻 |
|
||||
| 连通 | 整张格图必须连通;连通只是有解的必要条件 |
|
||||
| 路径 | 每格恰好经过一次;只可前进到相邻未走格;不能跳格、分叉或重复经过 |
|
||||
| 大格 | 整块是一个节点,只计一步;块内不再按小方格计数 |
|
||||
| 通关 | 路径覆盖全部 N 格,且所有数字与奇偶要求同时满足 |
|
||||
|
||||
“一笔画”是连续路径约束,不要求玩家全程不松手。松手可保留路径并继续;点按相邻格也能追加。
|
||||
|
||||
不引入斜走、传送、单向边、冰格、颜色门、限时等未实现机制;六边形和墙属于另一玩法,不直接加入数字玩法。
|
||||
|
||||
## 3. 线索设计规范
|
||||
|
||||
### 3.1 数字线索
|
||||
|
||||
- 数字 `c` 表示该格必须是路径第 `c` 步;反过来,第 `c` 步也必须落在这个格。
|
||||
- 数字为 `1..N` 的整数,同一关不能重复。
|
||||
- 可以位于路径中段,不必给终点 `N`。
|
||||
- 有 `1` 时玩家只能从它起手;没有 `1` 时玩家可尝试任意格,但只有满足全部条件的完整路线能通关。
|
||||
- 起点未知安排在掌握基础规则之后;当前首次出现于 **2-8(总第 18 关)**,教学包全部保留 `1`。
|
||||
|
||||
### 3.2 奇偶线索
|
||||
|
||||
- `odd` 要求本格步号为奇数,`even` 要求为偶数;仍按整块计步,与面积无关。
|
||||
- 原始关卡同格只能写数字或奇偶,不能同时写;一关可有多个奇偶格,也可与其他格的数字混用。
|
||||
- **每条奇偶线索必须必要**:单独删除任意一条后,应出现至少两条合法解。现有 `validate` 会拒绝冗余奇偶条件。
|
||||
- 不用普通方格盘上天然成立的黑白奇偶作为无效装饰;优先结合大格、异形改变路线步号关系。
|
||||
- 当前随机生成器只生成数字关。奇偶关采用人工配置线索,再由求解器验证。
|
||||
|
||||
### 3.3 少而有效
|
||||
|
||||
**硬要求:每关恰好一条满足全部线索的完整路径。** 唯一性按有序步号路径计算;没有 `1` 时也必须检查所有可能起点。
|
||||
|
||||
**设计原则:**在保持唯一解和教学效果的前提下尽量少给线索,中后期优先尝试删除端点暗示。前 8 关允许保留教学所需的冗余数字。
|
||||
|
||||
数字线索的最简性目前不是发布校验的硬门槛;需要用 `design --minimize` 人工检查。该工具输出的是“单独可删”的线索,不能把所有可删项同时删除;应逐次删除并重新验证。
|
||||
|
||||
## 4. 难度控制
|
||||
|
||||
| 旋钮 | 设计作用 | 当前引入节拍 |
|
||||
|---|---|---|
|
||||
| 格数 N | 增加路线长度与信息量 | 全程;开头可小至 3–5 格 |
|
||||
| 中段数字 | 要求计算到达时机,而非只认起终点 | 1-5 专项教学;1-3 已用中段数字辅助撤销教学 |
|
||||
| 空洞 | 切断直觉近路,迫使识别真实邻接 | 1-6 |
|
||||
| `2×2` | 一步对应多个出口,形成路线枢纽 | 1-4;1-8 专项强化 |
|
||||
| 横竖长条 | 利用长边邻接,要求先判断从哪边进入、离开 | 1-9;1-10 综合 |
|
||||
| `3×3` 与异形 | 扩大出口选择、改变凹口邻接 | 第二包;首个 `3×3` 为 2-2 |
|
||||
| 线索步号差 | 拉长尚未确定的路线区间,是现有难度主旋钮 | 全程,进阶包重点使用 |
|
||||
| 不给 `1` | 加入起点推理 | 2-8 |
|
||||
| 奇偶 | 用较弱的条件筛掉错误路线 | 第三包,先缩小盘面再组合 |
|
||||
|
||||
**线索步号差指按数值排序后的连续数字之差,不指空间上相邻两格。** 例如 N=21,线索为 `1、5、21`,差值为 `4、16`;生成器还计入最后一个线索到 N 的尾段,此例为 0。
|
||||
|
||||
进阶数字关通常先给 `1` 与一个早期引导数字,再将后续区间拉长。当前生成器的引导数字取值是 **4–7**,即从第 1 步到它有 3–6 次转移;不要混写成固定“相隔 4–7 步”。这不是每个手工关的硬约束。
|
||||
|
||||
GDD 的一般规模建议:教学约 8–12 格、常规 12–16 格、后期 17–22 格;基础教学与新机制教学可更小。当前数字关实际范围为 3–21 格。`N≤22` 是求解器的设计工作范围,解析器没有强制拒绝 N>22,扩展前需另做性能与手机操作验证。
|
||||
|
||||
策划建议:一次新增一种主要认知负担,之后用旧机制组合强化;包尾设综合题,新机制包开头回落难度。不能用格数、线索少或生成器分数直接代替真人难度判断。
|
||||
|
||||
## 5. 当前教学与关卡包结构
|
||||
|
||||
### 5.1 入门包:10 关,手工设计
|
||||
|
||||
| 关卡 | N | 教学目标 | 数字线索 |
|
||||
|---|---:|---|---|
|
||||
| 1-1 | 3 | 从 1 滑向邻格,自动计步 | 1 |
|
||||
| 1-2 | 4 | 必须走完所有格 | 1、4 |
|
||||
| 1-3 | 5 | 走错后可以退回 | 1、3 |
|
||||
| 1-4 | 4 | 大格只算一步 | 1、3 |
|
||||
| 1-5 | 7 | 中段数字限制路线 | 1、4 |
|
||||
| 1-6 | 9 | 空洞不相邻,先观察终点 | 1、9 |
|
||||
| 1-7 | 12 | 多处分叉,先推再画 | 1、4、8 |
|
||||
| 1-8 | 11 | `2×2` 的多出口枢纽 | 1、8 |
|
||||
| 1-9 | 11 | 横长条的进入、离开方向 | 1、6 |
|
||||
| 1-10 | 14 | 大格、长条与步数综合小考 | 1、6 |
|
||||
|
||||
1-4 是关键验收关:新玩家必须理解“大块不是多步”。若教不会,先改盘面和演示,再增加后续内容。
|
||||
|
||||
### 5.2 进阶包:10 关
|
||||
|
||||
当前 N=14–21,通常含 2–3 个多单位格,数字线索通常 3–4 个。最大步号间隔由前段约 7–8 逐步抬升到 16,2-8 加入起点未知。允许生成候选,但每关必须人工审核路线、读盘和推理。
|
||||
|
||||
### 5.3 奇与偶:6 关体验包
|
||||
|
||||
当前 N=4–18:偶数入门 → `3×3` 上的奇数条件 → 用奇偶决定绕行 → 异形、数字、多个奇偶共同推理。可复用几何重新设计线索,但要形成新的推理任务,并逐条证明奇偶条件必要。
|
||||
|
||||
当前共 **26 关(10+10+6)**。旧 GDD 中的“12 关教学”“每包 8 关”“44 关总量”是早期计划,不是现有包长与交付要求。正式包是否扩展到更多关卡,应另立内容计划。
|
||||
|
||||
## 6. 推理、路线与操作品质
|
||||
|
||||
以下为设计原则及策划评审建议,不能由唯一解检测代替:
|
||||
|
||||
1. **先写推理意图。** 每关说明关键分叉、排除错误路线的依据、玩家预期获得的顿悟。
|
||||
2. **能解释为什么。** 无提示时应可由步数、端点、邻接、封闭区域或奇偶条件推出来,避免主要靠枚举试错。
|
||||
3. **大格实际参与。** 进阶关尽量让多单位格位于路线中段,进出各一次;若改成单格也不改变关键推理,需要重新评估它的价值。
|
||||
4. **不只做蛇形填满。** 避免长时间机械沿唯一走廊扫过;综合关应有需要观察和排除的选择。
|
||||
5. **错误有依据。** 误导来自“靠得近但步数不对”“先走会封死剩余区域”等规则事实,不能来自看不清边界或含混邻接。
|
||||
6. **手机上可读可画。** 数字、奇偶标记、空洞、异形凹口和共边清晰;检查窄屏适配与快速滑动,避免用极端长宽比压小触点。
|
||||
|
||||
生成器现有过滤:多单位格不能当作者解端点;至少一个大格;按拼块中心估算的最长同向段比例不超过 0.5。这些只是候选筛选启发式,不是所有教学关都要遵守的硬规则,也不等同于“路线漂亮”。
|
||||
|
||||
## 7. 容错与提示对关卡设计的要求
|
||||
|
||||
- 允许踩错线索后继续画,错误格标红;不得把“错了立即失败”作为教学前提。
|
||||
- 指定步号被其他格占用时立即冲突,不能假设玩家必须踩到原线索格才会收到错误反馈。
|
||||
- 死路不会锁死或弹失败;数字玩法可点任意已走格,截掉后续路径,也可重开。
|
||||
- “补数字”只补一个确定步号,帮助缩小待解区间,不自动画路;即使玩家走错,也从真实解补数。
|
||||
- 补数可以落在奇偶格上,此时新增数字与原奇偶要求同时生效。这是对局辅助,原始 JSON 仍禁止同格同时配置二者。
|
||||
- 重开保留本局补数;退出重进恢复原始关卡。
|
||||
- “指路”:空路径指起点;正确前缀指下一步;偏离时指最后正确位置,要求回退,不沿错路继续提示。
|
||||
- 原关必须在不开提示时仍能人解,不能用补数来弥补缺失的基本可解性与教学。
|
||||
|
||||
## 8. 制作、数据交付与验收
|
||||
|
||||
### 8.1 制作流程
|
||||
|
||||
确定教学/推理目标 → 铺盘并确认邻接 → 找作者解 → 配少量线索 → 求解验证唯一 → 逐项删线索复验 → 人工推理解题 → 手机操作试玩 → 排入关卡包。
|
||||
|
||||
教学关手做;进阶可用生成器。每次修改几何、数字、奇偶条件后都重新验证。不要直接覆盖已验收的正式包来试生成参数,应先输出到候选文件。
|
||||
|
||||
### 8.2 数据格式
|
||||
|
||||
包字段:`mode`、`pack`、`title`、可选 `notes`、`levels`。单关字段:`id`、可选 `teach/caption`、`cells`。格字段见第 2、3 节。
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "01-number-stroke",
|
||||
"pack": 1,
|
||||
"title": "入门",
|
||||
"levels": [{
|
||||
"id": "1-1",
|
||||
"teach": "滑动,邻格自动出步号",
|
||||
"caption": "从 1 开始,滑到旁边的格",
|
||||
"cells": [
|
||||
{ "id": "a", "x": 0, "y": 0, "w": 1, "h": 1, "clue": 1 },
|
||||
{ "id": "b", "x": 1, "y": 0, "w": 1, "h": 1 },
|
||||
{ "id": "c", "x": 2, "y": 0, "w": 1, "h": 1 }
|
||||
]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
- 格 id 在单关内唯一;关 id 在玩法内唯一,建议按 `包号-包内序号` 编排。包内序号实际来自数组顺序,应与 id 一致。
|
||||
- `teach` 是设计备注,永不作为玩家文案;可记录目标、来源、参数和作者解。`caption` 才是对局教学文案。
|
||||
- 不配置供通关判定读取的答案字段;当前进阶关确实会在 `teach` 里记作者解,不能误解成文件完全不含答案。
|
||||
- 旧 GDD 样例中的单关 `pack/index/cellSize` 不在当前 `LevelDef` 中;`hintPath` 也没有对应实现。按实际类型与解析器交付,不照搬旧样例。
|
||||
- 新包需要在 `web/main.ts` 的 `packs` 中登记;正常版跨包线性解锁,单文件试玩版全部关卡开放。
|
||||
|
||||
策划交付建议另附:关键推理说明、错误路线及排除理由、作者解、数字删减记录、奇偶删除证明、预期难度与试玩记录。作者解不要放进 `caption`。
|
||||
|
||||
### 8.3 工具
|
||||
|
||||
在 `modes/01-number-stroke` 中运行:
|
||||
|
||||
```powershell
|
||||
npm run validate
|
||||
npm run validate -- 1-7
|
||||
npm run design -- 1-7 --minimize
|
||||
npm run design -- 1-7 --suggest 3
|
||||
npm run check
|
||||
```
|
||||
|
||||
`validate` 验证结构可加载、盘面连通、恰好一解、没有冗余奇偶条件。`check` 另含类型检查和单元测试。唯一性检查至少要允许找到第二条解,不能以“找到第一条”为合格依据。
|
||||
|
||||
### 8.4 发布验收表
|
||||
|
||||
| 检查 | 门槛 | 方式 |
|
||||
|---|---|---|
|
||||
| 几何、数字范围、重叠与格 id | 合法 | Board 构建 / validate |
|
||||
| 连通与完整路径唯一性 | 连通、恰好 1 解 | validate |
|
||||
| 奇偶有效性 | 每条删除后多解 | validate |
|
||||
| 数字有效性 | 无不必要数字,或说明教学保留理由 | design + 策划评审 |
|
||||
| 关 id、数组顺序与包登记 | 唯一、一致、可选可解锁 | 人工核对 |
|
||||
| 推理质量 | 关闭提示仍可人解,关键选择有依据 | 人工推理与试玩 |
|
||||
| 教学 | 1-4 前理解基础操作与大格计步;1-7 出现先观察再画 | 新玩家试玩 |
|
||||
| 操作与提示 | 可恢复错误,提示考虑全部条件,窄屏可读可画 | 手机试玩 |
|
||||
|
||||
建议找 3–5 位未接触玩法的玩家,记录思考时长、首次错误位置、回退次数、提示使用和口述推理。用这些记录修正关序;不要把自动评分当作实测结论。
|
||||
|
||||
## 9. 六边一笔的独立规范
|
||||
|
||||
共享“每格一次、数字对应步号、完整解唯一、允许改错”的目标,但不共享矩形拼块规则。
|
||||
|
||||
| 项目 | 六边玩法要求 |
|
||||
|---|---|
|
||||
| 几何 | 正六边形,轴坐标 `q/r`,六向共边;没有矩形大格与奇偶条件 |
|
||||
| 墙 | 放在两格真实共边上,封住该边,不占据一格 |
|
||||
| 墙有效性 | 教学包第 3 关起、后续包全部关,至少一堵静态墙或转墙;去掉全部墙后不再唯一 |
|
||||
| 静态墙冗余 | 设计上应去除;当前 validate 只报告“可删墙”,不会据此判失败 |
|
||||
| 转墙 | 实验机制:每落一格(含起手)顺时针转 120°;用出发前墙位判断通行,落格后再转;撤销恢复相位 |
|
||||
| 转墙必要性 | 删除任意转墙后不再唯一,且任意候选边上的静态墙都不能替代它维持唯一;validate 强制检查 |
|
||||
| 容错差异 | 当前六边玩法沿路退到上一格;不具备数字玩法点任意历史格一次截断的操作 |
|
||||
| 当前内容 | 教学 10 关+转墙实验 9 关;最多 4 个转墙只是当前实验规模,非正式内容上限 |
|
||||
|
||||
不要将 README 的“无冗余墙”描述误写成所有静态墙都已被自动校验器强制验收。转墙规则需要独立排关和试玩,不直接套用数字玩法的线索间隔评分。
|
||||
|
||||
## 10. 本次核对与来源
|
||||
|
||||
2026-10-12 重新执行工程现有 `tools/validate.ts`:数字玩法 **26/26** 通过;六边玩法 **19/19** 通过。数字关所有奇偶条件通过逐条删除检查;六边校验未报告可删静态墙。
|
||||
|
||||
环境缺少本地 `tsx`,标准 `npm run validate` 因依赖缺失无法启动;本次使用 Node 24 的 TypeScript 支持及临时的扩展名解析钩子直接加载原校验器,无修改工程代码。此结果仅代表关卡自动校验,不代表本次完成了完整 `npm run check` 或真人试玩。
|
||||
|
||||
主要依据:
|
||||
|
||||
- [产品结构](PRODUCT.md)与[仓库说明](../README.md):玩法边界、当前内容量。
|
||||
- [数字玩法 GDD](../modes/01-number-stroke/docs/GDD.md)及[玩法说明](../modes/01-number-stroke/README.md):目标、教学与内容原则。
|
||||
- [数据类型](../modes/01-number-stroke/src/types.ts)、[盘面](../modes/01-number-stroke/src/board.ts)、[对局规则](../modes/01-number-stroke/src/game.ts):当前硬规则。
|
||||
- [生成器](../modes/01-number-stroke/tools/generate.ts)、[设计工具](../modes/01-number-stroke/tools/design.ts)、[发布校验](../modes/01-number-stroke/tools/validate.ts):实际制作与验收能力。
|
||||
- [六边 GDD](../modes/03-hex-walls/docs/GDD.md)及[六边发布校验](../modes/03-hex-walls/tools/validate.ts):墙与转墙差异。
|
||||
|
||||
如以后扩展机制或包结构,应先更新对应玩法规则与验证能力,再同步本文。
|
||||
Reference in New Issue
Block a user