Skip to content

Commit f8d6986

Browse files
committed
enhancement: 改进战斗过程稳定性, 优化导航策略
1 parent ff09ce5 commit f8d6986

9 files changed

Lines changed: 322 additions & 44 deletions

File tree

autowsgr/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""AutoWSGR — 战舰少女R 自动化框架 (v2)"""
22

3-
__version__ = "2.0.1r2"
3+
__version__ = "2.0.2"

autowsgr/combat/recognizer.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@
1313
from autowsgr.infra import get_logger
1414
from autowsgr.context import GameContext
1515
from autowsgr.image_resources import TemplateKey
16-
from autowsgr.vision import CompositePixelSignature, ImageChecker, PixelChecker, PixelSignature
16+
from autowsgr.vision import (
17+
CompositePixelSignature,
18+
ImageChecker,
19+
MatchStrategy,
20+
PixelChecker,
21+
PixelRule,
22+
PixelSignature,
23+
)
1724

1825
_log = get_logger("combat.recognition")
1926

@@ -57,6 +64,20 @@ def _get_event_map_signatures() -> CompositePixelSignature:
5764
)
5865

5966

67+
_CHOOSE_FORMATION_SIGNATURE = PixelSignature(
68+
name="choose_formation",
69+
strategy=MatchStrategy.ALL,
70+
rules=[
71+
PixelRule.of(0.9413, 0.2011, (227, 227, 227), tolerance=30.0),
72+
PixelRule.of(0.9375, 0.3644, (227, 227, 227), tolerance=30.0),
73+
PixelRule.of(0.9569, 0.5278, (227, 227, 227), tolerance=30.0),
74+
PixelRule.of(0.5050, 0.5678, (24, 101, 181), tolerance=30.0),
75+
PixelRule.of(0.5019, 0.9478, (26, 103, 183), tolerance=30.0),
76+
PixelRule.of(0.8531, 0.9367, (32, 134, 219), tolerance=30.0),
77+
],
78+
)
79+
80+
6081
PHASE_SIGNATURES: dict[CombatPhase, PhaseSignature] = {
6182
CombatPhase.PROCEED: PhaseSignature(
6283
template_key=TemplateKey.PROCEED,
@@ -80,8 +101,9 @@ def _get_event_map_signatures() -> CompositePixelSignature:
80101
default_timeout=22.5,
81102
),
82103
CombatPhase.FORMATION: PhaseSignature(
83-
template_key=TemplateKey.FORMATION,
104+
template_key=None,
84105
default_timeout=22.5,
106+
pixel_signature=_CHOOSE_FORMATION_SIGNATURE,
85107
),
86108
CombatPhase.MISSILE_ANIMATION: PhaseSignature(
87109
template_key=TemplateKey.MISSILE_ANIMATION,

autowsgr/ops/navigate.py

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -67,41 +67,50 @@ def identify_current_page(ctx: GameContext) -> str | None:
6767
def _goto_page(ctx: GameContext, target: str) -> None:
6868
"""从当前页面导航到目标页面。
6969
70+
采用逐步重规划策略 (Step-by-Step Re-planning):
7071
1. 识别当前页面
7172
2. BFS 查找路径
72-
3. 逐边调用 ``edge.action(ctx)``(截图验证由页面控制器内部完成)
73+
3. 执行路径的第一步
74+
4. 循环回到 1,直到到达目标
75+
76+
这允许处理不确定的导航动作 (如: build -> sidebar | main)。
7377
7478
Raises
7579
------
7680
NavigationError
77-
无法识别当前页面或找不到路径
81+
无法识别当前页面、找不到路径或步数超限
7882
"""
79-
current = identify_current_page(ctx)
80-
if current is None:
81-
raise NavigationError(
82-
f"无法识别当前页面,无法导航到 '{target}'"
83-
)
84-
85-
if current == target:
86-
_log.info("[OPS] 已在目标页面: {}", target)
87-
return
88-
89-
path = find_path(current, target)
90-
if path is None:
91-
raise NavigationError(
92-
f"无法找到从 '{current}' 到 '{target}' 的路径"
93-
)
94-
95-
_log.debug("[OPS] 导航: {} → {} (共 {} 步)", current, target, len(path))
96-
97-
for i, edge in enumerate(path):
83+
MAX_STEPS = 20
84+
85+
for step in range(MAX_STEPS):
86+
# 1. 识别
87+
current = identify_current_page(ctx)
88+
if current is None:
89+
raise NavigationError(f"无法识别当前页面,导航中止 (目标: {target})")
90+
91+
# 2. 检查
92+
if current == target:
93+
_log.info("[OPS] 已在目标页面: {}", target)
94+
return
95+
96+
# 3. 寻路
97+
path = find_path(current, target)
98+
if path is None:
99+
raise NavigationError(f"无法找到从 '{current}' 到 '{target}' 的路径")
100+
101+
if not path: # Should be covered by current == target, but safe check
102+
_log.info("[OPS] 已在目标页面: {}", target)
103+
return
104+
105+
# 4. 执行一步
106+
edge = path[0]
98107
_log.debug(
99-
"[OPS] 步骤 {}/{}: {} → {} ({})",
100-
i + 1, len(path), edge.source, edge.target, edge.description,
108+
"[OPS] 步骤 {} (总限 {}): {} → {} ({})",
109+
step + 1, MAX_STEPS, edge.source, edge.target, edge.description,
101110
)
102111
edge.action(ctx)
103-
104-
_log.info("[OPS] 已到达: {}", target)
112+
113+
raise NavigationError(f"导航步数超限 ({MAX_STEPS}),目标: {target}")
105114

106115

107116
def goto_page(ctx: GameContext, target: str) -> None:

autowsgr/ops/normal_fight.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def _enter_fight(self) -> None:
141141

142142
def _prepare_for_battle(self) -> list[ShipDamageState]:
143143
"""出征准备: 舰队选择、修理、检测血量。
144-
144+
145145
Returns
146146
-------
147147
list[int]

autowsgr/ops/retry_plan.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Retry Mechanism Redesign Plan
2+
3+
## Objective
4+
Implement a robust retry mechanism for operations in `autowsgr/ops` that involve user interaction (clicking) followed by state verification. The design must allow applying a decorator directly to the operation methods, avoiding nested function definitions.
5+
6+
## Design
7+
8+
### 1. `retry_action` Decorator (`autowsgr/ops/retry.py`)
9+
10+
The decorator will be enhanced to support:
11+
- **Exception Handling**: Retrying when specific exceptions are raised.
12+
- **No explicit validation callback**: The decorated function itself is expected to raise an exception if the operation fails (e.g., using `wait_leave_page` inside the function).
13+
14+
```python
15+
# autowsgr/ops/retry.py
16+
17+
import functools
18+
import time
19+
from typing import Callable, TypeVar, Any, Sequence
20+
from autowsgr.infra.logger import get_logger
21+
22+
_log = get_logger("ops.retry")
23+
24+
T = TypeVar("T")
25+
26+
def retry_action(
27+
retries: int = 1,
28+
delay: float = 1.0,
29+
exceptions: Sequence[type[Exception]] = (Exception,),
30+
) -> Callable[[Callable[..., T]], Callable[..., T]]:
31+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
32+
@functools.wraps(func)
33+
def wrapper(*args: Any, **kwargs: Any) -> T:
34+
for i in range(retries + 1):
35+
try:
36+
return func(*args, **kwargs)
37+
except exceptions as e:
38+
if i == retries:
39+
_log.error(f"Operation {func.__name__} failed after {retries} retries: {e}")
40+
raise e
41+
_log.warning(f"Operation {func.__name__} failed: {e}. Retrying ({i+1}/{retries})...")
42+
time.sleep(delay)
43+
return func(*args, **kwargs)
44+
return wrapper
45+
return decorator
46+
```
47+
48+
### 2. Implementation Pattern
49+
50+
For each target operation, we will define a dedicated method (if not already present) that performs the atomic action: **Action + Verification**. This method will be decorated with `@retry_action`.
51+
52+
#### Pattern Example
53+
54+
```python
55+
@retry_action(retries=1, delay=1.0, exceptions=(NavigationError,))
56+
def _start_battle(self, page: BattlePreparationPage) -> None:
57+
"""Atomic operation: Click start and verify page transition."""
58+
page.start_battle()
59+
# Verification raises NavigationError if it fails
60+
wait_leave_page(
61+
self._ctrl,
62+
checker=BaseBattlePreparation.is_current_page,
63+
timeout=2.0,
64+
source=PageName.BATTLE_PREP,
65+
target="combat"
66+
)
67+
```
68+
69+
## Proposed Changes
70+
71+
### 1. `autowsgr/ops/normal_fight.py` (Normal Fight)
72+
- **Class**: `NormalFightRunner`
73+
- **New Method**: `_start_battle(self, page)` decorated with `@retry_action`.
74+
- **Usage**: Call `self._start_battle(page)` inside `_prepare_for_battle`.
75+
76+
### 2. `autowsgr/ops/exercise.py` (Exercise)
77+
- **Class**: `ExerciseRunner`
78+
- **New Method**: `_start_battle(self, page)` decorated with `@retry_action`.
79+
- **Usage**: Call `self._start_battle(page)` inside `_prepare_for_battle`.
80+
81+
### 3. `autowsgr/ops/event_fight.py` (Event Fight)
82+
- **Class**: `EventFightRunner`
83+
- **New Method**: `_start_battle(self, page)` decorated with `@retry_action`.
84+
- **Usage**: Call `self._start_battle(page)` inside `_prepare_for_battle`.
85+
86+
### 4. `autowsgr/ops/decisive/handlers.py` (Decisive Battle)
87+
- **Class**: `DecisivePhaseHandlers`
88+
- **New Method**: `_start_battle(self, page)` decorated with `@retry_action`.
89+
- **Usage**: Call `self._start_battle(page)` inside `_handle_prepare_combat`.
90+
91+
### 5. `autowsgr/ops/startup.py` (Game Startup)
92+
- **Function**: `_enter_game(ctrl)` (New helper function)
93+
- **Decorator**: Apply `@retry_action` to `_enter_game`.
94+
- **Usage**:
95+
```python
96+
@retry_action(retries=1, delay=1.0, exceptions=(TimeoutError,))
97+
def _enter_game(ctrl: AndroidController) -> None:
98+
StartScreenPage(ctrl).click_enter()
99+
if not wait_for_game_ui(ctrl, timeout=30.0):
100+
raise TimeoutError("Timeout waiting for game UI")
101+
102+
# In start_game:
103+
if StartScreenPage.is_current_page(ctrl.screenshot()):
104+
_enter_game(ctrl)
105+
```
106+
*Note: Since `startup.py` uses standalone functions, we can define the helper inside `start_game` or at module level. Defining it at module level (or as a private helper) is cleaner.*
107+
108+
## Verification Strategy
109+
- The decorator relies on the wrapped function raising an exception.
110+
- `wait_leave_page` raises `NavigationError`.
111+
- `wait_for_game_ui` returns `bool`, so we must manually raise `TimeoutError` if it returns `False`.

autowsgr/ui/build_page.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,14 +233,19 @@ def go_back(self) -> None:
233233
超时仍在建造页面。
234234
"""
235235
from autowsgr.ui.sidebar_page import SidebarPage
236+
from autowsgr.ui.main_page import MainPage
236237

237238
_log.info("[UI] 建造页面 → 返回侧边栏")
239+
240+
def _checker(screen: np.ndarray) -> bool:
241+
return SidebarPage.is_current_page(screen) or MainPage.is_current_page(screen)
242+
238243
click_and_wait_for_page(
239244
self._ctrl,
240245
click_coord=CLICK_BACK,
241-
checker=SidebarPage.is_current_page,
246+
checker=_checker,
242247
source=PageName.BUILD,
243-
target=PageName.SIDEBAR,
248+
target=f"{PageName.SIDEBAR}/{PageName.MAIN}",
244249
)
245250

246251
# ── 建造操作 ──────────────────────────────────────────────────────────

autowsgr/ui/main_page/constants.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -176,30 +176,30 @@ def ps(self) -> PixelSignature:
176176
name="news_overlay",
177177
strategy=MatchStrategy.ALL,
178178
rules=[
179-
PixelRule.of(0.1437, 0.9065, (254, 255, 255), tolerance=30.0),
180-
PixelRule.of(0.9411, 0.0685, (253, 254, 255), tolerance=30.0),
181-
PixelRule.of(0.9016, 0.0704, (254, 255, 255), tolerance=30.0),
182-
PixelRule.of(0.8599, 0.0685, (254, 255, 255), tolerance=30.0),
183-
PixelRule.of(0.2010, 0.9046, (254, 255, 255), tolerance=30.0),
184-
PixelRule.of(0.8849, 0.0574, (247, 249, 248), tolerance=30.0),
179+
PixelRule.of(0.1437, 0.9065, (254, 255, 255), tolerance=40.0),
180+
PixelRule.of(0.9411, 0.0685, (253, 254, 255), tolerance=40.0),
181+
PixelRule.of(0.9016, 0.0704, (254, 255, 255), tolerance=40.0),
182+
PixelRule.of(0.8599, 0.0685, (254, 255, 255), tolerance=40.0),
183+
PixelRule.of(0.2010, 0.9046, (254, 255, 255), tolerance=40.0),
184+
PixelRule.of(0.8849, 0.0574, (247, 249, 248), tolerance=40.0),
185185
],
186186
),
187187
Sig.NEWS_NOT_SHOW: PixelSignature(
188188
name="news_not_show",
189189
strategy=MatchStrategy.ALL,
190190
rules=[
191-
PixelRule.of(0.0714, 0.9065, (49, 130, 211), tolerance=30.0),
192-
PixelRule.of(0.0620, 0.9130, (52, 130, 205), tolerance=30.0),
191+
PixelRule.of(0.0714, 0.9065, (49, 130, 211), tolerance=40.0),
192+
PixelRule.of(0.0620, 0.9130, (52, 130, 205), tolerance=40.0),
193193
],
194194
),
195195
Sig.SIGN: PixelSignature(
196196
name="sign_overlay",
197197
strategy=MatchStrategy.ALL,
198198
rules=[
199-
PixelRule.of(0.8766, 0.3046, (216, 218, 215), tolerance=30.0),
200-
PixelRule.of(0.1490, 0.3000, (255, 255, 255), tolerance=30.0),
201-
PixelRule.of(0.1786, 0.4019, (250, 255, 255), tolerance=30.0),
202-
PixelRule.of(0.4432, 0.4019, (254, 255, 255), tolerance=30.0),
199+
PixelRule.of(0.8766, 0.3046, (216, 218, 215), tolerance=40.0),
200+
PixelRule.of(0.1490, 0.3000, (255, 255, 255), tolerance=40.0),
201+
PixelRule.of(0.1786, 0.4019, (250, 255, 255), tolerance=40.0),
202+
PixelRule.of(0.4432, 0.4019, (254, 255, 255), tolerance=40.0),
203203
],
204204
),
205205
Sig.BOOKING: PixelSignature(

tools/LLM.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- 通过 Emulator 模块可以连接到模拟器
66
- 提供了 `save_image` 函数来保存调试截图
7+
- **注意**: `save_image` 会自动在文件名后追加时间戳 (如 `debug_123456_789.png`)。在后续使用命令行工具 (如 `copy`) 操作该文件时,**必须**先检查输出日志或使用 `ls` 确认实际生成的文件名,不要直接使用代码中指定的 base name,否则会导致 "File not found" 错误。
78
- 使用 numpy 提供的函数来处理图像
89
- 调试完成后,将用到的测试图片保存到 `test_pkg` 中的有关目录,对被调试的函数建立测试防止后期回归
910

@@ -55,6 +56,15 @@ python tools/debug_screenshot.py --check-page decisive_battle
5556
### 回归测试约定
5657

5758
1. 将调试用到的截图保存到 `test_pkg/<模块名>/` (如 `test_pkg/decisive_ocr/`)
59+
- 复制时请重命名为有意义的名称 (去除时间戳),方便后续维护。
5860
2. 编写 pytest 测试文件 `test_pkg/<模块名>/test_xxx.py`
5961
3. OCR 相关测试使用 `@pytest.fixture(scope="module")` 共享 OCR 引擎实例避免重复初始化
60-
4. 测试应覆盖修复后的正确路径; 可选添加诊断测试记录修复前的错误行为
62+
4. 测试应覆盖修复后的正确路径; 可选添加诊断测试记录修复前的错误行为
63+
64+
### 视觉识别进阶
65+
66+
对于更复杂的视觉识别逻辑,特别是涉及 `PixelRules``ImageMatcher` 的使用,请参阅 [视觉识别系统指南](VisualIdentity.md)
67+
68+
### 补充
69+
70+
1. 调试遇到 bug 尝试并解决后,补充有意义的信息到本文档防止下次再犯。

0 commit comments

Comments
 (0)