Creando una Aventura Conversacional con Z88DK (III)

Terminamos en esta entrega con la explicación de las herramientas de texto de la librería z88dk aplicadas a un juego en concreto, nuestra aventura conversacional del pirata Guybrush Threpwood. Desde luego, con todo lo explicado no se pretende que el resultado final sea una aventura conversacional completa, eso conllevaría un trabajo adicional por parte del lector de la serie, sino que se sienten las bases para aprender a manejar estas herramientas de texto mediante un ejemplo práctico.

A partir de la siguiente entrega dejaremos atrás la parte de la librería referente al texto y pasaremos a los gráficos.

Lo mismo que se ha venido utilizando hasta el momento: el archivo aventura.c con el código fuente de nuestro juego, el archivo datos.h con las estructuras de datos que vamos a utilizar y el archivo Makefile, en el caso de que lo estemos utilizando.

Una parte muy importante de las aventuras suelen ser los PSI o Personajes Pseudo Inteligentes (tal como son llamados en el PAWS o en las aventuras de AD, por ejemplo). Básicamente, un PSI se limitará a deambular por alguna parte del mapeado de la aventura (aunque también podría estar fijo en una localización) y a responder a ciertas acciones que hagamos, e incluso podría hablar con nosotros.

Lo que pretendemos en esta entrega es la creación de un pirata que se mueva por la taberna (y sólo por la taberna, no saldrá fuera de ella ni irá al callejón), con el que podremos hablar, darle la jarra para que se emborrache, e incluso pelear con la espada.

La única información que tenemos que almacenar para que nuestro pirata cumpla con nuestros objetivos es la habitación en la que se encuentra en ese momento y si ha bebido de la jarra (y, por lo tanto, está borracho). Esto lo haremos mediante el uso de dos variables declaradas al inicio del método main (las marcadas como “nuevo”, como en anteriores ocasiones, nos indican cambios respecto al código de anteriores entregas):

	void main(void)
	{
        int habitacion = 0;
	    int final = 0;
		char comando[250];
		int i;
		int hayObjetos;
		char palabra[50];
		int pesoTransportado;
		int localizacionPirata = 5;      // nuevo
		short int borrachoPirata = 0;    // nuevo

La variable localizacionPirata nos va a indicar en qué habitación se encuentra el pirata en cada momento. Hemos hecho que su localización inicial sea la que se encuentra en la posición 5 del array de habitaciones (el reservado de la taberna). Este valor podrá cambiar aleatoriamente a lo largo de la aventura. Tras cada comando del jugador, se determinará al azar si el pirata cambia o no de habitación y hacia cuál. La variable borrachoPirata indica si el pirata está borracho porque le hemos dado la jarra. Solo le asignaremos el valor 0 si no lo está o el valor 1 si lo está; es por ello que empleamos un tipo short int, pues no vamos a necesitar reservar más memoria.

Lo siguiente que vamos a escribir es la parte del código que determina aleatoriamente si el pirata se va a mover o no y hacia donde. Todo esto lo introducimos dentro de una función de tal forma que luego podremos ponerlo en la parte que queramos. La función, que debe ser definida antes de main, tendría la siguiente forma:

int movimientoPirata(int localizacionPirata, THabitacion habitaciones[], int habitacion)
{
  int probabilidadMovimiento;
  int localizacionAnterior = localizacionPirata;
 
  probabilidadMovimiento = rand()/(RAND_MAX/4);
 
  if (habitaciones[localizacionPirata].direcciones[probabilidadMovimiento] > 2)
  {
    localizacionPirata =
      habitaciones[localizacionPirata].direcciones[probabilidadMovimiento]-1;
    if (localizacionAnterior != localizacionPirata)
    if (localizacionPirata == habitacion)
      printf("El pirata entra en la habitacion.\n\n");
    else if (localizacionAnterior == habitacion)
    {
      printf("El pirata sale de la habitacion hacia el ");
      if (probabilidadMovimiento == 0) printf("norte.");
      else if (probabilidadMovimiento == 1) printf("este.");
      else if (probabilidadMovimiento == 2) printf("sur.");
      else printf("oeste");
      printf("\n\n");
    }
  }
 
  return localizacionPirata;
}

En un momento explicaremos qué hace todo este código, pero antes, un apunte. Ya que vamos a utilizar la función rand(), hemos de tener en cuenta que esta función se encuentra definida en el interior del archivo de cabecera stdlib.h, que no hemos incluido hasta el momento. Por ello, deberemos añadir la siguiente línea al inicio del archivo, junto al resto de las cláusulas #include:

#include <stdlib.h>

Una vez aclarado esto, continuamos. La función recibe como parámetro la localización actual del pirata, el array de habitaciones, y la habitación actual en la que se encuentra el jugador. Recordemos que tanto el primer como el tercer parámetro indican posición dentro del array de habitaciones. La función devolverá un entero que será la nueva localización del pirata (tanto si se ha movido como si no).

Lo primero que se hace en la función es declarar dos variables, probabilidadMovimiento, que almacenará en qué dirección intenta moverse el pirata, y localizacionAnterior, que contiene la localizacion del pirata tal como se encuenta al iniciar la función, para guardar el valor anterior en el caso de que éste cambie.

La siguiente línea es la que determina al azar en qué dirección intentará moverse el pirata. Hemos de tener en cuenta que hemos decidido que el pirata sólo se podría mover por el interior de la taberna y, por supuesto, sólo podrá moverse en direcciones correctas, pero esto lo veremos más adelante. Para obtener números aleatorios, se hace uso de la función rand(). Esta función se llama sin parámetros y devuelve un entero aleatorio entre 0 y RAND_MAX, que tiene un valor de 32767 (según está definido en stdlib.h). Sin embargo, a nosotros nos interesa obtener un valor entre 0 y 3 (los posibles índices del array de direcciones dentro de la estructura THabitacion). Es por ello que recurrimos al truco que se muestra en el código.

Si en Linux ejecutamos man rand, veremos que allí se nos explica una técnica para poder calcular números aleatorios en un rango determinado (de 1 a 100, por ejemplo). Sin embargo, no podemos hacer uso de esta forma con z88dk, pues no se admiten los números de coma flotante. Tampoco podríamos multiplicar rand() por cuatro y después dividir, porque podríamos desbordar la capacidad del entero; es por ello que calculamos el entero entre 0 y 3 de la forma tan rebuscada que se muestra en el código. Si quisieramos que fuera de 1 a 4 sería igual, pero sumando 1 al total. Deberemos recordar esto siempre que queramos calcular un número aleatorio en un rango determinado (por ejemplo, una tirada de dado).

En la siguiente línea, correspondiente al if, es donde controlamos que el pirata se mueva si y solo si el momvimiento es correcto.

  if (habitaciones[localizacionPirata].direcciones[probabilidadMovimiento] > 2)

Dentro de la habitación correspondiente en el array de habitaciones cuyo índice se corresponde con la localización del pirata, comprobamos el valor de habitación a la que se movería el pirata según la dirección calculada al azar y almacenada en probabilidadMovimiento. Sólo realizaremos las siguientes acciones (que se explicarán a continuación) si este valor es mayor de 2. ¿Por qué esto es así? Porque si valiera 0, sería un movimento incorrecto (no hay habitación en esa dirección), y los valores 1 y 2 se corresponden con las habitaciones del juego situadas fuera de la taberna, por donde habíamos acordado anteriormente que el pirata no se podría mover.

Si esta condición se cumple, se ejecutará el siguiente trozo de código:

  localizacionPirata =
    habitaciones[localizacionPirata].direcciones[probabilidadMovimiento]-1;
  if (localizacionAnterior != localizacionPirata)
    if (localizacionPirata == habitacion)
      printf("El pirata entra en la habitacion.\n\n");
    else if (localizacionAnterior == habitacion)
    {
      printf("El pirata sale de la habitacion hacia el ");
      if (probabilidadMovimiento == 0) printf("norte.");
      else if (probabilidadMovimiento == 1) printf("este.");
      else if (probabilidadMovimiento == 2) printf("sur.");
      else printf("oeste");
      printf("\n\n");
    }

Primero hacemos que la nueva localización del pirata sea a la que se movería según la dirección calculada al azar (restamos uno, porque el array direcciones almacena identificadores de habitación, no índices dentro del array de habitaciones). Lo siguiente es informar al jugador de que el pirata ha entrado o ha salido de la habitación en la que actualmente se encuentra. Como el pirata se ha movido, si la habitación actual del pirata es la misma que la del jugador, eso quiere decir que el pirata ha entrado en la misma habitación en la que estaba el jugador, por lo que mostramos el mensaje adecuado. Por otra parte, como el pirata se ha movido, si la localización anterior del pirata era igual a la habitación en la que se encuentra el jugador, eso quiere decir que el pirata ha salido de esa habitación, por lo que mostramos un mensaje indicando hacia donde ha salido, según el valor aleatorio de probabilidadMovimiento.

Lo último que hace la función es devolver el valor de localizacionPirata, tanto si ha cambiado como si no.

Si ahora probáramos nuestra aventura, todavía no pasaría nada, porque no hemos incluido esta función en ningún lugar del código de main. Parece claro que si queremos determinar si el pirata se mueve después de cada movimiento del jugador, deberíamos llamar a esta función dentro del intérprete de órdenes, pero… ¿dónde?. Un buen lugar sería justo al comienzo de dicho intérprete de órdenes:

escribirDescripcion(habitaciones,habitacion,objetos);
while (final == 0)
{
 
  // nuevo (Principio)
  localizacionPirata = movimientoPirata(localizacionPirata,habitaciones,habitacion);  
  // nuevo (Fin)
 
  printf("Que hago ahora? - ");
  gets comando;

Evidentemente, el valor devuelto por movimientoPirata deberá ser almacenado en la variable localizacionPirata para que los cambios tengan lugar. Si ahora jugamos nuestra aventura podremos ver como a veces se nos informará de las entradas o salidas del pirata, pero no sabremos nunca a ciencia cierta donde se encuentra… falta un detalle… que se nos indique que el pirata se encuentra en la misma habitación que nosotros en el caso de que esto sea así, aunque no se mueva.

Por lo tanto, al final de la función escribirDescripción, que mostraba por pantalla la descripción de la habitación en la que nos encontrábamos, debemos añadir la línea marcadas:

if (hayObjetos == 0) printf(" nada");
printf("\n\n");
 
// Nuevo (Principio)
if (habitacion == localizacionPirata) printf("El pirata esta aqui.\n\n");
// Nuevo (Fin)

Simplemente comprobamos si la localización actual del pirata se corresponde con la habitación en la que se encuentra actualmente el jugador, y en caso de que sea así, se muestra un mensaje por pantalla. Introducir este cambio implica pasar un nuevo parámetro (int localizacionPirata) a la función escribirDescripción:

void escribirDescripcion(THabitacion habitaciones, int habitacion, 
  TObjeto objetos, int localizacionPirata)

Y por supuesto, añadir este nuevo parámetro a todas las llamadas a la función que se hagan dentro de main(). Si jugamos ya tendremos un pirata autista moviéndose por nuestro mundo, incapaz de comunicarse con nosotros y que no reacciona ante ninguna de nuestras acciones. Las siguientes dos secciones tratarán de cambiar esto.

El pirata es muy juguetón y no para de moverse...

Dar la capacidad al jugador de hablar con los PSI es algo complicado, pues sería necesario implementar un analizador sintáctico que permitiera al juego interpretar las palabras del jugador. Sería como un intérprete de comandos dentro del intérprete de comandos, pero algo más complicado. Esto, desde luego, queda fuera del objetivo de este artículo. Sin embargo, puede ser interesante indicar el comienzo del camino a seguir para que sea el propio lector el que pueda añadir esta funcionalidad de forma elaborada a su aventura, y a la vez hacer un par de anotaciones sobre características de las cadenas en z88dk.

Vamos a hacer que el jugador le pueda decir cosas al pirata, pero que éste sólo entienda la palabra “ayuda”. Si el jugador dice “ayuda” al pirata, éste le dirá qué es lo que tiene que hacer para terminar el juego (vencerle con la espada). Si el jugador dice cualquier otra cosa, el pirata no le entenderá. Como en la mayoría de las aventuras, el jugador deberá teclear algo similar a lo siguiente para poder decirle algo a un PSI:

	decir "frase"

Como sólo hay un PSI en nuestra aventura vamos a obviar el 'decir a', de forma que todo será más sencillo. De todas formas, si nos fijamos, lo que dice el jugador se encuentra entre comillas. Cuando tengamos que analizar el comando introducido por el jugador en el intérprete de comandos, tendremos que hacer uso de cadenas que almacenan comillas en su interior… ¿cómo podemos hacer esto sin que nos dé un error?. Si nosotros tecleamos en un programa printf(““hola””); esperando que la salida sea “hola”, nos llevaremos un chasco porque no funcionará. Deberemos hacer uso de las secuencias de escape.

En realidad, en entregas anteriores ya hemos hecho uso de algunas secuencias de escape (por ejemplo, \n ó \0). Una secuencia de escape es un símbolo de barra invertida \ seguido de un carácter. Su misión es introducir un carácter especial dentro de una cadena. Por ejemplo, la secuencia de escape \n introduce un salto de línea en la cadena, la secuencia de escape \a (en C estándar) hace sonar un pitido, etc. Si queremos introducir unas comillas dentro de una cadena para que formen parte de esa cadena (y no se interpreten como delimitador de la misma), deberemos hacer uso de la secuencia de escape \“. De esta forma, si quisiéramos escribir “hola” por pantalla, la instrucción que debemos incluir en nuestro programa podría ser printf(“\”hola\”“). El primer y último carácter de comillas son los delimitadores de la cadena, mientras que los símbolos de comillas precedidos por \ se escribirán por pantalla.

Dicho esto ya podemos introducir en nuestro intérprete de comandos las sentencias adecuadas para que el jugador pueda dialogar (de forma limitada) con el pirata. Introducimos el siguiente código justo después del comando “apagar antorcha” y justo antes de los comandos de más de una palabra:

 }
 // Nuevo (Principio)
 else if (localizacionPirata == habitacion)
 // comandos del pirata
 {
	strcpy(palabra,strtok(comando," "));
	if (strcmp(comando,"decir") == 0)
	{
		strcpy(palabra,strtok(0,"\0"));
		if (palabra == 0)
			printf("\n\nQue es lo que quieres decir?\n\n");
		else
		{
			if (!strcmp(palabra,"\"ayuda\""))
				printf("\n\nEl pirata dice \"venceme con la espada
				  si deseas convertirte en un gran bucanero\"\n\n");
			else
				printf("\n\nEl pirata no entiende lo que dices.\n\n");
		}
	}
 }
 // Nuevo (Fin)
 else
 // Comandos con más de una palabra
 {

Para entrar en esta parte del código e interpretar los comandos introducidos por el jugador relacionados con el pirata, deberemos comprobar que el pirata y el jugador se encuentran en la misma habitación. En caso de que no sea así, no se entrará en esta parte del código y se mostrará el típico mensaje de error cuando se introduce un comando incorrecto. De esta forma, si el jugador introdujera alguno de los comandos relacionados con el pirata sin haber descubierto previamente su existencia, no se le dará ninguna pista sobre ello.

El pirata parece un poco tonto, pero sabe más de lo que dice...

Haciendo uso de strtok (cuya explicación se realizó en la entrega anterior), comprobamos si la primera palabra del comando del jugador es decir. En caso de que sea así, tratamos de analizar qué es lo que ha dicho el jugador. Usando strtok de nuevo extraemos el resto del comando introducido del jugador, y lo comparamos con la cadena “ayuda” (haciendo uso de la secuencia de escape \” para poder introducir comillas dentro de la cadena). En caso de que el jugador haya dicho exactamente eso, el pirata le dará la pista que necesita. En caso contrario, el pirata ignorará al jugador.

Como se puede comprobar, el pirata no es un hombre de muchas palabras. De todas formas, con esto quedan sentadas las bases para una posible ampliación de su vocabulario (lo cual se deja como ejercicio al lector).

Por último vamos a permitir que Guybrush llegue a ser pirata. Para ello, vamos a introducir las modificaciones necesarias en el intérprete de comandos para que se pueda finalizar la aventura.

Como le dice el pirata a Guybrush si éste le pide ayuda, para poder llegar a ser un pirata de verdad deberá vencerle con la espada. Pero no es tan fácil, pues el pirata es un hábil espadachín que sólo podrá ser vencido si está ebrio. Por lo tanto, para finalizar la aventura, Guybrush deberá darle al pirata la jarra de grog que lleva desde el inicio de la aventura para que éste se emborrache, y luchar o pelear con la espada.

El código que debemos introducir en el intérprete de comandos, y dentro de la parte correspondiente a los comandos relacionados con el pirata, es el siguiente:

else if (localizacionPirata == habitacion)
// comandos del pirata
{
  // Nuevo (Principio)
  if (!strcmp(comando,"dar jarra a pirata"))
  {
    if (objetos[1].localizacion == -1)
    {
      printf("\n\nEl pirata coge la jarra y se bebe su contenido...\n\n");
      sleep(1);
      printf("\n\n... el pirata sigue bebiendo...\n\n");
      sleep(1);
      printf("\n\n... el pirata tiene sintomas evidentes de embriaguez.\n\n");
      borrachoPirata = 1;
      objetos[1].localizacion = -2;
      pesoTransportado -= objetos[1].peso;
    }
  }
  else if (!strcmp(comando,"luchar con pirata") || 
           !strcmp(comando,"pelear con pirata"))
    {
      if (objetos[0].localizacion == -1)
      {
        if (borrachoPirata == 0)
        {
          printf("\n\nEl pirata te vence sin dificultades...\n\n");
        }
        else
        {
          final = 1;
          printf("\n\nEl pirata esta tan borracho que no puede
            ni aguantar la espada... \n\n");
          sleep(1);
          printf("\n\nTras un larga lucha...\n\n");
          sleep(1);
          printf("\n\nUna larga, larga, lucha...\n\n");
          sleep(1);
          printf("\n\nConsigues vencerle!!\n\n");
          sleep(2);
          printf("\n\n\n\nFELICIDADES. Has conseguido completar la
            aventura y convertirte en un GRAN pirata!\n\n");
        }
     }
    }
    // Nuevo (Fin)
    else
    {
      strcpy(palabra,strtok(comando," "));
      if (strcmp(comando,"decir") == 0)
      {
        strcpy(palabra,strtok(0,"\0"));

Se ha añadido código para interpretar dos comandos: dar jarra a pirata y luchar con pirata (o pelear con pirata (Siempre sería deseable que el jugador pudiera utilizar sinónimos para las acciones que deba realizar). Vamos a analizar cada una de estas partes por separado.

Con respecto al código para dar la jarra al pirata:

if (!strcmp(comando,"dar jarra a pirata"))
{
  if (objetos[1].localizacion == -1)
  {
    printf("\n\nEl pirata coge la jarra y se bebe su contenido...\n\n");
    sleep(1);
    printf("\n\n... el pirata sigue bebiendo...\n\n");
    sleep(1);
    printf("\n\n... el pirata tiene sintomas evidentes de embriaguez.\n\n");
    borrachoPirata = 1;
    objetos[1].localizacion = -2;
    pesoTransportado -= objetos[1].peso;
  }
}

Comenzamos por comprobar si el jugador tiene la jarra en el inventario (que el objeto 1, la jarra, tiene -1 como valor de localización, lo cual quiere decir que está en el inventario del jugador). En el caso de que sea así, mostramos tres mensajes de texto con printf informando al jugador de la nueva situación ebria del pirata. Entre mensaje y mensaje, para añadir mayor dramatismo, se hace uso de la función sleep (que forma parte de stdlib.h). El parámetro de sleep indica el número de segundos que se detendrá la aplicación. Lo siguiente es darle el valor de 1 a la variable borrachoPirata, almacenando el estado de embriaguez del mismo. Por último, hacemos que la localización de la jarra sea -2 (con lo cual no está en el inventario del jugador, y tampoco en ninguna de las habitaciones: es imposible que el jugador vuelva a encontrase con la jarra) y le restamos su peso al peso transportado por el jugador. La jarra está 'destruida' y no podrá volver a utilizarse en la aventura.

Y con respecto al código de la espada:

else if (!strcmp(comando,"luchar con pirata") || !strcmp(comando,"pelear con pirata"))
{
  if (objetos[0].localizacion == -1)
  {
    if (borrachoPirata == 0)
    {
      printf("\n\nEl pirata te vence sin dificultades...\n\n");
    }
    else
    {
      final = 1;
      printf("\n\nEl pirata esta tan borracho que no puede ni
        aguantar la espada... \n\n");
      sleep(1);
      printf("\n\nTras un larga lucha...\n\n");
      sleep(1);
      printf("\n\nUna larga, larga, lucha...\n\n");
      sleep(1);
      printf("\n\nConsigues vencerle!!\n\n");
      sleep(2);
      printf("\n\n\n\nFELICIDADES. Has conseguido completar la aventura
        y convertirte en un GRAN pirata!\n\n");
    }
  }
}

Primero comprobamos si la espada está en el inventario del jugador (si el valor de localización del objeto 0, correspondiente a la espada, es -1). En el caso de que sea así, se comprueba si el pirata está borracho, según el valor de la variable borrachoPirata. Si vale 0, el pirata no está borracho, por lo que mostramos simplemente un mensaje de texto indicando que al estar sobrio el pirata vence al jugador sin dificultades.

En el caso contrario, la variable valdrá 1 y el pirata estará borracho. Le damos a la variable final el valor 1, indicando que el juego ha terminado. Si recordamos el código, veremos que al principio del intérprete de comandos había una sentencia while que hacía ejecutarse este intérprete hasta que el valor de la variable final cambiara. Pues bien, por fin podremos terminar la avenura. Lo siguiente es mostrar unos cuantos mensajes con printf, enfatizando con sleep, relatando la tremenda lucha con el pirata. Para el último mensaje, el de felicitación, se deja una espera más larga, para añadir algo de tensión escénica.

No había presupuesto para un final más espectacular

¡Y ya está! Tenemos una aventura simple pero completa, que puede ser terminada por un jugador avispado.

Durante estos tres artículos hemos desarrollado una sencilla aventura conversacional, con el objetivo de mostrar las características de las herramientas de texto de z88dk. Los resultados son poco espectaculares, pero hemos conseguido meter la cabeza y perder el miedo.

Lo que se habrá podido comprobar es que estas herramientas de texto son lentas. Esto quiere decir que solo se podrían utilizar con aplicaciones o juegos exclusivamente basados en texto. Para otro tipo de juegos, deberemos emplear otras técnicas, que iremos viendo conforme se presenten más entregas de z88dk.

¿Y a partir de ahora qué? Empezaremos a trabajar con conceptos gráficos y crear juegos más 'bonitos' visualmente. Veremos de qué capacidades hace gala z88dk para poder programar juegos de Spectrum en C. Pero todo esto, a partir del siguiente número…