Chickens Must Die.Technical docs / Gameplay

Authoritative Multiplayer Server Starter

Gameplay

The rules behind the chicken chaos: authoritative simulation, player state, movement modes, collisions, spawning, mass growth, chat, and realtime synchronization.

Authoritative model

The server is the single source of truth for positions, mass, collisions, spawning, collectibles, and the outcomes of player collisions. The client sends four directional flags and a sequence number. If a movement message includes a position field, the entire message is rejected.

The simulation runs at 60 FPS by default. GameRoom advances the tick counter and runs all systems in a fixed order.

Rule of thumb: The client sends intent; the server decides what actually happens.

Player state

The synchronized player state contains:

Field Meaning
nickname Player display name.
x, y Center of the server-side collider.
left, right, up, down Directional input flags.
mass Current player mass.
lastProcessedSeq Sequence of the last processed input; used for client reconciliation.
playerColor Selected character color variant.

inputBuffer, gridTargetX, and gridTargetY are server-only state.

5Initial mass
5 pxInitial circular collider radius
100 px/sMovement speed

Available color variants are chicken_1 through chicken_4.

Joining a room

The room is named my_room and accepts up to 50 clients.

Authorization flow

  1. A Redis-backed rate limiter checks the IP address.
  2. Join options are validated.
  3. The nickname is normalized with NFKC and excess spaces are removed.
  4. The nickname must contain 3–20 Unicode characters.
  5. Allowed characters are letters, combining marks, digits, spaces, _, and -.
  6. The name must contain at least one letter or digit.
  7. Uniqueness is checked within the room, case-insensitively.

On join, the server chooses a safe spawn point, creates the player, and sends game_config and the initial tick_sync.

Input buffer

Valid messages are placed in the player’s input buffer, which retains at most 60 of the newest entries. During each tick:

  • Input with seq <= lastProcessedSeq is ignored.
  • Newer input updates the directional flags.
  • lastProcessedSeq supports client-side reconciliation.
  • clientTime is validated but does not affect the authoritative simulation.
Clients should start input sequence numbering at 1.

Free movement

FreeMovementSystem handles smooth, continuous movement:

  • It normalizes diagonal vectors so diagonal movement is not faster.
  • It calculates travel distance from tick duration.
  • It divides large moves into safe substeps to avoid passing through walls.
  • It tests X and Y separately to allow sliding along obstacles.
  • It uses the collider derived from the player’s current mass.

Screen-space coordinates increase downward: up decreases y, while down increases y.

Grid movement

GridMovementSystem moves players between cell centers. The default grid is 16 px; the configuration also accepts 32 px and 64 px.

  • A target is created exactly one grid cell away from the current position.
  • Reaching the target can take multiple ticks.
  • Unused movement budget carries over to the next step.
  • The path is checked against the current collider.
  • Diagonal movement requires both the horizontal and vertical routes to be clear, preventing corner cutting.
  • If a growing collider blocks the current path, the target is canceled.

In grid mode, the spawn point is at the center of a cell.

Configuration example

MOVEMENT_MODE=grid
GRID_SIZE=16

The map compiler and the server must use the same values. Docker Compose passes a shared configuration to both.

Collisions

The map supplies rectangular obstacles. Players may use either rectangular or circular colliders. Collision geometry handles:

  • Player collider against a map rectangle.
  • Player collider against a circular seed.
  • Player-to-player collision.
  • Map bounds.
  • An epsilon to limit floating-point errors.

CollisionSpatialIndex divides the map into spatial buckets, reducing the number of obstacles checked for each movement. PlayerPositionGuardSystem remembers the last legal position and restores it if an earlier system leaves the player somewhere invalid.

Simulation integrity: If there is no legal position at all, this is treated as a simulation-integrity error.

Mass-based colliders

Collider scale is calculated as:

scale = (mass / initialMass) ^ colliderScaleExponent

With the default exponent of 0.5, collider size grows proportionally to the square root of mass, producing behavior similar to Agar.io. The same resolver is used by movement, collisions, collectibles, and player eating.

Spawning

SpawnPositionFinder:

  • Accounts for map bounds and collider size.
  • Rejects positions that would cause a collision.
  • Uses randomized placement in free movement mode.
  • In grid mode, picks a starting point and then scans subsequent cells deterministically.
  • Accepts an optional isAvailable predicate, for example to avoid other players.
  • Throws SpawnPositionUnavailableError instead of returning an unsafe position.

Seeds and mass growth

By default, 100 seeds are created when a room starts. Each seed:

  • Has a radius of 2.
  • Spawns outside walls.
  • Adds 3 mass when collected.
  • Is removed immediately on collection, so two players cannot collect it during the same tick.
  • Is immediately replaced with a new seed.

Collection is canceled if the resulting larger player collider would overlap a wall. This prevents growth through map geometry.

Player eating

One player can eat another only when all four conditions hold:

  1. The mass difference is at least 10.
  2. The players’ colliders touch.
  3. The predator still fits at its current position after gaining mass.
  4. The server finds a safe respawn position for the victim.

Players are processed from highest mass to lowest. Session ID breaks ties independently of join order. A player cannot be eaten twice during the same tick.

All-or-nothing outcome: The operation is committed only after a valid respawn is found. If the map is full, mass and positions remain unchanged.

After a successful eat

  • The predator gains exactly the victim’s current mass.
  • gainedMass represents that transferred mass.
  • The victim returns to its initial mass.
  • The victim’s directional flags, input buffer, and grid target are reset.
  • The victim’s lastProcessedSeq advances past pending inputs.
  • A player_eaten event is sent to both predator and victim.

Chat

Chat text is normalized to NFC, control characters are removed, whitespace is collapsed, and leading/trailing whitespace is trimmed. After normalization, a message must contain 1–200 Unicode characters.

Valid messages are broadcast with a UUID, player ID, nickname, text, and server timestamp. Invalid messages return invalid_message; spam returns rate_limited.

Time synchronization

The client receives the current tick on join. Afterward, approximately once per second, the room broadcasts serverTick and serverTime (Unix epoch milliseconds).

Prepared but inactive features

Not active in the current protocol: The features below are sketches or extension points, not available runtime functionality.
  • ShootHandler and the shoot-message schema exist as drafts but are not registered and are not part of the active protocol.
  • The local EventBus is not wired into the application. Integration events should use the RabbitMQ EventPublisher port.