用宝可梦解释 Prolog 基础
摘要
通过宝可梦属性相克作为示例,介绍 Prolog 编程,展示逻辑编程如何优雅地建模关系数据。
<p><a href="https://lobste.rs/s/0cvxwk/prolog_basics_explained_with_pokemon">评论</a></p>
查看缓存全文
缓存时间: 2026/05/15 06:57
# 用宝可梦解释 Prolog 基础
来源:https://unplannedobsolescence.com/blog/prolog-basics-pokemon/
## 计划性过时 (https://unplannedobsolescence.com/)
- 首页 (https://unplannedobsolescence.com/)
- 博客 (https://unplannedobsolescence.com/blog)
- 演讲 (https://unplannedobsolescence.com/talks)
- 关于 (https://unplannedobsolescence.com/about)
- RSS (https://unplannedobsolescence.com/atom.xml)
---
2026年1月5日
启发这篇文章的项目有点傻气——我将非常详细地描述一个儿童电子游戏的机制——但正是这个特定问题让我终于理解了 Prolog,这是我一直以来寻找的顿悟,自从读了 Bruce Tate 的《七周七语言》后我就在寻找。
这个练习让我学到了很多关于我试图在更实际的领域 (https://alexanderpetros.com/triptych/) 构建的接口类型。对于某些类型的关系,逻辑编程是我用过的最简洁、最具表现力的编程系统。
为了理解原因,让我们来谈谈宝可梦。
## 宝可梦基础 (https://unplannedobsolescence.com/blog/prolog-basics-pokemon/#pokemon-basics)
宝可梦是一个电子游戏系列/多媒体特许经营/生活方式品牌,设定在一个人类与一群色彩缤纷的动物角色共同生活的世界。
“宝可梦”既是系列的名称,也是这些动物角色本身的通用术语,它们都有各自的物种名称。从妙蛙种子 (#1 (https://bulbapedia.bulbagarden.net/wiki/Bulbasaur_(Pok%C3%A9mon))) 到桃歹郎 (#1025 (https://bulbapedia.bulbagarden.net/wiki/Pecharunt_(game))),有一千多种不同的宝可梦物种。
宝可梦皮卡丘,一只电系老鼠;宝可梦始祖大鸟,一只色彩鲜艳的岩石鸟;宝可梦裹蜜虫,看起来有点像掉在地上的糖苹果。
流行的宝可梦包括(从左到右):
皮卡丘 (#25) (https://bulbapedia.bulbagarden.net/wiki/Pikachu_(Pok%C3%A9mon))、始祖大鸟 (#567) (https://bulbapedia.bulbagarden.net/wiki/Archeops_(Pok%C3%A9mon)) 和裹蜜虫 (#1101) (https://bulbapedia.bulbagarden.net/wiki/Dipplin_(Pok%C3%A9mon))。
现在有各种各样的宝可梦游戏,但主系列一直围绕着捕捉和战斗。在对战中,你的六只宝可梦队伍与另一支队伍对战。每只宝可梦装备有四个招式,可以选择(通常)对对手造成伤害。你需要将所有对手宝可梦的 HP(体力值)降到零,而他们也要对你做同样的事。
每只宝可梦都有独特的特性,影响其战斗方式。它们有一套基础种族值、一个庞大的可能招式池、几种特性和一个属性。稍后你会看到,这里巨大的组合数量是尝试用软件追踪它的动机。
一张显示巨钳螳螂特性、属性和种族值的截图。巨钳螳螂是虫/钢属性,拥有高攻击和低速度(来自 Smogon (https://www.smogon.com/dex/bw/pokemon/scizor/))。
属性尤其重要。招式有属性,比如火或岩石,而宝可梦最多可以拥有两种属性。一个招式如果对对手的宝可梦效果绝佳,会造成两倍伤害;如果效果不好,则造成一半伤害。
用例子更容易理解。火属性招式喷射火焰对草属性宝可梦造成 2 倍伤害,因为草弱火,但水属性招式冲浪只造成 1/2 伤害,因为草抗水。
在第三代宝可梦游戏中,美纳斯对月石使用冲浪。月石 (https://bulbapedia.bulbagarden.net/wiki/Lunatone_(Pok%C3%A9mon)) 是岩石/超能力属性。岩石弱水,超能力对水属性中立,所以冲浪造成 2 倍伤害。
属性修正可以叠加。巨钳螳螂 (https://bulbapedia.bulbagarden.net/wiki/Scizor_(Pok%C3%A9mon)) 是虫/钢属性,而且虫和钢都弱火,所以火属性招式对巨钳螳螂造成 4 倍伤害。电属性弱水,但地面属性免疫,所以如果你对水/地面属性的巨沼怪 (https://bulbapedia.bulbagarden.net/wiki/Swampert_(Pok%C3%A9mon)) 使用电属性招式,会造成 0 伤害,因为 0×2 还是 0。
自然,有一张图表可以帮助你记忆。
宝可梦属性相克表。宝可梦属性相克表(来自 Wikimedia (https://commons.wikimedia.org/wiki/File:Pokemon_Type_Chart.svg))
这基本上就是我 8 岁时理解的宝可梦游戏机制。点击招式造成伤害,尝试使用属性相克好的招式。这些游戏是给儿童玩的,表面上并不难。
## Prolog 基础 (https://unplannedobsolescence.com/blog/prolog-basics-pokemon/#prolog-basics)
在我解释宝可梦机制在底层可以多么古怪之前,我需要先解释逻辑编程是如何工作的。宝可梦非常适合逻辑编程,因为宝可梦对战本质上是一个极其复杂的规则引擎。
让我们从一个包含大量事实的文件开始。
``
pokemon(bulbasaur).
pokemon(ivysaur).
pokemon(venusaur).
pokemon(charmander).
pokemon(charmeleon).
pokemon(charizard).
pokemon(squirtle).
pokemon(wartortle).
pokemon(blastoise).
``
在 Prolog 中,我们声明“谓词”。谓词定义关系:`bulbasaur` 是一个 `pokemon`,`charmander` 是一个 `pokemon`,依此类推。我们称这个谓词为 `pokemon/1`,因为谓词名称是 `pokemon`,它有一个参数。
这些事实被加载到一个称为“顶级”的交互式提示符中。你通过在提示符中输入一个语句来查询顶级;Prolog 尝试找到所有使该语句成立的方式。当有多个可能解时,顶级会显示第一个解,然后等待用户输入。然后你可以让它再显示一个解、所有解,或者完全停止。
在第一个例子中,我们输入 `pokemon(squirtle).` 并回车。顶级回答 `true.` 杰尼龟 (https://bulbapedia.bulbagarden.net/wiki/Squirtle_(Pok%C3%A9mon)) 确实是宝可梦。
``
?- pokemon(squirtle).
true.
``
并非所有东西都是宝可梦。
``
?- pokemon(alex).
false.
``
让我们添加宝可梦属性,作为谓词 `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).
``
回想一下,有些宝可梦只有一种属性,而另一些有两种。在后一种情况下,用两个 `type` 事实来建模。妙蛙种子 (https://bulbapedia.bulbagarden.net/wiki/Bulbasaur_(Pok%C3%A9mon)) 是草属性,妙蛙种子也是毒属性;两者都成立。这种范式类似于 SQL 数据库中的一对多关系。
交互式地,我们可以确认杰尼龟是否是水属性。
``
?- type(squirtle, water).
true.
``
我们能说杰尼龟是草属性吗?
``
?- type(squirtle, grass).
false.
``
不能,因为杰尼龟是水属性。
假设我们不知道杰尼龟是什么属性。我们可以问!
``
?- type(squirtle, Type).
Type = water.
``
在 Prolog 中,以大写字母开头的名称是变量。Prolog 尝试将谓词与变量的所有可能匹配进行“合一”。只有一种方式可以使这个特定的谓词成立:`Type` 必须是 `water`,因为杰尼龟的唯一属性是水。
对于有两种属性的宝可梦,谓词会合一两次。
``
?- type(venusaur, Type).
Type = grass
; Type = poison.
``
从语义上讲,第三行开头的分号表示“或”。`type(venusaur, Type)` 在 `Type = grass` 或 `Type = poison` 时为真。
任何项都可以是变量,这意味着我们可以从任何方向提问。所有草属性是什么?只需让第一个参数成为变量,并将第二个参数设置为 `grass`。
``
?- type(Pokemon, grass).
Pokemon = bulbasaur
; Pokemon = ivysaur
; Pokemon = venusaur
; Pokemon = oddish
; Pokemon = gloom
; Pokemon = vileplume
; Pokemon = paras
; Pokemon = parasect
; Pokemon = bellsprout
; ... .
``
我截断了,但提示符会乐意列出全部 164 个。
逗号可用于列出多个谓词——Prolog 会合一变量,使得所有谓词都为真。列出所有水/冰属性只需询问存在哪些宝可梦同时与水和冰属性合一。
``
?- 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` 值进行合一。例如,水/冰属性的白海狮 (https://bulbapedia.bulbagarden.net/wiki/Dewgong_(Pok%C3%A9mon)) 是一个解,因为我们的程序包含以下两个事实:
``
type(dewgong, water).
type(dewgong, ice).
``
因此,用 `dewgong` 替换 `Pokemon` 变量满足该查询。相比之下,杰尼龟只是水属性:存在 `pokemon(squirtle, water)`,但不存在 `pokemon(squirtle, ice)`。查询要求两者都合一,因此 `squirtle` 不是 `Pokemon` 的可能值。
宝可梦有很多数据可以玩。铁包袱 (https://bulbapedia.bulbagarden.net/wiki/Iron_Bundle_(Pok%C3%A9mon)) 是强大的水/冰属性宝可梦,拥有高特攻。具体有多高?
``
?- pokemon_spa(ironbundle, SpA).
SpA = 124.
``
特攻这么高,我们想利用强大的特殊招式。铁包袱知道哪些特殊招式?
``
?- 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.
``
冰冻干燥是一个特别好的特殊招式。这是一个查询,寻找所有特攻大于 120 且学会冰冻干燥的冰属性宝可梦。
``
?- 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.
``
在继续之前,最后一个概念:规则。规则有头部和主体,如果主体为真,则规则合一。
如果一个招式是物理招式或特殊招式,则被认为是伤害招式。`damaging_move/2` 谓词定义了所有造成直接伤害的招式。
``
damaging_move(Move) :-
move_category(Move, physical)
; move_category(Move, special).
``
这将与任何造成直接伤害的招式合一。
``
?- damaging_move(tackle).
true.
?- damaging_move(rest).
false.
``
## SQL 比较 (https://unplannedobsolescence.com/blog/prolog-basics-pokemon/#sql-comparison)
到目前为止,我展示的内容在逻辑上并不雄心勃勃——只是关于各种事实的“与”和“或”语句。它基本上是一个美化的查找表。尽管如此,花点时间欣赏一下查询这个数据库比可能的替代方案(如 SQL)要好得多。
对于我们看到的事实,我可能会像这样设置 SQL 表:
``
-- 省略其他种族值以保持简洁
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——但这是大多数人接触过的最好的声明式查询语言。令我惊讶的是,Prolog 版本如此简单和灵活。如果我们继续添加子句,SQL 查询会变得难以管理地复杂,而 Prolog 查询仍然易于阅读和编辑(一旦你掌握了变量的工作原理)。
## 升级 (https://unplannedobsolescence.com/blog/prolog-basics-pokemon/#level-up)
在奠定基础之后,这里提供一些我正在做的项目的背景信息。
宝可梦对战有数量惊人的机制,它们以复杂和概率性的方式相互作用。这些游戏的部分吸引力在于徒劳地尝试比对手更好地记住所有机制,利用这些信息来预测和智取对手的计划。这有点像非常傻的扑克。
我尚未提及的一小部分游戏机制:
- 某些招式有一定几率 miss,不造成伤害。
- 某些招式会提升或降低宝可梦的种族值。
- 宝可梦可以携带具有各种效果的物品。
- 伤害计算不是恒定的;招式在计算范围内造成正态分布的伤害。
- 宝可梦可能被冰冻、烧伤、麻痹、中毒或睡眠;这些都有各种不利影响。
- 有各种场地效果(如天气、场地、戏法空间),它们会改变招式伤害、出手顺序和其他东西。
- 每只宝可梦都有一个特性,具有各种效果,例如漂浮使你免疫地面招式,降雨在宝可梦换入时将天气变为雨天,强行禁用招式的副作用但将伤害乘以 1.3 倍。
- 玩家在游戏前(无形中)为每只宝可梦分配点数,以提升选定的种族值。根据队伍的构建方式,每只宝可梦可能比你预期的造成更多伤害或更好地承受攻击。
如果你想为这个游戏构建软件,挑战在于在不失去理智的情况下建模所有这些复杂性。Prolog 在这方面出奇地好,主要有两个原因:
1. 查询模型擅长描述临时组合。
2. 数据模型非常适合以一致的方式分层规则。
为了说明这一点,这是我如何为我的宝可梦选秀联赛实现先制招式。
宝可梦选秀基本就是字面意思。宝可梦根据其强度被赋予一个分值,每个玩家获得一定数量的点数来花费,然后你选秀直到每个玩家花完点数。你的队伍最终有大约 8-11 只宝可梦,每周你与联赛中的另一个人对决。我的朋友和 WMI (https://wemakeinter.net/) 合作者 Morry (https://wttdotm.com/) 几年前邀请我参加他的联赛,从那以后我就沉迷于这种形式了。
比赛是 6v6,所以战斗的一个很大部分是准备你的对手可能带来的所有六只组合,并组建你自己的六只能够应对所有组合的队伍。
自然,你只能用你选到的宝可梦来组建队伍。我刚刚做了这个谓词,用我的名字命名:`alex/1`。
``
alex(meowscarada).
alex(weezinggalar).
alex(swampertmega).
alex(latios).
alex(volcarona).
alex(tornadus).
alex(politoed).
alex(archaludon).
alex(beartic).
alex(dusclops).
``
我有哪些宝可梦学会冰冻干燥?
``
?- alex(Pokemon), learns(Pokemon, freezedry).
false.
``
一个都没有。可恶。
一种非常重要的招式类型是先制招式。我之前提到速度种族值控制哪只宝可梦先出手
相似文章
Prolog编程的陷阱
关于Prolog编程中常见陷阱的指南,强调使用纯声明式构造而非不纯的构造,如cut、全局状态和低级I/O。
无点逻辑编程
本文探讨了无点逻辑编程,这是一个与函数式编程范式相关的概念。
状态思维
本文解释了从命令式编程转向声明式编程所需的概念转变,并通过Prolog来阐述如何从关系而非可变状态的角度进行思考。
我对Prolog的不满
Hillel Wayne的一篇博文,详细讲述了他对Prolog编程语言的不满,包括字符串问题、缺乏函数、数据类型有限以及cut操作等。
Datalog
关于Datalog的全面笔记:什么是Datalog,如何用多种语言实现,以及在程序分析中的应用,附带代码示例和资源。