Programación

Lo primero que haremos será crear un proyecto Windows App usando Dev-C++, para crear una aplicación Windows GUI, con el nombre "Serpiente". Para crear este programa he usado la versión 4.9.6.0 de Dev-C++, que aún no es definitiva, es posible que existan algunos problemas si se intenta compilar con otras versiones, sobre todo si son más antiguas. Cuando exista una versión 5 definitiva se revisará este artículo.

Automáticamente se creará un fichero "main.cpp", pero no lo necesitamos, ya que escribiremos el programa completamente. De modo que lo eliminaremos.

Implementaremos cada clase en un fichero separado, de modo que añadiremos los siguientes módulos al proyecto:

  • Princip.cpp: programa principal, que contendrá el manejo de la ventana, el bucle de mensajes, etc.
  • Coordena.cpp: implementación de la clase CCoordenada.
  • Direccio.cpp: implementación de la clase CDireccion.
  • Extra.cpp: implementación de la clase CExtra.
  • Graficos.cpp: implementación de la clase CGraficos.
  • Juego.cpp: implementación de la clase CJuego.
  • Laberint.cpp: implementación de la clase CLaberinto.
  • Serpient.cpp: implementación de la clase CSerpiente.
  • Tanteo.cpp: implementación de la clase CTanteo.

También añadiremos un fichero de recursos, editando las opciones de proyecto y añadiendo el fichero "graficos.rc" a la sección de ficheros de recursos.

Por otro lado, crearemos también un fichero de cabecera para cada clase, que contenga la declaración de ésta y las de los tipos necesarios.

Ya hemos visto las formas de la clases en explicaciones anteriores, pero los ficheros de cabecera necesitan algunas cosas más.

Veremos ahora cada fichero que compone el programa completo.

Fichero de cabecera de identificadores

Usaremos un fichero de cabecera para definir los valores de las constantes e identificadores que usaremos en el programa.

Fichero ids.h:

// constantes de direcciones:

#define ARRIBA      1
#define DERECHA     2
#define ABAJO       4
#define IZQUIERDA   8

// Elementos de laberinto:
#define LIMPIA      0
#define COMIDA      15
#define MURO        16

// Mapas de bits:
#define MARCADOR    40
#define GRAFICOS    41
#define NUMERO      42     
#define EXTRAS      43

#define ANCHO       32
#define ALTO        22
#define CRECER      3

#define CM_SALIR    50
#define CM_SOBRE    51

Clase CDireccion

Fichero de cabecera "direccio.h":

// Serpiente con Clase
// (C) 2002 Con Clase http://www.conclase.net
// Salvador Pozo Coronado

// Declaración de clase "CDireccion":

#include <windows.h>

#ifndef CL_DIRECCION
#define CL_DIRECCION

enum eDireccion {arriba=1, derecha=2, abajo=4, izquierda=8};

// Clase para direcciones:
class CDireccion {
  public: 
   // constructores:
   CDireccion(eDireccion dir) : valor(dir) {}
   CDireccion(const CDireccion &amp;dir) : valor(dir.valor) {}
   
   // Operadores:
   CDireccion operator!();
   CDireccion &operator=(const CDireccion &dir) 
      {valor = dir.valor; return *this;}
   bool operator==(const CDireccion &amp;dir)
      {return dir.valor == valor;}
   operator int() { return (int)valor;}
   
   eDireccion LeerValor() const {return valor;}
   
  private:
   eDireccion valor;
};
#endif

No hay mucho que comentar, el operador ! se define en el fichero "direccio.cpp":

// Serpiente con Clase
// (C) 2002 Con Clase http://www.conclase.net
// Salvador Pozo Coronado

// Implementación de la clase para CDireccion:

#include "direccio.h"

// Clase para direcciones:
CDireccion CDireccion::operator!()
{
   eDireccion valor2;
   
   if(valor < abajo) valor2 = (eDireccion)(valor << 2); 
   else valor2 = (eDireccion)(valor >> 2); 
   
   return valor2;
}

Ya explicamos esto en la teoría, sobrecargamos el operador ! para obtener la dirección contraria. Esto se hace rotando dos bits a la izquierda si el valor de dirección es menor que "abajo", o dos a la derecha si el valor de dirección es mayor o igual que "abajo".

Clase CCoordenada

Fichero de cabecera "coordena.h":

// Serpiente con Clase
// (C) 2002 Con Clase http://www.conclase.net
// Salvador Pozo Coronado

// Declaración de clase "CCoordenada":

#include <windows.h>
#include "ids.h"
#include "direccio.h"

#ifndef CL_COORDENADA
#define CL_COORDENADA

class CCoordenada {
  public:
   CCoordenada(): x(0), y(0) {}
   CCoordenada(int xa, int ya): x(xa), y(ya) {}
   CCoordenada(const CCoordenada &coor) : x(coor.x), y(coor.y) {}

   void Avanzar(eDireccion dir);
   CCoordenada &operator=(const CCoordenada &coor) 
      {x = coor.x; y = coor.y; return *this;}
   bool operator==(const CCoordenada &coor) 
      {return (x == coor.x && y == coor.y);}
   CCoordenada &operator++(int) {
      x++; 
      if(x >= ANCHO) {x = 0; y++;}
      return *this;
   }
   int X() {return x;}
   int Y() {return y;}

  private:
   int x, y;
};
#endif

El código no está muy comentado, de hecho se podría decir que no está comentado en absoluto, pero es porque no creo que sea necesario.

Se hace referencia al fichero de cabecera "direccio.h" para incuir la declaración de la clase "CDireccion", ya que se usa como parámetro un objeto de este tipo en la función "Avanzar".

La mayor parte de las funciones y operadores son tan sencillos que se han definido dentro de la propia clase. La única excepción es la función "Avanzar", que se ha definido en el fichero "coordena.cpp".

/ Serpiente con Clase
// (C) 2002 Con Clase http://www.conclase.net
// Salvador Pozo Coronado

// Implementación de la clase para Coordenadas:

#include "coordena.h"
#include "direccio.h"

void CCoordenada::Avanzar(eDireccion dir)
{
   switch(dir) {
      case arriba: y--; break;
      case derecha: x++; break;
      case abajo: y++; break;
      case izquierda: x--;
   }
   // Bordes continuos:
   if(y < 0) y = ALTO-1;
   if(y >= ALTO) y = 0;
   if(x < 0) x = ANCHO-1;
   if(x >= ANCHO) x = 0;
}

El efecto de bordes continuos consiste en que la parte derecha e izquierda se comunican y lo mismo pasa con la parte superior e inferior. Esto se consigue haciendo que si se sobrepasa el borde, se pasa a la misma coordenada del borde opuesto.

Plantilla para cola

Esta plantilla está basada en la que se describe en el curso de estructuras dinámicas de datos. Se han hecho algunas modificaciones para adaptarla al programa, pero no demasiadas.

Fichero de cabecera "cola_pla.h":

// Plantilla para colas Con Clase
// (C) 2002 Con Clase http://www.conclase.net
// Salvador Pozo Coronado

template<class TIPO> class cola;

template<class TIPO>
class nodo {
   public:
    nodo(TIPO v, nodo<TIPO> *sig = NULL)
    {
       valor = v;
       siguiente = sig;
    }

   private:
    TIPO valor;
    nodo<TIPO> *siguiente;

   friend class cola<TIPO>;
};

template<class TIPO>
class cola {
   public:
    cola() : primero(NULL), ultimo(NULL), nelem(0) {}
    ~cola() {Vaciar();}

    void Anadir(TIPO v);
    TIPO Leer();
    void Vaciar();
    bool Consultar(TIPO &amp;v);
    bool Siguiente(TIPO &amp;v);
    TIPO &ObtenerUltimo();
    TIPO &ObtenerPrimero();
    
    int nelem;
   private:
    nodo<TIPO>* primero;
    nodo<TIPO>* ultimo;
    nodo<TIPO>* actual;
};

template<class TIPO>
void cola<TIPO>::Vaciar()
{
   while(primero) Leer();
}

template<class TIPO>
void cola<TIPO>::Anadir(TIPO v)
{
   nodo<TIPO> *nuevo;

   /* Crear un nodo nuevo */
   /* Este será el último nodo, no debe tener siguiente */
   nuevo = new nodo<TIPO>(v);
   /* Si la cola no estaba vacía, añadimos el nuevo a continuación de ultimo */
   if(ultimo) ultimo->siguiente = nuevo;
   /* Ahora, el último elemento de la cola es el nuevo nodo */
   ultimo = nuevo;
   /* Si primero es NULL, la cola estaba vacía, ahora primero apuntará también al nuevo nodo */
   if(!primero) primero = nuevo;
   nelem++;
}

template<class TIPO>
TIPO cola<TIPO>::Leer()
{
   nodo<TIPO> *Nodo; /* variable auxiliar para manipular nodo */
   TIPO v;      /* variable auxiliar para retorno */

   /* Nodo apunta al primer elemento de la pila */
   Nodo = primero;
   if(!Nodo) return 0; /* Si no hay nodos en la pila retornamos 0 */
   /* Asignamos a primero la dirección del segundo nodo */
   primero = Nodo->siguiente;
   /* Guardamos el valor de retorno */
   v = Nodo->valor;
   /* Borrar el nodo */
   delete Nodo;
   /* Si la cola quedó vacía, ultimo debe ser NULL también*/
   if(!primero) ultimo = NULL;
   nelem--;
   return v;
}

template<class TIPO>
bool cola<TIPO>::Consultar(TIPO &v)
{
   actual = primero;
   if(!actual) return false;
   v = actual->valor;
   return true;
}

template<class TIPO>
bool cola<TIPO>::Siguiente(TIPO &v)
{
   actual = actual->siguiente;
   if(!actual) return false;
   v = actual->valor;
   return true;
}

template<class TIPO>
TIPO &cola<TIPO>::ObtenerUltimo()
{
   return ultimo->valor;
}

template<class TIPO>
TIPO &cola<TIPO>::ObtenerPrimero()
{
   return primero->valor;
}

Las funciones "Consultar" y "Siguiente" se usan para leer secuencialmente toda la cola, sin tener que usar los métodos normales para las colas: "Leer" y "Anadir". Esto nos hará falta cuando tengamos que pintar toda la serpiente, ya sea al principio del juego o después de restaurar la pantalla.

Las funciones "ObtenerPrimero" y "ObtenerUltimo" nos devuelven una referencia al primer y último elemento de la cola, respectivamente. Esta referencia nos permitirá modificar los valores de esos elementos sin extraerlos de la cola y volver a añadirlos. Esto es necesario cuando la serpiente se mueve. Por una parte hay que cambiar el tipo de sección de cabeza y por otra la de cola.