From a2b04dbfba13eeb913c93de87981a58cb6b7d99d Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:12:46 +1000 Subject: [PATCH] Integrate exact Ixian Nexus purchased-card replacement --- README.md | 13 + components/game-table.tsx | 26 +- components/nexus-ixian-replacement.tsx | 80 + docs/CURRENT_STATUS.md | 31 +- docs/IMPLEMENTATION_STATUS.md | 76 + docs/NEXUS_CARD_RULES.md | 2 + docs/NEXUS_CARD_RUNTIME.md | 2 + docs/NEXUS_IXIAN_REPLACEMENT_RULES.md | 87 +- docs/RULE_DECISIONS.md | 9 +- game/admin-seat-ai-shape.ts | 1 + game/bots.ts | 22 +- game/engine.ts | 155 +- game/nexus-ixian-replacement.ts | 272 ++ game/reference.ts | 36 +- game/richese-cancellation.ts | 1 + game/table-turn.ts | 3 +- tests/bot-nexus-ixian-replacement.test.ts | 100 + tests/fixture-nexus-ixian-replacement.ts | 205 ++ .../fixtures/nexus-ixian-native-depleted.json | 2189 +++++++++++++++++ .../nexus-ixian-replacement-controls.test.tsx | 137 ++ tests/nexus-ixian-replacement-engine.test.ts | 345 +++ .../nexus-ixian-replacement-recovery.test.ts | 463 ++++ tests/nexus-ixian-replacement.test.ts | 236 ++ tests/prototype-room.test.ts | 122 + tools/prototype-room.ts | 7 + tools/start-prototype.ts | 2 +- 26 files changed, 4588 insertions(+), 34 deletions(-) create mode 100644 components/nexus-ixian-replacement.tsx create mode 100644 game/nexus-ixian-replacement.ts create mode 100644 tests/bot-nexus-ixian-replacement.test.ts create mode 100644 tests/fixture-nexus-ixian-replacement.ts create mode 100644 tests/fixtures/nexus-ixian-native-depleted.json create mode 100644 tests/nexus-ixian-replacement-controls.test.tsx create mode 100644 tests/nexus-ixian-replacement-engine.test.ts create mode 100644 tests/nexus-ixian-replacement-recovery.test.ts create mode 100644 tests/nexus-ixian-replacement.test.ts diff --git a/README.md b/README.md index 34e26496..3eba50ab 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,19 @@ this is not a second shipment, free upfront financing or extra Guild income. See [payment custody and source boundaries](docs/NEXUS_GUILD_RULES.md#betrayal-payment-replacement). Existing games and public start gates remain unchanged. +For local **Ixian Nexus purchased-card replacement**, use +`node --import tsx tools/start-prototype.ts --profile ixian-replacement` +on a fresh ready classic Basic/Advanced lobby with base Treachery and Nexus +alone. Exact-version/private-backup safeguards apply; existing games and +public starts are unchanged. A real alliance-qualified Nexus must first +give an unallied buyer its card, so effect scenarios need at least three +seats. **Keep purchased card** retains both cards; eligible **Use Ixian Nexus** +discards exactly the normal paid/printed-Karama purchase before drawing the +real deck top, with the original payment and auction continuation once. +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). + 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 28ecdb3c..4dbee962 100644 --- a/components/game-table.tsx +++ b/components/game-table.tsx @@ -81,6 +81,7 @@ import { SemutaReaction } from './semuta-reaction'; import { ChoamKull } from './choam-kull'; import { NexusRicheseBetrayal } from './nexus-richese-betrayal'; import { NexusGuildBetrayal } from './nexus-guild-betrayal'; +import { NexusIxianReplacement } from './nexus-ixian-replacement'; import { OrnithopterMovement } from './ornithopter-movement'; import { DiscoveryOrnithopterMovement } from './discovery-flight-movement'; import { PlanetologistMovement } from './planetologist-movement'; @@ -305,8 +306,8 @@ 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; - const reactionOwnsControls = !!g.kullReaction || !!g.kullCounterEvent || !!g.richeseBetrayalReaction || !!g.guildBetrayalReaction; + 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 canActivateKarama = (card: Card) => !g.karamaBlocked && canUseAsKaramaRole(g, me, card); const traitorBattle = g.battle; @@ -790,6 +791,13 @@ export function GameTable({ a.event !== guildReaction.event || (a.type === 'guildBetrayalUse' && (!guildReaction.canUse || guildReaction.blocked))) return; } + const replacement = g.nexusIxianReplacement; + if (replacement && a.type !== 'setAutopilot') { + if (reactionBusy || g.status !== 'playing' || !replacement.canPass || replacement.buyer !== me.id || + (a.type !== 'nexusIxianReplacementPass' && a.type !== 'nexusIxianReplacementUse') || + a.event !== replacement.event || + (a.type === 'nexusIxianReplacementUse' && (!replacement.canUse || replacement.blocked))) return; + } void send(a); }; const choose = (id: string) => { @@ -1077,6 +1085,14 @@ export function GameTable({ Preview rules and limits

)} + {g.nexusIxianReplacementPreview && ( +

+ Ixian Nexus purchased-card replacement development preview · classic Basic/Advanced, + base Treachery and Nexus only. Normal paid and printed Karama purchases are supported; + Harkonnen buyers and special auctions are guarded. Not complete Ixian or certified Nexus rules.{' '} + Preview rules and limits +

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