Fresh Richese-roster battles support clean aggressor changes after plans and revelation. Both combatants explicitly finish a public final opportunity regardless of held cards or result; an actual Sapho use clears both marks with a fresh event. Close priority before physical Stone allocation and other outcome settlement. Preserve plans, votes, participants, Habbanya priority and reserved physical cards. Keep old unstamped timing and discard signature bytes. Route all final vote exits through native Homeworld defense first. Add persistent controls and finite minimal bot readiness. Validation:16-file177/177, types/lint/build and eight original-action/rendered-table programmes. Actual cache1 and paid forces use40 Basic/47 Advanced source actions; genuine Stone1 turn1/Sapho1 turn2 uses86. Ordinary and mixed count2 ties change through sealed-plan/final-window Sapho, explicit revised acknowledgements and native losses. Sixteen minimal proposals are legal; controlled reservation and pure window/control cases are separate. Bounded review fixed the Harkonnen Nexus final-vote route; frozen feedback corrected four historical fixture assumptions. All checks then passed together. Final-window protocol is provisional. Direct late Semuta claim/capture, combined World/Nexus/Sapho, wider combinations, full modes, final assurance and browser/deployed acceptance remain open. No saved-room/database/reset/restart/deployment operation.
117 lines
4.6 KiB
TypeScript
117 lines
4.6 KiB
TypeScript
export type BattleOutcomeIdentity = {
|
|
event: string;
|
|
attacker: string;
|
|
defender: string;
|
|
turn: number;
|
|
};
|
|
export type BattleOutcomeWindow = {
|
|
version: 1;
|
|
event: string;
|
|
battle: string;
|
|
turn: number;
|
|
attacker: string;
|
|
defender: string;
|
|
revision: number;
|
|
ready: string[];
|
|
closed: boolean;
|
|
};
|
|
export class BattleOutcomeWindowError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = 'BattleOutcomeWindowError';
|
|
}
|
|
}
|
|
function requireWindow(value: unknown, message: string): asserts value {
|
|
if (!value) throw new BattleOutcomeWindowError(message);
|
|
}
|
|
const identifier = (value: unknown): value is string =>
|
|
typeof value === 'string' && value.trim().length > 0;
|
|
const count = (value: unknown): value is number =>
|
|
typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
const exact = (value: unknown, keys: string): value is Record<string, unknown> =>
|
|
!!value && typeof value === 'object' && !Array.isArray(value) &&
|
|
[Object.prototype, null].includes(Object.getPrototypeOf(value)) &&
|
|
Object.keys(value).sort().join(',') === keys;
|
|
function validateIdentity(identity: BattleOutcomeIdentity): void {
|
|
requireWindow(
|
|
exact(identity, 'attacker,defender,event,turn') && identifier(identity.event) &&
|
|
identifier(identity.attacker) && identifier(identity.defender) &&
|
|
identity.attacker !== identity.defender && count(identity.turn) && identity.turn > 0,
|
|
'The final battle window needs its original battle, turn and distinct combatants.',
|
|
);
|
|
}
|
|
const windowEvent = (identity: BattleOutcomeIdentity, revision: number): string =>
|
|
`battleOutcome:${JSON.stringify([identity.event, identity.turn, revision])}`;
|
|
|
|
/** Public identities and acknowledgements only; no outcome or held-card query. */
|
|
export function createBattleOutcomeWindow(
|
|
identity: BattleOutcomeIdentity,
|
|
revision: number,
|
|
): BattleOutcomeWindow {
|
|
validateIdentity(identity);
|
|
requireWindow(count(revision), 'The final battle window needs a safe nonnegative revision.');
|
|
return {
|
|
version: 1, event: windowEvent(identity, revision), battle: identity.event,
|
|
turn: identity.turn, attacker: identity.attacker, defender: identity.defender,
|
|
revision, ready: [], closed: false,
|
|
};
|
|
}
|
|
|
|
export function validateBattleOutcomeWindow(
|
|
state: BattleOutcomeWindow,
|
|
identity: BattleOutcomeIdentity,
|
|
revision: number,
|
|
): void {
|
|
validateIdentity(identity);
|
|
requireWindow(
|
|
count(revision) &&
|
|
exact(state, 'attacker,battle,closed,defender,event,ready,revision,turn,version') &&
|
|
state.version === 1 && state.battle === identity.event && state.turn === identity.turn &&
|
|
state.attacker === identity.attacker && state.defender === identity.defender &&
|
|
state.revision === revision && state.event === windowEvent(identity, revision),
|
|
'The final battle window has changed its original battle, combatants or revision.',
|
|
);
|
|
requireWindow(
|
|
Array.isArray(state.ready) &&
|
|
state.ready.every(player => player === identity.attacker || player === identity.defender) &&
|
|
new Set(state.ready).size === state.ready.length &&
|
|
typeof state.closed === 'boolean' && state.closed === (state.ready.length === 2),
|
|
'The final battle window needs distinct combatant acknowledgements and a matching closed status.',
|
|
);
|
|
}
|
|
|
|
export function readyBattleOutcomeWindow(
|
|
state: BattleOutcomeWindow,
|
|
identity: BattleOutcomeIdentity,
|
|
revision: number,
|
|
player: string,
|
|
event: string,
|
|
): BattleOutcomeWindow {
|
|
validateBattleOutcomeWindow(state, identity, revision);
|
|
requireWindow(!state.closed, 'The final battle window is already closed.');
|
|
requireWindow(event === state.event, 'This final battle window event has expired.');
|
|
requireWindow(
|
|
player === identity.attacker || player === identity.defender,
|
|
'Only a combatant can finish final battle actions.',
|
|
);
|
|
requireWindow(!state.ready.includes(player), 'This combatant already finished final battle actions.');
|
|
const ready = [...state.ready, player];
|
|
return { ...state, ready, closed: ready.length === 2 };
|
|
}
|
|
|
|
/** A public Sapho use creates a fresh shared opportunity before resolution. */
|
|
export function reviseBattleOutcomeWindow(
|
|
state: BattleOutcomeWindow,
|
|
identity: BattleOutcomeIdentity,
|
|
previousRevision: number,
|
|
nextRevision: number,
|
|
): BattleOutcomeWindow {
|
|
validateBattleOutcomeWindow(state, identity, previousRevision);
|
|
requireWindow(!state.closed, 'The final battle window is already closed.');
|
|
requireWindow(
|
|
count(nextRevision) && nextRevision === previousRevision + 1,
|
|
'A final battle window revision must advance exactly once.',
|
|
);
|
|
return createBattleOutcomeWindow(identity, nextRevision);
|
|
}
|