Add final Nexus alliances to combined turn commitments
This commit is contained in:
1 parent
921b4fe1e3
commit
1a79fd3131
15 files changed
+1143
-78
No files matched your search
+171
-41
@@ -1,7 +1,7 @@
|
||||
import { TurnActionError, liveTurnActionPromises, turnActionBranches, recordTurnActionPayment,
|
||||
recordTurnActionShipment, recordTurnActionMovement, closeTurnActionTransport, finishTurnActionTurn,
|
||||
validateTurnActionPromises, recordTurnActionBid, closeTurnActionBidding, recordTurnActionRevival,
|
||||
closeTurnActionRevival, type TurnActionClaim, type TurnActionPromise, type TurnActionBranchLeaf, type TurnActionPreparation } from './turn-action-promises';
|
||||
closeTurnActionRevival, settleTurnActionAlliance, type TurnActionClaim, type TurnActionPromise, type TurnActionBranchLeaf, type TurnActionPreparation } from './turn-action-promises';
|
||||
import { TravelPromiseError, liveTravelPromises, travelBranches, recordTravelShipment, recordTravelMovement,
|
||||
finishTravelTurn, validateTravelPromises, type TravelClaim, type TravelPromise, type TravelBranchLeaf } from './travel-expression-promises';
|
||||
import { mixedTraitorRequirement, type MixedTraitorClaim } from './mixed-traitor-question';
|
||||
@@ -26,7 +26,7 @@ import { LeaderRevivalPromiseError, liveLeaderRevivalPromises, leaderRevivalRequ
|
||||
leaderRevivalPromisesSatisfied, recordPromisedLeaderRevival, validateLeaderRevivalPromises,
|
||||
type LeaderRevivalClaim, type LeaderRevivalPromise } from './leader-revival-promises';
|
||||
import { AlliancePromiseError, alliancePromiseModeSupported,
|
||||
alliancePromisesMatch, validateAlliancePromises, type AllianceClaim, type AllianceOpportunity, type AlliancePromise } from './alliance-promises';
|
||||
alliancePromisesMatch, findAlliancePromisePlan, validateAlliancePromises, type AllianceClaim, type AllianceOpportunity, type AlliancePromise } from './alliance-promises';
|
||||
import { BribePromiseError, bribePromiseModeSupported, liveBribePromises, recordPromisedBribe,
|
||||
validateBribePromises, type BribeClaim, type BribePromise } from './bribe-promises';
|
||||
import { ordinaryDiscardIncomeMatches } from './ordinary-discard-income';
|
||||
@@ -15036,6 +15036,16 @@ function normalNexusAllianceQuote(g: Game, p: Player, target: string | null) {
|
||||
requireRule(!!bribePromiseCompletion(future, actor),
|
||||
'Pay the necessary ordinary bribe before offering an alliance that would prevent your whole answer.');
|
||||
}
|
||||
const namedPayment = (node: TurnActionClaim['mixed']): boolean => node.kind === 'action'
|
||||
? node.action.kind === 'payment' && node.action.recipient === other.id
|
||||
: node.kind !== 'fact' && node.terms.some(namedPayment);
|
||||
if (liveTurnActionPromises(g.turnActionPromises ?? [], p.id, g.turn).some(promise => namedPayment(promise.mixed))) {
|
||||
const future = structuredClone(g), owner = getPlayer(future, p.id), partner = getPlayer(future, other.id);
|
||||
for (const seat of future.players) if (seat.ally === owner.id || seat.ally === partner.id) seat.ally = null;
|
||||
owner.ally = partner.id; partner.ally = owner.id;
|
||||
requireRule(!!turnActionCompletion(future, owner.id),
|
||||
'Pay the necessary ordinary bribe before offering an alliance that would prevent your whole turn answer.');
|
||||
}
|
||||
const blocked = homeworldAllianceReason(g, p.id, other.id);
|
||||
requireRule(!blocked, blocked ?? 'This alliance is unavailable.');
|
||||
}
|
||||
@@ -15231,6 +15241,10 @@ function guardAlliancePromiseReady(g: Game, p: Player) {
|
||||
requireRule(alliancePromisesMatch(g.alliancePromises ?? [], p.id, opportunity, p.ally) &&
|
||||
allianceExpressionPromisesMatch(g.allianceExpressionPromises ?? [], p.id, opportunity, p.ally),
|
||||
'Match your Truthtrance alliance answers before finishing this Nexus.');
|
||||
if (liveTurnActionPromises(g.turnActionPromises ?? [], p.id, g.turn)
|
||||
.some(promise => promise.alliance?.source === opportunity.source && !promise.alliance.closed))
|
||||
requireRule(turnActionAllianceCanClose(g, p),
|
||||
'Retain a whole Truthtrance completion when this Nexus fixes final membership.');
|
||||
}
|
||||
function settleAlliancePromises(g: Game) {
|
||||
const opportunity = allianceOpportunity(g);
|
||||
@@ -15239,6 +15253,7 @@ function settleAlliancePromises(g: Game) {
|
||||
for (const promise of orderedAllianceCommitments(g))
|
||||
if (promise.turn === opportunity.turn && promise.source === opportunity.source && !promise.fulfilled && !promise.released)
|
||||
promise.fulfilled = true;
|
||||
if (g.turnActionPromises) g.turnActionPromises = turnActionAllianceSnapshot(g, opportunity);
|
||||
}
|
||||
function projectedAllianceExpressionPromises(g: Game, player: string) {
|
||||
return (g.allianceExpressionPromises ?? []).filter(promise => promise.player === player && !isMixedAllianceEvent(g, promise.event))
|
||||
@@ -18708,10 +18723,86 @@ function initializeTurnActionOrder(g: Game) {
|
||||
if (!g.travelPromiseOrder!.some(entry => entry.kind === kind && entry.index === index))
|
||||
g.travelPromiseOrder!.push({ kind, index });
|
||||
}
|
||||
type TurnActionCompletion = MovementCompletion & { payments: Action[]; bid?: Action; revival?: RevivalPromisePlan };
|
||||
/** All admissible final native partners use the original reciprocal quote.
|
||||
* Exact outcome rows live only in this witness; no route is persisted. */
|
||||
type TurnActionAllianceRoute = { steps: { player: string; target: string | null }[]; uncertain: boolean };
|
||||
function turnActionAlliancePlans(g: Game, actor: string, includeUnknown = false): (TurnActionAllianceRoute | undefined)[] {
|
||||
const scalars = g.alliancePromises ?? [], groups = g.allianceExpressionPromises ?? [];
|
||||
const active = [...scalars, ...groups].some(promise => promise.player === actor &&
|
||||
promise.turn === g.turn && !promise.fulfilled && !promise.released);
|
||||
if (!active) return [undefined];
|
||||
const opportunity = allianceOpportunity(g);
|
||||
if (!opportunity) return [];
|
||||
if ([...scalars, ...groups].some(promise => promise.player === actor && promise.turn === g.turn &&
|
||||
!promise.fulfilled && !promise.released && promise.source !== opportunity.source)) return [];
|
||||
const outcomes = allianceExpressionOutcomes(scalars, groups, actor, opportunity,
|
||||
{ players: g.players, offers: g.allianceOffers });
|
||||
const plans: TurnActionAllianceRoute[] = [], seen = new Set<string>();
|
||||
for (const partner of outcomes) {
|
||||
const exact: AlliancePromise[] = (partner === null ? g.players.filter(player => player.id !== actor)
|
||||
.map(player => ({ partner: player.id, answer: false })) : [{ partner, answer: true }])
|
||||
.map((claim, index) => ({ ...claim, turn: opportunity.turn, source: opportunity.source,
|
||||
event: `turn-alliance-outcome:${index}`, player: actor, asker: g.players.find(player => player.id !== actor)!.id }));
|
||||
const plan = alliancePromiseRule(() => findAlliancePromisePlan([...scalars, ...exact], actor, opportunity,
|
||||
{ players: g.players, offers: g.allianceOffers }));
|
||||
if (plan !== null) {
|
||||
const route = { steps: plan.map(action => ({ player: actor, target: action.target })), uncertain: false };
|
||||
const key = JSON.stringify(route);
|
||||
if (!seen.has(key)) { seen.add(key); plans.push(route); }
|
||||
continue;
|
||||
}
|
||||
if (!includeUnknown || partner === null || getPlayer(g, actor).ally === partner ||
|
||||
getPlayer(g, partner).ally === null && g.allianceOffers[partner] === actor) continue;
|
||||
// This detached consent scenario is used only to decide Unknown, never
|
||||
// Yes or a guide. Native break/offer/accept quotes still validate every
|
||||
// step; original payments and all other resource checks run below.
|
||||
let players = g.players.map(player => ({ id: player.id, ally: player.ally })), offers = { ...g.allianceOffers };
|
||||
const steps: TurnActionAllianceRoute['steps'] = [];
|
||||
const add = (player: string, target: string | null) => {
|
||||
const quote = nexusRule(() => quoteNexusAlliance({ players, offers, actor: player, target }));
|
||||
players = players.map(seat => ({ ...seat, ally: quote.allies[seat.id] })); offers = quote.offers;
|
||||
steps.push({ player, target });
|
||||
};
|
||||
try {
|
||||
if (players.find(player => player.id === actor)!.ally !== null) add(actor, null);
|
||||
if (players.find(player => player.id === partner)!.ally !== null) add(partner, null);
|
||||
add(partner, actor);
|
||||
if (players.find(player => player.id === actor)!.ally !== partner) add(actor, partner);
|
||||
const route = { steps, uncertain: true }, key = JSON.stringify(route);
|
||||
if (!seen.has(key)) { seen.add(key); plans.push(route); }
|
||||
} catch (error) { if (!(error instanceof RuleError)) throw error; }
|
||||
}
|
||||
return plans;
|
||||
}
|
||||
function turnActionAllianceSnapshot(g: Game, opportunity: AllianceOpportunity): TurnActionPromise[] {
|
||||
return turnActionRule(() => settleTurnActionAlliance(g.turnActionPromises ?? [], opportunity.turn,
|
||||
opportunity.source, Object.fromEntries(g.players.map(player => [player.id, player.ally]))));
|
||||
}
|
||||
/** Own Ready must retain a completion after membership has become final;
|
||||
* partner changes are no longer available in that detached closing state. */
|
||||
function turnActionAllianceCanClose(g: Game, player: Player): boolean {
|
||||
const opportunity = allianceOpportunity(g);
|
||||
if (!opportunity) return false;
|
||||
if (!alliancePromisesMatch(g.alliancePromises ?? [], player.id, opportunity, player.ally) ||
|
||||
!allianceExpressionPromisesMatch(g.allianceExpressionPromises ?? [], player.id, opportunity, player.ally)) return false;
|
||||
try {
|
||||
const closed = turnActionAllianceSnapshot(g, opportunity);
|
||||
if ((g.turnActionPromises ?? []).some((promise, index) => promise.player === player.id &&
|
||||
promise.turn === g.turn && !promise.fulfilled && !promise.released && closed[index].released)) return false;
|
||||
const source = { ...g, turnActionPromises: closed,
|
||||
alliancePromises: (g.alliancePromises ?? []).map(promise => promise.player === player.id &&
|
||||
promise.turn === opportunity.turn && promise.source === opportunity.source && !promise.released
|
||||
? { ...promise, fulfilled: true } : promise),
|
||||
allianceExpressionPromises: (g.allianceExpressionPromises ?? []).map(promise => promise.player === player.id &&
|
||||
promise.turn === opportunity.turn && promise.source === opportunity.source && !promise.released
|
||||
? { ...promise, fulfilled: true } : promise) };
|
||||
return !!turnActionCompletion(source, player.id);
|
||||
} catch (error) { if (error instanceof RuleError) return false; throw error; }
|
||||
}
|
||||
type TurnActionCompletion = MovementCompletion & { payments: Action[]; bid?: Action; revival?: RevivalPromisePlan; alliance?: Action[] };
|
||||
/** Whole-answer alternatives stay private. Only detached native rows quote a
|
||||
* route, sharing actual interval progress and the remaining physical purse. */
|
||||
function turnActionCompletion(source: Game, actor: string): TurnActionCompletion | null {
|
||||
function turnActionCompletion(source: Game, actor: string, unknownConsent?: { value: boolean }): TurnActionCompletion | null {
|
||||
const records = [
|
||||
...liveTravelPromises(source.travelPromises ?? [], actor, source.turn).map(promise =>
|
||||
() => travelRule(() => travelBranches(promise))),
|
||||
@@ -18749,7 +18840,10 @@ function turnActionCompletion(source: Game, actor: string): TurnActionCompletion
|
||||
else if (leaf.kind === 'forces') (trial.revivalPromises ??= []).push({ ...metadata,
|
||||
counter: leaf.counter, minimum: leaf.minimum, progress: leaf.progress });
|
||||
else if (leaf.kind === 'leader') (trial.leaderRevivalPromises ??= []).push({ ...metadata, leader: leaf.leader });
|
||||
else if (leaf.kind === 'alliance') (trial.alliancePromises ??= []).push({ ...metadata,
|
||||
source: leaf.source, partner: leaf.partner });
|
||||
});
|
||||
const alliancePlans = turnActionAlliancePlans(trial, actor, !!unknownConsent);
|
||||
for (const required of bribePromiseRule(() => bribeCompletionCandidates(
|
||||
trial.bribePromises!, trial.bribeExpressionPromises ?? [], actor, source.turn))) {
|
||||
// A quoted payment follows only a currently owned modal continuation;
|
||||
@@ -18763,36 +18857,58 @@ function turnActionCompletion(source: Game, actor: string): TurnActionCompletion
|
||||
if (!Number.isSafeInteger(total) || total > 0 && total > uncommittedSpice(trial, owner) ||
|
||||
required.some(payment => payment.recipient === actor || payment.recipient === owner.ally ||
|
||||
!trial.players.some(player => player.id === payment.recipient))) continue;
|
||||
const funded = structuredClone(trial), payer = getPlayer(funded, actor);
|
||||
try {
|
||||
for (const payment of required) recordBribePayment(funded, actor, payment.recipient, payment.amount);
|
||||
payer.spice -= total;
|
||||
const voluntary = { actor, action: { type: 'bribe' } };
|
||||
for (const payment of required)
|
||||
checkBureaucratContributors(funded, payment.recipient, [{ payer: actor, amount: payment.amount }]);
|
||||
reconcileBattlePromises(funded, voluntary);
|
||||
reconcileBidPromises(funded, voluntary);
|
||||
reconcileRevivalPromises(funded, voluntary);
|
||||
const lot = bidOpportunity(funded);
|
||||
const needsBid = !!lot && bidRequired(funded.bidPromises ?? [], actor, lot);
|
||||
const bid = needsBid ? bidPromiseCompletion(funded, payer) : undefined;
|
||||
if (needsBid && !bid) continue;
|
||||
const needsRevival = [
|
||||
...liveRevivalPromises(funded.revivalPromises ?? [], actor, source.turn),
|
||||
...liveLeaderRevivalPromises(funded.leaderRevivalPromises ?? [], actor, source.turn),
|
||||
...liveRevivalExpressionPromises(funded.revivalExpressionPromises ?? [], actor, source.turn),
|
||||
].some(promise => promise.answer);
|
||||
const revival = needsRevival ? revivalPromisePlan(funded, payer) : undefined;
|
||||
if (needsRevival && !revival) continue;
|
||||
const transport = [
|
||||
...liveShipmentPromises(funded.shipmentPromises ?? [], actor, source.turn),
|
||||
...liveMovementPromises(funded.movementPromises ?? [], actor, source.turn),
|
||||
...liveMovementExpressionPromises(funded.movementExpressionPromises ?? [], actor, source.turn),
|
||||
];
|
||||
const completion = transport.length ? groundMovementCompletionNative(funded, actor) : { actions: [] };
|
||||
if (completion) return { ...completion, ...(bid ? { bid } : {}), ...(revival ? { revival } : {}),
|
||||
payments: required.map(payment => ({ type: 'bribe', target: payment.recipient, amount: payment.amount })) };
|
||||
} catch (error) { if (!(error instanceof RuleError)) throw error; }
|
||||
for (const alliancePlan of alliancePlans) {
|
||||
const funded = structuredClone(trial), payer = getPlayer(funded, actor);
|
||||
try {
|
||||
for (const payment of required) recordBribePayment(funded, actor, payment.recipient, payment.amount);
|
||||
payer.spice -= total;
|
||||
for (const action of alliancePlan?.steps ?? []) {
|
||||
const quote = normalNexusAllianceQuote(funded, getPlayer(funded, action.player), action.target);
|
||||
for (const player of funded.players) player.ally = quote.allies[player.id];
|
||||
funded.allianceOffers = quote.offers;
|
||||
if (quote.formed) for (const id of quote.formed) getPlayer(funded, id).allySinceTurn = funded.turn;
|
||||
funded.ready = [];
|
||||
}
|
||||
if (alliancePlan) {
|
||||
const opportunity = allianceOpportunity(funded)!;
|
||||
funded.alliancePromises = (funded.alliancePromises ?? []).map(promise => promise.player === actor &&
|
||||
promise.turn === opportunity.turn && promise.source === opportunity.source && !promise.released
|
||||
? { ...promise, fulfilled: true } : promise);
|
||||
funded.allianceExpressionPromises = (funded.allianceExpressionPromises ?? []).map(promise => promise.player === actor &&
|
||||
promise.turn === opportunity.turn && promise.source === opportunity.source && !promise.released
|
||||
? { ...promise, fulfilled: true } : promise);
|
||||
}
|
||||
const voluntary = { actor, action: { type: 'bribe' } };
|
||||
for (const payment of required)
|
||||
checkBureaucratContributors(funded, payment.recipient, [{ payer: actor, amount: payment.amount }]);
|
||||
reconcileBattlePromises(funded, voluntary);
|
||||
reconcileBidPromises(funded, voluntary);
|
||||
reconcileRevivalPromises(funded, voluntary);
|
||||
const lot = bidOpportunity(funded);
|
||||
const needsBid = !!lot && bidRequired(funded.bidPromises ?? [], actor, lot);
|
||||
const bid = needsBid ? bidPromiseCompletion(funded, payer) : undefined;
|
||||
if (needsBid && !bid) continue;
|
||||
const needsRevival = [
|
||||
...liveRevivalPromises(funded.revivalPromises ?? [], actor, source.turn),
|
||||
...liveLeaderRevivalPromises(funded.leaderRevivalPromises ?? [], actor, source.turn),
|
||||
...liveRevivalExpressionPromises(funded.revivalExpressionPromises ?? [], actor, source.turn),
|
||||
].some(promise => promise.answer);
|
||||
const revival = needsRevival ? revivalPromisePlan(funded, payer) : undefined;
|
||||
if (needsRevival && !revival) continue;
|
||||
const transport = [
|
||||
...liveShipmentPromises(funded.shipmentPromises ?? [], actor, source.turn),
|
||||
...liveMovementPromises(funded.movementPromises ?? [], actor, source.turn),
|
||||
...liveMovementExpressionPromises(funded.movementExpressionPromises ?? [], actor, source.turn),
|
||||
];
|
||||
const completion = transport.length ? groundMovementCompletionNative(funded, actor) : { actions: [] };
|
||||
if (completion) {
|
||||
if (alliancePlan?.uncertain) { unknownConsent!.value = true; continue; }
|
||||
return { ...completion, ...(bid ? { bid } : {}), ...(revival ? { revival } : {}),
|
||||
...(alliancePlan ? { alliance: alliancePlan.steps.map(step => ({ type: 'alliance', target: step.target })) } : {}),
|
||||
payments: required.map(payment => ({ type: 'bribe', target: payment.recipient, amount: payment.amount })) };
|
||||
}
|
||||
} catch (error) { if (!(error instanceof RuleError)) throw error; }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -18803,16 +18919,19 @@ function turnActionPromise(g: Game, p: Player, claim: TurnActionClaim, answer: b
|
||||
const compiled = compileMixedActionExpression(claim.mixed,
|
||||
fact => truthFactAnswer(p, fact, truthKnowledgeOf(g, p)), answer);
|
||||
const open = turnActionTransportOpen(g, p.id);
|
||||
let bid = false, revival = false;
|
||||
let bid = false, revival = false, alliance = false;
|
||||
const visit = (node: TurnActionClaim['mixed']): void => {
|
||||
if (node.kind === 'action') {
|
||||
if (node.action.kind === 'bid') bid = true;
|
||||
if (node.action.kind === 'alliance') alliance = true;
|
||||
if (node.action.kind === 'forces' || node.action.kind === 'leader') revival = true;
|
||||
} else if (node.kind !== 'fact') node.terms.forEach(visit);
|
||||
};
|
||||
visit(claim.mixed);
|
||||
const lot = bid ? bidOpportunity(g) : null;
|
||||
requireRule(!bid || !!lot, 'Retain the original current ordinary auction lot.');
|
||||
const nexus = alliance ? allianceOpportunity(g) : null;
|
||||
requireRule(!alliance || !!nexus, 'Retain the original current native Nexus.');
|
||||
const preparation: TurnActionPreparation | undefined = bid || revival ? {
|
||||
bid: lot ? { lot: lot.lot, source: lot.source, maximum: 0, closed: false } : null,
|
||||
revival: revival ? { total: 0, normal: 0, elite: 0, leaders: [], closed: g.phase > 4 ||
|
||||
@@ -18820,32 +18939,34 @@ function turnActionPromise(g: Game, p: Player, claim: TurnActionClaim, answer: b
|
||||
} : undefined;
|
||||
return { turn: claim.turn, mixed: structuredClone(claim.mixed), event, player: p.id, asker, answer, compiled,
|
||||
...(preparation ? { preparation } : {}),
|
||||
...(nexus ? { alliance: { source: nexus.source, partner: null, closed: false } } : {}),
|
||||
shipment: { pending: open && (g.phase < 5 || shipmentAvailable(g, p)), event: null },
|
||||
progress: [], payments: [], transportClosed: !open, paymentClosed: false,
|
||||
fulfilled: compiled.kind === 'constant' && compiled.value === answer, released: false };
|
||||
}
|
||||
function turnActionAnswerSource(g: Game, player: string): Game {
|
||||
return g.phase === 2 || g.phase === 4 && !g.revivalOrder || g.phase === 8 ||
|
||||
function turnActionAnswerSource(g: Game, player: string, alliance = false): Game {
|
||||
return g.phase === 1 && alliance || g.phase === 2 || g.phase === 4 && !g.revivalOrder || g.phase === 8 ||
|
||||
g.phase === 7 && bribesCloseBeforeNextMentat(g)
|
||||
? { ...g, ready: g.ready.filter(id => id !== player) } : g;
|
||||
}
|
||||
function turnActionTruthAnswers(g: Game, p: Player, claim: TurnActionClaim): TruthAnswer[] {
|
||||
if (claim.turn !== g.turn || !turnActionTargetAvailable(g, p.id)) return [];
|
||||
const answers: TruthAnswer[] = [];
|
||||
const answers: TruthAnswer[] = [], consent = { value: false };
|
||||
for (const answer of [true, false]) {
|
||||
const promise = turnActionPromise(g, p, claim, answer, 'candidate', g.players.find(other => other.id !== p.id)!.id);
|
||||
if (promise.compiled.kind === 'constant' && promise.compiled.value !== answer) continue;
|
||||
const trial = { ...turnActionAnswerSource(g, p.id),
|
||||
const trial = { ...turnActionAnswerSource(g, p.id, !!promise.alliance),
|
||||
turnActionPromises: [...(g.turnActionPromises ?? []), promise], travelPromiseOrder: undefined };
|
||||
if (turnActionCompletion(trial, p.id)) answers.push(answer ? 'yes' : 'no');
|
||||
if (turnActionCompletion(trial, p.id, answer && promise.alliance ? consent : undefined)) answers.push(answer ? 'yes' : 'no');
|
||||
}
|
||||
if (!answers.includes('yes') && consent.value) answers.push('unknown');
|
||||
return answers.length ? answers : ['unknown'];
|
||||
}
|
||||
function bindTurnActionTruth(g: Game, p: Player, claim: TurnActionClaim, answer: boolean, asker: string) {
|
||||
requireRule(turnActionTruthAnswers(g, p, claim).includes(answer ? 'yes' : 'no'),
|
||||
'Choose a whole turn-action answer compatible with current funding and native actions.');
|
||||
const promise = turnActionPromise(g, p, claim, answer, crypto.randomUUID(), asker);
|
||||
g.ready = turnActionAnswerSource(g, p.id).ready;
|
||||
g.ready = turnActionAnswerSource(g, p.id, !!promise.alliance).ready;
|
||||
g.turnActionPromises ??= [];
|
||||
initializeTurnActionOrder(g);
|
||||
g.turnActionPromises.push(promise);
|
||||
@@ -18881,6 +19002,10 @@ function reconcileTurnActionPromises(g: Game, actor?: string, action?: Action, s
|
||||
if (promise.answer) promise.released = true; else promise.fulfilled = true;
|
||||
}
|
||||
if (!bribePromiseTurn(g) || g.truthtrance) return;
|
||||
const nexus = allianceOpportunity(g);
|
||||
if (nexus && action?.type === 'bribe' && g.turnActionPromises!.some(promise =>
|
||||
promise.player === actor && promise.turn === g.turn && promise.alliance?.source === nexus.source))
|
||||
g.ready = g.ready.filter(player => player !== actor);
|
||||
initializeTurnActionOrder(g);
|
||||
for (const p of g.players) {
|
||||
const early = g.phase < 5 || sourcePhase < 5;
|
||||
@@ -18912,6 +19037,9 @@ function reconcileTurnActionPromises(g: Game, actor?: string, action?: Action, s
|
||||
promise.released = true;
|
||||
log(g, `${p.name} can no longer fulfill an accepted Truthtrance commitment; that answer is no longer binding.`);
|
||||
}
|
||||
if (nexus && g.ready.includes(p.id) && liveTurnActionPromises(g.turnActionPromises!, p.id, g.turn)
|
||||
.some(promise => promise.alliance?.source === nexus.source && !promise.alliance.closed) &&
|
||||
!turnActionAllianceCanClose(g, p)) g.ready = g.ready.filter(player => player !== p.id);
|
||||
}
|
||||
}
|
||||
function projectedTurnActionPromises(g: Game, player: string) {
|
||||
@@ -18955,6 +19083,8 @@ function projectedTurnActionGuide(g: Game, player: Player): { event: string; act
|
||||
} else if (!action && g.phase === 5 && g.active === player.id && !promise.transportClosed)
|
||||
action = completion.actions[0] ?? { type: 'endMovement' };
|
||||
}
|
||||
if (!action && g.phase === 1 && allianceQuestionWindow(g) && !g.ready.includes(player.id))
|
||||
action = completion.alliance?.[0] ?? { type: 'ready' };
|
||||
if (!action && [7, 8].includes(g.phase) && !g.ready.includes(player.id)) action = { type: 'ready' };
|
||||
if (!action) return null;
|
||||
const packet = { ...structuredClone(action) };
|
||||
|
||||
+10
-7
@@ -3187,11 +3187,14 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
},
|
||||
{
|
||||
id: 'truthtrance-turn-actions',
|
||||
title: 'Truthtrance: combined bids, Revival, payments and transport',
|
||||
title: 'Truthtrance: combined alliances, bids, Revival, payments and transport',
|
||||
category: 'Cards', coverage: 'Partial', developmentStage: 'Prototyped',
|
||||
summary: 'Combine ordinary current-lot bids, native Revival, future payments and physical transport in one whole current-turn answer, with optional frozen facts.',
|
||||
summary: 'Combine final current-Nexus membership, ordinary current-lot bids, native Revival, future payments and physical transport in one whole current-turn answer, with optional frozen facts.',
|
||||
steps: [
|
||||
'Choose Combine this turn’s bids, Revival, payments and transport. Full AND/OR groups share sixteen logical leaves and four grouping levels, with optional current facts. This is a provisional composition of the native current-lot bid, ordinary Revival, funded-payment and owned-stage travel prototypes; wider action families and optional combinations remain unfinished.',
|
||||
'Choose Combine this turn’s alliances, bids, Revival, payments and transport. Full AND/OR groups share sixteen logical leaves and four grouping levels, with optional current facts. This is a provisional composition of the native current-lot bid, ordinary Revival, funded-payment and owned-stage travel prototypes; wider action families and optional combinations remain unfinished.',
|
||||
'A final-alliance clause names another current participant during this original quiet native Nexus. Existing invitations and reciprocal formation remain authoritative. Offers, temporary membership and your own Ready do not settle the clause; final actual membership is fixed only when the Nexus closes.',
|
||||
'Whole No can permit a temporary paid pair and later require a break. Own Ready must preserve a completion after final membership is fixed; remaining payment or physical-action alternatives keep their later deadlines. Definite answers, including constants, uniformly reopen own Nexus readiness; later ordinary payments require confirmation again.',
|
||||
'Missing foreign consent can offer I don’t know and save or change the question; it never invents a definite Yes. Unknown requires an otherwise funded native completion, so a known unaffordable branch stays No. Foreign withdrawal can release impossibility without vetoing the other player’s rights.',
|
||||
'Only actual ordinary bribes paid by the respondent after answering add payment progress. Payments to the same recipient accumulate. Shipment fees, gifts and later receipt of incoming bribes do not count. Native recipient restrictions, costs and receipt timing remain.',
|
||||
'An ordinary-bid clause names the current public lot and counts the maximum new accepted declaration after answering. Earlier bids, summed bids and later purchase invoices do not count. A pass can reset after another bid, so No stays binding through reentry until that original physical lot closes.',
|
||||
'Ordinary force Revival clauses accumulate future total, normal or elite physical returns. Personal native leader clauses include eligible Advanced Atreides Kwisatz. Prior returns, Ghola, foreign leaders and special returns do not count. Original costs, slots, cycles and actual return controls remain.',
|
||||
@@ -3201,13 +3204,13 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Transport closes at your actual Movement end. A payment alternative may remain after that close, through the full current turn including Mentat. Complete a required payment before finishing the turn. No payment caps remain until their native opportunity closes.',
|
||||
'Review the persistent own history and current source-bound native guide, or choose another legal manual route. Every definite answer uniformly reopens applicable concurrent preparation or final payment readiness. Four minimal AI profiles participate; complete strategy and final rules/deployed acceptance remain unfinished.',
|
||||
],
|
||||
related: ['card-truthtrance', 'truthtrance-travel', 'truthtrance-bid', 'truthtrance-revival', 'truthtrance-bribe-expression', 'implementation-checklist'],
|
||||
related: ['card-truthtrance', 'truthtrance-alliance', 'truthtrance-travel', 'truthtrance-bid', 'truthtrance-revival', 'truthtrance-bribe-expression', 'implementation-checklist'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Whole signed alternatives reuse native bid, Revival, payment and transport constraints with separate actual intervals and deadlines.', evidence: ['game/turn-action-promises.ts', 'game/mixed-action-question.ts', 'game/engine.ts', 'game/truthtrance.ts'] },
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Whole signed alternatives reuse native final-alliance, bid, Revival, payment and transport constraints with separate actual intervals and deadlines.', evidence: ['game/turn-action-promises.ts', 'game/mixed-action-question.ts', 'game/engine.ts', 'game/truthtrance.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Full editor, raw question, private answer, persistent own history and exact current-phase native guide.', evidence: ['components/turn-action-fields.tsx', 'components/turn-action-promises.tsx', 'components/truthtrance.tsx', 'components/game-table.tsx', 'tests/truth-turn-action-controls.test.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'Four minimal profiles answer and consume native guides; full strategy and calibration remain deferred.', evidence: ['game/bots.ts', 'tests/truth-turn-action-runtime.test.ts', 'tests/truth-turn-preparation-runtime.test.ts'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'Four minimal profiles answer and consume native guides; full strategy and calibration remain deferred.', evidence: ['game/bots.ts', 'tests/truth-turn-action-runtime.test.ts', 'tests/truth-turn-preparation-runtime.test.ts', 'tests/fixture-truth-turn-alliance.ts', 'tests/truth-turn-alliance-runtime.test.ts'] },
|
||||
{ area: 'Documentation', status: 'Implemented', detail: 'Current funding, physical event meaning, private alternatives, separate deadlines and provisional scope are explicit.', evidence: ['docs/TRUTHTRANCE_TURN_ACTIONS.md', 'docs/RULE_DECISIONS.md'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Focused helper, control and original native sources; broader composition and final acceptance remain.', evidence: ['tests/turn-action-promises.test.ts', 'tests/truth-turn-action-controls.test.tsx', 'tests/fixture-truth-turn-action.ts', 'tests/truth-turn-action-runtime.test.ts', 'tests/truth-turn-action-funding.test.ts', 'tests/fixture-truth-turn-preparation.ts', 'tests/truth-turn-preparation-runtime.test.ts'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Focused helper, control and original native sources; broader composition and final acceptance remain.', evidence: ['tests/turn-action-promises.test.ts', 'tests/truth-turn-action-controls.test.tsx', 'tests/fixture-truth-turn-action.ts', 'tests/truth-turn-action-runtime.test.ts', 'tests/truth-turn-action-funding.test.ts', 'tests/fixture-truth-turn-preparation.ts', 'tests/truth-turn-preparation-runtime.test.ts', 'tests/fixture-truth-turn-alliance.ts', 'tests/truth-turn-alliance-runtime.test.ts'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -392,6 +392,10 @@ function parseQuestion(g: Game, asker: string, value: unknown): TruthQuestion {
|
||||
if (action.kind === 'leader') check(leaders(respondent.faction).some(leader => leader.id === action.leader) ||
|
||||
action.leader === 'kwisatz' && g.advanced && respondent.faction === 'atreides',
|
||||
'Choose a personal native faction leader, or the Advanced Atreides Kwisatz Haderach.');
|
||||
if (action.kind === 'alliance') check(alliancePromiseModeSupported(g) && g.phase === 1 && g.nexus &&
|
||||
!g.spiceWindow && !g.spiceResolution && action.partner !== respondent.id &&
|
||||
g.players.some(player => player.id === action.partner),
|
||||
'Choose another seated final alliance partner in the current quiet native Nexus.');
|
||||
return action;
|
||||
}, { requireFact: false, maxDepth: TURN_ACTION_MAX_DEPTH, maxLeaves: TURN_ACTION_MAX_LEAVES });
|
||||
return { kind: 'turnActions', target: v.target as string, turn: g.turn, mixed };
|
||||
@@ -709,6 +713,7 @@ export function truthQuestionText(
|
||||
action => {
|
||||
if (action.kind === 'payment') return `you ${bribeClaimText({ turn: q.turn, ...action }, playerName)}`;
|
||||
if (action.kind === 'bid') return `you ${bidClaimText({ turn: q.turn, ...action })}`;
|
||||
if (action.kind === 'alliance') return `you will ${allianceClaimText({ turn: q.turn, partner: action.partner }, playerName)} (final membership; temporary offers or pairs do not settle this clause)`;
|
||||
if (action.kind === 'forces' || action.kind === 'leader') return `you ${revivalExpressionText(action,
|
||||
id => id === 'kwisatz' ? 'Kwisatz Haderach' : leaderName(id))} from now until your ordinary Revival opportunity ends this turn`;
|
||||
return `you ${action.kind === 'ship' ? shipmentClaimText(action) : movementClaimText(action)} before your Shipment and Movement turn ends`;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TERRITORIES } from './board';
|
||||
import { BribePromiseError, parseBribeClaim } from './bribe-promises';
|
||||
import { BidPromiseError, parseBidClaim } from './bid-promises';
|
||||
import { AlliancePromiseError, parseAllianceClaim } from './alliance-promises';
|
||||
import { parseRevivalActionClaim, type RevivalActionClaim } from './mixed-revival-question';
|
||||
import { RevivalExpressionPromiseError, type RevivalExpressionReturn } from './revival-expression-promises';
|
||||
import {
|
||||
@@ -22,19 +23,21 @@ export const TURN_ACTION_MAX_LEAVES = 16;
|
||||
export const TURN_ACTION_MAX_DEPTH = 4;
|
||||
|
||||
export type TurnActionLeaf = TravelActionClaim | { kind: 'payment'; recipient: string; minimum: number } |
|
||||
{ kind: 'bid'; lot: number; minimum: number } | RevivalActionClaim;
|
||||
{ kind: 'bid'; lot: number; minimum: number } | RevivalActionClaim | { kind: 'alliance'; partner: string };
|
||||
export type TurnActionClaim = { turn: number; mixed: MixedActionExpression<TurnActionLeaf> };
|
||||
export type TurnActionPayment = { recipient: string; amount: number };
|
||||
export type TurnActionBranchLeaf = TravelBranchLeaf |
|
||||
{ kind: 'payment'; recipient: string; minimum: number; progress: number; answer: boolean } |
|
||||
{ kind: 'bid'; lot: number; source: string; minimum: number; answer: boolean } |
|
||||
{ kind: 'forces'; counter: 'total' | 'normal' | 'elite'; minimum: number; progress: number; answer: boolean } |
|
||||
{ kind: 'leader'; leader: string; answer: boolean };
|
||||
{ kind: 'leader'; leader: string; answer: boolean } |
|
||||
{ kind: 'alliance'; partner: string; source: string; answer: boolean };
|
||||
export type TurnActionPreparation = {
|
||||
bid: { lot: number; source: string; maximum: number; closed: boolean } | null;
|
||||
revival: { total: number; normal: number; elite: number; leaders: string[]; closed: boolean } | null;
|
||||
};
|
||||
export type TurnActionBid = { lot: number; source: string; amount: number };
|
||||
export type TurnActionAlliance = { source: string; partner: string | null; closed: boolean };
|
||||
export type TurnActionPromise = TurnActionClaim & {
|
||||
event: string;
|
||||
player: string;
|
||||
@@ -53,6 +56,8 @@ export type TurnActionPromise = TurnActionClaim & {
|
||||
released: boolean;
|
||||
/** Optional only for original shipment/movement/payment records. */
|
||||
preparation?: TurnActionPreparation;
|
||||
/** Final membership is recorded only by the original Nexus closing event. */
|
||||
alliance?: TurnActionAlliance;
|
||||
};
|
||||
|
||||
export class TurnActionError extends Error {
|
||||
@@ -81,8 +86,13 @@ const printedTerritory = (value: unknown): value is string =>
|
||||
|
||||
/** Public leaves carry only their native destination/recipient and minimum. */
|
||||
export function parseTurnActionLeaf(value: unknown): TurnActionLeaf {
|
||||
check(object(value), 'Choose one future ordinary bid, Revival, reserve shipment, ground movement or bribe payment.');
|
||||
check(object(value), 'Choose one future ordinary bid, Revival, final Nexus alliance, reserve shipment, ground movement or bribe payment.');
|
||||
try {
|
||||
if (value.kind === 'alliance') {
|
||||
check(keys(value, 'kind,partner'), 'Supply only one final Nexus alliance partner.');
|
||||
const claim = parseAllianceClaim({ turn: 1, partner: value.partner });
|
||||
return { kind: 'alliance', partner: claim.partner };
|
||||
}
|
||||
if (value.kind === 'bid') {
|
||||
check(keys(value, 'kind,lot,minimum'), 'Supply only the current ordinary lot and positive bid minimum.');
|
||||
const claim = parseBidClaim({ turn: 1, lot: value.lot, minimum: value.minimum });
|
||||
@@ -95,7 +105,7 @@ export function parseTurnActionLeaf(value: unknown): TurnActionLeaf {
|
||||
return { kind: 'payment', recipient: claim.recipient, minimum: claim.minimum };
|
||||
} catch (error) {
|
||||
if (error instanceof TravelPromiseError || error instanceof BribePromiseError || error instanceof BidPromiseError ||
|
||||
error instanceof RevivalExpressionPromiseError)
|
||||
error instanceof RevivalExpressionPromiseError || error instanceof AlliancePromiseError)
|
||||
throw new TurnActionError(error.message);
|
||||
throw error;
|
||||
}
|
||||
@@ -116,6 +126,7 @@ function storedFact(value: unknown, depth = 0): TruthFact {
|
||||
}
|
||||
|
||||
function actionKey(action: TurnActionLeaf): string {
|
||||
if (action.kind === 'alliance') return JSON.stringify([action.kind, action.partner]);
|
||||
if (action.kind === 'leader') return JSON.stringify([action.kind, action.leader]);
|
||||
if (action.kind === 'forces') return JSON.stringify([action.kind, action.counter, action.minimum]);
|
||||
if (action.kind === 'bid') return JSON.stringify([action.kind, action.lot, action.minimum]);
|
||||
@@ -155,9 +166,11 @@ function validateEvent(event: unknown): asserts event is TravelEvent {
|
||||
}
|
||||
|
||||
function readPromise(promise: TurnActionPromise): CompiledMixedAction<TurnActionLeaf> {
|
||||
const fields = 'answer,asker,compiled,event,fulfilled,mixed,paymentClosed,payments,player,progress,released,shipment,transportClosed,turn';
|
||||
check(object(promise) && (keys(promise, fields) || keys(promise,
|
||||
'answer,asker,compiled,event,fulfilled,mixed,paymentClosed,payments,player,preparation,progress,released,shipment,transportClosed,turn')) &&
|
||||
check(object(promise), 'Retain the complete original turn-action commitment and its metadata.');
|
||||
const fields = 'answer,asker,compiled,event,fulfilled,mixed,paymentClosed,payments,player,progress,released,shipment,transportClosed,turn'.split(',');
|
||||
if ('preparation' in promise) fields.push('preparation');
|
||||
if ('alliance' in promise) fields.push('alliance');
|
||||
check(keys(promise, fields.sort().join(',')) &&
|
||||
positive(promise.turn) && identifier(promise.event) && identifier(promise.player) && identifier(promise.asker),
|
||||
'Retain the complete original turn-action commitment and its metadata.');
|
||||
let mixed: MixedActionExpression<TurnActionLeaf>;
|
||||
@@ -174,6 +187,7 @@ function readPromise(promise: TurnActionPromise): CompiledMixedAction<TurnAction
|
||||
const recipients = new Set<string>();
|
||||
const lots = new Set<number>();
|
||||
let hasRevival = false;
|
||||
let hasAlliance = false;
|
||||
const visit = (node: MixedActionExpression<TurnActionLeaf>): void => {
|
||||
if (node.kind === 'action') {
|
||||
originals.add(actionKey(node.action));
|
||||
@@ -181,11 +195,20 @@ function readPromise(promise: TurnActionPromise): CompiledMixedAction<TurnAction
|
||||
if (node.action.kind === 'payment') recipients.add(node.action.recipient);
|
||||
if (node.action.kind === 'bid') lots.add(node.action.lot);
|
||||
if (node.action.kind === 'forces' || node.action.kind === 'leader') hasRevival = true;
|
||||
if (node.action.kind === 'alliance') hasAlliance = true;
|
||||
} else if (node.kind !== 'fact') node.terms.forEach(visit);
|
||||
};
|
||||
visit(mixed);
|
||||
check(!lots.size || !!promise.preparation?.bid, 'Retain the original private lot binding for every ordinary bid leaf.');
|
||||
check(!hasRevival || !!promise.preparation?.revival, 'Retain this answer’s ordinary Revival interval.');
|
||||
check(!hasAlliance || !!promise.alliance, 'Retain the original private Nexus binding for every final-alliance leaf.');
|
||||
if ('alliance' in promise) {
|
||||
const alliance = promise.alliance;
|
||||
check(object(alliance) && keys(alliance, 'closed,partner,source') && identifier(alliance.source) &&
|
||||
typeof alliance.closed === 'boolean' && (alliance.partner === null || identifier(alliance.partner) &&
|
||||
alliance.partner !== promise.player) && (alliance.closed || alliance.partner === null),
|
||||
'Retain only the original Nexus source and its actual final membership after closure.');
|
||||
}
|
||||
if ('preparation' in promise) {
|
||||
const preparation = promise.preparation;
|
||||
check(object(preparation) && keys(preparation, 'bid,revival'), 'Retain only the private native preparation stages.');
|
||||
@@ -234,6 +257,7 @@ function readPromise(promise: TurnActionPromise): CompiledMixedAction<TurnAction
|
||||
}
|
||||
|
||||
function actionValue(action: TurnActionLeaf, promise: TurnActionPromise): boolean {
|
||||
if (action.kind === 'alliance') return !!promise.alliance?.closed && promise.alliance.partner === action.partner;
|
||||
if (action.kind === 'bid') return (promise.preparation?.bid?.maximum ?? 0) >= action.minimum;
|
||||
if (action.kind === 'forces') return (promise.preparation?.revival?.[action.counter] ?? 0) >= action.minimum;
|
||||
if (action.kind === 'leader') return promise.preparation?.revival?.leaders.includes(action.leader) ?? false;
|
||||
@@ -252,6 +276,7 @@ function evaluate(node: CompiledMixedAction<TurnActionLeaf>, promise: TurnAction
|
||||
}
|
||||
|
||||
function actionClosed(action: TurnActionLeaf, promise: TurnActionPromise): boolean {
|
||||
if (action.kind === 'alliance') return promise.alliance!.closed;
|
||||
if (action.kind === 'bid') return promise.preparation!.bid!.closed;
|
||||
if (action.kind === 'forces' || action.kind === 'leader') return promise.preparation!.revival!.closed;
|
||||
return action.kind === 'payment' ? promise.paymentClosed : action.kind === 'ship'
|
||||
@@ -289,6 +314,7 @@ export function liveTurnActionPromises<T extends TurnActionPromise>(promises: re
|
||||
function sameAction(left: TurnActionBranchLeaf, right: TurnActionBranchLeaf): boolean {
|
||||
if (left.kind !== right.kind) return false;
|
||||
if (left.kind === 'leader') return right.kind === 'leader' && left.leader === right.leader;
|
||||
if (left.kind === 'alliance') return right.kind === 'alliance' && left.partner === right.partner && left.source === right.source;
|
||||
if (left.kind === 'forces') return right.kind === 'forces' && left.counter === right.counter && left.progress === right.progress;
|
||||
if (left.kind === 'bid') return right.kind === 'bid' && left.lot === right.lot && left.source === right.source;
|
||||
if (left.kind === 'payment') return right.kind === 'payment' && left.recipient === right.recipient && left.progress === right.progress;
|
||||
@@ -299,18 +325,22 @@ function sameAction(left: TurnActionBranchLeaf, right: TurnActionBranchLeaf): bo
|
||||
function mergeBranches(left: TurnActionBranchLeaf[], right: TurnActionBranchLeaf[]): TurnActionBranchLeaf[] | null {
|
||||
const merged = left.map(leaf => ({ ...leaf }));
|
||||
for (const leaf of right) {
|
||||
if (leaf.kind === 'leader') {
|
||||
if (leaf.kind === 'leader' || leaf.kind === 'alliance') {
|
||||
const prior = merged.find(other => sameAction(other, leaf));
|
||||
if (prior && prior.answer !== leaf.answer) return null;
|
||||
if (leaf.kind === 'alliance' && leaf.answer && merged.some(other => other.kind === 'alliance' &&
|
||||
other.answer && other.source === leaf.source && other.partner !== leaf.partner)) return null;
|
||||
if (!prior) merged.push({ ...leaf });
|
||||
continue;
|
||||
}
|
||||
const same = merged.find(other => sameAction(other, leaf) && other.answer === leaf.answer);
|
||||
if (same && same.kind !== 'leader') same.minimum = leaf.answer ? Math.max(same.minimum, leaf.minimum) : Math.min(same.minimum, leaf.minimum);
|
||||
if (same && same.kind !== 'leader' && same.kind !== 'alliance')
|
||||
same.minimum = leaf.answer ? Math.max(same.minimum, leaf.minimum) : Math.min(same.minimum, leaf.minimum);
|
||||
else merged.push({ ...leaf });
|
||||
const yes = merged.find(other => sameAction(other, leaf) && other.answer);
|
||||
const no = merged.find(other => sameAction(other, leaf) && !other.answer);
|
||||
if (yes && no && yes.kind !== 'leader' && no.kind !== 'leader' && yes.minimum >= no.minimum) return null;
|
||||
if (yes && no && yes.kind !== 'leader' && no.kind !== 'leader' && yes.kind !== 'alliance' && no.kind !== 'alliance' &&
|
||||
yes.minimum >= no.minimum) return null;
|
||||
if (leaf.kind === 'ship' && leaf.answer && merged.some(other =>
|
||||
other.kind === 'ship' && other.answer && other.territory !== leaf.territory)) return null;
|
||||
}
|
||||
@@ -328,6 +358,7 @@ export function* turnActionBranches(promise: TurnActionPromise): Generator<TurnA
|
||||
const achieved = actionValue(action, promise);
|
||||
if (achieved || actionClosed(action, promise)) { if (achieved === answer) yield []; return; }
|
||||
if (action.kind === 'bid') yield [{ ...action, source: promise.preparation!.bid!.source, answer }];
|
||||
else if (action.kind === 'alliance') yield [{ ...action, source: promise.alliance!.source, answer }];
|
||||
else if (action.kind === 'forces') yield [{ ...action, progress: promise.preparation!.revival![action.counter], answer }];
|
||||
else if (action.kind === 'payment') yield [{ ...action, answer,
|
||||
progress: promise.payments.find(payment => payment.recipient === action.recipient)?.amount ?? 0 }];
|
||||
@@ -360,6 +391,29 @@ function settle<T extends TurnActionPromise>(promise: T, update: T): T {
|
||||
? { ...update, fulfilled: true } : update;
|
||||
}
|
||||
|
||||
/** Only the exact original Nexus closure records final membership. Temporary
|
||||
* proposals/pairs never call this hook. A newly impossible whole No releases;
|
||||
* the engine rejects own voluntary closure by inspecting that detached result. */
|
||||
export function settleTurnActionAlliance<T extends TurnActionPromise>(promises: readonly T[], turn: number,
|
||||
source: string, allies: Readonly<Record<string, string | null>>): T[] {
|
||||
check(Array.isArray(promises) && positive(turn) && identifier(source) && object(allies) &&
|
||||
Object.entries(allies).every(([player, partner]) => identifier(player) &&
|
||||
(partner === null || identifier(partner) && partner !== player)),
|
||||
'Retain the original Nexus and its actual final alliance snapshot.');
|
||||
for (const promise of promises) check(object(promise), 'Retain an original turn-action record.');
|
||||
return promises.map(promise => {
|
||||
if (promise.turn !== turn || promise.fulfilled || promise.released) return promise;
|
||||
readPromise(promise);
|
||||
const stage = promise.alliance;
|
||||
if (!stage || stage.closed || stage.source !== source) return promise;
|
||||
check(Object.prototype.hasOwnProperty.call(allies, promise.player),
|
||||
'Retain the respondent’s actual final membership at this original Nexus closure.');
|
||||
const update = { ...promise, alliance: { ...stage, partner: allies[promise.player], closed: true } };
|
||||
if (!promise.answer && turnActionValue(update)) return { ...update, released: true };
|
||||
return settle(promise, update);
|
||||
});
|
||||
}
|
||||
|
||||
/** Only an actually accepted ordinary declaration counts for its original lot.
|
||||
* Repeated declarations retain their largest amount, never their sum or invoice. */
|
||||
export function recordTurnActionBid<T extends TurnActionPromise>(promises: readonly T[], player: string,
|
||||
@@ -515,7 +569,7 @@ export function finishTurnActionTurn<T extends TurnActionPromise>(promises: read
|
||||
bid: promise.preparation.bid ? { ...promise.preparation.bid, closed: true } : null,
|
||||
revival: promise.preparation.revival ? { ...promise.preparation.revival, closed: true } : null,
|
||||
} } : {}) };
|
||||
const fulfilled = turnActionValue(closed) === promise.answer;
|
||||
const fulfilled = !(promise.alliance && !promise.alliance.closed) && turnActionValue(closed) === promise.answer;
|
||||
return { ...closed, fulfilled, released: !fulfilled };
|
||||
});
|
||||
}
|
||||
@@ -530,12 +584,15 @@ export function validateTurnActionPromises(promises: readonly TurnActionPromise[
|
||||
const satisfied = evaluate(compiled, promise);
|
||||
const allClosed = promise.transportClosed && promise.paymentClosed &&
|
||||
(!promise.preparation?.bid || promise.preparation.bid.closed) &&
|
||||
(!promise.preparation?.revival || promise.preparation.revival.closed);
|
||||
(!promise.preparation?.revival || promise.preparation.revival.closed) &&
|
||||
(!promise.alliance || promise.alliance.closed);
|
||||
const recipientsValid = (node: MixedActionExpression<TurnActionLeaf>): boolean => node.kind === 'action'
|
||||
? node.action.kind !== 'payment' || players.includes(node.action.recipient) && node.action.recipient !== promise.player
|
||||
? node.action.kind === 'alliance' ? players.includes(node.action.partner) && node.action.partner !== promise.player :
|
||||
node.action.kind !== 'payment' || players.includes(node.action.recipient) && node.action.recipient !== promise.player
|
||||
: node.kind === 'fact' || node.terms.every(recipientsValid);
|
||||
check(!events.has(promise.event) && players.includes(promise.player) && players.includes(promise.asker) &&
|
||||
promise.player !== promise.asker && recipientsValid(promise.mixed) &&
|
||||
(!promise.alliance || promise.alliance.partner === null || players.includes(promise.alliance.partner)) &&
|
||||
(promise.released || (promise.answer || !satisfied) &&
|
||||
(!promise.answer || promise.fulfilled === satisfied) &&
|
||||
(!promise.fulfilled || satisfied === promise.answer && (promise.answer || relevantClosed(compiled, promise))) &&
|
||||
|
||||
Reference in new issue
Block a user