Skip to content
HN On Hacker News ↗

Prolog Basics Explained with Pokémon

▲ 283 points 59 comments by birdculture 1mo ago HN discussion ↗

Pangram verdict · v3.3

We believe that this document is fully human-written

1 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 6 of 6
SEGMENTS · AI 0 of 6
WORD COUNT 1,843
PEAK AI % 2% · §5
Analyzed
May 17
backend: pangram/v3.3
Segments scanned
6 windows
avg 307 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,843 words · 6 segments analyzed

Human AI-generated
§1 Human · 1%

Unplanned Obsolescence Home Blog Talks About RSS

January 05, 2026

The project that inspired this post is a little silly—I am about to describe the mechanics of a children’s video game in great detail—but this particular problem is 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 Pokémon. Pokémon basics Pokémon is a video game series/multimedia franchise/lifestyle brand set in a world where humans live alongside a menagerie of colorful animal characters. “Pokémon” 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 Pokémon, from Bulbasaur (#1) to Pecharunt (#1025).

Popular Pokémon include (from left to right): Pikachu (#25), Archeops (#567) , and Dipplin (#1101).

There are all sorts of Pokémon games now, but the main series has always been about catching and battling them. During a battle, your team of six Pokémon faces off against another team. Each Pokémon is equipped with four moves that it can choose to (usually) do damage to their opponent. You need to reduce the HP (Hit Points) of all your opponent’s Pokémon to zero before they are able to do so to you. Each Pokémon has unique traits that affects how it battles. They have a set of base stats, a large pool of possible moves, a handful of abilities, and a typing. As you will see in a moment, the immense number of combinations here is the motivation for trying to track this with software.

Scizor is a Bug/Steel type with high Attack and low Speed (via Smogon)

Typing is especially important.

§2 Human · 1%

Moves have a type, like Fire or Rock, and Pokémon can have up to two types. A move with a type that is Super Effective against the opposing Pokémon will do double damage; a move that is Not Very Effective will do half damage. It’s a little more intuitive with examples. The Fire-type move Flamethrower will do 2x to Grass-type Pokémon, because Grass is weak to Fire, but the Water-type move Surf will only do ½ damage to them, because Grass resists Water.

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.

Pokémon Type Chart (via Wikimedia)

Those are effectively the mechanics of the Pokémon 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, at the surface level, they’re not very hard. Prolog basics Before I explain how wonky the Pokémon mechanics can get under the hood, I first need to explain how logic programming works. Pokémon is a great fit for logic programming because Pokémon battles are essentially an extremely intricate rules engine. Let’s 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 relationships: 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.

§3 Human · 1%

These facts are loaded into an interactive prompt called the “top-level.” You query the top-level by typing a statement into the prompt; Prolog tries to find all the ways to make that statement true. When there’s more than one possible solution, the top-level displays the first solution and then awaits user input. You can then have it display one more solution, all the solutions, or stop entirely. In this first example, we type pokemon(squirtle). and hit Enter. The top-level replies true. Squirtle is, in fact, a Pokémon. ?- pokemon(squirtle). true. Not all things are Pokémon. ?- pokemon(alex). false. Let’s add Pokémon 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 Pokémon have just one type while others have two. In the latter case, that’s modeled with two type facts. 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. Suppose 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. Prolog 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 Pokémon with two types, the predicate unifies twice.

§4 Human · 1%

?- 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 the prompt would happily 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 Pokémon 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.

§5 Human · 2%

Pokémon have lots of data that you can play around with. Iron Bundle is a strong Water/Ice-type Pokémon 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 Pokémon 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 speaking, very ambitious—just “and” and “or” statements about various facts. It’s essentially a glorified lookup table.

§6 Human · 2%

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, category TEXT); Then query it like so: SELECT DISTINCT pokemon, 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. Pokémon battles have an outrageous 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

Some moves miss a certain percentage of the time, doing no damage. Some moves raise or lower a Pokémon's stats. Pokémon can hold items that have various effects. Damage calculations aren't constant; moves do normally-distributed damage within the calculated range.