Animal, vegetal, mineral
Un juego de preguntas y respuestas
Ya tenemos un programa que usa este mecanismo en el apartado de fuentes, pero ahora le vamos a dar más opciones, y aprovechando lo que hemos aprendido sobre bases de datos, más capacidad para almacenar información.
La idea es crear una estructura en árbol y almacenarla en una base de datos. Cada nodo del árbol puede contener una pregunta o una respuesta. Todas las preguntas se almacenan en nodos rama, y todas las respuestas en nodos hoja.
Podemos crear un árbol para cada categoría, uno para animales, otro para vegetales y otro para minerales, y almacenarlos en distintas tablas de la misma base de datos, o usar una única tabla, pero añadir un campo para definir el tema.
Un posible uso de un programa de este tipo puede ser identificar entidades que el usuario no sabe qué son, pero que puede describir con detalle.
Por ejemplo, supongamos que hemos entrenado a nuestro programa para que identifique únicamente razas de gato. El usuario puede tener un gato, del que desconoce su raza, pero guiado por las preguntas del programa puede llegar a identificarla. En este caso, el tipo de preguntas puede referirse a colores de pelaje, o de ojos, forma de las orejas o de la cola, etc.
Podemos diseñar nuestro programa para crear una base de datos diferente para cada materia en la que queramos que sea un experto: animales, plantas, coches, juegos...
Otra posible optimización consiste en reestructurar el árbol para intentar equilibrar sus ramas, pero esto es difícil de hacer y puede requerir que se modifiquen algunas preguntas. Lo mejor, probablemente, será entregar el programa con una base de datos convenientemente diseñada, que ya contenga información bien organizada, en lugar de dejar que el usuario la cree desde cero.
Patrón Modelo, Vista, Controlador
Probablemente dedicaremos más contenidos a este patrón, pero en este artículo vamos a utilizarlo a modo de ejemplo.
El patrón modelo-vista-controlador, o MVC consiste en separar el código en tres partes diferentes, cada una con sus tareas claramente diferenciadas.
- Modelo: se encarga del trabajo relacionado con los datos. Generalmente, todo el acceso a las bases de datos, aunque el concepto de base de datos puede ser más flexible.
- Vista: se encarga de la interfaz con el usuario. Puede comunicarse con el modelo para mostrar datos al usuario.
- Controlador: contiene la lógica, es decir, el código del programa. Se comunica con el modelo para recuperar, almacenar o modificar los datos, y con la vista para mostrar información o requerirla del usuario.
Aprovechando el polimorfismo de C++ este patrón nos da mucha flexibilidad.
Esta forma de trabajar nos permite, por ejemplo, que el mismo controlador pueda trabajar tanto con una base de datos MySQL o SQLite, como con archivos de texto o arrays, sin modificar su código, ya que accede a los datos del modelo mediante una clase abstracta.
Lo mismo se aplica a las vistas. Sin modificar el controlador, se puede compilar el programa cambiando la vista, y crear una aplicación para consola o GUI, usando diferentes APIs, modificando sólo la clase de la vista.
Por supuesto, esto nos complica un poco el diseño, ya que todo es más abstracto, pero facilita mucho la portabilidad, y nos permite crear la misma aplicación para entornos diferentes, cambiando sólo la vista o el modelo, según el caso.
Para ello partiremos de dos clases abstractas, una para el modelo y otra para la vista. Crearemos clases derivadas para cada fuente de datos y para cada tipo de interfaz que queramos usar.
En el desarrollo de aplicaciones y páginas web es habitual hablar de capas, como "front end" y "back end". Aplicado al patrón MVC, el "front end" correspondería a la vista y el "back end" al controlador y al modelo. El "front end" se encarga del interfaz de usuario y el "back end" de la lógica de negocio, es decir, los cálculos y el acceso y manipulación de los datos. Cuando se trabaja en equipo es habitual que diferentes programadores se encarguen de cada capa, por lo tanto es importante definir bien qué datos debe representar cada vista y qué datos debe recuperar del usuario. El controlador se encargará de entregar los datos a la vista o decirle cómo obtenerlos del modelo, tanto como de validar y almacenar los datos entregados por las vistas.
Para diseñar las vistas necesitaremos saber qué información se debe mostrar al usuario y que datos tiene que suministrar el usuario al programa.
Por ejemplo, mostrar y procesar menús, mostrar texto, diálogos para capturar respuestas si/no, para capturar cadenas, o números, para elegir una opción entre varias, etc.
Debe disponer de los métodos necesarios para leer, modificar o borrar datos, también crear estructuras (en el caso de bases de datos, crear tablas), etc.
Controlador
El controlador es en esencia el programa, en este caso contiene el código del juego. Cuando lo escribamos deberemos tener en cuenta que cualquier acción que requiera interactuar con el usuario debe hacerse a través de la vista, y cualquier acción relacionada con los datos, se hará a través del modelo.
Por supuesto, el código de cada parte probablemente se escriba de forma simultánea, es decir, cada vez que el controlador se encuentre con una tarea que deba realizar apoyado en la vista o el modelo, deberemos usar, modificar o añadir métodos a esas partes, al menos de las clases abstractas.
A la hora de depurar el programa, también necesitaremos una vista y un modelo que no sean abstractos, por lo que también será necesarios crear esas clases derivadas.
Para el controlador de este programa usaremos una clase cJuego.
Como hemos extraído todo lo relacionado con la visualización y el almacenamiento de datos, nuestra clase cJuego se simplifica mucho:
class cJuego {
public:
cJuego(cDatos&, cInterfaz&);
void Run();
void Juego();
void NuevoTema();
void ElegirTema();
void EstablecerTema(long int id);
std::string TemaActual() { return tema.Nombre(); }
private:
cTema tema;
cDatos& datos;
cInterfaz& interfaz;
};
El constructor necesita dos parámetros: un objeto correspondiente al modelo, y otro para la vista. Usaremos referencias a esos objetos que se almacenarán en la propia clase, de modo que podamos usar polimorfismo y se invoquen los métodos de las clases derivadas del modelo y vista.
El método "Run" ejecutará el bucle principal del juego, que mostrará un menú y ejecutará las opciones del usuario.
El menú permitirá seleccionar el tema del juego, jugar, añadir un nuevo tema o salir. Por ello crearemos tres métodos, uno para cada opción.
Para una versión de consola, la función main será muy simple:
int main() {
cInterfazConsola interfaz;
cDatosSQLite datos("animales.db");
cJuego juego(datos, interfaz);
juego.Run();
return 0;
}
Básicamente crea los tres módulos y llama al método "Run".
De este modo nos podemos centrar sólo en la lógica del juego.
El constructor
El constructor almacenará las referencias al modelo y la vista. Además seleccionará un tema por defecto:
cJuego::cJuego(cDatos& d, cInterfaz& i) : datos(d), interfaz(i) {
datos.TemaPorDefecto(tema);
}
tema es un dato miembro privado de la clase que corresponde al tema actualmente seleccionado.
El constructor del modelo se encargará de crear un tema la primera vez que se ejecute el juego, ya que el programa no funcionará si no existe por lo menos un tema con un elemento. Empezaremos con el tema "animales" y añadiremos un único animal: el gato.
El tema por defecto se corresponderá con el primer tema que se encuentre en la base de datos.
El método Run
El método Run mostrará y procesará el menú principal:
void cJuego::Run() {
bool salir = false;
do{
switch(interfaz.Menu(menuPrincipal)) {
case '0':
salir = true;
break;
case '1': // Elegir tema
ElegirTema();
break;
case '2': // Jugar
if(tema.IdTema() != -1) Juego();
break;
case '3': // Nuevo tema
NuevoTema();
break;
}
} while(!salir);
}
Es posible que algunos métodos no sean necesarios en todas las implementaciones de vistas y modelos. Por ejemplo, el método Run no será necesario en una implementación que use GUI, ya que los menús se manejan de otro modo en esos entornos. Esto no es importante, sencillamente nuesto programa contendrá código que nunca será ejecutado.
Mostrar el menú y recuperar la opción seleccionada por el usuario es una tarea de la vista, que en nuestro caso se trata de la clase "interfaz",
El método ElegirTema
Este método se encarga de mostrar una lista con los temas disponibles para que el usuario elija uno. De nuevo, es probable que ciertas implementaciones no usen este método.
void cJuego::ElegirTema() {
int id;
char letra='a';
cTemas menuTema;
std::vector<cTema> temas;
menuTema.AsignaTitulo("ELEGIR TEMA");
if(datos.ConsultaTemas(temas)) {
for(size_t i=0; i<temas.size(); i++)
menuTema.InsertaOpcion(temas[i].IdTema(), letra++, temas[i].Nombre());
id = interfaz.ElegirTema(menuTema);
EstablecerTema(id);
}
}
El patrón MVC permite que la vista obtenga datos del modelo, pero esto no es obligatorio. En nuestro caso el controlador obtiene los datos y se los pasa a la vista.
ConsultaTemas recupera un vector con los temas disponibles. Con ellos crea una especie de menú, en este caso del tipo de selección de una opción entre varias.
El método EstablecerTema
Se encarga de leer el registro del tema que corresponde a un identificador determinado.
void cJuego::EstablecerTema(long int id) {
datos.LeerTema(id, tema);
}
El método NuevoTema
El juego permitirá crear temas nuevos. Para ello necesita algunos datos que le permitan crear la tablas y registros necesarios. Esto es, un registro de tema, que requiere un nombre, una palabra para referirse a una respuesta en singular y otra para referirse a respuestas en plural. Por ejemplo, para el tema de animales, el nombre será "Animales", la palabra para referirse a una respuesta será "animal", y para referirse a varias "animales". Estas palabras se usan para crear los literales de preguntas y textos correctamente.
Además, cuando se crea un tema es necesario crear la tabla que contendrá las preguntas y respuestas, y esa tabla debe contener al menos una respuesta.
void cJuego::NuevoTema() {
cTema tema;
cNuevoTema dialogo(tema);
std::string ejemplo;
dialogo.SetTitulo("NUEVO TEMA");
dialogo.SetEtqNombre("Nombre de tema (por ejemplo vegetales)");
dialogo.SetEtqSingular("Singular de tema para textos (una palabra en singular, por ejemplo planta)");
dialogo.SetEtqPlural("Plural de tema para textos (una palabra en plural, por ejemplo plantas)");
while(!interfaz.DialogoNuevoTema(dialogo, tema));
datos.InsertarTema(tema);
datos.CrearTablaNodo(tema);
interfaz.DialogoLeeCadena("Necesito un/a " + tema.Singular() + " de ejemplo como primer elemento", ejemplo);
datos.InsertarNodoHoja(tema, ejemplo);
}
El método Juego
El método principal es el que contiene el bucle del juego. El programa irá haciendo preguntas para desplazarse por el árbol hasta llegar a un nodo hoja. Si ese nodo contiene la respuesta correcta habrá adivinado en qué estaba pensando el jugador. En caso contrario añadirá una nueva respuesta a la base de datos. Para ello necesitará la respuesta correcta y una pregunta que le permita distinguir entre la respuesta propuesta y la correcta a la que se pueda responder sí o no.
void cJuego::Juego() {
long int anterior, actual;
cNodo nodo;
bool resp, respanterior=0;
std::string nuevoNodo;
std::string nuevaPregunta;
std::string texto;
do {
texto = "Conozco ";
texto += std::to_string(datos.CuentaNodos(tema));
texto += " ";
texto += tema.Plural() + ".\n";
texto += "Piensa en un " + tema.Singular() + " y te hare algunas preguntas para intentar adivinar cual es...\n¿Estás preparado?";
while(!interfaz.DialogoSiNo(texto));
anterior = -1;
// Si la tabla sólo contiene un registro se trata de una respuesta
// Si tiene más de uno, el nodo raíz, correspondiente a la primera pregunta, será el tercer registro.
if(datos.CuentaNodos(tema) == 1) actual=1; else actual=3;
do {
datos.LeerNodo(tema, actual, nodo);
if(nodo.Pregunta()) { // Es una pregunta
texto = "¿" + nodo.Texto() + "?";
} else {
texto = "¿Se trata de un " + nodo.Texto() + "?";
}
resp = interfaz.DialogoSiNo(texto);
if(nodo.Pregunta()) { // Es una pregunta
anterior = actual;
respanterior = resp;
if(resp) {
actual = nodo.Si();
} else {
actual = nodo.No();
}
}
} while(nodo.Pregunta());
if(resp) { // Si la ultima respuesta fue Si, el programa ha adivinado
interfaz.Texto("Lo he adivinado!!!");
} else { // En caso contrario, es que no lo conoce
texto = "No conozco este " + tema.Singular() + ", pero si me respondes a unas preguntas lo incluire en mi lista.";
interfaz.Texto(texto);
do { // Este bucle asegura que no se añadiran nodos con errores
texto = "De que " + tema.Singular() + " se trata?";
interfaz.DialogoLeeCadena(texto, nuevoNodo); // Lee el nombre del nuevo animal
// while(Nuevo[strlen(Nuevo) - 1] < ' ') Nuevo[strlen(Nuevo) - 1] = 0; /* Elimina retorno de linea */
texto = "Dame una pregunta que sirva para distinguir un/a " + nodo.Texto() + " de un/a " + nuevoNodo + " a la que se pueda contestar si o no:";
interfaz.DialogoLeeCadena(texto, nuevaPregunta);// Lee la pregunta para distinguir nuevo animal del ultimo que se pregunto
texto = "Que respuesta se ha de dar a esta pregunta: " + nuevaPregunta + " Para obtener: " + nuevoNodo + " como respuesta: Si o No:";
resp = interfaz.DialogoSiNo(texto); // Lee la respuesta adecuada a la pregunta leida
// Muestra al jugador los datos leidos para que verifique si son correctos
texto = "Veamos si lo he entendido bien:\nA la pregunta:\n" + nuevaPregunta + "\n";
if(resp) {
texto += "Si se responde SI se trata de un " + nuevoNodo + " y si se responde NO de un " + nodo.Texto();
} else {
texto += "Si se responde SI se trata de un " + nodo.Texto() + " y si se responde NO de un " + nuevoNodo;
}
texto+= "\nEs correcto?";
} while(!interfaz.DialogoSiNo(texto)); // Repite el proceso hasta que el jugador de el visto bueno
datos.InsertarNuevo(tema, resp, respanterior, actual, anterior, nuevoNodo, nuevaPregunta);
}
} while(interfaz.DialogoSiNo("¿Continuar jugando?"));
}
Vista
Para crear la vista partiremos de una clase virtual pura:
class cInterfaz {
public:
virtual int Menu()=0; /*< Muestra un menú y captura la respuesta */
virtual int ElegirTema(const cTemas& temas) =0; /*< Muestra la lista de temas y captura la respuesta */
virtual void Texto(const std::string&) const =0; /*< Muestra una cadena de texto */
virtual bool Respuesta() =0; /*< Lee una respuesta si/no */
virtual bool DialogoSiNo(const std::string&) =0; /*< Muestra un diálogo con la cadena indicada, las opciones de respuesta son si/no */
virtual void DialogoLeeCadena(const std::string&, std::string& respuesta) =0; /*< Muestra un diálogo para capturar una cadena */
virtual bool DialogoNuevoTema(cNuevoTema& dialogo, cTema& tema)=0; /*< Muestra un diálogo para capturar un nuevo tema */
protected:
cMenu menuPrincipal;
};
Para cada uno de los entornos para el que queramos crear el juego tendremos que crear una clase derivada de cInterfaz y definir todos sus métodos.
Las vistas también requieren algunas clases adicionales para mostrar texto, menús o diálogos. Estas clases no son virtuales.
Por ejemplo, la clase cMenu se usa para definir el contenido de un menú, y usa una clase auxiliar cOpciónMenu para definir cada una de sus opciones.
La clase cTemas funciona parecido a cMenu, pero para un conjunto de opciones de la que se debe seleccionar una. Usa la clase cOpcionTema para manejar cada opción.
La clase cNuevoTema se usa para manejar las etiquetas del diálogo que se usará para añadir un tema nuevo.
Modelo
class cDatos
{
public:
virtual long int CuentaTemas() const =0; /*< Cuenta el número de temas en la base de datos */
virtual void CrearTablaTemas() =0; /*< Crea las tablas de temas y animales */
virtual void InsertarTema(cTema& tema) =0; /*< Inserta un tema en la tabla de temas, crea la tabla correspondiente */
virtual void TemaPorDefecto(cTema& tema) =0; /*< Devuelve el valor del tema por defecto */
virtual long int CuentaNodos(const cTema& tema)=0; /*< Cuenta el número nodos hoja (respuestas) correspondientes a un tema */
virtual void CrearTablaNodo(const cTema& tema)=0; /*< Crea la tabla correspondiente a un tema */
virtual long int InsertarNodoHoja(const cTema& tema, const std::string& texto) =0; /*< Inserta un nodo hoja (una respuesta) devuelve su ID*/
virtual long int InsertarNodoPregunta(const cTema& tema, const std::string& pregunta, long int si, long int no) =0; /*< Inserta un nodo rama (una pregunta) devuelve su ID */
virtual bool ActualizaSi(const cTema& tema, long int anterior, long int idpregunta)=0; /*< Actualiza la referencia al nodo SI del nodo pregunta */
virtual bool ActualizaNo(const cTema& tema, long int anterior, long int idpregunta)=0; /*< Actualiza la referencia al nodo NO del nodo pregunta */
virtual void LeerTema(long int id, cTema& tema) =0; /*< Lee el tema con id */
virtual void LeerNodo(const cTema& tema, long int id, cNodo& nodo) =0; /*< Lee el nodo con id */
virtual bool ConsultaTemas(std::vector<cTema>& temas)=0; /*< Obtiene una lista de temas */
virtual void InsertarNuevo(cTema& tema, bool resp, bool respant, long int actual, long int anterior, const std::string& nuevoNodo, const std::string& nuevaPregunta)=0;
/*< Inserta un nuevo nodo */
virtual std::string Error()=0;
};
Ejemplos
Se incluyen dos versiones del juego, una para consola y otra usando wxWidgets para GUI. Ambas usan una base de datos SQLite para almacenar los datos.
Para ello se crea una clase derivada de cDatos, cDatosSQLite. Esta clase no está completa. El soporte para detectar errores no está terminado, pero por lo demás es completamente funcional.
Para la versión de consola se crea una clase derivada de cInterfaz, cInterfazConsola, y para la versión GUI la clase cInterfazwx.
Por supuesto, en la versión para wxWidgets se han añadido otras clases para los cuadros de diálogo de leer cadenas y añadir temas.
| Nombre | Fichero | Fecha | Tamaño | Contador | Descarga |
|---|---|---|---|---|---|
| Ejemplo para consola | animalconsola.zip | 2026-01-02 | 10120 bytes | 9 |
| Nombre | Fichero | Fecha | Tamaño | Contador | Descarga |
|---|---|---|---|---|---|
| Ejemplo para wxWidgets | animalwxwidgets.zip | 2026-01-02 | 15230 bytes | 10 |
Nota: En ambos casos hay que incluir sqlite3 a las opciones de librerías. En el caso de la versión para wxWidgets será necesario incluir también las opciones necesarias para crear una aplicación wxWidgets.