Skip to content

Commit 161b844

Browse files
yltxpre-commit-ci[bot]veadex
authored
fix(fleet): support selector rules and disambiguate same-name ships (#420)
* fix(fleet): support fleet_rules selectors and disambiguate same-name ships * chore(release): bump autowsgr to 2.1.9.post5 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix(review): address Copilot feedback for fleet rules * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix(review): address new Copilot comments on PR #419 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: X-vedeax <65024689+veadex@users.noreply.github.com>
1 parent 3ab8c6e commit 161b844

8 files changed

Lines changed: 729 additions & 67 deletions

File tree

autowsgr/__init__.py

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

3-
__version__ = '2.1.9.post4'
3+
__version__ = '2.1.9.post5'

autowsgr/ops/event_fight.py

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from __future__ import annotations
1414

1515
import time
16-
from typing import TYPE_CHECKING, Literal
16+
from typing import TYPE_CHECKING, Any, Literal
1717

1818
from autowsgr.combat import CombatMode, CombatPlan, CombatResult
1919
from autowsgr.combat.engine import run_combat
@@ -70,13 +70,15 @@ def __init__(
7070
event_name: str | None = None,
7171
fleet_id: int | None = None,
7272
fleet: list[str] | None = None,
73+
fleet_rules: list[Any] | None = None,
7374
) -> None:
7475
self._ctx = ctx
7576
self._ctrl = ctx.ctrl
7677
self._plan = plan
7778
self._entrance = entrance
7879
self._fleet_id = fleet_id if fleet_id is not None else (plan.fleet_id or 1)
7980
self._fleet = fleet if fleet is not None else plan.fleet
81+
self._fleet_rules = fleet_rules
8082
self._skip_check = False # 首次执行时检查难度和节点,后续重复执行时跳过检查以节省时间
8183

8284
# 推导 map_code
@@ -111,6 +113,35 @@ def __init__(
111113
self._results: list[CombatResult] = []
112114
self._fleet_ships = None
113115

116+
@staticmethod
117+
def _primary_names_from_rules(fleet_rules: list[Any] | None) -> list[str | None] | None:
118+
if not fleet_rules:
119+
return None
120+
121+
def _normalize_name(value: object) -> str | None:
122+
if value is None:
123+
return None
124+
name = str(value).strip()
125+
return name or None
126+
127+
names: list[str | None] = []
128+
for slot in fleet_rules[:6]:
129+
if isinstance(slot, str):
130+
names.append(_normalize_name(slot))
131+
continue
132+
133+
candidates = None
134+
if isinstance(slot, dict):
135+
candidates = slot.get('candidates')
136+
else:
137+
candidates = getattr(slot, 'candidates', None)
138+
139+
if isinstance(candidates, list) and len(candidates) > 0:
140+
names.append(_normalize_name(candidates[0]))
141+
continue
142+
names.append(None)
143+
return names
144+
114145
# ── 公共接口 ──
115146

116147
def run(self) -> CombatResult:
@@ -217,11 +248,20 @@ def _prepare_for_battle(self) -> list[ShipDamageState]:
217248
page.select_fleet(self._fleet_id)
218249
time.sleep(0.5)
219250

220-
# 换船 (如果指定了舰船列表)
221-
if self._plan.fleet is not None:
251+
resolved_ship_names: list[str | None] | None = None
252+
253+
# 换船 (若提供了规则则优先按规则执行)
254+
if self._fleet_rules is not None:
222255
page.change_fleet(
223256
self._fleet_id,
224-
self._plan.fleet,
257+
self._fleet_rules,
258+
)
259+
time.sleep(0.5)
260+
resolved_ship_names = page.detect_fleet()
261+
elif self._fleet is not None:
262+
page.change_fleet(
263+
self._fleet_id,
264+
self._fleet,
225265
)
226266
time.sleep(0.5)
227267

@@ -244,7 +284,14 @@ def _prepare_for_battle(self) -> list[ShipDamageState]:
244284
# 检测战前舰队信息 (血量 + 等级)
245285
fleet_info = page.detect_fleet_info()
246286
ship_stats = [fleet_info.ship_damage.get(i, ShipDamageState.NORMAL) for i in range(6)]
247-
self._fleet_ships = fleet_info.to_ships(self._plan.fleet)
287+
ship_names = resolved_ship_names
288+
if ship_names is None:
289+
ship_names = (
290+
self._primary_names_from_rules(self._fleet_rules)
291+
if self._fleet_rules is not None
292+
else self._fleet
293+
)
294+
self._fleet_ships = fleet_info.to_ships(ship_names)
248295

249296
# 出征
250297
page.start_battle()
@@ -302,6 +349,7 @@ def run_event_fight(
302349
gap: float = 0.0,
303350
fleet_id: int | None = None,
304351
fleet: list[str] | None = None,
352+
fleet_rules: list[Any] | None = None,
305353
) -> list[CombatResult]:
306354
"""执行活动战的便捷函数。
307355
@@ -331,6 +379,7 @@ def run_event_fight(
331379
entrance=entrance,
332380
fleet_id=fleet_id,
333381
fleet=fleet,
382+
fleet_rules=fleet_rules,
334383
)
335384
return runner.run_for_times(times, gap=gap)
336385

@@ -344,6 +393,7 @@ def run_event_fight_from_yaml(
344393
times: int = 1,
345394
fleet_id: int | None = None,
346395
fleet: list[str] | None = None,
396+
fleet_rules: list[Any] | None = None,
347397
) -> list[CombatResult]:
348398
"""从 YAML 文件加载计划并执行活动战。
349399
@@ -386,4 +436,5 @@ def run_event_fight_from_yaml(
386436
times=times,
387437
fleet_id=fleet_id,
388438
fleet=fleet,
439+
fleet_rules=fleet_rules,
389440
)

autowsgr/ops/normal_fight.py

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from __future__ import annotations
99

1010
import time
11-
from typing import TYPE_CHECKING
11+
from typing import TYPE_CHECKING, Any
1212

1313
from autowsgr.combat import CombatMode, CombatPlan, CombatResult
1414
from autowsgr.combat.engine import run_combat
@@ -37,12 +37,14 @@ def __init__(
3737
plan: CombatPlan,
3838
fleet_id: int | None = None,
3939
fleet: list[str] | None = None,
40+
fleet_rules: list[Any] | None = None,
4041
) -> None:
4142
self._ctx = ctx
4243
self._ctrl = ctx.ctrl
4344
self._plan = plan
4445
self._fleet_id = fleet_id if fleet_id is not None else plan.fleet_id
4546
self._fleet = fleet if fleet is not None else plan.fleet
47+
self._fleet_rules = fleet_rules
4648

4749
# 从 config 读取拆船配置
4850
self._dock_full_destroy = ctx.config.dock_full_destroy
@@ -61,6 +63,35 @@ def __init__(
6163
self._ship_acquired_count: int | None = None
6264
self._fleet_ships: list[Ship] | None = None
6365

66+
@staticmethod
67+
def _primary_names_from_rules(fleet_rules: list[Any] | None) -> list[str | None] | None:
68+
if not fleet_rules:
69+
return None
70+
71+
def _normalize_name(value: object) -> str | None:
72+
if value is None:
73+
return None
74+
name = str(value).strip()
75+
return name or None
76+
77+
names: list[str | None] = []
78+
for slot in fleet_rules[:6]:
79+
if isinstance(slot, str):
80+
names.append(_normalize_name(slot))
81+
continue
82+
83+
candidates = None
84+
if isinstance(slot, dict):
85+
candidates = slot.get('candidates')
86+
else:
87+
candidates = getattr(slot, 'candidates', None)
88+
89+
if isinstance(candidates, list) and len(candidates) > 0:
90+
names.append(_normalize_name(candidates[0]))
91+
continue
92+
names.append(None)
93+
return names
94+
6495
# ── 公共接口 ──
6596

6697
def run(self) -> CombatResult:
@@ -292,8 +323,17 @@ def _prepare_for_battle(self) -> list[ShipDamageState]:
292323
page.select_fleet(self._fleet_id)
293324
time.sleep(0.5)
294325

295-
# 换船 (如果指定了舰船列表)
296-
if self._fleet is not None:
326+
resolved_ship_names: list[str | None] | None = None
327+
328+
# 换船 (若提供了规则则优先按规则执行)
329+
if self._fleet_rules is not None:
330+
page.change_fleet(
331+
self._fleet_id,
332+
self._fleet_rules,
333+
)
334+
time.sleep(0.5)
335+
resolved_ship_names = page.detect_fleet()
336+
elif self._fleet is not None:
297337
page.change_fleet(
298338
self._fleet_id,
299339
self._fleet,
@@ -322,7 +362,14 @@ def _prepare_for_battle(self) -> list[ShipDamageState]:
322362
if ShipDamageState.SEVERE in ship_stats:
323363
_log.error('[OPS] 出征前检测到大破舰船,退出程序')
324364
raise ActionFailedError('出征前检测到大破舰船,退出程序')
325-
self._fleet_ships = fleet_info.to_ships(self._fleet)
365+
ship_names = resolved_ship_names
366+
if ship_names is None:
367+
ship_names = (
368+
self._primary_names_from_rules(self._fleet_rules)
369+
if self._fleet_rules is not None
370+
else self._fleet
371+
)
372+
self._fleet_ships = fleet_info.to_ships(ship_names)
326373

327374
# 出征
328375
page.start_battle()
@@ -394,13 +441,15 @@ def run_normal_fight(
394441
gap: float = 0.0,
395442
fleet_id: int | None = None,
396443
fleet: list[str] | None = None,
444+
fleet_rules: list[Any] | None = None,
397445
) -> list[CombatResult]:
398446
"""执行常规战的便捷函数。"""
399447
runner = NormalFightRunner(
400448
ctx,
401449
plan,
402450
fleet_id=fleet_id,
403451
fleet=fleet,
452+
fleet_rules=fleet_rules,
404453
)
405454
return runner.run_for_times(times, gap=gap)
406455

@@ -412,6 +461,7 @@ def run_normal_fight_from_yaml(
412461
times: int = 1,
413462
fleet_id: int | None = None,
414463
fleet: list[str] | None = None,
464+
fleet_rules: list[Any] | None = None,
415465
) -> list[CombatResult]:
416466
"""从 YAML 文件加载计划并执行常规战。
417467
@@ -422,4 +472,11 @@ def run_normal_fight_from_yaml(
422472
包数据目录中查找,可省略 ``.yaml`` 后缀。
423473
"""
424474
plan = get_normal_fight_plan(yaml_path)
425-
return run_normal_fight(ctx, plan, times=times, fleet_id=fleet_id, fleet=fleet)
475+
return run_normal_fight(
476+
ctx,
477+
plan,
478+
times=times,
479+
fleet_id=fleet_id,
480+
fleet=fleet,
481+
fleet_rules=fleet_rules,
482+
)

autowsgr/server/routes/task.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ def executor(task_info: Any) -> list[dict[str, Any]]:
111111
request_plan = request.plan
112112
override_fleet_id = request_plan.fleet_id if request_plan is not None else None
113113
override_fleet = request_plan.fleet if request_plan is not None else None
114+
override_fleet_rules = request_plan.fleet_rules if request_plan is not None else None
114115

115116
for i in range(request.times):
116117
if task_manager.should_stop():
@@ -126,6 +127,7 @@ def executor(task_info: Any) -> list[dict[str, Any]]:
126127
times=1,
127128
fleet_id=override_fleet_id,
128129
fleet=override_fleet,
130+
fleet_rules=override_fleet_rules,
129131
)[0]
130132
results.append(convert_combat_result(result, i + 1))
131133
task_manager.add_result(results[-1])
@@ -165,6 +167,7 @@ def executor(task_info: Any) -> list[dict[str, Any]]:
165167

166168
request_plan = request.plan
167169
override_fleet = request_plan.fleet if request_plan is not None else None
170+
override_fleet_rules = request_plan.fleet_rules if request_plan is not None else None
168171
# 优先级: 顶层 fleet_id > plan 覆盖 fleet_id > YAML 内 fleet_id
169172
if request.fleet_id is not None:
170173
fleet_id = request.fleet_id
@@ -187,6 +190,7 @@ def executor(task_info: Any) -> list[dict[str, Any]]:
187190
times=1,
188191
fleet_id=fleet_id,
189192
fleet=override_fleet,
193+
fleet_rules=override_fleet_rules,
190194
)[0]
191195
results.append(convert_combat_result(result, i + 1))
192196
task_manager.add_result(results[-1])

autowsgr/server/schemas.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from enum import StrEnum
66
from typing import Any, Literal
77

8-
from pydantic import BaseModel, Field
8+
from pydantic import BaseModel, Field, field_validator, model_validator
99

1010

1111
# ═══════════════════════════════════════════════════════════════════════════════
@@ -66,6 +66,35 @@ class NodeDecisionRequest(BaseModel):
6666
model_config = {'extra': 'forbid'}
6767

6868

69+
class FleetRuleRequest(BaseModel):
70+
"""编队槽位候选规则。"""
71+
72+
candidates: list[str] = Field(min_length=1, description='候选舰船名(按优先级)')
73+
search_name: str | None = Field(default=None, description='选船搜索关键词(用于同名舰船区分)')
74+
min_level: int | None = Field(default=None, ge=1, description='等级下限(含)')
75+
max_level: int | None = Field(default=None, ge=1, description='等级上限(含)')
76+
77+
@field_validator('candidates')
78+
@classmethod
79+
def _validate_candidates(cls, value: list[str]) -> list[str]:
80+
normalized = [name.strip() for name in value if name and name.strip()]
81+
if len(normalized) == 0:
82+
raise ValueError('candidates 不能为空')
83+
return normalized
84+
85+
@model_validator(mode='after')
86+
def _validate_level_range(self) -> FleetRuleRequest:
87+
if (
88+
self.min_level is not None
89+
and self.max_level is not None
90+
and self.max_level < self.min_level
91+
):
92+
raise ValueError('max_level 必须大于或等于 min_level')
93+
return self
94+
95+
model_config = {'extra': 'forbid'}
96+
97+
6998
class CombatPlanRequest(BaseModel):
7099
"""作战计划请求体。"""
71100

@@ -75,6 +104,10 @@ class CombatPlanRequest(BaseModel):
75104
map: int | str = Field(default=1, description='地图号')
76105
fleet_id: int = Field(default=1, ge=1, le=6, description='舰队编号')
77106
fleet: list[str] | None = Field(default=None, description='舰队成员')
107+
fleet_rules: list[str | FleetRuleRequest] | None = Field(
108+
default=None,
109+
description='舰队槽位规则(字符串或候选规则)',
110+
)
78111
repair_mode: list[int] = Field(
79112
default_factory=lambda: [2, 2, 2, 2, 2, 2],
80113
description='修理策略 (6个位置)',

0 commit comments

Comments
 (0)