포켓몬 배틀의 타입 상성과 규칙을 예로 들어, Prolog의 사실·질의·변수·규칙이 어떻게 작동하는지와 SQL/스프레드시트와의 대비를 통해 논리 프로그래밍의 표현력을 설명한다.
January 05, 2026
이 글에 영감을 준 프로젝트는 좀 우스꽝스럽다—내가 지금부터 어린이용 비디오 게임의 메커니즘을 아주 자세히 설명하려고 하니까—하지만 바로 이 문제 덕분에 Prolog가 마침내 내 머릿속에서 ‘탁’ 하고 맞아떨어졌다. Bruce Tate의 “Seven Languages in Seven Weeks”를 읽은 뒤로 계속 찾아 헤매던 깨달음이었다.
이 연습을 통해, 내가 좀 더 실용적인 분야에서 만들고자 하는 인터페이스가 어떤 것인지에 대해 많은 것을 배웠다. 어떤 종류의 관계를 다룰 때, 논리 프로그래밍은 내가 써본 어떤 프로그래밍 시스템보다도 훨씬 간결하고 표현력이 풍부하다.
그게 왜 그런지 이해하려면, Pokémon 이야기를 해보자.
Pokémon은 인간이 다채로운 동물 캐릭터들의 동물원 같은 무리와 함께 살아가는 세계를 배경으로 한 비디오 게임 시리즈/멀티미디어 프랜차이즈/라이프스타일 브랜드다.
“Pokémon”은 프랜차이즈의 이름이기도 하고 그 동물 캐릭터들 자체를 가리키는 일반 명칭이기도 하다. 각 캐릭터는 고유한 종 이름을 갖고 있다. Pokémon의 종은 천 종이 훌쩍 넘는데, Bulbasaur(#1)부터 Pecharunt(#1025)까지 있다.



인기 있는 Pokémon으로는 (왼쪽에서 오른쪽 순서로):
Pikachu (#25), Archeops (#567) , 그리고 Dipplin (#1101).
요즘은 Pokémon 게임이 아주 다양하지만, 메인 시리즈는 언제나 포획과 배틀이 중심이었다. 배틀에서는 여섯 마리 Pokémon으로 이루어진 당신의 팀이 다른 팀과 맞붙는다. 각 Pokémon은 네 가지 기술을 갖추고 있고, 그중 하나를 선택해 (대개) 상대에게 피해를 준다. 당신이 당하기 전에 상대의 모든 Pokémon의 HP(Hit Points)를 0으로 만들어야 한다.
각 Pokémon은 배틀 방식에 영향을 주는 고유한 특성을 갖는다. 기본 스탯 세트가 있고, 가능한 기술 풀이 매우 크며, 몇 가지 특성(ability)과 타입(typing)이 있다. 잠시 후 보게 되겠지만, 여기서 조합 수가 엄청나게 많기 때문에 이를 소프트웨어로 추적해보려는 동기가 생긴다.

Scizor는 Bug/Steel 타입이며 Attack이 높고 Speed가 낮다 (출처: Smogon)
타입은 특히 중요하다. 기술에는 Fire나 Rock 같은 타입이 있고, Pokémon은 최대 두 타입을 가질 수 있다. 상대 Pokémon에게 Super Effective인 타입의 기술은 피해가 두 배가 되고, Not Very Effective인 타입의 기술은 피해가 절반이 된다.
예시로 보면 더 직관적이다. Fire 타입 기술 Flamethrower는 Grass 타입 Pokémon에게 2x를 주는데, Grass가 Fire에 약하기 때문이다. 반대로 Water 타입 기술 Surf는 Grass가 Water를 저항하기 때문에 피해가 ½만 들어간다.

Lunatone은 Rock/Psychic 타입이다. Rock은 Water에 약하고 Psychic은 Water에 대해 중립이므로, Surf는 2x 피해를 준다.
타입 보정은 누적될 수 있다. Scizor는 Bug/Steel 타입인데, Bug와 Steel 둘 다 Fire에 약하므로 Fire 기술은 Scizor에게 4x 피해를 준다. Electric은 Water에 강하지만 Ground는 Electric에 면역이므로, Water/Ground인 Swampert에게 Electric 타입 기술을 쓰면 0 피해를 준다. 0×2는 여전히 0이니까.
당연히, 이를 정리한 표가 있다.
Pokémon 타입 차트 (출처: Wikimedia)
이것들이 내가 8살 때 이해했던 Pokémon 비디오 게임의 사실상 핵심 메커니즘이다. 기술을 눌러 피해를 주고, 타입 상성이 좋은 기술을 누르려고 한다. 이 게임들은 어린이를 위한 것이고, 표면적으로는 그렇게 어렵지 않다.
Pokémon 메커니즘이 내부적으로 얼마나 괴상해질 수 있는지 설명하기 전에, 먼저 논리 프로그래밍이 어떻게 동작하는지 설명해야 한다. Pokémon은 논리 프로그래밍과 궁합이 아주 좋은데, Pokémon 배틀이 본질적으로 극도로 정교한 규칙 엔진이기 때문이다.
먼저 사실(fact)들을 잔뜩 담은 파일을 만들어보자.
pokemon(bulbasaur).
pokemon(ivysaur).
pokemon(venusaur).
pokemon(charmander).
pokemon(charmeleon).
pokemon(charizard).
pokemon(squirtle).
pokemon(wartortle).
pokemon(blastoise).
Prolog에서는 “predicate”를 선언한다. predicate는 관계를 정의한다. bulbasaur는 pokemon이고, charmander는 pokemon이고, 이런 식이다. 이 predicate를 pokemon/1이라고 부르는데, predicate 이름이 pokemon이고 인자가 1개이기 때문이다.
이 사실들은 “top-level”이라고 불리는 대화형 프롬프트에 로드된다. top-level은 프롬프트에 문장을 입력해 질의(query)하는 방식으로 사용하며, Prolog는 그 문장이 참이 되도록 만드는 모든 방법을 찾으려 한다. 가능한 해가 여러 개면, top-level은 첫 번째 해를 보여준 뒤 사용자 입력을 기다린다. 그러면 해를 하나 더 보여주거나, 모든 해를 보여주거나, 완전히 중단할 수 있다.
첫 번째 예시에서 pokemon(squirtle).을 입력하고 Enter를 누른다. top-level은 true.라고 답한다. Squirtle은 실제로 Pokémon이다.
?- pokemon(squirtle).
true.
모든 것이 Pokémon은 아니다.
?- pokemon(alex).
false.
이번에는 Pokémon 타입을 type/2 predicate로 추가해보자.
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).
어떤 Pokémon은 타입이 하나뿐이고, 어떤 Pokémon은 두 개를 가진다는 것을 기억하자. 후자의 경우에는 type 사실을 두 개로 모델링한다. Bulbasaur는 Grass 타입이고, Bulbasaur는 Poison 타입이다. 둘 다 참이다. 이 패러다임은 SQL 데이터베이스의 One-To-Many 관계와 비슷하다.
대화형으로 Squirtle이 water 타입인지 확인할 수 있다.
?- type(squirtle, water).
true.
Squirtle이 Grass 타입이라고 말할 수 있을까?
?- type(squirtle, grass).
false.
아니다. Squirtle은 Water 타입이기 때문이다.
Squirtle이 어떤 타입인지 모른다고 가정해보자. 물어볼 수 있다!
?- type(squirtle, Type).
Type = water.
Prolog에서 대문자로 시작하는 이름은 변수다. Prolog는 변수가 가능한 모든 매치와 함께 predicate를 “unify(통일/일치)”하려고 한다. 하지만 이 predicate를 참으로 만들 방법은 하나뿐이다. Squirtle의 유일한 타입이 Water이므로 Type은 water여야 한다.
두 타입을 가진 Pokémon은 predicate가 두 번 unify된다.
?- type(venusaur, Type).
Type = grass
; Type = poison.
의미적으로, 세 번째 줄 앞의 세미콜론은 “또는”을 뜻한다. type(venusaur, Type)은 Type = grass일 때도 참이고, Type = poison일 때도 참이다.
어떤 항이든 변수가 될 수 있으므로, 어떤 방향으로든 질문할 수 있다. Grass 타입은 모두 무엇일까? 첫 번째 인자를 변수로 두고, 두 번째 인자를 grass로 설정하면 된다.
?- type(Pokemon, grass).
Pokemon = bulbasaur
; Pokemon = ivysaur
; Pokemon = venusaur
; Pokemon = oddish
; Pokemon = gloom
; Pokemon = vileplume
; Pokemon = paras
; Pokemon = parasect
; Pokemon = bellsprout
; ... .
중간에서 끊었지만, 프롬프트는 기꺼이 164개 전부를 나열해줄 것이다.
쉼표는 여러 predicate를 나열하는 데 쓸 수 있다. Prolog는 모두가 참이 되도록 변수를 unify한다. Water/Ice 타입을 전부 나열하는 것은 Water와 Ice 타입 둘 다와 unify되는 Pokémon이 무엇인지 묻기만 하면 된다.
?- 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.
Pokemon이 변수이긴 하지만, 이 질의의 맥락에서는 두 번 등장하는 Pokemon이 반드시 같아야 한다(대수학에서처럼). 질의는 두 predicate가 모두 성립하는 Pokemon 값에 대해서만 unify된다. 예를 들어 Water/Ice 타입 Dewgong은, 우리 프로그램에 다음 두 사실이 있기 때문에 해가 된다.
type(dewgong, water).
type(dewgong, ice).
따라서 Pokemon 변수에 dewgong을 대입하면 질의가 만족된다. 반면 Squirtle은 Water 타입만이다. pokemon(squirtle, water)는 있지만 pokemon(squirtle, ice)는 없다. 질의는 둘 다 unify되기를 요구하므로 squirtle은 Pokemon의 가능한 값이 아니다.
Pokémon에는 가지고 놀 수 있는 데이터가 아주 많다. Iron Bundle은 Special Attack이 높은 강력한 Water/Ice 타입 Pokémon이다. 정확히 얼마나 높을까?
?- pokemon_spa(ironbundle, SpA).
SpA = 124.
Special Attack이 이렇게 높다면 강력한 Special 기술을 활용하고 싶다. Iron Bundle은 어떤 Special 기술을 배울까?
?- 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는 특히 좋은 Special 기술이다. 다음은 Special Attack이 120을 초과하는 Ice 타입 Pokémon 중 Freeze-Dry를 배우는 모든 Pokémon을 찾는 질의다.
?- 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.
다음으로 넘어가기 전에 마지막 개념 하나: 규칙(rule). 규칙에는 head와 body가 있고, body가 참이면 unify된다.
기술이 Physical 기술이거나 Special 기술이면 그 기술은 공격(피해를 주는) 기술로 간주된다. damaging_move/2 predicate는 직접 피해를 주는 모든 기술을 정의한다.
damaging_move(Move) :-
move_category(Move, physical)
; move_category(Move, special).
이것은 직접 피해를 주는 어떤 기술과도 unify된다.
?- damaging_move(tackle).
true.
?- damaging_move(rest).
false.
지금까지 보여준 것들은 논리적으로 아주 야심찬 내용은 아니다. 다양한 사실에 대한 “and”와 “or” 문장들일 뿐이다. 본질적으로는 과장된 조회 테이블에 가깝다. 그래도, SQL 같은 그럴듯한 대안과 비교했을 때 이 데이터베이스를 질의하는 일이 얼마나 더 쾌적한지 잠시 음미해보자.
지금까지 본 사실들에 대해서라면, 나는 아마 SQL 테이블을 이렇게 구성했을 것이다:
-- 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);
그리고 이렇게 질의한다:
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'
);
비교를 위해, 같은 Prolog 질의를 다시 적어보면:
?- pokemon_spa(Pokemon, SpA),
SpA #> 120,
learns(Pokemon, freezedry),
type(Pokemon, ice).
SQL을 까는 게 아니다—나는 SQL을 정말 좋아한다—하지만 SQL은 대부분의 사람이 접하는 최고의 선언형 질의 언어다. Prolog 버전이 이렇게까지 단순하고 유연하다는 게 내겐 놀랍다. 절을 계속 추가해 나가면 SQL 질의는 감당하기 어려울 정도로 복잡해질 텐데, Prolog 질의는 (변수가 동작하는 방식에 익숙해지기만 하면) 읽고 수정하기가 계속 쉽다.
기초를 마련했으니, 이제 내가 작업 중인 프로젝트의 맥락을 좀 설명하겠다.
Pokémon 배틀에는 복잡하고 확률적인 방식으로 서로 상호작용하는 메커니즘이 터무니없이 많다. 이 게임들의 매력 중 하나는, 그 모든 것을 상대보다 더 잘 머릿속에 넣어두고 그 정보를 바탕으로 상대의 계획을 더 잘 예측하고 더 잘 움직이려는(하지만 결국은 허망한) 시도다. 아주 우스운 Poker 같은 느낌이랄까.
내가 아직 언급하지 않은 게임 메커니즘의 작은 부분
이 게임을 위한 소프트웨어를 만들고 싶다면, 정신을 놓지 않으면서 이 복잡성을 모델링하는 것이 과제다. Prolog는 두 가지 주요 이유로 이것에 놀라울 정도로 강하다:
이를 보여주기 위해, 내가 Pokémon 드래프트 리그에서 우선도(priority) 기술을 어떻게 구현했는지 소개하겠다.
Pokémon 드래프트는 말 그대로다. Pokémon은 얼마나 좋은지에 따라 포인트 값을 부여받고, 각 플레이어는 쓸 수 있는 포인트가 주어지며, 모든 플레이어가 포인트를 다 쓸 때까지 드래프트한다. 팀은 대략 8~11마리 Pokémon으로 구성되고, 매주 리그의 다른 사람과 1대1로 붙는다. 내 친구이자 WMI 협업자인 Morry가 몇 년 전에 자신의 리그에 나를 초대했고, 그 뒤로 나는 이 포맷에 푹 빠졌다.
경기는 6v6이므로, 배틀의 큰 부분은 상대가 가져올 수 있는 여섯 마리의 모든 가능한 조합에 대비하고, 그 모든 조합을 상대할 수 있는 여섯 마리를 내 쪽에서도 구성하는 것이다.
당연히, 드래프트한 Pokémon으로만 팀을 만들 수 있다. 그래서 나는 그 predicate를 내 이름으로 만들었다: alex/1.
alex(meowscarada).
alex(weezinggalar).
alex(swampertmega).
alex(latios).
alex(volcarona).
alex(tornadus).
alex(politoed).
alex(archaludon).
alex(beartic).
alex(dusclops).
내 팀에서 Freeze-Dry를 배우는 Pokémon은 무엇이 있을까?
?- alex(Pokemon), learns(Pokemon, freezedry).
false.
없다. 젠장.
매우 중요한 기술 유형 중 하나가 우선도(priority) 기술이다. 앞에서 Speed 스탯이 누가 먼저 움직이는지 결정한다고 말했다. 약간의 нюанс가 있는데: 가장 높은 우선도의 기술을 사용한 Pokémon이 먼저 움직이고, 둘 다 같은 우선도의 기술을 선택했다면 Speed가 더 높은 쪽이 먼저 움직인다.
대부분의 기술은 우선도가 0이다.
?- 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
; Move = acidspray, P = 0
; Move = acrobatics, P = 0
; Move = acupressure, P = 0
; Move = aerialace, P = 0
; Move = aeroblast, P = 0
하지만 전부는 아니다! Accelerock의 우선도는 1이다. Accelerock을 쓰는 Pokémon은, 상대가 우선도 0(또는 그 이하)인 기술을 사용한다면, 상대의 Speed 스탯이 더 높더라도 먼저 움직인다.
나는 learns_priority/3 predicate를 정의하는데, 이는 Pokémon, 그 Pokémon이 배우는 우선도 기술, 그리고 그 기술의 우선도를 unify한다.
learns_priority(Pokemon, Move, P) :-
learns(Pokemon, Move),
move_priority(Move, P),
move_priority #> 0.
“내 팀은 어떤 우선도 기술을 배우지?”라는 단순한 질의는 엄청나게 많은 답을 반환한다.
?- 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.
이 결과는 기술적으로는 맞지만(가장 좋은 종류의 맞음), 대부분은 실제로 유용하지 않다. Helping Hand와 Ally Switch는 우선도가 매우 높지만, Double Battle에서만 의미가 있으며 내가 플레이하는 포맷이 아니다.
이를 고치기 위해, 나는 Double Battle 기술들을 모두 정의하고 제외한다. 기능적으로 쓸모없는 Bide도 제외하겠다. \+/1 predicate는 “이 목표가 실패할 때 참”, dif/2는 “두 항이 서로 다름”을 뜻한다.
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).
그러면 다음 결과를 얻는다:
?- 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.
훨씬 낫다. 하지만 Detect처럼 사용자 자신을 피해나 상태이상으로부터 보호하기 때문에 먼저 발동하는 기술들이 여전히 몇 개 섞여 있다. 내가 말하는 우선도 기술은 사실 그런 게 아니다. 내가 관심 있는 것은 Quick Attack이나 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).
이 규칙들을 적용하면, 아주 유용한 답에 도달한다!
?- 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.
이번 주 상대가 어떤 우선도 기술을 갖고 있는지 조회하는 것은 더더욱 유용하다.
?- 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.
여기서 나는 Morry에게 프로그램을 보여줬고, 그는 내게 도전 과제를 던졌다. Prankster 특성을 가진 Pokémon은 상태이상 기술(status moves)의 우선도가 추가로 +1 된다. 이걸 규칙에 확장해서 반영할 수 있겠냐는 것이었다.
마침 내 팀에 그런 Pokémon이 하나 있다.
?- alex(Pokemon), pokemon_ability(Pokemon, prankster).
Pokemon = tornadus
; false.
Prolog의 if/then 구성인 ->/2를 사용해서, 이건 3분 걸렸다.
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.
이제 같은 질의는 Tornadus가 배우는 모든 status 기술을 증가된 우선도로 포함한다.
?- 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.
서두에서 이 경험이 내가 만들고 싶은 인터페이스의 종류에 대해 가르쳐줬다고 말했다. 그 교훈 중 하나는 꽤 명백하다. Prolog는 약간 투박할 수 있지만, 여기서 설명한 것 같은 관계를 표현하고 질의하기에는 우아한 언어다. 이는 당신이 나처럼 프로그래밍을 위해 선언형 DSL을 신중하게 사용하는 것에 관심이 있다면 의미가 있다.
또 다른 교훈은, 비-프로그래머에게 어떤 종류의 도구가 먹히는가에 관한 것이다.
나는 “상대 팀이 어떤 우선도 기술을 갖고 있는지 알면 좋겠다”라고 생각한 첫 번째 사람이 아니다. Pokémon 커뮤니티에는 이런 자원이 있는데, 역사상 최고의 프로그래밍 인터페이스인 소박한 스프레드시트로 만들어져 있다.

보기에도 훨씬 예쁘다.
나는 “Techno’s Prep Doc”의 사본을 사용한다. 야생에서 가끔 마주치는, 엄청나게 고급진 Google Sheets 중 하나다. 팀을 입력하면 매치업에 관한 유용한 정보를 잔뜩 생성해준다. 인터페이스가 훌륭하고, 다양한 포맷을 지원하며, 훑어보기 좋은 시각 자료가 있고, 자동완성까지 있다.
나는 우선도 기술을 찾는 수식이 궁금했다. 아주 흉악하다.
={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"))}
조금 클릭해보니, 이게 무엇을 하는지 대강 파악할 수 있었다. “Backend” 시트에 모든 기술 목록이 있다. 사실상 내 Prolog 질의를 하드코딩한 버전이다.
조회 수식은 필터링과 VLOOKUP, 그리고 약간의 메타프로그래밍(INDIRECT는 셀 참조를 반환한다)을 통해, 당신의 팀에 있는 Pokémon 중 Backend 목록에 들어 있는 Pokémon을 찾아 표시한다.
개인적으로는, 스프레드시트 VLOOKUP으로 구현된 버전보다 Prolog로 구현된 이 데이터베이스 버전을 작업하는 편을 선호하는 이유가 여럿 있다. 나는 기존 Pokémon 도구 모음으로는 못 하는 일들을 하는 웹앱을 만들 계획이다. (물론 내가 scryer-prolog를 WASM으로 컴파일할 수만 있다면 말이지만.)
게다가 Prolog 패러다임은 분명 더 확장 가능하다. 스프레드시트 백엔드는 주목할 만한 기술들의 하드코딩 목록이지만, 내 데이터베이스는 어떤 기술이든 조회할 수 있다. Tornadus가 배우는 Special 기술 중 Justin의 팀 구성원 누구에게든 상성이 좋은(super-effective) 기술을 전부 찾는 이 질의는 아직도 믿기 어렵다. 내가 아는 어떤 도구에도 이런 건 없다—보통은 끝없이 탭을 바꿔가며 알아내려 하는 종류의 일이다. 내 프로그램이 만든 문법이 있으니, 이건 30초 만에 만들었다.
?- 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.
?-
하지만 나는 구조적 프로그래밍이 스프레드시트보다 확장 가능하다는 사실에는 관심이 없다. 내가 왜 모든 프로그래밍을 스프레드시트로 하지 않는지, 나는 이미 알고 있다.
={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"))}
내가 매우 중요하다고 생각하는 질문은 이것이다: 이 특정한 문제, 그리고 이를 해결하려는 동기를 가진 사람들의 유형과 관련해, 이용 가능한 가장 잘 유지보수된 해법이 스프레드시트인 이유는 무엇일까?
세상에는 그런 문제가 매우 많다고 믿는다. 그리고 그 프로그래밍 패러다임을 개선할 여지도 아직 제대로 실현되지 않은 부분이 많다고 생각한다.
Morry Kolman이 이 블로그 초안을 읽어준 것에 감사한다.
\+/2나 ->/2 predicate를 쓰면 안 된다고 한다. 하지만 제시된 대체 수단으로 내가 원하는 것을 어떻게 표현해야 할지 잘 모르겠다. 누가 도와주고 싶다면 기꺼이 수정하겠다.