Make Fremen allied revival grants cancelable

Bind the original Fremen/ally/turn grant before Karama allowance or prevention.
A prevented grant leaves intrinsic free rates and prior payments intact. Permit
late grants under the explicit provisional Recruits current-rate policy, with
shared controls, minimal bot support and native public chronology evidence.
Complete rules and browser/deployed acceptance remain open.
This commit is contained in:
Quintine committed 2026-10-24 18:16:03 +11:00
1 parent d63efccb55
commit 0e3d6a250e
23 files changed
+1183 -44

No files matched your search

+1
View File
@@ -141,6 +141,7 @@ const responses = {
guildTransportPermission: true,
emperorGift: true,
emperorRevival: true,
fremenRevivalGrant: true,
stormPeek: true,
stormProtection: true,
wormPlacement: true,
+1 -2
View File
@@ -3245,8 +3245,7 @@ function policyActions(g: GameView): Action[] {
actions.push({ type: 'requestLeaderRevival', leader: dead.id });
}
}
if (me.faction === 'fremen' && me.ally && !g.freeRevival.includes(me.ally) &&
!g.recruitsPreview?.grantRevivalBlocked)
if (g.fremenRevivalGrant && !g.fremenRevivalGrant.granted && !g.fremenRevivalGrant.blocked)
actions.push({ type: 'grantRevival' });
if (me.faction === 'emperor' && me.ally && level > 0) {
const ally = g.players.find((p) => p.id === me.ally)!;
+40 -20
View File
@@ -532,6 +532,7 @@ import { quoteHomeworldPaymentIncome, quoteHomeworldBankPayment } from './homewo
import { quoteEcazPoisonIncome, EcazPoisonIncomeError, type EcazPoisonDiscard } from './ecaz-poison-income';
import { resolveBattleWeapons } from './effective-weapons';
import { choamGholaEvent, choamGholaSkillOfferEvent, choamGholaSkillSignature, choamMarketGholaError, choamSaleGholaTiming, type ChoamMarketGhola } from './choam-market-ghola';
import { quoteFremenRevivalGrant, validateFremenRevivalGrant, projectFremenRevivalGrant, FremenRevivalGrantError, type FremenRevivalGrantSource } from './fremen-revival-grant';
import {
charityAmount,
charityQuote,
@@ -1060,6 +1061,7 @@ type SpiceBankerIncomePayment = {
};
export type ResponseWindow = {
fremenGrant?: FremenRevivalGrantSource;
spiceBankerIncomePayment?: SpiceBankerIncomePayment;
bureaucratPayment?: BureaucratPaymentSource;
bureaucratPaymentEvent?: string;
@@ -1113,6 +1115,7 @@ export type ResponseWindow = {
| 'guildTransportPermission'
| 'emperorGift'
| 'emperorRevival'
| 'fremenRevivalGrant'
| 'stormPeek'
| 'stormProtection'
| 'wormPlacement'
@@ -14452,11 +14455,24 @@ function recruitsPreview(g: Game, viewer: Player): RecruitsPreview | null {
const allowance = normalRevivalAllowance(g, target);
return { player, freeRate: allowance.freeRate, limit: allowance.limit };
}),
grantRevivalBlocked: active
? 'Grant Fremen ally revival before Recruits; changing the free rate after activation awaits a rules ruling.'
: null,
grantRevivalBlocked: projectFremenRevivalGrant(g, viewer.id)?.blocked ?? null,
};
}
function fremenRevivalGrantRule<T>(quote: () => T): T {
try { return quote(); }
catch (error) {
if (error instanceof FremenRevivalGrantError) throw new RuleError(error.message);
throw error;
}
}
function currentFremenRevivalGrant(g: Game, response: ResponseWindow): FremenRevivalGrantSource {
requireRule(response.kind === 'fremenRevivalGrant' && response.fremenGrant &&
response.owner === response.fremenGrant.owner && response.recipient === response.fremenGrant.recipient,
'The Fremen revival grant lost its original owner and ally.');
requireRule(!g.pendingRevival, 'Finish the existing force return before resolving the Fremen grant.');
fremenRevivalGrantRule(() => validateFremenRevivalGrant(g, response.fremenGrant!));
return response.fremenGrant;
}
/** Pure normal-lot prerequisites; free payment deliberately does not require spice. */
function normalKaramaAuction(
g: Game,
@@ -14694,6 +14710,7 @@ function validateKaramaUse(
'Karama cancels another faction’s power.',
);
karamaCanceledSource(g, use.response);
if (use.response.kind === 'fremenRevivalGrant') currentFremenRevivalGrant(g, use.response);
validateAuctionContinuationCancellation(
g,
use.response,
@@ -27119,6 +27136,7 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
karamaConversionIntegrity(g);
currentFactionPayment(g);
const response = g.response!;
const fremenGrant = response.kind === 'fremenRevivalGrant' ? currentFremenRevivalGrant(g, response) : null;
if (g.pendingKull?.stage === 'counter' && response.kind === 'choamWorthless') kullIntegrity(g);
if (response.kind === 'nexusPrescience') validateNexusInspectionResponse(g, response);
if (response.kind === 'nexusAdvisorFlip') validateNexusAdvisorResponse(g, response);
@@ -27196,6 +27214,18 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
return;
}
if (offerOccupiedBonus(g, response)) return;
if (response.kind === 'fremenRevivalGrant') {
requireRule(fremenGrant, 'The original Fremen revival grant is missing.');
if (canceled) {
g.revivalRules ??= newRevivalRules();
(g.revivalRules.fremenGrantBlocked ??= []).push(fremenGrant.recipient);
log(g, `${getPlayer(g, fremenGrant.owner).name}'s allied revival grant was prevented for this Revival. The ally keeps its intrinsic free rate and completed returns.`);
} else {
g.freeRevival.push(fremenGrant.recipient);
log(g, `${getPlayer(g, fremenGrant.owner).name} granted ${getPlayer(g, fremenGrant.recipient).name} the Fremen allied free-revival rate for this phase. Current Recruits rates, prior usage and total limits still apply.`, { faction: 'fremen', name: 'Allied free revival' });
}
return;
}
if (response.kind === 'ecazOccupy') {
const b = g.battle, occupy = b?.ecazOccupy;
requireRule(b && occupy?.lead && occupy.ecaz === response.owner && !b.revealed &&
@@ -34783,23 +34813,12 @@ function applyActionInner(
return g;
}
if (t === 'grantRevival') {
requireRule(
g.phase === 4 && p.faction === 'fremen' && p.ally,
'Only Fremen may grant their ally free revival during revival.',
);
requireRule(
!g.freeRevival.includes(p.ally),
'Free revival has already been granted.',
);
requireRule(
g.recruits?.turn !== g.turn,
'Grant Fremen ally revival before Recruits; changing the free rate after activation awaits a rules ruling.',
);
g.freeRevival.push(p.ally);
log(
g,
`${p.name} granted their ally up to three free force revivals this phase.`,
);
requireRule(g.phase === 4 && p.faction === 'fremen' && p.ally,
'Only Fremen may grant their ally free revival during revival.');
requireRule(!recruitsInteractionPending(g), 'Finish the current revival transaction or priority window before granting allied revival.');
const source = fremenRevivalGrantRule(() => quoteFremenRevivalGrant(g, id));
g.response = { kind: 'fremenRevivalGrant', owner: source.owner,
recipient: source.recipient, fremenGrant: source, passed: [] };
return g;
}
if (t === 'tleilaxuRevivalLimit' || t === 'tleilaxuAllyDiscount') {
@@ -36803,6 +36822,7 @@ export function viewGame(state: Game, id: string) {
emperorExtra: g.emperorExtra ?? {},
freeRevival: g.freeRevival ?? [],
recruitsPreview: recruitsPreview(g, me),
fremenRevivalGrant: projectFremenRevivalGrant(g, id),
revivalRules: {
...(g.revivalRules ?? newRevivalRules()),
earlyBlocked: (g.revivalRules?.earlyBlocked ?? []).filter(
+111
View File
@@ -0,0 +1,111 @@
import type { Game } from './engine';
export type FremenRevivalGrantSource = {
turn: number;
owner: string;
recipient: string;
};
export type FremenRevivalGrantContext = Pick<
Game,
'status' | 'phase' | 'turn' | 'players' | 'freeRevival' | 'revivalRules' | 'recruits'
>;
export class FremenRevivalGrantError extends Error {
constructor(message: string) {
super(message);
this.name = 'FremenRevivalGrantError';
}
}
const id = (value: unknown): value is string =>
typeof value === 'string' && value.length > 0;
const count = (value: unknown): value is number =>
Number.isSafeInteger(value) && (value as number) >= 0;
/** Permission only. The engine owns the response and commits or denies this
* source once; normal revival quotes retain rates, prior usage and payment.
* No busy gate belongs here because settlement validates its own response. */
export function fremenRevivalGrantBlock(
g: FremenRevivalGrantContext,
owner: string,
): string | null {
if (g.status !== 'playing' || g.phase !== 4 || !count(g.turn) || g.turn < 1)
return 'Grant Fremen allied free revival during the current Revival phase.';
if (!id(owner) || !Array.isArray(g.players))
return 'The grant needs the current native Fremen owner.';
const owners = g.players.filter(player => player.id === owner);
const fremen = owners[0];
if (owners.length !== 1 || fremen.faction !== 'fremen' ||
g.players.filter(player => player.faction === 'fremen').length !== 1)
return 'The grant needs the current native Fremen owner.';
const recipients = g.players.filter(player => player.id === fremen.ally);
const recipient = recipients[0];
if (!id(fremen.ally) || recipients.length !== 1 ||
recipient.id === owner || recipient.ally !== owner)
return 'The grant needs the Fremen owner’s current mutual ally.';
if (!Array.isArray(g.freeRevival))
return 'The current Fremen revival permissions are invalid.';
if (g.freeRevival.includes(recipient.id))
return 'Fremen allied free revival has already been granted this phase.';
if (g.revivalRules?.fremenGrantBlocked !== undefined &&
(!Array.isArray(g.revivalRules.fremenGrantBlocked) ||
g.revivalRules.fremenGrantBlocked.some(target => !id(target))))
return 'The current Fremen grant prevention record is invalid.';
if (g.revivalRules?.fremenGrantBlocked?.includes(recipient.id))
return 'Karama prevented this Fremen allied grant for the current Revival phase.';
if (g.recruits?.turn === g.turn &&
(!count(recipient.revived) ||
(recipient.freeForcesRevived === undefined
? recipient.revived > 0
: !count(recipient.freeForcesRevived) ||
recipient.freeForcesRevived > recipient.revived)))
return 'The ally’s prior normal revival usage is inconsistent or unknown; this grant cannot safely change its rate.';
return null;
}
export function quoteFremenRevivalGrant(
g: FremenRevivalGrantContext,
owner: string,
): FremenRevivalGrantSource {
const blocked = fremenRevivalGrantBlock(g, owner);
if (blocked) throw new FremenRevivalGrantError(blocked);
const fremen = g.players.find(player => player.id === owner)!;
return { turn: g.turn, owner, recipient: fremen.ally! };
}
/** Bind the original relationship and turn before either response outcome.
* No source supplied by a client may retarget a pending grant. */
export function validateFremenRevivalGrant(
g: FremenRevivalGrantContext,
source: FremenRevivalGrantSource,
): void {
if (!source || typeof source !== 'object' || Array.isArray(source) ||
Object.keys(source).sort().join(',') !== 'owner,recipient,turn' ||
!count(source.turn) || source.turn !== g.turn ||
!id(source.owner) || !id(source.recipient))
throw new FremenRevivalGrantError('The Fremen allied revival grant has lost its original source.');
const current = quoteFremenRevivalGrant(g, source.owner);
if (current.recipient !== source.recipient)
throw new FremenRevivalGrantError('The Fremen allied revival grant no longer matches its original ally.');
}
/** Owner-only status. No wallet, Tanks, prior counts or quoted free amount is
* projected; the existing normal revival panel owns numerical allowances. */
export function projectFremenRevivalGrant(
g: FremenRevivalGrantContext,
viewer: string,
): { recipient: string; granted: boolean; blocked: string | null } | null {
if (!id(viewer) || !Array.isArray(g.players)) return null;
const owners = g.players.filter(player => player.id === viewer);
const owner = owners[0];
if (owners.length !== 1 || owner.faction !== 'fremen' ||
!id(owner.ally) || owner.ally === owner.id ||
g.players.filter(player => player.id === owner.ally).length !== 1)
return null;
return {
recipient: owner.ally,
granted: Array.isArray(g.freeRevival) && g.freeRevival.includes(owner.ally),
blocked: fremenRevivalGrantBlock(g, viewer),
};
}
+8
View File
@@ -189,6 +189,14 @@ const cancellationSources = {
: response.advisorResume === 'declaration'
? { battle: battleSource(g), active: g.active }
: movementSource(g),
fremenRevivalGrant: (g, response) => ({
source: response.fremenGrant ?? null,
ownerAlly: g.players.find(p => p.id === response.owner)?.ally ?? null,
recipientAlly: g.players.find(p => p.id === response.recipient)?.ally ?? null,
granted: g.freeRevival.includes(response.recipient ?? ''),
blocked: g.revivalRules?.fremenGrantBlocked?.includes(response.recipient ?? '') ?? false,
recruits: g.recruits?.turn === g.turn,
}),
bgCharity: () => null,
choamCharity: () => null,
choamInflation: (g) => ({
+1
View File
@@ -42,6 +42,7 @@ const cancellationLabels = {
guildTransportPermission: 'Guild transport permission',
emperorGift: 'the Emperor gift',
emperorRevival: 'Emperor revival',
fremenRevivalGrant: 'the Fremen allied free-revival grant',
stormPeek: 'the storm peek',
stormProtection: 'storm protection',
wormPlacement: 'additional worm placement',
+80 -3
View File
@@ -5206,6 +5206,83 @@ export const RULE_TOPICS: RuleTopic[] = [
{area:'Verification',status:'Partial',detail:"Classic Nexus optional14 now retains original Tech3+ and Advanced Strongholds2+ with selected33/36. Mandatory Banker remains separate from native phase income; actual third-party receipts, trained-disc posture, local bank support and original cleanup/replacement stay distinct. Real normal lobby, earned Carthag/Arrakeen and fixed/free Axlotl controls complete with four legal policies. Earlier native/Richese evidence remains dated; full modes, strategy and deferred assurance stay open.",evidence:['tests/nexus-core-tech-runtime.test.ts','tests/fixture-nexus-core-tech.ts','tests/nexus-core-strongholds-runtime.test.ts','tests/fixture-nexus-core-strongholds.ts','tests/classic-nexus-core.test.ts','tests/fixture-classic-nexus-core.ts','game/faction-module-profile.ts','tests/classic-independent-nexus-utility.test.ts', 'tests/classic-independent-nexus-battle.test.ts', 'tests/fixture-classic-independent-nexus.ts', 'tests/bureaucrat-richese-cache.test.ts','tests/bureaucrat-richese-black-market.test.ts','tests/fixture-bureaucrat-richese-funding.ts','docs/BUREAUCRAT_PAYMENTS.md','tests/diplomat-used-defense.test.ts', 'tests/diplomat-late-journey.test.ts', 'tests/fixture-diplomat-late-journey.ts', 'tests/diplomat-context-journey.test.ts', 'tests/fixture-diplomat-context-journey.ts', 'docs/DIPLOMAT_DEFENSE.md', 'tests/smuggler-kwisatz-journey.test.ts', 'tests/fixture-smuggler-kwisatz-journey.ts', 'tests/smuggler-artillery-journey.test.ts', 'tests/fixture-smuggler-artillery-journey.ts', 'docs/SMUGGLER_BATTLE.md', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts','tests/homeworld-native-e1e2-modules-runtime.test.ts','tests/homeworld-native-e3-modules-runtime.test.ts','tests/homeworld-classic-skills-runtime.test.ts','tests/homeworld-skills-discovery-runtime.test.ts','tests/homeworld-optional-modules-runtime.test.ts','tests/leader-skills-engine.test.ts','tests/leader-skills-capture.test.ts','tests/captured-skill-revival.test.ts','tests/fixture-captured-skill-revival.ts','tests/captured-skill-special-revival.test.ts','tests/fixture-captured-skill-special-revival.ts','tests/leader-battle-effects.test.ts','tests/suk-graduate.test.ts','tests/mentat-question.test.ts','tests/bureaucrat-payment.test.ts','tests/smuggler-battle.test.ts','tests/spice-banker.test.ts','tests/advanced-native-skills.test.ts','tests/fixture-advanced-native-skills.ts','tests/skills-tech-payments-runtime.test.ts','tests/skills-tech-battle-runtime.test.ts','tests/ix-choam-skills-tech-runtime.test.ts','tests/tleilaxu-skills-tech-runtime.test.ts','tests/richese-skills-tech-runtime.test.ts','tests/classic-skills-stronghold-runtime.test.ts','tests/native-skills-stronghold-runtime.test.ts','tests/ecaz-skills-modules-runtime.test.ts','tests/moritani-skills-modules-runtime.test.ts','docs/LEADER_SKILLS_RUNTIME.md','docs/HOMEWORLD_RUNTIME_IMPLEMENTATION.md']},
],
},
{
"id": "fremen-revival-grant",
"title": "Fremen allied free revival",
"category": "Factions",
"coverage": "Partial",
"developmentStage": "Prototyped",
"summary": "Grant the current ally its Fremen free-revival rate after a native Karama response, including after Recruits.",
"steps": [
"During Revival, Fremen may offer the current ally its ordinary free-force rate of three. The grant replaces the native ordinary rate; current bonuses, total limits and separate special-force limits still apply. It never moves counters by itself.",
"The original owner, ally and turn remain bound while another player may prevent the grant with Karama. All-pass commits once. Under the provisional prevention policy, cancellation denies this grant for the current Revival, preserving the ally’s intrinsic free rate, Recruits and completed returns/payments. The next native Revival clears the denial.",
"Recruits doubles the current granted rate including applicable bonuses before caps. With no other bonus, three becomes six. Provisionally accept grants before or after the card with the same calculation: deduct recorded free usage, preserve all earlier total returns and paid classifications, and give no refund. Finish a pending transaction before declaring a grant.",
"The Fremen panel keeps status and unavailable reasons visible. Your ally’s normal revival panel supplies the actual remaining amount. Complete modes, broader combinations and final acceptance remain unfinished."
],
"related": [
"revival",
"faction-fremen",
"card-recruits",
"card-karama"
],
"checklist": [
{
"area": "Implementation",
"status": "Partial",
"detail": "Original owner/mutual-ally/turn source validates before cancellation cost and both outcomes. Only all-pass commits; cancellation marks a phase-scoped denied grant without general free prevention. Existing native arithmetic retains limits, prices, bonuses and prior usage.",
"evidence": [
"game/fremen-revival-grant.ts",
"game/engine.ts",
"game/revival.ts",
"game/karama-context.ts",
"game/kull-purpose.ts"
]
},
{
"area": "Player controls",
"status": "Partial",
"detail": "Actual grant callback, native response and persistent status/policy guidance are integrated. Busy, interrupted, already granted and denied actions stay disabled; no private balance, Tanks or usage count is read.",
"evidence": [
"components/fremen-revival-grant.tsx",
"components/game-table.tsx",
"tests/fremen-revival-grant-controls.test.tsx"
]
},
{
"area": "AI",
"status": "Partial",
"detail": "Sixteen existing-profile native/Recruits paths accept80 legal own-view grant/response/return proposals. The same projected permission prevents repeat grants. Strategy and strength calibration remain deferred.",
"evidence": [
"game/bots.ts",
"tests/recruits-controls.test.tsx",
"docs/FREMEN_REVIVAL_GRANT.md"
]
},
{
"area": "Documentation",
"status": "Partial",
"detail": "Distinguishes printed native rates and general alliance cancellation from provisional digital response duration and late-Recruits current-rate symmetry.",
"evidence": [
"docs/FREMEN_REVIVAL_GRANT.md",
"docs/RECRUITS_RUNTIME.md",
"docs/RECRUITS_RULES.md"
]
},
{
"area": "Verification",
"status": "Partial",
"detail": "14 files305/305, types/lint/build, twelve actual control/gameplay/GameTable SSR journeys and sixteen minimal-policy paths pass. Genuine75–77 Basic/92–94 Advanced actions earn casualties and form a native Nexus alliance; Recruits is originally dealt and prior returns paid normally. Next actual Revival resets denial. Complete modes, visual/deployed and comprehensive assurance remain open.",
"evidence": [
"tests/fixture-fremen-revival-grant.ts",
"tests/fremen-revival-grant-engine.test.ts",
"tests/fremen-revival-grant-quote.test.ts",
"tests/fremen-revival-grant-controls.test.tsx",
"tests/recruits-engine.test.ts",
"tests/recruits-controls.test.tsx"
]
}
]
},
{
id: 'card-recruits',
title: 'Recruits and the Ecaz Treachery Cards variant',
@@ -5217,16 +5294,16 @@ export const RULE_TOPICS: RuleTopic[] = [
'The separate three-card variant includes Recruits, Reinforcements and Harass & Withdraw. It does not require the Ecaz faction. A development preview includes all three physical cards before setup; Recruits, bounded Reinforcements and Harass & Withdraw controls are connected.',
'The holder may play Recruits from its Revival panel. Discard the card once, then all players use the increased rates while this Revival phase remains open, including players who had already marked themselves ready. Completed returns still count toward their turn totals.',
'Atreides rises from two free forces to four; Fremen from three to six; Guild from one to two. The ordinary force limit becomes seven. Native unlimited CHOAM and Tleilaxu allowances remain unlimited, and their free rates become zero and four. Paid prices and separate elite limits still apply.',
'Free prevention still prevents free returns. Prior paid ordinary returns now have a visibly provisional policy: preserve payment, deduct only recorded free returns from the doubled free allowance and all returns from the total cap. Two free plus one paid Atreides may take two more free without refund; this is not a numerical user ruling. Missing/inconsistent ledgers, later Fremen grants, pending transactions and same-turn replay remain guarded.',
'Free prevention still prevents free returns. Prior paid ordinary returns now have a visibly provisional policy: preserve payment, deduct only recorded free returns from the doubled free allowance and all returns from the total cap. Two free plus one paid Atreides may take two more free without refund; this is not a numerical user ruling. Fremen may now grant after play under the same provisional current-rate policy, with its original Karama response settled before commitment. Missing/inconsistent ledgers, pending transactions and same-turn replay remain guarded.',
'Clean classic2–6 Basic/Advanced Homeworlds compose through the fresh independent36-card setup. Double current native free rates including low-population bonuses, within the seven-force total. Provisionally recheck population and recorded free use on each separate request: low Fremen may take7 together, but1 crossing high leaves5 further free and cannot buy a seventh without Tleilaxu. Original typed destinations and special-force limits stay. Other overlays/rosters, public activation and whole-mode acceptance remain unfinished.',
'Classic or selected Ix/CHOAM native/base rosters, including Homeworlds, may retain original unused Tech Tokens3–6 and/or Stronghold Cards2–6 Advanced. Setup, first-Storm Tech and actual end-Mentat custody remain original. Recruits changes current native free rates, not the once-per-phase Axlotl trigger/delayed payout. Harass returns are not free Revival.',
'Selected Ix/CHOAM native/base Basic/Advanced Worlds use original38/50 independent-card setup with optional original phase modules. Current native bonuses, typed deposits and accepted paid/free classifications stay. Ix keeps its multiple-Cyborg allowance; Tleilaxu and CHOAM remain unlimited rather than being reduced to seven. Other card/overlay/public-start gates remain.',
'Supported original all14/card rosters retain Tech3+ and Advanced Strongholds2+, including classic/E1/E2 Worlds. Current native free allowance doubles after recorded use; separate World groups re-observe native population. Typed elite caps, unlimited native allowances, paid Cyborg surcharges and any Tleilaxu recipient stay. Axlotl accrues once and waits for phase end; normal Banker uses the accepted final Bank invoice and waits for Mentat, not a net wallet delta after a separate native payout. No ledger, refund, owner or premature spendable income is invented.',
'Independent Discovery, with or without Nexus, retains original optional World/all14/phase modules. Purchased Recruits may double the native free rate against actual earlier nested-battle losses; accepted physical fee and dial casualties supply the Tanks. Existing late-play/current-rate policies stay provisional. Broader interactions and complete modes remain open.',
],
related: ['revival', 'homeworlds', 'ecaz-modules', 'expansion-faction-games', 'implementation-checklist'],
related: ['fremen-revival-grant', 'revival', 'homeworlds', 'ecaz-modules', 'expansion-faction-games', 'implementation-checklist'],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Independent physical play/discard and public turn rates use shared revival quotes, including clean classic Homeworlds and native typed deposits. Separate requests provisionally recompute current thresholds; prior paid returns retain payment and recorded free use. Unknown ledgers, pending repricing and later Fremen grants remain guarded; Reinforcements has its separate exact native-source fee composition.', evidence: ['game/ecaz-cards.ts', 'game/recruits.ts', 'game/revival.ts', 'game/engine.ts'] },
{ area: 'Implementation', status: 'Partial', detail: 'Independent physical play/discard and public turn rates use shared revival quotes, including clean classic Homeworlds and native typed deposits. Separate requests provisionally recompute current thresholds; prior paid returns retain payment and recorded free use. A later Fremen grant now uses the same current-rate/free-use quote after native Karama settlement; unknown ledgers and pending repricing remain guarded; Reinforcements has its separate exact native-source fee composition.', evidence: ['game/ecaz-cards.ts', 'game/recruits.ts', 'game/revival.ts', 'game/engine.ts'] },
{ area: 'Player controls', status: 'Implemented', detail: 'The owned physical card has full inspection, a Revival action and explicit unavailable reasons. All seats see the effective public rates after play.', evidence: ['components/recruits.tsx', 'components/game-table.tsx', 'game/card-presentation.ts'] },
{ area: 'AI', status: 'Partial', detail: 'All four profiles use the same legal owned offer for useful own or allied revival, then existing authoritative revival choices. Complete card strategy and calibration remain unfinished.', evidence: ['game/bots.ts'] },
{ area: 'Documentation', status: 'Implemented', detail: 'The guide records examples, limits, independent-cap inference and the visibly provisional late-paid policy; source alternatives and late-rate questions remain explicit.', evidence: ['docs/RECRUITS_RULES.md', 'docs/RECRUITS_RUNTIME.md'] },
+2
View File
@@ -28,6 +28,8 @@ export type RevivalRules = {
nexusFremen?: { turn: number; owners: string[] };
choamBlocked?: boolean;
freeBlocked?: string[];
/** This Revival turn's prevented native Fremen-to-ally grant opportunities. */
fremenGrantBlocked?: string[];
limitBlocked: boolean;
fullPrice: boolean;
allyDiscount: string | null;
+1
View File
@@ -127,6 +127,7 @@ const RESPONSES = {
guildTransportPermission: true,
emperorGift: true,
emperorRevival: true,
fremenRevivalGrant: true,
stormPeek: true,
stormProtection: true,
wormPlacement: true,