113 lines
6.3 KiB
TypeScript
113 lines
6.3 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, RuleError, viewGame, type Game } from '../game/engine';
|
|
import {
|
|
completedAdvancedNativeSkillsGame, stageAdvancedNativeSkillBattle,
|
|
openAdvancedNativeSkillBattle, finishAdvancedNativeSkillAftermath,
|
|
} from './fixture-advanced-native-skills';
|
|
import { takeHarassCard } from './fixture-harass-withdraw';
|
|
|
|
const player = (game: Game, id: string) => game.players.find(p => p.id === id)!;
|
|
|
|
/** Conserved rule-unit position after original setup, not a played phase history. */
|
|
function usedDefense(source: 'late' | 'shield' = 'late', legacy = false) {
|
|
let game = stageAdvancedNativeSkillBattle(completedAdvancedNativeSkillsGame({
|
|
family: 'richese', requestedSkill: 'diplomat', skillOwner: 'emperor', opponents: ['guild'],
|
|
}), 'emperor', 'guild');
|
|
const worthless = takeHarassCard(game, 'emperor', game.deck.find(c => c.kind === 'worthless')!.id);
|
|
const ownPoison = source === 'late'
|
|
? takeHarassCard(game, 'emperor', game.deck.find(c => c.kind === 'poison')!.id) : null;
|
|
const enemyPoison = takeHarassCard(game, 'guild', game.deck.find(c => c.kind === 'poison')!.id);
|
|
const shield = source === 'shield'
|
|
? takeHarassCard(game, 'guild', game.deck.find(c => c.kind === 'shield')!.id) : null;
|
|
const index = game.richeseCache!.findIndex(c => c.effect === 'portableSnooper');
|
|
assert.ok(index >= 0);
|
|
const portable = game.richeseCache!.splice(index, 1)[0];
|
|
const lateOwner = source === 'late' ? 'guild' : 'emperor';
|
|
player(game, lateOwner).hand.push(portable);
|
|
game = openAdvancedNativeSkillBattle(game, 'emperor', 'guild');
|
|
if (legacy) game.battle!.diplomatDefenseVersion = 2;
|
|
game = applyAction(game, 'emperor', { type: 'battlePlan', leader: 'emperor-0', dial: 0, support: 0,
|
|
weapon: ownPoison?.id ?? worthless.id, defense: ownPoison ? worthless.id : null });
|
|
game = applyAction(game, 'guild', { type: 'battlePlan', leader: 'guild-1', dial: 0, support: 0,
|
|
weapon: enemyPoison.id, defense: shield?.id });
|
|
return { game, worthless: worthless.id, portable: portable.id, lateOwner };
|
|
}
|
|
|
|
function copy(game: Game, card: string | null) {
|
|
assert.ok(game.decision?.kind === 'diplomatDefense');
|
|
return applyAction(game, 'emperor', { type: 'decision', event: game.decision!.event, card });
|
|
}
|
|
|
|
void test('a newly used late defense offers its actual role without reopening an earlier false traitor vote', () => {
|
|
const f = usedDefense();
|
|
let game = applyAction(f.game, 'emperor', { type: 'traitorCall', call: false });
|
|
game = applyAction(game, 'guild', { type: 'portableSnooper', event: game.battle!.event, card: f.portable });
|
|
assert.equal(game.decision?.kind, 'diplomatDefense');
|
|
assert.equal(game.battle!.traitorCalls.emperor, false);
|
|
assert.equal(game.battle!.diplomatDefense!.source, f.portable);
|
|
assert.equal(game.battle!.diplomatDefense!.kind, 'snooper');
|
|
assert.ok(viewGame(game, 'emperor').battle!.cards.some(c => c.id === f.portable));
|
|
const plans = structuredClone(game.battle!.plans);
|
|
game = copy(game, f.worthless);
|
|
assert.deepEqual(game.battle!.plans, plans);
|
|
const beforeDuplicate = structuredClone(game);
|
|
assert.throws(() => applyAction(game, 'emperor', { type: 'traitorCall', call: false }), RuleError);
|
|
assert.deepEqual(game, beforeDuplicate);
|
|
game = finishAdvancedNativeSkillAftermath(applyAction(game, 'guild', { type: 'traitorCall', call: false }));
|
|
assert.equal(player(game, 'emperor').leaders.find(l => l.id === 'emperor-0')!.dead, false);
|
|
assert.equal(game.discard.filter(c => c.id === f.worthless).length, 1);
|
|
assert.equal(game.discard.filter(c => c.id === f.portable).length, 1);
|
|
});
|
|
|
|
void test('declining an earlier copy preserves a later own Portable Snooper without reopening the skill', () => {
|
|
const f = usedDefense('shield');
|
|
let game = copy(f.game, null);
|
|
game = applyAction(game, 'emperor', { type: 'portableSnooper', event: game.battle!.event, card: f.portable });
|
|
assert.equal(game.decision, null);
|
|
assert.equal(game.battle!.diplomatDefense!.stage, 'declined');
|
|
assert.equal(game.battle!.lateDefense!.emperor, f.portable);
|
|
assert.ok(viewGame(game, 'guild').battle!.cards.some(c => c.id === f.portable));
|
|
game = applyAction(game, 'emperor', { type: 'traitorCall', call: false });
|
|
game = finishAdvancedNativeSkillAftermath(applyAction(game, 'guild', { type: 'traitorCall', call: false }));
|
|
assert.equal(player(game, 'emperor').leaders.find(l => l.id === 'emperor-0')!.dead, false);
|
|
assert.ok(player(game, 'emperor').hand.some(c => c.id === f.worthless), 'A declined copy adds no mandatory Worthless disposal');
|
|
});
|
|
|
|
void test('a copied Diplomat defense cannot gain a second actual defense through the late slot', () => {
|
|
const f = usedDefense('shield');
|
|
const game = copy(f.game, f.worthless), before = structuredClone(game);
|
|
assert.throws(() => applyAction(game, 'emperor', {
|
|
type: 'portableSnooper', event: game.battle!.event, card: f.portable,
|
|
}), RuleError);
|
|
assert.deepEqual(game, before);
|
|
});
|
|
|
|
void test('a successful traitor declaration does not open a fresh late-defense copy opportunity', () => {
|
|
const f = usedDefense();
|
|
const owner = player(f.game, 'emperor'), identity = 'guild-1';
|
|
if (!owner.traitors.includes(identity)) {
|
|
const returned = owner.traitors[0], donor = f.game.players.find(p => p.traitors.includes(identity));
|
|
if (donor) donor.traitors[donor.traitors.indexOf(identity)] = returned;
|
|
else {
|
|
const index = f.game.traitorReserve!.indexOf(identity); assert.ok(index >= 0);
|
|
f.game.traitorReserve![index] = returned;
|
|
}
|
|
owner.traitors[0] = identity;
|
|
}
|
|
let game = applyAction(f.game, 'emperor', { type: 'traitorCall', call: true });
|
|
game = applyAction(game, 'guild', { type: 'portableSnooper', event: game.battle!.event, card: f.portable });
|
|
assert.equal(game.battle!.traitorCalls.emperor, true);
|
|
assert.equal(game.battle!.diplomatDefense, undefined);
|
|
assert.equal(game.decision, null);
|
|
});
|
|
|
|
void test('an already-open printed-only capability keeps its original late-defense continuation', () => {
|
|
const f = usedDefense('late', true);
|
|
let game = applyAction(f.game, 'emperor', { type: 'traitorCall', call: false });
|
|
game = applyAction(game, 'guild', { type: 'portableSnooper', event: game.battle!.event, card: f.portable });
|
|
assert.equal(game.battle!.traitorCalls.emperor, false);
|
|
assert.equal(game.battle!.diplomatDefense, undefined);
|
|
assert.equal(game.decision, null);
|
|
});
|