初始提交:三玩法 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,59 @@
// 通过 ego-browser nodejs < tools/browser-smoke.mjs 运行;仅访问本地测试页面。
// 空间编号由首次 browser QA 建立,同一任务一直复用该空间。
const space = Number(process.env.GARDEN_SPACE);
if (!space || !process.env.GARDEN_QA_DIR) throw new Error('请设置 GARDEN_SPACE 与 GARDEN_QA_DIR,复用当前 QA 空间。');
const task = await taskSpace(space), page = task.page('p1');
await page.reload();
await page.waitForFunction(() => !!window.__garden);
const fs = await import('node:fs/promises');
const root = process.env.GARDEN_QA_DIR;
const report = { checks: [], levels: [] };
await page.evaluate(() => { window.__qaErrors = []; window.addEventListener('error', e => window.__qaErrors.push(e.message)); window.addEventListener('unhandledrejection', e => window.__qaErrors.push(String(e.reason))); });
const info = () => page.evaluate(() => {
const g = window.__garden, s = g.snapshot(), t = g.transform(), r = document.getElementById('board').getBoundingClientRect();
return { snapshot: s, level: g.level(), hint: g.hint(), points: g.hint().map(id => { const b = s.balls.find(b => b.id === id); return { x: r.left + t.x + b.x * t.scale, y: r.top + t.y + b.y * t.scale }; }) };
});
const send = async (type, points) => page.cdp('Input.dispatchTouchEvent', { type, touchPoints: points.map((p, id) => ({...p, id, radiusX: 5, radiusY: 5, force: 1})) });
let initial = await info();
if (initial.level.id !== 1) throw new Error('QA expects an untouched campaign');
// 两颗松手、系统取消、回滑撤回均不得扣步。
await send('touchStart', [initial.points[0]]); await send('touchMove', [initial.points[1]]); await send('touchEnd', []);
if ((await info()).snapshot.stats.merges !== 0) throw new Error('short gesture committed');
report.checks.push('两颗松手不扣步');
await send('touchStart', [initial.points[0]]); await send('touchMove', [initial.points[1]]); await send('touchCancel', []);
if ((await info()).snapshot.stats.merges !== 0) throw new Error('cancel committed');
report.checks.push('触摸取消不提交');
await send('touchStart', [initial.points[0]]); await send('touchMove', [initial.points[1]]); await send('touchMove', [initial.points[2]]); await send('touchMove', [initial.points[1]]); await send('touchEnd', []);
if ((await info()).snapshot.stats.merges !== 0) throw new Error('backtrack failed');
report.checks.push('回滑撤回');
await page.click('#sound');
await page.click('#hint');
await page.screenshot({path: `${root}/mobile-initial.png`});
for (let expected = 1; expected <= 10; expected++) {
let data = await info();
if (data.level.id !== expected) throw new Error(`Expected L${expected}, got ${data.level.id}`);
for (let turn = 0; turn < data.level.moves; turn++) {
data = await info();
if (data.snapshot.status === 'won') break;
if (data.snapshot.status !== 'playing') throw new Error(`L${expected} prematurely ${data.snapshot.status}`);
const previous = data.snapshot.stats.merges;
await send('touchStart', [data.points[0]]);
for (const p of data.points.slice(1)) await send('touchMove', [p]);
await send('touchEnd', []);
await page.waitForFunction(previous => window.__garden.snapshot().stats.merges > previous && !window.__garden.animating(), previous, { timeout: 5000 });
}
data = await info();
if (data.snapshot.status !== 'won') throw new Error(`L${expected} not won`);
report.levels.push({ level: expected, moves: data.snapshot.stats.merges, score: data.snapshot.stats.score });
console.log(`触摸通关 L${expected} ${data.snapshot.stats.merges} 步 ${data.snapshot.stats.score} 分`);
if (expected === 7) await page.screenshot({path: `${root}/mobile-result.png`});
if (expected < 10) {
await page.click('[data-action="next"]');
if (expected === 7) await page.screenshot({path: `${root}/mobile-level8.png`});
}
}
await page.screenshot({path: `${root}/mobile-all-clear.png`});
report.errors = await page.evaluate(() => window.__qaErrors);
if (report.errors.length) throw new Error(JSON.stringify(report.errors));
await fs.writeFile(`${root}/browser.json`, JSON.stringify(report, null, 2));
console.log(await page.snapshot());