Separate Guild transport permission, purchase price and loan approval

This commit is contained in:
Quintine committed 2026-10-15 22:52:37 +11:00
1 parent a12250320c
commit 11adb7dbf3
39 files changed
+1576 -121

No files matched your search

+1
View File
@@ -137,6 +137,7 @@ const responses = {
harkonnenTraitor: true,
guildIncome: true,
guildRate: true,
guildTransportPermission: true,
emperorGift: true,
emperorRevival: true,
stormPeek: true,
+3
View File
@@ -22,6 +22,9 @@ import { quoteSmugglerShipment } from './smuggler-shipment';
* are never treated as rejected candidates.
*/
export function botArrivalBlock(g: GameView, action: Action): string | null {
if (g.status === 'playing' && g.phase === 5 && g.active === g.me &&
action.type === 'guildShip' && action.nexus === undefined && g.guildTransportCanceled)
return 'Karama prevents native Guild cross/return transport for this shipment opportunity.';
if (
g.status !== 'playing' ||
g.phase !== 5 ||
+3 -3
View File
@@ -81,7 +81,7 @@ import {
canUseAsKaramaRole,
shrineTruthtranceBotAction,
} from './shrine';
import { reserveShipmentCost, alliedNoFieldShipmentPricing } from './shipment-price';
import { reserveShipmentCost } from './shipment-price';
import { moritaniSpecialKaramaBotAction } from './moritani-special-karama-options';
import { quoteSmugglerShipment } from './smuggler-shipment';
import { quoteSmugglerNoField } from './smuggler-no-field';
@@ -160,10 +160,10 @@ function alliedNoFieldOffer(g: GameView): Action | null {
alliedNoField?.owner === me.id &&
alliedNoField.canOfferAlly &&
!alliedNoField.allyDeclined &&
alliedNoField.private && ally
alliedNoField.private && alliedNoField.allyPricing && ally
) {
const inventory = alliedNoField.private;
const pricing = alliedNoFieldShipmentPricing(g, ally);
const pricing = alliedNoField.allyPricing;
const oldMarker = inventory.deployed;
const oldValue =
inventory.tokens.find((token) => token.id === oldMarker?.tokenId)
+242 -56
View File
@@ -1024,6 +1024,7 @@ export type ResponseWindow = {
advisors?: boolean;
/** Saved exact ordinary Guild-rate response; not the independent Karama rate. */
guildRateEvent?: string;
guildTransportEvent?: string;
advisorResume?: 'wormRide' | 'declaration' | 'ambassador' | 'discoveryEntry';
discoveryEntry?: DiscoveryEntryArrivalChild;
advisorAmbassadorEvent?: string;
@@ -1055,6 +1056,7 @@ export type ResponseWindow = {
| 'harkonnenTraitor'
| 'guildIncome'
| 'guildRate'
| 'guildTransportPermission'
| 'emperorGift'
| 'emperorRevival'
| 'stormPeek'
@@ -1112,6 +1114,7 @@ type PendingHomeworldShipment = HomeworldShipmentIntent & {
guildSecretEvent?: string;
guildNexusEvent?: string;
guildRateEvent?: string;
guildTransportEvent?: string;
/** Absent in older world-to-world declarations. */
route?: 'arrakis';
event: string;
@@ -1151,6 +1154,8 @@ type PendingShipment = {
type PendingGuildTransport = {
event: string;
signature: string;
/** Absent in original rate-only continuations. */
permission?: true;
turn: number;
player: string;
fromReserves: boolean;
@@ -1165,7 +1170,7 @@ type PendingGuildTransport = {
allyPayment: number;
advisors: boolean;
};
type GuildTransportOrder = Omit<PendingGuildTransport, 'event' | 'signature' | 'fromReserves'>;
type GuildTransportOrder = Omit<PendingGuildTransport, 'event' | 'signature' | 'fromReserves' | 'permission'>;
type JunctionTransportDeclaration = {
player: string;
turn: number;
@@ -2031,6 +2036,8 @@ export type Game = {
pendingGuildTransport?: PendingGuildTransport | null;
/** One canceled unaffordable declaration may be redeclared at full price this turn. */
guildRateBlocked?: { turn: number; player: string };
/** Denied cross/return privilege for this original shipment opportunity only. */
guildTransportBlocked?: { turn: number; player: string; opportunity: string };
pendingHomeworldShipment?: PendingHomeworldShipment | null;
junctionOffer?: JunctionOffer | null;
pendingExchange?: {
@@ -4183,6 +4190,13 @@ function noFieldAllyOfferBlock(
return 'Karama prevented No-Field use for this allied shipment opportunity.';
return null;
}
function alliedNoFieldPricing(g: Game, recipient: Pick<Player, 'id' | 'faction'>) {
return alliedNoFieldShipmentPricing({
purchasedGuildRate: g.karamaShipping?.player === recipient.id,
nativeGuildRateCanceled: g.guildRateBlocked?.turn === g.turn &&
g.guildRateBlocked.player === recipient.id,
}, recipient);
}
function alliedNoFieldQuote(
g: Game,
offer: RicheseAllyOffer,
@@ -4233,7 +4247,7 @@ function alliedNoFieldQuote(
false,
advisors,
);
const pricing = alliedNoFieldShipmentPricing(g, recipient);
const pricing = alliedNoFieldPricing(g, recipient);
const cost = territory(offer.territory).type === 'stronghold' ? pricing.stronghold : pricing.other;
requireRule(
offer.payer !== 'both' || cost === 2,
@@ -14452,7 +14466,7 @@ function finishAutomaticDecision(g: Game): boolean {
if (homeworldShipmentAutomatic(g)) {
const shipment = g.pendingHomeworldShipment!;
g.decision = null;
if (!openHomeworldGuildRate(g, shipment)) commitHomeworldShipment(g, shipment);
continueHomeworldShipment(g, shipment);
return true;
}
const market = g.choamMarket;
@@ -19267,6 +19281,7 @@ function completePhase(g: Game) {
g.turn++;
g.recruits = null;
delete g.guildRateBlocked;
delete g.guildTransportBlocked;
g.phase = 0;
observeOccupation(g, 'turnStart');
}
@@ -24819,6 +24834,10 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
if (response.kind === 'nexusFremenCunning') currentFremenCunningResponse(g, response);
if (response.kind === 'nexusEcazBetrayal') currentEcazBetrayalResponse(g, response);
if (response.kind === 'nexusGuildCunning') validateGuildCunningResponse(g,response);
if (response.kind === 'guildTransportPermission') {
if (g.pendingGuildTransport?.permission) validateGuildTransportPermissionResponse(g, response);
else validateHomeworldTransportPermission(g, response);
}
if (response.kind === 'guildRate') {
if (g.pendingGuildTransport) validateGuildTransportRateResponse(g, response);
else if (g.pendingHomeworldShipment?.guildRateEvent) validateHomeworldGuildRateResponse(g, response);
@@ -24894,6 +24913,11 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
log(g, canceled ? 'Karama canceled the combined dial: the selected lead now dials only their own forces; the other allied army contributes zero.' : 'Ecaz Occupy proceeds with its mandatory free contribution and the ally’s physical force pool.', { faction: 'ecaz', name: 'Occupy' });
return;
}
if (response.kind === 'guildTransportPermission') {
if (g.pendingGuildTransport?.permission) finishGuildTransportPermission(g, response, canceled);
else finishHomeworldTransportPermission(g, response, canceled);
return;
}
if (response.kind === 'guildRate') {
if (g.pendingGuildTransport) finishGuildTransportRateResponse(g, response, canceled);
else if (g.pendingHomeworldShipment?.guildRateEvent) finishHomeworldGuildRateResponse(g, response, canceled);
@@ -25999,7 +26023,8 @@ function homeworldShipmentEvent(g: Game, p: Player, route?: 'arrakis'): string {
return JSON.stringify([g.turn, g.phase, g.active, p.id, p.shipped,
homeworldContext(g), g.homeworlds?.custody,
g.players.map((seat) => [seat.id, seat.ally]),
...(currentGuildCunning(g,p.id) ? [currentGuildCunning(g,p.id)!.receipt.event] : [])]);
...(currentGuildCunning(g,p.id) ? [currentGuildCunning(g,p.id)!.receipt.event] : []),
...(g.karamaShipping?.player === p.id ? [g.karamaShipping] : [])]);
}
function homeworldShipmentBlock(g: Game, p: Player, checkPromises = true): string | null {
if (!g.homeworlds?.custody) return 'Homeworld shipment requires the Homeworld module.';
@@ -26009,8 +26034,6 @@ function homeworldShipmentBlock(g: Game, p: Player, checkPromises = true): strin
g.pendingNullentropy || g.pendingTreacheryDiscard || g.pendingShipment ||
g.pendingHomeworldShipment || g.pendingExchange || g.pendingRicheseGift || g.battle)
return 'Finish the current interaction before another Homeworld shipment.';
if (g.karamaShipping?.player === p.id)
return 'Homeworld shipment using a purchased Karama rate awaits its card-scope integration.';
const richese = byFaction(g, 'richese');
if (richese && !noFieldAllyOfferBlock(g, richese) &&
!(g.richeseAllyOpportunity?.turn === g.turn && g.richeseAllyOpportunity.recipient === p.id))
@@ -26021,6 +26044,8 @@ function homeworldShipmentBlock(g: Game, p: Player, checkPromises = true): strin
}
function guildHomeworldShipmentBlock(g: Game, p: Player): string | null {
if (p.faction !== 'guild') return 'Only Guild has this Arrakis-to-Homeworld shipment permission.';
if (guildTransportCanceled(g, p.id))
return 'Karama prevents native Guild cross/return transport for this shipment opportunity.';
return homeworldShipmentBlock(g, p);
}
function junctionTransportWindow(g: Game) {
@@ -26029,7 +26054,9 @@ function junctionTransportWindow(g: Game) {
if (!owner || !recipient || owner === recipient.id || g.phase !== 5 ||
g.status !== 'playing' || recipient.shipped) return null;
const offer = currentJunctionOffer(g);
const blocked = homeworldShipmentBlock(g, recipient, false);
const blocked = g.karamaShipping?.player === recipient.id
? 'Junction transport with a purchased Karama price awaits its separate source composition.'
: homeworldShipmentBlock(g, recipient, false);
return {owner, recipient: recipient.id, offer, blocked,
canOffer: !blocked, offerEvent: junctionOfferEvent(g),
event: JSON.stringify([homeworldShipmentEvent(g, recipient, 'arrakis'), offer])};
@@ -26149,10 +26176,15 @@ function performJunctionTransport(g: Game, p: Player, action: Action) {
}
function homeworldShipmentQuote(g: Game, intent: HomeworldShipmentIntent & {route?: 'arrakis';guildSecretEvent?:string}) {
requireRule(g.homeworlds?.custody, 'Homeworld shipment requires saved physical custody.');
requireRule(!intent.guildSecretEvent || g.karamaShipping?.player !== intent.player,
'Choose the purchased Karama price or Guild Secret Ally, not both.');
requireRule(!homeworldUsesGuildTransport(g, intent) || !guildTransportCanceled(g, intent.player),
'Karama prevents native Guild cross/return transport for this shipment opportunity.');
const context = {
...homeworldContext(g),
nativeGuildRateCanceled: g.guildRateBlocked?.turn === g.turn &&
g.guildRateBlocked.player === intent.player,
purchasedGuildRate: g.karamaShipping?.player === intent.player,
players: homeworldContext(g).players.map((seat) => ({...seat, ally: getPlayer(g, seat.id).ally})),
};
const order = {player: intent.player, destination: intent.destination, sources: intent.sources};
@@ -26179,8 +26211,6 @@ function validateHomeworldShipment(g: Game, shipment: PendingHomeworldShipment)
shipment.turn === g.turn && (shipment.route === undefined || shipment.route === 'arrakis') &&
shipment.event === homeworldShipmentEvent(g, p, shipment.route),
'This Homeworld declaration no longer matches its unused shipment and physical custody.');
requireRule(g.karamaShipping?.player !== p.id,
'Homeworld shipment using a purchased Karama rate awaits its card-scope integration.');
const quote = homeworldShipmentQuote(g, shipment);
requireRule(shipment.amount === quote.amount && shipment.elite === quote.elite &&
shipment.cost === quote.cost && JSON.stringify(shipment.pools) === JSON.stringify(quote.sources),
@@ -26196,6 +26226,13 @@ function homeworldShipmentIntegrity(g: Game) {
const decisions = homeworldSavedDecisions(g).filter((d) => d.kind === 'homeworldShipmentGuild');
const shipment = g.pendingHomeworldShipment;
if (!shipment && !decisions.length) return;
if (shipment?.guildTransportEvent) {
const responses = savedNoFieldResponses(g).filter(response => response.kind === 'guildTransportPermission');
requireRule(!decisions.length && responses.length === 1 && !g.pendingShipment,
'The saved Homeworld transport declaration lost its single permission response.');
validateHomeworldTransportPermission(g, responses[0]);
return;
}
if (shipment?.guildRateEvent) {
const responses = savedNoFieldResponses(g).filter(response => response.kind === 'guildRate');
requireRule(!decisions.length && responses.length === 1 && !g.pendingShipment,
@@ -26220,6 +26257,7 @@ function commitHomeworldShipment(g: Game, shipment: PendingHomeworldShipment, se
if (shipment.guildSecretEvent) recordGuildSecretShipment(g,shipment.player,shipment.guildSecretEvent,'homeworld',shipment);
if (shipment.guildNexusEvent) finishGuildCunningShipment(g,shipment.player,'homeworld',shipment);
const p = getPlayer(g, shipment.player);
const purchasedRate = g.karamaShipping?.player === p.id;
payWithAlly(g, p, quote.cost, shipment.allyPayment);
if (settlement?.recipient) getPlayer(g, settlement.recipient).spice += quote.cost;
g.homeworlds!.custody = quote.state;
@@ -26240,13 +26278,14 @@ function commitHomeworldShipment(g: Game, shipment: PendingHomeworldShipment, se
// Off-planet destinations do not create the Guild's ordinary onto-Dune income.
// November FAQ contributor routing remains independent of that trigger.
const guild = byFaction(g, 'guild');
const income = guild && p.ally !== guild.id ? shipment.allyPayment : 0;
const income = !purchasedRate && guild && p.ally !== guild.id ? shipment.allyPayment : 0;
if (!settlement?.recipient && guild && income > 0)
g.response = guildPaymentResponse(g, guild.id, [income], undefined,
[{ payer: p.ally!, amount: income }]);
if (!settlement?.recipient) settleBankerShipmentIncome(g, p, quote.cost, shipment.allyPayment,
{ own: false, ally: !!guild && p.ally !== guild.id });
{ own: false, ally: !purchasedRate && !!guild && p.ally !== guild.id });
finishShipmentPromises(g, p, null);
if (purchasedRate) g.karamaShipping = null;
log(g, `${p.name} shipped ${quote.amount} physical forces (${quote.elite} special) from ${quote.sourceNames.join(' and ')} to ${combatLocationName(g, shipment.destination)} for ${quote.cost} spice (${quote.cost - shipment.allyPayment} own, ${shipment.allyPayment} pledged). This uses their shipment; movement remains available.${shipment.guildSecretEvent ? ' Guild Secret Ally is spent for this half-price shipment; the payment goes to the bank.' : ''}`,
{faction: shipment.guildSecretEvent ? 'guild' : p.faction, name: shipment.guildSecretEvent ? 'Secret Ally Homeworld shipment' : 'Homeworld shipment'});
}
@@ -26262,7 +26301,68 @@ function declareHomeworldShipment(g: Game, p: Player, intent: HomeworldShipmentI
g.decision = {kind: 'homeworldShipmentGuild', player: guild.id, shipper: p.id,
destination: intent.destination, amount: quote.amount, event: shipment.event};
log(g, `${p.name} declared ${quote.amount} physical forces for ${combatLocationName(g, intent.destination)}. Payment and departure await the Guild interception decision.`);
} else if (!openHomeworldGuildRate(g, shipment)) commitHomeworldShipment(g, shipment);
} else continueHomeworldShipment(g, shipment);
}
function homeworldUsesGuildTransport(g: Game,
shipment: HomeworldShipmentIntent & {route?: 'arrakis'}): boolean {
return getPlayer(g, shipment.player).faction === 'guild' &&
(shipment.route === 'arrakis' || shipment.destination === 'homeworld:guild');
}
function continueHomeworldShipment(g: Game, shipment: PendingHomeworldShipment) {
if (!openHomeworldTransportPermission(g, shipment) && !openHomeworldGuildRate(g, shipment))
commitHomeworldShipment(g, shipment);
}
function openHomeworldTransportPermission(g: Game, shipment: PendingHomeworldShipment): boolean {
if (!homeworldUsesGuildTransport(g, shipment)) return false;
validateHomeworldShipment(g, shipment);
// The existing validator has proved this signed pending source.
const cunning = shipment.guildNexusEvent
? currentGuildCunning(g, shipment.player) as ReturnType<typeof validateGuildCunningShipment>
: null;
const event = crypto.randomUUID();
shipment.guildTransportEvent = event;
if (cunning) {
cunning.shipment!.frame = JSON.stringify(shipment);
saveGuildCunning(g, cunning);
}
g.pendingHomeworldShipment = shipment;
g.response = { kind: 'guildTransportPermission', owner: shipment.player, passed: [],
guildTransportEvent: event,
intent: `${getPlayer(g, shipment.player).name} declared native Guild cross/return transport to ${combatLocationName(g, shipment.destination)}. Karama may prevent only this faction transport privilege before payment or typed departure. The original shipment opportunity remains for a legal different route; an independent purchased price remains but grants no transport permission. Junction-sponsored transport is a separate immune Homeworld source.` };
return true;
}
function validateHomeworldTransportPermission(g: Game, response: ResponseWindow) {
const shipment = g.pendingHomeworldShipment;
requireRule(response.kind === 'guildTransportPermission' && shipment &&
shipment.guildRateEvent === undefined && homeworldUsesGuildTransport(g, shipment) &&
response.owner === shipment.player && typeof shipment.guildTransportEvent === 'string' &&
shipment.guildTransportEvent.length > 0 && response.guildTransportEvent === shipment.guildTransportEvent,
'The saved Homeworld permission lost its original native transport.');
validateHomeworldShipment(g, shipment);
return shipment;
}
function finishHomeworldTransportPermission(g: Game, response: ResponseWindow, canceled: boolean) {
const shipment = validateHomeworldTransportPermission(g, response);
const cunning = shipment.guildNexusEvent
? currentGuildCunning(g, shipment.player) as ReturnType<typeof validateGuildCunningShipment>
: null;
g.pendingHomeworldShipment = null;
delete shipment.guildTransportEvent;
if (canceled) {
g.guildTransportBlocked = { turn: g.turn, player: shipment.player,
opportunity: guildTransportOpportunity(g, shipment.player) };
if (cunning) {
delete cunning.shipment;
saveGuildCunning(g, cunning);
}
log(g, `Karama prevented the native Guild cross/return privilege. No typed forces or spice moved; ${getPlayer(g, shipment.player).name} retains a legal different shipment or movement.`);
return;
}
if (cunning) {
cunning.shipment!.frame = JSON.stringify(shipment);
saveGuildCunning(g, cunning);
}
if (!openHomeworldGuildRate(g, shipment)) commitHomeworldShipment(g, shipment);
}
function homeworldGuildRateFullCost(g: Game, shipment: PendingHomeworldShipment): number | null {
const p = getPlayer(g, shipment.player);
@@ -26401,11 +26501,13 @@ function leaderSkillNoFieldIntegrity(g: Game): boolean {
'Allied No-Field skill composition retains its original ordinary shipment only.');
const initial = responses.filter(r => r.kind === 'richeseNoField');
const guild = decisions.filter(d => d.kind === 'guildShipment');
requireRule(initial.length + guild.length === 1,
const rates = responses.filter(r => r.kind === 'guildRate');
requireRule(initial.length + guild.length + rates.length === 1,
'The allied No-Field must retain its original cancellation or Guild continuation.');
for (const response of initial) requireRule(noFieldCancellationQuote(g, response)?.kind === 'allied',
'The allied No-Field lost its original owner, recipient or token declaration.');
for (const decision of guild) validateGuildShipmentDecision(g, decision);
for (const rate of rates) validateGuildRateResponse(g, rate);
return true;
}
const pair = !!g.leaderSkills && !!shipment?.richesePair;
@@ -26579,16 +26681,18 @@ const CLASSIC_FACTIONS: Partial<Record<FactionId, true>> = {
function ordinaryGuildRateFullCost(g: Game, shipment: PendingShipment): number | null {
const guild = byFaction(g, 'guild');
const p = getPlayer(g, shipment.player);
const pricedAmount = shipment.noField || shipment.alliedNoField ? 1
: shipment.amount - (shipment.smuggler ? 1 : 0);
if (!guild || (p.id !== guild.id && !(p.ally === guild.id && guild.ally === p.id)) ||
g.karamaShipping?.player === p.id ||
(g.guildRateBlocked?.turn === g.turn && g.guildRateBlocked.player === p.id) ||
shipment.source || shipment.alliedNoField || shipment.guildSecretEvent ||
shipment.source || shipment.guildSecretEvent ||
shipment.nexusEvent || shipment.richesePair)
return null;
const fullCost = reserveShipmentCost({ faction: p.faction, halfRate: false },
territory(shipment.territory).type, shipment.amount - (shipment.smuggler ? 1 : 0));
territory(shipment.territory).type, pricedAmount);
const halfCost = reserveShipmentCost({ faction: p.faction, halfRate: true },
territory(shipment.territory).type, shipment.amount - (shipment.smuggler ? 1 : 0));
territory(shipment.territory).type, pricedAmount);
return shipment.cost === halfCost && shipment.cost < fullCost ? fullCost : null;
}
function openGuildRate(g: Game, shipment: PendingShipment): boolean {
@@ -26613,7 +26717,7 @@ function openGuildRate(g: Game, shipment: PendingShipment): boolean {
owner: byFaction(g, 'guild')!.id,
passed: [],
guildRateEvent: event,
intent: `${getPlayer(g, shipment.player).name} declared ${shipment.noField ? 'one concealed No-Field' : `${shipment.amount} reserve forces`}${shipment.smugglerCompanion ? ' with one free Smuggler companion' : shipment.smuggler ? ' including one free Smuggler companion' : ''} to ${territory(shipment.territory).name} at the Guild rate of ${shipment.cost} spice. Karama can remove this discount before any payment or arrival, but does not remove the independent free companion. The same paid units would cost ${fullCost} at full price; if the original authorized split cannot pay that amount, the declaration returns unused and a replacement this turn uses full price.`,
intent: `${getPlayer(g, shipment.player).name} declared ${shipment.alliedNoField ? 'one allied No-Field shipment' : shipment.noField ? 'one concealed No-Field' : `${shipment.amount} reserve forces`}${shipment.smugglerCompanion ? ' with one free Smuggler companion' : shipment.smuggler ? ' including one free Smuggler companion' : ''} to ${territory(shipment.territory).name} at the Guild rate of ${shipment.cost} spice. Karama can remove this discount before any payment or arrival.${shipment.smuggler || shipment.smugglerCompanion ? ' The independent free companion remains free.' : ''} The same paid units would cost ${fullCost} at full price; if the original authorized split cannot pay that amount, the declaration returns unused and a replacement this turn uses full price.`,
};
log(g, `${getPlayer(g, shipment.player).name} declared a Guild-rate reserve shipment; payment and arrival wait for the rate response.`);
return true;
@@ -26628,16 +26732,35 @@ function validateGuildRateResponse(g: Game, response: ResponseWindow) {
response.owner === guild.id && typeof shipment.guildRateEvent === 'string' &&
shipment.guildRateEvent.length > 0 &&
response.guildRateEvent === shipment.guildRateEvent &&
shipment.turn === g.turn && fullCost !== null &&
shipment.cost === reserveShipmentCost({ faction: p.faction, halfRate: true },
territory(shipment.territory).type, shipment.amount - (shipment.smuggler ? 1 : 0)),
shipment.turn === g.turn && fullCost !== null,
'The saved Guild-rate response lost its original physical shipment.');
return { shipment, fullCost };
}
function guildTransportPermissionIntegrity(g: Game) {
const responses = savedNoFieldResponses(g).filter(response => response.kind === 'guildTransportPermission');
const transport = g.pendingGuildTransport?.permission === true;
const homeworld = !!g.pendingHomeworldShipment?.guildTransportEvent;
const expected = Number(transport) + Number(homeworld);
requireRule(expected <= 1 && responses.length === expected,
'The native Guild transport lost its unique permission response.');
for (const response of responses) {
if (transport) validateGuildTransportPermissionResponse(g, response);
else validateHomeworldTransportPermission(g, response);
}
const blocked = g.guildTransportBlocked;
if (blocked) requireRule(blocked.turn === g.turn &&
g.players.some(player => player.id === blocked.player) &&
(blocked.opportunity === 'ordinary' ||
g.nexusGuildCunningHistory?.some(record => record.receipt.event === blocked.opportunity &&
record.receipt.turn === g.turn && record.receipt.owner === blocked.player)),
'The denied Guild transport belongs to another shipment opportunity.');
}
function guildRateIntegrity(g: Game) {
guildTransportPermissionIntegrity(g);
const responses = savedNoFieldResponses(g).filter(response => response.kind === 'guildRate');
const expected = Number(!!g.pendingShipment?.guildRateEvent) +
Number(!!g.pendingGuildTransport) + Number(!!g.pendingHomeworldShipment?.guildRateEvent);
Number(!!g.pendingGuildTransport && !g.pendingGuildTransport.permission) +
Number(!!g.pendingHomeworldShipment?.guildRateEvent);
requireRule(expected <= 1 && responses.length === expected,
'The saved Guild-rate shipment lost its single Karama response.');
if (responses.length) {
@@ -26662,7 +26785,10 @@ function finishGuildRateResponse(g: Game, response: ResponseWindow, canceled: bo
g.guildRateBlocked = { turn: g.turn, player: shipper.id };
const bounds = shipmentPaymentBounds(fullCost, shipper.spice,
aidFor(g, shipper)?.amount ?? 0);
if (shipment.allyPayment < bounds.minimum || shipment.allyPayment > bounds.maximum) {
// A loan supplier approved the original whole price, not an increased gift.
const supplierApprovalChanges = shipment.alliedNoField &&
shipment.alliedNoField.payer === shipment.alliedNoField.owner;
if (supplierApprovalChanges || shipment.allyPayment < bounds.minimum || shipment.allyPayment > bounds.maximum) {
if (cunning) {
delete cunning.shipment;
saveGuildCunning(g, cunning);
@@ -26680,15 +26806,17 @@ function finishGuildRateResponse(g: Game, response: ResponseWindow, canceled: bo
saveGuildCunning(g, cunning);
}
commitShipment(g, shipment);
if (canceled && !g.pendingGuildBetrayal) {
if (canceled && shipper.shipped && !g.pendingGuildBetrayal) {
delete g.guildRateBlocked;
log(g, `Karama removed Guild's half-price rate. ${shipper.name} paid the full ${fullCost} spice for the unchanged shipment.`);
}
}
function guildTransportSignature(intent: PendingGuildTransport): string {
return JSON.stringify([intent.event, intent.turn, intent.player, intent.fromReserves,
const fields = [intent.event, intent.turn, intent.player, intent.fromReserves,
intent.group, intent.eliteGroup, intent.origin, intent.to, intent.sector,
intent.amount, intent.elite, intent.cost, intent.allyPayment, intent.advisors]);
intent.amount, intent.elite, intent.cost, intent.allyPayment, intent.advisors];
if (intent.permission !== undefined) fields.push(intent.permission);
return JSON.stringify(fields);
}
function ordinaryGuildTransportFullCost(g: Game, p: Player, frame: GuildTransportOrder): number | null {
const guild = byFaction(g, 'guild');
@@ -26720,18 +26848,19 @@ function openGuildTransportRate(g: Game, p: Player, frame: GuildTransportOrder,
log(g, `${p.name} declared Guild-rate transport; payment and physical transfer wait for the Karama response.`);
return true;
}
function validateGuildTransportRateResponse(g: Game, response: ResponseWindow) {
function validateGuildTransportWindow(g: Game, response: ResponseWindow) {
const intent = g.pendingGuildTransport;
const guild = byFaction(g, 'guild');
const shipper = intent && g.players.find(p => p.id === intent.player);
const fullCost = intent && shipper
? ordinaryGuildTransportFullCost(g, shipper, intent) : null;
requireRule(response.kind === 'guildRate' && intent && guild && shipper &&
const permission = response.kind === 'guildTransportPermission';
requireRule(intent && guild && shipper &&
(permission ? intent.permission === true && response.guildTransportEvent === intent.event
: response.kind === 'guildRate' && intent.permission === undefined &&
response.guildRateEvent === intent.event) &&
g.status === 'playing' && g.phase === 5 && g.active === shipper.id &&
shipmentAvailable(g, shipper) && intent.turn === g.turn &&
typeof intent.event === 'string' && intent.event.length > 0 &&
response.owner === guild.id && response.guildRateEvent === intent.event &&
typeof intent.fromReserves === 'boolean' &&
response.owner === guild.id && typeof intent.fromReserves === 'boolean' &&
Array.isArray(intent.group) && intent.group.every(([key, n]) =>
typeof key === 'string' && key.length > 0 && Number.isSafeInteger(n) && n > 0) &&
intent.eliteGroup && typeof intent.eliteGroup === 'object' &&
@@ -26757,15 +26886,68 @@ function validateGuildTransportRateResponse(g: Game, response: ResponseWindow) {
intent.group.reduce((sum, [key]) => sum + intent.eliteGroup[key], 0) === intent.elite) &&
(intent.to !== 'reserves' || shipper.faction === 'guild') &&
typeof intent.signature === 'string' &&
intent.signature === guildTransportSignature(intent) &&
fullCost !== null &&
intent.signature === guildTransportSignature(intent),
'The saved Guild transport lost its original physical response.');
return { intent, shipper };
}
function validateGuildTransportRateResponse(g: Game, response: ResponseWindow) {
const { intent, shipper } = validateGuildTransportWindow(g, response);
const fullCost = ordinaryGuildTransportFullCost(g, shipper, intent);
requireRule(response.kind === 'guildRate' && fullCost !== null &&
intent.cost === guildShipmentCost(
intent.to === 'reserves' ? 'reserves' : territory(intent.to).type,
intent.amount),
'The saved Guild transport lost its original physical rate response.');
intent.to === 'reserves' ? 'reserves' : territory(intent.to).type, intent.amount),
'The saved Guild transport lost its original rate.');
return { intent, shipper, fullCost };
}
function guildTransportOrder(intent: PendingGuildTransport, cost = intent.cost): GuildTransportOrder {
return {
turn: intent.turn, player: intent.player, origin: intent.origin,
group: intent.group, eliteGroup: intent.eliteGroup, to: intent.to,
sector: intent.sector, amount: intent.amount, elite: intent.elite,
cost, allyPayment: intent.allyPayment, advisors: intent.advisors,
};
}
function guildTransportOpportunity(g: Game, player: string): string {
return currentGuildCunning(g, player)?.receipt.event ?? 'ordinary';
}
function guildTransportCanceled(g: Game, player: string): boolean {
const blocked = g.guildTransportBlocked;
return !!blocked && blocked.turn === g.turn && blocked.player === player &&
blocked.opportunity === guildTransportOpportunity(g, player);
}
function openGuildTransportPermission(g: Game, p: Player, frame: GuildTransportOrder, fromReserves: boolean) {
validateGuildTransportForCommit(g, p, frame, fromReserves);
const intent: PendingGuildTransport = {
...frame, fromReserves, event: crypto.randomUUID(), signature: '', permission: true,
};
intent.signature = guildTransportSignature(intent);
g.pendingGuildTransport = intent;
g.response = { kind: 'guildTransportPermission', owner: byFaction(g, 'guild')!.id,
passed: [], guildTransportEvent: intent.event,
intent: `${p.name} declared native Guild ${frame.to === 'reserves' ? 'return' : 'cross-planet'} transport of ${frame.amount} physical forces. Karama may prevent this transport privilege, independently of its price. Denial moves no forces or spice and leaves this shipment opportunity available for a legal different route. Native cross/return stays unavailable for that opportunity; an independent purchased price is retained, not a new transport permission.` };
}
function validateGuildTransportPermissionResponse(g: Game, response: ResponseWindow) {
const result = validateGuildTransportWindow(g, response);
requireRule(response.kind === 'guildTransportPermission',
'Use the original Guild transport permission response.');
validateGuildTransportForCommit(g, result.shipper, result.intent, result.intent.fromReserves);
return result;
}
function finishGuildTransportPermission(g: Game, response: ResponseWindow, canceled: boolean) {
const { intent, shipper } = validateGuildTransportPermissionResponse(g, response);
g.pendingGuildTransport = null;
if (canceled) {
g.guildTransportBlocked = { turn: g.turn, player: shipper.id,
opportunity: guildTransportOpportunity(g, shipper.id) };
log(g, `Karama prevented ${shipper.name}'s native Guild cross/return transport. No spice or forces moved; choose a legal different shipment route or continue movement.`);
return;
}
const frame = guildTransportOrder(intent);
if (!openGuildTransportRate(g, shipper, frame, intent.fromReserves))
commitGuildTransport(g, shipper, frame, intent.fromReserves);
}
function nativeGuildTransportPermission(g: Game, p: Player): boolean {
if (guildTransportCanceled(g, p.id)) return false;
if (p.faction === 'guild') return true;
const guild = byFaction(g, 'guild');
return !!guild && p.ally === guild.id && guild.ally === p.id;
@@ -26874,13 +27056,7 @@ function finishGuildTransportRateResponse(g: Game, response: ResponseWindow, can
const { intent, shipper, fullCost } = validateGuildTransportRateResponse(g, response);
g.pendingGuildTransport = null;
if (canceled) g.guildRateBlocked = { turn: g.turn, player: shipper.id };
const frame: GuildTransportOrder = {
turn: intent.turn, player: intent.player, origin: intent.origin,
group: intent.group, eliteGroup: intent.eliteGroup, to: intent.to,
sector: intent.sector, amount: intent.amount, elite: intent.elite,
cost: canceled ? fullCost : intent.cost,
allyPayment: intent.allyPayment, advisors: intent.advisors,
};
const frame = guildTransportOrder(intent, canceled ? fullCost : intent.cost);
try {
validateGuildTransportForCommit(g, shipper, frame, intent.fromReserves);
} catch (error) {
@@ -27064,7 +27240,7 @@ function commitShipment(g: Game, shipment: PendingShipment, settlement?: {recipi
cost,
allyPayment,
bankOnly: g.karamaShipping?.player === p.id ||
!!(shipment.alliedNoField && alliedNoFieldShipmentPricing(g, p).bankOnly),
!!(shipment.alliedNoField && alliedNoFieldPricing(g, p).bankOnly),
});
if (!settlement?.recipient && guild && guildPayment > 0)
g.response = guildPaymentResponse(g, guild.id, shipmentIncomeContributions(g, p, cost, allyPayment),
@@ -28519,6 +28695,18 @@ function specialKaramaAction(intent: SpecialKaramaIntent): Action {
}
}
/** Existing provisional special-stop policy: return only the original purchased card. */
function refundStoppedShipmentKarama(g: Game, shipper: Player) {
const benefit = g.karamaShipping;
if (benefit?.player !== shipper.id) return;
if (benefit.card) {
const index = g.discard.findIndex(card => card.id === benefit.card);
if (index >= 0)
getPlayer(g, benefit.owner).hand.push(...g.discard.splice(index, 1));
}
g.karamaShipping = null;
}
/** Revalidates exact custody and the original window before any mutation.
* A future paused-intent caller must restore its original decision/response first;
* this executor does not open, pass, or recreate a reaction window.
@@ -28741,6 +28929,7 @@ export function executeSpecialKaramaIntent(
p.specialKaramaUsed = true;
if (g.pendingHomeworldShipment?.guildNexusEvent) finishGuildCunningShipment(g,shipper.id,'homeworld',g.pendingHomeworldShipment,'stopped');
shipper.shipped = true;
refundStoppedShipmentKarama(g, shipper);
g.pendingHomeworldShipment = null;
g.decision = null;
finishShipmentPromises(g, shipper, null);
@@ -28764,15 +28953,7 @@ export function executeSpecialKaramaIntent(
if (g.pendingShipment) finishNexusRicheseShipment(g,g.pendingShipment,'stopped');
if (g.pendingShipment) finishRichesePairShipment(g, g.pendingShipment, 'stopped');
shipper.shipped = true;
// Provisional canceled-shipment settlement pending a primary-source
// clarification: retain the attempted shipment's spice and rate card.
const benefit = g.karamaShipping;
if (benefit?.player === shipper.id && benefit.card) {
const index = g.discard.findIndex((c) => c.id === benefit.card);
if (index >= 0)
getPlayer(g, benefit.owner).hand.push(...g.discard.splice(index, 1));
}
g.karamaShipping = null;
refundStoppedShipmentKarama(g, shipper);
g.pendingShipment = null;
g.decision = null;
log(
@@ -30928,7 +31109,7 @@ function applyActionInner(
decision.event === shipment.event && decision.shipper === shipment.player &&
decision.destination === shipment.destination && decision.amount === shipment.amount,
'The Guild decision no longer matches this Homeworld shipment.');
if (!openHomeworldGuildRate(g, shipment)) commitHomeworldShipment(g, shipment);
continueHomeworldShipment(g, shipment);
} else if (decision.kind === 'guildShipment') {
requireRule(
action.allow === true,
@@ -32758,8 +32939,10 @@ function applyActionInner(
turn: g.turn, player: id, origin, group, eliteGroup, to, sector,
amount: n, elite, cost, allyPayment, advisors,
};
if (!guildSecretEvent && openGuildTransportRate(g, p, frame, fromReserves))
if (!guildSecretEvent) {
openGuildTransportPermission(g, p, frame, fromReserves);
return g;
}
commitGuildTransport(g, p, frame, fromReserves, guildSecretEvent);
return g;
}
@@ -34031,6 +34214,7 @@ export function viewGame(state: Game, id: string) {
: null,
guildRateCanceled: g.guildRateBlocked?.turn === g.turn &&
g.guildRateBlocked.player === id,
...(guildTransportCanceled(g, id) ? { guildTransportCanceled: true as const } : {}),
stormPending: g.stormPending ?? null,
advancedAllySeparation: advancedAllySeparationView(g, me),
basicAlliedShipment: basicAlliedShipmentView(g, me),
@@ -34386,6 +34570,7 @@ export function viewGame(state: Game, id: string) {
const owner = byFaction(g, 'richese');
if (!owner?.noField || !owner.noFieldEvent) return null;
const mine = owner.id === me.id;
const ally = owner.ally ? getPlayer(g, owner.ally) : null;
const shipBlock = mine
? noFieldShipBlock(g, owner)
: 'Only Richese controls this token inventory.';
@@ -34407,6 +34592,7 @@ export function viewGame(state: Game, id: string) {
allyOfferBlock: mine
? noFieldAllyOfferBlock(g, owner)
: 'Only Richese may offer these tokens.',
allyPricing: ally && (mine || ally.id === me.id) ? alliedNoFieldPricing(g, ally) : null,
allyDeclined:
g.richeseAllyDeclined?.turn === g.turn &&
g.richeseAllyDeclined.recipient === owner.ally,
@@ -34416,7 +34602,7 @@ export function viewGame(state: Game, id: string) {
return null;
const { event, owner, recipient, territory, sector, payer } = offer;
const recipientSeat = g.players.find(player => player.id === recipient);
const pricingPolicy = recipientSeat ? alliedNoFieldShipmentPricing(g, recipientSeat).policy : null;
const pricingPolicy = recipientSeat ? alliedNoFieldPricing(g, recipientSeat).policy : null;
try {
const {
cost,
+11 -1
View File
@@ -53,6 +53,11 @@ export function guildHomeworldShipmentChoice(
result.blocked = option.blocked;
return result;
}
if (view.guildTransportCanceled) {
result.blocked =
'The native Guild transport permission is canceled; a purchased price cannot restore this route.';
return result;
}
const custody: HomeworldCustody = { salusa: null, visitors: {} };
for (const world of worlds) {
if (world.secondary) custody.salusa = { ...world.forces[world.native] };
@@ -69,6 +74,7 @@ export function guildHomeworldShipmentChoice(
{
advanced: view.advanced,
nativeGuildRateCanceled: view.guildRateCanceled,
purchasedGuildRate: view.karamaShipping?.player === view.me,
storm: view.storm,
mobileStronghold: view.mobileStronghold?.location ?? null,
discoveries: view.discoveries,
@@ -151,6 +157,7 @@ export function guildHomeworldShipmentActions(
me.faction !== 'guild' ||
!view.guildHomeworldShipment ||
view.guildHomeworldShipment.blocked ||
view.guildTransportCanceled ||
!nexusGuildShipmentAvailable(view) ||
view.phase !== 5 ||
view.active !== me.id
@@ -226,7 +233,10 @@ export function guildHomeworldShipmentActions(
);
const amount = Math.min(
total - retain,
budget * (view.guildRateCanceled ? 1 : 2),
budget *
(view.karamaShipping?.player === view.me || !view.guildRateCanceled
? 2
: 1),
[3, 5, 7, 9][difficulty],
desired,
);
+6 -3
View File
@@ -58,8 +58,8 @@ function printedSector(key: string) {
/** Guild's own special shipment from one Arrakis territory to a Homeworld.
* The caller owns timing, payment, alliance funding, responses and arrival
* effects. Native rate cancellation changes only the half tariff to full price.
* No private card, concealed-force value or transport usage is read.
* effects. An independently purchased ordinary Karama price survives native
* rate cancellation. No private card, concealed-force value or usage is read.
* The mobile stronghold's interior remains protected when its pointer is in
* storm. A Homeworld destination never becomes a fabricated Arrakis sector. */
export function quoteGuildHomeworldShipment(
@@ -207,7 +207,10 @@ export function quoteGuildHomeworldShipment(
...transferred,
amount,
elite: 0,
cost: context.nativeGuildRateCanceled ? amount : Math.ceil(amount / 2),
cost:
context.purchasedGuildRate || !context.nativeGuildRateCanceled
? Math.ceil(amount / 2)
: amount,
origin: origin!,
boardSources,
boardForces,
+26 -2
View File
@@ -65,9 +65,24 @@ export function homeworldShipmentChoice(
result.blocked = option.blocked;
return result;
}
if (
view.guildTransportCanceled &&
me.faction === 'guild' &&
destination === 'homeworld:guild'
) {
result.blocked =
'The native Guild return permission is canceled; a purchased price cannot restore this route.';
return result;
}
if (nexus !== undefined && view.karamaShipping?.player === view.me) {
result.blocked =
'A purchased Karama price is already active; do not combine it with Guild Nexus Secret Ally.';
return result;
}
const context = {
advanced: view.advanced,
nativeGuildRateCanceled: view.guildRateCanceled,
purchasedGuildRate: view.karamaShipping?.player === view.me,
players: view.players.map((player) => ({
id: player.id,
faction: player.faction,
@@ -154,7 +169,9 @@ export function homeworldShipmentActions(
!nexusGuildShipmentAvailable(view)
)
return [];
const useNexus = nexusGuildSecretAllyCanAct(view);
const useNexus =
view.karamaShipping?.player !== view.me &&
nexusGuildSecretAllyCanAct(view);
const difficulty = Math.max(0, Math.min(3, level));
const budget = Math.max(
0,
@@ -169,6 +186,11 @@ export function homeworldShipmentActions(
.filter(
(world) =>
(world.native !== me.id || me.faction === 'guild') &&
!(
view.guildTransportCanceled &&
me.faction === 'guild' &&
world.native === me.id
) &&
world.native !== me.ally,
)
.map((world) => {
@@ -233,7 +255,9 @@ export function homeworldShipmentActions(
pools.reduce((sum, pool) => sum + pool.capacity, 0),
[3, 5, 7, 9][difficulty],
budget *
((me.faction === 'guild' && !view.guildRateCanceled) || useNexus
((me.faction === 'guild' && !view.guildRateCanceled) ||
view.karamaShipping?.player === view.me ||
useNexus
? 2
: 1),
Math.max([1, 3, 4, 5][difficulty], required),
+8 -5
View File
@@ -15,6 +15,8 @@ export type HomeworldShipmentContext = Omit<
players: (HomeworldReserveSeat & { ally: string | null })[];
/** Only the native Guild faction rate; independent transport grants survive. */
nativeGuildRateCanceled?: boolean;
/** Independently purchased ordinary Karama price; never a route permission. */
purchasedGuildRate?: boolean;
};
export type HomeworldShipmentIntent = {
player: string;
@@ -35,10 +37,10 @@ function requireShipment(
/** E3 world-to-world transport: use the actual native or foreign source pool.
* Guild may also return a foreign garrison to its own world; other own-world
* exceptions need separately established permission. The caller owns timing, usage,
* funding, bank payment, responses and arrival effects. Guild's E3 half rate
* applies to its own shipment unless its native rate is canceled; an alliance
* alone does not grant that rate. Independent transport prices belong to their
* own grant, not this native cancellation flag. */
* funding, bank payment, responses and arrival effects. A purchased ordinary
* Karama price takes precedence over the native Guild half rate or its
* cancellation; an alliance alone does not grant either price. Independent
* Nexus and Junction prices belong to their own grants. */
export function quoteHomeworldShipment(
context: HomeworldShipmentContext,
custody: HomeworldCustody,
@@ -133,7 +135,8 @@ export function quoteHomeworldShipment(
amount,
elite,
cost:
player.faction === 'guild' && !context.nativeGuildRateCanceled
context.purchasedGuildRate ||
(player.faction === 'guild' && !context.nativeGuildRateCanceled)
? Math.ceil(amount / 2)
: amount,
sources: transferred.receipts.filter((r) => r.homeworld !== destination.id),
+7
View File
@@ -134,6 +134,13 @@ const cancellationSources = {
calls: g.battle?.traitorCalls ?? null,
}),
guildIncome: () => null,
guildTransportPermission: g => ({
...(g.pendingGuildTransport ? { transport: g.pendingGuildTransport } : {}),
...(g.pendingHomeworldShipment ? { homeworld: g.pendingHomeworldShipment } : {}),
active: g.active,
blocked: g.guildTransportBlocked ?? null,
purchasedRate: g.karamaShipping ?? null,
}),
guildRate: (g) => ({
pending: g.pendingShipment ?? null,
...(g.pendingGuildTransport ? { transport: g.pendingGuildTransport } : {}),
+23 -18
View File
@@ -142,10 +142,10 @@ const phaseDetails = [
[
'At the start of this phase, resolve the Atreides spice foresight response. If allowed, Atreides privately inspects the next card, including when using double spice blow.',
'Select a destination and sector. Ship from reserves before making a force move.',
'A native Guild or reciprocal ally’s discounted reserve/cross shipment waits before payment, including original module tables. Native Guild typed Homeworld transport and return retain their own source pools. Canceling native price preserves a free Smuggler companion; an own concealed No-Field prices one marker, never its hidden value. The same group and approved funding must pay full price or return uncommitted for a full-price replacement. Cunning retains its second allowance and original history. Junction sponsorship, independent Nexus/card prices and permission cancellation are distinct; affordability/return policies remain provisional.',
'Native Guild cross/return permission has a separate unpaid response before native price. Preventing it preserves legal ordinary shipment or movement and does not remove half price. The conservative provisional denied scope is that original shipment opportunity; signed Cunning second opportunity is distinct. Native rate cancellation keeps the same paid units and approved funding, or releases an unaffordable attempt for a full-price replacement. Free Smuggler counters stay free; own/allied No-Field bills one marker. Immune Junction and independent card prices remain separate sources.',
'For movement, select the source forces. Enable Combine sectors to include several groups from the same territory.',
'A Fremen move using the two-territory advantage has a Karama response. Cancellation leaves the forces in place and limits this move to one territory; city ornithopters and a later extra move keep their normal rules.',
'Guild transport moves one exact physical group across the planet or returns it to reserves. Native half-price cancellation is separate from transport permission, including supported expansion/Nexus tables and Cunning’s second shipment. Ordinary movement still checks range, storm and stronghold occupancy.',
'Guild transport moves one exact physical group across the planet or returns it to reserves. Permission and price are separate counter opportunities; common world-to-foreign-world shipment does not require Guild cross/return privilege. Purchased half price grants no new route. Ordinary movement still checks range, storm and stronghold occupancy.',
'You may pass through an ally’s territory on a legal route that ends elsewhere. Ordinary physical reserve shipments in both modes may enter allied territory before movement, then depart during that turn. Storm and stronghold capacity remain binding. Basic visibly labels its provisional no-exit policy: excess new visitors still present go to Tanks. Advanced follows supplied p15: all the ending player’s shared forces go to Tanks, including old forces, subject to advisor/Polar/Ecaz exemptions. Neither grants an extra move. Concealed No-Fields and optional arrivals remain separate.',
'Ecaz and its reciprocal ally may share territories and count as one faction toward stronghold capacity, without extra movement or storm/Homeworld exceptions. Their three-joint-stronghold victory and desert allocation are connected. Fremen/Ecaz cooccupation permits the Fremen special win in Sietch Tabr in Basic; supplied Advanced p32 permits both Tabr and Habbanya Sietch. Other victory conditions and exceptional combined interactions remain separate.',
'A restored pending physical shipment is checked again before forces or spice are committed. If its saved turn, force allocation, destination, price or authorized contribution no longer matches the table, it is rejected without changing the saved declaration.',
@@ -1028,7 +1028,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'A normal No-Field shipment pays the one-force price. The concealed token moves like a force. Before Battle, Richese may reveal it voluntarily; a token involved in battle is revealed with the Battle Plan. Revelation brings the indicated forces from reserves, limited by what remains there.',
'The revealed token stays face up until another token is placed. Revealing it does not clear the previous-shipment restriction. An allied No-Field shipment reveals immediately and uses the ally’s reserves; an existing token must be revealed before another shipment.',
'Before an eligible ally uses its shipment, the game holds an offer opportunity for Richese. Richese may pass or privately propose a token, destination and payment. The ally accepts the exact offer and chooses its permitted regular/elite mix, or declines without payment. Success consumes the ally’s shipment, not Richese’s shipment or either player’s movement.',
'Ordinary allied shipment costs one spice into a stronghold or two elsewhere, funded by either ally or one each at cost two. Guild recipients and already-active Karama rates use a visibly provisional one-spice price in both modes, paid entirely to bank even if Richese funds it. Prevention precedes payment and the old-marker/new-token reveals. Fremen reserves are on the planet and remain ineligible.',
'Ordinary allied shipment costs one spice into a stronghold or two elsewhere. Guild recipients and active purchased prices use the existing provisional bank-only discount. Canceling a native Guild recipient’s rate charges one full-price marker, never zero/five materialized forces. A supplier’s original whole-fee1 approval cannot silently become2; release unchanged and obtain a new full-price offer/consent. Independent purchased price remains. Fremen on-planet reserves stay ineligible.',
'Storm and worm exposure reveal the token and destroy its forces. A zero token still had one-force presence before revelation. Trip to Gamont forces a reveal, then returns one force if present. Revealing a token already in a territory does not trigger Terror a second time.',
'Development tables support your own concealed shipment, movement of a marker alone or with a physical group, voluntary reveal and one-force board presence. Physical reserves remain unchanged until reveal. Karama can prevent the No-Field shipment, with Guild shipment prevention and payment handled separately. Price and chosen allied funding are shown before shipment.',
'The map shows a concealed marker without its denomination to everyone. Richese privately sees its token values, last-use restriction and reveal control. Movement selection keeps the marker separate from physical forces. No hidden denomination or physical token identity is included in another player’s projected board state.',
@@ -1050,31 +1050,35 @@ export const RULE_TOPICS: RuleTopic[] = [
area: 'Implementation',
status: 'Partial',
detail:
'Own shipment, movement, reveal, effective presence, collection, hazards, Gamont and marker-only battles work in development fixtures. Basic and Advanced mixed battles have a provisional private pool and native support/aftermath. Allied consent, typed forces, full/split funding, prevention and immediate reveal are integrated; Guild/active-Karama offers in both modes have a shared provisional one-spice/bank quote. Combined mixed battles, whole-plan inspection and wider combinations remain separate.',
'Existing own lifecycle, hazards and battle bounds remain. Native allied Guild rate cancellation bills one marker and preserves the existing provisional bank payee. Exact supplier approval cannot increase; release retains original token/army and a new full-price offer needs fresh consent. Engine, fresh lender controls and AI share authorized recipient pricing rather than inferring another player’s state from a personal view. Wider source combinations remain partial.',
evidence: ['game/shipment-price.ts', 'game/engine.ts'],
},
{
area: 'Player controls',
status: 'Partial',
detail:
'Private inventory, token selection, exact payment/funding feedback, reveal and marker movement controls are integrated with concealed map markers and private battle bounds. Browser shipment, marker-only movement, invitation recovery and voluntary reveal have been checked. The allied opportunity, private proposal, payment selection and recipient force/consent controls are integrated; owner proposal, restart recovery and recipient acceptance have been checked in synthetic browser fixtures. Mobile and full battle acceptance remain pending.',
'Existing private inventory, movement, reveal and battle controls remain. Actual390px controlled supplier loan cancels half1 without spending or revealing its prior three-marker, shows authoritative full2, makes a new five-token offer, obtains Guild consent and original interception, then spends supplier2/materializes Richese3 plus Guild5 once. No recipient wallet is exposed. Earlier saved-room evidence remains historical, not new assurance.',
evidence: ['components/richese-allied-no-field.tsx', 'components/richese-no-field.tsx'],
},
{
area: 'AI',
status: 'Partial',
detail:
'All four profiles support own shipment, marker movement/reveal and marker-only battles using entitled projections. Mixed groups in both modes use the private authoritative battle pool; Basic and Advanced Easy planning/aftermath smokes pass. Allied offer/acceptance preserves human ownership and avoids repeated declined offers and forbidden dial prescience. Complete Richese-game strategy remains unverified.',
'Own and allied legal participation remains. Fresh offer calculations consume the same authorized recipient quote as human controls; no inferred rival wallet or exact strategic first-candidate pin. Four controlled low-budget probes choose no optional reoffer here while the recipient executes a legal ordinary shipment. Full Richese strategy/calibration stays deferred.',
evidence: ['game/bots.ts'],
},
{
area: 'Documentation',
status: 'Partial',
detail:
'The lifecycle, allied consent/payment/typed forces, on-planet Fremen exclusion, current controls, privacy and five separate progress facets are recorded. Source interpretations and guarded combinations remain explicit; no combined-mode certification is claimed.',
'Source distinctions, lifecycle, typed loan consent and current controls remain documented. Guild-recipient bank payee and one-marker price precedence stay provisional. Full2 supplier reapproval and the authoritative fresh-quote bridge are explicit; original versus controlled evidence and remaining sponsored/card combinations are not conflated.',
evidence: ['docs/RICHESE_ALLIED_SHIPMENT_AUDIT.md', 'docs/GUILD_RATE_KARAMA_RULES.md'],
},
{
area: 'Verification',
status: 'Partial',
detail:
'Thirteen pure-model, ten engine, eight independent review and nine AI scenarios pass, plus nineteen existing Gamont regressions. Tests include all hidden values, exact payments, private projections, zero battles, hazards, Gamont cancellation and JSON continuation. Four persisted-room tests also pass for private recovery, competing shipments, cancellation, stale movement and duplicate reveal. Allied shipment adds eleven engine, seven independent review, seven AI and three persisted scenarios, including declineable arrival preflight and AI fallback. The allied shipment checkpoint passed 1,207 rules/client and 90 persisted/API tests. Browser own shipment, movement and invitation recovery/reveal pass, and the allied owner proposal survived a server restart and recipient acceptance was checked in a synthetic fixture. Complete Richese-game acceptance remains pending.',
'Current affected25-file263/263 passes across frozen237+26 selections. Seven controlled allied source cases cover zero/five, recipient full price, supplier release/reapproval, poverty and independent price. Actual390px complete supplier reoffer/consent fixes observed fresh-quote drift; API and four controlled continuation probes pass. Earlier own/battle/persisted evidence remains historical. No new saved/deployed or complete-mode acceptance.',
evidence: [
'tests/richese-no-field.test.ts',
'tests/richese-no-field-engine.test.ts',
@@ -1086,6 +1090,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'tests/richese-allied-no-field-review.test.ts',
'tests/richese-allied-no-field-bots.test.ts',
'tests/richese-allied-no-field-recovery.test.ts',
'tests/allied-marker-guild-rate.test.ts',
],
},
],
@@ -1431,7 +1436,7 @@ export const RULE_TOPICS: RuleTopic[] = [
area: 'Documentation',
status: 'Partial',
detail:
'Source-bound effects, copies and Duke guides remain. Native own-arrival child-first/live-token order stays provisional. Native rate price-source composition now retains original modules, independent Smuggler/own-marker pricing, typed Homeworlds and Cunning receipts; immune Junction and borrowed prices remain distinct. Allied-marker/special receipts, permission cancellation and full combinations remain open.',
'Source-bound effects, copies and Duke guides remain. Native own-arrival child-first/live-token order stays provisional. Separate native transport permission, independent purchased typed prices and own/allied marker billing now preserve original sources and approvals. Cunning opportunity scope and special-stop card-return policy are explicit interpretations; sponsored/purchased composition and full combinations remain open.',
evidence: ['docs/ECAZ_AMBASSADORS_RULES.md', 'docs/DEFERRED_SHIPMENT_ARRIVAL.md', 'docs/BOT_ARRIVAL_PREFLIGHT.md'],
},
{
@@ -2551,13 +2556,13 @@ export const RULE_TOPICS: RuleTopic[] = [
category: 'Cards',
coverage: 'Partial',
summary:
'Buy an auction card or cancel a supported use of a faction power.',
'Buy an auction card, purchase shipment pricing or cancel a supported faction power.',
steps: [
'If eligible to bid, use the card to take the current auction card without paying spice. A full hand cannot bypass the bidding limit.',
'Holding Karama also permits bids beyond your available spice. A winner can choose to spend it; an outbid player keeps it.',
'Use Cancel with Karama during a power response. Supported responses include Voice, prescience, Atreides auction and spice peeks, spiritual advisors, Emperor gifts and revival, faction income, Harkonnen bonus cards and allied traitors, plus Fremen storm and worm powers, elite combat strength and Fremen free spice support.',
'Cancel a native Guild discount before payment or transfer on supported reserve/cross/return and typed Homeworld sources. Original module selection does not immunize the faction rate. Independent Smuggler counters remain free; an own No-Field bills one concealed marker. Cunning keeps its original second allowance. Full price uses the same approved split or releases the uncommitted attempt for a full-price replacement. Junction Homeworld sponsorship and independent card/Nexus prices stay separate. Income, transport permission and remaining special receipts are distinct; the no-debt/return policy remains provisional.',
'During shipment, use Guild rates for the active player before they ship. They choose and pay for their shipment at the rounded half rate, paid to the bank. You may provide this benefit for another player; it expires if they move or finish without shipping.',
'Native Guild cross/return permission has its own unpaid counter before price. Preventing it keeps a legal different shipment/movement without removing native half price. The conservative provisional scope is that original opportunity, not a separately signed Cunning second opportunity; immune Junction and common world-to-foreign-world permission remain independent. Native rate cancellation bills the same paid units and original approved split. Free counters stay free and own/allied No-Field prices one marker. Supplier whole-fee1 requires a new offer/consent before becoming2.',
'Use a held Karama to purchase the active player’s Force Shipment at half normal price, rounded up, paid entirely to bank. Original typed World routes now retain this independent source without new route permission or Nexus stacking. Completion consumes only its grant; original Guild special-stop provisionally returns the exact purchase card to its owner and preserves resources/movement while using shipment. An already purchased card with Junction sponsorship remains a separate unfinished composition.',
'Advanced Bene Gesserit may spend Worthless cards as Karama. Each conversion opens a separate cancellation response; the Worthless card is discarded even if the power is canceled. Normal Karama cards resolve without that conversion response.',
'A pending Worthless purchase keeps space for its original auction card. Gifts and hand exchanges must leave enough room for every committed incoming card. Refreshing restores the same auction, unused shipment opportunity or original canceled power.',
'Atreides full-plan inspection, Emperor revival, Fremen’s summoned worm, Harkonnen hand exchange the Guild shipment stop and Tleilaxu revival prevention are implemented as once-per-game uses. Other cancellations and special Karamas are still being implemented.',
@@ -4653,7 +4658,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Native Richese Cunning uses the original signed two-No-Field shipment in supported single/paired E2 or selected mixed Ix+CHOAM native Nexus tables with classics, optional Skills/Homeworlds/Discovery/Tech/Advanced Strongholds. Select two distinct unused physical tokens: one reveals immediately into reserve-capped forces, one remains concealed; pay one marker invoice and spend the Nexus. The previous shipment token cannot be either choice, and the concealed one becomes last-used. Native/Guild response and actual payer/counter sources remain; a nested destination must be revealed. Prevention keeps physical tokens and unpaid spice, after any printed card cost. Sandtrout, arbitrary companions, shared allocation and other pending combinations retain their guards.',
'Richese Betrayal has a separate opt-in auction preview. Before payment, an unallied rival may spend its physical Richese Nexus card to discard a public self-cache win without any purchase debit, or redirect another buyer’s cache/Black Market payment to the bank while preserving delivery and earned bonus. Every publicly possible holder acknowledges the same opportunity; private Use eligibility cannot determine public timing. Normal hidden self-purchases and private special-Karama acquisitions are not connected. See the dedicated guide for its existing local runtime/browser evidence and remaining release gates; that evidence does not verify Nexus Kull.',
'Guild Cunning is declared when finishing your ordinary Shipment and Movement turn. It spends the Nexus and opens one Karama response. Allowance grants a second native shipment at normal Guild prices, including supported cross-shipment, return and Homeworld routes. Cancellation leaves the completed first turn intact. Only an unused Hajr move can follow the second shipment; no ordinary movement or third move is granted. The physical Ornithopter card combination remains unavailable. The separate guild-betrayal profile admits only original native shipment producers and explicitly blocks this second-shipment offer before Nexus spending; Cunning outside that profile is unchanged.',
'Supported Cunning reserve/cross/typed Homeworld shipments retain native rate cancellation without clearing the original shipped history. Original module/source, event, paid group and approved split survive allow/cancel; unaffordable release preserves only the second attempt at full price. The first shipment and ordinary movement never reopen. Junction sponsorship and independent borrowed prices are not canceled by this native flag.',
'Cunning native cross/return permission and rate use separate unpaid controls while original shipped history stays unchanged. A denial within its second opportunity allows a legal reserve replacement; denial of an earlier ordinary opportunity does not remove that new signed grant. Typed sources, original prices and approval stay bound; neither first payment nor ordinary movement reopens. Independent purchased price, immune Junction and other card sources remain distinct.',
'With Guild absent, its Secret Ally is an explicit optional source for one ordinary shipment at Guild prices. It supports physical reserve shipment, cross-shipment and return to the holder’s own reserves, including the native-Homeworld return when Homeworlds are enabled, plus half-price independently legal world-to-world shipment. Fremen may retain free reinforcement or choose the paid card route beyond its normal range, outside the storm. Native faction, typed sources and normal movement stay unchanged. Arbitrary foreign-world transport and concealed No-Fields are not granted.',
'Borrowed Guild/Richese tariffs do not change native faction or physical reserve origin. Non-Guild/non-Fremen off-planet arrivals can accrue Heighliners once for collection at the original phase end. A paid borrowed Guild route from Fremen southern reserves is still on-planet and does not trigger it; stopped and native-Guild-only shipments likewise remain excluded. The existing Emperor bank-auction purchase retains the actual buyer wallet and creates no industry activity.',
'Guild Betrayal has a separate fresh classic/native-Guild development preview. Before a fully funded positive shipment settles, an unallied holder may spend the physical Guild Nexus to take the full original fee, including their own pre-funded payment, instead of Guild, bank or occupied Junction income. Original donor escrow is consumed once and original physical delivery is unchanged. Neutral public acknowledgements reveal no fee/source; only private eligibility exposes Use. Zero/stopped shipments create no award or Nexus cost. See the dedicated guide; new verification remains Planned and ordinary Homeworld occupation/rounding questions stay local.',
@@ -4662,11 +4667,11 @@ export const RULE_TOPICS: RuleTopic[] = [
],
related: ['ecaz-modules', 'spice-blow', 'alliance-funding', 'choam-worthless', 'choam-kull', 'nexus-choam-kull', 'nexus-richese-betrayal', 'nexus-guild-betrayal', 'nexus-ixian-replacement', 'nexus-ixian-betrayal'],
checklist: [
{area:"Implementation",status:'Partial',detail:"Earlier physical Traitor roles and receipts remain integrated. Native rate now follows original price/source rather than blanket module flags: independent Smuggler/own-marker billing, typed Homeworlds and Cunning world frames retain exact source/payment/allowance. Manual/automatic interception, Skills and No-Field integrity recognize the original rate stage. Deferred Betrayal retains full-price authority until actual settlement. Immune Junction and borrowed sources stay distinct; allied markers, permission cancellation and full acceptance remain open.",evidence:['game/nexus-cards.ts', 'game/nexus-card-phase.ts', 'game/nexus-traitor-exchange.ts', 'game/traitor-declarations.ts', 'game/nexus-face-dancers.ts', 'game/nexus-suboids.ts', 'game/nexus-advisors.ts', 'game/nexus-sardaukar-options.ts', 'game/nexus-fremen-revival.ts', 'game/nexus-fremen-cunning.ts', 'game/nexus-ecaz-betrayal.ts', 'game/nexus-ecaz-inquiry.ts', 'game/nexus-ecaz-duke.ts', 'game/duke-vidal.ts', 'game/nexus-emperor-secret-ally.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal.ts', 'game/nexus-richese-options.ts', 'game/nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/nexus-guild-secret-ally-options.ts', 'game/nexus-module-profile.ts', 'game/engine.ts', 'game/nexus-tleilaxu-revival.ts', 'game/nexus-borrowed-voice.ts', 'game/combat-response-quote.ts', 'game/karama-context.ts', 'game/nexus-fremen-protection.ts', 'game/admin-seat-ai-shape.ts', 'game/richese-cancellation.ts', 'game/bot-mobility.ts', 'game/planetologist-movement.ts', 'game/revival.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally.ts', 'game/revival-resume.ts', 'game/revival-cancellation.ts', 'game/homeworld-revival-deployment.ts', 'game/homeworld-revival-return.ts', "game/nexus-emperor-purchase.ts", "game/auction-continuation-quote.ts", "game/nexus-emperor-purchase.ts", "game/ambassador-phase.ts", "game/ambassador-resume.ts", "game/battle-aftermath-quote.ts", "game/ecaz-duke-acquisition.ts", "game/ecaz-duke-revival.ts", "game/nexus-ecaz-duke-options.ts", "game/nexus-play-boundary.ts"]},
{area:"Player controls",status:'Partial',detail:"Four current390px journeys exercise composed original Smuggler shipping and ground movement, typed native return with approved donor1, poor world release/full-price replacement, and explicitly controlled own No-Field cancellation/reveal. Native instructions and prices agree after cancellation; one marker never exposes its hidden value in billing. Three original scenes, one controlled; no overflow/browser errors. Earlier Hero/Traitor/Cunning surfaces remain historical evidence, not full acceptance.",evidence:['components/nexus-cards.tsx', 'components/nexus-traitors.tsx', 'components/nexus-tleilaxu.tsx', 'components/nexus-suboids.tsx', 'components/nexus-advisors.tsx', 'components/nexus-sardaukar.tsx', 'components/choam-power-cost.tsx', 'components/moritani-terror.tsx', 'components/nexus-moritani-betrayal.tsx', 'components/nexus-ecaz-betrayal.tsx', 'components/nexus-ecaz-inquiry.tsx', 'components/nexus-ecaz-duke.tsx', 'components/nexus-richese-betrayal.tsx', 'components/game-table.tsx', 'components/nexus-tleilaxu-revival.tsx', 'components/battle-preparation.tsx', 'components/nexus-fremen-revival.tsx','components/nexus-emperor-payment.tsx', 'components/nexus-emperor-secret-ally.tsx', "components/nexus-emperor-seller-purchase.tsx", "components/nexus-emperor-ambassador-purchase.tsx", "components/richese-special-karama.tsx"]},
{area:"AI",status:'Partial',detail:"Both typed Homeworld owner quotes and bot budgets honor the actual canceled native tariff without repricing independent Nexus/Junction sources. All four policies legally pass the original native Homeworld rate in this position; human cancellation is separate evidence. Earlier Cunning/Hero/Traitor participation remains covered; no strategic tuning or calibration.",evidence:['game/nexus-card-options.ts', 'game/nexus-traitor-options.ts', 'game/nexus-tleilaxu-options.ts', 'game/nexus-suboid-options.ts', 'game/nexus-advisor-options.ts', 'game/nexus-sardaukar-options.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal-options.ts', 'game/nexus-ecaz-betrayal-options.ts', 'game/nexus-ecaz-inquiry-options.ts', 'game/nexus-ecaz-duke-options.ts', 'game/nexus-richese-options.ts', 'game/bot-nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally-options.ts']},
{area:"Documentation",status:'Partial',detail:"Native price/source composition supersedes blanket module exclusions without canceling immune Homeworld or independent card benefits. Original versus controlled proof, approved escrow, private marker billing, Cunning world receipts and deferred full invoices are explicit. Earlier no-debt/return policies remain provisional; no new numeric or publisher-priority ruling. Allied markers, special receipts, permission cancellation and public acceptance remain separate.",evidence:['docs/NEXUS_CARD_RULES.md', 'docs/NEXUS_ATREIDES_RULES.md', 'docs/NEXUS_HARKONNEN_RULES.md', 'docs/NEXUS_TLEILAXU_RULES.md', 'docs/NEXUS_IXIAN_CUNNING_RULES.md', 'docs/NEXUS_CUNNING_RUNTIME.md', 'docs/NEXUS_BENE_GESSERIT_RULES.md', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'docs/NEXUS_EMPEROR_RULES.md', 'docs/NEXUS_SARDAUKAR_RUNTIME.md', 'docs/NEXUS_FREMEN_RULES.md', 'docs/NEXUS_ECAZ_RULES.md', 'docs/NEXUS_CHOAM_RULES.md', 'docs/NEXUS_CHOAM_RUNTIME.md', 'docs/NEXUS_MORITANI_RULES.md', 'docs/NEXUS_MORITANI_RUNTIME.md', 'docs/NEXUS_RICHESE_RUNTIME.md', 'docs/NEXUS_RICHESE_RULES.md', 'docs/NEXUS_GUILD_CUNNING_RUNTIME.md', 'docs/GUILD_RATE_KARAMA_RULES.md', 'docs/GUILD_HOMEWORLD_TRANSPORT_RULES.md', 'docs/NEXUS_GUILD_RULES.md', "docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md", "docs/RULE_DECISIONS.md", "docs/RICHESE_AMBASSADOR_RULES.md", "docs/RICHESE_AMBASSADOR_IMPLEMENTATION.md", "docs/RICHESE_ACQUISITION_RULES.md", "docs/DUKE_VIDAL_RULES.md", "docs/ECAZ_DUKE_ACQUISITION.md", "docs/ECAZ_REVIVAL_RULES.md"]},
{area:"Verification",status:'Partial',detail:"Current affected18-file198/198 includes32 new source cases. Twenty-one runtime/policy paths pass:17 original-action and four controlled Cunning/No-Field paths. Four actual390px journeys prove paid/free separation, exact typed return, no-debt full-price replacement and actual concealed-marker reveal; original movement/turn resumes once. Six controlled Betrayal Use/Pass cases retain full fee until deferred payment. Earlier lifecycle evidence stays historical; no saved/deployed or complete-module assurance.",evidence:['tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-fremen-cunning.test.ts', 'tests/nexus-fremen-cunning-recovery.test.ts', 'tests/nexus-fremen-cunning-summon.test.ts', 'tests/nexus-fremen-cunning-summon-recovery.test.ts', 'tests/nexus-ecaz-betrayal-quotes.test.ts', 'tests/nexus-ecaz-betrayal.test.ts', 'tests/nexus-ecaz-betrayal-recovery.test.ts', 'tests/nexus-ecaz-inquiry.test.ts', 'tests/nexus-ecaz-duke-quote.test.ts', 'tests/nexus-ecaz-duke-engine.test.ts', 'tests/nexus-ecaz-duke-recovery.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-moritani-betrayal.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/native-guild-rate-karama.test.ts', 'tests/native-guild-rate-modules.test.ts', 'tests/native-guild-homeworld-rate.test.ts', 'tests/native-guild-rate-betrayal.test.ts', 'tests/nexus-guild-cunning-engine.test.ts', 'tests/nexus-guild-cunning-bots.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-module-battles-runtime.test.ts', 'tests/paired-ix-nexus-modules-runtime.test.ts', 'tests/paired-choam-nexus-modules-runtime.test.ts', 'tests/classic-nexus-skills-payments-runtime.test.ts', 'tests/classic-nexus-skills-battles-runtime.test.ts', 'tests/nexus-skills-modules-payments-runtime.test.ts', 'tests/nexus-skills-modules-battles-runtime.test.ts', 'tests/paired-ix-nexus-skills-modules-runtime.test.ts', 'tests/paired-choam-nexus-skills-modules-runtime.test.ts', 'tests/ecaz-nexus-skills-modules-runtime.test.ts', 'tests/moritani-nexus-skills-modules-runtime.test.ts', 'docs/NEXUS_CARD_RULES.md', 'tests/nexus-tleilaxu-revival.test.ts', 'tests/fixture-nexus-tleilaxu-secret-ally.ts', 'tests/nexus-tleilaxu-secret-ally-runtime.test.ts', 'tests/nexus-borrowed-voice.test.ts', 'tests/fixture-nexus-borrowed-voice.ts', 'tests/nexus-borrowed-voice-runtime.test.ts', 'tests/nexus-fremen-protection.test.ts', 'tests/fixture-nexus-fremen-protection.ts', 'tests/nexus-fremen-protection-runtime.test.ts', 'tests/fixture-stronghold-factions.ts', 'tests/fixture-nexus-advisor-fresh.ts', 'tests/nexus-advisor-fresh-runtime.test.ts', 'tests/fixture-nexus-advisor-storm.ts', 'tests/nexus-advisor-storm-runtime.test.ts', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'tests/nexus-fremen-free-quota.test.ts', 'tests/fixture-nexus-fremen-free-quota.ts', 'tests/nexus-fremen-free-quota-runtime.test.ts', 'tests/nexus-fremen-revival.test.ts','tests/nexus-emperor-payment.test.ts','tests/fixture-nexus-emperor-payment.ts','tests/nexus-emperor-payment-runtime.test.ts','docs/NEXUS_EMPEROR_RULES.md', 'tests/nexus-emperor-extra-source.test.ts', 'tests/nexus-emperor-extra-continuation.test.ts', 'tests/fixture-nexus-emperor-extra.ts', 'tests/nexus-emperor-extra-runtime.test.ts', 'tests/fixture-single-nexus-e1.ts', 'docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md', "tests/fixture-nexus-emperor-seller.ts", "tests/nexus-emperor-seller-purchase.test.ts", "tests/nexus-emperor-seller-runtime.test.ts", "tests/fixture-nexus-emperor-ambassador.ts", "tests/nexus-emperor-ambassador-purchase.test.ts", "tests/nexus-emperor-ambassador-runtime.test.ts", "tests/fixture-nexus-emperor-special.ts", "tests/nexus-emperor-special-purchase.test.ts", "tests/nexus-emperor-special-runtime.test.ts", "tests/karama-intents.test.ts", "tests/fixture-nexus-emperor-bank.ts", "tests/nexus-emperor-bank-profile.test.tsLine truncated
{area:"Implementation",status:'Partial',detail:"Earlier physical roles remain integrated. Native cross/return permission and native price now have distinct original unpaid stages; Cunning opportunity scope preserves its source. Independent purchased typed prices are bank-only, with exact completion/stop-card handling and no new route. Own/allied marker bills one unit; supplier approval cannot silently increase. Authorized recipient pricing now connects fresh human/AI offers. Legacy rate-only signatures remain; purchased Junction composition and full acceptance stay open.",evidence:['game/nexus-cards.ts', 'game/nexus-card-phase.ts', 'game/nexus-traitor-exchange.ts', 'game/traitor-declarations.ts', 'game/nexus-face-dancers.ts', 'game/nexus-suboids.ts', 'game/nexus-advisors.ts', 'game/nexus-sardaukar-options.ts', 'game/nexus-fremen-revival.ts', 'game/nexus-fremen-cunning.ts', 'game/nexus-ecaz-betrayal.ts', 'game/nexus-ecaz-inquiry.ts', 'game/nexus-ecaz-duke.ts', 'game/duke-vidal.ts', 'game/nexus-emperor-secret-ally.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal.ts', 'game/nexus-richese-options.ts', 'game/nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/nexus-guild-secret-ally-options.ts', 'game/nexus-module-profile.ts', 'game/engine.ts', 'game/nexus-tleilaxu-revival.ts', 'game/nexus-borrowed-voice.ts', 'game/combat-response-quote.ts', 'game/karama-context.ts', 'game/nexus-fremen-protection.ts', 'game/admin-seat-ai-shape.ts', 'game/richese-cancellation.ts', 'game/bot-mobility.ts', 'game/planetologist-movement.ts', 'game/revival.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally.ts', 'game/revival-resume.ts', 'game/revival-cancellation.ts', 'game/homeworld-revival-deployment.ts', 'game/homeworld-revival-return.ts', "game/nexus-emperor-purchase.ts", "game/auction-continuation-quote.ts", "game/nexus-emperor-purchase.ts", "game/ambassador-phase.ts", "game/ambassador-resume.ts", "game/battle-aftermath-quote.ts", "game/ecaz-duke-acquisition.ts", "game/ecaz-duke-revival.ts", "game/nexus-ecaz-duke-options.ts", "game/nexus-play-boundary.ts"]},
{area:"Player controls",status:'Partial',detail:"Five current390px journeys exercise original prevention then immune Junction/movement, actual gifted World3 for bank2, controlled supplier1 release and fresh full2 consent, controlled special-stop/refund then movement, and original typed permission allowance then rate cancellation. Fresh lender quote drift was observed and fixed with authoritative recipient pricing. Three original scenes/two controlled; no overflow/application exception, with one aborted initial Vite dependency request. Earlier surfaces remain historical.",evidence:['components/nexus-cards.tsx', 'components/nexus-traitors.tsx', 'components/nexus-tleilaxu.tsx', 'components/nexus-suboids.tsx', 'components/nexus-advisors.tsx', 'components/nexus-sardaukar.tsx', 'components/choam-power-cost.tsx', 'components/moritani-terror.tsx', 'components/nexus-moritani-betrayal.tsx', 'components/nexus-ecaz-betrayal.tsx', 'components/nexus-ecaz-inquiry.tsx', 'components/nexus-ecaz-duke.tsx', 'components/nexus-richese-betrayal.tsx', 'components/game-table.tsx', 'components/nexus-tleilaxu-revival.tsx', 'components/battle-preparation.tsx', 'components/nexus-fremen-revival.tsx','components/nexus-emperor-payment.tsx', 'components/nexus-emperor-secret-ally.tsx', "components/nexus-emperor-seller-purchase.tsx", "components/nexus-emperor-ambassador-purchase.tsx", "components/richese-special-karama.tsx"]},
{area:"AI",status:'Partial',detail:"Native permission denial filters native routes but preserves legal ordinary alternatives; all four original policies execute them. Typed budgets retain independent purchased price without granting a permission. Fresh allied offer calculations use the authorized recipient quote, not a personal-view guess. Four controlled low-budget probes choose no optional reoffer and complete legal recipient shipments. No strategic first-candidate pin, tuning or calibration.",evidence:['game/nexus-card-options.ts', 'game/nexus-traitor-options.ts', 'game/nexus-tleilaxu-options.ts', 'game/nexus-suboid-options.ts', 'game/nexus-advisor-options.ts', 'game/nexus-sardaukar-options.ts', 'game/choam-power-options.ts', 'game/nexus-moritani-options.ts', 'game/nexus-moritani-betrayal-options.ts', 'game/nexus-ecaz-betrayal-options.ts', 'game/nexus-ecaz-inquiry-options.ts', 'game/nexus-ecaz-duke-options.ts', 'game/nexus-richese-options.ts', 'game/bot-nexus-richese-betrayal.ts', 'game/nexus-guild-cunning-options.ts', 'game/bots.ts', 'game/nexus-fremen-revival-options.ts','game/nexus-emperor-payment.ts', 'game/nexus-emperor-secret-ally-options.ts']},
{area:"Documentation",status:'Partial',detail:"Supplied authorized Advanced PDF physical16 supports any-player half-normal bank-paid shipment. Distinct native permission, independent typed price and one-marker loan approvals are documented. Original-opportunity denial scope and prepayment ordering are explicit conservative product choices, not new publisher timing rulings; older refund/bank-loan/return policies remain provisional. Original versus controlled evidence and the remaining purchased-Junction composition/public gates stay explicit.",evidence:['docs/NEXUS_CARD_RULES.md', 'docs/NEXUS_ATREIDES_RULES.md', 'docs/NEXUS_HARKONNEN_RULES.md', 'docs/NEXUS_TLEILAXU_RULES.md', 'docs/NEXUS_IXIAN_CUNNING_RULES.md', 'docs/NEXUS_CUNNING_RUNTIME.md', 'docs/NEXUS_BENE_GESSERIT_RULES.md', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'docs/NEXUS_EMPEROR_RULES.md', 'docs/NEXUS_SARDAUKAR_RUNTIME.md', 'docs/NEXUS_FREMEN_RULES.md', 'docs/NEXUS_ECAZ_RULES.md', 'docs/NEXUS_CHOAM_RULES.md', 'docs/NEXUS_CHOAM_RUNTIME.md', 'docs/NEXUS_MORITANI_RULES.md', 'docs/NEXUS_MORITANI_RUNTIME.md', 'docs/NEXUS_RICHESE_RUNTIME.md', 'docs/NEXUS_RICHESE_RULES.md', 'docs/NEXUS_GUILD_CUNNING_RUNTIME.md', 'docs/GUILD_RATE_KARAMA_RULES.md', 'docs/RICHESE_ALLIED_SHIPMENT_AUDIT.md', 'docs/GUILD_HOMEWORLD_TRANSPORT_RULES.md', 'docs/NEXUS_GUILD_RULES.md', "docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md", "docs/RULE_DECISIONS.md", "docs/RICHESE_AMBASSADOR_RULES.md", "docs/RICHESE_AMBASSADOR_IMPLEMENTATION.md", "docs/RICHESE_ACQUISITION_RULES.md", "docs/DUKE_VIDAL_RULES.md", "docs/ECAZ_DUKE_ACQUISITION.md", "docs/ECAZ_REVIVAL_RULES.md"]},
{area:"Verification",status:'Partial',detail:"Current affected25-file263/263 passes across frozen237+26 selections. Twenty main runtime/policy paths pass:15 original-action/five controlled, plus four explicitly controlled optional-offer/recipient probes. Five actual390px journeys prove distinct permission/price, bank-only gift, fresh supplier consent, exact refund and original movement. Controlled Fremen typed/free and BG conversion/counter cases retain original sources. Earlier lifecycle evidence stays historical; no saved/deployed or complete-module assurance.",evidence:['tests/mixed-nexus-modules-runtime.test.ts', 'tests/paired-e3-nexus-modules-runtime.test.ts', 'tests/sample-custody.test.ts', 'tests/ambassador-terror-overlap-engine.test.ts', 'tests/single-nexus-e1-runtime.test.ts', 'tests/single-nexus-e2-runtime.test.ts', 'tests/e3-nexus-ecaz-runtime.test.ts', 'tests/e3-nexus-moritani-runtime.test.ts', 'tests/homeworld-classic-nexus-skills-runtime.test.ts', 'tests/homeworld-classic-discovery-nexus-runtime.test.ts', 'tests/nexus-cards.test.ts', 'tests/nexus-card-engine.test.ts', 'tests/nexus-fremen-cunning.test.ts', 'tests/nexus-fremen-cunning-recovery.test.ts', 'tests/nexus-fremen-cunning-summon.test.ts', 'tests/nexus-fremen-cunning-summon-recovery.test.ts', 'tests/nexus-ecaz-betrayal-quotes.test.ts', 'tests/nexus-ecaz-betrayal.test.ts', 'tests/nexus-ecaz-betrayal-recovery.test.ts', 'tests/nexus-ecaz-inquiry.test.ts', 'tests/nexus-ecaz-duke-quote.test.ts', 'tests/nexus-ecaz-duke-engine.test.ts', 'tests/nexus-ecaz-duke-recovery.test.ts', 'tests/nexus-traitor-recovery.test.ts', 'tests/nexus-face-dancer-engine.test.ts', 'tests/nexus-suboids-engine.test.ts', 'tests/normal-free-support.test.ts', 'tests/nexus-suboid-controls.test.ts', 'tests/nexus-advisor-controls.test.ts', 'tests/nexus-advisor-bots.test.ts', 'tests/nexus-sardaukar-controls.test.ts', 'tests/nexus-sardaukar-bots.test.ts', 'tests/nexus-choam-controls.test.ts', 'tests/nexus-choam-bots.test.ts', 'tests/nexus-moritani-controls.test.ts', 'tests/nexus-moritani-bots.test.ts', 'tests/nexus-moritani-betrayal.test.ts', 'tests/nexus-richese-controls.test.ts', 'tests/nexus-richese-bots.test.ts', 'tests/nexus-richese-betrayal-engine.test.ts', 'tests/nexus-richese-betrayal-recovery.test.ts', 'tests/nexus-guild-cunning-controls.test.ts', 'tests/native-guild-rate-karama.test.ts', 'tests/native-guild-rate-modules.test.ts', 'tests/allied-marker-guild-rate.test.ts', 'tests/purchased-homeworld-rate.test.ts', 'tests/native-guild-transport-permission.test.ts', 'tests/native-guild-homeworld-rate.test.ts', 'tests/native-guild-rate-betrayal.test.ts', 'tests/nexus-guild-cunning-engine.test.ts', 'tests/nexus-guild-cunning-bots.test.ts', 'tests/nexus-module-payments-runtime.test.ts', 'tests/nexus-module-battles-runtime.test.ts', 'tests/paired-ix-nexus-modules-runtime.test.ts', 'tests/paired-choam-nexus-modules-runtime.test.ts', 'tests/classic-nexus-skills-payments-runtime.test.ts', 'tests/classic-nexus-skills-battles-runtime.test.ts', 'tests/nexus-skills-modules-payments-runtime.test.ts', 'tests/nexus-skills-modules-battles-runtime.test.ts', 'tests/paired-ix-nexus-skills-modules-runtime.test.ts', 'tests/paired-choam-nexus-skills-modules-runtime.test.ts', 'tests/ecaz-nexus-skills-modules-runtime.test.ts', 'tests/moritani-nexus-skills-modules-runtime.test.ts', 'docs/NEXUS_CARD_RULES.md', 'tests/nexus-tleilaxu-revival.test.ts', 'tests/fixture-nexus-tleilaxu-secret-ally.ts', 'tests/nexus-tleilaxu-secret-ally-runtime.test.ts', 'tests/nexus-borrowed-voice.test.ts', 'tests/fixture-nexus-borrowed-voice.ts', 'tests/nexus-borrowed-voice-runtime.test.ts', 'tests/nexus-fremen-protection.test.ts', 'tests/fixture-nexus-fremen-protection.ts', 'tests/nexus-fremen-protection-runtime.test.ts', 'tests/fixture-stronghold-factions.ts', 'tests/fixture-nexus-advisor-fresh.ts', 'tests/nexus-advisor-fresh-runtime.test.ts', 'tests/fixture-nexus-advisor-storm.ts', 'tests/nexus-advisor-storm-runtime.test.ts', 'docs/NEXUS_ADVISOR_RUNTIME.md', 'tests/nexus-fremen-free-quota.test.ts', 'tests/fixture-nexus-fremen-free-quota.ts', 'tests/nexus-fremen-free-quota-runtime.test.ts', 'tests/nexus-fremen-revival.test.ts','tests/nexus-emperor-payment.test.ts','tests/fixture-nexus-emperor-payment.ts','tests/nexus-emperor-payment-runtime.test.ts','docs/NEXUS_EMPEROR_RULES.md', 'tests/nexus-emperor-extra-source.test.ts', 'tests/nexus-emperor-extra-continuation.test.ts', 'tests/fixture-nexus-emperor-extra.ts', 'tests/nexus-emperor-extra-runtime.test.ts', 'tests/fixture-single-nexus-e1.ts', 'docs/NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md', "tests/fixture-nexus-emperor-seller.ts", "tests/nexus-emperor-seller-purchase.test.ts", "tests/nexus-emperor-seller-runtime.test.ts", "tests/fixture-nexus-emperor-ambassador.ts", "tests/nexus-emperor-ambassador-purchase.test.ts", "tests/nexus-emperor-ambassador-runtime.test.ts", "tests/fixture-nexus-emperor-special.ts", "tests/nexus-emperor-special-purchase.test.ts", "tests/nexuLine truncated
],
},
{
+1
View File
@@ -123,6 +123,7 @@ const RESPONSES = {
harkonnenTraitor: true,
guildIncome: true,
guildRate: true,
guildTransportPermission: true,
emperorGift: true,
emperorRevival: true,
stormPeek: true,
+15 -4
View File
@@ -32,13 +32,24 @@ const discountedAlliedNoFieldPricing = Object.freeze({
policy: 'Provisional pricing: a Guild recipient or already-active Karama rate uses the rounded one-force Guild fee (1 spice), paid entirely to the bank. This price/payee precedence is not an adjudicated publisher ruling.',
});
const canceledGuildAlliedNoFieldPricing = Object.freeze({
stronghold: 1, other: 2, bankOnly: true,
policy: 'Provisional pricing: canceling a Guild recipient’s native rate charges the full one-force fee, still paid entirely to the bank. This price/payee precedence is not an adjudicated publisher ruling.',
});
/** Shared first-version price/payee policy for Basic and Advanced. */
export function alliedNoFieldShipmentPricing(
context: { advanced: boolean; karamaShipping?: { player: string } | null },
recipient: { id: string; faction: FactionId },
context: {
purchasedGuildRate: boolean;
nativeGuildRateCanceled: boolean;
},
recipient: { faction: FactionId },
) {
return recipient.faction === 'guild' || context.karamaShipping?.player === recipient.id
? discountedAlliedNoFieldPricing : ordinaryAlliedNoFieldPricing;
if (context.purchasedGuildRate)
return discountedAlliedNoFieldPricing;
if (recipient.faction !== 'guild') return ordinaryAlliedNoFieldPricing;
return context.nativeGuildRateCanceled
? canceledGuildAlliedNoFieldPricing : discountedAlliedNoFieldPricing;
}
/** A pledge is already escrowed. Selecting a share never spends a donor's
+2
View File
@@ -18,6 +18,8 @@ export function guildTransportQuote(g: GameView, action: Action) {
action.nexus === g.nexusGuildSecretAlly?.event;
if (nexus && !nexusAllowed)
unavailableReasons.push('Choose a current Guild Secret Ally offer.');
if (g.guildTransportCanceled && !nexusAllowed)
unavailableReasons.push('Karama prevents native Guild cross/return transport for this shipment opportunity.');
const fromReserves = action.from === 'reserves';
let total = 0;
let origin: string | undefined;