Add a local faction-only prototype profile for all seven nonempty expansion selections, genuine Basic/Advanced initialization and existing private setup, controls, AI and saved continuation. Ecaz allocates six forces across Imperial Basin sectors and retains fourteen reserves before later setup; preserve Fremen, Advanced BG and Moritani ordering with printed and inferred boundaries explicit. Keep optional Ecaz Treachery cards separate, ordinary deck counts 33/35/47, and Richese cache separate. Public start and publication gates remain closed. Replace the Advanced Ixian/Richese pre-offer stop with an explicit per-lot Ixian decline. Bind the saved original offer to its round, turn, owner, independent event, exact card custody and terms before view/action/normalization. Decline resumes that unchanged lot once without using Technology; later normal lots still offer the exchange. Protect reserved cards across transfers, spending and random Harkonnen exchange, and preserve legitimate Gift/Truthtrance interruptions. The actual special-lot exchange still awaits replacement-custody interpretation. Validation: focused setup, rules, UI/bot, private JSON/SQLite reload and concurrent CAS tests and independent custody/persistence review passed. Typecheck, lint, 4573 offline tests, production build and 43 HTTP tests passed. Browser Ecaz setup rejected an incomplete total, saved a 2/2/2 split, and refreshed to fourteen reserves and unchanged private cards. A separate genuine Advanced QA game restored the Ixian choice; explicit decline opened the unchanged cache lot with legal bidding and retained private hand, spice, pool count and unused Technology after refresh. Five of six genuine four-profile samples finish: CHOAM Basic 1054 actions/28 JSON restores, CHOAM Advanced 225/6, Ecaz Basic 91/2, Ecaz Advanced 523/14, all-three Basic 476/12. Combined Advanced passes three former auction stops and reaches turn three/action614 with 16 restores, then hits an existing CHOAM movement/Ambassador arrival guard. Ecaz samples also include guarded arrival candidates with legal alternatives. Full combined play is not certified; arrival integration is the next gap. Retain initial failed logs, including a corrected private harness sold-card receipt alias. After the outage, all 597 opening rooms and original seat/recovery records were preserved (632 after QA/HTTP). A stale development module cache required restart after a fresh 598-room backup; all 598 matched afterward and the QA seat restored. Final integrity and foreign keys passed. No database reset, recurring automation or publication. Verified source tree: 5e2ff244a1167f2834bb46d609b8de914a34f7049ff7b06046243068c0935131
45 lines
1.3 KiB
TypeScript
45 lines
1.3 KiB
TypeScript
import { location, territory } from './board';
|
|
|
|
export const ECAZ_START_FORCES = 6;
|
|
export const ECAZ_START_LOCATIONS = Object.freeze(
|
|
territory('imperial_basin').sectors.map((sector) =>
|
|
location('imperial_basin', sector),
|
|
),
|
|
);
|
|
|
|
/** The printed territory and total constrain the allocation; return detached positive groups. */
|
|
export function quoteEcazStartingForces(
|
|
placements: unknown,
|
|
): Record<string, number> {
|
|
if (
|
|
!placements ||
|
|
typeof placements !== 'object' ||
|
|
Array.isArray(placements)
|
|
)
|
|
throw new Error(
|
|
'Choose the Imperial Basin sectors for six Ecaz starting forces.',
|
|
);
|
|
const entries = Object.entries(placements);
|
|
if (
|
|
!entries.every(
|
|
([key, amount]) =>
|
|
ECAZ_START_LOCATIONS.includes(key) &&
|
|
typeof amount === 'number' &&
|
|
Number.isSafeInteger(amount) &&
|
|
amount >= 0 &&
|
|
amount <= ECAZ_START_FORCES,
|
|
)
|
|
)
|
|
throw new Error(
|
|
'Ecaz starting forces require whole nonnegative amounts in explicit Imperial Basin sectors.',
|
|
);
|
|
if (
|
|
entries.reduce((total, [, amount]) => total + amount, 0) !==
|
|
ECAZ_START_FORCES
|
|
)
|
|
throw new Error(
|
|
'Place exactly six Ecaz starting forces in Imperial Basin.',
|
|
);
|
|
return Object.fromEntries(entries.filter(([, amount]) => amount > 0));
|
|
}
|