180 lines
6.9 KiB
TypeScript
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));
|
|
});
|