Allow travelers to pay in two-party travel deals
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
c1ffa1c28a
commit
3987c5c16b
9 files changed
+90
-21
No files matched your search
@@ -72,7 +72,7 @@ export function publicDealOfferAction(targets: readonly string[], terms: string,
|
||||
(groupTravel ? !!payment || travelKeys !== 'actor,answer,kind,minimum,territory' ||
|
||||
typeof travel.actor !== 'string' || !targets.includes(travel.actor) :
|
||||
travelKeys !== 'answer,kind,minimum,territory' ||
|
||||
!!payment && (payment.kind !== 'immediateBribe' || payment.payer !== proposer || payment.recipient !== targets[0])) ||
|
||||
!!payment && payment.kind !== 'immediateBribe') ||
|
||||
(Object.hasOwn(travel, 'dueTurn') && (!Number.isSafeInteger(travelDeadline) || (travelDeadline as number) < 1 ||
|
||||
(currentTurn !== undefined && (!Number.isSafeInteger(currentTurn) || currentTurn < 1 || (travelDeadline as number) < currentTurn)))) ||
|
||||
!['ship', 'move'].includes(travel.kind) || !TERRITORIES.some(territory => territory.id === travel.territory) ||
|
||||
@@ -227,7 +227,7 @@ export function PublicDeals({ game, act, busy }: {
|
||||
...(selected.length > 1 ? {actor:travelActor} : {}),
|
||||
...(travelTurn > game.turn ? {dueTurn:travelTurn} : {})} : undefined;
|
||||
const combinedPaymentValid = !includeTravel || !payment || payment.kind === 'immediateBribe' &&
|
||||
payment.payer === me?.id && selected.length === 1 && payment.recipient === selected[0];
|
||||
selected.length === 1 && paymentPeopleAndAmountValid;
|
||||
const offer = legalSelection && paymentValid && (!includeTravel || travelDueValid)
|
||||
? publicDealOfferAction(selected, terms, payment, me?.id, game.turn, travel) : null;
|
||||
const disabled = busy || !!game.roomControl?.paused || !!game.roomControl?.closed || !!me?.autopilot;
|
||||
@@ -308,8 +308,8 @@ export function PublicDeals({ game, act, busy }: {
|
||||
{includeTravel && <fieldset>
|
||||
<legend>Travel term</legend>
|
||||
<p className="fine">This term concerns one physical action during the selected turn. The sole recipient commits to travel in a two-party deal. In a group deal, choose one recipient to commit; they accept last after all other recipients. Written terms remain player-managed.</p>
|
||||
<p className="fine">A two-party travel term may include an immediate payment from the proposer to the recipient. The recipient collects their share at the next Mentat opening and can use it after collection. Group travel terms cannot include a payment.</p>
|
||||
{!combinedPaymentValid && <p role="alert">For two-party travel, choose an immediate payment from the proposer to the selected recipient. Group travel cannot include payment.</p>}
|
||||
<p className="fine">A two-party travel term may include an immediate payment in either direction. The payment recipient collects their share at the next Mentat opening and can use it after collection. Group travel terms cannot include a payment.</p>
|
||||
{!combinedPaymentValid && <p role="alert">For two-party travel, choose an immediate payment between the two participants. Group travel cannot include payment.</p>}
|
||||
{selected.length > 1 && <label>Committed traveler
|
||||
<select value={selected.includes(travelActor) ? travelActor : ''} disabled={disabled}
|
||||
onChange={event => setTravelActor(event.target.value)}>
|
||||
|
||||
@@ -17,6 +17,16 @@ open mode gates.
|
||||
|
||||
## Current checkpoint and work
|
||||
|
||||
**Reverse paid travel — 29 October. Prototyped / Partial.** Two-party Basic
|
||||
travel offers may pair the travel with an immediate bribe from the traveler
|
||||
to the proposer. Final consent and payment are atomic, and current-turn travel
|
||||
must remain affordable after payment. Native tests cover a completed shipment
|
||||
and rollback on insufficient remaining funds. Eleven related files pass 76
|
||||
cases; deferred and group-paid travel, broader interactions and deployed
|
||||
verification remain open. The named-Ghola guide also now credits pending
|
||||
native revival income before clearing its response in the detached forecast.
|
||||
[Contract](PUBLIC_DEALS.md#reverse-immediate-payment-with-travel--29-october-2026).
|
||||
|
||||
**Truthtrance named Ghola leader — 29 October. Prototyped / Partial.** A
|
||||
provisional question binds an actual named leader return with Ghola, distinct
|
||||
from ordinary revival or returning forces. Private human and bot guidance uses
|
||||
|
||||
+17
-3
@@ -1,5 +1,19 @@
|
||||
# Public deal records
|
||||
|
||||
## Reverse immediate payment with travel — 29 October 2026
|
||||
|
||||
**Prototyped / Partial.** A two-party Basic classic offer can pair a travel term with an immediate bribe from the traveler to the
|
||||
proposer as well as in the existing proposer-to-traveler direction. The
|
||||
traveler's final acceptance must atomically establish the travel obligation
|
||||
and settle the payment: an unavailable payment or incompatible due-turn
|
||||
travel completion leaves the offer and balances unchanged. The payment uses
|
||||
the native bribe and its deferred recipient collection; it does not invent an
|
||||
additional travel resource or foreign consent. Current- and later-turn travel
|
||||
retain their respective feasibility policies. Deferred payments paired with
|
||||
travel and multi-party paid travel remain outside this prototype. Native action tests cover payment, completed travel and rollback when the
|
||||
payment would make a current-turn shipment unaffordable. Broader interactions
|
||||
and deployed verification remain open.
|
||||
|
||||
## Structured current-turn travel — 29 October 2026
|
||||
|
||||
**Prototyped / Partial.** In a quiet Basic classic table, an offer between two
|
||||
@@ -17,12 +31,12 @@ can decline, and release still requires every party. This actor-last online
|
||||
ordering is provisional and does not restrict the printed scope of agreements.
|
||||
|
||||
A two-party offer can also pair that term with an immediate ordinary bribe
|
||||
from the proposer to the traveler. Final acceptance binds the travel and
|
||||
between the proposer and traveler in either direction. Final acceptance binds the travel and
|
||||
settles the payment together; if either cannot proceed, neither takes effect.
|
||||
The payer loses the spice immediately, and the recipient's share becomes
|
||||
spendable at the next Mentat opening, so it cannot fund this turn's travel.
|
||||
Later mutual release does not refund the settled bribe. Reverse payments,
|
||||
deferred payments in the same offer and multi-party paid travel remain open.
|
||||
Later mutual release does not refund the settled bribe. Deferred payments in
|
||||
the same offer and multi-party paid travel remain open.
|
||||
|
||||
Each receipt retains its public-deal identity, independent of Truthtrance
|
||||
history and cards. Unanimous release frees only that agreement; completion,
|
||||
|
||||
+1
-1
@@ -37529,9 +37529,9 @@ function projectedGholaLeaderGuide(g: Game, player: string): { action: Action }
|
||||
try {
|
||||
const trial = applyActionInner(g, player, action, 'movementPreparation');
|
||||
if (!trial.gholaLeaderPromises?.find(row => row.event === promise.event && row.fulfilled)) continue;
|
||||
trial.truthtrance = null; trial.response = null; trial.pendingKarama = null;
|
||||
const owner = getPlayer(trial, player);
|
||||
owner.spice += pendingBattleRevivalIncome(trial, owner);
|
||||
trial.truthtrance = null; trial.response = null; trial.pendingKarama = null;
|
||||
reconcileRevivalPromises(trial, { actor: player, action });
|
||||
if (turnActionCompletion(trial, player)) return { action };
|
||||
} catch (error) { if (!(error instanceof RuleError)) throw error; }
|
||||
|
||||
@@ -191,9 +191,8 @@ export function validatePublicDeals(value: unknown, players: readonly PublicDeal
|
||||
'Choose a travel turn no earlier than the offer turn.');
|
||||
requireDeal(!Object.hasOwn(deal, 'paymentStatus') && (multi
|
||||
? !!travel.actor && travel.actor !== deal.from && parties.includes(travel.actor) && !payment
|
||||
: !Object.hasOwn(travel, 'actor') && (!payment || payment.kind === 'immediateBribe' &&
|
||||
payment.payer === deal.from && payment.recipient === deal.to)),
|
||||
'Choose an invited traveler; payment is available only for a two-party proposer-to-recipient immediate bribe.');
|
||||
: !Object.hasOwn(travel, 'actor') && (!payment || payment.kind === 'immediateBribe')),
|
||||
'Choose an invited traveler; payment is available only as a two-party immediate bribe.');
|
||||
// The actor's final acceptance is checked privately. An unfinished offer
|
||||
// cannot already contain that vote, even after a decline or withdrawal.
|
||||
if (multi && deal.status !== 'active' && deal.status !== 'released')
|
||||
@@ -256,7 +255,7 @@ export function setPublicDealTravel(deal: PublicDeal, value: unknown, players: r
|
||||
!Object.hasOwn(deal, 'paymentStatus') && (deal.participants
|
||||
? !!deal.acceptVotes && deal.acceptVotes.length === 1 && deal.acceptVotes[0] === deal.from && !deal.payment
|
||||
: !Object.hasOwn(deal, 'acceptVotes') && (!deal.payment ||
|
||||
deal.payment.kind === 'immediateBribe' && deal.payment.payer === deal.from && deal.payment.recipient === deal.to)),
|
||||
deal.payment.kind === 'immediateBribe')),
|
||||
'Only a fresh offer may add a travel term with a supported payment.');
|
||||
notAllied(participants(players, publicDealParticipants(deal)));
|
||||
const result = { ...deal, travel: parsePublicDealTravel(value) };
|
||||
|
||||
@@ -304,6 +304,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'A deal may include one immediate spice payment between two participants. The last acceptance invokes the normal bribe action: the payer loses the gross amount immediately, Bureaucrat may divert two, and the recipient collects the remaining share at the next Mentat opening. Nothing is reserved before acceptance, and an unavailable payment leaves the offer open.',
|
||||
'A deal may instead specify one exact payment due this turn or a later turn. After everyone accepts, the payer can use that deal’s Pay button early or by the deadline. Future payments do not reserve current funds; obligations due now must still be respected. Separate deals require separate payments. A compatible Truthtrance bribe answer can be satisfied by the same payment. Mutual release cancels an unpaid term; a paid amount is not refunded.',
|
||||
'In a supported Basic classic table, two to six people can agree that one invited recipient will make or avoid one reserve shipment or ground move of at least a selected number of physical forces to a named territory in the current or a later turn. In a group offer the other recipients accept first and the named traveler accepts last. A current-turn term is checked on acceptance; a later-turn term is scheduled without reserving present forces or spice. The actual action fulfills a required term; a forbidden action cannot be taken. Compatible travel terms can share one action. A bounded due-turn search protects witnessed completions; when the travel turn closes without a witness, the game provisionally voids that term. Unsupported combinations remain in development.',
|
||||
'Two-party travel can include an immediate payment in either direction. Final consent and payment are atomic, and current-turn travel must remain feasible after the payer spends. Deferred and multi-party paid travel remain unfinished.',
|
||||
'Players must honor legal deal terms; an impossible part is void. The game does not interpret or enforce free-text promises or determine impossibility. Free text alone pays no spice. Deals must not transfer cards, leaders, forces or faction advantages or contradict the rules.',
|
||||
'Minimal AI follows supported Truthtrance ordinary-payment answers and their legal guides. AI deal negotiation and automatic enforcement of prose remain unfinished.',
|
||||
],
|
||||
|
||||
@@ -117,11 +117,50 @@ void test('unanimous release ends the pending travel obligation without refundin
|
||||
assert.equal(player(game, f.target).bribes, escrow);
|
||||
});
|
||||
|
||||
void test('paid travel excludes reversed, deferred and multiparty payment variants', () => {
|
||||
void test('paid travel excludes deferred and multiparty payment variants', () => {
|
||||
const f = fixture(), travel = { kind: 'ship', territory: f.shipmentTerritory, minimum: 1, answer: true };
|
||||
const base = { type: 'dealOffer', target: f.target, terms: 'Travel.', travel } satisfies Action;
|
||||
reject(f.game, f.asker, { ...base, payment: { kind: 'immediateBribe', payer: f.target, recipient: f.asker, amount: 1 } });
|
||||
reject(f.game, f.asker, { ...base, payment: { kind: 'deferredBribe', payer: f.asker, recipient: f.target, amount: 1, dueTurn: f.game.turn } });
|
||||
reject(f.game, f.asker, { type: 'dealOffer', targets: [f.target, f.asker], terms: 'Travel.', travel,
|
||||
payment: { kind: 'immediateBribe', payer: f.asker, recipient: f.target, amount: 1 } });
|
||||
});
|
||||
|
||||
void test('the travelling recipient can pay the proposer at final consent and then complete shipment', () => {
|
||||
const f = fixture(), payerBefore = player(f.game, f.target).spice,
|
||||
recipientBefore = player(f.game, f.asker).bribes;
|
||||
assert.ok(payerBefore >= f.shipmentCost + 1);
|
||||
const payment = { kind: 'immediateBribe', payer: f.target, recipient: f.asker, amount: 1 };
|
||||
const travel = { kind: 'ship', territory: f.shipmentTerritory, minimum: 1, answer: true };
|
||||
const offered = act(f.game, f.asker, { type: 'dealOffer', target: f.target,
|
||||
terms: 'Payment for agreed travel.', payment, travel });
|
||||
const deal = offered.deals!.at(-1)!;
|
||||
assert.equal(player(offered, f.target).spice, payerBefore);
|
||||
const accepted = accept(offered, f.target, deal.id);
|
||||
assert.equal(accepted.deals!.find(row => row.id === deal.id)!.status, 'active');
|
||||
assert.equal(player(accepted, f.target).spice, payerBefore - 1);
|
||||
assert.equal(player(accepted, f.asker).bribes, recipientBefore + 1);
|
||||
assert.equal(accepted.travelPromises!.filter(row => row.sourceDeal === deal.id).length, 1);
|
||||
assert.equal(status(accepted, f.target, deal.id), 'pending');
|
||||
const shipped = settle(act(accepted, f.target, f.shipmentAction));
|
||||
assert.equal(status(shipped, f.target, deal.id), 'fulfilled');
|
||||
assert.equal(player(shipped, f.target).spice, payerBefore - 1 - f.shipmentCost);
|
||||
assert.equal(player(shipped, f.asker).bribes, recipientBefore + 1);
|
||||
assert.deepEqual(inventory(shipped), f.originalCards);
|
||||
});
|
||||
|
||||
void test('reverse payment that consumes the travel funds rolls back the final consent atomically', () => {
|
||||
const f = fixture(), cash = player(f.game, f.target).spice,
|
||||
recipientEscrow = player(f.game, f.asker).bribes;
|
||||
assert.ok(cash >= f.shipmentCost);
|
||||
const payment = { kind: 'immediateBribe', payer: f.target, recipient: f.asker, amount: cash };
|
||||
const travel = { kind: 'ship', territory: f.shipmentTerritory, minimum: 1, answer: true };
|
||||
const offered = act(f.game, f.asker, { type: 'dealOffer', target: f.target,
|
||||
terms: 'Payment for agreed travel.', payment, travel });
|
||||
const deal = offered.deals!.at(-1)!;
|
||||
reject(offered, f.target, { type: 'dealRespond', deal: deal.id, accept: true });
|
||||
assert.equal(offered.deals!.find(row => row.id === deal.id)!.status, 'offered');
|
||||
assert.equal(player(offered, f.target).spice, cash);
|
||||
assert.equal(player(offered, f.asker).bribes, recipientEscrow);
|
||||
assert.equal(offered.travelPromises?.some(row => row.sourceDeal === deal.id) ?? false, false);
|
||||
assert.deepEqual(inventory(offered), f.originalCards);
|
||||
});
|
||||
@@ -53,7 +53,9 @@ void test('the offer packet retains legacy two-party terms and validates an expl
|
||||
assert.equal(publicDealOfferAction(['b','c'], 'Terms', payment, 'a', 2, {...travel, actor:'c'}), null);
|
||||
assert.deepEqual(publicDealOfferAction(['b'], 'Terms', payment, 'a', 2, travel),
|
||||
{type:'dealOffer', target:'b', terms:'Terms', payment, travel});
|
||||
assert.equal(publicDealOfferAction(['b'], 'Terms', {...payment,payer:'b',recipient:'a'}, 'a', 2, travel), null);
|
||||
const reverse = {...payment,payer:'b',recipient:'a'};
|
||||
assert.deepEqual(publicDealOfferAction(['b'], 'Terms', reverse, 'a', 2, travel),
|
||||
{type:'dealOffer', target:'b', terms:'Terms', payment:reverse, travel});
|
||||
assert.equal(publicDealOfferAction(['b'], 'Terms', {kind:'deferredBribe',payer:'a',recipient:'b',amount:2,dueTurn:2}, 'a', 2, travel), null);
|
||||
assert.equal(publicDealOfferAction(['b','c'], 'Terms', payment, 'a', 2, travel), null);
|
||||
assert.equal(publicDealOfferAction(['a'], 'Terms', undefined, 'a', 2, travel), null);
|
||||
@@ -142,7 +144,12 @@ void test('the authoring controls compose a proposer payment with recipient trav
|
||||
travel:{kind:'ship',territory:'carthag',minimum:1,answer:true}});
|
||||
ui.select(0,'deferredBribe'); assert.equal(button()?.props.disabled,true);
|
||||
ui.select(0,'immediateBribe'); ui.select(1,'b'); ui.select(2,'a');
|
||||
assert.equal(button()?.props.disabled,true);
|
||||
assert.equal(button()?.props.disabled,false);
|
||||
const reverseForm = ui.render().find(node => node.type === 'form'); assert.ok(reverseForm);
|
||||
(reverseForm.props.onSubmit as (event:{preventDefault():void}) => void)({preventDefault() {}});
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(ui.sent[1])), {type:'dealOffer',target:'b',terms:'Travel for spice',
|
||||
payment:{kind:'immediateBribe',payer:'b',recipient:'a',amount:1},
|
||||
travel:{kind:'ship',territory:'carthag',minimum:1,answer:true}});
|
||||
});
|
||||
|
||||
void test('a payment can also be added after selecting the travel term', () => {
|
||||
|
||||
@@ -172,8 +172,8 @@ void test('multiparty travel rejects forged actors, early votes, payments and no
|
||||
assert.equal(respondPublicDeal(acceptedOther, seats, 'b', true).status, 'active');
|
||||
});
|
||||
|
||||
void test('a proposer-to-recipient immediate payment can accompany travel through the complete deal lifecycle', () => {
|
||||
const payment = { kind: 'immediateBribe', payer: 'a', recipient: 'b', amount: 3 } as const;
|
||||
for (const [payer, recipient] of [['a', 'b'], ['b', 'a']] as const) void test(`a ${payer}-to-${recipient} immediate payment can accompany travel through the complete deal lifecycle`, () => {
|
||||
const payment = { kind: 'immediateBribe', payer, recipient, amount: 3 } as const;
|
||||
const original = createPublicDeal(seats, 'a', 'b', 'Payment for travel.', 3, 'paid-travel', payment);
|
||||
const offered = setPublicDealTravel(original, term, seats);
|
||||
assert.equal(original.travel, undefined);
|
||||
@@ -193,11 +193,10 @@ void test('a proposer-to-recipient immediate payment can accompany travel throug
|
||||
assert.equal(released.status, 'released');
|
||||
});
|
||||
|
||||
void test('paid travel rejects reverse, deferred, multiparty and forged persisted payment shapes', () => {
|
||||
const reverse = { kind: 'immediateBribe', payer: 'b', recipient: 'a', amount: 1 } as const;
|
||||
void test('paid travel rejects deferred, multiparty and forged persisted payment shapes', () => {
|
||||
const deferred = { kind: 'deferredBribe', payer: 'a', recipient: 'b', amount: 1, dueTurn: 3 } as const;
|
||||
const paid = { kind: 'immediateBribe', payer: 'a', recipient: 'b', amount: 1 } as const;
|
||||
for (const payment of [reverse, deferred]) {
|
||||
for (const payment of [deferred]) {
|
||||
const offer = createPublicDeal(seats, 'a', 'b', 'Terms', 3, `invalid-${payment.kind}`, payment);
|
||||
assert.throws(() => setPublicDealTravel(offer, term, seats), PublicDealError);
|
||||
assert.throws(() => validatePublicDeals([{ ...offer, travel: term }], seats), PublicDealError);
|
||||
|
||||
Reference in new issue
Block a user