初始提交:三玩法 Web 原型与设计文档

- 玩法一 数字一笔:规则库、求解器、26 关、竖屏触摸 Web 原型、测试
- 玩法二 连连气球:连消畅玩版核心、Web 原型、Cocos 球槽组件与导出脚本
- 玩法三 六边一笔:规则库、求解器、10 关 + 转墙实验包、Web 原型、测试
- 产品文档、UI 设计规范、各玩法 GDD / HANDOFF
- 文档移除全部外部参考作引用;删除玩法二 v0 旧稿
This commit is contained in:
cabinw
2026-09-16 14:30:59 +08:00
commit 8026e4628d
169 changed files with 22162 additions and 0 deletions
@@ -0,0 +1,65 @@
// 设计令牌单一来源守门:web/style.css 的 :root 与 src/theme.ts、web/canvas-painter.ts 必须一致。
// 改任一处而不同步另一处,这里会红。规格文档见仓库 docs/design.md。
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { test } from 'node:test';
import { fileURLToPath } from 'node:url';
import { MOTION, THEME } from '../src/theme';
import { BOARD_FONT_FAMILY } from '../web/canvas-painter';
const here = dirname(fileURLToPath(import.meta.url));
const css = readFileSync(resolve(here, '../web/style.css'), 'utf8');
function rootTokens(source: string): Map<string, string> {
const m = /:root\s*\{([\s\S]*?)\n\}/.exec(source);
assert.ok(m, 'style.css 缺少 :root 块');
const out = new Map<string, string>();
for (const line of m[1].split('\n')) {
const t = /^\s*(--[\w-]+)\s*:\s*(.+?);\s*$/.exec(line);
if (t) out.set(t[1], t[2]);
}
return out;
}
const tokens = rootTokens(css);
const hex = (s: string | undefined): string => (s ?? '').trim().toUpperCase();
test('颜色令牌:style.css :root 与 THEME 一致', () => {
const pairs: Array<[string, string]> = [
['--bg', THEME.bg],
['--cell', THEME.cell],
['--ink', THEME.clueInk],
['--clue-ring', THEME.clueRing],
['--primary', THEME.visited],
['--primary-bright', THEME.visitedBright],
['--on-primary', THEME.visitedInk],
['--danger', THEME.conflict],
['--hint', THEME.hint],
];
for (const [name, value] of pairs) {
assert.equal(hex(tokens.get(name)), hex(value), `${name} 与 theme.ts 不一致`);
}
});
test('动效时长令牌:--t-* 与 MOTION 一致', () => {
const pairs: Array<[string, number]> = [
['--t-fast', MOTION.fast],
['--t-base', MOTION.base],
['--t-slow', MOTION.slow],
['--t-hold', MOTION.hold],
];
for (const [name, seconds] of pairs) {
assert.equal(tokens.get(name), `${Math.round(seconds * 1000)}ms`, `${name} 与 theme.ts MOTION 不一致`);
}
assert.equal(MOTION.numberFade, MOTION.fast, '步号淡入应等于 fast 档');
assert.equal(MOTION.bridgeGrow, MOTION.fast, '桥生长应等于 fast 档');
assert.equal(MOTION.flashTime, MOTION.slow, '闪白应等于 slow 档');
assert.equal(MOTION.clearedSweep, MOTION.slow, '过关扫光应等于 slow 档');
assert.equal(MOTION.clearedHold, MOTION.hold, '过关停留应等于 hold 档');
assert.equal(MOTION.boardEnter, MOTION.base, '进场应等于 base 档');
});
test('字体栈:canvas 与 CSS --font 完全一致', () => {
assert.equal(tokens.get('--font'), BOARD_FONT_FAMILY);
});