Files
SimpleFarm/farm/server/actions.py
T
snowy-yang fcd4cb4649 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 项测试全绿。
2026-09-18 21:18:22 +08:00

306 lines
9.5 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.
"""统一的动作表:HTTP、WebSocket、命令行、机器人都走这一份映射。
为什么要有这一层
----------------
引擎(``farm.engine.FarmEngine``)提供的是**方法**,但外部调用者(AI Agent、
脚本、网页)手里拿到的只有字符串动作名 + 参数字典。如果让每个入口各自写一遍
``if action == "plant"`` 的分发,规则就会散落在三四个文件里,迟早出现
"HTTP 能做、WS 不能做" 这种不一致。
所以这里集中定义:动作名 → 参数规格 → 引擎调用 → 返回结果。
新增一个玩法动作,只需要在这张表里加一行。
"""
from __future__ import annotations
from collections.abc import Callable
from typing import Any
from .. import config
from ..engine import FarmEngine, GameError
class Param:
"""一个动作参数的规格说明(同时用于校验与自动生成给 AI 的文档)。"""
__slots__ = ("default", "desc", "kind", "name", "required")
def __init__(
self, name: str, kind: str, required: bool = True, default: Any = None, desc: str = ""
) -> None:
self.name = name
self.kind = kind # "int" / "str" / "bool"
self.required = required
self.default = default
self.desc = desc
def coerce(self, raw: Any) -> Any:
"""把外部传来的值转成期望类型,失败时抛出带 code 的业务异常。"""
if self.kind == "bool":
if isinstance(raw, bool):
return raw
if isinstance(raw, str) and raw.lower() in ("true", "false", "1", "0"):
return raw.lower() in ("true", "1")
if isinstance(raw, int):
return bool(raw)
raise GameError("bad_param", f"参数 {self.name} 需要布尔值,收到 {raw!r}")
if self.kind == "int":
if isinstance(raw, bool):
raise GameError("bad_param", f"参数 {self.name} 需要整数,收到布尔值")
if isinstance(raw, int):
return raw
if isinstance(raw, float) and raw.is_integer():
return int(raw)
if isinstance(raw, str) and raw.strip().lstrip("+-").isdigit():
return int(raw.strip())
raise GameError("bad_param", f"参数 {self.name} 需要整数,收到 {raw!r}")
if self.kind == "str":
if isinstance(raw, (str, int, float)):
return str(raw)
raise GameError("bad_param", f"参数 {self.name} 需要字符串,收到 {raw!r}")
return raw
def to_dict(self) -> dict[str, Any]:
return {
"name": self.name,
"type": self.kind,
"required": self.required,
"default": self.default,
"desc": self.desc,
}
class Action:
"""一个可执行动作。"""
__slots__ = ("handler", "name", "params", "realtime", "summary")
def __init__(
self,
name: str,
summary: str,
params: list[Param],
handler: Callable[[FarmEngine, dict[str, Any]], dict[str, Any]],
realtime: bool = True,
) -> None:
self.name = name
self.summary = summary
self.params = params
self.handler = handler
self.realtime = realtime
def to_dict(self) -> dict[str, Any]:
return {
"action": self.name,
"summary": self.summary,
"params": [p.to_dict() for p in self.params],
}
# ---------------------------------------------------------------------- 处理器
def _harvest_all(engine: FarmEngine, _params: dict[str, Any]) -> dict[str, Any]:
"""收获所有成熟作物。逐块调用引擎,个别地块失败不影响其它地块。"""
total_gold = 0
total_units = 0
plots: list[int] = []
errors: list[str] = []
# 复制列表:harvest 会修改 state.plots 中元素的内容,但不会增删元素
for plot in list(engine.state.plots):
if not plot.is_ripe:
continue
try:
result = engine.harvest(plot.index + 1)
except GameError as exc:
errors.append(f"{plot.index + 1}号地:{exc.message}")
continue
total_gold += int(result.get("earned", 0))
total_units += int(result.get("quantity", 0))
plots.append(plot.index + 1)
if not plots and not errors:
raise GameError("nothing_to_harvest", "当前没有成熟的作物可以收获")
return {
"message": f"收获 {len(plots)} 块地共 {total_units} 份作物,+{total_gold} 金币",
"plots": plots,
"quantity": total_units,
"earned": total_gold,
"errors": errors,
}
def _save(engine: FarmEngine, _params: dict[str, Any]) -> dict[str, Any]:
return engine.save()
def _reset(engine: FarmEngine, _params: dict[str, Any]) -> dict[str, Any]:
return engine.reset()
def _pause(engine: FarmEngine, params: dict[str, Any]) -> dict[str, Any]:
value = params.get("paused")
return engine.pause(value if isinstance(value, bool) else None)
# ---------------------------------------------------------------------- 动作表
ACTIONS: dict[str, Action] = {}
def _register(action: Action) -> None:
ACTIONS[action.name] = action
_register(
Action(
"plant",
"在指定地块播种",
[
Param("plot", "int", True, None, "地块编号,从 1 开始"),
Param("crop", "str", True, None, "作物标识,如 wheat"),
],
lambda e, p: e.plant(p["plot"], p["crop"]),
)
)
_register(
Action(
"water",
"给指定地块浇水(补满水分)",
[
Param("plot", "int", True, None, "地块编号,从 1 开始"),
],
lambda e, p: e.water(p["plot"]),
)
)
_register(
Action(
"harvest",
"收获指定地块的成熟作物",
[
Param("plot", "int", True, None, "地块编号,从 1 开始"),
],
lambda e, p: e.harvest(p["plot"]),
)
)
_register(Action("harvest_all", "一键收获所有成熟作物", [], _harvest_all))
_register(
Action(
"buy_animal",
"购买牲畜",
[
Param("kind", "str", True, None, "牲畜标识:chicken / sheep / cow"),
],
lambda e, p: e.buy_animal(p["kind"]),
)
)
_register(
Action(
"feed",
"喂食指定牲畜",
[
Param("animal_id", "int", True, None, "牲畜编号(state.animals[].id)"),
Param("quantity", "int", False, None, "喂食份数,默认按需求;多喂无益"),
],
lambda e, p: e.feed(p["animal_id"], p.get("quantity")),
)
)
_register(
Action(
"collect",
"收取牲畜的产出并自动出售",
[
Param("animal_id", "int", True, None, "牲畜编号"),
],
lambda e, p: e.collect(p["animal_id"]),
)
)
_register(
Action(
"buy_feed",
"购买饲料",
[
Param("quantity", "int", False, config.FEED_BULK, f"份数,默认 {config.FEED_BULK}"),
],
lambda e, p: e.buy_feed(p.get("quantity", config.FEED_BULK)),
)
)
_register(Action("buy_plot", "花钱开垦一块新土地", [], lambda e, p: e.buy_plot()))
_register(Action("sleep", "主动推进一天(有冷却)", [], lambda e, p: e.sleep()))
_register(
Action(
"pause",
"暂停或恢复自动推进时间",
[
Param("paused", "bool", False, None, "true 暂停 / false 恢复;省略则反转当前状态"),
],
_pause,
)
)
_register(Action("save", "把当前农场写入存档文件", [], _save))
_register(Action("reset", "重置农场,从头开始经营", [], _reset))
def action_names() -> list[str]:
return sorted(ACTIONS)
def describe_actions() -> list[dict[str, Any]]:
return [ACTIONS[name].to_dict() for name in action_names()]
def run_action(
engine: FarmEngine, name: Any, params: dict[str, Any] | None = None
) -> dict[str, Any]:
"""校验参数并执行动作。
这是所有入口的**唯一**执行通道,因此输入校验只需要在这里做一次。
"""
if not isinstance(name, str) or not name.strip():
raise GameError("bad_param", "缺少动作名 action")
key = name.strip().lower()
action = ACTIONS.get(key)
if action is None:
raise GameError(
"unknown_action",
f"未知动作 {name!r},可用动作:{'、'.join(action_names())}",
)
if params is None:
params = {}
if not isinstance(params, dict):
raise GameError("bad_param", "params 必须是对象")
known = {p.name for p in action.params}
unknown = [k for k in params if k not in known]
if unknown:
allowed = "、".join(sorted(known)) or "无"
raise GameError(
"bad_param",
f"动作 {key} 不认识参数 {'、'.join(unknown)}(可用:{allowed})",
)
clean: dict[str, Any] = {}
for spec in action.params:
if spec.name in params and params[spec.name] is not None:
clean[spec.name] = spec.coerce(params[spec.name])
elif spec.required:
raise GameError("missing_param", f"动作 {key} 缺少必需参数 {spec.name}")
elif spec.default is not None:
clean[spec.name] = spec.default
result = action.handler(engine, clean)
if not isinstance(result, dict):
result = {"message": str(result)}
result.setdefault("ok", True)
result["action"] = key
return result