初始提交:三玩法 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
+87
View File
@@ -0,0 +1,87 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { HexBoard } from '../src/board';
import { Game } from '../src/game';
import { PointerInput } from '../src/input';
import type { HexCellDef } from '../src/types';
const c = (id: string, q: number, r: number, clue?: number): HexCellDef => ({ id, q, r, clue });
const level13 = () => HexBoard.fromCells([c('a', 0, 0, 1), c('b', 1, 0), c('c', 0, 1), c('d', 1, 1, 4)], [['a', 'b']]);
function setup() {
const b = level13();
const g = new Game(b);
const input = new PointerInput(g);
const id = (s: string) => b.cellById(s).index;
return { b, g, input, id };
}
test('按下起点、滑到邻格追加、滑回撤销、松手保留', () => {
const { g, input, id } = setup();
assert.equal(input.down(id('a'))[0].type, 'step');
assert.ok(input.isDrawing);
assert.equal(input.move(id('c'))[0].type, 'step');
assert.deepEqual(input.move(id('c')), [], '同一格不重复触发');
assert.equal(input.move(id('a'))[0].type, 'unstep');
assert.equal(g.path.length, 1);
input.move(id('c'));
input.up();
assert.equal(g.status, 'idle');
assert.equal(g.path.length, 2);
});
test('滑向有墙的邻格 → wallHit 一次,路径不变', () => {
const { g, input, id } = setup();
input.down(id('a'));
const ev = input.move(id('b'));
assert.equal(ev.length, 1);
assert.equal(ev[0].type, 'wallHit');
assert.deepEqual(input.move(id('b')), [], '停在同一格不重复报');
assert.equal(g.path.length, 1);
// 指针逻辑上仍在末端:继续滑到 c 可追加
assert.equal(input.move(id('c'))[0].type, 'step');
});
test('点末端继续、点前一格撤销、点旁边空格追加、点墙外邻格报撞墙', () => {
const { g, input, id } = setup();
input.down(id('a'));
input.move(id('c'));
input.up();
assert.equal(input.down(id('c')).length, 0);
assert.ok(input.isDrawing);
input.up();
assert.equal(input.down(id('a'))[0].type, 'unstep');
assert.equal(g.path.length, 1);
input.up();
assert.equal(input.down(id('c'))[0].type, 'step', '点着走');
input.up();
// 路径 a,c;c 的邻居 b 无墙 → 追加;再从 b 去 a 已走 → 忽略
assert.equal(input.down(id('b'))[0].type, 'step');
input.up();
// 从 a 起手时点 b:撞墙反馈
input.cancel();
g.apply({ type: 'reset' });
input.down(id('a'));
input.up();
assert.equal(input.down(id('b'))[0].type, 'wallHit');
});
test('非法起手与空白按下', () => {
const { input, id } = setup();
assert.equal(input.down(id('b'))[0].type, 'rejectStart');
assert.ok(!input.isDrawing);
assert.deepEqual(input.down(-1), []);
assert.deepEqual(input.move(id('a')), [], '没在画,move 无效');
});
test('过关后手势失效', () => {
const { g, input, id } = setup();
input.down(id('a'));
input.move(id('c'));
input.move(id('b'));
const ev = input.move(id('d'));
assert.ok(ev.some((e) => e.type === 'cleared'));
assert.ok(!input.isDrawing);
assert.equal(g.isCleared, true);
assert.deepEqual(input.down(id('d')), []);
});