初始提交:三玩法 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
+79
View File
@@ -0,0 +1,79 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { HexBoard } from '../src/board';
import { clipPolygon, roundPolygon, stripesInBand } from '../src/geom';
import { bridgePolygon, cellCenter, fitBoard, hitTest, pixelToAxial, sharedEdge, wallBand } from '../src/layout';
import type { HexCellDef } from '../src/types';
const c = (id: string, q: number, r: number, clue?: number): HexCellDef => ({ id, q, r, clue });
const flower = () =>
HexBoard.fromCells([c('o', 0, 0), c('e', 1, 0), c('ne', 1, -1), c('nw', 0, -1), c('w', -1, 0), c('sw', -1, 1), c('se', 0, 1)], [['o', 'e']]);
test('fitBoard 按短边缩放并居中,不超过 maxSize', () => {
const b = flower();
const L = fitBoard(b, 400, 600, { padding: 10, maxSize: 60 });
assert.ok(L.size <= 60);
// 花形宽 = 3 × √3 × size,应贴合 380 宽或受 maxSize 限制
assert.ok(L.width <= 380 + 1e-6);
const o = cellCenter(b, 0, L);
assert.ok(Math.abs(o.x - 200) < 1e-6);
assert.ok(Math.abs(o.y - 300) < 1e-6);
const small = fitBoard(b, 100, 100, { padding: 0 });
assert.ok(small.width <= 100 + 1e-6 && small.height <= 100 + 1e-6);
});
test('像素 ↔ 轴坐标往返;命中中心;死区与空洞返回 -1', () => {
const b = flower();
const L = fitBoard(b, 400, 600, { padding: 10, maxSize: 60 });
for (const cell of b.cells) {
const p = cellCenter(b, cell.index, L);
assert.deepEqual(pixelToAxial(L, p.x, p.y), { q: cell.q, r: cell.r });
assert.equal(hitTest(b, L, p.x, p.y), cell.index);
}
const o = cellCenter(b, 0, L);
const e = cellCenter(b, 1, L);
// 两格中点在缝里 → 死区
assert.equal(hitTest(b, L, (o.x + e.x) / 2, (o.y + e.y) / 2), -1);
// 盘外
assert.equal(hitTest(b, L, 5, 5), -1);
// (2,0) 不存在
const far = { x: o.x + Math.sqrt(3) * L.size * 2, y: o.y };
assert.equal(hitTest(b, L, far.x, far.y), -1);
});
test('共用边、桥、墙带的几何', () => {
const b = flower();
const L = fitBoard(b, 400, 600, { padding: 10, maxSize: 60 });
const e = sharedEdge(b, 0, 1, L)!;
const o = cellCenter(b, 0, L);
const ea = cellCenter(b, 1, L);
assert.ok(Math.abs(e.mid.x - (o.x + ea.x) / 2) < 1e-6);
assert.ok(Math.abs(e.n.x - 1) < 1e-6, '东邻的法向是 +x');
assert.ok(Math.abs(e.u.y - 1) < 1e-6, '共用边竖直');
assert.equal(sharedEdge(b, 1, 3, L), null, 'ne 与 w 不相邻');
const br = bridgePolygon(b, 0, 1, L)!;
assert.equal(br.length, 4);
const wb = wallBand(b, 0, L);
assert.ok(wb.thick > 0 && wb.len > 0);
});
test('几何工具:圆角多边形顶点数、凸裁剪、斜纹都在带内', () => {
const sq = [
{ x: 0, y: 0 },
{ x: 10, y: 0 },
{ x: 10, y: 10 },
{ x: 0, y: 10 },
];
assert.equal(roundPolygon(sq, 2, 3).length, 16);
const tri = [
{ x: -5, y: 5 },
{ x: 15, y: 5 },
{ x: 5, y: 20 },
];
const clipped = clipPolygon(tri, sq);
assert.ok(clipped.length >= 3);
for (const p of clipped) assert.ok(p.x >= -1e-6 && p.x <= 10 + 1e-6 && p.y >= -1e-6 && p.y <= 10 + 1e-6);
const stripes = stripesInBand({ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 0, y: 1 }, 40, 8, 8, 4);
assert.ok(stripes.length >= 4);
for (const s of stripes) for (const p of s) assert.ok(Math.abs(p.x) <= 20 + 1e-6 && Math.abs(p.y) <= 4 + 1e-6);
});