Coder Donjons & Dragons... en typescript ?

J'ai commencé, encore, un nouveau projet

Donjons et Dragons, en typescript

Ainsi que je le disais sur le réseau social Bluesky, je me suis mis en tête à l'occasion de la mise à jour des règles de Donjons et Dragons en version 5.5 de voir si je pouvais coder une partie de celles-ci, sans la partie graphique, en Typescript.

Donjons & Dragons est un jeu de rôle, c'est à dire un ensemble de règles, plus ou moins complexes, permettant de simuler un univers fictif ainsi que la description de l'univers en question : ses pays, ses différentes formes de vies, son économie,... Ce n'est sans doute pas mon jeu de rôle préféré mais il a l'avantage de laisser peu de places à l'interprétation dans ses règles, assez bien formalisées et relativement cohérentes.

Quand à typescript, il s'agit d'un langage de programmation à l'histoire un peu particulière puisqu'il n'existe que pour combler les défauts d'un autre langage déjà existant mais perclus de défauts, le javascript. Imaginez ça comme un ajout de mots clés au langage, que vous avez le droit ou pas d'utiliser mais qui, quand vous le faites, rend tout plus formel.

Pour des raisons que nous allons expliquer plus loin.

Mais c'est quoi le travail de développeur et développeuse au juste ?

Sans doute que pour les gens dont ne c'est pas le métier, la principale activité d'une personne qui développe, qu'on appelera désormais "developpeureuse" par souci d'inclusion, c'est d'écrire des lignes de code, ce qui est en en grande partie vraie.

Quand on débute, la maitrise du langage informatique, voir de plusieurs, représente la plus grosse difficulté et l'activité sur laquelle on passe le plus de temps, à l'instar de la dessinatrice débutante qui doit se concentrer pleinement pour dessiner une guerrière à cheval chargeant une automobile vue de 3/4. Mais avec la pratique apparait le vrai métier de développeureuse : la modélisation.

Plus on avance et plus le métier consiste à recevoir une suite d'instructions en langage naturel, imprécises et ouvertes à l'interprétation, émises par des responsables produits, dirigeant·es de startup ou chef·fes de projet, pour les convertir en règles beaucoup plus strictes, formelles et surtout déterministes, qui prendront la forme de ligne de code qu'on tapera nous même ou confiera à une personne encore au stade de l'apprentissage de la technique de codage.

La capacité d'abstraction est une compétence commune à beaucoup de profession scientifiques ou créatives. Elle consiste, à mon avis, à saisir ce qui est la quintessence d'une chose, ce à quoi on peut la réduire au minimum sans trahir sa nature. Et pour le coup, je me permettrais de citer une célébrité connue parce que je trouve que ça colle parfaitement :

La perfection est atteinte, non pas lorsqu'il n'y a plus rien à ajouter, mais lorsqu'il n'y a plus rien à retirer.
Probablement Antoine de Saint-Exupéry

Des vertus du formalisme

C'est sans doute le propre des sciences, et peut être en partie de l'Art, de chercher, pour toute choses et tout phénomène, comment le réduire à sa forme la plus élementaire tout en expliquant et reproduisant un maximum des observations qui en ont été ou en seront faites. C'est aussi ce que certains et certaines d'entre nous font en informatique et que j'appelais plus haut "modéliser", décrire de la manière la plus succinte, mais qui reste fonctionelle pour le but recherché, les choses.

Ainsi par exemple, "un site web où l'utilisateur peut lire mes bd" deviendra "des listes d'images ordonnées, listes elles mêmes numérotées par date" puis


interface BdPage {
  images: string[];     // Dans la pluspart des cas une image c'est juste une url donnant son emplacement.
  date: Date;  
}

interface blog {
    pages: BdPages[];
}
                
et voilà ! vous avez tout ce qui faut pour faire un blog BD !

Types.

Revenons brièvement sur cette histoire de Typescript. Le web moderne repose pour beaucoup sur un langage, le dénommé javascript, qui a d'après la légende était conçu à la va vite. Ainsi, entre autre reproche qu'on lui fait, celui ci ne possède pas réellement de système de typage de ses variables, problème corrigé par un autre langage qui vient s'y greffer, le Typescript.

Une partie du métier de dévelopeureuse est justement de choisir comment décrire chaque élement qui va composer notre application. Et décrire, c'est typer. Voyons ça avec mon exemple de Donjons & Dragons et demandons nous ce qu'est un jet de dés au juste :


import constants from "@stdio/constant";

interface Die {
  dieValue: number;
}

interface DiceCupTemplate {
  dice: Die[];
  modifier?: number;
  roll(): number;
}

/**
 *  DiceCup is a cup with some dices, only the polyhedral ones,
 *  and maybe a modifier ( a number that add at the end )
 *  It's created from a classic DND string like "1D8 + 4".
 *
 */
class DiceCup implements DiceCupTemplate {
  dice: Die[] = [];
  modifier?: number;

  /**
   * constructor is a little bit complex because we parse string
   * First we split on the '+'
   * then, each element may be a number or a dice
   * It's a dice if splitting it on the D gives 2 elements
   */
  constructor(diceDescriptor: string) {
    //1D6, 1D6+9 etc
    const itemList: string[] = diceDescriptor.replace(/\s/g, "").split("+");

    for (let item of itemList) {
      const maybeADie: string[] = item.toLowerCase().split("d");

      if (maybeADie.length === 1) {
        const c = parseInt(maybeADie[0]);

        if (!isNaN(c)) {
          this.modifier = c;
        }
      }

      if (maybeADie.length === 2) {
        const dieCount: number = parseInt(maybeADie[0]);
        const dieValue: number = parseInt(maybeADie[1]);

        if (!isNaN(dieCount) && !isNaN(dieValue)) {
          if (constants.polyhedralDieValues.includes(dieValue)) {
            for (let i = 0; i < dieCount; i++) {
              this.dice.push({ dieValue } as Die);
            }
          }
        }
      }
    }
  }

  static getRandomIntInclusive(min: number, max: number) {
    const randomBuffer = new Uint32Array(1);

    window.crypto.getRandomValues(randomBuffer);

    let randomNumber = randomBuffer[0] / (0xffffffff + 1);

    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(randomNumber * (max - min + 1)) + min;
  }
  roll(): number {
    let total = 0;
    for (let die of this.dice) {
      total += DiceCup.getRandomIntInclusive(1, die.dieValue);
    }

    if (this.modifier) {
      total += this.modifier;
    }

    return total;
  }
}

export default DiceCup;                      
                    

Dans le jeu Donjons & Dragons, un jet de dé consiste à lancer un ou plusieurs dés polyédriques ( restreints aux polyèdres réguliers plus le dé à 100 faces qu'on peut simuler avec un dé à 10 faces) et à y ajouter un modificateur pour obtenir un nombre entier.

Un "jet de dé" c'est donc :

  • Une liste de 0, 1 ou plus dés,
  • 0 ou 1 nombre entier ( le modificateur),
  • Une méthode "lancer" (roll en anglais ) qui retourne en entier dépendant des dés lancer

et un dé c'est :

  • un nombre entier appartenant à l'ensemble 4,6,10,12,20

De plus, le jeu propose une formalisation de la description d'un lancer de dé sous la forme "2D6","1D20", "2D6+1D4+10". On rajoute donc la règle

Une description d'un lancer de dé est une suite de 0, 1 ou plus élèments séparés par le caractères "+". Chaque éléments est soit sous la forme d'un entier, soit sous la forme de 2 entiers séparés par la lettre "D"
Et boum. Vous voilà à inventer une grammaire et écrire un analyseur syntaxique pour déchiffre ( "parse") cette grammaire.

Bien qu'en apparence trivial, ce travail est déjà une énorme suite de choix et de simplifications. Après tout, j'aurais très bien pu décrire un lancer de dés comme un ensemble de polyhèdres non élastiques munis d'une vitesse initiale et d'un moment cinétique et soumis aux lois de la gravité mais pour ce jeu là en particulier, voir un dé comme un nombre entier muni d'une méthode "lancer" est suffisant (je connais au moins un jeu pour lequel ce serait nécessaire par contre)

Vous aurez évidemment remarqué que pour l'instant on ne s'est pas préoccupé de décrire COMMENT obtenir notre nombre entier à partir de la description de l'objet "lancer de dés" (Vous pouvez cependant lire le code en détail pour voir comment). Nous nous sommes juste demander "de quoi a ton besoin pour notre simulation" et "qu'est ce qui décrit ce dont on a besoin". Le boulot ensuite sera d'écrire ça sous forme de langage informatique et c'est là qu'on retrouvera la compétence qu'on prête aux dévelopeureuses : écrire du code


// Un dé c'est
interface Die {
  dieValue: number; // Juste un nombre ( Typescript ne fait pas de différence entre nombres flottants, entiers etc)
}
// Un jet de dé c'est :
interface DiceCupTemplate {
  dice: Die[]; // Une liste de dé
  modifier?: number; //  un nombre entier
  roll(): number; // et une méthode roll() qui retourne un entier, sans doute à partir de la liste de dé et du modificateur
}      
// Pour créer un jet de dé, on fournit une chaine de caractères
  constructor(diceDescriptor: string) 
  [...]