docs(0.S): the doc fences migrated by ludic migrate state; LANGUAGE.md's state section says what the tool and the checker do now; no module-level var left in the docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 15:37:29 +03:00
parent 19fcf60599
commit a476ec7976
79 changed files with 481 additions and 355 deletions

View file

@ -85,11 +85,11 @@ module bank
import "ledger.ludic"
# bank/ledger.ludic
var balance: int = 0 # private: only module bank sees it
export state Bank { balance: int = 0 } # its fields are bank's to change: only bank's functions do
export event Deposited { amount: int }
function add(n: int) -> void { balance += n }
export function deposit(n: int) -> void {
add(n)
function add(b: mut Bank, n: int) -> void { b.balance += n } # private: only module bank sees it
export function deposit(b: mut Bank, n: int) -> void {
add(b, n)
emit Deposited(amount: n)
}
```
@ -240,19 +240,25 @@ function hips_of(h: Hiker) -> int { return h.hips }
- **Read-only is checked where it is written.** Through a read-only state the compiler refuses an
assignment whose target starts at it (`h.hips = 1`, `h.list[i] = x`), a `push` onto something in
it, and passing it where a `mut` one is wanted (`bump changes Tally (c: mut Tally), and c is
read-only here`). It does not follow a reference read out of it into a local and changed there.
read-only here`). A reference read out of it into a local (`let l = h.list`, `let r = h.rows[0]`)
is read-only too, so a write through that is refused the same way; a value read out (`let n =
h.count`) is a copy and the local's own. `machine h.mode` writes its store on every `become`.
`mut` is for a state parameter only.
- **The runtime supplies it at the entry points** - the only code nothing in the program calls:
- a body that declares it: `entry (h: mut Hiker) { ... }`, `handler Draw(h: Hiker) phase Render
{ ... }`, `@On(Ping) handler Heard(h: mut Hiker) { ... }`, `test "name" (h: mut Hiker) { ... }`;
{ ... }`, `@On(Ping) handler Heard(h: mut Hiker) { ... }`, `@OnSpawn(M) handler Made(h: mut
Hiker) { ... }`, a scene's `on enter (h: mut Hiker) { ... }`, `test "name" (h: mut Hiker) { ... }`;
- a retained `ui` block, which names a state's instance by the state's name: `font: Menu.title_font`;
- a function value: `fn tick` of `function tick(h: mut Hiker, t: Tick)` is `tick` with its
leading states supplied, a `fn(Tick) -> void` - so a system's functions, a port's bind and any
callback a package calls are entry points without saying so;
- a port member bound to a state's field, `bind Purse { money: Wallet.cash }`;
- a call the compiler writes: a namespace method's target, a runtime built-in.
- a call the compiler writes: a namespace method's target (`Weapon.def(...)` of `function
weapon_def(w: mut Weapons, ...)`), an engine system, a runtime built-in.
Every other call passes its states explicitly.
- **Everything else module-level is immutable all the way down.** `let LIMITS: []int = [1, 2]`,
a `const`, a registry: an assignment or a `push` that starts at one is refused.
a `const`, a registry: an assignment or a `push` that starts at one is refused, and so is one
through a local that holds part of it (`let r = LIMITS; push(r, 3)`).
- **Tests get fresh states.** Each test block starts from states made new, in its own process under
`ludic test` and in the runner run directly.
- The toolchain's own programs (the compiler, the CLI) are not part of this yet: they build with
@ -267,19 +273,34 @@ counter.ludic:5: error: this assignment: LIMITS is module-level and immutable al
counter.ludic:3: error: x: mut int - mut is for a state parameter, and int is not a state
```
**`ludic migrate state [file]` moves a program there.** It compiles the program and, from the
compiler's own view of every name:
**`ludic migrate state [file|dir...] [--runtime] [--dry-run]` moves programs there.** It compiles
each program and, from the compiler's own view of every name:
1. each module's vars become one state, `state <Module>State { ... }`, where the first of them was;
2. every reference to one is rewritten to `<module>_st.<name>`;
3. each function's states - those it touches, and those of everything it calls, to a fixed point -
become its leading parameters, `mut` where it or something it calls writes;
4. each call passes them on, and each entry point declares them.
1. a var nothing writes, holding a value (an int, a string, an enum...), becomes a module-level
`let` where it stands;
2. each module's other vars become one state, `state <Module>State { ... }`, where the first of
them was - a module's by its name (`module fishing`: `FishingState`), a directory with no module
line by its path (`ludic.render3d`: `Render3dState`), the program's own file by the program's
name (`program SceneDemo`: `SceneDemoState`); the fields keep their comments;
3. every reference to one is rewritten to `<snake>_st.<name>` (`scene_demo_st.counter`), and in a
`ui` block to `<State>.<name>`;
4. each function's states - those it touches, and those of everything it calls, to a fixed point -
become its leading parameters, `mut` where it or something it calls writes; a state a run before
declared read-only becomes `mut` where it is now changed;
5. each call passes them on, and each entry point declares them.
It prints what it cannot decide (a var read in another global's initializer, a reference in
generated code), for a person to finish. Run it once per program that uses what changes - each of a
package's test programs, a game's entry and its lab: a later run finds the states an earlier one
made and passes them on. `--runtime` moves the runtime's own vars too.
Give it every program at once - a directory stands for the test programs under it - and it merges
their plans before it edits anything: programs that share a package agree about it, a module two of
them see different files of is one state, and a path reached as `../../packages/x` is the same
file as `packages/x`. It prints what it cannot decide (a var read in another global's initializer, a
reference in generated code), for a person to finish. A later run finds the states an earlier one
made and adds to them. `--runtime` moves the runtime's own vars too. gpp's packages and examples
were moved with one command:
```
ludic migrate state packages <every example program> packages/ludic.lab/example/plate.ludic
migrate: 1804 vars into 126 states, 64 into lets; 23498 edits in 460 files
```
### Types are checked before anything is emitted
@ -943,13 +964,13 @@ panels with padding / gap / alignment / grow), drawing (9-slice skins, images,
TrueType text, focus highlight) and keyboard focus + activation.
```ludic
var title_font: int = 0
state Menu { title_font: int = 0 }
ui MainMenu {
panel id: Root w: 288 pad: 16 gap: 6 skin: "assets/ui/panel.png" inset: 10 align: center {
label text: "CHRONO RIFT" font: title_font size: 26 fg: Color.Gold align: center
button id: NewGame text: "New Game" font: title_font size: 16 w: 236
button id: Quit text: "Quit" font: title_font size: 16 w: 236
label text: "CHRONO RIFT" font: Menu.title_font size: 26 fg: Color.Gold align: center
button id: NewGame text: "New Game" font: Menu.title_font size: 16 w: 236
button id: Quit text: "Quit" font: Menu.title_font size: 16 w: 236
}
}
```
@ -958,13 +979,14 @@ A widget inherits `font`, `size`, `fg` and `align` from the nearest ancestor tha
sets them, so a panel states a menu's look once and a label only says what differs.
Widget types: `panel` (container + optional skin/bg/border), `col` / `row`
(pure stacks), `label`, `button` (focusable), `image`, `spacer`. Props are
evaluated at build time, so `font: title_font` reads a value the program set first.
evaluated at build time, so `font: Menu.title_font` reads a value the program set first - a `ui`
block is an entry point, and names a state's one instance by the state's name.
Each `id: Name` mints a `UI_Name` handle (the `ui` block name too), used from
handlers:
```ludic
handler Boot phase Start {
title_font = Font.load("…Arial.ttf")
handler Boot(menu: mut Menu) phase Start {
menu.title_font = Font.load("…Arial.ttf")
Ui.build() # construct the tree (loads skins/images)
Ui.open(UI_MainMenu) # make it active, focus the first button
}
@ -1098,7 +1120,7 @@ any record's:
```ludic
# doc-check: skip — composite: declarations plus statements using them
property Hero { iframes: int = 0, roll_cooldown: int = 0 }
var player: int = -1
let player = World.query_next(World.prop_id("Hero"), 0)
Hero.of(player).iframes = 20 # assign a field
Hero.of(player).roll_cooldown -= 1 # compound-assign one
@ -1584,11 +1606,11 @@ snippet below does not compile today. Programs use `[]T` slices for now.
```ludic
# doc-check: skip — [T; N] fixed arrays are not yet implemented (design target)
var table: [int; 8] # module-level storage
handler S phase Update {
state Grid { table: [int; 8] } # a state's storage
handler S(g: mut Grid) phase Update {
let buf: [int; 4] # a local; no initializer needed
buf[0] = 10
table[2] = buf[0]
g.table[2] = buf[0]
}
```
@ -1678,19 +1700,24 @@ at the top level it is module state).
loop accumulators and anything that genuinely changes.
- **`const NAME = e`** — a compile-time constant (folded, no storage).
A program-scope `var` may be initialized with **any expression** — a literal, an
`Enum.Variant`, a `new Record`, a call. What the compiler can fold becomes the
global's initial value; the rest runs once at startup, in declaration order,
after the runtime boots and before the `Start` phase:
`var` is for locals. At module level there is no `var` (it is refused): what changes belongs to a
`state` (see "State" below), and what does not is a module-level `let`, immutable all the way
down. A state's field, or a module-level `let`, may be initialized with **any expression** — a
literal, an `Enum.Variant`, a `new Record`, a call. What the compiler can fold becomes the initial
value; the rest runs once at startup, in declaration order, after the runtime boots and before the
`Start` phase:
```ludic
# doc-check: skip — illustrative globals
var run: Progress = new Progress # allocated before Start
var origin: IVec2 = IVec2.zero()
var mode: HeroState = HeroState.Idle # folded
# doc-check: skip — illustrative
state Hero {
run: Progress = new Progress # allocated before Start
origin: IVec2 = IVec2.zero()
mode: HeroState = HeroState.Idle # folded
}
let ORIGIN_NAME: string = "camp" # a module-level let: a value nothing changes
```
Declaring the same `var` twice is an error — including a name the spliced engine
Declaring the same name twice is an error — including a name the spliced engine
runtime already uses, which the message says (`variable ui_font is also a
variable of the engine runtime; choose another name`).
@ -1767,19 +1794,20 @@ no `= value` on any state:
```ludic
# doc-check: skip — composite: declarations plus a machine over them
enum HeroState { Idle, Rolling, Swinging }
var hero_state: HeroState = HeroState.Idle
state Hero { mode: HeroState = HeroState.Idle }
machine hero_state {
machine hero.mode { # (hero: mut Hero) - become writes it
state Idle { if wants_roll { become Rolling } } # HeroState.Idle
state Rolling { if done { become Idle } } # HeroState.Rolling
state Swinging { … }
}
if hero_state == HeroState.Rolling { … } # readable from anywhere
if hero.mode == HeroState.Rolling { … } # readable wherever Hero is
```
A state that names no variant of the store's enum is a compile error. A bare
(payload-free) enum is an `int`-sized type wherever a type is written — a `var`,
a parameter, a field, a return.
A machine's store is a state's field (`machine hero.mode`), a local, or a register index; a
`become` writes it, so the function needs its state `mut`. A state that names no variant of the
store's enum is a compile error. A bare (payload-free) enum is an `int`-sized type wherever a type
is written — a `var`, a parameter, a field, a return.
## Enums

View file

@ -13,16 +13,18 @@ order: 3
```ludic
program ScoreModule {
var running_total: int = 0
@export function add_points(amount: int) -> int { # callable from a native host
running_total = running_total + amount
return running_total
state ScoreModuleState {
running_total: int = 0
}
entry {
print(add_points(10)) # 10
print(add_points(5)) # 15
@export function add_points(score_module_st: mut ScoreModuleState, amount: int) -> int { # callable from a native host
score_module_st.running_total = score_module_st.running_total + amount
return score_module_st.running_total
}
entry (score_module_st: mut ScoreModuleState) {
print(add_points(score_module_st, 10)) # 10
print(add_points(score_module_st, 5)) # 15
}
}
```

View file

@ -16,13 +16,15 @@ program BootHook {
property Health { current: int = 0, maximum: int = 100 }
model Player { Health }
var elapsed_frames: int = 0
@OnStart handler CreateWorld { # runs once at boot
spawn Player { Health { current: 100, maximum: 100 } }
elapsed_frames = 0
state BootHookState {
elapsed_frames: int = 0
}
handler CountFrames phase Update { elapsed_frames = elapsed_frames + 1 }
@OnStart handler CreateWorld(boot_hook_st: mut BootHookState) { # runs once at boot
spawn Player { Health { current: 100, maximum: 100 } }
boot_hook_st.elapsed_frames = 0
}
handler CountFrames(boot_hook_st: mut BootHookState) phase Update { boot_hook_st.elapsed_frames = boot_hook_st.elapsed_frames + 1 }
}
```

View file

@ -17,15 +17,15 @@ Keep the frame loop going while hidden so the child can be polled, but there is
```ludic skip
program Launcher {
var game: int = -1
function play() -> void {
game = Process.spawn(game_path(), new []string)
if game >= 0 { App.window_hide() }
state Child { game: int = -1 }
function play(c: mut Child) -> void {
c.game = Process.spawn(game_path(), new []string)
if c.game >= 0 { App.window_hide() }
}
handler Watch phase Update {
if game >= 0 and Process.poll(game) != -1 {
Process.free(game)
game = -1
handler Watch(c: mut Child) phase Update {
if c.game >= 0 and Process.poll(c.game) != -1 {
Process.free(c.game)
c.game = -1
App.window_show()
}
}

View file

@ -17,18 +17,20 @@ Parameters:
```ludic
program HighScore {
var score: int = 0
var best_score: int = 0
handler EarnPoints phase Update {
score = score + 5
best_score = max(best_score, score)
if score >= 20 { quit() }
state HighScoreState {
score: int = 0
best_score: int = 0
}
handler ReportBest phase Render {
handler EarnPoints(high_score_st: mut HighScoreState) phase Update {
high_score_st.score = high_score_st.score + 5
high_score_st.best_score = max(high_score_st.best_score, high_score_st.score)
if high_score_st.score >= 20 { quit() }
}
handler ReportBest(high_score_st: HighScoreState) phase Render {
Screen.clear(Color.MidnightBlue)
Screen.draw_number(x: 8, y: 8, value: best_score, color: Color.Gold, scale: 1)
Screen.draw_number(x: 8, y: 8, value: high_score_st.best_score, color: Color.Gold, scale: 1)
Screen.show()
}
}

View file

@ -16,12 +16,14 @@ Parameters:
```ludic
program PrintScore {
var score: int = 0
state PrintScoreState {
score: int = 0
}
handler CountUp phase Update {
score = score + 10
print(score)
if score >= 30 { quit() }
handler CountUp(print_score_st: mut PrintScoreState) phase Update {
print_score_st.score = print_score_st.score + 10
print(print_score_st.score)
if print_score_st.score >= 30 { quit() }
}
}
```

View file

@ -16,7 +16,7 @@ Parameters:
```ludic
program LabelValue {
var score: int = 250
let score: int = 250
handler ReportScore phase Start {
let label = string(score)

View file

@ -13,18 +13,20 @@ Constructs the retained-UI tree you declared in a <code>ui</code> block, laying
```ludic
program TitleMenu {
var title_font: int = 0
state TitleMenuState {
title_font: int = 0
}
ui MainMenu {
panel id: Root w: 240 pad: 16 gap: 6 bg: 0x1a1a2c align: center {
label text: "CHRONORIFT" font: title_font size: 24 fg: 0xffe060 align: center
button id: NewGame text: "New Game" font: title_font size: 16 w: 200
button id: Quit text: "Quit" font: title_font size: 16 w: 200
label text: "CHRONORIFT" font: TitleMenuState.title_font size: 24 fg: 0xffe060 align: center
button id: NewGame text: "New Game" font: TitleMenuState.title_font size: 16 w: 200
button id: Quit text: "Quit" font: TitleMenuState.title_font size: 16 w: 200
}
}
handler Boot phase Start {
title_font = Font.load("/System/Library/Fonts/Supplemental/Arial.ttf")
handler Boot(title_menu_st: mut TitleMenuState) phase Start {
title_menu_st.title_font = Font.load("/System/Library/Fonts/Supplemental/Arial.ttf")
ui_build()
Ui.open(UI_MainMenu)
}

View file

@ -14,11 +14,13 @@ A <code>become</code> statement performs a transition. Inside a <code>machine</c
```ludic
program EncounterFlow {
enum Stage { Explore, Battle, Victory }
var stage: int = Stage.Explore
var enemies_left: int = 2
state EncounterFlowState {
stage: int = Stage.Explore
}
let enemies_left: int = 2
handler RunStage phase Update {
machine stage {
handler RunStage(encounter_flow_st: mut EncounterFlowState) phase Update {
machine encounter_flow_st.stage {
state Explore {
if Input.key() == ' ' { become Battle }
}

View file

@ -13,12 +13,14 @@ order: 9
```ludic
program FindFirst {
var found: int = 0 - 1
state FindFirstState {
found: int = 0 - 1
}
handler Scan phase Update {
handler Scan(find_first_st: mut FindFirstState) phase Update {
for i in 0 .. 10 {
if i * i > 20 {
found = i
find_first_st.found = i
break # stop at the first i whose square exceeds 20
}
}

View file

@ -13,12 +13,14 @@ order: 10
```ludic
program SumOdds {
var total: int = 0
state SumOddsState {
total: int = 0
}
handler Add phase Update {
handler Add(sum_odds_st: mut SumOddsState) phase Update {
for i in 0 .. 10 {
if i % 2 == 0 { continue } # skip even numbers
total = total + i
sum_odds_st.total = sum_odds_st.total + i
}
}
}

View file

@ -14,10 +14,12 @@ A <code>machine</code> turns an integer store into an explicit state machine, re
```ludic
program TurnOrder {
enum Phase { KnightMenu, KnightResolve, EnemyTurn }
var battle_phase: int = Phase.KnightMenu
state TurnOrderState {
battle_phase: int = Phase.KnightMenu
}
handler RunTurn phase Update {
machine battle_phase {
handler RunTurn(turn_order_st: mut TurnOrderState) phase Update {
machine turn_order_st.battle_phase {
state KnightMenu {
if Input.key() == ' ' { become KnightResolve }
}
@ -30,9 +32,9 @@ program TurnOrder {
}
}
handler DrawWorld phase Render {
handler DrawWorld(turn_order_st: TurnOrderState) phase Render {
Screen.clear(Color.MidnightBlue)
Screen.draw_number(x: 8, y: 8, value: battle_phase, color: Color.Gold, scale: 2)
Screen.draw_number(x: 8, y: 8, value: turn_order_st.battle_phase, color: Color.Gold, scale: 2)
Screen.show()
}
}

View file

@ -14,17 +14,19 @@ A <code>state</code> declares one state of an enclosing <code>machine</code>: a
```ludic
program DoorControl {
enum DoorState { Closed, Opening, Open }
var door: int = DoorState.Closed
var elapsed_frames: int = 0
state DoorControlState {
door: int = DoorState.Closed
elapsed_frames: int = 0
}
handler RunDoor phase Update {
machine door {
handler RunDoor(door_control_st: mut DoorControlState) phase Update {
machine door_control_st.door {
state Closed {
if Input.key() == ' ' { elapsed_frames = 0; become Opening }
if Input.key() == ' ' { door_control_st.elapsed_frames = 0; become Opening }
}
state Opening {
elapsed_frames = elapsed_frames + 1
if elapsed_frames >= 30 { become Open }
door_control_st.elapsed_frames = door_control_st.elapsed_frames + 1
if door_control_st.elapsed_frames >= 30 { become Open }
}
state Open {
Screen.status("door open")

View file

@ -13,20 +13,22 @@ A <code>while</code> loop re-evaluates its condition before every pass and runs
```ludic
program Countdown {
var fuse: int = 5
var elapsed_frames: int = 0
state CountdownState {
fuse: int = 5
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 Tick(countdown_st: mut CountdownState) phase Update {
countdown_st.elapsed_frames = countdown_st.elapsed_frames + 1
while countdown_st.fuse > 0 and countdown_st.elapsed_frames % 60 == 0 {
countdown_st.fuse = countdown_st.fuse - 1
countdown_st.elapsed_frames = countdown_st.elapsed_frames + 1
}
}
handler DrawWorld phase Render {
handler DrawWorld(countdown_st: CountdownState) phase Render {
Screen.clear(Color.MidnightBlue)
Screen.draw_number(x: 8, y: 8, value: fuse, color: Color.Crimson, scale: 3)
Screen.draw_number(x: 8, y: 8, value: countdown_st.fuse, color: Color.Crimson, scale: 3)
Screen.show()
}
}

View file

@ -20,16 +20,18 @@ program SnapshotRoundTrip {
property Position { column: int = 0, row: int = 0 }
model Enemy { Position }
var snapshot_length: int = 0
state SnapshotRoundTripState {
snapshot_length: int = 0
}
handler Boot phase Start {
spawn Grunt { Position { column: 3, row: 3 } }
}
handler RoundTrip phase Update {
handler RoundTrip(snapshot_round_trip_st: mut SnapshotRoundTripState) phase Update {
let buffer = bytes(4096)
if Input.key() == 's' { snapshot_length = world_save(buffer) }
if Input.key() == 'l' { world_load(buffer, snapshot_length) }
if Input.key() == 's' { snapshot_round_trip_st.snapshot_length = world_save(buffer) }
if Input.key() == 'l' { world_load(buffer, snapshot_round_trip_st.snapshot_length) }
}
}
```

View file

@ -15,23 +15,25 @@ order: 2
program CancelExample {
event cancellable BeforeOpenDoor { key_count: int = 0 }
var keys: int = 0
var doors_opened: int = 0
state CancelExampleState {
keys: int = 0
doors_opened: int = 0
}
@On(BeforeOpenDoor) handler RequireKey { if key_count <= 0 { cancel } }
handler TryOpen phase Update {
handler TryOpen(cancel_example_st: mut CancelExampleState) phase Update {
if Input.key() == 'o' {
if emit BeforeOpenDoor(key_count: keys) == 0 {
doors_opened = doors_opened + 1
keys = keys - 1
if emit BeforeOpenDoor(key_count: cancel_example_st.keys) == 0 {
cancel_example_st.doors_opened = cancel_example_st.doors_opened + 1
cancel_example_st.keys = cancel_example_st.keys - 1
}
}
}
handler DrawWorld phase Render {
handler DrawWorld(cancel_example_st: CancelExampleState) phase Render {
Screen.clear(Color.MidnightBlue)
Screen.draw_number(x: 4, y: 4, value: doors_opened, color: Color.Gold, scale: 1)
Screen.draw_number(x: 4, y: 4, value: cancel_example_st.doors_opened, color: Color.Gold, scale: 1)
Screen.show()
}
}

View file

@ -15,19 +15,21 @@ order: 50
program CancellableExample {
event cancellable BeforeHurt { amount: int = 0 }
var current_health: int = 100
state CancellableExampleState {
current_health: int = 100
}
@On(BeforeHurt) handler AbsorbSmallHits { if amount <= 5 { cancel } }
handler TakeDamage phase Update {
handler TakeDamage(cancellable_example_st: mut CancellableExampleState) phase Update {
if Input.key() == 'h' {
if emit BeforeHurt(amount: 3) == 0 { current_health = current_health - 3 }
if emit BeforeHurt(amount: 3) == 0 { cancellable_example_st.current_health = cancellable_example_st.current_health - 3 }
}
}
handler DrawWorld phase Render {
handler DrawWorld(cancellable_example_st: CancellableExampleState) phase Render {
Screen.clear(Color.MidnightBlue)
Screen.draw_number(x: 4, y: 4, value: current_health, color: Color.Crimson, scale: 1)
Screen.draw_number(x: 4, y: 4, value: cancellable_example_st.current_health, color: Color.Crimson, scale: 1)
Screen.show()
}
}

View file

@ -15,19 +15,21 @@ order: 1
program EmitExample {
event cancellable BeforeSpend { amount: int = 0 }
var coins: int = 50
state EmitExampleState {
coins: int = 50
}
@On(BeforeSpend) handler RejectOverdraft { if amount > coins { cancel } }
@On(BeforeSpend) handler RejectOverdraft(emit_example_st: EmitExampleState) { if amount > emit_example_st.coins { cancel } }
handler TrySpend phase Update {
handler TrySpend(emit_example_st: mut EmitExampleState) phase Update {
if Input.key() == 'b' {
if emit BeforeSpend(amount: 20) == 0 { coins = coins - 20 }
if emit BeforeSpend(amount: 20) == 0 { emit_example_st.coins = emit_example_st.coins - 20 }
}
}
handler DrawWorld phase Render {
handler DrawWorld(emit_example_st: EmitExampleState) phase Render {
Screen.clear(Color.MidnightBlue)
Screen.draw_number(x: 4, y: 4, value: coins, color: Color.Gold, scale: 1)
Screen.draw_number(x: 4, y: 4, value: emit_example_st.coins, color: Color.Gold, scale: 1)
Screen.show()
}
}

View file

@ -15,18 +15,20 @@ order: 0
program EventExample {
event EnemyDefeated { points: int = 0 }
var score: int = 0
state EventExampleState {
score: int = 0
}
@On(EnemyDefeated) handler AddScore { score = score + points }
@On(EnemyDefeated) handler AddScore(event_example_st: mut EventExampleState) { event_example_st.score = event_example_st.score + points }
@On(EnemyDefeated) handler PlayChime { Screen.status("enemy down") }
handler ScoreOnKey phase Update {
if Input.key() == 'k' { emit EnemyDefeated(points: 10) }
}
handler DrawWorld phase Render {
handler DrawWorld(event_example_st: EventExampleState) phase Render {
Screen.clear(Color.MidnightBlue)
Screen.draw_number(x: 4, y: 4, value: score, color: Color.Gold, scale: 1)
Screen.draw_number(x: 4, y: 4, value: event_example_st.score, color: Color.Gold, scale: 1)
Screen.show()
}
}

View file

@ -15,9 +15,9 @@ Loads a TrueType font file and returns its handle. Load it before <code>Ui.build
```ludic
# doc-check: skip — illustrative
var menu_font: int = 0
handler Boot phase Start {
menu_font = Font.load(path: "assets/fonts/PixelOperator.ttf")
state Menu { font: int = 0 }
handler Boot(menu: mut Menu) phase Start {
menu.font = Font.load(path: "assets/fonts/PixelOperator.ttf")
Ui.build()
}
```

View file

@ -16,20 +16,22 @@ Using <code>Gl.*</code> links <code>gl.ll</code>, the thunks and <code>OpenGL.fr
program Triangle {
property Marker { on: int = 1 }
model Anchor { Marker }
var prog: int = 0
var vao: int = 0
handler Boot phase Start {
state TriangleState {
prog: int = 0
vao: int = 0
}
handler Boot(triangle_st: mut TriangleState) phase Start {
spawn Anchor {}
if not Gl.open(width: 640, height: 360, title: "GL") { quit() }
prog = Gl.program(vs: "#version 410 core\nvoid main(){ gl_Position = vec4(float(gl_VertexID == 1) * 2.0 - 0.5, float(gl_VertexID == 2) * 2.0 - 0.5, 0.0, 1.0); }\n",
triangle_st.prog = Gl.program(vs: "#version 410 core\nvoid main(){ gl_Position = vec4(float(gl_VertexID == 1) * 2.0 - 0.5, float(gl_VertexID == 2) * 2.0 - 0.5, 0.0, 1.0); }\n",
fs: "#version 410 core\nout vec4 o; void main(){ o = vec4(1.0, 0.5, 0.2, 1.0); }\n")
vao = Gl.vao()
triangle_st.vao = Gl.vao()
}
handler Draw phase Render {
handler Draw(triangle_st: TriangleState) phase Render {
Gl.clear_color(red: 0.1, green: 0.1, blue: 0.15, alpha: 1.0)
Gl.clear(mask: GL_COLOR_BUFFER_BIT)
Gl.use_program(prog: prog)
Gl.bind_vertex_array(array: vao)
Gl.use_program(prog: triangle_st.prog)
Gl.bind_vertex_array(array: triangle_st.vao)
Gl.draw_arrays(mode: GL_TRIANGLES, first: 0, count: 3)
Gl.swap()
}

View file

@ -15,7 +15,7 @@ Returns how many bytes of the response body have arrived. For a request given <a
```ludic
program Demo {
var dl: int = 0
let dl: int = 0
handler Bar phase Render {
let got = Http.received(handle: dl)
let all = Http.expected(handle: dl)

View file

@ -18,10 +18,12 @@ Parameters:
```ludic
program FrameHits {
var hits: []int = new []int
state FrameHitsState {
hits: []int = new []int
}
handler BeginFrame phase Update {
List.clear(hits) # start the frame with an empty list
handler BeginFrame(frame_hits_st: FrameHitsState) phase Update {
List.clear(frame_hits_st.hits) # start the frame with an empty list
}
}
```

View file

@ -19,12 +19,14 @@ Parameters:
```ludic
program Visited {
var seen: []int = new []int
state VisitedState {
seen: []int = new []int
}
handler Step phase Update {
handler Step(visited_st: VisitedState) phase Update {
let tile = 42
if not List.contains(seen, tile) {
List.push(seen, tile)
if not List.contains(visited_st.seen, tile) {
List.push(visited_st.seen, tile)
}
}
}

View file

@ -18,11 +18,13 @@ Parameters:
```ludic
program Queue {
var waiting: []int = new []int
state QueueState {
waiting: []int = new []int
}
handler Serve phase Update {
if List.len(waiting) > 0 {
let next = List.first(waiting)
handler Serve(queue_st: QueueState) phase Update {
if List.len(queue_st.waiting) > 0 {
let next = List.first(queue_st.waiting)
}
}
}

View file

@ -19,12 +19,14 @@ Parameters:
```ludic
program FindSlot {
var ids: []int = new []int
state FindSlotState {
ids: []int = new []int
}
handler Locate phase Update {
let slot = List.index_of(ids, 99)
handler Locate(find_slot_st: FindSlotState) phase Update {
let slot = List.index_of(find_slot_st.ids, 99)
if slot >= 0 {
List.swap(ids, slot, List.len(ids) - 1)
List.swap(find_slot_st.ids, slot, List.len(find_slot_st.ids) - 1)
}
}
}

View file

@ -18,11 +18,13 @@ Parameters:
```ludic
program Trail {
var points: []int = new []int
state TrailState {
points: []int = new []int
}
handler Head phase Update {
if List.len(points) > 0 {
let newest = List.last(points)
handler Head(trail_st: TrailState) phase Update {
if List.len(trail_st.points) > 0 {
let newest = List.last(trail_st.points)
}
}
}

View file

@ -18,10 +18,12 @@ Parameters:
```ludic
program CountEnemies {
var enemies: []int = new []int
state CountEnemiesState {
enemies: []int = new []int
}
handler Report phase Update {
let n = List.len(enemies)
handler Report(count_enemies_st: CountEnemiesState) phase Update {
let n = List.len(count_enemies_st.enemies)
}
}
```

View file

@ -18,11 +18,13 @@ Parameters:
```ludic
program UndoStack {
var history: []int = new []int
state UndoStackState {
history: []int = new []int
}
handler Undo phase Update {
if List.len(history) > 0 {
let last_action = List.pop(history)
handler Undo(undo_stack_st: UndoStackState) phase Update {
if List.len(undo_stack_st.history) > 0 {
let last_action = List.pop(undo_stack_st.history)
}
}
}

View file

@ -19,10 +19,12 @@ Parameters:
```ludic
program Collect {
var picked: []int = new []int
state CollectState {
picked: []int = new []int
}
handler Grab phase Update {
List.push(picked, 7)
handler Grab(collect_st: CollectState) phase Update {
List.push(collect_st.picked, 7)
}
}
```

View file

@ -18,11 +18,13 @@ Parameters:
```ludic
program PathOrder {
var path: []int = new []int
state PathOrderState {
path: []int = new []int
}
handler Finalize phase Update {
handler Finalize(path_order_st: PathOrderState) phase Update {
# path was built from the goal back to the start
List.reverse(path) # now start -> goal
List.reverse(path_order_st.path) # now start -> goal
}
}
```

View file

@ -20,13 +20,15 @@ Parameters:
```ludic
program RemoveFast {
var actors: []int = new []int
state RemoveFastState {
actors: []int = new []int
}
handler Kill phase Update {
handler Kill(remove_fast_st: RemoveFastState) 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
let last = List.len(remove_fast_st.actors) - 1
List.swap(remove_fast_st.actors, dead, last) # move it to the end...
let removed = List.pop(remove_fast_st.actors) # ...then drop it
}
}
```

View file

@ -18,8 +18,8 @@ Parameters:
```ludic
program ChaseDistance {
var target: int = 40
var here: int = 12
let target: int = 40
let here: int = 12
handler Report phase Update {
let gap = Math.abs(target - here) # 28, regardless of which is larger

View file

@ -20,7 +20,7 @@ Parameters:
program RowsNeeded {
const ROW_HEIGHT: fixed = 12.0
var content_height: fixed = 30.0
let content_height: fixed = 30.0
handler Layout phase Update {
let rows = Math.ceil(content_height / ROW_HEIGHT) # 3

View file

@ -22,10 +22,12 @@ Parameters:
program CameraBounds {
const WORLD_WIDTH: int = 320
var camera_x: int = 0
state CameraBoundsState {
camera_x: int = 0
}
handler Follow phase Update {
camera_x = Math.clamp(camera_x, 0, WORLD_WIDTH - 1)
handler Follow(camera_bounds_st: mut CameraBoundsState) phase Update {
camera_bounds_st.camera_x = Math.clamp(camera_bounds_st.camera_x, 0, WORLD_WIDTH - 1)
}
}
```

View file

@ -20,7 +20,7 @@ Parameters:
program TileIndex {
const TILE_SIZE: fixed = 16.0
var world_x: fixed = 40.5
let world_x: fixed = 40.5
handler Locate phase Update {
let column = Math.floor(world_x / TILE_SIZE) # 2

View file

@ -22,7 +22,7 @@ Parameters:
program HealthFraction {
const MAX_HP: fixed = 200.0
var hp: fixed = 150.0
let hp: fixed = 150.0
handler Draw phase Render {
let fraction = Math.inverse_lerp(0.0, MAX_HP, hp) # 0.75

View file

@ -20,11 +20,13 @@ Parameters:
```ludic
program EaseCamera {
var camera_x: fixed = 0.0
var target_x: fixed = 100.0
state EaseCameraState {
camera_x: fixed = 0.0
}
let 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
handler Follow(ease_camera_st: mut EaseCameraState) phase Update {
ease_camera_st.camera_x = Math.lerp(ease_camera_st.camera_x, target_x, 0.1) # glides a tenth of the way each frame
}
}
```

View file

@ -19,10 +19,12 @@ Parameters:
```ludic
program Countdown {
var timer: int = 60
state CountdownState {
timer: int = 60
}
handler Tick phase Update {
timer = Math.max(timer - 1, 0) # stops at 0, never negative
handler Tick(countdown_st: mut CountdownState) phase Update {
countdown_st.timer = Math.max(countdown_st.timer - 1, 0) # stops at 0, never negative
}
}
```

View file

@ -19,10 +19,12 @@ Parameters:
```ludic
program ClampScore {
var score: int = 0
state ClampScoreState {
score: int = 0
}
handler AddPoint phase Update {
score = Math.min(score + 1, 100) # never climbs past 100
handler AddPoint(clamp_score_st: mut ClampScoreState) phase Update {
clamp_score_st.score = Math.min(clamp_score_st.score + 1, 100) # never climbs past 100
}
}
```

View file

@ -25,7 +25,7 @@ program HealthBar {
const MAX_HP: fixed = 200.0
const BAR_WIDTH: fixed = 64.0
var hp: fixed = 150.0
let 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

@ -18,7 +18,7 @@ Parameters:
```ludic
program SnapToPixel {
var smooth_x: fixed = 10.5
let smooth_x: fixed = 10.5
handler Draw phase Render {
let px = Math.round(smooth_x) # 11

View file

@ -18,11 +18,13 @@ Parameters:
```ludic
program FaceTarget {
var x: int = 8
var target: int = 20
state FaceTargetState {
x: int = 8
}
let target: int = 20
handler StepToward phase Update {
x = x + Math.sign(target - x) # +1 while target is to the right
handler StepToward(face_target_st: mut FaceTargetState) phase Update {
face_target_st.x = face_target_st.x + Math.sign(target - face_target_st.x) # +1 while target is to the right
}
}
```

View file

@ -13,6 +13,6 @@ The bitwise operators work on the bits of an `int`: `&` (and), `|` (or), `^` (xo
```ludic
const FLAG_POISONED: int = 1
const FLAG_SHIELDED: int = 2
var status_flags: int = FLAG_POISONED | FLAG_SHIELDED
var is_shielded: bool = status_flags & FLAG_SHIELDED != 0
let status_flags: int = FLAG_POISONED | FLAG_SHIELDED
let is_shielded: bool = status_flags & FLAG_SHIELDED != 0
```

View file

@ -12,6 +12,6 @@ A comment begins with `#` and runs to the end of the line; the compiler ignores
```ludic
const TILE_SIZE: int = 16 # pixels per grid cell
var score: int = 0 # reset in the Start phase
let score: int = 0 # reset in the Start phase
# the head is drawn brighter than the body
```

View file

@ -15,18 +15,20 @@ order: 1
program InputExample {
const DIRECTION_LEFT: int = 0
const DIRECTION_RIGHT: int = 1
var facing: int = DIRECTION_RIGHT
handler ReadControls phase Input {
let pressed_key = Input.key()
if pressed_key == 'a' { facing = DIRECTION_LEFT }
if pressed_key == 'd' { facing = DIRECTION_RIGHT }
state InputExampleState {
facing: int = DIRECTION_RIGHT
}
handler DrawWorld phase Render {
handler ReadControls(input_example_st: mut InputExampleState) phase Input {
let pressed_key = Input.key()
if pressed_key == 'a' { input_example_st.facing = DIRECTION_LEFT }
if pressed_key == 'd' { input_example_st.facing = DIRECTION_RIGHT }
}
handler DrawWorld(input_example_st: InputExampleState) phase Render {
Screen.clear(Color.MidnightBlue)
var marker_x = 40
if facing == DIRECTION_RIGHT { marker_x = 200 }
if input_example_st.facing == DIRECTION_RIGHT { marker_x = 200 }
Screen.fill_rectangle(x: marker_x, y: 100, width: 16, height: 16, color: Color.LimeGreen)
Screen.show()
}

View file

@ -16,7 +16,9 @@ program FollowCamera {
property Position { column: int = 0, row: int = 0 }
model Player { Position }
var camera_column: int = 0
state FollowCameraState {
camera_column: int = 0
}
handler SpawnPlayer phase Start {
spawn Player { Position { column: 12, row: 0 } }
@ -29,8 +31,8 @@ program FollowCamera {
# runs after MovePlayer, so the camera sees the final position
@Queries(these: [Position])
handler TrackPlayer phase LateUpdate {
camera_column = Position.column
handler TrackPlayer(follow_camera_st: mut FollowCameraState) phase LateUpdate {
follow_camera_st.camera_column = Position.column
}
}
```

View file

@ -17,7 +17,7 @@ program OverlayExample {
model Player { Position }
const TILE_SIZE: int = 16
var score: int = 0
let score: int = 0
handler PlacePlayer phase Start {
spawn Player { Position { column: 5, row: 6 } }

View file

@ -17,7 +17,7 @@ program RenderExample {
model Player { Position }
const TILE_SIZE: int = 16
var score: int = 0
let score: int = 0
handler PlacePlayer phase Start {
spawn Player { Position { column: 5, row: 6 } }

View file

@ -17,18 +17,20 @@ program StartExample {
model Player { Position }
const GRID_WIDTH: int = 20
var score: int = 0
state StartExampleState {
score: int = 0
}
handler SetUpWorld phase Start {
handler SetUpWorld(start_example_st: mut StartExampleState) phase Start {
Random.seed(value: 1)
Map.size(width: GRID_WIDTH, height: 15)
score = 0
start_example_st.score = 0
spawn Player { Position { column: 10, row: 7 } }
}
handler DrawWorld phase Render {
handler DrawWorld(start_example_st: StartExampleState) phase Render {
Screen.clear(Color.MidnightBlue)
Screen.draw_number(x: 4, y: 4, value: score, color: Color.Gold, scale: 1)
Screen.draw_number(x: 4, y: 4, value: start_example_st.score, color: Color.Gold, scale: 1)
Screen.show()
}
}

View file

@ -17,24 +17,26 @@ program UpdateExample {
model Enemy { Position }
const GRID_WIDTH: int = 20
var elapsed_frames: int = 0
var score: int = 0
state UpdateExampleState {
elapsed_frames: int = 0
score: int = 0
}
handler SpawnWave phase Update {
elapsed_frames = elapsed_frames + 1
if elapsed_frames % 30 == 0 {
handler SpawnWave(update_example_st: mut UpdateExampleState) phase Update {
update_example_st.elapsed_frames = update_example_st.elapsed_frames + 1
if update_example_st.elapsed_frames % 30 == 0 {
let spawn_column = Random.range(low: 0, high: GRID_WIDTH - 1)
spawn Enemy { Position { column: spawn_column, row: 0 } }
score = score + 1
update_example_st.score = update_example_st.score + 1
}
}
handler DrawWorld phase Render {
handler DrawWorld(update_example_st: UpdateExampleState) phase Render {
Screen.clear(Color.MidnightBlue)
for (current_position) in query [Position, {Enemy}] {
Screen.fill_rectangle(x: current_position.column * 16, y: current_position.row * 16, width: 16, height: 16, color: Color.Crimson)
}
Screen.draw_number(x: 4, y: 4, value: score, color: Color.Gold, scale: 1)
Screen.draw_number(x: 4, y: 4, value: update_example_st.score, color: Color.Gold, scale: 1)
Screen.show()
}
}

View file

@ -17,14 +17,16 @@ A child ended by a signal (macOS) answers <code>128</code> plus the signal numbe
```ludic
program Demo {
var game: int = -1
handler Watch phase Update {
if game >= 0 {
let code = Process.poll(handle: game)
state DemoState {
game: int = -1
}
handler Watch(demo_st: mut DemoState) phase Update {
if demo_st.game >= 0 {
let code = Process.poll(handle: demo_st.game)
if code >= 0 or code < -1 {
if code != 0 { print("the game crashed") }
Process.free(handle: game)
game = -1
Process.free(handle: demo_st.game)
demo_st.game = -1
}
}
}

View file

@ -16,11 +16,13 @@ program EnterHookExample {
property Position { column: int = 0, row: int = 0 }
model Player { Position }
var score: int = 0
state EnterHookExampleState {
score: int = 0
}
scene Overworld start {
on enter {
score = 0
on enter (enter_hook_example_st: mut EnterHookExampleState) {
enter_hook_example_st.score = 0
spawn Player { Position { column: 5, row: 5 } }
}
layer World {

View file

@ -16,7 +16,7 @@ program LayerExample {
property Position { column: int = 0, row: int = 0 }
model Player { Position }
var score: int = 0
let score: int = 0
scene Overworld start {
on enter { spawn Player { Position { column: 6, row: 6 } } }

View file

@ -16,7 +16,9 @@ program SceneHookExample {
property Position { column: int = 0, row: int = 0 }
model Enemy { Position }
var elapsed_frames: int = 0
state SceneHookExampleState {
elapsed_frames: int = 0
}
scene Overworld start {
layer World {
@ -31,8 +33,8 @@ program SceneHookExample {
}
scene Battle {
on enter {
elapsed_frames = 0
on enter (scene_hook_example_st: mut SceneHookExampleState) {
scene_hook_example_st.elapsed_frames = 0
spawn Enemy { Position { column: 8, row: 4 } }
}
on exit {

View file

@ -13,13 +13,15 @@ A `scene` is one of the mutually-exclusive states a game moves between — a tit
```ludic
program SceneExample {
var elapsed_frames: int = 0
state SceneExampleState {
elapsed_frames: int = 0
}
scene Title start {
on enter { elapsed_frames = 0 }
on enter (scene_example_st: mut SceneExampleState) { scene_example_st.elapsed_frames = 0 }
layer Main {
handler WaitForStart phase Update {
elapsed_frames = elapsed_frames + 1
handler WaitForStart(scene_example_st: mut SceneExampleState) phase Update {
scene_example_st.elapsed_frames = scene_example_st.elapsed_frames + 1
if Input.key() != 0 { become Overworld }
}
handler DrawTitle phase Render {
@ -31,7 +33,7 @@ program SceneExample {
}
scene Overworld {
on enter { elapsed_frames = 0 }
on enter (scene_example_st: mut SceneExampleState) { scene_example_st.elapsed_frames = 0 }
layer World {
handler DrawWorld phase Render {
Screen.clear(Color.MidnightBlue)

View file

@ -18,15 +18,17 @@ Parameters:
```ludic
program ClearBackground {
var elapsed_frames: int = 0
handler CountFrames phase Update {
elapsed_frames = elapsed_frames + 1
state ClearBackgroundState {
elapsed_frames: int = 0
}
handler DrawWorld phase Render {
handler CountFrames(clear_background_st: mut ClearBackgroundState) phase Update {
clear_background_st.elapsed_frames = clear_background_st.elapsed_frames + 1
}
handler DrawWorld(clear_background_st: ClearBackgroundState) phase Render {
Screen.clear(Color.MidnightBlue)
Screen.draw_number(x: 8, y: 8, value: elapsed_frames, color: Color.White, scale: 1)
Screen.draw_number(x: 8, y: 8, value: clear_background_st.elapsed_frames, color: Color.White, scale: 1)
Screen.show()
}
}

View file

@ -22,16 +22,18 @@ Parameters:
```ludic
program ScoreHud {
var score: int = 0
handler EarnPoints phase Update {
score = score + 1
state ScoreHudState {
score: int = 0
}
handler DrawWorld phase Render {
handler EarnPoints(score_hud_st: mut ScoreHudState) phase Update {
score_hud_st.score = score_hud_st.score + 1
}
handler DrawWorld(score_hud_st: ScoreHudState) phase Render {
Screen.clear(Color.MidnightBlue)
Screen.draw_text(x: 6, y: 4, text: "SCORE", color: Color.White, scale: 1)
Screen.draw_number(x: 52, y: 4, value: score, color: Color.Gold, scale: 1)
Screen.draw_number(x: 52, y: 4, value: score_hud_st.score, color: Color.Gold, scale: 1)
Screen.show()
}
}

View file

@ -22,10 +22,12 @@ Parameters:
```ludic
program TitleScreen {
var elapsed_frames: int = 0
state TitleScreenState {
elapsed_frames: int = 0
}
handler CountFrames phase Update {
elapsed_frames = elapsed_frames + 1
handler CountFrames(title_screen_st: mut TitleScreenState) phase Update {
title_screen_st.elapsed_frames = title_screen_st.elapsed_frames + 1
}
handler DrawWorld phase Render {

View file

@ -18,11 +18,13 @@ Parameters:
```ludic
program StatusLine {
var elapsed_frames: int = 0
state StatusLineState {
elapsed_frames: int = 0
}
handler UpdateStatus phase Update {
elapsed_frames = elapsed_frames + 1
Screen.status(text: `frame {elapsed_frames}`)
handler UpdateStatus(status_line_st: mut StatusLineState) phase Update {
status_line_st.elapsed_frames = status_line_st.elapsed_frames + 1
Screen.status(text: `frame {status_line_st.elapsed_frames}`)
}
handler DrawWorld phase Render {

View file

@ -40,18 +40,20 @@ A tagged `match` must be **exhaustive**: it either handles every variant or ends
program BattleMenu {
enum Action { Attack, Guard, Item, Flee }
var current_action: int = Action.Attack
handler ChooseAction phase Input {
let pressed = Input.key()
if pressed == 'a' { current_action = Action.Attack }
if pressed == 'g' { current_action = Action.Guard }
if pressed == 'f' { current_action = Action.Flee }
state BattleMenuState {
current_action: int = Action.Attack
}
handler DrawWorld phase Render {
handler ChooseAction(battle_menu_st: mut BattleMenuState) phase Input {
let pressed = Input.key()
if pressed == 'a' { battle_menu_st.current_action = Action.Attack }
if pressed == 'g' { battle_menu_st.current_action = Action.Guard }
if pressed == 'f' { battle_menu_st.current_action = Action.Flee }
}
handler DrawWorld(battle_menu_st: BattleMenuState) phase Render {
Screen.clear(Color.MidnightBlue)
match current_action {
match battle_menu_st.current_action {
Action.Attack => Screen.draw_text(x: 8, y: 8, text: "ATTACK", color: Color.Crimson, scale: 2)
Action.Guard => Screen.draw_text(x: 8, y: 8, text: "GUARD", color: Color.White, scale: 2)
_ => Screen.draw_text(x: 8, y: 8, text: "FLEE", color: Color.Gold, scale: 2)

View file

@ -16,7 +16,7 @@ program SpaceDrift {
property Position { column: int = 0, row: int = 0 }
model Player { Position }
var score: int = 0
let score: int = 0
handler Boot phase Start {
spawn Hero { Position { column: 10, row: 8 } }

View file

@ -9,22 +9,24 @@ tip: Declare a retained widget tree as data; the engine lays it out and draws it
order: 50
---
A <code>ui</code> block declares a <b>retained</b> widget tree as data — panels, labels and buttons — and hands layout, drawing and keyboard focus to the engine, so you describe the interface once instead of repainting it every frame. Widget types are `panel` (a container with optional skin/background/border), `col`/`row` (pure stacks), `label`, `button` (focusable), `image` and `spacer`, and their props are evaluated at build time, so `font: title_font` reads a value the program set first. Each `id: Name` mints a `UI_Name` handle you drive from handlers: call `Ui.build()` then `Ui.open(UI_MainMenu)` at start, `Ui.tick(Input.key())` each update to move focus and activate, and `Ui.clicked(UI_Name)` to react. Draw it during `Render` with `Ui.render()` between `Screen.clear` and `Screen.show()`.
A <code>ui</code> block declares a <b>retained</b> widget tree as data — panels, labels and buttons — and hands layout, drawing and keyboard focus to the engine, so you describe the interface once instead of repainting it every frame. Widget types are `panel` (a container with optional skin/background/border), `col`/`row` (pure stacks), `label`, `button` (focusable), `image` and `spacer`, and their props are evaluated at build time, so `font: TitleScreenState.title_font` reads a value the program set first (a `ui` block names a state's one instance by the state's name). Each `id: Name` mints a `UI_Name` handle you drive from handlers: call `Ui.build()` then `Ui.open(UI_MainMenu)` at start, `Ui.tick(Input.key())` each update to move focus and activate, and `Ui.clicked(UI_Name)` to react. Draw it during `Render` with `Ui.render()` between `Screen.clear` and `Screen.show()`.
```ludic
program TitleScreen {
var title_font: int = 0
state TitleScreenState {
title_font: int = 0
}
ui MainMenu {
panel id: Root w: 288 pad: 16 gap: 6 bg: 0x1a1a2c align: center {
label text: "CHRONO RIFT" font: title_font size: 26 fg: 0xffe060 align: center
button id: NewGame text: "New Game" font: title_font size: 16 w: 236
button id: Quit text: "Quit" font: title_font size: 16 w: 236
label text: "CHRONO RIFT" font: TitleScreenState.title_font size: 26 fg: 0xffe060 align: center
button id: NewGame text: "New Game" font: TitleScreenState.title_font size: 16 w: 236
button id: Quit text: "Quit" font: TitleScreenState.title_font size: 16 w: 236
}
}
handler Boot phase Start {
title_font = Font.load("/System/Library/Fonts/Supplemental/Arial.ttf")
handler Boot(title_screen_st: mut TitleScreenState) phase Start {
title_screen_st.title_font = Font.load("/System/Library/Fonts/Supplemental/Arial.ttf")
Ui.build()
Ui.open(UI_MainMenu)
}

View file

@ -13,18 +13,20 @@ A <code>var</code> declares a <b>mutable</b> binding: unlike `let`, you may reas
```ludic
program Scorekeeper {
var score: int = 0
var elapsed_frames: int = 0
handler Tick phase Update {
elapsed_frames = elapsed_frames + 1
if elapsed_frames % 60 == 0 { score = score + 1 }
state ScorekeeperState {
score: int = 0
elapsed_frames: int = 0
}
handler DrawWorld phase Render {
handler Tick(scorekeeper_st: mut ScorekeeperState) phase Update {
scorekeeper_st.elapsed_frames = scorekeeper_st.elapsed_frames + 1
if scorekeeper_st.elapsed_frames % 60 == 0 { scorekeeper_st.score = scorekeeper_st.score + 1 }
}
handler DrawWorld(scorekeeper_st: ScorekeeperState) phase Render {
Screen.clear(Color.MidnightBlue)
Screen.draw_text(x: 6, y: 4, text: "SCORE", color: Color.White, scale: 1)
Screen.draw_number(x: 52, y: 4, value: score, color: Color.Gold, scale: 1)
Screen.draw_number(x: 52, y: 4, value: scorekeeper_st.score, color: Color.Gold, scale: 1)
Screen.show()
}
}

View file

@ -19,7 +19,7 @@ Parameters:
```ludic
program FirstIsDigit {
var entry: string = "7up"
let entry: string = "7up"
handler Check phase Update {
let c = Text.char_at(entry, 0)

View file

@ -19,7 +19,7 @@ Parameters:
```ludic
program Greeting {
var name: string = "world"
let name: string = "world"
handler Build phase Update {
let line = Text.concat("hello ", name) # "hello world"

View file

@ -19,7 +19,7 @@ Parameters:
```ludic
program Filter {
var line: string = "error: disk full"
let line: string = "error: disk full"
handler Scan phase Update {
if Text.contains(line, "error") {

View file

@ -19,7 +19,7 @@ Parameters:
```ludic
program IsImage {
var filename: string = "hero.png"
let filename: string = "hero.png"
handler Classify phase Update {
let png = Text.ends_with(filename, ".png") # true

View file

@ -19,8 +19,8 @@ Parameters:
```ludic
program Guess {
var answer: string = "swordfish"
var attempt: string = "swordfish"
let answer: string = "swordfish"
let attempt: string = "swordfish"
handler Check phase Update {
if Text.equals(attempt, answer) {

View file

@ -18,7 +18,7 @@ Parameters:
```ludic
program ScoreLabel {
var score: int = 1500
let score: int = 1500
handler Build phase Update {
let label = Text.concat("score: ", Text.from_int(score))

View file

@ -19,7 +19,7 @@ Parameters:
```ludic
program KeyValue {
var pair: string = "hp=42"
let pair: string = "hp=42"
handler Parse phase Update {
let eq = Text.index_of(pair, "=") # 2

View file

@ -18,7 +18,7 @@ Parameters:
```ludic
program NameWidth {
var label: string = "player one"
let label: string = "player one"
handler Draw phase Render {
let width = Text.length(label) * 6 # 6 px per glyph

View file

@ -20,7 +20,7 @@ Parameters:
```ludic
program FirstWord {
var line: string = "hello world"
let line: string = "hello world"
handler Split phase Update {
let space = Text.index_of(line, " ") # 5

View file

@ -19,7 +19,7 @@ Parameters:
```ludic
program Command {
var input: string = "/say hello"
let input: string = "/say hello"
handler Parse phase Update {
if Text.starts_with(input, "/say ") {

View file

@ -18,7 +18,7 @@ Parameters:
```ludic
program ParseSpeed {
var typed: string = "3.2"
let typed: string = "3.2"
handler Apply phase Update {
let speed = Text.to_float(typed) # 3.2

View file

@ -18,7 +18,7 @@ Parameters:
```ludic
program ParseAmount {
var typed: string = "250"
let typed: string = "250"
handler Apply phase Update {
let gold = Text.to_int(typed) # 250

View file

@ -13,7 +13,7 @@ order: 1
```ludic
const GRAVITY: fixed = 0.5
var velocity_y: fixed = fixed(0)
var next_velocity: fixed = velocity_y + GRAVITY
var pixel_row: int = floor(next_velocity)
let velocity_y: fixed = fixed(0)
let next_velocity: fixed = velocity_y + GRAVITY
let pixel_row: int = floor(next_velocity)
```

View file

@ -12,8 +12,8 @@ order: 0
`int` is a 32-bit signed integer and the default numeric type in Ludic: a literal like `42` or a hex literal like `0x1E90FF` is an `int`. It does far more than counting — colors are `int` hex values, the key from `Input.key()` is an `int` character code, and a tile from `Map.tile` is an `int`. Arithmetic (`+ - * / %`), comparison, and the bitwise operators all work on it, and it is the natural type for a `score`, a `column`, or an `elapsed_frames` counter. When you need fractions, reach for `fixed` and convert with `fx` / `flr`.
```ludic
var score: int = 0
let score: int = 0
const GRID_WIDTH: int = 20
var next_column: int = Random.range(low: 0, high: GRID_WIDTH - 1)
var next_score: int = score + 1
let next_column: int = Random.range(low: 0, high: GRID_WIDTH - 1)
let next_score: int = score + 1
```

View file

@ -22,8 +22,10 @@ import "ludic.base"
member and never binds it. A member with a default may be left out.
3. **Queues for facts.** What HAPPENED is pushed onto a `Queue<T>` the mechanic owns (`caught`)
and drained by the game in a later phase. A mechanic never acts on another's behalf.
4. **Verbs for changes.** A mechanic's state changes only through its exported functions
(`fishing_cast`, `pack_add`). Nothing assigns another module's globals.
4. **Verbs for changes.** A mechanic's data is its `state` (`ToyFishingState`), handed to its
functions as a parameter, and it changes only through its exported functions (`fishing_cast`,
`pack_add`). Nothing writes another module's state; a function value (`fn fishing_tick`) is
called with its states supplied, so a system never passes them itself.
5. **Phases for order.** A system names its phase; within a phase the game's list is the order.
No system says "after fishing".
6. **Its own save section.** Each system saves under its own key with its own version and
@ -56,26 +58,28 @@ numbers float
import "ludic.base"
export property Caught { species: int = 0, weight: float = 0.0 }
export port FishingWorld { is_water: fn(float, float) -> bool } # its port
var dice: Rng = null
var casts: int = 0
export var caught: Queue<Caught> = null # its facts
export function fishing_cast(x: float, z: float) -> bool { # its verb
export state ToyFishingState {
dice: Rng = null
casts: int = 0
caught: Queue<Caught> = null # its facts
}
export function fishing_cast(toy_fishing_st: mut ToyFishingState, x: float, z: float) -> bool { # its verb
if not FishingWorld.is_water(x, z) { return false }
casts += 1
toy_fishing_st.casts += 1
return true
}
function fishing_reset() -> void {
casts = 0
dice = rng_new(7)
caught = queue_new("fishing.caught")
function fishing_reset(base_st: mut BaseState, toy_fishing_st: mut ToyFishingState) -> void {
toy_fishing_st.casts = 0
toy_fishing_st.dice = rng_new(7)
toy_fishing_st.caught = queue_new(base_st, "fishing.caught")
}
function fishing_tick(t: Tick) -> void {
while casts > 0 {
function fishing_tick(base_st: mut BaseState, toy_fishing_st: mut ToyFishingState, t: Tick) -> void {
while toy_fishing_st.casts > 0 {
let c = new Caught
c.species = rng_between(dice, 0, 2)
c.weight = 0.5 + rng_float(dice)
q_push(caught, c)
casts -= 1
c.species = rng_between(toy_fishing_st.dice, 0, 2)
c.weight = 0.5 + rng_float(toy_fishing_st.dice)
q_push(base_st, toy_fishing_st.caught, c)
toy_fishing_st.casts -= 1
}
}
export function fishing_system() -> System {
@ -102,18 +106,18 @@ program Game {
bind FishingWorld { is_water: fn lake }
# the route: a landed fish goes into the pack
function route_fishing_pack(t: Tick) -> void {
let fish = q_drain(caught)
for i in 0 .. len(fish) { pack_add(fish[i].species, 1) }
function route_fishing_pack(base_st: mut BaseState, fishing: ToyFishingState, pack: mut ToyPackState, t: Tick) -> void {
let fish = q_drain(base_st, fishing.caught)
for i in 0 .. len(fish) { pack_add(pack, fish[i].species, 1) }
}
function game_start() -> void {
core_add(fishing_system())
function game_start(base_st: mut BaseState) -> void {
core_add(base_st, fishing_system())
let r = system_new("route.fishing_pack", PH_RESOLVE)
r.tick = fn route_fishing_pack
core_add(r)
core_add(pack_system())
core_reset_all()
r.tick = fn route_fishing_pack # a fn(Tick) -> void: its states are supplied
core_add(base_st, r)
core_add(base_st, pack_system())
core_reset_all(base_st)
}
}
```