/* ============================================================ components.jsx — presentational UI for GTO Heads-Up Exposes components on window for app.jsx ============================================================ */ const { useState, useEffect, useRef } = React; const E = window.Engine; const fmt = (x) => { const r = Math.round(x * 10) / 10; return Number.isInteger(r) ? String(r) : r.toFixed(1); }; /* ---------- Cards ---------- */ function Card({ c, back, muck, tiny }) { if (back) return
; if (c === null || c === undefined) return
; const lab = E.cardLabel(c); const red = lab.s === "h" || lab.s === "d"; const rank = lab.r === "T" ? "10" : lab.r; return (
{rank}
{lab.sym}
); } function Hole({ cards, hidden, muck, tiny }) { return (
{hidden ? [0, 1].map((i) => ) : cards.map((c, i) => )}
); } /* ---------- Seat ---------- */ function Seat({ player, isHero, active, position, reveal, bet }) { return (
{isHero ? "\u2665" : "\u25C6"}
{player.name} {position}
{fmt(player.stack)} bb
); } /* ---------- Board + Pot ---------- */ function Board({ board }) { const slots = []; for (let i = 0; i < 5; i++) slots.push(i < board.length ? :
); return
{slots}
; } function Pot({ pot }) { return (
Pot {fmt(pot)} bb
); } /* ---------- Action bar ---------- */ function targetForFrac(hand, i, frac) { const myBet = E.streetInvest(hand, i); const oppBet = E.streetInvest(hand, 1 - i); const toCall = Math.max(0, oppBet - myBet); const pot = E.potNow(hand); let to; if (toCall > 0) { const potAfter = pot + toCall; to = oppBet + frac * potAfter; } else { to = myBet + frac * pot; } return to; } function ActionBar({ hand, hero, onAction, busy, waitingNext, onNext, dealing, mode, sizeRef }) { const i = hero; const me = hand.players[i]; const myBet = E.streetInvest(hand, i); const oppBet = E.streetInvest(hand, 1 - i); const toCall = Math.max(0, oppBet - myBet); const pot = E.potNow(hand); const minTo = Math.min(E.minRaiseTo(hand), myBet + me.stack); const maxTo = myBet + me.stack; const canAggro = me.stack > toCall; const presets = hand.street === "preflop" && toCall > 0 && oppBet <= E.BB_SIZE + 0.01 ? [["2.5x", null, 2.5], ["3x", null, 3], ["3.5x", null, 3.5], ["Pot", 1, null], ["All-in", null, maxTo]] : [["33%", 0.33], ["50%", 0.5], ["75%", 0.75], ["Pot", 1], ["All-in", null, maxTo]]; const [to, setTo] = useState(minTo); const [activePreset, setActivePreset] = useState(null); useEffect(() => { // default sizing per street let def; if (hand.street === "preflop" && toCall > 0 && oppBet <= E.BB_SIZE + 0.01) def = Math.min(maxTo, 2.5 * E.BB_SIZE); else def = Math.min(maxTo, Math.max(minTo, targetForFrac(hand, i, 0.66))); setTo(Math.round(def * 10) / 10); setActivePreset(null); // eslint-disable-next-line }, [hand.street, hand.history.length, toCall, hand.toAct]); if (sizeRef) sizeRef.current = (!waitingNext && !busy && hand.toAct === hero && !hand.finished && canAggro) ? to : null; if (waitingNext) { return (
); } if (busy || hand.toAct !== hero || hand.finished) { return (
{hand.finished ? "Hand complete" : `${(hand.players[1 - hero] && hand.players[1 - hero].name) || "Opponent"} is thinking\u2026`}
); } const applyPreset = (p) => { let val; if (p[2] != null && p[0] === "All-in") val = maxTo; else if (hand.street === "preflop" && p[2] != null) val = Math.min(maxTo, p[2] * E.BB_SIZE); else if (p[1] != null) val = targetForFrac(hand, i, p[1]); else val = to; val = Math.max(minTo, Math.min(maxTo, val)); setTo(Math.round(val * 10) / 10); setActivePreset(p[0]); }; const raiseType = toCall > 0 ? "raise" : "bet"; const raiseVerb = toCall > 0 ? "Raise" : "Bet"; return (
{canAggro && (
{presets.map((p) => ( ))}
{ setTo(parseFloat(e.target.value)); setActivePreset(null); }} /> {fmt(to)} bb
)}
{toCall > 0 ? : } {toCall > 0 && ( )} {canAggro && ( )}
F {toCall > 0 ? "fold" : "check"} · C {toCall > 0 ? "call" : "check"}{canAggro && <>{" · "}R raise{" · "}A all-in} · N next
); } /* ---------- Action colors + solver mix bar ---------- */ const ACT_COLOR = { raise: "var(--accent)", bet: "var(--accent)", call: "var(--ok)", check: "var(--text-3)", fold: "var(--bad)" }; function MixBar({ mix }) { if (!mix || !mix.length) return null; return (
{mix.map((m, k) => (
))}
); } /* ---------- Study-mode feedback overlay ---------- */ function FeedbackOverlay({ fb, onContinue }) { if (!fb) return null; const cls = fb.color === "good" ? "fb-good" : fb.color === "ok" ? "fb-ok" : fb.color === "warn" ? "fb-warn" : "fb-bad"; return (
e.stopPropagation()}>
{ratingIcon(fb.color)} {fb.ratingLabel} {fb.evLoss > 0 ? `\u2212${fmt(fb.evLoss)} bb EV` : "0 bb lost"}
You played
{fb.chosenLabel}
GTO play
{fb.optimalLabel}
{fb.correct ? "✓" : "✦"} {fb.correct ? "Best play — you nailed it: " : "Solution — you should: "}{fb.solution}
{fb.mixLabel && } {fb.mixLabel &&
{fb.mixSource === "solver" ? "Solver mix" : "GTO mix"}{fb.mixLabel}
}
Your equity{fmt(fb.equity)}%
{fb.potOdds > 0 &&
Pot odds (need){fmt(fb.potOdds)}%
}
{fb.note}
); } function ratingIcon(color) { if (color === "good") return "\u2713"; if (color === "ok") return "\u2248"; if (color === "warn") return "!"; return "\u2715"; } /* ---------- Toast (play mode) ---------- */ function Toast({ fb }) { if (!fb) return null; const c = fb.color === "good" ? "var(--good)" : fb.color === "ok" ? "var(--ok)" : fb.color === "warn" ? "var(--warn)" : "var(--bad)"; return (
{ratingIcon(fb.color)} {fb.ratingLabel} {fb.evLoss > 0 && −{fmt(fb.evLoss)} bb}
); } window.UI = { Card, Hole, Seat, Board, Pot, ActionBar, FeedbackOverlay, Toast, MixBar, ACT_COLOR, fmt, ratingIcon, targetForFrac };