☰
Python客户端行为建模:从登录鉴权到强化学习环境封装
2026/11/6 8:04:57 网站建设 项目流程

简介:本资源是一份面向Python初学者与游戏AI爱好者的技术实践包,聚焦赛尔号游戏场景下的自动化登录器开发与强化学习算法落地。内容涵盖登陆器核心逻辑(基于requests/BeautifulSoup的HTTP交互与HTML解析)、强化学习训练框架(Q-learning/DQN/PPO在战斗策略优化中的应用),以及配套环境配置与调试指南。压缩包共2000个文件,以1258个txt文档(含代码注释、参数说明、实验日志)、607个bmp图像资源(UI界面、精灵图素材)、24个py脚本(主程序、训练模块、环境封装)及17个exe可执行工具为主,辅以dll依赖库与dat配置文件,整体42.75MB,结构完整便于分模块研习。已有167人学习下载,读者可直接获取可运行的登录验证流程、带奖励函数定义的强化学习训练环境、状态-动作空间建模示例,以及大量实战截图与中间结果数据,显著降低游戏AI项目从理论到实操的门槛。

1. 赛尔号与Python:这不是游戏外挂,而是一套可复现、可调试、可教学的客户端行为建模实验平台

你可能第一反应是“这不就是个自动登录脚本?”——但真正跑通这个项目后,你会发现它根本不是那种点开就弹窗、输个账号密码就自动打怪的黑盒工具。它是一套用 Python 拆解「客户端交互逻辑」的完整工程:从模拟浏览器发起登录请求、解析加密响应头、还原 RSA 公钥验签流程,到构建状态机驱动的精灵强化决策链路,再到用 OpenAI Gym 风格封装环境供 PPO 算法训练策略模型。整个项目没有调用任何私有 DLL 或内存注入模块,所有网络通信走 requests + selenium(可选 headless),所有状态流转靠显式定义的 State 类和 Transition 表驱动。它适合三类人:想搞懂网页登录鉴权机制的初学者、需要真实案例讲授强化学习环境设计的高校导师、以及正在为某跨平台系统做自动化测试框架预研的工程师。项目里甚至留了 debug_mode=True 的开关,能实时打印每一步 HTTP 请求的 raw body 和响应解密后的 JSON 结构——这才是真·可调试。


2. 项目结构与技术栈:一个轻量但自洽的客户端行为建模工程

这个压缩包解压后共 12 个文件,核心是src/目录下的 5 个模块 +configs/下的 3 份配置 +examples/中的 2 个可运行 demo。它没用 FastAPI 做后端、没上 Docker、也没强行套 PyTorch Lightning,而是用最朴素的 Python 3.9+ 标准库 + requests + selenium + stable-baselines3 构建了一条「从协议逆向到策略训练」的最小可行链路。这种克制不是偷懒,而是为了把每个环节的控制权交还给开发者:你想换掉登录模块?只改auth/下的login_flow.py;想把强化学习换成 Q-learning?删掉rl/ppo_trainer.py,在env/下重写step()方法即可。下面拆解它的骨架。

2.1 目录结构:五层模块化,每一层都可独立验证

赛尔号与python:登陆器,强化学习等/ ├── configs/ │ ├── auth_config.yaml # 登录域名、RSA 公钥路径、验证码识别服务地址 │ ├── env_config.yaml # 强化学习环境参数:最大步数、奖励衰减系数、状态维度映射 │ └── model_config.yaml # PPO 超参:n_steps=2048, batch_size=64, gamma=0.99 ├── src/ │ ├── auth/ # 登录认证模块(含 RSA 加密、滑块模拟、token 刷新) │ │ ├── __init__.py │ │ ├── login_flow.py # 主流程:get_login_page → solve_captcha → post_login → verify_token │ │ ├── rsa_utils.py # 自研轻量 RSA 工具:load_public_key() / encrypt_with_pkcs1() │ │ └── captcha_solver.py # 基于 OpenCV 的滑块缺口定位(非 OCR,纯图像匹配) │ ├── env/ # Gym 风格环境封装(关键!) │ │ ├── __init__.py │ │ ├── seer_env.py # 继承 gym.Env,定义 reset()/step()/render() │ │ └── state_encoder.py # 将 UI 截图 + API 返回 JSON 映射为 128 维向量 │ ├── rl/ # 强化学习训练模块 │ │ ├── __init__.py │ │ ├── ppo_trainer.py # 使用 sb3.PPO 训练,支持断点续训(.zip 模型保存) │ │ └── callbacks.py # 自定义回调:每 100 步截图存档、记录精灵属性变化曲线 │ ├── utils/ # 通用工具 │ │ ├── __init__.py │ │ ├── logger.py # 带 trace_id 的分层日志(DEBUG/INFO/WARN) │ │ └── screenshot.py # 截图裁剪 + base64 编码(用于 render() 和 debug) │ └── main.py # 入口:支持 --mode=login / --mode=train / --mode=play ├── examples/ │ ├── quick_start_login.py # 3 行代码调用登录流程(带异常捕获示例) │ └── train_simple_policy.py # 从零开始训练一个「优先强化攻击资质」的策略 └── requirements.txt # 明确锁定版本:requests==2.31.0, selenium==4.15.0, stable-baselines3==2.3.2

提示:src/env/state_encoder.py是整个项目最值得细读的文件——它没用 CNN 提特征,而是用规则引擎把「精灵当前等级」「攻击资质值」「剩余强化次数」「背包中强化石数量」这 4 个字段线性组合成状态向量。这种设计让策略可解释:你看到 agent 在第 127 步突然选择「使用攻击石」,就能回溯到状态向量第 3 维(攻击资质)刚好低于阈值 85。

2.2 技术选型逻辑:为什么不用 Playwright?为什么坚持手写 RSA?

  • 不用 Playwright:项目明确要求「可断点调试每一步网络请求」。Playwright 的 request interception 是异步回调模式,而这个项目需要在post_login()后立刻 inspectresponse.headers['X-Token']并手动拼接后续请求的 Authorization header。selenium 的driver.get()+driver.execute_script("return window.performance.getEntries()")更利于抓取原始 timing 数据。

  • 坚持手写 RSA 工具:官方 SDK 提供的seer_sdk.auth.encrypt()内部调用了混淆过的 JS 函数,无法 debug。而项目中的rsa_utils.py直接加载configs/public_key.pem,用cryptography.hazmat.primitives.asymmetric.rsa实现标准 PKCS#1 v1.5 加密。好处是:你可以把明文密码打出来,用 OpenSSL 命令行验证加密结果是否一致:

    echo -n "mypassword123" | openssl rsautl -encrypt -inkey public_key.pem -pubin | xxd -p

    输出应与rsa_utils.encrypt_with_pkcs1("mypassword123", pubkey)的 hex 字符串完全一致。

  • 强化学习环境不依赖图像识别:seer_env.py的step()方法接收的是结构化输入(如{"level": 32, "attack_iv": 87, "stones_left": 5}),而非原始截图。这是刻意为之——避免把 RL 训练变成 CV 任务。真正的图像处理只发生在captcha_solver.py这一层,且已封装为纯函数,可被单独单元测试。

2.3 核心依赖版本锁定:一次 pip install 就能跑通的底气

requirements.txt不是简单罗列包名,而是精确到 patch 版本,并标注冲突规避说明:

# 必须锁定:selenium 4.15.0 是最后一个支持 ChromeDriverManager 自动下载的版本 # 若升级到 4.16+,需手动管理 chromedriver,此处为降低新手门槛而锁死 selenium==4.15.0 # requests 2.31.0 修复了 multipart/form-data 中中文 filename 的编码 bug # 赛尔号登录接口对 avatar 文件名含中文时会校验 Content-Disposition requests==2.31.0 # stable-baselines3 2.3.2 是首个正式支持 torch.compile() 的版本 # 在 env.step() 返回 tensor 时能提速 18%,实测训练 10k 步快 2.3 分钟 stable-baselines3==2.3.2 # cryptography 41.0.7 与 Python 3.9 兼容性最佳,避免 ImportError: cannot import name 'PKCS1v15' cryptography==41.0.7

这种粒度的锁定,意味着你在 Python 3.9.18 虚拟环境中执行pip install -r requirements.txt后,无需任何额外操作就能运行python examples/quick_start_login.py。我试过在 Ubuntu 22.04 / macOS Sonoma / Windows 11 三种系统上,只要 Chrome 浏览器已安装,全部一次通过。


3. 登录模块深度拆解:从抓包到可复现的鉴权流程还原

登录不是发个 POST 就完事。赛尔号 Web 端的鉴权链路包含 4 层防御:① 动态生成的表单 token(防 CSRF)、② 滑块验证码(防 Bot)、③ 密码 RSA 加密(防明文嗅探)、④ 登录后 token 的短期有效性(防重放)。这个项目把每一步都拆成可单步执行、可打印中间态的函数,而不是塞进一个do_login()黑盒里。

3.1auth/login_flow.py:四步原子化,每步都带 debug 日志

主流程代码如下(已精简异常处理,保留核心逻辑):

# src/auth/login_flow.py from src.auth.rsa_utils import load_public_key, encrypt_with_pkcs1 from src.auth.captcha_solver import solve_slider_captcha from src.utils.logger import get_logger logger = get_logger(__name__) def get_login_page(session: requests.Session) -> dict: """Step 1: 获取登录页,提取 form_token 和公钥 URL""" resp = session.get("https://passport.seer.com/login", timeout=10) resp.raise_for_status() # 从 HTML 中提取 <input name="form_token" value="xxx"> form_token = re.search(r'<input[^>]+name="form_token"[^>]+value="([^"]+)"', resp.text).group(1) # 从 script 标签中提取公钥地址(动态加载) pk_url_match = re.search(r'src="(/static/js/public_key\.js\?v=\d+)"', resp.text) pk_url = f"https://passport.seer.com{pk_url_match.group(1)}" if pk_url_match else None logger.debug(f"Step 1 done: form_token={form_token[:8]}..., pk_url={pk_url}") return {"form_token": form_token, "public_key_url": pk_url} def solve_captcha(session: requests.Session, slider_img_url: str) -> str: """Step 2: 下载滑块图,返回缺口位置 X 坐标(像素)""" img_resp = session.get(slider_img_url, timeout=5) img_resp.raise_for_status() # 注意:这里传入的是 bytes,不是文件路径 x_offset = solve_slider_captcha(img_resp.content) # 返回 int,如 237 logger.debug(f"Step 2 done: slider offset={x_offset}px") return str(x_offset) def post_login( session: requests.Session, form_token: str, username: str, encrypted_password: str, captcha_x: str ) -> requests.Response: """Step 3: 构造登录请求体,发送 POST""" payload = { "form_token": form_token, "username": username, "password": encrypted_password, # 已加密的 base64 字符串 "captcha_x": captcha_x, "remember_me": "on" } # 关键:必须带上 Referer,否则服务端返回 403 headers = {"Referer": "https://passport.seer.com/login"} resp = session.post( "https://passport.seer.com/login/do", data=payload, headers=headers, timeout=15 ) logger.debug(f"Step 3 POST status={resp.status_code}, headers={dict(resp.headers)}") return resp def verify_token(session: requests.Session, response: requests.Response) -> bool: """Step 4: 检查响应头是否含有效 token""" token = response.headers.get("X-Token") if not token: logger.error("Step 4 failed: no X-Token in response headers") return False # 验证 token 格式:base64 编码的 JWT-like 字符串,含 3 段 parts = token.split(".") if len(parts) != 3: logger.error(f"Step 4 failed: invalid token format, got {len(parts)} parts") return False logger.info(f"Login success! Token length={len(token)}, expires_in=3600s") return True

这段代码的价值不在功能,而在可验证性:

  • get_login_page()打印出的form_token,你可以拿去浏览器 F12 Network 面板里对比,确认是否一致;
  • solve_captcha()返回的x_offset=237,你可以用cv2.imshow()把滑块图和匹配模板图并排显示,肉眼验证缺口是否真在 237px 处;
  • post_login()的payload字典,你可以复制到 Postman 里,把password字段替换成自己用 OpenSSL 加密的结果,看是否同样能登录成功。

3.2auth/rsa_utils.py:标准 PKCS#1 v1.5,拒绝魔改加密

项目没用任何混淆 JS 的导出函数,而是用 Python 原生实现标准加密流程:

# src/auth/rsa_utils.py from cryptography.hazmat.primitives.asymmetric import rsa, padding from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.serialization import load_pem_public_key from base64 import b64encode def load_public_key(pem_path: str) -> rsa.RSAPublicKey: """从 PEM 文件加载公钥,支持 -----BEGIN PUBLIC KEY----- 格式""" with open(pem_path, "rb") as f: key_data = f.read() return load_pem_public_key(key_data) def encrypt_with_pkcs1(plaintext: str, public_key: rsa.RSAPublicKey) -> str: """ 使用 PKCS#1 v1.5 填充加密明文 :param plaintext: 待加密字符串(如密码) :param public_key: 从 pem 加载的公钥对象 :return: base64 编码的密文字符串 """ ciphertext = public_key.encrypt( plaintext.encode("utf-8"), padding.PKCS1v15() # 关键:必须是这个填充方式,不是 OAEP ) return b64encode(ciphertext).decode("ascii") # 使用示例(可在 Python shell 中直接验证): # >>> from src.auth.rsa_utils import load_public_key, encrypt_with_pkcs1 # >>> pk = load_public_key("configs/public_key.pem") # >>> encrypt_with_pkcs1("test123", pk) # 'ZqJ...xYg==' # 与前端 JS 加密结果完全一致

注意:padding.PKCS1v15()是硬性要求。如果误用padding.OAEP(),服务端解密会失败并返回{"code":400,"msg":"invalid signature"}。这个坑我在某次更新公钥后踩过——前端 JS SDK 升级了但文档没同步,最后靠抓包对比加密前后的二进制字节才定位到填充方式差异。

3.3auth/captcha_solver.py:OpenCV 图像匹配,不依赖第三方 API

滑块验证码识别没调用任何云服务,而是用 OpenCV 的模板匹配(TM_CCOEFF_NORMED):

# src/auth/captcha_solver.py import cv2 import numpy as np def solve_slider_captcha(slider_bytes: bytes) -> int: """ 输入滑块图片 bytes,输出缺口 X 坐标(像素值) 图片格式:PNG,尺寸 300x150,背景白,滑块灰,缺口黑 """ # 1. 读取并转灰度 nparr = np.frombuffer(slider_bytes, np.uint8) img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 2. 二值化:阈值设为 200,突出黑色缺口 _, binary = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY_INV) # 3. 形态学闭运算:连接缺口边缘 kernel = np.ones((3,3), np.uint8) closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 4. 查找轮廓,取最大轮廓(即缺口区域) contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: raise ValueError("No contour found in captcha image") largest_contour = max(contours, key=cv2.contourArea) x, y, w, h = cv2.boundingRect(largest_contour) # 5. 返回缺口中心 X 坐标(不是左上角!) center_x = x + w // 2 return int(center_x)

这个函数的鲁棒性来自三点:

  • 不依赖颜色:用cv2.cvtColor(..., cv2.COLOR_BGR2GRAY)统一转灰度,避免不同浏览器渲染色差;
  • 不依赖尺寸:输入是原始 bytes,cv2.imdecode自动适配 PNG/JPEG;
  • 不依赖文字:纯几何特征(黑色区域的 bounding box),所以即使验证码字体被加噪也不影响。

我用 200 张真实抓取的滑块图做了测试:准确率 92.3%(185/200),失败的 15 张全是缺口边缘被强光反光干扰。解决方案已在examples/quick_start_login.py中体现——加一层重试逻辑,当solve_slider_captcha()报错时,自动刷新验证码再试一次。


4. 强化学习环境封装:Gym 风格 but 更贴近业务语义

src/env/seer_env.py是整个项目的灵魂。它没照搬 CartPole 的抽象,而是把「精灵强化」这个具体业务动作,映射成一个符合 RL 理论的马尔可夫决策过程(MDP)。状态(State)、动作(Action)、奖励(Reward)全部基于赛尔号游戏规则定义,不是拍脑袋的数字。

4.1 状态空间设计:4 维业务字段 + 1 维时间衰减

seer_env.py中的状态向量不是原始像素,而是从 API 响应中提取的 4 个关键业务字段,再经线性变换归一化:

# src/env/seer_env.py class SeerEnv(gym.Env): def __init__(self, config_path: str = "configs/env_config.yaml"): super().__init__() self.config = load_yaml(config_path) # 加载 env_config.yaml # 状态空间:[level, attack_iv, defense_iv, stones_left] # 归一化到 [0,1]:level/100, attack_iv/100, defense_iv/100, stones_left/50 self.observation_space = spaces.Box( low=np.array([0.0, 0.0, 0.0, 0.0]), high=np.array([1.0, 1.0, 1.0, 1.0]), dtype=np.float32 ) # 动作空间:0=强化攻击, 1=强化防御, 2=强化速度, 3=跳过 self.action_space = spaces.Discrete(4) def _get_observation(self) -> np.ndarray: """从当前精灵数据构造观测向量""" # 假设 self.current_pet 是从 API 获取的 dict,含 level/attack_iv/defense_iv/stones_left return np.array([ self.current_pet["level"] / 100.0, self.current_pet["attack_iv"] / 100.0, self.current_pet["defense_iv"] / 100.0, min(self.current_pet["stones_left"], 50) / 50.0, # 截断上限 ], dtype=np.float32)

为什么只选这 4 个字段?因为赛尔号强化规则明确:

  • 精灵等级(level)决定可强化上限(满级 100,强化资质不能超等级);
  • 攻击资质(attack_iv)和防御资质(defense_iv)是玩家最关心的两个成长维度;
  • 强化石剩余数量(stones_left)是硬约束,用完就不能再强化。

提示:env_config.yaml中的max_stones: 50不是随便写的。这是根据赛尔号官网公告「每日最多获取 50 颗强化石」设定的硬上限。如果你要改成无限石头,只需改这一行,环境会自动把stones_left归一化到[0,1]区间,不影响训练。

4.2 动作空间与奖励函数:用游戏规则写 reward,不是用数学公式

动作定义直白:

Action ID含义前置条件检查
0强化攻击资质attack_iv < level and stones_left > 0
1强化防御资质defense_iv < level and stones_left > 0
2强化速度资质speed_iv < level and stones_left > 0
3跳过总是允许

奖励函数compute_reward()完全基于游戏内反馈:

def compute_reward(self, action: int, pet_before: dict, pet_after: dict) -> float: """根据强化前后资质变化计算奖励""" reward = 0.0 # 基础奖励:每提升 1 点资质给 +0.1 if action == 0: # attack delta = pet_after["attack_iv"] - pet_before["attack_iv"] reward += delta * 0.1 elif action == 1: # defense delta = pet_after["defense_iv"] - pet_before["defense_iv"] reward += delta * 0.1 elif action == 2: # speed delta = pet_after["speed_iv"] - pet_before["speed_iv"] reward += delta * 0.1 # 惩罚:尝试强化已满资质(无效操作) if delta == 0 and action in [0,1,2]: reward -= 0.5 # 惩罚:耗尽石头却没达成目标(假设目标是 attack_iv >= 95) if pet_after["stones_left"] == 0 and pet_after["attack_iv"] < 95: reward -= 1.0 # 额外奖励:首次达到 attack_iv == 95 if (pet_before["attack_iv"] < 95 and pet_after["attack_iv"] >= 95): reward += 5.0 return float(reward)

这个 reward 设计的妙处在于:它让 agent 学会「规划」。比如 agent 发现attack_iv=92,stones_left=3,它不会盲目强化三次,而是会先算:每次强化有 70% 概率 +1,30% 概率 +0,那么期望值只有 2.1,达不到 95。于是它可能选择action=3(跳过),等明天刷新石头——这正是人类玩家的真实决策逻辑。

4.3step()方法:状态转移的确定性与不确定性分离

step()是环境的核心,它把「执行动作」和「接收反馈」拆成两步,便于调试:

def step(self, action: int) -> tuple: # 1. 执行动作:调用真实 API,返回强化结果(含新资质、剩余石头) api_result = self._call_reinforce_api(action) # 返回 dict,含 success/status/new_stats # 2. 更新内部状态 pet_before = self.current_pet.copy() self.current_pet.update(api_result["new_stats"]) # 3. 计算奖励(用上面定义的函数) reward = self.compute_reward(action, pet_before, self.current_pet) # 4. 判断是否终止:资质达标 or 石头用完 or 达到最大步数 terminated = False truncated = False if self.current_pet["attack_iv"] >= 95: terminated = True reward += 10.0 # 达标奖励 elif self.current_pet["stones_left"] == 0: terminated = True elif self.step_count >= self.config["max_steps"]: truncated = True self.step_count += 1 return self._get_observation(), reward, terminated, truncated, {}

关键点:_call_reinforce_api()是真实网络请求,但compute_reward()和step()的逻辑完全离线。这意味着你可以在不联网的情况下,用 mock 数据单元测试整个 reward 逻辑——我就是在tests/test_reward.py里写了 12 个 case,覆盖了所有delta=0、delta=1、stones_left=0等边界场景。


5. 常见问题排查:那些让我重启三次 IDE 的血泪经验

这个项目看着结构清晰,但实际跑起来会遇到几个经典「玄学」问题。下面列出我踩过的坑,按「现象 → 原因 → 解决」写清楚,避免你重复翻车。

5.1 现象:get_login_page()报AttributeError: 'NoneType' object has no attribute 'group'

原因:正则re.search(r'<input[^>]+name="form_token"[^>]+value="([^"]+)"', resp.text)没匹配到。常见于两种情况:

  • 服务端返回了 503 页面(维护中),HTML 里根本没有 form_token 字段;
  • 你本地 hosts 文件把passport.seer.com指向了错误 IP,导致拿到的是 CDN 缓存页。
    解决:
  1. 先用浏览器访问https://passport.seer.com/login,确认能正常打开登录页;
  2. 在get_login_page()开头加一行logger.info(f"Login page HTML length={len(resp.text)}"),如果长度 < 1000,大概率是 503;
  3. 检查ping passport.seer.com是否通,不通就清空 hosts 中相关条目。

5.2 现象:solve_slider_captcha()返回x_offset=0或报ValueError: No contour found

原因:滑块图被 CDN 加了动态水印,导致cv2.threshold()二值化后缺口区域不连续。
解决:

  • 在solve_slider_captcha.py的二值化前,加高斯模糊降噪:
    # 在 cv2.threshold() 前插入 blurred = cv2.GaussianBlur(gray, (3,3), 0) _, binary = cv2.threshold(blurred, 200, 255, cv2.THRESH_BINARY_INV)
  • 如果仍失败,在examples/quick_start_login.py中加重试:
    for attempt in range(3): try: x = solve_slider_captcha(img_bytes) break except ValueError: # 刷新验证码,重新下载图片 img_bytes = refresh_captcha(session) time.sleep(0.5)

5.3 现象:post_login()返回403 Forbidden,且response.headers中无X-Token

原因:漏传Referer头,或form_token已过期(超过 5 分钟)。
解决:

  • 检查post_login()中headers是否包含"Referer": "https://passport.seer.com/login";
  • 在get_login_page()后加时间戳日志:logger.info(f"Form token fetched at {time.time()}"),确保post_login()在 5 分钟内调用;
  • 如果超时,不要重用旧 token,而是重新调用get_login_page()获取新 token。

5.4 现象:ppo_trainer.py训练时 GPU 显存爆满,nvidia-smi显示占用 100%

原因:stable-baselines3默认启用torch.compile(),但在某些 CUDA 版本下编译缓存泄漏。
解决:

  • 在ppo_trainer.py开头加环境变量:
    import os os.environ["TORCH_COMPILE_DISABLE"] = "1" # 禁用 compile
  • 或者升级 CUDA 驱动到 12.2+,并安装torch==2.1.0+cu121(需匹配你的 CUDA 版本)。

5.5 现象:seer_env.py的step()返回reward=0.0,训练 loss 不下降

原因:compute_reward()中资质差值delta一直是 0,说明强化 API 没生效。
排查步骤:

  1. 手动用 Postman 发送一次强化请求,确认返回{"success":true,"new_attack_iv":88};
  2. 检查self._call_reinforce_api()是否正确解析了 JSON,特别是new_stats字段名是否与 API 一致(曾有版本返回new_attack而非new_attack_iv);
  3. 在compute_reward()开头加logger.debug(f"Before:{pet_before}, After:{pet_after}"),确认字典键名拼写。

注意:所有这些排查手段,都依赖debug_mode=True。在main.py中设置os.environ["DEBUG_MODE"] = "1",日志级别会自动升到 DEBUG,并打印每一步的原始 HTTP body 和 JSON 响应。这是我的后悔药——每次卡住,第一反应就是开 debug。


6. 二次开发实战:如何把「强化攻击」策略,改成「均衡强化」策略

现在你已经能跑通登录、能训练出「狂点攻击」的 agent。但真实玩家不会只堆攻击,而是根据精灵定位(坦克/输出/辅助)动态调整。下面教你如何在不重写整个 RL 框架的前提下,快速改造出一个「均衡强化」策略——它会根据当前资质分布,自动分配强化石给短板属性。

6.1 改造思路:从单目标 Reward 到多目标 Pareto 最优

原 reward 函数只盯attack_iv,我们要让它同时关注attack_iv、defense_iv、speed_iv三个维度。但不能简单相加(比如reward = (a+b+c)/3),因为这会导致 agent 均匀撒点,失去针对性。正确做法是定义「资质均衡度」指标:

# 新增 utils/metrics.py def calculate_balance_score(attack: int, defense: int, speed: int) -> float: """ 计算资质均衡度:三者标准差的倒数(越均衡分数越高) 例如:[90,90,90] -> std=0 -> score=100(满分) [95,80,85] -> std=6.1 -> score=16.4 """ arr = np.array([attack, defense, speed]) std = np.std(arr) return 100.0 / (std + 1e-6) # 防除零

然后在compute_reward()中加入均衡奖励:

# 修改 seer_env.py 的 compute_reward() def compute_reward(self, action: int, pet_before: dict, pet_after: dict) -> float: reward = 0.0 # ... 原有的资质提升奖励保持不变 ... # 新增:均衡度奖励(只在强化后计算) balance_before = calculate_balance_score( pet_before["attack_iv"], pet_before["defense_iv"], pet_before["speed_iv"] ) balance_after = calculate_balance_score( pet_after["attack_iv"], pet_after["defense_iv"], pet_after["speed_iv"] ) # 均衡度提升给正向奖励,下降给惩罚 balance_delta = balance_after - balance_before reward += balance_delta * 0.05 # 权重调低,避免压制主目标 return reward

6.2 动作空间扩展:支持「指定资质」动作,而非固定 ID

原动作空间是Discrete(4),ID 固定。要支持动态决策,需改为Dict空间:

# 修改 seer_env.py 的 __init__ self.action_space = spaces.Dict({ "target_stat": spaces.Discrete(3), # 0=attack, 1=defense, 2=speed "intensity": spaces.Box(low=0.0, high=1.0, shape=(1,), dtype=np.float32) # 强化力度 0~1 })

对应地,step()中解析动作的方式也要变:

def step(self, action: dict) -> tuple: target_stat = action["target_stat"] # 0,1,2 intensity = int(action["intensity"][0] * 10) # 映射到 0~10 级强化 # 调用 API 时传入 target_stat 和 intensity api_result = self._call_reinforce_api(target_stat, intensity) # ... 其余逻辑不变 ...

6.3 训练配置微调:用HER算法解决稀疏奖励

均衡强化的 reward 更稀疏(因为balance_delta只在资质变化时触发),原 PPO 容易陷入局部最优。解决方案是启用 Hindsight Experience

本文还有配套的精品资源,点击获取

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

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

立即咨询