diff --git a/README.md b/README.md index c3f3be24..df628ba4 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,10 @@ The single physical Hajr can be played again after legitimate Advanced still requires a fresh post-movement opportunity. Advanced Family Atomics is available after turn1, after storm distance is known and before movement, from physical fighters on/adjacent to the Wall with no -intervening storm. Its used card appears in **Removed from game**. Basic keeps +intervening storm. A concealed No-Field counts as one for this qualification, +even0; a marker on the Wall provisionally reveals before its actual forces +are destroyed. An adjacent marker stays concealed. The used Advanced card +appears in **Removed from game**. Basic keeps its current Storm/discard procedure. See [source and scope](docs/FAMILY_ATOMICS.md). After Guild Cunning, either physical Ornithopter mode may provisionally replace an earned unused Hajr movement. Cunning itself adds no ordinary move; abandoned diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 83adbef4..ed5e7be8 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -20,6 +20,43 @@ open mode gates. Keep identified Basic-function gaps ahead of Advanced and wider interactions. Prototype labels and final acceptance gates remain. +**Family Atomics / No-Field — 27 October. Prototyped / Partial.** +The public controlled marker now supplies one qualifying force, including0, +through shared native/view/card legality and the unchanged Basic territory or +Advanced direct-sector test. Hidden denomination/reserves never decide permission. +Wall destruction now uses the original `atomics` reveal cause: materialize only +available reserves and send actual Wall forces to Tanks. Adjacent markers stay +concealed with the same event. Forced destructive reveal is explicit provisional +source composition; marker-as-one is printed E2 FAQ10/supplied physical36 B. +Basic timing/discard and Advanced timing/removal remain. +[Source and remaining scope](FAMILY_ATOMICS.md#concealed-no-field--27-october-2026). + +Frozen **8-file union, 66/66**, types/lint/build and six original-game programmes +pass. Genuine public Richese/Atreides/Emperor +CHOAM-deck setup supplies original +cards/tokens. Actual marker shipments cost2 without reserve debit; first dials +0+0 or9+0 and the complete first turn earn actual Storm2. Runtime records +17 GameTable SSR, 5 public removed-pile renders and 5 actual configured +HandBrowser card-render Play callbacks. Hand selection/navigation and browser +hydration were not operated. Recorded source/further arrays total273/25. + +Advanced Wall0 reveals without invented losses; Wall5 materializes/dies once; +adjacent3 stays concealed; Storm10 blocks adjacent0; Basic0 uses its original +pre-distance timing/discard. An originally dealt Emperor cost with its actual +paid Basin9 fighter also destroys Richese Wall5. Ten new helper/native cases +include all0/3/5 values and four legal minimal Storm policies. No played hand, +marker, purse, army, phase, distance or casualty is installed. Other Storm and +response packets use native actions directly. A fixture compared the consumed +shipment-offer event with its committed deployment; that assertion was corrected. +The native case also caught the missing No-Field field in the dispatcher quote. + +Proof: `/home/quintine/.local/share/dune/checkpoints/20261027-atomics-no-field-proof-v1/`; +journeys/HTML: adjacent `20261027-atomics-no-field-runtime/run-v1/`. +All checks use one frozen source, followed only by a recorded three-document +evidence overlay. The existing server returned HTTP200 without restart. No +saved-game/database operation. Exhausted-reserve native journeys, broader +combinations, complete modes and final rules/recovery/privacy/browser/deployed +acceptance remain due; this does not certify the full goal. + **Sapho in ordered Advanced Charity — 27 October. Prototyped / Partial.** Sapho now takes its holder first/last among remaining Advanced Charity turns. Actual use creates version2 order history; untouched version1 frames retain @@ -80,9 +117,9 @@ Proof: `/home/quintine/.local/share/dune/checkpoints/20261027-family-atomics-pro HTML/journeys: adjacent `20261027-family-atomics-runtime/run-v2/`. Passed union/disposal/types/lint carry from v1 under identical source bytes; v2 repairs only the private hand-renderer capture. The existing server returned -HTTP200 without restart. No saved-game/database operation. Concealed No-Field -qualification, wider interactions and final full-mode/recovery/privacy/browser/ -deployed acceptance remain. +HTTP200 without restart. No saved-game/database operation. The later No-Field follow-up above supplies +qualification and provisional destructive reveal. Wider interactions and final +full-mode/recovery/privacy/browser/deployed acceptance remain. **CHOAM total support income — 27 October. Prototyped / Partial.** Advanced income now adds eligible support before rounding down once:3+5 yields4 diff --git a/docs/FAMILY_ATOMICS.md b/docs/FAMILY_ATOMICS.md index 65747923..5740ab8e 100644 --- a/docs/FAMILY_ATOMICS.md +++ b/docs/FAMILY_ATOMICS.md @@ -9,13 +9,44 @@ removal. It also expressly excludes advisors. The source is bound by the pdftotext -f 39 -l 39 -layout UNOFFICIAL_Revised_Dune_Rulebook_v.2.3_web.pdf - ``` +## Concealed No-Field — 27 October 2026 + +A deployed No-Field counts as one force for all effects until it reveals, +including denomination0. This is explicit in the [publisher CHOAM/Richese FAQ, +printed10](https://www.gf9games.com/dune/wp-content/uploads/2021/11/CHOAM-Rulebook-low-res.pdf#page=10) +and the supplied Advanced physical36/printed34 B. Fresh indexed publisher text +corroborated the paragraph; direct publisher-PDF retrieval returned403. The +existing [No-Field source contract](RICHESE_NO_FIELD_RULES.md#verified-lifecycle) +retains the wider component lifecycle. + +Qualification now uses the marker's public controller/location as one force, +independent of its hidden denomination, token inventory and available reserves. +Advanced applies the same direct touching-sector geometry and timing to that +location. Basic retains its original effective-presence territory gate. Advisors +still cannot qualify. A marker controlled by another player, outside the actual +nearby board sectors or no longer deployed supplies no entitlement. + +On the Wall, native Family Atomics reveals the original token, places only its +reserve-capped forces and destroys them with every other Wall group. The existing +`atomics` reveal cause retains physical token history and actual casualty paths. +This forced reveal is **provisional source composition** of all-effects presence +and all-Wall destruction, following the existing destructive No-Field procedure; +it is not a dedicated publisher Family Atomics/No-Field ruling. A0 marker reveals +and supplies no physical casualty. Qualification itself does not reveal a token; +an adjacent marker keeps its original concealment and event. Later ordinary +storm exposure may still reveal it through the normal Storm rules. + +Advanced still removes the used card; Basic still discards it. No additional +payment, privilege, action or saved-game conversion is introduced. Wider +interactions and complete-mode acceptance remain open. + ## Connected Advanced behavior - Play after the first game turn, after movement distance is calculated and before the storm actually moves. Weather Control zero is calculated distance. Native Advanced Storm2 reveals its card immediately at turn opening; the prototype does not invent an earlier quiet/null-distance turn2 boundary. -- Physical fighters must be on the Wall or in an adjacent territory with no +- Fighters or a controlled concealed No-Field must be on the Wall or in an adjacent territory with no storm strictly between their sector and the touching Wall sector. A storm in the originating sector or on the Wall is permitted. Basin11 reaches Wall9 through sector10; a storm10 blocks it. Pasty Mesa/Minor Erg approach Wall8. @@ -46,12 +77,12 @@ storm endpoints and optional waiting for distance, but did not locate a primary Basic card face imposing all four revision requirements. This cutover therefore does not silently import the supplied Advanced revision into Basic. -Physical-fighter qualification is connected. Concealed No-Field qualification -and broader optional-module/card combinations remain interaction scope; the +Physical and concealed-marker qualification are connected. Broader +optional-module/card combinations remain interaction scope; the prototype does not certify full modes. No database migration/reset, conversion of completed plays, recovery/privacy campaign or deployed acceptance is claimed. -## Focused evidence +## Initial source-cutover evidence Frozen **10-file union 82/82 plus one changed disposal case**, types/lint/build and five original-game programmes pass. Actual public Basic/explicit Advanced @@ -74,5 +105,44 @@ Proof: `/home/quintine/.local/share/dune/checkpoints/20261027-family-atomics-pro HTML/journeys: adjacent `20261027-family-atomics-runtime/run-v2/`. Tests/disposal/types/lint carry from v1 under the identical source snapshot; only the private HandBrowser-render capture changed. The existing server returned -HTTP200 without restart. Concealed No-Field qualification, broader interactions, +HTTP200 without restart. That earlier checkpoint left No-Field qualification open; +the later section above supplies the current behavior. Broader interactions, complete modes and final assurance/deployed acceptance remain open. + +## No-Field focused evidence + +The public controlled marker now supplies one qualifying force, including0, +through shared native/view/card legality and the unchanged Basic territory or +Advanced direct-sector test. Hidden denomination/reserves never decide permission. +Wall destruction now uses the original `atomics` reveal cause: materialize only +available reserves and send actual Wall forces to Tanks. Adjacent markers stay +concealed with the same event. Forced destructive reveal is explicit provisional +source composition; marker-as-one is printed E2 FAQ10/supplied physical36 B. +Basic timing/discard and Advanced timing/removal remain. +[Source and remaining scope](FAMILY_ATOMICS.md#concealed-no-field--27-october-2026). + +Frozen **8-file union, 66/66**, types/lint/build and six original-game programmes +pass. Genuine public Richese/Atreides/Emperor +CHOAM-deck setup supplies original +cards/tokens. Actual marker shipments cost2 without reserve debit; first dials +0+0 or9+0 and the complete first turn earn actual Storm2. Runtime records +17 GameTable SSR, 5 public removed-pile renders and 5 actual configured +HandBrowser card-render Play callbacks. Hand selection/navigation and browser +hydration were not operated. Recorded source/further arrays total273/25. + +Advanced Wall0 reveals without invented losses; Wall5 materializes/dies once; +adjacent3 stays concealed; Storm10 blocks adjacent0; Basic0 uses its original +pre-distance timing/discard. An originally dealt Emperor cost with its actual +paid Basin9 fighter also destroys Richese Wall5. Ten new helper/native cases +include all0/3/5 values and four legal minimal Storm policies. No played hand, +marker, purse, army, phase, distance or casualty is installed. Other Storm and +response packets use native actions directly. A fixture compared the consumed +shipment-offer event with its committed deployment; that assertion was corrected. +The native case also caught the missing No-Field field in the dispatcher quote. + +Proof: `/home/quintine/.local/share/dune/checkpoints/20261027-atomics-no-field-proof-v1/`; +journeys/HTML: adjacent `20261027-atomics-no-field-runtime/run-v1/`. +All checks use one frozen source, followed only by a recorded three-document +evidence overlay. The existing server returned HTTP200 without restart. No +saved-game/database operation. Exhausted-reserve native journeys, broader +combinations, complete modes and final rules/recovery/privacy/browser/deployed +acceptance remain due; this does not certify the full goal. diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index a169065b..596902e5 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,5 +1,43 @@ # Dune implementation status +## 27 October 2026 — Family Atomics and concealed No-Field + +Prototyped / Partial. The public controlled marker now supplies one qualifying force, including0, +through shared native/view/card legality and the unchanged Basic territory or +Advanced direct-sector test. Hidden denomination/reserves never decide permission. +Wall destruction now uses the original `atomics` reveal cause: materialize only +available reserves and send actual Wall forces to Tanks. Adjacent markers stay +concealed with the same event. Forced destructive reveal is explicit provisional +source composition; marker-as-one is printed E2 FAQ10/supplied physical36 B. +Basic timing/discard and Advanced timing/removal remain. +[Source and remaining scope](FAMILY_ATOMICS.md#concealed-no-field--27-october-2026). + +Frozen **8-file union, 66/66**, types/lint/build and six original-game programmes +pass. Genuine public Richese/Atreides/Emperor +CHOAM-deck setup supplies original +cards/tokens. Actual marker shipments cost2 without reserve debit; first dials +0+0 or9+0 and the complete first turn earn actual Storm2. Runtime records +17 GameTable SSR, 5 public removed-pile renders and 5 actual configured +HandBrowser card-render Play callbacks. Hand selection/navigation and browser +hydration were not operated. Recorded source/further arrays total273/25. + +Advanced Wall0 reveals without invented losses; Wall5 materializes/dies once; +adjacent3 stays concealed; Storm10 blocks adjacent0; Basic0 uses its original +pre-distance timing/discard. An originally dealt Emperor cost with its actual +paid Basin9 fighter also destroys Richese Wall5. Ten new helper/native cases +include all0/3/5 values and four legal minimal Storm policies. No played hand, +marker, purse, army, phase, distance or casualty is installed. Other Storm and +response packets use native actions directly. A fixture compared the consumed +shipment-offer event with its committed deployment; that assertion was corrected. +The native case also caught the missing No-Field field in the dispatcher quote. + +Proof: `/home/quintine/.local/share/dune/checkpoints/20261027-atomics-no-field-proof-v1/`; +journeys/HTML: adjacent `20261027-atomics-no-field-runtime/run-v1/`. +All checks use one frozen source, followed only by a recorded three-document +evidence overlay. The existing server returned HTTP200 without restart. No +saved-game/database operation. Exhausted-reserve native journeys, broader +combinations, complete modes and final rules/recovery/privacy/browser/deployed +acceptance remain due; this does not certify the full goal. + ## 27 October 2026 — Sapho in ordered Advanced Charity Prototyped / Partial. Sapho now takes its holder first/last among remaining Advanced Charity turns. diff --git a/docs/RICHESE_NO_FIELD_RULES.md b/docs/RICHESE_NO_FIELD_RULES.md index b63b282c..280ce521 100644 --- a/docs/RICHESE_NO_FIELD_RULES.md +++ b/docs/RICHESE_NO_FIELD_RULES.md @@ -2,6 +2,21 @@ Audit date: 2026-09-06. This document distinguishes the printed token lifecycle from engine integration and unresolved combinations. It does not certify Richese, Homeworlds, or any other gated game mode. +## Family Atomics qualification and Wall loss — 27 October 2026 + +The printed one-force-for-all-effects rule, including concealed0, now supplies +Family Atomics qualification from the marker's public current location. Advanced +retains its direct intervening-storm geometry and later calculated-distance +window; Basic retains its territory-level effective-presence gate. Neither the +quote nor a play from an adjacent territory reveals a token or places forces. + +A marker on the Wall follows the selected provisional destructive composition: +reveal the actual token, materialize no more than actual reserves, then destroy +all Wall groups once with the native `atomics` cause. Token history and event +follow the existing reveal path;0 produces no invented counter or casualty. +This is not a separate publisher combined-effect ruling. See the +[Family Atomics source and scope](FAMILY_ATOMICS.md#concealed-no-field--27-october-2026). + ## Planetologist two-origin gathering — 26 October 2026 The concealed marker may supply one of exactly two contributing territories, diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md index 9a024115..755d8610 100644 --- a/docs/RULE_DECISIONS.md +++ b/docs/RULE_DECISIONS.md @@ -1,5 +1,17 @@ # Rule decision index +## Family Atomics and concealed No-Field — 27 October 2026 + +Publisher E2 FAQ printed10 and supplied physical36 B count the concealed marker +as one force for all effects, even0. Qualify from its actual public controller/ +location with the existing Basic territory or Advanced direct-sector test; +never read denomination or reserves for permission. Under the standing recommended +interpretation, a Wall marker follows the existing destructive `atomics` reveal, +reserve-capped materialization and native whole-Wall loss. That forced reveal is +provisional composition, not a specific publisher combined-effect answer. +Adjacent markers stay concealed until an independent native reveal trigger. +Existing Advanced removal and Basic discard remain. [Current scope](FAMILY_ATOMICS.md#concealed-no-field--27-october-2026). + ## Sapho in ordered Advanced Charity — 27 October 2026 The printed first/last alternative applies to the supplied rulebook's newly @@ -21,7 +33,8 @@ physical fighters and the Wall. Endpoints may be stormed; advisors cannot qualify. Advanced use removes the physical card outside discard. Basic retains its current procedure after the earlier bounded primary comparison found no verified Basic card face imposing the full revision. Direct touching-sector -intervals implement the geometry; concealed No-Field composition remains open. +intervals implement the geometry. The later No-Field composition above supplies +marker qualification and provisional destructive revelation. No already-completed play is converted. [Source and scope](FAMILY_ATOMICS.md). ## CHOAM total support rounding — 27 October 2026 diff --git a/game/card-availability.ts b/game/card-availability.ts index d1bace73..3cebce10 100644 --- a/game/card-availability.ts +++ b/game/card-availability.ts @@ -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; advisors?: Record; + /** Public effective presence only; no token identity, denomination or reserves. */ + noField?: { deployed: Pick | null } | null; hand?: readonly Card[]; movesAllowed?: number; moved?: number; diff --git a/game/engine.ts b/game/engine.ts index e2cfaa56..693aa2f9 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -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( diff --git a/game/family-atomics.ts b/game/family-atomics.ts index 427f423d..4f58b2ea 100644 --- a/game/family-atomics.ts +++ b/game/family-atomics.ts @@ -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; +export type FamilyAtomicsPlayer = Pick; 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.'; } diff --git a/game/reference.ts b/game/reference.ts index 8aefa9a7..8984dd91 100644 --- a/game/reference.ts +++ b/game/reference.ts @@ -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'] }, ], }, { diff --git a/tests/family-atomics-no-field-engine.test.ts b/tests/family-atomics-no-field-engine.test.ts new file mode 100644 index 00000000..d4240aa4 --- /dev/null +++ b/tests/family-atomics-no-field-engine.test.ts @@ -0,0 +1,108 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { applyAction, viewGame, type Action, type Game } from '../game/engine'; +import { botActions } from '../game/bots'; +import type { Difficulty } from '../game/bot-profiles'; +import { actFamilyAtomicsNoField as act, createFamilyAtomicsNoFieldFixture as fixture, + familyAtomicsNoFieldInventory as inventory, familyAtomicsNoFieldPlayer as p, + familyAtomicsNoFieldReload as reload, finishFamilyAtomicsNoFieldStorm as finishStorm } from './fixture-family-atomics-no-field'; + +function reject(game: Game, actor: string, action: Action) { + const before = reload(game); assert.throws(() => applyAction(game, actor, action)); assert.deepEqual(game, before); +} +function assertWallCasualties(f: ReturnType, game: Game) { + const before = p(f.game, f.richese), after = p(game, f.richese), amount = Math.min(f.value, before.reserves); + assert.equal(game.shieldWallDestroyed, true); assert.equal(after.noField!.deployed, null); + assert.notEqual(after.noFieldEvent, f.event); assert.deepEqual(after.noField!.tokens, f.marker!.tokens); + assert.equal(after.noField!.lastShipped, f.token); + assert.equal(after.reserves, before.reserves - amount); assert.equal(after.tanks, before.tanks + amount); + assert.equal(Object.entries(after.forces).some(([key, n]) => key.startsWith('shield_wall:') && n > 0), false); + assert.deepEqual(inventory(game), f.original); +} + +void test('native concealed zero alone qualifies Advanced Atomics and reveals without inventing Wall casualties', () => { + const f = fixture({ value: 0 }); + assert.equal(f.initial.status, 'lobby'); assert.equal(f.setup.status, 'setup'); + assert.equal(f.sourceCounts.setup, f.setupActions.length); assert.equal(f.sourceCounts.firstDials, f.firstDials.length); + assert.equal(f.sourceCounts.shipment, f.shipment.steps.length); assert.equal(f.sourceCounts.source, f.sourceActions.length); + assert.ok(f.shipment.steps.some(step => step.actor === f.richese && step.action.type === 'ship' && step.action.noField === f.token)); + assert.equal(p(f.shipment.before, f.richese).spice - p(f.shipment.after, f.richese).spice, 2); + assert.deepEqual(p(f.game, f.richese).forces, {}); assert.equal(f.game.stormPending, 1); + const game = act(reload(f.game), f.owner, f.action); assertWallCasualties(f, game); + assert.equal(p(game, f.richese).tanks, 0); + assert.deepEqual(game.removedTreachery?.map(card => card.id), [f.card]); + assert.equal(game.discard.some(card => card.id === f.card), false); + assert.equal(game.pendingTreacheryDiscard ?? null, null); + reject(game, f.owner, f.action); +}); + +void test('original concealed three and five materialize once before all real Wall forces enter Tanks', () => { + for (const value of [3, 5] as const) { + const f = fixture({ value }); let game = act(reload(f.game), f.owner, f.action); + assertWallCasualties(f, game); + assert.equal(p(game, f.richese).tanks, value); + assert.deepEqual(game.removedTreachery?.map(card => card.id), [f.card]); + game = finishStorm(game); + assert.equal(p(game, f.richese).tanks, value); assert.equal(p(game, f.richese).noField!.deployed, null); + assert.equal(game.removedTreachery?.filter(card => card.id === f.card).length, 1); + assert.deepEqual(inventory(game), f.original); + } +}); + +void test('adjacent concealed zero three and five qualify without revealing denomination or changing their event', () => { + for (const value of [0, 3, 5] as const) { + const f = fixture({ value, geometry: 'adjacent' }), before = p(f.game, f.richese); + const game = act(reload(f.game), f.owner, f.action), after = p(game, f.richese); + assert.equal(game.shieldWallDestroyed, true); assert.deepEqual(after.noField, f.marker); + assert.equal(after.noFieldEvent, f.event); assert.equal(after.reserves, before.reserves); + assert.equal(after.tanks, before.tanks); assert.deepEqual(after.forces, {}); + const foreign = viewGame(game, 'atreides').players.find(player => player.id === f.richese)!; + assert.deepEqual(foreign.noField!.deployed, { controller: f.richese, + location: { territory: 'imperial_basin', sector: 11 }, effectiveForces: 1 }); + assert.deepEqual(game.removedTreachery?.map(card => card.id), [f.card]); + assert.deepEqual(inventory(game), f.original); + } +}); + +void test('a real intervening Storm blocks adjacent concealed zero before any card cost or marker revelation', () => { + const f = fixture({ value: 0, geometry: 'blocked' }); + assert.deepEqual(f.firstDials.map(step => step.action.amount), [9, 0]); assert.equal(f.game.storm, 10); + reject(f.game, f.owner, f.action); + assert.equal(f.game.shieldWallDestroyed, false); assert.deepEqual(p(f.game, f.richese).noField, f.marker); + assert.equal(p(f.game, f.richese).noFieldEvent, f.event); assert.equal(p(f.game, f.richese).tanks, 0); + assert.ok(p(f.game, f.owner).hand.some(card => card.id === f.card)); + assert.deepEqual(inventory(f.game), f.original); +}); + +void test('Basic marker-only Atomics retains early Storm timing and ordinary physical discard for all printed values', () => { + for (const value of [0, 3, 5] as const) { + const f = fixture({ advanced: false, value }); assert.equal(f.game.stormPending, null); + assert.equal(f.game.semutaPreview, true, 'Retain the actual public Basic Richese normalization marker'); + const game = act(reload(f.game), f.owner, f.action); assertWallCasualties(f, game); + assert.equal(game.discard.filter(card => card.id === f.card).length, 1); + assert.deepEqual(game.removedTreachery ?? [], []); + } +}); + +void test('an actual classic adjacent qualifier detonates another owner concealed Wall marker through native revelation', () => { + const f = fixture({ value: 5, foreignOwner: true }); assert.ok(f.physicalShipment); + assert.equal(f.owner, 'emperor'); assert.equal(p(f.game, f.owner).forces['imperial_basin:9'], 1); + assert.equal(p(f.physicalShipment.before, f.owner).spice - p(f.physicalShipment.after, f.owner).spice, 2); + const game = act(reload(f.game), f.owner, f.action); assertWallCasualties(f, game); + assert.equal(p(game, f.owner).forces['imperial_basin:9'], 1); assert.equal(p(game, f.owner).tanks, 0); + assert.equal(p(game, f.richese).tanks, 5); + assert.deepEqual(game.removedTreachery?.map(card => card.id), [f.card]); + assert.equal(game.discard.some(card => card.id === f.card), false); +}); + +void test('all four minimal profiles confirm and complete actual Storm while an adjacent zero remains concealed', () => { + const f = fixture({ value: 0, geometry: 'adjacent' }); + for (const difficulty of ['Easy', 'Medium', 'Hard', 'Brutal'] satisfies Difficulty[]) { + const view = viewGame(f.game, f.owner); view.players.find(player => player.id === f.owner)!.bot = difficulty; + const choice = botActions(view).find(action => action.type === 'ready'); assert.ok(choice); + let game = act(reload(f.game), f.owner, choice); game = finishStorm(game); + assert.equal(game.phase, 1); assert.equal(game.shieldWallDestroyed, false); + assert.deepEqual(p(game, f.richese).noField, f.marker); assert.equal(p(game, f.richese).noFieldEvent, f.event); + assert.ok(p(game, f.owner).hand.some(card => card.id === f.card)); assert.deepEqual(inventory(game), f.original); + } +}); diff --git a/tests/family-atomics.test.ts b/tests/family-atomics.test.ts index 709ba897..207e7487 100644 --- a/tests/family-atomics.test.ts +++ b/tests/family-atomics.test.ts @@ -13,6 +13,10 @@ function player(forces: Record = { 'shield_wall:8': 1 }, extra: Partial = {}): FamilyAtomicsPlayer { return { id: 'p', faction: 'atreides', forces, ...extra }; } +function concealed(territory = 'shield_wall', sector = 8, controller = 'p') { + return { ...player({}, { faction: 'richese' }), + noField: { deployed: { controller, location: { territory, sector } } } }; +} void test('Basic keeps its original pre-movement territory gate without Advanced turn or calculation requirements', () => { const basic = { phase: 0, shieldWallDestroyed: false }; @@ -61,14 +65,53 @@ void test('a strictly intervening storm blocks the direct sector route without a assert.equal(blocked(state({ storm: 4 }), player({ 'pasty_mesa:5': 1 })), null); }); -void test('BG advisors and a concealed No-Field marker do not supply Advanced physical fighters', () => { +void test('BG advisors cannot qualify while ordinary BG fighters remain eligible', () => { const advisors = player({ 'shield_wall:8': 2 }, { faction: 'beneGesserit', advisors: { shield_wall: {} } }); - assert.match(blocked(state(), advisors)!, /physical fighters/); + assert.match(blocked(state(), advisors)!, /fighters/); assert.match(blocked(state({ advanced: false }), advisors)!, /need forces/); assert.equal(blocked(state(), player({ 'shield_wall:8': 2 }, { faction: 'beneGesserit' })), null); - const concealed = { ...player({}, { faction: 'richese' }), +}); + +void test('an owned concealed No-Field alone counts at the Wall or adjacent sector under the existing geometry', () => { + assert.equal(blocked(state({ storm: 8 }), concealed()), null); + assert.equal(blocked(state(), concealed('pasty_mesa', 5)), null); + assert.equal(blocked(state({ storm: 8 }), concealed('pasty_mesa', 5)), null); + assert.match(blocked(state({ storm: 6 }), concealed('pasty_mesa', 5))!, /intervening storm/); + assert.match(blocked(state({ storm: 10 }), concealed('imperial_basin', 11))!, /intervening storm/); + assert.equal(blocked({ phase: 0, shieldWallDestroyed: false }, concealed()), null); + assert.equal(blocked(state({ advanced: false, turn: 1, storm: 6, stormPending: null }), + concealed('pasty_mesa', 5)), null, 'Basic retains its original territory-level requirement'); +}); + +void test('the public controlled marker supplies one presence without consulting hidden value or reserves', () => { + const publicOnly = concealed(); + assert.equal(blocked(state(), publicOnly), null); + for (const [value, reserves] of [[0, 0], [3, 1], [5, 20]]) { + // Extra private payload is irrelevant to this public marker-as-one quote. + const supplemented = { ...publicOnly, reserves, + noField: { deployed: { ...publicOnly.noField.deployed, value } } }; + const before = structuredClone(supplemented); + assert.equal(blocked(state(), supplemented), null); + assert.equal(blocked(state({ advanced: false }), supplemented), null); + assert.deepEqual(supplemented, before); + } +}); + +void test('an absent, invalid or foreign-controlled marker cannot qualify or hide ordinary eligible fighters', () => { + const missingController = { ...player({}, { faction: 'richese' }), noField: { deployed: { location: { territory: 'shield_wall', sector: 8 } } } }; - assert.match(blocked(state(), concealed)!, /physical fighters/); + const invalid: FamilyAtomicsPlayer[] = [ + concealed('shield_wall', 8, 'foreign'), concealed('shield_wall', 8, ''), + concealed('shield_wall', 0), concealed('pasty_mesa', 9), concealed('arsunt', 9), + player({}), player({}, { noField: null }), player({}, { noField: { deployed: null } }), + missingController as unknown as FamilyAtomicsPlayer, + ]; + for (const own of invalid) { + assert.match(blocked(state(), own)!, /fighters/); + assert.match(blocked(state({ advanced: false }), own)!, /need forces/); + assert.equal(blocked(state(), { ...own, forces: { 'shield_wall:8': 1 } }), null); + assert.equal(blocked(state({ advanced: false }), { ...own, forces: { 'shield_wall:8': 1 } }), null); + } }); void test('remote presence, invalid physical locations and nonpositive counters cannot establish the sector source', () => { @@ -76,7 +119,7 @@ void test('remote presence, invalid physical locations and nonpositive counters { 'shield_wall:8:extra': 1 }, { 'shield_wall:8': 0 }, { 'shield_wall:8': -1 }, { 'shield_wall:8': Number.NaN }]; for (const forces of invalidForces) - assert.match(blocked(state(), player(forces))!, /physical fighters/); + assert.match(blocked(state(), player(forces))!, /fighters/); }); void test('one unblocked owned fighter source is enough and quoting never changes state or forces', () => { diff --git a/tests/fixture-family-atomics-no-field.ts b/tests/fixture-family-atomics-no-field.ts new file mode 100644 index 00000000..cfef364a --- /dev/null +++ b/tests/fixture-family-atomics-no-field.ts @@ -0,0 +1,146 @@ +import assert from 'node:assert/strict'; +import { createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine'; +import { treacheryDeck } from '../game/cards'; +import { richeseCards } from '../game/richese-cards'; +import { traitorDeck } from '../game/traitors'; +import type { NoFieldValue } from '../game/richese-no-field'; +import { actFamilyAtomics, familyAtomicsPlayer, familyAtomicsReload } from './fixture-family-atomics'; +import { nextRicheseStrongholdsNativeStep } from './fixture-richese-strongholds'; +import type { StrongholdFactionsNativeStep } from './fixture-stronghold-factions'; + +export type FamilyAtomicsNoFieldStep = StrongholdFactionsNativeStep; +export const actFamilyAtomicsNoField = actFamilyAtomics; +export const familyAtomicsNoFieldPlayer = familyAtomicsPlayer; +export const familyAtomicsNoFieldReload = familyAtomicsReload; +export const familyAtomicsNoFieldClean = (game: Game) => !game.phaseOpening && !game.response && + !game.decision && !game.truthtrance && !game.pendingTreacheryDiscard && !game.pendingShipment; +export const nextFamilyAtomicsNoFieldStep = nextRicheseStrongholdsNativeStep; +export function advanceFamilyAtomicsNoField(game: Game, until: (state: Game) => boolean, + actions: FamilyAtomicsNoFieldStep[] = []): Game { + for (let count = 0; count < 1500; count++) { + if (until(game)) return game; + assert.notEqual(game.status, 'finished', 'The original marker chronology ended before its requested boundary'); + const step = nextFamilyAtomicsNoFieldStep(game); assert.ok(step, 'Complete the actual native pending control'); + game = actFamilyAtomicsNoField(game, step.actor, step.action, actions); + } + throw Error('The original Atomics/No-Field source did not reach its finite boundary'); +} +export function finishFamilyAtomicsNoFieldStorm(game: Game, actions: FamilyAtomicsNoFieldStep[] = []): Game { + return advanceFamilyAtomicsNoField(game, state => state.phase !== 0 && familyAtomicsNoFieldClean(state), actions); +} +export function familyAtomicsNoFieldInventory(game: Game) { + const cards = [...game.deck, ...game.discard, ...(game.removedTreachery ?? []), ...(game.richeseCache ?? []), + ...(game.richeseRemoved ?? []), ...game.players.flatMap(player => player.hand), + ...(game.auction?.cards.slice(game.auction.index + Number(game.currentAuctionSale?.origin === 'normal')) ?? [])] + .map(card => card.id).sort(); + assert.deepEqual(cards, [...treacheryDeck(game.expansions), ...richeseCards()].map(card => card.id).sort()); + const traitors = [...(game.traitorReserve ?? []), ...game.players.flatMap(player => + [...player.traitors, ...player.traitorChoices])].sort(); + assert.deepEqual(traitors, traitorDeck(game.players, false).sort()); + const spice = [...game.spiceDeck, ...game.spiceDiscard.flat()].map(card => JSON.stringify(card)).sort(); + for (const player of game.players) + assert.equal(player.reserves + player.tanks + Object.values(player.forces).reduce((sum, amount) => sum + amount, 0), 20); + return { cards, traitors, spice }; +} + +/** Fresh public Richese/classics with no optional modules. A real single-token + * shipment supplies the only Richese Arrakis presence. The hidden denomination + * is selected through the owned native packet, never installed into a marker. + */ +export function createFamilyAtomicsNoFieldFixture(options: { + advanced?: boolean; value?: NoFieldValue; geometry?: 'wall' | 'adjacent' | 'blocked'; foreignOwner?: boolean; +} = {}) { + const advanced = options.advanced ?? true, value = options.value ?? 0; + const geometry = options.geometry ?? 'wall', foreignOwner = options.foreignOwner ?? false; + assert.ok(!foreignOwner || geometry === 'wall', 'The bounded foreign-cost source destroys a Wall marker'); + const richese = 'richese', owner = foreignOwner ? 'emperor' : richese; + let game = createGame('ATOMICSNOFIELD', newPlayer(richese, 'Richese', 'richese'), advanced, ['choam']); + joinGame(game, newPlayer('atreides', 'Atreides', 'atreides')); + joinGame(game, newPlayer('emperor', 'Emperor', 'emperor')); + const initial = structuredClone(game), actions: FamilyAtomicsNoFieldStep[] = []; + const act = (actor: string, action: Action) => { game = actFamilyAtomicsNoField(game, actor, action, actions); }; + const reach = (until: (state: Game) => boolean) => { game = advanceFamilyAtomicsNoField(game, until, actions); }; + for (const player of game.players) act(player.id, { type: 'ready' }); + const lobby = structuredClone(game); + act(game.host, advanced ? { type: 'start', advancedPreview: true } : { type: 'start' }); + const setup = structuredClone(game); + assert.equal(game.status, 'setup'); assert.ok(game.players.every(player => !player.hand.length)); + const atomics = game.deck.find(card => card.effect === 'atomics'); + const weather = game.deck.find(card => card.effect === 'weather'); + const ordinary = game.deck.find(card => card.kind === 'projectile'); assert.ok(atomics && weather && ordinary); + const deal = foreignOwner ? [weather, ordinary, atomics] : [atomics, ordinary, weather]; + const dealOrder = deal.map((card, position) => { + const from = game.deck.findIndex(candidate => candidate.id === card.id); assert.ok(from >= position); + [game.deck[position], game.deck[from]] = [game.deck[from], game.deck[position]]; + return { position, from, card: card.id }; + }); + reach(state => state.status === 'playing'); + const afterSetup = structuredClone(game), setupActions = structuredClone(actions); + assert.deepEqual(game.players.flatMap(player => player.hand.map(card => card.id)), deal.map(card => card.id)); + assert.deepEqual(familyAtomicsNoFieldPlayer(game, richese).forces, {}); + const original = familyAtomicsNoFieldInventory(game); + const spiceOrder = [0, 1].map(position => { + const from = game.spiceDeck.findIndex((card, index) => index >= position && 'territory' in card && !card.discovery); + assert.ok(from >= position); const [card] = game.spiceDeck.splice(from, 1); game.spiceDeck.splice(position, 0, card); + return { position, from, card: structuredClone(card) }; + }); + const firstDialsFrom = actions.length; + for (const [index, actor] of game.stormDialers.entries()) + act(actor, { type: 'stormDial', amount: geometry === 'blocked' && index === 0 ? 9 : 0 }); + const firstDials = structuredClone(actions.slice(firstDialsFrom)); + reach(state => state.turn === 1 && state.phase === 5 && familyAtomicsNoFieldClean(state)); + const destination = geometry === 'wall' ? { territory: 'shield_wall', sector: 9 } + : { territory: 'imperial_basin', sector: 11 }; + let shipment: { before: Game; steps: FamilyAtomicsNoFieldStep[]; after: Game } | null = null; + let physicalShipment: { before: Game; steps: FamilyAtomicsNoFieldStep[]; after: Game } | null = null; + let token = '', shipmentEvent = ''; + for (let count = 0; game.phase === 5 && count < 80; count++) { + if (familyAtomicsNoFieldClean(game) && game.active === richese && !familyAtomicsNoFieldPlayer(game, richese).shipped) { + const own = familyAtomicsNoFieldPlayer(game, richese), selected = own.noField!.tokens.find(token => token.value === value); + assert.ok(selected && own.noFieldEvent); token = selected.id; shipmentEvent = own.noFieldEvent; + const before = structuredClone(game), from = actions.length; + act(richese, { type: 'ship', noField: token, event: shipmentEvent, ...destination }); reach(familyAtomicsNoFieldClean); + shipment = { before, steps: structuredClone(actions.slice(from)), after: structuredClone(game) }; + assert.equal(familyAtomicsNoFieldPlayer(before, richese).spice - familyAtomicsNoFieldPlayer(game, richese).spice, 2); + assert.equal(familyAtomicsNoFieldPlayer(before, richese).reserves, familyAtomicsNoFieldPlayer(game, richese).reserves); + continue; + } + if (foreignOwner && familyAtomicsNoFieldClean(game) && game.active === owner && !familyAtomicsNoFieldPlayer(game, owner).shipped) { + const before = structuredClone(game), from = actions.length; + act(owner, { type: 'ship', territory: 'imperial_basin', sector: 9, amount: 1, elite: 0, allyPayment: 0 }); + reach(familyAtomicsNoFieldClean); + physicalShipment = { before, steps: structuredClone(actions.slice(from)), after: structuredClone(game) }; + assert.equal(familyAtomicsNoFieldPlayer(before, owner).spice - familyAtomicsNoFieldPlayer(game, owner).spice, 2); + continue; + } + const step = nextFamilyAtomicsNoFieldStep(game); assert.ok(step); act(step.actor, step.action); + } + assert.ok(shipment); assert.ok(!foreignOwner || physicalShipment); + reach(state => state.turn === 2 && state.phase === 0 && familyAtomicsNoFieldClean(state)); + assert.equal(game.storm, geometry === 'blocked' ? 10 : 1); + assert.equal(game.stormPending, advanced ? 1 : null); + const own = familyAtomicsNoFieldPlayer(game, richese); + assert.deepEqual(own.forces, {}); assert.equal(own.tanks, 0); + assert.equal(own.noField!.deployed!.tokenId, token); + assert.deepEqual(own.noField!.deployed!.location, destination); + const deployedEvent = familyAtomicsNoFieldPlayer(shipment.after, richese).noFieldEvent; + assert.notEqual(deployedEvent, shipmentEvent, 'The accepted native shipment rotates its consumed offer event'); + assert.equal(own.noFieldEvent, deployedEvent, 'Native intervening phases retain the accepted concealed deployment event'); + assert.ok(familyAtomicsNoFieldPlayer(game, owner).hand.some(card => card.id === atomics.id)); + assert.deepEqual(familyAtomicsNoFieldInventory(game), original); + return { game, sourceGame: structuredClone(game), initial, lobby, setup, afterSetup, advanced, value, geometry, + foreignOwner, owner, richese, card: atomics.id, weather: weather.id, token, event: own.noFieldEvent, + destination, marker: structuredClone(own.noField), action: { type: 'card', card: atomics.id } satisfies Action, + shipment, physicalShipment, original, dealOrder, spiceOrder, firstDials, setupActions, actions, + sourceActions: actions, sourceActionCount: actions.length, + sourceCounts: { setup: setupActions.length, firstDials: firstDials.length, shipment: shipment.steps.length, + physicalShipment: physicalShipment?.steps.length ?? 0, source: actions.length }, + sourceDisclosure: `Actual public ${advanced ? 'Advanced preview' : 'Basic'} Richese/Atreides/Emperor +CHOAM-deck setup, original private Traitors and original three No-Field tokens. ` + + 'No Skills, Homeworlds, Nexus Cards or other optional modules. Only original unopened starting-card/land-Spice positions and scalar lotteries selected. ' + + `Richese actually ships its owned ${value} marker for two spice to ${destination.territory}:${destination.sector}; all physical reserves stay in supply while concealed. ` + + (foreignOwner ? 'Emperor separately ships an actual one-counter adjacent Basin9 qualifier for two spice and holds the originally dealt Atomics. ' : 'Richese itself holds the originally dealt Atomics; the marker is its only Arrakis presence. ') + + `Native first dials ${geometry === 'blocked' ? '9+0 earn Storm10' : '0+0 retain Storm1'}, then actual auctions/turns/phases reach original Storm2. ` + + (advanced ? 'Its calculated card1 is automatically revealed by the actual turn opening. ' : 'The original Basic second dial window remains uncalculated. ') + + 'Every count derives from recorded actions. No held hand, purse, physical force, phase, queue, token deployment, marker event, wall state or casualty is installed.' }; +} +export type FamilyAtomicsNoFieldFixture = ReturnType;