이 글은 포켓몬스터 게임의 복잡한 속성과 상성 규칙을 예시로 삼아 논리형 프로그래밍 언어인 Prolog의 기초 개념을 설명합니다. 다양한 관계성을 표현하고 규칙 기반의 엔진을 다루는 데 있어 논리형 프로그래밍이 얼마나 간결하고 강력한 표현력을 가지는지 보여줍니다.
번역된 본문
이 글을 작성하게 된 프로젝트는 약간 유치할 수 있습니다. 어린이용 비디오 게임의 메커니즘을 아주 상세히 설명하려고 하니까요. 하지만 바로 이 문제 덕분에 저는 Prolog를 완전히 이해하게 되었습니다. 이는 Bruce Tate의 저서 "7주 만에 배우는 7개의 언어(Seven Languages in Seven Weeks)"를 읽은 이래로 계속 찾아 헤매던 깨달음이었습니다. 이 실습을 통해 저는 조금 더 실용적인 분야에서 구축하려는 인터페이스의 유형에 대해 많은 것을 배웠습니다. 특정 종류의 관계를 다룰 때, 논리형 프로그래밍은 제가 사용해 본 어떤 프로그래밍 시스템보다도 훨씬 간결하고 표현력이 뛰어납니다. 그 이유를 이해하기 위해 포켓몬에 대해 이야기해 보겠습니다.
포켓몬 기초
포켓몬스터(Pokémon)는 인간이 다채로운 동물 캐릭터들과 함께 어우러져 사는 세계관을 배경으로 한 비디오 게임 시리즈이자 멀티미디어 프랜차이즈, 라이프스타일 브랜드입니다. '포켓몬'은 프랜차이즈의 이름이자 동물 캐릭터 자체를 지칭하는 일반 명사이기도 하며, 각 캐릭터들은 고유한 종 이름을 가지고 있습니다. 이상해꽃(Bulbasaur, #1)부터 페어리(Pecharunt, #1025)까지 1,000종이 넘는 포켓몬이 존재합니다. 지금은 다양한 종류의 포켓몬 게임이 나와 있지만, 본가 시리즈는 항상 포켓몬을 잡고 배틀하는 것에 관한 것이었습니다. 배틀 중에는 6마리의 포켓몬으로 구성된 팀이 상대 팀과 맞붙게 됩니다. 각 포켓몬은 4개의 기술(move)을 장착하고 있으며, 이를 사용해 (보통) 상대에게 피해를 입힐 수 있습니다. 상대 포켓몬의 HP(히트포인트)를 먼저 0으로 만들어 기절시켜야 게임에서 승리합니다. 각 포켓몬은 배틀에 영향을 미치는 고유한 특성을 가지고 있습니다. 기본 스탯, 방대한 기술 풀, 몇 가지 특성(abilities), 그리고 타입(typing)이 있습니다. 잠시 후 보시겠지만, 여기서 나오는 엄청난 수의 조합이 바로 소프트웨어로 이를 추적하고자 하는 동기가 됩니다. 타입은 특히 중요합니다. 기술에는 불꽃(Fire)이나 바위(Rock) 같은 타입이 있으며, 포켓몬은 최대 2개의 타입을 가질 수 있습니다. 기술의 타입이 상대 포켓몬에게 '효과가 뛰어남(Super Effective)'이라면 2배의 피해를 입히고, '효과가 별로임(Not Very Effective)'이라면 절반의 피해를 입힙니다. 예시를 보면 조금 더 직관적으로 이해할 수 있습니다. 불꽃 타입 기술인 '화염방사(Flamethrower)'는 풀 타입 포켓몬에게 2배의 피해를 줍니다. 풀이 불꽃에 약하기 때문입니다. 하지만 물(Water) 타입 기술인 '파도타기(Surf)'는 풀이 물을 저항하기 때문에 절반의 피해만 줍니다. 타입의 보정은 중첩될 수 있습니다. 핫삼(Scizor)은 벌레(Bug)/강철(Steel) 타입인데, 벌레와 강철 모두 불꽃에 약하므로 불꽃 기술은 핫삼에게 4배의 피해를 줍니다. 전기(Electric) 타입은 물에 약하지만 땅(Ground)은 면역입니다. 따라서 물/땅 타입인 대짱이(Swampert)에게 전기 타입 기술을 사용하면 0×2는 여전히 0이므로 아무런 피해도 입히지 못합니다. 당연히 이 모든 것을 쉽게 파악할 수 있도록 도와주는 상성표도 존재합니다. 제가 8살 때 이해했던 포켓몬 비디오 게임의 메커니즘은 사실상 이것이 전부였습니다. 기술을 클릭하여 피해를 입히고, 타입 상성이 좋은 기술을 클릭하려고 노력하는 것이죠. 이 게임들은 어린이를 위한 것이며 겉보기에는 그리 어렵지 않습니다.
Prolog 기초
포켓몬 메커니즘이 내부적으로 얼마나 복잡해질 수 있는지 설명하기 전에, 먼저 논리형 프로그래밍이 어떻게 작동하는지 설명해야 합니다. 포켓몬 배틀은 본질적으로 매우 복잡한 규칙 엔진(rules engine)이기 때문에 포켓몬은 논리형 프로그래밍에 매우 잘 어울립니다. 먼저 여러 사실(facts)이 들어있는 파일을 만들어 보겠습니다.
Prolog에서는 '술어(Predicates)'를 선언합니다. 술어는 관계를 정의합니다. 이상해씨(bulbasaur)는 포켓몬(pokemon)이다, 파이리(charmander)는 포켓몬이다 등과 같이 말이죠. 우리는 이 술어를 'pokemon/1'이라고 부릅니다. 술어의 이름이 pokemon이고 하나의 인자(argument)를 갖기 때문입니다. 이러한 사실(팩트)들은 '최상위 레벨(top-level)'이라고 불리는 대화형 프롬프트에 로드됩니다. 프롬프트에 문장을 입력하여 최상위 레벨을 질의(query)할 수 있으며, Prolog는 해당 문장을 참(true)으로 만들 수 있는 모든 방법을 찾으려고 시도합니다. 가능한 솔루션이 두 개 이상인 경우, 최상위 레벨은 첫 번째 솔루션을 표시한 다음 사용자의 입력을 기다립니다. 그런 다음 사용자는 시스템에 (다음 결과를 확인하도록) 하나 더 표시하게 할 수 있습니다...
Prolog Basics Explained with Pokémon Prolog Basics Explained with Pokémon 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 ). 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. Typing is especially important. 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. 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. 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. 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. ?- 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 . 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 S