Chaining with Imports and Runs
Build large rule sets as a chain of small calculations, each with its own blueprint and cases, joined with imports and run steps.
After reading this guide, you will know:
- How to import another blueprint by path or by name.
- How a
runstep feeds a calculation and reads back its outputs. - How
withruns one calculation twice on different values. - What
checkreports about a broken link. - How the shipped Call of Cthulhu blueprints chain together.
Why Chain
Big rule sets are a chain of small calculations, each with its own blueprint and cases, rather than one that knows everything. A general part (a postage table, a skill check, a damage roll) is written and tested once and used anywhere. Each link hands back only its declared outputs, so a chain can only depend on what a link promises.
Imports
imports: lists other blueprints. Their calculations and tables join this blueprint’s own.
A postage table, with its own cases:
# postage.yml
tables:
postage:
- {below: 0.5, price: 3.50}
- {below: 2, price: 7.95}
- {below: 5, price: 12.40}
- {price: 18.00}
calculations:
postage:
inputs:
kg: {type: number}
steps:
- name: price
lookup: {table: postage, where: {}, value: price, band: {by: kg, below: below}}
outputs: [price]
$ rmt test postage.yml postage_cases.yml
✓ 3 cases passed
A parcel that imports it:
# parcel.yml
imports: [postage.yml]
calculations:
parcel:
inputs:
kg: {type: number}
insured_value: {type: number}
steps:
- name: posted
run: {calculation: postage}
- name: insurance
formula: 'IF(insured_value > 100, ROUND((insured_value - 100) * 0.02, 2), 0)'
- name: total
formula: posted.price + insurance
outputs: [total]
An import that ends in .yml or .yaml or contains a / is a path, relative to the importing file. Any
other import is a name: locally, a shipped blueprint; on a service, one of the workspace’s definitions,
with @N for a published version or @draft (see The Hosted Service):
imports: [coc_7e_percentile] # a shipped blueprint
imports: [rates@3, tariffs@7] # on a service: pin what an application depends on
A name defined both here and in an import is a problem, and so is an import that imports this blueprint back. Give each part its own cases file and test it on its own.
On the service, an import names one of the workspace’s definitions, or else a shipped blueprint. With the licensed engine, a host app resolves names to its own definitions (see Calculations in Ruby).
Running a Calculation Once
A run without over runs the calculation once, with the values so far:
- name: posted
run: {calculation: postage}
postage needs kg, and the parcel has an input kg, so it is passed on. A calculation that another
runs declares everything it reads as an input, including what its parent provides.
Read its outputs as name.output, in formulas and in rule conditions:
- name: total
formula: posted.price + insurance
The trace keeps the nested calculation’s own steps:
{
"step": "posted",
"value": {
"price": "7.95"
},
"steps": [
{
"step": "price",
"value": "7.95",
"row": {
"below": "2",
"price": "7.95"
}
}
]
}
The Same Calculation on Different Values
with gives the calculation its inputs as formulas, so one calculation can run twice in a step list:
split:
inputs:
heavy_kg: {type: number}
light_kg: {type: number}
steps:
- name: heavy
run: {calculation: postage, with: {kg: heavy_kg}}
- name: light
run: {calculation: postage, with: {kg: light_kg}}
- name: both
formula: heavy.price + light.price
- name: together
run: {calculation: postage, with: {kg: heavy_kg + light_kg}}
- name: cheaper
formula: 'IF(together.price < both, "together", "apart")'
outputs: [both, cheaper]
$ echo '{"heavy_kg": 4.5, "light_kg": 0.4}' | rmt run parcel.yml --calculation split
both 15.9
cheaper together
Each with is a formula, so text needs its double quotes and a plain number is written as one. This
runs the shipped Call of Cthulhu skill check twice, once for each side:
imports: [coc_7e_percentile]
calculations:
sneak:
inputs:
stealth: {type: number}
listen: {type: number}
sneak_units: {type: number}
sneak_tens: {type: list}
hear_units: {type: number}
hear_tens: {type: list}
steps:
- name: sneaking
run:
calculation: skill_check
with: {skill: stealth, difficulty: '"regular"', units: sneak_units, tens: sneak_tens, bonus_dice: '0', penalty_dice: '0'}
- name: hearing
run:
calculation: skill_check
with: {skill: listen, difficulty: '"regular"', units: hear_units, tens: hear_tens, bonus_dice: '0', penalty_dice: '0'}
- name: unheard
formula: sneaking.passes AND NOT(hearing.passes)
outputs: [unheard]
- name: the investigator sneaks past a guard who fails to hear
# 35 against Stealth 50 passes; 72 against Listen 40 fails
facts: {stealth: 50, listen: 40, sneak_units: 5, sneak_tens: [3], hear_units: 2, hear_tens: [7]}
expect: {unheard: true}
$ rmt test skill.yml skill_cases.yml
✓ 1 case passed
When a Link Breaks
check reports a broken link before anything runs: a run whose inputs nothing provides by that step, a
name.output the run does not output, an output read from a run over a list, and a calculation that
does not exist:
imports: [postage.yml, missing.yml]
tables:
postage:
- {price: 1}
calculations:
order:
inputs:
parcels: {type: list}
steps:
- name: posted
run: {calculation: postage}
- name: each
run: {calculation: postage, over: parcels}
- name: shown
formula: posted.cost + each.price
- name: quoted
run: {calculation: quote, with: {weight: 2}}
outputs: [shown]
$ rmt check broken_chain.yml
✗ 6 problems
• table postage is defined here and in postage.yml
• imports missing.yml, which does not exist
• order: step posted runs postage, which needs kg; add it as an input or an earlier step, or give it with with
• order: step shown uses posted.cost, but posted only outputs price
• order: step shown uses each.price, but each is a list of rows; use sum, or PLUCK, FILTER or ANY over it
• order: step quoted runs quote, which is not a calculation in this blueprint
A Real Chain
The shipped coc_7e_combat blueprint plays one exchange of blows by running three smaller calculations
it imports, each tested on its own:
imports: [../coc_7e_encounters/blueprint.yml] # which imports coc_7e_percentile
calculations:
exchange:
steps:
- name: attack
run:
calculation: skill_check # from coc_7e_percentile
with: {skill: striker.fighting, difficulty: '"regular"', units: attack_units, tens: attack_tens,
bonus_dice: 'IF(outnumbered, 1, 0)', penalty_dice: "0"}
- name: defence
run: {calculation: skill_check, with: {skill: 'IF(response = "dodge", defender.dodge, defender.fighting)', ...}}
- name: outcome
run: {calculation: melee, with: {attacker_level: attack.success_level, defender_level: defence.success_level}}
- name: hit_by
formula: 'IF(acted, outcome.hit_by, "nobody")' # read a run's outputs as name.output
That is an excerpt of the shipped coc_7e_combat blueprint.
Next Steps
- Running over Lists - Run a calculation for each row, carrying state from one to the next.
- Repeating a Calculation - Run until something happens.
- The Hosted Service - Chain a workspace’s published definitions by name.