# Multiple event ladders

One server hosts Public and any number of event ladders. Accounts, bot identities,
tokens and version labels are shared. Ratings, records, qualification, rating
charts, matchmaking history, map exposure and house opponents belong to a ladder.
Existing matches and ratings remain in `public` when the database upgrades.

Players visit [Events](https://arena.victris.ai/ladders), sign in, and join:

| Access | Admission |
| --- | --- |
| `open` | Signing in and joining admits the account immediately. |
| `approval` | Joining requests admission; staff approve the account. |
| `invite` | An individual code admits its recipient once. Staff may bind it to an account. |

Public remains available without enrollment. Event admission belongs to the
account and covers all its bots. Legacy bots must be claimed before entering an
event. Approval is a staff decision: it does not automatically verify conference
registration. Staff can check a registration roster before approving contestants.
Every leaderboard, match and replay is public to spectators; admission controls
participation and event flag access.

After admission, the shipped TypeScript and Python starters select a ladder with:

```bash
ARENA_LADDER=saintcon-2026 ARENA_TOKEN='<your bot token>' node bot.ts
```

Custom clients send `{"type":"queue","version":"v1","ladder_id":"saintcon-2026"}`.
Omitting `ladder_id` selects Public. A connected bot can queue in one ladder at a
time, changing ladders between matches. Each successful `queued` reply and ladder
`end` includes `ladder_id`. Reconnect with the same selection. `unknown_ladder`,
`ladder_closed`, `enrollment_required` and `wrong_ladder` reject the queue; correct
the selection or admission and queue again. The starters stop on admission or
schedule errors; launch them once the ladder opens, or restart after resolving
the error. A rejected selection does not replace
an existing valid queue. Human games use Public.

## SAINTCON 2026

`ladders/default.json` prepares `saintcon-2026` with staff approval, up to 40
concurrent matches and four per owner. Enrollment is available before the window;
play opens October 27, 2026 at noon and closes October 30 at 6:00 AM in
America/Denver. The associated achievement event uses the same ladder and window.
Flag earning remains disabled until its secrets are configured and the event is
enabled; ladder play and achievement earning have separate switches.

The admin console's **Events** section lists each ladder with the events that play
on it (`/admin#ladders/<id>` opens one). A ladder's controls edit names,
descriptions, visibility, enrollment/play enablement and registration policy. Public stays visible, enabled
and open. `visible` defaults to true; hidden ladders and their achievement events
are omitted from public discovery lists. Signed-in accounts still see hidden
ladders where they have a membership; direct leaderboard links, enrollment routes
and achievement APIs remain available. Share `/ladders?ladder=<id>` to let new
contestants join or redeem invitations for a hidden ladder. Visibility is a
discovery setting.

Each event's controls show its earning switch,
official opponent readiness, flag configuration and unique player counts for each
achievement. Counts include valid awards across all of an account's bots and
versions; revoked awards have separate totals. Secret flags stay in server
configuration. Enabling earning requires approved house policies, accounts and all five flags. Disabling earning preserves previously earned flag access.

Admin edits persist in the ladder database and take precedence over manifest
values on restart. They do not change maps or achievement conditions. The
**Schedule** editor moves a ladder's window and every event it hosts together.
Staff can move only future times: an opening time that has passed is final, a
closed ladder's window is final, and new times must be in the future. A ladder
without events may drop its closing time to play on; Public always plays
continuously. Invitations keep the expiry they were created with.
Registration changes preserve existing admissions. The section also lists pending/active/revoked contestants,
approves accounts, revokes admission with a reason, and creates/revokes individual
invitations. Codes are shown once, stored only as SHA-256 hashes, and absent from
audit entries and subsequent listings. They expire after seven days or at ladder
closing. An unassigned code is a bearer invitation: whoever redeems it first is
admitted. Account-bound codes require the intended account. Use the invite access
policy when invitation redemption should control admission.

Revocation removes waiting bots from that ladder, blocks future queues and flag
reveals, and prevents active matches from being rated or earning awards. A later
approval starts a new enrollment identity, so it cannot rescue an earlier match.
Matches finishing at or after closing remain viewable but are unrated and cannot
earn flags. Previously recorded standings and awards remain historical evidence;
use the separate award-revocation control when an earned award must be invalidated.
Eligible contestants can reveal earned flags after closing while secrets remain
configured. A global account ban applies across all ladders.

## SAINTCON precon

The bundled `saintcon-precon-2026-10-12` ladder accepts open preregistration.
It plays continuously from October 12, 2026 at noon to October 16 at 6 AM,
America/Denver. Sign in and preregister before opening; use the simulator and
starter bots to prepare, then queue with its `ARENA_LADDER` ID when play opens.
The directory shows the event's purpose, audience, format and preparation notes.
Its standings are independent of Public and the main SAINTCON approval ladder;
this playtest offers five unofficial testing flags with no HC submissions. See [EVENTS.md](/docs/events).

## Preparing another event

The CLI loads `ladders/default.json`; override with `serve --ladders <path>` or
`ARENA_LADDERS_FILE`. A comma-separated list of paths joins their manifests, so an
opt-in ladder can be added without copying the defaults (for example
`ladders/default.json,benchmarks/ladder.json`). A manifest is an array of public definitions:

```json
[
  {
    "id": "my-event-2027",
    "name": "My Event 2027",
    "description": "A competition for invited contestants.",
    "access": "invite",
    "enabled": true,
    "visible": true,
    "opens_at": "2027-05-01T09:00:00-06:00",
    "closes_at": "2027-05-02T17:00:00-06:00",
    "maps": ["arena-21"],
    "max_concurrent": 40,
    "owner_concurrency": 4
  }
]
```

Public is inserted automatically. IDs are stable lowercase kebab names; use a new
ID for the next competition. Empty `maps` uses every server map; omitted dates
allow an ongoing ladder. The window and maps freeze in the manifest after the first
recorded match; staff can still move a window's future from the admin console. Access policy, description, enabled state and capacity can be changed by
editing the manifest and restarting, unless that field has a persisted admin
override. Changing access does not revoke already
admitted accounts. Definitions omitted from a later manifest are retained; close
or disable them explicitly to stop play. Closed boards remain browsable.

`serve --house` launches separate copies of the official policies for each enabled
upcoming or open ladder. Public keeps `house-*`; event names use a stable digest prefix. Each house
identity is pinned to one ladder and kind. Find its name in the event manifest or
`GET /api/lobby?ladder=<id>`. Standard policies anchor that ladder's rating scale;
the champion is measured as a competitor. Player names that collide with an
official house identity cause startup to refuse that roster.

An achievement event's `ladder_id` must identify a configured ladder. For event
ladders its window must match the ladder's window exactly (a window moved by staff
counts for both). Its four opponents must
name that ladder's official house identities and approved policy versions (any champion version counts). See
[event setup](/docs/events) for secrets, readiness, artifacts and flag modes. Ordinary
ladders need no flag provider or achievement definition.

Global server capacity, hourly bot quotas, version limits and pacing still apply.
Within each round, available slots are shared between active ladders, with the
first ladder rotated; per-ladder and per-owner ceilings provide additional bounds.
Each scope has its own pairing history, discovery and house-request credits.

## API and persistence

`GET /api/ladders` publishes policies, windows, maps, capacity, phase and links.
Add `?ladder=<id>` to leaderboard, matches, lobby, bot details or rating history.
These default to Public. `/api/status` is aggregate unless a ladder is requested.
Individual match responses always identify their ladder; match IDs remain global.

Browser enrollment routes use Supabase sign-in, origin checks and `private,
no-store` responses and reject agent keys, bot tokens and MCP tokens. The MCP tools
`list_my_ladders` and `enroll_ladder` inspect admission and join through the same policy
with bot/event consent and explicit owner approval. They cannot reveal flags or override
admission; bot tokens cannot enroll. See [MCP.md](/docs/mcp). Staff endpoints accept the admin's browser session or agent key.
See [API](/docs/api) for exact routes. Staff access grants no contestant flag bypass.

Schema 15 adds ladder definitions, memberships, hashed invitations, house bindings,
scoped versions/pair totals and a ladder ID on matches. Scope facts, awards and
ratings commit with the completed match. Back up before upgrading; an older build
refuses the newer database. Membership rows and bound invitations are removed or
revoked when an account is deleted; match history and audited decisions remain.
