115 lines
3.6 KiB
TypeScript
115 lines
3.6 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 } from '../game/bot-profiles';
|
|
import { territory } from '../game/board';
|
|
|
|
function fixture(fremen = false) {
|
|
const g = createGame(
|
|
'TRANSPORT',
|
|
newPlayer('p', 'Mover', fremen ? 'fremen' : 'guild'),
|
|
fremen,
|
|
);
|
|
g.players.push(newPlayer('q', 'Ally', fremen ? 'guild' : 'emperor'));
|
|
Object.assign(g, {
|
|
status: 'playing',
|
|
phase: 5,
|
|
turn: 2,
|
|
active: 'p',
|
|
order: ['p', 'q'],
|
|
movementRemaining: ['p', 'q'],
|
|
storm: 18,
|
|
});
|
|
for (const p of g.players) {
|
|
p.forces = {};
|
|
p.hand = [];
|
|
p.spice = 0;
|
|
}
|
|
if (fremen) {
|
|
g.players[0].ally = 'q';
|
|
g.players[1].ally = 'p';
|
|
} else {
|
|
g.players[0].forces = { 'red_chasm:7': 5 };
|
|
g.players[0].reserves = 15;
|
|
}
|
|
return g;
|
|
}
|
|
const transports = (g: Game) =>
|
|
botActions(viewGame(g, 'p')).filter((a) => a.type === 'guildShip');
|
|
const price = (to: unknown, n: unknown) =>
|
|
Math.ceil(
|
|
(Number(n) * (territory(String(to)).type === 'stronghold' ? 1 : 2)) / 2,
|
|
);
|
|
|
|
void test('Guild transport candidates use rounded total cost at every budget and preserve affordable target order', () => {
|
|
for (const level of DIFFICULTIES) {
|
|
const g = fixture();
|
|
g.players[0].bot = level;
|
|
g.players[0].spice = 1000;
|
|
const rich = transports(g);
|
|
assert.ok(rich.length);
|
|
for (const budget of [0, 1, 2, 3, 4, 5, 8]) {
|
|
g.players[0].spice = budget;
|
|
const actual = transports(g);
|
|
assert.deepEqual(
|
|
actual,
|
|
rich.filter((a) => price(a.territory, a.amount) <= budget),
|
|
);
|
|
for (const a of actual) {
|
|
const result = applyAction(g, 'p', a);
|
|
assert.equal(
|
|
result.players[0].spice,
|
|
budget - price(a.territory, a.amount),
|
|
);
|
|
assert.equal(result.players[0].shipped, true);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
void test('allied Fremen paid cross-shipping consumes the explicit shared budget and remains distinct from free reinforcements', () => {
|
|
for (const level of DIFFICULTIES) {
|
|
const g = fixture(true);
|
|
g.players[0].bot = level;
|
|
assert.deepEqual(transports(g), []);
|
|
g.players[1].spice = 3;
|
|
g.aid.q = { recipient: 'p', amount: 3 };
|
|
const offered = transports(g);
|
|
assert.ok(offered.length);
|
|
for (const a of offered) {
|
|
const cost = price(a.territory, a.amount);
|
|
assert.ok(cost > 0 && cost <= 3);
|
|
const result = applyAction(g, 'p', a);
|
|
assert.equal(result.players[0].spice, 0);
|
|
// The pledge is already escrowed; spend it without charging the donor twice.
|
|
assert.equal(result.players[1].spice, 3);
|
|
assert.equal(result.aid.q.amount, 3 - cost);
|
|
}
|
|
const free = botActions(viewGame(g, 'p')).filter((a) => a.type === 'ship');
|
|
assert.ok(free.length);
|
|
for (const a of free)
|
|
assert.equal(applyAction(g, 'p', a).players[1].spice, 3);
|
|
g.aid.q = { recipient: 'q', amount: 3 };
|
|
assert.deepEqual(transports(g), []);
|
|
}
|
|
});
|
|
void test('changing unpledged opposing resources and private cards never changes transport offers', () => {
|
|
for (const level of DIFFICULTIES) {
|
|
const g = fixture(true);
|
|
g.players[0].bot = level;
|
|
g.players[0].spice = 4;
|
|
const before = transports(g),
|
|
snapshot = structuredClone(g);
|
|
g.players[1].spice = 99999;
|
|
g.players[1].hand = [{ id: 'hidden', name: 'Hidden', kind: 'lasgun' }];
|
|
assert.deepEqual(transports(g), before);
|
|
assert.deepEqual(snapshot.players[0], g.players[0]);
|
|
}
|
|
});
|