268 lines
19 KiB
TypeScript
268 lines
19 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { viewGame, type Action, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import type { BribeExpression } from '../game/bribe-expression-promises';
|
|
import { enterMentatBribeWithUnpaidPromise } from './fixture-mentat-bribes';
|
|
import {
|
|
actTruthBribeExpression as act, answerTruthBribeExpression as answer,
|
|
askTruthBribeExpression as ask, bindTruthBribeExpression as bind,
|
|
completeTruthBribeExpression as complete, createTruthBribeExpressionFixture as fixture,
|
|
declareTruthBribeExpression as declare, finishTruthBribeExpression as finish,
|
|
truthBribeExpressionBalances as balances, truthBribeExpressionInventory as inventory,
|
|
truthBribeExpressionPlayer as player,
|
|
} from './fixture-truth-bribe-expression';
|
|
|
|
const payment = (recipient: string, minimum: number): BribeExpression => ({ kind: 'payment', recipient, minimum });
|
|
const group = (kind: 'and' | 'or', ...terms: BribeExpression[]): BribeExpression => ({ kind, terms });
|
|
const promise = (game: Game, target: string, index = 0) => game.bribeExpressionPromises!.filter(row => row.player === target)[index];
|
|
const progress = (game: Game, target: string, recipient: string, index = 0) =>
|
|
promise(game, target, index).progress.find(row => row.recipient === recipient)?.amount ?? 0;
|
|
const pay = (game: Game, actor: string, target: string, amount: number) => act(game, actor, { type: 'bribe', target, amount });
|
|
function rejected(game: Game, actor: string, action: Action) {
|
|
const before = structuredClone(game); assert.throws(() => act(game, actor, action)); assert.deepEqual(game, before);
|
|
}
|
|
function completePayments(game: Game, target: string): Game {
|
|
for (let count = 0; count < 8 && game.bribeExpressionPromises!.some(row => row.player === target && row.answer && !row.fulfilled); count++) {
|
|
const guide = viewGame(game, target).bribePromiseGuide; assert.ok(guide);
|
|
game = act(game, target, guide.action);
|
|
}
|
|
assert.ok(game.bribeExpressionPromises!.filter(row => row.player === target && row.answer).every(row => row.fulfilled));
|
|
return game;
|
|
}
|
|
|
|
for (const advanced of [false, true]) void test(`native ${advanced ? 'Advanced' : 'Basic'} grouped bribe AND requires both actual cumulative recipient payments before its one opening receipt`, () => {
|
|
const f = fixture({ advanced }); assert.equal(f.initial.status, 'lobby'); assert.equal(f.setup.status, 'setup');
|
|
assert.equal(f.originalCards.length, 33); assert.deepEqual(f.staging, []);
|
|
assert.equal(f.sourceActionCount, f.sourceActions.length);
|
|
assert.deepEqual(player(f.afterSetup, f.asker).hand.map(card => card.id), f.truths);
|
|
const source = structuredClone(f.game), before = balances(f.game), bound = bind(f);
|
|
assert.deepEqual(f.game, source); assert.deepEqual(promise(bound.game, f.target).progress, []);
|
|
assert.equal(bound.game.auction!.active, f.asker); assert.notEqual(bound.game.auction!.active, f.target);
|
|
let game = pay(bound.game, f.target, f.asker, 1);
|
|
assert.equal(progress(game, f.target, f.asker), 1); assert.ok(!promise(game, f.target).fulfilled);
|
|
game = pay(game, f.target, f.asker, 2);
|
|
assert.equal(progress(game, f.target, f.asker), 3); assert.equal(progress(game, f.target, f.opponent), 0);
|
|
assert.ok(!promise(game, f.target).fulfilled); assert.equal(player(game, f.target).spice, 7);
|
|
game = pay(game, f.target, f.opponent, 2);
|
|
assert.equal(progress(game, f.target, f.opponent), 2); assert.equal(promise(game, f.target).fulfilled, true);
|
|
assert.equal(player(game, f.target).spice, 5);
|
|
for (const recipient of [f.asker, f.opponent]) assert.equal(player(game, recipient).spice, before[recipient]);
|
|
assert.equal(player(game, f.asker).bribes, 3); assert.equal(player(game, f.opponent).bribes, 2);
|
|
const result = complete(f, game);
|
|
for (const [recipient, amount] of [[f.asker, 3], [f.opponent, 2]] as const) {
|
|
assert.equal(player(result.mentat.before, recipient).bribes, amount);
|
|
assert.equal(player(result.mentat.after, recipient).spice - player(result.mentat.before, recipient).spice, amount);
|
|
assert.equal(player(result.mentat.after, recipient).bribes, 0);
|
|
assert.equal(progress(result.game, f.target, recipient), amount);
|
|
}
|
|
assert.equal(result.game.turn, 2); assert.equal(result.game.phase, 0);
|
|
assert.equal(result.game.discard.filter(card => card.id === f.truth).length, 1);
|
|
assert.deepEqual(inventory(result.game), f.originalCards);
|
|
});
|
|
|
|
void test('native grouped bribe OR accepts the actual alternate recipient instead of saving its suggested route', () => {
|
|
const f = fixture(), claim = group('or', payment(f.asker, 3), payment(f.opponent, 2));
|
|
let game = bind(f, 'yes', claim).game;
|
|
const guide = viewGame(game, f.target).bribePromiseGuide; assert.ok(guide);
|
|
assert.ok(typeof guide.action.target === 'string');
|
|
const recipient = guide.action.target === f.asker ? f.opponent : f.asker;
|
|
const amount = recipient === f.asker ? 3 : 2;
|
|
assert.notEqual(recipient, guide.action.target); game = pay(game, f.target, recipient, amount);
|
|
assert.equal(promise(game, f.target).fulfilled, true); assert.equal(player(game, f.target).spice, 10 - amount);
|
|
assert.equal(progress(game, f.target, recipient), amount);
|
|
assert.equal(progress(game, f.target, guide.action.target), 0);
|
|
rejected(game, f.target, guide.action); assert.deepEqual(inventory(game), f.originalCards);
|
|
});
|
|
|
|
for (const first of ['harkonnen', 'atreides']) void test(`native grouped bribe AND No permits ${first} first and rejects only completing both recipients`, () => {
|
|
const f = fixture(); let game = bind(f, 'no').game;
|
|
const other = first === f.asker ? f.opponent : f.asker;
|
|
const firstMinimum = first === f.asker ? 3 : 2, otherMinimum = other === f.asker ? 3 : 2;
|
|
game = pay(game, f.target, first, firstMinimum);
|
|
game = pay(game, f.target, other, otherMinimum - 1);
|
|
assert.equal(progress(game, f.target, first), firstMinimum);
|
|
assert.equal(progress(game, f.target, other), otherMinimum - 1); assert.ok(!promise(game, f.target).fulfilled);
|
|
rejected(game, f.target, { type: 'bribe', target: other, amount: 1 });
|
|
game = finish(game); assert.equal(game.turn, 2); assert.equal(promise(game, f.target).fulfilled, true);
|
|
assert.deepEqual(inventory(game), f.originalCards);
|
|
});
|
|
|
|
void test('native grouped bribe OR No caps either qualifying recipient while allowing smaller cumulative payments', () => {
|
|
const f = fixture(); let game = bind(f, 'no', group('or', payment(f.asker, 3), payment(f.opponent, 2))).game;
|
|
rejected(game, f.target, { type: 'bribe', target: f.asker, amount: 3 });
|
|
rejected(game, f.target, { type: 'bribe', target: f.opponent, amount: 2 });
|
|
game = pay(game, f.target, f.asker, 2); game = pay(game, f.target, f.opponent, 1);
|
|
for (const recipient of [f.asker, f.opponent]) rejected(game, f.target, { type: 'bribe', target: recipient, amount: 1 });
|
|
game = finish(game); assert.equal(promise(game, f.target).fulfilled, true);
|
|
assert.equal(player(game, f.target).spice, 7); assert.deepEqual(inventory(game), f.originalCards);
|
|
});
|
|
|
|
void test('native repeated recipient leaves share one actual payment total rather than charging their thresholds twice', () => {
|
|
const f = fixture(), claim = group('and', payment(f.asker, 3), payment(f.asker, 5));
|
|
let game = bind(f, 'yes', claim).game;
|
|
game = pay(game, f.target, f.asker, 3);
|
|
assert.equal(progress(game, f.target, f.asker), 3); assert.ok(!promise(game, f.target).fulfilled);
|
|
game = pay(game, f.target, f.asker, 2);
|
|
assert.equal(promise(game, f.target).fulfilled, true); assert.equal(promise(game, f.target).progress.length, 1);
|
|
assert.equal(progress(game, f.target, f.asker), 5); assert.equal(player(game, f.target).spice, 5);
|
|
assert.equal(player(game, f.asker).bribes, 5); assert.deepEqual(inventory(game), f.originalCards);
|
|
});
|
|
|
|
void test('native prior bribes stay in the real receipt but are excluded from newly answered grouped progress', () => {
|
|
const f = fixture(); let game = pay(f.game, f.target, f.asker, 3);
|
|
game = bind(f, 'yes', f.claim, game).game;
|
|
assert.deepEqual(promise(game, f.target).progress, []); assert.equal(player(game, f.asker).bribes, 3);
|
|
game = pay(game, f.target, f.opponent, 2); assert.ok(!promise(game, f.target).fulfilled);
|
|
game = pay(game, f.target, f.asker, 3);
|
|
assert.equal(promise(game, f.target).fulfilled, true); assert.equal(progress(game, f.target, f.asker), 3);
|
|
assert.equal(player(game, f.asker).bribes, 6); assert.equal(player(game, f.target).spice, 2);
|
|
const result = complete(f, game);
|
|
assert.equal(player(result.mentat.after, f.asker).spice - player(result.mentat.before, f.asker).spice, 6);
|
|
assert.equal(progress(result.game, f.target, f.asker), 3); assert.deepEqual(inventory(result.game), f.originalCards);
|
|
});
|
|
|
|
void test('two original Truths share scalar H7 with grouped OR H7 A4 but spending A4 must retain the scalar H7', () => {
|
|
const f = fixture(); let game = declare(f, f.game, { copies: 2 }).game;
|
|
game = act(game, f.asker, { type: 'truthAsk', question: { kind: 'bribe', target: f.target,
|
|
turn: game.turn, recipient: f.asker, minimum: 7 } });
|
|
game = answer(game, f.target, 'yes'); game = ask(game, f.asker, f.target, group('or', payment(f.asker, 7), payment(f.opponent, 4)));
|
|
// Earlier H7 Yes already makes H7 OR A4 true; No cannot evade it.
|
|
assert.deepEqual(viewGame(game, f.target).truthBribeExpressionAnswers, ['yes']);
|
|
rejected(game, f.target, { type: 'truthAnswer', answer: 'no' });
|
|
game = answer(game, f.target, 'yes');
|
|
rejected(game, f.target, { type: 'bribe', target: f.opponent, amount: 4 });
|
|
game = pay(game, f.target, f.opponent, 3); assert.ok(!promise(game, f.target).fulfilled);
|
|
game = pay(game, f.target, f.asker, 7);
|
|
assert.equal(game.bribePromises![0].progress, 7); assert.equal(game.bribePromises![0].fulfilled, true);
|
|
assert.equal(promise(game, f.target).fulfilled, true); assert.equal(player(game, f.target).spice, 0);
|
|
assert.equal(progress(game, f.target, f.opponent), 3); assert.equal(progress(game, f.target, f.asker), 7);
|
|
assert.deepEqual(inventory(game), f.originalCards);
|
|
});
|
|
|
|
void test('two original Truths retain scalar No H5 while a grouped OR Yes uses actual A2', () => {
|
|
const f = fixture(); let game = declare(f, f.game, { copies: 2 }).game;
|
|
game = act(game, f.asker, { type: 'truthAsk', question: { kind: 'bribe', target: f.target,
|
|
turn: game.turn, recipient: f.asker, minimum: 5 } });
|
|
game = answer(game, f.target, 'no'); game = ask(game, f.asker, f.target, group('or', payment(f.asker, 5), payment(f.opponent, 2)));
|
|
game = answer(game, f.target, 'yes');
|
|
const guide = viewGame(game, f.target).bribePromiseGuide; assert.ok(guide);
|
|
assert.ok(typeof guide.action.amount === 'number');
|
|
assert.equal(guide.action.target, f.opponent); assert.equal(guide.action.amount, 2);
|
|
rejected(game, f.target, { type: 'bribe', target: f.asker, amount: 5 });
|
|
game = act(game, f.target, guide.action);
|
|
assert.equal(game.bribePromises![0].progress, 0); assert.ok(!game.bribePromises![0].fulfilled);
|
|
assert.equal(progress(game, f.target, f.opponent), 2); assert.equal(promise(game, f.target).fulfilled, true);
|
|
assert.equal(player(game, f.target).spice, 8); assert.deepEqual(inventory(game), f.originalCards);
|
|
});
|
|
|
|
void test('two whole questions retain one joint purse and a dynamically available OR completion', () => {
|
|
const f = fixture(); let game = declare(f, f.game, { copies: 2 }).game;
|
|
game = answer(ask(game, f.asker, f.target, group('or', payment(f.asker, 8), payment(f.opponent, 6))), f.target, 'yes');
|
|
game = ask(game, f.asker, f.target, group('and', payment(f.asker, 3), payment(f.opponent, 3)));
|
|
assert.deepEqual(viewGame(game, f.target).truthBribeExpressionAnswers, ['yes', 'no']); game = answer(game, f.target, 'yes');
|
|
rejected(game, f.target, { type: 'bribe', target: f.asker, amount: 8 });
|
|
game = pay(game, f.target, f.asker, 3);
|
|
assert.ok(!promise(game, f.target).fulfilled); assert.ok(!promise(game, f.target, 1).fulfilled);
|
|
game = pay(game, f.target, f.opponent, 6);
|
|
assert.ok(game.bribeExpressionPromises!.every(row => row.fulfilled)); assert.equal(player(game, f.target).spice, 1);
|
|
for (const index of [0, 1]) {
|
|
assert.equal(progress(game, f.target, f.asker, index), 3); assert.equal(progress(game, f.target, f.opponent, index), 6);
|
|
}
|
|
assert.deepEqual(inventory(game), f.originalCards);
|
|
});
|
|
|
|
void test('two whole questions preserve every No OR cap while funding the compatible A2 Yes alternative', () => {
|
|
const f = fixture(); let game = declare(f, f.game, { copies: 2 }).game;
|
|
game = answer(ask(game, f.asker, f.target, group('or', payment(f.asker, 5), payment(f.opponent, 3))), f.target, 'no');
|
|
game = answer(ask(game, f.asker, f.target, group('or', payment(f.asker, 5), payment(f.opponent, 2))), f.target, 'yes');
|
|
game = pay(game, f.target, f.asker, 4); game = pay(game, f.target, f.opponent, 2);
|
|
assert.ok(!promise(game, f.target).fulfilled); assert.equal(promise(game, f.target, 1).fulfilled, true);
|
|
for (const recipient of [f.asker, f.opponent]) rejected(game, f.target, { type: 'bribe', target: recipient, amount: 1 });
|
|
game = finish(game); assert.ok(game.bribeExpressionPromises!.every(row => row.fulfilled));
|
|
assert.equal(player(game, f.target).spice, 4); assert.deepEqual(inventory(game), f.originalCards);
|
|
});
|
|
|
|
void test('native Mentat grouped Yes reopens only its ready payer and its late receipts wait for the actual next opening', () => {
|
|
const f = fixture({ phase: 8 }); let game = act(f.game, f.target, { type: 'ready' });
|
|
game = act(game, f.asker, { type: 'ready' }); assert.equal(game.phase, 8);
|
|
assert.ok(game.ready.includes(f.target)); assert.ok(game.ready.includes(f.asker)); assert.ok(!game.ready.includes(f.opponent));
|
|
game = bind(f, 'yes', f.claim, game).game;
|
|
assert.ok(!game.ready.includes(f.target)); assert.ok(game.ready.includes(f.asker)); assert.ok(!game.ready.includes(f.opponent));
|
|
rejected(game, f.target, { type: 'ready' }); const before = balances(game);
|
|
game = pay(game, f.target, f.asker, 3); rejected(game, f.target, { type: 'ready' });
|
|
game = pay(game, f.target, f.opponent, 2); game = act(game, f.target, { type: 'ready' });
|
|
assert.equal(promise(game, f.target).fulfilled, true); assert.ok(game.ready.includes(f.target));
|
|
for (const recipient of [f.asker, f.opponent]) assert.equal(player(game, recipient).spice, before[recipient]);
|
|
const result = complete(f, game);
|
|
assert.equal(result.nextTurn.after.turn, 2); assert.equal(result.nextTurn.after.phase, 0);
|
|
for (const [recipient, amount] of [[f.asker, 3], [f.opponent, 2]] as const) {
|
|
for (const state of [result.nextTurn.after, result.collection.after, result.mentat.before]) {
|
|
assert.equal(player(state, recipient).bribes, amount); assert.equal(player(state, f.target).spice, 5);
|
|
}
|
|
assert.equal(player(result.mentat.after, recipient).spice - player(result.mentat.before, recipient).spice, amount);
|
|
assert.equal(player(result.mentat.after, recipient).bribes, 0);
|
|
}
|
|
assert.deepEqual(inventory(result.game), f.originalCards);
|
|
});
|
|
|
|
void test('native whole No remains binding across opening receipts and late payments until the actual turn ends', () => {
|
|
const f = fixture({ phase: 7 }); let game = bind(f, 'no').game;
|
|
game = pay(game, f.target, f.asker, 3); const before = player(game, f.asker).spice;
|
|
game = enterMentatBribeWithUnpaidPromise(game);
|
|
assert.equal(game.phase, 8); assert.equal(player(game, f.asker).spice, before + 3);
|
|
assert.equal(player(game, f.asker).bribes, 0); assert.equal(progress(game, f.target, f.asker), 3);
|
|
game = pay(game, f.target, f.opponent, 1);
|
|
rejected(game, f.target, { type: 'bribe', target: f.opponent, amount: 1 });
|
|
assert.ok(!promise(game, f.target).fulfilled); game = finish(game);
|
|
assert.equal(game.turn, 2); assert.equal(promise(game, f.target).fulfilled, true);
|
|
game = pay(game, f.target, f.opponent, 2);
|
|
assert.equal(progress(game, f.target, f.opponent), 1); assert.equal(player(game, f.opponent).bribes, 3);
|
|
assert.equal(player(game, f.target).spice, 4); assert.deepEqual(inventory(game), f.originalCards);
|
|
});
|
|
|
|
void test('native grouped question and ordinary guide retain exact target ownership current turn and payment event', () => {
|
|
const f = fixture(), declared = declare(f);
|
|
rejected(declared.game, f.opponent, f.questionAction);
|
|
rejected(declared.game, f.asker, { type: 'truthAsk', question: { ...f.question, turn: f.game.turn + 1 } });
|
|
rejected(declared.game, f.asker, { type: 'truthAsk', question: { ...f.question, claim: group('and', payment(f.asker, 3), payment(f.target, 1)) } });
|
|
const claim = group('and', payment(f.asker, 3), payment(f.asker, 2));
|
|
let game = ask(declared.game, f.asker, f.target, claim);
|
|
assert.equal(viewGame(game, f.asker).truthBribeExpressionAnswers, null);
|
|
assert.deepEqual(viewGame(game, f.target).truthBribeExpressionAnswers, ['yes', 'no']);
|
|
rejected(game, f.asker, { type: 'truthAnswer', answer: 'yes' }); game = answer(game, f.target, 'yes');
|
|
const guide = viewGame(game, f.target).bribePromiseGuide; assert.ok(guide);
|
|
assert.ok(typeof guide.action.amount === 'number');
|
|
assert.equal(guide.action.bribePromiseEvent, promise(game, f.target).event);
|
|
rejected(game, f.opponent, guide.action);
|
|
rejected(game, f.target, { ...guide.action, bribePromiseEvent: 'stale-grouped-bribe-event' });
|
|
rejected(game, f.target, { ...guide.action, amount: guide.action.amount + 1 });
|
|
game = act(game, f.target, guide.action); rejected(game, f.target, guide.action);
|
|
assert.equal(promise(game, f.target).fulfilled, true); assert.deepEqual(inventory(game), f.originalCards);
|
|
});
|
|
|
|
void test('all four minimal policies submit the exact legal ordinary guide until the whole grouped AND is paid', () => {
|
|
const f = fixture({ phase: 8 }), bound = bind(f).game;
|
|
for (const difficulty of DIFFICULTIES) {
|
|
let game = structuredClone(bound);
|
|
for (let count = 0; count < 8 && !promise(game, f.target).fulfilled; count++) {
|
|
const view = viewGame(game, f.target); view.players.find(seat => seat.id === f.target)!.bot = difficulty;
|
|
const guide = view.bribePromiseGuide; assert.ok(guide, difficulty);
|
|
const action = botActions(view).find(action => action.type === 'bribe' && action.bribePromiseEvent === guide.action.bribePromiseEvent &&
|
|
action.target === guide.action.target && action.amount === guide.action.amount);
|
|
assert.ok(action, difficulty); game = act(game, f.target, action);
|
|
}
|
|
assert.equal(promise(game, f.target).fulfilled, true, difficulty);
|
|
assert.equal(player(game, f.target).spice, 5); assert.equal(player(game, f.asker).bribes, 3);
|
|
assert.equal(player(game, f.opponent).bribes, 2); assert.deepEqual(inventory(game), f.originalCards);
|
|
}
|
|
});
|
|
|
|
void test('native exact grouped guides complete both recipients without changing the original auction turn', () => {
|
|
const f = fixture(), game = completePayments(bind(f).game, f.target);
|
|
assert.equal(game.auction!.active, f.game.auction!.active); assert.equal(promise(game, f.target).fulfilled, true);
|
|
assert.equal(progress(game, f.target, f.asker), 3); assert.equal(progress(game, f.target, f.opponent), 2);
|
|
assert.equal(player(game, f.target).spice, 5); assert.deepEqual(inventory(game), f.originalCards);
|
|
});
|