Bindings now signal mutability the way Rust/Swift do, instead of `let` meaning
"local" and `var` meaning "module-level":
- `let x = e` -> immutable binding; a later `x = …` is a compile error
(`cannot assign to immutable 'x' … use var`).
- `var x = e` -> mutable binding, at local OR module scope (position decides
scope; the keyword decides mutability).
- `const` -> unchanged (compile-time).
Immutability is of the *binding*, not the object: `let n = new Node; n.kind = 1`
is fine (mutation through the reference); only rebinding `n` is rejected. The
check lives in emit_assign — a direct `name =` whose target is a `let` local
(loc_mut == 0) errors; field/index targets and `var`/param/loop bindings are
unaffected.
Delivered as three reseeds so the self-hosting compiler never had to compile
source its own rules would reject:
A) add `var` as a local statement + per-local mutability tracking (loc_mut),
no enforcement;
B) migrate every reassigned `let` -> `var` across the compiler, runtime and
examples (337 declarations), driven by a per-function, string/comment-aware
scan (binding targets only, never `x.f =` / `x[i] =`);
C) turn on the check. bootstrap-cfree (compiler vs its own source) and every
golden build (which splices the runtime) then proved zero reassigned `let`
was missed anywhere.
Also folded in: removed leftover debug instrumentation in block() (a `cur=` /
print_int(777…) trace on the separator-error path) and fixed parse.ludic's stale
header comment (no more `struct`). Reseeded (21711 lines); C-free fixpoint holds;
goldens identical; 17/17; vocab + doc-fences clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
136 lines
4.3 KiB
Text
136 lines
4.3 KiB
Text
# ============================================================================
|
|
# snake.ludic — a second, unrelated game in Ludic, to prove the language is
|
|
# general: compiled by the SAME unmodified ludicc + runtime as the JRPG.
|
|
# No sprites/assets — pure primitives. Build: ./build.sh examples/snake.ludic
|
|
# ============================================================================
|
|
program Snake {
|
|
|
|
property Pos { x: int = 0, y: int = 0 }
|
|
property Seg { order: int = 0 }
|
|
|
|
const GW: int = 20
|
|
const GH: int = 15
|
|
const TS: int = 16
|
|
|
|
const R_DIR: int = 0 # 0 up 1 down 2 left 3 right
|
|
const R_LEN: int = 1
|
|
const R_TICK: int = 2
|
|
const R_FX: int = 3
|
|
const R_FY: int = 4
|
|
const R_MODE: int = 5 # 0 play 1 gameover
|
|
const R_SCORE: int = 6
|
|
const R_ATE: int = 7
|
|
const R_TX: int = 8
|
|
const R_TY: int = 9
|
|
const R_ACK: int = 10
|
|
|
|
fn reset(x: int) -> int {
|
|
for (p, s) in query [Pos, Seg] { despawn self() }
|
|
spawn S0 { Seg { order: 0 }; Pos { x: 10, y: 7 } }
|
|
spawn S1 { Seg { order: 1 }; Pos { x: 9, y: 7 } }
|
|
spawn S2 { Seg { order: 2 }; Pos { x: 8, y: 7 } }
|
|
set_reg(R_DIR, 3)
|
|
set_reg(R_LEN, 3)
|
|
set_reg(R_TICK, 0)
|
|
set_reg(R_MODE, 0)
|
|
set_reg(R_SCORE, 0)
|
|
set_reg(R_FX, 14)
|
|
set_reg(R_FY, 7)
|
|
return 0
|
|
}
|
|
|
|
handler Boot phase Start { reset(0) }
|
|
|
|
handler Input phase Input {
|
|
let k = key()
|
|
if reg(R_MODE) == 0 {
|
|
if k == 'w' { if reg(R_DIR) != 1 { set_reg(R_DIR, 0) } }
|
|
if k == 's' { if reg(R_DIR) != 0 { set_reg(R_DIR, 1) } }
|
|
if k == 'a' { if reg(R_DIR) != 3 { set_reg(R_DIR, 2) } }
|
|
if k == 'd' { if reg(R_DIR) != 2 { set_reg(R_DIR, 3) } }
|
|
}
|
|
if reg(R_MODE) == 1 {
|
|
if k == 0 { set_reg(R_ACK, 1) }
|
|
if k != 0 { if reg(R_ACK) == 1 { reset(0) } }
|
|
}
|
|
}
|
|
|
|
handler Move phase Update {
|
|
if reg(R_MODE) == 0 {
|
|
set_reg(R_TICK, reg(R_TICK) + 1)
|
|
if reg(R_TICK) >= 6 {
|
|
set_reg(R_TICK, 0)
|
|
let len = reg(R_LEN)
|
|
for (pt, stg) in query [Pos, Seg] {
|
|
if stg.order == len - 1 { set_reg(R_TX, pt.x); set_reg(R_TY, pt.y) }
|
|
}
|
|
for step in 0 .. len {
|
|
let i = len - 1 - step
|
|
if i >= 1 {
|
|
for (pa, sa) in query [Pos, Seg] {
|
|
if sa.order == i {
|
|
for (pb, sb) in query [Pos, Seg] {
|
|
if sb.order == i - 1 { pa.x = pb.x; pa.y = pb.y }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
set_reg(R_ATE, 0)
|
|
for (ph, sh) in query [Pos, Seg] {
|
|
if sh.order == 0 {
|
|
if reg(R_DIR) == 0 { ph.y = ph.y - 1 }
|
|
if reg(R_DIR) == 1 { ph.y = ph.y + 1 }
|
|
if reg(R_DIR) == 2 { ph.x = ph.x - 1 }
|
|
if reg(R_DIR) == 3 { ph.x = ph.x + 1 }
|
|
if ph.x < 0 { set_reg(R_MODE, 1) }
|
|
if ph.x >= GW { set_reg(R_MODE, 1) }
|
|
if ph.y < 0 { set_reg(R_MODE, 1) }
|
|
if ph.y >= GH { set_reg(R_MODE, 1) }
|
|
if ph.x == reg(R_FX) { if ph.y == reg(R_FY) { set_reg(R_ATE, 1) } }
|
|
}
|
|
}
|
|
for (ph, sh) in query [Pos, Seg] {
|
|
if sh.order == 0 {
|
|
for (pb, sb) in query [Pos, Seg] {
|
|
if sb.order > 0 {
|
|
if pb.x == ph.x { if pb.y == ph.y { set_reg(R_MODE, 1) } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if reg(R_ATE) == 1 {
|
|
set_reg(R_SCORE, reg(R_SCORE) + 1)
|
|
spawn Body { Seg { order: len }; Pos { x: reg(R_TX), y: reg(R_TY) } }
|
|
set_reg(R_LEN, len + 1)
|
|
set_reg(R_FX, rng_range(0, GW - 1))
|
|
set_reg(R_FY, rng_range(0, GH - 1))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
handler Draw phase Render {
|
|
clear(0x0d1020)
|
|
for gy in 0 .. GH {
|
|
for gx in 0 .. GW {
|
|
var col = 0x141a2e
|
|
if (gx + gy) % 2 == 0 { col = 0x10162a }
|
|
fill_rect(gx * TS, gy * TS, TS, TS, col)
|
|
}
|
|
}
|
|
fill_rect(reg(R_FX) * TS + 3, reg(R_FY) * TS + 3, TS - 6, TS - 6, 0xe0405a)
|
|
for (p, s) in query [Pos, Seg] {
|
|
var col = 0x40c060
|
|
if s.order == 0 { col = 0x9dffb0 }
|
|
fill_rect(p.x * TS + 1, p.y * TS + 1, TS - 2, TS - 2, col)
|
|
}
|
|
text(6, 4, "SCORE", 0xffffff, 1)
|
|
text_int(52, 4, reg(R_SCORE), 0xffe060, 1)
|
|
if reg(R_MODE) == 1 {
|
|
text(112, 104, "GAME OVER", 0xff5555, 2)
|
|
text(84, 140, "PRESS ANY KEY", 0xffffff, 1)
|
|
}
|
|
present()
|
|
}
|
|
}
|