Files
ark-stronghold-protocol/test/sim/feedback5-cb4-pins.test.js
T
sganggs 686f7ecf37 feedback5 CB4: three community reports already answered, pinned — the briefing types fill 7-9 of the 13 normal waves, at most 3 each (item 6, the official schedule; now stated in docs/PLAYING.md); 新硎's 4 断刃 since GitHub #107 (item 53); a freed prisoner re-enters 联防 confined (item 57)
No product change. Item 6: 400 seeds per mode, 险境/绝境/终极 7 in 141, 8 in 192, 9 in 67, per type 1-3 (R14/R15 take
slots of the 15). Item 53: v0.1.4 stacked a layer per attack (2430 -> 5670), this tree spends one blade per attack and
ends the bonus with the 4th (client template enemy_dead_spawn[sfji]). Item 57: a leak re-enters as a new spawn (key,
mods, LP, bounty only), confined.
Golden unchanged. Tests: test/sim/feedback5-cb4-pins.test.js (2). Docs: DESIGN §25.18.3-§25.18.5, docs/PLAYING.md.
2026-10-07 01:51:18 +08:00

77 lines
5.5 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.
// test/sim/feedback5-cb4-pins.test.js — community reports of 2026-10-06 that this tree already answers, pinned (DESIGN
// §25.18; they pass on the parent commit — nothing changed for them):
// item 6 「每局游戏开始展示的敌人类型(折射、飞行等等),实际出现波次在7到9波,而且每个最多三波,其余波次用特异类型填充」 — the
// official type schedule (client RandomEnemyGenerater, research 08 §2.1; act2autochess specialEnemyRandomTypeDict count 3,
// constData maxLevelCnt 15 / specialEnemyNum 3 / enemyTypeIdentifierToFillRandom 1): each briefing type owns 3 of the
// 15 round slots, SPECIAL (特异) the other 6, shuffled — R14 (leader) and R15 (hidden core) take slots too, so the 13
// normal rounds hold 7–9 type waves, at most 3 per type;
// item 57 「囚犯敌人的解放状态联防时不应继承」 — a leaked prisoner re-enters 联防 as a new spawn (unite.js planUnite →
// waves.js buildUniteWave: its key and spawn mods only), confined again (archetypes.js prisoner spawn); the freed look
// the report saw was 普通 / 老练囚犯's red clip set drawn from the gate, fixed with the prisoners' forms (§25.14.1).
// Run: node --test test/sim/feedback5-cb4-pins.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, chessRec, checkInvariants } from '../helpers/battleHarness.js';
import { GameData } from '../../server/match/gamedata.js';
import { setupMatchWaves, buildUniteWave } from '../../server/match/waves.js';
import { planUnite } from '../../server/match/unite.js';
import { createRng } from '../../server/sim/rng.js';
import { DATA } from '../match/harness.js';
test('item 6: the three briefing types fill 7–9 of the 13 normal waves (险境 / 绝境 / 终极), at most 3 each; the rest are SPECIAL', () => {
for (const modeId of ['mode_multi_normal', 'mode_single_hard', 'mode_multi_abyss']) {
const gd = new GameData(DATA, modeId);
assert.equal(gd.bossRound, 14);
const totals = new Set();
for (let seed = 1; seed <= 200; seed++) {
const s = setupMatchWaves(gd, createRng(seed));
const normal = s.typeSlots.slice(0, gd.bossRound - 1);
const per = s.factions.map((f) => normal.filter((t) => t === f).length);
for (const n of per) assert.ok(n >= 1 && n <= 3, `${modeId} seed ${seed}: ${per}`);
const total = per.reduce((a, b) => a + b, 0);
assert.ok(total >= 7 && total <= 9, `${modeId} seed ${seed}: ${total}`);
assert.equal(normal.filter((t) => t === 'SPECIAL').length, normal.length - total, 'every other wave is SPECIAL');
totals.add(total);
}
assert.deepEqual([...totals].sort(), [7, 8, 9], `${modeId}: all three totals occur`);
}
});
test('item 57: a prisoner freed in its own combat leaks and re-enters 联防 confined (no freed form, the confinement buff, freed only by its own attacks there)', () => {
const KEY = 'enemy_1116_liprr'; // 普通囚犯
const wall = chessRec({ id: 't_wall', profession: 'TANK', stats: { atk: 0, maxHp: 1e9, def: 0, blockCnt: 3 }, rangeGrid: [[0, 0]], skill: null });
const kits = { t_wall: () => ({ trait: { noAttack: true } }) };
const h = makeBattle({ autoFinish: false, timeLimit: 120, defs: { chess: { t_wall: wall } }, kits, units: [{ chessId: 't_wall', row: 9, col: 6 }], enemies: [{ key: KEY, route: 0 }] });
assert.ok(h.runUntil(() => h.enemy(KEY) && h.enemy(KEY).form === 'liberty', 60), 'freed by its own attacks');
const p = h.enemy(KEY);
assert.ok(p.findBuff('ab:liberty') && !p.findBuff('ab:confined'));
h.b.retreat(h.unit('t_wall'));
assert.ok(h.runUntil(() => !p.alive, 60), 'it walks on and leaks');
const r = h.result().perPlayer.p1;
assert.deepEqual(r.leaked.filter((l) => l.counted !== false).map((l) => l.enemyKey), [KEY]);
checkInvariants(h.b);
// 联防: the leaker's counted leak re-enters on the escaped template of one helper
const gd = new GameData(DATA, 'mode_multi_normal');
const seat = (id, s) => ({ playerId: id, seat: s, deployCount: 1, bonds: {}, layers: {}, board: new Map(), bounties: [] });
const m = { isSolo: false, alivePlayers: () => [seat('p1', 1), seat('H', 0)], gd: { unite: { maxHelpers: 2 }, enemy: (k) => gd.enemy(k) } };
const plan = planUnite(m, new Map([['p1', r], ['H', { perfect: true, leaked: [], unitsEnd: [] }]]));
assert.deepEqual(plan.leaked.map((l) => l.enemyKey), [KEY]);
assert.deepEqual(Object.keys(plan.leaked[0]).sort(), ['bounty', 'enemyKey', 'lpr', 'mods', 'sourcePlayerId', 'tag'], 'nothing of its state travels');
const wave = buildUniteWave(gd, plan.leaked, 1, 60);
const u = makeBattle({
kind: 'unite', stageId: 'act1autochess_escaped_single', autoFinish: false, timeLimit: 60, routes: wave.routes,
defs: { chess: { t_wall: wall } }, kits, units: [{ chessId: 't_wall', row: 9, col: 8 }],
enemies: wave.spawns.map((s) => ({ key: s.enemyKey, time: s.time, route: s.routeIndex, mods: s.mods, sourcePlayerId: s.sourcePlayerId })),
});
assert.ok(u.runUntil(() => u.enemy(KEY), 30), 're-entered');
const q = u.enemy(KEY);
assert.equal(q.form ?? null, null, 'drawn confined');
assert.ok(q.findBuff('ab:confined') && !q.findBuff('ab:liberty'), 'confined again');
assert.ok(u.runUntil(() => q.stats.attacks >= 1, 40), 'blocked, it attacks');
assert.equal(q.form ?? null, null, 'still confined after its first 联防 attack');
assert.ok(u.runUntil(() => q.form === 'liberty', 60), 'freed again only by its own attacks there');
assert.equal(q.stats.attacks, 4, 'at its 4th attack in 联防 (confinement.times), as in a fresh spawn');
checkInvariants(u.b);
});