Proposal: a fuller type system for game programming — economy numbers (decimal/bigint), idle big-numbers, 2D math, containers & ergonomic wrappers #1

Closed
opened 2026-08-29 17:51:10 +02:00 by orkun · 1 comment
Owner

Context

The current type surface is intentionally small (see the API Reference → Types):
int, fixed, bool, entity, str, ptr, byte, the typed buffers words / fixeds / ptrs, []T slices, property records and enums.

This is a revised, researched version of the first pass — informed by Go, C#, and Godot (a 2D engine), plus the number types tycoon and idle/incremental games depend on. Types are grouped by the job they do for a game developer. Each entry lists a precedent, the game use-case, and — because Ludic's anchor is a deterministic fixed-point runtime — an explicit determinism note.

Determinism policy (unchanged): no binary floating point (f32/f64) in the deterministic simulation. fixed stays the default fractional type; the number types below are either exact (deterministic) or explicitly flagged as display-scale.


A. Numbers for game economies (the part #1 got wrong)

Type What it is Precedent Game use Determinism
decimal 128-bit base-10 exact number, ~28–29 significant digits C# decimal Tycoon / economy money — no binary rounding on values like 0.10; prices, balances, taxes add up exactly Exact → deterministic
bigint Arbitrary-precision integer, no upper bound C# BigInteger, Go math/big.Int Idle counters, exact huge currencies, score hashing, deterministic RNG streams Exact → deterministic (slower)
huge (a.k.a. bignum) mantissa + exponent scientific number reaching ~1e9e15 break_infinity.js (the idle-game standard) Idle / incremental display magnitudes (10^100, 10^1000, …) where speed beats bit-exactness Display-scale, not lockstep — document clearly
sized / unsigned ints: u8 i16 u16 u32 i64 u64 explicit-width integers Go, C# packing, tile ids, RGBA channels, hashes, @Sync/serialize network payloads Exact → deterministic

Notes:

  • decimal and bigint are the correct answer for money and exact large counts — the thing a tycoon game must never round wrong. Prefer them over fixed when the domain is currency, not physics.
  • huge targets idle games specifically: numbers grow past 1e308, so games use a two-float (mantissa/exponent) representation. It should be its own type so its non-exact nature is explicit and it never sneaks into the deterministic sim.
  • Still not proposing f32/f64 — they break replay/lockstep determinism.

B. 2D math & spatial types (from Godot, a 2D engine)

Type Precedent Game use
vec2 (pair of fixed) / ivec2 (pair of int) Godot Vector2 / Vector2i positions, velocities, sizes, directions; + - * scalar, dot, length, normalized, floor -> ivec2
rect / recti Godot Rect2 / Rect2i HUD layout + fast overlap tests: contains(point), intersects(other)
transform2d (2×3) Godot Transform2D camera / parent transforms — translate, rotate, scale (later phase)
angle common gamedev wrapper an auto-wrapping fixed angle with sin/cos/rotate; avoids manual % TAU
color Godot Color first-class RGBA: rgb(), rgba(), lerp, darken; Color.* names already exist as ints

vec2/ivec2 is the single biggest ergonomic win — a 2D game touches vectors constantly, and today they are two loose fields. All of these lower to a couple of i32s, so no new runtime.

C. Containers

Type Precedent Game use
map<K,V> / dictionary Go map, C# Dictionary lookups by id/name — loot tables, entity registries, config
set<T> Go/C# membership — tags, visited tiles, unlocked achievements
[T; N] fixed arrays already a design target in LANGUAGE.md grids, inventories, per-entity slots — no heap
grid<T> (or [T; W*H] sugar) tilemap generalization boards / tilemaps of any element type (today Map.* is a fixed char grid)
tuples / multiple returns Go, C# return a pair without declaring a property

D. Ergonomic & safety wrappers (make the developer's job easier)

Type Precedent Why it helps a game dev
option<T> / maybe Rust/Swift; C# nullable "no target", "empty slot" without magic -1 sentinels
result<T, E> Rust/Swift fallible ops (load, parse, net) without out-params or silent failure
tagged-union enum (payloads) Rust/Swift, C# records + pattern matching model game state, events, tile kinds as one value and match on it — today enum is only int constants
handle / typed id generalizes Ludic's entity asset/entity handles that can't be mixed up with a bare int
duration / tick gamedev unit-safety frame/time values that don't get confused with plain ints
percent / normalized gamedev a clamped [0,1] fixed for health %, volume, lerp t
rune / char Go rune a Unicode code point for text handling
name / interned string Godot StringName fast id comparisons (compare by handle, not bytes)

Suggested phasing

  1. vec2 / ivec2 + rect + color — daily 2D ergonomics, biggest win.
  2. decimal + bigint — economy/idle correctness (the money problem).
  3. option / result + tagged-union enums — safety and clean control flow.
  4. map / set + [T; N] — containers.
  5. huge (idle display), sized ints, angle / percent / handle / duration / rune / name — polish.

All numeric/vector proposals lower to existing IR types (i32 pairs, i8/i16/i64, or a small struct), so most of this is front-end sugar + type checking, not new runtime.

References

Revised after researching Go, C#, and Godot; supersedes the initial sketch.

## Context The current type surface is intentionally small (see the API Reference → Types): `int`, `fixed`, `bool`, `entity`, `str`, `ptr`, `byte`, the typed buffers `words` / `fixeds` / `ptrs`, `[]T` slices, `property` records and `enum`s. This is a **revised, researched** version of the first pass — informed by **Go**, **C#**, and **Godot** (a 2D engine), plus the number types **tycoon** and **idle/incremental** games depend on. Types are grouped by the job they do for a game developer. Each entry lists a precedent, the game use-case, and — because Ludic's anchor is a **deterministic fixed-point runtime** — an explicit determinism note. > **Determinism policy (unchanged):** no binary floating point (`f32`/`f64`) in the deterministic simulation. `fixed` stays the default fractional type; the number types below are either exact (deterministic) or explicitly flagged as display-scale. --- ## A. Numbers for game economies (the part #1 got wrong) | Type | What it is | Precedent | Game use | Determinism | |---|---|---|---|---| | `decimal` | 128-bit **base-10** exact number, ~28–29 significant digits | **C# `decimal`** | **Tycoon / economy money** — no binary rounding on values like `0.10`; prices, balances, taxes add up exactly | Exact → deterministic | | `bigint` | Arbitrary-precision **integer**, no upper bound | **C# `BigInteger`**, **Go `math/big.Int`** | Idle counters, exact huge currencies, score hashing, deterministic RNG streams | Exact → deterministic (slower) | | `huge` (a.k.a. `bignum`) | **mantissa + exponent** scientific number reaching ~`1e9e15` | **break_infinity.js** (the idle-game standard) | **Idle / incremental** display magnitudes (10^100, 10^1000, …) where speed beats bit-exactness | **Display-scale, not lockstep** — document clearly | | sized / unsigned ints: `u8` `i16` `u16` `u32` `i64` `u64` | explicit-width integers | **Go**, **C#** | packing, tile ids, RGBA channels, hashes, `@Sync`/`serialize` network payloads | Exact → deterministic | Notes: - `decimal` and `bigint` are the correct answer for **money and exact large counts** — the thing a tycoon game must never round wrong. Prefer them over `fixed` when the domain is currency, not physics. - `huge` targets **idle games specifically**: numbers grow past `1e308`, so games use a two-float (mantissa/exponent) representation. It should be its own type so its non-exact nature is explicit and it never sneaks into the deterministic sim. - Still **not** proposing `f32`/`f64` — they break replay/lockstep determinism. ## B. 2D math & spatial types (from Godot, a 2D engine) | Type | Precedent | Game use | |---|---|---| | `vec2` (pair of `fixed`) / `ivec2` (pair of `int`) | **Godot `Vector2` / `Vector2i`** | positions, velocities, sizes, directions; `+ - * scalar`, `dot`, `length`, `normalized`, `floor -> ivec2` | | `rect` / `recti` | **Godot `Rect2` / `Rect2i`** | HUD layout + fast overlap tests: `contains(point)`, `intersects(other)` | | `transform2d` (2×3) | **Godot `Transform2D`** | camera / parent transforms — translate, rotate, scale (later phase) | | `angle` | common gamedev wrapper | an auto-wrapping `fixed` angle with `sin`/`cos`/`rotate`; avoids manual `% TAU` | | `color` | **Godot `Color`** | first-class RGBA: `rgb()`, `rgba()`, `lerp`, `darken`; `Color.*` names already exist as `int`s | `vec2`/`ivec2` is the single biggest ergonomic win — a 2D game touches vectors constantly, and today they are two loose fields. All of these lower to a couple of `i32`s, so no new runtime. ## C. Containers | Type | Precedent | Game use | |---|---|---| | `map<K,V>` / dictionary | **Go `map`**, **C# `Dictionary`** | lookups by id/name — loot tables, entity registries, config | | `set<T>` | Go/C# | membership — tags, visited tiles, unlocked achievements | | `[T; N]` fixed arrays | already a design target in `LANGUAGE.md` | grids, inventories, per-entity slots — no heap | | `grid<T>` (or `[T; W*H]` sugar) | tilemap generalization | boards / tilemaps of any element type (today `Map.*` is a fixed char grid) | | tuples / multiple returns | **Go**, **C#** | return a pair without declaring a `property` | ## D. Ergonomic & safety wrappers (make the developer's job easier) | Type | Precedent | Why it helps a game dev | |---|---|---| | `option<T>` / maybe | Rust/Swift; **C# nullable** | "no target", "empty slot" without magic `-1` sentinels | | `result<T, E>` | Rust/Swift | fallible ops (`load`, parse, net) without out-params or silent failure | | tagged-union `enum` (payloads) | Rust/Swift, **C# records + pattern matching** | model game state, events, tile kinds as one value and `match` on it — today `enum` is only int constants | | `handle` / typed id | generalizes Ludic's `entity` | asset/entity handles that can't be mixed up with a bare `int` | | `duration` / `tick` | gamedev unit-safety | frame/time values that don't get confused with plain ints | | `percent` / `normalized` | gamedev | a clamped `[0,1]` `fixed` for health %, volume, lerp `t` | | `rune` / `char` | **Go `rune`** | a Unicode code point for text handling | | `name` / interned string | **Godot `StringName`** | fast id comparisons (compare by handle, not bytes) | --- ## Suggested phasing 1. **`vec2` / `ivec2` + `rect` + `color`** — daily 2D ergonomics, biggest win. 2. **`decimal` + `bigint`** — economy/idle correctness (the money problem). 3. **`option` / `result` + tagged-union `enum`s** — safety and clean control flow. 4. **`map` / `set` + `[T; N]`** — containers. 5. **`huge`** (idle display), sized ints, `angle` / `percent` / `handle` / `duration` / `rune` / `name` — polish. All numeric/vector proposals lower to existing IR types (`i32` pairs, `i8`/`i16`/`i64`, or a small struct), so most of this is front-end sugar + type checking, not new runtime. ## References - C# numerics: [`decimal`](https://learn.microsoft.com/en-us/dotnet/standard/numerics), [`BigInteger`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.biginteger), [`System.Numerics`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics) (Vector2, Complex, Half) - Go type system: [Go 101 — Type System Overview](https://go101.org/article/type-system-overview.html) (`rune`, `byte`, `complex`, `map`, `slice`, `channel`) and `math/big` - Godot 2D built-ins: [`Rect2`](https://docs.godotengine.org/en/stable/classes/class_rect2.html), `Vector2`/`Vector2i`, `Color`, `Transform2D` - Idle-game big numbers: [break_infinity.js](https://patashu.github.io/break_infinity.js/index.html) _Revised after researching Go, C#, and Godot; supersedes the initial sketch._
orkun changed title from Proposal: richer built-in types for game programming (vectors, color, sized ints, fixed arrays) to Proposal: a fuller type system for game programming — economy numbers (decimal/bigint), idle big-numbers, 2D math, containers & ergonomic wrappers 2026-08-29 17:57:14 +02:00
orkun added the
proposal
priority:high
area:types
labels 2026-08-29 19:51:35 +02:00
Author
Owner

Phase 1 shipped — IVec2 + Rect (2c9f9ac)

Landing the first phase of this proposal and decomposing the rest into tracked issues so the roadmap survives.

What shipped (phase 1 — the "biggest ergonomic win")

Phase 1 was vec2 / ivec2 + rect + color. Two of the four already existed, so this pass adds the two that were missing:

  • IVec2 — an integer 2D vector, a pair of int packed into one i64 (mirrors the existing Vector, but whole-number). For tile/grid coordinates, cell offsets and integer sizes, where a fractional part would be a bug. Ops: make / zero / x / y / add / sub / scale / dot, the grid distance manhattan, equal, and to_vector (widen into the fixed-point Vector).
  • Rect — an axis-aligned rectangle, four Q16.16 fixed components packed into one i128. For HUD boxes, hitboxes and viewport regions. Ops: make / x / y / w / h, the derived right / bottom / center, and the contains (point) / intersects (overlap) tests.

Both are true by-value types — they copy like scalars and never allocate — and every operation is exact and deterministic, bit-identical on every platform, exactly as the determinism policy requires.

Already present, so left as-is: vec2 is the existing Vector (pair of fixed), and color is the existing first-class Color (constants + Color.* with rgb/rgba/lerp/darken). Phase 1 is therefore complete.

Wired end to end: emit_core llty (IVec2→i64, Rect→i128), emit_call dispatch, the FRAGS list + reseeded C-free seed, a self-host test (types2d), per-symbol docs + type pages + inventory, and full vocabulary/editor sync (header, JetBrains, TextMate, LSP). All suites green: x test 81/81, x test-tools 30/30, x selfhost-test 30/30 (incl. golden renders byte-identical and the bootstrap fixpoint), plus x check-impl / x check-vocabulary / x check-docs.

Remaining phases — now tracked as their own issues

This proposal is an umbrella RFC spanning ~30 types; the rest is best tracked as focused issues rather than one giant ticket:

  • #52 — phase 2: economy/idle correctness (decimal + bigint)
  • #53 — phase 3: safety & control flow (option / result + tagged-union enums)
  • #54 — phase 4: containers (map / set + [T; N])
  • #55 — phase 5: polish (huge, sized ints, angle / percent / handle / duration / rune / name)

Closing this proposal as accepted and phase-1-delivered; follow the sub-issues above for the rest.

## Phase 1 shipped — `IVec2` + `Rect` (2c9f9ac) Landing the first phase of this proposal and decomposing the rest into tracked issues so the roadmap survives. ### What shipped (phase 1 — the "biggest ergonomic win") Phase 1 was **vec2 / ivec2 + rect + color**. Two of the four already existed, so this pass adds the two that were missing: - **`IVec2`** — an integer 2D vector, a pair of `int` packed into one `i64` (mirrors the existing `Vector`, but whole-number). For tile/grid coordinates, cell offsets and integer sizes, where a fractional part would be a bug. Ops: `make` / `zero` / `x` / `y` / `add` / `sub` / `scale` / `dot`, the grid distance `manhattan`, `equal`, and `to_vector` (widen into the fixed-point `Vector`). - **`Rect`** — an axis-aligned rectangle, four Q16.16 `fixed` components packed into one `i128`. For HUD boxes, hitboxes and viewport regions. Ops: `make` / `x` / `y` / `w` / `h`, the derived `right` / `bottom` / `center`, and the `contains` (point) / `intersects` (overlap) tests. Both are true by-value types — they copy like scalars and never allocate — and every operation is exact and deterministic, bit-identical on every platform, exactly as the determinism policy requires. **Already present, so left as-is:** `vec2` is the existing `Vector` (pair of `fixed`), and `color` is the existing first-class `Color` (constants + `Color.*` with `rgb`/`rgba`/`lerp`/`darken`). Phase 1 is therefore complete. Wired end to end: `emit_core` `llty` (`IVec2`→i64, `Rect`→i128), `emit_call` dispatch, the FRAGS list + reseeded C-free seed, a self-host test (`types2d`), per-symbol docs + type pages + inventory, and full vocabulary/editor sync (header, JetBrains, TextMate, LSP). All suites green: `x test` 81/81, `x test-tools` 30/30, `x selfhost-test` 30/30 (incl. golden renders byte-identical and the bootstrap fixpoint), plus `x check-impl` / `x check-vocabulary` / `x check-docs`. ### Remaining phases — now tracked as their own issues This proposal is an umbrella RFC spanning ~30 types; the rest is best tracked as focused issues rather than one giant ticket: - **#52** — phase 2: economy/idle correctness (`decimal` + `bigint`) - **#53** — phase 3: safety & control flow (`option` / `result` + tagged-union `enum`s) - **#54** — phase 4: containers (`map` / `set` + `[T; N]`) - **#55** — phase 5: polish (`huge`, sized ints, `angle` / `percent` / `handle` / `duration` / `rune` / `name`) Closing this proposal as **accepted and phase-1-delivered**; follow the sub-issues above for the rest.
orkun closed this issue 2026-08-31 17:32:29 +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#1
No description provided.