9 - Ejemplo práctico: motor de "Elige tu propia aventura"

Los juegos de texto basados en decisiones se modelan naturalmente como árboles generales: algunas escenas ofrecen dos caminos, otras tres o más. Implementaremos un motor para la historia de un caminante donde cada elección abre una nueva rama.

9.1 Concepto del motor

  • La raíz representa el inicio del relato.
  • Cada nodo contiene el texto de la escena y una lista de opciones.
  • Cada opción enlaza a otro nodo, y ciertas escenas permiten elegir entre tres acciones distintas.

Con la representación LCRS evitamos arreglos de tamaño fijo y mantenemos el motor ligero.

9.2 Modelo de datos

Separar las opciones en una lista enlazada nos permite manejar un número variable de decisiones.

typedef struct Nodo Nodo;

typedef struct Opcion {
  char texto[64];
  Nodo *destino;
  struct Opcion *siguiente;
} Opcion;

struct Nodo {
  char descripcion[256];
  Opcion *opciones;
};

La función agregarOpcion agrega nuevas opciones al final de la lista, sin límite de cantidad.

Nodo *crearNodo(const char *descripcion) {
  Nodo *n = malloc(sizeof(Nodo));
  if (!n) return NULL;
  strncpy(n->descripcion, descripcion, sizeof(n->descripcion) - 1);
  n->descripcion[sizeof(n->descripcion) - 1] = '\0';
  n->opciones = NULL;
  return n;
}

void agregarOpcion(Nodo *nodo, const char *texto, Nodo *destino) {
  if (!nodo || !destino) return;
  Opcion *nueva = malloc(sizeof(Opcion));
  if (!nueva) return;
  strncpy(nueva->texto, texto, sizeof(nueva->texto) - 1);
  nueva->texto[sizeof(nueva->texto) - 1] = '\0';
  nueva->destino = destino;
  nueva->siguiente = NULL;
  if (!nodo->opciones) {
    nodo->opciones = nueva;
    return;
  }
  Opcion *actual = nodo->opciones;
  while (actual->siguiente) actual = actual->siguiente;
  actual->siguiente = nueva;
}

9.3 Carga de la historia del caminante

La historia incluye escenas con dos y tres opciones para comprobar el comportamiento del motor.

Nodo *crearHistoria(void) {
  Nodo *inicio = crearNodo("Al amanecer despiertas junto al camino principal. "
                           "El bosque murmura y una columna de humo se alza a lo lejos.");
  Nodo *rio = crearNodo("Sigues el rumor del rio y encuentras un puente de madera.");
  Nodo *colina = crearNodo("Subes la colina y descubres una fogata reciente.");
  Nodo *bosque = crearNodo("Te internas en el bosque y tres sendas se cruzan en un claro.");
  Nodo *aldea = crearNodo("Llegas a una aldea silenciosa; las puertas estan abiertas.");
  Nodo *cueva = crearNodo("Encuentras una cueva iluminada por cristales azules.");
  Nodo *laguna = crearNodo("Ante ti hay una laguna cristalina con un muelle oculto.");
  Nodo *torre = crearNodo("La torre de vigilancia cruje mientras el viento la azota.");
  Nodo *campamento = crearNodo("Un campamento vacio guarda restos de provisiones.");
  Nodo *mercado = crearNodo("Sigues a un zorro y descubres un mercado ambulante.");

  Nodo *finalRio = crearNodo("El pescador comparte comida y canciones. Descansas feliz.");
  Nodo *finalColina = crearNodo("Avivas la fogata y pasas la noche bajo las estrellas. Fin.");
  Nodo *finalAldea = crearNodo("Ayudas a reparar las defensas de la aldea. Te conviertes en su guardian.");
  Nodo *finalDesierto = crearNodo("Marchas al desierto y nuevas aventuras te esperan lejos.");
  Nodo *finalBosque = crearNodo("El arbol hueco canta historias y te duermes en el claro.");
  Nodo *finalLaguna = crearNodo("Nadas hasta el muelle y hallas un cuaderno olvidado. Fin.");
  Nodo *finalAgua = crearNodo("Guardas agua brillante y sigues tu viaje reconfortado.");
  Nodo *finalMercado = crearNodo("Degustas especias extrañas y compartes risas con mercaderes.");
  Nodo *finalBrujula = crearNodo("Obtienes una brujula que apunta a nuevas rutas legendarias.");
  Nodo *finalCampamento = crearNodo("Descansas en una hamaca improvisada hasta el atardecer.");
  Nodo *finalProvisiones = crearNodo("Empacas provisiones frescas y retomas la marcha con energía.");
  Nodo *finalCueva = crearNodo("Conversas con una criatura subterranea y recibes consejos.");
  Nodo *finalCristal = crearNodo("Los cristales te guian a un corredor iluminado que te devuelve al camino.");
  Nodo *finalTorre = crearNodo("Enciendes la alarma y adviertes a viajeros lejanos. Eres un heroe.");
  Nodo *finalHorizonte = crearNodo("Desde la torre observas el amanecer y trazas nuevos planes.");

  agregarOpcion(inicio, "Tomar el sendero del rio", rio);
  agregarOpcion(inicio, "Subir la colina humeante", colina);
  agregarOpcion(inicio, "Explorar el bosque espeso", bosque);

  agregarOpcion(rio, "Cruzar el puente hacia la aldea", aldea);
  agregarOpcion(rio, "Descender al cauce y explorar cuevas", cueva);
  agregarOpcion(rio, "Descansar a la orilla para pescar", finalRio);

  agregarOpcion(colina, "Avivar la fogata y esperar", finalColina);
  agregarOpcion(colina, "Subir a la torre de vigilancia", torre);
  agregarOpcion(colina, "Bajar por un sendero hacia el valle", campamento);

  agregarOpcion(bosque, "Seguir las luciernagas hasta una laguna", laguna);
  agregarOpcion(bosque, "Escuchar al arbol hueco", finalBosque);
  agregarOpcion(bosque, "Perseguir al zorro hasta un mercado", mercado);

  agregarOpcion(aldea, "Ofrecer ayuda a los habitantes", finalAldea);
  agregarOpcion(aldea, "Seguir camino hacia el desierto", finalDesierto);

  agregarOpcion(cueva, "Hablar con el eco subterraneo", finalCueva);
  agregarOpcion(cueva, "Seguir un tunel iluminado", finalCristal);

  agregarOpcion(laguna, "Nadar hasta el muelle oculto", finalLaguna);
  agregarOpcion(laguna, "Recolectar agua brillante y partir", finalAgua);

  agregarOpcion(torre, "Encender la alarma de emergencia", finalTorre);
  agregarOpcion(torre, "Observar el horizonte en silencio", finalHorizonte);

  agregarOpcion(campamento, "Descansar entre las tiendas", finalCampamento);
  agregarOpcion(campamento, "Tomar provisiones y volver al camino", finalProvisiones);

  agregarOpcion(mercado, "Comer en un puesto de especias", finalMercado);
  agregarOpcion(mercado, "Comprar una brujula misteriosa", finalBrujula);

  return inicio;
}

9.4 Motor del juego

El bucle muestra la escena actual, numera cualquier cantidad de opciones y avanza según la elección del usuario. Si un nodo no tiene opciones, la historia finaliza.

void jugar(Nodo *inicio) {
  if (!inicio) return;
  Nodo *actual = inicio;
  char buffer[16];

  while (actual) {
    puts("\n---------------------------");
    puts(actual->descripcion);
    if (!actual->opciones) {
      puts("\nFin de la historia. Gracias por jugar.");
      break;
    }

    int indice = 1;
    for (Opcion *op = actual->opciones; op; op = op->siguiente, ++indice) {
      printf("%d) %s\n", indice, op->texto);
    }
    printf("Selecciona una opcion: ");
    if (!fgets(buffer, sizeof(buffer), stdin)) break;
    long eleccion = strtol(buffer, NULL, 10);

    Opcion *opcionElegida = actual->opciones;
    for (int i = 1; opcionElegida && i < eleccion; ++i) {
      opcionElegida = opcionElegida->siguiente;
    }
    if (!opcionElegida) {
      puts("Opcion invalida, intenta nuevamente.");
      continue;
    }
    actual = opcionElegida->destino;
  }
}

9.5 Reporte completo del árbol

Antes de liberar memoria conviene imprimir el mapa completo para depurar la historia y comprobar que las ramas tienen la descripción y cantidad de opciones correctas. El siguiente helper recorre el árbol y dibuja la indentación.

void mostrarMapa(Nodo *nodo, int nivel) {
  if (!nodo) return;
  for (int i = 0; i < nivel; ++i) printf("  ");
  printf("* %s\n", nodo->descripcion);

  int indice = 1;
  for (Opcion *op = nodo->opciones; op; op = op->siguiente, ++indice) {
    for (int i = 0; i < nivel + 1; ++i) printf("  ");
    printf("(%d) %s\n", indice, op->texto);
    mostrarMapa(op->destino, nivel + 2);
  }
}

9.6 Recomendaciones

  • Probar escenas con tres opciones y validar que el motor numera correctamente.
  • Agregar un historial para deshacer decisiones o recomenzar desde la escena actual.
  • Cargar la historia desde JSON o YAML para permitir que los guionistas editen sin recompilar.

9.7 Código completo para CLion

Programa autocontenido: crea la aventura del caminante, ejecuta el motor y libera memoria.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct Nodo Nodo;

typedef struct Opcion {
  char texto[64];
  Nodo *destino;
  struct Opcion *siguiente;
} Opcion;

struct Nodo {
  char descripcion[256];
  Opcion *opciones;
};

Nodo *crearNodo(const char *descripcion) {
  Nodo *n = malloc(sizeof(Nodo));
  if (!n) return NULL;
  strncpy(n->descripcion, descripcion, sizeof(n->descripcion) - 1);
  n->descripcion[sizeof(n->descripcion) - 1] = '\0';
  n->opciones = NULL;
  return n;
}

void agregarOpcion(Nodo *nodo, const char *texto, Nodo *destino) {
  if (!nodo || !destino) return;
  Opcion *nueva = malloc(sizeof(Opcion));
  if (!nueva) return;
  strncpy(nueva->texto, texto, sizeof(nueva->texto) - 1);
  nueva->texto[sizeof(nueva->texto) - 1] = '\0';
  nueva->destino = destino;
  nueva->siguiente = NULL;
  if (!nodo->opciones) {
    nodo->opciones = nueva;
    return;
  }
  Opcion *actual = nodo->opciones;
  while (actual->siguiente) actual = actual->siguiente;
  actual->siguiente = nueva;
}

Nodo *crearHistoria(void) {
  Nodo *inicio = crearNodo("Al amanecer despiertas junto al camino principal. "
                           "El bosque murmura y una columna de humo se alza a lo lejos.");
  Nodo *rio = crearNodo("Sigues el rumor del rio y encuentras un puente de madera.");
  Nodo *colina = crearNodo("Subes la colina y descubres una fogata reciente.");
  Nodo *bosque = crearNodo("Te internas en el bosque y tres sendas se cruzan en un claro.");
  Nodo *aldea = crearNodo("Llegas a una aldea silenciosa; las puertas estan abiertas.");
  Nodo *cueva = crearNodo("Encuentras una cueva iluminada por cristales azules.");
  Nodo *laguna = crearNodo("Ante ti hay una laguna cristalina con un muelle oculto.");
  Nodo *torre = crearNodo("La torre de vigilancia cruje mientras el viento la azota.");
  Nodo *campamento = crearNodo("Un campamento vacio guarda restos de provisiones.");
  Nodo *mercado = crearNodo("Sigues a un zorro y descubres un mercado ambulante.");

  Nodo *finalRio = crearNodo("El pescador comparte comida y canciones. Descansas feliz.");
  Nodo *finalColina = crearNodo("Avivas la fogata y pasas la noche bajo las estrellas. Fin.");
  Nodo *finalAldea = crearNodo("Ayudas a reparar las defensas de la aldea. Te conviertes en su guardian.");
  Nodo *finalDesierto = crearNodo("Marchas al desierto y nuevas aventuras te esperan lejos.");
  Nodo *finalBosque = crearNodo("El arbol hueco canta historias y te duermes en el claro.");
  Nodo *finalLaguna = crearNodo("Nadas hasta el muelle y hallas un cuaderno olvidado. Fin.");
  Nodo *finalAgua = crearNodo("Guardas agua brillante y sigues tu viaje reconfortado.");
  Nodo *finalMercado = crearNodo("Degustas especias extrañas y compartes risas con mercaderes.");
  Nodo *finalBrujula = crearNodo("Obtienes una brujula que apunta a nuevas rutas legendarias.");
  Nodo *finalCampamento = crearNodo("Descansas en una hamaca improvisada hasta el atardecer.");
  Nodo *finalProvisiones = crearNodo("Empacas provisiones frescas y retomas la marcha con energía.");
  Nodo *finalCueva = crearNodo("Conversas con una criatura subterranea y recibes consejos.");
  Nodo *finalCristal = crearNodo("Los cristales te guian a un corredor iluminado que te devuelve al camino.");
  Nodo *finalTorre = crearNodo("Enciendes la alarma y adviertes a viajeros lejanos. Eres un heroe.");
  Nodo *finalHorizonte = crearNodo("Desde la torre observas el amanecer y trazas nuevos planes.");

  agregarOpcion(inicio, "Tomar el sendero del rio", rio);
  agregarOpcion(inicio, "Subir la colina humeante", colina);
  agregarOpcion(inicio, "Explorar el bosque espeso", bosque);

  agregarOpcion(rio, "Cruzar el puente hacia la aldea", aldea);
  agregarOpcion(rio, "Descender al cauce y explorar cuevas", cueva);
  agregarOpcion(rio, "Descansar a la orilla para pescar", finalRio);

  agregarOpcion(colina, "Avivar la fogata y esperar", finalColina);
  agregarOpcion(colina, "Subir a la torre de vigilancia", torre);
  agregarOpcion(colina, "Bajar por un sendero hacia el valle", campamento);

  agregarOpcion(bosque, "Seguir las luciernagas hasta una laguna", laguna);
  agregarOpcion(bosque, "Escuchar al arbol hueco", finalBosque);
  agregarOpcion(bosque, "Perseguir al zorro hasta un mercado", mercado);

  agregarOpcion(aldea, "Ofrecer ayuda a los habitantes", finalAldea);
  agregarOpcion(aldea, "Seguir camino hacia el desierto", finalDesierto);

  agregarOpcion(cueva, "Hablar con el eco subterraneo", finalCueva);
  agregarOpcion(cueva, "Seguir un tunel iluminado", finalCristal);

  agregarOpcion(laguna, "Nadar hasta el muelle oculto", finalLaguna);
  agregarOpcion(laguna, "Recolectar agua brillante y partir", finalAgua);

  agregarOpcion(torre, "Encender la alarma de emergencia", finalTorre);
  agregarOpcion(torre, "Observar el horizonte en silencio", finalHorizonte);

  agregarOpcion(campamento, "Descansar entre las tiendas", finalCampamento);
  agregarOpcion(campamento, "Tomar provisiones y volver al camino", finalProvisiones);

  agregarOpcion(mercado, "Comer en un puesto de especias", finalMercado);
  agregarOpcion(mercado, "Comprar una brujula misteriosa", finalBrujula);

  return inicio;
}

void jugar(Nodo *inicio) {
  if (!inicio) return;
  Nodo *actual = inicio;
  char buffer[16];

  while (actual) {
    puts("\n---------------------------");
    puts(actual->descripcion);
    if (!actual->opciones) {
      puts("\nFin de la historia. Gracias por jugar.");
      break;
    }

    int indice = 1;
    for (Opcion *op = actual->opciones; op; op = op->siguiente, ++indice) {
      printf("%d) %s\n", indice, op->texto);
    }
    printf("Selecciona una opcion: ");
    if (!fgets(buffer, sizeof(buffer), stdin)) break;
    long eleccion = strtol(buffer, NULL, 10);

    Opcion *opcionElegida = actual->opciones;
    for (int i = 1; opcionElegida && i < eleccion; ++i) {
      opcionElegida = opcionElegida->siguiente;
    }
    if (!opcionElegida) {
      puts("Opcion invalida, intenta nuevamente.");
      continue;
    }
    actual = opcionElegida->destino;
  }
}

void mostrarMapa(Nodo *nodo, int nivel) {
  if (!nodo) return;
  for (int i = 0; i < nivel; ++i) printf("  ");
  printf("* %s\n", nodo->descripcion);

  int indice = 1;
  for (Opcion *op = nodo->opciones; op; op = op->siguiente, ++indice) {
    for (int i = 0; i < nivel + 1; ++i) printf("  ");
    printf("(%d) %s\n", indice, op->texto);
    mostrarMapa(op->destino, nivel + 2);
  }
}

void liberarOpciones(Opcion *opcion) {
  while (opcion) {
    Opcion *sig = opcion->siguiente;
    free(opcion);
    opcion = sig;
  }
}

void liberarHistoria(Nodo *nodo) {
  if (!nodo) return;
  for (Opcion *op = nodo->opciones; op; op = op->siguiente) {
    liberarHistoria(op->destino);
  }
  liberarOpciones(nodo->opciones);
  free(nodo);
}

int main(void) {
  Nodo *aventura = crearHistoria();
  jugar(aventura);
  puts("\n=== Mapa completo de escenas ===");
  mostrarMapa(aventura, 0);
  liberarHistoria(aventura);
  return 0;
}
Diagrama del motor de aventura