"""规则机器人玩家——"AI 也能玩"的最小可用示例。 它的意义不在于"内置了 AI",而在于证明一件事: **服务器把状态和合法动作都摊开了,外面是谁在决策根本不重要。** 这个机器人做决策的方式是"给候选动作打分",而一个 LLM 做决策的方式是 "读同一份 ``/api/actions``,用语言模型选一个"。两者的接口调用序列一模一样。 所以: * :meth:`RuleBot.choose` 里那段打分逻辑 = 可以整体替换成 LLM 的"策略函数"; * :meth:`RuleBot.step` 的执行路径 = LLM 将来要走的那条路(读动作 → 执行 → 看回执)。 它只依赖 :mod:`farm.client`,因此既能在本机跑,也能从别处连上服务器跑—— 换个 ``--url`` 就是一个独立的外挂玩家。 节奏完全由服务器时钟决定。服务器**没有**"手动推进一天"的接口,没事可做时 它只能等时间流动(或收工)::meth:`RuleBot.run` 的 ``wait_seconds`` 就是"最多等多久",默认 0——一天 40 分钟,干等不划算。 """ from __future__ import annotations import random import threading import time from typing import Any from .engine import GameError from .models import format_duration class RuleBot: """一个会种地、会养殖、会投资的规则机器人。""" def __init__( self, client, verbose: bool = True, pause: float = 0.25, name: str = "bot" ) -> None: self.client = client # 任何与 RemoteFarm 接口一致的对象 self.verbose = verbose self.pause = pause self.name = name self.done: list[str] = [] self.failures = 0 self.actions_executed = 0 self._stop = threading.Event() # ------------------------------------------------------------ 输出 def say(self, text: str) -> None: if self.verbose: print(f"[{time.strftime('%H:%M:%S')}] {text}", flush=True) def stop(self) -> None: self._stop.set() @property def stopped(self) -> bool: return self._stop.is_set() # ------------------------------------------------------------ 打分 def score( self, action: dict[str, Any], state: dict[str, Any], catalog: dict[str, Any] ) -> float: """给一个候选动作打分。分数越高越先执行。 这张表就是整个"策略":先做免费的,再喂饱牲畜,再补水,再扩张。 换成 LLM 时,把这里换成一次模型调用即可,其余代码不用动。 """ name = action.get("action") params = action.get("params") or {} gold = state.get("gold", 0) feed = state.get("feed", 0) if name == "harvest": return 100.0 if name == "collect": return 96.0 if name == "feed": return 92.0 if name == "water": return 84.0 if name == "plant": # 优先种"每天净收益"最高的作物 crop = params.get("crop") entry = next((c for c in catalog.get("crops", []) if c["key"] == crop), None) per_day = entry["profit_per_day"] if entry else 0.0 return 60.0 + max(0.0, per_day) if name == "buy_feed": # 饲料快见底才补,且别把钱花光 if feed <= 2 and gold >= 200: return 40.0 return 5.0 if name == "buy_plot": price = state.get("next_plot_price") or 0 # 地太贵就别买,留下周转资金 if price > 0 and gold >= price * 3: return 34.0 return 4.0 if name == "buy_animal": info = next( (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: return 30.0 + (info.get("profit_per_day", 0) if info else 0) return 3.0 if name == "sell": # 卖货是唯一的现金流入口(收获不再直接给钱),所以优先级跟着"缺钱" # 和"仓快满"走:仓满了不腾地方,下一茬根本收不进来。 used = state.get("storage_used", 0) or 0 capacity = state.get("granary_capacity", 1) or 1 if gold < 150: return 90.0 if used >= capacity * 0.8: return 88.0 return 30.0 if name == "build": key = params.get("key") used = state.get("storage_used", 0) or 0 capacity = state.get("granary_capacity", 1) or 1 if key == "granary" and used >= capacity * 0.7: return 46.0 if key in ("well", "feeder"): # 这两个买的是"以后不用天天手动做",放置玩法里稳赚 return 38.0 return 22.0 return 1.0 def choose(self) -> dict[str, Any] | None: """在服务器给出的合法动作里挑一个最优的。""" actions = self.client.available_actions() if not actions: return None state = self.client.snapshot() catalog = self.client.catalog() best = max(actions, key=lambda item: self.score(item, state, catalog)) if self.score(best, state, catalog) <= 1.0: return None return best # ------------------------------------------------------------ 单步 def step(self) -> str | None: """执行一个动作,返回动作名;没得做或做不了返回 None。""" action = self.choose() if action is None: return None name = action["action"] params = action.get("params") or {} try: result = self.client._action(name, params) # 走通用动作入口 except GameError as exc: self.failures += 1 self.say(f"✗ {name} 失败({exc.code}):{exc.message}") return None self.actions_executed += 1 self.done.append(name) self.say(f"✓ {name} {params} → {result.get('message', '')}") if self.pause: time.sleep(self.pause) return name def wait_for_next_day(self, timeout: float) -> bool: """没得可做时等时间自己流动到下一天。 返回 True 表示等到了新的一天;``timeout`` 为 0 表示不等待、直接收工 (机器人本来就该"把能做的做完就走",不像真人会挂机守着)。 """ state = self.client.snapshot() if state.get("paused"): self.say("— 时间被暂停了,再等也不会有下一天,先收工。") return False if timeout <= 0: left = format_duration(state.get("seconds_to_tick")) self.say(f"— 暂时没事可做,距离下一天还有 {left},先收工。") return False start_day = state.get("day") self.say(f"— 等时间流动(最多 {timeout:.0f} 秒)…") deadline = time.time() + timeout while not self._stop.is_set() and time.time() < deadline: time.sleep(0.25) day = self.client.snapshot().get("day") if day != start_day: self.say(f"— 天亮了:第 {day} 天") return True self.say("— 没等到下一天,先收工。") return False # ------------------------------------------------------------ 主循环 def run( self, rounds: int = 0, max_minutes: float = 0.0, idle_limit: int = 8, wait_seconds: float = 0.0, ) -> dict[str, Any]: """跑机器人。 ``rounds`` 为 0 表示不限轮数;``max_minutes`` 为 0 表示不限时间; ``wait_seconds`` 是"没事可做时最多等多久"(等下一天),0 表示直接收工。 """ deadline = (time.time() + max_minutes * 60) if max_minutes > 0 else None start_day = self.client.snapshot().get("day", 1) idle = 0 round_no = 0 self.say(f"机器人 {self.name} 上线(第 {start_day} 天,{self.client.transport_label()})") while not self._stop.is_set(): round_no += 1 if rounds and round_no > rounds: break if deadline and time.time() > deadline: self.say("到时间了,收工。") break did = self.step() if did is None: idle += 1 if idle >= idle_limit: if not self.wait_for_next_day(wait_seconds): break idle = 0 else: time.sleep(0.4) else: idle = 0 end_day = self.client.snapshot().get("day", start_day) summary = { "name": self.name, "days": end_day - start_day, "actions": self.actions_executed, "failures": self.failures, "net_worth": self.client.snapshot().get("net_worth"), "gold": self.client.snapshot().get("gold"), } self.say( f"机器人 {self.name} 下线:跑了 {summary['days']} 天、" f"执行 {summary['actions']} 个动作、净资产 {summary['net_worth']}" ) return summary def random_bot(client, **kwargs) -> RuleBot: """随机策略机器人:用来做对照实验(证明"有策略"确实比乱按强)。""" bot = RuleBot(client, **kwargs) original = bot.choose def choose() -> dict[str, Any] | None: actions = client.available_actions() if not actions: return None return random.choice(actions) bot.choose = choose # type: ignore[assignment] bot.name = kwargs.get("name", "random") del original return bot