포켓몬을 통해 이해하는 Prolog 기초 개념
요약
본 글은 포켓몬스터 게임의 배틀 시스템을 예시로 들어 논리 프로그래밍(Logic Programming)의 개념과 필요성을 설명합니다. 포켓몬의 복잡한 타입 상성 및 데미지 계산 메커니즘(예: 핫삼에게 불꽃 기술이 4배 피해를 주는 경우, 대짱이가 전기 기술에 면역인 경우)은 방대한 조합을 가지며, 이를 추적하고 모델링하는 데 논리 프로그래밍이 매우 간결하고 표현력이 풍부함을 보여줍니다.
핵심 포인트
- 논리 프로그래밍(Logic Programming)은 특정 종류의 관계를 다룰 때 가장 간결하고 표현력이 풍부한 시스템이다.
- 포켓몬 배틀 메커니즘은 포켓몬 종, 기술, 타입, 상성 등 복잡하게 얽힌 방대한 조합을 포함한다.
- 타입 보정(Type Adjustments)은 중첩될 수 있으며, 특정 타입의 조합에 따라 데미지 계산이 매우 복잡해진다 (예: 핫삼).
- 논리 프로그래밍은 이러한 복잡한 규칙 기반 시스템(Rule-based System)을 모델링하는 데 적합하다.
- 포켓몬 게임 메커니즘 분석은 논리 프로그래밍 학습의 동기 부여가 되었다.
2026년 1월 5일
이 포스트에 영감을 준 프로젝트는 조금 우스꽝스럽습니다. 어린이용 비디오 게임의 메커니즘을 매우 상세하게 설명하려고 하기 때문입니다. 하지만 이 특정한 문제가 드디어 저에게 Prolog가 무엇인지 깨닫게 해주었습니다. 이는 Bruce Tate의 “Seven Languages in Seven Weeks”를 읽은 이후로 제가 계속 찾아 헤매던 깨달음이었습니다.
이 연습을 통해 저는 좀 더 실용적인 영역에서 구축하려는 인터페이스의 종류에 대해 많은 것을 배웠습니다. 특정 종류의 관계를 다룰 때, 논리 프로그래밍 (Logic Programming)은 제가 지금까지 사용해 본 프로그래밍 시스템 중 단연코 가장 간결하고 표현력이 풍부합니다.
그 이유를 이해하기 위해, 포켓몬 (Pokémon)에 대해 이야기해 봅시다.
포켓몬은 인간이 다채로운 동물 캐릭터들과 함께 살아가는 세계를 배경으로 하는 비디오 게임 시리즈이자 멀티미디어 프랜차이즈, 그리고 라이프스타일 브랜드입니다.
“Pokémon”은 프랜차이즈의 이름인 동시에, 각각 고유한 종(species)의 이름을 가진 동물 캐릭터 자체를 일컫는 일반 명칭이기도 합니다. 이상해꽃 (#1)부터 피charunt (#1025)까지, 천 개가 넘는 서로 다른 포켓몬 종이 존재합니다.
현재 온갖 종류의 포켓몬 게임이 있지만, 메인 시리즈는 항상 포켓몬을 잡고 배틀하는 것에 관한 것이었습니다. 배틀 중에는 여러분의 포켓몬 6마리로 구성된 팀이 상대 팀과 맞붙습니다. 각 포켓몬은 상대에게 (보통) 데미지를 입히기 위해 선택할 수 있는 네 가지 기술 (moves)을 갖추고 있습니다. 상대방이 여러분에게 데미지를 입히기 전에 상대방 포켓몬의 모든 HP (Hit Points)를 0으로 만들어야 합니다.
각 포켓몬은 배틀 방식에 영향을 미치는 고유한 특성을 가지고 있습니다. 이들은 일련의 종족값 (base stats), 방대한 기술 풀 (pool of possible moves), 몇 가지 특성 (abilities), 그리고 타입 (typing)을 가집니다. 잠시 후에 보게 되겠지만, 여기서 발생하는 엄청난 수의 조합이 바로 이를 소프트웨어로 추적하려고 시도하게 만드는 동기입니다.
타입 (Typing)은 특히 중요합니다. 기술에는 불꽃 (Fire)이나 바위 (Rock)와 같은 타입이 있으며, 포켓몬은 최대 두 개의 타입을 가질 수 있습니다. 상대 포켓몬에게 효과가 굉장한 (Super Effective) 타입을 가진 기술은 두 배의 데미지를 입히고, 효과가 별로인 (Not Very Effective) 기술은 절반의 데미지만 입힙니다.
예시를 통해 보면 조금 더 직관적입니다. 불꽃 (Fire) 타입 기술인 불꽃세례 (Flamethrower)는 풀 (Grass) 타입 포켓몬에게 2배의 데미지를 입힙니다. 풀 타입은 불꽃 타입에 약하기 때문입니다. 반면 물 (Water) 타입 기술인 파도타기 (Surf)는 풀 타입 포켓몬에게 1/2의 데미지만 입히는데, 이는 풀 타입이 물 타입을 저항하기 때문입니다.
타입 보정은 중첩될 수 있습니다.
핫삼 (Scizor)은 벌레/강철 (Bug/Steel) 타입이며, 벌레와 강철 타입 모두 불꽃 타입에 약하기 때문에 불꽃 타입 기술은 핫삼에게 4배의 데미지를 입힙니다.
전기 (Electric) 타입은 물 타입에 약하지만, 땅 (Ground) 타입은 면역입니다. 따라서 물/땅 (Water/Ground) 타입인 대짱이 (Swampert)에게 전기 타입 기술을 사용하면 0×2는 여전히 0이므로 데미지를 전혀 입히지 못합니다.
당연히 이를 추적하는 데 도움이 되는 상성표가 존재합니다.
이것들이 제가 8살이었을 때 이해했던 포켓몬 비디오 게임의 실질적인 메커니즘입니다. 데미지를 입히기 위해 기술을 클릭하고, 타입 상성이 좋은 기술을 클릭하려고 노력하는 것이죠. 이 게임들은 어린이를 위한 것이며, 표면적인 수준에서는 그리 어렵지 않습니다.
포켓몬의 메커니즘이 내부적으로 얼마나 복잡해질 수 있는지 설명하기 전에, 먼저 논리 프로그래밍 (Logic Programming)이 어떻게 작동하는지 설명해야 합니다. 포켓몬은 논리 프로그래밍에 매우 적합한데, 포켓몬 배틀은 본질적으로 매우 복잡한 규칙 엔진 (Rules Engine)이기 때문입니다.
먼저 여러 개의 사실 (Facts)이 담긴 파일을 만드는 것부터 시작해 봅시다.
pokemon(bulbasaur).
pokemon(ivysaur).
pokemon(venusaur).
...
Prolog에서는 "술어 (Predicates)"를 선언합니다.
술어는 관계를 정의합니다: bulbasaur는 pokemon이고, charmander도 pokemon이라는 식입니다.
우리는 이 술어를 pokemon/1이라고 부르는데, 술어의 이름이 pokemon이고 인자 (Argument)가 하나이기 때문입니다.
이러한 사실들은 "톱 레벨 (Top-level)"이라고 불리는 대화형 프롬프트에 로드됩니다. 프롬프트에 문장을 입력함으로써 톱 레벨에 질의 (Query)를 던지면, Prolog는 그 문장을 참으로 만들 수 있는 모든 방법을 찾으려고 시도합니다. 가능한 해결책이 하나 이상 있을 경우, 톱 레벨은 첫 번째 해결책을 표시한 후 사용자의 입력을 기다립니다. 그러면 사용자는 해결책을 하나 더 표시하거나, 모든 해결책을 표시하거나, 혹은 완전히 중단하도록 할 수 있습니다.
이 첫 번째 예시에서, 우리는 pokemon(squirtle).을 입력하고 엔터를 누릅니다.
톱 레벨은 true.라고 응답합니다.
스쿼틀은 실제로 포켓몬입니다.
?- pokemon(squirtle).
true.
모든 것이 포켓몬인 것은 아닙니다.
?- pokemon(alex).
false.
여기에 type/2 술어(predicate)를 사용하여 포켓몬 타입을 추가해 보겠습니다.
type(bulbasaur, grass).
type(bulbasaur, poison).
type(ivysaur, grass).
...
일부 포켓몬은 타입이 하나만 있는 반면 다른 포켓몬은 두 가지 타입을 가집니다. 후자의 경우, 이는 두 개의 type 사실(fact)로 모델링됩니다.
불바사르는 Grass 타입이고, 불바사르는 Poison 타입입니다. 둘 다 참입니다.
이 패러다임은 SQL 데이터베이스의 One-To-Many 관계와 유사합니다.
대화형으로 스쿼틀이 물 타입인지 확인할 수 있습니다.
?- type(squirtle, water).
true.
스쿼틀이 Grass 타입이라고 말할 수 있을까요?
?- type(squirtle, grass).
false.
아닙니다. 스쿼틀은 물 타입이기 때문입니다.
만약 우리가 스쿼틀의 타입을 모른다고 가정해 봅시다. 질문을 할 수 있습니다!
?- type(squirtle, Type).
Type = water.
Prolog에서 대문자로 시작하는 이름은 변수(variable)입니다. Prolog는 술어를 변수의 가능한 모든 일치 값과 “일관성 있게 만들려고(unify)” 시도합니다.
하지만 이 특정 술어를 참으로 만드는 방법은 오직 하나뿐입니다: Type은 water여야 합니다.
두 가지 타입을 가진 포켓몬의 경우, 술어는 두 번 일관성을 만듭니다.
?- type(venusaur, Type).
Type = grass
; Type = poison.
의미론적으로, 세 번째 줄의 선행 세미콜론(;)은 “또는(or)”을 의미합니다.
type(venusaur, Type)은 Type = grass이거나 Type = poison일 때 참입니다.
모든 항(term)이 변수일 수 있으므로, 어떤 방향으로든 질문할 수 있습니다.
어떤 Grass 타입들이 있나요?
첫 번째 인자만 변수로 만들고 두 번째 인자를 grass로 설정하면 됩니다.
?- type(Pokemon, grass).
Pokemon = bulbasaur
; Pokemon = ivysaur
...
저는 생략했지만, 프롬프트는 164개를 모두 기꺼이 나열할 것입니다.
쉼표(Commas)는 여러 술어 (Predicates)를 나열하는 데 사용될 수 있습니다. Prolog는 이 모든 술어들이 참이 되도록 변수 (Variables)를 단일화 (Unify)합니다. 모든 물/얼음 타입 (Water/Ice types)을 나열하는 것은 물 타입과 얼음 타입 모두와 단일화되는 포켓몬이 무엇인지 묻는 문제와 같습니다.
?- type(Pokemon, water), type(Pokemon, ice).
Pokemon = dewgong
; Pokemon = cloyster
...
Pokemon은 변수이지만, 질의 (Query)의 맥락 내에서 두 인스턴스는 (대수학에서와 마찬가지로) 동일해야 합니다.
질의는 두 술어가 모두 성립하는 Pokemon의 값에 대해서만 단일화됩니다.
예를 들어, 물/얼음 타입인 Dewgong은 우리 프로그램에 다음과 같은 두 가지 사실 (Facts)이 포함되어 있기 때문에 하나의 해답이 됩니다.
type(dewgong, water).
type(dewgong, ice).
따라서 Pokemon 변수에 dewgong을 대입하면 질의를 만족합니다.
반면, Squirtle은 단순히 물 타입일 뿐입니다. pokemon(squirtle, water)는 존재하지만, pokemon(squirtle, ice)는 존재하지 않습니다.
질의는 두 조건 모두가 단일화될 것을 요구하므로, squirtle은 Pokemon의 가능한 값이 될 수 없습니다.
포켓몬은 여러분이 다룰 수 있는 많은 데이터를 가지고 있습니다. Iron Bundle은 특수공격 (Special Attack)이 높은 강력한 물/얼음 타입 포켓몬입니다. 정확히 얼마나 높을까요?
?- pokemon_spa(ironbundle, SpA).
SpA = 124.
특수공격이 그 정도로 높다면, 강력한 특수 기술 (Special moves)을 활용하고 싶을 것입니다. Iron Bundle은 어떤 특수 기술을 알고 있을까요?
?- learns(ironbundle, Move), move_category(Move, special).
Move = aircutter
; Move = blizzard
...
Freeze-Dry는 특히 좋은 특수 기술입니다. 다음은 특수공격이 120보다 크면서 Freeze-Dry를 배우는 모든 얼음 타입 포켓몬을 찾는 질의입니다.
?- pokemon_spa(Pokemon, SpA), SpA #> 120, learns(Pokemon, freezedry), type(Pokemon, ice).
Pokemon = glaceon, SpA = 130
; Pokemon = kyurem, SpA = 130
...
다음 단계로 넘어가기 전 마지막 개념은 규칙 (Rules)입니다. 규칙은 머리 (Head)와 몸체 (Body)를 가지며, 몸체가 참일 경우 단일화됩니다.
어떤 기술이 물리 기술 (Physical Move)이거나 특수 기술 (Special Move)이라면, 그 기술은 공격 기술 (Damaging move)로 간주됩니다.
damaging_move/2
이 술어 (predicate)는 직접적인 피해를 주는 모든 기술을 정의합니다.
damaging_move(Move) :-
move_category(Move, physical)
; move_category(Move, special).
이것은 직접적인 피해를 주는 모든 기술과 단일화 (unify)됩니다.
?- damaging_move(tackle).
true.
?- damaging_move(rest).
...
지금까지 보여드린 내용은 논리적으로 말하자면 그리 야심 찬 내용은 아닙니다. 그저 다양한 사실 (facts)들에 대한 "그리고 (and)"와 "또는 (or)" 문장일 뿐입니다. 본질적으로는 미화된 조회 테이블 (lookup table)에 불과합니다. 그럼에도 불구하고, SQL과 같은 그럴듯한 대안보다 이 데이터베이스를 쿼리 (query)하는 것이 얼마나 더 멋진 일인지 잠시 감상해 보시기 바랍니다.
지금까지 본 사실들을 바탕으로, 저는 아마 SQL 테이블을 다음과 같이 설정했을 것입니다:
-- 간결함을 위해 다른 능력치들은 생략함
CREATE TABLE pokemon (pokemon_name TEXT, special_attack INTEGER);
CREATE TABLE pokemon_types(pokemon_name TEXT, type TEXT);
...
그런 다음 다음과 같이 쿼리합니다:
SELECT DISTINCT pokemon, special_attack
FROM pokemon as p
WHERE
...
비교를 위해, 이에 상응하는 Prolog 쿼리를 다시 보여드립니다:
?- pokemon_spa(Pokemon, SpA),
SpA #> 120,
learns(Pokemon, freezedry),
...
SQL을 비난하는 것은 아닙니다. 저도 SQL을 사랑합니다. 하지만 SQL은 대부분의 사람들이 접하는 가장 뛰어난 선언적 쿼리 언어 (declarative query language)입니다. Prolog 버전이 얼마나 더 단순하고 유연한지는 저에게 매우 놀랍게 다가옵니다. 만약 우리가 절 (clauses)을 계속 추가한다면 SQL 쿼리는 감당할 수 없을 정도로 복잡해지겠지만, Prolog 쿼리는 (변수가 어떻게 작동하는지 익히고 나면) 읽기 쉽고 편집하기 쉬운 상태를 유지합니다.
기초를 다졌으니, 제가 작업 중인 프로젝트에 대한 배경 설명을 드리겠습니다.
포켓몬 배틀에는 복잡하고 확률적인 방식으로 상호작용하는 엄청난 수의 메커니즘이 존재합니다. 이 게임들의 매력 중 하나는 상대방보다 이 모든 정보를 머릿속에 더 잘 담아두려는 헛된 시도이며, 그 정보를 사용하여 상대의 계획을 예측하고 앞질러 나가는 것입니다. 이것은 일종의 매우 유치한 포커 (Poker) 게임과 같습니다.
이 게임을 위한 소프트웨어를 구축하고자 한다면, 그 모든 복잡성을 정신줄을 놓지 않고 모델링(Modeling)하는 것이 과제입니다. Prolog는 크게 두 가지 이유로 이 작업에 놀라울 정도로 뛰어납니다.
이를 설명하기 위해, 제가 운영하는 포켓몬 드래프트 리그 (Pokémon draft league)를 위해 우선도 기술 (Priority moves)을 어떻게 구현했는지 보여드리겠습니다.
포켓몬 드래프트 (Pokémon draft)는 이름 그대로입니다. 포켓몬은 성능에 따라 포인트 값이 부여되고, 각 플레이어에게는 사용할 수 있는 일정량의 포인트가 주어지며, 모든 플레이어가 포인트를 다 쓸 때까지 드래프트를 진행합니다. 결과적으로 여러분의 팀은 약 8~11마리의 포켓몬을 갖게 되며, 매주 리그의 다른 상대와 맞붙게 됩니다. 제 친구이자 WMI 협력자인 Morry가 몇 년 전 저를 자신의 리그에 초대했고, 저는 그 이후로 이 방식에 매료되었습니다.
경기는 6대6으로 진행되므로, 전투의 큰 부분은 상대가 가져올 수 있는 가능한 모든 6마리의 조합에 대비하고, 그 모든 조합을 상대할 수 있는 자신만의 6마리를 구성하는 것입니다.
당연히, 여러분은 자신이 드래프트한 포켓몬으로만 팀을 구성할 수 있습니다.
저는 방금 그 술어 (Predicate)를 제 이름으로 만들었습니다: alex/1
alex(meowscarada).
alex(weezinggalar).
alex(swampertmega).
...
내가 가진 포켓몬 중 프리즈드라이 (Freeze-Dry)를 배우는 포켓몬은 무엇인가?
?- alex(Pokemon), learns(Pokemon, freezedry).
false.
없습니다. 아쉽네요.
매우 중요한 기술 유형 중 하나는 우선도 기술 (Priority moves)입니다. 앞서 스피드 (Speed) 능력치가 어떤 포켓몬이 먼저 움직일지를 결정한다고 언급했습니다. 약간의 세부 사항을 덧붙이자면, 가장 높은 우선도를 가진 기술을 사용한 포켓몬이 먼저 움직이며, 만약 두 포켓몬이 동일한 우선도의 기술을 선택했다면 스피드가 더 높은 포켓몬이 먼저 움직입니다.
대부분의 기술은 우선도가 0입니다.
?- move_priority(Move, P).
Move = '10000000voltthunderbolt', P = 0
; Move = absorb, P = 0
...
아, 하지만 모든 기술이 그런 것은 아닙니다! 아쿠아제트 (Accelerock)는 우선도가 1입니다. 아쿠아제트를 사용하는 포켓몬은, 상대 포켓몬의 스피드 능력치가 더 높더라도 우선도가 0(또는 그 이하)인 기술을 사용하는 어떤 포켓몬보다 먼저 움직입니다.
저는 learns_priority/3를 정의합니다.
포켓몬, 그 포켓몬이 배우는 우선도 기술, 그리고 그 기술의 우선도를 결합(unify)하는 술어(predicate)입니다.
learns_priority(Pokemon, Move, P) :-
learns(Pokemon, Move),
move_priority(Move, P),
...
"내 팀이 배우는 우선도 기술은 무엇인가?"라고 묻는 간단한 질의(query)는 매우 많은 답변을 반환합니다.
?- alex(Pokemon), learns_priority(Pokemon, Move, Priority).
Pokemon = meowscarada, Move = endure, Priority = 4
; Pokemon = meowscarada, Move = helpinghand, Priority = 5
...
이것이 기술적으로는 정확하지만(가장 좋은 방식이죠), 이 답변들 대부분은 실제로 유용하지 않습니다. Helping Hand와 Ally Switch는 우선도가 매우 높지만, 제가 플레이하는 방식이 아닌 더블 배틀(Double Battles)에서만 의미가 있기 때문입니다.
이를 해결하기 위해, 저는 모든 더블 배틀 기술을 정의하고 이를 제외할 것입니다.
또한 기능적으로 쓸모없는 기술인 Bide도 제외할 예정입니다. \+/1 술어(predicate)는 "이 목표가 실패하면 참(true)"을 의미하며, dif/2는 "이 두 항(term)이 서로 다르다"를 의미합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 HN AI Posts의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기