Honor group alliances in Truthtrance
Verify and publish checkpoint container / container (push) Successful in 11m22s
Verify and publish checkpoint container / container (push) Successful in 11m22s
This commit is contained in:
1 parent
9cb65a21f5
commit
3e0fa2a8a2
11 files changed
+592
-24
No files matched your search
+45
-6
@@ -47,7 +47,7 @@ import { TravelPromiseError, liveTravelPromises, travelBranches, recordTravelShi
|
||||
import { mixedTraitorRequirement, type MixedTraitorClaim } from './mixed-traitor-question';
|
||||
import type { MixedBattleActionClaim, MixedBattleClaim } from './mixed-battle-question';
|
||||
import { AllianceExpressionPromiseError, allianceExpressionOutcomes, findAllianceExpressionPlans,
|
||||
allianceExpressionPromisesMatch, evaluateAllianceExpression, validateAllianceExpressionPromises,
|
||||
allianceExpressionPromisesMatch, validateAllianceExpressionPromises,
|
||||
type AllianceExpression, type AllianceExpressionClaim, type AllianceExpressionPromise } from './alliance-expression-promises';
|
||||
import type { AllianceActionClaim, MixedAllianceClaim } from './mixed-alliance-question';
|
||||
import { mixedBidMinimum, type MixedBidClaim } from './mixed-bid-question';
|
||||
@@ -16049,8 +16049,7 @@ function coalitionNexusAvailable(g: Game): boolean {
|
||||
g.expansions.length === 0 && g.players.every(seat =>
|
||||
['atreides', 'beneGesserit', 'emperor', 'fremen', 'guild', 'harkonnen'].includes(seat.faction)) &&
|
||||
Object.values(g.aid).every(credit => credit.amount === 0) &&
|
||||
![g.alliancePromises, g.allianceExpressionPromises, g.bribePromises,
|
||||
g.bribeExpressionPromises, g.turnActionPromises].some(unsettledPromises);
|
||||
![g.bribePromises, g.bribeExpressionPromises, g.turnActionPromises].some(unsettledPromises);
|
||||
}
|
||||
function coalitionRule<T>(work: () => T): T {
|
||||
try { return work(); }
|
||||
@@ -16139,6 +16138,12 @@ function coalitionBidPaymentQuote(g: Game, p: Player, cost: number, contribution
|
||||
}
|
||||
/** The original native reciprocal quote owns identity and pairing legality. */
|
||||
function normalNexusAllianceQuote(g: Game, p: Player, target: string | null) {
|
||||
if (g.coalitionState) {
|
||||
const own = coalitionRule(() => coalitionOf(g.players, g.coalitionState, p.id));
|
||||
const other = target ? coalitionRule(() => coalitionOf(g.players, g.coalitionState, target)) : null;
|
||||
requireRule(!(own && own.members.length >= 3) && !(other && other.members.length >= 3),
|
||||
'Group members must use the group alliance controls.');
|
||||
}
|
||||
if (target !== null) {
|
||||
const other = getPlayer(g, target);
|
||||
requireRule(other.id !== p.id, 'Choose another player.');
|
||||
@@ -16167,10 +16172,45 @@ function normalNexusAllianceQuote(g: Game, p: Player, target: string | null) {
|
||||
return nexusRule(() => quoteNexusAlliance({ players: g.players.map(player => ({ id: player.id, ally: player.ally })),
|
||||
offers: g.allianceOffers, actor: p.id, target }));
|
||||
}
|
||||
/** A larger alliance has multiple allies, so final-Nexus answers must read
|
||||
* membership rather than the legacy single-partner scalar. */
|
||||
function allianceMember(g: Game, player: Player, partner: string): boolean {
|
||||
return g.coalitionState
|
||||
? coalitionRule(() => coalitionAllies(g.players, g.coalitionState, player.id)).includes(partner)
|
||||
: player.ally === partner;
|
||||
}
|
||||
function allianceExpressionValue(g: Game, player: Player, expression: AllianceExpression): boolean {
|
||||
if (expression.kind === 'partner') return allianceMember(g, player, expression.partner);
|
||||
return expression.kind === 'and'
|
||||
? expression.terms.every(term => allianceExpressionValue(g, player, term))
|
||||
: expression.terms.some(term => allianceExpressionValue(g, player, term));
|
||||
}
|
||||
function groupAllianceAnswersMatch(g: Game, player: Player, opportunity: AllianceOpportunity,
|
||||
promises: readonly AlliancePromise[], expressions: readonly AllianceExpressionPromise[]): boolean {
|
||||
const live = (row: AllianceCommitment) => row.player === player.id && row.turn === opportunity.turn &&
|
||||
row.source === opportunity.source && !row.fulfilled && !row.released;
|
||||
return promises.filter(live).every(row => allianceMember(g, player, row.partner) === row.answer) &&
|
||||
expressions.filter(live).every(row => allianceExpressionValue(g, player, row.claim) === row.answer);
|
||||
}
|
||||
function alliancePromisePlan(g: Game, p: Player, promises = g.alliancePromises ?? [],
|
||||
expressions = g.allianceExpressionPromises ?? []) {
|
||||
const opportunity = allianceOpportunity(g);
|
||||
if (!opportunity) return null;
|
||||
const group = g.coalitionState && coalitionRule(() => coalitionOf(g.players, g.coalitionState, p.id));
|
||||
if (group && group.members.length >= 3) {
|
||||
if (groupAllianceAnswersMatch(g, p, opportunity, promises, expressions))
|
||||
return {actions: [] as Action[], bribePreparation:false};
|
||||
if (g.coalitionState!.proposal || [g.bribePromises, g.bribeExpressionPromises, g.turnActionPromises].some(unsettledPromises))
|
||||
return null;
|
||||
const trial = structuredClone(g);
|
||||
try {
|
||||
applyCoalitionTransition(trial, coalitionRule(() => leaveCoalition(trial.players, trial.coalitionState,
|
||||
{actor:p.id, expectedRevision:trial.coalitionState!.revision})));
|
||||
if (groupAllianceAnswersMatch(trial, getPlayer(trial,p.id), opportunity, promises, expressions))
|
||||
return {actions: [{type:'coalitionLeave', expectedRevision:g.coalitionState!.revision} as Action], bribePreparation:false};
|
||||
} catch (error) { if (!(error instanceof RuleError)) throw error; }
|
||||
return null;
|
||||
}
|
||||
const plans = findAllianceExpressionPlans(promises, expressions, p.id, opportunity,
|
||||
{ players: g.players, offers: g.allianceOffers });
|
||||
// Try every consent-supported final outcome. A financial restriction on one
|
||||
@@ -16336,7 +16376,7 @@ function reconcileAlliancePromises(g: Game, voluntary?: { actor: string; action:
|
||||
if (promise.fulfilled || promise.released) continue;
|
||||
const same = opportunity && promise.turn === opportunity.turn && promise.source === opportunity.source;
|
||||
const player = getPlayer(g, promise.player), whole = 'claim' in promise;
|
||||
const matches = (whole ? evaluateAllianceExpression(promise.claim, player.ally) : player.ally === promise.partner) === promise.answer;
|
||||
const matches = (whole ? allianceExpressionValue(g, player, promise.claim) : allianceMember(g, player, promise.partner)) === promise.answer;
|
||||
const scalars = whole ? kept : [...kept, promise], groups = whole ? [...expressions, promise] : expressions;
|
||||
const possible = same && alliancePromisePlan(g, player, scalars, groups);
|
||||
if (!same && matches) { promise.fulfilled = true; continue; }
|
||||
@@ -16353,8 +16393,7 @@ function reconcileAlliancePromises(g: Game, voluntary?: { actor: string; action:
|
||||
function guardAlliancePromiseReady(g: Game, p: Player) {
|
||||
const opportunity = allianceOpportunity(g);
|
||||
if (!opportunity) return;
|
||||
requireRule(alliancePromisesMatch(g.alliancePromises ?? [], p.id, opportunity, p.ally) &&
|
||||
allianceExpressionPromisesMatch(g.allianceExpressionPromises ?? [], p.id, opportunity, p.ally),
|
||||
requireRule(groupAllianceAnswersMatch(g, p, opportunity, g.alliancePromises ?? [], g.allianceExpressionPromises ?? []),
|
||||
'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))
|
||||
|
||||
Reference in new issue
Block a user