LeafCat79 commited on
Commit
76b9939
·
verified ·
1 Parent(s): 59ee339

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +439 -0
app.py ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Text-to-Game Generator - Free Tier Edition
3
+ - Code : mistralai/Mistral-7B-Instruct-v0.3 via hf-inference (free)
4
+ - Images: black-forest-labs/FLUX.1-schnell via hf-inference (free)
5
+
6
+ Both are free with just an HF_TOKEN - no payment method needed.
7
+ Add HF_TOKEN as a Space secret: Settings > Variables and secrets > New secret.
8
+ """
9
+
10
+ import os
11
+ import re
12
+ import io
13
+ import base64
14
+ import traceback
15
+
16
+ import gradio as gr
17
+ from huggingface_hub import InferenceClient
18
+ from PIL import Image
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Models - both free on hf-inference
22
+ # ---------------------------------------------------------------------------
23
+
24
+ CODE_MODEL = "mistralai/Mistral-7B-Instruct-v0.3"
25
+ IMAGE_MODEL = "black-forest-labs/FLUX.1-schnell"
26
+ PROVIDER = "hf-inference"
27
+
28
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
29
+
30
+
31
+ def _check_token():
32
+ if not HF_TOKEN:
33
+ raise ValueError(
34
+ "HF_TOKEN is not set. Add it as a Space secret: "
35
+ "Settings > Variables and secrets > New secret > Name: HF_TOKEN"
36
+ )
37
+
38
+
39
+ def get_code_client():
40
+ _check_token()
41
+ return InferenceClient(provider=PROVIDER, api_key=HF_TOKEN)
42
+
43
+
44
+ def get_image_client():
45
+ _check_token()
46
+ return InferenceClient(provider=PROVIDER, api_key=HF_TOKEN)
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Game type configs
51
+ # ---------------------------------------------------------------------------
52
+
53
+ GAME_TYPES = {
54
+ "Platformer": {
55
+ "description": "Jump over enemies and obstacles to reach the goal.",
56
+ "prompt_template": (
57
+ "Create a complete, self-contained HTML5 platformer game with the theme: {theme}.\n"
58
+ "Requirements:\n"
59
+ "- Single HTML file with all CSS and JavaScript inline.\n"
60
+ "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
61
+ "- Player moves left/right with arrow keys or WASD and jumps.\n"
62
+ "- At least 5 platforms, 2 moving enemies, and a goal to reach.\n"
63
+ "- Show score/lives on the canvas.\n"
64
+ "- Use requestAnimationFrame for the game loop.\n"
65
+ "- For every image asset use: const img = new Image(); img.src = 'sprite_NAME.png';\n"
66
+ " e.g. sprite_player.png, sprite_enemy.png, sprite_background.png, sprite_platform.png.\n"
67
+ "- No external libraries or CDN links.\n"
68
+ "- Theme all visuals to match: {theme}.\n"
69
+ "Output ONLY the raw HTML. No explanation, no markdown fences."
70
+ ),
71
+ },
72
+ "Top-Down Shooter": {
73
+ "description": "Shoot waves of enemies before they reach you.",
74
+ "prompt_template": (
75
+ "Create a complete, self-contained HTML5 top-down shooter game with the theme: {theme}.\n"
76
+ "Requirements:\n"
77
+ "- Single HTML file with all CSS and JavaScript inline.\n"
78
+ "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
79
+ "- Player moves with WASD/arrows; shoots with Space or click.\n"
80
+ "- Enemies spawn from edges in escalating waves.\n"
81
+ "- Display health, score, and wave on canvas.\n"
82
+ "- Game-over screen with score and restart button.\n"
83
+ "- Use requestAnimationFrame for the game loop.\n"
84
+ "- For every image asset: const img = new Image(); img.src = 'sprite_NAME.png';\n"
85
+ " e.g. sprite_player.png, sprite_enemy.png, sprite_bullet.png, sprite_background.png.\n"
86
+ "- No external libraries or CDN links.\n"
87
+ "- Theme everything to match: {theme}.\n"
88
+ "Output ONLY the raw HTML. No explanation, no markdown fences."
89
+ ),
90
+ },
91
+ "Puzzle / Maze": {
92
+ "description": "Navigate a maze or solve a tile puzzle to escape.",
93
+ "prompt_template": (
94
+ "Create a complete, self-contained HTML5 maze/tile-puzzle game with the theme: {theme}.\n"
95
+ "Requirements:\n"
96
+ "- Single HTML file with all CSS and JavaScript inline.\n"
97
+ "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
98
+ "- Player navigates with arrow keys or WASD.\n"
99
+ "- At least 15x10 tile grid, collectible keys, and a locked exit.\n"
100
+ "- Show a move counter or timer.\n"
101
+ "- Win screen when player collects all keys and exits.\n"
102
+ "- For every image asset: const img = new Image(); img.src = 'sprite_NAME.png';\n"
103
+ " e.g. sprite_player.png, sprite_wall.png, sprite_floor.png, sprite_key.png, sprite_exit.png.\n"
104
+ "- No external libraries or CDN links.\n"
105
+ "- Theme everything to match: {theme}.\n"
106
+ "Output ONLY the raw HTML. No explanation, no markdown fences."
107
+ ),
108
+ },
109
+ "Arcade / Dodge": {
110
+ "description": "Dodge falling obstacles and survive as long as possible.",
111
+ "prompt_template": (
112
+ "Create a complete, self-contained HTML5 arcade dodge game with the theme: {theme}.\n"
113
+ "Requirements:\n"
114
+ "- Single HTML file with all CSS and JavaScript inline.\n"
115
+ "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
116
+ "- Player moves with arrow keys or WASD; obstacles increase in speed over time.\n"
117
+ "- Collision ends the game; show time survived as score.\n"
118
+ "- High score stored in a JS variable; restart button on game-over screen.\n"
119
+ "- Use requestAnimationFrame for the game loop.\n"
120
+ "- For every image asset: const img = new Image(); img.src = 'sprite_NAME.png';\n"
121
+ " e.g. sprite_player.png, sprite_obstacle.png, sprite_background.png.\n"
122
+ "- No external libraries or CDN links.\n"
123
+ "- Theme everything to match: {theme}.\n"
124
+ "Output ONLY the raw HTML. No explanation, no markdown fences."
125
+ ),
126
+ },
127
+ "Surprise Me!": {
128
+ "description": "Let the AI invent the genre - could be anything!",
129
+ "prompt_template": (
130
+ "Create a complete, self-contained HTML5 browser game with the theme: {theme}.\n"
131
+ "Pick any fun arcade genre: breakout, snake, flappy-style, space invaders, etc.\n"
132
+ "Requirements:\n"
133
+ "- Single HTML file with all CSS and JavaScript inline.\n"
134
+ "- Use an HTML5 canvas (id='gameCanvas') sized 800x450.\n"
135
+ "- Keyboard or mouse controlled.\n"
136
+ "- Clear win/lose conditions and a score display.\n"
137
+ "- Game-over / win screen with a restart button.\n"
138
+ "- Use requestAnimationFrame for the game loop.\n"
139
+ "- For every image asset: const img = new Image(); img.src = 'sprite_NAME.png';\n"
140
+ " e.g. sprite_player.png, sprite_background.png.\n"
141
+ "- No external libraries or CDN links.\n"
142
+ "- Theme everything vividly to match: {theme}.\n"
143
+ "Output ONLY the raw HTML. No explanation, no markdown fences."
144
+ ),
145
+ },
146
+ }
147
+
148
+ GAME_TYPE_NAMES = list(GAME_TYPES.keys())
149
+
150
+ THEME_EXAMPLES = {
151
+ "Platformer": [["Jungle temple with ancient traps"], ["Neon cyberpunk rooftops"], ["Underwater pirate shipwreck"]],
152
+ "Top-Down Shooter": [["Alien desert invasion"], ["Viking village under siege"], ["Steampunk robot uprising"]],
153
+ "Puzzle / Maze": [["Haunted library with secret doors"], ["Ice cave with frozen keys"], ["Egyptian pyramid tiles"]],
154
+ "Arcade / Dodge": [["Asteroid field in a tiny rocket"], ["Chef dodging ingredients"], ["Time traveller avoiding paradox storms"]],
155
+ "Surprise Me!": [["A sentient library that rearranges itself"], ["Deep sea bioluminescent creatures"], ["Retro space diner on a comet"]],
156
+ }
157
+
158
+ # ---------------------------------------------------------------------------
159
+ # Image helpers
160
+ # ---------------------------------------------------------------------------
161
+
162
+ def _pil_to_data_uri(pil_image: Image.Image, size: int = 64) -> str:
163
+ img = pil_image.resize((size, size), Image.LANCZOS)
164
+ buf = io.BytesIO()
165
+ img.save(buf, format="PNG")
166
+ return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
167
+
168
+
169
+ def _colored_placeholder(name: str) -> str:
170
+ colours = ["#e74c3c", "#3498db", "#2ecc71", "#f39c12", "#9b59b6", "#1abc9c"]
171
+ colour = colours[abs(hash(name)) % len(colours)]
172
+ label = name.replace("sprite_", "")[:6]
173
+ svg = (
174
+ '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">'
175
+ '<rect width="64" height="64" fill="' + colour + '" rx="8"/>'
176
+ '<text x="32" y="38" font-size="10" text-anchor="middle" fill="white">' + label + '</text>'
177
+ '</svg>'
178
+ )
179
+ return "data:image/svg+xml;base64," + base64.b64encode(svg.encode()).decode()
180
+
181
+
182
+ def _sprite_prompt(sprite_name: str, theme: str) -> str:
183
+ label = sprite_name.replace("sprite_", "").replace("_", " ")
184
+ return (
185
+ "Pixel-art game sprite: " + label + ". Theme: " + theme + ". "
186
+ "Vibrant colours, clear silhouette, plain dark background, 64x64 pixel style."
187
+ )
188
+
189
+
190
+ def _find_sprite_filenames(html_code: str) -> list:
191
+ pattern = r"""(?:src\s*=\s*['"]|\.src\s*=\s*['"])\s*(sprite_[a-zA-Z0-9_]+\.png)\s*['"]"""
192
+ return list(dict.fromkeys(re.findall(pattern, html_code)))
193
+
194
+
195
+ def _inject_sprites(html_code: str, sprite_map: dict) -> str:
196
+ for fname, data_uri in sprite_map.items():
197
+ html_code = html_code.replace('"' + fname + '"', '"' + data_uri + '"')
198
+ html_code = html_code.replace("'" + fname + "'", "'" + data_uri + "'")
199
+ return html_code
200
+
201
+
202
+ def generate_sprites(sprite_names: list, theme: str) -> dict:
203
+ if not sprite_names:
204
+ return {}
205
+ client = get_image_client()
206
+ mapping = {}
207
+ for fname in sprite_names:
208
+ name_no_ext = fname.replace(".png", "")
209
+ prompt = _sprite_prompt(name_no_ext, theme)
210
+ try:
211
+ pil_img = client.text_to_image(prompt, model=IMAGE_MODEL)
212
+ mapping[fname] = _pil_to_data_uri(pil_img, size=64)
213
+ except Exception as exc:
214
+ print("[Image] Failed '" + fname + "': " + str(exc))
215
+ mapping[fname] = _colored_placeholder(name_no_ext)
216
+ return mapping
217
+
218
+
219
+ # ---------------------------------------------------------------------------
220
+ # Code generation
221
+ # ---------------------------------------------------------------------------
222
+
223
+ def generate_game_code(game_type: str, theme: str, temperature: float, max_new_tokens: int):
224
+ if not theme.strip():
225
+ return "", "Please enter a theme first.", _placeholder_html("Enter a theme and generate a game.")
226
+
227
+ template = GAME_TYPES[game_type]["prompt_template"]
228
+ user_prompt = template.format(theme=theme.strip())
229
+ system_msg = (
230
+ "You are an expert HTML5 game developer. "
231
+ "Write a complete, working, single-file HTML5 game using canvas and vanilla JavaScript. "
232
+ "Use new Image() with src='sprite_NAME.png' for every visual asset. "
233
+ "Output ONLY the raw HTML - no markdown fences, no explanation."
234
+ )
235
+
236
+ try:
237
+ # Step 1: Generate HTML via Mistral-7B (hf-inference, free)
238
+ client = get_code_client()
239
+ completion = client.chat.completions.create(
240
+ model=CODE_MODEL,
241
+ messages=[
242
+ {"role": "system", "content": system_msg},
243
+ {"role": "user", "content": user_prompt},
244
+ ],
245
+ max_tokens=int(max_new_tokens),
246
+ temperature=float(temperature),
247
+ )
248
+ code = completion.choices[0].message.content.strip()
249
+ code = re.sub(r"^```[a-zA-Z]*\n?", "", code).strip()
250
+ code = re.sub(r"\n?```$", "", code).strip()
251
+ if "<html" not in code.lower() and "<!doctype" not in code.lower():
252
+ code = _wrap_in_html(code, theme)
253
+
254
+ # Step 2: Detect sprites
255
+ sprite_names = _find_sprite_filenames(code)
256
+
257
+ # Step 3: Generate sprites via FLUX.1-schnell (hf-inference, free)
258
+ sprite_map = generate_sprites(sprite_names, theme.strip())
259
+
260
+ # Step 4: Inject base64 images into HTML
261
+ final_code = _inject_sprites(code, sprite_map)
262
+
263
+ n_real = sum(1 for v in sprite_map.values() if "image/png" in v)
264
+ n_fallback = len(sprite_map) - n_real
265
+ status = (
266
+ "Done! " + str(len(sprite_names)) + " sprite(s) - "
267
+ + str(n_real) + " by FLUX.1-schnell, "
268
+ + str(n_fallback) + " fallback. Click Launch Game to play."
269
+ )
270
+ return final_code, status, _build_preview(final_code)
271
+
272
+ except Exception as exc:
273
+ traceback.print_exc()
274
+ err = str(exc)
275
+ if "401" in err or "unauthorized" in err.lower():
276
+ err = "Unauthorized - make sure your HF_TOKEN is added as a Space secret."
277
+ elif "402" in err or "payment" in err.lower():
278
+ err = "Free quota exceeded - wait a few minutes and try again."
279
+ elif "429" in err or "rate" in err.lower():
280
+ err = "Rate limited - wait a moment and try again."
281
+ else:
282
+ err = "Error: " + str(exc)
283
+ return "", err, _placeholder_html(err)
284
+
285
+
286
+ # ---------------------------------------------------------------------------
287
+ # HTML helpers
288
+ # ---------------------------------------------------------------------------
289
+
290
+ def _placeholder_html(message: str) -> str:
291
+ safe = message.replace("<", "&lt;").replace(">", "&gt;")
292
+ return (
293
+ '<div style="display:flex;align-items:center;justify-content:center;'
294
+ 'width:100%;height:460px;background:#0d0d0d;border-radius:12px;'
295
+ 'border:2px dashed #333;color:#555;font-family:monospace;font-size:14px;'
296
+ 'text-align:center;padding:24px;box-sizing:border-box;">'
297
+ '<pre style="margin:0;white-space:pre-wrap;">' + safe + '</pre></div>'
298
+ )
299
+
300
+
301
+ def _wrap_in_html(snippet: str, theme: str) -> str:
302
+ return (
303
+ "<!DOCTYPE html>\n<html lang='en'>\n<head>\n"
304
+ "<meta charset='UTF-8'><title>" + theme + "</title>\n"
305
+ "<style>body{margin:0;background:#111;display:flex;justify-content:center;"
306
+ "align-items:center;height:100vh;}canvas{display:block;}</style>\n"
307
+ "</head>\n<body>\n" + snippet + "\n</body>\n</html>"
308
+ )
309
+
310
+
311
+ def _build_preview(html_code: str) -> str:
312
+ encoded = base64.b64encode(html_code.encode("utf-8")).decode("ascii")
313
+ return (
314
+ '<iframe src="data:text/html;base64,' + encoded + '" '
315
+ 'style="width:100%;height:460px;border:none;border-radius:12px;background:#000;" '
316
+ 'sandbox="allow-scripts" title="Game Preview"></iframe>'
317
+ )
318
+
319
+
320
+ def launch_game(code: str) -> str:
321
+ if not code or not code.strip():
322
+ return _placeholder_html("No game code yet - generate a game first.")
323
+ return _build_preview(code)
324
+
325
+
326
+ # ---------------------------------------------------------------------------
327
+ # UI helpers
328
+ # ---------------------------------------------------------------------------
329
+
330
+ def update_type_description(game_type: str) -> str:
331
+ return "_" + GAME_TYPES[game_type]["description"] + "_"
332
+
333
+
334
+ def get_first_theme(game_type: str) -> str:
335
+ return THEME_EXAMPLES[game_type][0][0]
336
+
337
+
338
+ # ---------------------------------------------------------------------------
339
+ # Gradio UI
340
+ # ---------------------------------------------------------------------------
341
+
342
+ def build_ui():
343
+ with gr.Blocks(title="Game Generator") as demo:
344
+
345
+ gr.Markdown(
346
+ "# Game Generator\n"
347
+ "Describe a theme, pick a genre - "
348
+ "**Mistral-7B** writes the game and "
349
+ "**FLUX.1-schnell** generates every sprite. "
350
+ "Both are **100% free** via HF Inference.\n\n"
351
+ "> Add your **HF_TOKEN** as a Space secret "
352
+ "(Settings > Variables and secrets > New secret > Name: HF_TOKEN)."
353
+ )
354
+
355
+ with gr.Row():
356
+
357
+ with gr.Column(scale=1, min_width=300):
358
+
359
+ gr.Markdown("## 1. Configure your game")
360
+
361
+ game_type_dropdown = gr.Dropdown(
362
+ choices=GAME_TYPE_NAMES, value="Platformer", label="Game genre",
363
+ )
364
+ type_description = gr.Markdown(
365
+ value="_" + GAME_TYPES["Platformer"]["description"] + "_",
366
+ )
367
+ theme_box = gr.Textbox(
368
+ label="Theme / setting",
369
+ placeholder="e.g. Neon cyberpunk rooftops",
370
+ lines=2,
371
+ value=THEME_EXAMPLES["Platformer"][0][0],
372
+ )
373
+ gr.Examples(
374
+ examples=THEME_EXAMPLES["Platformer"],
375
+ inputs=[theme_box],
376
+ label="Theme examples",
377
+ )
378
+
379
+ gr.Markdown("## 2. Generation settings")
380
+
381
+ temperature_slider = gr.Slider(
382
+ minimum=0.3, maximum=1.2, value=0.7, step=0.05,
383
+ label="Temperature - higher = more creative",
384
+ )
385
+ max_tokens_slider = gr.Slider(
386
+ minimum=2000, maximum=8000, value=6000, step=500,
387
+ label="Max tokens - more = longer game",
388
+ )
389
+
390
+ generate_btn = gr.Button("Generate Game + Sprites", variant="primary")
391
+ gen_status = gr.Markdown(value="_No game generated yet._")
392
+
393
+ with gr.Column(scale=2, min_width=500):
394
+
395
+ gr.Markdown("## 3. Generated code (editable)")
396
+
397
+ code_box = gr.Code(
398
+ label="HTML source (sprites embedded as base64)",
399
+ language="html",
400
+ lines=12,
401
+ interactive=True,
402
+ )
403
+
404
+ launch_btn = gr.Button("Launch Game", variant="secondary")
405
+
406
+ gr.Markdown("## 4. Live game window")
407
+
408
+ game_frame = gr.HTML(
409
+ value=_placeholder_html("Generate a game to see it here."),
410
+ )
411
+
412
+ game_type_dropdown.change(fn=update_type_description, inputs=[game_type_dropdown], outputs=[type_description])
413
+ game_type_dropdown.change(fn=get_first_theme, inputs=[game_type_dropdown], outputs=[theme_box])
414
+
415
+ generate_btn.click(
416
+ fn=generate_game_code,
417
+ inputs=[game_type_dropdown, theme_box, temperature_slider, max_tokens_slider],
418
+ outputs=[code_box, gen_status, game_frame],
419
+ )
420
+
421
+ launch_btn.click(fn=launch_game, inputs=[code_box], outputs=[game_frame])
422
+
423
+ gr.Markdown(
424
+ "---\n"
425
+ "Code: **Mistral-7B-Instruct** via HF Inference (free). "
426
+ "Images: **FLUX.1-schnell** via HF Inference (free). "
427
+ "Edit the HTML source and click **Launch Game** to hot-reload."
428
+ )
429
+
430
+ return demo
431
+
432
+
433
+ # ---------------------------------------------------------------------------
434
+ # Entry point
435
+ # ---------------------------------------------------------------------------
436
+
437
+ if __name__ == "__main__":
438
+ app = build_ui()
439
+ app.launch()