Python入门练手项目:数字炸弹游戏开发教程
最近有新手朋友问我有没有那种“规则一听就懂、代码写起来却不无聊、练完还有成就感”的 Python 小项目。我第一个想到的就是“数字炸弹”。“数字炸弹”是个特别适合练习 Python 基础的小游戏程序在 1 到 100 之间随机选一个整数当作“炸弹”玩家每次输入一个猜测值猜中炸弹游戏结束猜错则根据猜测值与炸弹的大小关系不断缩小可猜范围。规则一句话就能讲清但实现它会碰到random随机数、while循环、input输入、if条件分支、字符串输出这些 Python 入门必学的硬知识点还会顺手踩一遍类型转换和异常处理的坑。适合刚学完语法、想写点完整代码的初学者也适合想带别人练手的同学当教学案例。这篇文章不是给你贴一段代码就完事。我会把设计思路、每个关键代码为什么要这么写、运行时会踩什么坑都拆开讲一遍最后给出完整源码。1. 为什么用“数字炸弹”来练 Python 基础1.1 游戏规则与它覆盖的知识点先把规则说严谨一点。程序先在心里藏一个 1 到 100 之间的整数这个数就是“炸弹”。玩家每轮输入一个整数程序会给出三种反馈输入值和炸弹相等炸弹爆炸玩家获胜。输入值比炸弹小提示“太小了”同时把猜测值作为新的下界。输入值比炸弹大提示“太大了”同时把猜测值作为新的上界。比如炸弹是 42玩家第一次猜 50那后续可猜的区间就从 1 到 100 缩小成 1 到 49因为 50 已经被排除了。这个不断收窄区间的过程像警察画包围圈一步步逼近目标。就这么一个简单规则映射到 Python 基础知识点上非常清晰知识点在游戏中的位置random.randint(1, 100)生成 1 到 100 之间的随机炸弹while Truebreak实现“猜不中就一直玩”的主循环input()接收玩家猜测值int()类型转换把字符串形式的输入转成能比较的整数if / elif / else判断“相等、太小、太大”三种情况print(f...)输出当前区间和提示信息try / except处理用户输入非数字导致的崩溃continue输入非法时跳过本轮重新输入这些知识点单看都不难但放在一个完整的交互流程里就逼着你真正理解程序的执行顺序。很多初学者单独做练习题没问题一写完整程序就懵是因为不懂得把分散的语法串成一条流程。数字炸弹正好给了你一个低门槛但真实完整的流程。1.2 用这个项目练手的三个理由第一个理由是“简单不等于没挑战”。几十行代码里能出问题的位置不少比如边界值要不要加 1、输入非数字会不会崩溃、死循环怎么跳出这些都是真实项目里会遇到的调试问题。做了这个项目你对“程序是按顺序一行行执行”这件事会有更深的体感。第二个理由是知识密度刚好。它覆盖的正好是“变量、类型、分支、循环、函数、异常”这一条主线但不需要碰复杂的数据结构和面向对象。对刚看完基础语法的人来说难度曲线非常友好。第三个理由是扩展空间极大。加双人轮流、加猜数计分、加文件排行榜、加图形界面甚至把玩法倒过来让程序猜你的数字都能在原有代码基础上加东西。一个新手项目能陪你从入门玩到进阶这一点很少有题目做得到。2. 动手前先想清楚设计拆解与运行流程2.1 程序运行的完整流程很多新手拿到题目就急着敲代码敲到一半发现逻辑理不清。我建议动手前先在纸上把运行流程写出来。数字炸弹的完整流程其实只有六步设定初始区间下界low为 1上界high为 100。生成一个随机整数作为炸弹secret。进入循环输出当前可猜区间。接收玩家输入并尝试转换成整数。判断输入值非法输入就重新输入超出当前区间就提示并重新输入。比较猜测值与炸弹的大小相等则结束游戏小于则更新下界大于则更新上界更新完回到第 3 步。这六步看起来简单但每一步之间是有依赖关系的。比如第 4 步必须放在第 5 步之前因为不转换类型就没法比较大小第 6 步更新完边界之后必须回到第 3 步重新输出区间否则玩家不知道范围变了。我用文字把这个流程描述得非常啰嗦但代码写出来其实很紧凑。关键在于你要明白这个循环不是固定循环几次而是“直到满足条件才结束”所以用while True配合break比用for合适得多。2.2 核心状态变量设计与为什么不用复杂结构整个游戏只需要四个状态变量low、high、secret、attempts。low和high保存当前可猜区间secret保存炸弹值attempts记录有效猜测次数。为什么不用数组或字典来存因为这个游戏的逻辑很简单四个变量就能完整描述所有状态。硬塞一个列表出来反而增加理解负担。初学者应该学会“用最少的结构表达清楚逻辑”而不是为了显得高级而滥用数据结构。low和high的初始值我建议写成常量MIN_NUM 1 MAX_NUM 100这样做的好处是以后想改成 1 到 1000 的难度只需要改这两个常量不用满代码找数字。这也是一个非常重要的代码习惯把魔法数字抽出来起个名字。attempts这个变量容易被忽略但它是整个游戏体验的关键。没有它玩家不知道自己猜了多少次也就没有“挑战纪录”的乐趣。加一个计数器只需要三处改动初始化时归零、每次有效猜测后加 1、猜中后把结果打印出来。3. 核心代码实现从骨架到能跑的版本3.1 先搭一个能跑起来的主循环逻辑理清之后代码就可以一层层往里填。先写主循环骨架import random MIN_NUM 1 MAX_NUM 100 secret random.randint(MIN_NUM, MAX_NUM) low MIN_NUM high MAX_NUM attempts 0 while True: # 这里写输入、判断、更新边界的逻辑 break这个骨架最大的价值是让你先确认“游戏一定会结束”。while True是故意写成死循环的依靠内部的break在猜中时退出。这样的写法比给 while 加一个复杂条件更直观因为它把退出的条件集中在“猜中”这一件事上。我见过很多初学者在这里犯浑想在 while 条件里写while low high:结果发现猜中之后还要额外处理变量逻辑绕来绕去。我的建议是主循环用while True退出条件单独写清楚代码读起来就像讲故事易读很多。3.2 边界收窄逻辑这个项目最关键的细节接下来是游戏的核心——边界更新。完整逻辑是这样的try: guess int(input(请输入你猜的数字: )) except ValueError: print(输入无效请输入一个整数。) continue if guess low or guess high: print(f数字必须在 {low} 到 {high} 之间。) continue attempts 1 if guess secret: print(fBOOM! 你猜中了炸弹就是 {secret}共用 {attempts} 次。) break elif guess secret: low guess 1 print(太小了炸弹不在这里。) else: high guess - 1 print(太大了炸弹不在这里。)这段代码里最容易被问“为什么”的一句是low guess 1。假设玩家输入 30程序提示“太小了”说明 30 不是炸弹那么 30 这个数字已经确认排雷下一轮可猜范围应该从 31 开始。如果写成low guess玩家就能反复猜同一个 30虽然程序不会出错但游戏体验会变得很蠢。同理猜大了之后更新high guess - 1。这个“排除已经猜过的数字”的思路是整个游戏的灵魂同时也是二分查找算法的雏形。你现在是在写游戏其实已经提前接触了一个重要的算法思想。为了让你看清整个对局过程假设这次炸弹值固定是 42运行效果如下轮次玩家输入程序提示更新后的区间150太大了[1, 49]230太小了[31, 49]345太大了[31, 44]442BOOM猜中游戏结束这个演示表方便你理解更新规则但真实运行时炸弹是随机生成的你不可能提前知道。测试的时候我常使用一个技巧临时把secret random.randint(MIN_NUM, MAX_NUM)改成secret 42把整个流程跑熟之后再改回随机数。3.3 加“再来一局”功能从一次性脚本变成完整程序只有一局的游戏略显单薄玩家总有“再来一次”的冲动。这时候引入函数把单局逻辑封装起来代码会更清晰import random MIN_NUM 1 MAX_NUM 100 def play_once(): secret random.randint(MIN_NUM, MAX_NUM) low MIN_NUM high MAX_NUM attempts 0 print(f炸弹已经埋好范围是 [{low}, {high}]。) while True: print(f当前区间: [{low}, {high}]) try: guess int(input(请输入你猜的数字: )) except ValueError: print(输入无效请输入一个整数。) continue if guess low or guess high: print(f数字必须在 {low} 到 {high} 之间。) continue attempts 1 if guess secret: print(fBOOM! 你猜中了炸弹就是 {secret}共用 {attempts} 次。) return attempts elif guess secret: low guess 1 print(太小了炸弹不在这里。) else: high guess - 1 print(太大了炸弹不在这里。)把单局逻辑放进play_once函数后主程序就变得非常清爽def main(): print( 数字炸弹 ) best None while True: count play_once() if best is None or count best: best count print(f刷新纪录当前最少尝试次数: {best}) else: print(f当前最少尝试次数: {best}) again input(再来一局(y/n): ).strip().lower() if again ! y: print(谢谢你玩数字炸弹再见) break if __name__ __main__: main()这段代码中best变量用于保存历史最少猜测次数相当于一个简单但完整的功能扩展。again ! y的判断给了玩家退出游戏的主动权也让主循环有了清晰出口。这里有一个细节值得注意input(再来一局(y/n): ).strip().lower()这行同时处理了玩家输入两边空格和大小写的情况。玩家输入Y或y都能正确识别。这种对用户输入“宽容处理”的意识是好的程序员和合格程序员的区别。4. 完整源码与进阶扩展4.1 基础版完整源码文末源码直接复制即用标题里说的文末源码其实就是下面这份完整代码。我把它整理成一个可以直接运行的 Python 文件保存成number_bomb.py然后运行python number_bomb.py就可以开玩。import random MIN_NUM 1 MAX_NUM 100 def play_once(): secret random.randint(MIN_NUM, MAX_NUM) low MIN_NUM high MAX_NUM attempts 0 print(f炸弹已经埋好范围是 [{low}, {high}]。) while True: print(f当前区间: [{low}, {high}]) try: guess int(input(请输入你猜的数字: )) except ValueError: print(输入无效请输入一个整数。) continue if guess low or guess high: print(f数字必须在 {low} 到 {high} 之间。) continue attempts 1 if guess secret: print(fBOOM! 你猜中了炸弹就是 {secret}共用 {attempts} 次。) return attempts elif guess secret: low guess 1 print(太小了炸弹不在这里。) else: high guess - 1 print(太大了炸弹不在这里。) def main(): print( 数字炸弹 ) best None while True: count play_once() if best is None or count best: best count print(f刷新纪录当前最少尝试次数: {best}) else: print(f当前最少尝试次数: {best}) again input(再来一局(y/n): ).strip().lower() if again ! y: print(谢谢你玩数字炸弹再见) break if __name__ __main__: main()完整代码的亮点是输入非数字不会崩溃、越界输入会拦截、尝试次数会统计、最好成绩会保留、支持连续多局。这些功能加在一起体验已经超过“练习作业”的级别完全可以拿给朋友玩。4.2 把它变成更有挑战的版本基础版跑通之后下面这几个扩展方向是我非常推荐的。如果你想练循环里的状态切换可以改成双人轮流猜。只需要增加一个玩家列表和一个turn指针players [玩家甲, 玩家乙] turn 0 while True: print(f轮到 {players[turn]} 猜。) # 其他逻辑不变 turn (turn 1) % 2(turn 1) % 2是一个经典的轮换写法两个人轮流三个人也可以用% 3。这个知识点以后写队列、写任务调度的时候还会遇到现在先混个脸熟。如果你想练算法可以玩“反向数字炸弹”。由玩家心里想一个 1 到 100 的数字程序通过二分法来猜玩家反馈“大了”或“小了”。这个版本逼着程序自动更新区间比你手动输入更有意思也更考验你的二分查找理解。如果你想练文件读写可以加一个排行榜。把每次玩的玩家名字和尝试次数写进一个文本文件下次启动时读取并显示历史前三名。这就把open()、read()、write()全串起来了。5. 常见报错与排查技巧实录5.1 猜完数字范围却没变化这是新手最容易犯的逻辑错误错误代码通常长这样elif guess secret: low guess print(太小了)表面上看没问题但玩家连续输入同一个数字时区间不会排除已经验证过的值。比如炸弹是 50玩家猜 30提示太小区间变成[30, 100]。再猜 30依然提示太小范围保持不变玩家会陷入死循环。正确的做法是low guess 1把已经排雷的数字彻底排除。写的时候多问自己一句这个数字还需要再猜一次吗如果答案是否定的那就应该在更新边界时把它剔除。5.2 输入字母直接报错崩溃经典报错信息是ValueError: invalid literal for int() with base 10: abc原因很简单input()返回的是字符串直接int(abc)一定会抛异常。很多初学者第一次跑程序兴冲冲输入一个字母程序直接崩掉非常打击信心。异常处理要放在int()外面而且要放在一个循环内配合continue使用try: guess int(input(请输入你猜的数字: )) except ValueError: print(输入无效请输入一个整数。) continue注意这里有个细节continue会让程序回到循环开头重新展示当前区间并让玩家再次输入。如果少了这个continue代码会继续往下走guess变量没有被赋值紧接着就会报NameError。这种“一个错误引发另一个错误”的情况在调试中非常常见要学会从 traceback 最后一行往上找根因。5.3 随机数范围搞混random.randint和random.randrange的边界规则经常让新手晕头转向。我直接列个表写法可能的取值是否包含 100random.randint(1, 100)1 到 100包含random.randrange(1, 100)1 到 99不包含random.randrange(1, 101)1 到 100包含random.choice(range(1, 101))1 到 100包含想表示 1 到 100 这个区间最直观的是random.randint(1, 100)。如果你的炸弹范围改成了 1 到 1000那就要同步改成random.randint(1, 1000)否则炸弹永远不可能超过 100游戏规则就名不副实了。5.4 死循环怎么排查while True最怕没有触发break。常见原因有三个一是把break写到了if guess secret外面导致猜中了也不会退出二是continue被放在了某些不应该跳过的代码之前把attempts 1给跳过了当然这只影响计数不影响退出三是边界更新写错了区间永远包含炸弹但玩家又一直猜不中。排查死循环我用得最多的方法是“打印法”在循环开头临时加一行调试输出while True: # 调试专用跑通后删除 print(DEBUG: secret , secret, low , low, high , high)这样程序跑起来后你能在控制台里看到每一轮循环时的状态马上定位是边界没变、还是输入判断卡住了。跑通之后记得把这行删掉不然玩家就能看到炸弹位置游戏就没意思了。5.5 还有一个容易忽略的坑输入空字符串有同学按回车没输入数字int()也会抛ValueError。虽然异常处理能挡住但提示信息写的是“请输入一个整数”对于空输入来说不够准确。如果想要更好的体验可以先用.strip()去掉首尾空白再判断是否为空raw input(请输入你猜的数字: ).strip() if not raw: print(输入不能为空。) continue try: guess int(raw) except ValueError: print(输入无效请输入一个整数。) continue这个细节在基础练习阶段不用太较真但实际项目里用户什么都有可能输入养成“先校验再转换”的习惯能帮你避开很多后期问题。6. 个人经验与最后的提醒6.1 我给初学者的三条建议第一条写代码之前先画流程图。数字炸弹的流程不复杂但你能在纸上把“输入、判断、更新、回到循环”之间的关系画清楚后面写代码会顺畅很多。画错了也没关系改纸比改代码省事得多。第二条不要全文复制代码。这是我的真心话。你看了文章吸收了思路然后关闭文章自己从头敲一遍。遇到不会的地方回来查。这个“回忆-失败-查证-修正”的过程才是真正把知识内化的过程。直接 CtrlC 复制十分钟后就忘了。第三条改一个数字再运行一次。把范围从 1 到 100 改成 1 到 1000看看代码哪些地方要改再改成 1 到 10000感受一下二分区间缩小的速度。这些“微实验”能让你对一个几十行的小程序有更细的理解。6.2 后续还能怎么玩数字炸弹这个项目是我带人练 Python 时最喜欢用的开场项目之一。它足够小小到能在半小时内跑通又足够大大到能延伸出异常处理、函数封装、循环控制、算法思想、文件读写、双人对战这些进阶话题。我见过不少朋友做完基础版之后自己主动加了计时功能、加了猜测次数排行、加了 AI 自动猜数。同样的规则不同的人能玩出完全不同的花样。这正是练手项目最有价值的地方它是一个起点不是终点。最后再分享一个我自己的习惯每写一个小项目我都会在main()函数里留一个if __name__ __main__:入口。现在你可能觉得这只是个形式但之后把代码拆成多模块时这个“入口保护”能让你的函数被导入时不被立刻执行。很多教材不讲这个但它每个真实项目都会用到。代码这种东西看十遍不如亲手调一次。你现在就可以把上面这份源码跑起来故意输入一个字母故意猜一个区间外的数字故意把guess 1改成guess看看会发生什么。每一个“意外”都是你下次不再踩坑的资本。