Spaces:
Sleeping
Sleeping
File size: 30,999 Bytes
76b9939 84ab022 d455ec5 84ab022 a22d8dd df65baa d455ec5 76b9939 675f75f 76b9939 4900774 76b9939 ff680de 76b9939 675f75f 507bbf7 d455ec5 4900774 76b9939 a22d8dd 76b9939 da2ca08 d455ec5 76b9939 d455ec5 507bbf7 da2ca08 ff680de ba60839 84ab022 b93c43a 675f75f 84ab022 4824a4a 84ab022 7f2f0ed 84ab022 4900774 ff680de 76b9939 de7a564 76b9939 ff680de 84ab022 ff680de df65baa ff680de df65baa ff680de df65baa ff680de 84ab022 76b9939 df65baa 84ab022 df65baa 84ab022 76b9939 df65baa ff680de 76b9939 4900774 84ab022 a22d8dd 84ab022 e61e8fe 4769793 bec5b87 e61e8fe 4769793 bec5b87 e61e8fe 84ab022 e61e8fe 4769793 e61e8fe 4769793 bec5b87 e61e8fe 4769793 e61e8fe 84ab022 ff680de e61e8fe 4769793 e61e8fe 4769793 e61e8fe 84ab022 ff680de 84ab022 a22d8dd d455ec5 4900774 bec5b87 84ab022 ff680de 4900774 df65baa 4900774 507bbf7 16640e1 84ab022 df65baa d455ec5 507bbf7 d455ec5 507bbf7 d455ec5 bec5b87 84ab022 507bbf7 84ab022 a505043 d455ec5 507bbf7 16640e1 507bbf7 16640e1 4900774 a22d8dd 84ab022 a22d8dd 4900774 84ab022 4900774 0aa39d5 84ab022 4900774 76b9939 84ab022 76b9939 de7a564 84ab022 f1221c3 f9787b6 df65baa c437906 df65baa 0ba8dab f9787b6 f1221c3 f9787b6 f1221c3 c437906 df65baa f1221c3 4769793 0ba8dab f1221c3 c437906 f9787b6 f1221c3 f9787b6 f1221c3 c437906 f9787b6 f1221c3 c437906 f9787b6 0ba8dab 84ab022 76b9939 84ab022 76b9939 84ab022 df65baa ff680de a22d8dd ff680de 84ab022 df65baa 84ab022 ff680de 84ab022 ff680de 16640e1 84ab022 ff680de 84ab022 16640e1 ff680de 16640e1 d455ec5 4900774 ff680de 84ab022 76b9939 84ab022 675f75f 76b9939 ff680de 76b9939 c437906 ff680de c437906 76b9939 675f75f 76b9939 84ab022 76b9939 675f75f 76b9939 a22d8dd 76b9939 84ab022 d455ec5 ff680de 76b9939 df65baa 76b9939 84ab022 76b9939 84ab022 76b9939 84ab022 76b9939 84ab022 a22d8dd 76b9939 84ab022 76b9939 84ab022 76b9939 84ab022 76b9939 84ab022 675f75f 84ab022 76b9939 84ab022 df65baa de7a564 df65baa 84ab022 ff680de 76b9939 ff680de 76b9939 ff680de 76b9939 a22d8dd 84ab022 76b9939 84ab022 76b9939 84ab022 d455ec5 df65baa 76b9939 675f75f 76b9939 d8b0471 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 | """
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 = (
'<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">'
'<rect width="64" height="64" fill="' + colour + '" rx="8"/>'
'<text x="32" y="38" font-size="10" text-anchor="middle" fill="white">' + label + '</text>'
'</svg>'
)
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 "<html" not in code.lower() and "<!doctype" not in code.lower():
code = _wrap_in_html(code, theme)
# Image prompts
image_prompts = generate_image_prompts(theme.strip(), game_type)
# Sprites
sprite_map, sprite_errors = generate_sprites(image_prompts)
# Inject
final_code = _inject_sprites(code, sprite_map)
n_real = sum(1 for v in sprite_map.values() if "image/png" in v)
n_fallback = len(sprite_map) - n_real
if sprite_errors:
status = f"Code done. Images: {n_real} FLUX, {n_fallback} fallback. Errors: {' | '.join(sprite_errors)}"
else:
status = f"Done! {n_real} sprite(s) by FLUX.1-dev. Click Launch Game to play."
prompt_summary = "\n\n".join(f"**{k}:**\n{v}" for k, v in image_prompts.items())
return final_code, prompt_summary, status, _build_preview(final_code)
except Exception as exc:
traceback.print_exc()
err = str(exc)
if "401" in err or "api_key" in err.lower():
err = "Invalid GROQ_API_KEY. Check your key at console.groq.com."
elif "429" in err or "rate" in err.lower():
err = "Rate limited by Groq - wait a few seconds and try again."
else:
err = "Error: " + str(exc)
return "", "", err, _placeholder_html(err)
# ---------------------------------------------------------------------------
# HTML helpers
# ---------------------------------------------------------------------------
def _placeholder_html(message: str) -> str:
safe = message.replace("<", "<").replace(">", ">")
return (
f'<div style="display:flex;align-items:center;justify-content:center;'
f'width:{CANVAS_W}px;height:{CANVAS_H}px;background:#0d0d0d;border-radius:12px;'
f'border:2px dashed #333;color:#555;font-family:monospace;font-size:14px;'
f'text-align:center;padding:24px;box-sizing:border-box;">'
f'<pre style="margin:0;white-space:pre-wrap;">{safe}</pre></div>'
)
def _wrap_in_html(snippet: str, theme: str) -> str:
return (
"<!DOCTYPE html>\n<html lang='en'>\n<head>\n"
"<meta charset='UTF-8'><title>" + theme + "</title>\n"
"<style>body{margin:0;background:#111;display:flex;justify-content:center;"
"align-items:center;height:100vh;}canvas{display:block;}</style>\n"
"</head>\n<body>\n" + snippet + "\n</body>\n</html>"
)
def _build_preview(html_code: str) -> str:
encoded = base64.b64encode(html_code.encode("utf-8")).decode("ascii")
return (
f'<div style="width:{CANVAS_W}px;height:{CANVAS_H}px;overflow:hidden;border-radius:12px;">'
f'<iframe src="data:text/html;base64,{encoded}" '
f'style="width:{CANVAS_W}px;height:{CANVAS_H}px;border:none;background:#000;display:block;" '
f'scrolling="no" '
'sandbox="allow-scripts" title="Game Preview"></iframe></div>'
)
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() |