Files
Dune/tests/fixture-discovery-battle.ts
Quintine f0d3d9c464 Prototype Jacurutu income, Testing Station and Shrine effects
Connect automatic unambiguous Jacurutu physical-force income, a sole-occupant
Testing Station storm choice and Shrine reciprocal physical card roles to
engine actions, player controls, legal AI and saved continuation. Preserve
Weather Control exclusion, exact card/force custody, private hand boundaries
and committed effects after restart. Shared role checks cover existing ordinary
and special Karama paths; reserved cards cannot enter another pending use.

Keep Orgiz, mixed-force Jacurutu allocation and contested Cistern/Testing
Station benefits explicitly pending. These are bounded prototypes; complete
Discovery/Advanced/expansion and publication gates remain closed.

Validation: types, lint and 4,345 offline tests passed (32 added), production
build passed, and 40 HTTP tests passed. Independent cross-reviews repaired
reserved-card deadlock, physical-card selection, alternate-role permission,
storm source/token binding and public-entitlement bot ordering. The first broad
run found five failures; focused repairs preceded the stable repeated check.

Browser A5EJQ2W4 used genuine Discovery setup/reveal/free entry with targeted QA
positioning. A human increased storm movement from two to three sectors,
moving sector 6 to 9. Refresh restored station forces and the private Snooper.
This was targeted control/recovery evidence, not an unmodified full game.
All 187 opening saved rooms remained unchanged; 31 QA/HTTP rooms were added
(218 total). Reused the running server; no restart or save reset.

Source-bound report:
/home/quintine/.local/share/dune/checkpoints/20260913-discovery-location-effects/verification-final/report.json
Source fingerprint: beecb85ab7ec39319115389c115434d161591e8549802c542a6343a0fc19bea6
Durations: check 140540ms; build 6741ms; integration 24956ms.
Aggregate goal counter 63,116,444 to 64,295,091 at final verification:
1,178,647 tokens before final Git/push overhead for three bounded functions.
Standing authorization: Push all from now on.
2026-09-13 14:50:09 +10:00

98 lines
2.8 KiB
TypeScript

import assert from 'node:assert/strict';
import { applyAction } from '../game/engine';
import { JACURUTU_SIETCH } from '../game/discoveries';
import { discoveryFixture, putDiscovery } from './fixture-discovery';
export function discoveryBattleBeforeFinalVote(
advanced = false,
ids: [string, string, string] = ['a', 'g', 'f'],
attackerTraitor = false,
) {
let game = discoveryFixture(advanced, ids);
const attacker = game.players[0];
const defender = game.players[1];
const token = putDiscovery(game, 'jacurutu-sietch');
token.revealedTurn = game.turn;
for (const player of game.players) {
player.forces = {};
player.reserves = 20;
player.tanks = 0;
player.spice = 20;
player.hand = [];
player.traitors = [];
for (const leader of player.leaders) leader.strength = 0;
}
attacker.forces[`${JACURUTU_SIETCH}:0`] = 5;
attacker.reserves = 15;
defender.forces[`${JACURUTU_SIETCH}:0`] = 5;
defender.reserves = 15;
attacker.leaders[0].strength = 5;
defender.leaders[0].strength = 1;
if (attackerTraitor) attacker.traitors = [defender.leaders[0].id];
Object.assign(game, {
phase: 6,
active: attacker.id,
ready: [],
response: null,
decision: null,
phaseOpening: null,
battle: null,
lastBattle: [],
lastBattleContext: undefined,
storm: 18,
});
game = applyAction(game, attacker.id, {
type: 'chooseBattle',
territory: JACURUTU_SIETCH,
target: defender.id,
});
for (const id of [attacker.id, defender.id])
if (game.battle?.preLeader && !game.battle.preLeader.ready.includes(id))
game = applyAction(game, id, {
type: 'battlePreparationReady',
event: game.battle.event,
});
for (
let step = 0;
step < 30 && (game.response || game.battle?.preparation);
step++
) {
if (game.response)
game = applyAction(
game,
game.players.find(
(player) => !game.response!.passed.includes(player.id),
)!.id,
{ type: 'passResponse' },
);
else
game = applyAction(game, game.battle!.preparation!.owner, {
type: 'declineBattlePower',
});
}
assert.equal(game.response, null);
assert.equal(game.battle?.preparation, undefined);
if (game.decision?.kind === 'fullPlanOffer')
game = applyAction(game, game.decision.player, {
type: 'decision',
decline: true,
});
game = applyAction(game, attacker.id, {
type: 'battlePlan',
dial: 1,
support: advanced ? 1 : 0,
leader: attacker.leaders[0].id,
});
game = applyAction(game, defender.id, {
type: 'battlePlan',
dial: 2,
support: advanced ? 2 : 0,
leader: defender.leaders[0].id,
});
game = applyAction(game, attacker.id, {
type: 'traitorCall',
call: attackerTraitor,
});
return { game, attacker: attacker.id, defender: defender.id };
}