Snail Mode game mode (BS-cb89aee1)#129
Open
byte-the-bot wants to merge 2 commits into
Open
Conversation
Port of the canonical Go community map (BattlesnakeOfficial/rules maps/snail_mode.go, by coreyja and jlafayette): snakes leave a decaying trail of stacked hazards behind their tail as they move. - rules::snail::post_update_board implements the decay / store / restore update; rules::snail::execute_turn runs the standard stages plus the post-update, mirroring royale.rs. - Pending tails are stored Go-style as off-board points (y + height) inside board.hazards, keeping BoardState self-describing. - BoardState::on_board_hazards() gives consumers the filtered view so bookkeeping points never leak to snakes or the board viewer. - Unit tests (trail timing, decay, double-tail, eliminated snakes, head-skip, stacked damage, food negation), the worked 5-turn trail example, and property tests with an independent per-square oracle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d frames
- create_initial_game: GameType::SnailMode arm (internal ruleset_name
"snail_mode", hazard_damage_per_turn 14 -- the Go rules default for
map-driven hazards).
- Per-turn dispatch in run_game_with_random_moves and apply_turn now
matches on game.meta.ruleset_name ("royale" / "snail_mode" / standard
fallback), covering both the random runner and the live game loop.
- wire.rs: snail games serialize ruleset.name = "standard" with
game.map = "snail_mode" for play.battlesnake.com parity (community
snakes key off game.map); off-board pending-tail bookkeeping points
are filtered so snakes never receive out-of-bounds hazards.
- frame.rs: frames carry only on-board hazards (stacked duplicates
included); frames are never read back into engine state, so the
bookkeeping points are excluded from persistence.
Co-Authored-By: Claude Fable 5 <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.
What
Implements Snail Mode: snakes leave a decaying trail of stacked hazards behind their tail as they move. Ported from the canonical Go community map (
maps/snail_mode.go, authored by coreyja and jlafayette) and verified against the real Go source (no Go test file exists — the implementation is the spec, so this PR ships thorough coverage of its own).Each turn, after the standard pipeline (move / health / hazard damage / feed / eliminate):
Points inboard.hazards;standard::damage_hazardsalready applies damage per entry, so a fresh length-N trail square deals N ×hazardDamagePerTurn(14, the Go rules default for map-driven hazards).Food spawns normally, and food on a trail square negates that square's hazard damage (already handled in
standard::damage_hazards).State-representation decision
Pending tails are stored Go-style, as off-board points (
y + board.height) insideboard.hazards, rather than in a dedicated side-channel field. Reasons:BoardStatefully self-describing, sorules::snail::execute_turn(board, moves, settings)needs no extra mutable state parameter and the trail state flows through every game loop (random runner, liveapply_turnloop) for free.hazardsduring the damage phase, where they can only ever match a head that is itself out of bounds and about to be eliminated — same as Go).run_gameholdsEngineGamein memory for the entire game and never reconstructs board state from persisted frames — a retriedGameRunnerJobon aRunninggame callsreset_game_state_for_retry(wipes turns/placements) and replays from turn 0, and aFinishedgame short-circuits to idempotent post-completion hooks. So pending-tail state does NOT need to round-trip through frame persistence, and frames stay a write-only feed for the board viewer.Both serialization boundaries filter through the new
BoardState::on_board_hazards()view:wire.rs(/move payloads) — snakes never receive out-of-bounds hazard points (hard requirement; test-enforced).frame.rs(persisted frames / board viewer) — frames carry only real on-board hazards, stacked duplicates included, matching what play.battlesnake.com showed. Off-board bookkeeping points are excluded from frames because nothing reads frames back into engine state and they would only confuse the viewer and any other frame consumer.Wire-protocol parity (
game.map)On play.battlesnake.com, snail was a map, not a ruleset: snakes saw
ruleset.name = "standard"withgame.map = "snail_mode". Arena's wire layer already had amapfield (always empty). This PR keeps that upstream parity: internally the engine dispatches onmeta.ruleset_name == "snail_mode", but the wire sendsruleset.name = "standard"+game.map = "snail_mode", so existing community snakes that key offgame.mapbehave correctly. Other modes are unchanged (mapstays"", ruleset name passes through).Deviations from Go (documented in module docs)
doubleTailwould panic on a 1-segment snake; this port treats bodies shorter than 2 as doubled (no trail). Unreachable in real games.StandardMap.PostUpdateBoardalso spawns food; the arena engine spawns food separately (same as every other mode here), so the snail post-update is hazards-only.Tests
rules::snailunit tests: trail appears with stack = snake length one turn after recording; decay by exactly 1/turn/square until gone; double-tail (just ate / spawn-stacked) spawns no trail; eliminated snakes leave no new trail (but their previously recorded trail still lands); head-occupied squares skip placement and the stack is dropped, not deferred; multiple snakes' trails stack independently; per-stack-entry damage (3×14 on a fresh length-3 square) and lethal-stack elimination cause; food negates trail damage; early game-over exit.with_cases, no committed seed files): an independent per-square oracle (new = max(old−1,0) + restored·(no live head)), off-board entries exactly equal live non-double-tail snakes' tails × length, full decay without snakes, determinism, and no malformed hazard entries.create_initial_gamesnail settings; live-loop-style frame test (trails appear in frames, off-board points never leak); random-runner smoke test; wire parity tests (standard+snail_modemap, other modes unchanged, off-board filter).cargo fmt --all,cargo clippy --workspace --all-targets(clean),cargo test -p rules(91 passed), fullcargo test -p arena --bin arena(420 passed) all green.Merge note
This PR trivially conflicts with the parallel Constrictor PR in the
server/src/engine/mod.rsdispatch matches (create_initial_gameand the per-turnmatch game.meta.ruleset_name.as_str()): both PRs make the identical refactor and each adds its own arm, so the resolution is a mechanical union of arms.🤖 Generated with Claude Code