Files
Dune/tests/native-world-marker.test.ts
T

219 lines
13 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { applyAction, RuleError, viewGame, type Game } from '../game/engine';
import {
createNativeWorldMarkerFixture, nativeWorldMarkerInventory, nativeWorldMarkerPlan,
type NativeWorldMarkerFixture,
} from './fixture-native-world-marker';
import {
finishRicheseReinforcementsBattle, prepareRicheseReinforcementsPlan,
RICHESE_HARASS, RICHESE_REINFORCEMENTS, richeseReinforcementsPlayer,
sealRicheseReinforcementsPlan,
} from './fixture-richese-reinforcements';
function physicalCards(game: Game) {
return [...game.deck, ...game.discard, ...(game.richeseCache ?? []), ...(game.richeseRemoved ?? []),
...game.players.flatMap(player => player.hand)].map(card => card.id).sort();
}
function primaryPool(game: Game, fixture: NativeWorldMarkerFixture) {
const home = viewGame(game, fixture.richese).homeworlds?.worlds
?.find(world => world.id === fixture.source.homeworld);
assert.ok(home);
return home.forces[fixture.richese];
}
function assertConserved(fixture: NativeWorldMarkerFixture, game: Game) {
assert.deepEqual(physicalCards(game), physicalCards(fixture.initial),
'Every original selected-deck/cache face retains one physical identity.');
for (const player of game.players) {
const before = nativeWorldMarkerInventory(fixture.afterSetup, player.id);
const after = nativeWorldMarkerInventory(game, player.id);
assert.deepEqual([after.total, after.normal, after.elite], [before.total, before.normal, before.elite],
`${player.faction}'s native reserves count once; special counters remain their own physical type.`);
assert.equal(after.total, 20);
if (player.faction === 'ixians') assert.equal(after.elite, 7);
assert.ok(player.hand.length <= (player.faction === 'harkonnen' ? 8 : 4));
}
const original = richeseReinforcementsPlayer(fixture.afterSetup, fixture.richese).noField!;
const current = richeseReinforcementsPlayer(game, fixture.richese).noField!;
assert.deepEqual(current.tokens, original.tokens, 'No token is minted, replaced or relabeled.');
assert.equal(current.lastShipped, fixture.token.id, 'Reveal preserves the last real shipment identity.');
}
function assertSpent(game: Game, card: string) {
assert.equal(game.discard.filter(face => face.id === card).length, 1);
assert.ok(game.players.every(player => player.hand.every(face => face.id !== card)));
assert.ok(game.deck.every(face => face.id !== card));
}
/** Preview and the first seal must not perform the eventual native reveal. */
function assertStillConcealed(fixture: NativeWorldMarkerFixture, game: Game) {
const owner = richeseReinforcementsPlayer(game, fixture.richese);
const before = richeseReinforcementsPlayer(fixture.game, fixture.richese);
assert.equal(game.battle!.revealed, false);
assert.deepEqual(owner.noField, before.noField);
assert.equal(owner.noFieldEvent, before.noFieldEvent);
assert.equal(owner.reserves, fixture.source.reservesBeforeReveal);
assert.deepEqual(primaryPool(game, fixture), { normal: owner.reserves, elite: 0 });
assert.equal(owner.forces[fixture.location] ?? 0, 0, 'A concealed marker is not a physical counter.');
assert.equal(owner.tanks, before.tanks);
}
for (const family of ['choam', 'ix+choam'] as const) {
// Opposite representative slots cover both paths without a Cartesian matrix.
const slots = [family === 'choam' ? 'weapon' : 'defense'] as const;
for (const slot of slots)
void test(`${family} World marker three funds R in the ${slot} slot only after its real primary reserve debit`, () => {
const fixture = createNativeWorldMarkerFixture({ family, marker: 3, reserves: 6 });
const original = structuredClone(fixture.game);
const preview = viewGame(fixture.game, fixture.richese).battle!.reinforcements!;
assert.deepEqual(fixture.game, original, 'The owner forecast does not materialize forces or update history.');
assert.equal(preview.blocked, null);
assert.deepEqual(preview.noField, { materializes: 3, reservesAfterReveal: 3 });
assertStillConcealed(fixture, fixture.game);
const first = prepareRicheseReinforcementsPlan(fixture.game, nativeWorldMarkerPlan(fixture, 'reinforcements', slot));
assertStillConcealed(fixture, first);
const revealed = sealRicheseReinforcementsPlan(first, fixture.plans.opponent);
const owner = richeseReinforcementsPlayer(revealed, fixture.richese);
assert.equal(revealed.battle!.revealed, true);
assert.deepEqual(primaryPool(revealed, fixture), { normal: 3, elite: 0 });
assert.equal(owner.reserves, 3, 'Materialization spends the same native primary counters once.');
assert.equal(owner.forces[fixture.location], 3);
assert.equal(owner.tanks, 0, 'The provisional fee settles at the actual result, not at first seal.');
assert.equal(owner.noField!.deployed, null);
assertConserved(fixture, revealed);
const done = finishRicheseReinforcementsBattle(revealed);
const after = richeseReinforcementsPlayer(done, fixture.richese);
assert.equal(done.lastBattleContext!.result, 'normal');
assert.equal(done.lastBattleContext!.winner, fixture.opponent);
assert.deepEqual(primaryPool(done, fixture), { normal: 0, elite: 0 });
assert.equal(after.reserves, 0);
assert.equal(after.tanks, 6, 'Three materialized casualties plus the three-reserve fee, not nine counters.');
assert.equal(after.battleLosses, 6);
assert.equal(after.forces[fixture.location] ?? 0, 0);
assertSpent(done, RICHESE_REINFORCEMENTS);
assertConserved(fixture, done);
});
for (const [marker, reserves, materializes] of [[0, 6, 0], [5, 8, 5]] as const)
void test(`${family} World funded marker ${marker} keeps its physical token and pays from the remaining native reserves`, () => {
const fixture = createNativeWorldMarkerFixture({ family, marker, reserves });
const preview = viewGame(fixture.game, fixture.richese).battle!.reinforcements!;
assert.equal(preview.blocked, null);
assert.deepEqual(preview.noField, { materializes, reservesAfterReveal: reserves - materializes });
const first = prepareRicheseReinforcementsPlan(fixture.game, nativeWorldMarkerPlan(fixture));
assertStillConcealed(fixture, first);
const revealed = sealRicheseReinforcementsPlan(first, fixture.plans.opponent);
assert.deepEqual(primaryPool(revealed, fixture), { normal: reserves - materializes, elite: 0 });
assert.equal(richeseReinforcementsPlayer(revealed, fixture.richese).forces[fixture.location] ?? 0, materializes);
const done = finishRicheseReinforcementsBattle(revealed);
const after = richeseReinforcementsPlayer(done, fixture.richese);
assert.equal(after.reserves, reserves - materializes - 3);
assert.deepEqual(primaryPool(done, fixture), { normal: after.reserves, elite: 0 });
assert.equal(after.tanks, materializes + 3);
assertSpent(done, RICHESE_REINFORCEMENTS);
assertConserved(fixture, done);
});
for (const [marker, reserves, materializes] of [[3, 5, 3], [5, 3, 3]] as const)
for (const slot of slots)
void test(`${family} World marker ${marker}/${reserves} refuses short R in ${slot} unchanged, without borrowing a future H return`, () => {
const fixture = createNativeWorldMarkerFixture({ family, marker, reserves });
const before = structuredClone(fixture.game);
const preview = viewGame(fixture.game, fixture.richese).battle!.reinforcements!;
assert.ok(preview.blocked);
assert.deepEqual(preview.noField, { materializes, reservesAfterReveal: reserves - materializes });
assert.equal(preview.normal + preview.elite, reserves - materializes);
assert.equal(viewGame(fixture.game, fixture.richese).battle!.harassWithdraw!.blocked, null);
assert.deepEqual(fixture.game, before);
// This exact original marker-three/reserves-five native position used
// to accept the raw first seal even though its owner preview refused it.
const rawR = nativeWorldMarkerPlan(fixture, 'reinforcements', slot);
delete rawR.action.reinforcementsSources;
for (const plan of [rawR, nativeWorldMarkerPlan(fixture, 'both', slot)]) {
assert.throws(() => applyAction(fixture.game, plan.actor, plan.action), RuleError);
assert.deepEqual(fixture.game, before, 'Rejection changes no wallet, cards, token, history, counters or windows.');
assert.deepEqual(fixture.game.battle!.plans, {});
}
assertStillConcealed(fixture, fixture.game);
const first = prepareRicheseReinforcementsPlan(fixture.game, nativeWorldMarkerPlan(fixture, 'harass', slot));
assertStillConcealed(fixture, first);
const revealed = sealRicheseReinforcementsPlan(first, fixture.plans.opponent);
assert.deepEqual(primaryPool(revealed, fixture), { normal: reserves - materializes, elite: 0 });
assert.equal(richeseReinforcementsPlayer(revealed, fixture.richese).forces[fixture.location], materializes,
'A five marker capped at three reserves reveals three, not five.');
const done = finishRicheseReinforcementsBattle(revealed);
const after = richeseReinforcementsPlayer(done, fixture.richese);
assert.equal(after.reserves, reserves - materializes + 1);
assert.deepEqual(primaryPool(done, fixture), { normal: after.reserves, elite: 0 },
'The real undialed counter deposits in the native primary World, not a foreign visitor pool.');
assert.equal(after.tanks, 2, 'Only the dialed counters die under H-only fallback.');
assertSpent(done, RICHESE_HARASS);
assert.ok(after.hand.some(card => card.id === RICHESE_REINFORCEMENTS), 'The rejected R remains unspent.');
assertConserved(fixture, done);
});
for (const slot of slots)
void test(`${family} World R+H in ${slot} charges native reserves before depositing the real undialed survivor`, () => {
const fixture = createNativeWorldMarkerFixture({ family, marker: 3, reserves: 6 });
const first = prepareRicheseReinforcementsPlan(fixture.game, nativeWorldMarkerPlan(fixture, 'both', slot));
assertStillConcealed(fixture, first);
const revealed = sealRicheseReinforcementsPlan(first, fixture.plans.opponent);
assert.deepEqual(primaryPool(revealed, fixture), { normal: 3, elite: 0 });
const done = finishRicheseReinforcementsBattle(revealed);
const after = richeseReinforcementsPlayer(done, fixture.richese);
assert.deepEqual(primaryPool(done, fixture), { normal: 1, elite: 0 });
assert.equal(after.reserves, 1, 'Three pay first; the one real undialed survivor returns afterwards.');
assert.equal(after.tanks, 5);
assert.equal(after.battleLosses, 5);
assertSpent(done, RICHESE_REINFORCEMENTS);
assertSpent(done, RICHESE_HARASS);
assertConserved(fixture, done);
});
for (const marker of [0, 3, 5] as const)
for (const slot of slots)
void test(`${family} Advanced World marker ${marker} pays R from its funded turn-one primary World in ${slot}`, () => {
const fixture = createNativeWorldMarkerFixture({ family, advanced: true, marker });
const before = richeseReinforcementsPlayer(fixture.game, fixture.richese);
const preview = viewGame(fixture.game, fixture.richese).battle!.reinforcements!;
assert.equal(preview.blocked, null);
assert.deepEqual(preview.noField, { materializes: marker, reservesAfterReveal: 20 - marker });
const first = prepareRicheseReinforcementsPlan(fixture.game, nativeWorldMarkerPlan(fixture, 'reinforcements', slot));
assertStillConcealed(fixture, first);
const revealed = sealRicheseReinforcementsPlan(first, fixture.plans.opponent);
assert.equal(richeseReinforcementsPlayer(revealed, fixture.richese).forces[fixture.location] ?? 0, marker);
assert.deepEqual(primaryPool(revealed, fixture), { normal: 20 - marker, elite: 0 });
const done = finishRicheseReinforcementsBattle(revealed);
const after = richeseReinforcementsPlayer(done, fixture.richese);
assert.equal(after.reserves, 17 - marker);
assert.deepEqual(primaryPool(done, fixture), { normal: 17 - marker, elite: 0 });
assert.equal(after.tanks, marker + 3);
assert.equal(after.spice, before.spice - Number(fixture.plans.reinforcements.action.support),
'The original physical Advanced support is still paid, not increased by two score strength.');
assertSpent(done, RICHESE_REINFORCEMENTS);
assertConserved(fixture, done);
});
void test(`${family} Advanced World marker five settles R then deposits four H survivors with original support`, () => {
const fixture = createNativeWorldMarkerFixture({ family, advanced: true, marker: 5 });
const before = richeseReinforcementsPlayer(fixture.game, fixture.richese);
const first = prepareRicheseReinforcementsPlan(fixture.game, nativeWorldMarkerPlan(fixture, 'both'));
const revealed = sealRicheseReinforcementsPlan(first, fixture.plans.opponent);
assert.deepEqual(primaryPool(revealed, fixture), { normal: 15, elite: 0 });
const done = finishRicheseReinforcementsBattle(revealed);
const after = richeseReinforcementsPlayer(done, fixture.richese);
assert.equal(after.reserves, 16, 'Fifteen minus the fee plus four actual undialed survivors.');
assert.deepEqual(primaryPool(done, fixture), { normal: 16, elite: 0 });
assert.equal(after.tanks, 4);
assert.equal(after.spice, before.spice - 1);
assertSpent(done, RICHESE_REINFORCEMENTS);
assertSpent(done, RICHESE_HARASS);
assertConserved(fixture, done);
});
}