Prototype Truthtrance ally funding commitments
Verify and publish checkpoint container / container (push) Successful in 10m48s

This commit is contained in:
Quintine committed 2026-10-29 05:44:01 +11:00
1 parent 68643925e7
commit c414a03a19
14 files changed
+795 -4

No files matched your search

+94
View File
@@ -0,0 +1,94 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
PledgePromiseError, livePledgePromises, parsePledgeClaim, pledgePromiseRequirements,
reconcilePledgePromises, recordPromisedPledge, validatePledgePromises,
type PledgePromise, type PledgeReconciliation,
} from '../game/pledge-promises';
const promise = (overrides: Partial<PledgePromise> = {}): PledgePromise => ({
turn: 2, phase: 3, ally: 'ally', minimum: 3,
event: 'event-1', player: 'donor', asker: 'asker', answer: true, ...overrides,
});
const context = (overrides: Partial<PledgeReconciliation> = {}): PledgeReconciliation => ({
turn: 2, phase: 3, mutualAlly: player => player === 'donor' ? 'ally' : null,
canFulfill: () => true, ...overrides,
});
void test('a pledge question requires a current supported phase, current ally identity and positive safe threshold', () => {
assert.deepEqual(parsePledgeClaim({turn: 2, phase: 5, ally: 'ally', minimum: 4}),
{turn: 2, phase: 5, ally: 'ally', minimum: 4});
for (const value of [null, {}, {turn: 0, phase: 3, ally: 'ally', minimum: 1},
{turn: 2, phase: 4, ally: 'ally', minimum: 1},
{turn: 2, phase: 3, ally: ' ally ', minimum: 1},
{turn: 2, phase: 3, ally: 'ally', minimum: 0},
{turn: 2, phase: 3, ally: 'ally', minimum: Number.MAX_SAFE_INTEGER + 1}])
assert.throws(() => parsePledgeClaim(value), PledgePromiseError);
});
void test('overlapping future Yes and No answers combine into a single bounded update', () => {
const rows = [promise({minimum: 2}), promise({event: 'event-2', minimum: 4}),
promise({event: 'event-3', answer: false, minimum: 6})];
assert.deepEqual(pledgePromiseRequirements(rows, 'donor', 2, 3, 'ally'),
{ally: 'ally', minimum: 4, maximum: 5});
assert.equal(pledgePromiseRequirements([...rows, promise({event: 'event-4', answer: false, minimum: 4})],
'donor', 2, 3, 'ally'), null);
assert.equal(pledgePromiseRequirements(rows, 'donor', 2, 3, 'different'), null);
assert.deepEqual(pledgePromiseRequirements(rows, 'donor', 2, 5, 'ally'), {ally: 'ally', minimum: 0});
});
void test('only a future accepted native funding update satisfies Yes, including an explicit same-value update', () => {
const rows = [promise({minimum: 3}), promise({event: 'event-2', minimum: 5}),
promise({event: 'event-3', answer: false, minimum: 6})];
const before = structuredClone(rows);
const low = recordPromisedPledge(rows, 'donor', 2, 3, {ally: 'ally', amount: 3});
assert.equal(low[0].fulfilled, true);
assert.equal(low[1].fulfilled, undefined);
assert.equal(low[2].fulfilled, undefined);
assert.deepEqual(rows, before);
const full = recordPromisedPledge(low, 'donor', 2, 3, {ally: 'ally', amount: 5});
assert.equal(full[1].fulfilled, true);
assert.equal(full[2].fulfilled, undefined, 'No continues to restrict later same-phase updates');
assert.throws(() => recordPromisedPledge(full, 'donor', 2, 3, {ally: 'ally', amount: 6}), PledgePromiseError);
assert.deepEqual(rows, before);
assert.deepEqual(recordPromisedPledge(rows, 'donor', 2, 3, {ally: 'other', amount: 10}), rows);
assert.deepEqual(recordPromisedPledge(rows, 'donor', 2, 5, {ally: 'ally', amount: 10}), rows);
});
void test('an owner cannot voluntarily evade a feasible Yes while a foreign impossibility releases it', () => {
const rows = [promise(), promise({event: 'no', answer: false, minimum: 5})];
const before = structuredClone(rows);
const keep = reconcilePledgePromises(rows, context({canFulfill: (_row, quote) => {
assert.deepEqual(quote, {ally: 'ally', minimum: 3, maximum: 4}); return true;
}}));
assert.deepEqual(keep, rows);
assert.throws(() => reconcilePledgePromises(rows, context({canFulfill: () => false, voluntaryActor: 'donor'})),
PledgePromiseError);
assert.deepEqual(rows, before);
const released = reconcilePledgePromises(rows, context({canFulfill: () => false, voluntaryActor: 'asker'}));
assert.equal(released[0].released, true);
assert.equal(released[1].fulfilled, undefined, 'The No survives a temporary affordability change');
assert.deepEqual(rows, before);
});
void test('phase or reciprocal alliance changes expire No and release an unfulfilled Yes', () => {
const rows = [promise(), promise({event: 'no', answer: false})];
for (const changed of [context({phase: 5}), context({turn: 3}), context({mutualAlly: () => null})]) {
const result = reconcilePledgePromises(rows, changed);
assert.equal(result[0].released, true);
assert.equal(result[1].fulfilled, true);
assert.deepEqual(livePledgePromises(result, 'donor', changed.turn, changed.phase), []);
}
assert.throws(() => reconcilePledgePromises(rows, context({phase: 5, voluntaryActor: 'donor'})), PledgePromiseError);
assert.throws(() => reconcilePledgePromises(rows, context({mutualAlly: () => null, voluntaryActor: 'donor'})), PledgePromiseError);
});
void test('saved rows retain distinct events, valid participants and noncontradictory terminal flags', () => {
const players = ['donor', 'asker', 'ally'];
assert.doesNotThrow(() => validatePledgePromises([promise()], players));
for (const bad of [
[promise(), promise()], [promise({player: 'ally'})], [promise({asker: 'donor'})],
[promise({event: ''})], [promise({fulfilled: true, released: true})],
[promise({answer: false, released: true})],
]) assert.throws(() => validatePledgePromises(bad, players), PledgePromiseError);
});
@@ -0,0 +1,48 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createElement, isValidElement, type ReactElement, type ReactNode } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { PledgeClaimFields, pledgeClaimInputError } from '../components/pledge-claim-fields';
type ElementProps = { children?: ReactNode; [key: string]: unknown };
type Element = ReactElement<ElementProps>;
function elements(node: ReactNode): Element[] {
if (Array.isArray(node)) return node.flatMap(elements);
if (!isValidElement<ElementProps>(node)) return [];
return [node, ...elements(node.props.children)];
}
function input(props: Parameters<typeof PledgeClaimFields>[0]) {
const field = elements(PledgeClaimFields(props)).find(element => element.props.id === 'truth-pledge-minimum');
assert.ok(field && typeof field.props.onChange === 'function');
return field;
}
function edit(props: Parameters<typeof PledgeClaimFields>[0], value: number) {
(input(props).props.onChange as (event: { currentTarget: { valueAsNumber: number } }) => void)(
{ currentTarget: { valueAsNumber: value } });
}
void test('ally funding question checks positive safe totals and respects disabled controls', () => {
const changes: number[] = [];
const props = {minimum: 2, allyName: 'House Guild', disabled: false, onChange: (value: number) => changes.push(value)};
assert.equal(pledgeClaimInputError(props.minimum), null);
edit(props, 1); edit(props, Number.MAX_SAFE_INTEGER);
for (const value of [0, -1, 1.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1]) {
assert.ok(pledgeClaimInputError(value));
edit(props, value);
}
edit({ ...props, disabled: true }, 3);
assert.deepEqual(changes, [1, Number.MAX_SAFE_INTEGER]);
assert.match(renderToStaticMarkup(createElement(PledgeClaimFields, { ...props, minimum: 0 })), /role="alert"/);
});
void test('ally funding explanation distinguishes an update, previous pledge and payment', () => {
const html = renderToStaticMarkup(createElement(PledgeClaimFields, {
minimum: 3, allyName: 'House Guild', disabled: false, onChange() {},
}));
assert.match(html, /Current ally: House Guild/);
assert.match(html, /after the answer during this phase/);
assert.match(html, /same total again counts/);
assert.match(html, /earlier pledge alone does not/);
assert.match(html, /not a payment/);
assert.doesNotMatch(html, /current balance|available wallet/i);
});
+234
View File
@@ -0,0 +1,234 @@
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);
});