T6-1: al_sdk::code — a starter harness + combat verb layer (CODE reads like JS CODE) #42
Loading…
Reference in a new issue
No description provided.
Delete branch "task/37-code-verb-layer"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this is
Issue #37 (T6-1). Rust CODE read like a graphics driver:
examples/hellospent ~150 lines onconnect_env→ sink →on_death→auto_respawn→on_tick_async→run_for→ exit table before a single tick ran, and the predicates made a user hand-buildRangeInput/AttackInput/MonsterArgs. The JS starter (htmls/contents/codes/default_code.js) spends one line on scaffold (setInterval(fn, 250)) and its body is verbs.This PR adds
al_sdk::code— a starter harness and a combat verb layer — and ports both example crates onto it.The new shape
code::Harness— the scaffold, onceOwns all eight:
connect_env(with a plain-language failure on stderr), the stdout sink ({prefix}: {message}, one line per log line — whatal runstreams verbatim and what the browser's log panel shows),on_death(log + count),auto_respawn,on_tick_asyncat your interval, the run loop, Ctrl-C / SIGTERM, and the summary + exit code.Two decisions worth a reviewer's eye:
Game::run— the JS's "keep running" semantics — and only a signal or your ownStopTokenends it. The signal watch is therefore on by default: it turns Ctrl-C / SIGTERM intorequest_stop, so a run ends as a decision (exit0) rather than a signal death, which is the one distinctional-runtime's restart budget keys on.0, feed died mid-run2, refused login or a loop that never started1.2for a drop is deliberate (0would tell the supervisor "this program decided to stop"). A crate whose own docs promise otherwise maps it withHarness::exit—examples/hellodoes exactly that, and keeps its documented "a mid-demo drop is a clean-ish end" promise.SummaryandOutcomeare separate types so a crate can render its own line and map its own codes;window_envnames a typo or a0on stderr rather than turning it into a hang or a silent zero-length run.code::verbs— the JS surface, one call eachengage(the wholeif (!is_in_range) move(half-way) else if (can_attack) attackladder, with every refusal named instead of silent),pick_target,change_target/clear_target,walk_to(one honest clamped leg),loot/loot_chest/chestsover adrop-fed chest table carrying the JS's two safety rails (LOOT_MIN_INTERVAL,LOOT_MAX_PER_CALL,LOOT_REACH),attack_when_ready, and borrowed twins onSnapshot(targeted_monster,midpoint,step_toward).verbs.rsopens with the JS-name → Rust-call table. Nothing invents a frame: each verb wraps an existing SDK primitive, or one of the two wire framesOutbounddoes not model yet (target,open_chest) throughsend_event, with the server-side line reference beside it. Banking / crafting / shops / duels are explicitly out of scope, as the brief scopes them.Also in this PR
examples/helloported onto the harness (acceptance), demo and contract intact: same tick, same walk, same summary fields, same exit table — now throughHarness::exit. Itsstep_towardis replaced bySnapshot::step_toward. Its offline test now also asserts the credentials hint is absent on a refused handshake (that hint explains a missing trio, not a dead socket).al serve:ALLOWED_CRATESgainsexamples/starterandDEFAULT_CRATEmoves to it, so Compile → Run starts on the crate whose body is the loop. Both pinned in the existing defaults test.rust/PLAN.md/rust/ARCHITECTURE.mdnote the new module and crate.Verification
bash rust/dev/check.shclippy -D warningsworkspace--all-targets, all tests, 28 doctests)drop/chest_opened/new_map/startchest fold, leg clamp + heading, the rate limit, every refusal naming itself)cargo test -p startercargo test -p starter -- --ignoredattacking12× and endedoutcome=finishedexit 0; an open-ended run sent SIGTERM and printedoutcome=stopped, exit 0cargo test -p hello -- --ignoredmoved=240.0 legs=2 arrived=true, exit 0)cargo test -p al-sdk --test live_game -- --ignored(AL_TEST_REQUIRE=1)cargo test -p al-tool --test live_serve --test live_cli -- --ignoredhello;al build+al runstill work)al run examples/starter --creds dev/.local --once --buildsummary ticks=23 outcome=finished deaths=0 reconnects=0→[al] exit 0: the child exited cleanly (0 restarts)al run examples/hello --creds dev/.local --oncesummary … arrived=true moved=240.0 legs=2→exit 0al serve --crate examples/starter/optionslists starter first withdefault_crate: examples/starter;/source?crate=examples/starterreturns starter's real textLayering rule held: both user crates depend on
al-sdkonly, plustokioas the process runtime (same exceptionhellodocumented). No new crate, noal-client/al-protocolname in either crate's source. Nounwrap/expectoutside tests, notodo!, library errors staythiserror.Notes for the reviewer
broadcastsubscriber on each read rather than by a spawned task, because aGameis borrowed by every tick handler so nothing wanting&Gamefor its whole life can betokio::spawned — the same reasonon_tick_asyncexists.Game::chestsis deliberately sync for that reason.engage's refusals are values (Engage::Guarded/Nowhere/Failed), not log lines, so a farm can count them; the starter prints the reason as its status line.Failed: exception, so both take a process-wide "one live run at a time" lock (poison-tolerant: a failing live test must not wedge its sibling).Co-Authored-By: Claude noreply@anthropic.com
99a55d508d7612e4dc3a