2025-2026学年 Python程序设计 实验四 20243405付鸿睿 「终末」抽卡模拟器
课程:《Python程序设计》
班级: python全院公选课
姓名: 付鸿睿
学号:20243405
实验教师:王志强
实验日期:2026年6月15日
必修/选修: 公选课
一、项目背景
当今社会,随着“全面深化改革”“深度对外开放”方针的精准落实,人民生活水平不断提高,经济基础日益扎实。在此之上,自全面建成小康社会以来,舶来的多元文化渐渐风靡于青年群体中,广大青年的消遣方式逐渐变得开放、多元,呈现出“泛娱乐化”的趋势,上层建筑已悄然发生改变——为迎合此类趋势,多款国产二次元养成类游戏(以下简称为“二游”)如雨后春笋般破土而出。自2023年8月以来,二游的热度与流水持续升温,维持在市场前十的高位,社区中更是因其优秀的剧情/音乐/技术革新屡屡诞生热点,不少二游都曾受到过“官媒”的“好评”。截至目前,作为市场“主流”之一,二游已拥有庞大的玩家群体与可观的日常活跃度。
在流水“爆款”的同时,市面上诸多二游“白璧微瑕”,存在着影响游玩体验的致命缺点——贵。由于大部分二游存在的“米池”抽卡机制,致使其抽取一名角色所花费的资源较高,而游戏中日常可获取的较少。为抽取一名心仪的角色,玩家动辄花费成百上千元进行充值。这一缺陷,也因此成为当今二游圈内一头顽劣的“灰犀牛”
为保障玩家轻松廉价的抽卡体验,在避免花费大量钱财的前提下同步享受到抽卡的刺激与获得角色的喜悦,本「终末」抽卡模拟器应运而生。集成《崩坏·星穹铁道(Honkai·Star Railway)》、《鸣潮(Wuthering Waves)》与《明日方舟·终末地(Arclights·Endfield)》三款现象级二游,为玩家带来免费充实的抽卡体验。
二、项目简介
为在免费的前提下为玩家提供充实的抽卡体验,本项目设计了以下功能
1.抽卡
经典抽卡界面,分为角色池与武器池
2.查看已有角色
角色仓库,可以查询已获得的角色及其星魂
3.查看已有武器
武器仓库,可以查询已获得的武器及其数量
4.充值
象征性的充值窗口,作为玩家“免费”获取抽卡资源的渠道。
5.历史记录
可以分别查询角色池与武器池的抽卡记录
6.意见反馈
与游戏的开发者(也就是我)联络的渠道,反馈游玩体验
7.重置游戏
将游戏的各项数据还原到初始状态
8.退出游戏
顾名思义
三、游玩指南
相信玩二游的小伙伴们已经迫不及待了,而没玩过二游的估计不能完全听懂我在说什么,接下来简单普及一下
1、初始有拥有100000星琼,角色/武器每抽均花费160星琼,十连则花费1600星琼
2、角色池可以获得:五星角色,四星角色或武器,三星武器。每抽获取五星角色的概率为0.6%【90抽为一保底(即90抽内至少获得一个五星角色),一保底中获取限定五星角色和常驻五星角色的概率各占50%,当一次保底获取的是常驻角色,则下一次保底必定获取限定角色】。每抽获得四星角色和武器的概率均为2.55%【十抽为一保底,(即十抽内至少获得一个四星角色或武器)】
3、武器池可以获得:五星武器,四星角色或武器,三星武器。每抽获取五星武器的概率为0.8%(80抽为一保底,80抽内至少获得一个五星武器,一保底中获取限定五星武器的概率为75%,获取常驻五星武器的概率各占25%,当一次保底获取的是常驻武器,则下一次保底必定获取限定武器)。每抽获得四星角色和武器的概率均为3.3%(十抽为一保底,至少获得一个四星角色或武器)
4.第二次至第七次获取相同角色时(无论4星还是5星,每次将转化为对应角色的星魂*1,(如抽卡示例5、6),第七次获取后,将该角色从卡池中移除,不会再获取该角色。如所有角色都被移除,则进入角色池时会提示角色已抽满并退出
5.一次十连抽中,武器和角色都是可以重复获得(一次性获得不止一个)的
6.限定5星角色:希儿、景元、银狼、罗刹、刃、卡芙卡、丹恒饮月、符玄、镜流、托帕、藿藿、银枝、阮梅、真理医生、黑天鹅、花火、黄泉、砂金、知更鸟、波提欧、流萤、翡翠、云璃、椒丘、刻律德菈、长夜月、丹恒腾荒、昔涟、大丽花、爻光、火花、不死途、银狼Lv999、绯英、千冶刃、忌炎、吟霖、今汐、长离、折枝、奥古斯塔、尤诺、嘉贝丽娜、仇远、千咲、琳奈、莫宁、爱弥斯、陆赫斯、西格莉卡、绯雪、达尼娅、露西、丽贝卡、洛瑟菈、莱万汀、洁尔佩塔、伊冯、汤汤、洛茜、庄方宜、弭弗
7.常驻5星角色:姬子、瓦尔特杨、彦卿、白露、布洛尼娅、杰帕德、克拉拉、卡卡罗、凌阳、维里奈、安可、鉴心、余烬、黎风、骏卫、别礼、艾尔黛拉
8.限定5星武器:于夜色中、拂晓之前、雨一直下、棺的回响、到不了的彼岸、只需等待、比阳光更明亮的、她已闭上双眼、此身为剑、烦恼着幸福着、惊魂夜、片刻留在眼底、镜中故我、纯粹思维的洗礼、重塑时光之忆、游戏尘寰、行于流逝的岸、命运从未公平、夜色流光溢彩、驶向第二次生命、梦应归于何处、偏偏希望无价、落日时起舞、那无数个春天、金血铭刻的时代、致长夜的星光、纵然山河万程、爱如此刻永恒、勿忘她的火焰、当她决定看见、花花世界迷人眼、一场谎言的终幕、欢迎来到银河城、邂逅于下一个花季、灼尽炼狱的新骸、苍鳞千嶂、擎傀之手、时和岁稔、赫奕流明、琼枝冰绡、驭冕铸雷之权、万物持存的注释、光影双生、裁竹、昙切、溢彩荧辉、宙算仪轨、永远的启明星、白昼之脊、昭日译注、灼霜、赝作的矮星、蜃影、碎骨、存帧、熔铸火焰、使命必达、艺术暴君、落草、狼之绯、孤舟、赤缨
9.常驻五星武器:如泥酣眠、制胜的瞬间、银河铁道之夜、但战斗还未结束、以世界之名、时节不居、无可取代的东西、漪澜浮录、擎渊怒涛、停驻之烟、千古洑流、浩境粼光、负山、不知归、大雷斑、赫拉芬格、沧溟星梦
10.四星角色:佩拉、玲可、三月七、丹恒、虎克、桑博、阿兰、艾丝妲、黑塔、娜塔莎、卢卡、希露瓦、停云、驭空、青雀、素裳、桂乃芬、寒鸦、雪衣、米沙、加拉赫、渊武、桃祈、莫特斐、丹瑾、秋水、散华、炽霞、白芷、秧秧、卜灵、佩丽卡、陈千语、弧光、赛希、艾维文娜、大潘、阿列什、狼卫、昼雪
11.四星武器: 晚安与睡颜、一场术后对话、记忆中的模样、我的诞生、猎物的视线、朗道的选择、论剑、与行星相会、秘密誓心、别让世界静下来、此时恰好、决心如汗珠般闪耀、宇宙市场趋势、舞舞舞、天才们的休憩、等价交换、无边曼舞、异度、骇行、飞逝、西升、东落、今州守望、袍泽之固、无眠烈火、不归孤军、永夜长明、奇幻变奏、呼啸重音、行进序曲、华彩乐段、异响空灵、清音、金掌、奔雷、飞景、纹秋、布道自由、终点之生、向心之引、莫奈何、十二问、坚城铸造者、探骊、仰止
12.三星武器:锋镝、物穰、天倾、琥珀、幽邃、齐颂、智库、离弦、嘉果、轮契、灵钥、蕃息、俱殁、开疆、匿影、调和、嗤笑、残泪、暗夜矩阵、源能臂铠、远行者配枪、戍关迅刀、钧天正音、寻路者信标、天使杀手、显锋、浪潮、全自动骇新星、荧光雷羽、呼啸守卫、长路、工业零点一、淬火者
13.为方便查看,抽卡模拟器中的五星角色/武器已用金色标出,四星角色/武器用紫色标出,三星武器则为白色。
四、游玩体验
0.主界面
进入游戏,显示的是这样子

1.抽卡
输入“1”则进入激动人心的抽卡环节

1.1、角色池
再输入“1”,进入角色池

开抽开抽

1.2、武器池
抽卡界面输入“2”则进入武器池

开抽开抽

2.查看已有角色
主界面输入“2”则进入角色仓库,可以查看已有的角色及其星魂

为方便观察,五星角色用金色标出,四星则是紫色
3.查看已有武器
主界面输入“3”,进入武器仓库,可以查看已有武器及其数量

同样,五星是金色,四星是紫色,三星则是白色
4.充值
主界面输入“4”则进入充值窗口

随便选一个就好,不会真的要你花钱的

5.历史记录
主界面输入“5”,查看抽卡的历史记录

5.1、角色池记录
输入“1”后进入角色池记录

可以查看每一抽的抽卡所得以及对应时间,按时间顺序倒序排列(从近期往前推),可以选择上一页/下一页进行翻页
5.2、武器池记录
输入“2”后进入武器池记录

功能同角色池记录
6.意见反馈
开发者(也就是我)的联系方式摆在这里(不过相信大部分人也都认识我)

2024年8月,米哈游公司CEO大伟哥(原名:刘伟)在原神5.0版本的开门直播中的访谈环节哭诉到“有一些声音特别特别的尖锐,把我们整个原神项目组贬低得一无是处。”“收到的声音太多了,我们很难分辨哪些是玩家真实的声音。”然而,实际上,以其为首的原神项目组已长期漠视玩家反馈与关心的问题,未能及时优化游戏质量,甚至暗插水军请托保举操控舆情,引起玩家受众极大不满。此次演说因此贻笑大方,原神玩家更是流失近半数,史称“伟泣”事件。此事与2023年5月原重庆狼队主教练林(原名:吕成林)在夺冠后喜极而泣地演说引发的“孤独颂”事件齐名,成为了上层脱离下层,服务者脱离被服务者,领导班子脱离人民群众的代名词。
相比之下,我就宽容大度许多了,有社么问题尽管提···我也很好奇这个东西还能有什么“问题”。
7.重置游戏
主界面输入“7”进入重置游戏环节

此时将弹出警告,输入“确认重置”将清除所有游戏数据,还原回初始状态

如若误触,只需回车或任意输入,即可回到主菜单。
8.演示视频
已同步上传到B站。
https://b23.tv/GHwJOKg
五、项目代码
1.完整源代码
全部代码如下所示(环境也相当简单,都不需要下什么拓展,一个标准的Pycharm就可以啦)
import random
import json
import os
from datetime import datetime
from typing import List, Dict, Tuple
# 颜色定义
class Colors:
"""ANSI颜色代码"""
RESET = '\033[0m'
# 文字颜色
GOLD = '\033[33m' # 金色(五星)
PURPLE = '\033[35m' # 紫色(四星)
WHITE = '\033[37m' # 白色(三星)
CYAN = '\033[36m' # 青色(提示信息)
GREEN = '\033[32m' # 绿色(成功信息)
RED = '\033[31m' # 红色(错误信息)
YELLOW = '\033[33m' # 黄色(警告信息)
BOLD = '\033[1m' # 粗体
class GachaSimulator:
def __init__(self):
self.save_file = "gacha_save.json"
self.init_pools()
if os.path.exists(self.save_file):
self.load_game()
else:
self.stellar_jade = 100000
self.role_pool_history = []
self.weapon_pool_history = []
self.roles = {}
self.weapons = {}
self.role_pity_5star = 0
self.role_pity_4star = 0
self.weapon_pity_5star = 0
self.weapon_pity_4star = 0
self.role_guarantee_limited = False
self.weapon_guarantee_limited = False
self.removed_roles = set()
def color_text(self, text: str, color: str, bold: bool = False) -> str:
"""给文字添加颜色"""
bold_code = Colors.BOLD if bold else ""
return f"{bold_code}{color}{text}{Colors.RESET}"
def get_rarity_color(self, rarity: str) -> str:
"""根据稀有度返回颜色"""
if rarity in ["五星角色", "五星武器"]:
return Colors.GOLD
elif rarity in ["四星角色", "四星武器"]:
return Colors.PURPLE
else:
return Colors.WHITE
def get_rarity_display(self, name: str, rarity: str, extra: str = "") -> str:
"""获取带颜色的稀有度显示文本"""
color = self.get_rarity_color(rarity)
if extra:
return self.color_text(f"{name}{extra}", color, bold=(rarity.startswith("五星")))
return self.color_text(name, color, bold=(rarity.startswith("五星")))
def init_pools(self):
"""初始化卡池内容"""
self.limited_5star_roles = [
"希儿", "景元", "银狼", "罗刹", "刃", "卡芙卡", "丹恒饮月", "符玄", "镜流", "托帕",
"藿藿", "银枝", "阮梅", "真理医生", "黑天鹅", "花火", "黄泉", "砂金", "知更鸟", "波提欧",
"流萤", "翡翠", "云璃", "椒丘", "刻律德菈", "长夜月", "丹恒腾荒", "昔涟", "大丽花", "爻光",
"火花", "不死途", "银狼Lv999", "绯英", "千冶刃", "忌炎", "吟霖", "今汐", "长离", "折枝",
"奥古斯塔", "尤诺", "嘉贝丽娜", "仇远", "千咲", "琳奈", "莫宁", "爱弥斯", "陆赫斯", "西格莉卡",
"绯雪", "达尼娅", "露西", "丽贝卡", "洛瑟菈", "莱万汀", "洁尔佩塔", "伊冯", "汤汤", "洛茜",
"庄方宜", "弭弗"
]
self.standard_5star_roles = [
"姬子", "瓦尔特杨", "彦卿", "白露", "布洛尼娅", "杰帕德", "克拉拉", "卡卡罗", "凌阳",
"维里奈", "安可", "鉴心", "余烬", "黎风", "骏卫", "别礼", "艾尔黛拉"
]
self.four_star_roles = [
"佩拉", "玲可", "三月七", "丹恒", "虎克", "桑博", "阿兰", "艾丝妲", "黑塔", "娜塔莎",
"卢卡", "希露瓦", "停云", "驭空", "青雀", "素裳", "桂乃芬", "寒鸦", "雪衣", "米沙",
"加拉赫", "渊武", "桃祈", "莫特斐", "丹瑾", "秋水", "散华", "炽霞", "白芷", "秧秧",
"卜灵", "佩丽卡", "陈千语", "弧光", "赛希", "艾维文娜", "大潘", "阿列什", "狼卫", "昼雪"
]
self.limited_5star_weapons = [
"于夜色中", "拂晓之前", "雨一直下", "棺的回响", "到不了的彼岸", "只需等待", "比阳光更明亮的",
"她已闭上双眼", "此身为剑", "烦恼着幸福着", "惊魂夜", "片刻留在眼底", "镜中故我", "纯粹思维的洗礼",
"重塑时光之忆", "游戏尘寰", "行于流逝的岸", "命运从未公平", "夜色流光溢彩", "驶向第二次生命",
"梦应归于何处", "偏偏希望无价", "落日时起舞", "那无数个春天", "金血铭刻的时代", "致长夜的星光",
"纵然山河万程", "爱如此刻永恒", "勿忘她的火焰", "当她决定看见", "花花世界迷人眼", "一场谎言的终幕",
"欢迎来到银河城", "邂逅于下一个花季", "灼尽炼狱的新骸", "苍鳞千嶂", "擎傀之手", "时和岁稔",
"赫奕流明", "琼枝冰绡", "驭冕铸雷之权", "万物持存的注释", "光影双生", "裁竹", "昙切", "溢彩荧辉",
"宙算仪轨", "永远的启明星", "白昼之脊", "昭日译注", "灼霜", "赝作的矮星", "蜃影", "碎骨", "存帧",
"熔铸火焰", "使命必达", "艺术暴君", "落草", "狼之绯", "孤舟", "赤缨"
]
self.standard_5star_weapons = [
"如泥酣眠", "制胜的瞬间", "银河铁道之夜", "但战斗还未结束", "以世界之名", "时节不居",
"无可取代的东西", "漪澜浮录", "擎渊怒涛", "停驻之烟", "千古洑流", "浩境粼光", "负山",
"不知归", "大雷斑", "赫拉芬格", "沧溟星梦"
]
self.four_star_weapons = [
"晚安与睡颜", "一场术后对话", "记忆中的模样", "我的诞生", "猎物的视线", "朗道的选择", "论剑",
"与行星相会", "秘密誓心", "别让世界静下来", "此时恰好", "决心如汗珠般闪耀", "宇宙市场趋势",
"舞舞舞", "天才们的休憩", "等价交换", "无边曼舞", "异度", "骇行", "飞逝", "西升", "东落",
"今州守望", "袍泽之固", "无眠烈火", "不归孤军", "永夜长明", "奇幻变奏", "呼啸重音", "行进序曲",
"华彩乐段", "异响空灵", "清音", "金掌", "奔雷", "飞景", "纹秋", "布道自由", "终点之生",
"向心之引", "莫奈何", "十二问", "坚城铸造者", "探骊", "仰止"
]
self.three_star_weapons = [
"锋镝", "物穰", "天倾", "琥珀", "幽邃", "齐颂", "智库", "离弦", "嘉果", "轮契",
"灵钥", "蕃息", "俱殁", "开疆", "匿影", "调和", "嗤笑", "残泪", "暗夜矩阵", "源能臂铠",
"远行者配枪", "戍关迅刀", "钧天正音", "寻路者信标", "天使杀手", "显锋", "浪潮", "全自动骇新星",
"荧光雷羽", "呼啸守卫", "长路", "工业零点一", "淬火者"
]
self.all_roles = (self.limited_5star_roles + self.standard_5star_roles +
self.four_star_roles)
def save_game(self):
"""保存游戏数据"""
save_data = {
"stellar_jade": self.stellar_jade,
"role_pool_history": [
{"name": r["name"], "type": r["type"], "time": r["time"].isoformat()}
for r in self.role_pool_history
],
"weapon_pool_history": [
{"name": w["name"], "type": w["type"], "time": w["time"].isoformat()}
for w in self.weapon_pool_history
],
"roles": self.roles,
"weapons": self.weapons,
"role_pity_5star": self.role_pity_5star,
"role_pity_4star": self.role_pity_4star,
"weapon_pity_5star": self.weapon_pity_5star,
"weapon_pity_4star": self.weapon_pity_4star,
"role_guarantee_limited": self.role_guarantee_limited,
"weapon_guarantee_limited": self.weapon_guarantee_limited,
"removed_roles": list(self.removed_roles)
}
try:
with open(self.save_file, 'w', encoding='utf-8') as f:
json.dump(save_data, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"保存失败: {e}")
def load_game(self):
"""加载游戏数据"""
try:
with open(self.save_file, 'r', encoding='utf-8') as f:
save_data = json.load(f)
self.stellar_jade = save_data["stellar_jade"]
self.role_pool_history = []
for r in save_data["role_pool_history"]:
r["time"] = datetime.fromisoformat(r["time"])
self.role_pool_history.append(r)
self.weapon_pool_history = []
for w in save_data["weapon_pool_history"]:
w["time"] = datetime.fromisoformat(w["time"])
self.weapon_pool_history.append(w)
self.roles = save_data["roles"]
self.weapons = save_data["weapons"]
self.role_pity_5star = save_data["role_pity_5star"]
self.role_pity_4star = save_data["role_pity_4star"]
self.weapon_pity_5star = save_data["weapon_pity_5star"]
self.weapon_pity_4star = save_data["weapon_pity_4star"]
self.role_guarantee_limited = save_data["role_guarantee_limited"]
self.weapon_guarantee_limited = save_data["weapon_guarantee_limited"]
self.removed_roles = set(save_data["removed_roles"])
print(self.color_text("存档加载成功!", Colors.GREEN))
except Exception as e:
print(self.color_text(f"加载存档失败: {e},将使用初始数据", Colors.RED))
def add_role(self, role_name: str, rarity: str = "五星角色") -> str:
"""添加角色,处理星魂转化"""
if role_name in self.removed_roles:
return self.get_rarity_display(role_name, rarity, "(但该角色已被移除)")
if role_name in self.roles:
current_star = self.roles[role_name]
if current_star < 6:
self.roles[role_name] += 1
if current_star + 1 == 6:
self.removed_roles.add(role_name)
self.save_game()
return self.get_rarity_display(
role_name, rarity,
f"(已转化为星魂*{current_star + 1},角色已满星并从卡池移除)"
)
self.save_game()
return self.get_rarity_display(
role_name, rarity,
f"(已转化为星魂*{current_star + 1})"
)
else:
return self.get_rarity_display(role_name, rarity, "(但该角色已满星,无法继续获取)")
else:
self.roles[role_name] = 0
self.save_game()
return self.get_rarity_display(role_name, rarity)
def add_weapon(self, weapon_name: str, rarity: str) -> str:
"""添加武器"""
if weapon_name in self.weapons:
self.weapons[weapon_name] += 1
else:
self.weapons[weapon_name] = 1
self.save_game()
return self.get_rarity_display(weapon_name, rarity)
def get_five_star_role(self) -> Tuple[str, str]:
"""获取五星角色"""
if self.role_guarantee_limited:
result = random.choice(self.limited_5star_roles)
self.role_guarantee_limited = False
return result, "limited"
else:
if random.random() < 0.5:
result = random.choice(self.limited_5star_roles)
return result, "limited"
else:
result = random.choice(self.standard_5star_roles)
self.role_guarantee_limited = True
return result, "standard"
def get_five_star_weapon(self) -> Tuple[str, str]:
"""获取五星武器"""
if self.weapon_guarantee_limited:
result = random.choice(self.limited_5star_weapons)
self.weapon_guarantee_limited = False
return result, "limited"
else:
if random.random() < 0.75:
result = random.choice(self.limited_5star_weapons)
return result, "limited"
else:
result = random.choice(self.standard_5star_weapons)
self.weapon_guarantee_limited = True
return result, "standard"
def draw_single(self, pool_type: str) -> str:
"""单抽"""
if pool_type == "role":
available_roles = [r for r in self.all_roles if r not in self.removed_roles]
if not available_roles:
return "ERROR:角色已抽满,无法继续抽卡"
self.role_pity_5star += 1
self.role_pity_4star += 1
if self.role_pity_5star >= 90:
role, role_type = self.get_five_star_role()
self.role_pity_5star = 0
self.role_pity_4star = 0
result = self.add_role(role, "五星角色")
self.role_pool_history.append({
"name": role,
"type": "五星角色",
"time": datetime.now()
})
self.save_game()
return result
if self.role_pity_4star >= 10:
self.role_pity_4star = 0
if random.random() < 0.5:
role = random.choice(self.four_star_roles)
result = self.add_role(role, "四星角色")
self.role_pool_history.append({
"name": role,
"type": "四星角色",
"time": datetime.now()
})
self.save_game()
return result
else:
weapon = random.choice(self.four_star_weapons)
result = self.add_weapon(weapon, "四星武器")
self.role_pool_history.append({
"name": weapon,
"type": "四星武器",
"time": datetime.now()
})
self.save_game()
return result
rand = random.random()
if rand < 0.006:
role, role_type = self.get_five_star_role()
self.role_pity_5star = 0
self.role_pity_4star = 0
result = self.add_role(role, "五星角色")
self.role_pool_history.append({
"name": role,
"type": "五星角色",
"time": datetime.now()
})
self.save_game()
return result
elif rand < 0.057:
self.role_pity_4star = 0
if random.random() < 0.5:
role = random.choice(self.four_star_roles)
result = self.add_role(role, "四星角色")
self.role_pool_history.append({
"name": role,
"type": "四星角色",
"time": datetime.now()
})
self.save_game()
return result
else:
weapon = random.choice(self.four_star_weapons)
result = self.add_weapon(weapon, "四星武器")
self.role_pool_history.append({
"name": weapon,
"type": "四星武器",
"time": datetime.now()
})
self.save_game()
return result
else:
weapon = random.choice(self.three_star_weapons)
result = self.add_weapon(weapon, "三星武器")
self.role_pool_history.append({
"name": weapon,
"type": "三星武器",
"time": datetime.now()
})
self.save_game()
return result
else:
self.weapon_pity_5star += 1
self.weapon_pity_4star += 1
if self.weapon_pity_5star >= 80:
weapon, weapon_type = self.get_five_star_weapon()
self.weapon_pity_5star = 0
self.weapon_pity_4star = 0
result = self.add_weapon(weapon, "五星武器")
self.weapon_pool_history.append({
"name": weapon,
"type": "五星武器",
"time": datetime.now()
})
self.save_game()
return result
if self.weapon_pity_4star >= 10:
self.weapon_pity_4star = 0
if random.random() < 0.5:
role = random.choice(self.four_star_roles)
result = self.add_role(role, "四星角色")
self.weapon_pool_history.append({
"name": role,
"type": "四星角色",
"time": datetime.now()
})
self.save_game()
return result
else:
weapon = random.choice(self.four_star_weapons)
result = self.add_weapon(weapon, "四星武器")
self.weapon_pool_history.append({
"name": weapon,
"type": "四星武器",
"time": datetime.now()
})
self.save_game()
return result
rand = random.random()
if rand < 0.008:
weapon, weapon_type = self.get_five_star_weapon()
self.weapon_pity_5star = 0
self.weapon_pity_4star = 0
result = self.add_weapon(weapon, "五星武器")
self.weapon_pool_history.append({
"name": weapon,
"type": "五星武器",
"time": datetime.now()
})
self.save_game()
return result
elif rand < 0.074:
self.weapon_pity_4star = 0
if random.random() < 0.5:
role = random.choice(self.four_star_roles)
result = self.add_role(role, "四星角色")
self.weapon_pool_history.append({
"name": role,
"type": "四星角色",
"time": datetime.now()
})
self.save_game()
return result
else:
weapon = random.choice(self.four_star_weapons)
result = self.add_weapon(weapon, "四星武器")
self.weapon_pool_history.append({
"name": weapon,
"type": "四星武器",
"time": datetime.now()
})
self.save_game()
return result
else:
weapon = random.choice(self.three_star_weapons)
result = self.add_weapon(weapon, "三星武器")
self.weapon_pool_history.append({
"name": weapon,
"type": "三星武器",
"time": datetime.now()
})
self.save_game()
return result
def draw_ten(self, pool_type: str) -> List[str]:
"""十连抽"""
if self.stellar_jade < 1600:
return None
self.stellar_jade -= 1600
results = []
for i in range(10):
result = self.draw_single(pool_type)
if result == "ERROR:角色已抽满,无法继续抽卡":
self.stellar_jade += 1600
return ["ERROR"]
results.append(result)
self.save_game()
return results
def show_roles(self):
"""显示已有角色"""
if not self.roles:
print("暂无角色")
return
for role, star in sorted(self.roles.items()):
# 判断角色稀有度
if role in self.limited_5star_roles or role in self.standard_5star_roles:
color = Colors.GOLD
bold = True
elif role in self.four_star_roles:
color = Colors.PURPLE
bold = False
else:
color = Colors.WHITE
bold = False
role_text = f"{role}(星魂{star})"
print(self.color_text(role_text, color, bold))
def show_weapons(self):
"""显示已有武器"""
if not self.weapons:
print("暂无武器")
return
# 创建武器稀有度映射
five_star_weapons = set(self.limited_5star_weapons + self.standard_5star_weapons)
four_star_weapons = set(self.four_star_weapons)
for weapon, count in sorted(self.weapons.items()):
if weapon in five_star_weapons:
color = Colors.GOLD
bold = True
elif weapon in four_star_weapons:
color = Colors.PURPLE
bold = False
else:
color = Colors.WHITE
bold = False
weapon_text = f"{weapon}*{count}"
print(self.color_text(weapon_text, color, bold))
def recharge(self, amount: int):
"""充值"""
self.stellar_jade += amount
self.save_game()
print(self.color_text(f"充值成功!获得{amount}星琼,当前星琼:{self.stellar_jade}", Colors.GREEN))
def show_history(self, pool_type: str, page: int = 1):
"""显示历史记录"""
history = self.role_pool_history if pool_type == "role" else self.weapon_pool_history
if not history:
print("暂无抽卡记录")
return None
reversed_history = list(reversed(history))
total_pages = (len(reversed_history) + 9) // 10
if page < 1 or page > total_pages:
print("页码无效")
return None
start = (page - 1) * 10
end = min(start + 10, len(reversed_history))
print(self.color_text(f"=== 第{page}页 ===", Colors.CYAN, True))
for i, record in enumerate(reversed_history[start:end], start + 1):
time_str = record["time"].strftime("%Y-%m-%d %H:%M:%S")
color = self.get_rarity_color(record["type"])
name_colored = self.color_text(record["name"], color, bold=(record["type"].startswith("五星")))
print(f"{i}.{name_colored} {time_str}")
return total_pages
def do_draw(self, pool_type: str, draw_type: str):
"""执行抽卡(单抽或十连)"""
if draw_type == "single":
if self.stellar_jade < 160:
print(self.color_text("星琼不足,请先充值!", Colors.RED))
return False
self.stellar_jade -= 160
print(self.color_text("\n=== 抽卡结果 ===", Colors.CYAN, True))
result = self.draw_single(pool_type)
if result == "ERROR:角色已抽满,无法继续抽卡":
print(self.color_text("角色已抽满,无法继续抽卡", Colors.RED))
return False
print(result)
print(self.color_text(f"\n当前星琼:{self.stellar_jade}", Colors.GREEN))
return True
elif draw_type == "ten":
if self.stellar_jade < 1600:
print(self.color_text("星琼不足,请先充值!", Colors.RED))
return False
print(self.color_text("\n=== 十连抽结果 ===", Colors.CYAN, True))
results = self.draw_ten(pool_type)
if results and results[0] == "ERROR":
print(self.color_text("角色已抽满,无法继续抽卡", Colors.RED))
return False
# 十连抽结果之间添加分隔符,更美观
for i, r in enumerate(results, 1):
print(f"{i:2d}. {r}")
print(self.color_text(f"\n当前星琼:{self.stellar_jade}", Colors.GREEN))
return True
return False
def pool_menu(self, pool_type: str):
"""卡池抽卡菜单(修复版)"""
pool_name = "角色池" if pool_type == "role" else "武器池"
while True:
# 显示当前卡池菜单
print(self.color_text(f"\n=== {pool_name} ===", Colors.CYAN, True))
print(f"当前星琼:{self.stellar_jade}")
print("1. 单抽(160星琼)")
print("2. 十连(1600星琼)")
print("3. 返回上级菜单")
choice = input("\n请选择:").strip()
if choice == "1":
# 执行单抽
self.do_draw(pool_type, "single")
# 抽完后询问是否继续
while True:
print(self.color_text("\n是否继续抽卡?", Colors.YELLOW))
print("1. 继续单抽")
print("2. 继续十连")
print("3. 返回卡池菜单")
cont = input("请选择:").strip()
if cont == "1":
if self.stellar_jade < 160:
print(self.color_text("星琼不足,请先充值!", Colors.RED))
break
# 继续单抽
self.do_draw(pool_type, "single")
elif cont == "2":
if self.stellar_jade < 1600:
print(self.color_text("星琼不足,请先充值!", Colors.RED))
break
# 继续十连
self.do_draw(pool_type, "ten")
elif cont == "3":
# 返回卡池菜单,跳出内层循环
break
else:
print(self.color_text("无效选择,请重新输入", Colors.RED))
elif choice == "2":
# 执行十连
self.do_draw(pool_type, "ten")
# 抽完后询问是否继续
while True:
print(self.color_text("\n是否继续抽卡?", Colors.YELLOW))
print("1. 继续单抽")
print("2. 继续十连")
print("3. 返回卡池菜单")
cont = input("请选择:").strip()
if cont == "1":
if self.stellar_jade < 160:
print(self.color_text("星琼不足,请先充值!", Colors.RED))
break
# 继续单抽
self.do_draw(pool_type, "single")
elif cont == "2":
if self.stellar_jade < 1600:
print(self.color_text("星琼不足,请先充值!", Colors.RED))
break
# 继续十连
self.do_draw(pool_type, "ten")
elif cont == "3":
# 返回卡池菜单,跳出内层循环
break
else:
print(self.color_text("无效选择,请重新输入", Colors.RED))
elif choice == "3":
break
else:
print(self.color_text("无效选择,请重新输入", Colors.RED))
def reset_game(self):
"""重置游戏"""
print(self.color_text("\n⚠️ 警告:重置将清除所有游戏数据!", Colors.RED, True))
confirm = input("确认重置?(输入 '确认重置' 继续): ")
if confirm == "确认重置":
if os.path.exists(self.save_file):
os.remove(self.save_file)
print(self.color_text("游戏已重置,请重启程序", Colors.GREEN))
exit()
def run(self):
"""主程序"""
print(self.color_text("\n欢迎回来!" if os.path.exists(self.save_file) else "欢迎来到「终末」抽卡模拟器!",
Colors.CYAN, True))
while True:
print(self.color_text("\n" + "=" * 50, Colors.CYAN))
print(self.color_text("「终末」抽卡模拟器", Colors.GOLD, True))
print(self.color_text("=" * 50, Colors.CYAN))
print("1. 抽卡")
print("2. 查看已有角色")
print("3. 查看已有武器")
print("4. 充值")
print("5. 历史记录")
print("6. 意见反馈")
print("7. 重置游戏")
print("0. 退出游戏")
choice = input("\n请选择:").strip()
if choice == "1":
self.draw_menu()
elif choice == "2":
print(self.color_text("\n=== 已有角色 ===", Colors.CYAN, True))
self.show_roles()
input("\n按回车继续...")
elif choice == "3":
print(self.color_text("\n=== 已有武器 ===", Colors.CYAN, True))
self.show_weapons()
input("\n按回车继续...")
elif choice == "4":
self.recharge_menu()
elif choice == "5":
self.history_menu()
elif choice == "6":
print(self.color_text("\n=== 意见反馈 ===", Colors.CYAN, True))
print("本人qq:2956618010")
input("\n按回车继续...")
elif choice == "7":
self.reset_game()
elif choice == "0":
self.save_game()
print(self.color_text("游戏数据已保存,感谢游玩!", Colors.GREEN))
break
else:
print(self.color_text("无效选择,请重新输入", Colors.RED))
def draw_menu(self):
"""抽卡菜单"""
while True:
print(self.color_text("\n=== 抽卡 ===", Colors.CYAN, True))
print("1. 角色池")
print("2. 武器池")
print("3. 返回主菜单")
choice = input("\n请选择:").strip()
if choice == "1":
self.pool_menu("role")
elif choice == "2":
self.pool_menu("weapon")
elif choice == "3":
break
else:
print(self.color_text("无效选择", Colors.RED))
def recharge_menu(self):
"""充值菜单"""
while True:
print(self.color_text("\n=== 充值 ===", Colors.CYAN, True))
print("1. 10星琼")
print("2. 60星琼")
print("3. 180星琼")
print("4. 300星琼")
print("5. 960星琼")
print("6. 1280星琼")
print("7. 1980星琼")
print("8. 3280星琼")
print("9. 6480星琼")
print("0. 返回")
choice = input("\n请选择充值档位:").strip()
amounts = {
"1": 10, "2": 60, "3": 180, "4": 300,
"5": 960, "6": 1280, "7": 1980, "8": 3280, "9": 6480
}
if choice in amounts:
self.recharge(amounts[choice])
elif choice == "0":
break
else:
print(self.color_text("无效选择", Colors.RED))
def history_menu(self):
"""历史记录菜单"""
while True:
print(self.color_text("\n=== 历史记录 ===", Colors.CYAN, True))
print("1. 角色池记录")
print("2. 武器池记录")
print("3. 返回")
choice = input("\n请选择:").strip()
if choice == "1":
self.view_history("role")
elif choice == "2":
self.view_history("weapon")
elif choice == "3":
break
else:
print(self.color_text("无效选择", Colors.RED))
def view_history(self, pool_type: str):
"""查看历史记录"""
page = 1
while True:
total_pages = self.show_history(pool_type, page)
if total_pages is None:
break
print(f"\n第{page}/{total_pages}页")
print("1. 上一页")
print("2. 下一页")
print("3. 返回")
choice = input("请选择:").strip()
if choice == "1" and page > 1:
page -= 1
elif choice == "2" and page < total_pages:
page += 1
elif choice == "3":
break
else:
if choice in ["1", "2"]:
print(self.color_text("已到边界", Colors.RED))
if __name__ == "__main__":
game = GachaSimulator()
game.run()
2.代码托管
传到gitee上了,非常的简洁,一组代码就够啦,想玩的自己去拷一下。
https://gitee.com/sdjxiw/ecxmnq

六、系统架构
1.文件存储
创建文件并初始化,后续星琼的消耗,角色/武器的获取都将填加至其中。
def __init__(self):
self.save_file = "gacha_save.json"
self.init_pools()
if os.path.exists(self.save_file):
self.load_game()
else:
self.stellar_jade = 100000
self.role_pool_history = []
self.weapon_pool_history = []
self.roles = {}
self.weapons = {}
self.role_pity_5star = 0
self.role_pity_4star = 0
self.weapon_pity_5star = 0
self.weapon_pity_4star = 0
self.role_guarantee_limited = False
self.weapon_guarantee_limited = False
self.removed_roles = set()
2.保存游戏数据
游戏数据(如角色、星琼等)发生变化时,都将同步保存至文件中,确保游戏数据不因退出游戏而丢失
def save_game(self):
"""保存游戏数据"""
save_data = {
"stellar_jade": self.stellar_jade,
"role_pool_history": [
{"name": r["name"], "type": r["type"], "time": r["time"].isoformat()}
for r in self.role_pool_history
],
"weapon_pool_history": [
{"name": w["name"], "type": w["type"], "time": w["time"].isoformat()}
for w in self.weapon_pool_history
],
"roles": self.roles,
"weapons": self.weapons,
"role_pity_5star": self.role_pity_5star,
"role_pity_4star": self.role_pity_4star,
"weapon_pity_5star": self.weapon_pity_5star,
"weapon_pity_4star": self.weapon_pity_4star,
"role_guarantee_limited": self.role_guarantee_limited,
"weapon_guarantee_limited": self.weapon_guarantee_limited,
"removed_roles": list(self.removed_roles)
}
try:
with open(self.save_file, 'w', encoding='utf-8') as f:
json.dump(save_data, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"保存失败: {e}")
3.加载游戏数据
重新登录游戏时,将游戏数据从文件中加载出来,接着上一次的玩
def load_game(self):
"""加载游戏数据"""
try:
with open(self.save_file, 'r', encoding='utf-8') as f:
save_data = json.load(f)
self.stellar_jade = save_data["stellar_jade"]
self.role_pool_history = []
for r in save_data["role_pool_history"]:
r["time"] = datetime.fromisoformat(r["time"])
self.role_pool_history.append(r)
self.weapon_pool_history = []
for w in save_data["weapon_pool_history"]:
w["time"] = datetime.fromisoformat(w["time"])
self.weapon_pool_history.append(w)
self.roles = save_data["roles"]
self.weapons = save_data["weapons"]
self.role_pity_5star = save_data["role_pity_5star"]
self.role_pity_4star = save_data["role_pity_4star"]
self.weapon_pity_5star = save_data["weapon_pity_5star"]
self.weapon_pity_4star = save_data["weapon_pity_4star"]
self.role_guarantee_limited = save_data["role_guarantee_limited"]
self.weapon_guarantee_limited = save_data["weapon_guarantee_limited"]
self.removed_roles = set(save_data["removed_roles"])
print(self.color_text("存档加载成功!", Colors.GREEN))
except Exception as e:
print(self.color_text(f"加载存档失败: {e},将使用初始数据", Colors.RED))
4.小保底随机性
和游戏内的规则是一样的,不过我也希望各位都能欧一点
def get_five_star_role(self) -> Tuple[str, str]:
"""获取五星角色"""
if self.role_guarantee_limited:
result = random.choice(self.limited_5star_roles)
self.role_guarantee_limited = False
return result, "limited"
else:
if random.random() < 0.5:
result = random.choice(self.limited_5star_roles)
return result, "limited"
else:
result = random.choice(self.standard_5star_roles)
self.role_guarantee_limited = True
return result, "standard"
def get_five_star_weapon(self) -> Tuple[str, str]:
"""获取五星武器"""
if self.weapon_guarantee_limited:
result = random.choice(self.limited_5star_weapons)
self.weapon_guarantee_limited = False
return result, "limited"
else:
if random.random() < 0.75:
result = random.choice(self.limited_5star_weapons)
return result, "limited"
else:
result = random.choice(self.standard_5star_weapons)
self.weapon_guarantee_limited = True
return result, "standard"
##5.抽卡概率
 谁不想来个十连双金呢(这里以角色池为例)
```python
rand = random.random()
if rand < 0.006:
role, role_type = self.get_five_star_role()
self.role_pity_5star = 0
self.role_pity_4star = 0
result = self.add_role(role, "五星角色")
self.role_pool_history.append({
"name": role,
"type": "五星角色",
"time": datetime.now()
})
self.save_game()
return result
elif rand < 0.057:
self.role_pity_4star = 0
if random.random() < 0.5:
role = random.choice(self.four_star_roles)
result = self.add_role(role, "四星角色")
self.role_pool_history.append({
"name": role,
"type": "四星角色",
"time": datetime.now()
})
self.save_game()
return result
else:
weapon = random.choice(self.four_star_weapons)
result = self.add_weapon(weapon, "四星武器")
self.role_pool_history.append({
"name": weapon,
"type": "四星武器",
"time": datetime.now()
})
self.save_game()
return result
else:
weapon = random.choice(self.three_star_weapons)
result = self.add_weapon(weapon, "三星武器")
self.role_pool_history.append({
"name": weapon,
"type": "三星武器",
"time": datetime.now()
})
七、实验问题与感悟
1.关于卡池
三个游戏里的几乎所有角色和武器聚合在一起当混池,这个实在是太多了,而且我根本记不下来有哪些
解决方案:卡池中的角色/武器内容可以去游戏里或者攻略网站上找,接下来,便是痛苦的手敲环节了···
2.关于系统逻辑
起初在选择连续抽卡时,会返回上级菜单,产生冗余步骤
解决方案:通过询问ai,发现循环的嵌套有误,重构pool_menu算法,使其选择继续单抽/十连时,直接执行抽卡,不会返回上级菜单
3.思想与感悟
「终末」抽卡模拟器的灵感来源其实非常简单,就是我们上课时候做过的幸运数字与石头剪刀布,都是“随机抽取”与“随机生成”,恰好当时我在抽绯英和达妮娅(绯英歪了,不过达妮娅比较欧),于是乎就诞生了做抽卡模拟器的初步想法。
本来我是打算做爬虫的,爬取豆瓣上的电影数据,但仔细一想,爬虫不免有些华而不实,专门为了应付作业了,既然我现在有想法,又有条件,那便要创造一点“真正有用的东西”。(雀食是很有用了,感觉比我之前做的大创和挑战者杯都要有用的多)
提到“想法”,我们不得不承认,小到科研,大到人生轨迹,很多时候都是因为某一个想法而发生了翻天覆地的变化。然而,起到决定性作用的,当真就是那一个特殊的想法么?显然不是,这是量变与质变的关系。量变是质变的必要准备,质变是量变的必然结果,并为新的量变开辟道路。揆诸现实,如若我没有学一学期的python,没有玩将近三年的二游,便不会有抽卡模拟器的出现。放到人生轨迹上呢?如若没有大二上一整个学期的卧薪尝胆、厚积薄发,便不会有现在的“新局面”出现。
提到“有用”,这便与人生轨迹联系得更紧密了,往小了说,我们要学点对自己“有用”的东西,往大了说,我们要做个对社会“有用”的人——这便是价值。对外界价值的汲取往往伴随着个人素质的提升,而个人素质的提升则引发个人价值的实现。如今,作为社会的主力军,我们新时代青年该如何实现个人价值呢?居庙堂之高则忧其民,处江湖之远则忧其君。矛盾是普遍性与特殊性的对立统一,大家是同样的人,却又不一样——我们每个人的基因编码、成长环境、人生历程各有不同,造就了每一个独一无二的个体。深度挖掘自身的特殊性,将个人的优势与特长无限放大,那么实现人生价值,便计日可待了。
这不免让我想到当下网上的又一则热点——张某某事件。诚然,在目前的教育制度下,高考是人生大事之一,而高考志愿填报则在很大程度上决定了人生的走向。正因如此,志愿绝非由“数据”“趋势”等参数机械性地分析而得出的结果,它取决于人——数据是为人服务的,而并非人的枷锁。人生的十字路口,应当由人的主观意愿来决定,张某某的高报模式拘泥于客观数据,而无视人的主观感受,颠倒了主体与客体,混淆了目的与手段,便是这种高报模式最大的缺陷。那它是否有好处呢?肯定是有的,不然它的市场也没法发展起来——平面的、直接的报考需求,完全可以靠他这一套大数据来解决。然而,形而上学的理论,上限是一眼望到头的。
对于张某某个人,我从未喜欢过他。近年来,我的态度更是由“不喜欢”转变为了“反感”最后“反对”。作为学生,文科对我有特殊的意义,所以他对文科的看法令我不爽;作为老师,还是教政治理论的老师,我只能说不是什么人都配叫做老师的;作为未来机要密码事业的接班人,我更不会默许他冠冕堂皇地给职业分高低贵贱三六九等的行为——诸类言行,仅局限于短暂的、片面的“经济效益”,而忽视了长远的、总体的“人的价值”的实现——诸般弊端,反加诸其身,最终,连同他自己的“价值”,也在一场场风花雪月、觥筹交错之中,化为了一场「永恒笑剧」。一切的一切,都将在「终末」之下重逢。
三年前,张某某说:新闻学已死!
三年后,新闻学说:张某某已死!
4.参考资料:
《程序设计与数据结构教程(第二版)》https://book.douban.com/subject/26851579/
《Python官方文档》https://docs.python.org/zh-cn/3/
《PyCharm调试指南》https://www.jetbrains.com/help/pycharm/debugging-code.html
《Git入门教程》https://gitee.com/help/categories/5
《中国近代史纲要(2023年版)》http://cc.bjtu.edu.cn:81/meol/common/script/preview/download_preview.jsp?fileid=3777688&resid=368523&lid=36919
《马克思主义原理(2023修订版)》https://eol.shzu.edu.cn/meol/homepage/course/listview.jsp?_colId=null&acttype=enter&folderid=152341489&lid=40867
···
浙公网安备 33010602011771号