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

+495 -26
View File
@@ -57,6 +57,11 @@ class FarmEngine:
# 状态变化回调(TUI 用它触发重绘)
self._listeners: list[Callable[[], None]] = []
# 开局先把今天与明天的天气掷好:否则第一天会顶着 DEFAULT_WEATHER 的
# 占位值开局,而"预报"字段又和它对不上。
self.state.weather = self._roll_weather()
self.state.forecast = self._roll_weather(previous=self.state.weather)
# ================================================================ 订阅
def subscribe(self, callback: Callable[[], None]) -> None:
with self._lock:
@@ -137,6 +142,16 @@ class FarmEngine:
"seconds_to_tick": self.seconds_to_tick,
"paused": self.paused,
"tick_seconds": self.tick_seconds,
"weather": state.weather,
"weather_name": config.weather_info(state.weather)["name"],
"forecast": state.forecast,
"forecast_name": config.weather_info(state.forecast)["name"],
"storage_used": state.storage_used,
"granary_capacity": state.granary_capacity,
"granary_free": state.granary_free,
# 存货是给 AI 的"卖什么"清单;只带非空项,通常只有几行
"inventory": dict(state.inventory),
"building_count": sum(state.buildings.values()),
# 待办清单是特意为 AI 准备的:直接读模型对象,不再过一遍序列化
"ripe_plots": [p.index + 1 for p in state.plots if p.is_ripe],
"hungry_animals": [a.aid for a in state.animals if not a.fed],
@@ -188,14 +203,98 @@ class FarmEngine:
"profit_per_day": round((price - feed_cost) / max(1, info["cycle"]), 2),
}
)
goods = self._goods_catalog()
return {
"crops": crops,
"animals": animals,
"goods": goods,
"buildings": self._building_catalog(),
"weather": self._weather_catalog(),
"granary": {
"used": self.state.storage_used,
"capacity": self.state.granary_capacity,
},
"feed_price": config.FEED_UNIT_PRICE,
"water_capacity": config.WATER_CAPACITY,
"ap_cost": config.AP_COST,
}
# ---- catalog 的三个子表。拆开是因为它们的"变化频率"各不相同:
# goods 随市价与存货变,buildings 随等级与已建数量变,weather 每天都在变。
def _goods_catalog(self) -> list[dict[str, Any]]:
"""所有可卖货物(作物 + 畜产品)的现价、基准价与仓库存量。
合成一张表是刻意的:``sell`` 的 key 空间就是这张表的 key 空间,
客户端不必自己拼 "animal:chicken" 这种前缀。
"""
state = self.state
goods: list[dict[str, Any]] = []
keys = list(config.CROPS) + [f"animal:{kind}" for kind in config.ANIMALS]
for key in keys:
price = self.market_price(key)
base = config.base_price(key)
stock = state.inventory.get(key, 0)
goods.append(
{
"key": key,
"name": config.goods_name(key),
"kind": "animal" if key.startswith("animal:") else "crop",
"price": price,
"base_price": base,
"ratio": round(price / base, 3) if base else 1.0,
"stock": stock,
"value": round(price * stock, 2),
}
)
return goods
def _building_catalog(self) -> list[dict[str, Any]]:
return [self._building_entry(self.state, key) for key in config.BUILDINGS]
def _building_entry(self, state: GameState, key: str) -> dict[str, Any]:
"""一座建筑的完整状态:报价、已建、上限、解锁。
catalog 与 available_actions 都读它——否则"报价"和"建议"两处
一旦各写一遍,迟早就出现"建议建、真去建却说太贵"这种矛盾。
"""
info = config.BUILDINGS[key]
owned = state.building_count(key)
maxed = owned >= info["max_level"]
return {
"key": key,
"name": info["name"],
"effect": info["effect"],
"cost": info["cost"],
"next_cost": -1 if maxed else config.build_price(key, owned),
"level_req": info["level_req"],
"locked": state.level < info["level_req"],
"owned": owned,
"max_level": info["max_level"],
"maxed": maxed,
}
def _weather_catalog(self) -> dict[str, Any]:
"""今天的天气、明天的预报,以及这张天气表本身(给 AI 讲清规则用)。"""
state = self.state
return {
"today": state.weather,
"today_name": config.weather_info(state.weather)["name"],
"forecast": state.forecast,
"forecast_name": config.weather_info(state.forecast)["name"],
"evaporation": self._evaporation(state, config.weather_info(state.weather)),
"table": [
{
"key": key,
"name": info["name"],
"desc": info["desc"],
"evaporation": info["evaporation"],
"water_gain": info["water_gain"],
"damage": info["damage"],
}
for key, info in config.WEATHER.items()
],
}
def available_actions(self) -> list[dict[str, Any]]:
"""列出当前**合法**的动作,供 AI 直接决策使用。
@@ -207,7 +306,9 @@ class FarmEngine:
actions: list[dict[str, Any]] = []
actions.extend(self._plot_actions(state))
actions.extend(self._animal_actions(state))
actions.extend(self._market_actions(state))
actions.extend(self._investment_actions(state))
actions.extend(self._build_actions(state))
return actions
# ---- available_actions 的三个分区;拆开是为了各自能独立阅读/测试
@@ -245,13 +346,18 @@ class FarmEngine:
"reason": f"{position} 号地的{config.CROPS[plot.crop]['name']}已成熟",
}
)
elif plot.moisture == 0 and state.ap >= config.AP_COST["water"]:
elif self._needs_water(plot) and state.ap >= config.AP_COST["water"]:
reason = (
f"{position} 号地缺水,作物已停止生长"
if plot.moisture <= 0
else f"{position} 号地水分只够撑到今天,不浇明天就会停长"
)
actions.append(
{
"action": "water",
"params": {"plot": position},
"cost": {"gold": 0, "ap": config.AP_COST["water"]},
"reason": f"{position} 号地缺水,作物已停止生长",
"reason": reason,
}
)
return actions
@@ -321,6 +427,59 @@ class FarmEngine:
)
return actions
def _market_actions(self, state: GameState) -> list[dict[str, Any]]:
"""该不该卖货:谷仓快满了要腾地方,价格明显高于基准价则值得出手。
这是"择时卖出"在 AI 侧的落点:把"现价 vs 基准价"直接算好写进 reason,
机器人/LLM 不需要自己记住每种货的基准价,也就能真的做出囤或抛的判断。
"""
actions: list[dict[str, Any]] = []
if not state.inventory:
return actions
used, capacity = state.storage_used, state.granary_capacity
tight = used >= capacity * 0.8
for key, count in sorted(state.inventory.items()):
if count <= 0:
continue
price = self.market_price(key)
base = config.base_price(key)
ratio = price / base if base else 1.0
reasons = [f"谷仓有 {count} 份{config.goods_name(key)},现价 {price}(基准 {base})"]
if tight:
reasons.append(f"谷仓已用 {used}/{capacity},该腾地方了")
if ratio >= 1.1:
reasons.append(f"比基准价高 {(ratio - 1) * 100:.0f}%,适合出手")
if len(reasons) == 1:
# 价格没优势、仓也不挤 → 建议继续囤着。不推荐就别出声,
# 否则候选动作里全是"卖了吧",AI 反而挑不出重点。
continue
actions.append(
{
"action": "sell",
"params": {"key": key, "quantity": count},
"cost": {"gold": 0, "ap": config.AP_COST["sell"]},
"reason": ";".join(reasons),
}
)
return actions
def _build_actions(self, state: GameState) -> list[dict[str, Any]]:
"""能造什么:等级够、没建满、且钱够。"""
actions: list[dict[str, Any]] = []
for key in config.BUILDINGS:
entry = self._building_entry(state, key)
if entry["locked"] or entry["maxed"] or state.gold < entry["next_cost"]:
continue
actions.append(
{
"action": "build",
"params": {"key": key},
"cost": {"gold": entry["next_cost"], "ap": config.AP_COST["build"]},
"reason": f"可造第 {entry['owned'] + 1} 座{entry['name']}:{entry['effect']}",
}
)
return actions
# ================================================================ 内部工具
def _is_locked(self, info: dict[str, Any]) -> bool:
"""这件作物/牲畜在当前等级是否还没解锁。
@@ -343,7 +502,7 @@ class FarmEngine:
"""正好在 level 这一级解锁的作物与牲畜名字(先作物后牲畜)。"""
return [
info["name"]
for table in (config.CROPS, config.ANIMALS)
for table in (config.CROPS, config.ANIMALS, config.BUILDINGS)
for info in table.values()
if info["level_req"] == level
]
@@ -424,6 +583,45 @@ class FarmEngine:
"""丰收回合判定:常规产出 1,暴击产出 2。"""
return 2 if random.random() < config.LUCKY_CHANCE else 1
def _store(self, key: str, amount: int) -> None:
"""把货物放进谷仓;装不下就抛 granary_full,且**不改动任何状态**。
先判容量再落袋,所以收获撞上满仓时作物还好好地留在地里,
玩家卖掉一点东西就能重来,不会"收了个寂寞"。
"""
amount = int(amount)
if amount <= 0:
return
state = self.state
free = state.granary_free
if amount > free:
raise GameError(
"granary_full",
f"谷仓装不下:还差 {amount - free} 格"
f"(已用 {state.storage_used}/{state.granary_capacity})。"
"先 sell 卖掉点存货,或者 build 一座谷仓。",
)
state.inventory[key] = state.inventory.get(key, 0) + amount
def _needs_water(self, plot: Plot) -> bool:
"""这块地现在该不该浇水。
* 已经干透 → 今天就长不了,必须浇;
* 水分刚好够今天蒸发、而明天又不下雨 → 明天会停长,提前浇能少浪费一天。
第二条用到了明天的预报——这就是"预报"这个字段存在的意义:
让"今天浇不浇"变成能提前算清的决策,而不是撞运气。
"""
if plot.crop is None:
return False
if plot.moisture <= 0:
return True
forecast = config.weather_info(self.state.forecast)
if forecast["water_gain"] > 0:
# 明天有雨,今天浇水等于把水倒进雨里
return False
return plot.moisture <= self._evaporation(self.state, forecast)
# ================================================================ 动作
def plant(self, plot_no: Any, crop: Any) -> dict[str, Any]:
with self._lock:
@@ -489,8 +687,11 @@ class FarmEngine:
self._spend_ap("harvest")
quantity = self._roll_yield()
# 先入库再清地:谷仓满了会在这里抛 granary_full,作物原地不动,
# 玩家卖掉一点就能重来,不会"收了个寂寞"。
self._store(crop_key, quantity)
price = self.market_price(crop_key)
earned = self._earn(price * quantity, info["name"])
value = round(price * quantity, 2)
plot.crop = None
plot.growth = 0.0
@@ -498,16 +699,21 @@ 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}入库,现价 {price}", "good")
levelups = self._gain_exp(info["exp"])
self._notify()
return {
"ok": True,
"message": f"收获 {quantity} 份{info['name']},获得 {earned} 金币",
"message": (
f"收获 {quantity} 份{info['name']}{lucky}入谷仓,现价 {price}"
f"(约 {value} 金币,需 sell 才能换成钱)"
),
"quantity": quantity,
"earned": earned,
"price": price,
"value": value,
"exp": info["exp"],
"level_ups": levelups,
"storage_used": self.state.storage_used,
}
def buy_animal(self, kind: Any) -> dict[str, Any]:
@@ -584,24 +790,31 @@ class FarmEngine:
)
self._spend_ap("collect")
quantity = animal.pending
key = f"animal:{animal.kind}"
# 与收获同理:先确认装得下,再清空待收,免得产出凭空消失
self._store(key, quantity)
animal.pending = 0
price = self.market_price(f"animal:{animal.kind}")
earned = self._earn(price * quantity, animal.product)
price = self.market_price(key)
value = round(price * quantity, 2)
self.state.total_collected += quantity
self.add_log(
f"收取 {quantity} 份{animal.product}({animal.name} #{animal.aid}),"
f"+{earned} 金币",
f"收取 {quantity} 份{animal.product}({animal.name} #{animal.aid})入库,"
f"现价 {price}",
"good",
)
levelups = self._gain_exp(animal.info["exp"] * quantity)
self._notify()
return {
"ok": True,
"message": f"收取 {quantity} 份{animal.product},获得 {earned} 金币",
"message": (
f"收取 {quantity} 份{animal.product}入谷仓,现价 {price}(约 {value} 金币)"
),
"quantity": quantity,
"earned": earned,
"price": price,
"value": value,
"level_ups": levelups,
"storage_used": self.state.storage_used,
}
def buy_feed(self, quantity: Any = config.FEED_BULK) -> dict[str, Any]:
@@ -650,6 +863,132 @@ class FarmEngine:
"next_plot_price": self.state.next_plot_price,
}
def sell(self, key: Any = None, quantity: Any = None) -> dict[str, Any]:
"""把谷仓里的货物按**当前市价**卖掉。
``key`` 省略 = 能卖的都卖;``quantity`` 省略 = 该货物全卖。
这是存货变回金币的唯一出口,所以 ``granary_full`` 时它是唯一的解法——
``available_actions`` 会在谷仓快满或价格见好时主动推荐它。
"""
with self._lock:
state = self.state
if key is None:
if quantity is not None:
raise GameError("bad_param", "不指定 key 时不能只卖指定数量")
if not state.inventory:
raise GameError("nothing_to_sell", "谷仓还是空的,没东西可卖")
return self._sell_everything(state)
if not isinstance(key, str) or config.goods_info(key) is None:
raise GameError(
"unknown_goods",
f"未知货物 {key!r},可选:{'、'.join(sorted(state.market))}",
)
name = config.goods_name(key)
count = state.inventory.get(key, 0)
if count <= 0:
raise GameError("nothing_to_sell", f"谷仓里没有{name}")
if quantity is None:
quantity = count
try:
quantity = int(quantity)
except (TypeError, ValueError):
raise GameError("bad_param", "卖出数量必须是整数") from None
if quantity <= 0:
raise GameError("bad_param", "卖出数量必须大于 0")
quantity = min(quantity, count)
price = self.market_price(key)
earned = self._earn(price * quantity, name)
left = count - quantity
if left > 0:
state.inventory[key] = left
else:
del state.inventory[key]
text = f"卖出 {quantity} 份{name}(单价 {price}),+{earned} 金币"
self.add_log(text, "good")
self._notify()
return {
"ok": True,
"message": text,
"key": key,
"quantity": quantity,
"price": price,
"earned": earned,
"gold": state.gold,
"storage_used": state.storage_used,
}
def _sell_everything(self, state: GameState) -> dict[str, Any]:
"""一键清仓。带上明细,回执才能说清"到底卖了什么、各卖了多少"。"""
sold: list[dict[str, Any]] = []
total = 0
for key in sorted(state.inventory):
count = state.inventory[key]
price = self.market_price(key)
earned = round(price * count)
total += earned
sold.append(
{
"key": key,
"name": config.goods_name(key),
"quantity": count,
"price": price,
"earned": earned,
}
)
state.inventory.clear()
self._earn(total, "卖出存货")
names = "、".join(f"{item['name']}×{item['quantity']}" for item in sold)
text = f"卖出 {names},共 +{total} 金币"
self.add_log(text, "good")
self._notify()
return {
"ok": True,
"message": text,
"earned": total,
"sold": sold,
"gold": state.gold,
"storage_used": state.storage_used,
}
def build(self, key: Any) -> dict[str, Any]:
"""造一座建筑。价格与解锁等级见 ``config.BUILDINGS``。"""
with self._lock:
if not isinstance(key, str) or key not in config.BUILDINGS:
raise GameError(
"unknown_building",
f"未知建筑 {key!r},可选:{'、'.join(config.BUILDINGS)}",
)
entry = self._building_entry(self.state, key)
if entry["locked"]:
raise GameError(
"locked",
f"{entry['name']}需要 Lv.{entry['level_req']},当前 Lv.{self.state.level}",
)
if entry["maxed"]:
raise GameError("build_limit", f"{entry['name']}已建满({entry['max_level']} 座)")
self._spend_gold(entry["next_cost"], f"建造{entry['name']}")
self._spend_ap("build")
self.state.buildings[key] = entry["owned"] + 1
built = self._building_entry(self.state, key)
self.add_log(
f"建成第 {built['owned']} 座{built['name']}(-{entry['next_cost']} 金币)",
"good",
)
self._notify()
return {
"ok": True,
"message": (
f"{built['name']} 已建成({built['owned']}/{built['max_level']}):"
f"{built['effect']}"
),
"building": built,
"gold": self.state.gold,
}
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
@@ -665,6 +1004,8 @@ class FarmEngine:
self.paused = False
if keep_tick:
self.tick_seconds = tick
self.state.weather = self._roll_weather()
self.state.forecast = self._roll_weather(previous=self.state.weather)
self.add_log("新的一天,新的开始。欢迎来到 SimpleFarm!", "info")
self._notify()
return {"ok": True, "message": "农场已重置"}
@@ -681,28 +1022,155 @@ class FarmEngine:
self.add_log(f"已存档到 {target}", "info")
return {"ok": True, "message": f"已存档到 {target}", "path": target}
# ================================================================ 天气与建筑
def _roll_weather(self, previous: str | None = None) -> str:
"""按权重掷一种天气;同一种不连着出现(最多重掷 WEATHER_REROLLS 次)。
权重已经表达了稀有度,连续性约束是额外补的一层手感——防的是
"一连五天干旱"这种既难玩、又不像真天气的序列。
"""
keys = list(config.WEATHER)
weights = [float(config.WEATHER[key]["weight"]) for key in keys]
pick = keys[0]
for _ in range(config.WEATHER_REROLLS):
pick = random.choices(keys, weights=weights, k=1)[0]
if pick != previous:
return pick
return pick
def _resolve_weather(self, state: GameState, forced: str | None) -> dict[str, Any]:
"""定下今天的天气,并把预报翻到明天,返回今天天气的定义。
"预报"是刻意做成**提前一天可知**的:这样"明天要干旱,今晚先浇一遍"
才是一个算得清的决策,而不是撞运气。正常玩法走预报;``forced``
只给测试与调试用(钉住"今天下雨"这类场景,否则断言会随掷骰抖动)。
"""
if forced is not None and forced in config.WEATHER:
today = forced
elif state.forecast in config.WEATHER:
today = state.forecast
else:
today = self._roll_weather()
state.weather = today
state.forecast = self._roll_weather(previous=today)
return config.WEATHER[today]
def _evaporation(self, state: GameState, info: dict[str, Any]) -> int:
"""当天实际蒸发量;温室让蒸发减半(向下取整)。
这是温室"省水"的那一半效果,另一半是免疫暴风雨(见 _storm_damage)。
"""
evaporate = int(info["evaporation"])
if state.building_count("greenhouse"):
evaporate //= 2
return evaporate
def _run_buildings(self, state: GameState) -> list[str]:
"""结算建筑的每日自动动作:水井浇水、喂食器喂食。
两件事都只替代"玩家本来要花行动点做的事",且都**不替玩家花钱**:
喂食器只在库存饲料够时动作,缺了就停摆并记一条日志,而不是像手动
喂食那样以应急价临时采购——自动系统悄悄花光金币是最容易被骂的设计。
"""
return self._wells(state) + self._feeders(state)
def _wells(self, state: GameState) -> list[str]:
quota = state.building_count("well")
if quota <= 0:
return []
events: list[str] = []
for plot in state.plots:
if quota <= 0:
break
if plot.crop is None or plot.moisture > 0:
continue
plot.moisture = config.WATER_CAPACITY
quota -= 1
events.append(f"水井给 {plot.index + 1} 号地浇了水")
return events
def _feeders(self, state: GameState) -> list[str]:
quota = state.building_count("feeder")
if quota <= 0:
return []
events: list[str] = []
for animal in state.animals:
if quota <= 0:
break
if animal.fed:
continue
need = int(animal.info["feed_per_day"])
if state.feed < need:
# 只报一次就停:饲料见底时每只都报一条会刷爆日志
events.append("饲料不够,自动喂食器停摆")
break
state.feed -= need
animal.fed = True
animal.starve = 0
quota -= 1
events.append(f"自动喂食器喂了{animal.name} #{animal.aid}")
return events
def _storm_damage(self, state: GameState, info: dict[str, Any]) -> list[str]:
"""暴风雨:每块**未成熟**的作物独立判定,被毁则地块直接清空。
成熟的作物不参与判定——"等你来收的东西被天灾吃掉"只会让人关掉游戏。
温室建成后整段失效。
"""
damage = float(info["damage"])
if damage <= 0 or state.building_count("greenhouse"):
return []
events: list[str] = []
for plot in state.plots:
if plot.crop is None or plot.is_ripe or random.random() >= damage:
continue
name = config.CROPS[plot.crop]["name"]
plot.crop = None
plot.growth = 0.0
plot.moisture = 0
plot.planted_day = 0
text = f"暴风雨毁了 {plot.index + 1} 号地的{name}"
events.append(text)
self.add_log(text, "bad")
return events
# ================================================================ 时间推进
def advance_day(self, reason: str = "自动") -> list[str]:
"""推进游戏内一天,返回本日发生的事件文本。"""
def advance_day(self, reason: str = "自动", weather: str | None = None) -> list[str]:
"""推进游戏内一天,返回本日发生的事件文本。
``weather`` 是留给测试与调试的显式入口:不传就按"昨天的预报"结算
(正常玩法走这条),传了就强制使用该天气。
"""
with self._lock:
state = self.state
info = self._resolve_weather(state, weather)
state.day += 1
state.ap = state.max_ap
events: list[str] = []
# ---- 作物生长
# ---- 建筑自动化。必须排在生长结算之前:水井救的正是"今天就断水"的
# 那块地,晚一步它今天就白等了。
events.extend(self._run_buildings(state))
# ---- 作物生长:水分消耗改由天气决定(0~2),浇水于是变成看天吃饭
evaporate = self._evaporation(state, info)
gain = int(info["water_gain"])
for plot in state.plots:
if plot.crop is None:
continue
if plot.moisture > 0:
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']}已成熟"
)
else:
if gain:
# 雨天/暴风雨先补水再蒸发,等于免费替你浇了一遍
plot.moisture = min(config.WATER_CAPACITY, plot.moisture + gain)
if plot.moisture <= 0:
events.append(f"{plot.index + 1} 号地缺水,作物停止生长")
continue
plot.moisture = max(0, plot.moisture - evaporate)
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.extend(self._storm_damage(state, info))
# ---- 牲畜
dead: list[Animal] = []
@@ -732,10 +1200,11 @@ class FarmEngine:
# ---- 写入日志(最多 4 条,避免刷屏)
for text in events[:4]:
self.add_log(text, "warn")
today = f"第 {state.day} 天开始({reason}),今日{info['name']}"
if events:
self.add_log(f"第 {state.day} 天开始({reason}),行动点已恢复", "info")
self.add_log(f"{today},行动点已恢复", "info")
else:
self.add_log(f"第 {state.day} 天开始({reason}),一切安好", "info")
self.add_log(f"{today},一切安好", "info")
self._notify()
return events