Restore prolog-pokemon curated post
This commit is contained in:
@@ -0,0 +1,409 @@
|
||||
---
|
||||
title: "用宝可梦解释 Prolog 基础"
|
||||
date: 2026-05-18T13:30:00+08:00
|
||||
draft: false
|
||||
description: "通过宝可梦对战系统理解逻辑编程语言 Prolog 的核心概念——事实、规则、查询,以及它为什么比 SQL 和电子表格更灵活。"
|
||||
tags:
|
||||
- Prolog
|
||||
- 宝可梦
|
||||
- 编程语言
|
||||
- 翻译
|
||||
poet: |
|
||||
众里寻他千百度,
|
||||
蓦然回首,那人却在,灯火阑珊处。
|
||||
——辛弃疾,中国,1140–1207
|
||||
---
|
||||
|
||||
> 原文作者:Alexander Petros
|
||||
> 原文链接:[Prolog Basics Explained with Pokémon](https://unplannedobsolescence.com/blog/prolog-basics-pokemon/)
|
||||
> 本文已获授权翻译,有删节和注释。
|
||||
|
||||
启发这篇文章的项目有点傻——我要详细描述一个儿童电子游戏的机制——但正是这个特定问题最终让我真正理解了 Prolog。这是自从读了 Bruce Tate 的《七周七语言》以来一直在追寻的顿悟。
|
||||
|
||||
对于某些类型的关系,逻辑编程是我用过的最简洁、最具表现力的编程系统。要理解为什么,让我们来聊聊宝可梦。
|
||||
|
||||
## 宝可梦基础
|
||||
|
||||
宝可梦是一个电子游戏系列,设定在一个人类与各种色彩缤纷的动物角色共存的世界。"Pokémon" 既是系列名称,也是这些动物角色的统称,每个角色都有自己的物种名称。从妙蛙种子(#1)到桃歹郎(#1025),共有超过一千种不同的宝可梦物种。
|
||||
|
||||
<div style="display:flex;gap:8px;justify-content:center;margin:1rem 0"><img src="/images/pokemon/pikachu.png" alt="皮卡丘" style="width:32%"><img src="/images/pokemon/archeops.png" alt="始祖大鸟" style="width:32%"><img src="/images/pokemon/dipplin.png" alt="裹蜜虫" style="width:32%"></div>
|
||||
|
||||
{{< caption >}}受欢迎的宝可梦包括(从左到右):皮卡丘(#25)、始祖大鸟(#567)和裹蜜虫(#1101)。{{< /caption >}}
|
||||
|
||||
现在有各种各样的宝可梦游戏,但主系列始终围绕着捕捉和对战。在对战中,你的六只宝可梦队伍与另一支队伍交锋。每只宝可梦配备四个招式,通常用来对对手造成伤害。你需要将对方所有宝可梦的 HP 减到零,同时防止对方先对你做同样的事。
|
||||
|
||||
每只宝可梦都有独特的特性影响对战表现:基础能力值、可学的招式、特性和属性组合。这里的庞大组合数量正是试图用软件来追踪这些信息的动机。
|
||||
|
||||

|
||||
|
||||
{{< caption >}}巨钳螳螂是虫/钢属性,攻击力高但速度低(数据来自 Smogon)。{{< /caption >}}
|
||||
|
||||
**速度**决定哪个招式先出手;**攻击和特攻**分别影响物理招式和特殊招式的伤害;**防御和特防**影响受到的伤害。
|
||||
|
||||
**属性**尤其重要。招式有属性(如火或岩石),宝可梦可以拥有最多两种属性。如果招式属性对对方宝可梦效果绝佳,造成双倍伤害;效果不佳则只造成一半伤害。
|
||||
|
||||

|
||||
|
||||
{{< caption >}}月石是岩石/超能力属性。岩石弱水,超能力对水中性,所以冲浪会造成 2 倍伤害。{{< /caption >}}
|
||||
|
||||
举个直观的例子:火属性的喷射火焰对草属性宝可梦造成 2 倍伤害(草弱火),但水属性的冲浪只造成 ½ 伤害(草抗水)。
|
||||
|
||||
属性修正可以叠加。巨钳螳螂是虫/钢属性,虫和钢都弱火,所以火属性招式对它造成 4 倍伤害。电弱水,但地面免疫电——如果你对水/地面的巨沼怪使用电属性招式,伤害为零,因为 0×2 还是 0。
|
||||
|
||||

|
||||
|
||||
{{< caption >}}宝可梦属性相克表。{{< /caption >}}
|
||||
|
||||
这些基本上就是我 8 岁时理解的宝可梦机制。点击招式造成伤害,尽量选择属性相克有利的招式。这些游戏是为儿童设计的,表面上看并不难。
|
||||
|
||||
## Prolog 基础
|
||||
|
||||
在解释宝可梦机制底层有多复杂之前,先解释逻辑编程的工作原理。宝可梦非常适合逻辑编程,因为宝可梦对战本质上是一个极其精密的规则引擎。
|
||||
|
||||
首先创建一个包含事实的文件:
|
||||
|
||||
```prolog
|
||||
pokemon(bulbasaur).
|
||||
pokemon(ivysaur).
|
||||
pokemon(venusaur).
|
||||
pokemon(charmander).
|
||||
pokemon(charmeleon).
|
||||
pokemon(charizard).
|
||||
pokemon(squirtle).
|
||||
pokemon(wartortle).
|
||||
pokemon(blastoise).
|
||||
```
|
||||
|
||||
在 Prolog 中,我们声明"谓词"(Predicate)。谓词定义关系:`bulbasaur` 是一个 `pokemon`,`charmander` 是一个 `pokemon`,以此类推。我们称这个谓词为 `pokemon/1`,因为它有一个参数。
|
||||
|
||||
这些事实被加载到一个交互式提示符——"顶层"(top-level)中。你输入一个语句到提示符,Prolog 试图找出所有使该语句为真的方式:
|
||||
|
||||
```
|
||||
?- pokemon(squirtle).
|
||||
true.
|
||||
```
|
||||
|
||||
不是所有东西都是宝可梦。
|
||||
|
||||
```
|
||||
?- pokemon(alex).
|
||||
false.
|
||||
```
|
||||
|
||||
接着添加宝可梦的属性,作为谓词 `type/2`:
|
||||
|
||||
```prolog
|
||||
type(bulbasaur, grass).
|
||||
type(bulbasaur, poison).
|
||||
type(ivysaur, grass).
|
||||
type(ivysaur, poison).
|
||||
type(charmander, fire).
|
||||
type(squirtle, water).
|
||||
type(blastoise, water).
|
||||
```
|
||||
|
||||
有些宝可梦只有一种属性,有些有两种。后一种情况用两个 `type` 事实建模。妙蛙种子是草属性,也是毒属性——两者都为真。
|
||||
|
||||
交互式查询:
|
||||
|
||||
```
|
||||
?- type(squirtle, water).
|
||||
true.
|
||||
?- type(squirtle, grass).
|
||||
false.
|
||||
```
|
||||
|
||||
Prolog 中首字母大写的名字是**变量**。Prolog 尝试将谓词与变量的所有可能匹配进行"合一":
|
||||
|
||||
```
|
||||
?- type(squirtle, Type).
|
||||
Type = water.
|
||||
```
|
||||
|
||||
对于有两种属性的宝可梦,谓词会合一两次:
|
||||
|
||||
```
|
||||
?- type(venusaur, Type).
|
||||
Type = grass
|
||||
; Type = poison.
|
||||
```
|
||||
|
||||
分号 `;` 表示"或"。任何参数都可以是变量,这意味着我们可以从任意方向提问。所有草属性宝可梦有哪些?只需把第一个参数设为变量:
|
||||
|
||||
```
|
||||
?- type(Pokemon, grass).
|
||||
Pokemon = bulbasaur
|
||||
; Pokemon = ivysaur
|
||||
; Pokemon = venusaur
|
||||
; ...
|
||||
```
|
||||
|
||||
逗号用于列出多个谓词——Prolog 会合一变量使得所有谓词都为真。列出所有水/冰属性的宝可梦:
|
||||
|
||||
```
|
||||
?- type(Pokemon, water), type(Pokemon, ice).
|
||||
Pokemon = dewgong
|
||||
; Pokemon = cloyster
|
||||
; Pokemon = lapras
|
||||
; Pokemon = ironbundle
|
||||
; false.
|
||||
```
|
||||
|
||||
Iron Bundle 是一只水/冰属性宝可梦,特攻很高。具体多高?
|
||||
|
||||
```
|
||||
?- pokemon_spa(ironbundle, SpA).
|
||||
SpA = 124.
|
||||
```
|
||||
|
||||
特攻这么高,我们想利用强力的特殊招式。Iron Bundle 会哪些特殊招式?
|
||||
|
||||
```
|
||||
?- learns(ironbundle, Move), move_category(Move, special).
|
||||
Move = blizzard
|
||||
; Move = freezedry
|
||||
; Move = hydropump
|
||||
; Move = icebeam
|
||||
; Move = icywind
|
||||
; ...
|
||||
```
|
||||
|
||||
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
|
||||
; Pokemon = kyuremwhite, SpA = 170
|
||||
; Pokemon = ironbundle, SpA = 124.
|
||||
```
|
||||
|
||||
最后一个概念:**规则**(Rules)。规则有头部和主体,如果主体为真,则规则合一:
|
||||
|
||||
```prolog
|
||||
damaging_move(Move) :-
|
||||
move_category(Move, physical)
|
||||
; move_category(Move, special).
|
||||
```
|
||||
|
||||
```
|
||||
?- damaging_move(tackle).
|
||||
true.
|
||||
?- damaging_move(rest).
|
||||
false.
|
||||
```
|
||||
|
||||
## SQL 对比
|
||||
|
||||
到目前为止展示的内容在逻辑上并不复杂——只是关于各种事实的"与"和"或"语句。不过请花一点时间体会一下,查询这个数据库比 SQL 舒服多少。
|
||||
|
||||
我会这样设置 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);
|
||||
```
|
||||
|
||||
然后查询特攻 > 120、会 Freeze-Dry 的冰属性宝可梦:
|
||||
|
||||
```sql
|
||||
SELECT DISTINCT pokemon, 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 查询仍然易于阅读和编辑。
|
||||
|
||||
## 升级
|
||||
|
||||
宝可梦对战拥有数量惊人的机制,以复杂且概率性的方式相互影响。我尚未提及的部分机制:
|
||||
|
||||
- 某些招式有一定概率落空
|
||||
- 某些招式会提升或降低能力值
|
||||
- 宝可梦可以携带具有各种效果的物品
|
||||
- 伤害计算不是恒定的,而是呈正态分布
|
||||
- 宝可梦可以被冰冻、灼伤、麻痹、中毒或陷入睡眠
|
||||
- 各种场地效果(天气、戏法空间等)会改变招式伤害和出手顺序
|
||||
- 每只宝可梦都有一个特性(如漂浮免疫地面招式、降雨改变天气、强行增伤 1.3 倍)
|
||||
- 玩家分配努力值提升选定能力值
|
||||
|
||||
如果你想为这个游戏构建软件,挑战在于在所有复杂性的建模过程中不疯掉。Prolog 在这方面出奇地擅长,主要有两个原因:
|
||||
|
||||
- 查询模型擅长描述临时组合
|
||||
- 数据模型非常适合以一致的方式分层叠加规则
|
||||
|
||||
为了说明这一点,以下是我为宝可梦选秀联赛实现优先度招式的过程。
|
||||
|
||||
宝可梦选秀大致就是字面意思。宝可梦根据实力被赋予分值,每个玩家获得一定分数,轮流选直到用完。你的队伍最终会有大约 8-11 只宝可梦,每周你和联赛中的另一个人正面交锋。
|
||||
|
||||
我定义的队伍:
|
||||
|
||||
```prolog
|
||||
alex(meowscarada).
|
||||
alex(weezinggalar).
|
||||
alex(swampertmega).
|
||||
alex(latios).
|
||||
alex(volcarona).
|
||||
alex(tornadus).
|
||||
alex(politoed).
|
||||
alex(archaludon).
|
||||
alex(beartic).
|
||||
alex(dusclops).
|
||||
```
|
||||
|
||||
我的宝可梦里有哪些会 Freeze-Dry?
|
||||
|
||||
```
|
||||
?- alex(Pokemon), learns(Pokemon, freezedry).
|
||||
false.
|
||||
```
|
||||
|
||||
一个都没有。唉。
|
||||
|
||||
非常重要的一类招式为**优先度招式**。大多数招式的优先度为零:
|
||||
|
||||
```
|
||||
?- move_priority(Move, P).
|
||||
Move = absorb, P = 0
|
||||
; Move = accelerock, P = 1
|
||||
; Move = acid, P = 0
|
||||
; ...
|
||||
```
|
||||
|
||||
Accelerock 的优先度为 1。使用它的宝可梦会先于任何使用优先度 0 招式的宝可梦行动,即使后者速度更高。
|
||||
|
||||
我定义一个 `learns_priority/3` 谓词:
|
||||
|
||||
```prolog
|
||||
learns_priority(Pokemon, Move, P) :-
|
||||
learns(Pokemon, Move),
|
||||
move_priority(Move, P),
|
||||
P #> 0.
|
||||
```
|
||||
|
||||
"我的队伍学会了哪些优先度招式"——返回了大量答案,但很多是双打专用招式。过滤掉双打招式和保护招式:
|
||||
|
||||
```prolog
|
||||
learns_priority(Mon, Move, Priority) :-
|
||||
learns(Mon, Move),
|
||||
\+ doubles_move(Move),
|
||||
\+ protection_move(Move),
|
||||
Move \= bide,
|
||||
move_priority(Move, Priority),
|
||||
Priority #> 0.
|
||||
|
||||
doubles_move(helpinghand).
|
||||
doubles_move(allyswitch).
|
||||
doubles_move(ragepowder).
|
||||
protection_move(detect).
|
||||
protection_move(protect).
|
||||
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 = suckerpunch, Priority = 1.
|
||||
```
|
||||
|
||||
优先度招式的缺乏实际上是我的队伍的一大弱点。
|
||||
|
||||
同样地查询对手的优先度招式也非常有用:
|
||||
|
||||
```
|
||||
?- morry(Pokemon), learns_priority(Pokemon, Move, Priority).
|
||||
Pokemon = mawilemega, Move = suckerpunch, Priority = 1
|
||||
; Pokemon = walkingwake, Move = aquajet, Priority = 1
|
||||
; ...
|
||||
```
|
||||
|
||||
此时,Morry 提了一个挑战:恶作剧之心特性的宝可梦,其变化招式额外获得 +1 优先度。能否在规则中体现?
|
||||
|
||||
我的队伍恰好有一只这样的宝可梦——Tornadus。使用 Prolog 的 if/then 结构 `->/2`,三分钟搞定:
|
||||
|
||||
```prolog
|
||||
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 所有变化招式的提升后优先度。
|
||||
|
||||
## 与电子表格的比较
|
||||
|
||||
宝可梦社区已经有类似的工具,构建在最优秀的编程界面上:朴素的电子表格。
|
||||
|
||||

|
||||
|
||||
{{< caption >}}Techno 的准备文档,一份功能强大的 Google Sheets。{{< /caption >}}
|
||||
|
||||
我使用一份被称为"Techno's Prep Doc"的 Google Sheets,输入队伍后自动生成海量对阵信息。
|
||||
|
||||
我好奇它的优先度招式查找公式是怎样的。
|
||||
|
||||

|
||||
|
||||
{{< caption >}}电子表格后端的硬编码招式列表。{{< /caption >}}
|
||||
|
||||
结果是——相当复杂:
|
||||
|
||||
```
|
||||
={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"))}
|
||||
```
|
||||
|
||||
Prolog 的范式显然更具可扩展性。电子表格后端是一个硬编码的显要招式列表,而我的数据库可以查找**任何**招式。这个查询——找出 Tornadus 学会的、对 Justin 队伍中任何成员效果绝佳的特殊招式——在现有工具中根本不存在。我只用了 30 秒就拼出来了:
|
||||
|
||||
```
|
||||
?- justin(Target), learns(tornadus, Move),
|
||||
super_effective_move(Move, Target), move_category(Move, special).
|
||||
Target = charizardmegay, Move = chillingwater
|
||||
; Target = scizor, Move = heatwave
|
||||
; Target = scizor, Move = incinerate
|
||||
; Target = screamtail, Move = sludgebomb
|
||||
; ...
|
||||
```
|
||||
|
||||
## 最后的思考
|
||||
|
||||
这个项目始于一个玩笑——"如果我拿 Prolog 来做宝可梦组队会怎样"——但它教给我的逻辑编程知识比任何教科书都多。
|
||||
|
||||
我最大的领悟是:**Prolog 迫使你思考什么是真的,而不是做什么。** 在命令式编程中,大部分精力花在控制流上。在 Prolog 中,你只需陈述什么是真的,让求解器处理剩下的事。
|
||||
|
||||
对于一个有着数千条互动规则的规则引擎(比如宝可梦对战),这简直是天启。与其写 if/else 链检查属性相克,不如声明属性表,让 Prolog 处理其余部分。
|
||||
|
||||
如果你感兴趣,推荐:
|
||||
1. 安装 SWI-Prolog 并玩玩 [prologdex](https://github.com/alexpetros/prologdex) 数据集
|
||||
2. 阅读 [Adventure in Prolog](http://www.amzi.com/AdventureInProlog/) 教程
|
||||
3. 用 Prolog 对你熟悉的领域建模——它会彻底改变你对数据和关系的思考方式
|
||||
|
||||
祝编码愉快,愿你的属性相克永远是效果绝佳。
|
||||
Reference in New Issue
Block a user