A tinhorn is a small-time gambler, named for the tin shaker chuck-a-luck dealers rattled their dice in.
Step right up: a terminal dice roller with a genuine tin-cup shake — the
dice land how they land. Nothing up these sleeves, friend: seed the roll
(--seed 42) and watch the very same throw land twice.

And what'll it cost you to see all this? Not one thin dime!
- A real physics arena. Dice are tossed, bounced, knocked together, and rolled off each other's backs.
- Your throw. Shake the cup and catch the meter at its peak. Put some arm into it!
- Set the stakes. Call your number —
d20+5 vs 15— and the arena hands down the verdict, margin and all. Playing it low?d20 < 10flips the bet to roll-under. The stats pane quotes you fair odds before you take it. - Sound from thin air. Every click, knock, and thunk synthesized live from the very impact that made it. No samples anywhere on the premises.
- Fancy notation. Advantage, drop-the-lowest, exploding dice, multipliers — the works.
- One-shot mode for quick gambles.
Install
You'll need a Rust toolchain. On Linux, the sound needs the ALSA headers to build (macOS and Windows need nothing extra):
Then install from crates.io:
If your fingers insist on the old ways: alias roll=tinhorn.
Run
macOS asked about the microphone? Recent macOS raises that prompt for any app playing audio through an output device that also carries mic inputs (a USB interface, a headset) — even Apple's
afplaytrips it. tinhorn never records and opens the default output device only, so deny it freely;--muteskips audio entirely and never asks.
Keys
| Key | Action |
|---|---|
Enter |
roll, per the mode (shake: press again to throw) |
Tab |
cycle the mode — shake → roll → insta |
type / Backspace |
edit the dice expression |
← → (Home/End) |
move the caret in the expression (jump to ends) |
↑ ↓ |
scroll an open pane that's taller than the screen |
? |
toggle the dice-notation help overlay |
Ctrl-H |
toggle the roll-history pane |
Ctrl-S |
toggle the statistics pane |
Ctrl-Q |
mute / unmute — Q for quiet |
Esc / Ctrl-C |
quit (Esc closes a pane or shake first) |
Three roll modes cycle on Tab: shake (drop into the cup and catch the
power meter), roll (dice tumble straight in), and insta (landed and
tallied at once).
Dice notation
A roll is a sequence of dice terms and optional flat modifiers,
separated by +, ,, whitespace, or simply written next to each other. A term
can carry modifiers (keep/drop, explode, multiply) written right after its
dN; these apply in pool order — explode → keep/drop → multiply — and
stack. Add stakes to check the total against a target.
The basics
| Input | Meaning |
|---|---|
3d6 |
three six-sided dice |
d20 |
one die — d6 means 1d6 |
d% |
percentile — shorthand for d100 |
d6+d8 |
one d6 and one d8, summed |
2d20-1 |
dice plus a flat +/− modifier |
Sizes are capped (≤ 60 dice, ≤ 1000 sides) so a fat-fingered 999d99999 can't
wedge the renderer.
Keep / drop
| Input | Meaning |
|---|---|
2d20kh1 |
advantage — roll two d20, keep the highest 1 |
2d20kl1 |
disadvantage — keep the lowest 1 |
4d6dl1 |
drop the lowest 1 (the classic ability-score roll) |
4d6dh1 |
drop the highest 1 |
kh/kl/dh/dl default to 1 (2d20kh = 2d20kh1) and clamp to the pool
size. Dropped dice are still thrown and bounce around — you watch advantage
discard the lower d20 — but they're rendered dimmed and left out of the total.
Stakes
Call a target and the arena hands down a verdict — margin and all. At most one
per roll, and it must come last: d20 > 4d6 is an error, not a surprise.
| Input | Meaning |
|---|---|
d20+5 > 15 |
meet or beat — succeed on a total ≥ 15 |
d20 vs 15 |
the same; vs is the word alias for > |
d20 < 10 |
roll-under — succeed on a total ≤ 10 |
Both comparisons are inclusive: you win on the number.
Exploding
| Input | Meaning |
|---|---|
3d6! |
a max face rolls another die (repeats) |
d10!>8 |
explode on any face > 8 (< and = too) |
Exploding plays out live: a die that settles on a qualifying face drops one
more die into the arena, which can explode in turn — capped at 40 extra dice
per term so d2! can't grow without bound.
Multiply
| Input | Meaning |
|---|---|
4d6*2 |
multiply this term's kept sum by 2 |
4d6!kh3*2 |
stack them: explode, keep the best 3, then double |
A multiplier binds to its own term: in 3d6*2 + d8 only the d6 sum is doubled.
Scripting (one-shot mode)
With an output flag — or whenever stdout isn't a terminal — tinhorn skips
the animation, evaluates the roll once, prints a result, and exits, so it
drops straight into scripts and pipelines:
| total= # capture it in a variable
&&
Under -p/-v, a staked roll exits 0 on success and 1 on failure, so scripts
branch on the check itself; --json and piped output always exit 0, and a
parse error goes to stderr and exits 2.
The --json output carries every die and its flags, the per-term subtotals,
the flat modifier, the total, and — when staked — target, goal
(over or under), success, and margin (how far the check was made or
missed by, whichever way the stake runs).
Contributing
Want a look behind the table? The design notes, the test suite, and the house rules all live in CONTRIBUTING.md — pull up a chair.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.