BOT Morpion, coder avec ChatGPT !

Automatiser le jeu Morpion avec Python

En tant que développeur, j’ai eu l’occasion d’aider des étudiants en Design à définir leurs besoins techniques afin qu’ils développent les prototypes des projets qu’ils ont imaginés. Nul doute que ChatGPT sera leur allié dans cette conception.

Afin de mieux les guider et d’avoir plus de recul sur la manière de donner des instructions à ChatGPT. J’ai cherché à définir un processus de conception de fonctions…

Pour ce faire, j’avais besoin d’un support, d’une expérience nouvelle, dans un langage que je n’utilise pas au quotidien. Pendant celle-ci j’aurai à m’interroger sur la manière d’exploiter au mieux ChatGPT. J’ai donc donc profité de quelques heures pluvieuses, un samedi après-midi, pour créer un bot (en #Python) qui joue au Morpion , en n’écrivant presque aucune ligne de code moi même.

J’ai commencé par définir le fonctionnement du BOT à travers plusieurs étapes (trouver la grille, créer une fonction pour identifier les symboles X et 0, etc.). Celles-ci ont mené à des interrogations et des difficultés, et je me suis vite rendu compte que je pouvais à chaque fois y répondre grâce à une logique / un processus.

Grâce à cet exercice et après quelques prompts où j’ai laissé ChatGPT rédiger plusieurs fonctions à ma place, j’ai réussi à dégager une logique de conception de fonction qui me semble maintenant évidente . En effet, quoi de plus logique que d’interroger l’intelligence artificielle de la même manière que l’on pourrai s’interroger soi-même en tant que développeur ?

Voici la logique de conception :

  1. Verbaliser le résultat de la fonction et rédiger une brève description de celle-ci
  2. Déterminer :
    1. les éléments passés à la fonction
    2. le résultat attendu sans entrer dans les détails,
    3. si nécessaire, et si nos compétences le permettent : les packages/modules à utiliser (par exemple des modules déjà présents dans le projet).
  3. Préparer une description de la fonction à soumettre ChatGPT, en indiquant précisément les arguments à passer, leurs types (le cas échéant, les valeurs par défaut), le résultat attendu et sa forme.
  4. Soumettre la demande à ChatGPT.
  5. Demander un exemple d’usage de cette fonction : comment l’utiliser ?
  6. Exécuter l’exemple et vérifier TRÈS attentivement la réponse sans se laisser éblouir par la facilité avec laquelle une réponse est proposée puis s’approprier celle-ci.
    1. La réponse n’est pas celle attendue : demander une explication de la fonction étape par étape et l’ajout de commentaire pour cibler le problème et demander à l’IA de le corriger.
    2. La réponse est celle attendue, intégrer la réponse.

Automatiser le jeu morpion avec Python

Le Morpion est un jeu de société simple et amusant (ChatGPT est bon publique), également connu sous le nom de Tic Tac Toe. Dans cet article, nous allons explorer comment automatiser ce jeu en utilisant Python et quelques bibliothèques utiles.

1° Installation des bibliothèques

Nous aurons besoin de certaines bibliothèques pour automatiser le Morpion. Nous allons utiliser OpenCV pour détecter l’emplacement de la grille de jeu sur l’écran, PyAutoGUI pour cliquer sur les cases de celle-ci. Vous pouvez installer ces bibliothèques à l’aide de pip3 en exécutant les commandes suivantes dans votre terminal :

pip3 install cv2
pip3 install pyautogui
pip3 install copy
pip3 install numpy

Langage du code : Bash (bash)

2° Trouver la grille de Tic Tac Toe

La première étape consiste à trouver la grille de jeu sur l’écran. Nous allons utiliser OpenCV pour détecter la grille de jeu grâce à la fonction matchTemplate et renvoyé sa position et ses dimensions. Voici le code pour cette étape :

import cv2
import numpy as np

def find_image_on_screen(image_path):
    # Load the query image
    query_image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

    # Get the screen dimensions
    screen_width, screen_height = pyautogui.size()

    # Capture a screenshot of the screen
    screen_image = np.array(pyautogui.screenshot())

    # Convert the screenshot to grayscale
    gray_screen_image = cv2.cvtColor(screen_image, cv2.COLOR_BGR2GRAY)

    # Perform template matching
    result = cv2.matchTemplate(gray_screen_image, query_image, cv2.TM_CCOEFF_NORMED)

    # Get the maximum value and its location in the result matrix
    min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)

    # Get the top-left corner of the bounding box for the matched image
    x, y = max_loc

    # Get the dimensions of the matched image
    w, h = query_image.shape[::-1]

    # Return the bounding box as a tuple (left, top, width, height)
    return (x, y, w, h)

Langage du code : Python (python)

3° Créer une fonction pour identifier les symboles X et 0

La fonction identify_symbol a pour but de trouver le symbole présent dans une cellule donnée de la grille. La fonction prend en entrée l’image de la cellule à analyser et renvoie le symbole correspondant.

import cv2

symbol_templates = {
    'O': cv2.imread('template-O.png', 0),
    'X': cv2.imread('template-X.png', 0)
}

def identify_symbol(cell_image):
    symbol = " "
    max_val = 0
    for template_name, template in symbol_templates.items():
        template_gray = template
        if cell_image.shape != template.shape:
            template_gray = cv2.resize(template, cell_image.shape[::-1])
        res = cv2.matchTemplate(cell_image, template_gray, cv2.TM_CCOEFF_NORMED)
        loc = np.where(res >= 0.4)

        if len(loc[0]) > 0 and res[loc].max() > max_val:
            max_val = res[loc].max()
            symbol = template_name

    return symbol

Langage du code : JavaScript (javascript)

4° Faire une capture d’écran du jeu et le convertir en donnée exploitable

La fonction get_game_state prend en entrée une image représentant le plateau de jeu de morpion et retourne l’état du jeu. Elle commence par traiter l’image et extraire le plateau en utilisant la fonction cv2.boundingRect pour détecter le rectangle englobant l’image. Ensuite, elle divise le plateau en 9 cellules en utilisant une double boucle for. Chaque cellule est extraite à l’aide des coordonnées calculées à partir de la position et de la taille du plateau.

La liste de cellules est ensuite parcourue et son contenu (X ou 0) est identifié à l’aide de la fonction identify_symbol.

Finalement, la fonction retourne une liste de cellules représentant le plateau de jeu.

import cv2

def get_game_state(image):
    # Preprocess the image and extract the board
    gray = cv2.imread(image, 0)
    x, y, w, h = cv2.boundingRect(gray)
    board = gray[y:y+h, x:x+w]

    # Divide the board into cells
    cell_width = w // 3
    cell_height = h // 3
    cells = []
    for row in range(3):
        row_cells = []
        for col in range(3):
            cell = board[row*cell_height:(row+1)*cell_height, col*cell_width:(col+1)*cell_width]
            row_cells.append(cell)
        cells.append(row_cells)

    # Identify the symbols in each cell and create the game state
    game_state = []
    for row in cells:
        row_state = []
        for cell in row:
            # Identify the symbol in the cell using image processing techniques
            # For example, you can use template matching or machine learning-based object recognition
            symbol = identify_symbol(cell)
            game_state.append(symbol)

    # Return the game state
    return game_state

Langage du code : PHP (php)

5° Déterminer la case sur laquelle cliquer

Le code ci-dessus contient plusieurs fonctions qui sont utilisées dans ce but :

  • La première fonction, check_for_win, vérifie si un joueur a gagné en vérifiant toutes les combinaisons possibles de lignes, colonnes et diagonales pour le symbole donné. Si le symbole se trouve dans une ligne, une colonne ou une diagonale, la fonction renvoie « True ».
  • La deuxième fonction, random_move, renvoie un mouvement aléatoire à partir des cases vides de la grille.
  • La troisième fonction, best_opponent_move, utilise les fonctions précédente afin de déterminé le coup suivant

⚠️ Bien que le nom de la fonction soit “best_opponent_move”, il ne s’agit pas du meilleur coup.

import random
import copy

def check_for_win(game_state, symbol):
    board = game_state['board']
    # Check rows
    for i in range(0, 9, 3):
        if board[i] == board[i+1] == board[i+2] == symbol:
            return True
    # Check columns
    for i in range(3):
        if board[i] == board[i+3] == board[i+6] == symbol:
            return True
    # Check diagonals
    if board[0] == board[4] == board[8] == symbol or board[2] == board[4] == board[6] == symbol:
        return True
    # No win found
    return False

def random_move(game_state):
    empty_spaces = [i for i, val in enumerate(game_state['board']) if val == ' ']
    return random.choice(empty_spaces)

def best_opponent_move(game_state):
    """Returns the index of the best move for the opponent"""
    opponent = 'O' if game_state['player'] == 'X' else 'X'
    for i in range(9):
        if game_state['board'][i] == ' ':
            new_game_state = copy.deepcopy(game_state)
            new_game_state['board'][i] = opponent
            if check_for_win(new_game_state, opponent):
                return i
    for i in range(9):
        if game_state['board'][i] == ' ':
            new_game_state = copy.deepcopy(game_state)
            new_game_state['board'][i] = game_state['player']
            if check_for_win(new_game_state, game_state['player']):
                return i
            
    return random_move(game_state)

Langage du code : PHP (php)

6° Déterminer l’emplacement de chaque case

La fonction « get_cell_centers » prend en paramètre les coordonnées du cadre englobant le jeu de morpion et retourne une liste contenant les coordonnées du centre de chaque case.

def get_cell_centers(bounding_box):
    x, y, w, h = bounding_box
    cell_width = w // 3
    cell_height = h // 3
    centers = []
    for i in range(3):
        for j in range(3):
            center_x = x + (j * cell_width) + (cell_width // 2)
            center_y = y + (i * cell_height) + (cell_height // 2)
            centers.append((center_x, center_y))
    return centers

Langage du code : JavaScript (javascript)

7° Assembler toutes les fonctions afin d’automatiser une partie

La fonction play_game permet de jouer une partie de morpion en utilisant les fonctions définies précédemment. Elle effectue une boucle tant qu’il n’y a pas de gagnant, avec un délai aléatoire entre 1,5 et 3 secondes (pour éviter la détection du bot). Ensuite, elle appelle la fonction play_action qui joue un coup et retourne l’état actuel de la grille. Elle affiche ensuite l’état actuel de la grille et vérifie si un joueur a gagné ou si la partie est nulle.

La check_for_draw, vérifie si la partie est terminée et si aucun joueur n’a gagné ou perdu. Si toutes les cases sont remplies et aucun joueur n’a gagné, la fonction renvoie « True ».

import pyautogui
import copy
import time
import random

# Find the template image on the screen
tictactoeBounding = find_image_on_screen('tictactoe.png')
left, top, width, height = tictactoeBounding
# Define the bounding box
# left, top, width, height = pyautogui.locateOnScreen('tictactoe.png')

# Take a screenshot of the bounding box
screenshot = pyautogui.screenshot(region=(left, top, width, height))

# Get the center of each cell of the tictactoe
centers = get_cell_centers(tictactoeBounding)

# Save the screenshot as a file
screenshot.save('screenshot.png')
game_state = {'player': 'X', 'board': get_game_state('screenshot.png')}

def check_for_draw(game_state):
    return ' ' not in game_state['board'] and not check_for_win(game_state, 'X') and not check_for_win(game_state, 'O')

def play_action():
    # Call get_game_state function to retrieve game state
    # Take a screenshot of the bounding box
    screenshot = pyautogui.screenshot(region=(left, top, width, height))

    # Save the screenshot as a file
    screenshot.save('screenshot.png')
    game_state = {'player': 'X', 'board': get_game_state('screenshot.png')}

    # if not check_for_win(game_state, 'O'):
    next_move = best_opponent_move(copy.deepcopy(game_state))
    game_state['board'][next_move] = "X"

    x = centers[next_move][0]
    y = centers[next_move][1]
    pyautogui.doubleClick(x / 2, y / 2)

    if not check_for_win(game_state, 'X') and not check_for_draw(game_state):
        # Wait for the opponent
        time.sleep(1)

    screenshot = pyautogui.screenshot(region=(left, top, width, height))
    # Save the screenshot as a file
    screenshot.save('screenshot.png')    

    return {'board': get_game_state('screenshot.png')}

def play_game():
    # Start with an empty board
    winner = None

    while not winner:

        # Generate a random delay between 1.5s and 3s
        delay = random.uniform(0.5, 1.5)
        
        # Wait for the random delay
        time.sleep(delay)

        board = play_action()

        print("         ")
        print("         ")
        # Print the current game state
        print(f" {board['board'][0]} | {board['board'][1]} | {board['board'][2]} ")
        print("---+---+---")
        print(f" {board['board'][3]} | {board['board'][4]} | {board['board'][5]} ")
        print("---+---+---")
        print(f" {board['board'][6]} | {board['board'][7]} | {board['board'][8]} ")

        if check_for_win(board, 'X'):
            winner = 'X'
        elif check_for_win(board, 'O'):
            winner = 'O'
        elif check_for_draw(board):
            winner = 'draw'

        # print(winner)
        
   # Print the final result
    if winner == 'draw':
        print("The game ended in a draw.")
    elif winner is not None:
        print(f"{winner} won the game!")

play_game()

Langage du code : PHP (php)

8° Images sources

Placez ces images suivantes à la racine du dossier ou se trouve le script:

  • tictactoe.png
  • template-O.png
  • template-X.png

9° Tester le script

Pour tester ce script python vous pouvez tapez dans Google tictactoe : un jeu de morpion s’ouvre.

Ensuite ouvrez la console à l’endroit ou vous avez enregistrer le script et executez la commande :

python3 <nom-du-fichier>.py
Langage du code : HTML, XML (xml)

Vous devriez voir la partie se jouer et l’état de chaque round s’inscrire dans la console.

CONCLUSION :

Cette expérimentation sur l’Intelligence artificielle générative m’a permis de prendre conscience que la manière dont je questionnais l’outil génératif ne différait que très peu de la manière dont je ME questionnais dans le même contexte. à travers une méthodologie à peine différente… On se rend donc bien compte qu’il est difficile de s’extraire d’une expertise professionnelle dans les échanges avec ChatGPT.

À terme, deux idées me viennent:

  • Avec pour but de créer un proto et non un produit viable à long terme, quelques bases en programmation et une bonne utilisation des prompts peuvent suffire aux designer comme aux étudiants pour challenger leurs idées.
  • l’utilisation de cet outil pour créer un proto permet de mieux comprendre le monde du dev et donc de communiquer plus efficacement avec les développeurs.

De mon côté j’y vois toujours un outil puissant capable de me délester des tâches triviale pour concentrer mon attention là où il y a vraiment de l’enjeu.

Après plusieurs mois d’utilisation, j’y vois maintenant un outil permettant  de gagner en productivité et par conséquent libérer du temps pour imager l’avenir de ma pratique. On se doute bien que que les métiers d’aujourd’hui seront ceux de demain !

Hugo Rulier