jueves, 29 de julio de 2010

Desambiguando en Inform6 (I)

A raíz de este post de Mastodon sobre la ambigüedad, voy a escribir dos artículos sobre cómo tratarla en Inform6.
Aviso que el güeno güeno es el segundo, en éste sólo voy a marear un poco la perdiz y a hacer pruebas. Pero también explicar dos modificaciones muy útiles que hacer a la librería por defecto para facilitar la desambiguación.

Si vamos a programar con InformATE debemos descargar esta librería:
http://www.caad.es/informate/informate/IntNombre.zip

Si en cambio programamos con InfSP6, puedes descargarla desde aquí:
http://www.caad.es/jarel/trastos/IntNombre.h
O en este paquete de librerías y extensiones, donde viene incluída:
http://www.caad.es/informate/infsp/downloads/extensiones.rar

Y vamos con el código base de ejemplo, es para InfSP6, aunque al final explicaré las modificaciones no obvias que hay que hacer para InformATE.

global variable1 =0; !esta variable la usaremos más adelante
Constant Story "desambiguación";
Constant ADMITIR_COMANDO_SALIDAS;
Replace ChooseObjects;
#Include "Parser";
!! ATENCIÓN. USAR VALORES DE prioritario entre 1 y 7
[ChooseObjects obj code;
   if(code==2){
        if(obj has nombreusado){

                if(obj provides prioritario){
                      return (obj.prioritario+2);
                      }
                     
                    return 1;
                        }
            return 0;
        }
if (code<2) { if (obj has scenery || obj has static) return 2; rfalse; } !
  if (action_to_be==##Eat && obj has edible) return 3;
  if (obj hasnt scenery || obj hasnt static) return 2;
  return 1;
];

#Include "Verblib";
[ Initialise;
location=habitacion;

rtrue;
];
!###########################################
object limbo "limbo"
with
! Esto no sirve para nada, es para evitar un error si no declaramos
! al menos una vez la propiedad prioritario
prioritario 0,
;

object habitacion "habitacion"
with
description "...",

has light;

object jarradeleche "jarra con leche" habitacion
with name 'jarra',
adjectives 'leche' 'con' 'que' 'contiene',
description "Es una jarra que contiene leche",
has female transparent;

object jarradeagua "jarra con agua" habitacion
with name 'jarra',
adjectives 'agua' 'con' 'que' 'contiene',
description "Es una jarra que contiene agua",
has female transparent;

object lechedelajarra "leche de la jarra" habitacion
with name 'leche',
adjectives 'jarra' 'blanca',
!prioritario 3,
description "Leche blanca, la jarra está llena de leche.",
has female;

!###########################################
! Procedemos a reemplazar el Parsenoun de la librería por el código de la
! librería Intnombre que hemos descargado. Con esto conseguimos que los
! adjetivos puntúen previa detección de un nombre.
!Replace ParseNoun;
!Include "IntnombreINFSP.h";
#Include "SpanishG";


Destacar que vamos a reemplazar dos funciones de la librería estandar, Parsenoun y ChooseObjects, para crear la infraestructura que nos permita desambiguar correctamente. Aunque de momento dejaremos desactivado el reemplazo de Parsenoun y no utilizaremos aún la propiedad "prioritario", para ver cómo parsea Inform6 por defecto... sí... POR DEFECTO (soy totalmente subjetivo).

Si compilamos el código de arriba, y procedemos a examinar los tres objetos presentes, la jarra con leche, la jarra con agua y la leche que está dentro de la jarra (oculta dentro de la jarra), ocurrirá lo siguiente:

Puedes ver una jarra con leche y una jarra con agua.
>x jarra
¿Cuál concretamente, la jarra con leche o la jarra con agua?

Vamos bien, hay dos objetos con nombre jarra, y nos pide concretar como no podía ser de otra manera.

>leche
Es una jarra que contiene leche

Al responder acto seguido "leche", el parser entiende que nos referimos a la jarra de leche. Una pena que no entienda también "a la de la leche".

>x jarra de leche
Es una jarra que contiene leche

De esta forma el parser no nos pide desambiguación, ya le hemos aportado el dato de que es la jarra de leche y no la de agua.

>x leche de jarra
Es una jarra que contiene leche

Primer problema, el parser no diferencia la "jarra de leche" de la "leche de la jarra", ya que ambos objetos tienen la misma puntuación, pero elige el objeto "jarra de leche" debido a que la "leche" está dentro de la jarra (En caso de empate el parser considera más importantes los objetos que no están dentro de otro, y toma su propia decisión). 
Si moviéramos el objeto "leche" a la localidad, obtendríamos una pregunta de desambiguación, al existir empate: ¿Cuál concretamente, la jarra con leche o la leche de la jarra?

>x leche
Leche, la jarra está llena de leche.

Correcto

>x blanca
Leche blanca, la jarra está llena de leche.

Nuevo problema: el sistema de puntuación por defecto valora los adjetivos aun ante la ausencia de un nombre, dando lugar a extrañezas como ésta. O incluso cosas peores, como que al escribir "EXAMINAR CONTIENE", el parser detecte que nos referimos a alguna de las jarras:
>x contiene
¿Cuál concretamente, la jarra con leche o la jarra con agua?


Ahora vamos a modificar el código del ejemplo de arriba, descomentando las siguientes líneas:
Replace ParseNoun;
Include "IntnombreINFSP.h";

Compilamos de nuevo, y algo ha cambiado en el parseado, vamos a ver:
Puedes ver una jarra con leche y una jarra con agua.
>x blanca
No veo eso que dices.

Hemos arreglado lo de los adjetivos que querían tener demasiado protagonismo.

>x leche blanca
Leche blanca, la jarra está llena de leche.

Como vemos, los adjetivos funcionan sólo acompañados de alguno de sus nombres.

>x jarra
¿Cuál concretamente, la jarra con leche o la jarra con agua?
>agua
Es una jarra que contiene agua

La pregunta de desambiguación del parser ante objetos con nombre idéntico sigue funcionando correctamente.

>x  jarra de leche
Es una jarra que contiene leche

Y si desambiguamos nosotros directamente en la orden, nos ahorramos la pregunta del parser.

>x leche de jarra
Es una jarra que contiene leche

Seguimos con el mismo problema de antes. Ambos objetos tienen la misma puntuación, pero la leche está contenida dentro de la jarra y sale perdiendo.
En cualquier caso, el sacar la leche de la jarra no arreglaría el problema, entraríamos en un bucle de preguntas "leche de jarra" versus "jarra de leche" del que sólo saldríamos aportando una palabra que esté contenida en el campo name o adjectives de uno de los objetos y no en el otro, por ejemplo:
Con "x leche BLANCA de la jarra" ganaría la leche.
Con "x jarra QUE CONTIENE leche" ganaría la jarra.

Pero de todos modos, de la misma forma:
Con "x jarra de la leche BLANCA" seguiría ganando la leche.
Y con "x leche QUE CONTIENE la jarra" seguiría ganando la jarra.

De modo que nuestro problema sigue ahí.
Pero vamos a solucionarlo en seguida:
Para empezar descomentamos la línea que le otorga prioritario al objeto lechedelajarra:
object lechedelajarra "leche de la jarra" habitacion
with name 'leche',
adjectives 'jarra' 'blanca',
prioritario 3,
description "Leche blanca, la jarra está llena de leche.",
has female;

De modo que a partir de ahora en caso de empate siempre ganará la leche sobre la jarra.
Si el objeto tiene la propiedad prioritario, al elegir el objeto ganador, en caso de empate éste recibirá unos puntos extra, por enchufe.
Según vemos en el código, en lugar de un punto por coincidencia, recibiría el valor de su propiedad prioritario más otros dos, un total de 5 puntos.
La función ChooseObjects se encarga de eso: de adjudicar puntos entre todos los objetos en base a las coincidencias de su vocabulario con lo que ha escrito el jugador para determinar cuál de ellos es el ganador.

Sólo con esto, la leche ganaría siempre con sólo cumplir que el jugador haya escrito "leche", y tampoco es lo que queremos, pues sería elegida al escribir "jarra de leche".

Vamos a plantear una regla sencilla tal que, si el jugador escribe jarra antes que leche, significará que se refiere a la jarra de leche; y si en cambio escribe leche antes que jarra, se referirá a la leche de la jarra.

Añadiremos este código detrás de la función Initialise de nuestro listado (ojo, sólo funciona para compilar en Glulx, al final incluiré las pequeñas modificaciones para que compile en Máquina-Z):

[BeforeParsing i j thisword thislength x exceso; !
    variable1=0;
    for (i=parse-->0,j=1:j<=i:j++) !ATENCIÓN; en Zcode es "parse->1", en Glulx es "parse-->0"
           !!!parse-->0 devuelve el número de palabras escritas,num_words = parse-->0;
    {!mnfo
        thisword = WordAddress(j);
        thislength = WordLength(j);
         if ( thisword -> 0 >= 'a' && thisword -> 0 <= 'z' )
            { !bucle
                  if(thisword->0=='j' && thisword->1=='a' && thisword->2=='r' && thisword->3=='r' && thisword->4=='a'){if(variable1==0)variable1=1;}
if(thisword->0=='l' && thisword->1=='e' && thisword->2=='c' && thisword->3=='h' && thisword->4=='e'){if(variable1==1)variable1=2;else variable1=3;}

            } !blucle
    }!mnfo
!Resumen:
!Si el jugador ha escrito jarra antes que leche, variable1 valdrá 2
!Si el jugador ha escrito leche antes que jarra, variable1 valdrá 3
!Si el jugador ha escrito jarra, pero no leche, variable1 valdrá 1
];

Ahora nos vamos a la función Chooseobjects, y añadimos una línea extra dejándola así:
[ChooseObjects obj code;
   if(code==2){
        if(obj has nombreusado){
                if(obj==jarradeleche && variable1==2)return (20); !
                if(obj provides prioritario){
                      return (obj.prioritario+2);
                      }
                     
                    return 1;
                        }
            return 0;
        }
if (code<2) { if (obj has scenery || obj has static) return 2; rfalse; } !
  if (action_to_be==##Eat && obj has edible) return 3;
  if (obj hasnt scenery || obj hasnt static) return 2;
  return 1;
];
Hemos introducido esta línea:
if(obj==jarradeleche && variable1==2)return (20);

Y lo que estamos haciendo, es decirle a la función que otorga puntuaciones que si el jugador ha escrito "jarra" y "leche", y además ha escrito "jarra" antes que "leche", que le dé 20 puntazos a la jarra ¡toma ya! ¡A ver qué otro objeto puede ganar ahora a la jarra por mucho prioritario que tenga!
No hace falta hacer lo mismo con la leche, pues recordemos que al haberle dado la propiedad prioritario, la leche ya recibirá una puntuación extra (menor, pero suficiente para ganar a la jarra) caso de que la premisa anterior no se cumpla.
A continuación compilamos y crucemos los dedos (aún no sé si funcionará):

>x jarra de leche
Es una jarra que contiene leche
>x leche de jarra
Leche blanca, la jarra está llena de leche.


¡¡¡Toma, toma y toma!!!

Por si acaso, comprobamos que la jarra no recibe los 20 puntacos cuando no especificamos que es la jarra de leche:
>x jarra
¿Cuál concretamente, la jarra con leche o la jarra con agua?

Y funciona correctamente.

A todo esto, igual alguien se está preguntando quién diablos va a utilizar "leche de la jarra" para referirse a la leche. Como esto es un ejercicio de desambiguación, vamos a imaginar que, además de la jarra con leche y la jarra con agua, existe un vaso con leche. Entonces, al escribir simplemente "leche", recibiríamos la pregunta ¿Cuál concretamente, la leche de la jarra o la leche del vaso? y de ahí que pueda ser importante que el parser diferencie la leche de la jarra de la leche del vaso, de la jarra de leche, y del vaso de leche.

Éste es el código definitivo:

global variable1 =0;
Constant Story "desambiguación";
Constant ADMITIR_COMANDO_SALIDAS;
Replace ChooseObjects;
#Include "Parser";
!! ATENCIÓN. USAR VALORES DE prioritario entre 1 y 7
[ChooseObjects obj code;
   if(code==2){
        if(obj has nombreusado){
                if(obj==jarradeleche && variable1==2)return (20); !
                if(obj provides prioritario){
                      return (obj.prioritario+2);
                      }
                     
                    return 1;
                        }
            return 0;
        }
if (code<2) { if (obj has scenery || obj has static) return 2; rfalse; } !
  if (action_to_be==##Eat && obj has edible) return 3;
  if (obj hasnt scenery || obj hasnt static) return 2;
  return 1;
];

#Include "Verblib";
[ Initialise;
location=habitacion;

rtrue;
];

[BeforeParsing i j thisword thislength x exceso; !
    variable1=0;
    for (i=parse-->0,j=1:j<=i:j++) !ATENCIÓN; en Zcode es "parse->1", en Glulx es "parse-->0"
           !!!parse-->0 devuelve el número de palabras escritas,num_words = parse-->0;
    {!mnfo
        thisword = WordAddress(j);
        thislength = WordLength(j);
         if ( thisword -> 0 >= 'a' && thisword -> 0 <= 'z' )
            { !bucle
                  if(thisword->0=='j' && thisword->1=='a' && thisword->2=='r' && thisword->3=='r' && thisword->4=='a'){if(variable1==0)variable1=1;}
if(thisword->0=='l' && thisword->1=='e' && thisword->2=='c' && thisword->3=='h' && thisword->4=='e'){if(variable1==1)variable1=2;else variable1=3;}

            } !blucle
    }!mnfo
!Resumen:
!Si el jugador ha escrito jarra antes que leche, variable1 valdrá 2
!Si el jugador ha escrito leche antes que jarra, variable1 valdrá 3
!Si el jugador ha escrito jarra, pero no leche, variable1 valdrá 1
];

!###########################################
object limbo "limbo"
with
! Esto no sirve para nada, es para evitar un error si no declaramos
! al menos una vez la propiedad prioritario
prioritario 0,
;

object habitacion "habitacion"
with
description "...",

has light;

object jarradeleche "jarra con leche" habitacion
with name 'jarra',
adjectives 'leche' 'con' 'que' 'contiene',
description "Es una jarra que contiene leche",
!prioritario 2,
has female transparent;

object jarradeagua "jarra con agua" habitacion
with name 'jarra',
adjectives 'agua' 'con' 'que' 'contiene',
description "Es una jarra que contiene agua",
!prioritario 2,
has female transparent;

object lechedelajarra "leche de la jarra" jarradeleche
with name 'leche',
adjectives 'jarra' 'blanca',
prioritario 3,
description "Leche blanca, la jarra está llena de leche.",
has female;

!###########################################
! Procedemos a reemplazar el Parsenoun de la librería por el código de la
! librería Intnombre que hemos descargado. Con esto conseguimos que los
! adjetivos puntúen previa detección de un nombre.
Replace ParseNoun;
Include "IntnombreINFSP.h";
#Include "SpanishG";

Pero no cantemos victoria. Este método, además de ser un tanto chusco, no es el correcto para este caso, ya que en cuanto intentemos "sacar la leche de la jarra de la leche" veremos cómo se nos cae todo el tinglao.
Por tanto no es correcto para objetos que puedan aparecer combinados dentro de la misma orden.
Ya avisé que iba a marear la perdiz.

Pero sí que existe una solución que funciona y realizada de forma más limpia, que explicaré en el segundo capítulo de desambiguación, con un ejemplo que apareció en los foros del CAAD:
¡¡¡¡LA CAJA DE CERILLAS!!!!


Por último pondré una par de ejemplos donde el uso de la propiedad prioritario es bastante interesante:
Tenemos dos PSIS, uno se llama Jose, y el otro Jose Luís.
object jose "Jose"
with name 'jose',
prioritario 2,
has animate proper;

object joseluis "Jose Luís"
with name 'jose' 'luis',
has animate proper;

De esta forma, cuando se encuentren ambos presentes, al escribir "Jose" detectaremos a Jose, pues al tener prioritario, es el "enchufado" por encima de Jose Luís. Y para Jose Luís deberemos escribir "Jose Luís", o "Luís".
Por otro lado, cuando sólo esté presente Jose Luís, podremos llamarle "Jose" a secas tranquilamente.

Otro ejemplo menos rebuscado es un objeto "rama suelta" con name 'rama', y un objeto "rama del árbol" con name 'rama' 'ramas' y adjectives 'arbol', que a diferencia de la primera es una rama que aún permanece unida a su árbol de origen.
Le ponemos la propiedad prioritario a la rama suelta, y cuando estén ambas presentes, al escribir ex rama, examinaremos la rama suelta. ¿Que queremos examinar la rama que pertenece al árbol? pues escribimos ex rama del árbol.
Si la rama suelta no está presente podemos escribir ex rama y el parser detectará la del árbol, pues no hay más.
También sería interesante que la rama del árbol dispusiera en su propiedad adjectives de vocabulario como 'otra' 'otras' y 'mas', por si el jugador quiere arrancar otra rama, o arrancar más ramas.

Si no deseamos intervenir y que sea el parser el que lance la pregunta de desambiguación, entonces no usaremos la propiedad prioritario, pero nos aseguraremos de que la rama suelta tenga en adjectives 'suelta' 'cortada' o 'arrancada', para disponer de vocabulario exclusivo que la diferencie de la otra.

Habiendo parcheado el modo de parseado por defecto que daba puntuación  a los adjetivos al margen de los nombres, con la librería Intnombre.h, la propiedad adjectives se abre para usos más allá de los adjetivos. Así podemos incluir palabras variopintas en la propiedad adjectives:
object corona "corona del Rey Cucufato"
with name 'corona',
adjectives 'que' 'fue' 'pertenecio' 'al' 'rey' 'cucufato' 'vieja' 'antigua' 'propiedad',
description "Vieja corona que perteneció al rey Cucufato.",
has female;

Notas importantes:
* Para que el BeforeParsing funcione en Máquina-Z hay que cambiar esto:
for (i=parse-->0,j=1:j<=i:j++) !ATENCIÓN; en Zcode es "parse->1", en Glulx es "parse-->0"
por esto otro:
for (i=parse->1,j=1:j<=i:j++) !ATENCIÓN; en Zcode es "parse->1", en Glulx es "parse-->0"

* Si estamos programando con InformATE en lugar de con InfSP6, la función Chooseobjects cambia, porque para empezar, se llama EligeObjetos, quedando así:
[ EligeObjetos obj codigo prio;   
    prio=ElegirObjetos(obj,codigo);
    if (codigo>=2)
    {
        if (obj has nombreusado){
         if (bandera_todo_vale==0) prio=prio+10;
    if(obj==jarradeleche && variable1==2)return (20); !!!!!     
    if(obj provides prioritario)prio=prio+obj.prioritario;  
    }
    if ((obj == jugador)||((obj has escenario)&&(obj notin brujula)))
      prio=prio-10;
    }   
    return prio;
];