Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Réalisation d'une IA jouant au blackjack avec MCTS

Présentation du blackjack

Jeu du blackjack

Principe du jeu

Le Blackjack est un jeu de cartes où le but est de battre le croupier (dealer) en obtenant une main dont la valeur est la plus proche possible de 21, sans jamais la dépasser.

Règles principales

  • Les cartes numérotées valent leur valeur (ex. 7 = 7 points).
  • Les figures (Valet, Dame, Roi) valent 10 points.
  • L’As vaut 1 ou 11 points, selon ce qui avantage le joueur.
  • Le joueur commence avec 2 cartes, tout comme le croupier (une visible, une cachée).
  • Le joueur peut :
    • Hit : tirer une nouvelle carte.
    • Stand : s’arrêter et conserver sa main actuelle.
  • Le croupier joue ensuite selon des règles fixes :
    • Il tire tant que sa main est ≤ 16.
    • Il reste à 17 ou plus.

Objectif

  • Gagner si :
    • Votre total est supérieur à celui du croupier sans dépasser 21.
    • Le croupier dépasse 21 .
  • Perdre si vous dépassez 21 ou si le croupier a une meilleure main.

Particularités de la version Gymnasium

  • Le jeu se joue dans un environnement simulé (Blackjack-v1) de la bibliothèque Gymnasium.
  • Les différents états possibles sont indépendants les uns les autres car il y a remise dans la pioche à chaque tirage.

Principe du Monte Carlo Tree Search (MCTS)

Le Monte Carlo Tree Search (MCTS) est un algorithme d’apprentissage par renforcement basé sur la simulation.
Il permet à un agent de prendre des décisions optimales en explorant un arbre de recherche et en simulant de nombreuses parties à partir des états possibles.

L’idée est d’équilibrer l’exploration (essayer de nouvelles actions) et l’exploitation (choisir les actions prometteuses déjà connues) pour estimer la meilleure décision à chaque étape.


1. Sélection (Selection)

À partir de la racine (état actuel), l’algorithme descend dans l’arbre en sélectionnant les nœuds selon une politique d’équilibre entre exploration et exploitation, à l’aide de la formule UCB1 (Upper Confidence Bound) :

  • : somme des récompenses du nœud i
  • : nombre de visites du nœud i
  • : nombre total de simulations depuis le nœud parent
  • : constante d’exploration (généralement 2)

2. Expansion (Expansion)

Si le nœud sélectionné n’est pas terminal, on ajoute un ou plusieurs nouveaux nœuds enfants correspondant aux actions possibles depuis cet état.

3. Simulation (Rollout)

Une simulation aléatoire est exécutée à partir du nouvel état jusqu’à la fin de la partie (ou un horizon fixé).
Le résultat (victoire, défaite, score) fournit une évaluation de cet état.

4. Rétropropagation (Backpropagation)

Le résultat de la simulation est propagé en remontant l’arbre : chaque nœud met à jour son nombre de visites et son score moyen.

À force de répétitions, le MCTS améliore ses estimations pour chaque action.
L’action optimale depuis l’état initial est généralement celle avec le plus grand nombre de visites ou la meilleure moyenne de récompense.


Structure de notre IA

L’intelligence artificielle repose sur deux classes principales.

La classe State

Cette classe représente un état du jeu, c’est-à-dire la situation actuelle dans une partie de Blackjack : les cartes du joueur, la carte visible du croupier, et la présence éventuelle d’un as compté comme 11.

Elle contient plusieurs attributs essentiels.
L’attribut state correspond à l’état du jeu tel qu’il est fourni par l’environnement Gymnasium.
visits indique combien de fois cet état a déjà été rencontré au cours des simulations.
actions regroupe les deux choix possibles dans une partie de Blackjack : « stand » (Action(0)) ou « hit » (Action(1)).
Enfin, __utc_c est une constante d’exploration utilisée dans le calcul de la valeur UCB, qui permet de gérer le compromis entre exploration et exploitation.

La méthode UCB_value(self, action) calcule la valeur UCB (Upper Confidence Bound) pour une action donnée. C’est à cet endroit que se décide l’équilibre entre tester de nouvelles actions et exploiter celles qui semblent déjà prometteuses.

La méthode interne __select_action(self) choisit l’action à jouer selon les valeurs UCB ou, si l’état n’a encore jamais été visité, de manière aléatoire. Concrètement, si self.visits vaut 0, on n’a encore aucune donnée, donc l’action est tirée au hasard. Sinon, on compare les deux valeurs UCB (pour les actions 0 et 1) et on garde celle qui a la meilleure valeur. En cas d’égalité parfaite, une action aléatoire est choisie afin de conserver un minimum d’exploration.

La méthode select_learning_action(self) sert d’interface publique pour la sélection d’une action pendant la phase d’apprentissage. Elle se contente d’appeler __select_action() et applique donc la logique du MCTS avec exploration activée.

La méthode select_operating_action(self) est utilisée quand l’IA joue réellement, c’est-à-dire lorsqu’elle doit exploiter pleinement ce qu’elle a appris sans explorer. Pour cela, la constante d’exploration __utc_c est temporairement désactivée, puis l’action est choisie en fonction des valeurs moyennes observées. Une fois la sélection faite, la valeur d’origine de __utc_c est rétablie. Ce procédé permet à l’IA de prendre la meilleure décision possible à partir de son expérience accumulée.

Enfin, la méthode backtraking(self) incrémente simplement le compteur de visites de l’état (self.visits += 1). Elle est appelée à la fin de chaque simulation afin de mettre à jour l’arbre de recherche.

Code source :

import gymnasium as gym
import math
import random

# specificly design for gymnasium blackjack , previous step doesn't influence the outcom of current
# so we redesigned some part of classical MCST to be optimized to this challenge !
class State:
    __utc_c = 1
    def __init__(self, state):
        self.state = state            
        self.visits = 0
        self.actions = [Action(0),Action(1)]

    
    # pré-requis : self.visits > 0
    def UCB_value(self,action):
        act_visits = max(1,action.get_visits()) # pour éviter la division par 0
        act_value = action.get_value()
        act_avg = act_value/act_visits

        exploration = math.sqrt(self.__utc_c*math.log(self.visits)/act_visits)

        return act_avg + exploration
    
    def choose_random_action(self):
        if random.uniform(0,1) >= 0.5:
            return self.actions[0]
        else:
            return self.actions[1]
    
    def __select_action(self):
        if self.visits == 0:
            return self.choose_random_action()
        
        act0_UCB = self.UCB_value(self.actions[0])
        act1_UCB = self.UCB_value(self.actions[1])

        if act0_UCB > act1_UCB :
            return self.actions[0]
        elif act0_UCB == act1_UCB:
            return self.choose_random_action()
        else:
            return self.actions[1]
        
    def select_learning_action(self):
        return self.__select_action()
    
    def select_operating_action(self):
        temp = self.__utc_c
        self.__utc_c = 0
        a =  self.__select_action()
        self.__utc_c = temp
        return a
    
    def backtraking(self):
        self.visits = self.visits + 1

La classe Action

La classe Action représente une action possible dans une partie de Blackjack. Elle correspond concrètement au choix que l’agent peut faire à un instant donné : tirer une nouvelle carte (hit) ou s’arrêter (stand). Chaque objet Action garde en mémoire ses statistiques individuelles au fil des simulations.

Lors de son initialisation, la classe reçoit un paramètre order qui identifie l’action à effectuer dans l’environnement (0 pour stand, 1 pour hit). Trois attributs sont ensuite définis :

  • order, qui indique le type d’action,

  • visits, le nombre de fois où cette action a été jouée depuis cet état,

  • value, la somme totale des récompenses obtenues lorsque cette action a été choisie.

Plusieurs méthodes permettent d’accéder à ces informations.
get_order() renvoie simplement le numéro associé à l’action.
get_visits() retourne le nombre de fois où cette action a été essayée.
get_value() donne la valeur totale accumulée grâce aux récompenses obtenues.

La méthode backtracking(self, reward) est appelée après une simulation, lorsque le résultat (ou la récompense) de l’action est connu. Elle met à jour les statistiques associées en incrémentant le compteur de visites et en ajoutant la récompense reçue à la valeur totale. C’est grâce à cette mise à jour que l’algorithme peut progressivement estimer la qualité réelle de chaque action et améliorer sa prise de décision au fil des parties.

Code source :

class Action:
    def __init__(self,order): 
        self.order = order   # la valeur de l'action dans l'environnement     
        self.visits = 0
        self.value = 0.0

    def get_order(self):
        return self.order
    
    def get_visits(self):
        return self.visits
    
    def get_value(self):
        return self.value
    
    def backtracking(self,reward):
        self.visits = self.visits + 1
        self.value += reward

Dans l'ensemble l'IA est un set de l'ensemble des states que l'on crée au cours du temps


Fonctionnalités développées

Nous avons développé 4 grandes fonctionnalités :

Entraînement de l’IA

Tout commence avec la fonction ai_training(). Celle-ci crée d’abord un environnement de jeu Blackjack grâce à la bibliothèque Gymnasium. L’utilisateur choisit ensuite combien de parties l’IA doit jouer pour s’entraîner. Ce nombre d’épisodes détermine la durée et la qualité de l’apprentissage : plus il est élevé, plus l’agent aura exploré de scénarios possibles.

Au début de chaque partie, on réinitialise le jeu et on crée un dictionnaire dic_node_game qui va garder en mémoire les états rencontrés et les actions associées pendant cette partie. Ce dictionnaire est temporaire et sera utilisé plus tard pour mettre à jour les statistiques.

Pendant le déroulement du jeu, l’IA observe l’état actuel (c’est-à-dire les cartes du joueur, la carte visible du croupier et la présence d’un as utilisable). Si cet état n’a jamais été rencontré auparavant, on le crée et on l’ajoute à la liste des états découverts.
Ensuite, l’agent choisit une action à l’aide de la méthode select_learning_action() de la classe State.

L’environnement exécute alors cette action grâce à la fonction env.step(), qui renvoie le nouvel état, la récompense obtenue et des indicateurs indiquant si la partie est terminée. Si le jeu n’est pas encore fini, l’IA continue à jouer jusqu’à ce que la manche se conclue.

Quand la partie se termine, le programme affiche la récompense finale (+1.5 pour un blackjack,+1 pour une victoire, -1 pour une défaite, 0 pour une égalité). Ensuite, la phase de mise à jour commence : pour chaque couple (état, action) rencontré pendant la partie, on appelle les méthodes backtraking() sur l’état et backtracking(reward) sur l’action. Ces deux appels servent à incrémenter les compteurs de visites et à mettre à jour les valeurs de récompense moyenne. C’est ce mécanisme qui permet à l’IA d’apprendre quelles décisions mènent le plus souvent à la victoire.

Une fois toutes les parties d’entraînement terminées, l’ensemble des états et des actions explorés est sauvegardé grâce à la classe AI_saver. Cela permet de réutiliser plus tard le modèle entraîné sans devoir recommencer tout le processus.

Extrait du code source :

def ai_training():
    descovered_states = {}

    # Utilisation de Gymnasium pour l'environnement de jeu
    env = gym.make("Blackjack-v1")

    n_episodes_train = ask_number("Please choose the number of game that the ai should train on")
    reward = 0
    for episode in range(n_episodes_train):
        print("#################################################")
        print(f"nouvelle épisode {episode}")
        state, info = env.reset() # Nouvelle partie
        reward = 0
        done = False

        dic_node_game = {} # Un dictionnaire State -> Action qui retient le déroulement de la partie

        while not done:
            if state not in descovered_states:
                descovered_states[state] = State(state)
    
            node_state = descovered_states[state]

            action = node_state.select_learning_action()

            dic_node_game[node_state] = action
    
        
            # On joue l'action proposer par l'IA et récupère le retour de l'environnement
            next_state, reward, terminated, truncated, info = env.step(action.get_order())
            done = terminated or truncated

            state = next_state
    
        # en dehors du while, la partie est finie
        print(f"Episode finished! Total reward: {reward}")
        # On rétropropage le reward de la partie
        for n_state,n_action in dic_node_game.items():
            n_state.backtraking()
            n_action.backtracking(reward)
    ai_saver.save(descovered_states,game_name,n_episodes_train)
    env.close()

Gestion des IA et sauvegarde des modèles

Pour éviter de devoir réentraîner une intelligence artificielle à chaque exécution, une partie essentielle du projet consiste à pouvoir sauvegarder et recharger une IA déjà entraînée. C’est exactement le rôle de la classe AI_saver, qui s’appuie sur le module Pickle de Python. L’objectif est de rendre la gestion des IA simple, pratique et flexible, tout en permettant de conserver plusieurs versions d’un même modèle.

Le fonctionnement général est le suivant : lorsqu’une IA termine sa phase d’apprentissage, l’utilisateur peut la sauvegarder dans un dossier spécial nommé .ai_saves. À ce moment-là, le programme lui demande de choisir un nom pour la retrouver plus facilement par la suite. Le fichier est ensuite automatiquement complété par la date et l’heure de la sauvegarde, ainsi que par le nombre d’épisodes d’entraînement effectués. Cela donne des noms de fichiers à la fois uniques et informatifs, comme par exemple
blackjack_ai_test_02-11-2025--17:30_trained_5000_times.plk.

Si le dossier n’existe pas encore, il est créé automatiquement grâce à os.makedirs. Une fois le nom final prêt, l’objet représentant l’IA est sauvegardé dans un fichier binaire grâce à la fonction pickle.dump().

Pickle est un module standard de Python qui permet de sérialiser et désérialiser des objets. La sérialisation, c’est le processus qui consiste à transformer un objet Python (par exemple une instance de classe contenant des dictionnaires, des listes et même d’autres objets) en une suite d’octets. Ces octets peuvent ensuite être enregistrés dans un fichier ou envoyés à travers un réseau. La désérialisation, c’est l’opération inverse : à partir du fichier binaire, Pickle reconstitue exactement l’objet d’origine, avec toutes ses valeurs, sa structure et son état interne.

Concrètement, quand on écrit pickle.dump(ai, f), Pickle parcourt récursivement tous les attributs de l’objet ai, traduit leur contenu en une forme binaire, et les écrit dans le fichier ouvert en mode wb (write binary). Lors du chargement avec ai = pickle.load(f), le module lit ces mêmes octets et reconstruit un objet Python identique à celui qui avait été sauvegardé, en restaurant toutes les références et les valeurs. C’est ce qui permet de retrouver une IA exactement dans l’état où elle était après son apprentissage, comme si elle n’avait jamais été arrêtée.

Il faut cependant noter que Pickle est spécifique à Python. Les fichiers produits ne sont pas lisibles directement par d’autres langages, et il ne faut pas charger un fichier Pickle provenant d’une source non fiable, car le processus de désérialisation peut exécuter du code malveillant.

La méthode load() de la classe AI_saver permet de restaurer une IA précédemment sauvegardée. Elle commence par lister tous les fichiers disponibles dans le dossier .ai_saves, éventuellement filtrés selon le nom du jeu, comme "blackjack". Cette recherche se fait grâce à la fonction list_files(), qui utilise le module glob pour parcourir les fichiers avec l’extension .plk.

Le programme affiche ensuite la liste des fichiers trouvés et demande à l’utilisateur de choisir celui qu’il souhaite recharger. Une fois la sélection faite, le fichier est ouvert en lecture binaire et passé à pickle.load(), qui reconstruit l’objet complet en mémoire. L’IA ainsi rechargée est ensuite renvoyée pour être utilisée dans les autres fonctions du programme, comme la visualisation ou la comparaison de stratégies.

Ce système de sauvegarde et de restauration rend la gestion des intelligences artificielles bien plus simple. Il devient possible d’entraîner plusieurs modèles différents, de les comparer entre eux ou de reprendre l’apprentissage là où on s’était arrêté, sans perte de données.

Code source :

import pickle
from datetime import datetime
import glob
import os
import sys

# retourne tous les fichiers dans le directory qui contiennent substring ou tous si None
def list_files(directory, substring=None):
    all_files = glob.glob(os.path.join(directory, "*.plk"))
    all_files = [f for f in all_files if os.path.isfile(f)]
    if substring:
        all_files = [f for f in all_files if substring in os.path.basename(f)]
    return all_files

def ask_number_in_list(msg, values_list):
    while True:
        try:
            val = int(input(f"{msg} {values_list} : "))
            if val in values_list:
                return val
            else:
                print(f"Wrong input. Please choose beetween {values_list}.")
        except ValueError:
            print("Invalid input. Please choose a number.")


class AI_saver:
    folder_name = ".ai_saves"
    def __init__(self):
        pass

    def save(self,ai,game_name="no_specified",nb_training=None):

        # construction du nom du fichier
        now = datetime.now()
        result = input("Choose a name to find more easely your ai :")
        name = self.folder_name+"/"+game_name+"_ai_"+result # nom de base du fichier
        name = name + "_" + now.strftime("%d-%m-%Y--%H:%M") # on ajoute la date

        # ajout du nombre d'entrainement eventuellement ainsi que l'extension du fichier 
        if nb_training is None :
            name += "_no_training_given.plk"
        else:
            name += "_trained_"+str(nb_training)+"_times.plk"

        # Création du dossier si nécessaire
        os.makedirs(os.path.dirname(name), exist_ok=True)

        with open(name, "wb") as f:
            pickle.dump(ai,f)

        print(f"the ai as been save under the name {name}")
    
    def load(self,game_name=None):
        availables_files = list_files(self.folder_name,game_name)

        if len(availables_files) == 0:
            print("files not found : exiting")
            sys.exit()
        
        choices = []
        print("here is the list of file found with their number choice:")
        for i in range(len(availables_files)):
            choices.append(i)
            print(f"choice {i} : {availables_files[i]}")
        
        choice = ask_number_in_list("select the ai you want to restore : ",choices)

        print(f"You have been select the file {availables_files[choice]}")

        with open(availables_files[choice], "rb") as f:
            ai_loaded = pickle.load(f)
        
        return ai_loaded

Comparaison de l’efficacité de l’IA

Une fois l’intelligence artificielle entraînée et sauvegardée, il est important de pouvoir évaluer ses performances et de les comparer à d’autres stratégies. C’est exactement le rôle de la fonction methods_comparaison(). Elle permet de mesurer, sur un grand nombre de parties, à quel point notre IA joue mieux qu’un joueur aléatoire ou qu’une stratégie classique de croupier.

Le principe est simple : on recharge d’abord une IA déjà entraînée puis on fait jouer trois « joueurs » différents sur le même environnement de Blackjack. Le premier est l’IA, le deuxième représente le comportement typique d’un croupier (tirer une carte quand la main est de 16 ou moins, et s’arrêter sinon), et le troisième joue complètement au hasard, en choisissant aléatoirement entre « hit » et « stand ».

Pour chacun de ces joueurs, le programme lance une série de parties, dont le nombre est défini par l’utilisateur. À chaque tour, il enregistre les résultats dans un dictionnaire qui compte le nombre de victoires, de défaites, d’égalités et la somme totale des récompenses. Ces statistiques sont mises à jour grâce à la fonction count_stats(), qui ajoute +1 dans la catégorie correspondante selon le résultat de la partie et cumule le score total pour calculer une moyenne à la fin.

Pendant les tests, l’IA joue de manière purement déterministe grâce à la méthode select_operating_action() de la classe State, c’est-à-dire qu’elle choisit toujours l’action qu’elle estime la meilleure, sans aucune exploration. Cette approche permet d’évaluer objectivement la qualité de son apprentissage, sans l’influence du hasard. En revanche, le joueur aléatoire sert de référence minimale, et la stratégie du croupier donne une idée de la performance moyenne d’un joueur humain raisonnable.

Une fois toutes les parties jouées, le programme affiche un récapitulatif complet des résultats pour les trois stratégies. Il calcule les pourcentages de victoires, de défaites et d’égalités, ainsi que la récompense moyenne obtenue. Ce dernier indicateur est particulièrement important, car il reflète le gain moyen attendu à long terme.

Grâce à cette comparaison, on peut évaluer objectivement l’efficacité de l’IA. Si elle dépasse largement les deux autres méthodes, cela signifie que son apprentissage a été efficace et qu’elle a bien intégré les stratégies gagnantes du Blackjack. Ce type de test permet aussi d’ajuster le nombre d’épisodes d’entraînement nécessaires ou de vérifier si le modèle commence à surapprendre (c’est-à-dire à trop s’adapter à des cas spécifiques).

Extrait du code source :

def count_stats(dic,value):
    if value>0:
        dic["win"] += 1
    elif value<0:
        dic["lose"] += 1
    else:
        dic["draw"] += 1
    
    dic["total"] += value


def methods_comparaison():
    descovered_states = ai_saver.load(game_name)
    env = gym.make("Blackjack-v1")

    n_episodes_train = ask_number("Please choose the number of game that the ai and methods should play")

    # plein de partie de l'ia
    ai_stats = {"win":0,"draw":0,"lose":0,"total":0}
    for _ in range(n_episodes_train):
        state, info = env.reset()
        done = False

        while not done:
            if state not in descovered_states:
                descovered_states[state] = State(state)
    
            node_state = descovered_states[state]

            action = node_state.select_operating_action()
    
            next_state, reward, terminated, truncated, info = env.step(action.get_order())
            done = terminated or truncated

            state = next_state
    
        count_stats(ai_stats,reward)
    print("ai's games finished")

    # Stratégie du dealer (« hit » si <= 16 « stand » sinon)
    dealer_stats = {"win":0,"draw":0,"lose":0,"total":0}
    for _ in range(n_episodes_train):
        state, info = env.reset()
        done = False

        while not done:
            hand,a,b = state
            if hand>16:
                action = 0
            else:
                action = 1

    
            next_state, reward, terminated, truncated, info = env.step(action)
            done = terminated or truncated

            state = next_state
    
        count_stats(dealer_stats,reward)
    print("dealer's strategie games finished")
    
    # Choix aléatoire entre « hit » et « stand » à chaque fois
    random_stats = {"win":0,"draw":0,"lose":0,"total":0}
    for _ in range(n_episodes_train):
        state, info = env.reset()
        done = False

        while not done:

            if random.uniform(0,1) >= 0.5:
                action = 0
            else:
                action = 1
    
            next_state, reward, terminated, truncated, info = env.step(action)
            done = terminated or truncated

            state = next_state
    
        count_stats(random_stats,reward)
    
    # affichage des résultats et des différentes statistiques 
    print(f"Based on the {n_episodes_train} games here's our results :")
    print(f"  Ai   : {round(ai_stats["win"]*100/n_episodes_train,2)} % of win; {round(ai_stats["draw"]*100/n_episodes_train,2)} % of draw; {round(ai_stats["lose"]*100/n_episodes_train,2)} % of lose; for a reward average of {round(ai_stats["total"]/n_episodes_train,5)}")
    print(f"Dealer : {round(dealer_stats["win"]*100/n_episodes_train,2)} % of win; {round(dealer_stats["draw"]*100/n_episodes_train,2)} % of draw; {round(dealer_stats["lose"]*100/n_episodes_train,2)} % of lose; for a reward average of {round(dealer_stats["total"]/n_episodes_train,5)}")
    print(f"Random : {round(random_stats["win"]*100/n_episodes_train,2)} % of win; {round(random_stats["draw"]*100/n_episodes_train,2)} % of draw; {round(random_stats["lose"]*100/n_episodes_train,2)} % of lose; for a reward average of {round(random_stats["total"]/n_episodes_train,5)}")

    env.close()

Visualisation et interaction avec l’IA

Une fois l’intelligence artificielle entraînée, il est essentiel de pouvoir observer son comportement en situation réelle et d’interagir directement avec elle pour comprendre la logique de ses décisions. C’est tout l’intérêt des parties du programme dédiées à la visualisation et à l’interprétation des choix de l’IA. L’idée est de permettre à l’utilisateur non seulement de voir comment l’IA applique ce qu’elle a appris, mais aussi de lui poser des questions comme à un véritable joueur expérimenté.

Lorsqu’on lance la phase de visualisation, l’IA précédemment sauvegardée est rechargée depuis le disque à l’aide du module de gestion des modèles. Le programme crée alors un environnement de jeu Blackjack grâce à la bibliothèque Gymnasium, configuré en mode graphique afin que le déroulement de chaque partie soit affiché à l’écran. L’utilisateur choisit combien de parties il souhaite observer, et l’IA se met à jouer automatiquement en prenant ses décisions selon la stratégie qu’elle a apprise. Chaque état rencontré pendant la partie — c’est-à-dire la somme des cartes du joueur, la carte visible du croupier et la présence éventuelle d’un as compté comme 11 — est analysé par le modèle. L’IA choisit ensuite la meilleure action possible selon ses connaissances, qu’il s’agisse de rester ou de tirer une nouvelle carte. Un léger délai entre chaque action rend la progression plus fluide et permet de suivre clairement la logique du jeu.

Ce système offre une fenêtre graphique sur le fonctionnement interne de l’intelligence artificielle. Plutôt que de se limiter à des statistiques, on peut visualiser ses choix, constater son comportement face à des situations critiques et voir comment elle adapte sa stratégie en fonction des circonstances. Il devient alors possible d’évaluer intuitivement la cohérence de son apprentissage : si elle prend les bonnes décisions, si elle évite les risques inutiles ou au contraire s’aventure trop souvent à tirer une carte supplémentaire.

En parallèle, le programme permet aussi une interaction directe avec l’IA à travers une interface textuelle où l’utilisateur peut lui demander conseil. On indique simplement les informations de la main en cours — la somme totale de ses cartes, la carte visible du croupier et la présence ou non d’un as utilisable — et l’IA répond en proposant l’action qu’elle estime la plus judicieuse. Derrière cette simplicité, elle exploite exactement la même logique que lorsqu’elle joue seule : elle identifie l’état correspondant dans sa mémoire et applique la politique optimale qu’elle a construite durant son apprentissage. Si la situation demandée n’a jamais été rencontrée, l’IA prévient qu’elle ne connaît pas la réponse, ce qui traduit bien la limite naturelle de tout apprentissage basé sur l’expérience.

Extrait du code source :

def blackjack_visual():
    descovered_states = ai_saver.load(game_name)
    dt = 0.7 # Délai entre deux actions pour faciliter l’observation de l’IA (en secondes)
    env = gym.make("Blackjack-v1",render_mode="human")

    n_episodes_train = ask_number("Please choose the number of game that the ai should play")
    for _ in range(n_episodes_train):
        state, info = env.reset()
        done = False

        while not done:
            if state not in descovered_states:
                descovered_states[state] = State(state)
    
            node_state = descovered_states[state]

            action = node_state.select_operating_action()
    
            next_state, reward, terminated, truncated, info = env.step(action.get_order())
            done = terminated or truncated

            state = next_state
            time.sleep(dt)
    
    env.close()
    
def ai_suggestion():
    descovered_states = ai_saver.load(game_name)
    done = False
    while not done:
        choice = ask_number_in_list("select 0 to get the ai response or 1 to exit",[0,1])
        if choice == 1:
            done = True
        else:
            player = ask_number("please enter the sum of your cards")
            dealer = ask_number("please enter dealer's cards")
            ace = ask_number("please enter 1 if you have a playable ace (count as 11) or 0 otherwise")
            state = player,dealer,ace,
            if state not in descovered_states:
                print("The ai don't know the solution for this case")
            else:
                node_state = descovered_states[state]
                action = node_state.select_operating_action()
                print(f"Knowing that a 0 means a stay and a 1 a hit, the ai recommands : {action.get_order()}")

Points clés à retenir

  • MCTS + UCB : L’IA utilise la recherche Monte Carlo Tree Search (MCTS) avec un score UCB pour équilibrer exploration et exploitation.
  • Suivi et mise à jour des états : Les résultats des simulations servent à mettre à jour l’arbre de recherche, guidant ainsi les décisions futures.
  • Paramètres de l’IA : Le nombre d’itérations et le facteur UCB influencent directement la précision et le style de jeu de l’IA.
  • Sauvegarde : L’état de l’IA peut être sauvegardé et rechargé via pickle, évitant ainsi de réentraîner l’IA à chaque utilisation.