Skip to main content

machin-ressort

Build 2D games on the machin-ressort engine (MFL + raylib) — a Torque2D-style declarative scene/behaviour model over a Spring-style deterministic simulation with record/replay/verify. Use when writing or extending a ressort game, authoring a .rml level, adding a behaviour or a system, drawing with the text-sprite format, or debugging a replay that diverges. Also read this before hashing or doing bit-twiddling in MFL — it documents two overflow traps that silently corrupt results.

Quellinformationen

Repository
javimosch/machin-ressort
Letzte Quellaktivität
3. September 2026 um 16:08
Erkannte Sprache von SKILL.md
Englisch
Sterne
0
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
machin-ressort
description
Build 2D games on the machin-ressort engine (MFL + raylib) — a Torque2D-style declarative scene/behaviour model over a Spring-style deterministic simulation with record/replay/verify. Use when writing or extending a ressort game, authoring a .rml level, adding a behaviour or a system, drawing with the text-sprite format, or debugging a replay that diverges. Also read this before hashing or doing bit-twiddling in MFL — it documents two overflow traps that silently corrupt results.
# machin-ressort A 2D engine in MFL at `~/ai/machin-ressort`, plus a Demolition Man stage-1 POC. Successor to [machin-gum-2d](https://github.com/javimosch/machin-gum-2d) (525 lines, Vec2/verlet/particles); ressort is the ambitious one. Read `README.md` first for the why. This is the how. ## The one rule **Synced code may never touch the view. Unsynced code may never write the world.** | synced (deterministic, headless, checksummed) | unsynced (raylib) | |---|---| | `engine/00_core 10_scene 20_tilemap 30_sim 40_scenefile` | `engine/50_ffi 60_view 70_forge 80_import 90_rig` | | `game/10_defs 30_sim` | `game/20_art 40_view` | Synced code must not call: raylib, `now()`, `now_ms()`, `rand_bytes()`, `GetFrameTime()`. For randomness use `sim_rand(s,n)` / `sim_randf(s)` / `sim_randr(s,lo,hi)` / `sim_chance(s,pct)` — they thread the PRNG that lives *inside* the world state. The view reacts to the sim only through **events**: the sim calls `sim_emit(s, EV_*, x, y, a, i)`, the view drains `s.events` each rendered frame. If you find yourself wanting to mutate `s` from the view, the answer is a new event. Break either rule and `demoman verify` stops meaning anything. ## Three binaries **`bin/ressort` is the engine's sprite toolchain** (`tools/sprite.src`); **`bin/soldier` is the playable pipeline demo** (`demo/soldier.src` — WASD + fire, runs on a forged fallback figure when given no `--spr`, so no third-party art ships here); **`bin/demoman` is the Demolition Man stage** (`game/*.src`). Both are ENGINE + one main. The tool works on `.spr`/`.sil`/`.rig`/`.anm` files and knows nothing about any game's art table; the game keeps `demoman art list|show|sheet` for its own sprites. Anything both need (flag parsing, the JSON printers, `SCREEN_W/H`, `pal_default`) lives in the engine — `game/20_art.src`'s `dm_palette()` is now a one-line alias over `pal_default()`. ## Build & verify ```sh ./build.sh # → bin/ressort + bin/demoman (vendors raylib 5.0 static into vendor/) ./build.sh test # headless engine suite, exit 0/1 ./bin/demoman scene game/level1.rml # validate a level, JSON out ./bin/demoman sim --frames 9000 --trace --record run.rr ./bin/demoman verify run.rr # exit 0 reproduced / 90 diverged / 2 bad input ./bin/demoman shot out.png --at 600 # DISPLAY=:0; raylib writes to CWD, not the path's dir ``` `machin encode a.src b.src c.src` concatenates modules — that is the module system. Order matters only for readability; the whole program is typechecked together, so the engine can call the game's `game_step` hook by name. Screenshot gotcha: `TakeScreenshot("docs/x.png")` writes `./x.png` — raylib strips the directory. Move it afterwards. Font gotcha: raylib's **default font is ASCII-only**. An em dash, `→` or `·` passed to `hud_text`/`DrawText` renders as `?`, and you only find out by reading the screenshot back. Keep anything drawn on screen ASCII; the docs can keep their typography. ## Adding an entity type 1. A `kind` code in `game/10_defs.src`. 2. A `prefab` line in the `.rml`, or an `entity` line for a one-off. 3. A branch in `game/30_sim.src` (a system, or a case in an existing one). 4. A sprite in `game/20_art.src` (forge it first — see below) and a branch in `draw_entities`. Add the name to `art_names` / `art_by_name` in `game/40_view.src` or it will not show up in `ressort sprite`. Slots the **engine** owns on every `Ent` — do not repurpose: | slot | owner | meaning | |---|---|---| | `s2` | `tm_move` | 1 = fall through one-way ledges this step | | `s3` | `sys_physics` | 1 = suspend gravity (rope, ladder, zipline, death) | | `t0` | `sys_lifetime` | countdown, `BEH_LIFETIME` only — never give the player that bit | Everything else (`s0 s1 f0..f3 t1`) is the game's. Document which behaviour owns which. ## The scene format (`.rml`) ``` scene name=… gravity=… tile=32 seed=… # once set key=value key=value # metadata; NO SPACES in a value layer name=… depth=0..1 color=r,g,b y= h= kind=0..4 prefab <name> k=v … # a reusable row spawn <prefab> at=cx,cy [overrides] # overrides win over the prefab entity kind=… name=… at=cx,cy hw= hh= hp= beh=a,b,c tiles …rows… end ``` - `at=` is in **cells**; the entity's feet land at that cell's bottom. Standing on a floor at row R means `cy = R-1`. `px=x,y` overrides with raw pixels. - `beh=` names resolve via `beh_by_name`; an unknown name is a **parse error**, not a silent drop. So is an unknown prefab and a missing `tiles` block. - layer `kind`: 0 flat band, 1 skyline, 2 lit windows, 3 smoke, 4 stars. - tile legend: `.` air, `#` solid, `=` one-way ledge, `H` ladder, `T` ladder top (standable + climb-through), `^` fire, `-` zip cable, `,` decor. **Ladder topology.** A ladder joining an upper floor at row `RU` to a lower floor at row `RL` is `T` at `(x, RU)` and `H` at `(x, RU+1 … RL-1)`. The actor standing on the lower floor is then already *inside* the bottom rung, so `UP` grabs it, and `T` replaces the upper floor tile so the climb passes through. Get this backwards and the ladder dead-ends into a solid slab. **Vertical clearance.** An actor `2*hh` px tall cannot fit a gap of `tile` px if `2*hh >= tile`. With 32 px tiles, `hh=15` (30 px) passes a one-tile gap; `hh=17` (34 px) does not, and every one-tile corridor becomes an invisible wall. This cost hours. ## Text sprites ``` func art_x() (s) { r := []string{} r = append(r, "..3333..") // '.'/' ' transparent, '0'-'9' then 'a'-'v' = palette 0..31 s = spr_anchor(spr(r), 4, 8) // anchor (ax, ay) in sprite pixels; feet = (w/2, h) } spr_draw(sprite, palette, x, y, pixel_scale, flip, tint) // flip = -1 mirrors spr_draw_flat(sprite, color, x, y, scale, flip) // silhouette (shadows) ``` The palette is a `[]Color`. It **cannot** be a field of an MFL struct (cstructs can't be), so pass it as an argument. `Sprite` is a plain MFL struct and caches fine — build them once in `art_new()`, never per frame. (A slice of *plain* structs inside a struct is fine — `Shade.mats []Mat` works; it is only the cstructs that cannot nest.) ## Making a sprite from a sentence ```sh ressort proj init --name game --canvas 24x40 --assets assets ressort sprite ask "a gunman in a long coat" --name gunman # -> a task, not a picture # …write assets/gunman.sil AND assets/gunman.rig, in one go… ressort sprite check gunman [--tone] # exit 0 / 90, writes the .spr when clean ``` - **`ressort.proj` is the style contract**: canvas, anchor, light, materials (which claim the zone chars `#%@&+` in order), and the house skeleton. Every `ask` inherits it, which is what stops a set of sprites from looking like a set of strangers. `--like <f.spr>` puts a reference tone map in the brief for proportions. - **Ask for the drawing and the rig in ONE reply.** Splitting them turns rigging into archaeology on someone else's picture. - `check` runs everything and answers once: canvas, connectivity (islands), anchor row, leg gap, rig coverage, pose survival. **Every problem carries a `fix` in the format's own vocabulary** — that is what makes an agent converge in one round instead of five. - It reports **facts** separately from **problems** (extent, opaque count, largest mass, leg gap, checksum). Facts are for judgement, problems are for correction. - **It declines to judge what it cannot measure.** Below `POSE_MIN` (200 opaque px) the pose check does not run and `pose_checked:0` says so: a two-pixel leg rotated 70° loses pixels to nearest-neighbour sampling, and reporting physics as a defect teaches the caller to ignore the checker. A checker that cries wolf is worse than none. - **`ressort status`** — every asset, its state (`no rig` / `uncovered` / `flat rig` / `stale spr` / `stale anm`) and the one command that advances it. Staleness is `stat` mtime, so an `.anm` older than its `.rig` is flagged before the animation is trusted. This is the project view; there is no GUI and there will not be one. - **`ressort sprite compare a b …`** — tone maps in columns plus the facts; `--contact F.png` for the human. How an agent picks among candidates without opening files. - **`ressort sprite critique <name> --blind`** — the sprite with the request, name and palette withheld, and one question. `ask` writes the request to `<name>.ask` so it can be kept back until the cold reader has answered. - Nothing here judges whether it *looks like* the request. That needs a cold reader — render the tone map and ask a fresh agent to name what it sees, blind. ## The forge — do not hand-shade a sprite `engine/70_forge.src` derives the expensive two thirds of a sprite. Draw a flat mass and let it do the rest; only reach for hand-authored rows when the result is not good enough, and then start from `--src`. ```sh ressort sprite list # every sprite + palette indices + checksum ressort sprite show <name> [--src] ressort sprite mats # the material presets and their ramps ressort sprite shade game/sil/drone.sil # flat drawing → shaded sprite ressort sprite reshade <name> --mat navy # flatten a hand-drawn sprite, shade it again ressort sprite anim <name> --kind walk|breathe|recoil|flinch|tumble --frames N ressort sprite variant <name> --from bc --to hi ressort sprite sheet out.png [--sil F] # the only verb here that needs a display ``` A `.sil` file is a drawing plus directives — `name`, `anchor ax ay`, `zone <char> <mat>`, `light -1|1`, `mirror <overlap>`. Everything else in the file is a row of art. - The shader does **form**, not material assignment. One zone character = one material, so a character with skin, hair and a vest needs three zone characters (`#`, `%`, `@`). - A character no material claims **passes through unchanged** — hand-place an eye or a muzzle in the silhouette and the shader draws around it. - Ramps are brightest-first. Depth below the top surface picks the entry; the rim overrides on the lit edge and the top, occlusion overrides underneath and on the shadow edge. Short ramps + `outline=1` on a narrow sprite = mostly outline. - `sil_mirror` squares ragged rows first. It has to: otherwise every row mirrors around its own right edge instead of the drawing's centre, and the sprite comes out lopsided. - Animation transforms clip at the canvas edge — the margins in these sprites are the budget. `spr_offset` moves the *anchor*, so a recoil costs nothing at all. - `spr_sum` digests rows + anchor exactly as the sim digests the world. Pin new sprites in `t_forge` and a shader change stops being invisible. ## Art from outside — `sprite import` For anything above ~32x32 the forge's shader is the wrong tool; transcribe instead. ```sh ressort sprite import x.png --name n --native 100 --out n.src # headless ressort sprite import x.png --native 100 --compare c.png # original vs transcription ressort sprite import x.png --native 100 --compare /dev/null --bench 24 # draw-call cost ``` - raylib is the PNG decoder only (`LoadImage`/`GetImageColor` are CPU-side — **no window needed**; `--compare`/`--bench` do open one). `ImageFormat(img, RL_RGBA8())` first, or `GetImageColor` reads whatever layout the file happened to carry. - **32 colours is the hard ceiling** — the format addresses `0`-`9` then `a`-`v`. Measured on a detailed 100x144 soldier, 32 is visually indistinguishable from 64 (RMS 7.3 vs 5.4) and even 16 survives. The palette limit is not what will stop you. - An imported character carries **its own palette** (`pal_<name>()`); `spr_draw` already takes the table as an argument, so nothing about the format changes. - **`--key auto|rrggbb` keys out a background colour.** Reference art is usually an illustration on white with no alpha at all; without keying the whole canvas is opaque and the bbox is the whole file. `auto` takes the top-left pixel. `--key-tol` defaults to 26. - Integer upscales are found by boundary contrast (`step_score`). A non-integer scale has no grid — the importer reports `detected_scale: 0` rather than guessing, and you pass `--native WxH` to area-resample. ## Two-bone limbs `leg_front` + `leg_front_lower` (`parent=leg_front`, pivot on the knee) — same for `leg_back`, `arm_front`, `arm_back`. Roles 7–10; `rig_role` checks the `*_lower` prefixes FIRST, since "leg_front_lower" also starts with "leg_front" and a shin must not be told it is a thigh. Joints bend ONE WAY (the negative half of the sine is discarded, not mirrored) — a hyperextending knee looks worse than none. `sprite check` reports a lower limb that is not parented to its upper, because a shin pivoting at the hip swings the whole leg twice. This is the single biggest visual win available: a one-box limb can only swing like a pendulum, which is what makes a walk read as a march. ## Asking a human — `bin/ressort-taste` ```sh ressort-taste hero.spr hero.rig --pairs 20 [--kind walk] [--base F.mot] [--amount 0.45] ressort-taste hero.spr hero.rig --pairs 5 --auto # answered by fitness, for CI ``` - A window is right HERE and nowhere else in this toolchain: agent-facing verbs are headless because an agent reads text; this one opens a window because a person watches motion. - **Never ask about a pair that does not LOOK different.** `mot_distance` is the share of pixels that differ frame-for-frame; pairs below `--min-diff` (0.12) are not shown. The first session lacked this and came back 13-7 for the left-hand side — a position bias is what a person produces when the content cannot decide it. - Candidates are **styles**, not noise: six coarse axes (`amp knee lag arms bob lean`) at three levels each, via `mot_restyle`. Two candidates differing by one level on one axis differ in a way a person can describe out loud, which is what makes the data worth fitting. - **Cap candidate reuse.** Pairs are chosen so no candidate appears more than twice, and a ONE-axis difference is preferred — a single-axis verdict is the only attributable kind. A session before that cap came back with the same loser in 10 of 12 pairs: twelve clicks answering one question twelve times. `ressort-taste report <f.pref>` prints the per-axis tally AND warns when the spread failed, because a readout that cannot say it is wrong is worse than none. - `ressort-evolve --runs N --out-dir D` gives champions from independent local optima; `ressort-taste --bases a.mot,b.mot,…` makes them compete. Cross-lineage pairs are a different question from cousins-of-one-base. - **`role_limit` is the anatomy**, and both the styler and the search obey it. With a flat cap the evolver leaned torsos as far as it swung knees and every champion walked tilted; with per-joint limits the champions came out upright AND scored higher. - `taste.pref` records the **seed**, not the motion (deterministic from base+seed), plus an 11-feature vector per candidate. Features are dimensions a person can NAME, so a fitted model can be read back to them in their own words. - Tags (`1`–`5`) describe what was wrong with the LOSER. Free text belongs in `taste.md` at the end of a session, and comes to the agent — a sentence usually names a measurement the fitness function does not have yet, which is where `poise` came from. ## The two holes a person found by watching - **`tread`** — while a foot is DOWN it must travel backward relative to the direction of travel. Every other term (`keep ground stride flow poise`) scores a reversed cycle identically, which is how a search produced a man walking backwards. Asserted against a reversed walk. - **`lean` is SIGNED, and `upright` is its own term.** An absolute value cannot tell forward from backward, so "wants less lean" meant "less of either" and the search leaned the body to the joint limit. A figure drawn standing up should stay standing up. - Related: `ressort-taste winner <pref> <spr> <rig> --out best.mot` hands back the candidate a person actually picked, regenerated from the style the file recorded. It scores LOWER on stride and tread than the search champion and looks better — so the weighting is still wrong and the eye is still ahead of it. Say that out loud rather than shipping the number. ## Fitting a taste
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen