Files
Dune/tests/two-player-bot-engine.test.ts

138 lines
7.6 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
applyAction, createGame, joinControlledFaction, newPlayer, viewGame, type Game,
} from '../game/engine';
import { runBots } from '../game/bots';
/** One real human controls two separately authenticated faction seats. */
function humanPair(): Game {
let g = createGame('TWOBOT01', newPlayer('human', 'Human', 'atreides'));
g = applyAction(g, 'human', { type: 'twoPlayerMode', enabled: true });
joinControlledFaction(g, newPlayer('second', 'Human second', 'harkonnen'), 'human');
return g;
}
function botPrimary(g: Game): { game: Game; id: string } {
const game = applyAction(g, 'human', { type: 'addBot', faction: 'emperor', difficulty: 'Medium' });
const bot = game.players.find(player => player.faction === 'emperor')!;
assert.ok(bot.bot);
return { game, id: bot.id };
}
function botPair(g: Game): { game: Game; primary: string; secondary: string } {
const first = botPrimary(g);
const game = applyAction(first.game, 'human', {
type: 'addControlledBot', controller: first.id, faction: 'guild', difficulty: 'Medium',
});
const secondary = game.players.find(player => player.faction === 'guild')!;
assert.ok(secondary.bot);
return { game, primary: first.id, secondary: secondary.id };
}
function unchanged(g: Game, action: () => unknown) {
const before = structuredClone(g);
assert.throws(action);
assert.deepEqual(g, before);
}
void test('a preexisting bot remains ready when enabling the two-controller lobby', () => {
let g = createGame('TWOBOT02', newPlayer('human', 'Human', 'atreides'));
g = applyAction(g, 'human', { type: 'addBot', faction: 'emperor', difficulty: 'Easy' });
const bot = g.players.find(player => player.bot)!;
assert.equal(bot.ready, true);
g = applyAction(g, 'human', { type: 'twoPlayerMode', enabled: true });
assert.equal(g.players.find(player => player.id === bot.id)!.ready, true);
assert.equal(g.players.find(player => player.id === 'human')!.ready, false);
assert.deepEqual(g.twoPlayerControllers!.controllers.map(controller => controller.seats), [['human'], [bot.id]]);
g = applyAction(g, 'human', { type: 'addControlledBot', controller: bot.id, faction: 'guild', difficulty: 'Easy' });
assert.equal(g.twoPlayerControllers!.controllers.find(controller => controller.id === bot.id)!.seats.length, 2);
});
void test('a bot controller gets two independent faction seats without giving the human their private views', () => {
const { game: g, primary, secondary } = botPair(humanPair());
assert.deepEqual(g.twoPlayerControllers!.controllers.map(controller => controller.seats), [
['human', 'second'], [primary, secondary],
]);
assert.deepEqual(viewGame(g, 'human').controlledFactionSeats.map(seat => seat.id), ['human', 'second']);
assert.deepEqual(viewGame(g, primary).controlledFactionSeats.map(seat => seat.id), [primary, secondary]);
for (const observer of ['human', 'second', primary, secondary])
for (const seat of viewGame(g, observer).players)
assert.equal('hand' in seat, seat.id === observer);
assert.equal(g.players.find(player => player.id === primary)!.ready, true);
assert.equal(g.players.find(player => player.id === secondary)!.ready, true);
});
void test('only the host can create bots, and bot and human controllers cannot be mixed', () => {
const initial = humanPair();
unchanged(initial, () => applyAction(initial, 'second', {
type: 'addBot', faction: 'emperor', difficulty: 'Easy',
}));
const { game, id } = botPrimary(initial);
unchanged(game, () => applyAction(game, 'second', {
type: 'addControlledBot', controller: id, faction: 'guild', difficulty: 'Easy',
}));
unchanged(game, () => applyAction(game, 'human', {
type: 'addControlledBot', controller: 'human', faction: 'guild', difficulty: 'Easy',
}));
unchanged(game, () => joinControlledFaction(game, newPlayer('forged', 'Forged', 'guild'), id));
unchanged(game, () => applyAction(game, 'human', {
type: 'addControlledBot', controller: id, faction: 'atreides', difficulty: 'Easy',
}));
unchanged(game, () => applyAction(game, 'human', {
type: 'addControlledBot', controller: id, faction: 'guild', difficulty: 'Impossible',
}));
});
void test('removing either bot updates its controller and promotes a surviving secondary', () => {
const { game, primary, secondary } = botPair(humanPair());
let g = applyAction(game, 'human', { type: 'removeBot', target: secondary });
assert.deepEqual(g.twoPlayerControllers!.controllers.find(controller => controller.id === primary)!.seats, [primary]);
assert.equal(g.players.some(player => player.id === secondary), false);
g = applyAction(g, 'human', { type: 'addControlledBot', controller: primary, faction: 'guild', difficulty: 'Easy' });
const replacement = g.players.find(player => player.faction === 'guild')!.id;
g = applyAction(g, 'human', { type: 'removeBot', target: primary });
assert.equal(g.players.some(player => player.id === primary), false);
assert.deepEqual(g.twoPlayerControllers!.controllers.find(controller => controller.id === replacement)!.seats, [replacement]);
g = applyAction(g, 'human', { type: 'addControlledBot', controller: replacement, faction: 'emperor', difficulty: 'Hard' });
assert.equal(g.twoPlayerControllers!.controllers.find(controller => controller.id === replacement)!.seats.length, 2);
assert.equal(viewGame(g, 'human').controlledFactionSeats.length, 2);
});
void test('per-seat bot configuration and human readiness remain usable in the paired lobby', () => {
const { game, primary, secondary } = botPair(humanPair());
const position = game.playerPositions![primary];
let g = applyAction(game, 'human', { type: 'ready' });
g = applyAction(g, 'second', { type: 'ready' });
g = applyAction(g, 'human', { type: 'configureBot', target: primary, faction: 'emperor', difficulty: 'Hard', position });
assert.equal(g.players.find(player => player.id === primary)!.bot, 'Hard');
assert.equal(g.players.find(player => player.id === secondary)!.bot, 'Medium');
assert.equal(g.players.find(player => player.id === 'human')!.ready, false);
assert.equal(g.players.find(player => player.id === 'second')!.ready, false);
assert.equal(g.players.find(player => player.id === primary)!.ready, true);
assert.equal(g.players.find(player => player.id === secondary)!.ready, true);
});
void test('real two-controller setup lets the scheduler make both AI faction decisions', () => {
const partial = botPrimary(humanPair());
let incomplete = applyAction(partial.game, 'human', { type: 'ready' });
incomplete = applyAction(incomplete, 'second', { type: 'ready' });
unchanged(incomplete, () => applyAction(incomplete, 'human', { type: 'start' }));
const complete = applyAction(incomplete, 'human', {
type: 'addControlledBot', controller: partial.id, faction: 'guild', difficulty: 'Medium',
});
const botIds = complete.players.filter(player => player.bot).map(player => player.id);
let g = complete;
for (const id of ['human', 'second']) g = applyAction(g, id, { type: 'ready' });
g = applyAction(g, 'human', { type: 'start' });
assert.equal(g.status, 'setup');
for (let n = 0; n < 8 && botIds.some(id => g.players.find(player => player.id === id)!.traitorChoices.length); n++)
g = runBots(g, 1);
for (const id of botIds) {
const bot = g.players.find(player => player.id === id)!;
assert.equal(bot.traitorChoices.length, 0);
assert.equal(bot.traitors.length, 1);
}
const human = g.players.find(player => player.id === 'human')!;
g = applyAction(g, 'human', { type: 'traitor', leader: human.traitorChoices[0] });
assert.equal(g.status, 'playing');
assert.equal(g.players.every(player => player.ally === null), true);
});