From e0101a937bc66fed7b5c6ab459fc89eac95bde75 Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:56:05 +1100 Subject: [PATCH] Draw a distinct Mentat Traitor replacement replaceMoritaniAssassinationTraitors took the reserve's first entry, but the face-up set-aside card can still sit in that list: re-drawing it produced a receipt whose replacement equalled the revealed card, which moritaniAssassinateIntegrity rejects mid-Mentat. The step now draws the first distinct physical Traitor Card and is blocked with an explicit reason when none remains, instead of committing an invalid receipt. This is the engine half of checkpoint e493bcf, whose message described it but whose staged path list omitted game/engine.ts. Offline suite 7,801/7,801 with typecheck and lint clean at that revision; this commit changes no behaviour the suite did not already exercise. --- game/engine.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/game/engine.ts b/game/engine.ts index 2258e0b9..34395e04 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -7799,7 +7799,12 @@ function replaceMoritaniAssassinationTraitors(g: Game) { requireRule(r.turn === g.turn && owner.traitors.includes(r.card!) && (g.traitorReserve?.length ?? 0) > 0, 'Mentat replacement needs the original revealed card and one remaining physical Traitor Card.'); owner.traitors = owner.traitors.filter(id => id !== r.card); - r.replacement = g.traitorReserve!.shift()!; + // The set-aside card can still sit in the reserve; the replacement must be + // a distinct physical Traitor Card, never the revealed one again. + const index = g.traitorReserve!.findIndex(id => id !== r.card); + requireRule(index >= 0, + 'The Traitor Deck cannot supply a distinct replacement for the revealed Assassinate traitor.'); + r.replacement = g.traitorReserve!.splice(index, 1)[0]; owner.traitors.push(r.replacement); r.stage = 'replaced'; syncMoritaniAssassinateReceipt(g,r);