0.2.6-dev: browser tests read the round's real rules — free deploy tiles come from the client's own placement legality (terrain 机变 cards, devices, the boss half) instead of the stage's plain tiles, the facing tour waits for a later round when its purchase merged into an elite, and real.e2e skips its one strategy turn only while 跳过 is enabled (PR #546); golden: unchanged

This commit is contained in:
sganggs committed 2026-10-13 14:20:02 +08:00
1 parent a1f0d1bb6e
commit 2b685011e9
3 files changed
+42 -11

No files matched your search

+30 -7
View File
@@ -342,25 +342,48 @@ export class Client {
await m.up(); await m.up();
} }
/** An empty legal tile for a piece (mirrors the deploy rules: melee on melee tiles, ranged anywhere). */ /**
* An empty legal tile for a piece: the client's own legality (gameLogic/placement.js boardTargets over the screen's
* placement context — the stage's deploy tiles under the player's terrain 机变 cards and devices, the boss field's
* half in a boss round), so a tile a round's 机变 card took away is never chosen; the data's plain deploy tiles
* (melee on melee tiles, ranged anywhere) when that module cannot be read.
*/
freeTileFor(uid, { prefer = 'gate' } = {}) { freeTileFor(uid, { prefer = 'gate' } = {}) {
return this.page.evaluate((uid, prefer) => { return this.page.evaluate(async (uid, prefer) => {
const { store, data } = globalThis.__SP__; const { store, data } = globalThis.__SP__;
const s = store.get(); const s = store.get();
const priv = s.match.private; const priv = s.match.private;
const st = data.lookup('stages', s.match.public.stageId); const pub = s.match.public;
const st = data.lookup('stages', pub.stageId);
const piece = [...priv.hand, ...priv.temp, ...priv.board].find((p) => p && p.uid === uid); const piece = [...priv.hand, ...priv.temp, ...priv.board].find((p) => p && p.uid === uid);
if (!piece || !st?.deployTiles?.normal) return null; if (!piece || !st?.deployTiles?.normal) return null;
const order = (list) => {
const a = list.slice();
if (prefer === 'gate') a.sort((x, y) => (y[1] - x[1]) || (x[0] - y[0]));
else a.sort((x, y) => (x[1] - y[1]) || (y[0] - x[0]));
return a;
};
const used = new Set(priv.board.map((p) => `${p.row},${p.col}`));
try {
const P = await import('/js/ui/gameLogic/placement.js');
const C = await import('/js/ui/gameLogic/camera.js');
const ctx = P.placementContext({
priv, stage: st, editable: true, field: C.deployFieldOf(pub, s.me?.playerId ?? s.me),
getChess: (id) => data.lookup('chess', id), getToken: (id) => data.lookup('tokens', id),
getItem: (id) => data.lookup('items', id), getEffect: (id) => data.lookup('effects', id), backups: data.get('backups'),
});
const legal = P.boardTargets(ctx, uid).legal.filter(([r, c]) => !used.has(`${r},${c}`));
if (legal.length) { const [r, c] = order(legal)[0]; return { row: r, col: c }; }
return null;
} catch { /* the plain deploy tiles below */ }
const rec = piece.kind === 'token' ? data.lookup('tokens', piece.id) : data.lookup('chess', piece.id); const rec = piece.kind === 'token' ? data.lookup('tokens', piece.id) : data.lookup('chess', piece.id);
const melee = rec?.position === 'MELEE'; const melee = rec?.position === 'MELEE';
const dt = st.deployTiles.normal; const dt = st.deployTiles.normal;
const tiles = melee ? dt.melee : [...dt.rangedOnly, ...dt.melee]; const tiles = melee ? dt.melee : [...dt.rangedOnly, ...dt.melee];
const used = new Set(priv.board.map((p) => `${p.row},${p.col}`));
const free = tiles.filter(([r, c]) => !used.has(`${r},${c}`)); const free = tiles.filter(([r, c]) => !used.has(`${r},${c}`));
if (!free.length) return null; if (!free.length) return null;
if (prefer === 'gate') free.sort((a, b) => (b[1] - a[1]) || (a[0] - b[0])); const [r, c] = order(free)[0];
else free.sort((a, b) => (a[1] - b[1]) || (b[0] - a[0])); return { row: r, col: c };
return { row: free[0][0], col: free[0][1] };
}, uid, prefer); }, uid, prefer);
} }
+3 -1
View File
@@ -798,7 +798,9 @@ async function facingTour(c, did) {
if (!did.item && !(await c.handPieces('item')).length) did.itemBought = await buyOne(c, '.shopbar__item .scard:not(.scard--sold):not(.is-disabled)').catch(() => false); if (!did.item && !(await c.handPieces('item')).length) did.itemBought = await buyOne(c, '.shopbar__item .scard:not(.scard--sold):not(.is-disabled)').catch(() => false);
const units = () => c.handPieces().then((h) => h.filter((p) => p.kind === 'chess')); const units = () => c.handPieces().then((h) => h.filter((p) => p.kind === 'chess'));
let hand = await units(); let hand = await units();
assert.ok(hand.length >= 1, 'bought operators'); // a bought operator can merge into an elite at once (two copies already fielded) and leave the bench empty: the
// placement steps then wait for a later round (`did` carries them over)
if (!hand.length) { c.note('facing: no operator on the bench this round (the bought ones merged)'); return; }
// place + choose a direction (UP: striped rotated range + 拖回中心区域取消 while held) — the unit with the widest range // place + choose a direction (UP: striped rotated range + 拖回中心区域取消 while held) — the unit with the widest range
// grid shows it best (a defender's grid is its own tile only) // grid shows it best (a defender's grid is its own tile only)
if (!did.place) { if (!did.place) {
+9 -3
View File
@@ -500,10 +500,16 @@ describe('real server + real browsers', { skip: !ENABLED && 'set SP_REAL_E2E=1 (
if (s.phase !== 'BAND_DRAFT' || picked.has(c.label) || s.draft?.turn !== s.me) continue; if (s.phase !== 'BAND_DRAFT' || picked.has(c.label) || s.draft?.turn !== s.me) continue;
if (!shotDraft) { await sleep(600); await c.shot('draft'); shotDraft = true; } if (!shotDraft) { await sleep(600); await c.shot('draft'); shotDraft = true; }
if (c === guest && !guestSkipped) { if (c === guest && !guestSkipped) {
await c.click('.draft-detail__btns .btn', '跳过');
guestSkipped = true; guestSkipped = true;
c.note('skipped once'); // one skip — only while someone is left to pass to: the last player still to pick sees 跳过 disabled (PR #546)
continue; const canSkip = await c.page.evaluate(() => [...document.querySelectorAll('.draft-detail__btns .btn')]
.some((b) => b.textContent.includes('跳过') && !b.disabled));
if (canSkip) {
await c.click('.draft-detail__btns .btn', '跳过');
c.note('skipped once');
continue;
}
c.note('跳过 disabled (the last player still to pick)');
} }
await c.click('.dband:not(.is-taken)', null, { nth: c === host ? 3 : 7 }); await c.click('.dband:not(.is-taken)', null, { nth: c === host ? 3 : 7 });
await c.click('.draft-detail__btns .btn--primary', '确认选择'); await c.click('.draft-detail__btns .btn--primary', '确认选择');