File size: 9,368 Bytes
6c96042 86a4911 e7fcba4 6c96042 e7fcba4 6c96042 86a4911 6c96042 c1d95e3 6c96042 8a86db4 6c96042 86a4911 6c96042 86a4911 e7fcba4 6c96042 86a4911 8a86db4 7fbc80c 86a4911 e7fcba4 86a4911 6c96042 86a4911 6c96042 e7fcba4 927e21b 86a4911 e7fcba4 86a4911 927e21b e7fcba4 927e21b e7fcba4 927e21b 86a4911 e7fcba4 86a4911 e7fcba4 6c96042 e7fcba4 86a4911 e7fcba4 86a4911 e7fcba4 6c96042 e7fcba4 6c96042 7fbc80c 8a86db4 e70c305 86a4911 e7fcba4 86a4911 e7fcba4 86a4911 e7fcba4 86a4911 e7fcba4 86a4911 e7fcba4 86a4911 e7fcba4 86a4911 e7fcba4 86a4911 e7fcba4 86a4911 e7fcba4 86a4911 e7fcba4 86a4911 e7fcba4 86a4911 e7fcba4 86a4911 e7fcba4 6c96042 86a4911 6c96042 c1d95e3 e7fcba4 c1d95e3 8a86db4 c1d95e3 86a4911 8a86db4 86a4911 8a86db4 86a4911 8a86db4 86a4911 8a86db4 927e21b 6c96042 e7fcba4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | """
Gradio demo UI for CricketCaptain-LLM.
Two modes:
1. Manual play — human picks tool + args, submits, sees result.
2. Auto-play — AI plays N balls using the built-in RandomAgent (no API key needed).
HF Space: opponent defaults to heuristic; no API key required for basic demo.
Set CRICKET_CAPTAIN_MODEL + HF_TOKEN secrets for live LLM captain/opponent.
"""
import json
import os
import random
import sys
from pathlib import Path
from typing import Any
sys.path.insert(0, str(Path(__file__).parent.parent))
import gradio as gr
from server.cricket_environment import CricketEnvironment
from server.captain_policy import (
OPPONENT_PRESETS,
captain_presets as _captain_presets,
pick_action as _pick_captain_action,
)
from models import CricketAction
# ------------------------------------------------------------------ #
# Constants #
# ------------------------------------------------------------------ #
ALL_TOOLS = [
"call_toss",
"set_match_plan", "update_match_plan",
"select_batter",
"set_strategy", "plan_shot", "play_delivery",
"choose_bowler", "set_bowling_strategy", "plan_delivery",
"set_field_setting", "bowl_delivery",
"reflect_after_ball", "analyze_situation",
]
SHOT_INTENTS = ["leave", "defensive", "single", "rotate", "boundary", "six"]
# Captain + opponent presets are now defined in server.captain_policy so the
# /custom cockpit driver and the /web Gradio UI share the same lookup table.
# ------------------------------------------------------------------ #
# Scorecard / metrics renderers #
# ------------------------------------------------------------------ #
def _scorecard(obs) -> str:
if obs is None:
return "*Click **New Match** to begin.*"
ctx = obs.game_context
strat = obs.declared_strategy
bowl = obs.bowling_strategy
opp = obs.opponent_plan
lines = [
f"### {ctx.get('game_state','').upper()} — {ctx.get('innings','first').upper()} INNINGS",
f"**Over** {ctx.get('over',0)}.{ctx.get('ball',0)} | "
f"**Score** {ctx.get('score',0)}/{ctx.get('wickets',0)} | "
f"**RR** {ctx.get('run_rate',0.0):.2f}",
]
if ctx.get("target"):
need = ctx["target"] - ctx.get("score", 0)
rrr = ctx.get("req_rate", 0.0)
lines.append(f"**Target** {ctx['target']} | **Need** {need} | **RRR** {rrr:.1f}")
lines.append(
f"**Phase** `{ctx.get('phase','?').upper()}` | "
f"**Bowler type** `{ctx.get('bowler_type','?').upper()}` | "
f"**Field** `{ctx.get('field_setting','Balanced')}`"
)
lines.append("")
if obs.game_state == "batting":
if strat:
lines.append(f"**Strategy** {strat.get('phase_intent','?')} (agg={strat.get('aggression',0):.2f})")
rat = strat.get("rationale", "")
if rat:
lines.append(f"*{rat[:100]}*")
else:
lines.append("*No batting strategy set yet.*")
elif obs.game_state == "bowling":
if bowl:
lines.append(
f"**Bowl plan** {bowl.get('delivery_type','?')} · "
f"{bowl.get('line','?')} · {bowl.get('length','?')}"
)
if opp and opp.get("shot_intent"):
lines.append(f"**Opponent intent** `{opp.get('shot_intent','?')}` (agg={opp.get('aggression',0):.2f})")
elif obs.game_state == "toss":
lines.append("*Waiting for toss call…*")
last = obs.last_ball_result or ""
if last:
lines.append(f"\n> 🏏 {last}")
available = obs.available_tools or []
lines.append(f"\n**Available tools:** " + " ".join(f"`{t}`" for t in available))
return "\n".join(lines)
def _metrics(env) -> str:
if env is None or not hasattr(env, "_state"):
return "No match started."
s = env._state
def _avg(lst): return sum(lst)/len(lst) if lst else 0.0
lines = [
f"**Coherence** {_avg(s.coherence_scores):.3f} | "
f"**Adaptation** {_avg(s.adaptation_scores):.3f} | "
f"**Opp-awareness** {_avg(s.opponent_awareness_scores):.3f}",
f"**Plan-commit** {_avg(s.plan_commitment_scores):.3f} | "
f"**Tool calls** {s.tool_calls_made} | "
f"**r_validity** {'1.0 ✅' if s.tool_calls_made > 0 else '—'}",
]
return "\n".join(lines)
# ------------------------------------------------------------------ #
# Random auto-play agent #
# ------------------------------------------------------------------ #
def _captain_action(obs, preset_key: str = "heuristic") -> CricketAction:
"""Pick a captain action via the named preset (delegates to captain_policy)."""
return _pick_captain_action(obs, preset_key, _auto_action, prompt_render=_scorecard)
def _auto_action(obs) -> CricketAction:
available = obs.available_tools or []
state = obs.game_state
phase = obs.strategic_phase
if "call_toss" in available:
return CricketAction(tool="call_toss", arguments={"call": "heads", "decision": "bat"})
if state == "bowling":
if "set_bowling_strategy" in available and phase in ("pre_over", "pre_ball") and random.random() < 0.3:
return CricketAction(tool="set_bowling_strategy", arguments={
"bowler_type": "pace", "line": "outside off", "length": "good length",
"delivery_type": "stock", "rationale": "Target corridor of uncertainty.",
})
if "plan_delivery" in available and phase == "pre_ball" and random.random() < 0.35:
return CricketAction(tool="plan_delivery", arguments={
"bowler_type": "pace", "line": "outside off", "length": "full",
"delivery_type": "outswinger", "rationale": "Test the outside edge.",
})
if "bowl_delivery" in available:
return CricketAction(tool="bowl_delivery", arguments={})
if "reflect_after_ball" in available and random.random() < 0.4:
return CricketAction(tool="reflect_after_ball", arguments={"reflection": "Maintain pressure."})
if "set_field_setting" in available:
return CricketAction(tool="set_field_setting", arguments={"setting": random.choice(["Aggressive","Balanced"])})
if available:
return CricketAction(tool=available[0], arguments={})
if state == "batting":
if "set_strategy" in available and not obs.declared_strategy and random.random() < 0.7:
return CricketAction(tool="set_strategy", arguments={
"phase_intent": "attack", "aggression": 0.6,
"rationale": "Powerplay — push for boundaries while wickets are in hand.",
})
if "plan_shot" in available and random.random() < 0.25:
return CricketAction(tool="plan_shot", arguments={
"shot_intent": "boundary", "target_area": "cover",
"risk": "medium", "trajectory": "ground",
"rationale": "Drive through cover gap.",
})
if "play_delivery" in available:
shot = random.choices(SHOT_INTENTS, weights=[5,15,25,20,25,10], k=1)[0]
return CricketAction(tool="play_delivery", arguments={
"shot_intent": shot, "explanation": f"Going for {shot}.",
})
if "reflect_after_ball" in available and random.random() < 0.35:
return CricketAction(tool="reflect_after_ball", arguments={"reflection": "Adjust based on outcome."})
if available:
return CricketAction(tool=available[0], arguments={})
return CricketAction(tool=available[0] if available else "analyze_situation", arguments={})
# ------------------------------------------------------------------ #
# Gradio UI #
# ------------------------------------------------------------------ #
def build_ui(
web_manager: Any = None,
action_fields: list | None = None,
metadata: Any = None,
is_chat_env: bool = False,
title: str = "CaptainRL Demo",
quick_start_md: str | None = None,
) -> gr.Blocks:
"""Render the OpenEnv "Custom" tab.
The full cricket cockpit SPA (live field view, captain's mind, training
metrics, Cartesia commentary audio) is served at `/custom`. Embedding it
here via an iframe gives the OpenEnv web-interface the same experience
without duplicating UI code in Gradio.
"""
iframe_html = """
<div style=\"width:100%; height:88vh; min-height:720px; border-radius:10px; overflow:hidden;\">
<iframe
src=\"/custom\"
title=\"Cricket Cockpit\"
allow=\"autoplay; clipboard-read; clipboard-write\"
style=\"width:100%; height:100%; border:0; background:#0b0d0e;\"
></iframe>
</div>
"""
with gr.Blocks(
title="CaptainRL",
theme=gr.themes.Soft(primary_hue="teal", secondary_hue="blue"),
css="footer { display: none !important; }",
) as demo:
gr.HTML(iframe_html)
return demo
if __name__ == "__main__":
build_ui().launch(server_name="0.0.0.0", server_port=7860, share=False)
|