Prototype conditional future battle Truthtrance
Verify and publish checkpoint container / container (push) Failing after 11m11s
Verify and publish checkpoint container / container (push) Failing after 11m11s
This commit is contained in:
1 parent
71009d73e3
commit
02695aea7c
15 files changed
+721
-8
No files matched your search
@@ -0,0 +1,127 @@
|
||||
/** A conditional commitment for the respondent's first later native battle.
|
||||
* Waiting does not require a battle to occur. The engine owns plan legality,
|
||||
* answer feasibility and the decision that an obligation became impossible. */
|
||||
export type FutureBattleLeaderReceipt = {
|
||||
turn: number;
|
||||
asker: string;
|
||||
player: string;
|
||||
leader: string;
|
||||
answer: boolean;
|
||||
battle?: string;
|
||||
status: 'waiting' | 'bound' | 'settled' | 'expired' | 'released';
|
||||
};
|
||||
|
||||
export type FutureBattleLeaderSource = {
|
||||
turn: number;
|
||||
event: string;
|
||||
attacker: string;
|
||||
defender: string;
|
||||
native: boolean;
|
||||
plansOpen: boolean;
|
||||
};
|
||||
|
||||
export class FutureBattleLeaderError extends Error {}
|
||||
|
||||
function check(condition: unknown, message: string): asserts condition {
|
||||
if (!condition) throw new FutureBattleLeaderError(message);
|
||||
}
|
||||
|
||||
function validId(value: unknown): value is string {
|
||||
return typeof value === 'string' && value.length <= 256 && value.trim() === value && value.length > 0;
|
||||
}
|
||||
|
||||
function validTurn(value: unknown): value is number {
|
||||
return Number.isSafeInteger(value) && (value as number) > 0;
|
||||
}
|
||||
|
||||
function validateReceipt(receipt: FutureBattleLeaderReceipt): void {
|
||||
check(!!receipt && typeof receipt === 'object' && !Array.isArray(receipt), 'Invalid future battle receipt.');
|
||||
check(validTurn(receipt.turn) && validId(receipt.asker) && validId(receipt.player) &&
|
||||
receipt.asker !== receipt.player && validId(receipt.leader) && typeof receipt.answer === 'boolean',
|
||||
'Invalid future battle question.');
|
||||
check(['waiting', 'bound', 'settled', 'expired', 'released'].includes(receipt.status),
|
||||
'Invalid future battle status.');
|
||||
check(receipt.battle === undefined || validId(receipt.battle), 'Invalid future battle identity.');
|
||||
check(receipt.status !== 'waiting' && receipt.status !== 'expired' || receipt.battle === undefined,
|
||||
'A waiting or expired question cannot name a battle.');
|
||||
check(receipt.status !== 'bound' && receipt.status !== 'settled' || receipt.battle !== undefined,
|
||||
'A bound or settled question must name its battle.');
|
||||
}
|
||||
|
||||
/** Caller supplies the respondent's public, native leader identities; no hand is inspected. */
|
||||
export function createFutureBattleLeader(
|
||||
question: Pick<FutureBattleLeaderReceipt, 'turn' | 'asker' | 'player' | 'leader' | 'answer'>,
|
||||
nativeLeaders: readonly string[],
|
||||
): FutureBattleLeaderReceipt {
|
||||
check(!!question && typeof question === 'object', 'Invalid future battle question.');
|
||||
const receipt: FutureBattleLeaderReceipt = { turn: question.turn, asker: question.asker,
|
||||
player: question.player, leader: question.leader, answer: question.answer, status: 'waiting' };
|
||||
validateReceipt(receipt);
|
||||
check(Array.isArray(nativeLeaders) && nativeLeaders.every(validId) &&
|
||||
new Set(nativeLeaders).size === nativeLeaders.length && nativeLeaders.includes(receipt.leader),
|
||||
'Choose one of the respondent’s native leader identities.');
|
||||
return receipt;
|
||||
}
|
||||
|
||||
/** Observe a real battle; unrelated battles and nonmatching turns leave it waiting. */
|
||||
export function bindFutureBattleLeader(
|
||||
receipt: FutureBattleLeaderReceipt,
|
||||
source: FutureBattleLeaderSource,
|
||||
): FutureBattleLeaderReceipt {
|
||||
validateReceipt(receipt);
|
||||
check(!!source && typeof source === 'object' && validTurn(source.turn) && validId(source.event) &&
|
||||
validId(source.attacker) && validId(source.defender) && source.attacker !== source.defender &&
|
||||
typeof source.native === 'boolean' && typeof source.plansOpen === 'boolean',
|
||||
'Invalid observed battle.');
|
||||
if (receipt.status !== 'waiting' || receipt.turn !== source.turn || !source.native ||
|
||||
(receipt.player !== source.attacker && receipt.player !== source.defender))
|
||||
return receipt;
|
||||
check(source.plansOpen, 'The respondent’s first qualifying battle is already past plan preparation.');
|
||||
return { ...receipt, battle: source.event, status: 'bound' };
|
||||
}
|
||||
|
||||
/** Only the actual revealed respondent plan can settle the linked receipt. */
|
||||
export function settleFutureBattleLeader(
|
||||
receipt: FutureBattleLeaderReceipt,
|
||||
observed: { turn: number; battle: string; player: string; revealed: boolean; leader: string | null },
|
||||
): FutureBattleLeaderReceipt {
|
||||
validateReceipt(receipt);
|
||||
check(!!observed && typeof observed === 'object' && validTurn(observed.turn) && validId(observed.battle) &&
|
||||
validId(observed.player) && typeof observed.revealed === 'boolean' &&
|
||||
(observed.leader === null || validId(observed.leader)), 'Invalid revealed battle plan.');
|
||||
check(receipt.turn === observed.turn && receipt.battle === observed.battle && receipt.player === observed.player,
|
||||
'The revealed plan does not belong to this question.');
|
||||
check((receipt.status === 'bound' || receipt.status === 'settled') && observed.revealed,
|
||||
'Only an actual revealed bound plan can settle this question.');
|
||||
check((observed.leader === receipt.leader) === receipt.answer,
|
||||
'The revealed plan contradicts the active future battle answer.');
|
||||
if (receipt.status === 'settled') return receipt;
|
||||
return { ...receipt, status: 'settled' };
|
||||
}
|
||||
|
||||
/** The caller must independently establish impossibility before invoking this. */
|
||||
export function releaseFutureBattleLeader(
|
||||
receipt: FutureBattleLeaderReceipt,
|
||||
source: { turn: number; battle?: string },
|
||||
): FutureBattleLeaderReceipt {
|
||||
validateReceipt(receipt);
|
||||
check(!!source && typeof source === 'object' && validTurn(source.turn) &&
|
||||
(source.battle === undefined || validId(source.battle)) && source.turn === receipt.turn &&
|
||||
source.battle === receipt.battle, 'The release does not belong to this question.');
|
||||
if (receipt.status === 'released') return receipt;
|
||||
check(receipt.status === 'waiting' || receipt.status === 'bound', 'This question cannot be released.');
|
||||
return { ...receipt, status: 'released' };
|
||||
}
|
||||
|
||||
/** Only unbound questions expire when their original Battle phase has ended. */
|
||||
export function expireFutureBattleLeader(
|
||||
receipt: FutureBattleLeaderReceipt,
|
||||
source: { turn: number; battlePhaseEnded: boolean },
|
||||
): FutureBattleLeaderReceipt {
|
||||
validateReceipt(receipt);
|
||||
check(!!source && typeof source === 'object' && validTurn(source.turn) &&
|
||||
typeof source.battlePhaseEnded === 'boolean' && source.turn === receipt.turn,
|
||||
'The phase end does not belong to this question.');
|
||||
if (!source.battlePhaseEnded || receipt.status !== 'waiting') return receipt;
|
||||
return { ...receipt, status: 'expired' };
|
||||
}
|
||||
Reference in new issue
Block a user