Dificultades

A la hora de implementar el juego e interactuar con el usuario tenemos varias opciones.

La forma más simple de capturar las jugadas es que el usuario señale el naipe que quiere mover con el ratón, marcarla de alguna manera, ya sea con video inverso o mediante un marco, esperar a que el usuario marque la columna de destino, y realizar el movimiento.

Pero el resultado será mucho más atractivo si el usuario puede pinchar, arrastrar y depositar el naipe en la columna deseada. Lamentablemente, este mecanismo es algo más complicado de implementar. Además, es mucho más difícil mantener separado la implementación del juego de su representación gráfica, que es algo que queremos hacer mientras sea posible.

Para ello añadimos un nuevo dato miembro a la clase "tapete", al que llamaremos "mano", de tipo "Naipe", y que nos permitirá almacenar el valor del naipe que estamos arrastrando, es decir, el que tenemos en la mano. Además, necesitamos otro dato para recordar de que pila hemos cogido el naipe, de modo que podamos devolverlo a ella en el caso de que nos arrepintamos y soltemos la carta, o para almacenar el movimiento, ya que necesitamos guardar la pila de origen.

El mecanismo del algoritmo del juego refleja cómo jugamos con una baraja real. Cuando queremos cambiar un naipe de pila, primero lo cogemos con la mano, y después lo depositamos en la pila de destino. En el juego haremos lo mismo: primero un "pop" de la pila de origen a la "mano", recordando de qué pila lo hemos cogido, después un "push" de la mano a la pila de destino, siempre que se trate de una jugada legal, o un "push" a la pila de origen, si se interrumpe la operación de arrastre, o la jugada indicada no es legal.

Mover naipes: gráficos

Desde el punto de vista gráfico, para arrastrar cartas sobre el tapete deberemos tener en cuenta algunos detalles interesantes:

  • Lo primero que hacemos es quitar de la pila la carta que vamos a mover, (pop). Eso se traduce en que deberemos borrarla físicamente de la pantalla, dibujando lo que hubiera debajo.
  • Windows nos obliga a realizar algunas tareas adicionales, como capturar el ratón y en este caso puede ser interesante cambiar la apariencia del cursor, para indicar que estamos arrastrando un naipe.
  • Cuardar el mapa de bits de lo que vaya a quedar oculto al pintar la carta en la nueva posición. Esto será necesario para restituir el estado del tapete cada vez que el naipe cambie de posición.
  • Ahora entramos en un bucle, que se repetirá hasta que soltemos el naipe que tenemos en la mano:
    • Borrar la carta de la posición anterior.
    • Guardar el mapa de bits de la posición actual.
    • Pintar la carta en la nuevas coordenadas.
  • Una vez que hemos soltado el naipe, deberemos borrarlo de su última posición, colocarlo en la pila de destino (push), y deshacerse de las variables temporales usadas (mapas de bits, etc).
  • Finalmente, liberamos el ratón y volvemos a cambiar el cursor por la flecha.
Arrastre
Arrastre

Hay un detalle importante a tener en cuenta al arrastrar el naipe. Cuando el jugador pincha con el ratón sobre el naipe, en general, no lo hará en la esquina superior izquierda, sino en un punto cualquiera, desplazado x pixeles a la derecha e y hacia abajo.

Cuando empiece la operación de arrastre borraremos el área ocupada por el naipe, y pintaremos la imagen que hubiera debajo. Después deberemos capturar el mapa de bits de la zona que ocupará el naipe en la siguiente posición.

Pero, las coordenadas de que disponemos son las de ratón, y las coordenadas que necesitamos para capturar y dibujar mapas de bits son las de la esquina superior izquierda del naipe. Por lo tanto deberemos calcular esas coordenadas a partir de las del ratón y de las coordenadas de la esquina superior izquierda del naipe en su posición de origen. Estos valores son, en realidad, el desplazamiento entre unas coordenadas y otras:

desp.x = raton.x - zona.x
desp.y = raton.y - zona.y

Donde zona son las coordenadas de la esquina superior izquierda del naipe.

Cada vez que tengamos que mostrar o capturar un mapa de bits usaremos las coordenadas del ratón y el desplazamiento según esta fórmula:

x = raton.x - desp.x
y = raton.y - desp.y

Por supuesto, todas estas tareas se dividirán entre las clases Tapete, Gráficos. El bucle de mensajes se encargará de algunas tareas extras, como procesar los mensajes del ratón.

Mover naipes: localizar naipes

Cuando movamos naipes de una pila a otra usaremos la función PtInRegion para saber en que región está contenida la posición actual del ratón.

En ese momento cambiaremos el cursor del ratón usando la función SetCursor. A partir de ese momento dibujaremos el naipe usando las coordenadsa del ratón. También capturamos el ratón para nuestra ventana, usando la función SetCapture.

Cuando soltemos el naipe, para localizar la pila de destino no nos vale la función PtInRegion ya que el naipe no ocupa un único punto, sino un rectángulo. El naipe, una vez soltado, puede estar sobre una pila o sobre dos. En este último caso, decidiremos cual es la de destino verificando en qué región está la parte central del naipe.

Para ello usaremos la función RectInRegion con un rectángulo. Las coordenadas y dimensiones del rectángulo las calcularemos teniendo en cuenta que las pilas están separadas horizontalmente 10 pixels, de modo que nuestro rectángulo tendrá 11, en el centro del naipe. De este modo nos aseguramos de que al menos un pixel de ese rectángulo está sólo en una de las pilas.

Verificar el final del juego

Veamos ahora cómo nos las arreglamos para saber si hemos terminado el juego.

Hay que distinguir entre cuando el juego está resuelto, y cuando no puede ser resuelto.

La primera prueba es sencilla, basta con comprobar si el último naipe de cada pila de salida es un rey. O más sencillo todavía, que las pilas correspondientes al Mazo, Montón y Trabajo están vacías, o lo que es lo mismo, que las cuatro pilas de salida tienen diez naipes cada una.

La segunda es más complicada. Primero, el mazo debe estar vacío. Mientras queden cartas por sacar del mazo, aún podemos pensar que es posible resolver el juego.

Luego existen varias posibilidades, aunque en este juego no probaremos todas ellas. Nos conformaremos con verificar que no hay ningún movimiento posible, entre las pilas de Montón y Trabajo entre ellas o a las de Salida.

Todas estas pruebas las haremos después de cada movimiento, incluido el movimiento del Mazo al montón.

Otras opciones del juego

Los diálogos de ayuda y sobre son sencillos, basta con mostrarlos y usar el botón OK para cerrarlos.

Elegir el dorso de los naipes

Diálogo de opciones
Diálogo de opciones

Como opción del juego permitiremos el que jugador pueda elegir el gráfico que se usará para el dorso de los naipes entre los disponibles en "cards.dll".

Para ello usaremos un cuadro de diálogo con una barra de desplazamiento mediante la cual podremos seleccionar cada uno de los mapas de bits.

El procedimiento de diálogo necesitará acceder al objeto de la clase "Graficos" ya que necesita obtener algunos datos, como los mapas de bits de los dorsos que deberemos obtener de "cards.dll", el valor del identificador de dorso actual, la máscara, las dimensiones de los mapas de bits, etc. Pasaremos un puntero a la estructura mediante el parámetro lParam de la función DialogBoxParam.

Si queremos que el valor de esta opción se mantenga en diferentes ejecuciones del programa deberemos guardarla en algún sitio. En este caso optaremos por crear una variable en el registro que leeremos cada vez que se inicie la aplicación y que guardaremos cuando el usuario la modifique.

Para ello crearemos dos funciones globales:

void LeerRegistro(Graficos *graficos);
void GuardarRegistro(Graficos *graficos);

Responder al doble clic

Aún podemos añadir algunas mejoras al juego, por ejemplo, nos gustaría poder colocar un naipe en la pila de salida adecuada haciendo doble clic sobre él.

Lo primero que se necesita para hacer que una aplicación responda al doble clic es que que la clase de la ventana tenga el estilo CS_DBLCLKS. Lo segundo, que procese los mensajes WM_LBUTTONDBLCLK.

Al procesar este mensaje verificaremos si las coordenadas del ratón corresponden a un naipe sobre la pila del "Montón" o sobre una de las pilas de "Trabajo". Si es así, verificaremos si la carta de la cima de esa pila se puede colocar en alguna de las pilas de "Salida". En ese caso, llevaremos a cabo el movimiento.