初始提交:三玩法 Web 原型与设计文档
- 玩法一 数字一笔:规则库、求解器、26 关、竖屏触摸 Web 原型、测试 - 玩法二 连连气球:连消畅玩版核心、Web 原型、Cocos 球槽组件与导出脚本 - 玩法三 六边一笔:规则库、求解器、10 关 + 转墙实验包、Web 原型、测试 - 产品文档、UI 设计规范、各玩法 GDD / HANDOFF - 文档移除全部外部参考作引用;删除玩法二 v0 旧稿
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { Board } from '../src/board';
|
||||
import { Game } from '../src/game';
|
||||
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 });
|
||||
|
||||
// 1-3:start(1) a b(3) / d c
|
||||
const level13 = () =>
|
||||
Board.fromCells([c('start', 0, 0, 1, 1, 1), c('a', 1, 0), c('b', 2, 0, 1, 1, 3), c('d', 1, 1), c('c', 2, 1)]);
|
||||
|
||||
test('有线索 1 时只能从它起手', () => {
|
||||
const g = new Game(level13());
|
||||
const ev = g.apply({ type: 'start', cell: 1 });
|
||||
assert.deepEqual(ev, [{ type: 'rejectStart', cell: 1, clueCell: 0 }]);
|
||||
assert.equal(g.status, 'empty');
|
||||
assert.equal(g.apply({ type: 'start', cell: 0 })[0].type, 'step');
|
||||
assert.equal(g.status, 'drawing');
|
||||
});
|
||||
|
||||
test('无线索 1 时任意格可起手', () => {
|
||||
const g = new Game(Board.fromCells([c('a', 0, 0), c('b', 1, 0, 1, 1, 2)]));
|
||||
assert.equal(g.apply({ type: 'start', cell: 0 })[0].type, 'step');
|
||||
});
|
||||
|
||||
test('追加只能到相邻未走格;冲突不打断;撤销恢复', () => {
|
||||
const b = level13();
|
||||
const g = new Game(b);
|
||||
const id = (s: string) => b.cellById(s).index;
|
||||
g.apply({ type: 'start', cell: id('start') });
|
||||
assert.deepEqual(g.apply({ type: 'append', cell: id('c') }), []); // 不相邻
|
||||
g.apply({ type: 'append', cell: id('a') });
|
||||
const ev = g.apply({ type: 'append', cell: id('d') }); // 第 3 步没落到预定的 b,立即冲突
|
||||
assert.equal(ev[0].type, 'step');
|
||||
assert.ok(ev.some(e => e.type === 'step' && e.conflict && !e.clueHit));
|
||||
assert.equal(g.isConflict(id('d')), true);
|
||||
assert.equal(g.stepOf(id('b')), 0, '无需先走到线索格,就能判断该步位置错误');
|
||||
g.apply({ type: 'append', cell: id('c') });
|
||||
const conflict = g.apply({ type: 'append', cell: id('b') });
|
||||
assert.equal(conflict[0].type, 'step');
|
||||
assert.equal((conflict[0] as { conflict: boolean }).conflict, true);
|
||||
assert.equal(g.isConflict(id('b')), true);
|
||||
assert.equal(g.path.length, 5);
|
||||
assert.equal(g.status, 'drawing', '填满但有冲突不算过关');
|
||||
// 走到头了但没过关 → deadEnd 事件
|
||||
assert.ok(conflict.some((e) => e.type === 'deadEnd'));
|
||||
const un = g.apply({ type: 'undo' });
|
||||
assert.deepEqual(un, [{ type: 'unstep', cell: id('b'), step: 5 }]);
|
||||
assert.equal(g.isConflict(id('b')), false);
|
||||
assert.equal(g.stepOf(id('b')), 0);
|
||||
});
|
||||
|
||||
test('全走完且线索全对 → cleared,之后锁住', () => {
|
||||
const b = level13();
|
||||
const g = new Game(b);
|
||||
const id = (s: string) => b.cellById(s).index;
|
||||
for (const s of ['start', 'a', 'b', 'c']) g.apply(g.path.length ? { type: 'append', cell: id(s) } : { type: 'start', cell: id(s) });
|
||||
const ev = g.apply({ type: 'append', cell: id('d') });
|
||||
assert.ok(ev.some((e) => e.type === 'cleared'));
|
||||
assert.equal(g.status, 'cleared');
|
||||
assert.deepEqual(g.apply({ type: 'undo' }), []);
|
||||
assert.equal(g.path.length, 5);
|
||||
});
|
||||
|
||||
test('release / resume / reset 状态流转', () => {
|
||||
const g = new Game(level13());
|
||||
g.apply({ type: 'start', cell: 0 });
|
||||
g.apply({ type: 'release' });
|
||||
assert.equal(g.status, 'idle');
|
||||
g.apply({ type: 'resume' });
|
||||
assert.equal(g.status, 'drawing');
|
||||
assert.deepEqual(g.apply({ type: 'reset' }), [{ type: 'reset' }]);
|
||||
assert.equal(g.status, 'empty');
|
||||
assert.equal(g.remaining, 5);
|
||||
assert.deepEqual(g.apply({ type: 'reset' }), [], '空盘重开不产生事件');
|
||||
});
|
||||
|
||||
test('撤销到空盘回到 empty', () => {
|
||||
const g = new Game(level13());
|
||||
g.apply({ type: 'start', cell: 0 });
|
||||
g.apply({ type: 'undo' });
|
||||
assert.equal(g.status, 'empty');
|
||||
assert.equal(g.last, -1);
|
||||
});
|
||||
|
||||
test('直接回退移除后缀冲突,保留前缀;非法目标、末端和过关后均忽略', () => {
|
||||
const g = new Game(level13());
|
||||
for (const cell of [0, 1, 3, 4, 2]) g.apply({ type: g.path.length ? 'append' : 'start', cell });
|
||||
assert.equal(g.hasConflict(), true);
|
||||
g.apply({ type: 'rewind', cell: 1 });
|
||||
assert.deepEqual(g.path, [0, 1]);
|
||||
assert.equal(g.hasConflict(), false);
|
||||
for (const cell of [-1, 4, 1]) assert.deepEqual(g.apply({ type: 'rewind', cell }), []);
|
||||
for (const cell of [2, 4, 3]) g.apply({ type: 'append', cell });
|
||||
assert.equal(g.isCleared, true);
|
||||
assert.deepEqual(g.apply({ type: 'rewind', cell: 0 }), []);
|
||||
assert.equal(g.path.length, 5);
|
||||
});
|
||||
Reference in New Issue
Block a user