From ea111a22a59afe8de5ef61e527596213ef6594f5 Mon Sep 17 00:00:00 2001 From: kael <100491237+KaelSensei@users.noreply.github.com> Date: Sun, 8 Nov 2026 11:04:36 +0000 Subject: [PATCH] fix(bracket): name each warned card once across printings Bracket warnings listed a banned, off-identity or Game Changer card once per deck row, so two printings of one card repeated its name. Share the deduplicating, case-insensitive name order already used by validateDeck. Co-Authored-By: Claude Opus 5.5 --- src/lib/deck/bracket.test.ts | 17 +++++++++++++++++ src/lib/deck/bracket.ts | 3 ++- src/lib/deck/validation.ts | 15 ++++++++++++--- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/lib/deck/bracket.test.ts b/src/lib/deck/bracket.test.ts index 1993e1d7..84f13645 100644 --- a/src/lib/deck/bracket.test.ts +++ b/src/lib/deck/bracket.test.ts @@ -106,6 +106,23 @@ describe("scoreBracket", () => { ); }); + it("names a card once when the deck holds several printings of it", () => { + const stats = makeStats({ + bannedCards: ["Mana Crypt", "Griselbrand", "Mana Crypt"], + colorIdentityViolations: ["Lightning Bolt", "Lightning Bolt"], + gameChangersCount: 2, + gameChangersList: ["Sol Ring", "Sol Ring"], + }); + + expect(assertBracket(scoreBracket(makeDeck(), stats)).warnings).toEqual( + expect.arrayContaining([ + "Banned cards in deck: Griselbrand, Mana Crypt", + "Color identity violations: Lightning Bolt", + "2 Game Changer(s): Sol Ring", + ]) + ); + }); + it("returns null for non-Commander formats", () => { const deck = makeDeck({ format: "standard" }); const stats = makeStats(); diff --git a/src/lib/deck/bracket.ts b/src/lib/deck/bracket.ts index e8e025fb..80028a5e 100644 --- a/src/lib/deck/bracket.ts +++ b/src/lib/deck/bracket.ts @@ -3,6 +3,7 @@ import type { Deck, DeckStats, BracketScore } from "./types"; import type { SpellbookVariant } from "@/lib/combos/spellbook"; import { getCompanionDeckWarnings } from "./companion"; import { getFormatConfig } from "./formats"; +import { sortedUniqueNames } from "./validation"; function clamp(value: number, min: number, max: number): number { return Math.max(min, Math.min(max, value)); @@ -92,7 +93,7 @@ function countTwoCardInfiniteCombos(combos: SpellbookVariant[]): number { } function warningCardNames(names: readonly string[]): string { - return [...names].sort((left, right) => left.localeCompare(right, "en")).join(", "); + return sortedUniqueNames(names).join(", "); } /** Helper: generate ban/legality warnings */ diff --git a/src/lib/deck/validation.ts b/src/lib/deck/validation.ts index 0dc518a4..1a5fabc3 100644 --- a/src/lib/deck/validation.ts +++ b/src/lib/deck/validation.ts @@ -84,15 +84,24 @@ export function validateCardForDeck( }; } +/** + * Deduplicate names and order them case-insensitively, with a case-sensitive tiebreak. + * @param names - Card names; several printings of one card may repeat a name. + * @returns Each name once, independent of input order. + */ +export function sortedUniqueNames(names: Iterable): string[] { + return [...new Set(names)].toSorted( + (left, right) => left.localeCompare(right, "en", { sensitivity: "base" }) || left.localeCompare(right) + ); +} + /** * List distinct card names in a stable, case-insensitive alphabetical order. * @param cards - Deck rows to name; several rows may share a name. * @returns Unique card names, independent of deck row order. */ export function sortedUniqueCardNames(cards: readonly DeckCard[]): string[] { - return [...new Set(cards.map((card) => card.name))].toSorted( - (left, right) => left.localeCompare(right, "en", { sensitivity: "base" }) || left.localeCompare(right) - ); + return sortedUniqueNames(cards.map((card) => card.name)); } function checkCardCount(allCards: DeckCard[], deckSize: number, errors: string[], warnings: string[]): void {