Connect multi-occupant Cistern and Orgiz Collection
This commit is contained in:
1 parent
554f2bf9e9
commit
388c5fa8d5
12 files changed
+536
-119
No files matched your search
@@ -2,6 +2,7 @@ import { isAdvisor } from './advisors';
|
||||
import { gameTerritories, splitLocation, type MobileBoard } from './board';
|
||||
import type { AdvisorRelease } from './board-resolution-quote';
|
||||
import type { Player } from './engine';
|
||||
import type { SpiceAllocationReceipt } from './ecaz-spice-allocation';
|
||||
import { presenceAt, type ForcePresence } from './force-presence';
|
||||
|
||||
export const CISTERN = 'cistern';
|
||||
@@ -89,7 +90,7 @@ function isCollectingFighter(
|
||||
released.some((entry) => entry.player === player.id && entry.territory === territory);
|
||||
}
|
||||
|
||||
function occupants(
|
||||
export function discoveryCollectionOccupants(
|
||||
context: DiscoveryCollectionContext,
|
||||
location: string,
|
||||
released: readonly AdvisorRelease[],
|
||||
@@ -99,7 +100,7 @@ function occupants(
|
||||
return context.players.filter(
|
||||
(player) => isCollectingFighter(player, location, released) &&
|
||||
presenceAt(player, location) > 0,
|
||||
);
|
||||
).sort((left,right) => context.order.indexOf(left.id)-context.order.indexOf(right.id));
|
||||
}
|
||||
|
||||
/** Public effective presence only; an ambiguous source cannot identify a payer. */
|
||||
@@ -132,9 +133,9 @@ function transferOrgiz(
|
||||
) {
|
||||
if (!collector || collector.id === ownerReceipt.player) return;
|
||||
const collectorReceipt = receipts.find(entry => entry.player === collector.id)!;
|
||||
if (collectorReceipt.balance === 0) return;
|
||||
requireCollection(
|
||||
collectorReceipt.balance >= 1 &&
|
||||
Number.isSafeInteger(ownerReceipt.balance + 1),
|
||||
Number.isSafeInteger(ownerReceipt.balance + 1),
|
||||
'Orgiz theft would invalidate a player spice balance.',
|
||||
);
|
||||
collectorReceipt.balance -= 1;
|
||||
@@ -146,14 +147,14 @@ function transferOrgiz(
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose sole-occupant Cistern income and a bounded Orgiz transfer over the
|
||||
* canonical ordinary collection quote. The ordinary `collected` and `desert`
|
||||
* fields remain factual: Cistern is bank income; Orgiz transfers player spice.
|
||||
* Compose each eligible occupant's Cistern income and original Orgiz transfers
|
||||
* over the canonical ordinary quote. Ordinary `collected` and `desert` remain
|
||||
* factual: Cistern is bank income; Orgiz transfers available player spice.
|
||||
*
|
||||
* Advanced uses the authorized revised rulebook's territory Collection event;
|
||||
* Basic retains the provisional one-positive-deposit interpretation. Contested
|
||||
* sources, different territory payers and shared lots withhold only their
|
||||
* uncertain effect; they never block ordinary collection.
|
||||
* Owners resolve in original storm order. Advanced uses territory Collection;
|
||||
* Basic retains the provisional positive-deposit trigger. Ambiguous collectors
|
||||
* and shared lots still withhold only their uncertain effect. Empty purses
|
||||
* cannot create debt or block ordinary Collection.
|
||||
*/
|
||||
export function quoteDiscoveryCollection(
|
||||
context: DiscoveryCollectionContext,
|
||||
@@ -250,9 +251,9 @@ export function quoteDiscoveryCollection(
|
||||
);
|
||||
|
||||
const effects: DiscoveryCollectionQuote['effects'] = [];
|
||||
const cistern = occupants(context, CISTERN, collection.released);
|
||||
if (cistern.length === 1) {
|
||||
const receipt = receipts.find((entry) => entry.player === cistern[0].id)!;
|
||||
const cistern = discoveryCollectionOccupants(context, CISTERN, collection.released);
|
||||
for (const owner of cistern) {
|
||||
const receipt = receipts.find((entry) => entry.player === owner.id)!;
|
||||
requireCollection(
|
||||
Number.isSafeInteger(receipt.balance + 2),
|
||||
'Cistern bank income would overflow the spice balance.',
|
||||
@@ -266,10 +267,9 @@ export function quoteDiscoveryCollection(
|
||||
});
|
||||
}
|
||||
|
||||
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)!;
|
||||
const orgiz = discoveryCollectionOccupants(context, ORGIZ_PROCESSING_STATION, collection.released);
|
||||
if (orgiz.length) {
|
||||
const ownerReceipts = orgiz.map(owner => receipts.find(entry => entry.player === owner.id)!);
|
||||
const transfers = context.advanced
|
||||
? new Map<string, { location: string; collector: DiscoveryCollectionSeat | undefined }>()
|
||||
: undefined;
|
||||
@@ -283,7 +283,8 @@ export function quoteDiscoveryCollection(
|
||||
) continue;
|
||||
const collector = collectorAt(context, key, collection.released);
|
||||
if (!transfers) {
|
||||
transferOrgiz(key, collector, ownerReceipt, receipts, effects);
|
||||
for (const ownerReceipt of ownerReceipts)
|
||||
transferOrgiz(key, collector, ownerReceipt, receipts, effects);
|
||||
continue;
|
||||
}
|
||||
const pending = transfers.get(territory);
|
||||
@@ -293,8 +294,44 @@ export function quoteDiscoveryCollection(
|
||||
}
|
||||
if (transfers)
|
||||
for (const pending of transfers.values())
|
||||
transferOrgiz(pending.location, pending.collector, ownerReceipt, receipts, effects);
|
||||
for (const ownerReceipt of ownerReceipts)
|
||||
transferOrgiz(pending.location, pending.collector, ownerReceipt, receipts, effects);
|
||||
}
|
||||
|
||||
return { receipts, effects };
|
||||
}
|
||||
|
||||
const SHARED_COLLECTION_RELEASES: readonly AdvisorRelease[] = [];
|
||||
|
||||
/** Advanced's one-territory event waits for the original bilateral allocation.
|
||||
* Charge the first actual positive foreign recipient (Ecaz, then ally), never
|
||||
* a guessed share, the owner itself, a city award or unavailable spice. */
|
||||
export function quoteAdvancedSharedOrgiz(
|
||||
context: DiscoveryCollectionContext,
|
||||
allocation: SpiceAllocationReceipt,
|
||||
balances: Readonly<Record<string,number>>,
|
||||
): OrgizCollectionReceipt[] {
|
||||
requireCollection(context.advanced && allocation.ecaz !== allocation.ally &&
|
||||
context.order.includes(allocation.ecaz) && context.order.includes(allocation.ally) &&
|
||||
whole(allocation.ecazAmount) && whole(allocation.allyAmount) &&
|
||||
record(balances) && context.order.every(id => whole(balances[id])),
|
||||
'Shared Orgiz needs the actual Advanced territory allocation and available balances.');
|
||||
const owners = discoveryCollectionOccupants(context,ORGIZ_PROCESSING_STATION,SHARED_COLLECTION_RELEASES);
|
||||
const effects: OrgizCollectionReceipt[] = [];
|
||||
let ecazDebited=0, allyDebited=0;
|
||||
for (const owner of owners) {
|
||||
let payer: string;
|
||||
if (owner.id !== allocation.ecaz && allocation.ecazAmount > 0 &&
|
||||
balances[allocation.ecaz] > ecazDebited) {
|
||||
payer=allocation.ecaz; ecazDebited++;
|
||||
} else if (owner.id !== allocation.ally && allocation.allyAmount > 0 &&
|
||||
balances[allocation.ally] > allyDebited) {
|
||||
payer=allocation.ally; allyDebited++;
|
||||
} else continue;
|
||||
requireCollection(Number.isSafeInteger(balances[owner.id]+1),
|
||||
'Shared Orgiz income would overflow the spice balance.');
|
||||
effects.push({kind:'orgiz',player:owner.id,from:payer,location:allocation.territory,
|
||||
amount:1,source:'player'});
|
||||
}
|
||||
return effects;
|
||||
}
|
||||
@@ -305,6 +305,7 @@ import {
|
||||
} from './board-resolution-quote';
|
||||
import {
|
||||
quoteDiscoveryCollection,
|
||||
quoteAdvancedSharedOrgiz,
|
||||
DiscoveryCollectionError,
|
||||
type DiscoveryCollectionQuote,
|
||||
} from './discovery-collection';
|
||||
@@ -19689,6 +19690,16 @@ function decideSharedSpice(g: Game, id: string, action: Action) {
|
||||
`${territory(receipt.territory).name} shared collection: Ecaz received ${receipt.ecazAmount} spice and ${faction(getPlayer(g, receipt.ally).faction).name} received ${receipt.allyAmount}, ${receipt.method === 'agreement' ? 'by agreement' : 'using the equal split with any odd spice going to the ally'}.`,
|
||||
{ faction: 'ecaz', name: 'Shared collection' },
|
||||
);
|
||||
if (g.advanced) {
|
||||
const balances = Object.fromEntries(g.players.map(player => [player.id,player.spice]));
|
||||
const transfers = nexusRule(() => quoteAdvancedSharedOrgiz(g,receipt,balances));
|
||||
for (const transfer of transfers) {
|
||||
getPlayer(g,transfer.from).spice -= transfer.amount;
|
||||
getPlayer(g,transfer.player).spice += transfer.amount;
|
||||
log(g, `${faction(getPlayer(g,transfer.player).faction).name} took ${transfer.amount} spice from ${faction(getPlayer(g,transfer.from).faction).name} through Orgiz Processing Station after the original shared territory allocation.`,
|
||||
{faction:getPlayer(g,transfer.player).faction,name:'Orgiz Processing Station'});
|
||||
}
|
||||
}
|
||||
} else
|
||||
log(
|
||||
g,
|
||||
|
||||
+2
-2
@@ -4226,7 +4226,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'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.',
|
||||
'Native typed forces enter as their actual ordinary and elite groups, never as passengers from the mobile stronghold or the concealed value of a No-Field. Richese may ship or move a concealed marker into an actually revealed nested location at sector zero; it remains one effective presence until its original reveal materializes available reserves. CHOAM Card Stash uses its five-card limit, drawing first and choosing any held discard afterward.',
|
||||
'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 Cistern occupant receives two bank spice during Collection. In Advanced, a sole Orgiz occupant takes one collected spice per territory where an unambiguous rival collected board spice. Multiple collected deposits in that territory do not add transfers. Basic retains its provisional one-per-positive-deposit interpretation. Contested ownership, different or shared payers leave only that uncertain transfer unpaid; ordinary Collection continues.',
|
||||
'During Collection, after ordinary advisor releases, each fighter occupant of Cistern receives two bank spice, and each fighter occupant of Orgiz takes one spice from the unique rival collector of a qualifying deposit. Orgiz owners resolve in original storm order; an empty purse cannot pay and creates no debt. Advanced uses the supplied territory Collection event and Basic keeps the provisional one-per-positive-deposit mapping. When Ecaz and its ally share a collected territory, the real allocation settles first and the Orgiz owner then takes from the first actual positive foreign recipient. Contested ownership, different payers and unresolved Cistern ownership contests remain guarded; ordinary Collection continues.',
|
||||
'Jacurutu pays one bank spice for each opposing undialed physical force actually sent to the Tanks. Where an Advanced dial admits different physical counts, the defeated player chooses an exact ordinary/elite/paid reading before support, casualties or rewards settle; unique readings remain automatic. This choice timing is a visibly provisional implementation interpretation. Original Harass returns bind their physical dial and actual withdrawal/retreat counters do not earn spice.',
|
||||
'Each non-advisor Testing Station occupant may decrease ordinary storm movement by one, keep it or increase it by one. The provisional order is the original storm order, once per owner, with cumulative adjustments on current distance. Choose after the ordinary card window; traversal/protection starts only after all choices. Weather Control is unaffected. Original natural source/base and existing sole-frame history remain authoritative; legacy movement without a source is not invented.',
|
||||
'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.',
|
||||
@@ -4236,7 +4236,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
checklist: [
|
||||
{
|
||||
area: 'Implementation', status: 'Partial',
|
||||
detail: 'Original classic and supported native Skills/Nexus/Discovery entries retain their original selected modules, Maker votes/typed rides, source-local held benefits, training and owner-labelled coalition aftermath. Mixed Jacurutu now pauses for an exact defeated-player physical reading before settlement, excluding actual returns from Tanks income. Testing Station uses a signed cumulative storm-order owner queue before one traversal; original source and sole-frame history remain. Advanced Orgiz aggregates territory Collection; Basic stays provisional. Other pending sources and public starts remain guarded.',
|
||||
detail: 'Original classic and supported native Skills/Nexus/Discovery entries retain their original selected modules, Maker votes/typed rides, source-local held benefits, training and owner-labelled coalition aftermath. Mixed Jacurutu now pauses for an exact defeated-player physical reading before settlement, excluding actual returns from Tanks income. Testing Station uses a signed cumulative storm-order owner queue before one traversal. Cistern pays each fighter occupant, and Orgiz charges each occupant in storm order without debt; Advanced shared Ecaz lots settle their real allocation before the transfer. Advanced Orgiz aggregates territory Collection; Basic stays provisional. Other pending sources and public starts remain guarded.',
|
||||
evidence: ['game/discovery-module-profile.ts', 'game/richese-no-field.ts', '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'],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user