=== ruff 版本 === ruff 0.16.8 === ruff check --statistics === 225 UP006 [*] non-pep585-annotation 54 UP045 [*] non-pep604-annotation-optional 32 UP035 [-] deprecated-import 23 E501 [ ] line-too-long 18 PTH120 [ ] os-path-dirname 15 PTH118 [ ] os-path-join 12 B904 [ ] raise-without-from-inside-except 12 SIM105 [ ] suppressible-exception 11 PTH123 [ ] builtin-open 8 PTH100 [ ] os-path-abspath 7 PTH103 [ ] os-makedirs 6 RUF100 [*] unused-noqa 5 PTH110 [ ] os-path-exists 5 UP015 [*] redundant-open-modes 5 PERF203 [ ] try-except-in-loop 5 RUF023 [*] unsorted-dunder-slots 3 PTH107 [ ] os-remove 3 UP037 [*] quoted-annotation 3 RUF046 [ ] unnecessary-cast-to-int 3 I001 [*] unsorted-imports 2 F541 [*] f-string-missing-placeholders 2 PERF401 [ ] manual-list-comprehension 2 RUF012 [ ] mutable-class-default 2 UP007 [*] non-pep604-annotation-union 2 F401 [*] unused-import 1 E741 [ ] ambiguous-variable-name 1 RUF005 [ ] collection-literal-concatenation 1 SIM115 [ ] open-file-with-context-handler 1 PTH105 [ ] os-replace 1 UP031 [ ] printf-string-formatting 1 RET504 [ ] unnecessary-assign 1 RUF022 [*] unsorted-dunder-all 1 B007 [ ] unused-loop-control-variable 1 F841 [ ] unused-variable Found 474 errors. [*] 312 fixable with the `--fix` option (17 hidden fixes can be enabled with the `--unsafe-fixes` option). === ruff format --check(哪些文件格式不合规) === unformatted: File would be reformatted --> README.md:260:1 | 259 | 260 + 261 | async def main(): -------------------------------------------------------------------------------- 272 | 273 + 274 | asyncio.run(main()) | unformatted: File would be reformatted --> ai_player.py:53:29 | 52 | try: - client = RemoteFarm(endpoint=args.url, farm=args.farm, token=args.token, - name=f"bot-{args.farm}", realtime=False) 53 + client = RemoteFarm( 54 + endpoint=args.url, 55 + farm=args.farm, 56 + token=args.token, 57 + name=f"bot-{args.farm}", 58 + realtime=False, 59 + ) 60 | except GameError as exc: -------------------------------------------------------------------------------- 65 | snapshot = client.snapshot() - print(f"已连接 {args.url} · 牧场「{args.farm}」· 第 {snapshot.get('day')} 天 · " - f"金币 {snapshot.get('gold')}") 66 + print( 67 + f"已连接 {args.url} · 牧场「{args.farm}」· 第 {snapshot.get('day')} 天 · " 68 + f"金币 {snapshot.get('gold')}" 69 + ) 70 | -------------------------------------------------------------------------------- 76 | bot.stop() - summary = {"name": args.farm, "actions": bot.actions_executed, - "failures": bot.failures, "interrupted": True} 77 + summary = { 78 + "name": args.farm, 79 + "actions": bot.actions_executed, 80 + "failures": bot.failures, 81 + "interrupted": True, 82 + } 83 | | unformatted: File would be reformatted --> farm\__init__.py:18:14 | 17 | __all__ = [ - "config", "models", "engine", "storage", "keys", "tui", - "client", "bot", "server", 18 + "config", 19 + "models", 20 + "engine", 21 + "storage", 22 + "keys", 23 + "tui", 24 + "client", 25 + "bot", 26 + "server", 27 | ] | unformatted: File would be reformatted --> farm\bot.py:30:18 | 29 | - def __init__(self, client, verbose: bool = True, pause: float = 0.25, - name: str = "bot") -> None: - self.client = client # 任何与 RemoteFarm 接口一致的对象 30 + def __init__( 31 + self, client, verbose: bool = True, pause: float = 0.25, name: str = "bot" 32 + ) -> None: 33 + self.client = client # 任何与 RemoteFarm 接口一致的对象 34 | self.verbose = verbose -------------------------------------------------------------------------------- 54 | # ------------------------------------------------------------ 打分 - def score(self, action: Dict[str, Any], state: Dict[str, Any], - catalog: Dict[str, Any]) -> float: 55 + def score( 56 + self, action: Dict[str, Any], state: Dict[str, Any], catalog: Dict[str, Any] 57 + ) -> float: 58 | """给一个候选动作打分。分数越高越先执行。 -------------------------------------------------------------------------------- 94 | if name == "buy_animal": - info = next((a for a in catalog.get("animals", []) - if a["key"] == params.get("kind")), None) 95 + info = next( 96 + (a for a in catalog.get("animals", []) if a["key"] == params.get("kind")), None 97 + ) 98 | cost = info["cost"] if info else 0 -------------------------------------------------------------------------------- 124 | try: - result = self.client._action(name, params) # 走通用动作入口 125 + result = self.client._action(name, params) # 走通用动作入口 126 | except GameError as exc: -------------------------------------------------------------------------------- 156 | # ------------------------------------------------------------ 主循环 - def run(self, rounds: int = 0, max_minutes: float = 0.0, - idle_limit: int = 8) -> Dict[str, Any]: 157 + def run(self, rounds: int = 0, max_minutes: float = 0.0, idle_limit: int = 8) -> Dict[str, Any]: 158 | """跑机器人。 -------------------------------------------------------------------------------- 165 | round_no = 0 - self.say(f"机器人 {self.name} 上线(第 {start_day} 天," - f"{self.client.transport_label()})") 166 + self.say(f"机器人 {self.name} 上线(第 {start_day} 天,{self.client.transport_label()})") 167 | -------------------------------------------------------------------------------- 202 | } - self.say(f"机器人 {self.name} 下线:跑了 {summary['days']} 天、" - f"执行 {summary['actions']} 个动作、净资产 {summary['net_worth']}") 203 + self.say( 204 + f"机器人 {self.name} 下线:跑了 {summary['days']} 天、" 205 + f"执行 {summary['actions']} 个动作、净资产 {summary['net_worth']}" 206 + ) 207 | return summary -------------------------------------------------------------------------------- 220 | - bot.choose = choose # type: ignore[assignment] 221 + bot.choose = choose # type: ignore[assignment] 222 | bot.name = kwargs.get("name", "random") | unformatted: File would be reformatted --> farm\client.py:234:18 | 233 | - def __init__(self, endpoint: str = DEFAULT_ENDPOINT, farm: str = "default", - token: Optional[str] = None, name: str = "", timeout: float = 6.0, - realtime: bool = True, autostart: bool = True) -> None: 234 + def __init__( 235 + self, 236 + endpoint: str = DEFAULT_ENDPOINT, 237 + farm: str = "default", 238 + token: Optional[str] = None, 239 + name: str = "", 240 + timeout: float = 6.0, 241 + realtime: bool = True, 242 + autostart: bool = True, 243 + ) -> None: 244 | self.endpoint = self._normalize(endpoint) -------------------------------------------------------------------------------- 252 | self.connected = False - self.mode = "http" # http / ws / poll 253 + self.mode = "http" # http / ws / poll 254 | self.last_error = "" -------------------------------------------------------------------------------- 265 | self._ws_loop: Any = None - self._catalog: Dict[str, Any] = {"crops": [], "animals": [], - "feed_price": config.FEED_UNIT_PRICE, - "water_capacity": config.WATER_CAPACITY, - "ap_cost": config.AP_COST} 266 + self._catalog: Dict[str, Any] = { 267 + "crops": [], 268 + "animals": [], 269 + "feed_price": config.FEED_UNIT_PRICE, 270 + "water_capacity": config.WATER_CAPACITY, 271 + "ap_cost": config.AP_COST, 272 + } 273 | self._log: Deque[Dict[str, Any]] = deque(maxlen=LOG_KEEP) -------------------------------------------------------------------------------- 286 | if endpoint.startswith("tcp://"): - endpoint = "http://" + endpoint[len("tcp://"):] 287 + endpoint = "http://" + endpoint[len("tcp://") :] 288 | if not endpoint.startswith(("http://", "https://")): -------------------------------------------------------------------------------- 302 | - def _http(self, method: str, path: str, payload: Optional[Dict[str, Any]] = None, - **params: Any) -> Dict[str, Any]: 303 + def _http( 304 + self, method: str, path: str, payload: Optional[Dict[str, Any]] = None, **params: Any 305 + ) -> Dict[str, Any]: 306 | url = self.url(path, **params) -------------------------------------------------------------------------------- 326 | "network", - f"连不上服务器 {self.endpoint}({exc})。" - "请先启动:python main.py serve --daemon", 327 + f"连不上服务器 {self.endpoint}({exc})。请先启动:python main.py serve --daemon", 328 | status=503, -------------------------------------------------------------------------------- 415 | self._log_seq = max(self._log_seq, seq) - self._log = deque(sorted(self._log, key=lambda e: int(e.get("seq", 0))), - maxlen=LOG_KEEP) 416 + self._log = deque( 417 + sorted(self._log, key=lambda e: int(e.get("seq", 0))), maxlen=LOG_KEEP 418 + ) 419 | 420 | # ------------------------------------------------------------ 写接口 - def _action(self, action: str, params: Optional[Dict[str, Any]] = None, - transport: str = "auto") -> Dict[str, Any]: 421 + def _action( 422 + self, action: str, params: Optional[Dict[str, Any]] = None, transport: str = "auto" 423 + ) -> Dict[str, Any]: 424 | """执行一个动作。 -------------------------------------------------------------------------------- 430 | params = params or {} - use_ws = (transport == "ws") or (transport == "auto" and self.mode == "ws" - and self.connected) 431 + use_ws = (transport == "ws") or ( 432 + transport == "auto" and self.mode == "ws" and self.connected 433 + ) 434 | if use_ws: -------------------------------------------------------------------------------- 451 | # full=1:让服务器顺手把完整快照带回来,省掉"操作后再查一次" - data = self._http("POST", "/api/actions", {"action": action, "params": params}, - full=1) 452 + data = self._http("POST", "/api/actions", {"action": action, "params": params}, full=1) 453 | self._apply(data.get("snapshot") or data.get("state") or {}) -------------------------------------------------------------------------------- 528 | self._stop.clear() - self._thread = threading.Thread(target=self._realtime_loop, name="farm-ws", - daemon=True) 529 + self._thread = threading.Thread(target=self._realtime_loop, name="farm-ws", daemon=True) 530 | self._thread.start() -------------------------------------------------------------------------------- 599 | async def _ws_main(self, websockets) -> None: - async with websockets.connect(self.ws_url, open_timeout=self.timeout, - ping_interval=20, ping_timeout=20, - max_size=4 * 1024 * 1024) as socket: 600 + async with websockets.connect( 601 + self.ws_url, 602 + open_timeout=self.timeout, 603 + ping_interval=20, 604 + ping_timeout=20, 605 + max_size=4 * 1024 * 1024, 606 + ) as socket: 607 | self.mode = "ws" -------------------------------------------------------------------------------- 667 | self._merge_log(message.get("log") or []) - future.set_result({ - "ok": True, - "message": message.get("message", "完成"), - **(message.get("result") or {}), - "state": message.get("state"), - "snapshot": message.get("snapshot"), - "log": message.get("log"), - }) 668 + future.set_result( 669 + { 670 + "ok": True, 671 + "message": message.get("message", "完成"), 672 + **(message.get("result") or {}), 673 + "state": message.get("state"), 674 + "snapshot": message.get("snapshot"), 675 + "log": message.get("log"), 676 + } 677 + ) 678 | else: - future.set_exception(GameError(message.get("error", "error"), - message.get("message", "操作失败"))) 679 + future.set_exception( 680 + GameError(message.get("error", "error"), message.get("message", "操作失败")) 681 + ) 682 | return 683 | 684 | if kind == "day": - self._merge_log([{ - "seq": self._log_seq + 1, - "day": message.get("day", 0), - "kind": "info", - "text": f"第 {message.get('day')} 天开始", - }] + [{ - "seq": self._log_seq + 2 + i, - "day": message.get("day", 0), - "kind": "warn", - "text": text, - } for i, text in enumerate(message.get("events") or [])]) 685 + self._merge_log( 686 + [ 687 + { 688 + "seq": self._log_seq + 1, 689 + "day": message.get("day", 0), 690 + "kind": "info", 691 + "text": f"第 {message.get('day')} 天开始", 692 + } 693 + ] 694 + + [ 695 + { 696 + "seq": self._log_seq + 2 + i, 697 + "day": message.get("day", 0), 698 + "kind": "warn", 699 + "text": text, 700 + } 701 + for i, text in enumerate(message.get("events") or []) 702 + ] 703 + ) 704 | return -------------------------------------------------------------------------------- 716 | - def _ws_action(self, action: str, params: Dict[str, Any], - timeout: Optional[float] = None) -> Dict[str, Any]: 717 + def _ws_action( 718 + self, action: str, params: Dict[str, Any], timeout: Optional[float] = None 719 + ) -> Dict[str, Any]: 720 | with self._lock: | unformatted: File would be reformatted --> farm\config.py:15:19 | 14 | - START_GOLD = 120 # 初始金币 - START_LEVEL = 1 # 初始等级 - START_AP = 10 # 每天行动点上限 - START_FEED = 5 # 初始饲料库存 15 + START_GOLD = 120 # 初始金币 16 + START_LEVEL = 1 # 初始等级 17 + START_AP = 10 # 每天行动点上限 18 + START_FEED = 5 # 初始饲料库存 19 | - BASE_PLOTS = 6 # 初始土地数量 - MAX_PLOTS = 16 # 土地数量硬上限 - PLOT_BASE_PRICE = 200 # 第一块新地的价格 20 + BASE_PLOTS = 6 # 初始土地数量 21 + MAX_PLOTS = 16 # 土地数量硬上限 22 + PLOT_BASE_PRICE = 200 # 第一块新地的价格 23 | PLOT_PRICE_GROWTH = 1.75 # 每多买一块,价格乘以该系数 24 | - FEED_UNIT_PRICE = 6 # 主动购买饲料的单价 25 + FEED_UNIT_PRICE = 6 # 主动购买饲料的单价 26 | FEED_EMERGENCY_PRICE = 9 # 自动补货(缺饲料时喂食)的惩罚单价 - FEED_BULK = 10 # 一次购买饲料的默认批量 27 + FEED_BULK = 10 # 一次购买饲料的默认批量 28 | - WATER_CAPACITY = 3 # 一次浇水可支撑的天数 29 + WATER_CAPACITY = 3 # 一次浇水可支撑的天数 30 | PLANT_INITIAL_MOISTURE = 2 # 播种后地块自带的水分 - PRICE_VOLATILITY = 0.18 # 每天价格波动幅度(±18%) - LUCKY_CHANCE = 0.12 # 丰收暴击概率(产出翻倍) - MAX_LOG_ENTRIES = 300 # 内存中保留的事件条数上限 - MAX_FEED_PURCHASE = 200 # 单次购买饲料的数量上限 31 + PRICE_VOLATILITY = 0.18 # 每天价格波动幅度(±18%) 32 + LUCKY_CHANCE = 0.12 # 丰收暴击概率(产出翻倍) 33 + MAX_LOG_ENTRIES = 300 # 内存中保留的事件条数上限 34 + MAX_FEED_PURCHASE = 200 # 单次购买饲料的数量上限 35 | -------------------------------------------------------------------------------- 50 | 51 + 52 | # 经验曲线:升到 level+1 所需经验 -------------------------------------------------------------------------------- 56 | 57 + 58 | # 升级奖励金币 -------------------------------------------------------------------------------- 76 | "wheat": { - "name": "小麦", "seed_cost": 5, "grow_days": 2, - "base_price": 14, "exp": 3, "level_req": 1, 77 + "name": "小麦", 78 + "seed_cost": 5, 79 + "grow_days": 2, 80 + "base_price": 14, 81 + "exp": 3, 82 + "level_req": 1, 83 | }, 84 | "carrot": { - "name": "胡萝卜", "seed_cost": 8, "grow_days": 3, - "base_price": 26, "exp": 5, "level_req": 1, 85 + "name": "胡萝卜", 86 + "seed_cost": 8, 87 + "grow_days": 3, 88 + "base_price": 26, 89 + "exp": 5, 90 + "level_req": 1, 91 | }, 92 | "tomato": { - "name": "番茄", "seed_cost": 16, "grow_days": 4, - "base_price": 52, "exp": 9, "level_req": 2, 93 + "name": "番茄", 94 + "seed_cost": 16, 95 + "grow_days": 4, 96 + "base_price": 52, 97 + "exp": 9, 98 + "level_req": 2, 99 | }, 100 | "corn": { - "name": "玉米", "seed_cost": 28, "grow_days": 6, - "base_price": 95, "exp": 14, "level_req": 3, 101 + "name": "玉米", 102 + "seed_cost": 28, 103 + "grow_days": 6, 104 + "base_price": 95, 105 + "exp": 14, 106 + "level_req": 3, 107 | }, 108 | "pumpkin": { - "name": "南瓜", "seed_cost": 45, "grow_days": 8, - "base_price": 180, "exp": 22, "level_req": 4, 109 + "name": "南瓜", 110 + "seed_cost": 45, 111 + "grow_days": 8, 112 + "base_price": 180, 113 + "exp": 22, 114 + "level_req": 4, 115 | }, 116 | "strawberry": { - "name": "草莓", "seed_cost": 70, "grow_days": 10, - "base_price": 320, "exp": 35, "level_req": 5, 117 + "name": "草莓", 118 + "seed_cost": 70, 119 + "grow_days": 10, 120 + "base_price": 320, 121 + "exp": 35, 122 + "level_req": 5, 123 | }, -------------------------------------------------------------------------------- 149 | "chicken": { - "name": "鸡", "cost": 90, "product": "鸡蛋", - "base_price": 15, "cycle": 1, "feed_per_day": 1, - "exp": 4, "level_req": 1, 150 + "name": "鸡", 151 + "cost": 90, 152 + "product": "鸡蛋", 153 + "base_price": 15, 154 + "cycle": 1, 155 + "feed_per_day": 1, 156 + "exp": 4, 157 + "level_req": 1, 158 | }, 159 | "sheep": { - "name": "羊", "cost": 220, "product": "羊毛", - "base_price": 55, "cycle": 2, "feed_per_day": 1, - "exp": 9, "level_req": 2, 160 + "name": "羊", 161 + "cost": 220, 162 + "product": "羊毛", 163 + "base_price": 55, 164 + "cycle": 2, 165 + "feed_per_day": 1, 166 + "exp": 9, 167 + "level_req": 2, 168 | }, 169 | "cow": { - "name": "牛", "cost": 480, "product": "牛奶", - "base_price": 72, "cycle": 1, "feed_per_day": 2, - "exp": 15, "level_req": 3, 170 + "name": "牛", 171 + "cost": 480, 172 + "product": "牛奶", 173 + "base_price": 72, 174 + "cycle": 1, 175 + "feed_per_day": 2, 176 + "exp": 15, 177 + "level_req": 3, 178 | }, -------------------------------------------------------------------------------- 190 | MAX_TICK_SECONDS = 600.0 - SLEEP_COOLDOWN = 2.0 # 手动"休息"推进一天的冷却(秒) 191 + SLEEP_COOLDOWN = 2.0 # 手动"休息"推进一天的冷却(秒) 192 | | unformatted: File would be reformatted --> farm\engine.py:80:36 | 79 | "day": self.state.day, - "kind": kind, # info / good / bad / warn / action 80 + "kind": kind, # info / good / bad / warn / action 81 | "text": text, -------------------------------------------------------------------------------- 150 | "locked": self.state.level < info["level_req"], - "profit_per_day": round( - (price - feed_cost) / max(1, info["cycle"]), 2 - ), 151 + "profit_per_day": round((price - feed_cost) / max(1, info["cycle"]), 2), 152 | } -------------------------------------------------------------------------------- 368 | if plot.crop is not None: - raise GameError("plot_busy", f"{plot.index + 1} 号地已经种了{config.CROPS[plot.crop]['name']}") 369 + raise GameError( 370 + "plot_busy", f"{plot.index + 1} 号地已经种了{config.CROPS[plot.crop]['name']}" 371 + ) 372 | -------------------------------------------------------------------------------- 428 | lucky = "(丰收!)" if quantity > 1 else "" - self.add_log( - f"收获 {quantity} 份{info['name']}{lucky},+{earned} 金币", "good" - ) 429 + self.add_log(f"收获 {quantity} 份{info['name']}{lucky},+{earned} 金币", "good") 430 | levelups = self._gain_exp(info["exp"]) -------------------------------------------------------------------------------- 643 | if plot.is_ripe: - events.append(f"{plot.index + 1} 号地的{config.CROPS[plot.crop]['name']}已成熟") 644 + events.append( 645 + f"{plot.index + 1} 号地的{config.CROPS[plot.crop]['name']}已成熟" 646 + ) 647 | else: | unformatted: File would be reformatted --> farm\models.py:26:29 | 25 | def __init__(self, index: int) -> None: - self.index = index # 0-based 下标,对外展示用 index+1 26 + self.index = index # 0-based 下标,对外展示用 index+1 27 | self.crop: Optional[str] = None - self.growth: float = 0.0 # 已生长天数 - self.moisture: int = 0 # 剩余可支撑天数 28 + self.growth: float = 0.0 # 已生长天数 29 + self.moisture: int = 0 # 剩余可支撑天数 30 | self.planted_day: int = 0 -------------------------------------------------------------------------------- 104 | self.kind = kind - self.fed = False # 今天是否已喂食 - self.progress = 0 # 距下次产出的累计天数 - self.starve = 0 # 连续未进食天数 - self.pending = 0 # 待收集的产出数量 105 + self.fed = False # 今天是否已喂食 106 + self.progress = 0 # 距下次产出的累计天数 107 + self.starve = 0 # 连续未进食天数 108 + self.pending = 0 # 待收集的产出数量 109 | -------------------------------------------------------------------------------- 210 | return -1 - return int(config.PLOT_BASE_PRICE * (config.PLOT_PRICE_GROWTH ** self.plots_bought)) 211 + return int(config.PLOT_BASE_PRICE * (config.PLOT_PRICE_GROWTH**self.plots_bought)) 212 | -------------------------------------------------------------------------------- 231 | land = sum( - config.PLOT_BASE_PRICE * (config.PLOT_PRICE_GROWTH ** i) - for i in range(self.plots_bought) 232 + config.PLOT_BASE_PRICE * (config.PLOT_PRICE_GROWTH**i) for i in range(self.plots_bought) 233 | ) -------------------------------------------------------------------------------- 296 | plots.extend(Plot(i) for i in range(len(plots), count)) - state.plots = plots[:config.MAX_PLOTS] 297 + state.plots = plots[: config.MAX_PLOTS] 298 | for i, plot in enumerate(state.plots): | unformatted: File would be reformatted --> farm\net.py:46:14 | 45 | - def open_url(target: Union[str, urllib.request.Request], timeout: float = 6.0, - host: Optional[str] = None) -> Any: 46 + def open_url( 47 + target: Union[str, urllib.request.Request], timeout: float = 6.0, host: Optional[str] = None 48 + ) -> Any: 49 | """打开一个 URL 或 Request,回环地址自动绕过代理。 | unformatted: File would be reformatted --> farm\server\__init__.py:25:19 | 24 | _LAZY = { - "API_VERSION", "SERVER_NAME", "ServerConfig", "ServerHandle", - "build_app", "create_app", "rules_text", "brief", - "Action", "Param", "ACTIONS", "run_action", "describe_actions", "action_names", - "FarmSession", "SessionManager", "FarmClock", "validate_farm_id", "DEFAULT_FARM", - "Client", "ConnectionHub", "Publisher", - "DaemonManager", "pid_alive", - "DASHBOARD_HTML", "dashboard_text", 25 + "API_VERSION", 26 + "SERVER_NAME", 27 + "ServerConfig", 28 + "ServerHandle", 29 + "build_app", 30 + "create_app", 31 + "rules_text", 32 + "brief", 33 + "Action", 34 + "Param", 35 + "ACTIONS", 36 + "run_action", 37 + "describe_actions", 38 + "action_names", 39 + "FarmSession", 40 + "SessionManager", 41 + "FarmClock", 42 + "validate_farm_id", 43 + "DEFAULT_FARM", 44 + "Client", 45 + "ConnectionHub", 46 + "Publisher", 47 + "DaemonManager", 48 + "pid_alive", 49 + "DASHBOARD_HTML", 50 + "dashboard_text", 51 | } -------------------------------------------------------------------------------- 55 | - def __getattr__(name: str) -> Any: # PEP 562:模块级惰性属性 56 + def __getattr__(name: str) -> Any: # PEP 562:模块级惰性属性 57 | if name not in _LAZY: -------------------------------------------------------------------------------- 64 | module = import_module("farm.server.webui") - elif name in ("Action", "Param", "ACTIONS", "run_action", "describe_actions", - "action_names"): 65 + elif name in ("Action", "Param", "ACTIONS", "run_action", "describe_actions", "action_names"): 66 | module = import_module("farm.server.actions") - elif name in ("FarmSession", "SessionManager", "FarmClock", "validate_farm_id", - "DEFAULT_FARM"): 67 + elif name in ("FarmSession", "SessionManager", "FarmClock", "validate_farm_id", "DEFAULT_FARM"): 68 | module = import_module("farm.server.session") | unformatted: File would be reformatted --> farm\server\__main__.py:23:25 | 22 | ) - parser.add_argument("--host", default="127.0.0.1", - help="监听地址,默认 127.0.0.1;局域网联机用 0.0.0.0") 23 + parser.add_argument( 24 + "--host", default="127.0.0.1", help="监听地址,默认 127.0.0.1;局域网联机用 0.0.0.0" 25 + ) 26 | parser.add_argument("--port", type=int, default=8765, help="监听端口,默认 8765") 27 | parser.add_argument("--token", default=None, help="访问令牌,设置后所有接口都要带") - parser.add_argument("--tick", type=float, default=config.DEFAULT_TICK_SECONDS, - help=f"每多少秒推进一天,默认 {config.DEFAULT_TICK_SECONDS:.0f} 秒") 28 + parser.add_argument( 29 + "--tick", 30 + type=float, 31 + default=config.DEFAULT_TICK_SECONDS, 32 + help=f"每多少秒推进一天,默认 {config.DEFAULT_TICK_SECONDS:.0f} 秒", 33 + ) 34 | parser.add_argument("--save-dir", default=None, help="存档目录,默认 <项目根>/saves") - parser.add_argument("--save", dest="save_file", default=None, - help="兼容旧参数:指定 default 牧场的存档文件") 35 + parser.add_argument( 36 + "--save", dest="save_file", default=None, help="兼容旧参数:指定 default 牧场的存档文件" 37 + ) 38 | parser.add_argument("--max-sessions", type=int, default=64, help="同时在线的牧场上限") - parser.add_argument("--idle-timeout", type=float, default=1800.0, - help="无人访问多少秒后把牧场移出内存(存档保留),默认 1800") 39 + parser.add_argument( 40 + "--idle-timeout", 41 + type=float, 42 + default=1800.0, 43 + help="无人访问多少秒后把牧场移出内存(存档保留),默认 1800", 44 + ) 45 | parser.add_argument("--world", default="SimpleFarm 公共农场", help="世界名称") - parser.add_argument("--log-level", default="info", - choices=["critical", "error", "warning", "info", "debug"]) 46 + parser.add_argument( 47 + "--log-level", default="info", choices=["critical", "error", "warning", "info", "debug"] 48 + ) 49 | parser.add_argument("--access-log", action="store_true", help="打印每个 HTTP 请求") - parser.add_argument("--no-remote-control", action="store_true", - help="禁止通过接口停机") - parser.add_argument("--daemon", action="store_true", - help="转到后台运行(等价于 python main.py serve --daemon)") - parser.add_argument("--pid-file", default=None, - help="把本进程 pid 写到该文件(守护进程模式会自动指定)") 50 + parser.add_argument("--no-remote-control", action="store_true", help="禁止通过接口停机") 51 + parser.add_argument( 52 + "--daemon", action="store_true", help="转到后台运行(等价于 python main.py serve --daemon)" 53 + ) 54 + parser.add_argument( 55 + "--pid-file", default=None, help="把本进程 pid 写到该文件(守护进程模式会自动指定)" 56 + ) 57 | parser.add_argument("--log-file", default=DEFAULT_LOG_FILE, help=argparse.SUPPRESS) -------------------------------------------------------------------------------- 82 | manager = DaemonManager( - host=args.host, port=args.port, token=args.token, - tick=clamp_tick(args.tick), save_dir=save_dir, 83 + host=args.host, 84 + port=args.port, 85 + token=args.token, 86 + tick=clamp_tick(args.tick), 87 + save_dir=save_dir, 88 | pid_file=args.pid_file or DEFAULT_PID_FILE, -------------------------------------------------------------------------------- 101 | import websockets # noqa: F401 只是探测,uvicorn 需要它来做 WS 协议升级 102 + 103 | ws_ok = True -------------------------------------------------------------------------------- 153 | if not ws_ok: - print(" ⚠ 未检测到 websockets 库,/ws 实时通道可能不可用;" - "网页端会自动降级为轮询。安装:pip install 'uvicorn[standard]'") 154 + print( 155 + " ⚠ 未检测到 websockets 库,/ws 实时通道可能不可用;" 156 + "网页端会自动降级为轮询。安装:pip install 'uvicorn[standard]'" 157 + ) 158 | if cfg.token: -------------------------------------------------------------------------------- 173 | if not getattr(server, "started", False): - print(f"启动失败:端口 {cfg.port} 可能已被占用。" - f"换一个端口试试:--port {cfg.port + 1}", file=sys.stderr) 174 + print( 175 + f"启动失败:端口 {cfg.port} 可能已被占用。换一个端口试试:--port {cfg.port + 1}", 176 + file=sys.stderr, 177 + ) 178 | return 3 | unformatted: File would be reformatted --> farm\server\actions.py:27:18 | 26 | - def __init__(self, name: str, kind: str, required: bool = True, - default: Any = None, desc: str = "") -> None: 27 + def __init__( 28 + self, name: str, kind: str, required: bool = True, default: Any = None, desc: str = "" 29 + ) -> None: 30 | self.name = name - self.kind = kind # "int" / "str" / "bool" 31 + self.kind = kind # "int" / "str" / "bool" 32 | self.required = required -------------------------------------------------------------------------------- 76 | - def __init__(self, name: str, summary: str, params: List[Param], - handler: Callable[[FarmEngine, Dict[str, Any]], Dict[str, Any]], - realtime: bool = True) -> None: 77 + def __init__( 78 + self, 79 + name: str, 80 + summary: str, 81 + params: List[Param], 82 + handler: Callable[[FarmEngine, Dict[str, Any]], Dict[str, Any]], 83 + realtime: bool = True, 84 + ) -> None: 85 | self.name = name -------------------------------------------------------------------------------- 150 | - _register(Action("plant", "在指定地块播种", [ - Param("plot", "int", True, None, "地块编号,从 1 开始"), - Param("crop", "str", True, None, "作物标识,如 wheat"), - ], lambda e, p: e.plant(p["plot"], p["crop"]))) 151 + _register( 152 + Action( 153 + "plant", 154 + "在指定地块播种", 155 + [ 156 + Param("plot", "int", True, None, "地块编号,从 1 开始"), 157 + Param("crop", "str", True, None, "作物标识,如 wheat"), 158 + ], 159 + lambda e, p: e.plant(p["plot"], p["crop"]), 160 + ) 161 + ) 162 | - _register(Action("water", "给指定地块浇水(补满水分)", [ - Param("plot", "int", True, None, "地块编号,从 1 开始"), - ], lambda e, p: e.water(p["plot"]))) 163 + _register( 164 + Action( 165 + "water", 166 + "给指定地块浇水(补满水分)", 167 + [ 168 + Param("plot", "int", True, None, "地块编号,从 1 开始"), 169 + ], 170 + lambda e, p: e.water(p["plot"]), 171 + ) 172 + ) 173 | - _register(Action("harvest", "收获指定地块的成熟作物", [ - Param("plot", "int", True, None, "地块编号,从 1 开始"), - ], lambda e, p: e.harvest(p["plot"]))) 174 + _register( 175 + Action( 176 + "harvest", 177 + "收获指定地块的成熟作物", 178 + [ 179 + Param("plot", "int", True, None, "地块编号,从 1 开始"), 180 + ], 181 + lambda e, p: e.harvest(p["plot"]), 182 + ) 183 + ) 184 | 185 | _register(Action("harvest_all", "一键收获所有成熟作物", [], _harvest_all)) 186 | - _register(Action("buy_animal", "购买牲畜", [ - Param("kind", "str", True, None, "牲畜标识:chicken / sheep / cow"), - ], lambda e, p: e.buy_animal(p["kind"]))) 187 + _register( 188 + Action( 189 + "buy_animal", 190 + "购买牲畜", 191 + [ 192 + Param("kind", "str", True, None, "牲畜标识:chicken / sheep / cow"), 193 + ], 194 + lambda e, p: e.buy_animal(p["kind"]), 195 + ) 196 + ) 197 | - _register(Action("feed", "喂食指定牲畜", [ - Param("animal_id", "int", True, None, "牲畜编号(state.animals[].id)"), - Param("quantity", "int", False, None, "喂食份数,默认按需求;多喂无益"), - ], lambda e, p: e.feed(p["animal_id"], p.get("quantity")))) 198 + _register( 199 + Action( 200 + "feed", 201 + "喂食指定牲畜", 202 + [ 203 + Param("animal_id", "int", True, None, "牲畜编号(state.animals[].id)"), 204 + Param("quantity", "int", False, None, "喂食份数,默认按需求;多喂无益"), 205 + ], 206 + lambda e, p: e.feed(p["animal_id"], p.get("quantity")), 207 + ) 208 + ) 209 | - _register(Action("collect", "收取牲畜的产出并自动出售", [ - Param("animal_id", "int", True, None, "牲畜编号"), - ], lambda e, p: e.collect(p["animal_id"]))) 210 + _register( 211 + Action( 212 + "collect", 213 + "收取牲畜的产出并自动出售", 214 + [ 215 + Param("animal_id", "int", True, None, "牲畜编号"), 216 + ], 217 + lambda e, p: e.collect(p["animal_id"]), 218 + ) 219 + ) 220 | - _register(Action("buy_feed", "购买饲料", [ - Param("quantity", "int", False, config.FEED_BULK, f"份数,默认 {config.FEED_BULK}"), - ], lambda e, p: e.buy_feed(p.get("quantity", config.FEED_BULK)))) 221 + _register( 222 + Action( 223 + "buy_feed", 224 + "购买饲料", 225 + [ 226 + Param("quantity", "int", False, config.FEED_BULK, f"份数,默认 {config.FEED_BULK}"), 227 + ], 228 + lambda e, p: e.buy_feed(p.get("quantity", config.FEED_BULK)), 229 + ) 230 + ) 231 | -------------------------------------------------------------------------------- 235 | - _register(Action("pause", "暂停或恢复自动推进时间", [ - Param("paused", "bool", False, None, "true 暂停 / false 恢复;省略则反转当前状态"), - ], _pause)) 236 + _register( 237 + Action( 238 + "pause", 239 + "暂停或恢复自动推进时间", 240 + [ 241 + Param("paused", "bool", False, None, "true 暂停 / false 恢复;省略则反转当前状态"), 242 + ], 243 + _pause, 244 + ) 245 + ) 246 | -------------------------------------------------------------------------------- 259 | - def run_action(engine: FarmEngine, name: Any, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: 260 + def run_action( 261 + engine: FarmEngine, name: Any, params: Optional[Dict[str, Any]] = None 262 + ) -> Dict[str, Any]: 263 | """校验参数并执行动作。 | unformatted: File would be reformatted --> farm\server\app.py:128:9 | 127 | f"2. 行动点:播种/浇水/喂食各消耗 1 点,收获与收取产出免费。行动点用完只能等第二天" - "(POST /api/actions {\"action\":\"sleep\"} 可主动推进一天,有 2 秒冷却)。\n" 128 + '(POST /api/actions {"action":"sleep"} 可主动推进一天,有 2 秒冷却)。\n' 129 | f"3. 水分:作物每天消耗 1 点水分(上限 {config.WATER_CAPACITY}),水分为 0 时停止生长。" -------------------------------------------------------------------------------- 141 | " GET /api/actions?farm=me → 拿到当前所有**合法**动作(含开销与理由)\n" - " POST /api/actions → {\"action\":\"<上一步里的 action>\"," - "\"params\":{...}} 直接执行\n" 142 + ' POST /api/actions → {"action":"<上一步里的 action>",' 143 + '"params":{...}} 直接执行\n' 144 | " 响应里的 state 就是执行后的最新状态,无需再查一次。\n" 145 | "\n实时通道:ws://host/ws?farm=me ,服务器在状态变化时推送 state 消息;" - "也可以直接在 WS 上发送 {\"type\":\"action\",\"action\":\"plant\"," - "\"params\":{\"plot\":1,\"crop\":\"wheat\"},\"id\":1} 并等待 result 回执。\n" 146 + '也可以直接在 WS 上发送 {"type":"action","action":"plant",' 147 + '"params":{"plot":1,"crop":"wheat"},"id":1} 并等待 result 回执。\n' 148 | ) -------------------------------------------------------------------------------- 252 | session.engine.add_log(f"已存档到 {path}", "info") - return {"ok": True, "action": "save", "path": path, - "message": f"已存档到 {path}"} 253 + return {"ok": True, "action": "save", "path": path, "message": f"已存档到 {path}"} 254 | if key == "reset": 255 | session.reset() - return {"ok": True, "action": "reset", - "message": f"牧场「{session.farm_id}」已重置,重新开始经营"} 256 + return { 257 + "ok": True, 258 + "action": "reset", 259 + "message": f"牧场「{session.farm_id}」已重置,重新开始经营", 260 + } 261 | return actions_mod.run_action(session.engine, name, params) 262 | - def run_and_wrap(session: FarmSession, name: Any, params: Any, - full: bool = False) -> Dict[str, Any]: 263 + def run_and_wrap( 264 + session: FarmSession, name: Any, params: Any, full: bool = False 265 + ) -> Dict[str, Any]: 266 | """执行动作并拼装统一响应。HTTP 与 WS 共用。""" -------------------------------------------------------------------------------- 318 | "log": "GET /api/log?farm=NAME&limit=20&since=0", - "execute": "POST /api/actions {\"action\":\"plant\",\"params\":{\"plot\":1,\"crop\":\"wheat\"}}", - "farms": "GET /api/farms | POST /api/farms {\"farm\":\"NAME\"}", 319 + "execute": 'POST /api/actions {"action":"plant","params":{"plot":1,"crop":"wheat"}}', 320 + "farms": 'GET /api/farms | POST /api/farms {"farm":"NAME"}', 321 | "realtime": "WS /ws?farm=NAME", -------------------------------------------------------------------------------- 409 | limit = _int(request.query_params.get("limit"), 20, 1, 200) - since = _int(request.query_params.get("since"), 0, 0, 10 ** 9) 410 + since = _int(request.query_params.get("since"), 0, 0, 10**9) 411 | entries = session.engine.recent_log(limit=limit, since=since) -------------------------------------------------------------------------------- 540 | # 例如牧场数量达到上限:说清楚原因再关,别让客户端以为是网络问题 - await websocket.send_json({"type": "error", "error": exc.code, - "message": exc.message}) 541 + await websocket.send_json({"type": "error", "error": exc.code, "message": exc.message}) 542 | await websocket.close(code=1013, reason=exc.message[:100]) -------------------------------------------------------------------------------- 546 | - await websocket.send_json({ - "type": "welcome", - "client_id": client.cid, - "farm": farm, - "server": { - "name": SERVER_NAME, - "version": API_VERSION, - "world": cfg.world_name, - "tick_seconds": session.engine.tick_seconds, - }, - "state": session.snapshot(), - "catalog": session.engine.catalog(), - "actions": session.engine.available_actions(), - "log": session.engine.recent_log(limit=12), - "online": hub.online(farm), - }) 547 + await websocket.send_json( 548 + { 549 + "type": "welcome", 550 + "client_id": client.cid, 551 + "farm": farm, 552 + "server": { 553 + "name": SERVER_NAME, 554 + "version": API_VERSION, 555 + "world": cfg.world_name, 556 + "tick_seconds": session.engine.tick_seconds, 557 + }, 558 + "state": session.snapshot(), 559 + "catalog": session.engine.catalog(), 560 + "actions": session.engine.available_actions(), 561 + "log": session.engine.recent_log(limit=12), 562 + "online": hub.online(farm), 563 + } 564 + ) 565 | hub.broadcast( 566 | farm, - {"type": "notice", "farm": farm, - "text": f"{client.name or client.cid} 来到了牧场(在线 {hub.online(farm)})"}, 567 + { 568 + "type": "notice", 569 + "farm": farm, 570 + "text": f"{client.name or client.cid} 来到了牧场(在线 {hub.online(farm)})", 571 + }, 572 | exclude=client, -------------------------------------------------------------------------------- 594 | except json.JSONDecodeError as exc: - await websocket.send_json({"type": "error", "error": "bad_json", - "message": f"不是合法 JSON:{exc}"}) 595 + await websocket.send_json( 596 + {"type": "error", "error": "bad_json", "message": f"不是合法 JSON:{exc}"} 597 + ) 598 | return 599 | if not isinstance(data, dict): - await websocket.send_json({"type": "error", "error": "bad_json", - "message": "消息必须是 JSON 对象"}) 600 + await websocket.send_json( 601 + {"type": "error", "error": "bad_json", "message": "消息必须是 JSON 对象"} 602 + ) 603 | return -------------------------------------------------------------------------------- 612 | if kind in ("sync", "refresh", "state"): - await websocket.send_json({ - "type": "sync", - "id": rid, - "seq": next_seq(), - "farm": session.farm_id, - "state": session.snapshot(), - "actions": session.engine.available_actions(), - "log": session.engine.recent_log(limit=20), - }) 613 + await websocket.send_json( 614 + { 615 + "type": "sync", 616 + "id": rid, 617 + "seq": next_seq(), 618 + "farm": session.farm_id, 619 + "state": session.snapshot(), 620 + "actions": session.engine.available_actions(), 621 + "log": session.engine.recent_log(limit=20), 622 + } 623 + ) 624 | return 625 | 626 | if kind == "help": - await websocket.send_json({"type": "help", "id": rid, "rules": rules_text(), - "actions": actions_mod.describe_actions()}) 627 + await websocket.send_json( 628 + { 629 + "type": "help", 630 + "id": rid, 631 + "rules": rules_text(), 632 + "actions": actions_mod.describe_actions(), 633 + } 634 + ) 635 | return -------------------------------------------------------------------------------- 640 | result = await asyncio.to_thread( - execute_action, session, data.get("action"), 641 + execute_action, 642 + session, 643 + data.get("action"), 644 | data.get("params") or {}, 645 | ) 646 | except GameError as exc: 647 | counters["errors"] += 1 - await websocket.send_json({"type": "result", "id": rid, "ok": False, - "error": exc.code, "message": exc.message, - "farm": session.farm_id}) 648 + await websocket.send_json( 649 + { 650 + "type": "result", 651 + "id": rid, 652 + "ok": False, 653 + "error": exc.code, 654 + "message": exc.message, 655 + "farm": session.farm_id, 656 + } 657 + ) 658 | return 659 | counters["actions"] += 1 660 | session.touch(count_action=True) - await websocket.send_json({ - "type": "result", - "id": rid, - "ok": True, - "farm": session.farm_id, - "message": result.get("message", "完成"), - "result": result, - "state": brief(session, hub), - # 回执里带上完整快照:客户端可以立刻刷新画面,不必等下一次推送 - "snapshot": session.snapshot(), - "log": session.engine.recent_log(limit=3), - "seq": next_seq(), - }) 661 + await websocket.send_json( 662 + { 663 + "type": "result", 664 + "id": rid, 665 + "ok": True, 666 + "farm": session.farm_id, 667 + "message": result.get("message", "完成"), 668 + "result": result, 669 + "state": brief(session, hub), 670 + # 回执里带上完整快照:客户端可以立刻刷新画面,不必等下一次推送 671 + "snapshot": session.snapshot(), 672 + "log": session.engine.recent_log(limit=3), 673 + "seq": next_seq(), 674 + } 675 + ) 676 | return 677 | - await websocket.send_json({"type": "error", "error": "unknown_message", - "message": f"不认识的消息类型 {kind!r}", - "hint": "可用:action / ping / sync / help"}) 678 + await websocket.send_json( 679 + { 680 + "type": "error", 681 + "error": "unknown_message", 682 + "message": f"不认识的消息类型 {kind!r}", 683 + "hint": "可用:action / ping / sync / help", 684 + } 685 + ) 686 | -------------------------------------------------------------------------------- 701 | session.farm_id, - {"type": "notice", "farm": session.farm_id, - "text": f"{client.name or client.cid} 离开了牧场(在线 {hub.online(session.farm_id)})"}, 702 + { 703 + "type": "notice", 704 + "farm": session.farm_id, 705 + "text": f"{client.name or client.cid} 离开了牧场(在线 {hub.online(session.farm_id)})", 706 + }, 707 | ) | unformatted: File would be reformatted --> farm\server\daemon.py:51:60 | 50 | kernel32 = ctypes.windll.kernel32 - handle = kernel32.OpenProcess(0x1000, False, pid) # QUERY_LIMITED_INFORMATION 51 + handle = kernel32.OpenProcess(0x1000, False, pid) # QUERY_LIMITED_INFORMATION 52 | if not handle: -------------------------------------------------------------------------------- 70 | - def __init__(self, host: str = "127.0.0.1", port: int = 8765, - token: Optional[str] = None, tick: float = 15.0, - save_dir: str = "", pid_file: str = DEFAULT_PID_FILE, - log_file: str = DEFAULT_LOG_FILE, python: Optional[str] = None) -> None: 71 + def __init__( 72 + self, 73 + host: str = "127.0.0.1", 74 + port: int = 8765, 75 + token: Optional[str] = None, 76 + tick: float = 15.0, 77 + save_dir: str = "", 78 + pid_file: str = DEFAULT_PID_FILE, 79 + log_file: str = DEFAULT_LOG_FILE, 80 + python: Optional[str] = None, 81 + ) -> None: 82 | self.host = host -------------------------------------------------------------------------------- 150 | sock.settimeout(0.6) - return sock.connect_ex((self.host if self.host != "0.0.0.0" else "127.0.0.1", - self.port)) == 0 151 + return ( 152 + sock.connect_ex((self.host if self.host != "0.0.0.0" else "127.0.0.1", self.port)) 153 + == 0 154 + ) 155 | 156 | # ------------------------------------------------------------ 启动 157 | def command(self) -> List[str]: 158 | args = [ - self.python, "-m", "farm.server", - "--host", self.host, - "--port", str(self.port), - "--tick", str(self.tick), - "--save-dir", self.save_dir, 159 + self.python, 160 + "-m", 161 + "farm.server", 162 + "--host", 163 + self.host, 164 + "--port", 165 + str(self.port), 166 + "--tick", 167 + str(self.tick), 168 + "--save-dir", 169 + self.save_dir, 170 | # 关键:pid 文件由**服务进程自己**写。 171 | # Windows 上 venv 的 python.exe 可能是个转发器,父进程拿到的 pid 172 | # 未必是真正在监听端口的那个进程,写错 pid 会导致 stop 杀错人。 - "--pid-file", self.pid_file, 173 + "--pid-file", 174 + self.pid_file, 175 | ] -------------------------------------------------------------------------------- 190 | os.makedirs(os.path.dirname(self.pid_file), exist_ok=True) - self.clear_pid() # 清掉上一次留下的陈旧 pid 191 + self.clear_pid() # 清掉上一次留下的陈旧 pid 192 | env = dict(os.environ) -------------------------------------------------------------------------------- 245 | - return False, ( - f"服务器进程已启动但 {wait:.0f} 秒内没响应。请查看日志:{self.log_file}" - ) 246 + return False, (f"服务器进程已启动但 {wait:.0f} 秒内没响应。请查看日志:{self.log_file}") 247 | -------------------------------------------------------------------------------- 277 | if force and os.name == "nt": - subprocess.run(["taskkill", "/F", "/T", "/PID", str(pid)], - capture_output=True, check=False) 278 + subprocess.run( 279 + ["taskkill", "/F", "/T", "/PID", str(pid)], capture_output=True, check=False 280 + ) 281 | if self._wait_offline(pid, 3.0): -------------------------------------------------------------------------------- 308 | url += "?token=" + urllib.parse.quote(self.token) - request = urllib.request.Request(url, data=b"{}", method="POST", - headers={"Content-Type": "application/json"}) 309 + request = urllib.request.Request( 310 + url, data=b"{}", method="POST", headers={"Content-Type": "application/json"} 311 + ) 312 | try: | unformatted: File would be reformatted --> farm\server\hub.py:31:24 | 30 | - STATE_INTERVAL = 0.25 # 状态推送的最小间隔(秒),天然把高频操作合并成一次 - QUEUE_SIZE = 32 # 单连接出口缓冲区;满了就丢最旧的(状态类消息可丢) 31 + STATE_INTERVAL = 0.25 # 状态推送的最小间隔(秒),天然把高频操作合并成一次 32 + QUEUE_SIZE = 32 # 单连接出口缓冲区;满了就丢最旧的(状态类消息可丢) 33 | -------------------------------------------------------------------------------- 50 | - __slots__ = ("cid", "farm_id", "loop", "queue", "name", "joined_at", "alive", - "_sent", "_dropped") 51 + __slots__ = ( 52 + "cid", 53 + "farm_id", 54 + "loop", 55 + "queue", 56 + "name", 57 + "joined_at", 58 + "alive", 59 + "_sent", 60 + "_dropped", 61 + ) 62 | -------------------------------------------------------------------------------- 156 | # ------------------------------------------------------------ 广播 - def broadcast(self, farm_id: str, message: Dict[str, Any], - exclude: Optional[Client] = None) -> int: 157 + def broadcast( 158 + self, farm_id: str, message: Dict[str, Any], exclude: Optional[Client] = None 159 + ) -> int: 160 | sent = 0 -------------------------------------------------------------------------------- 231 | session.published_day = day - self.hub.broadcast(session.farm_id, { - "type": "day", - "farm": session.farm_id, - "day": day, - "events": session.last_events, - "at": now, - }) 232 + self.hub.broadcast( 233 + session.farm_id, 234 + { 235 + "type": "day", 236 + "farm": session.farm_id, 237 + "day": day, 238 + "events": session.last_events, 239 + "at": now, 240 + }, 241 + ) 242 | self.hub.broadcast(session.farm_id, payload) -------------------------------------------------------------------------------- 248 | - def build_state_message(session, seq: int, note: str = "", extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: 249 + def build_state_message( 250 + session, seq: int, note: str = "", extra: Optional[Dict[str, Any]] = None 251 + ) -> Dict[str, Any]: 252 | """构造给客户端的状态消息(HTTP 与 WS 共用同一份结构)。""" | unformatted: File would be reformatted --> farm\server\session.py:66:73 | 65 | - def __init__(self, farm_id: str, save_dir: str, tick_seconds: float, - owner: str = "") -> None: 66 + def __init__(self, farm_id: str, save_dir: str, tick_seconds: float, owner: str = "") -> None: 67 | self.farm_id = farm_id -------------------------------------------------------------------------------- 182 | - def __init__(self, save_dir: str, tick_seconds: float = config.DEFAULT_TICK_SECONDS, - max_sessions: int = 64, idle_timeout: float = 1800.0) -> None: 183 + def __init__( 184 + self, 185 + save_dir: str, 186 + tick_seconds: float = config.DEFAULT_TICK_SECONDS, 187 + max_sessions: int = 64, 188 + idle_timeout: float = 1800.0, 189 + ) -> None: 190 | self.save_dir = save_dir -------------------------------------------------------------------------------- 209 | "too_many_farms", - f"服务器牧场数量已达上限({self.max_sessions})," - "请稍后再试或联系管理员扩容", 210 + f"服务器牧场数量已达上限({self.max_sessions}),请稍后再试或联系管理员扩容", 211 | status=503, -------------------------------------------------------------------------------- 258 | if session.engine.paused: - continue # 暂停是人为操作,说明玩家还会回来 259 + continue # 暂停是人为操作,说明玩家还会回来 260 | if now - session.last_active < self.idle_timeout: -------------------------------------------------------------------------------- 264 | except OSError: - continue # 存不下就别丢,宁可占着内存 265 + continue # 存不下就别丢,宁可占着内存 266 | with self._lock: | unformatted: File would be reformatted --> farm\server\webui.py:434:9 | 433 | " -H 'Content-Type: application/json' \\\n" - " -d '{\"action\":\"plant\",\"params\":{\"plot\":1,\"crop\":\"wheat\"}}'\n" 434 + ' -d \'{"action":"plant","params":{"plot":1,"crop":"wheat"}}\'\n' 435 | "\n" -------------------------------------------------------------------------------- 447 | "------\n" - "POST /api/actions {\"action\":\"...\",\"params\":{...}} 通用入口\n" - "POST /api/batch {\"actions\":[{...},{...}]} 批量\n" 448 + 'POST /api/actions {"action":"...","params":{...}} 通用入口\n' 449 + 'POST /api/batch {"actions":[{...},{...}]} 批量\n' 450 | "POST /api/plant|water|harvest|harvest_all|buy_animal|feed|\n" 451 | " collect|buy_feed|buy_plot|sleep|pause|save|reset 便捷入口\n" - "POST /api/farms {\"farm\":\"名字\"} 创建/进入牧场\n" 452 + 'POST /api/farms {"farm":"名字"} 创建/进入牧场\n' 453 | "POST /api/admin/save 全部存档\n" | unformatted: File would be reformatted --> farm\tui.py:44:18 | 43 | ANSI_RE = re.compile(r"\x1b\[[0-9;]*m") - CELL_WIDTH = 17 # 单块农田格子的显示宽度 44 + CELL_WIDTH = 17 # 单块农田格子的显示宽度 45 | CELL_INNER = CELL_WIDTH - 2 - SIDE_WIDTH = 36 # 右侧栏宽度 46 + SIDE_WIDTH = 36 # 右侧栏宽度 47 | MIN_TERM_WIDTH = 76 - MAX_VISIBLE_ANIMALS = 5 # 侧栏同时最多展示的牲畜数量 48 + MAX_VISIBLE_ANIMALS = 5 # 侧栏同时最多展示的牲畜数量 49 | -------------------------------------------------------------------------------- 135 | # ---------------------------------------------------------------- 绘制工具 - def draw_box(title: str, body: List[str], width: int, accent: Optional[str] = None, - use_color: bool = True) -> List[str]: 136 + def draw_box( 137 + title: str, body: List[str], width: int, accent: Optional[str] = None, use_color: bool = True 138 + ) -> List[str]: 139 | """画一个带标题的方框,返回行列表(宽度精确等于 ``width``)。""" -------------------------------------------------------------------------------- 168 | - def __init__(self, engine: Any, save_path: Optional[str] = None, - use_color: bool = True, autosave: bool = True, - server_label: str = "") -> None: 169 + def __init__( 170 + self, 171 + engine: Any, 172 + save_path: Optional[str] = None, 173 + use_color: bool = True, 174 + autosave: bool = True, 175 + server_label: str = "", 176 + ) -> None: 177 | # engine 既可以是本地 FarmEngine,也可以是远程 RemoteFarm——只要接口一致 -------------------------------------------------------------------------------- 185 | self.running = True - self.mode = "main" # main / help / select_crop / select_animal / confirm_reset 186 + self.mode = "main" # main / help / select_crop / select_animal / confirm_reset 187 | self.plot_cursor = 0 -------------------------------------------------------------------------------- 211 | try: - self.engine.save(self.save_path) # 本地引擎写文件;远程引擎让服务器落盘 212 + self.engine.save(self.save_path) # 本地引擎写文件;远程引擎让服务器落盘 213 | except Exception: -------------------------------------------------------------------------------- 620 | filled_count = max(0, min(10, int(round(ratio * 10)))) - progress = color("█" * filled_count + "░" * (10 - filled_count), - C_GREEN if plot["is_ripe"] else C_CYAN, self.use_color) 621 + progress = color( 622 + "█" * filled_count + "░" * (10 - filled_count), 623 + C_GREEN if plot["is_ripe"] else C_CYAN, 624 + self.use_color, 625 + ) 626 | -------------------------------------------------------------------------------- 640 | body = [ - color("│", border, self.use_color) + pad(line1, CELL_INNER) + color("│", border, self.use_color), - color("│", border, self.use_color) + pad(line2, CELL_INNER) + color("│", border, self.use_color), 641 + color("│", border, self.use_color) 642 + + pad(line1, CELL_INNER) 643 + + color("│", border, self.use_color), 644 + color("│", border, self.use_color) 645 + + pad(line2, CELL_INNER) 646 + + color("│", border, self.use_color), 647 | ] -------------------------------------------------------------------------------- 658 | # 只渲染一个跟随光标的窗口,避免牲畜变多后把界面撑爆 - start = max(0, min(max(0, len(animals) - MAX_VISIBLE_ANIMALS), - self.animal_cursor - MAX_VISIBLE_ANIMALS // 2)) 659 + start = max( 660 + 0, 661 + min( 662 + max(0, len(animals) - MAX_VISIBLE_ANIMALS), 663 + self.animal_cursor - MAX_VISIBLE_ANIMALS // 2, 664 + ), 665 + ) 666 | for index in range(start, min(len(animals), start + MAX_VISIBLE_ANIMALS)): -------------------------------------------------------------------------------- 671 | if animal["pending"] > 0: - status = color(f"{animal['product']}x{animal['pending']}", - C_BOLD + C_GOLD, self.use_color) 672 + status = color( 673 + f"{animal['product']}x{animal['pending']}", C_BOLD + C_GOLD, self.use_color 674 + ) 675 | elif animal["is_sick"]: -------------------------------------------------------------------------------- 683 | - head = marker + color(f" #{animal['id']} {animal['name']}", - C_WHITE, self.use_color) 684 + head = marker + color(f" #{animal['id']} {animal['name']}", C_WHITE, self.use_color) 685 | rows.append(pad(head, 13) + status) -------------------------------------------------------------------------------- 689 | rows.append(color(f" …还有 {hidden} 只(Tab 切换)", C_DIM, self.use_color)) - pasture = draw_box(f"牧场 {snap['animal_count']}/{snap['max_animals']}", rows, - side_width, accent=C_ORANGE, use_color=self.use_color) 690 + pasture = draw_box( 691 + f"牧场 {snap['animal_count']}/{snap['max_animals']}", 692 + rows, 693 + side_width, 694 + accent=C_ORANGE, 695 + use_color=self.use_color, 696 + ) 697 | -------------------------------------------------------------------------------- 719 | ) - market = draw_box("市场行情", market_rows, side_width, - accent=C_CYAN, use_color=self.use_color) 720 + market = draw_box( 721 + "市场行情", market_rows, side_width, accent=C_CYAN, use_color=self.use_color 722 + ) 723 | -------------------------------------------------------------------------------- 780 | rows = [ - color(" 玩法目标:靠种地和养殖把净资产做到 10000 金币,解锁全部 5 个里程碑。", C_WHITE, self.use_color), 781 + color( 782 + " 玩法目标:靠种地和养殖把净资产做到 10000 金币,解锁全部 5 个里程碑。", 783 + C_WHITE, 784 + self.use_color, 785 + ), 786 | "", 787 | color(" 时间", C_BOLD + C_GOLD, self.use_color) - + color(" 每 15 秒自动推进一天,作物成长、牲畜产奶、市场价格都在这一天结算。", C_GRAY, self.use_color), 788 + + color( 789 + " 每 15 秒自动推进一天,作物成长、牲畜产奶、市场价格都在这一天结算。", 790 + C_GRAY, 791 + self.use_color, 792 + ), 793 | color(" 水分", C_BOLD + C_GOLD, self.use_color) - + color(" 作物每天消耗 1 点水分,水为 0 就停止生长。按 W 浇水恢复到 3 点。", C_GRAY, self.use_color), 794 + + color( 795 + " 作物每天消耗 1 点水分,水为 0 就停止生长。按 W 浇水恢复到 3 点。", 796 + C_GRAY, 797 + self.use_color, 798 + ), 799 | color(" 行动点", C_BOLD + C_GOLD, self.use_color) - + color(" 每天 10 点,播种/浇水/喂食各消耗 1 点,收获和收取免费。", C_GRAY, self.use_color), 800 + + color( 801 + " 每天 10 点,播种/浇水/喂食各消耗 1 点,收获和收取免费。", C_GRAY, self.use_color 802 + ), 803 | color(" 牲畜", C_BOLD + C_GOLD, self.use_color) - + color(" 必须每天喂食才会产出;连续 3 天没吃东西会离开农场。", C_GRAY, self.use_color), 804 + + color( 805 + " 必须每天喂食才会产出;连续 3 天没吃东西会离开农场。", C_GRAY, self.use_color 806 + ), 807 | color(" 市场", C_BOLD + C_GOLD, self.use_color) 808 | + color(" 作物和畜产品价格每天波动 ±18%,高点卖出更赚。", C_GRAY, self.use_color), 809 | "", 810 | color(" 快捷键", C_BOLD + C_GOLD, self.use_color) - + color(" P 播种 / W 浇水 / H 收获 / E 一键收获 / F 喂食 / C 收取产出", C_GRAY, self.use_color), 811 + + color( 812 + " P 播种 / W 浇水 / H 收获 / E 一键收获 / F 喂食 / C 收取产出", 813 + C_GRAY, 814 + self.use_color, 815 + ), 816 | color(" ", C_GRAY, self.use_color) - + color(" B 买动物 / L 开垦土地 / K 买饲料 / N 休息 / 空格 暂停 / S 存档 / R 重置", C_GRAY, self.use_color), 817 + + color( 818 + " B 买动物 / L 开垦土地 / K 买饲料 / N 休息 / 空格 暂停 / S 存档 / R 重置", 819 + C_GRAY, 820 + self.use_color, 821 + ), 822 | "", 823 | color(" AI 接入", C_BOLD + C_GOLD, self.use_color) - + color(" 游戏跑在服务器进程里,HTTP 接口与 WebSocket 通道同时开放,", - C_GRAY, self.use_color), 824 + + color( 825 + " 游戏跑在服务器进程里,HTTP 接口与 WebSocket 通道同时开放,", 826 + C_GRAY, 827 + self.use_color, 828 + ), 829 | color(" ", C_GRAY, self.use_color) - + color(" AI 与你在同一个农场里操作(见 README「给 AI 的接口」)。", - C_GRAY, self.use_color), 830 + + color( 831 + " AI 与你在同一个农场里操作(见 README「给 AI 的接口」)。", C_GRAY, self.use_color 832 + ), 833 | "", | unformatted: File would be reformatted --> main.py:101:26 | 100 | p_serve.add_argument("--daemon", action="store_true", help="转到后台运行(守护进程)") - p_serve.add_argument("--host", default="127.0.0.1", - help="监听地址,默认 127.0.0.1;局域网联机用 0.0.0.0") 101 + p_serve.add_argument( 102 + "--host", default="127.0.0.1", help="监听地址,默认 127.0.0.1;局域网联机用 0.0.0.0" 103 + ) 104 | p_serve.add_argument("--port", type=int, default=8765, help="监听端口,默认 8765") 105 | p_serve.add_argument("--token", default=None, help="访问令牌,设置后接口需要携带") - p_serve.add_argument("--tick", type=float, default=config.DEFAULT_TICK_SECONDS, - help=f"每多少秒推进一天,默认 {config.DEFAULT_TICK_SECONDS:.0f} 秒") 106 + p_serve.add_argument( 107 + "--tick", 108 + type=float, 109 + default=config.DEFAULT_TICK_SECONDS, 110 + help=f"每多少秒推进一天,默认 {config.DEFAULT_TICK_SECONDS:.0f} 秒", 111 + ) 112 | p_serve.add_argument("--save-dir", default=None, help="存档目录,默认 <项目>/saves") 113 | p_serve.add_argument("--world", default="SimpleFarm 公共农场", help="世界名称") 114 | p_serve.add_argument("--max-sessions", type=int, default=64, help="同时在线的牧场上限") - p_serve.add_argument("--idle-timeout", type=float, default=1800.0, - help="无人访问多少秒后把牧场移出内存(存档保留)") 115 + p_serve.add_argument( 116 + "--idle-timeout", 117 + type=float, 118 + default=1800.0, 119 + help="无人访问多少秒后把牧场移出内存(存档保留)", 120 + ) 121 | p_serve.add_argument("--no-remote-control", action="store_true", help="禁止接口停机") 122 | p_serve.add_argument("--access-log", action="store_true", help="打印每个 HTTP 请求") - p_serve.add_argument("--log-level", default="info", - choices=["critical", "error", "warning", "info", "debug"]) 123 + p_serve.add_argument( 124 + "--log-level", default="info", choices=["critical", "error", "warning", "info", "debug"] 125 + ) 126 | p_serve.add_argument("--log-file", default=None, help="守护进程日志路径") -------------------------------------------------------------------------------- 151 | p_play.add_argument("--no-color", action="store_true") - p_play.add_argument("--no-realtime", action="store_true", - help="不用 WebSocket,改走 HTTP 轮询") 152 + p_play.add_argument("--no-realtime", action="store_true", help="不用 WebSocket,改走 HTTP 轮询") 153 | -------------------------------------------------------------------------------- 161 | p_do.add_argument("action", help="动作名:plant / water / harvest / ...") - p_do.add_argument("params", nargs="?", default="{}", - help='JSON 参数,例如 \'{"plot":1,"crop":"wheat"}\'') 162 + p_do.add_argument( 163 + "params", nargs="?", default="{}", help='JSON 参数,例如 \'{"plot":1,"crop":"wheat"}\'' 164 + ) 165 | _add_client_args(p_do) -------------------------------------------------------------------------------- 214 | return RemoteFarm( - endpoint=args.url, farm=getattr(args, "farm", "default"), 215 + endpoint=args.url, 216 + farm=getattr(args, "farm", "default"), 217 | token=getattr(args, "token", None), 218 | name=getattr(args, "name", "") or "cli", - realtime=realtime, autostart=True, 219 + realtime=realtime, 220 + autostart=True, 221 | ) -------------------------------------------------------------------------------- 235 | argv = [ - "--host", args.host, "--port", str(args.port), - "--tick", str(args.tick), "--world", args.world, - "--max-sessions", str(args.max_sessions), - "--idle-timeout", str(args.idle_timeout), - "--log-level", args.log_level, 236 + "--host", 237 + args.host, 238 + "--port", 239 + str(args.port), 240 + "--tick", 241 + str(args.tick), 242 + "--world", 243 + args.world, 244 + "--max-sessions", 245 + str(args.max_sessions), 246 + "--idle-timeout", 247 + str(args.idle_timeout), 248 + "--log-level", 249 + args.log_level, 250 | ] -------------------------------------------------------------------------------- 307 | for row in world["leaderboard"][:5]: - print(f" #{row['rank']} {row['farm']:<14} 净资产 {row['net_worth']:>8.0f}" - f" · 第 {row['day']} 天 · Lv.{row['level']} · 在线 {row['online']}") 308 + print( 309 + f" #{row['rank']} {row['farm']:<14} 净资产 {row['net_worth']:>8.0f}" 310 + f" · 第 {row['day']} 天 · Lv.{row['level']} · 在线 {row['online']}" 311 + ) 312 | -------------------------------------------------------------------------------- 342 | label = f"{client.farm} @ {client.host}" - app = TuiApp(client, save_path=None, use_color=not args.no_color, - autosave=True, server_label=label) 343 + app = TuiApp( 344 + client, save_path=None, use_color=not args.no_color, autosave=True, server_label=label 345 + ) 346 | try: -------------------------------------------------------------------------------- 392 | except json.JSONDecodeError as exc: - return die(f"参数不是合法 JSON:{exc}", '例子:python main.py do plant \'{"plot":1,"crop":"wheat"}\'') 393 + return die( 394 + f"参数不是合法 JSON:{exc}", 395 + '例子:python main.py do plant \'{"plot":1,"crop":"wheat"}\'', 396 + ) 397 | if not isinstance(params, dict): -------------------------------------------------------------------------------- 403 | except GameError as exc: - return die(f"{args.action} 失败({exc.code}):{exc.message}", - "看看现在能做什么:python main.py actions") 404 + return die( 405 + f"{args.action} 失败({exc.code}):{exc.message}", 406 + "看看现在能做什么:python main.py actions", 407 + ) 408 | - print(ok("✓ ") + str(result.get("message", "完成")) - + dim(f" ({time.time() - started:.2f}s)")) - extra = {k: v for k, v in result.items() - if k not in ("ok", "message", "state", "log", "action") and not isinstance(v, (dict, list))} 409 + print(ok("✓ ") + str(result.get("message", "完成")) + dim(f" ({time.time() - started:.2f}s)")) 410 + extra = { 411 + k: v 412 + for k, v in result.items() 413 + if k not in ("ok", "message", "state", "log", "action") and not isinstance(v, (dict, list)) 414 + } 415 | if extra: 416 | print(dim(" " + " ".join(f"{k}={v}" for k, v in extra.items()))) 417 | state = result.get("state") or {} 418 | if state: - print(f" 第 {state.get('day')} 天 · 金币 {state.get('gold')} · " - f"行动点 {state.get('ap')}/{state.get('max_ap')} · 净资产 {state.get('net_worth')}") 419 + print( 420 + f" 第 {state.get('day')} 天 · 金币 {state.get('gold')} · " 421 + f"行动点 {state.get('ap')}/{state.get('max_ap')} · 净资产 {state.get('net_worth')}" 422 + ) 423 | return 0 -------------------------------------------------------------------------------- 432 | except json.JSONDecodeError as exc: - return die(f"actions 不是合法 JSON:{exc}", - '例子:python main.py batch \'[{"action":"harvest_all"},{"action":"buy_feed","params":{"quantity":5}}]\'') 433 + return die( 434 + f"actions 不是合法 JSON:{exc}", 435 + '例子:python main.py batch \'[{"action":"harvest_all"},{"action":"buy_feed","params":{"quantity":5}}]\'', 436 + ) 437 | if not isinstance(items, list): -------------------------------------------------------------------------------- 453 | items = client.available_actions() - print(f"牧场 {client.farm} · 第 {state.get('day')} 天 · 金币 {state.get('gold')} · " - f"行动点 {state.get('ap')}/{state.get('max_ap')}") 454 + print( 455 + f"牧场 {client.farm} · 第 {state.get('day')} 天 · 金币 {state.get('gold')} · " 456 + f"行动点 {state.get('ap')}/{state.get('max_ap')}" 457 + ) 458 | if not items: -------------------------------------------------------------------------------- 465 | cost_text = " ".join(f"{k}:{v}" for k, v in cost.items()) or "免费" - print(f" [{index:>2}] {item['action']:<12} {json.dumps(item['params'], ensure_ascii=False):<42}" - f" ({cost_text})") 466 + print( 467 + f" [{index:>2}] {item['action']:<12} {json.dumps(item['params'], ensure_ascii=False):<42}" 468 + f" ({cost_text})" 469 + ) 470 | print(dim(f" {item.get('reason', '')}")) -------------------------------------------------------------------------------- 481 | print(f"牧场 {snap.get('farm')}") - print(f"第 {snap.get('day')} 天" - + (f" · {snap.get('seconds_to_tick')}s 后进入下一天" - if snap.get("seconds_to_tick") is not None else " · 已暂停")) - print(f"金币 {snap.get('gold')} 等级 Lv.{snap.get('level')} " - f"({snap.get('exp')}/{snap.get('exp_needed')})") 482 + print( 483 + f"第 {snap.get('day')} 天" 484 + + ( 485 + f" · {snap.get('seconds_to_tick')}s 后进入下一天" 486 + if snap.get("seconds_to_tick") is not None 487 + else " · 已暂停" 488 + ) 489 + ) 490 + print( 491 + f"金币 {snap.get('gold')} 等级 Lv.{snap.get('level')} " 492 + f"({snap.get('exp')}/{snap.get('exp_needed')})" 493 + ) 494 | print(f"行动点 {snap.get('ap')}/{snap.get('max_ap')} 饲料 {snap.get('feed')}") -------------------------------------------------------------------------------- 500 | status = "可收获" if plot["is_ripe"] else f"还需{plot['days_left']}天" - print(f" {plot['position']:>2} 号地 {plot['crop_name']:<6} 生长 {plot['growth']}" - f"/{plot['grow_days']} 水分 {plot['moisture']} · {status}") 501 + print( 502 + f" {plot['position']:>2} 号地 {plot['crop_name']:<6} 生长 {plot['growth']}" 503 + f"/{plot['grow_days']} 水分 {plot['moisture']} · {status}" 504 + ) 505 | for animal in snap.get("animals", []): - note = (f"{animal['product']} x{animal['pending']} 待收" if animal["pending"] - else ("已喂食" if animal["fed"] else "未喂食")) 506 + note = ( 507 + f"{animal['product']} x{animal['pending']} 待收" 508 + if animal["pending"] 509 + else ("已喂食" if animal["fed"] else "未喂食") 510 + ) 511 | print(f" #{animal['id']} {animal['name']:<4} {note}") -------------------------------------------------------------------------------- 537 | online = snap.get("online") - head = (f"第 {snap.get('day')} 天 | 金币 {snap.get('gold')} | " - f"Lv.{snap.get('level')} | 行动点 {snap.get('ap')}/{snap.get('max_ap')} | " - f"净资产 {snap.get('net_worth')}") 538 + head = ( 539 + f"第 {snap.get('day')} 天 | 金币 {snap.get('gold')} | " 540 + f"Lv.{snap.get('level')} | 行动点 {snap.get('ap')}/{snap.get('max_ap')} | " 541 + f"净资产 {snap.get('net_worth')}" 542 + ) 543 | if online is not None: -------------------------------------------------------------------------------- 572 | world = client.world() - print(f"世界:{world.get('world')} · 已运行 {world.get('uptime', 0):.0f} 秒 · " - f"牧场 {world.get('farms')} 个 · 连接 {world.get('clients')} 个") 573 + print( 574 + f"世界:{world.get('world')} · 已运行 {world.get('uptime', 0):.0f} 秒 · " 575 + f"牧场 {world.get('farms')} 个 · 连接 {world.get('clients')} 个" 576 + ) 577 | print(f"时钟:每 {world.get('tick_seconds')} 秒推进一天\n") 578 | print(f" {'#':>2} {'牧场':<14} {'净资产':>9} {'金币':>7} {'天':>4} {'等级':>4} {'在线':>4}") 579 | for row in world.get("leaderboard", []): - print(f" {row['rank']:>2} {row['farm']:<14} {row['net_worth']:>9.0f} " - f"{row['gold']:>7} {row['day']:>4} {row['level']:>4} {row['online']:>4}") 580 + print( 581 + f" {row['rank']:>2} {row['farm']:<14} {row['net_worth']:>9.0f} " 582 + f"{row['gold']:>7} {row['day']:>4} {row['level']:>4} {row['online']:>4}" 583 + ) 584 | return 0 -------------------------------------------------------------------------------- 660 | health = info.get("health") or {} - print(ok("✓ 服务器正在运行") - + f" http://{info['host']}:{info['port']}/ " - + dim(f"牧场 {health.get('farms', 0)} 个 · 在线 {health.get('clients', 0)}")) 661 + print( 662 + ok("✓ 服务器正在运行") 663 + + f" http://{info['host']}:{info['port']}/ " 664 + + dim(f"牧场 {health.get('farms', 0)} 个 · 在线 {health.get('clients', 0)}") 665 + ) 666 | else: -------------------------------------------------------------------------------- 677 | return handler(args) - except SystemExit as exc: # make_client 里的友好退出 678 + except SystemExit as exc: # make_client 里的友好退出 679 | return int(exc.code or 0) | unformatted: File would be reformatted --> tests\test_engine.py:19:26 | 18 | - from farm import config # noqa: E402 19 + from farm import config # noqa: E402 20 | from farm.engine import FarmEngine, GameError # noqa: E402 - from farm.models import GameState, Plot # noqa: E402 21 + from farm.models import GameState, Plot # noqa: E402 22 | from farm.storage import load_state, save_state # noqa: E402 -------------------------------------------------------------------------------- 192 | def test_buy_plot_without_gold(self) -> None: - self.engine.state.level = 5 # 先解锁土地上限,才能走到"钱不够"这一步 193 + self.engine.state.level = 5 # 先解锁土地上限,才能走到"钱不够"这一步 194 | self.engine.state.gold = 0 -------------------------------------------------------------------------------- 199 | def test_buy_plot_blocked_at_level_cap(self) -> None: - self.engine.state.gold = 10 ** 6 200 + self.engine.state.gold = 10**6 201 | with self.assertRaises(GameError) as ctx: -------------------------------------------------------------------------------- 264 | def test_pasture_limit(self) -> None: - self.engine.state.gold = 10 ** 6 265 + self.engine.state.gold = 10**6 266 | for _ in range(config.MAX_ANIMALS): -------------------------------------------------------------------------------- 282 | def test_buy_feed_validation(self) -> None: - self.engine.state.gold = 10 ** 6 283 + self.engine.state.gold = 10**6 284 | with self.assertRaises(GameError): -------------------------------------------------------------------------------- 352 | self.engine.buy_animal("chicken") - self.engine.state.gold = 500 # 操作完成后再定金币,避免被扣费干扰断言 353 + self.engine.state.gold = 500 # 操作完成后再定金币,避免被扣费干扰断言 354 | self.engine.state.feed = 7 -------------------------------------------------------------------------------- 404 | self.assertEqual(state.level, 1) - self.assertIsNone(state.plots[0].crop) # 非法作物被清空 - self.assertEqual(state.animals, []) # 非法动物被丢弃 - self.assertGreater(state.market["wheat"], 0) # 非法价格回落默认值 405 + self.assertIsNone(state.plots[0].crop) # 非法作物被清空 406 + self.assertEqual(state.animals, []) # 非法动物被丢弃 407 + self.assertGreater(state.market["wheat"], 0) # 非法价格回落默认值 408 | self.assertGreaterEqual(len(state.plots), config.BASE_PLOTS) 409 | 410 | # ============================================================ 并发 411 | def test_concurrent_operations_are_safe(self) -> None: - self.engine.state.gold = 10 ** 6 412 + self.engine.state.gold = 10**6 413 | errors = [] | unformatted: File would be reformatted --> tests\test_server.py:35:22 | 34 | HAS_FASTAPI = True - except ImportError: # pragma: no cover - 取决于环境 35 + except ImportError: # pragma: no cover - 取决于环境 36 | HAS_FASTAPI = False -------------------------------------------------------------------------------- 41 | HAS_UVICORN = True - except ImportError: # pragma: no cover 42 + except ImportError: # pragma: no cover 43 | HAS_UVICORN = False -------------------------------------------------------------------------------- 233 | self.tmp = tempfile.mkdtemp(prefix="farm-api-") - self.cfg = ServerConfig(host="127.0.0.1", port=8765, save_dir=self.tmp, - tick_seconds=60) 234 + self.cfg = ServerConfig(host="127.0.0.1", port=8765, save_dir=self.tmp, tick_seconds=60) 235 | self.app = create_app(self.cfg) -------------------------------------------------------------------------------- 278 | self.assertIn("已种下", data["message"]) - self.assertEqual(data["state"]["gold"], config.START_GOLD - config.CROPS["wheat"]["seed_cost"]) 279 + self.assertEqual( 280 + data["state"]["gold"], config.START_GOLD - config.CROPS["wheat"]["seed_cost"] 281 + ) 282 | -------------------------------------------------------------------------------- 287 | """{"action":"plant","plot":1,"crop":"wheat"} 这种平铺写法也要能用。""" - data = self.client.post("/api/actions?farm=flat", - json={"action": "plant", "plot": 1, "crop": "wheat"}).json() 288 + data = self.client.post( 289 + "/api/actions?farm=flat", json={"action": "plant", "plot": 1, "crop": "wheat"} 290 + ).json() 291 | self.assertTrue(data["ok"]) -------------------------------------------------------------------------------- 298 | def test_action_error_shape(self) -> None: - response = self.client.post("/api/actions?farm=err", - json={"action": "plant", "params": {"plot": 1, "crop": "nope"}}) 299 + response = self.client.post( 300 + "/api/actions?farm=err", json={"action": "plant", "params": {"plot": 1, "crop": "nope"}} 301 + ) 302 | self.assertEqual(response.status_code, 400) -------------------------------------------------------------------------------- 308 | def test_out_of_range_plot(self) -> None: - response = self.client.post("/api/actions?farm=err", - json={"action": "water", "params": {"plot": 999}}) 309 + response = self.client.post( 310 + "/api/actions?farm=err", json={"action": "water", "params": {"plot": 999}} 311 + ) 312 | self.assertEqual(response.status_code, 400) 313 | self.assertEqual(response.json()["error"], "no_such_plot") 314 | 315 | def test_bad_json_body(self) -> None: - response = self.client.post("/api/actions?farm=err", content=b"{oops", - headers={"Content-Type": "application/json"}) 316 + response = self.client.post( 317 + "/api/actions?farm=err", content=b"{oops", headers={"Content-Type": "application/json"} 318 + ) 319 | self.assertEqual(response.status_code, 400) -------------------------------------------------------------------------------- 324 | names = {item["action"] for item in data["actions"]} - self.assertIn("plant", names) # 有空地 + 有钱 → 可播种 - self.assertNotIn("harvest", names) # 还没种东西 → 不可收获 325 + self.assertIn("plant", names) # 有空地 + 有钱 → 可播种 326 + self.assertNotIn("harvest", names) # 还没种东西 → 不可收获 327 | 328 | def test_batch_endpoint(self) -> None: - items = {"actions": [ - {"action": "buy_feed", "params": {"quantity": 5}}, - {"action": "plant", "params": {"plot": 1, "crop": "wheat"}}, - {"action": "harvest", "params": {"plot": 1}}, # 必然失败 - ]} 329 + items = { 330 + "actions": [ 331 + {"action": "buy_feed", "params": {"quantity": 5}}, 332 + {"action": "plant", "params": {"plot": 1, "crop": "wheat"}}, 333 + {"action": "harvest", "params": {"plot": 1}}, # 必然失败 334 + ] 335 + } 336 | data = self.client.post("/api/batch?farm=multi", json=items).json() -------------------------------------------------------------------------------- 341 | def test_log_entries_and_since(self) -> None: - self.client.post("/api/actions?farm=logging", - json={"action": "plant", "params": {"plot": 1, "crop": "wheat"}}) 342 + self.client.post( 343 + "/api/actions?farm=logging", 344 + json={"action": "plant", "params": {"plot": 1, "crop": "wheat"}}, 345 + ) 346 | entries = self.client.get("/api/log?farm=logging").json()["entries"] -------------------------------------------------------------------------------- 355 | def test_farms_list_and_isolation(self) -> None: - self.client.post("/api/actions?farm=alice", - json={"action": "buy_feed", "params": {"quantity": 10}}) 356 + self.client.post( 357 + "/api/actions?farm=alice", json={"action": "buy_feed", "params": {"quantity": 10}} 358 + ) 359 | self.client.post("/api/actions?farm=bob", json={"action": "save"}) 360 | rows = self.client.get("/api/farms").json()["farms"] 361 | by_name = {row["farm"]: row for row in rows} 362 | self.assertIn("alice", by_name) - self.assertEqual(by_name["alice"]["gold"], - config.START_GOLD - 10 * config.FEED_UNIT_PRICE) 363 + self.assertEqual(by_name["alice"]["gold"], config.START_GOLD - 10 * config.FEED_UNIT_PRICE) 364 | -------------------------------------------------------------------------------- 381 | def test_admin_save(self) -> None: - self.client.post("/api/actions?farm=saver", - json={"action": "plant", "params": {"plot": 1, "crop": "wheat"}}) 382 + self.client.post( 383 + "/api/actions?farm=saver", 384 + json={"action": "plant", "params": {"plot": 1, "crop": "wheat"}}, 385 + ) 386 | data = self.client.post("/api/admin/save").json() -------------------------------------------------------------------------------- 432 | def test_action_blocked_without_token(self) -> None: - response = self.client.post("/api/actions", - json={"action": "buy_plot"}) 433 + response = self.client.post("/api/actions", json={"action": "buy_plot"}) 434 | self.assertEqual(response.status_code, 401) -------------------------------------------------------------------------------- 448 | cls.url = f"http://127.0.0.1:{cls.port}" - app = create_app(ServerConfig(host="127.0.0.1", port=cls.port, save_dir=cls.tmp, - tick_seconds=600, autosave_interval=3600)) 449 + app = create_app( 450 + ServerConfig( 451 + host="127.0.0.1", 452 + port=cls.port, 453 + save_dir=cls.tmp, 454 + tick_seconds=600, 455 + autosave_interval=3600, 456 + ) 457 + ) 458 | import uvicorn 459 | - cls.server = uvicorn.Server(uvicorn.Config( - app, host="127.0.0.1", port=cls.port, log_level="warning", - access_log=False, lifespan="on", - )) 460 + cls.server = uvicorn.Server( 461 + uvicorn.Config( 462 + app, 463 + host="127.0.0.1", 464 + port=cls.port, 465 + log_level="warning", 466 + access_log=False, 467 + lifespan="on", 468 + ) 469 + ) 470 | cls.thread = threading.Thread(target=cls.server.run, name="live-server", daemon=True) -------------------------------------------------------------------------------- 479 | time.sleep(0.2) - else: # pragma: no cover 480 + else: # pragma: no cover 481 | raise unittest.SkipTest("服务器没能在 20 秒内起来") -------------------------------------------------------------------------------- 491 | - return RemoteFarm(endpoint=self.url, farm=farm, realtime=realtime, timeout=8.0, - autostart=True) 492 + return RemoteFarm( 493 + endpoint=self.url, farm=farm, realtime=realtime, timeout=8.0, autostart=True 494 + ) 495 | -------------------------------------------------------------------------------- 506 | with self.assertRaises(GameError) as ctx: - client.plant(1, "carrot") # 地已经占用了 507 + client.plant(1, "carrot") # 地已经占用了 508 | self.assertEqual(ctx.exception.code, "plot_busy") 509 | 510 | actions = client.available_actions() 511 | names = {item["action"] for item in actions} - self.assertIn("plant", names) # 还有空地可以种 - self.assertNotIn("harvest", names) # 小麦还没成熟 512 + self.assertIn("plant", names) # 还有空地可以种 513 + self.assertNotIn("harvest", names) # 小麦还没成熟 514 | - client.water(1) # 播种后水分 2/3,可以补满 515 + client.water(1) # 播种后水分 2/3,可以补满 516 | self.assertEqual(client.state.plots[0].moisture, config.WATER_CAPACITY) 517 | with self.assertRaises(GameError) as ctx: - client.water(1) # 水分已满,再浇就是错的 518 + client.water(1) # 水分已满,再浇就是错的 519 | self.assertEqual(ctx.exception.code, "already_watered") -------------------------------------------------------------------------------- 541 | time.sleep(0.1) - self.assertEqual(watcher.state.plots[0].crop, "carrot", - "实时通道应该把另一方的操作推过来") 542 + self.assertEqual( 543 + watcher.state.plots[0].crop, "carrot", "实时通道应该把另一方的操作推过来" 544 + ) 545 | self.assertEqual(watcher.mode, "ws") | unformatted: File would be reformatted --> tools\preview_tui.py:30:30 | 29 | from farm.engine import FarmEngine # noqa: E402 - from farm.tui import TuiApp # noqa: E402 30 + from farm.tui import TuiApp # noqa: E402 31 | -------------------------------------------------------------------------------- 39 | client = RemoteFarm(endpoint=url, farm=farm, token=token, realtime=False) - app = TuiApp(client, use_color=True, autosave=False, - server_label=f"{client.farm} @ {client.host}") 40 + app = TuiApp( 41 + client, use_color=True, autosave=False, server_label=f"{client.farm} @ {client.host}" 42 + ) 43 | return client, app -------------------------------------------------------------------------------- 103 | parser.add_argument("--color", action="store_true", help="保留 ANSI 颜色") - parser.add_argument("--url", default=None, - help="连到真实服务器渲染(例如 http://127.0.0.1:8765)") 104 + parser.add_argument( 105 + "--url", default=None, help="连到真实服务器渲染(例如 http://127.0.0.1:8765)" 106 + ) 107 | parser.add_argument("--farm", default="default", help="--url 模式下要看的牧场") -------------------------------------------------------------------------------- 116 | state = engine.snapshot() - print(f"# 来自真实服务器 {args.url} · 牧场 {args.farm} · 第 {state.get('day')} 天 · " - f"金币 {state.get('gold')} · 在线 {state.get('online')}\n") 117 + print( 118 + f"# 来自真实服务器 {args.url} · 牧场 {args.farm} · 第 {state.get('day')} 天 · " 119 + f"金币 {state.get('gold')} · 在线 {state.get('online')}\n" 120 + ) 121 | else: | unformatted: File would be reformatted --> tools\ws_demo.py:41:15 | 40 | state = welcome.get("state", {}) - print(f"← welcome:牧场 {welcome['farm']} · 第 {state.get('day')} 天 · " - f"金币 {state.get('gold')} · 候选动作 {len(welcome.get('actions', []))} 个") 41 + print( 42 + f"← welcome:牧场 {welcome['farm']} · 第 {state.get('day')} 天 · " 43 + f"金币 {state.get('gold')} · 候选动作 {len(welcome.get('actions', []))} 个" 44 + ) 45 | -------------------------------------------------------------------------------- 65 | if kind == "result": - print(f"← result(id={message.get('id')}):{message.get('message')} " - f"[通过 WebSocket 提交的动作,ok={message.get('ok')}]") 66 + print( 67 + f"← result(id={message.get('id')}):{message.get('message')} " 68 + f"[通过 WebSocket 提交的动作,ok={message.get('ok')}]" 69 + ) 70 | elif kind == "state": 71 | snapshot = message.get("state", {}) - print(f"← state(seq={message.get('seq')}):第 {snapshot.get('day')} 天 · " - f"金币 {snapshot.get('gold')} · 行动点 {snapshot.get('ap')} · " - f"在线 {message.get('online')}") 72 + print( 73 + f"← state(seq={message.get('seq')}):第 {snapshot.get('day')} 天 · " 74 + f"金币 {snapshot.get('gold')} · 行动点 {snapshot.get('ap')} · " 75 + f"在线 {message.get('online')}" 76 + ) 77 | elif kind == "day": | 23 files would be reformatted, 4 files already formatted