Prove Advanced occupied Bureaucrat receipts with native timing

This commit is contained in:
Quintine committed 2026-10-13 18:44:58 +11:00
1 parent 304849f8d9
commit 9fb680da09
7 files changed
+102 -16

No files matched your search

+12 -2
View File
@@ -133,8 +133,18 @@ same family names. The native Guild path uses paid Emperor shipping to fund
the Guild emptying Junction, then a real Harkonnen invasion. The Emperor path
uses an actual paid auction to fund the original twenty-counter departure,
then a real Guild invasion of empty Kaitain. No force, cash or occupation
history is assigned. Basic current-sole receipts are exercised; Advanced fresh
entry is exercised, not an Advanced retention/full-game certification.
history is assigned. Basic current-sole and Advanced retained-qualification
receipts are exercised, not full-game or complete-module certification.
Advanced Emperor departure explicitly selects all native Homeworld groups:
fifteen normal Kaitain counters and five Salusa elites. The original twenty-
counter payment and typed delivery remain. Advanced Guild uses native
Harkonnen preparation and a fifth BG occupier. Flexible Guild timing postpones
the invasion to turn2; the funded receipt waits for the actual next worm and
alliance on turn3 rather than chasing an already-past turn2 Spice phase.
Both actual Advanced gross8 programs settle native3/occupied3/Bank2 with original
paid8/shares intact; four meaningful allowance/diversion cases and the previous
Basic consumers pass21/21 across both files, with types/lint.
Actual390px controls redirect the Emperor invoice, then the Guild occupier
keeps3 through the original allocation control, then the original Harkonnen
+13
View File
@@ -29,6 +29,19 @@ optional skill bands, combinations and public expansion starts remain open.
Rules implementation continues; comprehensive assurance and administration
remain deferred until all rules are implemented.
**Advanced occupied Bureaucrat receipts — 13 October.**
Actual Advanced Emperor and Guild gross8 receipts now prove native3/occupied3/
Bank2 through retained original qualifications, with original full payer shares.
Emperor's real twenty-counter departure uses15 normal Kaitain plus5 Salusa
elites. Guild's native flexible timing delays foreign invasion until turn2;
the later receipt uses a real turn3 worm/alliance, not a past-phase predicate.
Native Harkonnen preparation and a fifth BG occupier preserve original rules.
Initial fixture traversals chased an already-past Spice phase to the native
turn10 Guild win; turn-relative traversal fixes the fixture without changing
game rules. The old Basic paths remain. Four new outcome/boundary cases bring
the two-file union to21/21; types/lint pass. No new production code, strategy,
save/deployed, full-game or completed-mode claim follows.
**Actual occupied Homeworld / Skills composition — 13 October.**
The earlier marked-done split-recipient task had only proved plain Homeworld
funding; it did not finish the ruled occupied allocation. That missing composition
+22
View File
@@ -1,5 +1,27 @@
# Dune implementation status
## 13 October 2026 — actual Advanced occupied receipt coverage
Native Advanced Emperor and Guild programs settle actual gross8 into native3,
occupied3 and Bureaucrat Bank2 with original paid8/shares intact. Both preserve
the original retained qualification from the real prior invasion.
Advanced Emperor departure explicitly selects every native pool: Kaitain15
normal and Salusa5 elite, paying20 and committing20 with5 elites. Existing Basic
twenty-normal departure remains. Advanced Guild uses native Harkonnen prep and
a fifth BG occupier; original Guild timing remains flexible. The foreign
invasion occurs turn2, so a literal turn2 Spice predicate was already past and
traversed until the native turn10 Guild win. A relative next-turn boundary now
waits for actual turn3 worm/alliance and payment; no phase, victory or wallet
is changed. Temporary diagnostics are removed.
Four meaningful Advanced allowance/diversion cases assert original typed
withdrawal, full payer debits, occupied allocation, retained source, actual next
lot or committed shipment. Frozen two-file21/21 and types/lint pass. Actual
native programs cover both families; the earlier390px Basic surface proves
the unchanged controls. No production change, new strategy, saved-game audit,
deployment, complete-game or complete-mode certification is claimed.
## 13 October 2026 — actual occupied income and Leader Skills
Corrected the previous task completion claim:9b83b61 proved plain Homeworld
+14
View File
@@ -187,3 +187,17 @@ void test('redirect spends only one phase use; decline leaves it for the next ac
assertClassicHomeworldSkillsCustody(done);
}
});
for (const redirect of [false, true]) void test(`Advanced occupied Emperor ${redirect ? 'diversion' : 'allowance'} preserves the original two-world departure and next paid lot`, () => {
const f = fixture({ advanced: true });
assert.equal(player(f.setup, f.payee).elites!.reserves, 5);
assert.equal(player(f.emptyKaitain, f.payee).reserves, 0);
assert.equal(player(f.emptyKaitain, f.payee).elites!.reserves, 0);
assert.equal(Object.values(player(f.emptyKaitain, f.payee).forces).reduce((a,b) => a+b, 0), 20);
assert.equal(Object.values(player(f.emptyKaitain, f.payee).elites!.forces).reduce((a,b) => a+b, 0), 5);
const paid = allow(pay(f)); assertPaid(f, paid);
const split = choose(paid, redirect), done = allocate(split);
assertOutcome(f, split, done, redirect ? 2 : 0);
assert.equal(quoteStableHomeworldOccupation(done, 'kaitain').entitlement!.qualification,
quoteStableHomeworldOccupation(f.occupied, 'kaitain').entitlement!.qualification);
});
+18
View File
@@ -269,3 +269,21 @@ void test('every owner bot resolves the occupied Junction offer and the native p
outcome(f, done, 2);
}
});
for (const redirect of [false, true]) void test(`Advanced occupied Guild ${redirect ? 'diversion' : 'allowance'} follows actual flexible timing without replaying a past phase`, () => {
const f = createOccupiedGuildFixture({ advanced: true });
assert.ok(f.beforePledge.turn > f.occupied.turn);
const paid = declareOccupiedGuild(f);
const pending = allowOccupiedGuildIncome(paid);
const done = allocateOccupiedGuild(chooseOccupiedGuild(pending, redirect));
outcome(f, done, redirect ? 2 : 0);
assert.deepEqual(armies(done), armies(paid));
const receipt = done.homeworldOccupiedPercentageLedger!.receipts.at(-1)!;
assert.equal(receipt.nativeRetained, redirect ? 3 : 4);
assert.equal(receipt.occupiedAmount, redirect ? 3 : 4);
assert.equal(guildShippingSource(receipt).payment.paid, 8);
assert.deepEqual(guildShippingSource(receipt).payment.contributions,
[{payer:f.payer,amount:6},{payer:f.donor,amount:2}]);
assert.equal(quoteStableHomeworldOccupation(done, 'junction').entitlement!.qualification,
quoteStableHomeworldOccupation(f.occupied, 'junction').entitlement!.qualification);
});
+10 -6
View File
@@ -3,6 +3,8 @@ import { applyAction, viewGame, type Action, type Game } from '../game/engine';
import { territory } from '../game/board';
import { homeworldShipmentChoice } from '../game/homeworld-shipment-options';
import { quoteStableHomeworldOccupation } from '../game/homeworld-stable-occupation';
import { homeworldContext } from '../game/homeworld-game';
import { homeworldForceGroups } from '../game/homeworld-custody';
import {
assertClassicHomeworldSkillsCustody, classicHomeworldSkillsPlayer,
createClassicHomeworldSkillsSetup, nextClassicHomeworldSkillsStep,
@@ -88,22 +90,23 @@ export type OccupiedEmperorFixture = {
lot: string; bonus: string; karama: string; physical: string[]; actions: ClassicHomeworldSkillsStep[];
};
/** Actual Basic setup -> paid ten-spice lot -> twenty-counter paid shipment ->
/** Original native setup -> paid ten-spice lot -> twenty-counter paid shipment ->
* Guild's real bribes through Mentat -> worm/alliance -> empty Kaitain invasion
* -> later funded auction. No wallet, force, phase, alliance or authority edits. */
export function createOccupiedEmperorFixture(options: {
payerShare?: number; donorShare?: number; donor?: 'guild' | 'atreides' | 'emperor';
advanced?: boolean;
} = {}): OccupiedEmperorFixture {
const { payerShare = 5, donorShare = 3, donor = 'guild' } = options;
const { payerShare = 5, donorShare = 3, donor = 'guild', advanced = false } = options;
const payer = 'harkonnen', payee = 'emperor', trainer = 'atreides', occupier = 'guild';
const native = createClassicHomeworldSkillsSetup({ occupation: true });
const native = createClassicHomeworldSkillsSetup({ occupation: true, advanced });
const setup = structuredClone(native.afterSetup), actions = [...native.actions];
let game = structuredClone(setup);
const act = (actor: string, action: Action) => {
actions.push({ actor, action: structuredClone(action) }); game = applyAction(game, actor, action);
};
const reach = (until: (state: Game) => boolean) => { game = advanceOccupiedEmperor(game, until, actions); };
orderSpice(game, ['land']);
orderSpice(game, advanced ? ['land', 'land'] : ['land']);
reach(g => g.phase === 3 && !!g.auction && occupiedEmperorClean(g));
holdKarama(game, payer);
reach(g => g.auction!.active === payer && occupiedEmperorClean(g));
@@ -121,7 +124,8 @@ export function createOccupiedEmperorFixture(options: {
const destination = 'habbanya_ridge_sietch';
assert.ok(game.players.every(p => !Object.keys(p.forces).some(key => key.startsWith(`${destination}:`))));
act(payee, { type: 'ship', territory: destination, sector: territory(destination).sectors[0], amount: 20, elite,
homeworldSources: { 'homeworld:emperor': { normal: 20 - elite, elite } } });
homeworldSources: Object.fromEntries(homeworldForceGroups(homeworldContext(game), game.homeworlds!.custody!)
.filter(group => group.native === payee).map(group => [group.id, group.forces[payee]])) });
reach(g => g.phase === 5 && occupiedEmperorClean(g));
assert.equal(occupiedEmperorPlayer(game, payee).reserves, 0);
assert.equal(occupiedEmperorPlayer(game, payee).forces[`${destination}:${territory(destination).sectors[0]}`], 20);
@@ -132,7 +136,7 @@ export function createOccupiedEmperorFixture(options: {
act(occupier, { type: 'bribe', target: payee, amount: 5 });
reach(occupiedEmperorClean);
reach(g => g.turn === 2 && g.phase === 1 && occupiedEmperorClean(g));
orderSpice(game, ['worm', 'land']);
orderSpice(game, advanced ? ['worm', 'land', 'land'] : ['worm', 'land']);
reach(g => !!g.nexus && !g.spiceWindow && !g.spiceResolution && occupiedEmperorClean(g));
act(payer, { type: 'alliance', target: donor });
act(donor, { type: 'alliance', target: payer });
+13 -8
View File
@@ -105,14 +105,18 @@ export type OccupiedGuildFixture = {
export function createOccupiedGuildFixture(options: {
payer?: string; donor?: string; occupier?: string; prep?: string;
payerShare?: number; donorShare?: number;
advanced?: boolean;
} = {}): OccupiedGuildFixture {
const payer = options.payer ?? 'harkonnen';
const donor = options.donor ?? 'emperor';
const occupier = options.occupier ?? 'harkonnen';
const prep = options.prep ?? 'emperor';
const { payerShare = 6, donorShare = 2 } = options;
const occupier = options.occupier ?? (options.advanced ? 'beneGesserit' : 'harkonnen');
const prep = options.prep ?? (options.advanced ? 'harkonnen' : 'emperor');
const { payerShare = 6, donorShare = 2, advanced = false } = options;
const payee = 'guild', trainer = 'atreides';
const native = createClassicHomeworldSkillsSetup({ occupation: true });
// Use native Harkonnen preparation and a distinct BG occupier in Advanced;
// native flexible Guild timing and every original victory rule remain intact.
const native = createClassicHomeworldSkillsSetup({ occupation: true, advanced,
...(advanced ? { roster: ['emperor', 'guild', 'atreides', 'harkonnen', 'beneGesserit'] as const } : {}) });
const setup = structuredClone(native.afterSetup), actions = [...native.actions];
const staging = [...native.staging];
let game = structuredClone(setup);
@@ -123,7 +127,7 @@ export function createOccupiedGuildFixture(options: {
const reach = (until: (state: Game) => boolean) => {
game = advanceOccupiedGuild(game, until, actions);
};
orderSpice(game, ['land']);
orderSpice(game, advanced ? ['land', 'land'] : ['land']);
reach(g => g.turn === 1 && g.phase === 5 && g.active === prep && occupiedGuildClean(g));
const opening = occupiedGuildPlayer(game, payee).spice;
// One real four-spice shipment income funds the native Guild's later tariff.
@@ -155,8 +159,9 @@ export function createOccupiedGuildFixture(options: {
assert.ok(game.homeworldOccupationHistory!.sources.some(source =>
source.event === authority.entitlement!.qualification && source.cause !== 'setup'));
const occupied = structuredClone(game);
reach(g => g.turn === 2 && g.phase === 1 && occupiedGuildClean(g));
orderSpice(game, ['worm', 'land']);
const paymentTurn = game.turn + 1;
reach(g => g.turn === paymentTurn && g.phase === 1 && occupiedGuildClean(g));
orderSpice(game, advanced ? ['worm', 'land', 'land'] : ['worm', 'land']);
reach(g => !!g.nexus && !g.spiceWindow && !g.spiceResolution && occupiedGuildClean(g));
act(payer, { type: 'alliance', target: donor });
act(donor, { type: 'alliance', target: payer });
@@ -164,7 +169,7 @@ export function createOccupiedGuildFixture(options: {
assert.equal(occupiedGuildPlayer(game, donor).ally, payer);
const alliance = structuredClone(game);
// The later real paid shipment to an empty Arrakis territory.
reach(g => g.turn === 2 && g.phase === 5 && g.active === payer && occupiedGuildClean(g));
reach(g => g.turn === paymentTurn && g.phase === 5 && g.active === payer && occupiedGuildClean(g));
const karama = holdKarama(game, trainer);
const beforePledge = structuredClone(game);
if (donorShare) act(donor, { type: 'pledgeAid', amount: donorShare });