Dagaz: un nuevo comienzo

Corre hacia el sur y gira hacia el norte, gira, gira para correr con su viento
Y según sus circuitos vuelve el viento;
Todos los ríos desembocan en el mar, y el mar no se desborda,
Al lugar donde corren los ríos, - allí continúan corriendo;

El libro de los eclesiastés.

En 1998, se desarrolló una aplicación completamente única para su época que le permite reducir el proceso de desarrollo de un juego de tablero abstracto (o rompecabezas) a un lenguaje de descripción de texto pequeño, que recuerda vagamente a Lisp . Este proyecto se llamó Zillions of Games . Creó furor entre los fanáticos de los juegos de mesa. Actualmente, se han creado más de 2,000 aplicaciones utilizando esta tecnología.

Rápidamente se hizo evidente que ZoG tiene muchos inconvenientes. Ya escribí sobre esto en Habr y no me repetiré. Permítanme decir que los desarrolladores no tuvieron en cuenta las características de una gran cantidad de juegos existentes y algunas opciones importantes fueron codificadas, por lo que su cambio se volvió extremadamente problemático. Greg Schmidt, en 2007, trató de rectificar la situación al lanzar el kit de desarrollo Axiom , pero su estrecha integración con ZoG no permite resolver todos los problemas.

El Proyecto Ludi señaló nuevas fronteras, utilizando el "motor" del juego universal y los algoritmos genéticos para automatizar el proceso de desarrollo de nuevos juegos de mesa. Desafortunadamente, este enfoque se concibió inicialmente como una simplificación deliberada de la mecánica del juego y del nivel de la IA empleada. La discusión de los objetivos de este proyecto está más allá del alcance de este artículo, pero algunas de sus soluciones técnicas, sin duda, sirvieron como punto de partida para mi propio desarrollo.

Mi objetivo es el desarrollo de un "motor" más versátil y fácil de usar para la creación de juegos de mesa abstractos. Durante casi un año he estado estudiando la posibilidad de ZoG y Axiom y aprendí mucho sobre sus limitaciones. Creo que puedo resolver sus problemas creando una solución más universal y multiplataforma. Sobre el progreso del trabajo en este proyecto, informaré.

Apertura y modularidad.


Quizás el principal inconveniente de ZoG es su cierre. El producto se ensambló "una vez y para siempre" en una sola plataforma: Windows. Si se tratara de código de fuente abierta, uno podría intentar portarlo bajo Linux, Android, iOS ... Otro problema es su monolitismo.

En ZoG existen los inicios de la modularidad, lo que permite la conexión a la DLL de juegos, incluidas las implementaciones personalizadas de la IA. Axiom va un poco más allá, permitiéndole ejecutar aplicaciones en el modo de reproducción automática, sin usar el núcleo ZoG. A pesar de la seria limitación de esta solución (solo admite aplicaciones para dos jugadores), este ejemplo demuestra cómo sería útil la modularidad. No se puede sobrestimar la oportunidad de organizar un juego con dos bots (usando diferentes configuraciones de IA) y recopilar estadísticas sobre una gran cantidad de juegos. ¡Pero cuánto mejor sería si el producto hubiera sido completamente modular!

  • Mover módulo de generación
  • Mover módulo de ejecución
  • Módulo de control
  • Módulo AI
  • Módulo de visualización

Todo el trabajo que describe los juegos debe ser realizado por el módulo de generación de movimientos. Este es el "corazón" del proyecto. La transferencia de todas las tareas que no estén conectadas con esta función a otros módulos lo hará lo más simple posible. Puede mejorar este módulo, sin mirar los problemas de IA y la interacción del usuario. Puede cambiar completamente el formato de la descripción de los juegos o agregar soporte para las descripciones en el formato de ZoG, Axiom y Ludi. ¡La modularidad es la base de la flexibilidad de la solución!

El módulo de ejecución de movimiento es el custodio del estado del juego. La información sobre el estado actual del juego se transfiere a todos los demás módulos a pedido. Por las razones que daré a continuación, el progreso de la ejecución debe pasar por el módulo de generación, cuya tarea es la formación de un comando en términos de ejecución del módulo. Además, la tarea del módulo de generación de movimientos es la configuración principal del espacio del juego, basada en la descripción del juego.

El módulo de control es, de hecho, la aplicación misma. Pide al módulo de generación de movimientos una lista de posibles movimientos y cambia el estado del juego, pasando el movimiento seleccionado al módulo de ejecución de movimientos. El módulo de control se puede conectar para reproducir uno o más bots AI. ¡Todos los que necesite (y posiblemente diferentes)! El tipo de unidad de control está determinado por la división de tareas. Esto puede ser reproducción automática para recopilar estadísticas del juego, servidor de juegos (puede controlar varias tiendas estatales, liderando una gran cantidad de sesiones de juego) o aplicaciones individuales para jugar sin conexión.

La capacidad de conectar diferentes implementaciones de IA mejorará la calidad del juego. Se entiende que los módulos para el juego de ajedrez y Go deberían usar diferentes enfoques. Los juegos con información incompleta y los juegos que usan datos aleatorios también requieren un enfoque individual. ¡La implementación universal de IA será igualmente mala para todos los juegos! La conexión modular AI permitirá comparar la "fuerza" de los algoritmos, incluido un modo de juego "entre sí". Dado que la arquitectura AI está separada del estado de almacenamiento del juego, una instancia del robot de juego puede admitir un número ilimitado de sesiones de juego simultáneamente.

La visualización del proceso del juego también puede ser variada. Lo primero que viene a la mente son las implementaciones en 2D y 3D. La plataforma para la que se desarrolla la aplicación también es importante. ¡Menos obvio es que la visualización puede ser una parte importante del juego! Por ejemplo, en el juego Surakarta , tomar piezas será completamente no obvio en ausencia de una animación adecuada de los movimientos.


En general, la modularidad parece una buena idea para tal proyecto, y el código fuente abierto permitirá a todos los que deseen participar en el proyecto. En la actualidad, no me propongo propósitos comerciales, pero creo que, si lo deseo, encontraré la manera de ganar dinero sin cerrar el código fuente.

El espacio del juego


Antes de comenzar el espectáculo, debes preparar el escenario. El tablero no es solo un lugar donde se colocan las piezas. Además de eso, se puede determinar la dirección del movimiento de las piezas (de hecho, las conexiones entre las posiciones del tablero) áreas de juego (por ejemplo, áreas para la conversión de piezas) campos prohibidos, etc. Así es como se ve la definición del tablero de ajedrez en la implementación de ZoG:

Definiendo el tablero en ZoG
(define Board-Definitions (image "images\Chess\SHaag\Chess8x8.bmp" "images\Chess\Chess8x8.bmp") (grid (start-rectangle 5 5 53 53) (dimensions ("a/b/c/d/e/f/g/h" (49 0)) ; files ("8/7/6/5/4/3/2/1" (0 49)) ; ranks ) (directions (n 0 -1) (e 1 0) (s 0 1) (w -1 0) (ne 1 -1) (nw -1 -1) (se 1 1) (sw -1 1) ) ) (symmetry Black (ns)(sn) (nw sw)(sw nw) (ne se)(se ne)) (zone (name promotion-zone) (players White) (positions a8 b8 c8 d8 e8 f8 g8 h8) ) (zone (name promotion-zone) (players Black) (positions a1 b1 c1 d1 e1 f1 g1 h1) ) (zone (name third-rank) (players White) (positions a3 b3 c3 d3 e3 f3 g3 h3) ) (zone (name third-rank) (players Black) (positions a6 b6 c6 d6 e6 f6 g6 h6) ) ) 

Puede notar que además de la configuración del juego, aquí están las configuraciones asociadas con la visualización. Estoy firmemente convencido de que esta configuración no pertenece aquí. Al implementar un módulo de visualización, se pueden usar múltiples configuraciones y se pueden requerir diferentes configuraciones. Además, los juegos de simulación pueden funcionar sin ningún módulo de visualización (como la reproducción automática en Axiom). De hecho, dado que Axiom se usa para visualizar ZoG, la definición no contiene nada superfluo:

Definiendo el tablero en Axiom
 {board 8 8 {grid} board} {directions -1 0 {direction} n 1 0 {direction} s 0 1 {direction} e 0 -1 {direction} w -1 -1 {direction} nw 1 -1 {direction} sw -1 1 {direction} ne 1 1 {direction} se directions} {symmetries Black {symmetry} ns Black {symmetry} nw sw Black {symmetry} ne se symmetries} 

Desafortunadamente, Axiom tampoco tiene una forma de determinar las zonas de juego (la ubicación de las zonas de juego debe determinarse manualmente en el código). Esta no es la única simplificación de Axiom. La definición del tablero en este proyecto no puede contener más de una cuadrícula y esta cuadrícula debe ser bidimensional. El tablero, así definido, es una matriz unidimensional, pero para la conveniencia del programador, los sinónimos se definen para cada uno de los espacios de la siguiente manera:


En comparación con el esquema más flexible de definición de cuadrícula en ZoG, estas restricciones son bastante incómodas (especialmente dado el hecho de que el esquema de nomenclatura impuesto usó estos campos con el mismo propósito de visualización). Afortunadamente, es posible definir un tablero de forma arbitraria. Tanto Axiom como ZoG brindan la oportunidad de identificar cada posición del elemento en el tablero junto con la capacidad de determinar los vínculos entre pares de posiciones arbitrarias. Con este enfoque, podemos definir un tablero de cualquier topología. Su único inconveniente es la extrema verbosidad y complejidad de la descripción.

Además de la ubicación de las piezas en el tablero y en la reserva, el sistema debe tener la capacidad de almacenar atributos para piezas individuales y para los espacios en el tablero. Un buen ejemplo de la necesidad de utilizar los atributos de una regla de " enroque " en el ajedrez . Este es un movimiento difícil, que incluye el movimiento simultáneo del rey y una torre, permitido, siempre que ninguna de estas piezas se haya movido antes de realizar este movimiento. Se podría usar un atributo para almacenar una etiqueta booleana que muestre si la pieza se movió alguna vez. Los atributos de campo también pueden encontrar algunas aplicaciones interesantes.

Cabe señalar que los atributos no son solo variables sino parte del estado del juego. El valor de un atributo puede cambiarse mediante la ejecución de un turno (incluido el módulo AI) y debe estar disponible para todos los turnos posteriores, pero no para los turnos realizados en otra rama del juego. Actualmente, ZoG admite el almacenamiento de atributos booleanos de piezas. Los atributos de almacenamiento de Axiom no son compatibles, pero puede agregar a la definición de la placa una descripción de variables y matrices. Se pueden usar estas variables, como los contadores de la cantidad de piezas capturadas:

 {board 5 18 {grid} {variable} WhitePieces {variable} BlackPieces board} 

Otra limitación de ZoG y de Axiom es la regla de que cada posición del tablero no puede contener más de una pieza. Si alguna pieza completa un movimiento a una posición ocupada por otra pieza, la pieza que anteriormente ocupaba la posición se considera automáticamente "comida". Esta regla va bien con el principio de "ajedrez" de tomar piezas y sirve para simplificar la descripción de este juego, pero complica la implementación de juegos como " damas bashni " y " tavreli ".



En estos juegos, las piezas se pueden organizar en "columnas". Tal "columna" se puede mover todo junto, como una sola pieza. Después de reflexionar un poco, decidí que era mejor no abandonar la implementación automática de la captura de "Ajedrez", sino mejorar los mecanismos para mover grupos de piezas. De hecho, para la implementación de los "pilares", siempre puede agregar al tablero otra dimensión (esto es especialmente fácil, siempre que el módulo de visualización esté separado del módulo generador de movimiento y de la IA, entonces puede usar cualquier lógica que sea para representar el tablero tridimensional en su visualización bidimensional). Un argumento adicional a favor de esta decisión fue que el movimiento de piezas "apiladas" no es el único tipo de viaje grupal. Por ejemplo, en el " Pentago " se pueden rotar los fragmentos del tablero junto con las piezas montadas sobre el mismo.


Resumiendo, puedo decir que, para mi marco de juego, decidí tomar todo lo mejor que se ha pensado en ZoG, Axiom y Ludi, y agregar lo que, en mi opinión, les falta.

Mover generación


La generación de movimientos es similar a la programación no determinista . La tarea del generador de movimientos es proporcionar, previa solicitud, una lista de todos los movimientos posibles desde la posición actual. Qué movimiento de esta lista será seleccionado por un jugador o la IA no es su función. Veamos cómo se realiza la generación de movimientos en ZoG. Como ejemplo, tomamos la macro de generación de movimiento para una pieza de largo alcance (una reina u obispo). Así es como se usa para determinar los movimientos de estas piezas:

 (piece (name Bishop) (image White "images\Chess\SHaag\wbishop.bmp" "images\Chess\wbishop.bmp" Black "images\Chess\SHaag\bbishop.bmp" "images\Chess\bbishop.bmp") (moves (slide ne) (slide nw) (slide se) (slide sw) ) ) 

Como parámetro, a una macro se le pasa la dirección del movimiento en el tablero. Si no considera la posibilidad de instalar nuevas piezas en el tablero, la generación de un movimiento parece simple. Para cada una de las piezas en el tablero, se calculan todos los movimientos posibles de acuerdo con las reglas. Entonces comienza la magia ...

¡Cada una de las definiciones puede agregar a la lista una serie de movimientos posibles! La adición de un movimiento a la lista se realiza con el comando agregar (al mismo tiempo, coloca cada pieza en movimiento en el tablero). Ya escribí sobre cómo esta solución arquitectónica es extremadamente pobre. El comando para la formación del movimiento debe estar separado de los comandos que manipulan piezas (como se hizo en Axiom). Veamos cómo funciona la macro:

 (define slide ( $1 (while empty? add $1 ) (verify not-friend?) add )) 


Primero, el desplazamiento es realizado por una celda, en la dirección dada, luego, en un ciclo, el espacio alcanzado se verifica por la ausencia de las piezas en él, se forma un movimiento y la disposición avanza a otra celda en la misma dirección. Si te detienes aquí, la pieza puede "deslizarse" a través de celdas vacías, pero ¿cómo puedes tomar piezas enemigas?

Muy simple! Después de ejecutar el comando verificar, la verificación de que el campo no está ocupado por una pieza amiga, formamos otro comando de agregar, completando el movimiento. Si en esta celda se ubicó una pieza enemiga, se tomará automáticamente (como en un espacio del tablero, al mismo tiempo, no puede tener más de una pieza). Si la pieza era amigable, el cálculo del movimiento se cancelará con la verificación del comando (la violación de las condiciones especificadas en este comando termina inmediatamente el cálculo del movimiento actual).

Tanto en ZoG como en Axiom uno puede mover solo las propias piezas (o más bien, es posible mover las piezas del oponente, pero solo si se especifica en el modo de cálculo de un movimiento de una de las propias piezas). Me parece una restricción extremadamente inconveniente, porque hay muchos juegos en los que puedes mover directamente la pieza del oponente (en " Stavropol Checkers ", por ejemplo). Sería más consistente realizar el cálculo del movimiento para todas las piezas, independientemente de su afiliación. En la macro que determina el movimiento, uno solo necesitaría agregar un cheque para permitir mover solo las propias piezas:

 (define slide ( (verify friend?) $1 (while empty? add $1 ) (verify not-friend?) add )) 


Importante es la capacidad de realizar un movimiento que consta de varios movimientos "parciales". En implementaciones de borradores, esta capacidad se utiliza para realizar capturas de "cadena":

 (define checker-jump ($1 (verify enemy?) capture $1 (verify empty?) (if (not-in-zone? promotion-zone) (add-partial jumptype) else (add-partial King jumptype) ) ) ) 


El comando de movimiento parcial se forma con add-partial (para este comando, así como para el comando add, hay una variación del movimiento, con "transformación" de las piezas). Tal movimiento es siempre parte de un movimiento más grande, "compuesto". Como regla general, para movimientos posteriores, se establece un "modo", que la continuación debe implementar. Por lo tanto, en las fichas, una captura puede continuar solo con las siguientes capturas, pero no con un movimiento "suave" (sin captura).

Nota
En ZoG, la implementación de movimientos parciales es deficiente. Intentar ejecutar el comando add-partial en un ciclo provoca un error. Como resultado, la captura realizada por un rey corrector solo se puede realizar de la siguiente manera muy incómoda:

 (define king-jump-1 ($1 (while empty? $1 ) (verify enemy?) capture $1 (verify empty?) (add-partial jumptype) ) ) (define king-jump-2 ($1 (while empty? $1 ) (verify enemy?) capture $1 (verify empty?) $1 (verify empty?) (add-partial jumptype) ) ) 

¡Y así sucesivamente, hasta king-jump-7! Permítame recordarle que en la mayoría de las variedades de damas con un rey de "largo alcance", el rey, después de cada captura, puede detenerse en cualquier espacio de una cadena continua de espacios vacíos que siguen a la pieza capturada. Por cierto, hay una variante de este juego en la que la regla de captura de "cadena" se formula de manera diferente. Eso es justo lo que me gusta de las damas: todos pueden encontrar una variante a su gusto.

Tal sistema de descripción de las reglas es muy flexible, pero a veces se requiere una lógica más compleja. Por ejemplo, si la pieza, durante el progreso "parcial" no debe volver a pasar a través de un campo atravesado previamente, es lógico utilizar las banderas asociadas con las posiciones en el tablero. Después de visitar un espacio, configuramos una bandera, por lo que posteriormente no debemos volver a este espacio:

 (verify (not-position-flag? my-flag)) (set-position-flag my-flag true) 

Además de las banderas "posicionales", en ZoG puede usar banderas globales. Estas capacidades no deben confundirse con los atributos de las piezas. A diferencia de este último, estos no son parte del estado del juego. Desafortunadamente, ambos atributos de piezas y banderas en ZoG solo pueden ser booleanos (en Axiom los atributos ni siquiera son compatibles). Esta limitación dificulta la realización de operaciones asociadas con los diversos tipos de conteo. Por ejemplo, en este pequeño rompecabezas, tuve que usar para "contar" piezas, atrapadas en un "tenedor", un par de banderas booleanas (el número exacto que no necesitaba, siempre que las piezas fueran más de una).

Otra cosa que solucionar es la falta de un claro "ciclo de vida" en la ejecución del movimiento. Todas las banderas se reinician automáticamente antes de comenzar el movimiento, pero sería más fácil identificar claramente la fase de inicialización. En mi opinión, en el cálculo del movimiento, deberían ocurrir las siguientes fases:

  1. Inicialización de variables y comprobación de condiciones previas para el movimiento compuesto.
  2. Inicialización de variables y verificación de precondiciones para el movimiento parcial.
  3. Generación del movimiento parcial.
  4. Comprobación de las condiciones posteriores del movimiento parcial
  5. Generando, completando y verificando las condiciones posteriores del movimiento compuesto
  6. Verificando las condiciones de terminación del juego

El grupo de pasos del segundo al cuarto, en el movimiento compuesto completo, puede repetirse muchas veces. La idea de las condiciones previas y posteriores, que llamo invariantes, la tomé del proyecto Ludi. Te cuento más sobre el uso de invariantes más adelante.

Sobre la importancia de la notación


La generación de todos los movimientos posibles desde la posición es solo la mitad de la historia. Para controlar el estado del juego se requiere una presentación compacta de los movimientos generados. En ZoG, para este propósito, se usa la notación ZSG. Aquí hay una descripción de un posible comienzo de un juego de ajedrez de esta forma:

 1. Pawn e2 - e4 1. Pawn e7 - e5 2. Knight g1 - f3 2. Knight b8 - c6 3. Bishop f1 - c4 3. Knight g8 - f6 4. King e1 - g1 Rook h1 - f1 @ f1 0 0 @ g1 0 0 4. Pawn d7 - d5 5. Pawn e4 x d5 5. Knight f6 x d5 

Este script está cerca de la notación de ajedrez habitual y generalmente es fácil de usar. Solo el cuarto movimiento de las blancas puede causar cierta confusión. Entonces en ZSG parece un enroque . La parte de la descripción del movimiento antes del personaje '@' es bastante clara; es el movimiento simultáneo de la torre y el rey, pero ¿qué sigue? Por lo tanto, en ZSG, parece que se requiere un restablecimiento de los atributos de las piezas para evitar la posibilidad de enroque repetido.

Nota
ZoG usa su notación ZSG particularmente para mostrar el curso del juego en una forma comprensible para el jugador. A la derecha del tablero, puede abrir una subventana "Lista de movimientos". Esta lista se puede usar para navegar por el juego grabado. Esta lista no es muy conveniente, porque no se admite una vista de árbol ramificado de juegos alternativos. La parte de los giros registrados asociados con cambios en los atributos de las piezas, no se muestra al usuario.

La grabación de un movimiento en notación ZSG debe contener información completa suficiente para cambiar correctamente el estado del juego. Si se pierde información sobre un cambio de atributos, en un juego de acuerdo con dicho registro, un movimiento podría repetirse incorrectamente (por ejemplo, el jugador tendría la oportunidad de volver a ejecutar el enroque). Desafortunadamente, en las extensiones DLL (como Axiom), la información extendida no se puede transmitir.

Al trabajar con extensiones DLL, ZoG se ve obligado a hacer una manipulación bastante astuta cuando se posiciona en un movimiento seleccionado (por ejemplo, cuando retrocede un movimiento). Desde [cada] posición anterior [trabajando desde el comienzo del juego], se generan todos los movimientos posibles, y luego, dentro de esa lista, uno debe buscar un movimiento con la representación ZSG [correspondiente]. Los [efectos secundarios de cada] movimiento generado se aplican a [cada uno de los sucesivos] estados del juego, ya que es posible realizar efectos secundarios no reflejados en la representación ZSG del movimiento.

La situación se ve agravada por el hecho de que la única forma de llegar al estado del juego en el momento de un movimiento en el pasado, es la aplicación consistente de todos los movimientos desde el comienzo del juego, hasta el estado inicial del tablero. En casos realmente complejos , este tipo de navegación no se produce rápidamente. Otra desventaja de la notación ZSG puede ilustrarse mediante la grabación del siguiente movimiento en el juego de Go :

 1. White Stone G19 x A19 x B19 x C19 x D19 x E19 x F19 

Aquí, en la posición G19, se coloca una piedra blanca que captura un grupo de piedras negras. Dado que todas las piezas involucradas en el rendimiento de la colocación deben mencionarse en el rendimiento ZSG, el registro del turno puede parecer muy largo (en Go, una gota puede capturar hasta 360 piedras). A lo que esto puede conducir, escribí antes . El tamaño del búfer asignado para grabar el movimiento ZoG puede no ser suficiente. Además, si por alguna razón el orden de eliminación de piedras cambia (en el proceso de desarrollo del juego sucede), un intento de aplicar un movimiento, desde un antiguo orden de capturas, fracasará.

Afortunadamente, hay una manera simple de lidiar con todos estos problemas. Veamos cómo definir movimientos de piezas en ZRF:

 (piece (name Pawn) (image White "images\Chess\SHaag\wpawn.bmp" "images\Chess\wpawn.bmp" Black "images\Chess\SHaag\bpawn.bmp" "images\Chess\bpawn.bmp") (moves (Pawn-capture nw) (Pawn-capture ne) (Pawn-move) (En-Passant e) (En-Passant w) ) ) 

Los nombres de movimientos, definidos en las macros de ZoG, son inaccesibles como generadores de movimientos. Pero, ¿qué nos impide renunciar a las macros y hacer descripciones de los movimientos con sus nombres? Así es como se vería el registro para un juego de ajedrez:

 1. e2 - e4 Pawn-move 1. e7 - e5 Pawn-move 2. g1 - f3 leap2 n nw 2. b8 - c6 leap2 n ne 3. f1 - c4 slide nw 3. g8 - f6 leap2 n nw 4. e1 - g1 OO 4. d7 - d5 Pawn-move 5. e4 x d5 Pawn-capture nw 5. f6 x d5 leap2 w nw 

Nota
Los lectores astutos pueden notar que en los movimientos para "negro" usé direcciones que no son apropiadas para las direcciones reales en el tablero de ajedrez. Esto está relacionado con el hecho de que las "simetrías" se definen para el negro:

 (symmetry Black (ns)(sn) (nw sw)(sw nw) (ne se)(se ne)) 

En términos generales, entonces, lo que para el blanco es "norte", para el negro es "sur", y viceversa.

Los beneficios de tal registro no son obvios, pero tiene una ventaja importante. Todos los movimientos se describen de manera uniforme y estas descripciones no contienen nada adicional (los nombres de las descripciones de los movimientos, por supuesto, podrían hacerse más "descriptivos"). En la descripción del enroque se logró deshacerse tanto de los cambios de atributos como de la descripción del movimiento de torre (esta descripción ya no depende de los detalles de implementación del movimiento). Existe una utilidad aún más clara de dichos registros en el caso del juego Go:

 1. G19 drop-to-empty White Stone 

Y eso es todo! Si las piedras del oponente se toman de acuerdo con las reglas del juego, no hay necesidad de enumerarlas todas en la descripción del movimiento. Es suficiente indicar el espacio de desplazamiento inicial y final (posiblemente con una señal para tomar), el nombre del movimiento en ejecución y la línea de parámetros que se le pasó. Por supuesto, para realizar un movimiento de acuerdo con esta descripción, para la decodificación, es necesario acceder al módulo de generación de movimientos, ¡pero ZoG lo hace!

Otra posibilidad, que debería admitirse, aparece en la funcionalidad de los movimientos "parciales". Aquí hay un ejemplo de " damas rusas ":

 1. Checker g3 - f4 1. Checker f6 - g5 2. Checker e3 - d4 2. partial 2 Checker g5 - e3 = XChecker on f4 2. Checker e3 - c5 = XChecker on d4 x d4 x f4 

Aquí los negros, en su segundo movimiento, toman dos piezas en d4 y f4. Una "transformación" preliminar de estas piezas a XChecker es una característica de esta implementación y sirve para evitar la recuperación de piezas "derrotadas" en el mismo movimiento. La frase "parcial 2" describe el comienzo del curso "compuesto", que consiste en dos movimientos "parciales". Esta forma de descripción es inconveniente, porque en el momento de la generación del primer movimiento, la longitud de la secuencia de movimientos "parciales" puede no ser conocida. Así es como se verá esta descripción en un nuevo formato:

 1. g3 - f4 checker-shift nw 1. f6 - g5 checker-shift ne 2. e3 - d4 checker-shift nw 2. + g5 - e3 checker-jump nw 2. + e3 - c5 checker-jump sw 2. + 

Los detalles de implementación relacionados con la "transformación" de piezas son irrelevantes. La captura de piezas tampoco se especifica, ya que en las fichas, la captura se produce como un "efecto secundario" del movimiento de la pieza y no de acuerdo con el "principio del ajedrez". El progreso parcial se codificará con el símbolo "+" al principio de la linea. Un "+" solitario indica la finalización de un "movimiento compuesto" (de hecho, este es el movimiento "parcial" habitual, que contiene un movimiento faltante, una cadena vacía).

Por lo tanto, utilizando reglas con nombre para la implementación de movimientos, uno ha logrado crear una notación universal, satisfaciendo completamente nuestros requisitos. Por supuesto, no tiene nada que ver ni con el ajedrez estándar ni con ninguna otra notación, pero da la casualidad de que la notación convencional para ajedrez, damas y otros juegos tampoco tienen nada que ver entre sí. El módulo de visualización siempre puede convertir el registro de movimiento en una forma más familiar aceptada para un juego en particular. La conversión también puede ser de alguna forma universal, como SGF (Smart Game Format) .

El ciclo de vida del juego.


Además de la información sobre la colocación de piezas en el tablero, la secuencia de turnos es una parte del estado del juego, una variable en el proceso del juego. En el caso más simple (y más común), para almacenar esta información un bit será suficiente, pero ZoG brinda algunas oportunidades más para implementar casos más complejos. ¡Así es como podría verse una descripción de una secuencia de movimientos para el juego Splut! :

 (players South West North East) (turn-order South West West repeat North North North East East East South South South West West West ) 

En este juego, cada jugador hace tres movimientos a la vez, pero si tuvieras la oportunidad de hacer tres movimientos desde la posición inicial, él podría destruir una de las piezas del oponente, lo que le daría un Ventaja significativa. Por esta razón, el primer jugador debe hacer un solo movimiento (le da la oportunidad de prepararse para atacar a un jugador contrario, pero no atacarlo), el segundo: dos movimientos (esto tampoco es suficiente para atacar a un jugador contrario), después de que cada jugador siempre hace tres movimientos.


La repetición de la etiqueta indica el comienzo de una secuencia de movimientos que se repite cíclicamente. Si no aparece, la descripción completa se repite cíclicamente. ZoG no permite que el uso de la etiqueta se repita más de una vez. Otra característica importante es la especificación del orden de giro. Así es como puede verse una descripción de la secuencia de turnos para un juego en el que cada jugador realiza dos turnos (el primer movimiento: mover piezas, el segundo: capturar piezas del oponente):

 (players White Black) (turn-order (White normal-move) (White capture-move) (Black normal-move) (Black capture-move) ) 

Hay una capacidad más asociada con la descripción de mover las piezas de otra persona, pero es muy incómodo de usar. El problema es que tal descripción no tiene alternativa. Si la descripción indica que el movimiento debe ser realizado por una pieza enemiga, ¡el jugador debe realizar este movimiento! En ZoG es imposible describir la elección de mover la pieza de él o de otra persona. Si se necesita dicha capacidad en un juego (como en " Stavropol Checkers "), es necesario que todas las piezas sean neutrales (creando para este propósito un jugador que no participe en el juego) y determinar para todos los jugadores la oportunidad para mover una pieza neutral. He dicho anteriormente que, por defecto, es mucho más fácil permitir a todos los jugadores la posibilidad de mover cualquier pieza (tanto la suya como la de su oponente) agregando las comprobaciones necesarias en los algoritmos de generación de movimiento.

Como puede ver, el rango de opciones proporcionadas por ZoG para la descripción de la secuencia de giros es extremadamente limitado. Axiom tampoco logra agregar nuevas funciones, ya que (generalmente) se ejecuta sobre ZoG. Ludi, a este respecto, es aún más pobre. Para maximizar la unificación de las reglas del juego (requerido para la posibilidad de usar algoritmos genéricos), en este proyecto, todas las capacidades descriptivas se han simplificado deliberadamente, lo que ha provocado la eliminación de capas enteras de juegos.


" Bao Swahili " es un buen ejemplo de un juego con un ciclo de vida complejo. En este juego, hay dos fases con reglas para la ejecución del movimiento que difieren significativamente. Al comienzo del juego, parte de las piedras está "en la mano "De cada jugador. Si bien todavía hay piedras" en la mano ", las piedras se colocan en pozos, una piedra a la vez. Cuando las piedras" en la mano "se agotan, comienza la segunda fase del juego, con la distribución de insertos No se puede decir que este juego no se pueda describir en ZRF (el lenguaje de descripción de ZoG), pero debido a las limitaciones de ZoG, esta implementación sería extremadamente confusa (lo que ciertamente no es lo mejor para la calidad del trabajo de IA). Veamos cómo se vería la descripción de tal juego en un "mundo ideal":

 (players South North) (turn-order (turn-order (South pi-move) (North pi-move) ) (label phase-ii) (turn-order (South p-ii-move) (North p-ii-move) ) ) 

Aquí, cada lista de orden de turno determina su secuencia repetitiva de movimientos (distinguiéndose por el modo de ejecución del movimiento). La etiqueta de palabra clave define una etiqueta a la que se puede hacer una transición durante la generación del último movimiento. Puede notar que aquí procedemos de la suposición implícita de que dicha transición siempre ocurre después del movimiento del segundo jugador (de lo contrario, violaría la secuencia de movimientos). ¿Cómo hacer la transición a la siguiente fase en un momento arbitrario?

 (players South North) (turn-order (turn-order (South pi-move) (North pi-move) ) (turn-order (labels - phase-ii) (South p-ii-move) (labels phase-ii -) (North p-ii-move) ) ) 

Aquí, las etiquetas se llevan en el cuerpo del bucle y comprenden dos nombres. Los nombres de las etiquetas en las listas de etiquetas aparecen en el orden de transferencia de jugadores en la lista de jugadores. El nombre utilizado para la transición está determinado por el jugador que realizó el último movimiento. Si este fuera el Norte, pasará a la primera etiqueta, de lo contrario, a la segunda. Si no se utiliza alguno de los nombres en las etiquetas, la posición correspondiente se puede llenar con un guión.


Un aspecto importante en el manejo de movimientos alternos, es la capacidad de realizar un turno repetido. En juegos de la familia de Tablas , como Nard , Backgammon o Ur , por ejemplo, la capacidad de realizar turnos repetidos es un elemento importante de las tácticas de juego. En ZoG se puede usar pasar un turno para emular esta característica, pero este enfoque complica significativamente la descripción del juego (especialmente con más jugadores). Sería mucho más lógico usar una etiqueta para repetir un turno:

 (players South North) (turn-order (label repeat) South (label repeat) North ) 

Una vez que el juego saltó a la repetición de la etiqueta, el jugador volverá a jugar su turno (la etiqueta más cercana a la posición actual en la lista de turnos tendrá efecto). Me gusta el enfoque de Perl en sus definiciones implícitas. La generación implícita de estructuras de control puede simplificar significativamente la descripción del juego. En la medida en que los movimientos repetidos se pueden usar en muchos juegos, las etiquetas se repiten, anticipando la posible repetición de cualquier turno puede estar implícito:

 (players South North) (turn-order South North ) 

Además, dado que la secuencia de turnos es totalmente consistente con el orden escrito de los jugadores en la construcción de jugadores, puedes generar automáticamente la frase completa del orden de turno:

 (players South North) 

Cuanto más fácil sea escribir la descripción, mejor.

Invariable rompible


Lo principal que no me gusta en ZoG se puede expresar con una palabra: checkmated. A primera vista, es solo una condición (muy común en los juegos de la familia del ajedrez ) que vincula el final del juego con la formación de una situación de compañero. Por desgracia, en un examen más detallado, la simplicidad se muestra engañosa. El uso de esta palabra clave significa no solo el rendimiento, después de cada movimiento, de una verificación para la finalización del juego, sino que también impone al jugador cierto "comportamiento".


Desde el Shogi habitual, este juego difiere solo en el número de jugadores. Desafortunadamente, esta diferencia es suficiente para hacer que el trabajo de determinar el jaque mate (y todo lo relacionado con esta palabra "mágica") sea incorrecto. La verificación de estar bajo control se realiza solo con relación a uno de los jugadores. ¡Como resultado, el rey puede ser atacado y ser comido [por una combinación de turnos de oponentes incluso cuando no se deja en "cheque"]! Que esto no sea óptimo se reflejará en el trabajo de la IA.

Si este problema parece insignificante, vale la pena recordar que las coaliciones generalmente se forman en juegos de cuatro jugadores "par contra par". ¡En el caso de la formación de coaliciones, debemos considerar que las piezas amigas del rey no lo amenazan! Entonces, por ejemplo, dos reyes amigos pueden residir en espacios vecinos del tablero.


Se vuelve más complicado que nunca si un jugador puede tener varios reyes. En " Ajedrez Tamerlán ", el peón real se convierte en un príncipe (en realidad, un segundo rey). Si esto sucede, solo puedes ganar capturando al primer rey (cualquiera de los dos) y emparejando al segundo. ¡En este juego, puedes obtener incluso un tercer rey, doble gasto en la transformación del "peón de peones"! Las capacidades expresivas de "checkmated" no son suficientes para describir adecuadamente esta situación.

Otra dificultad puede ser el proceso mismo de dar mate. Entonces, en el ajedrez mongol ( Shatar ), el resultado del intento de mate depende del orden en que las piezas ejecutan el "chequeo" secuencial. El resultado puede ser una victoria o un empate (como un compañero por un peón), o incluso una derrota (el compañero de caballo está prohibido, pero puedes dar el cheque). Un poco menos exótico, a este respecto, es el Shogi japonés. En este juego, está prohibido dar mate con un peón caído, pero puedes dar el cheque con un peón caído y dar mate con un peón movido.

Nota
Hay un punto más importante que vale la pena mencionar. En algunos juegos, como Rhythmomagic, puede haber varias formas diferentes de finalizar el juego. La forma más obvia de ganar, que implica la destrucción de las piezas del oponente, es también la menos preferida. Para una victoria más significativa, uno debe organizar las piezas en el territorio enemigo en un cierto patrón.

Uno debe distinguir entre los tipos de victorias (y derrotas y empates) en el nivel de descripción del juego, ya que el tipo de final del juego puede ser importante para el jugador. Además, debería ser posible asignar prioridades numéricas a los diversos finales del juego. Tras el cumplimiento simultáneo de varias condiciones de finalización, la que tiene la mayor prioridad debe contar.

Obviamente, uno debe separar la lógica de verificación del final del juego de la prueba de que el rey haya caído en jaque, que es una regla invariable que se verifica después de cada turno. La violación de la regla hace que sea imposible realizar el movimiento (el movimiento se elimina de la lista de movimientos disponibles). Entonces, una prueba (simplificada) para que un Rey esté bajo control podría verse así para el "ajedrez Tamerlán":

 (verify (or (> (count (pieces my? (is-piece? King))) 1) (= (count (pieces my? (is-piece? King) is-attacked?)) 0) ) ) 

Es importante entender que esta prueba debe llevarse a cabo solo para los propios reyes (utilicé el predicado mi, porque el predicado amigo, con apoyo para las coaliciones, se satisfará no solo para las propias piezas, sino también para el piezas de todos los jugadores amigos). Aceptable (y deseable, [si hay múltiples reyes amigos]) es la situación en la que el rey enemigo cae bajo control, después de un movimiento, pero por el propio rey. ¡Esta situación debería ser imposible [a menos que haya múltiples reyes amigos]! Habiendo brindado apoyo para verificar tales reglas, verificar la finalización del juego por jaque mate se vuelve trivial. Si no hay movimientos posibles y el [único] rey está bajo control, el juego termina [si ese rey pertenece al último jugador superviviente de la segunda última coalición superviviente]:

 (loss-condition (and (= (count moves) 0) (= (count (pieces my? (is-piece? King)) 1) (> (count (pieces my? (is-piece? King) is-attacked?)) 0) ) ) 

La capacidad de determinar invariantes será útil en otros juegos, como en las damas . La mayor dificultad en la implementación de juegos de esta familia, está vinculada a la implementación de la "regla de la mayoría". En casi todos los juegos de draft, la captura es obligatoria. Además, en la mayoría de los juegos de esta familia, hay una finalización característica de "capturas en cadena" en un solo turno. El verificador, habiendo capturado, continúa tomando otras piezas, si es posible. En la mayoría de los juegos, el jugador debe realizar capturas en cadena hasta el final, pero hay excepciones a esta regla, por ejemplo, Fanorona .


Usando el mecanismo de movimientos parciales, implementar una "captura en cadena" es bastante simple. Las dificultades surgen cuando, además, uno impone una condición bajo la cual, de todas las opciones posibles, uno debe elegir una cadena en la que se capture un número máximo de piezas. En ZoG, esta lógica debe implementarse desde cero al nivel de "hardcoding":

 (option "maximal captures" true) 

Esta configuración es adecuada para " damas internacionales ", pero en las " damas italianas " la regla de la mayoría se formula de manera diferente. En esta versión del juego, si hay varias opciones para la misma cantidad de capturas, debes seleccionar una opción que capture la mayor cantidad de fichas transformadas (reyes). Los desarrolladores de ZoG han proporcionado esto. Ingresa la siguiente configuración:

 (option "maximal captures" 2) 

En esta configuración, se cuenta no solo el número de piezas capturadas, sino también su tipo. Lamentablemente, no todo puede preverse. Así es como se formula la "regla de la mayoría" en las "antiguas fichas francesas":

Si mediante una serie de capturas es posible capturar la misma cantidad de fichas con un hombre simple o con un rey, el jugador debe usar el rey. Sin embargo, si el número de fichas es el mismo en ambos casos, pero en uno hay un rey enemigo (o hay más), el jugador debe elegir esta opción, incluso si la captura se realiza utilizando el simple comprobador, y no usando al rey

Por supuesto, en la actualidad, casi nadie juega esta versión de las damas, pero su existencia demuestra claramente las deficiencias de la implementación "codificada". El uso del mecanismo de invariantes permite todas las opciones posibles para la "regla de la mayoría" de manera universal. Para la implementación de los " antiguos corredores franceses " sería la siguiente:

 (verify (>= capturing-count max-capturing-count) ) (if (> capturing-count max-capturing-count) (let max-capturing-count capturing-count) (let max-capturing-sum capturing-sum) (let max-attacking-value attacking-value) ) (verify (>= capturing-sum max-capturing-sum) ) (if (> capturing-sum max-capturing-sum) (let max-capturing-sum capturing-sum) (let max-attacking-value attacking-value) ) (verify (>= attacking-value max-attacking-value) ) (let max-attacking-value attacking-value) 

Aquí, asumimos que las reglas para la generación de captura llenan correctamente [las siguientes] variables locales:

  • recuento de captura - total de piezas capturadas
  • suma de captura - número de reyes capturados
  • valor de ataque - valor de captura de pieza

Asociado con cada una de estas variables hay un acumulador de valores, almacenado en una variable con el prefijo max. Las tres verificaciones se ejecutan en serie. La violación de cualquiera de las condiciones de verificación interrumpe inmediatamente la generación de la siguiente opción de turno (la captura no se almacena en la lista de turnos posibles). Como las comprobaciones realizadas están asociadas a valores variables, no es suficiente [para probar solo la nueva opción de captura actual]. Cada prueba genera una "regla flexible" asociada con la captura generada [que puede revisar el valor máximo acumulado]. Después de cada cambio en cualquier acumulador, todas las reglas asociadas deben verificarse nuevamente [para cada opción en la lista]. Si se infringe alguna de las condiciones para una opción generada previamente, esa opción debe eliminarse de la lista de posibles opciones de giro.

Conclusión


Esta es la traducción de mi artículo del año 2014. Desde entonces, he repensado mucho y el proyecto Dagaz se ha convertido en una realidad, pero no cambié casi nada en el texto. Este artículo fue traducido por mi amigo Howard McCay y le estoy agradecido por el trabajo realizado.

Source: https://habr.com/ru/post/481868/


All Articles