Preserve binding Truthtrance through Salusa Ghola

This commit is contained in:
Quintine committed 2026-10-27 16:16:30 +11:00
1 parent 53b37adf9f
commit b620cea873
15 files changed
+515 -33

No files matched your search

+22
View File
@@ -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 / live Truthtrance — 27 October. Prototyped / Partial.**
Before either full seal, feasible Yes, whole-No and grouped promises survive the
actual return. A return that makes the actor's own promise impossible rejects
without cost or release. Obsolete inspections renew before truth feasibility;
the real actor context survives effect and card disposal. Exact force-group
blocks feed current human controls, force maxima and minimal AI. The voluntary
self-impossibility policy remains an existing product interpretation; full
sealed-plan timing stays guarded. [Contract](HOMEWORLD_BENEFITS_RULES.md#salusa-ghola-and-live-truthtrance-promises--27-october-2026).
**Sixteen files145/145**, types/lint and three native runtime programmes pass.
Original public Advanced E/A/G base33/Homeworlds earn a real first-battle star
loss and use the originally dealt Atreides Truthtrance for actual questions/
definite answers. Prefixes total169 actions;16 further accepted actions compare
Yes2 retention, immutable rejected Yes1 then native completion/later Ghola, and
Prescience1 plus TruthYes2 renewal/completion. One rejection,11 GameTable/two
BattlePreparation SSR renders,3 group-block adapters and1 answer-adapter packet
are observed. Tests additionally cover whole-No, grouped OR, positive support
and all four minimal policies. Stateful Ghola/answer/plan forms are not operated
in a browser. No full-seal, played Cunning/Ghola, native-Salusa battle, complete
mode, mobile, deployed or final assurance acceptance is certified. No build was
repeated for this focused rule change.
**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
+1 -1
View File
@@ -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 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.
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 a full seal; [live-promise feasibility](HOMEWORLD_BENEFITS_RULES.md#salusa-ghola-and-live-truthtrance-promises--27-october-2026) before seals uses current population, original inspection renewal and actual voluntary actor context. These restrictions are development boundaries, not official prohibitions. Other Homeworld face effects, occupation rights and full interaction coverage are still required.
## Evidence
+62
View File
@@ -2,6 +2,68 @@
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 live Truthtrance promises — 27 October 2026
Before either full plan seals, a current battle promise no longer forbids every
population-crossing Ghola. Preserve a feasible Yes, whole-No or grouped claim.
Reject the actor's return if the resulting High support regime makes that
actor's own promise impossible, including an odd dial with only free double
stars or a positive support promise with no payable ordinary counter. A rejected
action consumes no card, force, revival allowance or binding.
The actual physical return and any obsolete inspection renewal occur before
Truthtrance feasibility. Carry the real Ghola actor/action through that check
and its printed card disposal. An uncontextual release must not retire the
actor's promise first. Feasible answers remain fixed; an opposing promise that
actually becomes impossible follows the existing release policy. No new promise
parser or battle solver is introduced.
The server quotes each real force group using a detached copy, the native Ghola
effect/disposal and the existing reachable-plan solver. Current force maxima,
elite allowance and exact `homeworldRevivalBlocks` use those results. Human
controls and minimal AI consume the existing shared block adapter; they do not
reconstruct claim legality or inspect opponents' plans.
The supplied PDF physical39 / printed37 requires Truthtrance compliance while
possible. [Official November2020 FAQp8](https://www.gf9games.com/dune/wp-content/uploads/2020/11/Dune-FAQ-Nov-2020.pdf#page=8)
permits current-turn and AND/OR questions. Neither separately defines deliberate
self-impossibility; rejecting the actor's voluntary destruction is the existing
[product interpretation](TRUTHTRANCE_NONBATTLE_READINESS_20260907.md#where-promises-currently-bind-actions),
not a new printed Ghola restriction. Full sealed-plan repricing remains an
unfinished preview boundary. Earlier inspection/promise guards below are dated
history where superseded by this follow-up.
### Live-promise evidence
Sixteen selected files pass145/145 with types/lint. Native tests use actual
initial Atreides Truthtrance, priority passes, current battle questions and
definite answers. They cover feasible Yes2, whole-No1, grouped OR1/2, own
impossible Yes1 and positive support, later use after completing the original
promise, and Prescience1 plus TruthYes2 renewal. Exact control-block reasons
match authoritative rejections and maxima. All four minimal profiles submit
legal required plans and omit blocked/spent Ghola.
The separate runtime has three original public Advanced E/A/G base33/Homeworld
prefixes totaling169 actions. A real paid first-battle star loss creates the
Tanks source; no played resource, phase, inspection, promise or receipt is
installed. Only unopened initial card/Spice positions and native scalar
lotteries are selected. Sixteen accepted continuation actions compare feasible
return, one immutable rejection followed by actual original-plan completion/
later Ghola, and original inspection renewal while preserving TruthYes2.
Eleven GameTable/two BattlePreparation SSR renders, three existing group-block
adapter checks and one shared answer-adapter packet connect current controls.
Stateful Ghola/answer/plan inputs are not operated in a browser. Full seals,
new played Cunning/Ghola pairs, native-Salusa battle, complete modes, mobile,
deployed and final recovery/privacy/custody acceptance remain separate.
No build was repeated for this focused rule change.
Private proof: `/home/quintine/.local/share/dune/checkpoints/20261027-salusa-ghola-truth-proof-v1/`;
runtime: `20261027-salusa-ghola-truth-runtime/run-v1/` beside it. Frozen tree
`d713902330ddca9084ab9a2d9e02b239ee7344b2d33d97eb9f040131a5f1c65c`
contains3354 files. Final prose overlays are limited to this contract, the
dashboard and implementation log.
## Salusa Ghola and renewed inspection answers — 27 October 2026
The earlier commitment-free window below now includes completed inspection
+2 -1
View File
@@ -2,7 +2,8 @@
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.
renewing impossible original fields. The [live-promise follow-up](HOMEWORLD_BENEFITS_RULES.md#salusa-ghola-and-live-truthtrance-promises--27-october-2026)
retains feasible claims and rejects self-impossible returns; full seals remain 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.
+1 -1
View File
@@ -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 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-benefits.ts::homeworldSardaukarGholaBlock` | Salusa population exactly one after a full seal | Remaining support-transition preview guard; pre-seal Ghola uses current population, renews obsolete inspections and enforces exact own-promise feasibility. 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. |
+33
View File
@@ -1,5 +1,38 @@
# Dune implementation status
## 27 October 2026 — Salusa Ghola with live Truthtrance
Before either full seal, preserve feasible Yes/whole-No/grouped promises and
reject the actor's own impossible return. Carry actual actor/action context
through inspection renewal, truth feasibility and physical card disposal; the
own promise cannot be released first. Exact-group native-effect/solver quotes
drive current force maxima, human controls and minimal AI. The existing
voluntary-impossibility product interpretation is explicit; full-seal timing
remains a preview boundary.
[Contract](HOMEWORLD_BENEFITS_RULES.md#salusa-ghola-and-live-truthtrance-promises--27-october-2026).
Sixteen-file145/145, types/lint and three original public native runtime
programmes pass. Advanced E/A/G base33/Homeworlds earn the earlier paid star
loss, receive original Truthtrance and make actual current-battle declarations/
definite answers. Three prefixes total169 actions;16 accepted continuations
compare feasible Yes2 return, immutable rejected Yes1 followed by original
completion/later Ghola, and Prescience1/TruthYes2 renewal/completion. One rejected
attempt,11 GameTable/two BattlePreparation SSR renders,3 group-block adapters
and1 answer-adapter packet are observed. Tests additionally cover whole-No,
OR1/2, paid-support impossibility and all four minimal policies. No played
resource, phase, inspection, promise or receipt is installed; only unopened
initial card/Spice positions and native scalar lotteries are selected.
Stateful input/browser operation, full seals, played Cunning/Ghola pairs,
native-Salusa battle, complete modes, mobile, deployed and final assurance are
not certified; no build was repeated for this focused rule change. Private
proof `20261027-salusa-ghola-truth-proof-v1/` and runtime
`20261027-salusa-ghola-truth-runtime/run-v1/` use the existing private checkpoint
directory. Frozen tree
`d713902330ddca9084ab9a2d9e02b239ee7344b2d33d97eb9f040131a5f1c65c`
contains3354 files; final prose overlays are limited to this log,
`CURRENT_STATUS.md` and `HOMEWORLD_BENEFITS_RULES.md`.
## 27 October 2026 — Salusa Ghola inspection renewal
An actual Salusa threshold return before either full plan seals now reuses the
+3
View File
@@ -73,6 +73,9 @@ The unresolved cases were reported to the coordinator before implementation deci
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.
The later [Truthtrance follow-up](HOMEWORLD_BENEFITS_RULES.md#salusa-ghola-and-live-truthtrance-promises--27-october-2026)
carries the voluntary Ghola actor through the subsequent promise check, so
retiring an obsolete inspection cannot release that actor's impossible promise.
**Selected provisional composition — 26 October 2026.** The 14 October
choose-recommended authority supersedes the earlier blanket product refusal.
+13
View File
@@ -1,5 +1,18 @@
# Rule decision index
## Salusa Ghola with binding Truthtrance — 27 October 2026
Remove the blanket live-promise restriction before either full seal. Preserve
feasible Yes/whole-No/grouped claims and reject a return that deliberately makes
the actor's own promise impossible. Renew obsolete inspections first, then test
with the actual Ghola actor/action through effect and physical card disposal;
do not release that own promise without actor context. Exact-group projections
share the native effect and existing solver with current controls/minimal AI.
Compliance-while-possible comes from the supplied physical39/printed37 and
official FAQp8; voluntary self-impossibility rejection remains the existing
product interpretation, not a separately printed Ghola rule. Full seals retain
their preview boundary. [Contract](HOMEWORLD_BENEFITS_RULES.md#salusa-ghola-and-live-truthtrance-promises--27-october-2026).
## Salusa Ghola inspection renewal — 27 October 2026
Before either full plan is sealed, an actual Salusa-crossing Ghola return may
+53 -10
View File
@@ -228,6 +228,7 @@ import {
homeworldLowBonus,
homeworldSardaukarFreeSupport,
homeworldSardaukarGholaBlock,
homeworldSardaukarGholaCrossing,
snapshotHomeworldRevival,
tleilaxuHomeworldFreeIncomeBlocked,
HomeworldBenefitError,
@@ -28288,7 +28289,8 @@ function playResidualPoison(
if (b.preLeader && !b.preLeader.closed)
b.preLeader.ready = b.preLeader.ready.filter((id) => id !== p.id);
}
function reconcileChangedBattleInspections(g: Game, cause: string, renewExtraWithNative = false) {
function reconcileChangedBattleInspections(g: Game, cause: string, renewExtraWithNative = false,
voluntary?: { actor: string; action: Action }) {
const b = g.battle!;
// Selected provisional priority: preserve an independently feasible native
// answer first, then require the extra answer to be jointly feasible. This
@@ -28339,7 +28341,7 @@ function reconcileChangedBattleInspections(g: Game, cause: string, renewExtraWit
log(g, `After ${cause}, the inspected element became impossible. Its owner must answer the same element again; the earlier private observation is retained.`);
}
}
reconcileBattlePromises(g);
reconcileBattlePromises(g, voluntary);
}
function residualPoisonView(g: Game, p: Player) {
const card = p.hand.find(
@@ -28879,6 +28881,30 @@ function gholaLeaders(g: Game, p: Player) {
native.push(g.dukeVidal!.leader);
return native;
}
/** Probe only this actor's population-crossing force choices on detached
* states. Use the real effect, card disposal and existing promise solver;
* neither the view nor a bot reconstructs Truthtrance legality. */
function salusaGholaPromiseBlocks(g: Game, p: Player, cards: readonly string[],
maximum: number, eliteRemaining: number) {
const blocks: { source: 'ghola'; amount: number; elite: number; reason: string }[] = [];
if (!cards.length || !eliteRemaining || !homeworldSardaukarGholaCrossing(g, p.id) ||
homeworldSardaukarGholaBlock(g, p.id) ||
!g.battle!.truthPromises?.some(promise => promise.player === p.id && !promise.released)) return blocks;
for (const action of gholaForcePreparations({ cards: cards.slice(0, 1), tanks: p.tanks,
eliteTanks: p.elites?.tanks ?? 0, maxForces: maximum, eliteRemaining })) {
if (!action.elite) continue;
const trial = structuredClone(g), owner = getPlayer(trial, p.id);
try {
applyGholaEffect(trial, owner, action);
discard(trial, owner, action.card as string);
reconcileBattlePromises(trial, { actor: p.id, action });
} catch (error) {
if (!(error instanceof RuleError)) throw error;
blocks.push({ source: 'ghola', amount: Number(action.amount), elite: Number(action.elite), reason: error.message });
}
}
return blocks;
}
function gholaOptions(g: Game, p: Player) {
const cards = p.hand
.filter(
@@ -28910,15 +28936,25 @@ function gholaOptions(g: Game, p: Player) {
strength,
}));
const kwisatz = !!(g.advanced && p.faction === 'atreides' && p.kwisatz?.dead);
const eliteBlock = homeworldSardaukarGholaBlock(g, p.id);
const eliteRemaining = eliteBlock ? 0 : Math.min(
let eliteBlock = homeworldSardaukarGholaBlock(g, p.id);
let eliteRemaining = eliteBlock ? 0 : Math.min(
p.elites?.tanks ?? 0,
eliteRevivalRemaining(p, g.advanced),
);
const maxForces = Math.min(
let maxForces = Math.min(
5,
p.tanks - (p.elites?.tanks ?? 0) + eliteRemaining,
);
const promiseBlocks = salusaGholaPromiseBlocks(g, p, cards, maxForces, eliteRemaining);
if (promiseBlocks.length) {
const legal = [...gholaForcePreparations({ cards: cards.slice(0, 1), tanks: p.tanks,
eliteTanks: p.elites?.tanks ?? 0, maxForces, eliteRemaining })].filter(action =>
!promiseBlocks.some(block => block.amount === action.amount && block.elite === action.elite));
maxForces = Math.max(0, ...legal.map(action => Number(action.amount)));
const eligibleElite = Math.max(0, ...legal.map(action => Number(action.elite)));
if (eligibleElite < eliteRemaining) eliteBlock = promiseBlocks[0].reason;
eliteRemaining = eligibleElite;
}
const reason = !card
? 'No Ghola card is available in your hand.'
: g.pendingTreacheryDiscard
@@ -28928,7 +28964,7 @@ function gholaOptions(g: Game, p: Player) {
: timing && !timing.available
? timing.reason
: !leaders.length && !kwisatz && maxForces <= 0
? 'No eligible leader or force is available in your Tanks.'
? eliteBlock ?? 'No eligible leader or force is available in your Tanks.'
: null;
return {
cards,
@@ -29069,6 +29105,7 @@ function playMarketGhola(g: Game, p: Player, action: Action) {
}
if (g.response) (g.response as ResponseWindow).intent = g.pendingChoamMarketGhola.event;
const used = discard(g, p, card.id);
reconcileBattlePromises(g, { actor: p.id, action });
guardTurnActionPrintedCardCost(g, p, used);
log(g, `${p.name} played ${card.name}. The declared CHOAM sale waits for this revival and its income to finish.`,
{ faction: p.faction, name: 'Ghola revival' });
@@ -29077,7 +29114,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);
const salusaCrossing = homeworldSardaukarGholaCrossing(g, p.id);
if (action.leader === 'kwisatz') {
requireRule(
g.advanced && p.faction === 'atreides' && p.kwisatz?.dead,
@@ -29143,9 +29180,8 @@ 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);
if (salusaCrossing && homeworldSardaukarFreeSupport(g, p.id))
reconcileChangedBattleInspections(g, 'the Salusa population increase', true, { actor: p.id, action });
}
/** Search complete legal plans without learning or consulting the opponent's sealed choices. */
function findLegalBattlePlan(
@@ -32934,6 +32970,10 @@ function projectedHomeworldRevivalReturn(g: Game, player: string) {
}
function homeworldRevivalChoiceBlocks(g: Game, p: Player) {
const blocks: { source: 'normal' | 'ghola'; amount: number; elite: number; reason: string }[] = [];
if (p.faction === 'emperor') return salusaGholaPromiseBlocks(g, p,
p.hand.filter(card => card.effect === 'ghola' &&
!(g as TurnActionCardProbe)._turnActionUnknownCards?.includes(card.id)).map(card => card.id), Math.min(5, p.tanks),
Math.min(p.elites?.tanks ?? 0, eliteRevivalRemaining(p, g.advanced)));
if (!g.homeworlds?.custody || !['fremen', 'tleilaxu'].includes(p.faction)) return blocks;
for (const source of ['normal', 'ghola'] as const) {
const maximum = source === 'normal' ? forceRevivalRemaining(g, p) : Math.min(5, p.tanks);
@@ -43191,6 +43231,9 @@ function applyActionInner(
);
const removed = g.advanced && c.effect === 'atomics';
const used = removed ? removeUsedAtomics(g, p, c.id) : discard(g, p, c.id);
// Preserve the voluntary actor through the printed Ghola cost, before a
// later disposal response can attempt an uncontextual promise release.
if (c.effect === 'ghola') reconcileBattlePromises(g, { actor: id, action });
if (execution === 'live' && (c.effect === 'hajr' || c.effect === 'ghola' || isWindowCardEffect(c.effect)))
guardTurnActionPrintedCardCost(g, p, used);
log(
+19 -14
View File
@@ -59,15 +59,12 @@ export function homeworldSardaukarFreeSupport(
);
}
/** 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(
/** One returned star crosses Salusa's current threshold during its native
* player's battle. The caller still owns card timing and actual return count. */
export function homeworldSardaukarGholaCrossing(
context: HomeworldBenefitContext & Pick<Game, 'battle'>,
playerId: string,
): string | null {
): boolean {
if (
!context.advanced ||
!context.homeworlds?.custody ||
@@ -75,8 +72,8 @@ export function homeworldSardaukarGholaBlock(
![context.battle.attacker, context.battle.defender].includes(playerId) ||
!context.players.some((p) => p.id === playerId && p.faction === 'emperor')
)
return null;
const crossing = homeworldPopulations(
return false;
return homeworldPopulations(
homeworldContext(context),
context.homeworlds.custody,
).some(
@@ -85,12 +82,20 @@ export function homeworldSardaukarGholaBlock(
home.card === 'salusa_secundus' &&
home.population === 1,
);
if (!crossing) return null;
const battle = context.battle;
const committed = battle.revealed || Object.keys(battle.plans).length > 0 ||
battle.truthPromises?.some((promise) => !promise.released);
}
/** Current population can change before sealed plans. The caller renews
* impossible inspections and enforces voluntary Truthtrance promises after
* the physical return. Inspect submission presence, never hidden plan values. */
export function homeworldSardaukarGholaBlock(
context: HomeworldBenefitContext & Pick<Game, 'battle'>,
playerId: string,
): string | null {
if (!homeworldSardaukarGholaCrossing(context, playerId)) return null;
const battle = context.battle!;
const committed = battle.revealed || Object.keys(battle.plans).length > 0;
return committed
? 'Ghola raising Salusa to high population after a sealed plan or during a binding battle promise is not yet supported. Finish this battle first.'
? 'Ghola raising Salusa to high population after a sealed plan is not yet supported. Finish this battle first.'
: null;
}
+5 -2
View File
@@ -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 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.",
"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. Feasible Truthtrance Yes/No/grouped promises remain; self-impossible returns reject with exact force-group control reasons. Full-seal 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.",
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 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.",
"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 full-seal Ghola timing boundaries; before full seals, a real Salusa return updates the shared free-support descriptor and owned inspection-renewal path. Exact native force-group blocks preserve binding promises without duplicating the solver in a policy. 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'],
},
{
@@ -5623,6 +5623,9 @@ export const RULE_TOPICS: RuleTopic[] = [
'tests/fixture-salusa-ghola.ts',
'tests/salusa-ghola-runtime.test.ts',
'tests/salusa-ghola-inspection.test.ts',
'tests/fixture-salusa-ghola-truth.ts',
'tests/salusa-ghola-truth.test.ts',
'tests/salusa-ghola-truth-controls.test.ts',
'tests/homeworld-combat.test.ts',
'tests/homeworld-battle-resolution.test.ts',
'tests/homeworld-battle-engine.test.ts',
+76
View File
@@ -0,0 +1,76 @@
import assert from 'node:assert/strict';
import { viewGame, type Action, type Game } from '../game/engine';
import type { PlanClaim } from '../game/battle-promises';
import type { TruthQuestion } from '../game/truthtrance';
import {
createSalusaGholaFixture, actSalusaGhola, advanceSalusaGhola, salusaGholaClean,
salusaGholaPlayer, salusaGholaInventory, type SalusaGholaStep,
} from './fixture-salusa-ghola';
export const actSalusaGholaTruth = actSalusaGhola;
export const salusaGholaTruthPlayer = salusaGholaPlayer;
export const salusaGholaTruthInventory = salusaGholaInventory;
export const salusaGholaTruthClean = salusaGholaClean;
/** Original public source and native Truth protocol. The optional inspection
* was really declared/answered before this original Truth card is used. No
* binding, frozen answer, battle commitment or physical receipt is installed. */
export function createSalusaGholaTruthFixture(options: {
claim?: PlanClaim; answer?: 'yes'|'no'; inspectionDial?: 1|2;
} = {}) {
const base = createSalusaGholaFixture({firstTerritory:'imperial_basin',truthtrance:true,
...(options.inspectionDial === undefined ? {} : {inspectionDial:options.inspectionDial})});
const actor = base.actor, opponent = base.opponent;
assert.equal(opponent,'atreides');
const truth = salusaGholaPlayer(base.game,opponent).hand.find(c => c.effect === 'truthtrance');
assert.ok(truth);
const claim: PlanClaim = structuredClone(options.claim ?? {kind:'dial',compare:'eq',value:2});
const answer = options.answer ?? 'yes';
const question: Extract<TruthQuestion,{kind:'battlePlan'}> = {
kind:'battlePlan',target:actor,territory:base.territory,claim,
};
const actions: SalusaGholaStep[] = [...base.sourceActions], at = actions.length;
let game = actSalusaGhola(structuredClone(base.game),opponent,{type:'card',card:truth.id},actions);
const declared = structuredClone(game);
for (let n = 0; game.truthtrance?.stage === 'priority' && n < game.players.length; n++) {
const owner = game.players.find(p => !game.truthtrance!.passed.includes(p.id)); assert.ok(owner);
game = actSalusaGhola(game,owner.id,{type:'truthPass'},actions);
}
assert.equal(game.truthtrance?.stage,'ask');
const beforeQuestion = structuredClone(game);
game = actSalusaGhola(game,opponent,{type:'truthAsk',question},actions);
const asked = structuredClone(game);
assert.ok(viewGame(game,actor).truthBattleAnswers?.includes(answer),
'The actual native Truth question must offer the requested definite answer.');
game = actSalusaGhola(game,actor,{type:'truthAnswer',answer},actions);
assert.equal(game.truthtrance,null);
assert.deepEqual(game.battle!.plans,{});
assert.equal(game.discard.filter(c => c.id === truth.id).length,1);
assert.deepEqual(salusaGholaInventory(game),base.original);
return {...base,base,game,source:structuredClone(game),actor,opponent,truthCard:truth.id,
claim,answer,question,declared,beforeQuestion,asked,event:game.battle!.event!,
actions,sourceActions:actions,sourceCount:actions.length,truthActions:actions.slice(at),
disclosure:base.disclosure+' Atreides actually received its original unopened Truthtrance instead of Shield. Its native printed declaration, original priority passes, battle-plan question and Emperor definite answer create the live binding; no promise is installed.'};
}
export type SalusaGholaTruthFixture = ReturnType<typeof createSalusaGholaTruthFixture>;
export function salusaGholaTruthPlan(game: Game, dial = 2, support = 0): Action {
const leaders = salusaGholaPlayer(game).leaders.filter(l => !l.dead && !l.usedAt && !l.capturedBy)
.sort((a,b) => b.strength-a.strength);
assert.ok(leaders[0]);
return {type:'battlePlan',leader:leaders[0].id,dial,support};
}
/** Actual native seals and calls only. It does not play Ghola or manufacture a
* replacement inspection answer; the caller must finish the current chooser. */
export function finishSalusaGholaTruthBattle(f: SalusaGholaTruthFixture, state: Game,
plan: Action = salusaGholaTruthPlan(state), actions: SalusaGholaStep[] = []) {
let game = actSalusaGhola(state,f.actor,plan,actions);
const afterPlan = structuredClone(game);
const enemy = salusaGholaPlayer(game,f.opponent).leaders.filter(l => !l.dead && !l.usedAt && !l.capturedBy)
.sort((a,b) => a.strength-b.strength)[0]; assert.ok(enemy);
game = actSalusaGhola(game,f.opponent,{type:'battlePlan',leader:enemy.id,dial:0,support:0},actions);
const revealed = structuredClone(game);
game = advanceSalusaGhola(game,g => !g.battle && salusaGholaClean(g),actions);
return {game,afterPlan,revealed,actions};
}
+5 -4
View File
@@ -75,7 +75,7 @@ function livingLeader(game: Game, actor: string, strongest: boolean) {
* while unopened; native setup, payments, typed shipments, split movement and
* the earlier combat casualty create every later resource and battle field. */
export function createSalusaGholaFixture(options: {
firstTerritory?: 'imperial_basin'|'arrakeen'; inspectionDial?: 1|2;
firstTerritory?: 'imperial_basin'|'arrakeen'; inspectionDial?: 1|2; truthtrance?: true;
} = {}) {
const actor = 'emperor', atreides = 'atreides', guild = 'guild';
let game = createGame('SALUSAGHOLA',newPlayer(actor,'Emperor','emperor'),true);
@@ -91,9 +91,10 @@ export function createSalusaGholaFixture(options: {
const setup = structuredClone(game);
assert.equal(game.status,'setup'); assert.ok(game.players.every(p => p.hand.length === 0));
const ghola = game.deck.find(c => c.effect === 'ghola');
const shield = game.deck.find(c => c.kind === 'shield'), projectile = game.deck.find(c => c.kind === 'projectile');
assert.ok(ghola && shield && projectile);
const selected = [ghola,shield,projectile], dealOrder = selected.map((card,position) => {
const atreidesCard = game.deck.find(c => options.truthtrance ? c.effect === 'truthtrance' : c.kind === 'shield');
const projectile = game.deck.find(c => c.kind === 'projectile');
assert.ok(ghola && atreidesCard && projectile);
const selected = [ghola,atreidesCard,projectile], dealOrder = selected.map((card,position) => {
const from = game.deck.findIndex(c => c.id === card.id); assert.ok(from >= position);
[game.deck[position],game.deck[from]] = [game.deck[from],game.deck[position]];
return {position,from,card:card.id};
+93
View File
@@ -0,0 +1,93 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import type { PlanClaim } from '../game/battle-promises';
import { homeworldRevivalActionBlock } from '../game/homeworld-revival-deployment-options';
import { createSalusaGholaTruthFixture } from './fixture-salusa-ghola-truth';
type Fixture = ReturnType<typeof createSalusaGholaTruthFixture>;
type Case = 'dial2' | 'dial1' | 'support';
const sources = new Map<Case, Fixture>();
function source(kind: Case): Fixture {
let fixture = sources.get(kind);
if (!fixture) {
const claim: PlanClaim = kind === 'support'
? { kind: 'support', compare: 'gte', value: 1 }
: { kind: 'dial', compare: 'eq', value: kind === 'dial1' ? 1 : 2 };
fixture = createSalusaGholaTruthFixture({ claim, answer: 'yes', inspectionDial: kind === 'dial1' ? 1 : 2 });
sources.set(kind, fixture);
}
return fixture;
}
const profiles = ['Easy', 'Medium', 'Hard', 'Brutal'] as const;
function ownPromise(game: Game, actor: string) {
const promise = game.battle?.truthPromises?.find(row => row.player === actor && !row.released);
assert.ok(promise, 'The actual answered Truthtrance promise remains live.');
return promise;
}
// These are shared control-adapter and minimal-policy checks over the earned
// native source. No Ghola amount selector or Battle Plan form is operated here;
// GameTable SSR is covered by the integration owner's separate runtime smoke.
void test('the exact owned Ghola group remains available when Salusa High preserves the native Yes dial2 promise', () => {
const f = source('dial2'), game = structuredClone(f.game), view = viewGame(game, f.actor);
assert.deepEqual(f.action, { type: 'card', card: f.card, amount: 1, elite: 1 });
assert.deepEqual(ownPromise(game, f.actor).claim, f.claim);
assert.equal(homeworldRevivalActionBlock(view, f.action), null);
assert.equal(view.ghola.available, true); assert.ok(view.ghola.cards.includes(f.card));
assert.equal(view.ghola.maxForces, 1); assert.equal(view.ghola.eliteRemaining, 1);
const returned = applyAction(game, f.actor, f.action);
assert.equal(returned.battle?.event, game.battle?.event);
assert.deepEqual(ownPromise(returned, f.actor).claim, f.claim);
assert.equal(ownPromise(returned, f.actor).answer, true);
assert.equal(viewGame(returned, f.actor).battle?.ownForces?.eliteFreeSupport, true);
assert.equal(returned.discard.filter(card => card.id === f.card).length, 1);
assert.equal(returned.players.find(player => player.id === f.actor)!.elites!.tanks, 0);
});
void test('the selected Ghola group block agrees with native rejection for an odd-dial or paid-support Yes promise', () => {
for (const kind of ['dial1', 'support'] as const) {
const f = source(kind), game = structuredClone(f.game), view = viewGame(game, f.actor);
const blocked = homeworldRevivalActionBlock(view, f.action);
assert.ok(blocked, `${kind}: the actual selected return needs a visible block.`);
assert.match(blocked, /Truthtrance|promise/i);
assert.ok(view.homeworldRevivalBlocks?.some(row => row.source === 'ghola' &&
row.amount === f.action.amount && row.elite === f.action.elite && row.reason === blocked));
assert.equal(view.ghola.maxForces, 0); assert.equal(view.ghola.eliteRemaining, 0);
const before = structuredClone(game);
assert.throws(() => applyAction(game, f.actor, f.action), error => {
assert.ok(error instanceof Error);
assert.equal(error.message, blocked);
return true;
});
assert.deepEqual(game, before);
assert.equal(ownPromise(game, f.actor).answer, true);
assert.ok(game.players.find(player => player.id === f.actor)!.hand.some(card => card.id === f.card));
}
});
void test('all four minimal profiles omit blocked Ghola and retain legal required plans before and after an allowed return', () => {
for (const kind of ['dial1', 'support', 'dial2'] as const) {
const f = source(kind);
const start = kind === 'dial2' ? applyAction(f.game, f.actor, f.action) : f.game;
for (const profile of profiles) {
const game = structuredClone(start), view = viewGame(game, f.actor);
view.players.find(player => player.id === f.actor)!.bot = profile;
const actions = botActions(view);
assert.ok(actions.length, `${kind}/${profile}: current legal participation remains available.`);
assert.ok(actions.every(action => action.type !== 'card' || action.card !== f.card),
`${kind}/${profile}: no blocked or already spent Ghola is offered.`);
const plans = actions.filter(action => action.type === 'battlePlan');
assert.ok(plans.length, `${kind}/${profile}: the live promise has a legal current plan.`);
for (const plan of plans) {
if (kind === 'support') assert.ok(Number(plan.support) >= 1);
else assert.equal(plan.dial, kind === 'dial1' ? 1 : 2);
if (kind === 'dial2') assert.equal(plan.support ?? 0, 0);
const sealed = applyAction(game, f.actor, plan);
assert.ok(sealed.battle?.plans[f.actor]);
assert.equal(ownPromise(sealed, f.actor).answer, true);
}
}
}
});
+127
View File
@@ -0,0 +1,127 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { applyAction, type Action, type Game } from '../game/engine';
import {
createSalusaGholaTruthFixture as fixture, actSalusaGholaTruth as act,
salusaGholaTruthPlayer as p, salusaGholaTruthInventory as inventory,
salusaGholaTruthPlan as plan, finishSalusaGholaTruthBattle as finish,
type SalusaGholaTruthFixture,
} from './fixture-salusa-ghola-truth';
import { salusaGholaArmy as army } from './fixture-salusa-ghola';
function reject(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => applyAction(game,actor,action));
assert.deepEqual(game,before);
}
function binding(game: Game, f: SalusaGholaTruthFixture) {
assert.deepEqual(game.battle!.truthPromises,[{player:f.actor,asker:f.opponent,
claim:f.claim,answer:f.answer === 'yes'}]);
assert.equal(game.discard.filter(c => c.id === f.truthCard).length,1);
assert.equal(game.truthHistory!.at(-1)!.answer,f.answer);
assert.deepEqual(game.truthHistory!.at(-1)!.question,f.question);
assert.deepEqual(inventory(game),f.base.original);
}
function done(game: Game, f: SalusaGholaTruthFixture, gholaUsed: boolean) {
assert.equal(game.battle,null);
assert.equal(game.lastBattleContext!.winner,f.actor);
assert.equal(game.discard.filter(c => c.id === f.truthCard).length,1);
assert.equal(game.discard.filter(c => c.id === f.card).length,Number(gholaUsed));
assert.equal(game.truthHistory!.at(-1)!.answer,f.answer);
assert.deepEqual(game.truthHistory!.at(-1)!.question,f.question);
assert.deepEqual(inventory(game),f.base.original);
}
void test('earned native Truth Yes dial2 permits Salusa Ghola and preserves its original binding through the actual plan', () => {
const f = fixture();
assert.equal(p(f.base.afterSetup,f.opponent).hand[0].id,f.truthCard);
assert.equal(f.base.beforeDeal.players.every(p => p.hand.length === 0),true);
assert.equal(f.truthActions.filter(s => s.action.type === 'card').length,1);
assert.equal(f.truthActions.filter(s => s.action.type === 'truthAsk').length,1);
assert.equal(f.truthActions.filter(s => s.action.type === 'truthAnswer').length,1);
assert.equal(f.sourceCount,f.sourceActions.length);
assert.deepEqual(f.game.battle!.plans,{});
binding(f.game,f);
const game = act(structuredClone(f.game),f.actor,f.action);
assert.deepEqual(army(game),{normal:0,elite:2});
assert.equal(p(game).elites!.tanks,0);
binding(game,f);
const result = finish(f,game);
assert.equal(result.afterPlan.battle!.plans[f.actor].dial,2);
assert.equal(result.afterPlan.battle!.plans[f.actor].support,0);
assert.equal(result.revealed.battle!.truthPromises![0].released,undefined);
done(result.game,f,true);
});
void test('earned native Truth No dial1 remains feasible after Salusa Ghola without releasing its aggregate answer', () => {
const f = fixture({claim:{kind:'dial',compare:'eq',value:1},answer:'no'});
const game = act(structuredClone(f.game),f.actor,f.action);
assert.deepEqual(army(game),{normal:0,elite:2});
binding(game,f);
const result = finish(f,game,plan(game,2,0));
assert.equal(result.revealed.battle!.truthPromises![0].answer,false);
assert.equal(result.revealed.battle!.truthPromises![0].released,undefined);
done(result.game,f,true);
});
void test('earned native Truth Yes odd1 rejects Salusa Ghola before cost and can finish dial1 before later printed use', () => {
const f = fixture({claim:{kind:'dial',compare:'eq',value:1}});
binding(f.game,f);
reject(f.game,f.actor,f.action);
assert.deepEqual(army(f.game),{normal:0,elite:1});
assert.equal(p(f.game).elites!.tanks,1);
assert.ok(p(f.game).hand.some(c => c.id === f.card));
assert.equal(f.game.discard.some(c => c.id === f.card),false);
const result = finish(f,structuredClone(f.game),plan(f.game,1,0));
assert.equal(result.afterPlan.battle!.plans[f.actor].dial,1);
done(result.game,f,false);
const returned = act(result.game,f.actor,f.action);
assert.deepEqual(army(returned),{normal:0,elite:2});
assert.equal(returned.discard.filter(c => c.id === f.card).length,1);
assert.deepEqual(inventory(returned),f.base.original);
});
void test('earned native Truth Yes paid support rejects Salusa Ghola while the original supported plan stays legal', () => {
const f = fixture({claim:{kind:'support',compare:'gte',value:1}});
binding(f.game,f);
reject(f.game,f.actor,f.action);
assert.deepEqual(army(f.game),{normal:0,elite:1});
assert.equal(p(f.game).elites!.tanks,1);
const result = finish(f,structuredClone(f.game),plan(f.game,2,1));
assert.equal(result.afterPlan.battle!.plans[f.actor].support,1);
assert.equal(result.afterPlan.battle!.plans[f.actor].dial,2);
done(result.game,f,false);
});
void test('earned grouped Truth Yes odd1 or even2 keeps its live even alternative after Salusa Ghola', () => {
const f = fixture({claim:{kind:'or',terms:[
{kind:'dial',compare:'eq',value:1},{kind:'dial',compare:'eq',value:2},
]}});
const game = act(structuredClone(f.game),f.actor,f.action);
binding(game,f);
assert.deepEqual(army(game),{normal:0,elite:2});
const result = finish(f,game,plan(game,2,0));
assert.deepEqual(result.revealed.battle!.truthPromises![0].claim,f.claim);
assert.equal(result.revealed.battle!.truthPromises![0].released,undefined);
done(result.game,f,true);
});
void test('earned Prescience odd1 and native Truth Yes even2 can renew the inspection through actual Salusa Ghola', () => {
const f = fixture({inspectionDial:1});
assert.ok(f.base.inspection);
assert.equal(f.base.inspection.answered.battle!.prescience!.value,1);
assert.equal(f.game.battle!.prescience!.value,1);
binding(f.game,f);
let game = act(structuredClone(f.game),f.actor,f.action);
assert.deepEqual(army(game),{normal:0,elite:2});
binding(game,f);
assert.equal(Object.hasOwn(game.battle!.prescience!,'value'),false);
assert.equal(game.battle!.preparation?.kind,'prescienceAnswer');
assert.equal(game.battle!.preparation?.owner,f.actor);
reject(game,f.actor,{type:'prescienceAnswer',value:1});
game = act(game,f.actor,{type:'prescienceAnswer',value:2});
assert.equal(game.battle!.prescience!.value,2);
binding(game,f);
const result = finish(f,game,plan(game,2,0));
done(result.game,f,true);
});