Arrastrar y soltar

Hola, me llamo Desirée y estoy haciendo el juego Torres de Hanói

y pretendo hacer un tutorial una vez esté completo. Pero estoy teniendo dificultades para hacer que funcione el arrastrar y soltar los discos. Utilizo el método aprender y pilas.habilidades.Arrastrable para poder mover los discos, pero necesito de alguna forma que al soltar el disco se ejecute una función, la cual debe encargarse de detectar si hay contacto (colisión) con uno de los postes y de ser así registrar el moviento. En concreto tengo dos problemas:

  1. Detectar cuando se suelta un disco
  2. Detectar la colisión entre la pieza soltada y los 3 posibles postes

Paso el código que tengo funcionando hasta ahora ( con solución automática incluída :sunglasses: ), para realizar movimientos manuales por ahora hay que llamar a la función realizar_movimiento(origen,destino), la cuál solo realiza el movimiento si éste es válido.

# coding: utf-8
import pilasengine

pilas = pilasengine.iniciar()

# Número de discos del juego
numero_discos = 5

# Estado del juego
#############
# El estado del juego se guarda en una lista de tres componentes,
# donde cada una corresponde a cada uno de los postes y cuyo valor
# es a su vez tambien una lista, que contiene los numeros de los
# discos que se encuentran en ese poste. Los discos se numeran de
# 0 a numero_discos desde el mas grande hasta el mas pequeño.
#
# Ejemplo con 5 discos: [[0,1,2],[4],[3]]
# en este ejemplo en el poste 1 (inicial) se encuentran los tres discos
# mas grandes, en el segundo se encuentra el mas pequeño y en el 
# tercero se encuentra el restante.
#
# De esta forma el estado del juego es valido siempre que las listas
# de los discos de cada poste se encuentren ordenadas de manera 
# ascendente

# Estado inicial del juego
juego = [range(numero_discos),[],[]]

# Número de movimientos realizados
movimientos = 0

ancho_max = 200
ancho_min = 50
alto = 20
delta = (ancho_max - ancho_min)/(numero_discos-1)
separacion_postes = 200
alto_postes = alto*(numero_discos+1)

def crear_rectangulo(ancho,alto,color,id=("tipo",0),colortexto=pilas.colores.negro):
    superficie = pilas.imagenes.cargar_superficie(ancho,alto)
    superficie.rectangulo(0,0,ancho,alto,color=color, relleno=True)
    superficie.rectangulo(0,0,ancho,alto,color=pilas.colores.negro, relleno=False,grosor=4)
    superficie.texto(str(id[1]), magnitud=12, color=colortexto,x=ancho/2-5)
    actor = pilas.actores.Actor(imagen=superficie)
    actor.crear_figura_de_colision_rectangular(0,0,ancho,alto)
    actor.id = id
    return actor

# Crear los postes y base
postes = [ crear_rectangulo(14,alto_postes,pilas.colores.negro,("poste",i),pilas.colores.blanco) for i in range (3) ]
for i in range(3):
    postes[i].x = separacion_postes*(i-1)
    postes[i].y = alto_postes/2-alto/2
base = crear_rectangulo(2*separacion_postes+ancho_max,alto,pilas.colores.negro)
base.y = -alto

# Crear los discos
discos = [ crear_rectangulo(ancho_max-delta*i,alto,pilas.colores.blanco,("disco",i)) for i in range(numero_discos)]
discos[-1].aprender(pilas.habilidades.Arrastrable)
for i in range(len(discos)):
    discos[i].y = alto*i
    discos[i].x = -separacion_postes

################################################################################
#                                                                              #
#                               Lógica del juego                               #
#                                                                              #
################################################################################

#
# Validar movimiento
#
# Esta funcion toma como entrada dos valores, el numero de poste
# desde el cual se saca
#
def validar_movimiento(origen,destino):
    if origen == destino:
        return False
    if len(juego[origen]) > 0:
        if len(juego[destino]) > 0:
            return juego[origen][-1] > juego[destino][-1]
        else:
            return True
    else:
        return False

#
# Realizar movimiento
#
ultimo_movimiento = "ninguno"
tiempo_movimiento = 0.6;
def realizar_movimiento(origen,destino):
    #realizar_movimiento_logico(origen,destino)
    #realizar_movimiento_grafico(origen,destino)
    if validar_movimiento(origen,destino):

        # realizar animacion
        x_inicial = separacion_postes*(origen-1)
        x_final = separacion_postes*(destino-1)
        y_inicial = alto*len(juego[origen])
        y_final = alto*len(juego[destino])
        discos[juego[origen][-1]].x = [x_inicial,x_final,x_final] , tiempo_movimiento/3
        discos[juego[origen][-1]].y = [alto_postes+30,alto_postes+30,y_final] , tiempo_movimiento/3
        global movimientos
        movimientos = movimientos + 1

        # actualizar habilidades
        if len(juego[origen]) > 1:
            discos[juego[origen][-2]].aprender(pilas.habilidades.Arrastrable)
        if len(juego[destino]) > 0:
            discos[juego[destino][-1]].eliminar_habilidades()

        # actualizar el estado del juego
        juego[destino].append(juego[origen].pop())
        global ultimo_movimiento
        ultimo_movimiento = (origen,destino)

        # mostrar el estado del juego
        global texto3
        texto3.texto = str(movimientos)
        pilas.avisar(str(juego))
        verificar_fin_de_juego()
    else:
        print("Movimiento inválido")

def verificar_fin_de_juego():
    for poste in juego[1:]: # todos los postes menos en el inicial (0)
        if len(poste) == numero_discos:
        # todos los discos están en este poste, si todos
        # los movimientos fueron válidos entonces la
        # torre debe estar completa y el juego ha sido
        # resuelto, no obstante vamos a verificar que los
        # discos estén en el orden correcto.
            for i in range(1,numero_discos):
                if poste[i]<poste[i-1]:
                    return False
            #pilas.actores.Texto("JUEGO RESUELTO!",y=200)
            #pilas.actores.Texto("numero de movimientos: "+str(movimientos),y=170)
            texto1.y = [200]
            #texto2.y = [170]
            #texto2.texto = "numero de movimientos: "+str(movimientos)
            pilas.avisar("Juego Resuelto! numero de movimientos: " + str(movimientos))
            return True
        elif len(poste) != 0:
        # los discos no están todos en el mismo poste
            return False
    return False

def reiniciar():
    global juego,movimientos,ultimo_movimiento
    juego = [range(numero_discos),[],[]]
    movimientos = 0
    ultimo_movimiento = (0,0)
    texto1.y=1200
    actualizar_posiciones()

def actualizar_posiciones():
    for p in range(len(juego)):
        for d in range(len(juego[p])):
            discos[juego[p][d]].x = [separacion_postes*(p-1)]
            discos[juego[p][d]].y = [alto*d]

resolviendo = False
def click_resolver():
    global resolviendo
    if not resolviendo:
        resolviendo = True
        resolver()

def resolver():
    global resolviendo
    if not verificar_fin_de_juego():
        paso_resolver()
        pilas.tareas.agregar(tiempo_movimiento,resolver)
    else:
        resolviendo = False

def paso_resolver():
    if not verificar_fin_de_juego():
        for i in range(1,3):
            if len(juego[i]) == 0:
                realizar_movimiento(0,i)
                return
        todos_los_movimientos = [ (0,1), (0,2), (1,0), (1,2), (2,0), (2,1) ]
        movimientos_validos = [ m for m in todos_los_movimientos if validar_movimiento(m[0],m[1]) ]
        movimientos_validos = [ m for m in movimientos_validos if (m[1],m[0]) != ultimo_movimiento ]
        movimientos_validos = [ m for m in movimientos_validos if len(juego[m[1]]) == 0 or juego[m[0]][-1]%2 != juego[m[1]][-1]%2 ]
        print(movimientos_validos)
        if len(movimientos_validos) > 1:
            print(movimientos_validos)
            tmp = [ m for m in movimientos_validos if juego[m[0]][-1] != numero_discos-1 ]
            if len(tmp) > 0:
                movimientos_validos = tmp
            print(movimientos_validos)
        if len(movimientos_validos) > 1:
            L = [ (len(juego[m[1]]),m) for m in movimientos_validos ]
            x = L[0]
            for m in L:
                if m[0] > x[0]:
                    x = m
            movimientos_validos = [x[1]]
        if len(movimientos_validos) == 1 :
            m = movimientos_validos[0]
            realizar_movimiento(m[0],m[1])
        else:
            print("Error! No se puede resolver... :(")


texto1 = pilas.actores.Texto("JUEGO RESUELTO!",y=1200)
texto2 = pilas.actores.Texto("numero de movimientos: ",y=-150)
texto3 = pilas.actores.Texto(str(movimientos),x=150,y=-150)

boton_paso_resolver = pilas.interfaz.Boton("Paso")
boton_paso_resolver.conectar(paso_resolver)
boton_paso_resolver.x = 60
boton_paso_resolver.y = -80

boton_resolver = pilas.interfaz.Boton("Resolver")
boton_resolver.conectar(click_resolver)
boton_resolver.x = 150
boton_resolver.y = -80

boton_reiniciar = pilas.interfaz.Boton("Reiniciar")
boton_reiniciar.conectar(reiniciar)
boton_reiniciar.x = 240
boton_reiniciar.y = -80

pilas.avisar(str(juego))    
pilas.ejecutar()

Desde ya muchas gracias y felicitaciones por tan interesante proyecto!

1 Like

Buenas, @Desiree0808 !

Sin duda un trabajo impresionante… :cold_sweat:

Aunque debo decir que tus conocimientos a buen seguro superan los mios, me voy a tomar la libertad de sugerirte que programes unas colisiones entre el disco que está siendo arrastrado y los postes.

Al realizarse la colision deberia ejecutarse la función [quote=“Desiree0808, post:1, topic:1107”]
realizar_movimiento(origen,destino)
[/quote]

Esta función ya la tienes programada correctamente, por lo que no deberia haber mayor problema.

Puedes consultar el manual sobre el tema de las colisiones, donde seguro encontrarás información.

Quizás seria más fácil detectar esas colisiones si palos y discos fueran actores diferentes (al menos es como yo lo hubiera probado en un principio)

Si aún así sigues teniendo dudas dínoslo y yo mismo intentaré programar unas colisiones entre los discos y las barras en un ejemplo sencillo partiendo de tu código.

Muchas gracias por tu interés y tu tiempo, y este maravilloso juego que estás creando! :smile:

Un abrazo.

Buenas, @Desiree0808 !

Después de realizar unas pruebas, me parece que lo mejor es crear unos puntos de colisión externos a las barras, ya que de inicio todos los discos están en colisión con las barras, y eso crea conflictos.

La colisión deberia llevar a la def validar_movimiento (que deberia actuar de “driver”), donde deberia verificarse si el movimiento es correcto y de ser asi llamar a la función def realizar_movimiento, pasándoles los parámetros…

pilas.colisiones.agregar(‘discos’,‘aceituna’, validar_movimiento)

Como ves validar_movimiento no lleva parámetros. Los parámetros los podria sacar del origen del disco y del Punto_col_ .control.

Espero que estas pequeñas pistas puedan ayudarte… lo cierto es que tu código supera mis conocimientos en muchos puntos, y no querria estar explicandote cosas que ya sabias… :smile:

Un abrazo.

Gracias jordinur por la ayuda,
pero sigo sin poder programar de forma sencilla la colisión. Por empezar no encuentro una forma de llamar a una función cuando se suelta el disco, se puede usar termina_click, pero esto no es lo mismo que cuando se suelta el disco, ya que este evento se ejecuta siempre que se suelta el click, sin importar si se estaba arrastrando o no el disco.
Las colisiones si las pude detectar utilizando figuras_en_contacto y actor_que_representa_como_area_de_colision como indica el manual.
Por lo que si bien puede usarse, el código resultante no es nada claro, situación no ideal para utilizar en un tutorial. Así que por ahora simplemente opté por utilizar 3 botones y también captar tres teclas de teclado.

El código resultante es el siguiente:

    # coding: utf-8
    import pilasengine

    pilas = pilasengine.iniciar()

    # Número de discos del juego
    numero_discos = 5

    # Estado del juego
    #
    # El estado del juego se guarda en una lista de tres componentes,
    # donde cada una corresponde a cada uno de los postes y cuyo valor
    # es a su vez tambien una lista, que contiene los numeros de los
    # discos que se encuentran en ese poste. Los discos se numeran de
    # 0 a numero_discos desde el mas grande hasta el mas pequeño.
    #
    # Ejemplo con 5 discos: [[0,1,2],[4],[3]]
    # en este ejemplo en el poste 1 (inicial) se encuentran los tres discos
    # mas grandes, en el segundo se encuentra el mas pequeño y en el 
    # tercero se encuentra el restante.
    #
    # De esta forma el estado del juego es valido siempre que las listas
    # de los discos de cada poste se encuentren ordenadas de manera 
    # ascendente

    # Estado inicial del juego
    juego = [range(numero_discos),[],[]]

    # Número de movimientos realizados
    movimientos = 0

    # Parámetros gráficos del juego
    ancho_max = 200
    ancho_min = 50
    alto = 20
    delta = (ancho_max - ancho_min)/(numero_discos-1)
    separacion_postes = 200
    alto_postes = alto*(numero_discos+1)

    def crear_rectangulo(ancho,alto,color,id=0,colortexto=pilas.colores.negro):
        superficie = pilas.imagenes.cargar_superficie(ancho,alto)
        superficie.rectangulo(0,0,ancho,alto,color=color, relleno=True)
        superficie.rectangulo(0,0,ancho,alto,color=pilas.colores.negro, relleno=False,grosor=4)
        superficie.texto(str(id), magnitud=12, color=colortexto,x=ancho/2-5)
        actor = pilas.actores.Actor(imagen=superficie)
        return actor

    # Crear los postes y base
    postes = [ crear_rectangulo(14,alto_postes,pilas.colores.negro,i,pilas.colores.blanco) for i in range (3) ]
    for i in range(3):
        postes[i].x = separacion_postes*(i-1)
        postes[i].y = alto_postes/2-alto/2
    base = crear_rectangulo(2*separacion_postes+ancho_max,alto,pilas.colores.negro)
    base.y = -alto

    # Crear los discos
    discos = [ crear_rectangulo(ancho_max-delta*i,alto,pilas.colores.blanco,i) for i in range(numero_discos)]
    for i in range(len(discos)):
        discos[i].y = alto*i
        discos[i].x = -separacion_postes

    ################################################################################
    #                                                                              #
    #                               Lógica del juego                               #
    #                                                                              #
    ################################################################################

    # Validar movimiento
    #
    # Esta funcion toma como entrada dos valores, el numero de poste
    # desde el cual se saca el disco y el número de poste en el cual
    # se quiere poner dicho disco, y devuelve True si es un movimiento
    # válido y False si no lo es
    #
    def validar_movimiento(origen,destino):
        if origen == destino:
            return False
        if len(juego[origen]) > 0:
            if len(juego[destino]) > 0:
                return juego[origen][-1] > juego[destino][-1]
            else:
                return True
        else:
            return False


    # Realizar movimiento
    #
    # Esta funcion toma como entrada dos valores, el numero de poste
    # desde el cual se saca el disco y el número de poste en el cual
    # se quiere poner dicho disco, y en caso de que el movimiento
    # sea válido lo realiza
    #
    historia_movimientos = []
    ultimo_movimiento = "ninguno"
    tiempo_movimiento = 0.6;
    def realizar_movimiento(origen,destino):
        if validar_movimiento(origen,destino):

            # realizar animacion
            x_inicial = separacion_postes*(origen-1)
            x_final = separacion_postes*(destino-1)
            y_inicial = alto*len(juego[origen])
            y_final = alto*len(juego[destino])
            discos[juego[origen][-1]].x = [x_inicial,x_final,x_final] , tiempo_movimiento/3
            discos[juego[origen][-1]].y = [alto_postes+30,alto_postes+30,y_final] , tiempo_movimiento/3
            global movimientos
            movimientos = movimientos + 1

            # actualizar el estado del juego
            juego[destino].append(juego[origen].pop())
            global ultimo_movimiento
            ultimo_movimiento = (origen,destino)
            historia_movimientos.append((origen,destino))

            # mostrar el estado del juego
            global texto3
            texto3.texto = str(movimientos)
            pilas.avisar(str(juego))
            verificar_fin_de_juego()
        else:
            pilas.avisar(u"Movimiento inválido")

    # Verificar fin de juego
    #
    # Esta funcion verifica el estado del juego y devuelve True si
    # el juego ha sido resuelto y False en caso contrario.
    def verificar_fin_de_juego():
        for poste in juego[1:]: # todos los postes menos en el inicial (0)
            if len(poste) == numero_discos:
            # todos los discos están en este poste, si todos
            # los movimientos fueron válidos entonces la
            # torre debe estar completa y el juego ha sido
            # resuelto, no obstante vamos a verificar que los
            # discos estén en el orden correcto.
                for i in range(1,numero_discos):
                    if poste[i]<poste[i-1]:
                        return False
                texto1.y = [200]
                pilas.avisar(u"Juego Resuelto! número de movimientos: " + str(movimientos))
                # Verificar si es un juego perfecto
                if movimientos == 2**numero_discos -1:
                    texto4.y = [-130]
                return True
            elif len(poste) != 0:
            # los discos no están todos en el mismo poste
                return False
        return False

    def reiniciar():
        global juego,movimientos,ultimo_movimiento
        juego = [range(numero_discos),[],[]]
        movimientos = 0
        ultimo_movimiento = (0,0)
        texto1.y=1200
        pilas.tareas.eliminar_todas()
        actualizar_posiciones()
        boton_resolver.activar()

    def actualizar_posiciones():
        for p in range(len(juego)):
            for d in range(len(juego[p])):
                discos[juego[p][d]].x = [separacion_postes*(p-1)]
                discos[juego[p][d]].y = [alto*d]

    def click_resolver():
        boton_resolver.desactivar()
        resolver()

    def resolver():
        if not verificar_fin_de_juego():
            pilas.tareas.agregar(tiempo_movimiento,resolver)
            paso_resolver()

    def paso_resolver():
        if not verificar_fin_de_juego():
            todos_los_movimientos = [ (0,1), (0,2), (1,0), (1,2), (2,0), (2,1) ]
            # Considerar sólo los movimientos válidos
            movimientos_validos = [ m for m in todos_los_movimientos if validar_movimiento(m[0],m[1]) ]
            # No deshacer el último movimiento
            movimientos_validos = [ m for m in movimientos_validos if (m[1],m[0]) != ultimo_movimiento ]
            # No poner discos pares sobre pares o impares sobre impares
            movimientos_validos = [ m for m in movimientos_validos if len(juego[m[1]]) == 0 or juego[m[0]][-1]%2 != juego[m[1]][-1]%2 ]
            # Si hay más de un movimiento posible, tratar de evitar mover el disco pequeño
            if len(movimientos_validos) > 1:
                tmp = [ m for m in movimientos_validos if juego[m[0]][-1] != numero_discos-1 ]
                if len(tmp) > 0:
                    movimientos_validos = tmp
            # Si hay más de un movimiento posible, mover hacia la pila más alta
            if len(movimientos_validos) > 1:
                mejor_movimiento = movimientos_validos[0]
                cantidad_max = 0
                for m in movimientos_validos:
                    cantidad = len(juego[m[1]])
                    if cantidad > cantidad_max:
                        cantidad_max = cantidad
                        mejor_movimiento = m
                movimientos_validos = [mejor_movimiento]
            # Si hay un sólo movimiento posible, realizarlo
            if len(movimientos_validos) == 1 :
                m = movimientos_validos[0]
                realizar_movimiento(m[0],m[1])
            # Si no, este método no permite resolver el juego
            else:
                pilas.avisar("Error! No se puede resolver... :(")
                print("Movimientos considerados:",movimientos_validos)
                pilas.tareas.eliminar_todas()
                boton_resolver.activar()

    texto1 = pilas.actores.Texto(u"¡JUEGO RESUELTO!",y=1200)
    texto2 = pilas.actores.Texto(u"número de movimientos: ",y=-100)
    texto3 = pilas.actores.Texto(str(movimientos),x=150,y=-100)
    texto4 = pilas.actores.Texto(u"¡Juego perfecto!",y=1200)

    boton_paso_resolver = pilas.interfaz.Boton("Paso")
    boton_paso_resolver.conectar(paso_resolver)
    boton_paso_resolver.x = 60
    boton_paso_resolver.y = -180

    boton_resolver = pilas.interfaz.Boton("Resolver")
    boton_resolver.conectar(click_resolver)
    boton_resolver.x = 150
    boton_resolver.y = -180

    boton_reiniciar = pilas.interfaz.Boton("Reiniciar")
    boton_reiniciar.conectar(reiniciar)
    boton_reiniciar.x = 240
    boton_reiniciar.y = -180


    A=[]
    def accion_boton_0():
        boton_0.desactivar()
        accion(0)
        
    def accion_boton_1():
        boton_1.desactivar()
        accion(1)
        
    def accion_boton_2():
        boton_2.desactivar()
        accion(2)
        
    def accion(poste):
        global A
        A.append(poste)
        if len(A) == 2:
            realizar_movimiento(A[0],A[1])
            A = []
            boton_0.activar()
            boton_1.activar()
            boton_2.activar()

    boton_0 = pilas.interfaz.Boton("A")
    boton_0.x = -200
    boton_0.y = -50
    boton_0.conectar(accion_boton_0)

    boton_1 = pilas.interfaz.Boton("S")
    boton_1.x = 0
    boton_1.y = -50
    boton_1.conectar(accion_boton_1)

    boton_2 = pilas.interfaz.Boton("D")
    boton_2.x = +200
    boton_2.y = -50
    boton_2.conectar(accion_boton_2)

    def control_por_teclado(e):
        if e.codigo == 'a':
            accion_boton_0()
        elif e.codigo == 's':
            accion_boton_1()
        elif e.codigo == 'd':
            accion_boton_2()
        
    pilas.eventos.pulsa_tecla.conectar(control_por_teclado)

    pilas.avisar(str(juego))    

    pilas.ejecutar()

Por si te sirve de algo, te armé este sencillo ejemplo de colision de un actor arrastrable… espero que pueda ayudarte :wink:

# coding: utf-8
import pilasengine

pilas = pilasengine.iniciar()

puntoA = -200
puntoB = 0
puntoC = 200

MiActor = pilas.actores.Mono()
MiActor.x = puntoA
MiActor.y = -100
MiActor.origen = puntoA

MiActor.aprender('arrastrable')

punto_colision = pilas.actores.Aceituna()
punto_colision.y = 100
punto_colision.pos = puntoC

def al_colisionar(mico, aceitu):
		
	def restituir():
		mico.aprender('arrastrable')
		mico.origen = aceitu.pos
		
	mico.eliminar_habilidades()
	mico.y = -100
	mico.x = mico.origen
	mico.x = [mico.origen, aceitu.pos],1
	pilas.tareas.agregar(2, restituir)
	
pilas.colisiones.agregar('mono', 'aceituna', al_colisionar)

pilas.ejecutar()