- As regras de batalha de Pokémon são mais próximas de um motor de regras em que se entrelaçam vantagens e desvantagens de tipo, golpes, atributos e habilidades, então podem ser expressas de forma concisa com o modelo de relações e regras do Prolog
- O Prolog registra fatos com predicados como
pokemon/1 e type/2, e usa variáveis em maiúsculas e unificação para encontrar Pokémon que satisfaçam condições de tipo e de golpes
- Para encontrar Pokémon que aprendem Freeze-Dry, são do tipo Ice e têm Special Attack maior que 120, uma consulta em Prolog é mais curta do que vários
EXISTS em SQL
- Times de draft podem ser representados com predicados como
alex/1 e morry/1, e regras de golpes de prioridade podem receber em camadas condições de exclusão e o efeito de Prankster
- Planilhas como o Techno's Prep Doc são poderosas, mas um banco de dados em Prolog é mais flexível para consultas de combinações arbitrárias, e isso é implementado com prologdex e Scryer Prolog
Por que as regras de batalha de Pokémon combinam com programação lógica
- As batalhas de Pokémon são mais parecidas com um motor de regras em que várias regras se encaixam de forma complexa, e programação lógica como Prolog é ótima para expressar essas relações de maneira concisa
- Pokémon são personagens com nomes de espécie, e existem mais de 1.000 espécies, de Bulbasaur #1) até Pecharunt #1025)
- Nas batalhas da série principal, times de 6 Pokémon lutam entre si, cada Pokémon normalmente escolhe um entre 4 golpes que em geral causam dano ao oponente, e vence quem reduzir o HP de todo o time adversário a 0
- O desempenho em batalha varia conforme atributos base, lista de golpes que pode aprender, habilidade, e tipo, e como o número de combinações é grande, vale a pena acompanhar isso por software
- Tipos são atribuídos tanto a golpes quanto a Pokémon, e se o tipo de um golpe é forte contra o tipo do alvo ele causa dano em dobro; se for fraco, causa metade do dano
- Os modificadores de tipo se acumulam
- Scizor) é do tipo Bug/Steel, e como ambos são fracos contra Fire, ele recebe 4× de dano de golpes Fire
- Se você usar um golpe Electric em Swampert), que é Water/Ground, o dano será 0 por causa da imunidade do tipo Ground
Modelo básico de Prolog
- Em Prolog, relações são declaradas como predicados (predicate)
pokemon(bulbasaur).
pokemon(ivysaur).
pokemon(venusaur).
pokemon(charmander).
pokemon(charmeleon).
pokemon(charizard).
pokemon(squirtle).
pokemon(wartortle).
pokemon(blastoise).
pokemon/1 é um predicado chamado pokemon com um argumento, e uma consulta como pokemon(squirtle). verifica se é possível tornar essa sentença verdadeira
?- pokemon(squirtle).
true.
?- pokemon(alex).
false.
- O tipo de um Pokémon pode ser expresso como uma relação de dois argumentos, como
type/2, e um Pokémon com dois tipos recebe dois fatos type para o mesmo Pokémon
type(bulbasaur, grass).
type(bulbasaur, poison).
type(charmander, fire).
type(charizard, fire).
type(charizard, flying).
type(squirtle, water).
- Nomes que começam com letra maiúscula são variáveis, e o Prolog tenta unificar consultas com variáveis com todos os valores possíveis
?- type(squirtle, Type).
Type = water.
?- type(venusaur, Type).
Type = grass
; Type = poison.
- Se você usar uma variável no primeiro argumento, como em
type(Pokemon, grass)., pode encontrar todos os Pokémon do tipo Grass; nos dados reais, isso retorna 164 resultados
- A vírgula significa que vários predicados precisam ser satisfeitos ao mesmo tempo, e o mesmo nome de variável precisa ter o mesmo valor dentro da 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.
- Como no caso de Iron Bundle), também é possível consultar relações sobre atributos e golpes que um Pokémon pode aprender
?- 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.
- Ao misturar restrições como
SpA #> 120, dá para encontrar diretamente Pokémon com Special Attack maior que 120, que aprendem Freeze-Dry e são do 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.
- As regras (rule) em Prolog são compostas por cabeça e corpo, e se o corpo for verdadeiro, a cabeça também é unificada
damaging_move(Move) :-
move_category(Move, physical)
; move_category(Move, special).
- Essa regra classifica diretamente golpes Physical ou Special como golpes que causam dano
?- damaging_move(tackle).
true.
?- damaging_move(rest).
false.
Expressão de consultas em comparação com SQL
- Os exemplos até aqui são, do ponto de vista lógico, combinações simples de
and e or, mas no Prolog as consultas de relações ficam mais curtas e fáceis de modificar do que em SQL
- Se os mesmos dados forem estruturados em SQL, Pokémon, tipos e movimentos podem ficar em tabelas separadas
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 em SQL Pokémon que aprendem Freeze-Dry, são do tipo Ice e têm Special Attack maior que 120, é preciso usar
EXISTS várias vezes
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'
);
- A mesma consulta em Prolog apenas lista diretamente as relações necessárias
?- pokemon_spa(Pokemon, SpA),
SpA #> 120,
learns(Pokemon, freezedry),
type(Pokemon, ice).
- À medida que condições continuam sendo adicionadas, consultas SQL tendem a ficar complexas, mas consultas em Prolog mantêm uma forma fácil de ler e editar quando você se acostuma com o funcionamento das variáveis
Forma de empilhar regras de batalha em camadas
- Nas batalhas de Pokémon há muitas regras de interação, como erros de precisão, aumentos e reduções de status, efeitos de itens, variação do dano, condições de status, efeitos de campo como clima, terreno e Trick Room, habilidades e distribuição prévia de status
- Ao criar software para Pokémon, é preciso lidar com essa complexidade mantendo o modelo em uma forma administrável
- O Prolog tem pontos fortes em um modelo de consulta que descreve combinações ad hoc e em uma sobreposição consistente de camadas de regras
- Você pode verificar essa complexidade diretamente no damage calculator
Liga draft e consultas de movimentos com prioridade
- No draft de Pokémon, cada Pokémon tem um valor definido, e os jogadores montam equipes de cerca de 8 a 11 Pokémon escolhendo dentro de uma quantidade fixa de pontos
- Como a batalha real é 6v6, é importante se preparar para as combinações de seis Pokémon que o oponente pode trazer e escolher os seis que vão enfrentá-los
- Os Pokémon que você escolheu podem ser representados diretamente com um predicado como
alex/1
alex(meowscarada).
alex(weezinggalar).
alex(swampertmega).
alex(latios).
alex(volcarona).
alex(tornadus).
alex(politoed).
alex(archaludon).
alex(beartic).
alex(dusclops).
- A consulta para encontrar Pokémon desse time que aprendem Freeze-Dry é simples, mas não há resultados
?- alex(Pokemon), learns(Pokemon, freezedry).
false.
- A ordem da batalha é determinada basicamente pela Speed, mas os movimentos têm prioridade (priority), e movimentos com prioridade mais alta saem primeiro
- A prioridade da maioria dos movimentos é 0, mas um movimento com prioridade 1, como Accelerock, age antes de um movimento de prioridade 0 de um Pokémon mais rápido
- Os movimentos com prioridade positiva que um certo Pokémon aprende podem ser encontrados combinando
learns/2, move_priority/2 e a condição de prioridade
- Uma consulta simples inclui até movimentos cujo significado é maior em Double Battles, como Helping Hand e Ally Switch, ou movimentos de pouco valor prático, como Bide
\+/1 é verdadeiro quando um objetivo falha, e dif/2 significa que dois termos são diferentes, então é possível adicionar regras para excluir movimentos de Double Battles e Bide
learns_priority(Mon, Move, Priority) :-
learns(Mon, Move),
\+ doubles_move(Move),
dif(Move, bide),
move_priority(Move, Priority),
Priority #> 0.
- Se também forem excluídos movimentos defensivos como Protect, Detect, Endure e Magic Coat, restam apenas os movimentos com prioridade que de fato podem causar dano ou efeitos negativos ao oponente
?- 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.
- Aplicando a mesma regra ao predicado do time adversário, também é possível encontrar imediatamente os movimentos com prioridade que o oponente tem
?- 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.
Expansão da habilidade Prankster
- Pokémon com a habilidade Prankster recebem +1 adicional na prioridade de movimentos de status, e esse efeito também pode ser somado à regra
learns_priority/3 existente
- Dentro do time, Tornadus tem a habilidade Prankster
?- alex(Pokemon), pokemon_ability(Pokemon, prankster).
Pokemon = tornadus
; false.
- Usando a sintaxe if/then
->/2 do Prolog, dá para fazer com que, se o Pokémon tiver Prankster e a categoria do movimento for status, 1 seja somado à prioridade base; caso contrário, a prioridade base é mantida
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.
- Depois dessa regra, a mesma consulta passa a incluir movimentos de status de Tornadus como Agility, Defog, Nasty Plot, Rain Dance, Tailwind, Taunt e Toxic com prioridade 1
- Ao estender uma única regra para refletir também o efeito da habilidade, fica evidente a vantagem de composição em camadas do Prolog
Contraste com ferramentas baseadas em planilha
- A comunidade de Pokémon já tem recursos para encontrar informações como movimentos prioritários do time adversário; um exemplo representativo são planilhas avançadas do Google Sheets como “Techno’s Prep Doc”
- Essa planilha gera muitas informações de matchup quando você insere os times, além de oferecer suporte a vários formatos, recursos visuais fáceis de escanear e preenchimento automático
- A fórmula para encontrar movimentos prioritários combina
FILTER, VLOOKUP e INDIRECT, e INDIRECT retorna uma referência de célula
={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"))}
- A aba Backend lista todos os movimentos, e essa estrutura se aproxima de uma versão com consultas Prolog hardcoded
- O banco de dados em Prolog é mais escalável do que uma abordagem que faz hardcode de uma lista de movimentos notáveis, e permite consultar qualquer movimento
- Também é possível expressar de forma curta perguntas combinatórias que não existem nas ferramentas atuais, como encontrar movimentos Special que Tornadus aprende e que são super effective contra membros do time do 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.
Notas de implementação e limitações
1 comentários
Comentários do Lobste.rs
Fico curioso se existe gente usando Prolog de forma realmente produtiva. Tanto faz se no trabalho ou para uso pessoal; até agora só vi exemplos de brinquedo assim
Estritamente falando, também tenho pelo menos um código em Prolog rodando em produção: um dashboard interno de análise. No passado, também escrevi em Prolog o backend de um app para iOS. Nesse processo, acabei criando uma biblioteca cliente de HTTP/2 para Prolog para enviar notificações APNS sem depender de serviços externos
Sem dúvida há gente na comunidade Prolog que gosta de usá-lo para coisas como servidores web, mas, para mim, é mais como desbloquear outra árvore de habilidades. Por exemplo, passei a perceber melhor quando um parser sob medida ou uma DSL combinam bem com um problema.
Com o que aprendi escrevendo esse texto, reimplementei um subconjunto útil do motor de lógica tributária do IRS Fact Graph. O Prolog se encaixa nisso de forma surpreendentemente boa, porque expõe e força a resolver cantos não documentados que uma implementação imperativa poderia simplesmente deixar passar.
A parte de “execução” ainda não está pronta, mas o parsing já avançou o suficiente para eu conseguir escrever um rascunho de documentação bem decente. Falta uma funcionalidade grande, aritmética de datas, e quando isso entrar pretendo organizar tudo separadamente.
Com DCG, Prolog é excelente para fazer parsing de texto estruturado complexo. Antes, quando awk não dava conta, eu pensava em Python ou JS, mas agora Prolog me parece adequado quando é preciso estrutura e disciplina. Também é satisfatório aquele ar antiquado de trabalhar com uma base de código complexa que cabe em um único arquivo, sem ser exageradamente abreviado como APL.
O exemplo em si é trivial, mas o caso de Pokémon não é. A maioria dos exemplos aparentemente simples só foi possível porque já existe um código que implementa de forma extremamente completa mecânicas de batalha absurdamente complexas. Tenho interesse em criar um motor de regras em Prolog que execute parte do que as ferramentas existentes fazem, e venho tentando isso aos poucos; a vantagem é que, em comparação com código imperativo, a busca em profundidade torna mais fácil revelar possibilidades e manter o sistema
A documentação do Scryer Prolog também é gerada por um programa em Prolog que chamo de DocLog. Também criei algumas bibliotecas usadas por esses programas. O próprio SWI Prolog usa SWI diretamente em seu site, na web IDE chamada Swish, no servidor ClioPatria etc.
Já organizei informações usando SQLite como uma espécie de planilha com segurança de tipos. Isso funcionava mesmo sem nenhuma interface CRUD por cima.
Ainda assim, nem sempre parecia a linguagem mais agradável de se ver. Também examinei Datalog por um tempo, mas a maioria das implementações parecia mais voltada a ser embutida em programas maiores do que a servir como ferramenta prática para registrar informações, como neste post.
Talvez Prolog seja, de fato, a ferramenta que eu deveria usar
Prolog é um superconjunto de Datalog, então consegue fazer tudo o que Datalog faz e ainda mais. Mas às vezes é justamente esse “mais” que vira problema: agora é uma linguagem Turing completa, então pode não terminar nunca, a inferência pode ficar um pouco mais difícil, e também pode ser usada de formas inseguras.
Como Datalog tem restrições, ele consegue usar atalhos, então em muitos casos o desempenho também é melhor.
Por fim, em Prolog os dados são o mesmo que código, ou seja, há homoiconicidade, então operações de criar/modificar/apagar acabam sendo, na prática, alterações no código-fonte. Dá para fazer isso dinamicamente, mas é difícil esperar que o fluxo seja realmente suave, e ainda é preciso algum grau de sincronização manual entre os arquivos e o programa carregado
Quero aprender Prolog mais a fundo para usá-lo em consultas de conjuntos de instruções.
Mas modelar lógica incluindo quantidades e inteiros em nível de bits é bastante difícil