Skip to content
← 1001 Widgets
N° 041 · ●●○

Tabletop RPG dice

Type 2d6+3, 4d6kh3 or 2d20kl1 and the dice roll: role-playing game notation, understood and computed. The right exercise to read a small syntax and draw at random without bias.

JS
What you'll learn
  • Read a notation with a regular expression
  • Draw a number without bias with crypto.getRandomValues
  • Keep the best or worst dice of a roll
Why this language

Pourquoi des dés de jeu de rôle ? Autour d'une table de Donjons et Dragons, on dit « deux d6 plus trois » ou « un d20 avec avantage ». Cette notation est une petite langue : NdF veut dire N dés à F faces, puis on ajoute ou retire des nombres.

On la découpe avec une expression régulière, terme par terme. On ajoute kh et kl pour garder les meilleurs ou les pires dés : 4d6kh3 lance quatre dés et garde les trois plus hauts, 2d20kl1 donne le désavantage.

Pour tirer chaque dé, on utilise crypto.getRandomValues, et on rejette les valeurs qui fausseraient le résultat. Les joueurs ont le droit à des dés honnêtes.

Pourquoi en JavaScript simple ? Une expression régulière, une boucle, un tri. Les messages d'erreur disent ce qui ne va pas dans la notation.

How it works
  1. UN DÉ HONNÊTE

    Le modulo seul favorise les petites valeurs : on rejette le haut de la plage.

    function de(faces) {
      const max = Math.floor(2 ** 32 / faces) * faces, b = new Uint32Array(1);
      do crypto.getRandomValues(b); while (b[0] >= max);
      return (b[0] % faces) + 1;
    }
  2. LIRE LA NOTATION

    Chaque terme : un signe, puis NdF avec un kh ou kl optionnel, ou un simple nombre.

    const TERME = /([+-]?)(?:(\d*)d(\d+)(?:k([hl])(\d+))?|(\d+))/gy;
    function analyser(texte) {
      const s = texte.toLowerCase().replace(/\s+/g, '');
      const termes = [];
      let pos = 0;
      while (pos < s.length) {
        TERME.lastIndex = pos;
        const m = TERME.exec(s);
        if (!m) throw new Error('Notation incomprise près de ' + s.slice(pos));
        const signe = m[1] === '-' ? -1 : 1;
        termes.push(m[3]
          ? { signe, n: Number(m[2] || 1), faces: Number(m[3]), garde: m[4], k: Number(m[5]) }
          : { signe, valeur: Number(m[6]) });
        pos = TERME.lastIndex;
      }
      return termes;
    }
  3. LANCER UN TERME

    kh3 : on trie du plus haut au plus bas et on garde les trois premiers.

    function lancerTerme(t) {
      const des = Array.from({ length: t.n }, () => de(t.faces));
      if (!t.garde) return des.reduce((a, b) => a + b, 0);
      const tri = [...des].sort((a, b) => t.garde === 'h' ? b - a : a - b);
      return tri.slice(0, t.k).reduce((a, b) => a + b, 0);
    }
  4. LE TOTAL

    const total = texte => analyser(texte)
      .reduce((somme, t) => somme + t.signe * ('valeur' in t ? t.valeur : lancerTerme(t)), 0);
  5. ESSAYER

    console.log(total('2d6+3'), total('4d6kh3'), total('1d20-1'));
Pitfalls

PIÈGE 1 : Le biais du modulo

Pitfall

Mauvais : nombreAleatoire % 6 + 1 sur un entier de 32 bits. Les plus petites faces sortent un tout petit peu plus souvent.

Fix

Correct : rejeter les valeurs au-dessus du plus grand multiple de 6, puis appliquer le modulo.

PIÈGE 2 : Accepter n'importe quoi

Pitfall

Mauvais : lancer 99999d99999 parce que la notation est valide. Le navigateur se fige.

Fix

Correct : des limites raisonnables, comme 100 dés et 1000 faces, avec un message clair.

PIÈGE 3 : Ignorer les morceaux incompris

Pitfall

Mauvais : une expression régulière sans le drapeau y qui saute ce qu'elle ne comprend pas. « 2d6x3 » donne un résultat sans rien dire.

Fix

Correct : lire terme par terme depuis la position courante, et signaler une erreur au premier caractère inattendu.

PERSONNALISER
  • Les dés qui explosent : un 6 se relance et s'ajoute, noté 3d6!.
  • Des lancers enregistrés pour chaque personnage, comme « Attaque à l'épée ».
  • L'avantage et le désavantage en un clic, sans taper la notation.
OPTIMISATION
  • Pour des statistiques, calculez la distribution exacte des résultats au lieu de lancer des milliers de fois.
  • Gardez l'historique des lancers dans localStorage pour les retrouver d'une séance à l'autre.
VERS REACT

L'analyseur et le tirage vivent dans un fichier pur, testable sans interface.

La notation est un useState, le dernier résultat et l'historique en sont deux autres.

Relancez l'animation des dés en changeant la key du conteneur à chaque lancer.

Comments
0/1000

Loading comments…

Audience measurement

With your consent, this site measures its traffic with Google Analytics and the Meta pixel, via Google Tag Manager. Nothing is loaded until you choose, and you can change your mind at any time with the "Cookies" link at the bottom of the page.