- 玩法一 数字一笔:规则库、求解器、26 关、竖屏触摸 Web 原型、测试 - 玩法二 连连气球:连消畅玩版核心、Web 原型、Cocos 球槽组件与导出脚本 - 玩法三 六边一笔:规则库、求解器、10 关 + 转墙实验包、Web 原型、测试 - 产品文档、UI 设计规范、各玩法 GDD / HANDOFF - 文档移除全部外部参考作引用;删除玩法二 v0 旧稿
80 lines
3.2 KiB
TypeScript
80 lines
3.2 KiB
TypeScript
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);
|
||
});
|