Allow Richese reveal during selected responses
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 07:58:48 +11:00
1 parent a1b2aacbac
commit 82d857b667
9 files changed
+518 -4

No files matched your search

+3 -1
View File
@@ -1144,7 +1144,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Basic and Advanced ordinary mixed battles have a provisional private pool: physical forces already here plus the token value capped by current reserves. The Battle Wheel labels that interpretation explicitly. Both plans seal before materialization, then native support, casualties and aftermath use actual forces. This first version does not adjudicate the FAQ mixed dial cap.',
'In supported card profiles, provisional materialization-first order forecasts the own marker privately without moving forces early. Both plans seal, the original token reveals and materializes its reserve-limited ordinary counters at its sector, then Harass selects/returns actual undialed counters. Reinforcements must leave three original reserves after that reveal and before any future Harass return; an unfunded card cannot seal a plan. Marker0, last-used identity and original reveal-event history remain native.',
'Concealed collection, occupancy and control use one-force presence. Exposed storm sectors and unprotected worm destruction reveal and remove the resulting actual forces. Storm shelter and other sectors remain separate. Trip to Gamont at a location with a marker reveals it, even if ordinary forces also occupy that sector, then returns one actual force if present. It is still used if zero forces remain. Canceling the CHOAM power leaves the marker concealed.',
'All four AI profiles choose from their own private token inventory, pay only the one-force shipment price, move the marker explicitly and reveal positive tokens when useful. They retain zero-token presence and use only public information to assess opponents. They also offer safe positive tokens from their own inventory during the allied opportunity, fund them with their own spice and accept or decline private offers using their own force and payment information. A human Richese player’s offer window holds an AI ally’s shipment. Unimplemented interruptions, including pending responses and Truthtrance, retain priority.',
'All four AI profiles choose from their own private token inventory, pay only the one-force shipment price, move the marker explicitly and reveal positive tokens when useful. They retain zero-token presence and use only public information to assess opponents. They also offer safe positive tokens from their own inventory during the allied opportunity, fund them with their own spice and accept or decline private offers using their own force and payment information. A human Richese player’s offer window holds an AI ally’s shipment. Supported Basic Charity, auction and Fremen grant responses can retain their original choices while the owner reveals; other unimplemented interruptions and Truthtrance retain priority.',
'Marker-only and ordinary-plus-marker No-Fields compose with the selected Advanced Occupy profile under the same provisional additive pool. Either selected lead gets its actual variable-owner quote; original fixed Ecaz strength stays separate. Prevention restores only the chosen lead’s own pool. Basic mixed co-side, Homeworld custody, wider Truthtrance and other profile/module boundaries remain. Complete Richese starts and final acceptance stay gated.',
],
related: [
@@ -1206,6 +1206,8 @@ export const RULE_TOPICS: RuleTopic[] = [
'tests/richese-reveal-interruption-runtime.test.ts',
'tests/richese-reveal-interruption-controls.test.tsx',
'tests/richese-reveal-interruption-privacy.test.ts',
'tests/richese-reveal-response-runtime.test.ts',
'tests/richese-reveal-fremen-grant-runtime.test.ts',
'tests/richese-no-field-engine.test.ts',
'tests/richese-no-field-review.test.ts',
'tests/richese-no-field-bots.test.ts',
+42 -1
View File
@@ -1,4 +1,5 @@
import type { Game } from './engine';
import { validateFremenRevivalGrant } from './fremen-revival-grant';
const factions = new Set([
'atreides', 'beneGesserit', 'emperor', 'fremen', 'guild', 'harkonnen',
@@ -18,7 +19,7 @@ export function richeseRevealInterruptSupported(g: Game): boolean {
g.players.some(player => (player.faction === 'ixians' || player.faction === 'tleilaxu') && !g.expansions.includes('ix')) ||
g.coalitionState || g.coalitionPreview || g.techTokens || g.strongholdCards || g.leaderSkills ||
g.homeworlds || g.nexusCards || g.discoveryEnabled || g.discoveries || g.discoveryStash ||
g.greatMaker || g.sandtrout || g.ecazTreachery || g.truthtrance || g.response ||
g.greatMaker || g.sandtrout || g.ecazTreachery || g.truthtrance ||
g.biddingEnd || g.battle || g.stormResolution || g.spiceResolution || g.spiceSequence ||
g.victoryContinuation || g.currentAuctionSale || g.richeseAllyOffer ||
g.bureaucratPayments?.pending) return false;
@@ -53,6 +54,46 @@ export function richeseRevealInterruptSupported(g: Game): boolean {
g.mixedAllianceQuestions, g.mixedMovementQuestions, g.mixedTraitorQuestions, g.mixedBattleQuestions];
if (mixed.some(rows => rows?.some(row => row.turn === g.turn && row.binding))) return false;
if (g.response) {
// Only independent, un-nested native responses with a stable original
// source are admitted. In particular, shipment and revival transactions
// still need their own continuation proofs.
const response = g.response;
if (g.decision || g.phaseOpening || g.choamMarket ||
!Array.isArray(response.passed) || new Set(response.passed).size !== response.passed.length ||
response.passed.some(id => !g.players.some(player => player.id === id))) return false;
const owner = g.players.find(player => player.id === response.owner);
if (!owner) return false;
const keys = Object.keys(response).sort().join(',');
if (response.kind === 'choamCharity')
return keys === 'kind,owner,passed' && g.phase === 2 && owner.faction === 'choam' &&
g.choamCharity?.turn !== g.turn;
if (response.kind === 'atreidesAuction') {
if (keys !== 'kind,owner,passed' || g.phase !== 3 || owner.faction !== 'atreides') return false;
return !g.richeseAuction && !!g.auction && Array.isArray(g.auction.cards) && Number.isSafeInteger(g.auction.index) && g.auction.index >= 0 &&
!!g.auction.cards[g.auction.index];
}
if (response.kind === 'richeseAuction') {
const round = g.richeseBidding;
return keys === 'kind,owner,passed' && g.phase === 3 && owner.faction === 'richese' &&
!!round && round.turn === g.turn && round.owner === response.owner && round.stage === 'declaration' &&
(round.position === 'first' || round.position === 'last') &&
Number.isSafeInteger(round.normalCount) && round.normalCount! >= 0 && round.normalCount! <= g.players.length &&
!round.cacheCanceled && !g.auction && !g.richeseAuction && !g.ixAuction && Array.isArray(g.richeseCache) &&
(round.normalCountBeforeCache === undefined || Number.isSafeInteger(round.normalCountBeforeCache) &&
round.normalCountBeforeCache >= round.normalCount! && round.normalCountBeforeCache <= g.players.length &&
round.normalCountBeforeCache - round.normalCount! <= 1);
}
if (response.kind === 'fremenRevivalGrant') {
if (keys !== 'fremenGrant,kind,owner,passed,recipient' || g.phase !== 4 ||
owner.faction !== 'fremen' || !response.fremenGrant ||
response.fremenGrant.owner !== response.owner || response.fremenGrant.recipient !== response.recipient)
return false;
try { validateFremenRevivalGrant(g, response.fremenGrant); return true; }
catch { return false; }
}
return false;
}
if (g.phaseOpening)
return g.expansions.includes('ix') && !!g.amalWindowOpen && !g.decision && !g.choamMarket;
const decision = g.decision;