ravensight-playtest 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,11 @@ AI personas play your game, then file a report you can read, mark up and argue
4
4
  with. The personas run on your machine, against your build; Ravensight stores
5
5
  the reports and renders them.
6
6
 
7
+ Simulation Runner adds seeded headless batches for mechanics, economy and
8
+ progression checks through your own game adapter. It runs locally without
9
+ model calls and saves bounded results to the same Playtest workspace. See
10
+ [Simulation Runner](docs/SIMULATION.md) for the held candidate and setup.
11
+
7
12
  You need a Ravensight account and Node 22 or newer. You do not need an
8
13
  Anthropic API key: model calls go through Ravensight under the same token you
9
14
  log in with, so there is one credential on your machine and you can revoke it
@@ -45,9 +50,32 @@ Credential Manager, libsecret on Linux). With no keychain available it falls
45
50
  back to a file with 0600 permissions under your home directory, and says so.
46
51
  In CI, set `RAVENSIGHT_TOKEN` instead and skip `login` entirely.
47
52
 
53
+ ## Upgrading to 0.3.2
54
+
55
+ - **A plain `run` plays one persona, not two.** The default persona set is now
56
+ `curious-kid` alone, so a first run costs one persona run. Name more with
57
+ `--personas curious-kid,grumpy-veteran` (or `personas` in
58
+ `.ravensight/config.json`); `ravensight-playtest personas` lists the slugs.
59
+ A script that relied on two personas by default has to name them.
60
+ - **`run` checks the build before it prices anything.** A web build's URL has
61
+ to answer 2xx or 3xx, or the `preflight` URL in `.ravensight/config.json` has
62
+ to answer what you set (see `run`). A failure exits 1 with no job registered.
63
+ `--skip-preflight` skips it, as it already skipped the native probe.
64
+ - **`brief` and `check` quote the run that `run` would start**: its modules and
65
+ its personas, the default one when you name none, each unit with its own
66
+ price. They used to quote a game profile with no personas.
67
+ - **Two new brief fields stay on this machine**: `expected_noise` and
68
+ `test_account` (see the `brief --from` file). `init` adds
69
+ `.ravensight/brief.local.json` to `.gitignore`.
70
+ - **Out of range report values are normalised before upload** instead of
71
+ failing the run (see how the report step reads what happened, under `run`).
72
+ - **A CSS selector is no longer a target.** A target is a ref, coordinates, a
73
+ line copied from the screen, or text on the screen; anything else is refused
74
+ at once instead of searched for.
75
+
48
76
  ## Commands
49
77
 
50
- Twelve commands. Flags below are the ones the code accepts, not the ones the
78
+ Thirteen commands. Flags below are the ones the code accepts, not the ones the
51
79
  prose elsewhere describes; `--help` on any command prints the same list.
52
80
 
53
81
  Every command takes these four:
@@ -89,9 +117,26 @@ to), `project binding` (the game and environment this directory is linked to,
89
117
  and whether that game is in the token's scope), `brief` (present, complete, and
90
118
  which of goals, success criteria, controls and how to start are empty), `build`
91
119
  (the URL, Godot project or native build a run would use), `automation server`
92
- (native drivers only, see below), then the balance against the next job's price,
93
- the content pack verified by sha256, disk, recommended concurrency, and the two
94
- optional tools.
120
+ (native drivers only, see below), then the content pack verified by sha256, the
121
+ balance against the price of the run `run` would start (its modules and
122
+ personas, the default persona when none is named, each unit with its price),
123
+ disk, recommended concurrency, and the two optional tools. A `--personas` slug
124
+ the pack does not have fails a `personas` row that lists the valid slugs, and
125
+ nothing is priced.
126
+
127
+ For a web build linked by URL (`--build-url` is a `run` flag; `check` reads the
128
+ URL from `.ravensight/config.json`), the `browser` row also loads the first
129
+ screen headless and a `first screen` row measures it the way a persona would
130
+ read it. When the whole text is over the 4,000 characters a persona reads per
131
+ step, or the screen has a list of 50 or more look-alike items, the row is a
132
+ warning that suggests `run --long` with the reason ("first screen is 18,400
133
+ characters, about 4.6x the 4,000 a persona reads per step") and both prices,
134
+ read from the server's free estimate. A light screen passes with its size. A
135
+ first screen that does not load within 20 seconds is a warning, never a
136
+ failure. The page is marked as a playtest before the game's scripts run (the
137
+ same `window.__RAVENSIGHT_PLAYTEST__` a run sets, with the token `check`), so a
138
+ build shipping the Ravensight web SDK has its session refused rather than
139
+ recorded as a real player.
95
140
 
96
141
  Every row is `pass`, `warn` or `fail`, and every row that is not `pass` prints
97
142
  its fix. Exit code 3 when any row fails; a warning never fails, so CI does not
@@ -118,7 +163,7 @@ setup with the expected rows at each step.
118
163
  "ok": true,
119
164
  "reserves_units": false,
120
165
  "note": "check reserves no units and starts no job: ...",
121
- "cli": { "installed": "0.3.1", "latest": "0.3.1", "min_supported": "0.1.0" },
166
+ "cli": { "installed": "0.3.2", "latest": "0.3.2", "min_supported": "0.1.0" },
122
167
  "login": { "present": true, "valid": true, "games": [{ "gameId": "g1", "name": "Pocket Knight" }] },
123
168
  "project": { "game": "g1", "environment": null, "in_scope": true },
124
169
  "brief": { "present": true, "complete": true, "missing": [] },
@@ -156,8 +201,8 @@ automatically. `check` shows that same warning as a row when it applies.
156
201
  | Flag | Effect |
157
202
  |---|---|
158
203
  | `--game <gameId>` | Check against this game instead of the linked one. |
159
- | `--modules <list>` | Price this module list, comma separated. |
160
- | `--personas <list>` | Price this persona list. Without it the balance row prices no personas, so the quote is lower than a real job's. |
204
+ | `--modules <list>` | Price this module list, comma separated. Default the modules `run` registers, `persona_playtest,aggregate_report`. |
205
+ | `--personas <list>` | Price this persona list. Without it, a module list with `persona_playtest` prices the repo config's personas or the default one, as `run` would. |
161
206
  | `--godot-path <path>` | Where Godot is, if it is not on the PATH. |
162
207
  | `--web` | Check the browser even when this repo is not linked to a web build. |
163
208
  | `--driver <name>` | Check the setup for this driver instead of the configured one. |
@@ -167,7 +212,8 @@ automatically. `check` shows that same warning as a row when it applies.
167
212
 
168
213
  Links this repo to a game and writes `.ravensight/config.json`. Refuses a game
169
214
  your token cannot see, so the first job does not fail with a 404. Adds
170
- `.ravensight/jobs/` to an existing `.gitignore`.
215
+ `.ravensight/jobs/` and `.ravensight/brief.local.json` to an existing
216
+ `.gitignore`.
171
217
 
172
218
  Without `--game`, one visible game is linked as is. With several, `init` lists
173
219
  them numbered and asks `Which game? [1-N]` when it is running in a terminal; in
@@ -180,11 +226,18 @@ login).
180
226
  | `--game <gameId>` | The game to link. Optional with one visible game, or in a terminal. |
181
227
  | `--force` | Relink a repo that is already linked. |
182
228
 
229
+ ### personas
230
+
231
+ Lists the persona slugs `--personas` accepts, from the content pack this game
232
+ gets: each one's name, action budget (its patience) and play style, with the
233
+ default marked. A free read. Takes `--game <gameId>` and `--json`.
234
+
183
235
  ### brief
184
236
 
185
237
  Writes the expectations brief: your goals, what success looks like, how the game
186
- is controlled, where a persona starts. Then prints what a job would cost,
187
- whether your balance covers it, and, past 8 personas, the same wall-clock
238
+ is controlled, where a persona starts. Then prints what the run `run` would
239
+ start costs ("A run of 1 persona (curious-kid) would cost $1.00: a persona run
240
+ $1.00 each"; the game profile is a separate job), whether your balance covers it, and, past 8 personas, the same wall-clock
188
241
  warning `run` shows. A brief is required before `run`; the server answers 409
189
242
  `brief_required` without one.
190
243
 
@@ -195,7 +248,7 @@ warning `run` shows. A brief is required before `run`; the server answers 409
195
248
  | `--draft` | Save as a draft even when it looks complete. |
196
249
  | `--from <file>` | Read the fields from a JSON file instead of asking. |
197
250
  | `--game <gameId>` | The game, when this repo is not linked. |
198
- | `--modules <list>`, `--personas <list>` | Price this module and persona list in the estimate it prints. |
251
+ | `--modules <list>`, `--personas <list>` | Price this module and persona list in the estimate it prints. Every unit is named with its own price, such as "profile job $4.00, a persona run $1.00 each". |
199
252
 
200
253
  #### The `brief --from` file
201
254
 
@@ -211,13 +264,43 @@ are what makes a brief complete; the rest are optional.
211
264
  | `build_notes.how_to_start` | string | Where a persona begins: the URL, the menu path, the first thing to press. |
212
265
  | `design_intent_notes` | array of strings | Deliberate choices a persona must not report as bugs. |
213
266
  | `known_issues` | array of strings | Issues you already know about, so they are not reported again. |
267
+ | `expected_noise` | array of strings | Console errors and failed requests that are expected, such as `"401 /auth/me before login"`. Never a finding. Kept on this machine. |
268
+ | `test_account` | `{ "email", "password", "how_to_sign_in" }` | A throwaway account the persona signs in with, so a run starts past sign-up. The password must be at least 8 characters. Kept on this machine. |
269
+
270
+ `expected_noise` and `test_account` never go to the server: its brief schema
271
+ refuses keys it does not list, and a password does not belong in a record the
272
+ studio shares. `brief --from` splits them into `.ravensight/brief.local.json`,
273
+ readable by you only, which `init` adds to `.gitignore`; `brief --show` prints
274
+ them with the password masked, and `run` and `resume` read them from there.
275
+
276
+ An `expected_noise` pattern matches an automatic console note when every status
277
+ code (`401`) and every path (`/auth/me`) in it appears in the note; the other
278
+ words describe it for the reader. A pattern with neither is matched as text,
279
+ and `/.../` is a regular expression. One rule is built in: a 401 on a who am I
280
+ check, a request whose last path segment is `/me`, `/whoami`, `/session` or
281
+ `/current_user` (such as `/auth/me`), is what a signed out page does and is not
282
+ a finding. A 401 anywhere else (`/me/inventory`, `/user/42/save`, `/token`,
283
+ `/login`) is left to the report step. A matching note is marked "expected
284
+ noise" in the report prompt, and the runner drops a finding whose evidence is
285
+ only expected noise, listing it in `report.md`. A note the persona wrote itself
286
+ is never noise.
287
+
288
+ **The test account password crosses the model proxy during play only**: it is
289
+ in the persona's prompt on every play turn, because that is how a persona can
290
+ type it. Use an account made for testing, with nothing behind it you would mind
291
+ losing. The report step never sees it: its prompt has no test account, and the
292
+ password is replaced by `[test password]`, wherever it stands as a whole token, in the notes, the action log, the
293
+ transcript, and in `report.md` and `report.json` before anything is uploaded. A
294
+ password field's value is never shown in an observation.
214
295
 
215
296
  ```json
216
297
  {
217
298
  "goals": [{ "goal": "onboarding", "priority": 1 }, { "goal": "find_bugs", "priority": 2 }],
218
299
  "success_criteria": "A new player reaches level 2\nNobody gets stuck in the menu",
219
300
  "build_notes": { "controls": "Arrow keys to move, space to jump", "how_to_start": "Press Start on the title screen" },
220
- "design_intent_notes": ["The fog on level 1 is intentional"]
301
+ "design_intent_notes": ["The fog on level 1 is intentional"],
302
+ "expected_noise": ["401 /auth/me before login"],
303
+ "test_account": { "email": "qa@example.test", "password": "a-throwaway-password", "how_to_sign_in": "Log in from the top right" }
221
304
  }
222
305
  ```
223
306
 
@@ -230,7 +313,36 @@ run options below.
230
313
  ### run
231
314
 
232
315
  Registers a job, plays it, writes the reports, uploads them, finishes the job.
233
- Prints the price and asks before spending.
316
+ Prints the price and asks before spending. Before any charge, even with
317
+ `--yes`, it prints one bold line with the persona count and the total ("This
318
+ run: 1 persona (curious-kid) on playwright_web, $1.00 in total: a persona run
319
+ $1.00 each"), the balance, and what a persona run buys: the action budget (the
320
+ persona's patience in the pack, lowered by `--max-actions` or the server's
321
+ ceiling) and that sign-up alone can take 8 of those actions. A `test_account`
322
+ in the brief lets the persona sign in instead. An unknown `--personas` slug is
323
+ refused with the valid slugs before anything is priced.
324
+
325
+ `--long` makes every persona run a long run: twice the actions, model budget
326
+ and wall time of a standard run, for a game whose screens are heavy or whose
327
+ goal is far in. It costs twice a standard run. The price is the server's
328
+ estimate for the long run, printed and confirmed like any other, never a figure
329
+ built into the CLI. Locally a long run uses the run's own action limit when the
330
+ server's register answer carries one, otherwise twice the standard ceilings
331
+ (the persona's patience and the per run ceiling); `--max-actions` still only
332
+ lowers it. `check` suggests `--long` when the first screen is heavy.
333
+
334
+ Before the estimate, `run` checks the build is up, so a down server is never
335
+ paid for. Set `preflight` in `.ravensight/config.json` to name what to check:
336
+
337
+ ```json
338
+ { "preflight": { "url": "/api/health", "expect_status": 200 } }
339
+ ```
340
+
341
+ `url` may be relative to the build URL; `expect_status` is a number or a list
342
+ of numbers, and without it any 2xx or 3xx passes (redirects are not followed).
343
+ With no `preflight`, a web build's own URL has to answer 2xx or 3xx; other
344
+ drivers are not checked unless a `preflight.url` is set. A failure prints the
345
+ status it saw (or the network error), exits 1, and registers nothing.
234
346
 
235
347
  With a native driver, `run` first runs the `build` and `automation server` rows
236
348
  of `check`. When either fails it exits 1 before the estimate, the confirmation
@@ -252,14 +364,15 @@ splitting the job in two is what gets everything played.
252
364
  | Flag | Effect |
253
365
  |---|---|
254
366
  | `--game <gameId>` | The game, when this repo is not linked. |
255
- | `--personas <list>` | Comma separated persona slugs. Defaults to the pack's set. |
367
+ | `--personas <list>` | Comma separated persona slugs. Defaults to one, `curious-kid`; `ravensight-playtest personas` lists them. |
256
368
  | `--driver <name>` | `playwright_web`, `godot_driver`, `cli_stdio`, or a native driver: `desktop_driver`, `unity_driver`, `unreal_driver`, `android_driver`, `ios_driver`. |
257
369
  | `--build-url <url>` | The web build to play. |
258
370
  | `--godot-project <dir>` | The Godot project directory. |
259
371
  | `--driver-config <file>` | The native driver config, or the JSON-RPC bridge config for `cli_stdio`. |
260
- | `--skip-preflight` | Native drivers only: register without running the free automation probe first. The job is registered and charged even if the automation server turns out to be down. |
372
+ | `--skip-preflight` | Register without the free checks first: the build health check (any driver) and the automation probe (native drivers). The job is registered and charged even if the build or the automation server turns out to be down. |
261
373
  | `--cli <command>` | The command that starts a text build. |
262
374
  | `--max-actions <n>` | Lower the actions one persona may take. It can only lower the persona's own patience (and the server's per run ceiling), never raise it. |
375
+ | `--long` | A long persona run: twice the actions, model budget and wall time of a standard run, at twice the price. The price is read from the estimate and confirmed before any charge. |
263
376
  | `--concurrency <n>` | Personas at once. Default 2; `check` recommends a number for this machine. |
264
377
  | `--upload-video` | Upload `session.webm` as well as the reports. |
265
378
  | `--upload-transcript` | Upload `transcript.jsonl` as well as the reports. |
@@ -304,6 +417,30 @@ message, the run is completed as `failed`, and the job exits 1.
304
417
 
305
418
  How the report step reads what happened:
306
419
 
420
+ - Values the report schema does not allow are normalised before upload, so a
421
+ run is not lost to them when the model has no budget left for a repair: a
422
+ severity that is a category word (`"severity": "ux"`) moves to `category`
423
+ when the category is missing or not allowed and the severity becomes
424
+ `minor`; any other unknown severity becomes `minor`; an unknown category
425
+ takes a close match the schema has (`ui` is `ux`, `crash` is `bug`), else
426
+ `other` when the schema has it, else `ux` (the server's schema has no
427
+ catch-all today); a value that differs only in case is respelled; any other
428
+ enum field takes the schema's default. Each change is a runner note at the
429
+ end of the finding's description and is listed in `report.md` under
430
+ "Normalised by the runner". The server's schema stays strict.
431
+ - After the report is written, each blocker or major (at most 3) is checked
432
+ against its own evidence: the screenshot it cites taken nearest after the
433
+ observation it cites is sent with its claim, and the question "does this
434
+ image contradict the claim: yes, no, unclear". A yes downgrades it one step,
435
+ with a note naming the screenshot, in both report files. The cost is 3
436
+ calls at most, each one screenshot in and 32 tokens out, drawn from the
437
+ run's own model budget (the one the 80 percent warning keeps room in for
438
+ the report), never charged separately; a refusal ends the checks and keeps
439
+ the report as written.
440
+ - Console notes that match the brief's `expected_noise`, or the built in rule
441
+ (a 401 on a who am I check while signed out), are expected noise
442
+ and never a finding; see the `brief --from` file.
443
+
307
444
  - Console errors are recorded as automatic notes. A 4xx on a request (`Failed
308
445
  to load resource`, `HTTP 404`, `status: 401`, `401 Unauthorized` and similar)
309
446
  is a network note: context, not a bug. On the web a `Failed to load
@@ -369,11 +506,26 @@ clicks and types by ref, which names exactly one element even when two share
369
506
  a name; two such elements are both shown, each with where it is ("in header
370
507
  strip", "in main"). A name that matches twice is answered with the refs to
371
508
  choose from, and a target written as locator code (`getByRole(...)`,
372
- `page.locator(...)`, `css=`, `xpath=`) is refused at once. Failures like these
509
+ `page.locator(...)`, `css=`, `xpath=`) is refused at once. A whole line copied
510
+ from the screen (`button "Run the week" [e12] @(1000,303)`, or any part of one)
511
+ is clicked by its ref, else its `@(x,y)` (refused like a stale ref when the page
512
+ has loaded a new document since); a ref or point inside a quoted name is part
513
+ of the name, and a bare `e4` is text when something on screen is named exactly
514
+ that. A target that is
515
+ none of a ref, coordinates or text on the screen is refused at once rather than
516
+ searched for. A name that matches one element on screen is clicked by that
517
+ element's ref, and a text search skips hidden elements, so a mobile only copy
518
+ hidden by CSS is never the one clicked. Failures like these
373
519
  are the driver's, not the game's: they are recorded as `driver_error`, and a
374
520
  blocker or major resting only on them is dropped before upload, as is any
375
521
  finding resting only on the persona's "waiting for X to finish" notes.
376
522
 
523
+ An action counts as changing the screen when the URL changed, the page loaded
524
+ a new document, the visible tree (field values included) changed, or for typing,
525
+ the field's own value changed, with one short second look for a navigation
526
+ still starting. That is the "screen changed" the report step's action log and
527
+ the downgrade rule read.
528
+
377
529
  Every field, checkbox, radio, slider and select the persona sees carries what
378
530
  it holds now, read from the page rather than from the accessibility text:
379
531
  `value="..."` (cut at 80 characters), `checked` or `not checked`, or
@@ -382,7 +534,9 @@ the page could not answer for shows no value at all, so a typed password never
382
534
  reaches the model.
383
535
 
384
536
  On a long web page the persona's text view of the screen is capped at 4,000
385
- characters. Over the cap, buttons, links and fields are kept first, a long run
537
+ characters. A run of 20 or more look-alike items is always shown as its first
538
+ two and "and N more like it", under the cap too; a shorter run under the cap is
539
+ shown as it is. Over the cap, buttons, links and fields are kept first, a long run
386
540
  of look-alike items (a list of 249 cards) is shown as its first two and "and
387
541
  247 more like it", and what changed since the last look comes next, so a form
388
542
  after a long list still reaches the persona.
@@ -394,7 +548,8 @@ reached. Every run gets its own identity to sign up with, an email
394
548
  short run id, so a second run never hits "that email is taken" from the first.
395
549
  `example.test` is a reserved name and never delivers mail. Point `--build-url` at a guest or demo path, or say where to start in
396
550
  the brief's `how_to_start` note (for example "choose Play as guest"), when the
397
- sign-up is not what you want tested.
551
+ sign-up is not what you want tested, or give the brief a `test_account` the
552
+ persona signs in with instead.
398
553
 
399
554
  ### fix
400
555
 
@@ -586,6 +741,7 @@ In your repo:
586
741
  ```
587
742
  .ravensight/
588
743
  config.json the game link and your defaults
744
+ brief.local.json expected_noise and test_account, this machine only (0600)
589
745
  jobs/<jobId>/
590
746
  state.json the resumable journal
591
747
  upload-queue.jsonl what was uploaded, and what is still owed
@@ -602,8 +758,8 @@ and a local `transcript.jsonl` of the apply loop (which tool, on what, how
602
758
  much came back) that is never uploaded and is how an attempt that wrote
603
759
  nothing gets explained afterwards.
604
760
 
605
- Add `.ravensight/jobs/` to your `.gitignore`; `init` does it for you when you
606
- already have one.
761
+ Add `.ravensight/jobs/` and `.ravensight/brief.local.json` to your
762
+ `.gitignore`; `init` does it for you when you already have one.
607
763
 
608
764
  `fix` also leaves a branch on your working copy, checked out, with the change in
609
765
  your working tree and nothing committed. It is the only thing in this CLI that
@@ -633,6 +789,16 @@ pins it equal to the server's file when a checkout is on the same machine.
633
789
  Findings cite a file and a line range. The source text behind that line is not
634
790
  uploaded.
635
791
 
792
+ `.ravensight/brief.local.json` is never uploaded. Its `expected_noise` goes into
793
+ the persona and report prompts. Its `test_account`, password included, goes into
794
+ the persona's prompt during play only, through the model proxy; the report step
795
+ never gets it, and the password is scrubbed to `[test password]` from the
796
+ transcript, the notes and both report files before upload.
797
+
798
+ `run`'s build health check is one GET to your own build (or the `preflight`
799
+ URL you set), made from your machine.
800
+
801
+
636
802
  A `fix` uploads `changeset.json` and `report.md`. The changeset carries the
637
803
  finding id, the branch name, the commit it branched from, the file paths it
638
804
  changed with their added and removed line counts, the verify verdict, and the
@@ -723,3 +889,17 @@ feedback and timing-sensitive gameplay.
723
889
  ## Build adapters
724
890
 
725
891
  Browser and Godot runners share the paid playtest lifecycle with desktop, Unity/Unreal export, Android/iOS and custom JSON-RPC adapters. Native adapters require a locally configured automation server and remain experimental until validated against your build. See [adapter setup and validation boundaries](docs/ADAPTERS.md) and the [native runner starter](docs/NATIVE_RUNNER.md); `check` probes the automation server for free before a paid native run.
892
+
893
+ ## Simulation Runner
894
+
895
+ Available in held local candidate `0.4.0`; not yet published. Simulation Runner requires macOS or Linux (including WSL); native Windows is refused before purchase.
896
+
897
+ Run seeded mechanics tests through a local game adapter, compare explicit strategies and replay selected traces. A batch uses the existing standard Playtest purchase of 5 AI units from prepaid balance, confirmed against the server quote. Only the summary is uploaded; game execution and replay traces stay local. It does not automatically call a model or replace testing the player interface.
898
+
899
+ ```sh
900
+ ravensight-playtest simulate --config simulation.json --driver-config bridge.json
901
+ ravensight-playtest simulate --resume .ravensight/simulations/BATCH_ID/state.json
902
+ ravensight-playtest simulation-replay trace-00000.json --driver-config bridge.json
903
+ ```
904
+
905
+ See [Simulation Runner setup, adapter protocol and limits](docs/SIMULATION.md). The package includes `examples/simulation/economy.mjs` as a deterministic starting adapter. Local replay creates no new paid job.
@@ -60,7 +60,7 @@ Every device command times out after 30 seconds and responses are capped at
60
60
  ## Step 1: the account side
61
61
 
62
62
  ```bash
63
- npm install -g ravensight-playtest@0.3.1
63
+ npm install -g ravensight-playtest@0.3.2
64
64
  ravensight-playtest login
65
65
  cd path/to/your/game
66
66
  ravensight-playtest init --game <gameId>
@@ -70,7 +70,7 @@ ravensight-playtest check --driver ios_driver
70
70
  Expected rows (your email, game and version differ):
71
71
 
72
72
  ```
73
- pass cli version installed 0.3.1; server latest 0.3.1, min_supported 0.1.0
73
+ pass cli version installed 0.3.2; server latest 0.3.2, min_supported 0.1.0
74
74
  pass login you@example.com, valid, scoped to 1 game: Pocket Knight (g1)
75
75
  pass project binding linked to Pocket Knight (g1), in the token's scope
76
76
  fail brief this game has no expectations brief, and a persona run needs one
@@ -0,0 +1,91 @@
1
+ # Simulation Runner
2
+
3
+ Held local candidate: `0.4.0`. This package has not been published; use a reviewed local tarball with the matching held server implementation.
4
+
5
+ Simulation Runner exercises game mechanics through a developer-owned local adapter. It can run seeded games across explicit strategies, summarize outcomes and metrics, and save selected replay traces. Use it for questions such as whether an economy supports several viable strategies or whether a progression rule traps a simulated player. It does not judge animation, interface usability, feel or subjective fun.
6
+
7
+ A batch uses the existing standard Playtest purchase: **5 AI units from prepaid balance**. The CLI obtains the current server quote and asks for confirmation before registration. One batch is one purchase, not one purchase per simulated game. It does not consume the monthly shared AI allowance. Local replay is a debugging operation with no new job or purchase. Automatic AI interpretation is not included; an agent can use the existing report tools when explicitly asked to interpret the summary.
8
+
9
+ ## Setup and run
10
+
11
+ Run Simulation Runner on macOS or Linux, including WSL. Native Windows is rejected during preflight before quoting or registering a purchase because this feature requires POSIX process-group cleanup. This restriction applies to Simulation Runner, not every Playtest adapter. The recorded real-process and 10,000-game validation was run on macOS; native Windows and WSL execution were not validated in this review.
12
+
13
+ Sign in and initialize your Ravensight project as usual. Implement the protocol below, or start with the packaged `examples/simulation/economy.mjs`. The example is deterministic and contains no networking or telemetry.
14
+
15
+ Create `bridge.json` with absolute paths for the Node executable and adapter:
16
+
17
+ ```json
18
+ {
19
+ "command": "/absolute/path/to/node",
20
+ "args": ["/absolute/path/to/economy.mjs"],
21
+ "cwd": "/absolute/path/to/game"
22
+ }
23
+ ```
24
+
25
+ The adapter is trusted local code, **not a sandbox**. No shell is used to spawn it. Account/provider credential environment variables are stripped. Paid execution injects a synthetic Playtest token and run/job IDs; preflight and replay inject an invalid token sentinel so an inherited token cannot accidentally mark the session as production.
26
+
27
+ Create `simulation.json`:
28
+
29
+ ```json
30
+ {
31
+ "version": 1,
32
+ "seed": "economy-review-1",
33
+ "runs": 100,
34
+ "max_steps": 12,
35
+ "workers": 4,
36
+ "strategies": [
37
+ { "name": "random", "policy": "random" },
38
+ { "name": "collector", "policy": "scored" },
39
+ { "name": "saver", "policy": "scripted", "actions": ["earn", "earn", "earn", "earn", "earn", "earn", "earn", "earn"] }
40
+ ]
41
+ }
42
+ ```
43
+
44
+ ```sh
45
+ ravensight-playtest simulate --config simulation.json --driver-config bridge.json
46
+ ```
47
+
48
+ A `driver_config` path inside the simulation file may replace `--driver-config`. The CLI probes the adapter before purchasing. A probe only starts a game and validates its first observation; it does not run the batch. Keep adapter initialization free of destructive side effects.
49
+
50
+ Strategy names start with a lowercase letter and contain at most 64 lowercase letters, digits, underscores or hyphens. Strategies are assigned round-robin. Each run receives a seed derived from the root seed and its zero-based run index. Worker count changes concurrency, not action selection. `random` chooses among legal action IDs deterministically. `scripted` follows supplied IDs and fails on an illegal action or exhaustion before a terminal state. `scored` chooses the highest score supplied by the game for that strategy name, with a deterministic action-ID tie break. Names such as "aggressive" have no built-in gameplay meaning.
51
+
52
+ ## Adapter protocol
53
+
54
+ Use one JSON-RPC 2.0 JSON object per line on stdout. Send diagnostics to stderr. The runner starts a fresh subprocess for each game, with one request outstanding at a time.
55
+
56
+ ```json
57
+ {"jsonrpc":"2.0","id":1,"method":"simulation.start","params":{"version":1,"seed":"derived-seed","strategy":"collector","max_steps":12}}
58
+ {"jsonrpc":"2.0","id":1,"result":{"done":false,"state_hash":"state-0","actions":[{"id":"earn","scores":{"collector":1}},{"id":"buy","scores":{"collector":3}}],"metrics":{"coins":4}}}
59
+ {"jsonrpc":"2.0","id":2,"method":"simulation.step","params":{"action":"buy"}}
60
+ {"jsonrpc":"2.0","id":2,"result":{"done":true,"state_hash":"state-1","actions":[],"metrics":{"coins":2,"inventory":1},"outcome":"collected"}}
61
+ ```
62
+
63
+ The game owns state hashing and must include every state component relevant to reproducibility. Return `done` as a boolean, a nonempty `state_hash` of at most 128 characters, and at most 256 unique action IDs of at most 128 characters. Nonterminal observations need legal actions. Optional action scores contain at most 16 finite numeric values keyed by strategy. Optional metrics contain at most 32 finite numeric values between -1e15 and 1e15 with keys of at most 80 characters. Optional outcomes are nonempty strings of at most 80 characters. Reserved names `__proto__`, `constructor` and `prototype` are rejected.
64
+
65
+ ## Limits and results
66
+
67
+ - 1 to 10,000 requested games, 1 to 10,000 actions per game, 1 to 16 workers and 1 to 16 strategies.
68
+ - At most 1,000,000 actions across a batch and at most 15 minutes of execution, subject to the registered server budget.
69
+ - At most 30 seconds per response and 256 KiB per response. Cancellation terminates the subprocess group, escalating from SIGTERM to SIGKILL.
70
+ - At most ten selected traces: the first ten run indices, not an exhaustive failure archive. Each trace is bounded to 4 MiB and 10,000 actions.
71
+ - Per-strategy summaries retain at most 32 distinct metric names and 128 outcome buckets. Additional outcome names are grouped as `other`; keep metric names stable between games.
72
+
73
+ `duration_ms` reports execution time within the batch deadline, excluding any cleanup or post-stop serialization beyond that deadline. `results.jsonl` streams one local result per requested run. `summary.json` contains completed, failed and truncated totals, total actions, terminal outcome counts and terminal metric count/min/max/mean by strategy. Metrics are run-level terminal values, not a mean across every observation. A terminal result without an outcome uses `unspecified`. Reaching `max_steps` is **truncated**, never a successful terminal outcome. Games not started because of cancellation or a batch limit count as failed, with the stop reason retained in the local report.
74
+
75
+ Only the bounded summary is uploaded to the existing Playtest job. Adapter scripts, raw states, per-game rows, replay traces and scripted action lists remain local. The public configuration hash covers the registered configuration and strategy names/policies; trace metadata also includes a separate full local configuration hash covering scripts.
76
+
77
+ ## Recovery and replay
78
+
79
+ The CLI prints a durable recovery journal path. Recover the same purchase rather than starting another batch:
80
+
81
+ ```sh
82
+ ravensight-playtest simulate --resume .ravensight/simulations/BATCH_ID/state.json
83
+ ```
84
+
85
+ Resume preserves the API host, game, configuration and purchase identity. Do not combine it with a new config or bridge argument. A changed bridge configuration is rejected. Keep the adapter build and its dependencies unchanged for reproducible recovery. After a process interruption, any restarted local attempt has its own output directory. If the summary was already saved, resume uploads that summary without executing the games again. A terminal server job is not restarted.
86
+
87
+ ```sh
88
+ ravensight-playtest simulation-replay /absolute/path/to/trace-00000.json --driver-config bridge.json
89
+ ```
90
+
91
+ Replay checks each recorded state hash, terminal flag, outcome and metrics and reports the first divergence. A matching replay verifies that recording with the current adapter; it does not prove the game is deterministic. External services, clocks, platform differences and ignored seeds can cause divergence. A trace from a max-step-truncated game only covers the recorded window, not a finished game. Size-capped or failed recordings can verify their captured prefix, reported as `matched_prefix: true` and `fully_replayed: false`. A recorded failed protocol action is retried after that prefix; `failure_reproduced` states whether the same sanitized protocol error recurred. A failed initial start has no state prefix to verify. Policy failures such as an exhausted script only support prefix replay, not recreation of the policy failure. These results never claim a fully reproduced terminal game.
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ // Trusted local adapter example. It has no networking or telemetry.
3
+ import readline from 'node:readline';
4
+ import { createHash } from 'node:crypto';
5
+ let seed, turn, coins, inventory;
6
+ function observe() {
7
+ const done = turn >= 8;
8
+ return { done, state_hash: createHash('sha256').update(JSON.stringify([seed, turn, coins, inventory])).digest('hex'),
9
+ actions: done ? [] : [{ id: 'earn', scores: { saver: 2, collector: 1 } }, ...(coins >= 2 ? [{ id: 'buy', scores: { saver: 0, collector: 3 } }] : [])],
10
+ metrics: { coins, inventory }, ...(done ? { outcome: inventory >= 2 ? 'collected' : 'saved' } : {}) };
11
+ }
12
+ for await (const line of readline.createInterface({ input: process.stdin })) {
13
+ let request;
14
+ try {
15
+ request = JSON.parse(line);
16
+ if (request.method === 'simulation.start') { seed = request.params.seed; turn = 0; inventory = 0; coins = parseInt(createHash('sha256').update(seed).digest('hex').slice(0, 4), 16) % 3; }
17
+ else if (request.method === 'simulation.step' && !observe().done && observe().actions.some(a => a.id === request.params.action)) {
18
+ turn++; if (request.params.action === 'earn') coins++; else { coins -= 2; inventory++; }
19
+ } else throw new Error('Illegal simulation request');
20
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: observe() }) + '\n');
21
+ } catch { process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: request?.id, error: { code: -32602, message: 'Invalid request' } }) + '\n'); }
22
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ravensight-playtest",
3
- "version": "0.3.1",
4
- "description": "AI personas play your game and file reports to Ravensight.",
3
+ "version": "0.4.0",
4
+ "description": "Local AI playtests and seeded game simulations with reports in Ravensight.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://ravensight.io/docs/#playtest",
7
7
  "bugs": {
@@ -12,6 +12,8 @@
12
12
  "playtest",
13
13
  "game-analytics",
14
14
  "ai-personas",
15
+ "simulation",
16
+ "game-balance",
15
17
  "ravensight"
16
18
  ],
17
19
  "type": "module",
@@ -27,6 +29,8 @@
27
29
  "addons",
28
30
  "docs/ADAPTERS.md",
29
31
  "docs/NATIVE_RUNNER.md",
32
+ "docs/SIMULATION.md",
33
+ "examples/simulation",
30
34
  "README.md",
31
35
  "LICENSE",
32
36
  "!src/**/*.test.js",
@@ -53,6 +57,6 @@
53
57
  },
54
58
  "optionalDependencies": {
55
59
  "keytar": "^7.9.0",
56
- "playwright": "^1.49.0"
60
+ "playwright": "^1.51.0"
57
61
  }
58
62
  }
package/src/api/client.js CHANGED
@@ -189,9 +189,15 @@ export function createClient(options = {}) {
189
189
  };
190
190
 
191
191
  const jobs = {
192
- estimate: (gameId, { modules = [], personas = [] } = {}) =>
193
- request('POST', `${gameBase(gameId)}/estimate`, { body: { modules, personas } })
194
- .then(answer => answer.body),
192
+ /**
193
+ * Price a job without charging. `runLength: 'long'` asks for the long
194
+ * persona run's price; anything else leaves `run_length` out, which the
195
+ * server reads as a standard run.
196
+ */
197
+ estimate: (gameId, { modules = [], personas = [], runLength = null, simulation } = {}) =>
198
+ request('POST', `${gameBase(gameId)}/estimate`, {
199
+ body: { modules, personas, ...(runLength === 'long' ? { run_length: 'long' } : {}), ...(simulation ? { simulation } : {}) }
200
+ }).then(answer => answer.body),
195
201
 
196
202
  /**
197
203
  * Register a job: this is the request that moves money.