LeafCat79 commited on
Commit
a22d8dd
ยท
verified ยท
1 Parent(s): a3feb0f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +270 -240
app.py CHANGED
@@ -1,14 +1,21 @@
1
  """
2
  Text-to-Game Generator - Groq + FLUX Edition
3
- - Code : llama-3.1-8b-instant via Groq API (free, no credit card)
4
- - Images: black-forest-labs/FLUX.1-schnell via nscale (HF Inference API, free with HF_TOKEN)
5
-
6
- Setup: add GROQ_API_KEY and HF_TOKEN as Space secrets.
 
 
 
 
 
 
7
  """
8
 
9
  import os
10
  import re
11
  import io
 
12
  import base64
13
  import traceback
14
 
@@ -18,136 +25,79 @@ from huggingface_hub import InferenceClient
18
  from PIL import Image
19
 
20
  # ---------------------------------------------------------------------------
21
- # Google AI Studio client (OpenAI-compatible endpoint)
22
  # ---------------------------------------------------------------------------
23
 
24
- CODE_MODEL = "llama-3.1-8b-instant"
25
- GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
 
 
 
26
 
27
 
28
- def _check_key():
29
  if not GROQ_API_KEY:
30
  raise ValueError(
31
- "GROQ_API_KEY is not set.\n"
32
- "1. Go to console.groq.com and sign in\n"
33
- "2. Click API Keys > Create API Key\n"
34
- "3. Copy the key (starts with gsk_)\n"
35
- "4. Add it as a Space secret: Settings > Variables and secrets > Name: GROQ_API_KEY"
36
  )
 
37
 
38
 
39
- def get_client():
40
- _check_key()
41
- return OpenAI(
42
- base_url="https://api.groq.com/openai/v1",
43
- api_key=GROQ_API_KEY,
44
- )
45
-
46
-
47
- # ---------------------------------------------------------------------------
48
- # Image model โ€” FLUX.1-schnell via nscale (free with HF_TOKEN)
49
- # ---------------------------------------------------------------------------
50
-
51
- IMAGE_MODEL = "black-forest-labs/FLUX.1-schnell"
52
- HF_TOKEN = os.environ.get("HF_TOKEN", "")
53
-
54
-
55
- def get_image_client():
56
  if not HF_TOKEN:
57
  return None
58
  return InferenceClient(provider="nscale", api_key=HF_TOKEN)
59
 
60
 
61
  # ---------------------------------------------------------------------------
62
- # Game type configs โ€” uses sprite_NAME.png when HF_TOKEN set, else canvas
63
  # ---------------------------------------------------------------------------
64
 
65
  GAME_TYPES = {
66
  "Platformer": {
67
  "description": "Jump over enemies and obstacles to reach the goal.",
68
- "prompt_template": (
69
- "Create a complete, self-contained HTML5 platformer game with the theme: {theme}.\n"
70
- "Requirements:\n"
71
- "- Single HTML file with all CSS and JavaScript inline.\n"
72
- "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
73
- "- Player moves left/right with arrow keys or WASD and jumps.\n"
74
- "- At least 5 platforms, 2 moving enemies, and a goal to reach.\n"
75
- "- Show score/lives on the canvas.\n"
76
- "- Use requestAnimationFrame for the game loop.\n"
77
- "- Draw ALL graphics using canvas 2D shapes and colors (fillRect, arc, fillStyle, etc).\n"
78
- "- Make colors, shapes and labels vividly match the theme: {theme}.\n"
79
- "- NO image files, NO external libraries, NO CDN links.\n"
80
- "Output ONLY the raw HTML. No explanation, no markdown fences."
81
  ),
82
  },
83
  "Top-Down Shooter": {
84
  "description": "Shoot waves of enemies before they reach you.",
85
- "prompt_template": (
86
- "Create a complete, self-contained HTML5 top-down shooter game with the theme: {theme}.\n"
87
- "Requirements:\n"
88
- "- Single HTML file with all CSS and JavaScript inline.\n"
89
- "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
90
- "- Player moves with WASD/arrows; shoots with Space or click.\n"
91
- "- Enemies spawn from edges in escalating waves.\n"
92
- "- Display health, score, and wave on canvas.\n"
93
  "- Game-over screen with score and restart button.\n"
94
- "- Use requestAnimationFrame for the game loop.\n"
95
- "- Draw ALL graphics using canvas 2D shapes and colors only.\n"
96
- "- Make colors, shapes and labels vividly match the theme: {theme}.\n"
97
- "- NO image files, NO external libraries, NO CDN links.\n"
98
- "Output ONLY the raw HTML. No explanation, no markdown fences."
99
  ),
100
  },
101
  "Puzzle / Maze": {
102
  "description": "Navigate a maze or solve a tile puzzle to escape.",
103
- "prompt_template": (
104
- "Create a complete, self-contained HTML5 maze/tile-puzzle game with the theme: {theme}.\n"
105
- "Requirements:\n"
106
- "- Single HTML file with all CSS and JavaScript inline.\n"
107
- "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
108
  "- Player navigates with arrow keys or WASD.\n"
109
- "- At least 15x10 tile grid, collectible keys, and a locked exit.\n"
110
- "- Show a move counter or timer.\n"
111
  "- Win screen when player collects all keys and exits.\n"
112
- "- Draw ALL graphics using canvas 2D shapes and colors only.\n"
113
- "- Make colors, shapes and labels vividly match the theme: {theme}.\n"
114
- "- NO image files, NO external libraries, NO CDN links.\n"
115
- "Output ONLY the raw HTML. No explanation, no markdown fences."
116
  ),
117
  },
118
  "Arcade / Dodge": {
119
  "description": "Dodge falling obstacles and survive as long as possible.",
120
- "prompt_template": (
121
- "Create a complete, self-contained HTML5 arcade dodge game with the theme: {theme}.\n"
122
- "Requirements:\n"
123
- "- Single HTML file with all CSS and JavaScript inline.\n"
124
- "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
125
- "- Player moves with arrow keys or WASD; obstacles increase in speed over time.\n"
126
- "- Collision ends the game; show time survived as score.\n"
127
- "- High score stored in a JS variable; restart button on game-over screen.\n"
128
- "- Use requestAnimationFrame for the game loop.\n"
129
- "- Draw ALL graphics using canvas 2D shapes and colors only.\n"
130
- "- Make colors, shapes and labels vividly match the theme: {theme}.\n"
131
- "- NO image files, NO external libraries, NO CDN links.\n"
132
- "Output ONLY the raw HTML. No explanation, no markdown fences."
133
  ),
134
  },
135
  "Surprise Me!": {
136
- "description": "Let the AI invent the genre - could be anything!",
137
- "prompt_template": (
138
- "Create a complete, self-contained HTML5 browser game with the theme: {theme}.\n"
139
- "Pick any fun arcade genre: breakout, snake, flappy-style, space invaders, etc.\n"
140
- "Requirements:\n"
141
- "- Single HTML file with all CSS and JavaScript inline.\n"
142
- "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
143
- "- Keyboard or mouse controlled.\n"
144
  "- Clear win/lose conditions and a score display.\n"
145
- "- Game-over / win screen with a restart button.\n"
146
- "- Use requestAnimationFrame for the game loop.\n"
147
- "- Draw ALL graphics using canvas 2D shapes and colors only.\n"
148
- "- Make colors, shapes and labels vividly match the theme: {theme}.\n"
149
- "- NO image files, NO external libraries, NO CDN links.\n"
150
- "Output ONLY the raw HTML. No explanation, no markdown fences."
151
  ),
152
  },
153
  }
@@ -155,7 +105,7 @@ GAME_TYPES = {
155
  GAME_TYPE_NAMES = list(GAME_TYPES.keys())
156
 
157
  THEME_EXAMPLES = {
158
- "Platformer": [["Jungle temple with ancient traps"], ["Neon cyberpunk rooftops"], ["Underwater pirate shipwreck"]],
159
  "Top-Down Shooter": [["Alien desert invasion"], ["Viking village under siege"], ["Steampunk robot uprising"]],
160
  "Puzzle / Maze": [["Haunted library with secret doors"], ["Ice cave with frozen keys"], ["Egyptian pyramid tiles"]],
161
  "Arcade / Dodge": [["Asteroid field in a tiny rocket"], ["Chef dodging ingredients"], ["Time traveller avoiding paradox storms"]],
@@ -163,7 +113,54 @@ THEME_EXAMPLES = {
163
  }
164
 
165
  # ---------------------------------------------------------------------------
166
- # Sprite helpers
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  # ---------------------------------------------------------------------------
168
 
169
  def _pil_to_data_uri(pil_image: Image.Image, size: int = 64) -> str:
@@ -176,7 +173,7 @@ def _pil_to_data_uri(pil_image: Image.Image, size: int = 64) -> str:
176
  def _colored_placeholder(name: str) -> str:
177
  colours = ["#e74c3c", "#3498db", "#2ecc71", "#f39c12", "#9b59b6", "#1abc9c"]
178
  colour = colours[abs(hash(name)) % len(colours)]
179
- label = name.replace("sprite_", "")[:6]
180
  svg = (
181
  '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">'
182
  '<rect width="64" height="64" fill="' + colour + '" rx="8"/>'
@@ -186,130 +183,155 @@ def _colored_placeholder(name: str) -> str:
186
  return "data:image/svg+xml;base64," + base64.b64encode(svg.encode()).decode()
187
 
188
 
189
- def _sprite_prompt(sprite_name: str, theme: str) -> str:
190
- label = sprite_name.replace("sprite_", "").replace("_", " ")
191
- return (
192
- "Pixel-art game sprite: " + label + ". Theme: " + theme + ". "
193
- "Vibrant colours, clear silhouette, plain dark background, 64x64 pixel style."
194
- )
195
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
 
197
- def _find_sprite_filenames(html_code: str) -> list:
198
- pattern = r"""(?:src\s*=\s*['"]|\.src\s*=\s*['"])\s*(sprite_[a-zA-Z0-9_]+\.png)\s*['"]"""
199
- return list(dict.fromkeys(re.findall(pattern, html_code)))[:3]
200
 
 
 
 
201
 
202
- def _inject_sprites(html_code: str, sprite_map: dict) -> str:
 
 
203
  for fname, data_uri in sprite_map.items():
204
- html_code = html_code.replace('"' + fname + '"', '"' + data_uri + '"')
205
- html_code = html_code.replace("'" + fname + "'", "'" + data_uri + "'")
206
- return html_code
 
207
 
208
 
209
- def generate_sprites(sprite_names: list, theme: str) -> dict:
210
- if not sprite_names:
211
- return {}
212
- client = get_image_client()
213
- if not client:
214
- return {n: _colored_placeholder(n.replace(".png","")) for n in sprite_names}
215
- mapping = {}
216
- for fname in sprite_names:
217
- name_no_ext = fname.replace(".png", "")
218
- prompt = _sprite_prompt(name_no_ext, theme)
219
- try:
220
- pil_img = client.text_to_image(prompt, model=IMAGE_MODEL)
221
- mapping[fname] = _pil_to_data_uri(pil_img, size=64)
222
- except Exception as exc:
223
- print("[FLUX] Failed '" + fname + "': " + str(exc))
224
- mapping[fname] = _colored_placeholder(name_no_ext)
225
- return mapping
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
 
227
 
228
  # ---------------------------------------------------------------------------
229
- # Code generation
230
  # ---------------------------------------------------------------------------
231
 
232
- def generate_game_code(game_type: str, theme: str, temperature: float, max_new_tokens: int):
 
 
 
233
  if not theme.strip():
234
- return "", "Please enter a theme first.", _placeholder_html("Enter a theme and generate a game.")
235
-
236
- template = GAME_TYPES[game_type]["prompt_template"]
237
- user_prompt = template.format(theme=theme.strip())
238
- use_sprites = bool(HF_TOKEN)
239
-
240
- if use_sprites:
241
- system_msg = (
242
- "You are an expert HTML5 game developer. "
243
- "Write a complete, working, single-file HTML5 game using canvas and vanilla JavaScript. "
244
- "CRITICAL RULES - follow every one exactly: "
245
- "1. Add keydown/keyup listeners on WINDOW not canvas: window.addEventListener('keydown', e => keys.add(e.key)). "
246
- "2. Gravity: every frame do velY += 0.5 AFTER moving, NOT inside collision. "
247
- "3. Platform collision: loop ALL platforms, find the one the player is standing on. "
248
- " Set grounded=true and velY=0 ONLY when found. Set grounded=false BEFORE the loop, not inside else. "
249
- "4. Jump: if (grounded && (keys.has('ArrowUp') || keys.has('w') || keys.has('W'))) { velY=-12; grounded=false; } "
250
- "5. Always add a solid ground platform spanning the full canvas width at y=420. "
251
- "6. Use new Image() with src='sprite_NAME.png' for every visual asset, max 3 sprites. "
252
- "7. Draw images with ctx.drawImage(img, x, y, w, h). "
253
- "Output ONLY the raw HTML - no markdown fences, no explanation."
254
- )
255
- else:
256
- system_msg = (
257
- "You are an expert HTML5 game developer. "
258
- "Write a complete, working, single-file HTML5 game using only canvas 2D drawing. "
259
- "CRITICAL RULES - follow every one exactly: "
260
- "1. Add keydown/keyup listeners on WINDOW not canvas: window.addEventListener('keydown', e => keys.add(e.key)). "
261
- "2. Gravity: every frame do velY += 0.5 AFTER moving, NOT inside collision. "
262
- "3. Platform collision: loop ALL platforms, find the one the player is standing on. "
263
- " Set grounded=true and velY=0 ONLY when found. Set grounded=false BEFORE the loop, not inside else. "
264
- "4. Jump: if (grounded && (keys.has('ArrowUp') || keys.has('w') || keys.has('W'))) { velY=-12; grounded=false; } "
265
- "5. Always add a solid ground platform spanning the full canvas width at y=420. "
266
- "6. Never use image files. Draw everything with canvas shapes and colors. "
267
- "Output ONLY the raw HTML - no markdown fences, no explanation."
268
- )
269
 
270
- try:
271
- client = get_client()
272
- completion = client.chat.completions.create(
273
- model=CODE_MODEL,
274
- messages=[
275
- {"role": "system", "content": system_msg},
276
- {"role": "user", "content": user_prompt},
277
- ],
278
- max_tokens=int(max_new_tokens),
279
- temperature=float(temperature),
280
- )
281
- code = completion.choices[0].message.content.strip()
282
- code = re.sub(r"^```[a-zA-Z]*\n?", "", code).strip()
283
- code = re.sub(r"\n?```$", "", code).strip()
284
- if "<html" not in code.lower() and "<!doctype" not in code.lower():
285
- code = _wrap_in_html(code, theme)
286
-
287
- if use_sprites:
288
- sprite_names = _find_sprite_filenames(code)
289
- sprite_map = generate_sprites(sprite_names, theme.strip())
290
- code = _inject_sprites(code, sprite_map)
291
- n_real = sum(1 for v in sprite_map.values() if "image/png" in v)
292
- n_fallback = len(sprite_map) - n_real
293
- status = (
294
- "Done! " + str(len(sprite_names)) + " sprite(s) - "
295
- + str(n_real) + " by FLUX.1-schnell, "
296
- + str(n_fallback) + " fallback. Click Launch Game to play."
297
- )
298
- else:
299
- status = "Done! Game generated (canvas graphics). Click Launch Game to play."
300
 
301
- return code, status, _build_preview(code)
 
 
 
302
 
 
 
 
 
 
303
  except Exception as exc:
304
  traceback.print_exc()
305
- err = str(exc)
306
- if "401" in err or "api_key" in err.lower():
307
- err = "Invalid GROQ_API_KEY. Check your key at console.groq.com."
308
- elif "429" in err or "rate" in err.lower():
309
- err = "Rate limited by Groq - wait a few seconds and try again."
310
- else:
311
- err = "Error: " + str(exc)
312
- return "", err, _placeholder_html(err)
313
 
314
 
315
  # ---------------------------------------------------------------------------
@@ -356,7 +378,7 @@ def launch_game(code: str) -> str:
356
  # UI helpers
357
  # ---------------------------------------------------------------------------
358
 
359
- def update_type_description(game_type: str) -> str:
360
  return "_" + GAME_TYPES[game_type]["description"] + "_"
361
 
362
 
@@ -369,33 +391,32 @@ def get_first_theme(game_type: str) -> str:
369
  # ---------------------------------------------------------------------------
370
 
371
  def build_ui():
372
- with gr.Blocks(title="Game Generator - Free") as demo:
373
 
374
  gr.Markdown(
375
- "# Game Generator (Free)\n"
376
- "**Gemini 2.5 Flash** via Google AI Studio generates complete playable games.\n"
377
- "Canvas-drawn graphics โ€” no image API, no quota, no cost.\n\n"
378
- "> **Setup:** Go to [ai.google.dev](https://ai.google.dev), sign in with any Google account, "
379
- "click **Get API key** (free, no credit card, 2 minutes). "
380
- "Then add it as a Space secret named `GOOGLE_API_KEY`."
381
  )
382
 
383
  with gr.Row():
384
 
 
385
  with gr.Column(scale=1, min_width=300):
386
 
387
- gr.Markdown("## 1. Configure your game")
388
 
389
- game_type_dropdown = gr.Dropdown(
390
  choices=GAME_TYPE_NAMES, value="Platformer", label="Game genre",
391
  )
392
- type_description = gr.Markdown(
393
- value="_" + GAME_TYPES["Platformer"]["description"] + "_",
394
  )
395
  theme_box = gr.Textbox(
396
- label="Theme / setting",
397
- placeholder="e.g. Neon cyberpunk rooftops",
398
- lines=2,
399
  value=THEME_EXAMPLES["Platformer"][0][0],
400
  )
401
  gr.Examples(
@@ -404,56 +425,65 @@ def build_ui():
404
  label="Theme examples",
405
  )
406
 
407
- gr.Markdown("## 2. Generation settings")
408
 
409
- temperature_slider = gr.Slider(
410
  minimum=0.3, maximum=1.2, value=0.7, step=0.05,
411
- label="Temperature - higher = more creative",
412
  )
413
- max_tokens_slider = gr.Slider(
414
  minimum=1000, maximum=6000, value=4000, step=500,
415
- label="Max tokens - more = longer / more complete game",
416
  )
417
 
418
- generate_btn = gr.Button("Generate Game", variant="primary")
419
- gen_status = gr.Markdown(value="_No game generated yet._")
420
 
 
421
  with gr.Column(scale=2, min_width=500):
422
 
423
- gr.Markdown("## 3. Generated code (editable)")
 
 
 
 
 
 
 
 
424
 
425
  code_box = gr.Code(
426
  label="HTML source",
427
  language="html",
428
- lines=12,
429
  interactive=True,
430
  )
431
 
432
- launch_btn = gr.Button("Launch Game", variant="secondary")
433
 
434
- gr.Markdown("## 4. Live game window")
435
 
436
  game_frame = gr.HTML(
437
  value=_placeholder_html("Generate a game to see it here."),
438
  )
439
 
440
- game_type_dropdown.change(fn=update_type_description, inputs=[game_type_dropdown], outputs=[type_description])
441
- game_type_dropdown.change(fn=get_first_theme, inputs=[game_type_dropdown], outputs=[theme_box])
 
 
442
 
443
  generate_btn.click(
444
- fn=generate_game_code,
445
- inputs=[game_type_dropdown, theme_box, temperature_slider, max_tokens_slider],
446
- outputs=[code_box, gen_status, game_frame],
447
  )
448
 
449
  launch_btn.click(fn=launch_game, inputs=[code_box], outputs=[game_frame])
450
 
451
  gr.Markdown(
452
  "---\n"
453
- "**Why Google AI Studio:** 1,500 free requests/day, no credit card, "
454
- "Gemini 2.5 Flash is within 5% of GPT-4o quality. "
455
- "Rate limits reset daily โ€” not monthly. "
456
- "Edit the HTML and click **Launch Game** to hot-reload."
457
  )
458
 
459
  return demo
 
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
 
 
25
  from PIL import Image
26
 
27
  # ---------------------------------------------------------------------------
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
  # ---------------------------------------------------------------------------
56
 
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
  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
  }
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:
 
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
  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
  # UI helpers
379
  # ---------------------------------------------------------------------------
380
 
381
+ def update_description(game_type: str) -> str:
382
  return "_" + GAME_TYPES[game_type]["description"] + "_"
383
 
384
 
 
391
  # ---------------------------------------------------------------------------
392
 
393
  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():
404
 
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],
421
  )
422
  gr.Examples(
 
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
 
465
  game_frame = gr.HTML(
466
  value=_placeholder_html("Generate a game to see it here."),
467
  )
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