地下城与卷轴
周师傅带着小哲,从一行「机器码」开始,一路打通十二层地牢,亲手做出一个命令行文本冒险游戏。每打通一层,你就多掌握一批编程概念——这就是编程语言的世界地图。
收徒:一张地图
motivation来了?坐。听说你想学编程?
嗯!我想做个游戏,就从这个最简单的开始——命令行文本冒险。玩家输入指令,勇者在地牢里走来走去、打怪、捡宝。我已经会写几行 Python 了!
好,有目标才有动力。不过在动手之前,我先给你看一张地图。
编程世界的概念不是一堆散装名词。它们是一棵家族树:每一层的概念,都是为了解决上一层留下的麻烦而生的。从最底下「机器只懂 0 和 1」,一直长到今天的大模型时代——一共 12 层。
咱们就把这棵家族树,当成一座十二层的地下城。你每学透一层概念,就在游戏里用一次,相当于打通一层楼。我们的武器,就是这款游戏——《地下城与卷轴》。
打地牢!我喜欢!那这张地图长什么样?
喏,这就是我们要逐层打通的地下城地图——
记住一句话:编程语言的所有概念,都是「人类偷懒 + 防错」的产物。越往上走,抽象越高,离硬件越远,但离「人的思维」越近。
好了,拿起你的剑(键盘),我们下地牢。第一层,从最底下开始——先搞清楚电脑到底是个什么东西。
根:机器只懂 0 和 1
机器码 → 汇编 → 变量这一层没有代码,只有「为什么」。搞懂它,后面所有楼层的地基就稳了。
师傅,我一直想问:我写 print("勇者出发!"),电脑是怎么「懂」的?它认识中文吗?
哈哈,电脑其实是个大笨蛋。它只认识两样东西:通电和断电,也就是 0 和 1。你写的每一行高级代码,最终都会变成一串 0 和 1 去驱动它。
可是人记不住 01001000 01100101 01101100 01101100 01101111 这种玩意儿啊,于是出现了「助记符」——把机器码翻译成人能看的缩写,这就是汇编语言。
; 这段汇编的意思是:把数值 100 放进寄存器 AX(相当于暂存格)
MOV AX, 100
; 再把 AX 的值搬进内存地址 0x1000
MOV [0x1000], AX
; 跳转到 0x2000 继续执行
JMP 0x2000
可你看,还是有问题:0x1000 里存的那 100,到底是勇者的血量?是金币?还是伤害值?没人记得住哪个地址里装的是什么——这就是最底下那层留给我们的麻烦。
于是,人类发明了编程史上最重要的偷懒:给数据起名字。这就是「变量」。
int hp = 100; /* 在内存里划一块地方,名字叫 hp,初始值 100 */
int gold = 30; /* 再来一块,叫 gold */
hp = hp - 15; /* 被蝙蝠咬了 15 点血 —— 本质是改内存里的数字 */
hp = 100 # 不需要写类型,Python 自己猜
gold = 30
hp -= 15 # 偷懒写法,等价于 hp = hp - 15
print("勇者剩余生命:", hp) # 勇者剩余生命:85
本层核心概念
- 机器码:电脑真正执行的 0/1 序列,人类没法直接写。
- 汇编:机器码的助记符,如
MOV / ADD / JMP,比机器码好记一点,但仍然要对着寄存器操作。 - 变量:给内存地址起个名字。底层还是一个地址,但人不用记了——这就是「偷懒」。
打通第 0 层的收获:你记不住内存地址 → 变量诞生。从此我们写游戏不用管「血量存在 0x1000 还是 0x2000」,只需要说 hp。
控制流:岔路口的勇者
if / switch / while勇者走进地牢……然后呢?程序默认只会从上往下「一条道走到黑」。控制流让程序会做选择、会重复。
我写好了第一版!勇者走进地牢……然后游戏就结束了。玩家啥都干不了,只能看。好无聊!
因为你还没给程序装上「大脑」。程序天生只会顺序执行:一行一行往下跑。想让它聪明,就要引入控制流——条件让程序走不同分支,循环让程序重复干活。
print("勇者走进了幽暗的地牢……")
print("前方有一个岔路口。")
print("(游戏结束)") # 玩家:就这??
看好了,加上 if,勇者就有了「选择」;加上 while,游戏就有了「循环」——只要勇者没死、没逃出去,游戏就一直转下去。这就是文本冒险的主循环。
hp = 100
escaped = False # 是否逃出地牢
# 主循环:只要活着且没逃出去,就一直玩
while hp > 0 and not escaped:
print("\n你站在岔路口。向左(1)是宝箱,向右(2)是怪物的低吼声。")
choice = input("输入 1 或 2:")
if choice == "1":
print("你发现了一个宝箱!获得 20 金币。")
gold = 30 + 20
elif choice == "2":
print("一只蝙蝠扑了过来!你损失 15 点生命。")
hp -= 15
else:
print("指令无效,你愣了一下。") # 防呆:处理非法输入
if hp <= 0:
print("勇者倒下了……游戏结束。")
print("—— 游戏结束 ——")
本层核心概念
- 顺序执行:程序默认的走路方式,一行一行往下。
- 条件分支:
if / elif / else让不同情况走不同分支;场景多了也可以用switch / match(Python 3.10+ 的match就是它的亲戚)。 - 循环:
while(条件成立就一直转)、for(数着次数转)、do-while(至少转一次)。场景:重复执行,不想复制粘贴。
打通第 1 层的收获:「不同情况走不同分支」用 if;「重复执行不抄代码」用循环。游戏立刻从「放 PPT」变成了「能玩」。
复用:战斗函数与回声洞穴
函数 / 参数 / 递归打蝙蝠、打哥布林、打骷髅——同一套战斗逻辑写三遍?函数说:不行,咱们写一遍。
师傅,游戏能玩了!但我把「打蝙蝠」的代码复制了一份改成「打哥布林」……现在代码里有三份几乎一样的战斗逻辑,改个伤害数值要改三个地方,快疯了。
哈哈哈,这就是复用要解决的麻烦:不想把所有代码都堆在一起。把「一段逻辑」打包成一个函数,起个名字,需要的时候调用它。这就是第二层——函数 / 过程 / 子程序。
def attack(attacker, defender):
"""一次攻击:造成伤害,并返回实际伤害值。"""
damage = attacker["atk"] - defender["def"]
if damage < 1:
damage = 1 # 至少掉 1 点血
defender["hp"] -= damage
return damage
bat = {"name": "蝙蝠", "hp": 20, "atk": 8, "def": 1}
goblin = {"name": "哥布林", "hp": 40, "atk": 10, "def": 3}
dmg1 = attack(player, bat) # 勇者打蝙蝠
dmg2 = attack(goblin, player) # 哥布林打勇者
print(f"{bat['name']} 受到 {dmg1} 点伤害!")
print(f"你受到 {dmg2} 点伤害,剩余 HP:{player['hp']}")
函数的三个衍生品
- 参数:函数入口的「输入口」,
attack(谁, 打谁)。 - 返回值:函数算完交给外面的「输出口」,
return damage。 - 作用域:函数里定义的变量是「局部」的,出去了就不认识——避免互相污染。
明白了!那「递归」又是啥?听说很难。
不慌。咱们地牢里有个「回声洞穴」——洞穴里面套着更小的洞穴,一直套到不能再套为止。这种「问题本身是自相似的」结构,最适合递归:函数调用它自己。
比如算 5 的阶乘 5! = 5×4×3×2×1:5! 的问题里藏着 4! 的问题,4! 里藏着 3!……一直缩到 1! = 1 这个「地基」,然后一路把答案传回来。
def explore_cave(depth):
"""回声洞穴:每一层都套着一个更小的洞。"""
if depth == 0: # 地基:最深处,到底了
print(" 你摸到了洞穴最深处,捡到一颗发光的石头!")
return 1
print(" 进入洞穴第", depth, "层……")
treasure = explore_cave(depth - 1) # 先探索更深的洞
print(" 回到第", depth, "层,带上战利品返回。")
return treasure + 1
total = explore_cave(3) # 从第 3 层开始往下钻
print("共收获", total, "件宝物")
本层核心概念
- 函数 / 过程 / 子程序:一段逻辑写一次、到处调用——解决「复制粘贴」。
- 递归:函数调用自己,适合处理「自相似」的问题——树、目录、汉诺塔、阶乘。
打通第 2 层的收获:战斗、捡宝、对话全抽成函数,游戏代码从「一团浆糊」变成了「说明书」。下一层,我们该收拾那一堆零散的变量了。
数据组织:把属性捆起来
数组 / 结构体 / 指针玩家的 hp、atk、def、位置、金币、背包……七八个零散变量满世界飞。该把「相关数据」捆成团了。
救命……我现在有 hp、max_hp、atk、def、pos_x、pos_y、gold、inventory……每次调用函数要传 8 个参数,传错一个就全乱套。
这就是第三层的核心:数据组织——把相关的数据捆起来。三种武器:数组(同类型的多个值)、结构体(不同类型的相关数据)、指针(间接访问内存)。
inventory = ["小药水", "木剑"] # 背包:一列同类物品
inventory.append("生锈的钥匙") # 捡到东西,塞进背包
item = inventory.pop() # 拿出最后一件(钥匙)
print("背包里有:", inventory) # ['小药水', '木剑']
print("你拿出了:", item) # 你拿出了:生锈的钥匙
# Python 里最朴素的「结构体」:一个 dict(字典)
player = {"name": "勇者阿哲", "hp": 100, "atk": 12, "def": 5,
"pos": (3, 5), "gold": 30, "inventory": []}
def attack(attacker, defender):
damage = attacker["atk"] - defender["def"]
defender["hp"] -= max(damage, 1) # 现在只传一个「人」,不用传 8 个参数!
player2 = player # 注意:这只是引用的复制
struct Player {
char name[32]; /* 名字,32 字节 */
int hp; /* 血量 */
int atk; /* 攻击力 */
int def; /* 防御力 */
}; /* 这些字段在内存里是挨着排的 */
struct Player p = {"阿哲", 100, 12, 5};
p.hp -= 15; /* 直接用 . 访问字段 */
结构体把数据捆好了,那「怪物的队伍」呢?地牢里怪物一个一个出现,数量不定,用数组装不下——于是有了指针:每个怪物身上挂一个 next,指向下一个怪物,串成一条链表。
指针的本质就一句话:不直接拿数据,先拿「数据住在哪」。它是链表、树、图这些高级结构的地基。
struct Monster {
int hp;
struct Monster *next; /* 指针:下一个怪物的地址 */
};
struct Monster m1 = {20, NULL};
struct Monster m2 = {40, NULL};
m1.next = &m2; /* m1 后面接上 m2(& 表示取地址) */
/* 顺着链表走一圈 */
for (struct Monster *cur = &m1; cur != NULL; cur = cur->next) {
printf("发现怪物,HP=%d\n", cur->hp);
}
本层核心概念
- 数组 / 列表:同类型多个值(背包、队伍)。
- 结构体 / 记录:不同类型但相关的数据放一起(玩家的属性们),衍生:字段、对齐、内存布局。
- 指针 / 引用:间接访问——先拿「地址」再拿「值」,衍生:链表、树、图。
打通第 3 层的收获:所有属性收进一个 player,函数传参从 8 个变成 1 个。数据开始「有形状」了。下一层,我们把这些形状「武装」起来——它们将拥有行为。
抽象与封装:把游戏拆成房间
模块 / 命名空间 / 类game.py 3000 行了。小哲要找「攻击函数」,得滚动鼠标滚三分钟。该给代码盖房子了——分成一个个房间(模块),再把数据和操作打包成「会动的结构体」(类)。
师傅,我的 game.py 已经 3000 行了,滚轮都滚出火星子了。上次我改了个函数名,结果在文件里搜出 17 处调用,漏改两处,游戏直接崩。
这就是第四层要解决的:隐藏细节,暴露接口。两个武器——
第一,模块 / 命名空间:把相关函数和数据分组,装进不同的文件,避免名字撞车。player.py 管玩家、monster.py 管怪物、map.py 管地图。
第二,类(class):把数据(字段)和操作(方法)打包成一个整体。还记得第 3 层的 player 字典吗?现在让玩家自己会动——这就是「会走路的结构体」。
class Player:
def __init__(self, name): # 构造方法:出生时自动调用
self.name = name
self.hp = 100 # 一出生就有完整属性
self.atk = 12
self.def = 5
self.inventory = []
self._secret = "不能告诉别人" # 下划线 = 私有,约定别碰
def take_damage(self, amount): # 方法:玩家的行为
self.hp -= max(amount - self.def, 1)
if self.hp < 0:
self.hp = 0
def heal(self, amount):
self.hp = min(self.hp + amount, 100) # 不能超过上限
def __del__(self): # 析构方法:对象消亡前的清理(Python 里很少用到)
pass
# 使用:new 一个勇者出来
hero = Player("阿哲")
hero.take_damage(15)
print(hero.name, "剩余 HP:", hero.hp) # 阿哲 剩余 HP:89
# main.py —— 游戏入口
from player import Player
from monster import Monster
from map import load_map
hero = Player("阿哲")
dungeon = load_map("dungeon_1.txt")
print("勇者", hero.name, "踏入了", dungeon.name)
本层核心概念
- 模块 / 命名空间:把相关函数和数据分组,避免名字冲突(两个文件都可以有
load())。 - 类(class):数据(字段)+ 操作(方法)打包成模板,用
类名()造出对象。 - 访问控制:
public / private / protected——哪些可以随便摸,哪些只有内部能动(Python 用下划线约定,Java/C++ 用关键字)。 - 构造 / 析构:对象出生即合法(
__init__),消亡前清理(__del__)。
打通第 4 层的收获:游戏被拆成 player.py / monster.py / map.py,改一个房间不影响别的房间。封装 = 「我不管你里面怎么算,你只管给我一个干净接口」。
继承 / 多态 / 泛型:怪物家族
IS-A · 同一接口不同实现 · 一套逻辑多种类型哥布林、蝙蝠、骷髅、恶龙——每个都有血条、都会攻击。难道每个都写一份?怪物家族表示:我们有共同的祖先。
我现在有 Goblin、Bat、Skeleton、Dragon 四个类,每个里面都有一份 hp / atk / attack()……代码重复得我要吐了。
因为它们都是「怪物」嘛!记住一句话:哥布林 IS-A 怪物(猫 IS-A 动物)。这就是继承——抽一个 Monster 基类,把公共的东西放进去,子类只管写自己的特色。
class Monster:
def __init__(self, name, hp, atk):
self.name = name
self.hp = hp
self.atk = atk
def attack(self, target): # 公共逻辑:所有怪物都会攻击
target.take_damage(self.atk)
return f"{self.name} 发动攻击!"
def roar(self): # 虚方法:子类各自实现
return "……(安静的威胁)"
class Goblin(Monster): # 哥布林 继承 怪物
def roar(self):
return "呜咕噜——!"
class Dragon(Monster): # 恶龙 继承 怪物
def roar(self):
return "吼————!!!(整座地牢都在震)"
# 多态:同一句代码,不同对象做出不同行为
for m in [Goblin("小绿", 40, 10), Dragon("红莲", 300, 25)]:
print(m.name, "说:", m.roar()) # 小绿说:呜咕噜——! / 红莲说:吼————!!!
看到没?for m in [...] 里,代码根本不知道 m 是哥布林还是恶龙,只要它「是怪物」就行——同一接口,不同实现,这就是多态。
至于接口(interface):它比继承更「纯」,只管「有什么能力」不管「怎么实现」。Python 里用 ABC(抽象基类)来定义接口。
最后是泛型 / 模板:写一套逻辑,支持多种类型。比如「装任何物品的背包容器」——不用为每种物品写一个容器类。
template <typename T> // T 是「类型参数」
class Backpack {
T items[10]; // 不管 T 是药水、钥匙还是符文石,都能装
int count = 0;
public:
void put(const T &it) { items[count++] = it; }
};
Backpack<Potion> bag1; // 装药水的包
Backpack<Key> bag2; // 装钥匙的包
本层核心概念
- 继承:IS-A 关系(哥布林是怪物)。衍生:单继承、多继承、接口(interface)。
- 多态:同一接口不同实现——
monster.roar(),哥布林呜咕噜、恶龙吼破天。 - 泛型 / 模板:写一套逻辑支持多种类型。衍生:类型参数、约束、特化。
打通第 5 层的收获:新怪物 = 继承 Monster + 重写一个 roar(),30 秒加一种怪。第五层的地牢里,怪物家族开枝散叶了。
内存管理:别让内存泄漏
malloc/free · GC · 所有权 · 智能指针为了性能,小哲用 C 重写了怪物系统。结果打完一千只怪,电脑内存越吃越多——怪死了,内存没还回来。
师傅!我用 C 重写了怪物系统,跑起来飞快,但玩一会儿电脑内存占用直线上升,最后卡死了……我没 malloc 错啊?
哈哈,这就是第六层——内存管理。你 malloc 给每只怪划了块内存,打完怪忘了 free 还回去,就叫内存泄漏。C 语言把内存当现金,得自己记账、自己还。
人类不想天天数钱,于是发明了三套「自动还钱」的方案:
struct Monster *m = malloc(sizeof(struct Monster)); /* 借钱 */
m->hp = 40;
/* ……打怪中…… */
free(m); /* 还钱!忘了这行 = 内存泄漏 */
自动管理三兄弟
- 垃圾回收(GC):Python / Java / Go 的做法——运行时定期扫一遍,「没人引用的对象」自动回收。代价是偶尔「卡一下」(STW)。我们游戏用的 Python 就是它。
- 所有权(Rust):编译期就保证内存安全,不用 GC。规则:每个值只有一个「主人」,主人一离开作用域,内存立刻释放。借给别人得签「借用合同」(
&)。 - 智能指针(C++):半自动方案,
unique_ptr / shared_ptr用 RAII 模式——对象析构时自动释放,引用计数归零就删。
struct Monster { hp: i32 }
fn fight(mut m: Monster) { // 怪物被「移动」进函数,成了唯一主人
m.hp -= 10;
println!("剩余 HP: {}", m.hp);
} // 函数结束,m 自动释放——不用手动 free
fn main() {
let goblin = Monster { hp: 40 };
fight(goblin); // 所有权转移(move)
// println!("{}", goblin.hp); // 编译错误!goblin 已经不属于你了
}
原来如此……那我用 Python 是不是就不用管了?
大方向是——GC 兜底,但「对象活着却再也用不到」(比如全局缓存里堆了一堆旧怪物)还是占内存。好习惯是:用完主动解除引用(del monster / 移出列表),别全指望垃圾回收。记住:内存安全 ≠ 内存管理不用心。
打通第 6 层的收获:C 手动管 = 记账;C++ 智能指针 = 半自动;Python/Java GC = 全自动;Rust 所有权 = 编译期裁决。我们的游戏用 Python,先学会「不瞎占内存」就够用了。
错误处理:存档坏了别崩溃
错误码 · 异常 · Result / Option勇者玩到第 9 层,读档时「啪」——存档文件损坏,游戏白屏退出。辛苦三天,一朝清零。错误处理,是给程序买的「保险」。
师傅!!我加了存档功能,但刚才存档文件被写坏了一半,一读档游戏直接崩溃!我的 9 层进度全没了!!
疼过这一次你就记住了:出错时,程序要体面地活着,而不是默默崩溃。这就是第七层——错误处理。有四种流派:
错误处理四流派
- 返回值 / 错误码:C 的老办法,函数返回
0 表示成功、-1 表示失败。简单,但太容易被忽略——忘了检查等于没处理。 - 异常(Exception):
try / catch / finally,错误和正常逻辑分离,出错了「扔」出去,由专门的代码接住。 - 断言(Assertion):开发期自检,假设错了立刻大声喊出来,上线后一般关掉。
- Result / Option 类型:函数式流派(Rust / Haskell / Swift),把「可能出错」写进类型里,强迫调用者处理——编译期就堵住漏洞。
def load_save(path):
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f) # 可能抛:文件不存在 / 格式错误
return data
except FileNotFoundError:
print("没有找到存档,从新游戏开始。")
return new_game()
except json.JSONDecodeError:
print("存档已损坏……我们为你保留了一个旧备份。")
return load_save(path + ".bak") # 优雅降级:读备份
finally:
print("存档系统就绪。") # 无论成败都会执行(收尾)
# 断言:开发期自检,血量不可能是负数
assert 0 <= hero.hp <= hero.max_hp, f"血量异常:{hero.hp}"
fn find_player(save: &Save) -> Option<&Player> {
save.players.iter().find(|p| p.name == "阿哲")
// 找到返回 Some(&p),找不到返回 None —— 没有「空指针」这一说
}
fn read_save(path: &str) -> Result<Save, String> {
// Ok(数据) 或者 Err("文件损坏") —— 调用者必须处理两种可能
...
}
学到了!那以后我写的代码,是不是每个可能出错的地方都要处理?
原则是:外部输入(文件、网络、用户按键)必须防;自己内部的逻辑靠断言;能早挂就早挂(fail fast),别让错误悄悄扩散。我们的游戏现在可以「存档损坏但依然体面」了——这层地牢,过!
打通第 7 层的收获:错误码最简单但易忽略;异常把错误和逻辑分开;断言负责「不可能的假设」;Result/Option 让编译器当监工。游戏从此有了保险丝。
并发与异步:音效、倒计时与多线程
多线程 · 锁 · async/await · Actor给游戏加「背景音乐」和「限时挑战模式」:音乐要一直播、倒计时要走、输入还要响应——程序只有一个大脑,怎么同时干三件事?
师傅!我加了两个新功能结果全卡住了:背景音乐一播,玩家输入就不响应了;倒计时挑战模式里,倒计时不走完玩家动不了……
这就是第八层:并发与异步——同时做多件事。先分清两类「同时」:
多线程:真·同时,多核 CPU 上多个线程真的并行跑(音效线程、倒计时线程、主游戏线程)。麻烦在于共享数据要加锁——两人同时改血量,会打架。
异步 / async / await:不是真并行,是「一个人来回切换」——等 IO 的时候不干坐着,先去干别的。游戏里上传存档到云端,网络在等,但游戏照玩。
import threading, time
lock = threading.Lock() # 锁:同一时间只许一个人碰 hp
def music_loop():
while True:
print("🎵 地牢主题曲……")
time.sleep(5) # 播 5 秒,期间游戏线程照常响应
def damage(hp, amount):
with lock: # 加锁:改血量期间别人不许动
hp[0] -= amount
return hp[0]
t = threading.Thread(target=music_loop, daemon=True)
t.start() # 音乐线程启动,主线程继续跑游戏
hp = [100]
print("勇者剩余 HP:", damage(hp, 15)) # 85
# 玩家输入照常响应,音乐在后台放——互不打扰
import asyncio
async def upload_save():
print("开始上传存档……")
await asyncio.sleep(2) # 假装网络 IO 等 2 秒
print("上传完成!")
return "ok"
async def main():
task = asyncio.create_task(upload_save()) # 后台挂起上传
print("玩家继续探索地牢……") # 不等网络,游戏照跑
print("捡到一件装备!")
await task # 最后等上传收尾
asyncio.run(main())
本层核心概念
- 多线程:CPU 多核,真并行(音效/倒计时/输入各占一线程)。
- 锁 / 互斥 / 通道:线程间共享数据要同步,否则「数据竞争」——两个线程同时写血量,结果谁都不对。
- 异步 / async / await:IO 等待时不阻塞线程,衍生:事件循环、协程。
- Actor 模型:Erlang 的路线——根本不共享数据,全靠「发消息」通信,怪物 AI 各发各的,天然无锁安全。
打通第 8 层的收获:真并行用线程 + 锁;等 IO 用 async;怕共享状态用 Actor 消息传递。游戏现在可以「边听音乐边倒计时边输入」了——活着的地牢,热闹起来了。
函数式:背包里的数学
一等函数 · 闭包 · map/filter/reduce小哲用 for 循环写「筛出药水」「算背包总价」,循环套循环,又长又容易错。函数式流派说:把计算当数学,循环交给三兄弟。
师傅,我写了个「背包统计」:筛出所有药水、给装备加 buff、算总价值。三个 for 循环嵌套,30 行代码,改一次错一次。
来认识函数式流派的三兄弟——map(逐个变换)、filter(按条件筛选)、reduce(汇总成一个值)。它们把「循环怎么走」抽象掉了,你只管说做什么,不管怎么遍历。
先认识它们的爹:一等函数——函数可以像数字一样被传来传去、存进变量。而闭包是函数记住自己出生时的环境(能「偷看」外面的变量)。
from functools import reduce
backpack = [
{"name": "小药水", "type": "potion", "value": 10},
{"name": "铁剑", "type": "weapon", "value": 50},
{"name": "大药水", "type": "potion", "value": 25},
{"name": "护符", "type": "trinket", "value": 80},
]
# filter:筛选出所有药水(只关心"筛",不关心"怎么循环")
potions = list(filter(lambda it: it["type"] == "potion", backpack))
# map:给每件装备 +10 点价值(逐个变换)
boosted = list(map(lambda it: {**it, "value": it["value"] + 10}, backpack))
# reduce:把背包总价值汇总成一个数
total = reduce(lambda acc, it: acc + it["value"], backpack, 0)
print("药水:", [p["name"] for p in potions]) # ['小药水', '大药水']
print("总价值:", total) # 165
def potion_factory(base_heal):
"""返回一个闭包:它记住了 base_heal 这个"环境"。"""
def make(level):
return {"name": f"治疗药水 Lv{level}",
"heal": base_heal * level} # 闭包"偷看"外面的 base_heal
return make
small_factory = potion_factory(20) # 小作坊:基础回复 20
big_factory = potion_factory(50) # 大作坊:基础回复 50
print(small_factory(2)) # {'name': '治疗药水 Lv2', 'heal': 40}
print(big_factory(1)) # {'name': '治疗药水 Lv1', 'heal': 50}
# 同一个函数,因为记住的环境不同,行为就不同——这就是闭包
本层核心概念
- 一等函数:函数可当参数、可当返回值、可存变量。
- 闭包:函数记住定义时的环境(内部变量)——药水工厂的神奇之处。
- 不可变数据:不修改原数据,永远造新数据 → 没有副作用,并发更安全。
- 高阶函数:
map / filter / reduce——把循环抽象成操作。 - 模式匹配:比 if/else 更强大的分支——直接「解构」数据(Python 的
match、Rust 的match)。
打通第 9 层的收获:30 行循环 → 3 行高阶函数。代码在表达「意图」而不是「步骤」。函数式的核心气质:数据流像水,函数是管道。
元编程:代码生成代码
宏 · 反射 · 装饰器想给「每次战斗、每次捡宝」都记一笔日志?手工加 50 行 print?元编程说:让代码自己写代码。
我想做「战报系统」:每次攻击、捡宝、喝药都要写进日志。可攻击函数有 17 个……难道要改 17 处,每处塞 3 行日志代码?
这就是第十层:元编程——代码生成代码。你不改那 17 个函数,而是写一个「装饰器」,像给函数贴标签一样,让它在调用时自动附加日志行为。
def log(fn):
"""装饰器:包装函数,调用前自动写日志。"""
def wrapper(*args, **kwargs):
result = fn(*args, **kwargs)
print(f"[战报] {fn.__name__} 被调用 → 结果: {result}")
return result
return wrapper
@log
def attack(name, dmg):
return f"{name} 造成 {dmg} 点伤害!"
@log
def pick_item(item):
return f"捡到了 {item}!"
attack("勇者阿哲", 18) # [战报] attack 被调用 → 结果: 勇者阿哲 造成 18 点伤害!
pick_item("生锈的钥匙") # [战报] pick_item 被调用 → 结果: 捡到了生锈的钥匙!
# 17 个函数?每行上面加 @log 就行,一处都不用改内部代码
// C 宏:编译期就展开成代码(简单粗暴的"代码生成")
#define MAX_HP 100
#define HEAL(p, n) ((p).hp = ((p).hp + (n) > MAX_HP) ? MAX_HP : (p).hp + (n))
# 反射:运行时检查/修改自身结构
skills = ["火球术", "治疗术", "嘲讽"]
hero.learn = lambda s: skills.append(s) # 运行时给对象"装"新技能
print(hasattr(hero, "attack")) # True —— 运行时问"你有没有 attack"
print([s for s in dir(hero) if not s.startswith("_")]) # 把对象里外看个遍
本层核心概念
- 宏:编译期生成代码(C 宏、Rust 宏、Lisp 宏)——写一次,展开 N 次。
- 反射:运行时检查 / 修改自身结构——
dir() / hasattr() / getattr()。 - 注解 / 装饰器:给代码加标记,由工具或框架读取(
@log、Java 的@Override、框架的@Route)。
打通第 10 层的收获:「改一处,处处生效」的魔法背后是元编程。它很强大,但用多了代码难读——装饰器加两层是优雅,加二十层是炼狱。
DSL:给地牢造一门语言
SQL · 正则 · 地图描述语言地图是二维数组,改一堵墙要改代码;关卡设计师想自己画地图,又不想学编程。DSL 说:为特定场景造一门小语言。
师傅,我画地牢地图是用二维数组手写的……改一堵墙都要数格子,眼睛都花了。而且我朋友想帮我设计关卡,他完全不会编程。
这就是第十一层:领域专用语言(DSL)——为特定场景造一门「小语言」,让非程序员也能写。咱们就为地牢造一门:一个字符 = 一种东西。
#####
#@..#
#.M.# # = 墙壁 . = 地砖 @ = 勇者 M = 怪物 G = 金币 E = 出口
#..G#
#####E
def parse_map(text):
tiles = {"#": "wall", ".": "floor", "@": "player",
"M": "monster", "G": "gold", "E": "exit"}
grid, player, monsters = [], None, []
for y, line in enumerate(text.strip().splitlines()):
row = []
for x, ch in enumerate(line.strip()):
row.append(tiles.get(ch, "floor"))
if ch == "@": player = (x, y)
if ch == "M": monsters.append((x, y))
grid.append(row)
return grid, player, monsters
# 用!朋友现在只要会用记事本画图,就能设计关卡
grid, player, monsters = parse_map("""#####
#@..#
#.M.#
#..G#
#####E""")
print("勇者出生点:", player) # (1, 1)
print("怪物位置:", monsters) # [(3, 2)]
print("该格是什么:", grid[2][1]) # floor
同样的思想,早就在你身边了——
- SQL:给「数据库查询」造的语言,
SELECT * FROM players WHERE hp > 50。 - HTML / CSS:给「网页结构 / 样式」造的语言。
- 正则表达式(RegExp):给「文本模式匹配」造的微语言。
- Shell:给「操作系统命令」造的语言。
我们游戏的排行榜也可以交给 SQL 存进数据库;解析玩家输入「向北走」「捡起剑」用正则就能拆词。
-- 排行榜:按通关层数排序取前 10
SELECT name, floor, gold
FROM players
ORDER BY floor DESC, gold DESC
LIMIT 10;
import re
cmd = "向北走并捡起生锈的钥匙"
moves = re.findall(r"(向北|向南|向东|向西)走", cmd) # ['向北']
items = re.findall(r"捡起(.+?)(?:并|$)", cmd) # ['生锈的钥匙']
print(moves, items) # ['向北'] ['生锈的钥匙']
本层核心概念
- DSL(领域特定语言):为特定场景造语言(配置、规则、流水线、地图)。
- DSL 家族成员:SQL、HTML/CSS、正则、Shell。
- DSL 的哲学:让非程序员也能写,把「表达」和「实现」分离。
打通第 11 层的收获:地图变成记事本就能画的字符画,朋友用「画图」而不是「编程」来设计关卡——这就是 DSL 的浪漫。
AI 时代:会聊天的 NPC 与智能攻略
Prompt · Agent · MCP · RAG2020 年代,大模型让抽象又往上跳了一层:自然语言本身,成了最高级的编程语言。
师傅!现在游戏基本完成了!但我有个大胆的想法——让地牢里的 NPC 真的会聊天,还能根据我的战况给攻略提示!
好,那咱们就爬这最后一层——AI / 大模型时代。你会发现,这一层的「编程方式」跟前面完全不同:写 Prompt(提示词)就是写代码,自然语言即代码。
import openai
def talk_to_merchant(player_hp, gold):
prompt = f"""
你是一位地牢里的神秘商人,说话神秘又幽默。
当前玩家:生命 {player_hp},金币 {gold}。
规则:每次只回复一句话,不超过 30 字,必须推销一件物品。
"""
resp = openai.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "system", "content": prompt},
{"role": "user", "content": "你好,商人!"}]
)
return resp.choices[0].message.content
# 输出可能是:"嘿嘿,来的正是时候——我这有一瓶回血药水,只卖你 50 金币。"
但光会聊天还不够。想让 AI 真能「帮玩家做事」——查攻略、查装备数据、甚至操作游戏——就要给它三件套:
- Agent(智能体):让模型自己「规划 → 调用工具 → 看结果 → 再规划」。它不再是回答问题的聊天框,而是会行动的助手。
- MCP(模型上下文协议):给 Agent 接「工具插头」——查存档、查地图、读背包,统一协议,即插即用。
- RAG(检索增强生成):把攻略、怪物图鉴「喂」给模型当参考资料——它回答你「恶龙弱点是火」时,是真的查过攻略库。
# 伪代码:Agent 的思考-行动循环
def game_guide_agent(user_question):
plan = llm.plan(user_question) # 1. Agent 规划:"需要查攻略 + 读当前存档"
docs = rag.search(user_question) # 2. RAG:从攻略库里检索相关资料
save = mcp.call("read_save", "slot1") # 3. MCP:调用工具读玩家存档
answer = llm.generate(plan, docs, save) # 4. 综合生成建议
return answer
# 玩家问:"恶龙怎么打?"
# → Agent 查攻略库(RAG):恶龙弱火、第三回合会喷火
# → MCP 读存档:你 50 级、有火球术、防御 300
# → 回答:"你现在的配置可以硬刚!开局先放火球术……"
本层核心概念
- Prompt:自然语言即代码——写提示词就是写程序。
- Agent:自主规划 + 工具调用——从「会说话」到「会行动」。
- MCP:模型上下文协议——给 AI 接统一标准的工具插头。
- RAG:检索增强生成——让 AI 基于你的资料库回答,而不是瞎编。
哇……所以第十二层是「抽象上升到人类语言本身」?
没错。从 0 和 1,到变量、函数、类、泛型、DSL……抽象一层层往上爬,每一层都比上一层更接近「人怎么想」。大模型这一层,直接住进了人类语言里。越往上,离硬件越远,离人越近——这就是整棵家族树的走向。
十二层,打通了。恭喜你出师,勇者。
打通第 12 层的收获:你会写代码,也会写 Prompt——两样都是编程。未来的地牢,NPC 会聊天、攻略会定制、甚至 AI 能替你打怪。而这,只是家族树继续往上长的开始。
问题 → 概念 对照表
遇到麻烦时翻这一页打怪打到一半卡住了?不知道用什么概念?这张表把「你遇到的麻烦」直接翻译成「该用的概念」——它是整座地牢的快速传送门。
| 你遇到的麻烦 | 对应的概念 |
|---|---|
| 记不住内存地址 | 变量 |
| 代码复制粘贴 | 函数 |
| 全局变量被乱改 | 类 / 模块 |
| 对象没初始化就用了 | 构造方法 |
| 假设错了但没发现 | 断言 |
| 内存泄漏 / 悬空指针 | 垃圾回收 / 所有权 |
| 错误处理代码太乱 | 异常 / Result |
| 等待 IO 时卡住 | 异步 / await |
| 写多遍类似代码 | 泛型 |
| 并发数据竞争 | 锁 / 通道 / Actor |
| 配置太复杂 | DSL / 注解 |
「人类偷懒 + 防错」的产物。