Support Fremen special Karama truth commitments
Verify and publish checkpoint container / container (push) Successful in 10m22s
Verify and publish checkpoint container / container (push) Successful in 10m22s
This commit is contained in:
1 parent
5079ab556f
commit
5b8d510470
14 files changed
+418
-18
No files matched your search
+43
-3
@@ -19002,7 +19002,8 @@ function karamaSpendingBlock(g: Game, p: Player, card: Card, purpose?: KullPurpo
|
||||
purpose === karamaUsePurpose(g, { kind: 'cancel', response: g.response }) ||
|
||||
required.effect === 'karamaHarkonnenSpecial' && purpose === 'special:harkonnen' ||
|
||||
required.effect === 'karamaEmperorSpecial' && purpose === 'special:emperor' ||
|
||||
required.effect === 'karamaAtreidesSpecial' && purpose === 'special:atreides') &&
|
||||
required.effect === 'karamaAtreidesSpecial' && purpose === 'special:atreides' ||
|
||||
required.effect === 'karamaFremenSpecial' && purpose === 'special:fremen') &&
|
||||
windowCardOpportunity(g, required.effect)?.source === required.source))
|
||||
return 'Retain this opportunity without a forbidden printed Karama cost.';
|
||||
const kullBlock = kullKaramaBlock(g, p, purpose);
|
||||
@@ -20494,7 +20495,7 @@ function spendKarama(g: Game, p: Player, card: Card, use: KaramaUse, kullOffered
|
||||
// still-open Harkonnen special promise before a foreign response can park
|
||||
// the shared reconciler.
|
||||
if (card.kind === 'special' && card.effect === 'karama' && auctionRows.some(row => {
|
||||
return (['karamaHarkonnenSpecial', 'karamaEmperorSpecial', 'karamaAtreidesSpecial'] as const).some(effect => {
|
||||
return (['karamaHarkonnenSpecial', 'karamaEmperorSpecial', 'karamaAtreidesSpecial', 'karamaFremenSpecial'] as const).some(effect => {
|
||||
const stage = row.windowCards?.[effect];
|
||||
return stage && !stage.closed && windowCardOpportunity(g, effect, stage.source)?.source === stage.source;
|
||||
});
|
||||
@@ -22385,9 +22386,28 @@ function quotedAmalReady(g: Game, actor: string): boolean {
|
||||
return !!windowCardOpportunity(g, 'amal') && !!g.phaseOpening && !g.phaseOpening.passed.includes(actor) &&
|
||||
!g.truthtrance && !g.pendingTreacheryDiscard;
|
||||
}
|
||||
/** The first detached worm witness has no foreign losses or decisions. */
|
||||
function emptyFremenSpecialWormTerritory(g: Game, id: string): boolean {
|
||||
return TERRITORIES.some(row => row.id === id && row.type === 'sand') &&
|
||||
g.players.every(player => at(player, id) === 0) &&
|
||||
!Object.entries(g.spice).some(([key, amount]) => amount > 0 && splitLocation(key).territory === id);
|
||||
}
|
||||
function* nativeWindowCardActions(g: Game, actor: string, effect: WindowCardEffect): Generator<Action> {
|
||||
if (!windowCardOpportunity(g, effect)) return;
|
||||
const player = getPlayer(g, actor);
|
||||
if (effect === 'karamaFremenSpecial') {
|
||||
if (player.faction !== 'fremen' || g.expansions.length || g.nexusCards || g.homeworlds || g.discoveryEnabled ||
|
||||
g.kullPreview || g.pendingKull || g.semutaPreview || g.semutaRecruitsPreview || g.semutaAdvancedOrdinaryPreview ||
|
||||
g.truthtrance || g.response || g.decision || g.phaseOpening || g.pendingKarama || g.pendingTreacheryDiscard ||
|
||||
g.pendingNullentropy || g.spiceWindow || g.spiceSequence || g.spiceResolution || g.nexus || g.summonedWorm || g.wormRides.length) return;
|
||||
for (const card of player.hand.filter(card => card.kind === 'special' && card.effect === 'karama'))
|
||||
for (const territory of TERRITORIES.filter(row => emptyFremenSpecialWormTerritory(g, row.id))) {
|
||||
const action: Action = { type: 'card', mode: 'special', card: card.id, territory: territory.id };
|
||||
try { if (prepareSpecialKaramaIntent(g, actor, action).kind === 'fremen') yield action; }
|
||||
catch (error) { if (!(error instanceof RuleError)) throw error; }
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (effect === 'karamaAtreidesSpecial') {
|
||||
if (player.faction !== 'atreides' || g.kullPreview || g.pendingKull || g.semutaPreview ||
|
||||
g.semutaRecruitsPreview || g.semutaAdvancedOrdinaryPreview || g.truthtrance || g.response ||
|
||||
@@ -23787,7 +23807,8 @@ function projectedTurnActionGuide(g: Game, player: Player): { event: string; act
|
||||
? 'karamaShipment' : held?.kind === 'special' && held.effect === 'karama' && action.mode === 'special'
|
||||
? player.faction === 'harkonnen' ? 'karamaHarkonnenSpecial'
|
||||
: player.faction === 'emperor' ? 'karamaEmperorSpecial'
|
||||
: player.faction === 'atreides' ? 'karamaAtreidesSpecial' : null
|
||||
: player.faction === 'atreides' ? 'karamaAtreidesSpecial'
|
||||
: player.faction === 'fremen' ? 'karamaFremenSpecial' : null
|
||||
: isWindowCardEffect(held?.effect) ? held.effect : null;
|
||||
const windows: WindowCardEffect[] = printedWindow ? [printedWindow]
|
||||
: action.type === 'decision' && action.karama === true ? ['karamaAuction']
|
||||
@@ -42767,7 +42788,26 @@ function resolveSpecialKaramaEffect(g: Game, p: Player, card: Card, intent: Spec
|
||||
);
|
||||
} else if (intent.kind === 'fremen') {
|
||||
const t = intent.territory;
|
||||
const opportunity = windowCardOpportunity(g, 'karamaFremenSpecial');
|
||||
const live = liveTurnActionPromises(g.turnActionPromises ?? [], p.id, g.turn);
|
||||
if (opportunity && card.kind === 'special' && card.effect === 'karama' && live.some(row =>
|
||||
row.windowCards?.karamaFremenSpecial?.source === opportunity.source && !row.windowCards.karamaFremenSpecial.closed)) {
|
||||
recordNativeTurnActionWindowCard(g, p.id, 'karamaFremenSpecial', opportunity.source);
|
||||
// With other owned promises, the first composition retains only an empty
|
||||
// appearance until foreign worm decisions can be jointly forecast. A
|
||||
// standalone fulfilled answer does not restrict the native destination.
|
||||
const other = liveTurnActionPromises(g.turnActionPromises ?? [], p.id, g.turn).length ||
|
||||
liveTravelPromises(g.travelPromises ?? [], p.id, g.turn).length ||
|
||||
liveShipmentPromises(g.shipmentPromises ?? [], p.id, g.turn).length || hasMovementPromises(g, p.id) ||
|
||||
hasBribePromises(g, p.id) || liveRevivalPromises(g.revivalPromises ?? [], p.id, g.turn).length ||
|
||||
liveLeaderRevivalPromises(g.leaderRevivalPromises ?? [], p.id, g.turn).length ||
|
||||
liveRevivalExpressionPromises(g.revivalExpressionPromises ?? [], p.id, g.turn).length ||
|
||||
liveGholaLeaderPromises(g.gholaLeaderPromises ?? [], p.id, g.turn).length;
|
||||
requireRule(!other || emptyFremenSpecialWormTerritory(g, t),
|
||||
'Choose empty sand without spice while other Truthtrance commitments remain.');
|
||||
}
|
||||
p.specialKaramaUsed = true;
|
||||
if (live.length) guardTurnActionPrintedCardCost(g, p, card);
|
||||
g.summonedWorm = {
|
||||
territory: t,
|
||||
event: crypto.randomUUID(),
|
||||
|
||||
Reference in new issue
Block a user