177 lines
5.3 KiB
TypeScript
177 lines
5.3 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { applyAction, viewGame } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import {
|
|
quoteNexusAlliance,
|
|
type NexusAllianceSeat,
|
|
} from '../game/nexus-alliance';
|
|
import {
|
|
nexusInventory,
|
|
nexusPlayer,
|
|
nexusReady,
|
|
nexusTurnTwo,
|
|
orderNexusSpice,
|
|
} from './fixture-nexus-cards';
|
|
|
|
const players = (): NexusAllianceSeat[] => [
|
|
{ id: 'a', ally: null },
|
|
{ id: 'b', ally: null },
|
|
{ id: 'c', ally: null },
|
|
{ id: 'd', ally: null },
|
|
];
|
|
|
|
void test('a unilateral offer preserves every alliance and unrelated offer without mutating input', () => {
|
|
const seats = players();
|
|
const offers = { c: 'd' };
|
|
const before = structuredClone({ seats, offers });
|
|
Object.freeze(seats);
|
|
Object.freeze(offers);
|
|
for (const seat of seats) Object.freeze(seat);
|
|
|
|
const quote = quoteNexusAlliance({
|
|
players: seats,
|
|
offers,
|
|
actor: 'a',
|
|
target: 'b',
|
|
});
|
|
assert.deepEqual(quote, {
|
|
allies: { a: null, b: null, c: null, d: null },
|
|
offers: { c: 'd', a: 'b' },
|
|
formed: null,
|
|
});
|
|
assert.deepEqual({ seats, offers }, before);
|
|
});
|
|
|
|
void test('mutual offers form a reciprocal pair and remove only that pair own offers', () => {
|
|
const quote = quoteNexusAlliance({
|
|
players: players(),
|
|
offers: { b: 'a', c: 'a', d: 'c' },
|
|
actor: 'a',
|
|
target: 'b',
|
|
});
|
|
assert.deepEqual(quote, {
|
|
allies: { a: 'b', b: 'a', c: null, d: null },
|
|
offers: { c: 'a', d: 'c' },
|
|
formed: ['a', 'b'],
|
|
});
|
|
});
|
|
|
|
void test('withdrawing breaks the reciprocal pair and removes only the actor offer', () => {
|
|
const quote = quoteNexusAlliance({
|
|
players: [
|
|
{ id: 'a', ally: 'b' },
|
|
{ id: 'b', ally: 'a' },
|
|
{ id: 'c', ally: null },
|
|
],
|
|
offers: { a: 'c', b: 'c', c: 'a' },
|
|
actor: 'a',
|
|
target: null,
|
|
});
|
|
assert.deepEqual(quote, {
|
|
allies: { a: null, b: null, c: null },
|
|
offers: { b: 'c', c: 'a' },
|
|
formed: null,
|
|
});
|
|
});
|
|
|
|
void test('invalid actors, targets and occupied alliances reject without mutation', () => {
|
|
const seats = players();
|
|
const offers = { c: 'd' };
|
|
const before = structuredClone({ seats, offers });
|
|
for (const [actor, target, pattern] of [
|
|
['missing', 'a', /seated player/],
|
|
['a', 'a', /another player/],
|
|
['a', 'missing', /another seated player/],
|
|
] as const)
|
|
assert.throws(
|
|
() => quoteNexusAlliance({ players: seats, offers, actor, target }),
|
|
pattern,
|
|
);
|
|
seats[0].ally = 'c';
|
|
seats[2].ally = 'a';
|
|
assert.throws(
|
|
() =>
|
|
quoteNexusAlliance({
|
|
players: seats,
|
|
offers,
|
|
actor: 'a',
|
|
target: 'b',
|
|
}),
|
|
/Break existing alliances/,
|
|
);
|
|
assert.deepEqual(
|
|
{ seats, offers },
|
|
{
|
|
seats: [
|
|
{ id: 'a', ally: 'c' },
|
|
before.seats[1],
|
|
{ id: 'c', ally: 'a' },
|
|
before.seats[3],
|
|
],
|
|
offers: before.offers,
|
|
},
|
|
);
|
|
});
|
|
|
|
function negotiations() {
|
|
let game = nexusTurnTwo();
|
|
orderNexusSpice(game, ['worm', 'land', 'land']);
|
|
game = nexusReady(game);
|
|
assert.ok(game.spiceWindow);
|
|
game = nexusReady(game);
|
|
assert.equal(game.nexus, true);
|
|
assert.equal(game.spiceWindow, null);
|
|
return game;
|
|
}
|
|
|
|
void test('AI initiates a reciprocal Nexus alliance without retrying its outstanding offer', () => {
|
|
let game = negotiations();
|
|
nexusPlayer(game, 'f').bot = 'Easy';
|
|
nexusPlayer(game, 'a').bot = 'Medium';
|
|
nexusPlayer(game, 'h').bot = 'Hard';
|
|
|
|
const proposal = botActions(viewGame(game, 'a'))[0];
|
|
assert.deepEqual(proposal, { type: 'alliance', target: 'f' });
|
|
game = applyAction(game, 'a', proposal);
|
|
assert.deepEqual(game.allianceOffers, { a: 'f' });
|
|
assert.deepEqual(botActions(viewGame(game, 'a'))[0], { type: 'ready' });
|
|
const acceptance = botActions(viewGame(game, 'f'))[0];
|
|
assert.deepEqual(acceptance, { type: 'alliance', target: 'a' });
|
|
game = applyAction(game, 'f', acceptance);
|
|
assert.equal(nexusPlayer(game, 'a').ally, 'f');
|
|
assert.equal(nexusPlayer(game, 'f').ally, 'a');
|
|
assert.deepEqual(game.allianceOffers, {});
|
|
game = nexusReady(game);
|
|
assert.equal(game.nexusCards?.phase?.stage, 'drawing');
|
|
const draw = botActions(viewGame(game, 'h'))[0];
|
|
assert.equal(draw.type, 'nexusCardChoice');
|
|
game = applyAction(game, 'h', draw);
|
|
assert.equal(game.nexusCards?.phase?.stage, 'complete');
|
|
nexusInventory(game);
|
|
});
|
|
|
|
void test('the actual engine preserves ordinary offer, formation and withdrawal side effects', () => {
|
|
let game = negotiations();
|
|
game.ready = ['h'];
|
|
game = applyAction(game, 'f', { type: 'alliance', target: 'a' });
|
|
assert.deepEqual(game.allianceOffers, { f: 'a' });
|
|
assert.equal(nexusPlayer(game, 'f').ally, null);
|
|
assert.deepEqual(game.ready, []);
|
|
|
|
game.ready = ['h'];
|
|
game = applyAction(game, 'a', { type: 'alliance', target: 'f' });
|
|
assert.equal(nexusPlayer(game, 'f').ally, 'a');
|
|
assert.equal(nexusPlayer(game, 'a').ally, 'f');
|
|
assert.equal(nexusPlayer(game, 'f').allySinceTurn, game.turn);
|
|
assert.equal(nexusPlayer(game, 'a').allySinceTurn, game.turn);
|
|
assert.deepEqual(game.allianceOffers, {});
|
|
assert.deepEqual(game.ready, []);
|
|
|
|
game.ready = ['h'];
|
|
game = applyAction(game, 'f', { type: 'alliance', target: null });
|
|
assert.equal(nexusPlayer(game, 'f').ally, null);
|
|
assert.equal(nexusPlayer(game, 'a').ally, null);
|
|
assert.deepEqual(game.ready, []);
|
|
});
|