Resolve definite Orgiz payers and prove native shared Collection
This commit is contained in:
1 parent
388c5fa8d5
commit
bc09ac3cfd
11 files changed
+310
-292
No files matched your search
@@ -94,9 +94,9 @@ export function DiscoveryPanel({ game, act, busy }: Props) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</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
|
<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
|
||||||
? 'A sole Orgiz occupant takes one spice for each territory where one rival collected board spice.'
|
? '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.'
|
||||||
: '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>
|
: '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>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-19
@@ -29,24 +29,28 @@ optional skill bands, combinations and public expansion starts remain open.
|
|||||||
Rules implementation continues; comprehensive assurance and administration
|
Rules implementation continues; comprehensive assurance and administration
|
||||||
remain deferred until all rules are implemented.
|
remain deferred until all rules are implemented.
|
||||||
|
|
||||||
**Cistern and Orgiz multi-occupant Collection — 14 October.**
|
**Orgiz payer ordering and native Collection proof — 14 October.**
|
||||||
Following the same choose-recommended-option priority, the prototype closes two
|
Every Cistern fighter occupant receives two bank spice. Orgiz occupants resolve
|
||||||
withheld benefits: each Cistern fighter occupant now collects its two bank spice
|
in original storm order without debt. Advanced charges once per territory,
|
||||||
after ordinary advisor releases, and each Orgiz fighter occupant takes one spice
|
selecting the first definite foreign collected-sector payer in storm order that
|
||||||
from the unique rival collector of a qualifying deposit. Orgiz owners resolve in
|
can pay; self and empty purses are skipped. Basic retains the provisional
|
||||||
original storm order; an empty purse pays nothing and creates no debt, so
|
positive-deposit mapping. Shared Ecaz desert lots wait for actual bilateral
|
||||||
ordinary Collection continues. Advanced keeps the supplied territory-Collection
|
allocation, then consider positive foreign recipients Ecaz first, ally second.
|
||||||
trigger; Basic keeps the provisional positive-deposit mapping. Advanced
|
These occupant/payer orderings are provisional recommended interpretations.
|
||||||
shared-Ecaz lots are no longer withheld: after the real bilateral allocation,
|
|
||||||
each occupant takes one spice from the first actual positive foreign recipient
|
The shared fixture now uses original setup, inspection/reveal, free entry,
|
||||||
(Ecaz, then ally), never from a zero share, an old wallet or itself.
|
worm alliance, real departure and fresh paid shipments. It no longer clears
|
||||||
Genuine fixtures reach these windows through real stage/phase controls, paid
|
armies/cards or fabricates alliances/phases/spice positions. Fresh original
|
||||||
shipments, Collection inspection/reveal, free entry, alliance and the engine's
|
desert blows avoid the former rock-terrain precondition failure. Native cases
|
||||||
own allocation protocol; equal/propose/zero splits, first-eligible recipient,
|
cover equal/full/zero Ecaz shares, self exclusion, acceptance timing,
|
||||||
one available spice for two owners, lone-advisor exclusion and deterministic
|
wrong-actor rejection and stale allocation. Final three-file union27/27;
|
||||||
re-settlement are exercised. Related17-file union127/127, five repeat runs
|
types/lint and app build pass. Actual390px table proposal4/0 leaves wallets7/11
|
||||||
8/8, types/lint and app build pass. Contested Cistern/Orgiz ownership and full
|
unchanged until Guild accepts, then credits Ecaz10/Guild12 with one transfer,
|
||||||
acceptance remain open. [Contract](DISCOVERY_PROTOTYPE.md#multi-occupant-and-shared-cisternorgiz-collection--14-october-2026).
|
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.**
|
**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
|
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,
|
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).
|
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.**
|
**Classic Homeworld normal Banker and whole-invoice accounting — 14 October.**
|
||||||
Fresh `banker-income --homeworlds` reuses original classic Basic/Advanced
|
Fresh `banker-income --homeworlds` reuses original classic Basic/Advanced
|
||||||
|
|||||||
+47
-42
@@ -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
|
### Multi-occupant and shared Cistern/Orgiz Collection — 14 October 2026
|
||||||
|
|
||||||
Cistern now pays every fighter occupant two bank spice after ordinary advisor
|
Cistern pays every fighter occupant two bank spice after ordinary advisor
|
||||||
releases; a lone advisor never qualifies. Orgiz resolves each fighter occupant
|
releases; a lone advisor never qualifies. Orgiz resolves fighter occupants in
|
||||||
in original storm order, taking one spice from the unique rival collector of a
|
original storm order. Advanced charges one actual foreign collector per
|
||||||
qualifying deposit. An empty purse simply cannot pay: no debt is created and
|
territory, choosing among definite collected-sector payers in original storm
|
||||||
ordinary Collection continues. Advanced keeps the supplied territory-Collection
|
order and skipping the owner and empty purses. Basic retains the provisional
|
||||||
trigger and Basic keeps the provisional positive-deposit mapping.
|
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
|
Shared Ecaz desert lots wait for the actual bilateral allocation. Each occupant
|
||||||
real bilateral allocation reports the actual split, each occupant takes one
|
considers positive foreign recipients Ecaz first, then ally, charging at most
|
||||||
spice from the first actual positive foreign recipient — Ecaz first, then the
|
one spice from an available current balance. A zero share or an old wallet
|
||||||
ally — never from a zero share, an old wallet or the occupant itself, and never
|
alone cannot establish eligibility; the owner never pays itself. These occupant
|
||||||
beyond that recipient's available spice. Total spice is conserved by the
|
and payer orderings are recommended provisional interpretations, not new
|
||||||
transfer; only the real collected lot is credited.
|
publisher rulings or separately answered numerical user questions.
|
||||||
|
|
||||||
Genuine evidence: the original Station/Ecaz-Guild/Fremen fixtures reach these
|
Current native fixtures preserve original armies, wallets, hands, alliances
|
||||||
windows through real stage/phase controls, paid shipments, Collection
|
and phases. They use real setup, token placement, paid shipments, inspection,
|
||||||
inspection/reveal, free entry, alliances and the engine's own allocation
|
reveal, free entry, a worm Nexus alliance and allocation. Only original
|
||||||
protocol; only unplayed Spice Card order, printed supply tokens and conserved
|
unplayed Spice Card order and the actual token lottery are staged. The shared
|
||||||
counter positions are controlled. Cases cover equal/propose/zero splits, the
|
route uses two fresh desert blows with different sectors, real Ecaz departure
|
||||||
first-eligible recipient, two owners with one available spice, lone-advisor
|
from Orgiz, and fresh paid Ecaz/Guild shipments. Rock blows cannot create an
|
||||||
exclusion, JSON normalization and deterministic re-settlement.
|
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/fixture-discovery-occupied-collection.ts`,
|
||||||
`tests/discovery-occupied-collection-runtime.test.ts`,
|
`tests/discovery-occupied-collection-runtime.test.ts`,
|
||||||
`tests/fixture-discovery-shared-orgiz.ts` and
|
`tests/fixture-discovery-shared-orgiz.ts` and
|
||||||
`tests/discovery-shared-orgiz-runtime.test.ts`. Other contested sources
|
`tests/discovery-shared-orgiz-runtime.test.ts`. Focused helper regressions also
|
||||||
(Cistern/Orgiz ownership contests, Ecaz shared-lot theft ordering variants) and
|
cover original storm-order payer selection, one available spice for two owners
|
||||||
full acceptance remain open.
|
and advisor exclusion. Ambiguous collected-sector sources and complete
|
||||||
|
combined-module acceptance remain open.
|
||||||
|
|
||||||
## Mixed E1/E2 and paired E3 Nexus — 7 October 2026
|
## 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**,
|
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
|
says Orgiz steals one collected spice **each time collection occurs in a
|
||||||
territory containing spice**, rather than the publisher's older per-blow text.
|
territory containing spice**, rather than the publisher's older per-blow text.
|
||||||
For Advanced, the quote maps an original territory's positive Collection to
|
For Advanced, each fighter occupant receives at most one transfer per actual
|
||||||
one transfer, even when multiple collected source keys identify the same
|
positive territory Collection. Multiple collected source keys do not multiply
|
||||||
unique rival payer. A different, missing, contested or shared payer withholds
|
the transfer. Definite rival collectors are considered in original storm
|
||||||
that territory's transfer; other territories and ordinary Collection proceed.
|
order, skipping self and empty purses. A collected sector whose actual payer
|
||||||
An owner's own Collection is not theft. This is the explicit application of
|
is ambiguous still withholds that territory's transfer. Shared Ecaz desert
|
||||||
the authorized Advanced trigger, not a newly discovered publisher ruling on
|
lots wait for the actual bilateral split, then consider positive foreign
|
||||||
contested ownership or shared allocation.
|
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
|
Basic retains its recorded provisional one-transfer-per-positive-deposit
|
||||||
interpretation. The context requires its existing public `advanced` boolean:
|
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
|
occupant of the revealed location receives two bank spice. A lone advisor
|
||||||
never qualifies. Two fighters therefore both collect; the former withheld
|
never qualifies. Two fighters therefore both collect; the former withheld
|
||||||
two-fighter gap is closed.
|
two-fighter gap is closed.
|
||||||
- **Orgiz:** during Collection, each fighter occupant takes one spice from the
|
- **Orgiz:** during Collection, each fighter occupant resolves in original storm
|
||||||
unique rival collector of each qualifying deposit after ordinary advisor
|
order after ordinary advisor releases. Advanced charges once per territory,
|
||||||
releases. Owners resolve in original storm order, and an empty purse simply
|
choosing the first definite foreign collector in original storm order that
|
||||||
cannot pay; no debt is created and ordinary Collection continues. Advanced
|
can pay. Basic retains one transfer per positive deposit with a unique rival
|
||||||
uses the territory Collection event; Basic keeps the provisional
|
collector. Empty purses never create debt. An ambiguous collected sector
|
||||||
positive-deposit trigger. For an Ecaz shared lot, the real bilateral
|
withholds the territory effect without blocking ordinary Collection. Shared
|
||||||
allocation settles first, then each occupant takes one from the first actual
|
Ecaz desert lots settle their real bilateral allocation first; positive
|
||||||
positive foreign recipient (Ecaz, then ally) that still has spice, never from
|
foreign recipients are considered Ecaz first, ally second. Zero shares and
|
||||||
a zero share, an old wallet or the occupant itself.
|
the occupant itself do not qualify. Ordering remains provisional.
|
||||||
|
|
||||||
- **Jacurutu:** one bank spice per opposing undialed physical counter actually
|
- **Jacurutu:** one bank spice per opposing undialed physical counter actually
|
||||||
sent to the Tanks. Unique readings remain automatic. For different possible
|
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.
|
This is targeted control/recovery evidence, not an unmodified complete game.
|
||||||
The 187 opening saved rooms remain preserved; no server restart was needed.
|
The 187 opening saved rooms remain preserved; no server restart was needed.
|
||||||
|
|
||||||
1. Resolve contested Cistern/Orgiz benefits and Ecaz shared-lot Orgiz allocation.
|
1. Resolve ambiguous collected-sector Orgiz sources and refine combined
|
||||||
Jacurutu mixed dial and Testing Station order now use the visible recommended
|
Discovery interactions. Multi-occupant Cistern/Orgiz, shared allocation,
|
||||||
interpretations above. Basic Orgiz remains provisional per collected deposit;
|
Jacurutu mixed dials and Testing Station ordering now use the visible
|
||||||
the Advanced territory count is source-cut over.
|
recommended interpretations above; Basic Orgiz remains provisional.
|
||||||
2. Integrate combined modules and complete games, then refine interaction,
|
2. Integrate combined modules and complete games, then refine interaction,
|
||||||
strategy and presentation coverage before opening normal start gates.
|
strategy and presentation coverage before opening normal start gates.
|
||||||
3. Use the existing prototype starter and saved-room verification for further
|
3. Use the existing prototype starter and saved-room verification for further
|
||||||
|
|||||||
@@ -2,40 +2,45 @@
|
|||||||
|
|
||||||
## 14 October 2026 — Cistern/Orgiz multi-occupant and shared-lot Collection
|
## 14 October 2026 — Cistern/Orgiz multi-occupant and shared-lot Collection
|
||||||
|
|
||||||
Same recommended-option priority, no further clarification prompt. Cistern now
|
Recommended-option priority: Advanced Orgiz now chooses one definite actual
|
||||||
pays each fighter occupant two bank spice after ordinary advisor releases, and
|
foreign collected-sector payer per territory in original storm order, skipping
|
||||||
Orgiz resolves each fighter occupant in original storm order, taking one spice
|
self and empty purses. Multiple source keys never multiply a territory charge.
|
||||||
from the unique rival collector of a qualifying deposit. An empty purse pays
|
Owners also resolve in original storm order; an earlier owner can exhaust a
|
||||||
nothing, creates no debt and never blocks ordinary Collection. Advanced keeps
|
purse, so later owners consider the next eligible actual collector. A sector
|
||||||
the supplied territory-Collection trigger; Basic keeps the provisional
|
with an ambiguous collector still withholds its territory effect.
|
||||||
positive-deposit mapping. A lone advisor still never qualifies.
|
|
||||||
|
|
||||||
Advanced shared-Ecaz lots now settle after their real bilateral allocation:
|
Cistern pays every fighter occupant two bank spice after ordinary advisor
|
||||||
each occupant takes one spice from the first actual positive foreign recipient
|
releases. Basic Orgiz keeps the provisional positive-deposit mapping. Shared
|
||||||
(Ecaz first, then ally), never from a zero share, the occupant itself, an old
|
Advanced Ecaz desert lots wait for actual bilateral allocation, then consider
|
||||||
wallet or beyond that recipient’s available spice. Total spice is conserved by
|
positive foreign recipients Ecaz first, ally second. No debt is created.
|
||||||
the transfer and only the real collected lot is credited.
|
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
|
Evidence correction for `388c5fa`: its shared fixture directly repositioned
|
||||||
was explicitly unpaid. AFTER both are connected through the engine’s own
|
armies/spice, fabricated alliance/phase fields and cleared physical hands.
|
||||||
Collection path. Two superseded expectation tests and one wording assertion were
|
It was not a genuine native route. Its claimed app-build success was also not
|
||||||
deleted rather than re-pinned; the exhausted Cistern/Orgiz guards and their
|
observed. Those claims are superseded, not used as current acceptance.
|
||||||
instructions were rewritten.
|
|
||||||
|
|
||||||
Evidence uses genuine paths only: original Ecaz/Guild/Fremen (or Station)
|
The replacement preserves original forces, wallets, cards, alliances and
|
||||||
Advanced lobbies, real stage/phase controls, paid shipments, Collection
|
phases. Original unplayed desert-card order and the actual token lottery are
|
||||||
inspection and reveal, next-turn free entry, a real worm Nexus alliance and the
|
the only staging. Real setup, token reveal, free entry, worm alliance, Ecaz
|
||||||
engine’s own allocation protocol. Conserved unplayed Spice Card order, printed
|
departure and fresh paid Ecaz/Guild shipments produce the original shared
|
||||||
supply tokens and counter positions are the only staging. Cases cover equal,
|
allocation. Rock terrain cannot create that lot. The third-party Collector
|
||||||
propose and zero splits, first-eligible recipient, two owners with a single
|
fixture now uses a definite Emperor shipment to an actual empty non-storm
|
||||||
available spice, lone-advisor exclusion, JSON normalization and deterministic
|
deposit rather than catching arbitrary failures or selecting an Orgiz owner.
|
||||||
re-settlement.
|
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/
|
Current final three-file union27/27; types/lint and app build pass. Actual
|
||||||
lint and app build pass with existing module-registration, chunk-size and
|
390×844 GameTable proof: original Funeral Plain lot4, proposal4/0 leaves
|
||||||
route-classification warnings. Contested Cistern/Orgiz ownership, Ecaz
|
Ecaz7/Guild11 unchanged; Guild acceptance produces Ecaz10/Guild12 and one
|
||||||
shared-lot theft ordering variants, complete modules and deployed acceptance
|
Orgiz transfer, no remaining decision and enabled Ready. No horizontal
|
||||||
remain open.
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
| 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 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) |
|
| 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) |
|
| 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) |
|
| 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) |
|
| 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) |
|
||||||
|
|||||||
@@ -131,9 +131,9 @@ function transferOrgiz(
|
|||||||
receipts: DiscoveryBaseCollectionReceipt[],
|
receipts: DiscoveryBaseCollectionReceipt[],
|
||||||
effects: DiscoveryCollectionQuote['effects'],
|
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)!;
|
const collectorReceipt = receipts.find(entry => entry.player === collector.id)!;
|
||||||
if (collectorReceipt.balance === 0) return;
|
if (collectorReceipt.balance === 0) return false;
|
||||||
requireCollection(
|
requireCollection(
|
||||||
Number.isSafeInteger(ownerReceipt.balance + 1),
|
Number.isSafeInteger(ownerReceipt.balance + 1),
|
||||||
'Orgiz theft would invalidate a player spice balance.',
|
'Orgiz theft would invalidate a player spice balance.',
|
||||||
@@ -144,6 +144,7 @@ function transferOrgiz(
|
|||||||
kind: 'orgiz', player: ownerReceipt.player, from: collector.id,
|
kind: 'orgiz', player: ownerReceipt.player, from: collector.id,
|
||||||
location: key, amount: 1, source: 'player',
|
location: key, amount: 1, source: 'player',
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -151,10 +152,10 @@ function transferOrgiz(
|
|||||||
* over the canonical ordinary quote. Ordinary `collected` and `desert` remain
|
* over the canonical ordinary quote. Ordinary `collected` and `desert` remain
|
||||||
* factual: Cistern is bank income; Orgiz transfers available player spice.
|
* factual: Cistern is bank income; Orgiz transfers available player spice.
|
||||||
*
|
*
|
||||||
* Owners resolve in original storm order. Advanced uses territory Collection;
|
* Owners resolve in original storm order. Advanced chooses one actual foreign
|
||||||
* Basic retains the provisional positive-deposit trigger. Ambiguous collectors
|
* collector per territory in storm order; Basic retains the provisional
|
||||||
* and shared lots still withhold only their uncertain effect. Empty purses
|
* positive-deposit trigger. Ambiguous collected sectors withhold the territory
|
||||||
* cannot create debt or block ordinary Collection.
|
* effect. Shared lots settle after bilateral allocation, not in this quote.
|
||||||
*/
|
*/
|
||||||
export function quoteDiscoveryCollection(
|
export function quoteDiscoveryCollection(
|
||||||
context: DiscoveryCollectionContext,
|
context: DiscoveryCollectionContext,
|
||||||
@@ -271,7 +272,10 @@ export function quoteDiscoveryCollection(
|
|||||||
if (orgiz.length) {
|
if (orgiz.length) {
|
||||||
const ownerReceipts = orgiz.map(owner => receipts.find(entry => entry.player === owner.id)!);
|
const ownerReceipts = orgiz.map(owner => receipts.find(entry => entry.player === owner.id)!);
|
||||||
const transfers = context.advanced
|
const transfers = context.advanced
|
||||||
? new Map<string, { location: string; collector: DiscoveryCollectionSeat | undefined }>()
|
? new Map<string, {
|
||||||
|
collectors: { location: string; collector: DiscoveryCollectionSeat }[];
|
||||||
|
ambiguous: boolean;
|
||||||
|
}>()
|
||||||
: undefined;
|
: undefined;
|
||||||
for (const key of sourceKeys) {
|
for (const key of sourceKeys) {
|
||||||
if (context.spice[key] <= collection.spice[key]) continue;
|
if (context.spice[key] <= collection.spice[key]) continue;
|
||||||
@@ -287,15 +291,25 @@ export function quoteDiscoveryCollection(
|
|||||||
transferOrgiz(key, collector, ownerReceipt, receipts, effects);
|
transferOrgiz(key, collector, ownerReceipt, receipts, effects);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const pending = transfers.get(territory);
|
let pending = transfers.get(territory);
|
||||||
if (!pending) transfers.set(territory, { location: key, collector });
|
if (!pending) {
|
||||||
else if (pending.collector?.id !== collector?.id)
|
pending = { collectors: [], ambiguous: false };
|
||||||
pending.collector = undefined;
|
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)
|
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)
|
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 };
|
return { receipts, effects };
|
||||||
|
|||||||
+4
-3
@@ -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.',
|
'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.',
|
'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.',
|
'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.',
|
'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.',
|
'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.',
|
'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',
|
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'],
|
evidence: ['docs/DISCOVERY_COMPONENTS.md', 'docs/DISCOVERY_PROTOTYPE.md'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area: 'Verification', status: 'Partial',
|
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',
|
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-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'],
|
'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'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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']) {
|
for (const second of ['a', 'g']) {
|
||||||
const game = fixture(true);
|
const game = fixture(true);
|
||||||
reveal(game, ORGIZ_PROCESSING_STATION);
|
reveal(game, ORGIZ_PROCESSING_STATION);
|
||||||
game.players[0].forces = { [`${ORGIZ_PROCESSING_STATION}:0`]: 1 };
|
game.players[0].forces = { [`${ORGIZ_PROCESSING_STATION}:0`]: 1 };
|
||||||
game.players[1].forces = { 'hagga_basin:12': 1, 'red_chasm:7': 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.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 };
|
game.spice = { 'hagga_basin:12': 3, 'hagga_basin:13': 2, 'red_chasm:7': 2 };
|
||||||
const { ordinary, discovery } = composed(game);
|
const { ordinary, discovery } = composed(game);
|
||||||
|
const payer = second === 'a' ? 'h' : 'g';
|
||||||
assert.deepEqual(discovery.effects, [{
|
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',
|
kind: 'orgiz', player: 'a', from: 'h', location: 'red_chasm:7',
|
||||||
amount: 1, source: 'player',
|
amount: 1, source: 'player',
|
||||||
}]);
|
}]);
|
||||||
assert.equal(balance(discovery, 'a'), ordinary.receipts[0].balance + 1);
|
assert.equal(balance(discovery, 'a'), ordinary.receipts[0].balance + 2);
|
||||||
assert.equal(balance(discovery, 'h'), ordinary.receipts[1].balance - 1);
|
assert.equal(balance(discovery, 'h'), ordinary.receipts.find(r => r.player === 'h')!.balance - (payer === 'h' ? 2 : 1));
|
||||||
assert.equal(balance(discovery, 'g'), ordinary.receipts[2].balance);
|
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);
|
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', () => {
|
void test('Advanced does not select a payer from an uncontested sector when another collected sector is contested', () => {
|
||||||
const game = fixture(true);
|
const game = fixture(true);
|
||||||
reveal(game, ORGIZ_PROCESSING_STATION);
|
reveal(game, ORGIZ_PROCESSING_STATION);
|
||||||
|
|||||||
@@ -1,68 +1,53 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { normalizeAutomaticGame, viewGame, type Game } from '../game/engine';
|
import {applyAction,type Game} from '../game/engine';
|
||||||
import { ORGIZ_PROCESSING_STATION } from '../game/discovery-collection';
|
import {ORGIZ_PROCESSING_STATION} from '../game/discovery-collection';
|
||||||
import { prepareSharedOrgizCollection, settleSharedOrgiz } from './fixture-discovery-shared-orgiz';
|
import {prepareSharedOrgizCollection,settleSharedOrgiz} from './fixture-discovery-shared-orgiz';
|
||||||
|
|
||||||
const wallets = (game: Game) => Object.fromEntries(game.players.map(player => [player.id, player.spice]));
|
const wallets=(game:Game)=>Object.fromEntries(game.players.map(p=>[p.id,p.spice]));
|
||||||
const transfers = (game: Game) =>
|
const transfers=(game:Game)=>game.log.filter(entry=>entry.automatic?.name==='Orgiz Processing Station');
|
||||||
game.log.filter(entry => entry.automatic?.name === 'Orgiz Processing Station');
|
|
||||||
|
|
||||||
for (const scenario of [
|
for(const split of ['equal','all-ecaz','all-guild'] as const) {
|
||||||
{ name: 'an equal split charges the first actual recipient', choice: { kind: 'equal' as const }, payer: 'e' },
|
void test(`Advanced native shared Collection: ${split} respects the actual foreign recipient and sole Orgiz owner`,()=>{
|
||||||
{ name: 'a full Ecaz share charges Ecaz', choice: { kind: 'propose' as const, ecazShare: 4 }, payer: 'e' },
|
const position=prepareSharedOrgizCollection();
|
||||||
{ name: 'a zero Ecaz share charges the ally instead', choice: { kind: 'propose' as const, ecazShare: 0 }, payer: 'g' },
|
const original=structuredClone(position.game);
|
||||||
]) {
|
const before=wallets(original);
|
||||||
void test(`Advanced shared Collection: ${scenario.name}, never an old wallet or an unavailable share`, () => {
|
const choice=split==='equal'?{kind:'equal' as const}:
|
||||||
const position = prepareSharedOrgizCollection();
|
{kind:'propose' as const,ecazShare:split==='all-ecaz'?position.lot.amount:0};
|
||||||
const lot = position.ordinary.shared[0];
|
const settled=settleSharedOrgiz(position,choice);
|
||||||
assert.equal(lot.territory, 'hagga_basin');
|
const receipt=settled.receipt;
|
||||||
assert.equal(lot.ecaz, position.ecaz);
|
const expected={...before};
|
||||||
assert.equal(lot.ally, position.guild);
|
expected[position.ecaz]+=receipt.ecazAmount;
|
||||||
const base = structuredClone(position.game);
|
expected[position.guild]+=receipt.allyAmount;
|
||||||
const before = wallets(base);
|
const tax=receipt.ecazAmount>0?1:0;
|
||||||
const settled = settleSharedOrgiz(position, scenario.choice);
|
expected[position.ecaz]-=tax;
|
||||||
assert.deepEqual(position.game, base, 'Settling must not mutate the prepared position.');
|
expected[position.owner]+=tax;
|
||||||
const receipt = settled.receipts.at(-1)!;
|
assert.deepEqual(wallets(settled.game),expected);
|
||||||
assert.equal(receipt.territory, lot.territory);
|
assert.equal(receipt.ecazAmount+receipt.allyAmount,position.lot.amount);
|
||||||
assert.equal(receipt.ecazAmount + receipt.allyAmount, lot.amount);
|
assert.equal(transfers(settled.game).length,tax);
|
||||||
assert.ok(receipt[scenario.payer === 'e' ? 'ecazAmount' : 'allyAmount'] > 0);
|
assert.equal(settled.game.players.find(p=>p.id===position.owner)!
|
||||||
const expected = { ...before };
|
.forces[`${ORGIZ_PROCESSING_STATION}:0`],2);
|
||||||
expected[receipt.ecaz] += receipt.ecazAmount;
|
assert.deepEqual(position.game,original);
|
||||||
expected[receipt.ally] += receipt.allyAmount;
|
const committed=structuredClone(settled.game);
|
||||||
expected[position.fremen] += 1;
|
const last=settled.steps.at(-1)!;
|
||||||
expected[scenario.payer] -= 1;
|
assert.throws(()=>applyAction(settled.game,last.actor,last.action));
|
||||||
assert.deepEqual(wallets(settled.game), expected);
|
assert.deepEqual(settled.game,committed,'A stale allocation cannot pay the same territory twice.');
|
||||||
// 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.');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void test('re-settling the same prepaid position is deterministic and never accumulates', () => {
|
void test('Native shared Orgiz waits for ally acceptance and rejects the wrong actor without changing wallets',()=>{
|
||||||
const position = prepareSharedOrgizCollection();
|
const position=prepareSharedOrgizCollection();
|
||||||
const first = settleSharedOrgiz(position, { kind: 'equal' });
|
const before=structuredClone(position.game);
|
||||||
const second = settleSharedOrgiz(position, { kind: 'equal' });
|
const event=before.ecazCollection!.event;
|
||||||
assert.equal(first.steps.length, second.steps.length);
|
const action={type:'decision' as const,event,allocation:{kind:'propose' as const,ecazShare:position.lot.amount}};
|
||||||
assert.deepEqual(wallets(second.game), wallets(first.game));
|
assert.throws(()=>applyAction(position.game,position.guild,action));
|
||||||
assert.deepEqual(transfers(second.game).length, transfers(first.game).length);
|
assert.deepEqual(position.game,before);
|
||||||
assert.deepEqual(wallets(position.game), wallets(position.before));
|
const proposed=applyAction(position.game,position.ecaz,action);
|
||||||
assert.deepEqual(normalizeAutomaticGame(structuredClone(second.game)), second.game);
|
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);
|
||||||
});
|
});
|
||||||
@@ -16,36 +16,23 @@ export function createOccupiedDiscoveryCollectionFixture(
|
|||||||
location:typeof CISTERN|typeof ORGIZ_PROCESSING_STATION,
|
location:typeof CISTERN|typeof ORGIZ_PROCESSING_STATION,
|
||||||
advanced=false,
|
advanced=false,
|
||||||
) {
|
) {
|
||||||
const fixture=createDiscoveryOccupantsFixture({location,advanced});
|
const fixture=createDiscoveryOccupantsFixture({location,advanced,adjacentBlow:location===ORGIZ_PROCESSING_STATION});
|
||||||
const actions:TestingStationContestStep[]=[...fixture.actions];
|
const actions:TestingStationContestStep[]=[...fixture.actions];
|
||||||
let game=fixture.game;
|
let game=fixture.game;
|
||||||
let collector:string|null=null,deposit:string|null=null;
|
let collector:string|null=null,deposit:string|null=null;
|
||||||
if(location===ORGIZ_PROCESSING_STATION) {
|
if(location===ORGIZ_PROCESSING_STATION) {
|
||||||
game=advanceTestingStationContest(game,g=>g.phase===5&&g.turn<=4&&g.active!==null&&
|
game=advanceTestingStationContest(game,g=>g.turn===2&&g.phase===5&&
|
||||||
(g.movementRemaining?.length??0)===1&&clean(g)&&
|
g.active==='emperor'&&clean(g),actions);
|
||||||
Object.entries(g.spice).some(([key,amount])=>amount>0&&
|
const candidate=Object.entries(game.spice).find(([key,amount])=>amount>=2&&
|
||||||
key!==`${ORGIZ_PROCESSING_STATION}:0`),actions);
|
splitLocation(key).sector!==game.storm&&game.players.every(p=>(p.forces[key]??0)===0));
|
||||||
const shipActor=game.active;
|
assert.ok(candidate,'An original printed blow must supply an empty non-storm deposit.');
|
||||||
assert.ok(shipActor,'A real movement window must have an active seat.');
|
deposit=candidate[0];
|
||||||
const candidates=Object.entries(game.spice)
|
collector='emperor';
|
||||||
.filter(([key,amount])=>amount>0&&key!==`${ORGIZ_PROCESSING_STATION}:0`)
|
const {territory,sector}=splitLocation(deposit);
|
||||||
.sort((left,right)=>right[1]-left[1]);
|
const shipment:Action={type:'ship',territory,sector,amount:2,elite:0,allyPayment:0};
|
||||||
assert.ok(candidates.length,'A real blow must supply a positive desert deposit.');
|
game=applyAction(game,collector,shipment);
|
||||||
let shipped:Game|null=null,shippedDeposit:string|null=null;
|
actions.push({actor:collector,action:shipment});
|
||||||
for(const [key] of candidates) {
|
game=advanceTestingStationContest(game,clean,actions);
|
||||||
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;
|
|
||||||
assert.equal(game.players.find(p=>p.id===collector)!.forces[deposit],2);
|
assert.equal(game.players.find(p=>p.id===collector)!.forces[deposit],2);
|
||||||
}
|
}
|
||||||
game=advanceTestingStationContest(game,g=>g.phase===5&&g.active!==null&&
|
game=advanceTestingStationContest(game,g=>g.phase===5&&g.active!==null&&
|
||||||
|
|||||||
@@ -1,103 +1,94 @@
|
|||||||
import assert from 'node:assert/strict';
|
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 {
|
import {
|
||||||
applyAction, createGame, initializeDiscoveryGameForAudit, joinGame, newPlayer,
|
createDiscoveryOccupantsFixture,advanceTestingStationContest,nextTestingStationContestStep,
|
||||||
normalizeAutomaticGame, viewGame, type Action, type Game,
|
type TestingStationContestStep,
|
||||||
} from '../game/engine';
|
} from './fixture-testing-station-contest';
|
||||||
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';
|
|
||||||
|
|
||||||
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
|
/** Preserve original armies/cards/alliances. Only original unplayed Spice Card
|
||||||
* unplayed Spice Card order, the printed supply token, reserve-counter positions
|
* order and the actual token lottery are staged by the shared native prefix. */
|
||||||
* and board spice already produced by real blows are controlled; every phase,
|
|
||||||
* reveal, payment, allocation, transfer and Collection is an original action. */
|
|
||||||
export function prepareSharedOrgizCollection() {
|
export function prepareSharedOrgizCollection() {
|
||||||
let game = createGame('SHAREDORGIZ', newPlayer('e', 'Ecaz', 'ecaz'), true, ['ecaz']);
|
const prefix=createDiscoveryOccupantsFixture({location:ORGIZ_PROCESSING_STATION,advanced:true,secondAmount:2});
|
||||||
joinGame(game, newPlayer('g', 'Guild', 'guild'));
|
const actions:TestingStationContestStep[]=[...prefix.actions];
|
||||||
joinGame(game, newPlayer('f', 'Fremen', 'fremen'));
|
let game=advanceTestingStationContest(prefix.game,g=>g.turn===2&&g.phase===5&&g.active==='ecaz'&&clean(g),actions);
|
||||||
for (const player of game.players)
|
const leave:Action={type:'move',from:`${ORGIZ_PROCESSING_STATION}:0`,
|
||||||
if (!player.ready) game = applyAction(game, player.id, { type: 'ready' });
|
territory:prefix.parent,sector:prefix.parentSector,amount:2,elite:0};
|
||||||
game.discoveryEnabled = true;
|
actions.push({actor:'ecaz',action:leave});
|
||||||
const initial = structuredClone(game);
|
game=applyAction(game,'ecaz',leave);
|
||||||
game = initializeDiscoveryGameForAudit(game);
|
game=advanceTestingStationContest(game,g=>g.turn===2&&g.phase===8&&clean(g),actions);
|
||||||
game = advanceTestingStationContest(game, state => state.status === 'playing');
|
// Two unused printed blows with different sectors guarantee that at least
|
||||||
const ecaz = 'e', guild = 'g', fremen = 'f';
|
// one new deposit is outside the next real Storm. Never edit a storm source.
|
||||||
for (const [position, name] of ['hagga_basin', 'cielago_south', 'red_chasm', 'funeral_plain'].entries()) {
|
let firstSector:number|null=null;
|
||||||
const index = game.spiceDeck.findIndex((card, i) => i >= position &&
|
const blowKeys:string[]=[];
|
||||||
'territory' in card && card.territory === name && !card.discovery);
|
for(let position=0;position<2;position++) {
|
||||||
assert.ok(index >= position);
|
const index=game.spiceDeck.findIndex((card,i)=>i>=position&&'territory' in card&&
|
||||||
game.spiceDeck.splice(position, 0, game.spiceDeck.splice(index, 1)[0]);
|
!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);
|
game=advanceTestingStationContest(game,g=>g.turn===3&&g.phase===5&&clean(g),actions);
|
||||||
nativeTypedClearBoard(game);
|
const target=Object.entries(game.spice).find(([key,amount])=>blowKeys.includes(key)&&amount>=2&&
|
||||||
const token = putDiscovery(game, ORGIZ_PROCESSING_STATION);
|
splitLocation(key).sector!==game.storm&&game.players.every(p=>(p.forces[key]??0)===0));
|
||||||
const source = `${token.territory}:${token.sector}`;
|
assert.ok(target,'An actual original blow must supply an empty non-storm shared collection deposit.');
|
||||||
assert.notEqual(token.sector, game.storm);
|
const destination=splitLocation(target[0]);
|
||||||
nativeTypedPlace(game, fremen, source, 2);
|
const shipped=new Set<string>();
|
||||||
game = nativeTypedPhase(game, 7);
|
let beforeCollection:Game|null=null;
|
||||||
assert.ok(viewGame(game, fremen).discoveries!.canInspect.includes(token.id));
|
for(let step=0;step<500&&game.turn===3&&!(game.phase===7&&game.decision?.kind==='ecazSpice');step++) {
|
||||||
game = applyAction(game, fremen, { type: 'discovery', token: token.id, reveal: false });
|
if(game.phase===5&&clean(game)&&game.movementRemaining?.length===1)
|
||||||
assert.ok(viewGame(game, fremen).discoveries!.canReveal.includes(token.id));
|
beforeCollection=game;
|
||||||
game = applyAction(game, fremen, { type: 'discovery', token: token.id, reveal: true });
|
let next:TestingStationContestStep|null;
|
||||||
game = nativeTypedPhase(game, 8);
|
if(game.turn===3&&game.phase===5&&clean(game)&&game.active&&
|
||||||
game = nativeTypedPhase(game, 5, 2);
|
['ecaz','guild'].includes(game.active)&&!shipped.has(game.active)) {
|
||||||
nativeTypedClearBoard(game);
|
const actor=game.active;
|
||||||
// Ecaz and its Guild ally hold the same real desert territory, so the original
|
next={actor,action:{type:'ship',territory:destination.territory,sector:destination.sector,
|
||||||
// quote creates the shared lot whose allocation decides the actual recipient.
|
amount:1,elite:0,allyPayment:0}};
|
||||||
const haggaKeys = territory('hagga_basin').sectors.filter(sector => sector !== game.storm)
|
shipped.add(actor);
|
||||||
.slice(0, 2).map(sector => `hagga_basin:${sector}`);
|
} else next=nextTestingStationContestStep(game);
|
||||||
assert.equal(haggaKeys.length, 2);
|
assert.ok(next,`Original shared Collection has no continuation: ${JSON.stringify({
|
||||||
const untouchedKey = 'the_great_flat:15';
|
status:game.status,turn:game.turn,phase:game.phase,decision:game.decision,
|
||||||
nativeTypedPlace(game, fremen, `${ORGIZ_PROCESSING_STATION}:0`, 1);
|
active:game.active,shipped:[...shipped],target:target[0],
|
||||||
nativeTypedPlace(game, ecaz, haggaKeys[0], 1);
|
})}`);
|
||||||
nativeTypedPlace(game, guild, haggaKeys[1], 1);
|
actions.push(next);
|
||||||
// Conserved precondition, as in the original Collection tests: the Ecaz/Guild
|
game=applyAction(game,next.actor,next.action);
|
||||||
// alliance exists before this Collection. The governed behavior under audit is
|
}
|
||||||
// the real allocation-then-transfer sequence below, not the pact's own rules.
|
assert.equal(game.decision?.kind,'ecazSpice',JSON.stringify({
|
||||||
game.players.find(player => player.id === ecaz)!.ally = guild;
|
turn:game.turn,phase:game.phase,shipped:[...shipped],target:target[0],
|
||||||
game.players.find(player => player.id === guild)!.ally = ecaz;
|
spice:beforeCollection?.spice[target[0]],storm:beforeCollection?.storm,
|
||||||
const boardSpice = Object.values(game.spice).reduce((sum, amount) => sum + amount, 0);
|
seats:beforeCollection?.players.map(p=>({id:p.id,ally:p.ally,forces:p.forces[target[0]]})),
|
||||||
assert.ok(boardSpice >= 6);
|
last:actions.slice(-12),
|
||||||
game.spice = { [haggaKeys[0]]: 4, [haggaKeys[1]]: 2, [untouchedKey]: boardSpice - 6 };
|
}));
|
||||||
const ordinary = quoteSpiceCollection(game);
|
assert.equal(game.players.find(p=>p.id==='ecaz')!.forces[`${ORGIZ_PROCESSING_STATION}:0`],undefined);
|
||||||
assert.equal(ordinary.shared.length, 1);
|
assert.equal(game.players.find(p=>p.id==='guild')!.forces[`${ORGIZ_PROCESSING_STATION}:0`],2);
|
||||||
assert.deepEqual({ ...ordinary.shared[0], amount: 0 },
|
const pending=game.ecazCollection!.allocation!;
|
||||||
{ territory: 'hagga_basin', ecaz, ally: guild, amount: 0 });
|
const lot=pending.lots[pending.index];
|
||||||
assert.ok(ordinary.shared[0].amount > 0, 'The real shared lot must hold collected desert spice.');
|
assert.equal(lot.territory,destination.territory);
|
||||||
assert.deepEqual(quoteDiscoveryCollection(game, ordinary).effects, []);
|
assert.ok(lot.amount>=2);
|
||||||
const before = structuredClone(game);
|
return {...prefix,game,actions,lot,key:target[0],ecaz:'ecaz',guild:'guild',owner:'guild'};
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Settle the real Collection, then answer each original Ecaz allocation prompt. */
|
|
||||||
export function settleSharedOrgiz(
|
export function settleSharedOrgiz(
|
||||||
position: ReturnType<typeof prepareSharedOrgizCollection>,
|
position:ReturnType<typeof prepareSharedOrgizCollection>,
|
||||||
choice: { kind: 'propose'; ecazShare: number } | { kind: 'equal' } = { kind: 'equal' },
|
choice:{kind:'propose';ecazShare:number}|{kind:'equal'}={kind:'equal'},
|
||||||
) {
|
) {
|
||||||
const steps: SharedOrgizStep[] = [];
|
let game=position.game;
|
||||||
let game = applyAction(position.game, position.fremen, { type: 'endMovement' });
|
const event=game.ecazCollection!.event;
|
||||||
steps.push({ actor: position.fremen, action: { type: 'endMovement' } });
|
const steps:TestingStationContestStep[]=[];
|
||||||
assert.equal(game.phase, 7);
|
while(game.decision?.kind==='ecazSpice'&&steps.length<20) {
|
||||||
const event = game.ecazCollection?.event ?? null;
|
const actor=game.decision.player;
|
||||||
assert.ok(event, 'The real Collection must own its allocation event.');
|
const allocation=actor===position.ecaz?choice:{kind:'accept' as const};
|
||||||
while (game.decision?.kind === 'ecazSpice' && steps.length < 20) {
|
const action:Action={type:'decision',event,allocation};
|
||||||
const actor = game.decision.player;
|
steps.push({actor,action});
|
||||||
const allocation = actor === position.ecaz ? choice : { kind: 'accept' as const };
|
game=applyAction(game,actor,action);
|
||||||
const action = { type: 'decision', event: game.ecazCollection!.event, allocation } as Action;
|
|
||||||
game = applyAction(game, game.decision.player, action);
|
|
||||||
steps.push({ actor: position.ecaz, action });
|
|
||||||
}
|
}
|
||||||
assert.equal(game.decision, null, 'The original shared allocation must settle without inventing another choice.');
|
assert.equal(game.decision,null);
|
||||||
const stable = JSON.parse(JSON.stringify(game)) as Game;
|
return {game,steps,event,receipt:game.ecazCollection!.settled.at(-1)!};
|
||||||
assert.deepEqual(normalizeAutomaticGame(JSON.parse(JSON.stringify(stable))), stable);
|
|
||||||
return { game, steps, event, receipts: game.ecazCollection?.settled ?? [] };
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user