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

+26 -6
View File
@@ -29,11 +29,12 @@ const publicBattleContext = (game: GameView) => game.battle && ({
preparation: game.battle.preparation,
traitorVoters: game.battle.traitorVoters, traitorSubmitted: game.battle.traitorSubmitted,
});
const isWindowEffect = (effect: unknown): effect is Exclude<WindowCardEffect, 'karamaAuction' | 'karamaShipment' | 'karamaBattleCancel'> =>
const isWindowEffect = (effect: unknown): effect is Exclude<WindowCardEffect, 'karamaAuction' | 'karamaShipment' | 'karamaBattleCancel' | 'karamaHarkonnenSpecial'> =>
effect === 'weather' || effect === 'atomics' || effect === 'harvester' || effect === 'thumper' || effect === 'amal';
const windowRoleNames: Record<WindowCardEffect, string> = { weather: 'Weather Control', atomics: 'Family Atomics', harvester: 'Harvester',
karamaAuction: 'printed Karama for this ordinary lot', karamaShipment: 'printed Karama for this shipment price',
karamaBattleCancel: 'printed Karama for this battle response', thumper: 'Thumper', amal: 'Amal' };
karamaBattleCancel: 'printed Karama for this battle response',
karamaHarkonnenSpecial: 'Harkonnen special Karama', thumper: 'Thumper', amal: 'Amal' };
const namesWindow = (mixed: MixedActionExpression<TurnActionLeaf>, effect: WindowCardEffect, source?: string): boolean =>
mixed.kind === 'action' ? mixed.action.kind === 'windowCardPlay' && mixed.action.effect === effect && (source === undefined || mixed.action.source === source)
: mixed.kind !== 'fact' && mixed.terms.some(term => namesWindow(term, effect, source));
@@ -124,6 +125,10 @@ export function TurnActionGuideButton(props: TurnActionProps) {
const battleCancelRoleNamed = !!windowPromise && !!currentBattleCancelWindow &&
namesWindow(windowPromise.mixed, 'karamaBattleCancel', currentBattleCancelWindow.source);
const printedBattleCancelAction = action.type === 'card' && action.mode === 'cancel' && battleCancelRoleNamed;
const currentHarkonnenWindow = windowCardOpportunity(game, 'karamaHarkonnenSpecial');
const harkonnenRoleNamed = !!windowPromise && !!currentHarkonnenWindow &&
namesWindow(windowPromise.mixed, 'karamaHarkonnenSpecial', currentHarkonnenWindow.source);
const printedHarkonnenAction = action.type === 'card' && action.mode === 'special' && harkonnenRoleNamed;
const printedInvoiceAction = action.type === 'decision' && action.karama === true && auctionRoleNamed;
const cardOwner = action.type === 'card' || printedInvoiceAction ? game.players.find(player => player.id === actor) : undefined;
const heldCard = cardOwner?.hand?.find(card => card.id === action.card);
@@ -131,7 +136,8 @@ export function TurnActionGuideButton(props: TurnActionProps) {
const windowEffect = (auctionPurchaseAction || printedInvoiceAction) && heldCard?.effect === windowCardNativeEffect('karamaAuction')
? 'karamaAuction' : printedShipmentAction && heldCard?.effect === windowCardNativeEffect('karamaShipment')
? 'karamaShipment' : printedBattleCancelAction && heldCard?.effect === windowCardNativeEffect('karamaBattleCancel')
? 'karamaBattleCancel' : isWindowEffect(heldCard?.effect) ? heldCard.effect : null;
? 'karamaBattleCancel' : printedHarkonnenAction && heldCard?.effect === windowCardNativeEffect('karamaHarkonnenSpecial')
? 'karamaHarkonnenSpecial' : isWindowEffect(heldCard?.effect) ? heldCard.effect : null;
const cardWindow = windowEffect ? windowCardOpportunity(game, windowEffect) : null;
const earlyWindowReadyAction = action.type === 'ready' && (phase === 0 || phase === 1);
const publicWindowState = (current: GameView) => JSON.stringify({
@@ -196,6 +202,11 @@ export function TurnActionGuideButton(props: TurnActionProps) {
const flightOwnerSignature = flightNeeded ? flightOwnerSource(game) : null;
const auctionCards = game.karamaAuctionCards, auctionCardsSignature = JSON.stringify(auctionCards);
const shipmentCards = game.karamaShipmentCards, shipmentCardsSignature = JSON.stringify(shipmentCards);
const harkonnenTargetSource = (current: GameView) => {
const target = current.players.find(player => player.id === action.target);
return JSON.stringify(target && { id: target.id, handCount: target.handCount });
};
const harkonnenTargetSignature = printedHarkonnenAction ? harkonnenTargetSource(game) : null;
const battleCancelControls = game.responseControls, battleCancelControlsSignature = JSON.stringify(battleCancelControls);
const battleCancelResponse = game.response, battleCancelResponseSignature = JSON.stringify(battleCancelResponse);
const thumperCards = game.thumperCards, thumperCardsSignature = JSON.stringify(thumperCards);
@@ -262,7 +273,7 @@ export function TurnActionGuideButton(props: TurnActionProps) {
? typeof action.leader === 'string' && (action.leader === 'kwisatz' ? ghola.kwisatz : ghola.leaders.some(leader => leader.id === action.leader))
: action.elite === undefined ? forceChoices.length === 1 : forceChoices.some(choice => choice.elite === action.elite));
const windowPacket = Object.keys(action).every(key => ['type', 'card', 'turnActionPromiseEvent', 'turnActionPromisePhase',
...(windowEffect === 'weather' ? ['amount'] : windowEffect === 'karamaAuction' || windowEffect === 'karamaBattleCancel' ? ['mode'] : windowEffect === 'karamaShipment' ? ['mode', 'target'] : [])].includes(key)) &&
...(windowEffect === 'weather' ? ['amount'] : windowEffect === 'karamaAuction' || windowEffect === 'karamaBattleCancel' ? ['mode'] : windowEffect === 'karamaShipment' ? ['mode', 'target'] : windowEffect === 'karamaHarkonnenSpecial' ? ['mode', 'target', 'amount'] : [])].includes(key)) &&
(windowEffect !== 'weather' || typeof action.amount === 'number' && Number.isSafeInteger(action.amount) && action.amount >= 0 && action.amount <= 10);
const printedWindowSource = ownPhysicalCard && !!heldCard && heldCard.kind === 'special' && !!windowEffect && !!cardWindow &&
heldCard.effect === windowCardNativeEffect(windowEffect) && !!windowPromise &&
@@ -282,6 +293,13 @@ export function TurnActionGuideButton(props: TurnActionProps) {
current.responseControls === battleCancelControls && JSON.stringify(current.responseControls) === battleCancelControlsSignature &&
current.responseControls?.cancelCards.includes(heldCard.id) &&
karamaBattleCancelOpportunity(current, actor, cardWindow?.source)?.source === cardWindow?.source;
const harkonnenTarget = current.players.find(player => player.id === action.target);
const nativeHarkonnenCard = windowEffect === 'karamaHarkonnenSpecial' && !!me && me.faction === 'harkonnen' &&
me.specialKaramaUsed === false && phase === 3 && action.mode === 'special' &&
typeof action.target === 'string' && action.target !== actor && !!harkonnenTarget &&
typeof harkonnenTarget.handCount === 'number' && Number.isSafeInteger(harkonnenTarget.handCount) && harkonnenTarget.handCount >= 1 &&
Number.isSafeInteger(action.amount) && action.amount === 1 &&
harkonnenTargetSource(current) === harkonnenTargetSignature;
const nativeThumperCard = windowEffect === 'thumper' && !!heldCard && phase === 1 && !current.spiceWindow && !current.nexus &&
Array.isArray(current.thumperCards) && current.thumperCards === thumperCards &&
JSON.stringify(current.thumperCards) === thumperCardsSignature && current.thumperCards.includes(heldCard.id);
@@ -295,6 +313,7 @@ export function TurnActionGuideButton(props: TurnActionProps) {
Number.isSafeInteger(me.handLimit) && Array.isArray(me.hand) && me.hand.length < me.handLimit
: windowEffect === 'karamaShipment' ? action.mode === 'shipment' && nativeShipmentCard
: windowEffect === 'karamaBattleCancel' ? action.mode === 'cancel' && nativeBattleCancelCard
: windowEffect === 'karamaHarkonnenSpecial' ? nativeHarkonnenCard
: windowEffect === 'thumper' ? action.mode === undefined && nativeThumperCard
: windowEffect === 'amal' ? action.mode === undefined && nativeAmalCard
: action.mode === undefined && ordinaryCardAvailability(current, heldCard.id, windowEffect === 'weather' ? { amount: action.amount } : undefined)?.available === true);
@@ -341,7 +360,7 @@ export function TurnActionGuideButton(props: TurnActionProps) {
!battle.revealed && [battle.attacker, battle.defender].includes(actor) && action.event === battle.event && action.event === residual.event &&
action.target === residual.target && action.target === (battle.attacker === actor ? battle.defender : battle.attacker) &&
Object.keys(action).every(key => ['type', 'card', 'event', 'target', 'turnActionPromiseEvent', 'turnActionPromisePhase'].includes(key));
const genericCard = ownCard && action.type === 'card' && !auctionPurchaseAction && !printedShipmentAction && !printedBattleCancelAction && quotedBoxCard &&
const genericCard = ownCard && action.type === 'card' && !auctionPurchaseAction && !printedShipmentAction && !printedBattleCancelAction && !printedHarkonnenAction && quotedBoxCard &&
(!boxRoleNamed || heldCard?.effect !== 'nullentropyBox' || printedBox) &&
(!shipmentPriceAction || (action.target ?? actor) === current.active) &&
!!heldCard && heldCard.effect !== 'ghola' && heldCard.effect !== 'hajr' && heldCard.effect !== 'distrans' && heldCard.effect !== 'juiceOfSapho' && heldCard.effect !== 'semutaDrug' && heldCard.effect !== 'ornithopter' && heldCard.effect !== 'residualPoison' && !isWindowEffect(heldCard.effect);
@@ -456,7 +475,7 @@ export function TurnActionGuideButton(props: TurnActionProps) {
Number.isSafeInteger(phase) && phase >= 0 && phase <= 8 && current.phase === phase && current.turn === turn && current.me === actor &&
current.turnActionGuide === guide && guide.event === event && guide.action === action &&
action.turnActionPromiseEvent === event && action.turnActionPromisePhase === phase && JSON.stringify(action) === signature && opportunity && movementPacket &&
(!printedBattleCancelAction || printedWindow) && (!sectorShipmentGuide || currentSectorShipment) &&
(!printedBattleCancelAction || printedWindow) && (!printedHarkonnenAction || printedWindow) && (!sectorShipmentGuide || currentSectorShipment) &&
current.turnActionPromises.some(promise => promise.event === event && promise.player === actor && promise.turn === turn &&
!promise.fulfilled && !promise.released) &&
(!amalReadyAction || openingReady) &&
@@ -489,6 +508,7 @@ export function TurnActionGuideButton(props: TurnActionProps) {
: action.type === 'card' && windowEffect === 'karamaAuction' ? 'Spend a printed Karama to purchase this ordinary lot while honoring your whole answer'
: action.type === 'card' && windowEffect === 'karamaShipment' ? `Spend a printed Karama to grant ${recipient} this native shipment price while honoring your whole answer`
: action.type === 'card' && windowEffect === 'karamaBattleCancel' ? `Spend a printed Karama to cancel this ${game.response?.kind === 'voice' ? 'Voice' : 'Prescience'} while honoring your whole answer`
: action.type === 'card' && windowEffect === 'karamaHarkonnenSpecial' ? `Use Harkonnen’s special Karama on ${recipient} while honoring your whole answer`
: action.type === 'card' && heldCard?.effect === 'nullentropyBox' ? 'Pay 2 spice to activate Nullentropy Box while honoring your whole answer'
: action.type === 'card' && heldCard?.effect === 'distrans' ? `Give ${distrans?.choices.find(choice => choice.recipient === action.target)?.cards.find(card => card.id === action.give)?.name ?? 'the quoted card'} to ${recipient} with Distrans while honoring your whole answer`
: action.type === 'card' && heldCard?.effect === 'juiceOfSapho' ? `Play Juice of Sapho: ${action.scope === 'battleAggressor'