Files
ark-stronghold-protocol/test/match/bonds.test.js
T
sganggs 6bfc08270c 0.2.4-dev: round-29 community fixes and the official shared card pool
- Shared card pool and item stock as in the official mode (the owner's request, from the 路标档案馆 video and public notes): solo low-tier exceptions, co-op operators x2, shared item stock with special-item exceptions, normal / advanced occupancy 1 / 2, re-check on buy, 机变 exhaustion, release on consume / destroy / elimination; 整备 never exceeds stock; invariant held <= cap.
- 突袭: one taunt-first reselect right after a revive (the owner's rule); ordinary polls and redeploys keep the 0.2.3 rule; a revive on an enemy's tile stays. #475: online boss / hidden-boss Raid may land on the empty half (owner-limited scope).
- 浊心斯卡蒂 S1 x 坚守 no longer bounce redirected damage, and the two shares conserve the hit (idea of PR #464 by @HappyTanuki).
- Operator fixes adapted from PRs by @Xinglan233 (#459 哈洛德 S2 elemental trigger, #460 涤火杰西卡 shield on the four adjacent tiles, #461 家族徽章 first hit after stealth, #462 松果 S2 regrow per deploy); 克莱门莎 S2 splash 1.1 / 100% ATK and air auto-trigger (#467); 白面 copying 歌蕾蒂娅 / 余 skips the three-member condition (#473, the owner's video).
- 香槟炸弹 is not chosen by friendly auras / heals (PR #468 / #476 by @Siornya); 琳琅诗怀雅 coin balance (PR #450 by @IceCodeNew); Touch prep display (PR #451 by @YinqiMargatroid); lobby header (PR #471 by @YuriRestia); Japanese UI terms (PR #448 by @scutkane).
- AI-only boss countdown grace, render-failure cache / recovery without restarting a battle, device units kept on a same-field re-entry, DIY detail / skill previews, large-text result buttons.
- golden: battle families equal 0.2.3 except diy-135 / diy-136 (克莱门莎); all 18 release matches move through the shared pool, every flip named in docs/history/0.2.4.md §29.12; two coverage scenarios added.
2026-10-11 01:58:52 +08:00

121 lines
6.6 KiB
JavaScript

// Bond counting modes, tiers, 调和, 独行, 绝技, 助力, layers (DESIGN §6.3, research 02 §2.1).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { GameData } from '../../server/match/gamedata.js';
import { computeBonds, tierFor, activatedLayers, bondList, thresholdsOf } from '../../server/match/bondsMeta.js';
import { DATA } from './harness.js';
const gd = new GameData(DATA, 'mode_multi_normal');
let uid = 1;
const piece = (id, items = []) => ({ uid: uid++, kind: 'chess', id, items: items.map((i) => ({ uid: uid++, kind: 'item', id: i })) });
const state = ({ board = [], hand = [], layers = {} } = {}) => {
const b = new Map();
board.forEach((p, i) => b.set(`${9 + (i % 4)},${2 + Math.floor(i / 4)}`, p));
const h = new Array(10).fill(null);
hand.forEach((p, i) => { h[9 - i] = p; });
return { board: b, hand: h, layers };
};
const members = (bond, n, extra = () => true) => Object.values(DATA.chess).filter((c) => c.visible && !c.isGolden && c.bonds.includes(bond) && extra(c)).slice(0, n).map((c) => c.chessId);
test('BOARD: distinct base chess on the board; normal + elite of one operator count once; the hand does not count', () => {
const [a, b, c] = members('yanShip', 3, (x) => x.bonds.length === 1 || true);
let s = computeBonds(gd, state({ board: [piece(a), piece(b)], hand: [piece(c)] }));
assert.equal(s.yanShip.count, 2);
assert.equal(s.yanShip.active, false);
s = computeBonds(gd, state({ board: [piece(a), piece(b), piece(c)] }));
assert.equal(s.yanShip.count, 3);
assert.equal(s.yanShip.tier, 1);
assert.equal(s.yanShip.active, true);
s = computeBonds(gd, state({ board: [piece(a), piece(DATA.chess[a].goldenId), piece(b)] }));
assert.equal(s.yanShip.count, 2, 'normal + elite of one operator = 1');
const six = members('yanShip', 6);
s = computeBonds(gd, state({ board: six.map((x) => piece(x)) }));
assert.equal(s.yanShip.tier, 2);
assert.deepEqual(thresholdsOf(DATA.bonds.yanShip), [3, 6, 9]);
});
test('BOARD_AND_DECK (远见/奇迹/投资人) also counts the hand', () => {
const [a, b] = members('visiShip', 2);
const s = computeBonds(gd, state({ board: [piece(a)], hand: [piece(b)] }));
assert.equal(s.visiShip.count, 2);
assert.equal(s.visiShip.active, true);
const v = bondList(gd, s, { full: true }).find((x) => x.bondId === 'visiShip');
assert.equal(v.countsHand, true);
});
test('绝技 (BOARD_ALL_CHESS): every elite on the board, duplicates included', () => {
const g = Object.values(DATA.chess).filter((c) => c.visible && c.isGolden !== true).slice(0, 5).map((c) => c.goldenId);
let s = computeBonds(gd, state({ board: [piece(g[0]), piece(g[0])] }));
assert.equal(s.suntShip.count, 2);
assert.equal(s.suntShip.active, true);
s = computeBonds(gd, state({ board: g.map((x) => piece(x)) }));
assert.equal(s.suntShip.tier, 2);
s = computeBonds(gd, state({ hand: [piece(g[0]), piece(g[1])] }));
assert.equal(s.suntShip.count, 0, 'hand elites do not count');
});
test('调和: +1 to core bonds with ≥ 1 real member on the board while active', () => {
const mani = members('maniShip', 1)[0];
assert.ok(mani, 'a visible 调和 chess exists');
const yan = members('yanShip', 2, (c) => !c.bonds.includes('maniShip'));
let s = computeBonds(gd, state({ board: [piece(mani), ...yan.map((x) => piece(x))] }));
assert.equal(s.maniShip.active, true);
assert.equal(s.yanShip.count, 3, '2 real + 1');
assert.equal(s.yanShip.active, true);
const noMember = s.sargonShip ? s.sargonShip.count : 0;
assert.equal(noMember, members('sargonShip', 99).includes(mani) ? 1 : 0, 'no +1 without a real member');
s = computeBonds(gd, state({ board: yan.map((x) => piece(x)) }));
assert.equal(s.yanShip.count, 2);
});
test('actual normal/elite 缪尔赛思 activates every available core bond at two real members; duplicates add only one', () => {
for (const id of gd.bondIds.filter(id => gd.bond(id).isCore && !gd.modeInactiveBonds.has(id))) {
const two = members(id, 2, c => !c.bonds.includes('maniShip'));
assert.equal(two.length, 2, id);
for (const mu of ['chess_char_6_11_a', 'chess_char_6_11_b']) {
const s = computeBonds(gd, state({ board: [piece(mu), ...two.map(id => piece(id))] }));
assert.equal(s[id].count, 3, `${id}: two operators plus ${mu}`); assert.ok(s[id].active);
}
const dup = computeBonds(gd, state({ board: [piece('chess_char_6_11_a'), piece('chess_char_6_11_b'), ...two.map(id => piece(id))] }));
assert.equal(dup[id].count, 3, `${id}: duplicate harmony is not +2`);
const hand = computeBonds(gd, state({ board: two.map(id => piece(id)), hand: [piece('chess_char_6_11_a')] }));
assert.equal(hand[id].count, 2, `${id}: harmony needs deployment`);
}
});
test('独行 (downward): active only while exactly 1 distinct 独行 operator is on the board', () => {
const solo = members('soloShip', 2);
assert.equal(tierFor(DATA.bonds.soloShip, 1), 1);
assert.equal(tierFor(DATA.bonds.soloShip, 2), 0);
assert.equal(tierFor(DATA.bonds.soloShip, 0), 0);
let s = computeBonds(gd, state({ board: [piece(solo[0])] }));
assert.equal(s.soloShip.active, true);
s = computeBonds(gd, state({ board: [piece(solo[0]), piece(DATA.chess[solo[0]].goldenId)] }));
assert.equal(s.soloShip.active, true, 'two copies of the same operator do not break it');
s = computeBonds(gd, state({ board: [piece(solo[0]), piece(solo[1])] }));
assert.equal(s.soloShip.active, false);
});
test('助力: upper tier counts operators differing in name OR elite state', () => {
const d = members('deputShip', 2);
let s = computeBonds(gd, state({ board: [piece(d[0]), piece(d[1])] }));
assert.equal(s.deputShip.tier, 1);
s = computeBonds(gd, state({ board: [piece(d[0]), piece(d[1]), piece(DATA.chess[d[1]].goldenId)] }));
assert.equal(s.deputShip.count, 2);
assert.equal(s.deputShip.tier, 2, 'normal + elite count as different for the 3-member tier');
});
test('layers persist on every bond; Σ activated layers only counts active bonds; FUNNY inactive bonds omitted', () => {
const [a, b, c] = members('yanShip', 3);
const s = computeBonds(gd, state({ board: [piece(a), piece(b), piece(c)], layers: { yanShip: 40, egirShip: 100 } }));
assert.equal(s.yanShip.layers, 40);
assert.equal(s.egirShip.layers, 100);
assert.equal(activatedLayers(s), 40);
const list = bondList(gd, s);
assert.equal(list[0].bondId, 'yanShip', 'active first');
assert.ok(list.some((x) => x.bondId === 'egirShip' && !x.active), 'inactive bonds with layers are listed');
const funny = new GameData(DATA, 'mode_multi_funny');
const f = computeBonds(funny, state({ board: [piece(a)] }));
for (const off of DATA.config.modes.mode_multi_funny.inactiveBondIds) assert.ok(!(off in f), `${off} omitted in FUNNY`);
});