Files
Dune/tests/charity-order.test.ts

129 lines
6.4 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
CharityOrderError,
charityOrderBlock as block,
createCharityOrder as create,
finishCharityOrder as finish,
startCharityOrder as start,
validateCharityOrder as validate,
type CharityOrder,
type CharityOrderContext,
} from '../game/charity-order';
function context(ready: readonly string[] = []): CharityOrderContext {
return { turn: 3, order: ['c', 'a', 'b'], players: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], ready };
}
function rejects(current: CharityOrderContext, frame: CharityOrder, action: () => unknown): void {
const beforeContext = structuredClone(current), beforeFrame = structuredClone(frame);
assert.throws(action, CharityOrderError);
assert.deepEqual(current, beforeContext);
assert.deepEqual(frame, beforeFrame);
}
void test('Charity preserves the actual rotated First Player order rather than roster order', () => {
const current = context(), frame = create(current);
assert.deepEqual(frame, { version: 1, turn: 3, order: ['c', 'a', 'b'], remaining: ['c', 'a', 'b'] });
assert.equal(JSON.stringify(frame), '{"version":1,"turn":3,"order":["c","a","b"],"remaining":["c","a","b"]}');
assert.deepEqual(start(current, frame, 'c'), frame);
assert.equal(Object.hasOwn(start(current, frame, 'c'), 'started'), false);
validate(current, frame);
assert.equal(block(current, frame, 'c'), null);
assert.match(block(current, frame, 'a')!, /Wait/);
assert.match(block(current, frame, 'foreign')!, /seated/);
const resumed = context(['c']);
assert.deepEqual(create(resumed), { ...frame, remaining: ['a', 'b'] });
assert.match(block(resumed, create(resumed), 'c')!, /finished/);
assert.equal(block(resumed, create(resumed), 'a'), null);
});
void test('only the current seat finishes, consuming the pre-finish prefix before the caller appends ready', () => {
const current = context(), frame = create(current);
rejects(current, frame, () => finish(current, frame, 'a'));
rejects(current, frame, () => start(current, frame, 'a'));
rejects(current, frame, () => finish(current, frame, 'foreign'));
const next = finish(current, frame, 'c');
assert.deepEqual(next.remaining, ['a', 'b']);
assert.deepEqual(current.ready, []);
validate(context(['c']), next);
rejects(current, frame, () => validate(current, next));
rejects(current, frame, () => finish(context(['c']), frame, 'c'));
const afterSecond = finish(context(['c']), next, 'a');
validate(context(['c', 'a']), afterSecond);
assert.deepEqual(afterSecond.remaining, ['b']);
rejects(context(['c']), next, () => finish(context(['c']), next, 'c'));
});
void test('the final completed Charity frame stays valid and never reopens a seat', () => {
const current = context(['c', 'a']), frame = create(current);
const done = finish(current, frame, 'b'), completed = context(['c', 'a', 'b']);
validate(completed, done);
assert.deepEqual(done.remaining, []);
assert.deepEqual(create(completed), done);
for (const player of completed.order) {
assert.match(block(completed, done, player)!, /complete/);
rejects(completed, done, () => finish(completed, done, player));
}
});
void test('the native context requires actual unique seats, their permutation and an exact completed prefix', () => {
const base = context();
for (const current of [
{ ...base, turn: 0 }, { ...base, turn: 1.5 }, { ...base, turn: Number.NaN },
{ ...base, players: [{ id: 'a' }] },
{ ...base, players: ['a', 'b', 'c', 'd', 'e', 'f', 'g'].map(id => ({ id })) },
{ ...base, players: [{ id: 'a' }, { id: 'a' }, { id: 'c' }] },
{ ...base, players: [{ id: 'a' }, { id: 'b' }, { id: ' ' }] },
{ ...base, order: ['c', 'a'] }, { ...base, order: ['c', 'a', 'a'] },
{ ...base, order: ['c', 'a', 'foreign'] }, context(['a']), context(['c', 'b']),
context(['c', 'c']), context(['c', 'a', 'b', 'foreign']),
{ ...base, claimed: ['foreign'] }, { ...base, claimed: ['c', 'c'] },
]) assert.throws(() => create(current), CharityOrderError);
for (const current of [null, [], { ...base, players: undefined }, { ...base, order: 'c,a,b' },
{ ...base, ready: undefined }, { ...base, order: Array.from({ length: 3 }) },
{ ...base, players: Array.from({ length: 3 }) }, { ...base, ready: Array.from({ length: 1 }) }])
assert.throws(() => create(current as unknown as CharityOrderContext), CharityOrderError);
for (const order of [['b', 'a'], ['f', 'a', 'b', 'c', 'd', 'e']]) {
const current = { turn: 1, order, players: [...order].reverse().map(id => ({ id })), ready: [] };
validate(current, create(current));
}
});
void test('stale, rearranged or extended frames cannot replace the original remaining suffix', () => {
const current = context(['c']), frame = create(current);
const missing = { ...frame } as Partial<CharityOrder>;
delete missing.remaining;
for (const value of [
null, [], missing, { ...frame, extra: true }, { ...frame, started: false },
{ ...frame, version: 2 }, { ...frame, turn: 2 },
{ ...frame, order: ['a', 'c', 'b'] }, { ...frame, order: ['c', 'a', 'a'] },
{ ...frame, remaining: ['b', 'a'] }, { ...frame, remaining: ['a'] },
{ ...frame, remaining: ['c', 'a', 'b'] }, { ...frame, remaining: ['a', 'foreign'] },
]) rejects(current, frame, () => validate(current, value as unknown as CharityOrder));
rejects(current, frame, () => validate({ ...current, turn: 4 }, frame));
rejects(current, frame, () => validate({ ...current, order: ['a', 'c', 'b'], ready: [] }, frame));
rejects(current, frame, () => finish(current, frame, ''));
});
void test('creation and finish detach both public arrays without changing the original context or frame', () => {
const current = context(['c']), before = structuredClone(current);
Object.freeze(current.order);
Object.freeze(current.ready);
Object.freeze(current.players);
Object.freeze(current);
const frame = create(current), original = structuredClone(frame);
Object.freeze(frame.order);
Object.freeze(frame.remaining);
Object.freeze(frame);
const next = finish(current, frame, 'a');
assert.notEqual(frame.order, current.order);
assert.notEqual(frame.remaining, current.order);
assert.notEqual(frame.order, frame.remaining);
assert.notEqual(next.order, frame.order);
assert.notEqual(next.remaining, frame.remaining);
next.order.pop();
next.remaining.pop();
assert.deepEqual(current, before);
assert.deepEqual(frame, original);
});