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:
1 parent
a1f0d1bb6e
commit
2b685011e9
3 files changed
+42
-11
No files matched your search
+30
-7
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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', '确认选择');
|
||||||
|
|||||||
Reference in new issue
Block a user