Run slither-sim serve from a release archive
without Rust, or cargo run --release --locked -- serve from a checkout. One
controller owns independent worlds and all their player slots. Send one UTF-8
JSON object per stdin line; read one stdout reply per request. EOF stops the
process. Rust callers can use in-process sessions or the
library directly.
{"protocol":"slither-sim-json-v6","result":{}}
{"protocol":"slither-sim-json-v6","error":{"code":11,"rule":null,"world":null,"slot":null,"field":null,"message":"Reset first"},"sessionEnded":false}Check protocol. Requests, configs and actions reject unknown fields; response
readers ignore additional fields. Version 6 removes the former spawn-order
food guarantee; list-order tie breaks and floating-point reductions in bot
policies may therefore choose different actions. Error message is descriptive; numeric code, optional rule and locations are stable.
I/O failures stop the stream. Use trusted local controllers.
Every error reply includes sessionEnded. When false, no world advanced and
the session is unchanged. When true, advancement or frame collection failed
(for example, accumulated pickup mass overflowed); all worlds were cleared.
Reset before stepping again. The stream survives malformed requests, rejections
and terminal session failures.
Worlds step in parallel, one task per world, using every core;
slither-sim serve --threads N caps threads without changing replies.
{"op":"info"}Works before reset. Returns version, rulesRevision (slither-sim-rules-v6), defaultConfig, defaultDecisionTicks,
maxAgents, maxWorlds, maxDecisionTicks, the food origins codes and the
observation precision. Representation limits: maxAgents and
maxDecisionTicks are 4294967295 (u32 slot indices and tick counts;
u32::MAX means no slot). maxWorlds is the platform's usize::MAX;
worlds × agents must fit usize, with memory limiting practical world counts.
{"op":"reset","seed":7,"worlds":1,"agents":2,"decisionTicks":6,"config":{"radius":1000,"dynamicRadius":false,"foodCount":350}}All fields except op are optional. Defaults: seed 1, one world, four agents,
six decision ticks and Config::official(1). Every world shares agents,
decisionTicks and config; world w uses seed seed + w (wrapping at 2³²).
config and nested objects use camelCase world settings
with defaults for omitted fields and an optional preset,
such as {"preset":"battledome"}. Replies print resolved fields.
Default and official worlds are first sized for agents
(population sizing).
Invalid resets preserve the previous session.
Returns seed, the resolved config, decisionTicks, warnings and worlds,
one frame per world. warnings lists valid but unmeasured settings
(limits), such as dt outside the measured server
loop; otherwise it is empty. serve also prints warnings to stderr.
Snakes start on a deterministic ring halfway to the boundary, facing inward:
a synthetic test setup. With config.spawnRadius, they instead lie and head
counterclockwise along that ring; with config.minScore, they start at its floor.
Optional placement defaults to {"kind":"testRing"}. This checks containment
but makes no neighbor-clearance promise. {"kind":"admitted"} instead tries
64 sampled poses per slot with preferred clearance 600; override these with
attempts (positive u32) and preferredClearance (finite, nonnegative).
It selects the first pose meeting that clearance, otherwise the first best
positive contact-margin pose. Exhaustion rejects the entire reset. Placement
uses a separate seed stream (seed ^ 0x706c6163), leaving food/prey draws intact.
This bounded core policy does not change host admission or retry behavior.
The same build, config, seed and actions produce the same worlds. Cross-platform bitwise floating-point equivalence is not promised; retain these inputs and the commit when recording results.
{"op":"step","actions":[{"heading":[3.141592653589793,0],"boost":[false,true]}]}actions has one entry per world, each with a heading and a boost array of
one entry per agent slot. Living slots require a finite heading in radians; dead
slots may use null. Headings are absolute: zero is +x and pi/2 is +y. The
whole request is validated before any world advances.
Each world holds its actions for decisionTicks physics ticks, stopping early if
all its snakes die. Time follows simulation ticks, independent of controller
compute time. config.dt is seconds per tick; defaults give a 42 ms decision
interval. There is no network delay, automatic respawn, reward, victory
condition or episode timeout. Use reset for new worlds.
Returns worlds, one frame per world. Frame rows, counts and offsets are u32.
Unrepresentable frames, numeric overflow and reply allocation failure end
the session after advancement, as described above.
{"op":"snapshot","world":0}Returns world world's (default 0) whole-world config, live radius, time, snakes,
food, prey and cumulative deaths at full precision, with string slot IDs.
This debug view excludes internal motion/RNG state and is not a restorable
checkpoint. config.radius retains the initial setting after startup sizing;
radius follows samples and the controller. This changed config meaning is the
geometric v4 cutover from v3. Keep snapshots out of limited-visibility bot evaluations.
Geometric v5 replaces the v4 error string with an object containing code,
rule, world, slot, field and message. Absent rule/location values are
null. Indices are zero-based; fields retain indexed paths. sessionEnded
remains outside the object and preserves the rejected-versus-ended lifecycle.
C inspection and Python exceptions use the same cause and location values.
Kind (ErrorCode) |
Code |
|---|---|
| InvalidRequest | 1 |
| InvalidConfig | 2 |
| ActionShape | 3 |
| InvalidAction | 4 |
| UnknownSlot / UnknownWorld / UnknownEntity | 5 / 6 / 7 |
| DuplicateId / InvalidState / PlacementExhausted | 8 / 9 / 10 |
| NotReset / WorldStopped | 11 / 12 |
| UnsupportedCheckpoint / InvalidCheckpoint | 13 / 14 |
| NumericOverflow / RepresentationLimit / AllocationFailed | 15 / 16 / 17 |
Constraint codes are Missing 1, NonFinite 2, Negative 3, NonPositive 4,
OutOfRange 5, LengthMismatch 6, Duplicate 7, Ordering 8, OutsideArena 9 and
InconsistentState 10. Codes identify kinds/constraints; diagnostic messages
may change. Location fields name the input being validated: native flat-array
shape errors use headings/boosts, while nested JSON array errors name the
world's heading/boost field. Future codes are additive; handle unknown values without treating
an error as success. Codes 18/19 are reserved for C pointer/panic boundaries.
A frame stores visible entities once in columns; each living slot's view indexes those rows.
| Member | Meaning |
|---|---|
time, ticks |
World time, and ticks advanced by this request (0 on reset). |
living |
Surviving snakes. |
deaths |
Death events from this request: time, victim and killer slots, reason and dropped food. |
radius, vision |
Live arena radius around the origin, and the vision radius. |
rules |
One world-rules object, shared by every slot in this frame; see below. |
snakes |
One row per snake that some living slot sees, including each slot's own. |
food, prey |
One row per item that some living slot sees. |
views |
One entry per slot: null for dead slots, otherwise self (its own snakes row) and the snakes, food and prey rows it sees. |
Every frame includes rules, even with no living slots:
{"boundaryMode":"sliding","pitRadius":null,"minScore":null,"killScoreSeconds":null,"freeBoostAtFloor":false}boundaryMode is sliding or hard; the optional numbers are null when absent.
freeBoostAtFloor is true when the configured permanent minScore floor makes
boosting free at that floor. Overrides are reflected directly; a preset name
is not a rules identity. Radius remains separate runtime state;
rules.pitRadius is the live pit radius and follows the wall in seat-following
circular floor worlds. The other mode settings remain fixed until the world is
replaced. Portable received-state ClientFrame.rules optionally carries the same
object through the SlitherSim wire extension.
It is unknown until received. Geometric JSON, Python and C ABI rule fields retain
their existing meanings and layouts.
Columns are arrays of equal length; row r is element r of each.
| Table | Columns |
|---|---|
snakes |
slot, head x/y, heading, speed, radius, length, segments, fam, score, kills, killScore; anchors bodyX/bodyY |
food |
x, y, value, size, origin (index into info.origins: ambient, boost, death) |
prey |
id, x, y, heading, targetHeading, speed, size |
segments: whole body segments.fam: growth not yet turned into a segment, in segment units; it can briefly exceed 1.length:segments + fam, the body length in segments, not a distance.score: the size points the game displays, fromsegmentsandfam.- Food
value: the gain for a minimum-size eater (length2); larger eaters gain less. Foodsize, when present, is the protocol pellet size, which sets the display and the size-dependent gain; without it the eater gainsvalue.
kills counts the current life's kills. killScore counts the slot's kills
within the last config.killScoreSeconds, across lives, and is 0 without that
window. Death reason is body, boundary or pit (only with config.pit).
Row r's body anchors are bodyX[bodyStart[r]:bodyStart[r+1]] and the same
range of bodyY, head to tail; bodyStart has one more entry than there are rows.
Rows and view lists follow current world storage order. Opponents follow slots;
food has no chronological order and pickup removal may move a tail item into
an earlier row. Row indices remain positions, not persistent identities.
Coordinates use world units; speeds use world units per second.
Food and prey are filtered by the vision circle. If an opponent's head or any anchor is visible, its entire stored snake record is included, even outside that circle. This authoritative simulation data includes metadata that may be unavailable to network clients.
The controller can read every slot and debug snapshots. A shared competition must separately restrict observations, isolate bots and define timing and disconnection rules.
Frames round coordinates (head, anchors, food, prey) to 0.01 world units,
headings to 0.0001 radians and speeds to 0.01 units per second; every other
number is exact. info.precision reports these steps. Rounding happens only
when a frame is written; simulation and snapshots keep full precision.
Nearest rounding changes a heading by at most 0.00005 radians and a point by at most 0.0071 world units. The precision study compares those bounds with packet grids and simulation scales. Rounding can still change a bot's choice at a decision threshold.
See the Python and Node.js controllers.
serve does not accept binary WebSocket clients or reproduce received packet
coverage, prediction, receipt ages or network latency. Use the
portable bot API for received-state inputs and the
WebSocket host for socket connections.
Simulator::new(WorldOptions { seed, config, population }) initializes a world;
see the basic example. InitialPopulation selects
Empty, Dormant { ids }, or Placed { ids, placement }. IDs are unique,
ordered, stable slots. Dormant slots are inactive and have no death timestamp.
Placement::default() is TestRing; Admitted uses the bounded search above.
Generated ecology is independent of the initial snake population. Configure
Config::official(n) explicitly when its ecology should be sized for n snakes.
Simulator fields are private. config(), snakes(), food(), prey(),
deaths(), pickup_events() and food_spawn_events() borrow existing storage;
radius() and time() return scalars. Config retains its existing flat keys
and retains the initial radius after startup sizing; radius() reports the live
boundary. Radius evolution keeps the initial Config as the density reference;
seat-following derives effective ecology targets from live area. An explicit
set_ambient_rate updates the initial-area attempt rate. No mutable slices or config setters are
exposed. Detached Snake, Food and Prey values remain editable.
world.rules() returns a copyable WorldRules without allocating.
world.geometric_context(slot)? returns those rules and the slot's rolling
kill_score, including credits retained across lives and their expiration.
Pass it beside world.observe(slot) to a raw Rust bot driver. It is not added
to Observation or Snake; unknown slots return UnknownSlot with the slot.
Use append_dormant, spawn, retire and the existing respawn methods for
slot lifecycle. spawn(id, body, heading) validates geometry, mass floor and
containment before appending every companion together. retire(slot) marks a
slot inactive without death food or a fabricated death event; slots never move.
Respawn and new replay lives clear per-life boost, speed, kiosk, pit and death
state and assign new join order, retaining rolling kill history.
import_life(slot, ReplayLife) installs a checked new replay life atomically,
including InitialSpeed::Infer(observed_speed) or Known(SpeedState).
ReplayLife::from_observed takes a detached snake and explicit speed choice;
body length supplies segment count, and mass supplies display values. The old
add, live update_mass, and two-call initialize_speed_state are removed.
correct_mass(slot, body, fam) and correct_pose(slot, head, body, heading)
preserve speed/progress, boost, kiosk credit/timers, join order, kills and pit
history. Pose correction retains segment count. Replay corrections allow finite
overlaps and out-of-arena geometry within the checkpoint continuation envelope
(positions at most 2e150 for living snakes and 3e150 for inactive snakes/food;
headings and snake speed × dt at most 1e150 in magnitude). The 430-segment
spawn limit is not a replay cap.
from_scenario(seed, config, Scenario { snakes, food, prey }) installs ordered
(id, ReplayLife) slots and ReplayPrey { prey, mode } at time zero without
generated ecology or placement draws. It labels supplied state; exact historical
continuation uses checkpoints. Checked replace_food, insert_food and
remove_food preserve order and invalidate occupancy caches; indices are immediate
positions, not durable identities. Replacement and scenario food establish birth
order from the supplied list; checkpoints retain existing birth identity. The corresponding prey operations enforce
unique IDs and synchronize modes/decision state. record_events(EventRecording)
enables step-local pickup/birth telemetry, retaining capacity when already enabled.
deaths() remains cumulative history for cursor-based consumers.
Use observe_all_into or observe_selected_into to reuse a buffer's vectors
and avoid most per-call allocation, with the same observations as observe_all
and observe_selected. Windows bot loops ran 1.8–3.5× faster with
observe_all_into than observe_all; see workloads.
Set default-features = false on a path or Git dependency for the core alone:
no WebSocket/TLS dependencies, client module or CLI binary. Core simulation,
observations, JSON transport and the basic/fidelity_replay examples remain
available. networking is enabled by default and restores the full interface.
Simulator::set_parallel(true) lets one world use the current
rayon pool without changing results. Large
observe_all and observe_selected batches split by observer, and steps with
at least 4,096 living snakes split by snake
(parallel step). It defaults off to avoid
competing with parallel worlds. Size the pool
with RAYON_NUM_THREADS or use ThreadPool::install. transport::serve steps
worlds on the same pool. Both need the default-enabled parallel feature.
cargo build --lib --no-default-features --lockedBefore embedding:
- Pin a dependency version or Git revision; record the resolved
Configand seed. Defaults and seeded trajectories can change between versions. Choose settings explicitly, including geometricvision. - Handle fallible core calls before committing related command queues or timers in your application. A rejected operation preserves the simulator world, not changes you already made to your own driver state.
- Define admission and respawn policy in your application.
| Method | Purpose |
|---|---|
observe_selected(&mask) |
Shared-index batch observations, one result per slot. Unselected/dead slots are None; wrong mask length is an error. |
body_is_clear(&body, clearance) |
Check all supplied edges against living snakes and the 60-unit arena margin; invalid input returns false. |
respawn_body(slot, body, heading) |
Replace a dead slot using supplied geometry and reset per-life motion, boost, penalty, death-time and join-order state. With minScore, the body needs at least the floor's segments; at exactly that many, the life starts at the floor's mass. |
spawn_body(head, heading) |
The configured new-life body and heading at head. With spawnRadius, its anchors lie along the circle through head, turning the way heading leans, heading along it; an error if that circle cannot hold them. Otherwise two straight segments behind heading. Check it with body_is_clear. |
set_ambient_rate(attempts_per_second) |
Change initial-area refill rate while preserving fractional placement credit; seat-following scales it with live area. Zero discards pending credit and disables refill. |
Supplied bodies are head-first, one anchor per segment, with 2–430 finite points.
Respawn requires all points inside the arena; it does not enforce clearance or
reject self-intersections. Call body_is_clear separately for spawn exclusion.
The straight-body spawn_is_clear and respawn methods remain available.
Refill rates must be finite and nonnegative, small enough that one attempt
interval still changes the refill clock, and large enough to represent the
rescaled pending clock. Use the setter instead of directly
editing config.ambient_replenish_per_second when changing a running world.
Rejected operations leave the world unchanged. Numeric overflow during a tick
(pickup mass or prey motion) completes that tick without the invalid update,
returns CoreStepError::Stopped(cause), and every later step returns
CoreStepError::Rejected with ErrorKind::WorldStopped. The original cause stays
available through Simulator::status(): WorldStatus::Running or
WorldStatus::Stopped(&SimError). Action validation failures return
CoreStepError::Rejected(cause) before any state changes. step_profiled has the
same lifecycle.
Other fallible core calls return SimError. Replace Result<_, String>
propagation and prose matching with SimError (or a boxed std::error::Error)
and checks on kind/code(). Causes carry optional zero-based world/slot
and string field locations, a Constraint where applicable, and
expected/actual counts for action shape errors. Config fields use JSON names,
with indexed paths for radius samples. ErrorCode numeric values 1–17 and
as_str() names are stable; message wording is diagnostic. Display/Error
retain human messages and chained causes. Successful steps allocate no error
strings. JSON, C and Python error handling is unchanged by the radius split;
geometric v5 now carries stable cause codes and locations.
The dependency-free bot feature exposes bot::Forager::new() and
Forager::default(), both using the adopted A4-mean parameters. The
decide(&mut self, &Observation, &Timing, &[Intent]) -> Action method uses the
received observation and explicit timing.
Timing, Sent and Intent live in bot; opponent intentions follow observation
order, with Intent::default() representing unknown wanted heading.
Parameters are private and fully resolved for the named release.
Use default-features = false, features = ["bot"] for an offline policy without
networking or Rayon. Core-only, C ABI and Python builds do not enable bot.
networking, enabled by default, includes it and exposes client::ForagerBot::new()
and ForagerBot::default(), implementing the existing client::Bot. The adapter derives timing and intentions
from the complete ClientFrame and returns None outside a compatible playing
frame with an observation. Host bots, native background bots and the Rust
examples use this default adapter; JSON operations are unchanged.
Host, native and socket bots use ForagerBot::new() and read the exact live pit
from ClientFrame.rules to enter the one-way pit and treat its edge as a wall
from inside. The configuration constructor with_world_rules has been removed.
NetworkBackend opts into rule reports automatically; other socket clients send
the physical-food header.
Offline callers can supply the live pit radius to Forager::decide_with_pit.
Without a pit these calls preserve the frozen A4 decisions.
One policy/adapter instance can be reused for sequential decisions across seats: scratch buffers retain no policy state. Keep each seat's received frame, sent commands and lifecycle separate. This heuristic does not guarantee safety; cross-platform floating-point identity is subject to OS math libraries. The port evidence pins the original kernel, immutable parameters, bounded fixtures and validation limits. The A4 adoption note records selected held-out gameplay and cost results from private tuning tooling, including the maintainer's live-mean cost gate and the earlier tail-limit failures.
Use serde_json::to_vec(&world.checkpoint()?) for exact core continuation;
Simulator and its runtime controls no longer implement serde. Load with
Simulator::from_checkpoint(&bytes, RestoreOptions::default()), or use
world.restore(&bytes, options) to validate a complete candidate before replacing
the current world. Export also validates the complete state. Load errors carry
UnsupportedCheckpoint or InvalidCheckpoint causes and field locations.
The envelope is slither-sim-checkpoint-v3, with
rulesRevision: "slither-sim-rules-v6", producer provenance, today's flat Config
and ordered slot/entity records, including food storage rows, their private
birth-sequence identities and the next birth sequence. config.radius is the
initial radius and
state.radius is the live radius; loading preserves both. Config must pass the
same validation as a new world's, and the live radius must equal config.radius
until the dynamic controller, circular host admission or a recorded update
could have moved it. Circular host admission may grow it before the first tick.
Versions 1 and 2 are rejected. Version 1 stored the live radius in Config;
version 2 lacks the birth identities required after unordered pickup removal.
Rules v1/v2/v3/v4/v5 are rejected: seat-following, live circular pit/spawn scaling,
pit-only food, constant-density arena sizing and unordered food rows change tick
behavior. Nutrition and pickup events still use reverse birth order: each gain
is valued at the mass after the preceding gain. Host color/lifetime selection
also retains chronological birth order.
The host queue clock is not core checkpoint state. No
controller state is added; occupancy uses living state and death timestamps,
and refill retains fractional credit in initial-area seconds.
The checkpoint includes RNG, timers and terminal causes;
debug snapshot replies and old Simulator JSON are rejected. Unknown fields and
missing companion records reject, as do radii, headings and one tick of snake
travel above 1e150, living snake coordinates beyond 2e150, and dead snake or food
coordinates beyond 3e150, which would overflow later steps. The live
radius may lie inside the initial spawn ring or pit after measured updates. Producer version/commit do not select rules.
RestoreOptions chooses EventRecording { pickups, food_births } and, with the
parallel feature, parallel; event buffers start empty and caches are rebuilt.
Sessions, sockets and host/client delivery are outside the format.
ErrorCode, ErrorKind and Constraint are non-exhaustive; include a wildcard
arm when matching them outside the crate.
transport::Session is the session serve runs, without the stream. JSON
requests and typed steps act on the same worlds and may be mixed.
| Method | Purpose |
|---|---|
Session::new() |
An empty session; reset it before stepping. |
reset(ResetOptions) |
Prepare and atomically commit fully initialized worlds and frames, returning ResetSummary. |
Session::prepare_reset(options) |
Build an opaque PreparedReset without changing any session; inspect summary() before committing. |
commit_reset(prepared) |
Install prepared worlds and frames without failure or allocation, returning the summary. |
request(line) |
Answers one JSON request line with exactly the reply line serve writes, both without the newline. Errors become error replies. |
step(&headings, &boosts) |
Steps every world like a JSON step without writing JSON. Both slices hold worlds × agents entries, world-major: slot s of world w is entry w × agents + s. |
frame(w) |
World w's current frame as a FrameRef of read-only columns, after any reset or step; None before reset or for unknown worlds. |
worlds(), agents() |
Session shape; 0 before the first reset. |
A NaN heading means no action; only dead slots may give it. Typed steps return
Result<(), transport::StepError>: Rejected(SimError) leaves the session
unchanged; Ended(SimError) clears every world after an
advancement or frame-collection failure, including worlds that succeeded.
The cause names the first failing world in stable world order, including when
worlds advance in parallel. Formatting StepError retains the JSON error text.
Then frame returns None, worlds() and agents() return 0; reset to continue.
FrameRef columns hold the values the JSON frame prints, rounded alike, in
fixed types: u64 prey id; u8 food origin and death reason; u32
ticks, living, slot, segments, kills, kill_score, death victim, killer and
food, and every row index and offset; f64 everything else. Where the JSON
frame nests or prints null, the columns differ:
- Views are compressed sparse rows.
views.me[s]is slots's ownsnakesrow, ortransport::NONE(u32::MAX) when the slot is dead. Its visible snakes are the rowsviews.snakes[views.snakes_start[s]..views.snakes_start[s + 1]], and likewisefoodandprey. Each*_starthas one entry more than there are slots; dead slots have empty ranges. deathsholds the columnstime,victim,killer(NONEwithout a killer),reason(an index intotransport::REASONS: body, boundary, pit) and droppedfood.food.originindexestransport::ORIGINS, or isNO_ORIGIN(255) without an origin;food.sizeis NaN without a size.
A FrameRef borrows its session, so it ends before the next request or step.
Sessions are Send: run separate sessions on separate threads.