feat(stdlib): DateTime format/parse + simulated Clock — completes #9
Some checks are pending
docs / build-and-deploy (push) Waiting to run

Finish issue #9 by adding the two remaining acceptance items on top of the
calendar/clock core, still pure-integer and deterministic:

  DateTime.format(dt, pattern) -> string   render an instant via a token
                                           pattern (YYYY/YY/MM/DD/HH/mm/ss;
                                           other chars pass through)
  DateTime.parse(text, pattern) -> int     read an instant back; -1 on a
                                           non-digit where one is expected
  Clock.now/set/advance/reset              a game-controlled simulated clock
                                           (the @L_clock global) that never
                                           touches the wall clock, so gameplay
                                           reading Clock.now() is replay-safe

format/parse take a string-LITERAL pattern and are expanded at compile time
(field offsets are then constant), folding @fn_str_concat over literal runs and
two small runtime helpers: @fn_dt_pad0 (zero-padded field) and @fn_dt_rd
(fixed-width digit reader that stops at the terminator and flags malformed
input). Clock is a universal i32 global declared in emit_head, so it works in
entry and game programs alike.

Adds examples/offline_rewards.ludic — the issue's worked "you were away N hours"
example, driven from its own entry and asserted in the regression suite — plus
selfhost/tests/datetime2.ludic (format/parse round-trip, parse failure, clock),
docs (Clock section + 4 pages, DateTime.format/parse pages), inventory and LSP
hover. Reseeded; C-free fixpoint holds; all suites green (27 self-host / 46
regression / 29 tools); check.py (366 symbols), check-impl.py (218 ns-methods)
and validate.py OK.

With this, #9's scope is fully delivered: DateTime/Date/Duration + core ops,
format/parse, a deterministic simulated clock, docs + offline-rewards example,
and tests. (v1 stays UTC-only, no leap seconds, i32 epoch valid through 2038.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-30 03:51:48 +03:00
parent 1a2c6ec2c7
commit b5455cd550
17 changed files with 13902 additions and 12278 deletions

View file

@ -0,0 +1,7 @@
---
id: clock
title: Clock
order: 6
---
A game-controlled simulated clock: a single seconds counter the game owns. Unlike <code>Time.now</code> / <code>Time.since</code>, it never reads the wall clock, so any gameplay that reads <code>Clock.now()</code> is deterministic and replay-safe. Set it outright, or advance it by a <code>Duration</code> each tick to run time at whatever rate the simulation wants. The clock is a plain instant (seconds since 1970), so all the <code>DateTime.*</code> readers work on it directly.

View file

@ -0,0 +1,22 @@
---
id: clock-advance
name: Clock.advance
category: clock
kind: namespace-method
tokens: Clock.advance
sig: Clock.advance(span)
tip: Move the clock forward by a Duration.
order: 2
ns: Clock
member: advance
---
Moves the simulated clock forward by <code>span</code> seconds &mdash; pair it with the <code>Duration.*</code> constructors. Advance it by a fixed step each tick to drive a deterministic day/night or season cycle.
```ludic
program Demo {
handler Step phase Update {
Clock.advance(Duration.minutes(1)) # one in-game minute per call
}
}
```

View file

@ -0,0 +1,23 @@
---
id: clock-now
name: Clock.now
category: clock
kind: namespace-method
tokens: Clock.now
sig: Clock.now() -> int
tip: The current simulated instant.
order: 0
ns: Clock
member: now
---
The current value of the simulated clock, as a <code>DateTime</code> (seconds since 1970). Deterministic &mdash; it returns exactly what the game last set or advanced it to, never the wall clock.
```ludic
program Demo {
handler Step phase Update {
let t = Clock.now()
let hour = DateTime.hour(t)
}
}
```

View file

@ -0,0 +1,22 @@
---
id: clock-reset
name: Clock.reset
category: clock
kind: namespace-method
tokens: Clock.reset
sig: Clock.reset()
tip: Reset the clock to the epoch (0).
order: 3
ns: Clock
member: reset
---
Resets the simulated clock back to <code>0</code> (the 1970 epoch). Handy at the start of a new run or a replay.
```ludic
program Demo {
handler Step phase Update {
Clock.reset()
}
}
```

View file

@ -0,0 +1,22 @@
---
id: clock-set
name: Clock.set
category: clock
kind: namespace-method
tokens: Clock.set
sig: Clock.set(t)
tip: Set the clock to an instant.
order: 1
ns: Clock
member: set
---
Sets the simulated clock to the instant <code>t</code>. Typically called once on load (e.g. from a saved <code>Time.now()</code>), after which gameplay advances it deterministically.
```ludic
program Demo {
handler Step phase Update {
Clock.set(DateTime.from(2026, 8, 30, 8, 0, 0))
}
}
```

View file

@ -0,0 +1,23 @@
---
id: datetime-format
name: DateTime.format
category: datetime
kind: namespace-method
tokens: DateTime.format
sig: DateTime.format(dt, pattern) -> string
tip: Render an instant as text using a token pattern.
order: 10
ns: DateTime
member: format
---
Renders the instant <code>dt</code> as a string, filling the tokens in <code>pattern</code> with zero-padded fields and copying every other character through verbatim. The pattern <b>must be a string literal</b> &mdash; it is expanded at compile time, so there is no runtime pattern scanner. Recognised tokens: <code>YYYY</code> (4-digit year), <code>YY</code> (2-digit year), <code>MM</code> (month), <code>DD</code> (day), <code>HH</code> (hour, 24h), <code>mm</code> (minute), <code>ss</code> (second).
```ludic
program Demo {
handler Step phase Update {
let t = DateTime.from(2026, 8, 30, 7, 5, 9)
let stamp = DateTime.format(t, "YYYY-MM-DD HH:mm:ss") # "2026-08-30 07:05:09"
}
}
```

View file

@ -0,0 +1,25 @@
---
id: datetime-parse
name: DateTime.parse
category: datetime
kind: namespace-method
tokens: DateTime.parse
sig: DateTime.parse(text, pattern) -> int
tip: Parse text into an instant; -1 on failure.
order: 11
ns: DateTime
member: parse
---
Parses <code>text</code> into a <code>DateTime</code> by reading each token of <code>pattern</code> from the fixed position the pattern lays out; missing fields default to 1970-01-01 00:00:00. Returns <code>-1</code> if a digit was expected but not found (so the caller can detect malformed input). The pattern <b>must be a string literal</b> and uses the same tokens as <code>DateTime.format</code>. <code>text</code> must match the pattern's fixed-width layout.
```ludic
program Demo {
handler Step phase Update {
let t = DateTime.parse("2026-08-30 07:05:09", "YYYY-MM-DD HH:mm:ss")
if t < 0 {
# malformed input
}
}
}
```

View file

@ -0,0 +1,29 @@
# offline_rewards.ludic — issue #9's worked example: an idle/farming game grants
# "you were away for N hours" rewards on load, using the Time/Date/Duration and
# the deterministic Clock. Everything is integer seconds, so the result replays
# identically — no wall clock, no floating point.
#
# bin/ludic examples/offline_rewards.ludic # prints 13 / 650 / 2026-08-30 / 0
program OfflineRewards {
entry {
# A save records when the player last quit. It is hardcoded here so the demo
# is deterministic; a real game writes Time.now() (or Clock.now()) at save.
let last_seen = DateTime.from(2026, 8, 29, 18, 30, 0)
# On load, set the game clock to "now". We drive it explicitly (rather than
# Time.now) so gameplay stays replay-safe and this sample is reproducible.
Clock.set(DateTime.from(2026, 8, 30, 7, 45, 0))
let away = Clock.now() - last_seen # a Duration in seconds (47700)
let hours = Duration.as_hours(away) # whole hours away
# Reward: 50 coins per full hour, capped at a day's worth.
let capped = Math.min(hours, 24)
let coins = capped * 50
print(hours) # 13
print(coins) # 650
print(DateTime.format(Clock.now(), "YYYY-MM-DD")) # 2026-08-30 (the day they returned)
print(DateTime.weekday(Clock.now())) # 0 = Sunday
}
}

View file

@ -126,10 +126,132 @@ function is_datetime_ns(meth: pointer) -> bool {
if (meth == "from") or (meth == "date") or (meth == "add") { return true }
if (meth == "year") or (meth == "month") or (meth == "day") or (meth == "weekday") { return true }
if (meth == "hour") or (meth == "minute") or (meth == "second") { return true }
if (meth == "format") or (meth == "parse") { return true }
return false
}
# concat two runtime string codes -> a fresh string code
function dt_concat(a: pointer, b: pointer) -> pointer {
return emit_bind(`call ptr @fn_str_concat(ptr {a}, ptr {b})`)
}
# does the pattern have token `tok` (length tlen) starting at index i?
function dt_tok_at(pat: pointer, n: int, i: int, tok: pointer, tlen: int) -> bool {
if (i + tlen) > n { return false }
var k = 0
while k < tlen { if pat[i + k] != tok[k] { return false }; k = k + 1 }
return true
}
# DateTime.format(t, "pattern") -> string. The pattern MUST be a string literal;
# the tokens YYYY / YY / MM / DD / HH / mm / ss expand to zero-padded fields and
# every other character is copied through verbatim. Expanded at compile time into
# a fold of @fn_str_concat over literal runs and @fn_dt_pad0 field conversions.
function emit_datetime_format(e: Node) -> Val {
g_uses_datert = true
g_uses_str = true
if e.kids[1].kind != E_STR { perr("DateTime.format needs a string-literal pattern") }
let t = emit_expr(e.kids[0])
let pat = e.kids[1].s
let n = len(pat)
# compute all six components once
let ed = dt_epochday(t.code)
let yp = emit_alloca("i32"); let mp = emit_alloca("i32"); let dp = emit_alloca("i32")
emit(` call void @fn_civil_from_days(i32 {ed}, ptr {yp}, ptr {mp}, ptr {dp})\n`)
let yv = emit_bind(`load i32, ptr {yp}`)
let mv = emit_bind(`load i32, ptr {mp}`)
let dv = emit_bind(`load i32, ptr {dp}`)
let sod = dt_secofday(t.code)
let hh = emit_bind(`sdiv i32 {sod}, 3600`)
let m3 = emit_bind(`srem i32 {sod}, 3600`)
let mi = emit_bind(`sdiv i32 {m3}, 60`)
let ss = emit_bind(`srem i32 {sod}, 60`)
let yy = emit_bind(`srem i32 {yv}, 100`)
var acc = emit_str_const("")
let lit = buf_new()
var i = 0
while i < n {
var field: pointer = null; var width = 0; var tlen = 0
if dt_tok_at(pat, n, i, "YYYY", 4) { field = yv; width = 4; tlen = 4 }
else { if dt_tok_at(pat, n, i, "YY", 2) { field = yy; width = 2; tlen = 2 }
else { if dt_tok_at(pat, n, i, "MM", 2) { field = mv; width = 2; tlen = 2 }
else { if dt_tok_at(pat, n, i, "DD", 2) { field = dv; width = 2; tlen = 2 }
else { if dt_tok_at(pat, n, i, "HH", 2) { field = hh; width = 2; tlen = 2 }
else { if dt_tok_at(pat, n, i, "mm", 2) { field = mi; width = 2; tlen = 2 }
else { if dt_tok_at(pat, n, i, "ss", 2) { field = ss; width = 2; tlen = 2 } } } } } } }
if (field == null) { buf_putc(lit, pat[i]); i = i + 1 }
else {
let ls = buf_str(lit)
if len(ls) > 0 { acc = dt_concat(acc, emit_str_const(ls)) }
let piece = emit_bind(`call ptr @fn_dt_pad0(i32 {field}, i32 {width})`)
acc = dt_concat(acc, piece)
lit.len = 0 # start a fresh literal run
i = i + tlen
}
}
let tail = buf_str(lit)
if len(tail) > 0 { acc = dt_concat(acc, emit_str_const(tail)) }
return val(acc, "string")
}
# DateTime.parse(s, "pattern") -> DateTime. The pattern MUST be a string literal;
# each field is read from a fixed offset (the layout the pattern fixes) and any
# non-digit where a digit is expected fails the parse, returning -1. Missing
# fields default to 1970-01-01 00:00:00.
function emit_datetime_parse(e: Node) -> Val {
g_uses_datert = true
if e.kids[1].kind != E_STR { perr("DateTime.parse needs a string-literal pattern") }
let s = emit_expr(e.kids[0])
let pat = e.kids[1].s
let n = len(pat)
let failp = emit_alloca("i32")
store_at("i32", "0", failp)
# component slots, seeded to the epoch defaults
let yp = emit_alloca("i32"); store_at("i32", "1970", yp)
let mp = emit_alloca("i32"); store_at("i32", "1", mp)
let dp = emit_alloca("i32"); store_at("i32", "1", dp)
let hp = emit_alloca("i32"); store_at("i32", "0", hp)
let np = emit_alloca("i32"); store_at("i32", "0", np)
let sp = emit_alloca("i32"); store_at("i32", "0", sp)
var off = 0
var i = 0
while i < n {
var slot: pointer = null; var width = 0; var tlen = 0
if dt_tok_at(pat, n, i, "YYYY", 4) { slot = yp; width = 4; tlen = 4 }
else { if dt_tok_at(pat, n, i, "MM", 2) { slot = mp; width = 2; tlen = 2 }
else { if dt_tok_at(pat, n, i, "DD", 2) { slot = dp; width = 2; tlen = 2 }
else { if dt_tok_at(pat, n, i, "HH", 2) { slot = hp; width = 2; tlen = 2 }
else { if dt_tok_at(pat, n, i, "mm", 2) { slot = np; width = 2; tlen = 2 }
else { if dt_tok_at(pat, n, i, "ss", 2) { slot = sp; width = 2; tlen = 2 } } } } } }
if (slot == null) { off = off + 1; i = i + 1 }
else {
let rd = emit_bind(`call i32 @fn_dt_rd(ptr {s.code}, i32 {itoa(off)}, i32 {width}, ptr {failp})`)
store_at("i32", rd, slot)
off = off + width
i = i + tlen
}
}
let y = emit_bind(`load i32, ptr {yp}`)
let mo = emit_bind(`load i32, ptr {mp}`)
let d = emit_bind(`load i32, ptr {dp}`)
let h = emit_bind(`load i32, ptr {hp}`)
let mn = emit_bind(`load i32, ptr {np}`)
let sc = emit_bind(`load i32, ptr {sp}`)
let ed = emit_bind(`call i32 @fn_days_from_civil(i32 {y}, i32 {mo}, i32 {d})`)
let days = emit_bind(`mul i32 {ed}, 86400`)
let hs = emit_bind(`mul i32 {h}, 3600`)
let ns = emit_bind(`mul i32 {mn}, 60`)
let a1 = emit_bind(`add i32 {days}, {hs}`)
let a2 = emit_bind(`add i32 {a1}, {ns}`)
let inst = emit_bind(`add i32 {a2}, {sc}`)
let failed = emit_bind(`load i32, ptr {failp}`)
let bad = emit_bind(`icmp ne i32 {failed}, 0`)
return val(emit_bind(`select i1 {bad}, i32 -1, i32 {inst}`), "int")
}
function emit_datetime_ns(meth: pointer, e: Node) -> Val {
if (meth == "format") { return emit_datetime_format(e) }
if (meth == "parse") { return emit_datetime_parse(e) }
if (meth == "from") { # from(year, month, day, hour, minute, second) -> DateTime
g_uses_datert = true
let y = emit_expr(e.kids[0]); let mo = emit_expr(e.kids[1]); let d = emit_expr(e.kids[2])
@ -259,4 +381,65 @@ function emit_datetime_prelude() -> void {
emith(" store i32 %dd, ptr %dp\n")
emith(" ret void\n")
emith("}\n")
# @fn_dt_pad0(v, w): a fresh, malloc'd, w-digit zero-padded decimal of v
# (v assumed non-negative). Backs DateTime.format's numeric fields.
emith("define ptr @fn_dt_pad0(i32 %v, i32 %w) {\n")
emith(" %we = zext i32 %w to i64\n")
emith(" %sz = add i64 %we, 1\n")
emith(" %buf = call ptr @malloc(i64 %sz)\n")
emith(" %endp = getelementptr i8, ptr %buf, i32 %w\n")
emith(" store i8 0, ptr %endp\n")
emith(" %kp = alloca i32\n %vp = alloca i32\n")
emith(" %km1 = sub i32 %w, 1\n store i32 %km1, ptr %kp\n store i32 %v, ptr %vp\n")
emith(" br label %loop\n")
emith("loop:\n %k = load i32, ptr %kp\n %kok = icmp sge i32 %k, 0\n br i1 %kok, label %body, label %done\n")
emith("body:\n %vv = load i32, ptr %vp\n %d = srem i32 %vv, 10\n %ch = add i32 %d, 48\n %ch8 = trunc i32 %ch to i8\n")
emith(" %cp = getelementptr i8, ptr %buf, i32 %k\n store i8 %ch8, ptr %cp\n")
emith(" %vn = sdiv i32 %vv, 10\n store i32 %vn, ptr %vp\n %kn = sub i32 %k, 1\n store i32 %kn, ptr %kp\n br label %loop\n")
emith("done:\n ret ptr %buf\n}\n")
# @fn_dt_rd(s, off, w, failp): read w decimal digits of s starting at off into an
# int; on any non-digit set *failp = 1. Backs DateTime.parse's fixed-width fields.
emith("define i32 @fn_dt_rd(ptr %s, i32 %off, i32 %w, ptr %failp) {\n")
emith(" %accp = alloca i32\n store i32 0, ptr %accp\n %kp = alloca i32\n store i32 0, ptr %kp\n")
emith(" br label %loop\n")
emith("loop:\n %k = load i32, ptr %kp\n %kok = icmp slt i32 %k, %w\n br i1 %kok, label %body, label %done\n")
emith("body:\n %idx = add i32 %off, %k\n %cp = getelementptr i8, ptr %s, i32 %idx\n %c8 = load i8, ptr %cp\n %c = sext i8 %c8 to i32\n")
emith(" %zero = icmp eq i32 %c, 0\n br i1 %zero, label %stop, label %digit\n") # stop at the terminator, never read past it
emith("stop:\n store i32 1, ptr %failp\n br label %done\n")
emith("digit:\n %lt = icmp slt i32 %c, 48\n %gt = icmp sgt i32 %c, 57\n %isbad = or i1 %lt, %gt\n br i1 %isbad, label %fail, label %ok\n")
emith("fail:\n store i32 1, ptr %failp\n br label %next\n")
emith("ok:\n %acc0 = load i32, ptr %accp\n %m10 = mul i32 %acc0, 10\n %dg = sub i32 %c, 48\n %acc1 = add i32 %m10, %dg\n store i32 %acc1, ptr %accp\n br label %next\n")
emith("next:\n %kn = add i32 %k, 1\n store i32 %kn, ptr %kp\n br label %loop\n")
emith("done:\n %r = load i32, ptr %accp\n ret i32 %r\n}\n")
}
# --- Clock.* — a game-controlled simulated clock -----------------------------
# @L_clock (declared in emit_head, universal) is a plain seconds counter the game
# owns: unlike Time.now / Time.since, it never touches the wall clock, so gameplay
# that reads Clock.now() is deterministic and replay-safe. Advance it however the
# simulation dictates (e.g. by Duration.* each tick), or set it outright.
function is_clock_ns(meth: pointer) -> bool {
if (meth == "now") or (meth == "set") or (meth == "advance") or (meth == "reset") { return true }
return false
}
function emit_clock_ns(meth: pointer, e: Node) -> Val {
if (meth == "now") { # the current simulated instant
return val(emit_bind("load i32, ptr @L_clock"), "int")
}
if (meth == "reset") { # back to the epoch (0)
emit(" store i32 0, ptr @L_clock\n")
return val("0", "void")
}
if (meth == "set") { # set the clock to instant t
let t = emit_expr(e.kids[0])
emit(` store i32 {t.code}, ptr @L_clock\n`)
return val("0", "void")
}
# advance: move the clock forward by a Duration (seconds)
let d = emit_expr(e.kids[0])
let cur = emit_bind("load i32, ptr @L_clock")
let nv = emit_bind(`add i32 {cur}, {d.code}`)
emit(` store i32 {nv}, ptr @L_clock\n`)
return val("0", "void")
}

View file

@ -247,6 +247,10 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
if is_datetime_ns(meth) { return emit_datetime_ns(meth, e) }
perr(`unknown builtin DateTime.{meth}`)
}
if (ns == "Clock") {
if is_clock_ns(meth) { return emit_clock_ns(meth, e) }
perr(`unknown builtin Clock.{meth}`)
}
var bare: pointer = null
let labels = new []pointer
if (ns == "Screen") {

View file

@ -70,6 +70,7 @@ function emit_header() -> void {
emith("@.fmt_line = private unnamed_addr constant [4 x i8] c\"%s\\0A\\00\"\n")
emith("@L_argc = internal global i32 0\n")
emith("@L_argv = internal global ptr null\n")
emith("@L_clock = internal global i32 0\n") # Clock.* — the game-controlled simulated clock
emith("@.gametitle = private unnamed_addr constant [")
emith(itoa(len(g_game_name) + 1)); emith(" x i8] c\""); emith(g_game_name); emith("\\00\"\n")
emith("%LSlice = type { ptr, i32, i32 }\n")

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,23 @@
program T {
entry {
let t = DateTime.from(2026, 8, 30, 7, 5, 9)
# format — tokens expand zero-padded, other chars pass through
print(DateTime.format(t, "YYYY-MM-DD")) # 2026-08-30
print(DateTime.format(t, "HH:mm:ss")) # 07:05:09
print(DateTime.format(t, "DD/MM/YY")) # 30/08/26
# parse — round-trips to the same instant; missing fields default to midnight
print(DateTime.parse("2026-08-30", "YYYY-MM-DD") - Date.to_epoch(Date.new(2026, 8, 30))) # 0
print(DateTime.parse("zz", "YYYY")) # -1 (non-digit -> parse fails)
# Clock — the game-controlled simulated clock
Clock.set(3600)
print(Clock.now()) # 3600
Clock.advance(Duration.minutes(30))
print(Clock.now()) # 5400
print(DateTime.minute(Clock.now())) # 30
Clock.reset()
print(Clock.now()) # 0
}
}

View file

@ -400,6 +400,14 @@
"datetime-weekday",
"datetime-hour",
"datetime-minute",
"datetime-second"
"datetime-second",
"datetime-format",
"datetime-parse"
],
"clock": [
"clock-now",
"clock-set",
"clock-advance",
"clock-reset"
]
}

View file

@ -1436,6 +1436,14 @@ program LudicLsp {
if (meth == "hour") { return "DateTime.hour(dt) -> int" }
if (meth == "minute") { return "DateTime.minute(dt) -> int" }
if (meth == "second") { return "DateTime.second(dt) -> int" }
if (meth == "format") { return "DateTime.format(dt, pattern) -> string" }
if (meth == "parse") { return "DateTime.parse(text, pattern) -> int" }
}
if (ns == "Clock") {
if (meth == "now") { return "Clock.now() -> int" }
if (meth == "set") { return "Clock.set(t)" }
if (meth == "advance") { return "Clock.advance(span)" }
if (meth == "reset") { return "Clock.reset()" }
}
if (ns == "Memory") {
if (meth == "bytes") { return "Memory.bytes(n) -> pointer" }

View file

@ -48,6 +48,7 @@ function cmd_selfhost_test() -> int {
sh_case("transcend", "2718 1 148 1000 0 2303 1024 1414 3 250 0 1000 1016")
sh_case("vector", "3 4 5 4 6 2 2 11 2 6 999 999 999 1570 5 10")
sh_case("datetime", "10800 330 172800 3 2 0 10957 2026 8 30 4 6 1 0 1 29 28 31 2 1 365 946729815 2000 1 1 12 30 15 6 10957 1")
sh_case("datetime2", "2026-08-30 07:05:09 30/08/26 0 -1 3600 5400 30 0")
sh_case("textsplit", "1 1 1 3 1 1 1 1 1 1 1")
sh_case("hash", "-2128831035 -468965076 114400290 114400290 0 -873187034 1095738169 0 1364076727 -2114883783 -845898438 -845898438 -78065325 -78057399 -3750763034362895579 -5808556873153909620 -4100651535478758590 -4100651535478758590 0 -5451962507482445012 7256831767414464289")
sh_case("long", "1000000000000 1000000000001 1000000000005 3000000000000 1 1 1 -1000000000000 1000000 13")

View file

@ -84,6 +84,9 @@ function cmd_test() -> int {
feat_case("detach", "", "15 1 25 0", "detach.ludic (attach/detach + @OnAttach/@OnDetach)")
feat_case("reason", "", "503 1009", "reason.ludic (@OnDespawn reason: Despawned vs Quit)")
# issue #9: the Time/Date/Duration/Clock stdlib, driven from its own `entry`.
net_case("offline_rewards", "13 650 2026-08-30 0")
# EV0b + NETWORKING N0–N6: each a self-contained pure-Ludic program, driven and
# asserted from its own `entry`. The transport is the compiler's built-in
# loopback, so a networked game runs with zero foreign code.