Initial commit
This commit is contained in:
commit
8c709fb52f
771 files changed
+222983
No files matched your search
@@ -0,0 +1,190 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
applyAction,
|
||||
createGame,
|
||||
newPlayer,
|
||||
viewGame,
|
||||
type Game,
|
||||
type GameView,
|
||||
} from '../game/engine';
|
||||
import { baseDeck } from '../game/cards';
|
||||
import { richeseCards } from '../game/richese-cards';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES, type Difficulty } from '../game/bot-profiles';
|
||||
import type { FactionId } from '../game/catalog';
|
||||
|
||||
function fixture(ally: FactionId = 'choam', donor: FactionId = 'atreides') {
|
||||
const g = createGame('DISTRANSBOT', newPlayer('d', 'Donor', donor));
|
||||
g.players.push(
|
||||
newPlayer('a', 'Ally', ally),
|
||||
newPlayer('o', 'Other', 'fremen'),
|
||||
);
|
||||
g.status = 'playing';
|
||||
g.advanced = true;
|
||||
g.phase = 2;
|
||||
g.turn = 2;
|
||||
g.order = ['d', 'a', 'o'];
|
||||
g.players[0].ally = 'a';
|
||||
g.players[1].ally = 'd';
|
||||
for (const p of g.players) {
|
||||
p.hand = [];
|
||||
p.spice = 10;
|
||||
}
|
||||
const deck = baseDeck();
|
||||
const worthless = deck.find((c) => c.kind === 'worthless')!;
|
||||
const weapons = deck.filter((c) => c.kind === 'projectile').slice(0, 2);
|
||||
const distrans = richeseCards().find((c) => c.effect === 'distrans')!;
|
||||
g.players[0].hand = [distrans, worthless, ...weapons];
|
||||
g.deck = deck.filter(
|
||||
(c) => !g.players[0].hand.some((held) => held.id === c.id),
|
||||
);
|
||||
return g;
|
||||
}
|
||||
function projected(g: Game, difficulty: Difficulty) {
|
||||
const v = viewGame(g, 'd');
|
||||
v.players[0].bot = difficulty;
|
||||
return v;
|
||||
}
|
||||
const command = (v: GameView) =>
|
||||
botActions(v).find((a) => a.type === 'card' && a.card === 'richese-distrans');
|
||||
|
||||
void test('all four profiles give a worthless card to BG or CHOAM ally with a single synchronous transfer and discard', () => {
|
||||
for (const difficulty of DIFFICULTIES)
|
||||
for (const ally of ['beneGesserit', 'choam'] as const) {
|
||||
const g = fixture(ally);
|
||||
const v = projected(g, difficulty),
|
||||
before = structuredClone(v);
|
||||
const action = command(v);
|
||||
assert.deepEqual(v, before);
|
||||
assert.ok(action, `${difficulty} -> ${ally}`);
|
||||
assert.equal(action.target, 'a');
|
||||
assert.equal(action.give, g.players[0].hand[1].id);
|
||||
const next = applyAction(g, 'd', action);
|
||||
assert.equal(next.players[0].hand.length, 2);
|
||||
assert.deepEqual(
|
||||
next.players[1].hand.map((c) => c.id),
|
||||
[action.give],
|
||||
);
|
||||
assert.deepEqual(
|
||||
next.discard.map((c) => c.id),
|
||||
['richese-distrans'],
|
||||
);
|
||||
assert.equal(next.response, null);
|
||||
assert.equal(next.decision, null);
|
||||
assert.deepEqual(
|
||||
next.players.map((p) => p.spice),
|
||||
g.players.map((p) => p.spice),
|
||||
);
|
||||
assert.deepEqual(next.deck, g.deck);
|
||||
assert.equal(
|
||||
command(
|
||||
projected(JSON.parse(JSON.stringify(next)) as Game, difficulty),
|
||||
),
|
||||
undefined,
|
||||
);
|
||||
assert.throws(() => applyAction(next, 'd', action));
|
||||
}
|
||||
});
|
||||
|
||||
void test('a full donor can share a duplicate ordinary weapon, defense or usable Karama while retaining one', () => {
|
||||
for (const difficulty of DIFFICULTIES)
|
||||
for (const kind of [
|
||||
'projectile',
|
||||
'poison',
|
||||
'shield',
|
||||
'snooper',
|
||||
'karama',
|
||||
]) {
|
||||
const g = fixture('emperor');
|
||||
const duplicates = baseDeck()
|
||||
.filter((c) =>
|
||||
kind === 'karama' ? c.effect === 'karama' : c.kind === kind,
|
||||
)
|
||||
.slice(0, 2);
|
||||
assert.equal(duplicates.length, 2);
|
||||
const filler = baseDeck().find((c) => c.kind === 'worthless')!;
|
||||
g.players[0].hand = [g.players[0].hand[0], ...duplicates, filler];
|
||||
g.deck = baseDeck().filter(
|
||||
(c) => !g.players[0].hand.some((held) => held.id === c.id),
|
||||
);
|
||||
const action = command(projected(g, difficulty));
|
||||
assert.ok(action, `${difficulty} ${kind}`);
|
||||
assert.equal(action.give, duplicates[0].id);
|
||||
const next = applyAction(g, 'd', action);
|
||||
assert.ok(next.players[0].hand.some((c) => c.id === duplicates[1].id));
|
||||
assert.equal(next.players[1].hand[0].id, duplicates[0].id);
|
||||
}
|
||||
});
|
||||
|
||||
void test('policy does not infer recipient contents and never targets an opponent or unavailable projected choice', () => {
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const v = projected(fixture(), difficulty);
|
||||
assert.equal(v.players[1].hand, undefined);
|
||||
assert.equal(v.players[1].handCount, undefined);
|
||||
const action = command(v);
|
||||
v.players[1].hand = richeseCards();
|
||||
v.players[1].spice = 999;
|
||||
assert.deepEqual(command(v), action);
|
||||
for (const change of [
|
||||
(view: GameView) => {
|
||||
view.distrans!.blocked = 'During a bid.';
|
||||
},
|
||||
(view: GameView) => {
|
||||
view.distrans!.choices.find((c) => c.recipient === 'a')!.blocked =
|
||||
'Full.';
|
||||
},
|
||||
(view: GameView) => {
|
||||
view.distrans!.choices.find((c) => c.recipient === 'a')!.cards = [];
|
||||
},
|
||||
(view: GameView) => {
|
||||
view.players[0].ally = null;
|
||||
},
|
||||
(view: GameView) => {
|
||||
view.distrans = null;
|
||||
},
|
||||
]) {
|
||||
const blocked = projected(fixture(), difficulty);
|
||||
change(blocked);
|
||||
assert.equal(command(blocked), undefined);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
void test('donors retain their own valuable worthless power and do not spend Distrans for a sole ordinary card or unsupported effect', () => {
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
for (const donor of ['beneGesserit', 'choam'] as const) {
|
||||
const g = fixture(donor === 'choam' ? 'beneGesserit' : 'choam', donor);
|
||||
g.players[0].hand = g.players[0].hand.slice(0, 2);
|
||||
assert.equal(command(projected(g, difficulty)), undefined);
|
||||
}
|
||||
const g = fixture('emperor');
|
||||
g.players[0].hand.pop();
|
||||
assert.equal(command(projected(g, difficulty)), undefined);
|
||||
g.players[0].hand = [
|
||||
g.players[0].hand[0],
|
||||
...richeseCards()
|
||||
.filter((c) => c.effect !== 'distrans' && c.effect !== 'karama')
|
||||
.slice(0, 3),
|
||||
];
|
||||
assert.equal(command(projected(g, difficulty)), undefined);
|
||||
}
|
||||
});
|
||||
|
||||
void test('Truthtrance, response and decision priority precede optional Distrans for every profile', () => {
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const truth = projected(fixture(), difficulty);
|
||||
truth.truthtrance = {
|
||||
stage: 'priority',
|
||||
queue: [],
|
||||
passed: [],
|
||||
} as unknown as NonNullable<GameView['truthtrance']>;
|
||||
assert.equal(botActions(truth)[0].type, 'truthPass');
|
||||
const decision = projected(fixture(), difficulty);
|
||||
decision.decision = { kind: 'richeseDeclaration', player: 'a' };
|
||||
assert.deepEqual(botActions(decision), []);
|
||||
const response = projected(fixture(), difficulty);
|
||||
response.response = { kind: 'emperorIncome', owner: 'o', passed: [] };
|
||||
assert.equal(command(response), undefined);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user