Files
Dune/game/nexus-fremen-betrayal.ts
T
Quintine 8422cf1f2d Compose selected E1/E2 independent cards with Nexus rules
Retain selected decks and original optional modules through normal setup, distinguish native Ixian Betrayal from absent-faction replacement, route explicit CHOAM fuel before printed effects, and preserve native No-Field invoices and pair continuation. Connect canonical native cancellation stock and existing human/minimal AI consumers.

Validation: 194 affected tests across 19 files, types/lint/build, bounded native invoice review and seven finite original-action/rendered-control programmes. Runtime ec5ad6c18996d8d958ec0d01132ef3e3ea6d23d36188902eca9a63d4db2efbf8; documentation-only proof overlay a5892004937434429916496497c07a3838a324b4a5d64d4a0eb804ab5212d5bd. Other envelopes, full combinations, comprehensive assurance and deployed acceptance remain open. No saved-room/provider changes.
2026-10-18 10:46:21 +11:00

86 lines
4.3 KiB
TypeScript

import type { Game } from './engine';
import { nexusCleanPlayBlocked } from './nexus-play-boundary';
import { nexusCoreProfile } from './faction-module-profile';
export type FremenNexusBetrayal = {
event: string;
owner: string;
target: string;
turn: number;
phase: 1 | 5;
mode: 'worm' | 'movement';
signature: string;
};
/** The first movement-only checkpoint wrote this six-field physical receipt. */
export type StoredFremenNexusBetrayal = FremenNexusBetrayal |
(Omit<FremenNexusBetrayal, 'mode' | 'phase'> & { phase: 5; mode?: never });
export type FremenBetrayalOffer = {
event: string;
blocked: string | null;
target: string;
mode: FremenNexusBetrayal['mode'];
};
export const fremenBetrayalEvent = (
turn: number, owner: string, target: string, mode: FremenNexusBetrayal['mode'],
) => JSON.stringify(['nexusFremenBetrayal', turn, owner, target, mode]);
export const fremenBetrayalSignature = (record: FremenNexusBetrayal) =>
JSON.stringify({ ...record, signature: undefined });
/** Clean advance declarations avoid a holder-dependent worm interruption
* while preserving both mutually exclusive printed alternatives. */
export function fremenBetrayalOffer(
g: Game, owner: string, automaticPending = false,
): FremenBetrayalOffer | null {
const holder = g.players.find(p => p.id === owner);
const fremen = g.players.find(p => p.faction === 'fremen');
if (!holder || !fremen || g.nexusCards?.cards?.hands[owner] !== 'fremen' ||
holder.faction === 'fremen') return null;
const mode = g.phase === 1 ? 'worm' : 'movement';
let blocked: string | null = null;
if (holder.ally) blocked = 'An allied player cannot use a Nexus card.';
else if (g.status !== 'playing' || (g.phase !== 1 && g.phase !== 5))
blocked = 'Use Fremen Betrayal before the first Spice Blow or before Fremen movement.';
else if (!nexusCoreProfile(g) &&
(g.expansions.length || g.homeworlds || g.leaderSkills || g.discoveryEnabled ||
g.discoveries || g.discoveryStash || g.greatMaker || g.ecazTreachery ||
g.techTokens || g.strongholdCards ||
g.players.some(p => !['atreides','harkonnen','emperor','fremen','guild','beneGesserit'].includes(p.faction))))
blocked = 'Combined expansion and module interactions remain unavailable.';
else if (nexusCleanPlayBlocked(g, automaticPending))
blocked = 'Finish the current interaction before using Fremen Betrayal.';
else if (g.nexusFremenBetrayalHistory?.some(record => record.turn === g.turn))
blocked = 'Fremen Betrayal already spent its one physical card this turn.';
else if (mode === 'worm') {
if (g.spiceSequence || g.spiceResolution || g.spiceWindow || g.nexus ||
g.wormRides.length || g.summonedBeforeBlow || g.nexusFremenCunningOffer ||
g.nexusFremenCunningRides?.some(row => row.stage !== 'complete'))
blocked = 'Declare worm-riding prevention before the first worm or spice blow this turn.';
} else if (!g.movementRemaining?.includes(fremen.id) || fremen.moved !== 0 ||
fremen.fremenNexusMovementBlockedTurn === g.turn)
blocked = 'Fremen have already moved or completed their turn.';
return { event: fremenBetrayalEvent(g.turn, owner, fremen.id, mode),
blocked, target: fremen.id, mode };
}
export function validateFremenBetrayal(
g: Game, record: StoredFremenNexusBetrayal,
): FremenNexusBetrayal['mode'] {
const keys = record && Object.keys(record).sort().join(',');
const legacy = keys === 'event,owner,phase,signature,target,turn';
const mode = legacy ? 'movement' : record?.mode;
if (!record ||
(!legacy && keys !== 'event,mode,owner,phase,signature,target,turn') ||
!Number.isSafeInteger(record.turn) || record.turn < 1 || record.turn > g.turn ||
(mode !== 'worm' && mode !== 'movement') ||
record.phase !== (mode === 'worm' ? 1 : 5) ||
record.owner === record.target ||
!g.players.some(p => p.id === record.owner && p.faction !== 'fremen') ||
!g.players.some(p => p.id === record.target && p.faction === 'fremen') ||
record.event !== (legacy
? JSON.stringify(['nexusFremenBetrayal', record.turn, record.owner, record.target])
: fremenBetrayalEvent(record.turn, record.owner, record.target, mode)) ||
record.signature !== JSON.stringify({ ...record, signature: undefined }))
throw new Error('Fremen Betrayal has lost its original turn-long restriction.');
return mode;
}