feat: 加天气 / 谷仓 / 建筑三套系统,收获改为入库 + 择时卖出

动手前先修掉了两处「死机制」,新增的三个系统各自激活一个:

* 天气:小麦 2 天成熟、初始水分 2、每天只掉 1 点,等于永远不需要浇水。
  现在水分消耗由天气决定(晴 1 / 多云 0 / 干旱 2,雨天反补 3),而
  `state.forecast` 把明天的天气提前告诉你——「今晚浇不浇」成了能算的决策。
  5 种天气、权重合计 100、同种不连出;暴风雨会毁未熟作物,温室免疫。
* 谷仓:市场波动原本纯装饰(收获当场按市价变现,没有择时空间)。现在收获与
  收取只进 `state.inventory`,靠新增的 `sell` 换金币,价格波动才真正参与决策。
  仓满时 `_store()` 抛 `granary_full` 且不改状态,作物原样留在地里。
* 建筑:新增 `build`,买的是「以后不用天天手动做」——水井自动浇干地、
  喂食器自动喂食(只吃库存饲料,绝不自动花钱)、温室、谷仓扩容。
  每天在 `advance_day` 里先于生长执行,同类越造越贵(×1.6)。

存档升到 `SAVE_VERSION=2`,配 `MIN_SAVE_VERSION=1` 向后兼容,旧档缺的新字段
一律走默认值,不作废玩家手里的档。`advance_day(weather=...)` 是留给测试的
确定性入口。

顺带修两个真 bug:
* `daemon.py` 三处 `makedirs(os.path.dirname(path))`:`--pid-file pid.txt`
  这种只给文件名的用法会让 dirname 变成空串,启动阶段直接崩。抽出
  `ensure_parent_dir()` 兜底。
* `RemoteFarm.snapshot()` 是本地缓存,非实时客户端轮询会一直读到旧数据,
  看起来像服务器卡死。补 docstring 指明要自己调 `refresh()`。

质量门禁:ruff format/check 全绿、pyright 0 错误、503 项测试 OK(skip 35)、
bench 无回归(brief 19.5µs / snapshot 48.5µs / POST full=1 400µs)。
另起真实服务器(--tick 3 + 预置存档)跑了 38 项端到端验收,全过。
This commit is contained in:
snowy-yang committed 2026-09-19 17:02:33 +08:00
1 parent a92c29ead2
commit cf24cb43f1
21 files changed
+1349 -112

No files matched your search

+31 -5
View File
@@ -99,8 +99,8 @@ class Action:
# ---------------------------------------------------------------------- 处理器
def _harvest_all(engine: FarmEngine, _params: dict[str, Any]) -> dict[str, Any]:
"""收获所有成熟作物。逐块调用引擎,个别地块失败不影响其它地块。"""
total_gold = 0
"""收获所有成熟作物,全部进谷仓。逐块调用引擎,个别地块失败不影响其它地块。"""
total_value = 0.0
total_units = 0
plots: list[int] = []
errors: list[str] = []
@@ -113,17 +113,20 @@ def _harvest_all(engine: FarmEngine, _params: dict[str, Any]) -> dict[str, Any]:
except GameError as exc:
errors.append(f"{plot.index + 1}号地:{exc.message}")
continue
total_gold += int(result.get("earned", 0))
total_value += float(result.get("value", 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} 金币",
"message": (
f"收获 {len(plots)} 块地共 {total_units} 份作物入谷仓,"
f"按现价约值 {round(total_value)} 金币(用 sell 才换成钱)"
),
"plots": plots,
"quantity": total_units,
"earned": total_gold,
"value": round(total_value, 2),
"errors": errors,
}
@@ -235,6 +238,29 @@ _register(
)
)
_register(
Action(
"sell",
"把谷仓里的货物按当前市价卖出(省略 key 即全部卖光)",
[
Param("key", "str", False, None, "货物标识,如 wheat / animal:chicken;省略=全卖"),
Param("quantity", "int", False, None, "卖出数量,省略=该货物全卖"),
],
lambda e, p: e.sell(p.get("key"), p.get("quantity")),
)
)
_register(
Action(
"build",
"建造谷仓 / 水井 / 自动喂食器 / 温室",
[
Param("key", "str", True, None, "建筑标识:granary / well / feeder / greenhouse"),
],
lambda e, p: e.build(p["key"]),
)
)
# 注意:save / reset **刻意不进**这张表。它们是"存档类动作",多牧场环境下
# 必须由牧场会话(FarmSession)处理——engine.save() 只会写默认路径,