From cdfe0e5370d6fd9f6844f064bafd8198ce897252 Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:58:07 +1000 Subject: [PATCH] Integrate Ixian Nexus Betrayal at original native attempts --- README.md | 13 + components/game-table.tsx | 32 +- components/nexus-ixian-betrayal.tsx | 60 ++ db/admin-directory.ts | 7 + docs/CURRENT_STATUS.md | 28 + docs/IMPLEMENTATION_STATUS.md | 61 ++ docs/NEXUS_CARD_RULES.md | 93 ++- docs/NEXUS_CARD_RUNTIME.md | 123 ++++ docs/RULE_DECISIONS.md | 1 + game/bot-nexus-ixian-betrayal.ts | 17 + game/bots.ts | 8 + game/engine.ts | 282 +++++++-- game/nexus-ixian-betrayal.ts | 288 ++++++++++ game/reference.ts | 56 +- game/table-turn.ts | 5 +- tests/admin-directory.test.ts | 92 +++ tests/bot-nexus-ixian-betrayal.test.ts | 132 +++++ tests/fixture-nexus-ixian-betrayal.ts | 144 +++++ tests/nexus-ixian-betrayal-controls.test.tsx | 182 ++++++ tests/nexus-ixian-betrayal-recovery.test.ts | 567 +++++++++++++++++++ tests/nexus-ixian-betrayal-runtime.test.ts | 225 ++++++++ tests/nexus-ixian-betrayal.test.ts | 297 ++++++++++ tests/prototype-room.test.ts | 165 ++++++ tools/prototype-room.ts | 7 + tools/start-prototype.ts | 2 +- 25 files changed, 2824 insertions(+), 63 deletions(-) create mode 100644 components/nexus-ixian-betrayal.tsx create mode 100644 game/bot-nexus-ixian-betrayal.ts create mode 100644 game/nexus-ixian-betrayal.ts create mode 100644 tests/bot-nexus-ixian-betrayal.test.ts create mode 100644 tests/fixture-nexus-ixian-betrayal.ts create mode 100644 tests/nexus-ixian-betrayal-controls.test.tsx create mode 100644 tests/nexus-ixian-betrayal-recovery.test.ts create mode 100644 tests/nexus-ixian-betrayal-runtime.test.ts create mode 100644 tests/nexus-ixian-betrayal.test.ts diff --git a/README.md b/README.md index 3eba50ab..3dbecf3f 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,19 @@ Full hands and same-card redraw from an exhausted deck are supported. Harkonnen buyers and special sale origins remain guarded; see [verified source, privacy and runtime boundaries](docs/NEXUS_IXIAN_REPLACEMENT_RULES.md). +For local **Ixian Nexus Betrayal**, use +`node --import tsx tools/start-prototype.ts --profile ixian-betrayal` +on a fresh ready Basic/Advanced lobby with native Ixians, otherwise classic +factions or Tleilaxu, the forty-seven-card Ix Treachery Deck and Nexus alone. +Exact-version/private-backup safeguards apply; no existing-game retrofit or +public start is enabled. Native alliance-qualified Nexus rights still apply. +Original Karama counters resolve first; neutral rival acknowledgements then +offer **Pass** or privately eligible **Use Ixian Nexus** before the original +Bidding inspection/draw or Advanced Technology exchange. Use spends one +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). + 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 4dbee962..757d0fd7 100644 --- a/components/game-table.tsx +++ b/components/game-table.tsx @@ -82,6 +82,7 @@ import { ChoamKull } from './choam-kull'; 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 { OrnithopterMovement } from './ornithopter-movement'; import { DiscoveryOrnithopterMovement } from './discovery-flight-movement'; import { PlanetologistMovement } from './planetologist-movement'; @@ -306,8 +307,11 @@ export function GameTable({ ); const reactionBusy = transportBusy || !!g.roomControl?.paused || !!g.roomControl?.closed || !!me.autopilot; - const busy = reactionBusy || g.automaticContinuationPending || !!g.semutaReaction || !!g.kullReaction || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction || !!g.nexusIxianReplacement; - const reactionOwnsControls = !!g.kullReaction || !!g.kullCounterEvent || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction || !!g.nexusIxianReplacement; + 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 canActivateKarama = (card: Card) => !g.karamaBlocked && canUseAsKaramaRole(g, me, card); const traitorBattle = g.battle; @@ -784,6 +788,15 @@ export function GameTable({ !botGroundMoveAllowed(g, me, key, boardLocation(selected, sector), 0), ); 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 ixianReaction = g.nexusIxianBetrayalReaction; + if (ixianReaction && ixianBetrayalOwnsControls) { + if (reactionBusy || g.status !== 'playing' || !ixianReaction.canPass || ixianReaction.hasPassed || + (a.type !== 'nexusIxianBetrayalPass' && a.type !== 'nexusIxianBetrayalUse') || + a.event !== ixianReaction.event || + (a.type === 'nexusIxianBetrayalUse' && (!ixianReaction.canUse || ixianReaction.blocked))) return; + } const guildReaction = g.guildBetrayalReaction; if (guildReaction) { if (reactionBusy || g.status !== 'playing' || !guildReaction.canPass || guildReaction.hasPassed || @@ -1029,6 +1042,8 @@ export function GameTable({ faction(g.players.find((p) => p.id === id)!.faction).name, ) .join(' + ') || 'No winner' + : ixianBetrayalOwnsControls + ? 'Native Ixian advantage acknowledgement' : g.guildBetrayalReaction ? 'Shipment payment acknowledgement' : g.richeseBetrayalReaction @@ -1093,6 +1108,15 @@ export function GameTable({ Preview rules and limits

)} + {g.nexusIxianBetrayalPreview && ( +

+ Ixian Nexus Betrayal development preview · native Ixian Bidding extra-card + inspection/draw and Advanced Technology exchange prevention. Native Ixians, + permitted classic/Tleilaxu seats, physical Ixian Treachery and Nexus only. + Native Karama responses finish first. Not complete Ixian or certified Nexus rules.{' '} + Preview rules and limits +

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