Files
Dune/tests/native-world-no-field-runtime.test.ts
T
Quintine 4aecf8eb9c
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify native zero-marker battle continuation
2026-10-29 04:59:34 +11:00

249 lines
15 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Game } from '../game/engine';
import { homeworldShipmentActions, homeworldShipmentChoice, richeseHomeworldMarkerChoice } from '../game/homeworld-shipment-options';
import { finishHarassPreparation } from './fixture-harass-withdraw';
import { homeworldContext, homeworldGameIntegrity } from '../game/homeworld-game';
import { homeworldForceGroups } from '../game/homeworld-custody';
import { currentHomeworldOccupationSnapshot, homeworldOccupationMarker,
homeworldOccupationSnapshotPresence } from '../game/homeworld-occupation-history';
import {
createRicheseHomeworldShipmentFixture as fixture,
passRicheseHomeworldShipment as pass,
richeseHomeworldShipmentInventory as inventory,
richeseHomeworldShipmentPlayer as player,
} from './fixture-richese-homeworld-shipment';
const owner = 'richese', foreign = 'homeworld:emperor', native = 'homeworld:richese';
function group(game: Game, location: string) {
const world = homeworldForceGroups(homeworldContext(game), game.homeworlds!.custody!)
.find(item => item.id === location); assert.ok(world);
return world.forces[owner] ?? {normal:0, elite:0};
}
/** Start from real public Advanced setup and native phase actions. Only a later
* fresh shipment opportunity and wallet are staged after the real first entry;
* the intervening battle is intentionally skipped, so this is not a full game. */
function stageNewTurn(game: Game) {
game.turn++;
Object.assign(game, { phase:5, phaseOpening:null, response:null, decision:null,
active:owner, order:game.players.map(p=>p.id),
movementRemaining:game.players.map(p=>p.id), ready:[] });
for (const p of game.players) { p.shipped=false; p.moved=0; }
player(game, owner).spice = 8;
homeworldGameIntegrity(game);
}
for (const value of [0,3,5] as const) void test(`Advanced concealed ${value} marker returns to its native World without materializing reserves`, () => {
const f = fixture({advanced:true, companions:2, value});
let game = f.game;
const initialInventory = inventory(game), original = structuredClone(game);
const view = viewGame(game,owner), token = view.richeseNoField!.private!.tokens.find(row=>row.value===value);
assert.ok(token);
const fresh = richeseHomeworldMarkerChoice(view, foreign, token.id, 0, undefined, {normal:2, elite:0});
assert.ok(fresh.action, fresh.blocked ?? 'The actual foreign entry must be available');
assert.equal(fresh.cost,3);
game = pass(applyAction(game,owner,fresh.action));
assert.ok(!game.pendingHomeworldShipment && player(game,owner).shipped);
assert.deepEqual(player(game,owner).noField!.deployed!.location,{homeworld:foreign});
assert.deepEqual(group(game,foreign),{normal:2,elite:0});
assert.equal(player(game,owner).reserves,player(original,owner).reserves-2);
assert.deepEqual(inventory(game),initialInventory);
stageNewTurn(game);
const before = structuredClone(game), beforeOwner = viewGame(game,owner);
const returning = richeseHomeworldMarkerChoice(beforeOwner,native,token.id,0,foreign,{normal:1,elite:0});
assert.ok(returning.action,returning.blocked ?? 'A distinct foreign-to-native return must be offered');
assert.equal(returning.amount,2);
assert.equal(returning.cost,2);
let returned = pass(applyAction(game,owner,returning.action));
assert.deepEqual(game,before);
assert.equal(player(returned,owner).spice,player(before,owner).spice-2);
assert.equal(player(returned,owner).reserves,player(before,owner).reserves+1);
assert.deepEqual(group(returned,foreign),{normal:1,elite:0});
assert.equal(player(returned,owner).noField!.deployed!.tokenId,token.id);
assert.deepEqual(player(returned,owner).noField!.deployed!.location,{homeworld:native});
assert.equal(player(returned,owner).noField!.lastShipped,token.id);
const observer = viewGame(returned,'emperor').richeseNoField!;
assert.equal(observer.private,null);
assert.deepEqual(observer.public.deployed!.location,{homeworld:native});
assert.equal(observer.public.deployed!.effectiveForces,1);
assert.equal(JSON.stringify(observer.public).includes(token.id),false);
assert.deepEqual(inventory(returned),initialInventory);
homeworldGameIntegrity(returned);
// A later ordinary shipment can move the same still-concealed marker back
// out of its native World with another physically paid companion.
let outgoing = structuredClone(returned);
stageNewTurn(outgoing);
const departure = richeseHomeworldMarkerChoice(viewGame(outgoing,owner),foreign,token.id,0,native,
{normal:1,elite:0});
assert.ok(departure.action, departure.blocked ?? 'The marker can depart its own World');
assert.equal(departure.cost,2);
const nativeBeforeDeparture = player(outgoing,owner).reserves;
outgoing = pass(applyAction(outgoing,owner,departure.action));
assert.deepEqual(player(outgoing,owner).noField!.deployed!.location,{homeworld:foreign});
assert.equal(player(outgoing,owner).reserves,nativeBeforeDeparture-1);
assert.deepEqual(group(outgoing,foreign),{normal:2,elite:0});
assert.deepEqual(inventory(outgoing),initialInventory);
homeworldGameIntegrity(outgoing);
const reserves = player(returned,owner).reserves, own = player(returned,owner);
returned = pass(applyAction(returned,owner,{type:'revealNoField',token:token.id,event:own.noFieldEvent}));
assert.equal(player(returned,owner).noField!.deployed,null);
assert.equal(player(returned,owner).reserves,reserves,
'Revealing at home does not withdraw or duplicate the native reserve pool.');
assert.deepEqual(group(returned,foreign),{normal:1,elite:0});
assert.deepEqual(inventory(returned),initialInventory);
homeworldGameIntegrity(returned);
});
/** Stages the later phase opportunities, but uses real native shipments,
* battle selection, preparation and plan actions. The native reserves and the
* invading group remain the original physical counters. */
for (const value of [0,3,5] as const) void test(`Advanced native battle counts a home No-Field ${value} without duplicating reserves`, () => {
const f = fixture({advanced:true, companions:2, value});
let game = f.game;
const originalInventory = inventory(game), own = viewGame(game,owner);
const token = own.richeseNoField!.private!.tokens.find(row=>row.value===value); assert.ok(token);
const first = richeseHomeworldMarkerChoice(own,foreign,token.id,0,undefined,{normal:2,elite:0});
assert.ok(first.action,first.blocked ?? 'The original marker deployment must be legal');
game = pass(applyAction(game,owner,first.action));
stageNewTurn(game);
const back = richeseHomeworldMarkerChoice(viewGame(game,owner),native,token.id,0,foreign,{normal:1,elite:0});
assert.ok(back.action,back.blocked ?? 'The native return must be legal');
game = pass(applyAction(game,owner,back.action));
const nativeReserves = player(game,owner).reserves;
const nativePhysical = group(game,native);
assert.equal(nativePhysical.normal,nativeReserves);
// Stage Emperor's next real World shipment from its original reserve pool.
game.turn++;
Object.assign(game,{phase:5,phaseOpening:null,response:null,decision:null,
active:'emperor',order:game.players.map(p=>p.id),
movementRemaining:game.players.map(p=>p.id),ready:[]});
for (const p of game.players) {p.shipped=false;p.moved=0;}
player(game,'emperor').spice = 10;
const invasion = homeworldShipmentChoice(viewGame(game,'emperor'),native,
{'homeworld:emperor':{normal:1,elite:0}});
assert.ok(invasion.action,invasion.blocked ?? 'An actual native World invasion must be legal');
game = pass(applyAction(game,'emperor',invasion.action));
assert.deepEqual(player(game,owner).noField!.deployed!.location,{homeworld:native});
homeworldGameIntegrity(game);
// The Battle phase is staged, then its ordinary chooser and simultaneous
// plans execute natively rather than constructing a saved battle object.
Object.assign(game,{phase:6,active:owner,ready:[],response:null,decision:null,phaseOpening:null});
const choice = viewGame(game,owner).battleChoices.find(row=>row.territory===native);
assert.ok(choice, 'The public home marker and invader create a native battle');
const target = choice.attacker === owner ? choice.defender : choice.attacker;
game = applyAction(game,owner,{type:'chooseBattle',territory:native,target});
const preview = viewGame(game,owner).battle!.ownForces!;
assert.equal(preview.normal,nativePhysical.normal,
'The native army is its existing physical pool, regardless of hidden marker value.');
assert.equal(preview.elite,nativePhysical.elite);
game = finishHarassPreparation(game);
for (const actor of [owner,'emperor']) {
const leader = player(game,actor).leaders.find(row=>!row.dead && (!row.usedAt || row.usedAt===native));
assert.ok(leader);
game = applyAction(game,actor,{type:'battlePlan',dial:0,support:0,leader:leader.id,weapon:null,defense:null});
}
assert.equal(game.battle!.revealed,true);
assert.equal(player(game,owner).noField!.deployed,null);
assert.equal(player(game,owner).reserves,nativeReserves);
assert.deepEqual(group(game,native),nativePhysical);
assert.deepEqual(inventory(game),originalInventory);
homeworldGameIntegrity(game);
});
void test('a concealed native zero marker with no reserves creates public battle presence but no phantom force', () => {
const f = fixture({advanced:true, companions:2, value:0});
let game = f.game;
const originalInventory = inventory(game), own = viewGame(game,owner);
const token = own.richeseNoField!.private!.tokens.find(row=>row.value===0); assert.ok(token);
const first = richeseHomeworldMarkerChoice(own,foreign,token.id,0,undefined,{normal:2,elite:0});
assert.ok(first.action, first.blocked ?? 'The initial deployment must be legal');
game = pass(applyAction(game,owner,first.action));
stageNewTurn(game);
const back = richeseHomeworldMarkerChoice(viewGame(game,owner),native,token.id,0,foreign,{normal:1,elite:0});
assert.ok(back.action, back.blocked ?? 'The native return must be legal');
game = pass(applyAction(game,owner,back.action));
// Explicit physical staging: move all native reserves into the Tanks while
// preserving the real returned marker and the one foreign garrison counter.
const richese = player(game,owner);
richese.tanks += richese.reserves;
richese.reserves = 0;
assert.deepEqual(group(game,native),{normal:0,elite:0});
assert.deepEqual(inventory(game),originalInventory);
homeworldGameIntegrity(game);
game.turn++;
Object.assign(game,{phase:5,phaseOpening:null,response:null,decision:null,
active:'emperor',order:game.players.map(p=>p.id),
movementRemaining:game.players.map(p=>p.id),ready:[]});
for (const p of game.players) {p.shipped=false;p.moved=0;}
player(game,'emperor').spice = 10;
const invasion = homeworldShipmentChoice(viewGame(game,'emperor'),native,
{'homeworld:emperor':{normal:1,elite:0}});
assert.ok(invasion.action,invasion.blocked ?? 'The real invading force must be legal');
game = pass(applyAction(game,'emperor',invasion.action));
assert.deepEqual(group(game,native),{normal:0,elite:0});
assert.equal(viewGame(game,'emperor').richeseNoField!.public.deployed!.effectiveForces,1);
const withMarker = currentHomeworldOccupationSnapshot(homeworldContext(game), game.homeworlds!.custody!,
homeworldOccupationMarker(game.players));
assert.deepEqual(homeworldOccupationSnapshotPresence(withMarker,native),['emperor','richese'],
'The native marker keeps the invading force from being alone before reveal.');
Object.assign(game,{phase:6,active:owner,ready:[],response:null,decision:null,phaseOpening:null});
const choice = viewGame(game,owner).battleChoices.find(row=>row.territory===native);
assert.ok(choice, 'The concealed marker remains a public battle participant even with zero native reserves');
const target = choice.attacker === owner ? choice.defender : choice.attacker;
game = applyAction(game,owner,{type:'chooseBattle',territory:native,target});
assert.equal(viewGame(game,owner).battle!.ownForces!.normal,0);
assert.equal(viewGame(game,owner).battle!.ownForces!.elite,0);
game = finishHarassPreparation(game);
for (const actor of [owner,'emperor']) {
const leader = player(game,actor).leaders.find(row=>!row.dead && (!row.usedAt || row.usedAt===native));
assert.ok(leader);
game = applyAction(game,actor,{type:'battlePlan',dial:0,support:0,leader:leader.id,weapon:null,defense:null});
}
assert.equal(game.battle!.revealed,true);
assert.equal(player(game,owner).noField!.deployed,null);
assert.equal(player(game,owner).reserves,0);
assert.deepEqual(group(game,native),{normal:0,elite:0});
const afterReveal = currentHomeworldOccupationSnapshot(homeworldContext(game), game.homeworlds!.custody!,
homeworldOccupationMarker(game.players));
assert.deepEqual(homeworldOccupationSnapshotPresence(afterReveal,native),['emperor'],
'The removed zero marker creates no physical native presence after reveal.');
assert.deepEqual(inventory(game),originalInventory);
homeworldGameIntegrity(game);
// Resolve the native Homeworld's actual Traitor decision and shared final
// opportunity, rather than stopping when the zero marker is revealed.
game = applyAction(game,owner,{type:'traitorCall',call:false});
while (game.battle?.outcomeWindow && !game.battle.outcomeWindow.closed) {
const window = game.battle.outcomeWindow;
const actor = [window.attacker,window.defender].find(id=>!window.ready.includes(id));
assert.ok(actor, 'The final battle opportunity must have an unfinished combatant');
game = applyAction(game,actor,{type:'battleOutcomeReady',event:window.event});
}
assert.equal(game.battle,null, 'The zero-reserve native battle must finish');
assert.equal(game.lastBattleContext!.territory,native);
assert.equal(player(game,owner).noField!.deployed,null);
assert.equal(player(game,owner).reserves,0);
assert.deepEqual(group(game,native),{normal:0,elite:0});
assert.deepEqual(inventory(game),originalInventory);
homeworldGameIntegrity(game);
});
void test('all four legal bot profiles offer a foreign-to-native concealed marker return', () => {
const f = fixture({advanced:true, companions:2, value:3});
let game = f.game;
const own = viewGame(game,owner), token = own.richeseNoField!.private!.tokens.find(row=>row.value===3);
assert.ok(token);
const first = richeseHomeworldMarkerChoice(own,foreign,token.id,0,undefined,{normal:2,elite:0});
assert.ok(first.action, first.blocked ?? 'The original marker deployment must be legal');
game = pass(applyAction(game,owner,first.action));
stageNewTurn(game);
for (let level=0; level<4; level++) {
const view = viewGame(game,owner);
const action = homeworldShipmentActions(view,level).find(candidate => candidate.type === 'homeworldShip' &&
candidate.destination === native && candidate.noFieldFrom === foreign && candidate.noField === token.id);
assert.ok(action, `Profile ${level} should offer the legal marker return`);
assert.doesNotThrow(() => applyAction(game,owner,action));
}
});