Compose native Stone and Reinforcements without a physical-token bonus

This commit is contained in:
Quintine committed 2026-10-16 07:10:18 +11:00
1 parent be0644c0e9
commit 842fd72761
15 files changed
+280 -65

No files matched your search

+103
View File
@@ -0,0 +1,103 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {viewGame, type Game} from '../game/engine';
import {holdOccupyCardsTraitor} from './fixture-ecaz-occupy-cards';
import {
STONE_CARD, MIRROR_CARD, stoneHarassOccupyFixture, stoneHarassPlayer,
takeStoneHarassCard, sealStoneHarassPlans, finishStoneHarassBattle, traitorIdentities,
} from './fixture-stone-harass';
const reinforcement = 'ecaz-reinforcements';
function prepare({lead = 'ecaz', holder = 'lead', mirror = false, opponentForces = 6,
variableDial = 1}: {lead?: 'ecaz' | 'ally'; holder?: 'lead' | 'opponent'; mirror?: boolean;
opponentForces?: number; variableDial?: number} = {}) {
const source = stoneHarassOccupyFixture({lead, harass: false,
stone: mirror ? 'opponent' : 'harass', mirror, variableDial,
support: variableDial, ecazForces: 5, allyForces: 4, opponentForces});
const owner = holder === 'lead' ? source.lead : source.opponent;
takeStoneHarassCard(source.game, owner, reinforcement);
const plan = source.plans.find(step => step.actor === owner)!.action;
if (plan.weapon) plan.defense = reinforcement;
else plan.weapon = reinforcement;
return {source, owner};
}
const count = (game: Game, id: string) => game.discard.filter(card => card.id === id).length;
const has = (game: Game, owner: string, id: string) => stoneHarassPlayer(game, owner).hand.some(card => card.id === id);
void test('same-plan Stone and Reinforcements pay only the actual fixed or variable card user without adding physical strength', () => {
for (const lead of ['ecaz', 'ally'] as const) for (const winning of [false, true]) {
const {source, owner} = prepare({lead, opponentForces: winning ? 4 : 6});
const before = structuredClone(source.game);
const done = finishStoneHarassBattle(sealStoneHarassPlans(source), {mode: 'ignore'});
assert.equal(done.lastBattleContext?.winner, winning ? source.lead : source.opponent,
'the five undialed coalition counters beat four but lose to six; Reinforcements adds no tokens');
const delta = (id: string, field: 'reserves' | 'tanks') =>
stoneHarassPlayer(done, id)[field] - stoneHarassPlayer(before, id)[field];
assert.equal(delta(owner, 'reserves'), -3);
assert.equal(delta(owner === source.ecaz ? source.ally : source.ecaz, 'reserves'), 0);
assert.equal(delta(source.ecaz, 'tanks'), winning ? lead === 'ecaz' ? 6 : 3 : lead === 'ecaz' ? 8 : 5);
assert.equal(delta(source.ally, 'tanks'), winning ? lead === 'ally' ? 4 : 1 : lead === 'ally' ? 7 : 4);
assert.equal(stoneHarassPlayer(done, source.ecaz).forces[source.location] ?? 0, winning ? 2 : 0);
assert.equal(stoneHarassPlayer(done, source.ally).forces[source.location] ?? 0, winning ? 3 : 0);
assert.equal(count(done, reinforcement), 1);
assert.equal(has(done, source.lead, STONE_CARD), winning);
assert.equal(count(done, STONE_CARD), winning ? 0 : 1);
assert.equal(done.battle, null);
}
});
void test('opposing Reinforcements cannot turn a five-token army into seven and still pays its original elite reserve cost', () => {
for (const lead of ['ecaz', 'ally'] as const) {
const {source, owner} = prepare({lead, holder: 'opponent', variableDial: 0, opponentForces: 5});
const player = stoneHarassPlayer(source.game, owner);
player.reserves = 3; player.tanks = 12;
player.elites!.reserves = 1; player.elites!.tanks = 4;
const cost = viewGame(source.game, owner).battle!.reinforcements!;
assert.equal(cost.normal, 2); assert.equal(cost.elite, 1);
const done = finishStoneHarassBattle(sealStoneHarassPlans(source), {mode: 'ignore'});
assert.equal(done.lastBattleContext?.winner, source.lead, 'six physical undialed counters beat five');
assert.equal(stoneHarassPlayer(done, owner).reserves, 0);
assert.equal(stoneHarassPlayer(done, owner).tanks, 20);
assert.equal(stoneHarassPlayer(done, owner).elites!.tanks, 5);
assert.equal(count(done, reinforcement), 1);
assert.equal(has(done, source.lead, STONE_CARD), true);
}
});
void test('the independent copied Stone mode preserves Mirror custody and the reserve fee', () => {
for (const lead of ['ecaz', 'ally'] as const) {
const {source, owner} = prepare({lead, mirror: true, variableDial: 0, opponentForces: 5});
const beforeReserves = stoneHarassPlayer(source.game, owner).reserves;
const done = finishStoneHarassBattle(sealStoneHarassPlans(source),
{modes: {[source.lead]: 'kill', [source.opponent]: 'ignore'}});
assert.equal(done.lastBattleContext?.winner, source.lead);
assert.equal(stoneHarassPlayer(done, owner).reserves, beforeReserves - 3);
assert.equal(has(done, source.lead, MIRROR_CARD), true);
assert.equal(count(done, MIRROR_CARD), 0);
assert.equal(count(done, STONE_CARD), 1);
assert.equal(count(done, reinforcement), 1);
for (const actor of [source.lead, source.opponent])
assert.equal(stoneHarassPlayer(done, actor).leaders.find(leader => leader.id === source.leaders[actor])!.dead, true);
}
});
void test('opposing successful Traitor overrides Stone kill but does not waive the Reinforcements reserve cost', () => {
for (const lead of ['ecaz', 'ally'] as const) {
const {source, owner} = prepare({lead, opponentForces: 4});
const physical = traitorIdentities(source.game);
const leader = stoneHarassPlayer(source.game, source.lead).leaders.find(leader =>
!leader.dead && !leader.usedAt && physical.has(leader.id))!;
assert.ok(leader);
source.plans.find(step => step.actor === source.lead)!.action.leader = leader.id;
holdOccupyCardsTraitor(source.game, source.opponent, leader.id);
const before = stoneHarassPlayer(source.game, owner).reserves;
const done = finishStoneHarassBattle(sealStoneHarassPlans(source), {mode: 'kill', traitors: [source.opponent]});
assert.equal(done.lastBattleContext?.result, 'traitor');
assert.equal(done.lastBattleContext?.winner, source.opponent);
assert.equal(stoneHarassPlayer(done, owner).reserves, before - 3);
assert.equal(stoneHarassPlayer(done, source.opponent).leaders.find(candidate =>
candidate.id === source.leaders[source.opponent])!.dead, false, 'the defeated Stone attack cannot kill the winning caller’s leader');
assert.equal(count(done, reinforcement), 1);
assert.equal(count(done, STONE_CARD), 1);
}
});