Connect native Discovery setup and nested force paths
This commit is contained in:
1 parent
8d17fab3de
commit
67ce690064
17 files changed
+1379
-39
No files matched your search
@@ -0,0 +1,45 @@
|
||||
import { FACTIONS } from './catalog';
|
||||
import type { FactionModuleProfile } from './faction-module-profile';
|
||||
|
||||
/** Public unused-lobby admission only. The original initializer owns physical
|
||||
* decks, tokens, leader/force inventories and all mandatory setup choices. */
|
||||
export function discoveryModuleProfile(game: FactionModuleProfile): boolean {
|
||||
return !game.discoveries && !game.discoveryStash && !game.greatMaker &&
|
||||
discoveryModeSupported(game);
|
||||
}
|
||||
|
||||
/** The same roster/module contract during play; live Discovery frames are expected. */
|
||||
export function discoveryModeSupported(game: FactionModuleProfile): boolean {
|
||||
if (typeof game.advanced !== 'boolean' || game.discoveryEnabled !== true ||
|
||||
game.players.length < 2 || game.players.length > 6 ||
|
||||
(game.techTokens && game.players.length < 3) || game.expansions.length > 2 ||
|
||||
game.strongholdCards || game.leaderSkills || game.homeworlds || game.nexusCards ||
|
||||
game.ecazTreachery ||
|
||||
game.semutaPreview || game.advancedPreview || game.kullPreview || game.nexusKullPreview ||
|
||||
game.guildBetrayalPreview || game.richeseBetrayalPreview ||
|
||||
game.nexusIxianReplacementPreview || game.nexusIxianBetrayalPreview || game.nexusHarkonnenBetrayalPreview)
|
||||
return false;
|
||||
const e3 = game.expansions.length === 1 && game.expansions[0] === 'ecaz';
|
||||
for (let i = 0; i < game.expansions.length; i++) {
|
||||
const expansion = game.expansions[i];
|
||||
if (e3 ? expansion !== 'ecaz' : expansion !== 'ix' && expansion !== 'choam') return false;
|
||||
for (let j = 0; j < i; j++) if (game.expansions[j] === expansion) return false;
|
||||
}
|
||||
let native = 0, moritani = false, harkonnen = false;
|
||||
for (let i = 0; i < game.players.length; i++) {
|
||||
const player = game.players[i];
|
||||
for (let j = 0; j < i; j++) if (game.players[j].faction === player.faction) return false;
|
||||
let expansion: string | undefined;
|
||||
for (const faction of FACTIONS)
|
||||
if (faction.id === player.faction) { expansion = faction.expansion; break; }
|
||||
if (expansion === undefined) return false;
|
||||
if (expansion !== 'base') {
|
||||
if (e3 ? player.faction !== 'ecaz' && player.faction !== 'moritani'
|
||||
: !game.expansions.includes(expansion)) return false;
|
||||
native++;
|
||||
}
|
||||
if (player.faction === 'moritani') moritani = true;
|
||||
if (player.faction === 'harkonnen') harkonnen = true;
|
||||
}
|
||||
return (!e3 || native === 1) && !(game.advanced && moritani && harkonnen);
|
||||
}
|
||||
+27
-8
@@ -1,5 +1,6 @@
|
||||
import { classicNexusModulesProfile, pairedNexusModulesProfile } from './nexus-module-profile';
|
||||
import { nativeFactionTechProfile } from './faction-module-profile';
|
||||
import { discoveryModeSupported, discoveryModuleProfile } from './discovery-module-profile';
|
||||
import { advancedMoritaniLeaderSkillsProfile, advancedNativeLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, ordinaryLeaderSkillModeSupported, pairedNexusLeaderSkillsProfile, standaloneE3NexusLeaderSkillsProfile, strongholdLeaderSkillsProfile } from './leader-skill-profile';
|
||||
import { bribeTimingBlock, maximumBribe, type BribeOptions } from './bribe-options';
|
||||
import { quoteSpicePlacement, stormExposesTerritory, stormSectorAfter, wormConsumesForces } from './disaster-rules';
|
||||
@@ -3827,6 +3828,8 @@ function revealPlayerNoField(g: Game, p: Player, cause: NoFieldRevealCause) {
|
||||
p.faction === 'richese' && p.noField?.deployed,
|
||||
'No concealed No-Field is deployed.',
|
||||
);
|
||||
requireRule(validGameLocation(g, p.noField!.deployed!.location.territory, p.noField!.deployed!.location.sector),
|
||||
'A No-Field can reveal only in a current board location.');
|
||||
const result = noFieldRule(() =>
|
||||
revealRicheseNoField(p.noField!, {
|
||||
tokenId: p.noField!.deployed!.tokenId,
|
||||
@@ -7607,6 +7610,9 @@ function e3StrongholdFactionProfile(g: Game): boolean {
|
||||
return native !== null && (native !== 'moritani' || !byFaction(g, 'harkonnen'));
|
||||
}
|
||||
function moritaniAssassinateModeSupported(g: Game) {
|
||||
if (g.discoveryEnabled || g.discoveries)
|
||||
return g.advanced && !!g.discoveries && discoveryModeSupported(g) &&
|
||||
g.players.some(player => player.faction === 'moritani');
|
||||
return g.advanced && g.expansions.length === 1 && g.expansions[0] === 'ecaz' &&
|
||||
g.players.some(p => p.faction === 'moritani') &&
|
||||
g.players.every(p => p.faction === 'moritani' || (p.faction !== 'harkonnen' && CLASSIC_FACTIONS[p.faction] === true)) &&
|
||||
@@ -7642,7 +7648,8 @@ function moritaniAssassinateIntegrity(g: Game) {
|
||||
for (const receipt of state.opportunities) {
|
||||
const opponent = getPlayer(g,receipt.opponent);
|
||||
requireRule(receipt.turn <= g.turn && opponent.faction === receipt.faction &&
|
||||
receipt.owner === state.owner && TERRITORIES.some(t => t.id === receipt.territory),
|
||||
receipt.owner === state.owner && (TERRITORIES.some(t => t.id === receipt.territory) ||
|
||||
validGameLocation(g, receipt.territory, 0)),
|
||||
'The assassination history no longer matches its original battle participants.');
|
||||
}
|
||||
const pending = state.opportunities.filter(r => r.stage === 'skills' || r.stage === 'choice');
|
||||
@@ -9496,9 +9503,13 @@ export function initializeNexusKullGameForAudit(state: Game): Game {
|
||||
}
|
||||
/** Prototype-only Discovery setup. Public starts stay gated while remaining effects are connected. */
|
||||
export function initializeDiscoveryGameForAudit(state: Game): Game {
|
||||
requireRule(state.discoveryEnabled === true && !state.discoveries && !state.discoveryStash && !state.greatMaker,
|
||||
'Enable Discoveries in a fresh audit lobby first.');
|
||||
return initializeSetupGameForAudit(state, false, false, false, true);
|
||||
requireRule(discoveryModuleProfile(state), 'Discovery requires a fresh supported classic, E1/E2 or standalone E3 lobby without unrelated overlays.');
|
||||
requireFreshBaseRuntime(state);
|
||||
requireFreshFactionInventory(state);
|
||||
const g = initializeSetupGameForAudit(state, false, false, false, true, false, false, state.expansions.length > 0);
|
||||
if (byFaction(g, 'ecaz')) g.ecazOccupyPreview = true;
|
||||
if (g.advanced && byFaction(g, 'moritani')) initializeMoritaniAssassinateState(g);
|
||||
return g;
|
||||
}
|
||||
/** Gated development setup; never dispatched by a player action or room route. */
|
||||
export function initializeLeaderSkillsGameForAudit(state: Game): Game {
|
||||
@@ -9544,9 +9555,10 @@ function initializeSetupGameForAudit(state: Game, homeworlds: boolean, nexus = f
|
||||
const skillStronghold = leaderSkills && strongholdLeaderSkillsProfile(g);
|
||||
const skillTech = leaderSkills && (classicTechLeaderSkillsProfile(g) || nativeTechLeaderSkillsProfile(g) || skillStronghold);
|
||||
const nativeTech = factions && !leaderSkills && nativeFactionTechProfile(g);
|
||||
const discoveryComposition = discovery && discoveryModuleProfile(g);
|
||||
const nexusModules = nexus && (classicNexusModulesProfile(g) || pairedNexusModulesProfile(g) ||
|
||||
(leaderSkills && (classicNexusLeaderSkillsProfile(g) || pairedNexusLeaderSkillsProfile(g) || standaloneE3NexusLeaderSkillsProfile(g))));
|
||||
requireRule(!g.techTokens || ((skillTech || nativeTech || nexusModules) &&
|
||||
requireRule(!g.techTokens || ((skillTech || nativeTech || nexusModules || discoveryComposition) &&
|
||||
JSON.stringify(g.techTokens) === JSON.stringify(createTechTokens())),
|
||||
'Tech Tokens require a fresh supported three-through-six-seat lobby with unused tokens.');
|
||||
requireRule(!g.strongholdCards || strongholdFactions || ((skillStronghold || nexusModules) &&
|
||||
@@ -9560,7 +9572,7 @@ function initializeSetupGameForAudit(state: Game, homeworlds: boolean, nexus = f
|
||||
(discovery || !g.discoveryEnabled) &&
|
||||
(nexus || !g.nexusCards) &&
|
||||
(ecazTreachery || !g.ecazTreachery) &&
|
||||
(!g.techTokens || skillTech || nativeTech || nexusModules) &&
|
||||
(!g.techTokens || skillTech || nativeTech || nexusModules || discoveryComposition) &&
|
||||
(strongholdFactions || skillStronghold || nexusModules || !g.strongholdCards) &&
|
||||
(homeworlds || !g.homeworlds) &&
|
||||
g.players.every((p) =>
|
||||
@@ -9580,6 +9592,8 @@ function initializeSetupGameForAudit(state: Game, homeworlds: boolean, nexus = f
|
||||
? 'The Ix prototype supports base, Ixian and Tleilaxu factions without optional modules.'
|
||||
: homeworlds
|
||||
? 'The Homeworld setup audit supports implemented deck sets without Tech Tokens or Stronghold Cards.'
|
||||
: discoveryComposition
|
||||
? 'Discovery supports fresh classic, selected E1/E2 or standalone E3 factions with optional original Tech and no other overlays.'
|
||||
: 'The audit initializer supports base factions without expansions or optional modules.',
|
||||
);
|
||||
initializeSetup(g, strongholdFactions || !!g.strongholdCards);
|
||||
@@ -24719,8 +24733,13 @@ function validatePhysicalShipment(g: Game, shipment: PendingShipment) {
|
||||
validateAmbassadorShipmentOrder(g, shipment);
|
||||
return;
|
||||
}
|
||||
// Concealed and allied No-Fields retain their separate custody/quote contracts.
|
||||
if (shipment.noField || shipment.alliedNoField) return;
|
||||
// A marker's effective one-force declaration is not a reserve withdrawal.
|
||||
// Its separate custody contract still requires a currently revealed destination.
|
||||
if (shipment.noField || shipment.alliedNoField) {
|
||||
requireRule(validGameLocation(g, shipment.territory, shipment.sector),
|
||||
'A No-Field shipment requires a current board destination.');
|
||||
return;
|
||||
}
|
||||
const p = getPlayer(g, shipment.player);
|
||||
requireRule(
|
||||
g.status === 'playing' &&
|
||||
|
||||
+6
-5
@@ -4079,13 +4079,14 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
summary: 'A development prototype connects Great Maker, six Discovery spice blows, private token inspection, stash rewards, revealed locations, next-turn entry, Cistern income, bounded Orgiz transfers, Ornithopter movement, Jacurutu income, Testing Station and Shrine. Remaining interpretations and normal public starts stay gated.',
|
||||
searchText: 'Hiereg Smuggler Jacurutu Sietch Cistern Ecological Testing Station Shrine Orgiz Processing Station Treachery Card Stash Spice Stash Ornithopter',
|
||||
steps: [
|
||||
'The Discovery prototype starts through genuine base-faction setup in Basic or Advanced, with seven extra Spice Cards and eight physical tokens. Its normal public start gate stays closed while the remaining effects are connected and verified.',
|
||||
'Fresh Discovery development setup supports classic, selected Ixian/Tleilaxu/CHOAM/Richese families, or standalone Ecaz or Moritani with classic opponents in Basic or Advanced, with seven extra Spice Cards and eight tokens. Original unused Tech Tokens may be selected with three through six seats. Other modules, paired or mixed Ecaz/Moritani families and normal public starts stay gated.',
|
||||
'Great Maker first resolves the ordinary worm consequences. All players then vote in storm order on creating a Nexus. A strict majority is required; a tie adds no Nexus and an existing Nexus in the phase stays recorded. Fremen may then ride with actual reserve forces to one legal destination for free, without using ordinary shipment or movement. Storm and occupancy restrictions still apply.',
|
||||
'Each of the six Discovery territory cards destroys prior spice and forces in the named blow territory, including Fremen forces, before placing six new spice subject to the storm. It also places a random available face-down token of the printed type at the card’s separate token destination.',
|
||||
'Fremen privately see placed Hiereg tokens; Guild privately sees placed Smuggler tokens. During Spice Collection, non-advisor forces in a token’s surrounding territory may inspect it privately, then choose whether to reveal it. The Discovery panel shows only authorized faces. Reading an already visible token’s rules does not reveal or use it.',
|
||||
'Spice Stash grants seven bank spice and leaves play. Treachery Card Stash draws one private Treachery Card and leaves play; if the new hand is too large, choose any held card to discard, including the newly drawn card. The owned discard panel includes a card inspector.',
|
||||
'Revealing Ornithopter records who carries it and the turn it was gained. On a later turn, its owner may spend it on one ordinary movement action to give that selected group a fixed range of three. It grants no extra action, adds no shipment and is removed only after the movement commits; saved arrival interactions retain the exact selected group.',
|
||||
'The five revealed location tokens become separate, initially empty territories inside their surroundings. A private peek does not open the location. Normal ground movement enters through the surrounding territory, shipment uses stronghold prices, at most two factions may occupy it, and forces inside are protected from storms and sandworms. Only Jacurutu Sietch counts toward stronghold victory.',
|
||||
'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.',
|
||||
'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.',
|
||||
'A sole occupant of Cistern receives two bank spice during Collection; contested occupancy withholds that bonus without blocking the phase. A sole Orgiz occupant takes one spice from each rival who collected a positive board deposit, using each collected pile as one observable blow. This is a provisional interpretation of the printed blow wording. Contested Orgiz occupancy and unresolved Ecaz shared lots withhold only the uncertain transfer while ordinary collection continues.',
|
||||
'Jacurutu automatically pays one bank spice for each opposing undialed physical force sent to the Tanks. Where Advanced normal/elite allocations would produce different rewards, this prototype records the unpaid gap pending a ruling; it never guesses from the numeric wheel alone.',
|
||||
@@ -4097,8 +4098,8 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
checklist: [
|
||||
{
|
||||
area: 'Implementation', status: 'Partial',
|
||||
detail: 'Working prototypes cover genuine setup, physical inventory, destructive blows, Great Maker vote/ride, private inspection/reveal, stash rewards, public nested locations, signed next-turn entry, sole-occupant Cistern income, bounded Orgiz transfers and later-turn Ornithopter spending, unambiguous Jacurutu income, sole-occupant Testing Station and Shrine card roles. Contested and mixed interpretations remain pending.',
|
||||
evidence: ['game/discoveries.ts', 'game/discovery-actions.ts', 'game/discovery-entry.ts', 'game/discovery-collection.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'],
|
||||
detail: 'Working prototypes cover original classic/E1/E2/standalone E3 setup with optional original Tech, native typed free entry, paid nested No-Field shipment/movement/reveal, CHOAM stash capacity, native Face Dance rewards and Moritani nested-site assassination. Existing destructive blows, Great Maker, Cistern, bounded Orgiz, Ornithopter, unambiguous Jacurutu, sole Testing Station and Shrine remain. Contested and mixed interpretations are pending.',
|
||||
evidence: ['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/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',
|
||||
@@ -4117,8 +4118,8 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
},
|
||||
{
|
||||
area: 'Verification', status: 'Partial',
|
||||
detail: 'Focused offline checks cover physical custody, private views, legal human/AI actions, immutable rejection, board admission, signed free entry, Cistern collection, carried flight, station movement, Jacurutu income, Shrine card custody and saved continuation. Broad checkpoint results and complete module/browser/multiplayer acceptance remain separate gates.',
|
||||
evidence: ['tests/discoveries.test.ts', 'tests/discovery-runtime.test.ts', 'tests/great-maker.test.ts', 'tests/discovery-board.test.ts', 'tests/discovery-admission.test.ts', 'tests/discovery-controls.test.ts', 'tests/discovery-entry.test.ts', 'tests/discovery-entry-runtime.test.ts', 'tests/discovery-entry-controls.test.ts', 'tests/discovery-entry-review.test.ts', 'tests/discovery-collection.test.ts', 'tests/discovery-flight.test.ts', 'tests/discovery-flight-engine.test.ts', 'tests/discovery-flight-recovery.test.ts', 'tests/discovery-battle.test.ts', 'tests/discovery-battle-recovery.test.ts', 'tests/discovery-storm.test.ts', 'tests/discovery-storm-recovery.test.ts', 'tests/discovery-shrine-engine.test.ts', 'tests/discovery-shrine-recovery.test.ts'],
|
||||
detail: 'Focused native runtime cases and actual phone controls cover original setup, typed entry, No-Field nested admission/materialization, stash capacity, Face Dance reward order and original Moritani assassination. Five genuine six-seat Advanced Tech samples finish after repairing and resuming the preserved Moritani profile. Specific changed effects use controlled source programs, not a claim that every sample naturally triggers them. Full Discovery, assurance and deployed acceptance remain open.',
|
||||
evidence: ['tests/discovery-module-profile.test.ts', 'tests/discovery-native-typed-runtime.test.ts', 'tests/discovery-native-marker-runtime.test.ts', 'tests/discovery-native-e3-runtime.test.ts', 'tests/discoveries.test.ts', 'tests/discovery-runtime.test.ts', 'tests/great-maker.test.ts', 'tests/discovery-board.test.ts', 'tests/discovery-admission.test.ts', 'tests/discovery-controls.test.ts', 'tests/discovery-entry.test.ts', 'tests/discovery-entry-runtime.test.ts', 'tests/discovery-entry-controls.test.ts', 'tests/discovery-entry-review.test.ts', 'tests/discovery-collection.test.ts', 'tests/discovery-flight.test.ts', 'tests/discovery-flight-engine.test.ts', 'tests/discovery-flight-recovery.test.ts', 'tests/discovery-battle.test.ts', 'tests/discovery-battle-recovery.test.ts', 'tests/discovery-storm.test.ts', 'tests/discovery-storm-recovery.test.ts', 'tests/discovery-shrine-engine.test.ts', 'tests/discovery-shrine-recovery.test.ts'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MOBILE_STRONGHOLD } from './board';
|
||||
import { isDiscoveryLocationId } from './discoveries';
|
||||
|
||||
export type NoFieldValue = 0 | 3 | 5;
|
||||
export type NoFieldLocation = { territory: string; sector: number };
|
||||
@@ -61,7 +62,8 @@ function validateLocation(location: NoFieldLocation) {
|
||||
(location.sector >= 1 ||
|
||||
(location.sector === 0 &&
|
||||
(location.territory === 'polar_sink' ||
|
||||
location.territory === MOBILE_STRONGHOLD))) &&
|
||||
location.territory === MOBILE_STRONGHOLD ||
|
||||
isDiscoveryLocationId(location.territory)))) &&
|
||||
location.sector <= 18,
|
||||
'Choose a valid planet territory and sector for the No-Field.',
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user