Resolve direct advanced Sneak arrivals
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 19:48:35 +11:00
1 parent 8a39f5a172
commit b79e448deb
8 files changed
+213 -8

No files matched your search

+14 -2
View File
@@ -17,11 +17,23 @@ open mode gates.
## Current checkpoint and work
**Direct Advanced Sneak Attack arrivals — 29 October. Prototyped / Partial.**
Outside worm continuations, a positive Sneak group now gets its own Advanced
arrival reactions, including an eligible Ecaz Ambassador after Bene Gesserit.
The original action and existing dedicated Homeworld continuations remain.
The focused native tests use genuine Advanced setup with controlled physical
token placement; ten related files pass 73 cases. A staged worm ride verifies
the temporary positive-entry guard and zero-force continuation. Full worm
support needs a separate saved rider continuation; optional combinations and
deployed verification remain open.
[Scope](MORITANI_ENTRY_TIMING.md#direct-advanced-sneak-arrivals--29-october-2026).
**Advanced Ambassador child arrivals in the Ecaz–Moritani overlap — 29
October. Prototyped / Partial.** Bare native Advanced games can use the Fremen
relocation and Guild shipment within the paired entry, retaining the child
Terror reaction and the original entry's remaining opportunity. Sneak Attack
queues its own Bene Gesserit reactions without overwriting that opportunity.
Terror reaction and the original entry's remaining opportunity. Outside worm
continuations, Sneak Attack queues its own Bene Gesserit reactions without
overwriting that opportunity.
Eight related files pass 52 cases, including actual Advanced setup, both owner
orders, controls and minimal bot actions; token selection and placements are
controlled in the fixtures. Optional-module combinations, other nested cases,
+26 -3
View File
@@ -2,6 +2,29 @@
Reviewed 2026-09-06. This supplements `MORITANI_TERROR_RULES.md`; it does not enable combined-faction play or certify an implementation order.
## Direct Advanced Sneak arrivals — 29 October 2026
**Prototyped / Partial.** A positive Advanced Sneak Attack outside a worm
continuation now queues its own committed off-planet group for the existing
Intrusion, Bene Gesserit accompaniment and eligible Ambassador reactions. The
original arrival finishes first, the new group spends no ordinary shipment or
spice, and zero forces create no extra entry. The ordering is a provisional
composition of the printed reserve entry and Ambassador interruption, not a
publisher priority ruling. Caladan and revival source receipts retain their
dedicated paths.
Worm continuations require a separately retained rider identity: a Moritani
child cannot simply inherit the Fremen rider's Ambassador continuation. The
positive choice is therefore temporarily blocked for Advanced worm parents;
Moritani may leave the token hidden or send zero. The staged worm regression
checks the visible reason, rejection and continued ride. Broader modules and
deployed verification remain open.
Ten related files pass 73 cases. Direct runtime fixtures use genuine Advanced
setup and physical token selection/placement under control, including BG and
no-BG, a free Guild Ambassador grant and legal bot choices. They do not certify
full-game or optional-module combinations.
## Advanced native Ambassador child arrivals — 29 October 2026
**Prototyped / Partial.** In the bare Advanced native-faction profile, the
@@ -9,7 +32,7 @@ Fremen Ambassador relocation and Guild Ambassador shipment can now create a
child arrival while the original paired Ambassador/Terror entry remains in
progress. The existing child-first continuation preserves the original entry,
remaining opportunity and physical movement or payment. A positive Sneak
Attack in this overlap queues its own reactions until the original decisions
Attack in this overlap outside a worm continuation queues its reactions until the original decisions
finish, including its Bene Gesserit opportunity. The owners' storm order and
child ordering remain the provisional software interpretations below, not an
official priority ruling; the printed Ambassador effects and interruption are
@@ -17,8 +40,8 @@ described in GF9 Ecaz & Moritani rules pp. 8 and 15.
Fremen destinations combining Advanced fighter Intrusion with Terror retain
their existing restriction, and simultaneous Ambassador/Terror on Guild
accompaniment remains separate. Other optional modules, Advanced Sneak outside
this overlap, complete rules and deployed verification remain open.
accompaniment remains separate. Other optional modules, worm continuations,
complete rules and deployed verification remain open.
Eight related files pass 52 cases. Real Advanced setup covers both owner orders,
Fremen and Guild child effects, child Terror decline or reveal, the paid original
+7
View File
@@ -1,5 +1,12 @@
# Rule decision index
## Direct Advanced Sneak arrival — 29 October 2026
A positive Sneak group outside a worm continuation uses the existing separate
arrival queue after the original entry. Its ordering is provisional; Advanced
worm parents temporarily block a positive group until the rider continuation is
preserved separately. [Scope](MORITANI_ENTRY_TIMING.md#direct-advanced-sneak-arrivals--29-october-2026).
## First-turn Duke proof and occupied Ecaz displacement — 29 October 2026
**Prototyped / Partial.** Public first-turn pre-battle state can prove that an
+8 -2
View File
@@ -27887,6 +27887,9 @@ function sneakAttackOptions(
let blocked: string | null = null;
try {
allowedEntry(g, owner, entry.territory, entry.sector);
if (g.advanced && (entry.resume === 'wormRide' || g.pendingArrivalOverlap?.resume === 'wormRide' ||
g.pendingAmbassador?.resume === 'wormRide'))
blocked = 'Sneak Attack during a worm ride needs its original ride continuation; send no forces or leave the token hidden.';
} catch (error) {
if (!(error instanceof RuleError)) throw error;
blocked = error.message;
@@ -28168,7 +28171,10 @@ function decideTerror(g: Game, p: Player, action: Action) {
const revivalArrival: RevivalEntryParent | undefined = amount > 0 && entry.revivalArrival
? {event:entry.revivalArrival.event,terror:{event:entry.entrySignature!,owner:p.id,
territory:entry.territory,sector:entry.sector,amount,elite:0}} : undefined;
const advancedNativeOverlap = g.advanced && bareAdvancedNativeArrivalOverlap(g) && !!g.pendingArrivalOverlap;
// Worm continuations can advance the phase synchronously; their child
// arrival needs a separate retained worm cursor before it can be queued.
const advancedNativeArrival = g.advanced && entry.resume !== 'wormRide' &&
g.pendingArrivalOverlap?.resume !== 'wormRide' && g.pendingAmbassador?.resume !== 'wormRide';
if (caladan) (g.homeworldVictoryReinforcement!.terrorArrivals ??= []).push(caladan.receipt);
finishTerrorEntry(g);
if (caladan) {
@@ -28185,7 +28191,7 @@ function decideTerror(g: Game, p: Player, action: Action) {
phase:g.phase,revivalArrival,revivalSneakAdvisor:true});
return;
}
if (amount > 0 && (advancedNativeOverlap || !g.advanced &&
if (amount > 0 && (advancedNativeArrival || !g.advanced &&
(g.pendingArrivalOverlap || g.pendingAmbassador || g.pendingTerrorEntry || g.response || g.decision))) {
// The child may have opened the original Terror decision. Let it finish
// before offering advisor reactions for this already-committed group.
+2 -1
View File
@@ -4227,6 +4227,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Sabotage: randomly draw and discard a Treachery Card from the entrant if possible. Then you may give that player a card of your choice from your own hand. Giving a card is optional. If the gift puts the recipient above their hand limit, the recipient provisionally chooses one held card to discard, including the gift; the original entry waits for that fresh discard and any supported Semuta reaction.',
'Sneak Attack: send up to five of your reserve forces into the triggered territory at no cost, respecting storm and occupancy restrictions. This particular entry is allowed even when Atomics Aftermath is present.',
'Before revealing Sneak Attack, inspect its private available maximum and any reason positive entry is blocked. You may keep it hidden. Revealing spends the token even if you then choose Send no forces. For a supported entry, select zero through the displayed maximum; forces enter the arrival’s territory and sector without consuming your ordinary shipment or movement turn. Zero remains available when positive entry is blocked. Aftermath’s printed exception allows this reserve entry even after Atomics resolves. A positive entry during Shipment and Movement is treated as an off-planet shipment for the Heighliners token, which accrues its once-per-turn income for its owner, and the committed entry finishes before Bene Gesserit may answer it with an Intrusion choice or a free spiritual-advisor shipment.',
'In Advanced play outside a worm continuation, the separate Sneak group can also trigger an eligible Ecaz Ambassador after its Bene Gesserit reactions. A positive Sneak group during a worm continuation is temporarily unavailable until the rider continuation can be preserved; leaving the token hidden or sending zero remains possible.',
'The random Assassination token is separate from the advanced Assassinate Leaders advantage. Neither ability supplies the other’s selection or timing rules.',
],
checklist: [
@@ -4234,7 +4235,7 @@ export const RULE_TOPICS: RuleTopic[] = [
{ area: 'Player controls', status: 'Partial', detail: 'Original actual390px Reveal Assassination resolves a captive Duke death6, displays Tanks inspection and resumes native Harkonnen movement. No selectable victim or projected pool; no overflow/browser errors.', evidence: ['components/game-table.tsx', 'components/duke-vidal.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'All four policies make accepted original decisions: Easy reveals, Medium/Hard/Brutal form an accepted Enemy of My Enemy alliance in the actual native position. Legal participation only; no calibrated strategy claim.', evidence: ['game/bots.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Effective-control exceptional pool, public victim and spent-empty behavior are labelled provisional and separate from Advanced battle-loss Assassinate Leaders. Native printed values and no foreign-Ghola Duke remain binding.', evidence: ['docs/MORITANI_ASSASSINATION_SNEAK.md', 'docs/MORITANI_TERROR_RULES.md', 'docs/RULE_DECISIONS.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Affected22-file195/195 and types/lint/build pass. Pure custody/identity/value boundaries plus original capture-to-Terror runtime and real table death6 pass. Empty boundary resumes genuine Guild entry but stages prior native deaths. Full combinations/deployed acceptance open.', evidence: ['tests/moritani-terror-assassination.test.ts', 'tests/moritani-terror-assassination-runtime.test.ts', 'tests/moritani-force-leader.test.ts'] },
{ area: 'Verification', status: 'Partial', detail: 'Advanced direct Sneak and related arrivals pass ten files and 73 cases, with genuine setup and controlled physical token placements; a staged worm case exercises its temporary guard. Earlier affected22-file195/195 and types/lint/build pass for Assassination. Pure custody/identity/value boundaries plus original capture-to-Terror runtime and real table death6 pass. Empty boundary resumes genuine Guild entry but stages prior native deaths. Full combinations/deployed acceptance open.', evidence: ['tests/advanced-sneak-direct-runtime.test.ts', 'tests/ambassador-terror-overlap-worm.test.ts', 'tests/moritani-terror-assassination.test.ts', 'tests/moritani-terror-assassination-runtime.test.ts', 'tests/moritani-force-leader.test.ts'] },
],
related: [
'moritani-terror',
+108
View File
@@ -0,0 +1,108 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Game } from '../game/engine';
import { guildAmbassadorShipments } from '../game/guild-ambassador';
import { reserveShipmentCost } from '../game/shipment-price';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { nextBasicSemutaAmbassadorStep } from './fixture-basic-semuta-ambassador';
import { act, player } from './fixture-advanced-overlap-child';
import { advancedDirectSneakFixture } from './fixture-advanced-sneak-direct';
function advanceTo(game: Game, until: (state: Game) => boolean): Game {
for (let i = 0; i < 60 && !until(game); i++) {
if (game.decision?.kind === 'advisor') {
game = act(game, game.decision.player, {type:'decision', accept:false});
continue;
}
if (game.decision?.kind === 'guildShipment') {
game = act(game, game.decision.player, {type:'decision', allow:true});
continue;
}
const next = nextBasicSemutaAmbassadorStep(game);
assert.ok(next, `The original arrival must continue: ${game.decision?.kind ?? game.response?.kind}`);
game = act(game, next.actor, next.action);
}
assert.ok(until(game), 'The requested native arrival reaction is reached');
return game;
}
function revealSneak(withBG: boolean) {
const f = advancedDirectSneakFixture(withBG);
const originalSpice = player(f.game, f.entrant).spice;
const price = reserveShipmentCost({faction:'guild', halfRate:true}, 'stronghold', 1);
let game = act(f.game, f.entrant, f.shipment);
game = advanceTo(game, state => !!state.pendingTerrorEntry);
assert.equal(game.pendingArrivalOverlap ?? null, null, 'Guild cannot trigger its own Ambassador');
assert.equal(game.pendingTerrorEntry?.token, f.sneak);
assert.equal(game.pendingTerrorEntry?.entrant, f.entrant);
assert.equal(player(game, f.entrant).spice, originalSpice - price);
game = act(game, f.moritani, {type:'decision', reveal:true});
assert.equal(game.pendingTerrorEntry?.stage, 'sneakAttack');
return {f, game, originalSpice, price};
}
for (const withBG of [false, true]) void test(
`Advanced direct Sneak opens the Moritani Ambassador entry ${withBG ? 'after BG accompaniment' : 'without BG'}`, () => {
const {f, game: before, originalSpice, price} = revealSneak(withBG);
const reserves = player(before, f.moritani).reserves;
let game = act(before, f.moritani, {type:'decision', amount:1});
if (withBG) {
assert.equal(game.decision?.kind, 'advisor');
assert.equal(game.decision?.player, 'beneGesserit');
game = act(game, 'beneGesserit', {type:'decision', accept:false});
}
game = advanceTo(game, state => !!state.pendingAmbassador);
const entry = game.pendingAmbassador; assert.ok(entry);
assert.equal(entry.token, f.ambassador);
assert.equal(entry.entrant, f.moritani);
assert.equal(player(game, f.moritani).reserves, reserves - 1);
assert.equal(player(game, f.moritani).forces[`${f.destination}:${f.sector}`], 1);
assert.equal(player(game, f.entrant).forces[`${f.destination}:${f.sector}`], 1);
assert.equal(player(game, f.entrant).spice, originalSpice - price);
for (const difficulty of DIFFICULTIES) {
const view = viewGame(game, 'ecaz');
view.players.find(p => p.id === 'ecaz')!.bot = difficulty;
const action = botActions(view)[0]; assert.ok(action, difficulty);
assert.doesNotThrow(() => act(game, 'ecaz', action), difficulty);
}
game = act(game, 'ecaz', {type:'decision', event:entry.event, decline:true});
assert.equal(game.pendingAmbassador, null);
assert.equal(game.pendingTerrorEntry, null);
assert.equal(game.pendingArrivalOverlap ?? null, null);
assert.equal(player(game, f.moritani).reserves, reserves - 1);
assert.equal(player(game, f.entrant).spice, originalSpice - price);
});
void test('Advanced direct Sneak sending zero opens no separate entry or accompaniment', () => {
const {f, game: before, originalSpice, price} = revealSneak(true);
const reserves = player(before, f.moritani).reserves;
const game = act(before, f.moritani, {type:'decision', amount:0});
assert.equal(player(game, f.moritani).reserves, reserves);
assert.equal(game.pendingAmbassador ?? null, null);
assert.equal(game.pendingTerrorEntry, null);
assert.notEqual(game.decision?.kind, 'advisor');
assert.equal(player(game, f.entrant).spice, originalSpice - price);
});
void test('Advanced direct Sneak can trigger the retained Guild Ambassador exactly once', () => {
const {f, game: before, originalSpice, price} = revealSneak(false);
const reserves = player(before, f.owner).reserves, spice = player(before, f.owner).spice;
let game = act(before, f.moritani, {type:'decision', amount:1});
game = advanceTo(game, state => !!state.pendingAmbassador);
const entry = game.pendingAmbassador; assert.ok(entry);
game = act(game, f.owner, {type:'decision', event:entry.event, trigger:true, beneficiary:f.owner});
assert.equal(game.pendingAmbassador?.stage, 'ship');
const target = guildAmbassadorShipments(game, f.owner).destinations.find(row => row.territory === 'polar_sink');
assert.ok(target);
game = act(game, f.owner, {type:'decision', event:entry.event, amount:1, elite:0,
territory:target.territory, sector:target.sector});
game = advanceTo(game, state => !state.pendingAmbassador);
assert.equal(game.pendingAmbassador, null);
assert.equal(game.pendingTerrorEntry, null);
assert.equal(player(game, f.owner).reserves, reserves - 1);
assert.equal(player(game, f.owner).spice, spice);
assert.equal(player(game, f.entrant).spice, originalSpice - price);
assert.equal(player(game, f.entrant).forces[`${f.destination}:${f.sector}`], 1);
assert.equal(game.ecazAmbassadors?.tokens.find(token => token.id === f.ambassador)?.zone, 'used');
});
@@ -69,6 +69,20 @@ function ready(state: Game) {
}
const queuedRide = { kind: 'wormRide', player: 'in', territory: 'hagga_basin' } as const;
void test('Advanced worm Sneak shows the temporary positive-entry guard and preserves the ride on zero', () => {
const { g } = table('sneakAttack');
let state = wormRide(g);
assert.match(viewGame(state, 'm').terrorEntry?.sneakAttack?.blocked ?? '', /worm ride/);
state = action(state, 'm', {type:'decision', reveal:true});
const before = structuredClone(state);
assert.throws(() => action(state, 'm', {type:'decision', amount:1}), /worm ride/);
assert.deepEqual(state, before);
state = action(state, 'm', {type:'decision', amount:0});
assert.equal(state.pendingArrivalOverlap?.stage, 'second');
state = action(state, 'ec', {type:'decision', event:state.pendingAmbassador!.event, decline:true});
assert.deepEqual(state.decision, queuedRide);
});
void test('Terror first keeps Robbery overflow custody and the ordered arrival intact', () => {
const { g, terror } = table('robbery');
seat(g, 'm').hand = g.deck.splice(0, 4);
+34
View File
@@ -0,0 +1,34 @@
import assert from 'node:assert/strict';
import { validateAmbassadors } from '../game/ecaz-ambassadors';
import { advancedOverlapChildFixture } from './fixture-advanced-overlap-child';
/** Genuine Advanced native lobby/setup and Guild shipment from the shared
* fixture. Select the existing Guild Ambassador and Sneak Attack markers at
* the earned clean turn boundary; no forces, spice or continuation are staged.
* The Guild entrant cannot trigger its own Ambassador, but Moritani can. */
export function advancedDirectSneakFixture(withBG: boolean) {
const f = advancedOverlapChildFixture('fremen', 'terror', withBG);
const game = structuredClone(f.game);
const ambassadors = game.ecazAmbassadors!;
const old = ambassadors.tokens.find(token => token.id === f.token);
const guild = ambassadors.tokens.find(token => token.effect === 'guild');
assert.ok(old && guild && old.zone === 'placed');
old.zone = 'supply'; old.location = null;
if (guild.zone === 'pool') {
const index = ambassadors.cohort.indexOf(old.id); assert.ok(index >= 0);
ambassadors.cohort[index] = guild.id;
old.zone = 'pool'; guild.zone = 'supply';
}
assert.equal(guild.zone, 'supply');
guild.zone = 'placed'; guild.location = f.destination;
validateAmbassadors(ambassadors);
const robbery = game.moritaniTerror!.tokens.find(token => token.id === f.terror);
const sneak = game.moritaniTerror!.tokens.find(token => token.kind === 'sneakAttack');
assert.ok(robbery && sneak);
robbery.status = 'available'; robbery.location = null;
// The six-seat shared fixture may already have staged Sneak at a second
// site. Move that same physical token rather than creating another one.
assert.ok(sneak.status === 'available' || sneak.status === 'placed');
sneak.status = 'placed'; sneak.location = f.destination;
return { ...f, game, ambassador:guild.id, sneak:sneak.id };
}