mini-jeu terminal Python curses

Mini-jeu terminal Python curses : Créer votre jeu de serpent

Tutoriel Python🎯 Intermédiaire

Mini-jeu terminal Python curses : Créer votre jeu de serpent

Lorsque vous parlez de mini-jeu terminal Python curses, vous pénétrez dans un univers fascinant où la programmation rencontre l’art ludique dans les limites graphiques du terminal. Ce concept permet de construire des jeux interactifs et fonctionnels sans nécessiter de bibliothèques graphiques lourdes comme Pygame. Nous allons explorer ensemble ce mécanisme puissant, très apprécié des développeurs qui souhaitent des démonstrations élégantes et légères.

Ce type de développement est particulièrement utile pour comprendre les boucles de jeu, la gestion d’état et la manipulation de la console, des compétences fondamentales pour tout développeur Python. Que vous souhaitiez créer un jeu de type Snake, Pong, ou même un MUD (Multi-User Dungeon), la maîtrise du mini-jeu terminal Python curses est une étape logique et motivante dans l’apprentissage de Python avancé. Cet article s’adresse aux développeurs ayant déjà des notions solides en Python et désireux de se confronter à des défis de niveau interactif.

Pour cette immersion, nous allons d’abord définir les prérequis techniques nécessaires pour manipuler le terminal. Ensuite, nous plongerons au cœur des concepts théoriques de curses, en comprenant comment il redéfinit l’affichage de manière non-bloquante. Après avoir analysé la source complète du jeu de Snake, nous détaillerons l’explication ligne par ligne. Nous explorerons enfin des cas d’usage avancés, vous montrant comment étendre ce mini-jeu dans un projet réel, tout en listant les erreurs courantes à éviter. Préparez-vous à faire passer votre Python au niveau interactif !

mini-jeu terminal Python curses
mini-jeu terminal Python curses — illustration

🛠️ Prérequis

Pour réussir à construire un mini-jeu terminal Python curses, certains outils et connaissances de base sont indispensables. La manipulation du terminal est un sujet qui demande de la précision. Voici un guide détaillé des prérequis.

Prérequis Techniques Essentiels

  • Connaissances Python : Vous devez maîtriser les structures de contrôle de base (boucles for, while), les fonctions, les classes (POO), et la gestion des exceptions (try...except). La compréhension de la programmation orientée objet est fortement recommandée pour structurer un jeu complexe.
  • L’environnement de travail : Un terminal Linux ou macOS est idéal, car les mécanismes de contrôle de curseurs sont mieux supportés. Sous Windows, l’utilisation de WSL (Windows Subsystem for Linux) est recommandée pour garantir la compatibilité avec curses.

Installation des librairies

Heureusement, la librairie curses est généralement incluse dans les installations standard de Python sur les systèmes Unix-like. Si vous utilisez un environnement virtuel (fortement conseillé), aucune installation n’est nécessaire pour curses, mais vous devez vous assurer d’avoir une version récente de Python (3.8+).

Pour garantir un environnement propre et reproductible, utilisez l’outil de gestion de paquets virtuel :

python3 -m venv venv

Activez ensuite votre environnement :

source venv/bin/activate

Vérifiez la version de Python :

python --version

Version recommandée : Python 3.10 ou supérieur. Cette version assure une meilleure gestion des asynchronismes et des fonctionnalités modernes du langage, ce qui facilite grandement l’écriture d’un mini-jeu terminal Python curses.

📚 Comprendre mini-jeu terminal Python curses

Comprendre le mini-jeu terminal Python curses, ce n’est pas juste savoir imprimer du texte. C’est maîtriser l’état et la redéfinition de l’affichage de manière extrêmement contrôlée. Historiquement, avant l’ère des frameworks graphiques, le terminal était le support graphique par défaut. Les mécanismes comme curses permettent de contourner les limites du simple print(), qui traite chaque sortie comme un événement séparé, défilant l’écran. curses, quant à lui, permet de gérer des « pairs » de coordonnées (X, Y) et de savoir exactement où chaque caractère doit apparaître, permettant ainsi de créer des images animées, comme un serpent se déplaçant.

Le cœur du système repose sur la notion de *redrawing* et de *non-bloquage*. Au lieu de laisser le programme se bloquer en attendant un appui de touche, curses initialise un mode interactif où les événements sont capturés en arrière-plan. Cela simule le comportement d’un système graphique : l’état est mis à jour, et l’interface est rafraîchie par un seul appel de rendu. Imaginez que votre terminal est une feuille de dessin qui est effacée et redessinée à chaque *tick* du jeu, plutôt que de simplement écrire des lignes les unes sous les autres.

Fonctionnement interne de curses et l’état du jeu

Le mécanisme de curses repose sur un concept d’espace mémoire virtuel appelé le *window*. Au lieu d’écrire directement sur le terminal, nous écrivons sur cette fenêtre virtuelle, et c’est curses.refresh() qui force le rendu effectif sur l’écran réel. Ceci est fondamental pour les jeux : cela permet de faire en sorte que le serpent ne laisse pas de traînées de caractères derrière lui, mais qu’il semble simplement *apparaître* à un nouveau point.

  • Initialisation (initscr()) : Met le terminal en mode non-canonique, ce qui signifie que les sauts de ligne automatiques et le traitement des caractères comme des entrées de lignes ne sont plus actifs, nous donnant un contrôle total.
  • Gestion des positions (move(y, x)) : Permet de placer le curseur exactement à l’endroit souhaité sur la grille de jeu.
  • Saisie asynchrone (nodelay()) : Permet au programme de vérifier s’il y a une entrée de touche sans attendre que l’utilisateur appuie sur Entrée, vital pour le mouvement continu dans un mini-jeu terminal Python curses.

En comparaison, d’autres langages comme C avec ncurses implémentent des concepts similaires. Python rend cette abstraction plus simple et plus « Pythonique ». Le piège à éviter est de confondre la *commande* de dessin (placer le caractère) et la *synchronisation* (attendre l’événement ou le temps écoulé). La combinaison de getch() ou nodelay() avec time.sleep() est la clé pour simuler le temps de jeu.

mini-jeu terminal Python curses
mini-jeu terminal Python curses

🐍 Le code — mini-jeu terminal Python curses

Python
import curses
import time

# --- Constantes du Jeu ---
TAILLE_INITIMALE = 20
DELAI = 0.1  # Vitesse du jeu en secondes

def obtenir_premiere_coordonnee(h, l, w, x, y):
    """Vérifie si le joueur est bien à l'intérieur du bord."""
    return (x > 0 and x < w - 1) and (y > 0 and y < h - 1)

def run_game(stdscr):
    """Fonction principale qui initialise et exécute le mini-jeu Snake."""
    global TAILLE_INITIMALE
    
    # 1. Configuration de curses
    curses.curs_invisible() # Cache le curseur du terminal
    stdscr.nodelay(1)     # Ne bloque pas l'entrée (non-bloquant)
    stdscr.timeout(100)    # Timeout de 100ms pour la gestion du délai

    # Initialisation de l'écran
    h, w = stdscr.getmaxyx()
    if h < 10 or w < 20: # Cas limite de taille d'écran trop petite
        stdscr.addstr(0, 0, "Terminal trop petit pour lancer ce mini-jeu.")
        stdscr.refresh()
        time.sleep(2)
        return

    # Initialisation du serpent (représenté par une liste de coordonnées [y, x])
    snake = [h // 2, w // 2]
    tail_len = 3
    direction = curses.KEY_RIGHT # Direction initiale
    score = 0
    
    # Placement de la nourriture au centre
    food = [h // 2, w // 2]
    
    # 2. Boucle principale du jeu
    while True:
        stdscr.clear() # Nettoie l'écran à chaque tick
        
        # --- Gestion des Inputs et Mouvement --- 
        # Récupère les touches pressées sans bloquer
        key = stdscr.getch()
        
        # Mise à jour de la direction en gérant l'inversion de direction instantanée
        if key == curses.KEY_LEFT and direction != curses.KEY_RIGHT:
            direction = curses.KEY_LEFT
        elif key == curses.KEY_RIGHT and direction != curses.KEY_LEFT:
            direction = curses.KEY_RIGHT
        elif key == curses.KEY_UP and direction != curses.KEY_DOWN:
            direction = curses.KEY_UP
        elif key == curses.KEY_DOWN and direction != curses.KEY_UP:
            direction = curses.KEY_DOWN
        elif key == ord('q'):
            break # Sortie contrôlée

        # Calcul de la nouvelle tête
        new_head_y = snake[0] - (1 if direction == curses.KEY_UP else (0 if direction == curses.KEY_DOWN else (1 if direction == curses.KEY_RIGHT else (-1))))
        new_head_x = snake[1] - (1 if direction == curses.KEY_LEFT else (0 if direction == curses.KEY_RIGHT else (1 if direction == curses.KEY_UP else (-1))))

        # 3. Gestion des Collisions (Murs et Soi-même)
        if (new_head_y <= 0 or new_head_y >= h - 1 or 
            new_head_x <= 0 or new_head_x >= w - 1 or 
            (new_head_y, new_head_x) in snake[:-1]): # Vérifie les murs et le corps
            break # Game Over

        # Mise à jour de la tête du serpent
        snake.insert(0, [new_head_y, new_head_x])
        
        # 4. Gestion de la Nourriture et de la Croissance
        if new_head_y == food[0] and new_head_x == food[1]:
            score += 10
            # Génère une nouvelle nourriture, assurant qu'elle n'est pas sur le serpent
            while True:
                food = [curses.randint(1, h - 2), curses.randint(1, w - 2)]
                if (food[0], food[1]) not in snake:
                    break
            snake.append([food[0], food[1]]) # Ajoute la nourriture comme segment temporaire
        else:
            # Le serpent bouge, on retire la queue
            snake.pop()
        
        # 5. Dessin de l'état
        stdscr.addstr(0, 0, f"SCORE: {score} | Direction: {'UP' if direction == curses.KEY_UP else 'DOWN' if direction == curses.KEY_DOWN else 'LEFT' if direction == curses.KEY_LEFT else 'RIGHT'}")
        
        # Dessin de la nourriture
        stdscr.addch(food[0], food[1], '@')
        
        # Dessin du serpent
        for i, segment in enumerate(snake):
            y, x = segment
            char = 'O' if i == 0 else 'o' # Différenciateur tête/corps
            stdscr.addch(y, x, char)

        stdscr.refresh() # Affichage final de la grille

    # --- Fin du Jeu --- 
    stdscr.addstr(h // 2, w // 2 - 10, "
GAME OVER!")
    stdscr.addstr(h // 2, w // 2 - 10 + 1, f"Votre score final : {score}")
    stdscr.addstr(h // 2, w // 2 - 10 + 2, "Appuyez sur Entrée pour quitter.")
    stdscr.refresh()
    stdscr.getch()

if __name__ == "__main__":
    try:
        # Wrapper pour gérer l'initialisation et le nettoyage de curses
        curses.wrapper(run_game)
    except Exception as e:
        # Gestion des erreurs de curses
        print(f"Erreur lors de l'exécution du mini-jeu : {e}")

📖 Explication détaillée

L’approche pour créer un mini-jeu terminal Python curses est beaucoup plus complexe qu’un simple script console. Elle exige une compréhension profonde de la gestion du temps et de l’état. Nous allons parcourir le code principal pour en décortiquer chaque mécanisme.

Analyse Détaillée du Code de Snake

Le cœur du jeu réside dans la fonction run_game(stdscr), qui est le wrapper curses. L’objet stdscr est l’interface de dessin de curses et ne doit pas être mélangé avec les variables de jeu.

Le point de départ crucial se trouve au début, concernant la configuration :

  • stdscr.nodelay(1) : Cette ligne est vitale. Sans elle, l’appel stdscr.getch() bloquerait l’exécution en attendant qu’une touche soit pressée. En mode non-bloquant, le programme continue même si aucune entrée n’est détectée, permettant ainsi de faire avancer le jeu même sans input utilisateur.
  • stdscr.timeout(100) : Il définit le délai maximal d’attente pour getch() à 100 millisecondes. Cela garantit que la boucle principale ne s’arrête pas complètement, permettant un mouvement fluide et régulier (une boucle de 100ms est une bonne fréquence de jeu).

Ensuite, la boucle principale while True gère le cycle de jeu. Le passage de l’état précédent à l’état suivant est le principe même d’un mini-jeu terminal Python curses.

Pour le mouvement, l’utilisation de snake.insert(0, [new_head_y, new_head_x]) simule la tête qui arrive, et snake.pop() simule le retrait de la queue. Cette gestion de la liste (représentant la colonne vertébrale du serpent) est le pattern de base de tout jeu de type « snake » ou « larve ».

Le Défi des Coordonnées et des Collisions

L’étape de vérification des collisions (if ... or (new_head_y, new_head_x) in snake[:-1]) est un exemple parfait de gestion d’état. Nous ne vérifions pas seulement les murs, mais aussi si la nouvelle tête se trouve déjà sur un segment du corps. Le snake[:-1] est une astuce pythonique qui exclut la tête courante de la liste pour éviter la détection immédiate de collision avec soi-même.

Le dessin final avec stdscr.addch(y, x, char) est le moment où toutes les modifications virtuelles sont rendues visibles. La commande stdscr.refresh() valide l’état final de la grille de jeu. Utiliser stdscr.clear() au début du cycle est une nécessité pour effacer l’ancien état avant de dessiner le nouvel état, simulant ainsi l’animation.

  • Astuce Technique : Pour améliorer les performances dans des jeux très rapides, il est souvent préférable d’utiliser un tampon mémoire (buffer) de curses plutôt que de rafraîchir directement, même si l’effet est similaire pour un mini-jeu simple.
  • Piège à éviter : Ne jamais mettre stdscr.clear() et stdscr.refresh() au même endroit s’ils sont appelés trop souvent ou sans raison, car cela peut ralentir considérablement le jeu.

🔄 Second exemple — mini-jeu terminal Python curses

Python
import curses
import time
# Assume que run_game() et la fonction de mouvement existent

def reset_food_location(snake, h, w):
    """Tente de placer la nourriture en dehors de la zone occupée par le serpent."""
    # Recherche de coordonnées non occupées
    occupied = set(snake)
    attempts = 0
    while attempts < 50:
        y = curses.randint(1, h - 2)
        x = curses.randint(1, w - 2)
        if (y, x) not in occupied:
            return [y, x]
        attempts += 1
    return [0, 0] # Fallback en cas d'échec (ne devrait pas arriver)

def booster_jeu(stdscr, snake, food):
    """Cas d'usage avancé : Implémentation d'un multiplicateur de score temporaire."""
    stdscr.addstr(h-1, 0, "!! BONUS ACTIF : x2 score !!")
    stdscr.refresh()
    
    # Simule la détection du bonus lors du prochain cycle
    new_head_y = snake[0] - (1 if direction == curses.KEY_UP else (0 if direction == curses.KEY_DOWN else (1 if direction == curses.KEY_RIGHT else (-1))))
    new_head_x = snake[1] - (1 if direction == curses.KEY_LEFT else (0 if direction == curses.KEY_RIGHT else (1 if direction == curses.KEY_UP else (-1))))
    
    if new_head_y == food[0] and new_head_x == food[1]:
        # L'ancien code donnait 10 points. On double.
        return 20
    return 0

# Ceci nécessite d'intégrer les fonctions de mouvement/scoring du code_source pour être exécuté.
# Ce snippet montre le pattern de bonus, où le score n'est pas simplement cumulatif.

▶️ Exemple d’utilisation

Imaginons que nous voulions lancer notre jeu Snake. Le scénario est simple : l’utilisateur doit naviguer à travers le terminal, éviter les murs et le corps du serpent, tout en mangeant la nourriture pour maximiser son score. Nous allons appeler simplement la fonction principale qui gère le cycle de vie du mini-jeu terminal Python curses.

Le code d’appel est minimaliste, car la complexité est encapsulée dans le wrapper curses :

if __name__ == "__main__":
try:
curses.wrapper(run_game)
except KeyboardInterrupt:
print("Jeu interrompu manuellement.")

Lorsque ce code est exécuté, plusieurs choses se passent en coulisses : le terminal est mis en mode jeu, le jeu commence et la boucle s’exécute jusqu’à collision ou arrêt de l’utilisateur. La sortie console elle-même est une série d’images redessinées à haute fréquence, ce qui donne l’illusion d’une animation fluide.

La sortie console attendue ne ressemble pas à un simple log de texte, mais à une capture d’écran figée du moment où le jeu s’arrête :

SCORE: 150 | Direction: RIGHT
Oooooooo@oOOOOooOO
OoOoooooo@oooOooO
Oooooooo@OOOOooooO
OooooO@oOOOOoOooO
OOOOo@oooOOOOooO
Oooo@oooo@oooooooo
Oooooo@ooooooooo
------------------------------
GAME OVER!
Votre score final : 150
Appuyez sur Entrée pour quitter.

Cette sortie signifie que le serpent est mort (collision), le panneau affiche le score final (150 points), et le mini-jeu terminal Python curses a correctement géré l’arrêt du cycle de jeu et le nettoyage de l’écran après le « Game Over ».

🚀 Cas d’usage avancés

Le mini-jeu terminal Python curses n’est pas limité à Snake. Il peut devenir la base de simulations complexes, de jeux de rôle (RPG textuels) ou même de visualisations de protocoles réseau. Voici trois cas d’usage avancés pour pousser votre maîtrise des concepts de curses.

1. Simulation de Mini-GPS en Temps Réel

Au lieu de déplacer un simple serpent, vous pouvez faire avancer un marqueur (l’avatar) sur une carte de grille prédéfinie (représentant un réseau routier). L’objectif est de trouver le chemin le plus court tout en gérant des obstacles (feux rouges, zones bloquées). Vous utiliserez la logique de recherche de chemin comme l’algorithme A* (A-star), où la grille de curses représente l’état du terrain. Chaque déplacement successful est un ‘tick’ du jeu.

Exemple de code (structure uniquement) :

# Variables : 'map' (matrice de coordonnées), 'player_pos', 'target_pos'
while True:
# 1. Vérification de la validité du mouvement selon la carte
if est_valide_move(player_pos, direction, map):
player_pos = calculer_nouvelle_pos(player_pos, direction)
# 2. Redessin de la carte et du joueur
stdscr.clear()
afficher_carte(stdscr, map)
stdscr.addch(player_pos[0], player_pos[1], '@')
stdscr.refresh()
# Gestion de la défaite ou de l'arrivée
time.sleep(0.1)

Ici, le défi n’est plus le mouvement, mais la logique de la carte et la gestion des états successifs de l’environnement.

2. Visualisation de Protocole Réseau (Packet Tracer)

Vous pouvez simuler le voyage de paquets de données à travers une topologie de nœuds. Chaque nœud est une zone de la grille curses. Le paquet est un caractère qui se déplace d’un nœud à l’autre selon un protocole prédéfini (ex: UDP ou TCP). L’état du réseau (congestion, perte de paquets) est géré en modifiant les positions et les délais de redémarrage.

Le mini-jeu terminal Python curses devient un outil de *monitoring*. Vous utilisez les touches de souris pour simuler les connexions et les données pour déclencher les événements. La gestion des *timers* devient plus complexe, nécessitant des mécanismes de temps multiples au lieu de simples sleep().

3. Jeu de Plateau de Style RPG (Roguelike)

C’est le cas d’usage le plus riche. Le plateau est la grille curses, et les entités (personnages, monstres, objets) sont des objets en Python gérés par leur position (y, x). L’état du jeu doit inclure l’inventaire, les points de vie, et la carte. Chaque tour de jeu (chaque redrawing) représente une action : se déplacer, attaquer, ou interagir. La boucle principale doit gérer des événements multiples (combat, rencontre aléatoire) et non seulement le mouvement de 1 pixel.

L’abstraction orientée objet est cruciale ici. Chaque entité (Personnage, Monstre, Trésor) devrait hériter d’une classe de base qui gère ses coordonnées (y, x) et ses actions. Le système de combat peut alors être résolu par des fonctions séparées, appelées lors de la collision entre deux entités.

  • Principe de l’état : L’état du jeu doit être encapsulé dans un objet principal (GameManager) pour éviter la pollution globale des variables.
  • Gestion du temps : Le mini-jeu terminal Python curses nécessite une gestion très fine du temps pour alterner entre le tour de l’IA et l’input du joueur.

✔️ Bonnes pratiques

Pour garantir la robustesse et l’évolutivité de votre mini-jeu terminal Python curses, adhérer à certaines bonnes pratiques est essentiel. Ces conseils vous feront passer d’un simple « mini-jeu » à une véritable application de jeu de terminal.

1. Encapsulation par Classes (POO)

Ne laissez pas la logique de mouvement, de collision et de score dans la fonction principale. Créez des classes : Snake (gère le corps, le mouvement), Food (gère la position et la génération), et GameManager (orchestre la boucle, la logique de jeu et le score). Ceci améliore la lisibilité et la maintenabilité du code.

2. Séparation des Responsabilités (View vs Logic)

Séparez la logique de jeu (calcul des nouvelles coordonnées, vérification de collision) de l’affichage (dessin des caractères curses). La fonction update_state() doit juste calculer l’état suivant, et une fonction render_game(stdscr, state) doit gérer tout le dessin. Cela permet de tester la logique pure avec des outils de débogage plus simples.

3. Utilisation des Gestionnaires de Contexte (Context Managers)

Comme vu, utiliser curses.wrapper() est la meilleure pratique. Il garantit que, quelle que soit l’exception levée (même un crash), le terminal est correctement restauré à son état initial. Ne jamais gérer ce nettoyage manuellement avec des blocs try...finally, préférez le wrapper.

4. Gestion des Données de Jeu Immuables

Le score, la taille de la grille et les constantes de jeu doivent être définis en tant que constantes globales ou, mieux, passés en tant que paramètres par constructeur au GameManager, plutôt que d’être des variables modifiées au hasard dans la boucle principale. Cela renforce la prédictibilité du code.

5. Débordements de coordonnées (Bounding Box Check)

Dans chaque calcul de mouvement, effectuez une vérification immédiate des limites (murs) et des collisions avec le corps avant d’insérer les coordonnées dans la liste du serpent. Traiter ces cas limites au début du cycle de jeu rend le code plus robuste.

📌 Points clés à retenir

  • Le <code>curses.wrapper()</code> est indispensable pour garantir que le terminal soit correctement restauré après l'exécution du mini-jeu, évitant ainsi de bloquer la session.
  • Le mode non-bloquant (<code>stdscr.nodelay(1)</code>) permet au <strong style="color: #CC0000;">mini-jeu terminal Python curses</strong> d'être réactif même sans input immédiat de l'utilisateur.
  • La structure du jeu est basée sur la gestion de l'état : à chaque cycle, l'ancien état est effacé (virtuellement) et le nouvel état (coordonnées du serpent, de la nourriture) est dessiné, simulant l'animation.
  • L'utilisation d'une liste Python pour le corps du serpent est un pattern efficace (<code>LIFO</code> – Last In, First Out) : insertion à l'indice 0 (tête) et suppression à la fin (queue).
  • La vérification de collision doit être multidimensionnelle : elle doit inclure les bords de la grille et l'intersection avec chaque segment du corps du serpent.
  • La synchronisation du jeu est gérée par <code>stdscr.timeout()</code>, qui définit la fréquence (vitesse) à laquelle l'état de la grille est rafraîchi, déterminant ainsi la fluidité du mini-jeu.

✅ Conclusion

Pour conclure, la réalisation d’un mini-jeu terminal Python curses est bien plus qu’un simple exercice de programmation ; c’est une véritable initiation à la programmation graphique sous ses formes les plus fondamentales. Nous avons parcouru les concepts allant de la gestion des coordonnées (X, Y) au maintien de l’état du jeu (la liste du serpent), en passant par la subtilité du mode non-bloquant de curses. Maîtriser ce concept vous donne les clés pour créer des expériences interactives incroyablement légères, ne dépendant que de l’environnement console.

Si ce jeu de Snake vous a inspiré, considérez-le comme un tremplin. Pour aller plus loin, nous vous encourageons fortement à explorer les systèmes de gestion d’état (State Pattern) lorsque vous passez à des jeux plus complexes (combat, inventaires). Vous pourriez par exemple construire une petite carte de type JRPG textuel, en remplaçant la gestion de la nourriture par la gestion des PNJ (Personnages Non-Joueurs) et en utilisant la logique de déplacement déjà maîtrisée.

N’oubliez jamais que le code est un muscle : plus vous pratiquez, plus il devient intuitif. Revoyez ce code, modifiez la vitesse, ajoutez un système de puissance temporaire, ou même inversez la gravité pour faire tomber le serpent par gravité! Le chemin vers la maîtrise passe par l’expérimentation.

La communauté Python est riche de ressources ; nous vous recommandons de consulter la documentation Python officielle pour approfondir les options de curses. N’hésitez jamais à vous lancer dans un nouveau mini-jeu terminal Python curses. Bonne programmation, et à bientôt pour de nouveaux défis Python !

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *