Support group-funded Karama shipment pricing
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 00:09:16 +11:00
1 parent 40c9651b80
commit 9cc659602c
7 files changed
+89 -10

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 funding with Karama shipment pricing — 29 October. Prototyped / Partial.**
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
games remain unavailable from the lobby; other interactions and complete-mode
verification remain open. The related four-file funding and transport union
passes 36 tests, including purchased-rate reserve and cross-planet shipments;
typecheck, lint and the production build pass. [Scope](OPTIONAL_ALLIANCE_VICTORY.md#group-funding-with-karama-shipment-pricing--29-october-2026).
**Staged group Fremen worm protection — 29 October. Prototyped / Partial.** **Staged group Fremen worm protection — 29 October. Prototyped / Partial.**
Fremen can separately protect either or both threatened allies before one Fremen can separately protect either or both threatened allies before one
worm resolves, with an independent Karama response for each and the original worm resolves, with an independent Karama response for each and the original
+15 -3
View File
@@ -1,5 +1,17 @@
# Optional three-stronghold alliance victory # Optional three-stronghold alliance victory
## Group funding with Karama shipment pricing — 29 October 2026
**Staged Basic prototype, not a selectable game mode.** Group members can
pledge and allocate separate shares while a shipper's purchased Karama rate
is active. The same rate applies to physical reserve shipments and native
Guild transport, and every paid share goes to the bank rather than Guild.
The existing native transport permission response still applies. Controls use
the purchased price; bots can ship using their own funds. The donor allocation
procedure remains provisional, and optional modules and other special
shipments remain outside this staged scope. Group games cannot yet be started
from the lobby.
## Group Fremen worm protection — 29 October 2026 ## Group Fremen worm protection — 29 October 2026
**Staged Basic prototype, not a selectable game mode.** When a worm threatens **Staged Basic prototype, not a selectable game mode.** When a worm threatens
@@ -29,7 +41,7 @@ same available group permission. The [publisher rules, pp. 9 and 19](https://www
and [FAQ, pp. 2 and 7](https://www.gf9games.com/dune/wp-content/uploads/2020/11/Dune-FAQ-Nov-2020.pdf) and [FAQ, pp. 2 and 7](https://www.gf9games.com/dune/wp-content/uploads/2020/11/Dune-FAQ-Nov-2020.pdf)
describe the pair power and cancellations. Applying it to both allies and describe the pair power and cancellations. Applying it to both allies and
the donor allocation procedure are provisional. Advanced, optional modules, the donor allocation procedure are provisional. Advanced, optional modules,
No-Field and purchased-rate combinations remain outside this staged path. and No-Field remain outside this staged path.
## Group Guild-rate shipments — 28 October 2026 ## Group Guild-rate shipments — 28 October 2026
@@ -46,7 +58,7 @@ The [publisher rules, p. 19](https://www.gf9games.com/dunegame/wp-content/upload
grant the Guild rate to its ally; the [FAQ, p. 7](https://www.gf9games.com/dune/wp-content/uploads/2020/11/Dune-FAQ-Nov-2020.pdf) grant the Guild rate to its ally; the [FAQ, p. 7](https://www.gf9games.com/dune/wp-content/uploads/2020/11/Dune-FAQ-Nov-2020.pdf)
permits Karama to prevent that rate. Applying the benefit and original split permits Karama to prevent that rate. Applying the benefit and original split
to both members of a larger group is a provisional composition. Other special to both members of a larger group is a provisional composition. Other special
transports, independent purchased rates, optional modules and full group play transports, optional modules and full group play
remain separate; no lobby exposes the group option. remain separate; no lobby exposes the group option.
## Group ordinary-bid funding — 28 October 2026 ## Group ordinary-bid funding — 28 October 2026
@@ -82,7 +94,7 @@ aggregated refunds for removed edges. Controls show only the viewer's own
outgoing and incoming pledges; bots may continue using their own spice. outgoing and incoming pledges; bots may continue using their own spice.
This first path is limited to Basic classic-faction staged groups without This first path is limited to Basic classic-faction staged groups without
optional modules, independently purchased Karama shipment pricing or special transport. optional modules or other special transport.
Other shipment types, special auctions, and the Other shipment types, special auctions, and the
remaining faction benefits and interactions are not connected. No lobby can remaining faction benefits and interactions are not connected. No lobby can
start a group game yet. The recipient-specific pledge and allocation procedure start a group game yet. The recipient-specific pledge and allocation procedure
+6
View File
@@ -1,5 +1,11 @@
# Rule decision index # Rule decision index
## Group funding with Karama shipment pricing — 29 October 2026
The staged Basic group prototype retains separate donor shares when a player
purchases the Karama shipment rate; all shares go to the bank. The multi-donor
allocation remains provisional. [Scope](OPTIONAL_ALLIANCE_VICTORY.md#group-funding-with-karama-shipment-pricing--29-october-2026).
## Group Fremen worm protection — 29 October 2026 ## Group Fremen worm protection — 29 October 2026
The staged Basic group prototype gives Fremen a separate protection choice The staged Basic group prototype gives Fremen a separate protection choice
+5 -5
View File
@@ -16096,12 +16096,12 @@ function applyCoalitionTransition(g: Game, result: CoalitionTransition) {
} }
syncCoalitionEscrow(g, before); syncCoalitionEscrow(g, before);
} }
/** Staged group-funding scope: ordinary Basic planet shipments and bids; /** Staged group-funding scope: Basic physical shipments and ordinary bids;
* independent purchased rates and special transactions remain separate. */ * optional modules and special transactions remain separate. */
function coalitionFundingAvailable(g: Game, p: Player, committedBid = false): boolean { function coalitionFundingAvailable(g: Game, p: Player, committedBid = false): boolean {
if (!g.coalitionEscrow || !g.coalitionState || g.status !== 'playing' || g.advanced || ![3, 5].includes(g.phase) || 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.expansions.length || g.homeworlds || g.nexusCards || g.leaderSkills || g.techTokens ||
g.strongholdCards || g.ecazTreachery || g.discoveryEnabled || g.karamaShipping || 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?.length || g.phaseBidPromises?.length || g.turnActionPromises?.length ||
g.revivalPromises?.length || g.bribePromises?.length))) return false; g.revivalPromises?.length || g.bribePromises?.length))) return false;
@@ -38526,7 +38526,7 @@ function commitGuildTransport(
const groupContributors = groupShares ? [{payer:p.id, amount:cost-groupTotal}, const groupContributors = groupShares ? [{payer:p.id, amount:cost-groupTotal},
...groupShares.map(share => ({payer:share.donor, amount:share.amount}))] ...groupShares.map(share => ({payer:share.donor, amount:share.amount}))]
.filter(share => share.payer !== guild?.id && share.amount > 0) : null; .filter(share => share.payer !== guild?.id && share.amount > 0) : null;
const guildPayment = groupContributors ? groupContributors.reduce((sum, share) => sum + share.amount, 0) : guildShipmentIncome({ const guildPayment = g.karamaShipping?.player === p.id ? 0 : groupContributors ? groupContributors.reduce((sum, share) => sum + share.amount, 0) : guildShipmentIncome({
guild: guild?.id, shipper: p.id, ally: p.ally, cost, allyPayment, guild: guild?.id, shipper: p.id, ally: p.ally, cost, allyPayment,
bankOnly: g.karamaShipping?.player === p.id, bankOnly: g.karamaShipping?.player === p.id,
}); });
@@ -38802,7 +38802,7 @@ function commitShipment(g: Game, shipment: PendingShipment, settlement?: {recipi
const groupContributors = groupShares ? [{payer:p.id, amount:cost - groupTotal}, const groupContributors = groupShares ? [{payer:p.id, amount:cost - groupTotal},
...groupShares.map(share => ({payer:share.donor, amount:share.amount}))] ...groupShares.map(share => ({payer:share.donor, amount:share.amount}))]
.filter(share => share.payer !== guild?.id && share.amount > 0) : null; .filter(share => share.payer !== guild?.id && share.amount > 0) : null;
const guildPayment = groupContributors ? groupContributors.reduce((sum, share) => sum + share.amount, 0) : guildShipmentIncome({ const guildPayment = g.karamaShipping?.player === p.id ? 0 : groupContributors ? groupContributors.reduce((sum, share) => sum + share.amount, 0) : guildShipmentIncome({
guild: guild?.id, guild: guild?.id,
shipper: p.id, shipper: p.id,
ally: p.ally, ally: p.ally,
+2 -2
View File
@@ -4501,11 +4501,11 @@ export const RULE_TOPICS: RuleTopic[] = [
example: example:
'At the final turn with neither Guild nor Fremen, two players each hold two qualifying strongholds and a third holds one. The two leaders of the count share the win.', 'At the final turn with neither Guild nor Fremen, two players each hold two qualifying strongholds and a third holds one. The two leaders of the count share the win.',
checklist: [ checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'A host-selectable shorter two-member target shares victory calculation. Unexposed staged Basic groups connect Nexus, selected funding, Guild transport, Emperor and Fremen aid, Fremen worm protection, Voice, Prescience and Harkonnen traitor support; other group benefits remain open.', evidence: ['game/alliance-victory-option.ts', 'game/victory-progress.ts', 'game/victory-quote.ts', 'game/engine.ts', 'game/coalition-worm-protection.ts', 'game/coalition-battle-preparation.ts', 'game/traitor-declarations.ts', 'game/terminal-cancellation.ts'] }, { area: 'Implementation', status: 'Partial', detail: 'A host-selectable shorter two-member target shares victory calculation. Unexposed staged Basic groups connect Nexus, selected funding including purchased Karama shipment prices, Guild transport, Emperor and Fremen aid, Fremen worm protection, Voice, Prescience and Harkonnen traitor support; other group benefits remain open.', evidence: ['game/alliance-victory-option.ts', 'game/victory-progress.ts', 'game/victory-quote.ts', 'game/engine.ts', 'game/coalition-worm-protection.ts', 'game/coalition-battle-preparation.ts', 'game/traitor-declarations.ts', 'game/terminal-cancellation.ts'] },
{ 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, 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-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, 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'] },
], ],
related: [ related: [
'mentat', 'mentat',
+24
View File
@@ -151,6 +151,30 @@ void test('Guild-allied group shipment uses half price and sends each non-Guild
conserved(done); conserved(done);
}); });
void test('a group-funded Karama shipment sends every payment to the bank', () => {
const {game: initial, karama} = fixture();
// Move the fixture's physical card to the shipper before buying the rate.
player(initial, 'h').hand = player(initial, 'h').hand.filter(card => card.id !== karama.id);
player(initial, 'a').hand.push(karama);
let game = fundAlly(initial);
game = send(game, 'a', {type:'card', card:karama.id, mode:'shipment'});
assert.equal(game.karamaShipping?.player, 'a');
assert.equal(viewGame(game, 'a').coalitionFunding?.incoming.length, 2,
'Both existing pledges remain available after purchasing the rate.');
game = send(game, 'a', discounted);
assert.equal(game.response ?? null, null, 'Karama shipment payments do not trigger Guild income.');
assert.equal(game.karamaShipping, null);
assert.equal(player(game, 'a').spice, 9);
assert.equal(player(game, 'g').spice, 7, 'Guild receives no payer share.');
assert.equal(player(game, 'e').spice, 8);
assert.equal(pledged(game, 'g', 'a'), 2);
assert.equal(pledged(game, 'e', 'a'), 1);
assert.equal(spiceIncludingEscrow(game), 37, 'All three spice went to the bank.');
assert.equal(player(game, 'a').forces['red_chasm:7'], 3);
assert.equal(game.discard.filter(card => card.id === karama.id).length, 1);
conserved(game);
});
void test('Karama removes the group Guild discount and preserves the declared donor shares', () => { void test('Karama removes the group Guild discount and preserves the declared donor shares', () => {
const {game: initial, karama} = fixture(); const {game: initial, karama} = fixture();
const declared = send(fundAlly(initial), 'a', discounted); const declared = send(fundAlly(initial), 'a', discounted);
+29
View File
@@ -121,6 +121,35 @@ void test('group cross-planet shipment retains donor shares across permission an
conserved(game); conserved(game);
}); });
void test('group-funded purchased-rate cross-planet shipment keeps permission and pays the bank', () => {
const initial = fixture();
let {game} = initial;
// Keep Harkonnen's physical Karama available to exercise the permission
// window; the purchaser uses the other undealt physical Karama.
const index = game.deck.findIndex(card => card.effect === 'karama'); assert.ok(index >= 0);
const [karama] = game.deck.splice(index,1); seat(game,'e').hand.push(karama);
seat(game,'e').forces = {'imperial_basin:10':3}; seat(game,'e').reserves = 17;
game = fund(game,'e');
game = send(game,'e',{type:'card',mode:'shipment',card:karama.id});
assert.equal(game.karamaShipping?.player,'e');
game = send(game,'e',{type:'guildShip', forces:{'imperial_basin:10':3},
territory:'red_chasm', sector:7, allyPayment:0, coalitionContributions:shares('e')});
assert.equal(game.response?.kind,'guildTransportPermission');
assert.equal(seat(game,'e').forces['imperial_basin:10'],3);
assert.equal(pledge(game,'g','e'),2); assert.equal(pledge(game,'f','e'),2);
game = passKind(game,'guildTransportPermission');
assert.equal(game.response ?? null,null, 'Purchased rate and bank payment need no Guild-rate or income response.');
assert.equal(seat(game,'e').forces['red_chasm:7'],3);
assert.equal(seat(game,'e').forces['imperial_basin:10'] ?? 0,0);
assert.equal(seat(game,'e').spice,9);
assert.equal(seat(game,'g').spice,8, 'Guild receives none of the shipment payment.');
assert.equal(seat(game,'f').spice,8);
assert.equal(pledge(game,'g','e'),1); assert.equal(pledge(game,'f','e'),1);
assert.equal(game.karamaShipping,null);
assert.equal(game.discard.filter(card => card.id === karama.id).length,1);
conserved(game);
});
void test('Karama reprices the selected group shares while preserving cross-planet shipment and income', () => { void test('Karama reprices the selected group shares while preserving cross-planet shipment and income', () => {
const initial = fixture(); const initial = fixture();
let {game} = initial; let {game} = initial;