Add public force counter inspectors and all-faction gallery
Each player summary opens enlarged original counter diagrams and public physical pools. The inventory separates reserves, Tanks, board locations and foreign Homeworld deployments. Native Homeworlds break down existing reserves; elites and advisor stances remain subsets. The reference gallery covers all twelve factions, including Suboids/Cyborgs, Sardaukar, Fedaykin and BG advisor faces. Actual force keys retain unplaced Mobile Stronghold and Discovery groups. Basic Homeworld special identity comes from public typed pools, not mode alone. Concealed No-Fields expose only a separate marker/location; no value or future materialization is inferred. Temporary battle strength creates no physical counter. Invalid projections show a refresh notice instead of invented totals. Inspection updates from current props and submits no action or saved mutation. Independent review found no material issue and clarified No-Field wording: revelation draws from reserves available then, without earmarking counters. Eleven new focused cases cover faction identity, typed pools, advisors, genuine Basic Homeworld public views, native/foreign splits, genuine Ix setup before HMS placement, a controlled Discovery position, JSON continuation, input immutability, concealed0/3/5 equivalence, named controls and malformed projection feedback. A test-only Homeworld projection initially omitted its card identity; corrected before final checks. Lint's semantic status element was corrected to output. Types, lint, all 4984 offline tests and production build passed on stable source. No HTTP/auth/schema/persistence behavior changed, so no additional HTTP suite was required. Browser QA reused an isolated existing three-human Basic Nexus without game actions. All three public inspectors showed correct pools; Escape restored trigger focus, Enter opened inspection and PageDown scrolled the labelled region. At390px the dialog359px fit, body text16px and Close44px high. All twelve gallery choices rendered named160px diagrams without gallery overflow. Internal guide, Back and refresh worked; viewport reset and preview retained. Screenshot capture timed out. Full visual/screen-reader/200% zoom, manufactured component fidelity and all-configuration browser acceptance remain open. All 1499 original rooms, 3171 seats and auxiliary rows are unchanged; SQLite integrity and foreign keys pass. The healthy server was reused without restart, migration, reset or automation. AI strategy/calibration remains deferred until non-AI features are complete; no mode/publication gate was opened. Private evidence: 20260921-force-inspection. Verified source tree: 5d7ffd471229faa13224fd5a87d33c3ac5af866446241a7881926a54c1ee49eb
This commit is contained in:
1 parent
2b071a44a2
commit
537da4a12c
13 files changed
+536
-13
No files matched your search
@@ -0,0 +1,94 @@
|
||||
import { splitLocation, territory } from './board';
|
||||
import type { FactionId } from './catalog';
|
||||
import { specialForceName } from './combat';
|
||||
import { isAdvisor } from './advisors';
|
||||
import { HOMEWORLD_CARDS } from './homeworld-cards';
|
||||
|
||||
/** Public physical custody only. No private cards, commitments or token faces. */
|
||||
export type PublicForcePlayer = {
|
||||
id: string;
|
||||
name: string;
|
||||
faction: FactionId;
|
||||
reserves: number;
|
||||
tanks: number;
|
||||
forces: Readonly<Record<string, number>>;
|
||||
elites?: { reserves: number; tanks: number; forces: Readonly<Record<string, number>> };
|
||||
advisors?: Record<string, { lockedTurn?: number }>;
|
||||
noField?: { deployed: { location: { territory: string; sector: number } } | null } | null;
|
||||
};
|
||||
export type PublicForceWorld = {
|
||||
id: string;
|
||||
native: string;
|
||||
card: string;
|
||||
forces: Readonly<Record<string, { normal: number; elite: number }>>;
|
||||
};
|
||||
export type ForcePool = {
|
||||
id: string;
|
||||
name: string;
|
||||
total: number;
|
||||
normal: number;
|
||||
elite: number;
|
||||
advisors: boolean;
|
||||
};
|
||||
|
||||
export function forceCounterName(faction: FactionId, kind: 'force' | 'special' | 'advisor') {
|
||||
if (kind === 'advisor') return 'Advisor';
|
||||
if (kind === 'special') return specialForceName(faction);
|
||||
return faction === 'ixians' ? 'Suboid' : faction === 'beneGesserit' ? 'Fighter' : 'Force';
|
||||
}
|
||||
|
||||
function locationName(id: string, sector: number) {
|
||||
if (!Number.isSafeInteger(sector) || sector < 0) throw new Error('Invalid force location.');
|
||||
const name = territory(id).name;
|
||||
return sector > 0 ? `${name} · sector ${sector}` : name;
|
||||
}
|
||||
|
||||
function pool(id: string, name: string, total: number, elite = 0, advisors = false): ForcePool {
|
||||
if (![total, elite].every(n => Number.isSafeInteger(n) && n >= 0) || elite > total)
|
||||
throw new Error('Invalid public force count.');
|
||||
return { id, name, total, normal: total - elite, elite, advisors };
|
||||
}
|
||||
|
||||
/** Native Homeworlds subdivide reserves; only foreign deployments add a pool.
|
||||
* Concealed No-Fields are markers, never physical counters or inferred values.
|
||||
* A malformed projection produces no fabricated totals and cannot crash a table. */
|
||||
export function forceInventory(player: PublicForcePlayer, worlds: readonly PublicForceWorld[] | null = null) {
|
||||
try {
|
||||
const reserves = pool('reserves', 'Reserves', player.reserves, player.elites?.reserves);
|
||||
const tanks = pool('tanks', 'Tleilaxu Tanks', player.tanks, player.elites?.tanks);
|
||||
const rows = [reserves, tanks];
|
||||
for (const key of [...new Set([...Object.keys(player.forces), ...Object.keys(player.elites?.forces ?? {})])].sort()) {
|
||||
const { territory: id, sector } = splitLocation(key);
|
||||
const row = pool(key, locationName(id, sector), player.forces[key] ?? 0,
|
||||
player.elites?.forces[key], isAdvisor(player, id));
|
||||
if (row.total > 0) rows.push(row);
|
||||
}
|
||||
const nativeReserves: ForcePool[] = [];
|
||||
for (const world of worlds ?? []) {
|
||||
const force = world.forces[player.id];
|
||||
if (!force) continue;
|
||||
const card = HOMEWORLD_CARDS.find(c => c.id === world.card);
|
||||
if (!card) throw new Error('Unknown Homeworld.');
|
||||
const row = pool(world.id, card.name, force.normal + force.elite, force.elite);
|
||||
if (world.native === player.id) nativeReserves.push(row);
|
||||
else if (row.total > 0) rows.push(row);
|
||||
}
|
||||
if (nativeReserves.length && (nativeReserves.reduce((n, r) => n + r.total, 0) !== reserves.total ||
|
||||
nativeReserves.reduce((n, r) => n + r.elite, 0) !== reserves.elite))
|
||||
throw new Error('Native reserves disagree with their Homeworld breakdown.');
|
||||
const marker = player.noField?.deployed?.location;
|
||||
const total = rows.reduce((n, r) => n + r.total, 0);
|
||||
if (!Number.isSafeInteger(total)) throw new Error('Invalid force inventory.');
|
||||
return {
|
||||
rows, nativeReserves,
|
||||
total,
|
||||
normal: rows.reduce((n, r) => n + r.normal, 0),
|
||||
elite: rows.reduce((n, r) => n + r.elite, 0),
|
||||
advisors: rows.reduce((n, r) => n + (r.advisors ? r.total : 0), 0),
|
||||
split: !!player.elites,
|
||||
marker: marker ? { name: locationName(marker.territory, marker.sector) } : null,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -2107,6 +2107,31 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Full-table rendering, JSON continuation, pending-window rejection and SQLite acceptance/withdrawal races are covered. All-configuration and full visual acceptance remain open.', evidence: ['tests/nexus-offer-controls.test.tsx', 'tests/nexus-offer-recovery.test.ts', 'tests/nexus-alliance.test.ts'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'force-counters',
|
||||
title: 'Inspect force counters and their locations',
|
||||
category: 'Getting started',
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Prototyped',
|
||||
summary: 'Enlarge faction counters and read public reserves, Tanks, deployed groups and Homeworld breakdowns.',
|
||||
steps: [
|
||||
'Use Inspect forces beneath any player’s force summary. Everyone can inspect these public counts, including while another player is making a decision. Opening the inspector does not move forces or submit a game action.',
|
||||
'Reserves, the Tleilaxu Tanks and each deployed location show physical counters. A special counter is included in that location’s total, not added again. Battle strength, spice support, leaders and temporary powers are separate.',
|
||||
'Ixian normal counters are Suboids and their special counters are Cyborgs. Emperor’s special counters are Sardaukar; Fremen’s are Fedaykin. When a table uses combined counts, it does not assign a separate starred count to each location.',
|
||||
'Bene Gesserit advisors are the reverse face of the same force counters. A territory’s advisor stance applies across its sectors; advisor counts are included in that territory’s total. Reserves and the Tanks have no deployed stance.',
|
||||
'Native Homeworld groups subdivide reserves and are listed separately as a breakdown. Foreign Homeworld deployments are additional locations. Counters inside the Mobile Stronghold remain inspectable before its setup placement; Discovery locations use their names without a map sector.',
|
||||
'A concealed No-Field appears as a separate marker with its public location. It counts as one for board presence, but adds no physical counter to this inventory. Its hidden value is not revealed or inferred; revelation draws from the reserves available then.',
|
||||
'Refresh restores the saved public groups. The inspector follows table updates while open. Counter illustrations are enlarged original diagrams; the gallery shows component types rather than quantities in a saved game.',
|
||||
],
|
||||
related: ['advanced-combat', 'ix-forces', 'advanced-advisors', 'homeworlds', 'richese-no-field'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Public physical pools, special subsets, advisor stances, native reserve breakdowns and foreign deployments are projected without changing game state. Full physical-component and rules certification remain open.', evidence: ['game/force-inventory.ts'] },
|
||||
{ area: 'Player controls', status: 'Implemented', detail: 'Every live player summary opens enlarged force inspection; the internal reference includes all twelve factions.', evidence: ['components/force-inspector.tsx', 'components/game-table.tsx', 'components/rules-reference.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'Read-only inspection adds no game action. Existing minimal legal participation is unchanged; strategy tuning remains deferred.' },
|
||||
{ area: 'Documentation', status: 'Implemented', detail: 'Physical counts, special identities, advisor faces, Homeworld accounting and concealed markers are explained.', evidence: ['docs/FORCE_INSPECTION.md'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Focused checks cover all faction identities, public-view equivalence, JSON recovery, physical subsets, Basic Homeworld special identity, unplaced HMS, foreign deployments and concealed-value privacy. Full visual and combined-game acceptance remain open.', evidence: ['tests/force-inventory.test.ts', 'tests/force-inspector.test.tsx'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'alliance-funding',
|
||||
title: 'Fund an ally’s bid or shipment',
|
||||
|
||||
Reference in new issue
Block a user