{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "poker-core",
  "title": "Poker core",
  "description": "Hand classes, the action, suit and grade palettes, and response types for the Poker Study AI strategy API.",
  "files": [
    {
      "path": "src/lib/pokerstudy/poker.ts",
      "content": "/**\n * Poker Study AI registry core: hand classes, the action / suit / grade\n * palettes, and the response types for the strategy API. Every other\n * `@pokerstudy` registry item builds on this file, and the app's own strategy\n * grid and cards read their colors from here too, so an installed component\n * looks exactly like it does on pokerstudy.ai.\n *\n * No imports, no React: safe on the server and the client.\n */\n\n/** Ranks high to low. The 13×13 hand matrix is indexed by these. */\nexport const RANKS = [\"A\", \"K\", \"Q\", \"J\", \"T\", \"9\", \"8\", \"7\", \"6\", \"5\", \"4\", \"3\", \"2\"] as const;\n\n/**\n * Every 169 hand class in matrix order, row-major: suited above the diagonal,\n * offsuit below, pairs on it (\"AA\", \"AKs\", …, \"32o\"). Higher rank first.\n */\nexport const ALL_HANDS: string[] = (() => {\n  const hands: string[] = [];\n  for (let r = 0; r < 13; r++) {\n    for (let c = 0; c < 13; c++) {\n      const hi = RANKS[Math.min(r, c)];\n      const lo = RANKS[Math.max(r, c)];\n      if (r === c) hands.push(`${hi}${hi}`);\n      else hands.push(`${hi}${lo}${r < c ? \"s\" : \"o\"}`);\n    }\n  }\n  return hands;\n})();\n\n/** Concrete combos in a class, ignoring card removal: pair 6, suited 4, offsuit 12. */\nexport function comboCount(hand: string): number {\n  if (hand.length === 2) return 6;\n  return hand.endsWith(\"s\") ? 4 : 12;\n}\n\nexport type ActionKind = \"fold\" | \"call\" | \"raise\" | \"allin\" | \"other\";\n\n/** One action at a preflop decision, as `GET /api/ranges/nl/v2/node` returns it. */\nexport interface NodeAction {\n  /** Raw action token: \"Fold\", \"Call\", \"Limp\", \"Open\", a size like \"77%\", or \"AI\". */\n  action: string;\n  kind: ActionKind;\n  /** Hand class → frequency of this action, in [0, 1]. */\n  weights: Record<string, number>;\n  /** Hand class → EV of this action in small blinds. */\n  evs?: Record<string, number>;\n  sizePct?: number;\n  combos?: number;\n}\n\n/** A whole preflop decision: who acts and every action they can take. */\nexport interface NodeResponse {\n  game: string;\n  stack: number;\n  history: string;\n  actor: string;\n  actions: NodeAction[];\n  continuingCombos?: number;\n  /** Present when the request named a `hand`. Action token → frequency. */\n  hand?: string;\n  strategy?: Record<string, number>;\n}\n\nexport type Grade = \"perfect\" | \"correct\" | \"inaccuracy\" | \"mistake\" | \"blunder\";\n\n/** `POST /api/play/evaluate`: the selected action scored against the agent's own. */\nexport interface EvaluateResponse {\n  equity: number;\n  model: string;\n  selected: { action: string; amount?: number; ev: number };\n  bot: { action: string; amount?: number; ev: number; source?: string; position?: string };\n  /** Big blinds given up versus the agent's action. Never negative. */\n  ev_loss_bb: number;\n  grade: Grade;\n}\n\n/**\n * Action colors. Raises are red, folds and checks blue, calls green. Fold and\n * Check share a color because they never appear at the same decision.\n */\nexport const ACTION_COLORS = {\n  Fold: \"#3b82f6\",\n  Check: \"#3b82f6\",\n  Call: \"#10b981\",\n  Bet: \"#ff4757\",\n  Raise: \"#c41e3a\",\n  AllIn: \"#8b1538\",\n} as const;\n\n/** Raises darken by size, so several raise sizes stay distinct in one cell. */\nconst RAISE_PALETTE = [ACTION_COLORS.Bet, \"#e0294f\", \"#c41f44\", \"#9c1838\", ACTION_COLORS.AllIn];\n\nconst KIND_ORDER: Record<ActionKind, number> = { fold: 0, call: 1, raise: 2, allin: 3, other: 4 };\n\n/** Fold → call → raises (small to large) → all-in. */\nexport function sortActions<T extends Pick<NodeAction, \"kind\" | \"sizePct\">>(actions: T[]): T[] {\n  return [...actions].sort(\n    (a, b) => KIND_ORDER[a.kind] - KIND_ORDER[b.kind] || (a.sizePct ?? 0) - (b.sizePct ?? 0),\n  );\n}\n\n/** Display label for an action token. `stack` labels the all-in. */\nexport function actionLabel(action: Pick<NodeAction, \"action\" | \"kind\">, stack?: number): string {\n  if (action.kind === \"fold\") return \"Fold\";\n  if (action.action === \"Limp\") return \"Limp\";\n  if (action.kind === \"call\") return \"Call\";\n  if (action.kind === \"allin\" || action.action === \"AI\") return stack ? `All-in ${stack}` : \"All-in\";\n  if (action.action === \"Open\") return \"Open\";\n  if (action.kind === \"raise\") return `Raise ${action.action}`;\n  return action.action;\n}\n\n/** The node's actions in display order, each with a stable color and label. */\nexport function colorActions(\n  actions: NodeAction[],\n  stack?: number,\n): { action: NodeAction; label: string; color: string }[] {\n  let raise = 0;\n  return sortActions(actions).map((action) => {\n    let color: string;\n    if (action.kind === \"fold\") color = ACTION_COLORS.Fold;\n    else if (action.kind === \"call\") color = ACTION_COLORS.Call;\n    else if (action.kind === \"allin\") color = ACTION_COLORS.AllIn;\n    else color = RAISE_PALETTE[Math.min(raise++, RAISE_PALETTE.length - 1)];\n    return { action, label: actionLabel(action, stack), color };\n  });\n}\n\n/** Color for an action by kind; a label starting with \"Bet\" picks the bet red. */\nexport function actionColor(kind: ActionKind | \"check\", label?: string): string {\n  switch (kind) {\n    case \"fold\":\n      return ACTION_COLORS.Fold;\n    case \"check\":\n      return ACTION_COLORS.Check;\n    case \"call\":\n      return ACTION_COLORS.Call;\n    case \"allin\":\n      return ACTION_COLORS.AllIn;\n    case \"raise\":\n      return label?.toLowerCase().startsWith(\"bet\") ? ACTION_COLORS.Bet : ACTION_COLORS.Raise;\n    default:\n      return \"#52525b\";\n  }\n}\n\n/**\n * Four-color deck. Faces read CSS variables so the registry docs can restyle\n * them; the fallbacks are the colors the rest of the app already uses, and\n * they apply wherever those variables are unset.\n */\nexport const SUIT_FACES = {\n  h: { sym: \"♥\", token: \"--ps-suit-heart\", fallback: \"#e0392b\" },\n  d: { sym: \"♦\", token: \"--ps-suit-diamond\", fallback: \"#2f6be0\" },\n  c: { sym: \"♣\", token: \"--ps-suit-club\", fallback: \"#1ea05f\" },\n  s: { sym: \"♠\", token: \"--ps-suit-spade\", fallback: \"#232329\" },\n} as const;\n\n/** Card face color for one suit character (\"h\", \"d\", \"c\", \"s\"). */\nexport function suitTile(suit: string): { sym: string; bg: string } {\n  const face = SUIT_FACES[suit.toLowerCase() as keyof typeof SUIT_FACES];\n  if (!face) return { sym: suit, bg: `var(${SUIT_FACES.s.token}, ${SUIT_FACES.s.fallback})` };\n  return { sym: face.sym, bg: `var(${face.token}, ${face.fallback})` };\n}\n\n/** Grade ramp, Call green for perfect through Raise crimson for a blunder. */\nexport const GRADE_COLORS: Record<Grade, string> = {\n  perfect: \"#10b981\",\n  correct: \"#84cc16\",\n  inaccuracy: \"#eab308\",\n  mistake: \"#f97316\",\n  blunder: \"#c41e3a\",\n};\n\nexport const GRADE_LABELS: Record<Grade, string> = {\n  perfect: \"Perfect\",\n  correct: \"Correct\",\n  inaccuracy: \"Inaccuracy\",\n  mistake: \"Mistake\",\n  blunder: \"Blunder\",\n};\n\n/** A counted frequency: `num` of `denom` opportunities, `pct` in [0, 1]. */\nexport interface StatValue {\n  num: number;\n  denom: number;\n  pct: number;\n}\n\nexport const STAT_KEYS = [\n  \"vpip\",\n  \"pfr\",\n  \"rfi\",\n  \"threeBet\",\n  \"foldToThreeBet\",\n  \"cbetFlop\",\n  \"cbetFlopIp\",\n  \"cbetFlopOop\",\n  \"foldToCbetFlop\",\n  \"checkRaiseFlop\",\n  \"cbetTurn\",\n  \"cbetRiver\",\n  \"wtsd\",\n  \"wonAtShowdown\",\n] as const;\nexport type StatKey = (typeof STAT_KEYS)[number];\n\n/** HUD stats for one player, aggregated from their hands. */\nexport type PlayerStats = { hands: number; netChips: number } & Record<StatKey, StatValue>;\n\nexport const STAT_LABELS: Record<StatKey, string> = {\n  vpip: \"VPIP\",\n  pfr: \"PFR\",\n  rfi: \"RFI\",\n  threeBet: \"3-bet\",\n  foldToThreeBet: \"Fold to 3-bet\",\n  cbetFlop: \"C-bet flop\",\n  cbetFlopIp: \"C-bet flop IP\",\n  cbetFlopOop: \"C-bet flop OOP\",\n  foldToCbetFlop: \"Fold to flop c-bet\",\n  checkRaiseFlop: \"Check-raise flop\",\n  cbetTurn: \"Double barrel\",\n  cbetRiver: \"Triple barrel\",\n  wtsd: \"Went to showdown\",\n  wonAtShowdown: \"Won at showdown\",\n};\n\n/** `GET /api/sessions`: the key owner's uploaded sessions, newest first. */\nexport interface SessionListItem {\n  id: string;\n  hero: string;\n  createdAt: string;\n  firstHandAt: string | null;\n  lastHandAt: string | null;\n  totalHands: number;\n  heroHandsPlayed: number;\n  netMbb: number;\n  sourceName: string | null;\n}\n\n/** One session's summary and the hero's stats (trimmed by the proxy). */\nexport interface SessionSummary {\n  id: string;\n  hero: string;\n  createdAt: string;\n  heroStats: PlayerStats;\n  session: {\n    totalHands: number;\n    heroHandsPlayed: number;\n    firstTs: string | null;\n    lastTs: string | null;\n    gameType: string | null;\n  };\n}\n\n/** A note Poker Study AI wrote about an opponent once a pattern had enough hands. */\nexport interface OpponentNote {\n  id: string;\n  street: string;\n  spotLabel: string;\n  leakType: string;\n  sampleSize: number;\n  autoGenerated: boolean;\n  manualText: string | null;\n}\n\nexport interface OpponentListItem {\n  id: string;\n  displayName: string;\n  handCount: number;\n  noteCount: number;\n  lastSeenAt: string | null;\n  gameTypes: string[];\n}\n\n/** `GET /api/opponents/{id}`. */\nexport interface OpponentDetail {\n  opponent: { id: string; displayName: string; handCount: number; firstSeenAt: string | null; lastSeenAt: string | null };\n  notes: OpponentNote[];\n  stats: PlayerStats;\n}\n\n/** `POST /api/analytics/opponent-limp-range`: hands opponents showed after limping first in. */\nexport interface LimpRange {\n  found: boolean;\n  shownLimps: number;\n  classes: { handClass: string; count: number; pct: number }[];\n  byPosition: Record<string, number>;\n  note?: string;\n}\n\ntype Tally = Record<Grade, number>;\n\n/** `GET /api/trainer/stats`: drill grades by stack depth and preflop scenario. */\nexport interface TrainerStats {\n  overall: { hands: number; correctPct: number; tally: Tally };\n  buckets: {\n    key: string;\n    label: string;\n    hands: number;\n    correctPct: number;\n    scenarios: { key: string; label: string; hands: number; correctPct: number; tally: Tally }[];\n  }[];\n}\n\n/** `POST /api/play/decide`: what the agent does at a table snapshot. */\nexport interface DecideResponse {\n  action: string;\n  amount?: number | null;\n  /** Which model answered, e.g. \"ranges\" (solver preflop) or \"equity\". */\n  source?: string;\n  [extra: string]: unknown;\n}\n\n/** A table snapshot in the Poker Study AI agent's wire format (cards like \"Ah\"). */\nexport interface TableSnapshot {\n  hole: string[];\n  board?: string[];\n  pot?: number;\n  big_blind?: number;\n  my_seat: number;\n  dealer_seat: number;\n  seats: number[];\n  valid_actions: (\n    | { action: \"fold\" | \"check\" }\n    | { action: \"call\"; amount: number }\n    | { action: \"raise\"; min: number; max: number }\n  )[];\n  [extra: string]: unknown;\n}\n",
      "type": "registry:lib",
      "target": "lib/pokerstudy/poker.ts"
    }
  ],
  "type": "registry:lib"
}