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:
sganggs committed 2026-10-03 07:42:20 +08:00
1 parent 7c13e38422
commit d1f8a49829
12 files changed
+799 -261

No files matched your search

+102
View File
@@ -0,0 +1,102 @@
// The 机密商店 after the official screenshots (the user, after the R3 / R9 / R11 screenshots of 22 official matches:
// "机密商店按官方改成可以重复吧"). The 4 official 机密商店 (R11 of matches 2, 4, 5 and 8 in
// test/fixtures/official-bounty-drafts.json) offer the same item twice (盟约之币 ×2 in 4 and 5, 变形同构体 ×2 in 8) and
// share one composition: exactly two tier-VI items, at least one tier V, at least one 盟约之币, no other tier-I / II item.
// choices.json `shopDraft` (tools/build-data.mjs SHOP_DRAFT) draws six slots on their own, with replacement; picking one
// of two identical cards is in test/content/choices.test.js (E2E), the overlay with two identical cards in
// test/ui/feedback1-secret-shop.test.js.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { DATA } from './harness.js';
import { GameData } from '../../server/match/gamedata.js';
import { generateDraft, cardView } from '../../server/match/choices.js';
import { createRng } from '../../server/sim/rng.js';
const OFFICIAL = JSON.parse(readFileSync(new URL('../fixtures/official-bounty-drafts.json', import.meta.url), 'utf8'));
const SHOP = DATA.choices.shopDraft;
const COIN = SHOP.coin;
const itemByName = (name) => Object.values(DATA.items).find((i) => i.name === name && !i.isGolden && i.itemType === 'EQUIP');
/** The official 机密商店: [{ match, cards: [[name, tier]] }]. */
const SHOPS = Object.entries(OFFICIAL.matches).flatMap(([m, rs]) => Object.values(rs).filter((d) => d.family === 'shop').map((d) => ({ match: Number(m), cards: d.cards })));
const count = (list, pred) => list.filter(pred).length;
test('机密商店 (4 official shops): six free items, two of them tier VI, a tier V and a 盟约之币 in each, the same item twice in three', () => {
assert.deepEqual(SHOPS.map((s) => s.match), [2, 4, 5, 8]);
assert.deepEqual(SHOP.seen, [2, 4, 5, 8]);
let dupShops = 0;
for (const { match, cards } of SHOPS) {
assert.equal(cards.length, 6, `match ${match}`);
for (const [name, tier] of cards) assert.equal(itemByName(name)?.tier, tier, `match ${match}: ${name} is tier ${tier} in the data`);
assert.equal(count(cards, ([, t]) => t === 6), 2, `match ${match}: exactly two tier-VI items`);
assert.ok(count(cards, ([, t]) => t === 5) >= 1, `match ${match}: a tier-V item`);
assert.ok(count(cards, ([n]) => n === '盟约之币') >= 1, `match ${match}: a 盟约之币`);
assert.ok(cards.every(([n, t]) => t >= 3 || n === '盟约之币'), `match ${match}: no other tier-I / II item`);
if (new Set(cards.map(([n]) => n)).size < cards.length) dupShops++;
}
assert.equal(dupShops, 3, '盟约之币 ×2 (matches 4, 5), 变形同构体 ×2 (match 8)');
// the data: four fixed slots (VI, VI, V, 盟约之币) + two drawn by what the other two cards of each shop were
assert.equal(DATA.items[COIN].name, '盟约之币');
assert.deepEqual(SHOP.slots.slice(0, 4), [{ 6: 1 }, { 6: 1 }, { 5: 1 }, { coin: 1 }]);
const rest = { 3: 0, 4: 0, 5: 0, coin: 0 };
for (const { cards } of SHOPS) {
const left = cards.map(([n, t]) => (n === '盟约之币' ? 'coin' : String(t)));
for (const k of ['6', '6', '5', 'coin']) left.splice(left.indexOf(k), 1);
for (const k of left) rest[k]++;
}
assert.deepEqual(rest, { 3: 1, 4: 2, 5: 3, coin: 2 });
for (const s of SHOP.slots.slice(4)) assert.deepEqual(s, rest);
assert.equal(SHOP.slots.length, 6);
// an item's weight within its tier: 1 + the official cards it showed on
const seen = {};
for (const { cards } of SHOPS) for (const [n] of cards) if (n !== '盟约之币') seen[itemByName(n).id] = (seen[itemByName(n).id] || 0) + 1;
assert.deepEqual(SHOP.itemWeights, Object.fromEntries(Object.entries(seen).map(([id, k]) => [id, 1 + k]).sort()));
assert.equal(SHOP.itemWeights[itemByName('变形同构体').id], 5, '变形同构体 on 4 of the 8 tier-VI cards');
});
test('机密商店 generated (co-op 绝境 R11): the official composition, drawn with replacement — the same item can come twice', () => {
const gd = new GameData(DATA, 'mode_multi_hard');
let shops = 0;
let dup = 0;
let dupItem = 0;
let coins2 = 0;
const offered = new Set();
for (let seed = 1; shops < 2000 && seed < 20000; seed++) {
const d = generateDraft(gd, createRng(seed * 104729 + 7), 11, { stageId: 'act2autochess_m01' });
if (d.family !== 'shop') continue;
shops++;
assert.equal(d.name, '机密商店');
assert.equal(d.cards.length, 6);
const tiers = d.cards.map((c) => DATA.items[c.id].tier);
assert.equal(count(tiers, (t) => t === 6), 2, 'two tier-VI items');
assert.ok(count(tiers, (t) => t === 5) >= 1, 'a tier-V item');
assert.ok(count(d.cards, (c) => c.id === COIN) >= 1, 'a 盟约之币');
assert.ok(d.cards.every((c) => c.id === COIN || DATA.items[c.id].tier >= 3), 'no other tier-I / II item');
assert.deepEqual(d.cards.map((c) => c.idx), [0, 1, 2, 3, 4, 5], 'each card its own index');
for (const c of d.cards) {
assert.equal(c.kind, 'item');
assert.equal(cardView(c).price, 0, '无需消耗资金');
assert.ok(!DATA.items[c.id].isGolden && !DATA.items[c.id].shopExcluded);
offered.add(c.id);
}
const ids = d.cards.map((c) => c.id);
if (new Set(ids).size < ids.length) dup++;
if (new Set(ids.filter((id) => id !== COIN)).size < count(ids, (id) => id !== COIN)) dupItem++;
if (count(ids, (id) => id === COIN) >= 2) coins2++;
}
assert.equal(shops, 2000);
assert.ok(dup / shops > 0.45 && dup / shops < 0.75, `a repeated item in ${(100 * dup / shops).toFixed(0)} % (official 3 of 4)`);
assert.ok(coins2 / shops > 0.35 && coins2 / shops < 0.55, `盟约之币 ×2 in ${(100 * coins2 / shops).toFixed(0)} % (official 2 of 4)`);
assert.ok(dupItem / shops > 0.08, `another item twice in ${(100 * dupItem / shops).toFixed(0)} % (official: 变形同构体 ×2)`);
// every shop item of tiers III–VI can come
for (const t of [3, 4, 5, 6]) for (const id of gd.shopItemsByTier[t]) assert.ok(offered.has(id), `${DATA.items[id].name} (tier ${t}) offered`);
// solo: 3 of the slots
const solo = new GameData(DATA, 'mode_single_hard');
for (let seed = 1, n = 0; n < 30 && seed < 5000; seed++) {
const d = generateDraft(solo, createRng(seed * 31 + 5), 11, {});
if (d.family !== 'shop') continue;
n++;
assert.equal(d.cards.length, 3);
assert.ok(d.cards.every((c) => c.id === COIN || DATA.items[c.id].tier >= 3));
}
});