Resolve Family Atomics with concealed No-Field forces

This commit is contained in:
Quintine committed 2026-10-27 11:54:38 +11:00
1 parent 28df687bf9
commit 57a59c1663
13 files changed
+511 -24

No files matched your search

+3
View File
@@ -1,4 +1,5 @@
import { familyAtomicsBlock } from './family-atomics';
import type { NoFieldDeployment } from './richese-no-field';
import { harvesterAvailable, type Card } from './cards';
import type { FactionId } from './catalog';
import { choamSaleGholaTiming } from './choam-market-ghola';
@@ -15,6 +16,8 @@ export type OrdinaryCardPlayer = {
faction: FactionId;
forces: Record<string, number>;
advisors?: Record<string, { lockedTurn?: number }>;
/** Public effective presence only; no token identity, denomination or reserves. */
noField?: { deployed: Pick<NoFieldDeployment, 'controller' | 'location'> | null } | null;
hand?: readonly Card[];
movesAllowed?: number;
moved?: number;
+2 -1
View File
@@ -42890,6 +42890,7 @@ function applyActionInner(
faction: p.faction,
forces: p.forces,
advisors: p.advisors,
noField: p.noField,
hand: p.hand,
movesAllowed: movesAllowed(g, p),
hajrUsed: g.hajr.includes(p.id),
@@ -42943,7 +42944,7 @@ function applyActionInner(
g.ready = [];
} else if (c.effect === 'atomics') {
g.shieldWallDestroyed = true;
for (const other of g.players) killTerritory(g, other, 'shield_wall');
for (const other of g.players) killTerritory(g, other, 'shield_wall', Infinity, false, 'atomics');
g.ready = [];
} else
throw new RuleError(
+14 -4
View File
@@ -1,6 +1,7 @@
import { fighterCount, isAdvisor } from './advisors';
import { splitLocation, TERRITORIES } from './board';
import type { OrdinaryCardPlayer } from './card-availability';
import { presenceByLocation } from './force-presence';
export type FamilyAtomicsState = {
advanced?: boolean;
@@ -12,7 +13,7 @@ export type FamilyAtomicsState = {
stormCardWindowOpen?: boolean;
stormResolution?: unknown;
};
export type FamilyAtomicsPlayer = Pick<OrdinaryCardPlayer, 'id' | 'faction' | 'forces' | 'advisors'>;
export type FamilyAtomicsPlayer = Pick<OrdinaryCardPlayer, 'id' | 'faction' | 'forces' | 'advisors' | 'noField'>;
const wall = TERRITORIES.find(territory => territory.id === 'shield_wall')!;
const nearby = TERRITORIES.filter(territory => territory.id === wall.id || territory.neighbors.includes(wall.id));
@@ -24,8 +25,17 @@ const nearby = TERRITORIES.filter(territory => territory.id === wall.id || terri
export function familyAtomicsBlock(state: FamilyAtomicsState, player: FamilyAtomicsPlayer): string | null {
if (state.shieldWallDestroyed || state.phase !== 0 || state.stormCardWindowOpen === false || state.stormResolution)
return 'Play Family Atomics before storm movement.';
// A concealed No-Field is one force for all effects (supplied physical36 B),
// including denomination0. Its public location suffices; never read private
// value, reserve capacity or token inventory merely to qualify this card.
const marker = player.noField?.deployed;
const markerHere = marker?.controller === player.id && nearby.some(territory =>
territory.id === marker.location.territory && territory.sectors.includes(marker.location.sector));
const forces = presenceByLocation({ forces: player.forces,
noField: markerHere ? { deployed: { location: marker!.location } } : null });
const effective = { faction: player.faction, advisors: player.advisors, forces };
if (!state.advanced)
return nearby.some(territory => fighterCount(player, territory.id) > 0)
return nearby.some(territory => fighterCount(effective, territory.id) > 0)
? null : 'You need forces on or adjacent to the Shield Wall.';
if (!Number.isSafeInteger(state.turn) || (state.turn ?? 0) <= 1)
return 'Play Family Atomics after the first game turn.';
@@ -35,7 +45,7 @@ export function familyAtomicsBlock(state: FamilyAtomicsState, player: FamilyAtom
if (!Number.isSafeInteger(state.storm) || (state.storm ?? -1) < 0 || (state.storm ?? 19) > 18)
return 'Family Atomics needs the current storm position.';
const storm = state.storm!;
const qualified = Object.entries(player.forces).some(([key, amount]) => {
const qualified = Object.entries(forces).some(([key, amount]) => {
if (!Number.isSafeInteger(amount) || amount <= 0) return false;
const source = splitLocation(key);
const territory = nearby.find(candidate => candidate.id === source.territory);
@@ -48,5 +58,5 @@ export function familyAtomicsBlock(state: FamilyAtomicsState, player: FamilyAtom
return !(storm > first && storm < last);
});
});
return qualified ? null : 'You need physical fighters on or adjacent to the Shield Wall with no intervening storm.';
return qualified ? null : 'You need fighters on or adjacent to the Shield Wall with no intervening storm.';
}
+4 -4
View File
@@ -2832,19 +2832,19 @@ export const RULE_TOPICS: RuleTopic[] = [
'Destroy the Shield Wall while you have forces on or adjacent to it.',
steps: [
'Basic keeps its current before-movement Storm window, including before or after distance revelation, and ordinary discard after use.',
'Advanced permits play only after the first game turn, once storm movement is calculated and before it moves. You need physical fighters on the Shield Wall or an adjacent territory, with no storm strictly between their sector and the touching Wall sector. Your own sector or the Wall may be in storm. Advisors do not qualify.',
'Advanced permits play only after the first game turn, once storm movement is calculated and before it moves. You need fighters or your concealed No-Field on the Shield Wall or an adjacent territory, with no storm strictly between their sector and the touching Wall sector. Your own sector or the Wall may be in storm. Advisors do not qualify.',
'All forces on the Wall are destroyed. Arrakeen, Carthag and Imperial Basin lose its storm protection for the rest of the game. The destroyed Wall remains rock and may still be entered. Ordinary storm casualties follow actual movement, without a retroactive sweep of an unchanged sector.',
'Advanced removes the used card from the game. Inspect its public face in Removed from game; it cannot be reshuffled or recovered from discard. Basic retains ordinary discard. Already completed older plays keep their recorded outcome.',
'Concealed No-Field qualification and broader card/module interactions remain under audit; this prototype does not certify complete Advanced.',
'A controlled concealed No-Field counts as one force for qualification, even0, using only its public location. A Wall marker provisionally reveals into its actual reserve-capped forces before all Wall casualties; an adjacent marker stays concealed. The forced destructive reveal is source composition, not a separate publisher combined-effect ruling. Broader interactions and complete Advanced remain under audit.',
'A saved play preserves the destroyed wall and its casualties. Recovery does not require your already-destroyed qualifying forces to remain on the wall.',
],
related: ['storm', 'card-weather'],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Advanced timing, direct intervening sectors, physical fighter qualification, Wall casualties and a separate removed-card zone are connected; wider combinations remain.', evidence: ['game/family-atomics.ts', 'game/card-availability.ts', 'game/engine.ts'] },
{ area: 'Implementation', status: 'Partial', detail: 'Advanced timing, direct intervening sectors, physical/marker qualification, provisional forced No-Field Wall reveal/casualties and a separate removed-card zone are connected; wider combinations remain.', evidence: ['game/family-atomics.ts', 'game/card-availability.ts', 'game/engine.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Shared unavailable reasons and public removed-card inspection retain native card controls.', evidence: ['components/game-table.tsx', 'components/removed-treachery.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'Existing minimal Storm confirmation remains legal; required printed-window guides share legality. Optional card strategy remains deferred.', evidence: ['game/bots.ts', 'game/engine.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Adopted Advanced text, Basic boundary, sector examples and removal are explicit.', evidence: ['docs/FAMILY_ATOMICS.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Focused helper, real turn-two engine and removed-pile sources; full modes, combinations and deployed acceptance remain.', evidence: ['tests/family-atomics.test.ts', 'tests/family-atomics-engine.test.ts', 'tests/removed-treachery.test.tsx'] },
{ area: 'Verification', status: 'Partial', detail: 'Focused helper, real turn-two engine and removed-pile sources; full modes, combinations and deployed acceptance remain.', evidence: ['tests/family-atomics.test.ts', 'tests/family-atomics-engine.test.ts', 'tests/fixture-family-atomics-no-field.ts', 'tests/family-atomics-no-field-engine.test.ts', 'tests/removed-treachery.test.tsx'] },
],
},
{