Proposal: a namespaced standard library — Math (trig/lerp), Text, List, Time, Audio, and namespacing the bare builtins (World/Net/Sys/Save/Mem) #2

Closed
opened 2026-08-29 18:02:33 +02:00 by orkun · 1 comment
Owner

Context

Ludic already namespaces part of its API — Screen.*, Input.*, Random.*, Map.*, Color.* — but the rest of the built-in surface is small and unnamespaced: print, save, load, len, push, min, max, abs, clamp, fx, flr, bytes, words, the world_* reflection ABI, the net_* networking calls, and process calls (arg, exit, run, getenv, read_char, file_*). Notably there is no sin/cos/sqrt/lerp/remap, almost no string manipulation, no audio, and no time/frame access — things every game reaches for on day one.

This proposal does two things:

  1. Adopt a consistent namespaced standard library (PascalCase, like the existing Screen.*), moving the bare builtins into namespaces.
  2. Fill the obvious gaps, researched against PICO-8 (a minimalist fantasy console — the best model for a small game language), Godot (2D engine), LÖVE, Unity Mathf, and Go / C# standard libraries.

Determinism first. Ludic's anchor is a deterministic fixed-point runtime. All Math.* trig/roots are computed in fixed-point via lookup/CORDIC tables so they are bit-identical on every platform — a real differentiator vs float-based engines. Anything non-deterministic (wall-clock time, wall RNG) is called out explicitly.

Legend: ✓ exists today (bare) → namespace it · + new.


Core math — Math. (the biggest gap)

Today only min max abs clamp fx flr exist. A game needs the rest.

Call Notes
Math.min Math.max Math.abs Math.clamp ✓ move from bare
Math.sign Math.floor Math.ceil Math.round + (floor = today's flr)
Math.sin Math.cos Math.tan Math.atan2 Math.asin Math.acos + fixed-point, table-driven, deterministic
Math.sqrt Math.pow Math.exp Math.log Math.hypot + fixed-point
Math.lerp Math.inverse_lerp Math.remap Math.smoothstep Math.ease + interpolation (Godot lerp/remap/smoothstep/ease)
Math.move_toward Math.ping_pong Math.wrap Math.snapped Math.posmod + (Godot move_toward/pingpong/wrapi/snapped)
Math.dist Math.dist2 Math.deg_to_rad Math.rad_to_deg + 2D distance + angle units
Math.to_fixed Math.floor_to_int ✓ today's fx / flr, renamed for clarity

Text / strings — Text.

Today: only str(x), len(s), and slicing s[a..b]. Games format HUDs and parse input constantly.

Text.length Text.char_at Text.slice Text.index_of Text.contains Text.starts_with Text.ends_with Text.upper Text.lower Text.trim Text.split Text.join Text.replace Text.repeat Text.pad_left Text.pad_right Text.from_int Text.to_int + (Go strings, C# String; interpolation via backticks already exists)

Collections — List. (over []T slices)

Today: push, len. PICO-8 has add/del/all/count/foreach.

List.push ✓ · List.pop List.insert List.remove_at List.remove List.contains List.index_of List.clear List.reverse List.sort List.swap List.first List.last + · plus a Map2.* / dictionary + Set.* once the container types land (see #1).

Drawing — extend Screen.

Today: clear fill_rectangle draw_rectangle put_pixel draw_text draw_number show width height status. Add the primitives every 2D game uses (PICO-8 line/circ/circfill/spr/camera/clip):

Screen.line Screen.circle Screen.fill_circle Screen.triangle Screen.fill_triangle Screen.oval Screen.sprite Screen.sprite_scaled Screen.camera Screen.clip Screen.measure_text Screen.blend_mode +

Input — extend Input.

Today: Input.key(). Add edge detection + pointer + pads (PICO-8 btn/btnp, LÖVE mouse):

Input.key_down(k) Input.key_pressed(k) Input.key_released(k) Input.mouse_x Input.mouse_y Input.mouse_down(b) Input.mouse_pressed(b) Input.wheel Input.axis(neg, pos) Input.button(pad, b) Input.text +

Audio — Audio. (missing entirely)

Audio.play_sound(id) Audio.play_music(id) Audio.stop_music Audio.stop_all Audio.volume(v) Audio.pitch(v) Audio.is_playing(id) + (PICO-8 sfx/music, LÖVE love.audio)

Time — Time.

Time.frame() (deterministic frame counter) · Time.elapsed() (fixed seconds since start) · Time.delta() (fixed dt) + · Time.now() + (wall clock — non-deterministic, flagged; for seeding/telemetry only)

Random — extend Random.

Today: range chance seed. Add: Random.value() (fixed 0..1) Random.int(max) Random.sign() Random.pick(list) Random.shuffle(list) Random.direction() -> vec2 Random.color() + · and Random.stream(name) + — independent, named deterministic streams so gameplay RNG and cosmetic RNG can't desync a replay.

Tilemap — extend Map.

Today: size row tile. Add: Map.set_tile(x, y, ch) Map.in_bounds(x, y) Map.fill(ch) Map.find(ch) -> list Map.neighbors(x, y) +

Rename the bare builtins into namespaces

Today (bare) Proposed
world_get/set/has/count/size/spawn/save/load/prop_id/field_id/model_id/kind/register_prop/attach_dyn/detach_dyn/query_next ✓ World.*
net_send/net_poll/serialize/apply/owner/set_owner/is_server/is_owner/local_id ✓ Net.*
save()/load() ✓ Save.write()/read() (+ Save.exists, Save.clear, Save.slot(n) +)
arg_count/arg/exit/run/getenv/read_char/file_* ✓ Sys.* (Sys.args, Sys.exit, Sys.run, Sys.env, Sys.read_char, Sys.stdout/stderr/write)
bytes/words (+ fixeds/ptrs) ✓ Mem.bytes/words/fixeds/ptrs + Mem.copy, Mem.fill, Mem.peek, Mem.poke + (PICO-8 memcpy/memset/peek/poke)
print ✓ keep print as a convenience and add Debug.print/assert/log/watch +
ui_build ✓ UI.build/open/tick/render/clicked/set_text/... (already partly namespaced conceptually)

Creative additions — leaning into what Ludic is (deterministic ECS + 2D)

  • Query. — ECS-native helpers beyond the query keyword: Query.count([Prop]), Query.first([Prop]) -> entity, Query.nearest(from, [Prop]) -> entity, Query.within(center, radius, [Prop]) -> list. The built-in ECS makes spatial/aggregate queries a natural builtin.
  • Vec. — 2D vector ops (pairs with the vec2 type in #1): Vec.make, add, sub, scale, dot, length, normalize, rotate, angle, from_angle, lerp, distance.
  • Ease. — tween curves: Ease.in, Ease.out, Ease.in_out, Ease.elastic, Ease.bounce, Ease.back — the "juice" layer, deterministic.
  • Collide. — 2D collision: Collide.rects(a, b), Collide.point_rect(p, r), Collide.circles(a, b), Collide.rect_circle(...).
  • Grid. / Path. — 2D staples: Path.a_star(cost_map, from, to) -> list, Grid.line(a, b) (Bresenham), Grid.flood_fill(x, y), Grid.line_of_sight(a, b).
  • Camera.: Camera.set(pos), Camera.follow(entity, lerp), Camera.shake(amount) (deterministic shake from the seeded RNG).
  • Color. extensions: Color.rgb, Color.rgba, Color.lerp, Color.darken, Color.lighten, Color.with_alpha.

Principles

  • PascalCase namespaces, matching the existing Screen.* — discoverable via Namespace. completion, and each becomes a documentation page (the reference already renders namespace overview pages).
  • Deterministic by default; the few non-deterministic calls (Time.now, a wall-clock RNG seed) are explicitly named and documented as such.
  • Tiny runtime, curated surface — follow PICO-8's discipline: enough to build real games, not a kitchen sink. Many Math.*/Vec.*/Ease.* calls are pure fixed-point and lower to a handful of IR ops.
  • Back-compat: keep bare aliases (print, min, max, abs, clamp, flr) during a transition; everything else moves under a namespace.

Suggested phasing

  1. Math.* (trig, sqrt, lerp/remap/move_toward/smoothstep) — unblocks real gameplay math today.
  2. Text.* + List.* — HUDs, parsing, collections.
  3. Namespace the bare set (World. Net. Sys. Save. Mem.).
  4. Time.*, Audio.*, extended Input.*/Screen.*.
  5. Creative: Vec. Query. Ease. Collide. Camera. Path..

References

  • PICO-8 API reference (minimalist fantasy-console surface: spr, map, btn, sfx, flr, sin, cos, rnd, mid, circ, line, camera, peek/poke)
  • Godot @GlobalScope (lerp, remap, move_toward, smoothstep, ease, snapped, wrapi, pingpong, posmod, trig)
  • LÖVE (love2d) wiki (love.graphics, love.audio, love.keyboard, love.math, love.timer)
  • Go strings / math, C# Math/MathF / String
  • Related: type proposal in #1 (vec2/ivec2/color back the Vec.*/Color.* calls).

Researched against PICO-8, Godot, LÖVE, Unity and Go/C#; creative additions chosen for a deterministic ECS-based 2D language.

## Context Ludic already namespaces part of its API — `Screen.*`, `Input.*`, `Random.*`, `Map.*`, `Color.*` — but the rest of the built-in surface is **small and unnamespaced**: `print`, `save`, `load`, `len`, `push`, `min`, `max`, `abs`, `clamp`, `fx`, `flr`, `bytes`, `words`, the `world_*` reflection ABI, the `net_*` networking calls, and process calls (`arg`, `exit`, `run`, `getenv`, `read_char`, `file_*`). Notably there is **no `sin`/`cos`/`sqrt`/`lerp`/`remap`**, almost no string manipulation, no audio, and no time/frame access — things every game reaches for on day one. This proposal does two things: 1. **Adopt a consistent namespaced standard library** (PascalCase, like the existing `Screen.*`), moving the bare builtins into namespaces. 2. **Fill the obvious gaps**, researched against **PICO-8** (a minimalist fantasy console — the best model for a small game language), **Godot** (2D engine), **LÖVE**, **Unity `Mathf`**, and **Go / C#** standard libraries. > **Determinism first.** Ludic's anchor is a deterministic fixed-point runtime. All `Math.*` trig/roots are computed in **fixed-point via lookup/CORDIC tables** so they are bit-identical on every platform — a real differentiator vs float-based engines. Anything non-deterministic (wall-clock time, wall RNG) is called out explicitly. Legend: **✓** exists today (bare) → namespace it · **+** new. --- ## Core math — `Math.` (the biggest gap) Today only `min max abs clamp fx flr` exist. A game needs the rest. | Call | Notes | |---|---| | `Math.min` `Math.max` `Math.abs` `Math.clamp` ✓ | move from bare | | `Math.sign` `Math.floor` `Math.ceil` `Math.round` + | (`floor` = today's `flr`) | | `Math.sin` `Math.cos` `Math.tan` `Math.atan2` `Math.asin` `Math.acos` + | **fixed-point, table-driven, deterministic** | | `Math.sqrt` `Math.pow` `Math.exp` `Math.log` `Math.hypot` + | fixed-point | | `Math.lerp` `Math.inverse_lerp` `Math.remap` `Math.smoothstep` `Math.ease` + | interpolation (Godot `lerp`/`remap`/`smoothstep`/`ease`) | | `Math.move_toward` `Math.ping_pong` `Math.wrap` `Math.snapped` `Math.posmod` + | (Godot `move_toward`/`pingpong`/`wrapi`/`snapped`) | | `Math.dist` `Math.dist2` `Math.deg_to_rad` `Math.rad_to_deg` + | 2D distance + angle units | | `Math.to_fixed` `Math.floor_to_int` ✓ | today's `fx` / `flr`, renamed for clarity | ## Text / strings — `Text.` Today: only `str(x)`, `len(s)`, and slicing `s[a..b]`. Games format HUDs and parse input constantly. `Text.length` `Text.char_at` `Text.slice` `Text.index_of` `Text.contains` `Text.starts_with` `Text.ends_with` `Text.upper` `Text.lower` `Text.trim` `Text.split` `Text.join` `Text.replace` `Text.repeat` `Text.pad_left` `Text.pad_right` `Text.from_int` `Text.to_int` + (Go `strings`, C# `String`; interpolation via backticks already exists) ## Collections — `List.` (over `[]T` slices) Today: `push`, `len`. PICO-8 has `add/del/all/count/foreach`. `List.push` ✓ · `List.pop` `List.insert` `List.remove_at` `List.remove` `List.contains` `List.index_of` `List.clear` `List.reverse` `List.sort` `List.swap` `List.first` `List.last` + · plus a `Map2.*` / dictionary + `Set.*` once the container types land (see #1). ## Drawing — extend `Screen.` Today: `clear fill_rectangle draw_rectangle put_pixel draw_text draw_number show width height status`. Add the primitives every 2D game uses (PICO-8 `line/circ/circfill/spr/camera/clip`): `Screen.line` `Screen.circle` `Screen.fill_circle` `Screen.triangle` `Screen.fill_triangle` `Screen.oval` `Screen.sprite` `Screen.sprite_scaled` `Screen.camera` `Screen.clip` `Screen.measure_text` `Screen.blend_mode` + ## Input — extend `Input.` Today: `Input.key()`. Add edge detection + pointer + pads (PICO-8 `btn/btnp`, LÖVE mouse): `Input.key_down(k)` `Input.key_pressed(k)` `Input.key_released(k)` `Input.mouse_x` `Input.mouse_y` `Input.mouse_down(b)` `Input.mouse_pressed(b)` `Input.wheel` `Input.axis(neg, pos)` `Input.button(pad, b)` `Input.text` + ## Audio — `Audio.` (missing entirely) `Audio.play_sound(id)` `Audio.play_music(id)` `Audio.stop_music` `Audio.stop_all` `Audio.volume(v)` `Audio.pitch(v)` `Audio.is_playing(id)` + (PICO-8 `sfx/music`, LÖVE `love.audio`) ## Time — `Time.` `Time.frame()` (deterministic frame counter) · `Time.elapsed()` (fixed seconds since start) · `Time.delta()` (fixed dt) + · `Time.now()` + (wall clock — **non-deterministic**, flagged; for seeding/telemetry only) ## Random — extend `Random.` Today: `range chance seed`. Add: `Random.value()` (fixed 0..1) `Random.int(max)` `Random.sign()` `Random.pick(list)` `Random.shuffle(list)` `Random.direction() -> vec2` `Random.color()` + · and **`Random.stream(name)`** + — independent, named deterministic streams so gameplay RNG and cosmetic RNG can't desync a replay. ## Tilemap — extend `Map.` Today: `size row tile`. Add: `Map.set_tile(x, y, ch)` `Map.in_bounds(x, y)` `Map.fill(ch)` `Map.find(ch) -> list` `Map.neighbors(x, y)` + ## Rename the bare builtins into namespaces | Today (bare) | Proposed | |---|---| | `world_get/set/has/count/size/spawn/save/load/prop_id/field_id/model_id/kind/register_prop/attach_dyn/detach_dyn/query_next` ✓ | **`World.*`** | | `net_send/net_poll/serialize/apply/owner/set_owner/is_server/is_owner/local_id` ✓ | **`Net.*`** | | `save()/load()` ✓ | **`Save.write()/read()`** (+ `Save.exists`, `Save.clear`, `Save.slot(n)` +) | | `arg_count/arg/exit/run/getenv/read_char/file_*` ✓ | **`Sys.*`** (`Sys.args`, `Sys.exit`, `Sys.run`, `Sys.env`, `Sys.read_char`, `Sys.stdout/stderr/write`) | | `bytes/words` (+ `fixeds/ptrs`) ✓ | **`Mem.bytes/words/fixeds/ptrs`** + `Mem.copy`, `Mem.fill`, `Mem.peek`, `Mem.poke` + (PICO-8 `memcpy/memset/peek/poke`) | | `print` ✓ | keep `print` as a convenience **and** add **`Debug.print/assert/log/watch`** + | | `ui_build` ✓ | **`UI.build/open/tick/render/clicked/set_text/...`** (already partly namespaced conceptually) | --- ## Creative additions — leaning into what Ludic is (deterministic ECS + 2D) - **`Query.` — ECS-native helpers** beyond the `query` keyword: `Query.count([Prop])`, `Query.first([Prop]) -> entity`, `Query.nearest(from, [Prop]) -> entity`, `Query.within(center, radius, [Prop]) -> list`. The built-in ECS makes spatial/aggregate queries a natural builtin. - **`Vec.` — 2D vector ops** (pairs with the `vec2` type in #1): `Vec.make`, `add`, `sub`, `scale`, `dot`, `length`, `normalize`, `rotate`, `angle`, `from_angle`, `lerp`, `distance`. - **`Ease.` — tween curves**: `Ease.in`, `Ease.out`, `Ease.in_out`, `Ease.elastic`, `Ease.bounce`, `Ease.back` — the "juice" layer, deterministic. - **`Collide.` — 2D collision**: `Collide.rects(a, b)`, `Collide.point_rect(p, r)`, `Collide.circles(a, b)`, `Collide.rect_circle(...)`. - **`Grid.` / `Path.` — 2D staples**: `Path.a_star(cost_map, from, to) -> list`, `Grid.line(a, b)` (Bresenham), `Grid.flood_fill(x, y)`, `Grid.line_of_sight(a, b)`. - **`Camera.`**: `Camera.set(pos)`, `Camera.follow(entity, lerp)`, `Camera.shake(amount)` (deterministic shake from the seeded RNG). - **`Color.` extensions**: `Color.rgb`, `Color.rgba`, `Color.lerp`, `Color.darken`, `Color.lighten`, `Color.with_alpha`. --- ## Principles - **PascalCase namespaces**, matching the existing `Screen.*` — discoverable via `Namespace.` completion, and each becomes a documentation page (the reference already renders namespace overview pages). - **Deterministic by default**; the few non-deterministic calls (`Time.now`, a wall-clock RNG seed) are explicitly named and documented as such. - **Tiny runtime, curated surface** — follow PICO-8's discipline: enough to build real games, not a kitchen sink. Many `Math.*`/`Vec.*`/`Ease.*` calls are pure fixed-point and lower to a handful of IR ops. - **Back-compat**: keep bare aliases (`print`, `min`, `max`, `abs`, `clamp`, `flr`) during a transition; everything else moves under a namespace. ## Suggested phasing 1. **`Math.*`** (trig, sqrt, lerp/remap/move_toward/smoothstep) — unblocks real gameplay math today. 2. **`Text.*`** + **`List.*`** — HUDs, parsing, collections. 3. Namespace the bare set (**`World.` `Net.` `Sys.` `Save.` `Mem.`**). 4. **`Time.*`**, **`Audio.*`**, extended **`Input.*`**/**`Screen.*`**. 5. Creative: **`Vec.` `Query.` `Ease.` `Collide.` `Camera.` `Path.`**. ## References - [PICO-8 API reference](https://pico-8.fandom.com/wiki/APIReference) (minimalist fantasy-console surface: `spr`, `map`, `btn`, `sfx`, `flr`, `sin`, `cos`, `rnd`, `mid`, `circ`, `line`, `camera`, `peek`/`poke`) - [Godot `@GlobalScope`](https://docs.godotengine.org/en/stable/classes/class_@globalscope.html) (`lerp`, `remap`, `move_toward`, `smoothstep`, `ease`, `snapped`, `wrapi`, `pingpong`, `posmod`, trig) - [LÖVE (love2d) wiki](https://love2d.org/wiki/Main_Page) (`love.graphics`, `love.audio`, `love.keyboard`, `love.math`, `love.timer`) - Go `strings` / `math`, C# `Math`/`MathF` / `String` - Related: type proposal in #1 (`vec2`/`ivec2`/`color` back the `Vec.*`/`Color.*` calls). _Researched against PICO-8, Godot, LÖVE, Unity and Go/C#; creative additions chosen for a deterministic ECS-based 2D language._
orkun added the
proposal
priority:high
area:stdlib
status:in-progress
labels 2026-08-29 19:51:35 +02:00
Author
Owner

The namespaced-stdlib core is implemented and lives on branch stdlib-namespaces (PR: main...stdlib-namespaces): 156 namespace methods across Math, Text, List, Ease, Collide, World, Net, Sys, Save, Mem, extended Screen, Color functions, extended Random, and Time — all tested, documented, and self-hosting (the C-free bootstrap fixpoint holds).

Closing this in favour of focused follow-ups for the scope that needs work outside this proposal:

  • #22 #23 #24 #25 — Audio (needs a runtime audio backend)
  • rendering/camera, spatial/pathfinding, and Vec+transcendentals (see the new issues above)

Already tracked elsewhere: Vec.*/rect types → #1; extended Input.* (mouse/gamepad/edge) → #7 (the input revamp, which supersedes #2's input sketch).

The namespaced-stdlib **core is implemented** and lives on branch `stdlib-namespaces` (PR: main...stdlib-namespaces): **156 namespace methods** across `Math`, `Text`, `List`, `Ease`, `Collide`, `World`, `Net`, `Sys`, `Save`, `Mem`, extended `Screen`, `Color` functions, extended `Random`, and `Time` — all tested, documented, and self-hosting (the C-free bootstrap fixpoint holds). Closing this in favour of focused follow-ups for the scope that needs work outside this proposal: - #22 #23 #24 #25 — Audio (needs a runtime audio backend) - rendering/camera, spatial/pathfinding, and Vec+transcendentals (see the new issues above) Already tracked elsewhere: **`Vec.*`/rect types → #1**; **extended `Input.*` (mouse/gamepad/edge) → #7** (the input revamp, which supersedes #2's input sketch).
orkun 2026-08-29 23:39:28 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: workshopsoft/ludic#2
No description provided.