Building games in machin
machin (MFL) compiles to a native binary through C, and reaches real games two ways:
- Terminal (TUI): ANSI escapes for drawing +
raw_mode/read_key for input. A no-dependency single binary. → machin-game-demo-snake (Snake).
- GUI / audio: raylib through machin's C FFI — a real OpenGL window, textures, sound. Links the system graphics/audio stack, so not self-contained. → machin-game-demo-2048 (shapes+text), machin-game-demo-flappy (sprites/textures), machin-game-demo-simon (audio), machin-game-demo-3d (3D + per-object rotation), machin-game-demo-anim (2D procedural), machin-game-demo-terrain (immediate-mode mesh), machin-game-demo-planet (GPU mesh / pointer FFI), machin-game-demo-cyberpunk (infinite noise world + fly camera), machin-game-demo-solar (solar-system sim, fixed-timestep, math3d module), machin-game-demo-physics (verlet physics sandbox, constraint relaxation, collision), machin-game-demo-galaxy (procedural spiral galaxy, rlgl point cloud), machin-game-demo-ballistics (interactive cannon sim, ballistic trajectory prediction, drag + wind), machin-game-demo-player (first-person 3D playground, walk/fly toggle, platform stepping, object pushing).
Where this domain is heading: docs/NORTH-STAR-GAMEDEV.md (tiers + the gap roadmap: pointer/array FFI for real GPU meshes, a vector/matrix layer, a noise builtin, shaders, callbacks).
Each game is its own public repo with a build.sh (machin encode src.src > app.mfl && machin build app.mfl -o app), a README.md, a repo-root SKILL.md, and committed assets/. This skill is the shared substrate; each game's SKILL.md has its specifics.
machin encode a.src b.src c.src accepts multiple files and concatenates them — that is the module system. No includes, no package manager: order the files, hand them all to encode, and the whole program typechecks together (so an engine module may call a hook the game file defines later by name). This is how an engine gets split from the game on top of it — see machin-games (framework/vec2.src framework/physics.src games/phys/x.src) and machin-ressort (7 engine modules + 5 game modules).
Run machin guide for the version-exact language surface (builtins, idioms, gotchas). This skill is about games specifically.
Build & verify workflow (this environment)
- raylib, no root: there's no system raylib and no passwordless sudo. The game
build.sh auto-vendors raylib's prebuilt static release (raylib-5.0_linux_amd64.tar.gz) into vendor/, then injects cflags "-I.../include -L.../lib" + link ":libraylib.a" into a throwaway .mfl so the committed source stays system-style (link "raylib"). A copy is cached at /tmp/rl/raylib-5.0_linux_amd64. build.sh prefers a system raylib (pkg-config --exists raylib) when present.
- Display + audio are live:
DISPLAY=:0, PulseAudio works. Run a game backgrounded, sleep, then kill it.
- Screenshot to verify rendering: prefer raylib's own
fn TakeScreenshot(string) over ImageMagick — it captures the game framebuffer at an exact frame, needs no window manager, and doesn't grab the whole 1920×1080 root. Gotcha: it strips the directory and writes to the CWD (TakeScreenshot("docs/x.png") → ./x.png); move it afterwards. Build it into a CLI verb — game shot out.png --at 600 renders to sim frame 600, shoots, and exits — so any frame is reproducible from the shell. Fallback if you need the desktop instead: DISPLAY=:0 import -window root /tmp/shot.png (scrot/maim/grim/xdotool are NOT installed). Read the PNG back to confirm the frame.
- Can't inject keystrokes (no xdotool). Verify gameplay with (a) a permanent deterministic bot behind a CLI flag (
--bot) that reads world state and returns input bits — far better than the old advice of sed-patching a throwaway autopilot: it survives refactors, doubles as an attract mode and a CI exerciser, and its output is reproducible; or (b) headless logic tests — factor the pure logic (slide/merge, collision) and run it with machin run, asserting output. Do the logic test first; it's faster and catches the type errors. Keep the bot's own state (waypoint index, etc.) outside the simulation, or a recorded replay — which doesn't run the bot — will diverge.
- Assets load by relative path → run the binary from the repo root (
./app, not from /tmp).
machin encode runs the typechecker, so most errors show at encode time (no cc needed). machin build x.mfl --emit-c prints the generated C when you need to see the FFI marshaling.
The raylib FFI surface
One extern "raylib" { … } block is the entire C boundary; everything else is pure MFL. Template (declare only what you call):
extern "raylib" {
header "raylib.h"
link "raylib" link "GL" link "m" link "pthread" link "dl" link "rt" link "X11"
cstruct Color { r u8 g u8 b u8 a u8 }
cstruct Texture2D { id u32 width i32 height i32 mipmaps i32 format i32 } // all-int handle
cstruct Rectangle { x f32 y f32 width f32 height f32 } // f32 fields
cstruct Vector2 { x f32 y f32 }
cstruct Sound {} // OPAQUE handle (v0.44.0)
fn InitWindow(i32, i32, string) fn SetTargetFPS(i32) fn WindowShouldClose() bool
fn BeginDrawing() fn EndDrawing() fn ClearBackground(Color) fn CloseWindow()
fn DrawRectangle(i32, i32, i32, i32, Color) fn DrawCircle(i32, i32, f32, Color)
fn DrawText(string, i32, i32, i32, Color) fn MeasureText(string, i32) i32
fn LoadTexture(string) Texture2D fn DrawTexture(Texture2D, i32, i32, Color)
fn DrawTextureRec(Texture2D, Rectangle, Vector2, Color)
fn DrawTexturePro(Texture2D, Rectangle, Rectangle, Vector2, f32, Color)
fn IsKeyPressed(i32) bool fn IsMouseButtonPressed(i32) bool fn GetMouseX() i32 fn GetMouseY() i32
fn InitAudioDevice() fn LoadSound(string) Sound fn PlaySound(Sound) fn CloseAudioDevice()
}
FFI tiers, all verified working:
- Scalars + by-value structs (
Color, and f32-field Rectangle/Vector2) passed by value.
- Struct return —
LoadTexture returns Texture2D (an all-int handle) by value.
- Opaque handles (
cstruct Sound {}, machin v0.44.0) — a by-value C struct that contains pointers (Sound/Music/Font). machin holds the real C struct and passes it back to fns without naming its fields. Receive it from a fn, store it (a var or []Sound), pass it on; no construct or .field. (A single pointer is simpler: the ptr FFI type, an int.)
- Nested cstructs (machin v0.45.0) — a
cstruct field may be another cstruct, marshaled recursively. Declare the inner one first. Required for 3D and 2D cameras: cstruct Vector3 { x f32 y f32 z f32 } then cstruct Camera3D { position Vector3 target Vector3 up Vector3 fovy f32 projection i32 }; construct with nested literals Camera3D{Vector3{12.0,7.0,0.0}, Vector3{0.0,1.5,0.0}, Vector3{0.0,1.0,0.0}, 45.0, 0} and pass by value to BeginMode3D.
3D (see machin-game-demo-3d): bracket 3D draws with BeginMode3D(cam) / EndMode3D(); DrawCube(Vector3, f32,f32,f32, Color), DrawCubeWires, DrawSphere(Vector3, f32, Color), DrawGrid(i32, f32); any DrawText after EndMode3D is screen-space. Rebuild the Camera3D each frame rather than mutating it. projection 0 = CAMERA_PERSPECTIVE.
Per-object transforms (rotation/scale). DrawCube/DrawSphere only translate (axis-aligned). To rotate or scale an object, use raylib's immediate-mode matrix stack from rlgl: rlPushMatrix → rlTranslatef(x,y,z) → rlRotatef(deg, ax,ay,az) / rlScalef(x,y,z) → draw at the local origin → rlPopMatrix. Those functions live in rlgl.h, not raylib.h — but their symbols are in libraylib.a, so declare them in a headerless extern block (machin emits the prototypes itself; symbols link from the raylib block's libs). No new machin feature — the existing scalar/void FFI carries it:
extern "rlgl" { fn rlPushMatrix() fn rlPopMatrix() fn rlTranslatef(f32,f32,f32) fn rlRotatef(f32,f32,f32,f32) fn rlScalef(f32,f32,f32) }
// spin a cube in place:
rlPushMatrix() rlTranslatef(x,y,z) rlRotatef(deg, 0.0,1.0,0.0) DrawCube(v3(0.0,0.0,0.0), s,s,s, c) rlPopMatrix()
(The same stack works in 2D between BeginDrawing/EndDrawing. The headerless-extern trick is general: any libraylib.a/system-lib symbol that's scalar/void can be reached this way without its header.)
Procedural meshes (immediate mode) — see machin-game-demo-terrain. Compute geometry in MFL and stream it triangle-by-triangle with rlgl: extern "rlgl" { fn rlBegin(i32) fn rlEnd() fn rlColor4ub(u8,u8,u8,u8) fn rlVertex3f(f32,f32,f32) fn rlDisableBackfaceCulling() }; rlBegin(4) (= RL_TRIANGLES), one rlColor4ub then three rlVertex3f per triangle, rlEnd() — inside BeginMode3D. Flat-shade in MFL (no light shader otherwise): face normal = cross of two edges, sh = 0.4 + 0.6*clamp(dot(n_unit, lightDir),0,1) (sqrt to normalize), multiply color by sh. rlDisableBackfaceCulling() if the surface is seen from both sides (else winding-dependent triangles vanish → thin sliver). ~10–15k rlVertex3f/frame is fine.
Point clouds via rlgl (RL_POINTS) — see machin-game-demo-galaxy. For thousands of individual points (stars, particles, sparks, dust), use rlBegin(0) (= RL_POINTS) instead of per-item DrawSphere calls. The same rlgl extern block applies: rlBegin(0), then per point rlColor4ub(r,g,b,255) + rlVertex3f(x,y,z), then rlEnd(). This renders the entire set in one GPU draw call vs N calls for N DrawSpheres. Good for ~5000+ points at 60fps. Brightness/visual size can be faked by emitting multiple overlapping rlVertex3f at the same position — 1 pass = 1 pixel, 2 passes = 2 pixels (medium bright), 3+ passes = bright/large. Combine with a dark ClearBackground (near-black blue: col(2,2,8)) for a space/atmosphere look. The headerless extern trick works: extern "rlgl" { fn rlBegin(i32) fn rlEnd() fn rlColor4ub(u8,u8,u8,u8) fn rlVertex3f(f32,f32,f32) }.
GPU meshes / pointer-array FFI (v0.47.0–v0.48.0). Build C buffers in raw memory and a C struct as a cstruct, then hand them to the GPU. Raw memory (pointers are ints): p := alloc(nbytes) (zeroed), poke_f32/poke_i32/poke_u8/poke_u16/poke_ptr(p, byteOffset, v), peek_f32/peek_i32, free(p). Pointer params: ptr (a raw pointer, void* → any T*); *T (deref a raw pointer, pass the struct by value); T* inout (pass a cstruct variable by pointer, writing the modified struct back). A cstruct field may be ptr, so you declare the struct and the C compiler lays it out. Mesh flow: alloc+poke the vertex/color arrays, then mesh := Mesh{vcount, vcount/3, vbuf, cbuf, 0, 0} with cstruct Mesh { vertexCount i32 triangleCount i32 vertices ptr colors ptr vaoId u32 vboId ptr } (field names match raylib's), UploadMesh(mesh, false) (inout Mesh*; writes vao/vbo back), LoadModelFromMesh(mesh) → Model (opaque), then DrawModelEx each frame. Build once, GPU-resident — vs. immediate mode which re-emits every frame. (see machin-game-demo-planet)
Procedural worlds + infinite terrain + fly camera — see machin-game-demo-cyberpunk. Noise (v0.49.0): noise2(x,y)/noise3(x,y,z) are deterministic Perlin, ~`[-1,1]; layer them into **fbm** in MFL (s += amp*noise2(x*fr,y*fr); amp*=0.5; fr*=2) for terrain/placement. **Infinite streaming:** a small slot pool — pre-fill []Modelwith zeroedModel{}(a zero opaque handle is a safe no-op forDrawModel/UnloadModel) + a cused[]flag +ccx/ccz; each frame unload chunks out of RADand build in-range ones into free slots, so only the leading edge regenerates. Bake **world coords** into each chunk mesh andDrawModelat the origin. **Fly camera:**forward=(cos(pitch)*sin(yaw), sin(pitch), cos(pitch)*cos(yaw)), right=normalize(cross(forward,up))=(-fz,0,fx)/len, driven by GetMouseDelta(yaw/pitch) +IsKeyDown+GetFrameTime; call DisableCursor()for mouse-look. raylib takes ownership of an uploaded mesh'salloc'd buffers (frees them on UnloadModel` — don't free yourself). Budget chunk builds (≤ a few mesh builds/frame) so flying never hitches the mouse.
Shaders / post-processing (composition — no new machin feature; see machin-game-demo-cyberpunk). Shader is {id u32, locs ptr} (a pointer cstruct field), RenderTexture2D is {id u32, texture Texture2D, depth Texture2D} (nested cstructs); LoadShaderFromMemory(vs, fs) (GLSL as \n-escaped single-line strings; standard names vertexPosition/vertexTexCoord/vertexColor/mvp/texture0), SetShaderValue(sh, loc, ptr, kind) (kind 0/1/2 = float/vec2/vec3, value from a small alloc'd buffer), SetShaderValueTexture (bind a sampler). Post-process: BeginTextureMode(rt) → draw scene → EndTextureMode, then BeginShaderMode(sh) → DrawTextureRec(rt.texture, Rectangle{0,0,W,-H}, ...) (negative height flips the render target) → EndShaderMode. Depth fog: linearize texture(depthTex,uv).r with near/far and skip depth>0.9995 (sky).
GPU instancing (thousands of meshes in one DrawMeshInstanced — also composition; flora in machin-game-demo-cyberpunk). Material is a partial cstruct { shader Shader, maps ptr } (the C params[4] array can't be a cstruct field, but it's left zeroed by marshaling — fine). Instancing VS declares in mat4 instanceTransform (gl_Position = mvp*instanceTransform*vec4(pos,1)). raylib 5.0 has no SHADER_LOC_VERTEX_INSTANCE_TX — it uses SHADER_LOC_MATRIX_MODEL (index 9) for the instance attribute: poke_i32(ish.locs, 36, GetShaderLocationAttrib(ish, "instanceTransform")). md := LoadMaterialDefault(); imat := Material{ish, md.maps}. Build the per-instance Matrix[] in raw memory: translation @ byte 12/28/44, scale @ 0/20/40, 1.0 @ 60, rest zero. Keep positions deterministic (world grid + noise2) so instances don't flicker. Math: machin has native math builtins (v0.46.0) — sin cos tan asin acos atan atan2 sqrt cbrt pow exp log log2 log10 floor ceil round trunc abs fmod hypot and pi() (numeric in, float out; -lm linked only when used). So an orbit is just v3(R*cos(a), h, R*sin(a)). (An extern "m" of the same name still shadows the builtin if you want a specific libm signature.)
Procedural skeletal animation (fauna in machin-game-demo-cyberpunk — composition over the rlgl matrix stack, no new feature). Pose a bone hierarchy with forward kinematics: nest rlPushMatrix/rlTranslatef(to the joint)/rlRotatef(the joint angle)/draw-at-local-origin/rlPopMatrix. A leg is two segments — translate to the hip, rotate the whole leg, DrawCube the upper segment, rlTranslatef down to the knee, rlRotatef the shin, DrawCube the lower segment. Drive a diagonal gait from sin/cos(t*speed): legs 0&3 share one phase, 1&2 the opposite (+π); swing = sin(phase)*A rotates the hip, a lift term (cos(phase) clamped >0) bends the knee on the forward stroke, and a body bob (sin(t*speed*2)*h) sells the weight shift. Wrap the whole creature in one more rlPush/rlTranslatef(world pos)/rlRotatef(heading)/rlPop. Snap to terrain height each frame. No skinned mesh needed — nested transforms over primitives carry it.
Pushing the draw distance past raylib's ~1 km clip. raylib's auto perspective uses a fixed far plane (rlSetClipPlanes does not exist in 5.0). Override it with rlSetMatrixProjection (rlgl), passing a Matrix by value — declare cstruct Matrix { m0 f32 m4 f32 m8 f32 m12 f32 m1 f32 m5 f32 m9 f32 m13 f32 m2 f32 m6 f32 m10 f32 m14 f32 m3 f32 m7 f32 m11 f32 m15 f32 } (raylib's field order) and build a perspective matrix with your own far plane: m0=f/aspect, m5=f (f=1/tan(fovy/2)), m10=-(far+near)/(far-near), m14=-(2·far·near)/(far-near), m11=-1, rest 0. Call it every frame right after BeginMode3D (it flushes the batch and sets the projection). Gotcha: a *Matrix deref-param in a headerless extern emits invalid C (extern void f(*Matrix)) — pass the struct by value (fn rlSetMatrixProjection(Matrix)) instead; the cstruct must be declared in a headered block so the C type resolves. For a believable far horizon, pair it with a coarse LOD underlay — one big low-res terrain mesh recentered on the camera as it drifts a snap cell — under the fine chunks, the seam hidden by fog.
Sprite tricks: a sprite sheet is one PNG; pick a frame with a source Rectangle{float(frame*48), 0, 48, 48} and rotate via DrawTexturePro around origin = Vector2{w/2, h/2}. Flip with a negative source height (Rectangle{0,0,88,-600}) — reuse one texture for both orientations. Center text with MeasureText.
raylib codes used by the games: keys SPACE 32, arrows LEFT 263 RIGHT 262 UP 265 DOWN 264, W 87 A 65 S 83 D 68, digits 1..4 = 49..52, R 82; mouse button left = 0. Esc is raylib's default window-close key (caught by WindowShouldClose()).
Verlet physics (position-based dynamics) — pure MFL, no C library. The solar-system sim drove the math module; the physics demo (machin-game-demo-physics) drives the simulation layer on top of it. Core patterns:
- Implicit velocity via
old_pos. A Particle stores pos (current) and old (previous frame). Velocity = pos - old. Integration per substep: new_pos = pos + (pos-old)*damping + gravity*dt². No Euler/Verlet distinction — one formula. (Matches MFL's value-semantic structs: you return a new []Particle each tick — fine for ~100–300 particles.)
- Distance constraint relaxation. A constraint holds two particle indices + a rest length. Each iteration projects both particles along the inter-particle axis:
delta = p2-p1; dist=len(delta); diff=(dist-rest_len)/(dist+ε); off=delta*diff*0.5; if !pinned: p1+=off; p2-=off. Run 3–5 iterations per substep for stable convergence. The *0.5 splits the correction equally (weight by inverse mass for non-uniform masses).
- Substeps decouple speed from stability. The physics tick is always
1/60; divide into 6–8 substeps of dt/6. Each substep runs integrate → constraint iterations → collision, so objects that move fast per frame don't tunnel.
- O(n²) sphere-sphere collision is fine for ≤200 particles (~40k distance checks/substep). Check
len(pos[j]-pos[i]) < r_i+r_j; push apart along the normalized delta by half the overlap each. Include the ε = 0.0000001 to avoid division by zero when particles are exactly coincident.
- Ground collision is a one-liner:
if pos.y < radius { pos.y = radius; old.y = pos.y + (old.y-pos.y)*bounce } where bounce=0.4 kills most of the rebound velocity.
- Velocity coloring for live diagnostics: compute
speed = len(pos-old)/dt and map it through a gradient (cold blue → cyan → yellow → hot red). One function, visualized instantly across all particles.
Ballistic physics (projectile simulation) — gravity + drag + wind. The ballistics demo (machin-game-demo-ballistics) adds the projectile layer for Tier 4 combat sims. Core patterns:
- Euler integration at a fixed timestep.
1/60s per step: v += a·dt; x += v·dt. Three forces: gravity (constant downward), quadratic drag (-drag_coeff · |v| · v — realistic because it scales with velocity squared), and wind (constant horizontal). The same integrator is used for both the predicted trajectory (computed ahead of time, drawn as dots) and the live projectile (frame-by-frame during flight). This makes the prediction trustworthy — it IS the simulation.
- Phase-state machine for interactive flow. A single
phase int (0=aiming, 1=firing, 2=impact) controls which inputs are read and what is drawn. Phase 0 reads arrows (angle/power) + SPACE (fire). Phase 1 advances the projectile and appends to a trail slice. Phase 2 shows hit/miss feedback with a countdown timer, then auto-resets to 0.
- Trajectory prediction with
compute_traj(). A function that runs the full integrator for up to 500 steps and returns ([]Vec3, steps). Called every frame during aiming. The result is rendered sparsely (every 3rd step, dim sphere) to show the arc without visual clutter.
- Hit detection with radial check. On ground impact (
y < 0), compute sqrt((px-tx)² + (pz-tz)²) and compare to the target radius. Display "HIT" or "MISS by Xm" (the distance computed from the impact position).
- Smooth arrow-key controls. Angle adjusts by
40°·dt per frame (frame-rate independent), clamped to [5°,85°]. Power adjusts by 20·dt, clamped to [4,50]. Using IsKeyDown for held-arrow adjustment and IsKeyPressed for the single-fire trigger (SPACE) keeps the controls feeling responsive.
- Trail as a growing slice. Each frame during phase 1,
trail = append(trail, pos). The trail is drawn as small spheres (dimmer toward the start). Capped at 500 entries to prevent unbounded memory growth.
First-person player controller — walking, jumping, pushing. The player demo (machin-game-demo-player) adds the character controller that every interactive 3D game needs. Core patterns:
- Movement from camera yaw. Horizontal movement direction is computed from
sin(yaw) / cos(yaw) — the same yaw that controls mouse look. W always moves where the player is looking, even when pitched up/down. Compute the 2D direction vector, normalize, scale by speed.
- Smooth acceleration. Instead of
vel = input_dir (instant), interpolate toward it: vel += (input - vel) * min(accel * dt, 1). For accel=12, this reaches 90% of target speed in ~0.2s. When no input, vel decays via friction vel *= (1 - friction*dt*10). This gives natural-feeling movement with no explicit state machine.
- Gravity + ground collision per frame.
pvy += GRAVITY*dt; pos += vel*dt. After integration, if foot_y < 0, snap to 0, zero pvy, set grounded=1, apply horizontal friction. This runs before platform checks.
- Platform stepping. For each platform, test
if foot_xz is within platform_xz bounds AND foot_y is near platform_top: snap foot to platform top + eye height, set grounded. This naturally handles stepping up and walking off edges. Multiple platforms require iterating the list each frame and testing each one.
- Walk/fly mode toggle. A single int
walk_mode (0/1) and F key flips it. In walk mode, the player controller runs. In fly mode, the free camera from solar operates. On transition to walk, transfer the fly camera's position/orientation to the player — fly to a spot, press F, land there.
- Object pushing with collision physics. Sphere-sphere collision between player and each object. The overlap is split: both player and object are pushed apart by half the overlap. Objects have their own gravity + ground collision + friction (separate from the player's). Multiple objects can be pushed simultaneously.
THE gotcha: no implicit int→float
This bit every game. MFL does not implicitly convert int→float. Only a flexible numeric literal (5, 560) promotes on contact with a float. A concrete int does not, and mixing it with a float is a hard int vs float compile error. Concrete ints come from: a function return (even func GROUND_Y() { n = 560 }), byte_at, len, a typed parameter, an int-slice element, and an f32/f64 cstruct field also won't take a concrete int.
Fixes (need float(), machin v0.43.0; int() goes the other way):
- Make world-coordinate constants floats:
func GROUND_Y() { n = 560.0 }.
- Wrap concrete-int math entering float:
160.0 + float((byte_at(r,0) << 8 | byte_at(r,1)) % 260).
- Concrete int into an
f32 field: Rectangle{float(frame*48), 0, 48, 48} (literals like 0/48 are fine).
- Keep loop indices pure
int: use a float accumulator (x = x + SPACING()), never i * SPACING() — multiplying the index by a float drags it to float and then arr[i] breaks.
- Going to an FFI
i32 arg from a float: int(GROUND_Y()).
Rule of thumb: keep each value in one numeric world; cross the boundary explicitly with float(x) / int(x).
Terminal (TUI) games
- Real-time input (machin v0.41.0):
raw_mode(1) puts the tty in cbreak/no-echo; read_key() is a non-blocking single-key read ("" if nothing waiting). Always raw_mode(0) before exit. input() is line-buffered and unusable for games.
- ANSI without
\x: MFL strings have no \x1b. Build ESC from hex: ESC := bytes_str(from_hex("1b")), then ESC+"[2J" (clear), ESC+"[H" (home), ESC+"[?25l"/[?25h" (hide/show cursor), ESC+"["+str(n)+"m" (color).
- One
print per frame (build the whole frame string, then flush()); per-cell printing flickers.
- Under a pipe/CI,
raw_mode no-ops and read_key falls back to a select poll — handy for a smoke test (snake runs straight into the wall and exits).
Other caveats
- A GUI binary is not self-contained — it links
libGL/libX11/raylib/audio and needs a display (and an audio device for sound). machin's no-dependency-binary property holds for the headless domain only. Say so in the README.
str(bool) works as of machin v0.42.0 ("true"/"false"); on older compilers it was a type error, so keep bools in control flow there.
No slice ranges FIXED in 0.124.0 — s[1:] / s[:2] / s[1:3] parse and return a fresh copy (never a view sharing the backing array; bounds checked unconditionally).
a < -b is a lexer trap. FIXED — re-verified on machin 0.123.0: h < -0.7, h < -7 and h < -g() all compile and evaluate correctly. The old workaround (a < 0.0 - b) is still correct but no longer required. Older compilers tightened < - into the channel-receive token <-.
- Random: there's no PRNG builtin;
byte_at(rand_bytes(1), 0) % N picks a value (a second byte < 26 ≈ a 10% branch).
- Frame timing: immediate-mode means never
sleep mid-game (it freezes the window). Drive animations/state with a per-frame tick counter and SetTargetFPS. (Terminal games do sleep(ms) per tick — they own the loop.)
- Mutating shared state: slices are reference-ish —
f(board) then board[i] = v is visible to the caller (return only summaries). Structs are value types (a copy), so a function can't mutate a caller's struct.
Signed int64 overflow is UB the C optimizer folds. FIXED in machin 0.124.0 — machin build now passes -fwrapv, so signed overflow wraps two's-complement on every target and optimization level, matching Go. Worth knowing what it was, because it is the canonical example of a -O0-correct/-O2-wrong bug: the textbook 64-bit FNV-1a round h = (h ^ b) * 1099511628211 collapsed to the constant INT64_MAX at -O2, so a state checksum silently stopped discriminating — two different worlds hashed identically, with no error anywhere. On a pre-0.124 compiler, keep every intermediate inside int64 (two lanes: a 32-bit lane with prime 16777619 masked 0xFFFFFFFF, plus a 31-bit lane with prime 33554467 masked 0x7FFFFFFF, packed as (hi & 0x7FFFFFFF) * 4294967296 + (lo & 0xFFFFFFFF)).
u8 cstruct fields wrap silently — and MFL and C then disagree. A tint that overshoots produces a different colour, not an error: col_mul(col(240,120,40), 1.5) → r=360 → the orange fire rendered green. The MFL side keeps the full int64 (so str(c.r) prints 360), and the truncation happens only in the generated mfl_to_<Struct> at the FFI boundary ((uint8_t)360 == 104) — so reading the field back cannot reveal it. Clamp before constructing a Color. As of 0.124.0, --safe range-checks narrow cstruct fields and panics with the field named (integer overflow (u8 field 'r')) instead of wrapping; the default build still plain-casts.
- Small parse/typecheck traps that misreport where they are.
Int literals are 64-bit signed — 0x9E3779B97F4A7C15 is a parse error. FIXED in 0.124.0: hex/binary/octal literals are accepted as 64-bit bit patterns (decimal above 2^63-1 still errors, with an actionable message). charat returns a string, not a byte — use byte_at(bytes(s), i), and note the typecheck error blames your accumulator, not charat. Multi-assign into a struct field doesn't parse. FIXED in 0.124.0: ns.rng, v = f() and xs[i], ok = f() work. The call is evaluated once, before the destinations' index expressions — Go evaluates left-hand index operands first, so the order differs if both have side effects. _ is not assignable as a sole destination. FIXED in 0.124.0: _ = f() discards the value and still evaluates the call. One inferred type per parameter, program-wide — a test helper ok(flag, name) cannot take both an int and a bool at different call sites; write two functions.
- Empty struct literals
T{} DO work and zero-fill every field. Since 0.124.0 non-empty []struct literals work too ([]P{P{1,2}, P{3,4}}), as do slice ranges (s[1:], s[:2], s[1:3]) — which always return a fresh copy, never a view.
- 2D platformer geometry: an actor taller than a tile cannot fit a one-tile gap. With
tile px cells, an actor of half-height hh needs 2*hh < tile — at 32 px, hh=17 (34 px) turns every one-tile corridor into an invisible wall you will debug for an hour; hh=15 (30 px) passes. Related: for a ladder joining an upper floor at row RU to a lower floor at row RL, the standable ladder-top tile replaces the upper floor tile (RU) and the rungs run RU+1 … RL-1, so an actor standing on the lower floor is already inside the bottom rung. Backwards, the ladder dead-ends into a solid slab.
- cstruct types CANNOT be fields in MFL
type structs. A cstruct declared in an extern block (Model, Color, Sound, Shader, …) can be a local variable, passed to functions, or stored in a slice ([]Model, []Color), but it cannot be a field of an MFL type struct. The C type map isn't available at the point the MFL struct's typedef generates. Workaround: use parallel slices — bodies := []Body{}; models := []Model{} — and index them together. (Discovered by machin-game-demo-solar.)
Non-empty []struct literals are not supported. FIXED in 0.124.0 — xs := []S{a, b, c} compiles. On a pre-0.124 compiler, build with append.
Each game drove a feature (the dogfood record)
| game |
exercises |
drove into machin |
| snake |
terminal real-time input |
raw_mode / read_key (v0.41.0) |
| 2048 |
raylib FFI: scalars + Color |
(composed; no new builtin) |
| flappy |
textures/sprites, f32 structs, struct-return |
float() int→float (v0.43.0) |
| simon |
audio: pointer-bearing Sound by value |
FFI opaque handles cstruct Name {} (v0.44.0) |
| 3d demo |
3D: Camera3D (struct of Vector3s); per-object rotation (rlgl matrix stack, headerless extern) |
FFI nested cstructs (v0.45.0); its libm orbit then drove native math builtins (v0.46.0); rotation = composition (no new feature) |
| anim |
2D procedural flow field (sin/cos/atan2/hypot over time) |
composition on native math + 2D FFI (no new feature) |
| terrain |
procedural mesh: per-vertex heights, flat-shaded, streamed via rlgl immediate mode |
composition (no new feature); points at the pointer/array FFI gap for real GPU VBOs |
| planet |
static GPU mesh: vertex/color arrays in raw memory, Mesh as a cstruct, upload to VRAM |
pointer/array FFI (v0.47.0): raw memory alloc/poke_*; then pointer cstruct fields + inout T* (v0.48.0) dropped the hard-coded offsets |
| cyberpunk |
infinite procedural world: fbm-noise terrain in GPU-mesh chunks, fly camera, grimy city districts, GPU-instanced flora, shader depth fog, skeletal fauna, 10 km draw distance |
noise2/noise3 Perlin (v0.49.0); buildings/instancing/shaders/fog/skeleton-animation/far-clip all composition (rlgl matrix stack + by-value Matrix cstruct) |
| solar |
3D solar-system sim: 7 noise3-textured GPU-mesh planets, fixed-timestep orbital sim (60 Hz accumulator), 6DOF fly camera, pure-MFL math3d module (Vec3 add/sub/scale/dot/cross/len/norm/lerp/dist) |
composition (no new builtin); drives the vector/math layer (feature #2) and fixed-timestep sim (feature #6) from the north star; surfaced the cstruct-in-struct-field + slice-literal caveats |
| physics |
verlet physics sandbox: ~100 particles falling/colliding/stacking, chain pendulum, velocity coloring, noise3-based random scene population |
composition (no new builtin); first constraint-based physics in pure MFL — verlet integration, distance constraint relaxation (iterations × substeps), O(n²) sphere-sphere collision, all over the Vec3 module; drives Tier 4 physics patterns |
| galaxy |
procedural spiral galaxy: ~5000 stars in 4 logarithmic spiral arms (θ = k·log(r)), spectral OBAFGKM colors, rlgl point-cloud render (RL_POINTS, one batch), slow rotation, core bulge |
composition (no new builtin); first demo to use rlgl point rendering (rlBegin(0) / rlColor4ub / rlVertex3f / rlEnd()); demonstrates procedural star placement, weighted spectral distribution, multi-pass point sizing, and the v3_rot_y() helper |
| ballistics |
interactive cannon sim: trajectory prediction with gravity + quadratic drag + wind, aim/power controls, live projectile with trail, hit/miss detection on a target |
composition (no new builtin); demonstrates ballistic Euler integration, the prediction-vs-execution pattern (same integrator for both), phase-state machine (aim→fire→impact), interactive HUD with real-time angle/power feedback, and IsKeyPressed for single-fire controls |
| ressort / Demolition Man |
a 2D ENGINE, not a game: Torque2D-style declarative scenes/prefabs/behaviours as data over a Spring-style deterministic 60 Hz fixed-step sim with a hard synced/unsynced split; record → replay → headless verify (a playthrough is a 2 kB text artifact that proves it lands on the same per-frame checksum); ASCII tilemaps + swept AABB collision; text-authored pixel sprites (zero binary assets) |
composition (no new builtin) — but it surfaced two real language hazards: signed int64 overflow is UB the optimizer folds (a 64-bit FNV checksum collapsed to INT64_MAX and silently stopped discriminating) and u8 cstruct fields wrap silently (fire rendered green). Both are in the caveats above; both point at machin's own honesty story |
| player |
first-person 3D playground: walking character with smooth acceleration, platform stepping (4 heights), 5 pushable spheres with gravity physics, walk/fly camera toggle (F) |
composition (no new builtin); demonstrates the first-person player controller layer — WASD movement relative to camera yaw, smooth acceleration vel += (input - vel)·min(accel·dt,1), gravity + ground + platform collision, walk/fly mode toggle, pushable-object physics (sphere-sphere collision with player, object gravity + friction), crosshair + HUD in screen space |
When a new game hits a wall, that's the point: fill the gap in the language, release, and note it here.
1---2name: machin-gamedev3description: Build native games and interactive desktop/terminal apps in machin (MFL) — the canonical setup, build-and-verify workflow, raylib C-FFI surface, audio, and the hard-won caveats/gotchas. Use when writing or debugging a machin game (terminal TUI or raylib GUI), or any machin program that draws a window, plays sound, or reads real-time input. Covers terminal TUI, raylib GUI/audio, 3D cameras, GPU meshes (pointer/array FFI), instancing, shaders, procedural worlds (noise), fixed-timestep sim loops, a pure-MFL math3d module (Vec3), verlet physics (position-based dynamics, constraint relaxation, collision), rlgl point-cloud rendering (RL_POINTS for particle systems and star fields), ballistic physics (gravity + quadratic drag + wind, trajectory prediction), and first-person player controllers (walking, jumping, platform stepping, object pushing, walk/fly toggle). Distilled from machin-game-demo-snake / -2048 / -flappy / -simon / -3d / -terrain / -planet / -cyberpunk / -solar / -physics / -galaxy / -ballistics / -pla4---56# Building games in machin78machin (MFL) compiles to a native binary through C, and reaches real games two ways:910- **Terminal (TUI):** ANSI escapes for drawing + `raw_mode`/`read_key` for input. A no-dependency single binary. → [machin-game-demo-snake](https://github.com/javimosch/machin-game-demo-snake) (Snake).11- **GUI / audio:** [raylib](https://www.raylib.com/) through machin's C **FFI** — a real OpenGL window, textures, sound. Links the system graphics/audio stack, so **not** self-contained. → [machin-game-demo-2048](https://github.com/javimosch/machin-game-demo-2048) (shapes+text), [machin-game-demo-flappy](https://github.com/javimosch/machin-game-demo-flappy) (sprites/textures), [machin-game-demo-simon](https://github.com/javimosch/machin-game-demo-simon) (audio), [machin-game-demo-3d](https://github.com/javimosch/machin-game-demo-3d) (3D + per-object rotation), [machin-game-demo-anim](https://github.com/javimosch/machin-game-demo-anim) (2D procedural), [machin-game-demo-terrain](https://github.com/javimosch/machin-game-demo-terrain) (immediate-mode mesh), [machin-game-demo-planet](https://github.com/javimosch/machin-game-demo-planet) (GPU mesh / pointer FFI), [machin-game-demo-cyberpunk](https://github.com/javimosch/machin-game-demo-cyberpunk) (infinite noise world + fly camera), [machin-game-demo-solar](https://github.com/javimosch/machin-game-demo-solar) (solar-system sim, fixed-timestep, math3d module), [machin-game-demo-physics](https://github.com/javimosch/machin-game-demo-physics) (verlet physics sandbox, constraint relaxation, collision), [machin-game-demo-galaxy](https://github.com/javimosch/machin-game-demo-galaxy) (procedural spiral galaxy, rlgl point cloud), [machin-game-demo-ballistics](https://github.com/javimosch/machin-game-demo-ballistics) (interactive cannon sim, ballistic trajectory prediction, drag + wind), [machin-game-demo-player](https://github.com/javimosch/machin-game-demo-player) (first-person 3D playground, walk/fly toggle, platform stepping, object pushing).1213Where this domain is heading: [`docs/NORTH-STAR-GAMEDEV.md`](../../docs/NORTH-STAR-GAMEDEV.md) (tiers + the gap roadmap: pointer/array FFI for real GPU meshes, a vector/matrix layer, a noise builtin, shaders, callbacks).1415Each game is its own public repo with a `build.sh` (`machin encode src.src > app.mfl && machin build app.mfl -o app`), a `README.md`, a repo-root `SKILL.md`, and committed `assets/`. This skill is the shared substrate; each game's SKILL.md has its specifics.1617**`machin encode a.src b.src c.src` accepts multiple files and concatenates them — that *is* the module system.** No includes, no package manager: order the files, hand them all to `encode`, and the whole program typechecks together (so an engine module may call a hook the game file defines later by name). This is how an engine gets split from the game on top of it — see machin-games (`framework/vec2.src framework/physics.src games/phys/x.src`) and machin-ressort (7 engine modules + 5 game modules).1819> Run `machin guide` for the version-exact language surface (builtins, idioms, gotchas). This skill is about *games* specifically.2021## Build & verify workflow (this environment)2223- **raylib, no root:** there's no system raylib and no passwordless sudo. The game `build.sh` auto-vendors raylib's prebuilt **static** release (`raylib-5.0_linux_amd64.tar.gz`) into `vendor/`, then injects `cflags "-I.../include -L.../lib"` + `link ":libraylib.a"` into a *throwaway* `.mfl` so the committed source stays system-style (`link "raylib"`). A copy is cached at `/tmp/rl/raylib-5.0_linux_amd64`. `build.sh` prefers a system raylib (`pkg-config --exists raylib`) when present.24- **Display + audio are live:** `DISPLAY=:0`, PulseAudio works. Run a game backgrounded, `sleep`, then `kill` it.25- **Screenshot to verify rendering:** prefer raylib's own `fn TakeScreenshot(string)` over ImageMagick — it captures the *game framebuffer* at an exact frame, needs no window manager, and doesn't grab the whole 1920×1080 root. **Gotcha: it strips the directory** and writes to the CWD (`TakeScreenshot("docs/x.png")` → `./x.png`); move it afterwards. Build it into a CLI verb — `game shot out.png --at 600` renders to sim frame 600, shoots, and exits — so any frame is reproducible from the shell. Fallback if you need the desktop instead: `DISPLAY=:0 import -window root /tmp/shot.png` (`scrot`/`maim`/`grim`/`xdotool` are NOT installed). Read the PNG back to confirm the frame.26- **Can't inject keystrokes** (no xdotool). Verify *gameplay* with (a) a **permanent deterministic bot** behind a CLI flag (`--bot`) that reads world state and returns input bits — far better than the old advice of `sed`-patching a throwaway autopilot: it survives refactors, doubles as an attract mode and a CI exerciser, and its output is reproducible; or (b) **headless logic tests** — factor the pure logic (slide/merge, collision) and run it with `machin run`, asserting output. Do the logic test first; it's faster and catches the type errors. Keep the bot's own state (waypoint index, etc.) **outside** the simulation, or a recorded replay — which doesn't run the bot — will diverge.27- **Assets load by relative path** → run the binary from the repo root (`./app`, not from `/tmp`).28- **`machin encode` runs the typechecker**, so most errors show at encode time (no `cc` needed). `machin build x.mfl --emit-c` prints the generated C when you need to see the FFI marshaling.2930## The raylib FFI surface3132One `extern "raylib" { … }` block is the entire C boundary; everything else is pure MFL. Template (declare only what you call):3334```35extern "raylib" {36 header "raylib.h"37 link "raylib" link "GL" link "m" link "pthread" link "dl" link "rt" link "X11"38 cstruct Color { r u8 g u8 b u8 a u8 }39 cstruct Texture2D { id u32 width i32 height i32 mipmaps i32 format i32 } // all-int handle40 cstruct Rectangle { x f32 y f32 width f32 height f32 } // f32 fields41 cstruct Vector2 { x f32 y f32 }42 cstruct Sound {} // OPAQUE handle (v0.44.0)43 fn InitWindow(i32, i32, string) fn SetTargetFPS(i32) fn WindowShouldClose() bool44 fn BeginDrawing() fn EndDrawing() fn ClearBackground(Color) fn CloseWindow()45 fn DrawRectangle(i32, i32, i32, i32, Color) fn DrawCircle(i32, i32, f32, Color)46 fn DrawText(string, i32, i32, i32, Color) fn MeasureText(string, i32) i3247 fn LoadTexture(string) Texture2D fn DrawTexture(Texture2D, i32, i32, Color)48 fn DrawTextureRec(Texture2D, Rectangle, Vector2, Color)49 fn DrawTexturePro(Texture2D, Rectangle, Rectangle, Vector2, f32, Color)50 fn IsKeyPressed(i32) bool fn IsMouseButtonPressed(i32) bool fn GetMouseX() i32 fn GetMouseY() i3251 fn InitAudioDevice() fn LoadSound(string) Sound fn PlaySound(Sound) fn CloseAudioDevice()52}53```5455FFI tiers, all verified working:56- **Scalars + by-value structs** (`Color`, and `f32`-field `Rectangle`/`Vector2`) passed by value.57- **Struct return** — `LoadTexture` returns `Texture2D` (an all-int handle) by value.58- **Opaque handles** (`cstruct Sound {}`, machin **v0.44.0**) — a by-value C struct that contains pointers (`Sound`/`Music`/`Font`). machin holds the real C struct and passes it back to fns without naming its fields. Receive it from a fn, store it (a var **or** `[]Sound`), pass it on; **no** construct or `.field`. (A single pointer is simpler: the `ptr` FFI type, an `int`.)59- **Nested cstructs** (machin **v0.45.0**) — a `cstruct` field may be another `cstruct`, marshaled recursively. Declare the inner one **first**. Required for **3D** and 2D cameras: `cstruct Vector3 { x f32 y f32 z f32 }` then `cstruct Camera3D { position Vector3 target Vector3 up Vector3 fovy f32 projection i32 }`; construct with nested literals `Camera3D{Vector3{12.0,7.0,0.0}, Vector3{0.0,1.5,0.0}, Vector3{0.0,1.0,0.0}, 45.0, 0}` and pass by value to `BeginMode3D`.6061**3D** (see [machin-game-demo-3d](https://github.com/javimosch/machin-game-demo-3d)): bracket 3D draws with `BeginMode3D(cam)` / `EndMode3D()`; `DrawCube(Vector3, f32,f32,f32, Color)`, `DrawCubeWires`, `DrawSphere(Vector3, f32, Color)`, `DrawGrid(i32, f32)`; any `DrawText` after `EndMode3D` is screen-space. Rebuild the `Camera3D` each frame rather than mutating it. `projection` `0` = `CAMERA_PERSPECTIVE`.6263**Per-object transforms (rotation/scale).** `DrawCube`/`DrawSphere` only translate (axis-aligned). To rotate or scale an object, use raylib's immediate-mode matrix stack from **rlgl**: `rlPushMatrix` → `rlTranslatef(x,y,z)` → `rlRotatef(deg, ax,ay,az)` / `rlScalef(x,y,z)` → draw at the **local origin** → `rlPopMatrix`. Those functions live in `rlgl.h`, **not** `raylib.h` — but their symbols are in `libraylib.a`, so declare them in a **headerless extern block** (machin emits the prototypes itself; symbols link from the raylib block's libs). No new machin feature — the existing scalar/`void` FFI carries it:6465```66extern "rlgl" { fn rlPushMatrix() fn rlPopMatrix() fn rlTranslatef(f32,f32,f32) fn rlRotatef(f32,f32,f32,f32) fn rlScalef(f32,f32,f32) }67// spin a cube in place:68rlPushMatrix() rlTranslatef(x,y,z) rlRotatef(deg, 0.0,1.0,0.0) DrawCube(v3(0.0,0.0,0.0), s,s,s, c) rlPopMatrix()69```7071(The same stack works in 2D between `BeginDrawing`/`EndDrawing`. The **headerless-extern** trick is general: any `libraylib.a`/system-lib symbol that's scalar/`void` can be reached this way without its header.)7273**Procedural meshes (immediate mode)** — see [machin-game-demo-terrain](https://github.com/javimosch/machin-game-demo-terrain). Compute geometry in MFL and stream it triangle-by-triangle with rlgl: `extern "rlgl" { fn rlBegin(i32) fn rlEnd() fn rlColor4ub(u8,u8,u8,u8) fn rlVertex3f(f32,f32,f32) fn rlDisableBackfaceCulling() }`; `rlBegin(4)` (= `RL_TRIANGLES`), one `rlColor4ub` then three `rlVertex3f` per triangle, `rlEnd()` — inside `BeginMode3D`. **Flat-shade in MFL** (no light shader otherwise): face normal = cross of two edges, `sh = 0.4 + 0.6*clamp(dot(n_unit, lightDir),0,1)` (`sqrt` to normalize), multiply color by `sh`. `rlDisableBackfaceCulling()` if the surface is seen from both sides (else winding-dependent triangles vanish → thin sliver). ~10–15k `rlVertex3f`/frame is fine.7475**Point clouds via rlgl (RL_POINTS)** — see [machin-game-demo-galaxy](https://github.com/javimosch/machin-game-demo-galaxy). For thousands of individual points (stars, particles, sparks, dust), use `rlBegin(0)` (= `RL_POINTS`) instead of per-item `DrawSphere` calls. The same rlgl extern block applies: `rlBegin(0)`, then per point `rlColor4ub(r,g,b,255)` + `rlVertex3f(x,y,z)`, then `rlEnd()`. This renders the entire set in **one GPU draw call** vs N calls for N `DrawSphere`s. Good for ~5000+ points at 60fps. Brightness/visual size can be faked by emitting **multiple overlapping `rlVertex3f` at the same position** — 1 pass = 1 pixel, 2 passes = 2 pixels (medium bright), 3+ passes = bright/large. Combine with a dark `ClearBackground` (near-black blue: `col(2,2,8)`) for a space/atmosphere look. The headerless extern trick works: `extern "rlgl" { fn rlBegin(i32) fn rlEnd() fn rlColor4ub(u8,u8,u8,u8) fn rlVertex3f(f32,f32,f32) }`.7677**GPU meshes / pointer-array FFI** (v0.47.0–v0.48.0). Build C buffers in raw memory and a C struct as a cstruct, then hand them to the GPU. Raw memory (pointers are `int`s): `p := alloc(nbytes)` (zeroed), `poke_f32`/`poke_i32`/`poke_u8`/`poke_u16`/`poke_ptr(p, byteOffset, v)`, `peek_f32`/`peek_i32`, `free(p)`. Pointer params: `ptr` (a raw pointer, `void*` → any `T*`); `*T` (deref a raw pointer, pass the struct by value); **`T*` inout** (pass a cstruct *variable* by pointer, writing the modified struct back). A `cstruct` **field** may be `ptr`, so you declare the struct and the C compiler lays it out. Mesh flow: `alloc`+`poke` the vertex/color arrays, then `mesh := Mesh{vcount, vcount/3, vbuf, cbuf, 0, 0}` with `cstruct Mesh { vertexCount i32 triangleCount i32 vertices ptr colors ptr vaoId u32 vboId ptr }` (field names match raylib's), `UploadMesh(mesh, false)` (inout `Mesh*`; writes vao/vbo back), `LoadModelFromMesh(mesh)` → `Model` (opaque), then `DrawModelEx` each frame. **Build once**, GPU-resident — vs. immediate mode which re-emits every frame. (see machin-game-demo-planet)7879**Procedural worlds + infinite terrain + fly camera** — see [machin-game-demo-cyberpunk](https://github.com/javimosch/machin-game-demo-cyberpunk). **Noise** (v0.49.0): `noise2(x,y)`/`noise3(x,y,z)` are deterministic Perlin, ~`[-1,1]`; layer them into **fbm** in MFL (`s += amp*noise2(x*fr,y*fr); amp*=0.5; fr*=2`) for terrain/placement. **Infinite streaming:** a small slot pool — pre-fill `[]Model` with zeroed `Model{}` (a zero opaque handle is a safe no-op for `DrawModel`/`UnloadModel`) + a `cused[]` flag + `ccx/ccz`; each frame unload chunks out of `RAD` and build in-range ones into free slots, so only the leading edge regenerates. Bake **world coords** into each chunk mesh and `DrawModel` at the origin. **Fly camera:** `forward=(cos(pitch)*sin(yaw), sin(pitch), cos(pitch)*cos(yaw))`, `right=normalize(cross(forward,up))=(-fz,0,fx)/len`, driven by `GetMouseDelta` (yaw/pitch) + `IsKeyDown` + `GetFrameTime`; call `DisableCursor()` for mouse-look. raylib takes ownership of an uploaded mesh's `alloc`'d buffers (frees them on `UnloadModel` — don't free yourself). **Budget chunk builds** (≤ a few mesh builds/frame) so flying never hitches the mouse.8081**Shaders / post-processing** (composition — no new machin feature; see machin-game-demo-cyberpunk). `Shader` is `{id u32, locs ptr}` (a pointer cstruct field), `RenderTexture2D` is `{id u32, texture Texture2D, depth Texture2D}` (nested cstructs); `LoadShaderFromMemory(vs, fs)` (GLSL as `\n`-escaped single-line strings; standard names `vertexPosition`/`vertexTexCoord`/`vertexColor`/`mvp`/`texture0`), `SetShaderValue(sh, loc, ptr, kind)` (`kind` 0/1/2 = float/vec2/vec3, value from a small `alloc`'d buffer), `SetShaderValueTexture` (bind a sampler). Post-process: `BeginTextureMode(rt)` → draw scene → `EndTextureMode`, then `BeginShaderMode(sh)` → `DrawTextureRec(rt.texture, Rectangle{0,0,W,-H}, ...)` (**negative height flips** the render target) → `EndShaderMode`. **Depth fog:** linearize `texture(depthTex,uv).r` with near/far and skip `depth>0.9995` (sky).8283**GPU instancing** (thousands of meshes in one `DrawMeshInstanced` — also composition; flora in machin-game-demo-cyberpunk). `Material` is a partial cstruct `{ shader Shader, maps ptr }` (the C `params[4]` array can't be a cstruct field, but it's left zeroed by marshaling — fine). Instancing VS declares `in mat4 instanceTransform` (`gl_Position = mvp*instanceTransform*vec4(pos,1)`). **raylib 5.0 has no `SHADER_LOC_VERTEX_INSTANCE_TX`** — it uses `SHADER_LOC_MATRIX_MODEL` (index **9**) for the instance attribute: `poke_i32(ish.locs, 36, GetShaderLocationAttrib(ish, "instanceTransform"))`. `md := LoadMaterialDefault(); imat := Material{ish, md.maps}`. Build the per-instance `Matrix[]` in raw memory: **translation @ byte 12/28/44, scale @ 0/20/40, 1.0 @ 60**, rest zero. Keep positions deterministic (world grid + `noise2`) so instances don't flicker. **Math:** machin has **native** math builtins (v0.46.0) — `sin cos tan asin acos atan atan2 sqrt cbrt pow exp log log2 log10 floor ceil round trunc abs fmod hypot` and `pi()` (numeric in, `float` out; `-lm` linked only when used). So an orbit is just `v3(R*cos(a), h, R*sin(a))`. (An `extern "m"` of the same name still shadows the builtin if you want a specific libm signature.)8485**Procedural skeletal animation** (fauna in machin-game-demo-cyberpunk — composition over the rlgl matrix stack, no new feature). Pose a bone hierarchy with **forward kinematics**: nest `rlPushMatrix`/`rlTranslatef`(to the joint)/`rlRotatef`(the joint angle)/draw-at-local-origin/`rlPopMatrix`. A leg is two segments — translate to the hip, rotate the whole leg, `DrawCube` the upper segment, `rlTranslatef` down to the knee, `rlRotatef` the shin, `DrawCube` the lower segment. Drive a **diagonal gait** from `sin`/`cos(t*speed)`: legs 0&3 share one phase, 1&2 the opposite (`+π`); `swing = sin(phase)*A` rotates the hip, a `lift` term (`cos(phase)` clamped `>0`) bends the knee on the forward stroke, and a body bob (`sin(t*speed*2)*h`) sells the weight shift. Wrap the whole creature in one more `rlPush`/`rlTranslatef`(world pos)/`rlRotatef`(heading)/`rlPop`. Snap to terrain height each frame. No skinned mesh needed — nested transforms over primitives carry it.8687**Pushing the draw distance past raylib's ~1 km clip.** raylib's auto perspective uses a fixed far plane (`rlSetClipPlanes` does **not** exist in 5.0). Override it with **`rlSetMatrixProjection`** (rlgl), passing a `Matrix` **by value** — declare `cstruct Matrix { m0 f32 m4 f32 m8 f32 m12 f32 m1 f32 m5 f32 m9 f32 m13 f32 m2 f32 m6 f32 m10 f32 m14 f32 m3 f32 m7 f32 m11 f32 m15 f32 }` (raylib's field order) and build a perspective matrix with your own far plane: `m0=f/aspect, m5=f` (`f=1/tan(fovy/2)`), `m10=-(far+near)/(far-near)`, `m14=-(2·far·near)/(far-near)`, `m11=-1`, rest 0. Call it **every frame right after `BeginMode3D`** (it flushes the batch and sets the projection). Gotcha: a `*Matrix` deref-param in a **headerless** extern emits invalid C (`extern void f(*Matrix)`) — pass the struct **by value** (`fn rlSetMatrixProjection(Matrix)`) instead; the cstruct must be declared in a **headered** block so the C type resolves. For a believable far horizon, pair it with a **coarse LOD underlay** — one big low-res terrain mesh recentered on the camera as it drifts a snap cell — under the fine chunks, the seam hidden by fog.8889Sprite tricks: a **sprite sheet** is one PNG; pick a frame with a source `Rectangle{float(frame*48), 0, 48, 48}` and rotate via `DrawTexturePro` around `origin = Vector2{w/2, h/2}`. **Flip** with a negative source height (`Rectangle{0,0,88,-600}`) — reuse one texture for both orientations. **Center text** with `MeasureText`.9091raylib codes used by the games: keys `SPACE 32`, arrows `LEFT 263 RIGHT 262 UP 265 DOWN 264`, `W 87 A 65 S 83 D 68`, digits `1..4 = 49..52`, `R 82`; mouse button `left = 0`. Esc is raylib's default window-close key (caught by `WindowShouldClose()`).9293**Verlet physics (position-based dynamics) — pure MFL, no C library.** The solar-system sim drove the math module; the physics demo ([machin-game-demo-physics](https://github.com/javimosch/machin-game-demo-physics)) drives the simulation layer on top of it. Core patterns:9495- **Implicit velocity via `old_pos`.** A `Particle` stores `pos` (current) and `old` (previous frame). Velocity = `pos - old`. Integration per substep: `new_pos = pos + (pos-old)*damping + gravity*dt²`. No Euler/Verlet distinction — one formula. (Matches MFL's value-semantic structs: you return a new `[]Particle` each tick — fine for ~100–300 particles.)96- **Distance constraint relaxation.** A constraint holds two particle indices + a rest length. Each iteration projects both particles along the inter-particle axis: `delta = p2-p1; dist=len(delta); diff=(dist-rest_len)/(dist+ε); off=delta*diff*0.5; if !pinned: p1+=off; p2-=off`. Run 3–5 iterations per substep for stable convergence. The `*0.5` splits the correction equally (weight by inverse mass for non-uniform masses).97- **Substeps decouple speed from stability.** The physics tick is always `1/60`; divide into 6–8 substeps of `dt/6`. Each substep runs integrate → constraint iterations → collision, so objects that move fast per frame don't tunnel.98- **O(n²) sphere-sphere collision** is fine for ≤200 particles (~40k distance checks/substep). Check `len(pos[j]-pos[i]) < r_i+r_j`; push apart along the normalized delta by half the overlap each. Include the `ε = 0.0000001` to avoid division by zero when particles are exactly coincident.99- **Ground collision** is a one-liner: `if pos.y < radius { pos.y = radius; old.y = pos.y + (old.y-pos.y)*bounce }` where `bounce=0.4` kills most of the rebound velocity.100- **Velocity coloring** for live diagnostics: compute `speed = len(pos-old)/dt` and map it through a gradient (cold blue → cyan → yellow → hot red). One function, visualized instantly across all particles.101102**Ballistic physics (projectile simulation) — gravity + drag + wind.** The ballistics demo ([machin-game-demo-ballistics](https://github.com/javimosch/machin-game-demo-ballistics)) adds the projectile layer for Tier 4 combat sims. Core patterns:103104- **Euler integration at a fixed timestep.** `1/60s` per step: `v += a·dt; x += v·dt`. Three forces: gravity (constant downward), quadratic drag (`-drag_coeff · |v| · v` — realistic because it scales with velocity squared), and wind (constant horizontal). The same integrator is used for both the **predicted trajectory** (computed ahead of time, drawn as dots) and the **live projectile** (frame-by-frame during flight). This makes the prediction trustworthy — it IS the simulation.105- **Phase-state machine for interactive flow.** A single `phase` int (0=aiming, 1=firing, 2=impact) controls which inputs are read and what is drawn. Phase 0 reads arrows (angle/power) + SPACE (fire). Phase 1 advances the projectile and appends to a trail slice. Phase 2 shows hit/miss feedback with a countdown timer, then auto-resets to 0.106- **Trajectory prediction with `compute_traj()`.** A function that runs the full integrator for up to 500 steps and returns `([]Vec3, steps)`. Called every frame during aiming. The result is rendered sparsely (every 3rd step, dim sphere) to show the arc without visual clutter.107- **Hit detection with radial check.** On ground impact (`y < 0`), compute `sqrt((px-tx)² + (pz-tz)²)` and compare to the target radius. Display "HIT" or "MISS by Xm" (the distance computed from the impact position).108- **Smooth arrow-key controls.** Angle adjusts by `40°·dt` per frame (frame-rate independent), clamped to `[5°,85°]`. Power adjusts by `20·dt`, clamped to `[4,50]`. Using `IsKeyDown` for held-arrow adjustment and `IsKeyPressed` for the single-fire trigger (SPACE) keeps the controls feeling responsive.109- **Trail as a growing slice.** Each frame during phase 1, `trail = append(trail, pos)`. The trail is drawn as small spheres (dimmer toward the start). Capped at 500 entries to prevent unbounded memory growth.110111**First-person player controller — walking, jumping, pushing.** The player demo ([machin-game-demo-player](https://github.com/javimosch/machin-game-demo-player)) adds the character controller that every interactive 3D game needs. Core patterns:112113- **Movement from camera yaw.** Horizontal movement direction is computed from `sin(yaw)` / `cos(yaw)` — the same `yaw` that controls mouse look. W always moves where the player is looking, even when pitched up/down. Compute the 2D direction vector, normalize, scale by speed.114- **Smooth acceleration.** Instead of `vel = input_dir` (instant), interpolate toward it: `vel += (input - vel) * min(accel * dt, 1)`. For `accel=12`, this reaches 90% of target speed in ~0.2s. When no input, `vel` decays via friction `vel *= (1 - friction*dt*10)`. This gives natural-feeling movement with no explicit state machine.115- **Gravity + ground collision per frame.** `pvy += GRAVITY*dt; pos += vel*dt`. After integration, if `foot_y < 0`, snap to 0, zero `pvy`, set `grounded=1`, apply horizontal friction. This runs before platform checks.116- **Platform stepping.** For each platform, test `if foot_xz is within platform_xz bounds AND foot_y is near platform_top`: snap `foot` to platform top + eye height, set grounded. This naturally handles stepping up and walking off edges. Multiple platforms require iterating the list each frame and testing each one.117- **Walk/fly mode toggle.** A single int `walk_mode` (0/1) and F key flips it. In walk mode, the player controller runs. In fly mode, the free camera from solar operates. On transition to walk, transfer the fly camera's position/orientation to the player — fly to a spot, press F, land there.118- **Object pushing with collision physics.** Sphere-sphere collision between player and each object. The overlap is split: both player and object are pushed apart by half the overlap. Objects have their own gravity + ground collision + friction (separate from the player's). Multiple objects can be pushed simultaneously.119120## THE gotcha: no implicit int→float121122This bit every game. MFL does **not** implicitly convert `int`→`float`. Only a *flexible numeric literal* (`5`, `560`) promotes on contact with a float. A **concrete** int does **not**, and mixing it with a float is a hard `int vs float` compile error. Concrete ints come from: **a function return** (even `func GROUND_Y() { n = 560 }`), `byte_at`, `len`, a **typed parameter**, an **`int`-slice element**, and an `f32`/`f64` **cstruct field** also won't take a concrete int.123124Fixes (need `float()`, machin **v0.43.0**; `int()` goes the other way):125- Make world-coordinate constants **floats**: `func GROUND_Y() { n = 560.0 }`.126- Wrap concrete-int math entering float: `160.0 + float((byte_at(r,0) << 8 | byte_at(r,1)) % 260)`.127- Concrete int into an `f32` field: `Rectangle{float(frame*48), 0, 48, 48}` (literals like `0`/`48` are fine).128- Keep loop indices pure `int`: use a float **accumulator** (`x = x + SPACING()`), never `i * SPACING()` — multiplying the index by a float drags it to float and then `arr[i]` breaks.129- Going to an FFI `i32` arg from a float: `int(GROUND_Y())`.130131Rule of thumb: keep each value in one numeric world; cross the boundary explicitly with `float(x)` / `int(x)`.132133## Terminal (TUI) games134135- **Real-time input** (machin **v0.41.0**): `raw_mode(1)` puts the tty in cbreak/no-echo; `read_key()` is a non-blocking single-key read (`""` if nothing waiting). Always `raw_mode(0)` before exit. `input()` is line-buffered and unusable for games.136- **ANSI without `\x`:** MFL strings have no `\x1b`. Build ESC from hex: `ESC := bytes_str(from_hex("1b"))`, then `ESC+"[2J"` (clear), `ESC+"[H"` (home), `ESC+"[?25l"`/`[?25h"` (hide/show cursor), `ESC+"["+str(n)+"m"` (color).137- **One `print` per frame** (build the whole frame string, then `flush()`); per-cell printing flickers.138- Under a pipe/CI, `raw_mode` no-ops and `read_key` falls back to a `select` poll — handy for a smoke test (snake runs straight into the wall and exits).139140## Other caveats141142- **A GUI binary is not self-contained** — it links `libGL`/`libX11`/raylib/audio and needs a display (and an audio device for sound). machin's no-dependency-binary property holds for the headless domain only. Say so in the README.143- **`str(bool)` works** as of machin **v0.42.0** (`"true"`/`"false"`); on older compilers it was a type error, so keep bools in control flow there.144- ~~**No slice ranges**~~ **FIXED in 0.124.0** — `s[1:]` / `s[:2]` / `s[1:3]` parse and return a fresh copy (never a view sharing the backing array; bounds checked unconditionally).145- ~~**`a < -b` is a lexer trap.**~~ **FIXED** — re-verified on machin **0.123.0**: `h < -0.7`, `h < -7` and `h < -g()` all compile and evaluate correctly. The old workaround (`a < 0.0 - b`) is still correct but no longer required. Older compilers tightened `< -` into the channel-receive token `<-`.146- **Random:** there's no PRNG builtin; `byte_at(rand_bytes(1), 0) % N` picks a value (a second byte `< 26` ≈ a 10% branch).147- **Frame timing:** immediate-mode means never `sleep` mid-game (it freezes the window). Drive animations/state with a per-frame tick counter and `SetTargetFPS`. (Terminal games *do* `sleep(ms)` per tick — they own the loop.)148- **Mutating shared state:** slices are reference-ish — `f(board)` then `board[i] = v` is visible to the caller (return only summaries). Structs are value types (a copy), so a function can't mutate a caller's struct.149- ~~**Signed int64 overflow is UB the C optimizer folds.**~~ **FIXED in machin 0.124.0** — `machin build` now passes `-fwrapv`, so signed overflow wraps two's-complement on every target and optimization level, matching Go. Worth knowing what it was, because it is the canonical example of a `-O0`-correct/`-O2`-wrong bug: the textbook 64-bit FNV-1a round `h = (h ^ b) * 1099511628211` **collapsed to the constant `INT64_MAX`** at `-O2`, so a state checksum silently stopped discriminating — two different worlds hashed identically, with no error anywhere. On a pre-0.124 compiler, keep every intermediate inside int64 (two lanes: a 32-bit lane with prime `16777619` masked `0xFFFFFFFF`, plus a 31-bit lane with prime `33554467` masked `0x7FFFFFFF`, packed as `(hi & 0x7FFFFFFF) * 4294967296 + (lo & 0xFFFFFFFF)`).150- **`u8` cstruct fields wrap silently — and MFL and C then disagree.** A tint that overshoots produces a *different colour*, not an error: `col_mul(col(240,120,40), 1.5)` → r=360 → the orange fire rendered **green**. The MFL side keeps the full `int64` (so `str(c.r)` prints `360`), and the truncation happens only in the generated `mfl_to_<Struct>` at the FFI boundary (`(uint8_t)360 == 104`) — so reading the field back **cannot** reveal it. Clamp before constructing a `Color`. As of **0.124.0**, `--safe` range-checks narrow cstruct fields and panics with the field named (`integer overflow (u8 field 'r')`) instead of wrapping; the default build still plain-casts.151- **Small parse/typecheck traps that misreport where they are.** ~~Int literals are 64-bit signed — `0x9E3779B97F4A7C15` is a *parse* error.~~ **FIXED in 0.124.0**: hex/binary/octal literals are accepted as 64-bit bit patterns (decimal above 2^63-1 still errors, with an actionable message). `charat` returns a **string**, not a byte — use `byte_at(bytes(s), i)`, and note the typecheck error blames your *accumulator*, not `charat`. ~~Multi-assign into a struct field doesn't parse.~~ **FIXED in 0.124.0**: `ns.rng, v = f()` and `xs[i], ok = f()` work. The call is evaluated once, *before* the destinations' index expressions — Go evaluates left-hand index operands first, so the order differs if both have side effects. ~~`_` is not assignable as a sole destination.~~ **FIXED in 0.124.0**: `_ = f()` discards the value and still evaluates the call. **One inferred type per parameter, program-wide** — a test helper `ok(flag, name)` cannot take both an int and a bool at different call sites; write two functions.152- **Empty struct literals `T{}` DO work** and zero-fill every field. Since **0.124.0** non-empty `[]struct` literals work too (`[]P{P{1,2}, P{3,4}}`), as do **slice ranges** (`s[1:]`, `s[:2]`, `s[1:3]`) — which always return a fresh **copy**, never a view.153- **2D platformer geometry: an actor taller than a tile cannot fit a one-tile gap.** With `tile` px cells, an actor of half-height `hh` needs `2*hh < tile` — at 32 px, `hh=17` (34 px) turns every one-tile corridor into an *invisible wall* you will debug for an hour; `hh=15` (30 px) passes. Related: for a ladder joining an upper floor at row `RU` to a lower floor at row `RL`, the standable ladder-top tile replaces the **upper** floor tile (`RU`) and the rungs run `RU+1 … RL-1`, so an actor standing on the lower floor is already inside the bottom rung. Backwards, the ladder dead-ends into a solid slab.154- **cstruct types CANNOT be fields in MFL `type` structs.** A `cstruct` declared in an `extern` block (`Model`, `Color`, `Sound`, `Shader`, …) can be a local variable, passed to functions, or stored in a **slice** (`[]Model`, `[]Color`), but it **cannot** be a field of an MFL `type` struct. The C type map isn't available at the point the MFL struct's typedef generates. Workaround: use **parallel slices** — `bodies := []Body{}; models := []Model{}` — and index them together. (Discovered by machin-game-demo-solar.)155- ~~**Non-empty `[]struct` literals are not supported.**~~ **FIXED in 0.124.0** — `xs := []S{a, b, c}` compiles. On a pre-0.124 compiler, build with `append`.156157## Each game drove a feature (the dogfood record)158159| game | exercises | drove into machin |160|------|-----------|-------------------|161| snake | terminal real-time input | `raw_mode` / `read_key` (v0.41.0) |162| 2048 | raylib FFI: scalars + `Color` | (composed; no new builtin) |163| flappy | textures/sprites, `f32` structs, struct-return | `float()` int→float (v0.43.0) |164| simon | audio: pointer-bearing `Sound` by value | FFI **opaque handles** `cstruct Name {}` (v0.44.0) |165| 3d demo | 3D: `Camera3D` (struct of `Vector3`s); per-object rotation (rlgl matrix stack, headerless extern) | FFI **nested cstructs** (v0.45.0); its libm orbit then drove **native math** builtins (v0.46.0); rotation = composition (no new feature) |166| anim | 2D procedural flow field (sin/cos/atan2/hypot over time) | composition on native math + 2D FFI (no new feature) |167| terrain | procedural mesh: per-vertex heights, flat-shaded, streamed via rlgl immediate mode | composition (no new feature); points at the **pointer/array FFI** gap for real GPU VBOs |168| planet | static **GPU mesh**: vertex/color arrays in raw memory, `Mesh` as a cstruct, upload to VRAM | **pointer/array FFI** (v0.47.0): raw memory `alloc`/`poke_*`; then **pointer cstruct fields + inout `T*`** (v0.48.0) dropped the hard-coded offsets |169| cyberpunk | **infinite** procedural world: fbm-noise terrain in GPU-mesh chunks, fly camera, grimy city districts, **GPU-instanced flora**, **shader depth fog**, **skeletal fauna**, **10 km draw distance** | **`noise2`/`noise3`** Perlin (v0.49.0); buildings/instancing/shaders/fog/skeleton-animation/far-clip all composition (rlgl matrix stack + by-value `Matrix` cstruct) |170| solar | **3D solar-system sim**: 7 noise3-textured GPU-mesh planets, **fixed-timestep** orbital sim (60 Hz accumulator), 6DOF fly camera, **pure-MFL math3d module** (`Vec3` add/sub/scale/dot/cross/len/norm/lerp/dist) | composition (no new builtin); drives the **vector/math layer** (feature #2) and **fixed-timestep sim** (feature #6) from the north star; surfaced the cstruct-in-struct-field + slice-literal caveats |171| physics | **verlet physics sandbox**: ~100 particles falling/colliding/stacking, chain pendulum, velocity coloring, noise3-based random scene population | composition (no new builtin); first constraint-based physics in pure MFL — verlet integration, distance constraint relaxation (iterations × substeps), O(n²) sphere-sphere collision, all over the `Vec3` module; drives Tier 4 physics patterns |172| galaxy | **procedural spiral galaxy**: ~5000 stars in 4 logarithmic spiral arms (`θ = k·log(r)`), spectral OBAFGKM colors, rlgl point-cloud render (RL_POINTS, one batch), slow rotation, core bulge | composition (no new builtin); first demo to use **rlgl point rendering** (`rlBegin(0)` / `rlColor4ub` / `rlVertex3f` / `rlEnd()`); demonstrates procedural star placement, weighted spectral distribution, multi-pass point sizing, and the `v3_rot_y()` helper |173| ballistics | **interactive cannon sim**: trajectory prediction with gravity + quadratic drag + wind, aim/power controls, live projectile with trail, hit/miss detection on a target | composition (no new builtin); demonstrates ballistic Euler integration, the prediction-vs-execution pattern (same integrator for both), phase-state machine (aim→fire→impact), interactive HUD with real-time angle/power feedback, and `IsKeyPressed` for single-fire controls |174| ressort / Demolition Man | **a 2D ENGINE, not a game**: Torque2D-style declarative scenes/prefabs/behaviours as data over a Spring-style deterministic 60 Hz fixed-step sim with a hard synced/unsynced split; record → replay → **headless `verify`** (a playthrough is a 2 kB text artifact that proves it lands on the same per-frame checksum); ASCII tilemaps + swept AABB collision; text-authored pixel sprites (zero binary assets) | composition (no new builtin) — but it surfaced two real language hazards: **signed int64 overflow is UB the optimizer folds** (a 64-bit FNV checksum collapsed to `INT64_MAX` and silently stopped discriminating) and **`u8` cstruct fields wrap silently** (fire rendered green). Both are in the caveats above; both point at machin's own honesty story |175| player | **first-person 3D playground**: walking character with smooth acceleration, platform stepping (4 heights), 5 pushable spheres with gravity physics, walk/fly camera toggle (F) | composition (no new builtin); demonstrates the first-person player controller layer — WASD movement relative to camera yaw, smooth acceleration `vel += (input - vel)·min(accel·dt,1)`, gravity + ground + platform collision, walk/fly mode toggle, pushable-object physics (sphere-sphere collision with player, object gravity + friction), crosshair + HUD in screen space |176177When a new game hits a wall, that's the point: fill the gap in the language, release, and note it here.