为什么:自动化建筑只有水井和喂食器,中期以后每天的操作只剩浇水喂食 的重复劳动。新设施把"替代行动点动作"的思路推到完整流水线: 浇水(井/喷灌)→ 收割机收熟地 → 磨坊把陈粮变饲料 → 喂食器喂牲口 → 售货机趁高价清库存,人不在也能持续产出。 - 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 全绿。
413 lines
13 KiB
Python
413 lines
13 KiB
Python
"""游戏平衡参数与静态定义。
|
||
|
||
本模块只描述"世界是什么样的"(作物表、动物表、经济参数),
|
||
不包含任何状态或逻辑,方便单独调参。
|
||
"""
|
||
|
||
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
|