"""The Adventures of the HF Knight -- Gradio app (Space entrypoint).
Left: the parchment scroll (our own HTML, not gr.Chatbot) + input.
Right: a HUD plaque -- rank ladder, stage, trial, strikes.
Game logic is the same agent layer as sim.py; the per-quest loop lives in two
handlers (begin / answer).
"""
import html
import warnings
import gradio as gr
from agents.judge import Judge
from agents.narrator import KICKOFF, Narrator
from config import MAX_ATTEMPT
from core.game_state import RANK_LADDER
from core.validation import Validator
from llm import LLM
from tools import GameSession
warnings.filterwarnings("ignore", message=".*moved from the Blocks constructor.*")
warnings.filterwarnings("ignore", message=".*HTTP_422_UNPROCESSABLE_ENTITY.*")
# gr.HTML doesn't run embedded
"""
PARCHMENT_CSS = """
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@600;800&family=EB+Garamond:ital@0;1&display=swap');
.gradio-container {
background:
radial-gradient(circle at 50% 0%, #4a3520 0%, rgba(0,0,0,0) 60%),
#2e2013 !important;
font-family: 'EB Garamond', Georgia, serif !important;
color: #e8d3a0 !important;
}
#title {
text-align: center; margin: 0.4em 0; padding: 8px 12px;
background: #f1e4c3; border: 2px solid #6b4f2c; border-radius: 8px;
box-shadow: 0 5px 16px rgba(0,0,0,0.5);
}
#title h1 {
font-family: 'EB Garamond', Georgia, serif !important; font-weight: 700;
font-size: 2.5rem; color: #120c05 !important; letter-spacing: 1px; margin: 0;
}
/* shared parchment surface for scroll + HUD + rules */
#tale, #hud, #rules {
background:
radial-gradient(circle at 25% 15%, #f9f1d6 0%, rgba(249,241,214,0) 70%),
radial-gradient(circle at 85% 95%, #e4d09c 0%, rgba(228,208,156,0) 70%),
#f2e6c4 !important;
border: 3px solid #6b4f2c; border-radius: 6px;
box-shadow: 0 8px 24px rgba(0,0,0,0.5), inset 0 0 50px rgba(120,90,50,0.18);
color: #3b2a18 !important;
}
/* lock text dark on the parchment regardless of Gradio light/dark mode */
#tale *, #hud *, #rules * { color: inherit !important; }
/* the tale scroll */
#tale {
padding: 26px 34px; height: 600px; overflow-y: auto;
line-height: 1.6; font-size: 1.12rem;
}
#tale .status {
text-align: center; font-family: 'Cinzel', serif; font-size: 0.8rem;
letter-spacing: 1px; text-transform: uppercase; color: #7a5a30;
border-bottom: 1px solid #c9b079; padding-bottom: 8px; margin-bottom: 18px;
}
#tale .herald { margin: 0 0 1.1em 0; }
#tale .knight { margin: 0 0 1.1em 2.2em; font-style: italic; color: #5b3a1a !important; }
#tale .who {
display: block; font-family: 'Cinzel', serif; font-size: 0.74rem;
letter-spacing: 1px; text-transform: uppercase; color: #8a6a3a !important; margin-bottom: 2px;
}
/* the HUD plaque */
#hud { padding: 6px 16px; }
#hud .rank {
font-family: 'Cinzel', serif; font-size: 1.1rem; color: #5b3a1a !important;
text-align: center; margin-bottom: 1px;
}
#hud ul.ladder { list-style: none; padding: 0; margin: 2px 0 0 0; font-size: 0.86rem; line-height: 1.2; }
#hud ul.ladder li { padding: 0 8px; color: #9a7b4a !important; border-radius: 4px; }
#hud ul.ladder li.cur { color: #3b2a18 !important; font-weight: 700; background: #e4cf9a; }
#hud ul.ladder li.done { color: #6b8a4a !important; }
#hud .sec {
font-family: 'Cinzel', serif; font-size: 0.72rem; letter-spacing: 1px;
text-transform: uppercase; color: #7a5a30 !important; margin: 3px 0 0 0;
border-top: 1px solid #c9b079; padding-top: 2px;
}
#hud .val { font-size: 0.9rem; }
#hud .strikes { font-size: 1.1rem; letter-spacing: 3px; color: #8a3a2a !important; }
/* the rules decree card */
/* kill Gradio's default flex gap between HUD and rules in the right column */
#rightcol { gap: 0 !important; }
#rules { padding: 12px 16px; margin: 0; }
#rules .sec {
font-family: 'Cinzel', serif; font-size: 0.72rem; letter-spacing: 1px;
text-transform: uppercase; color: #7a5a30 !important; margin: 0 0 8px 0;
}
#rules ul { list-style: none; padding: 0; margin: 0; font-size: 0.9rem; line-height: 1.3; }
#rules li { padding: 2px 0 2px 16px; position: relative; }
#rules li::before { content: "\\2756"; position: absolute; left: 0; color: #8a6a3a; font-size: 0.7rem; top: 5px; }
/* input row */
#answerbox textarea {
background: #f2e6c4 !important; color: #3b2a18 !important;
border: 2px solid #6b4f2c !important; font-family: 'EB Garamond', serif !important;
font-size: 1.05rem !important;
}
#sendbtn, #beginbtn {
background: #6b4f2c !important; color: #f2e6c4 !important;
border: 2px solid #4a3520 !important; font-family: 'Cinzel', serif !important;
letter-spacing: 1px;
}
"""
# model loaded once, lazily (keeps `gradio` hot-reload re-import fast)
_narrator: Narrator | None = None
def narrator() -> Narrator:
global _narrator
if _narrator is None:
_narrator = Narrator(LLM())
return _narrator
def render_tale(display) -> str:
"""The parchment scroll -- just the dialogue (rank/trial/strikes live in the HUD)."""
rows = []
for who, text in display:
body = html.escape(text).replace("\n", "
")
label = "Herald-Mentor" if who == "herald" else "Knight"
rows.append(f'
{label}{body}
') return f'Herald-Mentor' 'A spirit stirs in the Realm of the Hub. Press Begin thy quest to be ' 'summoned, Squire, and face thy first trial.