Dates and Dice
Count days, move dates and read weekdays in formulas, and read, bound and roll dice notation the same way every time.
After reading this guide, you will know:
- How dates are written in blueprints, cases and facts.
- How to add days, count days between dates and read the weekday.
- How to find the lowest and highest a set of dice can roll.
- How to roll dice in a blueprint so a run replays exactly.
- When to roll in the blueprint, and when to pass the dice in as facts.
Dates
Dates are written YYYY-MM-DD, everywhere: in tables, in cases, and in JSON facts. They load as dates, and come back from the CLI as the same text.
| Formula | Gives |
|---|---|
date + 2 |
the date two days later |
date - 14 |
the date fourteen days before |
leave - arrive |
the whole number of days between two dates |
WEEKDAY(date) |
0 for Sunday to 6 for Saturday |
date >= from, date = other |
comparisons |
calculations:
booking:
inputs:
arrive: {type: date}
leave: {type: date}
notice_days: {type: number, default: 14}
steps:
- {name: nights, formula: leave - arrive}
- {name: cancel_by, formula: arrive - notice_days}
- {name: arrives_on, formula: 'SWITCH(WEEKDAY(arrive), 0, "Sunday", 6, "Saturday", "a weekday")'}
- {name: first_night_weekend, formula: WEEKDAY(arrive) = 5 OR WEEKDAY(arrive) = 6}
outputs: [nights, cancel_by, arrives_on, first_night_weekend]
$ echo '{"arrive": "2026-07-03", "leave": "2026-07-06"}' | rmt run dates.yml
nights 3
cancel_by 2026-06-19
arrives_on a weekday
first_night_weekend true
A period that counts both its first and last day is period_to - period_from + 1, as the shipped
electricity bill counts its supply days. To walk the days of a period, repeat with times: days and
position: day, and work out each date as period_from + day - 1.
For rates that change on a date, see Tables and Lookups.
Any text fact written YYYY-MM-DD is read as a date. A string input given "2026-07-01" is refused as
not text, so give such values another shape, or declare the input as a date.
Times of day are not dates. Write an hour as a number (14.5 for 14:30), as the electricity bill does.
For splitting time at clock hours, the licensed engine has interval slicing and custom steps (see
Calculations in Ruby).
Dice
Dice are written as text such as 1D8, 2D6+3 or 1D8+1D4+2. Terms are added or taken away; a plain
number is a modifier.
| Formula | Gives |
|---|---|
DICE_MIN("1D8+1D4+2") |
the lowest total, 4 |
DICE_MAX("1D8+1D4+2") |
the highest total, 14 |
ROLL(dice, seed, key, ...) |
a roll, the same every time for the same seed and keys |
DICE_MAX is how the getting started example caps a roll at what a weapon can do, and how a Call of
Cthulhu extreme hit deals the most its weapon allows.
calculations:
dice:
inputs:
weapon: {type: string}
bonus: {type: string}
seed: {type: number}
steps:
- {name: lowest, formula: 'DICE_MIN(CONCAT(weapon, "+", bonus))'}
- {name: highest, formula: 'DICE_MAX(CONCAT(weapon, "+", bonus))'}
- {name: rolled, formula: 'ROLL(weapon, seed, 1, "Ada", "damage")'}
- {name: again, formula: 'ROLL(weapon, seed, 1, "Ada", "damage")'}
- {name: next_round, formula: 'ROLL(weapon, seed, 2, "Ada", "damage")'}
outputs: [lowest, highest, rolled, again, next_round]
$ echo '{"weapon": "1D8", "bonus": "1D4", "seed": 42}' | rmt run dice.yml
lowest 2
highest 12
rolled 6
again 6
next_round 2
$ echo '{"weapon": "1D8", "bonus": "1D4", "seed": 43}' | rmt run dice.yml
lowest 2
highest 12
rolled 3
again 3
next_round 7
Dice that cannot be read fail the run and say how to write them:
✗ dice > lowest: cannot read dice "a big stick+1D4"; write them like 1D8+1D4+2
Rolling the Same Way Every Time
ROLL is not random. It hashes the dice, the seed and the keys, so the same seed and keys always give
the same total. That is what lets a simulated fight replay exactly, and its cases pin the outcome.
Give each roll its own keys, so two rolls in the same round do not come out the same. The shipped
coc_7e_duel keys every roll by the seed, the round, who is rolling and what for:
- {name: attack_units, formula: 'ROLL("1D10", seed, round, name, "attack units") - 1'}
- {name: weapon_roll, formula: 'ROLL(weapon, seed, round, name, "weapon")'}
Above, rolled and again have the same keys and always match; next_round has another round and
does not.
Rolling at the Table
When people roll real dice, do not roll in the blueprint. Pass the totals in as facts, and let the
blueprint say what to roll. The shipped coc_7e_percentile takes the units and tens that were
rolled, and outputs dice_expected, how many tens dice the caller should roll. An app at the
table can ask the blueprint what to roll, roll it (or take what the players rolled), and pass the dice
in exactly as typed ones would be, so a blueprint gives the same answer for the same dice
either way.
Next Steps
- Repeating a Calculation - Work out odds over many seeded runs.
- Exact Numbers, Dates and Errors - How values load and come back.
- Formula Functions - Every function in one place.