{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mrdoge-conflict-adapter",
  "title": "Conflict Adapter (Mr. Doge SDK)",
  "description": "Flags mutually incompatible picks (Match Result vs Double Chance, Over/Under threshold overlap, BTTS vs low Totals) for disabledIds or conflictingPickIds.",
  "dependencies": [
    "@mrdoge/protocol"
  ],
  "registryDependencies": [
    "bet-slip"
  ],
  "files": [
    {
      "path": "lib/mrdoge-adapters/conflicts.ts",
      "content": "import type { Market } from \"@mrdoge/protocol\"\nimport type { BetSlipPick } from \"@/registry/mrdoge-ui/bet-slip/bet-slip\"\n\nexport interface ConflictCandidate {\n  id: string\n  matchId: string\n  /** Market sysname, e.g. \"SOCCER_UNDER_OVER\". */\n  betType: string\n  /** Outcome code, e.g. \"O\", \"1X\". */\n  code: string\n  /** Parsed Over/Under threshold; only set when `code` is \"O\"/\"U\". */\n  threshold?: number\n}\n\nconst MATCH_RESULT_TYPES = [\"SOCCER_MATCH_RESULT\", \"SOCCER_MATCH_RESULT_PRELIVE\"]\nconst DOUBLE_CHANCE = \"SOCCER_DOUBLE_CHANCE\"\nconst TOTALS = [\"SOCCER_UNDER_OVER\", \"SOCCER_UNDER_OVER_PRELIVE\"]\nconst BTTS = [\"SOCCER_BOTH_TEAMS_TO_SCORE\", \"SOCCER_BOTH_TEAMS_TO_SCORE_PRELIVE\"]\nconst BTTS_YES_CODES = [\"Y\", \"GG\"]\n\n// Over/Under outcomes only carry a generic \"O\"/\"U\" code, no threshold in\n// it. The threshold lives in the caption instead (\"Over 1.5\", \"Under\n// 1.5\"), so that's what has to be parsed to compare thresholds.\nfunction parseThreshold(caption: string | null | undefined): number | undefined {\n  const match = caption?.match(/[\\d.]+/)\n  const value = match ? parseFloat(match[0]) : NaN\n  return Number.isNaN(value) ? undefined : value\n}\n\n/**\n * Flattens every line of every market for one match into conflict\n * candidates, parsing an Over/Under threshold from the caption where\n * applicable.\n */\nexport function toConflictCandidates(matchId: string, markets: Market[]): ConflictCandidate[] {\n  const candidates: ConflictCandidate[] = []\n  for (const market of markets) {\n    for (const line of market.lines) {\n      const isTotals = line.code.startsWith(\"O\") || line.code.startsWith(\"U\")\n      candidates.push({\n        id: line.id,\n        matchId,\n        betType: market.betType,\n        code: line.code,\n        threshold: isTotals ? parseThreshold(line.caption) : undefined,\n      })\n    }\n  }\n  return candidates\n}\n\n// Pairwise incompatibility check: the three rule families this adapter\n// knows about. Symmetric: conflicts(a, b) === conflicts(b, a).\nfunction conflicts(a: ConflictCandidate, b: ConflictCandidate): boolean {\n  if (a.matchId !== b.matchId) return false\n\n  // Double Chance is just two of Match Result's three outcomes combined:\n  // the two markets describe overlapping information about the same\n  // result, not independent events. Even a \"compatible\" pair (Home win +\n  // \"Home or Away\") is a wasted leg, not a real parlay combination, so\n  // any Match Result selection blocks the entire Double Chance market and\n  // vice versa, not just the specific pairs that are outright impossible.\n  const isMrDcPair =\n    (MATCH_RESULT_TYPES.includes(a.betType) && b.betType === DOUBLE_CHANCE) ||\n    (MATCH_RESULT_TYPES.includes(b.betType) && a.betType === DOUBLE_CHANCE)\n  if (isMrDcPair) return true\n\n  if (TOTALS.includes(a.betType) && TOTALS.includes(b.betType)) {\n    if (a.threshold === undefined || b.threshold === undefined) return false\n    const aIsOver = a.code.startsWith(\"O\")\n    const bIsOver = b.code.startsWith(\"O\")\n\n    if (aIsOver === bIsOver) {\n      // Same side at different thresholds, e.g. Over 1.5 + Over 2.5: the\n      // higher threshold winning always means the lower one wins too, so\n      // the pair is never a genuinely new combination, just a redundant\n      // (or actively worse) parlay leg.\n      return a.threshold !== b.threshold\n    }\n\n    // Opposite sides: Over-at-T1 and Under-at-T2 are only both true when\n    // goals can land strictly between the two thresholds, e.g. Over 1.5 +\n    // Under 4.5 (2, 3, or 4 goals). Impossible once T1 >= T2, e.g. Under\n    // 1.5 + Over 4.5.\n    const overThreshold = aIsOver ? a.threshold : b.threshold\n    const underThreshold = aIsOver ? b.threshold : a.threshold\n    return overThreshold >= underThreshold\n  }\n\n  const [btts, totals] = BTTS.includes(a.betType) && TOTALS.includes(b.betType)\n    ? [a, b]\n    : BTTS.includes(b.betType) && TOTALS.includes(a.betType)\n      ? [b, a]\n      : []\n  if (btts && totals) {\n    const isBttsYes = BTTS_YES_CODES.includes(btts.code)\n    const isLowUnder = totals.code.startsWith(\"U\") && totals.threshold !== undefined && totals.threshold <= 1.5\n    return isBttsYes && isLowUnder\n  }\n\n  return false\n}\n\n/**\n * Given the ids currently selected and every candidate available (across\n * however many markets/matches are in play), returns the ids of\n * not-yet-selected candidates that conflict with at least one current\n * selection, e.g. for an Odds Selector's `disabledIds`. Same-match\n * scoping is baked into the comparison, not left to caller discipline.\n */\nexport function getConflictingIds(selectedIds: string[], candidates: ConflictCandidate[]): Set<string> {\n  const selected = candidates.filter((candidate) => selectedIds.includes(candidate.id))\n  const conflicting = new Set<string>()\n  for (const candidate of candidates) {\n    if (selectedIds.includes(candidate.id)) continue\n    if (selected.some((sel) => conflicts(sel, candidate))) {\n      conflicting.add(candidate.id)\n    }\n  }\n  return conflicting\n}\n\n/**\n * Convenience wrapper for consumers who only have already-built\n * `BetSlipPick[]` in hand (e.g. loaded from storage, no live `Market`\n * data around). Checks every pick against every other pick already in\n * the slip and returns the ids of any that conflict with each other, for\n * e.g. BetSlip's `conflictingPickIds`. Picks missing `matchId`/`betType`/\n * `code` (built by hand rather than via the Bet Slip Adapter) are skipped.\n */\nexport function getConflictingPickIds(picks: BetSlipPick[]): Set<string> {\n  const candidates: ConflictCandidate[] = picks\n    .filter((pick) => pick.matchId && pick.betType && pick.code)\n    .map((pick) => ({\n      id: pick.id,\n      matchId: pick.matchId!,\n      betType: pick.betType!,\n      code: pick.code!,\n      threshold: pick.threshold,\n    }))\n\n  const conflicting = new Set<string>()\n  for (let i = 0; i < candidates.length; i++) {\n    for (let j = i + 1; j < candidates.length; j++) {\n      if (conflicts(candidates[i], candidates[j])) {\n        conflicting.add(candidates[i].id)\n        conflicting.add(candidates[j].id)\n      }\n    }\n  }\n  return conflicting\n}\n",
      "type": "registry:lib",
      "target": "lib/mrdoge-adapters/conflicts.ts"
    }
  ],
  "type": "registry:lib"
}