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

+56 -52
View File
@@ -16,10 +16,12 @@ TUI 与 HTTP/gRPC 接口都调用同一组方法,因此三方共享完全一
from __future__ import annotations
import contextlib
import random
import threading
import time
from typing import Any, Callable, Dict, List, Optional
from collections.abc import Callable
from typing import Any
from . import config
from .models import Animal, GameState, Plot, clamp
@@ -34,7 +36,7 @@ class GameError(Exception):
self.message = message
self.status = status
def to_dict(self) -> Dict[str, Any]:
def to_dict(self) -> dict[str, Any]:
return {"ok": False, "error": self.code, "message": self.message}
@@ -52,9 +54,9 @@ class FarmEngine:
self._last_sleep = 0.0
self.paused = False
# 由 Scheduler 维护:下一次自动推进的绝对时间戳(无调度器时为 None)
self.next_tick_at: Optional[float] = None
self.next_tick_at: float | None = None
# 状态变化回调(TUI 用它触发重绘)
self._listeners: List[Callable[[], None]] = []
self._listeners: list[Callable[[], None]] = []
# ================================================================ 订阅
def subscribe(self, callback: Callable[[], None]) -> None:
@@ -63,11 +65,9 @@ class FarmEngine:
def _notify(self) -> None:
for callback in list(self._listeners):
try:
# 监听器异常不能影响游戏主流程
with contextlib.suppress(Exception):
callback()
except Exception:
# 监听器异常不能影响游戏主流程
pass
# ================================================================ 日志
def add_log(self, text: str, kind: str = "info") -> None:
@@ -77,20 +77,20 @@ class FarmEngine:
{
"seq": self._log_seq,
"day": self.state.day,
"kind": kind, # info / good / bad / warn / action
"kind": kind, # info / good / bad / warn / action
"text": text,
}
)
if len(self.state.log) > config.MAX_LOG_ENTRIES:
del self.state.log[: len(self.state.log) - config.MAX_LOG_ENTRIES]
def recent_log(self, limit: int = 12, since: int = 0) -> List[Dict[str, Any]]:
def recent_log(self, limit: int = 12, since: int = 0) -> list[dict[str, Any]]:
with self._lock:
entries = [e for e in self.state.log if e["seq"] > since]
return entries[-limit:] if limit > 0 else entries
# ================================================================ 查询
def snapshot(self) -> Dict[str, Any]:
def snapshot(self) -> dict[str, Any]:
"""返回完整状态快照(供 TUI 渲染与 API 响应)。"""
with self._lock:
data = self.state.to_dict()
@@ -101,7 +101,7 @@ class FarmEngine:
return data
@property
def seconds_to_tick(self) -> Optional[float]:
def seconds_to_tick(self) -> float | None:
if self.paused or self.next_tick_at is None:
return None
return round(max(0.0, self.next_tick_at - time.time()), 1)
@@ -110,7 +110,7 @@ class FarmEngine:
with self._lock:
return round(self.state.market.get(key, 0.0), 2)
def catalog(self) -> Dict[str, Any]:
def catalog(self) -> dict[str, Any]:
"""返回所有可种植/可养殖对象及其在市场上的实时价格。"""
with self._lock:
crops = []
@@ -148,9 +148,7 @@ class FarmEngine:
"exp": info["exp"],
"level_req": info["level_req"],
"locked": self.state.level < info["level_req"],
"profit_per_day": round(
(price - feed_cost) / max(1, info["cycle"]), 2
),
"profit_per_day": round((price - feed_cost) / max(1, info["cycle"]), 2),
}
)
return {
@@ -161,14 +159,14 @@ class FarmEngine:
"ap_cost": config.AP_COST,
}
def available_actions(self) -> List[Dict[str, Any]]:
def available_actions(self) -> list[dict[str, Any]]:
"""列出当前**合法**的动作,供 AI 直接决策使用。
每个动作包含:``action``(接口名)、``params``(参数模板)、
``cost``(金币/行动点开销)、``reason``(为什么值得做)。
"""
with self._lock:
actions: List[Dict[str, Any]] = []
actions: list[dict[str, Any]] = []
state = self.state
# ---- 收获 / 浇水 / 播种
@@ -193,7 +191,9 @@ class FarmEngine:
"action": "harvest",
"params": {"plot": plot.index + 1},
"cost": {"gold": 0, "ap": config.AP_COST["harvest"]},
"reason": f"{plot.index + 1} 号地的{config.CROPS[plot.crop]['name']}已成熟",
"reason": (
f"{plot.index + 1} 号地的{config.CROPS[plot.crop]['name']}已成熟"
),
}
)
elif plot.moisture == 0 and state.ap >= config.AP_COST["water"]:
@@ -274,7 +274,8 @@ class FarmEngine:
try:
number = int(plot_no)
except (TypeError, ValueError):
raise GameError("bad_param", f"地块编号必须是整数,收到 {plot_no!r}")
# 底层 ValueError 的细节对调用者没意义,只保留友好提示
raise GameError("bad_param", f"地块编号必须是整数,收到 {plot_no!r}") from None
if number < 1 or number > len(self.state.plots):
raise GameError(
"no_such_plot",
@@ -286,7 +287,7 @@ class FarmEngine:
try:
aid = int(animal_id)
except (TypeError, ValueError):
raise GameError("bad_param", f"动物编号必须是整数,收到 {animal_id!r}")
raise GameError("bad_param", f"动物编号必须是整数,收到 {animal_id!r}") from None
for animal in self.state.animals:
if animal.aid == aid:
return animal
@@ -316,14 +317,14 @@ class FarmEngine:
self.state.total_spent += amount
def _earn(self, amount: float, reason: str) -> int:
earned = max(0, int(round(amount)))
earned = max(0, round(amount))
self.state.gold += earned
self.state.total_earned += earned
return earned
def _gain_exp(self, amount: int) -> List[str]:
def _gain_exp(self, amount: int) -> list[str]:
"""增加经验并处理连续升级,返回升级提示文本。"""
messages: List[str] = []
messages: list[str] = []
self.state.exp += int(amount)
guard = 0
while self.state.exp >= self.state.exp_needed and guard < 50:
@@ -353,7 +354,7 @@ class FarmEngine:
return 2 if random.random() < config.LUCKY_CHANCE else 1
# ================================================================ 动作
def plant(self, plot_no: Any, crop: Any) -> Dict[str, Any]:
def plant(self, plot_no: Any, crop: Any) -> dict[str, Any]:
with self._lock:
if not isinstance(crop, str) or crop not in config.CROPS:
raise GameError(
@@ -368,7 +369,9 @@ class FarmEngine:
)
plot = self._get_plot(plot_no)
if plot.crop is not None:
raise GameError("plot_busy", f"{plot.index + 1} 号地已经种了{config.CROPS[plot.crop]['name']}")
raise GameError(
"plot_busy", f"{plot.index + 1} 号地已经种了{config.CROPS[plot.crop]['name']}"
)
self._spend_gold(info["seed_cost"], f"购买{info['name']}种子")
self._spend_ap("plant")
@@ -382,11 +385,13 @@ class FarmEngine:
self._notify()
return {
"ok": True,
"message": f"{plot.index + 1} 号地已种下{info['name']},{info['grow_days']} 天后成熟",
"message": (
f"{plot.index + 1} 号地已种下{info['name']},{info['grow_days']} 天后成熟"
),
"plot": plot.to_dict(),
}
def water(self, plot_no: Any) -> Dict[str, Any]:
def water(self, plot_no: Any) -> dict[str, Any]:
with self._lock:
plot = self._get_plot(plot_no)
if plot.crop is None:
@@ -402,7 +407,7 @@ class FarmEngine:
self._notify()
return {"ok": True, "message": f"{plot.index + 1} 号地已浇水", "plot": plot.to_dict()}
def harvest(self, plot_no: Any) -> Dict[str, Any]:
def harvest(self, plot_no: Any) -> dict[str, Any]:
with self._lock:
plot = self._get_plot(plot_no)
if plot.crop is None:
@@ -426,9 +431,7 @@ class FarmEngine:
self.state.total_harvested += quantity
lucky = "(丰收!)" if quantity > 1 else ""
self.add_log(
f"收获 {quantity} 份{info['name']}{lucky},+{earned} 金币", "good"
)
self.add_log(f"收获 {quantity} 份{info['name']}{lucky},+{earned} 金币", "good")
levelups = self._gain_exp(info["exp"])
self._notify()
return {
@@ -440,7 +443,7 @@ class FarmEngine:
"level_ups": levelups,
}
def buy_animal(self, kind: Any) -> Dict[str, Any]:
def buy_animal(self, kind: Any) -> dict[str, Any]:
with self._lock:
if not isinstance(kind, str) or kind not in config.ANIMALS:
raise GameError(
@@ -469,7 +472,7 @@ class FarmEngine:
"animal": animal.to_dict(),
}
def feed(self, animal_id: Any, quantity: Any = None) -> Dict[str, Any]:
def feed(self, animal_id: Any, quantity: Any = None) -> dict[str, Any]:
with self._lock:
animal = self._get_animal(animal_id)
if animal.fed:
@@ -481,7 +484,7 @@ class FarmEngine:
try:
quantity = int(quantity)
except (TypeError, ValueError):
raise GameError("bad_param", "喂食数量必须是整数")
raise GameError("bad_param", "喂食数量必须是整数") from None
if quantity <= 0:
raise GameError("bad_param", "喂食数量必须大于 0")
if quantity > need:
@@ -507,7 +510,7 @@ class FarmEngine:
"animal": animal.to_dict(),
}
def collect(self, animal_id: Any) -> Dict[str, Any]:
def collect(self, animal_id: Any) -> dict[str, Any]:
with self._lock:
animal = self._get_animal(animal_id)
if animal.pending <= 0:
@@ -524,7 +527,8 @@ class FarmEngine:
self.state.total_collected += quantity
self.add_log(
f"收取 {quantity} 份{animal.product}({animal.name} #{animal.aid}),+{earned} 金币",
f"收取 {quantity} 份{animal.product}({animal.name} #{animal.aid}),"
f"+{earned} 金币",
"good",
)
levelups = self._gain_exp(animal.info["exp"] * quantity)
@@ -537,12 +541,12 @@ class FarmEngine:
"level_ups": levelups,
}
def buy_feed(self, quantity: Any = config.FEED_BULK) -> Dict[str, Any]:
def buy_feed(self, quantity: Any = config.FEED_BULK) -> dict[str, Any]:
with self._lock:
try:
quantity = int(quantity)
except (TypeError, ValueError):
raise GameError("bad_param", "饲料数量必须是整数")
raise GameError("bad_param", "饲料数量必须是整数") from None
if quantity <= 0:
raise GameError("bad_param", "饲料数量必须大于 0")
if quantity > config.MAX_FEED_PURCHASE:
@@ -561,7 +565,7 @@ class FarmEngine:
"feed": self.state.feed,
}
def buy_plot(self) -> Dict[str, Any]:
def buy_plot(self) -> dict[str, Any]:
with self._lock:
price = self.state.next_plot_price
if price < 0:
@@ -583,7 +587,7 @@ class FarmEngine:
"next_plot_price": self.state.next_plot_price,
}
def sleep(self) -> Dict[str, Any]:
def sleep(self) -> dict[str, Any]:
"""手动推进一天,带冷却避免刷时间。"""
with self._lock:
elapsed = time.time() - self._last_sleep
@@ -594,14 +598,14 @@ class FarmEngine:
events = self.advance_day(reason="休息")
return {"ok": True, "message": f"进入第 {self.state.day} 天", "events": events}
def pause(self, paused: Optional[bool] = None) -> Dict[str, Any]:
def pause(self, paused: bool | None = None) -> dict[str, Any]:
with self._lock:
self.paused = bool(paused) if paused is not None else not self.paused
self.add_log("时间已暂停" if self.paused else "时间继续流动", "warn")
self._notify()
return {"ok": True, "paused": self.paused}
def reset(self, keep_tick: bool = True) -> Dict[str, Any]:
def reset(self, keep_tick: bool = True) -> dict[str, Any]:
with self._lock:
tick = self.tick_seconds
self.state = GameState()
@@ -614,7 +618,7 @@ class FarmEngine:
self._notify()
return {"ok": True, "message": "农场已重置"}
def save(self, path: Optional[str] = None) -> Dict[str, Any]:
def save(self, path: str | None = None) -> dict[str, Any]:
"""把当前状态写入磁盘。
单机模式下由 TUI 直接调用;服务器模式里存档位置由牧场会话决定,
@@ -627,13 +631,13 @@ class FarmEngine:
return {"ok": True, "message": f"已存档到 {target}", "path": target}
# ================================================================ 时间推进
def advance_day(self, reason: str = "自动") -> List[str]:
def advance_day(self, reason: str = "自动") -> list[str]:
"""推进游戏内一天,返回本日发生的事件文本。"""
with self._lock:
state = self.state
state.day += 1
state.ap = state.max_ap
events: List[str] = []
events: list[str] = []
# ---- 作物生长
for plot in state.plots:
@@ -643,12 +647,14 @@ class FarmEngine:
plot.moisture -= 1
plot.growth = min(plot.grow_days, plot.growth + 1)
if plot.is_ripe:
events.append(f"{plot.index + 1} 号地的{config.CROPS[plot.crop]['name']}已成熟")
events.append(
f"{plot.index + 1} 号地的{config.CROPS[plot.crop]['name']}已成熟"
)
else:
events.append(f"{plot.index + 1} 号地缺水,作物停止生长")
# ---- 牲畜
dead: List[Animal] = []
dead: list[Animal] = []
for animal in state.animals:
if animal.fed:
animal.fed = False
@@ -710,7 +716,7 @@ class Scheduler:
self.engine = engine
self.enabled = enabled
self._stop = threading.Event()
self._thread: Optional[threading.Thread] = None
self._thread: threading.Thread | None = None
def start(self) -> None:
if not self.enabled or self._thread is not None:
@@ -734,10 +740,8 @@ class Scheduler:
if deadline is None:
deadline = time.time() + engine.tick_seconds
if time.time() >= deadline:
try:
with contextlib.suppress(Exception):
engine.advance_day(reason="时间流逝")
except Exception:
pass
engine.next_tick_at = time.time() + engine.tick_seconds
else:
engine.next_tick_at = deadline