Connect bounded Orgiz spice transfers to Discovery Collection
This commit is contained in:
1 parent
c9a3625ff2
commit
209fb18a27
11 files changed
+327
-123
No files matched your search
@@ -1,7 +1,8 @@
|
||||
import { fighterCount, isAdvisor } from './advisors';
|
||||
import { isAdvisor } from './advisors';
|
||||
import { gameTerritories, splitLocation, type MobileBoard } from './board';
|
||||
import type { AdvisorRelease } from './board-resolution-quote';
|
||||
import type { Player } from './engine';
|
||||
import { presenceByLocation, type ForcePresence } from './force-presence';
|
||||
import { presenceAt, type ForcePresence } from './force-presence';
|
||||
|
||||
export const CISTERN = 'cistern';
|
||||
export const ORGIZ_PROCESSING_STATION = 'orgiz-processing-station';
|
||||
@@ -32,6 +33,8 @@ export type DiscoveryBaseCollectionReceipt = {
|
||||
};
|
||||
|
||||
export type DiscoveryBaseCollectionQuote = {
|
||||
/** Advisor stance changes already established by the ordinary quote. */
|
||||
released: readonly AdvisorRelease[];
|
||||
/** Deposits remaining after ordinary and shared collection. */
|
||||
spice: Readonly<Record<string, number>>;
|
||||
receipts: readonly DiscoveryBaseCollectionReceipt[];
|
||||
@@ -65,14 +68,6 @@ export type DiscoveryCollectionQuote = {
|
||||
effects: (CisternCollectionReceipt | OrgizCollectionReceipt)[];
|
||||
};
|
||||
|
||||
export type DiscoveryCollectionOptions = {
|
||||
/**
|
||||
* Draft interpretation only. The publisher text does not clarify whether a
|
||||
* stacked physical spice pile represents one or several “spice blows.”
|
||||
*/
|
||||
orgizObservableDeposits?: boolean;
|
||||
};
|
||||
|
||||
const record = (value: unknown): value is Record<string, unknown> =>
|
||||
!!value && typeof value === 'object' && !Array.isArray(value);
|
||||
const whole = (value: unknown): value is number =>
|
||||
@@ -84,29 +79,41 @@ function requireCollection(
|
||||
if (!condition) throw new DiscoveryCollectionError(message);
|
||||
}
|
||||
|
||||
function isCollectingFighter(
|
||||
player: DiscoveryCollectionSeat,
|
||||
territory: string,
|
||||
released: readonly AdvisorRelease[],
|
||||
): boolean {
|
||||
return !isAdvisor(player, territory) ||
|
||||
released.some((entry) => entry.player === player.id && entry.territory === territory);
|
||||
}
|
||||
|
||||
function occupants(
|
||||
context: DiscoveryCollectionContext,
|
||||
location: string,
|
||||
released: readonly AdvisorRelease[],
|
||||
): DiscoveryCollectionSeat[] {
|
||||
if (!gameTerritories(context).some((territory) => territory.id === location))
|
||||
return [];
|
||||
return context.players.filter((player) => fighterCount(player, location) > 0);
|
||||
return context.players.filter(
|
||||
(player) => isCollectingFighter(player, location, released) &&
|
||||
presenceAt(player, location) > 0,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose Cistern, plus an explicitly enabled bounded Orgiz draft, over the
|
||||
* Compose sole-occupant Cistern income and a bounded Orgiz transfer over the
|
||||
* canonical ordinary collection quote. The ordinary `collected` and `desert`
|
||||
* fields stay factual: Cistern is bank income and Orgiz moves spice afterward.
|
||||
* fields remain factual: Cistern is bank income; Orgiz transfers player spice.
|
||||
*
|
||||
* The printed source does not resolve two occupants claiming the same location
|
||||
* or identify a payer before an Ecaz shared lot is allocated. Cistern therefore
|
||||
* pays only an ordinary sole occupant; the unresolved contested case is retained
|
||||
* without a bonus instead of blocking the Collection phase.
|
||||
* The publisher does not specify whether a stacked physical pile is one or
|
||||
* several blows. One positive collected board deposit is treated as one
|
||||
* observable blow. Contested location benefits and unresolved shared lots
|
||||
* withhold only their uncertain effect; they never block ordinary collection.
|
||||
*/
|
||||
export function quoteDiscoveryCollection(
|
||||
context: DiscoveryCollectionContext,
|
||||
collection: DiscoveryBaseCollectionQuote,
|
||||
options: DiscoveryCollectionOptions = {},
|
||||
): DiscoveryCollectionQuote {
|
||||
requireCollection(
|
||||
record(context) &&
|
||||
@@ -130,10 +137,13 @@ export function quoteDiscoveryCollection(
|
||||
record(collection.spice) &&
|
||||
Array.isArray(collection.receipts) &&
|
||||
Array.isArray(collection.shared) &&
|
||||
record(options) &&
|
||||
Object.keys(options).every((key) => key === 'orgizObservableDeposits') &&
|
||||
(options.orgizObservableDeposits === undefined ||
|
||||
typeof options.orgizObservableDeposits === 'boolean'),
|
||||
Array.isArray(collection.released) &&
|
||||
collection.released.every(
|
||||
(entry) => record(entry) &&
|
||||
typeof entry.player === 'string' &&
|
||||
context.order.includes(entry.player) &&
|
||||
typeof entry.territory === 'string',
|
||||
),
|
||||
'Discovery collection needs the canonical public collection context and quote.',
|
||||
);
|
||||
|
||||
@@ -195,7 +205,7 @@ export function quoteDiscoveryCollection(
|
||||
);
|
||||
|
||||
const effects: DiscoveryCollectionQuote['effects'] = [];
|
||||
const cistern = occupants(context, CISTERN);
|
||||
const cistern = occupants(context, CISTERN, collection.released);
|
||||
if (cistern.length === 1) {
|
||||
const receipt = receipts.find((entry) => entry.player === cistern[0].id)!;
|
||||
requireCollection(
|
||||
@@ -211,39 +221,34 @@ export function quoteDiscoveryCollection(
|
||||
});
|
||||
}
|
||||
|
||||
const orgiz = occupants(context, ORGIZ_PROCESSING_STATION);
|
||||
const collectedLocations = sourceKeys.filter(
|
||||
(key) => context.spice[key] > collection.spice[key],
|
||||
);
|
||||
requireCollection(
|
||||
!options.orgizObservableDeposits ||
|
||||
orgiz.length <= 1 ||
|
||||
collectedLocations.length === 0,
|
||||
'Orgiz theft with two occupying factions awaits an explicit ruling.',
|
||||
);
|
||||
if (
|
||||
options.orgizObservableDeposits &&
|
||||
orgiz.length === 1 &&
|
||||
collectedLocations.length > 0
|
||||
) {
|
||||
requireCollection(
|
||||
!collection.shared.some((lot) => lot.amount > 0),
|
||||
'Orgiz theft from an unresolved Ecaz shared lot awaits an explicit ruling.',
|
||||
);
|
||||
const orgiz = occupants(context, ORGIZ_PROCESSING_STATION, collection.released);
|
||||
if (orgiz.length === 1) {
|
||||
const owner = orgiz[0];
|
||||
const ownerReceipt = receipts.find((entry) => entry.player === owner.id)!;
|
||||
for (const key of collectedLocations) {
|
||||
const collectors = context.players.filter(
|
||||
(player) =>
|
||||
!isAdvisor(player, splitLocation(key).territory) &&
|
||||
(presenceByLocation(player)[key] ?? 0) > 0,
|
||||
);
|
||||
requireCollection(
|
||||
collectors.length === 1,
|
||||
'Orgiz theft needs one ordinary collector for each collected spice blow.',
|
||||
);
|
||||
const collector = collectors[0];
|
||||
if (collector.id === owner.id) continue;
|
||||
for (const key of sourceKeys) {
|
||||
if (context.spice[key] <= collection.spice[key]) continue;
|
||||
const { territory, sector } = splitLocation(key);
|
||||
if (
|
||||
collection.shared.some(
|
||||
(lot) => lot.amount > 0 && lot.territory === territory,
|
||||
)
|
||||
) continue;
|
||||
let collector: DiscoveryCollectionSeat | undefined;
|
||||
let contested = false;
|
||||
for (const player of context.players) {
|
||||
if (!isCollectingFighter(player, territory, collection.released)) continue;
|
||||
const marker = player.noField?.deployed?.location;
|
||||
if (
|
||||
(player.forces[key] ?? 0) <= 0 &&
|
||||
!(marker?.territory === territory && marker.sector === sector)
|
||||
) continue;
|
||||
if (collector) {
|
||||
contested = true;
|
||||
break;
|
||||
}
|
||||
collector = player;
|
||||
}
|
||||
if (contested || !collector || collector.id === owner.id) continue;
|
||||
const collectorReceipt = receipts.find(
|
||||
(entry) => entry.player === collector.id,
|
||||
)!;
|
||||
|
||||
@@ -14110,7 +14110,6 @@ function commitCollection(
|
||||
) {
|
||||
let discovery: ReturnType<typeof quoteDiscoveryCollection>;
|
||||
try {
|
||||
// Orgiz remains disabled pending a source ruling on stacked spice blows.
|
||||
discovery = quoteDiscoveryCollection(g, quote);
|
||||
} catch (error) {
|
||||
if (error instanceof DiscoveryCollectionError)
|
||||
|
||||
+6
-6
@@ -3887,7 +3887,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
category: 'Advanced & expansions',
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Prototyped',
|
||||
summary: 'A development prototype connects Great Maker, six Discovery spice blows, private token inspection, stash rewards, revealed locations, next-turn entry, Cistern income, Ornithopter movement, Jacurutu income, Testing Station and Shrine. Remaining interpretations and normal public starts stay gated.',
|
||||
summary: 'A development prototype connects Great Maker, six Discovery spice blows, private token inspection, stash rewards, revealed locations, next-turn entry, Cistern income, bounded Orgiz transfers, Ornithopter movement, Jacurutu income, Testing Station and Shrine. Remaining interpretations and normal public starts stay gated.',
|
||||
searchText: 'Hiereg Smuggler Jacurutu Sietch Cistern Ecological Testing Station Shrine Orgiz Processing Station Treachery Card Stash Spice Stash Ornithopter',
|
||||
steps: [
|
||||
'The Discovery prototype starts through genuine base-faction setup in Basic or Advanced, with seven extra Spice Cards and eight physical tokens. Its normal public start gate stays closed while the remaining effects are connected and verified.',
|
||||
@@ -3898,7 +3898,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Revealing Ornithopter records who carries it and the turn it was gained. On a later turn, its owner may spend it on one ordinary movement action to give that selected group a fixed range of three. It grants no extra action, adds no shipment and is removed only after the movement commits; saved arrival interactions retain the exact selected group.',
|
||||
'The five revealed location tokens become separate, initially empty territories inside their surroundings. A private peek does not open the location. Normal ground movement enters through the surrounding territory, shipment uses stronghold prices, at most two factions may occupy it, and forces inside are protected from storms and sandworms. Only Jacurutu Sietch counts toward stronghold victory.',
|
||||
'At the start of the next turn, before the storm and mobile stronghold movement, each eligible faction may move any positive subset of its non-advisor groups from the surrounding territory into the newly revealed location. The signed choice preserves ordinary and elite forces across sectors, normal source storm and occupancy restrictions, and costs no spice, shipment or normal movement. The printed text gives no sequence and does not restate the source-storm restriction; processing tokens in reveal order and factions in storm order while excluding storm sources are source inferences.',
|
||||
'A sole occupant of Cistern receives two bank spice during Collection. The printed text does not settle which of two occupants receives this benefit, so the contested bonus is withheld without blocking the phase. Orgiz transfer remains unavailable pending a ruling on what counts as each collected spice blow.',
|
||||
'A sole occupant of Cistern receives two bank spice during Collection; contested occupancy withholds that bonus without blocking the phase. A sole Orgiz occupant takes one spice from each rival who collected a positive board deposit, using each collected pile as one observable blow. This is a provisional interpretation of the printed blow wording. Contested Orgiz occupancy and unresolved Ecaz shared lots withhold only the uncertain transfer while ordinary collection continues.',
|
||||
'Jacurutu automatically pays one bank spice for each opposing undialed physical force sent to the Tanks. Where Advanced normal/elite allocations would produce different rewards, this prototype records the unpaid gap pending a ruling; it never guesses from the numeric wheel alone.',
|
||||
'A sole non-advisor occupant of Ecological Testing Station may decrease ordinary storm movement by one, keep it, or increase it by one. Choose after the ordinary storm-card window, before movement and storm protection. Weather Control cannot be changed. Two-occupant adjustment order remains unresolved. An already saved pending storm without its original movement source keeps its distance until a new storm is produced.',
|
||||
'A current non-advisor Shrine occupant may play a physical Truthtrance card as Karama, or a physical Karama as Truthtrance, within the implemented timing and effect paths. The printed identity and actual card custody remain unchanged; a committed Truthtrance conversion survives departure. This does not create missing Karama powers or settle their pending rules.',
|
||||
@@ -3908,12 +3908,12 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
checklist: [
|
||||
{
|
||||
area: 'Implementation', status: 'Partial',
|
||||
detail: 'Working prototypes cover genuine setup, physical inventory, destructive blows, Great Maker vote/ride, private inspection/reveal, stash rewards, public nested locations, signed next-turn entry, sole-occupant Cistern income and later-turn Ornithopter spending, unambiguous Jacurutu income, sole-occupant Testing Station and Shrine card roles. Orgiz and material interpretations remain pending.',
|
||||
detail: 'Working prototypes cover genuine setup, physical inventory, destructive blows, Great Maker vote/ride, private inspection/reveal, stash rewards, public nested locations, signed next-turn entry, sole-occupant Cistern income, bounded Orgiz transfers and later-turn Ornithopter spending, unambiguous Jacurutu income, sole-occupant Testing Station and Shrine card roles. Contested and mixed interpretations remain pending.',
|
||||
evidence: ['game/discoveries.ts', 'game/discovery-actions.ts', 'game/discovery-entry.ts', 'game/discovery-collection.ts', 'game/discovery-flight.ts', 'game/discovery-battle.ts', 'game/discovery-storm.ts', 'game/shrine.ts', 'game/great-maker.ts', 'game/board.ts', 'game/engine.ts'],
|
||||
},
|
||||
{
|
||||
area: 'Player controls', status: 'Partial',
|
||||
detail: 'Private backs and readable authorized faces, optional inspection/reveal, owned stash discard with card inspection, ordered Great Maker controls, multi-sector free entry and carried-Ornithopter movement use shared table controls. Testing Station offers three legal distances and Shrine uses existing card controls. Unambiguous Jacurutu income is automatic; unresolved rewards have no invented choice.',
|
||||
detail: 'Private backs and readable authorized faces, optional inspection/reveal, owned stash discard with card inspection, ordered Great Maker controls, multi-sector free entry and carried-Ornithopter movement use shared table controls. Testing Station offers three legal distances and Shrine uses existing card controls. Unambiguous Jacurutu and bounded Orgiz income are automatic and logged; unresolved rewards have no invented choice.',
|
||||
evidence: ['components/discoveries.tsx', 'components/discovery-entry.tsx', 'components/discovery-flight-movement.tsx', 'components/discovery-storm.tsx', 'components/truthtrance.tsx', 'components/great-maker.tsx', 'components/spice-card-inspector.tsx', 'components/game-table.tsx'],
|
||||
},
|
||||
{
|
||||
@@ -3923,7 +3923,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
},
|
||||
{
|
||||
area: 'Documentation', status: 'Partial',
|
||||
detail: 'Sourced identities, connected behavior and the remaining dependency queue are recorded. Great Maker first-turn/Sandtrout handling, exhausted token supply, free-entry order and source-storm handling remain identified interpretations for refinement; contested Cistern/Testing Station, mixed-force Jacurutu and Orgiz collection cases remain unresolved.',
|
||||
detail: 'Sourced identities, connected behavior and the remaining dependency queue are recorded. Great Maker first-turn/Sandtrout handling, exhausted token supply, free-entry order, source-storm handling and Orgiz per-collected-deposit interpretation remain explicit inferences; contested Cistern/Testing Station/Orgiz, mixed-force Jacurutu and shared-lot theft remain unresolved.',
|
||||
evidence: ['docs/DISCOVERY_COMPONENTS.md', 'docs/DISCOVERY_PROTOTYPE.md'],
|
||||
},
|
||||
{
|
||||
@@ -4294,7 +4294,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
...(id === 'ecaz-modules'
|
||||
? [
|
||||
'Ecaz inventory, public Ambassador inspection, end-of-Revival placement, six entry effects with Bene Gesserit copies and direct Ecaz Duke acquisition have focused support. Storm/explosion token returns are supported. Other effects and competing arrival ordering remain unfinished. See the linked feature checklist; full expansion starts remain disabled.',
|
||||
'The Discovery prototype connects seven Spice Cards, eight tokens, Great Maker, private Collection inspection/reveal, stash rewards, revealed nested locations, next-turn free entry, sole-occupant Cistern income and later-turn Ornithopter movement. Jacurutu income, Testing Station and Shrine now have bounded prototypes. Orgiz, mixed-force Jacurutu and contested benefits remain pending; see the Discovery checklist.',
|
||||
'The Discovery prototype connects seven Spice Cards, eight tokens, Great Maker, private Collection inspection/reveal, stash rewards, revealed nested locations, next-turn free entry, sole-occupant Cistern income, bounded Orgiz transfers and later-turn Ornithopter movement. Jacurutu income, Testing Station and Shrine also have bounded prototypes. Mixed-force Jacurutu and contested benefits remain pending; see the Discovery checklist.',
|
||||
]
|
||||
: []),
|
||||
'The expansion catalog is present. Expansion faction mechanics, remaining modules and their full interaction reference remain in development.',
|
||||
|
||||
Reference in new issue
Block a user