Contenidos Parte II: Empieza lo interesante Descripciones variables Rompecabezas clásicos

Rompecabezas clásicos

El rompecabezas más viejo y más usado es aquel en el que un objeto está oculto por otro, y sólo se revela al examinar el segundo. Tenemos un par de rompecabezas así en nuestra aventura. En la mazmorra hay un esqueleto que al ser examinado revela un cuchillo, y en el dormitorio hay unas camas que al ser examinadas revelan unas fundas. También vamos a esconder un trozo de carbón en la chimenea, aunque no sirva para nada en el juego.

Hemos visto que Inform tiene un atributo engañosamente llamado concealed, que parece invitar a ser usado para esto. Sin embargo hemos visto también que concealed simplemente hace que la librería no mencione el objeto, pero no impide que el jugador se refiera a él.

Es decir, si pusieramos en la mazmorra un objeto Esqueleto y un objeto cuchillo, y diéramos a éste último el atributo concealed, Inform le diría al jugador al entrar en la localidad: "Puedes ver un esqueleto" (silenciando toda mención al cuchillo). Sin embargo, el jugador podría pese a todo escribir "COGE CUCHILLO" o "EXAMINA CUCHILLO" y la librería le permitiría la acción.

La única forma de impedir que el jugador pueda referirse al cuchillo es ¡no poner un cuchillo en esa localidad! Si un objeto está en una localidad, el jugador siempre podrá referirse a él como blanco de sus acciones.

Por tanto la forma típica de programar esto consiste en tener programado el objeto cuchillo, pero no ponerlo dentro de ninguna de las localidades del juego. De este modo, cualquier intento de manipular el cuchillo por parte del jugador, causará el mensaje de error "No veo eso por aqui".

Programaremos después el esqueleto de forma que, tan pronto como el jugador lo examine, el cuchillo será movido a la Mazmorra. Así, a partir de este punto el cuchillo se convertirá en un objeto más del juego, que podrá ser manipulado o no, según sus atributos y sus propias reglas programadas en su rutina before.

Para crear un objeto que no está en ninguna localidad, basta no poner en su cabecera en qué localidad se halla. Recuerda que éste era el último parámetro que se daba en la cabecera (echale un vistazo al objeto antorcha). Si el objeto cuchillo empieza así:

Object cuchillo "cuchillo"

al no poner en qué localidad se halla, resultará que no está en ninguna. De todas formas, es buena idea crear una localidad ficticia, que yo suelo llamar "Limbo", y meter en ella todos los objetos que no aparecen inicialmente en el juego. Cuando el objeto en cuestión deba aparecer, bastará moverlo desde el "Limbo" a la localidad apropiada. Y a la inversa, para hacer desaparecer un objeto del mundo del juego, bastará moverlo al "Limbo" (si no usaramos Limbo, la forma de hacer desaparecer un objeto sería usar remove objeto, lo que en realidad lo saca de todas las localidades).

Esqueleto y cuchillo

Programemos de momento un cuchillo mínimo, que conste simplemente de su nombre y una descripción, y hagamos que inicialmente se halle en la localidad ficticia llamada "Limbo". Debemos crear también esta localidad, claro. Esto puedes escribirlo en cualquier lugar del código, después de incluir Verblib. Un buen sitio puede ser justo antes de la rutina Initilaise, para tener juntos todos los objetos de limbo y encontrarlos fácilmente, para cuando haya que cambiar algo en su código.

Object Limbo "Limbo"
with description "Eh. ¿cómo has llegado aquí? Malditos
  betatesters...",
has light;

Object cuchillo "pequeño cuchillo" Limbo
with name 'pequeno' 'cuchillo' 'punal' 'hoja' 'cuchilla',
     description "Un pequeño cuchillo, cubierto de herrumbre"

Observa cómo he evitado el uso de la eñe en las palabras de vocabulario (las que figuran en la propiedad name). Esto no impide que el jugador pueda escribir "EXAMINA PEQUEÑO PUÑAL", por ejemplo. Si el jugador usa eñes, Inform las convertirá en enes antes de comenzar el parsing. Sin embargo, si hubieramos puesto 'puñal' en la lista de nombres de este objeto, estaríamos discriminando a los jugadores que intentan jugar desde un ordenador sin eñes, ya que nunca podrían escribir esa palabra. Lo mismo vale para los acentos. No uses acentos en las palabras de vocabulario si quieres lograr la máxima compatibilidad y difusión de tu juego.

Ahora debemos programar el esqueleto, y necesitamos que al ser examinado haga aparecer el cuchillo. Sin embargo hay que estar al tanto de un importante detalle. El cuchillo debe aparecer sólo la primera vez que se examina el esqueleto, y no cada vez que se le examine. Por tanto necesitamos llevar de algún modo el registro de si el esqueleto ya ha sido examinado o no.

Para estos menesteres, todos los objetos del juego tienen un atributo llamado general, que por defecto está desactivado en todos ellos. La librería nunca activa este atributo, ni lo consulta para nada. Por tanto, queda libre para que el programador lo use para sus propias necesidades. El uso más frecuente de este atributo es como indicador de "puzzle resuelto", de modo que tan pronto como el jugador resuelve algo (encuentra el cuchillo, por ejemplo), el objeto que causaba el puzzle (el esqueleto) recibe el atributo general, como indicador de "esto ya está". El código que hace aparecer el cuchillo, por tanto, sólo lo hará si el esqueleto no tiene ya activado el atributo general.

Vamos pues con la programación del esqueleto. El siguiente código conviene que lo pongas después del de la Mazmorra, para tener los objetos cerca de las localidades en que aparecen.

Object Esqueleto "esqueleto" Mazmorra
 with   name 'esqueleto' 'humano' 'muerto' 'cadaver',
        description [;
            print "Los huesos amarillentos, las cuencas vacías";
            if (Esqueleto hasnt general) 
            {
                move cuchillo to Mazmorra;
                print ". Junto a él ves un pequeño cuchillo";
                give Esqueleto general;
            }
            ".";
        ],
 has    scenery;

El código que hace aparecer el cuchillo lo hemos puesto dentro de la propia descripción del esqueleto, haciendo uso de la posibilidad de que las descripciones sean rutinas. Antes de hacer aparecer el cuchillo, verificamos si el esqueleto tiene el atributo general, pues esto indicaría que el cuchillo ya ha sido encontrado y no debe aparecer de nuevo. La condición para comprobar que un objeto no tenga un atributo, es hasnt (opuesto de has).

Si el esqueleto no tiene general, movemos el cuchillo a la mazmorra. La instrucción Inform para esto es move obj1 to obj2. El obj2 no tiene por qué ser necesariamente una localidad. Podría ser un recipiente, o incluso el propio jugador (en cuyo caso el obj1 pasaría directamente a su inventario). Seguidamente imprimimos un mensaje para informar al jugador de su descubrimiento y finalmente activamos el atributo general del esqueleto, para señalar que el puzzle ha sido resuelto y así evitar que el cuchillo aparezca al examinarlo por segunda vez.

La línea que pone simplemente ".", como ves, está fuera del if, y por tanto se ejecutará siempre, independientemente de la condición. Se trata de uno de esos print implícitos, que imprimirá el punto final de la frase, un retorno de carro, y finalizará la función retornando true. Observa cómo hemos omitido el punto final en los print anteriores. También podríamos haber puesto el punto final en ambos print, y omitir el que va fuera del if, pero en este caso convendría sustituir este último punto por un rtrue, para asegurarse de que nuestra función devuelve true. Si bien en este caso concreto da lo mismo lo que la función retorne, es buena costumbre retornar true cuando nuestra rutina ha mostrado algún mensaje que debe sustituir a los defectos de la librería.

En Inform siempre hay muchas formas diferentes de lograr un mismo resultado. Por ejemplo, podríamos haber escrito el código que hace aparecer el cuchillo como parte de la rutina before del esqueleto (ante la acción Examine), en lugar de ponerlo en su description. En este caso sí se haría imprescindible retornar true, pues si no lo hicieramos, después de nuestros mensajes la librería imprimiría los suyos (que consistirían en la descripción del esqueleto, si éste tiene una propiedad description, o en el mensaje "No observas nada especial en el esqueleto" si no la tiene).

Convendría ahora pararse a testear si nuestro puzzle funciona correctamente, y de paso intentar unas cuantas acciones previsibles sobre el esqueleto o el cuchillo (cogerlo, moverlo,...) para asegurarnos de que las respuestas por defecto de la librería son adecuadas en todos los casos, o para sustituir las que no lo sean mediante una rutina before en estos objetos. Aquí tienes una versión del juego que incorpora el Limbo, el cuchillo y el esqueleto, en código fuente.

Verifica (mediante el comando xlista) que el cuchillo inicialmente está en el limbo, y comprueba (de nuevo con xlista) cómo se mueve a la mazmorra tras examinar el esqueleto.

Truco Otro verbo de depuración que puede interesarte es el verbo CAMBIOS. Si pones esto en el juego, a partir de ese instante Inform te avisará cada vez que un objeto se mueva, un atributo se ponga o quite mediante give, o una propiedad cambie de valor mediante una asignación. Para ello es indispensable que trabajes con la opcion S y D activadas. Buscalas en el Switch Manager de JIF.

Vamos a probarlo. Tal vez te sorprenda ver la gran cantidad de atributos que cambian cada vez que efectúas algunas acciones. Sigue mis pasos: carga el juego, escribe IRDONDE ANTORCHA (para teleportarnos a la escalera de caracol) y seguidamente CAMBIOS. Agarrate fuerte a la silla y pon:

>empuja antorcha
[Setting Escalera de caracol.w_to to 29]
Al empujar la antorcha una porción de pared se abre al oeste
dando acceso a una estancia.

Vemos que ha cambiado el valor de la propiedad w_to de la escalera de caracol, a la que ha dado el valor 29. Este 29, evidentemente, debe ser el número interno de la localidad Mazmorra (puedes verificarlo con xlista).

Prosigamos, y caminemos hacia el oeste:

>o
[Moving ti mismo to Mazmorra]

Mazmorra
Una silenciosa estancia débilmente alumbrada por los rayos
de luna que se filtran a través de un pequeño ventanuco. El suelo
está lleno de paja, colgando de unos grilletes en la pared
observas un esqueleto humano.
[Giving Mazmorra visited]

La línea "[Moving ti mismo to Mazmorra]" indica que el objeto player ha sido movido al objeto Mazmorra. La línea "[Giving Mazmorra visited]" indica que se ha activado el atributo visited (o sea visitado) del objeto Mazmorra. Su función, como es evidente, es indicar si esa localidad ya ha sido visitada con anterioridad o no. La librería puede cambiar algunas cosas de la descripción en caso de que el jugador ya haya estado allí (por ejemplo, puede no escribir descripción en absoluto y limitarse a poner el título de la localidad. Como ya vimos, esto depende también del valor de la variable lookmode).

Finalmente, veamos como aparece el cuchillo:

>x esqueleto
Los huesos amarillentos, las cuencas vacías[Moving pequeño cuchillo to Mazmorra]
. Junto a él ves un pequeño cuchillo[Giving esqueleto general]
.

Vemos también que, tras la descripción del esqueleto, se nos informa de que el cuchillo ha sido movido a la mazmorra. Después aparece el mensaje para el jugador "Junto a él ves...", tras el cual Inform nos avisa de que se ha activado el atributo general del esqueleto. Finalmente, el punto final (aparece aqui en línea aparte porque los avisos entre corchetes añaden siempre un salto de línea).

Como ya hemos dicho, hay muchas formas de programar una misma idea en Inform. Por ejemplo, para verificar si el cuchillo ya había sido encontrado, podríamos haber mirado si estaba o no estaba en el limbo, en lugar de usar el atributo general del esqueleto. En este enfoque (más parecido al que seguiría un programador de PAWS) bastaría algo como:

        description [;
            print "Los huesos amarillentos, las cuencas vacías";
            if (cuchillo in Limbo) 
            {
                move cuchillo to Mazmorra;
                print ". Junto a él ves un pequeño cuchillo";
            }
            ".";
        ],

Este mecanismo, de hecho, parece más simple. Pero imagina que decidimos permitirle al jugador que lance el cuchillo por la ventana, y como "premio" a su inteligencia, hacemos desaparecer el cuchillo del juego moviendolo de nuevo al Limbo. En este caso, con la programación anterior, ¡el jugador haría reaparecer el cuchillo examinando el esqueleto de nuevo! La cosa podría solucionarse teniendo dos Limbos diferentes: un Limbo1 para los objetos que aun no han aparecido en el juego y un Limbo2 para los que han sido eliminados.

Examinar en Inform

La librería Inform tiene varias acciones relacionadas con la idea de examinar un objeto. Se trata de Examine, Search (que puede entenderse como "registrar o mirar dentro de un objeto") y LookUnder. Los verbos que el jugador puede usar para dar lugar a estas acciones son muy variados:

Examine
Esta acción es causada por cualquiera de los verbos siguientes: "EXAMINA OBJETO", "X OBJETO", "EX OBJETO", "MIRA OBJETO", "MIRA HACIA OBJETO", "DESCRIBE OBJETO", "INSPECCIONA OBJETO", "OBSERVA OBJETO" y "LEE OBJETO".
Clave Observa que "LEE OBJETO" causa la acción Examine. No hay acción Read (Leer) en Inform, aunque esto puede cambiarse si lo necesitas.
Search
Esta acción es causada por cualquiera de las formas siguientes: "MIRA EN OBJETO", "MIRA DENTRO DE OBJETO", "MIRA SOBRE OBJETO", "MIRA A TRAVES DE OBJETO", "MIRA POR OBJETO", "BUSCA EN OBJETO", "REGISTRA OBJETO" y "REGISTRA EN OBJETO".

Clave Observa que Inform no entiende por ejemplo "EXAMINA EN OBJETO". Esto es lógico, ya que esa frase es incorrecta en español, pero puede despistar a algunos jugadores que piensen erróneamente que "MIRA" equivale a "EXAMINA" en todos los contextos. Recuerda que en Inform el significado de un verbo depende de la frase en que aparece, y no solo de la palabra que lo representa.

LookUnder
Esta acción es causada por las formas: "MIRA BAJO OBJETO" y "MIRA DEBAJO DE OBJETO".
Clave De nuevo, "EXAMINA DEBAJO DE OBJETO" no sería una forma válida para Inform.

Es de destacar que Inform no incorpora verbos ni acciones para la idea de "mirar detrás de algo". Incluso la idea de "mirar a través de algo" (como una ventana) o "Mirar por algo" (como mirar por un telescopio), aunque sí están contempladas, desembocan en la acción Search en lugar de tener sus propias acciones. Conviene estar sobreaviso de todos estos detalles.

En nuestro esqueleto, el cuchillo aparecerá si la acción generada por el jugador ha sido Examine (ya que es esta acción la que da lugar a la ejecución de la rutina description del esqueleto). Sin embargo, si el jugador genera Search o LookUnder, al no haber contemplado en el esqueleto estas acciones, la respuesta del juego sería: "No encuentras nada interesante" y "No ves nada interesante", respectivamente. Esto puede considerarse un bug del juego, ya que si lo primero que pone el jugador, en lugar de "EXAMINA ESQUELETO" es "REGISTRA ESQUELETO", el cuchillo no aparecerá.

Bien, es cierto que el cuchillo está "junto al esqueleto", y por tanto al registrarlo no debería aparecer, pero sin embargo considero "juego sucio" obligar al jugador a poner "EXAMINA ESQUELETO" para descubrir el cuchillo (tambien se conoce esta idea como 'sindrome de la palabra exacta'). Deberíamos ser tolerantes y admitir cualquier intento de búsqueda en el esqueleto.

Por tanto, debemos escribir una rutina before para nuestro esqueleto, que se haga cargo de las acciones Search y LookUnder, y que cambie los mensajes por defecto para este caso. Por ejemplo:

  before [;
    Search, LookUnder: if (Esqueleto hasnt general)
              {
                 move cuchillo to Mazmorra;
                 give Esqueleto general;
                 "Al acercarte al esqueleto ves a su lado un pequeño
                  cuchillo.";
              }
              else "Este desgraciado no llevaba nada más.";
   ],

Fíjate cómo trato ambas acciones de la misma forma, poniéndolas separadas por comas antes de los dos puntos. Esta sintaxis indica que si la acción es Search o LookUnder, debe ejecutarse ese código. El código en cuestión es similar al que había escrito en la descripción del esqueleto. Pero he movido la línea give esqueleto general delante del mensaje (¿sabes por qué?).

Piensa cuáles serían las respuestas del juego si el jugador primero EXAMINA el esqueleto, y después lo REGISTRA. Piensa también cómo serían si hace estas acciones en orden inverso. Comprueba lo que ocurre realmente jugando el juego que tienes aquí

La chimenea y el carbón

Una vez que hemos visto la idea clave de usar general como indicador de puzzle resuelto, el problema de encontrar un carbón dentro de la chimenea se resuelve de forma análoga al del cuchillo:

Object carbon "trozo de carbón" Limbo
with name 'trozo' 'carbon',
     description "Un trozo de negro carbón que parece haber
                  sobrevivido al fuego.";

Object chimenea "chimenea" PuertaPrincipal
with name 'chimenea' 'hogar',
     description [;
          print "Hace mucho tiempo que no arde fuego alguno
              en esta vieja chimenea";
          if (chimenea hasnt general) {
             move carbon to PuertaPrincipal;
             print ". Un trozo de carbón es todo lo que queda
                 del antiguo hogar";
             give chimenea general;
          }
          ".";
     ],
has female scenery;

Observa un detalle en la propiedad name del trozo de carbón. No hemos incluido la palabra "de" en esta propiedad ¿cómo es posible que el parser entienda entonces "coge trozo de carbón"?

Clave El parser de Inform (INFSP) está preparado para ignorar la preposición "de" si esta aparece entre dos palabras que se refieren al mismo objeto. Así, en "COGE TROZO DE CARBON" el "DE" es ignorado por el parser, puesto que "TROZO" y "CARBON" se refieren a un mismo objeto. La acción resultante será Take con noun==carbon.

En cambio, en una orden como "COGE CARBON DE CHIMENEA", la preposición "DE" no es ignorada, pues "CARBON" y "CHIMENEA" no son palabras que estén en el name de un mismo objeto. En este caso "DE" se entiende como una preposición que separa dos objetos diferentes, y por tanto para que el parser comprenda esta orden debe existir una gramática que le diga que la sintaxis "COGE <cosa> DE <otra cosa>" es correcta. (De hecho, la gramática contempla esta forma y generaría la acción Remove (Sacar) con noun==carbon y second==chimenea)

La cama y la funda

Te dejaré como ejercicio, amable lector, que codifiques tú mismo este último puzzle. Cuando el jugador examine la cama por primera vez, debe aparecer el mensaje "Una funda de tela cubre la cama", y en los sucesivos exámenes de la cama "Sólo restos de paja cubren la cama".

Naturalmente, la funda no debe ser accesible hasta que el jugador haya examinado la cama. En cuanto a la paja, la versión fácil del ejercicio no hace aparecer ninguna paja, de modo que si el jugador escribe "EXAMINA PAJA" simplemente recibirá un "No veo eso por aquí". ¿Te atreves con una versión difícil en la que realmente aparezca un objeto paja al examinar la cama por segunda vez?

Por el momento, la funda puede ser cogida por el jugador. En la próxima entrega haremos que no pueda cogerla hasta que haya cortado las ataduras que la sujetan a la cama.

Aquí tienes la versión del juego que incluye la chimenea con su carbón, y la cama con su funda y la paja que aparece al mirar una segunda vez.


Zak McKraken - spinf_2000@yahoo.com.
Adaptado a JIF por Luis Fernandez nfseoi (en) yahoo.es bajo la licencia Creative Commons Attribution-ShareAlike 2.0
Adaptado a JIF3+INFSP por Sarganar - sarganar (en) gmail.com bajo la misma licencia.

Contenidos Parte II: Empieza lo interesante Descripciones variables Rompecabezas clásicos