noviembre 21, 2019

4RAYA-Prepara el entorno para el programa

Written by

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()
Ventana del programa con Python 3.8

Category : 4 EN RAYA ROBÓTICO

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Proudly powered by WordPress and Sweet Tech Theme