☰
用Python写一个终端版2048:从二维列表到核心算法全拆解
2026/10/10 15:29:47 网站建设 项目流程

2048 大概是我见过规则最简单、却最容易让人上头的数字游戏之一。明明只是在一个 4×4 的棋盘里滑动数字、合并相同的方块,却能让无数人一局接一局停不下来。作为一个 Python 学习者,这个游戏还有一个隐藏价值:它背后的数据结构和逻辑,几乎能把你入门阶段的语法全部串起来。

我第一次带新人学 Python 时,最喜欢的练手项目就是这个。不是因为 2048 有多简单,而是因为它难写——难得恰到好处:写棋盘要懂二维列表,处理合并要懂循环和条件,判断输赢要懂函数封装,随机生成方块还要碰一下 random 库的用法。把这些打通,你再看那些教程里的爬虫、量化分析、可视化例子,会发现很多代码你都能看得懂了。

这篇文章不聊高大上的框架,也不做花哨的 GUI,就用一个干净的终端版 2048,把核心算法一步步拆开讲明白。你拿到手就能跑,跑起来就能玩。

1. 为什么拿 2048 当 Python 练手项目?

之前经常有人问我:Python 基础语法学完了,但写完几个练习题就不知道干什么,怎么办?我的建议往往是同一个——找一个“写完能玩、跑起来有反馈、逻辑量刚好卡在脑子上”的小项目。2048 就是教科书级别的选择。

它好就好在三个地方。第一,规则足够简单,不需要游戏策划文档,三句话能说清楚:4×4 棋盘里有数字方块,每次滑动所有方块朝同方向移动,相同数字碰到一起就合并成它们的和,每移动一次棋盘空位会随机出现一个新的 2 或 4。第二,逻辑链路完整,从数据结构设计到算法实现,再到流程控制和交互输入,一套下来就是一次完整的程序开发演练。第三,可扩展性极强,做完基础版,你能加颜色、加动画、加最高分存档,还能给游戏写一个自动演示的 AI。这种“做完还能继续折腾”的项目,才不容易让学习热情熄灭。

1.1 一个刚刚好的练手项目

很多人学 Python 都会踩两个极端。一个极端是只抄别人的示例代码,抄完等于没学,脑子里留不下半点结构;另一个极端是上手就啃过于庞大的项目,比如直接去写爬虫框架、写量化交易回测系统,结果被各种第三方库的报错劝退。2048 恰好站在中间。

它用到的 Python 知识点非常集中:二维列表的创建与索引、列表推导式、函数定义与返回值、while 循环、if 条件分支、random 随机数、字符串格式化、以及最容易被忽略的“深拷贝与浅拷贝”问题。这些知识点单个拿出来都很基础,但要在一段代码里配合着用干净,就需要一点组织能力。这正是实际开发里最看重的东西:不是你会多少语法糖,而是能不能把一堆基础零件拼成一个能正常运行的系统。

另外这个项目还有一个很实际的好处——反馈极其直观。你写完一个显示函数,运行一下,就能看到棋盘;写完合并逻辑,玩两步就知道有没有 bug。不像某些后端项目,吭哧半天看不到一个界面,很容易失去耐心。

1.2 技术选型:为什么推荐“控制台版”

也许有人会问:2048 网页版那么多,为什么不直接用 tkinter 做图形界面,或者用 pygame 做动画?我的回答是:第一版最好别碰 GUI,碰了就偏离学习重点了。

控制台版有一个天然优势:零第三方依赖。你只需要一个 Python 解释器,不需要 pip install 任何东西,代码量也小,注意力可以全部集中在游戏的核心算法上。等你把棋盘数据结构、移动合并逻辑、输赢判断这些都吃透了,再想套 GUI 其实非常简单——界面只是换了一层皮,底层的move()、merge_row()函数可以原封不动搬过去复用。

还有一个不那么正经、但很现实的原因:终端版非常适合在等待编译、测试跑批的间隙摸两把。黑底白字的窗口看起来像在盯日志,和正在 Debug 的界面没有本质区别。不过说真的,游戏再好玩,工作该交付还是要交付,摸鱼也要讲基本法,我是说——记得让代码跑完。

方案第三方依赖学习重点开发耗时“隐蔽性”
控制台版无算法与流程控制1-2 小时高,像看日志
tkinter 版标准库自带GUI 布局与事件绑定3-4 小时低,明显有个窗口
pygame 版需 pip 安装游戏循环、精灵、音效5 小时以上低,窗口形态太明显

2. 把游戏规则翻译成数据结构

写游戏之前最重要的一步,是把现实规则翻译成程序里的数据模型。2048 的棋盘是一张 4×4 的方格,那么程序里最自然的对应物就是“二维列表”,也就是一个每个元素又是一个列表的列表。别小看这个选择,数据结构定好了,后面所有算法都会顺。

2.1 环境准备:一个 Python 解释器就够了

如果你还没装 Python,先去官网下载对应操作系统的安装包。Windows 用户尤其要记得一个细节:安装向导第一个界面就把“Add Python to PATH”勾上,否则后面在终端敲python会提示找不到命令。

装完之后打开终端(Windows 是 PowerShell 或 CMD,macOS/Linux 是 Terminal),输入python --version,看到版本号就说明环境好了。本文的代码不挑版本,Python 3.8 以上随便跑,语法非常保守,3.6 之后的版本问题都不大。

编辑器随你,VS Code 装个 Python 扩展就能自动补全和调试,PyCharm 也行,甚至纯记事本也能写。重点是能跑出结果,工具不是瓶颈。整篇文章里我们只会用到random和os这两个标准库,不需要额外安装任何包。

2.2 4×4 棋盘:用二维列表承载

棋盘用二维列表表示,每一行是一个列表,棋盘本身是包含 4 个列表的列表。创建空棋盘,最直接的写法是:

SIZE = 4 board = [[0] * SIZE for _ in range(SIZE)]

这里有个很多新手必踩的坑:不要写成board = [[0] * SIZE] * SIZE。如果这样写,表面上看是 4 行,实际上四行都是同一个列表对象的引用。你修改board[0][0],board[1][0]、board[2][0]、board[3][0]会一起变,因为它们是同一个东西。这就好像你把一张表格复印了四份,结果发现四份其实是同一张纸。用列表推导式[[0] * SIZE for _ in range(SIZE)]才会生成四个独立的列表。

理解了棋盘结构,还要理解“二维列表的索引”这个高频考点。board[r][c]表示第 r 行第 c 列,r 和 c 都从 0 开始。遍历棋盘时,我们通常用两重 for 循环,外层管行,内层管列。后面所有算法都建立在这个索引规则上。

2.3 先让棋盘“看得见”:写一个打印函数

写算法之前,先把显示函数做了。原因很简单:没有一个能看见棋盘的界面,你根本没法调试移动逻辑。显示函数不需要花哨,只要把二维列表里的数字按格子对齐打印出来就行。

import os def display(board, score): os.system('cls' if os.name == 'nt' else 'clear') print(f'Score: {score}') for row in board: print('+' + '------+' * SIZE) print('|' + ''.join(f'{cell:^6}|' if cell else ' |' for cell in row)) print('+' + '------+' * SIZE)

这段代码里有两个小技巧。第一,用f'{cell:^6}'做居中格式化,让每个数字占 6 个字符宽度,这样 2、4、16、128、2048 各种位数都能对齐,不会越打越乱。第二,cell为 0 时输出六个空格,看起来就是一个空位。如果你懒得处理空位显示,直接打印 0 也可以,只是观感差一些。

os.system()用来清屏,Windows 上是cls,macOS/Linux 上是clear。这个细节后面还会再提,因为清屏在不同系统里的兼容性是很多初学者会遇到的第一个环境问题。

3. 核心算法:移动与合并,这才是灵魂

2048 的灵魂不在界面,而在移动合并的算法。很多人以为这个游戏的核心逻辑很复杂,实际上它完全可以简化为一个很优雅的处理流程。我把整个问题拆成三个层次:先解决“一行怎么合并”,再解决“四个方向怎么统一”,最后加上“随机生成新方块”。

3.1 单行合并:压缩、合并、再压缩

把问题先降维——我们只处理一行。这一行里有数字,也有 0(表示空位)。向左移动这一行,本质上要完成三件事:把所有非零数字挪到左边,这叫压缩;把相邻且相同的数字合并,这叫合并;合并之后可能又产生空位,所以需要再一次压缩,把数字重新靠左。

举个例子。一行数据是[2, 0, 2, 2]。向左移动后,正确结果应该是[4, 2, 0, 0]。为什么不是[2, 2, 0, 2],也不是[8, 0, 0, 0]?因为 2048 的规则里,一次滑动过程中,一个数字只能参与一次合并。最右边的两个 2 合并成 4,中间那个 2 跟左边第一个 2 本来可以由一次滑动依次合并出 8,但规则不允许连续合并。所以只能是最右侧的两个 2 先碰头。

用代码实现这个逻辑,我推荐“三步法”:

def merge_row(row): # 第一步:取出所有非零数字,压缩到左边 tiles = [v for v in row if v != 0] # 第二步:从左到右扫描,相邻相同就合并,被合并的位先置 0 score = 0 for i in range(len(tiles) - 1): if tiles[i] == tiles[i + 1]: tiles[i] *= 2 score += tiles[i] tiles[i + 1] = 0 # 第三步:再把非零数字压缩到左边,后面补 0 tiles = [v for v in tiles if v != 0] tiles += [0] * (SIZE - len(tiles)) return tiles, score

第二步是核心。比如处理[4, 4, 4, 4]:第一步拿到[4, 4, 4, 4],第二步扫描,i=0 时两个 4 合并,变成[8, 0, 4, 4],i=1 时 0 和 4 不相等不动,i=2 时两个 4 合并,变成[8, 0, 8, 0],第三步压缩、补零,最后得到[8, 8, 0, 0]。这个过程符合 2048 规则:最左边两个 4 合并成 8,最右边两个 4 合并成另一个 8,这个 8 不会继续跟前面的 8 合并。

你可能会问:为什么合并且置 0 之后再压缩一遍,就能保证“只合并一次”?因为我们在合并过程中把被合并的位置改成了 0,接下来的扫描就不会再把它当成活跃数字去跟后面的数字碰。这个置 0 的操作,是防止连锁合并的关键。

3.2 四个方向统一成“行左移”

一行合并的逻辑写完了,接下来处理整个棋盘。如果只有向左移动,事情就简单了:对每一行调用merge_row()即可。但 2048 有上下左右四个方向,难道要写四份几乎一样的循环?不需要,这里有一个非常优雅的技巧:把四个方向都转化成“行处理”。

向左移动:直接对每行merge_row()。 向右移动:先把每行反转,对反转后的行做merge_row(),再把结果反转回来。原因是,右移等价于“从右边压缩合并”,反转之后就变成了从左边处理。 向上移动:把棋盘转置,对转置后的“行”(也就是原来的列)做merge_row(),再转置回来。 向下移动:先转置,再反转每行,做merge_row(),再反转回来,最后转置回来。

转置操作在 Python 里可以一行搞定:

def transpose(board): return [list(row) for row in zip(*board)]

zip(*board)会把原来按行存储的数据,变成按列分组,正好完成转置。不理解zip也没关系,你只要知道这个函数能把棋盘的 4 行变成 4 列即可。

完整的移动函数这样写:

def move(board, direction): total_score = 0 if direction == 'left': for i, row in enumerate(board): board[i], score = merge_row(row) total_score += score elif direction == 'right': for i, row in enumerate(board): board[i], score = merge_row(row[::-1]) board[i] = board[i][::-1] total_score += score elif direction == 'up': board = transpose(board) for i, row in enumerate(board): board[i], score = merge_row(row) total_score += score board = transpose(board) elif direction == 'down': board = transpose(board) for i, row in enumerate(board): board[i], score = merge_row(row[::-1]) board[i] = board[i][::-1] total_score += score board = transpose(board) return board, total_score

这里有一点需要注意:函数内部直接修改了传入的board,同时也返回了修改后的board。这种写法在实际项目里并不推荐,因为它可能造成“调用方数据被悄悄改动”的混乱。但在游戏主循环里,我们恰恰需要这个效果,因为棋盘是共享状态的,移动之后原棋盘应该直接更新。为了规避风险,主循环里会提前保存一份旧棋盘的副本,用于判断这次移动有没有产生实际变化。

这个“统一成行处理”的思路,是整篇代码里最值得反复品味的部分。它体现的正是编程里“抽象”的价值:把复杂问题降维,把重复代码收敛。以后你写任何涉及网格、表格、矩阵处理的程序,都可以试着找找有没有类似的降维手段。

3.3 随机生成新方块与开局

移动之后要在空位中随机生成一个新方块。原版 2048 的设定是:90% 的概率生成 2,10% 的概率生成 4。这个比例不是拍脑袋定的,生成 4 的概率太高,游戏难度会陡增,玩家很难滚起来;概率太低,前期又过于平淡。所以沿用 90/10 这个经典比例就好。

import random def add_new_tile(board): empty_cells = [(r, c) for r in range(SIZE) for c in range(SIZE) if board[r][c] == 0] if not empty_cells: return r, c = random.choice(empty_cells) board[r][c] = 2 if random.random() < 0.9 else 4

创建棋盘时,一般先放两个初始方块:

def create_board(): board = [[0] * SIZE for _ in range(SIZE)] for _ in range(2): add_new_tile(board) return board

这两步很基础,但不要漏。没有初始的两个数字,整个棋盘全是 0,玩家一开局就会懵。

4. 游戏闭环:主循环、计分与输赢判断

核心函数都写完,接下来就是把这些函数串成一个能持续运行的游戏主循环。这一部分包含输入处理、分数累计、胜负判断和完整代码,属于“最后拼图”阶段。

4.1 主循环:接收指令、移动、生成新块

游戏主循环的逻辑很简单:显示棋盘 → 读玩家指令 → 移动棋盘 → 生成新块 → 再次显示棋盘。用 while 循环包住即可。

def main(): board = create_board() score = 0 won = False while True: display(board, score) if is_game_over(board): print('Game Over') break if is_win(board) and not won: print('恭喜,合成了 2048!按 y 继续挑战,其他键退出') won = True if input().strip().lower() != 'y': break cmd = input('WASD 移动,Q 退出:').strip().lower() if cmd == 'q': break if cmd not in ('w', 'a', 's', 'd'): continue old_board = [row[:] for row in board] board, gained = move(board, cmd) score += gained # 只有棋盘真的发生变化,才生成新方块 if board != old_board: add_new_tile(board)

这里有两个关键细节。第一,old_board = [row[:] for row in board]这一行绝不能用old_board = board代替。因为old_board = board只是给同一个对象起了个别名,move()修改了board,old_board也会跟着变,那board != old_board永远不成立,每次输入都会触发随机生成新块,哪怕你只是按了一个无效键。这个 bug 非常隐蔽,一旦中招,游戏会“凭空冒数字”。所以深拷贝是必须的。

第二,为什么要先比较棋盘变化再生成新块?因为 2048 的规则约定:滑动方向如果没有产生任何有效移动,就不应该刷新棋盘。如果每次都生成,玩家往死路上按几个无效键,棋盘就会被新块填满,游戏失衡。

4.2 计分:把每一分的来路算清楚

计分规则在不同的 2048 版本里略有差异,最通用的是:两个数字合并成新数字时,加分为新数字本身。比如两个 2 合并成 4,加 4 分;两个 4 合并成 8,加 8 分。这个逻辑我直接放进了merge_row()里,所以move()的返回值里带上了total_score。

主循环里,每次移动后把gained累加到score,再传给display()显示。分数不是拍脑袋想出来的,它是玩家操作的直接反馈,也是后期做排行榜、做存档的基础。

4.3 输赢判断:游戏什么时候结束

胜利条件很简单:棋盘上出现 2048。

def is_win(board): return any(2048 in row for row in board)

难点在失败判断。游戏结束有两种情况同时满足:棋盘没有空格了,而且相邻格子也没有可以合并的数字对。注意,是“而且”,不是“或者”。棋盘满了但还能合并,游戏就没结束;棋盘没满但相邻可合并,游戏也没结束。

所以先写一个“是否还有邻居相等”的检查函数:

def can_merge(board): for r in range(SIZE): for c in range(SIZE): if r + 1 < SIZE and board[r][c] == board[r + 1][c]: return True if c + 1 < SIZE and board[r][c] == board[r][c + 1]: return True return False

这里最容易写错的地方是越界。判断右边邻居时要保证c + 1 < SIZE,判断下边邻居时要保证r + 1 < SIZE。一旦漏掉这个条件,程序会在访问最后一列或最后一行的邻居时抛IndexError。很多新手反复调试才发现是这个问题。

有了can_merge,失败判断就很清晰了:

def is_game_over(board): if any(0 in row for row in board): return False return not can_merge(board)

4.4 先把完整可运行版本给你

上面分段讲的代码,拼起来就是完整版。为了避免结构分散,这里放一个可以直接运行的整包。你复制到你的.py文件里,运行就能玩。

import os import random SIZE = 4 TARGET = 2048 def create_board(): board = [[0] * SIZE for _ in range(SIZE)] for _ in range(2): add_new_tile(board) return board def add_new_tile(board): empty_cells = [(r, c) for r in range(SIZE) for c in range(SIZE) if board[r][c] == 0] if not empty_cells: return r, c = random.choice(empty_cells) board[r][c] = 2 if random.random() < 0.9 else 4 def display(board, score): os.system('cls' if os.name == 'nt' else 'clear') print(f'Score: {score}') for row in board: print('+' + '------+' * SIZE) print('|' + ''.join(f'{cell:^6}|' if cell else ' |' for cell in row)) print('+' + '------+' * SIZE) def transpose(board): return [list(row) for row in zip(*board)] def merge_row(row): tiles = [v for v in row if v != 0] score = 0 for i in range(len(tiles) - 1): if tiles[i] == tiles[i + 1]: tiles[i] *= 2 score += tiles[i] tiles[i + 1] = 0 tiles = [v for v in tiles if v != 0] tiles += [0] * (SIZE - len(tiles)) return tiles, score def move(board, direction): total_score = 0 if direction == 'left': for i, row in enumerate(board): board[i], score = merge_row(row) total_score += score elif direction == 'right': for i, row in enumerate(board): board[i], score = merge_row(row[::-1]) board[i] = board[i][::-1] total_score += score elif direction == 'up': board = transpose(board) for i, row in enumerate(board): board[i], score = merge_row(row) total_score += score board = transpose(board) elif direction == 'down': board = transpose(board) for i, row in enumerate(board): board[i], score = merge_row(row[::-1]) board[i] = board[i][::-1] total_score += score board = transpose(board) return board, total_score def is_win(board): return any(TARGET in row for row in board) def can_merge(board): for r in range(SIZE): for c in range(SIZE): if r + 1 < SIZE and board[r][c] == board[r + 1][c]: return True if c + 1 < SIZE and board[r][c] == board[r][c + 1]: return True return False def is_game_over(board): if any(0 in row for row in board): return False return not can_merge(board) def main(): board = create_board() score = 0 won = False while True: display(board, score) if is_game_over(board): print('Game Over') break if is_win(board) and not won: print('恭喜,合成了 2048!按 y 继续挑战,其他键退出') won = True if input().strip().lower() != 'y': break cmd = input('WASD 移动,Q 退出:').strip().lower() if cmd == 'q': break if cmd not in ('w', 'a', 's', 'd'): continue old_board = [row[:] for row in board] board, gained = move(board, cmd) score += gained if board != old_board: add_new_tile(board) if __name__ == '__main__': main()

这个版本没有任何第三方依赖,复制粘贴即玩。如果你只想照着我的代码跑通,看到这里就够了。但如果你想把这个小项目变成“拿得出手”的作品,下面这些进阶方向值得再看一遍。

5. 进阶打磨:从“能玩”到“好玩”

第一版能运行、能玩之后,你就有了一个绝佳的升级试验田。我建议按下面的顺序做打磨,每加一个功能,你都能体会到“代码在生长”的感觉。

5.1 给数字上个色

终端版默认的白字黑底,玩久了确实有点干。ANSI 转义序列可以给输出加颜色,而且不需要任何第三方库。基本思路是:不同的数字用不同的颜色编码,打印完成后用\033[0m重置。

COLOR_MAP = { 2: '\033[93m', 4: '\033[96m', 8: '\033[92m', 16: '\033[95m', 32: '\033[94m', 64: '\033[91m', 128: '\033[1;33m', 256: '\033[1;36m', 512: '\033[1;32m', 1024: '\033[1;35m', 2048: '\033[1;31m' }

然后在display()里,把每个数字包一层颜色代码:

cell_str = f'{cell:^6}' color = COLOR_MAP.get(cell, '') print(colored(f'|{cell_str}|', end=''))

注意 Windows 老版本 CMD 对 ANSI 支持不好,颜色可能不会显示,而是一堆乱码。现在 Windows Terminal 和较新的 PowerShell 都支持,但如果你用的是远古 CMD,这功能就别开了。做颜色之前先考虑终端环境,这是过来人的经验。

5.2 存档最高分

没有存档的游戏,成就感少一半。加一个最高分记录非常简单,用文本文件就能搞定。游戏开始时读文件,游戏结束时如果当前分数超过纪录,就写回文件。

def load_best(): try: with open('best_score.txt') as f: return int(f.read().strip()) except FileNotFoundError: return 0 def save_best(score): best = load_best() if score > best: with open('best_score.txt', 'w') as f: f.write(str(score))

这里顺带练习了 Python 文件读写的三个点:open()打开文件、with上下文自动关闭、异常处理捕获“文件不存在”。在显示界面时,把最高分跟当前分数并排打出来,游戏的竞技性马上就出来了。

5.3 还能怎么扩展

做完颜色和存档之后,如果还有精力,下面这些方向按难度递增:

  • 支持撤销。每次移动前把棋盘和分数压进一个栈,按 U 键就能回退一步。这个功能涉及“历史状态栈”,是数据结构里的经典应用。
  • 自定义棋盘大小。把SIZE改成可以让玩家输入的值,比如玩 5×5 或者 3×3。这听起来简单,但你会发现合并逻辑和 UI 对齐都要跟着调。
  • 写一个自动演示模式。让程序随机走几千步,统计平均得分。这个实验能让你直观地看到“无脑乱滑”和“策略滑动”之间的差距。
  • 制作一个 AI 自动玩。通过启发式评分(比如空位数、最大数字的位置、平滑度)来选方向。这条路已经足够你从算法小白跳到策略优化入门了。

别小看这些扩展。现实中的很多项目,核心功能可能只有几百行代码,但真正让它好用的,就是这些细致打磨和外围功能。

6. 实测踩坑记录与排查清单

这一节是我最想写的内容。下面的坑,是我带人写这个项目时真实遇到过的,几乎每条都有对应的“看起来很合理、实际上悄悄埋雷”的写法。

6.1 合并只合并一次,是这里最大的坑

很多初学者写合并逻辑时,会写出“连续合并”的效果。比如处理[4, 4, 4, 4],有人期待变成[16, 0, 0, 0],因为 4+4=8,8+8=16,一路连加下去。但 2048 的规则明确:一次滑动中,同一个格子最多参与一次合并。所以正确结果是[8, 8, 0, 0]。

这个坑的根源在于“一边合并一边继续扫描”的写法。解决思路就是我在 3.1 里写的三步法:先压缩、合并时把被合并位置置 0、再压缩。被合并的格子一旦置 0,后续扫描就不会再让它参与下一轮合并,自然就保证了“只合并一次”。如果你发现自己写出来的合并结果异常,可以先拿[2, 2, 4, 4]、[4, 4, 4, 4]、[2, 2, 2, 2]这三组数据分别测一测,能通过这三组,合并逻辑基本就稳了。

6.2 深拷贝、随机数与“无效移动”

这个坑我在 4.1 里提到过,但值得单独拿出来再强调一次。old_board = board不是复制棋盘,只是取了个别名。正确做法是old_board = [row[:] for row in board],每一行都复制一份。更深层的理解是:Python 里的切片row[:]已经生成了新列表,所以逐行切片就能完成二维列表的深拷贝。如果只是board[:],外层是新列表,内层还是共享的旧行,改一行会全盘皆动。

另一个随机数相关的小坑是:如果你在调试时总是得到同样的开局,有可能是不小心调用了random.seed()并传了固定值。正常情况下不要设置 seed,让每次运行都真正随机。如果设置了,要记得那是“可复现实验”用的,不是默认玩法。

6.3 终端兼容:清屏、输入、方向键

跨平台这个事儿,在写终端游戏时躲不开。os.system('cls' if os.name == 'nt' else 'clear')是我推荐的做法,但要知道背后的原因:Windows 的 CMD 用的是cls,Linux/macOS 的 shell 用的是clear,不存在一个通用的清屏命令。

输入方面,初学阶段老老实实用input()+ WASD 就好。有同学问能不能直接用方向键,我可以负责任地告诉你:方向键在终端里会变成类似\x1b[A的转义序列,处理起来很麻烦,而且不同终端的行为还不完全一致。为了一个回车键的差异去折腾跨平台输入监听,对新手来说是典型的“投入产出不成比例”。先用 WASD 玩起来,以后做 GUI 版本时再考虑键盘事件。

6.4 常见问题速查表

现象可能原因解决方案
按方向键没反应,出现^[[A乱码终端把方向键转成了转义序列改用 WASD 输入
数字对不齐,棋盘越来越乱打印时没有固定字符宽度用f'{cell:^6}'统一宽度
棋盘没变化,却不断生成新块用old_board = board比较改成[row[:] for row in board]
游戏还能合并,却提示 Game Over判断失败条件写错了同时检查“无空格”和“无相邻可合并”
合并结果多翻了一倍合并逻辑允许连续合并合并后将被合并位置 0,再压缩
清屏不生效,报clear不存在Windows 环境用了clear用os.name == 'nt'区分cls/clear

如果你在运行过程中碰到了表里没列到的问题,也别太急。把merge_row()单独拎出来,多喂几组数据测试,是排查合并问题最快的方法。对于这种小项目,调试的价值不在于修好一个 bug,而在于建立“把算法拆开验证”的思维习惯。

我自己带新人刷 Python 基础,前前后后试过不少项目,2048 是少数几个能让人主动加练的。别小看这个终端小游戏,你把merge_row和move弄明白,以后遇到任何数组变换、表格数据、地图寻路的问题,思路都会顺很多。摸鱼也好、消遣也罢,重点是你亲手写出的第一版能跑、能玩,就已经比那些收藏了一堆教程却从不落地的人强太多了。

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

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

立即咨询