news 2026/9/14 23:09:57

用 aider 以对话方式从零构建 Pygame Pong:SEARCH/REPLACE 编辑格式与迭代式游戏开发的完整实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 aider 以对话方式从零构建 Pygame Pong:SEARCH/REPLACE 编辑格式与迭代式游戏开发的完整实战

用 aider 以对话方式从零构建 Pygame Pong:SEARCH/REPLACE 编辑格式与迭代式游戏开发的完整实战

【免费下载链接】Qwen3-CoderQwen3-Coder is the code version of Qwen3, the large language model series developed by Qwen team.项目地址: https://gitcode.com/GitHub_Trending/co/Qwen3-Coder

在本文中,我们基于 pong.md 这份真实聊天记录,完整复现"用自然语言驱动 aider 从零开发一个 Pygame Pong 游戏"的全过程:从创建pong_game.py基础骨架、加入 Paddle/Ball 类与游戏循环,到逐步调整球速、拍子尺寸与颜色,甚至实现"每次反弹加速 10%"的游戏机制。通过逐条拆解聊天中的SEARCH/REPLACE编辑块(edit block),并结合仓库内 editblock_coder.py 的解析与应用实现,读者将掌握 aider 的核心编辑协议、自动 git 提交流程,以及如何用纯对话完成从原型到可玩游戏的迭代开发。

聊天记录背景:aider 是怎么"看懂"编辑请求的

在进入 Pong 案例之前,先理解这份 transcript 的运行机制。根据 examples/README.md 的说明,这些聊天记录展示了 aider 的实际工作方式:

  • 每当 LLM 给出一个代码修改建议,aider 会自动将其应用到源文件;
  • 应用完成后,aider 会使用描述性 commit message 自动提交到 git;
  • LLM 只能看到并编辑"已加入聊天会话"的文件,用户通过命令行参数或聊天内的/add命令添加文件;当 LLM 主动要求查看某个文件时,aider 会先征得用户许可(transcript 中能看到文件被加入或移出会话的通知)。

transcript 的排版约定也很明确:>前缀是 aider 工具自身的输出,####是用户输入的聊天消息,蓝色字体的普通文本是 LLM 的回复,其中常包含彩色标注的 edit block 来指明代码改动。

edit block 的格式非常关键,它在 editblock_prompts.py 的 system reminder 中有严格定义,基本骨架如下:

文件路径(独占一行) <<<<<<< SEARCH 现有文件中的一段连续代码(必须逐字符精确匹配) ======= 要替换进去的新代码 >>>>>>> REPLACE

关于这一格式,源码给出的规则包括:SEARCH部分必须与现有文件内容逐字符精确匹配(包括所有注释、docstring、缩进);SEARCH/REPLACE 块会替换所有匹配出现的位置;块要保持简洁,大改动应拆成一系列小块;创建新文件时使用空 SEARCH 段加完整 REPLACE 段——这正是 Pong 案例中第一个 edit block 的做法。

第一步:从零创建 pong_game.py 基础骨架

用户的第一条指令是:

can you make a simple game of pong with pygame?

aider 先给出了开发步骤拆解(安装 Pygame、初始化窗口、创建 Paddle 与 Ball 类、初始化对象、游戏循环、输入处理、碰撞与计分、绘制、记分、显示分数),然后通过一个"新文件"类型的 edit block 创建了pong_game.py——注意 SEARCH 段为空,这对应源码中"新文件"的约定:

pong_game.py <<<<<<< ORIGINAL ======= import pygame import sys # Initialize Pygame pygame.init() # Set up the game window screen_width = 640 screen_height = 480 screen = pygame.display.set_mode((screen_width, screen_height)) pygame.display.set_caption("Pong") # Game loop running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # Update game objects # Draw game objects # Update the display pygame.display.flip() # Quit Pygame pygame.quit() sys.exit() >>>>>>> UPDATED

(注:transcript 原文中此处的分隔符写作ORIGINAL/UPDATED,与规范中的SEARCH/REPLACE等价,源码 editblock_coder.py 中定义了HEAD = "<<<<<<< SEARCH"DIVIDER = "======="UPDATED = ">>>>>>> REPLACE"三个标记。)

随后 transcript 记录了 aider 的关键交互流程:

Allow creation of new file pong_game.py? y Add pong_game.py to git? y Applied edit to pong_game.py Commit 14799cb aider: Added basic Pygame initialization and game loop structure topong_game.py.

这展示了一个重要事实:创建新文件与加入 git 都需要用户显式确认(输入y),这与 README 中"LLM 只能编辑已加入会话的文件"的原则一致——aider 在此处请求了用户许可后才真正落盘并提交。

第二步:加入 Paddle / Ball 类与完整游戏循环

用户说 "go ahead",aider 在同一个文件中继续追加代码。首先在初始化段之后插入类定义与random导入:

pong_game.py <<<<<<< ORIGINAL import pygame import sys # Initialize Pygame pygame.init() # Set up the game window screen_width = 640 screen_height = 480 screen = pygame.display.set_mode((screen_width, screen_height)) pygame.display.set_caption("Pong") ======= import pygame import sys import random # Initialize Pygame pygame.init() # Set up the game window screen_width = 640 screen_height = 480 screen = pygame.display.set_mode((screen_width, screen_height)) pygame.display.set_caption("Pong") # Paddle and Ball classes class Paddle(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.width = 10 self.height = 60 self.image = pygame.Surface((self.width, self.height)) self.image.fill((255, 255, 255)) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y def move(self, dy): if self.rect.y + dy >= 0 and self.rect.y + dy <= screen_height - self.height: self.rect.y += dy class Ball(pygame.sprite.Sprite): def __init__(self): super().__init__() self.width = 10 self.height = 10 self.image = pygame.Surface((self.width, self.height)) self.image.fill((255, 255, 255)) self.rect = self.image.get_rect() self.rect.x = screen_width // 2 - self.width // 2 self.rect.y = screen_height // 2 - self.height // 2 self.dx = random.choice([-1, 1]) * 4 self.dy = random.choice([-1, 1]) * 4 def update(self): self.rect.x += self.dx self.rect.y += self.dy # Collision with top and bottom if self.rect.y <= 0 or self.rect.y >= screen_height - self.height: self.dy = -self.dy # Scoring if self.rect.x <= 0 or self.rect.x >= screen_width - self.width: self.reset() def reset(self): self.rect.x = screen_width // 2 - self.width // 2 self.rect.y = screen_height // 2 - self.height // 2 self.dx = random.choice([-1, 1]) * 4 self.dy = random.choice([-1, 1]) * 4 def collide_with(self, paddle): if self.rect.colliderect(paddle.rect): self.dx = -self.dx >>>>>>> UPDATED

注意这里只替换了文件的开头部分(文件头与窗口初始化),文件后半部分的游戏循环保持不动——这正是 SEARCH/REPLACE 机制的典型用法:SEARCH 段精确锁定一小段上下文,REPLACE 段将其替换为更长的内容。

接着第二个 edit block 替换游戏循环占位符,加入对象初始化、键盘输入(W/S 控制左拍,方向键控制右拍)、球体更新与碰撞、绘制逻辑以及 16ms 帧延迟:

pong_game.py <<<<<<< ORIGINAL # Game loop running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # Update game objects # Draw game objects # Update the display pygame.display.flip() ======= # Initialize paddles and ball left_paddle = Paddle(10, screen_height // 2 - 30) right_paddle = Paddle(screen_width - 20, screen_height // 2 - 30) ball = Ball() # Game loop running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # Handle user input keys = pygame.key.get_pressed() if keys[pygame.K_w]: left_paddle.move(-5) if keys[pygame.K_s]: left_paddle.move(5) if keys[pygame.K_UP]: right_paddle.move(-5) if keys[pygame.K_DOWN]: right_paddle.move(5) # Update game objects ball.update() ball.collide_with(left_paddle) ball.collide_with(right_paddle) # Draw game objects screen.fill((0, 0, 0)) screen.blit(left_paddle.image, left_paddle.rect) screen.blit(right_paddle.image, right_paddle.rect) screen.blit(ball.image, ball.rect) # Update the display pygame.display.flip() pygame.time.delay(16) >>>>>>> UPDATED

两个 edit block 全部应用成功后,aider 再次提交:

Applied edit to pong_game.py Applied edit to pong_game.py Commit b20bcee aider: Added Paddle and Ball classes, handled user input, updated ball position, drew objects, and handled scoring.

到这一步,一个可玩的基础 Pong 游戏已经诞生。从实现上看,Ball.update()负责位移与上下边界反弹,collide_with()pygame.Rect.colliderect做 AABB 碰撞检测并反向水平速度,Paddle.move()则通过边界检查把拍子限制在窗口内——这些细节全部由 LLM 一次生成,用户无需手写任何代码。

第三步:微调游戏手感——降低球速

用户反馈 "the ball moves way too fast",aider 通过一个极小的 edit block 把球的初速度从 4 降到 2:

pong_game.py <<<<<<< ORIGINAL self.dx = random.choice([-1, 1]) * 4 self.dy = random.choice([-1, 1]) * 4 ======= self.dx = random.choice([-1, 1]) * 2 self.dy = random.choice([-1, 1]) * 2 >>>>>>> UPDATED

Applied edit to pong_game.py Commit f4ceed4 aider: Reduced ball speed by half.

这个改动同时影响Ball.__init__Ball.reset(两处代码相同,SEARCH/REPLACE 块会替换所有匹配出现的位置——这正是 editblock_prompts.py 中"will replaceallmatching occurrences"规则的实际体现)。aider 也在回复中提示用户可自行微调数值以获得更慢的效果,体现了它"尊重用户控制权"的设计。

第四步:视觉定制——加大拍子并改成红蓝配色

用户提出 "make the paddles a bit bigger? and make one paddle red and one blue?",aider 分两次改动完成:

先修改Paddle类,尺寸从 10x60 增大到 15x80,并依据拍子所在半区(x 坐标是否小于窗口中线)设置默认颜色——右半区默认蓝色:

pong_game.py <<<<<<< ORIGINAL self.width = 10 self.height = 60 self.image = pygame.Surface((self.width, self.height)) self.image.fill((255, 255, 255)) ======= self.width = 15 self.height = 80 self.image = pygame.Surface((self.width, self.height)) self.color = (255, 255, 255) if x < screen_width // 2 else (0, 0, 255) self.image.fill(self.color) >>>>>>> UPDATED

再单独把左拍设为红色(同时微调其初始 y 坐标以适配新尺寸):

pong_game.py <<<<<<< ORIGINAL left_paddle = Paddle(10, screen_height // 2 - 30) ======= left_paddle = Paddle(10, screen_height // 2 - 40) left_paddle.color = (255, 0, 0) left_paddle.image.fill(left_paddle.color) >>>>>>> UPDATED

Applied edit to pong_game.py Applied edit to pong_game.py Commit 2c645c9 aider: Changed paddle size and color, and set left paddle to red.

这里有两个值得注意的细节:一是对Paddle类的改动会自动作用于左右两个拍子(因为它们共用同一个类),二是 aider 用了两个独立的 edit block 来分别处理"类定义"与"实例初始化"两个位置,验证了 prompts 中"将大改动拆分为一系列小块"的建议。

第五步:游戏机制增强——每次反弹加速 10%

最后一个需求颇具游戏设计意味:

each time the ball bounces off the paddle, can you make it go 10% faster?

aider 修改Ball.collide_with(),把原本的纯反向改为反向并放大 1.1 倍:

pong_game.py <<<<<<< ORIGINAL def collide_with(self, paddle): if self.rect.colliderect(paddle.rect): self.dx = -self.dx ======= def collide_with(self, paddle): if self.rect.colliderect(paddle.rect): self.dx = -self.dx * 1.1 self.dy *= 1.1 >>>>>>> UPDATED

Applied edit to pong_game.py Commit 693faf4 aider: Increased ball speed by 10% in both x and y directions each time it bounces off a paddle.

这一改动使水平方向在反转的同时放大,垂直方向直接放大,球体随拍击次数逐次加速,为游戏引入了逐步上升的难度曲线——全部通过一句自然语言需求完成,无需用户自己定位方法或推导数学表达式。

源码解析:edit block 是如何被解析与应用的

看完整个案例,我们来追一遍 edit block 在源码中的生命周期,这能帮助读者理解"为什么 SEARCH 必须精确匹配"以及"匹配失败时会发生什么"。

解析阶段。在 editblock_coder.py 的find_original_update_blocks()中,回复文本按行扫描:遇到<<<<<<< SEARCH标记后收集 SEARCH 段直至=======,再收集 REPLACE 段直至>>>>>>> REPLACE,并以(文件名, SEARCH 文本, REPLACE 文本)三元组产出。文件名通过find_filename()从 edit block 上方最多 3 行中回溯查找,支持精确匹配、基于Path的 basename 匹配、以及difflib.get_close_matches的模糊匹配(cutoff=0.8)。

应用阶段。apply_edits()遍历解析出的 edit block,对每个 block 调用do_replace():如果目标文件不存在且 SEARCH 为空,则创建新文件(对应案例第一步);否则调用replace_most_similar_chunk()在文件中查找 SEARCH 文本并替换。查找按"完美匹配 → 容忍统一缩进偏差 → 容忍首个空行 → 支持...省略号(对应try_dotdotdots())"的优先级逐级降级容错。如果 SEARCH 段在文件中找不到匹配,aider 会返回详细的错误报告,指出该 block 匹配失败,并用find_similar_lines()基于SequenceMatcher相似度(阈值 0.6)给出"你是否想匹配这些行"的提示。

测试验证。仓库中的 test_editblock.py 对这一整套协议做了系统测试,例如test_find_original_update_blocks验证从带围栏文本中正确提取("foo.txt", "Two\n", "Tooooo\n")test_find_filename验证文件名可从# file1.pyfile1_py等变体中正确解析;test_find_original_update_blocks_missing_filenametest_find_original_update_blocks_unclosed则分别验证缺少文件名、缺少>>>>>>> REPLACE结束标记时抛出ValueError。这些测试从侧面印证了案例中每个 edit block 之所以能被"Applied edit"成功,是因为 SEARCH 段与文件内容的精确匹配得到了严格保证。

迭代成果:最终的 pong_game.py

将五个 edit block 依次应用到初始骨架后,最终可运行的游戏代码如下(累计了全部定制:球速 2、拍子 15x80、左红右蓝、反弹加速 10%):

import pygame import sys import random # Initialize Pygame pygame.init() # Set up the game window screen_width = 640 screen_height = 480 screen = pygame.display.set_mode((screen_width, screen_height)) pygame.display.set_caption("Pong") # Paddle and Ball classes class Paddle(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.width = 15 self.height = 80 self.image = pygame.Surface((self.width, self.height)) self.color = (255, 255, 255) if x < screen_width // 2 else (0, 0, 255) self.image.fill(self.color) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y def move(self, dy): if self.rect.y + dy >= 0 and self.rect.y + dy <= screen_height - self.height: self.rect.y += dy class Ball(pygame.sprite.Sprite): def __init__(self): super().__init__() self.width = 10 self.height = 10 self.image = pygame.Surface((self.width, self.height)) self.image.fill((255, 255, 255)) self.rect = self.image.get_rect() self.rect.x = screen_width // 2 - self.width // 2 self.rect.y = screen_height // 2 - self.height // 2 self.dx = random.choice([-1, 1]) * 2 self.dy = random.choice([-1, 1]) * 2 def update(self): self.rect.x += self.dx self.rect.y += self.dy # Collision with top and bottom if self.rect.y <= 0 or self.rect.y >= screen_height - self.height: self.dy = -self.dy # Scoring if self.rect.x <= 0 or self.rect.x >= screen_width - self.width: self.reset() def reset(self): self.rect.x = screen_width // 2 - self.width // 2 self.rect.y = screen_height // 2 - self.height // 2 self.dx = random.choice([-1, 1]) * 2 self.dy = random.choice([-1, 1]) * 2 def collide_with(self, paddle): if self.rect.colliderect(paddle.rect): self.dx = -self.dx * 1.1 self.dy *= 1.1 # Initialize paddles and ball left_paddle = Paddle(10, screen_height // 2 - 40) left_paddle.color = (255, 0, 0) left_paddle.image.fill(left_paddle.color) right_paddle = Paddle(screen_width - 20, screen_height // 2 - 30) ball = Ball() # Game loop running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # Handle user input keys = pygame.key.get_pressed() if keys[pygame.K_w]: left_paddle.move(-5) if keys[pygame.K_s]: left_paddle.move(5) if keys[pygame.K_UP]: right_paddle.move(-5) if keys[pygame.K_DOWN]: right_paddle.move(5) # Update game objects ball.update() ball.collide_with(left_paddle) ball.collide_with(right_paddle) # Draw game objects screen.fill((0, 0, 0)) screen.blit(left_paddle.image, left_paddle.rect) screen.blit(right_paddle.image, right_paddle.rect) screen.blit(ball.image, ball.rect) # Update the display pygame.display.flip() pygame.time.delay(16) # Quit Pygame pygame.quit() sys.exit()

运行方式为:

pip install pygame python pong_game.py

案例总结:这套工作流给开发者带来的启示

回看整段对话,从第一条需求到最终可玩的定制版 Pong,共经历 5 轮交互、6 次文件编辑、5 次自动 git 提交(14799cbb20bceef4ceed42c645c9693faf4),每一轮都遵循"用户提需求 → LLM 给出 SEARCH/REPLACE edit block → aider 精确应用 → 自动提交"的闭环。这个案例至少说明三点:

  1. edit block 是 aider 与模型之间的稳定协议:精确匹配的 SEARCH 段保证了编辑的可预期性,即使匹配失败,aider 也会给出清晰的诊断与相似行提示(见 editblock_coder.py 的错误处理逻辑);
  2. 粒度化提交天然形成开发历史:每个需求对应一次语义清晰的 commit,方便回滚与回溯,这得益于 base_coder.py 中对 git 操作的内建支持;
  3. 自然语言可以承载"设计意图"而不只是"代码操作":从"球太快"到"每次反弹加速 10%",用户描述的是体验与目标,模型负责将其翻译为具体数值与逻辑改动。

如果你想继续深入了解 edit block 的完整语法规范、更多示例 transcript(如 Flask 应用、多文件复杂改动、测试用例生成等),可以继续阅读 examples/README.md 中列出的其他对话记录,或直接查看 editblock_prompts.py 中写给模型的完整规则。

【免费下载链接】Qwen3-CoderQwen3-Coder is the code version of Qwen3, the large language model series developed by Qwen team.项目地址: https://gitcode.com/GitHub_Trending/co/Qwen3-Coder

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 23:08:34

沈阳网站建设tlmh:2026最新避坑指南,告别模板丑站

沈阳网站建设tlmh:2026最新避坑指南,告别模板丑站 模板网站确实太丑,而且功能僵化,根本撑不起你的品牌调性。很多沈阳老板花了几千块做了个站,结果上线后不仅客户觉得廉价,连搜索引擎都抓不住重点,流量惨淡得让人想哭。…

作者头像 李华
网站建设 2026/9/14 23:06:22

Git Graph 分支历史,Codex 连上 TaoToken 后能理清提交记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 23:05:06

uC3845 反激拓扑 ±12V 双极性电源设计:从环路补偿到量产实测全解析

副标题: 基于 uC3845 单端反激拓扑的 12V 双极性电源实战——覆盖变压器多绕组设计、Type II 环路补偿、交叉稳压抑制与 86.5% 效率实测验证,为工业模拟前端提供可落地的供电方案。 快速阅览: ① 拓扑选型:基于 uC3845 单端反激拓扑实现 12V 双极性输出,30W-60W 场景最优解…

作者头像 李华
网站建设 2026/9/14 23:03:17

HarmonyOS 6.0智能体框架开发实战指南

1. HarmonyOS 6.0 Agent Framework Kit技术全景作为HarmonyOS 6.0的核心能力升级之一&#xff0c;Agent Framework Kit&#xff08;智能体框架服务&#xff09;重新定义了应用与智能体的交互方式。我在实际开发中发现&#xff0c;这个框架真正实现了"智能体即服务"的…

作者头像 李华
网站建设 2026/9/14 23:03:15

OpenClaw开源AI智能体安全部署与加固实战

1. OpenClaw项目概述与安全挑战OpenClaw作为当前最热门的开源AI智能体项目之一&#xff0c;其核心价值在于通过自然语言指令实现复杂的自动化任务处理。这个项目在GitHub上短短几天就获得了超过16万star&#xff0c;充分证明了开发者社区对它的认可。但正如我在实际部署过程中发…

作者头像 李华