Add opt-in CHOAM market skip for the current turn
Verify and publish checkpoint container / container (push) Successful in 10m42s
Verify and publish checkpoint container / container (push) Successful in 10m42s
This commit is contained in:
1 parent
685bd8bdfc
commit
825f903cce
12 files changed
+536
-2
No files matched your search
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user