Open Tleilaxu special Karama throughout Revival

Permit native Advanced quiet any-player declarations alongside the original target-bound pending request. Preserve completed returns and charges. Adopt explicit provisional Ghola inclusion during the remaining Revival and phase expiry; keep printed Emperor special immunity and the existing low-Homeworld guard. Connect persistent controls and minimal legal bots through the shared quote.

Validation: 14 related files148/148, types/lint/build, six actual native control/engine/GameTable SSR journeys and four existing-profile continuations. Private-driver-only feedback reuses passed source-identical checks. Native111/113-action setup, paid shipment and battle sources earn originals. No saved-room/D1/restart/deployment operation; full modes and browser/deployed acceptance remain open.
This commit is contained in:
Quintine committed 2026-10-24 19:30:21 +11:00
1 parent 6ea691255a
commit 1a505525a8
18 files changed
+977 -59

No files matched your search

+13 -2
View File
@@ -2302,12 +2302,15 @@ function policyActions(g: GameView): Action[] {
target.id !== me.ally &&
(target.tanks >= [12, 8, 5, 3][level] ||
(level > 0 && d.revival === 'leader' && noLeader));
if (specialCard && shouldStop && !g.revival.specialKaramaBlock)
const offer = g.tleilaxuSpecialKarama;
const card = offer?.cards[0];
const allowed = offer?.targets.some(choice => choice.player === target.id && !choice.blocked);
if (card && offer && !offer.blocked && allowed && shouldStop)
return [
{
type: 'card',
mode: 'special',
card: specialCard.id,
card,
target: target.id,
},
];
@@ -3129,6 +3132,14 @@ function policyActions(g: GameView): Action[] {
: [{ type: 'passBid' }];
}
if (g.phase === 4) {
// Reuse the existing public-Tanks preference, with shared current permission.
const prevention = g.tleilaxuSpecialKarama;
if (prevention && !prevention.blocked && !prevention.pending && prevention.cards.length) {
const target = g.players.find(player => player.id !== me.id && player.id !== me.ally &&
player.tanks >= [12, 8, 5, 3][level] &&
prevention.targets.some(choice => choice.player === player.id && !choice.blocked));
if (target) return [{ type: 'card', mode: 'special', card: prevention.cards[0], target: target.id }];
}
if (g.revival.prevented)
return g.ready.includes(me.id) ? [] : [{ type: 'ready' }];
const actions: Action[] = [];
+8
View File
@@ -3,6 +3,7 @@ import { TERRITORIES } from './board';
import { harvesterAvailable, type Card } from './cards';
import type { FactionId } from './catalog';
import { choamSaleGholaTiming } from './choam-market-ghola';
import { revivalPrevented } from './revival';
export type OrdinaryCardEffect =
| 'weather'
@@ -26,6 +27,8 @@ export type OrdinaryCardPlayer = {
export type OrdinaryCardState = {
status: 'lobby' | 'setup' | 'playing' | 'finished';
phase: number;
turn?: number;
revivalPrevention?: { player: string; turn: number } | null;
active: string | null;
me: string;
nexusGuildCunning?: { active: { hajrAvailable: boolean } | null } | null;
@@ -102,6 +105,11 @@ export function ordinaryCardAvailability(
return blocked('Select a special card in your hand.');
switch (card.effect as OrdinaryCardEffect) {
case 'ghola':
if (revivalPrevented({ phase: state.phase, turn: state.turn ?? 0,
revivalPrevention: state.revivalPrevention ?? undefined }, own.id))
return blocked('Tleilaxu prevented this player’s revival, including Ghola, for the remaining Revival Phase.');
break;
case 'hajr':
if (
state.phase !== 5 ||
+40 -20
View File
@@ -11,6 +11,7 @@ import { classicDiscoveryLeaderSkillsProfile, classicDiscoveryNexusProfile, home
import { advancedMoritaniLeaderSkillsProfile, selectedDeckLeaderSkillsProfile, homeworldLeaderSkillsProfile, independentTreacheryLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, guildBetrayalNexusLeaderSkillsProfile, mixedE1E2NexusLeaderSkillsProfile, mixedE3NexusLeaderSkillsProfile, nativeDiscoveryLeaderSkillsProfile, nativeEcazLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, ordinaryLeaderSkillModeSupported, pairedE3LeaderSkillsProfile, pairedE3NexusLeaderSkillsProfile, pairedNexusLeaderSkillsProfile, singleE1E2NexusLeaderSkillsProfile, standaloneE3NexusLeaderSkillsProfile, strongholdLeaderSkillsProfile } from './leader-skill-profile';
import { bribeTimingBlock, maximumBribe, type BribeOptions } from './bribe-options';
import { quoteAtreidesFullPlan } from './atreides-full-plan';
import { quoteTleilaxuSpecialKarama } from './tleilaxu-special-karama';
import { quoteSpicePlacement, stormExposesTerritory, stormSectorAfter, wormConsumesForces } from './disaster-rules';
import { isStormCardDistance, type StormCardComponent } from './storm-cards';
import { createLeaderSkills, validateLeaderSkills, dealLeaderSkills, chooseLeaderSkill, returnDeadLeaderSkills, offerRevivedLeaderSkill, drawRevivedLeaderSkills, declineRevivedLeaderSkill, LeaderSkillError, type LeaderSkillsState, type LeaderSkillsView } from './leader-skills';
@@ -23289,6 +23290,8 @@ function playMarketGhola(g: Game, p: Player, action: Action) {
stageOrdinaryCardDiscard(g, p, used);
}
function applyGholaEffect(g: Game, p: Player, action: Action, cardId?: string) {
requireRule(!revivalPrevented(g, p.id),
'Tleilaxu prevented this player’s revival, including Ghola, for the remaining Revival Phase.');
if (action.leader === 'kwisatz') {
requireRule(
g.advanced && p.faction === 'atreides' && p.kwisatz?.dead,
@@ -30811,6 +30814,27 @@ function atreidesFullPlanView(g: Game, p: Player) {
return { ...quote, cards,
blocked: quote.blocked ?? (cards.length ? null : 'A spendable Karama card is required.') };
}
function tleilaxuSpecialKaramaQuote(g: Game, p: Player) {
const ownStop = g.decision?.kind === 'revivalStop' && g.decision.player === p.id &&
g.pendingRevival?.player === g.decision.recipient;
return quoteTleilaxuSpecialKarama({
status: g.status, phase: g.phase, advanced: g.advanced, owner: p,
players: g.players,
cards: p.hand.filter(card => canUseAsKaramaRole(g, p, card) &&
!karamaSpendingBlock(g, p, card, kullSpecialPurpose(p.faction))).map(card => card.id),
pendingPlayer: g.pendingRevival?.player ?? null,
blocked: g.truthtrance || g.response || (g.decision && !ownStop) ||
(g.pendingRevival && !ownStop) || g.phaseOpening || g.pendingKarama ||
g.choamMarket || g.pendingTreacheryDiscard || g.pendingNullentropy || g.pendingKull
? 'Resolve the current response, decision or card effect before declaring revival prevention.' : null,
targetBlocks: Object.fromEntries(g.players.map(player =>
[player.id, homeworldRevivalKaramaBlock(g, player.id)])),
});
}
function tleilaxuSpecialKaramaView(g: Game, p: Player) {
return g.status === 'playing' && g.advanced && p.faction === 'tleilaxu' && g.phase === 4
? tleilaxuSpecialKaramaQuote(g, p) : null;
}
function ecazSpecialKaramaContext(g: Game, p: Player) {
const b = g.battle;
return { status: g.status, phase: g.phase, advanced: g.advanced, owner: p,
@@ -30867,7 +30891,7 @@ export type SpecialKaramaIntent = {
| { kind: 'choam'; cards: string[] }
| { kind: 'richese'; acquire: string; nexusEvent?: string }
| { kind: 'ixians'; route: string[]; collect: boolean; sandmasterPiles?: string[] }
| { kind: 'tleilaxu'; target: string; revival: PendingRevival }
| { kind: 'tleilaxu'; target: string; revival: PendingRevival | null }
| { kind: 'fremen'; territory: string }
| {
kind: 'atreides';
@@ -31037,29 +31061,21 @@ export function prepareSpecialKaramaIntent(
...(action.sandmasterPiles !== undefined ? { sandmasterPiles } : {}),
};
} else if (p.faction === 'tleilaxu') {
const quote = tleilaxuSpecialKaramaQuote(g, p);
requireRule(!quote.blocked, quote.blocked ?? 'Revival prevention is unavailable.');
const target = action.target === undefined && g.pendingRevival
? g.pendingRevival.player : stringField(action.target);
const selection = quote.targets.find(candidate => candidate.player === target);
requireRule(
g.phase === 4 &&
!g.response &&
g.decision?.kind === 'revivalStop' &&
g.decision.player === p.id &&
g.pendingRevival,
'Wait for a normal revival declaration before preventing it.',
);
const target = g.pendingRevival.player;
const homeworldBlock = homeworldRevivalKaramaBlock(g, target);
requireRule(
!homeworldBlock,
homeworldBlock ?? '',
);
requireRule(
action.target === undefined || action.target === target,
'This decision applies to the faction currently reviving.',
selection,
g.pendingRevival ? 'This decision applies to the faction currently reviving.' : 'Choose a player to prevent during Revival.',
);
requireRule(!selection.blocked, selection.blocked ?? 'This target is unavailable.');
return {
...base,
kind: 'tleilaxu',
target,
revival: structuredClone(g.pendingRevival),
revival: g.pendingRevival ? structuredClone(g.pendingRevival) : null,
};
} else if (p.faction === 'fremen') {
requireRule(
@@ -31265,7 +31281,8 @@ export function executeSpecialKaramaIntent(
if (intent.kind === 'tleilaxu' && prepared.kind === 'tleilaxu')
requireRule(
intent.target === prepared.target &&
sameDeclaration(intent.revival, prepared.revival),
(intent.revival === null ? prepared.revival === null :
prepared.revival !== null && sameDeclaration(intent.revival, prepared.revival)),
'The original revival declaration is no longer pending.',
);
if (intent.kind === 'guild' && prepared.kind === 'guild')
@@ -31409,7 +31426,7 @@ function resolveSpecialKaramaEffect(g: Game, p: Player, card: Card, intent: Spec
if (g.revivalRequests) delete g.revivalRequests[target];
log(
g,
`${p.name} used special Karama to prevent ${getPlayer(g, target).name}’s normal force and leader revivals for this turn.`,
`${p.name} used special Karama to prevent ${getPlayer(g, target).name}’s force and leader revivals, including Ghola, for the remaining Revival Phase. Completed returns stay unchanged; the Emperor’s special Karama remains independent.`,
);
} else if (intent.kind === 'fremen') {
const t = intent.territory;
@@ -36003,6 +36020,8 @@ function applyActionInner(
{
status: g.status,
phase: g.phase,
turn: g.turn,
revivalPrevention: g.revivalPrevention,
active: g.active,
me: id,
nexusGuildCunning: projectedNexusGuildCunning(g,id),
@@ -37176,6 +37195,7 @@ export function viewGame(state: Game, id: string) {
: null,
richeseSpecialKarama: richeseSpecialKaramaView(g, me),
atreidesFullPlan: atreidesFullPlanView(g, me),
tleilaxuSpecialKarama: tleilaxuSpecialKaramaView(g, me),
...(me.faction === 'ecaz' && g.advanced && g.battle
? { ecazSpecialKarama: ecazSpecialKaramaView(g, me) } : {}),
...(g.moritaniSpecialKarama?.source.owner === me.id && g.moritaniSpecialKarama.stage === 'offer'
+13 -5
View File
@@ -66,7 +66,7 @@ export const HELP = {
ixForces:
'Suboids always dial one-half strength and cannot receive spice support. Cyborgs count two in basic play; in advanced play they need one spice each for strength two and otherwise count one. After losses, surviving suboids may replace cyborgs lost in that battle, one for one.',
tleilaxuSpecial:
'Once per game, Tleilaxu can spend a real Karama at a normal revival declaration to prevent that faction’s normal force and leader revivals for this turn. The attempt costs no spice and returns no pieces. Ghola treachery and Emperor special Karama remain separate; full timing and exception audits are pending.',
'Once per game during Revival, Tleilaxu may spend a real Karama to prevent any one player’s remaining force and leader revivals, before a request or at its normal revival decision. Completed returns and payments stay unchanged. Provisional scope includes Ghola during this Revival and expires with the phase; Emperor special Karama remains protected. Low-Homeworld overlap and wider interactions remain unfinished.',
techTokens:
'Tech tokens are public industries. Their income is paid once at the end of the matching phase, in an amount equal to the owner’s token count. A battle winner takes one from the loser. All three must belong to one player to count as a stronghold.',
summonedWorm:
@@ -2665,6 +2665,7 @@ export const RULE_TOPICS: RuleTopic[] = [
coverage: 'Partial',
summary: 'Return forces or a leader from the tanks using the card.',
steps: [
'During Revival, Tleilaxu special Karama prevention also blocks this card for the selected player. This inclusion is provisional and ends when Revival ends; completed returns stay unchanged. Ghola remains available afterward, subject to its other timing and target rules.',
'Choose a dead leader for leader revival, or leave the leader choice empty to return up to five forces. A card pays for one of these choices; it cannot revive both a leader and the Kwisatz Haderach.',
'A binding battle answer or prescience commitment may require this preparation. The private guide identifies a Ghola play that can fulfill the answer, without spending or revealing the card until you choose to play it. Resolve any revival-income response before using that income to support a battle.',
'A Basic ground-movement promise can also require Ghola force preparation. The guide returns physical Tanks to reserves, then preserves an unused affordable shipment and one later matching ground move. Ghola itself does not fulfill movement or consume normal revival allowances. A known discarded Ghola may be recovered with the paid Box and played again.',
@@ -2714,7 +2715,7 @@ export const RULE_TOPICS: RuleTopic[] = [
],
},
],
related: ['revival'],
related: ['revival', 'special-karama'],
},
{
id: 'atreides-full-plan',
@@ -3793,13 +3794,20 @@ export const RULE_TOPICS: RuleTopic[] = [
'The stopped shipment uses the shipper’s shipment opportunity while retaining forces, spice, allied credit and any shipment-rate card; ordinary movement remains available. These settlement details remain provisional pending primary-source clarification. The Guild decision appears regardless of its hidden hand.',
'Fremen may call a worm in a sand territory during Spice Blow and Nexus. It destroys spice and unprotected forces immediately. Ally protection and Fremen survival have separate responses; a second Karama may cause Fremen to be devoured. The special summon itself consumes no spice-deck card.',
'Resume any interrupted spice draw, fresh-blow window or worm response after the summoned worm resolves. Its Nexus occurs at the end of the blow, followed by eligible Fremen rides. During an open Nexus, the new ride joins the existing queue. Destroyed fresh spice cannot later be doubled by Harvester.',
'Tleilaxu receive a decision before another faction’s normal revival completes, whether or not they hold Karama. Decline to continue the revival, or spend a real Karama once per game to prevent that faction’s normal force and leader revivals for the current turn. A stopped attempt spends no spice, returns no pieces and consumes no elite or extra-revival quota.',
'This includes negotiated leaders, KH and Emperor-funded allied extra forces. The Emperor remains the payer when an extra revival is allowed. Hidden leader identities and prices are not added to the public stop decision. Ghola treachery and the Emperor’s special Karama currently remain separate from the normal-revival restriction.',
'The printed Tleilaxu power is brief. Its full-turn scope, precise timing and card/special-power exceptions remain provisional pending the complete primary-source audit. These choices are tested implementation behavior, not certification of the full expansion rules.',
'During a quiet Revival boundary, Tleilaxu may spend its once-per-game special Karama to select any one player, including itself, an ally or a ready faction. It also retains the original decision before another faction’s normal revival completes, independently of card holdings. Declining that decision continues the original return and preserves later quiet use. A pending decision can select only its original recipient.',
'Prevention stops further force and leader returns in this Revival, including negotiated leaders, KH and Emperor-funded extra forces. It does not undo completed returns, payments or used allowances. A stopped pending request spends no spice or revival quota. Hidden leader identities and negotiated prices remain outside the public decision. Emperor’s special Karama is immune.',
'Provisional scope includes Ghola Treachery returns during the affected Revival and ends when that phase ends; Ghola is available afterward. This composes the revised prevention wording with Ghola’s free-return rule, rather than quoting a combined publisher ruling. Low-population Homeworld overlap retains its existing guard. Full expansion and combination acceptance remains open.',
'CHOAM cash-in, Richese acquisition and Ixian HMS relocation also have connected special-use paths. Ecaz’s supplied p30 unarmed disc difference and Moritani’s supplied p35 post-loss winner-card disposal now have owned controls and native cost/once-use, with explicit provisional numeric/disposal interpretations. Advanced preview remains six-classic only; broader timing and combinations are not certified.',
'Richese’s special purchase is available in development tables: spend a Karama and three spice to choose privately from the cache, with separate Emperor income and explicit full-hand/final-cache guards. See its acquisition guide for current controls and limits.',
'See Ecaz special Karama and Moritani special Karama for their source-bound first versions. Their declarations do not create a normal faction-cancellation window; existing Kull/BG handling remains in the shared special-intent pipeline.',
],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Tleilaxu proactive Revival is prototyped: quiet any-player selection and original target-bound interruption share current physical-cost validation; completed returns stay intact. Ghola inclusion and phase expiry are provisional; Emperor special immunity and the low-Homeworld overlap guard remain.', evidence: ['game/tleilaxu-special-karama.ts', 'game/engine.ts', 'game/card-availability.ts', 'game/revival.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Native Advanced owner has persistent card/target controls before a request and in the pending decision, including self, ally, ready and prior-return choices. Enclosing priorities and target restrictions retain readable reasons.', evidence: ['components/tleilaxu-special-karama.tsx', 'components/game-table.tsx', 'tests/tleilaxu-special-karama-controls.test.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'Four existing profiles reuse public-Tanks thresholds through the shared current quote; no strategic refinement or difficulty calibration.', evidence: ['game/bots.ts', 'tests/tleilaxu-special-karama-window.test.ts', 'tests/ghola-bots.test.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Proactive source cutover, provisional Ghola/phase scope, printed Special Karama immunity and remaining Homeworld/combination limits are explicit.', evidence: ['docs/TLEILAXU_SPECIAL_KARAMA.md', 'docs/RULE_DECISIONS.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Native setup, paid shipment and battle earn the actual cards and twelve Tanks; focused regressions cover declaration, original interruption, prior returns, stale intent and real phase expiry. Frozen14-file148/148, types/lint/build, six actual callback/engine/GameTable SSR journeys and four legal existing-profile paths pass. Genuine111/113-action sources earn original cards and casualties. Private-driver-only repairs reuse unchanged-source checks. Full modes, browser/deployed and final assurance stay open.', evidence: ['tests/fixture-tleilaxu-special-karama.ts', 'tests/tleilaxu-special-karama-window.test.ts', 'tests/tleilaxu-special-karama-quote.test.ts', 'tests/tleilaxu-special.test.ts', 'tests/karama-intents.test.ts', 'tests/ghola-lifecycle.test.ts'] },
],
related: [
'card-karama',
'richese-acquisition',
+36
View File
@@ -0,0 +1,36 @@
import type { FactionId } from './catalog';
export type TleilaxuSpecialKaramaContext = {
status: string;
phase: number;
advanced: boolean;
owner: { id: string; faction: FactionId; specialKaramaUsed?: boolean };
players: readonly { id: string }[];
/** The authoritative engine supplies current spendable physical card IDs. */
cards: readonly string[];
/** The original normal-revival interruption, otherwise a quiet declaration. */
pendingPlayer: string | null;
blocked: string | null;
targetBlocks?: Readonly<Record<string, string | null>>;
};
/** Supplied physical p38 H permits choosing any one player during Revival.
* Tank count, hidden leader eligibility, prior returns and readiness do not
* determine permission. Existing enclosing priorities and target exceptions do.
*/
export function quoteTleilaxuSpecialKarama(context: TleilaxuSpecialKaramaContext) {
let blocked: string | null = null;
if (!context.advanced) blocked = 'Special Karama powers require the Advanced game.';
else if (context.owner.faction !== 'tleilaxu') blocked = 'Only native Tleilaxu may use this special Karama.';
else if (context.status !== 'playing' || context.phase !== 4)
blocked = 'Declare this special Karama during the Revival Phase.';
else if (context.owner.specialKaramaUsed)
blocked = 'Your special Karama power has already been used this game.';
else if (context.blocked) blocked = context.blocked;
else if (!context.cards.length) blocked = 'A spendable Karama card is required.';
const targets = context.players
.filter(player => context.pendingPlayer === null || player.id === context.pendingPlayer)
.map(player => ({ player: player.id, blocked: context.targetBlocks?.[player.id] ?? null }));
if (!blocked && !targets.length) blocked = 'Choose a player in this Revival Phase.';
return { blocked, cards: [...context.cards], targets, pending: context.pendingPlayer !== null };
}