Unplanned Obsolescence


Understanding Prolog through Pokemon

January 02, 2026

The project that inspired this post is a little silly—I am about to describe a children’s video game in excruciating detail—but it’s what finally made Prolog click for me, an epiphany I’ve been hunting for ever since reading Bruce Tate’s “Seven Languages in Seven Weeks.”

This exercise has taught me a lot about the kinds of interfaces I’m trying to build in somewhat more practical domains. For certain kinds of relationships, logic programming is by far the most concise and expressive programming system I’ve ever used.

To understand why, let’s talk about Pokemon.

Pokemon basics

Pokemon is a video game series/multimedia franchise/lifestyle brand set in a world where humans live alongside a menagerie of colorful animal characters.

“Pokemon” is both the name of the franchise and the generic term for the animal characters themselves, which all have their own individual species names. There are over a thousand distinct species of Pokemon, from Bulbasaur (#1) to Pecharunt (#1025).

the pokemon pikachu, an electric mouse the pokemon archeops, a colorfully-feathered rock bird the pokemon dipplin, which kind of looks like a candy apple that fell on the ground
Popular Pokemon include (from left to right):
Pikachu (#25), Archeops (#567) , and Dipplin (#1101).

Although you can engage with these little guys in an immense variety of game formats, the main series games are about catching and battling them. During a battle, your team of Pokemon faces off against another team, and you need to reduce the HP of all your opponent’s Pokemon to zero before they are able to do so to you.

The core gameplay loop of a battle is simple. Each turn both players choose to either switch out to a different Pokemon (you can have up to six), or use one of the current Pokemon’s four moves.

A side-by-side of the Pokemon battle menu from Generation 4, showing the fight, bag, run, Pokemon menu on the left, and the four move menu on the right.
Left, the battle menu; right, the four move options available when you select fight. The bag and run options are not relevant for this explanation.

Once each player has made a choice, the turn happens. Switches go first, then moves. If you select a move and you opponent opts to switch, the turn will proceed like this:

  1. The opponent switches out one Pokemon for another
  2. The move you selected hits the Pokemon that switched in

Moves do damage to the Pokemon that gets hit, subtracting from that Pokemon’s hit points (HP).

Glalie using Ice Beam against an Oddish in a screenshot from a Generation 3 Pokemon game.
The move Ice Beam being used to attack the opposing Pokemon.

When a Pokemon’s HP reaches zero, it faints and can no longer be used in the battle (even if it didn’t get to use a move yet). A new Pokemon will be sent out, and the game continues. This process repeats—each player opting to either move or switch out—until one player is out of usable Pokemon; the other player is the winner.

Pokemon have a handful of important stats that implicate how they battle against each other.

A screenshot showing Scizor's abilities, typing, and stats
Scizor's stat summary (via Smogon)

Two traits to note here are the typing (Bug/Steel) and the stats (Attack, Defense, etc. on the right). The stats control who goes first (Speed) and how much base damage the move does (all the others). The typing then applies multipliers to that damage.

Typing is very important. Moves have a type, like Fire or Rock, and Pokemon can have up to two types. A move with a type that is Super Effective against the opposing Pokemon will do double damage; a move that is Not Very Effective will do half damage. For instance, the Fire-type move Flamethrower will do 2x to Grass-type Pokemon, because Grass is weak to Fire, but the Water-type move Surf will only do ½ damage to them, because Grass resists Water.

Miltoic using Surf against a Lunatone in a Generation 3 Pokemon game.
Lunatone is a Rock/Psychic Type. Rock is weak to Water, and Psychic is neutral to it, so Surf will do 2x damage.

Type modifiers can stack. Scizor is a Bug/Steel type, and both Bug and Steel are weak to Fire, so Fire moves will do 4x damage to Scizor. Electric is weak to Water, but Ground is immune, so if you use an Electric type move against Water/Ground Swampert, you’ll do zero damage, since 0×2 is still 0.

Naturally, there is a chart to help you keep track.

The Pokemon type chart.
Pokemon Type Chart (via Wikimedia)

These are effectively the mechanics of the Pokemon video games as I understood them when I was 8. Click moves to do damage, try to click moves with good type matchups. These games are for children and they’re not very hard.

Of course, it wouldn’t be interesting if that’s all there was to it.

Unification basics

Before I explain just how wonky the Pokemon mechanics can get, we need to establish logic programming basics. Pokemon is a great fit for logic programming because Pokemon battles are essentially an extremely intricate rules engine.

We start by creating a file with a bunch of facts.

pokemon(bulbasaur).
pokemon(ivysaur).
pokemon(venusaur).
pokemon(charmander).
pokemon(charmeleon).
pokemon(charizard).
pokemon(squirtle).
pokemon(wartortle).
pokemon(blastoise).

In Prolog, we declare “predicates.” Predicates define relations between things: bulbasaur is a pokemon, charmander is a pokemon, and so on. We refer to this predicate as pokemon/1, because the name of the predicate is pokemon and it has one argument.

These facts are loaded into an interactive prompt called the “top-level,” which you can query. All the code blocks labeled “top-level” are the result of typing the first line and then pressing Enter.

In this first example, we type pokemon(squirtle). The top-level replies true. Squirtle is, in fact, a Pokemon.

?- pokemon(squirtle).
   true.

Not all things are Pokemon.

?- pokemon(alex).
   false.

Let’s add Pokemon types in there, as the predicate type/2.

type(bulbasaur, grass).
type(bulbasaur, poison).
type(ivysaur, grass).
type(ivysaur, poison).
type(venusaur, grass).
type(venusaur, poison).
type(charmander, fire).
type(charmeleon, fire).
type(charizard, fire).
type(charizard, flying).
type(squirtle, water).
type(wartortle, water).
type(blastoise, water).

Recall that some Pokemon have just one type while others have two. That’s modeled here by having two type facts in the latter case. Bulbasaur is a Grass type, and Bulbasaur is a Poison type; both are true. The paradigm is similar to a One-To-Many relation in a SQL database.

Interactively, we can confirm whether Squirtle is a water type.

?- type(squirtle, water).
   true.

Can we state that Squirtle is a Grass type?

?- type(squirtle, grass).
   false.

No, because Squirtle is a Water type.

Now let’s say we didn’t know what type Squirtle was. We can ask!

?- type(squirtle, Type).
   Type = water.

In Prolog, names that start with an upper-case letter are variables. The program tries to “unify” the predicate with all possible matches for the variable. There’s only one way to make this particular predicate true though: Type has to be water, because Squirtle’s only type is Water.

For Pokemon with two types, the predicate unifies twice.

?- type(venusaur, Type).
   Type = grass
;  Type = poison.

Semantically, that leading semicolon on the third line means “or.” type(venusaur, Type) is true when Type = grass or when Type = poison.

Any of the terms can be be a variable, which means we can ask questions in any direction. What are all the Grass types? Just make the first argument the variable, and set the second argument to grass.

?- type(Pokemon, grass).
   Pokemon = bulbasaur
;  Pokemon = ivysaur
;  Pokemon = venusaur
;  Pokemon = oddish
;  Pokemon = gloom
;  Pokemon = vileplume
;  Pokemon = paras
;  Pokemon = parasect
;  Pokemon = bellsprout
;  ... .

I cut it off, but it would list all 164 of them.

Commas can be used to list multiple predicates—Prolog will unify the variables such that all of them are true. Listing all the Water/Ice types is just a matter of asking what Pokemon exist that unify with both the Water and Ice types.

?- 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.

Even though Pokemon is a variable, in the context of the query, both instances of it have to be the same (just like in algebra). The query only unifies for values of Pokemon where both those predicates hold. For instance, the Water/Ice type Dewgong is a solution because our program contains the following two facts:

type(dewgong, water).
type(dewgong, ice).

Therefore, subbing in dewgong for the Pokemon variable satisfies the query. Squirtle, by contrast, is just a Water type: pokemon(squirtle, water) exists, but not pokemon(squirtle, ice). The query requires both to unify, so squirtle is not a possible value for Pokemon.

Pokemon have lots of data that you can play around with. Iron Bundle is a strong Ice-type Pokemon with high Special Attack. How high exactly?

?- pokemon_spa(ironbundle, SpA).
   SpA = 124.

With Special Attack that high, we want to make use of strong Special moves. What Special moves does Iron Bundle know?

?- 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.

Freeze-Dry is a particularly good Special move. Here’s a query for all Ice-type Pokemon with Special Attack greater than 120 that learn Freeze-Dry.

?- 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.

One last concept before we move on: Rules. Rules have a head and a body, and they unify if the body is true.

A move is considered a damaging move if it’s either a Physical Move or a Special Move. The damaging_move/2 predicate defines all the moves that do direct damage.

damaging_move(Move) :-
  move_category(Move, physical)
; move_category(Move, special).

This will unify with any moves that do direct damage.

?- damaging_move(tackle).
   true.
?- damaging_move(rest).
   false.

SQL comparison

Nothing I’ve shown so far is, logically, very ambitious—just “and” and “or” statements about various facts. It’s essentially a glorified lookup table. Still, take a moment to appreciate how much nicer it is to query this database than a plausible alternative, like SQL.

For the facts we’ve seen so far, I would probably set up SQL tables like this:

-- Omitting the other stats to be concise
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);

Then query it like so:

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'
  );

For comparison, here’s the equivalent Prolog query again:

?- pokemon_spa(Pokemon, SpA),
SpA #> 120,
learns(Pokemon, freezedry),
type(Pokemon, ice).

I’m not ripping on SQL—I love SQL—but that’s the best declarative query language most people interact with. It’s amazing to me how much simpler and more flexible the Prolog version is. The SQL query would become unmanageably complex if we continued to add clauses, while the Prolog query remains easy to read and edit (once you get the hang of how variables work).

Level up

With the basics established, here’s some context on the project I’m working on.

Pokemon battles have an outrageous number of number of mechanics that all interact in complex and probabilistic ways. Part of the appeal of these games is the futile attempt to keep them all in your head better than your opponent, using that information to out-predict and out-maneuver their plans. It’s a sort of like very silly Poker.

A small subset of game mechanics I have not yet mentioned

The challenge, if you want to build software for this game, is to model all that complexity without losing your mind. Prolog is stunningly good at this, for two main reasons:

  1. The query model excels at describing ad-hoc combinations.
  2. The data model is perfectly suited to layering rules in a consistent way.

To illustrate that, here’s how I implemented priority moves for my Pokemon draft league.

Pokemon draft is pretty much what it sounds like. Pokemon are given a point value based on how good they are, each player is given a certain amount of points to spend, and you draft until every player has spent their points. Your team ends up with about 8-11 Pokemon and each week you go head to head against another person in the league. My friend and WMI collaborator Morry invited me to his a couple years ago and I’ve been hooked on the format ever since.

The games are 6v6, so a big part of the battle is preparing for all the possible combinations of six your opponent could bring, and putting together six of your own that can handle all of them.

Naturally, you can only build teams with the Pokemon I drafted. I just made that predicate my name: alex/1.

alex(meowscarada).
alex(weezinggalar).
alex(swampertmega).
alex(latios).
alex(volcarona).
alex(tornadus).
alex(politoed).
alex(archaludon).
alex(beartic).
alex(dusclops).

What Pokemon do I have that learn Freeze-Dry?

?- alex(Pokemon), learns(Pokemon, freezedry).
   false.

None. Rats.

One very important type of move is priority moves. Earlier I mentioned that the Speed stat controls which Pokemon moves first. Some nuance: the Pokemon that used the move with the highest priority goes first, and if they both selected a move of the same priority, then the one with the higher Speed goes first.

Most moves have a priority of zero.

?- move_priority(Move, P).
   Move = '10000000voltthunderbolt', P = 0
;  Move = absorb, P = 0
;  Move = accelerock, P = 1
;  Move = acid, P = 0
;  Move = acidarmor, P = 0
;  Move = aciddownpour, P = 0
;  ... .

Ah, but not all! Accelerock has a priority of 1. A Pokemon that uses Accelerock will move before any Pokemon that uses a move with priority 0 (or less), even if the latter Pokemon is faster (has a higher Speed stat).

I define a learns_priority/3 predicate that unifies with a Pokemon, the priority move it learns, and what priority that move is.

learns_priority(Pokemon, Move, P) :-
  learns(Pokemon, Move),
  move_priority(Move, P),
  move_priority #> 0.

A simple query that asks “what priority moves does my team learn” returns a lot of answers.

?- alex(Pokemon), learns_priority(Pokemon, Move, Priority).
   Pokemon = meowscarada, Move = endure, Priority = 4
;  Pokemon = meowscarada, Move = helpinghand, Priority = 5
;  Pokemon = meowscarada, Move = protect, Priority = 4
;  Pokemon = meowscarada, Move = quickattack, Priority = 1
;  Pokemon = meowscarada, Move = allyswitch, Priority = 2
;  Pokemon = meowscarada, Move = suckerpunch, Priority = 1
;  Pokemon = weezinggalar, Move = endure, Priority = 4
;  Pokemon = weezinggalar, Move = protect, Priority = 4
;  Pokemon = swampertmega, Move = bide, Priority = 1
;  Pokemon = swampertmega, Move = endure, Priority = 4
;  Pokemon = swampertmega, Move = helpinghand, Priority = 5
;  Pokemon = swampertmega, Move = protect, Priority = 4
;  Pokemon = swampertmega, Move = wideguard, Priority = 3
;  Pokemon = latios, Move = allyswitch, Priority = 2
;  Pokemon = latios, Move = endure, Priority = 4
;  Pokemon = latios, Move = helpinghand, Priority = 5
;  Pokemon = latios, Move = magiccoat, Priority = 4
;  Pokemon = latios, Move = protect, Priority = 4
;  Pokemon = volcarona, Move = endure, Priority = 4
;  Pokemon = volcarona, Move = protect, Priority = 4
;  Pokemon = volcarona, Move = ragepowder, Priority = 2
;  Pokemon = tornadus, Move = endure, Priority = 4
;  Pokemon = tornadus, Move = protect, Priority = 4
;  Pokemon = politoed, Move = detect, Priority = 4
;  Pokemon = politoed, Move = endure, Priority = 4
;  Pokemon = politoed, Move = helpinghand, Priority = 5
;  Pokemon = politoed, Move = protect, Priority = 4
;  Pokemon = politoed, Move = bide, Priority = 1
;  Pokemon = archaludon, Move = endure, Priority = 4
;  Pokemon = archaludon, Move = protect, Priority = 4
;  Pokemon = beartic, Move = aquajet, Priority = 1
;  Pokemon = beartic, Move = bide, Priority = 1
;  Pokemon = beartic, Move = endure, Priority = 4
;  Pokemon = beartic, Move = protect, Priority = 4
;  Pokemon = dusclops, Move = allyswitch, Priority = 2
;  Pokemon = dusclops, Move = endure, Priority = 4
;  Pokemon = dusclops, Move = helpinghand, Priority = 5
;  Pokemon = dusclops, Move = protect, Priority = 4
;  Pokemon = dusclops, Move = shadowsneak, Priority = 1
;  Pokemon = dusclops, Move = snatch, Priority = 4
;  Pokemon = dusclops, Move = suckerpunch, Priority = 1
;  false.

Although this is technically correct (the best kind), most of these answers are not actually useful. Helping Hand and Ally Switch have very high priority, but they only have a purpose in Double Battles, which isn’t the format I’m playing.

To fix this, I define all the Double Battle moves and exclude them. I’m going to exclude the move Bide too, which is functionally useless. The \+/1 predicate means “true if this goal fails”, and dif/2 means “these two terms are different.”

learns_priority(Mon, Move, Priority) :-
  learns(Mon, Move),
  \+ doubles_move(Move),
  dif(Move, bide),
  move_priority(Move, Priority),
  Priority #> 0.

doubles_move(helpinghand).
doubles_move(afteryou).
doubles_move(quash).
doubles_move(allyswitch).
doubles_move(followme).
doubles_move(ragepowder).
doubles_move(aromaticmist).
doubles_move(holdhands).
doubles_move(spotlight).

We get the following results:

?- alex(Pokemon), learns_priority(Pokemon, Move, Priority).
   Pokemon = meowscarada, Move = endure, Priority = 4
;  Pokemon = meowscarada, Move = protect, Priority = 4
;  Pokemon = meowscarada, Move = quickattack, Priority = 1
;  Pokemon = meowscarada, Move = suckerpunch, Priority = 1
;  Pokemon = weezinggalar, Move = endure, Priority = 4
;  Pokemon = weezinggalar, Move = protect, Priority = 4
;  Pokemon = swampertmega, Move = endure, Priority = 4
;  Pokemon = swampertmega, Move = protect, Priority = 4
;  Pokemon = latios, Move = endure, Priority = 4
;  Pokemon = latios, Move = magiccoat, Priority = 4
;  Pokemon = latios, Move = protect, Priority = 4
;  Pokemon = volcarona, Move = endure, Priority = 4
;  Pokemon = volcarona, Move = protect, Priority = 4
;  Pokemon = tornadus, Move = endure, Priority = 4
;  Pokemon = tornadus, Move = protect, Priority = 4
;  Pokemon = politoed, Move = detect, Priority = 4
;  Pokemon = politoed, Move = endure, Priority = 4
;  Pokemon = politoed, Move = protect, Priority = 4
;  Pokemon = archaludon, Move = endure, Priority = 4
;  Pokemon = archaludon, Move = protect, Priority = 4
;  Pokemon = beartic, Move = aquajet, Priority = 1
;  Pokemon = beartic, Move = endure, Priority = 4
;  Pokemon = beartic, Move = protect, Priority = 4
;  Pokemon = dusclops, Move = endure, Priority = 4
;  Pokemon = dusclops, Move = protect, Priority = 4
;  Pokemon = dusclops, Move = shadowsneak, Priority = 1
;  Pokemon = dusclops, Move = snatch, Priority = 4
;  Pokemon = dusclops, Move = suckerpunch, Priority = 1
;  false.

That’s much better, but there’s a handful of moves in there that go first because they protect the user from damage or status, like Detect. That’s not really what I mean by priority move—I’m interested in moves that will surprise my opponent with damage or an adverse side effect, like Quick Attack and Sucker Punch.

learns_priority(Mon, Move, Priority) :-
  learns(Mon, Move),
  \+ doubles_move(Move),
  \+ protection_move(Move),
  Move \= bide,
  move_priority(Move, Priority),
  Priority #> 0.

protection_move(detect).
protection_move(protect).
protection_move(kingsshield).
protection_move(burningbulwark).
protection_move(spikyshield).
protection_move(banefulbunker).
protection_move(endure).
protection_move(magiccoat).

With those rules in place, we arrive at a very useful answer!

?- 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.

It’s even more useful to quickly look up what priority moves my opponent for the week has.

?- 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.

At this point, I showed the program to Morry and he hit me with a challenge. Pokemon with the Prankster ability get an additional +1 priority on their status moves. Could the rule be extended to note that?

I happen to have one such Pokemon on my team.

?- alex(Pokemon), pokemon_ability(Pokemon, prankster).
   Pokemon = tornadus
;  false.

This took me 3 minutes, using Prolog’s if/then construct, ->/2.

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.

Now the same query includes all of Tornadus’ status moves, with their increased priority.

?- alex(Pokemon), learns_priority(Pokemon, Move, P).
   Pokemon = meowscarada, Move = quickattack, P = 1
;  Pokemon = meowscarada, Move = suckerpunch, P = 1
;  Pokemon = tornadus, Move = agility, P = 1
;  Pokemon = tornadus, Move = attract, P = 1
;  Pokemon = tornadus, Move = bulkup, P = 1
;  Pokemon = tornadus, Move = confide, P = 1
;  Pokemon = tornadus, Move = defog, P = 1
;  Pokemon = tornadus, Move = doubleteam, P = 1
;  Pokemon = tornadus, Move = embargo, P = 1
;  Pokemon = tornadus, Move = leer, P = 1
;  Pokemon = tornadus, Move = metronome, P = 1
;  Pokemon = tornadus, Move = nastyplot, P = 1
;  Pokemon = tornadus, Move = raindance, P = 1
;  Pokemon = tornadus, Move = rest, P = 1
;  Pokemon = tornadus, Move = roleplay, P = 1
;  Pokemon = tornadus, Move = sandstorm, P = 1
;  Pokemon = tornadus, Move = scaryface, P = 1
;  Pokemon = tornadus, Move = sleeptalk, P = 1
;  Pokemon = tornadus, Move = snowscape, P = 1
;  Pokemon = tornadus, Move = substitute, P = 1
;  Pokemon = tornadus, Move = sunnyday, P = 1
;  Pokemon = tornadus, Move = swagger, P = 1
;  Pokemon = tornadus, Move = tailwind, P = 1
;  Pokemon = tornadus, Move = taunt, P = 1
;  Pokemon = tornadus, Move = torment, P = 1
;  Pokemon = tornadus, Move = toxic, P = 1
;  Pokemon = beartic, Move = aquajet, P = 1
;  Pokemon = dusclops, Move = shadowsneak, P = 1
;  Pokemon = dusclops, Move = snatch, P = 4
;  Pokemon = dusclops, Move = suckerpunch, P = 1
;  false.
?-

There’s something about spreadsheets

I’m not the first person to think “it would be nice to know what priority moves my opponent’s team has.” The Pokemon community has resources like this, encoded in the best programming interface known to man: the humble spreadsheet.

A screenshot of a spreadsheet listing Pokemon that know various useful moves.
Much prettier to look at, too.

I use a copy of “Techno’s Prep Doc”, which is one of those spectacularly-advanced Google Sheets you come across in the wild sometimes. You put in the teams and it generates tons of useful information about the matchup. It has a great interface, support for a variety of formats, scannable visuals, and even auto-complete.

I was curious about the formula for finding priority moves. It’s gnarly.

={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"))}

With a little bit of clicking around, I was basically able to figure out what this does. There’s a “Backend” sheet that lists all the moves. It’s effectively a hard-coded version of my Prolog query.

The lookup formula does some filtering, VLOOKUP-ing, and kinda-metaprogramming (INDIRECT returns a cell reference) to find all the Pokemon on your team that are in that Backend list, and display them.

There are a number of reasons that I, personally, would prefer to work on a version of this database implemented in Prolog over a version of this database implemented with spreadsheet VLOOKUPs. Time permitting, I plan to make some interesting webapps with the work described here. (If I can ever get scryer-prolog to compile to WASM, that is.)

Furthermore, it’s obvious to me that this paradigm is far more extensible. The spreadsheet backend is a hard-coded list of notable moves. My database can look up any move, which can produce some really powerful searches.

This particular query still blows my mind, because it instantly answers the kind of thing I used to try and figure out by endlessly switching back and forth between tabs. It finds all the Special moves that my Pokemon, Tornadus, learns which are super-effective against any member of Justin’s team.

?- 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.
?-

I’m not interested in how structured programming is more extensible than spreadsheets, though. I already know why I don’t do all my programming in spreadsheets. A question I find very important is: what is it about this particular problem, and the kinds of people who were motivated to solve it, where the best interactive program available is in a spreadsheet?

I believe there are a great many problems like that in the world, and a lot of improvements on that programming paradigm that can be offered if people like me took those problems seriously.

Notes