---
title: dice roller
canonical: "https://presque.cool/projects/dice-roller/"
description: "Un lanceur de dés pour rôlistes qui va au-delà du basique. Notation riche, historique, presets — et chaque lancer est vérifiable."
---

<div data-lang="fr">

## Le pitch

Dice Roller est une web-app de lancer de dés pour rôlistes. On entre une formule (`2d6+3`, `1d20`, `4d8kh3`...) et on obtient instantanément le résultat, avec le détail des dés lancés.

Ce qui la distingue : une notation riche (keep/drop, relances, tri, répétitions, tags) et un système de vérification "provably fair" qui permet d'auditer n'importe quel lancer.

**Essayer** : [presque.cool/dice/](https://presque.cool/dice/)

<div class="quick-overview">
  <div class="overview-item">
    <span class="overview-label">Durée</span>
    <span class="overview-value">~1 mois</span>
  </div>
  <div class="overview-item">
    <span class="overview-label">Période</span>
    <span class="overview-value">Oct 2025</span>
  </div>
  <div class="overview-item">
    <span class="overview-label">Stack</span>
    <div class="tech-tags">
      <span class="tech-tag">React</span>
      <span class="tech-tag">TypeScript</span>
      <span class="tech-tag">Zustand</span>
      <span class="tech-tag">PWA</span>
    </div>
  </div>
  <div class="overview-item">
    <span class="overview-label">Statut</span>
    <span class="overview-value accent">✓ En production</span>
  </div>
</div>

---

## Pourquoi ce projet

Tous les lanceurs de dés que je trouvais en ligne avaient le même problème : ils étaient laids, ou lourds, ou les deux. J'aimais bien celui de Google — simple, rapide — mais je voulais quelque chose de plus complet pour mes sessions de JDR.

C'était aussi le bon candidat pour ma première vraie application React. Un projet avec un périmètre clair, un besoin réel, et assez de complexité technique pour apprendre sans se noyer.

Trois objectifs :

1. **Apprendre React** — mon premier projet sérieux avec le framework.
2. **Créer un outil que j'utiliserais vraiment** — rapide, lisible, adapté aux sessions de jeu.
3. **Aller au-delà du basique** — supporter une notation riche et résoudre le problème de la confiance dans les résultats en ligne.

---

## Le cœur technique : un parser de notation

La plupart des lanceurs de dés se contentent de `XdY+N`. Dice Roller va plus loin :

- **Dés standard** : `d4`, `d6`, `d8`, `d10`, `d12`, `d20`, `d100`
- **Modificateurs** : `+3`, `-2`, et combinaisons (`2d8+1d6+2`)
- **Keep/Drop** : `kh3` (garder les 3 meilleurs), `dl1` (retirer le plus bas)
- **Relances** : `r` (relancer les 1), `ro>=5` (relancer une fois si ≥5)
- **Tri** : `sa` (croissant), `sd` (décroissant)
- **Répétition** : `(2d6+3)@10` pour lancer 10 fois la même formule
- **Tags** : `#attaque` pour annoter un lancer

Pour parser tout cela, j'ai implémenté un **parser récursif descendant**. C'était ma première fois — je ne savais pas exactement où j'allais au départ, mais l'approche s'est imposée naturellement face à la complexité des expressions imbriquées.

Le parser produit un AST léger (une liste de "parts" : dés ou modificateurs numériques), avec validation stricte : types de dés autorisés, bornes sur le nombre de dés (≤100 par terme), messages d'erreur explicites quand l'utilisateur tape `k3` au lieu de `kh3`.

---

## Le problème de la confiance : "provably fair"

En JDR en ligne, il y a toujours ce doute : "Est-ce que le dé était vraiment aléatoire, ou l'app m'a donné un résultat arrangé ?"

Dice Roller intègre un système de vérification :

1. Génération d'une **entropie locale** (8 bytes via `crypto.getRandomValues`)
2. Création d'un **nonce** (timestamp)
3. Calcul d'un hash SHA-256 à partir de l'entropie, du nonce et d'un secret applicatif
4. Utilisation d'un **PRNG déterministe** (Mulberry32) pour générer les résultats
5. Stockage d'un hash final dans l'historique

N'importe quel lancer peut être recalculé et vérifié côté client. Ce n'est pas une sécurité "anti-triche" absolue (pour cela il faudrait un serveur), mais cela répond au besoin réel : pouvoir prouver qu'un résultat n'a pas été modifié après coup.

---

## Performance : Web Workers et heuristiques

Le parsing peut devenir coûteux avec des notations comme `100d20`. La stratégie :

- Compter rapidement le nombre total de dés via regex
- Au-dessus d'un seuil (50 dés), basculer le parsing dans un **Web Worker**
- En cas d'échec (worker indisponible, timeout), fallback vers parsing synchrone

Cela garantit que l'interface reste fluide même avec des formules complexes.

---

## Architecture : séparation des responsabilités

Le projet suit une structure en trois couches :

**Services (métier pur)** :

- Parser de notation → AST
- Moteur d'exécution → résultats détaillés
- Crypto → entropie, hash, PRNG

**Hooks (orchestration UI)** :

- Gestion de l'état de lancer (`isRolling`, `lastRoll`)
- Décision d'utiliser ou non le Web Worker

**UI (composants)** :

- Input avec autocomplétion
- Affichage des résultats (total + détail des dés gardés/retirés)
- Historique filtrable
- Modales (aide, settings, vérification)

Cette séparation a un avantage clé : le cœur métier est testable et réutilisable indépendamment de React.

---

## UX : rapidité et lisibilité

L'objectif était "one-input, résultat immédiat". Quelques choix :

- **Autocomplétion intelligente** : suggestions de notation, navigation clavier (↑/↓/Tab/Enter)
- **Presets** : sauvegarder un lancer fréquent pour le retrouver dans l'autocomplétion
- **Historique filtrable** : recherche textuelle ou numérique (`>15`, `<=10`, `15-20`)
- **Affichage détaillé** : total visible immédiatement, détail des dés (gardés, retirés) accessible

L'autocomplétion est désactivée sur mobile pour éviter les conflits avec le clavier virtuel — un compromis pragmatique.

---

## Stack technique

<ul class="tech-stack">
<li><strong>React</strong> + <strong>TypeScript</strong></li>
<li><strong>Vite</strong> comme bundler (avec SWC)</li>
<li><strong>Zustand</strong> pour le state management — persistance localStorage (paramètres) + IndexedDB (historique)</li>
<li><strong>Tailwind CSS</strong> en mode CSS-first (tokens via variables CSS, couleurs OKLCH)</li>
<li><strong>PWA</strong> — installable, offline</li>
<li><strong>i18n</strong> maison (français/anglais)</li>
<li><strong>Web Worker</strong> optionnel pour les notations lourdes</li>
<li><strong>Compression</strong> — Brotli + Gzip sur les assets</li>
<li><strong>CSP</strong> — Content-Security-Policy avec hashes auto-générés</li>
</ul>

---

## Évolutions

| Date     | Version | Description                                                                             |
| -------- | ------- | --------------------------------------------------------------------------------------- |
| Oct 2025 | 1.0.0   | Version initiale — Lanceur de dés avec parser de notation, système provably fair et PWA |

---

## Bilan

**Durée** : environ 1 mois (octobre 2025).

**Ce que j'ai appris** :

- Construire une application React de bout en bout — mon premier projet sérieux avec le framework
- Implémenter un parser récursif descendant — une première, plus accessible que je ne le pensais
- Intégrer de la cryptographie côté client pour un cas d'usage concret

**Ce que je referais pareil** :

- Partir d'un besoin personnel réel — cela motive et donne une direction claire
- Séparer strictement le métier de l'UI dès le départ

**Ce que je changerais** :

- Ajouter une visualisation 3D pour voir les dés rouler. L'expérience serait plus immersive, et ce serait l'occasion d'explorer Three.js ou une lib similaire.

</div>

<div data-lang="en">

## The pitch

Dice Roller is a web app for tabletop RPG players. Enter a formula (`2d6+3`, `1d20`, `4d8kh3`...) and instantly get the result, with details of each die rolled.

What sets it apart: rich notation support (keep/drop, rerolls, sorting, repetitions, tags) and a "provably fair" verification system that allows auditing any roll.

**Try it**: [presque.cool/dice/](https://presque.cool/dice/)

<div class="quick-overview">
  <div class="overview-item">
    <span class="overview-label">Duration</span>
    <span class="overview-value">~1 month</span>
  </div>
  <div class="overview-item">
    <span class="overview-label">Period</span>
    <span class="overview-value">Oct 2025</span>
  </div>
  <div class="overview-item">
    <span class="overview-label">Stack</span>
    <div class="tech-tags">
      <span class="tech-tag">React</span>
      <span class="tech-tag">TypeScript</span>
      <span class="tech-tag">Zustand</span>
      <span class="tech-tag">PWA</span>
    </div>
  </div>
  <div class="overview-item">
    <span class="overview-label">Status</span>
    <span class="overview-value accent">✓ In production</span>
  </div>
</div>

---

## Why this project

Every dice roller I found online had the same problem: they were ugly, slow, or both. I liked Google's — simple, fast — but I wanted something more complete for my RPG sessions.

It was also the perfect candidate for my first real React application. A project with a clear scope, a real need, and enough technical complexity to learn without drowning.

Three goals:

1. **Learn React** — my first serious project with the framework.
2. **Create a tool I'd actually use** — fast, readable, suited for game sessions.
3. **Go beyond the basics** — support rich notation and solve the trust problem with online results.

---

## The technical core: a notation parser

Most dice rollers only handle `XdY+N`. Dice Roller goes further:

- **Standard dice**: `d4`, `d6`, `d8`, `d10`, `d12`, `d20`, `d100`
- **Modifiers**: `+3`, `-2`, and combinations (`2d8+1d6+2`)
- **Keep/Drop**: `kh3` (keep highest 3), `dl1` (drop lowest 1)
- **Rerolls**: `r` (reroll 1s), `ro>=5` (reroll once if ≥5)
- **Sorting**: `sa` (ascending), `sd` (descending)
- **Repetition**: `(2d6+3)@10` to roll the same formula 10 times
- **Tags**: `#attack` to annotate a roll

To parse all this, I implemented a **recursive descent parser**. It was my first time — I didn't know exactly where I was going at first, but the approach naturally emerged when facing the complexity of nested expressions.

The parser produces a lightweight AST (a list of "parts": dice or numeric modifiers), with strict validation: allowed dice types, bounds on dice count (≤100 per term), explicit error messages when users type `k3` instead of `kh3`.

---

## The trust problem: "provably fair"

In online RPGs, there's always that doubt: "Was the die truly random, or did the app give me a rigged result?"

Dice Roller includes a verification system:

1. Generation of **local entropy** (8 bytes via `crypto.getRandomValues`)
2. Creation of a **nonce** (timestamp)
3. Calculation of a SHA-256 hash from entropy, nonce, and an application secret
4. Use of a **deterministic PRNG** (Mulberry32) to generate results
5. Storage of a final hash in history

Any roll can be recalculated and verified client-side. It's not absolute "anti-cheat" security (that would require a server), but it addresses the real need: being able to prove a result wasn't modified after the fact.

---

## Performance: Web Workers and heuristics

Parsing can become expensive with notations like `100d20`. The strategy:

- Quickly count total dice via regex
- Above a threshold (50 dice), move parsing to a **Web Worker**
- On failure (worker unavailable, timeout), fallback to synchronous parsing

This ensures the interface stays smooth even with complex formulas.

---

## Architecture: separation of concerns

The project follows a three-layer structure:

**Services (pure business logic)**:

- Notation parser → AST
- Execution engine → detailed results
- Crypto → entropy, hash, PRNG

**Hooks (UI orchestration)**:

- Roll state management (`isRolling`, `lastRoll`)
- Decision to use Web Worker or not

**UI (components)**:

- Input with autocomplete
- Result display (total + kept/dropped dice details)
- Filterable history
- Modals (help, settings, verification)

This separation has a key advantage: the business core is testable and reusable independently of React.

---

## UX: speed and readability

The goal was "one input, instant result". Some choices:

- **Smart autocomplete**: notation suggestions, keyboard navigation (↑/↓/Tab/Enter)
- **Presets**: save a frequent roll to find it in autocomplete
- **Filterable history**: text or numeric search (`>15`, `<=10`, `15-20`)
- **Detailed display**: total immediately visible, dice details (kept, dropped) accessible

Autocomplete is disabled on mobile to avoid conflicts with the virtual keyboard — a pragmatic compromise.

---

## Tech stack

<ul class="tech-stack">
<li><strong>React</strong> + <strong>TypeScript</strong></li>
<li><strong>Vite</strong> as bundler (with SWC)</li>
<li><strong>Zustand</strong> for state management — localStorage (settings) + IndexedDB (history) persistence</li>
<li><strong>Tailwind CSS</strong> in CSS-first mode (tokens via CSS variables, OKLCH colors)</li>
<li><strong>PWA</strong> — installable, offline</li>
<li><strong>i18n</strong> custom (French/English)</li>
<li><strong>Web Worker</strong> optional for heavy notations</li>
<li><strong>Compression</strong> — Brotli + Gzip on assets</li>
<li><strong>CSP</strong> — Content-Security-Policy with auto-generated hashes</li>
</ul>

---

## Timeline

| Date     | Version | Description                                                                      |
| -------- | ------- | -------------------------------------------------------------------------------- |
| Oct 2025 | 1.0.0   | Initial version — Dice roller with notation parser, provably fair system and PWA |

---

## Takeaways

**Duration**: about 1 month (October 2025).

**What I learned**:

- Building a React application end-to-end — my first serious project with the framework
- Implementing a recursive descent parser — a first, more accessible than I thought
- Integrating client-side cryptography for a concrete use case

**What I'd do the same**:

- Start from a real personal need — it motivates and gives clear direction
- Strictly separate business logic from UI from the start

**What I'd change**:

- Add 3D visualization to see the dice roll. The experience would be more immersive, and it would be an opportunity to explore Three.js or a similar library.

</div>
