El lenguaje de los ObjetosInform como lenguaje de programaciónLeyendo datosEl lenguaje de los Datos

El lenguaje de los Datos

Directivas y constantes

Hasta ahora, todos los programas de ejemplo han consistido sólo en una secuencia de rutinas, cada una de ellas con enmarcadas entre [ y ]. Estas rutinas no tienen forma de compartir información entre ellas, excepto por el paso de parámetros. Esta estructura realmente no es la más adecuada para un programa largo cuya tarea sea simular algo complicado (como el mundo de un juego de aventuras): sería útil tener algún tipo de almacén central de información, al cual pudieran tener acceso todas las rutinas, cuando fuera necesario.

Este tipo de información disponible para todas las rutinas, se denomina "global", en contraposición a la información "local" de cada rutina. (Como se verá en la sección sobre objetos, hay una opción intermedia entre "global" y "local", en la cual la información está disponible sólo para un grupo de rutinas que trabajan juntas en una parte del programa).

Las estructuras de datos se añaden a los programas en Inform usando palabras especiales llamadas "directivas", en medio de las definiciones de las rutinas. Es importante distinguir entre las directivas, que indican a Inform que haga algo "ahora" (normalmente crear algún tipo de almacén de información), y las instrucciones que ocurren dentro de las rutinas, que simplemente son traducidas de algún modo, pero no ejecutadas hasta que el programa haya sido totalmente compilado y se esté ejecutando en un intérprete Z. De hecho, ya conocemos una directiva, aunque no lo habíamos dicho: se trata de la directiva [, que significa "traduce el siguiente conjunto de instrucciones, hasta que aparezca un ]". En total, Inform tiene 38 directivas que son las siguientes:

    Abbreviate  Array       Attribute  Class        Constant     Default
    Dictionary  End         Endif      Extend       Fake_action  Global
    Ifdef       Ifndef      Ifnot      Ifv3         Ifv5         Iftrue   
    Iffalse     Import      Include    Link         Lowstring    Message     
    Nearby      Object      Property   Release      Replace      Serial      
    Switches    Statusline  Stub       System_file  Trace        Verb        
    Version     [  

La mayoría de estas son demasiado técnicas y no son usadas por muchos programadores (por ejemplo Trace, Stub, Default, System_file, Abbreviate, y Dictionary). Otras sirven para controlar detalles sobre qué debe ser compilado y qué no (estas son Ifdef, Ifndef, Ifnot, Ifv3, Ifv5, Iftrue e Iffalse). Estas directivas que no son demasiado importantes se tratan en el capítulo sobre compilador.

Esto nos deja tan solo 9 directivas básicas para la creación de estructuras de datos, y estas son las que interesa conocer ahora:

 
    Array       Attribute   Class      Constant     Extend       Global
    Object      Property    Verb

Es habitual escribir estas directivas con la primera letra en mayúsculas, de este modo se distinguen fácilmente de las instrucciones. Las directivas Attribute, Class, Object y Property se tratarán en la sección sobre objetos.

La directiva más simple que tiene un efecto "global", es decir, que no tiene efecto sólamente dentro de una rutina, es la directiva Constant. El siguiente programa, un juego de azar bastante poco satisfactorio, muestra un uso típico de Constant

Constant PUNTUACION_MAXIMA = 100;

[ Main;
  print "Has obtenido ", random(PUNTUACION_MAXIMA), 
        " puntos, de un máximo de ", PUNTUACION_MAXIMA, ".^";
];

El valor de la puntuación máxima se usa dos veces en la rutina Main. Por supuesto, el programa sería el mismo si quitamos la definición de la Constant y sustituimos PUNTUACION_MAXIMA por 100 las dos veces que aparece en Main. La ventaja de usar Constant es que hace muy sencillo cambiar el valor, por ejemplo 50 en lugar de 100, puesto que basta con cambiar una sola línea del programa. Además hace el código fuente mucho más legible, puesto que de alguna forma "explica" lo que significa el número 100.

Si no se especifica ningún valor para una constante, como por ejemplo en la línea

Constant DEBUG;  

entonces la constante es creada con un valor igual a 0.

Variables globales

Como se ha comentado antes, hasta ahora las únicas variables permitidas eran "variables locales", propiedad privada de las rutinas en que se declaraban. Una "variable global" es una variable que es accesible desde cualquier rutina. Una vez que una variable global ha sido declarada, se usa de la misma forma que una variable local. La directiva para declarar una variable global es Global:

Global puntuacion = 36;  

Esto crea una variable llamada puntuacion, la cual, cuando el programa comienza, tiene el valor 36. El valor de puntuacion puede ser alterado o usado en cualquier punto, por debajo de la línea en la que la variable es definida.

Arrays

Un "array" es una colección indexada de variables (globales), que sirve para mantener una serie de números organizados en una secuenca. Permite definir reglas generales sobre cómo tratar a un grupo de variables. Por ejemplo, la directiva

Array mazo_de_cartas --> 52;

crea en realidad 52 variables, cuyos nombres serían

    mazo_de_cartas-->0   mazo_de_cartas-->1   ...   mazo_de_cartas-->51

Hay dos tipos básicos de arrays: "arrays tipo word" (que se declaran y utilizan poniendo -->, como el ejemplo anterior) y "arrays tipo byte" (para los que se pone -> de forma análoga). Los elementos de un "array tipo word" pueden contener cualquier número. En cambio los elementos de un "array tipo byte" sólo pueden contener números entre 0 y 255 (ambos inclusive). La única ventaja de un "array tipo byte" es que ahorra memoria, pero se recomienda a los principiantes que usen siempre "arrays tipo word".

Además de lo anterior, Inform proporciona un tipo de array especial, que tiene una pequeña estructura prefijada: este tipo de arrays guarda en su elemento 0 el número total de elementos que contiene el array. Un "array tipo word" con esta propiedad, se denomina una "tabla", mientras que un "array tipo byte" con esta propiedad se denomina una "cadena" (string). Existen directivas para declarar arrays así. Por ejemplo, el siguiente array

Array continentes table 5;

tiene de hecho 6 elementos (aunque su declaración diga 5). Esto es porque el elemento 0 (continentes-->0) almacena el número 5. Los restantes elementos (desde continentes-->1 hasta continentes-->6) quedan libres para almacenar en ellos lo que el programador quiera. (El programa puede cambiar el valor que está almacenado en el elemento 0, pero esto no hará que el array cambie de tamaño). En cuanto a los arrays tipo string, como se ha dicho, almacenan elementos que sólo pueden variar entre 0 y 255. Habitualmente se usan para almacenar letras, ya que cada letras internamente se codifica mediante un código que siempre está entre 0 y 255. Veamos un ejemplo:

Array clave string "PELIGRO";
Array numero_telefono string "1978-345-2160";
...
PrintString(clave);
...
PrintString(numero_telefono);
...
[ PrintString el_array i;
    for (i=1: i<=el_array->0: i++)
        print (char) el_array->i;
];

La ventaja de los arrays tipo string, como vemos, es que se pueden escribir rutinas genéricas, como PrintString, que funcionarán para arrays de cualquier tamaño (puesto que averiguan el tamaño real consultando el elemento 0).

Recapitulando, Inform proporciona cuatro tipos de array:

    -->   ->   table   string

(!) He escrito una descripción mucho más detallada, aunque tal vez demasiado técnica, sobre los tipos de array y su manejo, . Si eres el tipo de persona que te gusta comprender hasta el último detalle, tal vez te interese leer este documento.

También hay cuatro formas de inicializar un array, es decir, cargar los valores que han de tener inicialmente sus elementos. (Por tanto, la directiva Array admite en total 16 formas distintas de escribirse, según los cuatro tipos de array y las cuatro formas de inicialización).

En todos los ejemplos anteriores, los elementos del array contienen 0 cuando el programa empieza (puesto que no hemos especificado otro valor inicial para ellos). Esta es la primera forma de especificar los valores iniciales (es decir, no especificarlos en absoluto y dejar que todos valgan cero). La segunda forma consite en dar una lista de valores en la declaración. Por ejemplo:

Array primos --> 2 3 5 7 11 13;  

es un "array tipo word", que tendrá 6 elementos (llamados primos-->0, primos--1, etc. hasta primos-->5). El valor inicial de primos-->0 será 2, el de primos-->1 será 3, etc. Observar que en esta forma de declarar el array no especificamos mediante un número cuántos elementos tiene, sino que Inform contará cuántos valores iniciales hemos escrito (6, en el ejemplo) y creará un array con el espacio justo para ellos. Observar también que la lista de valores iniciales va separada por simples espacios.

La tercera forma de crear un array permite suministrar un texto como su valor inicial. Esto sólo tiene sentido en "arrays tipo byte", puesto que ese texto será convertido en una secuencia de códigos, cada uno de ellos de 1 byte. Ya hemos visto un par de ejemplos antes (los arrays clave y numero_telefono). Veamos otro ejemplo:

Array nombre_jugador -> "Francisco Botas";  

que prepara un array tipo byte, lo mismo que si hubieramos usado esta otra directiva:

Array nombre_jugador -> 'F' 'r' 'a' 'n' 'c' 'i' 's' 'c' 'o' ' ' 'B' 'o' 't' 'a' 's';

y en ambos casos se creará un array de 15 elementos, que es el número de letras suministradas (contando el espacio, por supuesto). Obsérvese que al haber utilizado la directiva -> y no string, el elemento 0 del array no contiene su longitud, sino que contiene el código de la letra F, que es el primer elemento suministrado en la declaración9.

(!)(!) La cuarta forma de inicializar un array ha sido desterrada de Inform. Era una forma obsoleta. Las nuevas versiones del compilador aún admiten esa forma por compatibilidad, pero no debe usarse más. Consistía en suministrar la lista de valores delimitada por [ y ], con comas entre los valores.

(!) Es responsabilidad del programador el asegurarse de que no se intenta leer o escribir en elementos inexistentes del array, es decir, con un índice superior al tamaño del array (por ejemplo mazo_de_cartas-->1000). Este tipo de error es famoso por ocasionar que los programas fallen de forma impredecible, y muy difícil de diagnosticar. He aquí por ejemplo un programa erróneo:

Array diez --> 10;
Array cincos --> 5 10 15 20 25;
[ Main n;
  for (n=1: n<=10: n++) diez-->n = -1;
  print cincos-->0, "^";
];

Este programa debería imprimir "5", puesto que ese es el valor de cincos-->0, pero de hecho imprimirá -1. El problema es que los elementos de diez van desde diez-->0 hasta diez-->9, y no (como el programa supone implícitamente) desde diez-->1 hasta diez-->10. Así que llegará el momento en que el programa intente guardar un -1 en diez-->10, un elemento que no existe. Aquí puede pasar cualquier cosa. En este caso, el valor es escrito en el primer elemento del siguiente array, "corrompiendo" los datos que había en él.

Ejemplo 7: mezclando una baraja

Este programa simula la mezcla de una baraja española de 40 cartas. Los naipes son representados mediante números que van desde 0 (que representa el As de Oros) hasta 39 (el Rey de Bastos). El mazo de cartas puede imaginarse como un array de 40 posiciones. La posición 0 representa la carta superior del mazo, y la posición 39 la carta inferior. Por tanto, el elemento

  mazo_de_cartas-->i

representa la carta en la posición i-ésima. Empezamos generando un mazo de cartas nuevo, como recién salido de fábrica, es decir, con todas sus cartas en orden. Esto significa que la carta de valor i está en la posición i del mazo. Por tanto, el mazo tiene el As de Oros como carta superior y el Rey de Bastos como carta inferior.

La rutina Intercambia sirve para intercambiar dos cartas del mazo al azar. LLamando a esta rutina repetidas veces será como mezclaremos la baraja. Después imprimiremos los nombres de las cartas una vez mezcladas, para lo cual definimos una regla nueva para print, llamada (naipe).

Constant MEZCLAS = 100;
Array mazo_de_cartas --> 40;

[ Intercambia x y z;

  !   Inicialmente x e y valen ambas cero

  while (x==y)  ! Elegimos al azar dos números diferentes
  {   x = random(40) - 1; y = random(40) - 1;
  }

  ! Ahora intercambiamos los contenidos de las posiciones
  ! x e y del array (necesitamos una variable intermedia
  ! para ello)
  z = mazo_de_cartas-->x;
  mazo_de_cartas-->x = mazo_de_cartas-->y;
  mazo_de_cartas-->y = z;
];

[ naipe n;
  switch(n%10)
  {   0: print "As";
      1 to 7: print n%10 + 1;
      10: print "Sota";
      11: print "Caballo";
      12: print "Rey";

  }
  print " de ";
  switch(n/10)
  {   0: print "Oros";
      1: print "Copas";
      2: print "Espadas";
      3: print "Bastos";
  }
];

[ Main i;

  !   Crear el paquete ordenado "de fábrica"
  for (i=0:i<52:i++) mazo_de_cartas-->i = i;

  !   Intercambiar cartas unas cuantas veces
  for (i=0:i<MEZCLAS:i++) Intercambia();
  print "El mazo ha sido mezclado. Ahora contiene:^";
  for (i=0:i<52:i++)
      print (naipe) mazo_de_cartas-->i, "^";
];

Observa cómo se ha programado la regla (naipe) para que escriba el nombre de la carta i. Observa también que 100 mezclas es lo justo para dejar la baraja lo bastante mezclada. Si lo cambiamos por 1000 el programa tardaría demasiado; si lo cambiamos por 10, el resultado sería muy sospechoso.
(!)

El ejemplo anterior hace la mezcla de una forma muy simple, pero hay métodos más eficientes. He aquí uno inventado por Dylan Thurston, que consigue una mezcla perfecta con tan sólo 51 intercambios (no parte de una baraja ordenada, sino que va construyendo sobre la marcha la baraja ya mezclada).

mazo_de_cartas-->0 = 0;
for (i=1:i<40:i++) 
{   j = random(i+1) - 1;
    mazo_de_cartas-->i = mazo_de_cartas-->j; mazo_de_cartas-->j = i;
}

Siete estructuras de datos especiales

(!)

Todos los programas Inform contienen automáticamente siete estructuras de datos especiales, cada una de un tipo diferente: el árbol de objetos, la gramática, la tabla de acciones, el número de versión, el número de serie, la "bandera de línea de estado" y el diccionario. Estas estructuras de datos están diseñadas expresamente para juegos de aventuras y (excepto el árbol de objetos) pueden ser ignoradas para otros tipos de programa. Por ello la mayoría de estas estructuras se tratan en el capítulo sobre la librería InformATE.

  1. Para el árbol de objetos (y las directivas Object y Class) véase la sección sobre objetos
  2. Para la gramática (y las directivas Verb y Extend), véanse la sección sección sobre parsing de verbos y la sección sección sobre elementos de la gramática.
  3. Para las acciones (y las instrucciones <...> y <<...>>, así como la notación ##), véase la sección sección sobre acciones y reacciones.
  4. El número de versión (que es impreso automáticamente al empezar el juego, si éste ha sido hecho con la librería InformATE) es 1, a menos que se especifique otra cosa mediante la directiva

    Release <número>;
    Normalmente, la versión 1 es la primera que se hace pública, y las versiones 2, 3, ... serían reversiones corregidas.
  5. El número de serie es un texto de 6 letras, que es inicializado automáticamente por el compilador a partir de la fecha en que se compile. Por ejemplo 000831 significaría "Año 2000, mes 8 (agosto), día 31". Si se desea, puede cambiarse por otra secuencia de 6 letras cualesquiera (dígitos normalmente) mediante la directiva
        Serial "dddddd";    
    
  6. La "bandera de línea de estado", sirve para elegir entre diferentes modelos de "línea de estado", que son las primeras líneas en la parte superior de la pantalla en un juego conversacional. Mira la sección sobre daemons y el paso del tiempo para ver un ejemplo de uso de la directiva StatusLine.
  7. El diccionario es una lista de palabras que el juego comprende, y es construido automáticamente por Inform, a medida que va compilando tu juego. El diccionario se compone de todas las palabras que aparecen en tu listado delimitadas por apóstrofes, como por ejemplo 'mesa'.
    (Esp) En algunos casos especiales, también las palabras que aparecen entre comillas dobles son incluidas en el diccionario. Estos casos especiales son: cuando la palabra aparece en la propiedad name o nombre de un objeto, o cuando aparecen en la definición de un verbo con la directiva Verb.

    De todas formas, incluso en esos casos especiales se admiten tambíen comillas simples, por lo que se aconseja usar comillas simples en todos los casos, para mayor consistencia. Las comillas dobles se usarán sólo para textos que deban ser impresos con print, o cadenas de texto almacenadas en un array.

    Si, por ejemplo, tu programa contiene una línea como esta:

    if (primera_palabra=='arenque') print "Has escrito la palabra arenque!";     
    

    Inform, al ver la palabra 'arenque' entre apóstrofes, la meterá en el diccionario.

    Observar que una letra entre comillas como 'a', es tratada por Inform como una constante de tipo carácter, y no como una palabra de diccionario. Si queremos meter en el diccionario una palabra de una sola letra, deberemos escribirla así 'a//'. Esto es importante en la definición de conjunciones, al crear la gramática.

(!)(!) De lo dicho, parece desprenderse que el diccionario es algo en donde se van metiendo palabras, sin poder recuperarlas despues. La utilidad aparece cuando la instrucción read intenta interpretar un texto escrito por el jugador:

read array_de_texto buffer_de_parsing;

Hay que resaltar que la instrucción read se limita a hacer la interpretación más simple posible, y no debe confundirse con el parser mucho más elaborado que se incluye en la librería InformATE.

Lo que hace es separar el texto que se ha leido en una secuencia de palabras, en la cual las comas y los puntos cuentan como una palabra más. (Se muestra un ejemplo en la sección sobre interpretación de los sustantivos).

Antes de ejecutar read, debemos almacenar en

    buffer_de_parsing->0

un número que indique el máximo número de palabras que queremos interpretar (si hay más palabras, las restantes se ignoran). El número de palabras realmente encontradas será guardado por read en

    buffer_de_parsing->1

(observar que en ambos casos usamos ->, luego estos elementos se tratan como tipo byte, y pueden tomar como máximo el valor 255). Además, en el array buffer_de_parsing se escribe un bloque de datos para cada palabra encontrada. Estos datos se acceden de una forma un tanto enrevesada. En el elemento

    buffer_de_parsing-->(n*2-1)

podemos encontrar el "valor de diccionario" de la palabra n-ésima (para n=1,2,3...). Este "valor de diccionario" es un número entre 1 y 65535 que identifica a la palabra. Si la n-ésima palabra que escribió el jugador no estaba en el diccionario, entonces ese elemento toma el valor 0.

Además, los elementos

    buffer_de_parsing->(n*4)
    buffer_de_parsing->(n*4+1)

contienen el número de letras de la palabra n-ésima, y el índice en el cual se halla la primera letra de esa palabra, dentro del "array de texto".

Por ejemplo,

[ PorFavorRespondeSiONo i;
  for (::)
  {   buffer->0 = 60;
      parse->0 = 1;
      print "Por favor responde ~Si~ o Ño~> ";
      read buffer parse;
      if (parse-->1 == 'si') rtrue;
      if (parse-->1 == 'no')  rfalse;
  }
];

(Observa que para acceder a la palabra n-ésima, habría que acceder a parse-->(n*2-1), y que esto nos da parse-->1 cuando n=1.


Zak McKraken - spinf@geocities.com

El lenguaje de los ObjetosInform como lenguaje de programaciónLeyendo datosEl lenguaje de los Datos