初始提交:三玩法 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,76 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { Board } from '../src/board';
import { bridgeRect, cellRect, cellShapeRects, fitBoard, hitTest } from '../src/layout';
import type { CellDef } from '../src/types';
const c = (id: string, x: number, y: number, w = 1, h = 1, clue?: number): CellDef => ({ id, x, y, w, h, clue });
test('按短边缩放并居中,受 maxUnit 限制', () => {
const b = Board.fromCells([c('a', 0, 0), c('b', 1, 0), c('c', 2, 0)]);
const L = fitBoard(b, 390, 600, { padding: 10, maxUnit: 96 });
assert.equal(L.unit, 96);
assert.equal(L.width, 288);
assert.equal(L.originX, (390 - 288) / 2);
const tall = Board.fromCells([c('a', 0, 0), c('b', 0, 1), c('c', 0, 2), c('d', 0, 3), c('e', 0, 4), c('f', 0, 5)]);
const L2 = fitBoard(tall, 390, 300, { padding: 0, maxUnit: 200 });
assert.equal(L2.unit, 50);
});
test('命中测试:格中心命中、缝与死区不命中、空洞不命中', () => {
const b = Board.fromCells([c('a', 0, 0), c('B', 1, 0, 2, 2), c('h', 0, 2)]);
const L = fitBoard(b, 300, 300, { padding: 0, maxUnit: 100 });
const ra = cellRect(b, 0, L);
assert.equal(hitTest(b, L, ra.x + ra.w / 2, ra.y + ra.h / 2), 0);
assert.equal(hitTest(b, L, L.originX + 2, L.originY + 2), -1, '角落死区');
assert.equal(hitTest(b, L, L.originX + 150, L.originY + 150), 1, '2×2 中心');
assert.equal(hitTest(b, L, L.originX + 50, L.originY + 150), -1, '空洞 (0,1)');
assert.equal(hitTest(b, L, -5, -5), -1);
});
test('桥矩形横跨缝并落在共享边中点', () => {
const b = Board.fromCells([c('a', 0, 0), c('B', 1, 0, 2, 2)]);
const L = fitBoard(b, 300, 200, { padding: 0, maxUnit: 100 });
const br = bridgeRect(b, 0, 1, L)!;
assert.ok(br);
assert.ok(Math.abs(br.x + br.w / 2 - (L.originX + 100)) < 1e-6, '中心在 x=1 的边上');
assert.ok(Math.abs(br.y + br.h / 2 - (L.originY + 50)) < 1e-6, '共享边 y∈[0,1] 的中点');
assert.equal(bridgeRect(b, 1, 1, L), null);
});
test('异形格绘制矩形:单位 + 接缝', () => {
// L4 占 (1,1)(1,2)(1,3)(2,3):4 个单位矩形 + 3 条内部缝(两竖一横)
// 补一个 (0,0) 的格钉住原点,免得 L 被平移
const b = Board.fromCells([
{ id: 'L', x: 1, y: 1, w: 2, h: 3, shape: [[0, 0], [0, 1], [0, 2], [1, 2]] },
c('z', 0, 0),
]);
const L = fitBoard(b, 300, 300, { padding: 0, maxUnit: 100 });
const { units, seams } = cellShapeRects(b, 0, L);
assert.equal(units.length, 4);
assert.equal(seams.length, 3);
const half = L.gap / 2;
// 横缝:(1,3)→(2,3),跨过 x=2 的竖缝,向两侧单位内各延长一个圆角半径(盖住圆角缺口)
assert.deepEqual(
seams.find((r) => r.w === L.gap + 2 * L.radius),
{
x: L.originX + 2 * L.unit - half - L.radius,
y: L.originY + 3 * L.unit + half,
w: L.gap + 2 * L.radius,
h: L.unit - L.gap,
},
);
// 竖缝:(1,1)→(1,2),跨过 y=2 的横缝,同样上下延长
assert.deepEqual(
seams.find((r) => r.h === L.gap + 2 * L.radius && r.y === L.originY + 2 * L.unit - half - L.radius),
{
x: L.originX + L.unit + half,
y: L.originY + 2 * L.unit - half - L.radius,
w: L.unit - L.gap,
h: L.gap + 2 * L.radius,
},
);
// 单位矩形与 cellRect 对单个 1×1 的算法一致
const u0 = units[0];
assert.deepEqual(u0, { x: L.originX + L.unit + half, y: L.originY + L.unit + half, w: L.unit - L.gap, h: L.unit - L.gap });
});