92 lines
5.3 KiB
TypeScript
92 lines
5.3 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { TERRITORIES } from '../game/board';
|
|
import { compileMixedActionExpression, type MixedActionExpression } from '../game/mixed-action-question';
|
|
import {
|
|
parseTravelActionClaim, recordTravelShipment, travelBranches, travelPromiseValue, validateTravelPromises,
|
|
type TravelActionClaim, type TravelPromise,
|
|
} from '../game/travel-expression-promises';
|
|
|
|
const destination = TERRITORIES.find(territory => territory.sectors.length >= 2)!;
|
|
assert.ok(destination);
|
|
const [first, second] = destination.sectors;
|
|
const ship = (minimum: number, sector?: number): MixedActionExpression<TravelActionClaim> => ({
|
|
kind: 'action', action: { kind: 'ship', territory: destination.id, minimum,
|
|
...(sector === undefined ? {} : { sector }) },
|
|
});
|
|
const group = (kind: 'and' | 'or', ...terms: MixedActionExpression<TravelActionClaim>[]): MixedActionExpression<TravelActionClaim> =>
|
|
({ kind, terms });
|
|
function promise(mixed: MixedActionExpression<TravelActionClaim>, answer = true): TravelPromise {
|
|
return { turn: 1, mixed, event: 'travel', player: 'owner', asker: 'asker', answer,
|
|
compiled: compileMixedActionExpression(mixed, () => 'yes', answer),
|
|
shipment: { pending: true, event: null }, progress: [], movementClosed: false,
|
|
fulfilled: false, released: false };
|
|
}
|
|
|
|
void test('only a physical reserve shipment may name a real printed destination sector', () => {
|
|
const input = { kind: 'ship', territory: destination.id, minimum: 2, sector: first };
|
|
assert.deepEqual(parseTravelActionClaim(input), input);
|
|
assert.deepEqual(parseTravelActionClaim({ kind: 'ship', territory: destination.id, minimum: 2 }),
|
|
{ kind: 'ship', territory: destination.id, minimum: 2 });
|
|
for (const value of [
|
|
{ ...input, kind: 'move' }, { ...input, sector: -1 }, { ...input, sector: 1.2 },
|
|
{ ...input, sector: undefined }, { ...input, minimum: 0 }, { ...input, extra: true },
|
|
{ ...input, sector: Math.max(...destination.sectors) + 100 },
|
|
]) assert.throws(() => parseTravelActionClaim(value));
|
|
});
|
|
|
|
void test('sector claims compare actual destination and counts without inventing a legacy sector', () => {
|
|
const row = promise(ship(2, first));
|
|
const [correct] = recordTravelShipment([row], 'owner', 1, { territory: destination.id, amount: 2, sector: first });
|
|
assert.equal(correct.fulfilled, true);
|
|
assert.equal(travelPromiseValue(correct), true);
|
|
assert.deepEqual(correct.shipment.event, { territory: destination.id, amount: 2, sector: first });
|
|
const [otherSector] = recordTravelShipment([row], 'owner', 1, { territory: destination.id, amount: 2, sector: second });
|
|
assert.equal(otherSector.fulfilled, false);
|
|
assert.equal(travelPromiseValue(otherSector), false);
|
|
const [legacy] = recordTravelShipment([row], 'owner', 1, { territory: destination.id, amount: 2 });
|
|
assert.equal(travelPromiseValue(legacy), false);
|
|
assert.deepEqual(legacy.shipment.event, { territory: destination.id, amount: 2 });
|
|
validateTravelPromises([JSON.parse(JSON.stringify(correct))], ['owner', 'asker']);
|
|
});
|
|
|
|
void test('sectorless original questions retain their original serialized receipt shape', () => {
|
|
const row = promise(ship(2));
|
|
const [settled] = recordTravelShipment([row], 'owner', 1, { territory: destination.id, amount: 2, sector: first });
|
|
assert.equal(settled.fulfilled, true);
|
|
assert.deepEqual(settled.shipment.event, { territory: destination.id, amount: 2 });
|
|
validateTravelPromises([JSON.parse(JSON.stringify(settled))], ['owner', 'asker']);
|
|
});
|
|
|
|
void test('a whole No blocks the matching sector and permits another legal sector or no shipment', () => {
|
|
const row = promise(ship(2, first), false);
|
|
assert.throws(() => recordTravelShipment([row], 'owner', 1, { territory: destination.id, amount: 2, sector: first }));
|
|
const [different] = recordTravelShipment([row], 'owner', 1, { territory: destination.id, amount: 2, sector: second });
|
|
assert.equal(travelPromiseValue(different), false);
|
|
const [absent] = recordTravelShipment([row], 'owner', 1, null);
|
|
assert.equal(travelPromiseValue(absent), false);
|
|
});
|
|
|
|
void test('AND and No alternatives retain single-shipment sector correlations', () => {
|
|
const incompatible = promise(group('and', ship(1, first), ship(1, second)));
|
|
assert.deepEqual([...travelBranches(incompatible)], []);
|
|
const compatible = promise(group('and', ship(2), ship(3, first)));
|
|
const branches = [...travelBranches(compatible)];
|
|
assert.equal(branches.length, 1);
|
|
assert.equal(branches[0].length, 2);
|
|
const alternative = promise(group('and', ship(2), ship(3, first)), false);
|
|
assert.equal([...travelBranches(alternative)].length, 2);
|
|
const mixed = promise(group('and', ship(3, first), ship(2)), false);
|
|
const [matching] = recordTravelShipment([mixed], 'owner', 1, { territory: destination.id, amount: 3, sector: second });
|
|
assert.equal(travelPromiseValue(matching), false);
|
|
});
|
|
|
|
void test('stored sector receipts reject mismatched or invented printed sectors', () => {
|
|
const row = promise(ship(2, first));
|
|
for (const event of [
|
|
{ territory: destination.id, amount: 1, sector: -1 },
|
|
{ territory: destination.id, amount: 1, sector: Math.max(...destination.sectors) + 100 },
|
|
{ territory: destination.id, amount: 1, sector: first, extra: true },
|
|
]) assert.throws(() => recordTravelShipment([row], 'owner', 1, event));
|
|
});
|