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

+20 -8
View File
@@ -56,11 +56,14 @@ uv run main.py solo # 本地引擎 + 终端界面,不开
| 系统 | 规则 |
| --- | --- |
| **时间** | 每个牧场每 **40 分钟**自动推进一天(`--tick` 可调),作物生长、牲畜产出、市场波动都在此刻结算 |
| **行动点** | 每天 10 点;播种 / 浇水 / 喂食各 1 点,收获和收取产出免费。用完只能等第二天——时间只能自然流动,没有手动快进 |
| **水分** | 作物每天消耗 1 点水分(上限 3),水分为 0 停止生长;播种自带 2 点 |
| **行动点** | 每天 10 点;播种 / 浇水 / 喂食各 1 点,收获、收取、卖货、建造都免费。用完只能等第二天——时间只能自然流动,没有手动快进 |
| **天气** | 每天从晴 / 多云 / 雨 / 干旱 / 暴风雨里抽一种(同种不连着出),而且**明天的天气提前可见**——「今晚浇不浇」因此是个能算的决策 |
| **水分** | 每天按天气消耗:晴 1、多云 0、干旱 2,雨天反补 3(上限 3);水分为 0 就停止生长。播种自带 2 点,只够晴天撑两天,所以下种前得先看预报 |
| **作物** | 小麦 → 胡萝卜 → 番茄 → 玉米 → 南瓜 → 草莓,按等级解锁,售价越高成熟越慢 |
| **牲畜** | 鸡 / 羊 / 牛,每天喂食才会累计产出周期;连续 3 天不喂会**永久离开** |
| **市场** | 作物与畜产品价格每天在基础价 ±18% 内波动,高位卖出更赚 |
| **谷仓** | 收获和收取**只入库、不当场给钱**(容量 40 起,随等级与谷仓建筑增长);仓满时收不进去,作物原样留在地里,卖了再来 |
| **市场** | 价格每天在基础价 ±18% 内波动;**靠 `sell` 择时变现**,高位卖才真的赚 |
| **建筑** | 谷仓(+25 仓容,最多 5 座)/ 水井(每天自动浇一块干透的地)/ 自动喂食器(每天自动喂一只,只吃库存饲料、绝不自动花钱)/ 温室(免疫暴风雨、水分蒸发减半);每多一座涨价 60% |
| **土地** | 初始 6 块,每升 1 级 +2 块,上限 16 块,买地价格指数递增 |
| **目标** | 净资产(金币+土地+牲畜+库存+地里作物)做到 10000 金币,共 5 个里程碑 |
| **丰收** | 收获有 12% 概率触发双倍产出 |
@@ -69,7 +72,8 @@ uv run main.py solo # 本地引擎 + 终端界面,不开
> **关于节奏**:这是一款“放置型”农场,一天 40 分钟是刻意设计的,不是笔误。
> 每天 10 点行动点,用完就是等时间流动——所以一天的玩法是“上来干十件事,然后离开”。
> 小麦 2 天(80 分钟)成熟,草莓 10 天(约 6.7 小时)。想调试就把节奏调快:
> 小麦 2 天(80 分钟)成熟,草莓 10 天(约 6.7 小时);断水会停长,所以实际天数看天吃饭——
> 好在预报提前一天就给你,来得及浇。想调试就把节奏调快:
> `uv run main.py play --tick 3`(一天 3 秒)。
---
@@ -81,7 +85,7 @@ uv run main.py solo # 本地引擎 + 终端界面,不开
```
[↑↓←→]选地 [P]播种 [W]浇水 [H]收获 [E]全部收获
[F]喂食 [C]收取 [Tab]换牲畜 [B]买动物 [L]买地 [K]买饲料
[空格]暂停 [S]存档 [R]重置 [Q]退出
[M]卖货 [G]建造 [空格]暂停 [S]存档 [R]重置 [Q]退出
```
界面用双宽字符感知的对齐算法(`display_width`),在 Windows Terminal / iTerm / VSCode 里边框都不会歪。
@@ -156,18 +160,18 @@ POST /api/actions → {"action":"<上一步的 action>","params":{.
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/api/health` | 健康检查(唯一不需要令牌的接口) |
| GET | `/api/help` | 规则 + 动作清单 + 参数类型 |
| GET | `/api/help` | 规则 + 动作清单 + 参数类型 + 建筑 / 天气表 |
| GET | `/api/world` | 世界概览:排行榜、在线连接、时钟 |
| GET | `/api/farms` | 牧场列表(按净资产排名) |
| POST | `/api/farms` | `{"farm":"名字","owner":"昵称","tick":10}` 创建/进入 |
| GET | `/api/state` | 完整状态快照 |
| GET | `/api/catalog` | 作物 / 牲畜 / 实时价格 / 每单位收益 |
| GET | `/api/catalog` | 作物 / 牲畜 / 商品与实时价格 / 建筑 / 天气表 / 仓容 |
| GET | `/api/actions` | 当前合法动作(AI 推荐入口) |
| GET | `/api/log?limit=20&since=0` | 事件流(`since` 做增量拉取) |
| GET | `/api/stream` | **SSE 实时推送**(服务器 → 客户端单向长连接) |
| POST | `/api/actions` | **通用动作入口** |
| POST | `/api/batch` | 批量动作(一次最多 50 个) |
| POST | `/api/{动作名}` | 便捷入口:`/api/plant` `/api/harvest` … 等 10 个 |
| POST | `/api/{动作名}` | 便捷入口:`/api/plant` `/api/harvest` … 等 12 个 |
| POST | `/api/admin/save` | 全部存档 |
| POST | `/api/admin/shutdown` | 优雅停机 |
| GET | `/docs` | 自动生成的 Swagger 文档 |
@@ -178,6 +182,7 @@ POST /api/actions → {"action":"<上一步的 action>","params":{.
plant(plot,crop) water(plot) harvest(plot) harvest_all()
buy_animal(kind) feed(animal_id,quantity?) collect(animal_id)
buy_feed(quantity?) buy_plot() pause(paused?)
sell(key?,quantity?) build(key) ← 谷仓变现与建造,0 行动点
```
> `save` / `reset` 是**存档类动作**,不进通用动作表:它们必须由牧场会话处理,
@@ -203,6 +208,13 @@ curl -X POST 'http://127.0.0.1:8765/api/batch?farm=alice' \
{"action":"buy_feed","params":{"quantity":10}},
{"action":"plant","params":{"plot":1,"crop":"carrot"}}]}'
# 收成变现 / 造建筑:都是 0 行动点,行动点用完了照样能做
curl -X POST 'http://127.0.0.1:8765/api/actions?farm=alice' \
-H 'Content-Type: application/json' -d '{"action":"sell"}'
curl -X POST 'http://127.0.0.1:8765/api/actions?farm=alice' \
-H 'Content-Type: application/json' \
-d '{"action":"build","params":{"key":"granary"}}'
# 要完整快照(默认只回精简状态)
curl -X POST 'http://127.0.0.1:8765/api/actions?farm=alice&full=1' ...
```