Créez une application de lancer de dés en Python

Créer une petite application pour lancer des dés vous permet d’améliorer vos compétences en programmation. Et plus particulièrement dans cet article, en Python.

L’application n’a pas à être compliquée. L’interface sera une simple saisie texte dans le terminal et le travail sera réellement sur le métier, sur le tirage.

Nous allons commencer simple : lancer un dé classique à 6 faces. Ensuite, nous allons ajouter le possibilité de choisir le nombre et le type de dé. Cette dernière possibilité sera adaptée aux rôlistes. En effet, les jeux de rôle utilisent des dés à 4, 6, 8, 1à, 12, 20, 30 et 100 faces. Ok, le 30 est assez rare et le 100 faces est utilisé dans les systèmes à base de pourcentages.

Dans cet article, nous verrons deux notions importantes : le hasard avec la bibliothèque random et l’analyse de texte avec les expressions rationnelles et le module re.

Objectif de cette application

Cette application a en fait deux objectifs : vous proposer une petit sujet pour mini projet afin de pratiquer et voir certaines astuces en Python.

Ainsi, je vous propose de commencer par prendre connaissance du sujet ci-dessous. Et n’allez pas plus loin… Essayez de résoudre ce projet. Essayez d’avancer de vous même avec de voir toute la proposition. Et après, n’hésitez pas à publier en commentaire votre proposition.

Nous allons écrire une petite application qui permet de simuler un jet de dé(s). Elle devra gérer le lancer de plusieurs dés et le choix de leur nombre de faces.

Ce sera une application terminal, donc en ligne de commande (Terminal ou Dos/Powershell). Ceci est le plus simple, l’interface homme/machine n’est pas le cœur du problème. Lorsqu’elle sera lancée, elle attendra une saisie de l’utilisateur pour lancer le ou les dé(s).

L’utilisateur pourra demande le lancer de n’importe quel nombre de dés. Le nombre de faces sera limité aux valeurs 4, 6, 8, 10, 12, 20, 30 et 100.

Lorsque l’utilisateur soumet la lettre q comme quitter en minuscule ou capitale, l’application s’arrête.

Les éléments qui seront abordés dans la proposition sont :

  • La boucle d’interaction infinie
  • Les fonctions
  • Les tirage aléatoire avec la lib random
  • Les regex avec la lib re pour extraire les informations d’une saisie utilisateur
  • Les exceptions

Vous avez toutes les informations. Dans un premier temps, essayez. Puis, revenez voir la proposition.

Commençons par lancer un dé

Lancer un dé est une action aléatoire et vous savez que l’aléatoire n’existe pas en informatique. On a donc une bibliothèque, random, qui permet d’avoir des valeurs pseudo-aléatoires. Nous allons commencer par importer la bibliothèque et appeler la fonction seed(). Sans paramètre, cette fonction utilise l’heure courante pour générer la suite de valeurs. Attention, à partir d’une valeur de départ donnée, la séquence de valeurs aléatoires sera toujours la mêmes. Utilisons donc la valeur par défaut.

import random as rd

rd.seed()

Ensuite, pour tirer une valeur au hasard, nous allons utiliser la fonction randint(a, b). Oui, c’est la signature et les paramètres de la doc, pas très parlant. Le paramètre a représente la valeur minimale et b la valeur maximale. La fonction randint(a, b) retournera donc un entier compris entre a et b (compris). Pour notre dé à six faces, nous avons donc :

print(rd.randint(1, 6))

À l’exécution de ce code, l’interpréteur affichera une valeur aléatoire entre 1 et 6.

Répétons les jets

L’interaction prévue par cette application, c’est une boucle infinie comme nous l’avons vu dans l’article correspondant. Et elle sera similaire à l’article sur la création du jeu de devinette. Il y a cependant une différence : ici, nous avons une boucle, plutôt indéterminée qu’infinie, qui s’arrêtera sur ordre de l’utilisateur.

Connaissons nous par avance combien d’itérations ? Non, donc c’est une structure while.

Nous allons partir sur une boucle while True qui sera interrompue par un break lorsque l’utilisateur souhaitera quitter

La première implémentation serait la suivante.

while True:
    answer = input("Lancer un dé ? (q pour quitter) : ")
    if answer in "qQ":
        break

    print(rd.randint(1, 6))

Petite astuce, dans notre cas, l’utilisateur peut saisir le caractère en minuscule ou en capitale, ce qui nous fait sortir de la boucle. Tester si la saisie est dans liste est 3 fois plus performant que uniformiser la casse et tester l’égalité avec la chaine attendu. Je vous laisse le vérifier.

Arranger le code

Comme nous l’avons vu dans notre article sur le fonctions, il est préférable de déporter le tirage des dés dans une fonction spécifique. Intuitivement, cette fonction ne nécessite pas de paramètres. On l’exécute et elle retourne un entier entre 1 et 6 inclus.

def roll_dice():
    return rd.randint(1, 6)

while True:
    answer = input("Lancer un dé ? (q pour quitter) : ")
    if answer in "qQ":
        break

    print(roll_dice())

Maintenant que le tirage du dé est déporté dans une fonction, nous pouvons la tester isolément du reste. Lancez un shell interactif ou iPython et importez votre module…

Ah… Oui… Quand vous importez votre module, vous interprétez tout le code. Donc vous déclenchez la boucle while. C’est entre autres pour éviter ce genre de situation que vous devez toujours appliquer le pattern avec le test if __name__ == "__main__". Mais ici aussi, nous allons déporter ce code dans une fonction qui sera la fonction d’interaction. La boucle while devient donc :

def interaction_loop():
    while True:
        answer = input("Lancer un dé ? (q pour quitter) : ")
        if answer in "qQ":
            break

        print(roll_dice())

if __name__ == "__main__":
    interaction_loop()

Ceci nous permettra par la suite de travailler sur spécifiquement sur un composant.

Choisir le nombre de faces

Si la majorité d’entre vous est habitué aux dés à 6 faces, les jeux ont d’autres types de dés : 4, 8, 10, 12, 20, 30 ou 100. Ce dernier est destiné aux jeux de rôle utilisant des systèmes en pourcentage. Dans la pratique, les vrai dés à 100 faces sont rares, on utilise 2 dés à 10 faces que l’on peut différentier et on convient que l’un est pour les dizaines et l’autre pour les unités. Le résultat de 0 et 0 correspondant à 100. Bon, ça, c’est pour votre culture générale car évidemment, nous ferons un tirage sur 100.

Nous avons une fonction qui tire un dé. Ce allons lui ajouter un paramètre optionnel sides avec la valeur par défaut 6.

def roll_dice(sides=6):
    return rd.randint(1, sides)

Nous allons en profiter pour valider le nombre de faces. Notre fonction devra donc vérifier si le nombre de faces est cohérent avant de tirer un résultat et si non, lever une exception. Pour cette dernière, ici, une ValueError fera l’affaire.

Notre fonction va donc devenir :

def roll_dice(sides=6):
    if sides not in [4, 6, 8, 10, 12, 20, 100]:
        raise ValueError(f'Non valide number of dice sides, got {sides}')

    return rd.randint(1, sides)

Lancer plusieurs dés

Nous avons défini une fonction simple tirant un seul dé avec une approche de joueur concerné par le nombre de faces, mais le nombre de dés est tout aussi important. Notre fonction devrait donc avoir un paramètre nombre de dés, optionnel, avec une valeur par défaut à 1.

Nous allons donc faire évoluer cette fonction. Voyons déjà le code.

def roll_dice(how_many=1, sides=6):
    if sides not in [4, 6, 8, 10, 12, 20, 100]:
        raise ValueError(f'Non valide number of dice sides, got {sides}')

    if how_many < 1:
        raise ValueError(f"At least one dice is required to be thrown, got {how_many}")

    dices_values = [rd.randint(1, sides) for _ in range(how_many)]
    return tuple(dices_values)

À la première ligne, nous avons rajouté le paramètre how_many=1 qui informe du nombre de dés à lancer. J’ai choisi de le mettre en premier paramètre car il sera plus commun de paramétrer le nombre de dés plutôt que le nombre de faces.

Nous allons également ajouter une validation de cette valeur. Elle ne pourra être inférieur à 1.

Maintenant que le nombre de dés peut autre différent de 1, le retour de la fonction va changer. Cette fonction doit retourner une séquence des résultats du lancer. Pour alimenter cette séquence, nous pouvons simplement passer par une comprehension list. Et nous choisissons de la retourner sous forme de N-uplet (communément appelé par son anglicisme, tuple). Petite précision, oui, on peut ici se passer de la variable dices_values et réaliser la comprehension list directement pour le return. J’ai cependant préféré détailler pour la lisibilité.

Nous avons maintenant une fonction qui s’occupe de déterminer le résultat de nos jets de dés. Nous allons devoir revenir à l’interaction avec l’utilisateur.

Évolution de l’intéraction avec l’utilisateur

La fonction a évoluée. Elle peut maintenant lever une exception dans deux cas : un nombre de faces non valide et un un nombre de faces négatif ou nul. Le retour a aussi changé en étant une séquence de valeurs au lieu d’une seule valeur. Cependant, les changements sur la fonction sont, pour l’instant, sans conséquence sur l’exécution du code.

Nous allons devoir faire évoluer le code pour demander à l’utilisateur de saisir le nombre de dés et le nombre de faces. La solution la plus simple consiste à poser deux questions avec deux input(). J’aimerai l’éviter car c’est fastidieux pour l’utilisateur qui passe car ces deux questions.

L’autre solution consiste à imposer une chaine de caractères qui décrit le lancer attendu. Communément, ce serait de la forme [nombre dés]séparateur[nombre faces]. Nous utiliserons comme séparateur la lettre D (ou d), comme dé(s). Nous aurons donc des saisies comme 1d6 ou 3D12.

Le plus adapté pour traiter ce type de chaines de caractères, c’est les expressions rationnelles, plus communément appelées par leur anglicisme expressions régulières ou simplement, regex. Grâce à elles, nous allons pouvoir définir un motif qui va décrire la chaine que nous cherchons à identifier dans un texte. Ce motif va nous permettre d’extraire des parties d’intérêt. Nous allons donc devoir définir ce motif.

Définir un motif

Commençons par le caractère séparateur, d ou D. Celui-ci se représente par la classe de caractères [dD]. Ceci exprime que nous attendons un caractère qui doit être soit d, soit D.

Les chiffres se représentent aussi par une classe de caractères qui sera [0-9] soit un caractère entre 0 et 9. Mais nous pouvons aussi représenter ceci avec par \d. Cette description ne concerne qu’un seul caractère. Nous devons préciser que nous en attendons au moins un. Au moins un se représente par le symbole +. Nous aurons donc \d+.

Encore une chose, pour pouvoir extraire les valeurs, nous allons les mettre entre parenthèses. L’expression finale sera la chaine de caractères "(\d+)[dD](\d+)". Ainsi, nous allons pouvoir extraire le groupe de caractères entre parenthèses de l’ensemble de la regex.

Utiliser les RegEx avec Python

En Python, la bibliothèque pour utiliser les regex d’appelle re. Celle-ci contient la fonction search(pattern, str) qui recherchera la présence du motif dans une chaine de caractères. Si il est trouvé, la fonction retournera un objet sinon None. Nous pourrons donc récupérer le retour dans une variable et si elle contient quelque chose, extraire les informations et sinon signaler que la saisie n’est pas valide. Bien entendu, nous ferons ceci dans une fonction.

Notre base de code est alors la suivante :

import re

DICE_PATTERN = "(\d+)[dD](\d+)"

def parse_dice_request(sentence):
    result = re.search(DICE_PATTERN, sentence)
    if not result:
        raise ValueError(f"Dice pattern not found in string {sentence}")

    pass  # Nous allons compléter. 

Une petite précision. J’ai énoncé dans le paragraphe précédent que si on avait un résultat, on l’exploitait, sinon, on levait une exception. Cette formulation devrait conduire à un if/else. Mais ce n’est pas tout à fait exacte. L’exception arrêtant la fonction, nous allons faire l’inverse : si il n’y a pas de résultat, on arrête le traitement en levant une exception. Le sinon est inutile car implicite si… on n’arrête pas.

Extraire une information d’une regex

Si le motif a été trouvé, cela signifie que les informations dont nous avons besoin sont présentes. Nous allons pouvoir les extraire grâce à la méthode group() de l’objet retourné. Cette méthode prends en paramètre un entier strictement positif correspondant au numéro du groupe dans l’ordre où ils sont déclarés. Ainsi, result.group(1) correspond aux nombre de dés et result.group(2) au nombre de faces. Chaque chaine retournée contiendra uniquement des caractères représentant des chiffres. Nous pouvons donc directement retourner ces valeurs, transtypées en entiers, et la fonction devient :

def parse_dice_request(sentence):
    result = re.search(dice_pattern, sentence)
    if not result:
        raise ValueError(f"Dice pattern not found in string {sentence}")

    return int(result.group(1)), int(result.group(2))

Il ne nous reste plus qu’à adapter la fonction de lancer des dés.

Adapter le code principal

Notre fonction d’interaction montre surtout comment utiliser ces deux fonctions. Le début permet simplement d’obtenir une chaine de caractères d’un utilisateur.

def interaction_loop():
    while True:
        answer = input("Lancer des dé ? (q pour quitter) : ")
        if answer in "qQ":
            break

        try:
            dices_number, dices_face = parse_dice_request(answer)
            dices_values = roll_dice(dices_number, dices_face)
            print(f"Valeurs : {dices_values}")
            print(f"Total : {sum(dices_values)}")
        except ValueError:
            print("Description non reconnue")

Le gros changement est entre les lignes 7 et 13. Ici, nous gérons simplement toutes les erreurs levées lors de la tentative de jet de dés. L’idéal serait d’afficher un récapitulatif de l’attendu, mais ce n’est pas tout à fait le sujet de ce post.

Le reste du code peut se passer de commentaires. On récupère les valeurs saisies à la ligne 8 et on les utilise pour le lancer de dé ligne suivante. Ligne 10, on affiche les résultats et leur somme (car c’est aussi ce qui peut être attendu) ligne 11.

Arranger le code (encore)

Il reste une dernière étape avant de publier ce petit projet : mettre tout ça au propre. Dans l’absolu, on pourrai ranger dans des modules (fichiers) dédiés. Cependant, dans l’état, c’est à dire pour la taille de ce petit projet, c’est tout à fait acceptable.

Il est par contre indispensable de documenter les fonctions. Documenter correctement nécessite un article dédié qui pour l’instant, manque à mon blog. Je vous le mettrai ici en référence dès sa rédaction. Le code publié sur mon Github est documenté.

En conclusion

Vous avez vu comment développer cette petite application à l’aide des bibliothèque random et re. Une notion importante abordée ici est la séparation des responsabilités. Une fonction est dédiée à l’analyse de la saisie, une autre au tirage et un troisième composant à l’interaction.

Le code complet est disponible sur mon Github. Le lien pointe normalement sur la version correspondant à cet article car ce code va évoluer avec d’autres articles.

Si vous avez aimé ce post, n’hésitez pas à laisser un commentaire ci-dessous ou sur la page Facebook 😉

À propos de... Darko Stankovski

iT guy, photographe et papa 3.0, je vous fais partager mon expérience et découvertes dans ces domaines. Vous pouvez me suivre sur les liens ci-dessous.

Vous aimerez aussi...

3 réponses

  1. Mulberry dit :

    Pratique merci Darko, ça va nous servir pour jouer à distance à des wargames.

  1. 10 juillet 2022

    […] avoir conçu une application de lancer de dés, il faudrait la rendre plus conviviale. Et par convivial, on entends en général avec une […]

Laisser un commentaire