feat(stdlib): namespaced standard library (issue #2)

Implement the bulk of the namespaced-stdlib proposal (workshopsoft/ludic#2):
156 namespace methods across Math, Text, List, Ease, Collide, World, Net,
Sys, Save, Mem, extended Screen, Color functions, extended Random, and Time.
All deterministic fixed-point; self-hosting (C-free bootstrap fixpoint holds).

Compiler (selfhost/):
- Math.*: sqrt/sin/cos/tan/atan2/asin/acos (fixed-point runtime prelude —
  bit-by-bit isqrt, 256-entry interpolated sine table, Ross atan2), plus
  hypot/dist/dist2/deg_to_rad/rad_to_deg/posmod/wrap/ping_pong/snapped/
  move_toward/smoothstep/lerp/remap/sign/floor/ceil/round.
- Text.* (complete): upper/lower/trim/repeat/pad, split/join/replace,
  and the libc-backed queries.
- List.* (complete): insert/remove_at/remove/sort plus the earlier ops.
- Ease.* (in/out/in_out/back/bounce) and Collide.* (rects/point_rect/
  circles/rect_circle).
- Phase 3: World/Net/Sys/Save namespaced over the bare builtins (byte-
  identical IR) and Mem.* (bytes/words/copy/fill/peek/poke).
- Screen.* extended (line/circle/fill_circle/triangle/fill_triangle via new
  runtime primitives; sprite/sprite_scaled aliases), Color.* functions,
  Random.* (value/int/sign), Time.* (frame/delta/elapsed/now — new
  game-loop frame counter).
- Fix a lexer bug: fixed-point literals with >4 fractional digits overflowed.

Docs & tooling:
- 129 new per-symbol doc pages; gen.py made data-driven (namespaces
  discovered from the docs, no hardcoded list); new check-impl.py enforces
  that every implemented namespace method / keyword / type / phase has a
  doc page, wired into `x test-tools`. Document the previously-undocumented
  keywords (break/continue/where/entry/new/public + and/or/not tokens).
- LSP: namespaced signature help (ns_method_sig) covering every namespace.

Tests: 12 new self-host/regression tests + a golden render for the drawing
primitives. All suites green (selfhost 21, regression 45, tools 29).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-30 00:26:19 +03:00
parent ff15c4e01d
commit a38195128f
235 changed files with 24676 additions and 7762 deletions

View file

@ -0,0 +1,7 @@
---
id: collide
title: Collide
order: 6
---
2D overlap tests on integer coordinates (pixels or tiles). Rectangles are <code>(x, y, w, h)</code> from the top-left; circles are <code>(x, y, r)</code>. Each returns a bool. Squared distances are computed in 64-bit so large coordinates never overflow.

View file

@ -0,0 +1,22 @@
---
id: collide-circles
name: Collide.circles
category: collide
kind: namespace-method
tokens: Collide.circles
sig: Collide.circles(ax, ay, ar, bx, by, br) -> bool
tip: Do two circles overlap?
order: 2
ns: Collide
member: circles
---
Returns <code>true</code> when circles centred at <code>(ax, ay)</code> and <code>(bx, by)</code> with radii <code>ar</code> and <code>br</code> overlap — that is, when the distance between centres is at most <code>ar + br</code>. It compares squared distances internally, so there is no square root and no precision loss.
```ludic
program Demo {
handler Step phase Update {
if Collide.circles(px, py, 8, ex, ey, 8) { collide() }
}
}
```

View file

@ -0,0 +1,22 @@
---
id: collide-point_rect
name: Collide.point_rect
category: collide
kind: namespace-method
tokens: Collide.point_rect
sig: Collide.point_rect(px, py, rx, ry, rw, rh) -> bool
tip: Is a point inside a rectangle?
order: 1
ns: Collide
member: point_rect
---
Returns <code>true</code> when the point <code>(px, py)</code> lies within the rectangle <code>(rx, ry, rw, rh)</code> — inclusive on the top-left edge, exclusive on the bottom-right. Use it for mouse or touch hit-testing against a button or a world region.
```ludic
program Demo {
handler Step phase Update {
if Collide.point_rect(mx, my, bx, by, bw, bh) { press() }
}
}
```

View file

@ -0,0 +1,22 @@
---
id: collide-rect_circle
name: Collide.rect_circle
category: collide
kind: namespace-method
tokens: Collide.rect_circle
sig: Collide.rect_circle(rx, ry, rw, rh, cx, cy, cr) -> bool
tip: Does a rectangle overlap a circle?
order: 3
ns: Collide
member: rect_circle
---
Returns <code>true</code> when the rectangle <code>(rx, ry, rw, rh)</code> overlaps the circle centred at <code>(cx, cy)</code> with radius <code>cr</code>. It finds the point on the rectangle nearest the circle's centre and checks whether it falls within the radius — the correct test for a round actor against a blocky tile.
```ludic
program Demo {
handler Step phase Update {
if Collide.rect_circle(tx, ty, 16, 16, bx, by, 6) { block() }
}
}
```

View file

@ -0,0 +1,22 @@
---
id: collide-rects
name: Collide.rects
category: collide
kind: namespace-method
tokens: Collide.rects
sig: Collide.rects(ax, ay, aw, ah, bx, by, bw, bh) -> bool
tip: Do two rectangles overlap?
order: 0
ns: Collide
member: rects
---
Returns <code>true</code> when the axis-aligned rectangles <code>(ax, ay, aw, ah)</code> and <code>(bx, by, bw, bh)</code> overlap. Edges that merely touch do not count as overlapping. This is the standard broad-phase test for two hitboxes, tiles, or UI regions.
```ludic
program Demo {
handler Step phase Update {
if Collide.rects(px, py, 16, 16, ex, ey, 16, 16) { hit() }
}
}
```

View file

@ -0,0 +1,7 @@
---
id: color
title: Color functions
order: 6
---
Building and blending colors at runtime. Colors are <code>0x00RRGGBB</code> ints; these pack channels and transform an existing color. The named palette constants (<code>Color.Charcoal</code>, …) are a separate compile-time set.

View file

@ -0,0 +1,24 @@
---
id: color-darken
name: Color.darken
category: color
kind: namespace-method
tokens: Color.darken
sig: Color.darken(c, amount) -> int
tip: Scale a color toward black.
order: 3
ns: Color
member: darken
---
Multiplies each channel of <code>c</code> by <code>1 - amount</code> (amount a fixed 0..1), returning a darker color. <code>Color.darken(c, 0.0)</code> is unchanged and <code>1.0</code> is black. Handy for shadows and pressed-button states.
```ludic
program Demo {
handler DrawWorld phase Render {
let c = Color.darken(Color.Red, 0.3)
Screen.clear(c)
Screen.show()
}
}
```

View file

@ -0,0 +1,24 @@
---
id: color-lerp
name: Color.lerp
category: color
kind: namespace-method
tokens: Color.lerp
sig: Color.lerp(c0, c1, t) -> int
tip: Blend between two colors.
order: 2
ns: Color
member: lerp
---
Interpolates each channel of <code>c0</code> and <code>c1</code> by the fixed amount <code>t</code> in <code>0.0</code>..<code>1.0</code>, returning the blended color. Use it for gradients, fades between two tints, or a flash that returns to base.
```ludic
program Demo {
handler DrawWorld phase Render {
let c = Color.lerp(Color.Black, Color.White, 0.5)
Screen.clear(c)
Screen.show()
}
}
```

View file

@ -0,0 +1,24 @@
---
id: color-lighten
name: Color.lighten
category: color
kind: namespace-method
tokens: Color.lighten
sig: Color.lighten(c, amount) -> int
tip: Scale a color toward white.
order: 4
ns: Color
member: lighten
---
Moves each channel of <code>c</code> a fraction <code>amount</code> of the way to 255, returning a lighter color — the counterpart to <code>Color.darken</code>. Good for highlights and hover states.
```ludic
program Demo {
handler DrawWorld phase Render {
let c = Color.lighten(Color.Blue, 0.3)
Screen.clear(c)
Screen.show()
}
}
```

View file

@ -0,0 +1,24 @@
---
id: color-rgb
name: Color.rgb
category: color
kind: namespace-method
tokens: Color.rgb
sig: Color.rgb(r, g, b) -> int
tip: Build a color from red, green, blue.
order: 0
ns: Color
member: rgb
---
Packs three 0..255 channel values into a single <code>0xRRGGBB</code> color int. Use it to build a color from computed channels rather than a fixed literal or a palette name.
```ludic
program Demo {
handler DrawWorld phase Render {
let c = Color.rgb(255, 128, 64)
Screen.clear(c)
Screen.show()
}
}
```

View file

@ -0,0 +1,24 @@
---
id: color-rgba
name: Color.rgba
category: color
kind: namespace-method
tokens: Color.rgba
sig: Color.rgba(r, g, b, a) -> int
tip: Build a color with an alpha byte.
order: 1
ns: Color
member: rgba
---
Packs <code>r</code>, <code>g</code>, <code>b</code>, and an alpha <code>a</code> (0..255) into a <code>0xAARRGGBB</code> int. The software renderer draws opaque, but the alpha byte is preserved for your own blending or storage.
```ludic
program Demo {
handler DrawWorld phase Render {
let c = Color.rgba(255, 128, 64, 200)
Screen.clear(c)
Screen.show()
}
}
```

View file

@ -0,0 +1,24 @@
---
id: color-with_alpha
name: Color.with_alpha
category: color
kind: namespace-method
tokens: Color.with_alpha
sig: Color.with_alpha(c, a) -> int
tip: Replace a color's alpha byte.
order: 5
ns: Color
member: with_alpha
---
Returns <code>c</code> with its alpha byte set to <code>a</code> (0..255), leaving the RGB channels untouched.
```ludic
program Demo {
handler DrawWorld phase Render {
let c = Color.with_alpha(Color.Green, 128)
Screen.clear(c)
Screen.show()
}
}
```

View file

@ -0,0 +1,27 @@
---
id: kw-break
name: break
category: control
kind: keyword
tokens: break
sig: break
tip: Leave the enclosing loop immediately.
order: 9
---
<code>break</code> exits the innermost <code>while</code> or <code>for</code> loop at once, skipping the rest of the body and any remaining iterations, and continues with the code after the loop. Use it to stop early the moment you have what you need — the first match in a scan, a collision that ends the sweep, or a guard inside <code>while true</code> that decides when to leave. It affects only the loop that contains it; an outer loop keeps running.
```ludic
program FindFirst {
var found: int = 0 - 1
handler Scan phase Update {
for i in 0 .. 10 {
if i * i > 20 {
found = i
break # stop at the first i whose square exceeds 20
}
}
}
}
```

View file

@ -0,0 +1,25 @@
---
id: kw-continue
name: continue
category: control
kind: keyword
tokens: continue
sig: continue
tip: Skip to the next iteration of the loop.
order: 10
---
<code>continue</code> abandons the rest of the current loop pass and jumps straight to the next one — the next condition check in a <code>while</code>, or the next index in a <code>for</code>. Use it to skip elements that do not apply without nesting the rest of the body inside an <code>if</code>: filter out empty slots, ignore inactive entities, or step over even numbers. Like <code>break</code>, it acts on the innermost loop only.
```ludic
program SumOdds {
var total: int = 0
handler Add phase Update {
for i in 0 .. 10 {
if i % 2 == 0 { continue } # skip even numbers
total = total + i
}
}
}
```

View file

@ -0,0 +1,25 @@
---
id: kw-where
name: where
category: control
kind: keyword
tokens: where
sig: for (Binds) in query [Comps] where cond { … }
tip: Filter a query loop to entities that satisfy a condition.
order: 11
---
<code>where</code> attaches a boolean condition to a <code>query</code> loop, so the body runs only for entities whose components satisfy it — the rows that fail the test are skipped entirely. The condition is an ordinary expression over the bound components, such as <code>where Battle.hp &lt;= 0 and Battle.side == 1</code>. It is the imperative twin of a component filter written inside a <code>@Queries</code> annotation, and keeps the guard next to the loop instead of repeating an <code>if</code> at the top of the body.
```ludic
program Cleanup {
property Health { hp: int = 0 }
model Foe { Health }
handler ReapDead phase LateUpdate {
for (Health) in query [Health, {Foe}] where Health.hp <= 0 {
# only dead foes reach here
}
}
}
```

View file

@ -0,0 +1,7 @@
---
id: ease
title: Ease
order: 6
---
Tween curves — the "juice" layer. Each takes a normalized amount <code>t</code> in <code>0.0</code>..<code>1.0</code> and returns an eased <code>fixed</code>, ready to feed to <code>Math.lerp</code>. All deterministic fixed-point.

View file

@ -0,0 +1,22 @@
---
id: ease-back
name: Ease.back
category: ease
kind: namespace-method
tokens: Ease.back
sig: Ease.back(t) -> fixed
tip: Ease in with a small backward anticipation.
order: 3
ns: Ease
member: back
---
Returns an ease-in that first dips slightly below <code>0</code> before shooting forward — the anticipation windup that gives a motion snap. Because it undershoots, the eased value can go negative near the start, so use it where a little overshoot reads as lively rather than wrong.
```ludic
program Demo {
handler Step phase Update {
let s = Math.lerp(rest, poke, Ease.back(progress))
}
}
```

View file

@ -0,0 +1,22 @@
---
id: ease-bounce
name: Ease.bounce
category: ease
kind: namespace-method
tokens: Ease.bounce
sig: Ease.bounce(t) -> fixed
tip: Ease out with a settling bounce.
order: 4
ns: Ease
member: bounce
---
Returns an ease-out that overshoots and bounces a few times before settling at <code>1.0</code>, like a ball dropping to the floor. Great for coins landing, badges popping, or a menu that lands with character.
```ludic
program Demo {
handler Step phase Update {
let y = Math.lerp(top, floor_y, Ease.bounce(progress))
}
}
```

View file

@ -0,0 +1,22 @@
---
id: ease-in
name: Ease.in
category: ease
kind: namespace-method
tokens: Ease.in
sig: Ease.in(t) -> fixed
tip: Accelerate from rest (quadratic ease-in).
order: 0
ns: Ease
member: in
---
Returns <code>t * t</code> — motion that starts slow and speeds up. Feed the result to <code>Math.lerp</code> as the blend amount so a value accelerates into its target. The input <code>t</code> is expected in <code>0.0</code>..<code>1.0</code>.
```ludic
program Demo {
handler Step phase Update {
let x = Math.lerp(start_x, end_x, Ease.in(progress))
}
}
```

View file

@ -0,0 +1,22 @@
---
id: ease-in_out
name: Ease.in_out
category: ease
kind: namespace-method
tokens: Ease.in_out
sig: Ease.in_out(t) -> fixed
tip: Ease in and then out (smoothstep).
order: 2
ns: Ease
member: in_out
---
Returns the smooth <code>3t^2 - 2t^3</code> S-curve: slow at both ends, fastest in the middle. Use it for a polished there-and-back or point-to-point move that neither jerks off the mark nor slams into the target.
```ludic
program Demo {
handler Step phase Update {
let a = Math.lerp(0.0, 1.0, Ease.in_out(progress))
}
}
```

View file

@ -0,0 +1,22 @@
---
id: ease-out
name: Ease.out
category: ease
kind: namespace-method
tokens: Ease.out
sig: Ease.out(t) -> fixed
tip: Decelerate to rest (quadratic ease-out).
order: 1
ns: Ease
member: out
---
Returns <code>t * (2 - t)</code> — motion that starts fast and slows as it arrives. The most natural-feeling easing for things settling into place, like a panel sliding in or a camera coming to rest.
```ludic
program Demo {
handler Step phase Update {
let y = Math.lerp(top, rest_y, Ease.out(progress))
}
}
```

View file

@ -0,0 +1,7 @@
---
id: list
title: List
order: 6
---
Operations over <code>[]T</code> slices — length, ends access, push/pop, swap, search, and in-place reverse. A slice is a shared growable buffer, so these mutate it in place and every holder sees the change. Arguments are positional.

View file

@ -0,0 +1,27 @@
---
id: list-clear
name: List.clear
category: list
kind: namespace-method
tokens: List.clear
sig: List.clear(s) -> void
tip: Remove all elements, keeping capacity.
order: 2
ns: List
member: clear
---
Empties <code>s</code> by setting its length back to zero. The backing buffer is kept, so refilling the slice afterward reuses the same memory without reallocating. Use it to reset a per-frame list — collisions, visible tiles, particles to draw — at the start of each update instead of building a new slice every frame.
Parameters:
- `s` — the slice to empty
```ludic
program FrameHits {
var hits: []int = new []int
handler BeginFrame phase Update {
List.clear(hits) # start the frame with an empty list
}
}
```

View file

@ -0,0 +1,31 @@
---
id: list-contains
name: List.contains
category: list
kind: namespace-method
tokens: List.contains
sig: List.contains(s, v) -> bool
tip: Whether a value appears in a slice.
order: 7
ns: List
member: contains
---
Scans <code>s</code> and returns <code>true</code> if any element equals <code>v</code>. Comparison is by value for scalar elements (ints, entities, booleans) and by identity for reference elements. Use it for membership tests — has this tile been visited, is this entity already in the target list, is the achievement unlocked. It is a linear scan, so for very large or hot-path sets a dedicated structure will be faster.
Parameters:
- `s` — the slice to search
- `v` — the value to look for
```ludic
program Visited {
var seen: []int = new []int
handler Step phase Update {
let tile = 42
if not List.contains(seen, tile) {
List.push(seen, tile)
}
}
}
```

View file

@ -0,0 +1,29 @@
---
id: list-first
name: List.first
category: list
kind: namespace-method
tokens: List.first
sig: List.first(s) -> T
tip: The first element of a slice.
order: 3
ns: List
member: first
---
Returns element <code>0</code> of <code>s</code> — the same as <code>s[0]</code>, named for readability. The slice must hold at least one element; calling it on an empty slice reads out of bounds, so guard with <code>List.len(s) > 0</code> when that is possible. Use it to peek at the head of a queue or the front of an ordered list.
Parameters:
- `s` — a non-empty slice
```ludic
program Queue {
var waiting: []int = new []int
handler Serve phase Update {
if List.len(waiting) > 0 {
let next = List.first(waiting)
}
}
}
```

View file

@ -0,0 +1,31 @@
---
id: list-index_of
name: List.index_of
category: list
kind: namespace-method
tokens: List.index_of
sig: List.index_of(s, v) -> int
tip: The index of a value in a slice, or -1 if absent.
order: 8
ns: List
member: index_of
---
Scans <code>s</code> and returns the index of the first element equal to <code>v</code>, or <code>-1</code> if none match. It uses the same value/identity comparison as <code>List.contains</code>, but tells you where the match is so you can act on it — swap it out, read a parallel slice at the same index, or guard with <code>if idx >= 0</code>. Only the first match is reported.
Parameters:
- `s` — the slice to search
- `v` — the value to locate
```ludic
program FindSlot {
var ids: []int = new []int
handler Locate phase Update {
let slot = List.index_of(ids, 99)
if slot >= 0 {
List.swap(ids, slot, List.len(ids) - 1)
}
}
}
```

View file

@ -0,0 +1,22 @@
---
id: list-insert
name: List.insert
category: list
kind: namespace-method
tokens: List.insert
sig: List.insert(s, i, v) -> void
tip: Insert an element at an index, shifting the rest up.
order: 10
ns: List
member: insert
---
Inserts <code>v</code> at index <code>i</code>, moving the elements at and after <code>i</code> one place toward the end and growing the backing buffer if needed. Use it to keep a list ordered as you build it, or to splice an item into the middle. Appending is cheaper with <code>List.push</code>.
```ludic
program Demo {
handler Step phase Update {
List.insert(queue, 0, next_id) # push to the front
}
}
```

View file

@ -0,0 +1,29 @@
---
id: list-last
name: List.last
category: list
kind: namespace-method
tokens: List.last
sig: List.last(s) -> T
tip: The last element of a slice.
order: 4
ns: List
member: last
---
Returns the final element of <code>s</code> — the value at index <code>List.len(s) - 1</code>. Like <code>List.first</code>, it assumes the slice is non-empty, so guard with a length check when the slice might be empty. Use it to read the most recently pushed item without removing it; use <code>List.pop</code> when you want to take it off.
Parameters:
- `s` — a non-empty slice
```ludic
program Trail {
var points: []int = new []int
handler Head phase Update {
if List.len(points) > 0 {
let newest = List.last(points)
}
}
}
```

View file

@ -0,0 +1,27 @@
---
id: list-len
name: List.len
category: list
kind: namespace-method
tokens: List.len
sig: List.len(s) -> int
tip: The number of elements in a slice.
order: 0
ns: List
member: len
---
Returns how many elements <code>s</code> currently holds. It reads the slice's length, which changes as you <code>List.push</code> and <code>List.pop</code>, and is the same value as the bare <code>len(s)</code>. Use it to bound a loop, test for emptiness, or find the last index with <code>List.len(s) - 1</code>.
Parameters:
- `s` — the slice to measure
```ludic
program CountEnemies {
var enemies: []int = new []int
handler Report phase Update {
let n = List.len(enemies)
}
}
```

View file

@ -0,0 +1,29 @@
---
id: list-pop
name: List.pop
category: list
kind: namespace-method
tokens: List.pop
sig: List.pop(s) -> T
tip: Remove and return the last element.
order: 5
ns: List
member: pop
---
Removes the final element of <code>s</code> and returns it, shortening the slice by one. Together with <code>List.push</code> this makes a slice work as a stack: push to add, pop to take the most recent back off. The slice must be non-empty. Use it to unwind a history, process a work list, or undo the last action.
Parameters:
- `s` — a non-empty slice
```ludic
program UndoStack {
var history: []int = new []int
handler Undo phase Update {
if List.len(history) > 0 {
let last_action = List.pop(history)
}
}
}
```

View file

@ -0,0 +1,28 @@
---
id: list-push
name: List.push
category: list
kind: namespace-method
tokens: List.push
sig: List.push(s, v) -> void
tip: Append an element to the end of a slice.
order: 1
ns: List
member: push
---
Adds <code>v</code> to the end of <code>s</code>, growing the backing buffer if needed, so the slice's length increases by one. It is the same operation as the bare <code>push(s, v)</code>. Because a slice is shared, the new element is visible to every holder. Use it to collect spawned entities, build a list of hits, or fill an inventory.
Parameters:
- `s` — the slice to append to
- `v` — the element to add (matching the slice's element type)
```ludic
program Collect {
var picked: []int = new []int
handler Grab phase Update {
List.push(picked, 7)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: list-remove
name: List.remove
category: list
kind: namespace-method
tokens: List.remove
sig: List.remove(s, v) -> void
tip: Remove the first element equal to a value.
order: 12
ns: List
member: remove
---
Scans for the first element equal to <code>v</code> and removes it (shifting the rest down); if no element matches, the slice is unchanged. Comparison is by value for scalars and by identity for reference elements, matching <code>List.contains</code>. Only the first match is removed.
```ludic
program Demo {
handler Step phase Update {
List.remove(active, dead_entity)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: list-remove_at
name: List.remove_at
category: list
kind: namespace-method
tokens: List.remove_at
sig: List.remove_at(s, i) -> void
tip: Remove the element at an index, shifting the rest down.
order: 11
ns: List
member: remove_at
---
Removes the element at index <code>i</code>, moving everything after it one place toward the front and shortening the slice by one. Order is preserved. When order does not matter, swapping the item to the end and calling <code>List.pop</code> is cheaper.
```ludic
program Demo {
handler Step phase Update {
List.remove_at(effects, expired)
}
}
```

View file

@ -0,0 +1,28 @@
---
id: list-reverse
name: List.reverse
category: list
kind: namespace-method
tokens: List.reverse
sig: List.reverse(s) -> void
tip: Reverse the order of a slice in place.
order: 9
ns: List
member: reverse
---
Reverses <code>s</code> so its first element becomes last and vice versa, mutating the slice in place rather than returning a copy. Use it to flip an order you built back-to-front — a path traced from goal to start, an undo stack you want oldest-first, or a list you appended to and now want to read in the opposite direction.
Parameters:
- `s` — the slice to reverse
```ludic
program PathOrder {
var path: []int = new []int
handler Finalize phase Update {
# path was built from the goal back to the start
List.reverse(path) # now start -> goal
}
}
```

View file

@ -0,0 +1,22 @@
---
id: list-sort
name: List.sort
category: list
kind: namespace-method
tokens: List.sort
sig: List.sort(s) -> void
tip: Sort a slice in ascending order, in place.
order: 13
ns: List
member: sort
---
Sorts the elements of <code>s</code> in ascending order in place, using an insertion sort. It is intended for slices of scalar elements (ints, entities, fixed) where <code>&lt;</code> is meaningful; reference elements are ordered by identity, which is rarely useful. Insertion sort is simple and fast for the small, nearly-sorted lists games usually hold.
```ludic
program Demo {
handler Step phase Update {
List.sort(scores)
}
}
```

View file

@ -0,0 +1,32 @@
---
id: list-swap
name: List.swap
category: list
kind: namespace-method
tokens: List.swap
sig: List.swap(s, i, j) -> void
tip: Exchange the elements at two indices.
order: 6
ns: List
member: swap
---
Exchanges the elements at indices <code>i</code> and <code>j</code> in <code>s</code>, in place. Both indices must be within the slice. Swapping is the building block of shuffles, sorts, and moving an item to the end before <code>List.pop</code> to remove it cheaply without shifting the rest.
Parameters:
- `s` — the slice to modify
- `i` — the first index
- `j` — the second index
```ludic
program RemoveFast {
var actors: []int = new []int
handler Kill phase Update {
let dead = 2
let last = List.len(actors) - 1
List.swap(actors, dead, last) # move it to the end...
let removed = List.pop(actors) # ...then drop it
}
}
```

View file

@ -0,0 +1,7 @@
---
id: math
title: Math
order: 6
---
Deterministic fixed-point math. Every function is computed in Q16.16 with plain integer arithmetic, so results are bit-identical on every platform and every run — the same guarantee the rest of the runtime gives. Arguments are positional.

View file

@ -0,0 +1,28 @@
---
id: math-abs
name: Math.abs
category: math
kind: namespace-method
tokens: Math.abs
sig: Math.abs(x) -> int
tip: The magnitude of a value, dropping its sign.
order: 2
ns: Math
member: abs
---
Returns the absolute value of <code>x</code> — its distance from zero, always non-negative. It preserves the type of its argument, so <code>Math.abs</code> of a <code>fixed</code> stays <code>fixed</code>. Reach for it when you care about how far apart two things are regardless of direction, such as the gap between a target and current position, or the speed behind a signed velocity. The bare form <code>abs(x)</code> is kept as a convenience alias.
Parameters:
- `x` — the value whose magnitude you want
```ludic
program ChaseDistance {
var target: int = 40
var here: int = 12
handler Report phase Update {
let gap = Math.abs(target - here) # 28, regardless of which is larger
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-acos
name: Math.acos
category: math
kind: namespace-method
tokens: Math.acos
sig: Math.acos(x) -> fixed
tip: The arccosine of a value, in radians.
order: 28
ns: Math
member: acos
---
Returns the angle in radians whose cosine is <code>x</code>, in <code>0</code>..<code>pi</code>. The input should be in <code>-1.0</code>..<code>1.0</code>. Handy for recovering the angle between two normalized directions from their dot product.
```ludic
program Demo {
handler Step phase Update {
let between = Math.acos(dot)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-asin
name: Math.asin
category: math
kind: namespace-method
tokens: Math.asin
sig: Math.asin(x) -> fixed
tip: The arcsine of a value, in radians.
order: 27
ns: Math
member: asin
---
Returns the angle in radians whose sine is <code>x</code>, in <code>-pi/2</code>..<code>pi/2</code>. The input should be in <code>-1.0</code>..<code>1.0</code>. Built from <code>Math.atan2</code> and <code>Math.sqrt</code>, so it is deterministic.
```ludic
program Demo {
handler Step phase Update {
let angle = Math.asin(height / radius)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-atan2
name: Math.atan2
category: math
kind: namespace-method
tokens: Math.atan2
sig: Math.atan2(y, x) -> fixed
tip: The angle of the vector (x, y), in radians.
order: 26
ns: Math
member: atan2
---
Returns the angle in radians from the positive x-axis to the point <code>(x, y)</code>, in the full range <code>-pi</code>..<code>pi</code> — the standard way to get a heading from a direction vector. Note the argument order is <code>y</code> then <code>x</code>. Deterministic fixed-point, accurate to within about a degree.
```ludic
program Demo {
handler Step phase Update {
let heading = Math.atan2(target_y - y, target_x - x)
}
}
```

View file

@ -0,0 +1,29 @@
---
id: math-ceil
name: Math.ceil
category: math
kind: namespace-method
tokens: Math.ceil
sig: Math.ceil(x) -> int
tip: Round a fixed value up to the nearest whole int.
order: 6
ns: Math
member: ceil
---
Takes a <code>fixed</code> value and returns the smallest <code>int</code> not less than it — rounding toward positive infinity, so <code>Math.ceil(2.1)</code> is <code>3</code> and <code>Math.ceil(4.0)</code> stays <code>4</code>. Use it when you need to cover a fractional amount with whole units: how many full tiles a span touches, or how many rows a variable-height list needs.
Parameters:
- `x` — the `fixed` value to round up
```ludic
program RowsNeeded {
const ROW_HEIGHT: fixed = 12.0
var content_height: fixed = 30.0
handler Layout phase Update {
let rows = Math.ceil(content_height / ROW_HEIGHT) # 3
}
}
```

View file

@ -0,0 +1,31 @@
---
id: math-clamp
name: Math.clamp
category: math
kind: namespace-method
tokens: Math.clamp
sig: Math.clamp(v, lo, hi) -> int
tip: Constrain a value to the range [lo, hi].
order: 3
ns: Math
member: clamp
---
Returns <code>v</code> held inside the inclusive range <code>lo</code>..<code>hi</code>: below <code>lo</code> it returns <code>lo</code>, above <code>hi</code> it returns <code>hi</code>, otherwise <code>v</code> unchanged. It is the single-call form of <code>Math.max(lo, Math.min(v, hi))</code> and works for <code>int</code> and <code>fixed</code> alike. Use it to keep a camera inside the world, a slider within its track, or a stat within its legal bounds. The bare form <code>clamp(v, lo, hi)</code> is kept as a convenience alias.
Parameters:
- `v` — the value to constrain
- `lo` — the lowest allowed result (inclusive)
- `hi` — the highest allowed result (inclusive)
```ludic
program CameraBounds {
const WORLD_WIDTH: int = 320
var camera_x: int = 0
handler Follow phase Update {
camera_x = Math.clamp(camera_x, 0, WORLD_WIDTH - 1)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-cos
name: Math.cos
category: math
kind: namespace-method
tokens: Math.cos
sig: Math.cos(radians) -> fixed
tip: Cosine of an angle in radians.
order: 13
ns: Math
member: cos
---
Returns the cosine of <code>radians</code> as a <code>fixed</code> in <code>-1.0</code>..<code>1.0</code>, computed as <code>Math.sin(radians + pi/2)</code> off the same deterministic table. Use <code>Math.cos</code> and <code>Math.sin</code> together to convert an angle into an <code>(x, y)</code> direction — for orbits, aiming, or steering.
```ludic
program Demo {
handler Step phase Update {
let x = center_x + Math.cos(angle) * radius
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-deg_to_rad
name: Math.deg_to_rad
category: math
kind: namespace-method
tokens: Math.deg_to_rad
sig: Math.deg_to_rad(degrees) -> fixed
tip: Convert degrees to radians.
order: 18
ns: Math
member: deg_to_rad
---
Converts an angle from degrees to radians (multiplying by <code>pi/180</code>), since <code>Math.sin</code>/<code>Math.cos</code>/<code>Math.tan</code> all take radians. Use it when your data or design speaks in degrees — a 90-degree turn, a 45-degree cone — and you need to feed it to the trig functions.
```ludic
program Demo {
handler Step phase Update {
let r = Math.sin(Math.deg_to_rad(90.0)) # ~1.0
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-dist
name: Math.dist
category: math
kind: namespace-method
tokens: Math.dist
sig: Math.dist(x0, y0, x1, y1) -> fixed
tip: Distance between two points.
order: 16
ns: Math
member: dist
---
Returns the straight-line distance between <code>(x0, y0)</code> and <code>(x1, y1)</code> — <code>sqrt(dx*dx + dy*dy)</code> in fixed-point. Use it for proximity and range checks. When you only need to compare distances (which is nearer? within radius?), prefer <code>Math.dist2</code>, which skips the square root.
```ludic
program Demo {
handler Step phase Update {
if Math.dist(px, py, ex, ey) < attack_range { strike() }
}
}
```

View file

@ -0,0 +1,23 @@
---
id: math-dist2
name: Math.dist2
category: math
kind: namespace-method
tokens: Math.dist2
sig: Math.dist2(x0, y0, x1, y1) -> fixed
tip: Squared distance between two points.
order: 17
ns: Math
member: dist2
---
Returns the squared distance between <code>(x0, y0)</code> and <code>(x1, y1)</code> — <code>dx*dx + dy*dy</code>, with no square root. Comparing squared distances gives the same ordering as comparing real distances, so this is the cheaper choice for "nearest" and "within radius" tests: compare against <code>radius * radius</code> instead of calling <code>Math.dist</code>.
```ludic
program Demo {
handler Step phase Update {
let r2 = radius * radius
if Math.dist2(px, py, ex, ey) < r2 { in_range() }
}
}
```

View file

@ -0,0 +1,29 @@
---
id: math-floor
name: Math.floor
category: math
kind: namespace-method
tokens: Math.floor
sig: Math.floor(x) -> int
tip: Round a fixed value down to the nearest whole int.
order: 5
ns: Math
member: floor
---
Takes a <code>fixed</code> value and returns the largest <code>int</code> not greater than it — rounding toward negative infinity, so <code>Math.floor(2.7)</code> is <code>2</code> and <code>Math.floor(-0.2)</code> is <code>-1</code>. This is the fixed-to-int conversion you reach for when turning a smooth position into a whole tile or pixel index. It is the same operation as the bare <code>flr(x)</code>.
Parameters:
- `x` — the `fixed` value to round down
```ludic
program TileIndex {
const TILE_SIZE: fixed = 16.0
var world_x: fixed = 40.5
handler Locate phase Update {
let column = Math.floor(world_x / TILE_SIZE) # 2
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-hypot
name: Math.hypot
category: math
kind: namespace-method
tokens: Math.hypot
sig: Math.hypot(x, y) -> fixed
tip: Length of the vector (x, y).
order: 15
ns: Math
member: hypot
---
Returns <code>sqrt(x*x + y*y)</code> — the length of the 2D vector <code>(x, y)</code>, or equivalently the hypotenuse of a right triangle with those legs. It is the one-call form for a magnitude and reads more clearly than spelling out the squares and root. To normalize a vector, divide each component by its <code>Math.hypot</code>.
```ludic
program Demo {
handler Step phase Update {
let length = Math.hypot(velocity_x, velocity_y)
}
}
```

View file

@ -0,0 +1,31 @@
---
id: math-inverse_lerp
name: Math.inverse_lerp
category: math
kind: namespace-method
tokens: Math.inverse_lerp
sig: Math.inverse_lerp(a, b, v) -> fixed
tip: Find where a value sits between two endpoints as a 0..1 fraction.
order: 9
ns: Math
member: inverse_lerp
---
The inverse of <code>Math.lerp</code>: given endpoints <code>a</code> and <code>b</code> and a value <code>v</code>, it returns the fraction <code>(v - a) / (b - a)</code> — <code>0.0</code> when <code>v</code> equals <code>a</code>, <code>1.0</code> when it equals <code>b</code>. Use it to turn a raw quantity into a normalized amount: how full a health bar is, how far a timer has run, or the <code>t</code> to feed into another <code>Math.lerp</code>. The endpoints must differ, since equal ones would divide by zero.
Parameters:
- `a` — the endpoint that maps to `0.0`
- `b` — the endpoint that maps to `1.0`
- `v` — the value to locate between them
```ludic
program HealthFraction {
const MAX_HP: fixed = 200.0
var hp: fixed = 150.0
handler Draw phase Render {
let fraction = Math.inverse_lerp(0.0, MAX_HP, hp) # 0.75
}
}
```

View file

@ -0,0 +1,30 @@
---
id: math-lerp
name: Math.lerp
category: math
kind: namespace-method
tokens: Math.lerp
sig: Math.lerp(a, b, t) -> fixed
tip: Blend between two values by a 0..1 amount.
order: 8
ns: Math
member: lerp
---
Linear interpolation: returns <code>a</code> when <code>t</code> is <code>0.0</code>, <code>b</code> when <code>t</code> is <code>1.0</code>, and a proportional blend in between — the exact value <code>a + (b - a) * t</code>. All three arguments are <code>fixed</code> and the result is <code>fixed</code>. It is the workhorse behind smooth motion: ease a camera toward a target, fade a value over time, or sample a gradient. Values of <code>t</code> outside <code>0..1</code> extrapolate past the endpoints.
Parameters:
- `a` — the value returned at `t = 0.0`
- `b` — the value returned at `t = 1.0`
- `t` — the blend amount, normally `0.0`..`1.0`
```ludic
program EaseCamera {
var camera_x: fixed = 0.0
var target_x: fixed = 100.0
handler Follow phase Update {
camera_x = Math.lerp(camera_x, target_x, 0.1) # glides a tenth of the way each frame
}
}
```

View file

@ -0,0 +1,28 @@
---
id: math-max
name: Math.max
category: math
kind: namespace-method
tokens: Math.max
sig: Math.max(a, b) -> int
tip: The larger of two values.
order: 1
ns: Math
member: max
---
Returns whichever of <code>a</code> and <code>b</code> is larger. Like <code>Math.min</code>, it works for both <code>int</code> and <code>fixed</code> operands. Use it to hold a value at a floor — for example keeping a countdown at zero instead of going negative, or a health bar from dropping below the minimum. The bare form <code>max(a, b)</code> is kept as a convenience alias.
Parameters:
- `a` — the first value
- `b` — the second value
```ludic
program Countdown {
var timer: int = 60
handler Tick phase Update {
timer = Math.max(timer - 1, 0) # stops at 0, never negative
}
}
```

View file

@ -0,0 +1,28 @@
---
id: math-min
name: Math.min
category: math
kind: namespace-method
tokens: Math.min
sig: Math.min(a, b) -> int
tip: The smaller of two values.
order: 0
ns: Math
member: min
---
Returns whichever of <code>a</code> and <code>b</code> is smaller. It works on plain <code>int</code> values and on <code>fixed</code> values alike — both are stored as signed 32-bit words, so the comparison keeps their order either way. Use it to cap a value at a ceiling, keep a cursor inside a list, or take the nearer of two distances. The bare form <code>min(a, b)</code> is kept as a convenience alias.
Parameters:
- `a` — the first value
- `b` — the second value
```ludic
program ClampScore {
var score: int = 0
handler AddPoint phase Update {
score = Math.min(score + 1, 100) # never climbs past 100
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-move_toward
name: Math.move_toward
category: math
kind: namespace-method
tokens: Math.move_toward
sig: Math.move_toward(from, to, delta) -> fixed
tip: Step from one value toward another by at most delta.
order: 24
ns: Math
member: move_toward
---
Moves <code>from</code> toward <code>to</code> by at most <code>delta</code>, never overshooting — when the gap is smaller than <code>delta</code> it returns exactly <code>to</code>. Unlike <code>Math.lerp</code>, which eases by a fraction of the remaining distance, this moves at a constant rate, which is what you want for turrets, meters, and UI values that should approach a target steadily and then stop.
```ludic
program Demo {
handler Step phase Update {
health_shown = Math.move_toward(health_shown, health, 2.0)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-ping_pong
name: Math.ping_pong
category: math
kind: namespace-method
tokens: Math.ping_pong
sig: Math.ping_pong(t, len) -> int
tip: Bounce a counter back and forth in [0, len].
order: 22
ns: Math
member: ping_pong
---
Maps an ever-increasing counter <code>t</code> to a value that rises from <code>0</code> to <code>len</code> and back, bouncing forever — the triangle wave to <code>Math.wrap</code>'s sawtooth. Feed it a frame counter to drive a patrol, a pulsing highlight, or a back-and-forth sweep without tracking direction yourself.
```ludic
program Demo {
handler Step phase Update {
let phase = Math.ping_pong(frame, 30)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-posmod
name: Math.posmod
category: math
kind: namespace-method
tokens: Math.posmod
sig: Math.posmod(a, m) -> int
tip: Modulo that is always non-negative.
order: 20
ns: Math
member: posmod
---
Returns <code>a</code> modulo <code>m</code>, but always in the range <code>0</code>..<code>m-1</code> — unlike the <code>%</code> operator, which keeps the sign of <code>a</code> (so <code>-1 % 5</code> is <code>-1</code>, while <code>Math.posmod(-1, 5)</code> is <code>4</code>). It is what you want for wrapping indices and grid coordinates that can go negative.
```ludic
program Demo {
handler Step phase Update {
let col = Math.posmod(x, grid_width) # never negative
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-rad_to_deg
name: Math.rad_to_deg
category: math
kind: namespace-method
tokens: Math.rad_to_deg
sig: Math.rad_to_deg(radians) -> fixed
tip: Convert radians to degrees.
order: 19
ns: Math
member: rad_to_deg
---
Converts an angle from radians to degrees (multiplying by <code>180/pi</code>) — the inverse of <code>Math.deg_to_rad</code>. Use it to present an internally-radians angle in the degrees a player or designer expects, for a HUD readout or a debug overlay.
```ludic
program Demo {
handler Step phase Update {
let heading_deg = Math.rad_to_deg(heading)
}
}
```

View file

@ -0,0 +1,34 @@
---
id: math-remap
name: Math.remap
category: math
kind: namespace-method
tokens: Math.remap
sig: Math.remap(v, in0, in1, out0, out1) -> fixed
tip: Rescale a value from one range into another.
order: 10
ns: Math
member: remap
---
Maps <code>v</code> from the input range <code>in0</code>..<code>in1</code> onto the output range <code>out0</code>..<code>out1</code>, keeping its relative position — equivalent to <code>Math.lerp(out0, out1, Math.inverse_lerp(in0, in1, v))</code>. All arguments are <code>fixed</code>. It is the one call that converts between units: a health value into a bar width, a noise sample in <code>-1..1</code> into a <code>0..255</code> shade, or a mouse position into a world coordinate. Swap the output ends to invert the mapping.
Parameters:
- `v` — the value to rescale
- `in0` — the start of the input range
- `in1` — the end of the input range
- `out0` — the start of the output range
- `out1` — the end of the output range
```ludic
program HealthBar {
const MAX_HP: fixed = 200.0
const BAR_WIDTH: fixed = 64.0
var hp: fixed = 150.0
handler Draw phase Render {
let width = Math.remap(hp, 0.0, MAX_HP, 0.0, BAR_WIDTH) # 48.0
}
}
```

View file

@ -0,0 +1,27 @@
---
id: math-round
name: Math.round
category: math
kind: namespace-method
tokens: Math.round
sig: Math.round(x) -> int
tip: Round a fixed value to the nearest whole int.
order: 7
ns: Math
member: round
---
Takes a <code>fixed</code> value and returns the nearest <code>int</code>, with halves rounding up: <code>Math.round(2.5)</code> is <code>3</code> and <code>Math.round(2.4)</code> is <code>2</code>. Choose it over <code>Math.floor</code> when you want the closest whole value rather than always the lower one — snapping a smoothly-moving sprite to a pixel, or reporting a fractional total as a clean number.
Parameters:
- `x` — the `fixed` value to round to nearest
```ludic
program SnapToPixel {
var smooth_x: fixed = 10.5
handler Draw phase Render {
let px = Math.round(smooth_x) # 11
}
}
```

View file

@ -0,0 +1,28 @@
---
id: math-sign
name: Math.sign
category: math
kind: namespace-method
tokens: Math.sign
sig: Math.sign(x) -> int
tip: The sign of a value as -1, 0, or 1.
order: 4
ns: Math
member: sign
---
Returns <code>-1</code> when <code>x</code> is negative, <code>1</code> when it is positive, and <code>0</code> when it is exactly zero. It works on both <code>int</code> and <code>fixed</code> values, since zero and sign are the same in Q16.16. Multiply a step by <code>Math.sign(dx)</code> to move one unit toward a target, or read the sign of a velocity to decide which way a sprite faces.
Parameters:
- `x` — the value to test
```ludic
program FaceTarget {
var x: int = 8
var target: int = 20
handler StepToward phase Update {
x = x + Math.sign(target - x) # +1 while target is to the right
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-sin
name: Math.sin
category: math
kind: namespace-method
tokens: Math.sin
sig: Math.sin(radians) -> fixed
tip: Sine of an angle in radians.
order: 12
ns: Math
member: sin
---
Returns the sine of <code>radians</code> as a <code>fixed</code> in <code>-1.0</code>..<code>1.0</code>. It is table-driven (a 256-entry Q16.16 sine table with linear interpolation) so it is fast and fully deterministic. Angles wrap, so any value works — no need to reduce into a range first. Pair it with <code>Math.cos</code> to turn an angle into a direction.
```ludic
program Demo {
handler Step phase Update {
let y = center_y + Math.sin(angle) * radius
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-smoothstep
name: Math.smoothstep
category: math
kind: namespace-method
tokens: Math.smoothstep
sig: Math.smoothstep(e0, e1, x) -> fixed
tip: A smooth 0..1 ramp between two edges.
order: 25
ns: Math
member: smoothstep
---
Returns <code>0.0</code> when <code>x</code> is at or below <code>e0</code>, <code>1.0</code> at or above <code>e1</code>, and a smooth S-curve (ease-in and ease-out) in between — the classic <code>t*t*(3 - 2t)</code> Hermite ramp. Use it for fades, dissolves, and easing a normalized amount before feeding it to <code>Math.lerp</code>, when a straight linear ramp looks too abrupt.
```ludic
program Demo {
handler Step phase Update {
let a = Math.smoothstep(0.0, 1.0, fade_t)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-snapped
name: Math.snapped
category: math
kind: namespace-method
tokens: Math.snapped
sig: Math.snapped(v, step) -> fixed
tip: Round a value to the nearest multiple of step.
order: 23
ns: Math
member: snapped
---
Rounds <code>v</code> to the nearest multiple of <code>step</code> (both <code>fixed</code>) — for snapping a position to a grid, quantizing an angle to 8 directions, or rounding a value to a tidy increment. <code>Math.snapped(x, 16.0)</code> snaps <code>x</code> to a 16-unit grid.
```ludic
program Demo {
handler Step phase Update {
let gx = Math.snapped(world_x, 16.0)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-sqrt
name: Math.sqrt
category: math
kind: namespace-method
tokens: Math.sqrt
sig: Math.sqrt(x) -> fixed
tip: The square root of a fixed value.
order: 11
ns: Math
member: sqrt
---
Returns the non-negative square root of <code>x</code>, computed in Q16.16 by a deterministic integer algorithm — so it is bit-identical on every platform, the same guarantee the rest of the runtime gives. Negative inputs return <code>0</code>. Use it for lengths and distances; when you have two legs of a right triangle reach for <code>Math.hypot</code>, and when you only need to compare magnitudes prefer the cheaper squared distance <code>Math.dist2</code>.
```ludic
program Demo {
handler Step phase Update {
let speed = Math.sqrt(vx * vx + vy * vy)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-tan
name: Math.tan
category: math
kind: namespace-method
tokens: Math.tan
sig: Math.tan(radians) -> fixed
tip: Tangent of an angle in radians.
order: 14
ns: Math
member: tan
---
Returns the tangent of <code>radians</code>, computed as <code>Math.sin(radians) / Math.cos(radians)</code> in fixed-point. Near odd multiples of <code>pi/2</code> the cosine approaches zero and the result grows without bound, so guard those angles. For most gameplay, <code>Math.sin</code>/<code>Math.cos</code> are what you want; <code>tan</code> is here for slopes and field-of-view math.
```ludic
program Demo {
handler Step phase Update {
let slope = Math.tan(pitch)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: math-wrap
name: Math.wrap
category: math
kind: namespace-method
tokens: Math.wrap
sig: Math.wrap(v, lo, hi) -> int
tip: Wrap a value into the range [lo, hi).
order: 21
ns: Math
member: wrap
---
Wraps <code>v</code> into the half-open range <code>lo</code>..<code>hi</code>, so values that run off one end reappear at the other — the toroidal counterpart to <code>Math.clamp</code>. Use it for wrap-around movement, cycling through a menu, or keeping a scrolling offset in bounds.
```ludic
program Demo {
handler Step phase Update {
let x = Math.wrap(player_x, 0, world_width)
}
}
```

View file

@ -0,0 +1,7 @@
---
id: mem
title: Mem
order: 6
---
Raw memory — allocate byte and word buffers, copy and fill regions, and peek/poke individual bytes. The low-level escape hatch.

View file

@ -0,0 +1,14 @@
---
id: mem-bytes
name: Mem.bytes
category: mem
kind: namespace-method
tokens: Mem.bytes
sig: Mem.bytes(n) -> ptr
tip: Allocate n bytes.
order: 0
ns: Mem
member: bytes
---
Allocates an <code>n</code>-byte buffer and returns a pointer to it.

View file

@ -0,0 +1,14 @@
---
id: mem-copy
name: Mem.copy
category: mem
kind: namespace-method
tokens: Mem.copy
sig: Mem.copy(dst, src, n) -> void
tip: Copy n bytes between buffers.
order: 2
ns: Mem
member: copy
---
Copies <code>n</code> bytes from <code>src</code> to <code>dst</code> (regions must not overlap).

View file

@ -0,0 +1,14 @@
---
id: mem-fill
name: Mem.fill
category: mem
kind: namespace-method
tokens: Mem.fill
sig: Mem.fill(buf, value, n) -> void
tip: Set n bytes to a value.
order: 3
ns: Mem
member: fill
---
Sets the first <code>n</code> bytes of <code>buf</code> to the byte <code>value</code>.

View file

@ -0,0 +1,14 @@
---
id: mem-peek
name: Mem.peek
category: mem
kind: namespace-method
tokens: Mem.peek
sig: Mem.peek(buf, i) -> int
tip: Read one byte.
order: 4
ns: Mem
member: peek
---
Returns the byte at <code>buf[i]</code> as an int in 0..255.

View file

@ -0,0 +1,14 @@
---
id: mem-poke
name: Mem.poke
category: mem
kind: namespace-method
tokens: Mem.poke
sig: Mem.poke(buf, i, value) -> void
tip: Write one byte.
order: 5
ns: Mem
member: poke
---
Stores the low byte of <code>value</code> at <code>buf[i]</code>.

View file

@ -0,0 +1,14 @@
---
id: mem-words
name: Mem.words
category: mem
kind: namespace-method
tokens: Mem.words
sig: Mem.words(n) -> words
tip: Allocate n 32-bit words.
order: 1
ns: Mem
member: words
---
Allocates a buffer of <code>n</code> 32-bit integer words, indexable as <code>w[i]</code>.

View file

@ -0,0 +1,7 @@
---
id: net
title: Net
order: 6
---
The low-level networking seam — send and poll datagrams, serialize and apply entity state, and read ownership and role. Offline these collapse to single-player defaults.

View file

@ -0,0 +1,14 @@
---
id: net-apply
name: Net.apply
category: net
kind: namespace-method
tokens: Net.apply
sig: Net.apply(entity, buf) -> int
tip: Apply serialized state to an entity.
order: 3
ns: Net
member: apply
---
Reads synced fields from <code>buf</code> into <code>entity</code> — the inverse of <code>Net.serialize</code>.

View file

@ -0,0 +1,14 @@
---
id: net-is_owner
name: Net.is_owner
category: net
kind: namespace-method
tokens: Net.is_owner
sig: Net.is_owner(entity) -> bool
tip: Does this peer own the entity?
order: 7
ns: Net
member: is_owner
---
Returns true when this peer owns <code>entity</code> — <code>owner(entity) == local_id()</code>.

View file

@ -0,0 +1,14 @@
---
id: net-is_server
name: Net.is_server
category: net
kind: namespace-method
tokens: Net.is_server
sig: Net.is_server() -> bool
tip: Is this peer the server?
order: 6
ns: Net
member: is_server
---
Returns true on the authoritative peer. Offline it is true, so server-only guards run in single-player.

View file

@ -0,0 +1,14 @@
---
id: net-local_id
name: Net.local_id
category: net
kind: namespace-method
tokens: Net.local_id
sig: Net.local_id() -> int
tip: This peer's own id.
order: 8
ns: Net
member: local_id
---
Returns the network id of the local peer.

View file

@ -0,0 +1,14 @@
---
id: net-owner
name: Net.owner
category: net
kind: namespace-method
tokens: Net.owner
sig: Net.owner(entity) -> int
tip: The peer that owns an entity.
order: 4
ns: Net
member: owner
---
Returns the peer id that owns <code>entity</code>'s <code>@Owned</code> state.

View file

@ -0,0 +1,14 @@
---
id: net-poll
name: Net.poll
category: net
kind: namespace-method
tokens: Net.poll
sig: Net.poll(buf, cap) -> int
tip: Read an inbound datagram.
order: 1
ns: Net
member: poll
---
Reads up to <code>cap</code> bytes of the next inbound datagram into <code>buf</code> and returns the byte count, or zero when none is waiting.

View file

@ -0,0 +1,14 @@
---
id: net-send
name: Net.send
category: net
kind: namespace-method
tokens: Net.send
sig: Net.send(peer, buf, len) -> void
tip: Put a datagram on the wire.
order: 0
ns: Net
member: send
---
Sends the first <code>len</code> bytes of <code>buf</code> to <code>peer</code>. Absent a real socket it uses the built-in loopback, so offline code still runs.

View file

@ -0,0 +1,14 @@
---
id: net-serialize
name: Net.serialize
category: net
kind: namespace-method
tokens: Net.serialize
sig: Net.serialize(entity, buf) -> int
tip: Serialize an entity's synced state.
order: 2
ns: Net
member: serialize
---
Writes the <code>@Sync</code> fields of <code>entity</code> into <code>buf</code> and returns the byte count, for sending over the wire.

View file

@ -0,0 +1,14 @@
---
id: net-set_owner
name: Net.set_owner
category: net
kind: namespace-method
tokens: Net.set_owner
sig: Net.set_owner(entity, peer) -> void
tip: Assign ownership of an entity.
order: 5
ns: Net
member: set_owner
---
Sets the owning peer of <code>entity</code> to <code>peer</code>.

View file

@ -3,6 +3,7 @@ id: op-logical
name: Logical
category: operators
kind: operator
tokens: and or not
sig: and or not
tip: The boolean combinators, spelled as words — never && or || or a bare !.
order: 2

View file

@ -0,0 +1,25 @@
---
id: random-int
name: Random.int
category: random
kind: namespace-method
tokens: Random.int
sig: Random.int(max) -> int
tip: A random integer in [0, max).
order: 4
ns: Random
member: int
---
Returns a deterministic integer from <code>0</code> up to but not including <code>max</code>. Use it to pick a random index into a list of length <code>max</code>. Returns 0 when <code>max</code> is not positive.
```ludic
program Demo {
handler Seed phase Start { Random.seed(value: 7) }
handler Step phase Update {
let idx = Random.int(list_count)
}
}
}
}
```

View file

@ -0,0 +1,25 @@
---
id: random-sign
name: Random.sign
category: random
kind: namespace-method
tokens: Random.sign
sig: Random.sign() -> int
tip: A random +1 or -1.
order: 5
ns: Random
member: sign
---
Returns <code>1</code> or <code>-1</code> with equal probability — a deterministic coin flip for choosing a direction or jitter sign.
```ludic
program Demo {
handler Seed phase Start { Random.seed(value: 7) }
handler Step phase Update {
let dir = Random.sign()
}
}
}
}
```

View file

@ -0,0 +1,25 @@
---
id: random-value
name: Random.value
category: random
kind: namespace-method
tokens: Random.value
sig: Random.value() -> fixed
tip: A random fixed value in [0, 1).
order: 3
ns: Random
member: value
---
Returns a deterministic fixed-point value in <code>0.0</code>..<code>1.0</code> (exclusive of 1). Multiply it into a range, or compare it against a probability. The sequence is fixed for a given <code>Random.seed</code>.
```ludic
program Demo {
handler Seed phase Start { Random.seed(value: 7) }
handler Step phase Update {
let t = Random.value()
}
}
}
}
```

View file

@ -0,0 +1,7 @@
---
id: save
title: Save
order: 6
---
Whole-world save and restore — write a binary snapshot of the ECS world and read it back.

View file

@ -0,0 +1,14 @@
---
id: save-read
name: Save.read
category: save
kind: namespace-method
tokens: Save.read
sig: Save.read() -> bool
tip: Restore the saved snapshot.
order: 1
ns: Save
member: read
---
Restores the world from the save slot, returning false when there is no snapshot to load.

View file

@ -0,0 +1,14 @@
---
id: save-write
name: Save.write
category: save
kind: namespace-method
tokens: Save.write
sig: Save.write() -> void
tip: Save a snapshot of the world.
order: 0
ns: Save
member: write
---
Writes a binary snapshot of the whole ECS world to the save slot.

View file

@ -0,0 +1,24 @@
---
id: screen-circle
name: Screen.circle
category: screen
kind: namespace-method
tokens: Screen.circle
sig: Screen.circle(x, y, radius, color)
tip: Draw a circle outline.
order: 11
ns: Screen
member: circle
---
Draws the outline of a circle centred at <code>(x, y)</code> with the given <code>radius</code> in <code>color</code>. Use it for rings, radii, and selection markers; for a solid disc use <code>Screen.fill_circle</code>.
```ludic
program Demo {
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
Screen.circle(x: 160, y: 120, radius: 40, color: Color.Cyan)
Screen.show()
}
}
```

View file

@ -0,0 +1,24 @@
---
id: screen-fill_circle
name: Screen.fill_circle
category: screen
kind: namespace-method
tokens: Screen.fill_circle
sig: Screen.fill_circle(x, y, radius, color)
tip: Draw a filled disc.
order: 12
ns: Screen
member: fill_circle
---
Draws a solid disc centred at <code>(x, y)</code> with the given <code>radius</code> in <code>color</code>, one horizontal span per row. Use it for round bodies, bullets, and glows.
```ludic
program Demo {
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
Screen.fill_circle(x: bx, y: by, radius: 6, color: Color.Gold)
Screen.show()
}
}
```

View file

@ -0,0 +1,24 @@
---
id: screen-fill_triangle
name: Screen.fill_triangle
category: screen
kind: namespace-method
tokens: Screen.fill_triangle
sig: Screen.fill_triangle(x1, y1, x2, y2, x3, y3, color)
tip: Draw a filled triangle.
order: 14
ns: Screen
member: fill_triangle
---
Fills the triangle with corners <code>(x1, y1)</code>, <code>(x2, y2)</code>, <code>(x3, y3)</code> in <code>color</code>, scanning its bounding box with an inside test. The building block for flat-shaded polygons and arrowheads.
```ludic
program Demo {
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
Screen.fill_triangle(x1: 10, y1: 10, x2: 30, y2: 10, x3: 20, y3: 26, color: Color.Red)
Screen.show()
}
}
```

View file

@ -0,0 +1,24 @@
---
id: screen-line
name: Screen.line
category: screen
kind: namespace-method
tokens: Screen.line
sig: Screen.line(x1, y1, x2, y2, color)
tip: Draw a straight line between two points.
order: 10
ns: Screen
member: line
---
Draws a one-pixel line from <code>(x1, y1)</code> to <code>(x2, y2)</code> in <code>color</code>, using an integer Bresenham walk so it looks the same on every platform. Use it for lasers, tethers, debug rays, and vector art. Draw during the <code>Render</code> phase.
```ludic
program Demo {
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
Screen.line(x1: px, y1: py, x2: tx, y2: ty, color: Color.White)
Screen.show()
}
}
```

View file

@ -0,0 +1,24 @@
---
id: screen-sprite
name: Screen.sprite
category: screen
kind: namespace-method
tokens: Screen.sprite
sig: Screen.sprite(id, x, y)
tip: Blit a sprite at its natural size.
order: 15
ns: Screen
member: sprite
---
Draws sprite <code>id</code> from the loaded sprite sheet at <code>(x, y)</code>, at its natural 16x16 size. Load the sheet once at start, then draw sprites each frame during <code>Render</code>.
```ludic
program Demo {
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
Screen.sprite(id: hero_frame, x: px, y: py)
Screen.show()
}
}
```

View file

@ -0,0 +1,24 @@
---
id: screen-sprite_scaled
name: Screen.sprite_scaled
category: screen
kind: namespace-method
tokens: Screen.sprite_scaled
sig: Screen.sprite_scaled(id, x, y, scale)
tip: Blit a sprite at an integer scale.
order: 16
ns: Screen
member: sprite_scaled
---
Draws sprite <code>id</code> at <code>(x, y)</code> enlarged by an integer <code>scale</code> (2 doubles it, and so on) — for chunky pixel-art zoom without blurring.
```ludic
program Demo {
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
Screen.sprite_scaled(id: boss, x: bx, y: by, scale: 3)
Screen.show()
}
}
```

View file

@ -0,0 +1,24 @@
---
id: screen-triangle
name: Screen.triangle
category: screen
kind: namespace-method
tokens: Screen.triangle
sig: Screen.triangle(x1, y1, x2, y2, x3, y3, color)
tip: Draw a triangle outline.
order: 13
ns: Screen
member: triangle
---
Draws the outline of the triangle with corners <code>(x1, y1)</code>, <code>(x2, y2)</code>, <code>(x3, y3)</code> in <code>color</code> — three lines. Use it for arrows, wedges, and vector shapes.
```ludic
program Demo {
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)
Screen.triangle(x1: 10, y1: 10, x2: 30, y2: 10, x3: 20, y3: 26, color: Color.White)
Screen.show()
}
}
```

View file

@ -0,0 +1,21 @@
---
id: kw-entry
name: entry
category: structure
kind: keyword
tokens: entry
sig: entry { … }
tip: The program's main block — runs once, top to bottom.
order: 10
---
<code>entry</code> declares the program's entry point: a single block that runs once when the program starts, in order, and then the program exits. It is the counterpart to the ECS game loop — where a game is built from <code>handler</code>s driven by phases each frame, an <code>entry</code> program is a straight-line script, which is why the compiler and tools in this repo are written with it. Use it for command-line programs and one-shot tools; use <code>program</code> with handlers for interactive games.
```ludic
program Greet {
entry {
print("hello")
print(2 + 2)
}
}
```

View file

@ -0,0 +1,24 @@
---
id: kw-new
name: new
category: structure
kind: keyword
tokens: new
sig: new Record { … } new []T
tip: Allocate a property record or an empty slice on the heap.
order: 12
---
<code>new</code> constructs a heap value and returns a reference to it. <code>new Record { field: value, … }</code> allocates a property (or struct) record, filling any omitted fields with their declared defaults; <code>new []T</code> allocates an empty growable slice of element type <code>T</code>, ready for <code>List.push</code>. Because both are references, passing one around shares the same underlying object. Use <code>new</code> for standalone data your code holds directly — as opposed to <code>spawn</code>, which creates a full ECS entity the world tracks and queries.
```ludic
program Build {
property Point { x: int = 0, y: int = 0 }
entry {
let origin = new Point { x: 3 } # y defaults to 0
let items = new []int # empty, then grow it
List.push(items, origin.x)
}
}
```

View file

@ -0,0 +1,23 @@
---
id: kw-public
name: public
category: structure
kind: keyword
tokens: public
sig: scene Name start public { … }
tip: Expose a declaration's lifecycle on the public event bus.
order: 11
---
<code>public</code> marks a <code>scene</code>, <code>layer</code>, or <code>event</code> as part of the game's public surface, so its lifecycle is emitted as named events other systems and mods can react to with <code>@On</code>. A <code>public</code> scene fires <code>scene_&lt;Name&gt;_enter</code> and <code>scene_&lt;Name&gt;_exit</code>; a <code>public</code> layer fires <code>layer_&lt;Name&gt;_show</code> / <code>_hide</code>. It is the declaration-level counterpart to the <code>@Public</code> annotation that promotes a model or handler's lifecycle hooks. Keep it off by default and add it only where the modding surface genuinely needs the hook.
```ludic
program Flow {
@On(scene_Menu_enter) handler Greet { print(1) }
scene Menu start public {
on enter { print(10) } # entering Menu prints 10, then fires scene_Menu_enter
on exit { print(20) }
}
}
```

Some files were not shown because too many files have changed in this diff Show more