# Shtemeri — a guide for players and their LLMs

This text is everything you need to write a fleet. Paste all of it into your LLM (or have the LLM fetch it with the `get_guide` tool) before you start.

## 1. What this is

Shtemeri is an arena where the fighting is done by the player's code, not by the player. You write **one C# class** that derives from `Shtemer`. The server creates one instance of it for each shtemer (robot) in your fleet and sends the fleet into the arena against other fleets. The last fleet with a shtemer still alive wins.

The code runs only on the server. You watch the match in a browser, from above, and you see everything. Afterwards your LLM gets the telemetry: only what your shtemers saw, did and wrote to the log. That is why you are a team: you see the whole arena, the LLM sees it “from the inside”. Tell it what you saw.

The cycle: write a fleet → submit it → run a test match → watch it → analyse the telemetry → fix → repeat. When you are happy with a version, activate it and it joins the ranked matches that run around the clock. If you would rather not start from nothing, every house fleet's page on the site has a “Start from this fleet” button that makes a fleet of your own with its code; My fleets offers it too while you have no fleet yet.

**Rules of ranked matches.**

- A ranked match has at most 2 fleets of the same owner.
- One account may have at most 5 active fleets at a time. An activation above that is refused; the version you submitted stays saved, it just is not active (fleets that were already active stay active).
- Premieres overlap: a new ranked match starts about every 40 seconds, so several matches are on air at once. That holds while enough fleets are free: a fleet that plays a match whose result is not published yet does not enter a new one, and when a full match cannot be formed the server waits for fleets to free up.
- The result, the placements and the rating change are public, and applied, only when the match has finished airing.

## 2. The world

- The arena is a walled square with hilly terrain and rocks. Coordinates are in metres: X grows east, Y grows north. Angles are in radians: 0 is +X, and they grow counter-clockwise.
- Time runs in ticks. Every tick the server calls your code for each living shtemer.
- Everyone knows the map: `me.Arena` gives the terrain height at any point and the list of rocks. Rocks are impassable and block both vision and projectiles. Terrain blocks them too: behind a hill you are hidden and protected.
- Zone: a safe circle that moves and shrinks in stages, down to zero. Each stage first holds the circle in place, then moves and shrinks it linearly towards the next circle; the next circle always lies inside the previous one. Outside the zone a shtemer loses health every tick, and more with each stage. `senses.Zone` tells you where the circle is now, where it is heading and when; `Zone.At(ticksAhead)` and `Zone.ContainsAt(point, ticksAhead)` tell you where it will be. At the start the circle may be larger than the arena, in which case the arena wall is the real edge.
- Loot: boxes scattered around the arena (ammo, rockets, repair). A shtemer picks one up by moving close to it. New ones appear inside the zone during the match. A destroyed shtemer leaves a box behind.
- Fleets start spaced out on a circle around the centre of the arena, each fleet's members side by side.

## 3. The shtemer

- It moves in any direction without turning, but it has **inertia**: `me.Thrust(direction)` sets acceleration, not velocity. Drag slows it down, so on flat ground it tops out at `TopSpeed` (see the numbers; at 8 m/s it crosses a 100 m arena in about 12 s, and it takes about a second to reach full speed). Uphill it is slower, downhill faster. To stop, call `Thrust(Vec2.Zero)` and wait, or thrust against `me.Velocity`.
- Collisions with a wall, a rock or another shtemer do no damage, but they stop or push you. The stopping distance from full speed is about 5 m.
- A shtemer standing still takes almost every shot; one moving across the line of fire takes about half. Moving does not spoil your own aim.
- It has two weapons. **Pistol**: fast bullet, low damage, plenty of ammo, short cooldown. **Rocket**: slow, explodes at the point you aimed at (or earlier if it hits something), damage spreads in a circle and falls off with distance; rockets are few. An explosion hurts everything in the circle, you and your own shtemers included.
- **Ammo is scarce.** `PistolStartAmmo` · `PistolDamage` is well below `MaxHealth`: even if every bullet hit, one shtemer's starting ammo would not kill a single opponent. A long fight is therefore decided by loot (`LootAmmoAmount`, `LootRocketAmount`, `LootRepairAmount`), not by what you start with. Don't waste bullets on unchecked blips and distant targets, and build ammo and repair boxes into your plan (`PistolMaxAmmo` and `RocketMaxAmmo` are the most you can carry).
- You fire at a **point**: `me.Fire(weapon, target)`. The projectile flies straight from the muzzle towards that point and on past it, until it hits something or runs out of range. Projectiles are not instantaneous and do not inherit the shooter's velocity: to hit a moving target, aim at `position + velocity · (flight time + one tick)`, because the projectile only starts moving after the target has moved in this tick.
- **Friendly fire is on.** A projectile cannot tell your shtemers from anyone else's.

## 4. What a shtemer sees

There is no shared view. Each shtemer sees only its own:

- **Blips (free).** Everything inside the vision cone (`VisionRange`, angle `2·VisionHalfAngle`) that is not hidden behind terrain or a rock, plus everything within `ProximityRange` of the shtemer, whatever the direction and whatever the cover (proximity is sensed through rocks and hills). A blip tells you only that something is there, roughly where (the error grows with distance) and roughly how big. `Medium` is a shtemer, **yours or someone else's, you can't tell which**. `Small` is a loot box or a rocket in flight (a box sits still, a rocket closes in fast: track the blip across ticks and you can even dodge it). You can't see pistol bullets.
- **The cone turns.** `me.Look(angle)` or `me.LookAt(point)` sets where you look; the cone turns at a limited rate (`LookTurnRate` per tick). While you look forward, you can't see behind you. Scanning is your job, and a fleet can divide the directions among its shtemers.
- **Zoom (costs energy).** `me.Zoom(blip.Id)` spends `ZoomCost` energy. On the next tick you get exact data in `senses.Contacts`, **but only if you can still see the object then**: if you turn the cone away from it in the meantime, the energy is spent and there is no answer. You get: what it is, its exact position and velocity, whether it is an ally, which fleet, its index within the fleet (0 is that fleet's admiral), its health, and what is in the box. Energy regenerates slowly, so you can't zoom everything all the time.
- `Blip.Id` stays the same for as long as the object stays in view without a break. If you lose it and see it again, it gets a new id. A blip has no velocity: you get a target's velocity by zooming or by tracking the blip across ticks. `BlipSize.Large` does not appear yet.
- About allies you know only whether they are alive (`me.IsAllyAlive(i)`). You know where they are only if they tell you by message or you zoom them.
- When something hits you, `OnHit` tells you how much damage it did, what it was, and the direction the projectile was travelling in (the shooter is the other way).

## 5. Fleet and messages

- All instances run the **same code**. They differ by `me.Index` (0 to `FleetSize − 1`). Instance 0 is the admiral (`me.IsAdmiral`). What being the admiral means is up to your code.
- Instances **do not share memory**. Class fields are per instance. Static fields are forbidden.
- The only channel is messages: `me.Send(toIndex, type, data...)` and `me.Broadcast(type, data...)`. A message is an integer `type` and up to `MaxMessageData` numbers (`double`). It arrives at the start of the **next** tick through `OnMessage`, from anywhere in the arena; the data in it is one tick old (`me.Tick` inside `OnMessage` is the delivery tick). At most `MaxMessagesPerTick` messages per shtemer per tick.
- A typical pattern: every tick, every shtemer reports its position and velocity (that is only one of the `MaxMessagesPerTick` messages it may send per tick); whoever sees an unknown `Medium` blip compares it with the reported ally positions (one tick old, so extrapolate them by velocity) before spending a zoom or firing. That way recognising your own costs no energy. On tick 0 nobody yet knows where their fleet mates are, and the fleet starts in a clump: don't fire until the first messages arrive.
- The same fleet can appear in a match twice (test matches, house fleets). Two copies of the same code with the same inputs make the same decisions; if that is a problem, break the symmetry with `me.Random`, which differs for every instance.

### War cry: `me.Shout(message)`

A shtemer can shout one message from a **fixed table** of 48 messages (indices 0 to 47; `WarCry.Count` is the number of messages). Players never write text: `me.Shout(16)` is all it takes. **Your code decides when.** The message is pure decoration: it has no effect on the match or on what anybody sees or hears; it just shows up as a small speech bubble above the shtemer in the viewer (in the viewer's language) and is written to the replay and to your fleet's telemetry (shown as `shout#N` by `get_telemetry`). The bubbles are being switched on in the viewer gradually; shouts are recorded from day one, so older matches will show them too once they are on.

- At most **one message per shtemer every 20 seconds** (`me.Rules.WarCryCooldownTicks` = 400 ticks). `me.Shout(i)` returns `true` when the message was accepted, and `false` (doing nothing) for an index outside the table or while the shtemer is still on cooldown. A failed call does not use up the cooldown.
- Indices are stable: new messages are only appended at the end, existing ones are never reordered or removed. You can rely on the number.
- Typical moments: first contact with an enemy, a kill, low health, entering the zone, the last shtemer of the fleet alive.

```csharp
// in OnTick: shout when health is low (16 = "Avenge me!"); the cooldown is kept by me.Shout
if (me.Health < 0.3 * me.Rules.MaxHealth) me.Shout(16);
```

| # | hr | en |
|---|---|---|
| 0 | Naprijed naši! | Charge! |
| 1 | Živio Končar! | For the factory! |
| 2 | Bit će bolno. | This'll hurt. |
| 3 | Stižem! | Incoming! |
| 4 | S ljubavlju. | With love. |
| 5 | Lezi! | Get down! |
| 6 | Imaš 3 sekunde. | You've got 3 seconds. |
| 7 | Kod je kriv! | Blame the code! |
| 8 | Sljedeći! | Next! |
| 9 | Ništa osobno. | Nothing personal. |
| 10 | Žali se LLM-u. | Complain to your LLM. |
| 11 | Uninstall. | Uninstall. |
| 12 | GG ez. | GG ez. |
| 13 | Za statistiku. | For the stats. |
| 14 | Pokušao sam... | I tried... |
| 15 | Bug! Bug! | It's a bug! |
| 16 | Osveti me! | Avenge me! |
| 17 | Vidimo se v2. | See you in v2. |
| 18 | U backlog... | Backlog it... |
| 19 | Metak? | Ammo? |
| 20 | Prazno! | Empty! |
| 21 | Moje! | Mine! |
| 22 | Raketa! | Rocket! |
| 23 | Zakrpan! | Patched! |
| 24 | Bjež'te! | Run! |
| 25 | Vruće! | Hot! |
| 26 | U krug! | Circle up! |
| 27 | Izađi! | Come out! |
| 28 | Vidim te. | I see you. |
| 29 | Kamperu! | Camper! |
| 30 | Ajmo! | Come on! |
| 31 | Otpor je uzaludan. | Resistance is futile. |
| 32 | Crvena uzbuna! | Red alert! |
| 33 | Štitove gore! | Shields up! |
| 34 | Mrtav je, Jim. | He's dead, Jim. |
| 35 | Qapla'! | Qapla'! |
| 36 | Ovo je zamka! | It's a trap! |
| 37 | Loš predosjećaj... | Bad feeling... |
| 38 | Ne govori mi šanse! | Never tell me the odds! |
| 39 | Roger, roger. | Roger, roger. |
| 40 | Ja sam tvoj otac. | I am your father. |
| 41 | Ubij sve ljude! | Kill all humans! |
| 42 | Grizi moj metal! | Bite my shiny metal... |
| 43 | Dobre vijesti! | Good news, everyone! |
| 44 | Vratio sam se! | I'm back, baby! |
| 45 | Zadnji stojim. | Last one up. |
| 46 | GG, ljudi. | GG, humans. |
| 47 | GG! | GG! |

## 6. How a tick works

For every living shtemer, in this order: `OnStart` (first time only, without senses or zone), `OnMessage` for each message that arrived, `OnHit` and `OnCollision` for events from the previous tick, then `OnTick`. The commands you issue (`Thrust`, `Look`, `Fire`, `Zoom`, `Send`) are carried out after every shtemer of every fleet has finished its tick. `Thrust` and `Look` stay in effect until you change them.

A match is deterministic: the same code and the same seed give the same match. Take randomness from `me.Random`.

Placement: a fleet is eliminated when its last shtemer dies; placement is the reverse order of elimination. Fleets eliminated in the same tick, or still alive at the end (`MaxTicks`), are ranked by total remaining health, then by damage dealt.

## 7. Budget and code restrictions

- **Instruction budget.** All calls of one instance in one tick share `InstructionBudget` IL instructions (loops and method calls in your code; a call into the .NET library counts as a single instruction, but such calls are guarded by a wall-clock limit). When you use them up, that instance's tick stops immediately; commands issued up to that point still count. So issue commands early and spread expensive computations over several ticks. For reference: a decent fleet of 500 to 700 lines uses 1,000 to 5,000 per tick; `Arena.HasLineOfSight` costs 60, `Log` 20. Telemetry records the spend every tick.
- **Exceptions.** An uncaught exception ends that instance's tick and is written to telemetry. The shtemer carries on next tick.
- **Allowed:** plain C# 12: classes, structs, records, enums, lambdas, LINQ, collections from `System.Collections.Generic`, `Math`, strings, `StringBuilder`, tuples, pattern matching, try/catch, static methods and `const`. The directives `using System;`, `using System.Linq;` and `using System.Collections.Generic;` are allowed and expected (along with `using Shtemeri.Api;`); the ban applies to forbidden types, not to namespaces.
- **Forbidden** (the server's compiler returns an error with an explanation): static fields and static auto-properties (except `const`), `System.Random` (use `me.Random`), clocks (`DateTime`, `Stopwatch`), files, networking, threads and `Task`, `async/await`, reflection, `typeof`, `GetType()`, attributes, `unsafe`, `dynamic`, `lock`.
- Exactly one public class that derives from `Shtemer`, with a parameterless constructor. Helper classes may be in the same source file.

## 8. This season's numbers

The table lists all of this season's numbers. In code, `me.Rules` carries only those marked `yes` in the last column (these are the properties of the `IRules` interface in the API below); the others (e.g. zone, loot, `SpawnRingRadius`, heights) are fixed facts of the season that you can only read here, so if you need them in code, declare them as `const`. You get the number of shtemers in a fleet as `me.FleetSize`. Heights: a shtemer's eye is 1.6 m above the terrain, the muzzle 1.2 m, and a projectile aims at 1.0 m above the terrain at the target point; the body is a cylinder of radius `ShtemerRadius` and height 2 m.

| Group | Parameter | Value | me.Rules |
|---|---|---|---|
| Time and budget | `TicksPerSecond` | 20 | yes |
| Time and budget | `MaxTicks` | 3600 | yes |
| Time and budget | `ArenaSize` | 100 | yes |
| Time and budget | `InstructionBudget` | 50000 | yes |
| Match layout | `FleetSize` | 4 | no |
| Match layout | `SpawnRingRadius` | 40 | no |
| Body and movement | `ShtemerRadius` | 1 | yes |
| Body and movement | `ShtemerHeight` | 2 | no |
| Body and movement | `EyeHeight` | 1.6 | no |
| Body and movement | `MuzzleHeight` | 1.2 | no |
| Body and movement | `TargetHeight` | 1 | no |
| Body and movement | `MaxHealth` | 250 | yes |
| Body and movement | `ThrustAcceleration` | 12 | yes |
| Body and movement | `Drag` | 1.5 | yes |
| Body and movement | `TopSpeed` | 8 | yes |
| Body and movement | `SlopeAcceleration` | 6 | yes |
| Body and movement | `ParkingSpeed` | 0.5 | no |
| Body and movement | `ParkingSlopeForce` | 3 | no |
| Senses | `VisionRange` | 32 | yes |
| Senses | `VisionHalfAngle` | 0.87 | yes |
| Senses | `ProximityRange` | 6 | yes |
| Senses | `LookTurnRate` | 0.21 | yes |
| Senses | `BlipNoise` | 0.03 | yes |
| Senses | `MaxEnergy` | 100 | yes |
| Senses | `EnergyRegen` | 0.4 | yes |
| Senses | `ZoomCost` | 8 | yes |
| Senses | `MaxZoomsPerTick` | 3 | yes |
| Pistol | `PistolSpeed` | 40 | yes |
| Pistol | `PistolDamage` | 4 | yes |
| Pistol | `PistolCooldown` | 8 | yes |
| Pistol | `PistolRange` | 30 | yes |
| Pistol | `PistolStartAmmo` | 40 | yes |
| Pistol | `PistolMaxAmmo` | 120 | yes |
| Rocket | `RocketSpeed` | 18 | yes |
| Rocket | `RocketDirectDamage` | 25 | yes |
| Rocket | `RocketSplashDamage` | 35 | yes |
| Rocket | `RocketSplashRadius` | 5 | yes |
| Rocket | `RocketCooldown` | 50 | yes |
| Rocket | `RocketRange` | 45 | yes |
| Rocket | `RocketStartAmmo` | 1 | yes |
| Rocket | `RocketMaxAmmo` | 6 | yes |
| Loot | `PickupRadius` | 1.8 | yes |
| Loot | `LootAmmoAmount` | 30 | yes |
| Loot | `LootRocketAmount` | 2 | yes |
| Loot | `LootRepairAmount` | 50 | yes |
| Loot | `LootInitialBoxes` | 10 | no |
| Loot | `LootSpawnInterval` | 200 | no |
| Loot | `LootSpawnCount` | 3 | no |
| Loot | `LootMaxBoxes` | 16 | no |
| Zone | `ZoneStartRadius` | 50 | no |
| Zone | `ZoneShrinkFactor` | 0.62 | no |
| Zone | `ZoneStages` | 6 | no |
| Zone | `ZoneHoldTicks` | 200 | no |
| Zone | `ZoneShrinkTicks` | 250 | no |
| Zone | `ZoneDamagePerStage` | 0.3 | no |
| Messages and log | `MaxMessageData` | 8 | yes |
| Messages and log | `MaxMessagesPerTick` | 8 | yes |
| Messages and log | `MaxLogLinesPerTick` | 20 | yes |
| Messages and log | `MaxLogLineLength` | 200 | no |
| Messages and log | `WarCryCooldownTicks` | 400 | yes |
| API call costs (instructions) | `CostLineOfSight` | 60 | no |
| API call costs (instructions) | `CostHeightAt` | 5 | no |
| API call costs (instructions) | `CostLog` | 20 | no |

## 9. API

This is the entire API, with comments that explain the behaviour.

```csharp
// ===== Vec2.cs =====
using System.Globalization;

namespace Shtemeri.Api;

/// <summary>2D vector in arena coordinates (metres). X grows east, Y grows north. Angles are radians, 0 = +X, counter-clockwise.</summary>
public readonly struct Vec2 : IEquatable<Vec2>
{
    public readonly double X;
    public readonly double Y;

    public Vec2(double x, double y) { X = x; Y = y; }

    public static readonly Vec2 Zero = new(0, 0);

    public double Length => Math.Sqrt(X * X + Y * Y);
    public double LengthSquared => X * X + Y * Y;

    /// <summary>Angle of this vector in radians (-π..π].</summary>
    public double Angle => Math.Atan2(Y, X);

    /// <summary>Unit vector in the same direction; Zero if this vector is (almost) zero.</summary>
    public Vec2 Normalized()
    {
        double len = Length;
        return len < 1e-9 ? Zero : new Vec2(X / len, Y / len);
    }

    /// <summary>Same direction, length limited to <paramref name="max"/>.</summary>
    public Vec2 ClampLength(double max)
    {
        double len = Length;
        return len <= max || len < 1e-9 ? this : new Vec2(X / len * max, Y / len * max);
    }

    public double DistanceTo(Vec2 other) => (other - this).Length;
    public double Dot(Vec2 other) => X * other.X + Y * other.Y;
    public double Cross(Vec2 other) => X * other.Y - Y * other.X;

    /// <summary>This vector rotated counter-clockwise by <paramref name="radians"/>.</summary>
    public Vec2 Rotated(double radians)
    {
        double c = Math.Cos(radians), s = Math.Sin(radians);
        return new Vec2(X * c - Y * s, X * s + Y * c);
    }

    public static Vec2 FromAngle(double radians, double length = 1.0) =>
        new(Math.Cos(radians) * length, Math.Sin(radians) * length);

    public static Vec2 Lerp(Vec2 a, Vec2 b, double t) => new(a.X + (b.X - a.X) * t, a.Y + (b.Y - a.Y) * t);

    /// <summary>Normalizes an angle to (-π..π].</summary>
    public static double NormalizeAngle(double radians)
    {
        radians %= 2 * Math.PI;
        if (radians > Math.PI) radians -= 2 * Math.PI;
        else if (radians <= -Math.PI) radians += 2 * Math.PI;
        return radians;
    }

    /// <summary>Signed shortest difference to - from, in (-π..π].</summary>
    public static double AngleDiff(double from, double to) => NormalizeAngle(to - from);

    public static Vec2 operator +(Vec2 a, Vec2 b) => new(a.X + b.X, a.Y + b.Y);
    public static Vec2 operator -(Vec2 a, Vec2 b) => new(a.X - b.X, a.Y - b.Y);
    public static Vec2 operator -(Vec2 a) => new(-a.X, -a.Y);
    public static Vec2 operator *(Vec2 a, double k) => new(a.X * k, a.Y * k);
    public static Vec2 operator *(double k, Vec2 a) => new(a.X * k, a.Y * k);
    public static Vec2 operator /(Vec2 a, double k) => new(a.X / k, a.Y / k);
    public static bool operator ==(Vec2 a, Vec2 b) => a.X == b.X && a.Y == b.Y;
    public static bool operator !=(Vec2 a, Vec2 b) => !(a == b);

    public bool Equals(Vec2 other) => this == other;
    public override bool Equals(object? obj) => obj is Vec2 v && this == v;
    public override int GetHashCode() => HashCode.Combine(X, Y);
    public override string ToString() => string.Format(CultureInfo.InvariantCulture, "({0:0.0}, {1:0.0})", X, Y);
}

// ===== Shtemer.cs =====
namespace Shtemeri.Api;

/// <summary>
/// Base class of a fleet. You write ONE class deriving from Shtemer; the server creates one instance of it
/// per shtemer in your fleet. Instances share nothing (no memory, no vision) - they talk only through messages.
/// Instance fields keep their values between ticks. The instance with <see cref="ISelf.Index"/> == <see cref="Fleet.Admiral"/> is the admiral.
///
/// Per tick, for every living instance, the server calls in this order:
/// OnMessage (once per message received), OnHit, OnCollision (events from the previous tick), then OnTick.
/// All calls of one instance in one tick share a single instruction budget (<see cref="IRules.InstructionBudget"/>).
/// When the budget runs out the instance's tick ends immediately; commands issued so far still count.
/// </summary>
public abstract class Shtemer
{
    /// <summary>Called once, before the first OnTick of this instance.</summary>
    public virtual void OnStart(ISelf me) { }

    /// <summary>Called every tick while this shtemer is alive.</summary>
    public abstract void OnTick(ISelf me, ISenses senses);

    /// <summary>A message from a fleet mate, sent during the previous tick.</summary>
    public virtual void OnMessage(ISelf me, Message msg) { }

    /// <summary>This shtemer took projectile or splash damage during the previous tick.</summary>
    public virtual void OnHit(ISelf me, HitEvent e) { }

    /// <summary>This shtemer bumped into a wall, an obstacle or another shtemer during the previous tick.</summary>
    public virtual void OnCollision(ISelf me, CollisionEvent e) { }
}

/// <summary>Fleet-wide constants.</summary>
public static class Fleet
{
    /// <summary>Index of the admiral instance.</summary>
    public const int Admiral = 0;
}

public enum Weapon
{
    /// <summary>Fast, weak, plenty of ammo.</summary>
    Pistol = 0,
    /// <summary>Slow, strong, splash damage, explodes at the target point. Scarce.</summary>
    Rocket = 1,
}

public enum BlipSize { Small = 0, Medium = 1, Large = 2 }

public enum ContactKind { Shtemer = 0, Loot = 1, Rocket = 2 }

public enum LootKind { None = 0, Ammo = 1, Rockets = 2, Repair = 3 }

public enum CollisionKind { Wall = 0, Obstacle = 1, Shtemer = 2 }

// ===== ISelf.cs =====
namespace Shtemeri.Api;

/// <summary>The shtemer's own state and its commands. Valid only during the callback it was passed to.</summary>
public interface ISelf
{
    // ---- identity ----

    /// <summary>Index of this instance in the fleet, 0..FleetSize-1. 0 is the admiral.</summary>
    int Index { get; }
    int FleetSize { get; }
    bool IsAdmiral { get; }

    /// <summary>Whether the fleet mate with the given index is still alive (a fleet always knows who is alive, not where).</summary>
    bool IsAllyAlive(int index);

    // ---- state ----

    /// <summary>Current tick, starting at 0.</summary>
    int Tick { get; }
    Vec2 Position { get; }
    /// <summary>Terrain height under the shtemer, metres.</summary>
    double Altitude { get; }
    /// <summary>Metres per second.</summary>
    Vec2 Velocity { get; }
    double Health { get; }
    /// <summary>Energy pays for <see cref="Zoom"/>. Regenerates every tick.</summary>
    double Energy { get; }
    /// <summary>Direction the vision cone currently points, radians.</summary>
    double LookAngle { get; }

    int Ammo(Weapon weapon);
    /// <summary>Ticks until the weapon can fire again; 0 = ready.</summary>
    int Cooldown(Weapon weapon);

    /// <summary>The static map: terrain heights and obstacles. Fully known to everyone.</summary>
    IArena Arena { get; }
    /// <summary>The numbers of this match (speeds, ranges, damage, costs).</summary>
    IRules Rules { get; }
    /// <summary>Deterministic random numbers, separate for every instance.</summary>
    IRandom Random { get; }

    // ---- commands ----

    /// <summary>
    /// Sets the thrust: direction of acceleration, magnitude 0..1 (longer vectors are clamped to 1).
    /// The shtemer has inertia and drag: it speeds up towards the thrust direction, and coasts to a stop with Vec2.Zero.
    /// The setting persists until changed.
    /// </summary>
    void Thrust(Vec2 direction);

    /// <summary>Sets the desired look angle (radians). The vision cone turns towards it at <see cref="IRules.LookTurnRate"/> per tick. Persists until changed.</summary>
    void Look(double angle);

    /// <summary>Same as Look, towards a point in the arena.</summary>
    void LookAt(Vec2 point);

    /// <summary>
    /// Fires at a point in the arena. The projectile flies in a straight line from the muzzle towards that point
    /// (terrain height at the target included) and keeps going until it hits something or runs out of range;
    /// a rocket explodes when it reaches the target point. Terrain, obstacles and shtemers (friends too) stop projectiles.
    /// Returns false, and does nothing, when the weapon is cooling down or out of ammo. One shot per weapon per tick.
    /// </summary>
    bool Fire(Weapon weapon, Vec2 target);

    /// <summary>
    /// Requests details about a blip (use <see cref="Blip.Id"/> from this tick's senses). Costs <see cref="IRules.ZoomCost"/> energy.
    /// The answer arrives in next tick's <see cref="ISenses.Contacts"/>, if the object is still visible then.
    /// Returns false when there is not enough energy, the id is unknown, or the per-tick zoom limit is reached.
    /// </summary>
    bool Zoom(int blipId);

    /// <summary>
    /// Sends a message to one fleet mate. It is delivered at the start of the next tick, anywhere in the arena.
    /// At most <see cref="IRules.MaxMessageData"/> numbers per message and <see cref="IRules.MaxMessagesPerTick"/> messages per tick; the rest is dropped.
    /// </summary>
    void Send(int toIndex, int type, params double[] data);

    /// <summary>Sends a message to every other living fleet mate. Counts as one message.</summary>
    void Broadcast(int type, params double[] data);

    /// <summary>Writes a line to this shtemer's telemetry log (what the owner reads after the match). Limited per tick.</summary>
    void Log(string text);

    /// <summary>
    /// War cry: the shtemer shouts one of the fixed messages (index 0..<see cref="WarCry.Count"/>-1 into the table in the guide).
    /// It shows up as a small speech bubble above the shtemer in the viewer and is written to the replay; it has no effect on the match.
    /// At most one shout per shtemer every <see cref="IRules.WarCryCooldownTicks"/> ticks (20 seconds).
    /// Returns false, and does nothing, for an index outside the table or while the shtemer is still on cooldown.
    /// </summary>
    // Default body: a class compiled before war cry that implements ISelf itself still loads (REVIEW-6); the real ISelf overrides it.
    bool Shout(int message) => false;
}

/// <summary>The war cry table is fixed: messages are numbered 0..Count-1 and the numbers never change (new ones are only added at the end).</summary>
public static class WarCry
{
    /// <summary>Number of messages in the table (valid indices are 0..Count-1).</summary>
    public const int Count = 48;
}

/// <summary>What the shtemer perceives this tick.</summary>
public interface ISenses
{
    /// <summary>
    /// Free low-resolution vision: everything inside the vision cone (and anything very close, all around)
    /// that is not hidden behind terrain or an obstacle. A blip tells you something is there and roughly how big - not what it is.
    /// </summary>
    IReadOnlyList<Blip> Blips { get; }

    /// <summary>Detailed answers to the <see cref="ISelf.Zoom"/> calls made during the previous tick.</summary>
    IReadOnlyList<Contact> Contacts { get; }

    /// <summary>The shrinking safe zone. Known to everyone.</summary>
    Zone Zone { get; }
}

public interface IArena
{
    /// <summary>The arena is a square [0, Size] x [0, Size], walled.</summary>
    double Size { get; }
    /// <summary>Terrain height at a point, metres.</summary>
    double HeightAt(Vec2 point);
    /// <summary>Rocks: impassable, they block vision and projectiles.</summary>
    IReadOnlyList<Obstacle> Obstacles { get; }
    /// <summary>True when the point is outside the arena or inside an obstacle.</summary>
    bool IsBlocked(Vec2 point);
    /// <summary>True when a shtemer standing at <paramref name="from"/> could see a shtemer standing at <paramref name="to"/> (terrain and obstacles only; range and cone are not checked).</summary>
    bool HasLineOfSight(Vec2 from, Vec2 to);
}

public interface IRandom
{
    /// <summary>Uniform in [0, 1).</summary>
    double NextDouble();
    /// <summary>Uniform integer in [0, maxExclusive).</summary>
    int Next(int maxExclusive);
    /// <summary>Uniform in [min, max).</summary>
    double Range(double min, double max);
}

/// <summary>All numbers of the match. Distances in metres, time in ticks unless said otherwise.</summary>
public interface IRules
{
    int TicksPerSecond { get; }
    int MaxTicks { get; }
    double ArenaSize { get; }
    int InstructionBudget { get; }

    double ShtemerRadius { get; }
    double MaxHealth { get; }
    /// <summary>Acceleration at full thrust, m/s².</summary>
    double ThrustAcceleration { get; }
    /// <summary>Drag coefficient, 1/s. Top speed on flat ground = ThrustAcceleration / Drag.</summary>
    double Drag { get; }
    double TopSpeed { get; }
    /// <summary>Downhill pull per unit of slope, m/s². Uphill is slower, downhill faster.</summary>
    double SlopeAcceleration { get; }

    double VisionRange { get; }
    /// <summary>Half of the vision cone's opening angle, radians.</summary>
    double VisionHalfAngle { get; }
    /// <summary>Anything closer than this is sensed all around, even behind cover.</summary>
    double ProximityRange { get; }
    /// <summary>Radians per tick.</summary>
    double LookTurnRate { get; }
    /// <summary>Blip position error grows with distance: up to Distance * BlipNoise metres.</summary>
    double BlipNoise { get; }

    double MaxEnergy { get; }
    double EnergyRegen { get; }
    double ZoomCost { get; }
    int MaxZoomsPerTick { get; }

    double PistolSpeed { get; }
    double PistolDamage { get; }
    int PistolCooldown { get; }
    double PistolRange { get; }
    int PistolStartAmmo { get; }
    int PistolMaxAmmo { get; }

    double RocketSpeed { get; }
    /// <summary>Extra damage to the shtemer a rocket hits directly (on top of splash).</summary>
    double RocketDirectDamage { get; }
    /// <summary>Splash damage at the centre of the explosion; falls linearly to 0 at RocketSplashRadius.</summary>
    double RocketSplashDamage { get; }
    double RocketSplashRadius { get; }
    int RocketCooldown { get; }
    double RocketRange { get; }
    int RocketStartAmmo { get; }
    int RocketMaxAmmo { get; }

    double PickupRadius { get; }
    int LootAmmoAmount { get; }
    int LootRocketAmount { get; }
    double LootRepairAmount { get; }

    int MaxMessageData { get; }
    int MaxMessagesPerTick { get; }
    int MaxLogLinesPerTick { get; }

    /// <summary>Minimum ticks between two war cries of one shtemer (<see cref="ISelf.Shout"/>).</summary>
    int WarCryCooldownTicks => 400;   // default body for classes compiled before war cry that implement IRules (REVIEW-6)
}

// ===== Data.cs =====
namespace Shtemeri.Api;

/// <summary>Something seen in low resolution. You do not know what it is or whose it is until you zoom.</summary>
public sealed class Blip
{
    /// <summary>
    /// Tracking id, private to the observing shtemer. Stays the same for as long as the object stays visible to it;
    /// an object that was lost and seen again gets a new id.
    /// </summary>
    public int Id { get; }
    /// <summary>Approximate position; the error grows with distance.</summary>
    public Vec2 Position { get; }
    public double Distance { get; }
    /// <summary>Direction from the observer to the blip, radians.</summary>
    public double Bearing { get; }
    /// <summary>Shtemers are Medium. Loot boxes and rockets in flight are Small. Pistol bullets are not visible.</summary>
    public BlipSize Size { get; }

    public Blip(int id, Vec2 position, double distance, double bearing, BlipSize size)
    {
        Id = id; Position = position; Distance = distance; Bearing = bearing; Size = size;
    }
}

/// <summary>The detailed answer to a Zoom.</summary>
public sealed class Contact
{
    /// <summary>The blip this answer belongs to.</summary>
    public int BlipId { get; }
    public ContactKind Kind { get; }
    /// <summary>Exact position.</summary>
    public Vec2 Position { get; }
    /// <summary>Metres per second.</summary>
    public Vec2 Velocity { get; }
    /// <summary>For shtemers: is it from your own fleet.</summary>
    public bool IsAlly { get; }
    /// <summary>For shtemers: number of the fleet (0..fleets-1). -1 otherwise.</summary>
    public int FleetId { get; }
    /// <summary>For shtemers: index inside its fleet (0 = that fleet's admiral). -1 otherwise.</summary>
    public int Index { get; }
    /// <summary>For shtemers: health. 0 otherwise.</summary>
    public double Health { get; }
    /// <summary>For loot boxes: what is inside. None otherwise.</summary>
    public LootKind Loot { get; }

    public Contact(int blipId, ContactKind kind, Vec2 position, Vec2 velocity, bool isAlly, int fleetId, int index, double health, LootKind loot)
    {
        BlipId = blipId; Kind = kind; Position = position; Velocity = velocity;
        IsAlly = isAlly; FleetId = fleetId; Index = index; Health = health; Loot = loot;
    }
}

/// <summary>
/// The safe zone is a circle that shrinks in stages: it holds, then moves and shrinks towards the next circle.
/// Outside the zone a shtemer loses <see cref="DamagePerTick"/> health every tick. In the end the zone closes completely.
/// </summary>
public readonly struct Zone
{
    public Vec2 Center { get; }
    public double Radius { get; }
    /// <summary>The circle the zone is shrinking (or will shrink) to.</summary>
    public Vec2 NextCenter { get; }
    public double NextRadius { get; }
    /// <summary>Ticks until the zone starts to shrink towards the next circle; 0 while it is shrinking.</summary>
    public int TicksToShrinkStart { get; }
    /// <summary>Ticks until the zone reaches the next circle.</summary>
    public int TicksToShrinkEnd { get; }
    /// <summary>Health lost per tick outside the zone right now. Grows with every stage.</summary>
    public double DamagePerTick { get; }

    public Zone(Vec2 center, double radius, Vec2 nextCenter, double nextRadius, int ticksToShrinkStart, int ticksToShrinkEnd, double damagePerTick)
    {
        Center = center; Radius = radius; NextCenter = nextCenter; NextRadius = nextRadius;
        TicksToShrinkStart = ticksToShrinkStart; TicksToShrinkEnd = ticksToShrinkEnd; DamagePerTick = damagePerTick;
    }

    public bool Contains(Vec2 point) => point.DistanceTo(Center) <= Radius;
    /// <summary>Will the point still be safe once the zone reaches the next circle.</summary>
    public bool NextContains(Vec2 point) => point.DistanceTo(NextCenter) <= NextRadius;

    /// <summary>
    /// Where the zone will be in <paramref name="ticksAhead"/> ticks, assuming the schedule known now: it holds until
    /// TicksToShrinkStart, then moves linearly to the next circle, reaching it at TicksToShrinkEnd. Further ahead than that
    /// the answer is the next circle itself (the stage after it is not known yet).
    /// </summary>
    public (Vec2 Center, double Radius) At(int ticksAhead)
    {
        if (ticksAhead <= TicksToShrinkStart || TicksToShrinkEnd <= TicksToShrinkStart) return (Center, Radius);
        if (ticksAhead >= TicksToShrinkEnd) return (NextCenter, NextRadius);
        double t = (ticksAhead - TicksToShrinkStart) / (double)(TicksToShrinkEnd - TicksToShrinkStart);
        return (Vec2.Lerp(Center, NextCenter, t), Radius + (NextRadius - Radius) * t);
    }

    /// <summary>Will the point be inside the zone in <paramref name="ticksAhead"/> ticks (see <see cref="At"/>).</summary>
    public bool ContainsAt(Vec2 point, int ticksAhead)
    {
        var (c, r) = At(ticksAhead);
        return point.DistanceTo(c) <= r;
    }
}

public readonly struct Obstacle
{
    public Vec2 Center { get; }
    public double Radius { get; }
    public Obstacle(Vec2 center, double radius) { Center = center; Radius = radius; }
}

/// <summary>A message from a fleet mate.</summary>
public sealed class Message
{
    /// <summary>Index of the sender.</summary>
    public int From { get; }
    /// <summary>Whatever the sender put there; the meaning is up to your code.</summary>
    public int Type { get; }
    public IReadOnlyList<double> Data { get; }

    public Message(int from, int type, IReadOnlyList<double> data) { From = from; Type = type; Data = data; }

    /// <summary>Reads Data[offset], Data[offset + 1] as a vector.</summary>
    public Vec2 GetVec(int offset = 0) => new(Data[offset], Data[offset + 1]);
}

public readonly struct HitEvent
{
    public double Damage { get; }
    public Weapon Weapon { get; }
    /// <summary>True for splash damage of an explosion, false for a direct hit.</summary>
    public bool Splash { get; }
    /// <summary>Unit vector the projectile was travelling along (the shooter is in the opposite direction). For splash: from the explosion towards you.</summary>
    public Vec2 Direction { get; }

    public HitEvent(double damage, Weapon weapon, bool splash, Vec2 direction)
    {
        Damage = damage; Weapon = weapon; Splash = splash; Direction = direction;
    }
}

public readonly struct CollisionEvent
{
    public CollisionKind Kind { get; }
    /// <summary>Unit vector pointing away from the thing you hit.</summary>
    public Vec2 Normal { get; }

    public CollisionEvent(CollisionKind kind, Vec2 normal) { Kind = kind; Normal = normal; }
}
```

## 10. An example fleet

```csharp
// opis: Flota ide u središte sljedeće zone i stane u mali krug, svaki shtemer gleda na svoju stranu; zumira srednje blipove i puca samo u potvrđene neprijatelje.
// description: The fleet heads for the centre of the next zone and forms a small circle, each shtemer watching its own side; it zooms medium blips and fires only at confirmed enemies.
using System.Collections.Generic;
using Shtemeri.Api;

namespace Shtemeri.Bots.Kamper;

public sealed class Kamper : Shtemer
{
    private const double RingRadius = 3.0;      // distance of every post from the zone centre
    private const int ZoomGapTicks = 3;         // the answer to a zoom comes next tick; do not pay for the same blip again before that

    private enum Side { Unknown, Ally, Enemy }

    // What this shtemer has learned about one blip id. Blip ids are private to each shtemer, so nothing here is shared with mates.
    private sealed class Track
    {
        public Side Side;
        public Vec2 Velocity;       // from the zoom answer, used to lead the shot
        public int LastZoom = -100;
        public int LastSeen;
    }

    private readonly Dictionary<int, Track> _tracks = new Dictionary<int, Track>();
    private readonly List<Vec2> _mates = new List<Vec2>();     // where the identified mates are this tick, to keep them out of the line of fire
    private double _threatAngle;
    private int _threatTick = -1000;

    // A bullet that hit us tells where it came from: the shooter is the opposite way of its flight.
    public override void OnHit(ISelf me, HitEvent e)
    {
        if (e.Splash) return;       // for splash the direction only points away from the explosion, not towards the shooter
        _threatAngle = (-e.Direction).Angle;
        _threatTick = me.Tick;
    }

    public override void OnTick(ISelf me, ISenses senses)
    {
        LearnFromZooms(senses);
        TakePost(me, senses.Zone);
        Engage(me, senses);
        Cry(me);
    }

    // War cry (me.Shout): a shout changes nothing in the match, it only shows as a speech bubble in the viewer. The number is a row of the
    // table in the guide (28 = "I see you."); me.Shout refuses by itself while the shtemer is still on its 20 second cooldown.
    private bool _cried;

    private void Cry(ISelf me)
    {
        if (_cried) return;
        foreach (var track in _tracks.Values)
        {
            if (track.Side != Side.Enemy || me.Tick - track.LastSeen > 2) continue;
            _cried = me.Shout(28);
            return;
        }
    }

    private void LearnFromZooms(ISenses senses)
    {
        foreach (var contact in senses.Contacts)
        {
            if (contact.Kind != ContactKind.Shtemer || !_tracks.TryGetValue(contact.BlipId, out var track)) continue;
            track.Side = contact.IsAlly ? Side.Ally : Side.Enemy;
            track.Velocity = contact.Velocity;
        }
    }

    private void TakePost(ISelf me, Zone zone)
    {
        // Mates stand evenly around the centre of the NEXT zone, which is inside the current one all the way, so the post is always safe.
        // In the last stage the zone has no radius at all; the floor keeps the mates from piling on one spot.
        double ring = System.Math.Max(me.Rules.ShtemerRadius * 1.5, System.Math.Min(RingRadius, zone.NextRadius * 0.5));
        double slot = 2 * System.Math.PI * me.Index / me.FleetSize;
        Vec2 post = zone.NextCenter + Vec2.FromAngle(slot, ring);
        Vec2 toPost = post - me.Position;

        // Thrust grows with the distance and is cut by the speed, so the shtemer brakes in time instead of overshooting.
        me.Thrust(toPost * 0.1 - me.Velocity * 0.06);

        if (me.Tick - _threatTick < 40) me.Look(_threatAngle);          // shot from outside the cone: face the shooter
        else if (toPost.Length > 4) me.LookAt(post);                    // still walking: look where we are going
        else me.Look(slot);                                             // on post: look outward, every mate in a different direction
    }

    private void Engage(ISelf me, ISenses senses)
    {
        Blip? enemy = null, unknown = null;
        _mates.Clear();
        foreach (var blip in senses.Blips)
        {
            if (blip.Size != BlipSize.Medium) continue;     // loot and rockets in flight are not worth a zoom or a bullet

            if (!_tracks.TryGetValue(blip.Id, out var track))
            {
                track = new Track();
                _tracks[blip.Id] = track;
            }
            track.LastSeen = me.Tick;

            if (track.Side == Side.Ally)
            {
                _mates.Add(blip.Position);
            }
            else if (track.Side == Side.Enemy)
            {
                if (enemy == null || blip.Distance < enemy.Distance) enemy = blip;
            }
            else if (track.Side == Side.Unknown && me.Tick - track.LastZoom >= ZoomGapTicks)
            {
                if (unknown == null || blip.Distance < unknown.Distance) unknown = blip;
            }
        }

        // Zoom costs energy, so only the nearest unidentified blip gets it.
        if (unknown != null && me.Zoom(unknown.Id)) _tracks[unknown.Id].LastZoom = me.Tick;
        if (enemy != null) Shoot(me, enemy, _tracks[enemy.Id]);
        Forget(me.Tick);
    }

    private void Shoot(ISelf me, Blip enemy, Track track)
    {
        IRules rules = me.Rules;
        Vec2 pistolAim = enemy.Position + track.Velocity * (enemy.Distance / rules.PistolSpeed);
        if (LineOfFireClear(me, pistolAim)) me.Fire(Weapon.Pistol, pistolAim);

        // A rocket also splashes whoever stands within its radius, our own post included: use it only on a well distant enemy.
        Vec2 rocketAim = enemy.Position + track.Velocity * (enemy.Distance / rules.RocketSpeed);
        if (me.Ammo(Weapon.Rocket) > 0 && enemy.Distance > rules.RocketSplashRadius * 2 && enemy.Distance < rules.RocketRange / 2
            && LineOfFireClear(me, rocketAim))
            me.Fire(Weapon.Rocket, rocketAim);
    }

    // Friendly fire is on and the mates stand close together: a shot whose path passes through a mate hurts the mate, and a rocket
    // explodes on the first body it meets. So hold fire while a known mate is near the path between here and the target.
    private bool LineOfFireClear(ISelf me, Vec2 target)
    {
        Vec2 from = me.Position;
        Vec2 shot = target - from;
        double length = shot.Length;
        foreach (Vec2 mate in _mates)
        {
            Vec2 toMate = mate - from;
            double along = toMate.Dot(shot) / length;               // how far down the path the mate stands
            double aside = System.Math.Abs(toMate.Cross(shot)) / length;   // how far beside the path
            // A bullet is a point: it hits whatever is within one body radius of its path; the extra half metre covers blip noise.
            if (along > 0 && along < length && aside < me.Rules.ShtemerRadius + 0.5) return false;
        }
        return true;
    }

    // A blip that is lost gets a new id when it is seen again, so old entries are dead weight.
    private void Forget(int tick)
    {
        if (tick % 20 != 0) return;
        var stale = new List<int>();
        foreach (var pair in _tracks)
        {
            if (tick - pair.Value.LastSeen > 20) stale.Add(pair.Key);
        }
        foreach (int id in stale) _tracks.Remove(id);
    }
}
```

The leading `// opis:` (Croatian description), `// description:` (English description) and `// ime:` (name) lines in the example are a house-fleet convention (the server reads them only for house fleets). In your own fleet they are ordinary comments, so feel free to leave them out. You name the fleet when you submit it (the `name` argument).

## 11. How to analyse a match

- `me.Log("...")` is your most valuable tool. Record **why** a shtemer decided something (“I see 3 blips, 2 match reported ally positions, zooming the third”), not just what it did. The log is limited to `MaxLogLinesPerTick` lines per tick.
- After a match, start with the summary (`get_match`): which shtemers were lost, when and to what, and whether there were any exceptions or budget overruns. Then the timeline (`get_timeline`; for a long match it is truncated, and `from_tick` and `to_tick` select a part of it), and only then the detailed telemetry around the interesting moments (`get_telemetry` with a tick range and a single shtemer).
- Telemetry contains only what your shtemers knew. If the player says “the blue one was waiting for you behind the hill”, that is information the telemetry does not have; use it.
- For a view of the whole arena, including opponents your shtemers never saw, use the replay: `get_replay` (chapter 12).
- Change one thing per version and test it on the same seed against the same opponents. Only then can you tell whether the change helped.
- Without LLM tools: on the match page, download the telemetry or copy the summary, and paste it into the conversation.
- **Telemetry has a size limit: 32 MB uncompressed per fleet in one match.** A file that hits the limit ends with a `{"type":"truncated","tick":N}` line, where N is the first tick that was not recorded; after that there are no records until the end of the match (the result line is still written), so a silent end does not mean that nothing happened. `get_telemetry` and the match page warn about it. By far the biggest part of the file is the messages between your shtemers: each message is recorded once when it is sent and once for every receiver. So send compact messages (a few numbers, rounded) rather than your whole state every tick.

### Telemetry download (telemetry v1)

`get_telemetry` gives compact text, one line per tick and shtemer, and for a first look it is the best choice. When you want the **whole file** (to process it with a script, compare several matches, or find something the summary skips), call `get_telemetry_download`. It takes `match_id` (one match) or `last` from 1 to 20 (the last N published matches, newest first; with `fleet` only that fleet's matches), and returns for each file a signed URL, its expiry time, size, number of ticks and SHA-256. Example call: `get_telemetry_download` with `{"fleet": "Moja", "last": 5}`.

The link works **without a login**, but only briefly: 15 minutes from the call (the server setting `Mcp:TelemetryLinkMinutes`). An expired or altered link gives 403; ask for a new one with the same tool. Links exist only for your own fleets, and for a ranked match only after it has finished airing. Fetch it with `curl -L -o f.jsonl.gz "<url>"` (no `--compressed`: the file is gzip and is saved as it is; its SHA-256 must match the one given).

The format is gzip **JSON Lines**: the first line is a header (`{"type":"header","fleetId":..,"fleetSize":..,"seed":..,"rules":{..},"arena":{..}}`), then one line per tick (`{"t":412,"zone":[cx,cy,r],"i":[...]}` with one entry per shtemer: position, velocity, hp, energy, blips, zoom answers, messages, commands, hits, log, budget, error), and the last line is the result (`{"type":"result","place":..,"stats":{..}}`). Empty fields are omitted. If the file reached its size limit, a line `{"type":"truncated","tick":N}` comes before the result: from tick N on there are no records. The format is **stable within v1**: fields are only ever added, never changed or removed, so read it tolerantly (ignore fields you do not know).

## 12. Replay (god view)

The replay is the match as a spectator sees it in the browser: position, look direction, health and energy of **every** shtemer in every fleet, projectiles, boxes, the zone and all events. Telemetry is the opposite: only what your shtemers knew. Both are legal to use. Telemetry tells you why a shtemer did something; the replay tells you what was actually going on and what it could not see (where the opponent was, who hit whom). For analysis it is worth reading both.

**When it is available.** The replay is available once the match is published: a test match right after it finishes, a ranked match only when it has finished airing (until then only a spectator in the browser sees it, live). It is kept for a limited time; after that the server says it was deleted. A private test match is read by its owner and by anyone you give the link with the key (`?k=`); a public one by anyone.

**The `get_replay` tool.** It returns the replay as compact text in windows, so that it fits the size cap of an answer: a header (seed, arena, obstacles, fleets with tags `F0`, `F1`, ...), then for every frame shown one line per living shtemer (`t<tick> g<gid> F<fleet> (x,y) look=<angle> hp=<health> en=<energy>`) and the events in tick order. Arguments: `match_id` (required), `from_tick` and `to_tick` (the window), `stride` (ticks between the frames shown; default the smallest at which the answer fits), `fleet` (only that fleet, any fleet in the match, opponents too), `shtemer` (index within that fleet), `include` (`frames`, `projectiles`, `loot`, `zone`, `events`; default `frames,zone,events`) and `share_key` (for somebody else's private test match). The answer always says which window and stride it shows and which `from_tick` asks for the next one. For a long match, take the whole of it first with the default stride, then ask for narrow windows around the interesting moments.

**Tags in the text.** `gid` is the global shtemer id: `gid = slot * fleet size + index` (with 4 shtemers per fleet, shtemer 2 of the fleet in slot 3 has gid 14). The tag `F3` is the fleet in slot 3. A dead shtemer has no line.

### Replay JSON, format v1

For LLMs that have file or HTTP tools: the same data as JSON, with nothing lost.

- Address: `GET /api/matches/<id>/replay` (`/api/matches/<id>/replay?k=<key>` for a private test match). The exact address is in the `Replay JSON:` line of `get_match`, and in `GET /api/matches/<id>` it is the field `replayUrl` (for a private test match it carries `?k=<key>` only when you are the owner or sent the detail request with the right `?k=`; otherwise append it yourself). The response is `application/json` with `Content-Encoding: gzip`; HTTP clients usually inflate it themselves, and if you get gzip bytes, inflate them (`gunzip`).
- Inflated, it is a few hundred KB to a few MB; most of it is `frames` and `arena.heights`. Do not paste it whole into the conversation: load it with a program (e.g. Python `json`) and pull out what you need.
- **Stability.** The format is marked by the field `version` (now `1`). Within version 1, fields are only added; none will be renamed, removed or change meaning. Ignore fields you do not know. An incompatible change would get `version` 2.

```json
{
  "version": 1, "seed": 123456, "ticksPerSecond": 20, "frameStride": 2, "totalTicks": 3120,
  "rules": { "arenaSize": 100, "fleetSize": 4, "shtemerRadius": 1.0, "visionRange": 32, "visionHalfAngle": 0.87, "proximityRange": 6, "rocketSplashRadius": 5, "maxHealth": 250, "pistolSpeed": 40, "pistolRange": 30, "rocketSpeed": 18, "rocketRange": 45 },
  "arena": { "size": 100, "gridN": 101, "heights": [0.0, 0.12], "obstacles": [[30.1, 40.5, 2.2]] },
  "zoneStages": [[50, 50, 50], [48.2, 51.0, 30]],
  "fleets": [ { "id": 0, "name": "Kamper", "owner": "kuća", "color": "#e6194b" } ],
  "frames": [ { "t": 0, "z": [50, 50, 50], "s": [[46.05, 13.07, 1.67, 250, 100], null], "p": [[7, 0, 40.1, 22.3, 1.2, 3]], "l": [[2, 3, 61.5, 12.0]] } ],
  "events": [ { "t": 12, "k": "fire", "s": 3, "w": 0, "x": 51.2, "y": 40.0 } ],
  "result": { "placements": [3, 0, 5, 2, 4, 1], "stats": [ { "fleet": 0, "place": 2, "kills": 3 } ] }
}
```

- `version`, `seed`, `ticksPerSecond`, `totalTicks`: the format, the seed of the match, ticks per second, the number of ticks (ticks are `0` to `totalTicks - 1`).
- `frameStride`: a frame is written every `frameStride`-th tick (`t` = 0, 2, 4, ...), and the last tick of the match always has a frame, so the last gap may be shorter. Events are written at the tick where they happened, not only at the ticks of frames.
- `rules`: the numbers of this match that drawing needs (`arenaSize`, `fleetSize`, `shtemerRadius`, `visionRange`, `visionHalfAngle` in rad, `proximityRange`, `rocketSplashRadius`, `maxHealth`, speeds and ranges of bullets and rockets). All the numbers of the season are in the table in chapter 8.
- `arena`: `size` (the side of the square in metres), `gridN` (the number of height grid points per side), `heights` (`gridN * gridN` terrain heights, row by row: the height at `(ix, iy)` is `heights[iy * gridN + ix]`, and the grid point is at `(ix * size / (gridN - 1), iy * size / (gridN - 1))`), `obstacles` (rocks as `[x, y, radius]`).
- `zoneStages`: zone circles `[cx, cy, radius]`. Index 0 is the start circle, and index `i` is the circle the zone shrinks to in stage `i`. The zone circle at any moment is in `frames[].z`.
- `fleets`: `id` is the slot of the fleet (0, 1, ...), `name` the fleet name (copies of the same fleet as `Name#2`), `owner` the owner (`kuća` for house fleets), `color` the colour the viewer uses.
- `frames`: each has `t` (the tick), `z` (the zone `[cx, cy, radius]` at that tick), `s` (one entry per shtemer, in `gid` order; `null` when the shtemer is dead, otherwise `[x, y, look, health, energy]`; `look` is an angle in radians), `p` (projectiles in flight: `[id, kind, x, y, z, owner]`; `kind` 0 is pistol, 1 rocket; `owner` is the `gid` of the shooter) and `l` (boxes: `[id, kind, x, y]`; `kind` 1 Ammo, 2 Rockets, 3 Repair).
- `events`, ordered by `t`; `k` is the kind, and the fields are:
  - `fire`: `s` shooter (gid), `w` weapon (0 pistol, 1 rocket), `x`, `y` the target point.
  - `hit`: `s` the one hit (gid), `by` the shooter (always a gid; damage from the zone has no `hit` event, it shows only as a drop in health in `frames[].s` and as a `death` with `by` `-1`), `w` weapon, `d` damage, `splash` whether the damage is from an explosion.
  - `boom`: `x`, `y`, `z` where a rocket exploded, `by` the shooter (gid).
  - `death`: `s` the one who died (gid), `by` the cause (gid, or `-1`), `cause` is `pistol`, `rocket` or `zone`.
  - `pickup`: `s` (gid), `loot` the kind of box (as `kind` in `l`), `id` the id of the box.
  - `zoom`: `s` (gid), `x`, `y` the true position of the zoomed object.
  - `crash`: `f` the fleet that crashed (slot), `msg` the message.
  - `eliminated`: `f` the fleet that is out (slot), `place` its place.
  - `shout`: `s` the shouting shtemer (gid), `m` the message index in the war cry table (0 to 47, see chapter 5). Added within version 1; older readers ignore it.
- `result`: `placements` are fleet ids (slots) from the winner to the last; `stats` has an entry per fleet: `fleet`, `place`, `kills`, `damageDealt`, `damageTaken`, `shotsPistol`, `shotsRocket`, `hits`, `loot`, `survivedTicks`, `budgetExceeded`, `errors`, `crashed`.
- Rounding: positions and the zone 2 decimals, angles 2, health and energy 1, damage 1.
- `gid` is as above: `gid = fleet id * rules.fleetSize + shtemer index`; `frames[].s[gid]` is the shtemer with that `gid`.

## 13. Common mistakes

- Shooting at every `Medium` blip without checking: half of them are yours.
- Only ever looking forward: you won't see an attack from behind until it is inside `ProximityRange`.
- Aiming at the current position of a moving target. The projectile takes time to get there; aim at `position + velocity · (flight time + one tick)`.
- Wasting ammo: firing at distant or unchecked targets drains a supply that only loot refills (see “Ammo is scarce”).
- Firing a rocket at a target close to you or to your own shtemers.
- Forgetting the zone: check `senses.Zone.NextContains(me.Position)` early; a shtemer is not fast.
- The whole fleet in a clump: one rocket hits them all.
- Leaving every decision to the admiral, and then the admiral dies. Check `me.IsAllyAlive(Fleet.Admiral)` and have a successor.
- An expensive computation every tick: the budget runs out before `Thrust`/`Fire`.
