Files
Dune/tests/guild-transport-karama-recovery.test.ts
T

108 lines
5.1 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { baseDeck } from '../game/cards';
import { newPlayer, type Game } from '../game/engine';
import type * as Rooms from '../db/rooms';
import { unitStore } from './fixture-nexus-room-store';
const clock: Rooms.RoomsClock = { now: () => 10000, sleep: async () => {} };
void test('a restarted room resolves exactly one canceled multi-sector Guild transport across a CAS race', async () => {
const store = unitStore();
try {
const made = await store.rooms.createRoom('Guild transport CAS', 'guild', false, []);
const code = made.view.code;
const joinedEmperor = await store.rooms.joinRoom(code, 'Emperor', 'emperor');
const joinedAtreides = await store.rooms.joinRoom(code, 'Atreides', 'atreides');
const tokens = [made.token, joinedEmperor.token!, joinedAtreides.token!];
const seats = await Promise.all(tokens.map(token => store.restart().authenticate(code, token)));
const ids = seats.map(seat => seat.playerId);
const initial = await store.restart().readRoom(code);
const g: Game = {
...initial,
players: [newPlayer(ids[0], 'Guild', 'guild'), newPlayer(ids[1], 'Emperor', 'emperor'),
newPlayer(ids[2], 'Atreides', 'atreides')],
status: 'playing', phase: 5, turn: 2, storm: 18,
order: ids, movementRemaining: ids, active: ids[0],
ready: [], decision: null, response: null, phaseOpening: null,
deck: baseDeck(), discard: [],
};
for (const player of g.players) {
player.spice = 20;
player.reserves = 20;
player.forces = {};
player.hand = [];
}
g.players[0].forces = { 'pasty_mesa:5': 2, 'pasty_mesa:6': 3 };
g.players[0].reserves = 15;
const index = g.deck.findIndex(card => card.effect === 'karama');
const karama = g.deck.splice(index, 1)[0];
g.players[2].hand.push(karama);
store.sqlite.prepare('UPDATE rooms SET state=?, version=? WHERE code=?')
.run(JSON.stringify(g), g.version, code);
store.writes.length = 0;
await store.restart().act(code, seats[0], g.version, {
type: 'guildShip', forces: { 'pasty_mesa:5': 1, 'pasty_mesa:6': 2 },
territory: 'carthag', sector: 11, allyPayment: 0,
}, clock);
// Native cross-planet transport first opens its permission window; pass it.
let permission = await store.restart().readRoom(code), passes = 0;
while (permission.response?.kind === 'guildTransportPermission') {
const index = ids.findIndex(id => id !== permission.response!.owner && !permission.response!.passed.includes(id));
await store.restart().act(code, seats[index], permission.version, { type: 'passResponse' }, clock);
permission = await store.restart().readRoom(code);
passes++;
}
const offered = permission;
assert.equal(offered.version, g.version + 1 + passes);
assert.equal(offered.response?.kind, 'guildRate');
assert.equal(offered.pendingGuildTransport?.cost, 2);
assert.equal(offered.players[0].spice, 20);
assert.equal(offered.players[0].reserves, 15);
assert.deepEqual(offered.players[0].forces, g.players[0].forces);
const responderView = await store.restart().readSeatView(code, seats[2]);
assert.equal(responderView.responseControls?.cancelCards.includes(karama.id), true);
assert.equal(responderView.players.find(player => player.id === ids[0])?.spice, undefined);
const before = JSON.parse(JSON.stringify(offered));
await assert.rejects(store.restart().act(code, seats[0], offered.version,
{ type: 'card', mode: 'cancel', card: karama.id }, clock));
assert.deepEqual(await store.restart().readRoom(code), before);
let arrivals = 0;
let release!: () => void;
const waiting = new Promise<void>(resolve => { release = resolve; });
store.hooks.beforeWrite = async () => {
if (++arrivals === 2) release();
await waiting;
};
let results: PromiseSettledResult<unknown>[];
try {
results = await Promise.allSettled([0, 1].map(() => store.restart().act(
code, seats[2], offered.version,
{ type: 'card', mode: 'cancel', card: karama.id }, clock,
)));
} finally {
delete store.hooks.beforeWrite;
}
assert.equal(arrivals, 2);
assert.equal(results.filter(result => result.status === 'fulfilled').length, 1);
assert.deepEqual(store.writes.slice(-2).map(write => write.changes).sort((a, b) => a - b), [0, 1]);
const done = await store.restart().readRoom(code);
assert.equal(done.version, offered.version + 1);
assert.equal(done.pendingGuildTransport, null);
assert.equal(done.response, null);
assert.equal(done.players[0].spice, 17);
assert.equal(done.players[0].reserves, 15);
assert.equal(done.players[0].forces['pasty_mesa:5'], 1);
assert.equal(done.players[0].forces['pasty_mesa:6'], 1);
assert.equal(done.players[0].forces['carthag:11'], 3);
assert.equal(done.discard.filter(card => card.id === karama.id).length, 1);
await assert.rejects(store.restart().act(code, seats[2], offered.version,
{ type: 'card', mode: 'cancel', card: karama.id }, clock));
assert.deepEqual(await store.restart().readRoom(code), done);
} finally {
store.sqlite.close();
}
});