diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index a16a01f8..a6b52c93 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -17,6 +17,14 @@ open mode gates. ## 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.** Fremen can separately protect either or both threatened allies before one worm resolves, with an independent Karama response for each and the original diff --git a/docs/OPTIONAL_ALLIANCE_VICTORY.md b/docs/OPTIONAL_ALLIANCE_VICTORY.md index 01c5c47a..c511c325 100644 --- a/docs/OPTIONAL_ALLIANCE_VICTORY.md +++ b/docs/OPTIONAL_ALLIANCE_VICTORY.md @@ -1,5 +1,17 @@ # 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 **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) describe the pair power and cancellations. Applying it to both allies and 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 @@ -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) 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 -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. ## 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. 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 remaining faction benefits and interactions are not connected. No lobby can start a group game yet. The recipient-specific pledge and allocation procedure diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md index 050a7cd6..fd73d777 100644 --- a/docs/RULE_DECISIONS.md +++ b/docs/RULE_DECISIONS.md @@ -1,5 +1,11 @@ # 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 The staged Basic group prototype gives Fremen a separate protection choice diff --git a/game/engine.ts b/game/engine.ts index 519086c7..e862facc 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -16096,12 +16096,12 @@ function applyCoalitionTransition(g: Game, result: CoalitionTransition) { } syncCoalitionEscrow(g, before); } -/** Staged group-funding scope: ordinary Basic planet shipments and bids; - * independent purchased rates and special transactions remain separate. */ +/** Staged group-funding scope: Basic physical shipments and ordinary bids; + * optional modules and special transactions remain separate. */ function coalitionFundingAvailable(g: Game, p: Player, committedBid = false): boolean { 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.karamaShipping || + g.strongholdCards || g.ecazTreachery || g.discoveryEnabled || g.spiceBankerIncomePreview || g.kullPreview || g.richeseAuction || g.ixAuction || (!committedBid && (g.bidPromises?.length || g.phaseBidPromises?.length || g.turnActionPromises?.length || g.revivalPromises?.length || g.bribePromises?.length))) return false; @@ -38526,7 +38526,7 @@ function commitGuildTransport( const groupContributors = groupShares ? [{payer:p.id, amount:cost-groupTotal}, ...groupShares.map(share => ({payer:share.donor, amount:share.amount}))] .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, 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}, ...groupShares.map(share => ({payer:share.donor, amount:share.amount}))] .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, diff --git a/game/reference.ts b/game/reference.ts index f9a515a3..48641bf4 100644 --- a/game/reference.ts +++ b/game/reference.ts @@ -4501,11 +4501,11 @@ export const RULE_TOPICS: RuleTopic[] = [ 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.', 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: '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, 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: [ 'mentat', diff --git a/tests/coalition-guild-shipment.test.ts b/tests/coalition-guild-shipment.test.ts index 1fe38959..38d2c81a 100644 --- a/tests/coalition-guild-shipment.test.ts +++ b/tests/coalition-guild-shipment.test.ts @@ -151,6 +151,30 @@ void test('Guild-allied group shipment uses half price and sends each non-Guild 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', () => { const {game: initial, karama} = fixture(); const declared = send(fundAlly(initial), 'a', discounted); diff --git a/tests/coalition-guild-transport.test.ts b/tests/coalition-guild-transport.test.ts index 69265b0b..20022ca9 100644 --- a/tests/coalition-guild-transport.test.ts +++ b/tests/coalition-guild-transport.test.ts @@ -121,6 +121,35 @@ void test('group cross-planet shipment retains donor shares across permission an 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', () => { const initial = fixture(); let {game} = initial;