Robot de morpion (tic-tac-toe)
Tutoriel vidéo : Tutoriel vidéo OpenMV de Singtown Technology - Robot bras articulé jouant au morpion, concours électronique 2024
Le robot de morpion se décompose en trois étapes : 1. Reconnaissance d'image, pour obtenir le tableau des pions. 2. Algorithme de stratégie de jeu. 3. Contrôle du bras articulé pour la saisie et la pose des pions.
Côté matériel, nous avons utilisé l'OpenMV4 H7 de Singtown Technology, tout le code s'exécute directement sur OpenMV ! Y compris le contrôle du bras articulé, le contrôle du relais, l'algorithme de stratégie de jeu, et l'algorithme de reconnaissance d'image.
Le bras articulé est directement imprimé en 3D, il utilise 3 servomoteurs, qui peuvent être contrôlés directement via la carte d'extension servo d'OpenMV.
La préhension des pions est réalisée grâce à un électroaimant, car j'ai récupéré 10 piles bouton en guise de pions. OpenMV peut contrôler l'électroaimant via un relais.
- La reconnaissance d'image est très simple : on obtient d'abord une image en niveaux de gris, puis, dans les 9 zones du plateau, on obtient les informations de couleur, réalisées dans OpenMV avec statistics. Grâce aux informations de niveaux de gris, on peut déterminer s'il s'agit d'un pion noir, d'un pion blanc, ou d'une case vide.
- Pour calculer le prochain coup à jouer, mon bon ami le professeur CHATGPT m'a suggéré d'utiliser l'algorithme minimax, et m'a même écrit le code correspondant.
- Le contrôle du bras articulé est également assez simple : dans la zone d'attente de prise de pion, ainsi que dans chacune des 9 zones de pose du plateau, on récupère la position du bras articulé, généralement réalisable via la fonction d'apprentissage par démonstration (teaching), mais comme mon bras articulé n'a été fabriqué qu'en une journée dans l'urgence, les positions sont directement définies dans le code en les observant.
# robot.py
import time
from servo import Servos
from machine import SoftI2C, Pin
import math
# Contrôle du relais, le relais contrôle l'électroaimant
pin1 = Pin('P1', Pin.OUT_PP, Pin.PULL_NONE)
pin1.value(0)
# Carte d'extension servo PCA9685
i2c = SoftI2C(sda=Pin('P5'), scl=Pin('P4'))
servo = Servos(i2c, address=0x40, freq=50, min_us=650, max_us=2800, degrees=180)
# Position initiale des trois servomoteurs
servo.position(0, 0)
servo.position(1, 90)
servo.position(2, 90)
# variable globale, utilisée pour un déplacement lent
servo_positions = [0,90,90]
# Contrôle le déplacement d'un servomoteur
def move(index, angle):
servo.position(index, angle)
servo_positions[index] = angle
# Contrôle le déplacement des trois servomoteurs
def move_list(angle_list):
print(angle_list)
move(0, int(angle_list[0]))
move(1, int(angle_list[1]))
move(2, int(angle_list[2]))
# Contrôle le déplacement lent des trois servomoteurs
def slow_move_to(angle_list):
init_positions = servo_positions.copy()
d0 = angle_list[0] - init_positions[0]
d1 = angle_list[1] - init_positions[1]
d2 = angle_list[2] - init_positions[2]
dm = int(max(abs(d0), abs(d1), abs(d2)))
if dm == 0:
return
for i in range(dm+1):
move_list([init_positions[0]+i*d0/dm,
init_positions[1]+i*d1/dm,
init_positions[2]+i*d2/dm])
time.sleep_ms(40)
# Définit la position de la zone de prise des pions
PICK = [[74,128,19], [82,128,17], [90,125,14], [98,128,17], [106,128,19]]
# Position du bras articulé relevé dans la zone de prise
HIGH_PICK = [90,95,55]
# Position de dépose sur le plateau
BOARD = [
[[82,150,55], [82,140,40], [81,133,30]],
[[90,150,55], [90,140,40], [90,133,30]],
[[98,150,55], [98,140,40], [99,133,30]]
]
# Position au-dessus du plateau lors de la pose du pion
HIGH_BOARD = [90,120,70]
# Prend un pion, et le place en x,y
def pick_and_place(x,y):
slow_move_to(HIGH_PICK)
time.sleep_ms(500)
slow_move_to(PICK[2])
time.sleep_ms(500)
slow_move_to(HIGH_PICK)
time.sleep_ms(500)
slow_move_to(HIGH_BOARD)
time.sleep_ms(500)
slow_move_to(BOARD[y][x])
time.sleep_ms(500)
pin1.value(1) # ouvre le relais
time.sleep_ms(500)
slow_move_to(HIGH_PICK)
pin1.value(0) # ferme le relais
time.sleep_ms(500)
slow_move_to([0,90,90])
if __name__ == "__main__":
# utilisé pour les tests de calibration
time.sleep_ms(1)
for order in [
BOARD[1][1], BOARD[0][0], BOARD[1][0],
BOARD[2][0], BOARD[2][1], BOARD[2][2],
BOARD[1][2], BOARD[0][2], BOARD[0][1],
]:
slow_move_to(HIGH_BOARD)
time.sleep_ms(500)
slow_move_to(order)
time.sleep_ms(500)
#slow_move_to(BOARD[0][2])
#for x in PICK:
#slow_move_to(HIGH_PICK)
#time.sleep_ms(500)
#slow_move_to(x)
#time.sleep_ms(500)
slow_move_to([0,90,90])
# chess.py
SIZE = 3
# Vérifie s'il y a une victoire
def check_win(board, player):
# Check rows and columns
for i in range(SIZE):
if all(board[i][j] == player for j in range(SIZE)) or \
all(board[j][i] == player for j in range(SIZE)):
return True
# Check diagonals
if all(board[i][i] == player for i in range(SIZE)) or \
all(board[i][SIZE - 1 - i] == player for i in range(SIZE)):
return True
return False
# Vérifie s'il y a match nul
def check_draw(board):
return all(board[i][j] != ' ' for i in range(SIZE) for j in range(SIZE))
# Calcule le score de la stratégie
def minimax(board, depth, is_maximizing):
computer = 'X'
player = 'O'
if check_win(board, computer):
return 10 - depth
if check_win(board, player):
return depth - 10
if check_draw(board):
return 0
if is_maximizing:
best_score = float('-inf')
for i in range(SIZE):
for j in range(SIZE):
if board[i][j] == ' ':
board[i][j] = computer
score = minimax(board, depth + 1, False)
board[i][j] = ' '
best_score = max(score, best_score)
return best_score
else:
best_score = float('inf')
for i in range(SIZE):
for j in range(SIZE):
if board[i][j] == ' ':
board[i][j] = player
score = minimax(board, depth + 1, True)
board[i][j] = ' '
best_score = min(score, best_score)
return best_score
# Calcule la position du prochain coup
def computer_move(board):
if board == [
[" "," "," "],
[" "," "," "],
[" "," "," "]
]:
return 1,1
best_score = float('-inf')
move = (-1, -1)
for i in range(SIZE):
for j in range(SIZE):
if board[i][j] == ' ':
board[i][j] = 'X'
score = minimax(board, 0, False)
board[i][j] = ' '
if score > best_score:
best_score = score
move = (i, j)
if move != (-1, -1):
# board[move[0]][move[1]] = 'X'
print(f"Computer places X at ({move[0]}, {move[1]})")
return move[0], move[1]
# Vérifie à qui de jouer
def check_turn(board):
x_count = sum(row.count("X") for row in board)
o_count = sum(row.count("O") for row in board)
return "X" if x_count == o_count else "O"
# main.py
import csi, image, time
csi0 = csi.CSI()
from pyb import Pin
import robot
import chess
csi0.reset()
csi0.pixformat(csi.GRAYSCALE)
csi0.framesize(csi.QVGA)
csi0.snapshot(time = 2000)
clock = time.clock()
# Bouton poussoir
pin0 = Pin('P0', Pin.IN, Pin.PULL_UP)
distance = 43
block = 10
# Génère les positions des zones de la grille 3x3
def generate_centered_rois(width, height, b, k):
rois = []
# Calcule le décalage de position du centre de chaque ROI
offset = (b - k) // 2
# Calcule la largeur et la hauteur totales de la matrice 3x3
total_width = 3 * b
total_height = 3 * b
# Calcule le point de départ en haut à gauche, pour centrer la matrice
start_x = (width - total_width) // 2
start_y = (height - total_height) // 2
for i in range(3):
row = []
for j in range(3):
x_center = start_x + j * b + b // 2
y_center = start_y + i * b + b // 2
x = x_center - k // 2
y = y_center - k // 2
row.append((x, y, k, k))
rois.append(row)
return rois
# Positions des zones de la grille 3x3
rois = generate_centered_rois(csi0.width(), csi0.height(), distance, block)
# Tableau du plateau
# Pion noir : X
# Pion blanc : O
# Pas de pion : chaîne vide
board = [
[" "," "," "],
[" "," "," "],
[" "," "," "],
]
# Attend que le bouton soit pressé puis relâché
def wait_key():
while pin0.value():
img = csi0.snapshot().lens_corr(1.8)
for y in range(len(rois)):
for x in range(len(rois[y])):
img.draw_rectangle(rois[y][x])
while not pin0.value():
time.sleep_ms(1)
while(True):
clock.tick()
wait_key()
img = csi0.snapshot().lens_corr(1.8)
# La reconnaissance d'image donne le tableau du plateau
for y in range(len(rois)):
for x in range(len(rois[y])):
gray = img.get_statistics(roi=rois[y][x]).mean
if gray < 100:
board[y][x] = "X"
elif gray > 200:
board[y][x] = "O"
else:
board[y][x] = " "
# Affiche le tableau actuel du plateau
for line in board:
print(line)
print()
# Dessine le tableau du plateau
for y in range(len(rois)):
for x in range(len(rois[y])):
if board[y][x] == "X":
color = 255
elif board[y][x] == "O":
color = 0
elif board[y][x] == " ":
color = 127
img.draw_rectangle(rois[y][x], color=color)
# Stratégie de jeu
if chess.check_win(board, 'O'):
print("Vous avez gagné !")
elif chess.check_win(board, 'X'):
print("J'ai gagné !")
elif chess.check_draw(board):
print("Match nul !")
elif chess.check_turn(board) == "X":
# Calcule où placer le prochain pion
line,row = chess.computer_move(board)
# Dessine une croix sur la case cible du plateau
img.draw_cross((int(rois[line][row][0]+block/2), int(rois[line][row][1]+block/2)), size=block, color=0)
csi0.flush()
# Le robot prend et place le pion
robot.pick_and_place(row, line)
csi0.flush()
elif chess.check_turn(board) == "O":
print("À vous de jouer !")