tinhorn 0.1.1

A fancy dice roller: shake the dice, watch em' fly, hear it bounce.
tinhorn-0.1.1 is not a library.

tinhorn

Step right up: a terminal dice roller with a genuine tin-cup shake. Type your dice in the usual notation, rattle the cup, and let 'em fly — real physics on every bounce, real racket off every wall, and the dice land how they land. Nothing up these sleeves, friend: seed the roll (--seed 42) and watch the very same throw land twice.

tinhorn: typing 2d20kh1 vs 15, shaking the cup, releasing at the peak, and landing a natural 20 — SUCCESS by 5

Why the name? A tinhorn is a small-time gambler, named for the tin shaker chuck-a-luck dealers rattled their dice in — small stakes, big noise. That shaker is this program: all rattle, honest dice.

And what'll it cost you to see all this? Not one thin dime:

  • A real physics arena. Six silhouettes — d4 triangle to d20 hexagon — tossed, bounced, knocked together, and rolled off each other's backs at sixty frames a second, painted by ratatui.
  • The Throw. Shake the cup, catch the meter at its peak, and put some arm into it. Power shapes the launch and never the dice — there's a test that swears to it.
  • Stakes. Call your number — d20+5 vs 15 — and the arena hands down the verdict, margin and all. The stats pane quotes you fair odds before you take the bet.
  • Sound from thin air. Every click, knock, and thunk synthesized live from the very impact that made it. No samples anywhere on the premises.
  • The fancy notation. Advantage, drop-the-lowest, exploding dice, multipliers — the works.
  • One-shot mode for scripts and pipes: asks no questions, prints a number, gets out of the way. Even the exit code carries the verdict.

House rules: MIT or Apache-2.0, your pick — the whole works sits on the table for inspection.

Install

You'll need a Rust toolchain. On Linux, the sound needs the ALSA headers to build (macOS and Windows need nothing extra):

sudo apt install libasound2-dev pkg-config   # Debian/Ubuntu
sudo dnf install alsa-lib-devel              # Fedora

Then install from crates.io:

cargo install tinhorn

For the latest unreleased code, install from the repository instead:

cargo install --git https://github.com/puradox/tinhorn

If your fingers insist on the old ways: alias roll=tinhorn.

Run

tinhorn                  # start empty, type an expression
tinhorn 3d6              # roll 3d6 the moment it opens
tinhorn "d6+d8"          # quote anything with shell-special characters
tinhorn --mute           # start silent (Ctrl-Q toggles at runtime)

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 afplay trips it. tinhorn never records and opens the default output device only, so deny it freely; --mute skips 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
? 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
type / Backspace edit the dice expression
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).

?, Ctrl-H, and Ctrl-S open the notation help, roll history, and statistics panes; they float over the animation and close on Esc.

Dice notation

A roll is a sequence of dice terms and optional flat modifiers, in any combination. Terms can be separated by +, ,, whitespace, or simply written next to each other.

Input Meaning
3d6 three six-sided dice
d% percentile — shorthand for d100
d6+d8 one d6 and one d8
d6d10 adjacency works as a separator
d6,d12 commas work too
2d20-1 two d20 with a −1 modifier
d20 + 5 whitespace is ignored
d20+5 vs 15 staked: succeed on a total ≥ 15

d6 means 1d6. Sizes are capped (≤ 60 dice, ≤ 1000 sides) so a fat-fingered 999d99999 can't wedge the renderer. A vs target must come last — d20 vs 4d6 is an error, not a surprise — and there's at most one per roll.

Per-die modifiers

A dice term can carry modifiers written right after the dN. They apply in pool order — explode → keep/drop → multiply — and can be stacked.

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
3d6! exploding — a max face rolls another die (repeats)
d10!>8 explode on any face > 8 instead of just the max
d6!=6 explode on exactly 6 (>/</= all work)
4d6*2 multiply this term's kept sum by 2
4d6!kh3*2 stack them: explode, keep the best 3, then double

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. 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. 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:

tinhorn -p 3d6              # 13            (just the total)
tinhorn 3d6 | cat           # 13            (piped stdout → one-shot automatically)
total=$(tinhorn -p 2d20kh1) # capture it in a variable
tinhorn --seed 42 4d6dl1    # reproducible: the same seed always rolls the same dice
tinhorn -v 4d6dl1+2         # a full breakdown (dropped dice in [brackets])
tinhorn --json 2d20kh1+3    # machine-readable for jq & friends

tinhorn -p d20+4 vs 14 && echo "the potion works"   # the exit code IS the check

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.

$ tinhorn -v --seed 1 "d20+5 vs 15"
  d20        17  = 17
  modifier   +5
  total      22
  vs 15      success by 7

The --json output carries every die and its flags, the per-term subtotals, the flat modifier, the total, and — when staked — target, success, and margin.

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. Built in Rust; the dice, the physics, and every sound are made from scratch on the premises.

License

Licensed under either of

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.