Resolve definite Orgiz payers and prove native shared Collection

This commit is contained in:
Quintine committed 2026-10-14 19:13:41 +11:00
1 parent 388c5fa8d5
commit bc09ac3cfd
11 files changed
+310 -292

No files matched your search

+3 -3
View File
@@ -94,9 +94,9 @@ export function DiscoveryPanel({ game, act, busy }: Props) {
);
})}
</div>
<p className="fine">Inspection and reveal are optional. Next-turn free entry, sole-occupant Cistern income 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
? 'A sole Orgiz occupant takes one spice for each territory where one rival collected board spice.'
: 'Basic keeps the provisional one-spice transfer per positive board deposit collected by one rival.'} Mixed physical Jacurutu dials now use a defeated-player choice before settlement. Testing Station occupants each choose once in original storm order, cumulatively. These ordering/allocation interpretations are provisional. Contested Cistern/Orgiz and different or shared collectors remain pending; uncertain transfers do not block ordinary Collection.</p>
<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>
</section>
);
}
+25 -19
View File
@@ -29,24 +29,28 @@ optional skill bands, combinations and public expansion starts remain open.
Rules implementation continues; comprehensive assurance and administration
remain deferred until all rules are implemented.
**Cistern and Orgiz multi-occupant Collection — 14 October.**
Following the same choose-recommended-option priority, the prototype closes two
withheld benefits: each Cistern fighter occupant now collects its two bank spice
after ordinary advisor releases, and each Orgiz fighter occupant takes one spice
from the unique rival collector of a qualifying deposit. Orgiz owners resolve in
original storm order; an empty purse pays nothing and creates no debt, so
ordinary Collection continues. Advanced keeps the supplied territory-Collection
trigger; Basic keeps the provisional positive-deposit mapping. Advanced
shared-Ecaz lots are no longer withheld: after the real bilateral allocation,
each occupant takes one spice from the first actual positive foreign recipient
(Ecaz, then ally), never from a zero share, an old wallet or itself.
Genuine fixtures reach these windows through real stage/phase controls, paid
shipments, Collection inspection/reveal, free entry, alliance and the engine's
own allocation protocol; equal/propose/zero splits, first-eligible recipient,
one available spice for two owners, lone-advisor exclusion and deterministic
re-settlement are exercised. Related17-file union127/127, five repeat runs
8/8, types/lint and app build pass. Contested Cistern/Orgiz ownership and full
acceptance remain open. [Contract](DISCOVERY_PROTOTYPE.md#multi-occupant-and-shared-cisternorgiz-collection--14-october-2026).
**Orgiz payer ordering and native Collection proof — 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
can pay; self and empty purses are skipped. Basic retains the provisional
positive-deposit mapping. Shared Ecaz desert lots wait for actual bilateral
allocation, then consider positive foreign recipients Ecaz first, ally second.
These occupant/payer orderings are provisional recommended interpretations.
The shared fixture now uses original setup, inspection/reveal, free entry,
worm alliance, real departure and fresh paid shipments. It no longer clears
armies/cards or fabricates alliances/phases/spice positions. Fresh original
desert blows avoid the former rock-terrain precondition failure. Native cases
cover equal/full/zero Ecaz shares, self exclusion, acceptance timing,
wrong-actor rejection and stale allocation. Final three-file union27/27;
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.
[Contract](DISCOVERY_PROTOTYPE.md#multi-occupant-and-shared-cisternorgiz-collection--14-october-2026).
**Recommended Jacurutu and Testing Station choices — 14 October.**
@@ -65,7 +69,9 @@ native city income remains separate. Guild+1 changes pending2→3 without moving
storm3; Ecaz−1 then moves it once to5 and opens Spice Blow, with both garrisons
intact. Browser errors empty and no root overflow. Related12-file union104/104,
types/lint and app build pass; [precise source and proof](DISCOVERY_PROTOTYPE.md#recommended-physical-dial-and-cumulative-storm-choices--14-october-2026).
Cistern/Orgiz/shared-collector effects and full acceptance remain open.
That checkpoint's Cistern/Orgiz/shared-collector gaps are superseded by the
Collection proof above; ambiguous collected-sector payers and full acceptance
remain open.
**Classic Homeworld normal Banker and whole-invoice accounting — 14 October.**
Fresh `banker-income --homeworlds` reuses original classic Basic/Advanced
+47 -42
View File
@@ -66,34 +66,37 @@ source/actor/transition/payment boundaries, not wording or fixture-copy echoes.
### Multi-occupant and shared Cistern/Orgiz Collection — 14 October 2026
Cistern now pays every fighter occupant two bank spice after ordinary advisor
releases; a lone advisor never qualifies. Orgiz resolves each fighter occupant
in original storm order, taking one spice from the unique rival collector of a
qualifying deposit. An empty purse simply cannot pay: no debt is created and
ordinary Collection continues. Advanced keeps the supplied territory-Collection
trigger and Basic keeps the provisional positive-deposit mapping.
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.
The Advanced shared-Ecaz branch now settles instead of withholding. After the
real bilateral allocation reports the actual split, each occupant takes one
spice from the first actual positive foreign recipient — Ecaz first, then the
ally — never from a zero share, an old wallet or the occupant itself, and never
beyond that recipient's available spice. Total spice is conserved by the
transfer; only the real collected lot is credited.
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.
Genuine evidence: the original Station/Ecaz-Guild/Fremen fixtures reach these
windows through real stage/phase controls, paid shipments, Collection
inspection/reveal, free entry, alliances and the engine's own allocation
protocol; only unplayed Spice Card order, printed supply tokens and conserved
counter positions are controlled. Cases cover equal/propose/zero splits, the
first-eligible recipient, two owners with one available spice, lone-advisor
exclusion, JSON normalization and deterministic re-settlement.
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.
`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`. Other contested sources
(Cistern/Orgiz ownership contests, Ecaz shared-lot theft ordering variants) and
full acceptance remain open.
`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.
## Mixed E1/E2 and paired E3 Nexus — 7 October 2026
@@ -749,13 +752,15 @@ Existing modified-defense, Atreides Suk/KH and private split-source guards remai
The supplied `UNOFFICIAL_Revised_Dune_Rulebook_v.2.3_web.pdf`, **physical page 24**,
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, the quote maps an original territory's positive Collection to
one transfer, even when multiple collected source keys identify the same
unique rival payer. A different, missing, contested or shared payer withholds
that territory's transfer; other territories and ordinary Collection proceed.
An owner's own Collection is not theft. This is the explicit application of
the authorized Advanced trigger, not a newly discovered publisher ruling on
contested ownership or shared allocation.
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.
Basic retains its recorded provisional one-transfer-per-positive-deposit
interpretation. The context requires its existing public `advanced` boolean:
@@ -863,15 +868,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 takes one spice from the
unique rival collector of each qualifying deposit after ordinary advisor
releases. Owners resolve in original storm order, and an empty purse simply
cannot pay; no debt is created and ordinary Collection continues. Advanced
uses the territory Collection event; Basic keeps the provisional
positive-deposit trigger. For an Ecaz shared lot, the real bilateral
allocation settles first, then each occupant takes one from the first actual
positive foreign recipient (Ecaz, then ally) that still has spice, never from
a zero share, an old wallet or the occupant itself.
- **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.
- **Jacurutu:** one bank spice per opposing undialed physical counter actually
sent to the Tanks. Unique readings remain automatic. For different possible
@@ -1017,10 +1022,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 contested Cistern/Orgiz benefits and Ecaz shared-lot Orgiz allocation.
Jacurutu mixed dial and Testing Station order now use the visible recommended
interpretations above. Basic Orgiz remains provisional per collected deposit;
the Advanced territory count is source-cut over.
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.
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
+35 -30
View File
@@ -2,40 +2,45 @@
## 14 October 2026 — Cistern/Orgiz multi-occupant and shared-lot Collection
Same recommended-option priority, no further clarification prompt. Cistern now
pays each fighter occupant two bank spice after ordinary advisor releases, and
Orgiz resolves each fighter occupant in original storm order, taking one spice
from the unique rival collector of a qualifying deposit. An empty purse pays
nothing, creates no debt and never blocks ordinary Collection. Advanced keeps
the supplied territory-Collection trigger; Basic keeps the provisional
positive-deposit mapping. A lone advisor still never qualifies.
Recommended-option priority: Advanced Orgiz now chooses one definite actual
foreign collected-sector payer per territory in original storm order, skipping
self and empty purses. Multiple source keys never multiply a territory charge.
Owners also resolve in original storm order; an earlier owner can exhaust a
purse, so later owners consider the next eligible actual collector. A sector
with an ambiguous collector still withholds its territory effect.
Advanced shared-Ecaz lots now settle after their real bilateral allocation:
each occupant takes one spice from the first actual positive foreign recipient
(Ecaz first, then ally), never from a zero share, the occupant itself, an old
wallet or beyond that recipient’s available spice. Total spice is conserved by
the transfer and only the real collected lot is credited.
Cistern pays every fighter occupant two bank spice after ordinary advisor
releases. Basic Orgiz keeps the provisional positive-deposit mapping. Shared
Advanced Ecaz desert lots wait for actual bilateral allocation, then consider
positive foreign recipients Ecaz first, ally second. No debt is created.
Occupant/payer ordering is a provisional recommended interpretation, not a
new publisher ruling or separately answered numerical user question.
BEFORE the two-occupant cases withheld their benefit; the shared Advanced branch
was explicitly unpaid. AFTER both are connected through the engine’s own
Collection path. Two superseded expectation tests and one wording assertion were
deleted rather than re-pinned; the exhausted Cistern/Orgiz guards and their
instructions were rewritten.
Evidence correction for `388c5fa`: its shared fixture directly repositioned
armies/spice, fabricated alliance/phase fields and cleared physical hands.
It was not a genuine native route. Its claimed app-build success was also not
observed. Those claims are superseded, not used as current acceptance.
Evidence uses genuine paths only: original Ecaz/Guild/Fremen (or Station)
Advanced lobbies, real stage/phase controls, paid shipments, Collection
inspection and reveal, next-turn free entry, a real worm Nexus alliance and the
engine’s own allocation protocol. Conserved unplayed Spice Card order, printed
supply tokens and counter positions are the only staging. Cases cover equal,
propose and zero splits, first-eligible recipient, two owners with a single
available spice, lone-advisor exclusion, JSON normalization and deterministic
re-settlement.
The replacement preserves original forces, wallets, cards, alliances and
phases. Original unplayed desert-card order and the actual token lottery are
the only staging. Real setup, token reveal, free entry, worm alliance, Ecaz
departure and fresh paid Ecaz/Guild shipments produce the original shared
allocation. Rock terrain cannot create that lot. The third-party Collector
fixture now uses a definite Emperor shipment to an actual empty non-storm
deposit rather than catching arbitrary failures or selecting an Orgiz owner.
Wording and deterministic-copy assertions were removed. Meaningful cases
cover payer order/depletion, self/zero-share exclusion, acceptance timing,
wrong actor and stale allocation.
Final related17-file union127/127, five consecutive randomised runs8/8, types/
lint and app build pass with existing module-registration, chunk-size and
route-classification warnings. Contested Cistern/Orgiz ownership, Ecaz
shared-lot theft ordering variants, complete modules and deployed acceptance
remain open.
Current final three-file union27/27; types/lint and app build pass. Actual
390×844 GameTable proof: original Funeral Plain lot4, proposal4/0 leaves
Ecaz7/Guild11 unchanged; Guild acceptance produces Ecaz10/Guild12 and one
Orgiz transfer, no remaining decision and enabled Ready. No horizontal
overflow; a fresh settled surface has no application errors. Initial temporary
QA middleware order was corrected before interaction. Local proof is outside
Git; owned hosts/tabs/scaffolding were removed. Existing DEP0205, large-chunk
and route-classification build warnings remain. Ambiguous collected-sector
payers, full modes, combined-module and deployed acceptance remain open.
+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 instead says each time Collection occurs in a territory containing spice: Advanced applies one transfer per actual positive territory Collection, only to a unique rival payer. Multiple source keys do not add transfers. Contested ownership, inconsistent/different payers and Ecaz shared lots still withhold only uncertain transfers; ordinary Collection proceeds. No new publisher ruling on those pending cases is claimed. | [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. 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) |
| 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) |
+27 -13
View File
@@ -131,9 +131,9 @@ function transferOrgiz(
receipts: DiscoveryBaseCollectionReceipt[],
effects: DiscoveryCollectionQuote['effects'],
) {
if (!collector || collector.id === ownerReceipt.player) return;
if (!collector || collector.id === ownerReceipt.player) return false;
const collectorReceipt = receipts.find(entry => entry.player === collector.id)!;
if (collectorReceipt.balance === 0) return;
if (collectorReceipt.balance === 0) return false;
requireCollection(
Number.isSafeInteger(ownerReceipt.balance + 1),
'Orgiz theft would invalidate a player spice balance.',
@@ -144,6 +144,7 @@ function transferOrgiz(
kind: 'orgiz', player: ownerReceipt.player, from: collector.id,
location: key, amount: 1, source: 'player',
});
return true;
}
/**
@@ -151,10 +152,10 @@ 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 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.
* 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.
*/
export function quoteDiscoveryCollection(
context: DiscoveryCollectionContext,
@@ -271,7 +272,10 @@ export function quoteDiscoveryCollection(
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 }>()
? new Map<string, {
collectors: { location: string; collector: DiscoveryCollectionSeat }[];
ambiguous: boolean;
}>()
: undefined;
for (const key of sourceKeys) {
if (context.spice[key] <= collection.spice[key]) continue;
@@ -287,15 +291,25 @@ export function quoteDiscoveryCollection(
transferOrgiz(key, collector, ownerReceipt, receipts, effects);
continue;
}
const pending = transfers.get(territory);
if (!pending) transfers.set(territory, { location: key, collector });
else if (pending.collector?.id !== collector?.id)
pending.collector = undefined;
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 });
}
if (transfers)
for (const pending of transfers.values())
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)
transferOrgiz(pending.location, pending.collector, ownerReceipt, receipts, effects);
for (const candidate of pending.collectors)
if (transferOrgiz(candidate.location, candidate.collector, ownerReceipt, receipts, effects))
break;
}
}
return { receipts, effects };
+4 -3
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, 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.',
'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.',
'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.',
@@ -4251,14 +4251,15 @@ 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 defeated-player physical Jacurutu allocation and cumulative storm-order Testing Station choices are explicit, visible prototype interpretations, not new publisher rulings. Great Maker, entry and source-storm inferences remain; contested Cistern/Orgiz and shared-lot theft stay open.',
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.',
evidence: ['docs/DISCOVERY_COMPONENTS.md', 'docs/DISCOVERY_PROTOTYPE.md'],
},
{
area: 'Verification', status: 'Partial',
detail: 'Current recommended Jacurutu/Testing Station batch passes104/104 in12 related files, types/lint and build. Genuine Advanced dial2/support1 produces1-or0 reward with the same original losses, support and later Collection; unique readings stay automatic. Original Basic/Advanced station sources, real alliance/garrisons and390px first/second cumulative choices preserve one final traversal, source and Weather exclusion. All four legal policies are exercised. Earlier native-family evidence remains source-bound; broader combinations, assurance, strategy and deployed acceptance stay open.',
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.',
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/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'],
},
],
+28 -4
View File
@@ -513,26 +513,50 @@ void test('Advanced counts each territory once across positive partial sector co
}
});
void test('Advanced withholds a territory with distinct collected payers, including an own collection, but not independent theft', () => {
void test('Advanced selects one actual foreign territory collector in storm order, skipping own collections', () => {
for (const second of ['a', 'g']) {
const game = fixture(true);
reveal(game, ORGIZ_PROCESSING_STATION);
game.players[0].forces = { [`${ORGIZ_PROCESSING_STATION}:0`]: 1 };
game.players[1].forces = { 'hagga_basin:12': 1, 'red_chasm:7': 1 };
game.players.find(player => player.id === second)!.forces['hagga_basin:13'] = 1;
game.order = ['a', 'g', 'h'];
game.spice = { 'hagga_basin:12': 3, 'hagga_basin:13': 2, 'red_chasm:7': 2 };
const { ordinary, discovery } = composed(game);
const payer = second === 'a' ? 'h' : 'g';
assert.deepEqual(discovery.effects, [{
kind: 'orgiz', player: 'a', from: payer,
location: second === 'a' ? 'hagga_basin:12' : 'hagga_basin:13',
amount: 1, source: 'player',
}, {
kind: 'orgiz', player: 'a', from: 'h', location: 'red_chasm:7',
amount: 1, source: 'player',
}]);
assert.equal(balance(discovery, 'a'), ordinary.receipts[0].balance + 1);
assert.equal(balance(discovery, 'h'), ordinary.receipts[1].balance - 1);
assert.equal(balance(discovery, 'g'), ordinary.receipts[2].balance);
assert.equal(balance(discovery, 'a'), ordinary.receipts[0].balance + 2);
assert.equal(balance(discovery, 'h'), ordinary.receipts.find(r => r.player === 'h')!.balance - (payer === 'h' ? 2 : 1));
assert.equal(balance(discovery, 'g'), ordinary.receipts.find(r => r.player === 'g')!.balance - (payer === 'g' ? 1 : 0));
assert.equal(ordinary.spice['hagga_basin:13'], 0);
}
});
void test('Advanced falls through to the next actual collector when an earlier Orgiz owner exhausts the first purse', () => {
const game = fixture(true, [['a', 'atreides'], ['h', 'harkonnen'], ['g', 'guild'], ['e', 'emperor']]);
reveal(game, ORGIZ_PROCESSING_STATION);
game.order = ['a', 'g', 'h', 'e'];
game.players.find(p => p.id === 'a')!.forces = { [`${ORGIZ_PROCESSING_STATION}:0`]: 1 };
game.players.find(p => p.id === 'g')!.forces = { [`${ORGIZ_PROCESSING_STATION}:0`]: 1 };
game.players.find(p => p.id === 'h')!.forces = { 'hagga_basin:12': 1 };
game.players.find(p => p.id === 'e')!.forces = { 'hagga_basin:13': 1 };
game.spice = { 'hagga_basin:12': 1, 'hagga_basin:13': 2 };
const { discovery } = composed(game);
assert.deepEqual(discovery.effects, [
{ kind: 'orgiz', player: 'a', from: 'h', location: 'hagga_basin:12', amount: 1, source: 'player' },
{ kind: 'orgiz', player: 'g', from: 'e', location: 'hagga_basin:13', amount: 1, source: 'player' },
]);
assert.equal(balance(discovery, 'h'), 0);
assert.equal(balance(discovery, 'e'), 1);
});
void test('Advanced does not select a payer from an uncontested sector when another collected sector is contested', () => {
const game = fixture(true);
reveal(game, ORGIZ_PROCESSING_STATION);
+45 -60
View File
@@ -1,68 +1,53 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { normalizeAutomaticGame, viewGame, type Game } from '../game/engine';
import { ORGIZ_PROCESSING_STATION } from '../game/discovery-collection';
import { prepareSharedOrgizCollection, settleSharedOrgiz } from './fixture-discovery-shared-orgiz';
import {applyAction,type Game} from '../game/engine';
import {ORGIZ_PROCESSING_STATION} from '../game/discovery-collection';
import {prepareSharedOrgizCollection,settleSharedOrgiz} from './fixture-discovery-shared-orgiz';
const wallets = (game: Game) => Object.fromEntries(game.players.map(player => [player.id, player.spice]));
const transfers = (game: Game) =>
game.log.filter(entry => entry.automatic?.name === 'Orgiz Processing Station');
const wallets=(game:Game)=>Object.fromEntries(game.players.map(p=>[p.id,p.spice]));
const transfers=(game:Game)=>game.log.filter(entry=>entry.automatic?.name==='Orgiz Processing Station');
for (const scenario of [
{ name: 'an equal split charges the first actual recipient', choice: { kind: 'equal' as const }, payer: 'e' },
{ name: 'a full Ecaz share charges Ecaz', choice: { kind: 'propose' as const, ecazShare: 4 }, payer: 'e' },
{ name: 'a zero Ecaz share charges the ally instead', choice: { kind: 'propose' as const, ecazShare: 0 }, payer: 'g' },
]) {
void test(`Advanced shared Collection: ${scenario.name}, never an old wallet or an unavailable share`, () => {
const position = prepareSharedOrgizCollection();
const lot = position.ordinary.shared[0];
assert.equal(lot.territory, 'hagga_basin');
assert.equal(lot.ecaz, position.ecaz);
assert.equal(lot.ally, position.guild);
const base = structuredClone(position.game);
const before = wallets(base);
const settled = settleSharedOrgiz(position, scenario.choice);
assert.deepEqual(position.game, base, 'Settling must not mutate the prepared position.');
const receipt = settled.receipts.at(-1)!;
assert.equal(receipt.territory, lot.territory);
assert.equal(receipt.ecazAmount + receipt.allyAmount, lot.amount);
assert.ok(receipt[scenario.payer === 'e' ? 'ecazAmount' : 'allyAmount'] > 0);
const expected = { ...before };
expected[receipt.ecaz] += receipt.ecazAmount;
expected[receipt.ally] += receipt.allyAmount;
expected[position.fremen] += 1;
expected[scenario.payer] -= 1;
assert.deepEqual(wallets(settled.game), expected);
// Total spice is conserved by the transfer itself, and the credited total
// equals the real collected lot.
assert.equal(Object.values(wallets(settled.game)).reduce((sum, amount) => sum + amount, 0),
Object.values(before).reduce((sum, amount) => sum + amount, 0) + lot.amount);
assert.equal(Object.values(settled.game.spice).reduce((sum, amount) => sum + amount, 0),
Object.values(base.spice).reduce((sum, amount) => sum + amount, 0) - lot.amount);
const owned = transfers(settled.game);
assert.equal(owned.length, 1);
assert.equal(owned[0].automatic?.faction, 'fremen');
assert.match(owned[0].text, /after the original shared territory allocation/);
// The occupied location keeps its real garrison and the fremen wallet grows.
assert.equal(settled.game.players.find(player => player.id === position.fremen)!.forces[`${ORGIZ_PROCESSING_STATION}:0`], 1);
assert.equal(wallets(settled.game)[position.fremen], before[position.fremen] + 1);
const stable = JSON.parse(JSON.stringify(settled.game)) as Game;
assert.deepEqual(normalizeAutomaticGame(JSON.parse(JSON.stringify(stable))), stable);
assert.equal(viewGame(settled.game, position.ecaz).players
.find(player => player.id === position.ecaz)!.spice, expected[position.ecaz]);
assert.equal(viewGame(settled.game, position.ecaz).players
.find(player => player.id === position.guild)!.spice, undefined,
'Opposing wallets stay private.');
for(const split of ['equal','all-ecaz','all-guild'] as const) {
void test(`Advanced native shared Collection: ${split} respects the actual foreign recipient and sole Orgiz owner`,()=>{
const position=prepareSharedOrgizCollection();
const original=structuredClone(position.game);
const before=wallets(original);
const choice=split==='equal'?{kind:'equal' as const}:
{kind:'propose' as const,ecazShare:split==='all-ecaz'?position.lot.amount:0};
const settled=settleSharedOrgiz(position,choice);
const receipt=settled.receipt;
const expected={...before};
expected[position.ecaz]+=receipt.ecazAmount;
expected[position.guild]+=receipt.allyAmount;
const tax=receipt.ecazAmount>0?1:0;
expected[position.ecaz]-=tax;
expected[position.owner]+=tax;
assert.deepEqual(wallets(settled.game),expected);
assert.equal(receipt.ecazAmount+receipt.allyAmount,position.lot.amount);
assert.equal(transfers(settled.game).length,tax);
assert.equal(settled.game.players.find(p=>p.id===position.owner)!
.forces[`${ORGIZ_PROCESSING_STATION}:0`],2);
assert.deepEqual(position.game,original);
const committed=structuredClone(settled.game);
const last=settled.steps.at(-1)!;
assert.throws(()=>applyAction(settled.game,last.actor,last.action));
assert.deepEqual(settled.game,committed,'A stale allocation cannot pay the same territory twice.');
});
}
void test('re-settling the same prepaid position is deterministic and never accumulates', () => {
const position = prepareSharedOrgizCollection();
const first = settleSharedOrgiz(position, { kind: 'equal' });
const second = settleSharedOrgiz(position, { kind: 'equal' });
assert.equal(first.steps.length, second.steps.length);
assert.deepEqual(wallets(second.game), wallets(first.game));
assert.deepEqual(transfers(second.game).length, transfers(first.game).length);
assert.deepEqual(wallets(position.game), wallets(position.before));
assert.deepEqual(normalizeAutomaticGame(structuredClone(second.game)), second.game);
void test('Native shared Orgiz waits for ally acceptance and rejects the wrong actor without changing wallets',()=>{
const position=prepareSharedOrgizCollection();
const before=structuredClone(position.game);
const event=before.ecazCollection!.event;
const action={type:'decision' as const,event,allocation:{kind:'propose' as const,ecazShare:position.lot.amount}};
assert.throws(()=>applyAction(position.game,position.guild,action));
assert.deepEqual(position.game,before);
const proposed=applyAction(position.game,position.ecaz,action);
assert.equal(proposed.decision?.player,position.guild);
assert.deepEqual(wallets(proposed),wallets(before));
assert.equal(transfers(proposed).length,0);
const accepted=applyAction(proposed,position.guild,
{type:'decision',event,allocation:{kind:'accept'}});
assert.equal(wallets(accepted)[position.ecaz],wallets(before)[position.ecaz]+position.lot.amount-1);
assert.equal(wallets(accepted)[position.guild],wallets(before)[position.guild]+1);
});
+13 -26
View File
@@ -16,36 +16,23 @@ export function createOccupiedDiscoveryCollectionFixture(
location:typeof CISTERN|typeof ORGIZ_PROCESSING_STATION,
advanced=false,
) {
const fixture=createDiscoveryOccupantsFixture({location,advanced});
const fixture=createDiscoveryOccupantsFixture({location,advanced,adjacentBlow:location===ORGIZ_PROCESSING_STATION});
const actions:TestingStationContestStep[]=[...fixture.actions];
let game=fixture.game;
let collector:string|null=null,deposit:string|null=null;
if(location===ORGIZ_PROCESSING_STATION) {
game=advanceTestingStationContest(game,g=>g.phase===5&&g.turn<=4&&g.active!==null&&
(g.movementRemaining?.length??0)===1&&clean(g)&&
Object.entries(g.spice).some(([key,amount])=>amount>0&&
key!==`${ORGIZ_PROCESSING_STATION}:0`),actions);
const shipActor=game.active;
assert.ok(shipActor,'A real movement window must have an active seat.');
const candidates=Object.entries(game.spice)
.filter(([key,amount])=>amount>0&&key!==`${ORGIZ_PROCESSING_STATION}:0`)
.sort((left,right)=>right[1]-left[1]);
assert.ok(candidates.length,'A real blow must supply a positive desert deposit.');
let shipped:Game|null=null,shippedDeposit:string|null=null;
for(const [key] of candidates) {
const {territory,sector}=splitLocation(key);
const shipment:Action={type:'ship',territory,sector,amount:2,elite:0,allyPayment:0};
try {
const next=applyAction(game,shipActor,shipment);
actions.push({actor:shipActor,action:shipment});
shipped=advanceTestingStationContest(next,clean,actions);
shippedDeposit=key;
break;
} catch { continue; }
}
assert.ok(shipped&&shippedDeposit,'A real paid shipment must reach one actual positive deposit.');
collector=shipActor; deposit=shippedDeposit;
game=shipped;
game=advanceTestingStationContest(game,g=>g.turn===2&&g.phase===5&&
g.active==='emperor'&&clean(g),actions);
const candidate=Object.entries(game.spice).find(([key,amount])=>amount>=2&&
splitLocation(key).sector!==game.storm&&game.players.every(p=>(p.forces[key]??0)===0));
assert.ok(candidate,'An original printed blow must supply an empty non-storm deposit.');
deposit=candidate[0];
collector='emperor';
const {territory,sector}=splitLocation(deposit);
const shipment:Action={type:'ship',territory,sector,amount:2,elite:0,allyPayment:0};
game=applyAction(game,collector,shipment);
actions.push({actor:collector,action:shipment});
game=advanceTestingStationContest(game,clean,actions);
assert.equal(game.players.find(p=>p.id===collector)!.forces[deposit],2);
}
game=advanceTestingStationContest(game,g=>g.phase===5&&g.active!==null&&
+82 -91
View File
@@ -1,103 +1,94 @@
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 {
applyAction, createGame, initializeDiscoveryGameForAudit, joinGame, newPlayer,
normalizeAutomaticGame, viewGame, type Action, type Game,
} from '../game/engine';
import { territory } from '../game/board';
import { quoteSpiceCollection } from '../game/board-resolution-quote';
import { ORGIZ_PROCESSING_STATION, quoteDiscoveryCollection } from '../game/discovery-collection';
import { putDiscovery } from './fixture-discovery';
import { nativeTypedClearBoard, nativeTypedPhase, nativeTypedPlace } from './fixture-discovery-native-typed';
import { advanceTestingStationContest } from './fixture-testing-station-contest';
createDiscoveryOccupantsFixture,advanceTestingStationContest,nextTestingStationContestStep,
type TestingStationContestStep,
} from './fixture-testing-station-contest';
export type SharedOrgizStep = { actor: string; action: Action };
const clean=(game:Game)=>!game.decision&&!game.response&&!game.phaseOpening;
/** Fresh original Ecaz/Guild/Fremen Advanced Discovery game. Only conserved
* unplayed Spice Card order, the printed supply token, reserve-counter positions
* and board spice already produced by real blows are controlled; every phase,
* reveal, payment, allocation, transfer and Collection is an original action. */
/** 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() {
let game = createGame('SHAREDORGIZ', newPlayer('e', 'Ecaz', 'ecaz'), true, ['ecaz']);
joinGame(game, newPlayer('g', 'Guild', 'guild'));
joinGame(game, newPlayer('f', 'Fremen', 'fremen'));
for (const player of game.players)
if (!player.ready) game = applyAction(game, player.id, { type: 'ready' });
game.discoveryEnabled = true;
const initial = structuredClone(game);
game = initializeDiscoveryGameForAudit(game);
game = advanceTestingStationContest(game, state => state.status === 'playing');
const ecaz = 'e', guild = 'g', fremen = 'f';
for (const [position, name] of ['hagga_basin', 'cielago_south', 'red_chasm', 'funeral_plain'].entries()) {
const index = game.spiceDeck.findIndex((card, i) => i >= position &&
'territory' in card && card.territory === name && !card.discovery);
assert.ok(index >= position);
game.spiceDeck.splice(position, 0, game.spiceDeck.splice(index, 1)[0]);
const prefix=createDiscoveryOccupantsFixture({location:ORGIZ_PROCESSING_STATION,advanced:true,secondAmount:2});
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;
const blowKeys:string[]=[];
for(let position=0;position<2;position++) {
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.');
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}`);
}
}
game = nativeTypedPhase(game, 5);
nativeTypedClearBoard(game);
const token = putDiscovery(game, ORGIZ_PROCESSING_STATION);
const source = `${token.territory}:${token.sector}`;
assert.notEqual(token.sector, game.storm);
nativeTypedPlace(game, fremen, source, 2);
game = nativeTypedPhase(game, 7);
assert.ok(viewGame(game, fremen).discoveries!.canInspect.includes(token.id));
game = applyAction(game, fremen, { type: 'discovery', token: token.id, reveal: false });
assert.ok(viewGame(game, fremen).discoveries!.canReveal.includes(token.id));
game = applyAction(game, fremen, { type: 'discovery', token: token.id, reveal: true });
game = nativeTypedPhase(game, 8);
game = nativeTypedPhase(game, 5, 2);
nativeTypedClearBoard(game);
// Ecaz and its Guild ally hold the same real desert territory, so the original
// quote creates the shared lot whose allocation decides the actual recipient.
const haggaKeys = territory('hagga_basin').sectors.filter(sector => sector !== game.storm)
.slice(0, 2).map(sector => `hagga_basin:${sector}`);
assert.equal(haggaKeys.length, 2);
const untouchedKey = 'the_great_flat:15';
nativeTypedPlace(game, fremen, `${ORGIZ_PROCESSING_STATION}:0`, 1);
nativeTypedPlace(game, ecaz, haggaKeys[0], 1);
nativeTypedPlace(game, guild, haggaKeys[1], 1);
// Conserved precondition, as in the original Collection tests: the Ecaz/Guild
// alliance exists before this Collection. The governed behavior under audit is
// the real allocation-then-transfer sequence below, not the pact's own rules.
game.players.find(player => player.id === ecaz)!.ally = guild;
game.players.find(player => player.id === guild)!.ally = ecaz;
const boardSpice = Object.values(game.spice).reduce((sum, amount) => sum + amount, 0);
assert.ok(boardSpice >= 6);
game.spice = { [haggaKeys[0]]: 4, [haggaKeys[1]]: 2, [untouchedKey]: boardSpice - 6 };
const ordinary = quoteSpiceCollection(game);
assert.equal(ordinary.shared.length, 1);
assert.deepEqual({ ...ordinary.shared[0], amount: 0 },
{ territory: 'hagga_basin', ecaz, ally: guild, amount: 0 });
assert.ok(ordinary.shared[0].amount > 0, 'The real shared lot must hold collected desert spice.');
assert.deepEqual(quoteDiscoveryCollection(game, ordinary).effects, []);
const before = structuredClone(game);
for (const player of game.players)
Object.assign(player, { hand: [], traitors: [], traitorChoices: [], shipped: true, moved: 1 });
Object.assign(game, { phase: 5, active: fremen, movementRemaining: [fremen], phaseOpening: null });
return { initial, game, before, ordinary, boardSpice, token: token.id, source,
haggaKeys, untouchedKey, ecaz, guild, fremen };
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]);
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;
let next:TestingStationContestStep|null;
if(game.turn===3&&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}};
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],
})}`);
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.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 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'};
}
/** Settle the real Collection, then answer each original Ecaz allocation prompt. */
export function settleSharedOrgiz(
position: ReturnType<typeof prepareSharedOrgizCollection>,
choice: { kind: 'propose'; ecazShare: number } | { kind: 'equal' } = { kind: 'equal' },
position:ReturnType<typeof prepareSharedOrgizCollection>,
choice:{kind:'propose';ecazShare:number}|{kind:'equal'}={kind:'equal'},
) {
const steps: SharedOrgizStep[] = [];
let game = applyAction(position.game, position.fremen, { type: 'endMovement' });
steps.push({ actor: position.fremen, action: { type: 'endMovement' } });
assert.equal(game.phase, 7);
const event = game.ecazCollection?.event ?? null;
assert.ok(event, 'The real Collection must own its allocation event.');
while (game.decision?.kind === 'ecazSpice' && steps.length < 20) {
const actor = game.decision.player;
const allocation = actor === position.ecaz ? choice : { kind: 'accept' as const };
const action = { type: 'decision', event: game.ecazCollection!.event, allocation } as Action;
game = applyAction(game, game.decision.player, action);
steps.push({ actor: position.ecaz, action });
let game=position.game;
const event=game.ecazCollection!.event;
const steps:TestingStationContestStep[]=[];
while(game.decision?.kind==='ecazSpice'&&steps.length<20) {
const actor=game.decision.player;
const allocation=actor===position.ecaz?choice:{kind:'accept' as const};
const action:Action={type:'decision',event,allocation};
steps.push({actor,action});
game=applyAction(game,actor,action);
}
assert.equal(game.decision, null, 'The original shared allocation must settle without inventing another choice.');
const stable = JSON.parse(JSON.stringify(game)) as Game;
assert.deepEqual(normalizeAutomaticGame(JSON.parse(JSON.stringify(stable))), stable);
return { game, steps, event, receipts: game.ecazCollection?.settled ?? [] };
assert.equal(game.decision,null);
return {game,steps,event,receipt:game.ecazCollection!.settled.at(-1)!};
}