Add Kwisatz Haderach component inspection
Connect an original companion face and enlarged rules to the private Atreides panel and battle composer, authorized full-plan inspection, public revealed plans and an internal reference topic. Private loss progress and availability come only from the existing owner projection; plan components receive only inclusion. Rules calculations, save shape, AI strategy and mode gates remain unchanged. Add focused projection, sealed/revealed rendering, JSON restoration, private-status and asset regressions. All 4950 offline tests, types, lint and production build pass. Independent privacy/rules review found no material findings. Browser checks verify reference opening, loaded artwork, Escape/Close focus return, mobile bounds and keyboard scrolling to the final rule. The focusable scroll region has a documented narrow lint exception. Earlier image-convention and focusability lint failures are retained in private evidence. HTTP tests are unnecessary for this presentation-only change. All 1373 rooms, 2883 seats and original auxiliary rows remain unchanged; SQLite integrity and foreign keys pass. Existing server reused without restart. Screenshot capture, complete live Advanced browser acceptance and final manufactured-component/art verification remain open. No rules gate is opened by this checkpoint. Private evidence: 20260921-kwisatz-inspection. Verified source tree: 1181471eed47b86390882999a2ffce0db6a44b1379c7e8f7fc406bef39a94179
This commit is contained in:
1 parent
ed53eb7d76
commit
66c41b2af5
16 files changed
+341
-24
No files matched your search
@@ -0,0 +1,39 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createElement } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { registerHooks } from 'node:module';
|
||||
import { kwisatzAvailability, type KwisatzDisplayState } from '../game/kwisatz-display';
|
||||
const aliases = registerHooks({ resolve(specifier, context, next) {
|
||||
return next(specifier === 'next/image' ? 'vinext/shims/image' : specifier, context);
|
||||
} });
|
||||
const { KwisatzPrivateStatus, KwisatzCardFace, KwisatzReferenceCard } = await import('../components/kwisatz-inspector');
|
||||
aliases.deregister();
|
||||
|
||||
void test('KH private progress distinguishes awakening, death, cancellation and territory reuse', () => {
|
||||
const base: KwisatzDisplayState = { active: true, dead: false, losses: 9 };
|
||||
const render = (state: KwisatzDisplayState) => renderToStaticMarkup(createElement(KwisatzPrivateStatus, { state, usedTerritoryName: 'Arrakeen' }));
|
||||
assert.match(render({ ...base, active: false, losses: 6 }), /6 of 7 battle losses/);
|
||||
assert.match(render({ ...base, active: false, losses: 6 }), /Not yet available/);
|
||||
assert.match(render(base), /9 of 7 battle losses · awakened/);
|
||||
assert.match(render(base), /max="7" value="7"/);
|
||||
assert.match(render({ ...base, dead: true }), /In the Tleilaxu Tanks/);
|
||||
const used = { ...base, usedAt: 'arrakeen' };
|
||||
assert.match(render(used), /Used this turn in Arrakeen/);
|
||||
assert.match(kwisatzAvailability(used, { territory: 'arrakeen', blocked: false }), /same territory/);
|
||||
assert.match(kwisatzAvailability(used, { territory: 'carthag', blocked: false }), /another territory/);
|
||||
assert.match(kwisatzAvailability(used, { territory: 'arrakeen', blocked: true }), /Karama prevents/);
|
||||
});
|
||||
|
||||
void test('KH reference has no private status and its original companion artwork is bundled separately', () => {
|
||||
const html = renderToStaticMarkup(createElement(KwisatzReferenceCard));
|
||||
assert.match(html, /Inspect Kwisatz Haderach · rules reference/);
|
||||
assert.doesNotMatch(html, /Your private status|<progress|of 7 battle losses/);
|
||||
const face = renderToStaticMarkup(createElement(KwisatzCardFace));
|
||||
assert.match(face, /kwisatz-haderach-v1.png/);
|
||||
const png = readFileSync(new URL('../public/art/leaders/kwisatz-haderach-v1.png', import.meta.url));
|
||||
assert.equal(png.subarray(1, 4).toString(), 'PNG');
|
||||
assert.equal(png.readUInt32BE(16), 1254);
|
||||
assert.equal(png.readUInt32BE(20), 1254);
|
||||
});
|
||||
@@ -10,6 +10,57 @@ import {
|
||||
type Game,
|
||||
} from '../game/engine';
|
||||
import { baseDeck } from '../game/cards';
|
||||
import { registerHooks } from 'node:module';
|
||||
import { createElement } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
const aliases = registerHooks({ resolve(specifier, context, next) {
|
||||
return next(specifier === 'next/image' ? 'vinext/shims/image' : specifier, context);
|
||||
} });
|
||||
const { RevealedBattle } = await import('../components/revealed-battle');
|
||||
const { PrivateBattlePlan } = await import('../components/private-battle-plan');
|
||||
const { BattleComponentInspection } = await import('../components/battle-component-inspection');
|
||||
const { KwisatzPrivateStatus } = await import('../components/kwisatz-inspector');
|
||||
aliases.deregister();
|
||||
|
||||
void test('KH component inspection follows real sealed and revealed projections without exposing private status', () => {
|
||||
let g = prepared(fixture(true));
|
||||
g.players[0].ally = 'g';
|
||||
g.players[2].ally = 'a';
|
||||
g = applyAction(g, 'a', { type: 'battlePlan', dial: 0, leader: 'atreides-0', kwisatz: true });
|
||||
for (const id of ['a', 'e', 'g']) {
|
||||
const v = viewGame(g, id);
|
||||
assert.equal(renderToStaticMarkup(createElement(RevealedBattle, { game: v })), '');
|
||||
assert.equal(Boolean(v.players[0].kwisatz), id === 'a');
|
||||
}
|
||||
g = applyAction(g, 'e', { type: 'battlePlan', dial: 0, leader: 'emperor-0' });
|
||||
const before = JSON.stringify(g);
|
||||
for (const id of ['a', 'e', 'g']) {
|
||||
const v = viewGame(g, id);
|
||||
const html = renderToStaticMarkup(createElement(RevealedBattle, { game: v }));
|
||||
assert.match(html, /Inspect Kwisatz Haderach · plan component/);
|
||||
assert.doesNotMatch(html, /Your private status|<progress|of 7 battle losses/);
|
||||
assert.equal(renderToStaticMarkup(createElement(RevealedBattle, { game: viewGame(JSON.parse(before), id) })), html);
|
||||
}
|
||||
const own = viewGame(g, 'a').players[0].kwisatz!;
|
||||
assert.match(renderToStaticMarkup(createElement(KwisatzPrivateStatus, { state: own })), /7 of 7 battle losses/);
|
||||
assert.equal(JSON.stringify(g), before);
|
||||
const basic = structuredClone(g);
|
||||
basic.advanced = false;
|
||||
assert.equal(viewGame(basic, 'a').players[0].kwisatz, undefined);
|
||||
});
|
||||
|
||||
void test('authorized full-plan KH inspection needs only inclusion and never accepts another viewer', () => {
|
||||
const v = viewGame(prepared(fixture(true)), 'a');
|
||||
const plan = { dial: 0, support: 0, leader: null, weapon: null, defense: null, kwisatz: true };
|
||||
// A constructed authorized view isolates the component boundary; timing is covered elsewhere.
|
||||
const inspected = { ...v, battle: { ...v.battle!, fullPlan: { owner: 'a', target: 'a', stage: 'inspected' as const }, fullPlanInsight: { target: 'a', plan, cards: [] } } };
|
||||
const html = renderToStaticMarkup(createElement(PrivateBattlePlan, { game: inspected }));
|
||||
assert.match(html, /Inspect Kwisatz Haderach · plan component/);
|
||||
assert.doesNotMatch(html, /Your private status|<progress|of 7 battle losses/);
|
||||
assert.equal(renderToStaticMarkup(createElement(PrivateBattlePlan, { game: { ...inspected, me: 'g' } })), '');
|
||||
assert.equal(renderToStaticMarkup(createElement(PrivateBattlePlan, { game: { ...inspected, battle: { ...inspected.battle, revealed: true } } })), '');
|
||||
assert.equal(renderToStaticMarkup(createElement(BattleComponentInspection, { cards: [], playerName: 'Atreides', kwisatz: false })), '');
|
||||
});
|
||||
/** Transfer fixture cards from the actual deck; never duplicate another seat's hand. */
|
||||
function setBattleHand(g: Game, id: string, ids: string[]) {
|
||||
const owner = g.players.find((p) => p.id === id)!;
|
||||
|
||||
Reference in new issue
Block a user