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:
1 parent
a92c29ead2
commit
cf24cb43f1
21 files changed
+1349
-112
No files matched your search
@@ -236,3 +236,47 @@
|
||||
最后一次核对:`git status` → "up to date with 'origin/main',working tree clean"。
|
||||
- 远端内容已用 API 核对:3 个提交齐全(450655b / f8e093c / 47dd818)、根目录 13 个条目
|
||||
(.gitattributes / .githubignore / README.md 25KB / farm/ / tests/ / tools/ / uv.lock / pyproject.toml …)。
|
||||
|
||||
## 2026-09-19 三大新系统:天气+水分重做 / 谷仓+择时卖出 / 建筑设施
|
||||
|
||||
- 用户答复「继续优化 添加功能等」;多选确认范围,选中三个(第四个「离线补偿」没选)。
|
||||
- 动机:动手前发现两处**死机制**——①小麦 2 天成熟、初始水分 2、每天只掉 1 点,
|
||||
等于**永远不需要浇水**;②市场波动纯装饰,收获当场按市价变现,玩家没有择时决策。
|
||||
加「天气」和「谷仓」正好各激活一个,建筑则给攒下来的钱一个出口。
|
||||
- **天气**(`config.WEATHER`):5 种、权重合计 100、同种不连出(`WEATHER_REROLLS=3` 重掷)。
|
||||
晴天蒸发 1 / 多云 0 / 雨补 3 / 干旱 2 / 暴风雨补 3 + 毁未熟作物 12%。
|
||||
`state.weather`(今天)+ `state.forecast`(明天,已知)→「今晚浇不浇」成了可计算的决策。
|
||||
温室建成后蒸发减半。`advance_day(weather=None)` 按 forecast 结算;
|
||||
**传 `weather="sunny"` 可钉死确定性**(测试/调试用,正常玩法不传)。
|
||||
- **谷仓**:收获/收取不再给钱,进 `state.inventory`(键与 `market` 同构:`wheat` / `animal:chicken`),
|
||||
再由 `sell` 按当前市价换金币;`granary_capacity = 40 + 10*(level-1) + 25*谷仓数`,上限 400。
|
||||
仓满时 `_store()` 抛 `granary_full` **且不改状态**(作物原地不动),不是「收了个寂寞」;
|
||||
卖空报 `nothing_to_sell`。
|
||||
- **建筑**(`config.BUILDINGS`):granary(300,Lv1,max5) / well(400,Lv2,max3) /
|
||||
feeder(650,Lv3,max3) / greenhouse(1500,Lv5,max1);第 n 座价 `int(cost*1.6**owned)`。
|
||||
每天在 `advance_day` 里**先于生长**跑(水井救的正是今天要断水那块地)。
|
||||
喂食器只吃库存饲料、**绝不自动花钱**。等级不够抛 `locked`、建满抛 `build_limit`。
|
||||
- **存档**:`SAVE_VERSION=2` + `MIN_SAVE_VERSION=1`(旧档缺的新字段一律走默认值,不作废)。
|
||||
- 动作表新增 `sell`(key?/quantity?) 与 `build`(key),两者都 0 AP;`bot` 在 AP=0 时
|
||||
仍会走一次 `step()`,因为卖货/建造/收获/收取都是免费的。
|
||||
- **回归测试改了 11 处**,全部是预期内的行为变化而非 bug:harvest/collect 回执从
|
||||
`earned` 改 `value`(钱在 sell 才进来);成长用例改成 `advance_day(weather="sunny")`
|
||||
才可确定;BRIEF_KEYS 那只测试原来断言 `session.snapshot()`,改为断言
|
||||
`brief(session, hub)`(BRIEF_KEYS 的数据源本来就是 brief_source,不是完整快照)。
|
||||
门禁:ruff/pyright 全绿、**503 项测试 OK(skip 35)**、bench 无回归
|
||||
(brief 19.5µs / snapshot 48.5µs / POST full=1 400µs)。
|
||||
|
||||
### 顺手修的两个真 bug
|
||||
- `daemon.py` 三处 `os.makedirs(os.path.dirname(path), exist_ok=True)`:路径只给文件名时
|
||||
(`--pid-file pid.txt`)`dirname` 是空串 → FileNotFoundError,**启动阶段直接崩**。
|
||||
抽出 `ensure_parent_dir()`(没有目录部分就什么都不建),`__main__.py` 里那份手写的
|
||||
`os.path.dirname(...) or "."` 兜底也统一过去。
|
||||
- `RemoteFarm.snapshot()` 是**本地缓存**,不发请求;非实时客户端轮询时会一直读到同一份
|
||||
旧数据——服务器明明在推进,看起来却像卡死。已加 docstring 说明「要新鲜数据自己调
|
||||
`refresh()`,或开 SSE」。这个坑是我自己写端到端脚本时踩的。
|
||||
|
||||
### 真机端到端验收(临时脚本,跑完即删)
|
||||
起 `--tick 3` 的服务器 + 预置存档(boss=5000 金币 Lv.5、noob=5000 金币 Lv.1),
|
||||
走真实 HTTP 验了 **38 项全过**:catalog 四张新表、天气/预报换班、种→缺水→浇水→成熟、
|
||||
收获进仓不给钱、`sell` 变现与空仓报错、`build` 扣钱/涨容量/涨价/未解锁 locked/未知建筑。
|
||||
其中「等待期间手动浇水 1 次」是被干旱实打实逼出来的,说明天气→浇水这条链真在起作用。
|
||||
@@ -35,6 +35,23 @@ TUI 是纯客户端,同时开放 HTTP + SSE 接口给 AI/脚本调用(服务
|
||||
- 新建牧场时 `FarmSession` 会立刻设好 `next_tick_at`,否则到时钟线程下一轮(≤0.2s)
|
||||
之间倒计时是 None,客户端会误显示成“已暂停/手动模式”。
|
||||
|
||||
## 三大系统:天气 / 谷仓 / 建筑(2026-09-19 起)
|
||||
- **天气**:`config.WEATHER` 5 种(晴 30 / 多云 27 / 雨 24 / 干旱 12 / 暴风雨 7,合计 100),
|
||||
同种不连出(`WEATHER_REROLLS` 重掷)。`state.weather`=今天、`state.forecast`=明天且
|
||||
**提前可见**,于是「今晚浇不浇」成了可计算的决策。水分消耗改由天气决定
|
||||
(晴 1 / 多云 0 / 干旱 2),雨天反补 3;建成温室后蒸发减半。
|
||||
- **谷仓**:收获 / 收取**不再直接给钱**,进 `state.inventory`(键与 `market` 同构:
|
||||
`wheat` / `animal:chicken`),靠 `sell` 换金币——市场波动因此从纯装饰变成真决策。
|
||||
仓满时 `_store()` 抛 `granary_full` **且不改状态**(作物原地不动,卖了再来)。
|
||||
- **建筑**:`config.BUILDINGS`,第 n 座价 `int(cost * 1.6**owned)`;每天在 `advance_day`
|
||||
里**先于生长**执行(水井救的正是今天要断水那块地)。喂食器只吃库存饲料、绝不自动花钱。
|
||||
- **存档**:`SAVE_VERSION=2` 配 `MIN_SAVE_VERSION=1`——旧档缺的新字段一律走默认值,
|
||||
**不作废玩家手里的档**。加字段时两处版本号都要看一遍。
|
||||
- **测试钩子**:`advance_day(weather="sunny")` 可钉死当天天气(不传就随机,测试会飘);
|
||||
这条链的入口是 `_resolve_weather` / `_evaporation` / `_run_buildings` / `_storm_damage`。
|
||||
- 动作表新增 `sell` 与 `build`,都是 **0 AP**;`bot` 在 AP=0 时仍会走一次 `step()`,
|
||||
正是为了执行这些免费动作。
|
||||
|
||||
## 架构约定(改动时必须遵守)
|
||||
- **分层**:`farm.engine` 只管规则,不认识网络/终端;`farm.tui` 不认识 socket;
|
||||
`farm/server/*` 不认识终端。新增玩法动作只改 `farm/server/actions.py` 一张表。
|
||||
@@ -101,6 +118,13 @@ TUI 是纯客户端,同时开放 HTTP + SSE 接口给 AI/脚本调用(服务
|
||||
不要在 `catalog` / `available_actions` / `plant` / `buy_animal` / `_gain_exp` 各写一份。
|
||||
|
||||
## 环境禁忌(本机特有)
|
||||
- `os.makedirs(os.path.dirname(path), exist_ok=True)` 在 path **只有文件名**时
|
||||
(`--pid-file pid.txt`)`dirname` 是空串,会以 FileNotFoundError 崩掉。
|
||||
已抽 `farm/server/daemon.py` 的 `ensure_parent_dir()` 兜底,新代码用它,别再手写。
|
||||
- `RemoteFarm.snapshot()` 是**本地缓存、不发请求**。关掉实时通道(`realtime=False`)的
|
||||
脚本必须自己调 `refresh()`,否则轮询永远读到同一份旧数据——服务器在推进却看起来像卡死。
|
||||
TUI / 网页走 SSE 所以没这个问题;`bot` 走 `available_actions()`(会顺手带回最新状态)。
|
||||
|
||||
- Windows 上**文件句柄还开着就 `os.remove()` 会失败**(PermissionError / WinError 32);
|
||||
若被 `except OSError` 吞掉就是静默不生效(`clear_pid_file` 曾因此永远删不掉 pid 文件)。
|
||||
一律"先读完 → 关掉句柄 → 再删"。
|
||||
|
||||
Reference in new issue
Block a user