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.
- spriteforge/__init__.py +1 -0
- spriteforge/__main__.py +3 -0
- spriteforge/agents/__init__.py +0 -0
- spriteforge/agents/assets.py +109 -0
- spriteforge/agents/cancellation.py +12 -0
- spriteforge/agents/director.py +50 -0
- spriteforge/agents/models.py +63 -0
- spriteforge/agents/roles/art-director.md +15 -0
- spriteforge/agents/roles/art-reviewer.md +14 -0
- spriteforge/agents/roles/director.md +14 -0
- spriteforge/agents/roles/game-designer.md +15 -0
- spriteforge/agents/roles/level-tile-designer.md +16 -0
- spriteforge/agents/roles/librarian.md +13 -0
- spriteforge/agents/roles/menu-designer.md +15 -0
- spriteforge/agents/roles/pixel-artist.md +14 -0
- spriteforge/agents/roles/programmer.md +14 -0
- spriteforge/agents/roles/qa-playtester.md +13 -0
- spriteforge/agents/roles/sound-director.md +14 -0
- spriteforge/agents/roles.py +39 -0
- spriteforge/agents/router.py +233 -0
- spriteforge/agents/router_eval.py +145 -0
- spriteforge/agents/routing_state.py +133 -0
- spriteforge/agents/runtime.py +430 -0
- spriteforge/agents/workflow.py +389 -0
- spriteforge/app/__init__.py +0 -0
- spriteforge/app/agents_view.py +248 -0
- spriteforge/app/chat.py +114 -0
- spriteforge/app/extensions.py +115 -0
- spriteforge/app/index.html +279 -0
- spriteforge/app/local_only.py +49 -0
- spriteforge/app/server.py +358 -0
- spriteforge/app/static/assets/index-L2s1wlcp.css +1 -0
- spriteforge/app/static/assets/index-aBfzgLQl.js +71 -0
- spriteforge/app/static/assets/press-start-2p-latin-400-normal-BKfmdmKK.woff +0 -0
- spriteforge/app/static/assets/press-start-2p-latin-400-normal-xj9lRvVs.woff2 +0 -0
- spriteforge/app/static/index.html +13 -0
- spriteforge/app/static/press-start-2p-LICENSE.txt +93 -0
- spriteforge/app/studio.py +278 -0
- spriteforge/app/workers.py +73 -0
- spriteforge/cli.py +241 -0
- spriteforge/config.py +193 -0
- spriteforge/config.yaml +157 -0
- spriteforge/core/__init__.py +0 -0
- spriteforge/core/acting.py +16 -0
- spriteforge/core/cost.py +236 -0
- spriteforge/core/credentials.py +24 -0
- spriteforge/core/demo.py +79 -0
- spriteforge/core/forms.py +20 -0
- spriteforge/core/gates.py +175 -0
- spriteforge/core/security.py +42 -0
- spriteforge/core/state.py +222 -0
- spriteforge/core/team.py +15 -0
- spriteforge/core/usage.py +68 -0
- spriteforge/game/__init__.py +0 -0
- spriteforge/game/actors.py +377 -0
- spriteforge/game/agent_tools.py +178 -0
- spriteforge/game/art.py +2097 -0
- spriteforge/game/art_checks.py +190 -0
- spriteforge/game/art_review.py +518 -0
- spriteforge/game/build.py +134 -0
- spriteforge/game/bundle.py +1178 -0
- spriteforge/game/cli.py +119 -0
- spriteforge/game/codegen.py +433 -0
- spriteforge/game/codeinfo.py +154 -0
- spriteforge/game/dbsource.py +137 -0
- spriteforge/game/design.py +342 -0
- spriteforge/game/export.py +62 -0
- spriteforge/game/gamedb.py +191 -0
- spriteforge/game/image_cache.py +61 -0
- spriteforge/game/lessons.py +259 -0
- spriteforge/game/level_gen.py +118 -0
- spriteforge/game/level_preview.py +97 -0
- spriteforge/game/levels.py +934 -0
- spriteforge/game/loops.py +38 -0
- spriteforge/game/menus.py +712 -0
- spriteforge/game/mock_game.js +109 -0
- spriteforge/game/mock_platformer.js +58 -0
- spriteforge/game/motion.py +881 -0
- spriteforge/game/pipeline.py +1768 -0
- spriteforge/game/plan.py +502 -0
- spriteforge/game/playtest.py +318 -0
- spriteforge/game/qa.py +664 -0
- spriteforge/game/qa_checks.py +223 -0
- spriteforge/game/qa_runtime.py +565 -0
- spriteforge/game/reach.py +807 -0
- spriteforge/game/record.py +138 -0
- spriteforge/game/research.py +262 -0
- spriteforge/game/revision.py +225 -0
- spriteforge/game/sdk/API.md +104 -0
- spriteforge/game/sdk/FLOW.md +19 -0
- spriteforge/game/sdk/LEVELS.md +65 -0
- spriteforge/game/sdk/sf.js +418 -0
- spriteforge/game/sdk/sfflow.js +442 -0
- spriteforge/game/sdk/sflevel.js +683 -0
- spriteforge/game/sdk/sfsound-editor.js +225 -0
- spriteforge/game/sdk/sfsound.js +260 -0
- spriteforge/game/sdk/sounds.html +67 -0
- spriteforge/game/sounds.py +793 -0
- spriteforge/game/story.py +188 -0
- spriteforge/game/structure.py +98 -0
- spriteforge/game/team_reports.py +394 -0
- spriteforge/game/tilesets.py +70 -0
- spriteforge/game/title_art.py +186 -0
- spriteforge/genres/__init__.py +0 -0
- spriteforge/library/__init__.py +15 -0
- spriteforge/library/cli.py +126 -0
- spriteforge/library/describe.py +242 -0
- spriteforge/library/looks.py +154 -0
- spriteforge/library/models.py +79 -0
- spriteforge/library/store.py +422 -0
- spriteforge/library/tilesets/README.md +32 -0
- spriteforge/library/tilesets/gandalf-seasons.json +111 -0
- spriteforge/library/tilesets/pixel-crawler-dungeon.json +66 -0
- spriteforge/mcp/__init__.py +0 -0
- spriteforge/mcp/cli.py +58 -0
- spriteforge/mcp/client.py +57 -0
- spriteforge/mcp/filesystem_read.py +32 -0
- spriteforge/mcp/gamedb.py +56 -0
- spriteforge/mcp/library.py +179 -0
- spriteforge/mcp/memory.py +37 -0
- spriteforge/mcp/pixel_tools.py +145 -0
- spriteforge/mcp/registry.py +145 -0
- spriteforge/mcp/service.py +224 -0
- spriteforge/mcp/templates/context7.json +6 -0
- spriteforge/mcp/templates/fetch.json +6 -0
- spriteforge/mcp/templates/filesystem-read.json +6 -0
- spriteforge/mcp/templates/pixel-tools.json +6 -0
- spriteforge/memory/README.md +10 -0
- spriteforge/memory/__init__.py +0 -0
- spriteforge/memory/cli.py +131 -0
- spriteforge/memory/okf/__init__.py +1 -0
- spriteforge/memory/okf/catalog.py +331 -0
- spriteforge/memory/okf/codec.py +63 -0
- spriteforge/memory/okf/git.py +104 -0
- spriteforge/memory/okf/indexes.py +50 -0
- spriteforge/memory/okf/models.py +114 -0
- spriteforge/memory/okf/search.py +53 -0
- spriteforge/memory/okf/seed.py +81 -0
- spriteforge/memory/okf/seeds/genres.yaml +121 -0
- spriteforge/memory/okf/seeds/other.yaml +150 -0
- spriteforge/memory/okf/store.py +402 -0
- spriteforge/memory/okf/validate.py +115 -0
- spriteforge/pixel/__init__.py +17 -0
- spriteforge/pixel/atlas.py +110 -0
- spriteforge/pixel/grid.py +174 -0
- spriteforge/pixel/palette.py +156 -0
- spriteforge/pixel/palettes/dungeon.json +9 -0
- spriteforge/pixel/postprocess.py +170 -0
- spriteforge/pixel/preview.py +40 -0
- spriteforge/pixel/schemas.py +139 -0
- spriteforge/pixel/source.py +237 -0
- spriteforge/pixel/terrain.py +48 -0
- spriteforge/pixel/validate.py +138 -0
- spriteforge/prompts/anim_frame.txt +1 -0
- spriteforge/prompts/character.txt +1 -0
- spriteforge/prompts/item.txt +1 -0
- spriteforge/prompts/tile.txt +1 -0
- spriteforge/prompts/ui.txt +1 -0
- spriteforge/providers/__init__.py +0 -0
- spriteforge/providers/base.py +97 -0
- spriteforge/providers/http.py +38 -0
- spriteforge/providers/mock.py +111 -0
- spriteforge/providers/muse_image.py +60 -0
- spriteforge/providers/muse_search.py +91 -0
- spriteforge/providers/muse_spark.py +66 -0
- spriteforge/skills/__init__.py +0 -0
- spriteforge/skills/builtin/LICENSE-awesome-gamedev-agent-skills.txt +201 -0
- spriteforge/skills/builtin/NOTICE-awesome-gamedev-agent-skills.txt +15 -0
- spriteforge/skills/builtin/THIRD_PARTY.md +21 -0
- spriteforge/skills/builtin/audio-design/SKILL.md +190 -0
- spriteforge/skills/builtin/game-feel/SKILL.md +174 -0
- spriteforge/skills/builtin/game-ui-ux/SKILL.md +167 -0
- spriteforge/skills/builtin/level-design/SKILL.md +169 -0
- spriteforge/skills/builtin/phaser-arcade-physics/SKILL.md +17 -0
- spriteforge/skills/builtin/physics-tuning/SKILL.md +164 -0
- spriteforge/skills/builtin/pixel-art-prompting/SKILL.md +12 -0
- spriteforge/skills/builtin/platformer/SKILL.md +153 -0
- spriteforge/skills/builtin/puzzle/SKILL.md +143 -0
- spriteforge/skills/builtin/sprite-animation-frames/SKILL.md +11 -0
- spriteforge/skills/builtin/tileset-design/SKILL.md +10 -0
- spriteforge/skills/catalog.py +200 -0
- spriteforge/skills/cli.py +41 -0
- spriteforge/stages/__init__.py +0 -0
- spriteforge/stages/asset_context.py +301 -0
- spriteforge/stages/asset_models.py +192 -0
- spriteforge/stages/asset_render.py +366 -0
- spriteforge/stages/asset_review.py +140 -0
- spriteforge/stages/asset_store.py +94 -0
- spriteforge/stages/chat_changes.py +236 -0
- spriteforge/stages/concept.py +42 -0
- spriteforge/stages/game.py +1699 -0
- spriteforge/stages/game_confirm.py +112 -0
- spriteforge/stages/game_final.py +220 -0
- spriteforge/stages/game_record.py +246 -0
- spriteforge/stages/gameplay.py +218 -0
- spriteforge/stages/intake.py +372 -0
- spriteforge/stages/preview_assets.py +212 -0
- spriteforge/stages/preview_models.py +167 -0
- spriteforge/stages/preview_render.py +169 -0
- spriteforge/stages/preview_repair.py +61 -0
- spriteforge/stages/preview_review.py +169 -0
- spriteforge/stages/production.py +170 -0
- spriteforge/stages/references.py +105 -0
- spriteforge/stages/rewind.py +128 -0
- spriteforge/stages/workflow.py +440 -0
- spriteforge/templates/phaser/audit.js +45 -0
- spriteforge/templates/phaser/font.png +0 -0
- spriteforge/templates/phaser/font.xml +1 -0
- spriteforge/templates/phaser/preview.html +9 -0
- spriteforge/templates/phaser/room.js +49 -0
- spriteforge/templates/phaser/vendor/LICENSE.md +20 -0
- spriteforge/templates/phaser/vendor/phaser.min.js +1 -0
- spriteforge-0.1.1.dist-info/METADATA +364 -0
- spriteforge-0.1.1.dist-info/RECORD +218 -0
- spriteforge-0.1.1.dist-info/WHEEL +5 -0
- spriteforge-0.1.1.dist-info/entry_points.txt +2 -0
- spriteforge-0.1.1.dist-info/licenses/LICENSE +27 -0
- spriteforge-0.1.1.dist-info/top_level.txt +1 -0
spriteforge/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Local-first pixel game harness."""
|
spriteforge/__main__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Immutable image versions and a stable-id registry; only explicit targets may be revised."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import io
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
from PIL import Image
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
9
|
+
|
|
10
|
+
from spriteforge.core.state import Artifact, Store
|
|
11
|
+
from spriteforge.providers.base import ImageProvider, ImageRequest
|
|
12
|
+
|
|
13
|
+
from .cancellation import check_active
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GenerateAsset(BaseModel):
|
|
17
|
+
model_config = ConfigDict(extra="forbid")
|
|
18
|
+
asset_id: str = Field(pattern=r"^[a-z0-9_-]+$")
|
|
19
|
+
instruction: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AssetResult(BaseModel):
|
|
23
|
+
asset_id: str
|
|
24
|
+
artifact_id: str
|
|
25
|
+
version: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Assets:
|
|
29
|
+
def __init__(self, store: Store, run_id: str, provider: ImageProvider):
|
|
30
|
+
self.store, self.run_id, self.provider = store, run_id, provider
|
|
31
|
+
with store.connect() as db:
|
|
32
|
+
db.execute("""CREATE TABLE IF NOT EXISTS asset_registry (
|
|
33
|
+
run_id TEXT REFERENCES runs(id), asset_id TEXT, artifact_id TEXT,
|
|
34
|
+
version INTEGER, locked INTEGER, PRIMARY KEY(run_id,asset_id))""")
|
|
35
|
+
|
|
36
|
+
def records(self) -> dict[str, AssetResult]:
|
|
37
|
+
with self.store.connect() as db:
|
|
38
|
+
return {
|
|
39
|
+
r[0]: AssetResult(asset_id=r[0], artifact_id=r[1], version=r[2])
|
|
40
|
+
for r in db.execute(
|
|
41
|
+
"SELECT asset_id,artifact_id,version FROM asset_registry WHERE run_id=?",
|
|
42
|
+
(self.run_id,),
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def generate(
|
|
47
|
+
self, request: GenerateAsset, *, allowed: set[str], revisions: dict[str, int] | None = None
|
|
48
|
+
) -> AssetResult:
|
|
49
|
+
if request.asset_id not in allowed:
|
|
50
|
+
raise PermissionError("Asset is outside the approved targets")
|
|
51
|
+
existing = self.records().get(request.asset_id)
|
|
52
|
+
target_version = (revisions or {}).get(request.asset_id, 1)
|
|
53
|
+
if existing and existing.version >= target_version:
|
|
54
|
+
return existing
|
|
55
|
+
if existing and request.asset_id not in (revisions or {}):
|
|
56
|
+
raise PermissionError("Locked asset requires a confirmed change request")
|
|
57
|
+
references = []
|
|
58
|
+
if existing:
|
|
59
|
+
references = [
|
|
60
|
+
a.data for a in self.store.artifacts(self.run_id) if a.id == existing.artifact_id
|
|
61
|
+
]
|
|
62
|
+
image_request = ImageRequest(
|
|
63
|
+
prompt=request.instruction, size=(48, 48), references=references
|
|
64
|
+
)
|
|
65
|
+
image = (
|
|
66
|
+
self.provider.edit(image_request) if existing else self.provider.generate(image_request)
|
|
67
|
+
)
|
|
68
|
+
check_active()
|
|
69
|
+
artifact = Artifact(
|
|
70
|
+
id=uuid4().hex,
|
|
71
|
+
run_id=self.run_id,
|
|
72
|
+
name=f"assets/{request.asset_id}/v{target_version}.png",
|
|
73
|
+
data=image.data,
|
|
74
|
+
)
|
|
75
|
+
record = AssetResult(
|
|
76
|
+
asset_id=request.asset_id, artifact_id=artifact.id, version=target_version
|
|
77
|
+
)
|
|
78
|
+
with self.store.connect() as db:
|
|
79
|
+
db.execute(
|
|
80
|
+
"INSERT INTO artifacts VALUES(?,?,?,?,?)",
|
|
81
|
+
(artifact.id, self.run_id, artifact.name, artifact.mime, artifact.data),
|
|
82
|
+
)
|
|
83
|
+
db.execute(
|
|
84
|
+
"INSERT OR REPLACE INTO asset_registry VALUES(?,?,?,?,1)",
|
|
85
|
+
(self.run_id, record.asset_id, record.artifact_id, record.version),
|
|
86
|
+
)
|
|
87
|
+
self.store._event(db, self.run_id, "Pixel Artist", "asset.locked", record.model_dump())
|
|
88
|
+
return record
|
|
89
|
+
|
|
90
|
+
def contact_sheet(self) -> str | None:
|
|
91
|
+
records = sorted(self.records().values(), key=lambda r: r.asset_id)
|
|
92
|
+
if not records:
|
|
93
|
+
return None
|
|
94
|
+
digest = hashlib.sha256("".join(r.artifact_id for r in records).encode()).hexdigest()[:16]
|
|
95
|
+
name = f"contact-sheet/{digest}.png"
|
|
96
|
+
artifacts = self.store.artifacts(self.run_id)
|
|
97
|
+
previous = next((a for a in artifacts if a.name == name), None)
|
|
98
|
+
if previous:
|
|
99
|
+
return previous.id
|
|
100
|
+
images = {a.id: a for a in artifacts}
|
|
101
|
+
sheet = Image.new("RGBA", (64 * len(records), 64), (24, 24, 32, 255))
|
|
102
|
+
for i, record in enumerate(records):
|
|
103
|
+
image = Image.open(io.BytesIO(images[record.artifact_id].data)).convert("RGBA")
|
|
104
|
+
sheet.paste(image, (i * 64 + 8, 8), image)
|
|
105
|
+
buffer = io.BytesIO()
|
|
106
|
+
sheet.save(buffer, format="PNG")
|
|
107
|
+
artifact = Artifact(id=uuid4().hex, run_id=self.run_id, name=name, data=buffer.getvalue())
|
|
108
|
+
self.store.save_artifact(artifact, "Art Director", "contact_sheet.created")
|
|
109
|
+
return artifact.id
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Cooperative cancellation boundary for synchronous provider/tool adapters."""
|
|
2
|
+
|
|
3
|
+
from contextvars import ContextVar
|
|
4
|
+
from threading import Event
|
|
5
|
+
|
|
6
|
+
cancelled: ContextVar[Event | None] = ContextVar("agent_cancelled", default=None)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def check_active() -> None:
|
|
10
|
+
signal = cancelled.get()
|
|
11
|
+
if signal is not None and signal.is_set():
|
|
12
|
+
raise TimeoutError("Agent invocation has ended")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Director planning and dispatch policy; plans and changes are schema-checked artifacts."""
|
|
2
|
+
|
|
3
|
+
from .models import Changes, StageNode, StagePlan
|
|
4
|
+
from .roles import Role
|
|
5
|
+
from .runtime import AgentRuntime, AgentTask
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Director:
|
|
9
|
+
def __init__(self, runtime: AgentRuntime, role: Role, roles: dict[str, Role]):
|
|
10
|
+
self.runtime, self.role, self.roles = runtime, role, roles
|
|
11
|
+
|
|
12
|
+
def validate_roles(self, plan: StagePlan) -> StagePlan:
|
|
13
|
+
if any(s.role and s.role not in self.roles for s in plan.stages):
|
|
14
|
+
raise ValueError("Plan names an unavailable role")
|
|
15
|
+
return plan
|
|
16
|
+
|
|
17
|
+
async def plan(self, task: AgentTask) -> tuple[StagePlan, str]:
|
|
18
|
+
artifact = await self.runtime.run(self.role, task, StagePlan)
|
|
19
|
+
return self.validate_roles(StagePlan.model_validate_json(artifact.data)), artifact.id
|
|
20
|
+
|
|
21
|
+
async def feedback(self, task: AgentTask, asset_ids: set[str]) -> tuple[Changes, str]:
|
|
22
|
+
artifact = await self.runtime.run(self.role, task, Changes)
|
|
23
|
+
changes = Changes.model_validate_json(artifact.data)
|
|
24
|
+
for change in changes.requests:
|
|
25
|
+
if isinstance(change.target, list) and set(change.target) - asset_ids:
|
|
26
|
+
raise ValueError("Feedback names unknown assets")
|
|
27
|
+
if change.needs_regen and not isinstance(change.target, list):
|
|
28
|
+
raise ValueError("Regeneration requires explicit asset ids")
|
|
29
|
+
return changes, artifact.id
|
|
30
|
+
|
|
31
|
+
async def replan(self, task: AgentTask, changes: Changes) -> tuple[StagePlan, str]:
|
|
32
|
+
# The confirmed requests are passed as explicit artifacts in task.inputs.
|
|
33
|
+
plan, ident = await self.plan(task)
|
|
34
|
+
if any(s.gate for s in plan.stages):
|
|
35
|
+
raise ValueError("A change plan must dispatch roles only")
|
|
36
|
+
if not changes.requests:
|
|
37
|
+
raise ValueError("Re-planning requires confirmed requests")
|
|
38
|
+
return plan, ident
|
|
39
|
+
|
|
40
|
+
async def dispatch(self, stage: StageNode, task: AgentTask, schema):
|
|
41
|
+
if stage.role not in self.roles:
|
|
42
|
+
raise ValueError("Cannot dispatch an unavailable role")
|
|
43
|
+
return await self.runtime.run(self.roles[stage.role], task, schema)
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def ready(plan: StagePlan, completed: set[str]) -> StageNode | None:
|
|
47
|
+
return next(
|
|
48
|
+
(s for s in plan.stages if s.id not in completed and set(s.depends_on) <= completed),
|
|
49
|
+
None,
|
|
50
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Artifacts exchanged by the Director, roles, and human gates."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Contract(BaseModel):
|
|
9
|
+
model_config = ConfigDict(extra="forbid")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RoleOutput(Contract):
|
|
13
|
+
summary: str
|
|
14
|
+
asset_ids: list[str] = Field(default_factory=list)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ChangeRequest(Contract):
|
|
18
|
+
target: list[str] | Literal["style", "mechanic", "level"]
|
|
19
|
+
instruction: str = Field(min_length=1)
|
|
20
|
+
severity: Literal["minor", "major", "critical"]
|
|
21
|
+
scope: str = Field(min_length=1)
|
|
22
|
+
needs_regen: bool
|
|
23
|
+
|
|
24
|
+
@model_validator(mode="after")
|
|
25
|
+
def nonempty_targets(self):
|
|
26
|
+
if isinstance(self.target, list) and not self.target:
|
|
27
|
+
raise ValueError("Asset targets cannot be empty")
|
|
28
|
+
return self
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Changes(Contract):
|
|
32
|
+
requests: list[ChangeRequest] = Field(min_length=1)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class StageNode(Contract):
|
|
36
|
+
id: str = Field(pattern=r"^[a-zA-Z0-9_-]+$")
|
|
37
|
+
role: str | None = None
|
|
38
|
+
gate: str | None = None
|
|
39
|
+
goal: str
|
|
40
|
+
depends_on: list[str] = Field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
@model_validator(mode="after")
|
|
43
|
+
def one_kind(self):
|
|
44
|
+
if (self.role is None) == (self.gate is None):
|
|
45
|
+
raise ValueError("Stage needs exactly one role or gate")
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class StagePlan(Contract):
|
|
50
|
+
stages: list[StageNode] = Field(min_length=1)
|
|
51
|
+
|
|
52
|
+
@model_validator(mode="after")
|
|
53
|
+
def dag(self):
|
|
54
|
+
ids = {stage.id for stage in self.stages}
|
|
55
|
+
if len(ids) != len(self.stages):
|
|
56
|
+
raise ValueError("Duplicate stage ids")
|
|
57
|
+
done = set()
|
|
58
|
+
while len(done) < len(ids):
|
|
59
|
+
ready = {s.id for s in self.stages if s.id not in done and set(s.depends_on) <= done}
|
|
60
|
+
if not ready:
|
|
61
|
+
raise ValueError("Cyclic plan or unknown dependency")
|
|
62
|
+
done.update(ready)
|
|
63
|
+
return self
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Art Director
|
|
3
|
+
goal: Specify a coherent readable pixel-art style.
|
|
4
|
+
model: mock
|
|
5
|
+
allowed_tools:
|
|
6
|
+
- library.library_packs
|
|
7
|
+
- library.library_search
|
|
8
|
+
- library.library_actors
|
|
9
|
+
- library.library_tiles
|
|
10
|
+
allowed_skills: [game-feel]
|
|
11
|
+
memory_scopes:
|
|
12
|
+
- project
|
|
13
|
+
max_steps: 8
|
|
14
|
+
---
|
|
15
|
+
Define palette, silhouettes, canvas sizes, and animation constraints. Respect approved art. Reference asset ids in all proposed revisions. The local library comes first: for every design entity, pick a library actor, whole object, tile-set tile or backdrop when the library has the same kind of creature or object for the game's camera (see `catalog/library`); only otherwise ask Muse Image with a concrete prompt (subject, colours, materials, pose). Pick by what the art looks like, not its file name: every library line says what the image shows and its measured colours, brightness and detail. When only the colour differs, keep the library art and set `recolor`; when the shape or detail differs (a flat tile for a brick wall), use Muse. Never send `ai_allowed: false` packs to Muse. With the `library.*` tools, `library_actors`, `library_tiles` and `library_search` list what exists and `library_packs` shows each pack's usage and AI flag. Motion design (L2.4): before the picks, write how each thing moves and reacts: per entity the animation rows with what every frame shows (a slime hops: squash, stretch, in the air, land; it never slides), when the game plays each row, fps and loop or once; break and die rows end with the thing gone; per form ability how the player looks in it; per feel event an effect (particles, flash, shake, squash) the SDK plays. Code checks the drawn rows and repairs what it can; a library actor that cannot show the rows is drawn by Muse.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Art Reviewer
|
|
3
|
+
goal: Judge a game's art as one set, in the running game, and name exactly what must change.
|
|
4
|
+
model: mock
|
|
5
|
+
allowed_tools:
|
|
6
|
+
- library.library_search
|
|
7
|
+
- library.library_actors
|
|
8
|
+
- library.library_tiles
|
|
9
|
+
allowed_skills: []
|
|
10
|
+
memory_scopes:
|
|
11
|
+
- project
|
|
12
|
+
max_steps: 8
|
|
13
|
+
---
|
|
14
|
+
Look at all of a game's art together: the numbered art sheet (native size and 4x) and screenshots of the running game, where sprites stand on the real floor at game scale with the HUD and crowds. Judge consistency (one style: outline, shading, pixel size), readability (each thing reads at game size and against the floor), perspective (every sprite matches the camera), scale (sizes against each other and the hero) and palette (threats, rewards and the player are easy to tell apart) as one set. Do the naming test honestly: say what each numbered sprite looks like from its pixels alone, before using the design. Report concrete issues by sheet number with a severity: blocking only when a player cannot tell what something is or cannot see it in play, for placeholders, or for a sprite drawn for another camera; major when it is clearly weaker than the set or confusable; minor for polish. Each fix is an instruction for an image edit of that sprite (what to change, keeping its framing, size and style). Do not invent problems. Never alter assets yourself: the harness fixes blocking issues once and offers the rest as suggestions. With the `library.*` tools (read-only), `library_search`, `library_actors`, `library_tiles` and `library_packs` show what the local library has.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Director
|
|
3
|
+
goal: Plan a playable game as a dependency DAG and route explicit changes.
|
|
4
|
+
model: mock
|
|
5
|
+
allowed_tools:
|
|
6
|
+
- game_db.game_index
|
|
7
|
+
- game_db.game_search
|
|
8
|
+
- game_db.game_show
|
|
9
|
+
allowed_skills: []
|
|
10
|
+
memory_scopes:
|
|
11
|
+
- project
|
|
12
|
+
max_steps: 8
|
|
13
|
+
---
|
|
14
|
+
Use stage ids and dependencies. Dispatch only defined roles. Interpret feedback conservatively. Return typed plans and change requests; never regenerate or approve assets yourself. When a human reviews a playable build, split their feedback into design changes (rules, entities, progression, difficulty), art changes (only the entities whose look they want different, with the new look) and code instructions (behaviour, controls, feel, UI); leave a part empty when the feedback does not touch it, so everything else stays locked. When the `library.*` tools are granted, copy ids from tool results instead of guessing them. With the `game_db.*` tools (read-only), the game's record (design, builds, decisions, QA tests, code modules) answers what exists and what the human already decided. Route a request for a story, named characters or lore to `story_changes`; never add a story the human did not ask for.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Game Designer
|
|
3
|
+
goal: Define clear mechanics, controls, and a compact playable loop.
|
|
4
|
+
model: mock
|
|
5
|
+
allowed_tools:
|
|
6
|
+
- library.library_actors
|
|
7
|
+
- library.library_search
|
|
8
|
+
- game_db.game_context
|
|
9
|
+
- game_db.game_show
|
|
10
|
+
allowed_skills: [level-design, platformer, puzzle]
|
|
11
|
+
memory_scopes:
|
|
12
|
+
- project
|
|
13
|
+
max_steps: 8
|
|
14
|
+
---
|
|
15
|
+
Before designing, when the brief asks for research, study how good published games of the genre work with web search: reference games with a page a search returned, the patterns that make their levels interesting, the feel numbers players expect and common pitfalls; references teach patterns, never copy a game's levels, characters, story or art. Then write the game plan: the human's requirements numbered and copied word for word, movement numbers in tiles and seconds (code derives the jump physics from them), abilities, mechanics and what each is for, a challenge vocabulary, per level a teaching goal, one new idea, its challenges and a difficulty that never drops, what the player sees and hears when something happens, animation rows per entity, risks, and which plan parts cover each requirement. Write the design from the plan and keep its numbers. Design for the approved brief. Keep every rule, number and mechanic the human asked for and invent nothing that contradicts it. List every entity the player will see (the art team finds or draws a sprite for each), every rule, the progression and the difficulty over time, win and lose. Write acceptance checks as events the code will emit with sf.event, reachable by a bot that cannot die within the session, and flag choices requiring a human gate. With the `library.*` tools, `library_actors` shows which creatures and animations already exist. Write a story bible only when the human asked for a story (the brief says so, or they choose Add a story): then the characters behind the design's entities (names, personalities, goals, backstories), the world, the arc and the canon later levels must keep; it never contradicts the design. Without a story, invent no names, personalities, backstories or arcs anywhere. When drafting the G1 brief, suggest a story only when the human's own words ask for one. With the `game_db.*` tools (read-only), `game_context` gives a game's record (rules, entities, art style, story when there is one) and `game_show` one concept.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Level/Tile Designer
|
|
3
|
+
goal: Design readable traversable levels and seamless tiles.
|
|
4
|
+
model: mock
|
|
5
|
+
allowed_tools:
|
|
6
|
+
- library.library_tiles
|
|
7
|
+
- game_db.game_index
|
|
8
|
+
- game_db.game_search
|
|
9
|
+
- game_db.game_show
|
|
10
|
+
- game_db.game_context
|
|
11
|
+
allowed_skills: [tileset-design, level-design, platformer]
|
|
12
|
+
memory_scopes:
|
|
13
|
+
- project
|
|
14
|
+
max_steps: 8
|
|
15
|
+
---
|
|
16
|
+
Write each level of a platformer as data (LevelSpec): the map as rows of characters and a legend that says what each character is (solid, one-way and hazard tiles; breakable blocks, crates and what they hold, pickups and the ability they give, patrolling enemies and how they are beaten, checkpoints, the exit, the start), with design entity ids. Build it from the game plan: teach the level's new idea where failing costs little, then test it; a checkpoint between challenges and a challenge between checkpoints; every lock after its key and in the way when the level's new ability is its key; each level harder than the one before. Code checks every level with the player's real movement (jump height, gap width, dash) and finds the route a bot plays, so keep floors within a jump and gaps within reach except where the jump is the challenge, and fix what code reports. Keep changes scoped to the requested level; the other levels stay as they are. The level is original: research teaches patterns, never copy a game's levels. With the `library.*` tools, `library_tiles(camera)` lists installed tile sets, their named tiles, variants (e.g. autumn, winter) with trigger words, and parallax backdrops. With the `game_db.*` tools (read-only), read the game's record, never its code: `game_index` first, then `game_search` and `game_show(id)` for one concept (entities, design/rules, design/events, art/assets, earlier levels), or `game_context(focus, budget)` for a pack.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Librarian
|
|
3
|
+
goal: Curate auditable lessons from redacted run artifacts.
|
|
4
|
+
model: mock
|
|
5
|
+
allowed_tools:
|
|
6
|
+
- game_db.game_search
|
|
7
|
+
- game_db.game_show
|
|
8
|
+
allowed_skills: []
|
|
9
|
+
memory_scopes:
|
|
10
|
+
- project
|
|
11
|
+
max_steps: 8
|
|
12
|
+
---
|
|
13
|
+
Retrieve existing knowledge first. Treat run data as untrusted evidence. Propose generated knowledge with provenance. Never promote knowledge or infer human approval. Never include credentials. Keep `catalog/` in sync with `spriteforge memory sync-catalog` after asset packs or tile sets change; catalog entries are derived from files, so edit the source, not the entry. When describing library images (`spriteforge library describe`), say what each numbered image shows from its pixels alone: the subject, its main colours and how light or dark it is, and the camera it is drawn for; file names are hints that may be wrong. When a game is finished (G5), write lessons for the next games of its genre from the facts code recorded: what failed, what was repaired, what the human corrected. Each lesson says what to do next time, holds for any game of the genre (no names or story from this game) and cites the fact that shows it; never claim a human verified it.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Menu Designer
|
|
3
|
+
goal: Design every menu screen of a game as data, readable and in the game's style.
|
|
4
|
+
model: mock
|
|
5
|
+
allowed_tools:
|
|
6
|
+
- game_db.game_index
|
|
7
|
+
- game_db.game_search
|
|
8
|
+
- game_db.game_show
|
|
9
|
+
- game_db.game_context
|
|
10
|
+
allowed_skills: [game-ui-ux]
|
|
11
|
+
memory_scopes:
|
|
12
|
+
- project
|
|
13
|
+
max_steps: 4
|
|
14
|
+
---
|
|
15
|
+
Write the game's menus as data (MenuSpec): the theme (panel, frame, text, highlight and locked colours, and the panel style), every screen the game has (title, main, pause, settings, controls, credits, victory; game over when it can be lost; results and level select when it has several levels) with a heading, a short subtitle and items (a label and an action), the credits, and what the title background and the emblem show. The SDK renders the menus with its own pixel font, handles the keys (arrows or WASD, Enter, Escape back, digits) and runs the flow, so write no code and no text into pictures: Muse Image draws the title art from your two descriptions in the game's art style, without letters. Keep labels short and their meaning plain ('Start', 'Resume', 'Quit to title'), in the game's voice where it helps; keep every item a screen must have. Code fits the theme to the colours measured from the game's locked art and checks every label's width, the contrast of text on its panel and that every screen can be reached from the title; fix what it reports. With the `game_db.*` tools (read-only), read the game's record, never its code: `game_index` first, then `game_search` and `game_show(id)`, or `game_context("menus")`. Feedback changes only what it names; the other screens stay as they are.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Pixel Artist
|
|
3
|
+
goal: Produce only requested pixel-art assets.
|
|
4
|
+
model: mock
|
|
5
|
+
allowed_tools:
|
|
6
|
+
- generate_asset
|
|
7
|
+
- library.library_search
|
|
8
|
+
allowed_skills: [pixel-art-prompting, sprite-animation-frames]
|
|
9
|
+
required_skills: [pixel-art-prompting]
|
|
10
|
+
memory_scopes:
|
|
11
|
+
- project
|
|
12
|
+
max_steps: 8
|
|
13
|
+
---
|
|
14
|
+
Use the generate_asset tool only for asset ids in the task. Preserve locked assets unless a confirmed change request targets them. Report resulting asset ids. Deterministic pixel checks enforce constraints.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Programmer
|
|
3
|
+
goal: Implement the approved game with Phaser 3 and no build step.
|
|
4
|
+
model: mock
|
|
5
|
+
allowed_tools:
|
|
6
|
+
- game_db.game_search
|
|
7
|
+
- game_db.game_show
|
|
8
|
+
allowed_skills: [phaser-arcade-physics, platformer, puzzle, game-feel, physics-tuning]
|
|
9
|
+
required_skills: [phaser-arcade-physics]
|
|
10
|
+
memory_scopes:
|
|
11
|
+
- project
|
|
12
|
+
max_steps: 8
|
|
13
|
+
---
|
|
14
|
+
Write the whole game from the approved design as plain Phaser 3 scripts against the SpriteForge SDK (its reference comes with each task). Implement every rule, the progression and the difficulty curve; do not invent unapproved mechanics. Read controls only through sf.input, time only through sf.clock or scene timers, use only listed asset ids, emit sf.event for every design check, and give the game an autoplay policy good enough to trigger them all. No network, modules, eval or URLs. When the harness reports failures or a reviewer asks for a change, answer with exact edits (an `old` snippet that occurs once, and its replacement), or complete files when asked for them. When the context7 tools are granted, look up Phaser APIs in the version the games run: library id /phaserjs/phaser/v3_90_0 (query-docs); never use an API of Phaser 4.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: QA Playtester
|
|
3
|
+
goal: Check playability and report reproducible defects.
|
|
4
|
+
model: mock
|
|
5
|
+
allowed_tools:
|
|
6
|
+
- game_db.game_search
|
|
7
|
+
- game_db.game_show
|
|
8
|
+
allowed_skills: [platformer]
|
|
9
|
+
memory_scopes:
|
|
10
|
+
- project
|
|
11
|
+
max_steps: 8
|
|
12
|
+
---
|
|
13
|
+
Test the approved mechanics, controls, difficulty, and asset constraints. The harness playtests every build for its whole session with the game's own bot and reports each design check; you write the tests the bot cannot do. Write QA tests from the design alone (never from the code), as steps the harness plays with real key presses: movement with real input, the level-up or upgrade menu, the lose path, restart after losing, and the HUD state changing. Test only what the design promises, with the events and sf.state keys every build exposes; keep each test short and deterministic. Never silently alter assets. Then, once the game's code exists, write code checks of this game's own rules (L2.8): small JavaScript scenarios run inside the running game with the `qa` helper. The design is the truth and the code may be wrong: read the code only to find where things live, set a scene up, let time run with real keys and assert what a player would see (what is beaten and how, what hurts, where projectiles go). A check never makes an outcome happen itself (no sf.event, no killing or opening things), and uses no timers, network or storage.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Sound Director
|
|
3
|
+
goal: Give every moment of a game a sound that fits the thing that makes it.
|
|
4
|
+
model: mock
|
|
5
|
+
allowed_tools:
|
|
6
|
+
- game_db.game_search
|
|
7
|
+
- game_db.game_show
|
|
8
|
+
- game_db.game_context
|
|
9
|
+
allowed_skills: [audio-design]
|
|
10
|
+
memory_scopes:
|
|
11
|
+
- project
|
|
12
|
+
max_steps: 4
|
|
13
|
+
---
|
|
14
|
+
Design the game's sound effects as data (SoundBrief): a one-line sound style and a list of cues, each with the moments that play it (`on`), what it sounds like, a preset from the library and pitch, length and volume tweaks. The SDK synthesises every sound in the browser from the preset (chiptune voices and filtered noise) and plays it the moment the game or the SDK emits that moment, so write no code and name no audio files. Make each sound fit what makes it: a slime splashes when it lands, squishes when it jumps and bubbles while it moves; wood splinters, stone crumbles, metal clangs, a shot goes pew, a coin dings. Use the moments you are given; `move` is a loop that repeats while the player moves. When several things share a moment (a crate and a wall both break), give each its own cue named after the thing. Add a moment of your own (e.g. `shoot`) only for something that happens and has no listed moment: the Programmer will emit it. Keep loops and menu sounds quiet and short. Code fits presets, moments and tweaks, adds a sound for any moment you left silent, and checks that every sound can be heard; fix what it reports. Feedback changes only the sounds it names; the human may also have shaped sounds in the sound editor, and those stay as they are.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Editable role definitions, loaded afresh when a run starts."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Role(BaseModel):
|
|
11
|
+
model_config = ConfigDict(extra="forbid")
|
|
12
|
+
name: str = Field(min_length=1)
|
|
13
|
+
goal: str = Field(min_length=1)
|
|
14
|
+
model: str = Field(min_length=1)
|
|
15
|
+
allowed_tools: list[str]
|
|
16
|
+
allowed_skills: list[str]
|
|
17
|
+
required_skills: list[str] = Field(default_factory=list)
|
|
18
|
+
memory_scopes: list[Literal["project", "private"]]
|
|
19
|
+
max_steps: int = Field(ge=1, le=100)
|
|
20
|
+
prompt: str = Field(min_length=1)
|
|
21
|
+
|
|
22
|
+
@model_validator(mode="after")
|
|
23
|
+
def required_are_allowed(self):
|
|
24
|
+
if set(self.required_skills) - set(self.allowed_skills):
|
|
25
|
+
raise ValueError("Required skills must be in the role allowlist")
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_roles(directory: Path | None = None) -> dict[str, Role]:
|
|
30
|
+
roles = {}
|
|
31
|
+
for path in sorted((directory or Path(__file__).with_name("roles")).glob("*.md")):
|
|
32
|
+
_, frontmatter, prompt = path.read_text(encoding="utf-8").split("---", 2)
|
|
33
|
+
role = Role.model_validate({**yaml.safe_load(frontmatter), "prompt": prompt.strip()})
|
|
34
|
+
if role.name in roles:
|
|
35
|
+
raise ValueError(f"Duplicate role: {role.name}")
|
|
36
|
+
roles[role.name] = role
|
|
37
|
+
if not roles:
|
|
38
|
+
raise ValueError("No role definitions found")
|
|
39
|
+
return roles
|