docs(api): per-symbol pages, fuzzy search, deep token linking, hover cards
All checks were successful
docs / build-and-deploy (push) Successful in 2s
All checks were successful
docs / build-and-deploy (push) Successful in 2s
Rebuild the API Reference around one page per symbol and richer, verified content.
Pages & navigation
- One HTML page per symbol (kw-*, type-*, phase-*, screen-*, fn-*, annot-*, op-*)
instead of a single scrolling page; namespace overview pages (ns-screen …
ns-color) and a searchable index (api.html) with client-side fuzzy search.
- Sticky-header scroll offset (scroll-margin) so a jumped-to entry/param/color is
never hidden, plus a flash highlight on the scrolled-to target.
Deep linking in every snippet & example
- Namespace members split: `Screen`→namespace page, `fill_rectangle`→method page;
`Color`→palette page, `Charcoal`→its swatch — separately.
- Named arguments (`width:`) link to that parameter's anchor on the method page.
- Hover any token for a summary card built from the real API data (symbols.json).
Content & coverage
- Full authoritative surface documented from the compiler: every keyword, type,
the 6 phases (Start/Input/FixedUpdate/Update/LateUpdate/Render, each its own
page), all 22 annotations, namespace methods with parameter docs, builtins,
the world_* reflection ABI, networking, operators — 155 symbols.
- Longer, clearer explanations; "model"/"model instance" terminology, not "entity";
descriptive identifiers in every example (Position{column,row}, Velocity{delta_x,
delta_y}, Health{current,maximum}, Player/Enemy) — no Pos/Seg/x/dx.
- Accuracy fixes from compiler ground-truth: world_count() takes no arg,
world_query_next(property, cursor) arg order, event fields bind by name; dropped
`when` and `module` (not in the self-hosted parser).
Tooling
- inventory.json + check.py: coverage guard (every symbol has a page), duplicate-
token guard, and broken-link guard — fail CI so docs can't drift.
- validate.py: compiles every ```ludic example against bin/ludicc (158 compile).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
25f987e30d
commit
3c7ec9b016
172 changed files with 5240 additions and 895 deletions
|
|
@ -5,8 +5,30 @@ category: control
|
|||
kind: keyword
|
||||
tokens: become
|
||||
sig: become Name
|
||||
tip: Transition: to another state of the enclosing machine, or to another scene.
|
||||
tip: Transition to another state of the enclosing machine, or to another scene.
|
||||
order: 8
|
||||
---
|
||||
|
||||
Transition: to another <code>state</code> of the enclosing machine, or to another <code>scene</code>.
|
||||
A <code>become</code> statement performs a transition. Inside a <code>machine</code>, `become Name` moves to another `state` of that machine by storing the target state's value back into the machine's store, so the next dispatch runs the new state. Used from a scene's layer handler, `become Scene` instead switches the active scene: the current scene's `on exit` runs, the active-scene register is set, and the target scene's `on enter` runs — two direct calls and a store, with no dispatch table. `become` names its target and knows from context which kind of transition it is, so you rarely think about the machinery. Transitioning is cheap and takes effect immediately for machine states.
|
||||
|
||||
```ludic
|
||||
program EncounterFlow {
|
||||
enum Stage { Explore, Battle, Victory }
|
||||
var stage: int = Stage.Explore
|
||||
var enemies_left: int = 2
|
||||
|
||||
handler RunStage phase Update {
|
||||
machine stage {
|
||||
state Explore {
|
||||
if Input.key() == ' ' { become Battle }
|
||||
}
|
||||
state Battle {
|
||||
if enemies_left <= 0 { become Victory }
|
||||
}
|
||||
state Victory {
|
||||
Screen.status("you win")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,12 +5,28 @@ category: control
|
|||
kind: keyword
|
||||
tokens: for
|
||||
sig: for name in a .. b { … }
|
||||
tip: Range loop.
|
||||
tip: Range loop — iterate the half-open range from a up to but not including b.
|
||||
order: 2
|
||||
---
|
||||
|
||||
Range loop. <b>Each pass binds a fresh, immutable <code>name</code></b> — it is not a variable you reuse or reassign; the range <code>a .. b</code> runs from <code>a</code> up to but not including <code>b</code>.
|
||||
The numeric <code>for … in</code> loop walks the half-open range `a .. b`, running the body for each value from `a` up to <b>but not including</b> `b`. <b>Each pass binds a fresh, immutable <code>name</code></b> — it is not a reusable variable and cannot be reassigned inside the body — which makes the loop easy to reason about. Both bounds are ordinary expressions, so ranges built from constants like `0 .. GRID_WIDTH` read cleanly. The same `for … in` keyword also drives ECS query loops (`for (…) in query […]`); this page is the numeric range form. Use `break` and `continue` to exit early or skip to the next value.
|
||||
|
||||
```ludic
|
||||
for gy in 0 .. GRID_H { … }
|
||||
program Grid {
|
||||
const GRID_WIDTH: int = 20
|
||||
const GRID_HEIGHT: int = 15
|
||||
const TILE_SIZE: int = 16
|
||||
|
||||
handler DrawWorld phase Render {
|
||||
Screen.clear(Color.MidnightBlue)
|
||||
for row in 0 .. GRID_HEIGHT {
|
||||
for column in 0 .. GRID_WIDTH {
|
||||
var tile_color = Color.MidnightBlue
|
||||
if (column + row) % 2 == 0 { tile_color = Color.White }
|
||||
Screen.fill_rectangle(x: column * TILE_SIZE, y: row * TILE_SIZE, width: TILE_SIZE, height: TILE_SIZE, color: tile_color)
|
||||
}
|
||||
}
|
||||
Screen.show()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,8 +5,35 @@ category: control
|
|||
kind: keyword
|
||||
tokens: if else
|
||||
sig: if cond { … } else { … }
|
||||
tip: A branch.
|
||||
tip: A branch — run one block when a condition holds, another when it doesn't.
|
||||
order: 0
|
||||
---
|
||||
|
||||
A branch. Conditions are plain expressions; no parentheses required.
|
||||
An <code>if</code> runs its block when the condition is true; an optional `else` block runs when it is false, and the `else` may itself be another `if` to form a ladder. The condition is a plain boolean expression with <b>no surrounding parentheses</b>, and the braces are always required even for a single statement. Ludic's boolean operators are the words `and`, `or` and `not` (not `&&`/`||`/`!`), and note that bitwise operators bind tighter than comparison, so `flags & MASK == 0` already means `(flags & MASK) == 0`. When the branches grow into a chain testing one value against several constants, reach for `match` instead.
|
||||
|
||||
```ludic
|
||||
program Threshold {
|
||||
property Health { current: int = 100, maximum: int = 100 }
|
||||
model Player { Health }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Health { current: 25 } }
|
||||
}
|
||||
|
||||
handler DrawWorld phase Render {
|
||||
Screen.clear(Color.MidnightBlue)
|
||||
for (health) in query [Health, {Player}] {
|
||||
if health.current <= 0 {
|
||||
Screen.draw_text(x: 8, y: 8, text: "DEFEATED", color: Color.Crimson, scale: 2)
|
||||
} else {
|
||||
if health.current < 30 {
|
||||
Screen.draw_text(x: 8, y: 8, text: "DANGER", color: Color.Gold, scale: 2)
|
||||
} else {
|
||||
Screen.draw_text(x: 8, y: 8, text: "OK", color: Color.LimeGreen, scale: 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
Screen.show()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,8 +5,28 @@ category: control
|
|||
kind: keyword
|
||||
tokens: in
|
||||
sig: for x in range | query
|
||||
tip: Binds the loop name to each value of a range or query.
|
||||
tip: The part of a for loop that names what to iterate — a range or a query.
|
||||
order: 3
|
||||
---
|
||||
|
||||
Binds the loop name to each value of a range or query.
|
||||
The <code>in</code> keyword is the bridge in a `for` loop between the loop's bindings and the source it walks. On the right of `in` you write either a numeric range, `a .. b`, binding one fresh index each pass, or an ECS `query [...]`, binding one variable per non-tag property for each matching model instance. It always pairs with `for` and never stands alone. In the query form the parentheses group the bound properties — `for (position, velocity) in query [Position, Velocity]` — and the body then runs once per instance that carries them all.
|
||||
|
||||
```ludic
|
||||
program Movement {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
property Velocity { delta_x: int = 0, delta_y: int = 0 }
|
||||
model Enemy { Position, Velocity }
|
||||
|
||||
handler Boot phase Start {
|
||||
for slot in 0 .. 3 {
|
||||
spawn Grunt { Position { column: slot * 4, row: 2 }; Velocity { delta_x: 1 } }
|
||||
}
|
||||
}
|
||||
|
||||
handler AdvancePositions phase Update {
|
||||
for (position, velocity) in query [Position, Velocity, {Enemy}] {
|
||||
position.column = position.column + velocity.delta_x
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,12 +5,35 @@ category: control
|
|||
kind: keyword
|
||||
tokens: machine
|
||||
sig: machine store { state Name { … } }
|
||||
tip: A state machine over an int var (or register).
|
||||
tip: An explicit state machine over an int var — dispatches on the store's value.
|
||||
order: 6
|
||||
---
|
||||
|
||||
A state machine over an int <code>var</code> (or register). It dispatches on the store's value.
|
||||
A <code>machine</code> turns an integer store into an explicit state machine, replacing brittle `if phase == N` chains. It reads the store — a named program-scope `var` is the modern choice — and dispatches to the matching `state` block; inside a state, `become Name` transitions to another state of the same machine. States number themselves by declaration order (the first is `0`, the next `1`, and so on), so you never write magic constants, though `state Name = expr` is accepted when a state needs a specific value. Because `become` compiles to a single store back into the `var`, the whole machine lowers to plain branches with no dispatch table. Place a `machine` inside a handler so it runs each frame.
|
||||
|
||||
```ludic
|
||||
machine turn_phase { state KnightMenu { … } }
|
||||
program TurnOrder {
|
||||
enum Phase { KnightMenu, KnightResolve, EnemyTurn }
|
||||
var battle_phase: int = Phase.KnightMenu
|
||||
|
||||
handler RunTurn phase Update {
|
||||
machine battle_phase {
|
||||
state KnightMenu {
|
||||
if Input.key() == ' ' { become KnightResolve }
|
||||
}
|
||||
state KnightResolve {
|
||||
become EnemyTurn
|
||||
}
|
||||
state EnemyTurn {
|
||||
become KnightMenu
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handler DrawWorld phase Render {
|
||||
Screen.clear(Color.MidnightBlue)
|
||||
Screen.draw_number(x: 8, y: 8, value: battle_phase, color: Color.Gold, scale: 2)
|
||||
Screen.show()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,12 +1,36 @@
|
|||
---
|
||||
id: kw-match
|
||||
name: match / when
|
||||
name: match
|
||||
category: control
|
||||
kind: keyword
|
||||
tokens: match
|
||||
sig: match x { when a, b => … }
|
||||
tip: Multi-way branch on a value, matching one or more literals per arm.
|
||||
sig: match value { 0, 1 => … _ => … }
|
||||
tip: Multi-way branch on one value, matching one or more literals per arm.
|
||||
order: 4
|
||||
---
|
||||
|
||||
Multi-way branch on a value, matching one or more literals per arm.
|
||||
A <code>match</code> replaces an `if`/`else` ladder that tests one value against several constants. It evaluates the subject once, then takes the first arm whose pattern matches; an arm lists one or more literal patterns separated by commas and points at a body with `=>`, and a lone `_` arm is the catch-all default. Patterns are compile-time constants — integers, char literals like `'w'`, or `enum` variants such as `Action.Guard` — which makes `match` ideal for dispatching on a key press, a tile code, or a mode. Each arm's body is a single statement or a `{ … }` block; matching lowers to plain branches, so it is as cheap as the `if` chain it replaces.
|
||||
|
||||
```ludic
|
||||
program Steering {
|
||||
property Velocity { delta_x: int = 0, delta_y: int = 0 }
|
||||
model Player { Velocity }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Velocity { delta_x: 0, delta_y: 0 } }
|
||||
}
|
||||
|
||||
handler ReadKeys phase Input {
|
||||
let pressed = Input.key()
|
||||
for (velocity) in query [Velocity, {Player}] {
|
||||
match pressed {
|
||||
'w' => velocity.delta_y = -1
|
||||
's' => velocity.delta_y = 1
|
||||
'a', 'h' => velocity.delta_x = -1
|
||||
'd', 'l' => velocity.delta_x = 1
|
||||
_ => velocity.delta_x = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,8 +5,31 @@ category: control
|
|||
kind: keyword
|
||||
tokens: state
|
||||
sig: state Name { … }
|
||||
tip: One state of a machine.
|
||||
tip: One state of a machine — its body runs while the machine sits in it.
|
||||
order: 7
|
||||
---
|
||||
|
||||
One state of a <code>machine</code>.
|
||||
A <code>state</code> declares one state of an enclosing <code>machine</code>: a named block whose body runs while the machine's store holds that state's value. States take their value from declaration order — the first `state` is `0`, the next `1`, and so on — so you refer to them by name and never track the numbers yourself (write `state Name = expr` only when a state must have a specific value). From inside a state, `become OtherName` transitions the machine by storing the target state's value back into the store. Keep each state focused on the logic for that mode and hand off with `become` when its condition to move on is met.
|
||||
|
||||
```ludic
|
||||
program DoorControl {
|
||||
enum DoorState { Closed, Opening, Open }
|
||||
var door: int = DoorState.Closed
|
||||
var elapsed_frames: int = 0
|
||||
|
||||
handler RunDoor phase Update {
|
||||
machine door {
|
||||
state Closed {
|
||||
if Input.key() == ' ' { elapsed_frames = 0; become Opening }
|
||||
}
|
||||
state Opening {
|
||||
elapsed_frames = elapsed_frames + 1
|
||||
if elapsed_frames >= 30 { become Open }
|
||||
}
|
||||
state Open {
|
||||
Screen.status("door open")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
---
|
||||
id: kw-when
|
||||
name: when
|
||||
category: control
|
||||
kind: keyword
|
||||
tokens: when
|
||||
sig: when value => result
|
||||
tip: One arm of a match.
|
||||
order: 5
|
||||
---
|
||||
|
||||
One arm of a <code>match</code>.
|
||||
|
|
@ -5,8 +5,29 @@ category: control
|
|||
kind: keyword
|
||||
tokens: while
|
||||
sig: while cond { … }
|
||||
tip: Loop while the condition holds.
|
||||
tip: Loop as long as a condition holds, re-checking it before each pass.
|
||||
order: 1
|
||||
---
|
||||
|
||||
Loop while the condition holds.
|
||||
A <code>while</code> loop re-evaluates its condition before every pass and runs the body as long as it stays true, so it is the tool when the number of iterations is not known up front. As with `if`, the condition needs no parentheses and the braces are required. `break` leaves the loop immediately and `continue` jumps to the next condition check. When you are simply counting over a fixed range, prefer the numeric `for i in a .. b` loop, which is clearer and binds a fresh index for you; reach for `while` when the step or the stopping test is irregular.
|
||||
|
||||
```ludic
|
||||
program Countdown {
|
||||
var fuse: int = 5
|
||||
var elapsed_frames: int = 0
|
||||
|
||||
handler Tick phase Update {
|
||||
elapsed_frames = elapsed_frames + 1
|
||||
while fuse > 0 and elapsed_frames % 60 == 0 {
|
||||
fuse = fuse - 1
|
||||
elapsed_frames = elapsed_frames + 1
|
||||
}
|
||||
}
|
||||
|
||||
handler DrawWorld phase Render {
|
||||
Screen.clear(Color.MidnightBlue)
|
||||
Screen.draw_number(x: 8, y: 8, value: fuse, color: Color.Crimson, scale: 3)
|
||||
Screen.show()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue