Files
SimpleFarm/farm/config.py
T
snowy-yang 15d290a60a 移除行动点系统,倒计时本地走秒,字体优化
为什么:游戏的时间(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。
2026-09-25 22:44:08 +08:00

398 lines
12 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_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