Running over Lists
Run a calculation for every row of a list, tell each row its place, carry state from one row to the next, and let rows look back at the ones before.
After reading this guide, you will know:
- What each row of a run over a list sees, and what it hands back.
- How
positiontells a row its place. - How
carrymoves state from row to row, andstartsets where it begins. - How
earlierlets a row look back at the rows already worked out. - How
update: truechanges a few columns of every row. - How rows that need to see each other get the whole list.
What a Row Sees
- name: log
run: {calculation: sighting, over: losses}
A run over a list runs the calculation once for each row of a list input or table. Each row sees the
parent’s values plus its own columns, and never the list it belongs to, so a calculation can run over
sub-items of its own shape. Each row hands back only the calculation’s outputs. Later steps read the
result as a list of rows, with sum, group, or FILTER, PLUCK, ANY and REDUCE in a formula.
A row’s columns are checked against the calculation’s inputs as it runs, so a bad row fails the run and says where:
✗ round > ranked: rank needs hp to be a number, not "eleven"
A Ledger
A sanity ledger: each sighting of a creature costs sanity, starting from what the last one left, and a creature already seen costs at most 8 in all.
calculations:
ledger:
inputs:
sanity: {type: number}
losses: {type: list}
steps:
- name: log
run: {calculation: sighting, over: losses, position: roll, carry: {sanity: sanity_after}, earlier: before}
- name: sanity_now
formula: 'REDUCE(log, last, e, e.sanity_after, sanity)'
- name: shaken
formula: 'ANY(log, e, e.shaken)'
outputs: [log, sanity_now, shaken]
sighting:
inputs:
roll: {type: number}
sanity: {type: number}
creature: {type: string}
loss: {type: number}
before: {type: list}
steps:
- name: seen_before
formula: 'SUM(PLUCK(FILTER(before, b, b.creature = creature), loss_taken))'
- name: loss_taken
formula: 'MAX(0, MIN(loss, 8 - seen_before))'
- name: sanity_after
formula: 'MAX(0, sanity - loss_taken)'
- name: shaken
formula: loss_taken >= 5
outputs: [roll, creature, loss_taken, sanity_after, shaken]
$ cat ledger_facts.json
{"sanity": 60, "losses": [{"creature": "ghoul", "loss": 6}, {"creature": "deep_one", "loss": 4}, {"creature": "ghoul", "loss": 6}]}
$ rmt run ledger.yml --facts ledger_facts.json
log
roll creature loss_taken sanity_after shaken
1 ghoul 6 54 true
2 deep_one 4 50 false
3 ghoul 2 48 false
sanity_now 48
shaken true
REDUCE(log, last, e, e.sanity_after, sanity) is the last row’s sanity_after, or the starting sanity
when there were no sightings.
position
position: roll gives each row its place in the list as roll, 1 for the first. Declare it as an
input of the row calculation.
carry
carry: {sanity: sanity_after} hands each row an output of the row before it. The first row sees
sanity as it is before the run (60, from the facts). Each later row sees the previous row’s
sanity_after as sanity. Carry as many values as you need, each an output of the row calculation:
carry: {sanity: sanity_after, mythos: mythos_after}.
earlier
earlier: before hands each row the rows already worked out, with their outputs, as before. That is
how the third row knows the ghoul already cost 6:
row 3
seen_before 6 SUM(PLUCK(FILTER(before, b, b.creature = creature), loss_taken))
loss_taken 2 MAX(0, MIN(loss, 8 - seen_before))
sanity_after 48 MAX(0, sanity - loss_taken)
Use carry when a row needs the last value of something, earlier when it needs to ask about any of
the rows before it, and SCAN in a formula when nothing inside the rows needs the running total.
Changing Some Columns of Every Row
A round of a fight: rank the fighters by hit points, then land each blow on the line-up as the last blow left it.
calculations:
round:
inputs:
fighters: {type: list}
blows: {type: list}
steps:
- name: line_up
formula: fighters
- name: ranked
run: {calculation: rank, over: fighters, update: true}
- name: struck
run: {calculation: strike, over: blows, carry: {standing: standing_after}, start: {standing: ranked}}
- name: after
formula: 'REDUCE(struck, last, s, s.standing_after, ranked)'
outputs: [ranked, after]
rank:
inputs:
hp: {type: number}
line_up: {type: list}
steps:
- name: place
formula: '1 + COUNT(FILTER(line_up, f, f.hp > hp))'
outputs: [place]
strike:
inputs:
target: {type: string}
damage: {type: number}
standing: {type: list}
steps:
- name: standing_after
run: {calculation: take, over: standing, update: true}
outputs: [standing_after]
take:
inputs:
name: {type: string}
hp: {type: number}
target: {type: string}
damage: {type: number}
steps:
- name: hp
formula: 'IF(name = target, MAX(0, hp - damage), hp)'
outputs: [hp]
$ echo '{"fighters": [{"name": "Ada", "hp": 11}, {"name": "ghoul", "hp": 13}], "blows": [{"target": "ghoul", "damage": 5}, {"target": "ghoul", "damage": 9}]}' | rmt run fight.yml
ranked
name hp place
Ada 11 2
ghoul 13 1
after
name hp place
Ada 11 2
ghoul 0 1
update
With update: true each row keeps its own columns, and the calculation’s outputs replace or add to
them. rank adds place; take replaces hp. So a calculation names only the columns it reads or
changes, not every column a row has.
Rows that need to see each other
A row never sees the list it is run over. When rows must compare themselves with the others, copy the
list in a formula step first, - {name: line_up, formula: fighters}, and declare line_up as an input
of the row calculation. Rows can then ask COUNT(FILTER(line_up, f, f.hp > hp)).
start
start: {standing: ranked} sets what the first row sees as standing, from a formula, instead of what
the values held before the run. It is how the stages of a round carry the same line-up one after
another: shots, then blows, then CON rolls, each starting where the last left it.
with on Each Row
with works on a run over a list too. It is worked out for each row and sees that row’s columns, so
with: {loader: name} gives every row its own name as loader. To hand the rows a value of the
calculation running them instead, name it in a step first: - {name: loader, formula: name}.
What check Reports
check reports a carry of an output the calculation does not have, a carry into a value the first row
has nothing for and no start, a carry and a with for the same value, and carry, start, earlier
or update on a run that is not over a list.
Next Steps
- Repeating a Calculation - Rows that make themselves, until something happens.
- Formulas and Functions -
FILTER,PLUCK,REDUCE,SCANandSORT. - Shipped Blueprints -
coc_7e_combatcarries a whole fight this way.