""" 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 = ( '' ) 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 + '