Files
SimpleFarm/farm/config.py
T
snowy-yang cb4c1e7800 feat(engine): 四座自动化设施——喷灌 / 收割机 / 售货机 / 磨坊
为什么:自动化建筑只有水井和喂食器,中期以后每天的操作只剩浇水喂食
的重复劳动。新设施把"替代行动点动作"的思路推到完整流水线:
浇水(井/喷灌)→ 收割机收熟地 → 磨坊把陈粮变饲料 → 喂食器喂牲口
→ 售货机趁高价清库存,人不在也能持续产出。

- config.BUILDINGS 注册 4 座:喷灌系统 2800 金(全量浇水)、
  自动收割机 900 金 ×3(每天各收 2 块熟地,刻意不给经验——机器收的
  不算劳动,给手动玩法留价值)、自动售货机 1200 金(市价 > 基准价才
  出手、每天上限 5 份、越贵越先卖)、饲料磨坊 500 金(1 份最便宜的
  作物磨 3 份饲料)。
- engine._run_buildings 扩成六段流水线,顺序即依赖:磨坊排在喂食器前,
  早上磨的饲料当天就能喂上;收割机遇满仓停摆、作物留地(同喂食器
  缺料停摆哲学——自动系统绝不硬来,更绝不自动花钱)。
- webui / build 动作 / catalog 全部由 config.BUILDINGS 驱动,零改动。
- 测试 +6(喷灌全量浇水、收割入库且无经验、满仓停摆留作物、磨坊挑
  最便宜、磨坊→喂食器同日链路、售货机阈值与配额)。修复断言时确认
  两个既有行为:引擎自带 START_FEED=5;advance_day 末尾会把动物 fed
  复位("喂到了"的证据是 pending 累计而非 fed 本身)。
- 门禁:534 项 OK(skip 39)、ruff/pyright 全绿。
2026-09-25 21:05:27 +08:00

413 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""游戏平衡参数与静态定义。
本模块只描述"世界是什么样的"(作物表、动物表、经济参数),
不包含任何状态或逻辑,方便单独调参。
"""
from __future__ import annotations
from typing import Any
# --------------------------------------------------------------------------
# 基础经济参数
# --------------------------------------------------------------------------
START_GOLD = 120 # 初始金币
START_LEVEL = 1 # 初始等级
START_AP = 10 # 每天行动点上限
START_FEED = 5 # 初始饲料库存
BASE_PLOTS = 6 # 初始土地数量
MAX_PLOTS = 16 # 土地数量硬上限
PLOT_BASE_PRICE = 200 # 第一块新地的价格
PLOT_PRICE_GROWTH = 1.75 # 每多买一块,价格乘以该系数
FEED_UNIT_PRICE = 6 # 主动购买饲料的单价
FEED_EMERGENCY_PRICE = 9 # 自动补货(缺饲料时喂食)的惩罚单价
FEED_BULK = 10 # 一次购买饲料的默认批量
WATER_CAPACITY = 3 # 一次浇水可支撑的天数
PLANT_INITIAL_MOISTURE = 2 # 播种后地块自带的水分
PRICE_VOLATILITY = 0.18 # 每天价格波动幅度(±18%)
LUCKY_CHANCE = 0.12 # 丰收暴击概率(产出翻倍)
MAX_LOG_ENTRIES = 300 # 内存中保留的事件条数上限
MAX_FEED_PURCHASE = 200 # 单次购买饲料的数量上限
# 每级提供的土地上限(基础 6 + 2/级,封顶 MAX_PLOTS)
PLOTS_PER_LEVEL = 2
# 行动点消耗表
AP_COST = {
"plant": 1,
"water": 1,
"harvest": 0,
"feed": 1,
"collect": 0,
"buy_animal": 0,
"buy_plot": 0,
"buy_feed": 0,
"build": 0,
"sell": 0,
}
# 经验曲线:升到 level+1 所需经验
def exp_to_next(level: int) -> int:
"""返回从 level 升到 level+1 所需的经验值。"""
return int(80 * (max(1, level) ** 1.6))
# 升级奖励金币
def level_up_bonus(level: int) -> int:
"""升到 level 时发放的金币奖励。"""
return 80 + 40 * level
# --------------------------------------------------------------------------
# 作物表
# --------------------------------------------------------------------------
# key : 内部标识(API 与存档使用)
# name : 中文名
# seed_cost : 种子价格
# grow_days : 成熟所需生长天数(需要水分支撑)
# base_price : 收获物的基础售价
# exp : 每次收获获得的经验
# level_req : 解锁所需等级
CROPS: dict[str, dict[str, Any]] = {
"wheat": {
"name": "小麦",
"seed_cost": 5,
"grow_days": 2,
"base_price": 14,
"exp": 3,
"level_req": 1,
},
"carrot": {
"name": "胡萝卜",
"seed_cost": 8,
"grow_days": 3,
"base_price": 26,
"exp": 5,
"level_req": 1,
},
"tomato": {
"name": "番茄",
"seed_cost": 16,
"grow_days": 4,
"base_price": 52,
"exp": 9,
"level_req": 2,
},
"corn": {
"name": "玉米",
"seed_cost": 28,
"grow_days": 6,
"base_price": 95,
"exp": 14,
"level_req": 3,
},
"pumpkin": {
"name": "南瓜",
"seed_cost": 45,
"grow_days": 8,
"base_price": 180,
"exp": 22,
"level_req": 4,
},
"strawberry": {
"name": "草莓",
"seed_cost": 70,
"grow_days": 10,
"base_price": 320,
"exp": 35,
"level_req": 5,
},
}
# 生长阶段阈值(按生长进度 0.0~1.0 划分)
GROWTH_STAGES = [
(0.00, "已播种"),
(0.30, "幼苗"),
(0.65, "生长中"),
(1.00, "已成熟"),
]
# --------------------------------------------------------------------------
# 动物表
# --------------------------------------------------------------------------
# key : 内部标识
# name : 中文名
# cost : 购买价格
# product : 产出物标识(中文名)
# base_price : 产出物基础售价
# cycle : 产出周期(天)
# feed_per_day: 每天消耗饲料数
# exp : 每次收集产出获得的经验
# level_req : 解锁所需等级
ANIMALS: dict[str, dict[str, Any]] = {
"chicken": {
"name": "鸡",
"cost": 90,
"product": "鸡蛋",
"base_price": 15,
"cycle": 1,
"feed_per_day": 1,
"exp": 4,
"level_req": 1,
},
"sheep": {
"name": "羊",
"cost": 220,
"product": "羊毛",
"base_price": 55,
"cycle": 2,
"feed_per_day": 1,
"exp": 9,
"level_req": 2,
},
"cow": {
"name": "牛",
"cost": 480,
"product": "牛奶",
"base_price": 72,
"cycle": 1,
"feed_per_day": 2,
"exp": 15,
"level_req": 3,
},
}
MAX_ANIMALS = 8 # 牧场总容量
# --------------------------------------------------------------------------
# 时间
# --------------------------------------------------------------------------
# 一天有多长(真实秒数)。慢是刻意的:这是“放置农场”——
# 一天 40 分钟,作物 2~10 天成熟(小麦 80 分钟、草莓约 6.7 小时)。
# 没有任何“手动推进一天”的接口,时间只能自然流动。
DEFAULT_TICK_SECONDS = 2400.0
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 与界面上)
HARVESTER_PLOTS = 2 # 每台自动收割机每天收获的地块数
VENDOR_QUOTA = 5 # 自动售货机每天最多卖出的份数
MILL_OUTPUT = 3 # 饲料磨坊每份作物磨出的饲料量
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": "作物不再被暴风雨损毁,水分蒸发减半",
},
"sprinkler": {
"name": "喷灌系统",
"cost": 2800,
"level_req": 4,
"max_level": 1,
"effect": "每天自动给所有缺水的地块浇满",
},
"harvester": {
"name": "自动收割机",
"cost": 900,
"level_req": 3,
"max_level": 3,
"effect": f"每天自动收获 {HARVESTER_PLOTS} 块成熟作物入谷仓(不给经验)",
},
"vendor": {
"name": "自动售货机",
"cost": 1200,
"level_req": 4,
"max_level": 1,
"effect": f"每天自动卖出市价高于基准价的库存(上限 {VENDOR_QUOTA} 份)",
},
"mill": {
"name": "饲料磨坊",
"cost": 500,
"level_req": 2,
"max_level": 1,
"effect": f"每天把 1 份库存作物磨成 {MILL_OUTPUT} 份饲料(挑最便宜的)",
},
}
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
# --------------------------------------------------------------------------
# 进度目标
# --------------------------------------------------------------------------
GOALS = [
(200, "起步:攒够 200 金币"),
(600, "小农:攒够 600 金币"),
(1500, "农场主:攒够 1500 金币"),
(4000, "庄园主:攒够 4000 金币"),
(10000, "农业大亨:攒够 10000 金币"),
]
def goal_index(net_worth: float) -> int:
"""返回已达成的最低目标索引(-1 表示一个都没达成)。"""
reached = -1
for i, (amount, _) in enumerate(GOALS):
if net_worth >= amount:
reached = i
return reached