👁️
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

deck validation pt2

+577 -3
+217
.claude/DECK_VALIDATION.md
··· 1 + # Deck Validation System 2 + 3 + Pure function validation system for MTG deck construction rules with comprehensive rules citations. 4 + 5 + ## Overview 6 + 7 + The `src/lib/deck-validation/` module provides: 8 + - **Rule-based validation** with MTG comprehensive rules citations (e.g., "100.2a", "903.5b") 9 + - **Format presets** that combine rules + config for each supported format 10 + - **Copy exception detection** for cards like Relentless Rats, Seven Dwarves 11 + - **Composable architecture** - formats are defined by spreading rule sets 12 + 13 + ## Usage 14 + 15 + ```typescript 16 + import { validateDeck } from "@/lib/deck-validation"; 17 + 18 + const result = validateDeck({ 19 + deck, 20 + cardLookup: (id) => cardMap.get(id), 21 + oracleLookup: (id) => oracleMap.get(id), 22 + getPrintings: (id) => printingsMap.get(id) ?? [], 23 + }); 24 + 25 + if (!result.valid) { 26 + for (const v of result.violations) { 27 + console.log(`[${v.rule}] ${v.message}`); 28 + } 29 + } 30 + ``` 31 + 32 + ## File Structure 33 + 34 + ``` 35 + src/lib/deck-validation/ 36 + ├── index.ts # Barrel export 37 + ├── types.ts # Core type definitions 38 + ├── exceptions.ts # Copy exception detection (Relentless Rats, etc.) 39 + ├── presets.ts # Format preset definitions 40 + ├── validate.ts # Main validation orchestration 41 + ├── rules/ 42 + │ ├── index.ts # Exports RULES object and RuleId type 43 + │ ├── base.ts # Generic rules (legality, copy limits, deck size) 44 + │ ├── commander.ts # Commander-family rules (color identity, partner) 45 + │ └── rarity.ts # Rarity rules (PDH uncommon commander) 46 + └── __tests__/ 47 + ├── exceptions.test.ts 48 + └── rules.test.ts 49 + ``` 50 + 51 + ## Key Types 52 + 53 + ```typescript 54 + // Rule citation from MTG comprehensive rules 55 + type RuleNumber = string & { readonly __brand: "RuleNumber" }; 56 + 57 + // Categories for grouping violations 58 + type RuleCategory = "legality" | "quantity" | "identity" | "structure"; 59 + 60 + // Violation severity 61 + type Severity = "error" | "warning"; 62 + 63 + // A single violation 64 + interface Violation { 65 + ruleId: string; // Internal ID (e.g., "singleton") 66 + rule: RuleNumber; // MTG rule (e.g., "903.5b") 67 + category: RuleCategory; 68 + cardName?: string; 69 + oracleId?: OracleId; 70 + section?: Section; 71 + quantity?: number; 72 + message: string; 73 + severity: Severity; 74 + } 75 + 76 + // Validation result 77 + interface ValidationResult { 78 + valid: boolean; // No errors (warnings okay) 79 + violations: Violation[]; 80 + byCard: Map<OracleId, Violation[]>; 81 + byRule: Map<RuleNumber, Violation[]>; 82 + } 83 + ``` 84 + 85 + ## Available Rules 86 + 87 + | Rule ID | MTG Citation | Description | 88 + |---------|--------------|-------------| 89 + | `cardLegality` | 100.2a | Card must be legal in format | 90 + | `banned` | 100.6a | Card is banned in format | 91 + | `restricted` | 100.6b | Restricted to 1 copy (Vintage) | 92 + | `singleton` | 903.5b | Max 1 copy (Commander variants) | 93 + | `playset` | 100.2a | Max 4 copies (60-card formats) | 94 + | `deckSizeMin` | 100.2a | Minimum deck size | 95 + | `deckSizeExact` | 903.5a | Exact deck size (Commander = 100) | 96 + | `sideboardSize` | 100.4a | Max sideboard cards | 97 + | `colorIdentity` | 903.4 | Cards must match commander colors | 98 + | `commanderRequired` | 903.3 | At least 1 commander | 99 + | `commanderPartner` | 702.124 | Valid partner pairing if 2 commanders | 100 + | `commanderLegendary` | 903.3 | Commander must be legendary creature | 101 + | `commanderUncommon` | 903.3 | Commander must be uncommon (PDH) | 102 + | `commanderPlaneswalker` | 903.3 | Commander must be planeswalker (Oathbreaker) | 103 + | `signatureSpell` | 903.3 | Oathbreaker signature spell requirement | 104 + 105 + ## Format Presets 106 + 107 + Presets are defined in `presets.ts` and combine rules + config: 108 + 109 + ```typescript 110 + // 60-card formats share common rules 111 + const SIXTY_CARD_RULES = [ 112 + "cardLegality", "banned", "playset", "deckSizeMin", "sideboardSize", 113 + ] as const; 114 + 115 + // Commander variants share core rules 116 + const COMMANDER_CORE_RULES = [ 117 + "cardLegality", "banned", "singleton", "colorIdentity", 118 + "deckSizeExact", "commanderRequired", "commanderPartner", 119 + ] as const; 120 + 121 + // Formats spread and extend 122 + const PRESETS = { 123 + modern: { rules: SIXTY_CARD_RULES, config: { legalityField: "modern", minDeckSize: 60, sideboardSize: 15 } }, 124 + commander: { rules: [...COMMANDER_CORE_RULES, "commanderLegendary"], config: { legalityField: "commander", deckSize: 100 } }, 125 + paupercommander: { rules: [...COMMANDER_CORE_RULES, "commanderUncommon"], config: { legalityField: "paupercommander", deckSize: 100 } }, 126 + // ... 127 + }; 128 + ``` 129 + 130 + ## Adding a New Rule 131 + 132 + 1. Create the rule in the appropriate file (`base.ts`, `commander.ts`, or new file) 133 + 2. Export it from `rules/index.ts` and add to the `RULES` object 134 + 3. Add tests in `__tests__/rules.test.ts` 135 + 4. Add to relevant presets in `presets.ts` 136 + 137 + ```typescript 138 + // In rules/base.ts 139 + export const myNewRule: Rule<"myNewRule"> = { 140 + id: "myNewRule", 141 + rule: asRuleNumber("123.4a"), 142 + category: "structure", 143 + description: "What this rule checks", 144 + validate(ctx: ValidationContext): Violation[] { 145 + // Return violations or empty array 146 + }, 147 + }; 148 + 149 + // In rules/index.ts 150 + export { myNewRule } from "./base"; 151 + export const RULES = { 152 + // ...existing rules 153 + myNewRule: myNewRule, 154 + } as const; 155 + ``` 156 + 157 + ## Adding a New Format 158 + 159 + 1. Add the format to `presets.ts` with appropriate rules and config 160 + 2. The `legalityField` should match Scryfall's `legalities.X` field name 161 + 3. Add to `FORMAT_GROUPS` in `format-utils.ts` for UI display 162 + 163 + ```typescript 164 + // In presets.ts 165 + myFormat: { 166 + rules: [...SIXTY_CARD_RULES, "someExtraRule"], 167 + config: { legalityField: "myformat", minDeckSize: 60, sideboardSize: 15 }, 168 + }, 169 + ``` 170 + 171 + ## Copy Exceptions 172 + 173 + The `exceptions.ts` module handles cards that bypass normal copy limits: 174 + 175 + - **Unlimited**: Relentless Rats, Shadowborn Apostle, Persistent Petitioners, etc. 176 + - **Limited**: Seven Dwarves (max 7), Nazgûl (max 9) 177 + - **Basic lands**: Always unlimited 178 + 179 + Detection is regex-based on oracle text for future-proofing: 180 + ```typescript 181 + // "A deck can have any number of cards named X" 182 + // "A deck can have up to seven cards named X" 183 + ``` 184 + 185 + ## Validation Options 186 + 187 + ```typescript 188 + interface ValidationOptions { 189 + disabledRules?: Set<string>; // Skip specific rules 190 + disabledCategories?: Set<RuleCategory>; // Skip entire categories 191 + configOverrides?: Partial<FormatConfig>; // Override preset config 192 + includeMaybeboard?: boolean; // Include maybeboard in validity 193 + } 194 + ``` 195 + 196 + ## Maybeboard Handling 197 + 198 + - Maybeboard violations are included in `result.violations` 199 + - But they don't affect `result.valid` unless `includeMaybeboard: true` 200 + - Color identity violations in maybeboard are warnings, not errors 201 + 202 + ## Partner Validation 203 + 204 + The `commanderPartnerRule` validates all partner pairings: 205 + - Generic Partner (both have Partner keyword) 206 + - Partner with X (names specific card) 207 + - Friends Forever (from Stranger Things Secret Lair) 208 + - Background (creature + Background enchantment) 209 + - Doctor's Companion (companion + Time Lord Doctor) 210 + 211 + ## PDH (Pauper Commander) Notes 212 + 213 + - Commander must have an uncommon printing in paper or MTGO 214 + - Arena-only uncommon downshifts don't count 215 + - Any printing can be used if a valid uncommon exists 216 + - Uses `legalities.paupercommander` for the 99, not `pauper` 217 + - Commander doesn't need to be legendary (just uncommon creature)
+351
src/lib/deck-validation/__tests__/rules.test.ts
··· 1 + import { beforeAll, describe, expect, it } from "vitest"; 2 + import { 3 + setupTestCards, 4 + type TestCardLookup, 5 + } from "@/lib/__tests__/test-card-lookup"; 6 + import type { Deck, DeckCard, Section } from "@/lib/deck-types"; 7 + import type { 8 + Card, 9 + ManaColor, 10 + OracleId, 11 + ScryfallId, 12 + } from "@/lib/scryfall-types"; 13 + import { 14 + colorIdentityRule, 15 + commanderLegendaryRule, 16 + commanderPartnerRule, 17 + commanderPlaneswalkerRule, 18 + commanderRequiredRule, 19 + signatureSpellRule, 20 + } from "../rules/commander"; 21 + import type { ValidationContext } from "../types"; 22 + 23 + describe("commander rules", () => { 24 + let cards: TestCardLookup; 25 + 26 + beforeAll(async () => { 27 + cards = await setupTestCards(); 28 + }, 30_000); 29 + 30 + function makeDeck(deckCards: DeckCard[], format = "commander"): Deck { 31 + return { 32 + $type: "com.deckbelcher.deck.list", 33 + name: "Test Deck", 34 + format, 35 + cards: deckCards, 36 + createdAt: new Date().toISOString(), 37 + }; 38 + } 39 + 40 + function makeCard(card: Card, section: Section, quantity = 1): DeckCard { 41 + return { 42 + scryfallId: card.id, 43 + oracleId: card.oracle_id, 44 + section, 45 + quantity, 46 + tags: [], 47 + }; 48 + } 49 + 50 + function makeContext( 51 + deck: Deck, 52 + commanderColors?: string[], 53 + ): ValidationContext { 54 + const cardMap = new Map<ScryfallId, Card>(); 55 + const oracleMap = new Map<OracleId, Card>(); 56 + 57 + return { 58 + deck, 59 + cardLookup: (id) => cardMap.get(id), 60 + oracleLookup: (id) => oracleMap.get(id), 61 + getPrintings: () => [], 62 + format: deck.format, 63 + commanderColors: commanderColors as ManaColor[] | undefined, 64 + config: { legalityField: "commander" }, 65 + }; 66 + } 67 + 68 + async function makeContextWithCards( 69 + deck: Deck, 70 + cardList: Card[], 71 + commanderColors?: string[], 72 + ): Promise<ValidationContext> { 73 + const cardMap = new Map<ScryfallId, Card>(); 74 + const oracleMap = new Map<OracleId, Card>(); 75 + 76 + for (const card of cardList) { 77 + cardMap.set(card.id, card); 78 + oracleMap.set(card.oracle_id, card); 79 + } 80 + 81 + return { 82 + deck, 83 + cardLookup: (id) => cardMap.get(id), 84 + oracleLookup: (id) => oracleMap.get(id), 85 + getPrintings: () => [], 86 + format: deck.format, 87 + commanderColors: commanderColors as ManaColor[] | undefined, 88 + config: { legalityField: "commander" }, 89 + }; 90 + } 91 + 92 + describe("commanderRequiredRule", () => { 93 + it("errors when no commander", () => { 94 + const deck = makeDeck([]); 95 + const ctx = makeContext(deck); 96 + const violations = commanderRequiredRule.validate(ctx); 97 + expect(violations).toHaveLength(1); 98 + expect(violations[0].severity).toBe("error"); 99 + }); 100 + 101 + it("passes with a commander", async () => { 102 + const solRing = await cards.get("Sol Ring"); 103 + const deck = makeDeck([makeCard(solRing, "commander")]); 104 + const ctx = await makeContextWithCards(deck, [solRing]); 105 + const violations = commanderRequiredRule.validate(ctx); 106 + expect(violations).toHaveLength(0); 107 + }); 108 + }); 109 + 110 + describe("commanderLegendaryRule", () => { 111 + it("errors when commander is not legendary creature", async () => { 112 + const solRing = await cards.get("Sol Ring"); 113 + const deck = makeDeck([makeCard(solRing, "commander")]); 114 + const ctx = await makeContextWithCards(deck, [solRing]); 115 + const violations = commanderLegendaryRule.validate(ctx); 116 + expect(violations).toHaveLength(1); 117 + expect(violations[0].message).toContain("not a legendary creature"); 118 + }); 119 + 120 + it("passes for legendary creature", async () => { 121 + const selvala = await cards.get("Selvala, Heart of the Wilds"); 122 + const deck = makeDeck([makeCard(selvala, "commander")]); 123 + const ctx = await makeContextWithCards(deck, [selvala]); 124 + const violations = commanderLegendaryRule.validate(ctx); 125 + expect(violations).toHaveLength(0); 126 + }); 127 + }); 128 + 129 + describe("commanderPartnerRule", () => { 130 + it("allows single commander without partner", async () => { 131 + const selvala = await cards.get("Selvala, Heart of the Wilds"); 132 + const deck = makeDeck([makeCard(selvala, "commander")]); 133 + const ctx = await makeContextWithCards(deck, [selvala]); 134 + const violations = commanderPartnerRule.validate(ctx); 135 + expect(violations).toHaveLength(0); 136 + }); 137 + 138 + it("allows two generic partner commanders", async () => { 139 + const thrasios = await cards.get("Thrasios, Triton Hero"); 140 + const tymna = await cards.get("Tymna the Weaver"); 141 + const deck = makeDeck([ 142 + makeCard(thrasios, "commander"), 143 + makeCard(tymna, "commander"), 144 + ]); 145 + const ctx = await makeContextWithCards(deck, [thrasios, tymna]); 146 + const violations = commanderPartnerRule.validate(ctx); 147 + expect(violations).toHaveLength(0); 148 + }); 149 + 150 + it("allows partner with specific card", async () => { 151 + const pir = await cards.get("Pir, Imaginative Rascal"); 152 + const toothy = await cards.get("Toothy, Imaginary Friend"); 153 + const deck = makeDeck([ 154 + makeCard(pir, "commander"), 155 + makeCard(toothy, "commander"), 156 + ]); 157 + const ctx = await makeContextWithCards(deck, [pir, toothy]); 158 + const violations = commanderPartnerRule.validate(ctx); 159 + expect(violations).toHaveLength(0); 160 + }); 161 + 162 + it("allows friends forever pairing", async () => { 163 + const bjorna = await cards.get("Bjorna, Nightfall Alchemist"); 164 + const cecily = await cards.get("Cecily, Haunted Mage"); 165 + const deck = makeDeck([ 166 + makeCard(bjorna, "commander"), 167 + makeCard(cecily, "commander"), 168 + ]); 169 + const ctx = await makeContextWithCards(deck, [bjorna, cecily]); 170 + const violations = commanderPartnerRule.validate(ctx); 171 + expect(violations).toHaveLength(0); 172 + }); 173 + 174 + it("allows background pairing", async () => { 175 + const wilson = await cards.get("Wilson, Refined Grizzly"); 176 + const raisedByGiants = await cards.get("Raised by Giants"); 177 + const deck = makeDeck([ 178 + makeCard(wilson, "commander"), 179 + makeCard(raisedByGiants, "commander"), 180 + ]); 181 + const ctx = await makeContextWithCards(deck, [wilson, raisedByGiants]); 182 + const violations = commanderPartnerRule.validate(ctx); 183 + expect(violations).toHaveLength(0); 184 + }); 185 + 186 + it("allows doctor's companion pairing", async () => { 187 + const barbara = await cards.get("Barbara Wright"); 188 + const doctor = await cards.get("The Eighth Doctor"); 189 + const deck = makeDeck([ 190 + makeCard(barbara, "commander"), 191 + makeCard(doctor, "commander"), 192 + ]); 193 + const ctx = await makeContextWithCards(deck, [barbara, doctor]); 194 + const violations = commanderPartnerRule.validate(ctx); 195 + expect(violations).toHaveLength(0); 196 + }); 197 + 198 + it("errors with 3 commanders", async () => { 199 + const thrasios = await cards.get("Thrasios, Triton Hero"); 200 + const tymna = await cards.get("Tymna the Weaver"); 201 + const selvala = await cards.get("Selvala, Heart of the Wilds"); 202 + const deck = makeDeck([ 203 + makeCard(thrasios, "commander"), 204 + makeCard(tymna, "commander"), 205 + makeCard(selvala, "commander"), 206 + ]); 207 + const ctx = await makeContextWithCards(deck, [thrasios, tymna, selvala]); 208 + const violations = commanderPartnerRule.validate(ctx); 209 + expect(violations).toHaveLength(1); 210 + expect(violations[0].message).toContain("3 commanders"); 211 + }); 212 + 213 + it("errors when partner with wrong card", async () => { 214 + const pir = await cards.get("Pir, Imaginative Rascal"); 215 + const thrasios = await cards.get("Thrasios, Triton Hero"); 216 + const deck = makeDeck([ 217 + makeCard(pir, "commander"), 218 + makeCard(thrasios, "commander"), 219 + ]); 220 + const ctx = await makeContextWithCards(deck, [pir, thrasios]); 221 + const violations = commanderPartnerRule.validate(ctx); 222 + expect(violations).toHaveLength(1); 223 + expect(violations[0].message).toContain("cannot be paired"); 224 + }); 225 + 226 + it("errors when incompatible commanders", async () => { 227 + const selvala = await cards.get("Selvala, Heart of the Wilds"); 228 + const solRing = await cards.get("Sol Ring"); 229 + const deck = makeDeck([ 230 + makeCard(selvala, "commander"), 231 + makeCard(solRing, "commander"), 232 + ]); 233 + const ctx = await makeContextWithCards(deck, [selvala, solRing]); 234 + const violations = commanderPartnerRule.validate(ctx); 235 + expect(violations).toHaveLength(1); 236 + }); 237 + }); 238 + 239 + describe("colorIdentityRule", () => { 240 + it("errors when card outside color identity", async () => { 241 + const lightningBolt = await cards.get("Lightning Bolt"); 242 + const deck = makeDeck([makeCard(lightningBolt, "mainboard")]); 243 + const ctx = await makeContextWithCards(deck, [lightningBolt], ["U", "G"]); 244 + const violations = colorIdentityRule.validate(ctx); 245 + expect(violations).toHaveLength(1); 246 + expect(violations[0].severity).toBe("error"); 247 + expect(violations[0].message).toContain("R not in UG"); 248 + }); 249 + 250 + it("warns for maybeboard cards outside identity", async () => { 251 + const lightningBolt = await cards.get("Lightning Bolt"); 252 + const deck = makeDeck([makeCard(lightningBolt, "maybeboard")]); 253 + const ctx = await makeContextWithCards(deck, [lightningBolt], ["U", "G"]); 254 + const violations = colorIdentityRule.validate(ctx); 255 + expect(violations).toHaveLength(1); 256 + expect(violations[0].severity).toBe("warning"); 257 + }); 258 + 259 + it("passes when card in color identity", async () => { 260 + const lightningBolt = await cards.get("Lightning Bolt"); 261 + const deck = makeDeck([makeCard(lightningBolt, "mainboard")]); 262 + const ctx = await makeContextWithCards(deck, [lightningBolt], ["R", "G"]); 263 + const violations = colorIdentityRule.validate(ctx); 264 + expect(violations).toHaveLength(0); 265 + }); 266 + 267 + it("passes for colorless cards in any identity", async () => { 268 + const solRing = await cards.get("Sol Ring"); 269 + const deck = makeDeck([makeCard(solRing, "mainboard")]); 270 + const ctx = await makeContextWithCards(deck, [solRing], ["W"]); 271 + const violations = colorIdentityRule.validate(ctx); 272 + expect(violations).toHaveLength(0); 273 + }); 274 + }); 275 + 276 + describe("commanderPlaneswalkerRule (Oathbreaker)", () => { 277 + it("errors when commander is not planeswalker", async () => { 278 + const selvala = await cards.get("Selvala, Heart of the Wilds"); 279 + const deck = makeDeck([makeCard(selvala, "commander")], "oathbreaker"); 280 + const ctx = await makeContextWithCards(deck, [selvala]); 281 + const violations = commanderPlaneswalkerRule.validate(ctx); 282 + expect(violations).toHaveLength(1); 283 + expect(violations[0].message).toContain("not a planeswalker"); 284 + }); 285 + 286 + it("passes for planeswalker", async () => { 287 + const bolas = await cards.get("Nicol Bolas, Planeswalker"); 288 + const deck = makeDeck([makeCard(bolas, "commander")], "oathbreaker"); 289 + const ctx = await makeContextWithCards(deck, [bolas]); 290 + const violations = commanderPlaneswalkerRule.validate(ctx); 291 + expect(violations).toHaveLength(0); 292 + }); 293 + 294 + it("ignores signature spell (instant/sorcery)", async () => { 295 + const bolas = await cards.get("Nicol Bolas, Planeswalker"); 296 + const darkRitual = await cards.get("Dark Ritual"); 297 + const deck = makeDeck( 298 + [makeCard(bolas, "commander"), makeCard(darkRitual, "commander")], 299 + "oathbreaker", 300 + ); 301 + const ctx = await makeContextWithCards(deck, [bolas, darkRitual]); 302 + const violations = commanderPlaneswalkerRule.validate(ctx); 303 + expect(violations).toHaveLength(0); 304 + }); 305 + }); 306 + 307 + describe("signatureSpellRule (Oathbreaker)", () => { 308 + it("errors when no signature spell", async () => { 309 + const bolas = await cards.get("Nicol Bolas, Planeswalker"); 310 + const deck = makeDeck([makeCard(bolas, "commander")], "oathbreaker"); 311 + const ctx = await makeContextWithCards(deck, [bolas]); 312 + const violations = signatureSpellRule.validate(ctx); 313 + expect(violations).toHaveLength(1); 314 + expect(violations[0].message).toContain("must have a signature spell"); 315 + }); 316 + 317 + it("passes with one signature spell", async () => { 318 + const bolas = await cards.get("Nicol Bolas, Planeswalker"); 319 + const darkRitual = await cards.get("Dark Ritual"); 320 + const deck = makeDeck( 321 + [makeCard(bolas, "commander"), makeCard(darkRitual, "commander")], 322 + "oathbreaker", 323 + ); 324 + const ctx = await makeContextWithCards(deck, [bolas, darkRitual]); 325 + const violations = signatureSpellRule.validate(ctx); 326 + expect(violations).toHaveLength(0); 327 + }); 328 + 329 + it("errors with multiple signature spells", async () => { 330 + const bolas = await cards.get("Nicol Bolas, Planeswalker"); 331 + const darkRitual = await cards.get("Dark Ritual"); 332 + const fireball = await cards.get("Fireball"); 333 + const deck = makeDeck( 334 + [ 335 + makeCard(bolas, "commander"), 336 + makeCard(darkRitual, "commander"), 337 + makeCard(fireball, "commander"), 338 + ], 339 + "oathbreaker", 340 + ); 341 + const ctx = await makeContextWithCards(deck, [ 342 + bolas, 343 + darkRitual, 344 + fireball, 345 + ]); 346 + const violations = signatureSpellRule.validate(ctx); 347 + expect(violations).toHaveLength(1); 348 + expect(violations[0].message).toContain("only have 1 signature spell"); 349 + }); 350 + }); 351 + });
+9 -3
src/lib/deck-validation/rules/commander.ts
··· 145 145 return { valid: true }; 146 146 } 147 147 148 - // Partner with: check if they name each other 149 - if (info1.partnerWithName && info1.partnerWithName === card2.name) { 148 + // Partner with: check if they name each other (case-insensitive) 149 + if ( 150 + info1.partnerWithName && 151 + info1.partnerWithName.toLowerCase() === card2.name.toLowerCase() 152 + ) { 150 153 return { valid: true }; 151 154 } 152 - if (info2.partnerWithName && info2.partnerWithName === card1.name) { 155 + if ( 156 + info2.partnerWithName && 157 + info2.partnerWithName.toLowerCase() === card1.name.toLowerCase() 158 + ) { 153 159 return { valid: true }; 154 160 } 155 161