LeafCat79 commited on
Commit
ff680de
Β·
verified Β·
1 Parent(s): 0b87d8b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +112 -173
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 nscale] --> 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
- 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": "Jump over enemies and obstacles to reach the goal.",
74
  "prompt_template": (
75
  "Create a complete, self-contained HTML5 platformer game with the theme: {theme}.\n"
76
- "Requirements:\n"
77
- "- Single HTML file with all CSS and JavaScript inline.\n"
78
- "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
79
- "- Player moves left/right with arrow keys or WASD and jumps.\n"
80
- "- At least 5 platforms, 2 moving enemies, and a goal to reach.\n"
81
- "- Show score/lives on the canvas.\n"
82
- "- Use requestAnimationFrame for the game loop.\n"
83
- "- Use new Image() with src='sprite_player.png' for the player character.\n"
84
- "- Use new Image() with src='sprite_background.png' for the background.\n"
85
- "- Use new Image() with src='sprite_enemy.png' for enemies.\n"
86
- "- Draw images with ctx.drawImage(img, x, y, w, h).\n"
87
- "- Draw platforms and UI with canvas shapes.\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 waves of enemies before they reach you.",
95
  "prompt_template": (
96
  "Create a complete, self-contained HTML5 top-down shooter game with the theme: {theme}.\n"
97
- "Requirements:\n"
98
- "- Single HTML file with all CSS and JavaScript inline.\n"
99
- "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
100
- "- Player moves with WASD/arrows; shoots with Space or click.\n"
101
- "- Enemies spawn from edges in escalating waves.\n"
102
- "- Display health, score, and wave on canvas.\n"
103
- "- Game-over screen with score and restart button.\n"
104
- "- Use requestAnimationFrame for the game loop.\n"
105
- "- Use new Image() with src='sprite_player.png' for the player.\n"
106
- "- Use new Image() with src='sprite_background.png' for the background.\n"
107
- "- Use new Image() with src='sprite_enemy.png' for enemies.\n"
108
- "- Draw images with ctx.drawImage(img, x, y, w, h).\n"
109
- "- NO external libraries, NO CDN links.\n"
110
- "- Theme colors and labels to match: {theme}.\n"
111
- "Output ONLY the raw HTML. No explanation, no markdown fences."
112
- ),
113
- },
114
- "Puzzle / Maze": {
115
- "description": "Navigate a maze or solve a tile puzzle to escape.",
116
- "prompt_template": (
117
- "Create a complete, self-contained HTML5 maze/tile-puzzle game with the theme: {theme}.\n"
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"], ["Steampunk robot uprising"]],
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 # fallback to raw seed
218
  return prompts
219
 
220
  # ---------------------------------------------------------------------------
221
- # Step 2: Generate images via FLUX.1-schnell
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 = "background" in sprite_name
263
- w, h = (800, 450) if is_bg else (64, 64)
264
- seed = abs(hash(sprite_name)) % 99999
265
- url = POLLINATIONS_URL.format(
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 = get_groq_client()
358
- user_prompt = GAME_TYPES[game_type]["prompt_template"].format(theme=theme.strip())
359
 
360
- # -- Code generation (Llama) --
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
- # -- Image prompt generation (Z-Image-Engineer via Groq) --
377
  image_prompts = generate_image_prompts(theme.strip(), game_type)
378
 
379
- # -- Sprite generation (FLUX.1-schnell) --
380
  sprite_map, sprite_errors = generate_sprites(image_prompts)
381
 
382
- # -- Inject sprites --
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
- error_detail = " | ".join(sprite_errors)
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("<", "&lt;").replace(">", "&gt;")
425
  return (
426
- '<div style="display:flex;align-items:center;justify-content:center;'
427
- 'width:100%;height:460px;background:#0d0d0d;border-radius:12px;'
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;">' + safe + '</pre></div>'
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,' + encoded + '" '
448
- 'style="width:100%;height:460px;border:none;border-radius:12px;background:#000;" '
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.1-schnell** renders the sprites.\n\n"
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
- gr.Markdown("## 4. Generated code (editable)")
534
-
535
- code_box = gr.Code(
536
- label="HTML source (sprites embedded as base64)",
537
- language="html",
538
- lines=12,
539
- interactive=True,
540
- )
 
541
 
542
- launch_btn = gr.Button("Launch Game", variant="secondary")
 
 
 
 
543
 
544
- gr.Markdown("## 5. Live game window")
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.AI/FLUX] sprites β†’ embedded in game. "
575
- "Edit the HTML and click **Launch Game** to hot-reload."
 
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("<", "&lt;").replace(">", "&gt;")
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