Resume native Intrusion after final-flight Semuta response
This commit is contained in:
1 parent
9efa17b2b7
commit
18693b3207
8 files changed
+137
-15
No files matched your search
@@ -29,6 +29,20 @@ optional skill bands, combinations and public expansion starts remain open.
|
||||
Rules implementation continues; comprehensive assurance and administration
|
||||
remain deferred until all rules are implemented.
|
||||
|
||||
**Semuta / native Intrusion — 11 October.** Final Ornithopter movement now
|
||||
offers the retired card before the original Bene Gesserit Intrusion choice.
|
||||
Forces and move counters stay committed; claim or decline cannot move twice or
|
||||
skip the original arrival. Basic keeps no Advanced Intrusion; Ecaz/Moritani and
|
||||
overlap guards remain. Types/lint pass and 113 affected cases pass after replacing
|
||||
a mistaken advisor-storage assertion with actual fighter-count behavior.
|
||||
Ten isolated runtime scenarios cover both modes, claim/pass, fighter retention,
|
||||
two-group completion and four legal bots. Real GameTable at 390px proves Commit,
|
||||
Become advisors and Finish shipment & movement advancing to the next seat with
|
||||
unchanged force counts and no horizontal overflow. The observed stale Semuta
|
||||
tooltip now names Black Market replacement, native Intrusion and the approved
|
||||
full-hand exchange. No saved store or deployed game was targeted. See
|
||||
[the movement continuation](ORNITHOPTER_DISCARD_CONTINUATIONS.md#11-october-2026--semuta-before-native-intrusion).
|
||||
|
||||
**Semuta / Black Market replacement — 11 October.** A clean native Ixian
|
||||
allied replacement now offers Semuta before drawing from a Black Market
|
||||
purchase, as already supported for normal auctions. The discarded card belongs
|
||||
|
||||
@@ -1,5 +1,37 @@
|
||||
# Dune implementation status
|
||||
|
||||
## 11 October 2026 — Semuta before original Bene Gesserit Intrusion
|
||||
|
||||
Final Ornithopter movement already commits forces, counters and a typed
|
||||
discard-before-arrival frame. Its Semuta admission still excluded every roster
|
||||
with Bene Gesserit. That software limit is removed without changing the arrival
|
||||
suffix: claim or all-seat decline precedes the original native Intrusion once.
|
||||
Basic retains no Advanced Intrusion; Ecaz/Moritani and overlap guards remain.
|
||||
This is the existing ordering composition, not a publisher priority ruling.
|
||||
|
||||
The regression fails before the admission change. The repaired test checks
|
||||
fighters remain fighters during the Semuta pause, then become zero fighters
|
||||
after the original optional flip while the two physical counters remain.
|
||||
An initial assertion incorrectly expected the advisor marker to be a boolean;
|
||||
it was deleted rather than re-pinning its object representation. Final 113/113
|
||||
affected cases pass. TypeScript and lint pass.
|
||||
|
||||
Ten isolated CLI scenarios cover Basic/Advanced claim/pass, Intrusion decline,
|
||||
final two-group completion and four legal bots. Real GameTable at 390px, using
|
||||
the installed production Vinext image adapter, proves Commit recovering the
|
||||
physical Ornithopter, the original Become advisors control and Finish shipment
|
||||
& movement advancing Atreides→Richese. Atreides retains Imperial Basin2/Arrakeen1
|
||||
and BG retains its original two counters as advisors. No horizontal overflow.
|
||||
The observed stale Semuta availability tooltip is corrected to describe the
|
||||
connected private Black Market replacement, native Intrusion and approved
|
||||
full-hand exchange.
|
||||
|
||||
The fixture uses real setup/engine/table with conserved staged forces and no
|
||||
saved store. Temporary scripts/services/tabs were removed or closed. This is
|
||||
local changed-path proof, not deployed, full-module, asset or recovery acceptance.
|
||||
Contracts: [Semuta](SEMUTA_SOURCE_UPDATE_20260907.md),
|
||||
[Ornithopter](ORNITHOPTER_DISCARD_CONTINUATIONS.md).
|
||||
|
||||
## 11 October 2026 — Semuta before Black Market allied replacement draw
|
||||
|
||||
The native Black Market purchase already offers Ixian allied replacement and
|
||||
|
||||
@@ -2,6 +2,24 @@
|
||||
|
||||
7 September 2026. Retiring the played Ornithopter now preserves the exact remaining arrival or turn-ending step. This continues the shared discard infrastructure without enabling Semuta or changing any unresolved movement rule or expansion start gate.
|
||||
|
||||
## 11 October 2026 — Semuta before native Intrusion
|
||||
|
||||
The explicit Semuta development envelope now pauses the final public Ornithopter
|
||||
discard even when Bene Gesserit is seated. Its forces and movement counters are
|
||||
already committed. Claim or all-seat decline retires the same saved frame before
|
||||
the original native Intrusion choice; it cannot move the group again or skip that
|
||||
choice. Basic has no Advanced Intrusion. Ecaz, Moritani and overlapping entry
|
||||
reactions remain outside this Semuta boundary. This is the existing typed
|
||||
discard-before-arrival ordering, not a new publisher priority ruling.
|
||||
|
||||
Ten isolated engine scenarios cover Basic/Advanced claim/pass, retaining fighters,
|
||||
final two-group completion and four legal bots. In real GameTable at 390px,
|
||||
Commit recovers the physical Ornithopter; Become advisors flips the original
|
||||
two BG counters; Finish shipment & movement advances from Atreides to Richese.
|
||||
Atreides retains two counters in Imperial Basin and one in Arrakeen. No saved
|
||||
store was targeted. [Current Semuta scope](SEMUTA_SOURCE_UPDATE_20260907.md).
|
||||
The dated 7 September infrastructure and assurance evidence below is historical.
|
||||
|
||||
## Two endings, one physical card
|
||||
|
||||
A completed final group has already moved its forces and concealed marker, consumed its original-group quota and incremented movement counters. The retired card is moved from played escrow to the discard pile once, and the retirement log is committed. Its `ornithopterDiscard` frame with source `move` preserves only the final movement facts needed to log and open arrival decisions. Recovery never repeats force removal, placement, marker event generation, shipment completion or movement counters.
|
||||
|
||||
@@ -121,12 +121,14 @@ claim or decline marks the one protected territory and resumes
|
||||
the same storm exactly once. Canceled cards, Nexus conversion
|
||||
and competing parents retain their direct paths.
|
||||
|
||||
Completed Ornithopter movement now offers the same retired public card
|
||||
before its already committed arrival suffix only in a roster without
|
||||
Bene Gesserit intrusion, Ecaz Ambassador or Moritani Terror response
|
||||
sources. The final forces and move counter are held in the existing
|
||||
bound flight frame; claim or decline retires that frame and logs the
|
||||
arrival once. Mixed entry reactions remain unsupported for Semuta.
|
||||
Completed Ornithopter movement now offers its retired public card before
|
||||
the original arrival suffix, including native Bene Gesserit Intrusion.
|
||||
The final forces and move counter stay committed in the bound flight frame.
|
||||
Claim or decline retires that frame before logging arrival and opening the
|
||||
original optional fighter-to-advisor choice once. This order is the existing
|
||||
discard-before-arrival composition, not a new publisher priority ruling.
|
||||
Ecaz Ambassador, Moritani Terror and overlapping entry reactions remain outside
|
||||
this Semuta boundary. (11 October 2026)
|
||||
|
||||
Clean `battleResolved` now pauses the first public mandatory batch
|
||||
after combat result and actual card custody are committed but before
|
||||
|
||||
@@ -233,8 +233,7 @@ This neutral all-seat acknowledgement is a conservative **application
|
||||
policy**, not a newly discovered publisher timing rule. Full-hand
|
||||
Semuta uses the explicit user-approved atomic exchange; its completed
|
||||
hand count is unchanged. Pending gifts, competing responses,
|
||||
completed Ornithopter
|
||||
arrivals with BG intrusion, Ecaz Ambassador or Moritani Terror,
|
||||
completed Ornithopter arrivals with Ecaz Ambassador or Moritani Terror,
|
||||
queued or promise-bound Truthtrance, pending battle rewards and other
|
||||
typed producers retain their automatic continuation; Semuta cannot
|
||||
claim those events yet.
|
||||
@@ -245,3 +244,17 @@ from this omission. The
|
||||
separate [reaction privacy audit](SEMUTA_PRIVACY_REVIEW.md) explains why
|
||||
auto-closing only when nobody secretly holds the card would leak
|
||||
possession. The main game and public expansion gates remain closed.
|
||||
|
||||
**11 October 2026 — native Intrusion suffix.** Final Ornithopter movement now
|
||||
offers the retired public card to Semuta before native Bene Gesserit Intrusion.
|
||||
Physical movement and counters have already committed; claim or decline resumes
|
||||
the same arrival and optional fighter-to-advisor choice once. Basic still has
|
||||
no Advanced Intrusion. Range3 and final two-group completion work; other entry
|
||||
families and overlap guards are unchanged. This is the existing typed
|
||||
discard-before-arrival composition, not a new publisher priority ruling.
|
||||
Ten isolated engine scenarios prove claim/pass in both modes, Intrusion decline,
|
||||
two-group completion and four legal bots. Real GameTable at 390px proves Commit,
|
||||
the original Become advisors control and Finish shipment & movement advancing
|
||||
to the next seat with unchanged force counts. Public/full-mode gates stay closed.
|
||||
TypeScript/lint pass; the final affected union passes 113 cases. The regression
|
||||
checks actual fighter counts rather than the incidental advisor marker shape.
|
||||
@@ -40,7 +40,7 @@ export function richeseCardActionBlock(card: VisibleCard): string | null {
|
||||
if (definition?.card.effect === 'distrans')
|
||||
return 'Choose a recipient and card in the Distrans transfer panel.';
|
||||
if (definition?.card.effect === 'semutaDrug')
|
||||
return 'Use the neutral fresh-discard response in the Semuta development preview. Clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, four Sapho scopes, unready pre-leader Residual Poison, printed Karama canceling CHOAM Charity, Inflation or Bene Gesserit Charity, battle-card and Ix-deck Thumper/Amal discards are connected; pending rewards, other producers and full-hand sequencing remain unfinished.';
|
||||
return 'Use the neutral fresh-discard response in the Semuta development preview. Clean ordinary-card, paid Box, retired Ornithopter (before native Intrusion), Distrans, Truthtrance, four Sapho scopes, unready pre-leader Residual Poison, printed Karama canceling CHOAM Charity, Inflation or Bene Gesserit Charity, battle-card and Ix-deck Thumper/Amal discards are connected, as are private normal-auction and Black Market Ixian allied replacements. The approved full-hand exchange spends Semuta without increasing the completed hand. Pending rewards, other producers and nested responses remain unfinished.';
|
||||
return definition && definition.card.effect !== 'karama'
|
||||
? 'This Richese card’s effect is not implemented yet. You can inspect its rules.'
|
||||
: null;
|
||||
|
||||
+2
-3
@@ -6640,7 +6640,7 @@ function treacheryDiscardIntegrity(g: Game) {
|
||||
}
|
||||
/** Bounded public single-card reaction after clean completed effects. Battle
|
||||
* winner cleanup may resume only without other pending battle rewards; final
|
||||
* flight arrival excludes factions that open intrusion or entry responses. */
|
||||
* flight arrival may resume native Intrusion, but other entry responses remain gated. */
|
||||
function semutaOfferSupported(
|
||||
g: Game,
|
||||
continuation: NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
|
||||
@@ -6674,8 +6674,7 @@ function semutaOfferSupported(
|
||||
!(continuation.kind === 'ornithopterDiscard' &&
|
||||
(continuation.source === 'end' ||
|
||||
(continuation.source === 'move' &&
|
||||
!g.players.some(p => p.faction === 'beneGesserit' ||
|
||||
p.faction === 'ecaz' || p.faction === 'moritani') &&
|
||||
!g.players.some(p => p.faction === 'ecaz' || p.faction === 'moritani') &&
|
||||
!g.pendingArrivalOverlap))) &&
|
||||
!(continuation.kind === 'truthtranceDiscard' &&
|
||||
continuation.consumed.source === undefined &&
|
||||
|
||||
@@ -2,6 +2,7 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { applyAction, createGame, initializeSemutaGameForAudit, joinGame, newPlayer, normalizeAutomaticGame, viewGame, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { fighterCount } from '../game/advisors';
|
||||
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
||||
import { spiceDeck } from '../game/cards';
|
||||
import { saphoAggressorGame, aggressorAction, prepareAggressorPlans } from './fixture-sapho-aggressor';
|
||||
@@ -22,11 +23,11 @@ function inventory(g: Game) {
|
||||
assert.equal(new Set(ids).size, ids.length);
|
||||
return ids;
|
||||
}
|
||||
function fixture(ix = false, ixianAlly = false, advanced = false) {
|
||||
function fixture(ix = false, ixianAlly = false, advanced = false, opponent: 'emperor' | 'beneGesserit' = 'emperor') {
|
||||
let g = createGame('SEMUTAQ2', newPlayer('r', 'Richese', 'richese'), advanced, ix ? ['choam', 'ix'] : ['choam']);
|
||||
joinGame(g, newPlayer('a', 'Atreides', 'atreides'));
|
||||
joinGame(g, newPlayer('e', ixianAlly ? 'Ixians' : 'Emperor',
|
||||
ixianAlly ? 'ixians' : 'emperor'));
|
||||
joinGame(g, newPlayer('e', ixianAlly ? 'Ixians' : opponent === 'beneGesserit' ? 'Bene Gesserit' : 'Emperor',
|
||||
ixianAlly ? 'ixians' : opponent));
|
||||
for (const p of g.players) { p.bot = 'Medium'; p.ready = true; }
|
||||
g = initializeSemutaGameForAudit(g);
|
||||
for (let step = 0; g.status === 'setup' && step < 80; step++) {
|
||||
@@ -538,6 +539,49 @@ void test('a retired Ornithopter can be claimed after one flight group without r
|
||||
assert.throws(() => applyAction(claimed, 'r', { type: 'semutaCommit', event }));
|
||||
});
|
||||
|
||||
void test('final Ornithopter discard resolves Semuta before the original Bene Gesserit intrusion without moving twice', () => {
|
||||
const { g, semuta, hajr } = fixture(false, false, true, 'beneGesserit');
|
||||
player(g, 'a').hand = [];
|
||||
g.deck.push(hajr);
|
||||
const ornithopter = take(g, 'richese-ornithopter');
|
||||
player(g, 'a').hand.push(ornithopter);
|
||||
Object.assign(player(g, 'a'), { forces: { 'imperial_basin:10': 3 }, reserves: 17, shipped: true });
|
||||
Object.assign(player(g, 'e'), { forces: { 'arrakeen:10': 2 }, reserves: 18, advisors: {} });
|
||||
const original = inventory(g);
|
||||
const pending = applyAction(g, 'a', {
|
||||
type: 'move', movementCard: ornithopter.id, ornithopter: 'range3',
|
||||
forces: { 'imperial_basin:10': 1 }, territory: 'arrakeen', sector: 10,
|
||||
});
|
||||
assert.equal(pending.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
||||
assert.equal(pending.decision, null);
|
||||
assert.equal(fighterCount(player(pending, 'e'), 'arrakeen'), 2);
|
||||
assert.equal(player(pending, 'a').moved, 1);
|
||||
assert.deepEqual(player(pending, 'a').forces, { 'imperial_basin:10': 2, 'arrakeen:10': 1 });
|
||||
const event = pending.pendingTreacheryDiscard!.batch.event;
|
||||
let declined = reload(pending);
|
||||
for (const id of ['a', 'e', 'r']) declined = applyAction(declined, id, { type: 'semutaPass', event });
|
||||
const claimed = applyAction(reload(pending), 'r', { type: 'semutaCommit', event });
|
||||
for (const resumed of [declined, claimed]) {
|
||||
assert.equal(resumed.pendingTreacheryDiscard, null);
|
||||
assert.equal(resumed.decision?.kind, 'intrusion');
|
||||
assert.equal(resumed.decision?.player, 'e');
|
||||
let finished = applyAction(resumed, 'e', { type: 'decision', accept: true });
|
||||
while (finished.response?.kind === 'advisorFlip') {
|
||||
const actor = finished.players.find(p => !finished.response!.passed.includes(p.id))!;
|
||||
finished = applyAction(finished, actor.id, { type: 'passResponse' });
|
||||
}
|
||||
assert.equal(finished.decision, null);
|
||||
assert.equal(fighterCount(player(finished, 'e'), 'arrakeen'), 0);
|
||||
assert.deepEqual(player(finished, 'e').forces, { 'arrakeen:10': 2 });
|
||||
assert.equal(player(finished, 'a').moved, 1);
|
||||
assert.deepEqual(player(finished, 'a').forces, { 'imperial_basin:10': 2, 'arrakeen:10': 1 });
|
||||
assert.deepEqual(inventory(finished), original);
|
||||
assert.throws(() => applyAction(finished, 'r', { type: 'semutaCommit', event }));
|
||||
}
|
||||
assert.equal(player(claimed, 'r').hand.some(card => card.id === ornithopter.id), true);
|
||||
assert.equal(claimed.discard.some(card => card.id === semuta.id), true);
|
||||
});
|
||||
|
||||
void test('a completed private Distrans transfer offers only its public used card', () => {
|
||||
const { g, semuta, hajr } = fixture();
|
||||
const distrans = take(g, 'richese-distrans');
|
||||
|
||||
Reference in new issue
Block a user