Prototype neutral Semuta reactions on clean ordinary discards
This commit is contained in:
1 parent
55b47ce039
commit
9ea50a792e
18 files changed
+908
-20
No files matched your search
@@ -0,0 +1,28 @@
|
||||
import type { Action, GameView } from './engine';
|
||||
import { SEMUTA_DRUG_ID } from './semuta-drug';
|
||||
|
||||
/** The offer is public, but only this seat's hand can justify committing. The
|
||||
* fresh discard faces are never inspected until the private select stage. */
|
||||
export function botSemutaActions(g: GameView): Action[] {
|
||||
const reaction = g.semutaReaction;
|
||||
if (g.status !== 'playing' || !reaction?.event) return [];
|
||||
|
||||
if (reaction.stage === 'select') {
|
||||
// Only the committed claimant receives candidates. A mandatory selection
|
||||
// must still complete when no strategic preference is available.
|
||||
const choice = reaction.candidates.find((card) => card.id);
|
||||
return choice
|
||||
? [{ type: 'semutaSelect', event: reaction.event, card: choice.id }]
|
||||
: [];
|
||||
}
|
||||
|
||||
if (reaction.stage !== 'offer' || reaction.passed) return [];
|
||||
const own = g.players.find((player) => player.id === g.me);
|
||||
const hasSemuta = own?.hand?.some(
|
||||
(card) => card.id === SEMUTA_DRUG_ID &&
|
||||
card.kind === 'special' && card.effect === 'semutaDrug',
|
||||
);
|
||||
if (reaction.canCommit && !reaction.blocked && hasSemuta)
|
||||
return [{ type: 'semutaCommit', event: reaction.event }];
|
||||
return [{ type: 'semutaPass', event: reaction.event }];
|
||||
}
|
||||
@@ -101,6 +101,7 @@ import { sandmasterWormCollection } from './sandmaster-worm';
|
||||
import { richeseCardDefinition } from './richese-cards';
|
||||
import { presenceAt } from './force-presence';
|
||||
import { botArrivalBlock, botWormRideArrivalBlock } from './bot-arrival';
|
||||
import { botSemutaActions } from './bot-semuta';
|
||||
import { validateCohortSelection, type OrnithopterMode } from './ornithopter';
|
||||
import { fighterCount, isAdvisor } from './advisors';
|
||||
import { casualtyOptions, maxCombatDial, maxCombatSupport, type CombatForces } from './combat';
|
||||
@@ -3822,6 +3823,7 @@ function standaloneGholaAction(g: GameView, ordinary: Action[]): Action | null {
|
||||
|
||||
/** Obligations apply across policy branches, including choosing to move first. */
|
||||
export function botActions(g: GameView): Action[] {
|
||||
if (g.semutaReaction) return botSemutaActions(g);
|
||||
const fremenRevival = g.nexusFremenRevival;
|
||||
if (fremenRevival && !fremenRevival.blocked && fremenRevival.eliteOptions.length) {
|
||||
const own = g.players.find(player => player.id === g.me);
|
||||
|
||||
@@ -39,6 +39,8 @@ export function richeseCardActionBlock(card: VisibleCard): string | null {
|
||||
return 'Use the Juice of Sapho panel to choose an available order change or become battle aggressor before plans.';
|
||||
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. Only clean public ordinary-card discards are connected; other discard events remain unfinished.';
|
||||
return definition && definition.card.effect !== 'karama'
|
||||
? 'This Richese card’s effect is not implemented yet. You can inspect its rules.'
|
||||
: null;
|
||||
@@ -89,6 +91,8 @@ export function cardPresentation(card: VisibleCard): CardPresentation {
|
||||
availability:
|
||||
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, with owner-only commitment and a conservative free-slot requirement. Other discard producers, full-hand sequencing 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'
|
||||
|
||||
+187
-7
@@ -377,7 +377,7 @@ import {
|
||||
resolveNullentropyBox,
|
||||
} from './nullentropy-box';
|
||||
import { transferDistrans } from './distrans';
|
||||
import type { FreshDiscardBatch } from './semuta-drug';
|
||||
import { SEMUTA_DRUG_ID, committedSemutaCandidates, resolveSemutaDrug, type FreshDiscardBatch, type SemutaContext } from './semuta-drug';
|
||||
import {
|
||||
prepareRicheseGift,
|
||||
transferRicheseGift,
|
||||
@@ -1105,6 +1105,9 @@ export type Game = {
|
||||
pendingTreacheryDiscard?: {
|
||||
sequence: number;
|
||||
batch: FreshDiscardBatch;
|
||||
reaction?:
|
||||
| { stage: 'offer'; passed: string[] }
|
||||
| { stage: 'select'; player: string; candidates: string[] };
|
||||
continuation:
|
||||
| { kind: 'ambassador'; entry: NonNullable<Game['pendingAmbassador']> }
|
||||
| { kind: 'discoveryStash'; event: string; owner: string; card: string }
|
||||
@@ -1682,6 +1685,8 @@ export type Game = {
|
||||
mentatQuestionPreview?: true;
|
||||
/** Private development opt-in while uniform hidden-eligibility UX is pending. */
|
||||
moritaniAssassinatePreview?: true;
|
||||
/** Explicit development-only neutral Semuta reaction profile. */
|
||||
semutaPreview?: true;
|
||||
moritaniAssassinate?: MoritaniAssassinateState;
|
||||
moritaniAssassinateCallEvents?: string[];
|
||||
moritaniAssassinateResume?: {
|
||||
@@ -4838,6 +4843,11 @@ function suspendedControlsIntegrity(
|
||||
auditorIntegrity(context);
|
||||
}
|
||||
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,
|
||||
'This Semuta preview no longer matches its original Richese profile.');
|
||||
const pending = g.pendingTreacheryDiscard;
|
||||
const sequence = g.treacheryDiscardSequence ?? 0;
|
||||
const resolved = g.resolvedTreacheryDiscardSequence ?? 0;
|
||||
@@ -4910,6 +4920,32 @@ function treacheryDiscardIntegrity(g: Game) {
|
||||
);
|
||||
ids.add(card.id);
|
||||
}
|
||||
const reaction = pending.reaction;
|
||||
requireRule(reaction !== undefined || !semutaOfferSupported(g, continuation, batch.entries),
|
||||
'The saved fresh discard lost its neutral Semuta reaction.');
|
||||
if (reaction !== undefined) {
|
||||
requireRule(semutaOfferSupported(g, continuation, batch.entries),
|
||||
'This saved Semuta opportunity has no supported fresh discard.');
|
||||
const physical = physicalTreacheryCards(g).filter(card => card.id === SEMUTA_DRUG_ID);
|
||||
requireRule(physical.length === 1 &&
|
||||
richeseCardDefinition(physical[0])?.card.effect === 'semutaDrug',
|
||||
'The saved Semuta reaction lost unique physical card custody.');
|
||||
if (reaction.stage === 'offer') {
|
||||
requireRule(Object.keys(reaction).sort().join(',') === 'passed,stage' &&
|
||||
Array.isArray(reaction.passed) &&
|
||||
new Set(reaction.passed).size === reaction.passed.length &&
|
||||
reaction.passed.length < g.players.length &&
|
||||
reaction.passed.every(id => g.players.some(p => p.id === id)),
|
||||
'The neutral Semuta passes lost their original participants.');
|
||||
} else if (reaction.stage === 'select') {
|
||||
const owner = g.players.find(p => p.id === reaction.player);
|
||||
requireRule(Object.keys(reaction).sort().join(',') === 'candidates,player,stage' &&
|
||||
owner && Array.isArray(reaction.candidates) && reaction.candidates.length >= 1 &&
|
||||
JSON.stringify(reaction.candidates) ===
|
||||
JSON.stringify(semutaCandidates(g, owner).map(card => card.id)),
|
||||
'The committed Semuta claim lost its private fresh candidates.');
|
||||
} else throw new RuleError('Unknown saved Semuta reaction stage.');
|
||||
}
|
||||
if (continuation?.kind === 'nullentropyDiscard') {
|
||||
const c = continuation;
|
||||
const owner = g.players.find((p) => p.id === c.player);
|
||||
@@ -5606,6 +5642,71 @@ function treacheryDiscardIntegrity(g: Game) {
|
||||
'The committed Kaitain discard no longer matches its payment and closing opportunity.');
|
||||
} else throw new RuleError('Unknown saved discard continuation.');
|
||||
}
|
||||
/** First playable Semuta boundary: one public ordinary-card discard with no
|
||||
* competing suspended transaction. Other typed frames retain their existing
|
||||
* automatic continuation until their reaction composition is integrated. */
|
||||
function semutaOfferSupported(
|
||||
g: Game,
|
||||
continuation: NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
|
||||
entries: FreshDiscardBatch['entries'],
|
||||
) {
|
||||
return g.semutaPreview === true && g.status === 'playing' &&
|
||||
g.players.some(p => p.faction === 'richese') &&
|
||||
continuation.kind === 'ordinaryCardDiscard' &&
|
||||
entries.length === 1 && entries[0].publicFace &&
|
||||
!continuation.resume.response && !continuation.resume.decision &&
|
||||
!continuation.resume.pendingKarama && !continuation.resume.phaseOpening &&
|
||||
!g.karamaShipping && !g.auction && !g.richeseAuction && !g.battle &&
|
||||
!g.pendingExchange && !g.pendingIxAlly && !g.pendingNullentropy &&
|
||||
!g.pendingAmbassador && !g.pendingTerrorEntry && !g.pendingShipment &&
|
||||
!g.pendingHomeworldShipment && !g.pendingRicheseGift &&
|
||||
!g.pendingRichesePurchaseIncome && !g.choamMarket &&
|
||||
!g.pendingChoamMarketGhola;
|
||||
}
|
||||
function semutaContext(g: Game, owner: Player): SemutaContext {
|
||||
const pending = g.pendingTreacheryDiscard!;
|
||||
return {
|
||||
batch: pending.batch, event: pending.batch.event, turn: g.turn, phase: g.phase,
|
||||
owner: owner.id, ownerHand: owner.hand, discard: g.discard,
|
||||
semutaId: SEMUTA_DRUG_ID, handLimit: handLimit(owner),
|
||||
incomingReservedSlots: 0, capacityPolicy: 'freeSlot', reservedTargetIds: [],
|
||||
};
|
||||
}
|
||||
function semutaCandidates(g: Game, owner: Player) {
|
||||
const context = semutaContext(g, owner);
|
||||
try {
|
||||
return committedSemutaCandidates(context,
|
||||
{ event: context.event, player: owner.id, semutaId: SEMUTA_DRUG_ID });
|
||||
} catch {
|
||||
throw new RuleError('This fresh Semuta claim is not available.');
|
||||
}
|
||||
}
|
||||
|
||||
function projectedSemutaReaction(g: Game, me: Player) {
|
||||
const pending = g.pendingTreacheryDiscard;
|
||||
const reaction = pending?.reaction;
|
||||
if (!pending || !reaction) return null;
|
||||
if (reaction.stage === 'select')
|
||||
return {
|
||||
event: pending.batch.event, stage: 'select' as const, passed: true,
|
||||
canCommit: false, blocked: null,
|
||||
candidates: reaction.player === me.id ? semutaCandidates(g, me) : [] as Card[],
|
||||
};
|
||||
const passed = reaction.passed.includes(me.id);
|
||||
const held = me.hand.some(card => card.id === SEMUTA_DRUG_ID &&
|
||||
richeseCardDefinition(card)?.card.effect === 'semutaDrug');
|
||||
const blocked = !held || passed ? null
|
||||
: me.hand.length >= handLimit(me)
|
||||
? 'A free hand slot is required before taking a card with Semuta Drug.'
|
||||
: pending.batch.entries.every(entry => entry.discardedBy === me.id)
|
||||
? 'No other player discarded a card in this fresh event.'
|
||||
: null;
|
||||
return {
|
||||
event: pending.batch.event, stage: 'offer' as const, passed,
|
||||
canCommit: held && !passed && !blocked, blocked, candidates: [] as Card[],
|
||||
};
|
||||
}
|
||||
|
||||
function stageTreacheryDiscard(
|
||||
g: Game,
|
||||
cause: string,
|
||||
@@ -5625,20 +5726,38 @@ function stageTreacheryDiscard(
|
||||
cause,
|
||||
entries: structuredClone(entries),
|
||||
},
|
||||
...(semutaOfferSupported(g, continuation, entries)
|
||||
? { reaction: { stage: 'offer' as const, passed: [] as string[] } } : {}),
|
||||
continuation: structuredClone(continuation),
|
||||
};
|
||||
treacheryDiscardIntegrity(g);
|
||||
}
|
||||
/** No reaction policy is selected here. Semuta activation remains unavailable.
|
||||
* The existing exchanges resume automatically from their exact committed stage. */
|
||||
function finishTreacheryDiscard(g: Game) {
|
||||
/** Retire a completed fresh discard before its typed continuation resumes. */
|
||||
function finishTreacheryDiscard(g: Game, claim?: { player: string; card: string }, finalPass?: string) {
|
||||
treacheryDiscardIntegrity(g);
|
||||
const pending = g.pendingTreacheryDiscard;
|
||||
if (!pending) return;
|
||||
const next = pending.continuation;
|
||||
// Retire before any suffix can draw or create the next semantic discard.
|
||||
requireRule(!pending.reaction ||
|
||||
(claim ? pending.reaction.stage === 'select' &&
|
||||
pending.reaction.player === claim.player &&
|
||||
pending.reaction.candidates.includes(claim.card) :
|
||||
pending.reaction.stage === 'offer' && finalPass !== undefined &&
|
||||
pending.reaction.passed.length === g.players.length - 1 &&
|
||||
g.players.some(p => p.id === finalPass) &&
|
||||
!pending.reaction.passed.includes(finalPass)),
|
||||
'Finish the fresh discard reaction before its continuation.');
|
||||
const owner = claim ? getPlayer(g, claim.player) : null;
|
||||
const result = claim && owner ? resolveSemutaDrug(semutaContext(g, owner), claim.card) : null;
|
||||
// Retire before the Semuta transfer or any suffix can draw the fresh card.
|
||||
g.resolvedTreacheryDiscardSequence = pending.sequence;
|
||||
g.pendingTreacheryDiscard = null;
|
||||
if (result && owner) {
|
||||
owner.hand = result.ownerHand;
|
||||
g.discard = result.discard;
|
||||
log(g, `${owner.name} played Semuta Drug and recovered one freshly discarded Treachery Card. The acquired card stays private.`,
|
||||
{ faction: owner.faction, name: 'Semuta Drug' });
|
||||
}
|
||||
if (next.kind === 'winnerMandatoryDiscard') {
|
||||
finishWinner(g, getPlayer(g, next.player), next.territory, next.optional);
|
||||
return;
|
||||
@@ -5722,6 +5841,40 @@ function finishTreacheryDiscard(g: Game) {
|
||||
finishBattle(g);
|
||||
}
|
||||
}
|
||||
|
||||
function applySemutaReaction(g: Game, id: string, action: Action) {
|
||||
const pending = g.pendingTreacheryDiscard;
|
||||
const reaction = pending?.reaction;
|
||||
requireRule(pending && reaction && action.event === pending.batch.event,
|
||||
'This fresh discard reaction is no longer available.');
|
||||
const player = getPlayer(g, id);
|
||||
if (reaction.stage === 'offer') {
|
||||
requireRule(!reaction.passed.includes(id), 'You already passed this discard.');
|
||||
if (action.type === 'semutaPass') {
|
||||
requireRule(Object.keys(action).sort().join(',') === 'event,type',
|
||||
'Pass only the current fresh discard.');
|
||||
if (reaction.passed.length + 1 === g.players.length)
|
||||
finishTreacheryDiscard(g, undefined, id);
|
||||
else reaction.passed.push(id);
|
||||
return;
|
||||
}
|
||||
requireRule(action.type === 'semutaCommit' &&
|
||||
Object.keys(action).sort().join(',') === 'event,type',
|
||||
'Choose whether to commit Semuta to this discard.');
|
||||
const candidates = semutaCandidates(g, player).map(card => card.id);
|
||||
pending.reaction = { stage: 'select', player: id, candidates };
|
||||
if (candidates.length === 1) finishTreacheryDiscard(g, { player: id, card: candidates[0] });
|
||||
else log(g, `${player.name} committed Semuta Drug to this fresh discard and must choose one card privately.`,
|
||||
{ faction: player.faction, name: 'Semuta Drug' });
|
||||
return;
|
||||
}
|
||||
requireRule(action.type === 'semutaSelect' &&
|
||||
Object.keys(action).sort().join(',') === 'card,event,type' &&
|
||||
id === reaction.player && typeof action.card === 'string' &&
|
||||
reaction.candidates.includes(action.card),
|
||||
'Choose one committed Semuta card from this fresh discard.');
|
||||
finishTreacheryDiscard(g, { player: id, card: action.card });
|
||||
}
|
||||
function place(p: Player, t: string, s: number, n: number, elite = 0) {
|
||||
const k = location(t, s);
|
||||
p.forces[k] = (p.forces[k] ?? 0) + n;
|
||||
@@ -7013,6 +7166,15 @@ export function initializeMoritaniAssassinateGameForAudit(state: Game): Game {
|
||||
g.moritaniAssassinateCallEvents = [];
|
||||
return g;
|
||||
}
|
||||
/** Development-only Semuta reaction in a fresh Richese roster; public starts stay gated. */
|
||||
export function initializeSemutaGameForAudit(state: Game): Game {
|
||||
requireRule(!state.semutaPreview && state.expansions.length === 1 &&
|
||||
state.expansions[0] === 'choam' && state.players.some(p => p.faction === 'richese'),
|
||||
'Semuta needs a fresh Richese faction game without other modules.');
|
||||
const g = initializeFactionExpansionsGameForAudit(state);
|
||||
g.semutaPreview = true;
|
||||
return g;
|
||||
}
|
||||
/** Prototype-only Discovery setup. Public starts stay gated while remaining effects are connected. */
|
||||
export function initializeDiscoveryGameForAudit(state: Game): Game {
|
||||
requireRule(state.discoveryEnabled === true && !state.discoveries && !state.discoveryStash && !state.greatMaker,
|
||||
@@ -22966,6 +23128,13 @@ export function applyAction(state: Game, id: string, action: Action): Game {
|
||||
}
|
||||
if (state.pendingTreacheryDiscard) {
|
||||
getPlayer(state, id);
|
||||
if (state.pendingTreacheryDiscard.reaction &&
|
||||
['semutaPass', 'semutaCommit', 'semutaSelect'].includes(action?.type)) {
|
||||
const g = structuredClone(state);
|
||||
applySemutaReaction(g, id, action);
|
||||
treacheryDiscardIntegrity(g);
|
||||
return g.pendingTreacheryDiscard?.reaction ? g : normalizeAutomaticGame(g);
|
||||
}
|
||||
requireRule(
|
||||
action?.type === 'advanceBots',
|
||||
'Finish the committed discard continuation before another game action.',
|
||||
@@ -23064,6 +23233,10 @@ export function applyAction(state: Game, id: string, action: Action): Game {
|
||||
}
|
||||
}
|
||||
finishActionContinuations(g);
|
||||
if (g.pendingTreacheryDiscard?.reaction) {
|
||||
treacheryDiscardIntegrity(g);
|
||||
return g;
|
||||
}
|
||||
harkonnenExchangeIntegrity(g);
|
||||
finishLeaderSkillCustody(g, state);
|
||||
leaderSkillsIntegrity(g);
|
||||
@@ -23099,7 +23272,9 @@ function finishActionContinuations(g: Game) {
|
||||
if (g.pendingNullentropy) return;
|
||||
// Losing cleanup may finish casualties and produce the winner's mandatory
|
||||
// discard. Retire that next physical batch before exposing optional choices.
|
||||
for (let i = 0; g.pendingTreacheryDiscard && i < 16; i++) finishTreacheryDiscard(g);
|
||||
for (let i = 0; g.pendingTreacheryDiscard && !g.pendingTreacheryDiscard.reaction && i < 16; i++)
|
||||
finishTreacheryDiscard(g);
|
||||
if (g.pendingTreacheryDiscard?.reaction) return;
|
||||
requireRule(!g.pendingTreacheryDiscard, 'The automatic discard chain did not finish.');
|
||||
if (pendingNexusTraitors(g)) return;
|
||||
observeOccupation(g);
|
||||
@@ -23233,6 +23408,10 @@ export function normalizeAutomaticGame(state: Game): Game {
|
||||
normalizeCardNames(g);
|
||||
normalizeBattle(g);
|
||||
finishActionContinuations(g);
|
||||
if (g.pendingTreacheryDiscard?.reaction) {
|
||||
treacheryDiscardIntegrity(g);
|
||||
return g;
|
||||
}
|
||||
reconcileBattlePromises(g);
|
||||
reconcileShipmentPromises(g);
|
||||
settleAutomaticContinuations(g);
|
||||
@@ -27144,7 +27323,8 @@ export function viewGame(state: Game, id: string) {
|
||||
: null,
|
||||
schema: g.schema,
|
||||
botsPending: g.botsPending ?? false,
|
||||
automaticContinuationPending: !!g.pendingTreacheryDiscard || homeworldRevealPending(g) || homeworldShipmentAutomatic(g) || grummanCollectionAutomatic(g),
|
||||
semutaReaction: projectedSemutaReaction(g, me),
|
||||
automaticContinuationPending: (!!g.pendingTreacheryDiscard && !g.pendingTreacheryDiscard.reaction) || homeworldRevealPending(g) || homeworldShipmentAutomatic(g) || grummanCollectionAutomatic(g),
|
||||
botNextActionAt: g.botNextActionAt ?? null,
|
||||
code: g.code,
|
||||
version: g.version,
|
||||
|
||||
+16
-10
@@ -414,7 +414,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'A player whose hand becomes full is automatically passed, or submits zero in an unfinished Silent lot. A full-hand owner’s unbid cache card is removed automatically. If every hand is full, preparing the normal pool does not draw and lose unseen cards.',
|
||||
'AI supports cache choices and all three offered bidding methods using its own projected information. It currently skips the optional Black Market prelude. A seller in an already offered Black Market lot passes or submits zero.',
|
||||
'Some unresolved rules are explicitly blocked: changing the normal card count after cache-auction cancellation, exhausted-cache arithmetic, positive Black Market self-bids, and advanced Ixian Technology substitution on special lots. These are implementation guards while the rules are resolved, not claims of official prohibitions.',
|
||||
'Juice of Sapho now supports bounded first/last Once Around, movement and remaining battle-choice ordering; see its timing guide for current limits. Richese’s Karama uses the existing generic handler, Distrans supports a separate private hand transfer, Nullentropy Box supports a paid private discard search, Ornithopter supports its two movement modes, and Residual Poison supports a random opposing leader death before leader commitment in development fixtures. Portable Snooper supports ordinary and late poison defense. Stone Burner supports a guarded weapon commitment and revealed leader-effect choice. Mirror Weapon copies opposing revealed attacks, with independent copy-first choices for Tooth and Stone in bounded CHOAM/Richese-deck classic-faction battles. A winning physical Mirror may be kept even after copying activated Tooth or Artillery under the user-selected interpretation, not a publisher clarification. Semuta Drug and other Sapho timing modes remain unfinished; this does not certify every Karama interaction. Richese expansion starts remain disabled. Each component guide identifies its currently available action. Independent continuation and persisted-room checks pass. Desktop checks confirmed all ten enlarged guides, direct topic links, Once Around payment and restored private Silent bidding. Mobile and full Richese-game verification remain unfinished.',
|
||||
'Juice of Sapho now supports bounded first/last Once Around, movement and remaining battle-choice ordering; see its timing guide for current limits. Richese’s Karama uses the existing generic handler, Distrans supports a separate private hand transfer, Nullentropy Box supports a paid private discard search, Ornithopter supports its two movement modes, and Residual Poison supports a random opposing leader death before leader commitment in development fixtures. Portable Snooper supports ordinary and late poison defense. Stone Burner supports a guarded weapon commitment and revealed leader-effect choice. Mirror Weapon copies opposing revealed attacks, with independent copy-first choices for Tooth and Stone in bounded CHOAM/Richese-deck classic-faction battles. A winning physical Mirror may be kept even after copying activated Tooth or Artillery under the user-selected interpretation, not a publisher clarification. Semuta Drug has a bounded clean public ordinary-discard preview; other discard producers and Sapho timing modes remain unfinished. This does not certify every Karama interaction. Richese expansion starts remain disabled. Each component guide identifies its currently available action. Independent continuation and persisted-room checks pass. Desktop checks confirmed all ten enlarged guides, direct topic links, Once Around payment and restored private Silent bidding. Mobile and full Richese-game verification remain unfinished.',
|
||||
],
|
||||
related: [
|
||||
'choam-modules',
|
||||
@@ -625,7 +625,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'The server checks alliance, exact card ownership, capacity and commitments again before transfer. An obsolete pending gift aborts without a partial transfer and restores its interrupted flow. Unrelated players see the gift power, not its private card identity. A pending card cannot also be spent, exchanged or randomly extracted.',
|
||||
'After cancellation, trying the same card again in the same phase is currently blocked pending a ruling on repeated use. This is an explicit implementation guard, not a printed once-per-phase limit on all gifts. Other legally available cards are assessed separately.',
|
||||
'All four AI profiles conservatively give only the canonical Richese Karama during Bidding to free a full hand when the ally’s public card count is lower and there is room. They do not inspect ally hand contents or infer an unknown need for a card. Pending responses, decisions and Truthtrance take priority.',
|
||||
'All ten canonical Richese cards may be transferred when legal, and Karama has its existing card-effect handler. Distrans supports its separate card-transfer effect, Nullentropy Box supports paid private discard search, Ornithopter supports its two movement modes, and Residual Poison supports a random opposing leader death before leader commitment. Portable Snooper supports ordinary and late poison defense. Stone Burner supports a guarded weapon commitment and revealed leader-effect choice. Mirror Weapon copies an opposing revealed weapon in bounded CHOAM/Richese-deck classic-faction battles, with copy-first Tooth/Stone choices; physical-Mirror winner retention even after copied activated Tooth/Artillery is a user-selected interpretation. Juice of Sapho supports bounded Once Around and movement ordering. Semuta Drug and other Sapho timing modes remain unfinished. Desktop gift transfer and fresh-tab pending recovery have been checked in a synthetic table. Mobile, broader interactions and full Richese games remain pending; Richese starts remain disabled.',
|
||||
'All ten canonical Richese cards may be transferred when legal, and Karama has its existing card-effect handler. Distrans supports its separate card-transfer effect, Nullentropy Box supports paid private discard search, Ornithopter supports its two movement modes, and Residual Poison supports a random opposing leader death before leader commitment. Portable Snooper supports ordinary and late poison defense. Stone Burner supports a guarded weapon commitment and revealed leader-effect choice. Mirror Weapon copies an opposing revealed weapon in bounded CHOAM/Richese-deck classic-faction battles, with copy-first Tooth/Stone choices; physical-Mirror winner retention even after copied activated Tooth/Artillery is a user-selected interpretation. Juice of Sapho supports bounded Once Around and movement ordering. Semuta Drug has a bounded clean ordinary-discard preview; other discard producers and Sapho timing modes remain unfinished. Desktop gift transfer and fresh-tab pending recovery have been checked in a synthetic table. Mobile, broader interactions and full Richese games remain pending; Richese starts remain disabled.',
|
||||
],
|
||||
related: [
|
||||
'richese-cards',
|
||||
@@ -690,7 +690,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'A transfer must preserve binding battle answers, compulsory discards and both sides of a pending Harkonnen hand exchange. The recipient must retain room for its mandatory incoming return. Optional proposals that become unavailable can still finish through their existing decline or no-effect path.',
|
||||
'A different legal transfer can fill or free space for a pending Richese gift. The ordinary Distrans transfer resolves first, and the pending gift rechecks capacity before moving its own card. A canceled alliance gift does not automatically prevent using this separate card effect.',
|
||||
'All four AI profiles use their own legal projected cards. They may give an allied Bene Gesserit or CHOAM a Worthless card, or share a duplicate ordinary weapon, defense or Karama to free a full hand. They do not inspect the recipient’s private cards and preserve existing decision priority.',
|
||||
'Semuta discard reactions, mobile acceptance, the unresolved bid and self-transfer cases, and full combined Richese games remain unfinished. Completing this transfer does not enable all remaining Richese cards or expansion starts.',
|
||||
'Semuta responses to a fresh Distrans discard, mobile acceptance, the unresolved bid and self-transfer cases, and full combined Richese games remain unfinished. The separate clean public ordinary-card Semuta preview does not enable this transfer composition, all remaining Richese cards or expansion starts.',
|
||||
],
|
||||
related: [
|
||||
'card-richese-distrans',
|
||||
@@ -704,7 +704,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
area: 'Implementation',
|
||||
status: 'Partial',
|
||||
detail:
|
||||
'Synchronous two-card custody, private transfer, normal Distrans disposal, both-card reservations and prospective transaction/promise checks are integrated. Open-lot timing and self-transfer remain explicit guards; future Semuta reactions are unfinished.',
|
||||
'Synchronous two-card custody, private transfer, normal Distrans disposal, both-card reservations and prospective transaction/promise checks are integrated. Open-lot timing and self-transfer remain explicit guards; Semuta reaction to Distrans disposal is not yet integrated.',
|
||||
},
|
||||
{
|
||||
area: 'Player controls',
|
||||
@@ -941,7 +941,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'An active Truthtrance question and exact card or transaction commitments must resolve as required. A full hand is currently guarded: the printed sequence adds the recovered card before discarding the Box, and the temporary capacity question is unresolved. The current implementation requires a pre-existing free hand slot. This is not a verified prohibition on every net-zero exchange.',
|
||||
'A pending Guild refund claim on a discarded shipping Karama also blocks starting the search while that provisional refund policy is unresolved. The guard protects the competing claim to that card; it is not a general ban on Box during Shipment. Empty or only-Box searches are rejected before payment under the current unsupported-empty-search policy; there is no fallback deck draw.',
|
||||
'All four AI profiles start only when their own projected availability and spice reserve allow it. They do not inspect unpaid discard contents. Once paid, they choose from their entitled cards, prefer existing functional effects over unfinished Richese faces and complete a legal selection even if only unfinished effects remain. A paid search takes priority over optional actions.',
|
||||
'This bounded search, its controls and recovery are integrated in development fixtures. Full-hand activation, the provisional Guild refund interaction and future Semuta discard reactions remain unresolved or unfinished. Mirror Weapon has a separate bounded CHOAM/Richese-deck classic-faction battle path; Semuta Drug remains inactive, and full Richese starts remain disabled. Complete browser, mobile and full-game acceptance is not claimed here.',
|
||||
'This bounded search, its controls and recovery are integrated in development fixtures. Full-hand activation, the provisional Guild refund interaction and Semuta response to the used Box remain unresolved or unfinished. Mirror Weapon has a separate bounded CHOAM/Richese-deck classic-faction battle path; Semuta Drug has a separate clean public ordinary-card preview, and full Richese starts remain disabled. Complete browser, mobile and full-game acceptance is not claimed here.',
|
||||
],
|
||||
related: [
|
||||
'card-richese-nullentropy-box',
|
||||
@@ -1154,6 +1154,8 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
? 'Choose Mirror Weapon in the battle weapon slot in a supported CHOAM/Richese-deck table. Its attack copies the opposing revealed weapon; copied Tooth or Stone choices resolve first and independently. Under the user-selected interpretation, a winning physical Mirror may be retained even after a copied Tooth or Artillery attack. Full Richese starts and combined modules remain guarded.'
|
||||
: definition.card.effect === 'portableSnooper'
|
||||
? 'Choose Portable Snooper as your ordinary poison defense, or use the late-defense panel after reveal before your own traitor decision. It uses ordinary winner retention and cannot stop Poison Tooth.'
|
||||
: definition.card.effect === 'semutaDrug'
|
||||
? 'In an explicitly opted-in Richese development game, every seat sees a neutral response after a clean public ordinary-card discard. Continue or commit your held Semuta with a free hand slot; the sole eligible card transfers automatically. Other discard producers, full-hand sequencing and normal Richese starts remain unfinished.'
|
||||
: 'Reference component only: this Richese card’s game actions are not enabled.',
|
||||
],
|
||||
...(definition.card.effect === 'semutaDrug'
|
||||
@@ -1163,32 +1165,36 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
area: 'Implementation' as const,
|
||||
status: 'Partial' as const,
|
||||
detail:
|
||||
'Fresh-card recovery has isolated transfer rules. Ixian and CHOAM exchanges, battle cleanup, Sabotage, Robbery, the used Nullentropy Box, retired Ornithopter, answered Truthtrance and completed ordinary card effects preserve unfinished steps for recovery. Random victims, completed draws and the paid Box shuffle are not repeated. Semuta activation and the remaining discard timings are unfinished.',
|
||||
'An explicitly opted-in Richese profile now interrupts clean public ordinary-card discards with a neutral all-seat offer. Committing a held Semuta atomically claims the sole eligible other-player card, discards Semuta and resumes the saved ordinary-card suffix once. Other typed discard producers and full-hand exchange remain unfinished.',
|
||||
},
|
||||
{
|
||||
area: 'Player controls' as const,
|
||||
status: 'Partial' as const,
|
||||
detail:
|
||||
'The complete card can be inspected. Playing Semuta, declining its opportunity and choosing a fresh discard are not enabled.',
|
||||
'All seats can Continue at the neutral fresh-discard boundary; only the actual holder sees Commit. A committed multi-card selection control is available when that later producer family is integrated. Normal Richese starts remain gated.',
|
||||
},
|
||||
{
|
||||
area: 'AI' as const,
|
||||
status: 'Partial' as const,
|
||||
detail:
|
||||
'AI resumes saved exchanges, battle cleanup, forced Terror discards, completed Box searches, retired Ornithopter movement, answered Truthtrance queues and completed ordinary card effects automatically. No difficulty can yet play Semuta or choose its claimed card.',
|
||||
'All four AI profiles use their own projected hand to commit or pass the exact current event, and can choose only privately projected committed candidates. This is minimal legal participation, not tuned strategy.',
|
||||
},
|
||||
{
|
||||
area: 'Documentation' as const,
|
||||
status: 'Partial' as const,
|
||||
detail:
|
||||
'The guide explains one fresh other-player discard and selection from simultaneous discards. Reaction timing and full-hand use remain unresolved.',
|
||||
'The guide distinguishes the bounded public ordinary-card prototype, privacy-neutral acknowledgements and conservative free-slot policy from unresolved full-hand sequencing and remaining discard sources.',
|
||||
},
|
||||
{
|
||||
area: 'Verification' as const,
|
||||
status: 'Partial' as const,
|
||||
detail:
|
||||
'Isolated transfer, private exchange, battle, Box, Ornithopter, Truthtrance and ordinary-card recovery, and concurrent settlement checks pass. These do not verify an actual Semuta opportunity or a complete Richese game.',
|
||||
'Genuine setup, physical card custody, neutral hidden-hand parity, saved JSON, authenticated SQLite CAS and all four legal AI profiles cover the bounded ordinary-card reaction. Other discard producers, full games and live deployment remain unverified.',
|
||||
evidence: [
|
||||
'tests/semuta-engine.test.ts',
|
||||
'tests/semuta-recovery.test.ts',
|
||||
'tests/bot-semuta.test.ts',
|
||||
'tests/semuta-reaction-controls.test.tsx',
|
||||
'tests/semuta-drug.test.ts',
|
||||
'tests/treachery-discard-continuations.test.ts',
|
||||
'tests/multiplayer-discard-continuations.test.ts',
|
||||
|
||||
Reference in new issue
Block a user