spriteforge 0.1.1__py3-none-any.whl

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 (218) hide show
  1. spriteforge/__init__.py +1 -0
  2. spriteforge/__main__.py +3 -0
  3. spriteforge/agents/__init__.py +0 -0
  4. spriteforge/agents/assets.py +109 -0
  5. spriteforge/agents/cancellation.py +12 -0
  6. spriteforge/agents/director.py +50 -0
  7. spriteforge/agents/models.py +63 -0
  8. spriteforge/agents/roles/art-director.md +15 -0
  9. spriteforge/agents/roles/art-reviewer.md +14 -0
  10. spriteforge/agents/roles/director.md +14 -0
  11. spriteforge/agents/roles/game-designer.md +15 -0
  12. spriteforge/agents/roles/level-tile-designer.md +16 -0
  13. spriteforge/agents/roles/librarian.md +13 -0
  14. spriteforge/agents/roles/menu-designer.md +15 -0
  15. spriteforge/agents/roles/pixel-artist.md +14 -0
  16. spriteforge/agents/roles/programmer.md +14 -0
  17. spriteforge/agents/roles/qa-playtester.md +13 -0
  18. spriteforge/agents/roles/sound-director.md +14 -0
  19. spriteforge/agents/roles.py +39 -0
  20. spriteforge/agents/router.py +233 -0
  21. spriteforge/agents/router_eval.py +145 -0
  22. spriteforge/agents/routing_state.py +133 -0
  23. spriteforge/agents/runtime.py +430 -0
  24. spriteforge/agents/workflow.py +389 -0
  25. spriteforge/app/__init__.py +0 -0
  26. spriteforge/app/agents_view.py +248 -0
  27. spriteforge/app/chat.py +114 -0
  28. spriteforge/app/extensions.py +115 -0
  29. spriteforge/app/index.html +279 -0
  30. spriteforge/app/local_only.py +49 -0
  31. spriteforge/app/server.py +358 -0
  32. spriteforge/app/static/assets/index-L2s1wlcp.css +1 -0
  33. spriteforge/app/static/assets/index-aBfzgLQl.js +71 -0
  34. spriteforge/app/static/assets/press-start-2p-latin-400-normal-BKfmdmKK.woff +0 -0
  35. spriteforge/app/static/assets/press-start-2p-latin-400-normal-xj9lRvVs.woff2 +0 -0
  36. spriteforge/app/static/index.html +13 -0
  37. spriteforge/app/static/press-start-2p-LICENSE.txt +93 -0
  38. spriteforge/app/studio.py +278 -0
  39. spriteforge/app/workers.py +73 -0
  40. spriteforge/cli.py +241 -0
  41. spriteforge/config.py +193 -0
  42. spriteforge/config.yaml +157 -0
  43. spriteforge/core/__init__.py +0 -0
  44. spriteforge/core/acting.py +16 -0
  45. spriteforge/core/cost.py +236 -0
  46. spriteforge/core/credentials.py +24 -0
  47. spriteforge/core/demo.py +79 -0
  48. spriteforge/core/forms.py +20 -0
  49. spriteforge/core/gates.py +175 -0
  50. spriteforge/core/security.py +42 -0
  51. spriteforge/core/state.py +222 -0
  52. spriteforge/core/team.py +15 -0
  53. spriteforge/core/usage.py +68 -0
  54. spriteforge/game/__init__.py +0 -0
  55. spriteforge/game/actors.py +377 -0
  56. spriteforge/game/agent_tools.py +178 -0
  57. spriteforge/game/art.py +2097 -0
  58. spriteforge/game/art_checks.py +190 -0
  59. spriteforge/game/art_review.py +518 -0
  60. spriteforge/game/build.py +134 -0
  61. spriteforge/game/bundle.py +1178 -0
  62. spriteforge/game/cli.py +119 -0
  63. spriteforge/game/codegen.py +433 -0
  64. spriteforge/game/codeinfo.py +154 -0
  65. spriteforge/game/dbsource.py +137 -0
  66. spriteforge/game/design.py +342 -0
  67. spriteforge/game/export.py +62 -0
  68. spriteforge/game/gamedb.py +191 -0
  69. spriteforge/game/image_cache.py +61 -0
  70. spriteforge/game/lessons.py +259 -0
  71. spriteforge/game/level_gen.py +118 -0
  72. spriteforge/game/level_preview.py +97 -0
  73. spriteforge/game/levels.py +934 -0
  74. spriteforge/game/loops.py +38 -0
  75. spriteforge/game/menus.py +712 -0
  76. spriteforge/game/mock_game.js +109 -0
  77. spriteforge/game/mock_platformer.js +58 -0
  78. spriteforge/game/motion.py +881 -0
  79. spriteforge/game/pipeline.py +1768 -0
  80. spriteforge/game/plan.py +502 -0
  81. spriteforge/game/playtest.py +318 -0
  82. spriteforge/game/qa.py +664 -0
  83. spriteforge/game/qa_checks.py +223 -0
  84. spriteforge/game/qa_runtime.py +565 -0
  85. spriteforge/game/reach.py +807 -0
  86. spriteforge/game/record.py +138 -0
  87. spriteforge/game/research.py +262 -0
  88. spriteforge/game/revision.py +225 -0
  89. spriteforge/game/sdk/API.md +104 -0
  90. spriteforge/game/sdk/FLOW.md +19 -0
  91. spriteforge/game/sdk/LEVELS.md +65 -0
  92. spriteforge/game/sdk/sf.js +418 -0
  93. spriteforge/game/sdk/sfflow.js +442 -0
  94. spriteforge/game/sdk/sflevel.js +683 -0
  95. spriteforge/game/sdk/sfsound-editor.js +225 -0
  96. spriteforge/game/sdk/sfsound.js +260 -0
  97. spriteforge/game/sdk/sounds.html +67 -0
  98. spriteforge/game/sounds.py +793 -0
  99. spriteforge/game/story.py +188 -0
  100. spriteforge/game/structure.py +98 -0
  101. spriteforge/game/team_reports.py +394 -0
  102. spriteforge/game/tilesets.py +70 -0
  103. spriteforge/game/title_art.py +186 -0
  104. spriteforge/genres/__init__.py +0 -0
  105. spriteforge/library/__init__.py +15 -0
  106. spriteforge/library/cli.py +126 -0
  107. spriteforge/library/describe.py +242 -0
  108. spriteforge/library/looks.py +154 -0
  109. spriteforge/library/models.py +79 -0
  110. spriteforge/library/store.py +422 -0
  111. spriteforge/library/tilesets/README.md +32 -0
  112. spriteforge/library/tilesets/gandalf-seasons.json +111 -0
  113. spriteforge/library/tilesets/pixel-crawler-dungeon.json +66 -0
  114. spriteforge/mcp/__init__.py +0 -0
  115. spriteforge/mcp/cli.py +58 -0
  116. spriteforge/mcp/client.py +57 -0
  117. spriteforge/mcp/filesystem_read.py +32 -0
  118. spriteforge/mcp/gamedb.py +56 -0
  119. spriteforge/mcp/library.py +179 -0
  120. spriteforge/mcp/memory.py +37 -0
  121. spriteforge/mcp/pixel_tools.py +145 -0
  122. spriteforge/mcp/registry.py +145 -0
  123. spriteforge/mcp/service.py +224 -0
  124. spriteforge/mcp/templates/context7.json +6 -0
  125. spriteforge/mcp/templates/fetch.json +6 -0
  126. spriteforge/mcp/templates/filesystem-read.json +6 -0
  127. spriteforge/mcp/templates/pixel-tools.json +6 -0
  128. spriteforge/memory/README.md +10 -0
  129. spriteforge/memory/__init__.py +0 -0
  130. spriteforge/memory/cli.py +131 -0
  131. spriteforge/memory/okf/__init__.py +1 -0
  132. spriteforge/memory/okf/catalog.py +331 -0
  133. spriteforge/memory/okf/codec.py +63 -0
  134. spriteforge/memory/okf/git.py +104 -0
  135. spriteforge/memory/okf/indexes.py +50 -0
  136. spriteforge/memory/okf/models.py +114 -0
  137. spriteforge/memory/okf/search.py +53 -0
  138. spriteforge/memory/okf/seed.py +81 -0
  139. spriteforge/memory/okf/seeds/genres.yaml +121 -0
  140. spriteforge/memory/okf/seeds/other.yaml +150 -0
  141. spriteforge/memory/okf/store.py +402 -0
  142. spriteforge/memory/okf/validate.py +115 -0
  143. spriteforge/pixel/__init__.py +17 -0
  144. spriteforge/pixel/atlas.py +110 -0
  145. spriteforge/pixel/grid.py +174 -0
  146. spriteforge/pixel/palette.py +156 -0
  147. spriteforge/pixel/palettes/dungeon.json +9 -0
  148. spriteforge/pixel/postprocess.py +170 -0
  149. spriteforge/pixel/preview.py +40 -0
  150. spriteforge/pixel/schemas.py +139 -0
  151. spriteforge/pixel/source.py +237 -0
  152. spriteforge/pixel/terrain.py +48 -0
  153. spriteforge/pixel/validate.py +138 -0
  154. spriteforge/prompts/anim_frame.txt +1 -0
  155. spriteforge/prompts/character.txt +1 -0
  156. spriteforge/prompts/item.txt +1 -0
  157. spriteforge/prompts/tile.txt +1 -0
  158. spriteforge/prompts/ui.txt +1 -0
  159. spriteforge/providers/__init__.py +0 -0
  160. spriteforge/providers/base.py +97 -0
  161. spriteforge/providers/http.py +38 -0
  162. spriteforge/providers/mock.py +111 -0
  163. spriteforge/providers/muse_image.py +60 -0
  164. spriteforge/providers/muse_search.py +91 -0
  165. spriteforge/providers/muse_spark.py +66 -0
  166. spriteforge/skills/__init__.py +0 -0
  167. spriteforge/skills/builtin/LICENSE-awesome-gamedev-agent-skills.txt +201 -0
  168. spriteforge/skills/builtin/NOTICE-awesome-gamedev-agent-skills.txt +15 -0
  169. spriteforge/skills/builtin/THIRD_PARTY.md +21 -0
  170. spriteforge/skills/builtin/audio-design/SKILL.md +190 -0
  171. spriteforge/skills/builtin/game-feel/SKILL.md +174 -0
  172. spriteforge/skills/builtin/game-ui-ux/SKILL.md +167 -0
  173. spriteforge/skills/builtin/level-design/SKILL.md +169 -0
  174. spriteforge/skills/builtin/phaser-arcade-physics/SKILL.md +17 -0
  175. spriteforge/skills/builtin/physics-tuning/SKILL.md +164 -0
  176. spriteforge/skills/builtin/pixel-art-prompting/SKILL.md +12 -0
  177. spriteforge/skills/builtin/platformer/SKILL.md +153 -0
  178. spriteforge/skills/builtin/puzzle/SKILL.md +143 -0
  179. spriteforge/skills/builtin/sprite-animation-frames/SKILL.md +11 -0
  180. spriteforge/skills/builtin/tileset-design/SKILL.md +10 -0
  181. spriteforge/skills/catalog.py +200 -0
  182. spriteforge/skills/cli.py +41 -0
  183. spriteforge/stages/__init__.py +0 -0
  184. spriteforge/stages/asset_context.py +301 -0
  185. spriteforge/stages/asset_models.py +192 -0
  186. spriteforge/stages/asset_render.py +366 -0
  187. spriteforge/stages/asset_review.py +140 -0
  188. spriteforge/stages/asset_store.py +94 -0
  189. spriteforge/stages/chat_changes.py +236 -0
  190. spriteforge/stages/concept.py +42 -0
  191. spriteforge/stages/game.py +1699 -0
  192. spriteforge/stages/game_confirm.py +112 -0
  193. spriteforge/stages/game_final.py +220 -0
  194. spriteforge/stages/game_record.py +246 -0
  195. spriteforge/stages/gameplay.py +218 -0
  196. spriteforge/stages/intake.py +372 -0
  197. spriteforge/stages/preview_assets.py +212 -0
  198. spriteforge/stages/preview_models.py +167 -0
  199. spriteforge/stages/preview_render.py +169 -0
  200. spriteforge/stages/preview_repair.py +61 -0
  201. spriteforge/stages/preview_review.py +169 -0
  202. spriteforge/stages/production.py +170 -0
  203. spriteforge/stages/references.py +105 -0
  204. spriteforge/stages/rewind.py +128 -0
  205. spriteforge/stages/workflow.py +440 -0
  206. spriteforge/templates/phaser/audit.js +45 -0
  207. spriteforge/templates/phaser/font.png +0 -0
  208. spriteforge/templates/phaser/font.xml +1 -0
  209. spriteforge/templates/phaser/preview.html +9 -0
  210. spriteforge/templates/phaser/room.js +49 -0
  211. spriteforge/templates/phaser/vendor/LICENSE.md +20 -0
  212. spriteforge/templates/phaser/vendor/phaser.min.js +1 -0
  213. spriteforge-0.1.1.dist-info/METADATA +364 -0
  214. spriteforge-0.1.1.dist-info/RECORD +218 -0
  215. spriteforge-0.1.1.dist-info/WHEEL +5 -0
  216. spriteforge-0.1.1.dist-info/entry_points.txt +2 -0
  217. spriteforge-0.1.1.dist-info/licenses/LICENSE +27 -0
  218. spriteforge-0.1.1.dist-info/top_level.txt +1 -0
spriteforge/cli.py ADDED
@@ -0,0 +1,241 @@
1
+ import json
2
+ import threading
3
+ import webbrowser
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ import uvicorn
8
+
9
+ from spriteforge.app.server import create_app
10
+ from spriteforge.config import init_workspace, load_config
11
+ from spriteforge.core.credentials import load_credentials
12
+ from spriteforge.core.state import Store
13
+ from spriteforge.game.cli import app as game_app
14
+ from spriteforge.library.cli import app as library_app
15
+ from spriteforge.mcp.cli import app as mcp_app
16
+ from spriteforge.memory.cli import app as memory_app
17
+ from spriteforge.skills.cli import app as skills_app
18
+
19
+ app = typer.Typer()
20
+ app.add_typer(memory_app, name="memory")
21
+ app.add_typer(skills_app, name="skills")
22
+ app.add_typer(mcp_app, name="mcp")
23
+ app.add_typer(library_app, name="library")
24
+ app.add_typer(game_app, name="game")
25
+
26
+
27
+ def setup(config: Path | None, workspace: Path | None):
28
+ load_credentials()
29
+ cfg = load_config(config)
30
+ root = init_workspace(workspace or cfg.workspace)
31
+ return cfg, Store(root / "state.sqlite3")
32
+
33
+
34
+ def serve(store, run_id, port, open_browser, cfg=None):
35
+ url = f"http://127.0.0.1:{port}"
36
+ typer.echo(f"SpriteForge Studio · {url}" + (f" · run {run_id}" if run_id else ""))
37
+ if open_browser:
38
+ timer = threading.Timer(1.5, webbrowser.open, args=(url,))
39
+ timer.daemon = True
40
+ timer.start()
41
+ app = create_app(store, run_id, cfg=cfg)
42
+ uvicorn.run(app, host="127.0.0.1", port=port, access_log=False)
43
+
44
+
45
+ @app.callback(invoke_without_command=True)
46
+ def main(ctx: typer.Context):
47
+ """SpriteForge. With no command, opens the studio in the browser."""
48
+ if ctx.invoked_subcommand is None:
49
+ studio()
50
+
51
+
52
+ @app.command()
53
+ def studio(
54
+ config: Path | None = None,
55
+ workspace: Path | None = None,
56
+ port: int = 8765,
57
+ open_browser: bool = True,
58
+ ):
59
+ """Open the studio: every project in the workspace, chat-first (the default command)."""
60
+ cfg, store = setup(config, workspace)
61
+ serve(store, None, port, open_browser, cfg)
62
+
63
+
64
+ @app.command()
65
+ def start(
66
+ mock: bool = False,
67
+ agents: bool = False,
68
+ intake: bool = False,
69
+ assets: bool = False,
70
+ legacy: bool = False,
71
+ goal: str = "",
72
+ config: Path | None = None,
73
+ workspace: Path | None = None,
74
+ port: int = 8765,
75
+ open_browser: bool = True,
76
+ ):
77
+ """Start a local UI; --agents uses the Phaser gameplay preview and human gates."""
78
+ cfg, store = setup(config, workspace)
79
+ if legacy:
80
+ typer.echo("DEPRECATED: --legacy enables the old concept/demo preview.", err=True)
81
+ if intake or assets or (agents and not legacy):
82
+ from spriteforge.stages.workflow import start_run
83
+
84
+ run = start_run(
85
+ store,
86
+ cfg,
87
+ mock=mock,
88
+ assets=assets,
89
+ gameplay_preview=not legacy,
90
+ legacy=legacy,
91
+ goal=goal,
92
+ )
93
+ serve(store, run.id, port, open_browser, cfg)
94
+ return
95
+ if agents and not mock:
96
+ raise typer.BadParameter("The agent acceptance workflow requires --mock")
97
+ if agents:
98
+ from spriteforge.agents.workflow import start_run
99
+
100
+ run = start_run(
101
+ store,
102
+ goal or "A top-down dungeon crawler with a hero and stone rooms",
103
+ cfg.budget,
104
+ router=cfg.router,
105
+ )
106
+ store.append_event(run.id, "Director", "preview.deprecated", {"legacy": True})
107
+ serve(store, run.id, port, open_browser, cfg)
108
+ return
109
+ run = (
110
+ store.create_run({"workflow": "mock-demo-v1", "budget": cfg.budget.model_dump()})
111
+ if mock
112
+ else None
113
+ )
114
+ serve(store, run.id if run else None, port, open_browser, cfg)
115
+
116
+
117
+ @app.command()
118
+ def router_eval(
119
+ live: bool = False,
120
+ max_calls: int = typer.Option(5, min=1, max=20),
121
+ offset: int = typer.Option(0, min=0),
122
+ cases: Path = Path("tests/router_cases.yaml"),
123
+ config: Path | None = None,
124
+ workspace: Path | None = None,
125
+ ):
126
+ """Report router precision/recall; --live makes at most --max-calls Spark requests."""
127
+ from uuid import uuid4
128
+
129
+ from spriteforge.agents.router_eval import evaluate, read_cases
130
+ from spriteforge.core.cost import Budget
131
+ from spriteforge.core.state import Artifact
132
+ from spriteforge.providers.mock import MockLLMProvider
133
+ from spriteforge.providers.muse_spark import MuseSparkProvider
134
+
135
+ cfg, store = setup(config, workspace)
136
+ suite = read_cases(cases)[offset:]
137
+ limits = (
138
+ cfg.budget.model_copy(
139
+ update={
140
+ "max_total_calls": min(
141
+ cfg.budget.max_total_calls
142
+ if cfg.budget.max_total_calls is not None
143
+ else max_calls,
144
+ max_calls,
145
+ )
146
+ }
147
+ )
148
+ if live
149
+ else cfg.budget
150
+ )
151
+ run = store.create_run(
152
+ {
153
+ "workflow": "router-eval",
154
+ "budget": limits.model_dump(),
155
+ "settings": cfg.model_dump(mode="json"),
156
+ }
157
+ )
158
+ budget = Budget(store, run.id, limits)
159
+ provider = MuseSparkProvider(cfg.llm, budget) if live else MockLLMProvider(budget)
160
+ try:
161
+ report = evaluate(
162
+ provider, suite, cfg.router, store.path.parent, live=live, max_calls=max_calls
163
+ )
164
+ finally:
165
+ if live:
166
+ provider.close()
167
+ store.save_artifact(
168
+ Artifact(
169
+ id=uuid4().hex,
170
+ run_id=run.id,
171
+ name="router-eval.json",
172
+ mime="application/json",
173
+ data=report.model_dump_json(indent=2).encode(),
174
+ ),
175
+ "Router",
176
+ "router.eval",
177
+ )
178
+ with store.connect() as db:
179
+ db.execute("UPDATE runs SET status='completed' WHERE id=?", (run.id,))
180
+ typer.echo(f"{report.mode}: {report.evaluated} cases, {report.skipped} skipped")
181
+ typer.echo(f"Spark calls attempted: {report.evaluated if live else 0} (cap {max_calls})")
182
+ typer.echo(f"Precision: {report.precision:.3f} Recall: {report.recall:.3f}")
183
+ typer.echo(f"Failures: {len(report.failures)}")
184
+ for failure in report.failures:
185
+ typer.echo(failure.model_dump_json())
186
+ typer.echo(f"Run: {run.id} (router-eval.json contains all results)")
187
+
188
+
189
+ @app.command()
190
+ def report(id: str, config: Path | None = None, workspace: Path | None = None):
191
+ """Report successful image/Spark usage and failed attempts from the durable ledger."""
192
+ from spriteforge.core.usage import usage_report
193
+
194
+ _, store = setup(config, workspace)
195
+ typer.echo(usage_report(store, id).model_dump_json(indent=2))
196
+
197
+
198
+ @app.command()
199
+ def runs(config: Path | None = None, workspace: Path | None = None):
200
+ """List runs persisted in the workspace database."""
201
+ _, store = setup(config, workspace)
202
+ for run in store.runs():
203
+ typer.echo(f"{run.id} {run.status:10} checkpoint={run.cursor}")
204
+
205
+
206
+ @app.command()
207
+ def resume(
208
+ id: str,
209
+ config: Path | None = None,
210
+ workspace: Path | None = None,
211
+ port: int = 8765,
212
+ open_browser: bool = True,
213
+ assets: bool = False,
214
+ legacy: bool = False,
215
+ ):
216
+ """Resume from the database checkpoint and show its timeline."""
217
+ cfg, store = setup(config, workspace)
218
+ try:
219
+ run = store.get_run(id)
220
+ except KeyError:
221
+ raise typer.BadParameter(f"Run {id} does not exist") from None
222
+ old_preview = run.config.get("workflow") == "agent-runtime-mock-v1" or (
223
+ run.config.get("workflow") == "intake-concept-v1" and not run.config.get("gameplay_preview")
224
+ )
225
+ if old_preview:
226
+ if not legacy:
227
+ raise typer.BadParameter("This deprecated preview requires explicit --legacy")
228
+ typer.echo("DEPRECATED: --legacy enables the old concept/demo preview.", err=True)
229
+ with store.connect() as db:
230
+ db.execute(
231
+ "UPDATE runs SET config=? WHERE id=?",
232
+ (json.dumps({**run.config, "legacy_preview": True}), id),
233
+ )
234
+ store._event(db, id, "Director", "preview.deprecated", {"legacy": True})
235
+ if assets:
236
+ from spriteforge.stages.production import enable_production
237
+
238
+ enable_production(store, id)
239
+ if run.status == "failed" and run.config.get("workflow") == "intake-concept-v1":
240
+ store.retry_failed(id)
241
+ serve(store, id, port, open_browser, cfg)
spriteforge/config.py ADDED
@@ -0,0 +1,193 @@
1
+ from pathlib import Path
2
+ from typing import Literal
3
+
4
+ import yaml
5
+ from pydantic import BaseModel, ConfigDict, Field, HttpUrl, model_validator
6
+
7
+
8
+ class Schema(BaseModel):
9
+ model_config = ConfigDict(extra="forbid")
10
+
11
+
12
+ class ImageConfig(Schema):
13
+ base_url: HttpUrl
14
+ model: str
15
+ generate_path: str
16
+ edit_path: str
17
+ # Sizes Muse Image has accepted live; requests pick the one nearest the subject's aspect.
18
+ sizes: list[tuple[int, int]] = Field(default_factory=lambda: [(1024, 1024)], min_length=1)
19
+
20
+
21
+ Effort = Literal["minimal", "low", "medium", "high", "xhigh", "max"]
22
+
23
+
24
+ class LLMConfig(Schema):
25
+ base_url: HttpUrl
26
+ model: str
27
+ chat_path: str
28
+ # Spark `reasoning_effort` per task (intake, art_plan, revision, design, code); a task
29
+ # not listed sends none and the model picks its own effort.
30
+ reasoning: dict[str, Effort] = Field(default_factory=dict)
31
+
32
+
33
+ class BudgetConfig(Schema):
34
+ max_image_calls: int = Field(default=150, ge=0)
35
+ max_calls_per_model: int = Field(default=200, ge=0)
36
+ max_total_calls: int | None = Field(default=None, ge=0)
37
+ image_cost: float = Field(default=0.01, ge=0)
38
+ # Meta standard Spark rates: https://dev.meta.ai/docs/pricing-rate-limits
39
+ spark_input_per_million: float = Field(default=1.25, ge=0)
40
+ spark_cached_input_per_million: float = Field(default=0.15, ge=0)
41
+ spark_output_per_million: float = Field(default=4.25, ge=0)
42
+ # Muse Spark web search: "$2.50 per 1,000 search queries, in addition to the request's
43
+ # token cost" (https://dev.meta.ai/docs/pricing-rate-limits, 2026-11-02).
44
+ web_search_cost: float = Field(default=0.0025, ge=0)
45
+ max_estimated_cost: float = Field(default=5, ge=0)
46
+ # True (config.yaml): each agent may spend `max_agent_cost` (USD, estimated) since the
47
+ # human's last action (a gate answer, Go back, Continue); the caps above do not apply. Its
48
+ # loops run until that is used up or a try brings no improvement (game.patience), and work
49
+ # the user asks for always starts. False: the caps above, per run (the legacy concept and
50
+ # production stages, budgets made in code).
51
+ per_request: bool = False
52
+ max_agent_cost: float = Field(default=10, ge=0)
53
+
54
+
55
+ class ResearchConfig(Schema):
56
+ """Muse Spark web search for the Game Designer's research (L2.2), on the Responses API."""
57
+
58
+ path: str = "/v1/responses"
59
+ # Built-in tool calls per research (searches and page opens; `max_tool_calls`).
60
+ max_searches: int = Field(default=8, ge=1, le=20)
61
+ search_context_size: Literal["low", "medium", "high"] = "medium"
62
+
63
+
64
+ class MemoryConfig(Schema):
65
+ min_score: float = Field(default=0, ge=0, allow_inf_nan=False)
66
+ relative_score: float = Field(default=0.5, ge=0, le=1)
67
+
68
+
69
+ class PreviewConfig(Schema):
70
+ max_image_calls: int = Field(default=16, ge=0, le=150)
71
+ mood_board: bool = True
72
+ # game: after G1 Spark writes a design (G2 gate) and then the game's code (G4 playtest
73
+ # gate); art comes from the library first, Muse Image for the rest (`game` settings).
74
+ # muse: the original Muse Image building-block preview.
75
+ mode: Literal["game", "muse"] = "game"
76
+
77
+ @model_validator(mode="before")
78
+ @classmethod
79
+ def legacy_kit_settings(cls, data):
80
+ """Runs saved before kits were removed: mode 'kit' and preview.max_art_calls."""
81
+ if isinstance(data, dict):
82
+ data = {k: v for k, v in data.items() if k != "max_art_calls"}
83
+ if data.get("mode") == "kit":
84
+ data["mode"] = "game"
85
+ return data
86
+
87
+
88
+ class GameConfig(Schema):
89
+ """Prompt-driven games: Spark writes the code, the harness checks and playtests it."""
90
+
91
+ # Agent loops (Programmer repairs, Muse redraws, QA check fixes, Menu and Level retries,
92
+ # Art Reviewer rounds) try again while they improve: they stop after `patience` tries in a
93
+ # row without improvement, or when the agent's allowance (budget.max_agent_cost) is used
94
+ # up. The counts below are optional fixed limits on top (None: no fixed limit).
95
+ patience: int = Field(default=2, ge=1, le=10)
96
+ max_repairs: int | None = Field(default=None, ge=0) # Programmer fix rounds after failed checks
97
+ max_art_calls: int | None = Field(default=None, ge=0) # Muse Image calls per build
98
+ image_workers: int = Field(default=4, ge=1, le=8) # Muse Image calls in flight at once
99
+ image_cache: bool = True # identical Muse requests reuse workspace/cache/muse-image
100
+ # The game's screen: 320x180 design pixels times screen_scale (6: 1920x1080, a laptop's
101
+ # full-HD screen). Every sprite, tile, text and distance is that many times bigger, so a
102
+ # sprite keeps more of the detail Muse drew and words stay readable.
103
+ screen_scale: int = Field(default=6, ge=1, le=8)
104
+ # The Level/Tile Designer studies the level design of similar platformers with web search
105
+ # (one search call per game, before the first level) and its level prompts read it.
106
+ level_research: bool = True
107
+ # Muse Image's strength for every first draw (the API has low and high; redraws and the
108
+ # Art Reviewer's fixes use high too).
109
+ image_reasoning: Literal["low", "high"] = "high"
110
+ art_retry: int | None = Field(default=None, ge=0) # redraws of a Muse frame that fails checks
111
+ native_grid_confidence: float = Field(default=0.6, ge=0, le=1) # big-pixel grid read-back
112
+ # Muse sprites as sheets: frames per animation row (1 = one image per sprite)
113
+ sheet_frames: int = Field(default=4, ge=1, le=6)
114
+ adapt_size: bool = True # a Muse sprite's frame takes the drawn art's shape
115
+ # Art Reviewer (M10.3): one vision call after the playtest; blocking issues are fixed once
116
+ # (at most review_max_fixes Muse Image edits at review_image_reasoning) before G4.
117
+ review: bool = True
118
+ review_max_fixes: int | None = Field(default=None, ge=0)
119
+ review_image_reasoning: Literal["low", "high"] = "high"
120
+ # Review call plus fixes stay within this estimate: fewer fixes when the review costs more.
121
+ review_cost_cap: float | None = Field(default=None, ge=0)
122
+ # QA tests (M10.4): the QA Playtester writes tests from each design version; every build runs
123
+ # them (and earlier versions' tests) with real key presses after the bot playtest.
124
+ qa: bool = True
125
+ # Code checks (L2.8): after the Programmer's first answer the QA Playtester reads the design
126
+ # and the code and writes checks of this game's own rules as JavaScript run in the game.
127
+ qa_checks: bool = True
128
+ # The bot playtest also looks at the HUD: texts drawn over each other or off the screen
129
+ # fail the build and go to the Programmer.
130
+ hud_check: bool = True
131
+ motion: bool = True # the Art Director's motion design: rows, forms, effects (L2.4)
132
+ # Menus and flow (L2.5): the Menu Designer's screens rendered by the SDK (title, pause,
133
+ # results, game over, victory, saved progress) and G3; title_art: Muse draws the title
134
+ # background and emblem (2 image calls; false or offline: code draws them).
135
+ menus: bool = True
136
+ title_art: bool = True
137
+ # Sounds (L2.7): the Sound Director's synthesised sounds (one Spark call per game), played by
138
+ # the SDK on the game's moments and edited in the build's sound editor or at G4.
139
+ sounds: bool = True
140
+ # Agent tools: in a live build each agent's Spark call may look things up with the tools its
141
+ # role grants (the built-in library and game-record MCP servers, its skills, and the user's
142
+ # MCP tools granted to it that need no confirmation), for up to `tool_rounds` rounds.
143
+ agent_tools: bool = True
144
+ tool_rounds: int = Field(default=3, ge=0, le=10)
145
+ max_code_kb: int = Field(default=200, ge=10, le=1000)
146
+ playtest_fps: int = Field(default=30, ge=10, le=60)
147
+ record_seconds: int = Field(default=20, ge=5, le=60)
148
+
149
+ @model_validator(mode="before")
150
+ @classmethod
151
+ def legacy_story_switch(cls, data):
152
+ """Runs saved before L2.1 had `game.story`; the G1 brief now decides (a story only
153
+ when the human asks for one)."""
154
+ if isinstance(data, dict):
155
+ data = {k: v for k, v in data.items() if k != "story"}
156
+ return data
157
+
158
+
159
+ class RouterConfig(Schema):
160
+ enabled: bool = True
161
+ max_skills: int = Field(default=3, ge=0, le=100)
162
+ max_tools: int = Field(default=5, ge=0, le=100)
163
+ token_budget: int = Field(default=4096, ge=0)
164
+
165
+
166
+ class Config(Schema):
167
+ workspace: Path
168
+ memory: MemoryConfig = Field(default_factory=MemoryConfig)
169
+ preview: PreviewConfig = Field(default_factory=PreviewConfig)
170
+ game: GameConfig = Field(default_factory=GameConfig)
171
+ router: RouterConfig = Field(default_factory=RouterConfig)
172
+ image: ImageConfig
173
+ llm: LLMConfig
174
+ research: ResearchConfig = Field(default_factory=ResearchConfig)
175
+ budget: BudgetConfig = Field(default_factory=BudgetConfig)
176
+
177
+
178
+ def load_config(path: Path | None = None) -> Config:
179
+ return Config.model_validate(
180
+ yaml.safe_load(
181
+ (path or Path(__file__).with_name("config.yaml")).read_text(encoding="utf-8")
182
+ )
183
+ )
184
+
185
+
186
+ def init_workspace(path: Path) -> Path:
187
+ root = path.expanduser().resolve()
188
+ for name in ("runs", "memory", "skills"):
189
+ (root / name).mkdir(parents=True, exist_ok=True)
190
+ mcp = root / "mcp.json"
191
+ if not mcp.exists():
192
+ mcp.write_text('{"mcpServers": {}}\n', encoding="utf-8")
193
+ return root
@@ -0,0 +1,157 @@
1
+ workspace: ~/spriteforge-workspace
2
+ router:
3
+ enabled: true
4
+ max_skills: 3
5
+ max_tools: 5
6
+ token_budget: 4096 # conservative UTF-8 byte bound for loaded skill context
7
+ preview:
8
+ max_image_calls: 16
9
+ mood_board: true
10
+ mode: game # game = Spark writes the design and code (G2/G4 gates); muse = Muse blocks
11
+ game:
12
+ # Agent loops (Programmer repairs, Muse redraws, QA check fixes, Menu and Level retries, Art
13
+ # Reviewer rounds) try again while they improve. A loop stops after `patience` tries in a
14
+ # row without improvement, or when the agent has spent its budget.max_agent_cost ($10)
15
+ # since your last action. max_repairs, max_art_calls, art_retry and review_max_fixes are
16
+ # optional fixed limits on top (unset: none).
17
+ patience: 2
18
+ # Muse jobs run in parallel (Meta allows 150 image requests/min). Call slots are handed out
19
+ # in priority order before dispatch, so the cap and the greybox choice stay deterministic.
20
+ image_workers: 4
21
+ # The game's screen: 320x180 design pixels times screen_scale. 6 = 1920x1080 (a laptop's
22
+ # full-HD screen; sprites keep 6x the pixels Muse drew, text is 6x bigger); 4 = 1280x720.
23
+ screen_scale: 6
24
+ # The Level/Tile Designer studies the level design of similar platformers with web search
25
+ # (one search call per game, before the first level); its level prompts read the findings.
26
+ level_research: true
27
+ # Muse Image's strength for every first draw: high (the API has low and high). Redraws and
28
+ # the Art Reviewer's fixes use high as well.
29
+ image_reasoning: high
30
+ # Identical requests (model, endpoint, prompt, size, reasoning strength, n, reference
31
+ # hashes) reuse workspace/cache/muse-image/<sha256>.png: no call, no cost.
32
+ image_cache: true
33
+ # A frame that fails check_frame is redrawn (reasoning_strength high) while redraws improve.
34
+ native_grid_confidence: 0.6 # below this, Muse art is fitted instead of read cell by cell
35
+ # Muse sprites are drawn as sheets (a row of frames per animation, up to 4 rows): each
36
+ # frame gets a slot of the image, so Muse draws it with fewer, bigger pixels, close to the
37
+ # requested grid, and the sprite animates. 1 = one image per sprite.
38
+ sheet_frames: 4
39
+ # A Muse sprite's frame takes the shape Muse drew (about the design's area) instead of
40
+ # squeezing it into the design box; the Programmer sees the real size in assets.json.
41
+ adapt_size: true
42
+ # Art Reviewer: after the playtest, Spark (vision) looks at all the art together and at 3
43
+ # screenshots of the running game; blocking issues are fixed before G4 with Muse Image
44
+ # edits at its highest setting (the API has low and high), then it reviews again while the
45
+ # blocking issues go down (patience, its $10 allowance); the rest are G4 suggestions.
46
+ review: true
47
+ review_image_reasoning: high
48
+ # QA Playtester: tests written from each design version (one Spark call), played with real
49
+ # key presses after the bot playtest; failures join the repairs; exported per build as
50
+ # build/tests/test_game_qa.py (pytest + Playwright).
51
+ qa: true
52
+ # Motion design (L2.4): the Art Director writes every entity's animation rows (what each
53
+ # frame shows, fps, loop), how the player looks in each form and the effects the SDK plays
54
+ # (sf.fx); Muse draws the rows, code checks and repairs them. false = the design's names.
55
+ motion: true
56
+ # Menus and flow (L2.5): the Menu Designer writes the menus as data (one Spark call per game;
57
+ # feedback revises them), the SDK renders them in the art's colours and runs the flow (title,
58
+ # pause, results, game over, victory, progress saved in the browser); G3 shows every screen
59
+ # before the playtest. title_art: Muse draws the title background and an emblem (2 image
60
+ # calls, locked once drawn; false: code draws them from the game's art).
61
+ menus: true
62
+ title_art: true
63
+ # Sounds (L2.7): the Sound Director picks a synthesised sound for every moment (one Spark
64
+ # call; no audio files, no image calls); the SDK plays them, the build's sounds.html edits them.
65
+ sounds: true
66
+ # Agent tools: in a live build each agent may look things up before it answers, with the
67
+ # tools its role grants (allowed_tools in agents/roles/*.md: the built-in library and game
68
+ # record MCP servers), its skills (load_skill) and your MCP tools granted to it that need no
69
+ # confirmation. Each round is one more Spark call; false: one call per answer, no tools.
70
+ agent_tools: true
71
+ tool_rounds: 3
72
+ # (No story switch: the G1 brief decides. A story bible is written only when the human asks
73
+ # for one; every game keeps its full record as an OKF bundle in runs/<id>/game/db.)
74
+ max_code_kb: 200
75
+ playtest_fps: 30
76
+ record_seconds: 20
77
+ image:
78
+ # Live smoke 2026-10-28: both paths accept size=1024x1024, b64_json,
79
+ # output_format=png and reasoning_strength=low, but return 1600x1600 PNGs.
80
+ # One data-URL reference verified; the maximum reference count is unverified.
81
+ base_url: https://api.meta.ai
82
+ model: muse-image-1.0
83
+ generate_path: /v1/images/generations
84
+ edit_path: /v1/images/edits
85
+ # Request sizes (aspect ratio only). Art requests use the one nearest the subject's aspect.
86
+ # Live probe 2026-10-31, all accepted: 1024x1024 -> 1600x1600,
87
+ # 1792x1024 -> 2016x1152, 1024x1536 -> 1280x1920. With the grid wording Muse draws big
88
+ # square pixels but chooses its own cell count (16x16 asked: 21x27 cells, ~46 px each).
89
+ # Live probe 2026-10-31: 2048x512 (4:1) accepted -> 3200x800.
90
+ # Live probe 2026-10-31: 1536x512 (3:1) accepted -> 2736x912.
91
+ sizes: [[1024, 1024], [1792, 1024], [1024, 1536], [2048, 512], [1536, 512]]
92
+ llm:
93
+ base_url: https://api.meta.ai
94
+ model: muse-spark-1.3
95
+ chat_path: /v1/chat/completions
96
+ # Live probe 2026-10-30: strict json_schema {files:[{path,content}]} returned a complete
97
+ # 375-line Phaser game (8578 completion tokens, finish_reason stop, 43 s); it ran without
98
+ # errors. One call per game file set is enough; no output-length setting was needed.
99
+ # Live probe 2026-10-31: top-level reasoning_effort is accepted and
100
+ # works (same question: low 113-119 reasoning tokens, max 366-455). Docs list minimal,
101
+ # low, medium, high, xhigh, max ("none" -> HTTP 400). Prefix caching is automatic: a
102
+ # repeated 1558-token prompt reported cached_tokens 1521 on a second run, also across
103
+ # different efforts (0 on back-to-back calls of the very first run: the cache fills
104
+ # shortly after a request). Prompts start with their stable part and send a
105
+ # prompt_cache_key per task.
106
+ reasoning:
107
+ intake: low # G1 genre pick, brief draft, follow-up questions, style options
108
+ art_plan: low # Art Director's library picks
109
+ revision: low # Director splitting G4 feedback
110
+ design: high # Game Designer's design doc and its revisions
111
+ code: high # Programmer: new game, repairs, changes
112
+ review: max # Art Reviewer (vision): the strongest setting Spark accepts
113
+ describe: low # Librarian describing library images (spriteforge library describe)
114
+ qa: high # QA Playtester writing tests from the design
115
+ story: high # Game Designer writing the story bible
116
+ research: medium # Game Designer studying similar games with web search (L2.2)
117
+ plan: high # Game Designer writing the game plan from the requirements and research
118
+ level: high # Level/Tile Designer writing each level as data (L2.3)
119
+ motion: medium # Art Director's motion design: rows, forms, effects (L2.4)
120
+ menus: medium # Menu Designer: the menus as data and their revisions (L2.5)
121
+ sounds: low # Sound Director: a preset and tweaks per moment, and their revisions (L2.7)
122
+ lessons: low # Librarian: lessons from a finished game, shown at G5 (L2.6)
123
+ research:
124
+ # Live probe 2026-11-02: Chat Completions rejects {"type": "web_search"}
125
+ # (HTTP 400 "tools[0] did not match any supported type"); the Responses API accepts it.
126
+ # Citations: `url_citation` annotations on output_text (few), and with
127
+ # include ["web_search_call.action.sources"] every search lists the pages it returned
128
+ # (11-12 each). json_schema (text.format, strict) and web_search work in one call: the last
129
+ # `message` item holds the JSON (earlier ones are progress lines). A URL in the JSON is not
130
+ # always one a search returned, so code checks every reference URL against the sources.
131
+ # Searches are not in `usage`: each `web_search_call` item (search or open_page) is counted
132
+ # as one search (budget.web_search_cost). One probe research: 10.8k input + 2.1k output
133
+ # tokens (low effort) and 4 tool calls, about $0.03.
134
+ path: /v1/responses
135
+ max_searches: 8 # max_tool_calls per research
136
+ search_context_size: medium
137
+ budget:
138
+ max_image_calls: 150
139
+ max_calls_per_model: 200
140
+ image_cost: 0.01
141
+ # Standard Spark USD / million tokens, verified 2026-10-29:
142
+ # https://dev.meta.ai/docs/pricing-rate-limits
143
+ spark_input_per_million: 1.25
144
+ spark_cached_input_per_million: 0.15
145
+ spark_output_per_million: 4.25
146
+ # https://dev.meta.ai/docs/pricing-rate-limits 2026-11-02: $2.50 per 1,000 search queries
147
+ web_search_cost: 0.0025
148
+ max_estimated_cost: 5.0 # per run, legacy stages only (per_request: false)
149
+ # Each agent may spend max_agent_cost (USD) since your last action (a gate answer, Go back,
150
+ # Continue); its loops run until that is used up or they stop improving (game.patience).
151
+ # Work you ask for always starts.
152
+ per_request: true
153
+ max_agent_cost: 10
154
+
155
+ memory:
156
+ min_score: 0
157
+ relative_score: 0.5
File without changes
@@ -0,0 +1,16 @@
1
+ """Which agent is making the model call now: the cost ledger records it with each call, so the
2
+ studio can show what every agent cost and which one is working."""
3
+
4
+ from contextlib import contextmanager
5
+ from contextvars import ContextVar
6
+
7
+ acting_agent: ContextVar[str | None] = ContextVar("acting_agent", default=None)
8
+
9
+
10
+ @contextmanager
11
+ def acting(agent: str):
12
+ token = acting_agent.set(agent)
13
+ try:
14
+ yield
15
+ finally:
16
+ acting_agent.reset(token)