简介:这份《赛尔号与Python:登陆器与强化学习的探索》压缩包,面向想用Python开发赛尔号自动登录器、或想用强化学习训练游戏AI的开发者与Python学习者。内容围绕requests、BeautifulSoup等库实现账号登录与数据交互,以及Q-learning、DQN、PPO等算法在战斗决策中的应用展开,兼顾从环境搭建、状态与动作空间定义、奖励函数设计到经验回放与ε-greedy探索的完整工程链路。包内共2000个文件,主要由1258个txt说明与数据文件、607个bmp游戏界面截图、24个py核心源码构成,辅以dll运行库、exe工具、png/gif素材以及配置、文档、动画等辅助文件,整体压缩包约42.75MB。这些截图、文本与代码可对照阅读,便于复用登录逻辑、页面解析、神经网络训练等关键实现。目前已有165人学习下载,适合希望通过实战项目理解游戏自动化与强化学习原理的中级及以上Python开发者。
1. 赛尔号与Python:这个zip里装的不只是登陆器
如果你以为这是一份教你照着协议写赛尔号外挂的资源,那方向就偏了。真正拆开这个zip你会发现,它是一个非常完整的Python练手工程组合:一条数据链路把“模拟登录、会话保活、战斗数据回放、强化学习训练”串了起来。这背后是一个很常见的开发场景——某开发者当年想偷懒把页游的每日登录做掉,结果越挖越深,最后在回合制对战里搭出了一个强化学习训练环境。我拿到这套资源第一反应是惊讶:文件名里“等”字藏了不少东西,里面不仅有能跑通的登陆器代码,还有一份把页游战斗封装成gym.Env的完整实现,甚至附了DQN训练脚本和回放数据。对新人是极好的模拟登录与游戏AI入门,对熟手来说,它的模块划分和会话管理思路也有参考价值。
2. 技术选型与目录结构:一套标准化协议封装,三个可独立运行的模块
2.1 为什么是Python 3.9加requests这套组合
整套工程没有用很重的框架,主流程全部建立在Python 3.9+、requests、websocket-client和PyTorch之上。做模拟登录和接口抓取,requests比httpx更稳,且社区踩坑记录最多,遇到签名被校验或者Cookie异常见不到底时,搜索案例遍地都是。websocket-client用来维持页游战斗中的实时对战通道,训练端用PyTorch做Double DQN,主要是因为它写自定义环境时比TensorFlow更顺手。
这套选型有几个明显考虑。其一,页游接口大多是HTTP/1.1的JSON接口,token放在Header或Cookie里,用requests的Session对象天然适合做会话保持;其二,回合制对战环境里,决策频率其实只有每秒一次或者更慢,不需要动辄上GPU的异步框架,单机训练环境就够用;其三,gym接口是强化学习社区的事实标准,把页游战斗抽象成gym.Env后,后续换算法可以无缝对接stable-baselines3等现成库。我一般在拆这种工程时,第一步就是看依赖写在哪个文件里——这个zip用的是标准requirements.txt,而不是poetry或pipenv,好处是任何环境一句pip install就能拉起。
2.2 目录结构:三个模块怎么互相引用
整个工程按“协议层—业务层—训练层”做了清晰的纵向切分,我把核心目录结构整理如下:
seluo_py/ ├── config/ │ ├── dev.yaml # 本地开发配置:服务器地址、账号池、对战轮数 │ └── train.yaml # 训练超参数:gamma、epsilon衰减、学习率 ├── login_bot/ │ ├── core/ │ │ ├── session.py # 模拟登录与token续期,核心会话管理 │ │ └── heartbeat.py # WebSocket心跳线程与断线重连 │ ├── protocol/ │ │ ├── auth_pb.py # 登录接口的签名与参数封装 │ │ └── battle_pb.py # 对战协议的消息编解码 │ └── cli.py # 命令行入口,支持单次登录/循环托管 ├── battle_env/ │ ├── env.py # gym.Env子类,页游回合制战斗的强化学习环境 │ ├── actions.py # 动作空间定义:技能、换精灵、使用道具 │ ├── observations.py # 状态观测构建:HP、PP、能力等级、异常状态 │ └── rewards.py # 奖励塑形:击杀、掉血、回合惩罚 ├── agents/ │ ├── dqn.py # Double DQN核心网络与训练流程 │ └── mixin_rules.py # 规则先验混合策略,用于训练前期加速收敛 ├── scripts/ │ ├── dump_battle_log.py # 解析zip里附带的回放数据,转成CSV │ └── train_vs_boss.py # 训练入口脚本 └── data/ ├── battle_replay/ # 历史对战回放,JSON序列化格式 └── models/ # checkpoint输出目录单看目录会发现login_bot和battle_env互不依赖,agents只依赖battle_env,这是有意为之。很多类似的工程会把登录逻辑写死在训练脚本里,结果换个账号或者换台机器就翻车。这套工程把登录器和训练环境剥离开,训练时直接用本地CSV回放数据做状态预演,只有真正跑在线自对弈时才会从login_bot反向拿会话。
2.3 配置化设计:环境变量、YAML和CLI入口
三个模块都读config目录下的YAML,而不是散落一堆常量。dev.yaml长这样:
server: origin: "https://game.example.internal" # 模拟的页游网关,示例地址 auth_endpoint: "/auth/ticket" battle_endpoint: "/ws/battle" session: expires_before_refresh: 300 # token到期前300秒主动续期 max_retries: 3 account_pool: - username: "bot001" device_id: "pc-wing-01" # 每个账号绑定设备指纹,防止异地登录 battle: max_steps: 128 single_mode: true # 默认为单精灵对战这里有个容易被忽视的参数:expires_before_refresh。页游登录token的有效期通常比网页端看起来长,但网关会在到期前强制踢下线,所以必须在真正过期前就主动刷新,而不是等请求失败后再补救。后端要传device_id也是常见做法,服务端会把token和设备绑定,同一账号在另一台设备登录,旧token立刻失效。我一般拿到这类资源后,会先把config全部读一遍,再去看代码——配置里的字段能告诉你这个工程的设计边界,比读README快得多。
cli.py入口支持两种模式:
python -m login_bot --config config/dev.yaml --action login python -m login_bot --config config/dev.yaml --action patrol --interval 3600login模式只做一次模拟登录,验证Cookie和token拿到手;patrol模式会进入循环托管,保持会话活跃。训练侧的入口是scripts/train_vs_boss.py,它会先尝试从data/battle_replay加载回放数据,如果不存在就启动登录器在线采集。这个设计让我觉得工程作者很清楚资源的边界:离线数据是首选,在线采集是对冲方案。
3. 登陆器核心实现:模拟登录、token续期与双端会话保活
3.1 登录协议分析:token、签名与时间戳
页游登录和普通网站的登录不太一样,它的核心不是拿到Cookie就完事,而是要经过“身份认证—换取临时票据—用票据换游戏会话”三个步骤。第一步用账号密码去/auth/ticket拿一个短期票据,第二步带票据去/auth/login换取正式token,第三步每一次进战斗、进主城都要带token走业务接口。
签名是第一个坑。服务端会校验时间戳和签名算法,通常在请求参数里加一个client_ts字段,然后把所有参数按字典序拼接后做哈希。这个工程的做法是:
# login_bot/protocol/auth_pb.py import time import hashlib import json def make_sign(params: dict, secret: str) -> str: # 参数拼接前先过滤掉空值,并按键名字典序排序 items = sorted( [f"{k}={v}" for k, v in params.items() if v is not None], key=lambda s: s.lower(), ) raw = "&".join(items) + f"&secret={secret}" return hashlib.sha256(raw.encode("utf-8")).hexdigest() def build_ticket_request(username: str, password_md5: str, device_id: str): ts = int(time.time()) params = { "username": username, "password": password_md5, # 服务端要求MD5摘要,不能明文传输 "device_id": device_id, "client_ts": ts, } params["sign"] = make_sign(params, secret="static_secret_demo") # 密钥在真实工程里从配置注入 return params这段代码里的make_sign函数是全工程签名逻辑的基石。需要注意它对参数做了过滤和排序,这是为了防止服务端校验签名时因为参数顺序不一致而误判。password传的是MD5摘要而不是原文,这是页游网关的普遍要求。secret默认值只是兜底,实际运行从config读取,我对这类硬编码密钥一般建议加个环境变量覆盖,免得泄露后整个签名体系失效。
3.2 会话管理代码:SessionAuthenticator与keep_alive
拿到票据之后,后面所有操作都走requests.Session。Session里会自动维护Cookie,但页游的做法是既认Cookie也认Header里的X-Auth-Token,两套机制需要同时维护。核心代码如下:
# login_bot/core/session.py import time import requests class SessionAuthenticator: def __init__(self, base_url: str, expires_before_refresh: int = 300): self.base_url = base_url.rstrip("/") self.session = requests.Session() self.expires_before_refresh = expires_before_refresh self.token = None self.token_expires_at = 0 def login(self, username: str, password_md5: str, device_id: str) -> str: # 第一步:换票据 ticket_resp = self.session.post( f"{self.base_url}/auth/ticket", json=build_ticket_request(username, password_md5, device_id), timeout=10, ) ticket = ticket_resp.json().get("ticket") # 第二步:用票据登录,拿到正式token与过期时间 login_resp = self.session.post( f"{self.base_url}/auth/login", json={"ticket": ticket, "device_id": device_id}, headers={"X-Requested-With": "XMLHttpRequest"}, timeout=10, ) data = login_resp.json() self.token = data["access_token"] # 服务端返回的expires_in是秒数,本地做一个到期时间戳 self.token_expires_at = time.time() + int(data["expires_in"]) return self.token def ensure_token(self) -> str: # 如果token快过期,则用旧token走refresh接口续期 if self.token is None: raise RuntimeError("not logged in yet") if time.time() > self.token_expires_at - self.expires_before_refresh: resp = self.session.post( f"{self.base_url}/auth/refresh", json={"refresh_token": self.refresh_token}, timeout=10, ) data = resp.json() self.token = data["access_token"] self.token_expires_at = time.time() + int(data["expires_in"]) return self.token逻辑上就是两个阶段:login负责初始认证,ensure_token负责透明续期。后者是全文精华所在,因为页游的长连接任务(比如挂机打BOSS)很容易跨过token的生命周期,没有这个方法,每次续期都要重新输入密码。构造时传入的expires_before_refresh是提前量,我通常设成服务端过期时间的十分之一,太短会偶发断线,太长又消耗过多刷新机会。
3.3 双端保活与异常重连
页游战斗走的是WebSocket通道,但服务端会在60秒无消息时主动断连。工程里heartbeat.py的主要职责就是每30秒发一次ping,并监听on_close事件做指数退避重连。我在改造时做过一次类似的实现,经验之谈是:断线重连后不能直接沿用旧token,必须先调ensure_token再重新发起WebSocket握手。
# login_bot/core/heartbeat.py import time import websocket class BattleSocket: def __init__(self, session: SessionAuthenticator): self.sess = session self.ws = None self.retries = 0 def connect(self, battle_room: str): token = self.sess.ensure_token() url = f"wss://game.example.internal/ws/battle/{battle_room}?token={token}" # 关闭自动重连,交给下面的loop控制,避免重复握手导致会话打架 self.ws = websocket.WebSocketApp( url, on_message=self._on_message, on_error=self._on_error, ) self.ws.run_forever(ping_interval=30, ping_timeout=5) def _on_error(self, ws, error): # 底层websocket库会在ping_timeout后触发error,走退避重连 time.sleep(min(2 ** self.retries, 30)) self.retries += 1这段代码里有三个细节值得关注。run_forever里带上ping_interval和ping_timeout,相当于把心跳交给库底层去做,比自己在消息循环里发ping更稳。断线后先sleep再重连,用指数退避避免服务端还没释放旧连接时客户端就疯狂握手。retries没有做上限,实际部署时通常配一个最大重试次数,超过就放弃并写日志,避免死循环。
4. 强化学习环境封装:把页游回合制对战改造成gym.Env
4.1 状态空间怎么设计:32维观测向量的来源
页游的回合制对战看起来复杂,但抽象成强化学习问题后,状态就是一场对局里的公开信息集合。这个工程把每一步的状态编码成32维浮点向量,我用一个表格拆开看:
| 组别 | 维度 | 来源 | 说明 |
|---|---|---|---|
| 己方精灵 | 8维 | HP比例、PP值、速度种族值、能力等级、异常状态位等 | 所有值归一化到0~1 |
| 对手精灵 | 8维 | 同上,但对手的PP只能根据其使用过的技能做贝叶斯估计 | 隐藏信息用概率近似 |
| 场地环境 | 8维 | 天气、地形、我方强化回合数、对手强化回合数 | 影响技能伤害系数 |
| 历史动作 | 4维 | 上一步我方动作与对方动作的one-hot稀疏编码 | 帮助智能体学习节奏 |
| 全局信息 | 4维 | 当前回合数、双方剩余精灵数、双方血线差 | 给终止状态做提前量 |
其中有些观测是部分可观测的,比如对手换精灵后技能未知,只能通过伤害反推种族值。工程的做法是把这些未知量初始化为0.5的中性值,随着对战推进用贝叶斯公式逐步修正。这个思路在训练初期会让收敛变慢,但比全部置0要科学,因为动作选择对中性值的敏感度更低。
4.2 动作空间与非法动作掩码
动作空间是离散的,默认配置下固定为13维:6个技能位、4个后备精灵位、2个道具位、1个逃跑。第4.1节提到观测是部分可观测的,而动作合法与否又依赖观测。比如己方精灵HP归零,则6个技能位和2个道具位全部非法;后备精灵阵亡,则对应精灵位非法。工程里用了一个mask数组来屏蔽非法动作,下面是核心实现:
# battle_env/actions.py import numpy as np def build_action_mask(state) -> np.ndarray: mask = np.ones(13, dtype=bool) hp = state[0] # 己方精灵HP比例 skill_pp = state[1:7] # 六个技能的PP剩余 # 技能位:HP为0或PP为0时非法 for i in range(6): if hp <= 0.0 or skill_pp[i] <= 0.0: mask[i] = False # 后备精灵位:阵亡标记从观测的第16~19维读取 for j in range(4): if state[16 + j] <= 0.0: mask[6 + j] = False # 道具位:道具数量为0时非法 mask[10:12] = state[20:22] > 0.0 return mask这个mask会被同时喂给智能体和环境。智能体的网络结构里,最后一层softmax之前会先加上一个极小的负数偏置,把非法动作的概率压到接近0,而不是直接屏蔽掉,否则梯度更新时对应维度会变成死神经元。环境侧则在step函数里再做一次硬校验,万一智能体输出了非法动作,就按错误处理并给出-0.5的惩罚。
4.3 奖励函数与step循环
奖励函数是强化学习里最玄学的部分,这套工程给出的设计很朴素,但训练效果稳定:掉血惩罚、击杀奖励、回合成本、道具使用成本四部分组合,每部分都在1秒内完成归一化。
# battle_env/rewards.py def compute_reward(prev_state, state, action, is_done, info): reward = 0.0 # 1. 血线变化:用双方HP比例的差来估计局势变化 my_hp_delta = state[0] - prev_state[0] opp_hp_delta = state[8] - prev_state[8] reward += 1.0 * (opp_hp_delta - my_hp_delta) # 2. 击杀奖励与阵亡惩罚 if state[8] <= 0.0 and prev_state[8] > 0.0: reward += 5.0 if state[0] <= 0.0 and prev_state[0] > 0.0: reward -= 5.0 # 3. 回合成本:每回合小额惩罚,鼓励尽快结束对战 reward -= 0.02 # 4. 道具使用成本:用道具说明用普通技能打不动了,给一个负反馈 if 10 <= action < 12: reward -= 0.3 return max(min(reward, 10.0), -10.0) # 最后做一个clip,防止单步奖励过大血线差的计算方式很讲技巧。直接用双方绝对HP变化,会因为精灵种族值不同而失衡,一个满级精灵掉500血和低级精灵掉500血完全不是一回事。所以这里先算比例再做差分,逻辑上是“如果对手掉血比我多,就认为这一步赚了”。击杀给5分而常规掉血只有1分,是为了让智能体敢于放弃血量优势去抢击杀,避免出现两边耗到天荒地老的保守局。clip上限设10,是为了防止出现连续击杀时奖励爆炸,这一点在训练曲线里会有明显反映。
4.4 环境主循环:reset与step的衔接
# battle_env/env.py import gym class BattleEnv(gym.Env): def __init__(self, battle_config: dict): super().__init__() self.action_space = gym.spaces.Discrete(13) self.observation_space = gym.spaces.Box( low=0.0, high=1.0, shape=(32,), dtype=float ) self.max_steps = battle_config.get("max_steps", 128) def reset(self): # 重置对局,从回放数据中随机抽取一场作为开局 self.state = self._sample_start_state() self.current_step = 0 return self.state def step(self, action): # 执行动作,推进一个回合,然后交换视角(我方动作+对手动作) mask = build_action_mask(self.state) if not mask[action]: penalty = -0.5 self.current_step += 1 return self.state, penalty, False, {"invalid_action": True} prev_state = self.state.copy() self.state, reward_base = self._simulate_round(action) reward = compute_reward( prev_state, self.state, action, self.current_step >= self.max_steps, {}, ) done = self.state[0] <= 0.0 or self.state[8] <= 0.0 return self.state, reward, done, {}关键在step函数的视角交换逻辑,每次step会模拟我方动作和对手动作两个决策,所以外界调用一次,环境内部实际走了两个回合。这么设计的好处是强化学习训练时步数消耗减半,训练速度快很多;坏处是必须保证动作的时间顺序正确,如果对手是先手,我方后手,对手动作和解必须插在我方动作之前。这个顺序在回放数据格式里有明确标注,训练时也会校验。
5. 实战排查与避坑:从HTTP 403到reward爆炸的5个翻车现场
5.1 HTTP 403:同源校验与token刷新策略
现象是模拟登录成功后,第一次调用业务接口直接返回403,重启程序也一样报错。后来抓包发现,页游网关除了校验token,还校验请求头里的Origin和Referer,二者必须和登录时记录的一致。直接原因是我用的requests请求带了自定义UA,但漏了Origin头;深层原因是网关把带有跨域标识的请求判定为CSRF攻击。解决方法是设计一个统一的header工厂,所有请求都从同一个函数取header,避免散落各处:
def build_common_headers(): return { "User-Agent": "Mozilla/5.0", "Origin": "https://game.example.internal", "Referer": "https://game.example.internal/index.html", "X-Requested-With": "XMLHttpRequest", }从那以后我每次搭建这类模拟登录都会先做一次“最小请求头”实验,即只带必要头部测试接口通过后,再逐步增加到业务需要的完整头,否则一旦混入无关头字段很难排查哑火请求。
5.2 Flash版接口不可用:必须切HTML5协议入口
拿到资源第一次跑训练脚本,发现环境一直卡在reset阶段,回放数据加载正常但进入对局失败。原因是这个页游的经典入口是Flash组件,Flash接口直接以二进制AMF协议通信,requests无法直接调用;而新版的HTML5入口虽然界面一样,底层协议变成了JSON。解决方法是启动时检测当前页面是Flash模式还是HTML5模式,然后动态切换协议解析器。工程里protocol/battle_pb.py里同时留有两条编解码路径,通过config里的client_mode字段选择。踩这个坑的教训是:拿一个带“登陆器”字样的资源,第一反应是看它的网络层是抓Flash还是抓HTML5,这决定了你后续所有工作是否有意义。
5.3 reward爆炸:除零与未归一化
训练跑到第2000个episode时,loss曲线突然出现一个尖峰,随后整个Q网络输出变成NaN。逐行打印reward后定位到compute_reward里一个隐藏问题:当精灵被换下时,血线比例计算出现0/0。原因是换精灵动作发生后,新上场的精灵HP比例是满血1.0,prev_state里旧精灵的HP比例是0.0,但代码里做差分时,两个精灵种族值不同,血线绝对变化不能直接比。修复方式很简单,换精灵时跳过血线差分:
if 6 <= action < 10: reward += 0.0 # 换精灵不计算血线差,只给动作成本同时把reward做了一次clip,保证任何单步回报都不会超过正负10。除零问题最稳妥的解法是给所有除法分母加上一个1e-6的epsilon,但这里的根本问题是语义错误,加epsilon只是掩盖症状。
5.4 非法动作导致环境崩溃:动作掩码不是可选项
刚开始我自己写了一个不带mask的版本,训练时智能体频繁对已经阵亡的后备精灵发起换人指令,环境直接在simulate_round里抛KeyError。我以为是环境模拟器有bug,调试很久后才发现问题不在模拟器,而是动作空间定义存在概念性漏洞:固定动作空间是13维,但每维的合法性随状态动态变化。动作掩码的作用不是加速收敛,而是保证环境本身不崩溃。正确做法是像4.2节那样在step入口就用build_action_mask做硬校验,非法动作不进入模拟器,而是直接返回负惩罚。我把这个校验从训练端挪到环境端之后,错误立刻消失,因为训练脚本可以计算出正确的合法动作集合。
5.5 WebSocket静默断连:心跳与重连
训练脚本跑在线采集模式时,经常在40分钟以后就收不到对战消息,但log里没有任何报错。抓包发现服务端在60秒无消息时断开TCP连接,而websocket库默认不主动探测链路,表现就是消息通道静默死亡。解决方法是启用ping_interval和ping_timeout,并配合第3.3节的指数退避重连。这里有个细节:重连后必须重新从服务端拉取当前的回合状态,而不是接着旧状态继续训练,否则智能体的观测会错位,reward计算也跟着错。我一般会在重连成功时强制做一次reset,宁可丢掉一小段训练数据,也不让状态错位污染整个经验回放池。
6. 二次开发思路:给智能体加规则先验,从打BOSS到打巅峰赛
6.1 规则先验混合策略
拿到这套资源后,最容易见效的第一个改造方向,是在训练启动阶段给DQN加一个教练策略。纯强化学习在回合制对战里前期完全是盲打,翻车率极高,但如果先在动作选择时用30%概率执行规则策略——比如血线低于30%时倾向换精灵,对手属性克制我方时优先放强化技能——收敛速度能提升一个量级。代码实现是在epsilon-greedy里插入一段规则分支:
# agents/mixin_rules.py def choose_with_rules(state, mask, net, eps): # 侦察到对手阵亡,强制补刀而不随机 if state[8] <= 0.0: return 0.0, 0 # 我方血量低于阈值,优先换精灵 if state[0] < 0.3 and mask[6:10].any(): return 0.15, int(np.argmax(mask[6:10].astype(int))) # 否则走正常的epsilon随机策略 return eps, None规则先验的权重不宜过高,否则智能体会养成依赖教练的懒惰策略,一旦规则覆盖不到的场面就原形毕露。我一般把规则介入概率压到15%左右,让智能体有足够空间自己摸索。
6.2 从单精灵到3V3阵容
当前环境的单精灵模式只是简化版本,二次开发时把它改成3V3会更有游戏性。主要改动落在状态观测上:观测向量不再是32维,而需要按6只精灵扩充到64维,动作空间也要从13维变成按当前精灵动态切换的机制。这里有个惯用技巧:把后备精灵的全部信息塞进观测,但动作只对在场精灵开放,等措施模型间接学会“什么时候换人”。改完记得重跑一遍回放数据解析脚本,确认CSV格式能容纳多精灵字段,否则训练端和回放端字段对不齐,模型喂进去就是乱码。
6.3 验证方法与一处返工经验
改完之后不要直接上巅峰赛测试,先用附带的历史回放做离线评测——跑100局,统计胜率、平均回合数、道具使用次数,然后再开在线对局对比。这套验证链路是:scripts/dump_battle_log.py把回放做成CSV,agents/dqn.py加载CSV做离线K步评测,最后才走在线模式。记得把训练起始的随机种子固定,否则两次实验选动作的初始权重不同,很难判断改动是有效还是玄学。我这里返工过一次:把3V3的观测维度做错了,忘了把每只精灵的PP值也加进去,模型训练了三天发现胜率反而低于原版,查了很久才发现是状态拼接顺序错误。从那以后我每次改观测空间的第一个动作,就是先打印出观测张量的逐字段说明,对照协议文档逐项核对,再启动训练。希望帮到你。
本文还有配套的精品资源,点击获取