LeafCat79 commited on
Commit
84ab022
·
verified ·
1 Parent(s): a22d8dd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +292 -255
app.py CHANGED
@@ -1,21 +1,19 @@
1
  """
2
- Text-to-Game Generator - Groq + FLUX Edition
3
- Flow:
4
- 1. User types game idea in chat box
5
- 2. Groq extracts sprite names + image prompts from the idea
6
- 3. FLUX.1-schnell generates each sprite image
7
- 4. Groq generates the full game code using the actual sprite data URIs
8
- 5. Game runs in browser iframe with real sprites embedded
9
 
10
  Secrets needed:
11
- GROQ_API_KEY - from console.groq.com (free)
12
- HF_TOKEN - from huggingface.co/settings/tokens (free)
13
  """
14
 
15
  import os
16
  import re
17
  import io
18
- import json
19
  import base64
20
  import traceback
21
 
@@ -28,28 +26,50 @@ from PIL import Image
28
  # Clients
29
  # ---------------------------------------------------------------------------
30
 
31
- CODE_MODEL = "llama-3.1-8b-instant"
32
- IMAGE_MODEL = "black-forest-labs/FLUX.1-schnell"
33
-
34
  GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
35
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
36
 
 
 
 
 
37
 
38
- def get_groq():
39
  if not GROQ_API_KEY:
40
  raise ValueError(
41
- "GROQ_API_KEY not set.\n"
42
- "Get a free key at console.groq.com then add it as a Space secret."
43
  )
44
- return OpenAI(base_url="https://api.groq.com/openai/v1", api_key=GROQ_API_KEY)
 
 
 
45
 
46
 
47
- def get_flux():
48
  if not HF_TOKEN:
49
  return None
50
  return InferenceClient(provider="nscale", api_key=HF_TOKEN)
51
 
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  # ---------------------------------------------------------------------------
54
  # Game type configs
55
  # ---------------------------------------------------------------------------
@@ -57,47 +77,102 @@ def get_flux():
57
  GAME_TYPES = {
58
  "Platformer": {
59
  "description": "Jump over enemies and obstacles to reach the goal.",
60
- "genre_rules": (
61
- "- Player moves left/right with arrow keys or WASD and jumps with ArrowUp/W/Space.\n"
62
- "- At least 5 platforms including a full-width ground at y=420.\n"
63
- "- 2 moving enemies that patrol platforms.\n"
64
- "- A goal object the player must reach to win.\n"
65
- "- Show score and lives on canvas.\n"
 
 
 
 
 
 
 
 
 
 
 
66
  ),
67
  },
68
  "Top-Down Shooter": {
69
  "description": "Shoot waves of enemies before they reach you.",
70
- "genre_rules": (
71
- "- Player moves with WASD/arrows; shoots with Space or click toward mouse.\n"
72
- "- Enemies spawn from canvas edges in escalating waves.\n"
73
- "- Display health, score, and wave number on canvas.\n"
 
 
 
 
74
  "- Game-over screen with score and restart button.\n"
 
 
 
 
 
 
 
 
75
  ),
76
  },
77
  "Puzzle / Maze": {
78
  "description": "Navigate a maze or solve a tile puzzle to escape.",
79
- "genre_rules": (
 
 
 
 
80
  "- Player navigates with arrow keys or WASD.\n"
81
- "- At least 15x10 tile grid with walls, collectible keys, and a locked exit.\n"
82
- "- Show a move counter or timer on canvas.\n"
83
  "- Win screen when player collects all keys and exits.\n"
 
 
 
 
 
 
84
  ),
85
  },
86
  "Arcade / Dodge": {
87
  "description": "Dodge falling obstacles and survive as long as possible.",
88
- "genre_rules": (
89
- "- Player moves left/right with arrow keys or WASD.\n"
90
- "- Obstacles fall from the top, increasing in speed over time.\n"
91
- "- Show time survived as score.\n"
92
- "- Game-over screen with high score and restart button.\n"
 
 
 
 
 
 
 
 
 
 
93
  ),
94
  },
95
  "Surprise Me!": {
96
- "description": "Let the AI invent the genre.",
97
- "genre_rules": (
98
- "- Pick any fun arcade genre: breakout, snake, flappy-style, space invaders, etc.\n"
 
 
 
 
 
99
  "- Clear win/lose conditions and a score display.\n"
100
- "- Game-over or win screen with a restart button.\n"
 
 
 
 
 
 
 
101
  ),
102
  },
103
  }
@@ -105,7 +180,7 @@ GAME_TYPES = {
105
  GAME_TYPE_NAMES = list(GAME_TYPES.keys())
106
 
107
  THEME_EXAMPLES = {
108
- "Platformer": [["Jungle temple with cursed mummies"], ["Neon cyberpunk rooftops"], ["Underwater pirate shipwreck"]],
109
  "Top-Down Shooter": [["Alien desert invasion"], ["Viking village under siege"], ["Steampunk robot uprising"]],
110
  "Puzzle / Maze": [["Haunted library with secret doors"], ["Ice cave with frozen keys"], ["Egyptian pyramid tiles"]],
111
  "Arcade / Dodge": [["Asteroid field in a tiny rocket"], ["Chef dodging ingredients"], ["Time traveller avoiding paradox storms"]],
@@ -113,58 +188,50 @@ THEME_EXAMPLES = {
113
  }
114
 
115
  # ---------------------------------------------------------------------------
116
- # Step 1 Extract sprite prompts from game idea via Groq
117
  # ---------------------------------------------------------------------------
118
 
119
- def extract_sprite_prompts(game_idea: str, game_type: str) -> list:
120
  """
121
- Ask Groq to decide what 3 sprites the game needs and write
122
- a detailed FLUX image prompt for each one.
123
- Returns a list of dicts: [{name, filename, flux_prompt}, ...]
124
  """
125
- system = (
126
- "You are a game art director. Given a game idea, decide what 3 sprite images "
127
- "are needed and write a detailed image generation prompt for each. "
128
- "Output ONLY valid JSON a list of 3 objects, each with keys: "
129
- "'name' (short label e.g. 'player'), "
130
- "'filename' (e.g. 'sprite_player.png'), "
131
- "'flux_prompt' (detailed prompt for FLUX image model, pixel-art style, 64x64, dark background). "
132
- "No explanation, no markdown, just the JSON array."
133
- )
134
- user = f"Game type: {game_type}\nGame idea: {game_idea}"
135
-
136
- try:
137
- client = get_groq()
138
- resp = client.chat.completions.create(
139
- model=CODE_MODEL,
140
- messages=[
141
- {"role": "system", "content": system},
142
- {"role": "user", "content": user},
143
- ],
144
- max_tokens=500,
145
- temperature=0.5,
146
- )
147
- raw = resp.choices[0].message.content.strip()
148
- raw = re.sub(r"^```[a-zA-Z]*\n?", "", raw).strip()
149
- raw = re.sub(r"\n?```$", "", raw).strip()
150
- sprites = json.loads(raw)
151
- return sprites[:3]
152
- except Exception as exc:
153
- print("[Sprite extractor] Error:", exc)
154
- # Fallback: generic sprites
155
- return [
156
- {"name": "player", "filename": "sprite_player.png", "flux_prompt": f"Pixel-art player sprite for a {game_idea} game, 64x64, dark background"},
157
- {"name": "enemy", "filename": "sprite_enemy.png", "flux_prompt": f"Pixel-art enemy sprite for a {game_idea} game, 64x64, dark background"},
158
- {"name": "background", "filename": "sprite_background.png", "flux_prompt": f"Pixel-art background tile for a {game_idea} game, 64x64, dark background"},
159
- ]
160
-
161
 
162
  # ---------------------------------------------------------------------------
163
- # Step 2 Generate sprites with FLUX
164
  # ---------------------------------------------------------------------------
165
 
166
- def _pil_to_data_uri(pil_image: Image.Image, size: int = 64) -> str:
167
- img = pil_image.resize((size, size), Image.LANCZOS)
 
 
 
168
  buf = io.BytesIO()
169
  img.save(buf, format="PNG")
170
  return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
@@ -173,7 +240,7 @@ def _pil_to_data_uri(pil_image: Image.Image, size: int = 64) -> str:
173
  def _colored_placeholder(name: str) -> str:
174
  colours = ["#e74c3c", "#3498db", "#2ecc71", "#f39c12", "#9b59b6", "#1abc9c"]
175
  colour = colours[abs(hash(name)) % len(colours)]
176
- label = name[:6]
177
  svg = (
178
  '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">'
179
  '<rect width="64" height="64" fill="' + colour + '" rx="8"/>'
@@ -183,155 +250,118 @@ def _colored_placeholder(name: str) -> str:
183
  return "data:image/svg+xml;base64," + base64.b64encode(svg.encode()).decode()
184
 
185
 
186
- def generate_sprite_images(sprite_list: list) -> dict:
187
- """
188
- Given list of {name, filename, flux_prompt}, generate each image.
189
- Returns dict: {filename: data_uri}
190
- """
191
- client = get_flux()
192
- mapping = {}
193
- for sprite in sprite_list:
194
- fname = sprite["filename"]
195
- prompt = sprite["flux_prompt"]
196
- name = sprite["name"]
197
- if client:
198
- try:
199
- pil_img = client.text_to_image(prompt, model=IMAGE_MODEL)
200
- mapping[fname] = _pil_to_data_uri(pil_img, size=64)
201
- print(f"[FLUX] Generated {fname}")
202
- except Exception as exc:
203
- print(f"[FLUX] Failed {fname}: {exc}")
204
- mapping[fname] = _colored_placeholder(name)
205
- else:
206
- mapping[fname] = _colored_placeholder(name)
207
- return mapping
208
-
209
 
210
  # ---------------------------------------------------------------------------
211
- # Step 3 Generate game code with sprites already embedded
212
  # ---------------------------------------------------------------------------
213
 
214
- def build_sprite_css(sprite_map: dict) -> str:
215
- """Build JS that preloads all sprites as Image objects with base64 src."""
216
- lines = ["const sprites = {};"]
217
  for fname, data_uri in sprite_map.items():
218
- var_name = fname.replace(".png", "").replace("-", "_")
219
- lines.append(f'sprites["{fname}"] = new Image();')
220
- lines.append(f'sprites["{fname}"].src = "{data_uri}";')
221
- return "\n".join(lines)
222
-
223
-
224
- def generate_game_code(game_type: str, theme: str, temperature: float,
225
- max_new_tokens: int, sprite_map: dict) -> str:
226
- """Generate HTML game code, injecting sprite data URIs directly into the prompt."""
227
-
228
- genre_rules = GAME_TYPES[game_type]["genre_rules"]
229
-
230
- # Build sprite instruction with actual filenames
231
- sprite_filenames = list(sprite_map.keys())
232
- sprite_instruction = (
233
- "Available sprites (already loaded as Image objects in a 'sprites' object):\n"
234
- + "\n".join(f' sprites["{f}"] — use as: ctx.drawImage(sprites["{f}"], x, y, w, h)' for f in sprite_filenames)
235
- + "\n"
236
- "Use ALL of these sprites to draw game objects. Do NOT use fillRect for objects that have a sprite.\n"
237
- )
238
-
239
- # Build sprite preload JS to inject into the HTML
240
- sprite_preload_js = build_sprite_css(sprite_map)
241
-
242
- system_msg = (
243
- "You are an expert HTML5 game developer. "
244
- "Write a complete, working, single-file HTML5 game using canvas and vanilla JavaScript. "
245
- "CRITICAL RULES — follow every one exactly:\n"
246
- "1. Add keydown/keyup listeners on WINDOW: window.addEventListener('keydown', e => keys.add(e.key)).\n"
247
- "2. Use a Set called 'keys' for input tracking.\n"
248
- "3. Gravity: every frame add 0.5 to velY AFTER position update.\n"
249
- "4. Platform collision: set grounded=false BEFORE the platform loop. "
250
- "Inside the loop set grounded=true and velY=0 when player lands on top.\n"
251
- "5. Jump: if (grounded && (keys.has('ArrowUp')||keys.has('w')||keys.has('W')||keys.has(' '))) { velY=-12; grounded=false; }\n"
252
- "6. Always include a full-width ground platform at y=420.\n"
253
- "7. The sprites are PRE-LOADED — include this exact JS block near the top of your script:\n"
254
- f"```\n{sprite_preload_js}\n```\n"
255
- "8. Use ctx.drawImage(sprites['filename.png'], x, y, w, h) to draw sprites.\n"
256
- "9. Output ONLY the raw HTML — no markdown fences, no explanation."
257
- )
258
-
259
- user_msg = (
260
- f"Create a complete HTML5 {game_type} game with the theme: {theme}.\n"
261
- "Requirements:\n"
262
- "- Single HTML file, all CSS and JS inline.\n"
263
- "- Canvas id='gameCanvas' sized 800x450.\n"
264
- f"{genre_rules}"
265
- "- Use requestAnimationFrame for the game loop.\n"
266
- f"{sprite_instruction}"
267
- "Output ONLY the raw HTML."
268
- )
269
-
270
- client = get_groq()
271
- resp = client.chat.completions.create(
272
- model=CODE_MODEL,
273
- messages=[
274
- {"role": "system", "content": system_msg},
275
- {"role": "user", "content": user_msg},
276
- ],
277
- max_tokens=int(max_new_tokens),
278
- temperature=float(temperature),
279
- )
280
- code = resp.choices[0].message.content.strip()
281
- code = re.sub(r"^```[a-zA-Z]*\n?", "", code).strip()
282
- code = re.sub(r"\n?```$", "", code).strip()
283
- if "<html" not in code.lower() and "<!doctype" not in code.lower():
284
- code = _wrap_in_html(code, theme)
285
- return code
286
-
287
 
288
  # ---------------------------------------------------------------------------
289
- # Main generation pipeline
290
  # ---------------------------------------------------------------------------
291
 
292
- def run_generation(game_type: str, theme: str, temperature: float,
293
- max_new_tokens: int, chat_history: list):
294
- """Full pipeline: extract sprites generate images generate game code."""
295
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  if not theme.strip():
297
- yield chat_history + [["", "⚠️ Please enter a theme first."]], "", _placeholder_html("Enter a theme.")
298
- return
299
 
300
- history = list(chat_history)
301
-
302
- # --- Stage 1: Extract sprite prompts ---
303
- history.append([theme, "🎨 Deciding what sprites your game needs..."])
304
- yield history, "", _placeholder_html("Analysing your game idea...")
305
-
306
- sprites_meta = extract_sprite_prompts(theme, game_type)
307
- sprite_names = [s["filename"] for s in sprites_meta]
308
 
309
- sprite_summary = "**Sprites planned:**\n" + "\n".join(
310
- f"- `{s['filename']}` — {s['name']}: _{s['flux_prompt'][:80]}..._"
311
- for s in sprites_meta
312
- )
313
- history[-1][1] = sprite_summary
314
- yield history, "", _placeholder_html("Generating sprite images with FLUX...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
 
316
- # --- Stage 2: Generate sprite images ---
317
- history.append(["", "🖼️ Generating sprites with FLUX.1-schnell..."])
318
- yield history, "", _placeholder_html("Generating sprites...")
 
319
 
320
- sprite_map = generate_sprite_images(sprites_meta)
321
- n_real = sum(1 for v in sprite_map.values() if "image/png" in v)
322
- history[-1][1] = f"✅ {n_real}/{len(sprites_meta)} sprites generated by FLUX. Now writing game code..."
323
- yield history, "", _placeholder_html("Writing game code with Groq...")
324
 
325
- # --- Stage 3: Generate game code ---
326
- try:
327
- code = generate_game_code(game_type, theme, temperature, max_new_tokens, sprite_map)
328
- history.append(["", f"✅ Game ready! ({len(code.splitlines())} lines of code)"])
329
- yield history, code, _build_preview(code)
330
  except Exception as exc:
331
  traceback.print_exc()
332
- err = f"❌ Error generating code: {exc}"
333
- history.append(["", err])
334
- yield history, "", _placeholder_html(err)
 
 
 
 
 
335
 
336
 
337
  # ---------------------------------------------------------------------------
@@ -378,7 +408,7 @@ def launch_game(code: str) -> str:
378
  # UI helpers
379
  # ---------------------------------------------------------------------------
380
 
381
- def update_description(game_type: str) -> str:
382
  return "_" + GAME_TYPES[game_type]["description"] + "_"
383
 
384
 
@@ -394,10 +424,11 @@ def build_ui():
394
  with gr.Blocks(title="Game Generator") as demo:
395
 
396
  gr.Markdown(
397
- "# 🎮 Game Generator\n"
398
- "Type your game idea below. The AI will plan the sprites, "
399
- "generate them with **FLUX.1-schnell**, then write the full game with **Llama-3.1-8B**.\n\n"
400
- "> Needs `GROQ_API_KEY` and `HF_TOKEN` as Space secrets."
 
401
  )
402
 
403
  with gr.Row():
@@ -405,16 +436,16 @@ def build_ui():
405
  # ── Left: controls ───────────────────────────────────────────
406
  with gr.Column(scale=1, min_width=300):
407
 
408
- gr.Markdown("## 1. Configure")
409
 
410
- game_type_dd = gr.Dropdown(
411
  choices=GAME_TYPE_NAMES, value="Platformer", label="Game genre",
412
  )
413
- type_desc = gr.Markdown(
414
- value="_" + GAME_TYPES["Platformer"]["description"] + "_"
415
  )
416
  theme_box = gr.Textbox(
417
- label="Game theme / idea",
418
  placeholder="e.g. Ancient Egyptian pyramid with cursed mummies",
419
  lines=3,
420
  value=THEME_EXAMPLES["Platformer"][0][0],
@@ -425,40 +456,38 @@ def build_ui():
425
  label="Theme examples",
426
  )
427
 
428
- gr.Markdown("## 2. Settings")
429
 
430
- temp_slider = gr.Slider(
431
  minimum=0.3, maximum=1.2, value=0.7, step=0.05,
432
- label="Temperature",
433
  )
434
- tokens_slider = gr.Slider(
435
  minimum=1000, maximum=6000, value=4000, step=500,
436
- label="Max tokens",
437
  )
438
 
439
- generate_btn = gr.Button("🚀 Generate Game", variant="primary")
 
440
 
441
- # ── Right: chat + code + game ─────────────────────────────────
442
- with gr.Column(scale=2, min_width=500):
443
-
444
- gr.Markdown("## 3. Generation log")
445
-
446
- chatbox = gr.Chatbot(
447
- label="Sprite & code pipeline",
448
- height=180,
449
- show_label=False,
450
  )
451
 
452
- gr.Markdown("## 4. Generated code _(editable)_")
 
 
 
453
 
454
  code_box = gr.Code(
455
- label="HTML source",
456
  language="html",
457
- lines=10,
458
  interactive=True,
459
  )
460
 
461
- launch_btn = gr.Button("Launch Game", variant="secondary")
462
 
463
  gr.Markdown("## 5. Live game window")
464
 
@@ -468,22 +497,30 @@ def build_ui():
468
 
469
  # ── Wiring ────────────────────────────────────────────────────────
470
 
471
- game_type_dd.change(fn=update_description, inputs=[game_type_dd], outputs=[type_desc])
472
- game_type_dd.change(fn=get_first_theme, inputs=[game_type_dd], outputs=[theme_box])
 
 
 
 
 
 
 
 
473
 
474
  generate_btn.click(
475
- fn=run_generation,
476
- inputs=[game_type_dd, theme_box, temp_slider, tokens_slider, chatbox],
477
- outputs=[chatbox, code_box, game_frame],
478
  )
479
 
480
  launch_btn.click(fn=launch_game, inputs=[code_box], outputs=[game_frame])
481
 
482
  gr.Markdown(
483
  "---\n"
484
- "**Pipeline:** Theme → Groq plans sprites FLUX generates images "
485
- "Groq writes game code with sprites pre-embedded as base64 "
486
- "Game runs in browser iframe. Edit HTML and click **Launch Game** to hot-reload."
487
  )
488
 
489
  return demo
 
1
  """
2
+ 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
15
  import re
16
  import io
 
17
  import base64
18
  import traceback
19
 
 
26
  # Clients
27
  # ---------------------------------------------------------------------------
28
 
 
 
 
29
  GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
30
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
31
 
32
+ CODE_MODEL = "llama-3.1-8b-instant" # Groq — game code
33
+ PROMPT_MODEL = "llama-3.3-70b-versatile" # Groq — better model for creative prompts
34
+ IMAGE_MODEL = "black-forest-labs/FLUX.1-schnell" # nscale
35
+
36
 
37
+ def get_groq_client():
38
  if not GROQ_API_KEY:
39
  raise ValueError(
40
+ "GROQ_API_KEY not set. "
41
+ "Get a free key at console.groq.com and add it as a Space secret."
42
  )
43
+ return OpenAI(
44
+ base_url="https://api.groq.com/openai/v1",
45
+ api_key=GROQ_API_KEY,
46
+ )
47
 
48
 
49
+ def get_image_client():
50
  if not HF_TOKEN:
51
  return None
52
  return InferenceClient(provider="nscale", api_key=HF_TOKEN)
53
 
54
 
55
+ # ---------------------------------------------------------------------------
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 "
62
+ "expanding missing details. First infer the core subject, action, setting, and emotional tone; "
63
+ "treat these as non-negotiable anchors. Then enhance with precise visual staging "
64
+ "(explicit foreground, midground, background), clear visual hierarchy and eye path, "
65
+ "physically plausible lighting (source, direction, softness, color temperature), and optical "
66
+ "strategy (if lens/aperture are provided, preserve exactly; if absent, choose fitting lens and "
67
+ "aperture and imply their depth-of-field effect). Integrate organic, manufactured, and "
68
+ "environmental textures with realistic material behavior, add motion/atmospheric cues only "
69
+ "when they support the scene, and apply a coherent color grade consistent with mood and "
70
+ "environment. Output ONLY the image prompt paragraph. No explanation, no preamble."
71
+ )
72
+
73
  # ---------------------------------------------------------------------------
74
  # Game type configs
75
  # ---------------------------------------------------------------------------
 
77
  GAME_TYPES = {
78
  "Platformer": {
79
  "description": "Jump over enemies and obstacles to reach the goal.",
80
+ "prompt_template": (
81
+ "Create a complete, self-contained HTML5 platformer game with the theme: {theme}.\n"
82
+ "Requirements:\n"
83
+ "- Single HTML file with all CSS and JavaScript inline.\n"
84
+ "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
85
+ "- Player moves left/right with arrow keys or WASD and jumps.\n"
86
+ "- At least 5 platforms, 2 moving enemies, and a goal to reach.\n"
87
+ "- Show score/lives on the canvas.\n"
88
+ "- Use requestAnimationFrame for the game loop.\n"
89
+ "- Use new Image() with src='sprite_player.png' for the player character.\n"
90
+ "- Use new Image() with src='sprite_background.png' for the background.\n"
91
+ "- Use new Image() with src='sprite_enemy.png' for enemies.\n"
92
+ "- Draw images with ctx.drawImage(img, x, y, w, h).\n"
93
+ "- Draw platforms and UI with canvas shapes.\n"
94
+ "- NO external libraries, NO CDN links.\n"
95
+ "- Theme colors and labels to match: {theme}.\n"
96
+ "Output ONLY the raw HTML. No explanation, no markdown fences."
97
  ),
98
  },
99
  "Top-Down Shooter": {
100
  "description": "Shoot waves of enemies before they reach you.",
101
+ "prompt_template": (
102
+ "Create a complete, self-contained HTML5 top-down shooter game with the theme: {theme}.\n"
103
+ "Requirements:\n"
104
+ "- Single HTML file with all CSS and JavaScript inline.\n"
105
+ "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
106
+ "- Player moves with WASD/arrows; shoots with Space or click.\n"
107
+ "- Enemies spawn from edges in escalating waves.\n"
108
+ "- Display health, score, and wave on canvas.\n"
109
  "- Game-over screen with score and restart button.\n"
110
+ "- Use requestAnimationFrame for the game loop.\n"
111
+ "- Use new Image() with src='sprite_player.png' for the player.\n"
112
+ "- Use new Image() with src='sprite_background.png' for the background.\n"
113
+ "- Use new Image() with src='sprite_enemy.png' for enemies.\n"
114
+ "- Draw images with ctx.drawImage(img, x, y, w, h).\n"
115
+ "- NO external libraries, NO CDN links.\n"
116
+ "- Theme colors and labels to match: {theme}.\n"
117
+ "Output ONLY the raw HTML. No explanation, no markdown fences."
118
  ),
119
  },
120
  "Puzzle / Maze": {
121
  "description": "Navigate a maze or solve a tile puzzle to escape.",
122
+ "prompt_template": (
123
+ "Create a complete, self-contained HTML5 maze/tile-puzzle game with the theme: {theme}.\n"
124
+ "Requirements:\n"
125
+ "- Single HTML file with all CSS and JavaScript inline.\n"
126
+ "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
127
  "- Player navigates with arrow keys or WASD.\n"
128
+ "- At least 15x10 tile grid, collectible keys, and a locked exit.\n"
129
+ "- Show a move counter or timer.\n"
130
  "- Win screen when player collects all keys and exits.\n"
131
+ "- Use new Image() with src='sprite_player.png' for the player.\n"
132
+ "- Use new Image() with src='sprite_background.png' for the background.\n"
133
+ "- Draw tiles and UI with canvas shapes.\n"
134
+ "- NO external libraries, NO CDN links.\n"
135
+ "- Theme colors and labels to match: {theme}.\n"
136
+ "Output ONLY the raw HTML. No explanation, no markdown fences."
137
  ),
138
  },
139
  "Arcade / Dodge": {
140
  "description": "Dodge falling obstacles and survive as long as possible.",
141
+ "prompt_template": (
142
+ "Create a complete, self-contained HTML5 arcade dodge game with the theme: {theme}.\n"
143
+ "Requirements:\n"
144
+ "- Single HTML file with all CSS and JavaScript inline.\n"
145
+ "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
146
+ "- Player moves with arrow keys or WASD; obstacles increase in speed over time.\n"
147
+ "- Collision ends the game; show time survived as score.\n"
148
+ "- High score stored in a JS variable; restart button on game-over screen.\n"
149
+ "- Use requestAnimationFrame for the game loop.\n"
150
+ "- Use new Image() with src='sprite_player.png' for the player.\n"
151
+ "- Use new Image() with src='sprite_background.png' for the background.\n"
152
+ "- Draw obstacles and UI with canvas shapes.\n"
153
+ "- NO external libraries, NO CDN links.\n"
154
+ "- Theme colors and labels to match: {theme}.\n"
155
+ "Output ONLY the raw HTML. No explanation, no markdown fences."
156
  ),
157
  },
158
  "Surprise Me!": {
159
+ "description": "Let the AI invent the genre - could be anything!",
160
+ "prompt_template": (
161
+ "Create a complete, self-contained HTML5 browser game with the theme: {theme}.\n"
162
+ "Pick any fun arcade genre: breakout, snake, flappy-style, space invaders, etc.\n"
163
+ "Requirements:\n"
164
+ "- Single HTML file with all CSS and JavaScript inline.\n"
165
+ "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
166
+ "- Keyboard or mouse controlled.\n"
167
  "- Clear win/lose conditions and a score display.\n"
168
+ "- Game-over / win screen with a restart button.\n"
169
+ "- Use requestAnimationFrame for the game loop.\n"
170
+ "- Use new Image() with src='sprite_player.png' for the player.\n"
171
+ "- Use new Image() with src='sprite_background.png' for the background.\n"
172
+ "- Draw other elements with canvas shapes.\n"
173
+ "- NO external libraries, NO CDN links.\n"
174
+ "- Theme colors and labels vividly to match: {theme}.\n"
175
+ "Output ONLY the raw HTML. No explanation, no markdown fences."
176
  ),
177
  },
178
  }
 
180
  GAME_TYPE_NAMES = list(GAME_TYPES.keys())
181
 
182
  THEME_EXAMPLES = {
183
+ "Platformer": [["Jungle temple with ancient traps"], ["Neon cyberpunk rooftops"], ["Underwater pirate shipwreck"]],
184
  "Top-Down Shooter": [["Alien desert invasion"], ["Viking village under siege"], ["Steampunk robot uprising"]],
185
  "Puzzle / Maze": [["Haunted library with secret doors"], ["Ice cave with frozen keys"], ["Egyptian pyramid tiles"]],
186
  "Arcade / Dodge": [["Asteroid field in a tiny rocket"], ["Chef dodging ingredients"], ["Time traveller avoiding paradox storms"]],
 
188
  }
189
 
190
  # ---------------------------------------------------------------------------
191
+ # Step 1: Generate image prompts via Z-Image-Engineer (Groq)
192
  # ---------------------------------------------------------------------------
193
 
194
+ def generate_image_prompts(theme: str, game_type: str) -> dict:
195
  """
196
+ Use Groq with Z-Image-Engineer system prompt to generate
197
+ cinematic image prompts for player and background sprites.
198
+ Returns dict: { 'sprite_player.png': prompt, 'sprite_background.png': prompt, 'sprite_enemy.png': prompt }
199
  """
200
+ client = get_groq_client()
201
+
202
+ seeds = {
203
+ "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",
204
+ "sprite_background.png": f"2D game background scene for a {theme} themed {game_type} game, wide landscape, atmospheric, game art style, 800x450",
205
+ "sprite_enemy.png": f"pixel-art enemy character for a {theme} themed {game_type} game, menacing, clear silhouette, 64x64 pixel style",
206
+ }
207
+
208
+ prompts = {}
209
+ for sprite_name, seed in seeds.items():
210
+ try:
211
+ response = client.chat.completions.create(
212
+ model=PROMPT_MODEL,
213
+ messages=[
214
+ {"role": "system", "content": Z_ENGINEER_SYSTEM},
215
+ {"role": "user", "content": seed},
216
+ ],
217
+ max_tokens=400,
218
+ temperature=0.8,
219
+ )
220
+ prompts[sprite_name] = response.choices[0].message.content.strip()
221
+ except Exception as exc:
222
+ print(f"[Z-Engineer] Failed {sprite_name}: {exc}")
223
+ prompts[sprite_name] = seed # fallback to raw seed
224
+ return prompts
 
 
 
 
 
 
 
 
 
 
 
225
 
226
  # ---------------------------------------------------------------------------
227
+ # Step 2: Generate images via FLUX.1-schnell
228
  # ---------------------------------------------------------------------------
229
 
230
+ def _pil_to_data_uri(pil_image: Image.Image, size: tuple = None) -> str:
231
+ if size:
232
+ img = pil_image.resize(size, Image.LANCZOS)
233
+ else:
234
+ img = pil_image
235
  buf = io.BytesIO()
236
  img.save(buf, format="PNG")
237
  return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
 
240
  def _colored_placeholder(name: str) -> str:
241
  colours = ["#e74c3c", "#3498db", "#2ecc71", "#f39c12", "#9b59b6", "#1abc9c"]
242
  colour = colours[abs(hash(name)) % len(colours)]
243
+ label = name.replace("sprite_", "")[:6]
244
  svg = (
245
  '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">'
246
  '<rect width="64" height="64" fill="' + colour + '" rx="8"/>'
 
250
  return "data:image/svg+xml;base64," + base64.b64encode(svg.encode()).decode()
251
 
252
 
253
+ def generate_sprites(image_prompts: dict) -> dict:
254
+ """Generate actual images from enhanced prompts via FLUX.1-schnell."""
255
+ image_client = get_image_client()
256
+ sprite_map = {}
257
+
258
+ for sprite_name, prompt in image_prompts.items():
259
+ if not image_client:
260
+ sprite_map[sprite_name] = _colored_placeholder(sprite_name.replace(".png", ""))
261
+ continue
262
+ try:
263
+ # Background gets full size, sprites get 64x64
264
+ is_bg = "background" in sprite_name
265
+ pil_img = image_client.text_to_image(prompt, model=IMAGE_MODEL)
266
+ size = None if is_bg else (64, 64)
267
+ sprite_map[sprite_name] = _pil_to_data_uri(pil_img, size=size)
268
+ print(f"[FLUX] Generated {sprite_name}")
269
+ except Exception as exc:
270
+ print(f"[FLUX] Failed {sprite_name}: {exc}")
271
+ sprite_map[sprite_name] = _colored_placeholder(sprite_name.replace(".png", ""))
272
+
273
+ return sprite_map
 
 
274
 
275
  # ---------------------------------------------------------------------------
276
+ # Step 3: Inject sprites into HTML
277
  # ---------------------------------------------------------------------------
278
 
279
+ def _inject_sprites(html_code: str, sprite_map: dict) -> str:
 
 
280
  for fname, data_uri in sprite_map.items():
281
+ html_code = html_code.replace(f'"{fname}"', f'"{data_uri}"')
282
+ html_code = html_code.replace(f"'{fname}'", f"'{data_uri}'")
283
+ return html_code
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
 
285
  # ---------------------------------------------------------------------------
286
+ # Step 4: Generate game code via Groq Llama
287
  # ---------------------------------------------------------------------------
288
 
289
+ CODE_SYSTEM = (
290
+ "You are an expert HTML5 game developer. "
291
+ "Write a complete, working, single-file HTML5 game using canvas and vanilla JavaScript. "
292
+ "CRITICAL RULES - follow every one exactly: "
293
+ "1. Add keydown/keyup listeners on WINDOW: window.addEventListener('keydown', e => keys.add(e.key)). "
294
+ "2. Gravity: every frame do player.velY += 0.5 AFTER moving the player. "
295
+ "3. Platform collision: set grounded=false BEFORE the platform loop. "
296
+ " Inside the loop, if player lands on a platform set grounded=true and velY=0. "
297
+ "4. Jump: if (grounded && (keys.has('ArrowUp') || keys.has('w') || keys.has('W') || keys.has(' '))) { velY=-12; grounded=false; } "
298
+ "5. Always include a solid ground platform spanning the full canvas width at y=420. "
299
+ "6. Load sprites: const playerImg = new Image(); playerImg.src = 'sprite_player.png'; "
300
+ " const bgImg = new Image(); bgImg.src = 'sprite_background.png'; "
301
+ " const enemyImg = new Image(); enemyImg.src = 'sprite_enemy.png'; "
302
+ "7. Draw background: ctx.drawImage(bgImg, 0, 0, canvas.width, canvas.height); "
303
+ "8. Draw player/enemies using ctx.drawImage(img, x, y, w, h). "
304
+ "Output ONLY the raw HTML - no markdown fences, no explanation, nothing else."
305
+ )
306
+
307
+
308
+ def generate_game_code(game_type: str, theme: str, temperature: float, max_new_tokens: int):
309
  if not theme.strip():
310
+ return "", "", "Please enter a theme first.", _placeholder_html("Enter a theme and generate a game.")
 
311
 
312
+ try:
313
+ client = get_groq_client()
314
+ user_prompt = GAME_TYPES[game_type]["prompt_template"].format(theme=theme.strip())
 
 
 
 
 
315
 
316
+ # -- Code generation (Llama) --
317
+ code_resp = client.chat.completions.create(
318
+ model=CODE_MODEL,
319
+ messages=[
320
+ {"role": "system", "content": CODE_SYSTEM},
321
+ {"role": "user", "content": user_prompt},
322
+ ],
323
+ max_tokens=int(max_new_tokens),
324
+ temperature=float(temperature),
325
+ )
326
+ code = code_resp.choices[0].message.content.strip()
327
+ code = re.sub(r"^```[a-zA-Z]*\n?", "", code).strip()
328
+ code = re.sub(r"\n?```$", "", code).strip()
329
+ if "<html" not in code.lower() and "<!doctype" not in code.lower():
330
+ code = _wrap_in_html(code, theme)
331
+
332
+ # -- Image prompt generation (Z-Image-Engineer via Groq) --
333
+ image_prompts = generate_image_prompts(theme.strip(), game_type)
334
+
335
+ # -- Sprite generation (FLUX.1-schnell) --
336
+ sprite_map = generate_sprites(image_prompts)
337
+
338
+ # -- Inject sprites --
339
+ final_code = _inject_sprites(code, sprite_map)
340
+
341
+ n_real = sum(1 for v in sprite_map.values() if "image/png" in v)
342
+ n_fallback = len(sprite_map) - n_real
343
+ status = (
344
+ f"Done! {n_real} sprite(s) generated by FLUX, "
345
+ f"{n_fallback} fallback. Click Launch Game to play."
346
+ )
347
 
348
+ # Show the enhanced prompts that were used
349
+ prompt_summary = "\n\n".join(
350
+ f"**{k}:**\n{v}" for k, v in image_prompts.items()
351
+ )
352
 
353
+ return final_code, prompt_summary, status, _build_preview(final_code)
 
 
 
354
 
 
 
 
 
 
355
  except Exception as exc:
356
  traceback.print_exc()
357
+ err = str(exc)
358
+ if "401" in err or "api_key" in err.lower():
359
+ err = "Invalid GROQ_API_KEY. Check your key at console.groq.com."
360
+ elif "429" in err or "rate" in err.lower():
361
+ err = "Rate limited by Groq - wait a few seconds and try again."
362
+ else:
363
+ err = "Error: " + str(exc)
364
+ return "", "", err, _placeholder_html(err)
365
 
366
 
367
  # ---------------------------------------------------------------------------
 
408
  # UI helpers
409
  # ---------------------------------------------------------------------------
410
 
411
+ def update_type_description(game_type: str) -> str:
412
  return "_" + GAME_TYPES[game_type]["description"] + "_"
413
 
414
 
 
424
  with gr.Blocks(title="Game Generator") as demo:
425
 
426
  gr.Markdown(
427
+ "# Game Generator\n"
428
+ "Type a theme the AI writes the game code, generates cinematic image prompts "
429
+ "using **Z-Image-Engineer V4** style, then **FLUX.1-schnell** renders the sprites.\n\n"
430
+ "> Secrets needed: `GROQ_API_KEY` (console.groq.com, free) "
431
+ "and `HF_TOKEN` (huggingface.co/settings/tokens, for FLUX images)."
432
  )
433
 
434
  with gr.Row():
 
436
  # ── Left: controls ───────────────────────────────────────────
437
  with gr.Column(scale=1, min_width=300):
438
 
439
+ gr.Markdown("## 1. Configure your game")
440
 
441
+ game_type_dropdown = gr.Dropdown(
442
  choices=GAME_TYPE_NAMES, value="Platformer", label="Game genre",
443
  )
444
+ type_description = gr.Markdown(
445
+ value="_" + GAME_TYPES["Platformer"]["description"] + "_",
446
  )
447
  theme_box = gr.Textbox(
448
+ label="Theme / setting",
449
  placeholder="e.g. Ancient Egyptian pyramid with cursed mummies",
450
  lines=3,
451
  value=THEME_EXAMPLES["Platformer"][0][0],
 
456
  label="Theme examples",
457
  )
458
 
459
+ gr.Markdown("## 2. Generation settings")
460
 
461
+ temperature_slider = gr.Slider(
462
  minimum=0.3, maximum=1.2, value=0.7, step=0.05,
463
+ label="Temperature - higher = more creative",
464
  )
465
+ max_tokens_slider = gr.Slider(
466
  minimum=1000, maximum=6000, value=4000, step=500,
467
+ label="Max tokens - more = longer game",
468
  )
469
 
470
+ generate_btn = gr.Button("Generate Game + Sprites", variant="primary")
471
+ gen_status = gr.Markdown(value="_No game generated yet._")
472
 
473
+ gr.Markdown("## 3. Generated image prompts")
474
+ prompt_display = gr.Markdown(
475
+ value="_Image prompts will appear here after generation._"
 
 
 
 
 
 
476
  )
477
 
478
+ # ── Right: code + game ────────────────────────────────────────
479
+ with gr.Column(scale=2, min_width=500):
480
+
481
+ gr.Markdown("## 4. Generated code (editable)")
482
 
483
  code_box = gr.Code(
484
+ label="HTML source (sprites embedded as base64)",
485
  language="html",
486
+ lines=12,
487
  interactive=True,
488
  )
489
 
490
+ launch_btn = gr.Button("Launch Game", variant="secondary")
491
 
492
  gr.Markdown("## 5. Live game window")
493
 
 
497
 
498
  # ── Wiring ────────────────────────────────────────────────────────
499
 
500
+ game_type_dropdown.change(
501
+ fn=update_type_description,
502
+ inputs=[game_type_dropdown],
503
+ outputs=[type_description],
504
+ )
505
+ game_type_dropdown.change(
506
+ fn=get_first_theme,
507
+ inputs=[game_type_dropdown],
508
+ outputs=[theme_box],
509
+ )
510
 
511
  generate_btn.click(
512
+ fn=generate_game_code,
513
+ inputs=[game_type_dropdown, theme_box, temperature_slider, max_tokens_slider],
514
+ outputs=[code_box, prompt_display, gen_status, game_frame],
515
  )
516
 
517
  launch_btn.click(fn=launch_game, inputs=[code_box], outputs=[game_frame])
518
 
519
  gr.Markdown(
520
  "---\n"
521
+ "**Pipeline:** Theme → [Groq Llama] game code + [Groq 70B as Z-Image-Engineer] "
522
+ "cinematic prompts [FLUX.1-schnell/nscale] sprites embedded in game. "
523
+ "Edit the HTML and click **Launch Game** to hot-reload."
524
  )
525
 
526
  return demo