Add: Rogue route framework

This commit is contained in:
LmeSzinc 2023-10-01 23:03:57 +08:00
parent f2e99a5db6
commit 76805d6753
9 changed files with 869 additions and 47 deletions

View File

@ -1,31 +1,30 @@
import os
import re
from dataclasses import dataclass, fields
from typing import Iterator
from module.base.code_generator import CodeGenerator
import numpy as np
from tqdm import tqdm
@dataclass
class RouteData:
name: str
route: str
plane: str
floor: str = 'F1'
position: tuple = None
from module.base.code_generator import CodeGenerator, MarkdownGenerator
from module.base.decorator import cached_property
from module.base.utils import SelectedGrids, load_image
from module.config.utils import iter_folder
from tasks.map.route.model import RouteModel
from tasks.rogue.route.model import RogueRouteListModel, RogueRouteModel, RogueWaypointListModel, RogueWaypointModel
class RouteExtract:
def __init__(self, folder):
self.folder = folder
def iter_files(self):
def iter_files(self) -> Iterator[str]:
for path, folders, files in os.walk(self.folder):
path = path.replace('\\', '/')
for file in files:
if file.endswith('.py'):
yield f'{path}/{file}'
def extract_route(self, file):
def extract_route(self, file) -> Iterator[RouteModel]:
print(f'Extract {file}')
with open(file, 'r', encoding='utf-8') as f:
content = f.read()
@ -37,10 +36,10 @@ class RouteExtract:
"""
regex = re.compile(
r'def (?P<func>[a-zA-Z0-9_]*?)\(self\):.*?'
r'self\.map_init\((.*?)\)'
, re.DOTALL)
r'self\.map_init\((.*?)\)',
re.DOTALL)
file = file.replace(self.folder, '').replace('.py', '').replace('/', '_').strip('_')
module = f"{self.folder.strip('./').replace('/', '.')}.{file.replace('_', '.')}"
module = f"{self.folder.strip('./').replace('/', '.')}.{file}"
for result in regex.findall(content):
func, data = result
@ -62,30 +61,384 @@ class RouteExtract:
else:
position = None
yield RouteData(
name=f'{file}__{func}',
name = f'{file}__{func}'
yield RouteModel(
name=name,
route=f'{module}:{func}',
plane=plane,
floor=floor,
position=position,
)
def iter_route(self):
"""
Yields:
RouteData
"""
for f in self.iter_files():
for row in self.extract_route(f):
yield row
def write(self, file):
gen = CodeGenerator()
gen.Import("""
from tasks.map.route.base import RouteData
from tasks.map.route.model import RouteModel
""")
gen.CommentAutoGenerage('dev_tools.route_extract')
for f in self.iter_files():
for row in self.extract_route(f):
with gen.Object(key=row.name, object_class='RouteData'):
for key in fields(row):
value = getattr(row, key.name)
gen.ObjectAttr(key.name, value)
for row in self.iter_route():
with gen.Object(key=row.name, object_class='RouteModel'):
for key, value in row.__iter__():
gen.ObjectAttr(key, value)
gen.write(file)
def model_to_json(model, file):
content = model.model_dump_json(indent=2)
with open(file, 'w', encoding='utf-8') as f:
f.write(content)
class RouteDetect:
GEN_END = '===== End of generated waypoints ====='
def __init__(self, folder):
self.folder = os.path.abspath(folder)
print(self.folder)
self.waypoints = SelectedGrids(list(self.iter_image()))
@cached_property
def detector(self):
from tasks.map.minimap.brute_force import MinimapWrapper
return MinimapWrapper()
def get_minimap(self, route: RogueWaypointModel):
return self.detector.all_minimap[route.plane_floor]
def iter_image(self) -> Iterator[RogueWaypointModel]:
regex_posi = re.compile(r'_?X(\d+)Y(\d+)')
for domain_folder in iter_folder(self.folder, is_dir=True):
domain = os.path.basename(domain_folder)
for route_folder in iter_folder(domain_folder, is_dir=True):
route = os.path.basename(route_folder)
try:
for image_file in iter_folder(os.path.join(route_folder, 'route'), ext='.png'):
waypoint = os.path.basename(image_file[:-4])
parts = route.split('_', maxsplit=3)
if len(parts) == 4:
world, plane, floor, position = parts
res = regex_posi.search(position)
if res:
position = int(res.group(1)), int(res.group(2))
else:
position = (0, 0)
elif len(parts) == 3:
world, plane, floor = parts
position = (0, 0)
elif len(parts) == 2:
world, plane = parts
floor = 'F1'
position = (0, 0)
else:
continue
file = f'{self.folder}/{domain}/{route}/route/{waypoint}.png'
res = regex_posi.search(waypoint)
if res:
position = int(res.group(1)), int(res.group(2))
# waypoint = regex_posi.sub('', waypoint)
elif waypoint != 'spawn':
position = (0, 0)
model = RogueWaypointModel(
domain=domain,
route=route,
waypoint=waypoint,
index=0,
file=file,
plane=f'{world}_{plane}',
floor=floor,
position=position,
direction=0.,
rotation=0,
)
yield model
# deep_set(out, keys=[image.route, image.waypoint], value=image)
except FileNotFoundError:
pass
def predict(self):
regex_posi = re.compile(r'_?X(\d+)Y(\d+)')
for waypoint in tqdm(self.waypoints.grids):
waypoint: RogueWaypointModel = waypoint
minimap = self.get_minimap(waypoint)
im = load_image(waypoint.file)
prev = waypoint.position
minimap.init_position(waypoint.position, show_log=False)
minimap.update(im, show_log=False)
waypoint.position = minimap.position
waypoint.direction = minimap.direction
waypoint.rotation = minimap.rotation
if prev != (0, 0) and np.linalg.norm(np.subtract(waypoint.position, prev)) > 1.5:
if waypoint.is_spawn:
print(f'Position changed: {self.folder}/{waypoint.domain}/{waypoint.route}'
f' -> {waypoint.plane}_{waypoint.floor}_{waypoint.positionXY}')
else:
name = regex_posi.sub('', waypoint.waypoint)
print(f'Position changed: {waypoint.file}'
f' -> {name}_{waypoint.positionXY}')
self.waypoints.create_index('route')
# Sort by distance
for waypoints in self.waypoints.indexes.values():
waypoints = self.sort_waypoints(waypoints.grids)
for index, waypoint in enumerate(waypoints):
waypoint.index = index
self.waypoints = self.waypoints.sort('route', 'index')
@staticmethod
def sort_waypoints(waypoints: list[RogueWaypointModel]) -> list[RogueWaypointModel]:
waypoints = sorted(waypoints, key=lambda point: point.waypoint, reverse=True)
middle = [point for point in waypoints if not point.is_spawn and not point.is_exit]
if not middle:
return waypoints
try:
spawn: RogueWaypointModel = [point for point in waypoints if point.is_spawn][0]
except IndexError:
return waypoints
prev = spawn.position
if prev == (0, 0):
return waypoints
sorted_middle = []
while len(middle):
distance = np.array([point.position for point in middle]) - prev
distance = np.linalg.norm(distance, axis=1)
index = np.argmin(distance)
sorted_middle.append(middle[index])
middle.pop(index)
end = [point for point in waypoints if point.is_exit]
waypoints = [spawn] + sorted_middle + end
return waypoints
def write(self):
waypoints = RogueWaypointListModel(self.waypoints.grids)
model_to_json(waypoints, f'{self.folder}/data.json')
def gen_route(self, waypoints: SelectedGrids):
gen = CodeGenerator()
spawn: RogueWaypointModel = waypoints.select(is_spawn=True).first_or_none()
exit_: RogueWaypointModel = waypoints.select(is_exit=True).first_or_none()
if spawn is None or exit_ is None:
return
class WaypointRepr:
def __init__(self, position):
if isinstance(position, RogueWaypointModel):
position = position.position
self.position = tuple(position)
def __repr__(self):
return f'Waypoint({self.position})'
__str__ = __repr__
def filter_waypoints(name: str):
return lambda x: x.waypoint.startswith(name)
def clear_elite():
with gen.Object('self.clear_elite'):
for w in waypoints.filter(filter_waypoints('enemy')):
gen.ObjectAttr(value=WaypointRepr(w))
def clear_item():
with gen.Object('self.clear_item'):
for w in waypoints.filter(filter_waypoints('item')):
gen.ObjectAttr(value=WaypointRepr(w))
def clear_event():
with gen.Object('self.clear_event'):
for w in waypoints.filter(filter_waypoints('event')):
gen.ObjectAttr(value=WaypointRepr(w))
def domain_reward():
with gen.Object('self.domain_reward'):
for w in waypoints.filter(filter_waypoints('reward')):
gen.ObjectAttr(value=WaypointRepr(w))
def domain_herta():
with gen.Object('self.domain_herta'):
for w in waypoints.filter(filter_waypoints('herta')):
gen.ObjectAttr(value=WaypointRepr(w))
with gen.tab():
with gen.Def(name=spawn.route, args='self'):
table = MarkdownGenerator(['Waypoint', 'Position', 'Direction', 'Rotation'])
for waypoint in waypoints:
table.add_row([
waypoint.waypoint,
f'{WaypointRepr(waypoint)},',
waypoint.direction,
waypoint.rotation
])
gen.add('"""')
for row in table.generate():
gen.add(row)
gen.add('"""')
position = tuple(spawn.position)
gen.add(f'self.map_init(plane={spawn.plane}, floor="{spawn.floor}", position={position})')
if spawn.is_DomainBoss or spawn.is_DomainElite or spawn.is_DomainRespite:
# Domain has only 1 exit
pass
else:
gen.add(f'self.register_domain_exit({WaypointRepr(exit_)}, end_rotation={exit_.rotation})')
# Domain specific
if spawn.is_DomainBoss or spawn.is_DomainElite:
gen.Empty()
clear_elite()
domain_reward()
if spawn.is_DomainRespite:
gen.Empty()
clear_item()
domain_herta()
if spawn.is_DomainOccurrence or spawn.is_DomainTransaction:
gen.Empty()
clear_item()
clear_event()
if spawn.is_DomainBoss or spawn.is_DomainElite or spawn.is_DomainRespite:
# Domain has only 1 exit
with gen.Object('self.domain_single_exit'):
gen.ObjectAttr(value=WaypointRepr(exit_))
# Single exit does not need end_rotation
# gen.ObjectAttr(key='end_rotation', value=exit_.rotation)
gen.Comment(self.GEN_END)
return gen.generate()
def insert(self, folder, base='tasks.map.route.base'):
# Create folder
self.waypoints.create_index('domain')
for index, waypoints in self.waypoints.indexes.items():
domain = index[0]
os.makedirs(f'{folder}/{domain}', exist_ok=True)
# Create file
self.waypoints.create_index('domain', 'plane', 'floor')
for index, waypoints in self.waypoints.indexes.items():
domain, plane, floor = index
file = f'{folder}/{domain}/{plane}_{floor}.py'
if not os.path.exists(file):
gen = CodeGenerator()
gen.Import(f"""
from {base} import RouteBase
""")
with gen.Class('Route', inherit='RouteBase'):
pass
# gen.Pass()
gen.write(file)
for index, routes in self.waypoints.indexes.items():
domain, plane, floor = index
file = f'{folder}/{domain}/{plane}_{floor}.py'
with open(file, 'r', encoding='utf-8') as f:
content = f.read()
# Add import
if base != 'tasks.map.route.base':
content = content.replace('tasks.map.route.base', base)
p = '_'.join(plane.split('_', maxsplit=2)[:2])
imp = [
'from tasks.map.control.waypoint import Waypoint',
f'from tasks.map.keywords.plane import {p}',
f'from {base} import RouteBase',
][::-1]
res = re.search(r'^(.*?)class Route', content, re.DOTALL)
if res:
head = res.group(1)
for row in imp:
if row not in head:
content = row + '\n' + content
# Replace or add routes
routes.create_index('route')
for waypoints in routes.indexes.values():
spawn = waypoints.select(is_spawn=True).first_or_none()
if spawn is None:
continue
regex = re.compile(rf'def {spawn.route}.*?{self.GEN_END}', re.DOTALL)
res = regex.search(content)
if res:
before = res.group(0).strip()
after = self.gen_route(waypoints).strip()
content = content.replace(before, after)
else:
content += '\n' + self.gen_route(waypoints)
# Sort routes
regex = re.compile(
r'(?=(\n def ([a-zA-Z0-9_]+)\(.*?\n def|\n def ([a-zA-Z0-9_]+)\(.*?$))', re.DOTALL)
funcs = regex.findall(content)
known_routes = [route[0] for route in routes.indexes.keys()]
routes = []
for code, route1, route2 in funcs:
if route1:
route = route1
elif route2:
route = route2
else:
continue
if route not in known_routes:
continue
code = code.removesuffix('\n def').removeprefix('\n')
routes.append((route, code))
sorted_routes = sorted(routes, key=lambda x: x[0])
routes = [route[1] for route in routes]
sorted_routes = [route[1] for route in sorted_routes]
new = ''
for before, after in zip(routes, sorted_routes):
left = content.index(before)
right = left + len(before)
new += content[:left]
new += after
content = content[right:]
new += content
content = new
# Format
content = re.sub(r'[\n ]+ def', '\n\n def', content, re.DOTALL)
content = content.rstrip('\n') + '\n'
# Write
with open(file, 'w', encoding='utf-8', newline='') as f:
f.write(content)
def rogue_extract(folder):
print('rogue_extract')
def iter_route():
for row in RouteExtract(f'{folder}').iter_route():
domain = row.name.split('_', maxsplit=1)[0]
row = RogueRouteModel(domain=domain, **row.model_dump())
row.name = f'{row.domain}_{row.route.split(":")[1]}'
row.route = row.route.replace('_', '.', 1)
yield row
routes = RogueRouteListModel(list(iter_route()))
model_to_json(routes, f'{folder}/route.json')
if __name__ == '__main__':
os.chdir(os.path.join(os.path.dirname(__file__), '../'))
RouteExtract('./route/daily').write('./tasks/map/route/route/daily.py')
self = RouteDetect('../SrcRoute/rogue')
self.predict()
self.write()
self.insert('./route/rogue', base='tasks.rogue.route.base')

View File

@ -108,6 +108,13 @@ class MapPlane(Keyword):
else:
raise ScriptError(f'Plane {self} does not have floor {floor}')
@cached_property
def rogue_domain(self) -> str:
if self.name.startswith('Rogue_Domain'):
return self.name.removeprefix('Rogue_Domain')
else:
return ''
@dataclass(repr=False)
class MapWorld(Keyword):

View File

@ -1,19 +1,8 @@
from dataclasses import dataclass
from tasks.map.control.control import MapControl
from tasks.map.control.waypoint import Waypoint
from tasks.map.keywords import MapPlane
@dataclass
class RouteData:
name: str
route: str
plane: str
floor: str = 'F1'
position: tuple = None
class RouteBase(MapControl):
"""
Base class of `Route`
@ -59,3 +48,9 @@ class RouteBase(MapControl):
self.minimap.set_plane(plane, floor=floor)
if position is not None:
self.minimap.init_position(position)
def before_route(self):
pass
def after_route(self):
pass

View File

@ -5,7 +5,12 @@ from module.base.decorator import del_cached_property
from module.exception import ScriptError
from module.logger import logger
from tasks.base.ui import UI
from tasks.map.route.base import RouteBase, RouteData
from tasks.map.route.base import RouteBase
from tasks.map.route.model import RouteModel
def empty_function(*arg, **kwargs):
return False
class RouteLoader(UI):
@ -18,14 +23,14 @@ class RouteLoader(UI):
self.route_module = ''
self.route_func = ''
def route_run(self, route: RouteData | str):
def route_run(self, route: RouteModel | str):
"""
Args:
route: .py module path such as `route.daily.ForgottenHallStage1:route`
which will load `./route/daily/ForgottenHallStage1.py` and run `Route.route()`
"""
logger.hr('Route run', level=1)
if isinstance(route, RouteData):
if isinstance(route, RouteModel):
route = route.route
logger.attr('Route', route)
try:
@ -58,7 +63,14 @@ class RouteLoader(UI):
raise ScriptError
self.route_module = module
# Get route func
# before_route()
try:
before_func_obj = self.route_obj.__getattribute__('before_route')
except AttributeError:
before_func_obj = empty_function
before_func_obj()
# Run route
try:
func_obj = self.route_obj.__getattribute__(func)
except AttributeError as e:
@ -66,6 +78,11 @@ class RouteLoader(UI):
logger.critical(f'Route class in {route} ({path}) does not have method {func}')
raise ScriptError
self.route_func = func
# Run
func_obj()
# after_route()
try:
after_route_obj = self.route_obj.__getattribute__('after_route')
except AttributeError:
after_route_obj = empty_function
after_route_obj()

12
tasks/map/route/model.py Normal file
View File

@ -0,0 +1,12 @@
from pydantic import BaseModel, RootModel
class RouteModel(BaseModel):
name: str
route: str
plane: str
floor: str
position: tuple[float, float]
RouteListModel = RootModel[list[RouteModel]]

View File

@ -1,37 +1,37 @@
from tasks.map.route.base import RouteData
from tasks.map.route.model import RouteModel
# This file was auto-generated, do not modify it manually. To generate:
# ``` python -m dev_tools.route_extract ```
ForgottenHallStage1__route = RouteData(
ForgottenHallStage1__route = RouteModel(
name='ForgottenHallStage1__route',
route='route.daily.ForgottenHallStage1:route',
plane='Jarilo_BackwaterPass',
floor='F1',
position=(369.4, 643.4),
)
HimekoTrial__route_item_enemy = RouteData(
HimekoTrial__route_item_enemy = RouteModel(
name='HimekoTrial__route_item_enemy',
route='route.daily.HimekoTrial:route_item_enemy',
plane='Jarilo_BackwaterPass',
floor='F1',
position=(519.9, 361.5),
)
HimekoTrial__route_item = RouteData(
HimekoTrial__route_item = RouteModel(
name='HimekoTrial__route_item',
route='route.daily.HimekoTrial:route_item',
plane='Jarilo_BackwaterPass',
floor='F1',
position=(519.9, 361.5),
)
HimekoTrial__route_enemy = RouteData(
HimekoTrial__route_enemy = RouteModel(
name='HimekoTrial__route_enemy',
route='route.daily.HimekoTrial:route_enemy',
plane='Jarilo_BackwaterPass',
floor='F1',
position=(519.9, 361.5),
)
HimekoTrial__exit = RouteData(
HimekoTrial__exit = RouteModel(
name='HimekoTrial__exit',
route='route.daily.HimekoTrial:exit',
plane='Jarilo_BackwaterPass',

152
tasks/rogue/route/base.py Normal file
View File

@ -0,0 +1,152 @@
from module.logger import logger
from tasks.map.control.waypoint import ensure_waypoints
from tasks.map.route.base import RouteBase as RouteBase_
from tasks.rogue.bleesing.blessing import RogueBlessingSelector
from tasks.rogue.bleesing.bonus import RogueBonusSelector
from tasks.rogue.bleesing.curio import RogueCurioSelector
from tasks.rogue.bleesing.ui import RogueUI
from tasks.rogue.route.exit import RogueExit
class RouteBase(RouteBase_, RogueUI, RogueExit):
registered_domain_exit = None
def combat_expected_end(self):
if self.is_page_choose_blessing():
logger.info('Combat ended at is_page_choose_blessing()')
return True
if self.is_page_choose_curio():
logger.info('Combat ended at is_page_choose_curio()')
return True
if self.is_page_choose_bonus():
logger.info('Combat ended at is_page_choose_bonus()')
return True
return False
def combat_execute(self, expected_end=None):
return super().combat_execute(expected_end=self.combat_expected_end)
def clear_blessing(self, skip_first_screenshot=True):
"""
Pages:
in: combat_expected_end()
out: is_in_main()
"""
logger.info(f'Clear blessing')
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
# End
if self.is_in_main():
logger.info(f'clear_blessing() ended at page_main')
break
if self.is_page_choose_blessing():
logger.hr('Choose blessing', level=2)
selector = RogueBlessingSelector(self)
selector.recognize_and_select()
if self.is_page_choose_curio():
logger.hr('Choose curio', level=2)
selector = RogueCurioSelector(self)
selector.recognize_and_select()
if self.is_page_choose_bonus():
logger.hr('Choose bonus', level=2)
selector = RogueBonusSelector(self)
selector.recognize_and_select()
"""
Additional rogue methods
"""
def clear_enemy(self, *waypoints):
logger.hr('Clear enemy', level=1)
result = super().clear_enemy(*waypoints)
self.clear_blessing()
return result
def clear_elite(self, *waypoints):
logger.hr('Clear elite', level=1)
waypoints = ensure_waypoints(waypoints)
end_point = waypoints[-1]
end_point.speed = 'run_2x'
# Use skill
pass
result = super().clear_enemy(*waypoints)
self.clear_blessing()
return result
def clear_event(self, *waypoints):
"""
Handle an event in DomainOccurrence, DomainEncounter, DomainTransaction
"""
logger.hr('Clear event', level=1)
result = self.goto(*waypoints)
return result
def domain_reward(self, *waypoints):
"""
Get reward of the DomainElite and DomainBoss
"""
logger.hr('Clear reward', level=1)
# Skip if not going to get reward
pass
result = self.goto(*waypoints)
return result
def domain_herta(self, *waypoints):
"""
Most people don't buy herta shop, skip
"""
pass
def domain_single_exit(self, *waypoints):
"""
Goto a single exit, exit current domain
end_rotation is not required
"""
logger.hr('Domain single exit', level=1)
waypoints = ensure_waypoints(waypoints)
result = self.goto(*waypoints)
self.domain_exit_interact()
return result
def domain_exit(self, *waypoints, end_rotation=None):
logger.hr('Domain exit', level=1)
waypoints = ensure_waypoints(waypoints)
end_point = waypoints[-1]
end_point.end_rotation = end_rotation
result = self.goto(*waypoints)
return result
"""
Route
"""
def register_domain_exit(self, *waypoints, end_rotation=None):
"""
Register an exit, call `domain_exit()` at route end
"""
self.registered_domain_exit = (waypoints, end_rotation)
def before_route(self):
self.registered_domain_exit = None
def after_route(self):
if self.registered_domain_exit is not None:
waypoints, end_rotation = self.registered_domain_exit
self.domain_exit(*waypoints, end_rotation=end_rotation)
else:
logger.info('No domain exit registered')

151
tasks/rogue/route/loader.py Normal file
View File

@ -0,0 +1,151 @@
from typing import Optional
from module.base.decorator import cached_property
from module.logger import logger
from tasks.base.main_page import MainPage
from tasks.map.keywords import MapPlane
from tasks.map.keywords.plane import (
Herta_MasterControlZone,
Herta_ParlorCar,
Jarilo_AdministrativeDistrict,
Luofu_AurumAlley,
Luofu_ExaltingSanctum
)
from tasks.map.minimap.minimap import Minimap
from tasks.map.route.loader import RouteLoader as RouteLoader_
from tasks.rogue.route.base import RouteBase
from tasks.rogue.route.model import RogueRouteListModel, RogueRouteModel
def model_from_json(model, file: str):
with open(file, 'r', encoding='utf-8') as f:
content = f.read()
data = model.model_validate_json(content)
return data
class RouteLoader(RouteLoader_, MainPage):
@cached_property
def all_minimap(self) -> dict[(str, str), Minimap]:
"""
Returns:
dict: Key: {world}_{plane}_{floor}, e.g. Jarilo_SilvermaneGuardRestrictedZone_F1
Value: Minimap object
"""
# No enemy spawn at the followings
blacklist = [
Herta_ParlorCar,
Herta_MasterControlZone,
Jarilo_AdministrativeDistrict,
Luofu_ExaltingSanctum,
Luofu_AurumAlley,
]
maps = {}
for plane in MapPlane.instances.values():
if plane in blacklist:
continue
if not plane.world:
continue
for floor in plane.floors:
minimap = Minimap()
minimap.set_plane(plane=plane, floor=floor)
maps[f'{plane.name}_{floor}'] = minimap
return maps
@cached_property
def all_route(self) -> list[RogueRouteModel]:
return model_from_json(RogueRouteListModel, './route/rogue/route.json').root
def get_minimap(self, route: RogueRouteModel):
return self.all_minimap[route.plane_floor]
def position_find_known(self, image) -> Optional[RogueRouteModel]:
"""
Try to find from known route spawn point
"""
logger.info('position_find_known')
plane = self.get_plane()
if plane is None:
logger.warning('Unknown rogue domain')
return
visited = []
for route in self.all_route:
if plane.rogue_domain and plane.rogue_domain != route.domain:
if plane.rogue_domain == 'Transaction' and route.is_DomainOccurrence:
# Treat "Transaction" as "Occurrence"
pass
elif plane.rogue_domain == 'Encounter' and route.is_DomainOccurrence:
# Treat "Encounter" as "Occurrence"
pass
else:
continue
minimap = self.get_minimap(route)
minimap.init_position(route.position, show_log=False)
try:
minimap.update_position(image)
except FileNotFoundError:
continue
visited.append((route, minimap.position_similarity))
if len(visited) < 3:
logger.warning('Too few routes to search from, not enough to make a prediction')
return
visited = sorted(visited, key=lambda x: x[1], reverse=True)
logger.info(f'Best 3 prediction: {[(r.name, s) for r, s in visited[:3]]}')
if visited[1][1] / visited[0][1] > 0.75:
logger.warning('Similarity too close, not enough to make a prediction')
return
logger.attr('RoutePredict', visited[0][0].name)
return visited[0][0]
def position_find_bruteforce(self, image) -> Minimap:
"""
Fallback method to find from all planes and floors
"""
logger.warning('position_find_bruteforce, this may take a while')
for name, minimap in self.all_minimap.items():
minimap.init_position((0, 0), show_log=False)
try:
minimap.update_position(image)
except FileNotFoundError:
pass
def get_name(minimap_: Minimap) -> str:
return f'{minimap_.plane.name}_{minimap_.floor}_X{int(minimap_.position[0])}Y{int(minimap_.position[1])}'
visited = sorted(self.all_minimap.values(), key=lambda x: x.position_similarity, reverse=True)
logger.info(f'Best 5 prediction: {[(get_name(m), m.position_similarity) for m in visited[:5]]}')
if visited[1].position_similarity / visited[0].position_similarity > 0.75:
logger.warning('Similarity too close, prediction may goes wrong')
logger.attr('RoutePredict', get_name(visited[0]))
return visited[0]
def route_run(self, route=None):
"""
Run a rogue domain
Pages:
in: page_main
out: page_main, at another domain
"""
route = self.position_find_known(self.device.image)
if route is not None:
super().route_run(route)
else:
self.position_find_bruteforce(self.device.image)
logger.error('New route detected, please record it')
if __name__ == '__main__':
self = RouteLoader('src', task='Rogue')
# self.image_file = r''
# self.position_find_bruteforce(self.device.image)
self.device.screenshot()
base = RouteBase(config=self.config, device=self.device, task='Rogue')
base.clear_blessing()
self.route_run()

135
tasks/rogue/route/model.py Normal file
View File

@ -0,0 +1,135 @@
from pydantic import BaseModel, RootModel
from tasks.map.route.model import RouteModel
class RogueWaypointModel(BaseModel):
"""
{
"domain": "Combat",
"route": "Herta_StorageZone_F1_X252Y84",
"waypoint": "spawn",
"index": 0,
"file": "./screenshots/rogue/Combat/Herta_StorageZone_F1_X252Y84/route/spawn.png",
"plane": "Herta_StorageZone",
"floor": "F1",
"position": [
252.8,
84.8
],
"direction": 300.1,
"rotation": 299
},
"""
domain: str
route: str
waypoint: str
index: int
file: str
plane: str
floor: str
position: tuple[float, float]
direction: float
rotation: int
@property
def plane_floor(self):
return f'{self.plane}_{self.floor}'
@property
def positionXY(self):
x, y = int(self.position[0]), int(self.position[1])
return f'X{x}Y{y}'
@property
def is_spawn(self) -> bool:
return self.waypoint.startswith('spawn')
@property
def is_exit(self) -> bool:
return self.waypoint.startswith('exit')
@property
def is_DomainBoss(self):
return self.domain == 'Boss'
@property
def is_DomainCombat(self):
return self.domain == 'Combat'
@property
def is_DomainElite(self):
return self.domain == 'Elite'
@property
def is_DomainEncounter(self):
return self.domain == 'Encounter'
@property
def is_DomainOccurrence(self):
return self.domain == 'Occurrence'
@property
def is_DomainRespite(self):
return self.domain == 'Respite'
@property
def is_DomainTransaction(self):
return self.domain == 'Transaction'
RogueWaypointListModel = RootModel[list[RogueWaypointModel]]
class RogueRouteModel(RouteModel):
"""
{
"name": "Boss_Luofu_ArtisanshipCommission_F1_X506Y495",
"domain": "Boss",
"route": "route.rogue.Boss.Luofu_ArtisanshipCommission_F1:Luofu_ArtisanshipCommission_F1_X506Y495",
"plane": "Luofu_ArtisanshipCommission",
"floor": "F1",
"position": [
506.0,
495.4
]
},
"""
domain: str
@property
def plane_floor(self):
return f'{self.plane}_{self.floor}'
@property
def is_DomainBoss(self):
return self.domain == 'Boss'
@property
def is_DomainCombat(self):
return self.domain == 'Combat'
@property
def is_DomainElite(self):
return self.domain == 'Elite'
@property
def is_DomainEncounter(self):
return self.domain == 'Encounter'
@property
def is_DomainOccurrence(self):
return self.domain == 'Occurrence'
@property
def is_DomainRespite(self):
return self.domain == 'Respite'
@property
def is_DomainTransaction(self):
return self.domain == 'Transaction'
RogueRouteListModel = RootModel[list[RogueRouteModel]]