Getting Started
Install the rmt CLI, write your first blueprint, and prove it with the check, test and run loop.
After reading this guide, you will know:
- How to install the
rmtCLI and give it an API key. - How to write a small blueprint with a table, a lookup and two formulas.
- How to find problems with
rmt check. - How to prove a blueprint against worked examples with
rmt test. - How to run it on new facts with
rmt run, and see how every value was reached.
Installation
The rmt CLI comes with the reason_me_this gem and works against your workspace on
https://reason-me-this.com. Install it, then give it an API key from
https://reason-me-this.com/app/api_keys:
gem install reason_me_this
export RMT_API_KEY=...
rmt guide # how to build a blueprint
rmt describe # step types, input types, formula functions
It needs Ruby 3.4 or later. Use bundle exec rmt inside a project that bundles reason_me_this.
To run blueprints in your own process instead, see Running the Engine in a Rails App.
The loop
Building a blueprint is a short loop, and the CLI is shaped around it:
- Read the source (a rulebook chapter, a price list, a policy) and list the outputs it asks for and the facts they depend on.
- Write worked examples first, in a cases file, from figures in the source where it gives them.
- Write the blueprint.
rmt check BLUEPRINTuntil it prints no problems.rmt test BLUEPRINT CASESuntil every case passes. Change the blueprint, not the cases, unless a case was worked out wrongly.rmt run BLUEPRINTon new facts. Add--tracewhen a value surprises you.
Your First Blueprint
A blow in a fight: look up the weapon’s damage dice, cap the roll at the most those dice can roll, take off armour, and work out hit points after.
Write the worked examples first, in blow_cases.yml. Each case has a name, the facts, and the outputs it
expects. Say in a comment how the figure was worked out:
- name: a club blow through a leather jacket
# 6 rolled on 1D8, less 1 armour = 5; 12 - 5 = 7
facts: {weapon: club, rolled: 6, armour: 1, hp: 12}
expect: {dealt: 5, hp_after: 7}
- name: a knife cannot deal more than its dice allow
# 6 is more than 1D4 can roll, so 4; less 1 armour = 3; 12 - 3 = 9
facts: {weapon: knife, rolled: 6, armour: 1, hp: 12}
expect: {dealt: 3, hp_after: 9}
- name: armour can stop a blow entirely
facts: {weapon: knife, rolled: 1, armour: 2, hp: 12}
expect: {dealt: 0, hp_after: 12}
Then the blueprint, blow.yml:
tables:
weapons:
- {weapon: knife, damage: 1D4}
- {weapon: club, damage: 1D8}
calculations:
blow:
inputs:
weapon: {type: string, one_of: {table: weapons, column: weapon}}
rolled: {type: number}
armour: {type: number}
hp: {type: number}
steps:
- name: dice
lookup: {table: weapons, where: {weapon: weapon}, value: damage}
- name: dealt
formula: MAX(0, MIN(rolled, DICE_MAX(dice)) - armour)
- name: hp_after
formula: MAX(0, hp - dealt)
outputs: [dealt, hp_after]
tablesholds data the calculation reads. A table is a list of rows.calculationsholds one or more calculations. The first is the onerunandtestuse by default.inputsare typed.one_ofnames a table column, soweaponmust beknifeorclub.stepsrun in order. Each has a snake_casenameand one step type: here alookupand twoformulas. A step can use the inputs, the tables and any earlier step by name.outputslists the steps the calculation hands back.
Checking It
check lists problems in plain words, before anything runs:
$ rmt check blow.yml
✓ No problems
Here is the same blueprint with three mistakes in it: a type that does not exist, armor spelt the
American way, and the hp input left out:
$ rmt check broken.yml
✗ 3 problems
• blow: input weapon has type text; input types are number, string, date, boolean, list
• blow: step dealt uses armor, which is not an input or an earlier step
• blow: step hp_after uses hp, which is not an input or an earlier step
$ echo $?
2
Testing It
test runs every case and compares the outputs each one expects:
$ rmt test blow.yml blow_cases.yml
✓ 3 cases passed
When a case fails, it says which outputs differ and what they were:
$ rmt test blow.yml failing_cases.yml
✗ 1 of 3 cases failed
✗ a knife cannot deal more than its dice allow
dealt: expected 4, got 3
hp_after: expected 8, got 9
$ echo $?
3
Here the case was wrong: a knife’s 1D4 can never deal 4 through 1 armour. Fix the case when the case is wrong, and the blueprint when the blueprint is.
Running It
run reads facts as JSON, from --facts FILE or standard input, and prints the outputs:
$ echo '{"weapon": "club", "rolled": 6, "armour": 1, "hp": 12}' > blow_facts.json
$ rmt run blow.yml --facts blow_facts.json
dealt 5
hp_after 7
Facts are checked against the inputs before anything runs. Every problem is listed at once:
$ echo '{"weapon": "axe", "rolled": "six", "hp": 12, "luck": 3}' | rmt run blow.yml
✗ 4 problems
• fact weapon should be one of knife, club, not "axe"
• fact rolled should be a number, not "six"
• fact luck is not an input of blow; its inputs are weapon, rolled, armour, hp
• facts need armour
$ echo $?
1
Seeing how it was worked out
--trace shows every step’s value and how it was reached: the formula, the table row, or each rule with
whether it matched:
$ rmt run blow.yml --facts blow_facts.json --trace
dealt 5
hp_after 7
How it was worked out
dice 1D8 row weapon club, damage 1D8
dealt 5 MAX(0, MIN(rolled, DICE_MAX(dice)) - armour)
hp_after 7 MAX(0, hp - dealt)
Text at a Terminal, JSON Everywhere Else
At a terminal the CLI answers in text. Anywhere else (a pipe, a file, an agent) it answers in JSON, and every number is an exact string:
$ rmt run blow.yml --facts blow_facts.json | cat
{
"dealt": "5",
"hp_after": "7"
}
--json and --text choose either way. Together with the exit codes (0 success, 1 the run failed or the
facts do not match, 2 the blueprint has problems, 3 a case failed), that is what lets an agent build a
blueprint on its own: it reads the JSON, fixes what it says, and loops until test exits 0. See
CLI Commands and Exit Codes.
Running It from Ruby
Push and publish the blueprint, then call it from any Ruby app with the reason_me_this gem:
rmt push blow blow.yml blow_cases.yml && rmt publish blow
require 'reason_me_this'
ReasonMeThis.configure do |config|
config.url = 'https://reason-me-this.com/api'
config.api_key = ENV['RMT_API_KEY']
end
answer = ReasonMeThis.run('blow', {weapon: 'club', rolled: 6, armour: 1, hp: 12})
answer.dealt # => 5
answer.hp_after # => 7
Numbers come back exact, as BigDecimal or Integer. See
Exact Numbers, Dates and Errors and
The Hosted Service.
Next Steps
- Blueprints and Inputs - Tables, calculations, typed inputs, defaults and
one_of. - Step Types - Formula, rules, lookup, run, repeat, sum, group and compare.
- Worked Examples - Everything a cases file can say.
- Shipped Blueprints - Real rule sets to read and build on.