feedback1 WB: bounty drafts and the 机密商店 from 66 official screenshots (22 matches)
Bounty drafts: every 悬赏决策 event is a fixed card list and the draft shows 6 different cards of it, drawn with weight 1 + the official drafts of its group the card showed in. - R3: 9 of the 10 fixed sets seen (two new: 20_1 + 20_5, and one with 17_6); 3 x I + 2 x II + 1 x III in 22 of 22. The unseen 10th set is built by a rule whose III is 10_6 or 20_6, the only two-battle cards no draft showed. - R9: 6 groups of 9 cards, one per bossInitial event. The 鼠王 group came in 14 of 22 matches (every grey-board match among them), so groups are picked by the matches they came in. Named bosses no longer always come together. - R11: lists of 7 cards, the draft leaves out one (matches 1 / 6 / 21 / 22 leave out four different cards of one list); 7 of 15 lists seen, the rest and the 4 partial lists' 7th cards built by rule. Families 14 / 4 / 4. 机密商店 (the user: "机密商店按官方改成可以重复吧"): choices.json shopDraft, six slots drawn with replacement - VI, VI, V, 盟约之币 and twice V / IV / III / 盟约之币, items weighted by the official cards they showed on. A repeated item now comes in about 61 % of shops (official 3 of 4). Two identical cards stay two cards (picks by index): E2E, overlay and a headless-Chrome test (game-mock ?variant=shop, the official match 8 shop).
This commit is contained in:
1 parent
7c13e38422
commit
d1f8a49829
12 files changed
+799
-261
No files matched your search
@@ -0,0 +1,83 @@
|
||||
// The 机密商店 overlay with the same item twice, in headless Chrome (puppeteer-core + system Chrome) through the mock
|
||||
// harness (public/dev/game-mock.html ?phase=SP_DRAFT&variant=shop: the official 机密商店 of match 8 R11 — 变形同构体 in
|
||||
// slots 1 and 4). Opt-in: SP_E2E=1.
|
||||
//
|
||||
// SP_E2E=1 node --test test/ui/feedback1-secret-shop.e2e.test.js
|
||||
//
|
||||
// Two identical cards render as two cards; a teammate taking the first leaves the second pickable; two taps on the
|
||||
// second pick it — its own index goes to the server (g.choice), only it shows my avatar. Screenshots:
|
||||
// test/e2e/out/fb1-secret-shop-*.png.
|
||||
import { test, describe, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync, mkdirSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const OUT = path.join(ROOT, 'test/e2e/out');
|
||||
const CHROME = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
||||
const ENABLED = process.env.SP_E2E === '1' && existsSync(CHROME);
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
describe('机密商店: the same item twice (mock harness, headless Chrome)', { skip: !ENABLED && 'set SP_E2E=1 (and have Chrome) to run' }, () => {
|
||||
let srv;
|
||||
let browser;
|
||||
let base;
|
||||
|
||||
before(async () => {
|
||||
const { startServer } = await import('../../server/index.js');
|
||||
const puppeteer = (await import('puppeteer-core')).default;
|
||||
srv = await startServer({ port: 0, host: '127.0.0.1', quiet: true });
|
||||
base = `http://127.0.0.1:${srv.port}`;
|
||||
browser = await puppeteer.launch({ executablePath: CHROME, headless: true, args: ['--no-sandbox'] });
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await browser?.close();
|
||||
await srv?.close();
|
||||
});
|
||||
|
||||
const cards = (page) => page.$$eval('.spov__grid .spcard', (els) => els.map((c) => ({
|
||||
name: c.querySelector('.spcard__name')?.textContent, taken: c.classList.contains('is-taken'), mine: c.classList.contains('is-mine'),
|
||||
armed: c.classList.contains('is-armed'), disabled: c.disabled, badge: !!c.querySelector('.spcard__taker'),
|
||||
})));
|
||||
|
||||
for (const [name, w, h, touch] of [['756x366', 756, 366, true], ['1920x1080', 1920, 1080, false]]) {
|
||||
test(`${name}: two 变形同构体 are two cards; the second stays pickable after the first is taken, and picking it sends its own index`, { timeout: 120000 }, async () => {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: w, height: h, deviceScaleFactor: 1, isMobile: touch, hasTouch: touch });
|
||||
const problems = [];
|
||||
page.on('pageerror', (e) => problems.push(`pageerror: ${e.message}`));
|
||||
page.on('console', (m) => { if (m.type() === 'error') problems.push(`console: ${m.text()}`); });
|
||||
await page.goto(`${base}/dev/game-mock.html?shot=1&phase=SP_DRAFT&variant=shop`, { waitUntil: 'networkidle0' });
|
||||
await page.waitForSelector('.spov__grid .spcard', { timeout: 20000 });
|
||||
await sleep(600);
|
||||
// the teammate p4 has taken the first 变形同构体 (slot 0)
|
||||
await page.evaluate(() => { const S = globalThis.__MOCK__.S(); S.pub.sp = { ...S.pub.sp, picks: { p4: 0 }, turn: 'p1' }; globalThis.__MOCK__.pushPublic(); });
|
||||
await sleep(300);
|
||||
let c = await cards(page);
|
||||
assert.equal(c.length, 6);
|
||||
assert.equal(c[0].name, '变形同构体');
|
||||
assert.equal(c[3].name, '变形同构体');
|
||||
assert.ok(c[0].taken && c[0].badge && c[0].disabled, 'the first twin: taken, with the taker\'s avatar');
|
||||
assert.ok(!c[3].taken && !c[3].badge && !c[3].disabled, 'the second twin: free');
|
||||
await page.screenshot({ path: path.join(OUT, `fb1-secret-shop-${name}.png`) });
|
||||
// two taps on the second twin
|
||||
const tap = async (i) => { const btn = (await page.$$('.spov__grid .spcard'))[i]; if (touch) await btn.tap(); else await btn.click(); await sleep(250); };
|
||||
await tap(3);
|
||||
c = await cards(page);
|
||||
assert.deepEqual(c.map((x) => x.armed), [false, false, false, true, false, false], 'only the tapped twin is armed');
|
||||
await page.screenshot({ path: path.join(OUT, `fb1-secret-shop-${name}-armed.png`) });
|
||||
await tap(3);
|
||||
const picks = await page.evaluate(() => ({ ...globalThis.__MOCK__.S().pub.sp.picks }));
|
||||
assert.deepEqual(picks, { p4: 0, p1: 3 }, 'g.choice sent the second twin\'s index');
|
||||
await sleep(250);
|
||||
c = await cards(page);
|
||||
assert.ok(c[3].mine && c[3].badge, 'the second twin is mine');
|
||||
assert.ok(c[0].taken && !c[0].mine, 'the first twin stays the teammate\'s');
|
||||
assert.deepEqual(problems, []);
|
||||
await page.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
// The 机密商店 overlay with the same item twice (the user: "机密商店按官方改成可以重复吧"; official match 8 R11: 变形同构体 ×2,
|
||||
// matches 4 / 5: 盟约之币 ×2). A real server draft (server/match/choices.js generateDraft → cardView), through m.public's
|
||||
// normalisation (ui/gameLogic.js normalizeSp), rendered by the overlay's pure view (ui/choiceOverlay.js ChoiceView): two
|
||||
// identical cards are two buttons keyed by their index — selecting one does not select the other, a teammate taking one
|
||||
// leaves the other pickable, and the pick sends that card's own index. The browser render is
|
||||
// test/ui/feedback1-secret-shop.e2e.test.js (SP_E2E=1).
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
|
||||
// the browser data store reads the real data files from disk
|
||||
globalThis.fetch = async (url) => {
|
||||
const name = String(url).split('/').pop();
|
||||
try {
|
||||
const body = readFileSync(path.join(ROOT, 'data', name), 'utf8');
|
||||
return { ok: true, status: 200, json: async () => JSON.parse(body) };
|
||||
} catch {
|
||||
return { ok: false, status: 404, json: async () => ({}) };
|
||||
}
|
||||
};
|
||||
|
||||
const { ChoiceView, cardPickable, spTap, armedCard } = await import('../../public/js/ui/choiceOverlay.js');
|
||||
const { normalizeSp } = await import('../../public/js/ui/gameLogic.js');
|
||||
const { DATA } = await import('../match/harness.js');
|
||||
const { GameData } = await import('../../server/match/gamedata.js');
|
||||
const { generateDraft, cardView } = await import('../../server/match/choices.js');
|
||||
const { createRng } = await import('../../server/sim/rng.js');
|
||||
const { data } = await import('../../public/js/data.js');
|
||||
|
||||
/** Every vnode of a preact tree (htm output), depth first. */
|
||||
function* walk(v) {
|
||||
if (Array.isArray(v)) { for (const x of v) yield* walk(x); return; }
|
||||
if (!v || typeof v !== 'object') return;
|
||||
yield v;
|
||||
yield* walk(v.props?.children);
|
||||
}
|
||||
const hasClass = (v, c) => typeof v?.props?.class === 'string' && v.props.class.split(/\s+/).includes(c);
|
||||
|
||||
await data.loadAll('items', 'effects', 'choices', 'assets', 'chess', 'bonds');
|
||||
|
||||
/** A generated co-op 绝境 R11 机密商店 with the same non-coin item twice. */
|
||||
function shopWithTwin() {
|
||||
const gd = new GameData(DATA, 'mode_multi_hard');
|
||||
const coin = DATA.choices.shopDraft.coin;
|
||||
for (let seed = 1; seed < 50000; seed++) {
|
||||
const d = generateDraft(gd, createRng(seed * 104729 + 7), 11, {});
|
||||
if (d.family !== 'shop') continue;
|
||||
const ids = d.cards.map((c) => c.id);
|
||||
const twin = ids.find((id, i) => id !== coin && ids.indexOf(id) !== i);
|
||||
if (twin) return { d, twin, at: ids.flatMap((id, i) => (id === twin ? [i] : [])) };
|
||||
}
|
||||
throw new Error('no generated shop with a repeated item');
|
||||
}
|
||||
|
||||
test('机密商店 overlay: the same item twice is two cards — own keys, own selection, one taken leaves the other pickable', () => {
|
||||
const { d, twin, at } = shopWithTwin();
|
||||
const [a, b] = at;
|
||||
const players = [{ playerId: 'me', seat: 0, name: 'Me' }, { playerId: 'p2', seat: 1, name: 'P2' }];
|
||||
const pub = (picks) => normalizeSp({ family: 'shop', name: d.name, desc: d.desc, cards: d.cards.map(cardView), order: ['p2', 'me'], turn: Object.keys(picks).length ? 'me' : 'p2', picks, taken: {} }, players);
|
||||
const view = (sp, armed = null) => [...walk(ChoiceView({ pub: { players, deadline: 0 }, sp, myId: 'me', solo: false, armed }))].filter((n) => hasClass(n, 'spcard'));
|
||||
|
||||
// before any pick: six buttons, keyed by index; the twins have the same name, icon and text
|
||||
let sp = pub({});
|
||||
let cards = view(sp);
|
||||
assert.equal(cards.length, 6);
|
||||
assert.deepEqual(cards.map((c) => c.key), [0, 1, 2, 3, 4, 5], 'keyed by index, never by item id');
|
||||
const name = (c) => [...walk(c)].find((n) => hasClass(n, 'spcard__name')).props.children;
|
||||
assert.equal(name(cards[a]), DATA.items[twin].name);
|
||||
assert.equal(name(cards[b]), DATA.items[twin].name);
|
||||
assert.equal(sp.cards[a].id, sp.cards[b].id);
|
||||
|
||||
// the teammate takes the first twin: only that card shows the taker; the second twin is still mine to pick
|
||||
sp = pub({ p2: a });
|
||||
assert.equal(sp.cards[a].takenBy, 'p2');
|
||||
assert.equal(sp.cards[b].takenBy, null);
|
||||
assert.equal(cardPickable(sp, sp.cards[a], { myId: 'me', solo: false }), false);
|
||||
assert.equal(cardPickable(sp, sp.cards[b], { myId: 'me', solo: false }), true);
|
||||
cards = view(sp);
|
||||
assert.ok(hasClass(cards[a], 'is-taken') && !hasClass(cards[b], 'is-taken'));
|
||||
assert.ok([...walk(cards[a])].some((n) => hasClass(n, 'spcard__taker')) && ![...walk(cards[b])].some((n) => hasClass(n, 'spcard__taker')));
|
||||
assert.equal(cards[a].props.disabled, true);
|
||||
assert.equal(cards[b].props.disabled, false);
|
||||
|
||||
// two taps on the second twin: it alone is armed, then its own index is sent
|
||||
let r = spTap(null, b, cardPickable(sp, sp.cards[b], { myId: 'me', solo: false }));
|
||||
assert.deepEqual(r, { armed: b, pick: null });
|
||||
assert.equal(armedCard(r.armed, sp, { myId: 'me', solo: false }), b);
|
||||
cards = view(sp, r.armed);
|
||||
assert.deepEqual(cards.map((c) => hasClass(c, 'is-armed')), cards.map((_, i) => i === b), 'only the tapped twin is armed');
|
||||
r = spTap(r.armed, b, true);
|
||||
assert.deepEqual(r, { armed: null, pick: b });
|
||||
});
|
||||
Reference in new issue
Block a user