# Sley Arena rules (ruleset `S1`)

Sley Arena is a weaving game for bots. Each bot is a **shuttle** that pulls a coloured
thread across a loom. Shuttles claim **cloth** by looping out from their own cloth and
tying the loop off back home. Every match ends as a woven tapestry.

This document is normative: the engine (`crates/arena-core`) implements exactly this, and
every rule below is deterministic. Numbers in `code` are the S1 constants (see
`Ruleset::s1_duel()` / `Ruleset::s1_brawl()`).

## 1. The loom

| | Duel | Brawl |
|---|---|---|
| Shuttles | 2 | 4 |
| Board (`W × H`) | `48 × 30` | `64 × 40` |
| Match length | `600` ticks | `800` ticks |

Coordinates are `(x, y)` with `0 ≤ x < W`, `0 ≤ y < H`, origin top-left. Directions:
`0 = up (y−1)`, `1 = right (x+1)`, `2 = down (y+1)`, `3 = left (x−1)`.

Every cell has:

- `owner`: none, or one seat;
- `layer`: `1` (single-woven) or `2` (double-woven) when owned;
- `thread`: none, or the seat whose **loose thread** lies there;
- `pillar`: impassable, never owned;
- `spool`: a fuel spool lies there;
- `motif`: none, or the id of the motif stencil the cell belongs to.

### 1.1 Map generation (seeded, mirrored)

All randomness comes from one seeded PRNG (`SplitMix64` seeded with the match seed; the
match keeps drawing from the same generator for spools, section 5). A draw "below `k`" is
`next_u64() mod k`; a draw "in `lo..=hi`" is `lo + below(hi − lo + 1)`. Maps are symmetric
so every seat faces the same board:

- **Duel:** point symmetry, `(x, y) ↔ (W−1−x, H−1−y)`.
- **Brawl:** both mirror axes, `(x, y) ↔ (W−1−x, y) ↔ (x, H−1−y) ↔ (W−1−x, H−1−y)`.

Spawns:

- Duel: seat 0 at `(9, 14)` facing right; seat 1 at its mirror, facing left.
- Brawl: seat 0 `(10, 9)` right, seat 1 `(W−11, 9)` left, seat 2 `(10, H−10)` right,
  seat 3 `(W−11, H−10)` left.

Each seat starts owning the single-woven `5 × 5` square centred on its spawn.

**Images.** Image `k` of a cell maps seat 0's spawn onto seat `k`'s spawn. Duel: image 0 is
the cell itself, image 1 is `(W−1−x, H−1−y)`. Brawl: image 0 the cell, image 1
`(W−1−x, y)`, image 2 `(x, H−1−y)`, image 3 `(W−1−x, H−1−y)`. "Within `d` of a spawn"
always means Manhattan distance `≤ d`.

**Pillars:** short straight segments (length 2–5) placed with all their mirror images,
never within a Manhattan distance of `7` of a spawn and never within 2 cells of the edge.
Duel places `12` segment groups, brawl `8`. A segment group is one segment plus its images.
Each group gets up to `50` attempts; an attempt draws, in this order, the length
(`2..=5`), the orientation (`below(2) = 0` is horizontal), `x` in `2..=W−3` and `y` in
`2..=H−3`. The segment runs right (horizontal) or down (vertical) from `(x, y)`. The
attempt is rejected unless every cell of every image:

- lies in `2..=W−3 × 2..=H−3` (two free cells along every edge);
- is more than `7` from every spawn;
- is not another image of the same group (no self-overlap);
- has no existing pillar among its 8 neighbours or itself;

and, once placed, all non-pillar cells are still 4-connected. A group whose 50 attempts
all fail is skipped.

**Motif stencils** (section 6) are placed after pillars, with their mirror images, on
cells that are not pillars and not within `6` of a spawn. First the season motif's shape
is drawn (`below(3)` into the season family, in the order listed in section 6), then the
wildcard's (`below(8)` into the wildcard library, in table order). Then, for the season
motif and then the wildcard, up to `200` attempts each draw a top-left corner `x` in
`1..=W−1−w` and `y` in `1..=H−1−h` (`w × h` is the stencil size). Stencils are never
rotated; copy `k` is image `k` of every stencil cell (so mirrored copies are mirrored
shapes). An attempt is rejected unless every cell of every copy keeps one free cell
along every edge, is not a pillar, is not part of another motif or another copy, and is
more than `6` from every spawn. A motif whose attempts all fail is skipped. Motif ids
count from `1`: the season copies first (image order), then the wildcard copies.

## 2. A tick

Each tick runs these phases in order. All seats act simultaneously: every decision in a
phase reads the state as it was at the start of that phase.

1. **Decide.** Every seat that is not stunned runs its bot and gets a direction (section 4).
   Asking to reverse (the opposite of the current heading) means "keep going straight".
   The fuel a call reports is capped at its fuel limit before it is subtracted.
2. **Fray.** A seat whose target cell is off the board or a pillar is snipped (it frays
   against the frame or a pillar). It does not move.
3. **Collide.** Using positions and targets as they are at the start of this phase:
   - Two or more seats with the same target cell are all snipped.
   - Two seats swapping cells (A moves into B's cell while B moves into A's) are both
     snipped.
   - A seat whose target holds a shuttle that was stunned at the start of the tick is
     snipped; the stunned shuttle is unaffected. Shuttles snipped earlier in the same
     tick are not obstacles, and moving into a cell that another shuttle is leaving this
     tick is fine.
4. **Move.** Remaining seats move to their targets.
5. **Cross.** Using the threads as they were at the start of this phase:
   - a seat that lands on its own loose thread is snipped;
   - a seat that lands on another seat's loose thread snips that seat (section 3.2).

   Several crossings in one tick are all applied, including mutual ones (both seats are
   snipped and each takes a bounty from the other). A seat that crosses a rival and is
   itself crossed in the same tick still collects its bounty. All bounties of this phase
   are computed from the fuel every seat has at the start of the phase and applied
   together.
6. **Weave.** For each seat that moved and was not snipped this tick, in order starting
   from seat `tick mod n` (`tick` counts from 1) and wrapping around; a seat swallowed
   earlier in the same pass is skipped:
   - if it stands on its own cloth and has loose thread: **tie-off** (section 3.1);
   - else if it stands outside its own cloth: lay loose thread on that cell.
7. **Spools.** A seat that moved this tick and was not snipped picks up the spool on its
   cell: `fuel += spool_fuel`, capped at the tank size. The spool is used up even if the
   tank was full.
8. **Upkeep.**
   - A seat that owns no cloth is snipped (cause `unraveled`). This check repeats, in
     seat order, until every seat owns cloth (a snip's re-take can strip another seat's
     last cloth).
   - On every tick that is a multiple of `spool_every` (`40`), new spools appear
     (section 5).
9. **End.** After the last tick the match ends and is scored (section 7).

**Snips take effect at once.** A seat snipped in any phase immediately loses its thread,
returns to its spawn and is stunned (section 3.2); it takes no further part in that tick's
moving, crossing, weaving or spool pickup. Shuttles may briefly share a cell (for example
when a snipped seat returns to a spawn cell a rival is standing on).

A stunned seat skips Decide and does not move. Its stun counter counts down by one at the
end of each tick, except on the tick it was snipped: a seat snipped on tick `t` sits out
ticks `t+1 … t+stun_ticks` and is called again on tick `t+stun_ticks+1`. A seat snipped
while stunned starts a full stun again.

After Upkeep the engine records motif changes (section 6). A seat's **live score** is its
cloth plus the bonuses of the motifs it currently completes; bot inputs and the timeline
show it every tick.

## 3. Weaving

### 3.1 Tie-off

When seat `s` returns to its own cloth carrying loose thread `L`:

1. **Enclose.** Starting from every board-edge cell that is neither `s`'s cloth nor in
   `L`, flood 4-connected through cells that are neither `s`'s cloth nor in `L`
   (pillars are passable for this flood). Every cell not reached, not `s`'s cloth and
   not in `L`, is **enclosed**.
2. **Claim** every cell in `L` and every enclosed non-pillar cell:

   | Cell before | Cell after |
   |---|---|
   | empty | `s`, single |
   | rival, single | `s`, single (stolen) |
   | rival, double | rival, single (**unpicked**: thinned, not stolen) |

3. **Reinforce.** Every cell of `s`'s own single-woven cloth inside the loop's bounding
   box (the min/max `x` and `y` of `L`) becomes double-woven. Only cloth `s` owned
   *before* this tie-off counts: cells claimed in step 2 stay single.
4. **Swallow.** A rival whose loose thread lies on any cell of the claim region (every
   cell of `L` and every enclosed non-pillar cell, including cells that were only
   thinned) is snipped by `s` (section 3.2), in seat order. Each pays its bounty from its
   fuel at that moment.
5. `s`'s loose thread is cleared.

Enclosure is global: any area cut off from the board edge by `s`'s cloth and `L` is
claimed, wherever it is, including rival cloth islands surrounded by `s`'s cloth.

Double-woven cloth can't be stolen by one loop. A first loop thins it, a second steals it.

### 3.2 Snips

A snipped seat:

- loses all its loose thread;
- returns to its spawn cell and takes its spawn heading (the heading it had at the start
  of the match);
- if it no longer owns its spawn cell, re-takes the `3 × 3` square around it as
  single-woven cloth (non-pillar cells; any owner's cloth there, single or double, is
  overwritten);
- is stunned for `stun_ticks` (`6`).

Its notebook is kept.

**Bounty.** When seat `s` snips a rival by crossing or swallowing its thread, `s` takes
`bounty_pct` (`25%`) of the victim's current fuel, rounded down (`fuel × 25 / 100`); the
victim loses that amount. If several seats snip the same victim in one tick, the bounty is
split equally (remainder to the lowest seat). Fuel gained from bounties is capped at the
tank size; the excess is lost. Frays, collisions, self-crossings and cloth-loss snips pay
no bounty. A seat that is crossed by a rival and also lands on its own thread in the same
tick counts as crossed.

## 4. Bots and fuel

A bot is a Sley program with this function:

```python
def act(board: list[int], info: list[int], notes: list[int]) -> int:
```

### 4.1 Fuel

- Each seat starts with a full tank of `tank` fuel: `240,000,000` VM fuel units in a duel,
  `320,000,000` in a brawl.
- Each call to `act` runs with a fuel limit of `min(remaining fuel, move_cap)`
  (`move_cap = 16,000,000`). The fuel it uses is subtracted from the tank.
- If the call runs out of fuel or traps, the seat keeps going straight and nothing is
  written to the notebook. The fuel it used is still spent.
- A seat with an empty tank is not called and keeps going straight ("dry"). Spools and
  bounties can refill it.
- Spools add `spool_fuel`: `24,000,000` in a duel, `32,000,000` in a brawl.

Fuel is exact and identical on every machine: it counts Sley VM actions, not time.

### 4.2 Inputs

All seats are **relative**: in the inputs, seat `1` is always the bot itself and seats
`2..n` are the rivals in seat order after it.

`board` has `W × H` ints, row-major (`board[y * W + x]`). Each cell's bits:

| Bits | Meaning |
|---|---|
| `0–2` | owner: `0` none, `1` me, `2..4` rivals |
| `3` | `1` = double-woven |
| `4–6` | loose thread: `0` none, `1` me, `2..4` rivals |
| `7` | pillar |
| `8` | spool |
| `9–13` | motif id (`0` none) |
| `14–16` | shuttle standing here: `0` none, `1` me, `2..4` rivals |

`info`:

| Index | Meaning |
|---|---|
| `0` | `W` |
| `1` | `H` |
| `2` | tick being decided (starts at 1 on the first decision) |
| `3` | last tick |
| `4` | number of seats `n` |
| `5` | tank size |
| `6` | spool fuel |
| `7` | motif count `m` |
| `8 + 8k ..` | seat `k+1` (me first): `x, y, dir, fuel, stun_left, score, cloth, thread_len` |
| `8 + 8n + 4j ..` | motif `j`: `id, cells, bonus, my_cells` |

Positions and directions are absolute. `score` is the live score. Motifs are listed in id
order; `my_cells` counts the motif's cells I own (either weave). If several shuttles share
a cell, bits `14–16` show the lowest relative seat.

`notes`: the bot's 8 notebook numbers, all `0` at the start of a match.

### 4.3 Output

The returned int `r` encodes:

| Part | Meaning |
|---|---|
| `r & 3` | direction (`0` up, `1` right, `2` down, `3` left) |
| `(r >> 2) & 15` | notebook slot to write: `0` none, `1..8` slot `0..7` |
| `r >> 6` | the value to write (arithmetic shift, signed) |

Slot codes `9..15` write nothing. A bot that only moves returns the direction.

## 5. Spools

Every `40` ticks (starting at tick 40, during Upkeep), the engine tries up to `64` random
cells. It places spools on the first cell whose mirror images are all empty of pillars,
threads, spools and shuttles (owned cloth is fine). It places one spool per seat in total
(the cell and its images), skipping the wave when that would put more than `2n` spools on
the board. Each try draws `x` (`below(W)`) and then `y` (`below(H)`); a skipped wave draws
nothing.

## 6. Motifs

A motif is a small stencil shape painted onto the board. Each match places:

- one **season motif**, drawn from the season's theme family;
- one **wildcard motif**, drawn from the full library.

Each is placed with its mirror images (2 copies in a duel, 4 in a brawl). Every copy is
its own motif with its own id.

A motif is **complete** for seat `s` when `s` owns every non-pillar cell of it, either
weave. At the end of the match each complete motif adds `bonus = 2 × cells` to its
owner's score. (Motif cells are never pillars.) Completion is checked at the end of every
tick; it counts in the live score while it lasts and can be lost again.

**Season S1 theme: "Squares"** (solid `3×3`, ring `5×5`, ring `4×4`).

Timeline `kind` names: `solid3`, `ring5`, `ring4`.

**Wildcard library** (the wildcard is drawn from this table only):

| Shape | Size (`W×H`) | Timeline `kind` | Cells |
|---|---|---|---|
| plus | `5×5` | `plus` | 9 |
| diamond | radius 2 | `diamond` | 13 |
| stripe | `1×7` (vertical) | `stripe` | 7 |
| L | `4×4` | `ell` | 7 |
| T | `5×3` | `tee` | 7 |
| zigzag | `5×4` | `zigzag` | 8 |
| checker | `4×4` | `checker` | 8 |
| knot | `5×5` | `knot` | 16 |

### 6.1 Stencils

`#` is a motif cell, `.` is not; `x` grows to the right and `y` downwards. These are the
shapes of copy 0; the other copies are their mirror images.

```text
solid3 (9)   ring5 (16)   ring4 (12)   plus (9)     diamond (13)
###          #####        ####         ..#..        ..#..
###          #...#        #..#         ..#..        .###.
###          #...#        #..#         #####        #####
             #...#        ####         ..#..        .###.
             #####                     ..#..        ..#..

stripe (7)   ell (7)      tee (7)      zigzag (8)   checker (8)  knot (16)
#            #...         #####        ##...        #.#.         .#.#.
#            #...         ..#..        .##..        .#.#         #####
#            #...         ..#..        ..##.        #.#.         .#.#.
#            ####                      ...##        .#.#         #####
#                                                                .#.#.
#
#
```

## 7. Scoring

`score = cells owned (either weave) + bonuses of complete motifs`.

The highest score wins. Equal top scores are a draw (no winner, even if other seats score
lower).

## 8. Replays

A match is fully reproduced by:

- the ruleset id;
- the seed;
- each seat's bot id;
- the per-tick record of each seat's **final direction** and **fuel spent**.

Replay files store everything except the bot ids (`docs/REPLAY_FORMAT.md`); the bot ids
live in the match metadata (`GET /matches/:id`), since the board does not depend on them.
So anyone can re-run the rules and watch any match.
