Allow group actions after settled Truthtrance
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 00:12:09 +11:00
1 parent 9cc659602c
commit 420943b890
8 files changed
+135 -5

No files matched your search

+8
View File
@@ -17,6 +17,14 @@ open mode gates.
## Current checkpoint and work ## Current checkpoint and work
**Staged group actions after Truthtrance history — 29 October. Prototyped / Partial.**
Settled commitment records no longer permanently disable group Nexus and
funding. Unresolved records remain blocked until reconciliation, and active
bribe-expression and revival variants are also fenced. Group play is still
unavailable from the lobby. Five related files pass 60 cases; typecheck, lint
and the production build pass. The new cases stage commitment histories and exercise real group
actions, rather than a full Truthtrance journey. [Scope](OPTIONAL_ALLIANCE_VICTORY.md#group-actions-after-truthtrance-history--29-october-2026).
**Staged group funding with Karama shipment pricing — 29 October. Prototyped / Partial.** **Staged group funding with Karama shipment pricing — 29 October. Prototyped / Partial.**
Group pledges remain available at a purchased Karama rate for physical reserve Group pledges remain available at a purchased Karama rate for physical reserve
shipments and native Guild transport, with all shares paid to the bank. Group shipments and native Guild transport, with all shares paid to the bank. Group
+10
View File
@@ -1,5 +1,15 @@
# Optional three-stronghold alliance victory # Optional three-stronghold alliance victory
## Group actions after Truthtrance history — 29 October 2026
**Staged Basic prototype, not a selectable game mode.** Fulfilled and released
Truthtrance records no longer disable later group Nexus negotiations or group
funding. Unresolved records remain fenced until their native reconciliation
closes them, including an unresolved older record; funding also fences live
bribe-expression and leader/expression revival commitments. The existing
committed-bid settlement exception is retained. Combining group actions with
unresolved commitments remains unfinished, and no lobby exposes group play.
## Group funding with Karama shipment pricing — 29 October 2026 ## Group funding with Karama shipment pricing — 29 October 2026
**Staged Basic prototype, not a selectable game mode.** Group members can **Staged Basic prototype, not a selectable game mode.** Group members can
+6
View File
@@ -1,5 +1,11 @@
# Rule decision index # Rule decision index
## Group actions after Truthtrance history — 29 October 2026
Settled commitment records no longer prevent staged group negotiation and
funding. Unresolved records continue to require native reconciliation before
these unsupported combinations can proceed. [Scope](OPTIONAL_ALLIANCE_VICTORY.md#group-actions-after-truthtrance-history--29-october-2026).
## Group funding with Karama shipment pricing — 29 October 2026 ## Group funding with Karama shipment pricing — 29 October 2026
The staged Basic group prototype retains separate donor shares when a player The staged Basic group prototype retains separate donor shares when a player
+10 -4
View File
@@ -16031,6 +16031,11 @@ function allianceQuestionWindow(g: Game): boolean {
return !!allianceOpportunity(g) && !g.spiceWindow && !g.spiceResolution && !g.response && return !!allianceOpportunity(g) && !g.spiceWindow && !g.spiceResolution && !g.response &&
!g.decision && !g.phaseOpening && !g.pendingTreacheryDiscard && !g.pendingKarama && !g.pendingKull; !g.decision && !g.phaseOpening && !g.pendingTreacheryDiscard && !g.pendingKarama && !g.pendingKull;
} }
/** Retained settled Truthtrance history is not an active commitment. Keep
* unresolved records fenced until native reconciliation has closed them. */
function unsettledPromises(records: readonly {fulfilled?: boolean; released?: boolean}[] | undefined): boolean {
return !!records?.some(record => !record.fulfilled && !record.released);
}
/** Group negotiation is deliberately unexposed until its other alliance /** Group negotiation is deliberately unexposed until its other alliance
* mechanics are integrated. This narrow profile keeps existing pair games * mechanics are integrated. This narrow profile keeps existing pair games
* and unrelated commitments on their original paths. */ * and unrelated commitments on their original paths. */
@@ -16044,8 +16049,8 @@ function coalitionNexusAvailable(g: Game): boolean {
g.expansions.length === 0 && g.players.every(seat => g.expansions.length === 0 && g.players.every(seat =>
['atreides', 'beneGesserit', 'emperor', 'fremen', 'guild', 'harkonnen'].includes(seat.faction)) && ['atreides', 'beneGesserit', 'emperor', 'fremen', 'guild', 'harkonnen'].includes(seat.faction)) &&
Object.values(g.aid).every(credit => credit.amount === 0) && Object.values(g.aid).every(credit => credit.amount === 0) &&
!(g.alliancePromises?.length || g.allianceExpressionPromises?.length || ![g.alliancePromises, g.allianceExpressionPromises, g.bribePromises,
g.bribePromises?.length || g.bribeExpressionPromises?.length || g.turnActionPromises?.length); g.bribeExpressionPromises, g.turnActionPromises].some(unsettledPromises);
} }
function coalitionRule<T>(work: () => T): T { function coalitionRule<T>(work: () => T): T {
try { return work(); } try { return work(); }
@@ -16103,8 +16108,9 @@ function coalitionFundingAvailable(g: Game, p: Player, committedBid = false): bo
g.expansions.length || g.homeworlds || g.nexusCards || g.leaderSkills || g.techTokens || g.expansions.length || g.homeworlds || g.nexusCards || g.leaderSkills || g.techTokens ||
g.strongholdCards || g.ecazTreachery || g.discoveryEnabled || g.strongholdCards || g.ecazTreachery || g.discoveryEnabled ||
g.spiceBankerIncomePreview || g.kullPreview || g.richeseAuction || g.ixAuction || g.spiceBankerIncomePreview || g.kullPreview || g.richeseAuction || g.ixAuction ||
(!committedBid && (g.bidPromises?.length || g.phaseBidPromises?.length || g.turnActionPromises?.length || (!committedBid && [g.bidPromises, g.phaseBidPromises, g.turnActionPromises,
g.revivalPromises?.length || g.bribePromises?.length))) return false; g.revivalPromises, g.leaderRevivalPromises, g.revivalExpressionPromises,
g.bribePromises, g.bribeExpressionPromises].some(unsettledPromises))) return false;
const group = coalitionRule(() => coalitionOf(g.players, g.coalitionState, p.id)); const group = coalitionRule(() => coalitionOf(g.players, g.coalitionState, p.id));
return group?.members.length === 3; return group?.members.length === 3;
} }
+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: '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: '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: '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, Guild transport, purchased-rate funding, Emperor and Fremen aid, worm protection, battle preparation and allied traitor votes; larger alliances and full combinations remain open.', evidence: ['tests/alliance-victory-option.test.ts', 'tests/short-alliance-victory-runtime.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, funding after staged settled Truthtrance records, Guild transport, purchased-rate funding, Emperor and Fremen aid, worm protection, battle preparation and allied traitor votes; larger alliances and full combinations remain open.', evidence: ['tests/alliance-victory-option.test.ts', 'tests/short-alliance-victory-runtime.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: [ related: [
'mentat', 'mentat',
+13
View File
@@ -104,6 +104,19 @@ void test('each donor share of a leading group bid remains locked until outbid',
assert.equal(spice(game), 32); assert.equal(spice(game), 32);
}); });
void test('fulfilled and released bid promises leave group bid funding usable', () => {
for (const history of ['settled', 'prior-turn'] as const) {
const initial = fixture();
initial.phaseBidPromises = [{event:`bid-${history}`, player:'d', asker:'a',
turn:history === 'prior-turn' ? initial.turn - 1 : initial.turn,
minimum:1, answer:true, ...(history === 'settled' ? {fulfilled:true} : {released:true})}];
const game = act(pledge(initial), 'a', groupBid);
assert.equal(game.auction?.bidder, 'a');
assert.deepEqual(game.auction?.coalitionContributions, groupBid.coalitionContributions);
assert.deepEqual(game.phaseBidPromises, initial.phaseBidPromises);
}
});
void test('only the bidder sees the combined group payment, without raw donor allocations', () => { void test('only the bidder sees the combined group payment, without raw donor allocations', () => {
const game = act(pledge(fixture()), 'a', groupBid); const game = act(pledge(fixture()), 'a', groupBid);
const own = viewGame(game, 'a').auction; assert.ok(own); const own = viewGame(game, 'a').auction; assert.ok(own);
+39
View File
@@ -65,6 +65,45 @@ void test('native pair expands only after three separate group approvals', () =>
assert.deepEqual(game.ready, []); assert.deepEqual(game.ready, []);
}); });
void test('fulfilled and released Truthtrance records do not prevent group Nexus negotiation', () => {
for (const history of ['settled', 'prior-turn'] as const) {
const game = originalPair();
// Stage only the public history; pair, proposal and answers are real actions.
game.bribePromises = [{event: `bribe-${history}`, player:'d', asker:'a',
turn:history === 'prior-turn' ? game.turn - 1 : game.turn,
recipient:'c', minimum:1, answer:true, progress:history === 'settled' ? 1 : 0,
...(history === 'settled' ? {fulfilled:true} : {released:true})}];
game.alliancePromises = [{event:`alliance-${history}`, source:'old-nexus',
player:'d', asker:'a', turn:game.turn - 1, partner:'c', answer:false, fulfilled:true}];
assert.ok(viewGame(game, 'a').coalitionNexus, 'Old answers do not hide the group controls.');
let formed = propose(game);
formed = answer(formed, 'b'); formed = answer(formed, 'c');
assert.deepEqual(coalitionOf(formed.players, formed.coalitionState, 'a')!.members, ['a','b','c']);
assert.deepEqual(formed.bribePromises, game.bribePromises);
assert.deepEqual(formed.alliancePromises, game.alliancePromises);
}
});
void test('an unresolved prior-turn Truthtrance record still blocks group Nexus', () => {
const game = originalPair();
game.bribePromises = [{event:'unreconciled-bribe', player:'d', asker:'a', turn:game.turn - 1,
recipient:'c', minimum:1, answer:true, progress:0}];
assert.equal(viewGame(game, 'a').coalitionNexus, null);
const pair = coalitionOf(game.players, game.coalitionState, 'a')!;
rejected(game, 'a', {type:'coalitionPropose', groupId:pair.id, entrant:'c',
expectedRevision:game.coalitionState!.revision});
});
void test('a current unanswered group-incompatible Truthtrance commitment still blocks group Nexus', () => {
const game = originalPair();
game.bribePromises = [{event:'live-bribe', player:'d', asker:'a', turn:game.turn,
recipient:'c', minimum:1, answer:true, progress:0}];
assert.equal(viewGame(game, 'a').coalitionNexus, null);
const pair = coalitionOf(game.players, game.coalitionState, 'a')!;
rejected(game, 'a', {type:'coalitionPropose', groupId:pair.id, entrant:'c',
expectedRevision:game.coalitionState!.revision});
});
void test('any participant may decline and the original pair remains unchanged', () => { void test('any participant may decline and the original pair remains unchanged', () => {
const original = originalPair(); const original = originalPair();
const pending = propose(original); const pending = propose(original);
+48
View File
@@ -101,6 +101,54 @@ void test('two allies fund one ordinary shipment with donor-specific settlement
assert.equal(spiceIncludingEscrow(game), originalSpice); assert.equal(spiceIncludingEscrow(game), originalSpice);
}); });
void test('fulfilled and released Truthtrance records leave group shipment funding usable', () => {
for (const history of ['settled', 'prior-turn'] as const) {
const initial = fixture();
initial.bribePromises = [{event:`bribe-${history}`, player:'d', asker:'a',
turn:history === 'prior-turn' ? initial.turn - 1 : initial.turn,
recipient:'c', minimum:1, answer:true, progress:history === 'settled' ? 1 : 0,
...(history === 'settled' ? {fulfilled:true} : {released:true})}];
let game = pledge(initial);
assert.equal(viewGame(game, 'a').coalitionFunding?.incoming.length, 2);
game = finishResponses(send(game, 'a', shipment));
assert.equal(game.players[0].forces['imperial_basin:9'], 3);
assert.equal(remaining(game, 'b'), 1);
assert.equal(remaining(game, 'c'), 1);
assert.deepEqual(game.bribePromises, initial.bribePromises);
conservedForces(game);
}
});
void test('an unreconciled prior-turn commitment blocks group shipment funding', () => {
const game = fixture();
game.bribePromises = [{event:'unreconciled-bribe', player:'d', asker:'a', turn:game.turn - 1,
recipient:'c', minimum:1, answer:true, progress:0}];
assert.equal(viewGame(game, 'a').coalitionFunding, null);
const before = structuredClone(game);
assert.throws(() => send(game, 'b', {type:'coalitionPledge', recipient:'a', amount:4}));
assert.deepEqual(game, before);
});
void test('a live No bribe-expression commitment blocks group shipment funding', () => {
const game = fixture();
game.bribeExpressionPromises = [{event:'live-expression-no', player:'d', asker:'a',
turn:game.turn, claim:{kind:'payment', recipient:'c', minimum:1}, answer:false, progress:[]}];
assert.equal(viewGame(game, 'a').coalitionFunding, null);
const before = structuredClone(game);
assert.throws(() => send(game, 'b', {type:'coalitionPledge', recipient:'a', amount:4}));
assert.deepEqual(game, before);
});
void test('a current unsupported Truthtrance commitment still blocks group shipment funding', () => {
const game = fixture();
game.bribePromises = [{event:'live-bribe', player:'d', asker:'a', turn:game.turn,
recipient:'c', minimum:1, answer:true, progress:0}];
assert.equal(viewGame(game, 'a').coalitionFunding, null);
const before = structuredClone(game);
assert.throws(() => send(game, 'b', {type:'coalitionPledge', recipient:'a', amount:4}));
assert.deepEqual(game, before);
});
void test('fake or insufficient donor contributions reject without payment or arrival', () => { void test('fake or insufficient donor contributions reject without payment or arrival', () => {
const game = pledge(fixture()); const game = pledge(fixture());
for (const contributions of [ for (const contributions of [