Download app.py from Girgie/token-holdem-final-validation: direct link, hf CLI and curl.
- Browser
- Download file 32.2 kB
-
https://huggingface.co/spaces/Girgie/token-holdem-final-validation/resolve/main/app.py
- Command line
-
hf download hf://spaces/Girgie/token-holdem-final-validation/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Girgie/token-holdem-final-validation/resolve/main/app.py
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) | |
| 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"]) | |