Initial commit

This commit is contained in:
Quintine committed 2026-09-08 07:37:52 +10:00
commit 8c709fb52f
771 files changed
+222983

No files matched your search

+215
View File
@@ -0,0 +1,215 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
applyAction,
createGame,
joinGame,
newPlayer,
viewGame,
type Action,
type Game,
} from '../game/engine';
import { botActions, runBots } from '../game/bots';
import { DIFFICULTIES, type Difficulty } from '../game/bot-profiles';
import { baseDeck } from '../game/cards';
function lobby() {
const g = createGame('AUTOPIL', newPlayer('a', 'Owner', 'atreides'));
joinGame(g, newPlayer('e', 'Guest', 'emperor'));
return g;
}
function playing() {
const g = lobby();
g.status = 'playing';
g.phase = 4;
g.order = ['a', 'e'];
g.players.forEach((p) => {
p.traitors = [p.leaders[0].id];
p.traitorChoices = [];
p.hand = [];
});
return applyAction(g, 'a', { type: 'advanceBots' });
}
function control(g: Game, difficulty: Difficulty | null, id = 'a') {
return applyAction(g, id, { type: 'setAutopilot', difficulty });
}
function gameplay(g: Game) {
const copy = structuredClone(g);
copy.log = [];
for (const p of copy.players) delete p.autopilot;
return copy;
}
function rejectsUnchanged(g: Game, id: string, action: Action) {
const before = structuredClone(g);
assert.throws(() => applyAction(g, id, action));
assert.deepEqual(g, before);
}
void test('human owners can enable, change and disable all policies without becoming native bots', () => {
let g = playing();
const original = structuredClone(g);
for (const difficulty of DIFFICULTIES) {
const before = structuredClone(g);
g = control(g, difficulty);
assert.deepEqual(gameplay(g), gameplay(before));
assert.equal(g.players[0].autopilot, difficulty);
assert.equal(g.players[0].bot, undefined);
assert.equal(g.players[1].autopilot, undefined);
assert.ok(g.log.length > before.log.length);
}
g = control(g, null);
assert.equal(g.players[0].autopilot, undefined);
assert.deepEqual(gameplay(g), gameplay(original));
assert.equal(original.players[0].autopilot, undefined);
g = control(g, 'Medium', 'e');
assert.equal(g.players[1].autopilot, 'Medium');
assert.equal(g.players[0].autopilot, undefined);
assert.deepEqual(control(g, 'Medium', 'e'), g);
assert.deepEqual(gameplay(g), gameplay(original));
});
void test('control rejects other-seat targets, invalid difficulty, native bots and inactive games atomically', () => {
const g = playing();
for (const target of ['e', 'a', null])
rejectsUnchanged(g, 'a', {
type: 'setAutopilot',
target,
difficulty: 'Hard',
});
for (const difficulty of ['Cheat', 'hard', '', 1, undefined])
rejectsUnchanged(g, 'a', { type: 'setAutopilot', difficulty });
rejectsUnchanged(g, 'absent', { type: 'setAutopilot', difficulty: 'Hard' });
for (const status of ['lobby', 'finished'] as const) {
const inactive = structuredClone(g);
inactive.status = status;
rejectsUnchanged(inactive, 'a', {
type: 'setAutopilot',
difficulty: 'Hard',
});
}
g.players[0].bot = 'Easy';
for (const difficulty of ['Hard', null])
rejectsUnchanged(g, 'a', { type: 'setAutopilot', difficulty });
});
void test('setup autopilot selects only its own traitor, then stops for the remaining human', () => {
let g = lobby();
g.players.forEach((p) => (p.ready = true));
g = applyAction(g, 'a', { type: 'start' });
const guestChoices = structuredClone(g.players[1].traitorChoices);
const ownChoices = structuredClone(g.players[0].traitorChoices);
const declared = control(g, 'Hard');
assert.deepEqual(gameplay(declared), gameplay(g));
g = runBots(declared, 8);
assert.equal(g.status, 'setup');
assert.equal(g.players[0].traitors.length, 1);
assert.ok(ownChoices.includes(g.players[0].traitors[0]));
assert.deepEqual(g.players[1].traitorChoices, guestChoices);
assert.deepEqual(g.players[1].traitors, []);
assert.equal(g.players[0].bot, undefined);
assert.equal(g.botsPending, false);
});
void test('retaking control preserves a sealed storm dial and every pending gameplay field', () => {
let g = playing();
g.phase = 0;
g.stormDialers = ['a', 'e'];
g.lastBattle = ['a', 'e'];
g.stormDials = { a: 17 };
g = control(g, 'Brutal');
const before = structuredClone(g);
g = control(JSON.parse(JSON.stringify(g)), null);
assert.deepEqual(
JSON.parse(JSON.stringify(gameplay(g))),
JSON.parse(JSON.stringify(gameplay(before))),
);
assert.deepEqual(g.stormDials, { a: 17 });
assert.equal(viewGame(g, 'e').stormRevealed, null);
assert.deepEqual(botActions(viewGame(g, 'a')), []);
const stopped = runBots(g, 4);
assert.deepEqual(stopped.stormDials, { a: 17 });
assert.equal(stopped.botsPending, false);
});
void test('owner can retake control through Truthtrance, phase opening, response and private decision locks', () => {
const windows: ((g: Game) => void)[] = [
(g) => {
g.truthtrance = {
stage: 'priority',
queue: [],
passed: [],
question: null,
};
},
(g) => {
g.phaseOpening = { passed: ['a'], initialize: false };
},
(g) => {
g.response = {
kind: 'revivalIncome',
owner: 'e',
recipient: 'a',
amount: 2,
passed: [],
};
},
(g) => {
g.decision = { kind: 'handExchange', player: 'a', target: 'e', count: 1 };
},
];
for (const install of windows) {
let g = control(playing(), 'Medium');
install(g);
const before = structuredClone(g);
g = control(g, null);
assert.deepEqual(gameplay(g), gameplay(before));
assert.equal(g.players[0].autopilot, undefined);
}
});
void test('autopilot difficulty and own private view produce the same actions as the native policy', () => {
for (const difficulty of DIFFICULTIES) {
let g = playing();
g.phase = 3;
g.auction = {
cards: [baseDeck()[0]],
index: 0,
bid: 4,
bidder: 'e',
active: 'a',
passed: [],
peekKnown: false,
opener: 0,
};
g.players[0].spice = 20;
g = control(g, difficulty);
const automatic = botActions(viewGame(g, 'a'));
assert.ok(automatic.length > 0, difficulty);
const native = structuredClone(g);
native.players[0].bot = difficulty;
delete native.players[0].autopilot;
assert.deepEqual(automatic, botActions(viewGame(native, 'a')), difficulty);
g.players[1].hand = baseDeck().slice(3, 7);
g.players[1].traitors = ['hidden-other-choice'];
g.deck.reverse();
assert.deepEqual(botActions(viewGame(g, 'a')), automatic, difficulty);
}
});
void test('public control mode survives JSON without disclosing another owner’s hand or traitors', () => {
let g = playing();
g.players[0].hand = [baseDeck()[0]];
g = control(g, 'Hard');
g = JSON.parse(JSON.stringify(g));
const own = viewGame(g, 'a');
const other = viewGame(g, 'e');
assert.equal(own.players[0].autopilot, 'Hard');
assert.equal(other.players[0].autopilot, 'Hard');
assert.equal(other.players[0].bot, undefined);
assert.equal(other.players[0].hand, undefined);
assert.equal(other.players[0].traitors, undefined);
assert.deepEqual(own.players[0].hand, g.players[0].hand);
assert.deepEqual(own.players[0].traitors, g.players[0].traitors);
assert.equal(own.me, 'a');
assert.equal(other.me, 'e');
});