Shipment-only AND/OR questions now evaluate one eventual reserve shipment or skip as a whole. Yes to AND requires every leaf, Yes to OR requires one, and No negates the complete expression. Separate answered questions remain jointly binding. The completion search considers all destination branches and owned preparation paths. Release and fulfillment apply once to the aggregate promise, and legacy flat records retain their shape. The two-condition browser builder, grouped question/history and respondent guidance, all four AI answer policies and saved continuation are connected. Nested expressions have explicit software size limits. Existing active-unused-shipment, Basic/no-Guild Advanced and module gates remain; mixed present/future predicates and unresolved rule questions are unchanged. Independent source, privacy and persistence review found no blocker. Types, lint, 4815 offline tests, build and 43 HTTP tests pass with stable source. The first typecheck caught an omitted flat-only guidance consumer and a legacy test requiring type narrowing; grouped guidance and an actual promised-state regression fix them. SQLite Basic/Advanced tests verify private reauthentication, second-OR-branch fulfillment, zero-write rejected actions, legal No skip and duplicate CAS with one result. Two fixed genuine Basic/Advanced mixed-profile games complete in 1534 accepted actions, 41 periodic private JSON restores and 0 rejected candidates. Each uses one scenario-directed compound question from an actual held Truthtrance at a real legal opportunity; AI supplies all responses and other actions. Exact card, force, typed-elite and post-setup traitor custody are checked. Initial private harness runs needed correct immutable setup configuration and full-game auction/Fedaykin inventory accounting; original failed traces remain private, and the final same-seed runs pass. The full sample tree is reconstructed exactly by replacing only the three later UI/test edits; all 219 game source files match the samples. No game logic changed after those runs. Browser QA used genuine base initialization before conserved turn-two staging. The human asked twenty to Carthag OR four to Arrakeen; the AI answered Yes and shipped four to the second destination. Public grouped history survived refresh. All 6 accepted actions replay exactly, with one discarded Truthtrance, one fulfilled aggregate promise, private JSON views and original seat custody intact. This remains bounded prototype evidence, not complete Truthtrance or mode certification. All 1004 opening games and original seat/recovery records are preserved (1038 rooms after checks), including all 1005 rooms present immediately before checks. SQLite integrity and foreign-key checks pass. The healthy server was reused without restart, reset or recurring automation. Verified source tree: 1b24372fbe34e24db56e8f095c25744eeefb27581f2bc6cced2fb752f5bdcb94
63 lines
3.0 KiB
TypeScript
63 lines
3.0 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { applyAction, viewGame, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { compoundShipmentGame, askCompoundShipment, compoundShipmentCustody } from './fixture-compound-shipment';
|
|
import { matchesShipment } from '../game/shipment-promises';
|
|
|
|
function pending(advanced: boolean) {
|
|
const g = compoundShipmentGame(advanced);
|
|
Object.assign(g.players[0], { reserves: 10, forces: { 'carthag:11': 10 }, spice: 4 });
|
|
return askCompoundShipment(g, { op: 'or', terms: [
|
|
{ territory: 'carthag', minimum: 20 }, { territory: 'arrakeen', minimum: 4 },
|
|
] });
|
|
}
|
|
void test('all four profiles answer and complete the later OR branch when the first branch is impossible', () => {
|
|
for (const advanced of [false, true]) for (const level of DIFFICULTIES) {
|
|
let g = pending(advanced);
|
|
g.players[0].bot = level;
|
|
const answer = botActions(viewGame(g, 'p'))[0];
|
|
assert.deepEqual(answer, { type: 'truthAnswer', answer: 'yes' });
|
|
g = applyAction(g, 'p', answer);
|
|
const candidates = botActions(viewGame(g, 'p'));
|
|
const ship = candidates.find(action => action.type === 'ship');
|
|
assert.ok(ship, `${level} must retain an executable completion beyond its strategic shortlist`);
|
|
for (const action of candidates) assert.doesNotThrow(() => applyAction(g, 'p', action));
|
|
const next = applyAction(g, 'p', ship);
|
|
assert.equal(next.players[0].spice, 0);
|
|
assert.equal(next.players[0].reserves, 6);
|
|
const promise = next.shipmentPromises?.[0];
|
|
assert.ok(promise);
|
|
assert.equal(promise.fulfilled, true);
|
|
assert.equal(matchesShipment(promise, { territory: 'arrakeen', amount: 4 }), true);
|
|
compoundShipmentCustody(next);
|
|
for (const player of next.players)
|
|
assert.deepEqual(JSON.parse(JSON.stringify(viewGame(next, player.id))),
|
|
JSON.parse(JSON.stringify(viewGame(JSON.parse(JSON.stringify(next)) as Game, player.id))));
|
|
}
|
|
});
|
|
|
|
void test('impossible different-destination AND never makes an AI publish Yes', () => {
|
|
for (const level of DIFFICULTIES) {
|
|
const g = askCompoundShipment(compoundShipmentGame(), { op: 'and', terms: [
|
|
{ territory: 'carthag', minimum: 1 }, { territory: 'arrakeen', minimum: 1 },
|
|
] });
|
|
g.players[0].bot = level;
|
|
assert.deepEqual(botActions(viewGame(g, 'p'))[0], { type: 'truthAnswer', answer: 'no' });
|
|
}
|
|
});
|
|
|
|
void test('compound answer ranking does not inspect rival private fields or mutate its supplied view', () => {
|
|
const g = pending(true);
|
|
g.players[0].bot = 'Hard';
|
|
const original = viewGame(g, 'p');
|
|
const expected = botActions(original);
|
|
const view = viewGame(g, 'p');
|
|
for (const rival of view.players.filter(player => player.id !== 'p'))
|
|
for (const field of ['hand', 'spice', 'traitors', 'faceDancers'])
|
|
Object.defineProperty(rival, field, { get() { throw new Error(`Unentitled ${field} access`); } });
|
|
assert.deepEqual(botActions(view), expected);
|
|
assert.deepEqual(viewGame(g, 'p'), original);
|
|
});
|