Historial de desarrollo en 2012 del proyecto Asalto y castigo en Forth

Descripción del contenido de la página

Historial de desarrollo durante 2012 del proyecto de la versión en Forth del juego conversacional Asalto y castigo.

Etiquetas:

2012-01-01

Primera versión (A-03) compilable también por Gforth. El principal problema era que en Gforth :NONAME deja en la pila (como por otra parte está especificado en el estándar ANS Forth) sus datos de control de la definición, en este caso cuatro elementos. Había que sortearlos para que las palabras que hacían uso de :NONAME tuvieran en la parte superior de la pila la dirección de ejecución de la nueva palabra, como ocurre al compilar en SP- Forth y lina.

Esto afectaba a las palabras de definición de atributos de entes, de sus descripciones, de acciones y de tramas de escenario, como se ve:

: :attributes  ( a -- )  \ Inicia la creación de una palabra sin nombre que definirá las propiedades de un ente
  :noname
  [gforth?]  [IF]  5 roll 5 roll  [THEN]
  (:attributes)  \ Crear la palabra y hacer las operaciones preliminares
  postpone [:attributes]  \ Compilar la palabra [:ATTRIBUTES] en la palabra creada, para que se ejecute cuando sea llamada
  \ lina necesita guardar una copia del puntero de la pila tras crear una palabra
  \ ( lo mismo ocurre después con las definiciones de :DESCRIPTION , :LOCATION_PLOT y :ACTION ):
  [lina?]  [IF]  !CSP  [THEN]
  ;

Además, en Gforth no es posible definir un simple alias para ; así:

' ; alias ;attributes immediate

Es necesario esto:

: ;attributes postpone ;  ;  immediate

También ha habido que definir la palabra (ALIAS) para crear un alias a partir de una cadena en la pila, no en el flujo de entrada. Con ella el código que crea los sinónimos de las palabras del juego no ha necesitado adaptación.

Primeras pruebas para definir en Gforth un intérprete exterior propio que igonore las palabras no reconocidas. Para lina no fue difícil:

: evaluate_command  ( a u -- )  \ Analiza el comando, ejecutando las palabras reconocidas que contenga
  save set-src
  begin   (word) ?dup
  while   present ?dup  if  >cfa execute  then
  repeat  drop
  restore
  ;

Pero Gforth es más complejo, tiene muchas más palabras y más «capas». Como es habitual en Forth, la documentación solo explica las palabras externas útiles para el programador; para comprender las palabras internas del propio sistema hay que estudiar las fuentes.

2012-01-02

Solucionado. Intérprete para Gforth:

: (evaluate_command)  ( -- )  \ Analiza la fuente actual, ejecutando las palabras reconocidas que contenga
  begin   parse-name ?dup
  while   find-name ?dup  if  name>int execute  then
  repeat  drop
  ;
: evaluate_command  ( a u -- )  \ Analiza el comando, ejecutando las palabras reconocidas que contenga
  ['] (evaluate_command) execute-parsing
  ;

2012-01-30

Nueva versión del código de cálculo de artículos y seudoartículos, que extrañamente no funcionaba bien en Gforth (e igual de extrañamente funcionaba bien en lina, a pesar de que la longitud de las cadenas es 10 octetos, pues su longitud debe incluirse, y no 9 como se asume en los cálculos).

El código antiguo es el siguiente:

create 'articles  \ Tabla de artículos
  \ Indefinidos:
  s" un       " s,
  s" una      " s,
  s" unos     " s,
  s" unas     " s,
  \ Definidos:
  s" el       " s,
  s" la       " s,
  s" los      " s,
  s" las      " s,
  \ Posesivos:
  s" tu       " s,
  s" tu       " s,
  s" tus      " s,
  s" tus      " s,
  \ Adjetivos que se tratan como «artículos negativos»:
  s" ningXn   " s,  \ La «X» evita el problema del número de caracteres en UTF-8 y será sustituida después por «ú»
  s" ninguna  " s,
  s" ningunos " s,
  s" ningunas " s,
  \ Adjetivos que se tratan como «artículos distantes»:
  s" ese      " s,
  s" esa      " s,
  s" esos     " s,
  s" esas     " s,
  \ Adjetivos que se tratan como «artículos cercanos»:
  s" este     " s,
  s" esta     " s,
  s" estos    " s,
  s" estas    " s,
9 constant /article  \ Longitud máxima de un artículo en la tabla, con sus espacios finales
1 /article * constant /article_gender_set  \ Separación entre cada grupo según el género (masculino y femenino)
2 /article * constant /article_number_set  \ Separación entre cada grupo según el número (singular y plural)
4 /article * constant /article_type_set  \ Separación entre cada grupo según el tipo (definidos, indefinidos, posesivos y negativos)

: article_number>  ( a -- u )
  \ Devuelve un desplazamiento en la tabla de artículos
  \ según el número gramatical del ente.
  has_plural_name? /article_number_set and
  ;
: article_gender>  ( a -- u )
  \ Devuelve un desplazamiento en la tabla de artículos
  \ según el género gramatical del ente.
  has_feminine_name? /article_gender_set and
  ;
: article_gender+number>  ( a -- u )
  \ Devuelve un desplazamiento en la tabla de artículos
  \ según el género gramatical y el número del ente.
  dup article_gender>
  swap article_number> +
  ;
: definite_article>  ( a -- 0 | 1 )
  \ Devuelve el desplazamiento (en número de grupos)
  \ para apuntar a los artículos definidos de la tabla,
  \ si el ente indicado necesita uno.
  dup has_definite_article?  \ Si el ente necesita siempre artículo definido
  swap is_known? or abs  \ O bien si el ente es ya conocido por el protagonista
  ;
: possesive_article>  ( a -- 0 | 2 )
  \ Devuelve el desplazamiento (en número de grupos)
  \ para apuntar a los artículos posesivos de la tabla,
  \ si el ente indicado necesita uno.
  is_owned? 2 and
  ;
: negative_articles>  ( -- u )
  \ Devuelve el desplazamiento (en número de caracteres)
  \ para apuntar a los «artículos negativos» de la tabla.
  /article_type_set 3 *
  ;
: undefined_articles>  ( -- u )
  \ Devuelve el desplazamiento (en número de caracteres)
  \ para apuntar a los artículos indefinidos de la tabla.
  0
  ;
: definite_articles>  ( -- u )
  \ Devuelve el desplazamiento (en número de caracteres)
  \ para apuntar a los artículos definidos de la tabla.
  /article_type_set
  ;
: distant_articles>  ( -- u )
  \ Devuelve el desplazamiento (en número de caracteres)
  \ para apuntar a los «artículos distantes» de la tabla.
  /article_type_set 4 *
  ;
: not_distant_articles>  ( -- u )
  \ Devuelve el desplazamiento (en número de caracteres)
  \ para apuntar a los «artículos cercanos» de la tabla.
  /article_type_set 5 *
  ;
: article_type  ( a -- u )
  \ Devuelve un desplazamiento en la tabla de artículos
  \ según el ente requiera un artículo definido, indefinido o posesivo.
  dup definite_article>  swap possesive_article>  max
  /article_type_set *
  ;
: >article  ( u -- a1 u1 )
  \ Devuelve un artículo de la tabla de artículos
  \ a partir de su índice.
  'articles + /article -trailing
  ;

La nueva versión utiliza una tabla de cadenas de longitud variable, apuntada por una segunda tabla con sus direcciones. Esto unifica y simplifica los cálculos.

: hs,  ( a u -- a1 )
  \ Compila una cadena en el diccionario
  \ y devuelve su dirección
  here rot rot s,
  ;

  \ Adjetivos que se tratan como «artículos cercanos»:
  s" este" hs, s" esta" hs, s" estos" hs, s" estas" hs,
  \ Adjetivos que se tratan como «artículos distantes»:
  s" ese" hs, s" esa" hs, s" esos" hs, s" esas" hs,
  \ Adjetivos que se tratan como «artículos negativos»:
  \ (la «X» evita el problema de UTF-8; después es sustituida por «ú»)
  s" ningXn" hs, s" ninguna" hs, s" ningunos" hs, s" ningunas" hs,
  \ Artículos posesivos:
  s" tu" hs, s" tu" hs, s" tus" hs, s" tus" hs,
  \ Artículos definidos:
  s" el" hs, s" la" hs, s" los" hs, s" las" hs,
  \ Artículos indefinidos:
  s" un" hs, s" una" hs, s" unos" hs, s" unas" hs,

create 'articles  \ Tabla índice de los artículos
  \ Compilar las direcciones de los artículos:
  , , , ,  \ Indefinidos
  , , , ,  \ Definidos
  , , , ,  \ Posesivos
  , , , ,  \ «Negativos»
  , , , ,  \ «Distantes»
  , , , ,  \ «Cercanos»

\ Separaciones entre artículos en la tabla índice (por tanto en celdas)
cell constant /article_gender_set  \ De femenino a masculino
2 cells constant /article_number_set  \ De plural a singular
4 cells constant /article_type_set  \ Entre grupos de diferente tipo

: article_number>  ( a -- u )
  \ Devuelve un desplazamiento en la tabla de artículos
  \ según el número gramatical del ente.
  has_singular_name? /article_number_set and
  ;
: article_gender>  ( a -- u )
  \ Devuelve un desplazamiento en la tabla de artículos
  \ según el género gramatical del ente.
  has_masculine_name? /article_gender_set and
  ;
: article_gender+number>  ( a -- u )
  \ Devuelve un desplazamiento en la tabla de artículos
  \ según el género gramatical y el número del ente.
  dup article_gender>
  swap article_number> +
  ;
: definite_article>  ( a -- u )
  \ Devuelve el desplazamiento en la tabla de artículos
  \ para apuntar a los artículos definidos
  \ si el ente indicado necesita uno.
  dup has_definite_article?  \ Si el ente necesita siempre artículo definido
  swap is_known? or  \ O bien si el ente es ya conocido por el protagonista
  abs  \ Un grupo (pues los definidos son el segundo)
  /article_type_set *
  ;
: possesive_article>  ( a -- u )
  \ Devuelve el desplazamiento en la tabla de artículos
  \ para apuntar a los artículos posesivos
  \ si el ente indicado necesita uno.
  is_owned? 2 and  \ Dos grupos (pues los posesivos son el tercero)
  /article_type_set *
  ;
: negative_articles>  ( -- u )
  \ Devuelve el desplazamiento en la tabla de artículos
  \ para apuntar a los «artículos negativos».
  3 /article_type_set *  \ Tres grupos (pues los negativos son el cuarto)
  ;
: undefined_articles>  ( -- u )
  \ Devuelve el desplazamiento en la tabla de artículos
  \ para apuntar a los artículos indefinidos.
  0  \ Desplazamiento cero, pues los indefinidos son el primer grupo.
  ;
: definite_articles>  ( -- u )
  \ Devuelve el desplazamiento en la tabla de artículos
  \ para apuntar a los artículos definidos.
  /article_type_set  \ Un grupo, pues los definidos son el segundo
  ;
: distant_articles>  ( -- u )
  \ Devuelve el desplazamiento en la tabla de artículos
  \ para apuntar a los «artículos distantes».
  4 /article_type_set *  \ Cuatro grupos, pues los «distantes» son el quinto
  ;
: not_distant_articles>  ( -- u )
  \ Devuelve el desplazamiento en la tabla de artículos
  \ para apuntar a los «artículos cercanos».
  5 /article_type_set *  \ Cinco grupos, pues los «cercanos» son el sexto
  ;
: article_type  ( a -- u )
  \ Devuelve un desplazamiento en la tabla de artículos
  \ según el ente requiera un artículo definido, indefinido o posesivo.
  dup definite_article>  swap possesive_article>  max
  ;
: >article  ( u -- a1 u1 )
  \ Devuelve un artículo de la tabla de artículos
  \ a partir de su índice.
  'articles + @ count
  ;

Ahora es en lina que el código no funciona bien. Parece que tiene relación con COUNT, a pesar de el funcionamiento de esta palabra en lina es estándar.

2012-02-01

Reorganizado el código que se ocupa de los términos y entes ambiguos en el vocabulario del juego: los entes ambiguos se resuelven ahora todos con palabras externas al vocabulario del juego, mientras que los términos ambiguos se resuelven en la propia palabra.

Implementado el vocabulario del jugador como una tabla en Gforth, que es la manera de crear un vocabulario sensible a mayúsculas (necesario para distinguir las palabras del juego de los comandos del sistema). La implementación estaba hecha por error para el vocabulario principal del juego.

Corregidos cinco errores en el sistema de grabación y restauración de partidas, que aún no estaba probado: Faltaba crear un alias de S" en el vocabulario correspondiente; la palabra creada en el fichero no debía ser restore_entity sino load_entity; para restaurar no había que llamar a INCLUDED directamente, sino a read_game_file; string>file creaba «s""» en lugar de «s" "» para guardar las cadenas vacías; se grababa la dirección de la ficha del ente (que es variable entre sesiones de juego) en lugar de su número ordinal; y por último el indicador que la palabra estándar RESTORE-INPUT deja en la pila no era tratado.

Con estos cambios ya se graba y recupera bien la partida, pero tras la carga se producen errores con ciertos comandos, debidos a un simple fallo de diseño fundamental: Las referencias a los entes son las direcciones de sus fichas en memoria, pero estas direcciones variarán entre las sesiones de juego. Por tanto hace falta usar los números de ente como referencia, y convertirlos en direcciones en el momento.

El diseño actual es muy flexible y prácticamente ya hace lo que necesitamos:

defer 'entities  \ Dirección de los entes; vector que después será redirigido a la palabra real
0 value #entities  \ Contador de entes, que se actualizará según se vayan creando

: #>entity  ( u -- a )
  \ Devuelve la dirección de la ficha de un ente a partir de su número ordinal
  \ (el número del primer ente es el cero).
  /entity * 'entities +
  ;
: entity>#  ( a -- u )
  \ Devuelve el número ordinal de un ente (el primero es el cero)
  \ a partir de la dirección de su ficha .
  'entities - /entity /
  ;
: entity:  ( "name" -- )
  \ Crea un nuevo identificador de ente, que devolverá la dirección de su ficha
  create
    #entities ,  \ Guardar la cuenta en el cuerpo de la palabra recién creada
    #entities 1+ to #entities  \ Actualizar el contador
  does>  ( pfa -- a )
    @ #>entity  \ El identificador devolverá la dirección de su ficha
  ;

No es difícil hacer unos retoques en las palabras que graban y restauran la partida, para que en el fichero de la partida se graben los números ordinales de los entes en lugar de las direcciones de sus fichas.

No obstante hay otro inconveniente: Algunos campos de las fichas de los entes guardan identificadores de ejecución, como los de las palabras de descripción de los entes. Esas direcciones se calculan y almacenan en tiempo de compilación al crear los entes por primera vez. ¿Cómo actualizarlas en las fichas al restaurar una partida? Eso sería muy complejo. Habría que usar un valor intermedio, como el nombre de la palabra, pero no siempre es posible, pues las palabras de descripción y de trama asociadas a cada ente no tienen nombre, solo un identificador de ejecución.

La solución es no salvar ni restaurar los campos de datos que contienen identificadores de ejecución de palabras. En realidad no hay motivo para que las palabras de descripción o de trama cambien durante una partida, pues la misma palabra puede variar su comportamiento según sea necesario. Está el caso aparte de los indicadores de ejecución de palabras de error asociadas al ente, que también representan a los propios códigos de error, y que sí tendría más utilidad poder cambiar durante una partida. Pero dada la dificultad de salvar y restaurar este tipo de valores, sería más fácil hacer palabras de error específicas para los casos especiales en que haya que cambiar el tipo de error durante el juego.

Conclusión: los campos que contienen otros entes se salvan como el número ordinal de estos, y el proceso inverso se hace durante la restauración; los campos que contienen identificadores de ejecución de palabras no se salvan y por tanto no pueden modificarse durante el juego.

Con todo ello queda funcionando (al menos en Gforth) el sistema de grabación y restauración de partidas.

2012-02-02

Primer borrador para implementar las (seudo)preposiciones que permitirán marcar los complementos. Hasta ahora solo se aceptaban dos complementos, que se guardaban en las variables main_complement y other_complement:

: (complement!)  ( a -- )
  \ Almacena un complemento.
  \ a = Identificador de ente
  [debug_parsing] ?halto" En (COMPLEMENT!)"  \ Depuración!!!
  other_complement @ second?  \ ¿Había ya un complemento secundario?
  if  too_many_complements_error# throw  \ Sí, error
  else  other_complement !  \ No, guardarlo
  then
  ;
: complement!  ( a -- )
  \ Comprueba y almacena un complemento.
  \ a = Identificador de ente
  [debug_parsing] ?halto" En COMPLEMENT!"  \ Depuración!!!
  main_complement @ second?  \ ¿Había ya complemento directo?
  if  (complement!)
  else  main_complement !
  then
  ;
: action|complement!  ( a1 a2 -- )
  \ Comprueba y almacena un complemento o una acción,
  \ ambos posibles significados de la misma palabra.
  \ a1 = Dirección de ejecución de la palabra de la acción
  \ a2 = Identificador de ente
  action @  \ ¿Había ya una acción reconocida?
  if  nip complement!  \ Sí: lo tomamos como complemento
  else  drop action!  \ No: lo tomamos como acción
  then
  ;

2012-02-03

Para evitar que redundancias como «norte norte» den error por complemento inesperado, se crea un nuevo filtro (y su error correspondiente) que permite discriminar esos casos al inicio de las acciones:

: main_complement{this_only}  ( a -- )
  \ Provoca un error si hay complemento principal y no es el indicado.
  \ a = Ente que será aceptado como complemento
  main_complement @ dup rot <> swap 0<> and
  not_allowed_main_complement_error# and throw
  ;

2012-02-04

Simplificación del complejo borrador del sistema de complementos (seudo)preposicionales: Implementación de la preposición «con» solo para herramienta, no compañía. Eliminación de las demás preposiciones en favor de un complemento secundario genérico (que representará el objeto indirecto, el origen o el destino de un movimiento). En particular la implementación de «a» era un reto importante, porque su significado (objeto directo de personas, objeto indirecto, lugar, destino...) depende de múltiples condiciones.

Implementada la primera versión de los errores y filtros relacionados con el complemento instrumental.

Corregido un pequeño fallo: no se elminaban los espacios laterales del comando. Por ello cualquier comando que constara solo de espacios era interpretado y provocaba error por falta de verbo.

2012-02-05

Al intentar crear una imagen del sistema en Gforth, que es la forma en que se distribuirá el programa, descubrí que no era posible. Gforth incluye para esta tarea el programa gforthmi, escrito en Bash, que carga el código del juego dos veces consecutivas para crear una imagen diferente en cada ocasión y compararlas después. Pero tras cada carga del código del juego Gforth se queda en estado normal: esperando un comando. Había que salir con Ctrl+C o con la palabra bye. Pero aunque se hiciera así gforthmi no terminaba su tarea.

Tras podar el código poco a poco en busca de la causa de este comportamiento, al final descubrí que el fallo estaba en esta línea de la versión para Gforth del fichero config.fs de la librería Forth Foundation Library, previa a la definición de la palabra arg@:

0 argc !            \ tell gforth not to process any more arguments

Tras anularla, gforthmi funcionó bien:

gforthmi ayc.fi ayc.fs

Para arrancar el juego a partir de su imagen, basta arrancar Gforth de la siguiente manera, ejecutando la palabra de arranque, que de momento en nuestro caso es go:

gforth --image ayc.fi -e go

No obstante el programa no funcionó bien, porque las imágenes solo contienen el espacio del diccionario, no los espacios de memoria reservados con ALLOCATE, como es el caso del almacén circular de texto utilizado. Así pues, todas las cadenas definidas y manipuladas durante la compilación tenían su dirección en una zona de memoria inaccesible en el momento de la ejecución. Para solucionar este problema hay que hacer que el almacen de cadenas de texto, una vez cargado, utilice el espacio del diccionario en vez:

\ Creamos el almacén circular de cadenas en el diccionario.

free_csb  \ Borrar el almacén predeterminado
2048 here  \ Longitud y dirección del nuevo almacén
over >bytes/csb allot  \ Hacer espacio en el diccionario para el almacén
csb_set  \ Inicializar el almacén

Tras implementar la gestión de los complementos secundario e instrumental, empiezo a añadir nuevos filtros necesarios para las acciones, y sus errores correspondientes.

Nueva, segunda versión de una comprobación:

: can_be_looked_at?  ( a -- ff )
  \ ¿El ente puede ser mirado?
  [ gforth? 0= ]  [IF]  \ Primera versión
    dup my_location =  \ ¿Es la localización del protagonista?
    over is_direction? or  \ ¿O es un ente dirección?
    over exits% = or  \ ¿O es el ente "salidas"?
    swap is_accessible? or  \ ¿O está accesible?
  [ELSE]  \ Segunda versión, menos elegante pero más rápida y legible
    { entity }  \ Variable local creada con el parámetro de la pila
    true  case
      entity my_location =  of  true  endof \ ¿Es la localización del protagonista?
      entity is_direction?  of  true  endof \ ¿Es un ente dirección?
      entity is_accessible?  of  true  endof  \ ¿Está accesible?
      entity exits% =  of  true  endof  \ ¿Es el ente "salidas"?
      false swap
    endcase
  [THEN]
  ;

2012-02-06

Pequeños retoques diversos.

2012-02-07

Código pendiente en las acciones de abrir y cerrar, aplicables a la puerta y al candado.

2012-02-17

Eliminado el campo ~is_owned?, que indicaba si un objeto pertenecía al protagonista, por otro más genérico ~owner para indicar el dueño. Esto permitirá implementar el reconocimiento de los genitivos y la distinción entre varios usos de la preposición «de». Todo el código existente se adapta fácilmente cambiando estas palabras de la interfaz de campos:

: is_owned?  ( a -- ff )  ~is_owned? @  ;
: is_owned  ( a -- ff )  ~is_owned? on  ;

Por estas:

: owner  ( a1 -- a2 )  ~owner @  ;
: belongs_to_protagonist?  ( a -- ff )  owner protagonist% =  ;
: belongs_to_protagonist  ( a -- ff )  ~owner protagonist% swap !  ;

Cuatro nuevas palabras permiten hacer las asignaciones y comprobaciones:

: owns?  ( a1 a2 -- ff )  owner =  ;
: belongs?  ( a1 a2 -- ff )  swap owns?  ;
: owns  ( a1 a2 -- ff )  ~owner !  ;
: belongs  ( a1 a2 -- ff )  swap owns  ;

Corregido un error en la lógica de cierre de puerta y candado.

2012-02-18

Implementado el sistema básico de pronombres, con recuerdo de los últimos y penúltimos complementos utilizados por el jugador, tanto absolutos como por género y número.

La implementación actual tiene en primer lugar una tabla para «recordar» los complementos:

create last_complement  \ Tabla para últimos complementos usados
\ Son necesarias cinco celdas:
\ una para el último complemento usado
\ y cuatro para cada último complemento usado de cada género y número.
\ El espacio se multiplica por dos
\ para guardar en la segunda mitad los penúltimos complementos.
5 cells 2*  dup constant /last_complements  allot \ Octetos necesarios para toda la tabla

(

La tabla LAST_COMPLEMENT sirve para guardar los
identificadores de entes correspondientes a los últimos
complementos utilizados en los comandos del jugador. De este
modo los pronombres podrán recuperarlos.

La estructura de la tabla LAST_COMPLEMENT es la siguiente,
con desplazamiento indicado en celdas:

Último complemento usado:
  +0 De cualquier género y número.
  +1 Masculino singular.
  +2 Femenino singular.
  +3 Masculino plural.
  +4 Femenino plural.
Penúltimo complemento usado:
  +5 De cualquier género y número.
  +6 Masculino singular.
  +7 Femenino singular.
  +8 Masculino plural.
  +9 Femenino plural.

)

\ Desplazamientos para acceder a los elementos de la tabla:
1 cells constant />masculine_complement  \ Respecto al inicio de tabla
2 cells constant />feminine_complement  \ Respecto al inicio de tabla
0 cells constant />singular_complement  \ Respecto a su género en singular
2 cells constant />plural_complement  \ Respecto a su género en singular
5 cells constant />but_one_complement  \ Respecto a la primera mitad de la tabla
: >masculine  ( a1 -- a2 )  />masculine_complement +  ;
: >feminine  ( a1 -- a2 )  />feminine_complement +  ;
: >singular  ( a -- a )  />singular_complement +  ;
: >plural  ( a -- a )  />plural_complement +  ;
: >but_one  ( a1 -- a2 )  />but_one_complement +  ;

: last_but_one_complement  ( - a )
  \ Devuelve la dirección del penúltimo complemento absoluto,
  \ que es también el inicio de la sección «penúltimos»
  \ de la tabla LAST_COMPLEMENTS .
  last_complement >but_one
  ;
: (>last_complement)  ( a1 a2 -- a3 )
  \ Apunta a la dirección adecuada para un ente
  \ en una sección de la tabla LAST_COMPLEMENT ,
  \ bien «últimos» o «penúltimos».
  \ Nota: Hace falta sumar los desplazamientos de ambos géneros
  \ debido a que ambos son respecto al inicio de la tabla.
  \ El desplazamiento para singular no es necesario,
  \ pues sabemos que es cero, a menos que se cambie la estructura.
  \ a1 = Ente para el que se calcula la dirección
  \ a2 = Dirección de una de las secciones de la tabla
  over has_feminine_name? />feminine_complement and +
  over has_masculine_name? />masculine_complement and +
  swap has_plural_name? />plural_complement and +
  ;
: >last_complement  ( a1 -- a2 )
  \ Apunta a la dirección adecuada para un ente
  \ en la sección «últimos» de la tabla LAST_COMPLEMENT .
  last_complement (>last_complement)
  ;
: >last_but_one_complement  ( a1 -- a2 )
  \ Apunta a la dirección adecuada para un ente
  \ en la sección «penúltimos» de la tabla LAST_COMPLEMENT .
  last_but_one_complement (>last_complement)
  ;

: erase_last_command_elements  ( -- )
  \ Borra todos los últimos elementos guardados de los comandos.
  last_action off
  last_complement /last_complements erase
  ;
erase_last_command_elements

En segundo lugar, herramientas para guardar en la tabla los complementos hallados durante el análisis del comando:

: >but_one!  ( a -- )
  \ Copia un complemento de la zona «últimos» a la «penúltimos»
  \ de la tabla LAST_COMPLEMENT .
  \ a = Dirección en la zona «últimos» de la tabla LAST_COMPLEMENT .
  dup @ swap >but_one !
  ;
: shift_last_complement  ( a -- )
  \ Copia el último complemento al lugar del penúltimo.
  \ a = Ente que fue encontrado como último complemento.
  >last_complement >but_one!  \ El último del mismo género y número
  last_complement >but_one!  \ El último absoluto
  ;
: new_last_complement  ( a -- )
  \ Guarda un nuevo complemento como el último complemento hallado.
  dup shift_last_complement  \ Copiar último a penúltimo
  dup last_complement !  \ Guardarlo como último absoluto
  dup >last_complement !  \ Guardarlo como último de su género y número
  ;

Y por último, la definición de los pronombres en el vocabulario del juego.

\ Pronombres

(
De momento no se implementan las formas sin tilde
porque obligarían a distinguir sus usos como adjetivos.
)

: éste  last_complement >masculine >singular @ complement!  ;
' éste synonyms{ ése }synonyms
: ésta  last_complement >feminine >singular @ complement!  ;
' ésta synonyms{ ésa }synonyms
: éstos  last_complement >masculine >plural @ complement!  ;
' éstos synonyms{ ésos }synonyms
: éstas  last_complement >feminine >plural @ complement!  ;
' éstas synonyms{ ésas }synonyms
: aquél  last_but_one_complement >masculine >singular @ complement!  ;
: aquélla  last_but_one_complement >feminine >singular @ complement!  ;
: aquéllos  last_but_one_complement >masculine >plural @ complement!  ;
: aquéllas  last_but_one_complement >feminine >plural @ complement!  ;

El siguiente paso será añadir las formas verbales con pronombres enclíticos.

2012-02-20

Implementados, para las acciones existentes, los términos verbales con pronombres enclíticos, como en este caso:

: abrir  ['] do_open action!  ;
' abrir synonyms{  abre abrid abro abra  }synonyms
: abrirlo  abrir éste  ;
' abrirlo synonyms{ ábrelo abridlo ábrolo ábralo }synonyms
: abrirla  abrir ésta  ;
' abrirla synonyms{ ábrela abridla ábrola ábrala }synonyms
: abrirlos  abrir éstos  ;
' abrirlos synonyms{ ábrelos abridlos ábrolos ábralos }synonyms
: abrirlas  abrir éstas  ;
' abrirlas synonyms{ ábrelas abridlas ábrolas ábralas }synonyms

2012-02-20

Nuevo campo ~times_open para contar el número de veces que se abre un ente. Esto permite adaptar el mensaje relativo a la apertura de la puerta.

2012-02-22

Mejora de los mensajes de apertura de la puerta; nuevas variantes.

2012-02-29

Corregido un error en la palabra wrong_yes$: Añadía aleatoriamente al final un mensaje que ya formaba parte del error general mostrado posteriormente.

Ampliación y variación aleatoria de los mensajes originales de despedida de Ambrosio tras las apertura de la cueva.

: ambrosio_byes  ( -- )
  \ Ambrosio se despide cuando se abre la puerta por primera vez.
  s" Ambrosio, alegre, se despide de ti:" narrate
  s{
  s{ s" Tengo" s" Ten" }s s" por seguro" s&
  s{ s" Estoy" s" Estate" }s s" seguro de" s&
  s{ s" Tengo" s" Ten" }s s" la" s& s{ s" seguridad" s" certeza" }s& s" de" s&
  s" No" s{ s" me cabe" s" te quepa" }s& s" duda de" s&
  s" No" s{ s" dudo" s" dudes" }s&
  }s s" que" s&
  s{
  s" nos volveremos a" s{ s" encontrar" s" ver" }s& again$ s?&
  s" volveremos a" s{ s" encontrarnos" s" vernos" }s& again$ s?&
  s" nos" s{ s" encontraremos" s" veremos" }s& again$ s&
  s" nuestros caminos" s{ s" volverán a cruzarse" s" se cruzarán" }s& again$ s&
  }s& period+ speak
  s{ s" Y" s" Pero" }s s" , antes de que puedas" s+
  s{ s" decir algo" s" corresponderle" s" responderle" s" despedirte" s" de él" s?& }s&
  s" , te das cuenta de que" s+ s" Ambrosio" s?&
  s" , misteriosamente," s?+
  s{ s" ha desaparecido" s" se ha marchado" s" se ha ido" s" ya no está" }s&
  period+ narrate
  ;

2012-03-01

Cambios para lograr que la puerta de la cueva sea accesible también desde el otro lado, pero no así el candado, que se supone que está fijo al otro lado de la puerta. Esto permite abrir y cerrar la puerta desde fuera de la cueva y adecuar los mensajes de descripción, con variantes aleatorias como casi siempre:

> mira e

La entrada de la cueva pasa casi desapercibida, aun cuando su puerta no está cerrada, como ahora.

> mira e

La entrada de la cueva está escondida, aun cuando la puerta no está cerrada, como ahora.

> mira e

La entrada de la cueva está bien camuflada, incluso cuando la puerta está abierta.

> mira e

La entrada de la cueva casi no se ve, incluso cuando su puerta no está cerrada.

> cierra puerta

La puerta emite un chirrido mientras la cierras.

> mira e

La entrada de la cueva casi no se ve, especialmente cuando su puerta no está abierta.

> mira e

La entrada de la cueva casi no se ve, sobre todo cuando la puerta no está abierta.

> mira e

La entrada de la cueva pasa casi desapercibida, especialmente cuando la puerta está cerrada, como ahora.

2012-03-17

Palabras para desconectar las salidas de los escenarios, complementarias a las existentes para hacer conexiones. Extracto de ejemplo:

: -->|  ( a1 u -- )
  \ Cierra la salida del ente a1 indicada por el desplazamiento u.
  \ a1 = Ente origen de la conexión
  \ u = Desplazamiento del campo de dirección a usar en a1
  + no_exit swap !
  ;

: n-->|  ( a1 -- )
  \ Desconecta la salida norte del ente a1.
  north_exit> -->|
  ;
: s-->|  ( a1 -- )
  \ Desconecta la salida sur del ente a1.
  south_exit> -->|
  ;

: n|<-->|  ( a1 a2 -- )
  \ Desconecta la salida norte del ente a1 con el ente a2 (y al contrario).
  s-->|  n-->|
  ;
: s|<-->|  ( a1 a2 -- )
  \ Desconecta la salida sur del ente a1 con el ente a2 (y al contrario).
  n-->|  s-->|
  ;

Implementadas las acciones de entrar y salir (de momento solo por direcciones estándar de escenario). Implementado el manejo de conexiones entre escenarios al abrir o cerrar la puerta de la cueva.

Nuevos textos variables en las descripciones de varios escenarios.

2012-03-19

Sustitución de los accesos explícitos al contenido de algunos campos (ejemplo: ~location @) por las palabras ya creadas para ello (ejemplo: location).

Conversión del sistema de tramas de escenario en tramas de entrada a los escenarios, para completarlo con un sistema paralelo de tramas de salida de los escenarios.

Ampliación de las palabras de asignación de nombres de entes, para poder especificar también las de género gramatical masculino. Esto es necesario para implementar el cambio de nombre cuando el jugador usa un sinónimo de diferente género o número, como en el nuevo ente creado para la hierba bajo la puerta:

: hierba  s" hierba" grass% fname! grass% complement!  ;
: hierbas  s" hierbas" grass% fnames! grass% complement!  ;
: hierbajo  s" hierbajo" grass% mname! grass% complement!  ;
: hierbajos  s" hierbajos" grass% mnames! grass% complement!  ;

Lo cual permite adaptar los mensajes de descripción (verbos, nombres y adjetivos) al nombre utilizado:

grass% :description
  door% times_open  if
    s" Está" self% verb_number_ending+
    s" aplastad" self% adjective_ending+ s&
    s{ s" en el" s" bajo el" s" a lo largo del" }s& s" recorrido" s&
    s{ s" de la puerta." s" que hizo la puerta al abrirse." }s&
  else
    s" Cubre" self% verb_number_ending+
    s" el suelo junto a la puerta," s&
    s{ s" lo que" s" lo cual" }s&
    s{ s" indica" s" significa" s" delata" }s
    s" que esta" s&
    s{ s" no ha sido abierta en" s" lleva cerrada"
    s" ha permanecido cerrada" s" durante" s?& }s
    s" mucho tiempo." s&
  then  paragraph
  ;description

2012-04-08

Revisión general del código fuente. Corrección de algunas erratas. Cambios de formato en comentarios. Varias pequeñas modificaciones y mejoras.

2012-04-09

Corrección y ajuste de algunos comentarios.

2012-04-19

Versión A-04. Cierro la versión A-03 tras decidir programar solo para Gforth. La compatibilidad con lina y SP- Forth ya no se mantiene desde hace tiempo porque no es práctico: aumenta mucho el trabajo de depuración. En cualquier caso Gforth es el sistema que permite un desarrollo más eficaz. Además tengo la intención de abandonar la arquitectura Intel en favor de ARM, por lo que lina y SP- Forth dejarán de ser opciones viables para programar, mientras que Gforth es más portable y puede ejecutarse en diferentes arquitecturas.

Extractos de la última versión A-03 servirán para un artículo sobre la experiencia de escribir código compatible con varios sistemas Forth.

2012-04-22

Actualizamos el sistema de pausas. La primera versión permitía pausas máximas en segundos acortables mediante la pulsación de una tecla. El código estaba anulado porque no funcionaba en SP- Forth para Linux, tal como se explica en el propio código:

\ Sistema original descartado, que funciona en SP-Forth para Windows

(

En SP-Forth para Linux, la palabra KEY? de ANS Forth
devuelve siempre cero porque aún no está completamente
implementada, mientras que en la versión para Windows sí
funciona correctamente.  Esto impide, en Linux, crear pausas
de duración máxima que puedan ser interrumpidas con una
pulsación de teclas.

Por ello hemos optado por un sistema alternativo.

)

dtm-create deadline  \ Variable para guardar el momento final de las pausas

: no_time_left?  ( -- ff )
  \ ¿Se acabó el tiempo?
  0 time&date  \ Fecha y hora actuales (más cero para los milisegundos)
  deadline dtm-compare  \ Comparar con el momento final (el resultado puede ser: -1, 0, 1)
  1 =  \ ¿Nos hemos pasado?
  ;
: no_key?  ( -- ff )
  \ ¿No hay una tecla pulsada?
  key? 0=
  ;
: seconds_wait ( u -- )
  \ Espera los segundos indicados, o hasta que se pulse una tecla.
  deadline dtm-init  \ Guardar la fecha y hora actuales como límite...
  s>d deadline dti-seconds+  \ ...y sumarle los segundos indicados
  begin  no_time_left? no_key? or  until
  begin  no_time_left? key? or  until
  ;
: narration_break  ( -- )
  \ Hace una pausa en la narración; se usa entre ciertos párrafos.
  1 seconds_wait
  ;
: scene_break  ( -- )
  \ Hace una pausa en la narración; se usa entre ciertos párrafos.
  3 seconds_wait
  ;

El método en uso hasta el momento era una solución de compromiso: no permitía pausas máximas que pudieran acortarse con la pulsación de una tecla, sino solo pausas fijas o indefinidas. En este segundo método estaban ya implementadas las pausas indicadas en el fichero de configuración del juego. Por otra parte, las pausas se expresaban en milisegundos.

\ Segundo sistema, con pausas fijas en milisegundos o indefinidas hasta la pulsación de una tecla

variable indent_pause_prompts?  \ ¿Hay que indentar también los prestos?
: .prompt  ( a u -- )
  \ Imprime un presto.
  indent_pause_prompts? @  if  indent  then  type
  ;
: wait  ( u -- )
  \ Hace una pausa.
  \ u = Milisegundos (o un número negativo para pausa sin fin hasta la pulsación de una tecla)
  dup 0<  if  key 2drop  else  ms  then
  ;
variable narration_break_milliseconds  \ Milisegundos de espera en las pausas de la narración
svariable narration_prompt  \ Guardará el presto usado en las pausas de la narración
: narration_prompt$  ( -- a u )
  \ Devuelve el presto usado en las pausas de la narración.
  narration_prompt count
  ;
: .narration_prompt  ( -- )
  \ Imprime el presto de fin de escena.
  narration_prompt_color narration_prompt$ .prompt
  ;
: (narration_break)  ( n -- )
  \ Alto en la narración: Muestra un presto y hace una pausa .
  \ u = Milisegundos (o un número negativo para hacer una pausa indefinida hasta la pulsación de una tecla)
  trm+save-cursor
  .narration_prompt wait
  trm+erase-line  trm+restore-cursor
  ;
: narration_break  ( -- )
  \ Alto en la narración, si es preciso.
  narration_break_milliseconds @ ?dup
  if  (narration_break)  then
  ;

variable scene_break_milliseconds  \ Milisegundos de espera en las pausas de final de escena
svariable scene_prompt  \ Guardará el presto de cambio de escena
: scene_prompt$  ( -- a u )
  \ Devuelve el presto de cambio de escena.
  scene_prompt count
  ;
: .scene_prompt  ( -- )
  \ Imprime el presto de fin de escena.
  scene_prompt_color scene_prompt$ .prompt
  ;
: (scene_break)  ( n -- )
  \ Final de escena: Muestra un presto y hace una pausa .
  \ n = Milisegundos (o un número negativo para hacer una pausa indefinida hasta la pulsación de una tecla)
  trm+save-cursor
  .scene_prompt wait
  trm+erase-line  trm+restore-cursor
  scene_page? @  if  new_page  then
  ;
: scene_break  ( -- )
  \ Final de escena, si es preciso.
  scene_break_milliseconds @ ?dup
  if  (scene_break)  then
  ;

Con la libertad que da programar solo para Gforth, es posible combinar la configurabilidad del segundo método con las pausas máximas del primero, como se muestra en la nueva versión del código:

\ Tercer sistema, combinación de los anteriores:
\ pausas máximas en segundos y acortables con la pulsación de una tecla,
\ o indefinidas hasta la pulsación de una tecla

variable indent_pause_prompts?  \ ¿Hay que indentar también los prestos?
: .prompt  ( a u -- )
  \ Imprime un presto.
  indent_pause_prompts? @  if  indent  then  type
  ;

dtm-create deadline  \ Variable para guardar el momento final de las pausas
: no_time_left?  ( -- ff )
  \ ¿Se acabó el tiempo?
  0 time&date  \ Fecha y hora actuales (más cero para los milisegundos)
  deadline dtm-compare  \ Comparar con el momento final (el resultado puede ser: -1, 0, 1)
  1 =  \ ¿Nos hemos pasado?
  ;
: no_key?  ( -- ff )
  \ ¿No hay una tecla pulsada?
  key? 0=
  ;
: seconds_wait ( u -- )
  \ Espera los segundos indicados, o hasta que se pulse una tecla.
  deadline dtm-init  \ Guardar la fecha y hora actuales como límite...
  s>d deadline dti-seconds+  \ ...y sumarle los segundos indicados
  begin  no_time_left? no_key? or  until
  begin  no_time_left? key? or  until
  ;
: wait  ( u -- )
  \ Hace una pausa.
  \ u = Segundos (o un número negativo para pausa sin fin hasta la pulsación de una tecla)
  dup 0<  if  key 2drop  else  seconds_wait  then
  ;

variable narration_break_seconds  \ Segundos de espera en las pausas de la narración
svariable narration_prompt  \ Guardará el presto usado en las pausas de la narración
: narration_prompt$  ( -- a u )
  \ Devuelve el presto usado en las pausas de la narración.
  narration_prompt count
  ;
: .narration_prompt  ( -- )
  \ Imprime el presto de fin de escena.
  narration_prompt_color narration_prompt$ .prompt
  ;
: (narration_break)  ( n -- )
  \ Alto en la narración: Muestra un presto y hace una pausa .
  \ u = Segundos (o un número negativo para hacer una pausa indefinida hasta la pulsación de una tecla)
  trm+save-cursor
  .narration_prompt wait
  trm+erase-line  trm+restore-cursor
  ;
: narration_break  ( -- )
  \ Alto en la narración, si es preciso.
  narration_break_seconds @ ?dup
  if  (narration_break)  then
  ;

variable scene_break_seconds  \ Segundos de espera en las pausas de final de escena
svariable scene_prompt  \ Guardará el presto de cambio de escena
: scene_prompt$  ( -- a u )
  \ Devuelve el presto de cambio de escena.
  scene_prompt count
  ;
: .scene_prompt  ( -- )
  \ Imprime el presto de fin de escena.
  scene_prompt_color scene_prompt$ .prompt
  ;
: (scene_break)  ( n -- )
  \ Final de escena: Muestra un presto y hace una pausa .
  \ n = Segundos (o un número negativo para hacer una pausa indefinida hasta la pulsación de una tecla)
  trm+save-cursor
  .scene_prompt wait
  trm+erase-line  trm+restore-cursor
  scene_page? @  if  new_page  then
  ;
: scene_break  ( -- )
  \ Final de escena, si es preciso.
  scene_break_seconds @ ?dup
  if  (scene_break)  then
  ;

Los comandos y variables de configuración relativos a milisegundos son renombrados en todo el programa y en el fichero de configuración para referirse a segundos.

2012-04-24

Ampliación del verbo «escalar» para deducir el objeto omitido, en el escenario del derrumbe; aplicable a otros casos especiales.

2012-04-27

Nuevo sistema para crear campos buleanos en la estructura de la base de datos de entes. Hasta ahora los campos buleanos eran campos ordinarios que ocupaban una celda (32 bitios) y se manipulaban con las palabras habituales. Para simplificar entre otras cosas el código que guarda y restaura las partidas, a partir de ahora los campos buleanos se guardan en un bitio. Esto además ahorra mucho espacio. Para definir estos campos con comodidad, la definición antigua de offset: ha sido sustituida por esto:

variable bit#  \ Contador de máscara de bitio para los campos buleanos.
: bit#_init  1 bit# !  ;
bit#_init
: offset:  (  u1 u2 "name" -- u3 )
  \ Crea un campo en una ficha de datos.
  \ u1 = Desplazamiento del campo a crear respecto al inicio de la ficha
  \ u2 = Espacio (en celdas) necesario para el campo
  \ u3 = Desplazamiento del próximo campo
  create  over , +  bit#_init
  does>  ( a pfa -- a' )  @ +
  ;
: bit#?  ( -- u )  bit# @  dup 0= abort" Too many bits defined in the field"  ;
: bit+  ( -- )  bit# @ 1 lshift bit# !  ;
: bit:  ( u1 "name" -- u1 )
  \ Crea un campo de bitio en el próximo campo de celda.
  \ u1 = Desplazamiento (respecto al inicio de la ficha) del próximo campo de celda que se creará
  \ u2 = Máscara del bitio
  \ a = Dirección de la ficha de datos.
  \ a' = Dirección del campo en que está definido el bitio en la ficha de datos.
  create  bit#? over 2, bit+
  does>  ( a pfa -- u2 a' )  2@ rot +
  ;
: bit@  ( u a -- ff )
  \ Devuelve el contenido de un campo de bitio.
  \ u = Máscara del bitio
  \ a = Dirección del campo
  @ and 0<>
  ;
: bit_on  ( u a -- )
  \ Activa un campo de bitio.
  \ u = Máscara del bitio
  \ a = Dirección del campo
  dup @ rot or swap !
  ;
: bit_off  ( u a -- )
  \ Desactiva un campo de bitio.
  \ u = Máscara del bitio
  \ a = Dirección del campo
  dup @ rot invert and swap !
  ;

Las nuevas palabras permiten crear campos todos los buleanos de la siguiente forma:

\ Campos de bitio utilizados en el próximo campo:
bit: ~has_definite_article?  \ Indicador: ¿el artículo debe ser siempre el artículo definido?
bit: ~has_feminine_name?  \ Indicador: ¿el género gramatical del nombre es femenino?
bit: ~has_no_article?  \ Indicador: ¿el nombre no debe llevar artículo?
bit: ~has_personal_name?  \ Indicador: ¿el nombre del ente es un nombre propio?
bit: ~has_plural_name?  \ Indicador: ¿el nombre es plural?
bit: ~is_animal?  \ Indicador: ¿es animal?
bit: ~is_character?  \ Indicador: ¿es un personaje?
bit: ~is_cloth?  \ Indicador: ¿es una prenda que puede ser puesta y quitada?
bit: ~is_decoration?  \ Indicador: ¿forma parte de la decoración de su localización?
bit: ~is_global_indoor?  \ Indicador ¿es global (común) en los escenarios interiores?
bit: ~is_global_outdoor?  \ Indicador ¿es global (común) en los escenarios al aire libre?
bit: ~is_human?  \ Indicador: ¿es humano?
bit: ~is_light?  \ Indicador: ¿es una fuente de luz que puede ser encendida?
bit: ~is_lit?  \ Indicador: ¿el ente, que es una fuente de luz que puede ser encendida, está encendido?
bit: ~is_location?  \ Indicador: ¿es un escenario?
bit: ~is_open?  \ Indicador: ¿está abierto?
bit: ~is_vegetal?  \ Indicador: ¿es vegetal?
bit: ~is_worn?  \ Indicador: ¿siendo una prenda, está puesta?
cell offset: ~flags_0  \ Campo para albergar los indicadores anteriores

No obstante, la definición de offset: es idéntica a la de +field, base de la definición de campos e incluida tanto Gforth como en el futuro estándar Comité de Forth-2012, salvo que reinicia el contador de bitios que usan los campos buleanos. Unos pequeños cambios permiten prescindir de offset: y hacer la nomenclatura más estándar:

: bitfields  1 bit# !  ;
: bit#?  ( -- u )  bit# @  dup 0= abort" Too many bits defined in the field"  ;
: bit#+  bit# @ 1 lshift bit# !  ;
: bitfield:  ( u1 "name" -- u1 )
  \ Crea un campo de bitio en el próximo campo de celda.
  \ u1 = Desplazamiento (respecto al inicio de la ficha) del próximo campo de celda que se creará
  \ u2 = Máscara del bitio
  \ a = Dirección de la ficha de datos.
  \ a' = Dirección del campo en que está definido el bitio en la ficha de datos.
  create  bit#? over 2, bit#+
  does>  ( a pfa -- u2 a' )  2@ rot +
  ;

El resto del código no varía. A cambio, hay que incluir bitfields antes de la definición de campos, como se muestra a continuación:

bitfields  \ Campos de bitio utilizados en el próximo campo:
  bitfield: ~has_definite_article?  \ Indicador: ¿el artículo debe ser siempre el artículo definido?
  bitfield: ~has_feminine_name?  \ Indicador: ¿el género gramatical del nombre es femenino?
  bitfield: ~has_no_article?  \ Indicador: ¿el nombre no debe llevar artículo?
  bitfield: ~has_personal_name?  \ Indicador: ¿el nombre del ente es un nombre propio?
  bitfield: ~has_plural_name?  \ Indicador: ¿el nombre es plural?
  bitfield: ~is_animal?  \ Indicador: ¿es animal?
  bitfield: ~is_character?  \ Indicador: ¿es un personaje?
  bitfield: ~is_cloth?  \ Indicador: ¿es una prenda que puede ser puesta y quitada?
  bitfield: ~is_decoration?  \ Indicador: ¿forma parte de la decoración de su localización?
  bitfield: ~is_global_indoor?  \ Indicador ¿es global (común) en los escenarios interiores?
  bitfield: ~is_global_outdoor?  \ Indicador ¿es global (común) en los escenarios al aire libre?
  bitfield: ~is_human?  \ Indicador: ¿es humano?
  bitfield: ~is_light?  \ Indicador: ¿es una fuente de luz que puede ser encendida?
  bitfield: ~is_lit?  \ Indicador: ¿el ente, que es una fuente de luz que puede ser encendida, está encendido?
  bitfield: ~is_location?  \ Indicador: ¿es un escenario?
  bitfield: ~is_open?  \ Indicador: ¿está abierto?
  bitfield: ~is_vegetal?  \ Indicador: ¿es vegetal?
  bitfield: ~is_worn?  \ Indicador: ¿siendo una prenda, está puesta?
cell +field ~flags_0  \ Campo para albergar los indicadores anteriores

Pequeño añadido: sinónino condicional para las banderas:

: dragones  flags% is_known?  if  banderas  then  ;
' dragones synonyms{ dragón }synonyms

2012-04-28

Completado el código relativo al imposible intento de escalar el derrumbe:

: you_try_climbing_the_fallen_away
  \ Imprime la primera parte del mensaje
  \ previo al primer intento de escalar el derrumbe.
  s{ s" Aunque" s" A pesar de que" }s
  s{  s" parece no haber salida"
      s" el obstáculo parece insuperable"
      s" la situación parece desesperada"
      s" el regreso parece inevitable"
      s" continuar parece imposible"
  }s& comma+
  s{ s" optas por" s" decides" s" tomas la decisión de" }s&
  s{ s" explorar" s" examinar" }s& s" el" s&
  s{ s" derrumbe" s" muro de" rocks$ s& }s&
  s{  s" en compañía de" s" junto con"
      s" ayudado por" s" acompañado por"
  }s&
  s{ s" algunos" s" varios" }s& s" de tus" s&
  s{ s" oficiales" soldiers$ }s& s" , con la" s+
  s{ s" vana" s" pobre" s" débil" }s& s" esperanza" s&
  s" de" s&
  s{ s" encontrar" s" hallar" s" descubrir" }s&
  s{ s" la" s" alguna" }s& s{ s" manera" s" forma" }s& s" de" s&
  s{  s" escalarlo" s" vencerlo" s" atravesarlo"
      s" superarlo" s" pasarlo"
      s{ s" pasar" s" cruzar" }s s" al otro lado" s&
  }s&  period+ narrate narration_break
  ;
: you_can_not_climb_the_fallen_away
  \ Imprime la segunda parte del mensaje
  \ previo al primer intento de escalar el derrumbe.
  ^but$
  s{  s{ s" pronto" s" enseguida" }s s" has de" s&
      s" no tardas mucho en"
  }s& s{
    s" rendirte ante"
    s" aceptar"
    s" resignarte ante"
  }s& s{
    s{ s" los hechos" s" la realidad" s" la situación" s" tu suerte" }s
    s{  s" la voluntad"
        s{ s" el" s" este" }s s" giro" s&
        s{ s" el" s" este" }s s" capricho" s&
        s{ s" la" s" esta" }s s" mudanza" s&
    }s s" de" s& s" la diosa" s?& s" Fortuna" s&
  }s& s" ..." s+ narrate narration_break
  ;
: do_climb_the_fallen_away_first
  \ Imprime el mensaje
  \ previo al primer intento de escalar el derrumbe.
  you_try_climbing_the_fallen_away
  you_can_not_climb_the_fallen_away
  ;
: climbing_the_fallen_away_is_impossible
  \ Imprime el mensaje de error de que
  \ es imposible escalar el derrumbe.
  s{ s" pasar" s" escalar" s" subir por" }s
  s{
    s" el derrumbe"
    s{ s" el muro" s" la pared" s" el montón" }s s" de" s& rocks$ s&
    s" las" rocks$ s&
  }s& is_impossible
  ;
: do_climb_the_fallen_away
  \ Escalar el derrumbe.
  climbed_the_fallen_away? @ 0=
  if  do_climb_the_fallen_away_first  then
  climbing_the_fallen_away_is_impossible
  climbed_the_fallen_away? on
  ;

Escrito el código que faltaba para guardar en el fichero de la partida las variables de la trama y recuperarlas:

: save_plot
  \ Escribe las variables de la trama en el fichero de la partida.
  \ Debe hacerse en orden alfabético.
  rule>file s" \ Trama" >file/
  ambrosio_follows? @ f>file
  battle# @ n>file
  climbed_the_fallen_away? @ f>file
  hacked_the_log? @ f>file
  talked_to_the_leader? @ f>file
  s" load_plot" >file/  \ Palabra que hará la restauración de la trama
  ;

: load_plot  ( x0 ... xn -- )
  \ Restaura las variables de la trama.
  \ Debe hacerse en orden alfabético inverso.
  talked_to_the_leader? !
  hacked_the_log? !
  climbed_the_fallen_away? !
  battle# !
  ambrosio_follows? !
  ;

Fallo corregido en .command_prompt: no se cambiaba el color de papel cuando había sido definida la variable background_paper (experimental), lo que dejaba un valor indeseado en la pila.

2012-04-29

Fallo corregido en la impresión de citas de diálogos relativo a la impresión de los puntos finales.

Unas sencillas herramientas nuevas para facilitar la gestión de cadenas de texto:

: (?keep)  ( a u ff -- a u | a 0 )
  \ Conserva una cadena si un indicador es 'true'
  \ si es 'false' la vacía.
  abs *
  ;
: ?keep  ( a u f -- a u | a 0 )
  \ Conserva una cadena si un indicador no es cero;
  \ si es cero la vacía.
  0<> (?keep)
  ;
: ?empty  ( a u f -- a u | a 0 )
  \ Vacía una cadena si un indicador no es cero;
  \ si es cero la conserva.
  0= (?keep)
  ;

Muchas mejoras en las conversaciones con el jefe de los refugiados, gracias a haber implementado el recuerdo del número de veces en que el protagonista intenta pasar con la espada o la piedra.

2012-04-30

Eliminada la antigua variable de trama talked_to_the_leader?, pues puede calcularse a partir del número de conversaciones.

Nuevo campo en la base de datos de entes, para marcar los entes que no han de ser listados en inventario o entre los entes presentes. Aplicado al líder de los refugiados.

Descripción detallada de los refugiados, variable según la trama:

: the_leader_said_they_want_peace$  ( -- a u )
  \ El líder te dijo qué buscan los refugiados.
  s" que," s" tal y" s?& s" como" s& leader% full_name s&
  s" te" s?& s" ha" s&{ s" referido" s" hecho saber" s" contado" s" explicado" }s& comma+
  they_want_peace$ s&
  ;
: you_don't_know_why_they're_here$  ( -- a u )
  \ No sabes por qué están aquí los refugiados.
  s{  s" Te preguntas"
      s" No" s{  s" terminas de"
                  s" acabas de"
                  s" aciertas a"
                  s" puedes"
                }s& to_understand$ s&
      s" No" s{ s" entiendes" s" comprendes" }s&
  }s{
    s" qué" s{ s" pueden estar" s" están" }s& s" haciendo" s&
    s" qué" s{ s" los ha" s{ s" podría" s" puede" }s s" haberlos" s& }s&
      s{ s" reunido" s" traído" s" congregado" }s&
    s" cuál es" s{ s" el motivo" s" la razón" }s&
      s" de que se" s&{ s" encuentren" s" hallen" }s&
    s" por qué" s{ s" motivo" s" razón" }s?& s" se encuentran" s&
  }s& here$ s& period+
  ;
: some_refugees_look_at_you$  ( -- a u )
  s" Algunos" s" de ellos" s?&
  s" reparan en" s&{ s" ti" s" tu persona" s" tu presencia" }s&
  ;
: in_their_eyes_and_gestures$  ( -- a u )
  \ En sus ojos y gestos.
  s" En sus" s{ s" ojos" s" miradas" }s&
  s" y" s" en sus" s?& s" gestos" s&? s&
  ;
: the_refugees_trust$  ( -- a u )
  \ Los refugiados confían.
  some_refugees_look_at_you$ period+
  in_their_eyes_and_gestures$ s&
  s{ s" ves" s" notas" s" adviertes" s" aprecias" }s&
  s{
    s" amabilidad" s" confianza" s" tranquilidad"
    s" serenidad" s" afabilidad"
  }s&
  ;
: you_feel_they_observe_you$  ( -- a u )
  \ Sientes que te observan.
  s{ s" tienes la sensación de que" s" sientes que" }s?
  s" te observan" s& s" como" s?&
  s{  s" con timidez" s" tímidamente"
      s" de" way$ s& s" subrepticia" s& s" subrepticiamente"
      s" a escondidas"
  }s& period+
  ;
: the_refugees_don't_trust$  ( -- a u )
  \ Los refugiados no confían.
  some_refugees_look_at_you$ s{ s" . Entonces" s"  y" }s+
  you_feel_they_observe_you$ s&
  in_their_eyes_and_gestures$ s&
  s{
    s{ s" crees" s" te parece" }s to_realize$ s&
    s{ s" ves" s" notas" s" adviertes" s" aprecias" }s
    s" parece" to_realize$ s& s" se" s+
  }s& s{
    s" cierta" s?{
      s" preocupación" s" desconfianza" s" intranquilidad"
      s" indignación" }s&
    s" cierto" s?{ s" nerviosismo" s" temor" }s&
  }s&
  ;
: diverse_people$  ( -- a u )
  s{ s" personas" s" hombres, mujeres y niños" }s
  s" de toda" s& s" edad y" s?& s" condición" s&
  ;
: refugees_description
  \ Descripición de los refugiados.
  talked_to_the_leader?
  if    s" Los refugiados son"
  else  s" Hay"
  then  diverse_people$ s&
  talked_to_the_leader?
  if    the_leader_said_they_want_peace$
  else  period+ you_don't_know_why_they're_here$
  then  s&
  do_you_hold_something_forbidden?
  if    the_refugees_don't_trust$
  else  the_refugees_trust$
  then  s& period+ paragraph
  ;

Corregido error en (men), usada para desambiguar el vocablo «gente».

Mejorado el mensaje sobre la prohibición de pasar con la piedra:

: you_can_not_take_the_stone$  ( -- a u )
  \ Mensaje de que no te puedes llevar la piedra.
  s{ s" No" s" En modo alguno" s" De ninguna" way$ s& s" De ningún modo" }s
  s" podemos" s&
  s{
    s{ s" permitiros" s" consentiros" }s
      s{ s" huir" s" escapar" s" marchar" s" pasar" }s& s" con" s&
    s{ s" permitir" s" consentir" s" aceptar" }s s" que" s&
      s{  s{ s" huyáis" s" escapéis" s" marchéis" s" paséis" }s s" con" s&
          s" os vayáis con"
          s" os" s? s" marchéis con" s&
          s" os llevéis"
          s" nos" s? s" robéis" s&
          s" os" s{ s" apropiés" s" apoderéis" s" adueñéis" }s& s" de" s&
      }s&
  }s& s" la" s" piedra del druida"
  2dup stone% fname!  \ Nuevo nombre para la piedra
  s& s& period+
  ;

2012-05-01

Hecha la adaptación al nuevo código de impresión de párrafos. Ha sido sencillo. El que se usaba hasta ahora estaba basado en la palabra str-columns del módulo str de Forth Foundation Library, y obligaba a construir el texto completo de un párrafo antes de imprimirlo justificado:

\ Indentación de la primera línea de cada párrafo (en caracteres):
2 constant default_indentation  \ Predeterminada
8 constant max_indentation  \ Máxima
variable /indentation  \ En curso

' cr alias cr+
: ?cr  ( u -- )
  \ Hace un salto de línea si hace falta.
  \ u = Longitud en caracteres del párrafo que ha sido imprimido
  0> cr? @ and if  cr+  then
  ;
: ?space
  \ Imprime un espacio si el cursor no está al comienza de una línea.
  column if  space  then
  ;
: not_first_line?  ( -- ff )
  \ ¿La línea de pantalla donde se imprimirá es la primera?
  row 0>
  ;
variable indent_first_line_too?  \ ¿Se indentará también la línea superior de la pantalla, si un párrafo empieza en ella?
: indentation?  ( -- ff )
  \ ¿Indentar la línea actual?
  not_first_line? indent_first_line_too? @ or
  ;
: indentation+
  \ Añade indentación ficticia (con un carácter distinto del espacio)
  \ a la cadena dinámica 'print_str', si la línea del cursor no es la primera.
  indentation? if
    [char] X /indentation @ char>string «+
  then
  ;
: indentation-  ( a1 u1 -- a2 u2 )
  \ Quita a una cadena tantos caracteres por la izquierda como el valor de la indentación.
  /indentation @ -  swap /indentation @ +  swap
  ;
: indent
  \ Mueve el cursor a la posición requerida por la indentación.
  /indentation @ ?dup if  trm+move-cursor-right  then
  ;
: indentation>  ( a1 u1 -- a2 u2 )
  \ Prepara la indentación de una línea
  [debug] [if]  s" Al entrar en INDENTATION>" debug  [then]  \ Depuración!!!
  indentation? if  indentation- indent  then
  [debug] [if]  s" Al salir de INDENTATION>" debug  [then]  \ Depuración!!!
  ;
: .line  ( a u -- )
  \ Imprime una línea de texto y un salto de línea.
  [debug] [if]  s" En .LINE" debug  [then]  \ Depuración!!!
  type cr+
  ;
: .lines  ( a1 u1 ... an un n -- )
  \ Imprime n líneas de texto, con salto de línea final.
  \ a1 u1 = Última línea de texto
  \ an un = Primera línea de texto
  \ n = Número de líneas de texto en la pila
  dup #lines !  scroll on
  0  ?do  .line  i .scroll_prompt?  loop
  ;
: .lines/  ( a1 u1 ... an un n -- )
  \ Imprime n líneas de texto, sin salto de línea final.
  \ a1 u1 = Última línea de texto
  \ an un = Primera línea de texto
  \ n = Número de líneas de texto en la pila
  dup #lines !  scroll on
  0  ?do  cr type i .scroll_prompt?  loop
  ;
: (paragraph)
  \ Imprime la cadena dinámica 'print_str' ajustándose al ancho de la pantalla.
  indentation+  \ Añade indentación ficticia
  print_str str-get cols str+columns  \ Divide la cadena dinámica 'print_str' en tantas líneas como haga falta
  [debug] [if]  s" En (PARAGRAPH)" debug  [then]  \ Depuración!!!
  >r indentation> r>  \ Prepara la indentación efectiva de la primera línea
  .lines  \ Imprime las líneas
  print_str str-init  \ Vacía la cadena dinámica
  ;
: paragraph/ ( a u -- )
  \ Imprime una cadena como un nuevo párrafo,
  \ ajustándose al ancho de la pantalla.
  print_str str-set (paragraph)
  ;
: paragraph  ( a u -- )
  \ Imprime una cadena como un nuevo párrafo,
  \ ajustándose al ancho de la pantalla;
  \ y una separación posterior si hace falta.
  dup >r  paragraph/ r> ?cr
  ;


Para permitir imprimir texto nuevo en cualquier posición en que se encuentre el cursor, había código adicional inacabado:

false [if]  \ Inacabado!!!
: str_first_word_length  ( a -- u )
  \ Devuelve la longitud de la primera palabra de una cadena dinámica.
  \ a = Dirección de la cadena dinámica
  >r s"  " 0 r@ str-find  \ Buscar el primer espacio
  dup -1 = if  \ No se encontró
    drop r@ str-length@  \ Devolver la longitud de la cadena
  then  rdrop
  ;
: ((text))  ( a1 u1 u2 -- )
  \ Imprime una cadena ajustándose al ancho de la pantalla,
  \ a partir de la posición actual del cursor, sabiendo
  \ el espacio disponible en la línea actual
  \ y que al menos la primera palabra del texto cabe en ese espacio.
  \ a1 u1 = Cadena de texto a imprimir (copiada en la cadena dinámica 'print_str')
  \ u2 = Caracteres libres en la línea actual de la pantalla
  \ Inacabado!!!
  str+columns  \ Divide la cadena en líneas del ancho disponible
  ( a1 u1 ... an un n -- )
  over >r >r  \ Salva la longitud de la primera línea y el número de estas
  ?space type  \ Imprime la primera línea
  r> 1- ?dup if  \ ¿Había más de una línea preparada?
    2mdrop  \ Borra el resto de líneas
    r> 0 print_str str-delete  \ Elimina de la cadena la zona ya imprimida
    print_str str-get cols str+columns .lines/  \ Imprime el resto de la cadena
  else  rdrop
  then
  ;
: (text)  ( a1 u1 u2 u3 -- )
  \ Imprime una cadena ajustándose al ancho de la pantalla,
  \ a partir de la posición actual del cursor, sabiendo
  \ el espacio disponible en la línea actual
  \ y la longitud de la primera palabra del texto.
  \ a1 u1 = Cadena de texto a imprimir (copiada en la cadena dinámica 'print_str')
  \ u2 = Caracteres libres en la línea actual de la pantalla
  \ u3 = Longitud de la primera palabra de la cadena de texto
  \ Inacabado!!!
  over <= if  ((text))  else  drop paragraph/  then
  ;
: text  ( a u -- )
  \ Imprime una cadena ajustándose al ancho de la pantalla,
  \ a partir de la posición actual del cursor.
  \ Inacabado!!!
  2dup print_str str-set  \ Copia la cadena en la cadena dinámica 'print_str'
  cols column - 1- dup 0 >  \ ¿Queda espacio en la línea actual?
  if    print_str str_first_word_length (text)
  else  drop paragraph/
  then
  ;

Pero todo eso se hace innecesario con un módulo independiente y más sencillo que hemos escrito a partir de un algoritmo de la librería print de 4tH. El módulo formará parte de una librería propia que está en desarrollo paralelo con el juego, para independizar todos los elementos reutilizables en otros proyectos. Al nuevo sistema de impresión solo resta implementarle la gestión de indentación opcional.

2012-05-02

Añadida la indentación al nuevo sistema de impresión.

Nueva variable de trama para controlar si el protagonista habla más de una vez con el líder de los refugiados sin cambiar de escenario. Esto permite crear diálogos más realistas.

2012-05-04

Las tramas del acceso a la cueva oscura y de la emboscada han sido movidas alternativamente al nivel de tramas de escenario, lo cual es más lógico. Pero la trama de la cueva oscura plantea el problema de tener que cambiar de escenario cuando aún no se ha resuelto el cambio de escenario que dispara la trama... De momento la compilación condicional permite elegir uno de ambos métodos para cada trama, hasta que el nuevo código esté probado.

Corregido un fallo que «desbocaba» dos bucles (0 0 do) cuando se intentaba listar el número de salidas de un escenario sin salidas; bastaba usar ?do en ambos casos: en unsort y en la creación en sí de la lista de salidas. Esta circunstancia solo se da durante la emboscada, antes de encontrar la salida del desfiladero.

Implementados el código y los textos del descubrimiento de la entrada de la cueva al mirar en la dirección correspondiente.

2012-05-05

Ampliación del código y los textos relacionados con el descubrimiento de la entrada de la cueva.

2012-05-06

Corregido un oscuro fallo que impedía «mirar la salida».

Ampliados los textos del final sin éxito: la captura por parte de los sajones.

Implementado el ente de la pared del desfiladero, y el código relacionado que descubre la entrada de la cueva.

Implementado el ente global «pared» y su desambiguación.

2012-05-07

Ampliaciones y correcciones en los textos relativos a la entrada de la cueva y la pared de roca.

El nuevo sistema de impresión, basado en el módulo print de la nueva librería Galope, es mucho más sencillo que el anterior:

\ Indentación de la primera línea de cada párrafo (en caracteres):
2 constant default_indentation  \ Predeterminada
8 constant max_indentation  \ Máxima
variable /indentation  \ En curso

variable indent_first_line_too?  \ ¿Se indentará también la línea superior de la pantalla, si un párrafo empieza en ella?
: not_first_line?  ( -- ff )
  \ ¿El cursor no está en la primera línea?
  row 0>
  ;
: indentation?  ( -- ff )
  \ ¿Indentar la línea actual?
  not_first_line? indent_first_line_too? @ or
  ;
: (indent)
  \ Indenta.
  /indentation @ print_indentation
  ;
: indent
  \ Indenta si es necesario.
  indentation? if  (indent)  then
  ;
: cr+
  print_cr indent
  ;
: paragraph  ( a u -- )
  \ Imprime un texto justificado como inicio de un párrafo.
  \ a u = Texto
  cr+ print
  ;

Pero tiene el inconveniente de que, debido a que print_indentation usa espacios para crear la indentación de la primera línea, colorea ese espacio con el color de fondo en uso. Para evitarlo se puede hacer esto:

: |paragraph (a u xt ) background_color cr+ execute print;

: report (a u) ['] error_color |paragraph system_color;: narrate (a u ) ['] narration_color |paragraph system_color; !#

Pero es más sencillo hacer que print_indentation haga el sangrado moviendo el cursor con la palabra correspondiente del módulo trm de Forth Foundation Library:

: print_indentation  ( u -- )
  dup trm+move-cursor-right dup indented+ printed+  ;

Esto permite simplificar de nuevo las llamadas a los párrafos tras un cambio de color:

: report  ( a u -- )
  \ Imprime una cadena como un informe de error.
  error_color paragraph system_color
  ;
: narrate  ( a u -- )
  \ Imprime una cadena como una narración.
  narration_color paragraph system_color
  ;

2012-05-08

Corregidos varios errores oscuros en la impresión de párrafos cuando había pausas de narración que coincidían con ciertas condiciones, como la última línea de la pantalla o una línea recién borrada. Por ejemplo, se creaba una línea en blanco indeseada o la sangría aumentaba en una posición.

Utilizada la palabra ?? para sustituir todas las estructuras if then simples; es más compacto y fácil de editar.

2012-05-09

Ampliados los textos del comienzo de la emboscada.

2012-05-12

Corregido error en la descripción de location_11%, el inicio de la gruta.

Iniciada la implementación de nuevas tramas opcionales para los entes escenario (hasta ahora solo había una): una que permita decidir si la entrada al escenario se realiza o no; otra que se ejecute antes de la descripción; otra entre la descripción y la lista de entes presentes (la que había hasta el momento); y una final tras la lista. En realidad la palabra que realiza la descripción del ente puede hacer cualquier labor de trama que se precise. Por tanto ese nivel no es necesario. Las tramas de escenario existentes habrán de convertirse en tramas previas o posteriores a la descripción, según el caso.

La implementación de las nuevas tramas de escenario es muy sencilla y permitirá crear cualquier condición requerida por los puzles o la trama global, ciñendo el código al escenario y momento exactos en que debe ejecutarse, sin tener que hacer ninguna comprobación en el bucle principal. El primer caso para el que esto es útil es la trama de entrada sin antorcha al tramo de cueva oscuro: podrá implementarse completa a nivel de escenario, que es donde le corresponde, en lugar de en la trama global.

2012-05-13

Concluida la implementación del nuevo sistema de tramas de escenario.

Los campos son:

\ Direcciones de ejecución de las tramas de escenario
cell +field ~can_i_enter_location?_xt  \ Trama previa a la entrada al escenario
cell +field ~before_describing_location_xt  \ Trama de entrada antes de describir el escenario
cell +field ~after_describing_location_xt  \ Trama de entrada tras describir el escenario
cell +field ~after_listing_entities_xt  \ Trama de entrada tras listar los entes presentes
cell +field ~before_leaving_location_xt  \ Trama antes de abandonar el escenario

Las palabras que permiten definir las tramas funcionan igual que la que ya existía, con sus diferentes nombres y campos asociados. La gestión se hace al intentar cambiar de escenario, igual que antes, pero de forma más versátil:

: ?execute  ( xt | 0 -- )
  \ Ejecuta un vector de ejecución, si no es cero.
  ?dup ?? execute
  ;
: before_describing_any_location
  \ Trama de entrada común a todos los entes escenario.
  \ No se usa!!!
  ;
: before_describing_location  ( a -- )
  \ Trama de entrada a un ente escenario.
  before_describing_any_location
  before_describing_location_xt ?execute
  ;
: after_describing_any_location
  \ Trama de entrada común a todos los entes escenario.
  \ No se usa!!!
  ;
: after_describing_location  ( a -- )
  \ Trama de entrada a un ente escenario.
  after_describing_any_location
  after_describing_location_xt ?execute
  ;
: after_listing_entities  ( a -- )
  \ Trama final de entrada a un ente escenario.
  after_listing_entities_xt ?execute
  ;
: before_leaving_any_location
  \ Trama de salida común a todos los entes escenario.
  \ No se usa!!!
  ;
: before_leaving_location  ( a -- )
  \ Ejecuta la trama de salida de un ente escenario.
  before_leaving_any_location
  before_leaving_location_xt ?execute
  ;
: (leave_location)  ( a -- )
  \ Tareas previas a abandonar un escenario.
  dup visits++
  dup before_leaving_location
  protagonist% was_there
  ;
: leave_location
  \ Tareas previas a abandonar el escenario actual.
  my_location ?dup if  (leave_location)  then
  ;
: actually_enter_location  ( a -- )
  \ Entra en un escenario.
  leave_location
  dup my_location!
  dup before_describing_location
  dup describe
  dup after_describing_location
  dup familiar++  .present
  after_listing_entities
  ;
: enter_location?  ( a -- ff )
  \ Ejecuta la trama previa a la entrada de un escenario,
  \ que devolverá un indicador de que puede entrarse en el escenario;
  \ si esta trama no está definida para el ente, el indicador será 'true'.
  \ a = Ente escenario
  \ ff = ¿Hay que entrar en él?
  can_i_enter_location?_xt ?dup if  execute  else  true  then
  ;
: enter_location  ( a -- )
  \ Entra en un escenario, si es posible.
  dup enter_location? and ?dup ?? actually_enter_location
  ;

Gracias a estos cambios, el paso a la zona oscura de la cueva puede crearse como trama previa de entrada al primer escenario oscuro, simplemente con dos palabras:

: dark_cave
  \ En la cueva y sin luz.
  \ Pendiente!!! ampliar y variar estos textos
  new_page
  s" Ante la reinante"
  s{ s" e intimidante" s" e impenetrable" s" y sobrecogedora" }s&
  s" oscuridad," s&
  s{ s" vuelves atrás" s" retrocedes" }s&
  s{ "" s" unos pasos" s" sobre tus pasos" }s&
  s" hasta donde puedes ver." s&
  narrate
  ;

location_20% :can_i_enter_location?  ( -- ff )
  location_17% am_i_there? no_torch? and
  dup 0= swap ?? dark_cave
  ;can_i_enter_location?

Las únicas tramas globales que quedan son la de la batalla y la del movimiento de Ambrosio, que podrían implementarse a nivel de escenario. Para ello convendría implementar una trama adicional de escenario que se ejecutara antes o después (o dos diferentes, una para cada caso) de cada comando del jugador. De este modo la palabra de la trama global no tendría que hacer ninguna comprobación, sino ejecutar la que corresponda, si existe. Esto no es importante en este juego porque la trama global es sencilla, pero es un método mejor que permitirá crear tramas muy complejas en otros proyectos.

2012-05-15

Descubierta una cuestión interesante: Como era sabido, Gforth completa las palabras que se están escribiendo en la línea de comandos con solo pulsar el tabulador. Cada pulsación muestra una nueva palabra que comience por las letras que hemos escrito. La buena noticia es que esto lo hace también en las entradas de texto con accept, lo que permite usar esta útil funcionalidad en el juego, para lo cual basta añadir en wait_for_input un par de palabras para limitar la lista de vocabularios al vocabulario del jugador y otra más para restaurar el estado normal, como se muestra:

: wait_for_input  ( -- a u )
  \ Espera y devuelve el comando introducido por el jugador.
  only player_vocabulary
  input_color
  command /command accept
  command swap  str+strip  \ Eliminar espacios laterales
  restore_vocabularies
  ;

La mala noticia es que las palabras se completan sin tener en cuenta que el vocabulario activo es una tabla, un tipo especial de vocabulario de Gforth que es sensible a mayúsculas. En su día se eligió usar una tabla para diferenciar los comandos del sistema (en mayúsculas) de los comandos del juego (en minúsculas):

\ Gforth necesita su propio método
\ para crear un vocabulario sensible a mayúsculas,
\ con la palabra 'table':
table value (player_vocabulary)
: player_vocabulary
  \ Reemplaza el vocabulario superior con el del jugador.
  \ Código adaptado de Gforth (compat/vocabulary.fs).
  get-order dup 0= 50 and throw  \ Error 50 («search-order underflow») si la lista está vacía
  nip (player_vocabulary) swap set-order
  ;

Pero la consecuencia es que al pulsar el tabulador se nos ofrezcen alternativas que no son reconocidas porque se completan sin unificar las mayúsculas de la palabra para que coincidan con su definición.

La solución es utilizar un vocabulario normal y diferenciar los comandos del sistema con un signo, por ejemplo una almohadilla como prefijo. Además, las letras que no forman parte de ASCII no son tenidas en cuenta por el algoritmo de Gforth que hace indistintas las mayúsculas de las minúsculas en los nombres de las palabras definidas, lo que obliga a definir sinónimos para las palabras del jugador con esas letras en mayúsculas, tantos como haya combinaciones posibles de esas letras en la palabra.

Tras terminar este cambio, queda cerrada la versión A-04 y se inicia la versión A-05.

Añadida la preposición «a» (con la contracción «al» cuando corresponda) en los listados de entes presentes, para las personas. Bastaba con definir una nueva palabra y usarla en (content_list) en sustitución de full_name:

: full_name_as_direct_complement  ( a -- a1 u1 )
  \ Devuelve el nombre completo de un ente en función de complemento directo.
  \ Esto es necesario para añadir la preposición «a» a las personas.
  dup s" a" rot is_human? and
  rot full_name s&
  s" al" s" a el" sreplace
  ;
: (content_list)  ( a -- )
  \ Añade a la lista en la cadena dinámica 'print_str' el separador y el nombre de un ente.
  #elements @ #listed @  list_separator
  dup full_name_as_direct_complement rot (worn)& »&  #listed ++
  ;

Corregidos errores de concatenación en varios textos aleatorios.

2012-05-17

Completado el código para el uso de complementos seudopreposicionales. Implementada la distinción entre «con» (herramienta o compañía) y sus alternativas como «mediante» (solo herramienta). Modificada la acción de hablar para utilizar el complemento adecuado. Creados nuevos filtros para cuando se requiera distinguir expresamente la herramienta.

Ampliadas con variaciones al azar las conversaciones con Ambrosio.

Poco a poco eliminamos las estructuras case que no sean imprescindibles, pues se compilan como estructuras if anidadas, lo que no deja de ser «azúcar sintáctica», aparte de que el cálculo se repite para cada opción. Es mucho más eficaz y rápido, y más propio del espíritu de Forth, usar una tabla de búsqueda, accesible con un solo cálculo. Así por ejemplo, la selección de la conversación actual con Ambrosio pasa de esto:

: (talk_to_ambrosio)
  \ Hablar con Ambrosio.
  ambrosio% conversations case
    0 of  conversation_0_with_ambrosio  endof
    1 of  conversation_1_with_ambrosio  endof
    2 of  conversation_2_with_ambrosio  endof
    \ Inacabado!!! Implementar qué hacer cuando ya no hay más conversaciones
  endcase
  ;

A esto:

create conversations_with_ambrosio
' conversation_0_with_ambrosio ,
' conversation_1_with_ambrosio ,
' conversation_2_with_ambrosio ,
' noop , \ Inacabado!!! Implementar qué hacer cuando ya no hay más conversaciones
: (talk_to_ambrosio)
  \ Hablar con Ambrosio.
  ambrosio% conversations cells conversations_with_ambrosio + perform
  ;

2012-06-04

Error corregido: en la descripción de Ambrosio el uso del nombre propio dependía de si su ente era conocido, no de si había habido conversaciones.

2012-07-11

Error corregido: el renombrado incompleto de varios ficheros la biblioteca Galope provocaba que el módulo de cadenas aleatorias cargara de nuevo el búfer circular de texto; esto restauraba la situación del búfer tras haber sido inicializado y provocaba un error en cuanto se hacía la primera operación en él.

2012-09-06

Retoque en la rutina de entrada de comandos; del tipo de mejoras que no se suelen ver al escribir el código pero sí al ojearlo días después: command /command accept command swap puede escribirse mejor así: command dup /command accept.

2012-09-23

Primeros cambios para manipular cadenas de texto en UTF -8.

Se elimina la definición de seconds y se usa la equivalente de la librería Galope.

2012-09-28

Mejora del sistema de errores lingüísticos con tres niveles de detalle: ningún mensaje, un mensaje genérico y el mensaje específico que ya existía.

Sustitución de la abreviatura de «arriba» «a» por «ar», para impedir que se reconozca en su lugar la preposición «a». Sustitución de la abreviatura de «abajo» «b» por «ab», para homogeneizar la pareja.

2012-09-29

Implementación de nivel de detalle, configurable, en los errores lingüísticos: no mostrar ningún mensaje; mostrar un mensaje genérico configurable; y mostrar un mensaje específico como hasta ahora. Los errores lingüísticos están muy centralizados, por lo que hacer esta modificación ha sido sencillo:

Una variable para el nivel y otra para el mensaje genérico (ambas modificables con los comandos adecuados en el fichero de configuración):

variable language_errors_verbosity  \ Nivel de detalle de los mensajes de error lingüístico
svariable 'language_error_general_message$  \ Mensaje de error lingüístico para el nivel 1

Código para el error específico, como se hacía hasta ahora:

: language_error_specific_message  ( a u -- )
  \ Muestra un mensaje detallado sobre un error lingüístico,
  \ combinándolo con una frase común.
  \ a u = Mensaje de error detallado
  \ xxx inacabado. hacer que use coma o punto y coma, al azar
  in_the_sentence$ s&  3 random
  if    ^uppercase period+ ^error_comment$
  else  ^error_comment$ comma+ 2swap
  then  period+ s&  (language_error)
  ;

Código nuevo para el error general:

: language_error_general_message$  ( -- a u )
  \ Devuelve el mensaje de error lingüístico para el nivel 1.
  'language_error_general_message$ count
  ;
: language_error_general_message  ( a u -- )
  \ Muestra el mensaje de error lingüístico para el nivel 1.
  \ a u = Mensaje de error detallado
  2drop language_error_general_message$ (language_error)
  ;

Una tabla con las direcciones de ejecución de los niveles:

create 'language_error_verbosity_xt
  ' 2drop ,  \ Verbosity 0
  ' language_error_general_message ,  \ Verbosity 1
  ' language_error_specific_message ,  \ Verbosity 2

Y la palabra central que hace la selección:

: language_error  ( a u -- )
  \ Muestra un mensaje sobre un error lingüístico,
  \ específico o genérico o ninguno, según la configuración.
  \ a u = Mensaje de error específico
  'language_error_verbosity_xt
  language_errors_verbosity @ cells + perform
  ;

2012-10-04

Primeros cambios para implementar mensajes específicos (y variables) tras completar una acción. Por ejemplo, «Te quitas tu capa» o «Te la quitas», en lugar del genérico «Hecho» que se usa hasta ahora para todo.

Para algunos de estos mensajes se necesitan los pronombres personales correspondientes a los entes. Por ello se añaden a la tabla de artículos y seudoartículos (que ya habrá que renombrar), con sus palabras correspondientes.

Implementada una opción de configuración para repetir la última acción si no se especifica una acción en el comando.

Primeras pruebas para implementar la ejecución secuencial automática de órdenes compuestas. Por ejemplo, quitarse una prenda para poder dejarla.

2012-10-07

Implementación de niveles de detalle de los errores operativos, los que se producen en la ejecución de los comandos, de forma análoga a los errores lingüísticos. Hasta ahora, las palabras de error operativo estaban creadas de la siguiente forma:

Primero se creaban como vectores los identificadores de todos los errores, necesario para poder referenciarlos en el código antes de ser definidos los errores efectivos:

defer cannot_see_error#

Más adelante se creaban los errores efectivos:

: cannot_see  ( a -- )
  \ Informa de que un ente no puede ser mirado.
  ^cannot_see$
  rot subjective_negative_name_as_direct_object s&
  period+ narrate
  ;

Y para cada error se actualizaba el vector de su identificador mediante una constante intermedia que contenía la dirección de ejecución del error:

' cannot_see constant (cannot_see_error#)
' (cannot_see_error#) is cannot_see_error#

A partir de ahora, para que los errores actúen de forma diferente según el nivel de detalle configurado por el usuario, usamos una palabra nueva para crearlos:

: action_error:  ( n xt "name1" -- xt2 )
  \ Crea un error operativo.
  \ n = número de parámetros del error operativo efectivo
  \ xt = dirección de ejecución del error operativo efectivo
  \ "name1" = nombre de la palabra de error a crear
  \ xt2 = dirección de ejecución de la palabra creada
  create , , latestxt
  does>  ( pfa )
    action_errors_verbosity @ case
      0 of  cell+ @ drops  endof
      1 of  cell+ @ drops action_error_general_message  endof
      2 of  perform  endof
    endcase
  ;

Así, el anterior error mostrado como ejemplo se definiría así:

Primero, usamos value para definir los identificadores (algo que podría haberse hecho antes también, para evitar crear una constante intermedia):

0 value cannot_see_error#

Segundo, creamos el error efectivo:

: (cannot_see)  ( a -- )
  \ Informa de que un ente no puede ser mirado.
  ^cannot_see$
  rot subjective_negative_name_as_direct_object s&
  period+ narrate
  ;

Y por último creamos la palabra de error «pública» y actualizamos su identificador:

1 ' (cannot_see) action_error: cannot_see
to cannot_see_error#

2012-10-10

Implementación de un sistema más eficaz para permitir varias seudopreposiciones en los comandos, anotándolas de forma paralela en una máscara de bitios que permita a cada acción hacer una selección de las combinaciones posibles.

Páginas relacionadas

Asalto y castigo [en Forth]
Juego de aventuras de texto escrito en Forth con Gforth.