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" import "ledger.ludic"
# bank/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 } export event Deposited { amount: int }
function add(n: int) -> void { balance += n } function add(b: mut Bank, n: int) -> void { b.balance += n } # private: only module bank sees it
export function deposit(n: int) -> void { export function deposit(b: mut Bank, n: int) -> void {
add(n) add(b, n)
emit Deposited(amount: 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 - **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 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 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. `mut` is for a state parameter only.
- **The runtime supplies it at the entry points** - the only code nothing in the program calls: - **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 - 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 - 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 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; 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 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. Every other call passes its states explicitly.
- **Everything else module-level is immutable all the way down.** `let LIMITS: []int = [1, 2]`, - **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 - **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. `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 - 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 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 **`ludic migrate state [file|dir...] [--runtime] [--dry-run]` moves programs there.** It compiles
compiler's own view of every name: 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; 1. a var nothing writes, holding a value (an int, a string, an enum...), becomes a module-level
2. every reference to one is rewritten to `<module>_st.<name>`; `let` where it stands;
3. each function's states - those it touches, and those of everything it calls, to a fixed point - 2. each module's other vars become one state, `state <Module>State { ... }`, where the first of
become its leading parameters, `mut` where it or something it calls writes; them was - a module's by its name (`module fishing`: `FishingState`), a directory with no module
4. each call passes them on, and each entry point declares them. 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 Give it every program at once - a directory stands for the test programs under it - and it merges
generated code), for a person to finish. Run it once per program that uses what changes - each of a their plans before it edits anything: programs that share a package agree about it, a module two of
package's test programs, a game's entry and its lab: a later run finds the states an earlier one them see different files of is one state, and a path reached as `../../packages/x` is the same
made and passes them on. `--runtime` moves the runtime's own vars too. 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 ### 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. TrueType text, focus highlight) and keyboard focus + activation.
```ludic ```ludic
var title_font: int = 0 state Menu { title_font: int = 0 }
ui MainMenu { ui MainMenu {
panel id: Root w: 288 pad: 16 gap: 6 skin: "assets/ui/panel.png" inset: 10 align: center { 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 label text: "CHRONO RIFT" font: Menu.title_font size: 26 fg: Color.Gold align: center
button id: NewGame text: "New Game" font: title_font size: 16 w: 236 button id: NewGame text: "New Game" font: Menu.title_font size: 16 w: 236
button id: Quit text: "Quit" font: 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. 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` Widget types: `panel` (container + optional skin/bg/border), `col` / `row`
(pure stacks), `label`, `button` (focusable), `image`, `spacer`. Props are (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 Each `id: Name` mints a `UI_Name` handle (the `ui` block name too), used from
handlers: handlers:
```ludic ```ludic
handler Boot phase Start { handler Boot(menu: mut Menu) phase Start {
title_font = Font.load("…Arial.ttf") menu.title_font = Font.load("…Arial.ttf")
Ui.build() # construct the tree (loads skins/images) Ui.build() # construct the tree (loads skins/images)
Ui.open(UI_MainMenu) # make it active, focus the first button Ui.open(UI_MainMenu) # make it active, focus the first button
} }
@ -1098,7 +1120,7 @@ any record's:
```ludic ```ludic
# doc-check: skip — composite: declarations plus statements using them # doc-check: skip — composite: declarations plus statements using them
property Hero { iframes: int = 0, roll_cooldown: int = 0 } 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).iframes = 20 # assign a field
Hero.of(player).roll_cooldown -= 1 # compound-assign one 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 ```ludic
# doc-check: skip — [T; N] fixed arrays are not yet implemented (design target) # doc-check: skip — [T; N] fixed arrays are not yet implemented (design target)
var table: [int; 8] # module-level storage state Grid { table: [int; 8] } # a state's storage
handler S phase Update { handler S(g: mut Grid) phase Update {
let buf: [int; 4] # a local; no initializer needed let buf: [int; 4] # a local; no initializer needed
buf[0] = 10 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. loop accumulators and anything that genuinely changes.
- **`const NAME = e`** — a compile-time constant (folded, no storage). - **`const NAME = e`** — a compile-time constant (folded, no storage).
A program-scope `var` may be initialized with **any expression** — a literal, an `var` is for locals. At module level there is no `var` (it is refused): what changes belongs to a
`Enum.Variant`, a `new Record`, a call. What the compiler can fold becomes the `state` (see "State" below), and what does not is a module-level `let`, immutable all the way
global's initial value; the rest runs once at startup, in declaration order, down. A state's field, or a module-level `let`, may be initialized with **any expression** — a
after the runtime boots and before the `Start` phase: 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 ```ludic
# doc-check: skip — illustrative globals # doc-check: skip — illustrative
var run: Progress = new Progress # allocated before Start state Hero {
var origin: IVec2 = IVec2.zero() run: Progress = new Progress # allocated before Start
var mode: HeroState = HeroState.Idle # folded 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 runtime already uses, which the message says (`variable ui_font is also a
variable of the engine runtime; choose another name`). variable of the engine runtime; choose another name`).
@ -1767,19 +1794,20 @@ no `= value` on any state:
```ludic ```ludic
# doc-check: skip — composite: declarations plus a machine over them # doc-check: skip — composite: declarations plus a machine over them
enum HeroState { Idle, Rolling, Swinging } 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 Idle { if wants_roll { become Rolling } } # HeroState.Idle
state Rolling { if done { become Idle } } # HeroState.Rolling state Rolling { if done { become Idle } } # HeroState.Rolling
state Swinging { … } 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 A machine's store is a state's field (`machine hero.mode`), a local, or a register index; a
(payload-free) enum is an `int`-sized type wherever a type is written — a `var`, `become` writes it, so the function needs its state `mut`. A state that names no variant of the
a parameter, a field, a return. 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 ## Enums

View file

@ -13,16 +13,18 @@ order: 3
```ludic ```ludic
program ScoreModule { program ScoreModule {
var running_total: int = 0 state ScoreModuleState {
running_total: int = 0
@export function add_points(amount: int) -> int { # callable from a native host
running_total = running_total + amount
return running_total
} }
entry { @export function add_points(score_module_st: mut ScoreModuleState, amount: int) -> int { # callable from a native host
print(add_points(10)) # 10 score_module_st.running_total = score_module_st.running_total + amount
print(add_points(5)) # 15 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 } property Health { current: int = 0, maximum: int = 100 }
model Player { Health } model Player { Health }
var elapsed_frames: int = 0 state BootHookState {
elapsed_frames: int = 0
@OnStart handler CreateWorld { # runs once at boot
spawn Player { Health { current: 100, maximum: 100 } }
elapsed_frames = 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 ```ludic skip
program Launcher { program Launcher {
var game: int = -1 state Child { game: int = -1 }
function play() -> void { function play(c: mut Child) -> void {
game = Process.spawn(game_path(), new []string) c.game = Process.spawn(game_path(), new []string)
if game >= 0 { App.window_hide() } if c.game >= 0 { App.window_hide() }
} }
handler Watch phase Update { handler Watch(c: mut Child) phase Update {
if game >= 0 and Process.poll(game) != -1 { if c.game >= 0 and Process.poll(c.game) != -1 {
Process.free(game) Process.free(c.game)
game = -1 c.game = -1
App.window_show() App.window_show()
} }
} }

View file

@ -17,18 +17,20 @@ Parameters:
```ludic ```ludic
program HighScore { program HighScore {
var score: int = 0 state HighScoreState {
var best_score: int = 0 score: int = 0
best_score: int = 0
handler EarnPoints phase Update {
score = score + 5
best_score = max(best_score, score)
if score >= 20 { quit() }
} }
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.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() Screen.show()
} }
} }

View file

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

View file

@ -16,7 +16,7 @@ Parameters:
```ludic ```ludic
program LabelValue { program LabelValue {
var score: int = 250 let score: int = 250
handler ReportScore phase Start { handler ReportScore phase Start {
let label = string(score) 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 ```ludic
program TitleMenu { program TitleMenu {
var title_font: int = 0 state TitleMenuState {
title_font: int = 0
}
ui MainMenu { ui MainMenu {
panel id: Root w: 240 pad: 16 gap: 6 bg: 0x1a1a2c align: center { 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 label text: "CHRONORIFT" font: TitleMenuState.title_font size: 24 fg: 0xffe060 align: center
button id: NewGame text: "New Game" font: title_font size: 16 w: 200 button id: NewGame text: "New Game" font: TitleMenuState.title_font size: 16 w: 200
button id: Quit text: "Quit" font: title_font size: 16 w: 200 button id: Quit text: "Quit" font: TitleMenuState.title_font size: 16 w: 200
} }
} }
handler Boot phase Start { handler Boot(title_menu_st: mut TitleMenuState) phase Start {
title_font = Font.load("/System/Library/Fonts/Supplemental/Arial.ttf") title_menu_st.title_font = Font.load("/System/Library/Fonts/Supplemental/Arial.ttf")
ui_build() ui_build()
Ui.open(UI_MainMenu) Ui.open(UI_MainMenu)
} }

View file

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

View file

@ -13,12 +13,14 @@ order: 9
```ludic ```ludic
program FindFirst { 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 { for i in 0 .. 10 {
if i * i > 20 { if i * i > 20 {
found = i find_first_st.found = i
break # stop at the first i whose square exceeds 20 break # stop at the first i whose square exceeds 20
} }
} }

View file

@ -13,12 +13,14 @@ order: 10
```ludic ```ludic
program SumOdds { 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 { for i in 0 .. 10 {
if i % 2 == 0 { continue } # skip even numbers 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 ```ludic
program TurnOrder { program TurnOrder {
enum Phase { KnightMenu, KnightResolve, EnemyTurn } enum Phase { KnightMenu, KnightResolve, EnemyTurn }
var battle_phase: int = Phase.KnightMenu state TurnOrderState {
battle_phase: int = Phase.KnightMenu
}
handler RunTurn phase Update { handler RunTurn(turn_order_st: mut TurnOrderState) phase Update {
machine battle_phase { machine turn_order_st.battle_phase {
state KnightMenu { state KnightMenu {
if Input.key() == ' ' { become KnightResolve } 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.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() Screen.show()
} }
} }

View file

@ -14,17 +14,19 @@ A <code>state</code> declares one state of an enclosing <code>machine</code>: a
```ludic ```ludic
program DoorControl { program DoorControl {
enum DoorState { Closed, Opening, Open } enum DoorState { Closed, Opening, Open }
var door: int = DoorState.Closed state DoorControlState {
var elapsed_frames: int = 0 door: int = DoorState.Closed
elapsed_frames: int = 0
}
handler RunDoor phase Update { handler RunDoor(door_control_st: mut DoorControlState) phase Update {
machine door { machine door_control_st.door {
state Closed { state Closed {
if Input.key() == ' ' { elapsed_frames = 0; become Opening } if Input.key() == ' ' { door_control_st.elapsed_frames = 0; become Opening }
} }
state Opening { state Opening {
elapsed_frames = elapsed_frames + 1 door_control_st.elapsed_frames = door_control_st.elapsed_frames + 1
if elapsed_frames >= 30 { become Open } if door_control_st.elapsed_frames >= 30 { become Open }
} }
state Open { state Open {
Screen.status("door 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 ```ludic
program Countdown { program Countdown {
var fuse: int = 5 state CountdownState {
var elapsed_frames: int = 0 fuse: int = 5
elapsed_frames: int = 0
}
handler Tick phase Update { handler Tick(countdown_st: mut CountdownState) phase Update {
elapsed_frames = elapsed_frames + 1 countdown_st.elapsed_frames = countdown_st.elapsed_frames + 1
while fuse > 0 and elapsed_frames % 60 == 0 { while countdown_st.fuse > 0 and countdown_st.elapsed_frames % 60 == 0 {
fuse = fuse - 1 countdown_st.fuse = countdown_st.fuse - 1
elapsed_frames = elapsed_frames + 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.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() Screen.show()
} }
} }

View file

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

View file

@ -15,23 +15,25 @@ order: 2
program CancelExample { program CancelExample {
event cancellable BeforeOpenDoor { key_count: int = 0 } event cancellable BeforeOpenDoor { key_count: int = 0 }
var keys: int = 0 state CancelExampleState {
var doors_opened: int = 0 keys: int = 0
doors_opened: int = 0
}
@On(BeforeOpenDoor) handler RequireKey { if key_count <= 0 { cancel } } @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 Input.key() == 'o' {
if emit BeforeOpenDoor(key_count: keys) == 0 { if emit BeforeOpenDoor(key_count: cancel_example_st.keys) == 0 {
doors_opened = doors_opened + 1 cancel_example_st.doors_opened = cancel_example_st.doors_opened + 1
keys = keys - 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.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() Screen.show()
} }
} }

View file

@ -15,19 +15,21 @@ order: 50
program CancellableExample { program CancellableExample {
event cancellable BeforeHurt { amount: int = 0 } 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 } } @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 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.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() Screen.show()
} }
} }

View file

@ -15,19 +15,21 @@ order: 1
program EmitExample { program EmitExample {
event cancellable BeforeSpend { amount: int = 0 } 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 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.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() Screen.show()
} }
} }

View file

@ -15,18 +15,20 @@ order: 0
program EventExample { program EventExample {
event EnemyDefeated { points: int = 0 } 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") } @On(EnemyDefeated) handler PlayChime { Screen.status("enemy down") }
handler ScoreOnKey phase Update { handler ScoreOnKey phase Update {
if Input.key() == 'k' { emit EnemyDefeated(points: 10) } 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.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() Screen.show()
} }
} }

View file

@ -15,9 +15,9 @@ Loads a TrueType font file and returns its handle. Load it before <code>Ui.build
```ludic ```ludic
# doc-check: skip — illustrative # doc-check: skip — illustrative
var menu_font: int = 0 state Menu { font: int = 0 }
handler Boot phase Start { handler Boot(menu: mut Menu) phase Start {
menu_font = Font.load(path: "assets/fonts/PixelOperator.ttf") menu.font = Font.load(path: "assets/fonts/PixelOperator.ttf")
Ui.build() 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 { program Triangle {
property Marker { on: int = 1 } property Marker { on: int = 1 }
model Anchor { Marker } model Anchor { Marker }
var prog: int = 0 state TriangleState {
var vao: int = 0 prog: int = 0
handler Boot phase Start { vao: int = 0
}
handler Boot(triangle_st: mut TriangleState) phase Start {
spawn Anchor {} spawn Anchor {}
if not Gl.open(width: 640, height: 360, title: "GL") { quit() } 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") 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_color(red: 0.1, green: 0.1, blue: 0.15, alpha: 1.0)
Gl.clear(mask: GL_COLOR_BUFFER_BIT) Gl.clear(mask: GL_COLOR_BUFFER_BIT)
Gl.use_program(prog: prog) Gl.use_program(prog: triangle_st.prog)
Gl.bind_vertex_array(array: vao) Gl.bind_vertex_array(array: triangle_st.vao)
Gl.draw_arrays(mode: GL_TRIANGLES, first: 0, count: 3) Gl.draw_arrays(mode: GL_TRIANGLES, first: 0, count: 3)
Gl.swap() Gl.swap()
} }

View file

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

View file

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

View file

@ -19,12 +19,14 @@ Parameters:
```ludic ```ludic
program Visited { 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 let tile = 42
if not List.contains(seen, tile) { if not List.contains(visited_st.seen, tile) {
List.push(seen, tile) List.push(visited_st.seen, tile)
} }
} }
} }

View file

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

View file

@ -19,12 +19,14 @@ Parameters:
```ludic ```ludic
program FindSlot { program FindSlot {
var ids: []int = new []int state FindSlotState {
ids: []int = new []int
}
handler Locate phase Update { handler Locate(find_slot_st: FindSlotState) phase Update {
let slot = List.index_of(ids, 99) let slot = List.index_of(find_slot_st.ids, 99)
if slot >= 0 { 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 ```ludic
program Trail { program Trail {
var points: []int = new []int state TrailState {
points: []int = new []int
}
handler Head phase Update { handler Head(trail_st: TrailState) phase Update {
if List.len(points) > 0 { if List.len(trail_st.points) > 0 {
let newest = List.last(points) let newest = List.last(trail_st.points)
} }
} }
} }

View file

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

View file

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

View file

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

View file

@ -18,11 +18,13 @@ Parameters:
```ludic ```ludic
program PathOrder { 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 # 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 ```ludic
program RemoveFast { 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 dead = 2
let last = List.len(actors) - 1 let last = List.len(remove_fast_st.actors) - 1
List.swap(actors, dead, last) # move it to the end... List.swap(remove_fast_st.actors, dead, last) # move it to the end...
let removed = List.pop(actors) # ...then drop it let removed = List.pop(remove_fast_st.actors) # ...then drop it
} }
} }
``` ```

View file

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

View file

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

View file

@ -22,10 +22,12 @@ Parameters:
program CameraBounds { program CameraBounds {
const WORLD_WIDTH: int = 320 const WORLD_WIDTH: int = 320
var camera_x: int = 0 state CameraBoundsState {
camera_x: int = 0
}
handler Follow phase Update { handler Follow(camera_bounds_st: mut CameraBoundsState) phase Update {
camera_x = Math.clamp(camera_x, 0, WORLD_WIDTH - 1) 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 { program TileIndex {
const TILE_SIZE: fixed = 16.0 const TILE_SIZE: fixed = 16.0
var world_x: fixed = 40.5 let world_x: fixed = 40.5
handler Locate phase Update { handler Locate phase Update {
let column = Math.floor(world_x / TILE_SIZE) # 2 let column = Math.floor(world_x / TILE_SIZE) # 2

View file

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

View file

@ -20,11 +20,13 @@ Parameters:
```ludic ```ludic
program EaseCamera { program EaseCamera {
var camera_x: fixed = 0.0 state EaseCameraState {
var target_x: fixed = 100.0 camera_x: fixed = 0.0
}
let target_x: fixed = 100.0
handler Follow phase Update { handler Follow(ease_camera_st: mut EaseCameraState) phase Update {
camera_x = Math.lerp(camera_x, target_x, 0.1) # glides a tenth of the way each frame 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 ```ludic
program Countdown { program Countdown {
var timer: int = 60 state CountdownState {
timer: int = 60
}
handler Tick phase Update { handler Tick(countdown_st: mut CountdownState) phase Update {
timer = Math.max(timer - 1, 0) # stops at 0, never negative countdown_st.timer = Math.max(countdown_st.timer - 1, 0) # stops at 0, never negative
} }
} }
``` ```

View file

@ -19,10 +19,12 @@ Parameters:
```ludic ```ludic
program ClampScore { program ClampScore {
var score: int = 0 state ClampScoreState {
score: int = 0
}
handler AddPoint phase Update { handler AddPoint(clamp_score_st: mut ClampScoreState) phase Update {
score = Math.min(score + 1, 100) # never climbs past 100 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 MAX_HP: fixed = 200.0
const BAR_WIDTH: fixed = 64.0 const BAR_WIDTH: fixed = 64.0
var hp: fixed = 150.0 let hp: fixed = 150.0
handler Draw phase Render { handler Draw phase Render {
let width = Math.remap(hp, 0.0, MAX_HP, 0.0, BAR_WIDTH) # 48.0 let width = Math.remap(hp, 0.0, MAX_HP, 0.0, BAR_WIDTH) # 48.0

View file

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

View file

@ -18,11 +18,13 @@ Parameters:
```ludic ```ludic
program FaceTarget { program FaceTarget {
var x: int = 8 state FaceTargetState {
var target: int = 20 x: int = 8
}
let target: int = 20
handler StepToward phase Update { handler StepToward(face_target_st: mut FaceTargetState) phase Update {
x = x + Math.sign(target - x) # +1 while target is to the right 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 ```ludic
const FLAG_POISONED: int = 1 const FLAG_POISONED: int = 1
const FLAG_SHIELDED: int = 2 const FLAG_SHIELDED: int = 2
var status_flags: int = FLAG_POISONED | FLAG_SHIELDED let status_flags: int = FLAG_POISONED | FLAG_SHIELDED
var is_shielded: bool = status_flags & FLAG_SHIELDED != 0 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 ```ludic
const TILE_SIZE: int = 16 # pixels per grid cell 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 # the head is drawn brighter than the body
``` ```

View file

@ -15,18 +15,20 @@ order: 1
program InputExample { program InputExample {
const DIRECTION_LEFT: int = 0 const DIRECTION_LEFT: int = 0
const DIRECTION_RIGHT: int = 1 const DIRECTION_RIGHT: int = 1
var facing: int = DIRECTION_RIGHT state InputExampleState {
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 }
} }
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) Screen.clear(Color.MidnightBlue)
var marker_x = 40 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.fill_rectangle(x: marker_x, y: 100, width: 16, height: 16, color: Color.LimeGreen)
Screen.show() Screen.show()
} }

View file

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

View file

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

View file

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

View file

@ -17,18 +17,20 @@ program StartExample {
model Player { Position } model Player { Position }
const GRID_WIDTH: int = 20 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) Random.seed(value: 1)
Map.size(width: GRID_WIDTH, height: 15) Map.size(width: GRID_WIDTH, height: 15)
score = 0 start_example_st.score = 0
spawn Player { Position { column: 10, row: 7 } } spawn Player { Position { column: 10, row: 7 } }
} }
handler DrawWorld phase Render { handler DrawWorld(start_example_st: StartExampleState) phase Render {
Screen.clear(Color.MidnightBlue) 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() Screen.show()
} }
} }

View file

@ -17,24 +17,26 @@ program UpdateExample {
model Enemy { Position } model Enemy { Position }
const GRID_WIDTH: int = 20 const GRID_WIDTH: int = 20
var elapsed_frames: int = 0 state UpdateExampleState {
var score: int = 0 elapsed_frames: int = 0
score: int = 0
}
handler SpawnWave phase Update { handler SpawnWave(update_example_st: mut UpdateExampleState) phase Update {
elapsed_frames = elapsed_frames + 1 update_example_st.elapsed_frames = update_example_st.elapsed_frames + 1
if elapsed_frames % 30 == 0 { if update_example_st.elapsed_frames % 30 == 0 {
let spawn_column = Random.range(low: 0, high: GRID_WIDTH - 1) let spawn_column = Random.range(low: 0, high: GRID_WIDTH - 1)
spawn Enemy { Position { column: spawn_column, row: 0 } } 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) Screen.clear(Color.MidnightBlue)
for (current_position) in query [Position, {Enemy}] { 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.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() Screen.show()
} }
} }

View file

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

View file

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

View file

@ -16,7 +16,7 @@ program LayerExample {
property Position { column: int = 0, row: int = 0 } property Position { column: int = 0, row: int = 0 }
model Player { Position } model Player { Position }
var score: int = 0 let score: int = 0
scene Overworld start { scene Overworld start {
on enter { spawn Player { Position { column: 6, row: 6 } } } 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 } property Position { column: int = 0, row: int = 0 }
model Enemy { Position } model Enemy { Position }
var elapsed_frames: int = 0 state SceneHookExampleState {
elapsed_frames: int = 0
}
scene Overworld start { scene Overworld start {
layer World { layer World {
@ -31,8 +33,8 @@ program SceneHookExample {
} }
scene Battle { scene Battle {
on enter { on enter (scene_hook_example_st: mut SceneHookExampleState) {
elapsed_frames = 0 scene_hook_example_st.elapsed_frames = 0
spawn Enemy { Position { column: 8, row: 4 } } spawn Enemy { Position { column: 8, row: 4 } }
} }
on exit { on exit {

View file

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

View file

@ -18,15 +18,17 @@ Parameters:
```ludic ```ludic
program ClearBackground { program ClearBackground {
var elapsed_frames: int = 0 state ClearBackgroundState {
elapsed_frames: int = 0
handler CountFrames phase Update {
elapsed_frames = elapsed_frames + 1
} }
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.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() Screen.show()
} }
} }

View file

@ -22,16 +22,18 @@ Parameters:
```ludic ```ludic
program ScoreHud { program ScoreHud {
var score: int = 0 state ScoreHudState {
score: int = 0
handler EarnPoints phase Update {
score = score + 1
} }
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.clear(Color.MidnightBlue)
Screen.draw_text(x: 6, y: 4, text: "SCORE", color: Color.White, scale: 1) 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() Screen.show()
} }
} }

View file

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

View file

@ -18,11 +18,13 @@ Parameters:
```ludic ```ludic
program StatusLine { program StatusLine {
var elapsed_frames: int = 0 state StatusLineState {
elapsed_frames: int = 0
}
handler UpdateStatus phase Update { handler UpdateStatus(status_line_st: mut StatusLineState) phase Update {
elapsed_frames = elapsed_frames + 1 status_line_st.elapsed_frames = status_line_st.elapsed_frames + 1
Screen.status(text: `frame {elapsed_frames}`) Screen.status(text: `frame {status_line_st.elapsed_frames}`)
} }
handler DrawWorld phase Render { 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 { program BattleMenu {
enum Action { Attack, Guard, Item, Flee } enum Action { Attack, Guard, Item, Flee }
var current_action: int = Action.Attack state BattleMenuState {
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 }
} }
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) 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.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) 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) _ => 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 } property Position { column: int = 0, row: int = 0 }
model Player { Position } model Player { Position }
var score: int = 0 let score: int = 0
handler Boot phase Start { handler Boot phase Start {
spawn Hero { Position { column: 10, row: 8 } } 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 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 ```ludic
program TitleScreen { program TitleScreen {
var title_font: int = 0 state TitleScreenState {
title_font: int = 0
}
ui MainMenu { ui MainMenu {
panel id: Root w: 288 pad: 16 gap: 6 bg: 0x1a1a2c align: center { 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 label text: "CHRONO RIFT" font: TitleScreenState.title_font size: 26 fg: 0xffe060 align: center
button id: NewGame text: "New Game" font: title_font size: 16 w: 236 button id: NewGame text: "New Game" font: TitleScreenState.title_font size: 16 w: 236
button id: Quit text: "Quit" font: title_font size: 16 w: 236 button id: Quit text: "Quit" font: TitleScreenState.title_font size: 16 w: 236
} }
} }
handler Boot phase Start { handler Boot(title_screen_st: mut TitleScreenState) phase Start {
title_font = Font.load("/System/Library/Fonts/Supplemental/Arial.ttf") title_screen_st.title_font = Font.load("/System/Library/Fonts/Supplemental/Arial.ttf")
Ui.build() Ui.build()
Ui.open(UI_MainMenu) 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 ```ludic
program Scorekeeper { program Scorekeeper {
var score: int = 0 state ScorekeeperState {
var elapsed_frames: int = 0 score: int = 0
elapsed_frames: int = 0
handler Tick phase Update {
elapsed_frames = elapsed_frames + 1
if elapsed_frames % 60 == 0 { score = score + 1 }
} }
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.clear(Color.MidnightBlue)
Screen.draw_text(x: 6, y: 4, text: "SCORE", color: Color.White, scale: 1) 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() Screen.show()
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,7 +13,7 @@ order: 1
```ludic ```ludic
const GRAVITY: fixed = 0.5 const GRAVITY: fixed = 0.5
var velocity_y: fixed = fixed(0) let velocity_y: fixed = fixed(0)
var next_velocity: fixed = velocity_y + GRAVITY let next_velocity: fixed = velocity_y + GRAVITY
var pixel_row: int = floor(next_velocity) 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`. `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 ```ludic
var score: int = 0 let score: int = 0
const GRID_WIDTH: int = 20 const GRID_WIDTH: int = 20
var next_column: int = Random.range(low: 0, high: GRID_WIDTH - 1) let next_column: int = Random.range(low: 0, high: GRID_WIDTH - 1)
var next_score: int = score + 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. 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`) 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. 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 4. **Verbs for changes.** A mechanic's data is its `state` (`ToyFishingState`), handed to its
(`fishing_cast`, `pack_add`). Nothing assigns another module's globals. 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. 5. **Phases for order.** A system names its phase; within a phase the game's list is the order.
No system says "after fishing". No system says "after fishing".
6. **Its own save section.** Each system saves under its own key with its own version and 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" import "ludic.base"
export property Caught { species: int = 0, weight: float = 0.0 } export property Caught { species: int = 0, weight: float = 0.0 }
export port FishingWorld { is_water: fn(float, float) -> bool } # its port export port FishingWorld { is_water: fn(float, float) -> bool } # its port
var dice: Rng = null export state ToyFishingState {
var casts: int = 0 dice: Rng = null
export var caught: Queue<Caught> = null # its facts casts: int = 0
export function fishing_cast(x: float, z: float) -> bool { # its verb 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 } if not FishingWorld.is_water(x, z) { return false }
casts += 1 toy_fishing_st.casts += 1
return true return true
} }
function fishing_reset() -> void { function fishing_reset(base_st: mut BaseState, toy_fishing_st: mut ToyFishingState) -> void {
casts = 0 toy_fishing_st.casts = 0
dice = rng_new(7) toy_fishing_st.dice = rng_new(7)
caught = queue_new("fishing.caught") toy_fishing_st.caught = queue_new(base_st, "fishing.caught")
} }
function fishing_tick(t: Tick) -> void { function fishing_tick(base_st: mut BaseState, toy_fishing_st: mut ToyFishingState, t: Tick) -> void {
while casts > 0 { while toy_fishing_st.casts > 0 {
let c = new Caught let c = new Caught
c.species = rng_between(dice, 0, 2) c.species = rng_between(toy_fishing_st.dice, 0, 2)
c.weight = 0.5 + rng_float(dice) c.weight = 0.5 + rng_float(toy_fishing_st.dice)
q_push(caught, c) q_push(base_st, toy_fishing_st.caught, c)
casts -= 1 toy_fishing_st.casts -= 1
} }
} }
export function fishing_system() -> System { export function fishing_system() -> System {
@ -102,18 +106,18 @@ program Game {
bind FishingWorld { is_water: fn lake } bind FishingWorld { is_water: fn lake }
# the route: a landed fish goes into the pack # the route: a landed fish goes into the pack
function route_fishing_pack(t: Tick) -> void { function route_fishing_pack(base_st: mut BaseState, fishing: ToyFishingState, pack: mut ToyPackState, t: Tick) -> void {
let fish = q_drain(caught) let fish = q_drain(base_st, fishing.caught)
for i in 0 .. len(fish) { pack_add(fish[i].species, 1) } for i in 0 .. len(fish) { pack_add(pack, fish[i].species, 1) }
} }
function game_start() -> void { function game_start(base_st: mut BaseState) -> void {
core_add(fishing_system()) core_add(base_st, fishing_system())
let r = system_new("route.fishing_pack", PH_RESOLVE) let r = system_new("route.fishing_pack", PH_RESOLVE)
r.tick = fn route_fishing_pack r.tick = fn route_fishing_pack # a fn(Tick) -> void: its states are supplied
core_add(r) core_add(base_st, r)
core_add(pack_system()) core_add(base_st, pack_system())
core_reset_all() core_reset_all(base_st)
} }
} }
``` ```