Files
Dune/game/ecaz-revival-cohort.ts
T

98 lines
4.3 KiB
TypeScript

import type { Leader } from './cards';
/** Native ordinary-disc accounting. Duke contributes to opening, never this roster. */
export type EcazRevivalCohort = {
cycle: number;
returned: string[];
};
type Context = {
leaders: readonly Pick<Leader, 'id' | 'dead' | 'deaths' | 'capturedBy' | 'gholaBy'>[];
revivalCycle: number;
ecazRevivalCohort?: EcazRevivalCohort;
};
type Quote = { cycle: number; returned: readonly string[] };
const invalid = () => { throw new Error('Ecaz revival cohort lost its original native discs.'); };
/** Provisional E3 composition: five actual Tanks open a native cohort; an
* unavailable five-disc roster retains the base captured-leader exception.
* Completed native returns, not death-counter equality, determine its remainder.
* Duke's own death counter never indexes or delays a native cohort. */
export function quoteEcazRevivalCohort(p: Context, dukeInTanks: boolean, waiveOpening = false): Quote {
const saved = p.ecazRevivalCohort;
if (p.leaders.length !== 5 || !Number.isSafeInteger(p.revivalCycle) || p.revivalCycle < 0)
invalid();
if (saved !== undefined) {
if (!saved || typeof saved !== 'object' || Array.isArray(saved) ||
!Number.isSafeInteger(saved.cycle) || saved.cycle < 0 || !Array.isArray(saved.returned))
invalid();
let keys = 0;
for (const key in saved) {
if (key !== 'cycle' && key !== 'returned') invalid();
keys++;
}
if (keys !== 2) invalid();
for (let i = 0; i < saved.returned.length; i++) {
if (!p.leaders.some(l => l.id === saved.returned[i]) ||
saved.returned.indexOf(saved.returned[i]) !== i) invalid();
}
}
let cycle = saved?.cycle ?? p.revivalCycle;
// Old valid Ecaz states have only the first cohort. Face-down/revived native
// histories seed its already-used identities once, not a shared death minimum.
let returned: readonly string[];
if (saved) returned = saved.returned;
else {
const seed: string[] = [];
for (const leader of p.leaders)
if (leader.deaths > 1 || (!leader.dead && leader.deaths > 0)) seed.push(leader.id);
returned = seed;
}
let dead = dukeInTanks ? 1 : 0, unavailable = true, pending = false;
for (const leader of p.leaders) {
const tanked = leader.dead && !leader.capturedBy && !leader.gholaBy;
if (tanked) {
dead++;
if (!returned.includes(leader.id)) pending = true;
}
if (!leader.dead && !leader.capturedBy && !leader.gholaBy) unavailable = false;
}
const opening = dead >= 5 || unavailable;
if (cycle === 0 && (opening || waiveOpening)) cycle = 1;
if (cycle > 0 && opening && !pending && returned.length > 0) {
cycle++;
returned = [];
}
return { cycle, returned };
}
/** Preserve an opening consumed by an independent Duke resurrection. */
export function recordEcazRevivalOpening(p: Context, dukeInTanks: boolean): void {
const quote = quoteEcazRevivalCohort(p, dukeInTanks);
if (p.ecazRevivalCohort?.cycle === quote.cycle &&
p.ecazRevivalCohort.returned === quote.returned) return;
if (!p.ecazRevivalCohort && quote.cycle === 0 && quote.returned.length === 0) return;
p.ecazRevivalCohort = { cycle: quote.cycle, returned: [...quote.returned] };
p.revivalCycle = Math.max(p.revivalCycle, quote.cycle);
}
/** Record only a completed native resurrection. Call before clearing dead. */
export function completeEcazRevivalCohortUse(p: Context, dukeInTanks: boolean, leader: string): void {
if (!p.leaders.some(l => l.id === leader && l.dead)) invalid();
const quote = quoteEcazRevivalCohort(p, dukeInTanks);
if (p.ecazRevivalCohort?.cycle === quote.cycle &&
quote.returned.includes(leader)) return;
p.ecazRevivalCohort = { cycle: quote.cycle,
returned: quote.returned.includes(leader) ? [...quote.returned] : [...quote.returned, leader] };
p.revivalCycle = Math.max(p.revivalCycle, quote.cycle);
}
/** A first-death capture execution is physically face down. It skips the first
* ordinary cohort; once that cohort is complete it has no extra lifetime ban. */
export function recordEcazFirstExecution(p: Context, dukeInTanks: boolean, leader: string): void {
const quote = quoteEcazRevivalCohort(p, dukeInTanks);
p.ecazRevivalCohort = { cycle: quote.cycle,
returned: quote.cycle <= 1 && !quote.returned.includes(leader)
? [...quote.returned, leader] : [...quote.returned] };
p.revivalCycle = Math.max(p.revivalCycle, quote.cycle);
}