""" Text-to-Game Generator Pipeline: Theme --> [Groq Llama] --> HTML5 game code (with sprite_NAME.png refs) Theme --> [Groq Llama acting as Z-Image-Engineer] --> cinematic image prompts --> [FLUX.1-dev via fal-ai] --> sprite images --> injected as base64 into game HTML Secrets needed: GROQ_API_KEY - console.groq.com (free, no credit card) HF_TOKEN - huggingface.co/settings/tokens (for FLUX.1-dev via fal-ai) """ import os import re import io import base64 import traceback import time import gradio as gr from openai import OpenAI import requests from urllib.parse import quote from huggingface_hub import InferenceClient from PIL import Image # --------------------------------------------------------------------------- # Clients # --------------------------------------------------------------------------- GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "") HF_TOKEN = os.environ.get("HF_TOKEN", "") CODE_MODEL = "llama-3.1-8b-instant" # Groq — game code PROMPT_MODEL = "llama-3.3-70b-versatile" # Groq — image prompt enhancement IMAGE_MODEL = "black-forest-labs/FLUX.1-dev" # fal-ai — image generation POLLINATIONS_URL = "https://image.pollinations.ai/prompt/{prompt}?width={w}&height={h}&model=flux&nologo=true&seed={seed}" # Canvas dimensions — iframe is fixed to these so game fits perfectly CANVAS_W = 800 CANVAS_H = 450 def get_groq_client(): if not GROQ_API_KEY: raise ValueError( "GROQ_API_KEY not set. " "Get a free key at console.groq.com and add it as a Space secret." ) return OpenAI( base_url="https://api.groq.com/openai/v1", api_key=GROQ_API_KEY, ) # --------------------------------------------------------------------------- # Z-Image-Engineer system prompt (from BennyDaBall/Qwen3-4b-Z-Image-Engineer-V4) # --------------------------------------------------------------------------- Z_ENGINEER_SYSTEM = ( "Interpret the user seed as production intent, then build a definitive 200-250 word " "single-paragraph image prompt that preserves every explicit constraint while intelligently " "expanding missing details. First infer the core subject, action, setting, and emotional tone; " "treat these as non-negotiable anchors. Then enhance with precise visual staging " "(explicit foreground, midground, background), clear visual hierarchy and eye path, " "physically plausible lighting (source, direction, softness, color temperature), and optical " "strategy (if lens/aperture are provided, preserve exactly; if absent, choose fitting lens and " "aperture and imply their depth-of-field effect). Integrate organic, manufactured, and " "environmental textures with realistic material behavior, add motion/atmospheric cues only " "when they support the scene, and apply a coherent color grade consistent with mood and " "environment. Output ONLY the image prompt paragraph. No explanation, no preamble." ) # --------------------------------------------------------------------------- # Game type configs — Platformer and Top-Down Shooter only # --------------------------------------------------------------------------- GAME_TYPES = { "Platformer": { "description": "Reach the goal platform while avoiding wandering monsters.", "prompt_template": ( "Create a complete, self-contained HTML5 platformer game with the theme: {theme}.\n" "EXACT GAME RULES — implement every one precisely:\n" "- Canvas: id='gameCanvas', size 800x450.\n" "- PLATFORMS: at least 6 platforms drawn with ctx.drawImage(platformImg, x, y, w, h). " " Include one full-width ground platform at y=420 height=20. " " Place 5 elevated platforms at varied x/y positions. " " Use new Image() with src='sprite_platform.png' for ALL platforms.\n" "- TARGET/GOAL: a glowing star or chest sprite at the top-right platform. " " Use new Image() with src='sprite_goal.png'. " " When player bounding box overlaps goal: show WIN screen with score and Restart button.\n" "- PLAYER: starts bottom-left. Size 40x40. " " Move left/right with A/D or ArrowLeft/ArrowRight (speed 4). " " Jump with W, ArrowUp, or Space (velY = -12, only when grounded). " " Gravity: velY += 0.5 every frame. " " Platform collision: set grounded=false BEFORE loop; inside loop if player feet hit platform top set grounded=true velY=0. " " Keep player inside canvas horizontally.\n" "- MONSTERS: 3 monsters, each patrolling back-and-forth on its own platform. " " Size 32x32. Speed 1.5 px/frame. Reverse direction at platform edges. " " Use new Image() with src='sprite_enemy.png'. " " If monster bounding box overlaps player: player lives -= 1, respawn player at start. " " 0 lives = GAME OVER screen with Restart button.\n" "- HUD: lives top-left, score top-right.\n" "- IMAGES: declare all at top of script before anything else: " " const playerImg = new Image(); playerImg.src = 'sprite_player.png'; " " const bgImg = new Image(); bgImg.src = 'sprite_background.png'; " " const enemyImg = new Image(); enemyImg.src = 'sprite_enemy.png'; " " const platformImg = new Image(); platformImg.src = 'sprite_platform.png'; " " const goalImg = new Image(); goalImg.src = 'sprite_goal.png'; " " Use Promise.all([loadImg(playerImg),loadImg(bgImg),loadImg(enemyImg),loadImg(platformImg),loadImg(goalImg)]).then(startGame).\n" "- Define ALL functions at TOP LEVEL, not inside Promise.then or startGame.\n" "- Add window keydown/keyup listeners to a keys Set inside startGame().\n" "- NO external libraries, NO CDN links.\n" "Output ONLY the raw HTML. No explanation, no markdown fences." ), }, "Top-Down Shooter": { "description": "Shoot monsters coming from the top before they reach you.", "prompt_template": ( "Create a complete, self-contained HTML5 top-down shooter game with the theme: {theme}.\n" "EXACT GAME RULES — implement every one precisely:\n" "- Canvas: id='gameCanvas', size 800x450.\n" "- MONSTERS spawn at random x positions along the TOP edge (y=0) and move DOWNWARD only (y += speed each frame).\n" "- Monster size: 32x32. Monster speed: 1-2 px/frame (slower than player speed of 4).\n" "- PLAYER starts at bottom-center, moves left/right only with A/D or ArrowLeft/ArrowRight keys.\n" "- Player size: 48x48. Player speed: 4 px/frame. Keep player inside canvas bounds.\n" "- LEFT MOUSE CLICK fires one bullet from player position toward the click point.\n" " Bullet speed: 10 px/frame. Remove bullets that leave canvas.\n" "- BULLET HIT: if a bullet rect overlaps a monster rect, remove BOTH the bullet and the monster. Score += 10.\n" "- MONSTER COLLISION: if a monster rect overlaps the player rect, remove the monster. Player health -= 1.\n" "- MONSTER ESCAPED: if a monster reaches y > canvas.height remove it (no health loss).\n" "- New monsters spawn every 90 frames. Spawn rate increases every 500 points.\n" "- HUD: draw score top-left, health top-right (show as hearts or number).\n" "- GAME OVER when health reaches 0: show score and a Restart button.\n" "- NO gravity, NO velY += 0.5, NO grounded, NO jumping.\n" "- Use new Image() with src='sprite_player.png' for the player (48x48).\n" "- Use new Image() with src='sprite_background.png' for the background (full canvas).\n" "- Use new Image() with src='sprite_enemy.png' for monsters (32x32).\n" "- Declare all images at top of script, use Promise.all to wait before starting gameLoop.\n" "- Define all functions at TOP LEVEL, not inside Promise.then or startGame.\n" "- Add window keydown/keyup listeners to a keys Set inside startGame().\n" "- NO external libraries, NO CDN links.\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": [["Ancient Egyptian tomb raid with cursed mummies"], ["Alien desert invasion"], ["Viking village under siege"]], } # --------------------------------------------------------------------------- # Step 1: Generate image prompts via Z-Image-Engineer (Groq) # --------------------------------------------------------------------------- def generate_image_prompts(theme: str, game_type: str) -> dict: client = get_groq_client() if game_type == "Top-Down Shooter": bg_style = "bird's eye overhead view, top-down 2D game background, viewed from directly above, like a map" char_style = ( "top-down overhead game sprite, viewed from directly above, " "character body seen from above like looking straight down, " "head at top shoulders below, like Metal Slug or GTA 2 sprite angle, " "pure black background, character only" ) else: bg_style = "2D side-scrolling platformer background, horizontal landscape viewed from the side" char_style = ( "2D side-view platformer game sprite, character facing right, " "full body visible from the side like Super Mario or Mega Man, " "classic side-scrolling game art style, " "pure black background, character only" ) seeds = { "sprite_player.png": ( f"{char_style}, {theme} theme, " f"vibrant colors, strong clear silhouette, 64x64 pixel style, " f"single character centered, no scenery no ground no environment" ), "sprite_background.png": ( f"{bg_style}, {theme} theme, " f"wide atmospheric scene, game art style, 800x450, " f"no characters no sprites, environment only" ), "sprite_enemy.png": ( f"{char_style}, {theme} theme, enemy monster villain, " f"menacing threatening design, strong clear silhouette, 64x64 pixel style, " f"single enemy centered, no scenery no ground no environment" ), } if game_type == "Platformer": seeds["sprite_platform.png"] = ( f"2D side-view pixel-art platform tile, {theme} theme, " f"rectangular solid surface like a game platform, stone or wood texture, " f"pure black background, platform only, 128x24 pixel style" ) seeds["sprite_goal.png"] = ( f"2D side-view pixel-art treasure chest or glowing star goal, {theme} theme, " f"glowing clearly visible reward item, " f"pure black background, item only, 40x40 pixel style" ) prompts = {} for sprite_name, seed in seeds.items(): try: response = client.chat.completions.create( model=PROMPT_MODEL, messages=[ {"role": "system", "content": Z_ENGINEER_SYSTEM}, {"role": "user", "content": seed}, ], max_tokens=400, temperature=0.8, ) prompts[sprite_name] = response.choices[0].message.content.strip() except Exception as exc: print(f"[Z-Engineer] Failed {sprite_name}: {exc}") prompts[sprite_name] = seed return prompts # --------------------------------------------------------------------------- # Step 2: Generate images via FLUX.1-dev (fal-ai) # --------------------------------------------------------------------------- def _remove_background(pil_image: Image.Image) -> Image.Image: """Remove background from sprite using rembg library.""" try: from rembg import remove buf_in = io.BytesIO() pil_image.save(buf_in, format="PNG") buf_in.seek(0) buf_out = io.BytesIO() remove(buf_in.read(), output=buf_out) buf_out.seek(0) return Image.open(buf_out).convert("RGBA") except Exception as exc: print(f"[rembg] Background removal failed: {exc}") return pil_image def _pil_to_data_uri(pil_image: Image.Image, size: tuple = None) -> str: img = pil_image.resize(size, Image.LANCZOS) if size else pil_image 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 _generate_via_hf(prompt: str) -> Image.Image: """Try FLUX.1-dev via HF auto-provider selection. HF automatically picks the fastest available provider — if one is quota-depleted it routes to the next available one automatically.""" client = InferenceClient(api_key=HF_TOKEN) return client.text_to_image(prompt, model=IMAGE_MODEL) def _generate_via_pollinations(prompt: str, sprite_name: str, is_bg: bool) -> Image.Image: """Fallback: Pollinations.AI, free, no key needed.""" w, h = (CANVAS_W, CANVAS_H) if is_bg else (64, 64) seed = abs(hash(sprite_name)) % 99999 url = POLLINATIONS_URL.format(prompt=quote(prompt), w=w, h=h, seed=seed) for attempt in range(3): try: resp = requests.get(url, timeout=120) resp.raise_for_status() return Image.open(io.BytesIO(resp.content)) except Exception as exc: if attempt < 2: time.sleep(20) else: raise exc def generate_sprites(image_prompts: dict) -> tuple: sprite_map = {} errors = [] for i, (sprite_name, prompt) in enumerate(image_prompts.items()): try: is_bg = "background" in sprite_name # Try FLUX.1-dev via fal-ai if HF_TOKEN is set if HF_TOKEN: try: pil_img = _generate_via_hf(prompt) provider = "FLUX.1-dev/auto" except Exception as fal_exc: fal_err = str(fal_exc) print(f"[fal-ai] Failed {sprite_name}: {fal_err} — falling back to Pollinations") errors.append(f"{sprite_name} (fal-ai failed, used Pollinations): {fal_err[:60]}") if i > 0: time.sleep(20) pil_img = _generate_via_pollinations(prompt, sprite_name, is_bg) provider = "Pollinations" else: # No HF_TOKEN — go straight to Pollinations if i > 0: time.sleep(20) pil_img = _generate_via_pollinations(prompt, sprite_name, is_bg) provider = "Pollinations" if not is_bg: pil_img = _remove_background(pil_img) size = None if is_bg else (64, 64) sprite_map[sprite_name] = _pil_to_data_uri(pil_img, size=size) print(f"[{provider}] OK: {sprite_name}") except Exception as exc: error_msg = str(exc) errors.append(f"{sprite_name}: {error_msg[:80]}") print(f"[Image] FAILED {sprite_name}: {error_msg}") sprite_map[sprite_name] = _colored_placeholder(sprite_name.replace(".png", "")) return sprite_map, errors # --------------------------------------------------------------------------- # Step 3: Inject sprites into HTML # --------------------------------------------------------------------------- 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 # --------------------------------------------------------------------------- # Step 4: Generate game code via Groq Llama # --------------------------------------------------------------------------- CODE_SYSTEM = ( "You are an expert HTML5 game developer. " "Write a complete, working, single-file HTML5 game using canvas and vanilla JavaScript. " "CRITICAL RULES - copy these patterns EXACTLY as written, do not deviate: " "1. The VERY FIRST lines of the script must be exactly: " " const canvas = document.getElementById('gameCanvas'); " " const ctx = canvas.getContext('2d'); " " const playerImg = new Image(); playerImg.src = 'sprite_player.png'; " " const bgImg = new Image(); bgImg.src = 'sprite_background.png'; " " const enemyImg = new Image(); enemyImg.src = 'sprite_enemy.png'; " " const keys = new Set(); " " const bullets = []; " " const enemies = []; " " let score = 0; let health = 3; let frameCount = 0; let gameOver = false; " "2. FOR TOP-DOWN SHOOTER declare player at CENTER - copy EXACTLY: " " let player = {x: canvas.width/2-24, y: canvas.height/2-24, w:48, h:48, speed:4}; " " DO NOT use canvas.height - 24 for player y. Player starts in CENTER not bottom. " "3. Use Promise.all AFTER all declarations: " " function loadImg(img) { return new Promise(r => { img.onload = r; }); } " " Promise.all([loadImg(playerImg), loadImg(bgImg), loadImg(enemyImg)]).then(startGame); " "4. startGame() adds ONLY keyboard listeners and calls gameLoop - NO click listener here: " " function startGame() { " " window.addEventListener('keydown', e => keys.add(e.key)); " " window.addEventListener('keyup', e => keys.delete(e.key)); " " canvas.addEventListener('click', onShoot); " " requestAnimationFrame(gameLoop); } " "5. gameLoop() NEVER redeclares canvas or ctx. Draw background FIRST with FULL SIZE: " " ctx.drawImage(bgImg, 0, 0, canvas.width, canvas.height); " " This is critical - always pass canvas.width and canvas.height as 3rd and 4th arguments. " "6. FOR TOP-DOWN SHOOTER movement - 4 directions, clamp inside canvas: " " if (keys.has('w')||keys.has('W')||keys.has('ArrowUp')) player.y -= player.speed; " " if (keys.has('s')||keys.has('S')||keys.has('ArrowDown')) player.y += player.speed; " " if (keys.has('a')||keys.has('A')||keys.has('ArrowLeft')) player.x -= player.speed; " " if (keys.has('d')||keys.has('D')||keys.has('ArrowRight')) player.x += player.speed; " " player.x = Math.max(0, Math.min(canvas.width-player.w, player.x)); " " player.y = Math.max(0, Math.min(canvas.height-player.h, player.y)); " "7. Bullets fire on MOUSE CLICK straight UP only - no vx component: " " function onShoot(e) { if (gameOver) return; " " bullets.push({x:player.x+player.w/2-4, y:player.y-16, w:8, h:16, vy:-10}); } " " Each frame: bullets[i].y += bullets[i].vy; draw yellow rect; remove when y+h<0. " "8. Enemies spawn every 120 frames, fall straight down, speed capped at 3.5: " " frameCount++; " " if (frameCount % 120 === 0) enemies.push({x:Math.random()*(canvas.width-32), y:0, w:32, h:32, speed: Math.min(1+score/500, 3.5)}); " " Each frame: e.y += e.speed; ctx.drawImage(enemyImg,e.x,e.y,32,32); " " Remove if e.y > canvas.height. If overlaps player: health--; remove enemy. " "9. Draw player: ctx.drawImage(playerImg, player.x, player.y, player.w, player.h). " "10. GAME OVER when health<=0 - set gameOver=true then draw overlay INSIDE gameLoop: " " ctx.fillStyle='rgba(0,0,0,0.7)'; ctx.fillRect(0,0,canvas.width,canvas.height); " " draw GAME OVER text and score at center. " " draw a green restart button rect at center+60px. " " add ONE-TIME click listener for restart ONLY when gameOver becomes true: " " canvas.removeEventListener('click', onShoot); " " canvas.addEventListener('click', restartHandler); " " then call return to stop the loop. " "11. restartHandler resets ALL variables and restores onShoot listener: " " function restartHandler() { " " canvas.removeEventListener('click', restartHandler); " " player.x=canvas.width/2-24; player.y=canvas.height/2-24; " " bullets.length=0; enemies.length=0; " " score=0; health=3; frameCount=0; gameOver=false; " " canvas.addEventListener('click', onShoot); " " requestAnimationFrame(gameLoop); } " "12. FOR PLATFORMER: gravity velY+=0.5, jump ArrowUp/W/Space velY=-12 when grounded. " " Set grounded=false BEFORE platform loop. Set true and velY=0 only on landing. " " Full-width ground at y=420. " "Output ONLY the raw HTML - no markdown fences, no explanation." ) 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.") try: client = get_groq_client() user_prompt = GAME_TYPES[game_type]["prompt_template"].format(theme=theme.strip()) # Code generation code_resp = client.chat.completions.create( model=CODE_MODEL, messages=[ {"role": "system", "content": CODE_SYSTEM}, {"role": "user", "content": user_prompt}, ], max_tokens=int(max_new_tokens), temperature=float(temperature), ) code = code_resp.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 ( f'
' f'
{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 ( f'
' f'
' ) 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" "Type a theme — the AI writes the game code, generates cinematic image prompts " "using **Z-Image-Engineer V4** style, then **FLUX.1-dev** renders the sprites.\n\n" "> Secrets needed: `GROQ_API_KEY` (console.groq.com, free, no credit card)." ) with gr.Row(): # ── Left: controls ─────────────────────────────────────────── 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. Ancient Egyptian pyramid with cursed mummies", lines=3, 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=1000, maximum=6000, value=4000, 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._") # ── Right: code + game ──────────────────────────────────────── with gr.Column(scale=2, min_width=500): # Collapsible code window with gr.Accordion("3. Generated code — click to expand/hide", open=False): 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") # Collapsible image prompts window with gr.Accordion("3b. Generated image prompts — click to expand/hide", open=False): prompt_display = gr.Markdown( value="_Image prompts will appear here after generation._" ) gr.Markdown("## 4. Live game window") game_frame = gr.HTML( value=_placeholder_html("Generate a game to see it here."), ) # ── Wiring ──────────────────────────────────────────────────────── 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, prompt_display, gen_status, game_frame], ) launch_btn.click(fn=launch_game, inputs=[code_box], outputs=[game_frame]) gr.Markdown( "---\n" "**Pipeline:** Theme → [Groq Llama] game code + [Groq 70B as Z-Image-Engineer] " "cinematic prompts → [FLUX.1-dev/fal-ai] sprites → embedded in game. " "Game window fixed to 800×450px — matches canvas exactly. " "Edit HTML and click **Launch Game** to hot-reload." ) return demo # --------------------------------------------------------------------------- # Entry point # --------------------------------------------------------------------------- if __name__ == "__main__": app = build_ui() app.launch()