Allow group departures with bribe commitments
Verify and publish checkpoint container / container (push) Successful in 10m51s

This commit is contained in:
Quintine committed 2026-10-29 00:46:59 +11:00
1 parent 3e0fa2a8a2
commit 0faa7923b0
13 files changed
+437 -37

No files matched your search

+2 -2
View File
@@ -3189,7 +3189,7 @@ function policyActions(g: GameView): Action[] {
const participants = [...proposal.original.members, proposal.entrant];
return participants.includes(me.id) && !proposal.approvals.includes(me.id)
? [{type: 'coalitionAnswer', event: proposal.event, nexus: proposal.nexus,
turn: proposal.turn, accept: true, expectedRevision: group.revision}]
turn: proposal.turn, accept: !group.admissionBlocked, expectedRevision: group.revision}]
: [];
}
const thumper = me.hand?.find((c) => c.effect === 'thumper');
@@ -3226,7 +3226,7 @@ function policyActions(g: GameView): Action[] {
const card = me.hand?.find((c) => c.effect === 'harvester');
if (card) return [{ type: 'card', card: card.id }];
}
if (g.nexus && !g.spiceWindow && !g.ready.includes(me.id) && group?.variant === 'unlimited' &&
if (g.nexus && !g.spiceWindow && !g.ready.includes(me.id) && group?.variant === 'unlimited' && !group.admissionBlocked &&
!group.groups.some(alliance => alliance.members.includes(me.id))) {
const alliance = group.groups.find(candidate => candidate.members.length >= 2 && candidate.members.length < 6);
if (alliance) return [{type:'coalitionPropose', groupId:alliance.id, entrant:me.id,
+16 -7
View File
@@ -16036,6 +16036,9 @@ function allianceQuestionWindow(g: Game): boolean {
function unsettledPromises(records: readonly {fulfilled?: boolean; released?: boolean}[] | undefined): boolean {
return !!records?.some(record => !record.fulfilled && !record.released);
}
function coalitionAdmissionBlocked(g: Game): boolean {
return [g.bribePromises, g.bribeExpressionPromises].some(unsettledPromises);
}
/** Group negotiation is deliberately unexposed until its other alliance
* mechanics are integrated. This narrow profile keeps existing pair games
* and unrelated commitments on their original paths. */
@@ -16049,7 +16052,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.bribePromises, g.bribeExpressionPromises, g.turnActionPromises].some(unsettledPromises);
!unsettledPromises(g.turnActionPromises);
}
function coalitionRule<T>(work: () => T): T {
try { return work(); }
@@ -16106,12 +16109,14 @@ function coalitionFundingAvailable(g: Game, p: Player, committedBid = false): bo
if (!g.coalitionEscrow || !g.coalitionState || g.status !== 'playing' || g.advanced || ![3, 5].includes(g.phase) ||
g.expansions.length || g.homeworlds || g.nexusCards || g.leaderSkills || g.techTokens ||
g.strongholdCards || g.ecazTreachery || g.discoveryEnabled ||
g.spiceBankerIncomePreview || g.kullPreview || g.richeseAuction || g.ixAuction ||
(!committedBid && [g.bidPromises, g.phaseBidPromises, g.turnActionPromises,
g.revivalPromises, g.leaderRevivalPromises, g.revivalExpressionPromises,
g.bribePromises, g.bribeExpressionPromises].some(unsettledPromises))) return false;
g.spiceBankerIncomePreview || g.kullPreview || g.richeseAuction || g.ixAuction) return false;
const group = coalitionRule(() => coalitionOf(g.players, g.coalitionState, p.id));
return !!group && group.members.length >= 3;
if (!group || group.members.length < 3) return false;
const groupHasUnsettled = (records: readonly {player: string; fulfilled?: boolean; released?: boolean}[] | undefined) =>
!!records?.some(record => group.members.includes(record.player) && !record.fulfilled && !record.released);
return committedBid || ![g.bidPromises, g.phaseBidPromises, g.turnActionPromises,
g.revivalPromises, g.leaderRevivalPromises, g.revivalExpressionPromises,
g.bribePromises, g.bribeExpressionPromises].some(groupHasUnsettled);
}
function coalitionEmperorGroup(g: Game, p: Player) {
if (!g.coalitionState || p.faction !== 'emperor' || g.status !== 'playing' || g.advanced ||
@@ -16200,7 +16205,7 @@ function alliancePromisePlan(g: Game, p: Player, promises = g.alliancePromises ?
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))
if (g.coalitionState!.proposal || unsettledPromises(g.turnActionPromises))
return null;
const trial = structuredClone(g);
try {
@@ -44560,6 +44565,9 @@ function applyActionInner(
}
if (t === 'coalitionPropose' || t === 'coalitionAnswer' || t === 'coalitionLeave') {
requireRule(coalitionNexusAvailable(g), 'Group negotiations are unavailable in this Nexus.');
if (t === 'coalitionPropose' || t === 'coalitionAnswer' && action.accept !== false)
requireRule(!coalitionAdmissionBlocked(g),
'Resolve current bribe commitments before inviting an ally.');
const revision = integer(action.expectedRevision, 0, Number.MAX_SAFE_INTEGER, 'Coalition revision');
if (t === 'coalitionPropose') {
initializeCoalitionEscrow(g);
@@ -46864,6 +46872,7 @@ export function viewGame(state: Game, id: string, now?: number) {
groups: coalitionRule(() => coalitionGroups(g.players, g.coalitionState)),
proposal: g.coalitionState!.proposal,
revision: g.coalitionState!.revision,
...(coalitionAdmissionBlocked(g) ? {admissionBlocked:true} : {}),
...(g.coalitionState!.variant ? {variant: g.coalitionState!.variant} : {}),
} : null,
homeworldAllianceBlocks: Object.fromEntries(g.players.filter((p) => p.id !== id)
+1 -1
View File
@@ -4505,7 +4505,7 @@ export const RULE_TOPICS: RuleTopic[] = [
{ area: 'Player controls', status: 'Partial', detail: 'The lobby offers the pair option. A staged group Emperor can choose either ally for spice and extra revival; group games cannot yet be started from the lobby.', evidence: ['components/game-table.tsx', 'components/coalition-emperor-aid.tsx', 'tests/short-alliance-victory-controls.test.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'Legal alliance and victory paths use the shared target; staged group Emperor bots can offer a targeted revival. Strategic tuning remains deferred.', evidence: ['game/bots.ts', 'game/victory-progress.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'The printed option and provisional two-controller precedence are recorded.', evidence: ['docs/OPTIONAL_ALLIANCE_VICTORY.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Focused pair and staged group evidence includes real Nexus actions, four-member unlimited admission, funding and victory, staged larger-group faction helpers and alliance Truthtrance, funding after settled Truthtrance records, Guild transport, Emperor and Fremen aid, worm protection, battle preparation and allied traitor votes; complete group play and combinations remain open.', evidence: ['tests/alliance-victory-option.test.ts', 'tests/short-alliance-victory-runtime.test.ts', 'tests/coalition-unlimited-engine.test.ts', 'tests/coalition-unlimited-powers.test.ts', 'tests/coalition-unlimited-emperor.test.ts', 'tests/coalition-truth-alliance.test.ts', 'tests/coalition-truth-alliance-controls.test.tsx', 'tests/coalition-truth-bribe-targets.test.ts', 'tests/coalition-unlimited-worm-engine.test.ts', 'tests/coalition-nexus-engine.test.ts', 'tests/coalition-shipment-funding.test.ts', 'tests/coalition-bid-funding.test.ts', 'tests/coalition-guild-shipment.test.ts', 'tests/coalition-guild-transport.test.ts', 'tests/coalition-fremen-worm.test.ts', 'tests/coalition-emperor-aid.test.ts', 'tests/coalition-fremen-revival.test.ts', 'tests/coalition-battle-preparation.test.ts', 'tests/coalition-battle-power-question.test.ts', 'tests/coalition-harkonnen-traitor.test.ts', 'tests/coalition-traitor-declarations.test.ts'] },
{ area: 'Verification', status: 'Partial', detail: 'Focused pair and staged group evidence includes real Nexus actions, four-member unlimited admission, funding and victory, staged larger-group faction helpers and alliance Truthtrance, funding with outsider-only commitments, group departures with live bribes, Guild transport, Emperor and Fremen aid, worm protection, battle preparation and allied traitor votes; complete group play and combinations remain open.', evidence: ['tests/alliance-victory-option.test.ts', 'tests/short-alliance-victory-runtime.test.ts', 'tests/coalition-unlimited-engine.test.ts', 'tests/coalition-unlimited-powers.test.ts', 'tests/coalition-unlimited-emperor.test.ts', 'tests/coalition-truth-alliance.test.ts', 'tests/coalition-truth-alliance-controls.test.tsx', 'tests/coalition-truth-bribe-targets.test.ts', 'tests/coalition-bribe-departure.test.tsx', 'tests/coalition-bribe-alliance-guides.test.ts', 'tests/coalition-outsider-commitment-funding.test.ts', 'tests/coalition-unlimited-worm-engine.test.ts', 'tests/coalition-nexus-engine.test.ts', 'tests/coalition-shipment-funding.test.ts', 'tests/coalition-bid-funding.test.ts', 'tests/coalition-guild-shipment.test.ts', 'tests/coalition-guild-transport.test.ts', 'tests/coalition-fremen-worm.test.ts', 'tests/coalition-emperor-aid.test.ts', 'tests/coalition-fremen-revival.test.ts', 'tests/coalition-battle-preparation.test.ts', 'tests/coalition-battle-power-question.test.ts', 'tests/coalition-harkonnen-traitor.test.ts', 'tests/coalition-traitor-declarations.test.ts'] },
],
related: [
'mentat',