diff --git a/.agents/daily-objectives/SKILL.md b/.agents/daily-objectives/SKILL.md index fcebc0f..5eac96b 100644 --- a/.agents/daily-objectives/SKILL.md +++ b/.agents/daily-objectives/SKILL.md @@ -25,7 +25,7 @@ Top-level keys, in declaration order (all accept three casings): | `levelProgressionMaps` | array of objects | | **`dailyObjectives`** | **jagged array** — `FCAOHDFPEAP[][]` | | `serverMaintenance` | object | -| `autoMicMutingConfig` | object | +| `autoMicMutingConfig` | object |econ | `storefrontConfig` | object | | `roomKeyConfig` | object | | `roomCurrencyConfig` | object | diff --git a/.agents/weekly-challenge-config/SKILL.md b/.agents/weekly-challenge-config/SKILL.md new file mode 100644 index 0000000..5e3b770 --- /dev/null +++ b/.agents/weekly-challenge-config/SKILL.md @@ -0,0 +1,183 @@ +--- +name: weekly-challenge-config +description: Read and author the `Config` rule tree in apps/econ/static/weekly-challenge.json — node types, scene-id predicates, and the shared-scene traps +--- + +# The weekly-challenge `Config` rule tree + +Reference for reading and writing the `Config` field of a challenge in +`apps/econ/static/weekly-challenge.json` (served by `GET /api/challenge/v2/getCurrent`). + +**The server never evaluates these rules.** The client reads the tree, watches its own +gameplay, and posts the tree back to `/api/challenge/v2/updateProgress` with its verdict. +So the tree is a _specification handed to the client_, and a malformed one fails silently — +the challenge just never completes. Nothing server-side will tell you. + +Everything here was read off one captured live rotation, not a spec. Meanings marked +_(inferred)_ are read from how values line up with the strings the client renders; the rest +are pinned by the data. + +## `Config` is an escaped JSON string + +Not a nested object. In the file it looks like: + +```json +"Config": "{\"ct\":0,\"ipc\":false,\"wc\":[...]}" +``` + +Author the tree as an object and stringify it into the field — don't hand-escape: + +```sh +bun -e 'const t={ct:0,ipc:false,wc:[{ct:6,vs:[2]}]}; console.log(JSON.stringify(JSON.stringify(t)))' +``` + +To read one back: + +```sh +bun -e 'const c=require("./apps/econ/static/weekly-challenge.json"); +for (const x of c.Challenges) console.log(x.ChallengeId, x.Description, "\n ", JSON.parse(x.Config))' +``` + +## Node types + +Each node carries a numeric type in `ct`. Two composite kinds appear: + +- **Match** (`ct: 0`) — `wc` is a list of predicates that must _all_ hold for one game + result (AND). +- **Counter** (`ct: 1`) — `ctc` holds the child node to count, `t` is the target count. + +Which slot a node uses (`wc` vs `ctc`) tells you what its children are; a node never has +both. `ipc` is `false` on every composite node in the reference data — purpose unknown, but +the client echoes it back, so keep emitting it. + +## Predicate leaves + +Leaves carry `vs`, a list of accepted values matched as OR. + +| `ct` | Shape | Meaning | +| ---- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `6` | `{"ct":6,"vs":[2]}` | _(inferred)_ The kind of event being matched — a finished game/session. Present in **every** leaf group and always `[2]`; nothing observed varying it, so treat it as required boilerplate. | +| `7` | `{"ct":7,"vs":[{"l":""}]}` | Scene allow-list: each `l` is a subroom's `UnitySceneId` (see `apps/rooms`). Matches if the game happened in any of them. | +| `9` | `{"ct":9,"vs":[true],"v":"won"}` | A named session variable (`v`) equals one of `vs` — here, the player won. | + +## The two idioms + +Every challenge in the captured rotation is one of these. + +```jsonc +// "Complete ^TheRiseOfJumbotron quest" — one winning session in one scene +{ "ct": 0, "ipc": false, "wc": [ + { "ct": 6, "vs": [2] }, + { "ct": 9, "vs": [true], "v": "won" }, + { "ct": 7, "vs": [{ "l": "acc06e66-…" }] } // TheRiseofJumbotron / Home +]} + +// "Complete 5 Charades games" — count matching sessions to a target +{ "ct": 1, "ipc": false, "t": 5, "ctc": [ + { "ct": 0, "ipc": false, "wc": [ + { "ct": 6, "vs": [2] }, + { "ct": 7, "vs": [{ "l": "a673712c-…" }, { "l": "4078dfed-…" }] } // 3DCharades + Legacy3DCharades + ]} +]} +``` + +The quest challenges have **no `t`** (one qualifying session is the whole goal) and the +counted ones have **no `won` predicate** (finishing counts, winning is irrelevant). A "one +map only" challenge is the counted shape with a single-entry scene list. + +## Scene ids, not room ids + +`ct: 7` matches `UnitySceneId`, so one guid can name several rooms — a screens room, its VR +twin, and the standalone base room all share a scene. The captured "Complete 10 games in +^Paintball" lists six guids, which are the subrooms of _both_ `Paintball` and `PaintballVR`, +each of which is also a standalone base room (`River`, `Clearcut`, …). One list covers every +way in. + +Resolve a guid against `SubRooms[].UnitySceneId` in `apps/rooms/static/ImportRooms.json` +(same data as `apps/rooms/migrations/0002_import_rooms.sql`). Run from the repo root: + +```sh +cat > /tmp/scene.ts <<'EOF' +// path is resolved against the cwd, so run this from the repo root +const rooms = await Bun.file('apps/rooms/static/ImportRooms.json').json() +const want = new Set(process.argv.slice(2)) +const byScene = new Map() +for (const r of rooms as any[]) + for (const s of r.SubRooms ?? []) + byScene.set(s.UnitySceneId, [...(byScene.get(s.UnitySceneId) ?? []), `${r.Name}/${s.Name}`]) +for (const [id, names] of byScene) if (!want.size || want.has(id)) console.log(id, names.join(', ')) +EOF +bun run /tmp/scene.ts 380d18b5-de9c-49f3-80f7-f4a95c1de161 +# → 380d18b5-… Paintball/Clearcut, PaintballVR/Clearcut, Clearcut/Home +``` + +With no arguments it dumps every scene, which is how you go the other way — from a room name +to the guid to put in `vs`. + +### Shared scenes to watch for + +These guids resolve to more than one room, so a challenge naming one also completes in the +others. Most are a deliberate screens/VR/base-room trio, but two are genuine surprises: +**`Soccer/Home` and `Stadium/Home` are the same scene**, so a soccer challenge also completes +in the Stadium, and `Dodgeball` shares its scene with the plain `Gym`. + +| Scene id | Rooms | +| ----------- | ------------------------------------------------------------------ | +| `6d5eea4b…` | Soccer/Home, **Stadium/Home** | +| `3d474b26…` | Dodgeball/Home, **Gym/Home**, DodgeballVR/Home | +| `ae929543…` | Bowling/Home, BowlingAlley/Home | +| `f6f7256c…` | DiscGolfLake/Home, Lake/Home | +| `d9378c9f…` | DiscGolfPropulsion/Home, PropulsionTestRange/Home | +| `239e676c…` | LaserTag/Hangar, Hangar/Home | +| `9d6456ce…` | LaserTag/CyberJunkCity, LaserTagCyberJunk/Home, CyberJunkCity/Home | +| `e122fe98…` | Paintball/River, PaintballVR/River, River/Home | +| `a785267d…` | Paintball/Homestead, PaintballVR/Homestead, Homestead/Home | +| `ff4c6427…` | Paintball/Quarry, PaintballVR/Quarry, Quarry/Home | +| `380d18b5…` | Paintball/Clearcut, PaintballVR/Clearcut, Clearcut/Home | +| `58763055…` | Paintball/Spillway, PaintballVR/Spillway, Spillway/Home | +| `65ddbb48…` | Paintball/Drive-in, PaintballVR/Drive-in, DriveIn/Home | + +Regenerate this list with the script above and no arguments. + +## Progress fields (`cc`, `c`) — client-side only + +On `updateProgress` the client posts the same tree back with its own progress written into +it: **`cc`** on the counter node is the current count (`…,"t":5,"cc":1`), and **`c`** +(`"c":true`) marks a node it now considers satisfied. + +Neither belongs in `weekly-challenge.json` — they are progress, not definition. The server +echoes the posted `Config` back untouched and never persists it (`challenge_status` stores +only the completion flag; see `apps/econ/src/challenge-db.ts`), so the running count lives +only in the client. Don't add `cc`/`c` to an authored tree, and don't try to read progress +out of one. + +## Authoring a new challenge + +1. Pick the idiom: one-shot (`ct: 0` root, add the `won` predicate if winning is required) + or counted (`ct: 1` root with `t`). +2. Resolve the scenes with the script above, and check the shared-scene table — decide + whether the extra rooms it lets in are acceptable. +3. Build the tree as an object, stringify it twice into `Config`. +4. Give the entry a `ChallengeId` unique **within the rotation** (they aren't sequential), + and write the real goal in `Description` — `Name` is an internal slug that is not + authoritative (captured id `63` is named `Complete3SpillwayGames` but its `Config` and + description are Clearcut). +5. Leave `Complete: false`; `getCurrent` stamps it per caller. +6. Bump `ChallengeMapId` if this is a new rotation — ids only need to be unique within one, + and a new map id is what resets stored completions. +7. Keep `ServerTime` inside `StartAt`…`EndAt`, or the client renders the rotation as expired. + +Sanity check the file parses and every tree parses: + +```sh +bun -e 'const c=require("./apps/econ/static/weekly-challenge.json"); +c.Challenges.forEach(x => JSON.parse(x.Config)); console.log("ok", c.Challenges.length)' +``` + +Then `bun turbo -F econ test` — `src/test/integration/api.test.ts` imports the file and +asserts `getCurrent` against it. + +## Related + +- `apps/econ/README.md` — the rest of the weekly-challenge file (top level, `Gift`, progress) +- `.agents/daily-objectives/SKILL.md` — the other objective system, on `GET api/config/v2` diff --git a/apps/econ/README.md b/apps/econ/README.md index fa5cf0f..f2ee6e8 100644 --- a/apps/econ/README.md +++ b/apps/econ/README.md @@ -192,60 +192,17 @@ same rotation writes both `"Complete 10 games in ^Paintball"` and, plainly, ### The `Config` rule tree -A tree of nodes, each with a numeric type in `ct`. Two node kinds appear: +An escaped JSON string holding a tree of nodes, each with a numeric type in `ct`: a +**Match** (`ct: 0`, `wc` is a list of predicates that must all hold for one game result) or +a **Counter** (`ct: 1`, `ctc` is the child node to count and `t` the target). Leaves match a +scene allow-list (`ct: 7`, subroom `UnitySceneId`s) or a session variable (`ct: 9`, e.g. +`won`). The server never evaluates any of it — the client does, and posts the tree back with +its own count written in. -- **Match** (`ct: 0`) — `wc` is a list of predicates that must _all_ hold for one game - result (AND). -- **Counter** (`ct: 1`) — `ctc` holds the child node to count and `t` is the target count. - -Which slot a node uses (`wc` vs `ctc`) tells you what its children are; a node never has -both. `ipc` is `false` on every composite node in the reference data — purpose unknown, but -the client echoes it back, so keep emitting it. - -Predicate leaves carry `vs`, a list of accepted values matched as OR: - -| `ct` | Shape | Meaning | -| ---- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `6` | `{"ct":6,"vs":[2]}` | _(inferred)_ The kind of event being matched — a finished game/session. Present in **every** leaf group and always `[2]`; nothing observed varying it, so treat it as required boilerplate. | -| `7` | `{"ct":7,"vs":[{"l":""}]}` | Scene allow-list: each `l` is a subroom's `UnitySceneId` (see `apps/rooms`). Matches if the game happened in any of them. | -| `9` | `{"ct":9,"vs":[true],"v":"won"}` | A named session variable (`v`) equals one of `vs` — here, the player won. | - -The two idioms in the file, unescaped: - -```jsonc -// "Complete ^TheRiseOfJumbotron quest" — one winning session in one scene -{ "ct": 0, "ipc": false, "wc": [ - { "ct": 6, "vs": [2] }, - { "ct": 9, "vs": [true], "v": "won" }, - { "ct": 7, "vs": [{ "l": "acc06e66-…" }] } // TheRiseofJumbotron / Home -]} - -// "Complete 5 Charades games" — count matching sessions to a target -{ "ct": 1, "ipc": false, "t": 5, "ctc": [ - { "ct": 0, "ipc": false, "wc": [ - { "ct": 6, "vs": [2] }, - { "ct": 7, "vs": [{ "l": "a673712c-…" }, { "l": "4078dfed-…" }] } // 3DCharades + Legacy3DCharades - ]} -]} -``` - -Note the quest challenges have **no `t`** (one qualifying session is the whole goal) and -the counted ones have **no `won` predicate** (finishing counts, winning is irrelevant). - -On `updateProgress` the client posts the same tree back with its own progress written into -it: **`cc`** on the counter node is the current count (`…,"t":5,"cc":1`), and **`c`** (`"c":true`) -marks a node it now considers satisfied. Neither appears in this file — they are progress, -not definition, which is why the tree isn't stored (only the top-level `Complete` is). The -count itself lives only in the client. - -**Scene ids, not room ids.** Because `ct: 7` matches `UnitySceneId`, a screens room and its -VR twin share ids and both count: the captured "Complete 10 games in Paintball" listed six -scenes, which are the subrooms of _both_ `Paintball` and `PaintballVR` — and each is also a -standalone base room (`River`, `Clearcut`, …). One list covers every way in. Resolve a guid -against the `SubRooms[].UnitySceneId` values in `apps/rooms/migrations/0002_import_rooms.sql`; -a "one map only" challenge is the same shape with a single-entry list. Watch for one trap -this creates: `Soccer / Home` and `Stadium / Home` are the same scene, so a soccer challenge -also completes in the Stadium. +**Reading or writing one? See `.agents/weekly-challenge-config/SKILL.md`** — the full +grammar, the two idioms the file uses, how to resolve a scene guid to a room, the shared +scenes that make a challenge complete in more rooms than you meant (`Soccer` and `Stadium` +are one scene), and an authoring checklist. ### The `Gift` block diff --git a/apps/econ/src/challenge-db.ts b/apps/econ/src/challenge-db.ts index efa7f9b..f9626ce 100644 --- a/apps/econ/src/challenge-db.ts +++ b/apps/econ/src/challenge-db.ts @@ -7,7 +7,7 @@ * That tree is the challenge's DEFINITION (it comes from static/weekly-challenge.json and * is identical for everyone), decorated with the client's running count in `cc`; the * server evaluates none of it, so persisting a per-player copy would only be a second, - * staler copy of the catalog. See the README's weekly-challenge section for the grammar. + * staler copy of the catalog. See .agents/weekly-challenge-config/SKILL.md for the grammar. * * Completion LATCHES within a rotation: the client reports progress repeatedly, and a * report that arrives with the challenge no longer complete (a fresh session, a reordered