Files
SimpleFarm/tools/sse_demo.py
T
snowy-yang 15d290a60a 移除行动点系统,倒计时本地走秒,字体优化
为什么:游戏的时间(40 分钟一天、只能自然流动)本身就是唯一的行动节奏
阀门,再叠一层"每天 10 点行动点"纯属重复惩罚——浇水还要体力既不拟真
也不好玩。删掉之后,稀缺的只剩时间本身,放置玩法的定位反而更纯粹。

改动范围(引擎到界面连根拔):
- config 删 START_AP/AP_COST;models 删 ap 字段与 max_ap,存档 payload
  里的旧 ap 键被 from_dict 自然忽略;
- engine 删 _spend_ap 与全部 8 个调用点、每日恢复逻辑与日志;
  available_actions 不再按行动点筛,cost 只剩金币;
- storage 建表去掉 ap 列,老库在 connect() 里 ALTER TABLE DROP COLUMN
  无痛迁移(实测 saves/farm.db 已自动迁完);
- bot 删 AP 分支(无 AP 后 available_actions 恒可用);client/TUI/CLI/
  README/帮助文本同步清理。

网页端顺带两件事:
- "下一天"倒计时改为客户端本地走秒——SSE 只在有变化时推送,以前倒计时
  只在推送瞬间跳变,现在每秒都在动(记基点 + 1s 定时器补齐);
- 字体优化:页面字体栈补齐 MiSans/HarmonyOS Sans/PingFang,正文字号
  14→15,统计数字 tabular-nums;canvas 小字统一字体栈并加粗提对比度。

测试适配:删 2 个 AP 专属用例(无 AP 可测)、改名/收敛 5 个,
532 项全绿(skip 39),ruff/pyright 干净。真机验证:老库迁移、
统计条无行动点、倒计时逐秒递减(39:04→39:02)、动作不再报 no_ap。
2026-09-25 22:44:08 +08:00

148 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""SSE 通道演示脚本:证明"服务器主动推、客户端用 HTTP 发"这一对方向都通。
它同时是一个"外部客户端"的样板——完全不知道游戏内部结构,只知道协议:
连上 /api/stream → 收 welcome(含状态)→ 用 HTTP 发一个动作 → 收 state 推送
只用标准库(urllib 流式读长连接),所以任何有 Python 的环境都能跑。
用法:
python tools/sse_demo.py --url http://127.0.0.1:8765 --farm demo --seconds 8
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
import urllib.parse
import urllib.request
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from farm import net
def stream_url(endpoint: str, farm: str, token: str | None) -> str:
base = endpoint if "://" in endpoint else "http://" + endpoint
query = {"farm": farm, "name": "sse-demo"}
if token:
query["token"] = token
return f"{base.rstrip('/')}/api/stream?{urllib.parse.urlencode(query)}"
def action_url(endpoint: str, farm: str, token: str | None) -> str:
base = endpoint if "://" in endpoint else "http://" + endpoint
query = {"farm": farm}
if token:
query["token"] = token
return f"{base.rstrip('/')}/api/actions?{urllib.parse.urlencode(query)}"
def post_action(url: str, action: str, params: dict | None = None) -> dict:
"""用普通 HTTP POST 提交动作——上行不占用实时通道。"""
body = json.dumps({"action": action, "params": params or {}}).encode("utf-8")
request = urllib.request.Request(
url, data=body, method="POST", headers={"Content-Type": "application/json"}
)
with net.open_url(request, timeout=8.0) as response:
return json.loads(response.read().decode("utf-8"))
def run(endpoint: str, farm: str, token: str | None, seconds: float) -> int:
url = stream_url(endpoint, farm, token)
print(f"连接 {url}")
request = urllib.request.Request(url, headers={"Accept": "text/event-stream"})
counts: dict[str, int] = {}
acted = False
deadline = time.time() + seconds
# 读超时必须比服务器心跳(15 秒)宽裕,否则空闲时会被自己的超时打断
with net.open_url(request, timeout=45.0) as response:
while True:
try:
raw_line = response.readline()
except (TimeoutError, OSError) as exc:
print(f"(读超时:{exc})")
break
if not raw_line:
print("(服务器关闭了长连接)")
break
line = raw_line.decode("utf-8", errors="replace").strip()
if not line or line.startswith(":"):
continue
if not line.startswith("data:"):
continue
message = json.loads(line[len("data:") :].strip())
kind = message.get("type", "?")
counts[kind] = counts.get(kind, 0) + 1
if kind == "welcome":
state = message.get("state", {})
print(
f"← welcome:牧场 {message.get('farm')} · 第 {state.get('day')} 天 · "
f"金币 {state.get('gold')} · 候选动作 {len(message.get('actions', []))} 个"
)
# welcome 里就带着当前合法动作,挑第一个执行,
# 验证"两条通道改的是同一个引擎"
candidates = message.get("actions") or []
if candidates:
pick = candidates[0]
reply = post_action(
action_url(endpoint, farm, token),
pick["action"],
pick.get("params") or {},
)
acted = True
print(
f"→ POST /api/actions {pick['action']} "
f"{pick.get('params') or {}}:{reply.get('message')}"
)
elif kind == "state":
snapshot = message.get("state", {})
print(
f"← state(seq={message.get('seq')}):第 {snapshot.get('day')} 天 · "
f"金币 {snapshot.get('gold')} · "
f"在线 {message.get('online')}"
)
if acted:
# 收到了动作触发的推送,两条路都验证完了
break
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}")
if time.time() >= deadline:
break
print(f"\n消息统计:{counts}")
if counts.get("welcome", 0) < 1:
print("✗ 没有收到 welcome,SSE 长连接可能没建立")
return 1
if counts.get("state", 0) < 1:
print("✗ 动作之后没有收到 state 推送(引擎与推送线程之间可能断了)")
return 1
print("✓ SSE 通道正常:状态会主动下行,动作经由 HTTP 上行")
return 0
def main() -> int:
parser = argparse.ArgumentParser(description="SSE 通道演示")
parser.add_argument("--url", default="http://127.0.0.1:8765")
parser.add_argument("--farm", default="sse-demo")
parser.add_argument("--token", default=None)
parser.add_argument("--seconds", type=float, default=8.0)
args = parser.parse_args()
return run(args.url, args.farm, args.token, args.seconds)
if __name__ == "__main__":
sys.exit(main())