spawnpoint-studio 0.1.1__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. {spawnpoint_studio-0.1.1/compiler/spawnpoint_studio.egg-info → spawnpoint_studio-0.2.0}/PKG-INFO +22 -10
  2. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/README.md +21 -9
  3. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/build.py +3 -1
  4. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/cli.py +24 -4
  5. spawnpoint_studio-0.2.0/compiler/spawnpoint/demo.py +247 -0
  6. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/llm_server.py +48 -5
  7. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/playtest/report.py +4 -1
  8. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/playtest/runner.py +16 -1
  9. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/playtest/tuner.py +43 -14
  10. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/sprites/generate.py +12 -0
  11. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/assemble.py +47 -5
  12. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/brief.py +16 -1
  13. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/llm.py +32 -7
  14. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/web.py +2 -2
  15. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0/compiler/spawnpoint_studio.egg-info}/PKG-INFO +22 -10
  16. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint_studio.egg-info/SOURCES.txt +9 -0
  17. spawnpoint_studio-0.2.0/compiler/tests/test_demo.py +79 -0
  18. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_playtest.py +24 -7
  19. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_studio.py +63 -0
  20. spawnpoint_studio-0.2.0/examples/dino_eggs.json +99 -0
  21. spawnpoint_studio-0.2.0/examples/night_survivor.json +428 -0
  22. spawnpoint_studio-0.2.0/examples/ninja_rescue.json +351 -0
  23. spawnpoint_studio-0.2.0/godot/addons/spawn_point/components/bubble_board_component.gd +413 -0
  24. spawnpoint_studio-0.2.0/godot/addons/spawn_point/components/experience_component.gd +53 -0
  25. spawnpoint_studio-0.2.0/godot/addons/spawn_point/components/level_up_menu_component.gd +225 -0
  26. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/spawner_component.gd +23 -1
  27. spawnpoint_studio-0.2.0/godot/addons/spawn_point/components/xp_orb_component.gd +67 -0
  28. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/manifest.json +395 -0
  29. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/hud.gd +48 -1
  30. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/world.gd +43 -0
  31. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/playtest.gd +57 -12
  32. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/record.gd +1 -0
  33. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/pyproject.toml +1 -1
  34. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/LICENSE +0 -0
  35. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/MANIFEST.in +0 -0
  36. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/__init__.py +0 -0
  37. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/analyze.py +0 -0
  38. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/cockpit/__init__.py +0 -0
  39. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/cockpit/server.py +0 -0
  40. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/cockpit/static/app.css +0 -0
  41. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/cockpit/static/app.js +0 -0
  42. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/cockpit/static/index.html +0 -0
  43. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/diagnostics.py +0 -0
  44. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/expr.py +0 -0
  45. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/gif.py +0 -0
  46. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/godot.py +0 -0
  47. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/graph.py +0 -0
  48. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/level.py +0 -0
  49. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/manifest.py +0 -0
  50. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/playtest/__init__.py +0 -0
  51. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/project.py +0 -0
  52. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/py.typed +0 -0
  53. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/rules.py +0 -0
  54. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/sfx.py +0 -0
  55. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/sprites/__init__.py +0 -0
  56. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/sprites/ai.py +0 -0
  57. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/sprites/contour.py +0 -0
  58. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/sprites/importer.py +0 -0
  59. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/sprites/resolve.py +0 -0
  60. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/sprites/spriteset.py +0 -0
  61. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/sprites/tiles.py +0 -0
  62. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/__init__.py +0 -0
  63. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/creative.py +0 -0
  64. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/dataset.py +0 -0
  65. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/edits.py +0 -0
  66. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/levelgen.py +0 -0
  67. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/orchestrator.py +0 -0
  68. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/prompts.py +0 -0
  69. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/webcheck.py +0 -0
  70. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio_cli.py +0 -0
  71. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint_studio.egg-info/dependency_links.txt +0 -0
  72. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint_studio.egg-info/entry_points.txt +0 -0
  73. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint_studio.egg-info/requires.txt +0 -0
  74. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint_studio.egg-info/top_level.txt +0 -0
  75. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/conftest.py +0 -0
  76. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_ai_sprites.py +0 -0
  77. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_analyze.py +0 -0
  78. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_cockpit.py +0 -0
  79. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_e2e.py +0 -0
  80. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_expr.py +0 -0
  81. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_gif.py +0 -0
  82. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_godot_library.py +0 -0
  83. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_graph.py +0 -0
  84. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_level.py +0 -0
  85. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_live_llm.py +0 -0
  86. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_manifest.py +0 -0
  87. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_rules.py +0 -0
  88. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_sfx.py +0 -0
  89. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_sprites.py +0 -0
  90. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_web.py +0 -0
  91. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/examples/coin_hopper.json +0 -0
  92. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/examples/neon_arena.json +0 -0
  93. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/examples/pixel_arena.json +0 -0
  94. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/examples/pixel_hopper.json +0 -0
  95. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/aim_component.gd +0 -0
  96. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/animator_component.gd +0 -0
  97. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/chase_ai_component.gd +0 -0
  98. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/collectible_component.gd +0 -0
  99. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/gate_component.gd +0 -0
  100. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/health_component.gd +0 -0
  101. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/hitbox_component.gd +0 -0
  102. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/hurtbox_component.gd +0 -0
  103. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/juice_component.gd +0 -0
  104. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/patrol_ai_component.gd +0 -0
  105. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/platformer_movement_component.gd +0 -0
  106. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/player_input_component.gd +0 -0
  107. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/projectile_component.gd +0 -0
  108. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/stomp_component.gd +0 -0
  109. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/top_down_movement_component.gd +0 -0
  110. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/trigger_zone_component.gd +0 -0
  111. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/weapon_component.gd +0 -0
  112. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/camera_rig.gd +0 -0
  113. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/cockpit.gd +0 -0
  114. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/entity_core.gd +0 -0
  115. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/fx.gd +0 -0
  116. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/game_state.gd +0 -0
  117. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/level.gd +0 -0
  118. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/sfx.gd +0 -0
  119. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/sp.gd +0 -0
  120. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/shaders/flash.gdshader +0 -0
  121. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/shaders/floor_grid.gdshader +0 -0
  122. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/build.gd +0 -0
  123. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/build.tscn +0 -0
  124. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/capture.gd +0 -0
  125. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/capture.tscn +0 -0
  126. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/dump_manifest.gd +0 -0
  127. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/dump_manifest.tscn +0 -0
  128. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/playtest.tscn +0 -0
  129. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/playtest_nav.gd +0 -0
  130. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/record.tscn +0 -0
  131. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/smoke.gd +0 -0
  132. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/smoke.tscn +0 -0
  133. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/setup.cfg +0 -0
  134. {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spawnpoint-studio
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: Turn a sentence into a juicy, playable Godot 4 game — locally, on a consumer GPU.
5
5
  Author: smoky1496
6
6
  License: MIT
@@ -29,6 +29,7 @@ Dynamic: license-file
29
29
  [![CI](https://github.com/leluong141996-dev/Spawn-Point/actions/workflows/ci.yml/badge.svg)](https://github.com/leluong141996-dev/Spawn-Point/actions/workflows/ci.yml)
30
30
  [![PyPI](https://img.shields.io/pypi/v/spawnpoint-studio)](https://pypi.org/project/spawnpoint-studio/)
31
31
  [![Model](https://img.shields.io/badge/%F0%9F%A4%97%20model-SpawnPoint--Coder--7B-yellow)](https://huggingface.co/smoky1496/SpawnPoint-Coder-7B-AWQ)
32
+ [![Play the demos](https://img.shields.io/badge/%E2%96%B6%20play-demos%20in%20your%20browser-ff004d)](https://leluong141996-dev.github.io/Spawn-Point/)
32
33
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
33
34
 
34
35
  <p align="center">
@@ -39,6 +40,9 @@ Dynamic: license-file
39
40
  <code>examples/pixel_arena.json</code>: generated sprites, collision traced from pixels, juice on by
40
41
  default. Recorded with <code>spawnpoint gif</code> (autopilot).</sub></p>
41
42
 
43
+ <p align="center"><b><a href="https://leluong141996-dev.github.io/Spawn-Point/">▶ Play the demos in your browser</a></b>: a Mario-style platformer, a ninja
44
+ rescue, a dinosaur-egg bubble shooter, a survivor with level-ups and an arena shooter. No install needed.</p>
45
+
42
46
  Spawn Point is an open-source, autonomous game studio that runs on your own machine. Instead of asking an LLM to type
43
47
  hundreds of lines of GDScript (slow, expensive, and easy to break with Godot 3/4 mix-ups), the LLM
44
48
  assembles **pre-tested components** like LEGO, places them on an **ASCII map**, and wires gameplay
@@ -62,16 +66,16 @@ graph.json ──► validate (Python) ──► build scenes (Godot headless)
62
66
  ## Install
63
67
 
64
68
  Requirements: Python ≥ 3.10, [Godot 4.6](https://godotengine.org/download) on your `PATH` (`godot4` or
65
- `godot`, or set `GODOT_BIN`), and for the LLM features either a 12 GB NVIDIA GPU with Docker (local model)
66
- or any OpenAI-compatible API.
69
+ `godot`, or set `GODOT_BIN`), and for the LLM features one of: a 12 GB NVIDIA GPU with Docker (vLLM, fastest),
70
+ [Ollama](https://ollama.com/download) on any machine (Mac, AMD, or CPU), or any OpenAI-compatible API.
67
71
 
68
72
  ```bash
69
73
  pip install spawnpoint-studio
70
- spawnpoint llm up # serves SpawnPoint-Coder-7B locally (or set SPAWNPOINT_LLM_* for an API)
74
+ spawnpoint llm up # SpawnPoint-Coder-7B: vLLM on NVIDIA, otherwise Ollama (or set SPAWNPOINT_LLM_*)
71
75
  spawnpoint create "a ninja platformer where you collect stars"
72
76
  ```
73
77
 
74
- No GPU? Try the hand-written examples, no LLM needed:
78
+ No GPU? [Play the demos in your browser](https://leluong141996-dev.github.io/Spawn-Point/), or build the hand-written examples locally, no LLM needed:
75
79
 
76
80
  ```bash
77
81
  spawnpoint examples copy pixel_hopper && spawnpoint build pixel_hopper.json --run
@@ -102,7 +106,7 @@ Standard library only on the server, one HTML/JS page on the client, Server-Sent
102
106
  ## Make a game from a sentence
103
107
 
104
108
  ```bash
105
- scripts/llm-up.sh # SpawnPoint-Coder-7B on vLLM (~11 GB VRAM) — or any OpenAI-compatible API
109
+ spawnpoint llm up # SpawnPoint-Coder-7B: vLLM (~11 GB VRAM) or Ollama — or any OpenAI-compatible API
106
110
  spawnpoint create "a ninja platformer where you collect stars"
107
111
  ```
108
112
 
@@ -148,10 +152,13 @@ edits as strict JSON, and the compiler turns that into Godot scenes and GDScript
148
152
  GPU (AWQ 4-bit). v2 on 30 human-written game ideas (English and Vietnamese): 87% fully right (v1: 73%),
149
153
  97% of checks passed; on held-out tasks: briefs 88% exact (v1: 36%), Vietnamese → English 97% (v1: 52%),
150
154
  tweaks 98%, entity edits 91%. It is tuned for 2D platformers and 2D top-down games only; other genres
151
- are no better than a general 7B coder. `scripts/llm-up.sh` downloads and serves it.
155
+ are no better than a general 7B coder. `spawnpoint llm up` downloads and serves it: the AWQ build on vLLM when
156
+ Docker and an NVIDIA GPU are present, otherwise the [GGUF build](https://huggingface.co/smoky1496/SpawnPoint-Coder-7B-GGUF)
157
+ (Q4_K_M, ~4.7 GB) in Ollama. Force one with `--backend vllm|ollama`.
152
158
 
153
- LLM settings: `SPAWNPOINT_LLM_BASE_URL` (default `http://localhost:8000/v1`),
154
- `SPAWNPOINT_LLM_MODEL` (default: the server's first model), `SPAWNPOINT_LLM_API_KEY`.
159
+ LLM settings: `SPAWNPOINT_LLM_BASE_URL` (default: vLLM at `http://localhost:8000/v1`, or Ollama at
160
+ `http://localhost:11434/v1` when only Ollama answers), `SPAWNPOINT_LLM_MODEL` (default: SpawnPoint-Coder if the
161
+ server has it, else its first model), `SPAWNPOINT_LLM_API_KEY`.
155
162
  Web builds need the Godot **web export templates** (Editor → Manage Export Templates, or unpack
156
163
  `web_*.zip` from the official `.tpz` into `~/.local/share/godot/export_templates/<version>/`);
157
164
  use `--no-web` to skip and play natively with `spawnpoint run`.
@@ -326,7 +333,9 @@ headless smoke test (spawn, scripted input, crash detection) and report `E_RUNTI
326
333
  | Movement | `PlatformerMovement` (coyote time, jump buffer, variable jump, apex hang, dash, double jump), `TopDownMovement` (accel/friction, dash) |
327
334
  | Controllers | `PlayerInput`, `PatrolAI`, `ChaseAI`, `Aim` |
328
335
  | Combat | `Health`, `Hitbox`, `Hurtbox`, `Stomp`, `Weapon`, `Projectile` |
329
- | World | `Collectible`, `TriggerZone`, `Spawner`, `Gate` |
336
+ | World | `Collectible`, `TriggerZone`, `Spawner` (ramping waves, spawns around the player), `Gate` |
337
+ | Progression | `Experience` (XP and levels), `XpOrb` (gems that fly to the player), `LevelUpMenu` (pause and pick one of three upgrades) |
338
+ | Game modes | `BubbleBoard` (a complete bubble shooter: bank shots, colour pops, falling clusters, a descending ceiling) |
330
339
  | Feel | `Juice` — attached to every entity unless `"juice": "none"` |
331
340
 
332
341
  Runtime autoloads: `GameState` (score, lives, variables, win/lose), `World` (spawning, rule
@@ -361,6 +370,9 @@ spawnpoint manifest # regenerate manifest.json after changing a componen
361
370
  godot4 --headless --path godot --fixed-fps 60 res://tests/run_tests.tscn # GDScript suite only
362
371
  ```
363
372
 
373
+ `spawnpoint demo -o site` builds the browser demo site: the showcase graphs in `examples/`, one shared Godot
374
+ engine, each game checked in headless Chrome. The Pages workflow publishes it on every push.
375
+
364
376
  Adding a component: create `godot/addons/spawn_point/components/<name>_component.gd` following the
365
377
  contract (typed `@export`s with ranges, `SP_NAME` / `SP_HOSTS` / `SP_REQUIRES` / `SP_CATEGORY`
366
378
  constants, `##` docs on everything public), add tests in `godot/tests/`, then run `spawnpoint manifest`.
@@ -5,6 +5,7 @@
5
5
  [![CI](https://github.com/leluong141996-dev/Spawn-Point/actions/workflows/ci.yml/badge.svg)](https://github.com/leluong141996-dev/Spawn-Point/actions/workflows/ci.yml)
6
6
  [![PyPI](https://img.shields.io/pypi/v/spawnpoint-studio)](https://pypi.org/project/spawnpoint-studio/)
7
7
  [![Model](https://img.shields.io/badge/%F0%9F%A4%97%20model-SpawnPoint--Coder--7B-yellow)](https://huggingface.co/smoky1496/SpawnPoint-Coder-7B-AWQ)
8
+ [![Play the demos](https://img.shields.io/badge/%E2%96%B6%20play-demos%20in%20your%20browser-ff004d)](https://leluong141996-dev.github.io/Spawn-Point/)
8
9
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
9
10
 
10
11
  <p align="center">
@@ -15,6 +16,9 @@
15
16
  <code>examples/pixel_arena.json</code>: generated sprites, collision traced from pixels, juice on by
16
17
  default. Recorded with <code>spawnpoint gif</code> (autopilot).</sub></p>
17
18
 
19
+ <p align="center"><b><a href="https://leluong141996-dev.github.io/Spawn-Point/">▶ Play the demos in your browser</a></b>: a Mario-style platformer, a ninja
20
+ rescue, a dinosaur-egg bubble shooter, a survivor with level-ups and an arena shooter. No install needed.</p>
21
+
18
22
  Spawn Point is an open-source, autonomous game studio that runs on your own machine. Instead of asking an LLM to type
19
23
  hundreds of lines of GDScript (slow, expensive, and easy to break with Godot 3/4 mix-ups), the LLM
20
24
  assembles **pre-tested components** like LEGO, places them on an **ASCII map**, and wires gameplay
@@ -38,16 +42,16 @@ graph.json ──► validate (Python) ──► build scenes (Godot headless)
38
42
  ## Install
39
43
 
40
44
  Requirements: Python ≥ 3.10, [Godot 4.6](https://godotengine.org/download) on your `PATH` (`godot4` or
41
- `godot`, or set `GODOT_BIN`), and for the LLM features either a 12 GB NVIDIA GPU with Docker (local model)
42
- or any OpenAI-compatible API.
45
+ `godot`, or set `GODOT_BIN`), and for the LLM features one of: a 12 GB NVIDIA GPU with Docker (vLLM, fastest),
46
+ [Ollama](https://ollama.com/download) on any machine (Mac, AMD, or CPU), or any OpenAI-compatible API.
43
47
 
44
48
  ```bash
45
49
  pip install spawnpoint-studio
46
- spawnpoint llm up # serves SpawnPoint-Coder-7B locally (or set SPAWNPOINT_LLM_* for an API)
50
+ spawnpoint llm up # SpawnPoint-Coder-7B: vLLM on NVIDIA, otherwise Ollama (or set SPAWNPOINT_LLM_*)
47
51
  spawnpoint create "a ninja platformer where you collect stars"
48
52
  ```
49
53
 
50
- No GPU? Try the hand-written examples, no LLM needed:
54
+ No GPU? [Play the demos in your browser](https://leluong141996-dev.github.io/Spawn-Point/), or build the hand-written examples locally, no LLM needed:
51
55
 
52
56
  ```bash
53
57
  spawnpoint examples copy pixel_hopper && spawnpoint build pixel_hopper.json --run
@@ -78,7 +82,7 @@ Standard library only on the server, one HTML/JS page on the client, Server-Sent
78
82
  ## Make a game from a sentence
79
83
 
80
84
  ```bash
81
- scripts/llm-up.sh # SpawnPoint-Coder-7B on vLLM (~11 GB VRAM) — or any OpenAI-compatible API
85
+ spawnpoint llm up # SpawnPoint-Coder-7B: vLLM (~11 GB VRAM) or Ollama — or any OpenAI-compatible API
82
86
  spawnpoint create "a ninja platformer where you collect stars"
83
87
  ```
84
88
 
@@ -124,10 +128,13 @@ edits as strict JSON, and the compiler turns that into Godot scenes and GDScript
124
128
  GPU (AWQ 4-bit). v2 on 30 human-written game ideas (English and Vietnamese): 87% fully right (v1: 73%),
125
129
  97% of checks passed; on held-out tasks: briefs 88% exact (v1: 36%), Vietnamese → English 97% (v1: 52%),
126
130
  tweaks 98%, entity edits 91%. It is tuned for 2D platformers and 2D top-down games only; other genres
127
- are no better than a general 7B coder. `scripts/llm-up.sh` downloads and serves it.
131
+ are no better than a general 7B coder. `spawnpoint llm up` downloads and serves it: the AWQ build on vLLM when
132
+ Docker and an NVIDIA GPU are present, otherwise the [GGUF build](https://huggingface.co/smoky1496/SpawnPoint-Coder-7B-GGUF)
133
+ (Q4_K_M, ~4.7 GB) in Ollama. Force one with `--backend vllm|ollama`.
128
134
 
129
- LLM settings: `SPAWNPOINT_LLM_BASE_URL` (default `http://localhost:8000/v1`),
130
- `SPAWNPOINT_LLM_MODEL` (default: the server's first model), `SPAWNPOINT_LLM_API_KEY`.
135
+ LLM settings: `SPAWNPOINT_LLM_BASE_URL` (default: vLLM at `http://localhost:8000/v1`, or Ollama at
136
+ `http://localhost:11434/v1` when only Ollama answers), `SPAWNPOINT_LLM_MODEL` (default: SpawnPoint-Coder if the
137
+ server has it, else its first model), `SPAWNPOINT_LLM_API_KEY`.
131
138
  Web builds need the Godot **web export templates** (Editor → Manage Export Templates, or unpack
132
139
  `web_*.zip` from the official `.tpz` into `~/.local/share/godot/export_templates/<version>/`);
133
140
  use `--no-web` to skip and play natively with `spawnpoint run`.
@@ -302,7 +309,9 @@ headless smoke test (spawn, scripted input, crash detection) and report `E_RUNTI
302
309
  | Movement | `PlatformerMovement` (coyote time, jump buffer, variable jump, apex hang, dash, double jump), `TopDownMovement` (accel/friction, dash) |
303
310
  | Controllers | `PlayerInput`, `PatrolAI`, `ChaseAI`, `Aim` |
304
311
  | Combat | `Health`, `Hitbox`, `Hurtbox`, `Stomp`, `Weapon`, `Projectile` |
305
- | World | `Collectible`, `TriggerZone`, `Spawner`, `Gate` |
312
+ | World | `Collectible`, `TriggerZone`, `Spawner` (ramping waves, spawns around the player), `Gate` |
313
+ | Progression | `Experience` (XP and levels), `XpOrb` (gems that fly to the player), `LevelUpMenu` (pause and pick one of three upgrades) |
314
+ | Game modes | `BubbleBoard` (a complete bubble shooter: bank shots, colour pops, falling clusters, a descending ceiling) |
306
315
  | Feel | `Juice` — attached to every entity unless `"juice": "none"` |
307
316
 
308
317
  Runtime autoloads: `GameState` (score, lives, variables, win/lose), `World` (spawning, rule
@@ -337,6 +346,9 @@ spawnpoint manifest # regenerate manifest.json after changing a componen
337
346
  godot4 --headless --path godot --fixed-fps 60 res://tests/run_tests.tscn # GDScript suite only
338
347
  ```
339
348
 
349
+ `spawnpoint demo -o site` builds the browser demo site: the showcase graphs in `examples/`, one shared Godot
350
+ engine, each game checked in headless Chrome. The Pages workflow publishes it on every push.
351
+
340
352
  Adding a component: create `godot/addons/spawn_point/components/<name>_component.gd` following the
341
353
  contract (typed `@export`s with ranges, `SP_NAME` / `SP_HOSTS` / `SP_REQUIRES` / `SP_CATEGORY`
342
354
  constants, `##` docs on everything public), add tests in `godot/tests/`, then run `spawnpoint manifest`.
@@ -142,5 +142,7 @@ def _judge_smoke(report: dict, plan: dict, diags: Diagnostics) -> None:
142
142
  return
143
143
  if plan["genre"] == "platformer" and not report.get("on_floor_after_1s"):
144
144
  diags.warning("W_SMOKE_FLOOR", "(smoke)", "the player is not standing on a floor after 1 second")
145
- if report.get("moved_px", 0) < 4:
145
+ player = next((e for e in plan["entities"] if any(c["name"] == "PlayerInput" for c in e["components"])), None)
146
+ moves = player is None or any(c["name"] in ("PlatformerMovement", "TopDownMovement") for c in player["components"])
147
+ if moves and report.get("moved_px", 0) < 4: # a bubble-shooter launcher does not walk
146
148
  diags.warning("W_SMOKE_STUCK", "(smoke)", "the player did not move under scripted input (stuck?)")
@@ -73,14 +73,20 @@ def main(argv: list[str] | None = None) -> int:
73
73
  tw.add_argument("--no-web", action="store_true")
74
74
  tw.add_argument("--record", action="store_true", help="save LLM calls as local fine-tuning data")
75
75
 
76
- lm = sub.add_parser("llm", help="run SpawnPoint-Coder-7B locally (Docker + NVIDIA GPU)")
76
+ lm = sub.add_parser("llm", help="run SpawnPoint-Coder-7B locally (vLLM on NVIDIA, or Ollama anywhere)")
77
77
  lm.add_argument("action", choices=["up", "down", "status"])
78
78
  lm.add_argument("--port", type=int, default=8000)
79
+ lm.add_argument("--backend", choices=["auto", "vllm", "ollama"], default="auto",
80
+ help="auto: vLLM with Docker + an NVIDIA GPU, otherwise Ollama")
79
81
 
80
82
  ex = sub.add_parser("examples", help="list the example graphs, or copy one here")
81
83
  ex.add_argument("action", nargs="?", choices=["list", "copy"], default="list")
82
84
  ex.add_argument("name", nargs="?")
83
85
 
86
+ dm = sub.add_parser("demo", help="build every example as a web game plus a gallery page (static site)")
87
+ dm.add_argument("-o", "--out", type=Path, default=Path("site"))
88
+ dm.add_argument("--no-screenshots", action="store_true", help="skip the headless-Chrome check and thumbnails")
89
+
84
90
  gf = sub.add_parser("gif", help="record a gameplay GIF of a built project (autopilot; needs a display)")
85
91
  gf.add_argument("project", type=Path)
86
92
  gf.add_argument("-o", "--out", type=Path, help="output file (default: <project>/demo.gif)")
@@ -130,7 +136,8 @@ def main(argv: list[str] | None = None) -> int:
130
136
  return {"validate": _validate, "build": _build, "run": _run, "components": _components,
131
137
  "manifest": _manifest, "create": studio_cli.create, "tweak": studio_cli.tweak,
132
138
  "serve": studio_cli.serve, "dataset": studio_cli.dataset, "playtest": studio_cli.playtest, "studio": studio_cli.studio,
133
- "sprites": _sprites, "gif": _gif, "llm": _llm, "examples": _examples}[args.cmd](args)
139
+ "sprites": _sprites, "gif": _gif, "llm": _llm, "examples": _examples,
140
+ "demo": _demo}[args.cmd](args)
134
141
  except godot.GodotError as e:
135
142
  print(f"error: {e}", file=sys.stderr)
136
143
  if e.output:
@@ -207,8 +214,9 @@ def _llm(args) -> int:
207
214
  from . import llm_server
208
215
 
209
216
  try:
210
- return {"up": lambda: llm_server.up(args.port), "down": llm_server.down,
211
- "status": lambda: llm_server.status(args.port)}[args.action]()
217
+ return {"up": lambda: llm_server.up(args.port, backend=args.backend),
218
+ "down": lambda: llm_server.down(backend=args.backend),
219
+ "status": lambda: llm_server.status(args.port, backend=args.backend)}[args.action]()
212
220
  except (RuntimeError, subprocess.CalledProcessError) as e:
213
221
  print(f"error: {e}", file=sys.stderr)
214
222
  return 2
@@ -242,6 +250,18 @@ def _gif(args) -> int:
242
250
  return 0
243
251
 
244
252
 
253
+ def _demo(args) -> int:
254
+ from .demo import build_site
255
+
256
+ print(f"Building the demo site in {args.out}/")
257
+ games = build_site(args.out, screenshots=not args.no_screenshots)
258
+ failed = [g for g in games if g.errors]
259
+ for g in failed:
260
+ print(f"error: {g.slug}: " + "; ".join(g.errors[:3]), file=sys.stderr)
261
+ print(f"{len(games) - len(failed)}/{len(games)} games ok. Preview: python -m http.server -d {args.out}")
262
+ return 1 if failed else 0
263
+
264
+
245
265
  def _sprites(args) -> int:
246
266
  from .sprites import generate as gen, importer
247
267
  from .sprites.spriteset import SpriteError, load
@@ -0,0 +1,247 @@
1
+ """Static demo site: every example game built for the web, playable from one gallery page.
2
+
3
+ spawnpoint demo --out site
4
+
5
+ The games are the SHOWCASE examples below. Each one is built, exported as a single-threaded web build
6
+ (works on GitHub Pages, no special headers), checked in headless Chrome (no console errors, a real
7
+ screenshot for the card) and linked from index.html.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import html
13
+ import io
14
+ import json
15
+ import shutil
16
+ import tempfile
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+
20
+ from . import manifest as manifest_mod
21
+ from .build import build, load_graph
22
+ from .studio import web, webcheck
23
+
24
+ REPO = "https://github.com/leluong141996-dev/Spawn-Point"
25
+ MODEL = "https://huggingface.co/smoky1496/SpawnPoint-Coder-7B-AWQ"
26
+ # (example graph, genre tag, one-line pitch, controls). Controls are ASCII: they travel through Godot's
27
+ # export_presets.cfg into the game page.
28
+ SHOWCASE = [
29
+ ("pixel_hopper.json", "platformer",
30
+ "A Mario-style romp: stomp slimes, double-jump, dash, and collect every coin to open the door.",
31
+ "Move: arrows / WASD | Jump: Space (twice to double-jump) | Dash: Shift | Restart: R"),
32
+ ("ninja_rescue.json", "platformer",
33
+ "Mom has been caged! Throw shurikens, dodge the masked throwers, grab the key up high and set her free.",
34
+ "Move: arrows / WASD | Jump: Space | Shuriken: X / J | Dash: Shift | Restart: R"),
35
+ ("dino_eggs.json", "bubble shooter",
36
+ "The classic egg shooter: bank shots off the walls, pop three dinosaur eggs of a colour, drop whole clusters.",
37
+ "Aim: mouse or Left / Right | Shoot: click / Space / X | Restart: R"),
38
+ ("night_survivor.json", "survivor",
39
+ "Survive three minutes of the night: auto-fire, collect gems, level up and pick a new power each time.",
40
+ "Move: arrows / WASD | Dash: Shift | Pick an upgrade: 1 / 2 / 3 or click | Restart: R"),
41
+ ("pixel_arena.json", "arena shooter",
42
+ "A top-down arena: blast through robots and turrets, then reach the portal.",
43
+ "Move: WASD | Aim: mouse | Shoot: click / X | Dash: Shift | Restart: R"),
44
+ ]
45
+
46
+
47
+ def game_overlay(controls: str) -> str:
48
+ """Injected into every game page: a way back to the gallery and the controls (bottom-left, clear of
49
+ the HUD). Hidden for screenshots (?shot)."""
50
+ return (
51
+ "<style>#sp-bar{position:fixed;bottom:30px;left:8px;z-index:10;display:flex;gap:10px;align-items:center;"
52
+ "font:12px/1.4 ui-monospace,Menlo,monospace;color:#fff;background:rgba(0,0,0,.55);padding:5px 10px;"
53
+ "border-radius:6px;pointer-events:none}#sp-bar a{color:#ffec27;text-decoration:none;pointer-events:auto}"
54
+ "@media (max-width:700px){#sp-bar span{display:none}}</style>"
55
+ "<script>addEventListener('DOMContentLoaded',function(){if(location.search.indexOf('shot')>=0)return;"
56
+ "var b=document.createElement('div');b.id='sp-bar';"
57
+ "b.innerHTML='<a href=\\\"../\\\">&lt;- all games</a><span>" + controls + "</span>';"
58
+ "document.body.appendChild(b);});</script>"
59
+ )
60
+
61
+
62
+ @dataclass
63
+ class Game:
64
+ slug: str
65
+ graph: Path
66
+ title: str = ""
67
+ tag: str = ""
68
+ blurb: str = ""
69
+ controls: str = ""
70
+ errors: list[str] = field(default_factory=list)
71
+
72
+
73
+ def discover(examples: Path | None = None) -> list[Game]:
74
+ examples = examples or manifest_mod.EXAMPLES_DIR
75
+ games: list[Game] = []
76
+ for name, tag, blurb, controls in SHOWCASE:
77
+ path = examples / name
78
+ if path.exists():
79
+ data = load_graph(path)
80
+ games.append(Game(path.stem.replace("_", "-"), path, data.get("name", path.stem), tag, blurb, controls))
81
+ return games
82
+
83
+
84
+ def build_site(out: Path, games: list[Game] | None = None, screenshots: bool = True, log=print) -> list[Game]:
85
+ """Builds every game into out/<slug>/ and writes out/index.html. Returns the games (with any errors)."""
86
+ games = discover() if games is None else games
87
+ out.mkdir(parents=True, exist_ok=True)
88
+ with tempfile.TemporaryDirectory() as tmp:
89
+ for g in games:
90
+ project = Path(tmp) / g.slug
91
+ result = build(load_graph(g.graph), project, base_dir=g.graph.parent)
92
+ if not result.ok:
93
+ g.errors = [str(d) for d in result.diagnostics.errors] or ["build failed"]
94
+ log(f" {g.slug}: build failed: {g.errors[0]}")
95
+ continue
96
+ seconds = web.export(project, head_include=game_overlay(g.controls))
97
+ target = out / g.slug
98
+ shutil.rmtree(target, ignore_errors=True)
99
+ shutil.copytree(project / web.WEB_DIR, target,
100
+ ignore=shutil.ignore_patterns(".gdignore", web.VERSION_FILE))
101
+ share_engine(out, target)
102
+ log(f" {g.slug}: built (web export {seconds:.1f}s)")
103
+ built = [g for g in games if (out / g.slug / "index.html").exists()]
104
+ if screenshots:
105
+ if webcheck.find_chrome() is None:
106
+ log("Chrome not found: skipping the browser check and screenshots")
107
+ else:
108
+ check_in_browser(out, built, log)
109
+ (out / "index.html").write_text(index_html(built))
110
+ (out / ".nojekyll").write_text("") # serve files as they are
111
+ return games
112
+
113
+
114
+ ENGINE_DIR = "engine"
115
+ ENGINE_FILES = ("index.wasm", "index.js", "index.audio.worklet.js", "index.audio.position.worklet.js")
116
+
117
+
118
+ def share_engine(out: Path, game: Path) -> None:
119
+ """Every game ships the same Godot engine (~37 MB of wasm). Keep one copy in out/engine/ and point each
120
+ game at it, so the site is small and the engine is cached after the first game."""
121
+ engine = out / ENGINE_DIR
122
+ engine.mkdir(exist_ok=True)
123
+ for name in ENGINE_FILES:
124
+ src = game / name
125
+ if not src.exists():
126
+ continue
127
+ dst = engine / name
128
+ if not dst.exists():
129
+ shutil.copy2(src, dst)
130
+ elif dst.read_bytes() != src.read_bytes():
131
+ raise RuntimeError(f"{game.name}/{name} differs from the shared engine (mixed Godot versions?)")
132
+ src.unlink()
133
+ page = game / "index.html"
134
+ text = page.read_text()
135
+ patched = (text.replace('src="index.js"', f'src="../{ENGINE_DIR}/index.js"')
136
+ .replace('"executable":"index"', f'"executable":"../{ENGINE_DIR}/index","mainPack":"index.pck"')
137
+ .replace('"index.wasm":', f'"../{ENGINE_DIR}/index.wasm":'))
138
+ if patched.count(f"../{ENGINE_DIR}/") != 3:
139
+ raise RuntimeError(f"{page}: unexpected Godot web shell, cannot point it at the shared engine")
140
+ page.write_text(patched)
141
+
142
+
143
+ def check_in_browser(out: Path, games: list[Game], log=print) -> None:
144
+ """Loads every game from the finished site in headless Chrome: no console errors, plus a thumbnail."""
145
+ import functools
146
+ import threading
147
+ from http.server import ThreadingHTTPServer
148
+
149
+ port = web.free_port(8150)
150
+ httpd = ThreadingHTTPServer(("127.0.0.1", port), functools.partial(web._Handler, directory=str(out)))
151
+ threading.Thread(target=httpd.serve_forever, daemon=True).start()
152
+ try:
153
+ for g in games:
154
+ check = webcheck.check(f"http://localhost:{port}/{g.slug}/?shot", seconds=8)
155
+ g.errors = list(check.errors)
156
+ if not any("Godot Engine" in line for line in check.console):
157
+ g.errors.append("the engine did not start")
158
+ if check.screenshot:
159
+ _thumbnail(check.screenshot, out / g.slug / "thumb.png")
160
+ log(f" {g.slug}: " + ("plays in Chrome" if not g.errors else "; ".join(g.errors[:2])))
161
+ finally:
162
+ httpd.shutdown()
163
+
164
+
165
+ def _thumbnail(png: bytes, path: Path, width: int = 480) -> None:
166
+ from PIL import Image
167
+
168
+ img = Image.open(io.BytesIO(png)).convert("RGB")
169
+ img.thumbnail((width, width))
170
+ img.save(path, optimize=True)
171
+
172
+
173
+ def index_html(games: list[Game]) -> str:
174
+ cards = "\n".join(_card(g) for g in games)
175
+ return f"""<!doctype html>
176
+ <html lang="en">
177
+ <head>
178
+ <meta charset="utf-8">
179
+ <meta name="viewport" content="width=device-width, initial-scale=1">
180
+ <title>Spawn Point demos</title>
181
+ <meta name="description" content="Five Godot 4 games built with Spawn Point: platformers, a bubble shooter, a survivor. Play in your browser.">
182
+ <meta property="og:title" content="Spawn Point demos: play Godot games in your browser">
183
+ <meta property="og:description" content="A Mario-style platformer, a ninja rescue, a dinosaur-egg shooter, a survivor and an arena, all built from Spawn Point components.">
184
+ <style>
185
+ :root {{ --bg:#14121c; --panel:#1f1c2b; --ink:#f4f1ff; --muted:#a59fbf; --accent:#ffec27; --ai:#29adff; --line:#332f45; }}
186
+ @media (prefers-color-scheme: light) {{
187
+ :root:not([data-theme="dark"]) {{ --bg:#f6f4fb; --panel:#ffffff; --ink:#1b1828; --muted:#5d5875; --accent:#b8860b; --ai:#0b6fb8; --line:#e3dff0; }}
188
+ }}
189
+ * {{ box-sizing:border-box }}
190
+ body {{ margin:0; background:var(--bg); color:var(--ink); font:16px/1.55 system-ui,-apple-system,Segoe UI,sans-serif }}
191
+ a {{ color:inherit }}
192
+ header, main, footer {{ max-width:1100px; margin:0 auto; padding:0 16px }}
193
+ header {{ padding-top:40px; padding-bottom:8px }}
194
+ h1 {{ font:700 clamp(28px,5vw,44px)/1.1 ui-monospace,Menlo,monospace; margin:0 0 10px; letter-spacing:-.02em }}
195
+ h1 span {{ color:var(--accent) }}
196
+ .lede {{ color:var(--muted); max-width:720px; margin:0 0 18px }}
197
+ .links {{ display:flex; flex-wrap:wrap; gap:10px; margin-bottom:12px }}
198
+ .links a {{ text-decoration:none; border:1px solid var(--line); background:var(--panel); padding:7px 12px; border-radius:8px; font-size:14px }}
199
+ .links a:hover {{ border-color:var(--accent) }}
200
+ code {{ font:13px ui-monospace,Menlo,monospace; background:var(--panel); border:1px solid var(--line); padding:2px 6px; border-radius:5px }}
201
+ .note {{ color:var(--muted); font-size:13px; margin:4px 0 26px }}
202
+ .grid {{ display:grid; grid-template-columns:repeat(auto-fill,minmax(300px,1fr)); gap:18px; padding-bottom:40px }}
203
+ .card {{ background:var(--panel); border:1px solid var(--line); border-radius:12px; overflow:hidden; display:flex; flex-direction:column; text-decoration:none; transition:transform .12s, border-color .12s }}
204
+ .card:hover {{ transform:translateY(-2px); border-color:var(--accent) }}
205
+ .thumb {{ aspect-ratio:16/9; background:#000 center/cover no-repeat; image-rendering:pixelated; position:relative }}
206
+ .play {{ position:absolute; right:10px; bottom:10px; background:var(--accent); color:#000; font:700 13px ui-monospace,monospace; padding:5px 10px; border-radius:6px }}
207
+ .body {{ padding:12px 14px 16px; display:flex; flex-direction:column; gap:6px }}
208
+ .title {{ font-weight:700; font-size:17px }}
209
+ .tags {{ display:flex; gap:6px; flex-wrap:wrap }}
210
+ .tag {{ font:600 11px ui-monospace,monospace; text-transform:uppercase; letter-spacing:.04em; padding:2px 7px; border-radius:4px; border:1px solid var(--line); color:var(--muted) }}
211
+ .tag.ai {{ color:var(--ai); border-color:var(--ai) }}
212
+ .blurb {{ margin:2px 0 0; color:var(--ink) }}
213
+ .controls {{ margin:0; color:var(--muted); font:12px/1.5 ui-monospace,Menlo,monospace }}
214
+ footer {{ color:var(--muted); font-size:13px; padding-bottom:40px }}
215
+ </style>
216
+ </head>
217
+ <body>
218
+ <header>
219
+ <h1>Spawn<span>Point</span> demos</h1>
220
+ <p class="lede">Five games, five ways to play, all built by Spawn Point from small JSON game graphs and a
221
+ library of tested Godot 4 components: game feel, pixel art and collisions come for free. Click a card to play in
222
+ your browser, no install needed.</p>
223
+ <div class="links">
224
+ <a href="{REPO}">⭐ GitHub repo</a>
225
+ <a href="{MODEL}">🤗 SpawnPoint-Coder-7B</a>
226
+ <a href="https://pypi.org/project/spawnpoint-studio/">📦 pip install spawnpoint-studio</a>
227
+ </div>
228
+ <p class="note">Desktop + keyboard. Make your own from a sentence:
229
+ <code>spawnpoint create "a ninja platformer where you collect stars"</code></p>
230
+ </header>
231
+ <main class="grid">
232
+ {cards}
233
+ </main>
234
+ <footer>Built by the <a href="{REPO}/blob/main/.github/workflows/pages.yml">pages workflow</a> from the graphs in
235
+ <a href="{REPO}/tree/main/examples">examples/</a>. MIT licensed.</footer>
236
+ </body>
237
+ </html>
238
+ """
239
+
240
+
241
+ def _card(g: Game) -> str:
242
+ esc = html.escape
243
+ thumb = f' style="background-image:url({g.slug}/thumb.png)"'
244
+ tags = f'<span class="tag ai">{esc(g.tag)}</span>' if g.tag else ""
245
+ return (f'<a class="card" href="{g.slug}/"><div class="thumb"{thumb}><span class="play">▶ PLAY</span></div>'
246
+ f'<div class="body"><div class="title">{esc(g.title)}</div><div class="tags">{tags}</div>'
247
+ f'<p class="blurb">{esc(g.blurb)}</p><p class="controls">{esc(g.controls)}</p></div></a>')
@@ -1,4 +1,8 @@
1
- """`spawnpoint llm up|down|status`: run SpawnPoint-Coder-7B locally in the vLLM Docker image."""
1
+ """`spawnpoint llm up|down|status`: run SpawnPoint-Coder-7B locally.
2
+
3
+ Two backends: vLLM in Docker on an NVIDIA GPU (AWQ, fastest), or Ollama anywhere else (GGUF Q4_K_M:
4
+ Apple Silicon, AMD, or plain CPU). `auto` picks vLLM when Docker and an NVIDIA GPU are present.
5
+ """
2
6
 
3
7
  from __future__ import annotations
4
8
 
@@ -14,6 +18,35 @@ NAME = os.environ.get("SPAWNPOINT_LLM_CONTAINER", "spawnpoint-llm")
14
18
  IMAGE = os.environ.get("VLLM_IMAGE", "vllm/vllm-openai:v0.10.2") # CUDA 12.8: NVIDIA driver >= 570
15
19
  MODEL = os.environ.get("SPAWNPOINT_MODEL", "smoky1496/SpawnPoint-Coder-7B-AWQ")
16
20
  SERVED = "SpawnPoint-Coder-7B"
21
+ OLLAMA_MODEL = os.environ.get("SPAWNPOINT_OLLAMA_MODEL", "hf.co/smoky1496/SpawnPoint-Coder-7B-GGUF:Q4_K_M")
22
+ OLLAMA_PORT = 11434
23
+
24
+
25
+ def pick_backend(choice: str = "auto") -> str:
26
+ if choice != "auto":
27
+ return choice
28
+ if shutil.which("docker") and shutil.which("nvidia-smi"):
29
+ return "vllm"
30
+ if shutil.which("ollama"):
31
+ return "ollama"
32
+ raise RuntimeError("no local backend found: install Ollama (https://ollama.com/download) on any machine, "
33
+ "or Docker + the NVIDIA container toolkit for vLLM. A hosted API works too: "
34
+ "set SPAWNPOINT_LLM_BASE_URL / SPAWNPOINT_LLM_MODEL / SPAWNPOINT_LLM_API_KEY.")
35
+
36
+
37
+ def _ollama(*args: str, capture: bool = True) -> subprocess.CompletedProcess:
38
+ return subprocess.run(["ollama", *args], capture_output=capture, text=True)
39
+
40
+
41
+ def ollama_up(say=print) -> int:
42
+ if _ollama("list").returncode != 0:
43
+ say("Ollama is installed but its server is not running: start the Ollama app or run `ollama serve`.")
44
+ return 1
45
+ say(f"Pulling {OLLAMA_MODEL} with Ollama (first time ~4.7 GB) ...")
46
+ if _ollama("pull", OLLAMA_MODEL, capture=False).returncode != 0:
47
+ return 1
48
+ say(f"Ready: http://localhost:{OLLAMA_PORT}/v1 (model {OLLAMA_MODEL}). Spawn Point finds it automatically.")
49
+ return 0
17
50
 
18
51
 
19
52
  def _docker(*args: str, check: bool = True, capture: bool = True) -> subprocess.CompletedProcess:
@@ -39,7 +72,9 @@ def ready(port: int = 8000) -> bool:
39
72
  return False
40
73
 
41
74
 
42
- def up(port: int = 8000, max_len: int = 16384, say=print) -> int:
75
+ def up(port: int = 8000, max_len: int = 16384, say=print, backend: str = "auto") -> int:
76
+ if pick_backend(backend) == "ollama":
77
+ return ollama_up(say)
43
78
  if _exists():
44
79
  _docker("start", NAME)
45
80
  else:
@@ -62,8 +97,11 @@ def up(port: int = 8000, max_len: int = 16384, say=print) -> int:
62
97
  return 0
63
98
 
64
99
 
65
- def down(say=print) -> int:
66
- if _exists():
100
+ def down(say=print, backend: str = "auto") -> int:
101
+ if pick_backend(backend) == "ollama":
102
+ _ollama("stop", OLLAMA_MODEL)
103
+ say("Unloaded from Ollama.")
104
+ elif _exists():
67
105
  _docker("stop", NAME)
68
106
  say("Stopped.")
69
107
  else:
@@ -71,7 +109,12 @@ def down(say=print) -> int:
71
109
  return 0
72
110
 
73
111
 
74
- def status(port: int = 8000, say=print) -> int:
112
+ def status(port: int = 8000, say=print, backend: str = "auto") -> int:
113
+ if pick_backend(backend) == "ollama":
114
+ have = OLLAMA_MODEL.split(":")[0].lower() in _ollama("list").stdout.lower()
115
+ say(f"Ollama model {OLLAMA_MODEL}: {'pulled' if have else 'not pulled'}; "
116
+ f"API on :{OLLAMA_PORT}: {'ready' if ready(OLLAMA_PORT) else 'not responding'}")
117
+ return 0
75
118
  say(f"container {NAME}: {'running' if _exists() and _running() else 'stopped'}; "
76
119
  f"API on :{port}: {'ready' if ready(port) else 'not responding'}")
77
120
  return 0
@@ -22,6 +22,7 @@ class ProfileStats:
22
22
  jumps_missed: int
23
23
  pickup_rate: float
24
24
  stuck: int
25
+ hits: int = 0 # times the player took damage (dying or not)
25
26
 
26
27
  @property
27
28
  def win_rate(self) -> float:
@@ -71,6 +72,7 @@ def aggregate(runs: list[dict], goal_entities: set[str] | None = None) -> Report
71
72
  jumps_missed=sum(len(r["jumps"]["missed"]) for r in rs),
72
73
  pickup_rate=round(sum(r["pickups"]["collected"] for r in rs) / total, 3) if total else 1.0,
73
74
  stuck=sum(len(r["stuck"]) for r in rs),
75
+ hits=sum(len(r.get("damage", [])) for r in rs),
74
76
  )
75
77
  return Report(stats, runs, _level_issues(runs, stats, goal_entities))
76
78
 
@@ -118,8 +120,9 @@ def heatmap(plan: dict, runs: list[dict], out: Path, cell: int = 12) -> Path:
118
120
  else:
119
121
  d.rectangle((box[0], box[1], box[2], box[1] + 2), fill=(171, 82, 54))
120
122
  ts = plan["tile_size"]
123
+ players = {e["name"] for e in plan["entities"] if any(c["name"] == "PlayerInput" for c in e["components"])}
121
124
  for p in plan["placements"]:
122
- if p["entity"] == "Player":
125
+ if p["entity"] in players:
123
126
  cx, cy = p["x"] / ts * cell, p["y"] / ts * cell
124
127
  d.ellipse((cx - 4, cy - 4, cx + 4, cy + 4), outline=(0, 228, 54), width=2)
125
128
  causes = sorted({dd["cause"] for r in runs for dd in r["deaths"]})