Connect owned Sandmaster destination pile choices
This commit is contained in:
1 parent
24863480cb
commit
0040ed1921
17 files changed
+458
-89
No files matched your search
+11
-6
@@ -409,6 +409,8 @@ function fremenAmbassadorMoves(g: GameView): Action[] {
|
||||
(presenceAt(me, destination.territory) ? 8 : 0) -
|
||||
(abandons ? 25 : 0) -
|
||||
(destination.advisors ? 3 : 0);
|
||||
const sandmaster = !marker
|
||||
? sandmasterDestinationCollection(g, me.id, source.territory, destination.territory, destination.sector) : null;
|
||||
candidates.push({
|
||||
score,
|
||||
action: {
|
||||
@@ -418,8 +420,8 @@ function fremenAmbassadorMoves(g: GameView): Action[] {
|
||||
eliteForces,
|
||||
territory: destination.territory,
|
||||
sector: destination.sector,
|
||||
...(!marker && sandmasterDestinationCollection(g, me.id, source.territory, destination.territory, destination.sector)?.blocked === null
|
||||
? { sandmasterCollect: true } : {}),
|
||||
...(sandmaster?.blocked === null
|
||||
? { sandmasterCollect: true, sandmasterPile: sandmaster.key } : {}),
|
||||
...(marker
|
||||
? { noField: { tokenId: marker.tokenId, event: marker.event } }
|
||||
: {}),
|
||||
@@ -2805,13 +2807,15 @@ function policyActions(g: GameView): Action[] {
|
||||
)
|
||||
.slice(0, 16)
|
||||
.map(
|
||||
(to): Action => ({
|
||||
(to): Action => {
|
||||
const sandmaster = sandmasterWormCollection(g, me.id, to.t, to.s);
|
||||
return {
|
||||
type: 'decision',
|
||||
accept: true,
|
||||
territory: to.t,
|
||||
sector: to.s,
|
||||
...(sandmasterWormCollection(g, me.id, to.t, to.s)?.blocked === null
|
||||
? { sandmasterCollect: true } : {}),
|
||||
...(sandmaster?.blocked === null
|
||||
? { sandmasterCollect: true, sandmasterPile: sandmaster.key } : {}),
|
||||
forces: Object.fromEntries(
|
||||
Object.entries(me.forces).filter(
|
||||
([key]) =>
|
||||
@@ -2819,7 +2823,8 @@ function policyActions(g: GameView): Action[] {
|
||||
splitLocation(key).sector !== g.storm,
|
||||
),
|
||||
),
|
||||
}),
|
||||
};
|
||||
},
|
||||
),
|
||||
{ type: 'decision', accept: false },
|
||||
];
|
||||
|
||||
+13
-4
@@ -14162,7 +14162,8 @@ function validateMovementOrder(g: Game, p: Player, move: MovementOrder) {
|
||||
territory: move.to,
|
||||
sector: move.sector,
|
||||
fighters: move.wantsFighters,
|
||||
...(move.ambassadorSandmaster ? { sandmasterCollect: true } : {}),
|
||||
...(move.ambassadorSandmaster
|
||||
? { sandmasterCollect: true, sandmasterPile: move.ambassadorSandmaster.key } : {}),
|
||||
...(move.noField
|
||||
? {
|
||||
noField: {
|
||||
@@ -29775,6 +29776,9 @@ function applyActionInner(
|
||||
);
|
||||
requireRule(action.sandmasterCollect === undefined || typeof action.sandmasterCollect === 'boolean',
|
||||
'Choose whether to collect spice with Sandmaster.');
|
||||
requireRule(action.sandmasterPile === undefined ||
|
||||
(action.sandmasterCollect === true && typeof action.sandmasterPile === 'string'),
|
||||
'Choose a Sandmaster destination pile only when collecting.');
|
||||
requireRule(action.accept || !action.sandmasterCollect,
|
||||
'Sandmaster collection requires an accepted worm ride.');
|
||||
if (action.accept) {
|
||||
@@ -29785,9 +29789,14 @@ function applyActionInner(
|
||||
'Choose another destination territory.',
|
||||
);
|
||||
allowedEntry(g, p, to, sector);
|
||||
const sandmaster = action.sandmasterCollect ? sandmasterWormCollection(g, p.id, to, sector, decision) : null;
|
||||
if (action.sandmasterCollect) requireRule(sandmaster && !sandmaster.blocked,
|
||||
sandmaster?.blocked ?? 'Sandmaster collection needs the living native trainer.');
|
||||
const sandmaster = action.sandmasterCollect
|
||||
? sandmasterWormCollection(g, p.id, to, sector, decision, action.sandmasterPile as string | undefined) : null;
|
||||
if (action.sandmasterCollect) {
|
||||
requireRule(sandmaster && !sandmaster.blocked && sandmaster.key,
|
||||
sandmaster?.blocked ?? 'Sandmaster collection needs the living native trainer.');
|
||||
requireRule(sandmaster.piles.length <= 1 || typeof action.sandmasterPile === 'string',
|
||||
'Choose the destination spice pile for Sandmaster collection.');
|
||||
}
|
||||
requireRule(
|
||||
action.forces &&
|
||||
typeof action.forces === 'object' &&
|
||||
|
||||
@@ -330,13 +330,20 @@ export function quoteFremenAmbassadorMove(
|
||||
);
|
||||
requireMove(action.sandmasterCollect === undefined || typeof action.sandmasterCollect === 'boolean',
|
||||
'Choose whether to collect spice with Sandmaster.');
|
||||
requireMove(action.sandmasterPile === undefined ||
|
||||
(action.sandmasterCollect === true && typeof action.sandmasterPile === 'string'),
|
||||
'Choose a Sandmaster spice pile only when collecting.');
|
||||
let ambassadorSandmaster: FremenAmbassadorMove['ambassadorSandmaster'];
|
||||
if (action.sandmasterCollect) {
|
||||
requireMove(physical > 0 && !noField,
|
||||
'Sandmaster collection with concealed Ambassador relocation is still being integrated.');
|
||||
const quote = sandmasterDestinationCollection(g, p.id, origin, to, sector);
|
||||
const quote = sandmasterDestinationCollection(
|
||||
g, p.id, origin, to, sector, action.sandmasterPile,
|
||||
);
|
||||
requireMove(quote && !quote.blocked && quote.key,
|
||||
quote?.blocked ?? 'Sandmaster collection needs the living native trainer.');
|
||||
requireMove(quote.piles.length <= 1 || typeof action.sandmasterPile === 'string',
|
||||
'Choose the destination spice pile for Sandmaster collection.');
|
||||
ambassadorSandmaster = { leader: quote.leader, key: quote.key, before: quote.before };
|
||||
}
|
||||
return {
|
||||
|
||||
+1
-1
@@ -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. 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 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 and physical Fremen Ambassador relocation likewise offer one selected destination pile before arrival reactions, without an invented ground route or normal movement use. These destination allocations are provisional. Native Ixian HMS passengers collect before faction spice, but its multi-pile controls and other special relocation remain unfinished.',
|
||||
'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.',
|
||||
|
||||
@@ -60,6 +60,7 @@ export type SandmasterDestinationCollection = {
|
||||
leader: string;
|
||||
key: string | null;
|
||||
before: number;
|
||||
piles: { key: string; before: number }[];
|
||||
blocked: string | null;
|
||||
};
|
||||
|
||||
@@ -70,18 +71,18 @@ export function sandmasterDestinationCollection(
|
||||
origin: string,
|
||||
destination: string,
|
||||
sector: number,
|
||||
selectedPile?: string,
|
||||
): SandmasterDestinationCollection | null {
|
||||
const leader = sandmasterLeader(game, player);
|
||||
if (!leader) return null;
|
||||
const blocked = (reason: string): SandmasterDestinationCollection =>
|
||||
({ leader, key: null, before: 0, blocked: reason });
|
||||
({ leader, key: null, before: 0, piles: [], blocked: reason });
|
||||
if (!sandmasterModeSupported(game))
|
||||
return blocked('Sandmaster collection with this configuration is still being integrated.');
|
||||
if (origin === destination || !validGameLocation(game, destination, sector) ||
|
||||
sector === game.storm)
|
||||
return blocked('Choose a different legal destination territory for Sandmaster collection.');
|
||||
let key: string | null = null;
|
||||
let before = 0;
|
||||
const piles: { key: string; before: number }[] = [];
|
||||
for (const pile in game.spice) {
|
||||
const at = splitLocation(pile);
|
||||
if (at.territory !== destination) continue;
|
||||
@@ -91,15 +92,16 @@ export function sandmasterDestinationCollection(
|
||||
!Number.isSafeInteger(amount) || amount < 0)
|
||||
return blocked('Sandmaster needs a valid destination spice pile.');
|
||||
if (!amount) continue;
|
||||
if (key) return blocked('Sandmaster collection among multiple spice piles awaits its allocation ruling.');
|
||||
key = pile;
|
||||
before = amount;
|
||||
piles.push({ key: pile, before: amount });
|
||||
}
|
||||
if (!key) return blocked('There is no spice to collect at this destination.');
|
||||
if (!piles.length) return blocked('There is no spice to collect at this destination.');
|
||||
piles.sort((a, b) => a.key.localeCompare(b.key));
|
||||
const chosen = selectedPile === undefined ? piles[0] : piles.find(pile => pile.key === selectedPile);
|
||||
if (!chosen) return blocked('Choose an existing positive spice pile at this destination.');
|
||||
const spice = game.players.find(p => p.id === player)?.spice;
|
||||
if (!Number.isSafeInteger(spice) || !Number.isSafeInteger(spice! + 1))
|
||||
return blocked('Sandmaster collection needs a valid spice balance.');
|
||||
return { leader, key, before, blocked: null };
|
||||
return { leader, key: chosen.key, before: chosen.before, piles, blocked: null };
|
||||
}
|
||||
|
||||
export function sandmasterPathBlocked(
|
||||
|
||||
@@ -11,16 +11,17 @@ export function sandmasterWormCollection(
|
||||
destination: string,
|
||||
sector: number,
|
||||
decision: Game['decision'] | GameView['decision'] = game.decision,
|
||||
selectedPile?: string,
|
||||
): SandmasterDestinationCollection | null {
|
||||
const leader = sandmasterLeader(game, player);
|
||||
if (!leader || game.phase !== 1 || decision?.kind !== 'wormRide' ||
|
||||
decision.player !== player ||
|
||||
game.players.find(p => p.id === player)?.faction !== 'fremen') return null;
|
||||
const blocked = (reason: string): SandmasterDestinationCollection =>
|
||||
({ leader, key: null, before: 0, blocked: reason });
|
||||
({ leader, key: null, before: 0, piles: [], blocked: reason });
|
||||
if (!sandmasterModeSupported(game) || game.ecazTreachery ||
|
||||
!(nativeExpansionLeaderSkillsProfile(game) ||
|
||||
game.players.every(p => faction(p.faction).expansion === 'base')))
|
||||
return blocked('Sandmaster worm collection with other optional modules is still being integrated.');
|
||||
return sandmasterDestinationCollection(game, player, decision.territory, destination, sector);
|
||||
return sandmasterDestinationCollection(game, player, decision.territory, destination, sector, selectedPile);
|
||||
}
|
||||
Reference in new issue
Block a user