22
33from __future__ import annotations
44
5- import re
65import time
76from dataclasses import dataclass
87from typing import TYPE_CHECKING
3231if TYPE_CHECKING :
3332 import numpy as np
3433
34+ from autowsgr .vision import EasyOCREngine
35+
3536
3637_log = get_logger ('ui' )
3738
39+ LOOT_MAX = 50
40+ """战利品 (胖次) 上限, 固定值。"""
41+
42+ SHIP_MAX = 500
43+ """舰船上限, 固定值。"""
44+
3845
3946# ── 数据类 ──
4047
@@ -46,49 +53,95 @@ class LootShipCount:
4653 Attributes
4754 ----------
4855 loot:
49- 战利品 (胖次) 已获取数量, 识别失败时为 ``None``。
56+ 战利品 (胖次) 已获取数量, 识别失败时为 ``None``。
5057 loot_max:
51- 战利品上限 (通常 50),识别失败时为 ``None`` 。
58+ 战利品上限, 固定 50。
5259 ship:
53- 舰船已获取数量, 识别失败时为 ``None``。
60+ 舰船已获取数量, 识别失败时为 ``None``。
5461 ship_max:
55- 舰船上限 (通常 500),识别失败时为 ``None`` 。
62+ 舰船上限, 固定 500。
5663 """
5764
5865 loot : int | None = None
59- loot_max : int | None = None
66+ loot_max : int = LOOT_MAX
6067 ship : int | None = None
61- ship_max : int | None = None
68+ ship_max : int = SHIP_MAX
6269
6370
64- # ── 内部工具 ──
71+ # ── 独立识别函数 ──
6572
66- _FRACTION_RE = re . compile ( r'(\d+)\s*[/|]\s*(\d+)' )
67- """匹配 "X/Y" 格式的正则 (兼容 OCR 把 / 识别为 | 的情况) 。"""
73+ _OCR_ALLOWLIST = '0123456789/|'
74+ """OCR 字符白名单。包含 ``/`` 和 ``|`` 使 OCR 正确识别斜线而非误读为 ``1`` 。"""
6875
69- _KNOWN_DENOMS = (500 , 50 )
70- """已知分母值, 用于 OCR 将 ``/`` 误识为 ``1`` 时的回退解析 (长的优先匹配)。"""
7176
77+ def _parse_numerator (text : str , max_val : int ) -> int :
78+ """从 ``"X/Y"`` 格式的 OCR 文本中提取分子 (``/`` 前的数字)。
7279
73- def _parse_fraction (text : str ) -> tuple [int , int ] | None :
74- """解析 ``"123/500"`` 格式文本, 返回 ``(numerator, denominator)``。"""
75- m = _FRACTION_RE .search (text )
76- if m :
77- return int (m .group (1 )), int (m .group (2 ))
78-
79- # 回退: OCR 有时将 '/' 误识为 '1', 导致纯数字串如 "17150" (实为 "17/50")。
80- # 尝试去掉已知分母前的多余 '1' 来还原。
80+ - 优先按 ``/`` 或 ``|`` 分割取第一段。
81+ - 回退: 若无分隔符, 按已知分母剥离末尾后缀。
82+ """
83+ # 优先: 按 "/" 或 "|" 分割
84+ for sep in ('/' , '|' ):
85+ if sep in text :
86+ left = text .split (sep , 1 )[0 ]
87+ digits = '' .join (c for c in left if c .isdigit ())
88+ if digits :
89+ return int (digits )
90+ raise ValueError (f'分子部分无数字: "{ text } "' )
91+
92+ # 回退: OCR 偶尔把 "/" 识别为 "1", 导致纯数字串如 "17150"。
93+ # 已知分母为 max_val, 则后缀为 "1" + str(max_val)。
8194 digits = '' .join (c for c in text if c .isdigit ())
82- if digits :
83- for denom in _KNOWN_DENOMS :
84- suffix = '1' + str (denom )
85- if digits .endswith (suffix ) and len (digits ) > len (suffix ):
86- numerator = int (digits [: - len (suffix )])
87- if numerator >= 0 and numerator <= denom :
88- return numerator , denom
95+ if not digits :
96+ raise ValueError (f'文本中无数字: "{ text } "' )
97+ suffix = '1' + str (max_val )
98+ if digits .endswith (suffix ) and len (digits ) > len (suffix ):
99+ return int (digits [: - len (suffix )])
100+ # 无 "1" 前缀: 可能分母直接拼接
101+ denom_str = str (max_val )
102+ if digits .endswith (denom_str ) and len (digits ) > len (denom_str ):
103+ return int (digits [: - len (denom_str )])
89104 return None
90105
91106
107+ def recognize_loot_count (screen : np .ndarray , ocr : EasyOCREngine ) -> int | None :
108+ """识别出征面板战利品 (胖次) 已获取数量。
109+
110+ OCR ``X/50`` 区域并提取 ``/`` 前的数字, 上限固定为 50。
111+ """
112+ img = PixelChecker .crop (screen , * LOOT_COUNT_CROP )
113+ text = ocr .recognize_single (img , allowlist = _OCR_ALLOWLIST ).text .strip ()
114+ if not text :
115+ _log .warning ('[UI] 战利品数量 OCR 无结果' )
116+ return None
117+ count = _parse_numerator (text , LOOT_MAX )
118+ if count > 50 and str (count ).endswith ('1' ):
119+ count = int (str (count )[:- 1 ]) # 可能 OCR 把 "/50" 识别成 "150"
120+ if count is not None :
121+ _log .info ('[UI] 战利品数量: {}/{}' , count , LOOT_MAX )
122+ else :
123+ _log .warning ("[UI] 战利品数量 OCR 解析失败: '{}'" , text )
124+ return count
125+
126+
127+ def recognize_ship_count (screen : np .ndarray , ocr : EasyOCREngine ) -> int | None :
128+ """识别出征面板舰船已获取数量。
129+
130+ OCR ``X/500`` 区域并提取 ``/`` 前的数字, 上限固定为 500。
131+ """
132+ img = PixelChecker .crop (screen , * SHIP_COUNT_CROP )
133+ text = ocr .recognize_single (img , allowlist = _OCR_ALLOWLIST ).text .strip ()
134+ if not text :
135+ _log .warning ('[UI] 舰船数量 OCR 无结果' )
136+ return None
137+ count = _parse_numerator (text , SHIP_MAX )
138+ if count is not None :
139+ _log .info ('[UI] 舰船数量: {}/{}' , count , SHIP_MAX )
140+ else :
141+ _log .warning ("[UI] 舰船数量 OCR 解析失败: '{}'" , text )
142+ return count
143+
144+
92145class SortiePanelMixin (BaseMapPage ):
93146 """Mixin: 出征面板操作 — 选择章节 / 地图节点 / 进入出征准备。"""
94147
@@ -206,7 +259,7 @@ def get_loot_and_ship_count(
206259 ) -> LootShipCount :
207260 """读取出征面板右上角的已获取舰船/战利品数量。
208261
209- 通过 OCR 识别 ``X/Y`` 格式的数字 。需要先处于出征面板。
262+ 通过 OCR 识别数字 。需要先处于出征面板。
210263
211264 Parameters
212265 ----------
@@ -218,35 +271,10 @@ def get_loot_and_ship_count(
218271 if screen is None :
219272 screen = self ._ctrl .screenshot ()
220273
221- loot = loot_max = ship = ship_max = None
222-
223- # -- 战利品 (胖次) --
224- loot_img = PixelChecker .crop (screen , * LOOT_COUNT_CROP )
225- loot_text = self ._ocr .recognize_single (loot_img , allowlist = '0123456789/|' ).text .strip ()
226- if loot_text :
227- parsed = _parse_fraction (loot_text )
228- if parsed :
229- loot , loot_max = parsed
230- _log .info ('[UI] 战利品数量: {}/{}' , loot , loot_max )
231- else :
232- _log .warning ("[UI] 战利品数量 OCR 解析失败: '{}'" , loot_text )
233- else :
234- _log .warning ('[UI] 战利品数量 OCR 无结果' )
235-
236- # -- 舰船 --
237- ship_img = PixelChecker .crop (screen , * SHIP_COUNT_CROP )
238- ship_text = self ._ocr .recognize_single (ship_img , allowlist = '0123456789/|' ).text .strip ()
239- if ship_text :
240- parsed = _parse_fraction (ship_text )
241- if parsed :
242- ship , ship_max = parsed
243- _log .info ('[UI] 舰船数量: {}/{}' , ship , ship_max )
244- else :
245- _log .warning ("[UI] 舰船数量 OCR 解析失败: '{}'" , ship_text )
246- else :
247- _log .warning ('[UI] 舰船数量 OCR 无结果' )
274+ loot = recognize_loot_count (screen , self ._ocr )
275+ ship = recognize_ship_count (screen , self ._ocr )
248276
249- return LootShipCount (loot = loot , loot_max = loot_max , ship = ship , ship_max = ship_max )
277+ return LootShipCount (loot = loot , ship = ship )
250278
251279 # ═══════════════════════════════════════════════════════════════════════
252280 # 进入出征
0 commit comments