3 puntos por GN⁺ 3 시간 전 | 1 comentarios | Compartir por WhatsApp
  • Las reglas de combate de Pokémon se parecen más a un motor de reglas en el que se entrelazan las ventajas de tipos, los movimientos, las estadísticas y las habilidades, por lo que pueden expresarse de forma concisa con el modelo de relaciones y reglas de Prolog
  • Prolog define hechos mediante predicados como pokemon/1 y type/2, y usa variables en mayúscula y unificación para encontrar Pokémon que cumplan condiciones de tipo o de movimientos
  • Para encontrar Pokémon que aprendan Freeze-Dry, sean de tipo Ice y tengan más de 120 de Special Attack, una consulta en Prolog es más corta que varios EXISTS en SQL
  • Los equipos de draft pueden representarse con predicados como alex/1 y morry/1, y a las reglas de movimientos con prioridad se les pueden ir agregando capas de condiciones de exclusión y efectos de Prankster
  • Las hojas de cálculo como Techno's Prep Doc son potentes, pero una base de datos de Prolog es más flexible para consultas de combinaciones arbitrarias, y se implementa con prologdex y Scryer Prolog

Por qué las reglas de combate de Pokémon encajan con la programación lógica

  • El combate de Pokémon se parece más a un motor de reglas donde múltiples reglas complejas se interconectan, y la programación lógica como Prolog sirve muy bien para expresar esas relaciones de forma concisa
  • Los Pokémon son personajes con nombre de especie, y hay más de 1,000 especies, desde Bulbasaur #1) hasta Pecharunt #1025)
  • En los combates de la serie principal, equipos de 6 Pokémon se enfrentan entre sí, y cada Pokémon normalmente elige uno de 4 movimientos que suelen causar daño al rival; gana quien logra reducir a 0 todos los HP del equipo contrario
  • El rendimiento en combate depende de las estadísticas base, la lista de movimientos que puede aprender, la habilidad y el tipo, y como hay muchísimas combinaciones, vale la pena seguirlas con software
  • El tipo se aplica tanto a los movimientos como a los Pokémon, y si el tipo de un movimiento es fuerte contra el tipo del rival, hace 2x de daño; si es débil, hace 1/2x de daño
  • Los modificadores de tipo se acumulan
    • Scizor) es de tipo Bug/Steel, y como ambos son débiles a Fire, recibe 4x de daño de los movimientos Fire
    • Si usas un movimiento Electric contra Swampert) de tipo Water/Ground, el daño pasa a ser 0 por la inmunidad de Ground

Modelo básico de Prolog

  • En Prolog, las relaciones se declaran como predicados (predicate)
pokemon(bulbasaur).
pokemon(ivysaur).
pokemon(venusaur).
pokemon(charmander).
pokemon(charmeleon).
pokemon(charizard).
pokemon(squirtle).
pokemon(wartortle).
pokemon(blastoise).
  • pokemon/1 es un predicado llamado pokemon con un argumento, y una consulta como pokemon(squirtle). verifica si puede hacerse verdadera esa oración
?- pokemon(squirtle).
   true.

?- pokemon(alex).
   false.
  • Los tipos de Pokémon pueden expresarse como una relación de dos argumentos como type/2, y si un Pokémon tiene dos tipos, se definen dos hechos type para ese mismo Pokémon
type(bulbasaur, grass).
type(bulbasaur, poison).
type(charmander, fire).
type(charizard, fire).
type(charizard, flying).
type(squirtle, water).
  • Los nombres que empiezan con mayúscula son variables, y Prolog intenta unificar una consulta con variables con todos los valores posibles
?- type(squirtle, Type).
   Type = water.

?- type(venusaur, Type).
   Type = grass
;  Type = poison.
  • Si dejas el primer argumento como variable, como en type(Pokemon, grass)., puedes encontrar todos los Pokémon de tipo Grass, y en los datos reales aparecen 164 resultados
  • La coma significa que deben cumplirse varios predicados a la vez, y el mismo nombre de variable debe tener el mismo valor dentro de la consulta
?- type(Pokemon, water), type(Pokemon, ice).
   Pokemon = dewgong
;  Pokemon = cloyster
;  Pokemon = lapras
;  Pokemon = laprasgmax
;  Pokemon = spheal
;  Pokemon = sealeo
;  Pokemon = walrein
;  Pokemon = arctovish
;  Pokemon = ironbundle
;  false.
  • También pueden consultarse como relaciones las estadísticas y los movimientos que puede aprender un Pokémon como Iron Bundle)
?- pokemon_spa(ironbundle, SpA).
   SpA = 124.

?- learns(ironbundle, Move), move_category(Move, special).
   Move = aircutter
;  Move = blizzard
;  Move = chillingwater
;  Move = freezedry
;  Move = hydropump
;  Move = hyperbeam
;  Move = icebeam
;  Move = icywind
;  Move = powdersnow
;  Move = swift
;  Move = terablast
;  Move = waterpulse
;  Move = whirlpool.
  • Si mezclas restricciones como SpA #> 120, puedes encontrar de inmediato los Pokémon que tienen más de 120 de Special Attack, aprenden Freeze-Dry y además son de tipo Ice
?- pokemon_spa(Pokemon, SpA), SpA #> 120, learns(Pokemon, freezedry), type(Pokemon, ice).
   Pokemon = glaceon, SpA = 130
;  Pokemon = kyurem, SpA = 130
;  Pokemon = kyuremwhite, SpA = 170
;  Pokemon = ironbundle, SpA = 124
;  false.
  • Las reglas (rule) de Prolog se componen de una cabeza y un cuerpo, y si el cuerpo es verdadero, la cabeza también se unifica
damaging_move(Move) :-
  move_category(Move, physical)
; move_category(Move, special).
  • Esta regla clasifica directamente como movimientos de daño a los movimientos Physical o Special
?- damaging_move(tackle).
   true.
?- damaging_move(rest).
   false.

Expresiones de consulta comparadas con SQL

  • Hasta ahora, los ejemplos son lógicamente combinaciones simples de and y or, pero en Prolog las consultas relacionales terminan siendo más cortas y más fáciles de modificar que en SQL
  • Si se organizan los mismos datos en SQL, se pueden tener tablas separadas para Pokémon, tipos y movimientos
CREATE TABLE pokemon (pokemon_name TEXT, special_attack INTEGER);
CREATE TABLE pokemon_types(pokemon_name TEXT, type TEXT);
CREATE TABLE pokemon_moves(pokemon_name TEXT, move TEXT, category TEXT);
  • Para encontrar en SQL Pokémon de tipo Ice, con Special Attack mayor a 120 y que aprendan Freeze-Dry, hay que usar EXISTS varias veces
SELECT DISTINCT pokmeon, special_attack
FROM pokemon as p
WHERE
  p.special_attack > 120
  AND EXISTS (
    SELECT 1
    FROM pokemon_moves as pm
    WHERE p.pokemon_name = pm.pokemon_name AND move = 'freezedry'
  )
  AND EXISTS (
    SELECT 1
    FROM pokemon_types as pt
    WHERE p.pokemon_name = pt.pokemon_name AND type = 'ice'
  );
  • La misma consulta en Prolog simplemente enumera las relaciones necesarias
?- pokemon_spa(Pokemon, SpA),
SpA #> 120,
learns(Pokemon, freezedry),
type(Pokemon, ice).
  • A medida que se siguen agregando condiciones, las consultas SQL tienden a volverse complejas, pero las consultas de Prolog mantienen una forma fácil de leer y modificar una vez que uno se acostumbra a cómo funcionan las variables

Forma de apilar reglas de combate por capas

  • En los combates Pokémon existen muchas reglas de interacción, como fallos de precisión, subidas y bajadas de estadísticas, efectos de objetos, rangos de daño, cambios de estado, efectos de campo como clima, terreno y Trick Room, habilidades y distribución previa de estadísticas
  • Al crear software para Pokémon, hay que manejar esta complejidad mientras se mantiene el modelo en una forma manejable
  • Prolog destaca en un modelo de consultas que describe combinaciones sobre la marcha y en el apilamiento consistente de reglas
  • Se puede comprobar esta complejidad directamente con el damage calculator

Ligas draft y consultas de movimientos con prioridad

  • En los drafts de Pokémon, cada Pokémon tiene un valor asignado, y los jugadores arman equipos de unas 8 a 11 criaturas eligiéndolas dentro de un límite de puntos
  • Como el combate real es 6v6, es importante prepararse para las posibles combinaciones de seis Pokémon que el rival puede traer y elegir los seis que mejor puedan responderles
  • Los Pokémon que uno seleccionó se pueden representar directamente con un predicado como alex/1
alex(meowscarada).
alex(weezinggalar).
alex(swampertmega).
alex(latios).
alex(volcarona).
alex(tornadus).
alex(politoed).
alex(archaludon).
alex(beartic).
alex(dusclops).
  • La consulta para encontrar Pokémon de este equipo que aprendan Freeze-Dry es simple, pero no devuelve resultados
?- alex(Pokemon), learns(Pokemon, freezedry).
   false.
  • El orden de combate lo determina normalmente la Speed, pero los movimientos tienen prioridad (priority), y los movimientos con prioridad más alta salen antes
  • La mayoría de los movimientos tienen prioridad 0, pero un movimiento con prioridad 1 como Accelerock sale antes que un movimiento de prioridad 0 de un Pokémon más rápido
  • Los movimientos con prioridad positiva que aprende un Pokémon específico se pueden encontrar combinando learns/2, move_priority/2 y una condición sobre la prioridad
  • Una consulta simple también incluye movimientos más relevantes en Double Battles, como Helping Hand y Ally Switch, o movimientos con poca relevancia práctica como Bide
  • \+/1 es verdadero cuando una meta falla, y dif/2 significa que dos términos son distintos, así que se puede agregar una regla para excluir movimientos de Double Battles y Bide
learns_priority(Mon, Move, Priority) :-
  learns(Mon, Move),
  \+ doubles_move(Move),
  dif(Move, bide),
  move_priority(Move, Priority),
  Priority #> 0.
  • Si además se excluyen movimientos defensivos como Protect, Detect, Endure y Magic Coat, solo quedan movimientos con prioridad que realmente pueden dañar al rival o aplicarle efectos negativos
?- alex(Pokemon), learns_priority(Pokemon, Move, Priority).
   Pokemon = meowscarada, Move = quickattack, Priority = 1
;  Pokemon = meowscarada, Move = suckerpunch, Priority = 1
;  Pokemon = beartic, Move = aquajet, Priority = 1
;  Pokemon = dusclops, Move = shadowsneak, Priority = 1
;  Pokemon = dusclops, Move = snatch, Priority = 4
;  Pokemon = dusclops, Move = suckerpunch, Priority = 1
;  false.
  • Si se aplica la misma regla al predicado del equipo rival, también se pueden encontrar de inmediato los movimientos con prioridad del oponente
?- morry(Pokemon), learns_priority(Pokemon, Move, Priority).
   Pokemon = mawilemega, Move = snatch, Priority = 4
;  Pokemon = mawilemega, Move = suckerpunch, Priority = 1
;  Pokemon = walkingwake, Move = aquajet, Priority = 1
;  Pokemon = ursaluna, Move = babydolleyes, Priority = 1
;  Pokemon = lokix, Move = feint, Priority = 2
;  Pokemon = lokix, Move = firstimpression, Priority = 2
;  Pokemon = lokix, Move = suckerpunch, Priority = 1
;  Pokemon = alakazam, Move = snatch, Priority = 4
;  Pokemon = skarmory, Move = feint, Priority = 2
;  Pokemon = froslass, Move = iceshard, Priority = 1
;  Pokemon = froslass, Move = snatch, Priority = 4
;  Pokemon = froslass, Move = suckerpunch, Priority = 1
;  Pokemon = dipplin, Move = suckerpunch, Priority = 1.

Ampliación de la habilidad Prankster

  • Los Pokémon con la habilidad Prankster reciben un +1 adicional a la prioridad de los movimientos de estado, y este efecto también puede sumarse a la regla existente learns_priority/3
  • Dentro del equipo, Tornadus tiene la habilidad Prankster
?- alex(Pokemon), pokemon_ability(Pokemon, prankster).
   Pokemon = tornadus
;  false.
  • Usando la sintaxis if/then ->/2 de Prolog, se puede hacer que, si el Pokémon tiene Prankster y la categoría del movimiento es status, se sume 1 a la prioridad base; de lo contrario, se use la prioridad base tal cual
learns_priority(Mon, Move, Priority) :-
  learns(Mon, Move),
  \+ doubles_move(Move),
  \+ protection_move(Move),
  Move \= bide,
  move_priority(Move, BasePriority),
  (
    pokemon_ability(Mon, prankster), move_category(Move, status) ->
      Priority #= BasePriority + 1
    ; Priority #= BasePriority
  ),
  Priority #> 0.
  • Después de esta regla, la misma consulta incluye movimientos de estado de Tornadus como Agility, Defog, Nasty Plot, Rain Dance, Tailwind, Taunt y Toxic con prioridad 1
  • Al ampliar una sola regla para reflejar también el efecto de una habilidad, se hace evidente la ventaja de la composición por capas en Prolog

Contraste con herramientas basadas en hojas de cálculo

  • En la comunidad de Pokémon ya existen recursos para encontrar información como los movimientos con prioridad del equipo rival; un ejemplo representativo son hojas avanzadas de Google Sheets como “Techno’s Prep Doc”
  • Esta hoja genera mucha información de matchup al ingresar un equipo, y ofrece soporte para varios formatos, materiales visuales fáciles de revisar y autocompletado
  • La fórmula para encontrar movimientos con prioridad combina FILTER, VLOOKUP e INDIRECT, y INDIRECT devuelve una referencia de celda
={IFERROR(ARRAYFORMULA(VLOOKUP(FILTER(INDIRECT(Matchup!$S$3&"!$AV$4:$AV"),INDIRECT(Matchup!$S$3&"!$AT$4:$AT")="X"),{Backend!$L$2:$L,Backend!$F$2:$F},2,FALSE))),IFERROR(FILTER(INDIRECT(Matchup!$S$3&"!$AW$4:$AW"),INDIRECT(Matchup!$S$3&"!$AT$4:$AT")="X"))}
  • La hoja Backend enumera todos los movimientos, y esta estructura se parece más a una versión con consultas de Prolog hardcodeadas
  • La base de datos de Prolog es más escalable que un enfoque de hardcodear una lista de movimientos destacados, y permite consultar cualquier movimiento
  • También se pueden expresar de forma breve preguntas combinatorias que las herramientas existentes no contemplan, como encontrar movimientos Special que aprende Tornadus y que sean superefectivos contra miembros del equipo de Justin
?- justin(Target), learns(tornadus, Move), super_effective_move(Move, Target), move_category(Move, special).
   Target = charizardmegay, Move = chillingwater
;  Target = terapagosterastal, Move = focusblast
;  Target = alomomola, Move = grassknot
;  Target = scizor, Move = heatwave
;  Target = scizor, Move = incinerate
;  Target = runerigus, Move = chillingwater
;  Target = runerigus, Move = darkpulse
;  Target = runerigus, Move = grassknot
;  Target = runerigus, Move = icywind
;  Target = screamtail, Move = sludgebomb
;  Target = screamtail, Move = sludgewave
;  Target = trapinch, Move = chillingwater
;  Target = trapinch, Move = grassknot
;  Target = trapinch, Move = icywind
;  false.
  • La tabla de efectividades de tipos está codificada en type-chart.pl
  • Para crear una webapp que haga cosas que las herramientas actuales de Pokémon no pueden, queda pendiente la tarea de compilar scryer-prolog a WASM

Notas de implementación y limitaciones

1 comentarios

 
GN⁺ 3 시간 전
Comentarios en Lobste.rs
  • Me pregunto si hay alguien que use Prolog de forma realmente productiva. Puede ser para trabajo o para uso personal; hasta ahora solo he visto ejemplos de juguete como este

    • Me gusta bastante Prolog y, como probablemente soy la persona que hizo la implementación del language server de Prolog más usada, lo uso mucho para scripts pequeños.
      En sentido estricto, incluso tengo al menos una pieza de código Prolog en producción: un dashboard interno de análisis, y antes también escribí en Prolog el backend de una app de iOS. En ese proceso terminé haciendo una librería cliente HTTP/2 para Prolog porque quería enviar notificaciones APNS sin depender de servicios externos
    • No sé si esto cuente como una respuesta satisfactoria, pero después de escribir esta entrada del blog empecé a sacar Prolog para problemas de una categoría completamente distinta a la de mi código anterior.
      En la comunidad de Prolog sin duda hay gente a la que le gusta usarlo para cosas como servidores web, pero para mí se parece más a desbloquear otro árbol de habilidades. Por ejemplo, te ayuda a reconocer mejor cuándo a un problema le vendría bien un parser a medida o un DSL.
      Con lo que aprendí al escribir este artículo, reimplementé un subconjunto útil del motor de lógica fiscal de IRS Fact Graph. Prolog encaja sorprendentemente bien en este trabajo, porque saca a la luz rincones no documentados que en una implementación imperativa sería fácil pasar por alto, y te obliga a resolverlos.
      La parte de “ejecución” todavía no la termino, pero el parsing ya está lo bastante avanzado como para haber podido escribir un borrador de documentación bastante decente. Falta una funcionalidad grande, la aritmética de fechas, y cuando eso esté listo pienso publicarlo por separado.
      Con DCG, Prolog es excelente para parsear texto estructurado complejo. Antes, cuando awk ya no alcanzaba, pensaba en Python o JS, pero ahora Prolog me parece una mejor opción cuando hace falta estructura y disciplina. También se siente bien ese estilo algo anticuado de tener un codebase complejo dentro de un solo archivo, y no es excesivamente críptico como APL.
      El ejemplo en sí es trivial, pero el caso de Pokémon no lo es. La mayoría de los ejemplos que parecen simples solo fueron posibles porque ya existe código que implementa de forma muy exhaustiva unas mecánicas de combate ridículamente complejas. Me interesa construir un motor de reglas en Prolog que haga parte de lo que hacen las herramientas existentes, y lo he intentado poco a poco; la ventaja es que, frente al código imperativo, con búsqueda en profundidad es más fácil exponer posibilidades y mantenerlo
    • Lo he usado a veces para análisis de programas. Hay bastantes herramientas que guardan información del programa en Datalog y te dejan escribir consultas encima, pero a veces uso herramientas que ofrecen SWI-Prolog real para aprovechar backtracking o funciones
    • Mi blog se genera con Prolog, y el código fuente también está publicado.
      La documentación de Scryer Prolog también se genera con un programa en Prolog que llamo DocLog. También hice algunas librerías que usan esos programas. SWI Prolog también usa SWI directamente en su propio sitio web, en el IDE web Swish y en el servidor ClioPatria
  • Alguna vez intenté organizar información usando SQLite como una especie de hoja de cálculo con seguridad de tipos. Funcionó incluso sin tener ninguna interfaz CRUD encima.
    Aun así, no siempre era el lenguaje más agradable de ver. También estuve revisando Datalog por un tiempo, pero la mayoría de las implementaciones parecían orientadas a integrarse en programas más grandes, más que a servir como una herramienta sencilla para registrar información, como en este artículo.
    Tal vez Prolog sí sea la herramienta que de verdad debería estar usando

    • Como Datalog en forma de base de datos externa están Mangle y Datomic, así que podría encajar, aunque no los he usado personalmente.
      Prolog es un superconjunto de Datalog, así que puede hacer todo lo que hace Datalog y más. Pero a veces ese “más” es precisamente el problema: ya es un lenguaje Turing completo, así que puede no terminar; el razonamiento puede volverse un poco más difícil; y también puede usarse de maneras inseguras.
      Datalog, al tener restricciones, puede aprovechar atajos y por eso a menudo rinde mejor.
      Por último, en Prolog los datos son lo mismo que el código, es decir, tiene homoiconicidad, así que las operaciones de crear/modificar/eliminar terminan siendo básicamente cambios al código fuente. Se puede hacer dinámicamente, pero no esperaría un flujo especialmente fluido, y todavía hace falta cierto grado de sincronización manual entre los archivos y el programa cargado
  • Me gustaría aprender Prolog más a fondo para usarlo en consultas sobre conjuntos de instrucciones.
    Pero modelar lógica, incluyendo cantidades y enteros vistos a nivel de bits, es bastante difícil