/* ============================================================ app.jsx — game orchestration for GTO Heads-Up ============================================================ */ const { useState, useEffect, useRef, useCallback } = React; const Eng = window.Engine; const G = window.GTO; const { Seat, Board, Pot, ActionBar, FeedbackOverlay, Toast } = window.UI; const { Header, CoachRail, SettingsModal } = window.UI2; const Sfx = window.Sfx || { forAction() {}, deal() {}, win() {}, lose() {}, click() {}, isEnabled: () => false, setEnabled() {} }; const LS = "gto_hu_v1"; function loadPrefs() { try { return JSON.parse(localStorage.getItem(LS)) || {}; } catch (e) { return {}; } } function savePrefs(p) { try { localStorage.setItem(LS, JSON.stringify(p)); } catch (e) {} } const blankSession = () => ({ hands: 0, decisions: 0, optimal: 0, bb: 0, evLost: 0, tally: { good: 0, ok: 0, warn: 0, bad: 0 } }); function normalizeAction(hand, a) { const legal = Eng.legalActions(hand).map((x) => x.type); if (!legal.includes(a.type)) { if (a.type === "bet" && legal.includes("check")) return { type: "check" }; if (a.type === "raise" && legal.includes("call")) return { type: "call", to: Eng.streetInvest(hand, 1 - hand.toAct) }; if (legal.includes("check")) return { type: "check" }; if (legal.includes("call")) return { type: "call", to: Eng.streetInvest(hand, 1 - hand.toAct) }; return { type: "fold" }; } return a; } function computeSpot(hand, heroIdx, showEquity) { const i = heroIdx; const me = hand.players[i]; const myBet = Eng.streetInvest(hand, i); const oppBet = Eng.streetInvest(hand, 1 - i); const toCall = Math.max(0, oppBet - myBet); const pot = Eng.potNow(hand); const potOdds = toCall > 0 ? (toCall / (pot + toCall)) * 100 : 0; const spr = pot > 0 ? me.stack / pot : null; let equity = null; if (showEquity) { // Use the SAME adaptive villain range the grader uses (it tightens as they show // aggression), so the equity shown on the table matches how the decision is graded. const oppPct = G.villainRangePct ? G.villainRangePct(hand, i) : 0.3; equity = Math.round(G.liveEquity(me.hole, hand.board, oppPct, [...me.hole, ...hand.board]) * 1000) / 10; } return { equity, potOdds, pot, spr, showEquity }; } function App({ onMenu }) { const prefs = loadPrefs(); const [ready, setReady] = useState(false); const [mode, setMode] = useState(prefs.mode || "study"); const [styleKey, setStyleKey] = useState(prefs.styleKey || "gto"); const [stack, setStack] = useState(prefs.stack || 100); const [session, setSession] = useState(prefs.session || blankSession()); const [, setTick] = useState(0); const rerender = useCallback(() => setTick((t) => t + 1), []); const handRef = useRef(null); const buttonRef = useRef(0); const botTimer = useRef(null); const keyRef = useRef(null); const sizeRef = useRef(null); const [busy, setBusy] = useState(false); const [waitingNext, setWaitingNext] = useState(false); const [pendingFb, setPendingFb] = useState(null); // {fb, action} const [lastFb, setLastFb] = useState(null); const [reviews, setReviews] = useState([]); // graded-decision history for the leak finder const [toast, setToast] = useState(null); const [railOpen, setRailOpen] = useState(false); const [railTab, setRailTab] = useState("Spot"); const [settings, setSettings] = useState(false); const modeRef = useRef(mode); modeRef.current = mode; const styleRef = useRef(styleKey); styleRef.current = styleKey; const sessionRef = useRef(session); sessionRef.current = session; // build preflop strength table once useEffect(() => { setTimeout(() => { G.buildStrength(); setReady(true); }, 30); }, []); useEffect(() => { savePrefs({ mode, styleKey, stack, session }); }, [mode, styleKey, stack, session]); // start first hand when ready useEffect(() => { if (ready && !handRef.current) newHand(); /* eslint-disable-next-line */ }, [ready]); // keyboard shortcuts: F fold, C call/check, A all-in, N next/continue useEffect(() => { const onKey = (e) => { const t = e.target; if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return; if (e.metaKey || e.ctrlKey || e.altKey) return; const k = e.key === " " || e.key === "Enter" ? "n" : e.key.toLowerCase(); if (["f", "c", "a", "r", "n"].includes(k)) { e.preventDefault(); keyRef.current && keyRef.current(k); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); const heroIdx = 0, botIdx = 1; const [muted, setMuted] = useState(!Sfx.isEnabled()); const toggleSound = () => { const on = !Sfx.isEnabled(); Sfx.setEnabled(on); setMuted(!on); if (on) Sfx.click(); }; function newHand() { if (botTimer.current) clearTimeout(botTimer.current); setWaitingNext(false); setPendingFb(null); setToast(null); buttonRef.current = sessionRef.current.hands % 2; // alternate button const st = { names: ["You", "Ivey"], stacks: [stack, stack], button: buttonRef.current }; const h = Eng.startHand(st); h.heroIdx = heroIdx; handRef.current = h; Sfx.deal(); setSession((s) => ({ ...s, hands: s.hands + 1 })); rerender(); afterApply(); } // Preset training spots: deal a hand and script the action up to a target decision for the hero. function startSpot(key) { if (key === "random") { newHand(); return; } if (botTimer.current) clearTimeout(botTimer.current); setWaitingNext(false); setPendingFb(null); setToast(null); setSettings(false); const heroIsSB = key === "sb_open" || key === "sb_vs_3bet"; buttonRef.current = heroIsSB ? heroIdx : botIdx; const st = { names: ["You", "Ivey"], stacks: [stack, stack], button: buttonRef.current }; const h = Eng.startHand(st); h.heroIdx = heroIdx; handRef.current = h; setSession((s) => ({ ...s, hands: s.hands + 1 })); try { if (key === "bb_vs_open") { Eng.applyAction(h, { type: "raise", to: 2.5 }); // bot (SB) opens } else if (key === "sb_vs_3bet") { Eng.applyAction(h, { type: "raise", to: 2.5 }); // hero (SB) auto-opens Eng.applyAction(h, { type: "raise", to: 9 }); // bot (BB) 3-bets } else if (key === "cbet_defense") { Eng.applyAction(h, { type: "raise", to: 2.5 }); // bot (SB) opens Eng.applyAction(h, { type: "call", to: 2.5 }); // hero (BB) calls -> flop if (h.street === "flop" && h.toAct === heroIdx) Eng.applyAction(h, { type: "check" }); // hero checks to the raiser if (h.street === "flop" && h.toAct === botIdx && !h.finished) { const pot = Eng.potNow(h); const to = Eng.streetInvest(h, botIdx) + Math.min(h.players[botIdx].stack, pot * 0.5); Eng.applyAction(h, { type: "bet", to }); // bot c-bets ~50% pot } } } catch (e) { console.warn("spot setup failed", e); } rerender(); afterApply(); } function afterApply() { const h = handRef.current; if (!h) return; if (h.finished) { finishHand(); return; } if (h.toAct === botIdx) { scheduleBot(); } rerender(); } function scheduleBot() { setBusy(true); rerender(); const delay = 600 + Math.random() * 700; botTimer.current = setTimeout(() => { const h = handRef.current; if (!h || h.finished || h.toAct !== botIdx) { setBusy(false); return; } let a = G.botDecide(h, styleRef.current); a = normalizeAction(h, a); Eng.applyAction(h, a); Sfx.forAction(a.type, h.players[botIdx].allIn && (a.type === "raise" || a.type === "bet")); setBusy(false); afterApply(); }, delay); } function finishHand() { const h = handRef.current; const delta = h.result ? h.result.delta[heroIdx] : 0; if (h.result) { if (h.result.winner === heroIdx) Sfx.win(); else if (h.result.type === "showdown" && h.result.winner === botIdx) Sfx.lose(); } setSession((s) => ({ ...s, bb: Math.round((s.bb + delta) * 10) / 10 })); setWaitingNext(true); setBusy(false); rerender(); } function recordDecision(fb) { setSession((s) => { const t = { ...s.tally }; t[fb.color] = (t[fb.color] || 0) + 1; const isOptimal = fb.color === "good" || fb.color === "ok"; return { ...s, decisions: s.decisions + 1, optimal: s.optimal + (isOptimal ? 1 : 0), evLost: Math.round((s.evLost + fb.evLoss) * 100) / 100, tally: t, }; }); } // Human-readable label for the spot a decision was made in (drives leak buckets). function situationLabel(h, fb) { if (h.street === "preflop") { return ({ SB_open: "SB open (RFI)", BB_vs_open: "BB vs open", SB_vs_3bet: "SB vs 3-bet", BB_vs_4bet: "BB vs 4-bet", SB_vs_jam: "SB vs jam", BB_option: "BB option", })[fb.node] || "Preflop"; } const inPos = h.button === heroIdx; const facing = Eng.streetInvest(h, botIdx) - Eng.streetInvest(h, heroIdx) > 0; const street = h.street.charAt(0).toUpperCase() + h.street.slice(1); return `${street} ${inPos ? "IP" : "OOP"}${facing ? " vs bet" : ""}`; } function pushReview(h, action, fb) { const rev = { id: Date.now() + "-" + Math.random().toString(36).slice(2, 6), situation: situationLabel(h, fb), hole: [...h.players[heroIdx].hole], chosenLabel: fb.chosenLabel, optimalLabel: fb.optimalLabel, color: fb.color, ratingLabel: fb.ratingLabel, evLoss: fb.evLoss, correct: fb.correct, }; setReviews((rs) => [rev, ...rs].slice(0, 200)); } function heroAct(action) { const h = handRef.current; if (!h || h.toAct !== heroIdx) return; const fb = G.evaluateDecision(h, heroIdx, action, styleRef.current); recordDecision(fb); pushReview(h, action, fb); setLastFb(fb); if (modeRef.current === "study") { setPendingFb({ fb, action }); } else { setToast(fb); setTimeout(() => setToast(null), 1700); applyHero(action); } } function applyHero(action) { const h = handRef.current; setPendingFb(null); Eng.applyAction(h, action); Sfx.forAction(action.type, h.players[heroIdx].allIn && (action.type === "raise" || action.type === "bet")); afterApply(); } function continueFromStudy() { if (!pendingFb) return; const action = pendingFb.action; setPendingFb(null); applyHero(action); } function resetSession() { setSession(blankSession()); setLastFb(null); setReviews([]); } if (!ready) { return (
GTO HEADS-UP
solving preflop ranges…
); } const h = handRef.current; const heroToAct = h && !h.finished && h.toAct === heroIdx && !pendingFb; const reveal = h && h.finished && h.result && h.result.type === "showdown"; const spot = h && heroToAct ? computeSpot(h, heroIdx, mode === "study") : (lastFb ? null : null); const spotForRail = h && heroToAct ? computeSpot(h, heroIdx, mode === "study") : null; const heroPos = h ? (h.button === heroIdx ? "BTN" : "BB") : "BB"; const botPos = h ? (h.button === botIdx ? "BTN" : "BB") : "BB"; const streetLabel = h ? ({ preflop: "PRE-FLOP", flop: "FLOP", turn: "TURN", river: "RIVER", showdown: "SHOWDOWN", done: "" })[h.street] : ""; keyRef.current = (k) => { if (pendingFb) { if (k === "n" || k === "c") continueFromStudy(); return; } if (waitingNext) { if (k === "n" || k === "c") newHand(); return; } if (!h || h.finished || h.toAct !== heroIdx) return; const legal = Eng.legalActions(h).map((a) => a.type); const me = h.players[heroIdx]; const oppBet = Eng.streetInvest(h, botIdx); if (k === "f") heroAct(legal.includes("fold") ? { type: "fold" } : { type: "check" }); else if (k === "c") heroAct(legal.includes("call") ? { type: "call", to: oppBet } : { type: "check" }); else if (k === "r" && (legal.includes("raise") || legal.includes("bet")) && sizeRef.current != null) heroAct({ type: legal.includes("raise") ? "raise" : "bet", to: sizeRef.current }); else if (k === "a" && (legal.includes("raise") || legal.includes("bet"))) heroAct({ type: legal.includes("raise") ? "raise" : "bet", to: Eng.streetInvest(h, heroIdx) + me.stack }); }; return (
setSettings(true)} onMenu={onMenu} muted={muted} onToggleSound={toggleSound} />
{h &&
{streetLabel}
} {h && spotForRail && spotForRail.showEquity && (
{window.UI.fmt(spotForRail.equity)}%
your equity
)} {h && ( )} {h && (
{h.finished && h.result && (
{h.result.type === "fold" ? (h.result.winner === heroIdx ? `${h.players[botIdx].name} folds \u2014 you win` : "You fold") : (h.result.winner === heroIdx ? `You win \u2014 ${h.result.hands[heroIdx]}` : h.result.winner === botIdx ? `${h.players[botIdx].name} wins \u2014 ${h.result.hands[botIdx]}` : "Split pot")}
)}
)} {h && ( )} {pendingFb && } {toast && }
{h && ( )}
setRailOpen(false)} tab={railTab} setTab={setRailTab} spot={spotForRail} lastFb={lastFb} session={session} hand={h} mode={mode} reviews={reviews} />
setSettings(false)} styleKey={styleKey} setStyle={setStyleKey} stack={stack} setStack={setStack} onReset={resetSession} onDrill={startSpot} />
); } function Root() { const [screen, setScreen] = useState("menu"); if (screen === "menu") return ; if (screen === "tourney") return setScreen("menu")} />; return setScreen("menu")} />; } ReactDOM.createRoot(document.getElementById("root")).render();