PROJ-RPGDICER
Loaded
Project detail

Problem

Most browser dice rollers are either shallow randomizers or noisy tools with weak logic. I wanted one that handled actual RPG use cases while still fitting the terminal language of the site. It also needed to stay fully client-side, preserve history without a backend, and avoid naive `Math.random()` handling for game results.

Context / users

This is a real utility, not a visual toy. It supports mixed pools, modifiers, notes, held-die rerolls, copyable notation, and keyboard shortcuts. The important part is the split between roll logic and presentation.

My role

I designed and built the project end to end: route integration, React component structure, roll engine design, notation parsing, held-die reroll behavior, keyboard interaction model, browser persistence, styling, and tests for the core logic layer.

Solution

I built the roller in two layers. The first is a reusable engine in `lib/roller.js` that handles parsing, validation, roll execution, rerolls, totals, and formatting. The second is a React UI layer for pool building, result display, and history. For randomness, I used `crypto.getRandomValues()` when available and paired it with rejection sampling to avoid modulo bias.

  • Reusable roll engine separated from the React UI layer
  • Web Crypto API randomization with rejection sampling
  • Held-die rerolls with stable position-based state
  • Notation-aware mixed dice pools and modifiers
  • Browser-persisted history with localStorage
  • Keyboard shortcuts and terminal-aligned UX

Architecture

The logic layer handles parsing, validation, rolling, rerolls, totals, and output shaping. The UI layer handles pool state, result display, history, and keyboard interaction. That separation keeps the engine portable and makes it easier to reuse in terminal commands.

Project structure

RPG Dice Roller (inside λstepweaver v3)
├── app/
│   └── dice-roller/
│       └── page.jsx              # Dedicated /dice-roller route
├── components/
│   └── DiceRoller/
│       ├── DiceRoller.jsx        # Main container, state model, keyboard interactions
│       ├── DicePoolBuilder.jsx   # Pool composition UI and modifier controls
│       ├── DiceResult.jsx        # Roll output, totals, held-die interactions
│       └── RollHistory.jsx       # Persistent recent-roll history
├── lib/
│   ├── roller.js                # Core engine: randomization, parsing, validation, rerolls
│   └── diceConstants.js         # Dice metadata, labels, limits, and UI helpers
├── utils/
│   └── dateFormatter.js         # Timestamp formatting for roll history
└── __tests__/ or test files     # Engine-level behavior verification

Stack

Frontend

  • Next.js 15 – Dedicated App Router route for /dice-roller
  • React 19 – Component composition, client-side state, memoized callbacks
  • Tailwind CSS 4 – Terminal-inspired layout, CRT glow, responsive styling
  • Lucide React / React Icons – UI symbols and interaction cues

Architecture

RPG Dice Roller (inside λstepweaver v3)
├── app/
│   └── dice-roller/
│       └── page.jsx              # Dedicated /dice-roller route
├── components/
│   └── DiceRoller/
│       ├── DiceRoller.jsx        # Main container, state model, keyboard interactions
│       ├── DicePoolBuilder.jsx   # Pool composition UI and modifier controls
│       ├── DiceResult.jsx        # Roll output, totals, held-die interactions
│       └── RollHistory.jsx       # Persistent recent-roll history
├── lib/
│   ├── roller.js                # Core engine: randomization, parsing, validation, rerolls
│   └── diceConstants.js         # Dice metadata, labels, limits, and UI helpers
├── utils/
│   └── dateFormatter.js         # Timestamp formatting for roll history
└── __tests__/ or test files     # Engine-level behavior verification

Outcome

  • Built a portfolio piece that behaves like a real utility instead of a visual demo
  • Created a reusable roll engine that can support both the dedicated `/dice-roller` UI and `/terminal` commands without duplicating logic
  • Improved randomness quality by using the browser's Web Crypto API for die results and rejection sampling to avoid uneven face distribution
  • Kept the entire experience client-side, which reduced complexity, preserved privacy, and made the tool feel instant
  • Added test coverage around notation parsing, pool validation, and total calculation so the project reads as engineered software, not just styled UI

Why It Matters

This is a good example of the kind of software I like building: focused, interactive, visually distinct, and technically cleaner than it needed to be for the size of the tool.

What I'd improve next

  • Typed notation input would make power-user workflows faster than repeated click-based pool construction
  • The held-die interaction could benefit from stronger visual cues so locked versus rerolled dice read more clearly
  • Accessibility could be improved with additional contrast tuning, clearer focus indicators, and more explicit ARIA support around result interactions
  • README coverage should expand so the dice roller is documented as part of the broader portfolio codebase rather than discovered only by browsing the site

Key Features

Reusable roll engine

The core logic lives in `lib/roller.js`, not in component click handlers. It parses notation, validates pools, rolls dice, calculates totals, shapes result objects, and formats output.

Web Crypto API over Math.random()

For die results, the roller prefers `crypto.getRandomValues()` over `Math.random()`. I also used rejection sampling before modulo conversion so arbitrary ranges map cleanly without subtle bias.

Held-die reroll state model

Selective rerolls are handled by tracking held dice with stable position-based keys. That preserves exact locked outcomes while rerolling only the dice the user has not held.

Notation-aware workflow

The project supports actual dice notation instead of only button clicks. Pool composition, modifiers, totals, and copyable formula strings all flow through the notation layer.

Client-side persistence without backend overhead

Recent rolls are stored in `localStorage` with timestamps and defensive error handling. History survives refreshes without introducing a database or account system.

Keyboard-first product design

The roller includes shortcuts for rolling, copying notation, resetting the pool, and clearing results. It feels faster and more like a tool.

User Experience and Usability

Strengths

  • Feels like a real tool: users can build practical pools, annotate them, reroll selectively, and keep history
  • Technical decisions reinforce trust: stronger randomization, notation-aware logic, and deterministic held-die behavior make the roller more defensible
  • Fast and private: everything runs client-side, so there is no waiting on an API and no need to send roll history anywhere
  • Consistent with the λstepweaver brand: the retro terminal presentation supports the portfolio identity without sacrificing utility
  • Logic/UI separation creates room for extension: the engine can be reused in terminal commands, dialogs, or future game-adjacent tools

Areas for Improvement

  • Typed notation input would make power-user workflows faster than repeated click-based pool construction
  • The held-die interaction could benefit from stronger visual cues so locked versus rerolled dice read more clearly
  • Accessibility could be improved with additional contrast tuning, clearer focus indicators, and more explicit ARIA support around result interactions
  • README coverage should expand so the dice roller is documented as part of the broader portfolio codebase rather than discovered only by browsing the site

Terminal Integration

I separated the logic layer from the UI so the same engine can power terminal commands elsewhere in the portfolio. Dice behavior stays consistent across surfaces instead of being reimplemented.

Usage

  • Reuse the same parser and result formatter for a future roll command inside the λstepweaver terminal
  • Keep notation support consistent between button-driven UI and typed command workflows
  • Return structured roll data that can be rendered either visually or as terminal text output
λ roll 3d6+2
Rolling 3d6+2
3d6: [4, 2, 6] = 12
Modifier: +2
Total: 14

Keyboard Shortcuts

  • ENTER: Roll the current dice pool
  • C: Copy the current notation or result string
  • R: Reset the dice pool to a clean state
  • ESC: Clear the current result while keeping the configured pool

Dice Notation

The dice roller uses standard RPG dice notation:

  • 3d6 – Roll three six-sided dice
  • 1d20+5 – Roll one d20 and add a modifier
  • 2d8+1d6-2 – Mixed pool with both addition and subtraction

README Recommendations

Suggested README updates to make the repository more informative:

  • Document the dice roller as a first-class project inside the main stepwever_v3 README
  • Call out the architecture split between UI components and `lib/roller.js` so readers see that the logic is reusable
  • Mention the Web Crypto API and rejection sampling explicitly; that is one of the stronger technical decisions in the project
  • List keyboard shortcuts, localStorage persistence behavior, and the hold-and-reroll system in the usage section
  • Show one or two example notation strings and explain how the same engine could be reused in terminal commands

Conclusion

This is a good example of the kind of software I like building: focused, interactive, visually distinct, and technically cleaner than it needed to be for the size of the tool.

Like what you see?

Feel free to reach out if you have questions about this project or want to chat about working together.

> RPG Dice RollerWeb Development