Snake (M27): lean ray-cast observation + net-guided multi-ply search — food@12 ~21 → ~78.6#11
Open
PieterjanDeClippel wants to merge 5 commits into
Open
Snake (M27): lean ray-cast observation + net-guided multi-ply search — food@12 ~21 → ~78.6#11PieterjanDeClippel wants to merge 5 commits into
PieterjanDeClippel wants to merge 5 commits into
Conversation
…s eval) The campaign runs its own authoritative 12x12 eval + save-best; the trainer's internal eval ran long shielded episodes and was ~halving throughput. Set the trainer EvalEvery off so the curriculum stages train ~2x faster. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the 9x9 obstacle/food patch (162 of 177 inputs, myopic ±4 cells, mostly zeros) with CodeBullet-style 8-direction ray-cast vision: per ray, food-on-line + 1/dist-to-body + 1/dist-to-wall (24 inputs of long-range awareness a fixed window can't give). Kept flood-fill (anti-trap), food/tail bearing, heading, length. ObservationSize 177 -> 39: leaner + the right info (rays + flood-fill). Size-invariant. Removed the dead patch consts + loop. 8 env tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The lean ray-cast obs trained to ~53 greedy (vs the shipped 50.7), confirming the reactive-DQN plateau is structural. So this takes the user's chosen route — the EfficientCube idea of searching on top of a learned value function, applied to Snake. SnakeSearchAgent (Environments/Snake): Snake is deterministic between food (the food RNG lives in the env's saved state), so look-ahead is exact. It simulates every legal line on a private env clone (SaveState/RestoreState) to ~20 plies, scores each leaf by flood-fill survivability (new SnakeEnv.FreeSpaceAhead, a hard self-trap penalty) + food eaten + the trained net's value + tiebreaks, and plays the best line's first move. The net is the leaf evaluator (net-guided, not brute force). Sweep findings (food@12, 12×12; greedy ≈ 50): depth is the lever (d20 sweet spot; deeper gets misranked under beam pruning), wider beam hurts, space-weight helps then saturates, net weight is marginal (search carries it; forward skipped when weight 0), and the starvation window is not the cap — self-traps beyond the horizon are. Shipped config (net-guided d20, beam 32, space 50): food@12 ≈ 78.6 (3.7× the original 21). - SnakeEnv: + FreeSpaceAhead (look-ahead survivability), + configurable starveLimitCells - SnakeSearchAgent / SnakeSearchOptions; SnakeSearchAgentTests (beats greedy, deterministic, legal) - Lab eval: --search/--depth/--beam/--w-*/--starve - SnakeModelService exposes the net; SnakeController.Live drives the demo via the planner - models/snake.dqn.ckpt -> lean 39-dim net (old 177-dim is incompatible; net + SnakeEnv ship together) Reaching a clean 100 is the open stretch — it needs a tail-reachability survival invariant / Hamiltonian endgame (not yet built). See PLAN M27. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bs API New public surface on this branch since master: the 39-dim ray-cast observation, SnakeEnv.FreeSpaceAhead + configurable starveLimitCells, and SnakeSearchAgent / SnakeSearchOptions (net-guided multi-ply look-ahead). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 tasks
PieterjanDeClippel
added a commit
that referenced
this pull request
Jul 5, 2026
…erence) (#24) * docs(m33): PRD+plan for client-side Snake & MountainCar AI; bump Polyglot 0.1.4->0.3.0 3-agent investigation + Polyglot 0.3.0 verification. 0.3.0 fixes all of #11 (transcendentals added, module imports now link, i32() TS wrap fixed) and #9 stays fixed — so MountainCar's transcendental fork dissolves (cos/tanh writable in a .pg; sub-ULP C#<->TS drift harmless post-cutover). FruitCake TS codegen verified content-identical on 0.3.0 (22 tests green). Filed remaining gaps as MintPlayer.Polyglot#11 (all addressed in 0.3.0). Plan: Snake first (clean full-Polyglot port), MountainCar second (now also uniform Polyglot). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(m33): Snake single-source .pg (SN0-SN3 core) — parked pending MintPlayer.Polyglot#14 snake_solver.pg ports SnakeEnv (dynamics + 177-dim observation + action mask incl. the anti-self-trap flood-fill shield) over flat Lists (body head-at-end, occupancy bool-list, BFS via list+cursor — no HashSet/LinkedList/Queue), plus PgDuelingNet + a masked-greedy chooseAction. It type-checks and generates correct C#/TS individually. BLOCKED from building alongside FruitCake by MintPlayer.Polyglot#14: with a 2nd .pg each transpiled .cs re-emits the prelude (Option/Some/None) → CS0101. Parked via PolyglotFile Remove so the branch stays green; delete the Remove + continue (facade/tests/director) once the prelude is deduped upstream. Env build green with Snake excluded. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(m33): unblock Snake .pg on Polyglot 0.3.1 (#14 fixed) — builds alongside FruitCake Bump 0.3.0->0.3.1 (shared __polyglot_prelude.cs + partial PolyglotProgram fix #14), drop the PolyglotFile Remove workaround. Fixes to snake_solver.pg: rename PgDuelingNet->PgSnakeNet (distinct name; a deliberate copy of FruitCake's net — one assembly, so a shared name would clash; a shared nn.pg is a future refactor), and rename a fill-loop var to avoid a C# i-scope clash (CS0136). Environments builds with both .pg; 22 FruitCake/Polyglot tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(m33): SnakeEnv is now a facade over the single-source PgSnakeEnv (SN1-SN3) Rewrite the C# SnakeEnv to delegate to the transpiled PgSnakeEnv (dynamics + 177-dim observation + action mask incl. the flood-fill shield now live once in snake_solver.pg, shared with the browser's generated snake_solver.ts). The facade re-adds host concerns: IEnvironment/IStatefulEnvironment API, the food RNG (Xoshiro, kept out of the single source via the core's needsFood signal so determinism holds), the head-first Body/state format, and the throw-on-illegal-action contract. Also commit the generated snake_solver.ts. All 9 SnakeEnvTests green (dynamics, tail-follow occupancy sync, eating/wall/mask/throw, same-seed obs, save/restore round-trip); Occupied() helper updated to read the core's occupancy list. Full solution builds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(m33): Snake AI fully client-side (SN4-SN6) — ship net, director, retire server SN4: ship the 177-dim net as ClientApp/public/snake-net.ckpt (LFS) + snake-net.ts parser (builds PgSnakeNet from the dueling-q checkpoint). SN5: SnakeDirector runs the whole AI in the browser — PgSnakeEnv dynamics + the flood-fill action mask + masked-greedy chooseAction over the loaded net, on a discrete tick. Rewire the Snake component's watch mode to it (drop the WebSocket/SnakeApi); both modes now run in-browser. SN6: delete SnakeController, SnakeModelService (+ Program.cs regs), snake-api.ts, the stale models/snake.dqn.ckpt, and SnakeApiTests (server WS/status path). EpisodeStreamer kept (MountainCar still uses it). Web builds; 32 Snake/FruitCake/Polyglot tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(m33): Snake flood-fill via relaxation (avoids Polyglot evolving-any TS7022) The BFS queue (List<i32> whose pushed values derive from reading the same list) transpiled to an untyped `let queue = []` with circular element inference -> TS7022 in the browser build. Rewrite reachableFreeSpace as iterative relaxation over the `seen` bool-list (BFS-equivalent; values come from a fixed 0..cells range, never a list read). Snake grids are small so O(cells^2) is fine. C# + TS regen; 9 SnakeEnvTests green (obs/mask unchanged). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(m33): Snake client-side AI DONE + verified (PRD/PLAN) Snake ships fully client-side (branch snake-clientside-ai): env+obs+mask+net single-sourced in snake_solver.pg, C# SnakeEnv facade, browser director, server path retired. Verified in browser (AI ate 40 / length ~35, 0 console errors, 0 /api/snake calls). MountainCar remains (now uniform-Polyglot since 0.3.0 added cos/tanh). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(m33): MountainCar AI fully client-side — single-source via Polyglot, retire server mountaincar_solver.pg single-sources the classic-control dynamics (cos), the normalised 2-dim observation, and the PPO policy forward (PgMlpNet: Linear+Tanh hidden, linear output, argmax) — cos/tanh now available in Polyglot 0.3.0+ (sub-ULP C#<->TS drift is harmless: no server twin, argmax decision; C# stays exact via Math.Cos). C# MountainCarEnv is a facade over PgMountainCarEnv (6 MountainCarEnvTests green, incl. the Gymnasium golden dynamics). Browser: ship models/mountaincar.ppo.ckpt -> public/mountaincar-net.ckpt (LFS) + a mlp .ckpt parser (mountaincar-net.ts -> PgMlpNet) + MountainCarDirector; rewire the watch mode off the WebSocket. Retire the server path: MountainCarController, MountainCarModelService (+ Program.cs regs), mountaincar-api.ts, stale net, MountainCarApiTests, and EpisodeStreamer (now unused). Removed app.UseWebSockets() — no server WS remains (all games client-side). Web builds; 38 env/Polyglot tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(m33): MountainCar client-side DONE — M33 complete (Snake + MountainCar) Both remaining WebSocket-AI games now run entirely in the browser; no server WebSocket remains anywhere (EpisodeStreamer + UseWebSockets removed). Verified in-browser (Snake ate 40; MountainCar reaches the flag). PRD status -> IMPLEMENTED; PLAN M33 both games done. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
PieterjanDeClippel
added a commit
that referenced
this pull request
Jul 10, 2026
…d@12) (#28) * feat(snake): M34 — net-guided look-ahead search, client-side (~50 → ~70 food@12) The reactive net is structurally capped at ~50 food@12 (M27 sweep); strength comes from planning, not training. Port PR #11's idea (net-guided multi-ply look-ahead) into the single-source snake_solver.pg — PR #11 itself is unmergeable (predates the M32/M33 Polyglot + client-side rewrite). chooseActionSearch: beam search over cloned envs with pure-survival leaf scoring (reuses the shipped reachableFreeSpace flood-fill); the trained net breaks ties between equally-safe root moves (one forward per move, not per node — per-node buys no strength for ~9x the latency). No retrain, no obs change. Runs in C# eval AND the browser director byte-identically. Measured (shipped 177-dim net, no retrain; greedy ~50): d12/b16 ~70 food@12 @ ~11ms/move (the sweet spot — d20/b32 misranks under beam pruning and scores worse). Browser-verified: plays strongly, no console errors. Snake tests 10/10. - Environments: chooseActionSearch + clone/simSpawnFood/freeSpaceAhead in the .pg; SnakeSearch.cs (public SnakeSearchConfig + ckpt->PgSnakeNet loader); SnakeEnv LoadSearchNet/ChooseActionSearch facade. - Web: snake-director.ts uses the search (safeMask off); snake_solver.ts regenerated; snake.html copy updated. - Lab: --search/--depth/--beam/--w-* eval path. - Docs: SNAKE_SEARCH_PRD.md, PLAN M34, PRD §15, and a Polyglot bug handoff (local-List type drop + multi-.pg incremental-rebuild prelude collision). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(snake): M34 anti-fragmentation search term — ~71 → ~81 food@12 (+14%) The biggest single lever found: score the FRACTION of currently-free cells still reachable (freeSpaceAhead / freeCount) in the search leaf — the reachability-ratio idea, applied in the planner rather than as a net input. It catches fragmentation the absolute `reachable < length` trap test misses (the snake cutting itself off from most of the board while its body still fits the pocket). Paired sweep (d12/b16), confirmed on a second seed base: SpaceRatioWeight 0=70.3/72.6 50k=75.8 100k=81.3/80.6 200k=82.2/79.2 400k=76.0 100k is the robust peak (+14%, single games now reach a near-full board 106-108); 400k over-weights connectivity and under-eats. Shipped SpaceRatioWeight=100000. - .pg: leafScoreSearch + chooseActionSearch take spaceRatioWeight; term added. - SnakeSearchConfig.SpaceRatioWeight default 100_000; Lab --w-ratio knob. - Director W_RATIO=100_000; snake_solver.ts regenerated. - Docs: SNAKE_SEARCH_PRD ledger/gate, PLAN M34, PRD §15 updated to ~81. Clean rebuild, Snake tests 10/10, strict-TS typecheck clean, browser-verified (plays monotonically, no console errors). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes the Snake demo AI substantially stronger by combining two changes, in line with the M27 goal of pushing toward 100 food on the 12×12 grid:
Lean ray-cast observation (already on this branch): replaced the myopic 9×9 egocentric patch (177 inputs, mostly zeros) with 8-direction ray-cast vision — per ray: food-on-line, 1/dist-to-body, 1/dist-to-wall — plus flood-fill, food/tail bearing+distance, heading, and length. ObservationSize 177 → 39, grid-size-invariant. A reactive DQN on this obs tops out at ~50 food (vs the old net's ~21), confirming the reactive plateau is structural.
Net-guided multi-ply look-ahead (
SnakeSearchAgent) — the EfficientCube idea of searching on top of a learned value function, applied to Snake. Snake is deterministic between food (the food RNG lives in the env's saved state), so look-ahead is exact: the agent simulates every legal line ~20 plies deep on a private env clone (SaveState/RestoreState), scores each leaf by flood-fill survivability (the newSnakeEnv.FreeSpaceAhead, a hard self-trap penalty) + food eaten + the trained net's value + tiebreaks, and plays the first move of the best line. The net is the leaf evaluator — genuinely net-guided, not brute force.Result: food@12 ≈ 78.6 — 3.7× the original 21, ~1.6× the reactive net, from the same trained net amplified by search.
What the sweep found
Changes
SnakeEnv: +FreeSpaceAhead(look-ahead survivability), + configurablestarveLimitCells(training stays tight; inference can widen). Lean 39-dim ray-cast observation.SnakeSearchAgent/SnakeSearchOptions(new) +SnakeSearchAgentTests(beats greedy with the same net, deterministic, always legal).--search/--depth/--beam/--w-food/--w-trap/--w-net/--w-space/--starve.SnakeModelServiceexposes the net;SnakeController.Livedrives the demo via the planner.models/snake.dqn.ckpt→ the lean 39-dim net (the old 177-dim net is incompatible with this obs; net +SnakeEnvship together so the fix(web): Snake 'stream closed' — refresh shipped checkpoints + reject incompatible net #10InputSizeguard stays satisfied).Environmentspackage bumped0.2.0 → 0.3.0(new public API).Testing
Honest ceiling / follow-up
Reaching a clean 100 needs a tail-reachability survival invariant (guarantee the head can always reach its own tail → follow it indefinitely) and/or explicit Hamiltonian-style endgame play. Not built here — proposed as the next step.
🤖 Generated with Claude Code