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.
98 lines
2.8 KiB
TypeScript
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 };
|
|
}
|