Prototype Jacurutu income, Testing Station and Shrine effects
Connect automatic unambiguous Jacurutu physical-force income, a sole-occupant Testing Station storm choice and Shrine reciprocal physical card roles to engine actions, player controls, legal AI and saved continuation. Preserve Weather Control exclusion, exact card/force custody, private hand boundaries and committed effects after restart. Shared role checks cover existing ordinary and special Karama paths; reserved cards cannot enter another pending use. Keep Orgiz, mixed-force Jacurutu allocation and contested Cistern/Testing Station benefits explicitly pending. These are bounded prototypes; complete Discovery/Advanced/expansion and publication gates remain closed. Validation: types, lint and 4,345 offline tests passed (32 added), production build passed, and 40 HTTP tests passed. Independent cross-reviews repaired reserved-card deadlock, physical-card selection, alternate-role permission, storm source/token binding and public-entitlement bot ordering. The first broad run found five failures; focused repairs preceded the stable repeated check. Browser A5EJQ2W4 used genuine Discovery setup/reveal/free entry with targeted QA positioning. A human increased storm movement from two to three sectors, moving sector 6 to 9. Refresh restored station forces and the private Snooper. This was targeted control/recovery evidence, not an unmodified full game. All 187 opening saved rooms remained unchanged; 31 QA/HTTP rooms were added (218 total). Reused the running server; no restart or save reset. Source-bound report: /home/quintine/.local/share/dune/checkpoints/20260913-discovery-location-effects/verification-final/report.json Source fingerprint: beecb85ab7ec39319115389c115434d161591e8549802c542a6343a0fc19bea6 Durations: check 140540ms; build 6741ms; integration 24956ms. Aggregate goal counter 63,116,444 to 64,295,091 at final verification: 1,178,647 tokens before final Git/push overhead for three bounded functions. Standing authorization: Push all from now on.
This commit is contained in:
1 parent
ab0ab52849
commit
f0d3d9c464
31 files changed
+2524
-138
No files matched your search
@@ -0,0 +1,200 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
applyAction,
|
||||
normalizeAutomaticGame,
|
||||
viewGame,
|
||||
type Game,
|
||||
type Action,
|
||||
} from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { discoveryStormActions } from '../game/discovery-storm-options';
|
||||
import {
|
||||
createStormSource,
|
||||
discoveryStormOffer,
|
||||
discoveryStormSignature,
|
||||
} from '../game/discovery-storm';
|
||||
import {
|
||||
discoveryStormFixture,
|
||||
confirmDiscoveryStorm,
|
||||
holdStormCard,
|
||||
} from './fixture-discovery-storm';
|
||||
|
||||
const reload = (g: Game): Game => JSON.parse(JSON.stringify(g));
|
||||
function reject(g: Game, owner: string, action: Action) {
|
||||
const before = reload(g);
|
||||
assert.throws(() => applyAction(g, owner, action));
|
||||
assert.deepEqual(g, before);
|
||||
}
|
||||
for (const advanced of [false, true])
|
||||
for (const delta of [-1, 0, 1]) {
|
||||
void test(`Ecological Testing Station changes genuine ${advanced ? 'Advanced card' : 'Basic dial'} movement by ${delta} after the storm window`, () => {
|
||||
let g = discoveryStormFixture(advanced);
|
||||
assert.equal(g.ecologicalStorm, undefined);
|
||||
assert.equal(g.stormMovementSource?.kind, advanced ? 'card' : 'dials');
|
||||
g = confirmDiscoveryStorm(g);
|
||||
assert.equal(g.decision?.kind, 'ecologicalStorm');
|
||||
assert.equal(g.storm, 6);
|
||||
assert.deepEqual(normalizeAutomaticGame(reload(g)), g);
|
||||
const action = discoveryStormActions(viewGame(g, 'a')).find(
|
||||
(a) => a.delta === delta,
|
||||
)!;
|
||||
const before = reload(g);
|
||||
g = applyAction(reload(g), 'a', action);
|
||||
assert.deepEqual(
|
||||
before.players.map((p) => p.hand),
|
||||
g.players.map((p) => p.hand),
|
||||
);
|
||||
assert.equal(g.storm, 8 + delta);
|
||||
assert.equal(g.phase, 1);
|
||||
assert.equal(g.ecologicalStorm?.stage, 'complete');
|
||||
assert.equal(g.stormMovementSource, undefined);
|
||||
assert.equal(g.stormResolution, null);
|
||||
assert.deepEqual(normalizeAutomaticGame(reload(g)), g);
|
||||
reject(g, 'a', action);
|
||||
});
|
||||
}
|
||||
void test('station choice is private to its current sole fighter occupant; wrong owner, stale event and injected distance are immutable rejections', () => {
|
||||
const g = confirmDiscoveryStorm(discoveryStormFixture()),
|
||||
before = reload(g);
|
||||
assert.equal(viewGame(g, 'g').ecologicalStorm, null);
|
||||
assert.deepEqual(discoveryStormActions(viewGame(g, 'g')), []);
|
||||
const v = viewGame(g, 'a'),
|
||||
action = discoveryStormActions(v)[0];
|
||||
v.ecologicalStorm!.options[0].distance = 999;
|
||||
assert.deepEqual(g, before);
|
||||
reject(g, 'g', action);
|
||||
reject(g, 'a', { ...action, event: 'stale' });
|
||||
for (const delta of [-2, 2, 0.5, NaN]) reject(g, 'a', { ...action, delta });
|
||||
reject(g, 'a', { ...action, distance: 10 });
|
||||
reject(g, 'a', { type: 'ready' });
|
||||
});
|
||||
void test('all four AI levels select a legal owned station choice', () => {
|
||||
const g = confirmDiscoveryStorm(discoveryStormFixture(true));
|
||||
for (const level of DIFFICULTIES) {
|
||||
const v = viewGame(g, 'a');
|
||||
v.players[0].bot = level;
|
||||
const actions = botActions(v);
|
||||
assert.ok(actions.length);
|
||||
for (const action of actions)
|
||||
assert.doesNotThrow(() => applyAction(g, 'a', action));
|
||||
}
|
||||
});
|
||||
void test('Weather Control replaces natural provenance and bypasses the station, including zero movement', () => {
|
||||
for (const advanced of [false, true])
|
||||
for (const amount of [0, 3]) {
|
||||
let g = discoveryStormFixture(advanced);
|
||||
const card = holdStormCard(g, 'a', 'weather');
|
||||
g = applyAction(g, 'g', { type: 'ready' });
|
||||
g = applyAction(g, 'a', { type: 'card', card: card.id, amount });
|
||||
assert.equal(g.stormMovementSource?.kind, 'weather');
|
||||
assert.deepEqual(g.ready, []);
|
||||
assert.match(discoveryStormOffer(g)!.blocked!, /Weather Control/);
|
||||
g = confirmDiscoveryStorm(reload(g));
|
||||
assert.equal(g.ecologicalStorm, undefined);
|
||||
assert.equal(g.storm, 6 + amount);
|
||||
assert.equal(g.discard.filter((c) => c.id === card.id).length, 1);
|
||||
}
|
||||
});
|
||||
void test('unoccupied, contested and legacy unknown-source storms continue without inventing entitlement or changing distance', () => {
|
||||
for (const mode of ['empty', 'contested', 'legacy']) {
|
||||
let g = discoveryStormFixture();
|
||||
if (mode === 'empty') {
|
||||
g.players[0].reserves +=
|
||||
g.players[0].forces['ecological-testing-station:0'];
|
||||
delete g.players[0].forces['ecological-testing-station:0'];
|
||||
}
|
||||
if (mode === 'contested') {
|
||||
g.players[1].reserves--;
|
||||
g.players[1].forces['ecological-testing-station:0'] = 1;
|
||||
}
|
||||
if (mode === 'legacy') delete g.stormMovementSource;
|
||||
g = confirmDiscoveryStorm(g);
|
||||
assert.equal(g.ecologicalStorm, undefined);
|
||||
assert.equal(g.storm, 8);
|
||||
}
|
||||
});
|
||||
void test('station can reduce a one-sector natural storm to zero and never offers a negative distance', () => {
|
||||
let g = discoveryStormFixture(true);
|
||||
g.stormPending = 1;
|
||||
g.stormMovementSource = createStormSource(g.turn, 'card', 1);
|
||||
g = confirmDiscoveryStorm(g);
|
||||
g = applyAction(g, 'a', {
|
||||
type: 'decision',
|
||||
event: g.ecologicalStorm!.event,
|
||||
delta: -1,
|
||||
});
|
||||
assert.equal(g.storm, 6);
|
||||
assert.equal(g.phase, 1);
|
||||
const source = discoveryStormFixture();
|
||||
source.stormPending = 0;
|
||||
source.stormMovementSource = createStormSource(source.turn, 'dials', 0);
|
||||
assert.deepEqual(
|
||||
discoveryStormOffer(source)!.options.map((o) => o.delta),
|
||||
[0, 1],
|
||||
);
|
||||
});
|
||||
void test('orphaned station choices, rewritten Weather sources and mismatched traversal receipts fail closed on reads and normalization', () => {
|
||||
const original = confirmDiscoveryStorm(discoveryStormFixture());
|
||||
for (const mutate of [
|
||||
(g: Game) => {
|
||||
delete g.ecologicalStorm;
|
||||
},
|
||||
(g: Game) => {
|
||||
g.decision = null;
|
||||
},
|
||||
(g: Game) => {
|
||||
g.ecologicalStorm!.owner = 'g';
|
||||
g.ecologicalStorm!.signature = discoveryStormSignature(
|
||||
g.ecologicalStorm!,
|
||||
);
|
||||
},
|
||||
(g: Game) => {
|
||||
g.stormMovementSource = createStormSource(g.turn, 'weather', 2);
|
||||
},
|
||||
(g: Game) => {
|
||||
g.stormPending = 3;
|
||||
},
|
||||
]) {
|
||||
const g = reload(original);
|
||||
mutate(g);
|
||||
const before = reload(g);
|
||||
assert.throws(() => viewGame(g, 'a'));
|
||||
assert.throws(() => normalizeAutomaticGame(g));
|
||||
assert.deepEqual(g, before);
|
||||
}
|
||||
});
|
||||
void test('adjusted Advanced storm retains its receipt through Fremen protection and typed loss continuation', () => {
|
||||
let g = discoveryStormFixture(true);
|
||||
const f = g.players[2];
|
||||
f.reserves -= 3;
|
||||
f.forces['red_chasm:7'] = 3;
|
||||
f.elites!.reserves--;
|
||||
f.elites!.forces['red_chasm:7'] = 1;
|
||||
holdStormCard(g, 'g', 'karama');
|
||||
g = confirmDiscoveryStorm(g);
|
||||
g = applyAction(g, 'a', {
|
||||
type: 'decision',
|
||||
event: g.ecologicalStorm!.event,
|
||||
delta: 1,
|
||||
});
|
||||
assert.equal(g.response?.kind, 'stormProtection');
|
||||
assert.equal(g.ecologicalStorm!.stage, 'traversal');
|
||||
assert.equal(g.stormResolution!.discoveryStorm, g.ecologicalStorm!.signature);
|
||||
assert.deepEqual(normalizeAutomaticGame(reload(g)), g);
|
||||
const corrupt = reload(g);
|
||||
delete corrupt.stormResolution!.discoveryStorm;
|
||||
assert.throws(() => viewGame(corrupt, 'f'));
|
||||
for (let step = 0; g.response?.kind === 'stormProtection'; step++) {
|
||||
assert.ok(step < 10);
|
||||
const owner = g.players.find((p) => !g.response!.passed.includes(p.id))!.id;
|
||||
g = applyAction(reload(g), owner, { type: 'passResponse' });
|
||||
}
|
||||
assert.equal(g.decision?.kind, 'stormLosses');
|
||||
g = applyAction(reload(g), 'f', { type: 'decision', elite: 1 });
|
||||
assert.equal(g.players[2].forces['red_chasm:7'], 1);
|
||||
assert.equal(g.players[2].elites!.forces['red_chasm:7'] ?? 0, 0);
|
||||
assert.equal(g.ecologicalStorm?.stage, 'complete');
|
||||
assert.equal(g.storm, 9);
|
||||
});
|
||||
Reference in new issue
Block a user