Files
Dune/tests/effective-battle-effects.test.ts
T

180 lines
6.9 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { baseDeck, ixBattleCards, type Card } from '../game/cards';
import { richeseCards } from '../game/richese-cards';
import {
battleCardEffects,
battleWeaponsExplode,
isMirrorWeapon,
isWeaponCard,
validBattleCardPair,
defaultVoiceMatch,
playedVoiceMatch,
} from '../game/battle-cards';
import { strongholdBattleEffects } from '../game/stronghold-battle';
const cards = [...baseDeck(), ...ixBattleCards(), ...richeseCards()];
const get = (kind: Card['kind']) => cards.find((card) => card.kind === kind)!;
const mirror = richeseCards().find((card) => card.effect === 'mirrorWeapon')!;
const portable = richeseCards().find(
(card) => card.effect === 'portableSnooper',
)!;
const stone = richeseCards().find((card) => card.effect === 'stoneBurner')!;
const defenses = [
undefined,
get('shield'),
get('snooper'),
get('shieldSnooper'),
get('weirdingWay'),
get('chemistry'),
portable,
];
// Independent printed defense matrix: undefended, shield, snooper, combined,
// defensive Weirding Way, defensive Chemistry, Portable Snooper.
const attacks: [Card['kind'], boolean[]][] = [
['projectile', [true, false, true, false, false, true, true]],
['poison', [true, true, false, false, true, false, false]],
['poisonBlade', [true, true, true, false, true, true, true]],
['weirdingWay', [true, false, true, false, false, true, true]],
['chemistry', [true, true, false, false, true, false, false]],
['lasgun', [true, true, true, true, true, true, true]],
];
void test('revealed ordinary and copied attacks use the printed defense matrix in either orientation', () => {
for (const [kind, expected] of attacks) {
const weapon = get(kind);
for (const [index, defense] of defenses.entries()) {
// Chemistry's original owner must supply its complementary defense.
const originalDefense = kind === 'chemistry' ? get('shield') : undefined;
const direct = battleCardEffects(
weapon,
originalDefense,
undefined,
defense,
);
assert.equal(direct.defenderDead, expected[index], `${kind}/${index}`);
const copied = battleCardEffects(
mirror,
defense,
weapon,
originalDefense,
);
assert.equal(
copied.attackerDead,
expected[index],
`original ${kind}/${index}`,
);
const reversed = battleCardEffects(
weapon,
originalDefense,
mirror,
defense,
);
assert.equal(
reversed.defenderDead,
expected[index],
`reverse ${kind}/${index}`,
);
// The copy attacks the original owner; both attacks remain simultaneous.
const originalDefenseIndex = kind === 'chemistry' ? 1 : 0;
assert.equal(copied.defenderDead, expected[originalDefenseIndex]);
assert.equal(reversed.attackerDead, expected[originalDefenseIndex]);
assert.equal(copied.stunned, false);
assert.equal(copied.noBounty, false);
}
}
});
void test('copied Poison Tooth activation belongs independently to each player and Chemistry protects either leader', () => {
for (const mirroredAttacker of [true, false])
for (const toothA of [false, true])
for (const toothD of [false, true])
for (const [index, defense] of defenses.entries()) {
const effects = battleCardEffects(
mirroredAttacker ? mirror : get('poisonTooth'),
defense,
mirroredAttacker ? get('poisonTooth') : mirror,
get('chemistry'),
toothA,
toothD,
);
assert.equal(effects.attackerDead, (toothA || toothD) && index !== 5);
assert.equal(effects.defenderDead, false);
assert.equal(effects.stunned, false);
assert.equal(effects.noBounty, false);
}
});
void test('copied Artillery attacks both leaders and applies one battle-wide suppression without a bounty', () => {
for (const mirroredAttacker of [true, false])
for (const [aIndex, ad] of defenses.entries())
for (const [dIndex, dd] of defenses.entries()) {
const effects = battleCardEffects(
mirroredAttacker ? mirror : get('artillery'),
ad,
mirroredAttacker ? get('artillery') : mirror,
dd,
);
assert.deepEqual(effects, {
attackerDead: aIndex !== 1 && aIndex !== 3,
defenderDead: dIndex !== 1 && dIndex !== 3,
stunned: true,
noBounty: true,
});
}
});
void test('copied Lasgun uses the same explosion predicate and only actual shields trigger it', () => {
for (const mirroredAttacker of [true, false])
for (const [aIndex, ad] of defenses.entries())
for (const [dIndex, dd] of defenses.entries())
assert.equal(
battleWeaponsExplode(
mirroredAttacker ? mirror : get('lasgun'),
ad,
mirroredAttacker ? get('lasgun') : mirror,
dd,
),
[aIndex, dIndex].some((index) => index === 1 || index === 3),
);
assert.equal(battleWeaponsExplode(get('poison'), get('shield')), false);
assert.equal(battleWeaponsExplode(get('lasgun'), get('shield')), true);
});
void test('Carthag does not grant free Snooper when physical Mirror copies a poison attack', () => {
const shield = get('shield');
const poison = get('poison');
const copied = strongholdBattleEffects(mirror, shield, poison, undefined, true, true, 'carthag');
assert.equal(copied.attackerDead, true);
const ordinary = strongholdBattleEffects(get('projectile'), shield, poison, undefined, true, true, 'carthag');
assert.equal(ordinary.attackerDead, false);
});
void test('empty and Worthless copies have no attack; Stone remains a separate post-reveal outcome', () => {
for (const weapon of [undefined, get('worthless'), stone]) {
const before = structuredClone([mirror, weapon]);
assert.deepEqual(battleCardEffects(mirror, undefined, weapon), {
attackerDead: false,
defenderDead: false,
stunned: false,
noBounty: false,
});
assert.equal(battleWeaponsExplode(mirror, undefined, weapon), false);
assert.deepEqual([mirror, weapon], before);
}
});
void test('only the canonical physical Mirror is a named weapon in plans and Voice', () => {
assert.equal(isMirrorWeapon(mirror), true);
assert.equal(isWeaponCard(mirror), true);
assert.equal(validBattleCardPair(mirror, get('weirdingWay')), true);
assert.equal(defaultVoiceMatch(mirror, 'mirrorWeapon'), true);
assert.equal(playedVoiceMatch(mirror, 'weapon', 'mirrorWeapon'), true);
const counterfeit = { ...mirror, id: 'forged-mirror' };
assert.equal(isMirrorWeapon(counterfeit), false);
assert.equal(isWeaponCard(counterfeit), false);
assert.equal(validBattleCardPair(counterfeit), false);
assert.throws(() => battleCardEffects(mirror, undefined, mirror));
assert.throws(() => battleWeaponsExplode(mirror, undefined, mirror));
});