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' && (