Renew impossible Salusa Ghola inspection answers
This commit is contained in:
1 parent
1bb7a99836
commit
53b37adf9f
15 files changed
+307
-34
No files matched your search
@@ -20,6 +20,28 @@ open mode gates.
|
||||
Keep identified Basic-function gaps ahead of Advanced and wider interactions.
|
||||
Prototype labels and final acceptance gates remain.
|
||||
|
||||
**Salusa Ghola inspection renewal — 27 October. Prototyped / Partial.**
|
||||
Before either full plan seals, an actual Salusa-crossing Ghola return now keeps
|
||||
a feasible inspection answer or reopens an impossible original question. The
|
||||
owner must answer before plans continue. The existing native-first Cunning
|
||||
renewal protocol is reused without a new field, cost or cancellation. Full
|
||||
seals and active Truthtrance battle promises remain guarded. The single-answer
|
||||
update follows the authorized PDF's identified unofficial ruling; the paired
|
||||
priority remains provisional. [Contract](HOMEWORLD_BENEFITS_RULES.md#salusa-ghola-and-renewed-inspection-answers--27-october-2026).
|
||||
|
||||
**Fourteen files115/115**, types/lint and two native runtime programmes pass.
|
||||
Original public Advanced E/A/G base33/Homeworlds select an actually offered
|
||||
Basin first battle, earn its paid star casualty, then ask and answer actual
|
||||
Atreides Prescience in Arrakeen. Two prefixes total104 actions;11 further
|
||||
actions distinguish impossible odd1 renewal from feasible2 retention and
|
||||
finish both native battles. Nine GameTable/two BattlePreparation SSR renders
|
||||
and one shared answer-adapter packet connect guidance and the legal answer.
|
||||
All four minimal profiles answer legally. Amount/elite/answer/plan inputs are
|
||||
not operated in a browser. An older explicitly staged Residual/Cunning fixture
|
||||
now removes its obsolete Charity frame at its controlled Battle boundary.
|
||||
No new played Cunning/Ghola pair, full modes, mobile, deployed or final
|
||||
assurance is certified. No build was repeated for this focused rule change.
|
||||
|
||||
**Salusa Ghola before battle commitments — 27 October. Prototyped / Partial.**
|
||||
An otherwise legal Sardaukar Ghola return can raise Salusa from one to two
|
||||
during the Emperor's uncommitted battle. That same battle uses current High
|
||||
|
||||
@@ -16,7 +16,7 @@ Ixian winners may exchange surviving Suboids for Cyborgs lost at that same Homew
|
||||
|
||||
## Explicit unfinished interactions
|
||||
|
||||
The [replacement source audit](HOMEWORLD_REPLACEMENT_RULES.md) does not resolve the Advanced Emperor’s returned-army allocation between Kaitain and Salusa. That Face Dance return is gated with the same server, AI and interface reason; declining remains available. The Salusa Ghola crossing remains gated after battle commitments; the [commitment-free window](HOMEWORLD_BENEFITS_RULES.md#salusa-ghola-before-battle-commitments--27-october-2026) now uses actual current population. These restrictions are development boundaries, not official prohibitions. Other Homeworld face effects, occupation rights and full interaction coverage are still required.
|
||||
The [replacement source audit](HOMEWORLD_REPLACEMENT_RULES.md) does not resolve the Advanced Emperor’s returned-army allocation between Kaitain and Salusa. That Face Dance return is gated with the same server, AI and interface reason; declining remains available. The Salusa Ghola crossing remains gated after full seals or during active Truthtrance battle promises; [inspection renewal](HOMEWORLD_BENEFITS_RULES.md#salusa-ghola-and-renewed-inspection-answers--27-october-2026) before full seals uses actual current population and the original question. These restrictions are development boundaries, not official prohibitions. Other Homeworld face effects, occupation rights and full interaction coverage are still required.
|
||||
|
||||
## Evidence
|
||||
|
||||
|
||||
@@ -2,8 +2,64 @@
|
||||
|
||||
Source audit, 9 September 2026. This separates publisher wording, rule composition, and remaining decisions. It does not enable Homeworld games or replace the outstanding Ambassador/Duke revival rulings.
|
||||
|
||||
## Salusa Ghola and renewed inspection answers — 27 October 2026
|
||||
|
||||
The earlier commitment-free window below now includes completed inspection
|
||||
answers before either full plan is sealed. Commit the actual Ghola return and
|
||||
current Salusa High benefit first. Preserve a still-feasible native answer;
|
||||
if it becomes impossible, its owner must answer the same field again before
|
||||
plans can be submitted. An odd dial using only now-free double-strength stars
|
||||
is such a case. The old value is not made into a sealed plan, refunded or used
|
||||
as permission to weaken a star. Normal modal interactions still finish first.
|
||||
|
||||
Authority for the update is the supplied PDF physical41 / printed39: its
|
||||
outlined impossible-Prescience Q&A requires an updated valid answer before
|
||||
reveal. Physical3 / printed1 identifies outlined rulings as unofficial; their
|
||||
authority here is the user's adoption of this PDF, not a newly found GF9 FAQ.
|
||||
Immediate Ghola remains physical39 / printed37, with no separate exception.
|
||||
|
||||
Reuse the existing [native-first inspection renewal](NEXUS_ATREIDES_RULES.md#residual-poison-and-two-inspected-commitments).
|
||||
For Cunning, keep a feasible native answer first, then test the extra answer
|
||||
jointly. If the native answer is impossible, renew its original field before
|
||||
the extra original field. Retain observation history and the spent Nexus
|
||||
record; do not charge again, offer another cancellation or choose new fields.
|
||||
This multi-answer priority remains a provisional online composition. Full
|
||||
sealed plans and active Truthtrance battle promises retain their explicit
|
||||
preview guard; this slice does not define their repricing or release.
|
||||
|
||||
### Inspection-renewal evidence
|
||||
|
||||
Fourteen selected files pass115/115 with types/lint. The actual original public
|
||||
Advanced E/A/G base33/Homeworld source can select Basin from the real chooser's
|
||||
first-battle alternatives, earn a paid Sardaukar casualty and reach the later
|
||||
Arrakeen battle. Actual Atreides declarations, counter-response passes and
|
||||
Emperor answers create the initial dial1/2 commitments. No played hand, force,
|
||||
purse, clock, inspection answer or battle receipt is installed. Only unopened
|
||||
initial card/Spice positions and native scalar lotteries are selected.
|
||||
|
||||
The separate runtime uses two prefixes totaling104 actions and11 continuation
|
||||
actions. Actual Ghola returns once in each: odd1 requires the owned same-field
|
||||
answer2 before actual plans/calls; feasible2 stays fixed without another
|
||||
question. Nine GameTable and two BattlePreparation SSR renders plus one actual
|
||||
shared UI answer-adapter packet connect the current controls. Stateful input
|
||||
operation/browser hydration is not claimed. All four minimal profiles provide
|
||||
legal pending answers. Existing Cunning/Residual tests cover the inherited
|
||||
protocol; no new played Cunning/Ghola pair is certified. The older explicitly
|
||||
controlled resource fixture clears its stale Charity frame when staging Battle.
|
||||
|
||||
Private proof: `/home/quintine/.local/share/dune/checkpoints/20261027-salusa-ghola-inspection-proof-v2/`;
|
||||
runtime: `20261027-salusa-ghola-inspection-runtime/run-v1/` beside it. Frozen
|
||||
tree `1321c432fa0a204e5e8db3406be76b4608dac129409c9395fa77eac2bde1290a`
|
||||
contains3351 files. Final evidence overlays are limited to this document, the
|
||||
dashboard and implementation log. No build was repeated for this focused rule
|
||||
change; complete modes, native-Salusa battle, browser/mobile, deployed and final
|
||||
recovery/privacy/custody acceptance remain separate.
|
||||
|
||||
## Salusa Ghola before battle commitments — 27 October 2026
|
||||
|
||||
Historical first window: inspection restrictions described in this subsection
|
||||
are superseded by the renewed-answer follow-up above. Its proof remains dated.
|
||||
|
||||
An otherwise legal Ghola may return a dead Sardaukar to Salusa during the
|
||||
Emperor's battle while no plan is sealed and no inspection answer or active
|
||||
Truthtrance battle promise is committed. Returning the actual counter from
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Homeworld combat foundation and Salusa support
|
||||
|
||||
Current follow-up: [commitment-free Ghola](HOMEWORLD_BENEFITS_RULES.md#salusa-ghola-before-battle-commitments--27-october-2026)
|
||||
can raise Salusa during an ongoing battle. The older whole-battle restriction
|
||||
below is superseded only before commitments; later repricing remains guarded.
|
||||
Current follow-up: [Ghola inspection renewal](HOMEWORLD_BENEFITS_RULES.md#salusa-ghola-and-renewed-inspection-answers--27-october-2026)
|
||||
can raise Salusa before either full plan seals, retaining feasible answers or
|
||||
renewing impossible original fields. Full-seal/promise repricing remains guarded.
|
||||
|
||||
9 September 2026. This extends the native-reserve runtime. Homeworld invasion and live off-planet battle actions remain unavailable; no start gate is opened.
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ disclosure ability is explicitly lost under occupation.
|
||||
| `charity.ts::charityQuote`, `revival.ts::freeRevivalRate` | Shared low bonus | Preserve ordinary eligibility, bank funding, inflation, Recruits and resource caps. Changing the shared helper reaches engine/view quotes. |
|
||||
| `homeworld-benefits.ts::snapshotHomeworldRevival` | Tleilax physically low at Revival opening | Snapshot the verified low-penalty condition at the original phase-opening event. `tleilaxuHomeworldFreeIncomeBlocked` must continue reading that saved receipt, not current population or occupation. |
|
||||
| `homeworld-benefits.ts::homeworldSardaukarFreeSupport` | Advanced Salusa high | A native high capability. Occupied Salusa's loss of Sardaukar advantage needs its own rule composition, not a physical force conversion. |
|
||||
| `homeworld-benefits.ts::homeworldSardaukarGholaBlock` | Salusa population exactly one during a committed Emperor battle | Remaining support-transition preview guard; commitment-free Ghola now uses current population. If occupation changes effective support, compare the relevant before/after capability; do not remove the guard solely because occupation exists. |
|
||||
| `homeworld-benefits.ts::homeworldSardaukarGholaBlock` | Salusa population exactly one after a full seal or during an active battle promise | Remaining support-transition preview guard; pre-seal Ghola uses current population and renews impossible original inspections. If occupation changes effective support, compare the relevant before/after capability; do not remove the guard solely because occupation exists. |
|
||||
| `homeworld-mobility.ts::homeworldMovementForesightBlock` | Caladan low | Retained low penalty blocks a new Movement peek; it does not erase already acquired knowledge. |
|
||||
| `homeworld-mobility.ts::homeworldMobileStrongholdMovementBlock` | Ix low | Retained low penalty blocks stronghold relocation, including its special Karama route; ordinary troop movement remains separate. |
|
||||
| `homeworld-mobility.ts::homeworldNoFieldMovementBlock` | Richese low | Retained low penalty restricts existing marker movement. Shipment and reveal rules remain separate. |
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
# Dune implementation status
|
||||
|
||||
## 27 October 2026 — Salusa Ghola inspection renewal
|
||||
|
||||
An actual Salusa threshold return before either full plan seals now reuses the
|
||||
existing inspection reconciliation: feasible answers stay fixed, impossible
|
||||
original fields require an owned new answer, and Cunning retains its provisional
|
||||
native-first order and spent record. No repeated charge/cancellation or new
|
||||
field selection occurs. The supplied PDF's outlined impossible-answer ruling
|
||||
has user-authorized unofficial provenance. Full seals and active Truthtrance
|
||||
battle promises remain explicit preview boundaries.
|
||||
[Contract](HOMEWORLD_BENEFITS_RULES.md#salusa-ghola-and-renewed-inspection-answers--27-october-2026).
|
||||
|
||||
Fourteen-file115/115 and types/lint pass. Two original public Advanced E/A/G
|
||||
base33/Homeworld runtime programmes earn the actual first-battle star loss,
|
||||
then actually declare and answer Atreides dial Prescience in Arrakeen. Prefixes
|
||||
total104 actions;11 continuations compare odd1 renewal/answer2 with feasible2
|
||||
retention and finish actual plans/calls. Nine GameTable/two BattlePreparation
|
||||
SSR renders and one shared answer-adapter packet check the current control
|
||||
path. Four minimal AI profiles answer legally. Stateful inputs are not operated
|
||||
in a browser. An older explicitly controlled Residual/Cunning fixture removes
|
||||
its stale Charity frame at its staged Battle boundary. No new played
|
||||
Cunning/Ghola pair, complete modes, browser/mobile, deployed or final assurance
|
||||
is certified; no build was repeated for this focused rule change.
|
||||
|
||||
Private proof `20261027-salusa-ghola-inspection-proof-v2/` and runtime
|
||||
`20261027-salusa-ghola-inspection-runtime/run-v1/` use the existing private
|
||||
checkpoint directory. Frozen tree
|
||||
`1321c432fa0a204e5e8db3406be76b4608dac129409c9395fa77eac2bde1290a`
|
||||
contains3351 files; final prose overlays are limited to this log,
|
||||
`CURRENT_STATUS.md` and `HOMEWORLD_BENEFITS_RULES.md`.
|
||||
|
||||
## 27 October 2026 — Commitment-free Salusa Ghola
|
||||
|
||||
Remove the whole-battle threshold guard only before either sealed plan,
|
||||
|
||||
@@ -70,6 +70,10 @@ The unresolved cases were reported to the coordinator before implementation deci
|
||||
|
||||
## Residual Poison and two inspected commitments
|
||||
|
||||
The [Salusa Ghola follow-up](HOMEWORLD_BENEFITS_RULES.md#salusa-ghola-and-renewed-inspection-answers--27-october-2026)
|
||||
also reuses this native-first ordered renewal before either full plan seals.
|
||||
Its actual physical return occurs once; it does not buy another inspection.
|
||||
|
||||
**Selected provisional composition — 26 October 2026.** The 14 October
|
||||
choose-recommended authority supersedes the earlier blanket product refusal.
|
||||
The printed Residual Poison window remains available after two completed
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# Rule decision index
|
||||
|
||||
## Salusa Ghola inspection renewal — 27 October 2026
|
||||
|
||||
Before either full plan is sealed, an actual Salusa-crossing Ghola return may
|
||||
retain a feasible inspection or renew an impossible original field. The supplied
|
||||
PDF's outlined impossible-answer Q&A (physical41 / printed39) is an accepted
|
||||
unofficial ruling under user authority; physical3 / printed1 explains its
|
||||
marking. Reuse the existing provisional native-first Cunning order and original
|
||||
spent record, without new fields, charge or cancellation. Full seals and active
|
||||
Truthtrance battle promises remain guarded separately. This supersedes only
|
||||
the inspection restriction in the earlier window below.
|
||||
[Contract](HOMEWORLD_BENEFITS_RULES.md#salusa-ghola-and-renewed-inspection-answers--27-october-2026).
|
||||
|
||||
## Salusa Ghola commitment-free battle window — 27 October 2026
|
||||
|
||||
Remove the whole-battle population-crossing restriction before any sealed plan,
|
||||
|
||||
@@ -29077,6 +29077,7 @@ function playMarketGhola(g: Game, p: Player, action: Action) {
|
||||
function applyGholaEffect(g: Game, p: Player, action: Action, cardId?: string) {
|
||||
requireRule(!revivalPrevented(g, p.id),
|
||||
'Tleilaxu prevented this player’s revival, including Ghola, for the remaining Revival Phase.');
|
||||
const salusaWasHigh = homeworldSardaukarFreeSupport(g, p.id);
|
||||
if (action.leader === 'kwisatz') {
|
||||
requireRule(
|
||||
g.advanced && p.faction === 'atreides' && p.kwisatz?.dead,
|
||||
@@ -29142,6 +29143,9 @@ function applyGholaEffect(g: Game, p: Player, action: Action, cardId?: string) {
|
||||
techIncome(g, 'axlotl', p);
|
||||
collectRevivalIncome(g, p, 0, false, true);
|
||||
recordNativePrintedCardPlay(g, p.id, 'ghola');
|
||||
if (!salusaWasHigh && homeworldSardaukarFreeSupport(g, p.id) && g.battle &&
|
||||
[g.battle.attacker, g.battle.defender].includes(p.id))
|
||||
reconcileChangedBattleInspections(g, 'the Salusa population increase', true);
|
||||
}
|
||||
/** Search complete legal plans without learning or consulting the opponent's sealed choices. */
|
||||
function findLegalBattlePlan(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Game } from './engine';
|
||||
import { homeworldContext } from './homeworld-game';
|
||||
import { homeworldPopulations } from './homeworld-population';
|
||||
import { committedPlanElements } from './battle-inspections';
|
||||
|
||||
type HomeworldBenefitContext = Pick<
|
||||
Game,
|
||||
@@ -60,9 +59,10 @@ export function homeworldSardaukarFreeSupport(
|
||||
);
|
||||
}
|
||||
|
||||
/** Current population can change before battle commitments. The remaining
|
||||
* preview boundary protects sealed dials/support and binding answers from
|
||||
* repricing; it does not freeze population or forbid Ghola for a whole battle.
|
||||
/** Current population can change before sealed plans. The caller renews an
|
||||
* impossible inspection answer after the return. The remaining preview boundary
|
||||
* protects sealed dials/support and binding Truthtrance promises from repricing;
|
||||
* it does not freeze population or forbid Ghola for a whole battle.
|
||||
* Inspect submission presence, never an opponent's sealed plan contents. */
|
||||
export function homeworldSardaukarGholaBlock(
|
||||
context: HomeworldBenefitContext & Pick<Game, 'battle'>,
|
||||
@@ -88,11 +88,9 @@ export function homeworldSardaukarGholaBlock(
|
||||
if (!crossing) return null;
|
||||
const battle = context.battle;
|
||||
const committed = battle.revealed || Object.keys(battle.plans).length > 0 ||
|
||||
[battle.attacker, battle.defender].some(
|
||||
(id) => committedPlanElements(battle, id).length > 0,
|
||||
) || battle.truthPromises?.some((promise) => !promise.released);
|
||||
battle.truthPromises?.some((promise) => !promise.released);
|
||||
return committed
|
||||
? 'Ghola raising Salusa to high population after a battle plan or binding answer is not yet supported. Finish this battle first.'
|
||||
? 'Ghola raising Salusa to high population after a sealed plan or during a binding battle promise is not yet supported. Finish this battle first.'
|
||||
: null;
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -102,7 +102,7 @@ export const HELP = {
|
||||
'The table pauses before this power resolves. Other factions can use a legal Karama cancellation; everyone can allow the power.',
|
||||
ally: 'Set aside spice for an ally’s bids or shipments. You may reclaim unused funds, but a current winning bid must remain funded.',
|
||||
prescience:
|
||||
'Only the requested element becomes fixed. The opponent keeps the rest of their plan private until both combatants seal their plans.',
|
||||
'Only the requested element becomes fixed. Advanced Salusa Ghola may renew an impossible answer before either full plan is sealed; answer that same field again. Other plan elements stay private until reveal.',
|
||||
};
|
||||
|
||||
const phaseDetails = [
|
||||
@@ -5445,7 +5445,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
area: 'Implementation',
|
||||
status: 'Partial',
|
||||
detail:
|
||||
"Fresh Basic Southern multi-star returns have provisional per-star allocation with exact child arrivals, a fixed original grant and one deferred income. All 13 cards, saved typed custody, supported-deck setup, Emperor movement, native Arrakis shipment sources, revival destinations, low charity/free-revival benefits, Tleilax’s phase-start income penalty and high-Salusa free Sardaukar support are connected. Ghola can now raise Salusa during a commitment-free battle; sealed/inspected/promised timing remains guarded. World-to-world invasion, Guild transport from Arrakis and Guild native returns use exact source pools, funding and interception. Homeworld battles connect native bonuses, limited explosion casualties, native-only Traitors and Face Dancers, and Ixian substitution. Junction offers connect sponsored board and Homeworld routes, including own returns, foreign departures and their distinct payments. Caladan foresight, Wallach advisor counts, Ix stronghold movement and Richese token movement now respect native population. High Tupile blocks Worthless sales and Ambassador payouts, including after an interrupted Ghola revival. Kaitain paid discards share the closing Bidding opportunity with CHOAM; Ecaz poison income uses actual physical disposal and current native population. Mandatory winning-card disposal follows winner casualties. Supported low Kaitain and Junction payment receipts preserve the payer’s cost and use current native population; divergent allied rounding remains guarded. Giedi grants its once-per-phase bonus on actual positive desert receipts, including settled Ecaz shares. Low Grumman uses original public entry counts across Terror and competing-reaction checks. Richese High now connects actual paid reserve companions, one combined provisional invoice, original marker proof/responses, physical withdrawal and later reserve-capped reveal. Nexus Cunning retains that explicit paid group in its pair receipt, reserving paid counters before immediate materialization; original counter and spent-card outcomes remain. Ix High now returns physical Suboids for paid Cyborgs with provisional additional allowance and shortage handling, retaining typed free allocation, original invoice and free-event income. Southern and Tleilax preserve the exact eligible revival group through one optional placement. Provisional physical entry now opens native BG, Terror and Ambassador responses, retaining the original held income, independent counters and ordered overlap; a committed Sneak group has its own arrival source. Southern placement provisionally retains the ordinary storm and HMS entry route; Tleilax retains the Ixian-only direct off-planet restriction. Fresh occupation additionally connects bank and percentage receipts, completed Southern Collection, immediate sharing, Caladan inspection, displaced Ix pool, Richese card-only choice with native terms, real Giedi receiver choice and native protections. Fresh native CHOAM entries additionally connect original Tupile slots, actual normal-limit cleanup and restored low-query authority without resetting usage. Advanced retains sole-qualified ownership until last departure. Basic Tupile provisionally follows current sole control: observed departure, contest or native high population ends its slot, while a new activation waits for earlier normal-limit cleanup. Continuous turns do not stack slots or reset intelligence use. Exceptional Duke, concealed transport and full/public acceptance remain unfinished. Already purchased ordinary Karama now composes with immune Junction through explicit unused-price acknowledgment; original tariff/payees stay unchanged and only delivered transport consumes its grant.",
|
||||
"Fresh Basic Southern multi-star returns have provisional per-star allocation with exact child arrivals, a fixed original grant and one deferred income. All 13 cards, saved typed custody, supported-deck setup, Emperor movement, native Arrakis shipment sources, revival destinations, low charity/free-revival benefits, Tleilax’s phase-start income penalty and high-Salusa free Sardaukar support are connected. Ghola can raise Salusa before full seals, retaining feasible inspections or reopening impossible original fields; sealed-plan and binding-Truthtrance timing remain guarded. World-to-world invasion, Guild transport from Arrakis and Guild native returns use exact source pools, funding and interception. Homeworld battles connect native bonuses, limited explosion casualties, native-only Traitors and Face Dancers, and Ixian substitution. Junction offers connect sponsored board and Homeworld routes, including own returns, foreign departures and their distinct payments. Caladan foresight, Wallach advisor counts, Ix stronghold movement and Richese token movement now respect native population. High Tupile blocks Worthless sales and Ambassador payouts, including after an interrupted Ghola revival. Kaitain paid discards share the closing Bidding opportunity with CHOAM; Ecaz poison income uses actual physical disposal and current native population. Mandatory winning-card disposal follows winner casualties. Supported low Kaitain and Junction payment receipts preserve the payer’s cost and use current native population; divergent allied rounding remains guarded. Giedi grants its once-per-phase bonus on actual positive desert receipts, including settled Ecaz shares. Low Grumman uses original public entry counts across Terror and competing-reaction checks. Richese High now connects actual paid reserve companions, one combined provisional invoice, original marker proof/responses, physical withdrawal and later reserve-capped reveal. Nexus Cunning retains that explicit paid group in its pair receipt, reserving paid counters before immediate materialization; original counter and spent-card outcomes remain. Ix High now returns physical Suboids for paid Cyborgs with provisional additional allowance and shortage handling, retaining typed free allocation, original invoice and free-event income. Southern and Tleilax preserve the exact eligible revival group through one optional placement. Provisional physical entry now opens native BG, Terror and Ambassador responses, retaining the original held income, independent counters and ordered overlap; a committed Sneak group has its own arrival source. Southern placement provisionally retains the ordinary storm and HMS entry route; Tleilax retains the Ixian-only direct off-planet restriction. Fresh occupation additionally connects bank and percentage receipts, completed Southern Collection, immediate sharing, Caladan inspection, displaced Ix pool, Richese card-only choice with native terms, real Giedi receiver choice and native protections. Fresh native CHOAM entries additionally connect original Tupile slots, actual normal-limit cleanup and restored low-query authority without resetting usage. Advanced retains sole-qualified ownership until last departure. Basic Tupile provisionally follows current sole control: observed departure, contest or native high population ends its slot, while a new activation waits for earlier normal-limit cleanup. Continuous turns do not stack slots or reset intelligence use. Exceptional Duke, concealed transport and full/public acceptance remain unfinished. Already purchased ordinary Karama now composes with immune Junction through explicit unused-price acknowledgment; original tariff/payees stay unchanged and only delivered transport consumes its grant.",
|
||||
evidence: [
|
||||
'game/homeworld-module-profile.ts',
|
||||
'game/faction-module-profile.ts',
|
||||
@@ -5555,7 +5555,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
area: 'AI',
|
||||
status: 'Partial',
|
||||
detail:
|
||||
"All four profiles share legal Southern portion/destination choices and the remaining-reserves option. Shared public choices supply native shipment counters, Imperial transfers and current low revival allowances. All four profiles use Salusa’s per-type support legality and respect the remaining special-Karama and post-commitment Ghola timing boundaries; before commitments, a real Salusa return updates the shared free-support descriptor. All four profiles handle world-to-world shipment, Guild transport from Arrakis, native returns and interception, Homeworld battle choices, typed losses, late defense and supported replacement actions. All four profiles can offer and use Junction transport through public quotes, respect population-based movement restrictions and choose legal Spiritual Advisor quantities. Market choices exclude high-Tupile Worthless sales. All four profiles use eligible unused low-Tupile requests without reading opponents’ private hands or balances. All four profiles use the shared closing opportunity for CHOAM actions, affordable Kaitain discards and readiness. Ecaz income needs no separate claim. Fresh occupied decisions supply legal bank/percentage allocation, one eligible real bonus receiver, displaced Ix/cache selection and native Richese terms. Concealed transport, remaining card-effect strategy and difficulty calibration are unfinished. All four original profiles execute the immune full offered quote with its explicit acknowledgment. This is minimal legal participation, not strategy tuning.",
|
||||
"All four profiles share legal Southern portion/destination choices and the remaining-reserves option. Shared public choices supply native shipment counters, Imperial transfers and current low revival allowances. All four profiles use Salusa’s per-type support legality and respect the remaining special-Karama and post-commitment Ghola timing boundaries; before full seals, a real Salusa return updates the shared free-support descriptor and the existing owned inspection-renewal path. All four profiles handle world-to-world shipment, Guild transport from Arrakis, native returns and interception, Homeworld battle choices, typed losses, late defense and supported replacement actions. All four profiles can offer and use Junction transport through public quotes, respect population-based movement restrictions and choose legal Spiritual Advisor quantities. Market choices exclude high-Tupile Worthless sales. All four profiles use eligible unused low-Tupile requests without reading opponents’ private hands or balances. All four profiles use the shared closing opportunity for CHOAM actions, affordable Kaitain discards and readiness. Ecaz income needs no separate claim. Fresh occupied decisions supply legal bank/percentage allocation, one eligible real bonus receiver, displaced Ix/cache selection and native Richese terms. Concealed transport, remaining card-effect strategy and difficulty calibration are unfinished. All four original profiles execute the immune full offered quote with its explicit acknowledgment. This is minimal legal participation, not strategy tuning.",
|
||||
evidence: ['game/homeworld-options.ts', 'game/homeworld-shipment-options.ts', 'game/guild-homeworld-shipment-options.ts', 'game/junction-transport-options.ts'],
|
||||
},
|
||||
{
|
||||
@@ -5622,6 +5622,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'tests/homeworld-salusa-engine.test.ts',
|
||||
'tests/fixture-salusa-ghola.ts',
|
||||
'tests/salusa-ghola-runtime.test.ts',
|
||||
'tests/salusa-ghola-inspection.test.ts',
|
||||
'tests/homeworld-combat.test.ts',
|
||||
'tests/homeworld-battle-resolution.test.ts',
|
||||
'tests/homeworld-battle-engine.test.ts',
|
||||
|
||||
@@ -138,6 +138,8 @@ export function createResidualCunningPriorityFixture(options: { kind?: ResidualC
|
||||
residualCunningPriorityPlayer(game, asker).reserves = 15;
|
||||
Object.assign(game, { phase: 6, active: asker, order: [asker, target, observer], storm: 18,
|
||||
ready: [], response: null, decision: null, phaseOpening: null });
|
||||
// This explicitly controlled Battle position has no live Charity turn.
|
||||
delete game.charityOrder;
|
||||
const cards = game.nexusCards!.cards!, index = cards.deck.indexOf('atreides'); assert.ok(index >= 0);
|
||||
cards.deck.splice(index, 1); cards.deck.unshift('atreides');
|
||||
game.nexusCards!.cards = drawNexusCard(cards, asker, game.players, () => 0);
|
||||
|
||||
@@ -74,7 +74,9 @@ function livingLeader(game: Game, actor: string, strongest: boolean) {
|
||||
/** Earned public one-turn source. Initial hands/Spice order are selected only
|
||||
* while unopened; native setup, payments, typed shipments, split movement and
|
||||
* the earlier combat casualty create every later resource and battle field. */
|
||||
export function createSalusaGholaFixture() {
|
||||
export function createSalusaGholaFixture(options: {
|
||||
firstTerritory?: 'imperial_basin'|'arrakeen'; inspectionDial?: 1|2;
|
||||
} = {}) {
|
||||
const actor = 'emperor', atreides = 'atreides', guild = 'guild';
|
||||
let game = createGame('SALUSAGHOLA',newPlayer(actor,'Emperor','emperor'),true);
|
||||
joinGame(game,newPlayer(atreides,'Atreides','atreides'));
|
||||
@@ -134,7 +136,8 @@ export function createSalusaGholaFixture() {
|
||||
}
|
||||
assert.ok(movement); assert.equal(game.phase,6);
|
||||
reach(g => salusaGholaClean(g) && !g.battle && !!g.active);
|
||||
const beforeFirstBattle = structuredClone(game), firstTerritory = game.active === guild ? 'imperial_basin' : 'arrakeen';
|
||||
const beforeFirstBattle = structuredClone(game), firstTerritory = options.firstTerritory ??
|
||||
(game.active === guild ? 'imperial_basin' : 'arrakeen');
|
||||
const firstOpponent = firstTerritory === 'arrakeen' ? atreides : guild;
|
||||
const firstChooser = game.active!;
|
||||
assert.ok(viewGame(game,firstChooser).battleChoices.some(c => c.territory === firstTerritory));
|
||||
@@ -159,6 +162,21 @@ export function createSalusaGholaFixture() {
|
||||
assert.ok(viewGame(game,chooser).battleChoices.some(c => c.territory === secondTerritory));
|
||||
act(chooser,{type:'chooseBattle',territory:secondTerritory,target:chooser === actor ? opponent : actor});
|
||||
const preparation = structuredClone(game);
|
||||
let inspection: {beforeDeclaration:Game;declared:Game;beforeAnswer:Game;answered:Game;steps:SalusaGholaStep[]}|null = null;
|
||||
if (options.inspectionDial !== undefined) {
|
||||
assert.equal(opponent,atreides,'An actual native Atreides opponent supplies the requested inspection.');
|
||||
reach(g => salusaGholaClean(g) && g.battle?.preparation?.kind === 'prescience' &&
|
||||
g.battle.preparation.owner === atreides);
|
||||
const beforeDeclaration = structuredClone(game), inspectionAt = actions.length;
|
||||
act(atreides,{type:'prescience',field:'dial'});
|
||||
const declared = structuredClone(game);
|
||||
reach(g => salusaGholaClean(g) && g.battle?.preparation?.kind === 'prescienceAnswer' &&
|
||||
g.battle.preparation.owner === actor);
|
||||
const beforeAnswer = structuredClone(game);
|
||||
act(actor,{type:'prescienceAnswer',value:options.inspectionDial});
|
||||
inspection = {beforeDeclaration,declared,beforeAnswer,answered:structuredClone(game),
|
||||
steps:structuredClone(actions.slice(inspectionAt))};
|
||||
}
|
||||
reach(planReady);
|
||||
assert.deepEqual(game.battle!.plans,{});
|
||||
assert.ok(salusaGholaPlayer(game).hand.some(c => c.id === ghola.id));
|
||||
@@ -170,8 +188,9 @@ export function createSalusaGholaFixture() {
|
||||
territory:secondTerritory,firstTerritory,firstOpponent,firstPlans,
|
||||
initial,lobby,setup,beforeDeal,afterSetup,dealOrder,spiceOrder,original,setupActions,
|
||||
shipments,movement,beforeFirstBattle,firstPreparation,firstBeforePlans,firstResolution,firstActions,
|
||||
beforeSecondBattle,preparation,actions,sourceActions:actions,sourceCount:actions.length,
|
||||
disclosure:'Original public Advanced E/A/G with Homeworlds enabled before Ready/start, base33 and no optional modules. Only unopened starting cards, two land Spice positions and scalar lotteries selected. Real typed E4stars desert shipment costs8 and leaves Salusa1; real two-star movement creates Arrakeen contest, and real Guild1 shipment creates Basin contest. The first actual E win uses paid support1/dial2 and kills one real Sardaukar. Stop at the other actual live unsealed battle with Ghola held, Salusa1, tanks1star and two fighting stars. No later phase, force, purse, Tank, hand, battle commitment or receipt is installed.'};
|
||||
beforeSecondBattle,preparation,inspection,actions,sourceActions:actions,sourceCount:actions.length,
|
||||
disclosure:'Original public Advanced E/A/G with Homeworlds enabled before Ready/start, base33 and no optional modules. Only unopened starting cards, two land Spice positions and scalar lotteries selected. Real typed E4stars desert shipment costs8 and leaves Salusa1; real two-star movement creates Arrakeen contest, and real Guild1 shipment creates Basin contest. The first actual E win uses paid support1/dial2 and kills one real Sardaukar. Stop at the other actual live unsealed battle with Ghola held, Salusa1, tanks1star and two fighting stars. No later phase, force, purse, Tank, hand, battle commitment or receipt is installed.' +
|
||||
(inspection ? ` Optional actual Atreides native dial Prescience and Emperor answer${options.inspectionDial} are recorded before either plan seals; the preferred first battle is selected only from the actual chooser options.` : '')};
|
||||
}
|
||||
export type SalusaGholaFixture = ReturnType<typeof createSalusaGholaFixture>;
|
||||
|
||||
|
||||
@@ -473,7 +473,7 @@ void test('an opponent-only sealed plan keeps the remaining Salusa timing bounda
|
||||
assert.deepEqual(g, before);
|
||||
});
|
||||
|
||||
void test('Salusa threshold protection also preserves an answered Prescience dial before either full plan is sealed', () => {
|
||||
void test('Salusa Ghola renews an impossible answered Prescience dial before either full plan is sealed', () => {
|
||||
const fixture = gholaThresholdBattle(0);
|
||||
const card = fixture.card;
|
||||
let g = fixture.g;
|
||||
@@ -491,19 +491,19 @@ void test('Salusa threshold protection also preserves an answered Prescience dia
|
||||
g = applyAction(g, 'e', { type: 'prescienceAnswer', value: 1 });
|
||||
assert.equal(g.battle!.prescience!.value, 1);
|
||||
assert.deepEqual(g.battle!.plans, {});
|
||||
const saved = reload(g);
|
||||
const reason = viewGame(saved, 'e').ghola.eliteBlock;
|
||||
assert.ok(reason);
|
||||
assert.throws(
|
||||
() => applyAction(saved, 'e', { type: 'card', card, amount: 1, elite: 1 }),
|
||||
(error) => error instanceof Error && error.message === reason,
|
||||
);
|
||||
assert.deepEqual(saved, g);
|
||||
assert.equal(saved.battle!.prescience!.value, 1);
|
||||
const done = resolve(saved, 1, 0);
|
||||
assert.equal(viewGame(g, 'e').ghola.eliteBlock, null);
|
||||
g = applyAction(g, 'e', { type: 'card', card, amount: 1, elite: 1 });
|
||||
assert.equal(g.battle!.prescience!.field, 'dial');
|
||||
assert.equal(Object.hasOwn(g.battle!.prescience!, 'value'), false);
|
||||
assert.equal(g.battle!.preparation!.kind, 'prescienceAnswer');
|
||||
assert.equal(g.battle!.preparation!.owner, 'e');
|
||||
assert.throws(() => applyAction(g, 'e', plan(g, 2)), /battle plan is not available/);
|
||||
assert.throws(() => applyAction(g, 'e', { type: 'prescienceAnswer', value: 1 }), /legal battle plan/);
|
||||
g = applyAction(g, 'e', { type: 'prescienceAnswer', value: 2 });
|
||||
const done = resolve(g, 2, 0);
|
||||
assert.equal(done.lastBattleContext!.winner, 'e');
|
||||
assert.equal(done.homeworlds!.custody!.salusa!.elite, 1);
|
||||
assert.equal(emperor(done).elites!.tanks, 2);
|
||||
assert.equal(done.discard.filter((held) => held.id === card).length, 0);
|
||||
assert.equal(done.homeworlds!.custody!.salusa!.elite, 2);
|
||||
assert.equal(emperor(done).elites!.tanks, 1);
|
||||
assert.equal(done.discard.filter((held) => held.id === card).length, 1);
|
||||
inventory(done);
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import {
|
||||
createSalusaGholaFixture, playSalusaGholaBattle, actSalusaGhola as act,
|
||||
advanceSalusaGhola as advance, salusaGholaClean as clean,
|
||||
salusaGholaArmy as army, salusaGholaPlayer as p, salusaGholaInventory as inventory,
|
||||
type SalusaGholaFixture,
|
||||
} from './fixture-salusa-ghola';
|
||||
|
||||
const sources = new Map<number,SalusaGholaFixture>();
|
||||
function source(answer: 1|2) {
|
||||
let f = sources.get(answer);
|
||||
if (!f) {
|
||||
f = createSalusaGholaFixture({firstTerritory:'imperial_basin',inspectionDial:answer});
|
||||
sources.set(answer,f);
|
||||
}
|
||||
return f;
|
||||
}
|
||||
function reject(game: Game, actor: string, action: Action) {
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => applyAction(game,actor,action));
|
||||
assert.deepEqual(game,before);
|
||||
}
|
||||
function leader(game: Game, actor: string, strongest: boolean) {
|
||||
const choices = p(game,actor).leaders.filter(l => !l.dead && !l.usedAt && !l.capturedBy);
|
||||
choices.sort((a,b) => strongest ? b.strength-a.strength : a.strength-b.strength);
|
||||
assert.ok(choices[0]); return choices[0].id;
|
||||
}
|
||||
function finish(f: SalusaGholaFixture, state: Game) {
|
||||
let game = act(state,f.actor,{type:'battlePlan',leader:leader(state,f.actor,true),dial:2,support:0});
|
||||
game = act(game,f.opponent,{type:'battlePlan',leader:leader(game,f.opponent,false),dial:0,support:0});
|
||||
return advance(game,g => !g.battle && clean(g));
|
||||
}
|
||||
|
||||
void test('earned Basin-first source answers real Atreides dial Prescience before either plan is sealed', () => {
|
||||
const f = source(1), inspection = f.inspection;
|
||||
assert.ok(inspection);
|
||||
assert.equal(f.firstTerritory,'imperial_basin');
|
||||
assert.equal(f.firstOpponent,'guild');
|
||||
assert.equal(f.territory,'arrakeen');
|
||||
assert.equal(f.opponent,'atreides');
|
||||
assert.ok(viewGame(f.beforeFirstBattle,f.beforeFirstBattle.active!).battleChoices
|
||||
.some(choice => choice.territory === 'imperial_basin'));
|
||||
assert.equal(inspection.beforeDeclaration.battle!.preparation!.kind,'prescience');
|
||||
assert.equal(inspection.beforeDeclaration.battle!.preparation!.owner,'atreides');
|
||||
assert.equal(inspection.steps[0].actor,'atreides');
|
||||
assert.deepEqual(inspection.steps[0].action,{type:'prescience',field:'dial'});
|
||||
assert.equal(inspection.beforeAnswer.battle!.preparation!.owner,f.actor);
|
||||
assert.ok(inspection.steps.some(step => step.actor === f.actor &&
|
||||
step.action.type === 'prescienceAnswer' && step.action.value === 1));
|
||||
assert.equal(f.game.battle!.prescience!.player,'atreides');
|
||||
assert.equal(f.game.battle!.prescience!.field,'dial');
|
||||
assert.equal(f.game.battle!.prescience!.value,1);
|
||||
assert.deepEqual(f.game.battle!.plans,{});
|
||||
assert.equal(f.game.battle!.preparation,undefined);
|
||||
assert.equal(p(f.game).elites!.tanks,1);
|
||||
assert.deepEqual(army(f.game),{normal:0,elite:1});
|
||||
assert.equal(f.sourceCount,f.sourceActions.length);
|
||||
assert.deepEqual(inventory(f.game),f.original);
|
||||
});
|
||||
|
||||
void test('actual Ghola invalidates an impossible old odd dial and requires owned legal reanswer before native plans', () => {
|
||||
const f = source(1);
|
||||
let game = act(structuredClone(f.game),f.actor,f.action);
|
||||
assert.deepEqual(army(game),{normal:0,elite:2});
|
||||
assert.equal(game.battle!.event,f.game.battle!.event);
|
||||
assert.equal(game.battle!.prescience!.field,'dial');
|
||||
assert.equal(game.battle!.prescience!.player,'atreides');
|
||||
assert.equal(Object.hasOwn(game.battle!.prescience!,'value'),false);
|
||||
assert.equal(game.battle!.preparation?.kind,'prescienceAnswer');
|
||||
assert.equal(game.battle!.preparation?.owner,f.actor);
|
||||
assert.equal(game.battle!.preparation?.beneficiary,'atreides');
|
||||
assert.deepEqual(game.battle!.plans,{});
|
||||
assert.equal(viewGame(game,f.actor).battle!.ownForces!.eliteFreeSupport,true);
|
||||
assert.equal(game.discard.filter(c => c.id === f.card).length,1);
|
||||
assert.equal(p(game).elites!.tanks,0);
|
||||
reject(game,f.actor,{type:'battlePlan',leader:leader(game,f.actor,true),dial:1,support:0});
|
||||
reject(game,f.actor,{type:'prescienceAnswer',value:1});
|
||||
reject(game,f.opponent,{type:'prescienceAnswer',value:2});
|
||||
game = act(game,f.actor,{type:'prescienceAnswer',value:2});
|
||||
assert.equal(game.battle!.prescience!.value,2);
|
||||
assert.equal(game.battle!.preparation,undefined);
|
||||
assert.equal(p(game).elites!.tanks,0);
|
||||
game = finish(f,game);
|
||||
assert.equal(game.lastBattleContext!.winner,f.actor);
|
||||
assert.equal(p(game).elites!.tanks,1);
|
||||
assert.deepEqual(army(game),{normal:0,elite:2});
|
||||
assert.equal(game.discard.filter(c => c.id === f.card).length,1);
|
||||
assert.deepEqual(inventory(game),f.original);
|
||||
});
|
||||
|
||||
void test('actual Ghola retains a still-feasible native dial2 answer and finishes without another inspection', () => {
|
||||
const f = source(2), result = playSalusaGholaBattle(f);
|
||||
assert.equal(f.game.battle!.prescience!.value,2);
|
||||
assert.equal(result.returned.battle!.prescience!.value,2);
|
||||
assert.equal(result.returned.battle!.preparation,undefined);
|
||||
assert.equal(result.returned.battle!.prescience!.player,'atreides');
|
||||
assert.equal(result.afterPlan.battle!.plans[f.actor].dial,2);
|
||||
assert.equal(result.afterPlan.battle!.plans[f.actor].support,0);
|
||||
assert.equal(result.actions.filter(step => step.action.type === 'prescience' ||
|
||||
step.action.type === 'prescienceAnswer').length,0);
|
||||
assert.equal(result.game.lastBattleContext!.winner,f.actor);
|
||||
assert.equal(result.game.discard.filter(c => c.id === f.card).length,1);
|
||||
assert.deepEqual(inventory(result.game),f.original);
|
||||
});
|
||||
|
||||
void test('all four minimal profiles can supply a legal current Salusa Prescience reanswer', () => {
|
||||
const f = source(1), pending = act(structuredClone(f.game),f.actor,f.action);
|
||||
for (const profile of ['Easy','Medium','Hard','Brutal'] as const) {
|
||||
const game = structuredClone(pending), view = viewGame(game,f.actor);
|
||||
view.players.find(p => p.id === f.actor)!.bot = profile;
|
||||
const action = botActions(view).find(action => action.type === 'prescienceAnswer');
|
||||
assert.ok(action,`${profile} needs the actual owned reanswer action.`);
|
||||
assert.notEqual(action.value,1);
|
||||
const next = act(game,f.actor,action);
|
||||
assert.equal(next.battle!.prescience!.value,action.value);
|
||||
assert.equal(next.battle!.preparation,undefined);
|
||||
assert.deepEqual(next.battle!.plans,{});
|
||||
assert.equal(next.discard.filter(c => c.id === f.card).length,1);
|
||||
assert.deepEqual(army(next),{normal:0,elite:2});
|
||||
assert.deepEqual(inventory(next),f.original);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user