Files
ark-stronghold-protocol/test/match/feedback1-secret-shop.test.js
sganggs 3b18376eb7 feedback1 WB: R11 bounty list drawn from the 7 seen lists; R9 group sizes stated as they are; official 机密商店 composition only at R11
- R11 悬赏决策: the list is one of the 7 seen lists, uniform (slots 7, taken as the events
  bounty_hunter_1..7, one block of effectChoiceInfoDict). No list is built from nothing any more;
  the rule only completes the 4 lists seen with 6 cards, so 12_8 / 16_2 / 16_6 come in about
  1 R11 bounty draft in 15 (14 drafts without one: about 39 %).
- R9: the groups hold 9, 9, 9, 9, 8 and 6 cards (comments, docs, test); match 4's base-only
  group may be a draft of another group (open). 58 -> 59 bounty drafts in the build-data note.
- 机密商店: shopDraft.rounds [11]. The official composition (VI, VI, V, 盟约之币 + 2) applies at
  R11 (绝境 / 终极, where the screenshots are); the earlier 标准 / 险境 shops keep the previous draw,
  any tier I-VI per card with replacement (repeats still possible, about 26 %).
- Tests: R11 slots / block order / never-shown rate, R9 group sizes, the non-R11 shop draw.
2026-10-03 08:09:39 +08:00

143 lines
9.0 KiB
JavaScript
Raw Permalink 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, at R11 (the
// round of the screenshots; 绝境 / 终极 only); the earlier 机密商店 of 标准 / 险境 (no screenshot) keep the previous draw,
// any tier I–VI 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);
assert.deepEqual(SHOP.rounds, [11], 'the composition where the screenshots are: R11 (绝境 / 终极)');
// 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));
}
});
test('机密商店 outside R11 (标准 R3 / R9, 险境 R3 / R6 / R9 — no screenshot): the previous draw, any tier I–VI per card, with replacement', () => {
// the data's shop events sit in three blocks (artifact_paid_1 by the R3 events, _2 / _3 by the R9 ones, _4 / _5 by
// hardbuff_select), so an early 机密商店 need not look like the R11 one [ASSUMED: keep the previous draw]
const all = new Set([1, 2, 3, 4, 5, 6].flatMap((t) => new GameData(DATA, 'mode_multi_funny').shopItemsByTier[t] || []));
for (const [modeId, rounds] of [['mode_multi_funny', [3, 9]], ['mode_multi_normal', [3, 6, 9]]]) {
assert.deepEqual(DATA.choices.schedule[modeId].spRounds, rounds);
const gd = new GameData(DATA, modeId);
for (const round of rounds) {
let shops = 0;
let dup = 0;
let twoVI = 0;
let lowTier = 0;
const tiers = new Set();
for (let seed = 1; shops < 300 && seed < 40000; seed++) {
const d = generateDraft(gd, createRng(seed * 7717 + round), round, { stageId: 'act2autochess_m01' });
if (d.family !== 'shop') continue;
shops++;
assert.equal(d.cards.length, 6);
assert.deepEqual(d.cards.map((c) => c.idx), [0, 1, 2, 3, 4, 5]);
for (const c of d.cards) {
assert.ok(all.has(c.id), `${modeId} R${round}: ${c.name} is a normal shop item`);
assert.equal(cardView(c).price, 0);
tiers.add(DATA.items[c.id].tier);
}
const ids = d.cards.map((c) => c.id);
if (new Set(ids).size < ids.length) dup++;
if (count(ids, (id) => DATA.items[id].tier === 6) === 2) twoVI++;
if (ids.some((id) => id !== COIN && DATA.items[id].tier <= 2)) lowTier++;
}
assert.equal(shops, 300, `${modeId} R${round}`);
assert.deepEqual([...tiers].sort(), [1, 2, 3, 4, 5, 6], `${modeId} R${round}: every tier`);
assert.ok(twoVI / shops < 0.4, `${modeId} R${round}: two tier-VI items in ${(100 * twoVI / shops).toFixed(0)} % (R11: always)`);
assert.ok(lowTier / shops > 0.5, `${modeId} R${round}: a tier-I / II item besides 盟约之币 in ${(100 * lowTier / shops).toFixed(0)} %`);
assert.ok(dup / shops > 0.12 && dup / shops < 0.42, `${modeId} R${round}: a repeat in ${(100 * dup / shops).toFixed(0)} % (6 of 51, with replacement: about 26 %)`);
}
}
});