""" Text-to-Game Generator - Code : Qwen/Qwen3-Coder-Next via HF router (novita provider) - Images: black-forest-labs/FLUX.1-schnell via hf-inference (free) Both run on HF servers. Zero local RAM, no GPU, no crashes. Add HF_TOKEN as a Space secret: Settings > Variables and secrets > New secret. """ import os import re import io import base64 import traceback import gradio as gr from openai import OpenAI from huggingface_hub import InferenceClient from PIL import Image # --------------------------------------------------------------------------- # Models and clients # --------------------------------------------------------------------------- CODE_MODEL = "Qwen/Qwen3-Coder-Next:novita" # via HF router IMAGE_MODEL = "baidu/ERNIE-Image" # via fal-ai HF_TOKEN = os.environ.get("HF_TOKEN", "") def _check_token(): if not HF_TOKEN: raise ValueError( "HF_TOKEN is not set. Add it as a Space secret: " "Settings > Variables and secrets > New secret > Name: HF_TOKEN" ) def get_code_client(): _check_token() return OpenAI( base_url="https://router.huggingface.co/v1", api_key=HF_TOKEN, ) def get_image_client(): _check_token() return InferenceClient(provider="fal-ai", api_key=HF_TOKEN) # --------------------------------------------------------------------------- # Game type configs # --------------------------------------------------------------------------- GAME_TYPES = { "Platformer": { "description": "Jump over enemies and obstacles to reach the goal.", "prompt_template": ( "Create a complete, self-contained HTML5 platformer game with the theme: {theme}.\n" "Requirements:\n" "- Single HTML file with all CSS and JavaScript inline.\n" "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n" "- Player moves left/right with arrow keys or WASD and jumps.\n" "- At least 5 platforms, 2 moving enemies, and a goal to reach.\n" "- Show score/lives on the canvas.\n" "- Use requestAnimationFrame for the game loop.\n" "- For every image asset use: const img = new Image(); img.src = 'sprite_NAME.png';\n" " e.g. sprite_player.png, sprite_enemy.png, sprite_background.png, sprite_platform.png.\n" "- No external libraries or CDN links.\n" "- Theme all visuals to match: {theme}.\n" "Output ONLY the raw HTML. No explanation, no markdown fences." ), }, "Top-Down Shooter": { "description": "Shoot waves of enemies before they reach you.", "prompt_template": ( "Create a complete, self-contained HTML5 top-down shooter game with the theme: {theme}.\n" "Requirements:\n" "- Single HTML file with all CSS and JavaScript inline.\n" "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n" "- Player moves with WASD/arrows; shoots with Space or click.\n" "- Enemies spawn from edges in escalating waves.\n" "- Display health, score, and wave on canvas.\n" "- Game-over screen with score and restart button.\n" "- Use requestAnimationFrame for the game loop.\n" "- For every image asset: const img = new Image(); img.src = 'sprite_NAME.png';\n" " e.g. sprite_player.png, sprite_enemy.png, sprite_bullet.png, sprite_background.png.\n" "- No external libraries or CDN links.\n" "- Theme everything to match: {theme}.\n" "Output ONLY the raw HTML. No explanation, no markdown fences." ), }, "Puzzle / Maze": { "description": "Navigate a maze or solve a tile puzzle to escape.", "prompt_template": ( "Create a complete, self-contained HTML5 maze/tile-puzzle game with the theme: {theme}.\n" "Requirements:\n" "- Single HTML file with all CSS and JavaScript inline.\n" "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n" "- Player navigates with arrow keys or WASD.\n" "- At least 15x10 tile grid, collectible keys, and a locked exit.\n" "- Show a move counter or timer.\n" "- Win screen when player collects all keys and exits.\n" "- For every image asset: const img = new Image(); img.src = 'sprite_NAME.png';\n" " e.g. sprite_player.png, sprite_wall.png, sprite_floor.png, sprite_key.png, sprite_exit.png.\n" "- No external libraries or CDN links.\n" "- Theme everything to match: {theme}.\n" "Output ONLY the raw HTML. No explanation, no markdown fences." ), }, "Arcade / Dodge": { "description": "Dodge falling obstacles and survive as long as possible.", "prompt_template": ( "Create a complete, self-contained HTML5 arcade dodge game with the theme: {theme}.\n" "Requirements:\n" "- Single HTML file with all CSS and JavaScript inline.\n" "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n" "- Player moves with arrow keys or WASD; obstacles increase in speed over time.\n" "- Collision ends the game; show time survived as score.\n" "- High score stored in a JS variable; restart button on game-over screen.\n" "- Use requestAnimationFrame for the game loop.\n" "- For every image asset: const img = new Image(); img.src = 'sprite_NAME.png';\n" " e.g. sprite_player.png, sprite_obstacle.png, sprite_background.png.\n" "- No external libraries or CDN links.\n" "- Theme everything to match: {theme}.\n" "Output ONLY the raw HTML. No explanation, no markdown fences." ), }, "Surprise Me!": { "description": "Let the AI invent the genre - could be anything!", "prompt_template": ( "Create a complete, self-contained HTML5 browser game with the theme: {theme}.\n" "Pick any fun arcade genre: breakout, snake, flappy-style, space invaders, etc.\n" "Requirements:\n" "- Single HTML file with all CSS and JavaScript inline.\n" "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n" "- Keyboard or mouse controlled.\n" "- Clear win/lose conditions and a score display.\n" "- Game-over / win screen with a restart button.\n" "- Use requestAnimationFrame for the game loop.\n" "- For every image asset: const img = new Image(); img.src = 'sprite_NAME.png';\n" " e.g. sprite_player.png, sprite_background.png.\n" "- No external libraries or CDN links.\n" "- Theme everything vividly to match: {theme}.\n" "Output ONLY the raw HTML. No explanation, no markdown fences." ), }, } GAME_TYPE_NAMES = list(GAME_TYPES.keys()) THEME_EXAMPLES = { "Platformer": [["Jungle temple with ancient traps"], ["Neon cyberpunk rooftops"], ["Underwater pirate shipwreck"]], "Top-Down Shooter": [["Alien desert invasion"], ["Viking village under siege"], ["Steampunk robot uprising"]], "Puzzle / Maze": [["Haunted library with secret doors"], ["Ice cave with frozen keys"], ["Egyptian pyramid tiles"]], "Arcade / Dodge": [["Asteroid field in a tiny rocket"], ["Chef dodging ingredients"], ["Time traveller avoiding paradox storms"]], "Surprise Me!": [["A sentient library that rearranges itself"], ["Deep sea bioluminescent creatures"], ["Retro space diner on a comet"]], } # --------------------------------------------------------------------------- # Image helpers # --------------------------------------------------------------------------- def _pil_to_data_uri(pil_image: Image.Image, size: int = 64) -> str: img = pil_image.resize((size, size), Image.LANCZOS) buf = io.BytesIO() img.save(buf, format="PNG") return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode() def _colored_placeholder(name: str) -> str: colours = ["#e74c3c", "#3498db", "#2ecc71", "#f39c12", "#9b59b6", "#1abc9c"] colour = colours[abs(hash(name)) % len(colours)] label = name.replace("sprite_", "")[:6] svg = ( '' '' '' + label + '' '' ) return "data:image/svg+xml;base64," + base64.b64encode(svg.encode()).decode() def _sprite_prompt(sprite_name: str, theme: str) -> str: label = sprite_name.replace("sprite_", "").replace("_", " ") return ( "Pixel-art game sprite: " + label + ". Theme: " + theme + ". " "Vibrant colours, clear silhouette, plain dark background, 64x64 pixel style." ) def _find_sprite_filenames(html_code: str) -> list: pattern = r"""(?:src\s*=\s*['"]|\.src\s*=\s*['"])\s*(sprite_[a-zA-Z0-9_]+\.png)\s*['"]""" return list(dict.fromkeys(re.findall(pattern, html_code))) def _inject_sprites(html_code: str, sprite_map: dict) -> str: for fname, data_uri in sprite_map.items(): html_code = html_code.replace('"' + fname + '"', '"' + data_uri + '"') html_code = html_code.replace("'" + fname + "'", "'" + data_uri + "'") return html_code def generate_sprites(sprite_names: list, theme: str) -> dict: if not sprite_names: return {} client = get_image_client() mapping = {} for fname in sprite_names: name_no_ext = fname.replace(".png", "") prompt = _sprite_prompt(name_no_ext, theme) try: pil_img = client.text_to_image(prompt, model=IMAGE_MODEL) mapping[fname] = _pil_to_data_uri(pil_img, size=64) except Exception as exc: print("[FLUX] Failed '" + fname + "': " + str(exc)) mapping[fname] = _colored_placeholder(name_no_ext) return mapping # --------------------------------------------------------------------------- # Code generation # --------------------------------------------------------------------------- def generate_game_code(game_type: str, theme: str, temperature: float, max_new_tokens: int): if not theme.strip(): return "", "Please enter a theme first.", _placeholder_html("Enter a theme and generate a game.") template = GAME_TYPES[game_type]["prompt_template"] user_prompt = template.format(theme=theme.strip()) system_msg = ( "You are an expert HTML5 game developer. " "Write a complete, working, single-file HTML5 game using canvas and vanilla JavaScript. " "Use new Image() with src='sprite_NAME.png' for every visual asset. " "Output ONLY the raw HTML - no markdown fences, no explanation." ) try: # Step 1: Generate HTML via Qwen3-Coder-Next (HF router -> novita) client = get_code_client() completion = client.chat.completions.create( model=CODE_MODEL, messages=[ {"role": "system", "content": system_msg}, {"role": "user", "content": user_prompt}, ], max_tokens=int(max_new_tokens), temperature=float(temperature), ) code = completion.choices[0].message.content.strip() code = re.sub(r"^```[a-zA-Z]*\n?", "", code).strip() code = re.sub(r"\n?```$", "", code).strip() if " str: safe = message.replace("<", "<").replace(">", ">") return ( '
' '
' + safe + '
' ) def _wrap_in_html(snippet: str, theme: str) -> str: return ( "\n\n\n" "" + theme + "\n" "\n" "\n\n" + snippet + "\n\n" ) def _build_preview(html_code: str) -> str: encoded = base64.b64encode(html_code.encode("utf-8")).decode("ascii") return ( '' ) def launch_game(code: str) -> str: if not code or not code.strip(): return _placeholder_html("No game code yet - generate a game first.") return _build_preview(code) # --------------------------------------------------------------------------- # UI helpers # --------------------------------------------------------------------------- def update_type_description(game_type: str) -> str: return "_" + GAME_TYPES[game_type]["description"] + "_" def get_first_theme(game_type: str) -> str: return THEME_EXAMPLES[game_type][0][0] # --------------------------------------------------------------------------- # Gradio UI # --------------------------------------------------------------------------- def build_ui(): with gr.Blocks(title="Game Generator") as demo: gr.Markdown( "# Game Generator\n" "Describe a theme, pick a genre - " "**Qwen3-Coder-Next** (via Novita/HF router) writes the game and " "**FLUX.1-schnell** (via HF Inference) generates every sprite. " "Zero RAM used in your Space.\n\n" "> Add your **HF_TOKEN** as a Space secret " "(Settings > Variables and secrets > New secret > Name: HF_TOKEN)." ) with gr.Row(): with gr.Column(scale=1, min_width=300): gr.Markdown("## 1. Configure your game") game_type_dropdown = gr.Dropdown( choices=GAME_TYPE_NAMES, value="Platformer", label="Game genre", ) type_description = gr.Markdown( value="_" + GAME_TYPES["Platformer"]["description"] + "_", ) theme_box = gr.Textbox( label="Theme / setting", placeholder="e.g. Neon cyberpunk rooftops", lines=2, value=THEME_EXAMPLES["Platformer"][0][0], ) gr.Examples( examples=THEME_EXAMPLES["Platformer"], inputs=[theme_box], label="Theme examples", ) gr.Markdown("## 2. Generation settings") temperature_slider = gr.Slider( minimum=0.3, maximum=1.2, value=0.7, step=0.05, label="Temperature - higher = more creative", ) max_tokens_slider = gr.Slider( minimum=2000, maximum=8000, value=6000, step=500, label="Max tokens - more = longer game", ) generate_btn = gr.Button("Generate Game + Sprites", variant="primary") gen_status = gr.Markdown(value="_No game generated yet._") with gr.Column(scale=2, min_width=500): gr.Markdown("## 3. Generated code (editable)") code_box = gr.Code( label="HTML source (sprites embedded as base64)", language="html", lines=12, interactive=True, ) launch_btn = gr.Button("Launch Game", variant="secondary") gr.Markdown("## 4. Live game window") game_frame = gr.HTML( value=_placeholder_html("Generate a game to see it here."), ) game_type_dropdown.change(fn=update_type_description, inputs=[game_type_dropdown], outputs=[type_description]) game_type_dropdown.change(fn=get_first_theme, inputs=[game_type_dropdown], outputs=[theme_box]) generate_btn.click( fn=generate_game_code, inputs=[game_type_dropdown, theme_box, temperature_slider, max_tokens_slider], outputs=[code_box, gen_status, game_frame], ) launch_btn.click(fn=launch_game, inputs=[code_box], outputs=[game_frame]) gr.Markdown( "---\n" "Code: **Qwen3-Coder-Next** via HF router (novita). " "Images: **ERNIE-Image** via fal-ai. " "Edit the HTML source and click **Launch Game** to hot-reload." ) return demo # --------------------------------------------------------------------------- # Entry point # --------------------------------------------------------------------------- if __name__ == "__main__": app = build_ui() app.launch()