Photochess
Introducción
Esta proyecto se basa en recrear un ajedrez en el cual se pueda jugar contra la máquina. La máquina mostrará por medio de una pantalla los movimientos que realiza. Para ello, una cámara reconoce los movimientos realizados por el jugador, y mediante reconocimiento de imágenes en Python, este movimiento se pasa a Arduino para que pueda indicar el próximo movimiento que realizará la máquina. Además, se podrá elegir opcionalmente el tiempo límite para realizar la partida. El tiempo predefinido son 5 minutos, pero se podrá modificar sumando o restando un minuto entre 1 minuto y 10 minutos o incluso jugar sin tiempo. Al finalizar la partida, se guardaran todos los movimientos en un fichero de texto así como las imágenes de los movimientos de la partida.
Funcionamiento
A continuación mostraremos el funcionamiento del proyecto:
- Primero se establece conexión entre Arduino y Phyton
- Una vez conectados se intenta calibrar, es decir, tomar los rangos de color para diferenciar los colores. Para ello pulsamos el botón hasta que aparezca el mensaje que se ha calibrado correctamente
- A continuación, se mostrará por una pantalla LCD el tiempo que tendrá el usuario para poder pensar los movimientos. Para modificar este tiempo podrá pulsar los botones amarillo de la caja. Una vez decidido el tiempo pulsará el botón rojo para seguir con el proceso
- El siguiente paso será reconocer que las piezas están bien colocadas. Para ello el usuario deberá pulsar el botón rojo para indicarle al sistema que tome una foto y reconozca las piezas.
- Una vez reconocidas las piezas comienza la partida. En el turno del usuario tendrá que mover su ficha y para que avance la partida pulsará el botón rojo. En caso de que sea correcto, el sistema mostrará el movimiento de la máquina y el usuario moverá la ficha de las máquina a su nueva posición y pulsará el botón rojo.
- La partida terminará cuando se acabe el tiempo o se produzca un jaque mate ya sea del usuario o de la máquina.
La secuencia anterior será la ejecución principal del sistema, pero también existen muchos caminos alternativos:
- No se ha conseguido calibrar correctamente el tablero. Se mostrará un mensaje de error y se volverá a pedir que el usuario vuelva a pulsar el botón rojo para volver a intentar calibrar el sistema.
- Las piezas están mal colocadas. Se mostrará un mensaje de error y se pedirá que se coloquen las piezas de forma adecuada.
- El movimiento del usuario es incorrecto. Se mostrará un mensaje de error, y se pedirá volver a realizar un movimiento.
- El usuario no realiza el movimiento de la máquina correctamente. La máquina volverá a enseñar el movimiento de la máquina hasta que el usuario coloque en la posición indicada la ficha de la máquina.
Algunos de estos casos se demostrarán al final de la entrada del blog en unos vídeos.
Hardware
La parte hardware se divide en dos módulos: Módulo del ajedrez y Módulo Arduino.
El módulo del ajedrez es una estructura donde se coloca el móvil en una posición fija desde la que pueda sacar una foto en la que se vea bien el tablero y las fichas. Se trata de dos plataformas sostenidas mediante barras de plásticos y separadas dejando la altura suficiente para obtener fotografías donde se vea completamente el tablero. Además, en la plataforma inferior donde se colocará el tablero, se han agregado tacos de madera para mantener el tablero en una posición fija para evitar problemas a la hora de realizar la calibración. Esta estructura es independiente de los componentes Hardware de Arduino. A continuación, se especificará los materiales y el presupuesto.
Elemento | Precio (€) | Descripción |
Zapatero reutilizado | 10 | Soporte fijo del móvil |
Tablero ajedrez | 3 | |
Goma Eva | 1,2 | Fichas |
Móvil antiguo | 0 | |
Extras: bridas, tacos madera | 0 | Para inmovilizar el tablero |
El módulo será el siguiente:
El otro módulo será el módulo de Arduino. Sus componentes del Hardware se encuentran empotrados en una caja de cartón. Desde el exterior únicamente se puede ver la pantalla LCD y los tres botones. El usuario interactuará únicamente con estos tres componentes. Permitirá interactuar al usuario con el sistema. A continuación se mostrará dos imágenes: la estructura y un esquema del hardware en Tinkercard:
Software
El código se basará en la comunicación entre un programa de Arduino y un programa de Phyton. Cada uno tendrá una función y gracias su combinación podremos jugar una partida contra la máquina.
El programa de Phyton se encargará de obtener las imágenes del tablero de Arduino para calcular que movimiento se ha realizado en el tablero mediante segmentación de color y poder comunicárselo a Arduino. Para ello utilizamos una serie de funciones auxiliares que nos ayudan a cumplir distintas tareas, como tomar una foto dada la IP de un teléfono, calibrar el tablero, o calcular el movimiento realizado.
import os
import cv2
import numpy as np
import serial
import time
def establish_connection():
arduino = serial.Serial("COM3", 9600)
time.sleep(2)
write_serial(arduino, "Connected")
if read_serial(arduino) == "Connected":
print("Conexion establecida")
return arduino
else:
print("Conexion fallida")
def read_serial(arduino):
return arduino.readline().decode().strip()
def write_serial(arduino, string):
arduino.write(str(string).encode())
def take_pic(ip, size=(2160, 972)):
directory = r'C:\Users\jemar\Pictures\Sistemas empotrados'
url = "https://" + ip + "/shot.jpg"
cap = cv2.VideoCapture(url)
while True:
camera, frame = cap.read()
if frame is not None:
img = cv2.resize(frame, size)
os.chdir(directory)
cv2.imwrite("FOTO.jpg", img)
return img
q = cv2.waitKey(1)
if q == ord("q"):
break
cv2.destroyAllWindows()
def calculate_corner_distortion(pto_ext, pto_med, pto_int):
dist_ext_med = np.sqrt((pto_ext[0][0] - pto_med[0][0]) ** 2 + (pto_ext[0][1] - pto_med[0][1]) ** 2)
dist_med_int = np.sqrt((pto_med[0][0] - pto_int[0][0]) ** 2 + (pto_med[0][1] - pto_int[0][1]) ** 2)
ratio = dist_ext_med / dist_med_int
return [pto_ext[0][0] + (pto_ext[0][0] - pto_med[0][0]) * ratio,
pto_ext[0][1] + (pto_ext[0][1] - pto_med[0][1]) * ratio]
def calibrate_board(img, size=1200):
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
pattern_size = (7, 7)
found, corners = cv2.findChessboardCorners(gray, pattern_size, None)
if not found:
return None
else:
corners_refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1),
criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001))
esq1 = calculate_corner_distortion(corners_refined[0], corners_refined[8], corners_refined[16])
esq2 = calculate_corner_distortion(corners_refined[6], corners_refined[12], corners_refined[18])
esq3 = calculate_corner_distortion(corners_refined[42], corners_refined[36], corners_refined[30])
esq4 = calculate_corner_distortion(corners_refined[48], corners_refined[40], corners_refined[32])
points = order_points([esq1, esq2, esq3, esq4])
src_pts = np.float32(points)
dst_pts = np.float32([[0, 0], [0, size], [size, size], [size, 0]])
perspective_matrix = cv2.getPerspectiveTransform(src_pts, dst_pts)
return perspective_matrix
def order_points(corners):
sums = [corners[0][0] + corners[0][1], corners[1][0] + corners[1][1], corners[2][0] + corners[2][1],
corners[3][0] + corners[3][1]]
mini = min(sums)
maxi = max(sums)
mid = list()
for i in range(4):
if sums[i] == mini:
esq_ne = corners[i]
elif sums[i] == maxi:
esq_so = corners[i]
else:
mid.append(corners[i])
if mid[0][0] < mid[1][0]:
esq_no = mid[0]
esq_se = mid[1]
else:
esq_no = mid[1]
esq_se = mid[0]
return [esq_no, esq_so, esq_se, esq_ne]
def warp_image(img, perspective_matrix, size=1200):
return cv2.warpPerspective(img, perspective_matrix, (size, size), flags=cv2.INTER_LINEAR)
def add_points(i, offset=15):
side = 1200 / 8
x = (i % 8) * side
y = (i // 8) * side
points = list()
points.append([int(x + side * 0.2), int(y + side / 2)])
points.append([int(x + side * 0.8), int(y + side / 2)])
return points
def calibrate_points(hsv, puntos):
h = list()
s = list()
v = list()
for p in puntos:
# print(p, ": ", hsv[p[1]][p[0]])
h.append(hsv[p[1]][p[0]][0])
s.append(hsv[p[1]][p[0]][1])
v.append(hsv[p[1]][p[0]][2])
# hsv = cv2.circle(hsv, (p[0], p[1]), radius=0, color=(0, 0, 255), thickness=3)
h = [min(h) - 10, max(h) + 10]
s = [min(s) - 10, max(s) + 10]
v = [min(v) - 10, max(v) + 10]
"""cv2.imshow("puntos", hsv)
cv2.waitKey(0)
cv2.destroyAllWindows()"""
"""print([min(h) - 10, " ", max(h) + 5], max(h)-min(h))
print([min(s) - 10, " ", max(s) + 10], max(s)-min(s))
print([min(v) - 10, " ", max(v) + 10], max(v)-min(v))"""
return [np.array([h[0], s[0], v[0]]), np.array([h[1], s[1], v[1]])]
def calculate_masks(hsv, ranges):
mask = cv2.inRange(hsv, ranges[0], ranges[1])
kernel = np.ones((5, 5), np.uint8)
mask = cv2.erode(mask, kernel, iterations=1)
mask = cv2.dilate(mask, kernel, iterations=1)
return mask
def get_mask(hsv, img, color):
if color == "white":
mask = cv2.inRange(hsv, (0, 0, 185), (179, 25, 220))
else:
# mask1 = cv2.inRange(hsv, (0, 65, 80), (15, 160, 160))
# mask2 = cv2.inRange(hsv, (170, 65, 80), (179, 160, 160))
mask1 = cv2.inRange(hsv, (0, 65, 60), (30, 160, 160))
mask2 = cv2.inRange(hsv, (170, 65, 60), (179, 160, 160))
mask = cv2.bitwise_or(mask1, mask2)
kernel = np.ones((5, 5), np.uint8)
img = cv2.bitwise_and(img, img, mask=mask)
img = cv2.erode(img, kernel, iterations=1)
img = cv2.dilate(img, kernel, iterations=2)
return img
def calculate_pieces(mask, size=400, percentage=0.3):
mask = cv2.resize(mask, (size, size))
side = size // 8
matrix = []
for i in range(64):
pixels = 0
for x in range(side * (i % 8), side * (i % 8) + side - 1):
for y in range(side * (i // 8), side * (i // 8) + side - 1):
if (x - (side * (i % 8) + side / 2)) ** 2 + (y - (side * (i // 8) + side / 2)) ** 2 <= (
(side * 0.8) // 2) ** 2:
if mask[y][x][0] != 0 or mask[y][x][1] != 0 or mask[y][x][2] != 0:
pixels = pixels + 1
if i // 8 == 0:
matrix.append([])
matrix[i // 8].append([])
if pixels > (side * 0.4) ** 2 * 3 * percentage:
matrix[i // 8][i % 8] = 1
else:
matrix[i // 8][i % 8] = 0
return matrix
def castle(squares, prev_mat, actual_mat):
if prev_mat[squares[0][0]][squares[0][1]] != 1:
return False
if prev_mat[squares[1][0]][squares[1][1]] != 0:
return False
if prev_mat[squares[2][0]][squares[2][1]] != 0:
return False
if prev_mat[squares[3][0]][squares[3][1]] != 1:
return False
if actual_mat[squares[0][0]][squares[0][1]] != 0:
return False
if actual_mat[squares[1][0]][squares[1][1]] != 1:
return False
if actual_mat[squares[2][0]][squares[2][1]] != 1:
return False
if actual_mat[squares[3][0]][squares[3][1]] != 0:
return False
return True
def calculate_move(prev_mat, actual_mat, rotated=0):
letters = [['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], ['h', 'g', 'f', 'e', 'd', 'c', 'b', 'a']]
numbers = [['8', '7', '6', '5', '4', '3', '2', '1'], ['1', '2', '3', '4', '5', '6', '7', '8']]
prev_square = ""
actual_square = ""
if castle([[0, 0], [0, 2], [0, 3], [0, 4]], prev_mat, actual_mat):
return "e8c8"
if castle([[0, 4], [0, 5], [0, 6], [0, 7]], prev_mat, actual_mat):
return "e8g8"
if castle([[7, 0], [7, 2], [7, 3], [7, 4]], prev_mat, actual_mat):
return "e1c1"
if castle([[7, 4], [7, 5], [7, 6], [7, 7]], prev_mat, actual_mat):
return "e1g1"
for y in range(8):
for x in range(8):
if (prev_mat[y][x] == 1) & (actual_mat[y][x] == 0):
if len(prev_square) == 0:
prev_square = letters[rotated][x] + numbers[rotated][y]
elif (prev_mat[y][x] == 0) & (actual_mat[y][x] == 1):
if len(actual_square) == 0:
actual_square = letters[rotated][x] + numbers[rotated][y]
if prev_square == "":
prev_square = "No"
if actual_square == "":
actual_square = "ne"
return prev_square + actual_square + "\n"
def compare_matrix(mat_a, mat_b):
for y in range(8):
for x in range(8):
if mat_a[y][x] != mat_b[y][x]:
return False
return True
ip = "192.168.70.134:8080"
arduino = establish_connection()
directory = r'C:\Users\jemar\Pictures\Sistemas empotrados'
os.chdir(directory)
file = open("partida.txt", "w")
while True:
cod = read_serial(arduino)
print(cod)
if cod == "calibrate":
img = take_pic(ip)
perspective_mat = calibrate_board(img)
if perspective_mat is None:
print("error")
write_serial(arduino, "-1")
else:
write_serial(arduino, "0")
break
else:
print("Codigo erroneo, no se ha calibrado el tablero")
prev_matrix_white = [[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1]]
prev_matrix_black = [[1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]]
while True:
cod = read_serial(arduino)
print(cod)
if cod == "startGame":
img = take_pic(ip)
warped_img = warp_image(img, perspective_mat)
cv2.imwrite("warped.jpg", warped_img)
hsv = cv2.cvtColor(warped_img, cv2.COLOR_BGR2HSV)
white_points = list()
for i in range(48, 64):
white_points = white_points + add_points(i)
white_range = calibrate_points(hsv, white_points)
white_mask = calculate_masks(hsv, white_range)
masked_img = cv2.bitwise_and(warped_img, warped_img, mask=white_mask)
cv2.imwrite("white.jpg", masked_img)
actual_matrix_white = calculate_pieces(masked_img)
print(actual_matrix_white)
white_ok = compare_matrix(prev_matrix_white, actual_matrix_white)
print(white_ok)
black_points = list()
for i in range(16):
black_points = black_points + add_points(i)
black_range = calibrate_points(hsv, black_points)
black_mask = calculate_masks(hsv, black_range)
masked_img = cv2.bitwise_and(warped_img, warped_img, mask=black_mask)
cv2.imwrite("black.jpg", masked_img)
actual_matrix_black = calculate_pieces(masked_img)
print(actual_matrix_black)
black_ok = compare_matrix(prev_matrix_black, actual_matrix_black)
print(black_ok)
if white_ok and black_ok:
prev_matrix_white = actual_matrix_white
prev_matrix_black = actual_matrix_black
write_serial(arduino, "1")
file.write("NUEVA PARTIDA\n\n")
break
else:
print("Piezas mal colocadas")
write_serial(arduino, "-1")
else:
print("Codigo erroneo, partida no iniciada")
n_moves = 0
problematic_matrix_white = [[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 1, 0, 0, 1, 0, 0],
[1, 1, 1, 1, 0, 1, 1, 1], [1, 0, 1, 1, 1, 1, 0, 1]]
problematic_matrix_black = [[1, 0, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 0, 1, 1, 1],
[0, 0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]]
while True:
cod = read_serial(arduino)
print(cod)
if cod == "whiteMoves":
img = take_pic(ip)
warped_img = warp_image(img, perspective_mat)
cv2.imwrite("warped.jpg", warped_img)
hsv = cv2.cvtColor(warped_img, cv2.COLOR_BGR2HSV)
white_mask = calculate_masks(hsv, white_range)
masked_img = cv2.bitwise_and(warped_img, warped_img, mask=white_mask)
cv2.imwrite("white.jpg", masked_img)
actual_matrix_white = calculate_pieces(masked_img)
"""if n_moves == 4:
black_mask = calculate_masks(hsv, black_range)
masked_img = cv2.bitwise_and(warped_img, warped_img, mask=black_mask)
cv2.imwrite("black.jpg", masked_img)
actual_matrix_black = calculate_pieces(masked_img)
if compare_matrix(actual_matrix_white, problematic_matrix_white) and compare_matrix(actual_matrix_black, problematic_matrix_black):
print(prev_matrix_white)
print(actual_matrix_white)
move =
else:
print(prev_matrix_white)
print(actual_matrix_white)
move = calculate_move(prev_matrix_white, actual_matrix_white)
else:"""
print(prev_matrix_white)
print(actual_matrix_white)
move = calculate_move(prev_matrix_white, actual_matrix_white)
print(move)
write_serial(arduino, move)
elif cod == "whiteOK":
prev_matrix_white = actual_matrix_white
black_mask = calculate_masks(hsv, black_range)
masked_img = cv2.bitwise_and(warped_img, warped_img, mask=black_mask)
cv2.imwrite("black.jpg", masked_img)
prev_matrix_black = calculate_pieces(masked_img)
n_moves = n_moves + 1
file.write(str(n_moves) + ". Jugador mueve: " + str(move))
elif cod == "blackMoves":
img = take_pic(ip)
warped_img = warp_image(img, perspective_mat)
cv2.imwrite("warped.jpg", warped_img)
hsv = cv2.cvtColor(warped_img, cv2.COLOR_BGR2HSV)
black_mask = calculate_masks(hsv, black_range)
masked_img = cv2.bitwise_and(warped_img, warped_img, mask=black_mask)
cv2.imwrite("black.jpg", masked_img)
actual_matrix_black = calculate_pieces(masked_img)
print(prev_matrix_black)
print(actual_matrix_black)
move = calculate_move(prev_matrix_black, actual_matrix_black)
print(move)
write_serial(arduino, move)
elif cod == "blackOK":
prev_matrix_black = actual_matrix_black
white_mask = calculate_masks(hsv, white_range)
masked_img = cv2.bitwise_and(warped_img, warped_img, mask=white_mask)
cv2.imwrite("white.jpg", masked_img)
prev_matrix_white = calculate_pieces(masked_img)
n_moves = n_moves + 1
file.write(str(n_moves) + ". Maquina mueve: " + str(move))
elif cod == "whiteWins":
file.write(str(n_moves) + ". Jugador mueve: " + str(move) + ". JAQUE MATE!\n")
file.close()
break
elif cod == "blackWins":
file.write(str(n_moves) + ". Maquina mueve: " + str(move) + ". JAQUE MATE!\n")
file.close()
break
file.close()
La función del programa de Arduino es gestionar la partida de ajedrez, comprobar los movimientos realizados, calcular los movimientos de la máquina, y actuar como interfaz para el usuario. Para todo lo relacionado con los movimientos de ajedrez, hemos utilizado el código Chessuino, creado por Diego Cuevas. Este algoritmo es capaz de comprobar si un movimiento está permitido, y de calcular un movimiento para jugar contra él. El código es el siguiente:
#include <LiquidCrystal.h>
const int rs = 12, en=11, d4=2, d5=3, d6=4, d7=5;
LiquidCrystal lcd (rs, en,d4 , d5, d6,d7);
const int buttonPin=7;
int buttonState=0;
#define W while
#define M 0x88
#define S 128
#define I 8000
#define MYRAND_MAX 65535 /* 16bit pseudo random generator */
long N, T; /* N=evaluated positions+S, T=recursion limit */
short Q, O, K, R, k = 16; /* k=moving side */
char *p, c[5], Z; /* p=pointer to c, c=user input, computer output, Z=recursion counter */
char L,
w[] = {0, 2, 2, 7, -1, 8, 12, 23}, /* relative piece values */
o[] = { -16, -15, -17, 0, 1, 16, 0, 1, 16, 15, 17, 0, 14, 18, 31, 33, 0, /* step-vector lists */
7, -1, 11, 6, 8, 3, 6, /* 1st dir. in o[] per piece*/
6, 3, 5, 7, 4, 5, 3, 6
}; /* initial piece setup */
/* board is left part, center-pts table is right part, and dummy */
char b[] = {
22, 19, 21, 23, 20, 21, 19, 22, 28, 21, 16, 13, 12, 13, 16, 21,
18, 18, 18, 18, 18, 18, 18, 18, 22, 15, 10, 7, 6, 7, 10, 15,
0, 0, 0, 0, 0, 0, 0, 0, 18, 11, 6, 3, 2, 3, 6, 11,
0, 0, 0, 0, 0, 0, 0, 0, 16, 9, 4, 1, 0, 1, 4, 9,
0, 0, 0, 0, 0, 0, 0, 0, 16, 9, 4, 1, 0, 1, 4, 9,
0, 0, 0, 0, 0, 0, 0, 0, 18, 11, 6, 3, 2, 3, 6, 11,
9, 9, 9, 9, 9, 9, 9, 9, 22, 15, 10, 7, 6, 7, 10, 15,
14, 11, 13, 15, 12, 13, 11, 14, 28, 21, 16, 13, 12, 13, 16, 21, 0
};
unsigned int seed = 0;
int mn = 1;//move number
char lastH[5], lastM[5]; //arrays to hold Human / Machine move characters + 1 null
bool stringComplete = false; // whether the string is complete
int lengthMove=0;
void setup() {
Serial.begin(9600);
pinMode(buttonPin,INPUT);
lcd.begin (16,2);
String cod;
delay(1000);
do{
lcd.print("Esperando");
lcd.setCursor(0, 1);
lcd.print("conexion...");
lcd.home();
while(Serial.available()==0){}
cod = leerCodigo();
}while(cod != "Connected");
lcd.clear();
lcd.print("Conectado");
delay(1500);
Serial.println("Connected");
lcd.clear();
do{
lcd.print("Pulsa para");
lcd.setCursor(0, 1);
lcd.print("calibrar");
lcd.home();
buttonState=digitalRead(buttonPin);
if(buttonState==HIGH){
Serial.println("calibrate");
lcd.clear();
lcd.print("Calibrando...");
Serial.flush();
while(Serial.available()==0){}
cod = leerCodigo();
if(cod != "0"){
lcd.clear();
lcd.print("Error calibrando");
delay(1000);
}
lcd.clear();
}
}while(cod != "0");
Serial.flush();
do{
Serial.flush();
lcd.print("Pulsa para");
lcd.setCursor(0, 1);
lcd.print("iniciar partida");
lcd.home();
buttonState=digitalRead(buttonPin);
if(buttonState==HIGH){
Serial.println("startGame");
lcd.clear();
lcd.print("Reconociendo...");
while(Serial.available()==0){}
cod = leerCodigo();
if(cod != "1"){
lcd.clear();
lcd.print("Piezas mal");
lcd.setCursor(0, 1);
lcd.print("colocadas");
delay(1000);
lcd.home();
lcd.print("Pulsa para");
lcd.setCursor(0, 1);
lcd.print("iniciar partida");
lcd.home();
}
lcd.clear();
}
}while(cod != "1");
lcd.print("Realizar");
lcd.setCursor(0, 1);
lcd.print("movimiento");
lcd.home();
lastH[0] = 0;
}
void loop() {
buttonState=digitalRead(buttonPin);
if(buttonState==HIGH){
lcd.clear();
lcd.home();
int r;
if(stringComplete==false){
leerMovimiento("whiteMoves");
}
lcd.print("Nuevo movimiento");
lcd.setCursor(0, 1);
lcd.print(c[0]);
lcd.print(c[1]);
lcd.print(c[2]);
lcd.print(c[3]);
delay(0);
lengthMove=0;
stringComplete = false;
lcd.clear();
lcd.print("Calculando..."); /* Turn for ARDUINO */
delay(0);
lcd.home();
K = *c - 16 * c[1] + 799, L = c[2] - 16 * c[3] + 799; /* parse entered move */
N = 0;
T = 0x3F; /* T=Computer Play strength */
r = D(-I, I, Q, O, 1, 3); /* Check & do the human movement */
if ( !(r > -I + 1) ) {
lcd.print("Has ganado");
lcd.setCursor(0,1);
gameOver();
delay(2000);
}
if (k == 0x10) { /* The flag turn must change to 0x08 */
lcd.print("Mov incorrecto");
lcd.setCursor(0, 1);
lcd.print("Realice otro");
lcd.home();
return;
}
strcpy(lastH, c); /* Valid human movement */
Serial.println("whiteOK");
mn++; /* Next move */
K = I;
N = 0;
T = 0x3F; /* T=Computer Play strength */
r = D(-I, I, Q, O, 1, 3); /* Think & do*/
if ( !(r > -I + 1) ) {
lcd.clear();
lcd.print("Has ganado");
lcd.setCursor(0,1);
gameOver();
delay(2000);
}
if(k == 0x08){ /* Some times the algorithm do not */
lcd.setCursor(10, 1); /* execute the move and do not change */
lcd.print("ERR 3 "); /* the turn flag */
gameOver(); /* 1. b1c3 c7c5? 2. f2f4? */
}
strcpy(lastM, c); /* Valid ARDUINO movement */
r = D(-I, I, Q, O, 1, 3);
if ( !(r > -I + 1) ) {
lcd.clear();
lcd.print("Jaque mate");
lcd.setCursor(0,1);
Serial.println(lastM);
lcd.print(lastM);
delay(2000);
lcd.clear();
lcd.home();
lcd.print("Has perdido");
gameOver();
delay(2000);
}
if(k == 0x08){ /* Some times the algorithm do not */
lcd.setCursor(10, 1); /* execute the move and do not change */
lcd.print("ERR 3 "); /* the turn flag */
gameOver(); /* 1. b1c3 c7c5? 2. f2f4? */
}
/////////////////////////////////////Computer move section
do{
lcd.print("Maquina mueve");
lcd.setCursor(0,1);
lcd.print(lastM);
lcd.home();
buttonState=digitalRead(buttonPin);
if(buttonState==HIGH){
leerMovimiento("blackMoves");
if ((lastM[0] != c[0]) or (lastM[1] != c[1]) or (lastM[2] != c[2]) or (lastM[3] != c[3])){
lcd.clear();
lcd.print("Movimiento");
lcd.setCursor(0, 1);
lcd.print("incorrecto");
lcd.clear();
}
stringComplete = false;
}
}while((lastM[0] != c[0]) or (lastM[1] != c[1]) or (lastM[2] != c[2]) or (lastM[3] != c[3]));
Serial.println("blackOK");
lcd.clear();
lcd.print("Realizar");
lcd.setCursor(0, 1);
lcd.print("movimiento");
lcd.home();
}
}
/////////////////////////////////FIN LOOP///////////////////////////////////
String leerCodigo(){
if (Serial.available()) {
String data = "";
while (Serial.available()) {
char a = Serial.read();
data += a;
delay(2);
}
return data;
}
}
void leerMovimiento(String cod){
Serial.flush();
Serial.println(cod);
lcd.clear();
lcd.print("Leyendo...");
lcd.home();
while(Serial.available()==0){}
if (Serial.available()) {
String data = "";
while (Serial.available()) {
char a = Serial.read();
data += a;
delay(2);
}
c[0]=data[0];
data.remove(0, 1);
c[1]=data[0];
data.remove(0, 1);
c[2]=data[0];
data.remove(0, 1);
c[3]=data[0];
data.remove(0, 1);
c[4]=0;
data.remove(0, data.length());
stringComplete=true;
}
}
unsigned short myrand(void) {
unsigned short r = (unsigned short)(seed % MYRAND_MAX);
return r = ((r << 11) + (r << 7) + r) >> 1;
}
/* recursive minimax search */
short D(short q, short l, short e, unsigned char E, unsigned char z, unsigned char n) {
short m, v, i, P, V, s;
unsigned char t, p, u, x, y, X, Y, H, B, j, d, h, F, G, C;
signed char r;
if (++Z > 30) { /* stack underrun check */
--Z; return e;
}
q--; /* adj. window: delay bonus */
k ^= 24; /* change sides */
d = Y = 0; /* start iter. from scratch */
X = myrand() & ~M; /* start at random field */
W(d++ < n || d < 3 || /* iterative deepening loop */
z & K == I && (N < T & d < 98 || /* root: deepen upto time */
(K = X, L = Y & ~M, d = 3))) /* time's up: go do best */
{ x = B = X; /* start scan at prev. best */
h = Y & S; /* request try noncastl. 1st*/
P = d < 3 ? I : D(-l, 1 - l, -e, S, 0, d - 3); /* Search null move */
m = -P < l | R > 35 ? d > 2 ? -I : e : -P; /* Prune or stand-pat */
++N; /* node count (for timing) */
do {
u = b[x]; /* scan board looking for */
if (u & k) { /* own piece (inefficient!)*/
r = p = u & 7; /* p = piece type (set r>0) */
j = o[p + 16]; /* first step vector f.piece*/
W(r = p > 2 & r < 0 ? -r : -o[++j]) /* loop over directions o[] */
{ A: /* resume normal after best */
y = x; F = G = S; /* (x,y)=move, (F,G)=castl.R*/
do { /* y traverses ray, or: */
H = y = h ? Y ^ h : y + r; /* sneak in prev. best move */
if (y & M)break; /* board edge hit */
m = E - S & b[E] && y - E < 2 & E - y < 2 ? I : m; /* bad castling */
if (p < 3 & y == E)H ^= 16; /* shift capt.sqr. H if e.p.*/
t = b[H]; if (t & k | p < 3 & !(y - x & 7) - !t)break; /* capt. own, bad pawn mode */
i = 37 * w[t & 7] + (t & 192); /* value of capt. piece t */
m = i < 0 ? I : m; /* K capture */
if (m >= l & d > 1)goto C; /* abort on fail high */
v = d - 1 ? e : i - p; /* MVV/LVA scoring */
if (d - !t > 1) /* remaining depth */
{ v = p < 6 ? b[x + 8] - b[y + 8] : 0; /* center positional pts. */
b[G] = b[H] = b[x] = 0; b[y] = u | 32; /* do move, set non-virgin */
if (!(G & M))b[F] = k + 6, v += 50; /* castling: put R & score */
v -= p - 4 | R > 29 ? 0 : 20; /* penalize mid-game K move */
if (p < 3) /* pawns: */
{ v -= 9 * ((x - 2 & M || b[x - 2] - u) + /* structure, undefended */
(x + 2 & M || b[x + 2] - u) - 1 /* squares plus bias */
+ (b[x ^ 16] == k + 36)) /* kling to non-virgin King */
- (R >> 2); /* end-game Pawn-push bonus */
V = y + r + 1 & S ? 647 - p : 2 * (u & y + 16 & 32); /* promotion or 6/7th bonus */
b[y] += V; i += V; /* change piece, add score */
}
v += e + i; V = m > q ? m : q; /* new eval and alpha */
C = d - 1 - (d > 5 & p > 2 & !t & !h);
C = R > 29 | d < 3 | P - I ? C : d; /* extend 1 ply if in check */
do
s = C > 2 | v > V ? -D(-l, -V, -v, /* recursive eval. of reply */
F, 0, C) : v; /* or fail low if futile */
W(s > q&++C < d); v = s;
if (z && K - I && v + I && x == K & y == L) /* move pending & in root: */
{ Q = -e - i; O = F; /* exit if legal & found */
R += i >> 7; --Z; return l; /* captured non-P material */
}
b[G] = k + 6; b[F] = b[y] = 0; b[x] = u; b[H] = t; /* undo move,G can be dummy */
}
if (v > m) /* new best, update max,best*/
m = v, X = x, Y = y | S & F; /* mark double move with S */
if (h) {
h = 0; /* redo after doing old best*/
goto A;
}
if (x + r - y | u & 32 | /* not 1st step,moved before*/
p > 2 & (p - 4 | j - 7 || /* no P & no lateral K move,*/
b[G = x + 3 ^ r >> 1 & 7] - k - 6 /* no virgin R in corner G, */
|| b[G ^ 1] | b[G ^ 2]) /* no 2 empty sq. next to R */
)t += p < 5; /* fake capt. for nonsliding*/
else F = y; /* enable e.p. */
} W(!t); /* if not capt. continue ray*/
}
}
} W((x = x + 9 & ~M) - B); /* next sqr. of board, wrap */
C: if (m > I - M | m < M - I)d = 98; /* mate holds to any depth */
m = m + I | P == I ? m : 0; /* best loses K: (stale)mate*/
if (z && d > 2)
{ *c = 'a' + (X & 7); c[1] = '8' - (X >> 4); c[2] = 'a' + (Y & 7); c[3] = '8' - (Y >> 4 & 7); c[4] = 0;
char buff[150];
}
} /* encoded in X S,8 bits */
k ^= 24; /* change sides back */
--Z; return m += m < e; /* delayed-loss bonus */
}
void gameOver() {
Serial.print("Partida acabada");
lcd.clear();
lcd.print("Partida acabada");
}
Problemas y Soluciones
Algunos problemas que hemos tenido durante la realización del proyecto han sido:
- Queríamos utilizar una cámara conectada directamente a la placa de Arduino pero su resolución era bastante baja y tuvimos que utilizar un teléfono móvil.
- Para poder conectar el móvil y el ordenador usamos una aplicación externa pasando la dirección IP. Sin embargo, algunas redes no nos permiten acceder a ella (como por ejemplo la red de la universidad eduroam) y tuvimos que conectarnos a los datos de otro teléfono móvil.
- La calibración del sistema para poder reconocer los colores correctamente y por tanto las piezas del tablero. El tablero suele reflejar luz y esto provoca que entren en juego colores que no sirven para la segmentación y por tanto no se reconoce bien el tablero. La solución que hemos optado es tomar al comienzo de la partida una foto del tablero y tomar las referencias de los colores de algunos puntos del tablero para conocer que escalas de colores disponemos en ese momento y aplicar algoritmos (gradiente, medias…) para intentar encontrar los intervalos de cada color y así poder diferenciarlos.
- El principal problema ha sido el algoritmo de ajedrez. Hemos dedicado bastante tiempo a encontrar un algoritmo funcional que se ejecutase en Arduino hasta que encontramos el algoritmo diseñado por Diego Cuevas. Sin embargo, debido a la inexistente documentación y ayuda, nos ha sido bastante difícil depurar y comprender el código para fusionarlo con nuestro proyecto. Además durante el proceso nos percatamos de que era bastante básico ya que en determinadas ocasiones el algoritmo suele repetir los movimientos. Un ejemplo puede ser el primer movimiento de la máquina que siempre será b8c6 independientemente del movimiento del usuario.
Mejoras
Algunas mejoras que podrían aplicarse a nuestro proyecto serían:
- Elegir el lado donde juega el usuario, es decir, poder elegir el lado de las fichas blancas o negras ya que actualmente solo se puede el lado de las blancas.
- Elegir la dificultad del algoritmo de la máquina.
- Diseñar un brazo robótico que mueva automáticamente las fichas de la máquina
- Sistema de puntuación y clasificación.
- Crear una simulación de la partida a partir de los movimientos y de las imágenes.
Demostración
A continuación algunos vídeos que mostraran una pequeña demostración de nuestro proyecto:
Conectado
Calibrar el tablero
Seleccionar tiempo
Piezas mal colocadas
Movimiento correcto
Movimiento incorrecto
Partida terminada