dead-simple-dice
v0.2.7
Published
A tiny, dependency-free CLI dice roller (dsd) for tabletop, shell, scripts, and pipes.
Downloads
576
Readme
What it is
Dead-Simple-Dice is a small Node.js CLI that installs the command dsd.
It rolls simple dice notation and gets out of the way. The default output is deliberately boring so it works in shells, scripts, pipes, and tabletop notes:
dsd d20
# 17
# four six-sided dice
dsd 4d6
# 3 5 1 6
# pipe notation in
echo d20 | dsd
# 12No ads. No accounts. No network calls. No telemetry. No runtime dependencies.
Quick start
git clone https://github.com/M0KA907/Dead-Simple-Dice.git
cd Dead-Simple-Dice
./install.sh
dsd d20
dsd 4d6
echo d20 | dsdFor terminal flair, opt in explicitly:
dsd 4d6 --ui
dsd 4d6 --cyberPiped or redirected output always stays plain, even when display effects are enabled.
At a glance
| Thing | Value |
|---|---|
| Command | dsd |
| Package | dead-simple-dice |
| Version | 0.2.0 |
| Runtime | Node.js ESM |
| Minimum Node | >=18 |
| Runtime deps | 0 |
| Default output | Plain script-safe text |
| Fancy output | Opt-in TUI / cyber modes |
| Random source | Node crypto.randomInt |
| Test runner | Built-in node:test |
| License | MIT |
| Registry status | Not published to npm yet |
Highlights
| Need | What dsd does |
|---|---|
| Roll fast | dsd d20, dsd 4d6, dsd d6 -n 4 |
| Pipe cleanly | Reads stdin line-by-line and prints plain results |
| Stay script-safe | No ANSI, boxes, or animation in non-TTY output |
| Look good in a terminal | Optional --ui and --cyber modes |
| Stay predictable | Small notation, bounded values, clear errors |
| Stay offline | Zero runtime dependencies, no network calls |
Install
Dead-Simple-Dice is currently installed from a local git checkout. Registry installation is future work.
Fresh install
git clone https://github.com/M0KA907/Dead-Simple-Dice.git
cd Dead-Simple-Dice
./install.sh./install.sh delegates to ./scripts/install.sh, which checks Node and npm,
then runs npm ci, npm test, npm link, and verifies dsd --version plus
dsd d20. It fails fast with a useful error, needs no sudo, and does not touch
your shell startup files.
Update an existing checkout
For a local git checkout, pull and relink/retest in one step:
cd Dead-Simple-Dice
./update.sh./update.sh delegates to ./scripts/update.sh. It verifies you are in a git
repo, shows the current branch, then runs git pull --ff-only, npm ci,
npm test, npm link, and re-verifies dsd. It only fast-forwards — it never
merges, rebases, resets, or force-pulls. If your working tree has uncommitted
changes it stops and tells you to commit, stash, or restore them yourself
first; it never auto-stashes or deletes your changes.
If dsd is not found
npm link installs the command into your npm global bin directory. If the link
succeeds but your shell cannot find dsd, add that directory to PATH.
For example, if npm config get prefix prints $HOME/.local/npm, add:
export PATH="$HOME/.local/npm/bin:$PATH"Put that line in ~/.bashrc, ~/.zshrc, or your shell's equivalent startup
file, then open a new terminal or source the file.
Uninstall
./uninstall.sh./uninstall.sh delegates to ./scripts/uninstall.sh, which runs
npm unlink -g dead-simple-dice. This removes the global linked dsd command
but does not delete the repo — remove the cloned directory yourself if you
want the source gone too.
Manual fallback (the underlying commands)
# install
npm ci
npm test
npm link
# update (from inside the checkout)
git pull --ff-only
npm ci
npm test
npm link
# uninstall
npm unlink -g dead-simple-dicenpm link exposes the local checkout as the global dsd command.
These scripts are for local git checkouts, not registry installs.
Use it
dsd <notation> [options]
echo <notation> | dsdCommon rolls
| Command | Result |
|---|---|
| dsd d20 | Roll one twenty-sided die |
| dsd 4d6 | Roll four six-sided dice |
| dsd d6 -n 4 | Roll four six-sided dice |
| dsd d6 --number 4 | Roll four six-sided dice |
| dsd d100 --number 2 | Roll two hundred-sided dice |
| echo d20 \| dsd | Roll notation from stdin |
| printf 'd20\n4d6\n' \| dsd | Roll several stdin lines |
Flags
| Flag | Meaning |
|---|---|
| -n <N> | Number of dice to roll |
| --number <N> | Number of dice to roll |
| -h, --help | Print help |
| -V, --version | Print version |
Display presets
| Flag | Meaning |
|---|---|
| --plain | Force plain output; disables fancy/color/animation |
| --ui | Static fancy TUI box |
| --cyber | Fancy TUI + reveal animation + colored border |
| --fast | Faster animation timing |
| --slow | Slower animation timing |
| --no-color | Disable animated border color |
| --ascii | Force the ASCII box theme (no Unicode glyphs) |
| --unicode | Force the Unicode box theme |
Dice notation
Supported notation is intentionally small.
| Notation | Meaning |
|---|---|
| d20 | One twenty-sided die |
| 4d6 | Four six-sided dice |
| d100 | One hundred-sided die |
| d100 --number 2 | Two hundred-sided dice |
Rules:
- Dice count must be positive.
- Dice sides must be positive.
- Count is capped at
100. - Sides are capped at
1,000,000. - The
dis case-insensitive:4D6works. - Leading/trailing whitespace is trimmed.
- Internal notation whitespace is rejected:
d 20is invalid. -n/--numberoverrides a leading count:4d6 -n 2rolls two d6.- Repeated
-n/--numberuses the last value.
Not supported yet:
d%
advantage/disadvantage
exploding dice
keep/drop notation
full dice expressionsOutput format
Default output is plain on purpose.
dsd d20
# 17
dsd 4d6
# 3 5 1 6That means output stays useful in scripts:
roll="$(dsd d20)"
printf 'rolled: %s\n' "$roll"No banners. No JSON by default. No color unless a display mode is explicitly enabled and stdout is a real terminal.
Display modes
Plain text is the default. Fancy output is opt-in.
| Command | Result |
|---|---|
| dsd d20 | Plain default |
| dsd d20 --plain | Force plain output |
| dsd d20 --ui | Static TUI box |
| dsd d20 --cyber | TUI box + reveal animation + colored border |
| dsd d20 --cyber --fast | Faster reveal |
| dsd d20 --cyber --slow | Slower reveal |
| dsd d20 --cyber --no-color | Animated TUI without colored border |
Fancy TUI
╭─ DSD // DEAD-SIMPLE-DICE ─╮
│ notation : 4d6 │
│ result : 3 5 1 6 │
╰───────────────────────────╯ASCII fallback:
+- DSD // DEAD-SIMPLE-DICE -+
| notation : 4d6 |
| result : 3 5 1 6 |
+---------------------------+Cyber reveal
The animation is not a loading bar. It is a left-to-right lock-in that runs
through three phases — ROLLING (churning block/noise placeholders), LOCKING
(real dice revealed one at a time), and RESULT (the full result, held for a
couple of settle frames). For final 4d6 = 3 5 1 6:
ROLLING ▒ ▓ █ ░
LOCKING 3 ▓ █ ░
LOCKING 3 5 █ ░
LOCKING 3 5 1 ░
RESULT 3 5 1 6The dice are rolled once; the placeholder frames are purely visual and never
re-roll. Normal frames are paced at a readable ~140ms each. --fast (~70ms)
uses a shorter delay, and --slow (~220ms) a longer one.
With --cyber, the same reveal plays inside the box, the box gains a phase
row, and the box border pulses through a restrained cyberpunk palette
(cyan, violet, magenta, bright white) as the frames advance. Only the border is
colored, the rolled numbers stay uncolored, and every colored run is reset, so
color never leaks. The final frame always shows the real result. Use
--no-color or DSD_COLOR=0 to keep the fancy animation but disable the
colored border.
Advanced display flags
These are kept for compatibility:
dsd d20 --fancy # alias for --ui
dsd d20 --pretty # alias for --ui
dsd d20 --no-fancy # force fancy box off
dsd d20 --no-pretty # alias for --no-fancy
dsd d20 --animate # plain reveal animation
dsd d20 --no-animate # force animation off--plain wins over display flags and display environment variables. If both --fast and --slow are passed, the last speed flag wins.
Environment toggles
Truthy values: 1, true, yes, on.
DSD_UI=1 # static fancy TUI
DSD_CYBER=1 # fancy + animation + color
DSD_COLOR=0 # disable animated border color
DSD_ASCII=1 # force the ASCII box theme
DSD_SPEED=fast # fast, normal, or slowBackward-compatible env vars:
DSD_FANCY_TUI=1 # static fancy TUI box
DSD_PRETTY=1 # alias for DSD_FANCY_TUI
DSD_ROLL_ANIMATION=1 # reveal animationFalsy values: unset, 0, false, no, off. Unknown values are falsy for on/off toggles.
Terminal rendering safety
The rendering layer is built as a small cell-layout system with strict invariants:
- Effects render only on a real terminal. Piped or redirected stdout is always plain text — no ANSI, color, box, or animation.
- Each animation frame is a complete, measured frame. Every frame re-reads the terminal width, rebuilds the whole frame at that width, moves the cursor up the previous frame's height, clears downward, and writes the new frame, so a mid-animation resize never leaves a smear.
- Every row is clipped and padded to one chosen width. The box width is decided once per frame from the title and the widest content row, clamped to the terminal columns. If content is too wide, the content is clipped with an ellipsis — the border is never pushed past the terminal.
- Borders and text are separate concepts. Content is built and measured as plain "label : value" rows; borders are wrapped around them afterward.
- Color is applied after layout. Visible-width math ignores ANSI escapes, so color can never affect alignment, and every colored segment self-resets.
- Tiny terminals fall back to compact output (below ~28 columns) instead of drawing a broken box.
- Unicode vs ASCII is chosen by
--ascii/--unicode(orDSD_ASCII=1); adumbterminal auto-selects ASCII. Non-TTY output stays plain regardless.
Piping
dsd is built to behave like a boring Unix utility.
echo d20 | dsdprintf 'd20\n4d6\nd100\n' | dsdBehavior:
- CLI args override stdin.
- No args + piped stdin reads stdin.
- Multiple stdin lines produce multiple output lines.
- Blank stdin lines are ignored.
- Empty input exits
1with usage/help. - Normal terminal use should not hang waiting for input.
Non-TTY safety
Effects only render when stdout is a real terminal. Piped or redirected output is always plain text:
DSD_CYBER=1 dsd d20 | cat
# 17No ANSI leaks. No animated noise frames in pipes. No color in redirected output.
Errors and exit codes
Invalid commands exit 1 and print the problem to stderr.
dsd d0
# dice sides must be at least 1, got 0
dsd 0d6
# dice count must be at least 1, got 0
dsd 4dd6
# invalid dice notation: "4dd6" (expected forms like d20 or 4d6)
dsd d6 -n
# missing value for -n (expected a positive integer)
dsd --badflag
# unknown flag: --badflag| Code | Meaning |
|---:|---|
| 0 | Success |
| 1 | Invalid input, invalid notation, or usage error |
Randomness
Normal CLI rolls use Node's built-in crypto.randomInt through the production roller.
What that means:
- rolls are generated directly in
1..sides - there is no modulo-bias shortcut
- there is no smoothing or balancing
- there is no hidden reroll logic
- there are still normal random clumps and bumps in real samples
crypto.randomInt is chosen over Math.random deliberately: it draws a uniform
integer with no modulo bias, and its per-roll cost is negligible for an
interactive CLI. The roller is injectable (rng(sides)), so tests stay
deterministic without weakening the production path.
Manual randomness audit
Run a one-million-roll d20 audit:
npm run analyze:randomIt prints count data and a chi-square statistic, then writes local reports:
| File | Purpose |
|---|---|
| reports/random-d20-1m.csv | Raw count data |
| reports/random-d20-1m.svg | Browser-viewable bar chart |
Open the graph:
xdg-open reports/random-d20-1m.svgThis is a visual sanity check, not proof of perfect randomness. reports/ is gitignored.
Development
./install-dev
./status
./test
./auditThe developer wrappers keep the common workflows short:
| Command | Purpose |
|---|---|
| ./dev | Cozy project dashboard with version, branch, tests, and release readiness |
| DSD_DEV=1 ./dev | Developer dashboard with extra release/remote internals |
| ./doctor | Toolchain, permissions, workflow, release, and npm auth checks |
| ./test | Existing npm test suite |
| ./audit | Tests, release checks, pack dry run, workflow sanity, and shellcheck |
| ./status | Branch, package version, npm/release/CI status, dirty files, pending tags |
| ./sync | Fetch, rebase from upstream, and install dependencies |
| ./sync --hard | Reset to GitHub source of truth, clean, submodules, reinstall |
| ./release patch | Clean-tree release pipeline; also supports minor and major |
The deterministic test suite covers:
- parser valid/invalid cases
- validation bounds
- CLI flags
- missing flag values
- unknown flags
- stdin behavior
- output formatting
- display presets
- non-TTY safety
- install script structure
- deterministic injected RNG behavior
- clean stderr on failures
Layout
package.json
package-lock.json
src/
index.js
parser.js
validate.js
roll.js
format.js
env.js
display.js
color.js # compat re-export of render/color.js
tui.js # compat re-export of render/{terminal,animation}.js
cli.js
render/
index.js # public render entrypoint
ansi.js # raw ANSI escape constants/helpers
color.js # SGR palette + border color (applied after layout)
cell.js # visible width, clip, pad, fit (ignores ANSI)
layout.js # rows/cells/borders -> measured box lines; themes
model.js # pure display model from a rolled result
animation.js # deterministic frame generation (pure)
terminal.js # the only writer: redraw loop, cursor, resize
scripts/
lib.sh
dev
test
release
sync
audit
doctor
status
install-dev
benchmark
clean
update
install.sh
update.sh
uninstall.sh
analyze-random.js
tests/
parser.test.js
validate.test.js
roll.test.js
format.test.js
display.test.js
color.test.js
cell.test.js
layout.test.js
animation.test.js
terminal.test.js
install-script.test.js
cli.test.jsArchitecture boundary
parse -> validate -> roll -> build display model -> renderRules:
- parser does not roll dice
- validator owns bounds
- roller does not parse notation
- formatter does not generate randomness
- CLI owns argv/stdin/stdout/stderr/exit codes
- tests can inject deterministic RNG / die rollers
- rendering never parses, validates, rolls, or re-rolls; rolling knows nothing
about ANSI, terminal width, frames, color, or layout (see
src/render/)
Security posture
Dead-Simple-Dice is intentionally narrow.
Current expectations:
- no network calls
- no telemetry
- no accounts
- no config system
- no shell execution from dice input
- no arbitrary file reads from dice notation
- no normal-roll file writes
- no runtime dependencies
- generated reports ignored by git
See SECURITY.md for the full policy.
Roadmap
| Done | Later | Maybe never |
|---|---|---|
| MVP CLI | npm package publication | full dice expression language |
| d20, 4d6, d6 -n 4 | shell completions | exploding dice |
| stdin support | man page | keep/drop rules |
| help/version flags | d% shorthand | advantage/disadvantage shortcuts |
| clear errors | --sum | interactive mode |
| deterministic tests | --quiet | full-screen GUI |
| crypto-backed rolls | --json | online rolling |
| manual randomness audit | --seed | |
| display presets | | |
| install/uninstall scripts | | |
Project docs
| File | Purpose |
|---|---|
| CLAUDE.md | Implementation/spec notes for coding agents |
| TODO.md | Checklist and future work |
| SECURITY.md | Security policy |
| CONTRIBUTING.md | Contribution rules |
| LICENSE | MIT license |
AI-use disclosure
This project has been developed with assistance from AI tools, including ChatGPT Plus, Codex, and Claude Code.
Those tools can be wrong. Code, docs, tests, and project direction still need human review before they are trusted.
The maintainer understands why many programmers, artists, writers, and workers distrust AI tools. That distrust is not dismissed here.
Contributing
Keep patches small, testable, offline, and easy to review.
Before changing behavior:
npm ci
npm testUseful smoke checks:
dsd --help
dsd --version
dsd d20
dsd 4d6
echo d20 | dsdRead CONTRIBUTING.md before opening a patch.
License
MIT. See LICENSE.
