Files
Dune/tests/truth-pledge-runtime.test.ts
Quintine c414a03a19
Verify and publish checkpoint container / container (push) Successful in 10m48s
Prototype Truthtrance ally funding commitments
2026-10-29 05:44:01 +11:00

235 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { actTruthAlliance as act, advanceTruthAlliance as advance,
createTruthAllianceFixture, truthAllianceClean as clean, truthAllianceInventory as inventory,
truthAlliancePlayer as player } from './fixture-truth-alliance';
import { declareTruthBribe } from './fixture-truth-bribe';
type PledgePromise = { player: string; ally: string; minimum: number; answer: boolean;
turn: number; phase: number; fulfilled?: boolean; released?: boolean };
const promise = (game: Game): PledgePromise => {
const rows = (game as Game & { pledgePromises?: PledgePromise[] }).pledgePromises;
assert.ok(rows?.length); return rows.at(-1)!;
};
function rejected(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => act(game, actor, action));
assert.deepEqual(game, before);
}
/** A real Basic setup, storm, worm, ordinary Nexus, reciprocal consent and
* later Bidding opening. Only the source fixture's original unopened deal and
* Spice order are selected; no alliance, purse, aid or question is staged. */
function fixture(phase: 3 | 5 = 3) {
const base = createTruthAllianceFixture({ consent: 'allied' });
const game = advance(base.game, state => state.phase === phase && (phase !== 3 || !!state.auction) && clean(state));
assert.equal(player(game, base.target).ally, base.partner);
assert.equal(player(game, base.partner).ally, base.target);
assert.deepEqual(inventory(game), base.originalCards);
return { ...base, game, ally: base.partner, physical: inventory(game) };
}
type Fixture = ReturnType<typeof fixture>;
function ask(f: Fixture, source: Game, minimum: number) {
let game = declareTruthBribe(f, source).game;
const askerView = viewGame(game, f.asker);
assert.ok(askerView.truthPledgeTargets.includes(f.target));
game = act(game, f.asker, { type: 'truthAsk', question: { kind: 'pledgeAid', target: f.target,
turn: game.turn, phase: game.phase, ally: f.ally, minimum } });
return game;
}
function answer(game: Game, actor: string, response: 'yes' | 'no') {
return act(game, actor, { type: 'truthAnswer', answer: response });
}
void test('Basic Yes binds a future accepted same-value ally-funding update without treating it as a payment', () => {
const f = fixture();
let game = act(f.game, f.target, { type: 'pledgeAid', amount: 3 });
const priorSpice = player(game, f.target).spice;
const originalAid = structuredClone(game.aid[f.target]);
game = ask(f, game, 3);
assert.ok(viewGame(game, f.target).truthPledgeAnswers?.includes('yes'));
assert.equal(viewGame(game, f.asker).truthPledgeAnswers, null);
game = answer(game, f.target, 'yes');
assert.equal(promise(game).player, f.target);
assert.equal(promise(game).ally, f.ally);
assert.equal(promise(game).minimum, 3);
assert.ok(!promise(game).fulfilled, 'A pre-answer pledge does not fulfill a future update');
assert.deepEqual(game.aid[f.target], originalAid);
const guide = viewGame(game, f.target).pledgePromiseGuide;
assert.ok(guide); assert.equal(guide.action.type, 'pledgeAid'); assert.equal(guide.action.amount, 3);
for (const difficulty of DIFFICULTIES) {
const view = viewGame(game, f.target);
view.players.find(seat => seat.id === f.target)!.bot = difficulty;
const action = botActions(view)[0]; assert.ok(action, difficulty);
assert.doesNotThrow(() => act(game, f.target, action));
}
game = act(game, f.target, guide.action);
assert.equal(promise(game).fulfilled, true);
assert.deepEqual(game.aid[f.target], originalAid);
assert.equal(player(game, f.target).spice, priorSpice);
assert.deepEqual(inventory(game), f.physical);
});
void test('Basic No rejects a qualifying pledge without changing the real wallet or escrow', () => {
const f = fixture();
let game = ask(f, f.game, 3);
assert.ok(viewGame(game, f.target).truthPledgeAnswers?.includes('no'));
game = answer(game, f.target, 'no');
assert.equal(promise(game).answer, false);
game = act(game, f.target, { type: 'pledgeAid', amount: 2 });
const aid = structuredClone(game.aid[f.target]), spice = player(game, f.target).spice;
rejected(game, f.target, { type: 'pledgeAid', amount: 3 });
assert.deepEqual(game.aid[f.target], aid);
assert.equal(player(game, f.target).spice, spice);
assert.deepEqual(inventory(game), f.physical);
});
void test('an unaffordable Yes is unavailable and cannot be forced by the client', () => {
const f = fixture(), available = player(f.game, f.target).spice;
const game = ask(f, f.game, available + 1);
assert.deepEqual(viewGame(game, f.target).truthPledgeAnswers, ['no']);
rejected(game, f.target, { type: 'truthAnswer', answer: 'yes' });
});
void test('an unfulfilled Yes prevents the owner voluntarily spending its remaining pledge capacity', () => {
const f = fixture();
let game = ask(f, f.game, 4);
game = answer(game, f.target, 'yes');
const available = player(game, f.target).spice;
rejected(game, f.target, { type: 'bribe', target: f.asker, amount: available - 1 });
assert.ok(!promise(game).fulfilled && !promise(game).released);
const guide = viewGame(game, f.target).pledgePromiseGuide; assert.ok(guide);
game = act(game, f.target, guide.action);
assert.equal(promise(game).fulfilled, true);
assert.deepEqual(inventory(game), f.physical);
});
void test('a No pledge commitment stops applying when actual Bidding closes', () => {
const f = fixture();
let game = ask(f, f.game, 3);
game = answer(game, f.target, 'no');
game = advance(game, state => state.phase === 4 && clean(state));
assert.ok(promise(game).released || promise(game).phase !== game.phase,
'The earlier phase commitment cannot constrain a new phase');
assert.deepEqual(inventory(game), f.physical);
});
void test('Basic Shipment and Movement Yes preserves existing escrow and uses native pledge funding once', () => {
const f = fixture(5);
let game = act(f.game, f.target, { type: 'pledgeAid', amount: 1 });
const before = structuredClone(game), recipientSpice = player(game, f.ally).spice;
game = ask(f, game, 4);
assert.ok(viewGame(game, f.target).truthPledgeAnswers?.includes('yes'));
game = answer(game, f.target, 'yes');
assert.equal(promise(game).phase, 5);
assert.equal(game.aid[f.target].amount, 1);
assert.equal(player(game, f.target).spice, player(before, f.target).spice);
const guide = viewGame(game, f.target).pledgePromiseGuide;
assert.ok(guide); assert.equal(guide.action.amount, 4);
game = act(game, f.target, guide.action);
assert.equal(game.aid[f.target].recipient, f.ally);
assert.equal(game.aid[f.target].amount, 4);
assert.equal(player(game, f.target).spice, player(before, f.target).spice - 3);
assert.equal(player(game, f.ally).spice, recipientSpice, 'Pledged spice has not been paid to the ally');
assert.equal(promise(game).fulfilled, true);
assert.deepEqual(inventory(game), f.physical);
});
void test('Basic Shipment and Movement No rejects qualifying aid and retains the accepted lower escrow', () => {
const f = fixture(5);
let game = ask(f, f.game, 4);
game = answer(game, f.target, 'no');
game = act(game, f.target, { type: 'pledgeAid', amount: 3 });
const before = structuredClone(game);
rejected(game, f.target, { type: 'pledgeAid', amount: 4 });
assert.deepEqual(game.aid[f.target], before.aid[f.target]);
assert.equal(player(game, f.target).spice, player(before, f.target).spice);
game = act(game, f.target, { type: 'pledgeAid', amount: 2 });
assert.equal(game.aid[f.target].amount, 2);
assert.deepEqual(inventory(game), f.physical);
});
for (const response of ['yes', 'no'] as const) {
void test(`a live ${response} pledge excludes a new independent structured action question`, () => {
const f = fixture();
let game = ask(f, f.game, 3);
game = answer(game, f.target, response);
const remainingTruth = f.truths.find(id => player(game, f.asker).hand.some(card => card.id === id));
assert.ok(remainingTruth, 'The native source retains its second physical Truthtrance');
game = declareTruthBribe({ ...f, truths: [remainingTruth] }, game).game;
rejected(game, f.asker, { type: 'truthAsk', question: { kind: 'bribe', target: f.target,
turn: game.turn, recipient: f.asker, minimum: 1 } });
assert.deepEqual(inventory(game), f.physical);
});
}
void test('another player’s final deal acceptance releases a now-unaffordable pledge Yes after the real payment', () => {
const f = fixture();
const minimum = 4, available = player(f.game, f.target).spice;
const amount = available - minimum + 1;
assert.ok(amount > 0);
// The donor actually offers and therefore consents to the immediate
// payment before the later Truthtrance answer. No pending deal is staged.
let game = act(f.game, f.target, { type: 'dealOffer', target: f.asker,
terms: 'A previously agreed payment.',
payment: { kind: 'immediateBribe', payer: f.target, recipient: f.asker, amount } });
const deal = game.deals?.at(-1); assert.ok(deal);
game = ask(f, game, minimum);
game = answer(game, f.target, 'yes');
assert.ok(!promise(game).fulfilled && !promise(game).released);
const before = structuredClone(game), oldBribes = player(game, f.asker).bribes;
// The other player’s final acceptance, rather than a new voluntary action
// by the payer, executes the already-consented native immediate bribe.
game = act(game, f.asker, { type: 'dealRespond', deal: deal.id, accept: true });
assert.equal(player(game, f.target).spice, player(before, f.target).spice - amount);
assert.equal(player(game, f.asker).bribes, oldBribes + amount);
assert.deepEqual(game.aid[f.target], before.aid[f.target]);
assert.equal(promise(game).released, true);
assert.ok(!promise(game).fulfilled);
assert.deepEqual(inventory(game), f.physical);
});
void test('a hidden pledge amount does not reveal whether a public Yes is fulfilled', () => {
const f = fixture();
let bound = ask(f, f.game, 3);
bound = answer(bound, f.target, 'yes');
// Both are real updates to the same alliance after the same public answer.
// Their publicly logged action and event count are identical, but only the
// private escrow amount and fulfillment differ.
const below = act(bound, f.target, { type: 'pledgeAid', amount: 2 });
const fulfilled = act(bound, f.target, { type: 'pledgeAid', amount: 3 });
assert.ok(!promise(below).fulfilled);
assert.equal(promise(fulfilled).fulfilled, true);
const left = viewGame(below, f.asker), right = viewGame(fulfilled, f.asker);
assert.deepEqual(left.log, right.log);
assert.deepEqual(left.truthHistory, right.truthHistory);
assert.deepEqual(left.aid, right.aid);
assert.deepEqual(left.pledgePromises, []);
assert.deepEqual(right.pledgePromises, []);
assert.deepEqual(left.truthPledgeTargets, right.truthPledgeTargets);
assert.deepEqual(left.truthBribeTargets, right.truthBribeTargets);
const remainingTruth = f.truths.find(id => player(below, f.asker).hand.some(card => card.id === id));
assert.ok(remainingTruth);
const rejection = (source: Game) => {
const waiting = declareTruthBribe({ ...f, truths: [remainingTruth] }, source).game;
const before = structuredClone(waiting);
let reason = '';
try {
act(waiting, f.asker, { type: 'truthAsk', question: { kind: 'bribe', target: f.target,
turn: waiting.turn, recipient: f.asker, minimum: 1 } });
} catch (error) {
reason = error instanceof Error ? error.message : String(error);
}
assert.ok(reason, 'The independent future-action question remains unavailable');
assert.deepEqual(waiting, before);
return reason;
};
assert.equal(rejection(below), rejection(fulfilled));
assert.deepEqual(inventory(below), f.physical);
assert.deepEqual(inventory(fulfilled), f.physical);
});