Add opt-in CHOAM market skip for the current turn
Verify and publish checkpoint container / container (push) Successful in 10m42s

This commit is contained in:
Quintine committed 2026-10-29 07:08:53 +11:00
1 parent 685bd8bdfc
commit 825f903cce
12 files changed
+536 -2

No files matched your search

+69
View File
@@ -2385,6 +2385,8 @@ export type Game = {
card: Card;
} | null;
choamMarket?: ChoamMarket | null;
/** A human CHOAM may waive future markets for this turn, and can revoke it. */
choamMarketSkip?: { owner: string; turn: number };
biddingEnd?: BiddingEnd | null;
ecazPoisonIncome?: { player: string; turn: number; phase: number; amount: number; count: number }[];
pendingWinnerDiscards?: { event: string; turn: number; territory: string; player: string; cards: string[]; optional: string[]; signature: string } | null;
@@ -20549,6 +20551,41 @@ function finishHarkonnenExchange(
log(g, `${owner.name} returned ${decision.count} cards to ${target.name}${automatic ? ' automatically; every held card was required' : ''}.`,
{ faction: 'harkonnen', name: 'Harkonnen exchange' });
}
/** Explicit human consent, independent of every private hand. The first
* version excludes optional rules and live commitments needing a combined
* completion planner; no future market is forfeited by a hidden-card test. */
function choamMarketSkipProfile(g: Game, p: Player): boolean {
return g.status === 'playing' && !g.advanced && g.expansions.length === 1 &&
g.expansions[0] === 'choam' && p.faction === 'choam' && !p.bot && !p.autopilot &&
g.players.every(seat => ['choam', 'atreides', 'beneGesserit', 'emperor', 'fremen', 'guild', 'harkonnen'].includes(seat.faction)) &&
!g.ecazTreachery && !g.techTokens && !g.strongholdCards && !g.leaderSkills && !g.homeworlds &&
!g.nexusCards && !g.discoveryEnabled && !g.sandtrout && !g.coalitionState;
}
function choamMarketSkipCommitments(g: Game): boolean {
// Treat the entire turn consistently after any recorded answer, including
// already completed answers. Timing must not reveal private fulfillment.
const recorded = (rows: readonly {turn: number}[] | undefined) =>
!!rows?.some(row => row.turn === g.turn);
return !!g.truthHistory?.some(row => row.turn === g.turn) ||
recorded(g.travelPromises) || recorded(g.turnActionPromises) || recorded(g.shipmentPromises) ||
recorded(g.bidPromises) || recorded(g.phaseBidPromises) || recorded(g.revivalPromises) ||
recorded(g.leaderRevivalPromises) || recorded(g.revivalExpressionPromises) || recorded(g.bribePromises) ||
recorded(g.bribeExpressionPromises) || recorded(g.pledgePromises) || recorded(g.alliancePromises) ||
recorded(g.allianceExpressionPromises) || recorded(g.movementPromises) || recorded(g.movementExpressionPromises) ||
recorded(g.futureBattleLeaderPromises) ||
!!g.deals?.some(row => row.status === 'active' && row.paymentStatus === 'pending');
}
function choamMarketSkipAvailable(g: Game, p: Player): boolean {
return choamMarketSkipProfile(g, p) && g.decision?.kind === 'choamMarket' &&
g.decision.player === p.id && g.choamMarket?.owner === p.id && !g.biddingEnd &&
!g.response && !g.truthtrance && !g.phaseOpening && !g.pendingNullentropy &&
!g.pendingRicheseGift && !g.pendingKarama && !g.pendingKull && !g.pendingExchange &&
!g.pendingTreacheryDiscard && !g.choamMarket.sale && !g.choamMarket.trade &&
!choamMarketSkipCommitments(g);
}
function choamMarketSkipActive(g: Game, p: Player): boolean {
return choamMarketSkipProfile(g, p) && g.choamMarketSkip?.owner === p.id && g.choamMarketSkip.turn === g.turn;
}
function finishAutomaticDecision(g: Game): boolean {
if (g.pendingEmperorAuctionGift) return false;
if (g.biddingEnd && biddingEndQuiet(g)) return finishBiddingEnd(g);
@@ -20568,6 +20605,15 @@ function finishAutomaticDecision(g: Game): boolean {
return true;
}
const market = g.choamMarket;
if (decision?.kind === 'choamMarket' && market?.owner === decision.player) {
const owner = getPlayer(g, market.owner);
if (choamMarketSkipActive(g, owner) && choamMarketSkipAvailable(g, owner)) {
log(g, `${owner.name} finished the market using their chosen skip for this turn.`,
{ faction: 'choam', name: 'Market complete' });
finishChoamMarket(g);
return true;
}
}
if (
decision?.kind === 'ecazAmbassador' &&
g.pendingAmbassador?.stage === 'ship' &&
@@ -29774,6 +29820,14 @@ function decideChoamMarket(g: Game, decision: Extract<Decision, {kind: 'choamMar
const owner = getPlayer(g, market.owner);
if (decision.kind === 'choamMarket') {
if (action.done === true) {
if (action.skipMarketsThisTurn !== undefined) {
requireRule(action.skipMarketsThisTurn === true &&
Object.keys(action).sort().join(',') === 'done,skipMarketsThisTurn,type' &&
choamMarketSkipAvailable({...g, decision}, owner),
'The opt-in market skip is unavailable during this opportunity.');
g.choamMarketSkip = {owner: owner.id, turn: g.turn};
log(g, `${owner.name} chose to skip remaining market opportunities this turn.`);
}
finishChoamMarket(g);
} else if (action.mode === 'sell') {
const sale = quoteSale(
@@ -42237,6 +42291,18 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
ambassadorRevivalIncomeIntegrity(state);
ecazCollectionIntegrity(state);
ecazAllianceIntegrity(state);
if (action?.type === 'cancelChoamMarketSkip') {
const player = getPlayer(state, id);
requireRule(Object.keys(action).sort().join(',') === 'type' && choamMarketSkipActive(state, player) &&
!state.response && !state.truthtrance && !state.phaseOpening && !state.pendingKarama &&
!state.pendingKull && !state.pendingTreacheryDiscard && !state.pendingNullentropy &&
(!state.decision || state.decision.kind === 'choamMarket' && state.decision.player === id),
'Cancel your current market skip when no other interaction is pending.');
const g = structuredClone(state);
delete g.choamMarketSkip;
log(g, `${player.name} canceled their market skip; later markets this turn remain available.`);
return g;
}
if (action?.type === 'dealPay') {
requireRule(state.status === 'playing' && Object.keys(action).sort().join(',') === 'deal,type' &&
typeof action.deal === 'string', 'Choose your pending deal payment.');
@@ -42349,6 +42415,7 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
const g = structuredClone(state);
const owner = getPlayer(g, id);
if ((owner.autopilot ?? null) === action.difficulty) return g;
if (action.difficulty !== null && g.choamMarketSkip?.owner === id) delete g.choamMarketSkip;
if (action.difficulty === null) {
delete owner.autopilot;
log(g, `${owner.name} took back control from AI.`);
@@ -48475,6 +48542,8 @@ export function viewGame(state: Game, id: string, now?: number) {
),
}
: null,
choamMarketSkipAvailable: choamMarketSkipAvailable(g, me),
choamMarketSkipActive: choamMarketSkipActive(g, me),
choamMarket: g.choamMarket
? {
owner: g.choamMarket.owner,