Files
ark-stronghold-protocol/test/match/feedback1-secret-shop.test.js
T
sganggs d1f8a49829 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).
2026-10-03 07:42:20 +08:00

103 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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));
}
});