geometric packing game (tetris-ish) on an icosahedron. save boards and completed runs as records in your atproto PDS oscillatory.net/rad-cohesion
7.9 kB · 127 lines
Markdown

Records and APIs #

This reference covers public data contracts. Start with Architecture for ownership and data flow, or Deployment for OAuth and ingestion configuration. Core checkpoint contracts are in sessions and snapshots.

Published records #

Checked-in Lexicons define net.oscillatory.radcohesion.scenario and net.oscillatory.radcohesion.run. Shared parsers and mappers live in src/records/; network and storage input must pass validation before use. Cell/color tuples in the core map to Lexicon-compatible objects on the wire.

Scenarios contain the name, creation time, identity, explicit polyhedron and subdivision, topology fingerprint, occupied cells, and colors. Shape is icosahedron or octahedron, subdivision is an integer from 1 through 10, and every cell/piece ID must belong to that board. Scenario identity describes canonical board content, independent of the name and publication location.

A public reference is a DID-based AT URI plus record CID. Handles are discovery and display metadata. Pinned reads fail if that exact version is unavailable; they do not silently substitute a newer layout. Local imports are separate editable copies. Saved challenges embed their full layout beside the reference.

Runs contain one setup: seed, starting level, rules version, RNG version, and initialBoard. That board requires shape, subdivision, fingerprint, occupied cells, and colors; empty arrays represent the default starting board. Optional publishedScenario pins the source URI/CID. Publishing a run does not create a scenario record. Challenge identity is derived from the setup, not stored in the record, and excludes seed and publication URI. Ending level derives from starting level and faces cleared. Continuous timing is part of the run's rules version; RNG version remains explicit.

Results include start/finish wall-clock times, score, cleared faces, misses, placedPieces, segmentPlayingTimeMs, totalPlayingTimeMs, startKind, and endReason (strikes or jammed). Fresh attempts have equal segment and total playing time. Checkpoint attempts retain cumulative totals while segment time counts active play since resume. Pauses are excluded. An already-finished checkpoint does not create another result; restart creates a fresh attempt. Missing cumulative time or board compatibility metadata is rejected.

The public $type identifies the schema; runs have no second numeric record version. Incompatible public schemas need a new Lexicon NSID. Local storage documents and core snapshots have independent versions. There is no automatic missing-field conversion. All results are unverified summaries: valid JSON, CID, author, or timestamps do not prove earned scores or uninterrupted play. Records contain no move history or replay reference. Checkpoint publication and replay verification remain roadmap work.

Publication and retries #

OAuth requests atproto plus create/delete permissions for the two app collections; it requests neither game-save nor Bluesky social writes. An insufficient session grant produces an actionable sign-out/sign-in error. The browser SDK handles PKCE, DPoP, refresh, and OAuth session storage. Record creation allows unresolved Lexicons rather than requiring public schema discovery; local parsing still validates the payload.

Each local run ID maps to a deterministic record key. Before creating it, publication checks the key; retries and concurrent publishers accept an existing record only when the normalized payload matches. A different payload at that key is a conflict. Deletion uses the loaded CID as a swap guard. Publishing scenarios creates independent copies. Deleting public records cannot recall copies others made. Signing in never automatically publishes local data.

Public reads #

Paths are relative to the deployment base. Reads require no login.

Endpoint Query and response
GET /api/scenarios Optional trimmed name substring q (≤64 characters), limit (default 24, max 100), offset (max 100,000). Newest active-author scenarios, exact URI/CID, name, author/handle, publication date, occupied count, layout, and pagination total.
GET /api/players/<DID> kind=scenarios|runs, limit (default 24, max 100), offset (max 1,000,000). Player metadata and paginated publications; unknown/inactive players return 404. Local and Guest items are excluded.
GET /api/leaderboards challenge=<identity>, mode=best|all, limit (default 25, max 100), offset (max 100,000). Ranked current results for an exact challenge.
GET /api/leaderboards/overview limit (default 24, max 100), offset (max 100,000), optional challenge=<identity>. Submitted layouts, populated starting levels/identities, run counts, latest activity, and representative pinned references.

Reads use SQLite, without per-item PDS requests. Standings combine active indexed account runs and unpromoted Guest runs. Best mode selects each source identity's highest score before pagination; All keeps all current results. Equal scores share a rank; stable secondary ordering affects only row display. Account labels use a currently verified handle when available, otherwise DID; blank anonymous names display as Guest.

Overview groups equivalent board layouts independently of publication URI. runCount counts eligible submissions; bestRunCount applies the same per-player selection as standings. Names are taken from current indexed scenarios only when URI, CID, and active author still match. Other custom layouts are labeled Custom scenario. Looking up a challenge's submitted snapshot keeps its standings and play setup accessible after source deletion.

Guest submissions and publication linking #

GET /api/leaderboards/identity issues a random one-year HttpOnly, SameSite=Lax cookie scoped to the API path. SQLite stores its SHA-256 hash, not the bearer value. POST /api/leaderboards/runs accepts an existing local FinishedRun as run, plus optional displayName (≤40 characters). It uses the same run validator and challenge derivation as ingestion and creates no AT Protocol record.

The same run ID and payload retry idempotently after rate checks; the first display name is retained. Reusing an ID with a different payload conflicts. Names and results are public, and the cookie establishes browser ownership rather than verified player identity.

POST /api/leaderboards/promotions accepts at most 200 unique {runId, uri, cid} references owned by that cookie. It never treats a caller-supplied DID as proof and creates neither a Guest identity nor a submission. Pending links persist until indexing confirms an active account's exact URI, CID, and full canonical run payload. Commits, PDS snapshots, and reactivation reconcile links. Mismatched, absent, invalid, or inactive publications leave Guest rows visible. Confirmed rows remain hidden receipts: deleting the publication does not restore the Guest result, and retries cannot create another Guest entry.

The browser queues unaccepted link requests locally and retries across navigation/reload, with delays increasing from 5 to 60 seconds and on restored connectivity. It does not republish records. Once accepted, the server waits for indexing independently of the browser.

Submission/linking requests check same-origin, cookie ownership, a 128 KiB body limit, a 15-second body deadline, 30 requests/minute per peer IP, and 10/minute per cookie. Bounds include 200 Guest runs per browser, 100,000 total runs, and 200,000 identity rows. Empty identities expire after 30 days; cleanup runs at most hourly after the identity endpoint's peer-IP check. Configure proxy trust as described in Deployment so visitors do not all share the proxy's rate-limit identity.