用宝可梦解释 Prolog 基础

Lobsters Hottest 工具

摘要

通过宝可梦属性相克作为示例,介绍 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编程的陷阱

Hacker News Top

关于Prolog编程中常见陷阱的指南,强调使用纯声明式构造而非不纯的构造,如cut、全局状态和低级I/O。

无点逻辑编程

Lobsters Hottest

本文探讨了无点逻辑编程,这是一个与函数式编程范式相关的概念。

状态思维

Lobsters Hottest

本文解释了从命令式编程转向声明式编程所需的概念转变,并通过Prolog来阐述如何从关系而非可变状态的角度进行思考。

我对Prolog的不满

Hillel Wayne — Computer Things

Hillel Wayne的一篇博文,详细讲述了他对Prolog编程语言的不满,包括字符串问题、缺乏函数、数据类型有限以及cut操作等。

Datalog

Lobsters Hottest

关于Datalog的全面笔记:什么是Datalog,如何用多种语言实现,以及在程序分析中的应用,附带代码示例和资源。