| """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">' |
| '🎨 Pick a difficulty below to get three words to draw!' |
| '</div></div>' |
| ) |
| if state.status == "choosing": |
| return ( |
| '<div class="topbar"><div class="prompt">' |
| '✏️ <b>Pick a word</b> to draw' |
| ' 👇 (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' · 💡 <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">🏆 {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} · 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>🔥 streak {state.streak}{hint_tag}</span>' |
| f' <div class="scores"><span class="score">⭐ {state.score}</span>{best_tag}</div>' |
| f' </div>' |
| f'</div>' |
| ) |
|
|
|
|
| def render_ai(state) -> str: |
| face = "🤖" |
| mood = "idle" |
| if state.status == "won": |
| face, mood = "🎉", "win" |
| elif state.status == "drawing": |
| mood = "think" |
|
|
| if state.status == "idle": |
| bubble = "👋 Ready when you are!" |
| elif state.status == "choosing": |
| face, mood = "🙈", "idle" |
| bubble = "👀 Eyes closed — pick your word!" |
| elif state.status == "won": |
| bubble = f"🎉 Got it — <b>{html.escape(state.last_caught)}</b>!" |
| elif state.status == "lost": |
| bubble = f"😵 Argh! It was <b>{html.escape(state.word)}</b> — 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 = "🔍 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... 👀</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">🧠 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" 🔥 <b>{state.streak} in a row!</b>" |
| if state.streak > 1 else "" |
| ) |
| best_txt = ( |
| f" 🏆 <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! 🎉</div>' |
| f'<div class="r-sub">{html.escape(state.last_caught)}{streak_txt}{best_txt}' |
| f' — 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">⏱️ Time\'s up — 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>" |
|
|