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.
- {spawnpoint_studio-0.1.1/compiler/spawnpoint_studio.egg-info → spawnpoint_studio-0.2.0}/PKG-INFO +22 -10
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/README.md +21 -9
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/build.py +3 -1
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/cli.py +24 -4
- spawnpoint_studio-0.2.0/compiler/spawnpoint/demo.py +247 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/llm_server.py +48 -5
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/playtest/report.py +4 -1
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/playtest/runner.py +16 -1
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/playtest/tuner.py +43 -14
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/sprites/generate.py +12 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/assemble.py +47 -5
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/brief.py +16 -1
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/llm.py +32 -7
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/web.py +2 -2
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0/compiler/spawnpoint_studio.egg-info}/PKG-INFO +22 -10
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint_studio.egg-info/SOURCES.txt +9 -0
- spawnpoint_studio-0.2.0/compiler/tests/test_demo.py +79 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_playtest.py +24 -7
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_studio.py +63 -0
- spawnpoint_studio-0.2.0/examples/dino_eggs.json +99 -0
- spawnpoint_studio-0.2.0/examples/night_survivor.json +428 -0
- spawnpoint_studio-0.2.0/examples/ninja_rescue.json +351 -0
- spawnpoint_studio-0.2.0/godot/addons/spawn_point/components/bubble_board_component.gd +413 -0
- spawnpoint_studio-0.2.0/godot/addons/spawn_point/components/experience_component.gd +53 -0
- spawnpoint_studio-0.2.0/godot/addons/spawn_point/components/level_up_menu_component.gd +225 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/spawner_component.gd +23 -1
- spawnpoint_studio-0.2.0/godot/addons/spawn_point/components/xp_orb_component.gd +67 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/manifest.json +395 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/hud.gd +48 -1
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/world.gd +43 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/playtest.gd +57 -12
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/record.gd +1 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/pyproject.toml +1 -1
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/LICENSE +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/MANIFEST.in +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/__init__.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/analyze.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/cockpit/__init__.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/cockpit/server.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/cockpit/static/app.css +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/cockpit/static/app.js +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/cockpit/static/index.html +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/diagnostics.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/expr.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/gif.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/godot.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/graph.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/level.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/manifest.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/playtest/__init__.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/project.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/py.typed +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/rules.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/sfx.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/sprites/__init__.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/sprites/ai.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/sprites/contour.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/sprites/importer.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/sprites/resolve.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/sprites/spriteset.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/sprites/tiles.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/__init__.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/creative.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/dataset.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/edits.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/levelgen.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/orchestrator.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/prompts.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio/webcheck.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint/studio_cli.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint_studio.egg-info/dependency_links.txt +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint_studio.egg-info/entry_points.txt +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint_studio.egg-info/requires.txt +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/spawnpoint_studio.egg-info/top_level.txt +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/conftest.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_ai_sprites.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_analyze.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_cockpit.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_e2e.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_expr.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_gif.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_godot_library.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_graph.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_level.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_live_llm.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_manifest.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_rules.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_sfx.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_sprites.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/compiler/tests/test_web.py +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/examples/coin_hopper.json +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/examples/neon_arena.json +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/examples/pixel_arena.json +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/examples/pixel_hopper.json +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/aim_component.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/animator_component.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/chase_ai_component.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/collectible_component.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/gate_component.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/health_component.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/hitbox_component.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/hurtbox_component.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/juice_component.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/patrol_ai_component.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/platformer_movement_component.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/player_input_component.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/projectile_component.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/stomp_component.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/top_down_movement_component.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/trigger_zone_component.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/components/weapon_component.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/camera_rig.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/cockpit.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/entity_core.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/fx.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/game_state.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/level.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/sfx.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/runtime/sp.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/shaders/flash.gdshader +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/shaders/floor_grid.gdshader +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/build.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/build.tscn +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/capture.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/capture.tscn +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/dump_manifest.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/dump_manifest.tscn +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/playtest.tscn +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/playtest_nav.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/record.tscn +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/smoke.gd +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/godot/addons/spawn_point/tools/smoke.tscn +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/setup.cfg +0 -0
- {spawnpoint_studio-0.1.1 → spawnpoint_studio-0.2.0}/setup.py +0 -0
{spawnpoint_studio-0.1.1/compiler/spawnpoint_studio.egg-info → spawnpoint_studio-0.2.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spawnpoint-studio
|
|
3
|
-
Version: 0.
|
|
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
|
[](https://github.com/leluong141996-dev/Spawn-Point/actions/workflows/ci.yml)
|
|
30
30
|
[](https://pypi.org/project/spawnpoint-studio/)
|
|
31
31
|
[](https://huggingface.co/smoky1496/SpawnPoint-Coder-7B-AWQ)
|
|
32
|
+
[](https://leluong141996-dev.github.io/Spawn-Point/)
|
|
32
33
|
[](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
|
|
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 #
|
|
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?
|
|
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
|
-
|
|
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. `
|
|
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:
|
|
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
|
|
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
|
[](https://github.com/leluong141996-dev/Spawn-Point/actions/workflows/ci.yml)
|
|
6
6
|
[](https://pypi.org/project/spawnpoint-studio/)
|
|
7
7
|
[](https://huggingface.co/smoky1496/SpawnPoint-Coder-7B-AWQ)
|
|
8
|
+
[](https://leluong141996-dev.github.io/Spawn-Point/)
|
|
8
9
|
[](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
|
|
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 #
|
|
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?
|
|
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
|
-
|
|
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. `
|
|
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:
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
211
|
-
"
|
|
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=\\\"../\\\"><- 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
|
|
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
|
|
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"]
|
|
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"]})
|