diff --git a/README.md b/README.md index f520936..fd44921 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ uv run main.py solo # 本地引擎 + 终端界面,不开 | 系统 | 规则 | | --- | --- | | **时间** | 每个牧场每 **40 分钟**自动推进一天(`--tick` 可调),作物生长、牲畜产出、市场波动都在此刻结算 | -| **行动点** | 每天 10 点;播种 / 浇水 / 喂食各 1 点,收获、收取、卖货、建造都免费。用完只能等第二天——时间只能自然流动,没有手动快进 | +| **节奏** | 动作**没有**行动点/体力限制,想种就种——稀缺的只有时间本身:一天就是 40 分钟,只能自然流动,没有手动快进 | | **天气** | 每天从晴 / 多云 / 雨 / 干旱 / 暴风雨里抽一种(同种不连着出),而且**明天的天气提前可见**——「今晚浇不浇」因此是个能算的决策 | | **水分** | 每天按天气消耗:晴 1、多云 0、干旱 2,雨天反补 3(上限 3);水分为 0 就停止生长。播种自带 2 点,只够晴天撑两天,所以下种前得先看预报 | | **作物** | 小麦 → 胡萝卜 → 番茄 → 玉米 → 南瓜 → 草莓,按等级解锁,售价越高成熟越慢 | @@ -73,7 +73,7 @@ uv run main.py solo # 本地引擎 + 终端界面,不开 净资产在 TUI 顶部实时显示,`goal_index` 表示已解锁的里程碑数。 > **关于节奏**:这是一款“放置型”农场,一天 40 分钟是刻意设计的,不是笔误。 -> 每天 10 点行动点,用完就是等时间流动——所以一天的玩法是“上来干十件事,然后离开”。 +> 动作没有体力限制,但作物、牲畜、市场的进展都随换天推进——所以一天的玩法是“上来把事干完,然后离开”。 > 小麦 2 天(80 分钟)成熟,草莓 10 天(约 6.7 小时);断水会停长,所以实际天数看天吃饭—— > 好在预报提前一天就给你,来得及浇。想调试就把节奏调快: > `uv run main.py play --tick 3`(一天 3 秒)。 @@ -125,18 +125,18 @@ uv run main.py solo # 本地引擎 + 终端界面,不开 ```bash $ uv run main.py actions -牧场 default · 第 3 天 · 金币 96 · 行动点 7/10 +牧场 default · 第 3 天 · 金币 96 共 9 个候选动作: [ 1] harvest {"plot": 2} (免费) 2 号地的胡萝卜已成熟 - [ 2] plant {"plot": 4, "crop": "wheat"} (gold:5 ap:1) + [ 2] plant {"plot": 4, "crop": "wheat"} (gold:5) 4 号地空闲,可种小麦 ... $ uv run main.py do harvest '{"plot":2}' ✓ 收获 2 份胡萝卜(丰收!),获得 104 金币 (0.01s) - 第 3 天 · 金币 200 · 行动点 7/10 · 净资产 512.0 + 第 3 天 · 金币 200 · 净资产 512.0 ``` --- @@ -189,7 +189,7 @@ POST /api/actions → {"action":"<上一步的 action>","params":{. plant(plot,crop) water(plot) harvest(plot) harvest_all() buy_animal(kind) feed(animal_id,quantity?) collect(animal_id) buy_feed(quantity?) buy_plot() pause(paused?) -sell(key?,quantity?) build(key) ← 谷仓变现与建造,0 行动点 +sell(key?,quantity?) build(key) ← 谷仓变现与建造 ``` > `save` / `reset` 是**存档类动作**,不进通用动作表:它们必须由牧场会话处理, @@ -215,7 +215,7 @@ curl -X POST 'http://127.0.0.1:8765/api/batch?farm=alice' \ {"action":"buy_feed","params":{"quantity":10}}, {"action":"plant","params":{"plot":1,"crop":"carrot"}}]}' -# 收成变现 / 造建筑:都是 0 行动点,行动点用完了照样能做 +# 收成变现 / 造建筑 curl -X POST 'http://127.0.0.1:8765/api/actions?farm=alice' \ -H 'Content-Type: application/json' -d '{"action":"sell"}' curl -X POST 'http://127.0.0.1:8765/api/actions?farm=alice' \ @@ -481,7 +481,7 @@ None 误用、属性不存在、参数类型不符。本项目的状态快照是 * 未知动作 → `400 unknown_action`,并列出可用动作; * 缺必需参数 → `missing_param`;多传参数 → `bad_param`(明确拒绝而不是静默忽略); * 类型错误 → `bad_param`,但宽容地把 `"3"` 这类数字字符串转成整数(命令行/表单常见); -* 越界地块 → `no_such_plot`;未解锁作物 → `locked`;金币/行动点不足 → `not_enough_gold` / `no_ap`; +* 越界地块 → `no_such_plot`;未解锁作物 → `locked`;金币不足 → `not_enough_gold`; * 牧场名会被用作文件名 → 白名单校验,挡掉 `../`、`a/b` 之类的路径穿越; * 请求体上限 64KB、批量动作上限 50、`limit` 参数自动夹取到合法区间。 diff --git a/farm/bot.py b/farm/bot.py index 4c399a6..005dd45 100644 --- a/farm/bot.py +++ b/farm/bot.py @@ -13,8 +13,8 @@ 它只依赖 :mod:`farm.client`,因此既能在本机跑,也能从别处连上服务器跑—— 换个 ``--url`` 就是一个独立的外挂玩家。 -节奏完全由服务器时钟决定。服务器**没有**"手动推进一天"的接口,所以行动点 -用完之后它只能等时间流动(或收工)::meth:`RuleBot.run` 的 ``wait_seconds`` +节奏完全由服务器时钟决定。服务器**没有**"手动推进一天"的接口,没事可做时 +它只能等时间流动(或收工)::meth:`RuleBot.run` 的 ``wait_seconds`` 就是"最多等多久",默认 0——一天 40 分钟,干等不划算。 """ @@ -69,7 +69,6 @@ class RuleBot: params = action.get("params") or {} gold = state.get("gold", 0) feed = state.get("feed", 0) - ap = state.get("ap", 0) if name == "harvest": return 100.0 @@ -101,7 +100,7 @@ class RuleBot: (a for a in catalog.get("animals", []) if a["key"] == params.get("kind")), None ) cost = info["cost"] if info else 0 - if cost and gold >= cost * 3 and ap >= 1: + if cost and gold >= cost * 3: return 30.0 + (info.get("profit_per_day", 0) if info else 0) return 3.0 if name == "sell": @@ -213,16 +212,6 @@ class RuleBot: self.say("到时间了,收工。") break - state = self.client.snapshot() - if state.get("ap", 0) <= 0: - # 行动点用完 ≠ 没事可做:sell / build / harvest / collect / buy_* - # 都是 0 消耗,而 available_actions 本身就按行动点筛过一遍了, - # 所以照常挑一次最优的;真的没得选才等下一天。 - if self.step() is None and not self.wait_for_next_day(wait_seconds): - break - idle = 0 - continue - did = self.step() if did is None: idle += 1 diff --git a/farm/client.py b/farm/client.py index 3895850..91bd56a 100644 --- a/farm/client.py +++ b/farm/client.py @@ -218,10 +218,6 @@ class StateView: def exp(self) -> int: return int(self._d.get("exp", 0)) - @property - def ap(self) -> int: - return int(self._d.get("ap", 0)) - @property def feed(self) -> int: return int(self._d.get("feed", 0)) @@ -315,7 +311,6 @@ class RemoteFarm: "granary": {}, "feed_price": config.FEED_UNIT_PRICE, "water_capacity": config.WATER_CAPACITY, - "ap_cost": config.AP_COST, } self._log: deque[dict[str, Any]] = deque(maxlen=LOG_KEEP) self._log_seq = 0 diff --git a/farm/config.py b/farm/config.py index bba2069..82df639 100644 --- a/farm/config.py +++ b/farm/config.py @@ -14,7 +14,6 @@ from typing import Any START_GOLD = 120 # 初始金币 START_LEVEL = 1 # 初始等级 -START_AP = 10 # 每天行动点上限 START_FEED = 5 # 初始饲料库存 BASE_PLOTS = 6 # 初始土地数量 @@ -36,20 +35,6 @@ 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: diff --git a/farm/engine.py b/farm/engine.py index 1d7deab..aa790ef 100644 --- a/farm/engine.py +++ b/farm/engine.py @@ -129,8 +129,6 @@ class FarmEngine: "level": state.level, "exp": state.exp, "exp_needed": state.exp_needed, - "ap": state.ap, - "max_ap": state.max_ap, "feed": state.feed, "plot_count": len(state.plots), "max_plots": state.max_plots, @@ -216,7 +214,6 @@ class FarmEngine: }, "feed_price": config.FEED_UNIT_PRICE, "water_capacity": config.WATER_CAPACITY, - "ap_cost": config.AP_COST, } # ---- catalog 的三个子表。拆开是因为它们的"变化频率"各不相同: @@ -299,7 +296,7 @@ class FarmEngine: """列出当前**合法**的动作,供 AI 直接决策使用。 每个动作包含:``action``(接口名)、``params``(参数模板)、 - ``cost``(金币/行动点开销)、``reason``(为什么值得做)。 + ``cost``(金币开销)、``reason``(为什么值得做)。 """ with self._lock: state = self.state @@ -314,15 +311,12 @@ class FarmEngine: # ---- available_actions 的三个分区;拆开是为了各自能独立阅读/测试 def _plot_actions(self, state: GameState) -> list[dict[str, Any]]: """每块地当前能做的事:播种 / 收获 / 浇水。""" - # 能种什么只取决于等级、金币和行动点,与**具体是哪块地**无关, + # 能种什么只取决于等级和金币,与**具体是哪块地**无关, # 所以整趟只筛一次,不要放进地块循环里做 O(地块 × 作物) 次判断。 - plant_ap = config.AP_COST["plant"] plantable = [ (key, info) for key, info in config.CROPS.items() - if not self._is_locked(info) - and state.gold >= info["seed_cost"] - and state.ap >= plant_ap + if not self._is_locked(info) and state.gold >= info["seed_cost"] ] actions: list[dict[str, Any]] = [] for plot in state.plots: @@ -333,7 +327,7 @@ class FarmEngine: { "action": "plant", "params": {"plot": position, "crop": key}, - "cost": {"gold": info["seed_cost"], "ap": plant_ap}, + "cost": {"gold": info["seed_cost"]}, "reason": f"{position} 号地空闲,可种{info['name']}", } ) @@ -342,11 +336,11 @@ class FarmEngine: { "action": "harvest", "params": {"plot": position}, - "cost": {"gold": 0, "ap": config.AP_COST["harvest"]}, + "cost": {"gold": 0}, "reason": f"{position} 号地的{config.CROPS[plot.crop]['name']}已成熟", } ) - elif self._needs_water(plot) and state.ap >= config.AP_COST["water"]: + elif self._needs_water(plot): reason = ( f"{position} 号地缺水,作物已停止生长" if plot.moisture <= 0 @@ -356,7 +350,7 @@ class FarmEngine: { "action": "water", "params": {"plot": position}, - "cost": {"gold": 0, "ap": config.AP_COST["water"]}, + "cost": {"gold": 0}, "reason": reason, } ) @@ -371,19 +365,19 @@ class FarmEngine: { "action": "collect", "params": {"animal_id": animal.aid}, - "cost": {"gold": 0, "ap": config.AP_COST["collect"]}, + "cost": {"gold": 0}, "reason": f"{animal.name}有 {animal.pending} 份{animal.product}待收取", } ) elif not animal.fed: need = animal.info["feed_per_day"] cost = 0 if state.feed >= need else need * config.FEED_EMERGENCY_PRICE - if state.gold >= cost and state.ap >= config.AP_COST["feed"]: + if state.gold >= cost: actions.append( { "action": "feed", "params": {"animal_id": animal.aid}, - "cost": {"gold": cost, "ap": config.AP_COST["feed"]}, + "cost": {"gold": cost}, "reason": f"{animal.name}(#{animal.aid})今天还没喂食", } ) @@ -398,7 +392,7 @@ class FarmEngine: { "action": "buy_plot", "params": {}, - "cost": {"gold": price, "ap": config.AP_COST["buy_plot"]}, + "cost": {"gold": price}, "reason": f"可花 {price} 金币开垦第 {len(state.plots) + 1} 块地", } ) @@ -408,7 +402,7 @@ class FarmEngine: { "action": "buy_feed", "params": {"quantity": config.FEED_BULK}, - "cost": {"gold": bulk_cost, "ap": config.AP_COST["buy_feed"]}, + "cost": {"gold": bulk_cost}, "reason": "饲料库存偏低,建议补货", } ) @@ -421,7 +415,7 @@ class FarmEngine: { "action": "buy_animal", "params": {"kind": key}, - "cost": {"gold": info["cost"], "ap": config.AP_COST["buy_animal"]}, + "cost": {"gold": info["cost"]}, "reason": f"可购买{info['name']}({info['cost']} 金币)", } ) @@ -457,7 +451,7 @@ class FarmEngine: { "action": "sell", "params": {"key": key, "quantity": count}, - "cost": {"gold": 0, "ap": config.AP_COST["sell"]}, + "cost": {"gold": 0}, "reason": ";".join(reasons), } ) @@ -474,7 +468,7 @@ class FarmEngine: { "action": "build", "params": {"key": key}, - "cost": {"gold": entry["next_cost"], "ap": config.AP_COST["build"]}, + "cost": {"gold": entry["next_cost"]}, "reason": f"可造第 {entry['owned'] + 1} 座{entry['name']}:{entry['effect']}", } ) @@ -531,17 +525,6 @@ class FarmEngine: return animal raise GameError("no_such_animal", f"没有编号为 {aid} 的动物") - def _spend_ap(self, action: str) -> None: - cost = config.AP_COST.get(action, 0) - if cost and self.state.ap < cost: - raise GameError( - "no_ap", - f"行动点不足:{action} 需要 {cost} 点,当前只剩 {self.state.ap} 点。" - f"等一天的时间自然走完就会恢复。", - ) - if cost: - self.state.ap -= cost - def _spend_gold(self, amount: int, what: str) -> None: amount = int(amount) if amount < 0: @@ -639,7 +622,6 @@ class FarmEngine: ) self._spend_gold(info["seed_cost"], f"购买{info['name']}种子") - self._spend_ap("plant") plot.crop = crop plot.growth = 0.0 @@ -666,7 +648,6 @@ class FarmEngine: "already_watered", f"{plot.index + 1} 号地水分已满({plot.moisture}/{config.WATER_CAPACITY})", ) - self._spend_ap("water") plot.moisture = config.WATER_CAPACITY self.add_log(f"给 {plot.index + 1} 号地浇水(水分 {config.WATER_CAPACITY})", "action") self._notify() @@ -684,7 +665,6 @@ class FarmEngine: ) crop_key = plot.crop info = config.CROPS[crop_key] - self._spend_ap("harvest") quantity = self._roll_yield() # 先入库再清地:谷仓满了会在这里抛 granary_full,作物原地不动, @@ -729,7 +709,6 @@ class FarmEngine: raise GameError("pasture_full", f"牧场已满(上限 {config.MAX_ANIMALS} 只)") self._spend_gold(info["cost"], f"购买{info['name']}") - self._spend_ap("buy_animal") animal = Animal(kind) self.state.animals.append(animal) @@ -766,7 +745,6 @@ class FarmEngine: self.state.feed += shortfall self.add_log(f"饲料不足,临时高价补货 {shortfall} 份(-{cost} 金币)", "warn") - self._spend_ap("feed") self.state.feed -= quantity animal.fed = True animal.starve = 0 @@ -788,7 +766,6 @@ class FarmEngine: f"{animal.name} #{animal.aid} 暂时没有可收取的{animal.product}" f"(还需 {animal.days_to_product} 天)", ) - self._spend_ap("collect") quantity = animal.pending key = f"animal:{animal.kind}" # 与收获同理:先确认装得下,再清空待收,免得产出凭空消失 @@ -850,7 +827,6 @@ class FarmEngine: f"土地已达当前等级上限({self.state.max_plots} 块),升级后可继续开垦", ) self._spend_gold(price, "开垦新土地") - self._spend_ap("buy_plot") plot = Plot(len(self.state.plots)) self.state.plots.append(plot) self.state.plots_bought += 1 @@ -971,7 +947,6 @@ class FarmEngine: raise GameError("build_limit", f"{entry['name']}已建满({entry['max_level']} 座)") self._spend_gold(entry["next_cost"], f"建造{entry['name']}") - self._spend_ap("build") self.state.buildings[key] = entry["owned"] + 1 built = self._building_entry(self.state, key) self.add_log( @@ -1070,7 +1045,7 @@ class FarmEngine: 顺序是一条流水线:浇水(水井/喷灌)保住作物 → 收割机把熟了的收进 谷仓 → 磨坊把陈粮磨成饲料 → 喂食器喂牲口 → 售货机趁高价清库存。 - 共同的设计底线:**只替代玩家本来要花行动点做的事,绝不自动花钱** + 共同的设计底线:**只替代玩家本来要花工夫做的事,绝不自动花钱** ——喂食器缺饲料停摆、收割机遇满仓停摆,都宁可停也不硬来。 """ return ( @@ -1246,7 +1221,6 @@ class FarmEngine: state = self.state info = self._resolve_weather(state, weather) state.day += 1 - state.ap = state.max_ap events: list[str] = [] # ---- 建筑自动化。必须排在生长结算之前:水井救的正是"今天就断水"的 @@ -1303,7 +1277,7 @@ class FarmEngine: self.add_log(text, "warn") today = f"第 {state.day} 天开始({reason}),今日{info['name']}" if events: - self.add_log(f"{today},行动点已恢复", "info") + self.add_log(f"{today},新的一天开始了", "info") else: self.add_log(f"{today},一切安好", "info") diff --git a/farm/models.py b/farm/models.py index 8d554ff..7420832 100644 --- a/farm/models.py +++ b/farm/models.py @@ -199,7 +199,6 @@ class GameState: self.gold: int = config.START_GOLD self.level: int = config.START_LEVEL self.exp: int = 0 - self.ap: int = config.START_AP self.feed: int = config.START_FEED self.plots: list[Plot] = [Plot(i) for i in range(config.BASE_PLOTS)] self.animals: list[Animal] = [] @@ -231,10 +230,6 @@ class GameState: self.forecast: str = config.DEFAULT_WEATHER # ---------------------------------------------------------------- 派生属性 - @property - def max_ap(self) -> int: - return config.START_AP - @property def max_plots(self) -> int: """当前等级允许持有的土地上限。""" @@ -327,8 +322,6 @@ class GameState: "level": self.level, "exp": self.exp, "exp_needed": self.exp_needed, - "ap": self.ap, - "max_ap": self.max_ap, "feed": self.feed, "inventory": dict(sorted(self.inventory.items())), "storage_used": self.storage_used, @@ -362,7 +355,6 @@ class GameState: state.level = max(1, int(data.get("level", 1))) state.exp = max(0, int(data.get("exp", 0))) state.feed = max(0, int(data.get("feed", 0))) - state.ap = int(clamp(int(data.get("ap", state.max_ap)), 0, state.max_ap)) plots = [Plot.from_dict(p) for p in data.get("plots", []) if isinstance(p, dict)] # 保证至少有初始土地,且不超过硬上限 diff --git a/farm/server/app.py b/farm/server/app.py index dddf63f..663c227 100644 --- a/farm/server/app.py +++ b/farm/server/app.py @@ -115,8 +115,6 @@ BRIEF_KEYS = ( "level", "exp", "exp_needed", - "ap", - "max_ap", "feed", "plot_count", "max_plots", @@ -158,8 +156,8 @@ def rules_text() -> str: "==========================================\n" f"1. 时间:每个牧场每 {config.DEFAULT_TICK_SECONDS / 60:.0f} 分钟" f"({config.DEFAULT_TICK_SECONDS:.0f} 秒)自动推进一天,可配置。" - f"每天开始时行动点恢复为 {config.START_AP} 点,作物生长、牲畜产出、市场价格都在此刻结算。\n" - f"2. 行动点:播种/浇水/喂食各消耗 1 点,收获与收取产出免费。行动点用完只能等第二天——" + f"作物生长、牲畜产出、市场价格都在此刻结算。\n" + "2. 节奏:动作没有行动点/体力限制,想种就种——稀缺的只有时间本身。" "时间只能自然流动,没有任何「手动推进一天」的接口,别白费请求。\n" f"3. 天气与水分:每天随机一种天气(晴/多云/雨/干旱/暴风雨)。作物每天按天气蒸发" f" 0~2 点水分(上限 {config.WATER_CAPACITY}),水分为 0 时停止生长;" diff --git a/farm/server/webui.py b/farm/server/webui.py index 1123586..c0ea525 100644 --- a/farm/server/webui.py +++ b/farm/server/webui.py @@ -27,7 +27,8 @@ DASHBOARD_HTML = r""" * { box-sizing: border-box; } body { margin: 0; background: var(--bg); color: var(--ink); - font: 14px/1.6 "Segoe UI", "Microsoft YaHei", system-ui, sans-serif; + font: 15px/1.6 "Segoe UI Variable Text", "Segoe UI", "MiSans", + "HarmonyOS Sans SC", "Microsoft YaHei", "PingFang SC", system-ui, sans-serif; } header { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; @@ -53,7 +54,7 @@ DASHBOARD_HTML = r""" main { padding: 16px 20px 40px; max-width: 1180px; margin: 0 auto; } .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); gap: 10px; } .stat { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 10px 12px; } - .stat b { display: block; font-size: 18px; } + .stat b { display: block; font-size: 18px; font-variant-numeric: tabular-nums; } .stat small { color: var(--dim); } .cols { display: grid; grid-template-columns: 1.35fr 1fr; gap: 14px; margin-top: 14px; } @media (max-width: 900px) { .cols { grid-template-columns: 1fr; } } @@ -206,6 +207,7 @@ DASHBOARD_HTML = r"""