Connect Basic Moritani to the full Leader Skills prototype
Admit the genuine private setup profile for Basic Moritani with base opponents, exactly the Ecaz expansion and no other optional modules. Keep all14 physical skills, private two-card offers, starting-card/Traitor order and Moritani's final six-force placement. Public starts and complete-mode/publication gates remain. Share public profile eligibility across Banker, Diplomat, Sandmaster movement and worm riding, Smuggler shipping/collection, Bureaucrat payments, Planetologist controls/bots and Suk/Rihani/Sandmaster battle settlement. Admission reads no hidden cards or random deal. Preserve base/CHOAM boundaries and existing source questions; no full AI implementation, strategy or calibration changes. Genuine setup regressions cover all14 assignments, legal profiles and guards, Terror Assassination death and exact skill return, optional own revival draw/privacy, Sabotage, movement controls/bots, Smuggler shipment and Bureaucrat paid bribes. Battle actions cover Rihani exchange, Sandmaster spice, Banker funds, Suk casualties and Smuggler collection. Actual enemy alliances prove losing-ally retention excludes mandatory Planetologist Special and Diplomat-copy disposal. A real worm/Nexus ride collects Sandmaster spice once. Authenticated in-memory SQLite restarts and concurrent skill choices preserve one assignment, private offers, saved seats and final Moritani placement. Conserved staged positions are explicitly distinct from played phase histories. Add a reproducible moritani-skills profile to the existing faction-game runner, including fixed2-6-player Basic rosters, seed offsets16-20 and saved resume. Verify expected module presence, all14 physical skills and ordinary Traitor custody alongside existing card/force/private-view checks. Default six expansion samples and base samples retain their rosters/ordinals. No live DB is used by these complete-game samples. CLI regressions check interrupted continuation and Advanced refusal; corruption tests catch lost cards and entire module loss. Independent review caught an old all-base worm guard and a sample checker that missed whole-module deletion; both fixed with meaningful regressions and reviewed. Initial edits had a stale helper reference and final lint found an unused import; these were corrected. Types, lint, all 5034 offline tests and build pass on stable final source. Five genuine complete-game samples passed with no rejected actions:2 players189 actions/5 restores,3 players53/1,4 players862/23, 5 players116/3 and6 players1338/36. These are legality samples, not calibration or exhaustive faction/card acceptance. No endpoint/auth/storage changes; existing HTTP suite not repeated, while new authenticated storage and live browser actions exercise the newly admitted profile through production boundaries. Browser created a fresh Basic human Moritani/Easy Emperor lobby, then the existing backed-up local initializer started that named ready room without replacing seats. Human inspected both Killer Medic bands, assigned Lupino Ord, chose a private Traitor and placed six in Polar Sink after all other setup. Refresh preserved the two public skills, own hand/Traitor and6 deployed/14 reserves at pending Storm. Mobile skill panel339px, client/scroll337; viewport restored afterward. Screenshot capture timed out, so full visual/screen-reader acceptance stays open. All 1580 opening games, 3348 saved seats and auxiliary rows remain identical. One isolated QA game adds one human seat and entry receipt; its row is unchanged after setup through refresh, with all seats unchanged. SQLite integrity and foreign keys pass. No server restart, migration, reset or recurring automation. Existing unfinished Leader Skill bands and material rulings remain explicit in the new integration guide and internal checklist. Private evidence: 20260921-moritani-skills. Verified source tree: d5078661ecc2199b2bbfebd18034b16585d06410a697a78dc514bfa5b3b77a1c
This commit is contained in:
1 parent
e7e16024b7
commit
489bd3265d
29 files changed
+1474
-126
No files matched your search
+2
-3
@@ -83,10 +83,9 @@ import {
|
||||
fremenReserveEntry,
|
||||
guildTransportCost,
|
||||
} from './bot-mobility';
|
||||
import { planetologistLeader } from './planetologist-movement';
|
||||
import { planetologistMovementModeSupported, planetologistLeader } from './planetologist-movement';
|
||||
import { sandmasterDefaultChoice } from './sandmaster-movement';
|
||||
import { sandmasterWormCollection } from './sandmaster-worm';
|
||||
import { faction } from './catalog';
|
||||
import { richeseCardDefinition } from './richese-cards';
|
||||
import { presenceAt } from './force-presence';
|
||||
import { botArrivalBlock } from './bot-arrival';
|
||||
@@ -3301,7 +3300,7 @@ function policyActions(g: GameView): Action[] {
|
||||
movementAvailable &&
|
||||
planetologist &&
|
||||
!g.ornithopter?.active &&
|
||||
g.players.every((player) => faction(player.faction).expansion === 'base')
|
||||
planetologistMovementModeSupported(g)
|
||||
) {
|
||||
const usefulTargets = targets
|
||||
.filter(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { basicMoritaniLeaderSkillsProfile, noOtherLeaderSkillModules, type LeaderSkillProfile } from './leader-skill-profile';
|
||||
export type BureaucratPaymentKind = 'auction' | 'shipment' | 'bribe';
|
||||
export type BureaucratPaymentSource = {
|
||||
event: string;
|
||||
@@ -33,25 +34,11 @@ export class BureaucratPaymentError extends Error {}
|
||||
export function bureaucratPaymentSignature(value: object): string {
|
||||
return JSON.stringify({ ...value, signature: undefined });
|
||||
}
|
||||
export function bureaucratPaymentModeSupported(game: {
|
||||
expansions: readonly string[];
|
||||
homeworlds?: unknown;
|
||||
nexusCards?: unknown;
|
||||
discoveries?: unknown;
|
||||
discoveryEnabled?: unknown;
|
||||
strongholdCards?: unknown;
|
||||
techTokens?: unknown;
|
||||
}): boolean {
|
||||
return (
|
||||
export function bureaucratPaymentModeSupported(game: LeaderSkillProfile): boolean {
|
||||
return noOtherLeaderSkillModules(game) &&
|
||||
(!game.expansions.length ||
|
||||
(game.expansions.length === 1 && game.expansions[0] === 'choam')) &&
|
||||
!game.homeworlds &&
|
||||
!game.nexusCards &&
|
||||
!game.discoveries &&
|
||||
!game.discoveryEnabled &&
|
||||
!game.strongholdCards &&
|
||||
!game.techTokens
|
||||
);
|
||||
(game.expansions.length === 1 && game.expansions[0] === 'choam') ||
|
||||
basicMoritaniLeaderSkillsProfile(game));
|
||||
}
|
||||
export function bureaucratUsed(
|
||||
used: readonly BureaucratPaymentUse[],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ordinaryLeaderSkillModeSupported, type LeaderSkillProfile } from './leader-skill-profile';
|
||||
import { baseDeck, type Card } from './cards';
|
||||
import type { BattleLeaderSkill } from './leader-skill-combat';
|
||||
|
||||
@@ -69,22 +70,6 @@ export function copiedDiplomatDefense(
|
||||
return { id: card, name: 'Diplomat defense', kind: quote.kind };
|
||||
}
|
||||
|
||||
export function diplomatDefenseModeSupported(game: {
|
||||
expansions: readonly string[];
|
||||
homeworlds?: unknown;
|
||||
nexusCards?: unknown;
|
||||
discoveries?: unknown;
|
||||
discoveryEnabled?: unknown;
|
||||
strongholdCards?: unknown;
|
||||
techTokens?: unknown;
|
||||
}): boolean {
|
||||
return (
|
||||
!game.expansions.length &&
|
||||
!game.homeworlds &&
|
||||
!game.nexusCards &&
|
||||
!game.discoveries &&
|
||||
!game.discoveryEnabled &&
|
||||
!game.strongholdCards &&
|
||||
!game.techTokens
|
||||
);
|
||||
export function diplomatDefenseModeSupported(game: LeaderSkillProfile): boolean {
|
||||
return ordinaryLeaderSkillModeSupported(game);
|
||||
}
|
||||
+10
-8
@@ -1,3 +1,4 @@
|
||||
import { basicMoritaniLeaderSkillsProfile, ordinaryLeaderSkillModeSupported } from './leader-skill-profile';
|
||||
import { bribeTimingBlock, maximumBribe, type BribeOptions } from './bribe-options';
|
||||
import { quoteSpicePlacement, stormExposesTerritory, stormSectorAfter, wormConsumesForces } from './disaster-rules';
|
||||
import { isStormCardDistance, type StormCardComponent } from './storm-cards';
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
import { quoteSukRescue, sukReceiptSignature, sukRescueOptions, type SukForceGroup, type SukRescueOption, type SukRescueReceipt } from './suk-graduate';
|
||||
import { leaderSkillStrongholdCount, sandmasterVictorySpice } from './leader-skill-battle-board';
|
||||
import { beginRihani, chooseRihaniDraw, finishRihani, validateRihani, type RihaniReceipt, type RihaniSkill } from './rihani-decipherer';
|
||||
import { planetologistLeader, planetologistRange, type PlanetologistMovement } from './planetologist-movement';
|
||||
import { planetologistMovementModeSupported, planetologistLeader, planetologistRange, type PlanetologistMovement } from './planetologist-movement';
|
||||
import { quoteSmugglerNoField, smugglerNoFieldModeSupported, type SmugglerNoFieldCompanion } from './smuggler-no-field';
|
||||
import { quoteSmugglerShipment, type SmugglerShipment } from './smuggler-shipment';
|
||||
import { createSmugglerBattle, settleSmugglerBattle, smugglerBattleModeSupported, smugglerBattlePlanBlock, smugglerBattlePile, smugglerBattleSignature, type SmugglerBattleReceipt } from './smuggler-battle';
|
||||
@@ -5902,7 +5903,8 @@ export function initializeLeaderSkillsGameForAudit(state: Game): Game {
|
||||
requireRule(!state.leaderSkills, 'Leader Skills cannot redeal existing skill cards.');
|
||||
const g = structuredClone(state);
|
||||
g.leaderSkills = createLeaderSkills(random);
|
||||
return initializeSetupGameForAudit(g, false, false, false, false, true, g.expansions.length === 1 && g.expansions[0] === 'choam');
|
||||
return initializeSetupGameForAudit(g, false, false, false, false, true,
|
||||
g.expansions.length === 1 && g.expansions[0] === 'choam', basicMoritaniLeaderSkillsProfile(g));
|
||||
}
|
||||
/** Gated development setup for the independent three-card Ecaz variant. */
|
||||
export function initializeEcazTreacheryGameForAudit(state: Game): Game {
|
||||
@@ -9398,7 +9400,7 @@ function validatePlanetologistMove(g: Game, p: Player, move: Pick<MovementOrder,
|
||||
(skill.mode === 'range' || skill.mode === 'gather') &&
|
||||
skill.leader === planetologistLeader(g, p.id),
|
||||
'This Planetologist movement no longer has its living, uncaptured skilled leader.');
|
||||
requireRule(g.players.every(player => faction(player.faction).expansion === 'base'),
|
||||
requireRule(planetologistMovementModeSupported(g),
|
||||
'Planetologist movement combined with expansion factions is still being implemented.');
|
||||
requireRule(!move.noField && !move.ornithopterEvent && !move.discoveryFlight,
|
||||
'Planetologist combined with special movement cards or No-Field is still being implemented.');
|
||||
@@ -15615,12 +15617,12 @@ function currentBattleResolutionQuote(g: Game, canceledVoter?: string) {
|
||||
!g.advanced || getPlayer(g, quote.winner!).faction !== 'atreides',
|
||||
'Suk Graduate rescue for Advanced Atreides awaits the Kwisatz Haderach loss-count ruling.');
|
||||
if (quote.sukGraduate) requireRule(
|
||||
!g.homeworlds && !g.nexusCards && !g.discoveryEnabled && !g.strongholdCards && !g.techTokens && !g.expansions.length &&
|
||||
g.players.every((p) => faction(p.faction).expansion === 'base'),
|
||||
ordinaryLeaderSkillModeSupported(g) &&
|
||||
(basicMoritaniLeaderSkillsProfile(g) || g.players.every((p) => faction(p.faction).expansion === 'base')),
|
||||
'Suk Graduate rescue with expansion factions or other optional modules is still being implemented.');
|
||||
if (quote.rihani || quote.sandmaster) requireRule(
|
||||
!g.homeworlds && !g.nexusCards && !g.discoveryEnabled && !g.strongholdCards && !g.techTokens && !g.expansions.length &&
|
||||
g.players.every((p) => faction(p.faction).expansion === 'base'),
|
||||
ordinaryLeaderSkillModeSupported(g) &&
|
||||
(basicMoritaniLeaderSkillsProfile(g) || g.players.every((p) => faction(p.faction).expansion === 'base')),
|
||||
'These Leader Skill victory effects with expansion factions or other optional modules are still being integrated.');
|
||||
const smuggler = b.smugglerCollection ? nexusRule(() => settleSmugglerBattle(b.smugglerCollection!,
|
||||
!(b.smugglerCollection!.player === b.attacker ? quote.leaderDeaths.attacker : quote.leaderDeaths.defender), g.spice)) : null;
|
||||
@@ -18714,7 +18716,7 @@ function validatePhysicalShipment(g: Game, shipment: PendingShipment) {
|
||||
if (shipment.smuggler) requireRule(
|
||||
!shipment.source && !shipment.noField && !shipment.alliedNoField &&
|
||||
!shipment.guildSecretEvent && !shipment.guildNexusEvent && !shipment.nexusEvent &&
|
||||
!g.expansions.length && !g.homeworlds && !g.nexusCards && !g.discoveryEnabled && !g.strongholdCards && !g.techTokens,
|
||||
ordinaryLeaderSkillModeSupported(g),
|
||||
'Smuggler shipping with expansion modules is still being integrated.',
|
||||
);
|
||||
if (shipment.guildSecretEvent) {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { FACTIONS } from './catalog';
|
||||
|
||||
/** Public configuration only; never gate a skill on a hidden card or random deal. */
|
||||
export type LeaderSkillProfile = {
|
||||
expansions: readonly string[];
|
||||
advanced?: boolean;
|
||||
players?: readonly { faction: string }[];
|
||||
homeworlds?: unknown;
|
||||
nexusCards?: unknown;
|
||||
discoveries?: unknown;
|
||||
discoveryEnabled?: unknown;
|
||||
strongholdCards?: unknown;
|
||||
techTokens?: unknown;
|
||||
ecazTreachery?: unknown;
|
||||
};
|
||||
|
||||
export function noOtherLeaderSkillModules(game: LeaderSkillProfile): boolean {
|
||||
return !game.homeworlds && !game.nexusCards && !game.discoveries &&
|
||||
!game.discoveryEnabled && !game.strongholdCards && !game.techTokens && !game.ecazTreachery;
|
||||
}
|
||||
|
||||
/** Bounded integration of the ordinary Moritani roster, not Advanced assassination. */
|
||||
export function basicMoritaniLeaderSkillsProfile(game: LeaderSkillProfile): boolean {
|
||||
return game.advanced === false && game.expansions.length === 1 &&
|
||||
game.expansions[0] === 'ecaz' && noOtherLeaderSkillModules(game) &&
|
||||
!!game.players?.some(player => player.faction === 'moritani') &&
|
||||
game.players.every(player => player.faction === 'moritani' ||
|
||||
FACTIONS.some(faction => faction.id === player.faction && faction.expansion === 'base'));
|
||||
}
|
||||
|
||||
/** Shared by rule quotes, controls and minimal legal bot participation. */
|
||||
export function ordinaryLeaderSkillModeSupported(game: LeaderSkillProfile): boolean {
|
||||
return noOtherLeaderSkillModules(game) &&
|
||||
(!game.expansions.length || basicMoritaniLeaderSkillsProfile(game));
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { FACTIONS } from './catalog';
|
||||
import { basicMoritaniLeaderSkillsProfile } from './leader-skill-profile';
|
||||
import type { Game, GameView } from './engine';
|
||||
|
||||
export type PlanetologistMoveMode = 'range' | 'gather';
|
||||
@@ -35,3 +37,9 @@ export function planetologistRange(
|
||||
): number {
|
||||
return mode === 'range' ? Math.min(3, base + 1) : base;
|
||||
}
|
||||
|
||||
/** Keep the existing base-roster boundary and add the connected Basic Moritani profile. */
|
||||
export function planetologistMovementModeSupported(game: Game | GameView): boolean {
|
||||
return game.players.every(player => FACTIONS.some(f => f.id === player.faction && f.expansion === 'base')) ||
|
||||
basicMoritaniLeaderSkillsProfile(game);
|
||||
}
|
||||
+24
-1
@@ -4000,6 +4000,28 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Physical custody, commitments, support solver, territory-selection controls, bot and recovery regressions; Emperor Cunning browser checks pass while complete module games remain a separate requirement.', evidence: ['tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/nexus-guild-cunning-bots.test.ts'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'moritani-leader-skills',
|
||||
title: 'Moritani with Leader Skills',
|
||||
category: 'Advanced & expansions',
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Prototyped',
|
||||
summary: 'Basic Moritani and base opponents use the full fourteen-card Leader Skills deck in development tables.',
|
||||
steps: [
|
||||
'Assign skills after starting Treachery Cards and before Traitor choices. Moritani places its six starting forces last. All fourteen skills remain in the physical deck; each player keeps one of two private choices and publicly assigns it to an eligible leader.',
|
||||
'Connected movement, shipping, payment and battle skills use the same controls and legal quotes as the base profile. Terror Assassination returns a killed leader’s exact skill once; a later own-leader revival offers the normal private optional replacement. Sabotage affects Treachery Cards, not skills.',
|
||||
'An Enemy of My Enemy alliance preserves each faction’s assignments. A losing ally cannot retain a used Planetologist Special or the Worthless card used for a Diplomat copy, because those cards must be discarded.',
|
||||
'This profile uses Basic rules, Moritani with base opponents and no other optional modules. Existing unfinished Leader Skill effects and disputed cases remain unfinished. Advanced Moritani, Ecaz and further roster/module combinations need separate integration; ordinary public starts remain disabled.',
|
||||
],
|
||||
related: ['leader-skills', 'expansion-faction-games', 'implementation-checklist'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Genuine setup and shared public-profile guards connect existing skill effects to Basic Moritani. Terror custody and ally-retention disposal reuse settled contracts; unfinished effects remain explicit.', evidence: ['game/leader-skill-profile.ts', 'game/engine.ts'] },
|
||||
{ area: 'Player controls', status: 'Implemented', detail: 'Existing private offers, public assignments, battle and revival choices are reused; Planetologist controls share the expanded server predicate.', evidence: ['components/leader-skills.tsx', 'components/planetologist-movement.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'Existing profiles retain minimal legal participation through shared public choices. Strategy and difficulty calibration remain deferred until game features are complete.', evidence: ['game/bots.ts', 'tools/faction-games.ts'] },
|
||||
{ area: 'Documentation', status: 'Implemented', detail: 'The integration guide records entry, exact scope, existing source contracts and remaining effects without claiming full module compliance.', evidence: ['docs/MORITANI_LEADER_SKILLS.md'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Genuine setup, all physical assignments, Terror death/revival, movement/payment/battle continuations, mandatory retention exclusions and authenticated concurrent recovery have focused checks. Sample and browser results are recorded separately; full combined-rule acceptance remains open.', evidence: ['tests/moritani-skills-integration.test.ts', 'tests/moritani-skills-custody.test.ts', 'tests/moritani-skills-battle.test.ts', 'tests/moritani-skills-recovery.test.ts', 'tests/moritani-skills-worm.test.ts'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'leader-skills',
|
||||
title: 'Leader Skills',
|
||||
@@ -4008,6 +4030,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
developmentStage: 'Prototyped',
|
||||
summary: 'Fourteen physical skills, private assignment choices, five battle disciplines, Planetologist movement/battle effects and Suk Graduate rescues, Mentat/Bureaucrat scoring and Bureaucrat payment redirection, Sandmaster victory spice, Rihani exchanges, optional Smuggler shipping and unmodified battle collection, Sandmaster route and worm-ride collection, sealed Spice Banker battle spending and optional Diplomat base-defense copying in development tables. Other effects and combined-module acceptance remain unfinished.',
|
||||
steps: [
|
||||
'A development profile also connects Basic Moritani with base opponents and no other optional modules. It keeps the full deck and existing unfinished-effect boundaries; see Moritani with Leader Skills.',
|
||||
'With this optional module, starting Treachery Cards are dealt after prediction and before skill assignment. Every faction privately receives two skills, keeps one, returns the other to the shuffled deck, and assigns its chosen card to an eligible leader. Skill and leader become public before traitor selection.',
|
||||
'Before Battle Plans, leave the skilled leader face up to keep its normal effect, or move it behind the shield for possible use. A face-up leader cannot be selected for the plan. Concealing it while choosing another leader gives neither effect. A leader that must be used because no alternative remains moves behind the shield automatically.',
|
||||
'Warmaster, Master of Assassins, Swordmaster of Ginaz, Killer Medic and Prana-Bindu Adept are connected. They give another leader +1 for the matching physical card role, or the surviving assigned leader +3. These bonuses do not stack to +4. They change battle strength, not the printed disc value or leader bounty.',
|
||||
@@ -4028,7 +4051,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Other special Sandmaster relocation, Spice Banker normal income and Diplomat retreat remain missing. Foreign gholas, Face Dancers and combined modules remain unfinished. This development prototype does not enable public module starts.',
|
||||
...LEADER_SKILL_CARDS.map(card => `${card.name}. Normal: ${card.normal.join(' ')} Skilled battle: ${card.battle.join(' ')}`),
|
||||
],
|
||||
related: ['setup','choam-modules','implementation-checklist'],
|
||||
related: ['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; Spice Banker normal income, other remaining bands, Diplomat retreat 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/sandmaster-movement.ts','game/sandmaster-worm.ts','game/diplomat-defense.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. 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/game-table.tsx'] },
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ordinaryLeaderSkillModeSupported } from './leader-skill-profile';
|
||||
import type { Action, Game, GameView } from './engine';
|
||||
import { GRAPH, location, mobileRouteDistance, splitLocation } from './board';
|
||||
import { isAdvisor } from './advisors';
|
||||
@@ -32,15 +33,7 @@ const record = (value: unknown): value is Record<string, unknown> =>
|
||||
!!value && typeof value === 'object' && !Array.isArray(value);
|
||||
|
||||
export function sandmasterModeSupported(g: Game | GameView): boolean {
|
||||
return (
|
||||
!g.expansions.length &&
|
||||
!g.homeworlds &&
|
||||
!g.nexusCards &&
|
||||
!g.discoveries &&
|
||||
!('discoveryEnabled' in g && g.discoveryEnabled) &&
|
||||
!g.strongholdCards &&
|
||||
!g.techTokens
|
||||
);
|
||||
return ordinaryLeaderSkillModeSupported(g);
|
||||
}
|
||||
export function sandmasterLeader(
|
||||
g: Game | GameView,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Game, GameView } from './engine';
|
||||
import { splitLocation, validGameLocation } from './board';
|
||||
import { faction } from './catalog';
|
||||
import { basicMoritaniLeaderSkillsProfile } from './leader-skill-profile';
|
||||
import { sandmasterLeader, sandmasterModeSupported } from './sandmaster-movement';
|
||||
|
||||
export type SandmasterWormCollection = {
|
||||
@@ -25,7 +26,8 @@ export function sandmasterWormCollection(
|
||||
const blocked = (reason: string): SandmasterWormCollection =>
|
||||
({ leader, key: null, before: 0, blocked: reason });
|
||||
if (!sandmasterModeSupported(game) || game.ecazTreachery ||
|
||||
game.players.some(p => faction(p.faction).expansion !== 'base'))
|
||||
!(basicMoritaniLeaderSkillsProfile(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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FACTIONS } from './catalog';
|
||||
import { basicMoritaniLeaderSkillsProfile, ordinaryLeaderSkillModeSupported, type LeaderSkillProfile } from './leader-skill-profile';
|
||||
import type { Card, Leader } from './cards';
|
||||
import { faction } from './catalog';
|
||||
import { splitLocation, validGameLocation } from './board';
|
||||
import { leaderSkillBattleBonus, usesSurvivingSkilledLeader, type BattleLeaderSkill } from './leader-skill-combat';
|
||||
|
||||
@@ -8,13 +9,9 @@ export type SmugglerBattleReceipt = {
|
||||
frame: string; strength: number; key: string | null; before: number; amount: number;
|
||||
stage: 'pending' | 'collected' | 'void'; signature: string;
|
||||
};
|
||||
export function smugglerBattleModeSupported(g: {
|
||||
expansions: readonly string[]; players: readonly { faction: string }[];
|
||||
homeworlds?: unknown; nexusCards?: unknown; discoveries?: unknown;
|
||||
discoveryEnabled?: boolean; strongholdCards?: unknown; techTokens?: unknown; ecazTreachery?: unknown;
|
||||
}) {
|
||||
return !g.expansions.length && g.players.every(p => faction(p.faction)?.expansion === 'base') &&
|
||||
!g.homeworlds && !g.nexusCards && !g.discoveries && !g.discoveryEnabled && !g.strongholdCards && !g.techTokens && !g.ecazTreachery;
|
||||
export function smugglerBattleModeSupported(g: LeaderSkillProfile & { players: readonly { faction: string }[] }) {
|
||||
return ordinaryLeaderSkillModeSupported(g) &&
|
||||
(basicMoritaniLeaderSkillsProfile(g) || g.players.every(p => FACTIONS.some(f => f.id === p.faction && f.expansion === 'base')));
|
||||
}
|
||||
export type SmugglerBattlePlan = {
|
||||
assignments: readonly BattleLeaderSkill[];
|
||||
|
||||
+3
-18
@@ -1,3 +1,4 @@
|
||||
import { ordinaryLeaderSkillModeSupported, type LeaderSkillProfile } from './leader-skill-profile';
|
||||
import {
|
||||
usesSurvivingSkilledLeader,
|
||||
type BattleLeaderSkill,
|
||||
@@ -6,24 +7,8 @@ import {
|
||||
export class SpiceBankerError extends Error {}
|
||||
|
||||
/** The same development boundary applies to declarations, saved plans, controls and bots. */
|
||||
export function spiceBankerModeSupported(game: {
|
||||
expansions: readonly string[];
|
||||
homeworlds?: unknown;
|
||||
nexusCards?: unknown;
|
||||
discoveries?: unknown;
|
||||
discoveryEnabled?: boolean;
|
||||
strongholdCards?: unknown;
|
||||
techTokens?: unknown;
|
||||
}): boolean {
|
||||
return (
|
||||
!game.expansions.length &&
|
||||
!game.homeworlds &&
|
||||
!game.nexusCards &&
|
||||
!game.discoveries &&
|
||||
!game.discoveryEnabled &&
|
||||
!game.strongholdCards &&
|
||||
!game.techTokens
|
||||
);
|
||||
export function spiceBankerModeSupported(game: LeaderSkillProfile): boolean {
|
||||
return ordinaryLeaderSkillModeSupported(game);
|
||||
}
|
||||
|
||||
export function spiceBankerBattleMaximum(
|
||||
|
||||
Reference in new issue
Block a user