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.
112 lines
4.8 KiB
TypeScript
112 lines
4.8 KiB
TypeScript
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),
|
||
};
|
||
}
|