diff --git a/docs/BUREAUCRAT_PAYMENTS.md b/docs/BUREAUCRAT_PAYMENTS.md index cfdf5ac9..fed553fc 100644 --- a/docs/BUREAUCRAT_PAYMENTS.md +++ b/docs/BUREAUCRAT_PAYMENTS.md @@ -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 diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 33ec63ba..80c6f945 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -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 diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index 74d6f5ff..7fdf910a 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -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 diff --git a/tests/bureaucrat-occupied-emperor.test.ts b/tests/bureaucrat-occupied-emperor.test.ts index 8f0152bd..7aa4ccad 100644 --- a/tests/bureaucrat-occupied-emperor.test.ts +++ b/tests/bureaucrat-occupied-emperor.test.ts @@ -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); +}); diff --git a/tests/bureaucrat-occupied-guild.test.ts b/tests/bureaucrat-occupied-guild.test.ts index f66b03e9..1d7a2c32 100644 --- a/tests/bureaucrat-occupied-guild.test.ts +++ b/tests/bureaucrat-occupied-guild.test.ts @@ -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); +}); diff --git a/tests/fixture-bureaucrat-occupied-emperor.ts b/tests/fixture-bureaucrat-occupied-emperor.ts index 8a45bfa5..2bfb1a0a 100644 --- a/tests/fixture-bureaucrat-occupied-emperor.ts +++ b/tests/fixture-bureaucrat-occupied-emperor.ts @@ -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 }); diff --git a/tests/fixture-bureaucrat-occupied-guild.ts b/tests/fixture-bureaucrat-occupied-guild.ts index dbbd0e4f..bb916745 100644 --- a/tests/fixture-bureaucrat-occupied-guild.ts +++ b/tests/fixture-bureaucrat-occupied-guild.ts @@ -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 });