#!/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())