Implement Tleilaxu and Ixian Nexus Cunning with durable private recovery
This commit is contained in:
1 parent
d5830d260c
commit
40912498b1
32 files changed
+2761
-24
No files matched your search
@@ -0,0 +1,132 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createGame, newPlayer, viewGame, type GameView } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { nexusSuboidBotActions } from '../game/nexus-suboid-options';
|
||||
import { casualtyOptions } from '../game/combat';
|
||||
|
||||
/** Authoritative projections isolate bot consumers; engine tests prove actual
|
||||
* card spending and changes to physical battle strength. */
|
||||
function fixture(): GameView {
|
||||
const g = createGame('SUBOIDBOTS', newPlayer('i', 'Ixians', 'ixians'), true);
|
||||
g.players.push(
|
||||
newPlayer('h', 'Harkonnen', 'harkonnen'),
|
||||
newPlayer('a', 'Atreides', 'atreides'),
|
||||
);
|
||||
Object.assign(g, {
|
||||
status: 'playing',
|
||||
phase: 6,
|
||||
turn: 2,
|
||||
active: 'i',
|
||||
storm: 18,
|
||||
order: ['i', 'h', 'a'],
|
||||
});
|
||||
for (const player of g.players.slice(0, 2)) {
|
||||
player.forces = { 'arrakeen:10': 5 };
|
||||
player.reserves = 15;
|
||||
player.spice = 10;
|
||||
}
|
||||
g.players[0].elites!.forces = { 'arrakeen:10': 2 };
|
||||
g.players[0].elites!.reserves = 5;
|
||||
g.battle = {
|
||||
event: 'suboid-battle',
|
||||
territory: 'arrakeen',
|
||||
attacker: 'i',
|
||||
defender: 'h',
|
||||
prepared: true,
|
||||
plans: {},
|
||||
revealed: false,
|
||||
traitorCalls: {},
|
||||
};
|
||||
const v = viewGame(g, 'i');
|
||||
v.nexusCards = {
|
||||
card: 'ixians',
|
||||
deckCount: 11,
|
||||
discardCount: 0,
|
||||
held: { i: true, h: false, a: false },
|
||||
turn: null,
|
||||
choices: [],
|
||||
waiting: [],
|
||||
};
|
||||
v.nexusSuboids = {
|
||||
offer: { event: 'suboid-event', blocked: null },
|
||||
active: false,
|
||||
};
|
||||
return v;
|
||||
}
|
||||
|
||||
void test('all profiles use only the offered native Cunning before their battle plan', () => {
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const v = fixture();
|
||||
v.players[0].bot = difficulty;
|
||||
const before = structuredClone(v);
|
||||
assert.deepEqual(botActions(v), [
|
||||
{ type: 'nexusSuboids', event: 'suboid-event' },
|
||||
]);
|
||||
assert.deepEqual(v, before);
|
||||
v.nexusSuboids!.offer!.blocked =
|
||||
'An earlier commitment would become impossible.';
|
||||
assert.deepEqual(nexusSuboidBotActions(v), []);
|
||||
v.nexusSuboids!.offer!.blocked = null;
|
||||
v.battle!.submitted = ['i'];
|
||||
assert.deepEqual(nexusSuboidBotActions(v), []);
|
||||
v.battle!.submitted = [];
|
||||
v.truthtrance = {
|
||||
stage: 'priority',
|
||||
queue: [],
|
||||
passed: [],
|
||||
question: null,
|
||||
};
|
||||
assert.deepEqual(botActions(v), [{ type: 'truthPass' }]);
|
||||
}
|
||||
});
|
||||
|
||||
void test('all profiles produce feasible boosted Suboid plans using projected support, including zero spice', () => {
|
||||
for (const difficulty of DIFFICULTIES)
|
||||
for (const spice of [0, 2]) {
|
||||
const v = fixture();
|
||||
v.players[0].bot = difficulty;
|
||||
v.players[0].spice = spice;
|
||||
v.nexusCards!.card = null;
|
||||
v.nexusSuboids = { offer: null, active: true };
|
||||
const forces = v.battle!.ownForces!;
|
||||
forces.normalFixedHalf = false;
|
||||
forces.normalFreeSupport = true;
|
||||
const before = structuredClone(v);
|
||||
const actions = botActions(v);
|
||||
assert.ok(actions.length);
|
||||
for (const action of actions) {
|
||||
assert.equal(action.type, 'battlePlan');
|
||||
assert.ok(Number(action.support) <= spice);
|
||||
assert.ok(Number(action.support) <= forces.elite);
|
||||
assert.ok(
|
||||
casualtyOptions(forces, Number(action.dial), Number(action.support))
|
||||
.length,
|
||||
);
|
||||
}
|
||||
assert.deepEqual(v, before);
|
||||
}
|
||||
});
|
||||
|
||||
void test('native Cunning policy reads no unseen hands or opposing sealed plans', () => {
|
||||
const v = fixture();
|
||||
v.players[0].bot = 'Brutal';
|
||||
for (const player of v.players)
|
||||
for (const field of ['hand', 'traitors', 'faceDancers', 'spice'])
|
||||
Object.defineProperty(player, field, {
|
||||
get() {
|
||||
throw new Error(`Private ${field} read`);
|
||||
},
|
||||
});
|
||||
Object.defineProperty(v.battle!, 'plans', {
|
||||
get() {
|
||||
throw new Error('Read sealed plans');
|
||||
},
|
||||
});
|
||||
assert.deepEqual(botActions(v), [
|
||||
{ type: 'nexusSuboids', event: 'suboid-event' },
|
||||
]);
|
||||
v.me = 'h';
|
||||
assert.deepEqual(nexusSuboidBotActions(v), []);
|
||||
});
|
||||
Reference in new issue
Block a user