Add whole-territory Truthtrance force questions
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 03:37:52 +11:00
1 parent 7552bb5394
commit f1007a4ea8
9 files changed
+149 -21

No files matched your search

+1 -1
View File
@@ -195,7 +195,7 @@ export const FACTION_RULES: Record<
'After losing a qualifying battle with no traitor called and the opposing leader surviving, you may reveal a held Traitor Card naming a different leader from that opposing faction. A living target dies and pays its printed strength from the bank; an already-dead target pays nothing.',
'During Mentat Pause, set the revealed card aside face up as that faction’s use marker and draw its replacement. Use this advantage once against each faction; Karama cannot prevent it.',
'Once per game after a battle loss with a winner, special Karama can force kept/discarded subsets of that opponent’s public played cards. Owned selectors, native cost and cleanup are connected; mandatory named-card disposal has explicitly provisional precedence.',
'Advanced assassination is available in an opted-in development preview. The duration of forfeiture after calling a normal traitor and exceptional leader-custody interactions remain unresolved.',
'Advanced assassination is available in an opted-in development preview. Revealing your Traitor Card normally forfeits this advantage for the rest of the game. Exceptional leader-custody interactions remain unfinished.',
],
},
};
+4 -4
View File
@@ -3027,9 +3027,9 @@ export const RULE_TOPICS: RuleTopic[] = [
category: 'Cards',
coverage: 'Partial',
developmentStage: 'Prototyped',
summary: 'Ask about current physical counters in reserves, the Tanks or an exact board location.',
summary: 'Ask about current physical counters in reserves, the Tanks, an entire territory or an exact board location.',
steps: [
'Choose Current physical forces, then the pool, counter type and an Exactly, At least or At most comparison. A board question names both territory and sector. Available HMS interiors and revealed Discovery locations are separate from their outside territories.',
'Choose Current physical forces, then the pool, counter type and an Exactly, At least or At most comparison. Ask about an entire territory or name an exact sector. Available HMS interiors and revealed Discovery locations are separate from their outside territories.',
'Every physical counter counts once, including advisors. The total includes elites, and the normal count excludes them. Combat bonuses, concealed No-Field markers and their hidden force values do not enter the count.',
'Use the separate Current concealed No-Field printed value question for a token denomination. Concealed5 can coexist with exactly zero physical counters at its sector and zero reserves; this still is not five materialized forces. Public Yes/No does not itself reveal or materialize the marker.',
'Reserves includes the native Homeworld reserve breakdown. These controls do not query individual Homeworld populations. A Basic faction whose starred counters are not tracked separately supports only the aggregate total.',
@@ -3037,8 +3037,8 @@ export const RULE_TOPICS: RuleTopic[] = [
],
related: ['card-truthtrance', 'force-counters', 'truthtrance-spice', 'richese-no-field', 'privacy'],
checklist: [
{ area: 'Implementation', status: 'Implemented', detail: 'Physical pool comparisons, exact live locations, typed-counter availability and saved fact revalidation are connected.', evidence: ['game/truthtrance-force-count.ts', 'game/truthtrance.ts'] },
{ area: 'Player controls', status: 'Implemented', detail: 'Pool, territory/sector, counter type and numeric comparison share validation with the server.', evidence: ['components/truthtrance-force-count.tsx', 'components/truthtrance.tsx'] },
{ area: 'Implementation', status: 'Implemented', detail: 'Physical pool comparisons, entire territories and exact live locations, typed-counter availability and saved fact revalidation are connected.', evidence: ['game/truthtrance-force-count.ts', 'game/truthtrance.ts'] },
{ area: 'Player controls', status: 'Implemented', detail: 'Pool, whole territory or exact sector, counter type and numeric comparison share validation with the server.', evidence: ['components/truthtrance-force-count.tsx', 'components/truthtrance.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'All profiles answer the shared private projection legally. Broader questioning strategy and tuning remain deferred.', evidence: ['game/bots.ts', 'tests/truthtrance-force-engine.test.ts'] },
{ area: 'Documentation', status: 'Implemented', detail: 'Source composition, present custody, marker exclusions and remaining question forms are explicit.', evidence: ['docs/TRUTHTRANCE_FORCE_FACTS.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Genuine setup, typed pools, private compounds, live board boundaries, later legal actions, malformed saves, controls and authenticated concurrent answers have focused checks.', evidence: ['tests/truthtrance-force-count.test.ts', 'tests/truthtrance-force-engine.test.ts', 'tests/truthtrance-force-controls.test.tsx', 'tests/truthtrance-force-recovery.test.ts'] },
+30 -8
View File
@@ -6,6 +6,7 @@ export type ForceCountFact = {
zone:
| { kind: 'reserves' }
| { kind: 'tanks' }
| { kind: 'territory'; territory: string }
| { kind: 'location'; territory: string; sector: number };
counter: 'total' | 'normal' | 'elite';
compare: 'eq' | 'gte' | 'lte';
@@ -83,6 +84,14 @@ export function parseForceCountFact(
if (!exactKeys(zone, ['kind']))
throw new Error('Reserve and Tanks facts cannot name a board location.');
parsedZone = { kind: zone.kind };
} else if (zone.kind === 'territory') {
if (
!exactKeys(zone, ['kind', 'territory']) ||
typeof zone.territory !== 'string' ||
!validLocations.some(candidate => candidate.territory === zone.territory)
)
throw new Error('Choose a current board territory.');
parsedZone = { kind: 'territory', territory: zone.territory };
} else if (zone.kind === 'location') {
if (
!exactKeys(zone, ['kind', 'territory', 'sector']) ||
@@ -101,7 +110,7 @@ export function parseForceCountFact(
sector: zone.sector as number,
};
} else {
throw new Error('Choose reserves, the Tanks, or a current board location.');
throw new Error('Choose reserves, the Tanks, a current territory, or a board location.');
}
return {
kind: 'forceCount',
@@ -140,13 +149,24 @@ function chosenPool(player: ForceCountPlayer, fact: ForceCountFact) {
} else {
if (!record(player.forces))
throw new Error('The selected board force pool is malformed.');
const key = location(fact.zone.territory, fact.zone.sector);
totalValue = player.forces[key] ?? 0;
if (eliteState !== undefined) {
if (!record(eliteState.forces))
throw new Error('The selected elite board force pool is malformed.');
eliteValue = eliteState.forces[key] ?? 0;
if (eliteState !== undefined && !record(eliteState.forces))
throw new Error('The selected elite board force pool is malformed.');
const sectors = fact.zone.kind === 'territory'
? territory(fact.zone.territory).sectors
: [fact.zone.sector];
let total = 0, elite = 0;
for (const sector of sectors) {
const key = location(fact.zone.territory, sector);
const sectorTotal = count(player.forces[key] === undefined ? 0 : player.forces[key], 'The selected physical force count');
const storedElite = eliteState?.forces[key];
const sectorElite = count(storedElite === undefined ? 0 : storedElite, 'The selected elite force count');
if (sectorElite > sectorTotal)
throw new Error('The selected elite force count exceeds its physical force pool.');
total = count(total + sectorTotal, 'The selected physical force count');
elite = count(elite + sectorElite, 'The selected elite force count');
}
totalValue = total;
eliteValue = elite;
}
const total = count(totalValue, 'The selected physical force count');
const elite = count(eliteValue, 'The selected elite force count');
@@ -197,6 +217,8 @@ export function forceCountFactText(fact: ForceCountFact): string {
? 'in your reserves'
: fact.zone.kind === 'tanks'
? 'in the Tleilaxu Tanks'
: `in ${territory(fact.zone.territory).name}, sector ${fact.zone.sector}`;
: fact.zone.kind === 'territory'
? `in ${territory(fact.zone.territory).name} across all sectors`
: `in ${territory(fact.zone.territory).name}, sector ${fact.zone.sector}`;
return `you currently have ${comparison} ${fact.value} ${counters} ${zone}`;
}