Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -3,12 +3,11 @@ Text-to-Game Generator
|
|
| 3 |
Pipeline:
|
| 4 |
Theme --> [Groq Llama] --> HTML5 game code (with sprite_NAME.png refs)
|
| 5 |
Theme --> [Groq Llama acting as Z-Image-Engineer] --> cinematic image prompts
|
| 6 |
-
--> [FLUX.1-schnell via
|
| 7 |
--> injected as base64 into game HTML
|
| 8 |
|
| 9 |
Secrets needed:
|
| 10 |
GROQ_API_KEY - console.groq.com (free, no credit card)
|
| 11 |
-
HF_TOKEN - huggingface.co/settings/tokens (for FLUX via nscale)
|
| 12 |
"""
|
| 13 |
|
| 14 |
import os
|
|
@@ -16,7 +15,10 @@ import re
|
|
| 16 |
import io
|
| 17 |
import base64
|
| 18 |
import traceback
|
|
|
|
|
|
|
| 19 |
|
|
|
|
| 20 |
import gradio as gr
|
| 21 |
from openai import OpenAI
|
| 22 |
from PIL import Image
|
|
@@ -33,6 +35,10 @@ PROMPT_MODEL = "llama-3.3-70b-versatile" # Groq β creative prompts
|
|
| 33 |
# Pollinations.AI β completely free, no API key, no signup needed
|
| 34 |
POLLINATIONS_URL = "https://image.pollinations.ai/prompt/{prompt}?width={w}&height={h}&model=flux&nologo=true&seed={seed}"
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
def get_groq_client():
|
| 38 |
if not GROQ_API_KEY:
|
|
@@ -65,107 +71,74 @@ Z_ENGINEER_SYSTEM = (
|
|
| 65 |
)
|
| 66 |
|
| 67 |
# ---------------------------------------------------------------------------
|
| 68 |
-
# Game type configs
|
| 69 |
# ---------------------------------------------------------------------------
|
| 70 |
|
| 71 |
GAME_TYPES = {
|
| 72 |
"Platformer": {
|
| 73 |
-
"description": "
|
| 74 |
"prompt_template": (
|
| 75 |
"Create a complete, self-contained HTML5 platformer game with the theme: {theme}.\n"
|
| 76 |
-
"
|
| 77 |
-
"-
|
| 78 |
-
"-
|
| 79 |
-
"
|
| 80 |
-
"
|
| 81 |
-
"
|
| 82 |
-
"-
|
| 83 |
-
"
|
| 84 |
-
"
|
| 85 |
-
"-
|
| 86 |
-
"
|
| 87 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
"- NO external libraries, NO CDN links.\n"
|
| 89 |
-
"- Theme colors and labels to match: {theme}.\n"
|
| 90 |
"Output ONLY the raw HTML. No explanation, no markdown fences."
|
| 91 |
),
|
| 92 |
},
|
| 93 |
"Top-Down Shooter": {
|
| 94 |
-
"description": "Shoot
|
| 95 |
"prompt_template": (
|
| 96 |
"Create a complete, self-contained HTML5 top-down shooter game with the theme: {theme}.\n"
|
| 97 |
-
"
|
| 98 |
-
"-
|
| 99 |
-
"-
|
| 100 |
-
"-
|
| 101 |
-
"-
|
| 102 |
-
"-
|
| 103 |
-
"-
|
| 104 |
-
"
|
| 105 |
-
"-
|
| 106 |
-
"-
|
| 107 |
-
"-
|
| 108 |
-
"-
|
| 109 |
-
"-
|
| 110 |
-
"-
|
| 111 |
-
"
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
"
|
| 118 |
-
"Requirements:\n"
|
| 119 |
-
"- Single HTML file with all CSS and JavaScript inline.\n"
|
| 120 |
-
"- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
|
| 121 |
-
"- Player navigates with arrow keys or WASD.\n"
|
| 122 |
-
"- At least 15x10 tile grid, collectible keys, and a locked exit.\n"
|
| 123 |
-
"- Show a move counter or timer.\n"
|
| 124 |
-
"- Win screen when player collects all keys and exits.\n"
|
| 125 |
-
"- Use new Image() with src='sprite_player.png' for the player.\n"
|
| 126 |
-
"- Use new Image() with src='sprite_background.png' for the background.\n"
|
| 127 |
-
"- Draw tiles and UI with canvas shapes.\n"
|
| 128 |
-
"- NO external libraries, NO CDN links.\n"
|
| 129 |
-
"- Theme colors and labels to match: {theme}.\n"
|
| 130 |
-
"Output ONLY the raw HTML. No explanation, no markdown fences."
|
| 131 |
-
),
|
| 132 |
-
},
|
| 133 |
-
"Arcade / Dodge": {
|
| 134 |
-
"description": "Dodge falling obstacles and survive as long as possible.",
|
| 135 |
-
"prompt_template": (
|
| 136 |
-
"Create a complete, self-contained HTML5 arcade dodge game with the theme: {theme}.\n"
|
| 137 |
-
"Requirements:\n"
|
| 138 |
-
"- Single HTML file with all CSS and JavaScript inline.\n"
|
| 139 |
-
"- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
|
| 140 |
-
"- Player moves with arrow keys or WASD; obstacles increase in speed over time.\n"
|
| 141 |
-
"- Collision ends the game; show time survived as score.\n"
|
| 142 |
-
"- High score stored in a JS variable; restart button on game-over screen.\n"
|
| 143 |
-
"- Use requestAnimationFrame for the game loop.\n"
|
| 144 |
-
"- Use new Image() with src='sprite_player.png' for the player.\n"
|
| 145 |
-
"- Use new Image() with src='sprite_background.png' for the background.\n"
|
| 146 |
-
"- Draw obstacles and UI with canvas shapes.\n"
|
| 147 |
-
"- NO external libraries, NO CDN links.\n"
|
| 148 |
-
"- Theme colors and labels to match: {theme}.\n"
|
| 149 |
-
"Output ONLY the raw HTML. No explanation, no markdown fences."
|
| 150 |
-
),
|
| 151 |
-
},
|
| 152 |
-
"Surprise Me!": {
|
| 153 |
-
"description": "Let the AI invent the genre - could be anything!",
|
| 154 |
-
"prompt_template": (
|
| 155 |
-
"Create a complete, self-contained HTML5 browser game with the theme: {theme}.\n"
|
| 156 |
-
"Pick any fun arcade genre: breakout, snake, flappy-style, space invaders, etc.\n"
|
| 157 |
-
"Requirements:\n"
|
| 158 |
-
"- Single HTML file with all CSS and JavaScript inline.\n"
|
| 159 |
-
"- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
|
| 160 |
-
"- Keyboard or mouse controlled.\n"
|
| 161 |
-
"- Clear win/lose conditions and a score display.\n"
|
| 162 |
-
"- Game-over / win screen with a restart button.\n"
|
| 163 |
-
"- Use requestAnimationFrame for the game loop.\n"
|
| 164 |
-
"- Use new Image() with src='sprite_player.png' for the player.\n"
|
| 165 |
-
"- Use new Image() with src='sprite_background.png' for the background.\n"
|
| 166 |
-
"- Draw other elements with canvas shapes.\n"
|
| 167 |
"- NO external libraries, NO CDN links.\n"
|
| 168 |
-
"- Theme colors and labels vividly to match: {theme}.\n"
|
| 169 |
"Output ONLY the raw HTML. No explanation, no markdown fences."
|
| 170 |
),
|
| 171 |
},
|
|
@@ -175,10 +148,7 @@ GAME_TYPE_NAMES = list(GAME_TYPES.keys())
|
|
| 175 |
|
| 176 |
THEME_EXAMPLES = {
|
| 177 |
"Platformer": [["Jungle temple with ancient traps"], ["Neon cyberpunk rooftops"], ["Underwater pirate shipwreck"]],
|
| 178 |
-
"Top-Down Shooter": [["Alien desert invasion"], ["Viking village under siege"]
|
| 179 |
-
"Puzzle / Maze": [["Haunted library with secret doors"], ["Ice cave with frozen keys"], ["Egyptian pyramid tiles"]],
|
| 180 |
-
"Arcade / Dodge": [["Asteroid field in a tiny rocket"], ["Chef dodging ingredients"], ["Time traveller avoiding paradox storms"]],
|
| 181 |
-
"Surprise Me!": [["A sentient library that rearranges itself"], ["Deep sea bioluminescent creatures"], ["Retro space diner on a comet"]],
|
| 182 |
}
|
| 183 |
|
| 184 |
# ---------------------------------------------------------------------------
|
|
@@ -186,19 +156,15 @@ THEME_EXAMPLES = {
|
|
| 186 |
# ---------------------------------------------------------------------------
|
| 187 |
|
| 188 |
def generate_image_prompts(theme: str, game_type: str) -> dict:
|
| 189 |
-
"""
|
| 190 |
-
Use Groq with Z-Image-Engineer system prompt to generate
|
| 191 |
-
cinematic image prompts for player and background sprites.
|
| 192 |
-
Returns dict: { 'sprite_player.png': prompt, 'sprite_background.png': prompt, 'sprite_enemy.png': prompt }
|
| 193 |
-
"""
|
| 194 |
client = get_groq_client()
|
| 195 |
-
|
| 196 |
seeds = {
|
| 197 |
"sprite_player.png": f"pixel-art game player character for a {theme} themed {game_type} game, front-facing sprite, vibrant colors, clear silhouette, 64x64 pixel style",
|
| 198 |
"sprite_background.png": f"2D game background scene for a {theme} themed {game_type} game, wide landscape, atmospheric, game art style, 800x450",
|
| 199 |
"sprite_enemy.png": f"pixel-art enemy character for a {theme} themed {game_type} game, menacing, clear silhouette, 64x64 pixel style",
|
| 200 |
}
|
| 201 |
-
|
|
|
|
|
|
|
| 202 |
prompts = {}
|
| 203 |
for sprite_name, seed in seeds.items():
|
| 204 |
try:
|
|
@@ -214,18 +180,15 @@ def generate_image_prompts(theme: str, game_type: str) -> dict:
|
|
| 214 |
prompts[sprite_name] = response.choices[0].message.content.strip()
|
| 215 |
except Exception as exc:
|
| 216 |
print(f"[Z-Engineer] Failed {sprite_name}: {exc}")
|
| 217 |
-
prompts[sprite_name] = seed
|
| 218 |
return prompts
|
| 219 |
|
| 220 |
# ---------------------------------------------------------------------------
|
| 221 |
-
# Step 2: Generate images via
|
| 222 |
# ---------------------------------------------------------------------------
|
| 223 |
|
| 224 |
def _pil_to_data_uri(pil_image: Image.Image, size: tuple = None) -> str:
|
| 225 |
-
if size
|
| 226 |
-
img = pil_image.resize(size, Image.LANCZOS)
|
| 227 |
-
else:
|
| 228 |
-
img = pil_image
|
| 229 |
buf = io.BytesIO()
|
| 230 |
img.save(buf, format="PNG")
|
| 231 |
return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
|
|
@@ -245,28 +208,16 @@ def _colored_placeholder(name: str) -> str:
|
|
| 245 |
|
| 246 |
|
| 247 |
def generate_sprites(image_prompts: dict) -> tuple:
|
| 248 |
-
"""Generate images via Pollinations.AI (free, no API key, no signup).
|
| 249 |
-
Returns (sprite_map, errors_list)"""
|
| 250 |
-
import requests
|
| 251 |
-
from urllib.parse import quote
|
| 252 |
-
|
| 253 |
sprite_map = {}
|
| 254 |
-
errors
|
| 255 |
-
|
| 256 |
for i, (sprite_name, prompt) in enumerate(image_prompts.items()):
|
| 257 |
-
# Wait 20s between requests β Pollinations free tier allows 1 per 15s
|
| 258 |
if i > 0:
|
| 259 |
-
import time
|
| 260 |
time.sleep(20)
|
| 261 |
try:
|
| 262 |
-
is_bg
|
| 263 |
-
w, h
|
| 264 |
-
seed
|
| 265 |
-
url
|
| 266 |
-
prompt=quote(prompt),
|
| 267 |
-
w=w, h=h, seed=seed
|
| 268 |
-
)
|
| 269 |
-
# Retry up to 3 times with increasing timeout
|
| 270 |
for attempt in range(3):
|
| 271 |
try:
|
| 272 |
response = requests.get(url, timeout=120)
|
|
@@ -274,7 +225,6 @@ def generate_sprites(image_prompts: dict) -> tuple:
|
|
| 274 |
break
|
| 275 |
except Exception as retry_exc:
|
| 276 |
if attempt < 2:
|
| 277 |
-
import time
|
| 278 |
time.sleep(20)
|
| 279 |
else:
|
| 280 |
raise retry_exc
|
|
@@ -287,7 +237,6 @@ def generate_sprites(image_prompts: dict) -> tuple:
|
|
| 287 |
errors.append(f"{sprite_name}: {error_msg}")
|
| 288 |
print(f"[Pollinations] FAILED {sprite_name}: {error_msg}")
|
| 289 |
sprite_map[sprite_name] = _colored_placeholder(sprite_name.replace(".png", ""))
|
| 290 |
-
|
| 291 |
return sprite_map, errors
|
| 292 |
|
| 293 |
# ---------------------------------------------------------------------------
|
|
@@ -295,7 +244,6 @@ def generate_sprites(image_prompts: dict) -> tuple:
|
|
| 295 |
# ---------------------------------------------------------------------------
|
| 296 |
|
| 297 |
def _inject_sprites(html_code: str, sprite_map: dict) -> str:
|
| 298 |
-
# Step 1: Direct string replacement of filename with base64 data URI
|
| 299 |
for fname, data_uri in sprite_map.items():
|
| 300 |
html_code = html_code.replace('"' + fname + '"', '"' + data_uri + '"')
|
| 301 |
html_code = html_code.replace("'" + fname + "'", "'" + data_uri + "'")
|
|
@@ -342,6 +290,8 @@ CODE_SYSTEM = (
|
|
| 342 |
" const dist = Math.sqrt(dx*dx+dy*dy); "
|
| 343 |
" bullets.push({x: player.x, y: player.y, vx: dx/dist*10, vy: dy/dist*10}); } "
|
| 344 |
" FOR PLATFORMER: use gravity velY += 0.5, grounded checks, jump with ArrowUp. "
|
|
|
|
|
|
|
| 345 |
"6. Draw background FIRST each frame: ctx.drawImage(bgImg, 0, 0, canvas.width, canvas.height). "
|
| 346 |
"7. Draw player/enemies with ctx.drawImage(playerImg, x, y, w, h). "
|
| 347 |
"8. Always keep player inside canvas boundaries. "
|
|
@@ -354,10 +304,10 @@ def generate_game_code(game_type: str, theme: str, temperature: float, max_new_t
|
|
| 354 |
return "", "", "Please enter a theme first.", _placeholder_html("Enter a theme and generate a game.")
|
| 355 |
|
| 356 |
try:
|
| 357 |
-
client
|
| 358 |
-
user_prompt
|
| 359 |
|
| 360 |
-
#
|
| 361 |
code_resp = client.chat.completions.create(
|
| 362 |
model=CODE_MODEL,
|
| 363 |
messages=[
|
|
@@ -373,35 +323,24 @@ def generate_game_code(game_type: str, theme: str, temperature: float, max_new_t
|
|
| 373 |
if "<html" not in code.lower() and "<!doctype" not in code.lower():
|
| 374 |
code = _wrap_in_html(code, theme)
|
| 375 |
|
| 376 |
-
#
|
| 377 |
image_prompts = generate_image_prompts(theme.strip(), game_type)
|
| 378 |
|
| 379 |
-
#
|
| 380 |
sprite_map, sprite_errors = generate_sprites(image_prompts)
|
| 381 |
|
| 382 |
-
#
|
| 383 |
final_code = _inject_sprites(code, sprite_map)
|
| 384 |
|
| 385 |
n_real = sum(1 for v in sprite_map.values() if "image/png" in v)
|
| 386 |
n_fallback = len(sprite_map) - n_real
|
| 387 |
|
| 388 |
if sprite_errors:
|
| 389 |
-
|
| 390 |
-
status = (
|
| 391 |
-
f"Code generated. Images: {n_real} by Pollinations/FLUX, {n_fallback} fallback. "
|
| 392 |
-
f"FLUX errors: {error_detail}"
|
| 393 |
-
)
|
| 394 |
else:
|
| 395 |
-
status = (
|
| 396 |
-
f"Done! {n_real} sprite(s) generated by Pollinations/FLUX. "
|
| 397 |
-
"Click Launch Game to play."
|
| 398 |
-
)
|
| 399 |
-
|
| 400 |
-
# Show the enhanced prompts that were used
|
| 401 |
-
prompt_summary = "\n\n".join(
|
| 402 |
-
f"**{k}:**\n{v}" for k, v in image_prompts.items()
|
| 403 |
-
)
|
| 404 |
|
|
|
|
| 405 |
return final_code, prompt_summary, status, _build_preview(final_code)
|
| 406 |
|
| 407 |
except Exception as exc:
|
|
@@ -423,11 +362,11 @@ def generate_game_code(game_type: str, theme: str, temperature: float, max_new_t
|
|
| 423 |
def _placeholder_html(message: str) -> str:
|
| 424 |
safe = message.replace("<", "<").replace(">", ">")
|
| 425 |
return (
|
| 426 |
-
'<div style="display:flex;align-items:center;justify-content:center;'
|
| 427 |
-
'width:
|
| 428 |
-
'border:2px dashed #333;color:#555;font-family:monospace;font-size:14px;'
|
| 429 |
-
'text-align:center;padding:24px;box-sizing:border-box;">'
|
| 430 |
-
'<pre style="margin:0;white-space:pre-wrap;">
|
| 431 |
)
|
| 432 |
|
| 433 |
|
|
@@ -444,8 +383,8 @@ def _wrap_in_html(snippet: str, theme: str) -> str:
|
|
| 444 |
def _build_preview(html_code: str) -> str:
|
| 445 |
encoded = base64.b64encode(html_code.encode("utf-8")).decode("ascii")
|
| 446 |
return (
|
| 447 |
-
'<iframe src="data:text/html;base64,
|
| 448 |
-
'style="width:
|
| 449 |
'sandbox="allow-scripts" title="Game Preview"></iframe>'
|
| 450 |
)
|
| 451 |
|
|
@@ -478,9 +417,8 @@ def build_ui():
|
|
| 478 |
gr.Markdown(
|
| 479 |
"# Game Generator\n"
|
| 480 |
"Type a theme β the AI writes the game code, generates cinematic image prompts "
|
| 481 |
-
"using **Z-Image-Engineer V4** style, then **FLUX
|
| 482 |
-
"> Secrets needed: `GROQ_API_KEY` (console.groq.com, free
|
| 483 |
-
"and `HF_TOKEN` (huggingface.co/settings/tokens, for FLUX images)."
|
| 484 |
)
|
| 485 |
|
| 486 |
with gr.Row():
|
|
@@ -522,26 +460,26 @@ def build_ui():
|
|
| 522 |
generate_btn = gr.Button("Generate Game + Sprites", variant="primary")
|
| 523 |
gen_status = gr.Markdown(value="_No game generated yet._")
|
| 524 |
|
| 525 |
-
gr.Markdown("## 3. Generated image prompts")
|
| 526 |
-
prompt_display = gr.Markdown(
|
| 527 |
-
value="_Image prompts will appear here after generation._"
|
| 528 |
-
)
|
| 529 |
-
|
| 530 |
# ββ Right: code + game ββββββββββββββββββββββββββββββββββββββββ
|
| 531 |
with gr.Column(scale=2, min_width=500):
|
| 532 |
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
|
|
|
| 541 |
|
| 542 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
|
| 544 |
-
gr.Markdown("##
|
| 545 |
|
| 546 |
game_frame = gr.HTML(
|
| 547 |
value=_placeholder_html("Generate a game to see it here."),
|
|
@@ -571,8 +509,9 @@ def build_ui():
|
|
| 571 |
gr.Markdown(
|
| 572 |
"---\n"
|
| 573 |
"**Pipeline:** Theme β [Groq Llama] game code + [Groq 70B as Z-Image-Engineer] "
|
| 574 |
-
"cinematic prompts β [Pollinations
|
| 575 |
-
"
|
|
|
|
| 576 |
)
|
| 577 |
|
| 578 |
return demo
|
|
|
|
| 3 |
Pipeline:
|
| 4 |
Theme --> [Groq Llama] --> HTML5 game code (with sprite_NAME.png refs)
|
| 5 |
Theme --> [Groq Llama acting as Z-Image-Engineer] --> cinematic image prompts
|
| 6 |
+
--> [FLUX.1-schnell via Pollinations] --> sprite images
|
| 7 |
--> injected as base64 into game HTML
|
| 8 |
|
| 9 |
Secrets needed:
|
| 10 |
GROQ_API_KEY - console.groq.com (free, no credit card)
|
|
|
|
| 11 |
"""
|
| 12 |
|
| 13 |
import os
|
|
|
|
| 15 |
import io
|
| 16 |
import base64
|
| 17 |
import traceback
|
| 18 |
+
import time
|
| 19 |
+
from urllib.parse import quote
|
| 20 |
|
| 21 |
+
import requests
|
| 22 |
import gradio as gr
|
| 23 |
from openai import OpenAI
|
| 24 |
from PIL import Image
|
|
|
|
| 35 |
# Pollinations.AI β completely free, no API key, no signup needed
|
| 36 |
POLLINATIONS_URL = "https://image.pollinations.ai/prompt/{prompt}?width={w}&height={h}&model=flux&nologo=true&seed={seed}"
|
| 37 |
|
| 38 |
+
# Canvas dimensions β iframe is fixed to these so game fits perfectly
|
| 39 |
+
CANVAS_W = 800
|
| 40 |
+
CANVAS_H = 450
|
| 41 |
+
|
| 42 |
|
| 43 |
def get_groq_client():
|
| 44 |
if not GROQ_API_KEY:
|
|
|
|
| 71 |
)
|
| 72 |
|
| 73 |
# ---------------------------------------------------------------------------
|
| 74 |
+
# Game type configs β Platformer and Top-Down Shooter only
|
| 75 |
# ---------------------------------------------------------------------------
|
| 76 |
|
| 77 |
GAME_TYPES = {
|
| 78 |
"Platformer": {
|
| 79 |
+
"description": "Reach the goal platform while avoiding wandering monsters.",
|
| 80 |
"prompt_template": (
|
| 81 |
"Create a complete, self-contained HTML5 platformer game with the theme: {theme}.\n"
|
| 82 |
+
"EXACT GAME RULES β implement every one precisely:\n"
|
| 83 |
+
"- Canvas: id='gameCanvas', size 800x450.\n"
|
| 84 |
+
"- PLATFORMS: at least 6 platforms drawn with ctx.drawImage(platformImg, x, y, w, h). "
|
| 85 |
+
" Include one full-width ground platform at y=420 height=20. "
|
| 86 |
+
" Place 5 elevated platforms at varied x/y positions. "
|
| 87 |
+
" Use new Image() with src='sprite_platform.png' for ALL platforms.\n"
|
| 88 |
+
"- TARGET/GOAL: a glowing star or chest sprite at the top-right platform. "
|
| 89 |
+
" Use new Image() with src='sprite_goal.png'. "
|
| 90 |
+
" When player bounding box overlaps goal: show WIN screen with score and Restart button.\n"
|
| 91 |
+
"- PLAYER: starts bottom-left. Size 40x40. "
|
| 92 |
+
" Move left/right with A/D or ArrowLeft/ArrowRight (speed 4). "
|
| 93 |
+
" Jump with W, ArrowUp, or Space (velY = -12, only when grounded). "
|
| 94 |
+
" Gravity: velY += 0.5 every frame. "
|
| 95 |
+
" Platform collision: set grounded=false BEFORE loop; inside loop if player feet hit platform top set grounded=true velY=0. "
|
| 96 |
+
" Keep player inside canvas horizontally.\n"
|
| 97 |
+
"- MONSTERS: 3 monsters, each patrolling back-and-forth on its own platform. "
|
| 98 |
+
" Size 32x32. Speed 1.5 px/frame. Reverse direction at platform edges. "
|
| 99 |
+
" Use new Image() with src='sprite_enemy.png'. "
|
| 100 |
+
" If monster bounding box overlaps player: player lives -= 1, respawn player at start. "
|
| 101 |
+
" 0 lives = GAME OVER screen with Restart button.\n"
|
| 102 |
+
"- HUD: lives top-left, score top-right.\n"
|
| 103 |
+
"- IMAGES: declare all at top of script before anything else: "
|
| 104 |
+
" const playerImg = new Image(); playerImg.src = 'sprite_player.png'; "
|
| 105 |
+
" const bgImg = new Image(); bgImg.src = 'sprite_background.png'; "
|
| 106 |
+
" const enemyImg = new Image(); enemyImg.src = 'sprite_enemy.png'; "
|
| 107 |
+
" const platformImg = new Image(); platformImg.src = 'sprite_platform.png'; "
|
| 108 |
+
" const goalImg = new Image(); goalImg.src = 'sprite_goal.png'; "
|
| 109 |
+
" Use Promise.all([loadImg(playerImg),loadImg(bgImg),loadImg(enemyImg),loadImg(platformImg),loadImg(goalImg)]).then(startGame).\n"
|
| 110 |
+
"- Define ALL functions at TOP LEVEL, not inside Promise.then or startGame.\n"
|
| 111 |
+
"- Add window keydown/keyup listeners to a keys Set inside startGame().\n"
|
| 112 |
"- NO external libraries, NO CDN links.\n"
|
|
|
|
| 113 |
"Output ONLY the raw HTML. No explanation, no markdown fences."
|
| 114 |
),
|
| 115 |
},
|
| 116 |
"Top-Down Shooter": {
|
| 117 |
+
"description": "Shoot monsters coming from the top before they reach you.",
|
| 118 |
"prompt_template": (
|
| 119 |
"Create a complete, self-contained HTML5 top-down shooter game with the theme: {theme}.\n"
|
| 120 |
+
"EXACT GAME RULES β implement every one precisely:\n"
|
| 121 |
+
"- Canvas: id='gameCanvas', size 800x450.\n"
|
| 122 |
+
"- MONSTERS spawn at random x positions along the TOP edge (y=0) and move DOWNWARD only (y += speed each frame).\n"
|
| 123 |
+
"- Monster size: 32x32. Monster speed: 1-2 px/frame (slower than player speed of 4).\n"
|
| 124 |
+
"- PLAYER starts at bottom-center, moves left/right only with A/D or ArrowLeft/ArrowRight keys.\n"
|
| 125 |
+
"- Player size: 48x48. Player speed: 4 px/frame. Keep player inside canvas bounds.\n"
|
| 126 |
+
"- LEFT MOUSE CLICK fires one bullet from player position toward the click point.\n"
|
| 127 |
+
" Bullet speed: 10 px/frame. Remove bullets that leave canvas.\n"
|
| 128 |
+
"- BULLET HIT: if a bullet rect overlaps a monster rect, remove BOTH the bullet and the monster. Score += 10.\n"
|
| 129 |
+
"- MONSTER COLLISION: if a monster rect overlaps the player rect, remove the monster. Player health -= 1.\n"
|
| 130 |
+
"- MONSTER ESCAPED: if a monster reaches y > canvas.height remove it (no health loss).\n"
|
| 131 |
+
"- New monsters spawn every 90 frames. Spawn rate increases every 500 points.\n"
|
| 132 |
+
"- HUD: draw score top-left, health top-right (show as hearts or number).\n"
|
| 133 |
+
"- GAME OVER when health reaches 0: show score and a Restart button.\n"
|
| 134 |
+
"- NO gravity, NO velY += 0.5, NO grounded, NO jumping.\n"
|
| 135 |
+
"- Use new Image() with src='sprite_player.png' for the player (48x48).\n"
|
| 136 |
+
"- Use new Image() with src='sprite_background.png' for the background (full canvas).\n"
|
| 137 |
+
"- Use new Image() with src='sprite_enemy.png' for monsters (32x32).\n"
|
| 138 |
+
"- Declare all images at top of script, use Promise.all to wait before starting gameLoop.\n"
|
| 139 |
+
"- Define all functions at TOP LEVEL, not inside Promise.then or startGame.\n"
|
| 140 |
+
"- Add window keydown/keyup listeners to a keys Set inside startGame().\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
"- NO external libraries, NO CDN links.\n"
|
|
|
|
| 142 |
"Output ONLY the raw HTML. No explanation, no markdown fences."
|
| 143 |
),
|
| 144 |
},
|
|
|
|
| 148 |
|
| 149 |
THEME_EXAMPLES = {
|
| 150 |
"Platformer": [["Jungle temple with ancient traps"], ["Neon cyberpunk rooftops"], ["Underwater pirate shipwreck"]],
|
| 151 |
+
"Top-Down Shooter": [["Ancient Egyptian tomb raid with cursed mummies"], ["Alien desert invasion"], ["Viking village under siege"]],
|
|
|
|
|
|
|
|
|
|
| 152 |
}
|
| 153 |
|
| 154 |
# ---------------------------------------------------------------------------
|
|
|
|
| 156 |
# ---------------------------------------------------------------------------
|
| 157 |
|
| 158 |
def generate_image_prompts(theme: str, game_type: str) -> dict:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
client = get_groq_client()
|
|
|
|
| 160 |
seeds = {
|
| 161 |
"sprite_player.png": f"pixel-art game player character for a {theme} themed {game_type} game, front-facing sprite, vibrant colors, clear silhouette, 64x64 pixel style",
|
| 162 |
"sprite_background.png": f"2D game background scene for a {theme} themed {game_type} game, wide landscape, atmospheric, game art style, 800x450",
|
| 163 |
"sprite_enemy.png": f"pixel-art enemy character for a {theme} themed {game_type} game, menacing, clear silhouette, 64x64 pixel style",
|
| 164 |
}
|
| 165 |
+
if game_type == "Platformer":
|
| 166 |
+
seeds["sprite_platform.png"] = f"pixel-art solid platform tile for a {theme} themed platformer game, rectangular, textured surface, 128x24 pixel style"
|
| 167 |
+
seeds["sprite_goal.png"] = f"pixel-art goal or treasure chest for a {theme} themed platformer game, glowing, clearly visible, 40x40 pixel style"
|
| 168 |
prompts = {}
|
| 169 |
for sprite_name, seed in seeds.items():
|
| 170 |
try:
|
|
|
|
| 180 |
prompts[sprite_name] = response.choices[0].message.content.strip()
|
| 181 |
except Exception as exc:
|
| 182 |
print(f"[Z-Engineer] Failed {sprite_name}: {exc}")
|
| 183 |
+
prompts[sprite_name] = seed
|
| 184 |
return prompts
|
| 185 |
|
| 186 |
# ---------------------------------------------------------------------------
|
| 187 |
+
# Step 2: Generate images via Pollinations.AI
|
| 188 |
# ---------------------------------------------------------------------------
|
| 189 |
|
| 190 |
def _pil_to_data_uri(pil_image: Image.Image, size: tuple = None) -> str:
|
| 191 |
+
img = pil_image.resize(size, Image.LANCZOS) if size else pil_image
|
|
|
|
|
|
|
|
|
|
| 192 |
buf = io.BytesIO()
|
| 193 |
img.save(buf, format="PNG")
|
| 194 |
return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
|
|
|
|
| 208 |
|
| 209 |
|
| 210 |
def generate_sprites(image_prompts: dict) -> tuple:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
sprite_map = {}
|
| 212 |
+
errors = []
|
|
|
|
| 213 |
for i, (sprite_name, prompt) in enumerate(image_prompts.items()):
|
|
|
|
| 214 |
if i > 0:
|
|
|
|
| 215 |
time.sleep(20)
|
| 216 |
try:
|
| 217 |
+
is_bg = "background" in sprite_name
|
| 218 |
+
w, h = (CANVAS_W, CANVAS_H) if is_bg else (64, 64)
|
| 219 |
+
seed = abs(hash(sprite_name)) % 99999
|
| 220 |
+
url = POLLINATIONS_URL.format(prompt=quote(prompt), w=w, h=h, seed=seed)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
for attempt in range(3):
|
| 222 |
try:
|
| 223 |
response = requests.get(url, timeout=120)
|
|
|
|
| 225 |
break
|
| 226 |
except Exception as retry_exc:
|
| 227 |
if attempt < 2:
|
|
|
|
| 228 |
time.sleep(20)
|
| 229 |
else:
|
| 230 |
raise retry_exc
|
|
|
|
| 237 |
errors.append(f"{sprite_name}: {error_msg}")
|
| 238 |
print(f"[Pollinations] FAILED {sprite_name}: {error_msg}")
|
| 239 |
sprite_map[sprite_name] = _colored_placeholder(sprite_name.replace(".png", ""))
|
|
|
|
| 240 |
return sprite_map, errors
|
| 241 |
|
| 242 |
# ---------------------------------------------------------------------------
|
|
|
|
| 244 |
# ---------------------------------------------------------------------------
|
| 245 |
|
| 246 |
def _inject_sprites(html_code: str, sprite_map: dict) -> str:
|
|
|
|
| 247 |
for fname, data_uri in sprite_map.items():
|
| 248 |
html_code = html_code.replace('"' + fname + '"', '"' + data_uri + '"')
|
| 249 |
html_code = html_code.replace("'" + fname + "'", "'" + data_uri + "'")
|
|
|
|
| 290 |
" const dist = Math.sqrt(dx*dx+dy*dy); "
|
| 291 |
" bullets.push({x: player.x, y: player.y, vx: dx/dist*10, vy: dy/dist*10}); } "
|
| 292 |
" FOR PLATFORMER: use gravity velY += 0.5, grounded checks, jump with ArrowUp. "
|
| 293 |
+
" Set grounded=false BEFORE platform loop. Set grounded=true and velY=0 only on landing. "
|
| 294 |
+
" Always include full-width ground platform at y=420. "
|
| 295 |
"6. Draw background FIRST each frame: ctx.drawImage(bgImg, 0, 0, canvas.width, canvas.height). "
|
| 296 |
"7. Draw player/enemies with ctx.drawImage(playerImg, x, y, w, h). "
|
| 297 |
"8. Always keep player inside canvas boundaries. "
|
|
|
|
| 304 |
return "", "", "Please enter a theme first.", _placeholder_html("Enter a theme and generate a game.")
|
| 305 |
|
| 306 |
try:
|
| 307 |
+
client = get_groq_client()
|
| 308 |
+
user_prompt = GAME_TYPES[game_type]["prompt_template"].format(theme=theme.strip())
|
| 309 |
|
| 310 |
+
# Code generation
|
| 311 |
code_resp = client.chat.completions.create(
|
| 312 |
model=CODE_MODEL,
|
| 313 |
messages=[
|
|
|
|
| 323 |
if "<html" not in code.lower() and "<!doctype" not in code.lower():
|
| 324 |
code = _wrap_in_html(code, theme)
|
| 325 |
|
| 326 |
+
# Image prompts
|
| 327 |
image_prompts = generate_image_prompts(theme.strip(), game_type)
|
| 328 |
|
| 329 |
+
# Sprites
|
| 330 |
sprite_map, sprite_errors = generate_sprites(image_prompts)
|
| 331 |
|
| 332 |
+
# Inject
|
| 333 |
final_code = _inject_sprites(code, sprite_map)
|
| 334 |
|
| 335 |
n_real = sum(1 for v in sprite_map.values() if "image/png" in v)
|
| 336 |
n_fallback = len(sprite_map) - n_real
|
| 337 |
|
| 338 |
if sprite_errors:
|
| 339 |
+
status = f"Code done. Images: {n_real} FLUX, {n_fallback} fallback. Errors: {' | '.join(sprite_errors)}"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
else:
|
| 341 |
+
status = f"Done! {n_real} sprite(s) by Pollinations/FLUX. Click Launch Game to play."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
|
| 343 |
+
prompt_summary = "\n\n".join(f"**{k}:**\n{v}" for k, v in image_prompts.items())
|
| 344 |
return final_code, prompt_summary, status, _build_preview(final_code)
|
| 345 |
|
| 346 |
except Exception as exc:
|
|
|
|
| 362 |
def _placeholder_html(message: str) -> str:
|
| 363 |
safe = message.replace("<", "<").replace(">", ">")
|
| 364 |
return (
|
| 365 |
+
f'<div style="display:flex;align-items:center;justify-content:center;'
|
| 366 |
+
f'width:{CANVAS_W}px;height:{CANVAS_H}px;background:#0d0d0d;border-radius:12px;'
|
| 367 |
+
f'border:2px dashed #333;color:#555;font-family:monospace;font-size:14px;'
|
| 368 |
+
f'text-align:center;padding:24px;box-sizing:border-box;">'
|
| 369 |
+
f'<pre style="margin:0;white-space:pre-wrap;">{safe}</pre></div>'
|
| 370 |
)
|
| 371 |
|
| 372 |
|
|
|
|
| 383 |
def _build_preview(html_code: str) -> str:
|
| 384 |
encoded = base64.b64encode(html_code.encode("utf-8")).decode("ascii")
|
| 385 |
return (
|
| 386 |
+
f'<iframe src="data:text/html;base64,{encoded}" '
|
| 387 |
+
f'style="width:{CANVAS_W}px;height:{CANVAS_H}px;border:none;border-radius:12px;background:#000;display:block;" '
|
| 388 |
'sandbox="allow-scripts" title="Game Preview"></iframe>'
|
| 389 |
)
|
| 390 |
|
|
|
|
| 417 |
gr.Markdown(
|
| 418 |
"# Game Generator\n"
|
| 419 |
"Type a theme β the AI writes the game code, generates cinematic image prompts "
|
| 420 |
+
"using **Z-Image-Engineer V4** style, then **FLUX/Pollinations** renders the sprites.\n\n"
|
| 421 |
+
"> Secrets needed: `GROQ_API_KEY` (console.groq.com, free, no credit card)."
|
|
|
|
| 422 |
)
|
| 423 |
|
| 424 |
with gr.Row():
|
|
|
|
| 460 |
generate_btn = gr.Button("Generate Game + Sprites", variant="primary")
|
| 461 |
gen_status = gr.Markdown(value="_No game generated yet._")
|
| 462 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
# ββ Right: code + game ββββββββββββββββββββββββββββββββββββββββ
|
| 464 |
with gr.Column(scale=2, min_width=500):
|
| 465 |
|
| 466 |
+
# Collapsible code window
|
| 467 |
+
with gr.Accordion("3. Generated code β click to expand/hide", open=False):
|
| 468 |
+
code_box = gr.Code(
|
| 469 |
+
label="HTML source (sprites embedded as base64)",
|
| 470 |
+
language="html",
|
| 471 |
+
lines=12,
|
| 472 |
+
interactive=True,
|
| 473 |
+
)
|
| 474 |
+
launch_btn = gr.Button("Launch Game", variant="secondary")
|
| 475 |
|
| 476 |
+
# Collapsible image prompts window
|
| 477 |
+
with gr.Accordion("3b. Generated image prompts β click to expand/hide", open=False):
|
| 478 |
+
prompt_display = gr.Markdown(
|
| 479 |
+
value="_Image prompts will appear here after generation._"
|
| 480 |
+
)
|
| 481 |
|
| 482 |
+
gr.Markdown("## 4. Live game window")
|
| 483 |
|
| 484 |
game_frame = gr.HTML(
|
| 485 |
value=_placeholder_html("Generate a game to see it here."),
|
|
|
|
| 509 |
gr.Markdown(
|
| 510 |
"---\n"
|
| 511 |
"**Pipeline:** Theme β [Groq Llama] game code + [Groq 70B as Z-Image-Engineer] "
|
| 512 |
+
"cinematic prompts β [Pollinations/FLUX] sprites β embedded in game. "
|
| 513 |
+
f"Game window fixed to {CANVAS_W}Γ{CANVAS_H}px β matches canvas exactly. "
|
| 514 |
+
"Edit HTML and click **Launch Game** to hot-reload."
|
| 515 |
)
|
| 516 |
|
| 517 |
return demo
|