plai_testr 0.2.0 → 0.2.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63333363764746166c9322fd4e6fb527b8afed9768ebbe7263b978ee4f0b7d9a
4
- data.tar.gz: 26e22087d22195de7b0b8f78ce5a9c2b8e60fd2e52e07692fa3b64aa77b737ec
3
+ metadata.gz: b196ea99733867443eb84edef59110ec4eed9f507bc7bc740b1f7077ceae9ce5
4
+ data.tar.gz: 6ac4777ce7f000d856f4309baad759720152b72f50d2e0e45e83f4c7ba369a11
5
5
  SHA512:
6
- metadata.gz: 0303e0a165de516ff226c9a46f3ec599846c0d8c5e4867a7f84da97118fc541f4032b5881ac99500d508d75f1c58f71c285c37b27bfe56f5d3f1fccd21f06bed
7
- data.tar.gz: 23120c2a8fbe909d6c4807038807a0b5cf39064ac64d9cc50d6b1baebbeba49353c3c0cc562c3fba5de712e045b8eba602d3a4336c28e05965709f2a695a155c
6
+ metadata.gz: 13b9b106fb45f6c7bf41b16a4e3ed0fe668507999b6cc838b5a7b7aaad5eacf34f8f3dff5ce2ca21b7235ea695ee2cba266accb2fb9d5a21c5cfc85a09c09ec5
7
+ data.tar.gz: 8cd8ffadd8dfcb2e2c34764e255cc62b667b73471a2843479cb1cfc82e8d70e3e38a62e75c622aebce4ab86ef3d6b3d6626c17a6cf4bae1b213946b268f5d67c
data/CHANGELOG.md CHANGED
@@ -1,6 +1,15 @@
1
1
  # Changelog
2
2
 
3
- ## 0.2.0 (unreleased)
3
+ ## Development
4
+
5
+ ## 0.2.1
6
+
7
+ - Docs reorganized: the README is an overview linking to guides in `docs/` (getting started, ingesting a game with an agent or manually, running playtests) and a technical reference in `docs/technical/`.
8
+ - The Gin Rummy example reads its deck from `data/components/cards.csv`, like Hearts.
9
+ - `new` stamps each example game with its template checksum (`games/<name>/.plai_testr.yml`), so a later update can tell an untouched example (safe to replace) from an edited one (the new version goes alongside).
10
+ - Play and botrun reports show the wall time; a resumed session adds each sitting.
11
+
12
+ ## 0.2.0
4
13
 
5
14
  Upgrading never breaks a game: every new hook has a default, and existing overrides keep working. See *Upgrading an existing app* below.
6
15
 
data/README.md CHANGED
@@ -2,215 +2,41 @@
2
2
 
3
3
  Automated tabletop game playtesting with local LLM players via [Ollama](https://ollama.com).
4
4
 
5
- Your game lists the legal moves each turn as a numbered menu; a model (or a scripted bot) picks one; PlAI Testr applies it, checkpoints, and writes a markdown report. Bots alone can play thousands of games in minutes for balance numbers; model players add reasoning and end-of-game feedback.
5
+ You describe your game to PlAI Testr as a set of rules it can enforce. Each turn it lists the legal moves as a numbered menu, a player picks one, and PlAI Testr applies it, saves the game, and moves on. The player can be a local language model, which reasons about its choices and answers questions about the game afterwards, or a scripted bot, which plays thousands of games in minutes. Every session ends in a markdown report.
6
+
7
+ - **Botruns**: bots play hundreds or thousands of games for balance numbers: who wins, from which seat, with which strategy, how games end and how long they take.
8
+ - **Model games**: language models play a full game, explaining each choice, then answer debrief questions about how it felt and what was confusing.
9
+ - **Personas**: give players different strategies and play styles, and compare how each fares.
10
+ - **Everything runs on your machine.** No accounts or API keys; bots need no model at all.
6
11
 
7
12
  ## Requirements
8
13
 
9
- - Ruby 3.4+
10
- - [Ollama](https://ollama.com), local or on your network (not needed for bot-only runs)
14
+ Ruby 3.4+, and [Ollama](https://ollama.com) for model games (local or on your network; botruns don't need it).
11
15
 
12
16
  ## Quick start
13
17
 
14
18
  ```sh
15
- gem install plai_testr # or in a Gemfile: gem 'plai_testr', github: 'armahillo/plai_testr'
16
- plai_testr new my_playtests # an app with two example games: Hearts and Gin Rummy
19
+ gem install plai_testr
20
+ plai_testr new my_playtests # an app with two example games: Hearts and Gin Rummy
17
21
  cd my_playtests
18
22
  bundle install
19
23
 
20
- bundle exec plai_testr hearts botrun --games 100 # bots only, no model: a report in seconds
21
- bundle exec plai_testr gin_rummy play # one model game (pulls qwen3:8b if missing)
22
- ```
23
-
24
- Reports land in `games/<game>/reports/`. Add your own game with `bundle exec plai_testr generate game my_game`, then fill in the hooks below. The agent guide, [`docs/AGENTS.md`](docs/AGENTS.md), walks an AI coding assistant through building a harness, so you can have your own agent do it. Apps don't copy it: `.plai_testr/gem` in each app links to the installed gem (refreshed by `new`, every command and the app's spec_helper; git-ignored), the app's `CLAUDE.md` imports `@.plai_testr/gem/docs/AGENTS.md`, and the app's own `AGENTS.md` keeps only app-specific notes. Upgrading the gem upgrades the guide.
25
-
26
- ## Commands
27
-
28
- ```
29
- plai_testr <game> play [--seed N] [--model M] [--think] [--mode llm|hybrid|bot] [--players N] [--personas A,B] [--referee M]
30
- [--debrief-model M] [--debrief-think]
31
- plai_testr <game> botrun [--games N | --seeds 1,2,3] [--players N] [--personas all|A,B]
32
- plai_testr <game> resume <session_id>
33
- plai_testr <game> debrief <session_id> [--model M] [--think]
34
- plai_testr <game> preflight
35
- plai_testr <game> compare [--since ID]
36
- plai_testr <game> list
37
- plai_testr new <dir>
38
- plai_testr generate game <name>
39
- plai_testr games
40
- plai_testr help [<command>]
41
- plai_testr --version
42
- ```
43
-
44
- - Every command takes `--help` (`plai_testr hearts botrun --help`, or `plai_testr help botrun`) to list its options and what they do. A command refuses options it doesn't take.
45
-
46
- - `play` runs one game. Without `--seed` it picks one from 1–100 and prints it. Before playing, preflight checks Ollama, pulls missing models, warms them up, measures the first prompt against the context size, and plays 5 bot games to catch menu bugs. `--skip-preflight` skips all of that.
47
- - `--mode hybrid` lets the bot play moves it marks *obvious*; `--mode bot` uses no model.
48
- - After a model game each player answers the game's debrief questions. `--debrief-model` and `--debrief-think` give the debrief its own model and thinking. `debrief <session_id>` asks a completed session's players again (another model, thinking, or a changed `debrief_facts`) and rebuilds its report; earlier answers stay in the checkpoint under `debrief_history`. See [Debrief](#debrief).
49
- - `botrun` plays many bot-only games in memory and writes one report: endings, wins by persona and seat with 95% ranges, game length, bot rules used, warnings and crashes.
50
- - `--quiet` shows a progress line and the result; `--verbose` adds raw model output (`play`) or every game (`botrun`).
51
- - Ctrl+C (or SIGTERM) saves and stops after the current move; a botrun finishes its game and reports. A second Ctrl+C quits at once.
52
- - Errors print one line. `PLAI_TESTR_DEBUG=1` shows the backtrace; mistakes in game code (`MoveError`) always do.
53
-
54
- ## Writing a game
55
-
56
- `generate game` creates `games/<name>/` with everything below. The examples in [`templates/examples/`](templates/examples) are complete games to copy from.
57
-
58
- ```
59
- games/<name>/
60
- lib/<name>.rb Requires the game and calls PlaiTestr.register
61
- lib/<name>/bot.rb Bot rules (bot_choice), included in the runner
62
- lib/<name>/session/ Runner (rules engine)
63
- lib/<name>/state/ GameState / PlayerState subclasses
64
- data/rules.json Name, version, player count, numbers the engine uses
65
- data/components/ Card lists and other components, one file each (.csv, .yml or .json)
66
- data/rules.md Rules text the model players read
67
- data/personas.yml Optional: strategy and play styles for model players
68
- data/bots.yml Optional: parameters the bot rules read
69
- spec/
70
- ```
71
-
72
- ### Components
73
-
74
- Each file in `data/components/` is one component, named after the file: `PlaiTestr::Deck.build(@rules, component: 'cards')` returns `data/components/cards.csv` (or `.yml`, `.json`) as a list of cards, each with its own `uid` (without `component:`, the first deck). A file is a list of records: a CSV with a header row (numbers become numbers; empty cells are left out), or a YAML or JSON list of hashes. A `copies` field makes that many copies. Small decks can stay inline under `components` in `rules.json` instead (`games/gin_rummy` does this; `games/hearts` uses `cards.csv`).
75
-
76
- PlAI Testr reads only this format. Converting a designer's own files (spreadsheets, YAML with metadata, exports from card tools) is the game's job: copy or convert them into `data/components/`, with a small import script in the game folder if the source keeps changing.
77
-
78
- ### Hooks
79
-
80
- A game subclasses `PlaiTestr::Session::Runner`. These hooks are its public API ([`runner.rb`](lib/plai_testr/session/runner.rb)):
81
-
82
- | Hook | Required | Purpose |
83
- |---|---|---|
84
- | `setup` | yes | Build the starting state. Not called on resume. |
85
- | `legal_moves(player)` | yes | The menu: an array of `option(...)` entries |
86
- | `apply_action_effects(player, action)` | yes | Apply the chosen move's action to `@game_state` |
87
- | `next_player` | | Who acts next (default: alternate) |
88
- | `pick_range(player)` | | `[min, max]` to pick several options at once |
89
- | `game_over?` | | Default: `status['game_over']` |
90
- | `turn_phase(player)` | | Short step label for the console and report |
91
- | `view_text_for(player)` | expected | The player's view of the game, as text |
92
- | `rules_text_for(player)` | | Rules text (default: the game's `data/rules.md`; call `super` to extend it) |
93
- | `prompt_note_for(player)` | | A one-off note for this prompt |
94
- | `recent_events_for(player)` | | Recent events from the event log (default: the last 8 this player may see; `recent_events:` in `register`) |
95
- | `log_turn`, `log_turn_name` | | The number stamped on log entries and its name (default: the decision number) |
96
- | `persona_for(player)` | | Strategy text for the system prompt (default: built from `personas.yml`) |
97
- | `display_name(player)` | | Name in the console and reports (default: the persona's key, title-cased, else the player id) |
98
- | `bot_choice(player, moves, pick: nil)` | | Bot rules: `nil` or `{ 'choice' => n, 'rule' => name, 'obvious' => bool }` (`'choices'` on pick turns) |
99
- | `think_about?(player)` | | With `--think`: whether this turn gets thinking (default: every turn) |
100
- | `debrief_questions` | | End-of-game feedback questions |
101
- | `debrief_facts(player)` | expected | Engine-counted facts for the debrief, as a Hash (default: `botrun_stats`; see [Debrief](#debrief)) |
102
- | `debrief_view_for(player)` | | The final position the debrief reads (default: the result, final scores, `view_text_for` and the player's visible log) |
103
- | `debrief_summary(player)` | | Built by the gem, not overridden: the summary that opens each debrief prompt |
104
- | `report_sections`, `report_debrief?` | | Extra report sections; false to render the debrief yourself |
105
- | `botrun_stats` | | Game-specific numbers for one botrun game |
106
-
107
- Helpers for hooks: `option`, `menu(player)` (the menu as the runner sees it), `data`, `log(text, to: nil)`, `event_log`, `players`, `pstate(player)`, `whose(player, viewer)`, `tally(player, key, amount = 1)` (counts in `data['tally']`), `sole_leader(scores)`, `persona(player)`, `persona_name(player)`, `bot_strategy_for(player)`, `@rng`, `@rules`, `@game_state`, `out`. Specs drive a runner with `setup`, `play_turn`, `play_out` and the hooks above.
108
-
109
- ### Moves
110
-
111
- ```ruby
112
- def legal_moves(player)
113
- hand(player).map { |card| option("Play #{card['name']}", public: "played #{card['name']}", play: { value: card['uid'] }) }
114
- end
115
-
116
- def apply_action_effects(player, action)
117
- case action
118
- in { play: { value: uid } } then play(player, uid)
119
- in { pass: { value: uids } } then pass(player, uids)
120
- end
121
- end
122
- ```
123
-
124
- `label` (the first argument) is what the acting player sees; `public:` is what others see; `log:` is a shorter console line. An action has one name; `value:` is the main parameter and others sit beside it. Moves and actions have symbol keys, so `apply_action_effects` can pattern-match them; a hand-written move with string keys is converted. On a pick turn the chosen values arrive together: `{ pass: { value: [42, 17, 9] } }`. Every menu is checked before it is used: a misspelled key or a malformed action raises `PlaiTestr::MoveError` naming the hook, player, decision and option. A menu with one option is applied without asking anyone.
125
-
126
- ### State
127
-
128
- `@game_state` ([`game_state.rb`](lib/plai_testr/state/game_state.rb)) holds `turn` (decisions so far), `players`, `status` and `data`; each `player_state(name)` ([`player_state.rb`](lib/plai_testr/state/player_state.rb)) holds `score`, `hand` and `data`.
129
-
130
- - `status` belongs to the runner: set `game_over`, `winner` and `end_reason` when the game ends, with `@game_state.apply_changes('status' => {...})`.
131
- - `data` is the game's own state, as plain JSON-safe values so checkpoints work without extra code.
132
- - Subclass `GameState` to name your keys (`def step = @data['step']`) and to hide information per player with `data_for(observer)` (the default already filters the event log per player; extend it with `super.except('deck')`); subclass `PlayerState` and override `serialize_for` / `public_data` to choose what opponents see. The examples show both.
133
-
134
- ### Personas and bots
135
-
136
- `data/personas.yml` gives model players a shared `basic_strategy` and play styles under `personas`; `data/bots.yml` gives the bot rules parameters, per persona, read with `bot_strategy_for(player)`. Both files are optional and the generated copies explain their format. Each player gets a persona from `--personas a,b` or at random per seed; `botrun --personas all` cycles every pairing.
137
-
138
- ### Randomness and limits
139
-
140
- - Use `@rng` for all randomness. It is reset for each decision from the seed and the decision number, so the same seed and the same choices always give the same game, interrupted or not.
141
- - When the model never gives a valid answer, the bot's choice is played instead (or a random legal move from a separate generator), and the turn is flagged.
142
- - Every game stops at a decision limit (`end_reason: 'decision_limit_reached'`, no winner) so a buggy game cannot run forever. Set `max_decisions:` in `PlaiTestr.register`; otherwise the app backstop applies.
143
-
144
- ### Specs
145
-
146
- `require 'plai_testr/rspec'` (the generated spec_helper does) provides the checks every game runs:
147
-
148
- ```ruby
149
- RSpec.describe MyGame::Session::Runner do
150
- it_behaves_like 'a PlAI Testr game', 'my_game' # seeds: [1, 2, 3], pending: 'reason'
151
- end
24
+ bundle exec plai_testr hearts botrun --games 100 # bots only: a report in seconds
25
+ bundle exec plai_testr gin_rummy play # one model game (downloads qwen3:8b if missing)
152
26
  ```
153
27
 
154
- At every player count in `rules.json`: bot games reach an ending; every turn and debrief prompt builds; state survives a JSON round trip; resuming from a mid-game checkpoint gives the same result; no player sees another's hand. New checks arrive with gem upgrades.
155
-
156
- ### Debrief
157
-
158
- After a model game, each player answers `debrief_questions`. The prompt opens with a summary the gem builds (`debrief_summary`):
159
-
160
- - **Outcome**: winner, end reason, final scores
161
- - **Game facts**: `debrief_facts(player)`, shown as YAML
162
- - **Your decisions**: how many the model made, how often it chose the obvious move, fallbacks
163
- - **Your plan as it changed**: the player's own `plan` each time it changed, with the decision and step
164
-
165
- then `debrief_view_for(player)`. The model is told to treat the summary as correct and to cite the round of any event it mentions. Small models invent answers when they have to recall a long game from its log, so the facts carry the debrief:
166
-
167
- - Return the numbers the questions depend on, per player: points by source, counts of key actions, races or majorities and who won them, who ended the game and how.
168
- - Compute them only from `data` and player state. `debrief <session_id>` rebuilds them from the checkpoint later.
169
- - Ask questions the facts and the player's own plans can answer.
170
-
171
- `check!` (run by every game command) warns when a game defines neither `debrief_facts` nor `botrun_stats`. Preflight builds every player's debrief after each lint game, so a broken hook stops before a model game starts.
172
-
173
- ### Nested decisions and rulings
174
-
175
- - [FrameQueue](docs/frame_queue.md): attacks and responses, triggered effects, decisions inside decisions.
176
- - [Referee](docs/referee.md): yes/no rulings on free-text conditions by a second model.
177
-
178
- ## Configuration
179
-
180
- `config/plai_testr.rb` in the app (generated, all commented out):
181
-
182
- ```ruby
183
- PlaiTestr.configure do |config|
184
- config.max_decisions = 100_000 # backstop for games without their own limit
185
- config.player_model = 'qwen3:8b'
186
- config.ollama_host = 'localhost'
187
- config.ollama_port = 11434
188
- config.model_settings = { context_size: 8192 }
189
- end
190
- ```
191
-
192
- A game can override model settings in `PlaiTestr.register(..., model_settings: { ... })`.
193
-
194
- | Model setting | Default | Used for |
195
- |---|---|---|
196
- | `context_size` | 8192 | Menu turns without `--think`; the referee |
197
- | `long_context_size` | 16384 | Sessions with `--think`; the debrief |
198
- | `menu_budget` | 512 | Output tokens for a menu answer |
199
- | `think_budget` | 8192 | Output tokens for a thinking turn |
200
- | `debrief_budget` | 1024 | Output tokens for the debrief |
201
- | `referee_budget` | 2048 | Output tokens for a ruling |
202
- | `temperature` | 0.6 | Player sampling |
203
-
204
- Values outside the hard limits are clamped with a warning: context sizes between 2048 and the model's own maximum, temperature 0–2, budgets above 0 and below the context size. The values used are recorded in each report. Environment variables set the defaults: `PLAI_TESTR_PLAYER_MODEL`, `PLAI_TESTR_REFEREE_MODEL`, `PLAI_TESTR_OLLAMA_HOST`, `PLAI_TESTR_OLLAMA_PORT`.
28
+ ## Documentation
205
29
 
206
- ## Session output
30
+ - [Getting started](docs/getting-started.md): install PlAI Testr and Ollama, create your app, and check that everything works. Start here.
31
+ - [Ingesting a game with an agent](docs/ingesting-a-game-with-an-agent.md): have an AI coding assistant (Claude Code or similar) build the harness for your game, and what to give it.
32
+ - [Ingesting a game manually](docs/ingesting-a-game-manually.md): build the harness yourself in Ruby, step by step, with links to the technical reference.
33
+ - [Running playtests](docs/running-playtests.md): every command and option, settings, output files, and how to get useful results.
207
34
 
208
- - `sessions/<id>/state.json`: the checkpoint, replaced atomically after every decision
209
- - `sessions/<id>/transcript.jsonl`: one JSON object per decision, with the model's reasoning. After a crash the last line may be cut off; skip lines that do not parse.
210
- - `sessions/<id>/think.log`: every model reply with Ollama's timings
211
- - `reports/<id>.md`: the session report; `debrief <id>` rebuilds it
35
+ Reference:
212
36
 
213
- `resume` continues an interrupted session exactly, or refuses: if the game's data, code or the gem version changed since the session started, start a new one with `play`.
37
+ - [Technical reference](docs/technical/): how the runner, state, components, personas, bots, debriefs, reports and specs work, for anyone writing or reviewing harness code.
38
+ - [Agent guide](docs/AGENTS.md): instructions written for AI assistants working in a PlAI Testr app. Your app's `CLAUDE.md` imports it.
39
+ - [Changelog](CHANGELOG.md): what changed in each version, and how to upgrade an existing app.
214
40
 
215
41
  ## Development
216
42
 
data/docs/AGENTS.md CHANGED
@@ -12,9 +12,9 @@ When you open an app:
12
12
  2. Run `bundle exec rspec`. It is fast, needs no model, shows the state of every game, and refreshes `.plai_testr/gem`.
13
13
  3. Look at `games/`. Each game's `data/rules.md` is its rulebook, and `NOTES.md` (if present) holds rulings and open questions.
14
14
  4. Read the gem's docs when you need them, under `.plai_testr/gem/` (they match the installed version):
15
- - `README.md`: every runner hook, the state classes, configuration
16
- - `docs/frame_queue.md`: decisions inside decisions (attacks and responses, triggered effects), with a complete example
17
- - `docs/referee.md`: yes/no rulings on free-text conditions by a second model, with an example
15
+ - `docs/technical/`: the technical reference. `runner.md` (every hook, moves, helpers, randomness, limits), `state.md` (state, hidden information, checkpoints), `rules-and-components.md`, `personas-and-bots.md`, `debrief.md`, `reports.md`, `specs.md`, `frame-queue.md` (decisions inside decisions, with a complete example) and `referee.md` (yes/no rulings by a second model)
16
+ - `docs/running-playtests.md`: every command and option, settings, output files
17
+ - `docs/getting-started.md`, `docs/ingesting-a-game-with-an-agent.md`, `docs/ingesting-a-game-manually.md`: the designer's guides, to point them at
18
18
  - `templates/examples/`: pristine copies of the example games, in case they were deleted from `games/`
19
19
 
20
20
  The example games show working patterns to copy:
@@ -48,6 +48,7 @@ games/<name>/
48
48
  data/bots.yml Optional: parameters the bot reads, per persona
49
49
  NOTES.md Optional: open questions and unconfirmed engine choices
50
50
  spec/ The game's specs
51
+ .plai_testr.yml Example games only: the checksum of the template it was copied from (keep it: updates read it)
51
52
  sessions/ reports/ Output (gitignored)
52
53
  ```
53
54
 
@@ -94,11 +95,11 @@ A menu with one move is applied without asking anyone. `pick_range(player)` retu
94
95
 
95
96
  Randomness comes from `@rng`. It is reset for each decision from the seed and the decision number, so the same seed and the same choices always give the same game, interrupted or not.
96
97
 
97
- The gem's `README.md` (`.plai_testr/gem/README.md`) lists every hook.
98
+ `.plai_testr/gem/docs/technical/runner.md` lists every hook and helper.
98
99
 
99
100
  ## Components
100
101
 
101
- Card lists, tiles and other components go in `data/components/`, one file per component, named after the file. `PlaiTestr::Deck.build(@rules, component: 'cards')` returns `data/components/cards.csv` (or `.yml`, `.json`) with a `uid` on every card; without `component:` it returns the first deck. A file is a list of records: a CSV with a header row (numbers become numbers, empty cells are left out) or a YAML or JSON list of hashes. A `copies` field makes that many copies. `games/hearts` reads `data/components/cards.csv`; `games/gin_rummy` keeps its deck inline under `components` in `rules.json`, which suits small generated decks. `generate game` creates a placeholder `data/components/cards.yml`; replace it.
102
+ Card lists, tiles and other components go in `data/components/`, one file per component, named after the file. `PlaiTestr::Deck.build(@rules, component: 'cards')` returns `data/components/cards.csv` (or `.yml`, `.json`) with a `uid` on every card; without `component:` it returns the first deck. A file is a list of records: a CSV with a header row (numbers become numbers, empty cells are left out) or a YAML or JSON list of hashes. A `copies` field makes that many copies. Both example games (`games/hearts`, `games/gin_rummy`) read `data/components/cards.csv`. `generate game` creates a placeholder `data/components/cards.yml`; replace it.
102
103
 
103
104
  - Ask the designer for their component list in whatever form they have it (a spreadsheet or CSV export, YAML, a pasted table, a PDF). Don't retype it from the rules text.
104
105
  - Convert it to the format above. Keep the fields the engine or the players need; leave out print layout, art notes and other metadata.
@@ -0,0 +1,137 @@
1
+ # Getting started
2
+
3
+ This guide covers setting up PlAI Testr on your computer and checking that it works. You'll need to be comfortable typing commands into a terminal; you don't need to know Ruby or Ollama.
4
+
5
+ ## Step 0: Prerequisites
6
+
7
+ ### Hardware
8
+
9
+ Running a language model locally takes a lot of processing power, ideally a GPU. If your computer can't run a model well, PlAI Testr is still useful: botruns use no model at all. Personally, I've found Apple M-series computers well suited to this, but a fairly modern graphics card may be fine too. There's no harm in trying!
10
+
11
+ Models also take disk space. The default model, `qwen3:8b`, is about 5 GB.
12
+
13
+ ### Ruby
14
+
15
+ PlAI Testr needs Ruby 3.4 or newer. Check with:
16
+
17
+ ```sh
18
+ ruby -v
19
+ ```
20
+
21
+ You should see something like `ruby 3.4.4 (...)`. If you don't have Ruby, or it's older, the [official installation guide](https://www.ruby-lang.org/en/documentation/installation/) lists the options. After installing, open a new terminal window before continuing.
22
+
23
+ ### Ollama
24
+
25
+ [Ollama](https://ollama.com) is a free program that runs language models on your computer, with no internet connection needed once a model is downloaded. If you're on a Mac, I wrote a guide: [Setting up a local LLM on a MacOS device](https://armahillo.dev/guides/local-llm-macos/ollama.html). The steps are similar on other systems.
26
+
27
+ You don't need to download a model yourself; PlAI Testr downloads the one it needs the first time it runs. Ollama can also run on another computer on your network (see [Running playtests](running-playtests.md#settings)).
28
+
29
+ ## Step 1: Install the gem
30
+
31
+ PlAI Testr is a Ruby *gem*: a package of Ruby code, installed with Ruby's `gem` command.
32
+
33
+ ```sh
34
+ gem install plai_testr
35
+ ```
36
+
37
+ ## Step 2: Create your app
38
+
39
+ An *app* is a folder that holds your games, their settings and their results. This guide calls it `my_playtests`; I call mine `game_tester`. Pick a name in lowercase, with `_` instead of spaces and preferably only letters: `game_tester`, `my_playtests` and `simulator` are all fine; `Game Tester Software`, `MyPlaytests` and `Simulator9000` might work, but are best avoided.
40
+
41
+ ```sh
42
+ plai_testr new my_playtests
43
+ cd my_playtests
44
+ bundle install
45
+ ```
46
+
47
+ `bundle install` installs the other gems PlAI Testr depends on, at the versions this app needs. Gems depending on other gems is a common Ruby practice, comparable to `import` in Python or JavaScript. Run it again whenever the app's `Gemfile` changes.
48
+
49
+ From here on, commands start with `bundle exec`, as in `bundle exec plai_testr hearts botrun`. It tells Ruby to use the exact gem versions this app was set up with. Commands often work without it, but using it avoids version mix-ups.
50
+
51
+ ### What's in the app
52
+
53
+ ```
54
+ my_playtests/
55
+ games/
56
+ hearts/ Example game: 4-player trick-taking
57
+ gin_rummy/ Example game: 2-player rummy
58
+ config/plai_testr.rb Settings for the whole app: model, Ollama address (all optional)
59
+ AGENTS.md, CLAUDE.md Instructions for AI coding assistants (see Ingesting a game with an agent)
60
+ Gemfile The gems this app uses
61
+ spec/ Automated checks for every game
62
+ ```
63
+
64
+ Each game lives entirely in its own folder under `games/`. Its results go in `games/<game>/reports/`, and saved games in `games/<game>/sessions/`.
65
+
66
+ Hearts and Gin Rummy are complete, working games. Use them to check your setup now, and as examples when you add your own game.
67
+
68
+ ## Step 3: Check that it works
69
+
70
+ Run these in order, from the app folder. Each one tests a little more.
71
+
72
+ ### Preflight (fast)
73
+
74
+ `preflight` checks that Ollama is running and reachable, downloads the model if it's missing, sends one test prompt, and plays a few quick bot games to catch mistakes in the game's code. The first run may take a while to download the model; after that it takes seconds.
75
+
76
+ ```sh
77
+ bundle exec plai_testr gin_rummy preflight
78
+ ```
79
+
80
+ ```
81
+ Ollama at localhost:11434: reachable
82
+ qwen3:8b: ready
83
+ First prompt: 1196 of 8192 tokens
84
+ Lint: 5 bot games played and debriefs built without errors
85
+ ```
86
+
87
+ If Ollama isn't reachable, make sure the Ollama app is running and try again.
88
+
89
+ ### A botrun (fast)
90
+
91
+ A botrun plays many games with scripted bots instead of a model: simple "if this, then that" rules, no reasoning. Bots are fast (100 or even 1000 games take seconds) and good at showing balance problems and dominant strategies.
92
+
93
+ ```sh
94
+ bundle exec plai_testr gin_rummy botrun --games 100
95
+ ```
96
+
97
+ The summary prints when it finishes and is saved in `games/gin_rummy/reports/`. It shows how games ended, win rates by strategy and seat, game lengths, and which bot rules were used.
98
+
99
+ ### A model game (slow)
100
+
101
+ A model game is one full game played by a language model, which explains each choice and answers questions about the game at the end. It takes much longer: on my Apple M1 Max, a game of Hearts took 23 minutes for about 550 decisions.
102
+
103
+ ```sh
104
+ bundle exec plai_testr hearts play
105
+ ```
106
+
107
+ `play` runs preflight first, then shows each decision as it happens:
108
+
109
+ ```
110
+ Turn 373 — Moon Shooter (Player 1) · hand 7 · trick 10 (4 options)
111
+ 🧠 LLM -> Play 10♦
112
+ Turn 374 — Safe Player (Player 2) · hand 7 · trick 10: Play K♦ (wins the trick so far) [only option]
113
+ Turn 375 — Moon Shooter (Player 3) · hand 7 · trick 10 (4 options)
114
+ 🧠 LLM -> Play 3♠ (cannot win this trick)
115
+ ```
116
+
117
+ Press Ctrl+C to stop; the game is saved and can be continued later. When the game finishes, the players are asked the debrief questions, and the report's file name is printed. Reports are in `games/hearts/reports/`.
118
+
119
+ If all three worked, you're set up. Next:
120
+
121
+ - [Ingesting a game with an agent](ingesting-a-game-with-an-agent.md) or [manually](ingesting-a-game-manually.md), to add your own game.
122
+ - [Running playtests](running-playtests.md), for every command and option.
123
+
124
+ Looking for an easy first game to try? Model Blackjack ([rules](https://bicyclecards.com/how-to-play/blackjack), public domain) and give its bots a few different strategies. The ingesting guides use it as their example.
125
+
126
+ ## Updating PlAI Testr
127
+
128
+ ```sh
129
+ gem update plai_testr
130
+ bundle update plai_testr
131
+ ```
132
+
133
+ The app's `Gemfile` allows updates within the same minor version (for example 0.2.x). To move to a new one, change the version in the `Gemfile` (`gem 'plai_testr', '~> 0.3'`) and run `bundle update plai_testr`.
134
+
135
+ Before updating, read the [changelog](../CHANGELOG.md): versions that need changes to existing apps or games list them under *Upgrading an existing app*. Then run `bundle exec rspec` to check every game still works.
136
+
137
+ A game saved under one version can't be resumed under another; finish interrupted games before updating, or start them again.
@@ -0,0 +1,112 @@
1
+ # Ingesting a game manually
2
+
3
+ This guide walks through building a harness for your game by hand: the order to work in and what each step produces. It assumes you can program and can look up Ruby syntax as you go. How each piece works is covered in the [technical reference](technical/); this guide links to it at each step. To have an AI assistant do this instead, see [Ingesting a game with an agent](ingesting-a-game-with-an-agent.md).
4
+
5
+ If writing the rules was "draw two circles", this is "now draw the rest of the owl". Expect it to take a while, with plenty of trial and error: a simple card game is an afternoon, a game with nested decisions or many card effects much longer. I applaud your curiosity and adventurous spirit!
6
+
7
+ The two approaches mix well, too: have an agent write the first version, then read and change it yourself. The standard checks catch most mistakes either way. Most of the time goes into the runner. The example games, `games/hearts` and `games/gin_rummy` in your app, are complete harnesses to read alongside this guide.
8
+
9
+ ## How it fits together
10
+
11
+ The gem runs the game loop: it asks your code who acts next and what their legal moves are, shows the moves to a model or bot as a numbered menu, passes the chosen move back to your code to apply, saves the game, and repeats until your code says the game is over. Then it debriefs the model players and writes the report.
12
+
13
+ Your game supplies the rules: a **runner** (a subclass of `PlaiTestr::Session::Runner`) that builds menus and applies moves, a **state** class describing what is stored and who may see it, a **bot**, and **data files** for the rules text and components. See [The runner](technical/runner.md) for the loop in detail.
14
+
15
+ ## Step 1: Generate the skeleton
16
+
17
+ ```sh
18
+ bundle exec plai_testr generate game blackjack
19
+ ```
20
+
21
+ ```
22
+ games/blackjack/
23
+ lib/blackjack.rb Requires the game's files and registers it with PlaiTestr.register
24
+ lib/blackjack/session/runner.rb The runner: every hook you must write, with comments
25
+ lib/blackjack/state/game_state.rb The GameState subclass
26
+ lib/blackjack/bot.rb bot_choice, included in the runner
27
+ data/rules.json Name, version, player count; add the numbers your code uses
28
+ data/rules.md Rules text the model players read
29
+ data/components/cards.yml A placeholder component file
30
+ data/personas.yml, data/bots.yml Empty, with the format in comments
31
+ spec/session/runner_spec.rb The standard checks, pending until the game can play itself
32
+ ```
33
+
34
+ The generated runner raises `NotImplementedError` from each hook you still have to write, with a hint of what to return. Comments list the hooks the gem already implements, so you can see what not to write.
35
+
36
+ ## Step 2: Rules and components
37
+
38
+ Write the rules text in `data/rules.md`. Put the game's numbers (points to win, hand size, limits) in `data/rules.json` so the code reads them instead of hard-coding them, and the components in `data/components/`, one file per component.
39
+
40
+ Read the rules as an implementer: list every gap or ambiguity, settle each one, and record the answer in a **Rulings** section at the end of `rules.md`. If the harness deliberately simplifies a rule, say so in a **Simplifications** section.
41
+
42
+ See [Rules and components](technical/rules-and-components.md).
43
+
44
+ ## Step 3: State
45
+
46
+ Decide what your game stores: decks, hands, piles, scores, whose turn it is, and the current *step* (the point in a turn the game is at, such as `draw` or `discard`). It all goes in `data`, as plain JSON values, so saving and resuming need no extra code. Use stable ids for things players refer to, such as each card's `uid`.
47
+
48
+ Then decide what each player may see, and hide the rest in your `GameState` subclass.
49
+
50
+ See [State](technical/state.md).
51
+
52
+ ## Step 4: The runner
53
+
54
+ Write the three required hooks:
55
+
56
+ - `setup`: build the starting state (shuffle, deal) with `@rng`.
57
+ - `legal_moves(player)`: the menu for the current step, built with `option(...)`.
58
+ - `apply_action_effects(player, action)`: apply the chosen move, advance the step, and end the game when it's over.
59
+
60
+ Then `view_text_for(player)`, which shows the player the game as text, and `turn_phase(player)`, a short label for the console. Add `next_player` if turns don't simply alternate, and `pick_range` for "choose N cards" decisions.
61
+
62
+ See [The runner](technical/runner.md). For attacks and responses, triggered effects or other decisions inside decisions, see [Nested decisions with FrameQueue](technical/frame-queue.md). For card conditions too awkward to code, see [Yes/no rulings with the referee](technical/referee.md).
63
+
64
+ ## Step 5: Specs for the rules
65
+
66
+ Before any model plays, write specs that set up a situation and check the menu and the effects: "with a soft 17, the dealer must hit", "a bust ends the hand". Rules bugs are cheapest to find here.
67
+
68
+ ```sh
69
+ bundle exec rspec games/blackjack/spec
70
+ ```
71
+
72
+ See [Specs](technical/specs.md).
73
+
74
+ ## Step 6: The bot
75
+
76
+ Write `bot_choice` in `lib/blackjack/bot.rb` so the game can play itself, then remove the `pending:` from the standard checks in `spec/session/runner_spec.rb`. Run a botrun:
77
+
78
+ ```sh
79
+ bundle exec plai_testr blackjack botrun --games 1000
80
+ ```
81
+
82
+ It should finish with no crashes and sensible endings. Add personas and per-persona bot settings if you want to compare play styles.
83
+
84
+ See [Personas and bots](technical/personas-and-bots.md).
85
+
86
+ ## Step 7: The debrief
87
+
88
+ Write `debrief_questions` and `debrief_facts` together. The facts are numbers your code counts during the game (points by source, how often each key action happened, who ended the game), which keep the models' answers about the game accurate.
89
+
90
+ See [Debrief](technical/debrief.md).
91
+
92
+ ## Step 8: Check the prompts
93
+
94
+ Read a turn prompt and a debrief prompt yourself before running a model. Check that every option states facts rather than advice, that nothing hidden leaks, and that the view has what a player needs to decide. [The runner](technical/runner.md#checking-a-prompt) shows a script that prints both.
95
+
96
+ ## Step 9: Play
97
+
98
+ ```sh
99
+ bundle exec plai_testr blackjack preflight
100
+ bundle exec plai_testr blackjack play --mode hybrid
101
+ bundle exec plai_testr blackjack play
102
+ ```
103
+
104
+ `--mode hybrid` lets the bot play the moves it marks obvious, which makes a quicker first model game. See [Running playtests](running-playtests.md).
105
+
106
+ ## Optional: Reports
107
+
108
+ Add sections to each session report, or numbers to the botrun report, for what you want to track in your game. See [Reports](technical/reports.md).
109
+
110
+ ## Feedback
111
+
112
+ I welcome feedback on what more information or affordances would make this easier: [open an issue](https://github.com/armahillo/plai_testr/issues).