在引入 uv 依赖管理、ruff 静态检查与格式化、pyright 类型检查之前, 先把当前可正常运行的状态固定下来,作为后续大规模自动改动的回滚点。 此时状态:97 项测试全绿,HTTP + WebSocket 多牧场服务器可用。
2079 lines
105 KiB
Plaintext
2079 lines
105 KiB
Plaintext
=== 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
|