diff --git a/README.md b/README.md index 3dbecf3f..e6b59414 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,19 @@ physical Nexus and prevents only that attempt; all-pass keeps its native effect and continuation. See the [bounded verified runtime](docs/NEXUS_CARD_RUNTIME.md#bounded-ixian-betrayal-local-runtime--1-october-2026). +For local **Harkonnen Nexus Betrayal**, use +`node --import tsx tools/start-prototype.ts --profile harkonnen-betrayal` +on a fresh ready Basic/Advanced classic lobby with native Harkonnen and Nexus +alone. Exact-version/private-backup safeguards and natural alliance-qualified +Nexus rights apply; public starts and existing games are unchanged. After any +original allied Karama counter allows an actual matching native traitor call, +the declared-card acknowledgement offers **Pass** or privately eligible +**Use Harkonnen Nexus**. Use returns that exact physical traitor immediately +and preserves one automatic private actual Mentat replacement, not a draw-now +exchange. This is a bounded provisional local timing/privacy convention; +bounded verification is Partial/Development Verified; the universal reaction-policy question stays open. +See the [source contract and excluded modes](docs/NEXUS_HARKONNEN_RULES.md#bounded-betrayal-preview-contract--1-october-2026). + Above the board, **Treachery draw pile** and **Spice draw pile** show live card counts separately from hands, auction cards and discards. See [draw-pile counts](docs/DRAW_PILES.md). diff --git a/components/game-table.tsx b/components/game-table.tsx index 4d381882..6013946c 100644 --- a/components/game-table.tsx +++ b/components/game-table.tsx @@ -83,6 +83,7 @@ import { NexusRicheseBetrayal } from './nexus-richese-betrayal'; import { NexusGuildBetrayal } from './nexus-guild-betrayal'; import { NexusIxianReplacement } from './nexus-ixian-replacement'; import { NexusIxianBetrayal } from './nexus-ixian-betrayal'; +import { NexusHarkonnenBetrayal } from './nexus-harkonnen-betrayal'; import { OrnithopterMovement } from './ornithopter-movement'; import { DiscoveryOrnithopterMovement } from './discovery-flight-movement'; import { PlanetologistMovement } from './planetologist-movement'; @@ -310,8 +311,11 @@ export function GameTable({ const ixianBetrayalOwnsControls = !!g.nexusIxianBetrayalReaction && !g.response && !g.decision && !g.truthtrance && !g.phaseOpening && !g.automaticContinuationPending && !g.nexusCards?.waiting.length && !g.nexusTraitors?.pending; - const busy = reactionBusy || g.automaticContinuationPending || !!g.semutaReaction || !!g.kullReaction || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction || !!g.nexusIxianReplacement || ixianBetrayalOwnsControls; - const reactionOwnsControls = !!g.kullReaction || !!g.kullCounterEvent || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction || !!g.nexusIxianReplacement || ixianBetrayalOwnsControls; + const harkonnenBetrayalOwnsControls = !!g.nexusHarkonnenBetrayalReaction && + !g.response && !g.decision && !g.truthtrance && !g.phaseOpening && + !g.automaticContinuationPending && !g.nexusCards?.waiting.length && !g.nexusTraitors?.pending; + const busy = reactionBusy || g.automaticContinuationPending || !!g.semutaReaction || !!g.kullReaction || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction || !!g.nexusIxianReplacement || ixianBetrayalOwnsControls || harkonnenBetrayalOwnsControls; + const reactionOwnsControls = !!g.kullReaction || !!g.kullCounterEvent || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction || !!g.nexusIxianReplacement || ixianBetrayalOwnsControls || harkonnenBetrayalOwnsControls; const canActivateKarama = (card: Card) => !g.karamaBlocked && canUseAsKaramaRole(g, me, card); const traitorBattle = g.battle; @@ -790,6 +794,13 @@ export function GameTable({ const act = (a: Action) => { // Control of one's own seat is independent of every gameplay response. if (a.type === 'setAutopilot') { void send(a); return; } + const harkonnenReaction = g.nexusHarkonnenBetrayalReaction; + if (harkonnenReaction && harkonnenBetrayalOwnsControls) { + if (reactionBusy || g.status !== 'playing' || !harkonnenReaction.canPass || harkonnenReaction.hasPassed || + (a.type !== 'nexusHarkonnenBetrayalPass' && a.type !== 'nexusHarkonnenBetrayalUse') || + a.event !== harkonnenReaction.event || + (a.type === 'nexusHarkonnenBetrayalUse' && (!harkonnenReaction.canUse || harkonnenReaction.blocked))) return; + } const ixianReaction = g.nexusIxianBetrayalReaction; if (ixianReaction && ixianBetrayalOwnsControls) { if (reactionBusy || g.status !== 'playing' || !ixianReaction.canPass || ixianReaction.hasPassed || @@ -1042,6 +1053,8 @@ export function GameTable({ faction(g.players.find((p) => p.id === id)!.faction).name, ) .join(' + ') || 'No winner' + : harkonnenBetrayalOwnsControls + ? 'Declared Harkonnen traitor acknowledgement' : ixianBetrayalOwnsControls ? 'Native Ixian advantage acknowledgement' : g.guildBetrayalReaction @@ -1117,6 +1130,16 @@ export function GameTable({ Preview rules and limits

)} + {g.nexusHarkonnenBetrayalPreview && ( +

+ Harkonnen Nexus Betrayal development preview · cancel an actually declared native Harkonnen + traitor call, return that physical card to the Traitor Deck, and draw one private replacement + automatically at this turn’s Mentat Pause. Classic Basic/Advanced, native Harkonnen, base + Treachery and Nexus only. Native allied Karama counters finish first. Not complete Harkonnen + or certified Nexus rules.{' '} + Preview rules and limits +

+ )} {g.status !== 'lobby' && (