Prototype Tleilaxu special Karama Truthtrance
Verify and publish checkpoint container / container (push) Successful in 11m6s

This commit is contained in:
Quintine committed 2026-10-29 18:35:24 +11:00
1 parent 5b8d510470
commit cfccfe5acb
15 files changed
+391 -20

No files matched your search

+7 -3
View File
@@ -9,7 +9,7 @@ import { parseBattleActionClaim } from '@/game/mixed-battle-question';
import { battlePowerOf, nativeBattlePowerOpportunity, type BattlePowerClaim } from '@/game/battle-power-question';
import { battleCardLabel, voiceChoices } from '@/game/battle-cards';
import { nativeStormDialOpportunity } from '@/game/storm-dial-question';
import { karamaBattleCancelOpportunity, parseKaramaHarkonnenSpecialSource, parseKaramaEmperorSpecialSource, parseKaramaAtreidesSpecialSource, parseKaramaFremenSpecialSource, WINDOW_CARD_EFFECTS, windowCardOpportunity, type WindowCardEffect } from '@/game/window-card-play';
import { karamaBattleCancelOpportunity, parseKaramaHarkonnenSpecialSource, parseKaramaEmperorSpecialSource, parseKaramaAtreidesSpecialSource, parseKaramaFremenSpecialSource, parseKaramaTleilaxuSpecialSource, WINDOW_CARD_EFFECTS, windowCardOpportunity, type WindowCardEffect } from '@/game/window-card-play';
import { parseTurnActionLeaf, TURN_ACTION_MAX_DEPTH, TURN_ACTION_MAX_LEAVES, type TurnActionLeaf } from '@/game/turn-action-promises';
import { BattleClaimFields } from './battle-promises';
import { MixedActionFields, mixedActionInputError, type MixedActionConfig } from './mixed-action-fields';
@@ -59,6 +59,7 @@ const windowRoleNames: Record<WindowCardEffect, string> = { weather: 'Weather Co
karamaEmperorSpecial: 'Use Emperor’s special Karama during Revival',
karamaAtreidesSpecial: 'Use Atreides’ special Karama in this battle',
karamaFremenSpecial: 'Use Fremen’s special Karama during Spice Blow',
karamaTleilaxuSpecial: 'Use Tleilaxu’s special Karama during Revival',
thumper: 'Play Thumper before this turn’s first spice reveal', amal: 'Play Amal in this original phase opening' };
const battleCancelOpportunity = karamaBattleCancelOpportunity;
/** Harkonnen is public; do not inspect that player's hand or target's cards. */
@@ -73,6 +74,8 @@ const windowOpportunity = (game: GameView, target: string, effect: WindowCardEff
(!opportunity || parseKaramaAtreidesSpecialSource(opportunity.source)?.owner !== target)) return null;
if (effect === 'karamaFremenSpecial' &&
(!opportunity || parseKaramaFremenSpecialSource(opportunity.source)?.owner !== target)) return null;
if (effect === 'karamaTleilaxuSpecial' &&
(!opportunity || parseKaramaTleilaxuSpecialSource(opportunity.source)?.owner !== target)) return null;
return opportunity;
};
const config: MixedActionConfig<TurnActionLeaf> = {
@@ -278,11 +281,11 @@ const config: MixedActionConfig<TurnActionLeaf> = {
<select id={`${id}-window-effect`} value={WINDOW_CARD_EFFECTS.includes(value.effect) ? value.effect : ''}
disabled={disabled} aria-invalid={!WINDOW_CARD_EFFECTS.includes(value.effect)} onChange={event => {
const effect = event.currentTarget.value;
if (effect === 'weather' || effect === 'atomics' || effect === 'harvester' || effect === 'karamaAuction' || effect === 'karamaShipment' || effect === 'karamaBattleCancel' || effect === 'karamaHarkonnenSpecial' || effect === 'karamaEmperorSpecial' || effect === 'karamaAtreidesSpecial' || effect === 'karamaFremenSpecial' || effect === 'thumper' || effect === 'amal')
if (effect === 'weather' || effect === 'atomics' || effect === 'harvester' || effect === 'karamaAuction' || effect === 'karamaShipment' || effect === 'karamaBattleCancel' || effect === 'karamaHarkonnenSpecial' || effect === 'karamaEmperorSpecial' || effect === 'karamaAtreidesSpecial' || effect === 'karamaFremenSpecial' || effect === 'karamaTleilaxuSpecial' || effect === 'thumper' || effect === 'amal')
onChange({ kind: 'windowCardPlay', effect, source: windowOpportunity(game, target, effect)?.source ?? '' });
}}>
<option value="">Choose a printed role</option>
{WINDOW_CARD_EFFECTS.filter(effect => (effect !== 'karamaBattleCancel' && effect !== 'karamaHarkonnenSpecial' && effect !== 'karamaEmperorSpecial' && effect !== 'karamaAtreidesSpecial' && effect !== 'karamaFremenSpecial') || !!windowOpportunity(game, target, effect) || value.effect === effect)
{WINDOW_CARD_EFFECTS.filter(effect => (effect !== 'karamaBattleCancel' && effect !== 'karamaHarkonnenSpecial' && effect !== 'karamaEmperorSpecial' && effect !== 'karamaAtreidesSpecial' && effect !== 'karamaFremenSpecial' && effect !== 'karamaTleilaxuSpecial') || !!windowOpportunity(game, target, effect) || value.effect === effect)
.map(effect => <option key={effect} value={effect}>{windowRoleNames[effect]}</option>)}
</select>
<label htmlFor={`${id}-window-source`}>Original native window</label>
@@ -302,6 +305,7 @@ const config: MixedActionConfig<TurnActionLeaf> = {
: value.effect === 'karamaEmperorSpecial' ? `Turn ${currentWindow.turn} · Emperor special Karama during Revival`
: value.effect === 'karamaAtreidesSpecial' ? `Turn ${currentWindow.turn} · Atreides special Karama in this battle`
: value.effect === 'karamaFremenSpecial' ? `Turn ${currentWindow.turn} · Fremen special Karama during Spice Blow`
: value.effect === 'karamaTleilaxuSpecial' ? `Turn ${currentWindow.turn} · Tleilaxu special Karama during Revival`
: value.effect === 'karamaBattleCancel' ? `Turn ${currentWindow.turn} · ${game.response?.kind === 'voice' ? 'Voice' : 'Prescience'} declared by ${game.players.find(player => player.id === game.response?.owner)?.name ?? game.response?.owner} · ${game.combatLocations.find(location => location.id === game.battle?.territory)?.name ?? game.battle?.territory}`
: currentWindow.effect === 'karamaShipment' ? `Turn ${currentWindow.turn} · ${game.players.find(player => player.id === currentWindow.recipient)?.name ?? currentWindow.recipient}’s current native shipment price`
: `Turn ${currentWindow.turn} · Before this Storm movement`}</option>}
+23 -7
View File
@@ -29,12 +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' | 'karamaHarkonnenSpecial' | 'karamaEmperorSpecial' | 'karamaAtreidesSpecial' | 'karamaFremenSpecial'> =>
const isWindowEffect = (effect: unknown): effect is Exclude<WindowCardEffect, 'karamaAuction' | 'karamaShipment' | 'karamaBattleCancel' | 'karamaHarkonnenSpecial' | 'karamaEmperorSpecial' | 'karamaAtreidesSpecial' | 'karamaFremenSpecial' | 'karamaTleilaxuSpecial'> =>
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',
karamaHarkonnenSpecial: 'Harkonnen special Karama', karamaEmperorSpecial: 'Emperor special Karama', karamaAtreidesSpecial: 'Atreides special Karama', karamaFremenSpecial: 'Fremen special Karama', thumper: 'Thumper', amal: 'Amal' };
karamaHarkonnenSpecial: 'Harkonnen special Karama', karamaEmperorSpecial: 'Emperor special Karama', karamaAtreidesSpecial: 'Atreides special Karama', karamaFremenSpecial: 'Fremen special Karama', karamaTleilaxuSpecial: 'Tleilaxu 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));
@@ -141,6 +141,10 @@ export function TurnActionGuideButton(props: TurnActionProps) {
const fremenRoleNamed = !!windowPromise && !!currentFremenWindow &&
namesWindow(windowPromise.mixed, 'karamaFremenSpecial', currentFremenWindow.source);
const printedFremenAction = action.type === 'card' && action.mode === 'special' && fremenRoleNamed;
const currentTleilaxuWindow = windowCardOpportunity(game, 'karamaTleilaxuSpecial');
const tleilaxuRoleNamed = !!windowPromise && !!currentTleilaxuWindow &&
namesWindow(windowPromise.mixed, 'karamaTleilaxuSpecial', currentTleilaxuWindow.source);
const printedTleilaxuAction = action.type === 'card' && action.mode === 'special' && tleilaxuRoleNamed;
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);
@@ -152,7 +156,8 @@ export function TurnActionGuideButton(props: TurnActionProps) {
? 'karamaHarkonnenSpecial' : printedEmperorAction && heldCard?.effect === windowCardNativeEffect('karamaEmperorSpecial')
? 'karamaEmperorSpecial' : printedAtreidesAction && heldCard?.effect === windowCardNativeEffect('karamaAtreidesSpecial')
? 'karamaAtreidesSpecial' : printedFremenAction && heldCard?.effect === windowCardNativeEffect('karamaFremenSpecial')
? 'karamaFremenSpecial' : isWindowEffect(heldCard?.effect) ? heldCard.effect : null;
? 'karamaFremenSpecial' : printedTleilaxuAction && heldCard?.effect === windowCardNativeEffect('karamaTleilaxuSpecial')
? 'karamaTleilaxuSpecial' : 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({
@@ -230,6 +235,8 @@ export function TurnActionGuideButton(props: TurnActionProps) {
revival: current.revival, used: owner?.specialKaramaUsed });
};
const emperorRevivalSignature = printedEmperorAction ? emperorRevivalSource(game) : null;
const tleilaxuQuote = printedTleilaxuAction ? game.tleilaxuSpecialKarama : null;
const tleilaxuSignature = JSON.stringify(tleilaxuQuote);
const battleCancelControls = game.responseControls, battleCancelControlsSignature = JSON.stringify(battleCancelControls);
const battleCancelResponse = game.response, battleCancelResponseSignature = JSON.stringify(battleCancelResponse);
const thumperCards = game.thumperCards, thumperCardsSignature = JSON.stringify(thumperCards);
@@ -296,7 +303,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'] : windowEffect === 'karamaHarkonnenSpecial' ? ['mode', 'target', 'amount'] : windowEffect === 'karamaEmperorSpecial' ? ['mode', 'leader', 'amount', 'elite'] : windowEffect === 'karamaAtreidesSpecial' ? ['mode', 'target'] : windowEffect === 'karamaFremenSpecial' ? ['mode', 'territory'] : [])].includes(key)) &&
...(windowEffect === 'weather' ? ['amount'] : windowEffect === 'karamaAuction' || windowEffect === 'karamaBattleCancel' ? ['mode'] : windowEffect === 'karamaShipment' ? ['mode', 'target'] : windowEffect === 'karamaHarkonnenSpecial' ? ['mode', 'target', 'amount'] : windowEffect === 'karamaEmperorSpecial' ? ['mode', 'leader', 'amount', 'elite'] : windowEffect === 'karamaAtreidesSpecial' || windowEffect === 'karamaTleilaxuSpecial' ? ['mode', 'target'] : windowEffect === 'karamaFremenSpecial' ? ['mode', 'territory'] : [])].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 &&
@@ -346,6 +353,12 @@ export function TurnActionGuideButton(props: TurnActionProps) {
Object.entries(current.spice).every(([key, amount]) => !key.startsWith(`${fremenTerritory}:`) || amount <= 0) &&
current.players.every(player => Object.entries(player.forces).every(([key, amount]) =>
!key.startsWith(`${fremenTerritory}:`) || amount <= 0));
const nativeTleilaxuCard = windowEffect === 'karamaTleilaxuSpecial' && !!me && me.faction === 'tleilaxu' &&
me.specialKaramaUsed === false && phase === 4 && action.mode === 'special' && typeof action.target === 'string' &&
!!heldCard && !!tleilaxuQuote && current.tleilaxuSpecialKarama === tleilaxuQuote &&
JSON.stringify(current.tleilaxuSpecialKarama) === tleilaxuSignature && tleilaxuQuote.blocked === null &&
tleilaxuQuote.cards.includes(heldCard.id) && tleilaxuQuote.targets.some(target => target.player === action.target && target.blocked === null) &&
(!tleilaxuQuote.pending || current.decision?.kind === 'revivalStop' && current.decision.player === actor);
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);
@@ -363,6 +376,7 @@ export function TurnActionGuideButton(props: TurnActionProps) {
: windowEffect === 'karamaEmperorSpecial' ? nativeEmperorCard
: windowEffect === 'karamaAtreidesSpecial' ? nativeAtreidesCard
: windowEffect === 'karamaFremenSpecial' ? nativeFremenCard
: windowEffect === 'karamaTleilaxuSpecial' ? nativeTleilaxuCard
: 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);
@@ -409,7 +423,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 && !printedHarkonnenAction && !printedEmperorAction && !printedAtreidesAction && !printedFremenAction && quotedBoxCard &&
const genericCard = ownCard && action.type === 'card' && !auctionPurchaseAction && !printedShipmentAction && !printedBattleCancelAction && !printedHarkonnenAction && !printedEmperorAction && !printedAtreidesAction && !printedFremenAction && !printedTleilaxuAction && 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);
@@ -524,13 +538,14 @@ 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) && (!printedHarkonnenAction || printedWindow) && (!printedEmperorAction || printedWindow) && (!printedAtreidesAction || printedWindow) && (!sectorShipmentGuide || currentSectorShipment) &&
(!printedBattleCancelAction || printedWindow) && (!printedHarkonnenAction || printedWindow) && (!printedEmperorAction || printedWindow) && (!printedAtreidesAction || printedWindow) && (!printedTleilaxuAction || printedWindow) && (!sectorShipmentGuide || currentSectorShipment) &&
current.turnActionPromises.some(promise => promise.event === event && promise.player === actor && promise.turn === turn &&
!promise.fulfilled && !promise.released) &&
(!amalReadyAction || openingReady) &&
!!me && !me.autopilot && (!current.semutaReaction || nativeSemuta) && (!current.truthtrance || nativeSemuta) &&
(!current.response || nativeSemuta || windowEffect === 'karamaBattleCancel' && printedWindow) &&
(!current.decision || nativeSemuta || paidBox || invoice || saphoDecision || earlyWindowBox && ownInvoiceSource) &&
(!current.decision || nativeSemuta || paidBox || invoice || saphoDecision || earlyWindowBox && ownInvoiceSource ||
windowEffect === 'karamaTleilaxuSpecial' && printedWindow && tleilaxuQuote?.pending && current.decision.kind === 'revivalStop' && current.decision.player === actor) &&
(!current.phaseOpening || nativeSemuta || allowedOpeningAction) && (!current.automaticContinuationPending || nativeSemuta) && !current.roomControl?.paused && !current.roomControl?.closed;
};
const amount = action.forces && typeof action.forces === 'object'
@@ -561,6 +576,7 @@ export function TurnActionGuideButton(props: TurnActionProps) {
: action.type === 'card' && windowEffect === 'karamaEmperorSpecial' ? `Use Emperor’s special Karama to revive ${action.leader ? 'the selected leader' : `${amount} forces`} while honoring your whole answer`
: action.type === 'card' && windowEffect === 'karamaAtreidesSpecial' ? `Use Atreides’ special Karama to inspect ${recipient}’s battle plan while honoring your whole answer`
: action.type === 'card' && windowEffect === 'karamaFremenSpecial' ? `Use Fremen’s special Karama to summon Shai-Hulud in empty ${destination} while honoring your whole answer`
: action.type === 'card' && windowEffect === 'karamaTleilaxuSpecial' ? `Use Tleilaxu’s special Karama to prevent ${recipient}’s Revival 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'