Support group Guild-rate reserve shipments
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
1eeeb32566
commit
b89c225d5b
9 files changed
+297
-26
No files matched your search
+3
-2
@@ -89,7 +89,7 @@ import {
|
||||
canUseAsKaramaRole,
|
||||
shrineTruthtranceBotAction,
|
||||
} from './shrine';
|
||||
import { reserveShipmentCost } from './shipment-price';
|
||||
import { groupGuildShipmentRate, reserveShipmentCost } from './shipment-price';
|
||||
import { moritaniSpecialKaramaBotAction } from './moritani-special-karama-options';
|
||||
import { quoteSmugglerShipment } from './smuggler-shipment';
|
||||
import { quoteSmugglerNoField } from './smuggler-no-field';
|
||||
@@ -3541,9 +3541,10 @@ function policyActions(g: GameView): Action[] {
|
||||
if (g.ornithopter?.active && me.shipped) return [{ type: 'endMovement' }];
|
||||
const actions: Action[] = [...guildHomeworldShipmentActions(g, level), ...homeworldShipmentActions(g, level)];
|
||||
const targets = destinations(g);
|
||||
const groupGuildRate = groupGuildShipmentRate(g.players, me.id, g.coalitionState);
|
||||
const halfRate = g.karamaShipping?.player === me.id ||
|
||||
(!g.guildRateCanceled &&
|
||||
(me.faction === 'guild' ||
|
||||
(me.faction === 'guild' || groupGuildRate ||
|
||||
g.players.some((p) => p.faction === 'guild' && p.id === me.ally)));
|
||||
const shipmentBudget = (me.spice ?? 0) + g.aid.available;
|
||||
const guildSecret = nexusGuildSecretAllyCanAct(g);
|
||||
|
||||
+24
-11
@@ -623,6 +623,7 @@ import { IxStartingCardError, ixStartingCardPoolSize, ixStartingCardDealPlan } f
|
||||
import { ecazDukeRevivalBlock, quoteEcazDukeRevival } from './ecaz-duke-revival';
|
||||
import {
|
||||
reserveShipmentCost,
|
||||
groupGuildShipmentRate,
|
||||
guildShipmentCost,
|
||||
guildShipmentIncome,
|
||||
shipmentPaymentBounds,
|
||||
@@ -16047,8 +16048,8 @@ function applyCoalitionTransition(g: Game, result: CoalitionTransition) {
|
||||
}
|
||||
syncCoalitionEscrow(g, before);
|
||||
}
|
||||
/** Initial staged group-funding scope: ordinary Basic planet shipments, with
|
||||
* Guild outside the group. Guild-allied rate and other purchases remain separate. */
|
||||
/** Staged group-funding scope: ordinary Basic planet shipments and bids;
|
||||
* independent purchased rates 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 ||
|
||||
@@ -16057,8 +16058,10 @@ function coalitionFundingAvailable(g: Game, p: Player, committedBid = false): bo
|
||||
(!committedBid && (g.bidPromises?.length || g.phaseBidPromises?.length || g.turnActionPromises?.length ||
|
||||
g.revivalPromises?.length || g.bribePromises?.length))) return false;
|
||||
const group = coalitionRule(() => coalitionOf(g.players, g.coalitionState, p.id));
|
||||
const guild = byFaction(g, 'guild');
|
||||
return group?.members.length === 3 && (g.phase === 3 || !guild || !group.members.includes(guild.id));
|
||||
return group?.members.length === 3;
|
||||
}
|
||||
function guildGroupAlly(g: Game, p: Player): boolean {
|
||||
return coalitionRule(() => groupGuildShipmentRate(g.players, p.id, g.coalitionState));
|
||||
}
|
||||
function coalitionShipmentQuote(g: Game, p: Player, cost: number, contributions: CoalitionContribution[]) {
|
||||
requireRule(g.phase === 5 && coalitionFundingAvailable(g, p), 'Group funding is unavailable for this shipment.');
|
||||
@@ -38028,7 +38031,8 @@ function ordinaryGuildRateFullCost(g: Game, shipment: PendingShipment): number |
|
||||
const pricedAmount = shipment.noField ? 1 + (shipment.noFieldReserves ?? 0)
|
||||
: shipment.alliedNoField || shipment.nexusEvent ? 1
|
||||
: shipment.amount - (shipment.smuggler ? 1 : 0);
|
||||
if (!guild || (p.id !== guild.id && !(p.ally === guild.id && guild.ally === p.id)) ||
|
||||
if (!guild || (p.id !== guild.id && !(p.ally === guild.id && guild.ally === p.id) &&
|
||||
!guildGroupAlly(g, p)) ||
|
||||
g.karamaShipping?.player === p.id ||
|
||||
(g.guildRateBlocked?.turn === g.turn && g.guildRateBlocked.player === p.id) ||
|
||||
shipment.source || shipment.guildSecretEvent || shipment.richesePair)
|
||||
@@ -38132,10 +38136,16 @@ function finishGuildRateResponse(g: Game, response: ResponseWindow, canceled: bo
|
||||
g.guildRateBlocked = { turn: g.turn, player: shipper.id };
|
||||
const bounds = shipmentPaymentBounds(fullCost, shipper.spice,
|
||||
aidFor(g, shipper)?.amount ?? 0);
|
||||
let groupFunded = true;
|
||||
if (shipment.coalitionContributions !== undefined) {
|
||||
try { coalitionShipmentQuote(g, shipper, fullCost, shipment.coalitionContributions); }
|
||||
catch (error) { if (!(error instanceof RuleError)) throw error; groupFunded = false; }
|
||||
}
|
||||
// A loan supplier approved the original whole price, not an increased gift.
|
||||
const supplierApprovalChanges = shipment.alliedNoField &&
|
||||
shipment.alliedNoField.payer === shipment.alliedNoField.owner;
|
||||
if (supplierApprovalChanges || shipment.allyPayment < bounds.minimum || shipment.allyPayment > bounds.maximum) {
|
||||
if (supplierApprovalChanges || (shipment.coalitionContributions !== undefined ? !groupFunded
|
||||
: shipment.allyPayment < bounds.minimum || shipment.allyPayment > bounds.maximum)) {
|
||||
if (cunning) {
|
||||
delete cunning.shipment;
|
||||
saveGuildCunning(g, cunning);
|
||||
@@ -38680,7 +38690,10 @@ function commitShipment(g: Game, shipment: PendingShipment, settlement?: {recipi
|
||||
const guild = byFaction(g, 'guild');
|
||||
const groupShares = shipment.coalitionContributions;
|
||||
const groupTotal = groupShares?.reduce((sum, share) => sum + share.amount, 0) ?? 0;
|
||||
const guildPayment = groupShares ? (guild ? cost : 0) : guildShipmentIncome({
|
||||
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({
|
||||
guild: guild?.id,
|
||||
shipper: p.id,
|
||||
ally: p.ally,
|
||||
@@ -38690,9 +38703,8 @@ function commitShipment(g: Game, shipment: PendingShipment, settlement?: {recipi
|
||||
!!(shipment.alliedNoField && alliedNoFieldPricing(g, p).bankOnly),
|
||||
});
|
||||
if (!settlement?.recipient && guild && guildPayment > 0) {
|
||||
const named = groupShares ? [{payer:p.id, amount:cost - groupTotal},
|
||||
...groupShares.map(share => ({payer:share.donor, amount:share.amount}))]
|
||||
: [{ payer: p.id, amount: p.id === guild.id ? 0 : cost - allyPayment },
|
||||
const named = groupContributors ??
|
||||
[{ payer: p.id, amount: p.id === guild.id ? 0 : cost - allyPayment },
|
||||
...(allyPayment && p.ally !== guild.id ? [{ payer: p.ally!, amount: allyPayment }] : [])];
|
||||
g.response = guildPaymentResponse(g, guild.id,
|
||||
groupShares ? named.map(share => share.amount) : shipmentIncomeContributions(g, p, cost, allyPayment),
|
||||
@@ -39292,8 +39304,9 @@ function richesePairIntegrity(g: Game) {
|
||||
}
|
||||
}
|
||||
function shipmentHalfRate(g: Game, p: Player) {
|
||||
const guild = byFaction(g, 'guild');
|
||||
return g.karamaShipping?.player === p.id ||
|
||||
((p.faction === 'guild' || byFaction(g,'guild')?.id === p.ally) &&
|
||||
((p.faction === 'guild' || guild?.id === p.ally || guildGroupAlly(g, p)) &&
|
||||
!(g.guildRateBlocked?.turn === g.turn && g.guildRateBlocked.player === p.id));
|
||||
}
|
||||
function nexusRicheseSignature(record: NonNullable<Game['nexusRicheseHistory']>[number]) {
|
||||
|
||||
+2
-2
@@ -2795,10 +2795,10 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
example:
|
||||
'You have two spice and your ally pledges four. You may bid six; winning uses two of yours and four of theirs.',
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Ordinary pair funding is connected. Staged Basic group games support recipient-specific pledges and named contributions for ordinary bids, plus physical shipments when Guild is outside the group; Guild-allied shipment rates and special transactions remain open.', evidence: ['game/coalition-escrow.ts', 'game/coalition-funding.ts', 'game/engine.ts'] },
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Ordinary pair funding is connected. Staged Basic group games support recipient-specific pledges and named contributions for ordinary bids and physical shipments, including the native Guild rate and its Karama response. Special transactions remain open.', evidence: ['game/coalition-escrow.ts', 'game/coalition-funding.ts', 'game/engine.ts', 'game/shipment-price.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Pair funding has normal table controls. Staged group bid and shipment controls project each player’s own outgoing and incoming pledges; group play is not selectable yet.', evidence: ['components/coalition-funding.tsx', 'components/game-table.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'Existing bots can participate using their own spice; group funding strategy is deferred.' },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Focused staged cases exercise multiple donors, an outside Guild, ordinary auctions, Emperor income, rejected splits and individual refunds. Full group play and deployed acceptance remain open.', evidence: ['tests/coalition-bid-funding.test.ts', 'tests/coalition-shipment-funding.test.ts', 'tests/coalition-escrow.test.ts', 'tests/coalition-funding-controls.test.tsx'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Focused staged cases exercise multiple donors, Guild and Emperor income, ordinary auctions, rate cancellation, rejected splits and individual refunds. Full group play and deployed acceptance remain open.', evidence: ['tests/coalition-guild-shipment.test.ts', 'tests/coalition-bid-funding.test.ts', 'tests/coalition-shipment-funding.test.ts', 'tests/coalition-escrow.test.ts', 'tests/coalition-funding-controls.test.tsx'] },
|
||||
],
|
||||
related: ['bidding', 'movement', 'faction-guild', 'card-karama'],
|
||||
},
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import type { FactionId } from './catalog';
|
||||
import { areCoalitionAllies, type CoalitionState } from './coalition-alliance';
|
||||
|
||||
/** Public group membership only; legacy pair and Guild self rates stay with
|
||||
* the caller's native rules. This grants no separate Guild transport action. */
|
||||
export function groupGuildShipmentRate(
|
||||
players: readonly {id:string; faction:FactionId; ally:string|null; allySinceTurn?:number}[],
|
||||
player: string, state?: CoalitionState,
|
||||
): boolean {
|
||||
const guild = players.find(seat => seat.faction === 'guild');
|
||||
return !!state && !!guild && guild.id !== player && areCoalitionAllies(players, state, player, guild.id);
|
||||
}
|
||||
|
||||
/** Ordinary reserve shipment; other transport powers have separate prices. */
|
||||
export function reserveShipmentCost(
|
||||
|
||||
Reference in new issue
Block a user