Add final Nexus alliances to combined turn commitments
This commit is contained in:
1 parent
921b4fe1e3
commit
1a79fd3131
15 files changed
+1143
-78
No files matched your search
@@ -581,7 +581,7 @@ export function Truthtrance({
|
||||
<option value="mixedBid">Combine current facts and ordinary bids</option>
|
||||
<option value="traitorCall">Bind this battle’s Traitor declaration</option>
|
||||
<option value="mixedTraitorCall">Combine current facts and this Traitor declaration</option>
|
||||
<option value="turnActions">Combine this turn’s bids, Revival, payments, shipment and ground movements</option>
|
||||
<option value="turnActions">Combine this Nexus’s alliances with this turn’s bids, Revival, payments, shipment and ground movements</option>
|
||||
<option value="travel">Combine this turn’s shipment and ground movements</option>
|
||||
<option value="shipment">
|
||||
Bind a shipment from reserves
|
||||
@@ -602,7 +602,7 @@ export function Truthtrance({
|
||||
<>
|
||||
<TurnActionFields value={turnActions} game={g} target={target}
|
||||
disabled={busy || !g.truthTurnActionTargets.includes(target)} onChange={setTurnActions} />
|
||||
{!g.truthTurnActionTargets.includes(target) && <p className="notice">Choose a player with a supported ordinary bid, Revival, payment, shipment or ground-movement opportunity this turn.
|
||||
{!g.truthTurnActionTargets.includes(target) && <p className="notice">Choose a player with a supported alliance, ordinary bid, Revival, payment, shipment or ground-movement opportunity this turn.
|
||||
Ordinary payments may remain available after their Shipment and Movement opportunity ends.</p>}
|
||||
<p className="notice" aria-label="Public turn-action question preview">{turnActionsError ?? truthQuestionText(question, leaderName, locationName, name)}</p>
|
||||
</>
|
||||
|
||||
@@ -20,11 +20,16 @@ const leaders = (game: GameView, target: string) => {
|
||||
...(game.advanced && faction === 'atreides' ? [{ id: 'kwisatz', name: 'Kwisatz Haderach' }] : [])];
|
||||
};
|
||||
const currentLot = (game: GameView) => game.bidPromiseLot?.turn === game.turn ? game.bidPromiseLot : null;
|
||||
const partners = (game: GameView, target: string) => game.players.some(player => player.id === target)
|
||||
? game.players.filter(player => player.id !== target) : [];
|
||||
const currentNexus = (game: GameView, target: string) => game.status === 'playing' && game.phase === 1 && game.nexus && !game.spiceWindow &&
|
||||
game.alliancePromiseNexus?.turn === game.turn && game.truthMixedAllianceTargets.includes(target) &&
|
||||
!game.response && !game.decision && !game.phaseOpening && !game.automaticContinuationPending;
|
||||
const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
prefix: 'turn-action',
|
||||
legend: 'Future bids, Revival, payments, shipment and ground movements, with optional current facts',
|
||||
actionLabel: 'bid, Revival, payment, shipment or ground-movement condition',
|
||||
actionTiming: 'Future ordinary bid, Revival, payment, reserve shipment or ground move · this turn',
|
||||
legend: 'Future alliances, bids, Revival, payments, shipment and ground movements, with optional current facts',
|
||||
actionLabel: 'alliance, bid, Revival, payment, shipment or ground-movement condition',
|
||||
actionTiming: 'Future alliance, ordinary bid, Revival, payment, reserve shipment or ground move · this turn',
|
||||
actionHelp: <>Ordinary payment conditions sum future real bribes to the same recipient after the answer this turn.
|
||||
Earlier payments, gifts and ally aid do not count. The sender pays immediately; the recipient collects at the next Mentat opening.
|
||||
Payments made during Mentat Pause wait until next turn's Mentat opening; collection never counts twice.
|
||||
@@ -33,13 +38,16 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
Ordinary Revival conditions count cumulative future normal and elite physical force returns, with each counter counted once,
|
||||
or an actual future return of a named native leader. Smaller ordinary force returns add together.
|
||||
Earlier returns, requests alone, Ghola, foreign leaders and special card returns do not count.
|
||||
Alliance conditions concern actual final membership when this current Nexus closes. Offers alone are not alliances.
|
||||
A remaining alliance condition settles at the actual close; forming an alliance does not fulfill it early.
|
||||
No can allow temporary membership followed by a break before the close. You cannot promise another player's consent.
|
||||
Shipment conditions concern the single future original reserve shipment after the answer.
|
||||
Ground-movement conditions use the maximum physical forces in one actual future move to that destination; smaller moves never add together.
|
||||
Bid conditions close with their named ordinary lot; Revival conditions close with your native Revival opportunity.
|
||||
Shipment and movement close at your own Movement turn end, while payment conditions last until the actual turn ends.
|
||||
AND requires every condition; OR keeps legal alternatives. No makes the whole expression false, not every condition separately.
|
||||
Original costs, timing, cards, preparation and action rights still apply; this claim grants no extra action.</>,
|
||||
fallbackError: 'Choose a future ordinary bid, Revival, payment, reserve shipment or ground-movement condition, with optional current facts.',
|
||||
fallbackError: 'Choose a future alliance, ordinary bid, Revival, payment, reserve shipment or ground-movement condition, with optional current facts.',
|
||||
maxLeaves: TURN_ACTION_MAX_LEAVES,
|
||||
maxDepth: TURN_ACTION_MAX_DEPTH,
|
||||
requireFact: false,
|
||||
@@ -52,16 +60,19 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
throw new Error('Choose the current public ordinary lot for the future bid.');
|
||||
if (action.kind === 'leader' && !leaders(game, target).some(leader => leader.id === action.leader))
|
||||
throw new Error('Choose a public native leader for the respondent’s faction.');
|
||||
if (action.kind === 'alliance' && (!currentNexus(game, target) || !partners(game, target).some(partner => partner.id === action.partner)))
|
||||
throw new Error('Choose a public other partner during the current open Nexus.');
|
||||
return action;
|
||||
},
|
||||
renderAction: ({ value, id, game, target, disabled, onChange }) => {
|
||||
const choices = recipients(game, target), leaderChoices = leaders(game, target), lot = currentLot(game);
|
||||
const choices = recipients(game, target), leaderChoices = leaders(game, target), lot = currentLot(game), partnerChoices = partners(game, target);
|
||||
return <>
|
||||
<label htmlFor={`${id}-action`}>Future action</label>
|
||||
<select id={`${id}-action`} value={value.kind} disabled={disabled} onChange={event => {
|
||||
const kind = event.currentTarget.value;
|
||||
if (kind === value.kind) return;
|
||||
const minimum = value.kind === 'leader' ? 1 : value.minimum;
|
||||
const minimum = value.kind === 'leader' || value.kind === 'alliance' ? 1 : value.minimum;
|
||||
if (kind === 'alliance') onChange({ kind, partner: '' });
|
||||
if (kind === 'payment') onChange({ kind, recipient: '', minimum });
|
||||
if (kind === 'bid') onChange({ kind, lot: lot?.lot ?? NaN, minimum });
|
||||
if (kind === 'forces') onChange({ kind, counter: 'total', minimum });
|
||||
@@ -69,6 +80,7 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
if (kind === 'ship' || kind === 'move') onChange({ kind,
|
||||
territory: value.kind === 'ship' || value.kind === 'move' ? value.territory : 'carthag', minimum });
|
||||
}}>
|
||||
<option value="alliance">End this Nexus allied with a named partner</option>
|
||||
<option value="bid">Place an ordinary-lot bid</option>
|
||||
<option value="forces">Revive ordinary physical forces</option>
|
||||
<option value="leader">Revive a named ordinary leader</option>
|
||||
@@ -76,7 +88,18 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
<option value="ship">Ship from reserves</option>
|
||||
<option value="move">Make ground movement</option>
|
||||
</select>
|
||||
{value.kind === 'bid' ? <>
|
||||
{value.kind === 'alliance' ? <>
|
||||
<label htmlFor={`${id}-partner`}>Final Nexus alliance partner</label>
|
||||
<select id={`${id}-partner`} value={partnerChoices.some(partner => partner.id === value.partner) ? value.partner : ''}
|
||||
disabled={disabled} aria-invalid={!partnerChoices.some(partner => partner.id === value.partner)} onChange={event => {
|
||||
const partner = event.currentTarget.value;
|
||||
if (partner === '' || partnerChoices.some(choice => choice.id === partner)) onChange({ ...value, partner });
|
||||
}}>
|
||||
<option value="">Choose a partner</option>
|
||||
{partnerChoices.map(partner => <option key={partner.id} value={partner.id}>{partner.name}</option>)}
|
||||
</select>
|
||||
{!currentNexus(game, target) && <p className="fine">Alliance conditions require the current open Nexus.</p>}
|
||||
</> : value.kind === 'bid' ? <>
|
||||
<label htmlFor={`${id}-lot`}>Current ordinary lot</label>
|
||||
<select id={`${id}-lot`} value={lot?.lot === value.lot ? String(value.lot) : ''} disabled={disabled}
|
||||
aria-invalid={lot?.lot !== value.lot} onChange={event => {
|
||||
@@ -128,7 +151,7 @@ const config: MixedActionConfig<TurnActionLeaf> = {
|
||||
{TERRITORIES.map(destination => <option key={destination.id} value={destination.id}>{destination.name}</option>)}
|
||||
</select>
|
||||
</>}
|
||||
{value.kind !== 'leader' && <>
|
||||
{value.kind !== 'leader' && value.kind !== 'alliance' && <>
|
||||
<label htmlFor={`${id}-minimum`}>{value.kind === 'payment' ? 'Minimum future ordinary payments in spice'
|
||||
: value.kind === 'bid' ? 'Minimum future accepted bid in spice'
|
||||
: value.kind === 'forces' ? 'Minimum future ordinary force returns'
|
||||
|
||||
@@ -2,12 +2,17 @@
|
||||
|
||||
import type { Action, GameView } from '@/game/engine';
|
||||
import { bribeAction } from '@/game/bribe-options';
|
||||
import { alliancePromiseModeSupported } from '@/game/alliance-promises';
|
||||
import { nativePromiseScopeSupported } from '@/game/native-promise-scope';
|
||||
import { quoteNexusAlliance } from '@/game/nexus-alliance';
|
||||
import { truthQuestionText, type TruthAnswer } from '@/game/truthtrance';
|
||||
import { CHEAP_HERO_TRAITOR } from '@/game/traitors';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
type TurnActionProps = { game: GameView; busy: boolean; act: (action: Action) => void };
|
||||
const publicAllianceContext = (game: GameView) => ({
|
||||
players: game.players.map(player => ({ id: player.id, ally: player.ally })), offers: game.allianceOffers,
|
||||
});
|
||||
|
||||
/** One projected native packet; no client branch selection or completion solver. */
|
||||
export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
@@ -28,6 +33,8 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
passed: current.richeseAuction.passed, acted: current.richeseAuction.acted,
|
||||
});
|
||||
const auctionSignature = auctionSource(game), nativeSignature = nativeSource(game), orderSignature = JSON.stringify(order);
|
||||
const nexus = game.alliancePromiseNexus, nexusOpen = game.nexus, nexusSpiceWindow = game.spiceWindow;
|
||||
const nexusSignature = JSON.stringify({ ...publicAllianceContext(game), ready: game.ready });
|
||||
const box = game.nullentropy, search = box?.search;
|
||||
const canAct = () => {
|
||||
const current = props.game, me = current.players.find(player => player.id === actor);
|
||||
@@ -61,7 +68,16 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
const revivalTurn = current.revival === revival && !current.revival.pending && !current.revival.orderBlocked &&
|
||||
current.revivalOrder === order && JSON.stringify(current.revivalOrder) === orderSignature &&
|
||||
(!order || order.turn === turn && order.remaining[0] === actor && current.active === actor);
|
||||
const opportunity = payment || paidBox || (phase === 2
|
||||
const nexusWindow = phase === 1 && nexusOpen && current.nexus === nexusOpen && !nexusSpiceWindow &&
|
||||
current.spiceWindow === nexusSpiceWindow && !!nexus &&
|
||||
current.alliancePromiseNexus === nexus && nexus.turn === turn && !current.ready.includes(actor) &&
|
||||
alliancePromiseModeSupported(current) && JSON.stringify({ ...publicAllianceContext(current), ready: current.ready }) === nexusSignature;
|
||||
let ownedAlliance = action.type === 'alliance' && action.target === null;
|
||||
if (action.type === 'alliance' && typeof action.target === 'string') {
|
||||
try { ownedAlliance = !!quoteNexusAlliance({ ...publicAllianceContext(current), actor, target: action.target }).formed; }
|
||||
catch { ownedAlliance = false; }
|
||||
}
|
||||
const opportunity = payment || paidBox || nexusWindow && (ownedAlliance || action.type === 'ready') || (phase === 2
|
||||
? !current.ready.includes(actor) && ['charity', 'ready', 'card'].includes(action.type)
|
||||
: phase === 3
|
||||
? invoice || nativePass || requiredBid || action.type === 'passBid' && !current.richeseAuction && currentAuction && current.auction?.active === actor
|
||||
@@ -70,7 +86,7 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
: phase === 5
|
||||
? current.active === actor && (action.type === 'guildShip' && action.from === 'reserves' ||
|
||||
['ship', 'move', 'endMovement', 'card', 'prepareAdvisors', 'revealNoField'].includes(action.type))
|
||||
: (phase === 1 && current.nexus && !current.spiceWindow || phase === 7 || phase === 8) &&
|
||||
: (phase === 7 || phase === 8) &&
|
||||
action.type === 'ready' && !current.ready.includes(actor));
|
||||
return !props.busy && current === game && nativePromiseScopeSupported(current) && current.status === 'playing' &&
|
||||
Number.isSafeInteger(phase) && phase >= 0 && phase <= 8 && current.phase === phase && current.turn === turn && current.me === actor &&
|
||||
@@ -88,9 +104,14 @@ export function TurnActionGuideButton(props: TurnActionProps) {
|
||||
const recipient = game.players.find(player => player.id === action.target)?.name ?? 'the quoted recipient';
|
||||
const sector = typeof action.sector === 'number' ? `, sector ${action.sector}` : '';
|
||||
const label = action.type === 'bribe' ? `Pay ${amount} spice to ${recipient} while honoring your whole answer`
|
||||
: action.type === 'alliance' ? action.target === null
|
||||
? game.players.find(player => player.id === actor)?.ally ? 'Break your current alliance while preserving your whole answer'
|
||||
: 'Withdraw your alliance offer while preserving your whole answer'
|
||||
: `Accept ${recipient}’s alliance invitation while preserving your whole answer`
|
||||
: action.type === 'charity' ? 'Claim Charity while preserving your whole answer'
|
||||
: action.type === 'ready' ? phase === 8 ? 'Ready to end this turn while honoring your whole answer'
|
||||
: 'Finish this preparation phase while preserving your whole answer'
|
||||
: phase === 1 ? 'Confirm Ready for this Nexus while preserving your whole answer'
|
||||
: 'Finish this preparation phase while preserving your whole answer'
|
||||
: action.type === 'passBid' ? 'Pass this bid while preserving your whole answer'
|
||||
: action.type === 'bid' ? `Place the required bid of ${amount} while preserving your whole answer`
|
||||
: action.type === 'richeseBid' ? action.amount === 0 ? 'Submit zero in this Silent auction while preserving your whole answer'
|
||||
@@ -119,7 +140,7 @@ export function TurnActionPromises(props: TurnActionProps) {
|
||||
if (!own.length) return null;
|
||||
const live = own.some(promise => promise.turn === game.turn && !promise.fulfilled && !promise.released);
|
||||
return <section className="notice space-y-3" aria-label="Your Truthtrance turn-action promises" id="turn-action-promise-guidance">
|
||||
<h3>Your bid, Revival, payment, shipment and movement promises</h3>
|
||||
<h3>Your alliance, bid, Revival, payment, shipment and movement promises</h3>
|
||||
<ul>{own.map(promise => <li key={promise.event}>
|
||||
Turn {promise.turn}: You answered {promise.answer ? 'Yes' : 'No'} to {truthQuestionText({ kind: 'turnActions', target: promise.player,
|
||||
turn: promise.turn, mixed: promise.mixed }, id => id === 'kwisatz' ? 'Kwisatz Haderach' : id === CHEAP_HERO_TRAITOR ? 'Cheap Hero / Heroine'
|
||||
@@ -141,6 +162,9 @@ export function TurnActionPromises(props: TurnActionProps) {
|
||||
Force Revival conditions count cumulative future ordinary returns; normal and elite physical counters each count once.
|
||||
Named leader conditions require an actual future ordinary native return. Earlier returns, requests alone, Ghola,
|
||||
foreign leaders and special card returns do not count.
|
||||
Alliance conditions concern final actual membership at this Nexus close. Offers alone are not alliances.
|
||||
A remaining alliance condition settles only at the actual close; forming an alliance does not fulfill it early.
|
||||
No can allow temporary membership followed by a break before the close. Neither answer promises another player’s consent.
|
||||
Shipment conditions concern one future original reserve shipment. Ground-movement conditions count the maximum physical forces in one actual future move,
|
||||
never the sum of smaller moves. Earlier shipment and movement do not count.
|
||||
Bid conditions close with their named ordinary lot; Revival conditions close with your native Revival opportunity.
|
||||
@@ -170,11 +194,13 @@ export function TurnActionAnswers(props: TurnActionProps) {
|
||||
!!me && !me.autopilot && !current.response && !current.decision && !current.phaseOpening &&
|
||||
!current.automaticContinuationPending && !current.roomControl?.paused && !current.roomControl?.closed;
|
||||
};
|
||||
return <section aria-label="Answer the whole bid, Revival, payment, shipment and movement question" className="space-y-3">
|
||||
return <section aria-label="Answer the whole alliance, bid, Revival, payment, shipment and movement question" className="space-y-3">
|
||||
<p className="notice">Answer the whole expression. Current facts freeze now; OR keeps legal alternatives and No keeps the whole expression false.
|
||||
Future real ordinary payments add together per recipient. Only one future original reserve shipment and actual future ground-movement groups count.
|
||||
Ordinary bids use the maximum future accepted declaration in the named current lot; ordinary force returns add together,
|
||||
while named leaders must actually return through ordinary native Revival.
|
||||
Alliance conditions use actual final membership when the current Nexus closes; offers alone do not count.
|
||||
A remaining alliance condition stays open through temporary membership until that close. Neither answer promises another player’s consent.
|
||||
Bids close with their lot; Revival closes with your native Revival opportunity.
|
||||
Shipment and movement close at your own Movement turn end; payment conditions last until the actual turn ends.
|
||||
Original rights, preparation, timing and costs still apply. Only your whole answer is shared.</p>
|
||||
|
||||
Reference in new issue
Block a user