Accounts, published scenarios, and finished runs #
The app uses browser OAuth for explicit scenario and finished-run publication. Game checkpoints and local result history stay in this browser. Signing in does not upload local data.
User flows #
- The account button in the shared app header opens sign-in using an atproto handle, DID, or provider. The official browser OAuth client handles authorization, session storage, token refresh, PKCE, and DPoP. Open the account button menu to sign out.
- Scenarios keeps local board setups and account-owned published scenarios separate. Publishing creates a public copy; it does not move or synchronize the local entry. Publishing again creates another record.
- A public scenario link uses
/scenarios/published/?uri=…&cid=…and includes its AT URI and CID. Anyone can open it while signed out, import a local copy, or start Free Play or a timed challenge at a chosen level. A missing or changed pinned version produces an error. - Library → Finished runs has Local and Published lists, with one list shown at a time. Submit score publishes the existing local run record to the signed-in account's PDS, or submits it to the Guest leaderboard while signed out. The optional public name is available only while signed out. Signing in does not publish local results automatically. The Published list supports refresh and deletion of the current account's records. There is no public run detail page in this slice.
Game over also offers Submit score beside Restart once the result is saved locally. Signed-in players publish with one click. Signed-out players see a guest submission dialog with an optional public name and a sign-in link. Failed submissions can be retried without leaving the result screen.
The built-in Default game starts from an empty board. Use New game to choose its starting level. It needs no account or published scenario record.
Local data belongs to a browser profile and site origin, not to the signed-in account. Switching accounts does not move, hide, or publish local data. A PDS record is public repository data; removing it cannot recall copies others made.
Data boundaries #
| Structure | Purpose |
|---|---|
BoardScenario |
Reusable initial cells and colors, independent of a particular attempt. |
ChallengeDefinition |
Scenario plus starting level, rules, RNG version, and timing policy; identifies a score category. |
GameSessionSnapshot |
Complete live checkpoint used to resume an attempt. |
PublicScenarioRecord |
Public name, creation time, and scenario content in a Lexicon-compatible representation. |
FinishedRun |
Attempt setup and result totals, with no move history. |
FinishedRunRecord |
Public summary and setup for one finished attempt, with no move history. |
Core gameplay stays independent of OAuth and protocol records. Record mappers translate core cell-color tuples into objects supported by Lexicon. The app validates network and browser-storage data before use. A scenario reference is a DID-based AT URI plus a record CID; handles are display and discovery names.
A saved game launched from a public scenario keeps the pinned reference beside its checkpoint. The full challenge remains embedded, so resuming does not require downloading the public scenario again. An imported local scenario is an independent editable copy.
A fresh run records its start and finish wall-clock times, total playing time,
and segment duration; total and segment duration are equal, and both exclude
pauses. A resumed checkpoint records total playing time accumulated before and
after the save, plus the active duration for the recorded segment since resume
(startKind: "checkpoint"). Score, clears, placed pieces, and misses are also
cumulative checkpoint totals. Opening an already-finished checkpoint does not
create another result. Restart starts a fresh attempt. The cumulative total is
required in local and published result records; missing totals are rejected
without migration.
The local history retains the most recent 200 results. A “Fresh attempt” label
describes the stored start kind; it does not prove that the attempt was never
interrupted or resumed through another path.
Published finished-run records contain one setup: seed, starting level, rules
version, RNG version, and initialBoard (one topology fingerprint, occupied
cells, and colors). Every initial board includes the required integer
subdivision, including 4 for the default board. Empty arrays represent the
default starting board. An optional publishedScenario pins the source AT URI
and CID; the embedded layout
keeps the result self-contained. Publishing a result does not publish a separate
scenario.
Challenge identity is computed from setup when displaying or indexing results;
it is not stored in the record, and seeds do not distinguish score categories.
Ending level is derived from starting level and faces cleared. Continuous timing
is part of the current rules version, rather than a separate run field. RNG
version remains explicit. placedPieces names the placement counter,
segmentPlayingTimeMs is playing time since the recorded start/resume, and
totalPlayingTimeMs includes earlier play. endReason is strikes or jammed;
the UI supplies the explanatory sentence. The current rules end strikes at three
misses and jammed runs before that limit.
The record's $type identifies its schema; there is no second numeric result
version. Future incompatible public schemas should use a new Lexicon NSID.
Local storage documents and core checkpoints retain their own formats. Old run
payloads are rejected without migration. Run records have no reserved replay
field; a replay format will be designed when verification is implemented.
All results are unverified. A score, CID, account author, or valid record does not prove how the score was earned, who played, whether play was solo, or whether the result is complete. Records produced by this feature contain no move history or replay reference and preserve no evidence about moves between checkpoints. The AppView displays both indexed records and separately stored anonymous submissions on public leaderboards; neither source provides verification or moderation. Checkpoints and named saves remain local. Publishing them is deferred until a separate design defines the source and parent checkpoint history needed to interpret resumed results.
Each local run ID maps to a deterministic valid record key. Before creating a record, the app checks that key; retries and concurrent attempts accept an existing record only when its normalized payload matches the intended result. This avoids duplicate records after an unclear network response without storing a separate local publication map. If the key already holds a different result, the app reports a conflict and leaves the existing record unchanged. Deletion uses the loaded CID as an AT Protocol swap guard. Removing a record cannot recall copies others made.
Submitting the same local run as Guest and later publishing it from the same browser can replace the Guest leaderboard entry. The browser sends its run ID and the exact URI/CID returned by PDS publication to the AppView. The AppView links only a Guest row owned by that browser's HttpOnly cookie, and hides that row only after indexing confirms an active account's exact URI, CID, and full canonical run payload. Links wait in SQLite across restarts when indexing is delayed, and are checked by live commits, PDS snapshots, and account reactivation. A mismatch or inactive account leaves the Guest result visible. The confirmed Guest row remains as a hidden receipt after public deletion, so retrying the same run cannot make a second Guest entry.
The browser saves unaccepted linking requests locally and retries them across page navigation and app reloads. Failed requests use increasing delays from 5 seconds up to 60 seconds, with no retry limit, and retry when connectivity returns. These requests never republish a run. Once the server accepts a link, the existing ingestion process waits for its matching publication.
Collections and permissions #
Checked-in Lexicons use:
net.oscillatory.radcohesion.scenarionet.oscillatory.radcohesion.run
OAuth requests atproto and create/delete permission for the scenario and run
collections. Existing sessions created before run permissions were requested
receive an actionable error when a publish or delete is attempted; sign out and
sign in again, then approve the updated access request. The app does not request
game-save writes or Bluesky social permissions. Records are parsed locally even
if the PDS does not know these Lexicons. Record creation leaves server
validation optional rather than requiring an unpublished schema to be
resolvable.
Node deployment #
Build with the exact public HTTPS address, including the deployment subdirectory:
SITE_URL=https://oscillatory.net/rad-cohesion/ npm run build
Build and run the full Node application at that address. SvelteKit serves
oauth-client-metadata.json, the OAuth callback page, prerendered game pages,
and the AppView APIs from one origin. The public metadata URL is the OAuth
client ID; the browser OAuth client needs no client secret. Changing the host
or deployment path requires rebuilding with the new SITE_URL.
For local OAuth, open http://127.0.0.1:5173/game/ and keep the same host and
port through the callback. The development app can sign in to and publish
directly to a real PDS; it does not require a deployed app, a local PDS, or a
public metadata URL. The SDK's loopback client ID uses http://localhost while
its callback uses 127.0.0.1; those hostnames have separate browser storage,
so the app offers a link to the same page at the IP address when opened on
localhost. The link preserves the port, app base, query, and fragment. Click
Sign in again after switching; browser saves and the active game remain
on the original hostname.
AT Protocol's localhost client-ID exception is optional for authorization
servers, so local OAuth availability depends on the provider. See the official
OAuth specification's localhost client development section.
HANDLE_RESOLVER configures the trusted handle resolution service at build time
(default https://bsky.social). Public scenario reads resolve the author's DID
to its current PDS; they do not assume every account is hosted by Bluesky.
OAuth callback and metadata responses are excluded from service-worker caching.
Local gameplay and libraries continue to work offline; authentication,
publication, and downloading public scenarios require connectivity.
To make the Lexicons discoverable on the network, publish their JSON definitions
as com.atproto.lexicon.schema records in the chosen schema owner's repository,
with each NSID as its record key. Then configure:
_lexicon.radcohesion.oscillatory.net TXT "did=did:plc:<schema-owner>"
The namespace authority is net.oscillatory.radcohesion, whose reversed domain is
radcohesion.oscillatory.net. Publishing schemas and setting DNS are deployment
steps; checked-in JSON does not perform them. See the official
Lexicon publishing guide and
OAuth guide.
Next slices #
The AppView can backfill and tail published runs and scenarios into SQLite when configured with a Jetstream API key. It serves paginated best-per-player and all-run leaderboards grouped by shared challenge identity, and accepts anonymous results without an AT Protocol account.
- Add scenario discovery, eligibility controls, and moderation workflows.
- Define a replay format and deterministic verification for separately verified results.
A list of records from one PDS is not a global leaderboard. AT Protocol stores and distributes records; ranking, discovery, and eligibility still need an app service and explicit rules.
Verification and deployment status #
The OAuth browser specs exercise the real browser SDK against mocked identity,
OAuth, and PDS endpoints, including PKCE and signed DPoP requests. They do not
use a real account. No real-account sign-in or publishing test has been
performed. Local real-PDS use does not require deployment when the provider
supports the protocol's optional localhost client-ID exception. A hosted app
still needs its public metadata URL in SITE_URL before OAuth can be used from
that hosted origin. The app does not publish Lexicons or configure DNS during
a build.