doodle-duel / visuals.py
Doodle Duel Deploy
Deploy Doodle Duel to HF Space
3072601
Raw
History Blame Contribute Delete
6.66 kB
"""HTML/CSS rendering for Doodle Duel (human-draws mode only)."""
from __future__ import annotations
import html
import random
import config
import game
_LEADS = ["hmm...", "ooh!", "wait--", "is it...", "I think...", "let me guess...", "could be...", "aha,"]
def _fmt_time(secs) -> str:
t = int(secs)
return f"{t // 60}:{t % 60:02d}" if t >= 60 else str(t)
def _timer_ring(state) -> str:
frac = max(0.0, min(1.0, state.time_left / config.ROUND_SECONDS))
R = 26
circ = 2 * 3.14159 * R
off = circ * (1 - frac)
color = "#46d39a" if frac > 0.5 else "#f4c04e" if frac > 0.2 else "#ef5d6c"
return (
'<svg width="64" height="64" class="ring">'
f'<circle cx="32" cy="32" r="{R}" stroke="#ffffff22" stroke-width="7" fill="none"/>'
f'<circle cx="32" cy="32" r="{R}" stroke="{color}" stroke-width="7" fill="none"'
f' stroke-linecap="round" stroke-dasharray="{circ:.1f}" stroke-dashoffset="{off:.1f}"'
' transform="rotate(-90 32 32)"/>'
f'<text x="32" y="38" text-anchor="middle" class="ring-t">{_fmt_time(state.time_left)}</text>'
'</svg>'
)
def render_topbar(state) -> str:
if state.status == "idle":
return (
'<div class="topbar"><div class="prompt">'
'&#127912; Pick a difficulty below to get three words to draw!'
'</div></div>'
)
if state.status == "choosing":
return (
'<div class="topbar"><div class="prompt">'
'&#9999;&#65039; <b>Pick a word</b> to draw'
' &#128071; &nbsp;(the robot isn\'t peeking yet!)</div></div>'
)
ring = _timer_ring(state)
hint_tag = ""
if state.hints_used:
pat = game.masked(state.word, state.revealed).replace(" ", " / ")
hint_tag = f' &middot; &#128161; <code>{html.escape(pat)}</code>'
cat_badge = (
f'<span class="cat">{html.escape(state.category)}</span>'
if state.category else ""
)
diff_icon = {"easy": "🟢", "medium": "🟡", "hard": "🔴"}.get(state.difficulty, "")
diff_badge = f'<span class="cat">{diff_icon} {state.difficulty}</span>'
best_tag = (
f'<span class="best">&#127942; {state.best}</span>'
if state.best > 0 else ""
)
return (
f'<div class="topbar">'
f' <div class="draw-card">'
f' <span class="dl">round {state.round} &middot; draw this</span>'
f' <span class="word">{html.escape(state.word.upper())}</span>'
f' {cat_badge} {diff_badge}</div>'
f' {ring}'
f' <div class="score-card">'
f' <span>&#128293; streak {state.streak}{hint_tag}</span>'
f' <div class="scores"><span class="score">&#11088; {state.score}</span>{best_tag}</div>'
f' </div>'
f'</div>'
)
def render_ai(state) -> str:
face = "&#129302;"
mood = "idle"
if state.status == "won":
face, mood = "&#127881;", "win"
elif state.status == "drawing":
mood = "think"
if state.status == "idle":
bubble = "&#128075; Ready when you are!"
elif state.status == "choosing":
face, mood = "&#128584;", "idle"
bubble = "&#128064; Eyes closed &mdash; pick your word!"
elif state.status == "won":
bubble = f"&#127881; Got it &mdash; <b>{html.escape(state.last_caught)}</b>!"
elif state.status == "lost":
bubble = f"&#128565; Argh! It was <b>{html.escape(state.word)}</b> &mdash; I never stood a chance!"
elif state.guess_log:
recent = state.guess_log[-4:]
chips = "".join(f'<span class="chip">{html.escape(g)}</span>' for g in recent)
bubble = f"{random.choice(_LEADS)} {chips}?"
else:
bubble = "&#128269; Hmm, let me see what you\'re drawing..."
return (
f'<div class="ai-area">'
f' <div class="robot {mood}">{face}</div>'
f' <div class="ai-bubble">{bubble}</div>'
f'</div>'
)
def render_thinking(state) -> str:
if state.status in ("idle", "choosing"):
body = (
'<div class="tp-empty">The robot\'s reasoning will show up here '
'line by line as it watches your doodle take shape... &#128064;</div>'
)
elif state.status == "drawing" and not state.reason_log:
body = (
'<div class="thought analyzing">'
'<span class="analyzing-spinner"></span>Analyzing your canvas...</div>'
)
elif not state.reason_log:
body = '<div class="tp-empty">No thoughts recorded this round.</div>'
else:
body = "".join(
f'<div class="thought{" latest" if i == 0 else ""}">{html.escape(t)}</div>'
for i, t in enumerate(reversed(state.reason_log))
)
guesses_seen = len(state.reason_log)
subtitle = (
f'<span class="tp-sub">{guesses_seen} look{"s" if guesses_seen != 1 else ""}</span>'
if guesses_seen else ""
)
return (
f'<div class="think-panel">'
f' <div class="tp-title">&#129504; Robot\'s thoughts {subtitle}</div>'
f' <div class="tp-body">{body}</div>'
f'</div>'
)
def render_result(state) -> str:
if state.status == "won":
confetti = _make_confetti()
streak_txt = (
f" &nbsp;&#128293; <b>{state.streak} in a row!</b>"
if state.streak > 1 else ""
)
best_txt = (
f" &nbsp;&#127942; <b>New best: {state.best}!</b>"
if state.score == state.best and state.best > 0 else ""
)
return (
f'<div class="result win" id="win-result">{confetti}'
f'<div class="r-title">The robot guessed it! &#127881;</div>'
f'<div class="r-sub">{html.escape(state.last_caught)}{streak_txt}{best_txt}'
f' &#8212; press <b>Next</b> to keep the streak going.</div></div>'
)
if state.status == "lost":
best_streak_txt = (
f" Best streak: <b>{state.best_streak}</b>."
if state.best_streak > 1 else ""
)
return (
f'<div class="result lose">'
f'<div class="r-title">&#9201;&#65039; Time\'s up &#8212; the robot couldn\'t guess it!</div>'
f'<div class="r-sub">It was <b>{state.emoji} {html.escape(state.word)}</b>. '
f'Streak reset.{best_streak_txt} Press <b>Next</b> to try again.</div></div>'
)
return ""
def _make_confetti() -> str:
return '<div class="confetti">' + "".join(
f'<i style="left:{(i*9)%100}%;animation-delay:{(i%10)*0.1:.2f}s;'
f'background:hsl({(i*47)%360}deg 85% 62%)"></i>' for i in range(36)
) + "</div>"