'use client'; import type { Action, GameView } from '@/game/engine'; import { allianceClaimText, alliancePromiseModeSupported } from '@/game/alliance-promises'; import { evaluateAllianceExpression } from '@/game/alliance-expression-promises'; import { quoteNexusAlliance } from '@/game/nexus-alliance'; import { Button } from './ui/button'; type AlliancePromiseProps = { game: GameView; busy: boolean; send: (action: Action) => void }; const publicContext = (game: GameView) => ({ players: game.players.map(player => ({ id: player.id, ally: player.ally })), offers: game.allianceOffers, }); /** A guide may end a pair, accept real consent or confirm current membership. * It never sends a proposal on another player's behalf. */ export function AlliancePromiseGuideButton(props: AlliancePromiseProps) { const game = props.game, guide = game.alliancePromiseGuide, nexus = game.alliancePromiseNexus; if (!guide || !nexus) return null; const actor = game.me, turn = nexus.turn, action = guide.action; const event = action.alliancePromiseEvent, signature = JSON.stringify(action); const contextSignature = JSON.stringify(publicContext(game)); const canAct = () => { const current = props.game, me = current.players.find(player => player.id === current.me); const live = current.alliancePromises.filter(promise => promise.player === actor && promise.turn === turn && !promise.fulfilled && !promise.released); const pure = current.allianceExpressionPromises.filter(promise => promise.player === actor && promise.turn === turn && !promise.fulfilled && !promise.released); const mixed = current.mixedAlliancePromises.filter(promise => promise.player === actor && promise.turn === turn && !promise.fulfilled && !promise.released); // Pure membership is visible. The server alone knows a mixed question's // frozen facts and reduced future obligation; never reconstruct it here. const matches = (ally: string | null) => live.every(promise => (ally === promise.partner) === promise.answer) && pure.every(promise => evaluateAllianceExpression(promise.claim, ally) === promise.answer); let ownedAction = action.type === 'ready' && !!me && matches(me.ally); if (action.type === 'alliance') { if (action.target === null) ownedAction = true; else if (typeof action.target === 'string' && action.target !== actor && current.players.some(player => player.id === action.target)) { try { const quote = quoteNexusAlliance({ ...publicContext(current), actor, target: action.target }); ownedAction = !!quote.formed && matches(quote.allies[actor]); } catch { ownedAction = false; } } } return !props.busy && current === game && alliancePromiseModeSupported(current) && current.status === 'playing' && current.phase === 1 && current.nexus && !current.spiceWindow && !current.ready.includes(actor) && current.me === actor && current.turn === turn && current.alliancePromiseNexus?.turn === turn && current.alliancePromiseGuide === guide && current.alliancePromiseGuide.action === action && JSON.stringify(action) === signature && JSON.stringify(publicContext(current)) === contextSignature && typeof event === 'string' && [...live, ...pure, ...mixed].some(promise => promise.event === event) && ownedAction && !!me && !me.autopilot && !current.truthtrance && !current.response && !current.decision && !current.phaseOpening && !current.automaticContinuationPending && !current.roomControl?.paused && !current.roomControl?.closed; }; const me = game.players.find(player => player.id === actor); const partner = game.players.find(player => player.id === action.target)?.name ?? 'the named partner'; return ; } export function AlliancePromises(props: AlliancePromiseProps) { const own = props.game.alliancePromises.filter(promise => promise.player === props.game.me); if (!own.length) return null; const live = own.some(promise => promise.turn === props.game.alliancePromiseNexus?.turn && !promise.fulfilled && !promise.released); const name = (id: string) => props.game.players.find(player => player.id === id)?.name ?? 'the named partner'; return

Your alliance promises

The answer concerns your final actual alliance membership at this Nexus close. Yes remains live after forming an alliance and settles only at the close. No requires ending without the named partner; a temporary alliance can still be ended before the close. Offers alone are not alliances.

An owned Yes route needs an existing alliance or an actual reciprocal invitation. Neither answer promises another player’s consent or prevents their original actions. If no owned route exists, No or unknown may apply; the holder can ask again or save the card. Honor any earlier promises first; their own controls handle required payments. {' '}Alliance promise rules.

{live && }
; }