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
@@ -0,0 +1,169 @@
1
+ ---
2
+ name: level-design
3
+ description: >
4
+ Design and build playable levels — the blockout/whitebox-to-playable workflow,
5
+ player metrics and grid layout, pacing and flow (tension/rest curve), gating
6
+ and the critical path, and encounter design. Engine-neutral practice. Use when
7
+ the user mentions level design, blockout/whitebox/greybox, level layout, level
8
+ pacing, encounter design, or the critical path through a level.
9
+ ---
10
+
11
+ # Level design
12
+
13
+ A level is a **sequence of intentional experiences** delivered through space.
14
+ Good level design is a *process*: define the metrics movement is built on, block
15
+ out geometry with primitives, play it, then dress it — never the reverse. This
16
+ skill is the engine-neutral practice; use `godot-tilemap`/`unity-tilemap-2d` to
17
+ lay out 2D grids and gridmaps for 3D.
18
+
19
+ ## When to use
20
+
21
+ - Use to plan a level's structure: critical path, pacing, gating, encounters,
22
+ and where the player learns vs is tested.
23
+ - Use the **blockout → test → iterate → dress** workflow to build a level that
24
+ plays well before any art exists.
25
+ - Use to derive level **metrics** from the character's movement so geometry is
26
+ reachable and fair.
27
+
28
+ **When *not* to use:** to *generate* levels algorithmically, use `procedural-gen`
29
+ (authored and procedural design are complementary). For the engine's tile/grid
30
+ painting tools, use `godot-tilemap` / `unity-tilemap-2d`. For the movement
31
+ abilities the metrics come from, that's the engine movement skill + `input-systems`.
32
+
33
+ ## Core workflow
34
+
35
+ 1. **Derive metrics first.** Measure the character: max jump height and distance,
36
+ run speed, reach, camera range. Every gap, ledge, and corridor is sized in
37
+ these units. Lock them before building geometry.
38
+ 2. **Blockout (whitebox/greybox).** Build the whole level from untextured
39
+ primitives at correct scale. Validate flow, sightlines, and reachability while
40
+ changes are cheap. No art yet. Tools: Godot `CSGBox3D` and friends with
41
+ `use_collision` on (prototype only: the docs flag their CPU cost, so bake to
42
+ static meshes later), Unity's ProBuilder package (`com.unity.probuilder`),
43
+ Unreal's Modeling Mode (Cube Grid), and a plain grey tileset for 2D.
44
+ 3. **Define the critical path** (start → goal) and the **golden path** you expect
45
+ most players to take. Layer optional/secret paths off it.
46
+ 4. **Pace the experience.** Alternate tension and rest in a deliberate curve;
47
+ don't run combat-combat-combat. Give the player room to breathe and to
48
+ anticipate.
49
+ 5. **Teach, then test.** Introduce each mechanic in a safe space, let the player
50
+ practice, then test it under pressure. Difficulty rises in a sawtooth, not a
51
+ straight line.
52
+ 6. **Gate with intent.** Use locks/keys, abilities, and one-way drops to control
53
+ order and pacing; guide with light, lines, and landmarks rather than walls.
54
+ 7. **Playtest and iterate.** Watch real players: where do they get lost, stuck,
55
+ bored, or killed unfairly? Fix the blockout; only dress when it plays well.
56
+
57
+ ## Patterns
58
+
59
+ ### 1. Player metrics drive every dimension
60
+
61
+ ```gdscript
62
+ # Derive the metrics from the SAME constants the character controller uses, then
63
+ # size geometry in these units. If the jump changes, the numbers follow.
64
+ const RUN_SPEED := 240.0 # px/s (or m/s in 3D)
65
+ const JUMP_VELOCITY := 420.0 # initial upward speed of a full jump
66
+ const GRAVITY := 980.0 # px/s², the value the controller applies
67
+ const MAX_JUMP_H := JUMP_VELOCITY * JUMP_VELOCITY / (2.0 * GRAVITY) # 90: peak height
68
+ const AIR_TIME := 2.0 * JUMP_VELOCITY / GRAVITY # ~0.86 s, take-off to same height
69
+ const MAX_JUMP_DIST := RUN_SPEED * AIR_TIME # ~206: running jump, level ground
70
+ const SAFE_GAP := MAX_JUMP_DIST * 0.7 # comfortable, not pixel-perfect
71
+ const HARD_GAP := MAX_JUMP_DIST * 0.95 # a deliberate skill check
72
+ # Build platforms so required jumps use SAFE_GAP; reserve HARD_GAP for optional reward.
73
+ ```
74
+
75
+ The formulas are the ideal arc. Variable jump height, a separate fall gravity,
76
+ air acceleration, the fixed physics step and the collider's width all move the
77
+ real numbers, and landing higher shortens the reach. Confirm them once in a
78
+ test room with a measured gap ladder, then treat the measured values as the
79
+ truth. A platform placed `MAX_JUMP_DIST + 1` away is impossible; one at
80
+ `SAFE_GAP` is fair. Keep these constants beside the level data so designers and
81
+ code agree.
82
+
83
+ ### 2. Encounter / pacing as data (a tension timeline)
84
+
85
+ ```gdscript
86
+ # Author the level as a sequence of beats with an intended intensity (0..1).
87
+ # This makes the pacing curve explicit and reviewable before you build rooms.
88
+ const BEATS := [
89
+ { "room": "entry", "type": "teach", "intensity": 0.1 },
90
+ { "room": "hall_1", "type": "combat", "intensity": 0.5 },
91
+ { "room": "vista", "type": "rest", "intensity": 0.1 }, # breather + reward
92
+ { "room": "gauntlet", "type": "combat", "intensity": 0.8 },
93
+ { "room": "save_room", "type": "rest", "intensity": 0.2 }, # before the boss
94
+ { "room": "boss", "type": "climax", "intensity": 1.0 },
95
+ ]
96
+ # Read the intensity column top-to-bottom: it should rise overall but dip for rests
97
+ # (a sawtooth), never flatline high. Drive spawns/music intensity from this.
98
+ ```
99
+
100
+ ### 3. Gating and the critical path (a small graph)
101
+
102
+ ```gdscript
103
+ # Model the level as rooms + gated connections. Validate that the goal is
104
+ # reachable with the keys/abilities the player can actually obtain in order.
105
+ const ROOMS := {
106
+ "entry": { "exits": [ { "to": "hall_1" } ] },
107
+ "hall_1": { "exits": [ { "to": "vista", "needs": "double_jump" },
108
+ { "to": "side_room" } ] }, # optional branch
109
+ "side_room": { "exits": [ { "to": "hall_1" } ], "grants": "double_jump" },
110
+ "vista": { "exits": [ { "to": "boss", "needs": "red_key" } ] },
111
+ }
112
+
113
+ # Grow the reachable set until nothing new opens: collect what each reached room
114
+ # grants, then follow every exit whose `needs` is already held. Assumes keys are
115
+ # never used up and doors never re-lock; model consumable keys as counts.
116
+ func goal_reachable(rooms: Dictionary, start: String, goal: String) -> bool:
117
+ var have := {} # keys/abilities collected
118
+ var reached := {start: true}
119
+ var changed := true
120
+ while changed:
121
+ changed = false
122
+ for room in reached.keys():
123
+ var info: Dictionary = rooms.get(room, {})
124
+ var grant = info.get("grants")
125
+ if grant != null and not have.has(grant):
126
+ have[grant] = true
127
+ changed = true
128
+ for exit in info.get("exits", []):
129
+ var open: bool = not exit.has("needs") or have.has(exit["needs"])
130
+ if open and not reached.has(exit["to"]):
131
+ reached[exit["to"]] = true
132
+ changed = true
133
+ return reached.has(goal)
134
+ # goal_reachable(ROOMS, "entry", "boss") is FALSE for the table above: no room
135
+ # grants "red_key". That is exactly the soft-lock this check exists to catch.
136
+ # Run it in a test or editor tool whenever the room graph changes.
137
+ ```
138
+
139
+ ## Pitfalls
140
+
141
+ - **Dressing before it plays.** Detailing a blockout you haven't validated wastes
142
+ the most expensive work on a layout you'll change. Greybox and test first.
143
+ - **Geometry that ignores metrics**: gaps the jump can't clear, ledges below
144
+ reach, corridors narrower than the camera needs. Size everything in player units.
145
+ - **Flat pacing.** Wall-to-wall combat (or wall-to-wall calm) numbs the player.
146
+ Alternate tension and rest; place a breather and a save before the climax.
147
+ - **Testing a mechanic before teaching it.** Players meet a hazard for the first
148
+ time in a lethal spot. Introduce safely, let them practice, then test.
149
+ - **Soft-locks and dead ends.** A gate needs an ability/key obtainable only past
150
+ the gate. Validate the critical path's key/ability order, not just connectivity.
151
+ - **No readability / guidance.** Players get lost when nothing draws the eye. Use
152
+ light, leading lines, color, and landmarks to point toward the path.
153
+ - **One-way drops with no signposting** strand or surprise players. Telegraph
154
+ irreversible moves.
155
+ - **Confusing procedural with authored.** Generation gives variety, not
156
+ authored pacing. Use `procedural-gen` for variety; hand-author for intent.
157
+
158
+ ## References
159
+
160
+ - `references/pacing-and-flow.md` — the difficulty/tension curve in depth,
161
+ teaching-loop design (introduce→develop→twist→test), readability and guidance
162
+ techniques, 2D vs 3D layout considerations, and a blockout review checklist.
163
+
164
+ ## Related skills
165
+
166
+ - `godot-tilemap`, `unity-tilemap-2d` — paint 2D level grids; gridmaps for 3D.
167
+ - `procedural-gen` — generate variety to complement authored structure.
168
+ - `game-ai` — encounter enemies that navigate the space you build.
169
+ - `platformer`, `puzzle`, `roguelike` — genres that compose this skill.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: phaser-arcade-physics
3
+ description: Implement Phaser 3 Arcade Physics movement, collision and lifecycle for small pixel games.
4
+ ---
5
+ Use Phaser 3 browser scripts with no build step. Enable pixelArt and roundPixels,
6
+ and keep logical screen size separate from CSS scaling. Use Arcade bodies for
7
+ simple axis-aligned collision: top-down actors need a small foot collider rather
8
+ than their entire 48x48 canvas. Normalize diagonal input and set velocity rather
9
+ than accumulating per-frame displacement. Separate overlaps for damage from solid
10
+ colliders. Register input and colliders once per scene and clean up on shutdown.
11
+ Check walls, corners, simultaneous contacts, pause, restart and missing atlas keys.
12
+ A sprite added to a physics group (`group.add`, `group.create`) takes the group's body
13
+ defaults: its gravity, velocity, bounce and world-bounds settings are reset. Create the
14
+ group with its defaults (`this.physics.add.group({gravityY: G})`) or set body properties
15
+ after adding it. Side-view actors that walk on the ground need gravity and a collider with
16
+ the floor; spawn them with their feet on it, never hanging at a fixed height.
17
+ Use the Phaser 3 documentation: https://docs.phaser.io/phaser/concepts/physics/arcade
@@ -0,0 +1,164 @@
1
+ ---
2
+ name: physics-tuning
3
+ description: >
4
+ Tune game physics for stable, good-feeling motion — fixed vs variable
5
+ timestep, render interpolation, mass/gravity/drag, continuous collision
6
+ detection (CCD) to stop tunneling, fixing jitter, and collision layers/masks.
7
+ Engine-neutral. Use when the user mentions physics feel, jitter, tunneling,
8
+ fixed timestep, FixedUpdate, CCD, bouncing/unstable physics, or collision layers.
9
+ ---
10
+
11
+ # Physics tuning
12
+
13
+ Most "bad physics" is not a bug in the engine — it's a mismatch between the
14
+ **fixed-timestep simulation** and the **variable-rate render loop**, or untuned
15
+ mass/drag/CCD/layer settings. This skill covers the engine-neutral knobs that
16
+ make physics stable and responsive; pair it with `godot-physics` or
17
+ `unity-physics` for the concrete APIs.
18
+
19
+ ## When to use
20
+
21
+ - Use when motion jitters, objects pass through walls (tunneling), stacks
22
+ explode, or movement feels floaty/sticky/laggy.
23
+ - Use to decide what goes in the fixed (physics) step vs the render frame, and
24
+ how to interpolate between them.
25
+ - Use to tune gravity, mass, drag, restitution, solver iterations, sleeping, and
26
+ collision layers/masks.
27
+
28
+ **When *not* to use:** for an engine's exact physics nodes/components and
29
+ collision callbacks, use `godot-physics` or `unity-physics`. For *movement
30
+ decisions* (when to jump, AI steering) use `input-systems` and `game-ai`. For
31
+ platformer jump-feel specifics like coyote time/jump buffering, that's input/
32
+ controller territory — see `input-systems` and the `platformer` genre.
33
+
34
+ ## Core workflow
35
+
36
+ 1. **Run physics on a fixed timestep.** Simulate at a constant rate (e.g. 50–60
37
+ Hz). A fixed `dt` makes the simulation deterministic-ish and stable; a
38
+ variable `dt` makes integration and collisions inconsistent.
39
+ 2. **Put physics work in the physics callback**, not the render frame. Apply
40
+ forces/velocities and read collisions in the fixed step (`FixedUpdate` /
41
+ `_physics_process`), using that step's `dt`.
42
+ 3. **Interpolate rendering between physics ticks.** The render frame rate ≠ the
43
+ physics rate, so smoothly interpolate transforms toward the latest physics
44
+ state, or enable the engine's Rigidbody interpolation, to remove visible
45
+ stutter.
46
+ 4. **Tune the body, not the scene.** Set mass for relative weight, drag for
47
+ damping, gravity scale per object, and restitution/friction via materials.
48
+ 5. **Stop tunneling with CCD** on small/fast bodies; cap maximum velocity.
49
+ 6. **Stabilize stacks/joints** with more solver iterations, sane mass ratios, and
50
+ sleeping for resting bodies.
51
+ 7. **Verify by feel and stress test.** Play at low and high frame rates; throw
52
+ fast objects at thin walls; stack and shove bodies. Report what you observed.
53
+
54
+ ## Patterns
55
+
56
+ ### 1. Fixed timestep for simulation, render interpolation for smoothness
57
+
58
+ ```gdscript
59
+ # Physics callback: runs at the FIXED rate. Use its dt for all integration.
60
+ func _physics_process(dt): # Unity: void FixedUpdate()
61
+ velocity += gravity * dt # integrate with the FIXED dt
62
+ move_and_slide() # engine resolves collisions this step
63
+ _prev_pos = _curr_pos; _curr_pos = global_position # record for interpolation
64
+
65
+ # Render frame: runs as fast as the display. Interpolate between physics states.
66
+ func _process(_frame_dt): # Unity: void Update()
67
+ var alpha = Engine.get_physics_interpolation_fraction() # 0..1 within the tick
68
+ visual.global_position = _prev_pos.lerp(_curr_pos, alpha)
69
+ # RIGHT: integrate in the fixed step, render via interpolation.
70
+ # WRONG: applying forces in _process/Update with frame dt — speed and collisions
71
+ # then depend on frame rate and jitter under load.
72
+ ```
73
+
74
+ Most engines offer this for you: Godot's project setting
75
+ `physics/common/physics_interpolation` (off by default; it interpolates every
76
+ node, 2D and 3D) and Unity's `Rigidbody.interpolation = Interpolate`. Prefer the
77
+ built-in before hand-rolling, and never do both on the same node.
78
+
79
+ ### 2. Stop tunneling: CCD + a speed cap
80
+
81
+ ```gdscript
82
+ # Fast, small bodies skip past thin colliders between ticks. Two fixes:
83
+ body.continuous_cd = true # RigidBody3D bool (RigidBody2D: CCD_MODE_* enum)
84
+ # Unity: rb.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative (cheap, covers
85
+ # dynamic bodies too) or ContinuousDynamic (fast vs fast); plain Continuous sweeps vs static only.
86
+ # Cap velocity so a single step can't move more than ~one collider thickness.
87
+ const MAX_SPEED := 40.0
88
+ if velocity.length() > MAX_SPEED:
89
+ velocity = velocity.normalized() * MAX_SPEED
90
+ # Rule of thumb: max_distance_per_step (= speed / physics_hz) should be < the
91
+ # thinnest wall. Raise physics_hz or enable CCD when that fails.
92
+ ```
93
+
94
+ ### 3. Body tuning: mass, drag, gravity scale, material
95
+
96
+ ```gdscript
97
+ # Mass is RELATIVE weight in collisions; it does NOT change fall speed (gravity
98
+ # accelerates all masses equally). Use drag and gravity_scale to shape feel.
99
+ body.mass = 2.0 # heavier pushes lighter in collisions
100
+ body.linear_damp = 0.5 # air drag: higher = stops sooner (Unity 6: linearDamping, was drag)
101
+ body.gravity_scale = 1.5 # per-object gravity multiplier (Unity: Rigidbody2D.gravityScale)
102
+ # Bounce/slide come from the physics material, not code:
103
+ material.bounce = 0.2 # restitution 0..1 (Unity: PhysicsMaterial.bounciness)
104
+ material.friction = 0.8 # surface grip
105
+ body.physics_material_override = material
106
+ ```
107
+
108
+ ### 4. Collision layers and masks (who collides with whom)
109
+
110
+ ```gdscript
111
+ # A body is ON its layer(s) and SCANS the layers in its mask. Both directions of a
112
+ # pair must be configured for them to interact.
113
+ player.collision_layer = LAYER_PLAYER
114
+ player.collision_mask = LAYER_WORLD | LAYER_ENEMY # player detects world+enemies
115
+ pickup.collision_layer = LAYER_PICKUP
116
+ pickup.collision_mask = LAYER_PLAYER # pickup only reacts to player
117
+ # Unity equivalent: assign GameObject layers and edit the Physics collision matrix
118
+ # (or Physics.IgnoreLayerCollision). Keep a named layer constant table, not magic numbers.
119
+ ```
120
+
121
+ ## Pitfalls
122
+
123
+ - **Applying forces/movement in the render frame** (`Update`/`_process`) makes
124
+ behavior frame-rate dependent — faster PCs run faster, and collisions get
125
+ flaky. Do simulation in the fixed step.
126
+ - **Visible jitter** even with a fixed step usually means no render
127
+ interpolation: the physics rate and display rate beat against each other.
128
+ Enable interpolation.
129
+ - **Tunneling** through thin walls: discrete collision misses fast movers. Enable
130
+ CCD, cap speed, thicken walls, or raise the physics rate.
131
+ - **Expecting heavier objects to fall faster.** Gravity is acceleration; mass
132
+ affects collision response, not fall speed. Use `gravity_scale`/drag for feel.
133
+ - **Exploding stacks / jittery joints**: mass ratios too extreme, or too few
134
+ solver iterations. Keep mass ratios modest and raise iteration counts.
135
+ - **Bodies that never rest** burn CPU and twitch. Enable sleeping and a sensible
136
+ sleep threshold for resting objects.
137
+ - **One-directional layer setup**: A's mask includes B but B's mask excludes A.
138
+ Detection/collision can need both sides; verify the full matrix.
139
+ - **Huge `dt` spikes** (load hitches, breakpoints) blow up integration. Clamp the
140
+ max physics step / substep count so a stall doesn't launch everything (Godot
141
+ `physics/common/max_physics_steps_per_frame`, Unity *Maximum Allowed Timestep*).
142
+ - **Teleports smear across the screen** with interpolation on: the renderer
143
+ blends from the old position. Godot: call `reset_physics_interpolation()`
144
+ *after* moving the node. Unity: move interpolated bodies through the
145
+ Rigidbody; a direct `transform` write is ignored unless followed by
146
+ `Physics.SyncTransforms()`.
147
+ - **Feel changes after raising the physics rate**: Godot (GodotPhysics and Jolt
148
+ alike) damps by multiplying velocity by `1 - damp / ticks_per_second` each
149
+ tick, an approximation that drifts as `damp` approaches the tick rate (at
150
+ `damp >= ticks_per_second` the body stops in one tick). Small damping values
151
+ barely change; re-tune large ones after changing `physics_ticks_per_second`.
152
+
153
+ ## References
154
+
155
+ - `references/timestep-and-ccd.md` — the fixed-timestep accumulator loop,
156
+ interpolation math, substepping, CCD modes, solver/iteration tuning, sleeping,
157
+ and a stability checklist.
158
+
159
+ ## Related skills
160
+
161
+ - `godot-physics`, `unity-physics` — concrete bodies, colliders, and callbacks.
162
+ - `input-systems` — responsive controls, jump buffering, coyote time.
163
+ - `game-ai` — agent movement that must agree with the physics step.
164
+ - `platformer`, `fps-shooter` — genres whose feel depends on this tuning.
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: pixel-art-prompting
3
+ description: Prompt Muse Image for readable chunky sprites matching an approved palette and style anchor.
4
+ ---
5
+ Use the approved style.json and anchor. Request one centered full-body subject on
6
+ a flat solid magenta field, with chunky pixel clusters, consistent top-left light,
7
+ and no text, watermark, gradients or anti-aliasing. A 48x48 character canvas should
8
+ contain about 40x40 logical pixels of artwork. Requested provider dimensions are
9
+ not guaranteed: process the decoded image and inspect the detected grid and logical
10
+ size. Preserve accepted assets; use an edit reference for approved pose changes.
11
+ Inspect at native size and nearest-neighbor enlargement. Report palette, outline,
12
+ background-spill and silhouette failures instead of hiding them in the prompt.
@@ -0,0 +1,153 @@
1
+ ---
2
+ name: platformer
3
+ description: >
4
+ Build a 2D platformer: run/jump control with coyote time, jump buffering, and variable jump
5
+ height, wall slide and wall jump, plus tiled levels and hazards. Use for a platformer or
6
+ Mario/Celeste-like, or tuning jump feel.
7
+ ---
8
+
9
+ # Platformer
10
+
11
+ A playbook for 2D platformers — the run/jump controller "feel", level structure, hazards,
12
+ and goals. This is a **compositional** skill: it wires an engine movement skill, a tilemap
13
+ skill, and design skills into a working game. It does **not** re-teach physics or tilemaps;
14
+ it tells you what to build and how to make jumping feel good.
15
+
16
+ ## When to use
17
+
18
+ - Use when building a side-scrolling or single-screen platformer, a "Mario-like" /
19
+ "Celeste-like", or any game whose core verb is **jump between surfaces**.
20
+ - Use when a jump feels floaty, unresponsive, or "unfair" and you need feel fixes
21
+ (coyote time, jump buffering, variable height, corner correction).
22
+
23
+ **When *not* to use:** top-down movement with no gravity → use the engine movement skill
24
+ directly. 3D first-person traversal → `fps-shooter`. Grid/turn movement → `roguelike`.
25
+ For the raw kinematic body API, use `godot-2d-movement` (or your engine's controller skill).
26
+
27
+ ## Core loop
28
+
29
+ **Observe a gap/hazard → commit to a jump or move → land safely (or die) → reach the next
30
+ checkpoint/goal.** A platformer lives or dies on the *moment-to-moment* feel of that single
31
+ jump, repeated thousands of times. Tighten the controller first; everything else is content.
32
+
33
+ ## Must-have systems
34
+
35
+ 1. **Run/jump controller** — horizontal accel/decel, gravity, jump, with the feel aids below.
36
+ 2. **Solid + one-way collision** — ground, walls, and "jump-through" platforms.
37
+ 3. **Level geometry** — a tilemap or hand-placed colliders; the playable space.
38
+ 4. **Hazards + death/respawn** — spikes, pits, enemies; reset to the last checkpoint.
39
+ 5. **Checkpoints / level goal** — progress markers and a win condition (flag, door, exit).
40
+ 6. **Camera** — follows the player with a deadzone and look-ahead, clamped to level bounds.
41
+ 7. **Juice** — landing dust, squash/stretch, hit-stop, sound. Cheap, huge feel payoff.
42
+
43
+ ## Design knobs (make the jump feel right)
44
+
45
+ Tune these by **outcome** (height in tiles, time to apex in seconds), not by raw numbers.
46
+
47
+ | Knob | Effect | Sane starting point |
48
+ |------|--------|---------------------|
49
+ | Max jump height | reach | 3–4 tiles |
50
+ | Time to apex | "weight"/snappiness | 0.30–0.40 s |
51
+ | Fall gravity multiplier | snappy, non-floaty fall | 1.5–2.0× rise gravity |
52
+ | Coyote time | jump just after leaving a ledge | 0.08–0.12 s (~5–7 frames @60) |
53
+ | Jump buffer | press just before landing still jumps | 0.10–0.15 s |
54
+ | Variable jump cut | tap = short hop, hold = full | cut upward velocity ×0.4–0.5 on release |
55
+ | Apex hang | brief float at the top for air control | reduce gravity ×0.5 near `|vy|`<threshold |
56
+ | Ground accel / friction | responsiveness vs. ice | reach top speed in 0.05–0.1 s |
57
+ | Corner correction | nudge past a ledge clipped by 1–2 px | nudge up to ~4 px sideways |
58
+ | Wall slide / wall jump | controlled descent and kick-off | wall grace, kick/steering lock, air-jump refresh policy; see refs §9 |
59
+
60
+ Derive gravity and jump velocity from the *feel* values rather than guessing — see Pattern 1.
61
+
62
+ ## Patterns
63
+
64
+ ### 1. Solve jump physics from height + time (not magic numbers)
65
+
66
+ ```python
67
+ # Pseudocode. Pick the FEEL you want, then derive the physics. y-axis points DOWN.
68
+ # From kinematics: h = (g * t^2) / 2 and v0 = g * t.
69
+ JUMP_HEIGHT = 3.5 * TILE # how high, in world units
70
+ TIME_TO_APEX = 0.35 # seconds to reach the top
71
+
72
+ gravity = (2 * JUMP_HEIGHT) / (TIME_TO_APEX ** 2) # rising gravity
73
+ jump_velocity = -(2 * JUMP_HEIGHT) / TIME_TO_APEX # negative = upward
74
+ fall_gravity = gravity * 1.8 # heavier on the way down → less floaty
75
+ ```
76
+
77
+ ### 2. Coyote time + jump buffer + variable height (the feel core)
78
+
79
+ For wall or air jumps, replace this simple jump resolver with the unified resolver in refs §9;
80
+ keep variable-height and gravity integration only once per tick.
81
+
82
+ ```python
83
+ # Pseudocode in the per-frame update. dt = seconds since last frame.
84
+ # Timers count DOWN; refresh coyote while grounded, buffer on a fresh press.
85
+ if on_floor:
86
+ coyote_timer = COYOTE_TIME # 0.1
87
+ if jump_pressed_this_frame:
88
+ buffer_timer = JUMP_BUFFER # 0.12
89
+ coyote_timer -= dt
90
+ buffer_timer -= dt
91
+
92
+ # A jump is allowed if we pressed recently AND were grounded recently.
93
+ if buffer_timer > 0 and coyote_timer > 0:
94
+ velocity.y = jump_velocity
95
+ buffer_timer = 0
96
+ coyote_timer = 0 # consume both so we can't double-jump
97
+
98
+ # Variable height: releasing jump early while still rising cuts the arc short.
99
+ if jump_released_this_frame and velocity.y < 0:
100
+ velocity.y *= 0.45
101
+
102
+ # Asymmetric gravity: snappier fall than rise; clamp to a terminal speed.
103
+ g = fall_gravity if velocity.y > 0 else gravity
104
+ velocity.y = min(velocity.y + g * dt, MAX_FALL_SPEED)
105
+ ```
106
+
107
+ ### 3. One-way platforms
108
+
109
+ Solid from above, pass-through from below. Use the engine's flag rather than collision math:
110
+ Godot `CollisionShape2D.one_way_collision` (or the one-way option on a TileSet collision
111
+ polygon), Unity a `PlatformEffector2D` with **Use One Way** on a collider set to be used by the
112
+ effector, Phaser Arcade `platform.body.checkCollision.down = false` (plus `left`/`right` for thin
113
+ ledges). Let the player **drop through** by ignoring that platform for ~0.1–0.2 s when they hold
114
+ Down + Jump (refs §5).
115
+
116
+ ## Pitfalls / failure modes
117
+
118
+ - **Per-frame movement not scaled by `dt`** → speed changes with frame rate. Every velocity
119
+ integration and timer must use `dt`. (See `physics-tuning`.)
120
+ - **Jump height differs between machines** → the jump runs on a variable render `dt`. Simple
121
+ per-step integration lands a few percent short of the designed height, by a different amount
122
+ at each frame rate. Run the controller on the fixed physics tick (Godot `_physics_process`,
123
+ Unity `FixedUpdate`) or use the half-step update in refs §1.
124
+ - **Player is flung off or doubles speed leaving a moving platform** → platform velocity added
125
+ twice. Godot's `CharacterBody2D` already carries the body and adds the platform's velocity on
126
+ leave (`platform_on_leave`); don't add it again in script (refs §6).
127
+ - **Floaty jumps** → symmetric gravity. Make fall gravity heavier than rise gravity.
128
+ - **"The jump didn't register"** → no input buffering. Buffer presses for ~0.1 s before landing.
129
+ - **"I fell off and couldn't jump"** → no coyote time. Allow a jump for ~0.1 s after leaving ground.
130
+ - **Wall jump overrides a ground jump or consumes the double jump** → resolve one buffered
131
+ press in ground/coyote → wall → air order, with an explicit air-jump refresh policy (refs §9).
132
+ - **Sticking to walls / catching on tile seams** → use a single capsule/box collider, not
133
+ per-tile colliders, and add corner correction.
134
+ - **Tunneling through floors at high speed** → enable continuous collision / smaller fixed
135
+ timestep for fast bodies (see `physics-tuning`).
136
+ - **Camera snaps and induces nausea** → smooth/lerp the follow, add a deadzone, clamp to bounds.
137
+ - **Difficulty wall from bad teaching** → introduce one mechanic per area before combining them.
138
+
139
+ ## Composition (build it from these skills)
140
+
141
+ - **Controller body:** `godot-2d-movement` (Godot `CharacterBody2D`); for other engines use
142
+ the engine core + physics skill (`unity-physics`, `phaser-arcade-physics`, `pygame-core`).
143
+ - **Levels:** `godot-tilemap` / `unity-tilemap-2d` for geometry; `level-design` for layout,
144
+ pacing, and teaching order.
145
+ - **Feel/physics:** `physics-tuning` for timestep, CCD, and stability.
146
+ - **Input:** `input-systems` for buffering, rebinding, and gamepad support.
147
+ - **Polish:** `audio-design` for SFX/music; the engine animation skill for squash/stretch.
148
+ - **Process:** `prototype-fast` to greybox the controller before building content.
149
+
150
+ ## References
151
+
152
+ - For jump math derivation, wall slide/jump, a full feel-tuning table, corner correction, moving/one-way
153
+ platforms, and camera follow, read `references/feel-tuning.md`.
@@ -0,0 +1,143 @@
1
+ ---
2
+ name: puzzle
3
+ description: >
4
+ Build a puzzle game: grid/board state, move input, rule-based resolution (match-3 cascades, sokoban
5
+ pushes, tile logic), scoring, and undo. Use for a match-3, sokoban, or grid-logic puzzle.
6
+ ---
7
+
8
+ # Puzzle
9
+
10
+ A playbook for grid/board puzzle games — the board model, move input, rule resolution
11
+ (matching, pushing, logic), scoring, undo, and level progression. This is a **compositional**
12
+ skill: it models board state and rules and presents them through a tilemap/UI. It does not
13
+ re-teach tilemaps; it defines the resolution loop and the correctness rules (clean state,
14
+ deterministic resolution, undo) that keep a puzzle fair and bug-free.
15
+
16
+ ## When to use
17
+
18
+ - Use when the game is a **discrete board** the player changes with moves, and the board
19
+ **resolves by rules**: match-3/tile-matching, sokoban/block-pusher, sliding puzzle, logic grid.
20
+ - Use when designing match/cascade resolution, undo, level progression, or solvability.
21
+
22
+ **When *not* to use:** real-time grid action with permadeath → `roguelike`. Card zones/turns →
23
+ `card-game`. Physics-based "puzzle platformer" → `platformer` + `physics-tuning`. For the tile
24
+ rendering, use `godot-tilemap` / `unity-tilemap-2d`.
25
+
26
+ ## Core loop
27
+
28
+ **Read the board → plan a move → make the move → the board resolves by its rules (match, push,
29
+ fall, fill, cascade) → see progress toward the objective → repeat until solved/failed.** The
30
+ fun is the *planning*; the engine's job is to resolve each move **deterministically** and
31
+ present it clearly.
32
+
33
+ ## Must-have systems
34
+
35
+ 1. **Board model** — a grid of cells holding pieces; the single source of truth (logic, not visuals).
36
+ 2. **Move input** — swap, push, drag, rotate, or place; validate legality before applying.
37
+ 3. **Rule resolution** — detect and apply the genre's rule (matches, pushes, logic) until stable.
38
+ 4. **Cascades/chains** — when resolution changes the board, re-resolve until no more changes.
39
+ 5. **Objectives + scoring** — win/lose conditions (score, clear all, reach goal); move/time limits.
40
+ 6. **Undo** — revert the last move (and its resolution) exactly; essential for thinky puzzles.
41
+ 7. **Level progression + (often) generation** — hand-authored or generated **solvable** boards.
42
+ 8. **Feedback ("juice")** — clear, satisfying animation/sound for matches, falls, and chains.
43
+
44
+ ## Design knobs
45
+
46
+ | Knob | Effect | Notes |
47
+ |------|--------|-------|
48
+ | Grid size / shape | complexity | Square is standard; hex/irregular change feel. |
49
+ | Match/push rule | genre identity | 3-in-a-row, shapes, push-into-goal, etc. |
50
+ | Cascade scoring | reward depth | Bigger chains = exponential payoff. |
51
+ | Move / time limit | pressure | Move-limited = puzzly; time = arcade. |
52
+ | Difficulty curve | learning | Introduce one mechanic at a time. |
53
+ | Undo depth | forgiveness | Single-step vs. full history. |
54
+ | Solvability guarantee | fairness | Generated boards must be solvable. |
55
+ | Deadlock handling | no dead ends | Detect no-moves; shuffle or end (refs). |
56
+
57
+ ## Patterns
58
+
59
+ ### 1. Board model + match detection (logic separate from visuals)
60
+
61
+ ```python
62
+ # Pseudocode. The board is the truth; rendering reads from it. (0,0) top-left, y grows down.
63
+ board = [[piece_or_empty for _ in range(W)] for _ in range(H)]
64
+
65
+ def find_matches(board):
66
+ matched = set()
67
+ for y in range(H): # horizontal runs of >= 3 equal pieces
68
+ run = 1
69
+ for x in range(1, W):
70
+ if board[y][x] and board[y][x] == board[y][x-1]: run += 1
71
+ else:
72
+ if run >= 3: matched |= {(y, k) for k in range(x-run, x)}
73
+ run = 1
74
+ if run >= 3: matched |= {(y, k) for k in range(W-run, W)}
75
+ # ... repeat the same scan vertically (columns) ...
76
+ return matched
77
+ ```
78
+
79
+ ### 2. Resolve → collapse → refill → cascade (repeat to stability)
80
+
81
+ ```python
82
+ # Pseudocode. One player move can trigger a chain; loop until the board stops changing.
83
+ def resolve(board):
84
+ chain = 0
85
+ while True:
86
+ matches = find_matches(board)
87
+ if not matches: break # stable: resolution complete
88
+ chain += 1
89
+ score += score_for(matches, chain) # later chain steps score more (see refs)
90
+ clear(board, matches) # remove matched pieces
91
+ apply_gravity(board) # pieces fall into the gaps
92
+ refill(board, rng) # spawn new pieces at the top (seeded RNG)
93
+ return chain
94
+ ```
95
+
96
+ ### 3. Undo via state snapshot or command
97
+
98
+ ```python
99
+ # Pseudocode. Snapshot before each move; undo restores it exactly (board + score + counters + RNG).
100
+ def make_move(move):
101
+ history.append(snapshot(board, score, moves_left, rng.state)) # push BEFORE applying
102
+ apply(move); resolve(board); moves_left -= 1
103
+
104
+ def undo():
105
+ if history:
106
+ board, score, moves_left, rng.state = history.pop() # exact revert, including refills
107
+ ```
108
+
109
+ For large boards prefer the **command** pattern (store the move + enough to invert it) over full
110
+ snapshots to save memory; snapshots are simplest and fine for small boards.
111
+
112
+ ## Pitfalls / failure modes
113
+
114
+ - **Mixing logic and visuals** → animations desync from state and cause bugs. The board model is
115
+ the single source of truth; the view only renders it.
116
+ - **Resolving only once** → cascades/chains are missed. Loop resolution until the board is stable
117
+ (Pattern 2).
118
+ - **Undo that doesn't restore everything** → score/move-count/random-state drift. Snapshot *all*
119
+ state, or make the move fully invertible. If undo skips the RNG state, redoing the same move
120
+ refills different pieces and players learn to "reroll" by undoing.
121
+ - **Unseeded refill RNG** → can't reproduce a level / no deterministic undo or daily puzzle. Seed it.
122
+ - **Generated boards that aren't solvable** → unfair dead ends. Generate-and-verify, or generate
123
+ from a known solution backward (refs). A sliding-tile puzzle filled by a random shuffle is
124
+ unsolvable half the time; check the parity rule in refs §7.
125
+ - **No deadlock detection** (match-3) → board with no valid moves softlocks. Detect "no moves"
126
+ and shuffle or end the level (refs).
127
+ - **Difficulty spikes** → too many mechanics at once. Teach one mechanic per level before combining.
128
+ - **Resolution mid-animation accepts input** → double-moves/corruption. Lock input until the
129
+ board is stable.
130
+
131
+ ## Composition (build it from these skills)
132
+
133
+ - **Board rendering:** `godot-tilemap` / `unity-tilemap-2d` for the grid; `godot-ui-control` for HUD, score, and menus.
134
+ - **Levels:** `level-design` for hand-authored puzzles and difficulty pacing; `procedural-gen` for solvable generated boards.
135
+ - **Persistence:** `save-systems` for level progress, high scores, and seeded daily puzzles.
136
+ - **Juice:** `game-feel` for match/cascade pop, screen shake, and chain feedback; the engine animation/`Tween` skill for swaps/falls/clears; `audio-design` for match and chain cues.
137
+ - **Scripting:** `godot-gdscript` / `unity-csharp-scripting` for the resolution loop and rules.
138
+
139
+ ## References
140
+
141
+ - For match-3 detection/gravity/refill/cascade detail, deadlock detection and reshuffles,
142
+ sokoban/rule-based puzzles, undo strategies, solvable generation, and scoring, read
143
+ `references/board-and-resolution.md`.