228 lines
12 KiB
TypeScript
228 lines
12 KiB
TypeScript
export { revivalPromiseModeSupported as revivalExpressionPromiseModeSupported } from './revival-promises';
|
|
|
|
export type RevivalExpressionLeaf =
|
|
| { kind: 'forces'; counter: 'total' | 'normal' | 'elite'; minimum: number }
|
|
| { kind: 'leader'; leader: string };
|
|
export type RevivalExpression = RevivalExpressionLeaf |
|
|
{ kind: 'and' | 'or'; terms: RevivalExpression[] };
|
|
export type RevivalExpressionClaim = { turn: number; claim: RevivalExpression };
|
|
export type RevivalExpressionProgress = {
|
|
total: number; normal: number; elite: number; leaders: string[];
|
|
};
|
|
export type RevivalExpressionPromise = RevivalExpressionClaim & {
|
|
event: string; player: string; asker: string; answer: boolean;
|
|
progress: RevivalExpressionProgress;
|
|
fulfilled?: boolean; released?: boolean;
|
|
};
|
|
export type RevivalExpressionReturn =
|
|
| { kind: 'forces'; amount: number; elite: number }
|
|
| { kind: 'leader'; leader: string };
|
|
export type RevivalExpressionBranchLeaf =
|
|
| { kind: 'forces'; counter: 'total' | 'normal' | 'elite'; minimum: number; progress: number; answer: boolean }
|
|
| { kind: 'leader'; leader: string; answer: boolean };
|
|
|
|
export const REVIVAL_EXPRESSION_MAX_DEPTH = 4;
|
|
export const REVIVAL_EXPRESSION_MAX_LEAVES = 16;
|
|
|
|
export class RevivalExpressionPromiseError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = 'RevivalExpressionPromiseError';
|
|
}
|
|
}
|
|
|
|
function check(value: unknown, message: string): asserts value {
|
|
if (!value) throw new RevivalExpressionPromiseError(message);
|
|
}
|
|
function object(value: unknown): value is Record<string, unknown> {
|
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
const identifier = (value: unknown): value is string =>
|
|
typeof value === 'string' && value.length > 0 && value === value.trim();
|
|
const optionalBoolean = (value: unknown) => value === undefined || typeof value === 'boolean';
|
|
const keys = (value: Record<string, unknown>, expected: string) => Object.keys(value).sort().join(',') === expected;
|
|
const count = (value: unknown): value is number =>
|
|
typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= 20;
|
|
|
|
/** Software input limits shared with shipment, not a printed question limit.
|
|
* Parse every subtree before any evaluation can short-circuit its answer. */
|
|
export function parseRevivalExpression(value: unknown): RevivalExpression {
|
|
let leaves = 0;
|
|
const parse = (input: unknown, depth: number): RevivalExpression => {
|
|
check(object(input) && depth <= REVIVAL_EXPRESSION_MAX_DEPTH,
|
|
'Use a revival expression within four nested groups.');
|
|
if (input.kind === 'and' || input.kind === 'or') {
|
|
check(keys(input, 'kind,terms') && Array.isArray(input.terms) && input.terms.length >= 2 &&
|
|
input.terms.length <= REVIVAL_EXPRESSION_MAX_LEAVES,
|
|
'Join two or more ordinary revival statements with AND or OR.');
|
|
return { kind: input.kind, terms: input.terms.map(term => parse(term, depth + 1)) };
|
|
}
|
|
check(++leaves <= REVIVAL_EXPRESSION_MAX_LEAVES,
|
|
'Use at most sixteen ordinary revival statements.');
|
|
if (input.kind === 'forces') {
|
|
check(keys(input, 'counter,kind,minimum') &&
|
|
(input.counter === 'total' || input.counter === 'normal' || input.counter === 'elite') &&
|
|
count(input.minimum) && input.minimum >= 1,
|
|
'Choose total, normal or elite physical forces and a minimum from 1 to 20.');
|
|
return { kind: 'forces', counter: input.counter, minimum: input.minimum };
|
|
}
|
|
check(input.kind === 'leader' && keys(input, 'kind,leader') && identifier(input.leader),
|
|
'Choose an exact ordinary leader identity.');
|
|
return { kind: 'leader', leader: input.leader };
|
|
};
|
|
return parse(value, 0);
|
|
}
|
|
|
|
export function parseRevivalExpressionClaim(value: unknown): RevivalExpressionClaim {
|
|
check(object(value) && keys(value, 'claim,turn') && typeof value.turn === 'number' &&
|
|
Number.isSafeInteger(value.turn) && value.turn >= 1,
|
|
'Choose the current turn and an ordinary revival expression.');
|
|
return { turn: value.turn, claim: parseRevivalExpression(value.claim) };
|
|
}
|
|
|
|
export function revivalExpressionText(expression: RevivalExpression,
|
|
leaderName: (id: string) => string = id => id === 'kwisatz' ? 'Kwisatz Haderach' : id): string {
|
|
const text = (node: RevivalExpression): string => {
|
|
if ('terms' in node)
|
|
return `(${node.terms.map(text).join(node.kind === 'and' ? ' AND ' : ' OR ')})`;
|
|
if (node.kind === 'leader') return `revive ${leaderName(node.leader)} through ordinary leader revival`;
|
|
const counters = node.counter === 'total' ? 'physical forces' : `${node.counter} forces`;
|
|
return `revive at least ${node.minimum} ${counters} through ordinary force revival`;
|
|
};
|
|
return text(parseRevivalExpression(expression));
|
|
}
|
|
|
|
function validateProgress(value: unknown): asserts value is RevivalExpressionProgress {
|
|
check(object(value) && keys(value, 'elite,leaders,normal,total') && count(value.total) &&
|
|
count(value.normal) && count(value.elite) && value.total === value.normal + value.elite &&
|
|
Array.isArray(value.leaders) && value.leaders.every(identifier) &&
|
|
new Set(value.leaders).size === value.leaders.length,
|
|
'Retain cumulative typed physical returns and unique returned leader identities.');
|
|
}
|
|
|
|
function evaluate(expression: RevivalExpression, progress: RevivalExpressionProgress): boolean {
|
|
if ('terms' in expression) return expression.kind === 'and' ?
|
|
expression.terms.every(term => evaluate(term, progress)) : expression.terms.some(term => evaluate(term, progress));
|
|
return expression.kind === 'leader' ? progress.leaders.includes(expression.leader) :
|
|
progress[expression.counter] >= expression.minimum;
|
|
}
|
|
|
|
export function evaluateRevivalExpression(expression: RevivalExpression, progress: RevivalExpressionProgress): boolean {
|
|
const parsed = parseRevivalExpression(expression);
|
|
validateProgress(progress);
|
|
return evaluate(parsed, progress);
|
|
}
|
|
|
|
export function liveRevivalExpressionPromises(promises: readonly RevivalExpressionPromise[], player: string,
|
|
turn: number): RevivalExpressionPromise[] {
|
|
return promises.filter(promise => promise.player === player && promise.turn === turn &&
|
|
!promise.fulfilled && !promise.released);
|
|
}
|
|
|
|
function validateReturn(event: RevivalExpressionReturn): void {
|
|
check(object(event) && (event.kind === 'forces' && keys(event, 'amount,elite,kind') && count(event.amount) &&
|
|
count(event.elite) && event.elite <= event.amount ||
|
|
event.kind === 'leader' && keys(event, 'kind,leader') && identifier(event.leader)),
|
|
'Retain the actual typed ordinary return event.');
|
|
}
|
|
|
|
/** Only completed ordinary native returns call this function. Each answer has
|
|
* its own future interval; past returns and special/card returns are not added. */
|
|
export function recordRevivalExpressionReturn(promises: readonly RevivalExpressionPromise[], player: string,
|
|
turn: number, event: RevivalExpressionReturn): RevivalExpressionPromise[] {
|
|
validateReturn(event);
|
|
const updates = new Map<RevivalExpressionPromise, RevivalExpressionPromise>();
|
|
for (const promise of liveRevivalExpressionPromises(promises, player, turn)) {
|
|
const claim = parseRevivalExpression(promise.claim);
|
|
validateProgress(promise.progress);
|
|
check(typeof promise.answer === 'boolean', 'Retain a definite revival expression answer.');
|
|
const old = promise.progress;
|
|
if (event.kind === 'forces' && event.amount === 0 || event.kind === 'leader' && old.leaders.includes(event.leader))
|
|
continue;
|
|
const progress: RevivalExpressionProgress = event.kind === 'forces' ? {
|
|
total: old.total + event.amount, normal: old.normal + event.amount - event.elite,
|
|
elite: old.elite + event.elite, leaders: [...old.leaders],
|
|
} : { ...old, leaders: [...old.leaders, event.leader] };
|
|
validateProgress(progress);
|
|
const satisfied = evaluate(claim, progress);
|
|
check(promise.answer || !satisfied,
|
|
'This ordinary return conflicts with your whole Truthtrance revival No answer.');
|
|
updates.set(promise, { ...promise, progress, ...(promise.answer && satisfied ? { fulfilled: true } : {}) });
|
|
}
|
|
return promises.map(promise => updates.get(promise) ?? promise);
|
|
}
|
|
|
|
/** Keep the strongest bound for each counter/polarity and reject contradictory
|
|
* same-counter or named-disc requirements before expanding the next subtree. */
|
|
function mergeBranches(left: RevivalExpressionBranchLeaf[], right: RevivalExpressionBranchLeaf[]):
|
|
RevivalExpressionBranchLeaf[] | null {
|
|
const merged = left.map(leaf => ({ ...leaf }));
|
|
for (const leaf of right) {
|
|
if (leaf.kind === 'leader') {
|
|
const prior = merged.find(other => other.kind === 'leader' && other.leader === leaf.leader);
|
|
if (prior && prior.answer !== leaf.answer) return null;
|
|
if (!prior) merged.push({ ...leaf });
|
|
continue;
|
|
}
|
|
const same = merged.find(other => other.kind === 'forces' && other.counter === leaf.counter && other.answer === leaf.answer);
|
|
if (same?.kind === 'forces') same.minimum = leaf.answer ? Math.max(same.minimum, leaf.minimum) : Math.min(same.minimum, leaf.minimum);
|
|
else merged.push({ ...leaf });
|
|
const positive = merged.find(other => other.kind === 'forces' && other.counter === leaf.counter && other.answer);
|
|
const negative = merged.find(other => other.kind === 'forces' && other.counter === leaf.counter && !other.answer);
|
|
if (positive?.kind === 'forces' && negative?.kind === 'forces' && positive.minimum >= negative.minimum) return null;
|
|
}
|
|
return merged;
|
|
}
|
|
|
|
/** Lazy alternatives retain original minima plus this answer's counter
|
|
* progress. Whole No uses De Morgan, never a No imposed on every AND child.
|
|
* Native allowance, prices and cross-counter feasibility remain quote-owned. */
|
|
export function* revivalExpressionBranches(promise: RevivalExpressionPromise): Generator<RevivalExpressionBranchLeaf[]> {
|
|
const expression = parseRevivalExpression(promise.claim);
|
|
validateProgress(promise.progress);
|
|
check(typeof promise.answer === 'boolean', 'Retain a definite revival expression answer.');
|
|
if (promise.fulfilled || promise.released) { yield []; return; }
|
|
function* alternatives(node: RevivalExpression, answer: boolean): Generator<RevivalExpressionBranchLeaf[]> {
|
|
if (node.kind === 'forces' || node.kind === 'leader') {
|
|
if (evaluate(node, promise.progress)) { if (answer) yield []; return; }
|
|
yield [node.kind === 'forces' ? { ...node, progress: promise.progress[node.counter], answer } : { ...node, answer }];
|
|
return;
|
|
}
|
|
const conjunction = node.kind === 'and' ? answer : !answer;
|
|
if (!conjunction) {
|
|
for (const term of node.terms) yield* alternatives(term, answer);
|
|
return;
|
|
}
|
|
const terms = node.terms;
|
|
function* combine(index: number, prefix: RevivalExpressionBranchLeaf[]): Generator<RevivalExpressionBranchLeaf[]> {
|
|
if (index === terms.length) { yield prefix; return; }
|
|
for (const suffix of alternatives(terms[index], answer)) {
|
|
const merged = mergeBranches(prefix, suffix);
|
|
if (merged) yield* combine(index + 1, merged);
|
|
}
|
|
}
|
|
yield* combine(0, []);
|
|
}
|
|
if (promise.answer && evaluate(expression, promise.progress)) { yield []; return; }
|
|
yield* alternatives(expression, promise.answer);
|
|
}
|
|
|
|
export function validateRevivalExpressionPromises(promises: readonly RevivalExpressionPromise[], players: readonly string[]): void {
|
|
check(Array.isArray(promises), 'The revival expression commitment record is invalid.');
|
|
const events = new Set<string>();
|
|
for (const promise of promises) {
|
|
check(object(promise), 'The revival expression commitment record is invalid.');
|
|
const { claim } = parseRevivalExpressionClaim({ turn: promise.turn, claim: promise.claim });
|
|
validateProgress(promise.progress);
|
|
const satisfied = evaluate(claim, promise.progress);
|
|
check(identifier(promise.event) && !events.has(promise.event) &&
|
|
identifier(promise.player) && identifier(promise.asker) && players.includes(promise.player) &&
|
|
players.includes(promise.asker) && promise.player !== promise.asker && typeof promise.answer === 'boolean' &&
|
|
optionalBoolean(promise.fulfilled) && optionalBoolean(promise.released) && !(promise.fulfilled && promise.released) &&
|
|
(promise.answer || !satisfied) &&
|
|
(!promise.answer || promise.released || !!promise.fulfilled === satisfied),
|
|
'The revival expression lost its event, participants or whole-answer progress.');
|
|
events.add(promise.event);
|
|
}
|
|
}
|