/* ============================================================ tournament.jsx — main menu + 9-max single-table tournament (SNG). Uses window.Table (engine + controller), window.Charts (multiway GTO grading), window.Engine (equity), and reuses window.UI feedback widgets. Exposes window.TournamentUI = { Menu, TournamentApp }. ============================================================ */ const { useState: useT, useRef: useR, useEffect: useE, useCallback: useC } = React; const Tbl = window.Table, Ch = window.Charts, Eg = window.Engine, GT = window.GTO; const { FeedbackOverlay: FBOverlay, Toast: TToast } = window.UI; const Sfx = window.Sfx || { forAction() {}, deal() {}, win() {}, lose() {}, click() {}, isEnabled: () => false, setEnabled() {} }; const chips = (n) => Math.round(n).toLocaleString(); // 9 seat anchor points around the oval rim (% of table box). id 0 = hero, bottom-center. const POS9 = [ { x: 50, y: 93 }, { x: 18, y: 85 }, { x: 6, y: 57 }, { x: 11, y: 27 }, { x: 33, y: 10 }, { x: 67, y: 10 }, { x: 89, y: 27 }, { x: 94, y: 57 }, { x: 82, y: 85 }, ]; const TCENTER = { x: 50, y: 50 }; // point between a seat and the pot (for bet chips / dealer button) const lerp = (p, t) => ({ x: TCENTER.x + (p.x - TCENTER.x) * t, y: TCENTER.y + (p.y - TCENTER.y) * t }); /* ---------- bot policy (multiway) ---------- */ function botMove(H, id) { const s = H.seats[id], bb = H.blinds.bb; const toCall = Math.max(0, H.currentBet - s.street); const legal = Tbl.legalActions(H).map((a) => a.type); const has = (t) => legal.includes(t); const sizeTo = (mult) => Math.min(Tbl.maxRaiseTo(H), Math.max(Tbl.minRaiseTo(H), Math.round(mult))); if (H.street === "preflop") { const code = GT.codeOfHole(s.hole); let behind = 0; for (const oid of H.ring) { const x = H.seats[oid]; if (oid !== id && !x.folded && !x.acted) behind++; } const raised = H.currentBet > bb + 1e-9; if (!raised && toCall === 0) return GT.pctlOf(s.hole) > 0.9 && has("raise") ? { type: "raise", to: sizeTo(2.3 * bb) } : { type: "check" }; const facing = !raised ? "first-in" : (toCall >= s.stack * 0.6 ? "shove" : "raise"); const adv = Ch.advise({ code, effStackBB: s.stack / bb, playersBehind: behind, facing, toCallBB: toCall / bb, potBB: Tbl.potTotal(H) / bb, posName: "" }); if (adv.action === "shove" && has("raise")) return { type: "raise", to: s.street + s.stack }; if (adv.action === "raise" && has("raise")) return { type: "raise", to: sizeTo(raised ? H.currentBet * 3 : 2.3 * bb) }; if (adv.action === "call" && has("call")) return { type: "call", to: H.currentBet }; if (has("check")) return { type: "check" }; return { type: "fold" }; } const numOpp = H.ring.filter((o) => !H.seats[o].folded).length - 1; const eq = Eg.equityMulti(s.hole, H.board, Math.max(1, numOpp), 200); const pot = Tbl.potTotal(H), potOdds = toCall > 0 ? toCall / (pot + toCall) : 0; if (toCall > 0) { if (eq > 0.66 && has("raise") && Math.random() < 0.5) return { type: "raise", to: sizeTo(H.currentBet + pot * 0.6) }; if (eq >= potOdds + 0.03 && has("call")) return { type: "call", to: H.currentBet }; return has("check") ? { type: "check" } : { type: "fold" }; } if (eq > 0.55 && has("bet")) return { type: "bet", to: s.street + Math.round(pot * 0.6) }; return { type: "check" }; } /* ---------- main menu ---------- */ function Menu({ onPick }) { return (
GTO POKER
A No-Limit Hold'em trainer that grades every decision against a solver — with a detailed reason why.
♥ 7-card evaluator◆ Monte-Carlo equity♣ Mixed-strategy GTO★ Why on every play
Heads-up preflop is a real live solve. Multiway & postflop use honest published charts + equity heuristics — not a real-time solve.
); } /* ---------- tournament table ---------- */ function TournamentApp({ onMenu }) { const [, setTick] = useT(0); const rerender = useC(() => setTick((t) => t + 1), []); const tourRef = useR(null); const handRef = useR(null); const botTimer = useR(null); const keyRef = useR(null); const sizeRef = useR(null); // current raise sizing exposed by the action bar (for the R key) const heroSeat = 0; const [busy, setBusy] = useT(false); const [waitNext, setWaitNext] = useT(false); const [pending, setPending] = useT(null); const [toast, setToast] = useT(null); const [study, setStudy] = useT(true); const [msg, setMsg] = useT(""); const [muted, setMuted] = useT(!Sfx.isEnabled()); const toggleSound = () => { const on = !Sfx.isEnabled(); Sfx.setEnabled(on); setMuted(!on); if (on) Sfx.click(); }; useE(() => { newTourney(); return () => clearTimeout(botTimer.current); /* eslint-disable-next-line */ }, []); // keyboard shortcuts: F fold, C call/check, A all-in, N next/continue useE(() => { 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 === " " ? "n" : e.key.toLowerCase(); if (["f", "c", "a", "r", "n", "enter"].includes(k)) { e.preventDefault(); keyRef.current && keyRef.current(k === "enter" ? "n" : k); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); function newTourney() { clearTimeout(botTimer.current); setWaitNext(false); setPending(null); setToast(null); setMsg(""); tourRef.current = Tbl.createTournament({ players: 9, startStack: 1500, heroSeat }); nextHand(); } function nextHand() { const T = tourRef.current; setWaitNext(false); setPending(null); setToast(null); setMsg(""); const H = Tbl.startHand(T); handRef.current = H; Sfx.deal(); rerender(); step(); } function canHeroAct(H) { return H && !H.finished && H.toAct === heroSeat && !H.seats[heroSeat].folded && !H.seats[heroSeat].allIn && H.seats[heroSeat].stack > 0; } function step() { const H = handRef.current; if (!H) return; if (H.finished) { onHandEnd(); return; } if (canHeroAct(H)) { setBusy(false); rerender(); return; } setBusy(true); rerender(); botTimer.current = setTimeout(() => { const h = handRef.current; if (!h || h.finished) { onHandEnd(); return; } if (canHeroAct(h)) { setBusy(false); rerender(); return; } const id = h.toAct; const mv = botMove(h, id); const allin = (mv.type === "raise" || mv.type === "bet") && mv.to >= h.seats[id].street + h.seats[id].stack - 0.5; Tbl.applyAction(h, mv); Sfx.forAction(mv.type, allin); rerender(); step(); }, 480 + Math.random() * 360); } function onHandEnd() { const T = tourRef.current, H = handRef.current; setBusy(false); const heroWon = H && H.result && H.result.pots && H.result.pots.some((p) => p.winners.includes(heroSeat)); Tbl.finishHand(T); rerender(); if (T.finished) { const champ = T.winner === heroSeat; champ ? Sfx.win() : Sfx.lose(); setMsg(champ ? "🏆 You win the tournament!" : `Tournament over — ${T.players[T.winner].name} wins. You placed ${T.players[heroSeat].place}.`); setWaitNext("done"); return; } if (T.players[heroSeat].eliminated) { Sfx.lose(); setMsg(`You busted — ${ordinal(T.players[heroSeat].place)} place of ${T.n}.`); setWaitNext("done"); return; } if (heroWon) Sfx.win(); setWaitNext(true); } function heroAct(action) { const H = handRef.current; if (!canHeroAct(H)) return; const fb = Ch.grade(H, heroSeat, action); if (study) { setPending({ fb, action }); rerender(); } else { setToast(fb); setTimeout(() => setToast(null), 1600); applyHero(action); } } function applyHero(action) { setPending(null); const h = handRef.current, s = h.seats[heroSeat]; const allin = (action.type === "raise" || action.type === "bet") && action.to >= s.street + s.stack - 0.5; Tbl.applyAction(h, action); Sfx.forAction(action.type, allin); rerender(); step(); } const T = tourRef.current, H = handRef.current; if (!T) return null; const bl = Tbl.blinds(T); const heroH = H && H.seats[heroSeat]; const reveal = H && H.finished && H.result && H.result.type === "showdown"; const winnerIds = H && H.finished && H.result && H.result.pots ? new Set(H.result.pots.flatMap((p) => p.winners)) : new Set(); keyRef.current = (k) => { if (pending) { if (k === "n" || k === "c") applyHero(pending.action); return; } if (waitNext) { if (k === "n" || k === "c") (waitNext === "done" ? newTourney() : nextHand()); return; } const h = handRef.current; if (!canHeroAct(h)) return; const legal = Tbl.legalActions(h).map((a) => a.type); const s = h.seats[heroSeat]; if (k === "f") heroAct(legal.includes("fold") ? { type: "fold" } : { type: "check" }); else if (k === "c") heroAct(legal.includes("call") ? { type: "call", to: h.currentBet } : { 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: s.street + s.stack }); }; return (
GTO POKER · Tournament
9-max sit & go
{bl.sb}/{bl.bb}blinds · L{T.level + 1}
{Tbl.aliveSeats(T).length}left
{chips(T.players[heroSeat].stack)}your stack
{H &&
POT {chips(Tbl.potTotal(H) - (H.finished ? potJustWon(H) : 0))}
} {H &&
{H.board.map((c, i) => )}{Array.from({ length: 5 - H.board.length }).map((_, i) =>
)}
} {H && msg &&
{msg}
} {H && (() => { const b = lerp(POS9[H.btnId], 0.34); return
D
; })()} {T.players.map((p) => { const inHand = H && H.seats[p.id] && !p.eliminated; const s = inHand ? H.seats[p.id] : null; const pos = POS9[p.id]; const role = H ? (p.id === H.sbId ? "SB" : p.id === H.bbId ? "BB" : "") : ""; return (
{p.name}{role && {role}}
{p.eliminated ? "OUT" : chips(p.stack)}
{s && !p.eliminated && !s.folded &&
{(p.id === heroSeat || reveal || winnerIds.has(p.id)) ? s.hole.map((c, i) => ) : [0, 1].map((i) => )}
} {s && s.allIn && !s.folded &&
ALL-IN
}
); })} {H && T.players.map((p) => { const s = H.seats[p.id]; if (!s || p.eliminated || s.street <= 0) return null; const bp = lerp(POS9[p.id], 0.45); return
{chips(s.street)}
; })}
{/* action area */} {waitNext === "done" ? (
) : waitNext ? (
{handResultText(H, heroSeat)}
) : ( )}
{pending && applyHero(pending.action)} />} {toast && }
); } function potJustWon(H) { if (!H.result || !H.result.pots) return 0; return H.result.pots.reduce((a, p) => a + p.amount, 0); } function ordinal(n) { const s = ["th", "st", "nd", "rd"], v = n % 100; return n + (s[(v - 20) % 10] || s[v] || s[0]); } function handResultText(H, hero) { if (!H || !H.result || !H.result.pots || !H.result.pots.length) return "Hand complete"; const nm = (id) => (id === hero ? "You" : (H.seats[id] && H.seats[id].name) || "Player"); const w = H.result.pots[0].winners; const names = w.map(nm).join(" & "); const v = (w.length === 1 && w[0] === hero) ? "win" : "wins"; if (w.length > 1) return `Split pot — ${names}.`; if (H.result.type === "fold") return `${names} ${v} — everyone folded.`; const hand = H.result.hands && H.result.hands[w[0]]; return `${names} ${v}${hand ? " with " + hand : ""}.`; } /* ---------- tournament action bar (chips) ---------- */ function TActionBar({ H, heroSeat, canAct, busy, onAction, sizeRef }) { const s = H && H.seats[heroSeat]; const toCall = s ? Math.max(0, H.currentBet - s.street) : 0; const pot = H ? Tbl.potTotal(H) : 0; const minTo = H ? Tbl.minRaiseTo(H) : 0, maxTo = s ? s.street + s.stack : 0; const canAggro = s && s.stack > toCall; const [to, setTo] = useT(minTo); useE(() => { let def = H && H.street === "preflop" ? Math.min(maxTo, Math.max(minTo, Math.round((H.currentBet > H.blinds.bb ? H.currentBet * 3 : H.blinds.bb * 2.3)))) : Math.min(maxTo, Math.max(minTo, Math.round((s ? s.street : 0) + pot * 0.6))); setTo(def || minTo); // eslint-disable-next-line }, [H && H.toAct, H && H.street, H && H.history.length]); // publish current raise sizing for the R shortcut if (sizeRef) sizeRef.current = (H && !H.finished && canAct && canAggro) ? to : null; if (!H || H.finished || !canAct) { if (sizeRef) sizeRef.current = null; const actor = H && !H.finished && H.toAct != null && H.seats[H.toAct] ? H.seats[H.toAct].name : null; return
{actor ? `${actor} is acting…` : "Waiting…"}
; } const raiseVerb = toCall > 0 ? "Raise" : "Bet"; const raiseType = toCall > 0 ? "raise" : "bet"; const presets = [ ["½", { frac: 0.5 }], ["¾", { frac: 0.75 }], ["Pot", { frac: 1 }], ["2bb", { bb: 2 }], ["3bb", { bb: 3 }], ["4bb", { bb: 4 }], ["5bb", { bb: 5 }], ["All-in", { allin: true }], ]; const applyPreset = (p) => { const d = p[1]; let v; if (d.allin) v = maxTo; else if (d.bb != null) v = d.bb * H.blinds.bb; // total bet of N big blinds else v = s.street + d.frac * (pot + toCall); // pot-fraction raise/bet setTo(Math.max(minTo, Math.min(maxTo, Math.round(v)))); }; return (
{canAggro && (
{presets.map((p) => )}
setTo(parseInt(e.target.value))} /> {chips(to)}
)}
{toCall > 0 ? : } {toCall > 0 && } {canAggro && }
F {toCall > 0 ? "fold" : "check"} · C {toCall > 0 ? "call" : "check"}{canAggro && <>{" · "}R raise{" · "}A all-in} · N next
); } window.TournamentUI = { Menu, TournamentApp };