Offer Semuta on clean mandatory battle batch
This commit is contained in:
1 parent
e55d275014
commit
dd6d3a89ca
10 files changed
+308
-71
No files matched your search
@@ -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 completed ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement-order Sapho and winner battle-card discards are connected; other producers and full-hand sequencing remain unfinished.';
|
||||
return 'Use the neutral fresh-discard response in the Semuta development preview. Clean completed ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement-order Sapho and battle-card batches are connected; pending rewards, other producers and full-hand sequencing remain unfinished.';
|
||||
return definition && definition.card.effect !== 'karama'
|
||||
? 'This Richese card’s effect is not implemented yet. You can inspect its rules.'
|
||||
: null;
|
||||
|
||||
+41
-2
@@ -1783,6 +1783,10 @@ export type Game = {
|
||||
result: 'normal' | 'traitor' | 'mutualTraitors' | 'explosion' | 'legacy';
|
||||
cardRoles?: Record<string, Record<string, Omit<EcazPoisonDiscard, 'card'>>>;
|
||||
cardRolesSignature?: string;
|
||||
mandatoryDiscard?: {
|
||||
entries: { card: string; owner: string }[];
|
||||
signature: string;
|
||||
};
|
||||
winnerDiscards?: { cards: string[]; completed: boolean; signature: string };
|
||||
sukRescue?: { signature: string; completed: boolean };
|
||||
ixSubstitution?: { signature: string; completed: boolean };
|
||||
@@ -4542,6 +4546,13 @@ function battleCardRolesSignature(context: NonNullable<Game['lastBattleContext']
|
||||
const { event, turn, territory, combatants, winner, result, cardRoles } = context;
|
||||
return JSON.stringify({ event, turn, territory, combatants, winner, result, cardRoles });
|
||||
}
|
||||
function battleMandatoryDiscardSignature(context: NonNullable<Game['lastBattleContext']>) {
|
||||
return JSON.stringify({
|
||||
event: context.event, turn: context.turn, territory: context.territory,
|
||||
combatants: context.combatants, winner: context.winner, result: context.result,
|
||||
entries: context.mandatoryDiscard?.entries,
|
||||
});
|
||||
}
|
||||
function battleCardRolesIntegrity(g: Game) {
|
||||
const context = g.lastBattleContext;
|
||||
if (!context?.cardRoles && !context?.cardRolesSignature) return;
|
||||
@@ -5547,6 +5558,20 @@ function treacheryDiscardIntegrity(g: Game) {
|
||||
),
|
||||
'The saved battle discard does not match its resolved combatants.',
|
||||
);
|
||||
if (c.kind === 'battleResolved' && context.mandatoryDiscard) {
|
||||
const receipt = context.mandatoryDiscard;
|
||||
requireRule(
|
||||
Array.isArray(receipt.entries) &&
|
||||
receipt.entries.length === batch.entries.length &&
|
||||
receipt.entries.every(entry =>
|
||||
typeof entry.card === 'string' && typeof entry.owner === 'string') &&
|
||||
receipt.signature === battleMandatoryDiscardSignature(context) &&
|
||||
JSON.stringify(receipt.entries) === JSON.stringify(batch.entries.map(entry => ({
|
||||
card: entry.card.id, owner: entry.discardedBy,
|
||||
}))),
|
||||
'The fresh mandatory battle cards no longer match their former owners.',
|
||||
);
|
||||
}
|
||||
// Projection must validate the private Auditor owner as strictly as actions.
|
||||
auditorIntegrity(g);
|
||||
const loser = c.combatants.find((id) => id !== context.winner);
|
||||
@@ -5755,6 +5780,7 @@ function semutaOfferSupported(
|
||||
continuation.kind !== 'saphoMovementDiscard' &&
|
||||
!(continuation.kind === 'battleCleanup' && continuation.source === 'winner') &&
|
||||
continuation.kind !== 'winnerMandatoryDiscard' &&
|
||||
continuation.kind !== 'battleResolved' &&
|
||||
!(continuation.kind === 'ornithopterDiscard' &&
|
||||
(continuation.source === 'end' ||
|
||||
(continuation.source === 'move' &&
|
||||
@@ -5764,8 +5790,11 @@ function semutaOfferSupported(
|
||||
!(continuation.kind === 'truthtranceDiscard' &&
|
||||
continuation.consumed.source === undefined &&
|
||||
continuation.remaining === null && continuation.promise === null)) return false;
|
||||
if (continuation.kind === 'battleResolved' &&
|
||||
!g.lastBattleContext?.mandatoryDiscard) return false;
|
||||
if ((continuation.kind === 'battleCleanup' ||
|
||||
continuation.kind === 'winnerMandatoryDiscard') &&
|
||||
continuation.kind === 'winnerMandatoryDiscard' ||
|
||||
continuation.kind === 'battleResolved') &&
|
||||
(g.pendingAuditor || g.pendingCapture || g.pendingTech ||
|
||||
g.pendingFaceDance || g.pendingChoamBattleIncome ||
|
||||
g.pendingWinnerDiscards || g.moritaniRetention ||
|
||||
@@ -5780,11 +5809,13 @@ function semutaOfferSupported(
|
||||
g.players.some(p => p.faction === 'richese') &&
|
||||
entries.length > 0 &&
|
||||
(continuation.kind === 'battleCleanup' ||
|
||||
continuation.kind === 'winnerMandatoryDiscard'
|
||||
continuation.kind === 'winnerMandatoryDiscard' ||
|
||||
continuation.kind === 'battleResolved'
|
||||
? entries.every(entry => entry.publicFace)
|
||||
: entries.length === 1 && entries[0].publicFace) &&
|
||||
(continuation.kind === 'battleCleanup' ||
|
||||
continuation.kind === 'winnerMandatoryDiscard' ||
|
||||
continuation.kind === 'battleResolved' ||
|
||||
(!continuation.resume.response && !continuation.resume.decision &&
|
||||
!continuation.resume.pendingKarama && !continuation.resume.phaseOpening)) &&
|
||||
!g.karamaShipping && !g.auction && !g.richeseAuction && !g.battle &&
|
||||
@@ -18132,6 +18163,14 @@ function resolveBattle(g: Game, retreat?: DiplomatRetreatSelection) {
|
||||
...(casualtyCommitment ? { commitment: { forces: {...casualtyCommitment.forces}, dial: casualtyCommitment.dial, support: casualtyCommitment.support } } : {}),
|
||||
};
|
||||
}
|
||||
if (discarded.length) {
|
||||
g.lastBattleContext.mandatoryDiscard = {
|
||||
entries: discarded.map(entry => ({ card: entry.card.id, owner: entry.discardedBy })),
|
||||
signature: '',
|
||||
};
|
||||
g.lastBattleContext.mandatoryDiscard.signature =
|
||||
battleMandatoryDiscardSignature(g.lastBattleContext);
|
||||
}
|
||||
g.battle = null;
|
||||
const cards = [...quote.winnerCards];
|
||||
const continuation: Extract<
|
||||
|
||||
+5
-5
@@ -1156,7 +1156,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
: 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 clean public ordinary-card, paid Box, retired Ornithopter, private Distrans, final Truthtrance, movement-order Sapho or winner battle-card discards without pending rewards. Mandatory winning cards and later optional winner discards are separate fresh batches. Continue or commit held Semuta with a free hand slot; a sole card transfers automatically. For several cards, commit first, then privately select exactly one. The original effect or battle suffix resumes once. BG/Ecaz/Moritani arrivals, loser and mixed-owner mandatory battle batches, competing transactions, full-hand sequencing and normal Richese starts remain unfinished.'
|
||||
? 'In an explicitly opted-in Richese development game, every seat sees a neutral response after clean public ordinary-card, paid Box, retired Ornithopter, private Distrans, final Truthtrance, movement-order Sapho or battle-card discards without pending rewards. The battle’s first mandatory batch can include cards from both combatants; a later mandatory winning-card cleanup and optional winner selection are separate fresh batches. Continue or commit held Semuta with a free hand slot; only another player’s card is eligible. A sole card transfers automatically; for several, commit first and privately select exactly one. Casualties and awards never replay. Mixed arrivals, reward-pending battle effects, competing transactions, 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'
|
||||
@@ -1166,13 +1166,13 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
area: 'Implementation' as const,
|
||||
status: 'Partial' as const,
|
||||
detail:
|
||||
'An explicitly opted-in Richese profile interrupts clean public ordinary-card, paid Box, retired Ornithopter, private Distrans, final Truthtrance, movement-order Sapho, mandatory winner and optional winner battle-card discards with a neutral all-seat offer. Mandatory and later optional winner cards are separate batches; committed multi-card candidates belong only to the holder. Semuta transfers one other-player card, is discarded once and resumes the original effect once. Mixed arrivals, loser battle batches, pending rewards, other typed producers and full-hand exchange remain unfinished.',
|
||||
'The explicit Richese preview interrupts clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement-order Sapho and three separate battle discard stages: initial mandatory combatants, mandatory winner and optional winner. A neutral all-seat offer precedes each eligible batch. Only a committed holder sees multi-card candidates from other players; Semuta transfers one physical card, is discarded once and resumes the saved battle/casualty suffix once. Mixed arrivals, pending battle rewards, other producers and full-hand exchange remain unfinished.',
|
||||
},
|
||||
{
|
||||
area: 'Player controls' as const,
|
||||
status: 'Partial' as const,
|
||||
detail:
|
||||
'All seats can Continue at the neutral fresh-discard boundary; only the actual holder sees Commit. A committed winner-battle batch gives that holder the card inspector and exact one-card selector; nonholders receive no candidates. Normal Richese starts remain gated.',
|
||||
'All seats can Continue at the neutral fresh-discard boundary; only the actual holder sees Commit. In a mixed-owner battle batch, a committed holder can inspect and select only cards discarded by another player; nonholders receive no candidates. Normal Richese starts remain gated.',
|
||||
},
|
||||
{
|
||||
area: 'AI' as const,
|
||||
@@ -1184,13 +1184,13 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
area: 'Documentation' as const,
|
||||
status: 'Partial' as const,
|
||||
detail:
|
||||
'The guide distinguishes bounded public ordinary-card, paid Box, clean Ornithopter, Distrans, final Truthtrance, movement-order Sapho and mandatory/optional winner battle batches from privacy-neutral acknowledgements, provisional free-slot policy and remaining discard sources.',
|
||||
'The guide distinguishes bounded clean public ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement-order Sapho and initial/mandatory-winner/optional-winner battle batches from privacy-neutral acknowledgements, provisional free-slot policy and remaining discard sources.',
|
||||
},
|
||||
{
|
||||
area: 'Verification' as const,
|
||||
status: 'Partial' as const,
|
||||
detail:
|
||||
'Genuine setup, physical custody, hidden-hand parity, saved JSON, authenticated SQLite CAS and legal AI participation cover clean ordinary-card, paid Box, Ornithopter, Distrans, final Truthtrance, movement-order Sapho and consecutive mandatory/optional winner battle reactions. Loser and mixed-owner batches, full games and live deployment remain unverified.',
|
||||
'Genuine setup, physical custody, hidden-hand parity, saved JSON, authenticated SQLite CAS and legal AI participation cover clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement-order Sapho and separate mandatory/optional battle reactions, including a two-owner simultaneous batch. Pending battle rewards, full games and live deployment remain unverified.',
|
||||
evidence: [
|
||||
'tests/semuta-engine.test.ts',
|
||||
'tests/semuta-recovery.test.ts',
|
||||
|
||||
Reference in new issue
Block a user