Files
SimpleFarm/_ruff1.txt
T
snowy-yang a7725f8812 chore: 工程化改造前的基线快照
在引入 uv 依赖管理、ruff 静态检查与格式化、pyright 类型检查之前,
先把当前可正常运行的状态固定下来,作为后续大规模自动改动的回滚点。

此时状态:97 项测试全绿,HTTP + WebSocket 多牧场服务器可用。
2026-09-18 21:12:40 +08:00

2079 lines
105 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
=== 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