style: 引入 ruff,474 项告警清零

配置见 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 项测试全绿。
This commit is contained in:
snowy-yang committed 2026-09-18 21:18:22 +08:00
1 parent c7c8a6308a
commit fcd4cb4649
25 files changed
+1204 -794

No files matched your search

+118 -61
View File
@@ -98,21 +98,31 @@ def build_parser() -> argparse.ArgumentParser:
# ---- 服务器
p_serve = sub.add_parser("serve", help="启动游戏服务器(可后台运行)")
p_serve.add_argument("--daemon", action="store_true", help="转到后台运行(守护进程)")
p_serve.add_argument("--host", default="127.0.0.1",
help="监听地址,默认 127.0.0.1;局域网联机用 0.0.0.0")
p_serve.add_argument(
"--host", default="127.0.0.1", help="监听地址,默认 127.0.0.1;局域网联机用 0.0.0.0"
)
p_serve.add_argument("--port", type=int, default=8765, help="监听端口,默认 8765")
p_serve.add_argument("--token", default=None, help="访问令牌,设置后接口需要携带")
p_serve.add_argument("--tick", type=float, default=config.DEFAULT_TICK_SECONDS,
help=f"每多少秒推进一天,默认 {config.DEFAULT_TICK_SECONDS:.0f} 秒")
p_serve.add_argument(
"--tick",
type=float,
default=config.DEFAULT_TICK_SECONDS,
help=f"每多少秒推进一天,默认 {config.DEFAULT_TICK_SECONDS:.0f} 秒",
)
p_serve.add_argument("--save-dir", default=None, help="存档目录,默认 <项目>/saves")
p_serve.add_argument("--world", default="SimpleFarm 公共农场", help="世界名称")
p_serve.add_argument("--max-sessions", type=int, default=64, help="同时在线的牧场上限")
p_serve.add_argument("--idle-timeout", type=float, default=1800.0,
help="无人访问多少秒后把牧场移出内存(存档保留)")
p_serve.add_argument(
"--idle-timeout",
type=float,
default=1800.0,
help="无人访问多少秒后把牧场移出内存(存档保留)",
)
p_serve.add_argument("--no-remote-control", action="store_true", help="禁止接口停机")
p_serve.add_argument("--access-log", action="store_true", help="打印每个 HTTP 请求")
p_serve.add_argument("--log-level", default="info",
choices=["critical", "error", "warning", "info", "debug"])
p_serve.add_argument(
"--log-level", default="info", choices=["critical", "error", "warning", "info", "debug"]
)
p_serve.add_argument("--log-file", default=None, help="守护进程日志路径")
p_serve.add_argument("--pid-file", default=None, help="守护进程 pid 文件路径")
@@ -139,8 +149,7 @@ def build_parser() -> argparse.ArgumentParser:
p_play.add_argument("--token", default=None)
p_play.add_argument("--name", default="", help="在服务器上显示的昵称")
p_play.add_argument("--no-color", action="store_true")
p_play.add_argument("--no-realtime", action="store_true",
help="不用 WebSocket,改走 HTTP 轮询")
p_play.add_argument("--no-realtime", action="store_true", help="不用 WebSocket,改走 HTTP 轮询")
p_solo = sub.add_parser("solo", help="离线单机模式(本地引擎 + 终端界面,不开服务器)")
p_solo.add_argument("--tick", type=float, default=config.DEFAULT_TICK_SECONDS)
@@ -150,8 +159,9 @@ def build_parser() -> argparse.ArgumentParser:
p_do = sub.add_parser("do", help="执行一个动作(脚本/AI 用途)")
p_do.add_argument("action", help="动作名:plant / water / harvest / ...")
p_do.add_argument("params", nargs="?", default="{}",
help='JSON 参数,例如 \'{"plot":1,"crop":"wheat"}\'')
p_do.add_argument(
"params", nargs="?", default="{}", help='JSON 参数,例如 \'{"plot":1,"crop":"wheat"}\''
)
_add_client_args(p_do)
p_batch = sub.add_parser("batch", help="批量执行动作(一个请求干多件事)")
@@ -202,10 +212,12 @@ def make_client(args, realtime: bool = False):
try:
return RemoteFarm(
endpoint=args.url, farm=getattr(args, "farm", "default"),
endpoint=args.url,
farm=getattr(args, "farm", "default"),
token=getattr(args, "token", None),
name=getattr(args, "name", "") or "cli",
realtime=realtime, autostart=True,
realtime=realtime,
autostart=True,
)
except GameError as exc:
hint = (
@@ -213,7 +225,7 @@ def make_client(args, realtime: bool = False):
"先启动服务器:python main.py serve --daemon\n"
"再确认地址端口:python main.py status"
)
raise SystemExit(die(f"无法连接服务器:{exc.message}", hint))
raise SystemExit(die(f"无法连接服务器:{exc.message}", hint)) from None
# ---------------------------------------------------------------------- 子命令
@@ -221,11 +233,20 @@ def cmd_serve(args) -> int:
from farm.server.__main__ import main as server_main
argv = [
"--host", args.host, "--port", str(args.port),
"--tick", str(args.tick), "--world", args.world,
"--max-sessions", str(args.max_sessions),
"--idle-timeout", str(args.idle_timeout),
"--log-level", args.log_level,
"--host",
args.host,
"--port",
str(args.port),
"--tick",
str(args.tick),
"--world",
args.world,
"--max-sessions",
str(args.max_sessions),
"--idle-timeout",
str(args.idle_timeout),
"--log-level",
args.log_level,
]
if args.save_dir:
argv += ["--save-dir", args.save_dir]
@@ -265,7 +286,7 @@ def cmd_status(args) -> int:
if not info["running"]:
print(warn("• 服务器未运行"))
print(dim(f" 启动:python main.py serve --daemon"))
print(dim(" 启动:python main.py serve --daemon"))
print(dim(f" 地址:http://{args.host}:{args.port}/"))
return 1
@@ -284,8 +305,10 @@ def cmd_status(args) -> int:
if world and world.get("leaderboard"):
print(" 排行榜")
for row in world["leaderboard"][:5]:
print(f" #{row['rank']} {row['farm']:<14} 净资产 {row['net_worth']:>8.0f}"
f" · 第 {row['day']} 天 · Lv.{row['level']} · 在线 {row['online']}")
print(
f" #{row['rank']} {row['farm']:<14} 净资产 {row['net_worth']:>8.0f}"
f" · 第 {row['day']} 天 · Lv.{row['level']} · 在线 {row['online']}"
)
if args.logs:
print(dim(" ---- 日志尾部 ----"))
@@ -317,8 +340,9 @@ def cmd_play(args) -> int:
)
client = make_client(args, realtime=not args.no_realtime)
label = f"{client.farm} @ {client.host}"
app = TuiApp(client, save_path=None, use_color=not args.no_color,
autosave=True, server_label=label)
app = TuiApp(
client, save_path=None, use_color=not args.no_color, autosave=True, server_label=label
)
try:
app.run()
finally:
@@ -366,7 +390,10 @@ def cmd_do(args) -> int:
try:
params = json.loads(args.params) if args.params else {}
except json.JSONDecodeError as exc:
return die(f"参数不是合法 JSON:{exc}", '例子:python main.py do plant \'{"plot":1,"crop":"wheat"}\'')
return die(
f"参数不是合法 JSON:{exc}",
'例子:python main.py do plant \'{"plot":1,"crop":"wheat"}\'',
)
if not isinstance(params, dict):
return die("参数必须是 JSON 对象")
@@ -374,19 +401,25 @@ def cmd_do(args) -> int:
try:
result = client._action(args.action, params)
except GameError as exc:
return die(f"{args.action} 失败({exc.code}):{exc.message}",
"看看现在能做什么:python main.py actions")
return die(
f"{args.action} 失败({exc.code}):{exc.message}",
"看看现在能做什么:python main.py actions",
)
print(ok("✓ ") + str(result.get("message", "完成"))
+ dim(f" ({time.time() - started:.2f}s)"))
extra = {k: v for k, v in result.items()
if k not in ("ok", "message", "state", "log", "action") and not isinstance(v, (dict, list))}
print(ok("✓ ") + str(result.get("message", "完成")) + dim(f" ({time.time() - started:.2f}s)"))
extra = {
k: v
for k, v in result.items()
if k not in ("ok", "message", "state", "log", "action") and not isinstance(v, (dict, list))
}
if extra:
print(dim(" " + " ".join(f"{k}={v}" for k, v in extra.items())))
state = result.get("state") or {}
if state:
print(f" 第 {state.get('day')} 天 · 金币 {state.get('gold')} · "
f"行动点 {state.get('ap')}/{state.get('max_ap')} · 净资产 {state.get('net_worth')}")
print(
f" 第 {state.get('day')} 天 · 金币 {state.get('gold')} · "
f"行动点 {state.get('ap')}/{state.get('max_ap')} · 净资产 {state.get('net_worth')}"
)
return 0
@@ -397,8 +430,11 @@ def cmd_batch(args) -> int:
try:
items = json.loads(args.actions)
except json.JSONDecodeError as exc:
return die(f"actions 不是合法 JSON:{exc}",
'例子:python main.py batch \'[{"action":"harvest_all"},{"action":"buy_feed","params":{"quantity":5}}]\'')
return die(
f"actions 不是合法 JSON:{exc}",
"例子:python main.py batch "
'\'[{"action":"harvest_all"},{"action":"buy_feed","params":{"quantity":5}}]\'',
)
if not isinstance(items, list):
return die("actions 必须是 JSON 数组")
try:
@@ -416,8 +452,10 @@ def cmd_actions(args) -> int:
client = make_client(args)
state = client.snapshot()
items = client.available_actions()
print(f"牧场 {client.farm} · 第 {state.get('day')} 天 · 金币 {state.get('gold')} · "
f"行动点 {state.get('ap')}/{state.get('max_ap')}")
print(
f"牧场 {client.farm} · 第 {state.get('day')} 天 · 金币 {state.get('gold')} · "
f"行动点 {state.get('ap')}/{state.get('max_ap')}"
)
if not items:
print(warn("• 当前没有可执行的动作"))
print(dim(" 试试:python main.py do sleep (推进一天)"))
@@ -426,8 +464,8 @@ def cmd_actions(args) -> int:
for index, item in enumerate(items, start=1):
cost = item.get("cost") or {}
cost_text = " ".join(f"{k}:{v}" for k, v in cost.items()) or "免费"
print(f" [{index:>2}] {item['action']:<12} {json.dumps(item['params'], ensure_ascii=False):<42}"
f" ({cost_text})")
params_text = json.dumps(item["params"], ensure_ascii=False)
print(f" [{index:>2}] {item['action']:<12} {params_text:<42} ({cost_text})")
print(dim(f" {item.get('reason', '')}"))
print(dim("\n执行方式:python main.py do <动作名> '<参数JSON>'"))
return 0
@@ -440,11 +478,18 @@ def cmd_state(args) -> int:
return 0
snap = client.snapshot()
print(f"牧场 {snap.get('farm')}")
print(f"第 {snap.get('day')} 天"
+ (f" · {snap.get('seconds_to_tick')}s 后进入下一天"
if snap.get("seconds_to_tick") is not None else " · 已暂停"))
print(f"金币 {snap.get('gold')} 等级 Lv.{snap.get('level')} "
f"({snap.get('exp')}/{snap.get('exp_needed')})")
print(
f"第 {snap.get('day')} 天"
+ (
f" · {snap.get('seconds_to_tick')}s 后进入下一天"
if snap.get("seconds_to_tick") is not None
else " · 已暂停"
)
)
print(
f"金币 {snap.get('gold')} 等级 Lv.{snap.get('level')} "
f"({snap.get('exp')}/{snap.get('exp_needed')})"
)
print(f"行动点 {snap.get('ap')}/{snap.get('max_ap')} 饲料 {snap.get('feed')}")
print(f"净资产 {snap.get('net_worth')}")
print(f"土地 {snap.get('plot_count')}/{snap.get('max_plots')} 块")
@@ -452,11 +497,16 @@ def cmd_state(args) -> int:
if plot["is_empty"]:
continue
status = "可收获" if plot["is_ripe"] else f"还需{plot['days_left']}天"
print(f" {plot['position']:>2} 号地 {plot['crop_name']:<6} 生长 {plot['growth']}"
f"/{plot['grow_days']} 水分 {plot['moisture']} · {status}")
print(
f" {plot['position']:>2} 号地 {plot['crop_name']:<6} 生长 {plot['growth']}"
f"/{plot['grow_days']} 水分 {plot['moisture']} · {status}"
)
for animal in snap.get("animals", []):
note = (f"{animal['product']} x{animal['pending']} 待收" if animal["pending"]
else ("已喂食" if animal["fed"] else "未喂食"))
note = (
f"{animal['product']} x{animal['pending']} 待收"
if animal["pending"]
else ("已喂食" if animal["fed"] else "未喂食")
)
print(f" #{animal['id']} {animal['name']:<4} {note}")
return 0
@@ -471,7 +521,6 @@ def cmd_log(args) -> int:
def cmd_watch(args) -> int:
"""实时滚动:状态每轮刷新,日志增量打印。适合盯着 AI 玩。"""
import threading
client = make_client(args, realtime=True)
print(BANNER)
@@ -484,9 +533,11 @@ def cmd_watch(args) -> int:
last_seq = max(last_seq, int(entry.get("seq", 0)))
print(f"[D{entry['day']:>3}] {entry['text']}")
online = snap.get("online")
head = (f"第 {snap.get('day')} 天 | 金币 {snap.get('gold')} | "
f"Lv.{snap.get('level')} | 行动点 {snap.get('ap')}/{snap.get('max_ap')} | "
f"净资产 {snap.get('net_worth')}")
head = (
f"第 {snap.get('day')} 天 | 金币 {snap.get('gold')} | "
f"Lv.{snap.get('level')} | 行动点 {snap.get('ap')}/{snap.get('max_ap')} | "
f"净资产 {snap.get('net_worth')}"
)
if online is not None:
head += f" | 在线 {online}"
print("\r" + head + " " * 8, end="", flush=True)
@@ -517,13 +568,17 @@ def cmd_bot(args) -> int:
def cmd_farms(args) -> int:
client = make_client(args)
world = client.world()
print(f"世界:{world.get('world')} · 已运行 {world.get('uptime', 0):.0f} 秒 · "
f"牧场 {world.get('farms')} 个 · 连接 {world.get('clients')} 个")
print(
f"世界:{world.get('world')} · 已运行 {world.get('uptime', 0):.0f} 秒 · "
f"牧场 {world.get('farms')} 个 · 连接 {world.get('clients')} 个"
)
print(f"时钟:每 {world.get('tick_seconds')} 秒推进一天\n")
print(f" {'#':>2} {'牧场':<14} {'净资产':>9} {'金币':>7} {'天':>4} {'等级':>4} {'在线':>4}")
for row in world.get("leaderboard", []):
print(f" {row['rank']:>2} {row['farm']:<14} {row['net_worth']:>9.0f} "
f"{row['gold']:>7} {row['day']:>4} {row['level']:>4} {row['online']:>4}")
print(
f" {row['rank']:>2} {row['farm']:<14} {row['net_worth']:>9.0f} "
f"{row['gold']:>7} {row['day']:>4} {row['level']:>4} {row['online']:>4}"
)
return 0
@@ -601,9 +656,11 @@ def main(argv=None) -> int:
info = DaemonManager().status()
if info["running"]:
health = info.get("health") or {}
print(ok("✓ 服务器正在运行")
+ f" http://{info['host']}:{info['port']}/ "
+ dim(f"牧场 {health.get('farms', 0)} 个 · 在线 {health.get('clients', 0)}"))
print(
ok("✓ 服务器正在运行")
+ f" http://{info['host']}:{info['port']}/ "
+ dim(f"牧场 {health.get('farms', 0)} 个 · 在线 {health.get('clients', 0)}")
)
else:
print(warn("• 服务器未运行"))
except Exception:
@@ -616,7 +673,7 @@ def main(argv=None) -> int:
return die(f"未知命令 {args.command}", "python main.py --help 查看全部命令")
try:
return handler(args)
except SystemExit as exc: # make_client 里的友好退出
except SystemExit as exc: # make_client 里的友好退出
return int(exc.code or 0)
except KeyboardInterrupt:
print("\n已中断。")