Honor group alliances in Truthtrance
Verify and publish checkpoint container / container (push) Successful in 11m22s
Verify and publish checkpoint container / container (push) Successful in 11m22s
This commit is contained in:
1 parent
9cb65a21f5
commit
3e0fa2a8a2
11 files changed
+592
-24
No files matched your search
@@ -2,15 +2,25 @@
|
||||
|
||||
import type { Action, GameView } from '@/game/engine';
|
||||
import { allianceClaimText, alliancePromiseModeSupported } from '@/game/alliance-promises';
|
||||
import { evaluateAllianceExpression } from '@/game/alliance-expression-promises';
|
||||
import { evaluateAllianceExpression, type AllianceExpression } from '@/game/alliance-expression-promises';
|
||||
import { coalitionAllies, coalitionOf } from '@/game/coalition-alliance';
|
||||
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,
|
||||
players: game.players.map(player => ({ id: player.id, ally: player.ally, allySinceTurn: player.allySinceTurn })),
|
||||
offers: game.allianceOffers, coalitionState: game.coalitionState,
|
||||
});
|
||||
|
||||
/** Group membership is public. Evaluate only pure expressions here: mixed
|
||||
* questions retain their privately compiled facts on the server. */
|
||||
function expressionMatchesMembers(expression: AllianceExpression, allies: readonly string[]): boolean {
|
||||
if (expression.kind === 'partner') return allies.includes(expression.partner);
|
||||
return expression.kind === 'and' ? expression.terms.every(term => expressionMatchesMembers(term, allies))
|
||||
: expression.terms.some(term => expressionMatchesMembers(term, allies));
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
@@ -29,15 +39,27 @@ export function AlliancePromiseGuideButton(props: AlliancePromiseProps) {
|
||||
!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);
|
||||
const matches = (allies: readonly string[]) => live.every(promise => allies.includes(promise.partner) === promise.answer) &&
|
||||
pure.every(promise => (allies.length <= 1 ? evaluateAllianceExpression(promise.claim, allies[0] ?? null)
|
||||
: expressionMatchesMembers(promise.claim, allies)) === promise.answer);
|
||||
let group: ReturnType<typeof coalitionOf> = null, allies = me?.ally ? [me.ally] : [];
|
||||
try {
|
||||
if (current.coalitionState && me) {
|
||||
group = coalitionOf(current.players, current.coalitionState, actor);
|
||||
allies = coalitionAllies(current.players, current.coalitionState, actor);
|
||||
}
|
||||
} catch { return false; }
|
||||
let ownedAction = action.type === 'ready' && !!me && matches(allies);
|
||||
if (action.type === 'coalitionLeave')
|
||||
ownedAction = !!group && group.members.length >= 3 && !!current.coalitionState &&
|
||||
current.coalitionState.proposal === null && action.expectedRevision === current.coalitionState.revision && matches([]);
|
||||
if (action.type === 'alliance') {
|
||||
if (action.target === null) ownedAction = true;
|
||||
if ((group?.members.length ?? 0) >= 3) ownedAction = false;
|
||||
else 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]);
|
||||
ownedAction = !!quote.formed && matches(quote.allies[actor] ? [quote.allies[actor]!] : []);
|
||||
} catch { ownedAction = false; }
|
||||
}
|
||||
}
|
||||
@@ -55,6 +77,7 @@ export function AlliancePromiseGuideButton(props: AlliancePromiseProps) {
|
||||
return <Button className="game-action min-h-11 whitespace-normal" data-testid="alliance-promise-guide"
|
||||
disabled={!canAct()} onClick={() => { if (canAct()) props.send(action); }}>
|
||||
{action.type === 'ready' ? 'Confirm Ready for this Nexus while honoring your answers'
|
||||
: action.type === 'coalitionLeave' ? 'Leave your group alliance while honoring your answers'
|
||||
: action.target === null ? me?.ally ? 'Break your current alliance while honoring your answers'
|
||||
: 'Withdraw your alliance offer while honoring your answers'
|
||||
: `Accept ${partner}’s alliance invitation while honoring your answers`}
|
||||
|
||||
Reference in new issue
Block a user