3 Punkte von GN⁺ 3 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • Die Pokémon-Kampfregeln ähneln eher einer Regel-Engine, in der Typenwechselwirkungen, Attacken, Werte und Fähigkeiten miteinander verflochten sind, sodass sie sich mit Prologs Beziehungs- und Regelmodell prägnant ausdrücken lassen
  • Prolog legt Fakten mit Prädikaten wie pokemon/1 und type/2 ab und findet mit großgeschriebenen Variablen und Unifikation Pokémon, die zu Typ- und Attackenbedingungen passen
  • Um Pokémon zu finden, die Freeze-Dry lernen, vom Typ Ice sind und einen Special Attack von mehr als 120 haben, ist eine Prolog-Abfrage kürzer als mehrere EXISTS in SQL
  • Draft-Teams lassen sich mit Prädikaten wie alex/1 und morry/1 ausdrücken, und bei Regeln für Prioritäts-Attacken können Ausschlussbedingungen und der Prankster-Effekt schichtweise ergänzt werden
  • Tabellen wie Techno's Prep Doc sind mächtig, aber eine Prolog-Datenbank ist für beliebige Kombinationsabfragen flexibler und wird mit prologdex und Scryer Prolog umgesetzt

Warum Pokémon-Kampfregeln gut zu logischer Programmierung passen

  • Pokémon-Kämpfe ähneln eher einer Regel-Engine, in der viele Regeln komplex ineinandergreifen, und logische Programmierung wie Prolog eignet sich gut dazu, solche Beziehungen knapp auszudrücken
  • Pokémon sind Charaktere mit Artnamen; es gibt über 1.000 Arten, von Bulbasaur #1) bis Pecharunt #1025)
  • In den Kämpfen der Hauptreihe treten Teams aus je 6 Pokémon gegeneinander an; jedes Pokémon wählt normalerweise eine von 4 Attacken, die meist dem Gegner Schaden zufügen, und gewinnt, wenn die HP des gegnerischen Teams vollständig auf 0 gebracht werden
  • Die Kampfstärke hängt von Basiswerten, der Liste erlernbarer Attacken, Fähigkeiten und dem Typ ab; wegen der vielen möglichen Kombinationen lohnt es sich, das per Software nachzuverfolgen
  • Typen gelten sowohl für Attacken als auch für Pokémon; wenn ein Attackentyp gegen den gegnerischen Typ stark ist, verursacht er doppelten Schaden, ist er schwach, verursacht er halben Schaden
  • Typenmodifikatoren werden kumuliert
    • Scizor) ist vom Typ Bug/Steel, und da beide schwach gegen Fire sind, erleidet es von Fire-Attacken den vierfachen Schaden
    • Setzt man eine Electric-Attacke gegen das Water/Ground-Pokémon Swampert) ein, wird der Schaden wegen der Ground-Immunität zu 0

Grundmodell von Prolog

  • In Prolog werden Beziehungen als Prädikate (predicate) deklariert
pokemon(bulbasaur).
pokemon(ivysaur).
pokemon(venusaur).
pokemon(charmander).
pokemon(charmeleon).
pokemon(charizard).
pokemon(squirtle).
pokemon(wartortle).
pokemon(blastoise).
  • pokemon/1 ist ein Prädikat mit dem Namen pokemon und einem Argument; eine Abfrage wie pokemon(squirtle). prüft, ob die Aussage wahr gemacht werden kann
?- pokemon(squirtle).
   true.

?- pokemon(alex).
   false.
  • Pokémon-Typen lassen sich als Beziehung mit zwei Argumenten wie type/2 ausdrücken; ein Pokémon mit zwei Typen erhält für dasselbe Pokémon zwei type-Fakten
type(bulbasaur, grass).
type(bulbasaur, poison).
type(charmander, fire).
type(charizard, fire).
type(charizard, flying).
type(squirtle, water).
  • Namen, die mit einem Großbuchstaben beginnen, sind Variablen, und Prolog versucht, Abfragen mit Variablen mit allen möglichen Werten zu unifizieren
?- type(squirtle, Type).
   Type = water.

?- type(venusaur, Type).
   Type = grass
;  Type = poison.
  • Setzt man im ersten Argument eine Variable wie in type(Pokemon, grass)., kann man alle Pokémon vom Typ Grass finden; in den realen Daten liefert das 164 Ergebnisse
  • Ein Komma bedeutet, dass mehrere Prädikate alle erfüllt sein müssen, und derselbe Variablenname muss innerhalb der Abfrage denselben Wert haben
?- 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.
  • Wie bei Iron Bundle) lassen sich auch Werte und erlernbare Attacken relational abfragen
?- 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.
  • Mischt man Einschränkungen wie SpA #> 120 ein, kann man direkt Pokémon finden, deren Special Attack über 120 liegt, die Freeze-Dry lernen und vom Typ Ice sind
?- 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.
  • Regeln (rules) in Prolog bestehen aus Kopf und Rumpf; ist der Rumpf wahr, wird auch der Kopf unifiziert
damaging_move(Move) :-
  move_category(Move, physical)
; move_category(Move, special).
  • Diese Regel klassifiziert Physical- oder Special-Attacken als direkte Schadensattacken
?- damaging_move(tackle).
   true.
?- damaging_move(rest).
   false.

Abfrageausdrücke im Vergleich zu SQL

  • Die bisherigen Beispiele sind logisch gesehen nur einfache Kombinationen aus and und or, aber in Prolog werden relationale Abfragen kürzer und leichter anpassbar als in SQL.
  • Modelliert man dieselben Daten in SQL, kann man Pokémon, Typen und Attacken in separaten Tabellen ablegen.
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);
  • Um in SQL Pokémon zu finden, die Freeze-Dry lernen, vom Typ Ice sind und einen Special Attack-Wert über 120 haben, muss man mehrfach EXISTS verwenden.
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'
  );
  • Die entsprechende Prolog-Abfrage listet einfach die benötigten Relationen auf.
?- pokemon_spa(Pokemon, SpA),
SpA #> 120,
learns(Pokemon, freezedry),
type(Pokemon, ice).
  • Wenn immer mehr Bedingungen hinzukommen, werden SQL-Abfragen leicht komplex, während Prolog-Abfragen, sobald man sich an die Arbeitsweise von Variablen gewöhnt hat, gut lesbar und leicht veränderbar bleiben.

Kampfregeln schichtweise aufbauen

  • In Pokémon-Kämpfen gibt es viele interagierende Regeln: Fehlversuche bei Treffern, Statuswert-Erhöhungen und -Senkungen, Item-Effekte, Schadensspannen, Statusveränderungen, Feldeffekte wie Wetter, Terrain und Trick Room, Fähigkeiten sowie die vorherige Verteilung von Statuswerten.
  • Wenn man Software für Pokémon entwickelt, muss man mit dieser Komplexität umgehen und das Modell zugleich in einer handhabbaren Form halten.
  • Prolog hat Stärken bei einem Abfragemodell zur Beschreibung spontaner Kombinationen und bei konsistent geschichteten Regelwerken.
  • Im damage calculator kann man diese Komplexität direkt nachvollziehen.

Draft-Liga und Abfragen zu Prioritäts-Attacken

  • In einem Pokémon-Draft ist jedem Pokémon ein Wert zugewiesen, und die Spieler stellen innerhalb eines festgelegten Punktebudgets ein Team von etwa 8 bis 11 Pokémon zusammen.
  • Da der eigentliche Kampf 6v6 ist, ist die Vorbereitung wichtig: Man muss mögliche Sechser-Kombinationen des Gegners abdecken und die eigenen sechs Pokémon auswählen, die dagegen antreten sollen.
  • Die selbst gedrafteten Pokémon lassen sich direkt als Prädikat wie alex/1 ausdrücken.
alex(meowscarada).
alex(weezinggalar).
alex(swampertmega).
alex(latios).
alex(volcarona).
alex(tornadus).
alex(politoed).
alex(archaludon).
alex(beartic).
alex(dusclops).
  • Die Abfrage nach Pokémon in diesem Team, die Freeze-Dry lernen, ist einfach, liefert aber kein Ergebnis.
?- alex(Pokemon), learns(Pokemon, freezedry).
   false.
  • Die Zugreihenfolge wird grundsätzlich durch Speed bestimmt, aber Attacken haben Priorität (priority), und Attacken mit höherer Priorität werden zuerst ausgeführt.
  • Die meisten Attacken haben Priorität 0, aber eine Attacke mit Priorität 1 wie Accelerock wird vor einer Attacke mit Priorität 0 eines schnelleren Pokémon ausgeführt.
  • Welche Attacken mit positiver Priorität ein bestimmtes Pokémon lernen kann, lässt sich durch die Kombination von learns/2, move_priority/2 und einer Prioritätsbedingung finden.
  • Eine einfache Abfrage enthält auch Attacken wie Helping Hand und Ally Switch, die vor allem in Double Battles relevant sind, oder Attacken wie Bide, die im kompetitiven Spiel kaum Bedeutung haben.
  • \+/1 ist wahr, wenn ein Ziel fehlschlägt, und dif/2 bedeutet, dass zwei Terme verschieden sind; daher kann man eine Regel hinzufügen, die Attacken für Double Battles und Bide ausschließt.
learns_priority(Mon, Move, Priority) :-
  learns(Mon, Move),
  \+ doubles_move(Move),
  dif(Move, bide),
  move_priority(Move, Priority),
  Priority #> 0.
  • Wenn man zusätzlich schützende Attacken wie Protect, Detect, Endure und Magic Coat ausschließt, bleiben nur noch Prioritäts-Attacken übrig, die dem Gegner tatsächlich Schaden oder negative Effekte zufügen können.
?- 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.
  • Wendet man dieselbe Regel auf das Prädikat des gegnerischen Teams an, kann man auch die Prioritäts-Attacken des Gegners sofort finden.
?- 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.

Erweiterung der Fähigkeit Prankster

  • Pokémon mit der Fähigkeit Prankster erhalten für Status-Moves zusätzlich +1 Priorität, und auch dieser Effekt lässt sich zur bestehenden Regel learns_priority/3 addieren
  • Im Team besitzt Tornadus die Fähigkeit Prankster
?- alex(Pokemon), pokemon_ability(Pokemon, prankster).
   Pokemon = tornadus
;  false.
  • Mit dem Prolog-If/Then-Konstrukt ->/2 lässt sich festlegen, dass bei einem Pokémon mit Prankster und einem Move der Kategorie status 1 zur Basispriorität addiert wird, andernfalls bleibt die Basispriorität unverändert
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.
  • Nach dieser Regel umfasst dieselbe Abfrage bei Tornadus Status-Moves wie Agility, Defog, Nasty Plot, Rain Dance, Tailwind, Taunt und Toxic mit Priorität 1
  • Durch die Erweiterung einer einzigen Regel lassen sich sogar Fähigkeitseffekte berücksichtigen, was die Vorteile der Schichtung in Prolog zeigt

Kontrast zu tabellenbasierten Tools

  • In der Pokémon-Community gibt es bereits Ressourcen, um Informationen wie Prioritäts-Moves gegnerischer Teams zu finden; verbreitet sind etwa fortgeschrittene Google Sheets wie „Techno’s Prep Doc“
  • Dieses Spreadsheet erzeugt nach Eingabe der Teams viele Matchup-Informationen und bietet Unterstützung für verschiedene Formate, leicht überfliegbare Visualisierungen und Autovervollständigung
  • Die Formel zum Auffinden von Prioritäts-Moves kombiniert FILTER, VLOOKUP und INDIRECT; INDIRECT gibt Zellbezüge zurück
={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"))}
  • Im Backend-Sheet sind alle Moves aufgelistet, und diese Struktur kommt einer hardcodierten Version von Prolog-Abfragen nahe
  • Eine Prolog-Datenbank ist skalierbarer als ein Ansatz, bei dem eine Liste bemerkenswerter Moves hardcodiert wird, und sie kann beliebige Moves abfragen
  • Auch kombinatorische Fragen, die vorhandene Tools nicht abdecken, lassen sich knapp ausdrücken, etwa das Finden von Special-Moves, die Tornadus lernen kann und die gegen Teammitglieder von Justin sehr effektiv sind
?- 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.

Implementierungsnotizen und Grenzen

1 Kommentare

 
GN⁺ 3 시간 전
Lobste.rs-Kommentare
  • Ich frage mich, ob es Leute gibt, die Prolog tatsächlich produktiv einsetzen. Ob beruflich oder privat, ist egal; bisher habe ich nur solche Spielzeugbeispiele gesehen.

    • Ich mag Prolog ziemlich gern, und als jemand, der vermutlich die am weitesten verbreitete Implementierung eines Prolog Language Servers gebaut hat, nutze ich es oft für kleine Skripte.
      Streng genommen betreibe ich sogar mindestens ein Stück Prolog-Code in Produktion: ein internes Analyse-Dashboard. Früher habe ich auch den Backend-Server einer iOS-App in Prolog geschrieben. Dabei habe ich sogar eine HTTP/2-Client-Bibliothek für Prolog gebaut, um APNS-Benachrichtigungen ohne externen Dienst zu verschicken.
    • Ich weiß nicht, ob das eine zufriedenstellende Antwort ist, aber seit ich diesen Blogbeitrag geschrieben habe, hole ich Prolog für Probleme hervor, die auf einer ganz anderen Achse liegen als mein älterer Code.
      In der Prolog-Community gibt es sicher Leute, die es gern für Dinge wie Webserver einsetzen, aber für mich fühlt es sich eher so an, als würde es einen anderen Skill-Tree freischalten. Zum Beispiel erkenne ich besser, wann ein maßgeschneiderter Parser oder eine DSL zu einem Problem passt.
      Mit dem Wissen aus diesem Beitrag habe ich eine nützliche Teilmenge der Tax-Logic-Engine von IRS Fact Graph neu implementiert. Prolog passt erstaunlich gut zu dieser Arbeit, weil es undokumentierte Ecken sichtbar macht und einen dazu zwingt, sie zu lösen, über die man in einer imperativen Implementierung leicht hinweggehen würde.
      Den „Ausführungs“-Teil habe ich noch nicht fertiggestellt, aber das Parsing ist weit genug, dass ich einen ziemlich guten ersten Dokumententwurf schreiben konnte. Es fehlt noch ein großes Feature, nämlich Datumsarithmetik, und wenn das drin ist, werde ich es separat aufschreiben.
      Mit DCG ist Prolog hervorragend für das Parsen komplexer strukturierter Texte. Früher dachte ich, wenn awk nicht reicht, dann eben Python oder JS, aber jetzt passt Prolog für mich gut dort, wo Struktur und Disziplin nötig sind. Dieses altmodische Gefühl, eine komplexe Codebasis zu schreiben, die in eine Datei passt, ist ebenfalls befriedigend, und es ist nicht übermäßig kryptisch wie APL.
      Das Beispiel selbst ist trivial, aber das Pokémon-Beispiel ist es nicht. Die meisten scheinbar trivialen Beispiele waren nur möglich, weil es bereits Code gibt, der die lächerlich komplexen Kampfmechaniken sehr gründlich implementiert. Ich interessiere mich dafür, eine Prolog-Regel-Engine zu bauen, die einen Teil dessen übernimmt, was bestehende Werkzeuge tun, und probiere das nach und nach aus; der Vorteil liegt darin, dass sich Möglichkeiten durch Tiefensuche leichter aufdecken lassen als in imperativem Code und sich das Ganze besser warten lässt.
    • Ich habe es gelegentlich für Programmanalyse verwendet. Ziemlich viele Werkzeuge speichern Programminformationen in Datalog und lassen einen darauf Abfragen schreiben, aber manchmal habe ich auch Tools benutzt, die direkt SWI-Prolog bereitstellen, sodass ich Backtracking oder Funktionen nutzen konnte.
    • Mein Blog wird mit Prolog erzeugt, und der Quellcode ist ebenfalls offen verfügbar.
      Die Scryer-Prolog-Dokumentation wird auch von einem Prolog-Programm erzeugt, das ich DocLog nenne. Ich habe auch einige Bibliotheken gebaut, die diese Programme verwenden. SWI Prolog setzt SWI ebenfalls direkt für seine Website, die Web-IDE Swish und den ClioPatria-Server ein.
  • Ich habe Informationen schon einmal organisiert, indem ich SQLite wie eine Art typsichere Tabellenkalkulation verwendet habe. Das ging sogar ganz ohne CRUD-Oberfläche darüber.
    Allerdings war es nicht immer die schönste Sprache dafür. Ich habe mir auch eine Zeit lang Datalog angesehen, aber die meisten Implementierungen wirkten eher dafür gedacht, in größere Programme eingebettet zu werden, statt wie in diesem Beitrag ein Werkzeug zu sein, mit dem man Informationen unkompliziert festhalten kann.
    Vielleicht ist Prolog tatsächlich das Werkzeug, das man dafür verwenden sollte.

    • Als Datalog in Form einer externen Datenbank gibt es Mangle und Datomic, also könnte das passen, aber ich habe keines von beiden selbst benutzt.
      Prolog ist eine Obermenge von Datalog, kann also alles, was Datalog kann, und noch mehr. Manchmal wird genau dieses „mehr“ aber zum Problem, denn dann ist es eine turingvollständige Sprache und kann nicht terminieren, Schlussfolgerungen können etwas schwieriger werden, und man kann sie auf unsichere Weise verwenden.
      Datalog hat dank seiner Einschränkungen Abkürzungen zur Verfügung und ist deshalb bei der Performance oft besser.
      Und schließlich sind in Prolog Daten dasselbe wie Code, es gibt also Homoikonizität, weshalb Erzeugen/Ändern/Löschen praktisch bedeutet, den Quellcode zu verändern. Das geht zwar auch dynamisch, aber man sollte keinen besonders reibungslosen Ablauf erwarten, und zwischen Dateien und dem geladenen Programm ist weiterhin ein gewisses Maß an manueller Synchronisation nötig.
  • Ich würde Prolog gern tiefer lernen und für Instruction-Set-Abfragen einsetzen.
    Aber Logik zu modellieren, einschließlich Ganzzahlen in Bezug auf Mengen und auf Bit-Ebene, ist ziemlich schwierig.