没想到吧,我第一篇有模有样的程序作品,不是“Hello World”,而是一款能拿得出手、还能跟朋友炫耀的俄罗斯方块游戏。而且整个制作过程里,超过一半的代码可以说是AI替我敲的,但我学到的东西,比一点一点手敲代码多得多。这篇文章我就把从零到能玩的完整经过和踩坑经验都记下来,想用AI做游戏、或者想靠AI学编程的人,应该能少走很多弯路。
俄罗斯方块游戏本身的逻辑看着简单,真做起来其实包含了游戏开发最核心的几块:界面渲染、事件监听、状态管理、碰撞检测、评分机制。对我们这种不是科班出身、不想啃几千页教材的人来说,它的体量刚好——不会复杂到让人想放弃,也不会简单到什么都学不到。尤其当你有一只“AI助手”在旁边,你会发现整个过程更像是“当一个真正懂项目的负责人”——你提需求、审方案、验成果,AI负责写代码。
我先交代一下我用的是Python加Pygame这套组合。选它的原因很直接:Python是现在AI工具支持得最好的语言,Pygame又足够轻量,一个命令就能装上,跑游戏只需要一个窗口,不用去折腾网页前端的各种构建工具。下面我会按项目推进的顺序,把需求拆解、提示词写法、代码审查、翻车修复、再到功能进阶,完整过一遍。如果你想动手做,看完就可以开一个py文件了。
1. 为什么选俄罗斯方块做AI练手项目:玩法边界清,反馈看得见
先说结论:一个人想通过AI学编程,第一台“游戏机”应该就是俄罗斯方块。这不是情怀,是因为它的项目特性实在太适合AI协作。
1.1 规则完整但可控:为什么这是一道标准题目
我第一次跟AI提需求的时候,心里其实没底。但当我试着把俄罗斯方块的规则一条一条写出来,发现它的边界非常清楚:一块10列20行的网格、七种形状、四种操作、一行全满就会消除。规则越清晰,AI越不容易跑偏。
对比一下:如果你想用AI做一个“像大型开放世界那样自由探索的游戏”,需求本身就是一团雾,AI生成什么你都得推翻重来。而俄罗斯方块的胜利条件和失败条件都清清楚楚,AI写出来的东西是可以逐条验证的。这对新手特别重要,因为你不具备一眼看出代码有没有问题的能力时,一个能明确验收的项目就是最好的学习素材。
它覆盖的知识点也很“完整”。网格是二维数组,方块是矩阵,旋转是矩阵运算,下落是坐标变化,碰撞是数组越界和值判断,消行是数组删除和插入。这些听着抽象,但做完游戏你就发现全在你的肌肉记忆里了,以后再接触其他游戏或者数据处理项目,会很顺手。
1.2 AI编程工具怎么选:我的建议不是越贵越好
现在市面上能帮写代码的AI工具不少,大体分两类:一类是对话式大模型,比如你打开网页把我说的需求贴进去,让它生成完整代码;另一类是IDE里的AI编程插件,比如Cursor、GitHub Copilot、通义灵码这类,它们能读你当前打开的整个项目,支持你边写边补全,也能在对话框里改代码。
我个人的建议是这样的表:
| 工具体系 | 适合人群 | 优势 | 需要留意的地方 |
|---|---|---|---|
| 对话式大模型 | 零基础、想理解整体步骤的人 | 能一次性给出完整代码和注释 | 代码跑不通时需要来回复制粘贴 |
| Cursor | 想边写边改、喜欢AI自动补全的人 | 能读项目上下文,改代码比较快 | 对完全没有概念的人,上手有点门槛 |
| IDE插件(Copilot、通义灵码等) | 已经用VS Code、PyCharm的人 | 不改变原有习惯,补全自然 | 让AI改大段代码时,交互不如对话式直观 |
如果你是完全的新手,我的建议是先从对话式大模型开始,等代码能跑了,再慢慢接触IDE插件。因为对话式大模型会逼你把需求说清楚,这个练习本身比代码值钱。我自己做俄罗斯方块时,大部分代码生成用的是对话式工具,后面加功能时才换到Cursor里调整,两种方式各有各的舒服。
1.3 先把环境跑通:Python和Pygame的十分钟检查
不管AI多强,它不能替你装环境。开始之前,先在命令行里确认两件事:
bash复制python --version
pip --version
如果你还没装Python,直接去官网下载最新稳定版,安装时记得勾选“Add Python to PATH”,否则后面命令行会一直提示找不到python。接下来装Pygame:
bash复制pip install pygame
输入一个简单的检测脚本,能弹出一个空白窗口就说明环境正常:
python复制import pygame
pygame.init()
screen = pygame.display.set_mode((400, 600))
pygame.display.set_caption("Pygame OK")
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
pygame.display.flip()
这一步别跳过。很多人后面的问题不是AI代码不行,而是环境没跑通,然后连着环境报错一起丢给AI,AI也会被绕晕。环境干净了,后面所有报错信息才有意义。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手前先把需求拆给AI:提示词这样写,AI才不跑偏
AI写代码很像一个执行力很强的实习生,你交代得越具体,它交付的东西越接近你想要的。很多人觉得AI生成的东西没法用,十有八九是需求描述太模糊。
2.1 把游戏需求拆成AI能理解的最小单元
拿到“做俄罗斯方块”这个目标后,我第一件事是把它拆成一张需求清单。不用写得多专业,但要覆盖游戏必须有的行为:
- 画布:一块10列20行的网格,窗口大小自己定
- 方块:标准7种形状,能做顺时针旋转
- 移动:左右移动、向下软降、按空格硬降
- 碰撞:碰到左右边界或已锁定方块就不能再继续移动
- 消行:一行填满就消除并计分
- 速度:分数越高,方块下落越快
- 结束:新方块一出现就撞到已有方块时,游戏结束
把这些列出来,其实我已经完成了一大半“产品设计”。AI接收到的是不是一句“帮我做个游戏”,而是精确到行为的验收清单,它自然不容易自由发挥。
2.2 一个好用的提示词模板:我实测的写法
下面是我用下来效果比较稳的提示词模板,你可以直接替换参数:
text复制你是一位资深的Python游戏开发工程师。请用Pygame实现一个俄罗斯方块游戏,要求如下:
1. 窗口大小400x600,游戏区域为10列20行,每个格子像素大小为20。
2. 包含标准7种方块:I、O、T、S、Z、J、L,每种方块用矩阵表示。
3. 支持方向键操作:左右移动、上键旋转、下键软降、空格硬降。
4. 实现碰撞检测,方块无法移动出游戏区域或穿过已锁定方块。
5. 实现消行逻辑:整行满时删除该行,并从上往下补齐。
6. 计分规则:消1行100分,2行300分,3行500分,4行800分。
7. 下落速度初始为500毫秒,分数每增加1000分,下落间隔减少50毫秒,最低不能低于100毫秒。
8. 生成本次需求中“最完整可运行”的main.py文件,代码要包含必要的注释,最后给出运行说明。
这个模板里,我刻意写清楚了三个维度:边界(10x20和格子大小)、规则(操作和消行计分)、约束(速度随分数变化的下限)。让AI看到的不只是“做什么”,还有“做到什么程度”。
2.3 一次性生成整段代码,还是分段生成:我的选择
对俄罗斯方块这种规模的程序,我建议让AI第一次就把main.py完整生成出来。理由很简单:你先拿到一个能跑通的最小闭环,后面再基于这个闭环去优化。如果我让它一次只写一个函数,拼起来的过程反而是新手最容易懵的地方。
等能跑之后,再针对性地让AI改局部,比如“把下落速度改成根据消行数动态调整”“把硬降换成空格键”。这样做的好处是,AI有一个完整的上下文,改出来的代码不会前后矛盾。如果你一开始就分段问,AI每次生成都是“无状态”的,前一段变量名叫board,后一段可能就叫grid,对接时你光改变量名就能改到怀疑人生。
3. 游戏核心逻辑的代码审查:旋转、碰撞、消行三个高危区
AI把代码交给你之后,不是复制粘贴就完事。我拿到生成结果的第一个动作是通读一遍关键函数,因为俄罗斯方块有三个地方最容易出问题:旋转、碰撞、消行。AI生成写得像那么回事,但逻辑细节经常翻车。
3.1 方块数据与旋转:矩阵转置的思路比魔改索引可靠
所有方块本质上都是一个二维矩阵,1代表有格子,0代表空。举个T型的例子:
python复制'T': [[0, 1, 0],
[1, 1, 1],
[0, 0, 0]]
顺时针旋转就是把矩阵做一次“转置后翻转”,代码是这样的:
python复制def rotate_clockwise(shape):
return [list(row) for row in zip(*shape[::-1])]
为什么要单独讲这个?因为AI生成旋转代码时,喜欢直接写各种魔改索引,比如shape[y][x] = shape[x][y],这种写法在小方块上碰巧能跑,但一遇到I和O就出问题。我见过AI生成的版本里,I型方块旋转后把自己截成了两段。用上面这种基于矩阵操作的写法,对所有方块都成立。
O型方块不需要旋转,如果你发现AI给O型也做了旋转,并且旋转后位置错乱,就在逻辑里加一行跳过O的处理,然后顺手把代码交给AI修,比你自己去抠它的转换逻辑快得多。
3.2 碰撞检测:边界判断要同时管住左右和底部
碰撞检测的核心思路很简单:尝试把方块移动到新位置,检查每个非空格子在新位置是否越界、是否和已锁定方块重叠。一个标准的版本长这样:
python复制def collides(board, shape, offset):
ox, oy = offset
for y, row in enumerate(shape):
for x, cell in enumerate(row):
if not cell:
continue
nx, ny = ox + x, oy + y
if nx < 0 or nx >= COLS or ny >= ROWS:
return True
if ny >= 0 and board[ny][nx]:
return True
return False
注意上面代码里的ny >= 0这个判断。新方块出生在网格上方,它顶部的一部分可能在网格线之外,这时候你不能把“负数行”也算作碰撞。很多AI初版会忽略这个细节,导致方块一出生就提示游戏结束。
如果你把代码交给AI审查,建议直接问它“方块在出生位置时,顶部溢出部分为什么不触发碰撞?”,它通常会意识到判断顺序的问题。
3.3 消行逻辑:删除行的方向,决定计分是否错乱
消行的标准实现是:遍历每一行,如果整行都不含0,就删除这行,并在最前面补一个空行。听起来简单,但这里有个非常经典的顺序陷阱。
网上有些AI生成代码是这样的:
python复制for y in range(ROWS):
if all(board[y]):
del board[y]
board.insert(0, [0] * COLS)
问题在于:删除行会让索引整体变化。如果你正从第0行往上遍历,删除第2行后,原本的第3行变成了新的第2行,可循环已经看过了第2行,于是这条本该被消除的行就这么漏过去了。表现就是你发现消了3行,计分却只按1行算,还经常出现隔行消不掉的怪象。
正确的做法是优先处理下方行,或者像我下面这样用列表推导一次性过滤:
python复制def clear_lines(board):
remaining = [row for row in board if any(cell == 0 for cell in row)]
lines_cleared = ROWS - len(remaining)
if lines_cleared:
return [[0] * COLS for _ in range(lines_cleared)] + remaining, lines_cleared
return board, 0
先把不需要消除的行保留下来,数一下消除了几行,再在最前面补同样数量的空行。这个写法不会出现索引错乱,也方便AI继续扩展计分逻辑。
3.4 主循环与帧率:60帧只是起点,下落计时是关键
Pygame主循环的骨架是固定的:处理事件、更新游戏状态、绘制画面、控制帧率。帧率我一般设60,因为手感比较顺。但注意,帧率负责的是画面刷新,方块下落本身应该用事件计时或时间差来控制,而不是在每一帧里机械地位移。
比较推荐的做法是:
python复制MOVE_DOWN_EVENT = pygame.USEREVENT + 1
pygame.time.set_timer(MOVE_DOWN_EVENT, fall_speed)
然后在事件循环里监听这个自定义事件:
python复制if event.type == MOVE_DOWN_EVENT:
move_down()
这样做的最大好处是:下落速度变化时,你只要重新调用一次set_timer,就能动态调整节奏,不需要在自己的循环代码里维护一堆计数器。AI生成初版代码时经常把落下的判断放在主循环里用帧数累计,那也不是不行,只是后期做速度曲线会很别扭。
4. 实测运行与问题修复合集:AI代码翻车的四个真实场景
接下来这部分,是我跑AI生成版本时踩过的坑。每一个都可以对照着检查你手里的代码。我特意把排查过程写出来,不直接给答案,因为排查思路才是真正能迁移到别处的经验。
4.1 方块卡在边界外:旋转后坐标越界
第一次运行,我把方块移动到右墙边,按上键旋转,方块直接有一半“悬挂”在墙外,还能继续下落。排查的时候先在collides函数里打印每个格子的nx和ny,发现旋转后部分格子的nx等于10,正好超出网格最右列。
这里的问题是两点:一是旋转后没有做碰撞检测就更新坐标,二是方块靠近墙边时,旋转后需要尝试向左或向右偏移。简单修复是在旋转后尝试几个候选偏移坐标,谁不碰撞就选谁,这就是后来被我管叫“墙踢”的处理:
python复制def rotate_with_wall_kick(board, current_shape, offset):
rotated = rotate_clockwise(current_shape)
for dx, dy in [(0, 0), (-1, 0), (1, 0), (-2, 0), (2, 0)]:
new_offset = (offset[0] + dx, offset[1] + dy)
if not collides(board, rotated, new_offset):
return rotated, new_offset
return current_shape, offset
这个墙踢列表是简化版,对应基础游戏够用了。如果以后想追求专业手感,可以再去查Tetris官方SRS规定的准确踢墙表。
4.2 消行后计分不对:遍历方向导致漏行
有一次我连续铺了两行,系统只消除了一行,分数也只有100分。检查后发现AI用的是最朴素的for y in range(ROWS)加删除插行,结果就是前面说的索引错乱。
我当时的排查方法是在clear_lines前后分别打印board里非空行的索引,对比哪一行消失了、哪一行还留着。立刻就能看出来,被“吞掉”的是循环指针后跳过的那一行。改成从下往上遍历后,问题一次解决。如果你不想看复杂的遍历,可以直接用前面给的列表推导写法,它天然安全。
4.3 下落速度没有渐变:参数被AI写死
第三个问题是手感问题。AI生成的初版,下落间隔永远是500毫秒,不管消多少行速度都不变。我翻了生成代码,发现speed变量在初始化后就没有再被更新过。
修复思路很简单,在消行计分后加一段速度重算逻辑:
python复制new_speed = max(100, 500 - score // 1000 * 50)
if new_speed != fall_speed:
fall_speed = new_speed
pygame.time.set_timer(MOVE_DOWN_EVENT, fall_speed)
核心是set_timer可以被重复调用来改变触发频率,这是Pygame内置事件计时机制允许的。不要自己去维护一堆“还剩多少帧才下落”的计数器,那才是头疼的开始。
4.4 按住方向键不连续移动:Pygame的KEYDOWN陷阱
新手最容易忽略的是键盘操作手感。Pygame的KEYDOWN事件在系统层面有默认的重复延迟,按住左键时,方块往往只移动一格,停顿一下,再重复移动,操作起来像“卡带”。我一开始还以为AI代码吃掉了按键,后来定位到是事件监听方式的问题。
要做出丝滑的连续移动,建议在每帧里用pygame.key.get_pressed()检查按键状态,并自己控制移动冷却时间:
python复制keys = pygame.key.get_pressed()
move_cooldown -= 1
if keys[pygame.K_LEFT] and move_cooldown <= 0:
move(-1)
move_cooldown = 5
if keys[pygame.K_RIGHT] and move_cooldown <= 0:
move(1)
move_cooldown = 5
旋转仍然建议用KEYDOWN事件触发,按住就连续旋转对操作反而是灾难。这个细节做好之后,游戏的“可玩性”会上一个台阶,不是代码能跑就等于好玩。
5. 从“能玩”到“好玩”:预览、硬降与7-bag随机
基础版能跑、能消行、能计分之后,下一步不是马上换新项目,而是继续让AI加功能。俄罗斯方块的耐玩程度,很大程度上取决于随机算法和下块预览这样的细节。
5.1 7-bag随机:告别连续8个S的恐怖开局
AI生成的基础版用的是random.choice,每次随机选一个方块类型。这样玩久了就会出现极端情况,比如连续出很多S型、Z型,这种局面下再厉害的操作也没法玩。稍微懂点游戏设计的人都会用7-bag随机算法:把7种方块放进袋子里,洗牌后依次抽出,抽空再洗。
python复制import random
bag = []
def get_next_piece():
global bag
if not bag:
bag = list(SHAPES.keys())
random.shuffle(bag)
return bag.pop()
这个改动很小但体感提升巨大。让AI做增量修改时,提示词可以这么写:“在现有代码基础上,把随机方块改为7-bag算法,保持原有函数名和接口不变,不允许改动主循环结构。”
5.2 预览、软降与硬降:手感三件套
很多看起来理所当然的功能,比如下一块预览、空格硬降,基础版AI不一定默认给。你要一条条加。我当时的做法是让AI新增一个draw_next_piece函数,在窗口右侧画一个小预览区;硬降则是在空格键事件里,沿当前列一路下落,直到碰撞为止,再直接锁定。
软降要不要额外计分,这属于规则设计问题。很多街机版会给你软降一格加一分,来奖励果断操作。你可以把它当自定义规则交给自己。我建议基础版先不做加分,等玩熟了再让AI加上去,不然调平衡会更费劲。
5.3 让AI重构代码:从一坨脚本到可维护的结构
随着功能越来越多,AI生成的一整段脚本会越来越难改。这时候可以让AI做一次重构:把所有逻辑拆成类,比如Board、Piece、Game,每个类只负责一件事。提示词可以是:
text复制请把当前main.py按模块化方式重构:将游戏区域、方块定义、游戏状态拆成三个类,主循环只保留事件处理和调用方法。重构过程中保持游戏行为完全不变,不要新增功能,重要逻辑必须写清楚注释。
我建议这一步放在功能稳定之后再做。重构的收益是后面你想加“暂停功能”“音效”“幽灵块”时,话术会非常清晰,因为代码结构本身就是一份文档。
6. AI编程的边界:它替你写了代码,但没替你思考
整个项目做完,最大的感触是:AI写代码的速度确实快,但它是典型的“指哪打哪”。你让它做游戏,它就能做游戏,但你要是没想清楚游戏规则,它也会替你写一堆半吊子的东西。
6.1 让AI解释代码,而不是盲目接受
我拿到AI生成的代码后,会先挑三个核心函数,让AI逐行解释。它解释的过程,其实就是我在学游戏逻辑的过程。比如它会告诉你“collides函数里nx < 0就是左边界,board[ny][nx]表示那块位置已经有方块”。听一遍解释,比你自己读十遍代码记得牢。
如果你发现AI解释得含糊其辞,那基本可以断定这段代码有问题或者工具在胡说,这时候继续追问它“那这个条件会在什么场景下触发?”,通常能问出破绽。
6.2 提示词迭代的几个技巧
做这个项目的过程中,我的提示词也在进化。几个比较实用的经验:
- 分开对待“需求”和“约束”。需求是做什么,约束是做到什么程度,写提示词时都单独列条目
- 出现报错时,直接把完整的报错信息贴给AI,不要只贴一行“它崩了”
- 想加功能的时候,永远先声明“保留现有逻辑不变”,AI才会做增量而不是推倒重来
- 让AI给你写一段测试用的暂存代码,比如输入一组固定方块顺序,看游戏行为是否符合预期
- 如果AI连续两三次没改对,别再让它硬猜,把出错前后的代码贴出来,请它做“代码审查员”,经常能发现AI自己生成时忽略的问题
这些技巧看着很简单,但能明显减少来回沟通的次数。我后来做别的项目,也一直在用这套方式。
