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.
This commit is contained in:
1 parent
ab0ab52849
commit
f0d3d9c464
31 files changed
+2524
-138
No files matched your search
@@ -0,0 +1,125 @@
|
||||
import {
|
||||
casualtyOptions,
|
||||
validCombatForces,
|
||||
type Casualties,
|
||||
type CombatForces,
|
||||
} from './combat';
|
||||
import { JACURUTU_SIETCH } from './discoveries';
|
||||
|
||||
/** GF9 Ecaz & Moritani rulebook, p. 13. */
|
||||
export const JACURUTU_BATTLE_INCOME_RULE =
|
||||
'If you win a battle in Jacurutu Sietch, gain one spice for each opposing undialed force sent to the Tanks.';
|
||||
|
||||
export type JacurutuTankLosses = Readonly<{
|
||||
normal: number;
|
||||
elite: number;
|
||||
}>;
|
||||
|
||||
export type JacurutuBattleIncomeQuote =
|
||||
| Readonly<{
|
||||
kind: 'income';
|
||||
winner: string;
|
||||
opponent: string;
|
||||
amount: number;
|
||||
sentToTanks: JacurutuTankLosses;
|
||||
dialed: readonly Casualties[];
|
||||
}>
|
||||
| Readonly<{
|
||||
kind: 'allocationRequired';
|
||||
winner: string;
|
||||
opponent: string;
|
||||
sentToTanks: JacurutuTankLosses;
|
||||
choices: readonly Readonly<{ dialed: Casualties; amount: number }>[];
|
||||
}>;
|
||||
|
||||
export type JacurutuBattleIncomeInput = Readonly<{
|
||||
territory: string;
|
||||
jacurutuRevealed: boolean;
|
||||
winner: string | null;
|
||||
opponent: string | null;
|
||||
opponentForces: CombatForces | null;
|
||||
opponentDial: number | null;
|
||||
opponentSupport: number | null;
|
||||
/** Actual opposing counters destroyed by this battle, after any future
|
||||
* withdrawal or other non-Tanks movement has been removed from the pool. */
|
||||
sentToTanks: JacurutuTankLosses | null;
|
||||
}>;
|
||||
|
||||
export class JacurutuBattleIncomeError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'JacurutuBattleIncomeError';
|
||||
}
|
||||
}
|
||||
|
||||
function whole(value: number): boolean {
|
||||
return Number.isSafeInteger(value) && value >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes the automatic bank income from exact settled-battle evidence.
|
||||
*
|
||||
* A numeric Battle Wheel dial is not a physical counter count in Advanced
|
||||
* combat. When different legal ordinary/elite dial allocations would change
|
||||
* the income, the quote preserves those choices instead of inventing one.
|
||||
*/
|
||||
export function quoteJacurutuBattleIncome(
|
||||
input: JacurutuBattleIncomeInput,
|
||||
): JacurutuBattleIncomeQuote | null {
|
||||
if (
|
||||
input.territory !== JACURUTU_SIETCH ||
|
||||
!input.jacurutuRevealed ||
|
||||
input.winner === null
|
||||
)
|
||||
return null;
|
||||
const forces = input.opponentForces;
|
||||
const sent = input.sentToTanks;
|
||||
if (
|
||||
!input.opponent ||
|
||||
input.opponent === input.winner ||
|
||||
!forces ||
|
||||
!validCombatForces(forces) ||
|
||||
input.opponentDial === null ||
|
||||
input.opponentSupport === null ||
|
||||
!sent ||
|
||||
!whole(sent.normal) ||
|
||||
!whole(sent.elite) ||
|
||||
sent.normal > forces.normal ||
|
||||
sent.elite > forces.elite
|
||||
)
|
||||
throw new JacurutuBattleIncomeError(
|
||||
'Jacurutu income needs the opposing plan and exact forces sent to the Tanks.',
|
||||
);
|
||||
const dialed = casualtyOptions(
|
||||
forces,
|
||||
input.opponentDial,
|
||||
input.opponentSupport,
|
||||
).filter(
|
||||
(choice) => choice.normal <= sent.normal && choice.elite <= sent.elite,
|
||||
);
|
||||
if (!dialed.length)
|
||||
throw new JacurutuBattleIncomeError(
|
||||
'Jacurutu income has no physical dial allocation matching the settled Tanks losses.',
|
||||
);
|
||||
const choices = dialed.map((choice) => ({
|
||||
dialed: { ...choice },
|
||||
amount: sent.normal + sent.elite - choice.normal - choice.elite,
|
||||
}));
|
||||
const amounts = new Set(choices.map((choice) => choice.amount));
|
||||
if (amounts.size > 1)
|
||||
return {
|
||||
kind: 'allocationRequired',
|
||||
winner: input.winner,
|
||||
opponent: input.opponent,
|
||||
sentToTanks: { ...sent },
|
||||
choices,
|
||||
};
|
||||
return {
|
||||
kind: 'income',
|
||||
winner: input.winner,
|
||||
opponent: input.opponent,
|
||||
amount: choices[0].amount,
|
||||
sentToTanks: { ...sent },
|
||||
dialed: choices.map((choice) => choice.dialed),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user