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 +4 -4
- data/CHANGELOG.md +10 -1
- data/README.md +20 -194
- data/docs/AGENTS.md +6 -5
- data/docs/getting-started.md +137 -0
- data/docs/ingesting-a-game-manually.md +112 -0
- data/docs/ingesting-a-game-with-an-agent.md +138 -0
- data/docs/running-playtests.md +193 -0
- data/docs/technical/README.md +15 -0
- data/docs/technical/debrief.md +55 -0
- data/docs/technical/personas-and-bots.md +67 -0
- data/docs/technical/reports.md +57 -0
- data/docs/technical/rules-and-components.md +61 -0
- data/docs/technical/runner.md +158 -0
- data/docs/technical/specs.md +62 -0
- data/docs/technical/state.md +60 -0
- data/lib/plai_testr/cli.rb +1 -1
- data/lib/plai_testr/examples.rb +37 -0
- data/lib/plai_testr/generator.rb +3 -2
- data/lib/plai_testr/reporter/botrun_report.rb +1 -1
- data/lib/plai_testr/reporter/duration.rb +12 -0
- data/lib/plai_testr/reporter/report_builder.rb +1 -0
- data/lib/plai_testr/session/runner.rb +3 -1
- data/lib/plai_testr/state/game_state.rb +1 -1
- data/lib/plai_testr/version.rb +1 -1
- data/lib/plai_testr.rb +2 -0
- data/templates/app/AGENTS.md.erb +1 -1
- data/templates/app/README.md.erb +3 -3
- data/templates/examples/gin_rummy/data/components/cards.csv +53 -0
- data/templates/examples/gin_rummy/data/rules.json +1 -64
- metadata +18 -3
- /data/docs/{frame_queue.md → technical/frame-queue.md} +0 -0
- /data/docs/{referee.md → technical/referee.md} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b196ea99733867443eb84edef59110ec4eed9f507bc7bc740b1f7077ceae9ce5
|
|
4
|
+
data.tar.gz: 6ac4777ce7f000d856f4309baad759720152b72f50d2e0e45e83f4c7ba369a11
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 13b9b106fb45f6c7bf41b16a4e3ed0fe668507999b6cc838b5a7b7aaad5eacf34f8f3dff5ce2ca21b7235ea695ee2cba266accb2fb9d5a21c5cfc85a09c09ec5
|
|
7
|
+
data.tar.gz: 8cd8ffadd8dfcb2e2c34764e255cc62b667b73471a2843479cb1cfc82e8d70e3e38a62e75c622aebce4ab86ef3d6b3d6626c17a6cf4bae1b213946b268f5d67c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
16
|
-
plai_testr new my_playtests
|
|
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
|
|
21
|
-
bundle exec plai_testr gin_rummy play # one model game (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- `
|
|
16
|
-
- `docs/
|
|
17
|
-
- `docs/
|
|
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
|
-
|
|
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`
|
|
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).
|