178 lines
10 KiB
TypeScript
178 lines
10 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 { splitLocation } from '../game/board';
|
|
import { advanceToForecast, createAdvancedSourceFixture, settleAdvancedSourceResponses } from './fixture-advanced-source';
|
|
|
|
void test('all four policies dial the actual first Storm then confirm the recorded result exactly once', () => {
|
|
for (const profile of DIFFICULTIES) {
|
|
const fixture = createAdvancedSourceFixture({ storm: 'first', fremen: false });
|
|
let game = fixture.game;
|
|
for (const dialer of fixture.game.stormDialers) {
|
|
const own = viewGame(game, dialer);
|
|
own.players.find(player => player.id === dialer)!.bot = profile;
|
|
const actions = botActions(own);
|
|
assert.equal(actions[0]?.type, 'stormDial');
|
|
assert.ok(Number.isInteger(actions[0].amount));
|
|
assert.ok(Number(actions[0].amount) >= 0 && Number(actions[0].amount) <= 20);
|
|
game = applyAction(game, dialer, actions[0]);
|
|
const submitted = viewGame(game, dialer);
|
|
submitted.players.find(player => player.id === dialer)!.bot = profile;
|
|
if (game.stormPending === null) assert.deepEqual(botActions(submitted), []);
|
|
}
|
|
assert.notEqual(game.stormPending, null);
|
|
const actor = game.players[0].id;
|
|
const own = viewGame(game, actor);
|
|
own.players.find(player => player.id === actor)!.bot = profile;
|
|
assert.deepEqual(botActions(own), [{ type: 'ready' }]);
|
|
game = applyAction(game, actor, botActions(own)[0]);
|
|
const confirmed = viewGame(game, actor);
|
|
confirmed.players.find(player => player.id === actor)!.bot = profile;
|
|
assert.deepEqual(botActions(confirmed), []);
|
|
}
|
|
});
|
|
|
|
void test('all four policies confirm subsequent Advanced public cards without a Fremen seat or phantom forecast', () => {
|
|
for (const profile of DIFFICULTIES) {
|
|
const fixture = createAdvancedSourceFixture({ storm: 'next', fremen: false });
|
|
let game = fixture.game;
|
|
assert.deepEqual(game.stormDialers, []);
|
|
assert.ok(Number(game.stormPending) >= 1 && Number(game.stormPending) <= 6);
|
|
const distance = game.stormPending;
|
|
for (const player of fixture.game.players) {
|
|
const own = viewGame(game, player.id);
|
|
own.players.find(seat => seat.id === player.id)!.bot = profile;
|
|
assert.equal(own.stormForecast, null);
|
|
assert.deepEqual(botActions(own), [{ type: 'ready' }]);
|
|
game = applyAction(game, player.id, botActions(own)[0]);
|
|
}
|
|
game = settleAdvancedSourceResponses(game);
|
|
assert.notEqual(game.phase, 0);
|
|
assert.equal(game.storm, ((fixture.game.storm - 1 + Number(distance)) % 18) + 1);
|
|
assert.equal(game.stormCard, null);
|
|
}
|
|
});
|
|
|
|
void test('all four policies finish actual later Basic dial protocols without drawing a card', () => {
|
|
for (const profile of DIFFICULTIES) {
|
|
const fixture = createAdvancedSourceFixture({ advanced: false, storm: 'next', fremen: false });
|
|
let game = fixture.game;
|
|
const amounts: number[] = [];
|
|
for (const dialer of fixture.game.stormDialers) {
|
|
const own = viewGame(game, dialer);
|
|
own.players.find(player => player.id === dialer)!.bot = profile;
|
|
const actions = botActions(own);
|
|
assert.equal(actions[0]?.type, 'stormDial');
|
|
const amount = Number(actions[0].amount);
|
|
assert.ok(Number.isInteger(amount) && amount >= 1 && amount <= 3);
|
|
amounts.push(amount);
|
|
game = applyAction(game, dialer, actions[0]);
|
|
}
|
|
assert.equal(game.stormPending, amounts.reduce((sum, amount) => sum + amount, 0));
|
|
assert.equal(game.stormMovementSource?.kind, 'dials');
|
|
assert.equal(game.stormCard, null);
|
|
const own = viewGame(game, game.players[0].id);
|
|
own.players[0].bot = profile;
|
|
assert.deepEqual(botActions(own), [{ type: 'ready' }]);
|
|
game = applyAction(game, own.me, botActions(own)[0]);
|
|
assert.ok(game.ready.includes(own.me));
|
|
}
|
|
});
|
|
|
|
void test('every legal policy retains a real end action when finishing destroys its own allied group', () => {
|
|
for (const profile of DIFFICULTIES) {
|
|
const fixture = createAdvancedSourceFixture({ alliance: 'newThisTurn' });
|
|
const own = viewGame(fixture.game, fixture.actor);
|
|
own.players.find(player => player.id === fixture.actor)!.bot = profile;
|
|
assert.ok(own.advancedAllySeparation?.territories.length);
|
|
const end = botActions(own).find(action => action.type === 'endMovement');
|
|
assert.ok(end, `${profile} must not deadlock on a mandatory consequence`);
|
|
const before = fixture.game.players.find(player => player.id === fixture.actor)!;
|
|
const allyBefore = fixture.game.players.find(player => player.id === fixture.ally)!;
|
|
const losses = Object.entries(before.forces).filter(([key]) => fixture.lossTerritories.includes(splitLocation(key).territory))
|
|
.reduce((sum, [, amount]) => sum + amount, 0);
|
|
const eliteLosses = Object.entries(before.elites?.forces ?? {}).filter(([key]) => fixture.lossTerritories.includes(splitLocation(key).territory))
|
|
.reduce((sum, [, amount]) => sum + amount, 0);
|
|
const result = applyAction(fixture.game, fixture.actor, end);
|
|
const after = result.players.find(player => player.id === fixture.actor)!;
|
|
assert.equal(after.tanks, before.tanks + losses);
|
|
assert.equal(after.elites?.tanks ?? 0, (before.elites?.tanks ?? 0) + eliteLosses);
|
|
assert.equal(after.reserves, before.reserves);
|
|
assert.equal(after.elites?.reserves, before.elites?.reserves);
|
|
assert.deepEqual(after.hand, before.hand);
|
|
for (const id of fixture.lossTerritories) {
|
|
assert.equal(Object.entries(after.forces).filter(([key]) => key.startsWith(`${id}:`)).reduce((sum, [, amount]) => sum + amount, 0), 0);
|
|
}
|
|
assert.deepEqual(result.players.find(player => player.id === fixture.ally)!.forces, allyBefore.forces);
|
|
const allyAfter = result.players.find(player => player.id === fixture.ally)!;
|
|
assert.equal(allyAfter.reserves, allyBefore.reserves);
|
|
assert.equal(allyAfter.tanks, allyBefore.tanks);
|
|
assert.deepEqual(allyAfter.elites, allyBefore.elites);
|
|
assert.deepEqual(allyAfter.advisors, allyBefore.advisors);
|
|
assert.deepEqual(allyAfter.hand, allyBefore.hand);
|
|
assert.notEqual(result.active, fixture.actor);
|
|
assert.equal(viewGame(result, fixture.actor).advancedAllySeparation, null);
|
|
}
|
|
});
|
|
|
|
void test('all four policies can finish with mandatory losses after their actual ordinary movement is exhausted', () => {
|
|
for (const profile of DIFFICULTIES) {
|
|
const fixture = createAdvancedSourceFixture({ alliance: 'newThisTurn' });
|
|
const own = viewGame(fixture.game, fixture.actor);
|
|
const actor = own.players.find(player => player.id === fixture.actor)!;
|
|
actor.bot = profile;
|
|
const move = botActions(own).find(action => action.type === 'move' &&
|
|
typeof action.from === 'string' && fixture.lossTerritories.includes(splitLocation(action.from).territory) &&
|
|
(actor.forces[String(action.from)] ?? 0) > 1);
|
|
assert.ok(move, `${profile} has an actual legal movement from its shared group`);
|
|
const game = settleAdvancedSourceResponses(applyAction(fixture.game, fixture.actor,
|
|
{ ...move, amount: 1, elite: 0 }));
|
|
const exhausted = viewGame(game, fixture.actor);
|
|
const afterMove = exhausted.players.find(player => player.id === fixture.actor)!;
|
|
afterMove.bot = profile;
|
|
assert.equal(afterMove.moved, afterMove.movesAllowed);
|
|
assert.ok(exhausted.advancedAllySeparation?.territories.length);
|
|
const end = botActions(exhausted).find(action => action.type === 'endMovement');
|
|
assert.ok(end, `${profile} retains forced legal end instead of an invalid-movement loop`);
|
|
const finished = applyAction(game, fixture.actor, end);
|
|
assert.ok(finished.players.find(player => player.id === fixture.actor)!.tanks > afterMove.tanks);
|
|
assert.notEqual(finished.active, fixture.actor);
|
|
}
|
|
});
|
|
|
|
void test('native higher-priority forecast response cannot fall through to Storm confirmation or movement for any profile', () => {
|
|
for (const profile of DIFFICULTIES) {
|
|
const fixture = createAdvancedSourceFixture({ storm: 'first', fremen: true, karama: true });
|
|
const pending = advanceToForecast(fixture.game);
|
|
assert.equal(pending.response?.kind, 'stormPeek');
|
|
const counter = pending.players.find(player => player.faction !== 'fremen' && player.hand.some(card => card.effect === 'karama'));
|
|
assert.ok(counter, 'the native deal supplies a genuinely eligible forecast counter');
|
|
const card = counter.hand.find(card => card.effect === 'karama')!;
|
|
assert.ok(viewGame(pending, counter.id).responseControls?.cancelCards.includes(card.id));
|
|
const forces = pending.players.map(player => ({id: player.id, forces: player.forces,
|
|
reserves: player.reserves, tanks: player.tanks, elites: player.elites ?? null, advisors: player.advisors ?? null}));
|
|
for (const player of pending.players) {
|
|
const own = viewGame(pending, player.id);
|
|
own.players.find(seat => seat.id === player.id)!.bot = profile;
|
|
assert.equal(own.stormForecast, null);
|
|
for (const action of botActions(own)) {
|
|
assert.ok(action.type === 'passResponse' || (action.type === 'card' && action.mode === 'cancel'));
|
|
const result = applyAction(pending, player.id, action);
|
|
assert.equal(result.phase, pending.phase);
|
|
assert.equal(result.storm, pending.storm);
|
|
assert.deepEqual(result.players.map(player => ({id: player.id, forces: player.forces,
|
|
reserves: player.reserves, tanks: player.tanks, elites: player.elites ?? null, advisors: player.advisors ?? null})), forces);
|
|
for (const seat of result.players) {
|
|
const before = pending.players.find(before => before.id === seat.id)!;
|
|
assert.deepEqual(seat.hand, before.hand.filter(held =>
|
|
!(action.type === 'card' && seat.id === player.id && held.id === action.card)));
|
|
}
|
|
if (action.type === 'card') assert.equal(result.discard.filter(discarded => discarded.id === action.card).length, 1);
|
|
else assert.deepEqual(result.discard, pending.discard);
|
|
}
|
|
}
|
|
}
|
|
});
|