Written by admin
Escogemos el Python 3 para revisar el programa de 4 en raya. Como el que se usaba en la primera versión estaba hecho para Python 2.7 hay que hacer algunos cambios.
En primer lugar instalamos en el ordenador la versión moderna de Python, y para ello seguimos las instrucciones que hay en el tutorial:
http://tecnologia.iesciudadjardin.es/?p=3643
Además tenemos que instalar algunas librerías adicionales para que funcione el programa, así que abrimos una ventana de comandos, ejecutando como administrador y escribimos los siguientes:
pip install pyserial
pip install pillow
pip install imutils
Hacemos unos pequeños cambios en el programa y lo probamos.
# Programa de 4 en raya con tablero gráfico para brazo robot
# Algoritmo Minimax alfa-beta truncado por tiempo
# Python 3.8
#
# Fecha: 23/11/2019
######################## LIBRERÍAS ######################
from tkinter import *
from tkinter import messagebox
from PIL import Image
from PIL import ImageTk
import imutils
from imutils.video import VideoStream
from copy import deepcopy
import time
import serial
import cv2
######################## DEFINICIÓN DE CONSTANTES ######################
ANCHOTBL = 7 # Número de fichas en horizontal
ALTOTBL = 6 # Número de fichas en vertical
CAMARA = 0 # 0=cámara por defecto, 1=cámara USB
PUERTO = 'COM9' # Puerto serie para comunicaciones
COLORHUM = 'red' # Color de fichas del humano
COLORORD = 'turquoise' # Color de fichas del ordenador
COLORFICHA = (240,40,40) # Color RGB para considerar la ficha roja
DISTCOLOR = 30 # Distancia máxima del color para ser considerado correcto
NUMJUGADABIBL=4 # Número de jugadas a hacer por biblioteca
FICHAPHUM='FichApHum.bin' # Fichero de aperturas cuando inicia el humano
FICHAPORD='FichApOrd.bin' # Fichero de aperturas cuando inicia el ordenador
JUEGONORMAL=0 # Modo de juego normal
JUEGOPRUEBA=1 # Modo de juego sin cámara ni robot
MODOJUEGO=JUEGOPRUEBA
####################### CLASE PARA LÓGICA DE JUEGO #####################
class Board:
nodes = {}
def __init__(self,other=None):
self.player = 'H'
self.opponent = 'O'
self.empty = '.'
self.width = ANCHOTBL
self.height = ALTOTBL
self.NumJugada=0
self.Jugadas=[]
self.fields = {}
for y in range(self.height):
for x in range(self.width):
self.fields[x,y] = self.empty
# copy constructor
if other:
self.__dict__ = deepcopy(other.__dict__)
# Comprueba un movimiento válido en una columna en el tablero activo
def move(self,x):
board = Board(self)
for y in range(board.height):
if board.fields[x,y] == board.empty:
board.fields[x,y] = board.player
break
board.player,board.opponent = board.opponent,board.player
return board
def __heuristic(self):
return self.__heuristic_score(self.player)-self.__heuristic_score(self.opponent)
def __heuristic_score(self, player):
lines = self.__winlines(player)
winpositions = self.__winpositions(lines,player)
score = 0
for x in range(self.width):
for y in range(self.height-1,0,-1):
win = winpositions.get("{0},{1}".format(x,y),False)
below = winpositions.get("{0},{1}".format(x,y-1),False)
if win and below:
score+=self.height-y*100
for line in lines:
pieces = 0
height = []
for x,y in line:
if self.fields[x,y] == player:
pieces = pieces + 1
elif self.fields[x,y] == self.empty:
height.append(y)
heightscore = self.height - int(sum(height) / float(len(height)))
score=score+pieces*heightscore
return score
def __winpositions(self, lines, player):
lines = self.__winlines(player)
winpositions = {}
for line in lines:
pieces = 0
empty = None
for x,y in line:
if self.fields[x,y] == player:
pieces = pieces + 1
elif self.fields[x,y] == self.empty:
if not empty == None:
break
empty = (x,y)
if pieces==3:
winpositions["{0},{1}".format(x,y)]=True
return winpositions
def __winlines(self, player):
lines = []
# horizontal
for y in range(self.height):
winning = []
for x in range(self.width):
if self.fields[x,y] == player or self.fields[x,y] == self.empty:
winning.append((x,y))
if len(winning) >= 4:
lines.append(winning[-4:])
else:
winning = []
# vertical
for x in range(self.width):
winning = []
for y in range(self.height):
if self.fields[x,y] == player or self.fields[x,y] == self.empty:
winning.append((x,y))
if len(winning) >= 4:
lines.append(winning[-4:])
else:
winning = []
# diagonal
winning = []
for cx in range(self.width-1):
sx,sy = max(cx-2,0),abs(min(cx-2,0))
winning = []
for cy in range(self.height):
x,y = sx+cy,sy+cy
if x<0 or y<0 or x>=self.width or y>=self.height:
continue
if self.fields[x,y] == player or self.fields[x,y] == self.empty:
winning.append((x,y))
if len(winning) >= 4:
lines.append(winning[-4:])
else:
winning = []
# other diagonal
winning = []
for cx in range(self.width-1):
sx,sy = self.width-1-max(cx-2,0),abs(min(cx-2,0))
winning = []
for cy in range(self.height):
x,y = sx-cy,sy+cy
if x<0 or y<0 or x>=self.width or y>=self.height:
continue
if self.fields[x,y] == player or self.fields[x,y] == self.empty:
winning.append((x,y))
if len(winning) >= 4:
lines.append(winning[-4:])
else:
winning = []
# return
return lines
def __iterative_deepening(self,think):
g = (3,None)
start = time.time()
for d in range(1,10):
g = self.__mtdf(g, d)
if time.time()-start>think:
break
return g;
def __mtdf(self, g, d):
upperBound = +1000
lowerBound = -1000
best = g
while lowerBound < upperBound:
if g[0] == lowerBound:
beta = g[0]+1
else:
beta = g[0]
g = self.__minimax(True, d, beta-1, beta)
if g[1]!=None:
best = g
if g[0] < beta:
upperBound = g[0]
else:
lowerBound = g[0]
return best
def __minimax(self, player, depth, alpha, beta):
lower = Board.nodes.get(str(self)+str(depth)+'lower',None)
upper = Board.nodes.get(str(self)+str(depth)+'upper',None)
if lower != None:
if lower >= beta:
return (lower,None)
alpha = max(alpha,lower)
if upper != None:
if upper <= alpha:
return (upper,None)
beta = max(beta,upper)
if self.won():
if player:
return (-999,None)
else:
return (+999,None)
elif self.tied():
return (0,None)
elif depth==0:
return (self.__heuristic(),None)
elif player:
best = (alpha,None)
for x in range(self.width):
if self.fields[x,self.height-1]==self.empty:
value = self.move(x).__minimax(not player,depth-1,best[0],beta)[0]
if value>best[0]:
best = value,x
if value>beta:
break
else:
best = (beta,None)
for x in range(self.width):
if self.fields[x,self.height-1]==self.empty:
value = self.move(x).__minimax(not player,depth-1,alpha,best[0])[0]
if value<best[0]:
best = value,x
if alpha>value:
break
if best[0] <= alpha:
Board.nodes[str(self)+str(depth)+"upper"] = best[0]
Board.nodes[self.__mirror()+str(depth)+"upper"] = best[0]
elif best[0] >= beta:
Board.nodes[str(self)+str(depth)+"lower"] = best[0]
Board.nodes[self.__mirror()+str(depth)+"lower"] = best[0]
return best
# Obtiene el mejor movimiento en el tiempo fijado
def best(self):
if self.NumJugada < NUMJUGADABIBL:
if self.NumJugada==0: # Jugada 1
posic=self.Jugadas[0]
elif self.NumJugada==1: # Jugada 2
posic=7*(1+self.Jugadas[0])+self.Jugadas[1]
elif self.NumJugada==2: # Jugada 3
posic=7+7*7*(1+self.Jugadas[0])+7*self.Jugadas[1]+self.Jugadas[2]
elif self.NumJugada==3: # Jugada 4
posic=7+7*7+7*7*7+7*7*7*self.Jugadas[0]+7*7*self.Jugadas[1]+7*self.Jugadas[2]+self.Jugadas[3]
elif self.NumJugada==4: # Jugada 5
posic=7+7*7+7*7*7+7*7*7*7+7*7*7*7*self.Jugadas[0]+7*7*7*self.Jugadas[1]+7*7*self.Jugadas[2]+7*self.Jugadas[3]+self.Jugadas[4]
Fich=open(self.FichAp,"rb")
Fich.read(posic)
move=int(Fich.read(1))
if move<0 or move>=ANCHOTBL:
move=self.__iterative_deepening(1)[1]
print(move)
Fich.close()
return move
else:
return self.__iterative_deepening(1)[1]
# Comprueba si hay fichas por poner en el tablero
def tied(self):
for (x,y) in self.fields:
if self.fields[x,y]==self.empty:
return False
return True
# Comprueba si se ha ganado (4 en línea)
def won(self):
# horizontal
for y in range(self.height):
winning = []
for x in range(self.width):
if self.fields[x,y] == self.opponent:
winning.append((x,y))
if len(winning) == 4:
return winning
else:
winning = []
# vertical
for x in range(self.width):
winning = []
for y in range(self.height):
if self.fields[x,y] == self.opponent:
winning.append((x,y))
if len(winning) == 4:
return winning
else:
winning = []
# diagonal /
winning = []
for cx in range(self.width-1):
sx,sy = max(cx-2,0),abs(min(cx-2,0))
winning = []
for cy in range(self.height):
x,y = sx+cy,sy+cy
if x<0 or y<0 or x>=self.width or y>=self.height:
continue
if self.fields[x,y] == self.opponent:
winning.append((x,y))
if len(winning) == 4:
return winning
else:
winning = []
# diagonal \
winning = []
for cx in range(self.width-1):
sx,sy = self.width-1-max(cx-2,0),abs(min(cx-2,0))
winning = []
for cy in range(self.height):
x,y = sx-cy,sy+cy
if x<0 or y<0 or x>=self.width or y>=self.height:
continue
if self.fields[x,y] == self.opponent:
winning.append((x,y))
if len(winning) == 4:
return winning
else:
winning = []
# default
return None
def __mirror(self):
string = ''
for y in range(self.height):
for x in range(self.width):
string+=' '+self.fields[self.width-1-x,self.height-1-y]
string+="\n"
return string
def __str__(self):
string = ''
for y in range(self.height):
for x in range(self.width):
string+=' '+self.fields[x,self.height-1-y]
string+="\n"
return string
######################### CLASE PARA GESTIÓN DE VÍDEO #######################
class VideoImagen:
def __init__(self, root, Board):
# Activa objeto padre y panel de imagen
self.root = root
self.panel = None
# Inicializa el objeto para gestión de la cámara y
# detección de fichas para la jugada del humano
self.cam = cv2.VideoCapture(CAMARA)
self.imCanvas=Canvas(self.root)
self.imCanvas.place(x=460,y=25)
self.board=Board
self.frame = None
self.thread = None
self.stopEvent = None
self.JugadaHumano=False
def videoLoop(self, Huecos):
# Captura posibles excepciones en el manejo del vídeo
try:
self.Salir=False
self.JugadaHumano=False
print(Huecos)
# Bucle para mostrar imágenes de la cámara hasta que se haga la jugada humana
while (not self.JugadaHumano) and (not self.Salir):
# Toma imagen de vídeo y la ajusta a 320 pixel
retval,self.frame = self.cam.read()
self.frame = imutils.resize(self.frame, width=320)
# OpenCV represents images in BGR order; however PIL
# represents images in RGB order, so we need to swap
# the channels, then convert to PIL and ImageTk format
image1 = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB)
image2 = Image.fromarray(image1)
width, height = image2.size
print(str(width)+','+str(height))
image3 = ImageTk.PhotoImage(image2)
# Dibuja unas líneas para situar las fichas
self.imCanvas.create_image(0,0,anchor=NW,image=image3)
for x in range(ANCHOTBL):
self.imCanvas.create_line(23+45*x, 10, 23+45*x, 230, fill="red")
for y in range(ALTOTBL):
self.imCanvas.create_line(10,20+40*y, 310, 20+40*y, fill="red")
self.root.update()
# Comprueba si se ha puesto una nueva ficha
for (x,y) in Huecos:
if self.ficha_en(image1,x,y):
self.JugadaHumano=True
self.ColumnaJugada=x
break;
time.sleep(1)
except RuntimeError as err:
print("RAYA4: error en ejecución")
# Comprueba si hay una ficha en una posición del tablero
# Devuelve True si el color en 9 pixel alrededor es el buscado
def ficha_en(self,imagen,cx,cy):
px=23+45*cx
py=20+40*(ALTOTBL-cy-1)
print(str(cx)+','+str(cy)+'-'+str(imagen[py,px]))
for x in range(3):
for y in range(3):
# dist=abs(imagen[py-1+y,px-1+x][0]-COLORFICHA[0])+abs(imagen[py-1+y,px-1+x][1]-COLORFICHA[1])+abs(imagen[py-1+y,px-1+x][2]-COLORFICHA[2])
dist=abs(imagen[py-1+y,px-1+x][0]-COLORFICHA[0])
print(dist)
if dist > DISTCOLOR:
return False
return True
# Apaga la cámara y cierra el programa
def Close(self):
self.Salir=True
self.cam.release();
####################### CLASE PARA COMUNICACIÓN SERIE #####################
class Serie:
# Inicializa el puerto de comunicaciones
def __init__(self):
self.ErrorCom=False
try:
self.ser = serial.Serial(
port=PUERTO,
baudrate=9600,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS
)
except IOError as err:
messagebox.showerror('ERROR','No se puede abrir el puerto COM')
self.ErrorCom=True
# Envía un mensaje por el puerto
def Envia(self,txt):
if not self.ErrorCom:
self.ser.write(txt)
# Recibe un mensaje por el puerto
def Recibe(self):
if not self.ErrorCom:
out=''
while self.ser.inWaiting() > 0:
out += self.ser.read(1)
return out
# Cierra el puerto serie
def Close(self):
if not self.ErrorCom:
self.ser.close()
######################## CLASE DE INTERFAZ Y TABLERO ######################
class Tablero:
# Inicializa entorno con tablero botones y opciones
def __init__(self):
self.app = Tk()
self.app.title('4 EN RAYA')
self.app.geometry("790x300")
self.app.resizable(width=False, height=False)
self.JugadorInicial=IntVar()
self.txMensaje=StringVar()
self.PrimeraJugada=True
self.Respuesta=-1
self.Salir=False
self.board = Board()
self.frame = Frame(self.app, borderwidth=1, relief="raised")
self.tiles = {}
self.frame.grid(row=1, column=0, columnspan=self.board.width)
for x,y in self.board.fields:
tile = Canvas(self.frame, width=50, height=50, bg="navy", highlightthickness=0)
tile.grid(row=self.board.height-1-y, column=x)
self.tiles[x,y] = tile
self.update()
# Etiquetas y opciones
lb1=Label(self.app, text='Jugador inicial:')
lb1.place(x=360,y=10)
lb2=Label(self.app, text='Cámara')
lb2.place(x=460,y=10)
self.rbH=Radiobutton(self.app,text='Jugador',variable=self.JugadorInicial,value=1)
self.rbH.place(x=360,y=30)
self.rbH.select()
self.rbO=Radiobutton(self.app,text='Ordenador',variable=self.JugadorInicial,value=2)
self.rbO.place(x=360,y=50)
# Botones
self.btInicioJuego=Button(self.app,text='Iniciar Juego',width=11,command=self.InicioJuego)
self.btInicioJuego.place(x=360,y=80)
self.btSalir=Button(self.app,text='Salir',width=11,command=self.FinJuego)
self.btSalir.place(x=360,y=120)
# Zona de mensajes
lb3=Label(self.app, text='MENSAJES',bg='light blue')
lb3.place(x=360,y=160)
self.lbMensaje=Label(self.app,textvariable=self.txMensaje,bg='light blue')
self.lbMensaje.place(x=360,y=180)
self.txMensaje.set('Esperando')
# Inicializa el juego
def reset(self):
self.board = Board()
self.update()
self.PrimeraJugada=True
# Realiza un movimiento en el juego
def move(self,x):
self.txMensaje.set('Pensando Jugada')
if (not self.PrimeraJugada) or (self.JugadorInicial == 1):
self.board = self.board.move(x)
self.board.Jugadas.append(x)
self.update()
self.app.update()
self.Respuesta = self.board.best()
else:
self.Respuesta=x
self.board.NumJugada=-1 # No tiene en cuenta la 1ª jugada del ordenador
if self.Respuesta!=None:
self.board = self.board.move(self.Respuesta)
self.update()
self.txMensaje.set('Esperando respuesta')
self.PrimeraJugada=False
self.board.NumJugada+=1
self.app.update()
# Comprueba los huecos en cada columna del tablero
# para saber cuáles pueden ser las siguientes jugadas
def huecos(self):
huecos=[]
for x in range(self.board.width):
for y in range(self.board.height):
if self.board.fields[x,y] == self.board.empty:
huecos.append((x,y))
break
return huecos
# Repinta el tablero y fichas
def update(self):
for (x,y) in self.board.fields:
text = self.board.fields[x,y]
if (text=='.'):
self.tiles[x,y].create_oval(5, 5, 45, 45, fill="black", outline="blue", width=1)
if (text=='H'):
self.tiles[x,y].create_oval(5, 5, 45, 45, fill=COLORHUM, outline="blue", width=1)
if (text=='O'):
self.tiles[x,y].create_oval(5, 5, 45, 45, fill=COLORORD, outline="blue", width=1)
# Comprueba si se ha ganado el juego para dibujar la jugada ganadora
winning = self.board.won()
if winning:
self.FinJuego=True
for x,y in winning:
self.tiles[x,y].create_oval(15, 15, 36, 36, fill="white")
# Inicia un juego con el jugador seleccionado
def InicioJuego(self):
self.JugadorInicial=self.JugadorInicial.get()
self.btInicioJuego['state']=DISABLED
if self.JugadorInicial==1:
self.rbO['state']=DISABLED
self.rbH['state']=DISABLED
else:
self.rbH['state']=DISABLED
self.rbO['state']=DISABLED
self.app.update()
# Inicializa los objetos cámara y comunicaciones serie
self.Com=Serie()
self.VI=VideoImagen(self.app,self.board)
# Activa el fichero de aperturas
if self.JugadorInicial == 1:
self.board.FichAp=FICHAPHUM
else:
self.board.FichAp=FICHAPORD
# Empieza el juego si el primero es el ordenador
if self.JugadorInicial == 2:
self.board.player = 'O'
self.board.opponent = 'H'
self.move(3)
OrdenRobot="C3"
self.Com.Envia(OrdenRobot)
self.txMensaje.set('Colocando ficha')
while (not self.Salir):
self.app.update()
time.sleep(1)
Respuesta=self.Com.Recibe()
if Respuesta!='':
print(Respuesta)
if (Respuesta=="F"):
break
# Bucle de movimiento por fotos
self.FinJuego=False
while (not self.FinJuego) and (not self.Salir):
if MODOJUEGO == JUEGONORMAL:
self.VI.videoLoop(self.huecos())
else:
self.VI.ColumnaJugada=input("Columna:")
if not self.Salir:
self.move(self.VI.ColumnaJugada)
OrdenRobot="C"+str(self.Respuesta)
print(OrdenRobot)
self.Com.Envia(OrdenRobot)
self.txMensaje.set('Colocando ficha')
if MODOJUEGO == JUEGONORMAL:
while (not self.Salir):
self.app.update()
time.sleep(1)
Respuesta=self.Com.Recibe()
if Respuesta!='':
print(Respuesta)
if (Respuesta=="F"):
break
self.txMensaje.set('Fin de partida')
# Fin del juego
# Apaga la cámara y cierra la ventana
def FinJuego(self):
print('Cerrando RAYA4')
self.Salir=True
self.VI.Close()
self.Com.Close()
self.app.destroy()
# Bucle principal del programa, para refresco de pantalla
def mainloop(self):
self.app.mainloop()
############### ENTRADA DEL PROGRAMA PRINCIPAL ###############
if __name__ == '__main__':
Tablero().mainloop()
Deja una respuesta