Definición de Objetos

Datos de "Serpiente"

Para la serpiente necesitamos mantener varios valores importantes, que nos permiten saber en cada instante dónde se encuentra cada una de sus partes, en qué dirección se mueve, si está creciendo, y qué longitud le queda por crecer.

Nuestra serpiente estará "cuantificada", es decir, cada vez que se mueva lo hará en una unidad, y cada vez que crezca también lo hará en una unidad. La serpiente también tendrá siempre un número entero de unidades.

Una unidad, en este juego, es un cuadrado de 16x16 pixels.

Para cada unidad o sección de la serpiente guardaremos un objeto con un conjunto de datos en una estructura de tipo cola.

La cola es la estructura dinámica que mejor se ajusta al movimiento de la serpiente, ya que es una estructura FIFO: el primero en entrar será el primero en salir

  • Cada vez que se mueva añadiremos la siguiente posición de la cabeza de la serpiente en un extremo de la cola, concretamente en el extremo de entrada.
  • Si la serpiente no está creciendo, en cada movimiento eliminaremos una sección del otro extremo de la cola, el extremo de salida.
  • Si la serpiente está creciendo, sencillamente no eliminaremos la última sección, de modo que realmente crecerá en longitud.

Analizando el comportamiento de la serpiente veremos que en cada movimiento no es necesario volver a pintarla toda completa. Supongamos que tenemos la serpiente como en el gráfico siguiente, y el jugador pulsa la tecla "abajo" para dirigirse a la comida.

Movimiento
Movimiento

Sólo necesitamos pintar la cabeza en su nueva posición (1), borrarla de la posición anterior, sustituyéndola por el dibujo de una sección del cuerpo (2) y, si es necesario, borrar la cola (3) y sustituir la que ahora es la nueva sección de cola por el gráfico correspondiente (4).

Esto nos será muy útil, ya que el tiempo necesario para pintar la serpiente en cada movimiento es independiente de su longitud, y por lo tanto predecible, y prácticamente constante.

Para el conjunto de la serpiente completa, guardaremos los siguientes valores

  • "Dirección" actual del movimiento.
  • Dirección "deseada" por el usuario.
  • Unidades que tiene que "por Crecer" todavía.
  • Una "cola" con las secciones de la serpiente.

Funciones de "Serpiente"

Para la clase "Serpiente" no necesitamos definir muchas funciones. La primera que veremos será la de "Avanzar", y tiene varias tareas que realizar

  1. Verificar si el movimiento elegido por el usuario es legal o no. Recordemos que la serpiente no puede invertir su dirección.
  2. Modificar el tipo de sección de la cabeza actual, que pasará de ser una sección de "cabeza" a ser sección de cuerpo, y dependiendo de la dirección actual de movimiento, podrá ser una sección recta o de ángulo.
  3. Calcular la próxima posición de la cabeza. Para ello tomará la posición actual de la cabeza, y dependiendo del la dirección de movimiento, actualizará el valor de la coordenada x o y según corresponda.
  4. Comprobar si ha habido una colisión, ya sea con un muro o con la propia serpiente.
  5. Insertar una nueva sección de cabeza en la cola de secciones.
  6. Comprobar si hemos llegado a una posición con comida. En ese caso tendremos que incrementar el valor que todavía tiene que crecer la serpiente, colocar una nueva comida en el laberinto.
  7. Si la serpiente está creciendo, decrementamos el número de unididades que tiene que crecer todavía e incrementamos el contador. En caso contrario, eliminamos la última sección de la cola de secciones, y actualizamos el valor de la actual última sección como sección de "cola".

Cada actualización, borrado o insercción de sección implica una actualización de dicha sección en pantalla y en el laberinto.

Otra función es la de "Mostrar". Ya hemos explicado que durante el juego sólo necesitaremos actualizar la visualización de dos o tres secciones de la serpiente en cada avance. Sin embargo es necesario incluir una función para mostrar la serpiente completa. Windows requiere que se responda a los mensajes WM_PAINT cuando una ventana es restaurada o cuando pasa a primer plano. En ese caso será necesario actializar el contenido completo de la ventana.

Otra función necesaria es la de "Iniciar", que creará una serpiente nueva cada vez que demos comienzo a una partida.

Además necesitaremos otra función, aunque muy sencilla. Se trata de "Deseada", que nos permitirá asignar valores a la dirección deseada en función de las entradas del teclado.

También añadiremos una última función, ésta privada, que se encargará de calcular el tipo de sección de cola que corresponde después de cada movimiento.

// Clase para serpiente:
class CSerpiente {
  public:
   // Constructor:
   CSerpiente(CGraficos *, CLaberinto *, CComida *, CExtra *, CTanteo *);

   void Iniciar(int);
   void Mostrar(HDC dc=NULL);
   void Deseada(eDireccion dir) {deseada = dir;}
   bool Avanzar();

  private:
   int CalcularCola(int anterior, int ultimo);

   cola<CSeccion> ColaSecciones;
   CDireccion direccion;
   CDireccion deseada;
   int porCrecer;
   CGraficos *Graficos;
   CLaberinto *Laberinto;
   CComida *Comida;
   CExtra *Extra;
   CTanteo *Tanteo;
};

Datos de "Sección de Serpiente"

Crearemos otra clase para trabajar con secciones de la serpiente. Para cada una de ellas guardaremos varios valores

  • "Coordenadas" en el laberinto.
  • "Tipo" de sección de serpiente. Puede tratarse de la cabeza, la cola, una sección recta vertical u horizontal, o una sección de ángulo.

Funciones de "Sección de Serpiente"

Se trata de un objeto auxiliar, por lo tanto las funciones que definiremos serán sencillas: modificar y leer el tipo y las coordenadas. También sobrecargaremos el operador de asignación.

// Clase para secciones:
class CSeccion {
  public:
   // Constructores:
   CSeccion() : posicion(0,0), tipo(0) {}
   CSeccion(const CSeccion &sec) :
      posicion(sec.posicion), tipo(sec.tipo) {}
   CSeccion(const CCoordenada &coor, int t) :
      posicion(coor), tipo(t) {};
   CSeccion(int x, int y, int t) :
      posicion(x,y), tipo(t) {};
   CSeccion(int v) : posicion(0,0), tipo(v) {};

   void ModificarTipo(int t) {tipo=t;}
   const CCoordenada Posicion() {return posicion;}
   const int Tipo() {return tipo;}
   CSeccion &operator=(const CSeccion &sec)
      {posicion = sec.posicion; tipo = sec.tipo; return *this;}

  private:
   CCoordenada posicion;
   int tipo;
};

Datos de "Coordenada"

Evidentemente, contiene los datos de las componentes x e y de la coordenada.

Funciones de "Coordenada"

Además de las de consulta de componentes, añadiremos una función para avanzar usando como parámetro la dirección de avance. También sobrecargaremos los operadores "=" y "==". Además, sobrecargaremos el operador de postincremento para hacer barridos a lo largo del laberinto como si se tratase de una estructura lineal.

// Clase para coordenadas:
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;
};

Datos de "Dirección"

En este caso, sólo el valor de la dirección.

Funciones de "Dirección"

Además de la función de consulta, sobrecargaremos los operadores "!", para calcular la dirección contraria, "=", "==" y el operador de conversión de tipo a entero.

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 &dir) : valor(dir.valor) {}

   // Operadores:
   CDireccion operator!();
   CDireccion &operator=(const CDireccion &dir)
      {valor = dir.valor; return *this;}
   bool operator==(const CDireccion &dir)
      {return dir.valor == valor;}
   operator int() { return (int)valor;}

   eDireccion LeerValor() const {return valor;}

  private:
   eDireccion valor;
};

Datos de "Comida"

Sólo nos interesa guardar la posición en el laberinto.

Funciones de "Comida"

Como sólo crearemos un objeto de este tipo, en lugar de destruirlo y volver a crearlo cada vez que la serpiente se lo coma, usaremos siempre el mismo, y modificaremos la posición cada vez.

Por lo tanto, crearemos una función "Situar", para situar la comida en una posición válida del laberinto.

Además añadiremos una función de "Mostrar" y otra para consultar la posición.

También tendremos una función "Posicion" para obtener la posición actual de la comida.

// Clase para comida:
class CComida {
  public:
   CComida(CGraficos *graf, CLaberinto *lab) :
         Graficos(graf), Laberinto(lab) {}

   void Situar(const CCoordenada &pos) {
      posicion = pos;
      Laberinto->Modificar(posicion, COMIDA);
   }
   CCoordenada Posicion() {return posicion;}
   void Mostrar(HDC dc=NULL) {Graficos->Mostrar(posicion, COMIDA, dc);}

  private:
   CCoordenada posicion;
   CGraficos *Graficos;
   CLaberinto *Laberinto;
};

Datos de "Extra"

Además de la posición en el laberinto, nos interesa saber si está o no activo. También el tipo de extra activo, y el estado si está activo o lo que falta para activarse, si no lo está.

Funciones de "Extra"

Como sólo crearemos un objeto de este tipo, en lugar de destruirlo y volver a crearlo cada vez que la serpiente se lo coma, usaremos siempre el mismo, y modificaremos la posición cada vez.

Por lo tanto, crearemos una función "Situar", para situarlo en una posición válida del laberinto, igual que la comida.

Además añadiremos una función de "Mostrar" y otra para consultar la posición.

También tendremos una función "Posicion" para obtener la posición actual de la comida.

Otras funciones serán: "Siguiente" para actualizar el estado y la imagen en cada iteración del temporizador. "Valor" para obtener el valor del estado actual. "Anular" para desactivar el extra cuando desaparece o es comido. "Activo" para consultar si el extra está o no activo.

// Clase para extras:
class CExtra {
  public:
   CExtra(CGraficos *graf, CLaberinto *lab) :
         Graficos(graf), Laberinto(lab) { Anular();}

   void Situar();
   CCoordenada Posicion() {return posicion;}
   void Mostrar(HDC dc=NULL) {Graficos->MostrarExtra(posicion, tipo, estado, dc);}
   void Siguiente();
   int Valor() { return estado >> 2; }
   void Anular();
   bool Activo() { return activo; }

  private:
   CCoordenada posicion;
   int tipo;   // Diferentes tipos de extras
   int estado; // 0 a 15. 0..3 previos, 4..14 activo 15 "plof"
   int cuenta; // Lo que falta para que aparezca el próximo
   bool activo; // Si está o no activo
   CGraficos *Graficos;
   CLaberinto *Laberinto;
};

Datos de "Laberinto"

En este objeto almacenaremos datos sobre el tablero, concretamente un array con los valores de cada casilla: muros, serpiente, comida o extra, y una variable entera con el número de casillas libres que quedan.

Ese dato se usará para obtener una posición libre del tablero para situar una comida o un extra.

También incluímos un array dinámico para almacenar la forma del laberinto: "laberinto" y otra para saber cuántos datos contiene: "nMuros".

La variable "actual" se usa para almacenar el número del laberinto actualmente cargado.

Funciones de "Laberinto"

Definiremos una función "Colisión" para saber si una coordenada concreta está ocupada o no. Una función "Modificar" para cambiar el contenido de una casilla. Una función "Mostrar", que muestre el laberinto en pantalla. Para eso se usa el array dinámico con la estructura del laberinto.

Además, necesitamos una función "Iniciar" para cargar un laberinto nuevo cuando el usuario lo elija.

También una función "Tablero" para consultar el contenido de una celda del laberinto.

La función "ObtenerLibre" sirve para obtener las coordenadas aleatoriamente de una casilla que no esté ocupada. El algoritmo es el siguiente

  • Obtenermos un valor aleatorio 'v' entre 0 y el número de casillas libres.
  • Hacemos un barrido a lo largo del laberinto ignorando las casillas ocupadas, hasta encontrar la libre que hace el número 'v'.
  • Devolvemos el valor de esa casilla.
// Clase para laberinto:
class CLaberinto {
  public:
   CLaberinto(CGraficos *);

   void Iniciar(int);
   void Mostrar(HDC dc=NULL);
   bool Colision(CCoordenada pos);
   void Modificar(CCoordenada pos, int valor);
   int Tablero(CCoordenada pos) {return tablero[pos.X()][pos.Y()];}
   CCoordenada ObtenerLibre();

  private:
   int tablero[ANCHO][ALTO];
   CCoordenada *laberinto;
   int nMuros;
   int actual;
   CGraficos *Graficos;
   int libres;
};

Datos de "Gráficos"

Para mostrar gráficos necesitaremos algunas variables del sistema, como un manipulador de la ventana del juego y otro de la instancia del programa. El primero se usará para obtener un DC de la ventana y mostrar los gráficos. El segundo para obtener los recursos gráficos del juego, mapas de bits, cadenas, etc.

Otros datos serán los mapas de bits de la serpiente, tablero y comida y de los números de los contadores.

Funciones de "Gráficos"

Necesitamos varias funciones: una para "Mostrar" partes del laberinto, de la serpiente o la comida, otra para visualizar contadores "Texto", una tercera para borrar la pantalla, otra para mostrar el fondo de la pantalla y otra para mostrar el gráfico del extra cuando esté activo, hay que recordar que el extra tiene una vida limitada, para informar al jugador del estado del extra el gráfico se modifica en función del tiempo.

// Clase para gráficos:
class CGraficos {
  public:
   CGraficos(HINSTANCE, HWND);
   ~CGraficos();

   void Mostrar(CCoordenada, int, HDC dc=NULL);
   void Texto(CCoordenada, int, HDC dc=NULL);
   void MostrarExtra(CCoordenada, int tipo, int estado, HDC dc=NULL);
   void Borrar();
   void MostrarFondo(CCoordenada coor, HDC dc);

  private:
   HINSTANCE hInstance;
   HWND hwnd;
   HBITMAP bmp;
   HBITMAP extras;
   HBITMAP marcador;
   HBITMAP num;
};

Datos de "Tanteo"

Pues básicamente los puntos que lleva acumulados el jugador. (Posteriormente añadiremos un contador de objetivo que servirá para aumentar el nivel.

Funciones de "Tanteo"

Una función de "Actualizar", que podrá sumar o restar puntos. Y una función de "Mostrar" igual que el resto de los objetos.

// Clase para tanteo:
class CTanteo {
  public:
   CTanteo(CGraficos *graf) :
      Graficos(graf), puntos(0) {}

   void Iniciar() {puntos=0;}
   void Actualizar(int incremento) { puntos += incremento; Mostrar();}
   void Mostrar(HDC dc=NULL);
   const int Puntos() {return puntos;}

  private:
   int puntos;
   CGraficos *Graficos;
};

Datos de "Juego"

Esta clase almacena los datos "globales" del juego. Incluiremos aquí los siguientes datos

  • pausa, indica si el juego está o no detenido.
  • nLaberintos, indica el número de laberintos disponibles, este dato se lee desde un fichero de texto externo, lo que permitirá añadir más laberintos al juego, sin necesidad de compilarlo de nuevo.
  • hwnd, manipulador de ventana del juego.

Hay varios valores que almacenaremos en el registro para que estén disponibles cada vez que se ejecute el programa, y no se pierdan al cerrarlo.

  • laberinto: último laberinto seleccionado.
  • velocidad: último valor de velocidad seleccionado.
  • puntuacion: array con las puntuaciones máximas obtenidas para cada laberinto.

Funciones de "Juego"

La función "LeerMenu" construirá un menú en función del fichero de texto de configuración, que contiene el número de laberintos disponibles y los nombres de cada uno que se usarán para cada ítem del menú.

La función "LeerRegistro" leerá los valores almacenados en el registro al comienzo de la ejecución del programa. Y la función "GuardarRegistro" volverá a almacenar esos valores en el registro antes de abandonar la ejecución del programa.

"Mostrar" actualizará la ventana completa, llamando a las funciones "Mostrar" de todos los objetos que componen el juego: laberinto, comida, extra y tanteo.

"CambiaLaberinto" responderá a la orden del usuario cuando elija un blaberinto diferente, esto implica actualizar el menú y, por supuesto, la ventana.

"CambiaVelocidad" responderá a la orden del usuario de selección de velocidad.

"ActivarMenus" sirve tanto para desactivar los menús durante la ejecución del juego como para activarlos de nuevo cuando este se detiene.

"Movimiento" se encarga de modificar la posición de la serpiente y del extra.

"Velocidad" obtiene el valor de velocidad actual.

"Empezar" sale del estado de pausa, inicia los objetos, activa el temporizador en función de la velocidad y desactiva los menús.

"MoverSerpiente" actualiza el valor deseado de movimiento de la serpiente.

// Clase para juego:
class CJuego {
  public:
   CJuego(HWND, HINSTANCE);
   ~CJuego();
   //  Funciones auxiliares:
   void LeerMenu(HWND);
   void LeerRegistro();
   void GuardarRegistro();
   void Mostrar(HDC);
   void CambiaLaberinto(int);
   void CambiaVelocidad(int);
   void ActivarMenus(bool);
   void Movimiento();
   const int Velocidad() {return velocidad;}
   void Empezar();
   const bool Parado() {return pausa;}
   void MoverSerpiente(eDireccion);

  private:
   // Variables y objetos globales:
   CSerpiente *Serpiente;
   CGraficos *Graficos;
   CLaberinto *Laberinto;
   CTanteo *Tanteo;
   CComida *Comida;
   CExtra *Extra;

   bool pausa;
   int nLaberintos;
   HWND hwnd;

   // Datos que se almacenan en el registro:
   int laberinto;
   int velocidad;
   int *puntuacion;
};