Alters

A buscar M!nas: diseño de algoritmos

Buenas!

Hoy empezamos con el diseño; esta sección durará dos entradas: una dedicada al diseño de algoritmos, y otra dedicada al diseño de pantallas.

En la entrada anterior explicamos el comportamiento básico de nuestro juego de las minas. En esta profundizaremos en ello hasta conseguir buenos algoritmos en pseudo-código.

El modus operandi que uso para generar algoritmos se basa en funciones de profundidad. Es decir: creo una base muy simple (dos o tres instrucciones), y después para cada instrucción voy ampliando el nivel de profundidad del algoritmo. Con esta técnica se consiguen algoritmos compactos y muy modulares.

Empezaremos con la parte del backend, es decir, dibujar minas, colocarlas...:

[Carga la pantalla]

 muestra(pantalla_inicial)

 si gen = 0 cierra
 sino
   carga()
   dibuja()
  fin

Tenemos el aspecto general de carga terminado. Una anotación: los token del pseudo-código los he puesto en rojo y negrita; las variables en azul y las funciones propias en verde.

Sigamos ahora con el algoritmo de la función carga() (no os preocupéis si veis variables "inconexas"; luego lo juntamos todo).

[Función carga]

carga(){
    min = 10
    fil = 9
    col = 9
    img =Random(min, max)

    si gen = 2 img += max

    imF = Imagen(concat(ruta, string(img), '.jpg'))
    anc = width(imF)
    alt = height(imF)

    si dif = 1
        min = 40
        fil = 16
        col = 16
    sino si dif = 2
        min = 99
        fil = 16
        col = 30
    fin

    anB = anc / col
    alB = alt / fil
    pos = LlenarMinas(fil*col, min)
    res = ComprobarMinas(fil*col, col, pos, min)
    minas = min
    cuadros = (fil * col) - min
}

Revisemos rápidamente como funciona esta función:

Primero asumimos los valores para la dificultad fácil (tres primeras líneas). Acto seguido creamos un aleatorio para la imágen de fondo. Si la opción es 2, le sumamos max (esto es porque hay los mismos fondos de la primera opción como de la segunda; los fondos están numerados. Si es la segunda opción, el número más bajo será max, o lo que es lo mismo, el primer fondo de la opción 2).

Lo siguiente es crear la imágen en imF, y capturar su ancho y largo.

Seguidamente consultamos la dificultad. Si no está en fácil variamos los datos pertinentes.
Finalmente, calculamos el ancho y el largo de los controles (anB y alB), llenamos las minas y las comprobamos.

Las funciones LlenarMinas() y ComprobarMinas() las veremos después de la función dibuja(), la cual veremos a continuación:

[Función dibuja]

dibuja(){
    max = fil * col
    h = alB
    w = anB
    c = col
    f = fil
   con = array(max*2)

    de 0 hasta max-1 usando i
        o = label()
        p = button()

        top(o) = t
        left(o) = l
        width(o) = w
        height(o) = h
        name(o) = concat('L', string(i))
        visible(o) = 1
        backColor(o) = color('transparent')
        font(o) = font('Times new Roman', 10, 'regular', 'pixel')
        top(p) = t
        left(p) = l
        width(p) = w
        height(p) = h
        name(p) = concat('B', string(i))
        visible(p) = 1

        Evento(p.MouseDown)
        Evento(o.MouseEnter)
        Evento(o.MouseLeave)

        con(i) = p
        con(i+max) = o

        si i%c = c-1
            l = 0
            t += h
        sino
            l += w
         fin
    sigue

    add(ventana, con)
    height(ventana) = alt + 57
    width(ventana) = anc + 13
    maxSize(ventana) = size(ventana)
    minSize(ventana) = size(ventana)
    backgroundImage(ventana) = imF
    text(strip1) = string(min)
}

Si en la anterior función predominaban la asignación de variables, en esta predominan las asignaciones a controles. Explico rápidamente el algoritmo:

Se reocgen datos (ya se que parece que sean inútiles las asignaciones del principio).

Después se entra en un bucle, en el que vamos definiendo, en cada vuelta, un button y un label. Les damos altura, anchura, color, posición... y los agregamos al array, de manera que quedan agrupados todos los button con los button y las label con las label.

El siguiente paso es calcular la siguiente posición, y volver a empezar.

Cuando termina el bucle, incluimos todos los controles en la ventana y le fijamos un ancho y un alto. Después hacemos que no se pueda modificar el tamaño, y ponemos el número de minas abajo (en un strip).

El siguiente paso es definir la función LlenarMinas():

[Función LlenarMinas]

LlenarMinas(){
    de 0 hasta min-1 usando i
        pass = 1
        num = Random(0, max+1)

        de 0 hasta length(pos)-1 usando j
            si pos(j) = num pass = 0
        sigue

            si pass pos(i) = num
            sino -= 1
    sigue
}

Este algoritmo es corto y conciso: hace "min-1" iteraciones, en cada una de la cual busca un aleatorio que no esté en "pos". En caso de estar, retrocedemos en una unidad el contador, para volver a la misma posición.

 Para el backend solo falta definir el algoritmo ComprobarMinas(). Pero, además, faltan los eventos que definimos en dibuja(). Éstos entrarán en juego cuando el usuario pulse un button, entre en una label o salga de ella.

Por ahora, vamos a por ComprobarMinas():

[Función ComprobarMinas]

ComprobarMinas(){
    de 0 hasta max-1 usando i
        de 0 hasta min-1 usando j
            si i%col = 0
                si (pos(j) = i - col o pos(j) = i - (col - 1) o
                    pos(j) = i + 1 o pos(j) = i + col o pos(j) = i + (col + 1))
                    num++
                sino
                    num = 10
                    break
                fin
            sino si i%col = col - 1
                si (pos(j) = i - (col + 1) o pos(j) = i - col o
                    pos(j) = i - 1 o pos(j) = i + (col - 1) o pos(j) = i + col)
                    num++
                sino
                    num = 10
                    break
                fin
            sino
                si (pos(j) = i - (col + 1) o pos(j) = i - col o pos(j) = i - (col - 1) o
                    pos(j) = i - 1 o pos(j) = i + 1 o   pos(j) = i + (col - 1) o
                    pos(j) = i + col o pos(j) = i + (col + 1))
                    num++
                sino
                    num = 10
                    break
                fin
            fin
        sigue

        ret(i) = num
        num = 0
    sigue
}

Esta función parece complicada, pero es bastante sencilla: recorremos el array "pos", y por cada posición buscamos dentro de él los lugares colindantes (como si fuera un array de dos dimensiones pero en uno). El complejo de condiciones simplemente sirven para delimitar los "bordes" de la martiz.

Entonces, si encuentra una coincidencia, incrementa "num". En caso contrario deja "num" con un valor de "10" (ya veremos para qué sirve esto).

NOTA: Sé que no es precisamente óptimo, pero esto está hecho de hace un año, y, sinceramente, como funciona no me atrevo a tocarlo...

Finalmente nos faltan los pseudo-códigos de los eventos (tranquilos, ya acabo :-P). Empezamos por los eventos de las label (más cortos):

[Label::mouseEnter]

backColor(s) = color('white')
text(s) = coM(substring(name(s), 1))

Simplemente le ponemos un fondo de color blanco y un texto con las minas que tiene alrededor.

Ahora el otro evento de la Label:

[Label::mouseLeave]

text(s) = ''
backColor(s) = color('transparent')

Aquí volvemos a dejar la label en el mismo estado que cuando la definimos.

Por último, el evento del button:

[Button::mouseDown]

si button(e) = mouseButtons(left)
    cuadros--

    si cuadros = 0
        msg('Has ganado')
        clear(ventana, controls)
    fin

    visible(s) = 0

    si coM(substring(name(s), 1)) = 10
        msg('PUM!')
        muestra(fail)
    fin
sino si button(e) = mousebuttons(right)
    si text(s) = 'X'
        text(s) = ''
        backColor(s) = Color('white')
        minas++
    sino
        text(s) = 'X'
        backColor(s) = Color('red')
        minas--
    fin

    text(strip) = string(minas)
fin

Como escribí en la anterior entrada; comprueba el click hecho (izquierdo o derecho). Si es derecho, permuta entre un texto "X" y un color, indicando que está marcado o no como mina. También modifica el texto del strip (que lleva la cuenta de las minas que nos faltan por marcar).

En cambio, si el click es izquierdo, destapa el cuadro, y busca el valor de la label. OJO he marcado el "10" en negrita para que lo veáis; es el misterioso "10" de CompruebaMinas(). ¿Cómo funciona? Sencillo:

Si os fijáis los buttons y los labels tienen nombres del estilo "B1", "L25". En el evento se extrae el número, y se coteja con el array (por eso es necesariamente unidimensional) y recoge el número del array que CompruebaMinas() introjudo (que es el valor de las minas que tiene alrededor; 10 si es mina). ¡Volia!

Ahora, por fin tenemos todos los algoritmos necesarios. Solo nos falta diseñar las pantallas. Esto nos llevará más bien poco, ya que la mitad del trabajo de diseño está aquí incluido (función dibuja()).

Os dejo, como siempre,
Hasta la próxima!