Skip to content

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
masterfrom
snake-ray-obs
Open

Snake (M27): lean ray-cast observation + net-guided multi-ply search — food@12 ~21 → ~78.6#11
PieterjanDeClippel wants to merge 5 commits into
masterfrom
snake-ray-obs

Conversation

@PieterjanDeClippel

Copy link
Copy Markdown
Member

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:

  1. 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.

  2. 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 new SnakeEnv.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.63.7× the original 21, ~1.6× the reactive net, from the same trained net amplified by search.

What the sweep found

  • Depth is the lever — 65 (d6) → 71 (d10) → ~79–84 (d20); d28/d36 give no reliable gain (deep lines get misranked under beam pruning). d20 is the sweet spot.
  • Wider beam hurts (keeps greedy food-grabs that trap later); space-weight helps then saturates; net weight is marginal (the survival search carries it — the forward is skipped when its weight is 0).
  • Starvation window is not the cap — widening it 2→4→8 saturates at ~80. The residual cap is self-traps that form beyond the horizon.

Changes

  • SnakeEnv: + FreeSpaceAhead (look-ahead survivability), + configurable starveLimitCells (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).
  • Lab eval: --search/--depth/--beam/--w-food/--w-trap/--w-net/--w-space/--starve.
  • SnakeModelService exposes the net; SnakeController.Live drives 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 + SnakeEnv ship together so the fix(web): Snake 'stream closed' — refresh shipped checkpoints + reject incompatible net #10 InputSize guard stays satisfied).
  • Environments package bumped 0.2.0 → 0.3.0 (new public API).
  • PRD §14 + PLAN M27 updated.

Testing

  • 269/269 fast tests pass; all Snake tests pass (fast + the slow campaign-contract tests).
  • One unrelated slow training-threshold gate for another game failed in the full slow run (a known-noisy class; outside the Snake changes — the diff touches no non-Snake training code). Worth a confirming re-run before merge.

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

PieterjanDeClippel and others added 5 commits June 22, 2026 09:57
…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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant