Connect Semuta reactions before original native Tech award

This commit is contained in:
Quintine committed 2026-10-12 05:44:56 +11:00
1 parent a1d620d7b0
commit a6869b0166
12 files changed
+220 -14

No files matched your search

+3 -1
View File
@@ -1163,7 +1163,9 @@ export function GameTable({
{g.semutaPreview && (
<p className="notice" role="status">
Semuta Drug development preview · bounded fresh-discard reactions,
not complete Richese or certified expansion rules. Native Moritani
not complete Richese or certified expansion rules. Original Tech Tokens
may remain in supported fresh 3–6-seat CHOAM/Ix profiles; the original
winner’s award follows card cleanup and precedes Face Dance. Native Moritani
support is Basic only: clean shipment/movement Sabotage and Robbery
overflow; other arrival continuations remain guarded.{' '}
<a href="/rules?topic=card-richese-semuta-drug#card-richese-semuta-drug">Preview rules and limits</a>
+17
View File
@@ -29,6 +29,23 @@ 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 / original native Tech — 11 October.** Fresh three-through-six-seat
Basic/Advanced Richese games with CHOAM and optional Ix now retain original
unused Tech under existing native roster prerequisites. Printed and first-Storm
owners remain native. Mandatory/winner card reactions preserve token custody
until original cleanup, then the original winner takes one loser-owned token
before Face Dance. Other native Tech callers remain strict; Skills, Strongholds,
other overlays and the separate CHOAM/Ecaz Moritani entry remain excluded.
Four regressions pass after the original profile failure. Types/lint and
123 affected unit cases across eight files pass. Eight genuine fresh-start
runtime scenarios cover both modes and three through six seats; twenty-four
battle cases cover both producers, claim/pass and four legal AI profiles,
including sixteen original Tech AI choices. Real GameTable at390px claims
Hunter Seeker, takes only Heighliners for Atreides, restores native Face Dance
and reaches phase7 original Ix opening with the card/owners intact and no
horizontal overflow. No existing game/store, deployment or mode gate changed.
See [entry and reward contract](SEMUTA_SOURCE_UPDATE_20260907.md#30-september-2026--bounded-clean-discard-runtime).
**Semuta / native Auditor — 11 October.** Mandatory/winner discard reactions
now preserve the original audit before cleanup/income finish and its owner
offer. Frozen survival and opposing-player used IDs are unchanged; the current
+31
View File
@@ -1,5 +1,36 @@
# Dune implementation status
## 11 October 2026 — original native Tech after Semuta battle discards
The fresh CHOAM/optional-Ix Semuta entry now retains original unused Tech at
three through six seats, Basic/Advanced, under existing native roster rules.
An explicit Semuta context reuses the original profile predicate; existing
callers retain its strict default. Skills, Strongholds, other overlays, the
separate Basic CHOAM/Ecaz Moritani entry and played-game conversion stay closed.
Only the pending Tech battle-reaction exclusion is removed.
Four regressions reproduce the original profile failure and pass after the
cutover. Genuine setup/first Storm retains the printed Tleilaxu Axlotl owner
and assigns the remaining physical tokens. Mandatory/winner claim or decline
leaves custody unchanged until cleanup finishes, then the original winner
chooses one of two loser-owned tokens. Both selections preserve the other
owners and claimed card, reject repeat use, and precede native Face Dance.
Types/lint and 123 unit cases across eight affected files pass. Eight isolated
fresh-start runtime cases cover both modes and three through six seats.
Twenty-four battle cases cover both producers, claim/pass/four legal Semuta AI
profiles; sixteen also take an original legal Tech AI choice. Actual casualties,
physical Treachery custody and one award survive the native Face Dance decline.
Real GameTable at390px claims Hunter Seeker, shows both token choices, takes
only Heighliners for Atreides, restores Tleilaxu's Decline Face Dancer and
reaches phase7 original Ix opening with owners/card intact and no overflow.
The isolated Vite probe required route-order correction and a retry after its
dependency optimization reload; no production UI defect was observed.
Entry help, persistent warning, card tooltips, internal reference and Semuta
contracts are updated. Temporary probes/services/tabs removed. Existing games,
storage, user PDF and deployment untouched; no complete-mode claim.
## 11 October 2026 — native Auditor after Semuta battle discards
Removing only the audit reward exclusion admits mandatory/winner discard
+4 -1
View File
@@ -199,7 +199,10 @@ and an optional winner selection uses a third `battleCleanup` batch.
Every supported stage has a neutral all-seat offer; committed holders
select one fresh card, leave the others in the pile and resume exactly
once. Native capture, Face Dance, quoted CHOAM force income and Auditor remain
pending across the offer; tech and other rewards remain automatic without Semuta.
pending across the offer; original native Tech is also supported in the fresh
three-to-six-seat CHOAM/optional-Ix entry. Token custody stays unchanged until
card cleanup completes and the original winner takes one loser-owned token,
still before Face Dance. Other rewards remain automatic without Semuta.
The explicit Richese preview also accepts a fresh physical Ix deck.
Clean Thumper disposal stages `thumperDiscard` before `blowSpice`
+23
View File
@@ -78,6 +78,12 @@ Do not ask again about normal targets, forced-discard owner bookkeeping, simulta
The explicit `semuta` development profile starts a fresh Richese roster
with CHOAM & Richese and optionally the physical Ix deck, or the bounded Basic
native Moritani entry described below, without opening public expansion starts.
Original unused Tech Tokens may also remain in the CHOAM/optional-Ix entry at
three through six seats, Basic or Advanced, under its existing native roster
prerequisites. Genuine setup and printed/first-Storm ownership remain native.
This does not add Tech to the separate Basic CHOAM/Ecaz Moritani entry or admit
Skills, Strongholds, other overlays, played-game conversion or public starts.
It interrupts **clean public
ordinary-card, paid Box, retired Ornithopter, used Distrans, final
Truthtrance, four bounded Sapho scopes, pre-leader Residual Poison,
@@ -613,3 +619,20 @@ 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.
**11 October 2026 — original native Tech reward.** Mandatory and winner battle
card discards now offer Semuta before the original winner takes one loser-owned
Tech Token. Every token and income ledger remains in place across the reaction.
Claim or all-seat decline completes original casualties/card cleanup, then
restores the same token choice before native Face Dance. The card claimant does
not inherit the award. No token is a Treachery discard target.
Eight isolated genuine lobby/setup/first-Storm scenarios cover both modes and
three through six seats. Twenty-four battle runtime scenarios cover both
producers, claim/pass and four legal AI profiles; sixteen also use the original
winner's legal Tech AI choice. Physical Treachery custody, casualties and one
award survive the native Face Dance decline suffix. Real GameTable at 390px
claims Hunter Seeker, offers Axlotl Tanks/Heighliners to Atreides, takes only
Heighliners, then restores Tleilaxu's Decline Face Dancer control. Phase7 original
Ix opening retains the card and all three owners with no horizontal overflow.
This is local changed-path evidence, not complete Semuta or deployed acceptance.
+2 -2
View File
@@ -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, 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. Battle discards may pause before native Advanced capture, Face Dance, quoted CHOAM income or Auditor; original cleanup and the same opportunity resume once, using the current audited hand when relevant. A Basic Richese/Moritani CHOAM/Ecaz profile reacts to private Sabotage and Robbery overflow after clean shipment/movement arrivals. Clean reactive Baliset and La La La pause after cost without completing the stopped move or free revival. Proactive/final-Mentat Trip to Gamont preserves the exact ordinary/elite return or native No-Field reveal and capped recall, restoring its final owner window when applicable. The approved full-hand exchange spends Semuta without increasing the completed hand. Other pending rewards, producers and nested responses 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. Battle discards may pause before original Tech award, native Advanced capture, Face Dance, quoted CHOAM income or Auditor; original cleanup and the same opportunity resume once, using the current audited hand when relevant. Tech requires a supported fresh 3–6-seat CHOAM/optional-Ix profile. A Basic Richese/Moritani CHOAM/Ecaz profile reacts to private Sabotage and Robbery overflow after clean shipment/movement arrivals. Clean reactive Baliset and La La La pause after cost without completing the stopped move or free revival. Proactive/final-Mentat Trip to Gamont preserves the exact ordinary/elite return or native No-Field reveal and capped recall, restoring its final owner window when applicable. The approved full-hand exchange spends Semuta without increasing the completed hand. Other pending rewards, 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;
@@ -92,7 +92,7 @@ export function cardPresentation(card: VisibleCard): CardPresentation {
richese.card.effect === 'juiceOfSapho'
? 'Richese effect integration and verification are incomplete. Supported controls cover Once Around first or last while your bid remains open, bounded movement and battle-choice ordering, and aggressor during the shared pre-plan opportunity. Earlier bids stay committed. Later aggressor intervention and other phase or auction modes remain unfinished; expansion starts remain disabled.'
: richese.card.effect === 'semutaDrug'
? 'The explicit Semuta development preview offers neutral responses after clean public ordinary-card discards, paid Box disposal, retired Ornithopter movement (including a following Bene Gesserit Intrusion), Distrans, ordinary and queued definite Truthtrance answers, four Sapho scopes, unready pre-leader Residual Poison, printed Karama cancelling CHOAM Charity/Inflation/Bene Gesserit Charity or settling a normal-auction purchase or winning bid, a completed CHOAM sale, clean Kulon, proactive or reactive La La La/Baliset, proactive/final-Mentat Trip to Gamont with an ordinary/elite force or concealed No-Field, accepted Jubba Cloak before storm protection, Ix-deck Thumper/Amal, three battle discard stages (including before native Advanced capture, Face Dance, quoted CHOAM income or Auditor) and private normal-auction or Black Market Ixian ally replacements. The Basic Richese/Moritani CHOAM/Ecaz entry adds private Sabotage and Robbery overflow after clean shipment/movement arrivals, preserving the original victim/draw and arrival suffix. Commitment is owner-only; the approved full-hand exchange spends Semuta without increasing your completed hand. Other pending rewards, producers, nested responses and normal expansion starts remain unfinished.'
? 'The explicit Semuta development preview offers neutral responses after clean public ordinary-card discards, paid Box disposal, retired Ornithopter movement (including a following Bene Gesserit Intrusion), Distrans, ordinary and queued definite Truthtrance answers, four Sapho scopes, unready pre-leader Residual Poison, printed Karama cancelling CHOAM Charity/Inflation/Bene Gesserit Charity or settling a normal-auction purchase or winning bid, a completed CHOAM sale, clean Kulon, proactive or reactive La La La/Baliset, proactive/final-Mentat Trip to Gamont with an ordinary/elite force or concealed No-Field, accepted Jubba Cloak before storm protection, Ix-deck Thumper/Amal, three battle discard stages (including before original Tech award, native Advanced capture, Face Dance, quoted CHOAM income or Auditor) and private normal-auction or Black Market Ixian ally replacements. Tech requires a supported fresh 3–6-seat CHOAM/optional-Ix profile. The Basic Richese/Moritani CHOAM/Ecaz entry adds private Sabotage and Robbery overflow after clean shipment/movement arrivals, preserving the original victim/draw and arrival suffix. Commitment is owner-only; the approved full-hand exchange spends Semuta without increasing your completed hand. Other pending rewards, producers, nested responses and normal expansion starts remain unfinished.'
: richese.card.effect === 'mirrorWeapon'
? 'Mirror Weapon works in bounded CHOAM/Richese-deck classic-faction battles; copied Tooth and Stone choices resolve separately, and a winning physical Mirror may be kept even after copying Tooth or Artillery. This winner-retention treatment is a user-selected interpretation, not an official clarification. Combined modes and full Richese starts remain gated.'
: richese.card.effect === 'stoneBurner'
+6 -7
View File
@@ -5517,7 +5517,7 @@ function treacheryDiscardIntegrity(g: Game) {
requireRule(g.semutaPreview === undefined || g.semutaPreview === true &&
g.expansions.includes('choam') && !!byFaction(g, 'richese') &&
!g.homeworlds && !g.nexusCards && !g.leaderSkills && !g.discoveries &&
!g.discoveryEnabled && !g.techTokens && !g.strongholdCards,
!g.discoveryEnabled && (!g.techTokens || nativeFactionTechProfile(g, true)) && !g.strongholdCards,
'This Semuta preview no longer matches its original Richese profile.');
const pending = g.pendingTreacheryDiscard;
const sequence = g.treacheryDiscardSequence ?? 0;
@@ -6654,7 +6654,7 @@ function treacheryDiscardIntegrity(g: Game) {
} else throw new RuleError('Unknown saved discard continuation.');
}
/** Bounded public single-card reaction after completed effects. Battle cleanup
* may preserve native capture, Face Dance, CHOAM income and Auditor; other rewards stay gated.
* may preserve native Tech, capture, Face Dance, CHOAM income and Auditor; other rewards stay gated.
* Final flight arrival may resume native Intrusion, but other entry responses remain gated. */
function semutaOfferSupported(
g: Game,
@@ -6702,8 +6702,7 @@ function semutaOfferSupported(
if ((continuation.kind === 'battleCleanup' ||
continuation.kind === 'winnerMandatoryDiscard' ||
continuation.kind === 'battleResolved') &&
(g.pendingTech ||
g.pendingWinnerDiscards || g.moritaniRetention ||
(g.pendingWinnerDiscards || g.moritaniRetention ||
g.pendingIxSubstitution)) return false;
if (continuation.kind === 'terrorDiscard' &&
(continuation.entry.resume !== 'none' ||
@@ -9555,8 +9554,8 @@ export function initializeMoritaniAssassinateGameForAudit(state: Game): Game {
initializeMoritaniAssassinateState(g);
return g;
}
/** Development-only Semuta in fresh Richese games: optional Ix deck, or a
* bounded Basic native Moritani/E3 profile. Public expansion starts stay gated. */
/** Development-only Semuta in fresh Richese games: optional Ix deck and original
* native Tech profile, or bounded Basic native Moritani/E3. Public starts stay gated. */
export function initializeSemutaGameForAudit(state: Game): Game {
const nativeTerror = !state.advanced && state.expansions.length === 2 &&
state.expansions.includes('choam') && state.expansions.includes('ecaz') &&
@@ -9567,7 +9566,7 @@ export function initializeSemutaGameForAudit(state: Game): Game {
state.expansions.includes('choam') &&
(state.expansions.every(id => id === 'choam' || id === 'ix') || nativeTerror) &&
state.players.some(p => p.faction === 'richese'),
'Semuta needs a fresh Richese faction game with no optional modules: CHOAM with optional Ix, or Basic native Moritani with CHOAM and Ecaz decks and classic opponents.');
'Semuta needs a fresh Richese faction game: CHOAM with optional Ix and original native Tech prerequisites, or Basic native Moritani with CHOAM and Ecaz decks and classic opponents; other optional modules remain excluded.');
const g = initializeFactionExpansionsGameForAudit(state);
g.semutaPreview = true;
return g;
+4 -3
View File
@@ -26,15 +26,16 @@ export type FactionModuleProfile = {
nexusHarkonnenBetrayalPreview?: unknown;
};
/** Original E1/E2 families or standalone Ecaz/Moritani; no unrelated overlays. */
export function nativeFactionTechProfile(game: FactionModuleProfile): boolean {
/** Original E1/E2 families or standalone Ecaz/Moritani; explicit Semuta context only. */
export function nativeFactionTechProfile(game: FactionModuleProfile, semuta = false): boolean {
if (typeof game.advanced !== 'boolean' || !game.techTokens ||
game.players.length < 3 || game.players.length > 6 ||
game.expansions.length < 1 || game.expansions.length > 2 ||
(game.strongholdCards && !game.advanced) || game.leaderSkills ||
game.homeworlds || game.nexusCards || game.discoveryEnabled || game.discoveries ||
game.discoveryStash || game.greatMaker || game.ecazTreachery ||
game.semutaPreview || game.advancedPreview || game.kullPreview || game.nexusKullPreview ||
(game.semutaPreview && (!semuta || game.semutaPreview !== true)) ||
game.advancedPreview || game.kullPreview || game.nexusKullPreview ||
game.guildBetrayalPreview || game.richeseBetrayalPreview ||
game.nexusIxianReplacementPreview || game.nexusIxianBetrayalPreview ||
game.nexusHarkonnenBetrayalPreview) return false;
+1
View File
@@ -1176,6 +1176,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Mandatory and winner battle discards also pause before native Tleilaxu Face Dance. Claim or Continue preserves the original winner’s casualties and card cleanup, then restores the same private Face Dancer opportunity. Replacement, leader death and force sources remain native; the claimed Treachery card stays with its claimant, and no Face Dancer card becomes a Semuta target.',
'Mandatory and winner battle discards also pause before quoted Advanced CHOAM force-payment income. Claim or Continue leaves the original amount unpaid through casualties and card cleanup, then opens the same allow/Karama response. Allow pays once; cancellation sends that income to the bank without refunding the original force payments. The original post-battle market still completes afterward.',
'Mandatory and winner battle discards also pause before a native Auditor opportunity. Claim or Continue preserves the original survival limit and opponent-used card identities, then eligibility and the full cancellation price use the current opposing hand. A card acquired from the other combatant can be eligible if the audited player did not use it. Payment or private inspection resumes once; only CHOAM receives the historical snapshot.',
'Fresh three-through-six-seat Basic/Advanced Richese games with CHOAM and optional Ix may retain original unused Tech Tokens under the existing native roster prerequisites. Mandatory and winner discards now offer Semuta before the original winner takes one loser-owned token. Claim or Continue leaves every token and income ledger in place until original cleanup finishes; the winner’s selected award still precedes Face Dance. Skills, Strongholds, other overlays and the separate Basic Moritani entry remain excluded.',
'An ordinary Ghola played during battle also offers its consumed card after revival. Claim or Continue preserves the revived leader or forces and the opposing sealed plan, then normal planning resumes. It cannot revive again by resuming the receipt.',
'A completed paid Nullentropy Box search during battle offers only the used Box. Claim or Continue preserves the two-spice payment, private recovered card, already-shuffled pile and sealed opposing plan. The recovered card may be used in normal legal planning; the search is not repeated.',
'A clean Distrans transfer during battle also offers only the used Distrans. The private card has already reached its recipient; claim or Continue cannot repeat the gift or change a sealed plan. A recipient holding Semuta may claim the used Distrans and keep the already-transferred card.',
+58
View File
@@ -0,0 +1,58 @@
import assert from 'node:assert/strict';
import { applyAction, createGame, initializeSemutaGameForAudit, joinGame, newPlayer, viewGame, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
/** Genuine original lobby/setup/first Storm; later battle components are staged by conserved moves. */
export function initializeSemutaTechGame(advanced = false): Game {
let game = createGame('SEMUTATECH', newPlayer('r', 'Richese', 'richese'), advanced, ['choam', 'ix']);
joinGame(game, newPlayer('a', 'Atreides', 'atreides'));
joinGame(game, newPlayer('e', 'Tleilaxu', 'tleilaxu'));
game = applyAction(game, 'r', { type: 'techTokens', enabled: true });
for (const player of game.players) { player.bot = 'Medium'; player.ready = true; }
game = initializeSemutaGameForAudit(game);
for (let step = 0; (game.status === 'setup' || Object.values(game.techTokens!).some(token => !token.owner)) && step < 160; step++) {
let next: Game | undefined;
for (const player of game.players) {
const action = botActions(viewGame(game, player.id))[0];
if (action) { next = applyAction(game, player.id, action); break; }
}
assert.ok(next, 'Original setup and first Storm need a legal native continuation.');
game = next;
}
assert.equal(game.status, 'playing');
assert.ok(Object.values(game.techTokens!).every(token => token.owner));
assert.equal(game.techTokens!.axlotl.owner, 'e');
return game;
}
export function semutaTechBattleFixture(advanced = false): { game: Game; semuta: string; weapon: string; defense: string; cards: string[] } {
const game = initializeSemutaTechGame(advanced);
game.deck.push(...game.players.flatMap(player => player.hand));
for (const player of game.players) player.hand = [];
const take = (id: string) => {
for (const zone of [game.deck, game.discard, game.richeseCache!]) {
const index = zone.findIndex(card => card.id === id);
if (index >= 0) return zone.splice(index, 1)[0];
}
throw new Error(`Missing physical ${id}`);
};
const semuta = take(SEMUTA_DRUG_ID);
const weapon = take(game.deck.find(card => card.kind === 'projectile')!.id);
const defense = take(game.deck.find(card => card.kind === 'snooper')!.id);
game.players.find(player => player.id === 'r')!.hand.push(semuta);
game.players.find(player => player.id === 'a')!.hand.push(weapon);
game.players.find(player => player.id === 'e')!.hand.push(defense);
for (const player of game.players) {
Object.assign(player, { forces: player.id === 'r' ? {} : { 'arrakeen:10': 5 }, reserves: player.id === 'r' ? 20 : 15, tanks: 0 });
}
// Stage two public tokens with the loser to exercise the original owned choice;
// all three physical tokens and their existing ledgers remain original.
game.techTokens!.heighliners.owner = 'e';
game.techTokens!.production.owner = 'r';
Object.assign(game, { turn: 2, phase: 6, active: 'a', order: ['a', 'e', 'r'], ready: [],
response: null, decision: null, phaseOpening: null, movementRemaining: [] });
const cards = [...game.deck, ...game.discard, ...game.richeseCache!, ...game.players.flatMap(player => player.hand)].map(card => card.id).sort();
assert.equal(new Set(cards).size, cards.length);
return { game, semuta: semuta.id, weapon: weapon.id, defense: defense.id, cards };
}
+70
View File
@@ -0,0 +1,70 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { applyAction, type Game } from '../game/engine';
import { initializeSemutaTechGame, semutaTechBattleFixture } from './fixture-semuta-tech';
const reload = (game: Game): Game => JSON.parse(JSON.stringify(game));
const player = (game: Game, id: string) => game.players.find(p => p.id === id)!;
const inventory = (game: Game) => [...game.deck, ...game.discard, ...game.richeseCache!, ...game.players.flatMap(p => p.hand)].map(card => card.id).sort();
const pass = (game: Game) => {
const event = game.pendingTreacheryDiscard!.batch.event;
for (const id of ['a', 'e', 'r']) game = applyAction(game, id, { type: 'semutaPass', event });
return game;
};
for (const advanced of [false, true]) {
void test(`Semuta ${advanced ? 'Advanced' : 'Basic'} keeps original setup and first-Storm Tech ownership`, () => {
const game = initializeSemutaTechGame(advanced);
assert.equal(game.techTokens!.axlotl.owner, 'e');
assert.equal(new Set(Object.values(game.techTokens!).map(token => token.owner)).size, 3);
});
void test(`Semuta ${advanced ? 'Advanced' : 'Basic'} resumes one original token choice after mandatory and winner discards`, () => {
const f = semutaTechBattleFixture(advanced);
let state = applyAction(f.game, 'a', { type: 'chooseBattle', territory: 'arrakeen', target: 'e' });
for (let step = 0; step < 40; step++) {
if (state.response) {
const actor = state.players.find(p => !state.response!.passed.includes(p.id))!;
state = applyAction(state, actor.id, { type: 'passResponse' });
} else if (state.battle?.preparation)
state = applyAction(state, state.battle.preparation.owner, { type: 'declineBattlePower' });
else if (state.decision?.kind === 'fullPlanOffer')
state = applyAction(state, state.decision.player, { type: 'decision', decline: true });
else break;
}
for (const id of ['a', 'e']) state = applyAction(state, id, { type: 'battlePreparationReady', event: state.battle!.preLeader!.event });
state = applyAction(state, 'a', { type: 'battlePlan', dial: 2, support: advanced ? 2 : 0,
leader: player(state, 'a').leaders[0].id, weapon: f.weapon });
state = applyAction(state, 'e', { type: 'battlePlan', dial: 0, support: 0,
leader: player(state, 'e').leaders[0].id, defense: f.defense });
for (const id of ['a', 'e']) state = applyAction(state, id, { type: 'traitorCall', call: false });
assert.equal(state.pendingTreacheryDiscard?.reaction?.stage, 'offer');
const winner = applyAction(pass(reload(state)), 'a', { type: 'decision', discard: [f.weapon] });
assert.equal(winner.pendingTreacheryDiscard?.reaction?.stage, 'offer');
for (const pending of [state, winner]) {
const event = pending.pendingTreacheryDiscard!.batch.event;
const fresh = pending.pendingTreacheryDiscard!.batch.entries[0].card.id;
for (const claim of [false, true]) {
let resumed = claim ? applyAction(reload(pending), 'r', { type: 'semutaCommit', event }) : pass(reload(pending));
if (resumed.decision?.kind === 'battleCards') resumed = applyAction(resumed, 'a', { type: 'decision', discard: [] });
assert.equal(resumed.decision?.kind, 'techToken');
assert.equal(resumed.decision?.player, 'a');
assert.equal(resumed.techTokens!.axlotl.owner, 'e');
assert.equal(resumed.techTokens!.heighliners.owner, 'e');
for (const token of ['axlotl', 'heighliners'] as const) {
let awarded = applyAction(reload(resumed), 'a', { type: 'decision', token });
assert.equal(awarded.techTokens![token].owner, 'a');
assert.equal(awarded.techTokens![token === 'axlotl' ? 'heighliners' : 'axlotl'].owner, 'e');
assert.equal(awarded.techTokens!.production.owner, 'r');
assert.equal(awarded.decision?.kind, 'faceDance');
assert.throws(() => applyAction(awarded, 'a', { type: 'decision', token }));
awarded = applyAction(awarded, 'e', { type: 'decision', reveal: false });
assert.equal(awarded.techTokens![token].owner, 'a');
assert.equal(player(awarded, 'r').hand.some(card => card.id === fresh), claim);
assert.equal(awarded.discard.some(card => card.id === f.semuta), claim);
assert.deepEqual(inventory(awarded), f.cards);
assert.throws(() => applyAction(awarded, 'r', { type: 'semutaCommit', event }));
}
}
}
});
}
+1
View File
@@ -25,6 +25,7 @@ async function main() {
console.log(
'Usage: node --import tsx tools/start-prototype.ts --profile ix|discovery|homeworld-occupation|leader-skills|banker-income|factions|stronghold-factions|nexus|moritani-assassinate|ecaz-treachery|ecaz-occupy|semuta|kull|richese-betrayal|nexus-kull|guild-betrayal|ixian-replacement|ixian-betrayal|harkonnen-betrayal --db PATH --room CODE --version NUMBER --out /private/new-directory\nStarts only a fresh ready lobby for the selected development prototype. Stronghold-factions opts a fresh ready 2–6-seat Advanced roster with at least one selected native Ixians, Tleilaxu, CHOAM or Richese faction and otherwise classic opponents into Stronghold Cards, optionally preserving unused Tech Tokens at 3–6 seats. Select one or both distinct Ix/CHOAM decks; each native faction requires its family deck. The selected decks supply 47 Ix or combined, or 35 CHOAM canonical physical Treachery Cards, with a separate ten-card Richese cache when Richese plays. Native setup choices, HMS placement, Face Dancers, Richese cache and CHOAM Auditor remain genuine; custody starts unowned and is claimed by actual END Mentat. It does not redeal, reset or retrofit a saved game. Banker-income opts a fresh 2–6-seat classic Basic/Advanced base-deck roster, supported Basic Ixian/Tleilaxu/CHOAM/Moritani skill roster with its single required deck, or Advanced native Ixians, Tleilaxu and/or CHOAM plus classic roster with distinct required family decks, into automatic deferred normal income and native Mentat collection with all fourteen Leader Skills. Original bank payer costs exclude the separate Tleilaxu free-revival award; already-earned spice stays with its original faction under the existing provisional policy. Nexus enables the existing private Nexus setup. Moritani-assassinate explicitly opts into the bounded Advanced assassination preview. Semuta opts a fresh Richese roster into bounded clean-discard reactions; an included Ix deck supplies physical Thumper without opening public starts. Kull opts a fresh eligible CHOAM roster into the user-selected Karama interception preview. Richese-betrayal opts a fresh paired CHOAM/Richese Nexus roster into cache-purchase veto and sale-bank diversion. Nexus-kull opts a fresh CHOAM-plus-classic roster with CHOAM/Ix decks and Nexus into source-aware printed or any-Treachery-fueled Cunning Kull. Guild-betrayal opts a fresh classic roster with native Guild into full funded shipment-payment replacement; it preserves an explicitly enabled Homeworld module for Junction routes. Ixian-replacement opts a fresh classic Basic/Advanced base-deck roster, or Basic Tleilaxu plus classic opponents with the original Ix47 deck and no optional Sandtrout, into the exact normal-auction purchased-card replacement with Nexus alone. Native Ixians, Harkonnen buyers and special purchase sources remain unsupported. Ixian-betrayal opts a fresh 2–6-seat Basic/Advanced native Ixian plus classic/Tleilaxu roster with Ix 47-card Treachery and Nexus only into prevention of one actual native Bidding draw or Advanced Technology exchange, after native Karama counters. Harkonnen-betrayal opts a fresh 2–6-seat classic Basic/Advanced roster with native Harkonnen, base 33-card Treachery and Nexus alone into cancellation of an actual declared personal or allied Harkonnen Traitor Card after native counters, physical retirement and shuffle, and one private actual Mentat replacement. Modules, overlays, public starts and old-game retrofits remain excluded except for each profile’s documented composition. Nexus is not dealt initially: a qualifying alliance requires at least three seats; two unallied receivers require four. Backs up all rooms first, preserves sessions and existing games, and rejects stale versions. Normal game-start and publication gates remain closed.',
'\nSemuta additionally admits a fresh Basic roster with native Richese and Moritani, classic opponents, exactly CHOAM and Ecaz decks, and no optional modules. Native setup remains genuine. Private Sabotage and Robbery overflow discards may offer Semuta before a clean ordinary shipment/movement arrival resumes. The victim/draw and original payment/forces are already committed; candidate faces stay hidden until commitment. Advanced Moritani, native Ecaz, Ix/E3 mixtures, worm/Ambassador/overlap arrivals and public starts remain outside this entry.',
'\nSemuta with CHOAM and optional Ix additionally preserves original unused Tech Tokens in fresh 3–6-seat Basic/Advanced Richese lobbies under existing native roster prerequisites. Original setup and first-Storm owners remain. Mandatory/winner card discards react before the original winner takes one loser-owned token, still before Face Dance. Other modules, the separate CHOAM/Ecaz Moritani entry, played-game retrofit and public starts remain excluded.',
'\nEcaz-occupy (--profile ecaz-occupy) opts only a fresh ready 2–6-seat lobby with native Ecaz and classic/Ixian/Tleilaxu/CHOAM/Richese opponents (optional native Moritani). Advanced uses distinct selected ecaz/ix/choam decks; Basic uses the exact ecaz deck with classic/optional Moritani opponents. Ordinary33/47/35 cards retain real original offers/HMS/Auditor/Face Dancers and Richese cache. Explicit --ecaz-treachery independently adds the three physical variant cards, making36/50/38 (Advanced only). Basic combined battles require an even Ecaz force count; the preserved publisher odd-force casualty-rounding conflict rejects before any lead choice. Native Richese ordinary and marker-only pools compose with selected lead plans; original own mixed No-Field dialing, whole-plan disclosure and optional overlays remain guarded.' +
'\nLeader-skills retains existing classic and Basic native entries and adds native Basic Richese/CHOAM. Advanced Ixians, Tleilaxu, CHOAM and/or Richese plus classic rosters use distinct required family decks and all fourteen skills, with original cache, Face Dancers, Auditor exclusion and typed aftermath. Foreign revival grants no new skill. Separate non-Harkonnen Advanced Moritani uses the ecaz deck and original skill-first assassination preview. Native Ecaz supports its five ordinary discs, original Ambassador/Duke custody and ecaz deck with classic opponents in Basic/Advanced; Advanced Harkonnen retains its existing Duke capture exclusion. Normal Mentat questions use a uniform private card-show step in supported Leader Skills games. Banker-income additionally supports native Richese and Ecaz, never ordinary seller/allowed Emperor income or a bank grant as a payer cost. Basic auction recipients remain unchanged. Public activation, exhausted-cache counts, native Ixian Technology on Richese lots, own mixed No-Field, captured replacement, shared-Duke assignment, combined Occupy skills and other pending module/rulings remain guarded.',
'\nStronghold-factions additionally admits standalone Advanced Ecaz OR Moritani with classic opponents and the exact ecaz deck, six Stronghold Cards with optional unused Tech Tokens at 3–6 seats. Original setup and actual end-Mentat claims remain. Ecaz holds shared-control cards and only the actual selected holder-plan benefits. Native Moritani retains original assassination with printed bounty and one private Mentat replacement; Harkonnen is excluded there. An E3 pair, E1/E2/E3 mixtures and other modules remain separate.' +