Support Harkonnen special Karama truth commitments
Verify and publish checkpoint container / container (push) Successful in 10m17s

This commit is contained in:
Quintine committed 2026-10-29 18:08:04 +11:00
1 parent 15a4f3038a
commit b448144c52
15 files changed
+449 -35

No files matched your search

+45 -5
View File
@@ -18999,7 +18999,8 @@ function karamaSpendingBlock(g: Game, p: Player, card: Card, purpose?: KullPurpo
(required.effect === 'karamaAuction' && purpose === 'auction-free-purchase' ||
required.effect === 'karamaShipment' && purpose === 'shipment-price' ||
required.effect === 'karamaBattleCancel' && !!g.response &&
purpose === karamaUsePurpose(g, { kind: 'cancel', response: g.response })) &&
purpose === karamaUsePurpose(g, { kind: 'cancel', response: g.response }) ||
required.effect === 'karamaHarkonnenSpecial' && purpose === 'special:harkonnen') &&
windowCardOpportunity(g, required.effect)?.source === required.source))
return 'Retain this opportunity without a forbidden printed Karama cost.';
const kullBlock = kullKaramaBlock(g, p, purpose);
@@ -20487,6 +20488,13 @@ function spendKarama(g: Game, p: Player, card: Card, use: KaramaUse, kullOffered
'Retain your promised printed Karama cancellation or another whole-answer completion.');
}
const used = discard(g, p, card.id);
// Spending this physical Karama for a different role must preserve any
// still-open Harkonnen special promise before a foreign response can park
// the shared reconciler.
if (card.kind === 'special' && card.effect === 'karama' && auctionRows.some(row => {
const stage = row.windowCards?.karamaHarkonnenSpecial;
return stage && !stage.closed && windowCardOpportunity(g, 'karamaHarkonnenSpecial', stage.source)?.source === stage.source;
})) guardTurnActionPrintedCardCost(g, p, used);
if (auctionWindow && auctionRows.some(row => row.windowCards?.karamaAuction?.source === auctionWindow))
guardTurnActionAuctionClosure(g, p.id, auctionRows);
if (shipmentWindow && use.kind === 'shipment') {
@@ -21592,7 +21600,7 @@ function validatePlanetologistMove(g: Game, p: Player, move: Pick<MovementOrder,
skill.leader === planetologistLeader(g, p.id),
'This Planetologist movement no longer has its living, uncaptured skilled leader.');
requireRule(planetologistMovementModeSupported(g),
'Planetologist movement combined with expansion factions is still being implemented.');
'Planetologist movement is not yet supported in this game configuration.');
requireRule(!(move.ornithopterEvent && move.discoveryFlight),
'Choose one Ornithopter flight source for this Planetologist movement.');
// Selected composition: the concealed marker moves as one force for either
@@ -22376,6 +22384,22 @@ function quotedAmalReady(g: Game, actor: string): boolean {
function* nativeWindowCardActions(g: Game, actor: string, effect: WindowCardEffect): Generator<Action> {
if (!windowCardOpportunity(g, effect)) return;
const player = getPlayer(g, actor);
if (effect === 'karamaHarkonnenSpecial') {
const opportunity = windowCardOpportunity(g, effect);
// The public source identifies the unique native owner; eligibility and
// each target's hand size are checked here, without inspecting its faces.
if (player.faction !== 'harkonnen' || !opportunity || g.kullPreview || g.pendingKull ||
g.semutaPreview || g.semutaRecruitsPreview || g.semutaAdvancedOrdinaryPreview ||
g.truthtrance || g.response || g.decision || g.phaseOpening || g.pendingKarama ||
g.pendingTreacheryDiscard || g.pendingNullentropy) return;
for (const card of player.hand.filter(card => card.kind === 'special' && card.effect === 'karama'))
for (const target of g.players.filter(candidate => candidate.id !== actor && candidate.hand.length > 0)) {
const action: Action = { type: 'card', mode: 'special', card: card.id, target: target.id, amount: 1 };
try { if (prepareSpecialKaramaIntent(g, actor, action).kind === 'harkonnen') yield action; }
catch (error) { if (!(error instanceof RuleError)) throw error; }
}
return;
}
if (effect === 'karamaBattleCancel') {
for (const card of quotedBattleCancelKaramaCards(g, player)) yield { type: 'card', card, mode: 'cancel' };
return;
@@ -22459,7 +22483,18 @@ function* turnActionWindowCardSeeds(source: Game, actor: string, requirements: r
}
for (const action of nativeWindowCardActions(state, actor, required.effect)) {
try {
const next = applyActionInner(state, actor, action, 'movementPreparation');
let next: Game;
if (required.effect === 'karamaHarkonnenSpecial') {
// An opaque legal witness returns the unseen drawn cards to the
// target. It spends the actual card but never samples, copies or
// derives a benefit from another player's private hand.
next = structuredClone(state);
const intent = prepareSpecialKaramaIntent(next, actor, action);
requireRule(intent.kind === 'harkonnen', 'Retain the original Harkonnen special opportunity.');
const owner = getPlayer(next, actor);
discard(next, owner, intent.card);
owner.specialKaramaUsed = true;
} else next = applyActionInner(state, actor, action, 'movementPreparation');
if (required.effect === 'karamaAuction') {
const before = getPlayer(state, actor), after = getPlayer(next, actor);
const known = before.faction === 'atreides' && state.auction?.peekKnown
@@ -34856,11 +34891,11 @@ function currentBattleResolutionQuote(g: Game, canceledVoter?: string) {
if (quote.sukGraduate) requireRule(
ordinaryLeaderSkillModeSupported(g) &&
(nativeExpansionLeaderSkillsProfile(g) || g.players.every((p) => faction(p.faction).expansion === 'base')),
'Suk Graduate rescue with expansion factions or other optional modules is still being implemented.');
'Suk Graduate rescue is not yet supported in this game configuration.');
if (quote.rihani || quote.sandmaster) requireRule(
ordinaryLeaderSkillModeSupported(g) &&
(nativeExpansionLeaderSkillsProfile(g) || g.players.every((p) => faction(p.faction).expansion === 'base')),
'These Leader Skill victory effects with expansion factions or other optional modules are still being integrated.');
'These Leader Skill victory effects are not yet supported in this game configuration.');
const collection = b.smugglerCollection;
const collects = !!collection && !quote.effects?.stunned &&
!(collection.player === b.attacker ? quote.leaderDeaths.attacker : quote.leaderDeaths.defender);
@@ -42787,7 +42822,12 @@ function resolveSpecialKaramaEffect(g: Game, p: Player, card: Card, intent: Spec
} else if (intent.kind === 'harkonnen') {
const target = getPlayer(g, intent.target),
count = intent.amount;
const opportunity = windowCardOpportunity(g, 'karamaHarkonnenSpecial');
if (opportunity && card.kind === 'special' && card.effect === 'karama')
recordNativeTurnActionWindowCard(g, p.id, 'karamaHarkonnenSpecial', opportunity.source);
p.specialKaramaUsed = true;
if (liveTurnActionPromises(g.turnActionPromises ?? [], p.id, g.turn).length)
guardTurnActionPrintedCardCost(g, p, card);
const taken = shuffle(target.hand).slice(0, count);
target.hand = target.hand.filter((c) => !taken.some((t) => t.id === c.id));
p.hand.push(...taken);