用Python写集五福小游戏:随机抽取、集合去重与存档全解析

发布时间:2026/10/12 23:48:49
用Python写集五福小游戏:随机抽取、集合去重与存档全解析 第一次在朋友圈看到别人晒集五福的进度我第一反应不是去到处扫卡而是琢磨另一个问题这个东西我能不能自己写一个后来花了一个晚上的时间用 Python 打出了一个能跑的“集五福”小游戏。也就是这次尝试让我对随机逻辑、数据去重、文件持久化这些基础概念有了特别直观的理解。今天的博文不是介绍什么高深框架而是完整拆解这个小项目的设计思路、核心代码和调试过程。你不需要有很强的编程基础只要学过 Python 语法基础比如list、dict、while循环、random库就可以跟着做出来。做完之后你会有一种很扎实的获得感花里胡哨的网页活动自己动手也能复刻个七七八八。1. 项目整体设计与思路拆解1.1 集五福的玩法本质是什么先别急着写代码我们把规则抽象一下。线上集福活动的核心规则特别简单有一批卡片系统每次从卡池里随机发一张给你你收集到五张不同的卡就算达成目标。听起来容易难点在于“随机”这两个字。如果一张卡总是重复出现你抽了几十次还缺一张体验就会变得非常急躁。这个规则映射到程序里其实就三个核心要素一个卡池包含五种福卡的全部类型。一个抽取动作每次从卡池中随机取出一张。一个收集容器保存你已经拥有的卡片并且自动忽略重复项。这三个要素分别对应 Python 里的列表、随机函数和集合。搞清楚这一点代码结构就清晰了。很多人写这种游戏先从界面开始这是反的应该先写逻辑把核心规则跑通再加菜单和界面。版本 1 不需要任何 GUI命令行交互足够用。1.2 为什么用 Python 做这个项目Python 作为入门语言写这种小游戏最大的优势是开发速度快。从新建文件到跑通第一个抽卡动作通常不会超过二十分钟。你不需要配置环境变量也不需要引入重型依赖一个标准库里的random模块就能完成最关键的功能。此外Python 数据结构表达能力很强。五种卡片用字典去存每种卡片带权值、名称、说明后面想调概率、加新卡改动成本低。这种“先跑通再优化”的开发节奏很适合业余时间做小项目。我见过很多人一上来就想用游戏引擎最后被对象树和场景管理劝退相反从命令行小游戏开始每一步都能看到成果动力反而更足。1.3 这一版要完成哪些功能这一版的功能目标我定义为“最小可用集卡循环”程序启动后显示当前拥有哪些福卡。用户输入指令抽卡模拟一次随机抽取。抽到新卡自动加入收藏抽到已有卡片则提示重复。集齐五种卡片时提示完成并询问是否重新开始。支持退出和查看背包两种基础操作。不必要的功能一概不做。比如积分系统、好友赠送、浇花、扫一扫识别之类全都是后话。先把这个循环跑通后续再加什么都容易。2. 核心数据设计五张卡怎么存最舒服2.1 福卡的语义设计既然叫“集五福”卡片就得有五张而且得有个好听的名字。我在本地项目里用的是健康福、平安福、好运福、团圆福、财富福。每张卡我还加了一句祝福语展示的时候不至于太干。这个设计是模仿线上活动的包装但实现起来很简单就是一个带嵌套结构的字典列表。FA_CARD_POOL [ {name: 健康福, message: 身体倍儿棒吃饭倍儿香}, {name: 平安福, message: 出入平安顺顺当当}, {name: 好运福, message: 锦鲤附体好运连连}, {name: 团圆福, message: 家庭美满围炉夜话}, {name: 财富福, message: 财源广进钱包鼓鼓}, ]这里有个很容易纠结的点很多人会用list直接写死这五种卡这没问题但如果你想以后做概率加权比如某张卡更稀有那你就需要写成字典结构。提前用字典后面改成加权抽取的时候只需要加一个weight字段而不用重写整个卡池。提示给数据加一层结构化包装永远比直接写裸变量更值得。刚开始可能看不到好处一旦项目从五张卡变成五十张卡差距就是修改十行和修改二百行的区别。2.2 用集合还是列表来存背包这里是一个新手非常容易踩坑的地方。你要记录“已经拥有的卡片”那么请你先想一想玩这个游戏同一种卡片拥有多张是否有意义在集福规则里一张健康福和十张健康福是等价的系统只关心“你有没有”不关心“你有几张”。所以正确做法是用集合set()存已有卡片的名称集合。为什么不用列表因为列表会存重复项每次你都要自己判断if card in my_cards判断本身倒是不费力但代码会变得啰嗦。集合自带去重add的时候如果元素已经存在集合不会重复添加而且in判断的执行效率也比列表高。虽然这里五张卡量级没有任何性能差别但养成用正确数据结构的习惯比代码本身更重要。# 推荐 collected set() collected.add(健康福) collected.add(健康福) # 不会重复 # 也可以但冗余 collected_list [] if 健康福 not in collected_list: collected_list.append(健康福)2.3 抽卡概率的两种玩法最简单的方式是用random.choice(pool)从五张卡里等概率选择一个。这适合版本 1 快速实现。但你玩过线上活动就知道不同福卡的稀有度完全不一样往往某一两张特别难抽。要想实现“某些卡更难出”就得引入概率权重设计。核心思路是先构造一张带权重的表计算总权重生成一个0到总权重之间的随机数然后逐项累积确定随机数落在哪一段区间。import random def draw_card(weighted_pool): total_weight sum(card[weight] for card in weighted_pool) rand_val random.uniform(0, total_weight) cumulative 0 for card in weighted_pool: cumulative card[weight] if rand_val cumulative: return card我当时版本 1 先用了random.choice因为结构简单、更容易调试。等你把完整流程跑通再把这段加权逻辑替换进去你会发现抽卡体验立刻有了层次感。建议读者也这样走先跑通再调优。3. 完整代码实现与逐步讲解3.1 代码目录与运行环境先准备环境。我这边用的是 Python 3.10其实 3.8 以上都行。不需要pip install任何第三方库只需要标准库random、os和sys。这也是这个项目适合初学者的重要原因零依赖一个.py文件搞定全部功能。项目结构非常简单collect_wufu/ └── wufu_game.py所有代码写在一个文件里总共一百多行。对于这个量级的练习项目单文件是合理的。如果以后要加 GUI、加数据库再拆分成多个模块不迟。3.2 先搭建卡池函数模块我们先把基础数据结构和几个独立的小函数写出来。独立函数方便单独测试。这里我写了一个显示当前背包的函数一个抽取卡片的函数一个判断是否集齐的函数。import random import os FA_CARD_POOL [ {name: 健康福, message: 身体倍儿棒吃饭倍儿香}, {name: 平安福, message: 出入平安顺顺当当}, {name: 好运福, message: 锦鲤附体好运连连}, {name: 团圆福, message: 家庭美满围炉夜话}, {name: 财富福, message: 财源广进钱包鼓鼓}, ] collected set() total_draws 0 def draw_one(): 从卡池随机抽一张卡 return random.choice(FA_CARD_POOL) def add_card(card): 把抽到的卡加入背包返回是否为新卡 global total_draws total_draws 1 name card[name] if name in collected: return False collected.add(name) return True def is_finished(): return len(collected) len(FA_CARD_POOL) def show_status(): os.system(cls if os.name nt else clear) print( 集五福 · 本地版 ) print(已抽次数, total_draws) print(当前拥有{}/{}.format(len(collected), len(FA_CARD_POOL))) for card in FA_CARD_POOL: mark ✔ if card[name] in collected else ✘ print(f[{mark}] {card[name]}: {card[message]}) print()如果你是在 Windows 系统上运行os.system(cls)会清屏macOS 或 Linux 上则用clear。这里用os.name做了系统判断在跨平台分享代码时能省去别人手动改代码的麻烦。注意show_status()函数内部使用了全局变量total_draws读取全局变量不需要声明global只有修改的时候才需要。类如add_card内部对total_draws做自增运算所以必须写global total_draws。这个细节初学者特别容易漏漏了就会看到UnboundLocalError: local variable total_draws referenced before assignment。3.3 主循环与用户交互主程序应该是整个项目的骨架。我设计成最简单的文本菜单循环输入1抽卡输入2查看背包输入3重置游戏输入4退出。def main(): while True: show_status() if is_finished(): print(恭喜你集齐了五福) print(按 3 重置游戏按 4 退出) print(请选择操作) print(1. 抽一张卡) print(2. 查看背包) print(3. 重置游戏) print(4. 退出) choice input( ).strip() if choice 1: card draw_one() is_new add_card(card) if is_new: print(f\n抽到{card[name]}{card[message]}) else: print(f\n抽到重复的 {card[name]}差一点点运气……) input(按回车继续...) elif choice 2: show_backpack() input(按回车继续...) elif choice 3: reset_game() print(背包已重置) input(按回车继续...) elif choice 4: print(下次再战祝你好运) break else: print(无效输入请重新选择) input(按回车继续...)主循环的写法其实没有什么技巧含量但它把整个游戏串起来了。很多人写代码容易陷入“逻辑完美主义”觉得主函数应该很优雅、很精炼。但实际上游戏项目的主循环本质上就是一个状态机不同指令对应不同动作状态用collected集合表达。只要状态清楚、动作清楚主循环写成这样反而最好理解。3.4 辅助函数补齐刚才主循环里出现了show_backpack和reset_game两个还没定义的函数现在补齐。show_backpack只展示集合里的元素reset_game负责把背包清空、抽卡次数归零。这里注意既然我们用全局变量存状态重置函数就必须同时重新赋值这两个变量。def show_backpack(): if not collected: print(背包空空如也快去抽卡吧~) return print(当前背包) for name in sorted(collected): print( ·, name) def reset_game(): 重置游戏状态 global collected, total_draws collected set() total_draws 0为什么要用global因为collected set()这个语句是在函数内部给变量重新绑定了新对象Python 会认为它是一个局部变量。如果不加global声明程序运行到这句就会报错。而在入口处collected已经定义你表面上看起来是在“修改”但解释器不这么理解它看到就认为是局部变量。这是 Python 作用域规则里最经典的一个陷阱。3.5 完整代码的结构拼接把所有代码拼到一起这个版本就能跑了。我当时跑通第一个“集齐五福”的时候顺手截了个图发到群里朋友说这么简单的东西也值得发。但说实话做这类项目的成就感不在于它的技术含量而在于你亲手把一套规则变成了可交互的程序。这里再补一个让代码更规范的细节可以加上if __name__ __main__的入口判断。这样当你以后把函数模块给别人复用时不会因为模块被导入而自动执行主循环。虽然当前项目是单文件但这个习惯从第一天开始养成一点也不亏。if __name__ __main__: main()4. 图形界面版本与体验升级4.1 为什么从命令行升级到窗口版命令行的版本跑通以后你会立即发现一个痛点视觉效果太朴素。抽到一个新卡只是一行文字没有卡片样式没有颜色没有那种“开卡”的兴奋感。这倒不是必须解决的需求但如果你想把它分享给朋友命令行版本说服力不够。我用 Python 自带的tkinter做了一个图形界面版本。这个版本最大意义在于你不需要额外安装任何 GUI 库Python 标准库自带改造成本很低。当然功能上它依然只是“展示卡片”和“按钮抽卡”没有复杂的动画但对于一个本地练习项目已经足够。4.2 做一个最小可用的 tkinter 界面下面是简化后的核心代码。思路很简单初始化一个窗口左侧放一个标签用来展示卡片文字右侧放按钮点击按钮调用抽卡逻辑更新标签内容。import tkinter as tk import random POOL [健康福, 平安福, 好运福, 团圆福, 财富福] root tk.Tk() root.title(集五福 · 桌面版) root.geometry(420x300) card_label tk.Label(root, text点击下方按钮抽卡, font(微软雅黑, 20)) card_label.pack(pady40) result_label tk.Label(root, text, font(微软雅黑, 12)) result_label.pack() def draw_once(): card random.choice(POOL) card_label.config(textcard) result_label.config(text抽到了 card) btn tk.Button(root, text抽一张, commanddraw_once, width15, height2) btn.pack(pady20) root.mainloop()这个版本大概只有四十行但它已经是一个完整的 GUI 应用。很多初学者被“图形界面”四个字吓到实际上 tkinter 的标签、按钮、布局就是几个组件的组合根本没有那么复杂。你唯一需要记住的是mainloop()会让窗口保持响应状态只处理事件不用手动写循环。4.3 给图形界面添加收藏统计光抽卡没有收藏进度游戏体验依然不够完整。可以加一句状态提示在每次抽卡后更新显示“已拥有 3/5”。我习惯把这种状态逻辑单独写一个函数界面只负责调用不要和抽卡逻辑混在一起。collected set() def draw_once_action(): card random.choice(POOL) collected.add(card) card_label.config(textcard) status_label.config(textf当前进度{len(collected)} / {len(POOL)}) if len(collected) len(POOL): status_label.config(text集齐五福程序即将关闭) btn.config(statedisabled) root.after(1500, root.destroy)这里出现了一个很实用的功能root.after(1500, root.destroy)意思是 1500 毫秒后自动执行销毁窗口。这比用time.sleep()处理等待要优雅得多因为sleep会卡住整个界面事件循环按钮和标签都会变成“假死”状态。GUI 编程的时间等待优先用after这是新手最容易踩的坑。4.4 界面资源和压缩打包图形版本做好之后如果想让朋友直接运行最好打成可执行文件。我常用的工具是PyInstaller一条命令就能把包含依赖的程序打包成独立的.exe文件。命令如下pip install pyinstaller pyinstaller -F -w wufu_gui.py解释一下参数含义-F打包成单个可执行文件-w表示运行时不会弹出控制台黑框。如果你需要自定义程序图标可以加-i icon.ico。不过要特别注意打包出来的文件大小可能超出预期因为 Python 解释器本身要一起被塞进去。这属正常现象不用纠结。提示打包工具不会帮你处理资源文件路径。如果你在代码里读取了本地图片最好用sys._MEIPASS机制处理好路径否则打包出的程序在别的机器上会报找不到文件。单文件打包的典型坑第一次打的人几乎都会遇到。5. 常见问题与调试心得5.1 为什么抽了一百次还集不齐五福这是一个概率问题也是版本 1 最容易暴露的问题。如果你用random.choice等概率抽取理论上五张卡各有 20% 的概率但实际运行中经常会出现某张卡长时间不出。这并不代表代码有 bug而是随机数本身的波动特性。把需求再拔高一点线下活动的“集齐难度”通常是故意调过的。如果我希望某张卡概率低到 5%其余四张平分剩余概率就必须用前面提到的加权抽取方式。改法很容易核心是体会到“游戏体验本质上靠概率设计而不是靠随机本身”。5.2 函数里修改全局变量报错怎么办这个坑上文提过再展开一点。当你写出这段代码collected set() def add_card(card_name): collected.add(card_name)它是能正常运行的因为collected.add是调用对象方法并没有重新绑定变量。但一旦你写成def reset_game(): collected set()Python 就会把它当局部变量。遇到这种问题我的排查建议是先看函数内部有没有给这个变量赋值的等于号有的话基本就是作用域问题。要么在最前面加global collected要么把函数改成返回值的形式避免使用全局变量。对初学者我更推荐后者因为显式传参和返回会让数据流向更清楚。5.3 程序一运行就闪退如果双击.py文件后窗口一闪而过最常见的原因是程序执行到了异常但没有被看到运行直接退出了。解决方案有两种。第一种在终端中手动运行文件异常信息会停留在终端里方便排查。第二种在主循环外面套一层try/except捕获异常后打印到窗口并暂停if __name__ __main__: try: main() except Exception as e: print(程序运行出错, e) input(按回车退出...)这个写法不解决 bug但能让你看清 bug。对初学者来说让错误可见比修复错误本身更重要。5.4 输入内容没反应或者乱码版本 1 的用户输入是我用input()实现的。如果用户输入的是数字但带空格比如 1 那choice 1判断就不成立程序会一直提示无效输入。解决办法是取输入后立刻执行.strip()去除两端空白这是我上面代码里已经用到的小细节。另外还有一个中文字体显示问题主要在 Windows 控制台中容易遇到中文显示成方块或者乱码。一般是因为系统编码不是 UTF-8。你可以在文件第一行加编码声明但对于 Python 3 来说更稳妥的方式是确保源码文件用 UTF-8 保存并在控制台里执行chcp 65001切换代码页。这类环境问题排查通常很烦人但和代码逻辑无关别钻牛角尖。5.5 随机性测试不能“只跑一次”很多初学者验证抽卡概率时只运行一次程序看到结果和预期不符就开始怀疑代码。正确的做法是做一个蒙特卡洛模拟循环执行一万次抽卡统计每种卡的出现频率看看是否接近设置的概率。这个思路在游戏开发后期特别重要因为线上活动需要保持“奖池概率”的透明度和稳定性。from collections import Counter sim_results Counter(random.choice(POOL) for _ in range(10000)) for k, v in sim_results.items(): print(k, v / 10000)运行大幅抽样以后你才会真正理解随机不等于平均也会对概率设计产生更实际的感觉。这比背再多概率公式都管用。6. 玩法扩展与长期优化方向6.1 每日任务与抽卡次数限制真实集福活动不会让你无限抽卡否则一天就集齐了。版本 2 可以加入一个简单的次数限制每天只能抽三次抽完就提示“明天再来”。实现方式是用datetime记录上一次重置抽卡次数的日期当日期变化时把剩余次数恢复。这样游戏的耐玩度会明显提升也让你第一次体验到“限制资源”对游戏节奏的影响。6.2 数据持久化重启游戏不丢进度读者如果玩过几次一定会发现当前版本重启后背包就清空了。保存进度是绕不开的需求。最简单的方案是存成 JSON 文件。每个数据项都是字符串或数字天然适合 JSON 序列化。import json def save_game(): data { collected: list(collected), total_draws: total_draws, } with open(save.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) def load_game(): global collected, total_draws try: with open(save.json, r, encodingutf-8) as f: data json.load(f) collected set(data.get(collected, [])) total_draws data.get(total_draws, 0) except FileNotFoundError: pass这里有个容易忽略的细节JSON 只能保存基本类型set不能直接序列化所以保存时转成列表读取时再转回集合。每次进主循环前自动加载每次抽卡后自动保存玩家的本地存档也就有了基础模型。再往前一步你还可以做卡片收藏记录、统计最稀有的卡是多少抽才出这些都会让项目越来越有料。6.3 添加好友互赠和“沾福气”如果你连朋友之间的玩法也想模拟纯本地版可以做一个最简单的“赠送模拟”复制一份背包状态文件给朋友朋友导入后如果拥有你没有的卡可以选择一张赠与你。考虑到安全性和复杂度本地版不必做得太认真毕竟真正的线上活动需要服务端和数据库来保证数据不被篡改。但作为理解“协作式收集”玩法的动手实验它很有价值。6.4 用日志系统记录抽卡历史最后建议加一个简单的抽卡日志。每次抽卡后把结果、时间戳写入一个log.txt。别小看这个功能它就是以后做数据分析和概率验证的基础。如果你以后想对比不同概率方案对“平均几天集齐”的影响日志是不可或缺的证据。import datetime def write_log(card_name, is_new): timestamp datetime.datetime.now().strftime(%Y-%m-%d %H:%M:%S) with open(log.txt, a, encodingutf-8) as f: f.write(f[{timestamp}] {card_name} new{is_new}\n)把日志写进游戏以后你会发现“调试游戏”一下子变成了“分析游戏数据”。这种视角的转变比任何教程都更能催生你做项目的兴趣。我个人做了几轮之后最深的体会不是“代码有多么优雅”而是把一套规则尤其是随机规则准确翻译成程序逻辑这件事本身需要持续的迭代和验证。我从最开始等概率抽取到后来调整权重再到加入平滑次数限制机制每一步改动的底层逻辑都是围绕着玩家体验展开的。版本 1 只是迈出去的第一小步但正是这一步让我意识到看似复杂的线上玩法剥掉包装和商业系统以后核心逻辑往往就是几个数据结构和三五个函数的事。建议感兴趣的朋友今晚就打开编辑器哪怕只写一个抽卡函数也比刷满一百条教程强得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询