Connect owned Sandmaster ground pile selection

This commit is contained in:
Quintine committed 2026-10-09 03:54:00 +11:00
1 parent 5544b69d7e
commit 24863480cb
10 files changed
+155 -89

No files matched your search

+6 -2
View File
@@ -21,7 +21,7 @@ import { planetologistMovementModeSupported, planetologistLeader, planetologistR
import { quoteSmugglerNoField, smugglerNoFieldModeSupported, type SmugglerNoFieldCompanion } from './smuggler-no-field';
import { quoteSmugglerShipment, type SmugglerShipment } from './smuggler-shipment';
import { createSmugglerBattle, settleSmugglerBattle, smugglerBattleModeSupported, smugglerBattlePlanBlock, smugglerBattlePile, smugglerBattleSignature, smugglerBattleAllocationRequired, chooseSmugglerBattleAllocation, type SmugglerBattleReceipt, type SmugglerBattleCollectionOffer } from './smuggler-battle';
import { quoteSandmasterMovement, validateSandmasterMovement, sandmasterCollectible, sandmasterLeader, sandmasterRouteDistance, type SandmasterMovement, type SandmasterOrder } from './sandmaster-movement';
import { quoteSandmasterMovement, validateSandmasterMovement, sandmasterCollectionPiles, sandmasterLeader, sandmasterRouteDistance, type SandmasterMovement, type SandmasterOrder } from './sandmaster-movement';
import { sandmasterWormCollection } from './sandmaster-worm';
import { spiceBankerModeSupported, validateSpiceBankerSpend } from './spice-banker';
import { BankerIncomeError, createBankerIncomeState, validateBankerIncomeState, quoteBankerIncome, commitBankerIncome, quoteBankerIncomeCollection, commitBankerIncomeCollection, projectBankerIncome, type BankerIncomeState, type BankerIncomeAuthority, type BankerIncomeContext } from './spice-banker-income';
@@ -25782,7 +25782,11 @@ function relocateMobileStronghold(
// into, so the Sandmaster skill collects there before the faction's own
// traversed-sector collection.
if (sandmasterLeader(g, move.player))
for (const pile of sandmasterCollectible(g, { interior: move.route })) {
for (const piles of Object.values(sandmasterCollectionPiles(g, { interior: move.route }))) {
// HMS relocation keeps its existing single-pile scope until its
// separate route controls can select the Sandmaster debit sector.
if (piles.length !== 1) continue;
const pile = piles[0];
g.spice[pile]--;
if (!g.spice[pile]) delete g.spice[pile];
p.spice++;
+1 -1
View File
@@ -4730,7 +4730,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Suk Graduate returns one selected casualty to reserves while its leader stays face up, or lets its surviving battle leader save up to three: one stays in its original sector and the rest return to reserves. The effects do not stack. Rescued counters still count toward Advanced Atreides’ seven battle losses for Kwisatz Haderach; combined modules remain unfinished.',
'In the supported classic Nexus/Skills composition, Emperor Cunning counts five actual ordinary counters as temporary Sardaukar. Normal or trained Suk rescue still saves physical ordinary casualties. The original casualty allocation remains pending while the rescue choice is open; physical counters move only when rescue settles. Original support is already paid and is not refunded or charged twice. A normal save returns one to reserves; a trained save keeps one and returns the rest. Actual starred reserves and Tanks remain unchanged.',
'A surviving selected Mentat adds two battle strength. A surviving selected Bureaucrat subtracts one from the opponent’s total per distinct stronghold occupied by that opponent; advisors do not count. Printed disc values and bounties stay unchanged.',
'A living native Sandmaster may collect one spice per entered territory along a declared legal ground route. Choose each collection separately; canceled moves earn nothing. The initial source and a sector-only reposition earn nothing. Separate movements have separate collection opportunities. Native Fremen worm rides may collect once at the destination before faction arrival reactions, without a ground route or normal movement use. When a native Ixian Hidden Mobile Stronghold relocates, its interior passengers count as entering the territories it points into, so the Sandmaster collects there before the faction’s own traversed-sector collection. Multiple piles and other special relocation remain unfinished.',
'A living native Sandmaster may collect one spice per entered territory along a declared legal ground route. With multiple positive piles, provisionally choose one debit sector per territory, or decline collection; two piles never grant two spice from the same territory. Canceled moves earn nothing. The initial source and a sector-only reposition earn nothing. Separate movements have separate collection opportunities. Native Fremen worm rides may collect once at the destination before arrival reactions, without a ground route or normal movement use. Native Ixian HMS interior passengers count as entering the territories it points into and collect before faction spice. Multi-pile worm/Ambassador destination and HMS collection, and other special relocation, remain separate unfinished paths.',
'A surviving skilled Sandmaster winner adds three board spice to an existing pile in the battle territory, even with no surviving forces. No pile adds nothing; a single pile resolves automatically. With several remaining positive piles, the winner selects one sector after any Smuggler allocation. Owner selection is a visibly provisional placement policy, not a publisher ruling.',
'Rihani normally inspects two random Traitor Deck cards privately, then reshuffles them. A surviving selected native Rihani also offers a separate optional draw of two. Decide before seeing them: once drawn, keep one new card and publicly reveal and return one unused old card. The other new card returns secretly. A captured Rihani grants only the optional exchange. Your private history records both samples; a card just called to win is already used.',
'Smuggler can include one free accompanying force in an off-planet shipment into an empty territory. Select the entire physical total; three forces cost the ordinary price of two. The checkbox can decline this optional benefit. Guild rounding follows the free force. Richese may instead ship one separate free reserve force beside its owned No-Field into an empty territory; the marker retains its ordinary price and hidden value. Existing forces, including advisors, prevent the bonus; ordinary Fremen on-planet reinforcement has no bonus.',
+16 -15
View File
@@ -163,17 +163,18 @@ export function sandmasterEnteredTerritories(
),
];
}
/** Multiple piles retain the existing unresolved physical-pile boundary. */
export function sandmasterCollectible(
/** Optional collection chooses at most one existing pile in each entered territory. */
export function sandmasterCollectionPiles(
g: Game | GameView,
routes: Record<string, string[]>,
): string[] {
return sandmasterEnteredTerritories(routes).flatMap((id) => {
const piles = Object.entries(g.spice).filter(
([key, n]) => splitLocation(key).territory === id && n > 0,
);
return piles.length === 1 ? [piles[0][0]] : [];
});
): Record<string, string[]> {
const choices: Record<string, string[]> = {};
const keys = Object.keys(g.spice);
for (const id of sandmasterEnteredTerritories(routes)) {
const piles = keys.filter(key => splitLocation(key).territory === id && g.spice[key] > 0).sort();
if (piles.length) choices[id] = piles;
}
return choices;
}
export function quoteSandmasterMovement(
g: Game | GameView,
@@ -221,12 +222,12 @@ export function quoteSandmasterMovement(
routes[from] = [...route];
}
const collect = value.collect;
const available = sandmasterCollectible(g, routes);
const offered = sandmasterCollectionPiles(g, routes);
requireMove(
collect.every(
(key) => typeof key === 'string' && available.includes(key),
) && new Set(collect).size === collect.length,
'Collect once per entered territory with one unambiguous spice pile, or decline that collection.',
collect.every(key => typeof key === 'string' &&
offered[splitLocation(key).territory]?.includes(key)) &&
new Set(collect.map(key => splitLocation(key).territory)).size === collect.length,
'Choose at most one positive pile per entered territory, or decline its collection.',
);
return {
leader,
@@ -361,5 +362,5 @@ export function sandmasterDefaultChoice(
if (!route) return null;
routes[from] = route;
}
return { routes, collect: sandmasterCollectible(g, routes) };
return { routes, collect: Object.values(sandmasterCollectionPiles(g, routes)).map(piles => piles[0]) };
}