Files
Dune/tests/richese-homeworld-marker-http.test.ts
T

89 lines
5.4 KiB
TypeScript

// @dune-suite integration
import assert from 'node:assert/strict';
import test from 'node:test';
import { botActions } from '../game/bots';
import { richeseHomeworldMarkerChoice } from '../game/homeworld-shipment-options';
import type { Action, GameView } from '../game/engine';
const base = process.env.DUNE_TEST_URL ?? 'http://localhost:3000';
async function request(path: string, cookie?: string, body?: unknown) {
const response = await fetch(base + path, {
method: body === undefined ? 'GET' : 'POST', signal: AbortSignal.timeout(15_000),
headers: { ...(body === undefined ? {} : { 'Content-Type': 'application/json' }), ...(cookie ? { cookie } : {}) },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
return { status: response.status, cookie: response.headers.get('set-cookie')?.split(';')[0],
data: await response.json() as GameView & { error?: string } };
}
/** All state belongs to the new QA room. The existing bot adapter proposes
* legal actions from each participant's own authenticated view; no server
* state, physical tokens, phases, hands or balances are staged. */
void test('authenticated Richese ships a concealed marker to a Homeworld without revealing its identity', async () => {
const host = await request('/api/rooms', undefined, {
name: 'Homeworld marker owner', faction: 'richese', advanced: false, expansions: ['choam'],
});
assert.equal(host.status, 201, JSON.stringify(host.data)); assert.ok(host.cookie);
const path = `/api/rooms/${host.data.code}`;
const peer = await request(path, undefined, { type: 'join', name: 'Homeworld marker opponent', faction: 'atreides' });
assert.equal(peer.status, 200, JSON.stringify(peer.data)); assert.ok(peer.cookie);
const seats = [{ cookie: host.cookie, id: host.data.me }, { cookie: peer.cookie, id: peer.data.me }];
let view = peer.data;
const act = async (cookie: string, action: Action) => {
const next = await request(path, cookie, { version: view.version, action });
assert.equal(next.status, 200, `Rejected ${action.type}: ${JSON.stringify(next.data)}`);
view = next.data; return next.data;
};
await act(host.cookie, { type: 'homeworlds', enabled: true });
for (const seat of seats) await act(seat.cookie, { type: 'ready' });
await act(host.cookie, { type: 'start' });
async function read(cookie: string): Promise<GameView> {
const result = await request(path, cookie);
assert.equal(result.status, 200, JSON.stringify(result.data));
view = result.data; return result.data;
}
async function advance(until: (owner: GameView) => boolean) {
for (let count = 0; count < 300; count++) {
const owner = await read(host.cookie!);
if (until(owner)) return owner;
assert.notEqual(owner.status, 'finished', 'The QA game ended before its marker shipment.');
let proposed: { cookie: string; action: Action } | null = null;
for (const seat of seats) {
const current = await read(seat.cookie!);
const local = structuredClone(current);
const self = local.players.find(player => player.id === seat.id); assert.ok(self);
self.bot = 'Easy'; // Local action adapter only; the server seat remains human.
const actions = botActions(local);
if (actions.length) { proposed = { cookie: seat.cookie!, action: actions[0] }; break; }
}
assert.ok(proposed, `No native continuation at ${owner.status}/${owner.phase}/${owner.decision?.kind ?? 'no decision'}.`);
// During response windows pass rather than spending a physical Karama.
if (view.response && !view.response.passed.includes(view.me)) proposed.action = { type: 'passResponse' };
await act(proposed.cookie, proposed.action);
}
throw Error('The dedicated HTTP room did not reach its finite native marker boundary.');
}
const before = await advance(owner => owner.status === 'playing' && owner.phase === 5 && owner.active === owner.me &&
!!owner.homeworldShipment && !owner.homeworldShipment.blocked && !!owner.richeseNoField?.canShip);
const info = before.richeseNoField!; assert.ok(info.private);
const token = info.private.tokens.find(candidate => candidate.id !== info.private!.lastShipped); assert.ok(token);
const tokens = info.private.tokens.map(candidate => candidate.id);
const payer = before.players.find(candidate => candidate.id === before.me)!;
const originalSpice = payer.spice, originalReserves = payer.reserves;
const quote = richeseHomeworldMarkerChoice(before, 'homeworld:atreides', token.id);
assert.ok(quote.action, quote.blocked ?? 'The original private shipment should be available.');
await act(host.cookie, quote.action);
const settled = await advance(owner => owner.status === 'playing' && owner.phase === 5 &&
!!owner.richeseNoField?.private?.deployed && owner.players.find(player => player.id === owner.me)?.shipped === true);
const after = settled.players.find(candidate => candidate.id === settled.me)!;
assert.equal(after.spice, originalSpice! - 1);
assert.equal(after.reserves, originalReserves);
assert.deepEqual(settled.richeseNoField!.private!.deployed!.location, { homeworld: 'homeworld:atreides' });
const peerView = await read(peer.cookie);
assert.equal(peerView.richeseNoField!.private, null);
assert.deepEqual(peerView.richeseNoField!.public.deployed!.location, { homeworld: 'homeworld:atreides' });
for (const id of tokens) assert.equal(JSON.stringify(peerView).includes(id), false, 'A foreign seat cannot see token IDs.');
});