Connect Sandmaster collection to physical Ambassador relocation
This commit is contained in:
1 parent
7cef1a3412
commit
039fe4e87f
13 files changed
+175
-46
No files matched your search
+3
-1
@@ -99,7 +99,7 @@ import {
|
||||
guildTransportCost,
|
||||
} from './bot-mobility';
|
||||
import { planetologistMovementModeSupported, planetologistLeader } from './planetologist-movement';
|
||||
import { sandmasterDefaultChoice } from './sandmaster-movement';
|
||||
import { sandmasterDefaultChoice, sandmasterDestinationCollection } from './sandmaster-movement';
|
||||
import { sandmasterWormCollection } from './sandmaster-worm';
|
||||
import { richeseCardDefinition } from './richese-cards';
|
||||
import { presenceAt } from './force-presence';
|
||||
@@ -418,6 +418,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 } : {}),
|
||||
...(marker
|
||||
? { noField: { tokenId: marker.tokenId, event: marker.event } }
|
||||
: {}),
|
||||
|
||||
@@ -14167,6 +14167,7 @@ function validateMovementOrder(g: Game, p: Player, move: MovementOrder) {
|
||||
territory: move.to,
|
||||
sector: move.sector,
|
||||
fighters: move.wantsFighters,
|
||||
...(move.ambassadorSandmaster ? { sandmasterCollect: true } : {}),
|
||||
...(move.noField
|
||||
? {
|
||||
noField: {
|
||||
@@ -15758,6 +15759,14 @@ function ambassadorRelocationMovement(g: Game, player: string) {
|
||||
function commitAmbassadorRelocation(g: Game, move: FremenAmbassadorMove) {
|
||||
const { entry, beneficiary: p } = currentFremenAmbassador(g);
|
||||
validateAmbassadorRelocationArrival(g, move);
|
||||
if (move.ambassadorSandmaster) {
|
||||
const { key } = move.ambassadorSandmaster;
|
||||
g.spice[key]--;
|
||||
if (!g.spice[key]) delete g.spice[key];
|
||||
p.spice++;
|
||||
log(g, `${p.name}'s Sandmaster collected 1 spice at ${territory(move.to).name} during Fremen Ambassador relocation, before arrival reactions.`,
|
||||
{ faction: p.faction, name: 'Sandmaster collection' });
|
||||
}
|
||||
if (move.noField) {
|
||||
requireRule(
|
||||
p.noField && p.noFieldEvent === move.noField.event,
|
||||
@@ -27407,6 +27416,7 @@ type CompletedMovement = Pick<
|
||||
> & { noField: boolean };
|
||||
type MovementOrder = {
|
||||
sandmaster?: SandmasterMovement;
|
||||
ambassadorSandmaster?: FremenAmbassadorMove['ambassadorSandmaster'];
|
||||
source?: 'ambassador';
|
||||
ambassadorEvent?: string;
|
||||
player: string;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { presenceAt } from './force-presence';
|
||||
import { territoryEntryBlock } from './occupancy';
|
||||
import { ecazOccupancyRelation } from './ecaz-occupy';
|
||||
import { homeworldNoFieldMovementBlock } from './homeworld-mobility';
|
||||
import { sandmasterDestinationCollection } from './sandmaster-movement';
|
||||
|
||||
export class FremenAmbassadorMoveError extends Error {
|
||||
constructor(message: string) {
|
||||
@@ -32,6 +33,7 @@ export type FremenAmbassadorMove = {
|
||||
wantsFighters: boolean;
|
||||
lockedTurn?: number;
|
||||
noField?: { tokenId: string; event: string; from: string };
|
||||
ambassadorSandmaster?: { leader: string; key: string; before: number };
|
||||
};
|
||||
export type FremenAmbassadorMovement = {
|
||||
sources: {
|
||||
@@ -326,6 +328,17 @@ export function quoteFremenAmbassadorMove(
|
||||
count((p.elites?.forces[target] ?? 0) + elite),
|
||||
'The resulting force quantity is invalid.',
|
||||
);
|
||||
requireMove(action.sandmasterCollect === undefined || typeof action.sandmasterCollect === 'boolean',
|
||||
'Choose whether to collect spice with Sandmaster.');
|
||||
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);
|
||||
requireMove(quote && !quote.blocked && quote.key,
|
||||
quote?.blocked ?? 'Sandmaster collection needs the living native trainer.');
|
||||
ambassadorSandmaster = { leader: quote.leader, key: quote.key, before: quote.before };
|
||||
}
|
||||
return {
|
||||
player: p.id,
|
||||
group,
|
||||
@@ -339,6 +352,7 @@ export function quoteFremenAmbassadorMove(
|
||||
wantsFighters,
|
||||
...(lockedTurn === undefined ? {} : { lockedTurn }),
|
||||
...(noField ? { noField } : {}),
|
||||
...(ambassadorSandmaster ? { ambassadorSandmaster } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -4542,7 +4542,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Connected movement, shipping, payment and battle skills share their normal controls and legal choices. CHOAM cannot spend a card already committed to a sale or sell a Special already discarded after Planetologist battle use.',
|
||||
'Ghola may revive an eligible own leader while a declared sale waits. Finish the spent Ghola, optional private replacement draw or decline, and any required assignment before the original sale resumes. The replacement belongs to the actual revived leader.',
|
||||
'The sale resumes with its original card, price and response passes. Canceling that sale preserves the completed free revival and does not return the spent Ghola.',
|
||||
'Fresh Advanced CHOAM and/or Ixians plus classic tables use distinct required family decks and all fourteen skills. Auditor is excluded both at assignment and paid revival; original support income and bank-held allied funding remain distinct. Richese and further optional combinations retain separate boundaries. Remaining skill effects are unfinished; this prototype does not certify complete expansion rules or AI strength.',
|
||||
'Fresh Advanced CHOAM and/or Ixians plus classic tables use distinct required family decks and all fourteen skills. Auditor cannot be assigned a skill, including after its paid revival; original support income and bank-held allied funding remain distinct. Richese and further optional combinations retain separate boundaries. Remaining skill effects are unfinished; this prototype does not certify complete expansion rules or AI strength.',
|
||||
],
|
||||
related: ['leader-skills', 'choam-modules', 'implementation-checklist'],
|
||||
checklist: [
|
||||
@@ -4739,14 +4739,14 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Bureaucrat may redirect two spice from a payment of at least five between two other players, once per phase. The payer still pays in full; the recipient receives two less. Declining keeps the use available for a later payment. Paid bribes qualify when transferred, with the remaining share still unavailable until Mentat Pause. Ordinary auctions, Guild income, Richese sales and actual paid Tleilaxu revival income in supported Basic or bounded Advanced native skill profiles have connected payment paths. A separate free-revival award is not part of the paid price and stays with Tleilaxu. Split funding, gifts, other payment families and combined modules remain unfinished.',
|
||||
'Smuggler records the existing battle-territory spice when plans reveal, capped by the selected leader’s unmodified strength. If that leader survives, it collects automatically even when losing or no forces remain. A captive pays its controller. Pending spice cannot fund the plan. Collection precedes Sandmaster’s addition; a drained pile receives none. Modified strength, multiple positive piles and combined modules remain guarded. Older saved battles keep their original behavior.',
|
||||
'In classic and supported E1/E2 native Tech composition, actual Smuggler prices and normal Banker qualifying fees remain distinct from phase-end Tech income. Tech piles cannot fund their triggering action; Banker front-shield grants still wait for actual Mentat. Winning physical Suk rescue and Sandmaster board spice precede original played-card cleanup and mandatory winner token reward. One token transfers automatically, several require a selection, and no token skips without an invented decline. A later Face Dancer retains the original winner’s cards, bounty and Tech reward, kills the matching trainer without another bounty and replaces only actual survivors from real sources. Native Tleilaxu-only free revival excludes Axlotl; CHOAM initial income is not Charity. Richese’s free companion does not change its marker price or create the concealed counters early.',
|
||||
'Other special Sandmaster relocation remains missing. Normal Banker income has separate fresh classic or supported native source-to-Mentat previews with provisional original-earned-faction custody, not general activation. Diplomat retreat retains its documented conservative outcome order. Native Advanced foreign gholas/Face Dancers, skill-first Moritani assassination and ordinary-disc Ecaz skills follow their bounded contracts; shared-Duke assignment, combined Occupy skills and other combined modules remain unfinished. This prototype does not enable public module starts.',
|
||||
'Physical Fremen Ambassador relocation now offers optional Sandmaster destination collection before arrival reactions, without an invented route or ordinary movement use. Same-territory repositioning earns none; concealed relocation and other special movement still retain their own limits. Normal Banker income has separate fresh classic or supported native source-to-Mentat previews with provisional original-earned-faction custody, not general activation. Diplomat retreat retains its documented conservative outcome order. Native Advanced foreign gholas/Face Dancers, skill-first Moritani assassination and ordinary-disc Ecaz skills follow their bounded contracts; shared-Duke assignment, combined Occupy skills and other combined modules remain unfinished. This prototype does not enable public module starts.',
|
||||
...LEADER_SKILL_CARDS.map(card => `${card.name}. Normal: ${card.normal.join(' ')} Skilled battle: ${card.battle.join(' ')}`),
|
||||
],
|
||||
related: ['spice-banker-income', 'choam-leader-skills', 'ixian-leader-skills', 'tleilaxu-leader-skills', 'richese-leader-skills', 'ecaz-leader-skills', 'moritani-leader-skills', 'setup','choam-modules','implementation-checklist'],
|
||||
checklist: [
|
||||
{ area:'Implementation',status:'Partial',detail:'Exact fourteen-card custody, source-ordered setup, public capture, concealment, death and own revival connect five role bonuses, Planetologist movement/Special substitution and Suk Graduate casualty rescue. Rihani inspection/exchange and lower Mentat/Bureaucrat/Sandmaster effects are connected. Smuggler normal reserve shipping, the owned Richese No-Field companion, unmodified battle collection and optional Sandmaster ground-route and native Fremen worm-ride collection are connected. A separately opted-in Mentat preview connects private pre-plan questions and historical observations while ordinary activation awaits the uniform-response decision. Spice Banker lower spending and native Diplomat base Shield/Snooper copying are connected; skilled Diplomat retreat now transfers undialed normal/elite forces before losing casualties in supported battles. Normal Banker income connects only in the separate fresh classic or supported Basic native banker-income profile; other remaining bands and combined modules remain missing. Bureaucrat redirects supported third-party auction, shipment and bribe payments with exact saved once-per-phase use.',evidence:['game/leader-skill-cards.ts','game/leader-skills.ts','game/leader-skill-combat.ts','game/suk-graduate.ts','game/rihani-decipherer.ts','game/mentat-question.ts','game/bureaucrat-payment.ts','game/leader-skill-battle-board.ts','game/smuggler-shipment.ts','game/smuggler-no-field.ts','game/smuggler-battle.ts','game/spice-banker.ts','game/spice-banker-income.ts','game/sandmaster-movement.ts','game/sandmaster-worm.ts','game/diplomat-defense.ts','game/diplomat-retreat.ts','game/engine.ts'] },
|
||||
{ area:'Player controls',status:'Partial',detail:'Private card/leader selection, readable inspectors, public battle posture and revival draw/decline, Planetologist range/gather controls, Special weapon selection and Suk Graduate physical rescue choices are connected. Rihani has private history and separate keep-new/reveal-old controls; automatic battle effects need no confirmation. Smuggler has explicit ordinary shipping opt-out and No-Field companion opt-in with physical-versus-priced force counts, plus automatic surviving-leader battle collection with reveal-time guidance. Banker exposes separate sealed spending and authorized inspection. Sandmaster exposes explicit legal routes, per-territory collection choices and an optional worm-destination checkbox. Diplomat offers a named committed Worthless choice or decline after public reveal, and a separate surviving-loser destination/normal/elite retreat or decline. Bureaucrat offers redirect or full payment with explicit recipient amounts and deferred bribe custody. Remaining effect controls are missing.',evidence:['components/leader-skills.tsx','components/planetologist-movement.tsx','components/suk-graduate.tsx','components/rihani-decipherer.tsx','components/mentat-question.tsx','components/bureaucrat-payment.tsx','components/leader-skill-battle-guide.tsx','components/smuggler-shipment.tsx','components/spice-banker.tsx','components/sandmaster-movement.tsx','components/sandmaster-worm.tsx','components/diplomat-defense.tsx','components/diplomat-retreat.tsx','components/game-table.tsx'] },
|
||||
{ area:'AI',status:'Partial',detail:'All four profiles use private offers, legal visibility/revival actions and Planetologist movement/battle candidates and legal Suk Graduate rescues, Rihani exchanges, shared battle scoring, Smuggler reserve/owned No-Field shipments and unmodified collection plans, Sandmaster route and worm collection, funded Banker commitments and optional Diplomat copying from public revealed defenses or choosing legal losing-battle retreat. Bureaucrat uses the public payee alliance to redirect enemy income or preserve allied income. These are legal prototype paths; complete skill strategy and difficulty calibration remain unfinished.',evidence:['game/bots.ts'] },
|
||||
{ area:'Implementation',status:'Partial',detail:'Exact fourteen-card custody, source-ordered setup, public capture, concealment, death and own revival connect five role bonuses, Planetologist movement/Special substitution and Suk Graduate casualty rescue. Rihani inspection/exchange and lower Mentat/Bureaucrat/Sandmaster effects are connected. Smuggler normal reserve shipping, the owned Richese No-Field companion, unmodified battle collection and optional Sandmaster ground-route, native Fremen worm-ride and physical Fremen Ambassador destination collection are connected. A separately opted-in Mentat preview connects private pre-plan questions and historical observations while ordinary activation awaits the uniform-response decision. Spice Banker lower spending and native Diplomat base Shield/Snooper copying are connected; skilled Diplomat retreat now transfers undialed normal/elite forces before losing casualties in supported battles. Normal Banker income connects only in the separate fresh classic or supported Basic native banker-income profile; other remaining bands and combined modules remain missing. Bureaucrat redirects supported third-party auction, shipment and bribe payments with exact saved once-per-phase use.',evidence:['game/leader-skill-cards.ts','game/leader-skills.ts','game/leader-skill-combat.ts','game/suk-graduate.ts','game/rihani-decipherer.ts','game/mentat-question.ts','game/bureaucrat-payment.ts','game/leader-skill-battle-board.ts','game/smuggler-shipment.ts','game/smuggler-no-field.ts','game/smuggler-battle.ts','game/spice-banker.ts','game/spice-banker-income.ts','game/sandmaster-movement.ts','game/sandmaster-worm.ts','game/fremen-ambassador.ts','game/diplomat-defense.ts','game/diplomat-retreat.ts','game/engine.ts'] },
|
||||
{ area:'Player controls',status:'Partial',detail:'Private card/leader selection, readable inspectors, public battle posture and revival draw/decline, Planetologist range/gather controls, Special weapon selection and Suk Graduate physical rescue choices are connected. Rihani has private history and separate keep-new/reveal-old controls; automatic battle effects need no confirmation. Smuggler has explicit ordinary shipping opt-out and No-Field companion opt-in with physical-versus-priced force counts, plus automatic surviving-leader battle collection with reveal-time guidance. Banker exposes separate sealed spending and authorized inspection. Sandmaster exposes explicit legal routes, per-territory collection choices and optional worm/physical Ambassador destination checkboxes. Diplomat offers a named committed Worthless choice or decline after public reveal, and a separate surviving-loser destination/normal/elite retreat or decline. Bureaucrat offers redirect or full payment with explicit recipient amounts and deferred bribe custody. Remaining effect controls are missing.',evidence:['components/leader-skills.tsx','components/planetologist-movement.tsx','components/suk-graduate.tsx','components/rihani-decipherer.tsx','components/mentat-question.tsx','components/bureaucrat-payment.tsx','components/leader-skill-battle-guide.tsx','components/smuggler-shipment.tsx','components/spice-banker.tsx','components/sandmaster-movement.tsx','components/sandmaster-worm.tsx','components/ambassador-movement.tsx','components/diplomat-defense.tsx','components/diplomat-retreat.tsx','components/game-table.tsx'] },
|
||||
{ area:'AI',status:'Partial',detail:'All four profiles use private offers, legal visibility/revival actions and Planetologist movement/battle candidates and legal Suk Graduate rescues, Rihani exchanges, shared battle scoring, Smuggler reserve/owned No-Field shipments and unmodified collection plans, Sandmaster route, worm and physical Ambassador destination collection, funded Banker commitments and optional Diplomat copying from public revealed defenses or choosing legal losing-battle retreat. Bureaucrat uses the public payee alliance to redirect enemy income or preserve allied income. These are legal prototype paths; complete skill strategy and difficulty calibration remain unfinished.',evidence:['game/bots.ts'] },
|
||||
{ area:'Documentation',status:'Partial',detail:'All fourteen faces and common rules are recorded. Skilled capture follows public physical-card custody; replacement entitlement and other material timing interpretations remain explicit.',evidence:['docs/LEADER_SKILLS_RULES.md','docs/LEADER_SKILLS_CAPTURE.md','docs/LEADER_SKILLS_RUNTIME.md','docs/PLANETOLOGIST_RULES.md','docs/SUK_GRADUATE_RULES.md','docs/LEADER_BATTLE_EFFECTS.md','docs/MENTAT_QUESTION.md','docs/BUREAUCRAT_PAYMENTS.md','docs/SMUGGLER_SHIPMENT.md','docs/SPICE_BANKER_RUNTIME.md','docs/SANDMASTER_WORM.md','docs/DIPLOMAT_DEFENSE.md'] },
|
||||
{ area:'Verification',status:'Partial',detail:'Classic Homeworld Nexus optional modules add13 meaningful cases; affected242/242, types/lint/build pass. Twenty-two original six-seat games across15 families finish14233/14233 actions with375 JSON on one unchanged source tree. Human390px original typed invasion/zero-Suk and exact-three native return retain Kaitain14/Salusa5/visitor1; actual random Shrine entry, typed Maker ride and both-pile closing Moritani draw retain original armies. Native35-case and earlier source-qualified evidence remains in canonical logs. Native compositions, pending effects, full rules/combinations and deployed acceptance remain open.',evidence:['tests/homeworld-classic-nexus-skills-runtime.test.ts','tests/homeworld-classic-discovery-nexus-runtime.test.ts','tests/homeworld-native-e1e2-modules-runtime.test.ts','tests/homeworld-native-e3-modules-runtime.test.ts','tests/homeworld-classic-skills-runtime.test.ts','tests/homeworld-skills-discovery-runtime.test.ts','tests/homeworld-optional-modules-runtime.test.ts','tests/leader-skills-engine.test.ts','tests/leader-skills-capture.test.ts','tests/leader-battle-effects.test.ts','tests/suk-graduate.test.ts','tests/mentat-question.test.ts','tests/bureaucrat-payment.test.ts','tests/smuggler-battle.test.ts','tests/spice-banker.test.ts','tests/advanced-native-skills.test.ts','tests/fixture-advanced-native-skills.ts','tests/skills-tech-payments-runtime.test.ts','tests/skills-tech-battle-runtime.test.ts','tests/ix-choam-skills-tech-runtime.test.ts','tests/tleilaxu-skills-tech-runtime.test.ts','tests/richese-skills-tech-runtime.test.ts','tests/classic-skills-stronghold-runtime.test.ts','tests/native-skills-stronghold-runtime.test.ts','tests/ecaz-skills-modules-runtime.test.ts','tests/moritani-skills-modules-runtime.test.ts','docs/LEADER_SKILLS_RUNTIME.md','docs/HOMEWORLD_RUNTIME_IMPLEMENTATION.md'] },
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ordinaryLeaderSkillModeSupported } from './leader-skill-profile';
|
||||
import type { Action, Game, GameView } from './engine';
|
||||
import { GRAPH, location, mobileRouteDistance, splitLocation, MOBILE_LOCATION } from './board';
|
||||
import { GRAPH, location, mobileRouteDistance, splitLocation, MOBILE_LOCATION, validGameLocation } from './board';
|
||||
import { isAdvisor } from './advisors';
|
||||
import { strongholdPathBlocked } from './occupancy';
|
||||
import { botMovementRange } from './bot-mobility';
|
||||
@@ -55,6 +55,53 @@ export function sandmasterLeader(
|
||||
? leader.id
|
||||
: null;
|
||||
}
|
||||
|
||||
export type SandmasterDestinationCollection = {
|
||||
leader: string;
|
||||
key: string | null;
|
||||
before: number;
|
||||
blocked: string | null;
|
||||
};
|
||||
|
||||
/** Direct relocation enters only its destination, never an invented route. */
|
||||
export function sandmasterDestinationCollection(
|
||||
game: Game | GameView,
|
||||
player: string,
|
||||
origin: string,
|
||||
destination: string,
|
||||
sector: number,
|
||||
): SandmasterDestinationCollection | null {
|
||||
const leader = sandmasterLeader(game, player);
|
||||
if (!leader) return null;
|
||||
const blocked = (reason: string): SandmasterDestinationCollection =>
|
||||
({ leader, key: null, before: 0, 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;
|
||||
for (const pile in game.spice) {
|
||||
const at = splitLocation(pile);
|
||||
if (at.territory !== destination) continue;
|
||||
const amount = game.spice[pile];
|
||||
if (!validGameLocation(game, at.territory, at.sector) ||
|
||||
pile !== location(at.territory, at.sector) ||
|
||||
!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;
|
||||
}
|
||||
if (!key) return blocked('There is no spice to collect 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 };
|
||||
}
|
||||
|
||||
export function sandmasterPathBlocked(
|
||||
g: Game | GameView,
|
||||
player: string,
|
||||
|
||||
+5
-30
@@ -1,15 +1,8 @@
|
||||
import type { Game, GameView } from './engine';
|
||||
import { splitLocation, validGameLocation } from './board';
|
||||
import { faction } from './catalog';
|
||||
import { nativeExpansionLeaderSkillsProfile } from './leader-skill-profile';
|
||||
import { sandmasterLeader, sandmasterModeSupported } from './sandmaster-movement';
|
||||
|
||||
export type SandmasterWormCollection = {
|
||||
leader: string;
|
||||
key: string | null;
|
||||
before: number;
|
||||
blocked: string | null;
|
||||
};
|
||||
import { sandmasterDestinationCollection, sandmasterLeader, sandmasterModeSupported,
|
||||
type SandmasterDestinationCollection } from './sandmaster-movement';
|
||||
|
||||
/** A worm ride names a destination, not a traversed ground route. */
|
||||
export function sandmasterWormCollection(
|
||||
@@ -18,34 +11,16 @@ export function sandmasterWormCollection(
|
||||
destination: string,
|
||||
sector: number,
|
||||
decision: Game['decision'] | GameView['decision'] = game.decision,
|
||||
): SandmasterWormCollection | null {
|
||||
): 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): SandmasterWormCollection =>
|
||||
const blocked = (reason: string): SandmasterDestinationCollection =>
|
||||
({ leader, key: null, before: 0, 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.');
|
||||
if (destination === decision.territory ||
|
||||
!validGameLocation(game, destination, sector) || sector === game.storm)
|
||||
return blocked('Choose a different legal destination for the worm ride.');
|
||||
const piles = Object.entries(game.spice)
|
||||
.filter(([key]) => splitLocation(key).territory === destination);
|
||||
if (piles.some(([key, amount]) => {
|
||||
const at = splitLocation(key);
|
||||
return !validGameLocation(game, at.territory, at.sector) ||
|
||||
key !== `${at.territory}:${at.sector}` || !Number.isSafeInteger(amount) || amount < 0;
|
||||
})) return blocked('Sandmaster needs a valid destination spice pile.');
|
||||
const positive = piles.filter(([, amount]) => amount > 0);
|
||||
if (positive.length > 1)
|
||||
return blocked('Sandmaster collection among multiple spice piles awaits its allocation ruling.');
|
||||
if (!positive.length) return blocked('There is no spice to collect at this destination.');
|
||||
const [key, before] = positive[0];
|
||||
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 sandmasterDestinationCollection(game, player, decision.territory, destination, sector);
|
||||
}
|
||||
Reference in new issue
Block a user