Expose an explicitly unfinished Advanced preview in normal lobbies
Add room-creation and host lobby rules selectors, readiness reset, explicit preview start and a persistent warning. Admit the six classic factions, Tech Tokens with 3+ players and optional Stronghold Cards through genuine setup. Keep expansion/module gates and complete-rules/publication criteria. Persist the preview marker and enforce fresh native leaders, unchanged piece custody and clean runtime/pacing state. Preserve private projections, JSON continuation, exact room-entry retries and version/CAS protection. Independent review resolved malformed leader, extra card-zone, missing required-key and stale bot-deadline admission gaps. Validation on the shared development tree: types, lint, 5169 offline tests, build and 50 HTTP tests passed. The tree also contained the separate deployment task's changes; this commit stages only preview scope. All 57 base rosters completed real setup with each legal AI profile. Five normal-preview-start games (2–6 seats) completed 1950 actions and 51 JSON restores without rejected actions; no strategy calibration. Browser: actual create, host switch/readiness/Stronghold reset, explicit start, exact private refresh, desktop and mobile warning checked. Retained dev process stopped; backed up 1,629 saved games, restored server with all unchanged, and resumed the same traitor choice into Storm. Baseline all-table preservation retained 1626 rooms and 3442 seats; totals now 1669 rooms and 3536 seats. No reset or automation. Preview remains incomplete. Legacy unsupported expansion creation payloads remain accepted for exact retry compatibility but cannot start. Existing rules ambiguities and full visual acceptance remain open.
This commit is contained in:
1 parent
0f79f28ccb
commit
c5508b727a
16 files changed
+998
-115
No files matched your search
+113
-69
@@ -1554,6 +1554,8 @@ export type Game = {
|
||||
mentatHistory?: MentatObservation[];
|
||||
mentatHistoryEvents?: string[];
|
||||
advanced: boolean;
|
||||
/** Explicit normal-lobby opt-in; unfinished rules remain labeled as a preview. */
|
||||
advancedPreview?: true;
|
||||
expansions: string[];
|
||||
players: Player[];
|
||||
/** Fixed printed player circles; absent only in rooms created before this rule fix. */
|
||||
@@ -5082,26 +5084,93 @@ function takeBattleLosses(g: Game, p: Player, t: string, losses: Casualties) {
|
||||
);
|
||||
return lostCyborgs;
|
||||
}
|
||||
function start(g: Game) {
|
||||
function baseStartProfileBlock(g: Game): string | null {
|
||||
if (g.homeworlds) return 'Homeworld gameplay is still being implemented.';
|
||||
if (g.nexusCards) return 'Nexus card effects are still being implemented.';
|
||||
if (g.discoveryEnabled || g.discoveries || g.discoveryStash || g.greatMaker)
|
||||
return 'Discoveries are still being implemented.';
|
||||
if (g.leaderSkills) return 'Leader Skills are still being implemented.';
|
||||
if (g.ecazTreachery) return 'Ecaz Treachery Cards are still being implemented.';
|
||||
if (g.mentatQuestionPreview || g.moritaniAssassinatePreview || g.moritaniAssassinate)
|
||||
return 'Separate development profiles cannot be enabled by a normal start.';
|
||||
if (!g.players.every(p => FACTIONS.some(entry => entry.id === p.faction && entry.expansion === 'base')))
|
||||
return 'Expansion factions are still being implemented.';
|
||||
if (g.expansions.length !== 0) return 'Expansion rules are still being implemented.';
|
||||
return null;
|
||||
}
|
||||
function advancedPreviewIntegrity(g: Game) {
|
||||
if (g.advancedPreview === undefined) return;
|
||||
requireRule(g.advancedPreview === true && g.advanced === true && g.status !== 'lobby' && !baseStartProfileBlock(g),
|
||||
'The saved Advanced preview no longer matches its supported rules profile.');
|
||||
}
|
||||
function requireFreshSetup(g: Game, allowIxElites = false) {
|
||||
requireRule(g.status === 'lobby' && !g.setupStage && g.turn === 1 && g.phase === 0 && g.advancedPreview === undefined,
|
||||
'Start only from a fresh lobby.');
|
||||
requireRule(g.players.length >= 2 && g.players.length <= 6 &&
|
||||
new Set(g.players.map(p => p.id)).size === g.players.length &&
|
||||
new Set(g.players.map(p => p.faction)).size === g.players.length &&
|
||||
g.players.some(p => p.id === g.host) && g.players.every(p => p.id && p.ready),
|
||||
'Start requires two through six distinct ready players and an existing host.');
|
||||
requireRule(!g.deck.length && !g.discard.length && !g.spiceDeck.length &&
|
||||
!g.spiceDiscard.some(pile => pile.length) && !g.traitorReserve?.length &&
|
||||
g.ecazLoyalty === undefined && !g.decision && !g.response && !g.battle && !g.winner.length &&
|
||||
g.players.every(p => !p.hand.length && !p.traitors.length && !p.traitorChoices.length &&
|
||||
Object.keys(p.forces).length === 0 && p.reserves === 20 && p.tanks === 0 && p.spice === 0 &&
|
||||
p.moved === 0 && !p.shipped && !p.prediction && !p.advisorSetup &&
|
||||
(!p.elites || (allowIxElites && p.faction === 'ixians' && p.elites.reserves === 7 &&
|
||||
p.elites.tanks === 0 && p.elites.revived === 0 && Object.keys(p.elites.forces).length === 0))),
|
||||
'Starting setup cannot redeal or replace existing game pieces.');
|
||||
requireRule(g.players.every(p => {
|
||||
const native = leaders(p.faction);
|
||||
return p.leaders.length === native.length && native.every(expected => {
|
||||
const actual = p.leaders.find(leader => leader.id === expected.id);
|
||||
return actual && Object.keys(actual).length === Object.keys(expected).length &&
|
||||
Object.entries(expected).every(([key, value]) => actual[key as keyof Leader] === value);
|
||||
}) && p.ally === null && p.allySinceTurn === undefined && p.bribes === 0 && p.revived === 0 &&
|
||||
(p.freeForcesRevived ?? 0) === 0 && p.leaderRevived === false && p.revivalCycle === 0 && p.battleLosses === 0 &&
|
||||
!p.specialKaramaUsed && p.kwisatz === undefined && p.charityTurn === undefined &&
|
||||
p.noField === undefined && p.noFieldEvent === undefined && p.noFieldBlockedTurn === undefined &&
|
||||
p.advisors === undefined && p.gholaBlocked === undefined && p.ixMovementBlocked === undefined &&
|
||||
p.fremenMovementBlocked === undefined && p.faceDancers === undefined &&
|
||||
p.faceDancerReplacedTurn === undefined && p.revealedTraitors === undefined;
|
||||
}), 'Starting setup requires unused native leaders and no prior alliance, revival or battle history.');
|
||||
requireRule(Object.keys(g.playerPositions ?? {}).every(id => g.players.some(p => p.id === id)),
|
||||
'Player circles must belong to seated players.');
|
||||
normalizedPlayerPositions(g);
|
||||
}
|
||||
function requireFreshBaseRuntime(g: Game) {
|
||||
requireRule((g.botsPending === undefined || g.botsPending === false) && g.botNextActionAt === undefined,
|
||||
'Start with no pending bot work or prior action deadline.');
|
||||
const fresh = createGame(g.code, getPlayer(g, g.host), g.advanced);
|
||||
// These are the live lobby settings and storage metadata. Module admission and
|
||||
// unowned optional components are checked separately; gameplay fields stay fresh.
|
||||
const configuration = new Set<keyof Game>(['code', 'version', 'host', 'players', 'playerPositions',
|
||||
'advanced', 'expansions', 'log', 'botsPending', 'botNextActionAt', 'techTokens', 'strongholdCards',
|
||||
'homeworlds', 'nexusCards', 'discoveryEnabled', 'leaderSkills', 'ecazTreachery']);
|
||||
const keys = [...new Set([...Object.keys(g), ...Object.keys(fresh)])] as (keyof Game)[];
|
||||
requireRule(keys.every(key => configuration.has(key) ||
|
||||
JSON.stringify(g[key]) === JSON.stringify(fresh[key])),
|
||||
'Starting a base table cannot retain existing game resources, histories or continuations.');
|
||||
}
|
||||
function start(g: Game, advancedPreview = false) {
|
||||
requireRule(g.players.length >= 2, 'At least two players are needed.');
|
||||
requireRule(
|
||||
g.players.every((p) => p.ready),
|
||||
'Every player must be ready.',
|
||||
);
|
||||
requireRule(!g.advanced, 'Advanced rules are still being implemented.');
|
||||
requireRule(!g.homeworlds, 'Homeworld gameplay is still being implemented.');
|
||||
requireRule(!g.nexusCards, 'Nexus card effects are still being implemented.');
|
||||
requireRule(!g.discoveryEnabled, 'Discoveries are still being implemented.');
|
||||
requireRule(!g.leaderSkills, 'Leader Skills are still being implemented.');
|
||||
requireRule(
|
||||
g.players.every((p) => faction(p.faction).expansion === 'base'),
|
||||
'Expansion factions are still being implemented.',
|
||||
);
|
||||
requireRule(
|
||||
g.expansions.length === 0,
|
||||
'Expansion rules are still being implemented.',
|
||||
);
|
||||
requireRule(typeof g.advanced === 'boolean' && (!g.advanced || advancedPreview),
|
||||
'Advanced rules are unfinished. Explicitly choose Start Advanced preview to continue.');
|
||||
requireRule(!advancedPreview || g.advanced, 'Choose Advanced preview rules before starting that preview.');
|
||||
const blocked = baseStartProfileBlock(g);
|
||||
requireRule(!blocked, blocked ?? 'This rules profile is unavailable.');
|
||||
requireFreshSetup(g);
|
||||
requireRule(!g.techTokens || JSON.stringify(g.techTokens) === JSON.stringify(createTechTokens()),
|
||||
'Start with unused Tech Tokens before assigning their initial owners.');
|
||||
requireRule(!g.strongholdCards || JSON.stringify(g.strongholdCards) === JSON.stringify(createStrongholdCards()),
|
||||
'Start with unused Stronghold Cards before assigning ownership.');
|
||||
requireFreshBaseRuntime(g);
|
||||
initializeSetup(g);
|
||||
if (advancedPreview) g.advancedPreview = true;
|
||||
}
|
||||
function skillRule<T>(operation: () => T): T {
|
||||
try {
|
||||
@@ -5935,19 +6004,7 @@ function initializeSetupGameForAudit(state: Game, homeworlds: boolean, nexus = f
|
||||
homeworldDefenseIntegrity(state);
|
||||
homeworldShipmentIntegrity(state);
|
||||
const g = structuredClone(state);
|
||||
requireRule(
|
||||
g.status === 'lobby' && !g.setupStage && g.turn === 1 && g.phase === 0,
|
||||
'The audit initializer requires a fresh lobby.',
|
||||
);
|
||||
requireRule(
|
||||
g.players.length >= 2 &&
|
||||
g.players.length <= 6 &&
|
||||
new Set(g.players.map((p) => p.id)).size === g.players.length &&
|
||||
new Set(g.players.map((p) => p.faction)).size === g.players.length &&
|
||||
g.players.some((p) => p.id === g.host) &&
|
||||
g.players.every((p) => p.id && p.ready),
|
||||
'The audit initializer requires two through six distinct ready players and an existing host.',
|
||||
);
|
||||
requireFreshSetup(g, homeworlds || nexus || ix || factions);
|
||||
requireRule(
|
||||
(homeworlds || nexus || ix || choam || factions || g.expansions.length === 0) &&
|
||||
(choam || factions || !g.expansions.includes('choam')) &&
|
||||
@@ -5974,47 +6031,6 @@ function initializeSetupGameForAudit(state: Game, homeworlds: boolean, nexus = f
|
||||
? 'The Homeworld setup audit supports implemented deck sets without Tech Tokens or Stronghold Cards.'
|
||||
: 'The audit initializer supports base factions without expansions or optional modules.',
|
||||
);
|
||||
requireRule(
|
||||
!g.deck.length &&
|
||||
!g.discard.length &&
|
||||
!g.spiceDeck.length &&
|
||||
!g.spiceDiscard.some((pile) => pile.length) &&
|
||||
!g.traitorReserve?.length &&
|
||||
g.ecazLoyalty === undefined &&
|
||||
!g.decision &&
|
||||
!g.response &&
|
||||
!g.battle &&
|
||||
!g.winner.length &&
|
||||
g.players.every(
|
||||
(p) =>
|
||||
!p.hand.length &&
|
||||
!p.traitors.length &&
|
||||
!p.traitorChoices.length &&
|
||||
Object.keys(p.forces).length === 0 &&
|
||||
p.reserves === 20 &&
|
||||
p.tanks === 0 &&
|
||||
p.spice === 0 &&
|
||||
p.moved === 0 &&
|
||||
!p.shipped &&
|
||||
!p.prediction &&
|
||||
!p.advisorSetup &&
|
||||
(!p.elites ||
|
||||
((homeworlds || nexus || ix || factions) &&
|
||||
p.faction === 'ixians' &&
|
||||
p.elites.reserves === 7 &&
|
||||
p.elites.tanks === 0 &&
|
||||
p.elites.revived === 0 &&
|
||||
Object.keys(p.elites.forces).length === 0)),
|
||||
),
|
||||
'The audit initializer cannot redeal or replace existing game pieces.',
|
||||
);
|
||||
requireRule(
|
||||
Object.keys(g.playerPositions ?? {}).every((id) =>
|
||||
g.players.some((p) => p.id === id),
|
||||
),
|
||||
'Player circles must belong to seated players.',
|
||||
);
|
||||
normalizedPlayerPositions(g);
|
||||
initializeSetup(g);
|
||||
return normalizeAutomaticGame(g);
|
||||
}
|
||||
@@ -20722,6 +20738,7 @@ function normalizeCardNames(g: Game) {
|
||||
]);
|
||||
}
|
||||
export function applyAction(state: Game, id: string, action: Action): Game {
|
||||
advancedPreviewIntegrity(state);
|
||||
harkonnenExchangeIntegrity(state);
|
||||
validateEcazLoyalty(state);
|
||||
ecazTreacheryIntegrity(state);
|
||||
@@ -20990,6 +21007,7 @@ function settleAutomaticContinuations(g: Game) {
|
||||
}
|
||||
/** Internal authoritative continuation. Callers must persist with their usual CAS fence. */
|
||||
export function normalizeAutomaticGame(state: Game): Game {
|
||||
advancedPreviewIntegrity(state);
|
||||
harkonnenExchangeIntegrity(state);
|
||||
validateEcazLoyalty(state);
|
||||
ecazTreacheryIntegrity(state);
|
||||
@@ -22647,6 +22665,27 @@ function applyActionInner(
|
||||
return g;
|
||||
}
|
||||
if (g.status === 'lobby') {
|
||||
if (t === 'rules') {
|
||||
requireRule(id === g.host, 'Only the host can change the rules mode.');
|
||||
requireRule(typeof action.advanced === 'boolean' &&
|
||||
Object.keys(action).every(key => key === 'type' || key === 'advanced'),
|
||||
'Choose Basic or Advanced preview rules.');
|
||||
if (g.advanced === action.advanced) return g;
|
||||
if (action.advanced) {
|
||||
const blocked = baseStartProfileBlock(g);
|
||||
requireRule(!blocked, blocked ?? 'This configuration cannot use Advanced preview.');
|
||||
}
|
||||
const removedStrongholds = !action.advanced && !!g.strongholdCards;
|
||||
if (removedStrongholds) {
|
||||
requireRule(JSON.stringify(g.strongholdCards) === JSON.stringify(createStrongholdCards()),
|
||||
'Only unused lobby Stronghold Cards can be cleared when choosing Basic rules.');
|
||||
g.strongholdCards = null;
|
||||
}
|
||||
g.advanced = action.advanced;
|
||||
g.players.forEach(player => (player.ready = !!player.bot));
|
||||
log(g, `${p.name} selected ${g.advanced ? 'Advanced preview' : 'Basic'} rules.${removedStrongholds ? ' Stronghold Cards were disabled because they require Advanced rules.' : ''} Human readiness was cleared; AI seats remain ready.`);
|
||||
return g;
|
||||
}
|
||||
if (t === 'configureBot') {
|
||||
const positions = normalizedPlayerPositions(g);
|
||||
const quote = quoteLobbyBotConfiguration(
|
||||
@@ -22785,7 +22824,10 @@ function applyActionInner(
|
||||
}
|
||||
if (t === 'start') {
|
||||
requireRule(id === g.host, 'Only the host can start the game.');
|
||||
start(g);
|
||||
requireRule(Object.keys(action).every(key => key === 'type' || key === 'advancedPreview') &&
|
||||
(action.advancedPreview === undefined || action.advancedPreview === true),
|
||||
'Start this table, or explicitly choose Advanced preview.');
|
||||
start(g, action.advancedPreview === true);
|
||||
return g;
|
||||
}
|
||||
if (t === 'faction') {
|
||||
@@ -24629,6 +24671,7 @@ function applyActionInner(
|
||||
throw new RuleError('That action is not available.');
|
||||
}
|
||||
export function viewGame(state: Game, id: string) {
|
||||
advancedPreviewIntegrity(state);
|
||||
harkonnenExchangeIntegrity(state);
|
||||
validateEcazLoyalty(state);
|
||||
ecazTreacheryIntegrity(state);
|
||||
@@ -24739,6 +24782,7 @@ export function viewGame(state: Game, id: string) {
|
||||
mentat: projectedMentat(g, id),
|
||||
rihani: projectedRihani(g, id),
|
||||
advanced: g.advanced,
|
||||
advancedPreview: g.advancedPreview === true,
|
||||
dukeVidal: g.dukeVidal
|
||||
? { ...g.dukeVidal, leader: projectLeader(g, g.dukeVidal.leader, id) }
|
||||
: null,
|
||||
|
||||
+12
-12
@@ -198,7 +198,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Verification entries identify focused regression suites. Passing those checks does not certify every interaction, browser journey or complete expansion game.',
|
||||
'Development stages distinguish Missing functions, working Prototypes, Integrated functions, Verified behavior and Polished presentation. A prototype stage does not certify complete rules coverage; check the stated limits and evidence.',
|
||||
'This checklist is being extended across the rules. Features without a checklist still have a topic-level coverage label and are not implicitly complete.',
|
||||
'Advanced and full expansion faction starts remain disabled. Development fixture coverage is not permission to start an unsupported mode.',
|
||||
'Choose Rules → Advanced preview when creating a room, or as host in the lobby. After everyone is ready, choose Begin Advanced preview. This unfinished mode supports the six classic factions, with optional Tech Tokens (3+ players) and Stronghold Cards. Expansion starts remain disabled; preview access does not certify complete rules compliance.',
|
||||
],
|
||||
related: [
|
||||
'setup',
|
||||
@@ -1240,7 +1240,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Before any initial Traitor Cards are drawn, randomly select one of the five ordinary Ecaz Traitor Cards and set it aside face up. Everyone may inspect it. It remains outside the deck for the rest of the game, including later Traitor and Face Dancer draws.',
|
||||
'Loyalty is automatic and cannot be stopped with Karama. It creates no player choice, payment or acknowledgment. The card names a leader, but does not remove, kill, transfer or otherwise change that leader disc. Duke Vidal has no Traitor Card and is never a candidate.',
|
||||
'In new Advanced development games with Ecaz, the table displays the selected card before the Traitor choices. Inspect traitor enlarges its identity; refreshing or resuming the saved game keeps the same card. Basic games and games without native Ecaz do not use this power.',
|
||||
'Older saved games that have already dealt their Traitor Cards retain that inventory; they do not gain a retroactive Loyalty draw. Full Advanced and expansion starts remain gated, including unfinished combinations with Leader Skills and Moritani assassination.',
|
||||
'Older saved games that have already dealt their Traitor Cards retain that inventory; they do not gain a retroactive Loyalty draw. Ecaz Advanced and expansion starts remain gated, including unfinished combinations with Leader Skills and Moritani assassination.',
|
||||
],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'One persisted native Ecaz card is selected before genuine initial dealing, excluded from setup and Nexus deck construction, and validated against all known circulating or retired Traitor locations. Older saves stay unchanged. Broader combinations remain gated.', evidence: ['game/ecaz-loyalty.ts', 'game/engine.ts'] },
|
||||
@@ -1955,7 +1955,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Each player receives four private traitor choices and keeps one. Harkonnen keeps all four automatically. Every remaining selection must finish before starting spice and forces are placed. Your starting Treachery hand is still empty.',
|
||||
'Starting spice and fixed forces are then placed. Fremen distributes ten forces among Sietch Tabr, False Wall South and False Wall West, choosing their printed sectors. Basic Bene Gesserit starts with one fighter in Polar Sink. In Advanced setup, Bene Gesserit instead chooses a printed territory and sector after Fremen placement; its advisor becomes a fighter when alone.',
|
||||
'After all starting force choices, each player receives one private Treachery Card and Harkonnen receives two. Dealing and entry into the first Storm phase are automatic. The table names the player whose setup choice is still required; there is no separate confirmation for dealing.',
|
||||
'Older saved tables that already received their cards keep those cards and their existing unfinished choices. Returning to a table does not restart setup or deal another hand. Advanced and expansion starts remain unavailable while their remaining rules and interactions are completed.',
|
||||
'Older saved tables that already received their cards keep those cards and their existing unfinished choices. Returning to a table does not restart setup or deal another hand. The six classic factions can start Advanced preview from the lobby while remaining rules and interactions are completed. Expansion starts remain unavailable.',
|
||||
'The browser keeps a private seat cookie. Returning to the same room in that browser restores your seat. Separate players should use separate browsers or profiles.',
|
||||
],
|
||||
related: ['privacy', 'ai-players', 'storm', 'implementation-checklist'],
|
||||
@@ -2762,7 +2762,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
? 'Partial'
|
||||
: 'Planned',
|
||||
summary:
|
||||
'Advanced faction rules. Some powers have tested engine support; advanced table starts remain disabled until the full rules are ready.',
|
||||
'Advanced faction rules. The six classic factions can use the unfinished Advanced preview; complete rules compliance remains under verification.',
|
||||
steps: rules.advanced,
|
||||
related: [`faction-${id}`, 'advanced-combat', ...(id === 'ecaz' ? ['ecaz-loyalty'] : [])],
|
||||
}),
|
||||
@@ -2944,7 +2944,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'An already-dead named leader remains a legal reveal, awarding no death or spice. During Mentat Pause, set the revealed card aside face up as a marker and draw a new Traitor Card. You can use this advantage only once against each faction in the game.',
|
||||
'The printed rule says a normal traitor reveal loses this advantage, but its duration remains unresolved. The preview guards further assassination after a normal call. Karama has no effect against Assassinate Leaders.',
|
||||
'If Harkonnen has captured your own leader and you hold its Traitor Card, you may call that traitor normally; you may not use Assassinate Leaders on that basis.',
|
||||
'Terror Assassination chooses a random leader when its token is triggered. It does not use this battle-loss condition, different-leader Traitor Card or once-per-faction allowance. Advanced starts and Moritani starts remain unavailable.',
|
||||
'Terror Assassination chooses a random leader when its token is triggered. It does not use this battle-loss condition, different-leader Traitor Card or once-per-faction allowance. Moritani starts remain unavailable in Basic and Advanced.',
|
||||
],
|
||||
checklist: [
|
||||
{area:'Implementation',status:'Partial',detail:'Explicit Advanced Moritani/base-opponent preview before any normal traitor call; native disc deaths, printed bounty, dead-target zero bounty and exact Mentat replacement.',evidence:['game/moritani-assassinate.ts','game/engine.ts']},
|
||||
@@ -2968,7 +2968,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
summary:
|
||||
'Spice-supported combat, elite units, casualty selection and Kwisatz Haderach.',
|
||||
steps: [
|
||||
'The combat engine now distinguishes ordinary and elite tokens, accepts half-strength dials and seals spice support with the battle plan. Advanced table starts remain disabled while other advanced rules are built.',
|
||||
'The combat engine now distinguishes ordinary and elite tokens, accepts half-strength dials and seals spice support with the battle plan. The unfinished Advanced preview is available for the six classic factions while other Advanced rules are completed.',
|
||||
'One spice supports one token at its full strength. Unsupported tokens fight at half strength. Fremen uses full strength without spice; Sardaukar counts as ordinary against Fremen.',
|
||||
'After resolution, casualties must match both the dial and spice spent. One legal ordinary/elite allocation settles automatically; several legal allocations ask the winner to choose. The losing army is destroyed. Only a traitor winner avoids its committed spice payment.',
|
||||
'Elite tokens are tracked in reserves, territories and tanks. Normal revival, Ghola and the Emperor’s extra revival all share the limit of one elite revival per faction per turn.',
|
||||
@@ -3005,7 +3005,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
summary:
|
||||
'Two spice piles, Fremen forecasts, half storm losses and additional worms.',
|
||||
steps: [
|
||||
'These phase rules have engine support and focused tests. Advanced table starts remain disabled while the other advanced powers are completed.',
|
||||
'These phase rules have engine support and focused tests. The six classic factions can start the unfinished Advanced preview while remaining powers are completed.',
|
||||
'Resolve blow A, its worms, Harvester window, Nexus and rides, then do the same for blow B. Each pile uses its own previous discard. First-turn worms are set aside and shuffled back only after both blows finish.',
|
||||
'After the first storm, Fremen privately learns a random storm card for the next turn. A foresight cancellation hides that card from Fremen without changing its eventual distance. Weather Control may replace a revealed card distance before movement.',
|
||||
'Fremen storm losses are half the exposed group, rounded up. Choose ordinary and Fedaykin casualties when more than one combination is possible. A legal reserve shipment into storm applies this loss only to the arriving group.',
|
||||
@@ -3039,7 +3039,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'When neither Guild nor Fremen is playing and nobody has reached the normal target, compare individual qualifying stronghold counts. Every player tied for the highest count wins. A correct Bene Gesserit prediction can replace this stronghold win.',
|
||||
'With tech tokens enabled, owning all three adds one stronghold. One player must hold all three, even in an alliance. If several sides reach their target at the same check, they share a normal win before any correct prediction replaces it.',
|
||||
'The table’s Stronghold victory progress shows current qualifying territories, a complete Tech Token set, Ecaz joint occupation and Fremen’s final-turn conditions. Allied Ecaz and Fremen may co-occupy Sietch Tabr without blocking that special win; solitary or non-allied Ecaz does not receive this exception. Tech Tokens cannot supply a jointly occupied territory. These public counts do not resolve a private prediction or award victory before the proper checkpoint.',
|
||||
'Other expansion additions to victory remain in development. Advanced and expansion faction starts remain disabled.',
|
||||
'Other expansion additions to victory remain in development. Advanced preview supports the six classic factions; expansion starts remain disabled.',
|
||||
],
|
||||
example:
|
||||
'At the final turn with neither Guild nor Fremen, two players each hold two qualifying strongholds and a third holds one. The two leaders of the count share the win.',
|
||||
@@ -3090,7 +3090,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'The mobile card copies a currently controlled stronghold, even if another faction still holds that location’s card. Holding a card after leaving its location does not itself make that location an eligible copy. The choice is public and fixed before battle plans; a sole eligible advantage is announced automatically.',
|
||||
'Arrakeen pays only for actual support and creates no personal cash. CHOAM force-payment income includes the bank contribution, subject to its normal traitor and donor exclusions. Carthag does not protect an empty or Worthless defense, stop Poison Tooth, or remove a Shield’s Lasgun explosion risk.',
|
||||
'Sietch Tabr uses the opposing dial, excluding leaders and other bonuses. A single traitor victory qualifies; mutual destruction does not. Tuek pays after an ordinary loss or a single traitor outcome as well as a win; a double traitor gives neither faction spice. Habbanya also decides a tied Stone Burner comparison.',
|
||||
'The six effects, public ownership and pre-plan choice are integrated in development games. Complete Advanced games and every expansion interaction remain under verification, so public Advanced starts remain unavailable.',
|
||||
'The six effects, public ownership and pre-plan choice are integrated in development games. Enable Stronghold Cards in an Advanced preview lobby. Complete Advanced compliance and every expansion interaction remain under verification.',
|
||||
],
|
||||
example:
|
||||
'You hold Arrakeen’s card and support four forces. The bank pays two spice; you and any permitted ally cover the remaining two. Leaving Arrakeen later does not transfer its card until turn end.',
|
||||
@@ -3190,7 +3190,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'A retained captive is available only to Harkonnen. Its territorial use restrictions remain. It can turn traitor, returns to its faction if it survives its one battle, and enters its original faction’s tanks if killed.',
|
||||
'When all native Harkonnen leaders are dead, return every unused captive immediately. A new captive retained in that situation also returns immediately.',
|
||||
'Only Harkonnen and the original owner know a concealed living captive’s identity. Tleilaxu can inspect a concealed executed leader. Using a captive in a revealed battle makes its identity public.',
|
||||
'The capture engine and private views have focused tests. Advanced table starts remain disabled while other advanced rules and expansion interactions are completed.',
|
||||
'The capture engine and private views have focused tests. The six classic factions can start Advanced preview; remaining Advanced rules and expansion interactions are unfinished.',
|
||||
],
|
||||
related: [
|
||||
'advanced-harkonnen',
|
||||
@@ -3224,7 +3224,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Tleilaxu receive a decision before another faction’s normal revival completes, whether or not they hold Karama. Decline to continue the revival, or spend a real Karama once per game to prevent that faction’s normal force and leader revivals for the current turn. A stopped attempt spends no spice, returns no pieces and consumes no elite or extra-revival quota.',
|
||||
'This includes negotiated leaders, KH and Emperor-funded allied extra forces. The Emperor remains the payer when an extra revival is allowed. Hidden leader identities and prices are not added to the public stop decision. Ghola treachery and the Emperor’s special Karama currently remain separate from the normal-revival restriction.',
|
||||
'The printed Tleilaxu power is brief. Its full-turn scope, precise timing and card/special-power exceptions remain provisional pending the complete primary-source audit. These choices are tested implementation behavior, not certification of the full expansion rules.',
|
||||
'Remaining expansion special powers, plus further timing and expansion interaction audits, remain unfinished. Advanced table starts remain disabled.',
|
||||
'Remaining expansion special powers, plus further timing and expansion interaction audits, remain unfinished. Advanced preview is available for the six classic factions.',
|
||||
'Richese’s special purchase is available in development tables: spend a Karama and three spice to choose privately from the cache, with separate Emperor income and explicit full-hand/final-cache guards. See its acquisition guide for current controls and limits.',
|
||||
],
|
||||
related: [
|
||||
@@ -3640,7 +3640,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'When another faction ships, moves or worm rides into your fighters, immediately choose whether to become advisors. The choice opens a Karama response. Repeated entry can offer a new choice.',
|
||||
'After another faction’s off-planet shipment, choose a free force in the Polar Sink or an accompanying force at the shipment destination. An accompanying advisor group cannot flip to fighters in that turn while other factions remain.',
|
||||
'Before the first shipment, choose eligible advisor territories to prepare for battle. Each chosen flip has a Karama response. Advisors cannot prepare against an ally or where storm prevents the battle.',
|
||||
'The engine and these choices have focused tests. The full advisor timing audit, forced-flip cancellation and expansion interactions remain unfinished; advanced table starts are disabled.',
|
||||
'The engine and these choices have focused tests. Advanced preview is available for the six classic factions; the full advisor timing audit, forced-flip cancellation and expansion interactions remain unfinished.',
|
||||
],
|
||||
related: ['faction-beneGesserit', 'movement', 'battle', 'victory'],
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user