☰
终端打砖块游戏与Claude任务进度联动:Python+curses实战
2026/10/23 8:47:20 网站建设 项目流程

上周三下午,我把一个“重构认证模块”的任务丢给 Claude 之后,就进入了那种最磨人的状态:盯着终端,看日志一条一条刷,不知道它下一次回话是什么时候。等到第十分钟,我开始在脑子里数砖墙——不是比喻,是我真的想玩打砖块了。

这个念头后来长成了一个周末项目:在终端里跑一个打砖块小游戏,同时让 Claude 在后台干活。它不是什么花哨的 GUI 游戏,就是一个用字符拼出来的、能真的把球打来打去的 Python 脚本,再加上一层与任务进度联动的逻辑。Claude 每完成一个子任务,进度文件就更新一次,游戏里的砖块就随之少一块;我这边则用球板把其余砖块一个个敲掉,谁先清完,谁赢。

这篇文章适合两类人:一类是天天跟 AI 命令行工具打交道、等任务等到发慌的开发者;另一类是纯粹想在终端里折腾点好玩东西的 TUI 爱好者。我会从终端画布原理讲到打砖块的物理和手感,再讲怎么把任务进度和游戏状态串起来,最后是实测一周踩过的坑。代码都是 Python + curses,几十行就能跑起来。

1. 为什么把游戏塞进终端而不是开个窗口

1.1 终端游戏到底图什么

直接开个网页版的打砖块不好吗?当然好,但有几个现实问题。第一,当我通过 SSH 连着一台服务器的时候,网页游戏帮不上忙,服务器上只有一坨黑底白字的终端。第二,项目跑到一半,你不太想切换到浏览器去刷新一个高亮闪烁的页面,这会打断注意力,还会让同事以为你在摸鱼刷网页。终端里的小游戏反而理直气壮——看起来就像在跑某个命令行工具。

更关键的一点是“上下文不切换”。玩终端游戏的时候,我的眼睛始终没有离开命令行环境,余光能扫到 Claude 的输出。网页游戏就不行,切出去之后经常会忘了切回来,等想起来的时候任务早跑完了,白等。终端游戏本质上是一种“盯盘辅助工具”,它把等待 AI 干活这件事从被动发呆变成了主动参与。

1.2 选型对比:四套方案,我为什么用 curses

做终端游戏,市面上流行好几套路子,我列一下实测后的感受:

方案语言特点适合场景
cursesPython标准库自带,跨 Unix 平台,Windows 需补包快速原型、脚本小游戏
裸 ANSI 转义任意语言零依赖,但光标管理、颜色管理全靠自己单文件小玩具、炫技
richPython渲染文本漂亮,但实时刷新偏重日志输出、进度条类工具
ratatuiRust性能好,状态管理严谨正式点的 TUI 工具

我最终选了 Python + curses。原因很朴素:Python 写起来快,curses 把终端初始化、光标移动、颜色设置、键盘输入这些脏活全包了,我在家里用 macOS 能跑,连到服务器上也能跑。curses 唯一的麻烦是 Windows 上要装一个windows-curses包,装完之后行为也基本一致。输入这块,curses 的getch()支持方向键,这在裸 ANSI 方案里是要自己解析的,省了很多事。

2. 终端画布原理:ANSI 转义序列就是一套打印指令

2.1 终端是怎么“画画”的

很多人以为终端只能打印文本,其实终端的本质是一个字符网格,每一个格子都可以单独设置前景色和背景色。你只要记住一个核心概念:终端的“画笔”就是光标,发出去的一切都是“把光标挪到某个位置,然后打印字符”。

控制光标位置和颜色靠的是 ANSI 转义序列,形式是一串以ESC(也就是\033)开头的指令。比如:

# 先清屏,再把光标挪到第 5 行第 10 列,画一个红色方块 print("\033[2J", end="") print("\033[5;10H", end="") print("\033[41m \033[0m", end="")

\033[2J是清屏,\033[5;10H是把光标移到第 5 行第 10 列,\033[41m是设置红色背景,末尾的\033[0m是重置所有样式。这个套路就是所有终端游戏的底层原理:每一次刷新,本质上都是“清屏 + 把画面里的每个元素挪到新位置重新打印一遍”。

如果只用裸 ANSI,你要自己维护整个画面的坐标和状态,写起来很累。curses 的价值就在这里:它把“光标移动、颜色、清屏、刷新”封装成了函数,还帮你处理了终端尺寸变化、特殊按键这些事情。

2.2 curses 帮你干了哪些脏活

一个最小的 curses 程序长这样:

import curses import time def main(stdscr): curses.curs_set(0) # 隐藏光标 stdscr.nodelay(True) # getch 不阻塞 stdscr.timeout(16) # 最多等 16ms,约 60 帧 h, w = stdscr.getmaxyx() px = w // 2 # 球板位置 while True: key = stdscr.getch() if key == ord("q"): break if key == curses.KEY_LEFT: px = max(0, px - 2) if key == curses.KEY_RIGHT: px = min(w - 8, px + 2) stdscr.erase() stdscr.addstr(h - 2, px, "=" * 8) stdscr.refresh() time.sleep(0.016) curses.wrapper(main)

curses.wrapper(main)负责进入和退出终端图形模式,就算程序崩了也会把终端恢复正常,这个很重要。nodelay(True)让getch()在没有按键时立刻返回-1,配合timeout(16)让游戏循环以固定节奏跑起来。erase()清空画面,画完东西后refresh()一次性把整个缓冲区推到屏幕上。

2.3 双缓冲与刷新策略

我第一次写的时候有个明显问题:画面闪烁。原因是每帧都清屏、画、刷新,而清屏和画之间隔着一点时间,终端会先闪成空白再出现画面。curses 的解法是把画面先画到一块离屏缓冲区里,再用refresh()一次性提交,这就是经典的双缓冲。

如果画面里有大量元素,还可以用curses.newpad建一个比屏幕还大的画布,只把需要看的部分渲染出来。打砖块这种简单游戏用不着 pad,erase()+refresh()就够了。重要的一点是别在addstr和refresh之间做耗时操作,比如读网络文件,那会明显拉低帧率。

3. 打砖块的物理与手感:从球速到反弹角度的调优

3.1 游戏循环的四种职责

任何终端游戏,循环体都是四件事:读输入、更新状态、绘制画面、按节奏休眠。打砖块的状态尤其简单,无非是球的坐标、球板坐标、砖块列表,外加一个计分器。

球的运动本质上是坐标的线性变化:

bx += vx * speed by += vy * speed

vx和vy是单位方向向量,speed是每帧移动的格数。碰到左右墙就把vx取反,碰到天花板就把vy取反,球掉出屏幕底部就扣一条命。这个逻辑几乎没有难度,真正麻烦的是“碰撞判定”和“手感”。

3.2 球的反弹角度:中间平直,两边刁钻

如果球碰到球板永远是垂直向上反弹,游戏十几秒就无聊了。经典的街机做法是:根据球打在球板的哪个位置来决定反弹角度,打在中间接近垂直,打在边缘就飞向斜侧。换算成代码是这样的:

import math def reflect(hit_pos, paddle_w): # hit_pos 是球碰到球板时,相对球板左边缘的偏移 ratio = hit_pos / paddle_w # 0 到 1 angle = (ratio - 0.5) * (2 * math.pi / 3) # 映射到 -60° ~ +60° vx = math.sin(angle) vy = -math.cos(angle) mag = math.hypot(vx, vy) return vx / mag, vy / mag

这个函数是整场游戏手感的灵魂。angle的范围我调了很久,最初用 ±45°,结果球总是走太斜的路线,经常一两个来回就漏掉;后来放宽到 ±60°,局面立刻活了。再配合一个技巧:球板在移动时给球一个微小的水平速度加成,这样球不会永远在几条固定轨迹上打转。

3.3 砖块碰撞:先分清撞的是横边还是竖边

砖块碰撞最容易出错的地方是搞不清该翻转哪个方向。简单粗暴的做法是碰到砖块就同时翻转vx和vy,这会让球的轨迹变得不可控。正确的思路是先判断球是从水平方向撞进砖块,还是从垂直方向撞进去:

def brick_hit(bx, by, brick): x0, y0, x1, y1 = brick if not (x0 <= bx <= x1 and y0 <= by <= y1): return None # 比较球心到四条边的距离,哪边近就视为从哪边撞入 dx = min(bx - x0, x1 - bx) dy = min(by - y0, y1 - by) return 'x' if dx < dy else 'y'

返回'x'就翻转vx,返回'y'就翻转vy。这个判定其实是个近似解,但对字符网格来说足够准确。砖块的排列我用 6 行、每行 10 个,每个砖块占 6 个字符宽度,砖块之间留一个空字符,避免碰撞判定时两个砖块连成一片。

3.4 手感调优的三板斧

第一是速度递增。每打掉 4 块砖,speed涨 0.1,但设一个上限,比如 3.0。没有递增的游戏打到一半会犯困,递增太快又会让连续反弹失控,我最后用的是“温和递增 + 硬上限”的组合。

第二是防止穿模。当球速很快时,球一帧可能移动 2 格以上,这就可能直接“跳过”一块只有 1 格厚的砖块。我的做法是把一次移动拆成多个小步,每步最多移动 0.5 格,分别做碰撞检测。游戏规模不大,这个开销可以忽略。

第三是输得体面。三颗球掉光才判负,每次掉球给一个 1 秒的停顿和屏幕中央的提示。别小看这个停顿,它给了玩家调整呼吸的节奏,让手感接近真正的街机游戏。

4. 让 Claude 的进度变成砖块:任务联动与胜负判定

4.1 进程编排:让 Claude 在后台跑,游戏在前台跑

这里要解决的是“一个终端怎么同时干两件事”。最简单粗暴的方案是开两个终端窗口,左边跑 Claude,右边跑游戏。但我更推荐 tmux 分屏,一套会话里左看任务、右玩游戏,随时切换注意力:

tmux new-session -d -s claude-work 'bash run_steps.sh /tmp/claude_progress.json' tmux split-window -h 'python tbreak.py --progress /tmp/claude_progress.json' tmux attach -t claude-work

不过分屏还是“人肉联动”,你得自己用眼睛看任务走到哪一步。更好的做法是让游戏自动感知 Claude 的进度。

4.2 进度文件协议:一个 JSON 文件连接两边

我的方案是让任务脚本把进度写进一个 JSON 文件,游戏每帧读取一次。写文件时最重要的一课是“原子写”:直接往目标文件写内容,游戏可能读到写了一半的半个 JSON。正确做法是先写临时文件,再mv覆盖:

#!/usr/bin/env bash set -u PROGRESS_FILE=/tmp/claude_progress.json rm -f "$PROGRESS_FILE" steps=( "梳理认证模块现有调用链,输出文件清单" "给登录接口补充超时与重试逻辑" "重构 token 刷新流程" "为改动补充单元测试" "跑全量测试并输出变更总结" ) total=${#steps[@]} for ((i=0; i<total; i++)); do current="${steps[$i]}" prompt="任务进度 $((i+1))/${total},请完成:${current}。完成后只回复 DONE。" claude -p "$prompt" || echo "WARN: step $i 执行失败" done_count=$((i+1)) printf '{"done": %d, "total": %d, "current": "%s"}' \ "$done_count" "$total" "$current" > "$PROGRESS_FILE.tmp" mv "$PROGRESS_FILE.tmp" "$PROGRESS_FILE" done printf '{"done": %d, "total": %d, "current": "%s"}' \ "$total" "$total" "全部完成" > "$PROGRESS_FILE.tmp" mv "$PROGRESS_FILE.tmp" "$PROGRESS_FILE"

游戏这边每帧读取:

import json def read_progress(path): try: with open(path, "r", encoding="utf-8") as f: return json.load(f) except (FileNotFoundError, json.JSONDecodeError): return {"done": 0, "total": 0, "current": ""} pg = read_progress("/tmp/claude_progress.json") done = pg.get("done", 0) total = pg.get("total", 0)

读取可能失败,所以except里返回默认值很重要,游戏不能因为任务脚本还没开始写文件就崩溃。我还在画面顶部把current字段打印出来,这样游戏界面本身就是一个任务监视器,看砖块的同时能看到 Claude 正在做哪一步。

4.3 和 Claude 赛跑:谁先清完谁赢

这一步是最有乐趣的设计。游戏初始化时生成total块砖,索引小于done的砖块直接显示为“已被任务清掉”的灰色状态。玩家要打的是剩下的砖块,而 Claude 每完成一步,灰色砖块就多一块,玩家的目标是在 Claude 清完之前把自己的砖块全部敲掉。

胜负判定有三个分支:

  • 玩家球掉光三次,直接判负,和 Claude 进度无关;
  • Claude 的done先到达total,游戏弹出“Claude 赢了,回去干活吧”;
  • 玩家先清完全部砖块,显示“你赢了,但任务可能还在跑”。

这个“赛跑”机制让等待过程变成了真正的游戏:任务越顺,砖块消失越快,你就得加快手速;任务卡住,砖块半天不动,你反而可以慢慢打。它把一个不可控的等待过程,变成了一个和 AI 互动的节奏。我实测下来,任务耗时长短对心态的影响明显变小了。

4.4 不想要联动怎么办

如果你只是想在等任务的时候单纯打砖块,完全可以关掉进度联动,游戏退化成普通街机模式。我在实现里留了一个--entertainment参数,传进去就忽略进度文件,砖块固定为 60 块,Claude 的进展只在屏幕角落用一行小字显示。两种模式用同一个游戏内核,只是砖块生成的方式不同。

5. 实测一周的体验与那些终端游戏坑

5.1 一周实测:等待焦虑真的变成了游戏节奏

我把这套东西实际用了一周,跑了三个不同类型的任务:一次代码重构、一次文档生成、一次数据清洗脚本的长时间运行。文档生成任务最久,跑了四十多分钟,期间我大概打了几十局。最大的感受是:等待 AI 干活这件事从“焦虑的盯盘”变成了“有节奏的互动”。砖块还剩多少、Claude 跑到哪一步,余光一扫就知道,不需要一直盯着屏幕刷新。

不过也有副作用。砖块多的时候会打到忘我,好几次任务早就完成了,我还沉浸在连续反弹的节奏里,直到系统提示音响起才意识到。所以我给游戏加了两个机制:一是 Claude 写完全部任务时终端会响铃(用到beep()),二是游戏界面顶部始终显示当前任务步数,加粗高亮。这样就算你打上头了,也有一个强提示把你拉回来。

5.2 终端游戏的经典坑

坑表现解决办法
终端窗口尺寸变化画面错位、addstr 越界报错监听KEY_RESIZE,然后重新读取getmaxyx()并重排砖块
SSH 下颜色丢失颜色全部变成默认色连服务器时设置TERM=xterm-256color,或者检测到 8 色模式时降级配色
Windows 上方向键失灵方向键返回奇怪字符安装windows-curses,并支持 WASD 作为备用键位
球速过快穿模球穿过砖块不掉头分步子移动,每步不超过 0.5 格,逐段做碰撞检测
忘记恢复终端游戏退出后终端花掉始终用curses.wrapper启动,异常退出也能恢复

最让我意外的是 CPU 占用。我一开始想当然地觉得一个字符游戏不会费什么电,实测在笔记本上跑time.sleep(0.016)的循环,CPU 大约占 3% 到 5%,基本可以忽略。但如果你把sleep去掉,循环变成纯忙等,CPU 直接拉满,笔记本风扇就开始响了。所以定时休眠不是可选项,是必须项。

5.3 还能怎么玩:老板键、难度曲线、声音开关

这一周里我顺手加了几个让同事直呼“离谱”的功能。第一个是“老板键”:按b键,游戏画面立刻消失,显示成一个假的日志滚动界面,内容就取自 Claude 的真实输出,再按一次b切回游戏。这个功能在办公室真的很实用。

第二个是难度曲线调整。我把砖块按行分层,越靠下的砖块分数越高、颜色越亮,玩家的目标也从“清完所有砖”变成了“优先抢高分砖块”,这样和 Claude 赛跑时策略性更强。

第三个是声音的克制使用。curses 自带的beep()在终端里会发出蜂鸣声,用好了是任务完成提示,用多了就是办公室公敌。我把它做成开关,默认只在“Claude 全部完成”和“玩家输掉”时响一声。

最后说点实在的。如果你也想搞一个,直接按上面的思路做就行,最难的部分其实不是物理,也不是渲染,而是想清楚“什么时候让游戏结束”。游戏结束的时机设计好了,等待 AI 干活这段碎片时间就会变成一天里难得的专注小游戏时间;设计不好,它就是一个让你忘记正事的摸鱼工具。我自己现在的习惯是:大任务启动后先打两局热身,等进度过半就收手,专心等 Claude 收尾。砖块能不能全清不重要,重要的是你别把自己给玩进去了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询