配置见 pyproject.toml:
- line-length=100:贴着现有代码定。若用默认的 88 会重排 122 行,
噪音淹没真正有价值的修复。
- 启用 E/W/F/I/UP/B/C4/SIM/RET/PERF/RUF;**不**启用 PTH。因为本项目的路径
多是"字符串身份"——要塞进 subprocess 命令行、写进 JSON 存档、打印给用户,
全换 pathlib 只是包一层再拆一层,换来 70 处纯风格改动。
- 忽略 RUF001/002/003:中文全角标点是正确写法,不是"歧义字符"。
- 忽略 PERF203:剩下两处都是"逐项容错"语义(逐个牧场存档要立刻上报失败、
收获全部要跳过失败地块)。把 try 提到循环外会毁掉"一项失败不影响其它项"
这个行为,是错的优化。
自动修复 358 项:Dict/List/Optional 换成 PEP 585/604 写法、导入排序、
失效的 noqa、空占位 f-string 等。
手工修复的(这些是真问题,不是风格偏好):
- B904 x9:except 中 raise 补异常链。刻意区分——网络/IO 失败用 from exc 保留
根因;参数解析失败用 from None,因为底层 ValueError 对玩家只是噪音。
- RUF012 x2:StateView 的 plots/animals 原本是**类级共享可变列表**,
改成在 __init__ 里初始化(apply({}) 会提前返回,所以必须显式摆好)。
- F841:TUI 里一个赋值后再没用过的死变量。
- SIM105 x11:try/except/pass 统一改成 contextlib.suppress。
- SIM115:日志句柄改用 with。Popen 已把 fd 复制给子进程,父进程可安全关闭。
- E501 x7:拆超长行;main.py 那处顺手提取变量,避免超长嵌套表达式。
- E741:把 l/r 改成 left_line/right_line。
- 其余:RUF046 / PERF401 / RET504 / RUF005 / UP031 / B007。
ruff format 重排 24 个文件。
验证:97 项测试全绿。
106 lines
4.2 KiB
Python
106 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
||
"""WebSocket 通道演示脚本:证明"服务器推、客户端拉/发"这一对方向都通。
|
||
|
||
它同时是一个"外部客户端"的样板——完全不知道游戏内部结构,只知道协议:
|
||
|
||
连接 → 收 welcome(含状态)→ 发一个动作 → 收 result 回执 → 收 state 推送
|
||
|
||
用法:
|
||
python tools/ws_demo.py --url http://127.0.0.1:8765 --farm demo --seconds 8
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import asyncio
|
||
import json
|
||
import os
|
||
import sys
|
||
import urllib.parse
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
|
||
|
||
def ws_url(endpoint: str, farm: str, token: str | None) -> str:
|
||
parts = urllib.parse.urlsplit(endpoint if "://" in endpoint else "http://" + endpoint)
|
||
scheme = "wss" if parts.scheme == "https" else "ws"
|
||
query = {"farm": farm, "name": "ws-demo"}
|
||
if token:
|
||
query["token"] = token
|
||
return f"{scheme}://{parts.netloc}/ws?{urllib.parse.urlencode(query)}"
|
||
|
||
|
||
async def run(endpoint: str, farm: str, token: str | None, seconds: float) -> int:
|
||
import websockets
|
||
|
||
url = ws_url(endpoint, farm, token)
|
||
print(f"连接 {url}")
|
||
async with websockets.connect(url, open_timeout=8) as socket:
|
||
welcome = json.loads(await socket.recv())
|
||
state = welcome.get("state", {})
|
||
print(
|
||
f"← welcome:牧场 {welcome['farm']} · 第 {state.get('day')} 天 · "
|
||
f"金币 {state.get('gold')} · 候选动作 {len(welcome.get('actions', []))} 个"
|
||
)
|
||
|
||
async def sender() -> None:
|
||
"""通过 WebSocket 发一个动作,等回执。"""
|
||
await asyncio.sleep(0.3)
|
||
request = {"type": "action", "id": 1, "action": "sleep", "params": {}}
|
||
print(f"→ action:{json.dumps(request, ensure_ascii=False)}")
|
||
await socket.send(json.dumps(request))
|
||
|
||
deadline = asyncio.get_event_loop().time() + seconds
|
||
send_task = asyncio.create_task(sender())
|
||
counts: dict[str, int] = {}
|
||
try:
|
||
while asyncio.get_event_loop().time() < deadline:
|
||
try:
|
||
raw = await asyncio.wait_for(socket.recv(), timeout=seconds)
|
||
except asyncio.TimeoutError:
|
||
break
|
||
message = json.loads(raw)
|
||
kind = message.get("type", "?")
|
||
counts[kind] = counts.get(kind, 0) + 1
|
||
if kind == "result":
|
||
print(
|
||
f"← result(id={message.get('id')}):{message.get('message')} "
|
||
f"[通过 WebSocket 提交的动作,ok={message.get('ok')}]"
|
||
)
|
||
elif kind == "state":
|
||
snapshot = message.get("state", {})
|
||
print(
|
||
f"← state(seq={message.get('seq')}):第 {snapshot.get('day')} 天 · "
|
||
f"金币 {snapshot.get('gold')} · 行动点 {snapshot.get('ap')} · "
|
||
f"在线 {message.get('online')}"
|
||
)
|
||
elif kind == "day":
|
||
print(f"← day:进入第 {message.get('day')} 天,事件 {message.get('events')}")
|
||
elif kind == "notice":
|
||
print(f"← notice:{message.get('text')}")
|
||
else:
|
||
print(f"← {kind}")
|
||
finally:
|
||
send_task.cancel()
|
||
|
||
print(f"\n消息统计:{counts}")
|
||
if counts.get("result", 0) < 1 or counts.get("state", 0) < 1:
|
||
print("✗ WebSocket 通道没有按预期工作(result / state 消息缺失)")
|
||
return 1
|
||
print("✓ WebSocket 双向通道正常:动作可上行,状态会主动下行")
|
||
return 0
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description="WebSocket 通道演示")
|
||
parser.add_argument("--url", default="http://127.0.0.1:8765")
|
||
parser.add_argument("--farm", default="ws-demo")
|
||
parser.add_argument("--token", default=None)
|
||
parser.add_argument("--seconds", type=float, default=8.0)
|
||
args = parser.parse_args()
|
||
return asyncio.run(run(args.url, args.farm, args.token, args.seconds))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|