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
+165
@@ -46,6 +46,8 @@ AP_COST = {
|
||||
"buy_animal": 0,
|
||||
"buy_plot": 0,
|
||||
"buy_feed": 0,
|
||||
"build": 0,
|
||||
"sell": 0,
|
||||
}
|
||||
|
||||
|
||||
@@ -193,6 +195,169 @@ MIN_TICK_SECONDS = 3.0 # 调试用:把一天缩到几秒
|
||||
MAX_TICK_SECONDS = 86400.0 # 上限给到一整天,想多慢都不会被夹住
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 天气
|
||||
# --------------------------------------------------------------------------
|
||||
# 每天随机一种天气,直接改写"水分"这条曲线:
|
||||
# * evaporation 决定作物每天掉多少水分(0~2),浇水于是从"常量消耗"变成看天吃饭;
|
||||
# * water_gain 是自动补水,雨天/暴风雨相当于免费替你浇了一遍;
|
||||
# * damage 只作用于**还没成熟**的地块——已经熟了、正等你收的东西不该被天灾吃掉。
|
||||
# 权重合计 100;同一种天气不连着出现(重掷,见 WEATHER_REROLLS),免得一连旱一周。
|
||||
#
|
||||
# key : 内部标识(存档与 API 使用)
|
||||
# name : 中文名
|
||||
# weight : 出现权重
|
||||
# evaporation : 当天水分蒸发量
|
||||
# water_gain : 当天自动补水(封顶 WATER_CAPACITY)
|
||||
# damage : 每块未成熟地块被毁的独立概率
|
||||
# desc : 给玩家与 AI 看的一句话
|
||||
|
||||
WEATHER: dict[str, dict[str, Any]] = {
|
||||
"sunny": {
|
||||
"name": "晴",
|
||||
"weight": 30,
|
||||
"evaporation": 1,
|
||||
"water_gain": 0,
|
||||
"damage": 0.0,
|
||||
"desc": "阳光充足,水分正常蒸发",
|
||||
},
|
||||
"cloudy": {
|
||||
"name": "多云",
|
||||
"weight": 27,
|
||||
"evaporation": 0,
|
||||
"water_gain": 0,
|
||||
"damage": 0.0,
|
||||
"desc": "日头不毒,作物几乎不失水",
|
||||
},
|
||||
"rain": {
|
||||
"name": "雨",
|
||||
"weight": 24,
|
||||
"evaporation": 0,
|
||||
"water_gain": WATER_CAPACITY,
|
||||
"damage": 0.0,
|
||||
"desc": "雨水充足,所有作物自动补满水分",
|
||||
},
|
||||
"drought": {
|
||||
"name": "干旱",
|
||||
"weight": 12,
|
||||
"evaporation": 2,
|
||||
"water_gain": 0,
|
||||
"damage": 0.0,
|
||||
"desc": "水分蒸发加倍,一天不浇就可能停长",
|
||||
},
|
||||
"storm": {
|
||||
"name": "暴风雨",
|
||||
"weight": 7,
|
||||
"evaporation": 0,
|
||||
"water_gain": WATER_CAPACITY,
|
||||
"damage": 0.12,
|
||||
"desc": "暴雨伴狂风,未成熟的作物可能被毁",
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_WEATHER = "sunny" # 新牧场开局天气
|
||||
WEATHER_REROLLS = 3 # 同一天气最多重掷几次
|
||||
|
||||
|
||||
def weather_info(key: str | None) -> dict[str, Any]:
|
||||
"""取某种天气的完整定义;未知 key 退回默认天气。"""
|
||||
return WEATHER.get(key or "", WEATHER[DEFAULT_WEATHER])
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 谷仓(仓储 + 自由买卖)
|
||||
# --------------------------------------------------------------------------
|
||||
# 收获不再当场按市价卖掉,而是先进谷仓;卖不卖、什么时候卖由玩家决定。
|
||||
# 这样"市场每天波动"才第一次变成真决策:囤到高位再出,或者急着用钱低价走。
|
||||
|
||||
GRANARY_BASE = 40 # 初始仓容
|
||||
GRANARY_PER_LEVEL = 10 # 每升 1 级 +10
|
||||
GRANARY_PER_UPGRADE = 25 # 每座谷仓建筑 +25
|
||||
MAX_GRANARY = 400 # 仓容硬上限
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 建筑
|
||||
# --------------------------------------------------------------------------
|
||||
# 给金币一个"买地 / 买牲畜"之外的长期出口,而且买的是**自动化**:
|
||||
# 花一笔钱,换掉一件每天都要手动做的事。
|
||||
#
|
||||
# key : 内部标识
|
||||
# name : 中文名
|
||||
# cost : 第一座的价格(第 owned+1 座 = cost * BUILD_PRICE_GROWTH ** owned)
|
||||
# level_req : 解锁等级
|
||||
# max_level : 最多能造几座
|
||||
# effect : 一句话说明(原样出现在 catalog 与界面上)
|
||||
|
||||
BUILDINGS: dict[str, dict[str, Any]] = {
|
||||
"granary": {
|
||||
"name": "谷仓",
|
||||
"cost": 300,
|
||||
"level_req": 1,
|
||||
"max_level": 5,
|
||||
"effect": f"仓储上限 +{GRANARY_PER_UPGRADE}",
|
||||
},
|
||||
"well": {
|
||||
"name": "水井",
|
||||
"cost": 400,
|
||||
"level_req": 2,
|
||||
"max_level": 3,
|
||||
"effect": "每天自动给 1 块干透的地浇满",
|
||||
},
|
||||
"feeder": {
|
||||
"name": "自动喂食器",
|
||||
"cost": 650,
|
||||
"level_req": 3,
|
||||
"max_level": 3,
|
||||
"effect": "每天自动喂 1 只牲畜(只吃库存饲料,不自动花钱)",
|
||||
},
|
||||
"greenhouse": {
|
||||
"name": "温室",
|
||||
"cost": 1500,
|
||||
"level_req": 5,
|
||||
"max_level": 1,
|
||||
"effect": "作物不再被暴风雨损毁,水分蒸发减半",
|
||||
},
|
||||
}
|
||||
|
||||
BUILD_PRICE_GROWTH = 1.6 # 同类建筑第 n 座的价格系数
|
||||
|
||||
|
||||
def build_price(key: str, owned: int) -> int:
|
||||
"""第 owned+1 座建筑的价格(owned 是这类建筑已建的数量)。"""
|
||||
return int(BUILDINGS[key]["cost"] * (BUILD_PRICE_GROWTH**owned))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 货物(可买卖的东西)
|
||||
# --------------------------------------------------------------------------
|
||||
# 谷仓、市场、sell 动作共用同一套 key:作物直接用作物 key(wheat),
|
||||
# 畜产品用 animal:<牲畜 key>(animal:chicken)。命名收敛在一处,
|
||||
# 客户端就不必自己拼前缀,也不会出现「两个地方各拼一种」的漂移。
|
||||
|
||||
|
||||
def goods_info(key: str) -> dict[str, Any] | None:
|
||||
"""货物 key 对应的静态定义;未知 key 返回 None。"""
|
||||
if key.startswith("animal:"):
|
||||
return ANIMALS.get(key.split(":", 1)[1])
|
||||
return CROPS.get(key)
|
||||
|
||||
|
||||
def goods_name(key: str) -> str:
|
||||
"""货物 key -> 中文名。畜产品给的是产出名(鸡蛋)而不是牲畜名(鸡)。"""
|
||||
if key.startswith("animal:"):
|
||||
info = ANIMALS.get(key.split(":", 1)[1])
|
||||
return str(info["product"]) if info else key
|
||||
info = CROPS.get(key)
|
||||
return str(info["name"]) if info else key
|
||||
|
||||
|
||||
def base_price(key: str) -> float:
|
||||
"""货物 key 的基准价,也就是市场每天围绕它波动的那个中心。"""
|
||||
info = goods_info(key)
|
||||
return float(info["base_price"]) if info else 0.0
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 进度目标
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user