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 the Guild emptying Junction, then a real Harkonnen invasion. The Emperor path
uses an actual paid auction to fund the original twenty-counter departure, 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 then a real Guild invasion of empty Kaitain. No force, cash or occupation
history is assigned. Basic current-sole receipts are exercised; Advanced fresh history is assigned. Basic current-sole and Advanced retained-qualification
entry is exercised, not an Advanced retention/full-game certification. 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 Actual390px controls redirect the Emperor invoice, then the Guild occupier
keeps3 through the original allocation control, then the original Harkonnen 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 Rules implementation continues; comprehensive assurance and administration
remain deferred until all rules are implemented. 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.** **Actual occupied Homeworld / Skills composition — 13 October.**
The earlier marked-done split-recipient task had only proved plain Homeworld The earlier marked-done split-recipient task had only proved plain Homeworld
funding; it did not finish the ruled occupied allocation. That missing composition funding; it did not finish the ruled occupied allocation. That missing composition
+22
View File
@@ -1,5 +1,27 @@
# Dune implementation status # 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 ## 13 October 2026 — actual occupied income and Leader Skills
Corrected the previous task completion claim:9b83b61 proved plain Homeworld 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); 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); 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 { territory } from '../game/board';
import { homeworldShipmentChoice } from '../game/homeworld-shipment-options'; import { homeworldShipmentChoice } from '../game/homeworld-shipment-options';
import { quoteStableHomeworldOccupation } from '../game/homeworld-stable-occupation'; import { quoteStableHomeworldOccupation } from '../game/homeworld-stable-occupation';
import { homeworldContext } from '../game/homeworld-game';
import { homeworldForceGroups } from '../game/homeworld-custody';
import { import {
assertClassicHomeworldSkillsCustody, classicHomeworldSkillsPlayer, assertClassicHomeworldSkillsCustody, classicHomeworldSkillsPlayer,
createClassicHomeworldSkillsSetup, nextClassicHomeworldSkillsStep, createClassicHomeworldSkillsSetup, nextClassicHomeworldSkillsStep,
@@ -88,22 +90,23 @@ export type OccupiedEmperorFixture = {
lot: string; bonus: string; karama: string; physical: string[]; actions: ClassicHomeworldSkillsStep[]; 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 * Guild's real bribes through Mentat -> worm/alliance -> empty Kaitain invasion
* -> later funded auction. No wallet, force, phase, alliance or authority edits. */ * -> later funded auction. No wallet, force, phase, alliance or authority edits. */
export function createOccupiedEmperorFixture(options: { export function createOccupiedEmperorFixture(options: {
payerShare?: number; donorShare?: number; donor?: 'guild' | 'atreides' | 'emperor'; payerShare?: number; donorShare?: number; donor?: 'guild' | 'atreides' | 'emperor';
advanced?: boolean;
} = {}): OccupiedEmperorFixture { } = {}): 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 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]; const setup = structuredClone(native.afterSetup), actions = [...native.actions];
let game = structuredClone(setup); let game = structuredClone(setup);
const act = (actor: string, action: Action) => { const act = (actor: string, action: Action) => {
actions.push({ actor, action: structuredClone(action) }); game = applyAction(game, actor, action); actions.push({ actor, action: structuredClone(action) }); game = applyAction(game, actor, action);
}; };
const reach = (until: (state: Game) => boolean) => { game = advanceOccupiedEmperor(game, until, actions); }; 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)); reach(g => g.phase === 3 && !!g.auction && occupiedEmperorClean(g));
holdKarama(game, payer); holdKarama(game, payer);
reach(g => g.auction!.active === payer && occupiedEmperorClean(g)); reach(g => g.auction!.active === payer && occupiedEmperorClean(g));
@@ -121,7 +124,8 @@ export function createOccupiedEmperorFixture(options: {
const destination = 'habbanya_ridge_sietch'; const destination = 'habbanya_ridge_sietch';
assert.ok(game.players.every(p => !Object.keys(p.forces).some(key => key.startsWith(`${destination}:`)))); 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, 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)); reach(g => g.phase === 5 && occupiedEmperorClean(g));
assert.equal(occupiedEmperorPlayer(game, payee).reserves, 0); assert.equal(occupiedEmperorPlayer(game, payee).reserves, 0);
assert.equal(occupiedEmperorPlayer(game, payee).forces[`${destination}:${territory(destination).sectors[0]}`], 20); 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 }); act(occupier, { type: 'bribe', target: payee, amount: 5 });
reach(occupiedEmperorClean); reach(occupiedEmperorClean);
reach(g => g.turn === 2 && g.phase === 1 && occupiedEmperorClean(g)); 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)); reach(g => !!g.nexus && !g.spiceWindow && !g.spiceResolution && occupiedEmperorClean(g));
act(payer, { type: 'alliance', target: donor }); act(payer, { type: 'alliance', target: donor });
act(donor, { type: 'alliance', target: payer }); act(donor, { type: 'alliance', target: payer });
+13 -8
View File
@@ -105,14 +105,18 @@ export type OccupiedGuildFixture = {
export function createOccupiedGuildFixture(options: { export function createOccupiedGuildFixture(options: {
payer?: string; donor?: string; occupier?: string; prep?: string; payer?: string; donor?: string; occupier?: string; prep?: string;
payerShare?: number; donorShare?: number; payerShare?: number; donorShare?: number;
advanced?: boolean;
} = {}): OccupiedGuildFixture { } = {}): OccupiedGuildFixture {
const payer = options.payer ?? 'harkonnen'; const payer = options.payer ?? 'harkonnen';
const donor = options.donor ?? 'emperor'; const donor = options.donor ?? 'emperor';
const occupier = options.occupier ?? 'harkonnen'; const occupier = options.occupier ?? (options.advanced ? 'beneGesserit' : 'harkonnen');
const prep = options.prep ?? 'emperor'; const prep = options.prep ?? (options.advanced ? 'harkonnen' : 'emperor');
const { payerShare = 6, donorShare = 2 } = options; const { payerShare = 6, donorShare = 2, advanced = false } = options;
const payee = 'guild', trainer = 'atreides'; 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 setup = structuredClone(native.afterSetup), actions = [...native.actions];
const staging = [...native.staging]; const staging = [...native.staging];
let game = structuredClone(setup); let game = structuredClone(setup);
@@ -123,7 +127,7 @@ export function createOccupiedGuildFixture(options: {
const reach = (until: (state: Game) => boolean) => { const reach = (until: (state: Game) => boolean) => {
game = advanceOccupiedGuild(game, until, actions); 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)); reach(g => g.turn === 1 && g.phase === 5 && g.active === prep && occupiedGuildClean(g));
const opening = occupiedGuildPlayer(game, payee).spice; const opening = occupiedGuildPlayer(game, payee).spice;
// One real four-spice shipment income funds the native Guild's later tariff. // 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 => assert.ok(game.homeworldOccupationHistory!.sources.some(source =>
source.event === authority.entitlement!.qualification && source.cause !== 'setup')); source.event === authority.entitlement!.qualification && source.cause !== 'setup'));
const occupied = structuredClone(game); const occupied = structuredClone(game);
reach(g => g.turn === 2 && g.phase === 1 && occupiedGuildClean(g)); const paymentTurn = game.turn + 1;
orderSpice(game, ['worm', 'land']); 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)); reach(g => !!g.nexus && !g.spiceWindow && !g.spiceResolution && occupiedGuildClean(g));
act(payer, { type: 'alliance', target: donor }); act(payer, { type: 'alliance', target: donor });
act(donor, { type: 'alliance', target: payer }); act(donor, { type: 'alliance', target: payer });
@@ -164,7 +169,7 @@ export function createOccupiedGuildFixture(options: {
assert.equal(occupiedGuildPlayer(game, donor).ally, payer); assert.equal(occupiedGuildPlayer(game, donor).ally, payer);
const alliance = structuredClone(game); const alliance = structuredClone(game);
// The later real paid shipment to an empty Arrakis territory. // 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 karama = holdKarama(game, trainer);
const beforePledge = structuredClone(game); const beforePledge = structuredClone(game);
if (donorShare) act(donor, { type: 'pledgeAid', amount: donorShare }); if (donorShare) act(donor, { type: 'pledgeAid', amount: donorShare });