Prototype Semuta after Recruits in Basic play
Verify and publish checkpoint container / container (push) Successful in 10m20s

This commit is contained in:
Quintine committed 2026-10-28 22:19:12 +11:00
1 parent 0e9209531a
commit 1ba942138d
9 files changed
+311 -18

No files matched your search

+40 -3
View File
@@ -1659,7 +1659,7 @@ export type Game = {
kind: 'ordinaryCardDiscard';
player: string;
card: string;
effect: 'hajr' | 'ghola' | 'harvester' | 'weather' | 'atomics';
effect: 'hajr' | 'ghola' | 'harvester' | 'weather' | 'atomics' | 'recruits';
resume: NonNullable<Game['pendingNullentropy']>['resume'];
stateSignature: string;
}
@@ -2605,6 +2605,8 @@ export type Game = {
moritaniAssassinatePreview?: true;
/** Explicit development-only neutral Semuta reaction profile. */
semutaPreview?: true;
/** Fresh Basic independent-card preview for Semuta after Recruits only. */
semutaRecruitsPreview?: true;
moritaniAssassinate?: MoritaniAssassinateState;
moritaniAssassinateCallEvents?: string[];
moritaniAssassinateResume?: {
@@ -6230,6 +6232,8 @@ function ordinaryDiscardSignature(g: Game, next: OrdinaryDiscardContinuation) {
...(g.grummanCollection === undefined ? {} : { grummanCollection: g.grummanCollection }),
dukeVidal: g.dukeVidal,
revivalFreeIncome: g.revivalFreeIncome,
...(next.effect === 'recruits' ? {recruits: g.recruits, freeRevival: g.freeRevival,
revivalRules: g.revivalRules, revivalOrder: g.revivalOrder} : {}),
players: g.players.map((p) => ({
id: p.id,
faction: p.faction,
@@ -6243,6 +6247,7 @@ function ordinaryDiscardSignature(g: Game, next: OrdinaryDiscardContinuation) {
moved: p.moved,
shipped: p.shipped,
revived: p.revived,
...(next.effect === 'recruits' ? {freeForcesRevived: p.freeForcesRevived} : {}),
revivalCycle: p.revivalCycle,
...(p.ecazRevivalCohort === undefined ? {} : { ecazRevivalCohort: p.ecazRevivalCohort }),
leaderRevived: p.leaderRevived,
@@ -7809,6 +7814,9 @@ function tupileDiscardSignature(g: Game, c: TupileDiscardContinuation) {
}
function treacheryDiscardIntegrity(g: Game) {
const discovery = g.discoveryEnabled === true && !!g.discoveries && discoveryModeSupported(g);
requireRule(g.semutaRecruitsPreview === undefined || g.semutaRecruitsPreview === true &&
basicIndependentSemutaProfile(g) && !g.semutaPreview,
'This Recruits Semuta preview no longer matches its original Basic profile.');
requireRule(g.semutaPreview === undefined || g.semutaPreview === true &&
g.expansions.includes('choam') && !!byFaction(g, 'richese') &&
(!g.homeworlds || g.homeworldOccupationPreview === true && semutaHomeworldRoster(g)) &&
@@ -7999,7 +8007,7 @@ function treacheryDiscardIntegrity(g: Game) {
const owner = g.players.find((p) => p.id === c.player);
requireRule(
owner &&
['hajr', 'ghola', 'harvester', 'weather', 'atomics'].includes(
['hajr', 'ghola', 'harvester', 'weather', 'atomics', 'recruits'].includes(
c.effect,
) &&
batch.cause === `ordinary:${c.effect}` &&
@@ -8022,7 +8030,11 @@ function treacheryDiscardIntegrity(g: Game) {
'The consumed card no longer matches its pending revival income.',
);
requireRule(
c.effect === 'hajr'
c.effect === 'recruits'
? g.semutaRecruitsPreview === true && basicIndependentSemutaProfile(g) &&
g.phase === 4 && g.recruits?.turn === g.turn && g.recruits.player === c.player &&
g.recruits.card === c.card
: c.effect === 'hajr'
? g.phase === 5 &&
g.active === c.player &&
g.hajr.filter((id) => id === c.player).length === 1
@@ -9282,6 +9294,14 @@ function semutaOfferSupported(
continuation: NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
entries: FreshDiscardBatch['entries'],
) {
if (continuation.kind === 'ordinaryCardDiscard' && continuation.effect === 'recruits')
return g.semutaRecruitsPreview === true && basicIndependentSemutaProfile(g) &&
g.status === 'playing' && g.phase === 4 && g.recruits?.turn === g.turn &&
g.recruits.player === continuation.player && g.recruits.card === continuation.card &&
entries.length === 1 && entries[0].publicFace && entries[0].discardedBy === continuation.player &&
entries[0].card.id === RECRUITS_CARD_ID && entries[0].card.effect === 'recruits' &&
!continuation.resume.response && !continuation.resume.decision &&
!continuation.resume.pendingKarama && !continuation.resume.phaseOpening;
if (continuation.kind === 'kullSuccessDiscard' || continuation.kind === 'kullCounterDiscard')
return g.semutaPreview === true && g.advanced === false && bareBasicSemutaProfile(g) &&
!!byFaction(g, 'richese') && kullSemutaParentSupported(kullSource(g, continuation.frame), continuation.frame) &&
@@ -12928,6 +12948,17 @@ export function initializeMoritaniAssassinateGameForAudit(state: Game): Game {
'Assassination requires Advanced Moritani in a supported selected native roster and module composition.');
return initializeFactionExpansionsGameForAudit(state);
}
/** Independent Ecaz cards with Richese and classic factions, without other
* modules, reuse the already supported Basic Semuta discard machinery. */
function basicIndependentSemutaProfile(g: Game): boolean {
return g.advanced === false && g.ecazTreachery === true && g.expansions.length === 1 &&
g.expansions[0] === 'choam' && !!byFaction(g, 'richese') &&
g.players.length >= 2 && g.players.length <= 6 &&
g.players.every(p => p.faction === 'richese' || faction(p.faction).expansion === 'base') &&
!g.homeworlds && !g.nexusCards && !g.leaderSkills && !g.techTokens && !g.strongholdCards &&
!g.discoveryEnabled && !g.discoveries && !g.discoveryStash && !g.greatMaker &&
!g.kullPreview && !g.nexusKullPreview && !g.ecazOccupyPreview && !g.moritaniAssassinatePreview;
}
/** Bare native Basic starts share the original deck/roster validation. Optional
* modules retain their own explicit Semuta envelopes; saved games are not upgraded. */
function bareBasicSemutaProfile(g: Game): boolean {
@@ -13123,6 +13154,8 @@ function independentCardModulesProfile(g: Game): boolean {
/** Shared fresh setup for the independent three-card Ecaz variant. */
export function initializeEcazTreacheryGameForAudit(state: Game): Game {
requireRule(!state.ecazTreachery, 'Ecaz Treachery Cards cannot redeal an existing inventory.');
requireRule(state.semutaRecruitsPreview === undefined,
'Fresh independent cards cannot inherit an existing Recruits Semuta marker.');
const g = structuredClone(state);
g.ecazTreachery = true;
const discovery = independentDiscoveryProfile(g) && discoveryModuleProfile(g);
@@ -13139,6 +13172,7 @@ export function initializeEcazTreacheryGameForAudit(state: Game): Game {
requireFreshFactionInventory(g);
}
const initialized = initializeSetupGameForAudit(g, homeworlds, independentNexusProfile(g), false, discovery, false, false, true, true);
if (basicIndependentSemutaProfile(initialized)) initialized.semutaRecruitsPreview = true;
if (byFaction(initialized, 'ecaz')) initialized.ecazOccupyPreview = true;
if (moritaniAssassinateModeSupported(initialized)) initializeMoritaniAssassinateState(initialized);
return initialized;
@@ -41692,6 +41726,8 @@ function applyActionInner(
`${p.name} played Recruits. Current free revival rates are doubled and the ordinary force revival limit is seven for this turn.`,
{ faction: p.faction, name: 'Recruits' },
);
if (execution === 'live' && g.semutaRecruitsPreview && basicIndependentSemutaProfile(g))
stageOrdinaryCardDiscard(g, p, card);
return g;
}
if (t === 'card' && action.card === 'richese-juice-of-sapho') {
@@ -45915,6 +45951,7 @@ export function viewGame(state: Game, id: string, now?: number) {
botsPending: g.botsPending ?? false,
semutaReaction: projectedSemutaReaction(g, me),
semutaPreview: g.semutaPreview === true,
semutaRecruitsPreview: g.semutaRecruitsPreview === true,
automaticContinuationPending: (!!g.pendingTreacheryDiscard && !g.pendingTreacheryDiscard.reaction) || homeworldRevealPending(g) || homeworldShipmentAutomatic(g) || homeworldOccupiedIncomeAutomatic(g) || grummanCollectionAutomatic(g) || tupileAutomatic(g),
botNextActionAt: g.botNextActionAt ?? null,
code: g.code,
+1
View File
@@ -6606,6 +6606,7 @@ export const RULE_TOPICS: RuleTopic[] = [
steps: [
'The separate three-card variant includes Recruits, Reinforcements and Harass & Withdraw. It does not require the Ecaz faction. A development preview includes all three physical cards before setup; Recruits, bounded Reinforcements and Harass & Withdraw controls are connected.',
'The holder may play Recruits from its Revival panel. Discard the card once, then all players use the increased rates while this Revival phase remains open, including players who had already marked themselves ready. Completed returns still count toward their turn totals.',
'In a fresh Basic game with Richese, classic factions, the CHOAM deck and these independent cards, Semuta Drug may claim Recruits immediately after another player discards it. The Recruits benefit still lasts for the turn and cannot be played again. This preview covers the Recruits discard only; other Semuta discard opportunities in this combination remain unfinished.',
'Atreides rises from two free forces to four; Fremen from three to six; Guild from one to two. The ordinary force limit becomes seven. Native unlimited CHOAM and Tleilaxu allowances remain unlimited, and their free rates become zero and four. Paid prices and separate elite limits still apply.',
'Free prevention still prevents free returns. Prior paid ordinary returns now have a visibly provisional policy: preserve payment, deduct only recorded free returns from the doubled free allowance and all returns from the total cap. Two free plus one paid Atreides may take two more free without refund; this is not a numerical user ruling. Fremen may now grant after play under the same provisional current-rate policy, with its original Karama response settled before commitment. Missing/inconsistent ledgers, pending transactions and same-turn replay remain guarded.',
'Clean classic2–6 Basic/Advanced Homeworlds compose through the fresh independent36-card setup. Double current native free rates including low-population bonuses, within the seven-force total. Provisionally recheck population and recorded free use on each separate request: low Fremen may take7 together, but1 crossing high leaves5 further free and cannot buy a seventh without Tleilaxu. Original typed destinations and special-force limits stay. Other overlays/rosters, public activation and whole-mode acceptance remain unfinished.',