Files
Dune/tests/bot-nexus-guild-betrayal.test.ts

137 lines
7.2 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { createGuildBetrayalFixture } from './fixture-nexus-guild-betrayal';
void test('all four legal bot profiles replace the original payment for every supported shipment adapter', () => {
for (const profile of DIFFICULTIES) {
const scenarios = [
{ source: 'reserve' },
{ source: 'guildTransport' },
{ source: 'homeworld' },
{ source: 'junction' },
{ source: 'reserve', homeworlds: true, occupiedJunction: true },
] as const;
for (const options of scenarios) {
const fixture = createGuildBetrayalFixture(options);
const game = structuredClone(fixture.game);
const own = viewGame(game, fixture.holder);
own.players.find(player => player.id === fixture.holder)!.bot = profile;
// Bots must not reconstruct eligibility or price from hidden opponents.
for (const rival of own.players) {
if (rival.id !== own.me) {
delete rival.hand;
delete rival.spice;
}
}
const reaction = own.guildBetrayalReaction!;
assert.equal(reaction.canUse, true);
const actions = botActions(own);
assert.deepEqual(actions, [{ type: 'guildBetrayalUse', event: fixture.event }]);
const result = applyAction(game, fixture.holder, actions[0]);
assert.equal(viewGame(result, fixture.holder).guildBetrayalReaction, null);
assert.equal(result.players.find(player => player.id === fixture.holder)!.spice,
game.players.find(player => player.id === fixture.holder)!.spice + fixture.price);
assert.equal(result.players.find(player => player.id === fixture.shipper)!.spice,
game.players.find(player => player.id === fixture.shipper)!.spice - fixture.price);
assert.equal(result.nexusCards?.cards?.hands[fixture.holder], null);
assert.ok(result.nexusCards?.cards?.discard.includes('guild'));
assert.deepEqual(result.players.map(player => ({ id: player.id, forces: player.forces, reserves: player.reserves, elites: player.elites, shipped: player.shipped })),
fixture.original.players.map(player => ({ id: player.id, forces: player.forces, reserves: player.reserves, elites: player.elites, shipped: player.shipped })));
assert.deepEqual(result.homeworlds?.custody, fixture.original.homeworlds?.custody);
}
}
});
void test('all profiles legally take their own fully funded invoice without treating a refund as payment funding', () => {
for (const profile of DIFFICULTIES) {
const fixture = createGuildBetrayalFixture({ payer: 'holder' });
const own = viewGame(fixture.game, fixture.holder);
own.players.find(player => player.id === fixture.holder)!.bot = profile;
const actions = botActions(own);
assert.deepEqual(actions, [{ type: 'guildBetrayalUse', event: fixture.event }]);
const result = applyAction(fixture.game, fixture.holder, actions[0]);
assert.equal(result.players.find(player => player.id === fixture.holder)!.spice,
fixture.game.players.find(player => player.id === fixture.holder)!.spice);
assert.equal(result.players.find(player => player.id === fixture.holder)!.reserves,
fixture.game.players.find(player => player.id === fixture.holder)!.reserves - 2);
assert.equal(result.players.find(player => player.id === fixture.holder)!.shipped, true);
assert.equal(result.nexusCards?.cards?.hands[fixture.holder], null);
assert.equal(viewGame(result, fixture.holder).guildBetrayalReaction, null);
}
});
void test('every profile acknowledges irrelevant faces until the original settlement completes, without fallthrough', () => {
for (const profile of DIFFICULTIES) {
for (const payer of ['holder', 'other'] as const) {
const fixture = createGuildBetrayalFixture({ payer, nexusFace: 'choam' });
let game = structuredClone(fixture.game);
for (const player of game.players) {
const own = viewGame(game, player.id);
own.players.find(seat => seat.id === player.id)!.bot = profile;
const reaction = own.guildBetrayalReaction;
if (!reaction) break;
const actions = botActions(own);
if (!reaction.canPass || reaction.hasPassed) {
assert.deepEqual(actions, []);
continue;
}
assert.deepEqual(actions, [{ type: 'guildBetrayalPass', event: fixture.event }]);
game = applyAction(game, player.id, actions[0]);
const after = viewGame(game, player.id);
after.players.find(seat => seat.id === player.id)!.bot = profile;
if (after.guildBetrayalReaction) assert.deepEqual(botActions(after), []);
}
assert.equal(viewGame(game, fixture.holder).guildBetrayalReaction, null);
assert.deepEqual(game.players.map(player => ({ id: player.id, spice: player.spice, forces: player.forces, reserves: player.reserves, shipped: player.shipped })),
fixture.original.players.map(player => ({ id: player.id, spice: player.spice, forces: player.forces, reserves: player.reserves, shipped: player.shipped })));
assert.equal(game.nexusCards?.cards?.hands[fixture.holder], 'choam');
}
}
});
void test('seat-private blocked and response ownership gates override visible held-card hints for all profiles', () => {
for (const profile of DIFFICULTIES) {
const fixture = createGuildBetrayalFixture();
const own = viewGame(fixture.game, fixture.holder);
own.players.find(player => player.id === fixture.holder)!.bot = profile;
assert.equal(own.nexusCards?.card, 'guild');
const reaction = own.guildBetrayalReaction!;
reaction.blocked = 'The committed promise prevents spending this Nexus card.';
assert.deepEqual(botActions(own), [{ type: 'guildBetrayalPass', event: fixture.event }]);
reaction.blocked = null;
reaction.canUse = false;
assert.deepEqual(botActions(own), [{ type: 'guildBetrayalPass', event: fixture.event }]);
reaction.canUse = true;
reaction.hasPassed = true;
assert.deepEqual(botActions(own), []);
reaction.hasPassed = false;
reaction.canPass = false;
assert.deepEqual(botActions(own), []);
reaction.canPass = true;
own.status = 'finished';
assert.deepEqual(botActions(own), []);
own.status = 'playing';
reaction.event = '';
assert.deepEqual(botActions(own), []);
}
});
void test('human seats require delegation while autopilot obeys the same private response offer', () => {
const fixture = createGuildBetrayalFixture({ payer: 'holder' });
const own = viewGame(fixture.game, fixture.holder);
const seat = own.players.find(player => player.id === fixture.holder)!;
delete seat.bot;
delete seat.autopilot;
assert.deepEqual(botActions(own), []);
seat.autopilot = 'Easy';
const actions = botActions(own);
assert.deepEqual(actions, [{ type: 'guildBetrayalUse', event: fixture.event }]);
const result = applyAction(fixture.game, fixture.holder, actions[0]);
assert.equal(result.nexusCards?.cards?.hands[fixture.holder], null);
assert.equal(result.players.find(player => player.id === fixture.holder)!.spice,
fixture.game.players.find(player => player.id === fixture.holder)!.spice);
});