Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<string, ScryfallCard>([
["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<string, ScryfallCard>([
["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<string, ScryfallCard>([
["Lightning Bolt", card(["R"])],
]);
const deck = [{ name: "Lightning Bolt", count: 1 }];

expect(
getColorIdentityViolations(deck, ["Kozilek, Butcher of Truth"], cache),
).toEqual([]);
});
});
9 changes: 8 additions & 1 deletion client/src/components/deck-builder/commanderUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand All @@ -36,6 +40,9 @@ export function getColorIdentityViolations(
cardDataCache: Map<string, ScryfallCard>,
): 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) {
Expand Down
Loading