import hashlib
import html
import json
import random
import re
import time
from typing import Any, Dict, List, Tuple
import gradio as gr
try:
import spaces
except Exception:
class _SpacesFallback:
@staticmethod
def GPU(duration=25):
def decorator(fn):
return fn
return decorator
spaces = _SpacesFallback()
try:
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
except Exception as exc:
torch = None
AutoModelForCausalLM = None
AutoTokenizer = None
IMPORT_ERROR = exc
else:
IMPORT_ERROR = None
MODEL_ID = "openbmb/MiniCPM5-1B"
MAX_ROUNDS = 5
MAX_SCORE_PER_ROUND = 20
MAX_TOTAL_SCORE = MAX_ROUNDS * MAX_SCORE_PER_ROUND
tokenizer = None
model = None
MODEL_STATUS = "fallback"
MODEL_ERROR = ""
if IMPORT_ERROR is None:
try:
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
torch_dtype="auto",
device_map="auto",
)
model.eval()
MODEL_STATUS = "model loaded"
except Exception as exc:
MODEL_ERROR = f"{type(exc).__name__}: {exc}"
MODEL_STATUS = "fallback"
else:
MODEL_ERROR = f"{type(IMPORT_ERROR).__name__}: {IMPORT_ERROR}"
FALLBACK_TITLES = [
"The Moon Forgot Its Shoes",
"A Teacup Is Haunting City Hall",
"The Library Wants a Sandwich",
"A Tiny Volcano Needs Manners",
"The Clocktower Is Sneezing Glitter",
]
FALLBACK_ITEMS = [
["velvet magnet", "borrowed thunder", "pocket ladder", "polite jelly"],
["map of yesterday", "tin crown", "soup compass", "apology balloon"],
["sleepy key", "confetti wrench", "moonlit receipt", "bubble helmet"],
["rubber prophecy", "miniature foghorn", "cake shovel", "friendly spark"],
["paper comet", "clock seed", "sock telescope", "whisper net"],
]
BADGES = [
"Pocket Mythmaker",
"Errand Acrobat",
"Tiny Titan Tactician",
"Chaos Diplomat",
"Snack-Sized Sorcerer",
"Impossible Intern",
]
def stable_rng(seed: int, salt: str) -> random.Random:
digest = hashlib.sha256(f"{seed}:{salt}".encode("utf-8")).hexdigest()
return random.Random(int(digest[:12], 16))
def initial_state() -> Dict[str, Any]:
return {
"round_number": 0,
"total_score": 0,
"current_challenge": None,
"history": [],
"seed": int(time.time() * 1000) % 1_000_000,
"game_over": False,
"last_status": "Ready. Press Start Game.",
}
def escape(value: Any) -> str:
return html.escape(str(value), quote=True)
def extract_json(text: str) -> Dict[str, Any]:
if not text:
raise ValueError("empty model response")
cleaned = text.strip()
cleaned = re.sub(r"^```(?:json)?\s*", "", cleaned, flags=re.IGNORECASE)
cleaned = re.sub(r"\s*```$", "", cleaned)
try:
parsed = json.loads(cleaned)
if isinstance(parsed, dict):
return parsed
except json.JSONDecodeError:
pass
match = re.search(r"\{.*\}", cleaned, flags=re.DOTALL)
if not match:
raise ValueError("no JSON object found")
parsed = json.loads(match.group(0))
if not isinstance(parsed, dict):
raise ValueError("JSON payload is not an object")
return parsed
def clamp_score(value: Any) -> int:
try:
score = int(round(float(value)))
except Exception:
score = 0
return max(0, min(MAX_SCORE_PER_ROUND, score))
def normalize_challenge(data: Dict[str, Any], seed: int, round_number: int) -> Dict[str, Any]:
fallback = fallback_challenge(seed, round_number)
items = data.get("items", fallback["items"])
if not isinstance(items, list):
items = fallback["items"]
items = [str(item).strip() for item in items if str(item).strip()][:4]
while len(items) < 4:
items.append(fallback["items"][len(items)])
return {
"title": str(data.get("title") or fallback["title"]).strip()[:90],
"scene": str(data.get("scene") or fallback["scene"]).strip()[:260],
"goal": str(data.get("goal") or fallback["goal"]).strip()[:180],
"items": items,
"hidden_rule": str(data.get("hidden_rule") or fallback["hidden_rule"]).strip()[:180],
"vibe": str(data.get("vibe") or fallback["vibe"]).strip()[:80],
"judge_hint": str(data.get("judge_hint") or fallback["judge_hint"]).strip()[:140],
}
def normalize_judgment(data: Dict[str, Any], seed: int, round_number: int, move: str) -> Dict[str, Any]:
fallback = fallback_judgment(seed, round_number, move)
return {
"verdict": str(data.get("verdict") or fallback["verdict"]).strip()[:240],
"score_delta": clamp_score(data.get("score_delta", fallback["score_delta"])),
"badge": str(data.get("badge") or fallback["badge"]).strip()[:60],
"twist": str(data.get("twist") or fallback["twist"]).strip()[:160],
"next_hook": str(data.get("next_hook") or fallback["next_hook"]).strip()[:160],
}
def fallback_challenge(seed: int, round_number: int) -> Dict[str, Any]:
rng = stable_rng(seed, f"challenge:{round_number}")
title = FALLBACK_TITLES[(round_number - 1) % len(FALLBACK_TITLES)]
item_set = FALLBACK_ITEMS[(round_number - 1) % len(FALLBACK_ITEMS)]
goal_bits = [
"deliver a secret apology before the parade learns math",
"convince a grumpy landmark to stop floating sideways",
"trade a rumor for a harmless miracle by sunset",
"escort a nervous sparkle through a very official doorway",
"make breakfast for an idea that has not been invented yet",
]
hidden_rules = [
"Awards extra points for using the least practical item as the key tool.",
"Awards extra points for kindness toward the weirdest character.",
"Awards extra points for turning a problem into a tiny ceremony.",
"Awards extra points for avoiding brute force and choosing misdirection.",
"Awards extra points for making the errand funnier than necessary.",
]
return {
"title": title,
"scene": "A brass bell rings inside a shoebox arcade. The Quest Clerk slides over a stamp pad, a fizzy map, and a problem that refuses to be normal.",
"goal": goal_bits[rng.randrange(len(goal_bits))],
"items": item_set,
"hidden_rule": hidden_rules[rng.randrange(len(hidden_rules))],
"vibe": rng.choice(["jolly mischief", "cozy chaos", "button-mashing folklore", "snack-sized epic"]),
"judge_hint": "The judge likes clever item use, cheerful weirdness, and one clean sentence.",
}
def fallback_judgment(seed: int, round_number: int, move: str) -> Dict[str, Any]:
rng = stable_rng(seed, f"judge:{round_number}:{move.lower().strip()}")
word_count = len(re.findall(r"\w+", move))
has_item_bonus = min(6, word_count // 3)
score = max(5, min(20, 8 + has_item_bonus + rng.randrange(0, 7)))
return {
"verdict": "The Quest Clerk squints, stamps the paperwork upside down, and accepts the plan as legally whimsical.",
"score_delta": score,
"badge": BADGES[rng.randrange(len(BADGES))],
"twist": rng.choice([
"The errand gets easier after everyone agrees to whisper in rhymes.",
"A tiny crowd applauds from inside a coat pocket.",
"The least useful object becomes suspiciously essential.",
"The paperwork sprouts legs and files itself.",
]),
"next_hook": "Another bell rings. The next impossible errand is already tapping its foot.",
}
def challenge_prompt(seed: int, round_number: int) -> str:
return f"""
Create round {round_number} of 5 for Tiny Quest Forge, a joyful AI-native mini-game.
Invent a whimsical impossible errand, exactly 4 strange item cards, one hidden scoring rule, a vibe, and one public judge hint.
Use seed {seed} for variety.
Return ONLY JSON with this exact shape:
{{
"title": "...",
"scene": "...",
"goal": "...",
"items": ["...", "...", "...", "..."],
"hidden_rule": "...",
"vibe": "...",
"judge_hint": "..."
}}
Keep every field short, playful, and concrete.
""".strip()
def judge_prompt(challenge: Dict[str, Any], move: str) -> str:
return f"""
Judge this Tiny Quest Forge move. The player wrote one sentence.
Challenge:
Title: {challenge["title"]}
Scene: {challenge["scene"]}
Goal: {challenge["goal"]}
Items: {", ".join(challenge["items"])}
Hidden scoring rule: {challenge["hidden_rule"]}
Judge hint shown to player: {challenge["judge_hint"]}
Player move: {move}
Return ONLY JSON with this exact shape:
{{
"verdict": "...",
"score_delta": 0,
"badge": "...",
"twist": "...",
"next_hook": "..."
}}
score_delta must be an integer from 0 to 20. Reward clever item use, joyful specificity, and satisfying the hidden rule.
""".strip()
@spaces.GPU(duration=25)
def generate_text(prompt: str, max_new_tokens: int = 180) -> str:
if model is None or tokenizer is None or torch is None:
raise RuntimeError(MODEL_ERROR or "model unavailable")
messages = [
{
"role": "system",
"content": "You are the Tiny Quest Forge game engine. You return compact valid JSON only.",
},
{"role": "user", "content": prompt},
]
try:
try:
input_ids = tokenizer.apply_chat_template(
messages,
tokenize=True,
enable_thinking=False,
add_generation_prompt=True,
return_tensors="pt",
)
except TypeError:
input_ids = tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_tensors="pt",
)
device = next(model.parameters()).device
input_ids = input_ids.to(device)
with torch.no_grad():
outputs = model.generate(
input_ids,
max_new_tokens=min(max_new_tokens, 220),
do_sample=True,
temperature=0.78,
top_p=0.92,
repetition_penalty=1.05,
pad_token_id=tokenizer.eos_token_id,
)
new_tokens = outputs[0][input_ids.shape[-1]:]
return tokenizer.decode(new_tokens, skip_special_tokens=True).strip()
finally:
if torch is not None and torch.cuda.is_available():
torch.cuda.empty_cache()
def create_challenge(seed: int, round_number: int) -> Tuple[Dict[str, Any], str]:
try:
raw = generate_text(challenge_prompt(seed, round_number), max_new_tokens=190)
data = extract_json(raw)
return normalize_challenge(data, seed, round_number), "model loaded"
except Exception as exc:
return fallback_challenge(seed, round_number), f"fallback: {type(exc).__name__}"
def judge_move(challenge: Dict[str, Any], move: str, seed: int, round_number: int) -> Tuple[Dict[str, Any], str]:
try:
raw = generate_text(judge_prompt(challenge, move), max_new_tokens=170)
data = extract_json(raw)
return normalize_judgment(data, seed, round_number, move), "model loaded"
except Exception as exc:
return fallback_judgment(seed, round_number, move), f"fallback: {type(exc).__name__}"
def rank_title(score: int) -> str:
if score >= 90:
return "Grand Errand Wizard"
if score >= 70:
return "Pocket Quest Champion"
if score >= 50:
return "Certified Odd-Job Hero"
if score >= 30:
return "Promising Button Masher"
return "Apprentice of Tiny Chaos"
def share_card(state: Dict[str, Any]) -> str:
badges = [entry["judgment"]["badge"] for entry in state.get("history", []) if entry.get("judgment", {}).get("badge")]
badge_text = ", ".join(badges[:5]) if badges else "no badges yet"
return (
"Tiny Quest Forge result\n"
f"Score: {state.get('total_score', 0)}/{MAX_TOTAL_SCORE}\n"
f"Rank: {rank_title(state.get('total_score', 0))}\n"
f"Badges: {badge_text}\n"
"I solved five impossible errands with a 1B local model."
)
def round_pills(round_number: int, history_count: int, game_over: bool) -> str:
pills = []
for idx in range(1, MAX_ROUNDS + 1):
if game_over or idx <= history_count:
cls = "done"
label = f"R{idx} done"
elif idx == round_number:
cls = "active"
label = f"R{idx} live"
else:
cls = "locked"
label = f"R{idx}"
pills.append(f'{escape(label)}')
return "".join(pills)
def render_history(history: List[Dict[str, Any]]) -> str:
if not history:
return '
Badges and judged errands will stack here.
'
rows = []
for entry in history[-5:]:
challenge = entry["challenge"]
judgment = entry["judgment"]
rows.append(
f"""
{escape(challenge["title"])}
+{escape(judgment["score_delta"])} pts
{escape(judgment["verdict"])}
{escape(judgment["badge"])}
Rule revealed: {escape(challenge["hidden_rule"])}
"""
)
return "".join(rows)
def render_board(state: Dict[str, Any]) -> str:
state = state or initial_state()
total = int(state.get("total_score", 0))
progress = max(0, min(100, round((total / MAX_TOTAL_SCORE) * 100)))
round_number = int(state.get("round_number", 0))
history = state.get("history", [])
game_over = bool(state.get("game_over", False))
challenge = state.get("current_challenge")
status = state.get("last_status", "Ready.")
if challenge:
item_html = "".join(f'{escape(item)}' for item in challenge["items"])
challenge_html = f"""
Round {escape(round_number)} errand | {escape(challenge["vibe"])}
{escape(challenge["title"])}
{escape(challenge["scene"])}
Goal{escape(challenge["goal"])}
{item_html}
{escape(challenge["judge_hint"])}
"""
elif game_over:
challenge_html = ""
else:
challenge_html = """
Ready at the tiny counter
Press Start Game
Five impossible errands are waiting. Each one gives you four strange item cards and one sentence to make trouble useful.
borrowed thunder
sock telescope
soup compass
paper comet
"""
final_html = ""
if game_over:
card = share_card(state)
badges = [entry["judgment"]["badge"] for entry in history if entry.get("judgment", {}).get("badge")]
badges_html = "".join(f'{escape(badge)}' for badge in badges) or 'No badges'
final_html = f"""
Final result
{escape(rank_title(total))}
{escape(total)}/{MAX_TOTAL_SCORE}
{badges_html}
{escape(card)}
"""
return f"""
Tiny Quest Forge
Five tiny errands. One impossible sentence each.
Score
{escape(total)}/{MAX_TOTAL_SCORE}
{round_pills(round_number, len(history), game_over)}
{escape(status)}
{challenge_html}
{final_html}
Quest ledger
{render_history(history)}
"""
def status_html(message: str) -> str:
return f'{escape(message)}
'
def start_game() -> Tuple[Dict[str, Any], str, str, Any]:
state = initial_state()
state["round_number"] = 1
state["last_status"] = "Generating round 1..."
yield state, render_board(state), status_html(state["last_status"]), gr.update(value="")
challenge, source = create_challenge(state["seed"], 1)
state["current_challenge"] = challenge
state["last_status"] = f"{source}. Round 1 is ready."
yield state, render_board(state), status_html(state["last_status"]), gr.update(value="")
def reset_game() -> Tuple[Dict[str, Any], str, str, Any]:
state = initial_state()
return state, render_board(state), status_html(state["last_status"]), gr.update(value="")
def new_round(state: Dict[str, Any]) -> Tuple[Dict[str, Any], str, str, Any]:
state = state or initial_state()
if state.get("game_over"):
state["last_status"] = "Final card is ready. Reset to play again."
yield state, render_board(state), status_html(state["last_status"]), gr.update()
return
if not state.get("history"):
yield from start_game()
return
if len(state["history"]) >= MAX_ROUNDS:
state["game_over"] = True
state["last_status"] = "Final score locked."
yield state, render_board(state), status_html(state["last_status"]), gr.update()
return
if state.get("current_challenge") and len(state["history"]) < state.get("round_number", 0):
state["last_status"] = "Submit this move before drawing another errand."
yield state, render_board(state), status_html(state["last_status"]), gr.update()
return
next_round = len(state["history"]) + 1
state["round_number"] = next_round
state["current_challenge"] = None
state["last_status"] = f"Generating round {next_round}..."
yield state, render_board(state), status_html(state["last_status"]), gr.update(value="")
challenge, source = create_challenge(state["seed"], next_round)
state["current_challenge"] = challenge
state["last_status"] = f"{source}. Round {next_round} is ready."
yield state, render_board(state), status_html(state["last_status"]), gr.update(value="")
def submit_move(move: str, state: Dict[str, Any]) -> Tuple[Dict[str, Any], str, str, Any]:
state = state or initial_state()
move = (move or "").strip()
if state.get("game_over"):
state["last_status"] = "Game complete. Reset to play another run."
yield state, render_board(state), status_html(state["last_status"]), gr.update()
return
if not state.get("current_challenge"):
state["last_status"] = "Start a game before submitting a move."
yield state, render_board(state), status_html(state["last_status"]), gr.update()
return
if not move:
state["last_status"] = "Type one sentence before submitting."
yield state, render_board(state), status_html(state["last_status"]), gr.update()
return
if len(re.findall(r"[.!?]", move)) > 2 or len(move) > 220:
move = move[:220].strip()
round_number = int(state.get("round_number", 1))
state["last_status"] = "Generating judgment..."
yield state, render_board(state), status_html(state["last_status"]), gr.update()
judgment, source = judge_move(state["current_challenge"], move, state["seed"], round_number)
judgment["score_delta"] = clamp_score(judgment["score_delta"])
state["total_score"] = int(state.get("total_score", 0)) + judgment["score_delta"]
state["history"].append(
{
"round": round_number,
"move": move,
"challenge": state["current_challenge"],
"judgment": judgment,
}
)
if len(state["history"]) >= MAX_ROUNDS:
state["game_over"] = True
state["current_challenge"] = None
state["last_status"] = f"{source}. Final result forged."
else:
state["current_challenge"] = None
state["last_status"] = f"{source}. Badge earned. Press New Round."
yield state, render_board(state), status_html(state["last_status"]), gr.update(value="")
CSS = """
:root {
--tqf-ink: #17141f;
--tqf-paper: #fff9e8;
--tqf-panel: #fff3c6;
--tqf-line: #2d2440;
--tqf-indigo: #4831a8;
--tqf-violet: #7b3fa3;
--tqf-yellow: #ffd84d;
--tqf-mint: #5de1b5;
--tqf-coral: #ff8066;
}
.gradio-container {
background:
linear-gradient(135deg, rgba(255,216,77,.18), rgba(93,225,181,.14)),
repeating-linear-gradient(0deg, #f7f0dc 0, #f7f0dc 18px, #f3e7c4 19px);
color: var(--tqf-ink);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.tqf-shell {
width: min(1120px, calc(100vw - 28px));
margin: 0 auto;
padding: 22px 0 18px;
}
.tqf-hero,
.tqf-status,
.tqf-band {
border: 3px solid var(--tqf-line);
box-shadow: 6px 6px 0 var(--tqf-line);
}
.tqf-hero {
display: flex;
justify-content: space-between;
gap: 18px;
align-items: stretch;
background: var(--tqf-indigo);
color: white;
padding: 20px;
}
.tqf-brand {
width: fit-content;
background: var(--tqf-yellow);
color: var(--tqf-ink);
border: 2px solid var(--tqf-line);
padding: 5px 9px;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 0;
}
.tqf-hero h1 {
margin: 12px 0 0;
max-width: 690px;
font-size: clamp(2rem, 4.5vw, 4rem);
line-height: 1;
letter-spacing: 0;
}
.tqf-scorebox {
min-width: 180px;
background: var(--tqf-paper);
color: var(--tqf-ink);
border: 2px solid var(--tqf-line);
padding: 12px;
display: grid;
align-content: center;
gap: 6px;
}
.tqf-scorebox span,
.tqf-kicker {
font-size: .78rem;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 0;
}
.tqf-scorebox strong {
font-size: 2rem;
line-height: 1;
}
.tqf-meter {
height: 14px;
border: 2px solid var(--tqf-line);
background: white;
overflow: hidden;
}
.tqf-meter i {
display: block;
height: 100%;
background: linear-gradient(90deg, var(--tqf-mint), var(--tqf-yellow), var(--tqf-coral));
}
.tqf-status {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin: 14px 0;
padding: 10px;
background: #fefefe;
font-weight: 800;
}
.tqf-rounds {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.tqf-pill,
.tqf-chip,
.tqf-badge {
display: inline-flex;
align-items: center;
min-height: 30px;
border: 2px solid var(--tqf-line);
color: var(--tqf-ink);
font-weight: 850;
line-height: 1.1;
}
.tqf-pill {
padding: 4px 8px;
background: #ece8f7;
}
.tqf-pill.active { background: var(--tqf-yellow); }
.tqf-pill.done { background: var(--tqf-mint); }
.tqf-pill.locked { opacity: .62; }
.tqf-grid {
display: grid;
grid-template-columns: minmax(0, 1.45fr) minmax(280px, .75fr);
gap: 16px;
align-items: start;
}
.tqf-band {
background: var(--tqf-paper);
padding: 18px;
}
.tqf-board h2,
.tqf-final h2 {
margin: 8px 0 10px;
font-size: clamp(1.7rem, 3vw, 2.7rem);
line-height: 1.05;
letter-spacing: 0;
}
.tqf-scene {
margin: 0 0 14px;
font-size: 1.05rem;
line-height: 1.45;
}
.tqf-goal {
display: grid;
gap: 5px;
background: white;
border: 2px dashed var(--tqf-violet);
padding: 12px;
font-weight: 750;
}
.tqf-goal span {
color: var(--tqf-violet);
text-transform: uppercase;
font-size: .75rem;
font-weight: 950;
}
.tqf-items,
.tqf-badges {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin: 14px 0 10px;
}
.tqf-chip {
background: #fefefe;
padding: 8px 10px;
}
.tqf-chip:nth-child(2n) { background: #e6fff6; }
.tqf-chip:nth-child(3n) { background: #fff0ed; }
.tqf-hint {
margin: 10px 0 0;
font-weight: 800;
color: #473a60;
}
.tqf-ledger {
min-height: 280px;
}
.tqf-empty {
margin-top: 12px;
color: #5d526c;
font-weight: 700;
}
.tqf-log {
border-top: 2px solid var(--tqf-line);
padding: 12px 0;
}
.tqf-log-top {
display: flex;
justify-content: space-between;
gap: 10px;
font-weight: 900;
}
.tqf-log p {
margin: 8px 0;
line-height: 1.35;
}
.tqf-log small {
display: block;
margin-top: 8px;
color: #5d526c;
line-height: 1.35;
}
.tqf-badge {
background: var(--tqf-yellow);
padding: 5px 8px;
width: fit-content;
}
.tqf-final {
margin-top: 16px;
background: #e9fff6;
}
.tqf-final-score {
font-size: 3rem;
font-weight: 950;
line-height: 1;
}
.tqf-final pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
background: white;
border: 2px solid var(--tqf-line);
padding: 12px;
font-weight: 750;
}
.tqf-mini-status {
width: min(1120px, calc(100vw - 28px));
margin: 0 auto 10px;
color: #312747;
font-weight: 850;
}
#move_box textarea {
min-height: 78px;
border: 3px solid var(--tqf-line);
box-shadow: 4px 4px 0 var(--tqf-line);
background: #fffef8;
color: var(--tqf-ink);
font-weight: 750;
}
#controls {
width: min(1120px, calc(100vw - 28px));
margin: 0 auto;
}
#controls button {
min-height: 46px;
border: 2px solid var(--tqf-line) !important;
box-shadow: 3px 3px 0 var(--tqf-line);
font-weight: 950;
}
@media (max-width: 760px) {
.tqf-shell {
width: min(100% - 18px, 1120px);
padding-top: 10px;
}
.tqf-hero,
.tqf-status {
flex-direction: column;
align-items: stretch;
}
.tqf-grid {
grid-template-columns: 1fr;
}
.tqf-scorebox {
min-width: 0;
}
.tqf-hero,
.tqf-status,
.tqf-band {
box-shadow: 4px 4px 0 var(--tqf-line);
}
}
"""
with gr.Blocks(css=CSS, title="Tiny Quest Forge", fill_width=True) as demo:
game_state = gr.State(initial_state())
board = gr.HTML(render_board(initial_state()))
status_line = gr.HTML(status_html(f"{MODEL_STATUS}. Press Start Game."))
with gr.Column(elem_id="controls"):
move_box = gr.Textbox(
label="One-sentence solution",
placeholder="Example: I bribe the moon with the soup compass, then use the apology balloon as a tiny parachute.",
lines=2,
max_lines=3,
max_length=220,
elem_id="move_box",
)
with gr.Row():
start_button = gr.Button("Start Game", variant="primary")
submit_button = gr.Button("Submit Move", variant="huggingface")
new_round_button = gr.Button("New Round")
reset_button = gr.Button("Reset")
outputs = [game_state, board, status_line, move_box]
start_button.click(start_game, inputs=None, outputs=outputs, show_progress="minimal")
submit_button.click(submit_move, inputs=[move_box, game_state], outputs=outputs, show_progress="minimal")
move_box.submit(submit_move, inputs=[move_box, game_state], outputs=outputs, show_progress="minimal")
new_round_button.click(new_round, inputs=game_state, outputs=outputs, show_progress="minimal")
reset_button.click(reset_game, inputs=None, outputs=outputs, show_progress="hidden")
if __name__ == "__main__":
demo.queue(default_concurrency_limit=1).launch()