feedback1 integrate: ticker lines by broadcast priority; AI 托管 keeps a human's items out of temp

- the strip queues by the official broadcast priority (BOSS_HIT 30 > CHAR_DAMAGE 20 > SHOP_LEVEL 11 > ...),
  match-flow notices at 25, never cutting the line on screen short
- on a human seat under AI 托管 an item left in temp with a full hand gets a hand slot (a bench operator sold)
This commit is contained in:
sganggs committed 2026-10-03 07:57:28 +08:00
1 parent 75e2f02590
commit 0a15c6e41d
7 files changed
+99 -18

No files matched your search

+15
View File
@@ -334,6 +334,21 @@ test('#4 the transformation completes a merge on the board: the cell is returned
m.dispose();
});
test('#10 AI 托管 never destroys a human\'s item to clear temp: a bench operator is sold for its slot instead', () => {
const s = setup({ seed: 34 });
const { m, ps } = s;
ps.autoplay = true; // a human seat under AI 托管 (ps.isBot stays false)
const elites = [1, 2, 3, 4, 5, 6].flatMap((t) => plainOf(m, t)).map((id) => m.gd.goldenIdOf(id)).filter((g) => g && m.pool.left(m.gd.baseIdOf(g)) >= m.gd.goldenCopies);
while (ps.hand.some((x) => x == null)) give(m, ps, elites.shift(), 'hand');
const item = Object.values(DATA.items).find((it) => it && it.id !== CELL && !it.isGolden && it.price > 0 && /_e_a$/.test(it.id));
giveItem(m, ps, item.id, 'temp');
botPrepEnd(m, ps);
assert.equal(ownedItems(ps, item.id).length, 1, `${item.name}: kept (it used to be destroyed when the hand was full)`);
assert.ok(ps.tempEmpty, 'temp resolved');
checkInvariants(m);
m.dispose();
});
test('#4 a bot never destroys the cell: left in temp with a full hand and nobody to inject, it gets a hand slot', () => {
const s = setup({ seed: 33 });
const { m, ps } = s;
+32 -1
View File
@@ -147,10 +147,41 @@ test('timeline: a Final Assault burst shows its top milestone before the round e
assert.equal(shown.find((s) => s.text === '隐秘核心已解锁').at, 5.5, 'the Hidden Core\'s unlock line shows as its round starts');
});
test('lines queue by broadcast priority (research 06 §9.2 "The highest priority wins"), first in first out among equals; the lowest priority\'s oldest goes past QUEUE_MAX', () => {
const P = { BOSS_HIT: 30, CHAR_DAMAGE: 20, SHOP_LEVEL: 11, GOLDEN_CHAR: 2, CHAR_GIFT: 1, CUSTOM: 0 };
const pl = (type, text, priority = P[type]) => ({ type, round: 14, text, priority });
const shop1 = pl('SHOP_LEVEL', 's1'), gold = pl('GOLDEN_CHAR', 'g'), shop2 = pl('SHOP_LEVEL', 's2');
const boss = { ...hit('A', 20, 14), priority: 30 }, dmg = pl('CHAR_DAMAGE', 'd'), flow = pl('CUSTOM', '隐秘核心已解锁', 25);
let r = enqueueTickerLines([], pl('CUSTOM', 'on screen'), [shop1, gold, shop2]);
assert.deepEqual(texts(r.queue), ['s1', 's2', 'g']);
r = enqueueTickerLines(r.queue, r.cur, [boss]);
assert.equal(r.cur.text, 'on screen', 'the line on screen is never cut short');
assert.deepEqual(texts(r.queue), [boss.text, 's1', 's2', 'g'], 'the leader-damage line jumps the queue');
r = enqueueTickerLines(r.queue, r.cur, [dmg, flow]);
assert.deepEqual(texts(r.queue), [boss.text, '隐秘核心已解锁', 'd', 's2'], 'flow notice under the leader line, then operator damage; past QUEUE_MAX the promotion line, then the older shop line go');
// the superseding rule still holds under priorities
r = enqueueTickerLines(r.queue, r.cur, [{ ...hit('A', 50, 14), priority: 30 }]);
assert.deepEqual(texts(r.queue).slice(0, 2), ['A博士对敌方领袖造成的伤害超过50%!', '隐秘核心已解锁']);
assert.equal(r.queue.filter((t) => t.type === 'BOSS_HIT').length, 1);
});
test('timeline with priorities: shop-level and promotion lines of the boss prep no longer hold the Final Assault\'s milestones until the round ends', () => {
const pr = (o, priority) => ({ ...o, priority });
const shown = playStrip([
{ at: 0, round: 14, lines: [pr(line('SHOP_LEVEL', null, 'B6'), 11), pr(line('SHOP_LEVEL', null, 'C6'), 11), pr(line('GOLDEN_CHAR', null, 'Bgold'), 2), pr(line('SHOP_LEVEL', null, 'D6'), 11)] },
{ at: 1, round: 14, lines: [pr(hit('A', 20), 30)] },
{ at: 4, round: 14, lines: [pr(hit('A', 50), 30)] },
{ at: 12, round: 15, lines: [pr(line('CUSTOM', null, '隐秘核心已解锁'), 25)] },
], 40);
const r14 = shown.filter((s) => s.round === 14).map((s) => s.text);
assert.deepEqual(r14, ['B6', 'A博士对敌方领袖造成的伤害超过50%!', 'C6'], `the milestone plays second (${r14.join(' | ')})`);
assert.equal(shown.find((s) => s.text === '隐秘核心已解锁').round, 15);
});
test('main.js stamps every ticker line with its type, player and round; the strip is built on the helpers', () => {
const main = readFileSync(new URL('../../public/js/main.js', import.meta.url), 'utf8');
const handler = main.slice(main.indexOf("net.on('m.ticker'"), main.indexOf("net.on('m.emote'"));
assert.match(handler, /type, playerId, round:\s*s\.match\?\.public\?\.round/, 'the m.ticker handler keeps type, player and round');
assert.match(handler, /type, playerId, round:\s*s\.match\?\.public\?\.round \?\? null, priority/, 'the m.ticker handler keeps type, player, round and priority');
const strip = readFileSync(new URL('../../public/js/ui/ticker.js', import.meta.url), 'utf8');
const comp = strip.slice(strip.indexOf('export function Ticker('));
assert.match(comp, /useStore\(\(s\) => s\.match\?\.public\?\.round/, 'the strip follows m.public.round');