Resolve direct advanced Sneak arrivals
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
8a39f5a172
commit
b79e448deb
8 files changed
+213
-8
No files matched your search
+14
-2
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in new issue
Block a user