Use actual Collection receipts for co-present Orgiz payers

This commit is contained in:
Quintine committed 2026-10-14 19:44:39 +11:00
1 parent bc09ac3cfd
commit bcd1b1041a
13 files changed
+287 -172

No files matched your search

+1 -1
View File
@@ -96,7 +96,7 @@ export function DiscoveryPanel({ game, act, busy }: Props) {
</div>
<p className="fine">Inspection and reveal are optional. Next-turn free entry, Cistern income for every fighter occupant and later-turn Ornithopter movement are available in the development prototype. Jacurutu income, Testing Station storm choices and Shrine card conversion are prototyped. {game.advanced
? 'Each Orgiz fighter occupant takes one spice per qualifying territory from the first actual foreign collector in storm order. Shared Ecaz lots wait for the actual allocation: Ecaz first, then ally, excluding zero shares and the occupant itself.'
: 'Each Orgiz fighter occupant keeps the provisional one-spice transfer per positive board deposit collected by one rival.'} An empty purse never creates debt. Mixed physical Jacurutu dials use a defeated-player choice before settlement. Testing Station occupants each choose once in original storm order, cumulatively. These ordering/allocation interpretations are provisional. Ambiguous collected sectors still withhold their territory transfer without blocking ordinary Collection.</p>
: 'Each Orgiz fighter occupant keeps the provisional one-spice transfer per positive board deposit from the first actual foreign collector in storm order. Basic shared Ecaz allocation remains pending.'} Actual ordinary Collection receipts identify each payer; co-present forces or old wallets alone never qualify. An empty purse never creates debt. Mixed physical Jacurutu dials use a defeated-player choice before settlement. Testing Station occupants each choose once in original storm order, cumulatively. These ordering/allocation interpretations are provisional.</p>
</section>
);
}
+24 -3
View File
@@ -29,7 +29,27 @@ optional skill bands, combinations and public expansion starts remain open.
Rules implementation continues; comprehensive assurance and administration
remain deferred until all rules are implemented.
**Orgiz payer ordering and native Collection proof — 14 October.**
**Canonical Orgiz Collection sources — 14 October.**
The original ordinary quote now records positive player/location/amount deposit
receipts before Orgiz. Both modes use actual collectors, not co-present force
inference: Basic groups by positive deposit; Advanced groups by territory.
Original storm-order eligibility skips self, zero actual collection and empty
purses. City income and unresolved Ecaz escrow cannot identify an ordinary
payer. The former shared-sector withholding branch and `collectorAt` are gone;
phase controls and Advanced bilateral allocation remain original.
Genuine Basic/Advanced Sihaya Ridge paths preserve setup, cards, armies,
alliance, reveal/entry, departure and paid shipments. Exact source receipts
show both collectors taking2, or one consuming the original6-spice pile with
the other taking0. Face1 is selected at the actual turn-two forecast shuffle,
not assigned to a later source. Final six-file union72/72; types/lint and app
build pass. Actual390px Finish shipment & movement turns phase5→7,
Ecaz7/Guild10→Ecaz8/Guild14 with one Orgiz transfer and Ready restored.
No horizontal overflow or browser errors. Basic shared-allocation settlement,
complete modules and deployed acceptance remain open.
[Contract](DISCOVERY_PROTOTYPE.md#multi-occupant-and-shared-cisternorgiz-collection--14-october-2026).
**Earlier Orgiz payer ordering and native proof (`bc09ac3`) — 14 October.**
Every Cistern fighter occupant receives two bank spice. Orgiz occupants resolve
in original storm order without debt. Advanced charges once per territory,
selecting the first definite foreign collected-sector payer in storm order that
@@ -48,8 +68,9 @@ types/lint and app build pass. Actual390px table proposal4/0 leaves wallets7/11
unchanged until Guild accepts, then credits Ecaz10/Guild12 with one transfer,
no remaining decision and enabled Ready. No horizontal overflow; fresh settled
surface has no application errors. The earlier checkpoint's genuine-fixture
and app-build claims were unsupported and are corrected here. Ambiguous
collected-sector payers and complete/deployed acceptance remain open.
and app-build claims were unsupported and are corrected here. That checkpoint's
ordinary-source ambiguity gap is superseded by the canonical receipt pass
above; Basic shared allocation and complete/deployed acceptance remain open.
[Contract](DISCOVERY_PROTOTYPE.md#multi-occupant-and-shared-cisternorgiz-collection--14-october-2026).
+47 -41
View File
@@ -68,35 +68,41 @@ source/actor/transition/payment boundaries, not wording or fixture-copy echoes.
Cistern pays every fighter occupant two bank spice after ordinary advisor
releases; a lone advisor never qualifies. Orgiz resolves fighter occupants in
original storm order. Advanced charges one actual foreign collector per
territory, choosing among definite collected-sector payers in original storm
order and skipping the owner and empty purses. Basic retains the provisional
positive-deposit mapping. An ambiguous collected sector still withholds that
territory's transfer. No debt is created; ordinary Collection continues.
original storm order. The original `CollectionQuote.deposits` records each
actual positive ordinary map collection by player/location/amount, excluding
city income and unresolved shared escrow. Orgiz considers those actual foreign
recipients in storm order, skipping self and empty purses: once per territory
in Advanced, once per positive deposit under Basic's provisional mapping.
Co-present forces and old wallets alone do not establish collection.
Shared Ecaz desert lots wait for the actual bilateral allocation. Each occupant
considers positive foreign recipients Ecaz first, then ally, charging at most
one spice from an available current balance. A zero share or an old wallet
alone cannot establish eligibility; the owner never pays itself. These occupant
and payer orderings are recommended provisional interpretations, not new
publisher rulings or separately answered numerical user questions.
Advanced shared Ecaz desert lots wait for actual bilateral allocation. Each
occupant considers positive foreign recipients Ecaz first, then ally, charging
at most one spice from an available current balance. A zero share or an old
wallet alone cannot establish eligibility; the owner never pays itself. Basic
shared-allocation Orgiz settlement remains pending. Occupant/payer ordering
is a recommended provisional interpretation, not a new publisher ruling or
separately answered numerical user question.
Current native fixtures preserve original armies, wallets, hands, alliances
and phases. They use real setup, token placement, paid shipments, inspection,
reveal, free entry, a worm Nexus alliance and allocation. Only original
unplayed Spice Card order and the actual token lottery are staged. The shared
route uses two fresh desert blows with different sectors, real Ecaz departure
from Orgiz, and fresh paid Ecaz/Guild shipments. Rock blows cannot create an
Ecaz shared lot. Cases cover equal/full/zero Ecaz shares, owner exclusion,
acceptance timing, wrong-actor rejection and stale-allocation rejection.
reveal, free entry, a worm alliance and allocation. Staging is limited to
original unplayed Spice Card order, the actual token lottery and face1 at the
real turn-two forecast shuffle; the resulting turn-three source is original.
Shared desert lots and ordinary co-present rock collectors use real Ecaz
departure and fresh paid Ecaz/Guild shipments. Rock spice does not create a
shared Ecaz lot, but each positive ordinary recipient is now an eligible payer.
An exhausted pile leaves the later collector ineligible even with old spice.
`tests/fixture-discovery-occupied-collection.ts`,
`tests/discovery-occupied-collection-runtime.test.ts`,
`tests/fixture-discovery-shared-orgiz.ts` and
`tests/discovery-shared-orgiz-runtime.test.ts`. Focused helper regressions also
cover original storm-order payer selection, one available spice for two owners
and advisor exclusion. Ambiguous collected-sector sources and complete
combined-module acceptance remain open.
`tests/discovery-shared-orgiz-runtime.test.ts`. Focused helper regressions cover
original storm-order payer selection, depleted purses, advisor exclusion and
zero actual collection. The six-file related union72/72, types/lint and app
build pass. Actual390px Finish shipment & movement produces phase7,
Ecaz8/Guild14 from7/10, two actual2-spice source receipts, one Orgiz transfer,
no decision, enabled Ready, no overflow and no browser errors. Basic shared
allocation, broader combined modules and full acceptance remain open.
## Mixed E1/E2 and paired E3 Nexus — 7 October 2026
@@ -754,13 +760,13 @@ says Orgiz steals one collected spice **each time collection occurs in a
territory containing spice**, rather than the publisher's older per-blow text.
For Advanced, each fighter occupant receives at most one transfer per actual
positive territory Collection. Multiple collected source keys do not multiply
the transfer. Definite rival collectors are considered in original storm
order, skipping self and empty purses. A collected sector whose actual payer
is ambiguous still withholds that territory's transfer. Shared Ecaz desert
lots wait for the actual bilateral split, then consider positive foreign
recipients Ecaz first, ally second. Those payer/occupant orderings are explicitly
provisional implementations under the recommended-option priority; the
territory trigger is sourced, not the ordering. Ordinary Collection proceeds.
the transfer. Canonical positive ordinary deposit receipts identify actual
foreign payers in original storm order, including multiple collectors in one
sector; self and empty purses are skipped. No presence-based ambiguity gate
remains. Advanced shared Ecaz desert lots wait for actual bilateral allocation,
then consider positive foreign recipients Ecaz first, ally second. Basic
shared settlement remains pending. Ordering is provisional under the
recommended-option priority; the Advanced territory trigger itself is sourced.
Basic retains its recorded provisional one-transfer-per-positive-deposit
interpretation. The context requires its existing public `advanced` boolean:
@@ -868,15 +874,15 @@ conversion was fabricated.
occupant of the revealed location receives two bank spice. A lone advisor
never qualifies. Two fighters therefore both collect; the former withheld
two-fighter gap is closed.
- **Orgiz:** during Collection, each fighter occupant resolves in original storm
order after ordinary advisor releases. Advanced charges once per territory,
choosing the first definite foreign collector in original storm order that
can pay. Basic retains one transfer per positive deposit with a unique rival
collector. Empty purses never create debt. An ambiguous collected sector
withholds the territory effect without blocking ordinary Collection. Shared
Ecaz desert lots settle their real bilateral allocation first; positive
foreign recipients are considered Ecaz first, ally second. Zero shares and
the occupant itself do not qualify. Ordering remains provisional.
- **Orgiz:** fighter occupants resolve in original storm order after advisor
releases. Actual positive ordinary deposit receipts identify eligible foreign
payers, even when forces share a sector. Advanced charges once per territory;
Basic provisionally charges once per positive deposit. The first actual
foreign collector in storm order with available spice pays; mere presence,
self and zero actual collection do not qualify. Empty purses never create
debt. Advanced shared Ecaz lots wait for allocation, then consider positive
foreign recipients Ecaz first, ally second. Basic shared settlement remains
pending; occupant/payer ordering remains provisional.
- **Jacurutu:** one bank spice per opposing undialed physical counter actually
sent to the Tanks. Unique readings remain automatic. For different possible
@@ -1022,10 +1028,10 @@ to Spice Blow. Refresh restored the same station forces and private Snooper.
This is targeted control/recovery evidence, not an unmodified complete game.
The 187 opening saved rooms remain preserved; no server restart was needed.
1. Resolve ambiguous collected-sector Orgiz sources and refine combined
Discovery interactions. Multi-occupant Cistern/Orgiz, shared allocation,
Jacurutu mixed dials and Testing Station ordering now use the visible
recommended interpretations above; Basic Orgiz remains provisional.
1. Connect Basic shared-allocation Orgiz settlement and refine combined
Discovery interactions. Canonical ordinary receipts now close the guessed
shared-sector payer gap. Other recommended Discovery interpretations remain
visibly provisional; the Basic positive-deposit mapping is not certified.
2. Integrate combined modules and complete games, then refine interaction,
strategy and presentation coverage before opening normal start gates.
3. Use the existing prototype starter and saved-room verification for further
+26
View File
@@ -1,5 +1,31 @@
# Dune implementation status
## 14 October 2026 — Canonical positive Collection receipts for Orgiz
The original ordinary quote now emits `deposits` with actual positive
player/location/amount receipts. It excludes city awards and shared escrow;
zero-take forces are not collectors. Discovery consumes that original source
instead of inferring a unique collector from occupancy. `collectorAt` and the
whole-territory ambiguity guard were removed. Basic groups by positive deposit;
Advanced groups by territory, retaining source-key event order and original
storm-order owner/payer selection. Self and empty purses are skipped.
Native Basic/Advanced Ecaz/Guild rock scenarios use genuine setup, token
lottery/reveal/entry, alliance, departure and paid shipments. Both collect2
from original Sihaya Ridge spice, or the first actual collector consumes the
whole6-spice pile and the later co-present force takes0. The latter cannot pay
solely because it has an old wallet. Actual Storm face1 is selected at the
original forecast producer after turn-two Storm, not assigned to a later
forecast or source. Shared desert allocation keeps its original separate path.
Final six-file union72/72, types/lint and app build pass. Actual390px table:
Finish shipment & movement produces phase7 with two2-spice ordinary source
receipts, Ecaz8/Guild14 from7/10, exactly one Orgiz transfer, no decision and
enabled Ready. No horizontal overflow or browser errors. Local proof is
outside Git; owned hosts/tabs/scaffolding were removed. DEP0205, large-chunk
and route-classification warnings remain. Basic shared-allocation settlement,
full modules, assurance and deployed acceptance remain open.
## 14 October 2026 — Cistern/Orgiz multi-occupant and shared-lot Collection
Recommended-option priority: Advanced Orgiz now chooses one definite actual
+1 -1
View File
@@ -549,7 +549,7 @@ These are unresolved in the cited records. Do not infer an answer from elapsed t
| Nexus responses and composition | Betrayal private-response timing, partial revival/payment choices, Emperor fewer-than-five, fresh advisor/storm cases, Duke restoration and remaining faction-specific combinations. Tleilaxu Secret Ally force accounting, leader slot/eligibility and force-only use remain unresolved after requested publisher/designer source searches; do not infer answers from native Tleilaxu revival or Ghola. | [Common boundaries](NEXUS_CARD_RULES.md#remaining-source-questions-and-release-boundaries), [Tleilaxu source search](NEXUS_TLEILAXU_RULES.md#source-search-follow-up) and individual faction source/runtime documents |
| Moritani Atomics after alliance change | The publisher's persistent −1 hand-limit text names Moritani and “your ally,” but not whether the penalty follows a later ally, remains on the original ally or affects both. Requested online source searches found no clarification; the alliance-change case remains gated. | [Terror source boundary](MORITANI_TERROR_RULES.md#material-gaps-and-ordinary-compositions) |
| Moritani Sneak Attack entry classification | The printed verb is “Send”; no retrieved ruling classifies the off-planet reserve transfer as a shipment for other arrival powers. The engine now **explicitly adopts** the supported shipment composition: a positive Sneak Attack in Shipment and Movement accrues the Heighliners token once for its owner, then offers the Bene Gesserit Intrusion (Advanced, with fighters present) and the free spiritual-advisor shipment, with the committed Terror entry resolving first; a zero-force resolution accrues nothing and opens no reaction. This order is an implementation interpretation, not a publisher ruling. | [Source boundary and adopted classification](MORITANI_ASSASSINATION_SNEAK.md#adopted-shipment-classification-7-october-2026) |
| Orgiz collection | Basic retains the publisher-p13 per-blow ambiguity and provisional one-transfer-per-positive-deposit mapping. Authorized supplied Advanced rulebook physical24 says each time Collection occurs in a territory containing spice: one transfer per actual positive territory Collection per fighter occupant, never extra transfers for multiple source keys. Under the choose-recommended-option priority, distinct definite collectors are considered in original storm order, skipping the owner and empty purses; a sector whose actual collector is ambiguous still withholds its territory transfer. Shared Ecaz lots wait for actual bilateral allocation, then consider positive foreign recipients Ecaz first, ally second. Occupant and payer ordering are provisional implementation interpretations, not separate numerical user answers or publisher rulings. Ordinary Collection proceeds without debt. | [Advanced source cutover and Basic boundary](DISCOVERY_PROTOTYPE.md#authorized-advanced-collection-wording) |
| Orgiz collection | Basic retains the publisher-p13 per-blow ambiguity and provisional one-transfer-per-positive-deposit mapping. Authorized supplied Advanced rulebook physical24 says each time Collection occurs in a territory containing spice: one transfer per actual positive territory Collection per fighter occupant, never extra transfers for multiple source keys. Canonical positive ordinary deposit receipts now identify actual payers, including co-present collectors; force presence and old wallets alone do not qualify. Under choose-recommended-option priority, owners and eligible foreign ordinary payers resolve in original storm order, skipping self and empty purses. Advanced shared Ecaz lots wait for actual allocation and consider positive foreign recipients Ecaz first, ally second; Basic shared settlement remains pending. Ordering is provisional implementation interpretation, not a separate numerical user answer or publisher ruling. No debt is created. | [Advanced source cutover and Basic boundary](DISCOVERY_PROTOTYPE.md#authorized-advanced-collection-wording) |
| Jacurutu physical dial | Under the 14 October choose-recommendation priority, the prototype selects the defeated-player exact ordinary/elite/paid reading before settlement. Unique readings remain automatic; actual returned counters never earn income. This replaces the earlier unpaid gap without claiming a separate numerical user answer or publisher ruling. | [Recommended runtime and source](DISCOVERY_PROTOTYPE.md#recommended-physical-dial-and-cumulative-storm-choices--14-october-2026) |
| Testing Station contest/timing | The 14 October recommended prototype gives each non-advisor occupant one cumulative adjustment in original storm order, after the ordinary card window and before a single traversal. Original source/base and Weather immunity stay unchanged. Ordering/stacking is explicit provisional interpretation, not a newly found publisher answer. | [Recommended runtime and source](DISCOVERY_PROTOTYPE.md#recommended-physical-dial-and-cumulative-storm-choices--14-october-2026) |
| CHOAM Secret Ally battle timing | The printed after-victory inspection is optional and the Nexus identity stays secret. The connected prototype chooses a uniform use-or-continue window for every winner when Nexus is enabled and CHOAM absent; nonholders pass, while a holder may decide after the result. This privacy-preserving prompt is a product inference, not publisher text. A holder-only pause would reveal custody and a precommitment would remove the printed choice. Ordinary Karama cancellation remains unestablished by retrieved sources; no response is invented. | [After-victory inspection boundary](NEXUS_CHOAM_SECRET_ALLY.md#after-victory-inspection-audit) |
+6 -1
View File
@@ -169,6 +169,9 @@ export type CollectionQuote = {
desert: number;
balance: number;
}[];
/** Positive ordinary map collections in actual storm order, before Orgiz.
* Shared escrow has no recipient until its original allocation settles. */
deposits: { player: string; location: string; amount: number }[];
/** Removed from the map, but not credited until the allies resolve allocation. */
shared: SharedSpiceLot[];
collectionBonus: {
@@ -292,6 +295,7 @@ export function quoteSpiceCollection(
const shared: SharedSpiceLot[] = [];
const pooled = new Set<string>();
const receipts: CollectionQuote['receipts'] = [];
const deposits: CollectionQuote['deposits'] = [];
for (const id of g.order) {
const p = players.find((p) => p.id === id)!,
original = g.players.find((p) => p.id === id)!;
@@ -334,6 +338,7 @@ export function quoteSpiceCollection(
continue;
}
const amount = Math.min(spice[key] ?? 0, capacity);
if (amount > 0) deposits.push({ player: id, location: key, amount });
collected += amount;
if (sites.some((site) => site.id === t && site.type === 'sand'))
desert += amount;
@@ -346,7 +351,7 @@ export function quoteSpiceCollection(
);
receipts.push({ player: id, strongholds, collected, desert, balance });
}
return { released, spice, receipts, shared, collectionBonus };
return { released, spice, receipts, deposits, shared, collectionBonus };
}
export type BattlePhaseEndContext = CollectionContext & {
+36 -67
View File
@@ -1,6 +1,6 @@
import { isAdvisor } from './advisors';
import { gameTerritories, splitLocation, type MobileBoard } from './board';
import type { AdvisorRelease } from './board-resolution-quote';
import type { AdvisorRelease, CollectionQuote } from './board-resolution-quote';
import type { Player } from './engine';
import type { SpiceAllocationReceipt } from './ecaz-spice-allocation';
import { presenceAt, type ForcePresence } from './force-presence';
@@ -40,6 +40,8 @@ export type DiscoveryBaseCollectionQuote = {
/** Deposits remaining after ordinary and shared collection. */
spice: Readonly<Record<string, number>>;
receipts: readonly DiscoveryBaseCollectionReceipt[];
/** Actual positive ordinary source receipts, never inferred from occupancy. */
deposits: readonly CollectionQuote['deposits'][number][];
shared: readonly {
territory: string;
ecaz: string;
@@ -103,36 +105,15 @@ export function discoveryCollectionOccupants(
).sort((left,right) => context.order.indexOf(left.id)-context.order.indexOf(right.id));
}
/** Public effective presence only; an ambiguous source cannot identify a payer. */
function collectorAt(
context: DiscoveryCollectionContext,
key: string,
released: readonly AdvisorRelease[],
): DiscoveryCollectionSeat | undefined {
const { territory, sector } = splitLocation(key);
let collector: DiscoveryCollectionSeat | undefined;
for (const player of context.players) {
if (!isCollectingFighter(player, territory, released)) continue;
const marker = player.noField?.deployed?.location;
if (
(player.forces[key] ?? 0) <= 0 &&
!(marker?.territory === territory && marker.sector === sector)
) continue;
if (collector) return undefined;
collector = player;
}
return collector;
}
function transferOrgiz(
key: string,
collector: DiscoveryCollectionSeat | undefined,
collector: string,
ownerReceipt: DiscoveryBaseCollectionReceipt,
receipts: DiscoveryBaseCollectionReceipt[],
effects: DiscoveryCollectionQuote['effects'],
) {
if (!collector || collector.id === ownerReceipt.player) return false;
const collectorReceipt = receipts.find(entry => entry.player === collector.id)!;
if (collector === ownerReceipt.player) return false;
const collectorReceipt = receipts.find(entry => entry.player === collector)!;
if (collectorReceipt.balance === 0) return false;
requireCollection(
Number.isSafeInteger(ownerReceipt.balance + 1),
@@ -141,7 +122,7 @@ function transferOrgiz(
collectorReceipt.balance -= 1;
ownerReceipt.balance += 1;
effects.push({
kind: 'orgiz', player: ownerReceipt.player, from: collector.id,
kind: 'orgiz', player: ownerReceipt.player, from: collector,
location: key, amount: 1, source: 'player',
});
return true;
@@ -152,10 +133,9 @@ function transferOrgiz(
* over the canonical ordinary quote. Ordinary `collected` and `desert` remain
* factual: Cistern is bank income; Orgiz transfers available player spice.
*
* Owners resolve in original storm order. Advanced chooses one actual foreign
* collector per territory in storm order; Basic retains the provisional
* positive-deposit trigger. Ambiguous collected sectors withhold the territory
* effect. Shared lots settle after bilateral allocation, not in this quote.
* Owners and actual foreign payers resolve in original storm order. Advanced
* groups canonical deposits by territory; Basic groups by positive deposit.
* Shared escrow settles after bilateral allocation, not in this quote.
*/
export function quoteDiscoveryCollection(
context: DiscoveryCollectionContext,
@@ -184,6 +164,7 @@ export function quoteDiscoveryCollection(
record(collection.spice) &&
Array.isArray(collection.receipts) &&
Array.isArray(collection.shared) &&
Array.isArray(collection.deposits) &&
Array.isArray(collection.released) &&
collection.released.every(
(entry) => record(entry) &&
@@ -250,6 +231,13 @@ export function quoteDiscoveryCollection(
),
'Discovery collection needs valid retained shared-spice lots.',
);
requireCollection(
collection.deposits.every(entry => record(entry) &&
typeof entry.player === 'string' && context.order.includes(entry.player) &&
typeof entry.location === 'string' && Object.hasOwn(context.spice, entry.location) &&
whole(entry.amount) && entry.amount > 0 && entry.amount <= context.spice[entry.location]),
'Discovery collection needs actual positive ordinary deposit receipts.',
);
const effects: DiscoveryCollectionQuote['effects'] = [];
const cistern = discoveryCollectionOccupants(context, CISTERN, collection.released);
@@ -271,45 +259,26 @@ export function quoteDiscoveryCollection(
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, {
collectors: { location: string; collector: DiscoveryCollectionSeat }[];
ambiguous: boolean;
}>()
: undefined;
const transfers = new Map<string, CollectionQuote['deposits']>();
for (const deposit of collection.deposits) {
const event = context.advanced ? splitLocation(deposit.location).territory : deposit.location;
const candidates = transfers.get(event);
if (candidates) candidates.push(deposit);
else transfers.set(event, [deposit]);
}
// Preserve original source-key event order, independent of force-map order.
for (const key of sourceKeys) {
if (context.spice[key] <= collection.spice[key]) continue;
const { territory } = splitLocation(key);
if (
collection.shared.some(
(lot) => lot.amount > 0 && lot.territory === territory,
)
) continue;
const collector = collectorAt(context, key, collection.released);
if (!transfers) {
for (const ownerReceipt of ownerReceipts)
transferOrgiz(key, collector, ownerReceipt, receipts, effects);
continue;
}
let pending = transfers.get(territory);
if (!pending) {
pending = { collectors: [], ambiguous: false };
transfers.set(territory, pending);
}
if (!collector) pending.ambiguous = true;
else if (!pending.collectors.some(entry => entry.collector.id === collector.id))
pending.collectors.push({ location: key, collector });
const event = context.advanced ? splitLocation(key).territory : key;
const candidates = transfers.get(event);
if (!candidates) continue;
transfers.delete(event);
candidates.sort((left, right) =>
context.order.indexOf(left.player) - context.order.indexOf(right.player));
for (const ownerReceipt of ownerReceipts)
for (const candidate of candidates)
if (transferOrgiz(candidate.location, candidate.player, ownerReceipt, receipts, effects))
break;
}
if (transfers)
for (const pending of transfers.values()) {
if (pending.ambiguous) continue;
pending.collectors.sort((left, right) =>
context.order.indexOf(left.collector.id) - context.order.indexOf(right.collector.id));
for (const ownerReceipt of ownerReceipts)
for (const candidate of pending.collectors)
if (transferOrgiz(candidate.location, candidate.collector, ownerReceipt, receipts, effects))
break;
}
}
return { receipts, effects };
+6 -5
View File
@@ -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.',
'During Collection, after ordinary advisor releases, every Cistern fighter occupant receives two bank spice. Orgiz fighter occupants resolve in original storm order. Advanced charges once per territory from the first definite actual foreign collector in storm order that can pay; Basic keeps one transfer per positive deposit with a unique rival collector. Empty purses never create debt. Shared Ecaz desert lots wait for their actual bilateral split, then consider positive foreign recipients Ecaz first, ally second, excluding self and zero shares. Payer and occupant ordering remain provisional. Ambiguous collected-sector payers still withhold their territory effect without blocking ordinary Collection.',
'During Collection, after advisor releases, every Cistern fighter occupant receives two bank spice. Orgiz occupants resolve in original storm order using canonical positive ordinary deposit receipts, not force presence or old wallets. Advanced charges once per territory; Basic provisionally charges once per positive deposit. The first actual foreign collector in storm order with available spice pays; self and zero actual collection do not qualify. Advanced shared Ecaz desert lots wait for their real split, then consider positive foreign recipients Ecaz first, ally second. Basic shared settlement remains pending. Payer/occupant ordering is provisional; empty purses never create debt.',
'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,8 +4236,8 @@ 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. 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'],
detail: 'Original classic and supported native Skills/Nexus/Discovery entries retain their selected modules and rule controls. Mixed Jacurutu uses an exact defeated-player physical reading before native settlement. Testing Station queues signed cumulative storm-order choices before one traversal. Cistern pays each fighter occupant; Orgiz consumes actual positive ordinary deposit receipts in original storm order, grouping by territory in Advanced and deposit in Basic without guessing co-present payers or creating debt. Advanced shared lots wait for bilateral allocation; Basic shared settlement and broader mode acceptance remain open.',
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/board-resolution-quote.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',
@@ -4251,15 +4251,16 @@ export const RULE_TOPICS: RuleTopic[] = [
},
{
area: 'Documentation', status: 'Partial',
detail: 'Sourced identities and native/classic-skill boundaries are recorded. Advanced Orgiz follows territory Collection; Basic remains provisional per deposit. Recommended Discovery occupant/payer ordering, defeated-player Jacurutu allocation and cumulative Testing Station choices are explicit visible interpretations, not publisher rulings. Great Maker, entry, source-storm inferences and ambiguous collected-sector Orgiz sources remain open.',
detail: 'Sourced identities and native/classic-skill boundaries are recorded. Advanced Orgiz follows territory Collection and actual ordinary receipts; Basic retains its provisional positive-deposit mapping. Recommended Discovery occupant/payer ordering, Jacurutu allocation and cumulative Testing Station choices are visible interpretations, not publisher rulings. Great Maker, entry/source-storm inferences and Basic shared Orgiz settlement remain open.',
evidence: ['docs/DISCOVERY_COMPONENTS.md', 'docs/DISCOVERY_PROTOTYPE.md'],
},
{
area: 'Verification', status: 'Partial',
detail: 'Current Collection batch passes27/27 in3 related files, types/lint and build. Genuine setup, token reveal/entry, alliance, departure and paid shipments produce actual shared desert allocation; equal/full/zero shares, acceptance timing, wrong actor, stale allocation, payer order and depletion are exercised. Actual390px proposal4/0 preserves wallets until acceptance, then produces Ecaz10/Guild12 and one Orgiz transfer with Ready restored. The earlier Jacurutu/Testing Station batch passes104/104 in12 files with native dial/sole-source/two-owner traversal evidence. Broader combinations, assurance, strategy and deployed acceptance remain open.',
detail: 'Current canonical Collection batch passes72/72 in6 related files, types/lint and build. Genuine Basic/Advanced co-present Sihaya Ridge collectors take2 each or exhaust the6-spice pile in actual storm order; zero actual collection cannot qualify through old wallets. Actual390px Finish shipment & movement produces phase7, Ecaz8/Guild14 from7/10 and one Orgiz transfer with Ready restored, no overflow or errors. Earlier shared-allocation proof passes27/27 and Jacurutu/Testing Station proof104/104, each source-bound. Basic shared settlement, combinations, assurance, strategy and deployed acceptance remain open.',
evidence: ['tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/homeworld-paired-nexus-runtime.test.ts', 'tests/homeworld-standalone-nexus-runtime.test.ts',
'tests/fixture-jacurutu-mixed-dial.ts', 'tests/jacurutu-mixed-dial-runtime.test.ts', 'tests/fixture-testing-station-contest.ts', 'tests/testing-station-contest-runtime.test.ts',
'tests/fixture-discovery-occupied-collection.ts', 'tests/discovery-occupied-collection-runtime.test.ts', 'tests/fixture-discovery-shared-orgiz.ts', 'tests/discovery-shared-orgiz-runtime.test.ts',
'tests/board-resolution-quote.test.ts', 'tests/ecaz-collection-quote.test.ts',
'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-discovery-runtime.test.ts', 'tests/homeworld-skills-discovery-runtime.test.ts', 'tests/homeworld-optional-modules-runtime.test.ts', 'tests/discovery-native-e1-skills-runtime.test.ts', 'tests/discovery-native-e2-skills-runtime.test.ts', 'tests/discovery-native-e3-skills-runtime.test.ts', 'tests/discovery-skills-strongholds-runtime.test.ts', 'tests/discovery-native-strongholds-runtime.test.ts', 'tests/ecaz-stronghold-runtime.test.ts', 'tests/e3-native-tech-runtime.test.ts', 'tests/discovery-classic-nexus-lifecycle.test.ts', 'tests/discovery-classic-nexus-effects.test.ts', 'tests/discovery-orgiz-advanced-runtime.test.ts', 'tests/discovery-classic-skills-runtime.test.ts', 'tests/discovery-module-profile.test.ts', 'tests/discovery-native-typed-runtime.test.ts', 'tests/discovery-native-marker-runtime.test.ts', 'tests/discovery-native-e3-runtime.test.ts', 'tests/discoveries.test.ts', 'tests/discovery-runtime.test.ts', 'tests/great-maker.test.ts', 'tests/discovery-board.test.ts', 'tests/discovery-admission.test.ts', 'tests/discovery-controls.test.ts', 'tests/discovery-entry.test.ts', 'tests/discovery-entry-runtime.test.ts', 'tests/discovery-entry-controls.test.ts', 'tests/discovery-entry-review.test.ts', 'tests/discovery-collection.test.ts', 'tests/discovery-flight.test.ts', 'tests/discovery-flight-engine.test.ts', 'tests/discovery-flight-recovery.test.ts', 'tests/discovery-battle.test.ts', 'tests/discovery-battle-recovery.test.ts', 'tests/discovery-storm.test.ts', 'tests/discovery-storm-recovery.test.ts', 'tests/discovery-shrine-engine.test.ts', 'tests/discovery-shrine-recovery.test.ts', 'docs/HOMEWORLD_RUNTIME_IMPLEMENTATION.md'],
},
],
+8
View File
@@ -146,6 +146,10 @@ void test('collection uses storm order and shared remaining spice, with city rat
{ player: 'e', strongholds: 0, collected: 3, desert: 3, balance: 13 },
{ player: 'b', strongholds: 0, collected: 0, desert: 0, balance: 10 },
]);
assert.deepEqual(q.deposits, [
{ player: 'a', location: 'red_chasm:7', amount: 6 },
{ player: 'e', location: 'red_chasm:7', amount: 3 },
]);
assert.equal(q.spice['red_chasm:7'], 0);
assert.deepEqual(g, before);
g.order = ['e', 'a', 'b'];
@@ -153,6 +157,10 @@ void test('collection uses storm order and shared remaining spice, with city rat
quoteSpiceCollection(g).receipts.map((r) => r.collected),
[4, 5, 0],
);
assert.deepEqual(quoteSpiceCollection(g).deposits, [
{ player: 'e', location: 'red_chasm:7', amount: 4 },
{ player: 'a', location: 'red_chasm:7', amount: 5 },
]);
});
void test('collection counts a concealed marker as one without inspecting its secret token or reserve value', () => {
const g = fixture();
+20 -1
View File
@@ -557,7 +557,7 @@ void test('Advanced falls through to the next actual collector when an earlier O
assert.equal(balance(discovery, 'e'), 1);
});
void test('Advanced does not select a payer from an uncontested sector when another collected sector is contested', () => {
void test('Advanced uses actual shared-sector receipts rather than withholding an entire territory for co-present forces', () => {
const game = fixture(true);
reveal(game, ORGIZ_PROCESSING_STATION);
game.players[0].forces = { [`${ORGIZ_PROCESSING_STATION}:0`]: 1 };
@@ -568,12 +568,31 @@ void test('Advanced does not select a payer from an uncontested sector when anot
assert.equal(ordinary.spice['hagga_basin:12'], 2);
assert.equal(ordinary.spice['hagga_basin:13'], 0);
assert.deepEqual(discovery.effects, [{
kind: 'orgiz', player: 'a', from: 'h', location: 'hagga_basin:12',
amount: 1, source: 'player',
}, {
kind: 'orgiz', player: 'a', from: 'h', location: 'red_chasm:7',
amount: 1, source: 'player',
}]);
assert.equal(balance(discovery, 'g'), ordinary.receipts[2].balance);
});
void test('Orgiz never charges a co-present faction that collected zero, even with an old wallet', () => {
for (const advanced of [false, true])
for (const pile of [2, 4]) {
const game = fixture(advanced);
reveal(game, ORGIZ_PROCESSING_STATION);
game.players[1].forces = { [`${ORGIZ_PROCESSING_STATION}:0`]: 1, 'hagga_basin:12': 1 };
game.players[2].forces = { 'hagga_basin:12': 1 };
game.players[2].spice = 20;
const { ordinary, discovery } = composed({ ...game, spice: { 'hagga_basin:12': pile } });
assert.equal(ordinary.deposits.some(entry => entry.player === 'g'), pile === 4);
assert.equal(balance(discovery, 'g'), 20 + (pile === 4 ? 1 : 0));
assert.equal(balance(discovery, 'h'), 2 + (pile === 4 ? 1 : 0));
assert.equal(discovery.effects.length, pile === 4 ? 1 : 0);
}
});
void test('Advanced shared territories remain unpaid across all their source keys while independent Collection proceeds', () => {
const game = fixture(true, [['e', 'ecaz'], ['a', 'atreides'], ['g', 'guild']]);
reveal(game, ORGIZ_PROCESSING_STATION);
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
import {applyAction,normalizeAutomaticGame,type Game} from '../game/engine';
import {CISTERN,ORGIZ_PROCESSING_STATION} from '../game/discovery-collection';
import {createOccupiedDiscoveryCollectionFixture} from './fixture-discovery-occupied-collection';
import {prepareOrdinaryPairedOrgizCollection} from './fixture-discovery-shared-orgiz';
const wallets=(game:Game)=>Object.fromEntries(game.players.map(p=>[p.id,p.spice]));
const stock=(game:Game)=>game.players.map(p=>({id:p.id,
@@ -47,3 +48,25 @@ for(const advanced of [false,true]) {
assert.deepEqual(wallets(done),expected);
});
}
for(const advanced of [false,true])
for(const exhaustFirst of [false,true]) {
void test(`${advanced?'Advanced':'Basic'} original co-present rock collectors ${exhaustFirst?'exhaust the pile in storm order':'both collect'} without guessing an Orgiz payer`,()=>{
const f=prepareOrdinaryPairedOrgizCollection(advanced,exhaustFirst);
const before=wallets(f.game);
const done=applyAction(f.game,f.finalAction.actor,f.finalAction.action);
const expected={...before};
if(exhaustFirst)expected[f.first]+=f.amount;
else {expected[f.ecaz]+=2;expected[f.guild]+=2}
if(advanced)expected[f.guild]+=1; // Original Tuek's Sietch Collection.
const ecazCollected=!exhaustFirst||f.first===f.ecaz;
if(ecazCollected) {expected[f.ecaz]-=1;expected[f.guild]+=1}
assert.deepEqual(wallets(done),expected);
assert.equal(done.spice[f.key],exhaustFirst?0:f.amount-4);
assert.equal(done.log.filter(entry=>entry.automatic?.name==='Orgiz Processing Station').length,
ecazCollected?1:0);
assert.equal(done.phase,7);
assert.equal(done.decision,null);
assert.equal(done.players.find(p=>p.id===f.guild)!.forces[`${ORGIZ_PROCESSING_STATION}:0`],2);
});
}
+50 -37
View File
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
import {applyAction,type Action,type Game} from '../game/engine';
import {splitLocation,territory} from '../game/board';
import {ORGIZ_PROCESSING_STATION} from '../game/discovery-collection';
import {quoteSpiceCollection} from '../game/board-resolution-quote';
import {
createDiscoveryOccupantsFixture,advanceTestingStationContest,nextTestingStationContestStep,
type TestingStationContestStep,
@@ -9,70 +10,82 @@ import {
const clean=(game:Game)=>!game.decision&&!game.response&&!game.phaseOpening;
/** Preserve original armies/cards/alliances. Only original unplayed Spice Card
* order and the actual token lottery are staged by the shared native prefix. */
export function prepareSharedOrgizCollection() {
const prefix=createDiscoveryOccupantsFixture({location:ORGIZ_PROCESSING_STATION,advanced:true,secondAmount:2});
/** Original setup/forces/cards/alliances. Only unplayed card order, the actual
* token lottery and face1 at its real turn-two forecast shuffle are selected. */
function prepareOrgizCollectors(advanced:boolean,terrainType:'sand'|'rock',exhaustFirst=false) {
const prefix=createDiscoveryOccupantsFixture({location:ORGIZ_PROCESSING_STATION,
advanced,secondAmount:2,ordinaryBlowTerrain:'sand',forecastOne:true});
const actions:TestingStationContestStep[]=[...prefix.actions];
let game=advanceTestingStationContest(prefix.game,g=>g.turn===2&&g.phase===5&&g.active==='ecaz'&&clean(g),actions);
const leave:Action={type:'move',from:`${ORGIZ_PROCESSING_STATION}:0`,
territory:prefix.parent,sector:prefix.parentSector,amount:2,elite:0};
actions.push({actor:'ecaz',action:leave});
game=applyAction(game,'ecaz',leave);
game=advanceTestingStationContest(game,g=>g.turn===2&&g.phase===8&&clean(g),actions);
// Two unused printed blows with different sectors guarantee that at least
// one new deposit is outside the next real Storm. Never edit a storm source.
let firstSector:number|null=null;
game=advanceTestingStationContest(game,g=>g.turn===3&&g.phase===0&&
g.stormPending!==null&&clean(g),actions);
const incomingStorm=(game.storm+game.stormPending!)%18;
const blowKeys:string[]=[];
for(let position=0;position<2;position++) {
const wanted=position===0?terrainType:'sand';
const index=game.spiceDeck.findIndex((card,i)=>i>=position&&'territory' in card&&
!card.discovery&&territory(card.territory).type==='sand'&&card.sector!==firstSector);
assert.ok(index>=position,'Original unused ordinary blows must supply the native Collection route.');
!card.discovery&&territory(card.territory).type===wanted&&
card.territory!==prefix.parent&&card.sector!==incomingStorm);
assert.ok(index>=position,'Original unused blows must supply the native Collection route.');
const card=game.spiceDeck.splice(index,1)[0];
game.spiceDeck.splice(position,0,card);
if('territory' in card) {
firstSector=card.sector;
blowKeys.push(`${card.territory}:${card.sector}`);
}
if('territory' in card)blowKeys.push(`${card.territory}:${card.sector}`);
}
game=advanceTestingStationContest(game,g=>g.turn===3&&g.phase===5&&clean(g),actions);
const target=Object.entries(game.spice).find(([key,amount])=>blowKeys.includes(key)&&amount>=2&&
splitLocation(key).sector!==game.storm&&game.players.every(p=>(p.forces[key]??0)===0));
assert.ok(target,'An actual original blow must supply an empty non-storm shared collection deposit.');
const destination=splitLocation(target[0]);
assert.equal(game.storm,incomingStorm);
const key=blowKeys[0];
const amount=game.spice[key];
assert.ok(amount>=4&&game.players.every(p=>(p.forces[key]??0)===0));
const destination=splitLocation(key);
const first=game.order.find(id=>id==='ecaz'||id==='guild')!;
const shipped=new Set<string>();
let beforeCollection:Game|null=null;
for(let step=0;step<500&&game.turn===3&&!(game.phase===7&&game.decision?.kind==='ecazSpice');step++) {
if(game.phase===5&&clean(game)&&game.movementRemaining?.length===1)
beforeCollection=game;
for(let step=0;step<500&&game.turn===3;step++) {
if(game.phase===5&&clean(game)&&game.movementRemaining?.length===1&&shipped.size===2)break;
let next:TestingStationContestStep|null;
if(game.turn===3&&game.phase===5&&clean(game)&&game.active&&
if(game.phase===5&&clean(game)&&game.active&&
['ecaz','guild'].includes(game.active)&&!shipped.has(game.active)) {
const actor=game.active;
next={actor,action:{type:'ship',territory:destination.territory,sector:destination.sector,
amount:1,elite:0,allyPayment:0}};
amount:exhaustFirst&&actor===first?Math.ceil(amount/2):1,elite:0,allyPayment:0}};
shipped.add(actor);
} else next=nextTestingStationContestStep(game);
assert.ok(next,`Original shared Collection has no continuation: ${JSON.stringify({
status:game.status,turn:game.turn,phase:game.phase,decision:game.decision,
active:game.active,shipped:[...shipped],target:target[0],
})}`);
assert.ok(next,'Original collector route must retain a legal continuation.');
actions.push(next);
game=applyAction(game,next.actor,next.action);
}
assert.equal(game.decision?.kind,'ecazSpice',JSON.stringify({
turn:game.turn,phase:game.phase,shipped:[...shipped],target:target[0],
spice:beforeCollection?.spice[target[0]],storm:beforeCollection?.storm,
seats:beforeCollection?.players.map(p=>({id:p.id,ally:p.ally,forces:p.forces[target[0]]})),
last:actions.slice(-12),
}));
assert.equal(game.phase,5);
assert.equal(game.movementRemaining?.length,1);
assert.equal(shipped.size,2);
assert.equal(game.players.find(p=>p.id==='ecaz')!.forces[`${ORGIZ_PROCESSING_STATION}:0`],undefined);
assert.equal(game.players.find(p=>p.id==='guild')!.forces[`${ORGIZ_PROCESSING_STATION}:0`],2);
const ordinary=quoteSpiceCollection(game);
const finalAction:TestingStationContestStep={actor:game.active!,action:{type:'endMovement'}};
return {...prefix,game,actions,ordinary,finalAction,key,amount,first,
ecaz:'ecaz',guild:'guild',owner:'guild'};
}
/** Desert collectors enter the original shared allocation before any credit. */
export function prepareSharedOrgizCollection() {
const position=prepareOrgizCollectors(true,'sand');
const game=applyAction(position.game,position.finalAction.actor,position.finalAction.action);
assert.equal(game.decision?.kind,'ecazSpice');
const pending=game.ecazCollection!.allocation!;
const lot=pending.lots[pending.index];
assert.equal(lot.territory,destination.territory);
assert.ok(lot.amount>=2);
return {...prefix,game,actions,lot,key:target[0],ecaz:'ecaz',guild:'guild',owner:'guild'};
assert.equal(lot.territory,splitLocation(position.key).territory);
assert.equal(lot.amount,4);
return {...position,game,lot,actions:[...position.actions,position.finalAction]};
}
/** Rock retains ordinary Collection: two allies may be present, but only
* actual positive source receipts can fund an Orgiz transfer. */
export function prepareOrdinaryPairedOrgizCollection(advanced=false,exhaustFirst=false) {
const position=prepareOrgizCollectors(advanced,'rock',exhaustFirst);
assert.deepEqual(position.ordinary.shared,[]);
return position;
}
export function settleSharedOrgiz(
+39 -15
View File
@@ -10,7 +10,7 @@ import {
type Game,
} from '../game/engine';
import { fighterCount } from '../game/advisors';
import { gameDistance } from '../game/board';
import { gameDistance, territory } from '../game/board';
import { DISCOVERY_CARD_PLACEMENTS, DISCOVERY_SPICE_CARDS, DISCOVERY_TOKEN_BY_ID, type DiscoveryTokenFace } from '../game/discoveries';
import { discoveryStormActions } from '../game/discovery-storm-options';
import { PLAYER_CIRCLE_SECTORS } from '../game/player-positions';
@@ -186,6 +186,25 @@ function selectedTokenLottery<T>(
}
}
/** Select printed face1 at its actual forecast shuffle after turn-two Storm,
* not by assigning a later forecast or movement-source field. */
function selectedForecastOne<T>(enabled:boolean,operation:()=>T):T {
if(!enabled)return operation();
const descriptor=Object.getOwnPropertyDescriptor(crypto,'getRandomValues');
const native=crypto.getRandomValues.bind(crypto);
crypto.getRandomValues=<V extends ArrayBufferView|null>(array:V):V=>{
if(array instanceof Uint32Array&&array.length===1) {
array[0]=0xffffffff;
return array;
}
return Reflect.apply(native,crypto,[array]) as V;
};
try{return operation()}finally{
if(descriptor)Object.defineProperty(crypto,'getRandomValues',descriptor);
else Reflect.deleteProperty(crypto,'getRandomValues');
}
}
/** Held-card staging only: a real unplayed Weather Control card moves to a hand. */
export function holdTestingStationStormCard(
game: Game,
@@ -206,7 +225,7 @@ export function holdTestingStationStormCard(
/** Original setup, printed Discovery supply and real reciprocal alliance create
* two occupants without fabricated counters, wallets, phases or token faces. */
export function createDiscoveryOccupantsFixture(
options: { location: OccupiedDiscoveryLocation; advanced?: boolean; secondAmount?: number; adjacentBlow?: boolean },
options: { location: OccupiedDiscoveryLocation; advanced?: boolean; secondAmount?: number; adjacentBlow?: boolean; ordinaryBlowTerrain?: 'sand' | 'rock'; forecastOne?: boolean },
): DiscoveryOccupantsFixture {
const advanced = options.advanced ?? false, location = options.location;
const secondAmount = options.secondAmount ?? 1;
@@ -240,10 +259,14 @@ export function createDiscoveryOccupantsFixture(
const wormPosition = advanced ? 2 : 1;
frontCard(game, 0, (candidate) => 'territory' in candidate && candidate.discovery === card.discovery);
if (advanced)
frontCard(game, 1, (candidate) => 'territory' in candidate && !candidate.discovery && candidate.territory !== placement.territory);
frontCard(game, 1, (candidate) => 'territory' in candidate && !candidate.discovery &&
candidate.territory !== placement.territory &&
(!options.ordinaryBlowTerrain || territory(candidate.territory).type === options.ordinaryBlowTerrain));
frontCard(game, wormPosition, (card) => 'worm' in card && !card.greatMaker);
for (let position = wormPosition + 1; position < wormPosition + 3; position++)
frontCard(game, position, (candidate) => 'territory' in candidate && !candidate.discovery && candidate.territory !== placement.territory);
frontCard(game, position, (candidate) => 'territory' in candidate && !candidate.discovery &&
candidate.territory !== placement.territory &&
(!options.ordinaryBlowTerrain || territory(candidate.territory).type === options.ordinaryBlowTerrain));
if (options.adjacentBlow)
frontCard(game,wormPosition+1,candidate => 'territory' in candidate && !candidate.discovery &&
gameDistance(game,`${placement.territory}:${placement.sector}`,`${candidate.territory}:${candidate.sector}`) === 1);
@@ -340,17 +363,18 @@ export function createDiscoveryOccupantsFixture(
'The first occupant entered through real next-turn free entry; no force was placed there directly.',
);
game = advanceTestingStationContest(
game,
(state) =>
state.turn === 2 &&
state.phase === 1 &&
!!state.nexus &&
!state.spiceWindow &&
!state.spiceResolution &&
clean(state),
actions,
);
game = selectedForecastOne(advanced && !!options.forecastOne, () =>
advanceTestingStationContest(
game,
(state) =>
state.turn === 2 &&
state.phase === 1 &&
!!state.nexus &&
!state.spiceWindow &&
!state.spiceResolution &&
clean(state),
actions,
));
const offer: TestingStationContestStep = { actor: ecaz, action: { type: 'alliance', target: guild } };
const accept: TestingStationContestStep = { actor: guild, action: { type: 'alliance', target: ecaz } };
for (const next of [offer, accept]) {