LeafCat79 commited on
Commit
df65baa
Β·
verified Β·
1 Parent(s): d8b0471

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +90 -196
app.py CHANGED
@@ -7,10 +7,9 @@ Pipeline:
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
-
14
  import os
15
  import re
16
  import io
@@ -19,29 +18,23 @@ import traceback
19
  import time
20
  from urllib.parse import quote
21
 
22
-
23
  import requests
24
  import gradio as gr
25
  from openai import OpenAI
26
  from PIL import Image
27
 
28
-
29
  # ---------------------------------------------------------------------------
30
  # Clients
31
  # ---------------------------------------------------------------------------
32
 
33
-
34
  GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
35
 
36
-
37
- CODE_MODEL = "llama-3.1-8b-instant" # Groq β€” game code
38
- PROMPT_MODEL = "llama-3.3-70b-versatile" # Groq β€” creative prompts
39
-
40
 
41
  # Pollinations.AI β€” completely free, no API key, no signup needed
42
  POLLINATIONS_URL = "https://image.pollinations.ai/prompt/{prompt}?width={w}&height={h}&model=flux&nologo=true&seed={seed}"
43
 
44
-
45
  # Canvas dimensions β€” iframe is fixed to these so game fits perfectly
46
  CANVAS_W = 800
47
  CANVAS_H = 450
@@ -63,7 +56,6 @@ def get_groq_client():
63
  # Z-Image-Engineer system prompt (from BennyDaBall/Qwen3-4b-Z-Image-Engineer-V4)
64
  # ---------------------------------------------------------------------------
65
 
66
-
67
  Z_ENGINEER_SYSTEM = (
68
  "Interpret the user seed as production intent, then build a definitive 200-250 word "
69
  "single-paragraph image prompt that preserves every explicit constraint while intelligently "
@@ -78,17 +70,9 @@ Z_ENGINEER_SYSTEM = (
78
  "environment. Output ONLY the image prompt paragraph. No explanation, no preamble."
79
  )
80
 
81
-
82
  # ---------------------------------------------------------------------------
83
  # Game type configs β€” Platformer and Top-Down Shooter only
84
  # ---------------------------------------------------------------------------
85
- #
86
- # FIX: The Platformer prompt_template now explicitly requires four-direction
87
- # movement (WASD + arrow keys in all four directions). Gravity/grounded
88
- # physics are kept for vertical feel, but S/ArrowDown is wired to move the
89
- # player downward (fast-drop / crouch-walk) so all four directions work.
90
- # ---------------------------------------------------------------------------
91
-
92
 
93
  GAME_TYPES = {
94
  "Platformer": {
@@ -105,22 +89,17 @@ GAME_TYPES = {
105
  " Use new Image() with src='sprite_goal.png'. "
106
  " When player bounding box overlaps goal: show WIN screen with score and Restart button.\n"
107
  "- PLAYER: starts bottom-left. Size 40x40. "
108
- " FOUR-DIRECTION MOVEMENT β€” implement ALL of the following:\n"
109
- " * Move LEFT: A key OR ArrowLeft -> player.x -= 4\n"
110
- " * Move RIGHT: D key OR ArrowRight -> player.x += 4\n"
111
- " * Move UP / JUMP: W key OR ArrowUp OR Space -> if grounded: player.velY = -12; grounded = false\n"
112
- " * Move DOWN (fast-drop): S key OR ArrowDown -> if (!grounded): player.velY += 3 (fast-fall); "
113
- " if (grounded): player.y += 2 (crouch/walk down a step, capped at canvas bottom)\n"
114
- " All four directions MUST be handled in the update loop via a keys Set. "
115
  " Gravity: velY += 0.5 every frame. "
116
  " Platform collision: set grounded=false BEFORE loop; inside loop if player feet hit platform top set grounded=true velY=0. "
117
- " Keep player inside canvas horizontally and vertically (clamp x and y).\n"
118
  "- MONSTERS: 3 monsters, each patrolling back-and-forth on its own platform. "
119
  " Size 32x32. Speed 1.5 px/frame. Reverse direction at platform edges. "
120
  " Use new Image() with src='sprite_enemy.png'. "
121
  " If monster bounding box overlaps player: player lives -= 1, respawn player at start. "
122
  " 0 lives = GAME OVER screen with Restart button.\n"
123
- "- HUD: lives top-left, score top-right. Show control hint: 'WASD / Arrows: move | W/Up/Space: jump | S/Down: fast-fall'.\n"
124
  "- IMAGES: declare all at top of script before anything else: "
125
  " const playerImg = new Image(); playerImg.src = 'sprite_player.png'; "
126
  " const bgImg = new Image(); bgImg.src = 'sprite_background.png'; "
@@ -135,104 +114,47 @@ GAME_TYPES = {
135
  ),
136
  },
137
  "Top-Down Shooter": {
138
- "description": "Move in all four directions, shoot chasing monsters, and survive waves.",
139
  "prompt_template": (
140
  "Create a complete, self-contained HTML5 top-down shooter game with the theme: {theme}.\n"
141
- "EXACT GAME RULES β€” implement every one precisely. This is a TOP-DOWN game, not a platformer.\n"
142
- "\n"
143
- "VARIABLES β€” CRITICAL:\n"
144
- "- Declare score, health, wave, frameCount, gameOver, spawnInterval as `let`, NOT `const`.\n"
145
- " Example: let score = 0; let health = 5; let wave = 1; let frameCount = 0;\n"
146
- " let gameOver = false; let spawnInterval = 90;\n"
147
- "- Mutating a `const` is a silent no-op in JavaScript β€” the game will break invisibly.\n"
148
- "\n"
149
- "CANVAS: id='gameCanvas', size 800x450.\n"
150
- "\n"
151
- "PLAYER:\n"
152
- "- Object: {{ x, y, w:48, h:48, speed:4 }}. Start near center-bottom.\n"
153
- "- FOUR-DIRECTION MOVEMENT using a keys Set β€” implement exactly:\n"
154
- " let dx = 0, dy = 0;\n"
155
- " if (keys.has('w') || keys.has('ArrowUp')) dy -= 1;\n"
156
- " if (keys.has('s') || keys.has('ArrowDown')) dy += 1;\n"
157
- " if (keys.has('a') || keys.has('ArrowLeft')) dx -= 1;\n"
158
- " if (keys.has('d') || keys.has('ArrowRight')) dx += 1;\n"
159
- " if (dx !== 0 && dy !== 0) {{ const len = Math.sqrt(2); dx /= len; dy /= len; }}\n"
160
- " player.x += dx * player.speed;\n"
161
- " player.y += dy * player.speed;\n"
162
- "- Clamp: player.x = Math.max(0, Math.min(canvas.width - player.w, player.x));\n"
163
- " player.y = Math.max(0, Math.min(canvas.height - player.h, player.y));\n"
164
- "- NO gravity, NO velY, NO grounded, NO jumping, NO platforms.\n"
165
- "\n"
166
- "ENEMIES:\n"
167
- "- Each enemy object MUST have: {{ x, y, w:32, h:32, speed }}.\n"
168
- " w and h are required for collision detection β€” do not omit them.\n"
169
- "- Spawn enemies from the TOP EDGE ONLY: x = random within canvas width, y = -32.\n"
170
- "- Chase player each frame: const edx = player.x-e.x; const edy = player.y-e.y;\n"
171
- " const edist = Math.sqrt(edx*edx+edy*edy);\n"
172
- " if (edist > 0) {{ e.x += (edx/edist)*e.speed; e.y += (edy/edist)*e.speed; }}\n"
173
- "- Enemy speed: 1.2 to 2 px/frame.\n"
174
- "- Spawn 2 enemies immediately at game start, then 1 more every spawnInterval frames.\n"
175
- "- Use frameCount (incremented each frame) to trigger spawning β€” NOT score % N.\n"
176
- " Example: if (frameCount % spawnInterval === 0) spawnEnemy();\n"
177
- "- Increase difficulty over time by reducing spawnInterval (min 30) as frameCount grows.\n"
178
- "\n"
179
- "BULLETS:\n"
180
- "- Each bullet object: {{ x, y, vx, vy, w:10, h:10 }}.\n"
181
- "- Fire from player center toward mouse on mousedown.\n"
182
- "- Remove bullets that leave canvas bounds.\n"
183
- "- Bullet/enemy collision: use rect overlap (x,y,w,h). Remove both bullet and enemy. score += 10.\n"
184
- "- Draw bullets as bright yellow circles so they are visible against dark backgrounds.\n"
185
- "\n"
186
- "COLLISIONS β€” use rectangle overlap for everything:\n"
187
- " function rectsOverlap(a, b) {{\n"
188
- " return a.x < b.x+b.w && a.x+a.w > b.x && a.y < b.y+b.h && a.y+a.h > b.y;\n"
189
- " }}\n"
190
- "- Enemy overlaps player: health--; remove that enemy. If health <= 0: gameOver = true.\n"
191
- "\n"
192
- "DRAW ORDER β€” every frame, in this exact order:\n"
193
- " 1. ctx.drawImage(bgImg, 0, 0, canvas.width, canvas.height) ← background first\n"
194
- " 2. ctx.drawImage(playerImg, player.x, player.y, player.w, player.h)\n"
195
- " 3. draw all enemies with ctx.drawImage(enemyImg, e.x, e.y, e.w, e.h)\n"
196
- " 4. draw bullets as bright yellow filled circles\n"
197
- " 5. draw HUD text on top (score top-left, health top-right, wave top-center)\n"
198
- "- DO NOT draw a solid black fillRect over the canvas β€” it hides everything underneath.\n"
199
- "\n"
200
- "GAME OVER: when gameOver is true, draw a semi-transparent overlay with final score.\n"
201
- " Clicking canvas calls restartGame() which resets all let variables and arrays.\n"
202
- "\n"
203
- "IMAGES:\n"
204
- "- const playerImg = new Image(); playerImg.src = 'sprite_player.png';\n"
205
- "- const bgImg = new Image(); bgImg.src = 'sprite_background.png';\n"
206
- "- const enemyImg = new Image(); enemyImg.src = 'sprite_enemy.png';\n"
207
- "- const keys = new Set();\n"
208
- "- function loadImg(img) {{ return new Promise(r => {{ img.onload = r; if (img.complete) r(); }}); }}\n"
209
- "- Promise.all([loadImg(playerImg), loadImg(bgImg), loadImg(enemyImg)]).then(startGame);\n"
210
- "\n"
211
- "STRUCTURE:\n"
212
- "- Define ALL functions at TOP LEVEL β€” not inside Promise.then or startGame.\n"
213
- "- startGame() sets up keydown/keyup listeners, mousedown listener, spawns 2 enemies, calls requestAnimationFrame(gameLoop).\n"
214
  "- NO external libraries, NO CDN links.\n"
215
- "\n"
216
  "Output ONLY the raw HTML. No explanation, no markdown fences."
217
  ),
218
  },
219
  }
220
 
221
-
222
  GAME_TYPE_NAMES = list(GAME_TYPES.keys())
223
 
224
-
225
  THEME_EXAMPLES = {
226
- "Platformer": [["Jungle temple with ancient traps"], ["Neon cyberpunk rooftops"], ["Underwater pirate shipwreck"]],
227
  "Top-Down Shooter": [["Ancient Egyptian tomb raid with cursed mummies"], ["Alien desert invasion"], ["Viking village under siege"]],
228
  }
229
 
230
-
231
  # ---------------------------------------------------------------------------
232
  # Step 1: Generate image prompts via Z-Image-Engineer (Groq)
233
  # ---------------------------------------------------------------------------
234
 
235
-
236
  def generate_image_prompts(theme: str, game_type: str) -> dict:
237
  client = get_groq_client()
238
  seeds = {
@@ -261,12 +183,10 @@ def generate_image_prompts(theme: str, game_type: str) -> dict:
261
  prompts[sprite_name] = seed
262
  return prompts
263
 
264
-
265
  # ---------------------------------------------------------------------------
266
  # Step 2: Generate images via Pollinations.AI
267
  # ---------------------------------------------------------------------------
268
 
269
-
270
  def _pil_to_data_uri(pil_image: Image.Image, size: tuple = None) -> str:
271
  img = pil_image.resize(size, Image.LANCZOS) if size else pil_image
272
  buf = io.BytesIO()
@@ -276,8 +196,8 @@ def _pil_to_data_uri(pil_image: Image.Image, size: tuple = None) -> str:
276
 
277
  def _colored_placeholder(name: str) -> str:
278
  colours = ["#e74c3c", "#3498db", "#2ecc71", "#f39c12", "#9b59b6", "#1abc9c"]
279
- colour = colours[abs(hash(name)) % len(colours)]
280
- label = name.replace("sprite_", "")[:6]
281
  svg = (
282
  '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">'
283
  '<rect width="64" height="64" fill="' + colour + '" rx="8"/>'
@@ -289,15 +209,15 @@ def _colored_placeholder(name: str) -> str:
289
 
290
  def generate_sprites(image_prompts: dict) -> tuple:
291
  sprite_map = {}
292
- errors = []
293
  for i, (sprite_name, prompt) in enumerate(image_prompts.items()):
294
  if i > 0:
295
  time.sleep(20)
296
  try:
297
  is_bg = "background" in sprite_name
298
- w, h = (CANVAS_W, CANVAS_H) if is_bg else (64, 64)
299
- seed = abs(hash(sprite_name)) % 99999
300
- url = POLLINATIONS_URL.format(prompt=quote(prompt), w=w, h=h, seed=seed)
301
  for attempt in range(3):
302
  try:
303
  response = requests.get(url, timeout=120)
@@ -309,7 +229,7 @@ def generate_sprites(image_prompts: dict) -> tuple:
309
  else:
310
  raise retry_exc
311
  pil_img = Image.open(io.BytesIO(response.content))
312
- size = None if is_bg else (64, 64)
313
  sprite_map[sprite_name] = _pil_to_data_uri(pil_img, size=size)
314
  print(f"[Pollinations] OK: {sprite_name}")
315
  except Exception as exc:
@@ -319,91 +239,70 @@ def generate_sprites(image_prompts: dict) -> tuple:
319
  sprite_map[sprite_name] = _colored_placeholder(sprite_name.replace(".png", ""))
320
  return sprite_map, errors
321
 
322
-
323
  # ---------------------------------------------------------------------------
324
  # Step 3: Inject sprites into HTML
325
  # ---------------------------------------------------------------------------
326
 
327
-
328
  def _inject_sprites(html_code: str, sprite_map: dict) -> str:
329
  for fname, data_uri in sprite_map.items():
330
  html_code = html_code.replace('"' + fname + '"', '"' + data_uri + '"')
331
  html_code = html_code.replace("'" + fname + "'", "'" + data_uri + "'")
332
  return html_code
333
 
334
-
335
  # ---------------------------------------------------------------------------
336
  # Step 4: Generate game code via Groq Llama
337
  # ---------------------------------------------------------------------------
338
- #
339
- # FIX: The CODE_SYSTEM prompt now clearly describes four-direction movement
340
- # for the Platformer case (S/ArrowDown = fast-fall/step-down) instead of
341
- # leaving down-movement unspecified. The top-down section is unchanged.
342
- # ---------------------------------------------------------------------------
343
 
344
  CODE_SYSTEM = (
345
  "You are an expert HTML5 game developer. "
346
  "Write a complete, working, single-file HTML5 game using canvas and vanilla JavaScript. "
347
- "CRITICAL RULES β€” follow every one exactly or the game will be broken and unplayable: "
348
-
349
- "RULE 1 β€” MUTABLE STATE: Declare score, health, wave, frameCount, gameOver, spawnInterval "
350
- "and all game-state arrays with `let`, NEVER `const`. "
351
- "Assigning to a const is a silent no-op: health-- on a const health stays 5 forever. "
352
- "Only use const for objects/images that are never reassigned (playerImg, canvas, ctx, etc.). "
353
-
354
- "RULE 2 β€” SPRITES: At the very top of the script declare: "
355
- "const playerImg = new Image(); playerImg.src = 'sprite_player.png'; "
356
- "const bgImg = new Image(); bgImg.src = 'sprite_background.png'; "
357
- "const enemyImg = new Image(); enemyImg.src = 'sprite_enemy.png'; "
358
- "const keys = new Set(); "
359
- "function loadImg(img) { return new Promise(r => { img.onload = r; if (img.complete) r(); }); } "
360
- "Promise.all([loadImg(playerImg), loadImg(bgImg), loadImg(enemyImg)]).then(startGame); "
361
-
362
- "RULE 3 β€” FUNCTION SCOPE: Define gameLoop() and ALL game logic functions at TOP LEVEL, "
363
- "not inside startGame() or Promise.then(). "
364
-
365
- "RULE 4 β€” DRAW ORDER (every frame, this exact order, no exceptions): "
366
- "(a) ctx.drawImage(bgImg, 0, 0, canvas.width, canvas.height) β€” background first. "
367
- "(b) draw player, enemies, bullets on top of it. "
368
- "(c) draw HUD text last. "
369
- "NEVER call ctx.fillRect to paint a solid colour over the whole canvas β€” it hides everything. "
370
-
371
- "RULE 5 β€” ENEMY OBJECTS must include w and h: "
372
- "enemies.push({ x, y, w: 32, h: 32, speed: ... }); "
373
- "Without w/h the collision function gets undefined and nothing works. "
374
-
375
- "RULE 6 β€” SPAWN TRIGGER: Use frameCount (incremented each frame) to time enemy spawns. "
376
- "NEVER use score % N === 0 β€” this fires on frame 0 (score=0) and floods the screen instantly. "
377
- "Correct pattern: if (frameCount % spawnInterval === 0) spawnEnemy(); "
378
-
379
- "RULE 7 β€” TOP-DOWN SHOOTER MOVEMENT (no gravity, no velY, no grounded): "
380
- "let ddx = 0, ddy = 0; "
381
- "if (keys.has('w') || keys.has('ArrowUp')) ddy -= 1; "
382
- "if (keys.has('s') || keys.has('ArrowDown')) ddy += 1; "
383
- "if (keys.has('a') || keys.has('ArrowLeft')) ddx -= 1; "
384
- "if (keys.has('d') || keys.has('ArrowRight')) ddx += 1; "
385
- "if (ddx !== 0 && ddy !== 0) { const len = Math.sqrt(2); ddx /= len; ddy /= len; } "
386
- "player.x += ddx * player.speed; player.y += ddy * player.speed; "
387
- "player.x = Math.max(0, Math.min(canvas.width - player.w, player.x)); "
388
- "player.y = Math.max(0, Math.min(canvas.height - player.h, player.y)); "
389
-
390
- "RULE 8 β€” TOP-DOWN SHOOTER ENEMY CHASE: "
391
- "const edx = player.x - e.x; const edy = player.y - e.y; "
392
- "const edist = Math.sqrt(edx*edx + edy*edy); "
393
- "if (edist > 0) { e.x += (edx/edist)*e.speed; e.y += (edy/edist)*e.speed; } "
394
-
395
- "RULE 9 β€” COLLISION: use rectangle overlap for both bullet-enemy and enemy-player: "
396
- "function rectsOverlap(a,b){return a.x<b.x+b.w&&a.x+a.w>b.x&&a.y<b.y+b.h&&a.y+a.h>b.y;} "
397
-
398
- "RULE 10 β€” BULLETS: draw as bright yellow circles (ctx.fillStyle='#ffe94d') so they are "
399
- "visible against dark backgrounds. Each bullet needs w and h (e.g. w:10,h:10) for collision. "
400
-
401
- "RULE 11 β€” PLATFORMER (only applies to platformer type, not top-down): "
402
- "use gravity velY += 0.5, grounded checks, jump with W/ArrowUp/Space. "
403
- "S/ArrowDown = fast-fall (!grounded: velY+=3) or step-down (grounded: y+=2 clamped). "
404
- "Always include full-width ground platform at y=420. "
405
-
406
- "Output ONLY the raw HTML β€” no markdown fences, no explanation, nothing else."
407
  )
408
 
409
 
@@ -412,7 +311,7 @@ def generate_game_code(game_type: str, theme: str, temperature: float, max_new_t
412
  return "", "", "Please enter a theme first.", _placeholder_html("Enter a theme and generate a game.")
413
 
414
  try:
415
- client = get_groq_client()
416
  user_prompt = GAME_TYPES[game_type]["prompt_template"].format(theme=theme.strip())
417
 
418
  # Code generation
@@ -427,7 +326,7 @@ def generate_game_code(game_type: str, theme: str, temperature: float, max_new_t
427
  )
428
  code = code_resp.choices[0].message.content.strip()
429
  code = re.sub(r"^```[a-zA-Z]*\n?", "", code).strip()
430
- code = re.sub(r"\n?```$", "", code).strip()
431
  if "<html" not in code.lower() and "<!doctype" not in code.lower():
432
  code = _wrap_in_html(code, theme)
433
 
@@ -467,7 +366,6 @@ def generate_game_code(game_type: str, theme: str, temperature: float, max_new_t
467
  # HTML helpers
468
  # ---------------------------------------------------------------------------
469
 
470
-
471
  def _placeholder_html(message: str) -> str:
472
  safe = message.replace("<", "&lt;").replace(">", "&gt;")
473
  return (
@@ -508,7 +406,6 @@ def launch_game(code: str) -> str:
508
  # UI helpers
509
  # ---------------------------------------------------------------------------
510
 
511
-
512
  def update_type_description(game_type: str) -> str:
513
  return "_" + GAME_TYPES[game_type]["description"] + "_"
514
 
@@ -521,7 +418,6 @@ def get_first_theme(game_type: str) -> str:
521
  # Gradio UI
522
  # ---------------------------------------------------------------------------
523
 
524
-
525
  def build_ui():
526
  with gr.Blocks(title="Game Generator") as demo:
527
 
@@ -534,7 +430,7 @@ def build_ui():
534
 
535
  with gr.Row():
536
 
537
- # ── Left: controls ──────────────────────────────────────────────
538
  with gr.Column(scale=1, min_width=300):
539
 
540
  gr.Markdown("## 1. Configure your game")
@@ -569,9 +465,9 @@ def build_ui():
569
  )
570
 
571
  generate_btn = gr.Button("Generate Game + Sprites", variant="primary")
572
- gen_status = gr.Markdown(value="_No game generated yet._")
573
 
574
- # ── Right: code + game ──────────────────────────────────────────
575
  with gr.Column(scale=2, min_width=500):
576
 
577
  # Collapsible code window
@@ -622,8 +518,7 @@ def build_ui():
622
  "**Pipeline:** Theme β†’ [Groq Llama] game code + [Groq 70B as Z-Image-Engineer] "
623
  "cinematic prompts β†’ [Pollinations/FLUX] sprites β†’ embedded in game. "
624
  f"Game window fixed to {CANVAS_W}Γ—{CANVAS_H}px β€” matches canvas exactly. "
625
- "Edit HTML and click **Launch Game** to hot-reload.\n\n"
626
- "**Platformer controls:** A/← Left Β· D/β†’ Right Β· W/↑/Space Jump Β· S/↓ Fast-fall"
627
  )
628
 
629
  return demo
@@ -633,7 +528,6 @@ def build_ui():
633
  # Entry point
634
  # ---------------------------------------------------------------------------
635
 
636
-
637
  if __name__ == "__main__":
638
  app = build_ui()
639
  app.launch()
 
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
14
  import re
15
  import io
 
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
25
 
 
26
  # ---------------------------------------------------------------------------
27
  # Clients
28
  # ---------------------------------------------------------------------------
29
 
 
30
  GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
31
 
32
+ CODE_MODEL = "llama-3.1-8b-instant" # Groq β€” game code
33
+ PROMPT_MODEL = "llama-3.3-70b-versatile" # Groq β€” creative prompts
 
 
34
 
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
 
56
  # Z-Image-Engineer system prompt (from BennyDaBall/Qwen3-4b-Z-Image-Engineer-V4)
57
  # ---------------------------------------------------------------------------
58
 
 
59
  Z_ENGINEER_SYSTEM = (
60
  "Interpret the user seed as production intent, then build a definitive 200-250 word "
61
  "single-paragraph image prompt that preserves every explicit constraint while intelligently "
 
70
  "environment. Output ONLY the image prompt paragraph. No explanation, no preamble."
71
  )
72
 
 
73
  # ---------------------------------------------------------------------------
74
  # Game type configs β€” Platformer and Top-Down Shooter only
75
  # ---------------------------------------------------------------------------
 
 
 
 
 
 
 
76
 
77
  GAME_TYPES = {
78
  "Platformer": {
 
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'; "
 
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
  },
145
  }
146
 
 
147
  GAME_TYPE_NAMES = list(GAME_TYPES.keys())
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
  # ---------------------------------------------------------------------------
155
  # Step 1: Generate image prompts via Z-Image-Engineer (Groq)
156
  # ---------------------------------------------------------------------------
157
 
 
158
  def generate_image_prompts(theme: str, game_type: str) -> dict:
159
  client = get_groq_client()
160
  seeds = {
 
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()
 
196
 
197
  def _colored_placeholder(name: str) -> str:
198
  colours = ["#e74c3c", "#3498db", "#2ecc71", "#f39c12", "#9b59b6", "#1abc9c"]
199
+ colour = colours[abs(hash(name)) % len(colours)]
200
+ label = name.replace("sprite_", "")[:6]
201
  svg = (
202
  '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">'
203
  '<rect width="64" height="64" fill="' + colour + '" rx="8"/>'
 
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)
 
229
  else:
230
  raise retry_exc
231
  pil_img = Image.open(io.BytesIO(response.content))
232
+ size = None if is_bg else (64, 64)
233
  sprite_map[sprite_name] = _pil_to_data_uri(pil_img, size=size)
234
  print(f"[Pollinations] OK: {sprite_name}")
235
  except Exception as exc:
 
239
  sprite_map[sprite_name] = _colored_placeholder(sprite_name.replace(".png", ""))
240
  return sprite_map, errors
241
 
 
242
  # ---------------------------------------------------------------------------
243
  # Step 3: Inject sprites into HTML
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 + "'")
250
  return html_code
251
 
 
252
  # ---------------------------------------------------------------------------
253
  # Step 4: Generate game code via Groq Llama
254
  # ---------------------------------------------------------------------------
 
 
 
 
 
255
 
256
  CODE_SYSTEM = (
257
  "You are an expert HTML5 game developer. "
258
  "Write a complete, working, single-file HTML5 game using canvas and vanilla JavaScript. "
259
+ "CRITICAL RULES - follow every one exactly: "
260
+ "1. SPRITES: Declare these at the very top of the script BEFORE anything else: "
261
+ " const playerImg = new Image(); playerImg.src = 'sprite_player.png'; "
262
+ " const bgImg = new Image(); bgImg.src = 'sprite_background.png'; "
263
+ " const enemyImg = new Image(); enemyImg.src = 'sprite_enemy.png'; "
264
+ " const keys = new Set(); "
265
+ "2. IMAGE LOADING: After declaring sprites use Promise.all to start the game: "
266
+ " function loadImg(img) { return new Promise(r => { img.onload = r; }); } "
267
+ " Promise.all([loadImg(playerImg), loadImg(bgImg), loadImg(enemyImg)]).then(startGame); "
268
+ "3. startGame function sets up listeners and calls gameLoop: "
269
+ " function startGame() { "
270
+ " window.addEventListener('keydown', e => keys.add(e.key)); "
271
+ " window.addEventListener('keyup', e => keys.delete(e.key)); "
272
+ " canvas.addEventListener('click', onShoot); "
273
+ " requestAnimationFrame(gameLoop); } "
274
+ "4. Define gameLoop() and ALL game logic functions at TOP LEVEL - NOT inside startGame or Promise.then. "
275
+ "5. MOVEMENT RULES DEPEND ON GAME TYPE: "
276
+ " FOR TOP-DOWN SHOOTER - follow these EXACTLY inside gameLoop() every frame: "
277
+ " NO gravity, NO velY += 0.5, NO grounded variable, NO jumping. "
278
+ " STEP A - move player from keys every frame: "
279
+ " if (keys.has('w') || keys.has('ArrowUp')) player.y -= player.speed; "
280
+ " if (keys.has('s') || keys.has('ArrowDown')) player.y += player.speed; "
281
+ " if (keys.has('a') || keys.has('ArrowLeft')) player.x -= player.speed; "
282
+ " if (keys.has('d') || keys.has('ArrowRight')) player.x += player.speed; "
283
+ " player.x = Math.max(0, Math.min(canvas.width-player.w, player.x)); "
284
+ " player.y = Math.max(0, Math.min(canvas.height-player.h, player.y)); "
285
+ " STEP B - move every bullet forward every frame (THIS IS REQUIRED): "
286
+ " bullets.forEach(b => { b.x += b.vx; b.y += b.vy; }); "
287
+ " bullets = bullets.filter(b => b.x>=0 && b.x<=canvas.width && b.y>=0 && b.y<=canvas.height); "
288
+ " STEP C - move enemies toward player every frame: "
289
+ " enemies.forEach(e => { const dx=player.x-e.x; const dy=player.y-e.y; "
290
+ " const d=Math.sqrt(dx*dx+dy*dy); if(d>0){e.x+=dx/d*e.speed; e.y+=dy/d*e.speed;} }); "
291
+ " STEP D - for shooting, add click listener on canvas inside startGame(): "
292
+ " canvas.addEventListener('click', function(e){ "
293
+ " const r=canvas.getBoundingClientRect(); "
294
+ " const mx=e.clientX-r.left; const my=e.clientY-r.top; "
295
+ " const dx=mx-(player.x+player.w/2); const dy=my-(player.y+player.h/2); "
296
+ " const d=Math.sqrt(dx*dx+dy*dy); "
297
+ " if(d>0) bullets.push({x:player.x+player.w/2, y:player.y+player.h/2, vx:dx/d*10, vy:dy/d*10, w:6, h:6}); }); "
298
+ " FOR PLATFORMER: use gravity velY += 0.5, grounded checks, jump with ArrowUp/W/Space (velY=-12). "
299
+ " Set grounded=false BEFORE platform loop. Set grounded=true and velY=0 only on landing. "
300
+ " Always include full-width ground platform at y=420. "
301
+ " FOR PLATFORMER: also declare platformImg and goalImg at top and include them in Promise.all. "
302
+ "6. Draw background FIRST each frame: ctx.drawImage(bgImg, 0, 0, canvas.width, canvas.height). "
303
+ "7. Draw player/enemies with ctx.drawImage(playerImg, x, y, w, h). "
304
+ "8. Always keep player inside canvas boundaries. "
305
+ "Output ONLY the raw HTML - no markdown fences, no explanation, nothing else."
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  )
307
 
308
 
 
311
  return "", "", "Please enter a theme first.", _placeholder_html("Enter a theme and generate a game.")
312
 
313
  try:
314
+ client = get_groq_client()
315
  user_prompt = GAME_TYPES[game_type]["prompt_template"].format(theme=theme.strip())
316
 
317
  # Code generation
 
326
  )
327
  code = code_resp.choices[0].message.content.strip()
328
  code = re.sub(r"^```[a-zA-Z]*\n?", "", code).strip()
329
+ code = re.sub(r"\n?```$", "", code).strip()
330
  if "<html" not in code.lower() and "<!doctype" not in code.lower():
331
  code = _wrap_in_html(code, theme)
332
 
 
366
  # HTML helpers
367
  # ---------------------------------------------------------------------------
368
 
 
369
  def _placeholder_html(message: str) -> str:
370
  safe = message.replace("<", "&lt;").replace(">", "&gt;")
371
  return (
 
406
  # UI helpers
407
  # ---------------------------------------------------------------------------
408
 
 
409
  def update_type_description(game_type: str) -> str:
410
  return "_" + GAME_TYPES[game_type]["description"] + "_"
411
 
 
418
  # Gradio UI
419
  # ---------------------------------------------------------------------------
420
 
 
421
  def build_ui():
422
  with gr.Blocks(title="Game Generator") as demo:
423
 
 
430
 
431
  with gr.Row():
432
 
433
+ # ── Left: controls ───────────────────────────────────────────
434
  with gr.Column(scale=1, min_width=300):
435
 
436
  gr.Markdown("## 1. Configure your game")
 
465
  )
466
 
467
  generate_btn = gr.Button("Generate Game + Sprites", variant="primary")
468
+ gen_status = gr.Markdown(value="_No game generated yet._")
469
 
470
+ # ── Right: code + game ────────────────────────────────────────
471
  with gr.Column(scale=2, min_width=500):
472
 
473
  # Collapsible code window
 
518
  "**Pipeline:** Theme β†’ [Groq Llama] game code + [Groq 70B as Z-Image-Engineer] "
519
  "cinematic prompts β†’ [Pollinations/FLUX] sprites β†’ embedded in game. "
520
  f"Game window fixed to {CANVAS_W}Γ—{CANVAS_H}px β€” matches canvas exactly. "
521
+ "Edit HTML and click **Launch Game** to hot-reload."
 
522
  )
523
 
524
  return demo
 
528
  # Entry point
529
  # ---------------------------------------------------------------------------
530
 
 
531
  if __name__ == "__main__":
532
  app = build_ui()
533
  app.launch()