token-holdem / app.py
Girgie's picture
Deploy Token Hold'em Space
81c1867 verified
Raw
History Blame Contribute Delete
32.2 kB
from __future__ import annotations
import itertools
import json
import time
import uuid
import warnings
from dataclasses import dataclass, field
from html import escape
warnings.filterwarnings(
"ignore",
message="'HTTP_422_UNPROCESSABLE_ENTITY' is deprecated.*",
)
import gradio as gr
from token_holdem.agents import ROSTER, fallback_decide
from token_holdem.cards import cards_label
from token_holdem.engine import GameState, make_players
from token_holdem.leaderboard import Leaderboard
from token_holdem.logging_utils import get_logger, log_event
from token_holdem.logging_utils import LOG_FILE
from token_holdem.model_runtime import ModelRuntimeUnavailable, SUPPORTED_TRANSFORMERS_MODELS, get_model_runtime, model_runtime_statuses
from token_holdem.render import CSS, hall_markdown, leaderboard_markdown, log_markdown, table_html
HAND_COUNTER = itertools.count(1)
@dataclass
class AppSession:
game: GameState | None = None
chats: list[str] = field(default_factory=list)
stopped: bool = False
session_id: str = field(default_factory=lambda: uuid.uuid4().hex[:10])
hand_no: int = 0
orbit_no: int = 1
leaderboard = Leaderboard()
model_runtime = get_model_runtime()
logger = get_logger("token_holdem.app")
def _play_outputs(session: AppSession | None, status: str = ""):
game = session.game if session else None
return (
session,
table_html(game, chats=session.chats if session else None),
log_markdown(game, session.chats if session else None),
_leaderboard_md(),
_hall_md(),
status or _play_status(session),
hand_review_markdown(session),
*_action_button_updates(session),
gr.update(interactive=bool(session and session.stopped)),
)
def _play_status(session: AppSession | None) -> str:
if session is None or session.game is None:
return "Quick Seat starts a cash-game session."
if session.stopped:
return "You are busted. Rebuy to keep playing this cash game."
game = session.game
human = next((player for player in game.players if player.is_human), None)
human_status = ""
if human:
to_call = min(max(0, game.current_bet - human.bet), human.stack)
human_status = f" To Call: {to_call} · Your Stack: {human.stack}."
if game.result:
return f"Hand complete: {game.result.summary} Click Next Hand to continue with current stacks.{human_status}"
current = game.current_player()
if current.is_human:
legal = game.legal_actions(current.id)
return f"Your turn. To Call: {legal['to_call']} · Your Stack: {current.stack}."
return f"{current.name} is thinking...{human_status}"
def _action_button_updates(session: AppSession | None):
hidden = [
gr.update(interactive=False, visible=False, value="Fold"),
gr.update(interactive=False, visible=False, value="Check"),
gr.update(interactive=False, visible=False, value="Call"),
gr.update(interactive=False, visible=False, value="Min Raise"),
gr.update(interactive=False, visible=False, value="Half-Pot"),
gr.update(interactive=False, visible=False, value="Pot"),
gr.update(interactive=False, visible=False, value="All-In"),
]
disabled = [
gr.update(interactive=False, visible=True, value="Fold"),
gr.update(interactive=False, visible=True, value="Check"),
gr.update(interactive=False, visible=True, value="Call"),
gr.update(interactive=False, visible=True, value="Min Raise"),
gr.update(interactive=False, visible=True, value="Half-Pot"),
gr.update(interactive=False, visible=True, value="Pot"),
gr.update(interactive=False, visible=True, value="All-In"),
]
if session is None or session.game is None or session.game.result or session.stopped:
return hidden
game = session.game
current = game.current_player()
if not current.is_human:
return disabled
legal = game.legal_actions(current.id)
actions = set(legal["actions"])
presets = legal["raise_presets"]
return [
gr.update(interactive="fold" in actions, visible=True, value="Fold"),
gr.update(interactive="check" in actions, visible=True, value="Check"),
gr.update(interactive="call" in actions, visible=True, value=f"Call {legal['to_call']}"),
gr.update(interactive="raise" in actions, visible=True, value=f"Min {presets.get('min', 0)}"),
gr.update(interactive="raise" in actions, visible=True, value=f"Half-Pot {presets.get('half_pot', 0)}"),
gr.update(interactive="raise" in actions, visible=True, value=f"Pot {presets.get('pot', 0)}"),
gr.update(interactive="all_in" in actions, visible=True, value=f"All-In {presets.get('all_in', current.stack)}"),
]
def hand_review_markdown(session: AppSession | None) -> str:
if session is None or session.game is None:
return "**Hand Review:** No active hand."
game = session.game
button = game.players[game.dealer_index].name
small_blind = game.players[game.small_blind_index].name if game.small_blind_index is not None else "-"
big_blind = game.players[game.big_blind_index].name if game.big_blind_index is not None else "-"
current = "Complete" if game.result else game.current_player().name
legal = {} if game.result else game.legal_actions(game.current_player().id)
showdown = ""
if game.result and game.result.showdown:
showdown_lines = []
for player in game.players:
if player.id in game.result.showdown:
marker = " winner" if player.id in game.result.winners else ""
showdown_lines.append(f" - {player.name}: {game.result.showdown[player.id]}{marker}")
showdown = "\n- Showdown:\n" + "\n".join(showdown_lines)
return (
"**Hand Review**\n\n"
f"- Session: `{game.session_id or '-'}`\n"
f"- Hand: `{game.hand_id or '-'}`\n"
f"- Orbit: `{game.orbit_id or '-'}`\n"
f"- Button: **{button}**\n"
f"- Small blind: **{small_blind}**\n"
f"- Big blind: **{big_blind}**\n"
f"- Street: **{game.street.value}**\n"
f"- Current actor: **{current}**\n"
f"- Legal actions: `{legal}`"
f"{showdown}"
)
def _ensure_roster() -> None:
for idx, profile in enumerate(ROSTER):
leaderboard.ensure_player(f"ai_{idx}", profile.name, "model", profile.model_id, profile.persona)
def _start_game(human_name: str, seed: int | None, mode: str) -> AppSession:
_ensure_roster()
human_name = (human_name or "Human Wanderer").strip()[:32]
if mode == "play":
leaderboard.ensure_player("human", human_name, "human")
profiles = ROSTER[:7]
players = make_players([profile.name for profile in profiles], human_name=human_name)
else:
profiles = ROSTER[:7]
players = make_players([profile.name for profile in profiles])
hand_no = 1
session = AppSession(hand_no=hand_no, orbit_no=1)
game = GameState(
players=players,
seed=(seed or int(time.time())) + next(HAND_COUNTER),
session_id=session.session_id,
hand_id=f"{session.session_id}-h{hand_no:03d}",
orbit_id=f"{session.session_id}-o{session.orbit_no:02d}",
)
game.start_hand()
session.game = game
log_event(logger, "session_started", mode=mode, session_id=session.session_id, hand_id=game.hand_id, orbit_id=game.orbit_id, hand_no=hand_no, seed=game.seed, players=[player.name for player in players])
return session
def _start_next_cash_hand(session: AppSession, seed: int | None, human_name: str) -> AppSession:
if session.game is None:
return _start_game(human_name, seed, "play")
game = session.game
for player in game.players:
if not player.is_human and player.stack <= 0:
player.stack = 1_000
log_event(logger, "ai_auto_rebuy", player=player.name, amount=1_000)
human = next((player for player in game.players if player.is_human), None)
if human and human.stack <= 0:
session.stopped = True
session.chats.append(f"{human.name}: busted and needs a rebuy to keep playing.")
log_event(logger, "human_busted_rebuy_required", player=human.name)
return session
next_button = game.next_active_index(game.dealer_index)
session.hand_no += 1
session.orbit_no = ((session.hand_no - 1) // len(game.players)) + 1
next_game = GameState(
players=game.players,
dealer_index=next_button,
seed=(seed or int(time.time())) + next(HAND_COUNTER),
session_id=session.session_id,
hand_id=f"{session.session_id}-h{session.hand_no:03d}",
orbit_id=f"{session.session_id}-o{session.orbit_no:02d}",
)
next_game.start_hand()
session.game = next_game
session.stopped = False
log_event(
logger,
"cash_hand_started",
session_id=session.session_id,
hand_id=next_game.hand_id,
orbit_id=next_game.orbit_id,
hand_no=session.hand_no,
seed=next_game.seed,
button=next_game.players[next_game.dealer_index].name,
small_blind=next_game.players[next_game.small_blind_index].name if next_game.small_blind_index is not None else None,
big_blind=next_game.players[next_game.big_blind_index].name if next_game.big_blind_index is not None else None,
stacks={player.name: player.stack for player in next_game.players},
)
return session
def start_play(human_name: str, seed: int | None):
try:
log_event(logger, "callback_start", callback="start_play", human_name=human_name, seed=seed)
session = _start_game(human_name, seed, "play")
_advance_ai_until_human(session, next(HAND_COUNTER))
return _play_outputs(session)
except Exception:
logger.exception("callback_failed", extra={"event": {"callback": "start_play"}})
raise
def start_play_with_runtime(human_name: str, seed: int | None):
try:
log_event(logger, "callback_start", callback="start_play_with_runtime", human_name=human_name, seed=seed, use_models=True)
session = _start_game(human_name, seed, "play")
yielded = False
for output in _advance_ai_until_human_stream(session, next(HAND_COUNTER), use_models=True):
yielded = True
yield output
if not yielded:
yield _play_outputs(session)
except Exception:
logger.exception("callback_failed", extra={"event": {"callback": "start_play_with_runtime"}})
raise
def human_action(session: AppSession | None, action: str):
try:
log_event(logger, "callback_start", callback="human_action", action=action, use_models=True, has_session=session is not None)
if session is None or session.game is None or session.game.result:
yield _play_outputs(session)
return
game = session.game
legal = game.legal_actions()
amount = 0
if action == "min_raise":
action = "raise"
amount = legal["raise_presets"].get("min", 0)
elif action == "half_pot":
action = "raise"
amount = legal["raise_presets"].get("half_pot", 0)
elif action == "pot":
action = "raise"
amount = legal["raise_presets"].get("pot", 0)
game.apply_action(action, amount)
yield _play_outputs(session, status="Action accepted. The tavern waits for the next move...")
for output in _advance_ai_until_human_stream(session, next(HAND_COUNTER), use_models=True):
yield output
if game.result:
_record_result(game, "play")
yield _play_outputs(session)
except Exception:
logger.exception("callback_failed", extra={"event": {"callback": "human_action", "action": action}})
raise
def next_play_hand(session: AppSession | None, human_name: str, seed: int | None):
try:
log_event(logger, "callback_start", callback="next_play_hand", human_name=human_name, seed=seed, use_models=True)
if session is None:
for output in start_play_with_runtime(human_name, seed):
yield output
return
session = _start_next_cash_hand(session, seed, human_name)
if not session.stopped:
yielded = False
for output in _advance_ai_until_human_stream(session, next(HAND_COUNTER), use_models=True):
yielded = True
yield output
if not yielded:
yield _play_outputs(session)
else:
yield _play_outputs(session)
except Exception:
logger.exception("callback_failed", extra={"event": {"callback": "next_play_hand"}})
raise
def rebuy_user(session: AppSession | None, amount: int = 1_000):
try:
log_event(logger, "callback_start", callback="rebuy_user", amount=amount, has_session=session is not None)
if session and session.game:
for player in session.game.players:
if player.is_human:
player.stack = amount
session.stopped = False
session.chats.append(f"{player.name} rebuys for {amount} chips.")
log_event(logger, "human_rebuy", player=player.name, amount=amount)
break
return _play_outputs(session)
except Exception:
logger.exception("callback_failed", extra={"event": {"callback": "rebuy_user"}})
raise
def action_handler(action: str):
def handle(session: AppSession | None):
yield from human_action(session, action)
return handle
def run_arena(seed: int | None, hands: int):
try:
log_event(logger, "callback_start", callback="run_arena", seed=seed, hands=hands, use_models=True)
session = _start_game("", seed, "arena")
for _ in range(max(1, min(int(hands or 1), 10))):
if session.game is None or session.game.result:
session = _start_game("", seed, "arena")
hand_no = next(HAND_COUNTER)
while session.game and not session.game.result:
try:
_take_ai_turn(session, hand_no, use_models=True)
except ModelRuntimeUnavailable as exc:
_surface_model_error(session, session.game.current_player().name if session.game else "unknown", exc)
yield table_html(session.game, reveal_all=True, chats=session.chats), log_markdown(session.game, session.chats), _leaderboard_md(), _hall_md()
return
yield table_html(session.game, reveal_all=True, chats=session.chats), log_markdown(session.game, session.chats), _leaderboard_md(), _hall_md()
time.sleep(0.25)
if session.game and session.game.result:
_record_result(session.game, "arena")
session.chats.append(_chronicle(session.game))
yield table_html(session.game, reveal_all=True, chats=session.chats), log_markdown(session.game, session.chats), _leaderboard_md(), _hall_md()
time.sleep(0.5)
except Exception:
logger.exception("callback_failed", extra={"event": {"callback": "run_arena", "seed": seed, "hands": hands, "use_models": True}})
raise
def _advance_ai_until_human(session: AppSession, hand_no: int, use_models: bool = False) -> None:
while session.game and not session.game.result and not session.game.current_player().is_human:
_take_ai_turn(session, hand_no, use_models=use_models)
def _advance_ai_until_human_stream(session: AppSession, hand_no: int, use_models: bool = False):
while session.game and not session.game.result and not session.game.current_player().is_human:
player = session.game.current_player()
yield _play_outputs(session, status=f"{player.name} is thinking with {'model runtime' if use_models else 'fallback persona'}...")
try:
_take_ai_turn(session, hand_no, use_models=use_models)
except ModelRuntimeUnavailable as exc:
_surface_model_error(session, player.name, exc)
yield _play_outputs(session, status=f"Model unavailable: {exc}")
return
yield _play_outputs(session, status=f"{player.name} acted. {_play_status(session)}")
def _take_ai_turn(session: AppSession, hand_no: int, use_models: bool = False) -> None:
game = session.game
if game is None:
return
player = game.current_player()
profile = next((profile for profile in ROSTER if profile.name == player.name), ROSTER[0])
summary = {
"hand_no": hand_no,
"street": game.street.value,
"hole_cards": player.hole_cards,
"community_cards": game.community_cards,
"stack": player.stack,
"pot": game.pot(),
"legal": game.legal_actions(player.id),
"history": game.action_log,
"recent_chats": session.chats[-12:],
"seed": game.seed,
"session_id": game.session_id,
"hand_id": game.hand_id,
"orbit_id": game.orbit_id,
}
if use_models:
runtime_result = model_runtime.decide(profile, summary)
decision = runtime_result.decision
session.chats.append(f"{player.name}: {decision['table_talk']}")
log_event(logger, "ai_decision", session_id=game.session_id, hand_id=game.hand_id, orbit_id=game.orbit_id, player=player.name, source=runtime_result.source, status=runtime_result.status, action=decision.get("action"), amount=decision.get("amount"), street=game.street.value)
else:
decision = fallback_decide(profile, summary, seed=game.seed)
session.chats.append(f"{player.name}: {decision['table_talk']}")
log_event(logger, "ai_decision", session_id=game.session_id, hand_id=game.hand_id, orbit_id=game.orbit_id, player=player.name, source="fallback", status="disabled", action=decision.get("action"), amount=decision.get("amount"), street=game.street.value)
game.apply_action(decision["action"], int(decision.get("amount") or 0))
def _surface_model_error(session: AppSession, player_name: str, exc: ModelRuntimeUnavailable) -> None:
game = session.game
session.chats.append(f"System: {player_name} could not act because model inference is unavailable: {exc}")
if game:
log_event(logger, "ai_decision_blocked", session_id=game.session_id, hand_id=game.hand_id, orbit_id=game.orbit_id, player=player_name, error=str(exc))
def _record_result(game: GameState, mode: str) -> None:
if not game.result:
return
if getattr(game, "_recorded", False):
log_event(logger, "hand_record_skipped_duplicate", mode=mode, summary=game.result.summary)
return
names = {player.id: player.name for player in game.players}
leaderboard.record_hand(
mode,
game.result.pot,
cards_label(game.result.board),
game.result.winners,
game.result.deltas,
names,
game.result.summary,
game.session_id,
game.hand_id,
game.orbit_id,
)
setattr(game, "_recorded", True)
log_event(logger, "hand_recorded", mode=mode, session_id=game.session_id, hand_id=game.hand_id, orbit_id=game.orbit_id, pot=game.result.pot, winners=game.result.winners, deltas=game.result.deltas)
def _chronicle(game: GameState) -> str:
if not game.result:
return ""
winner = game.player(game.result.winners[0]).name
return f"Chronicle: {winner} raises a tiny mug to the rafters after dragging {game.result.pot} chips through the candle smoke."
def _leaderboard_md() -> str:
return leaderboard_markdown(leaderboard.rows())
def _hall_md() -> str:
return hall_markdown(leaderboard.hall_of_fame())
def recent_logs(limit: int = 80) -> str:
if not LOG_FILE.exists():
return "No app logs yet. Trigger a hand or Arena run first."
lines = LOG_FILE.read_text(encoding="utf-8").splitlines()[-limit:]
return "```json\n" + "\n".join(lines) + "\n```"
def model_runtime_report(limit: int = 800) -> str:
rows: dict[str, dict[str, str]] = {
profile.name: {
"model": SUPPORTED_TRANSFORMERS_MODELS.get(profile.name, profile.model_id),
"source": "not observed",
"reason": "No decision logged in the recent log window.",
"last_action": "-",
}
for profile in ROSTER
}
if LOG_FILE.exists():
for line in LOG_FILE.read_text(encoding="utf-8").splitlines()[-limit:]:
try:
event = json.loads(line)
except json.JSONDecodeError:
continue
player = event.get("player")
if player not in rows:
continue
message = event.get("message")
if message == "model_runtime_success":
rows[player]["source"] = "real inference"
rows[player]["reason"] = event.get("model_id", rows[player]["model"])
rows[player]["last_action"] = _runtime_action_label(event)
elif message == "model_runtime_partial_fallback":
rows[player]["source"] = "partial fallback"
rows[player]["reason"] = f"{event.get('model_id', rows[player]['model'])}: {event.get('reason', 'unknown')}"
rows[player]["last_action"] = _runtime_action_label(event)
elif message == "model_runtime_unsupported":
rows[player]["source"] = "fallback"
rows[player]["reason"] = f"{event.get('model_id', rows[player]['model'])}: {event.get('reason', 'unsupported runtime')}"
elif message == "model_runtime_failed":
rows[player]["source"] = "fallback"
rows[player]["reason"] = f"{event.get('error_type', 'Error')}: {str(event.get('error', 'unknown'))[:180]}"
elif message == "model_runtime_modal_success":
rows[player]["source"] = "modal inference"
rows[player]["reason"] = event.get("model_id", rows[player]["model"])
rows[player]["last_action"] = _runtime_action_label(event)
elif message == "model_runtime_modal_failed":
rows[player]["source"] = "unavailable"
rows[player]["reason"] = f"Modal: {str(event.get('error', 'unknown'))[:180]}"
elif message == "model_runtime_modal_disabled":
rows[player]["source"] = "disabled"
rows[player]["reason"] = str(event.get("reason", "disabled"))[:180]
elif message == "model_runtime_modal_call_started":
rows[player]["source"] = "modal call started"
rows[player]["reason"] = event.get("model_id", rows[player]["model"])
elif message == "model_runtime_deterministic_dev":
rows[player]["source"] = "deterministic dev"
rows[player]["reason"] = "Explicit TOKEN_HOLDEM_ALLOW_DETERMINISTIC_BOTS=1 mode"
rows[player]["last_action"] = _runtime_action_label(event)
elif message == "ai_decision":
rows[player]["last_action"] = _runtime_action_label(event)
if event.get("source") in {"fallback", "local_model", "local_model_partial_fallback", "modal_model", "deterministic_dev"}:
rows[player]["source"] = {
"fallback": "fallback",
"local_model": "real inference",
"local_model_partial_fallback": "partial fallback",
"modal_model": "modal inference",
"deterministic_dev": "deterministic dev",
}[event["source"]]
rows[player]["reason"] = str(event.get("status", rows[player]["reason"]))[:220]
lines = [
'<div class="runtime-report">',
"<h3>Model Runtime Status</h3>",
"<table><thead><tr><th>Seat</th><th>Configured model</th><th>Recent source</th><th>Last action</th><th>Reason/status</th></tr></thead><tbody>",
]
for profile in ROSTER:
row = rows[profile.name]
lines.append(
"<tr>"
f"<td>{escape(profile.name)}</td>"
f"<td><code>{escape(row['model'])}</code></td>"
f"<td>{escape(row['source'])}</td>"
f"<td>{escape(row['last_action'])}</td>"
f"<td>{escape(row['reason'])}</td>"
"</tr>"
)
lines.append("</tbody></table></div>")
lines.append('<div class="runtime-report"><h3>Configured Roster</h3><table><thead><tr><th>Seat</th><th>Runtime state</th><th>Note</th></tr></thead><tbody>')
for row in model_runtime_statuses():
lines.append(
"<tr>"
f"<td>{escape(row['name'])}</td>"
f"<td>{escape(row['state'])}</td>"
f"<td>{escape(row['note'])}</td>"
"</tr>"
)
lines.append("</tbody></table></div>")
return "\n".join(lines)
def _runtime_action_label(event: dict) -> str:
action = event.get("action") or "-"
amount = event.get("amount")
if amount not in {None, "", 0, "0"}:
return f"{action} {amount}"
return str(action)
def diagnostics_outputs():
return model_runtime_report(), recent_logs()
def build_app() -> gr.Blocks:
_ensure_roster()
with gr.Blocks(title="Token Hold'em") as demo:
gr.HTML('<div class="app-title"><h1>Token Hold\'em</h1><p>Candlelit Texas Hold\'em for one human challenger and seven tavern-tuned LLMs.</p></div>')
play_state = gr.State(None)
with gr.Tab("Play Mode"):
with gr.Group(elem_classes="tavern-app-shell"):
gr.HTML(
"""
<div class="tavern-background" aria-hidden="true">
<div class="tavern-stonework"></div>
<div class="tavern-bar-backdrop"></div>
<div class="tavern-floor"></div>
</div>
<div class="tavern-ambient-lights" aria-hidden="true">
<span></span><span></span><span></span><span></span>
</div>
"""
,
elem_classes="tavern-scene-chrome",
)
with gr.Column(elem_classes="tavern-left-rail"):
gr.HTML(
"""
<section class="rail-plaque">
<div class="rail-kicker">Thousand Token Wood</div>
<h2>Token Hold'em</h2>
<p>Quick-seat into the bottom chair. The seven model regulars handle the rest.</p>
</section>
"""
)
with gr.Group(elem_classes="tavern-panel setup-panel"):
human_name = gr.Textbox(label="Your tavern name", value="Human Wanderer")
play_seed = gr.Number(label="Seed", value=0, precision=0)
start_btn = gr.Button("Quick Seat", variant="primary")
next_btn = gr.Button("Next Hand")
rebuy_btn = gr.Button("Rebuy 1000", interactive=False)
with gr.Column(elem_classes="poker-stage"):
play_table = gr.HTML(table_html(None))
with gr.Column(elem_classes="tavern-right-rail"):
play_log = gr.Markdown("No hand yet.", elem_classes="tavern-panel log-panel")
with gr.Group(elem_classes="action-bar"):
play_status = gr.Markdown("Quick Seat starts a cash-game session.", elem_classes="status-scroll action-status")
gr.HTML('<div class="action-bar-caption"><span>Live poker controls</span><strong>Choose your move</strong></div>')
with gr.Row(elem_classes="action-row"):
fold_btn = gr.Button("Fold", interactive=False, elem_classes="poker-action fold-action")
check_btn = gr.Button("Check", interactive=False, elem_classes="poker-action check-action")
call_btn = gr.Button("Call", interactive=False, elem_classes="poker-action call-action")
min_btn = gr.Button("Min Raise", interactive=False, elem_classes="poker-action raise-action")
half_btn = gr.Button("Half-Pot", interactive=False, elem_classes="poker-action raise-action")
pot_btn = gr.Button("Pot", interactive=False, elem_classes="poker-action raise-action")
all_in_btn = gr.Button("All-In", interactive=False, elem_classes="poker-action all-in-action")
with gr.Group(elem_classes="bottom-status-panel"):
hand_review = gr.Markdown("**Hand Review:** No active hand.", elem_classes="tavern-panel review-panel")
with gr.Tab("AI Arena"):
with gr.Row(elem_classes="tavern-panel setup-panel"):
arena_seed = gr.Number(label="Seed", value=0, precision=0)
arena_hands = gr.Slider(label="Hands to auto-play", minimum=1, maximum=10, value=3, step=1)
arena_btn = gr.Button("Start Arena", variant="primary")
arena_table = gr.HTML(table_html(None))
arena_log = gr.Markdown("The tavern is quiet.", elem_classes="tavern-panel log-panel")
with gr.Tab("Leaderboard"):
leaderboard_out = gr.Markdown(_leaderboard_md(), elem_classes="tavern-panel leaderboard-panel")
with gr.Tab("Hall of Fame"):
hall_out = gr.Markdown(_hall_md(), elem_classes="tavern-panel hall-panel")
with gr.Tab("About"):
gr.Markdown(
"## Token Hold'em\n\n"
"A deterministic Texas Hold'em engine runs the table while local model personas choose actions and table talk. "
"The tavern cast uses inspired character identities, not logos or brand marks, so every seat feels like part of the same cozy poker room.\n\n"
"- **Play Mode:** sit as the bottom-center human challenger.\n"
"- **AI Arena:** watch the seven LLM seats battle with revealed cards.\n"
"- **Leaderboard:** track bankrolls, records, and Hall of Fame plaques.",
elem_classes="tavern-panel about-panel",
)
with gr.Tab("Diagnostics"):
gr.Markdown("Structured app logs are written to `logs/token_holdem.jsonl`. Framework warnings from Gradio/Starlette may still appear in the terminal separately.", elem_classes="tavern-panel")
refresh_logs = gr.Button("Refresh Recent Logs")
runtime_out = gr.Markdown(model_runtime_report(), elem_classes="tavern-panel runtime-panel")
logs_out = gr.Markdown(recent_logs(), elem_classes="tavern-panel log-panel")
play_outputs = [play_state, play_table, play_log, leaderboard_out, hall_out, play_status, hand_review, fold_btn, check_btn, call_btn, min_btn, half_btn, pot_btn, all_in_btn, rebuy_btn]
start_btn.click(start_play_with_runtime, inputs=[human_name, play_seed], outputs=play_outputs)
next_btn.click(next_play_hand, inputs=[play_state, human_name, play_seed], outputs=play_outputs)
rebuy_btn.click(rebuy_user, inputs=[play_state], outputs=play_outputs)
for button, action in [
(fold_btn, "fold"),
(check_btn, "check"),
(call_btn, "call"),
(min_btn, "min_raise"),
(half_btn, "half_pot"),
(pot_btn, "pot"),
(all_in_btn, "all_in"),
]:
button.click(action_handler(action), inputs=[play_state], outputs=play_outputs)
arena_btn.click(run_arena, inputs=[arena_seed, arena_hands], outputs=[arena_table, arena_log, leaderboard_out, hall_out])
refresh_logs.click(diagnostics_outputs, outputs=[runtime_out, logs_out])
return demo
demo = build_app()
if __name__ == "__main__":
demo.launch(css=CSS, allowed_paths=["assets"])