Files
Dune/game/fremen-revival-grant.ts
T
Quintine 0e3d6a250e Make Fremen allied revival grants cancelable
Bind the original Fremen/ally/turn grant before Karama allowance or prevention.
A prevented grant leaves intrinsic free rates and prior payments intact. Permit
late grants under the explicit provisional Recruits current-rate policy, with
shared controls, minimal bot support and native public chronology evidence.
Complete rules and browser/deployed acceptance remain open.
2026-10-24 18:16:03 +11:00

112 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Game } from './engine';
export type FremenRevivalGrantSource = {
turn: number;
owner: string;
recipient: string;
};
export type FremenRevivalGrantContext = Pick<
Game,
'status' | 'phase' | 'turn' | 'players' | 'freeRevival' | 'revivalRules' | 'recruits'
>;
export class FremenRevivalGrantError extends Error {
constructor(message: string) {
super(message);
this.name = 'FremenRevivalGrantError';
}
}
const id = (value: unknown): value is string =>
typeof value === 'string' && value.length > 0;
const count = (value: unknown): value is number =>
Number.isSafeInteger(value) && (value as number) >= 0;
/** Permission only. The engine owns the response and commits or denies this
* source once; normal revival quotes retain rates, prior usage and payment.
* No busy gate belongs here because settlement validates its own response. */
export function fremenRevivalGrantBlock(
g: FremenRevivalGrantContext,
owner: string,
): string | null {
if (g.status !== 'playing' || g.phase !== 4 || !count(g.turn) || g.turn < 1)
return 'Grant Fremen allied free revival during the current Revival phase.';
if (!id(owner) || !Array.isArray(g.players))
return 'The grant needs the current native Fremen owner.';
const owners = g.players.filter(player => player.id === owner);
const fremen = owners[0];
if (owners.length !== 1 || fremen.faction !== 'fremen' ||
g.players.filter(player => player.faction === 'fremen').length !== 1)
return 'The grant needs the current native Fremen owner.';
const recipients = g.players.filter(player => player.id === fremen.ally);
const recipient = recipients[0];
if (!id(fremen.ally) || recipients.length !== 1 ||
recipient.id === owner || recipient.ally !== owner)
return 'The grant needs the Fremen owner’s current mutual ally.';
if (!Array.isArray(g.freeRevival))
return 'The current Fremen revival permissions are invalid.';
if (g.freeRevival.includes(recipient.id))
return 'Fremen allied free revival has already been granted this phase.';
if (g.revivalRules?.fremenGrantBlocked !== undefined &&
(!Array.isArray(g.revivalRules.fremenGrantBlocked) ||
g.revivalRules.fremenGrantBlocked.some(target => !id(target))))
return 'The current Fremen grant prevention record is invalid.';
if (g.revivalRules?.fremenGrantBlocked?.includes(recipient.id))
return 'Karama prevented this Fremen allied grant for the current Revival phase.';
if (g.recruits?.turn === g.turn &&
(!count(recipient.revived) ||
(recipient.freeForcesRevived === undefined
? recipient.revived > 0
: !count(recipient.freeForcesRevived) ||
recipient.freeForcesRevived > recipient.revived)))
return 'The ally’s prior normal revival usage is inconsistent or unknown; this grant cannot safely change its rate.';
return null;
}
export function quoteFremenRevivalGrant(
g: FremenRevivalGrantContext,
owner: string,
): FremenRevivalGrantSource {
const blocked = fremenRevivalGrantBlock(g, owner);
if (blocked) throw new FremenRevivalGrantError(blocked);
const fremen = g.players.find(player => player.id === owner)!;
return { turn: g.turn, owner, recipient: fremen.ally! };
}
/** Bind the original relationship and turn before either response outcome.
* No source supplied by a client may retarget a pending grant. */
export function validateFremenRevivalGrant(
g: FremenRevivalGrantContext,
source: FremenRevivalGrantSource,
): void {
if (!source || typeof source !== 'object' || Array.isArray(source) ||
Object.keys(source).sort().join(',') !== 'owner,recipient,turn' ||
!count(source.turn) || source.turn !== g.turn ||
!id(source.owner) || !id(source.recipient))
throw new FremenRevivalGrantError('The Fremen allied revival grant has lost its original source.');
const current = quoteFremenRevivalGrant(g, source.owner);
if (current.recipient !== source.recipient)
throw new FremenRevivalGrantError('The Fremen allied revival grant no longer matches its original ally.');
}
/** Owner-only status. No wallet, Tanks, prior counts or quoted free amount is
* projected; the existing normal revival panel owns numerical allowances. */
export function projectFremenRevivalGrant(
g: FremenRevivalGrantContext,
viewer: string,
): { recipient: string; granted: boolean; blocked: string | null } | null {
if (!id(viewer) || !Array.isArray(g.players)) return null;
const owners = g.players.filter(player => player.id === viewer);
const owner = owners[0];
if (owners.length !== 1 || owner.faction !== 'fremen' ||
!id(owner.ally) || owner.ally === owner.id ||
g.players.filter(player => player.id === owner.ally).length !== 1)
return null;
return {
recipient: owner.ally,
granted: Array.isArray(g.freeRevival) && g.freeRevival.includes(owner.ally),
blocked: fremenRevivalGrantBlock(g, viewer),
};
}