Respect individual Bureaucrat payment thresholds and correct Homeworld evidence

This commit is contained in:
Quintine committed 2026-10-13 00:12:10 +11:00
1 parent 38feaf17ba
commit 68f7a101a3
11 files changed
+318 -104

No files matched your search

+33 -9
View File
@@ -87,15 +87,39 @@ available for a later qualifying payment in that phase.
Existing auction, shipment and bribe paths have public payment amounts; the
negotiated revival price remains limited to the participating seats and trainer.
Qualifying allied funding,
including an aggregate such as three plus three and a payment funded entirely by
the ally, is guarded before costs or commitment. It needs separate payment-unit
and privacy work, as does split-recipient Homeworld income. Independent Karama
shipments paid entirely to the Bank bypass this player-income guard. Do not infer
eligibility from an aggregate that silently combines payers or reveals concealed
funding. A successful use is retained for the physical
card's turn and phase, including a same-phase ownership change. That physical
card accounting is an explicit implementation inference.
Allied funding is evaluated per actual payer, not by adding different payers'
shares to create the five-spice trigger. A three-plus-three payment, or
four-plus-three, has no eligible payer and proceeds through original funding,
payment, card/force commitment and recipient income without a Bureaucrat offer.
Individually qualifying private or split payments (including a payment funded
entirely by a qualifying ally) still need payment-unit/privacy integration and
remain guarded before costs. Split-recipient Homeworld income stays separate.
Independent Karama shipments paid entirely to the Bank bypass player-income
guards. A successful use remains tied to the physical card's turn and phase,
including same-phase ownership changes; that accounting is an implementation
inference.
## Individual allied-payer threshold — 13 October 2026
The shared contributor check previously aggregated all payer shares and
rejected three-plus-three before commitment. The card's threshold names
one paying player; separate below-threshold costs are not one six-spice
payment by that player. The check now preserves that individual threshold
without opening the unresolved qualifying split-payment reaction.
Real Basic and Advanced skill setup trains Bureaucrat; a real turn-two worm
forms the Harkonnen/Fremen alliance. Conserved starting counters and undealt
Spice Card ordering isolate the payments; no wallet, phase, alliance or ledger
is assigned. Both before shipment programs rejected valid three-plus-three.
Afterward, Basic/Advanced shipment and ordinary auction programs preserve the
six-spice debit and full Guild/Emperor receipt. Eight cases in
`tests/bureaucrat-split-threshold.test.ts` cover three/four personal spice plus
three allied, ordinary acquisition and the remaining five-plus-three guard.
The actual390px Basic controls pledge Fremen's three,
ship six counters into Habbanya Sietch17 for three personal plus three allied,
credit Guild5→11 and finish the original Movement turn. Initial Vite optimizer
request aborts and cosmetic action notices are recorded; this is local proof,
not deployed or full-family acceptance.
## Saved choices and verification
+39 -18
View File
@@ -29,6 +29,29 @@ optional skill bands, combinations and public expansion starts remain open.
Rules implementation continues; comprehensive assurance and administration
remain deferred until all rules are implemented.
**Bureaucrat individual-payer threshold and evidence correction — 13 October.**
Separate allied shares below five are no longer summed to create Bureaucrat
eligibility. Basic/Advanced native setup and an actual worm alliance reproduce
the rejected three-plus-three shipment; four post-cutover shipment/auction
programs retain the original six-spice payment and full recipient income.
Eight cases cover three/four personal plus three allied and the still-guarded
five-plus-three contribution. The actual390px Basic controls pledge three,
ship six counters for three personal plus three allied and finish Movement.
This does not open individually qualifying private/split-payment reactions.
The Homeworld Box, Distrans and Truthtrance controls now have six actual390px
claim/pass flows, including real paid search/transfer/question controls and
original Movement completion. The evidence bundle is
`/tmp/dune-homeworld-controls-proof-3212.json`; it is local, not deployed or
saved-room acceptance. Earlier summaries wrongly claimed these UI runs and
pre-cutover Box proof; the feature log now distinguishes observed evidence.
The unrelated shared parent-signature augmentation is removed. It had changed
other Homeworld receipt bytes; no historical-save certification follows.
The frozen batch passed types, lint and135/135 across13 unit files. Eight
native bot programs (both modes × four profiles) complete shipment without
rejected candidates; Hard/Brutal actually use below-threshold allied shares.
**Semuta / Homeworld Truthtrance reaction — 11 October.**
`truthtranceDiscard` is now admitted in the same envelope, so a definite
Truthtrance answer offers the consumed card. A genuine original Homeworld
@@ -48,32 +71,30 @@ occupation authority and original Movement are unchanged. The claim/pass
pair and all four legal bots pass alongside the existing Homeworld cases.
**Semuta / Homeworld paid Box reaction — 11 October.**
The same envelope also admitted no paid Nullentropy Box reaction, so a
completed Box search produced no Semuta offer. `semutaOfferSupported` now
admits `nullentropyDiscard` there as well, and the shared parent signature
binds Homeworld custody, occupation history and the percentage ledger when a
Homeworld module is present; non-Homeworld continuations keep their existing
shape. A genuine original Homeworld state runs a real paid Box search
(two-spice payment, shuffled pile, used Box on top) and then offers the used
Box. Claim recovers the exact Box, pass leaves it discarded; the payment,
searched card, occupation authority and original Movement are unchanged.
One before program had no offer; the claim/pass pair and all four legal bots
pass. Other Homeworld producers remain guarded.
`semutaOfferSupported` admits `nullentropyDiscard` in this envelope.
A real post-cutover singleton search pays two spice, recovers the selected
card and places the used Box on top before offering Semuta. Claim recovers
the exact Box; pass leaves it discarded. No pre-cutover Box program was run;
the previous absence claim was inferred from the gate. The unrelated shared
parent-signature augmentation is removed by the subsequent correction:
it changed other Homeworld receipts and was unnecessary for admission.
The final Box checkpoint passed 107/107 across ten files with types/lint;
other producers remain guarded.
**Semuta / Homeworld ordinary-card reaction — 11 October.**
The Homeworld Semuta envelope admitted only three producers, so a used
ordinary special card played there produced no reaction even though the
printed card has no module restriction. `semutaOfferSupported` now also
admits `ordinaryCardDiscard` in that roster; its existing signature already
binds Homeworld custody, occupation history and percentage ledger, so no new
source identity or payment rule was added. A genuine original Homeworld
admits `ordinaryCardDiscard` in that roster; its existing effect signature
includes Homeworlds/revival state, not the separate history/percentage fields.
No new payment rule was added. A genuine original Homeworld
occupation state at an active Movement owner plays Hajr, commits its
movement grant, then offers the used card. Claim recovers it, pass leaves it
discarded; wallets, custody, occupation history, percentage ledger and the
Hajr grant are unchanged and original Movement still finishes. One before
program had no offer; four claim/pass and all four legal bot profiles pass;
seven consumer cases and the frozen six-file union pass with types/lint
clean. Other Homeworld producers remain guarded.
Hajr grant are unchanged and original Movement still finishes. The before
program printed no offer, then failed an incidental null/undefined assertion.
Four post-cutover programs, seven new cases and the six-file union77/77
passed with types/lint clean. The actual390px claim and finish were exercised.
**Tleilaxu Ambassador first-death Kwisatz Haderach — 11 October.**
The source-supported separate KH identity (base p17) now appears in the
+75 -26
View File
@@ -1,27 +1,73 @@
# Dune implementation status
## 13 October 2026 — individual Bureaucrat payer threshold; evidence correction
The printed skill names one other payer's payment of at least five spice.
`checkBureaucratContributors` instead aggregated different payer shares,
rejecting three-plus-three before shipment or bid commitment. It now enters
the existing integration guard only when an eligible individual payer reaches
five. Private/split payments with a qualifying individual still retain that
guard; no reaction, recipient split or private-funding convention is invented.
The new fixture uses real original all14 Skills setup, training and a real
turn-two worm alliance. Conserved counters and undealt Spice Card ordering
isolate payment without assigning wallets, alliances, phase or ledger state.
Two valid Basic/Advanced shipment programs rejected before the change.
Four post-cutover shipment and auction programs charge personal three and
allied three, credit the full six to Guild/Emperor, preserve the physical
shipment/acquisition and keep Bureaucrat unused. Eight cases also cover
four-plus-three and rejection before mutation for five-plus-three.
The older aggregate-three-plus-three rejection expectation is removed.
The actual390px Basic table pledges Fremen's three native spice, ships six
ordinary counters into Habbanya Sietch17, credits Guild5→11 and finishes
Harkonnen's original Movement. Four initial optimizer requests aborted;
the controls subsequently worked. No deployed or full-family claim.
Separately, the Box/Distrans/Truthtrance Homeworld UI proof is now real rather
than retroactively attributed to earlier checkpoints. Six390px claim/pass
flows use actual paid Box search, private Distrans transfer, Truthtrance
declaration/priority/question/answer and original Movement completion.
The proof bundle `/tmp/dune-homeworld-controls-proof-3212.json` retains fresh
AX, screenshots, accepted actions and observed state outside Git. Parent
inspected the three claimed-card screenshots. Local prebuilt native fixture
progression is not deployed, persisted-room or natural full-game proof.
The shared parent-signature Homeworld augmentation is removed: it changed
unrelated receipt bytes and was unnecessary to enable the new discard
producers. Previous claims about unchanged in-flight receipts and already
signed occupation-history fields were incorrect. No historical-save or
intermediate-checkpoint receipt certification is asserted.
Verification: TypeScript passed; the first lint run found an unused mock
import and a never-reassigned setup variable, both removed. The final lint
and13-file unit union passed135/135. Eight native bot programs (both modes ×
four profiles) complete shipment with zero rejected candidates. A real bribe
reduces the payer's native ten spice to three, its original Bureaucrat choice
is declined, then Fremen pledges three. Hard/Brutal use actual three-plus-one
or three-plus-two funding; the normal Advanced Guild allowance finishes before
checking committed forces/payment. No strategy or difficulty tuning changed.
## 11 October 2026 — Homeworld Truthtrance Semuta reaction
`truthtranceDiscard` is now admitted in the same Homeworld envelope, so a
definite Truthtrance answer offers the consumed card. Its signature already
binds the committed answer record, queue remainder and the shared parent
signature (Homeworld-aware when the module is present).
binds the committed answer record, queue remainder and suspended controls.
A genuine original Homeworld state declares Truthtrance, passes storm
priority and answers a real held-card fact; the consumed card is then the
fresh batch. Claim recovers the exact Truthtrance, pass leaves it discarded,
and the published answer history, wallets, occupation authority and original
Movement are unchanged; the answer cannot be replayed. The claim/pass pair
and all four legal bot profiles pass. Twenty-five consumer cases and the
frozen six-file union pass with types/lint. Battle, Ornithopter, Sapho and
other Homeworld producers remain guarded.
and all four legal bot profiles pass. The final checkpoint ran 25 cases in
the new file and 140/140 across 11 affected files, with types/lint clean.
Battle, Ornithopter, Sapho and other Homeworld producers remain guarded.
## 11 October 2026 — Homeworld Distrans Semuta reaction
`distransDiscard` is now admitted in the same Homeworld envelope, so a
completed private Distrans transfer offers its used card. The continuation
already binds both hands and the shared parent signature (now Homeworld-aware
when the module is present), so no new custody or price rule was added.
already binds both hands and suspended controls; no new custody or price
rule was required.
A genuine original Homeworld state plays Distrans, delivers the chosen private
card to a real seat, and offers the used Distrans. The transferred card is
@@ -29,20 +75,19 @@ never a candidate; the gift has already completed. Claim recovers the exact
Distrans, pass leaves it discarded, and the gift, wallets, occupation
authority and original Movement are unchanged; replaying the transfer is
rejected. The claim/pass pair and all four legal bot profiles pass alongside
the existing Homeworld cases. Nineteen consumer cases and the frozen
six-file union pass with types/lint. Truthtrance, battle and other Homeworld
producers remain guarded.
the existing Homeworld cases. The final checkpoint ran 19 cases in the new
file and 113/113 across 10 affected files, with types/lint clean. Truthtrance
was still excluded at that checkpoint.
## 11 October 2026 — Homeworld paid Box Semuta reaction
The Homeworld envelope also admitted no paid Nullentropy Box reaction, so a
completed Box search in that roster produced no Semuta offer.
`semutaOfferSupported` now admits `nullentropyDiscard` there, and the shared
`nullentropyParentSignature` additionally binds Homeworld custody, occupation
history and the percentage ledger when `g.homeworlds` is present. Non-Homeworld
continuations keep their previous signature shape, so no existing saved or
in-flight receipt changes meaning.
`semutaOfferSupported` now admits `nullentropyDiscard` there. The checkpoint
also added Homeworld state to the unrelated shared parent signature. That
addition changed other Homeworld receipt bytes, so the claim that no existing
receipt changed was incorrect; the subsequent correction removes it.
A genuine original Homeworld state runs a real paid Box search: two
uncommitted spice are paid, the pile is shuffled, the selected card is
@@ -50,9 +95,11 @@ recovered and the used Box is placed on top. Semuta is then offered for that
used Box. Claim recovers the exact Box; pass leaves it discarded. The payment,
the searched card, Homeworld custody, occupation history, the percentage
ledger and the remaining Movement are unchanged, and the Box cannot be
searched again. The before program produced no offer; the claim/pass pair and
all four legal bot profiles pass. Thirteen consumer cases and the frozen
six-file union pass with types/lint. Other Homeworld producers remain guarded.
searched again by its former owner. No pre-cutover Box program was run:
the absence was inferred from the gate, not observed. The real post-cutover
program showed a singleton search completing automatically before Semuta.
The final checkpoint ran 13 cases in the new file and 107/107 across 10
affected files, with types/lint clean. Other producers remain guarded.
## 11 October 2026 — Homeworld ordinary-card Semuta reaction
@@ -63,20 +110,22 @@ card carries no module restriction; the exclusion was an implementation
boundary of the new envelope.
`semutaOfferSupported` now also admits `ordinaryCardDiscard` when the
Homeworld roster matches. That continuation's existing signature already
binds Homeworld custody, occupation history and the percentage ledger, so no
new source identity, price or economic rule was introduced. Ordinary resume
already restores the saved response/decision/pending-Karama/opening.
Homeworld roster matches. The existing effect signature includes Homeworlds
and revival state, but not the separate occupation-history/percentage fields;
the earlier claim that those fields were already signed was incorrect.
Ordinary resume restores saved response/decision/pending-Karama/opening.
A genuine original Homeworld occupation state at an active Movement owner
plays Hajr: its movement grant commits, the used card becomes the fresh
batch, and Semuta is offered. Claim recovers the exact used card; pass leaves
it discarded. Wallets, Homeworld custody, occupation history, percentage
ledger, the Hajr grant and the remaining Movement are unchanged; a second
play and a repeated commit are rejected. The before program produced no
offer; four claim/pass and all four legal bot profiles pass. Seven consumer
cases and the frozen six-file union pass with types/lint. Other Homeworld
producers, Nexus/Skills/module mixtures and public starts remain guarded.
play and a repeated commit are rejected. The before program printed no offer,
then failed an incidental null-versus-undefined assertion; it was not a green
before check. Four post-cutover programs and seven consumer cases passed.
The final checkpoint passed 77/77 across six files with types/lint clean;
the actual 390px Hajr claim and original Movement finish were exercised.
Other producers, module mixtures and public starts remain guarded.
## 11 October 2026 — source-supported first-death KH Ambassador return
+1 -1
View File
@@ -400,7 +400,7 @@ dedicated retrieved worked FAQs. [Contract and remaining limits](HOMEWORLD_OCCUP
| Smuggler shipping | Optional free companion is included in the actual total; apply Guild rounding to the reduced priced count. Ordinary Fremen reserves are on planet. The officially permitted owned No-Field plus a separate free force is now prototyped at the unchanged one-marker price. Legacy unstamped ordinary declarations keep their quoted full price. | [Ordinary contract](SMUGGLER_SHIPMENT.md), [No-Field combination](SMUGGLER_NO_FIELD.md) |
| Sandmaster movement | One optional board spice per distinct entered territory; source-only repositioning earns none. Per-movement Hajr reset is a documented grammatical inference. Canceled moves earn nothing. Native Fremen worm rides and physical Fremen Ambassador relocations collect optionally at the destination before arrival reactions, without an invented intermediate route. Owned one-pile-per-territory allocation now covers ground, direct destinations and native HMS relocation; sector allocation remains provisional and concealed No-Field movement stays separate. | [Ground/direct/HMS relocation](SANDMASTER_MOVEMENT.md), [worm rides](SANDMASTER_WORM.md) |
| Diplomat defense | A committed Worthless copies the opponent's canonical printed defensive role: base/Ix Shield/Snooper, hybrid, defensive Weirding/Chemistry or sealed Portable. Named combat distinctions remain. Copying the used role, not activation or own-slot prerequisites, is a visibly qualified first version. Reveal-before-traitor timing and mandatory disposal remain implementation interpretations; existing base-only battle windows are not reopened. Late/mirror/numeric modifications remain separate. | [Source and connected prototype](DIPLOMAT_DEFENSE.md) |
| Bureaucrat payments | Once per phase, optionally divert two from an actual third-party payment of at least five; payer cost is unchanged. Bribes trigger at transfer to the recipient's shield, not later collection. Native income is offered after Karama allowance as an explicit outcome-based inference; split contributions, split recipients and remaining payment families need further integration. | [Source and connected boundary](BUREAUCRAT_PAYMENTS.md) |
| Bureaucrat payments | Once per phase, optionally divert two when one third-party payer pays at least five; payer cost is unchanged. Different below-five allied shares are not aggregated to create eligibility and proceed without an offer. Bribes trigger at transfer to the recipient's shield, not later collection. Native income follows Karama allowance as an explicit outcome-based inference; individually qualifying split contributions, split recipients and remaining payment families need further integration. | [Source and connected boundary](BUREAUCRAT_PAYMENTS.md) |
| Leader battle effects | Rihani normal peek and reshuffle precede a separate committed optional draw; ordering is an explicit printed-order inference. Unused excludes actual calls, not mere disclosure. Mentat/Bureaucrat change scores; Sandmaster adds board spice. Owned multi-pile Smuggler debits and Sandmaster victory placement are connected under provisional allocation policies; modifier and exceptional Face Dancer composition remain separate. | [Source contract and prototype](LEADER_BATTLE_EFFECTS.md), [Smuggler allocation](SMUGGLER_BATTLE.md) |
| Source and readiness | Separate published rules, explicit implementation choices and unfinished interpretations; Advanced/expansion gates stay closed. | [Base/Advanced audit](BASE_ADVANCED_READINESS_20260907.md), [checklist](../game/reference.ts) |
| Setup and Fremen movement | Preserve source-ordered setup and the distinct declared-movement cancellation; do not recreate fixed historical gaps. | [Setup contract](ADVANCED_SETUP_TEST_SEAM.md), [Fremen Karama](FREMEN_MOVEMENT_KARAMA_RULES.md) |
+5 -3
View File
@@ -862,9 +862,11 @@ card of a completed paid Nullentropy Box search, not only paid Kaitain
disposal, Tupile cleanup and printed Karama canceling income. The used card is the same public fresh batch; claim recovers it and
pass leaves it discarded, with Homeworld custody, occupation history, the
percentage ledger, wallets, the two-spice Box payment and the card's own
committed effect unchanged. The shared parent signature additionally binds
Homeworld state when that module is present. One before program lacked the
offer; the claim/pass pair and all four legal bots pass. Other Homeworld
committed effect unchanged. The shared parent-signature augmentation was
unnecessary for this admission and changed other Homeworld receipt bytes;
the correction removes it. The Hajr before program printed no offer then
failed an incidental null/undefined assertion. Box, Distrans and Truthtrance
had post-cutover programs but no observed pre-cutover run. Other Homeworld
producers remain guarded, not prohibited by a newly found printed rule.
**11 October 2026 — native paid-income cancellation costs.** Printed Karama
+2 -9
View File
@@ -4947,14 +4947,7 @@ function nullentropyParentSignature(
)
.sort(([a], [b]) => a.localeCompare(b)),
);
return JSON.stringify({
searchEvent, resume, parents,
...(g.homeworlds ? {
homeworlds: g.homeworlds,
occupation: g.homeworldOccupationHistory,
percentages: g.homeworldOccupiedPercentageLedger,
} : {}),
});
return JSON.stringify({ searchEvent, resume, parents });
}
type OrdinaryDiscardContinuation = Extract<
NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
@@ -8442,7 +8435,7 @@ function checkBureaucratContributors(g: Game,payee:string|undefined,shares:{paye
const paid = shares.filter(share => share.amount > 0);
const owner = nativeBureaucrat(g)?.owner;
if (!owner || owner === payee || bureaucratUsed(g.bureaucratPayments?.used ?? [],g.turn,g.phase) ||
paid.reduce((total,share) => total+share.amount,0) < 5 || !paid.some(share => share.payer !== owner && share.payer !== payee)) return;
!paid.some(share => share.amount >= 5 && share.payer !== owner && share.payer !== payee)) return;
requireRule(bureaucratPaymentModeSupported(g),'Bureaucrat with other optional modules is still being integrated.');
requireRule(paid.length === 1 && !privateFunding,'Bureaucrat on allied funding or multiple payer contributions is awaiting private payment-split integration.');
}
+1 -1
View File
@@ -4778,7 +4778,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Spice Banker can commit one through three own spice with its Battle Plan, separate from force support. Only the surviving selected skilled disc gains that strength. The amount stays private until authorized plan inspection or public reveal. It is paid on win or loss, including leader death, except a sole traitor-call winner spends nothing. Normal deferred income connects only in the separate fresh classic or supported native banker-income profiles, including the bounded Advanced Moritani roster; ordinary/default starts are unchanged.',
'Diplomat may use one committed Worthless as the opponent’s canonical printed defensive role when you played no actual defense: Shield, Snooper, Shield Snooper, defensive Weirding Way or Chemistry, or sealed Portable Snooper. Hybrid protection, Weirding’s non-Shield behavior and Chemistry’s Poison Tooth exception remain distinct. Copying the used role without its activation or own-slot prerequisite is the explicit first-version interpretation. Choose or decline after reveal, before traitors; the Worthless must be discarded even if you win. Already-open base-only battles keep that source window. A native Diplomat can protect its own selected disc; a captive grants no upper band. A surviving trained loser may retreat selected undialed physical counters up to printed/copied disc strength to one empty adjacent non-stronghold before losses. Late/mirror/numeric modifications remain separate.',
'Normal Mentat questions use the user-approved uniform private card-show step before posture and faction powers in supported Leader Skills games. The opponent shows the named held weapon or chooses another held card, even for an exact match or sole eligible card. Weirding Way is a nameable default weapon; Chemistry is not. Cards stay held and need not enter a Battle Plan. New battles activate the question without an extra opt-in; already-opened unversioned battles retain their original flow. Empty hands skip without an answer. Private history records an observation, not a later hand guarantee.',
'Bureaucrat may redirect two spice from a payment of at least five between two other players, once per phase. The payer still pays in full; the recipient receives two less. Declining keeps the use available for a later payment. Paid bribes qualify when transferred, with their remaining share unavailable until Mentat Pause. Supported auctions, Guild income, Richese sales and paid Tleilaxu revival have connected paths; a separate free-revival award does not make a smaller price qualify. By user ruling, Emperor gifts are not payments: no Bureaucrat prompt/use or threshold disclosure, and exact gift amounts remain private. Split funding and other payment-family/module interactions remain unfinished.',
'Bureaucrat may redirect two spice when one other player pays at least five to a player other than the skill owner, once per phase. The payer still pays in full; the recipient receives two less. Declining keeps the use available for a later payment. Paid bribes qualify when transferred, with their remaining share unavailable until Mentat Pause. Supported auctions, Guild income, Richese sales and paid Tleilaxu revival have connected paths; a separate free-revival award does not make a smaller price qualify. Allied payer shares below five each, such as three plus three, do not qualify merely because their total exceeds five and may proceed through original payment controls. Individually qualifying split funding remains guarded. By user ruling, Emperor gifts are not payments: no Bureaucrat prompt/use or threshold disclosure, and exact gift amounts remain private. Other payment-family/module interactions remain unfinished.',
'Smuggler records all existing positive battle-territory piles when plans reveal, capped in total by the selected leader’s unmodified strength. A surviving controller collects even when losing or no forces remain. A partial collection across several piles asks that owner for exact sector amounts; all/none and single-pile collection stay automatic. This allocation policy is provisional. Pending spice cannot fund the plan. Collection precedes Sandmaster addition; a drained pile receives none. Modified strength and other combined-module limits remain guarded. Older saved battles retain their recorded behavior.',
'In classic and supported E1/E2 native Tech composition, actual Smuggler prices and normal Banker qualifying fees remain distinct from phase-end Tech income. Tech piles cannot fund their triggering action; Banker front-shield grants still wait for actual Mentat. Winning physical Suk rescue and Sandmaster board spice precede original played-card cleanup and mandatory winner token reward. One token transfers automatically, several require a selection, and no token skips without an invented decline. A later Face Dancer retains the original winner’s cards, bounty and Tech reward, kills the matching trainer without another bounty and replaces only actual survivors from real sources. Native Tleilaxu-only free revival excludes Axlotl; CHOAM initial income is not Charity. Richese’s free companion does not change its marker price or create the concealed counters early.',
'Physical Fremen Ambassador relocation now offers optional Sandmaster destination collection before arrival reactions, without an invented route or ordinary movement use. Same-territory repositioning earns none; concealed relocation and other special movement still retain their own limits. Normal Banker income has separate fresh classic or supported native source-to-Mentat previews with provisional original-earned-faction custody, not general activation. Diplomat retreat retains its documented conservative outcome order. Native Advanced foreign gholas/Face Dancers, skill-first Moritani assassination and ordinary-disc Ecaz skills follow their bounded contracts; shared-Duke assignment, combined Occupy skills and other combined modules remain unfinished. This prototype does not enable public module starts.',
+5 -22
View File
@@ -1,5 +1,5 @@
import assert from 'node:assert/strict';
import test, { mock } from 'node:test';
import test from 'node:test';
import {
applyAction,
normalizeAutomaticGame,
@@ -294,37 +294,20 @@ void test('a real simultaneous Bene Gesserit advisor choice survives the deferre
assert.deepEqual(done.decision, original);
assert.equal(bureaucratPlayer(done, 'g').spice, 33);
});
void test('aggregate 3+3 and fully allied-funded qualifying payments reject before cost, force/card commitment or RNG', () => {
for (const allyPayment of [1, 3, 6]) {
void test('individually qualifying mixed or fully allied-funded payments retain the integration guard before commitment', () => {
for (const allyPayment of [1, 6]) {
let g = bureaucratPaymentGame();
bureaucratPlayer(g, 'p').ally = 'e';
bureaucratPlayer(g, 'e').ally = 'p';
g = applyAction(g, 'e', { type: 'pledgeAid', amount: 6 });
mock.method(globalThis.crypto, 'randomUUID', () => {
throw new Error('Unexpected event before payment preflight');
});
try {
reject(
g,
'p',
{ ...ship, amount: 6, allyPayment },
/private payment-split/,
);
} finally {
mock.restoreAll();
}
reject(g, 'p', { ...ship, amount: 6, allyPayment });
g = bureaucratPaymentGame();
bureaucratPlayer(g, 'p').ally = 'g';
bureaucratPlayer(g, 'g').ally = 'p';
g = beginBureaucratAuction(g);
g = applyAction(g, 'g', { type: 'pledgeAid', amount: 6 });
g = applyAction(g, 'b', { type: 'passBid' });
reject(
g,
'p',
{ type: 'bid', amount: 6, allyPayment },
/private payment-split/,
);
reject(g, 'p', { type: 'bid', amount: 6, allyPayment });
}
});
void test('saved Bureaucrat receipt, source, unique decision, continuation and phase-use mutations reject immutably before reads', () => {
+80
View File
@@ -0,0 +1,80 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { applyAction, viewGame, type Game } from '../game/engine';
import { nextClassicNexusSkillsPaymentsNativeStep } from './fixture-classic-nexus-skills-payments';
import { createBureaucratSplitThresholdFixture, advanceBureaucratThresholdToMovement } from './fixture-bureaucrat-split-threshold';
function settle(state: Game): Game {
let game = state;
for (let n = 0; (game.response || game.decision) && n < 40; n++) {
const next = nextClassicNexusSkillsPaymentsNativeStep(game);
assert.ok(next);
game = applyAction(game, next.actor, next.action);
}
assert.ok(!game.response && !game.decision);
return game;
}
const player = (game: Game, id: string) => game.players.find(p => p.id === id)!;
for (const advanced of [false, true]) {
for (const personal of [3, 4]) void test(`${advanced ? 'Advanced' : 'Basic'} shipment preserves ${personal}+3 allied payments below Bureaucrat’s individual threshold`, () => {
const f = createBureaucratSplitThresholdFixture(advanced);
let game = advanceBureaucratThresholdToMovement(f.game, f.payer);
const before = structuredClone(game);
game = applyAction(game, f.donor, { type: 'pledgeAid', amount: 3 });
const amount = personal + 3, key = 'habbanya_ridge_sietch:17';
const paid = settle(applyAction(game, f.payer, {
type: 'ship', territory: 'habbanya_ridge_sietch', sector: 17, amount, allyPayment: 3,
}));
assert.equal(player(paid, f.payer).spice, player(before, f.payer).spice - personal);
assert.equal(player(paid, f.donor).spice, player(before, f.donor).spice - 3);
assert.equal(player(paid, f.guild).spice, player(before, f.guild).spice + amount);
assert.equal(player(paid, f.trainer).spice, player(before, f.trainer).spice);
assert.equal(player(paid, f.payer).forces[key], amount);
assert.equal(player(paid, f.payer).reserves, player(before, f.payer).reserves - amount);
assert.equal(paid.aid[f.donor]?.amount ?? 0, 0);
assert.equal(viewGame(paid, f.trainer).bureaucrat.usedThisPhase, false);
assert.throws(() => applyAction(paid, f.payer, {
type: 'ship', territory: 'habbanya_ridge_sietch', sector: 17, amount, allyPayment: 3,
}));
const finished = applyAction(paid, f.payer, { type: 'endMovement' });
assert.ok(!finished.movementRemaining?.includes(f.payer));
});
void test(`${advanced ? 'Advanced' : 'Basic'} auction preserves separate three-spice payer costs and full Emperor income`, () => {
const f = createBureaucratSplitThresholdFixture(advanced);
let game = f.game;
while (game.auction!.active !== f.payer) {
const next = nextClassicNexusSkillsPaymentsNativeStep(game); assert.ok(next);
game = applyAction(game, next.actor, next.action);
}
const card = game.auction!.cards[game.auction!.index].id;
const before = structuredClone(game);
game = applyAction(game, f.donor, { type: 'pledgeAid', amount: 3 });
game = applyAction(game, f.payer, { type: 'bid', amount: 6, allyPayment: 3 });
for (let n = 0; !player(game, f.payer).hand.some(c => c.id === card) && n < 40; n++) {
const next = nextClassicNexusSkillsPaymentsNativeStep(game); assert.ok(next);
game = applyAction(game, next.actor, next.action);
}
const paid = settle(game);
assert.ok(player(paid, f.payer).hand.some(c => c.id === card));
assert.equal(player(paid, f.payer).spice, player(before, f.payer).spice - 3);
assert.equal(player(paid, f.donor).spice, player(before, f.donor).spice - 3);
assert.equal(player(paid, f.emperor).spice, player(before, f.emperor).spice + 6);
assert.equal(player(paid, f.trainer).spice, player(before, f.trainer).spice);
assert.equal(viewGame(paid, f.trainer).bureaucrat.usedThisPhase, false);
assert.equal(paid.auction!.index, before.auction!.index + 1);
assert.ok(!paid.auction!.cards.slice(paid.auction!.index).some(c => c.id === card));
});
void test(`${advanced ? 'Advanced' : 'Basic'} an individual five-spice contribution retains the unresolved mixed-payment guard`, () => {
const f = createBureaucratSplitThresholdFixture(advanced);
let game = advanceBureaucratThresholdToMovement(f.game, f.payer);
game = applyAction(game, f.donor, { type: 'pledgeAid', amount: 3 });
const before = structuredClone(game);
assert.throws(() => applyAction(game, f.payer, {
type: 'ship', territory: 'habbanya_ridge_sietch', sector: 17, amount: 8, allyPayment: 3,
}));
assert.deepEqual(game, before, 'No payer, escrow or physical force changes on the unsupported qualifying split');
});
}
@@ -0,0 +1,69 @@
import assert from 'node:assert/strict';
import { applyAction, createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine';
import { initializeClassicNexusSkillsPaymentsSetup, nextClassicNexusSkillsPaymentsNativeStep } from './fixture-classic-nexus-skills-payments';
/** Original skill offers/setup and actual turn-two worm alliance. Conserved
* starting counters are staged into reserves to isolate payment, not combat.
* Wallets, alliances, clocks and payment records are never assigned. */
export function createBureaucratSplitThresholdFixture(advanced = false) {
const lobby = createGame('BUREAUCRATTHRESHOLD', newPlayer('b', 'Bureaucrat', 'atreides'), advanced);
for (const [id, faction] of [['p', 'harkonnen'], ['f', 'fremen'], ['e', 'emperor'], ['g', 'guild']] as const)
joinGame(lobby, newPlayer(id, faction, faction));
let game = initializeClassicNexusSkillsPaymentsSetup({ initial: lobby, skill: 'bureaucrat' });
const actions: { actor: string; action: Action }[] = [];
const step = () => {
const next = nextClassicNexusSkillsPaymentsNativeStep(game);
assert.ok(next, `Native payment setup/progression stalled at ${game.phase}/${game.setupStage}`);
actions.push(next);
game = applyAction(game, next.actor, next.action);
};
for (let n = 0; game.status === 'setup' && n < 240; n++) step();
assert.equal(game.status, 'playing');
assert.ok(game.leaderSkills!.assignments.some(a => a.owner === 'b' && a.skill === 'bureaucrat'));
for (const player of game.players) {
player.reserves += Object.values(player.forces).reduce((a, b) => a + b, 0);
player.forces = {};
if (player.elites) {
player.elites.reserves += Object.values(player.elites.forces).reduce((a, b) => a + b, 0);
player.elites.forces = {};
}
if (player.advisors) player.advisors = {};
}
const orderSpice = (kinds: ('land' | 'worm')[]) => {
const front = kinds.map(kind => {
const index = game.spiceDeck.findIndex(card => kind === 'land' ? 'territory' in card : 'worm' in card);
assert.ok(index >= 0);
return game.spiceDeck.splice(index, 1)[0];
});
game.spiceDeck.unshift(...front);
};
orderSpice(['land', 'land']);
for (let n = 0; !(game.turn === 2 && game.phase === 1) && n < 1800; n++) step();
assert.equal(game.turn, 2); assert.equal(game.phase, 1);
orderSpice(['worm', 'land', 'land']);
let allied = false;
for (let n = 0; !(allied && game.phase === 3 && game.auction && !game.phaseOpening && !game.response && !game.decision) && n < 600; n++) {
if (game.nexus && !game.phaseOpening && !game.response && !game.decision && !game.spiceWindow && !allied) {
for (const [actor, target] of [['p', 'f'], ['f', 'p']]) {
const action: Action = { type: 'alliance', target };
actions.push({ actor, action }); game = applyAction(game, actor, action);
}
allied = true;
} else step();
}
assert.ok(allied && game.auction);
assert.equal(game.players.find(p => p.id === 'p')!.ally, 'f');
assert.equal(game.players.find(p => p.id === 'f')!.ally, 'p');
return { game, actions, payer: 'p', donor: 'f', trainer: 'b', guild: 'g', emperor: 'e' };
}
export function advanceBureaucratThresholdToMovement(state: Game, actor: string): Game {
let game = state;
for (let n = 0; !(game.phase === 5 && game.active === actor && !game.phaseOpening && !game.response && !game.decision) && n < 600; n++) {
const next = nextClassicNexusSkillsPaymentsNativeStep(game);
assert.ok(next, 'Native payment continuation stalled');
game = applyAction(game, next.actor, next.action);
}
assert.equal(game.phase, 5); assert.equal(game.active, actor);
return game;
}
+8 -15
View File
@@ -15,20 +15,11 @@ import {
for (const claim of [false, true]) void test(
`Homeworld ordinary special play Semuta ${claim ? 'claims' : 'passes'} the used card without disturbing occupation`, () => {
const f = createSemutaHomeworldOrdinaryFixture();
assert.equal(f.game.phase, 5);
assert.equal(f.game.active, 'a');
assert.ok(semutaEmperorPlayer(f.game, f.holder).hand.some(card => card.id === SEMUTA_DRUG_ID));
const played = applyAction(f.game, 'a', f.action);
const pending = played.pendingTreacheryDiscard;
assert.ok(pending, 'The used ordinary special must offer its printed Semuta cost');
assert.equal(pending.batch.cause, 'ordinary:hajr');
assert.deepEqual(pending.batch.entries.map(entry => ({
id: entry.card.id, by: entry.discardedBy, face: entry.publicFace,
})), [{ id: f.hajr, by: 'a', face: true }]);
assert.equal(pending.reaction?.stage, 'offer');
assert.deepEqual(played.hajr, ['a'], 'The Hajr movement grant already committed');
assert.equal(played.discard.filter(card => card.id === f.hajr).length, 1);
for (const player of played.players) viewGame(played, player.id);
const before = semutaHomeworldOrdinaryState(played);
const event = pending.batch.event;
assert.throws(() => applyAction(played, f.owner, { type: 'semutaCommit', event }));
@@ -59,14 +50,14 @@ for (const claim of [false, true]) void test(
const searched = applyAction(f.game, f.owner, f.action);
const pending = searched.pendingTreacheryDiscard;
assert.ok(pending, 'The completed paid Box search must offer its printed Semuta cost');
assert.equal(pending.batch.cause, 'nullentropyBox');
assert.deepEqual(pending.batch.entries.map(entry => ({
id: entry.card.id, by: entry.discardedBy, face: entry.publicFace,
})), [{ id: f.box, by: f.owner, face: true }]);
assert.equal(searched.discard.at(-1)?.id, f.box, 'The used Box stays on top of the shuffled pile');
const continuation = pending.continuation;
assert.equal(continuation.kind, 'nullentropyDiscard');
if (continuation.kind !== 'nullentropyDiscard') throw Error('Missing completed Box search');
const selected = continuation.selected;
assert.ok(semutaEmperorPlayer(searched, f.owner).hand.some(card => card.id === selected.id));
assert.equal(semutaEmperorPlayer(searched, f.owner).spice,
semutaEmperorPlayer(f.game, f.owner).spice - f.cost);
for (const player of searched.players) viewGame(searched, player.id);
const before = semutaHomeworldOrdinaryState(searched);
const event = pending.batch.event;
assert.throws(() => applyAction(searched, f.holder, { type: 'semutaSelect', event, card: f.box }));
@@ -77,6 +68,9 @@ for (const claim of [false, true]) void test(
assert.equal(holder.hand.some(card => card.id === f.box), claim);
assert.equal(holder.hand.some(card => card.id === SEMUTA_DRUG_ID), !claim);
assert.equal(done.discard.some(card => card.id === f.box), !claim);
assert.deepEqual(semutaEmperorPlayer(done, f.owner).hand,
semutaEmperorPlayer(searched, f.owner).hand,
'The selected card remains in the searching player’s hand without a second search');
assert.deepEqual(semutaHomeworldOrdinaryState(done), before,
'The two-spice payment, searched card and occupation authority are unchanged');
assert.throws(() => applyAction(done, f.owner, f.action));
@@ -172,7 +166,6 @@ for (const claim of [false, true]) void test(
assert.equal(answered.truthHistory!.length, 1);
assert.equal(answered.truthHistory![0].answer, 'yes');
assert.equal(answered.truthtrance, null);
for (const player of answered.players) viewGame(answered, player.id);
const before = semutaHomeworldOrdinaryState(answered);
const event = pending.batch.event;
assert.throws(() => applyAction(answered, f.holder, { type: 'semutaSelect', event, card: f.truthtrance }));