"""Gradio UI for the AI Game Builder — Build Small Hackathon Track 2."""
from __future__ import annotations
import html
import os
import random
import gradio as gr
from dotenv import load_dotenv
from game_agent import GameOrchestrator
load_dotenv()
MODEL_PROVIDER = os.getenv("MODEL_PROVIDER", "hf")
OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://127.0.0.1:11434")
MODEL_ID = os.getenv("HF_MODEL") or os.getenv("OLLAMA_MODEL", "Qwen/Qwen2.5-Coder-32B-Instruct")
orchestrator = GameOrchestrator(
model=MODEL_ID,
ollama_host=OLLAMA_HOST,
provider=MODEL_PROVIDER,
)
_PLACEHOLDER = """
INSERT IDEA
Describe a tiny arcade game
Try “a frog dodging moon rain” or click a cartridge prompt.
"""
_CSS = """
:root {
--ink: #f7f0d6;
--muted: #9fb8a8;
--panel: #141319;
--panel-2: #1e1a22;
--line: #3d332e;
--mint: #5df2c2;
--amber: #ffb13b;
--red: #ff5e58;
--blue: #6fb8ff;
}
.gradio-container {
background:
radial-gradient(circle at 20% 0%, rgba(255,177,59,.16), transparent 28%),
linear-gradient(135deg, #171116 0%, #0a1014 48%, #12140d 100%) !important;
color: var(--ink) !important;
}
footer { display: none !important; }
#title-bar {
margin: 14px 14px 8px;
padding: 14px 18px;
background: linear-gradient(90deg, #261818, #101b1b);
border: 2px solid var(--line);
border-radius: 8px;
box-shadow: 0 14px 40px rgba(0,0,0,.34);
display: flex;
justify-content: space-between;
gap: 16px;
align-items: center;
}
#app-name {
color: var(--ink);
font-family: Georgia, "Times New Roman", serif;
font-size: 27px;
font-weight: 900;
letter-spacing: 0;
}
#app-sub {
color: var(--mint);
font-family: "Courier New", monospace;
font-size: 12px;
text-align: right;
}
.panel-heading {
color: var(--amber) !important;
font-family: Georgia, "Times New Roman", serif !important;
}
.quick-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.quick-grid button, .control-row button {
border-radius: 7px !important;
}
.cabinet-shell {
padding: 14px;
background: linear-gradient(160deg, #3b231e, #161114 62%, #0b0f10);
border: 3px solid #55372d;
border-radius: 8px;
box-shadow: inset 0 0 0 2px rgba(255,255,255,.05), 0 20px 60px rgba(0,0,0,.42);
}
.cabinet-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
font-family: "Courier New", monospace;
color: var(--muted);
font-size: 12px;
}
.lamp {
display: inline-block;
width: 9px;
height: 9px;
margin-left: 7px;
border-radius: 999px;
background: var(--red);
box-shadow: 0 0 12px var(--red);
}
.lamp:nth-child(2) {
background: var(--amber);
box-shadow: 0 0 12px var(--amber);
}
.lamp:nth-child(3) {
background: var(--mint);
box-shadow: 0 0 12px var(--mint);
}
.cabinet-screen {
min-height: 560px;
border: 2px solid #080808;
border-radius: 6px;
overflow: hidden;
background: #050505;
box-shadow: inset 0 0 38px rgba(0,0,0,.88);
}
.empty-screen {
display: flex;
align-items: center;
justify-content: center;
}
.empty-copy {
max-width: 340px;
text-align: center;
font-family: "Courier New", monospace;
color: var(--ink);
}
.empty-copy strong {
display: block;
margin: 12px 0 8px;
font-family: Georgia, "Times New Roman", serif;
font-size: 26px;
}
.empty-copy small {
color: var(--muted);
}
.coin-slot {
display: inline-block;
padding: 6px 10px;
border: 1px solid var(--amber);
color: var(--amber);
border-radius: 6px;
}
.arcade-frame iframe {
display: block;
width: 100%;
height: 560px;
border: 0;
background: #050505;
outline: 2px solid transparent;
}
.arcade-frame:focus-within iframe {
outline-color: var(--mint);
}
.focus-hint {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 8px 10px;
background: #111315;
border-bottom: 1px solid #303039;
color: var(--muted);
font: 12px "Courier New", monospace;
}
.focus-hint strong {
color: var(--mint);
font-weight: 700;
}
.gradio-container button.primary {
background: var(--amber) !important;
color: #20130d !important;
}
"""
_TITLE_BAR = """
Backyard Arcade
Tiny games grown from strange prompts | Build Small Track 2
"""
_WELCOME = (
"Welcome to Backyard Arcade: tiny games grown from strange prompts.\n\n"
"Tell me a game idea. I’ll design a safe arcade cartridge using one of four "
"render tags: `dodge`, `collector`, `jumper`, or `snake`. Ask for something "
"strange and I can blend two tags together.\n\n"
"Try: *a snake game where I eat glowing orbs*, *a frog dodging moon rain*, "
"or *jump between falling blocks and reach the last one*."
)
_STRANGE_REMIX_INSTRUCTIONS = (
(
"Strange remix seed `{seed}`. Blend the current game with one different "
"render tag from dodge, collector, jumper, or snake. Add one rule inversion "
"that changes how the player thinks, but keep the controls simple and the "
"game immediately playable."
),
(
"Strange remix seed `{seed}`. Make a genre collision: keep the current game "
"recognizable, then mix in one different render tag and one surprising win "
"condition. Use weirdness 8-10, but avoid confusing controls or invisible "
"goals."
),
)
_GAME_FOCUS_SCRIPT = """
"""
def _inject_game_focus_script(raw_html: str) -> str:
"""Make generated games easier to control inside Gradio's iframe."""
if "function focusGame()" in raw_html:
return raw_html
if "