round4: H1 escape目标带实时方位 + H2 simon轮次占用预告(根治手势两义) + H3 重听账目真实化(诊断=初始发放1次vs承诺2次) + H4 input看门狗5s + H5 归因真话(gd_miss_none) + 20语音 + guide120断言全绿
This commit is contained in:
+190
-3
@@ -25,6 +25,7 @@ const GAME_DIRS = {
|
||||
SimonGame: path.join(ROOT, 'web', 'simon'),
|
||||
RhythmGame: path.join(ROOT, 'web', 'rhythm'),
|
||||
SpaceGame: path.join(ROOT, 'web', 'space'),
|
||||
EscapeGame: path.join(ROOT, 'web', 'escape'),
|
||||
};
|
||||
let currentGameDir = GAME_DIRS.StorylineGame;
|
||||
|
||||
@@ -169,6 +170,7 @@ const { StorylineGame } = await import('../storyline/game.js');
|
||||
const { SimonGame } = await import('../simon/game.js');
|
||||
const { RhythmGame } = await import('../rhythm/game.js');
|
||||
const { SpaceGame } = await import('../space/game.js');
|
||||
const { EscapeGame } = await import('../escape/game.js');
|
||||
|
||||
// guide='full':本文件验证向导层行为(既有 playtest 以 guide='off' 验证零干扰)
|
||||
settings.set({ guide: 'full', voice: 'female', speed: 1.0, bgm: 1.0 });
|
||||
@@ -275,6 +277,12 @@ realLog('== ②/①b simon:首轮 turn cue(earcon 事件+confirm 震+your_tu
|
||||
await advance(() => game.teachAwait && game.phase === 'tutorial');
|
||||
await gesture(game, g);
|
||||
}
|
||||
// round4 H2/H4 前提锚定:announce 时点固定第 1 轮序列为无 up/doubletap 的两音
|
||||
//(guide.test 的 Math.random 是真随机——不固定则 gd_relisten 断言会随占用预告
|
||||
// 动态置空而偶发跳过;固定后本块行为与 round3 基线一致)
|
||||
await advance(() => game.phase === 'announce');
|
||||
game.sequence[0] = 'down';
|
||||
game.sequence[1] = 'tap';
|
||||
await advance(() => game.phase === 'input');
|
||||
assert(game.round === 1, '教学走完进入第 1 轮 input');
|
||||
|
||||
@@ -283,12 +291,12 @@ realLog('== ②/①b simon:首轮 turn cue(earcon 事件+confirm 震+your_tu
|
||||
assert(vibrated([20]), 'turn cue 带 confirm 轻震');
|
||||
assert(voicePlays(playLog, 'voice/your_turn') === 1, '第 1 轮保留 your_turn 语音');
|
||||
|
||||
// ①b input 看门狗:10s nudge+gd_relisten
|
||||
// ①b input 看门狗:round4 H4 首级 10s→5s,nudge+gd_relisten
|
||||
const tInput = now;
|
||||
await advance(() => events.some((e) => e.type === 'nudge'));
|
||||
assert(now - tInput === 10000, `input 10s 后 nudge 触发(实际 ${now - tInput}ms)`);
|
||||
assert(now - tInput === 5000, `H4:input 首级 5s 后 nudge 触发(实际 ${now - tInput}ms)`);
|
||||
assert(events.some((e) => e.type === 'watchdog' && e.voiceId === 'gd_relisten'),
|
||||
'input 10s 无输入发 gd_relisten 事件');
|
||||
'input 5s 无输入发 gd_relisten 事件');
|
||||
assert(voicePlays(playLog, 'voice/gd_relisten') === 1, 'gd_relisten 语音播放');
|
||||
assert(game.chances === 1, '看门狗提示不消耗重听机会');
|
||||
|
||||
@@ -599,6 +607,185 @@ realLog('== ⑨ G7 space:loud 警报档(撞击前 500ms)同窗 dir_* 超
|
||||
resetTimers();
|
||||
}
|
||||
|
||||
// ================================================================ ⑩ H1~H5(round4)
|
||||
|
||||
realLog('== ⑩a H2/H3/H4 simon:占用预告三句、被占用轮无重听建议、5s 看门狗、余量句 ==');
|
||||
async function driveSimonToAnnounce(rig) {
|
||||
const { audio, game } = rig;
|
||||
await audio.unlock();
|
||||
game.start();
|
||||
await advance(() => game.phase === 'tutorial' && game.teachAwait);
|
||||
for (const g of ['up', 'down', 'left', 'right', 'tap', 'doubletap']) {
|
||||
await advance(() => game.teachAwait && game.phase === 'tutorial');
|
||||
await gesture(game, g);
|
||||
}
|
||||
await advance(() => game.phase === 'announce');
|
||||
return game;
|
||||
}
|
||||
/** 测试钩子(13 号文档允许直改实例字段):固定当前轮序列;未指定位用无 up/dt 填充。 */
|
||||
function fixSequence(game, recipe) {
|
||||
const fill = ['down', 'right', 'tap', 'left'];
|
||||
for (let i = 0; i < game.sequence.length; i++) {
|
||||
game.sequence[i] = i < recipe.length ? recipe[i] : fill[i % fill.length];
|
||||
}
|
||||
}
|
||||
{
|
||||
const rig = makeRig(SimonGame);
|
||||
const { playLog, game, events } = rig;
|
||||
await driveSimonToAnnounce(rig);
|
||||
|
||||
// R1(含 up):序列播放完成后、「轮到你」提示前播 gd_occ_up
|
||||
fixSequence(game, ['up', 'tap']);
|
||||
await advance(() => game.phase === 'input');
|
||||
assert(voicePlays(playLog, 'voice/gd_occ_up') === 1, 'H2:含 up 轮次播 gd_occ_up(序列播放完成后)');
|
||||
const iOcc = playLog.findIndex((e) => e.id === 'voice/gd_occ_up');
|
||||
const iTurn = playLog.findIndex((e) => e.id === 'voice/your_turn');
|
||||
assert(iOcc > -1 && iTurn > iOcc, 'H2:占用预告在「轮到你」提示之前(插在轮次链中)');
|
||||
|
||||
// 被占用轮 input 看门狗:H4 首级 5s 只发 nudge,不播重听建议句(say 动态置空)
|
||||
const tInput = now;
|
||||
const rl0 = voicePlays(playLog, 'voice/gd_relisten');
|
||||
const nb0 = events.filter((e) => e.type === 'nudge').length;
|
||||
await advance(() => events.filter((e) => e.type === 'nudge').length > nb0);
|
||||
assert(now - tInput === 5000, `H4:input 看门狗首级 5s(实际 ${now - tInput}ms)`);
|
||||
assert(voicePlays(playLog, 'voice/gd_relisten') === rl0,
|
||||
'H2:up 被占用轮看门狗只发 nudge earcon,不播重听建议句');
|
||||
|
||||
// 期望恰为 up 时上滑=作答(00 §5.3 豁免同款)→ 清第 1 轮
|
||||
for (const g of game.sequence) await gesture(game, g);
|
||||
await advance(() => game.phase === 'announce' && game.round === 2);
|
||||
|
||||
// R2(仅含 doubletap):gd_occ_dt;重听建议不受影响(建议只关联 up 占用)
|
||||
fixSequence(game, ['down', 'doubletap', 'left']);
|
||||
await advance(() => game.phase === 'input');
|
||||
assert(voicePlays(playLog, 'voice/gd_occ_dt') === 1, 'H2:含 doubletap 轮次播 gd_occ_dt');
|
||||
const t2 = now;
|
||||
const rl1 = voicePlays(playLog, 'voice/gd_relisten');
|
||||
const nb1 = events.filter((e) => e.type === 'nudge').length;
|
||||
await advance(() => events.filter((e) => e.type === 'nudge').length > nb1);
|
||||
assert(now - t2 === 5000, 'H4:看门狗首级恒 5s(升级级 25s 与级数不变)');
|
||||
assert(voicePlays(playLog, 'voice/gd_relisten') === rl1 + 1,
|
||||
'H2:仅 doubletap 占用不影响上滑重听建议');
|
||||
for (const g of game.sequence) await gesture(game, g);
|
||||
await advance(() => game.phase === 'announce' && game.round === 3);
|
||||
|
||||
// R3(up+doubletap 双占用):gd_occ_both;看门狗同样静默只 nudge
|
||||
fixSequence(game, ['up', 'doubletap', 'left', 'right']);
|
||||
await advance(() => game.phase === 'input');
|
||||
assert(voicePlays(playLog, 'voice/gd_occ_both') === 1, 'H2:up+doubletap 双占用播 gd_occ_both');
|
||||
const rl2 = voicePlays(playLog, 'voice/gd_relisten');
|
||||
const nb2 = events.filter((e) => e.type === 'nudge').length;
|
||||
await advance(() => events.filter((e) => e.type === 'nudge').length > nb2);
|
||||
assert(voicePlays(playLog, 'voice/gd_relisten') === rl2, 'H2:双占用轮看门狗同样不播重听建议');
|
||||
for (const g of game.sequence) await gesture(game, g);
|
||||
await advance(() => game.phase === 'announce' && game.round === 4);
|
||||
|
||||
// H3:余量句与真实余量一致——第 1 次免费重听用掉(2→1)播 left1,第 2 次(1→0)播 left0
|
||||
fixSequence(game, []); // 无占用填充轮
|
||||
await advance(() => game.phase === 'input');
|
||||
game.chances = 2; // 测试钩子:满额免费重听(初始 1 + 第 4 轮完成 +1,上限 2)
|
||||
const rel0 = voicePlays(playLog, 'voice/relisten');
|
||||
const l1_0 = voicePlays(playLog, 'voice/gd_chances_left1');
|
||||
await gesture(game, 'up'); // 期望非 up → 主动重听①:2→1
|
||||
await advance(() => voicePlays(playLog, 'voice/gd_chances_left1') === l1_0 + 1); // 余量句在 relisten 链尾
|
||||
assert(game.chances === 1 && voicePlays(playLog, 'voice/gd_chances_left1') === l1_0 + 1,
|
||||
'H3:第 1 次免费重听用掉播 gd_chances_left1(余量句与真实余量一致)');
|
||||
assert(voicePlays(playLog, 'voice/relisten') === rel0 + 1, 'H3:重听照常播 relisten 并重播序列');
|
||||
await advance(() => game.phase === 'input');
|
||||
assert(game.inputIdx === 0, 'H3:重听后输入进度清零');
|
||||
const l0_0 = voicePlays(playLog, 'voice/gd_chances_left0');
|
||||
await gesture(game, 'up'); // 重听②:1→0
|
||||
await advance(() => voicePlays(playLog, 'voice/gd_chances_left0') === l0_0 + 1);
|
||||
assert(game.chances === 0 && voicePlays(playLog, 'voice/gd_chances_left0') === l0_0 + 1,
|
||||
'H3:第 2 次免费重听用掉播 gd_chances_left0(「用完了」只在真实余量为 0 时出现)');
|
||||
await advance(() => game.phase === 'input');
|
||||
const co0 = voicePlays(playLog, 'voice/chances_out');
|
||||
await gesture(game, 'up'); // 余 0 再求重听:拒绝路径(机会账)行为不变
|
||||
assert(game.phase === 'input' && voicePlays(playLog, 'voice/chances_out') === co0 + 1,
|
||||
'H3:余 0 再求重听仍拒绝并播 chances_out(答错救援的机会账路径不变)');
|
||||
while (game.phase === 'input') await gesture(game, game.sequence[game.inputIdx]);
|
||||
game.stop();
|
||||
resetTimers();
|
||||
}
|
||||
|
||||
realLog('== ⑩b H5 rhythm:教学应答窗口无任何尝试 → gd_miss_none(none 优先级最高)==');
|
||||
{
|
||||
const rig = makeRig(RhythmGame);
|
||||
const { audio, playLog, game } = rig;
|
||||
await audio.unlock();
|
||||
game.start();
|
||||
await advance(() => audio.isVoicePlaying()); // welcome 开播
|
||||
await gesture(game, 'tap'); // 播放中单击 = 跳过 → 教学 T1
|
||||
await advance(() => game.phase === 'tut' && game._cues.length > 0);
|
||||
// T1 整句不出手(窗口关闭时该音未判定)→ 首次失败归因 gd_miss_none
|
||||
await advance(() => voicePlays(playLog, 'voice/gd_miss_none') === 1);
|
||||
assert(game._tutFails === 1, '前提:T1 首次失败(整句不出手)');
|
||||
assert(voicePlays(playLog, 'voice/gd_miss_none') === 1, 'H5:无尝试播 gd_miss_none(这拍没接)');
|
||||
assert(voicePlays(playLog, 'voice/gd_miss_late') === 0 && voicePlays(playLog, 'voice/gd_miss_wrong') === 0,
|
||||
'H5:没出手不再被归因为「慢了半拍」(归因真话律:宁可粗不可假)');
|
||||
game.stop();
|
||||
resetTimers();
|
||||
}
|
||||
|
||||
realLog('== ⑩c H1 escape:目标句后按房间图播 gd_room_*(同房间不播,移动坐标系)==');
|
||||
{
|
||||
// 方向映射按图校验(直连单元断言)
|
||||
const rig0 = makeRig(EscapeGame);
|
||||
const { game: g0 } = rig0;
|
||||
await rig0.audio.unlock();
|
||||
g0.x = 1; g0.y = 1;
|
||||
assert(g0._roomDirVoice([0, 1]) === 'voice/gd_room_l', 'H1:目标在西 → gd_room_l');
|
||||
assert(g0._roomDirVoice([2, 1]) === 'voice/gd_room_r', 'H1:目标在东 → gd_room_r');
|
||||
assert(g0._roomDirVoice([1, 0]) === 'voice/gd_room_u', 'H1:目标在北(上面的房间)→ gd_room_u');
|
||||
assert(g0._roomDirVoice([1, 2]) === 'voice/gd_room_d', 'H1:目标在南(下面的房间)→ gd_room_d');
|
||||
assert(g0._roomDirVoice([1, 1]) === null, 'H1:目标在同一房间不播');
|
||||
g0.stop();
|
||||
resetTimers();
|
||||
|
||||
// 三处目标句接线点:found_key → cabinet(2,1);gd_next_fuse → box(0,0);fix_box → gate(2,2)
|
||||
const rig = makeRig(EscapeGame);
|
||||
const { audio, playLog, game } = rig;
|
||||
storageData.set('soundgame.escape.guideDone', '1'); // 二周目直达 REOPEN→PLAY
|
||||
await audio.unlock();
|
||||
game.start();
|
||||
await advance(() => game.phase === 'REOPEN');
|
||||
await advance(() => !audio.isVoicePlaying()); // reopen_prompt 播完
|
||||
await gesture(game, 'tap'); // 直接开始 → PLAY (1,1)
|
||||
await advance(() => game.phase === 'PLAY' && !audio.isVoicePlaying());
|
||||
assert(game.x === 1 && game.y === 1 && !game.hasKey, '前提:二周目 PLAY 起点、身上无钥匙');
|
||||
|
||||
// ① found_key:储物间拾钥匙 → 铁柜房在东(右面的房间)
|
||||
await gesture(game, 'left'); // (0,1)
|
||||
await gesture(game, 'down'); // (0,2) 储物间(首到播 room_storage)
|
||||
await advance(() => !audio.isVoicePlaying()); // 房间描述播完再单击(否则单击=跳句)
|
||||
await gesture(game, 'tap'); // 检查 → found_key(800ms 后)→ 方位句
|
||||
await advance(() => playLog.some((e) => e.id === 'voice/gd_room_r'));
|
||||
const ifk = playLog.findIndex((e) => e.id === 'voice/found_key');
|
||||
assert(ifk > -1 && playLog.findIndex((e) => e.id === 'voice/gd_room_r') > ifk,
|
||||
'H1:found_key 后接播 gd_room_r(目标铁柜房按图在东)');
|
||||
|
||||
// ② gd_next_fuse:开柜得保险丝 → 电箱房在西(左面的房间)
|
||||
await gesture(game, 'right'); await gesture(game, 'right'); // (1,2) (2,2)
|
||||
await gesture(game, 'up'); // (2,1) 铁柜房
|
||||
await gesture(game, 'doubletap'); // 开柜
|
||||
await advance(() => playLog.some((e) => e.id === 'voice/gd_room_l'));
|
||||
const inf = playLog.findIndex((e) => e.id === 'voice/gd_next_fuse');
|
||||
assert(inf > -1 && playLog.findIndex((e) => e.id === 'voice/gd_room_l') > inf,
|
||||
'H1:gd_next_fuse 后接播 gd_room_l(目标电箱房按图在西)');
|
||||
|
||||
// ③ fix_box:修电箱通电 → 大门在东(右面的房间)
|
||||
await gesture(game, 'left'); await gesture(game, 'left'); // (1,1) (0,1)
|
||||
await gesture(game, 'up'); // (0,0) 电箱房
|
||||
await gesture(game, 'doubletap'); // 修电箱
|
||||
await advance(() => playLog.filter((e) => e.id === 'voice/gd_room_r').length >= 2);
|
||||
const ifx = playLog.findIndex((e) => e.id === 'voice/fix_box');
|
||||
assert(ifx > -1 && playLog.map((e) => e.id).lastIndexOf('voice/gd_room_r') > ifx,
|
||||
'H1:fix_box 后接播 gd_room_r(目标大门按图在东)');
|
||||
assert(game.powerOn, '前提:电箱已通电(接线点均在真实解谜链上)');
|
||||
game.stop();
|
||||
resetTimers();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 汇总
|
||||
|
||||
realLog(`\n通过 ${passCount} 条断言,失败 ${failures.length} 条`);
|
||||
|
||||
Reference in New Issue
Block a user