feedback7: watch-bonds' "after the own battle" test gets a real window — the guest's enemies spawn at twice their times (new fastServer hook SP_SLOW_SPAWNS, startRealServer fast.slowSpawns), the host fields its 萨尔贡 five without 菲莱 (the planner puts a zero-range guard on the road since PR #339 and she held the host's battle to 109 s of 110), and 返回战场 tolerates the battles ending; measured: at the scheduled times a leaker's walk outlasted the host by 5–11 s in 0.2.1 and 0.2.2 alike (the 0.2.2 full pass lost 返回战场 to 联防)
This commit is contained in:
1 parent
e5e3f05bb2
commit
9bacd9ad45
3 files changed
+40
-9
No files matched your search
+4
-2
@@ -61,10 +61,11 @@ export function freePort() {
|
|||||||
* `port`: e.g. the port of a server just stopped — a server restart the open clients reconnect to).
|
* `port`: e.g. the port of a server just stopped — a server restart the open clients reconnect to).
|
||||||
* @param {{ port?: number, fast?: { timerScale?: number, combatSpeed?: number, startRound?: 'boss'|'hidden'|number, kit?: number, chess?: string[],
|
* @param {{ port?: number, fast?: { timerScale?: number, combatSpeed?: number, startRound?: 'boss'|'hidden'|number, kit?: number, chess?: string[],
|
||||||
* items?: string[], idleBots?: boolean, kits?: string[][], botChess?: string[], autoPlace?: boolean, eliminate?: number[], stage?: string,
|
* items?: string[], idleBots?: boolean, kits?: string[][], botChess?: string[], autoPlace?: boolean, eliminate?: number[], stage?: string,
|
||||||
* finishAfter?: number } }} [opts]
|
* finishAfter?: number, slowSpawns?: string } }} [opts]
|
||||||
* kits: per-human starter kits (seat order); botChess: the AI seats' kit; autoPlace: the kits go onto the board at the
|
* kits: per-human starter kits (seat order); botChess: the AI seats' kit; autoPlace: the kits go onto the board at the
|
||||||
* first prep; eliminate: humans (seat order) eliminated at the jump; stage: the stage of every match; finishAfter: the
|
* first prep; eliminate: humans (seat order) eliminated at the jump; stage: the stage of every match; finishAfter: the
|
||||||
* match ends (RESULT) once that round has settled — fastServer.mjs hooks
|
* match ends (RESULT) once that round has settled; slowSpawns: '<humanIdx>:<factor>' — that human's normal battles
|
||||||
|
* spawn at factor × the scheduled times — fastServer.mjs hooks
|
||||||
* @returns {Promise<{ base: string, port: number, logs: string[], stop: (o?: { hard?: boolean }) => Promise<void> }>}
|
* @returns {Promise<{ base: string, port: number, logs: string[], stop: (o?: { hard?: boolean }) => Promise<void> }>}
|
||||||
*/
|
*/
|
||||||
export async function startRealServer(opts = {}) {
|
export async function startRealServer(opts = {}) {
|
||||||
@@ -90,6 +91,7 @@ export async function startRealServer(opts = {}) {
|
|||||||
if (opts.fast.stage) env.SP_STAGE = String(opts.fast.stage);
|
if (opts.fast.stage) env.SP_STAGE = String(opts.fast.stage);
|
||||||
if (opts.fast.level) env.SP_START_LEVEL = String(opts.fast.level);
|
if (opts.fast.level) env.SP_START_LEVEL = String(opts.fast.level);
|
||||||
if (opts.fast.finishAfter) env.SP_FINISH_AFTER = String(opts.fast.finishAfter);
|
if (opts.fast.finishAfter) env.SP_FINISH_AFTER = String(opts.fast.finishAfter);
|
||||||
|
if (opts.fast.slowSpawns) env.SP_SLOW_SPAWNS = String(opts.fast.slowSpawns);
|
||||||
}
|
}
|
||||||
const child = spawn(process.execPath, [entry], { cwd: ROOT, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
const child = spawn(process.execPath, [entry], { cwd: ROOT, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
const logs = [];
|
const logs = [];
|
||||||
|
|||||||
@@ -26,6 +26,9 @@
|
|||||||
// SP_START_SHOP stocks its first slots) — test/ui/diy.e2e.test.js: a tier-5 自选 piece is sold from level 5.
|
// SP_START_SHOP stocks its first slots) — test/ui/diy.e2e.test.js: a tier-5 自选 piece is sold from level 5.
|
||||||
// SP_FINISH_AFTER=<round>: the match ends (a defeat: m.result, the RESULT screen) once that round has settled, instead
|
// SP_FINISH_AFTER=<round>: the match ends (a defeat: m.result, the RESULT screen) once that round has settled, instead
|
||||||
// of going on to the next round — test/ui/standin.e2e.test.js: the result lineup of the board just fought with.
|
// of going on to the next round — test/ui/standin.e2e.test.js: the result lineup of the board just fought with.
|
||||||
|
// SP_SLOW_SPAWNS=<humanIdx>:<factor>: that human's (seat order) normal battles spawn their enemies at <factor> × the
|
||||||
|
// scheduled times (and intervals) — a leaker's battle then runs to the round's time limit, long after a teammate's quick
|
||||||
|
// one (test/ui/watch-bonds.e2e.test.js, "after the own battle": a window for watching and a reload).
|
||||||
// Not a test file (node --test runs it as a no-op module when NODE_TEST_CONTEXT is set).
|
// Not a test file (node --test runs it as a no-op module when NODE_TEST_CONTEXT is set).
|
||||||
|
|
||||||
import { startServer } from '../../server/index.js';
|
import { startServer } from '../../server/index.js';
|
||||||
@@ -52,6 +55,8 @@ if (!process.env.NODE_TEST_CONTEXT) {
|
|||||||
const forcedStage = String(process.env.SP_STAGE || '').trim();
|
const forcedStage = String(process.env.SP_STAGE || '').trim();
|
||||||
const startLevel = Math.max(0, Math.min(6, Number(process.env.SP_START_LEVEL) || 0));
|
const startLevel = Math.max(0, Math.min(6, Number(process.env.SP_START_LEVEL) || 0));
|
||||||
const finishAfter = Math.max(0, Number(process.env.SP_FINISH_AFTER) || 0);
|
const finishAfter = Math.max(0, Number(process.env.SP_FINISH_AFTER) || 0);
|
||||||
|
const [slowIdx, slowFactor] = String(process.env.SP_SLOW_SPAWNS || '').split(':').map(Number);
|
||||||
|
const slowSpawns = Number.isInteger(slowIdx) && slowIdx >= 0 && slowFactor > 0 ? { idx: slowIdx, factor: slowFactor } : null;
|
||||||
|
|
||||||
class FastMatch extends Match {
|
class FastMatch extends Match {
|
||||||
constructor(opts) {
|
constructor(opts) {
|
||||||
@@ -127,6 +132,19 @@ if (!process.env.NODE_TEST_CONTEXT) {
|
|||||||
super.afterSettle();
|
super.afterSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ccField(args) {
|
||||||
|
const f = super._ccField(args);
|
||||||
|
const ps = slowSpawns && f.kind === 'normal' ? this.order.filter((x) => !x.isBot)[slowSpawns.idx] : null;
|
||||||
|
if (ps && f.players.includes(ps.playerId)) {
|
||||||
|
for (const x of f.spec.spawns) {
|
||||||
|
if (!x || !Number.isFinite(Number(x.time))) continue;
|
||||||
|
x.time = Number(x.time) * slowSpawns.factor;
|
||||||
|
if (Number(x.interval) > 0) x.interval = Number(x.interval) * slowSpawns.factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
scheduleBotPrep(ps, i = 0) {
|
scheduleBotPrep(ps, i = 0) {
|
||||||
if (!idleBots) { super.scheduleBotPrep(ps, i); return; }
|
if (!idleBots) { super.scheduleBotPrep(ps, i); return; }
|
||||||
const round = this.round;
|
const round = this.round;
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
// the pair field → its first player's bonds (never its own), taps the second player's operator and its card's bond
|
// the pair field → its first player's bonds (never its own), taps the second player's operator and its card's bond
|
||||||
// chip → the SECOND player's popup (the strip stays on the first), taps each pair player → their half + bonds, taps
|
// chip → the SECOND player's popup (the strip stays on the first), taps each pair player → their half + bonds, taps
|
||||||
// the lone field's player → that field and its player's bonds.
|
// the lone field's player → that field and its player's bonds.
|
||||||
// After the own battle (2 humans + 1 idle AI from round 3, 1× combat): the host's battle ends first, the host watches a
|
// After the own battle (2 humans + 1 idle AI from round 3, 1× combat, the guest's enemies spawning at twice their times):
|
||||||
// teammate still fighting → the strip shows that teammate's bonds with the observing pill "👁 name" + 返回战场; a
|
// the host's battle ends first, the host watches a teammate still fighting → the strip shows that teammate's bonds with the observing pill "👁 name" + 返回战场; a
|
||||||
// reload keeps all three (the resent field is adopted — battle/observe.js resumedWatch); 返回战场 → the own bonds.
|
// reload keeps all three (the resent field is adopted — battle/observe.js resumedWatch); 返回战场 → the own bonds.
|
||||||
// Unit counterparts: test/ui/watch-bonds.test.js (selection logic incl. 联防), test/match/watch-bonds.test.js (views),
|
// Unit counterparts: test/ui/watch-bonds.test.js (selection logic incl. 联防), test/match/watch-bonds.test.js (views),
|
||||||
// test/match/observe.test.js (resumedWatch).
|
// test/match/observe.test.js (resumedWatch).
|
||||||
@@ -39,6 +39,10 @@ const GUEST_KIT = ['chess_char_1_03_a', 'chess_char_1_09_a']; // 惊蛰 (炎),
|
|||||||
// the AI seats five 拉特兰 ones + 古米 (信仰搅拌机, 莫斯提马, 能天使, 空弦, 送葬人; 坚守) — different bonds on each strip
|
// the AI seats five 拉特兰 ones + 古米 (信仰搅拌机, 莫斯提马, 能天使, 空弦, 送葬人; 坚守) — different bonds on each strip
|
||||||
const UNITE_HOST_KIT = ['chess_char_4_23_a', 'chess_char_4_24_a', 'chess_char_3_06_a', 'chess_char_3_13_a', 'chess_char_1_12_a', 'chess_char_2_06_a'];
|
const UNITE_HOST_KIT = ['chess_char_4_23_a', 'chess_char_4_24_a', 'chess_char_3_06_a', 'chess_char_3_13_a', 'chess_char_1_12_a', 'chess_char_2_06_a'];
|
||||||
const BOT_KIT = ['chess_char_4_01_a', 'chess_char_4_02_a', 'chess_char_3_01_a', 'chess_char_3_21_a', 'chess_char_2_01_a', 'chess_char_1_10_a'];
|
const BOT_KIT = ['chess_char_4_01_a', 'chess_char_4_02_a', 'chess_char_3_01_a', 'chess_char_3_21_a', 'chess_char_2_01_a', 'chess_char_1_10_a'];
|
||||||
|
// "after the own battle": the same host without 菲莱 — the layout planner behind fastServer's autoPlace puts a zero-range
|
||||||
|
// guard on a road tile first (PR #339, §27.35), where she holds the enemies and the host's battle could run 109 s of a
|
||||||
|
// 110 s round-3 limit (measured; 53–68 s without her)
|
||||||
|
const AFTER_HOST_KIT = UNITE_HOST_KIT.filter((id) => id !== 'chess_char_3_06_a');
|
||||||
|
|
||||||
/** The strip as shown: whose (data-owner), the bonds (data-bond) with their counts, the tag's text. */
|
/** The strip as shown: whose (data-owner), the bonds (data-bond) with their counts, the tag's text. */
|
||||||
const stripOf = (c) => c.page.evaluate(() => {
|
const stripOf = (c) => c.page.evaluate(() => {
|
||||||
@@ -470,10 +474,12 @@ describe('DESIGN §20.15 — the bond strip shows the watched teammate\'s bonds
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('after the own battle: the watched teammate\'s bonds, kept through a reload with the observing pill + 返回战场 (desktop)', { timeout: 12 * 60 * 1000 }, async () => {
|
test('after the own battle: the watched teammate\'s bonds, kept through a reload with the observing pill + 返回战场 (desktop)', { timeout: 12 * 60 * 1000 }, async () => {
|
||||||
// the host's six 萨尔贡 operators end their battle early; the guest (no board) and the idle AI leak for the enemies'
|
// the host's five 萨尔贡 operators end their battle first; the guest (no board) leaks, and its enemies spawn at twice
|
||||||
// whole walk (combat at 1×: a window for the reload) — the host watches one of them, reloads, then goes back
|
// their times (fastServer SP_SLOW_SPAWNS), so its battle runs to the round's time limit — a window of 40 s and more
|
||||||
|
// for watching it and a reload (at the scheduled times a leaker's walk outlasted the host's battle by 5–11 s in 0.2.1
|
||||||
|
// and 0.2.2 alike, and the 0.2.2 full pass lost 返回战场 to 联防); the host watches the guest, reloads, then goes back
|
||||||
const srv = await startRealServer({ fast: { timerScale: 1, combatSpeed: 1, startRound: 3, idleBots: true, autoPlace: true,
|
const srv = await startRealServer({ fast: { timerScale: 1, combatSpeed: 1, startRound: 3, idleBots: true, autoPlace: true,
|
||||||
kits: [UNITE_HOST_KIT, []] } });
|
kits: [AFTER_HOST_KIT, []], slowSpawns: '1:2' } });
|
||||||
const P = (await import('puppeteer-core')).default;
|
const P = (await import('puppeteer-core')).default;
|
||||||
let host = null;
|
let host = null;
|
||||||
let guest = null;
|
let guest = null;
|
||||||
@@ -539,8 +545,13 @@ describe('DESIGN §20.15 — the bond strip shows the watched teammate\'s bonds
|
|||||||
for (const id of await pubBonds()) assert.ok(st.bonds.includes(id), `after the reload: ${name}'s ${id} on the strip (${JSON.stringify(st.bonds)})`);
|
for (const id of await pubBonds()) assert.ok(st.bonds.includes(id), `after the reload: ${name}'s ${id} on the strip (${JSON.stringify(st.bonds)})`);
|
||||||
await sleep(1500); // the phase banner of the fresh screen
|
await sleep(1500); // the phase banner of the fresh screen
|
||||||
await host.shot('reloaded');
|
await host.shot('reloaded');
|
||||||
// 返回战场 → the own bonds, untagged
|
// 返回战场 → the own bonds, untagged. The watched battle may end meanwhile — 联防 or the settlement takes the
|
||||||
await host.click('.chud__observe .chud__back', '返回战场');
|
// screen and the pill with it (the 0.2.2 full pass) — then the next round tries again
|
||||||
|
if (!(await host.click('.chud__observe .chud__back', '返回战场', { optional: true, timeout: 8000 }))) {
|
||||||
|
const phase = (await host.st()).phase;
|
||||||
|
if (phase !== 'COMBAT') { console.log(`round ${round}: the battles ended before 返回战场 (${phase}), next round`); continue; }
|
||||||
|
throw new Error('host: nothing clickable for .chud__observe .chud__back "返回战场" while the battle runs');
|
||||||
|
}
|
||||||
await host.page.waitForFunction(() => !document.querySelector('.gm__bonds .bstrip')?.getAttribute('data-owner'), { timeout: 8000 });
|
await host.page.waitForFunction(() => !document.querySelector('.gm__bonds .bstrip')?.getAttribute('data-owner'), { timeout: 8000 });
|
||||||
st = await stripOf(host);
|
st = await stripOf(host);
|
||||||
assert.deepEqual([st.owner, st.observe], [null, null], 'back: own bonds, no observing pill');
|
assert.deepEqual([st.owner, st.observe], [null, null], 'back: own bonds, no observing pill');
|
||||||
|
|||||||
Reference in new issue
Block a user