"""规则机器人玩家——"AI 也能玩"的最小可用示例。 它的意义不在于"内置了 AI",而在于证明一件事: **服务器把状态和合法动作都摊开了,外面是谁在决策根本不重要。** 这个机器人做决策的方式是"给候选动作打分",而一个 LLM 做决策的方式是 "读同一份 ``/api/actions``,用语言模型选一个"。两者的接口调用序列一模一样。 所以: * :meth:`RuleBot.choose` 里那段打分逻辑 = 可以整体替换成 LLM 的"策略函数"; * :meth:`RuleBot.step` 的执行路径 = LLM 将来要走的那条路(读动作 → 执行 → 看回执)。 它只依赖 :mod:`farm.client`,因此既能在本机跑,也能从别处连上服务器跑—— 换个 ``--url`` 就是一个独立的外挂玩家。 """ from __future__ import annotations import random import threading import time from typing import Any, Dict, List, Optional from .engine import GameError 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) ap = state.get("ap", 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 and ap >= 1: return 30.0 + (info.get("profit_per_day", 0) if info else 0) return 3.0 return 1.0 def choose(self) -> Optional[Dict[str, Any]]: """在服务器给出的合法动作里挑一个最优的。""" 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) -> Optional[str]: """执行一个动作,返回动作名;没得做或做不了返回 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 if exc.code == "cooldown": # 休息有冷却;这是正常节奏,不该记成失败 self.failures -= 1 self.say(f"冷却中… {exc.message}") time.sleep(1.0) return None 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 rest(self) -> bool: """行动点用完时推进一天。""" try: result = self.client.sleep() except GameError as exc: if exc.code == "cooldown": time.sleep(1.0) return True self.say(f"✗ sleep 失败:{exc.message}") return False self.say(f"— {result.get('message', '进入下一天')}") return True # ------------------------------------------------------------ 主循环 def run(self, rounds: int = 0, max_minutes: float = 0.0, idle_limit: int = 8) -> Dict[str, Any]: """跑机器人。 ``rounds`` 为 0 表示不限轮数;``max_minutes`` 为 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} 天," f"{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 state = self.client.snapshot() if state.get("ap", 0) <= 0: # 行动点用完:休息推进一天,顺便等冷却 self.rest() continue did = self.step() if did is None: idle += 1 if idle >= idle_limit: self.say("暂时没有可做的事,休息一天再看。") self.rest() 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() -> Optional[Dict[str, Any]]: 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