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
+80
-2
@@ -219,6 +219,17 @@ class GameState:
|
||||
self.total_spent: int = 0
|
||||
self.plots_bought: int = 0
|
||||
|
||||
# 谷仓:收获先进这里,卖不卖、什么时候卖由玩家决定。
|
||||
# 键与 self.market 完全同构(作物用 "wheat",畜产品用 "animal:chicken"),
|
||||
# 这样定价、序列化、存档校验都只有一套规则。
|
||||
self.inventory: dict[str, int] = {}
|
||||
# 建筑:key -> 已建座数(0 表示还没造)
|
||||
self.buildings: dict[str, int] = dict.fromkeys(config.BUILDINGS, 0)
|
||||
# 天气:今天与明天。掷骰在 engine 里做,模型层只持有结果,
|
||||
# 这样"存/读档"不需要重放随机序列就能复原当天的天气。
|
||||
self.weather: str = config.DEFAULT_WEATHER
|
||||
self.forecast: str = config.DEFAULT_WEATHER
|
||||
|
||||
# ---------------------------------------------------------------- 派生属性
|
||||
@property
|
||||
def max_ap(self) -> int:
|
||||
@@ -254,7 +265,7 @@ class GameState:
|
||||
|
||||
@property
|
||||
def net_worth(self) -> float:
|
||||
"""净资产:金币 + 已购地价 + 牲畜折价 + 库存 + 地里未收的作物。"""
|
||||
"""净资产:金币 + 已购地价 + 牲畜折价 + 饲料 + 地里未收的作物 + 谷仓存货。"""
|
||||
land = sum(
|
||||
config.PLOT_BASE_PRICE * (config.PLOT_PRICE_GROWTH**i) for i in range(self.plots_bought)
|
||||
)
|
||||
@@ -262,7 +273,8 @@ class GameState:
|
||||
for plot in self.plots:
|
||||
if plot.crop:
|
||||
crops += self.market.get(plot.crop, 0) * (plot.growth / max(1, plot.grow_days))
|
||||
return self.gold + land + self.livestock_value + self.inventory_value + crops
|
||||
stored = self.goods_value
|
||||
return self.gold + land + self.livestock_value + self.inventory_value + crops + stored
|
||||
|
||||
@property
|
||||
def goal_index(self) -> int:
|
||||
@@ -272,6 +284,32 @@ class GameState:
|
||||
def next_goal(self) -> dict[str, Any] | None:
|
||||
return next_goal_after(self.goal_index)
|
||||
|
||||
@property
|
||||
def granary_capacity(self) -> int:
|
||||
"""仓储上限:基础值 + 等级成长 + 谷仓建筑。"""
|
||||
cap = (
|
||||
config.GRANARY_BASE
|
||||
+ config.GRANARY_PER_LEVEL * (self.level - 1)
|
||||
+ config.GRANARY_PER_UPGRADE * self.building_count("granary")
|
||||
)
|
||||
return int(clamp(cap, config.GRANARY_BASE, config.MAX_GRANARY))
|
||||
|
||||
@property
|
||||
def storage_used(self) -> int:
|
||||
return sum(self.inventory.values())
|
||||
|
||||
@property
|
||||
def granary_free(self) -> int:
|
||||
return max(0, self.granary_capacity - self.storage_used)
|
||||
|
||||
@property
|
||||
def goods_value(self) -> float:
|
||||
"""谷仓存货按**当前市价**的估值(会随市场波动,这正是囤货的风险与收益)。"""
|
||||
return sum(count * self.market.get(key, 0.0) for key, count in self.inventory.items())
|
||||
|
||||
def building_count(self, key: str) -> int:
|
||||
return int(self.buildings.get(key, 0))
|
||||
|
||||
# ---------------------------------------------------------------- 序列化
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
# net_worth 要遍历地块与牲畜,是这里最贵的一项;下面三处都要用它,
|
||||
@@ -280,6 +318,11 @@ class GameState:
|
||||
goal = config.goal_index(worth)
|
||||
return {
|
||||
"day": self.day,
|
||||
"weather": self.weather,
|
||||
"weather_name": config.weather_info(self.weather)["name"],
|
||||
"weather_desc": config.weather_info(self.weather)["desc"],
|
||||
"forecast": self.forecast,
|
||||
"forecast_name": config.weather_info(self.forecast)["name"],
|
||||
"gold": self.gold,
|
||||
"level": self.level,
|
||||
"exp": self.exp,
|
||||
@@ -287,6 +330,11 @@ class GameState:
|
||||
"ap": self.ap,
|
||||
"max_ap": self.max_ap,
|
||||
"feed": self.feed,
|
||||
"inventory": dict(sorted(self.inventory.items())),
|
||||
"storage_used": self.storage_used,
|
||||
"granary_capacity": self.granary_capacity,
|
||||
"granary_free": self.granary_free,
|
||||
"buildings": dict(self.buildings),
|
||||
"plots": [p.to_dict() for p in self.plots],
|
||||
"plot_count": len(self.plots),
|
||||
"max_plots": self.max_plots,
|
||||
@@ -333,6 +381,22 @@ class GameState:
|
||||
animals.append(animal)
|
||||
state.animals = animals
|
||||
|
||||
raw_buildings = data.get("buildings", {})
|
||||
if isinstance(raw_buildings, dict):
|
||||
for key, info in config.BUILDINGS.items():
|
||||
try:
|
||||
owned = int(raw_buildings.get(key, 0))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
state.buildings[key] = int(clamp(owned, 0, info["max_level"]))
|
||||
|
||||
weather = data.get("weather")
|
||||
if isinstance(weather, str) and weather in config.WEATHER:
|
||||
state.weather = weather
|
||||
forecast = data.get("forecast")
|
||||
if isinstance(forecast, str) and forecast in config.WEATHER:
|
||||
state.forecast = forecast
|
||||
|
||||
market = data.get("market", {})
|
||||
for key in list(state.market.keys()):
|
||||
try:
|
||||
@@ -341,6 +405,20 @@ class GameState:
|
||||
continue
|
||||
state.market[key] = clamp(value, 1.0, 100000.0)
|
||||
|
||||
# 存货键必须存在于 market:顺手挡住"手改存档塞进来的野键",
|
||||
# 否则它会一直躺在谷仓里、既卖不掉也算不进净资产。
|
||||
raw_inventory = data.get("inventory", {})
|
||||
if isinstance(raw_inventory, dict):
|
||||
for key, count in raw_inventory.items():
|
||||
if key not in state.market:
|
||||
continue
|
||||
try:
|
||||
amount = int(count)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if amount > 0:
|
||||
state.inventory[key] = amount
|
||||
|
||||
stats = data.get("stats", {})
|
||||
state.total_harvested = max(0, int(stats.get("harvested", 0)))
|
||||
state.total_collected = max(0, int(stats.get("collected", 0)))
|
||||
|
||||
Reference in new issue
Block a user