Allow concealed No-Fields in free Discovery entry

This commit is contained in:
Quintine committed 2026-10-27 13:22:14 +11:00
1 parent 11e50e7f49
commit 7f999f4573
14 files changed
+830 -38

No files matched your search

+22 -4
View File
@@ -1,8 +1,10 @@
import type { Action, GameView } from './engine';
import type {
DiscoveryEntryOffer,
DiscoveryEntryNoField,
DiscoveryEntrySource,
} from './discovery-entry';
import { splitLocation } from './board';
function entryDecision(game: GameView) {
const decision = game.decision;
@@ -32,8 +34,9 @@ function whole(value: unknown): value is number {
function selectedGroups(
offer: DiscoveryEntryOffer,
groups: readonly DiscoveryEntrySource[],
allowEmpty = false,
): DiscoveryEntrySource[] | null {
if (!Array.isArray(groups) || !groups.length) return null;
if (!Array.isArray(groups) || (!groups.length && !allowEmpty)) return null;
const selected = groups
.map((group) => ({ ...group }))
.sort((a, b) => a.source.localeCompare(b.source));
@@ -61,16 +64,31 @@ function selectedGroups(
export function discoveryEntryMoveAction(
game: GameView,
groups: readonly DiscoveryEntrySource[],
noField?: DiscoveryEntryNoField,
): Action | null {
const entry = currentEntry(game);
if (!entry) return null;
const selected = selectedGroups(entry.offer, groups);
if (noField !== undefined) {
const offered = entry.offer.noField;
if (!noField || typeof noField !== 'object' || Array.isArray(noField) ||
Object.keys(noField).sort().join(',') !== 'event,source,tokenId' ||
typeof noField.tokenId !== 'string' || !noField.tokenId.trim() ||
typeof noField.event !== 'string' || !noField.event.trim() ||
typeof noField.source !== 'string' ||
!offered || noField.tokenId !== offered.tokenId || noField.event !== offered.event ||
noField.source !== offered.source) return null;
const source = splitLocation(noField.source);
if (source.territory !== entry.offer.parent || noField.source !== `${source.territory}:${source.sector}`)
return null;
}
const selected = selectedGroups(entry.offer, groups, noField !== undefined);
return selected
? {
type: 'decision',
event: entry.decision.event,
accept: true,
groups: selected,
...(noField ? { noField: noField.tokenId, noFieldEvent: noField.event } : {}),
}
: null;
}
@@ -84,11 +102,11 @@ export function discoveryEntryDeclineAction(
: null;
}
/** Uses only the current player's projected physical source groups. */
/** Uses only the current player's projected groups and offered owned marker. */
export function discoveryEntryBotActions(game: GameView): Action[] {
const offer = game.discoveryEntry;
if (!currentEntry(game) || !offer) return [];
const move = discoveryEntryMoveAction(game, offer.sources),
const move = discoveryEntryMoveAction(game, offer.sources, offer.noField),
decline = discoveryEntryDeclineAction(game);
return [move, decline].filter((action): action is Action => action !== null);
}
+60 -10
View File
@@ -10,6 +10,7 @@ import {
} from './discoveries';
import { splitLocation, validGameLocation, type MobileBoard } from './board';
import { territoryEntryBlock, type OccupancySeat } from './occupancy';
import type { RicheseNoField } from './richese-no-field';
export class DiscoveryEntryError extends Error {}
@@ -29,6 +30,8 @@ export type DiscoveryEntrySeat = OccupancySeat & {
faction: FactionId;
forces: Record<string, number>;
elites?: { forces: Record<string, number> };
noField?: RicheseNoField | null;
noFieldEvent?: string;
};
export type DiscoveryEntryContext = Omit<MobileBoard, 'discoveries'> & {
@@ -45,6 +48,12 @@ export type DiscoveryEntrySource = {
elite: number;
};
export type DiscoveryEntryNoField = {
tokenId: string;
event: string;
source: string;
};
export type DiscoveryEntryArrival = {
event: string;
token: DiscoveryOpaqueTokenId;
@@ -55,6 +64,7 @@ export type DiscoveryEntryArrival = {
destinationBefore: number;
destinationEliteBefore: number;
groups: DiscoveryEntrySource[];
noField?: DiscoveryEntryNoField;
amount: number;
elite: number;
signature: string;
@@ -74,6 +84,7 @@ export type DiscoveryEntryRound = {
export type DiscoveryEntrySelection = {
groups: readonly DiscoveryEntrySource[];
noField?: DiscoveryEntryNoField;
};
export type DiscoveryEntryOffer = {
@@ -84,6 +95,7 @@ export type DiscoveryEntryOffer = {
destination: DiscoveryLocationId;
sector: 0;
sources: DiscoveryEntrySource[];
noField?: DiscoveryEntryNoField;
blocked: string | null;
arrival: DiscoveryEntryArrival | null;
};
@@ -110,6 +122,7 @@ function arrivalPayload(arrival: Omit<DiscoveryEntryArrival, 'signature'>) {
arrival.groups.map((group) => [group.source, group.normal, group.elite]),
arrival.amount,
arrival.elite,
...(arrival.noField ? [[true, arrival.noField.source]] : []),
];
}
@@ -161,6 +174,13 @@ function strictSource(value: unknown): value is DiscoveryEntrySource {
);
}
function strictNoField(value: unknown): value is DiscoveryEntryNoField {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
const marker = value as DiscoveryEntryNoField;
return Object.keys(marker).sort().join(',') === 'event,source,tokenId' &&
identifier(marker.tokenId) && identifier(marker.event) && identifier(marker.source);
}
function validateInventory(discoveries: DiscoveryState) {
try {
validateDiscoveryState(discoveries);
@@ -181,7 +201,10 @@ function validateArrivalShape(
typeof arrival === 'object' &&
!Array.isArray(arrival) &&
Object.keys(arrival).sort().join(',') ===
'amount,destination,destinationBefore,destinationEliteBefore,elite,event,groups,owner,parent,sector,signature,token' &&
(Object.hasOwn(arrival, 'noField')
? 'amount,destination,destinationBefore,destinationEliteBefore,elite,event,groups,noField,owner,parent,sector,signature,token'
: 'amount,destination,destinationBefore,destinationEliteBefore,elite,event,groups,owner,parent,sector,signature,token') &&
(!Object.hasOwn(arrival, 'noField') || strictNoField(arrival.noField)) &&
arrival.event === `${frame.event}:${frame.cursor}` &&
arrival.token === current.token &&
arrival.owner === current.owner &&
@@ -192,7 +215,7 @@ function validateArrivalShape(
whole(arrival.destinationEliteBefore) &&
arrival.destinationEliteBefore <= arrival.destinationBefore &&
Array.isArray(arrival.groups) &&
arrival.groups.length > 0 &&
(arrival.groups.length > 0 || !!arrival.noField) &&
arrival.groups.every(strictSource) &&
arrival.groups.every(
(group, index) =>
@@ -202,12 +225,12 @@ function validateArrivalShape(
whole(arrival.amount) &&
arrival.amount > 0 &&
whole(arrival.elite) &&
arrival.elite <= arrival.amount &&
arrival.elite <= arrival.amount - (arrival.noField ? 1 : 0) &&
arrival.amount ===
arrival.groups.reduce(
(sum, group) => sum + group.normal + group.elite,
0,
) &&
) + (arrival.noField ? 1 : 0) &&
arrival.elite ===
arrival.groups.reduce((sum, group) => sum + group.elite, 0) &&
arrival.signature === discoveryEntryArrivalSignature(arrival),
@@ -267,6 +290,12 @@ export function validateDiscoveryEntryRound(
const token = g.discoveries.tokens.find(
(candidate) => candidate.id === frame.arrival!.token,
)!;
const validSource = (key: string) => {
const source = splitLocation(key);
return key === `${source.territory}:${source.sector}` &&
source.territory === frame.arrival!.parent && source.sector !== g.storm &&
validGameLocation(g, source.territory, source.sector);
};
const validSources = frame.arrival.groups.every((group) => {
const source = splitLocation(group.source);
return (
@@ -275,7 +304,7 @@ export function validateDiscoveryEntryRound(
source.sector !== g.storm &&
validGameLocation(g, source.territory, source.sector)
);
});
}) && (!frame.arrival.noField || validSource(frame.arrival.noField.source));
requireEntry(
token.territory === frame.arrival.parent &&
token.face === frame.arrival.destination &&
@@ -342,6 +371,18 @@ function sourceOffer(
return sources.sort((a, b) => a.source.localeCompare(b.source));
}
/** A current owned marker is independently eligible; its value and reserves
* do not supply physical counters or alter this optional relocation. */
function noFieldOffer(g: DiscoveryEntryContext, player: DiscoveryEntrySeat, parent: string): DiscoveryEntryNoField | undefined {
const marker = player.noField?.deployed;
if (player.faction !== 'richese' || !marker || marker.controller !== player.id ||
!identifier(marker.tokenId) || !identifier(player.noFieldEvent) ||
marker.location.territory !== parent || marker.location.sector === g.storm ||
!validGameLocation(g, marker.location.territory, marker.location.sector)) return undefined;
return { tokenId: marker.tokenId, event: player.noFieldEvent,
source: `${marker.location.territory}:${marker.location.sector}` };
}
function currentOffer(g: DiscoveryEntryContext, frame: DiscoveryEntryRound) {
const current = step(frame);
const player = g.players.find((candidate) => candidate.id === current.owner)!;
@@ -391,6 +432,7 @@ export function quoteDiscoveryEntry(
'This Discovery entry belongs to another player.',
);
const sources = sourceOffer(g, player, parent);
const noField = noFieldOffer(g, player, parent);
let blocked: string | null = null;
try {
blocked = territoryEntryBlock(g.players, owner, destination);
@@ -399,7 +441,7 @@ export function quoteDiscoveryEntry(
error instanceof Error ? error.message : 'Invalid Discovery occupancy.',
);
}
if (!blocked && !sources.length)
if (!blocked && !sources.length && !noField)
blocked =
'No non-advisor forces outside the storm can enter this location.';
const base = {
@@ -410,6 +452,7 @@ export function quoteDiscoveryEntry(
destination,
sector: 0 as const,
sources,
...(noField ? { noField: { ...noField } } : {}),
blocked,
};
if (!selection) return { ...base, arrival: null };
@@ -418,12 +461,17 @@ export function quoteDiscoveryEntry(
selection &&
typeof selection === 'object' &&
!Array.isArray(selection) &&
Object.keys(selection).join(',') === 'groups' &&
Object.keys(selection).sort().join(',') === (Object.hasOwn(selection, 'noField') ? 'groups,noField' : 'groups') &&
(!Object.hasOwn(selection, 'noField') || strictNoField(selection.noField)) &&
Array.isArray(selection.groups) &&
selection.groups.length > 0 &&
(selection.groups.length > 0 || !!selection.noField) &&
selection.groups.every(strictSource),
'Choose explicit positive ordinary and elite source groups.',
);
if (selection.noField)
requireEntry(noField && selection.noField.tokenId === noField.tokenId &&
selection.noField.event === noField.event && selection.noField.source === noField.source,
'Choose the current owned No-Field, event and parent source for this Discovery entry.');
const groups = selection.groups
.map((group) => ({ ...group }))
.sort((a, b) => a.source.localeCompare(b.source));
@@ -459,8 +507,9 @@ export function quoteDiscoveryEntry(
destinationBefore,
destinationEliteBefore,
groups,
amount: groups.reduce((sum, group) => sum + group.normal + group.elite, 0),
amount: groups.reduce((sum, group) => sum + group.normal + group.elite, 0) + (selection.noField ? 1 : 0),
elite: groups.reduce((sum, group) => sum + group.elite, 0),
...(selection.noField ? { noField: { ...selection.noField } } : {}),
};
const arrival: DiscoveryEntryArrival = {
...unsigned,
@@ -501,7 +550,7 @@ export function advanceDiscoveryEntry(
return next;
}
/** Sign an accepted source selection before the caller moves physical forces. */
/** Sign a selection before the caller transfers physical forces and any marker. */
export function beginDiscoveryEntryArrival(
g: DiscoveryEntryContext,
frame: DiscoveryEntryRound,
@@ -518,6 +567,7 @@ export function beginDiscoveryEntryArrival(
arrival: {
...quote.arrival,
groups: quote.arrival.groups.map((group) => ({ ...group })),
...(quote.arrival.noField ? { noField: { ...quote.arrival.noField } } : {}),
},
signature: '',
};
+32 -10
View File
@@ -13288,6 +13288,17 @@ function discoveryEntryIntegrity(g: Game) {
requireRule(g.discoveryEnabled && g.discoveries && g.status === 'playing' && g.phase === 0 &&
frame.stage !== 'complete', 'Discovery entry must finish before the Storm Phase begins.');
nexusRule(() => validateDiscoveryEntryRound({...g, discoveries:g.discoveries!},frame));
if (frame.stage === 'arrival') {
const arrival = frame.arrival!, owner = getPlayer(g, arrival.owner), key = `${arrival.destination}:0`;
const physical = arrival.amount - Number(!!arrival.noField), marker = arrival.noField;
requireRule((owner.forces[key] ?? 0) === arrival.destinationBefore + physical &&
(owner.elites?.forces[key] ?? 0) === arrival.destinationEliteBefore + arrival.elite,
'The Discovery arrival has lost its transferred ordinary or elite force custody.');
if (marker) requireRule(owner.faction === 'richese' && owner.noField?.deployed?.tokenId === marker.tokenId &&
owner.noField.deployed.controller === owner.id && owner.noField.deployed.location.territory === arrival.destination &&
owner.noField.deployed.location.sector === 0 && !!owner.noFieldEvent && owner.noFieldEvent !== marker.event,
'The Discovery arrival has lost its transferred concealed No-Field.');
}
const automatic = g.pendingAutomaticAdvisorRelocation;
if (automatic?.receipt.kind === 'discovery') {
requireRule(frame.stage === 'arrival' && !choices.length && !children.length,
@@ -13308,10 +13319,6 @@ function discoveryEntryIntegrity(g: Game) {
choices[0].event === `${frame.event}:${frame.cursor}`, 'The Discovery entry has lost its owned choice.');
} else {
requireRule(!choices.length && children.length === 1, 'The Discovery entry has lost its pending arrival interaction.');
const arrival = frame.arrival!, owner = getPlayer(g, arrival.owner), key = `${arrival.destination}:0`;
requireRule(owner.forces[key] === arrival.destinationBefore + arrival.amount &&
(owner.elites?.forces[key] ?? 0) === arrival.destinationEliteBefore + arrival.elite,
'The Discovery arrival has lost its transferred ordinary or elite force custody.');
for (const child of children) {
requireRule(child.valid && child.actor === byFaction(g,'beneGesserit')?.id &&
child.territory === frame.arrival!.destination && child.receipt,
@@ -13346,25 +13353,40 @@ function decideDiscoveryEntry(g: Game, p: Player, action: Action) {
const frame = g.discoveryEntry;
requireRule(frame && g.discoveries && typeof action.accept === 'boolean' &&
action.event === `${frame.event}:${frame.cursor}` &&
Object.keys(action).every(key => ['type','event','accept','groups'].includes(key)),
'Choose the current Discovery entry and explicit physical force groups.');
Object.keys(action).every(key => ['type','event','accept','groups','noField','noFieldEvent'].includes(key)),
'Choose the current Discovery entry, physical groups and optional owned No-Field.');
if (!action.accept) {
requireRule(action.groups === undefined, 'Declining Discovery entry must not select forces.');
requireRule(action.groups === undefined && action.noField === undefined && action.noFieldEvent === undefined,
'Declining Discovery entry must not select forces or a No-Field.');
g.discoveryEntry = nexusRule(() => advanceDiscoveryEntry({...g,discoveries:g.discoveries!},frame,p.id));
log(g, `${p.name} declined this optional free Discovery entry.`);
continueDiscoveryEntry(g);
return;
}
const marker = action.noField === undefined && action.noFieldEvent === undefined ? undefined : (() => {
requireRule(p.noField?.deployed, 'Choose your currently deployed concealed No-Field.');
return {tokenId:stringField(action.noField),event:stringField(action.noFieldEvent),
source:location(p.noField.deployed.location.territory,p.noField.deployed.location.sector)};
})();
g.discoveryEntry = nexusRule(() => beginDiscoveryEntryArrival({...g,discoveries:g.discoveries!},frame,p.id,
{groups:action.groups as DiscoveryEntrySelection['groups']}));
{groups:action.groups as DiscoveryEntrySelection['groups'],...(marker?{noField:marker}:{})}));
const arrival = g.discoveryEntry.arrival!;
const physical = arrival.amount - Number(!!arrival.noField);
const advisors = arrivalAsAdvisor(g,p,arrival.destination,arrival.parent);
removeGroup(p,arrival.groups.map(group => [group.source,group.normal+group.elite]),
Object.fromEntries(arrival.groups.map(group => [group.source,group.elite])));
place(p,arrival.destination,0,arrival.amount,arrival.elite);
if (arrival.noField) {
p.noField = noFieldRule(() => moveRicheseNoField(p.noField!,arrival.noField!.tokenId,
{territory:arrival.destination,sector:0}));
p.noFieldEvent = crypto.randomUUID();
}
if (physical) place(p,arrival.destination,0,physical,arrival.elite);
if (advisors) (p.advisors ??= {})[arrival.destination] ??= {};
else if (p.advisors) delete p.advisors[arrival.destination];
log(g, `${p.name} moved ${arrival.amount} ${arrival.amount === 1 ? 'force' : 'forces'}${arrival.elite ? `, including ${arrival.elite} elite ${arrival.elite === 1 ? 'force' : 'forces'},` : ''} from ${territory(arrival.parent).name} into ${territory(arrival.destination).name} before the storm. This one-time Discovery entry spends no spice or normal movement.`);
const group = arrival.noField
? `${physical ? `${physical} physical ${physical === 1 ? 'force' : 'forces'} and ` : ''}one concealed No-Field`
: `${arrival.amount} ${arrival.amount === 1 ? 'force' : 'forces'}`;
log(g, `${p.name} moved ${group}${arrival.elite ? `, including ${arrival.elite} elite ${arrival.elite === 1 ? 'force' : 'forces'},` : ''} from ${territory(arrival.parent).name} into ${territory(arrival.destination).name} before the storm. This one-time Discovery entry spends no spice or normal movement.${arrival.noField ? ' Provisional marker entry preserves concealment, reserves and the last-shipment restriction.' : ''}`);
const child: DiscoveryEntryArrivalChild = {owner:p.id,territory:arrival.destination,sector:0,
amount:arrival.amount,elite:arrival.elite,discoveryEntry:arrival.signature};
if (prepareRelocationAdvisorFlips(g, {kind:'discovery',player:p.id,origin:arrival.parent,
+5 -4
View File
@@ -5736,6 +5736,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Native typed forces enter as their actual ordinary and elite groups, never as passengers from the mobile stronghold or the concealed value of a No-Field. Richese may ship or move a concealed marker into an actually revealed nested location at sector zero; it remains one effective presence until its original reveal materializes available reserves. CHOAM Card Stash uses its five-card limit, drawing first and choosing any held discard afterward.',
'A printed Discovery blow destroys all old spice and forces in its target territory, including Fremen. A concealed No-Field provisionally reveals, materializes only available reserves and loses those actual counters with its physical companions; zero creates no casualty. This destructive reveal is a selected interpretation. It preserves token history and the original new-spice, storm and face-down placement sequence. Separately identified nested locations and other territories stay outside the parent target. Continue through normal Ready and Accept spice blow controls.',
'At the start of the next turn, before the storm and mobile stronghold movement, each eligible faction may move any positive subset of its non-advisor groups from the surrounding territory into the newly revealed location. The signed choice preserves ordinary and elite forces across sectors, normal source storm and occupancy restrictions, and costs no spice, shipment or normal movement. The printed text gives no sequence and does not restate the source-storm restriction; processing tokens in reveal order and factions in storm order while excluding storm sources are source inferences.',
'Provisional free entry also offers your current owned concealed No-Field separately from physical counters, alone or in a mixed group. It enters the revealed location concealed, including zero, renews its owner event and preserves last-shipped identity and reserves. It spends no spice, ordinary shipment or movement. Select or clear the marker separately; declining or moving only physical counters leaves it outside. Current ownership/event, source storm, normal occupancy and arrival interactions remain. This combines the independent Discovery move grant with one-force marker presence, without a specific combined ruling.',
'After advisor releases, every Cistern fighter occupant receives two bank spice. Orgiz occupants resolve in original storm order using actual ordinary deposit receipts, not mere force presence or old wallets. Advanced charges once per territory; Basic provisionally charges once per positive deposit. The first actual foreign ordinary collector in storm order with available spice pays. Shared Ecaz lots in both modes wait for the actual split, then consider positive foreign recipients Ecaz first, ally second; Advanced counts the territory once and Basic counts actually reduced source deposits. Self, zero shares and empty purses do not qualify. Balances track each transfer without debt; ordering and Basic mapping remain provisional.',
'Jacurutu pays one bank spice for each opposing undialed physical force actually sent to the Tanks. Where an Advanced dial admits different physical counts, the defeated player chooses an exact ordinary/elite/paid reading before support, casualties or rewards settle; unique readings remain automatic. This choice timing is a visibly provisional implementation interpretation. Original Harass returns bind their physical dial and actual withdrawal/retreat counters do not earn spice.',
'Each non-advisor Testing Station occupant may decrease ordinary storm movement by one, keep it or increase it by one. The provisional order is the original storm order, once per owner, with cumulative adjustments on current distance. Choose after the ordinary card window; traversal/protection starts only after all choices. Weather Control is unaffected. Original natural source/base and existing sole-frame history remain authoritative; legacy movement without a source is not invented.',
@@ -5747,17 +5748,17 @@ export const RULE_TOPICS: RuleTopic[] = [
checklist: [
{
area: 'Implementation', status: 'Partial',
detail: 'Concealed No-Fields now follow provisional destructive revelation at a printed Discovery blow through a distinct cause and all-player preflight; only actual reserve-capped counters are lost before original placement. Original classic and supported native Skills/Nexus/Discovery entries retain their selected modules and controls. Jacurutu uses an exact defeated-player reading before native settlement; Testing Station queues cumulative choices before one traversal. Cistern pays every fighter occupant. Orgiz uses actual ordinary receipts and bilateral shared allocations in both modes, grouping by territory in Advanced and positive source deposit in Basic without guessed payers or debt. Available balances track all transfers and cumulative overflow. Broader mode/module acceptance remains open.',
detail: 'Owned concealed No-Fields may also use the original next-turn free entry alone/mixed, with private metadata, public source-only signature, exact physical custody and no action/counter materialization. Concealed No-Fields now follow provisional destructive revelation at a printed Discovery blow through a distinct cause and all-player preflight; only actual reserve-capped counters are lost before original placement. Original classic and supported native Skills/Nexus/Discovery entries retain their selected modules and controls. Jacurutu uses an exact defeated-player reading before native settlement; Testing Station queues cumulative choices before one traversal. Cistern pays every fighter occupant. Orgiz uses actual ordinary receipts and bilateral shared allocations in both modes, grouping by territory in Advanced and positive source deposit in Basic without guessed payers or debt. Available balances track all transfers and cumulative overflow. Broader mode/module acceptance remains open.',
evidence: ['game/disaster-preflight.ts', 'game/discovery-module-profile.ts', 'game/richese-no-field.ts', 'game/discoveries.ts', 'game/discovery-actions.ts', 'game/discovery-entry.ts', 'game/discovery-collection.ts', 'game/board-resolution-quote.ts', 'game/discovery-flight.ts', 'game/discovery-battle.ts', 'game/discovery-storm.ts', 'game/shrine.ts', 'game/great-maker.ts', 'game/board.ts', 'game/engine.ts'],
},
{
area: 'Player controls', status: 'Partial',
detail: 'Private backs and readable authorized faces, optional inspection/reveal, owned stash discard, ordered Great Maker, free entry and carried movement use existing controls. Jacurutu offers exact physical readings only to the defeated owner before settlement. Testing Station offers cumulative legal distances to each current owner in original storm order. Unique rewards remain automatic; Shrine uses original card controls.',
detail: 'Free entry selects an owned marker separately from physical groups, with a distinct event fence and persistent provisional guidance. Private backs and readable authorized faces, optional inspection/reveal, owned stash discard, ordered Great Maker, free entry and carried movement use existing controls. Jacurutu offers exact physical readings only to the defeated owner before settlement. Testing Station offers cumulative legal distances to each current owner in original storm order. Unique rewards remain automatic; Shrine uses original card controls.',
evidence: ['components/discoveries.tsx', 'components/jacurutu-dial.tsx', 'components/discovery-entry.tsx', 'components/discovery-flight-movement.tsx', 'components/discovery-storm.tsx', 'components/truthtrance.tsx', 'components/great-maker.tsx', 'components/spice-card-inspector.tsx', 'components/game-table.tsx'],
},
{
area: 'AI', status: 'Partial',
detail: 'Four profiles share private quoted inspection, discard, vote, ride, entry and carried-flight actions. Properly configured policies select a legal Jacurutu physical reading and each current Testing Station owner uses the same cumulative controls. Shrine uses original Karama/Truthtrance policies. Strategy, combined games and calibration remain unfinished.',
detail: 'Minimal free-entry policies include the owned offered marker through the shared selection adapter. Four profiles share private quoted inspection, discard, vote, ride, entry and carried-flight actions. Properly configured policies select a legal Jacurutu physical reading and each current Testing Station owner uses the same cumulative controls. Shrine uses original Karama/Truthtrance policies. Strategy, combined games and calibration remain unfinished.',
evidence: ['game/discovery-options.ts', 'game/discovery-entry-options.ts', 'game/discovery-flight-options.ts', 'game/discovery-storm-options.ts', 'game/shrine.ts', 'game/great-maker-options.ts', 'game/bots.ts'],
},
{
@@ -5768,7 +5769,7 @@ export const RULE_TOPICS: RuleTopic[] = [
{
area: 'Verification', status: 'Partial',
detail: 'Independent Discovery adds frozen15-file118/118, types/lint/build and six original-action/rendered-control programmes; broader modes, visual/deployed acceptance and final assurance remain open. Current Collection batch passes80/80 in6 related files, types/lint and build. Genuine Basic/Advanced cases cover shared equal/full/zero splits, acceptance timing, wrong actors and stale events; helper boundaries cover multiple deposits, exhausted recipients, self/zero-share exclusion and cumulative overflow. Actual390px Basic Red Chasm lot4 changes Ecaz7/Guild8 to8/11 with one transfer and Ready restored, no overflow or errors. Four legal policies complete that Basic allocation via actual actions. Earlier canonical-source and Jacurutu/Station evidence remains source-bound; combinations, assurance, strategy and deployed acceptance remain open.',
evidence: ['tests/fixture-discovery-no-field-blow.ts', 'tests/discovery-no-field-blow-engine.test.ts', 'tests/discovery-no-field-blow-preflight.test.ts', 'tests/independent-discovery-nexus-entry.test.ts', 'tests/independent-discovery-cards.test.ts', 'tests/independent-discovery-battles.test.ts', 'tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/homeworld-paired-nexus-runtime.test.ts', 'tests/homeworld-standalone-nexus-runtime.test.ts',
evidence: ['tests/fixture-discovery-no-field-entry.ts', 'tests/discovery-no-field-entry-engine.test.ts', 'tests/discovery-no-field-entry-quote.test.ts', 'tests/discovery-no-field-entry-controls.test.tsx', 'tests/fixture-discovery-no-field-blow.ts', 'tests/discovery-no-field-blow-engine.test.ts', 'tests/discovery-no-field-blow-preflight.test.ts', 'tests/independent-discovery-nexus-entry.test.ts', 'tests/independent-discovery-cards.test.ts', 'tests/independent-discovery-battles.test.ts', 'tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/homeworld-paired-nexus-runtime.test.ts', 'tests/homeworld-standalone-nexus-runtime.test.ts',
'tests/fixture-jacurutu-mixed-dial.ts', 'tests/jacurutu-mixed-dial-runtime.test.ts', 'tests/fixture-testing-station-contest.ts', 'tests/testing-station-contest-runtime.test.ts',
'tests/fixture-discovery-occupied-collection.ts', 'tests/discovery-occupied-collection-runtime.test.ts', 'tests/fixture-discovery-shared-orgiz.ts', 'tests/discovery-shared-orgiz-runtime.test.ts',
'tests/board-resolution-quote.test.ts', 'tests/ecaz-collection-quote.test.ts',