为什么:游戏的时间(40 分钟一天、只能自然流动)本身就是唯一的行动节奏 阀门,再叠一层"每天 10 点行动点"纯属重复惩罚——浇水还要体力既不拟真 也不好玩。删掉之后,稀缺的只剩时间本身,放置玩法的定位反而更纯粹。 改动范围(引擎到界面连根拔): - config 删 START_AP/AP_COST;models 删 ap 字段与 max_ap,存档 payload 里的旧 ap 键被 from_dict 自然忽略; - engine 删 _spend_ap 与全部 8 个调用点、每日恢复逻辑与日志; available_actions 不再按行动点筛,cost 只剩金币; - storage 建表去掉 ap 列,老库在 connect() 里 ALTER TABLE DROP COLUMN 无痛迁移(实测 saves/farm.db 已自动迁完); - bot 删 AP 分支(无 AP 后 available_actions 恒可用);client/TUI/CLI/ README/帮助文本同步清理。 网页端顺带两件事: - "下一天"倒计时改为客户端本地走秒——SSE 只在有变化时推送,以前倒计时 只在推送瞬间跳变,现在每秒都在动(记基点 + 1s 定时器补齐); - 字体优化:页面字体栈补齐 MiSans/HarmonyOS Sans/PingFang,正文字号 14→15,统计数字 tabular-nums;canvas 小字统一字体栈并加粗提对比度。 测试适配:删 2 个 AP 专属用例(无 AP 可测)、改名/收敛 5 个, 532 项全绿(skip 39),ruff/pyright 干净。真机验证:老库迁移、 统计条无行动点、倒计时逐秒递减(39:04→39:02)、动作不再报 no_ap。
398 lines
12 KiB
Python
398 lines
12 KiB
Python
"""游戏平衡参数与静态定义。
|
||
|
||
本模块只描述"世界是什么样的"(作物表、动物表、经济参数),
|
||
不包含任何状态或逻辑,方便单独调参。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 基础经济参数
|
||
# --------------------------------------------------------------------------
|
||
|
||
START_GOLD = 120 # 初始金币
|
||
START_LEVEL = 1 # 初始等级
|
||
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
|
||
|
||
|
||
# 经验曲线:升到 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
|