Files
Dune/tests/bot-worm-ride.test.ts
2026-09-08 07:37:52 +10:00

148 lines
4.8 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
applyAction,
createGame,
newPlayer,
viewGame,
type Game,
} from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES, type Difficulty } from '../game/bot-profiles';
import { gameDistance, location, territory } from '../game/board';
function fixture(advanced = false): Game {
const g = createGame(
'WORMRIDEBOT',
newPlayer('f', 'Rider', 'fremen'),
advanced,
);
g.players.push(
newPlayer('e', 'Emperor', 'emperor'),
newPlayer('h', 'Harkonnen', 'harkonnen'),
);
Object.assign(g, {
status: 'playing',
phase: 1,
turn: 2,
storm: 18,
active: 'f',
order: ['f', 'e', 'h'],
spiceSequence: { pile: 1, skipped: [] },
wormRides: [],
});
for (const p of g.players)
Object.assign(p, { hand: [], forces: {}, reserves: 20, spice: 10 });
const source = location(
'the_great_flat',
territory('the_great_flat').sectors[0],
);
g.players[0].forces = { [source]: 4 };
g.players[0].reserves = 16;
g.decision = { kind: 'wormRide', player: 'f', territory: 'the_great_flat' };
return g;
}
function rides(g: Game, difficulty: Difficulty) {
const view = viewGame(g, 'f');
view.players[0].bot = difficulty;
const before = structuredClone(view);
const actions = botActions(view);
assert.deepEqual(view, before);
assert.ok(
actions.some((a) => a.type === 'decision' && a.accept === false),
'Declining a ride remains available.',
);
for (const a of actions)
assert.doesNotThrow(
() => applyAction(g, 'f', a),
`${difficulty}: ${JSON.stringify(a)}`,
);
const accepted = actions.filter((a) => a.accept === true);
assert.ok(accepted.length > 0);
return accepted;
}
void test('all profiles exclude a highly ranked worm origin while retaining legal long-distance rides', () => {
for (const difficulty of DIFFICULTIES)
for (const advanced of [false, true]) {
const g = fixture(advanced);
for (const sector of territory('the_great_flat').sectors)
g.spice[location('the_great_flat', sector)] = 100;
const actions = rides(g, difficulty);
assert.ok(actions.every((a) => a.territory !== 'the_great_flat'));
const source = Object.keys(g.players[0].forces)[0];
assert.ok(
actions.some(
(a) =>
gameDistance(
g,
source,
location(String(a.territory), Number(a.sector)),
) > 3,
),
'Riding Shai-Hulud has no ordinary ground-movement range cap.',
);
if (difficulty !== 'Easy')
assert.ok(
actions.some((a) => a.territory === 'arrakeen'),
'The free Fremen reinforcement radius does not constrain a worm ride.',
);
}
});
void test('worm rides obey destination storm, ally and stronghold occupancy even for Advanced Fremen', () => {
for (const difficulty of DIFFICULTIES)
for (const advanced of [false, true]) {
const g = fixture(advanced);
g.storm = 10;
g.players[0].ally = 'e';
g.players[1].ally = 'f';
g.players[1].forces = { 'carthag:11': 1, 'polar_sink:0': 1 };
g.players[1].reserves = 18;
g.players[2].forces = { 'carthag:11': 1 };
g.players[2].reserves = 19;
const actions = rides(g, difficulty);
assert.ok(
actions.every((a) => a.sector !== 10 && a.territory !== 'carthag'),
);
g.spice['polar_sink:0'] = 100;
if (difficulty !== 'Easy')
assert.ok(
rides(g, difficulty).some((a) => a.territory === 'polar_sink'),
'Allied occupation does not prohibit Polar Sink.',
);
}
});
void test('worm rides select all eligible source sectors and preserve unavoidable elite forces while leaving storm forces behind', () => {
for (const difficulty of DIFFICULTIES) {
const g = fixture(true);
g.storm = 7;
g.players[0].forces = {
'pasty_mesa:5': 2,
'pasty_mesa:6': 2,
'pasty_mesa:7': 1,
};
g.players[0].reserves = 15;
g.players[0].elites = {
forces: { 'pasty_mesa:5': 1, 'pasty_mesa:7': 1 },
reserves: 1,
tanks: 0,
revived: 0,
};
g.decision = { kind: 'wormRide', player: 'f', territory: 'pasty_mesa' };
const action = rides(g, difficulty)[0];
assert.deepEqual(action.forces, { 'pasty_mesa:5': 2, 'pasty_mesa:6': 2 });
const next = applyAction(g, 'f', action);
assert.equal(next.players[0].forces['pasty_mesa:7'], 1);
assert.equal(next.players[0].elites!.forces['pasty_mesa:7'], 1);
const destination = location(
String(action.territory),
Number(action.sector),
);
assert.equal(next.players[0].forces[destination], 4);
assert.equal(next.players[0].elites!.forces[destination], 1);
assert.equal(next.players[0].reserves, 15);
}
});