diff --git a/client/src/components/deck-builder/__tests__/commanderUtils.test.ts b/client/src/components/deck-builder/__tests__/commanderUtils.test.ts new file mode 100644 index 0000000000..e88bf4eaa6 --- /dev/null +++ b/client/src/components/deck-builder/__tests__/commanderUtils.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; + +import type { ScryfallCard } from "../../../services/scryfall"; +import { getColorIdentityViolations } from "../commanderUtils"; + +function card(colorIdentity: string[]): ScryfallCard { + return { color_identity: colorIdentity } as ScryfallCard; +} + +describe("getColorIdentityViolations", () => { + it("flags off-color cards for a genuinely colorless commander (CR 903.5c)", () => { + const cache = new Map([ + ["Kozilek, Butcher of Truth", card([])], + ["Lightning Bolt", card(["R"])], + ["Wastes", card([])], + ]); + const deck = [ + { name: "Lightning Bolt", count: 1 }, + { name: "Wastes", count: 1 }, + ]; + + const violations = getColorIdentityViolations( + deck, + ["Kozilek, Butcher of Truth"], + cache, + ); + + expect(violations).toEqual(["Lightning Bolt"]); + }); + + it("does not flag on-identity cards for a colored commander", () => { + const cache = new Map([ + ["Krenko, Mob Boss", card(["R"])], + ["Lightning Bolt", card(["R"])], + ["Counterspell", card(["U"])], + ]); + const deck = [ + { name: "Lightning Bolt", count: 1 }, + { name: "Counterspell", count: 1 }, + ]; + + expect( + getColorIdentityViolations(deck, ["Krenko, Mob Boss"], cache), + ).toEqual(["Counterspell"]); + }); + + it("returns no violations while commander data is still loading", () => { + // Commander not in cache — an empty identity must NOT flag every colored card. + const cache = new Map([ + ["Lightning Bolt", card(["R"])], + ]); + const deck = [{ name: "Lightning Bolt", count: 1 }]; + + expect( + getColorIdentityViolations(deck, ["Kozilek, Butcher of Truth"], cache), + ).toEqual([]); + }); +}); diff --git a/client/src/components/deck-builder/commanderUtils.ts b/client/src/components/deck-builder/commanderUtils.ts index 411d3ece44..c4513e07f0 100644 --- a/client/src/components/deck-builder/commanderUtils.ts +++ b/client/src/components/deck-builder/commanderUtils.ts @@ -25,7 +25,11 @@ export function getCombinedColorIdentity( } function isInColorIdentity(card: ScryfallCard, identity: string[]): boolean { - if (identity.length === 0) return true; + // A card is legal iff every color in its identity is within the commander's. + // For a colorless commander (`identity` empty), only colorless cards pass — + // `[].every(...)` is `true`, a colored card's `.every(...)` is `false` — which + // is exactly CR 903.5c. (Callers must only invoke this once commander data is + // loaded; an empty identity from a cache miss would otherwise flag everything.) const identitySet = new Set(identity); return card.color_identity.every((c) => identitySet.has(c)); } @@ -36,6 +40,9 @@ export function getColorIdentityViolations( cardDataCache: Map, ): string[] { if (commanders.length === 0) return []; + // Don't compute violations until every commander's data has loaded — a cache + // miss yields an empty identity that would wrongly flag every colored card. + if (!commanders.every((name) => cardDataCache.has(name))) return []; const identity = getCombinedColorIdentity(commanders, cardDataCache); const violations: string[] = []; for (const entry of deck) {