diff --git a/campaign/event_20200903_en/sp0.py b/campaign/event_20200903_en/sp0.py new file mode 100644 index 000000000..91cba3e52 --- /dev/null +++ b/campaign/event_20200903_en/sp0.py @@ -0,0 +1,60 @@ +from module.campaign.campaign_base import CampaignBase +from module.map.map_base import CampaignMap +from module.map.map_grids import SelectedGrids, RoadGrids +from module.logger import logger + +MAP = CampaignMap('SP0') +MAP.shape = 'J6' +MAP.camera_data = ['D2', 'D4', 'G2', 'G4'] +MAP.camera_data_spawn_point = ['D2', 'D4'] +MAP.map_data = """ + -- -- ++ ++ ++ ++ -- -- ++ ++ + -- ++ -- -- -- ++ -- -- -- ++ + SP -- -- -- -- -- ++ -- -- -- + SP -- -- -- -- -- -- -- -- MB + -- -- -- -- ++ -- -- ++ -- -- + -- -- -- ++ ++ -- ++ ++ ++ -- +""" +MAP.weight_data = """ + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 + 50 50 50 50 50 50 50 50 50 50 +""" +MAP.land_based_data = [['D6', 'up'], ['H5', 'up'], ['F2', 'down'], ['C1', 'down']] +MAP.spawn_data = [ + {'battle': 0, 'boss': 1}, + # {'battle': 1, 'enemy': 2}, + # {'battle': 2, 'enemy': 1}, + # {'battle': 3, 'enemy': 1, 'mystery': 1}, + # {'battle': 4, 'enemy': 1}, +] +A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, \ +A2, B2, C2, D2, E2, F2, G2, H2, I2, J2, \ +A3, B3, C3, D3, E3, F3, G3, H3, I3, J3, \ +A4, B4, C4, D4, E4, F4, G4, H4, I4, J4, \ +A5, B5, C5, D5, E5, F5, G5, H5, I5, J5, \ +A6, B6, C6, D6, E6, F6, G6, H6, I6, J6, \ + = MAP.flatten() + +mechanism = SelectedGrids([C6, E2, G5]) + + +class Config: + # ===== Start of generated config ===== + MAP_HAS_MAP_STORY = False + MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_LAND_BASED = True + # ===== End of generated config ===== + + +class Campaign(CampaignBase): + MAP = MAP + + def battle_0(self): + self.clear_mechanism(mechanism) + + return self.clear_boss() diff --git a/campaign/event_20200903_en/sp1.py b/campaign/event_20200903_en/sp1.py index b6c09891e..ab762204a 100644 --- a/campaign/event_20200903_en/sp1.py +++ b/campaign/event_20200903_en/sp1.py @@ -5,7 +5,7 @@ from module.logger import logger MAP = CampaignMap('SP1') MAP.shape = 'K7' -MAP.camera_data = ['D2', 'D5', 'H2', 'H5'] +MAP.camera_data = ['D3', 'D5', 'H3', 'H5'] MAP.camera_data_spawn_point = ['D2'] MAP.map_data = """ ++ ++ ++ -- MS -- -- -- ME ++ -- @@ -16,15 +16,25 @@ MAP.map_data = """ ME -- -- ++ -- -- -- -- -- -- -- -- -- -- ++ -- -- -- ++ ++ ++ ++ """ +MAP.map_data_loop = """ + ++ ++ ++ -- MS -- -- -- ME ++ -- + -- ME -- -- -- ++ ++ -- ME ++ MB + SP -- ME -- -- ME ++ -- Me -- Me + SP -- __ -- -- -- ME -- ME -- -- + ME -- -- -- ME -- -- -- -- -- -- + ME -- -- ++ -- -- -- -- -- -- -- + -- -- -- ++ -- -- -- -- ++ ++ ++ +""" MAP.weight_data = """ 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 - 50 50 50 50 50 50 50 50 50 50 30 + 50 50 50 50 50 55 50 50 50 50 30 50 50 50 50 50 50 50 50 50 50 40 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 """ +MAP.land_based_data = [['H7', 'up'], ['F4', 'down'], ['J3', 'down'], ['D2', 'down']] MAP.spawn_data = [ {'battle': 0, 'enemy': 2, 'siren': 1}, {'battle': 1, 'enemy': 1}, @@ -45,14 +55,16 @@ road_main = RoadGrids([K3]) class Config: + # ===== Start of generated config ===== MAP_SIREN_TEMPLATE = ['Z18'] MOVABLE_ENEMY_TURN = (3,) MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True MAP_HAS_MAP_STORY = False MAP_HAS_FLEET_STEP = True - MAP_HAS_AMBUSH = False - MAP_HAS_MOVABLE_ENEMY = True + MAP_HAS_LAND_BASED = True + # ===== End of generated config ===== class Campaign(CampaignBase): @@ -62,6 +74,8 @@ class Campaign(CampaignBase): if self.clear_siren(): return True + self.clear_mechanism() + if self.clear_roadblocks([road_main]): return True diff --git a/campaign/event_20200903_en/sp2.py b/campaign/event_20200903_en/sp2.py index f790d6cd6..2df4ed4bd 100644 --- a/campaign/event_20200903_en/sp2.py +++ b/campaign/event_20200903_en/sp2.py @@ -6,7 +6,7 @@ from .sp1 import Config as ConfigBase MAP = CampaignMap('SP2') MAP.shape = 'K7' -MAP.camera_data = ['D2', 'D5', 'H2', 'H5'] +MAP.camera_data = ['D3', 'D5', 'H3', 'H5'] MAP.camera_data_spawn_point = ['D2', 'D5'] MAP.map_data = """ ++ ++ ++ -- -- -- -- ME ++ ++ -- @@ -17,6 +17,15 @@ MAP.map_data = """ ME ++ ++ -- -- -- -- -- ++ ME MB -- ++ -- ME ME -- -- ME ++ ++ ++ """ +MAP.map_data_loop = """ + ++ ++ ++ -- -- -- -- ME ++ ++ -- + ME -- -- -- -- ++ -- -- MS ++ -- + SP -- -- MS -- -- -- -- -- Me -- + -- ME -- ME ++ ++ ME __ -- -- ++ + SP -- -- ME ++ ++ ME -- ME Me -- + ME ++ -- -- -- -- -- -- -- ME MB + -- ++ -- ME ME -- -- ME ++ ++ ++ +""" MAP.weight_data = """ 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 @@ -26,6 +35,7 @@ MAP.weight_data = """ 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 """ +MAP.land_based_data = [['I6', 'up'], ['C6', 'right'], ['F3', 'right'], ['C2', 'down']] MAP.spawn_data = [ {'battle': 0, 'enemy': 2, 'siren': 2}, {'battle': 1, 'enemy': 2}, @@ -48,11 +58,16 @@ road_main = RoadGrids([J5]) class Config(ConfigBase): + # ===== Start of generated config ===== MAP_SIREN_TEMPLATE = ['Z18'] MOVABLE_ENEMY_TURN = (3,) MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True MAP_HAS_MAP_STORY = False MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_LAND_BASED = True + # ===== End of generated config ===== class Campaign(CampaignBase): @@ -65,6 +80,8 @@ class Campaign(CampaignBase): if self.clear_siren(): return True + self.clear_mechanism() + if self.config.MAP_HAS_MOVABLE_ENEMY: self.fleet_2_push_forward() diff --git a/campaign/event_20200903_en/sp3.py b/campaign/event_20200903_en/sp3.py index 8d03bc301..c378f38b9 100644 --- a/campaign/event_20200903_en/sp3.py +++ b/campaign/event_20200903_en/sp3.py @@ -17,6 +17,15 @@ MAP.map_data = """ ME ME -- __ -- -- -- -- -- ME -- ME ME ++ ME ME ME -- ++ -- MS ME """ +MAP.map_data_loop = """ + ME -- -- MS -- ME ME -- ME ME -- + -- -- -- -- -- -- ++ -- ME ME ME + SP -- ++ ++ ME -- -- -- ++ ++ ++ + SP -- ++ ++ ME -- -- -- Me Me MB + -- -- -- -- ME ME -- -- ++ ++ ++ + ME ME -- __ -- -- -- -- -- ME -- + ME ME ++ ME ME ME -- -- -- MS ME +""" MAP.weight_data = """ 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 @@ -26,6 +35,7 @@ MAP.weight_data = """ 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 """ +MAP.land_based_data = [['H7', 'up'], ['D5', 'left'], ['G3', 'down'], ['C2', 'right']] MAP.spawn_data = [ {'battle': 0, 'enemy': 2, 'siren': 2}, {'battle': 1, 'enemy': 2}, @@ -48,11 +58,16 @@ road_main = RoadGrids([I4, J4]) class Config(ConfigBase): + # ===== Start of generated config ===== MAP_SIREN_TEMPLATE = ['Z18'] MOVABLE_ENEMY_TURN = (3,) MAP_HAS_SIREN = True + MAP_HAS_MOVABLE_ENEMY = True MAP_HAS_MAP_STORY = False MAP_HAS_FLEET_STEP = True + MAP_HAS_AMBUSH = False + MAP_HAS_LAND_BASED = True + # ===== End of generated config ===== class Campaign(CampaignBase): @@ -65,6 +80,8 @@ class Campaign(CampaignBase): if self.clear_siren(): return True + self.clear_mechanism() + if self.config.MAP_HAS_MOVABLE_ENEMY: self.fleet_2_push_forward() diff --git a/dev_tools/map_extractor.py b/dev_tools/map_extractor.py index 1e2eea599..21df75be1 100644 --- a/dev_tools/map_extractor.py +++ b/dev_tools/map_extractor.py @@ -137,6 +137,15 @@ class MapData: target = location2node((effect[2], effect[1])) self.portal.append((address, target)) + # land_based + # land_based = {{6, 7, 1}, ...} + # Format: {y, x, rotation} + land_based_rotation_dict = {1: 'up', 2: 'down', 3: 'left', 4: 'right'} + self.land_based = [] + for lb in data['land_based'].values(): + y, x, r = lb.values() + self.land_based.append([location2node((x, y)), land_based_rotation_dict[r]]) + # config self.MAP_SIREN_TEMPLATE = [] self.MOVABLE_ENEMY_TURN = set() @@ -154,6 +163,7 @@ class MapData: self.MAP_HAS_FLEET_STEP = bool(data['is_limit_move']) self.MAP_HAS_AMBUSH = bool(data['is_ambush']) or bool(data['is_air_attack']) self.MAP_HAS_PORTAL = bool(len(self.portal)) + self.MAP_HAS_LAND_BASED = bool(len(self.land_based)) for n in range(1, 4): self.__setattr__(f'STAR_REQUIRE_{n}', data[f'star_require_{n}']) except Exception as e: @@ -223,7 +233,7 @@ class MapData: f'MAP.camera_data = {[location2node(loca) for loca in camera_data]}') camera_sp = camera_spawn_point(camera_data, sp_list=[k for k, v in self.map_data.items() if v == 'SP']) lines.append(f'MAP.camera_data_spawn_point = {[location2node(loca) for loca in camera_sp]}') - if len(self.portal): + if self.MAP_HAS_PORTAL: lines.append(f'MAP.portal_data = {self.portal}') lines.append('MAP.map_data = \"\"\"') for y in range(self.shape[1] + 1): @@ -238,6 +248,8 @@ class MapData: for y in range(self.shape[1] + 1): lines.append(' ' + ' '.join(['50'] * (self.shape[0] + 1))) lines.append('\"\"\"') + if self.MAP_HAS_LAND_BASED: + lines.append(f'MAP.land_based_data = {self.land_based}') lines.append('MAP.spawn_data = [') for battle in self.spawn_data: lines.append(' ' + str(battle) + ',') @@ -268,6 +280,8 @@ class MapData: lines.append(f' MAP_HAS_AMBUSH = {self.MAP_HAS_AMBUSH}') if self.MAP_HAS_PORTAL: lines.append(f' MAP_HAS_PORTAL = {self.MAP_HAS_PORTAL}') + if self.MAP_HAS_LAND_BASED: + lines.append(f' MAP_HAS_LAND_BASED = {self.MAP_HAS_LAND_BASED}') for n in range(1, 4): if not self.__getattribute__(f'STAR_REQUIRE_{n}'): lines.append(f' STAR_REQUIRE_{n} = 0') diff --git a/module/campaign/campaign_base.py b/module/campaign/campaign_base.py index 76d914f8c..b15caa56c 100644 --- a/module/campaign/campaign_base.py +++ b/module/campaign/campaign_base.py @@ -59,6 +59,9 @@ class CampaignBase(Map, CampaignUI): if remain.count > 0: if self.clear_siren(): return True + + self.clear_mechanism() + return self.battle_default() else: backup = self.config.FLEET_BOSS diff --git a/module/config/config.py b/module/config/config.py index e9b203d20..313397620 100644 --- a/module/config/config.py +++ b/module/config/config.py @@ -136,7 +136,7 @@ class AzurLaneConfig: STOP_IF_TRIGGER_EMOTION_LIMIT = False STOP_IF_DOCK_FULL = False STOP_IF_REACH_LV120 = False - STOP_IF_MAP_REACH = 'no' # no, map_100, map_3_star, map_green_without_3_star, map_green + STOP_IF_MAP_REACH = 'no' # no, map_100, map_3_star, map_green_without_3_star, map_green STOP_IF_GET_SHIP = False MAP_CLEAR_ALL_THIS_TIME = False @@ -266,6 +266,7 @@ class AzurLaneConfig: MAP_HAS_PT_BONUS = False # 100% PT bonus if success to catch enemy else 50%. Retreat get 0%. MAP_IS_ONE_TIME_STAGE = False MAP_HAS_PORTAL = False + MAP_HAS_LAND_BASED = False MAP_ENEMY_TEMPLATE = ['Light', 'Main', 'Carrier', 'Treasure'] MAP_SIREN_TEMPLATE = ['DD', 'CL', 'CA', 'BB', 'CV'] MAP_SIREN_MOVE_WAIT = 1.5 # The enemy moving takes about 1.2 ~ 1.5s. @@ -421,7 +422,7 @@ class AzurLaneConfig: # When having 6 ships in dorm, to use 6 kind of food, need interval (in minutes) greater than: # (14, 28, 42, 70, 139, 278) DORM_FEED_INTERVAL = '278, 480' # str, such as '20', '10, 40'. - DORM_COLLECT_INTERVAL = '60, 180' # str, such as '20', '10, 40'. + DORM_COLLECT_INTERVAL = '60, 180' # str, such as '20', '10, 40'. DORM_FEED_FILTER = '20000 > 10000 > 5000 > 3000 > 2000 > 1000' ENABLE_DATA_KEY_COLLECT = True diff --git a/module/handler/fast_forward.py b/module/handler/fast_forward.py index 98e7c8a81..dfeb46c71 100644 --- a/module/handler/fast_forward.py +++ b/module/handler/fast_forward.py @@ -1,135 +1,136 @@ -from module.base.base import ModuleBase -from module.base.utils import color_bar_percentage -from module.handler.assets import * -from module.logger import logger -from module.ui.switch import Switch - -fast_forward = Switch('Fast_Forward') -fast_forward.add_status('on', check_button=FAST_FORWARD_ON) -fast_forward.add_status('off', check_button=FAST_FORWARD_OFF) -fleet_lock = Switch('Fleet_Lock', offset=(5, 5)) -fleet_lock.add_status('on', check_button=FLEET_LOCKED) -fleet_lock.add_status('off', check_button=FLEET_UNLOCKED) - - -class FastForwardHandler(ModuleBase): - map_clear_percentage = 0. - map_achieved_star_1 = False - map_achieved_star_2 = False - map_achieved_star_3 = False - map_is_clear = False - map_is_3_star = False - map_is_green = False - map_has_fast_forward = False - map_is_clear_mode = False # Clear mode == fast forward - - map_clear_record = None - - def map_get_info(self): - """ - Logs: - | INFO | [Map_info] 98%, star_1, star_2, star_3, clear, 3_star, green, fast_forward - """ - self.map_clear_percentage = color_bar_percentage( - self.device.image, area=MAP_CLEAR_PERCENTAGE.area, prev_color=(231, 170, 82)) - self.map_achieved_star_1 = self.appear(MAP_STAR_1) - self.map_achieved_star_2 = self.appear(MAP_STAR_2) - self.map_achieved_star_3 = self.appear(MAP_STAR_3) - self.map_is_clear = self.map_clear_percentage > 0.95 - self.map_is_3_star = self.map_achieved_star_1 and self.map_achieved_star_2 and self.map_achieved_star_3 - self.map_is_green = self.appear(MAP_GREEN) - self.map_has_fast_forward = self.map_is_3_star and self.map_is_green and fast_forward.appear(main=self) - - # Override config - if self.map_achieved_star_1: - # Story before boss spawn, Attribute "story_refresh_boss" in chapter_template.lua - self.config.MAP_HAS_MAP_STORY = False - self.config.MAP_CLEAR_ALL_THIS_TIME = self.config.STAR_REQUIRE_3 \ - and not self.__getattribute__(f'map_achieved_star_{self.config.STAR_REQUIRE_3}') \ - and self.config.STOP_IF_MAP_REACH != 'map_green_without_3_star' \ - and self.config.STOP_IF_MAP_REACH != 'map_100' - logger.attr('MAP_CLEAR_ALL_THIS_TIME', self.config.MAP_CLEAR_ALL_THIS_TIME) - - # Log - names = ['map_achieved_star_1', 'map_achieved_star_2', 'map_achieved_star_3', 'map_is_clear', 'map_is_3_star', - 'map_is_green', 'map_has_fast_forward'] - strip = ['map', 'achieved', 'is', 'has'] - log_names = ['_'.join([x for x in name.split('_') if x not in strip]) for name in names] - text = ', '.join([l for l, n in zip(log_names, names) if self.__getattribute__(n)]) - text = f'{int(self.map_clear_percentage * 100)}%, ' + text - logger.attr('Map_info', text) - - def handle_fast_forward(self): - if not self.map_has_fast_forward: - self.map_is_clear_mode = False - return False - - if self.config.ENABLE_FAST_FORWARD: - self.config.MAP_HAS_AMBUSH = False - self.config.MAP_HAS_FLEET_STEP = False - self.config.MAP_HAS_MOVABLE_ENEMY = False - self.config.MAP_HAS_PORTAL = False - self.map_is_clear_mode = True - else: - # When disable fast forward, MAP_HAS_AMBUSH depends on map settings. - # self.config.MAP_HAS_AMBUSH = True - self.map_is_clear_mode = False - pass - - status = 'on' if self.config.ENABLE_FAST_FORWARD else 'off' - changed = fast_forward.set(status=status, main=self) - return changed - - def handle_map_fleet_lock(self): - # Fleet lock depends on if it appear on map, not depends on map status. - # Because if already in map, there's no map status, - if not fleet_lock.appear(main=self): - logger.info('No fleet lock option.') - return False - - status = 'on' if self.config.ENABLE_MAP_FLEET_LOCK else 'off' - changed = fleet_lock.set(status=status, main=self) - - return changed - - def get_map_clear_percentage(self): - """ - Returns: - float: 0 to 1. - """ - return color_bar_percentage(self.device.image, area=MAP_CLEAR_PERCENTAGE.area, prev_color=(231, 170, 82)) - - def triggered_map_stop(self): - """ - Returns: - bool: - """ - if self.config.STOP_IF_MAP_REACH == 'map_100': - if self.map_is_clear: - return True - - if self.config.STOP_IF_MAP_REACH == 'map_3_star': - if self.map_is_clear and self.map_is_3_star: - return True - - if self.config.STOP_IF_MAP_REACH == 'map_green_without_3_star': - if self.map_is_clear and self.map_is_green: - return True - - if self.config.STOP_IF_MAP_REACH == 'map_green': - if self.map_is_clear and self.map_is_3_star and self.map_is_green: - return True - - return False - - def handle_map_stop(self): - if self.map_clear_record is True: - return False - - flag = self.triggered_map_stop() - if self.map_clear_record is None: - self.map_clear_record = flag - elif self.map_clear_record is False and flag: - return True - +from module.base.base import ModuleBase +from module.base.utils import color_bar_percentage +from module.handler.assets import * +from module.logger import logger +from module.ui.switch import Switch + +fast_forward = Switch('Fast_Forward') +fast_forward.add_status('on', check_button=FAST_FORWARD_ON) +fast_forward.add_status('off', check_button=FAST_FORWARD_OFF) +fleet_lock = Switch('Fleet_Lock', offset=(5, 5)) +fleet_lock.add_status('on', check_button=FLEET_LOCKED) +fleet_lock.add_status('off', check_button=FLEET_UNLOCKED) + + +class FastForwardHandler(ModuleBase): + map_clear_percentage = 0. + map_achieved_star_1 = False + map_achieved_star_2 = False + map_achieved_star_3 = False + map_is_clear = False + map_is_3_star = False + map_is_green = False + map_has_fast_forward = False + map_is_clear_mode = False # Clear mode == fast forward + + map_clear_record = None + + def map_get_info(self): + """ + Logs: + | INFO | [Map_info] 98%, star_1, star_2, star_3, clear, 3_star, green, fast_forward + """ + self.map_clear_percentage = color_bar_percentage( + self.device.image, area=MAP_CLEAR_PERCENTAGE.area, prev_color=(231, 170, 82)) + self.map_achieved_star_1 = self.appear(MAP_STAR_1) + self.map_achieved_star_2 = self.appear(MAP_STAR_2) + self.map_achieved_star_3 = self.appear(MAP_STAR_3) + self.map_is_clear = self.map_clear_percentage > 0.95 + self.map_is_3_star = self.map_achieved_star_1 and self.map_achieved_star_2 and self.map_achieved_star_3 + self.map_is_green = self.appear(MAP_GREEN) + self.map_has_fast_forward = self.map_is_3_star and self.map_is_green and fast_forward.appear(main=self) + + # Override config + if self.map_achieved_star_1: + # Story before boss spawn, Attribute "story_refresh_boss" in chapter_template.lua + self.config.MAP_HAS_MAP_STORY = False + self.config.MAP_CLEAR_ALL_THIS_TIME = self.config.STAR_REQUIRE_3 \ + and not self.__getattribute__(f'map_achieved_star_{self.config.STAR_REQUIRE_3}') \ + and self.config.STOP_IF_MAP_REACH != 'map_green_without_3_star' \ + and self.config.STOP_IF_MAP_REACH != 'map_100' + logger.attr('MAP_CLEAR_ALL_THIS_TIME', self.config.MAP_CLEAR_ALL_THIS_TIME) + + # Log + names = ['map_achieved_star_1', 'map_achieved_star_2', 'map_achieved_star_3', 'map_is_clear', 'map_is_3_star', + 'map_is_green', 'map_has_fast_forward'] + strip = ['map', 'achieved', 'is', 'has'] + log_names = ['_'.join([x for x in name.split('_') if x not in strip]) for name in names] + text = ', '.join([l for l, n in zip(log_names, names) if self.__getattribute__(n)]) + text = f'{int(self.map_clear_percentage * 100)}%, ' + text + logger.attr('Map_info', text) + + def handle_fast_forward(self): + if not self.map_has_fast_forward: + self.map_is_clear_mode = False + return False + + if self.config.ENABLE_FAST_FORWARD: + self.config.MAP_HAS_AMBUSH = False + self.config.MAP_HAS_FLEET_STEP = False + self.config.MAP_HAS_MOVABLE_ENEMY = False + self.config.MAP_HAS_PORTAL = False + self.config.MAP_HAS_LAND_BASED = False + self.map_is_clear_mode = True + else: + # When disable fast forward, MAP_HAS_AMBUSH depends on map settings. + # self.config.MAP_HAS_AMBUSH = True + self.map_is_clear_mode = False + pass + + status = 'on' if self.config.ENABLE_FAST_FORWARD else 'off' + changed = fast_forward.set(status=status, main=self) + return changed + + def handle_map_fleet_lock(self): + # Fleet lock depends on if it appear on map, not depends on map status. + # Because if already in map, there's no map status, + if not fleet_lock.appear(main=self): + logger.info('No fleet lock option.') + return False + + status = 'on' if self.config.ENABLE_MAP_FLEET_LOCK else 'off' + changed = fleet_lock.set(status=status, main=self) + + return changed + + def get_map_clear_percentage(self): + """ + Returns: + float: 0 to 1. + """ + return color_bar_percentage(self.device.image, area=MAP_CLEAR_PERCENTAGE.area, prev_color=(231, 170, 82)) + + def triggered_map_stop(self): + """ + Returns: + bool: + """ + if self.config.STOP_IF_MAP_REACH == 'map_100': + if self.map_is_clear: + return True + + if self.config.STOP_IF_MAP_REACH == 'map_3_star': + if self.map_is_clear and self.map_is_3_star: + return True + + if self.config.STOP_IF_MAP_REACH == 'map_green_without_3_star': + if self.map_is_clear and self.map_is_green: + return True + + if self.config.STOP_IF_MAP_REACH == 'map_green': + if self.map_is_clear and self.map_is_3_star and self.map_is_green: + return True + + return False + + def handle_map_stop(self): + if self.map_clear_record is True: + return False + + flag = self.triggered_map_stop() + if self.map_clear_record is None: + self.map_clear_record = flag + elif self.map_clear_record is False and flag: + return True + return False \ No newline at end of file diff --git a/module/map/fleet.py b/module/map/fleet.py index 5a0ecbb22..917341f79 100644 --- a/module/map/fleet.py +++ b/module/map/fleet.py @@ -1,719 +1,724 @@ -import itertools - -import numpy as np - -import module.config.server as server -from module.base.timer import Timer -from module.exception import MapWalkError, MapEnemyMoved -from module.handler.ambush import AmbushHandler -from module.logger import logger -from module.map.camera import Camera -from module.map.map_base import SelectedGrids -from module.map.map_base import location2node, location_ensure -from module.map.utils import match_movable - - -class Fleet(Camera, AmbushHandler): - fleet_1_location = () - fleet_2_location = () - fleet_current_index = 1 - battle_count = 0 - mystery_count = 0 - siren_count = 0 - fleet_ammo = 5 - ammo_count = 3 - - @property - def fleet_1(self): - if self.fleet_current_index != 1: - self.fleet_switch() - return self - - @fleet_1.setter - def fleet_1(self, value): - self.fleet_1_location = value - - @property - def fleet_2(self): - if self.fleet_current_index != 2: - self.fleet_switch() - return self - - @fleet_2.setter - def fleet_2(self, value): - self.fleet_2_location = value - - @property - def fleet_current(self): - if self.fleet_current_index == 2: - return self.fleet_2_location - else: - return self.fleet_1_location - - @property - def fleet_boss(self): - if self.config.FLEET_BOSS == 2 or self.config.FLEET_2: - return self.fleet_2 - else: - return self.fleet_1 - - @property - def fleet_boss_index(self): - if self.config.FLEET_BOSS == 2 or self.config.FLEET_2: - return 2 - else: - return 1 - - @property - def fleet_step(self): - if not self.config.MAP_HAS_FLEET_STEP: - return 0 - if self.fleet_current_index == 2: - return self.config.FLEET_2_STEP - else: - return self.config.FLEET_1_STEP - - def fleet_switch(self): - self.fleet_switch_click() - self.fleet_current_index = 1 if self.fleet_current_index == 2 else 2 - if server.server == 'jp': - # [JP] After fleet switch, camera don't focus on fleet, but about 0.75 grids higher than grid center. - # So need to correct camera position. - self.ensure_edge_insight() - else: - self.camera = self.fleet_current - self.update() - self.find_path_initial() - self.map.show_cost() - self.show_fleet() - self.hp_get() - self.lv_get() - self.handle_strategy(index=self.fleet_current_index) - - def switch_to(self): - pass - - round = 0 - enemy_round = {} - - def round_next(self): - """ - Call this method after fleet arrived. - """ - if not self.config.MAP_HAS_MOVABLE_ENEMY: - return False - self.round += 1 - logger.info(f'Round: {self.round}, enemy_round: {self.enemy_round}') - - def round_battle(self): - """ - Call this method after cleared an enemy. - """ - if not self.config.MAP_HAS_MOVABLE_ENEMY: - return False - if not self.map.select(is_siren=True): - self.enemy_round = {} - try: - data = self.map.spawn_data[self.battle_count] - except IndexError: - data = {} - enemy = data.get('siren', 0) - if enemy > 0: - r = self.round - self.enemy_round[r] = self.enemy_round.get(r, 0) + enemy - - def round_reset(self): - """ - Call this method after entering map. - """ - self.round = 0 - self.enemy_round = {} - - @property - def round_is_new(self): - """ - Usually, MOVABLE_ENEMY_TURN = 2. - So a walk round is `player - player - enemy`, player moves twice, enemy moves once. - - Different sirens have different MOVABLE_ENEMY_TURN: - 2: Non-siren elite, SIREN_CL - 3: SIREN_CA - - Returns: - bool: If it's a new walk round, which means enemies have moved. - """ - if not self.config.MAP_HAS_MOVABLE_ENEMY: - return False - for enemy in self.enemy_round.keys(): - for turn in self.config.MOVABLE_ENEMY_TURN: - if self.round - enemy > 0 and (self.round - enemy) % turn == 0: - return True - - return False - - @property - def round_wait(self): - """ - Returns: - float: Seconds to wait enemies moving. - """ - if not self.config.MAP_HAS_MOVABLE_ENEMY: - return 0 - count = 0 - for enemy, c in self.enemy_round.items(): - for turn in self.config.MOVABLE_ENEMY_TURN: - if self.round + 1 - enemy > 0 and (self.round + 1 - enemy) % turn == 0: - count += c - break - - return count * self.config.MAP_SIREN_MOVE_WAIT - - movable_before: SelectedGrids - - def _goto(self, location, expected=''): - """Goto a grid directly and handle ambush, air raid, mystery picked up, combat. - - Args: - location (tuple, str, GridInfo): Destination. - """ - location = location_ensure(location) - result_mystery = '' - self.movable_before = self.map.select(is_siren=True) - if self.hp_withdraw_triggered(): - self.withdraw() - is_portal = self.map[location].is_portal - - while 1: - sight = self.map.camera_sight - self.in_sight(location, sight=(sight[0], 0, sight[2], sight[3])) - self.focus_to_grid_center() - grid = self.convert_map_to_grid(location) - - self.ambush_color_initial() - self.enemy_searching_color_initial() - grid.__str__ = location - result = 'nothing' - self.device.click(grid) - arrived = False - # Wait to confirm fleet arrived. It does't appear immediately if fleet in combat . - extra = 4.5 if self.config.SUBMARINE_MODE == 'hunt_only' else 0 - arrive_timer = Timer(0.5 + self.round_wait + extra, count=2) - arrive_unexpected_timer = Timer(1.5 + self.round_wait + extra, count=6) - # Wait after ambushed. - ambushed_retry = Timer(0.5) - # If nothing happens, click again. - walk_timeout = Timer(20) - walk_timeout.start() - - while 1: - self.device.screenshot() - grid.image = np.array(self.device.image) - if is_portal: - self.update() - grid = self.view[self.view.center_loca] - - # Combat - if self.config.ENABLE_MAP_FLEET_LOCK and not self.is_in_map(): - if self.handle_retirement(): - self.map_offensive() - walk_timeout.reset() - if self.handle_combat_low_emotion(): - walk_timeout.reset() - if self.combat_appear(): - self.combat(expected_end=self._expected_combat_end(expected), fleet_index=self.fleet_current_index) - self.hp_get() - self.lv_get(after_battle=True) - arrived = True if not self.config.MAP_HAS_MOVABLE_ENEMY else False - result = 'combat' - self.battle_count += 1 - self.fleet_ammo -= 1 - if 'siren' in expected or (self.config.MAP_HAS_MOVABLE_ENEMY and not expected): - self.siren_count += 1 - elif self.map[location].may_enemy: - self.map[location].is_cleared = True - - self.handle_boss_appear_refocus() - grid = self.convert_map_to_grid(location) - walk_timeout.reset() - - # Ambush - if self.handle_ambush(): - self.hp_get() - self.lv_get(after_battle=True) - ambushed_retry.start() - walk_timeout.reset() - - # Mystery - mystery = self.handle_mystery(button=grid) - if mystery: - self.mystery_count += 1 - result = 'mystery' - result_mystery = mystery - - # Cat attack animation - if self.handle_map_cat_attack(): - walk_timeout.reset() - continue - - if self.handle_walk_out_of_step(): - raise MapWalkError('walk_out_of_step') - - # Arrive - if self.is_in_map() and \ - (grid.predict_fleet() or - (walk_timeout.reached() and grid.predict_current_fleet())): - if not arrive_timer.started(): - logger.info(f'Arrive {location2node(location)}') - arrive_timer.start() - arrive_unexpected_timer.start() - if not arrive_timer.reached(): - continue - if expected and result not in expected: - if arrive_unexpected_timer.reached(): - logger.warning('Arrive with unexpected result') - else: - continue - if is_portal: - location = self.map[location].portal_link - self.camera = location - logger.info(f'Arrive {location2node(location)} confirm. Result: {result}. Expected: {expected}') - arrived = True - break - - # Story - if expected == 'story': - if self.handle_story_skip(): - result = 'story' - continue - - # End - if ambushed_retry.started() and ambushed_retry.reached(): - break - if walk_timeout.reached(): - logger.warning('Walk timeout. Retrying.') - self.ensure_edge_insight() - break - - # End - if arrived: - # Ammo grid needs to click again, otherwise the next click doesn't work. - if self.map[location].may_ammo: - self.device.click(grid) - break - - self.map[self.fleet_current].is_fleet = False - self.map[location].wipe_out() - self.map[location].is_fleet = True - self.__setattr__('fleet_%s_location' % self.fleet_current_index, location) - if result_mystery == 'get_carrier': - self.full_scan_carrier() - if result == 'combat': - self.round_battle() - self.round_next() - if self.round_is_new: - self.full_scan_movable(enemy_cleared=result == 'combat') - self.find_path_initial() - raise MapEnemyMoved - self.find_path_initial() - - def goto(self, location, optimize=True, expected=''): - # self.device.sleep(1000) - location = location_ensure(location) - if (self.config.MAP_HAS_AMBUSH or self.config.MAP_HAS_FLEET_STEP or self.config.MAP_HAS_PORTAL) and optimize: - nodes = self.map.find_path(location, step=self.fleet_step) - for node in nodes: - try: - self._goto(node, expected=expected if node == nodes[-1] else '') - except MapWalkError: - logger.warning('Map walk error.') - self.ensure_edge_insight() - nodes_ = self.map.find_path(node, step=1) - for node_ in nodes_: - self._goto(node_, expected=expected if node == nodes[-1] else '') - else: - self._goto(location, expected=expected) - - def find_path_initial(self): - """ - Call this method after fleet moved or entered map. - """ - if self.fleet_1_location: - self.map[self.fleet_1_location].is_fleet = True - if self.fleet_2_location: - self.map[self.fleet_2_location].is_fleet = True - location_dict = {} - if self.config.FLEET_2: - location_dict[2] = self.fleet_2_location - location_dict[1] = self.fleet_1_location - self.map.find_path_initial_multi_fleet( - location_dict, current=self.fleet_current, has_ambush=self.config.MAP_HAS_AMBUSH) - - def show_fleet(self): - fleets = [] - for n in [1, 2]: - fleet = self.__getattribute__('fleet_%s_location' % n) - if len(fleet): - text = 'Fleet_%s: %s' % (n, location2node(fleet)) - if self.fleet_current_index == n: - text = '[%s]' % text - fleets.append(text) - logger.info(' '.join(fleets)) - - def full_scan(self, queue=None, must_scan=None, mode='normal'): - super().full_scan( - queue=queue, must_scan=must_scan, battle_count=self.battle_count, mystery_count=self.mystery_count, - siren_count=self.siren_count, carrier_count=self.carrier_count, mode=mode) - - if self.config.FLEET_2 and not self.fleet_2_location: - fleets = self.map.select(is_fleet=True, is_current_fleet=False) - if fleets.count: - logger.info(f'Predict fleet_2 to be {fleets[0]}') - self.fleet_2_location = fleets[0].location - - for loca in [self.fleet_1_location, self.fleet_2_location]: - if len(loca) and loca in self.map: - grid = self.map[loca] - if grid.may_boss and grid.is_caught_by_siren: - # Only boss appears on fleet's face - pass - else: - self.map[loca].wipe_out() - - def full_scan_carrier(self): - """ - Call this method if get enemy searching in mystery. - """ - prev = self.map.select(is_enemy=True) - self.full_scan(mode='carrier') - diff = self.map.select(is_enemy=True).delete(prev) - logger.info(f'Carrier spawn: {diff}') - - def full_scan_movable(self, enemy_cleared=True): - """ - Call this method if enemy moved. - - Args: - enemy_cleared (bool): True if cleared an enemy and need to scan spawn enemies. - False if just a simple walk and only need to scan movable enemies. - """ - before = self.movable_before - for grid in before: - grid.wipe_out() - - self.full_scan(queue=None if enemy_cleared else before, must_scan=before, mode='movable') - - # Track siren moving - after = self.map.select(is_siren=True) - step = self.config.MOVABLE_ENEMY_FLEET_STEP - matched_before, matched_after = match_movable( - before=before.location, - spawn=self.map.select(may_siren=True).location, - after=after.location, - fleets=[self.fleet_current] if enemy_cleared else [], - fleet_step=step - ) - matched_before = self.map.to_selected(matched_before) - matched_after = self.map.to_selected(matched_after) - logger.info(f'Movable enemy {before} -> {after}') - logger.info(f'Tracked enemy {matched_before} -> {matched_after}') - - # Delete wrong prediction - for grid in after.delete(matched_after): - if not grid.may_siren: - logger.warning(f'Wrong detection: {grid}') - grid.wipe_out() - - # Predict missing siren - diff = before.delete(matched_before) - _, missing = self.map.missing_get( - self.battle_count, self.mystery_count, self.siren_count, self.carrier_count, mode='normal') - if diff and missing['siren'] != 0: - logger.warning(f'Movable enemy tracking lost: {diff}') - covered = self.map.grid_covered(self.map[self.fleet_current], location=[(0, -2)]) - if self.fleet_1_location: - covered = covered.add(self.map.grid_covered(self.map[self.fleet_1_location], location=[(0, -1)])) - if self.fleet_2_location: - covered = covered.add(self.map.grid_covered(self.map[self.fleet_2_location], location=[(0, -1)])) - for grid in after: - covered = covered.add(self.map.grid_covered(grid)) - logger.attr('enemy_covered', covered) - accessible = SelectedGrids([]) - for grid in diff: - self.map.find_path_initial(grid, has_ambush=False) - accessible = accessible.add(self.map.select(cost=0)) \ - .add(self.map.select(cost=1)).add(self.map.select(cost=2)) - self.map.find_path_initial(self.fleet_current, has_ambush=self.config.MAP_HAS_AMBUSH) - logger.attr('enemy_accessible', accessible) - predict = accessible.intersect(covered).select(is_sea=True, is_fleet=False) - logger.info(f'Movable enemy predict: {predict}') - for grid in predict: - grid.is_siren = True - grid.is_enemy = True - elif missing['siren'] == 0: - logger.info(f'Movable enemy tracking drop: {diff}') - - for grid in matched_after: - if grid.location != self.fleet_current: - grid.is_movable = True - - def find_all_fleets(self): - logger.hr('Find all fleets') - queue = self.map.select(is_spawn_point=True) - while queue: - queue = queue.sort_by_camera_distance(self.camera) - self.in_sight(queue[0], sight=(-1, 0, 1, 2)) - grid = self.convert_map_to_grid(queue[0]) - if grid.predict_fleet(): - if grid.predict_current_fleet(): - self.fleet_1 = queue[0].location - else: - self.fleet_2 = queue[0].location - queue = queue[1:] - - def find_current_fleet(self): - logger.hr('Find current fleet') - if not self.config.POOR_MAP_DATA: - fleets = self.map.select(is_fleet=True, is_spawn_point=True) - else: - fleets = self.map.select(is_fleet=True) - logger.info('Fleets: %s' % str(fleets)) - count = fleets.count - if count == 1: - if not self.config.FLEET_2: - self.fleet_1 = fleets[0].location - else: - logger.info('Fleet_2 not detected.') - if self.config.POOR_MAP_DATA and not self.map.select(is_spawn_point=True): - self.fleet_1 = fleets[0].location - elif self.map.select(is_spawn_point=True).count == 2: - logger.info('Predict fleet to be spawn point') - another = self.map.select(is_spawn_point=True).delete(SelectedGrids([fleets[0]]))[0] - if fleets[0].is_current_fleet: - self.fleet_1 = fleets[0].location - self.fleet_2 = another.location - else: - self.fleet_1 = another.location - self.fleet_2 = fleets[0].location - else: - cover = self.map.grid_covered(fleets[0], location=[(0, -1)]) - if fleets[0].is_current_fleet and len(cover) and cover[0].is_spawn_point: - self.fleet_1 = fleets[0].location - self.fleet_2 = cover[0].location - else: - self.find_all_fleets() - elif count == 2: - current = self.map.select(is_current_fleet=True) - if current.count == 1: - self.fleet_1 = current[0].location - self.fleet_2 = fleets.delete(current)[0].location - else: - fleets = fleets.sort_by_camera_distance(self.camera) - self.in_sight(fleets[0], sight=(-1, 0, 1, 2)) - if self.convert_map_to_grid(fleets[0]).predict_current_fleet(): - self.fleet_1 = fleets[0].location - self.fleet_2 = fleets[1].location - else: - self.in_sight(fleets[1], sight=(-1, 0, 1, 2)) - if self.convert_map_to_grid(fleets[1]).predict_current_fleet(): - self.fleet_1 = fleets[1].location - self.fleet_2 = fleets[0].location - else: - logger.warning('Current fleet not found') - self.fleet_1 = fleets[0].location - self.fleet_2 = fleets[1].location - else: - if count == 0: - logger.warning('No fleets detected.') - fleets = self.map.select(is_current_fleet=True) - if fleets.count: - self.fleet_1 = fleets[0].location - if count > 2: - logger.warning('Too many fleets: %s.' % str(fleets)) - self.find_all_fleets() - - self.fleet_current_index = 1 - self.show_fleet() - return self.fleet_current - - def map_init(self, map_): - logger.hr('Map init') - self.fleet_1_location = () - self.fleet_2_location = () - self.fleet_current_index = 1 - self.battle_count = 0 - self.mystery_count = 0 - self.carrier_count = 0 - self.siren_count = 0 - self.ammo_count = 3 - self.map = map_ - self.map.reset() - self.handle_map_green_config_cover() - self.map.poor_map_data = self.config.POOR_MAP_DATA - self.map.load_map_data(use_loop=self.map_is_clear_mode) - self.map.grid_connection_initial( - wall=self.config.MAP_HAS_WALL, - portal=self.config.MAP_HAS_PORTAL, - ) - - self.handle_strategy(index=1 if not self.fleets_reversed() else 2) - self.update() - if self.handle_fleet_reverse(): - self.handle_strategy(index=1) - self.hp_reset() - self.hp_get() - self.lv_reset() - self.lv_get() - self.ensure_edge_insight(preset=self.map.in_map_swipe_preset_data) - self.full_scan(must_scan=self.map.camera_data_spawn_point) - self.find_current_fleet() - self.find_path_initial() - self.map.show_cost() - self.round_reset() - self.round_battle() - - def handle_map_green_config_cover(self): - if not self.map_is_green: - return False - - if self.config.POOR_MAP_DATA and self.map.is_map_data_poor: - self.config.POOR_MAP_DATA = False - - return True - - def _expected_combat_end(self, expected): - for data in self.map.spawn_data: - if data.get('battle') == self.battle_count and 'boss' in expected: - return 'in_stage' - if data.get('battle') == self.battle_count + 1: - if data.get('enemy', 0) + data.get('siren', 0) + data.get('boss', 0) > 0: - return 'with_searching' - else: - return 'no_searching' - - if 'boss' in expected: - return 'in_stage' - - return None - - def fleet_at(self, grid, fleet=None): - """ - Args: - grid (Grid): - fleet (int): 1, 2 - - Returns: - bool: If fleet is at grid. - """ - if fleet is None: - return self.fleet_current == grid.location - if fleet == 1: - return self.fleet_1_location == grid.location - else: - return self.fleet_2_location == grid.location - - def check_accessibility(self, grid, fleet=None): - """ - Args: - grid (Grid): - fleet (int, str): 1, 2, 'boss' - - Returns: - bool: If accessible. - """ - if fleet is None: - return grid.is_accessible - if isinstance(fleet, str) and fleet.isdigit(): - fleet = int(fleet) - if fleet == 'boss': - fleet = self.fleet_boss_index - - if fleet == self.fleet_current_index: - return grid.is_accessible - else: - backup = self.fleet_current_index - self.fleet_current_index = fleet - self.find_path_initial() - result = grid.is_accessible - - self.fleet_current_index = backup - self.find_path_initial() - return result - - def brute_find_roadblocks(self, grid, fleet=None): - """ - Args: - grid (Grid): - fleet (int): 1, 2. Default to current fleet. - - Returns: - SelectedGrids: - """ - if fleet is not None and fleet != self.fleet_current_index: - backup = self.fleet_current_index - self.fleet_current_index = fleet - self.find_path_initial() - else: - backup = None - - if grid.is_accessible: - if backup is not None: - self.fleet_current_index = backup - self.find_path_initial() - return SelectedGrids([]) - - enemies = self.map.select(is_enemy=True) - logger.info(f'Potential enemy roadblocks: {enemies}') - for repeat in range(1, enemies.count + 1): - for select in itertools.product(enemies, repeat=repeat): - for block in select: - block.is_enemy = False - self.find_path_initial() - for block in select: - block.is_enemy = True - - if grid.is_accessible: - select = SelectedGrids(list(select)) - logger.info(f'Enemy roadblock: {select}') - if backup is not None: - self.fleet_current_index = backup - self.find_path_initial() - return select - - logger.warning('Enemy roadblock try exhausted.') - - def handle_boss_appear_refocus(self): - """ - - """ - appear = False - for data in self.map.spawn_data: - if data.get('battle') == self.battle_count and data.get('boss', 0): - logger.info('Catch camera re-positioning after boss appear') - appear = True - - # if self.config.POOR_MAP_DATA: - # self.device.screenshot() - # grids = Grids(self.device.image, config=self.config) - # grids.predict() - # grids.show() - # for grid in grids: - # if grid.is_boss: - # logger.info('Catch camera re-positioning after boss appear') - # appear = True - # for g in self.map: - # g.wipe_out() - # break - - if appear: - camera = self.camera - self.ensure_edge_insight() - logger.info('Refocus to previous camera position.') - self.focus_to(camera) - return True - else: - return False - - def fleet_checked_reset(self): - self.map_fleet_checked = False - self.fleet_1_formation_fixed = False - self.fleet_2_formation_fixed = False +import itertools + +import numpy as np + +import module.config.server as server +from module.base.timer import Timer +from module.exception import MapWalkError, MapEnemyMoved +from module.handler.ambush import AmbushHandler +from module.logger import logger +from module.map.camera import Camera +from module.map.map_base import SelectedGrids +from module.map.map_base import location2node, location_ensure +from module.map.utils import match_movable + + +class Fleet(Camera, AmbushHandler): + fleet_1_location = () + fleet_2_location = () + fleet_current_index = 1 + battle_count = 0 + mystery_count = 0 + siren_count = 0 + fleet_ammo = 5 + ammo_count = 3 + + @property + def fleet_1(self): + if self.fleet_current_index != 1: + self.fleet_switch() + return self + + @fleet_1.setter + def fleet_1(self, value): + self.fleet_1_location = value + + @property + def fleet_2(self): + if self.fleet_current_index != 2: + self.fleet_switch() + return self + + @fleet_2.setter + def fleet_2(self, value): + self.fleet_2_location = value + + @property + def fleet_current(self): + if self.fleet_current_index == 2: + return self.fleet_2_location + else: + return self.fleet_1_location + + @property + def fleet_boss(self): + if self.config.FLEET_BOSS == 2 or self.config.FLEET_2: + return self.fleet_2 + else: + return self.fleet_1 + + @property + def fleet_boss_index(self): + if self.config.FLEET_BOSS == 2 or self.config.FLEET_2: + return 2 + else: + return 1 + + @property + def fleet_step(self): + if not self.config.MAP_HAS_FLEET_STEP: + return 0 + if self.fleet_current_index == 2: + return self.config.FLEET_2_STEP + else: + return self.config.FLEET_1_STEP + + def fleet_switch(self): + self.fleet_switch_click() + self.fleet_current_index = 1 if self.fleet_current_index == 2 else 2 + if server.server == 'jp': + # [JP] After fleet switch, camera don't focus on fleet, but about 0.75 grids higher than grid center. + # So need to correct camera position. + self.ensure_edge_insight() + else: + self.camera = self.fleet_current + self.update() + self.find_path_initial() + self.map.show_cost() + self.show_fleet() + self.hp_get() + self.lv_get() + self.handle_strategy(index=self.fleet_current_index) + + def switch_to(self): + pass + + round = 0 + enemy_round = {} + + def round_next(self): + """ + Call this method after fleet arrived. + """ + if not self.config.MAP_HAS_MOVABLE_ENEMY: + return False + self.round += 1 + logger.info(f'Round: {self.round}, enemy_round: {self.enemy_round}') + + def round_battle(self): + """ + Call this method after cleared an enemy. + """ + if not self.config.MAP_HAS_MOVABLE_ENEMY: + return False + if not self.map.select(is_siren=True): + self.enemy_round = {} + try: + data = self.map.spawn_data[self.battle_count] + except IndexError: + data = {} + enemy = data.get('siren', 0) + if enemy > 0: + r = self.round + self.enemy_round[r] = self.enemy_round.get(r, 0) + enemy + + def round_reset(self): + """ + Call this method after entering map. + """ + self.round = 0 + self.enemy_round = {} + + @property + def round_is_new(self): + """ + Usually, MOVABLE_ENEMY_TURN = 2. + So a walk round is `player - player - enemy`, player moves twice, enemy moves once. + + Different sirens have different MOVABLE_ENEMY_TURN: + 2: Non-siren elite, SIREN_CL + 3: SIREN_CA + + Returns: + bool: If it's a new walk round, which means enemies have moved. + """ + if not self.config.MAP_HAS_MOVABLE_ENEMY: + return False + for enemy in self.enemy_round.keys(): + for turn in self.config.MOVABLE_ENEMY_TURN: + if self.round - enemy > 0 and (self.round - enemy) % turn == 0: + return True + + return False + + @property + def round_wait(self): + """ + Returns: + float: Seconds to wait enemies moving. + """ + if not self.config.MAP_HAS_MOVABLE_ENEMY: + return 0 + count = 0 + for enemy, c in self.enemy_round.items(): + for turn in self.config.MOVABLE_ENEMY_TURN: + if self.round + 1 - enemy > 0 and (self.round + 1 - enemy) % turn == 0: + count += c + break + + return count * self.config.MAP_SIREN_MOVE_WAIT + + movable_before: SelectedGrids + + def _goto(self, location, expected=''): + """Goto a grid directly and handle ambush, air raid, mystery picked up, combat. + + Args: + location (tuple, str, GridInfo): Destination. + """ + location = location_ensure(location) + result_mystery = '' + self.movable_before = self.map.select(is_siren=True) + if self.hp_withdraw_triggered(): + self.withdraw() + is_portal = self.map[location].is_portal + + while 1: + sight = self.map.camera_sight + self.in_sight(location, sight=(sight[0], 0, sight[2], sight[3])) + self.focus_to_grid_center() + grid = self.convert_map_to_grid(location) + + self.ambush_color_initial() + self.enemy_searching_color_initial() + grid.__str__ = location + result = 'nothing' + self.device.click(grid) + arrived = False + # Wait to confirm fleet arrived. It does't appear immediately if fleet in combat. + extra = 0 + if self.config.SUBMARINE_MODE == 'hunt_only': + extra += 4.5 + if self.config.MAP_HAS_LAND_BASED and grid.is_mechanism_trigger: + extra += grid.mechanism_wait + arrive_timer = Timer(0.5 + self.round_wait + extra, count=2) + arrive_unexpected_timer = Timer(1.5 + self.round_wait + extra, count=6) + # Wait after ambushed. + ambushed_retry = Timer(0.5) + # If nothing happens, click again. + walk_timeout = Timer(20) + walk_timeout.start() + + while 1: + self.device.screenshot() + grid.image = np.array(self.device.image) + if is_portal: + self.update() + grid = self.view[self.view.center_loca] + + # Combat + if self.config.ENABLE_MAP_FLEET_LOCK and not self.is_in_map(): + if self.handle_retirement(): + self.map_offensive() + walk_timeout.reset() + if self.handle_combat_low_emotion(): + walk_timeout.reset() + if self.combat_appear(): + self.combat(expected_end=self._expected_combat_end(expected), fleet_index=self.fleet_current_index) + self.hp_get() + self.lv_get(after_battle=True) + arrived = True if not self.config.MAP_HAS_MOVABLE_ENEMY else False + result = 'combat' + self.battle_count += 1 + self.fleet_ammo -= 1 + if 'siren' in expected or (self.config.MAP_HAS_MOVABLE_ENEMY and not expected): + self.siren_count += 1 + elif self.map[location].may_enemy: + self.map[location].is_cleared = True + + self.handle_boss_appear_refocus() + grid = self.convert_map_to_grid(location) + walk_timeout.reset() + + # Ambush + if self.handle_ambush(): + self.hp_get() + self.lv_get(after_battle=True) + ambushed_retry.start() + walk_timeout.reset() + + # Mystery + mystery = self.handle_mystery(button=grid) + if mystery: + self.mystery_count += 1 + result = 'mystery' + result_mystery = mystery + + # Cat attack animation + if self.handle_map_cat_attack(): + walk_timeout.reset() + continue + + if self.handle_walk_out_of_step(): + raise MapWalkError('walk_out_of_step') + + # Arrive + if self.is_in_map() and \ + (grid.predict_fleet() or + (walk_timeout.reached() and grid.predict_current_fleet())): + if not arrive_timer.started(): + logger.info(f'Arrive {location2node(location)}') + arrive_timer.start() + arrive_unexpected_timer.start() + if not arrive_timer.reached(): + continue + if expected and result not in expected: + if arrive_unexpected_timer.reached(): + logger.warning('Arrive with unexpected result') + else: + continue + if is_portal: + location = self.map[location].portal_link + self.camera = location + logger.info(f'Arrive {location2node(location)} confirm. Result: {result}. Expected: {expected}') + arrived = True + break + + # Story + if expected == 'story': + if self.handle_story_skip(): + result = 'story' + continue + + # End + if ambushed_retry.started() and ambushed_retry.reached(): + break + if walk_timeout.reached(): + logger.warning('Walk timeout. Retrying.') + self.ensure_edge_insight() + break + + # End + if arrived: + # Ammo grid needs to click again, otherwise the next click doesn't work. + if self.map[location].may_ammo: + self.device.click(grid) + break + + self.map[self.fleet_current].is_fleet = False + self.map[location].wipe_out() + self.map[location].is_fleet = True + self.__setattr__('fleet_%s_location' % self.fleet_current_index, location) + if result_mystery == 'get_carrier': + self.full_scan_carrier() + if result == 'combat': + self.round_battle() + self.round_next() + if self.round_is_new: + self.full_scan_movable(enemy_cleared=result == 'combat') + self.find_path_initial() + raise MapEnemyMoved + self.find_path_initial() + + def goto(self, location, optimize=True, expected=''): + # self.device.sleep(1000) + location = location_ensure(location) + if (self.config.MAP_HAS_AMBUSH or self.config.MAP_HAS_FLEET_STEP or self.config.MAP_HAS_PORTAL) and optimize: + nodes = self.map.find_path(location, step=self.fleet_step) + for node in nodes: + try: + self._goto(node, expected=expected if node == nodes[-1] else '') + except MapWalkError: + logger.warning('Map walk error.') + self.ensure_edge_insight() + nodes_ = self.map.find_path(node, step=1) + for node_ in nodes_: + self._goto(node_, expected=expected if node == nodes[-1] else '') + else: + self._goto(location, expected=expected) + + def find_path_initial(self): + """ + Call this method after fleet moved or entered map. + """ + if self.fleet_1_location: + self.map[self.fleet_1_location].is_fleet = True + if self.fleet_2_location: + self.map[self.fleet_2_location].is_fleet = True + location_dict = {} + if self.config.FLEET_2: + location_dict[2] = self.fleet_2_location + location_dict[1] = self.fleet_1_location + self.map.find_path_initial_multi_fleet( + location_dict, current=self.fleet_current, has_ambush=self.config.MAP_HAS_AMBUSH) + + def show_fleet(self): + fleets = [] + for n in [1, 2]: + fleet = self.__getattribute__('fleet_%s_location' % n) + if len(fleet): + text = 'Fleet_%s: %s' % (n, location2node(fleet)) + if self.fleet_current_index == n: + text = '[%s]' % text + fleets.append(text) + logger.info(' '.join(fleets)) + + def full_scan(self, queue=None, must_scan=None, mode='normal'): + super().full_scan( + queue=queue, must_scan=must_scan, battle_count=self.battle_count, mystery_count=self.mystery_count, + siren_count=self.siren_count, carrier_count=self.carrier_count, mode=mode) + + if self.config.FLEET_2 and not self.fleet_2_location: + fleets = self.map.select(is_fleet=True, is_current_fleet=False) + if fleets.count: + logger.info(f'Predict fleet_2 to be {fleets[0]}') + self.fleet_2_location = fleets[0].location + + for loca in [self.fleet_1_location, self.fleet_2_location]: + if len(loca) and loca in self.map: + grid = self.map[loca] + if grid.may_boss and grid.is_caught_by_siren: + # Only boss appears on fleet's face + pass + else: + self.map[loca].wipe_out() + + def full_scan_carrier(self): + """ + Call this method if get enemy searching in mystery. + """ + prev = self.map.select(is_enemy=True) + self.full_scan(mode='carrier') + diff = self.map.select(is_enemy=True).delete(prev) + logger.info(f'Carrier spawn: {diff}') + + def full_scan_movable(self, enemy_cleared=True): + """ + Call this method if enemy moved. + + Args: + enemy_cleared (bool): True if cleared an enemy and need to scan spawn enemies. + False if just a simple walk and only need to scan movable enemies. + """ + before = self.movable_before + for grid in before: + grid.wipe_out() + + self.full_scan(queue=None if enemy_cleared else before, must_scan=before, mode='movable') + + # Track siren moving + after = self.map.select(is_siren=True) + step = self.config.MOVABLE_ENEMY_FLEET_STEP + matched_before, matched_after = match_movable( + before=before.location, + spawn=self.map.select(may_siren=True).location, + after=after.location, + fleets=[self.fleet_current] if enemy_cleared else [], + fleet_step=step + ) + matched_before = self.map.to_selected(matched_before) + matched_after = self.map.to_selected(matched_after) + logger.info(f'Movable enemy {before} -> {after}') + logger.info(f'Tracked enemy {matched_before} -> {matched_after}') + + # Delete wrong prediction + for grid in after.delete(matched_after): + if not grid.may_siren: + logger.warning(f'Wrong detection: {grid}') + grid.wipe_out() + + # Predict missing siren + diff = before.delete(matched_before) + _, missing = self.map.missing_get( + self.battle_count, self.mystery_count, self.siren_count, self.carrier_count, mode='normal') + if diff and missing['siren'] != 0: + logger.warning(f'Movable enemy tracking lost: {diff}') + covered = self.map.grid_covered(self.map[self.fleet_current], location=[(0, -2)]) + if self.fleet_1_location: + covered = covered.add(self.map.grid_covered(self.map[self.fleet_1_location], location=[(0, -1)])) + if self.fleet_2_location: + covered = covered.add(self.map.grid_covered(self.map[self.fleet_2_location], location=[(0, -1)])) + for grid in after: + covered = covered.add(self.map.grid_covered(grid)) + logger.attr('enemy_covered', covered) + accessible = SelectedGrids([]) + for grid in diff: + self.map.find_path_initial(grid, has_ambush=False) + accessible = accessible.add(self.map.select(cost=0)) \ + .add(self.map.select(cost=1)).add(self.map.select(cost=2)) + self.map.find_path_initial(self.fleet_current, has_ambush=self.config.MAP_HAS_AMBUSH) + logger.attr('enemy_accessible', accessible) + predict = accessible.intersect(covered).select(is_sea=True, is_fleet=False) + logger.info(f'Movable enemy predict: {predict}') + for grid in predict: + grid.is_siren = True + grid.is_enemy = True + elif missing['siren'] == 0: + logger.info(f'Movable enemy tracking drop: {diff}') + + for grid in matched_after: + if grid.location != self.fleet_current: + grid.is_movable = True + + def find_all_fleets(self): + logger.hr('Find all fleets') + queue = self.map.select(is_spawn_point=True) + while queue: + queue = queue.sort_by_camera_distance(self.camera) + self.in_sight(queue[0], sight=(-1, 0, 1, 2)) + grid = self.convert_map_to_grid(queue[0]) + if grid.predict_fleet(): + if grid.predict_current_fleet(): + self.fleet_1 = queue[0].location + else: + self.fleet_2 = queue[0].location + queue = queue[1:] + + def find_current_fleet(self): + logger.hr('Find current fleet') + if not self.config.POOR_MAP_DATA: + fleets = self.map.select(is_fleet=True, is_spawn_point=True) + else: + fleets = self.map.select(is_fleet=True) + logger.info('Fleets: %s' % str(fleets)) + count = fleets.count + if count == 1: + if not self.config.FLEET_2: + self.fleet_1 = fleets[0].location + else: + logger.info('Fleet_2 not detected.') + if self.config.POOR_MAP_DATA and not self.map.select(is_spawn_point=True): + self.fleet_1 = fleets[0].location + elif self.map.select(is_spawn_point=True).count == 2: + logger.info('Predict fleet to be spawn point') + another = self.map.select(is_spawn_point=True).delete(SelectedGrids([fleets[0]]))[0] + if fleets[0].is_current_fleet: + self.fleet_1 = fleets[0].location + self.fleet_2 = another.location + else: + self.fleet_1 = another.location + self.fleet_2 = fleets[0].location + else: + cover = self.map.grid_covered(fleets[0], location=[(0, -1)]) + if fleets[0].is_current_fleet and len(cover) and cover[0].is_spawn_point: + self.fleet_1 = fleets[0].location + self.fleet_2 = cover[0].location + else: + self.find_all_fleets() + elif count == 2: + current = self.map.select(is_current_fleet=True) + if current.count == 1: + self.fleet_1 = current[0].location + self.fleet_2 = fleets.delete(current)[0].location + else: + fleets = fleets.sort_by_camera_distance(self.camera) + self.in_sight(fleets[0], sight=(-1, 0, 1, 2)) + if self.convert_map_to_grid(fleets[0]).predict_current_fleet(): + self.fleet_1 = fleets[0].location + self.fleet_2 = fleets[1].location + else: + self.in_sight(fleets[1], sight=(-1, 0, 1, 2)) + if self.convert_map_to_grid(fleets[1]).predict_current_fleet(): + self.fleet_1 = fleets[1].location + self.fleet_2 = fleets[0].location + else: + logger.warning('Current fleet not found') + self.fleet_1 = fleets[0].location + self.fleet_2 = fleets[1].location + else: + if count == 0: + logger.warning('No fleets detected.') + fleets = self.map.select(is_current_fleet=True) + if fleets.count: + self.fleet_1 = fleets[0].location + if count > 2: + logger.warning('Too many fleets: %s.' % str(fleets)) + self.find_all_fleets() + + self.fleet_current_index = 1 + self.show_fleet() + return self.fleet_current + + def map_init(self, map_): + logger.hr('Map init') + self.fleet_1_location = () + self.fleet_2_location = () + self.fleet_current_index = 1 + self.battle_count = 0 + self.mystery_count = 0 + self.carrier_count = 0 + self.siren_count = 0 + self.ammo_count = 3 + self.map = map_ + self.map.reset() + self.handle_map_green_config_cover() + self.map.poor_map_data = self.config.POOR_MAP_DATA + self.map.load_map_data(use_loop=self.map_is_clear_mode) + self.map.load_mechanism(land_based=self.config.MAP_HAS_LAND_BASED) + self.map.grid_connection_initial( + wall=self.config.MAP_HAS_WALL, + portal=self.config.MAP_HAS_PORTAL, + ) + + self.handle_strategy(index=1 if not self.fleets_reversed() else 2) + self.update() + if self.handle_fleet_reverse(): + self.handle_strategy(index=1) + self.hp_reset() + self.hp_get() + self.lv_reset() + self.lv_get() + self.ensure_edge_insight(preset=self.map.in_map_swipe_preset_data) + self.full_scan(must_scan=self.map.camera_data_spawn_point) + self.find_current_fleet() + self.find_path_initial() + self.map.show_cost() + self.round_reset() + self.round_battle() + + def handle_map_green_config_cover(self): + if not self.map_is_green: + return False + + if self.config.POOR_MAP_DATA and self.map.is_map_data_poor: + self.config.POOR_MAP_DATA = False + + return True + + def _expected_combat_end(self, expected): + for data in self.map.spawn_data: + if data.get('battle') == self.battle_count and 'boss' in expected: + return 'in_stage' + if data.get('battle') == self.battle_count + 1: + if data.get('enemy', 0) + data.get('siren', 0) + data.get('boss', 0) > 0: + return 'with_searching' + else: + return 'no_searching' + + if 'boss' in expected: + return 'in_stage' + + return None + + def fleet_at(self, grid, fleet=None): + """ + Args: + grid (Grid): + fleet (int): 1, 2 + + Returns: + bool: If fleet is at grid. + """ + if fleet is None: + return self.fleet_current == grid.location + if fleet == 1: + return self.fleet_1_location == grid.location + else: + return self.fleet_2_location == grid.location + + def check_accessibility(self, grid, fleet=None): + """ + Args: + grid (Grid): + fleet (int, str): 1, 2, 'boss' + + Returns: + bool: If accessible. + """ + if fleet is None: + return grid.is_accessible + if isinstance(fleet, str) and fleet.isdigit(): + fleet = int(fleet) + if fleet == 'boss': + fleet = self.fleet_boss_index + + if fleet == self.fleet_current_index: + return grid.is_accessible + else: + backup = self.fleet_current_index + self.fleet_current_index = fleet + self.find_path_initial() + result = grid.is_accessible + + self.fleet_current_index = backup + self.find_path_initial() + return result + + def brute_find_roadblocks(self, grid, fleet=None): + """ + Args: + grid (Grid): + fleet (int): 1, 2. Default to current fleet. + + Returns: + SelectedGrids: + """ + if fleet is not None and fleet != self.fleet_current_index: + backup = self.fleet_current_index + self.fleet_current_index = fleet + self.find_path_initial() + else: + backup = None + + if grid.is_accessible: + if backup is not None: + self.fleet_current_index = backup + self.find_path_initial() + return SelectedGrids([]) + + enemies = self.map.select(is_enemy=True) + logger.info(f'Potential enemy roadblocks: {enemies}') + for repeat in range(1, enemies.count + 1): + for select in itertools.product(enemies, repeat=repeat): + for block in select: + block.is_enemy = False + self.find_path_initial() + for block in select: + block.is_enemy = True + + if grid.is_accessible: + select = SelectedGrids(list(select)) + logger.info(f'Enemy roadblock: {select}') + if backup is not None: + self.fleet_current_index = backup + self.find_path_initial() + return select + + logger.warning('Enemy roadblock try exhausted.') + + def handle_boss_appear_refocus(self): + """ + + """ + appear = False + for data in self.map.spawn_data: + if data.get('battle') == self.battle_count and data.get('boss', 0): + logger.info('Catch camera re-positioning after boss appear') + appear = True + + # if self.config.POOR_MAP_DATA: + # self.device.screenshot() + # grids = Grids(self.device.image, config=self.config) + # grids.predict() + # grids.show() + # for grid in grids: + # if grid.is_boss: + # logger.info('Catch camera re-positioning after boss appear') + # appear = True + # for g in self.map: + # g.wipe_out() + # break + + if appear: + camera = self.camera + self.ensure_edge_insight() + logger.info('Refocus to previous camera position.') + self.focus_to(camera) + return True + else: + return False + + def fleet_checked_reset(self): + self.map_fleet_checked = False + self.fleet_1_formation_fixed = False + self.fleet_2_formation_fixed = False diff --git a/module/map/map.py b/module/map/map.py index 186f0debb..8285ef78f 100644 --- a/module/map/map.py +++ b/module/map/map.py @@ -1,552 +1,581 @@ -from module.logger import logger -from module.map.fleet import Fleet -from module.map.map_grids import SelectedGrids, RoadGrids -from module.map_detection.grid_info import GridInfo - - -class Map(Fleet): - def clear_chosen_enemy(self, grid, expected=''): - """ - Args: - grid (GridInfo): - expected (str): - """ - logger.info('Clear enemy: %s' % grid) - expected = f'combat_{expected}' if expected else 'combat' - self.show_fleet() - if self.config.ENABLE_EMOTION_REDUCE and self.config.ENABLE_MAP_FLEET_LOCK: - self.emotion.wait(fleet=self.fleet_current_index) - self.goto(grid, expected=expected) - - self.full_scan() - self.find_path_initial() - self.map.show_cost() - - def clear_chosen_mystery(self, grid): - """ - Args: - grid (GridInfo): - """ - logger.info('Clear mystery: %s' % grid) - self.show_fleet() - self.goto(grid, expected='mystery') - # self.mystery_count += 1 - self.map.show_cost() - - def pick_up_ammo(self, grid=None): - """ - Args: - grid (GridInfo): - """ - if grid is None: - grid = self.map.select(may_ammo=True) - if not grid: - logger.info('Map has no ammo.') - return False - grid = grid[0] - - if self.ammo_count > 0 and grid.is_accessible: - logger.info('Pick up ammo: %s' % grid) - self.goto(grid, expected='') - self.ensure_no_info_bar() - - # self.ammo_count -= 5 - self.battle_count - recover = 5 - self.fleet_ammo - recover = 3 if recover > 3 else recover - logger.attr('Got ammo', recover) - - self.ammo_count -= recover - self.fleet_ammo += recover - - @staticmethod - def select_grids(grids, nearby=False, is_accessible=True, scale=(), genre=(), strongest=False, weakest=False, - sort=('weight', 'cost'), ignore=None): - """ - Args: - grids (SelectedGrids): - nearby (bool): - is_accessible (bool): - scale (tuple[int], list[int]): Tuple: select out of order, list: select in order. - genre (tuple[str], list[str]): light, main, carrier, treasure. (Case insensitive). - strongest (bool): - weakest (bool): - sort (tuple(str)): - ignore (SelectedGrids): - - Returns: - SelectedGrids: - """ - if nearby: - grids = grids.select(is_nearby=True) - if is_accessible: - grids = grids.select(is_accessible=True) - if ignore is not None: - grids = grids.delete(grids=ignore) - if len(scale): - enemy = SelectedGrids([]) - for enemy_scale in scale: - enemy = enemy.add(grids.select(enemy_scale=enemy_scale)) - if isinstance(scale, list) and enemy: - break - grids = enemy - if len(genre): - enemy = SelectedGrids([]) - for enemy_genre in genre: - enemy = enemy.add(grids.select(enemy_genre=enemy_genre.capitalize())) - if isinstance(genre, list) and enemy: - break - grids = enemy - if strongest: - for scale in [3, 2, 1, 0]: - enemy = grids.select(enemy_scale=scale) - if enemy: - grids = enemy - break - if weakest: - for scale in [1, 2, 3, 0]: - enemy = grids.select(enemy_scale=scale) - if enemy: - grids = enemy - break - - if grids: - grids = grids.sort(*sort) - - return grids - - @staticmethod - def show_select_grids(grids, **kwargs): - length = 3 - keys = list(kwargs.keys()) - for index in range(0, len(keys), length): - text = [f'{key}={kwargs[key]}' for key in keys[index:index + length]] - text = ', '.join(text) - logger.info(text) - - logger.info(f'Grids: {grids}') - - def clear_all_mystery(self, **kwargs): - """Methods to pick up all mystery. - - Returns: - bool: False, because didn't clear any enemy. - """ - kwargs['sort'] = ('cost',) - while 1: - grids = self.map.select(is_mystery=True) - grids = self.select_grids(grids, **kwargs) - - if not grids: - break - - logger.hr('Clear all mystery') - self.show_select_grids(grids, **kwargs) - self.clear_chosen_mystery(grids[0]) - - return False - - def clear_enemy(self, **kwargs): - """Methods to clear a enemy. May not do anything if no suitable enemy. - - Returns: - bool: True if clear an enemy. - """ - grids = self.map.select(is_enemy=True, is_boss=False) - grids = self.select_grids(grids, **kwargs) - - if grids: - logger.hr('Clear enemy') - self.show_select_grids(grids, **kwargs) - self.clear_chosen_enemy(grids[0]) - return True - - return False - - def clear_roadblocks(self, roads, **kwargs): - """Clear roadblocks. - - Args: - roads(list[RoadGrids]): - - Returns: - bool: True if clear an enemy. - """ - grids = SelectedGrids([]) - for road in roads: - grids = grids.add(road.roadblocks()) - - grids = self.select_grids(grids, **kwargs) - - if grids: - logger.hr('Clear roadblock') - self.show_select_grids(grids, **kwargs) - self.clear_chosen_enemy(grids[0]) - return True - - return False - - def clear_potential_roadblocks(self, roads, **kwargs): - """Avoid roadblock that only has one grid empty. - - Args: - roads(list[RoadGrids]): - - Returns: - bool: True if clear an enemy. - """ - grids = SelectedGrids([]) - for road in roads: - grids = grids.add(road.potential_roadblocks()) - - grids = self.select_grids(grids, **kwargs) - - if grids: - logger.hr('Avoid potential roadblock') - self.show_select_grids(grids, **kwargs) - self.clear_chosen_enemy(grids[0]) - return True - - return False - - def clear_first_roadblocks(self, roads, **kwargs): - """Ensure every roadblocks have one grid with is_cleared=True. - - Args: - roads(list[RoadGrids]): - - Returns: - bool: True if clear an enemy. - """ - grids = SelectedGrids([]) - for road in roads: - grids = grids.add(road.first_roadblocks()) - - grids = self.select_grids(grids, **kwargs) - - if grids: - logger.hr('Clear first roadblock') - self.show_select_grids(grids, **kwargs) - self.clear_chosen_enemy(grids[0]) - return True - - return False - - def clear_grids_for_faster(self, grids, **kwargs): - """Clear some grids to walk a shorter distance. - - Args: - grids(SelectedGrids): - - Returns: - bool: True if clear an enemy. - """ - - grids = grids.select(is_enemy=True) - grids = self.select_grids(grids, **kwargs) - - if grids: - logger.hr('Clear grids for faster') - self.show_select_grids(grids, **kwargs) - self.clear_chosen_enemy(grids[0]) - return True - - return False - - def clear_boss(self): - """This method is deprecated, although it works well in simple map. - In a complex map, brute_clear_boss is recommended. - - Returns: - bool: - """ - grids = self.map.select(is_boss=True, is_accessible=True) - grids = grids.add(self.map.select(may_boss=True, is_caught_by_siren=True)) - logger.info('Is boss: %s' % grids) - if not grids.count: - grids = grids.add(self.map.select(may_boss=True, is_enemy=True, is_accessible=True)) - logger.warning('Boss not detected, using may_boss grids.') - logger.info('May boss: %s' % self.map.select(may_boss=True)) - logger.info('May boss and is enemy: %s' % self.map.select(may_boss=True, is_enemy=True)) - - if grids: - logger.hr('Clear BOSS') - grids = grids.sort('weight', 'cost') - logger.info('Grids: %s' % str(grids)) - self.clear_chosen_enemy(grids[0]) - - logger.warning('BOSS not detected, trying all boss spawn point.') - return self.clear_potential_boss() - - def capture_clear_boss(self): - """This method is deprecated, although it works well in simple map. - In a complex map, brute_clear_boss is recommended. - Note: Lazy method to handle with grand capture map - - Returns: - bool: - """ - - grids = self.map.select(is_boss=True, is_accessible=True) - grids = grids.add(self.map.select(may_boss=True, is_caught_by_siren=True)) - logger.info('Is boss: %s' % grids) - if not grids.count: - grids = grids.add(self.map.select(may_boss=True, is_enemy=True, is_accessible=True)) - logger.warning('Boss not detected, using may_boss grids.') - logger.info('May boss: %s' % self.map.select(may_boss=True)) - logger.info('May boss and is enemy: %s' % self.map.select(may_boss=True, is_enemy=True)) - - if grids: - logger.hr('Clear BOSS') - grids = grids.sort('weight', 'cost') - logger.info('Grids: %s' % str(grids)) - self.clear_chosen_enemy(grids[0]) - - logger.warning('Grand Capture detected, Withdrawing.') - self.withdraw() - - def clear_potential_boss(self): - """ - Method to step on all boss spawn point when boss not detected. - """ - grids = self.map.select(may_boss=True, is_accessible=True) - logger.info('May boss: %s' % grids) - battle_count = self.battle_count - - for grid in grids: - logger.hr('Clear potential BOSS') - grids = grids.sort('weight', 'cost') - logger.info('Grid: %s' % str(grid)) - self.fleet_boss.clear_chosen_enemy(grid) - if self.battle_count > battle_count: - logger.info('Boss guessing correct.') - return True - else: - logger.info('Boss guessing incorrect.') - - grids = self.map.select(may_boss=True, is_accessible=False) - logger.info('May boss: %s' % grids) - - for grid in grids: - logger.hr('Clear potential BOSS roadblocks') - fleet = 2 if self.config.FLEET_BOSS == 2 and self.config.FLEET_2 else 1 - roadblocks = self.brute_find_roadblocks(grid, fleet=fleet) - roadblocks = roadblocks.sort('weight', 'cost') - logger.info('Grids: %s' % str(roadblocks)) - self.fleet_1.clear_chosen_enemy(roadblocks[0]) - return True - - return False - - def brute_clear_boss(self): - """ - Method to clear boss, using brute-force to find roadblocks. - Note: This method will use 2 fleets. - """ - boss = self.map.select(is_boss=True) - if boss: - logger.info('Brute clear BOSS') - fleet = 2 if self.config.FLEET_BOSS == 2 and self.config.FLEET_2 else 1 - grids = self.brute_find_roadblocks(boss[0], fleet=fleet) - if grids: - if self.brute_fleet_meet(): - return True - logger.info('Brute clear BOSS roadblocks') - grids = grids.sort('weight', 'cost') - logger.info('Grids: %s' % str(grids)) - self.clear_chosen_enemy(grids[0]) - return True - else: - return self.fleet_boss.clear_boss() - elif self.map.select(may_boss=True, is_caught_by_siren=True): - logger.info('BOSS appear on fleet grid') - self.fleet_2.switch_to() - return self.clear_chosen_enemy(self.map.select(may_boss=True, is_caught_by_siren=True)[0]) - else: - logger.warning('BOSS not detected, trying all boss spawn point.') - return self.clear_potential_boss() - - def brute_fleet_meet(self): - """ - Method to clear roadblocks between fleets, using brute-force to find roadblocks. - """ - if not self.config.FLEET_2 or not self.fleet_2_location: - return False - grids = self.brute_find_roadblocks(self.map[self.fleet_2_location], fleet=1) - if grids: - logger.info('Brute clear roadblocks between fleets.') - grids = grids.sort('weight', 'cost') - logger.info('Grids: %s' % str(grids)) - self.clear_chosen_enemy(grids[0]) - return True - else: - return False - - def clear_siren(self, **kwargs): - """ - Returns: - bool: True if clear an enemy. - """ - if not self.config.MAP_HAS_SIREN: - return False - - if self.config.FLEET_2: - kwargs['sort'] = ('weight', 'cost_2') - grids = self.map.select(is_siren=True) - grids = self.select_grids(grids, **kwargs) - - if grids: - logger.hr('Clear siren') - self.show_select_grids(grids, **kwargs) - self.clear_chosen_enemy(grids[0], expected='siren') - return True - - return False - - def fleet_2_step_on(self, grids, roadblocks): - """Fleet step on a grid which can reduce the ambush frequency another fleet. - Of course, you can simply use 'self.fleet_2.goto(grid)' and do the same thing. - However, roads can be block by enemy and this method can handle that. - - Args: - grids (SelectedGrids): - roadblocks (list[RoadGrids]): - - Returns: - bool: if clear an enemy. - """ - if not self.config.FLEET_2: - return False - for grid in grids: - if self.fleet_at(grid=grid, fleet=2): - return False - # if grids.count == len([grid for grid in grids if grid.is_enemy or grid.is_cleared]): - # logger.info('Fleet 2 step on, no need') - # return False - all_cleared = grids.select(is_cleared=True).count == grids.count - - logger.info('Fleet 2 step on') - for grid in grids: - if grid.is_enemy or (not all_cleared and grid.is_cleared): - continue - if self.check_accessibility(grid=grid, fleet=2): - logger.info('Fleet_2 step on %s' % grid) - self.fleet_2.goto(grid) - self.fleet_1.switch_to() - return False - - logger.info('Fleet_2 step on got roadblocks.') - clear = self.fleet_1.clear_roadblocks(roadblocks) - self.fleet_1.clear_all_mystery() - return clear - - def fleet_2_break_siren_caught(self): - if not self.config.FLEET_2: - return False - if not self.config.MAP_HAS_SIREN or not self.config.MAP_HAS_MOVABLE_ENEMY: - return False - if not self.map.select(is_caught_by_siren=True): - logger.info('No fleet caught by siren.') - return False - if not self.fleet_2_location or not self.map[self.fleet_2_location].is_caught_by_siren: - logger.warning('Appear caught by siren, but not fleet_2.') - for grid in self.map: - grid.is_caught_by_siren = False - return False - - logger.info(f'Break siren caught, fleet_2: {self.fleet_2_location}') - self.fleet_2.switch_to() - self.ensure_edge_insight() - self.clear_chosen_enemy(self.map[self.fleet_2_location]) - self.fleet_1.switch_to() - for grid in self.map: - grid.is_caught_by_siren = False - return True - - def fleet_2_push_forward(self): - """Move fleet 2 to the grid with lower grid.weight - This will reduce the possibility of Boss fleet get stuck by enemies, especially for those one-way-road map - from chapter 7 to chapter 9. - - Know more (in Chinese simplified): - 9章道中战最小化路线规划 (Route Planning for battle minimization in chapter 9) - https://wiki.biligame.com/blhx/9%E7%AB%A0%E9%81%93%E4%B8%AD%E6%88%98%E6%9C%80%E5%B0%8F%E5%8C%96%E8%B7%AF%E7%BA%BF%E8%A7%84%E5%88%92 - - Returns: - bool: If pushed forward. - """ - if not self.config.FLEET_2: - return False - - logger.info('Fleet_2 push forward') - grids = self.map.select(is_land=False).sort('weight', 'cost') - if self.map[self.fleet_2_location].weight <= grids[0].weight: - logger.info('Fleet_2 pushed to destination') - self.fleet_1.switch_to() - return False - - fleets = SelectedGrids([self.map[self.fleet_1_location], self.map[self.fleet_2_location]]) - grids = grids.select(is_accessible_2=True, is_sea=True).delete(fleets) - if not grids: - logger.info('Fleet_2 has no where to push') - return False - if self.map[self.fleet_2_location].weight <= grids[0].weight: - logger.info('Fleet_2 pushed to closest grid') - return False - - logger.info(f'Grids: {grids}') - logger.info(f'Push forward: {grids[0]}') - self.fleet_2.goto(grids[0]) - self.fleet_1.switch_to() - return True - - def fleet_2_rescue(self, grid): - """Use mob fleet to rescue boss fleet. - - Args: - grid (GridInfo): Destination. Usually to be boss spawn grid. - - Returns: - bool: If clear an enemy. - """ - if not self.config.FLEET_2: - return False - - grids = self.brute_find_roadblocks(grid, fleet=2) - if not grids: - return False - logger.info('Fleet_2 rescue') - grids = self.select_grids(grids) - if not grids: - return False - - self.clear_chosen_enemy(grids[0]) - return True - - def fleet_2_protect(self): - """ - Mob fleet moves around boss fleet, clear any approaching sirens. - - Returns: - bool: If clear an enemy. - """ - if not self.config.FLEET_2 or not self.config.MAP_HAS_MOVABLE_ENEMY: - return False - - for n in range(20): - if not self.map.select(is_siren=True): - return False - - nearby = self.map.select(cost_2=1).add(self.map.select(cost_2=2)) - approaching = nearby.select(is_siren=True) - if approaching: - grids = self.select_grids(approaching, sort=('cost_2', 'cost_1')) - self.clear_chosen_enemy(grids[0], expected='siren') - return True - else: - grids = nearby.delete(self.map.select(is_fleet=True)) - grids = self.select_grids(grids, sort=('cost_2', 'cost_1')) - self.goto(grids[0]) - continue - - logger.warning('fleet_2_protect no siren approaching') - return False +from module.exception import MapEnemyMoved +from module.logger import logger +from module.map.fleet import Fleet +from module.map.map_grids import SelectedGrids, RoadGrids +from module.map_detection.grid_info import GridInfo + + +class Map(Fleet): + def clear_chosen_enemy(self, grid, expected=''): + """ + Args: + grid (GridInfo): + expected (str): + """ + logger.info('Clear enemy: %s' % grid) + expected = f'combat_{expected}' if expected else 'combat' + self.show_fleet() + if self.config.ENABLE_EMOTION_REDUCE and self.config.ENABLE_MAP_FLEET_LOCK: + self.emotion.wait(fleet=self.fleet_current_index) + self.goto(grid, expected=expected) + + self.full_scan() + self.find_path_initial() + self.map.show_cost() + + def clear_chosen_mystery(self, grid): + """ + Args: + grid (GridInfo): + """ + logger.info('Clear mystery: %s' % grid) + self.show_fleet() + self.goto(grid, expected='mystery') + # self.mystery_count += 1 + self.map.show_cost() + + def pick_up_ammo(self, grid=None): + """ + Args: + grid (GridInfo): + """ + if grid is None: + grid = self.map.select(may_ammo=True) + if not grid: + logger.info('Map has no ammo.') + return False + grid = grid[0] + + if self.ammo_count > 0 and grid.is_accessible: + logger.info('Pick up ammo: %s' % grid) + self.goto(grid, expected='') + self.ensure_no_info_bar() + + # self.ammo_count -= 5 - self.battle_count + recover = 5 - self.fleet_ammo + recover = 3 if recover > 3 else recover + logger.attr('Got ammo', recover) + + self.ammo_count -= recover + self.fleet_ammo += recover + + def clear_mechanism(self, grids=None): + """ + Args: + grids (SelectedGrids): Grids that triggers mechanism. If None, select all mechanism triggers. + + Returns: + bool: False, because didn't clear any enemy. + """ + if not self.config.MAP_HAS_LAND_BASED: + return False + + if not grids: + grids = self.map.select(is_mechanism_trigger=True, is_mechanism_block=False) + else: + grids = grids.select(is_mechanism_trigger=True, is_mechanism_block=False) + grids = self.select_grids(grids, is_accessible=True, sort=('weight', 'cost')) + + for grid in grids: + logger.info(f'Clear mechanism: {grid}') + self.goto(grid) + self.map.show_cost() + logger.info(f'Mechanism trigger release: {grid.mechanism_trigger}') + logger.info(f'Mechanism block release: {grid.mechanism_block}') + raise MapEnemyMoved + + logger.info('Mechanism all cleared') + return False + + @staticmethod + def select_grids(grids, nearby=False, is_accessible=True, scale=(), genre=(), strongest=False, weakest=False, + sort=('weight', 'cost'), ignore=None): + """ + Args: + grids (SelectedGrids): + nearby (bool): + is_accessible (bool): + scale (tuple[int], list[int]): Tuple: select out of order, list: select in order. + genre (tuple[str], list[str]): light, main, carrier, treasure. (Case insensitive). + strongest (bool): + weakest (bool): + sort (tuple(str)): + ignore (SelectedGrids): + + Returns: + SelectedGrids: + """ + if nearby: + grids = grids.select(is_nearby=True) + if is_accessible: + grids = grids.select(is_accessible=True) + if ignore is not None: + grids = grids.delete(grids=ignore) + if len(scale): + enemy = SelectedGrids([]) + for enemy_scale in scale: + enemy = enemy.add(grids.select(enemy_scale=enemy_scale)) + if isinstance(scale, list) and enemy: + break + grids = enemy + if len(genre): + enemy = SelectedGrids([]) + for enemy_genre in genre: + enemy = enemy.add(grids.select(enemy_genre=enemy_genre.capitalize())) + if isinstance(genre, list) and enemy: + break + grids = enemy + if strongest: + for scale in [3, 2, 1, 0]: + enemy = grids.select(enemy_scale=scale) + if enemy: + grids = enemy + break + if weakest: + for scale in [1, 2, 3, 0]: + enemy = grids.select(enemy_scale=scale) + if enemy: + grids = enemy + break + + if grids: + grids = grids.sort(*sort) + + return grids + + @staticmethod + def show_select_grids(grids, **kwargs): + length = 3 + keys = list(kwargs.keys()) + for index in range(0, len(keys), length): + text = [f'{key}={kwargs[key]}' for key in keys[index:index + length]] + text = ', '.join(text) + logger.info(text) + + logger.info(f'Grids: {grids}') + + def clear_all_mystery(self, **kwargs): + """Methods to pick up all mystery. + + Returns: + bool: False, because didn't clear any enemy. + """ + kwargs['sort'] = ('cost',) + while 1: + grids = self.map.select(is_mystery=True) + grids = self.select_grids(grids, **kwargs) + + if not grids: + break + + logger.hr('Clear all mystery') + self.show_select_grids(grids, **kwargs) + self.clear_chosen_mystery(grids[0]) + + return False + + def clear_enemy(self, **kwargs): + """Methods to clear a enemy. May not do anything if no suitable enemy. + + Returns: + bool: True if clear an enemy. + """ + grids = self.map.select(is_enemy=True, is_boss=False) + grids = self.select_grids(grids, **kwargs) + + if grids: + logger.hr('Clear enemy') + self.show_select_grids(grids, **kwargs) + self.clear_chosen_enemy(grids[0]) + return True + + return False + + def clear_roadblocks(self, roads, **kwargs): + """Clear roadblocks. + + Args: + roads(list[RoadGrids]): + + Returns: + bool: True if clear an enemy. + """ + grids = SelectedGrids([]) + for road in roads: + grids = grids.add(road.roadblocks()) + + grids = self.select_grids(grids, **kwargs) + + if grids: + logger.hr('Clear roadblock') + self.show_select_grids(grids, **kwargs) + self.clear_chosen_enemy(grids[0]) + return True + + return False + + def clear_potential_roadblocks(self, roads, **kwargs): + """Avoid roadblock that only has one grid empty. + + Args: + roads(list[RoadGrids]): + + Returns: + bool: True if clear an enemy. + """ + grids = SelectedGrids([]) + for road in roads: + grids = grids.add(road.potential_roadblocks()) + + grids = self.select_grids(grids, **kwargs) + + if grids: + logger.hr('Avoid potential roadblock') + self.show_select_grids(grids, **kwargs) + self.clear_chosen_enemy(grids[0]) + return True + + return False + + def clear_first_roadblocks(self, roads, **kwargs): + """Ensure every roadblocks have one grid with is_cleared=True. + + Args: + roads(list[RoadGrids]): + + Returns: + bool: True if clear an enemy. + """ + grids = SelectedGrids([]) + for road in roads: + grids = grids.add(road.first_roadblocks()) + + grids = self.select_grids(grids, **kwargs) + + if grids: + logger.hr('Clear first roadblock') + self.show_select_grids(grids, **kwargs) + self.clear_chosen_enemy(grids[0]) + return True + + return False + + def clear_grids_for_faster(self, grids, **kwargs): + """Clear some grids to walk a shorter distance. + + Args: + grids(SelectedGrids): + + Returns: + bool: True if clear an enemy. + """ + + grids = grids.select(is_enemy=True) + grids = self.select_grids(grids, **kwargs) + + if grids: + logger.hr('Clear grids for faster') + self.show_select_grids(grids, **kwargs) + self.clear_chosen_enemy(grids[0]) + return True + + return False + + def clear_boss(self): + """This method is deprecated, although it works well in simple map. + In a complex map, brute_clear_boss is recommended. + + Returns: + bool: + """ + grids = self.map.select(is_boss=True, is_accessible=True) + grids = grids.add(self.map.select(may_boss=True, is_caught_by_siren=True)) + logger.info('Is boss: %s' % grids) + if not grids.count: + grids = grids.add(self.map.select(may_boss=True, is_enemy=True, is_accessible=True)) + logger.warning('Boss not detected, using may_boss grids.') + logger.info('May boss: %s' % self.map.select(may_boss=True)) + logger.info('May boss and is enemy: %s' % self.map.select(may_boss=True, is_enemy=True)) + + if grids: + logger.hr('Clear BOSS') + grids = grids.sort('weight', 'cost') + logger.info('Grids: %s' % str(grids)) + self.clear_chosen_enemy(grids[0]) + + logger.warning('BOSS not detected, trying all boss spawn point.') + return self.clear_potential_boss() + + def capture_clear_boss(self): + """This method is deprecated, although it works well in simple map. + In a complex map, brute_clear_boss is recommended. + Note: Lazy method to handle with grand capture map + + Returns: + bool: + """ + + grids = self.map.select(is_boss=True, is_accessible=True) + grids = grids.add(self.map.select(may_boss=True, is_caught_by_siren=True)) + logger.info('Is boss: %s' % grids) + if not grids.count: + grids = grids.add(self.map.select(may_boss=True, is_enemy=True, is_accessible=True)) + logger.warning('Boss not detected, using may_boss grids.') + logger.info('May boss: %s' % self.map.select(may_boss=True)) + logger.info('May boss and is enemy: %s' % self.map.select(may_boss=True, is_enemy=True)) + + if grids: + logger.hr('Clear BOSS') + grids = grids.sort('weight', 'cost') + logger.info('Grids: %s' % str(grids)) + self.clear_chosen_enemy(grids[0]) + + logger.warning('Grand Capture detected, Withdrawing.') + self.withdraw() + + def clear_potential_boss(self): + """ + Method to step on all boss spawn point when boss not detected. + """ + grids = self.map.select(may_boss=True, is_accessible=True) + logger.info('May boss: %s' % grids) + battle_count = self.battle_count + + for grid in grids: + logger.hr('Clear potential BOSS') + grids = grids.sort('weight', 'cost') + logger.info('Grid: %s' % str(grid)) + self.fleet_boss.clear_chosen_enemy(grid) + if self.battle_count > battle_count: + logger.info('Boss guessing correct.') + return True + else: + logger.info('Boss guessing incorrect.') + + grids = self.map.select(may_boss=True, is_accessible=False) + logger.info('May boss: %s' % grids) + + for grid in grids: + logger.hr('Clear potential BOSS roadblocks') + fleet = 2 if self.config.FLEET_BOSS == 2 and self.config.FLEET_2 else 1 + roadblocks = self.brute_find_roadblocks(grid, fleet=fleet) + roadblocks = roadblocks.sort('weight', 'cost') + logger.info('Grids: %s' % str(roadblocks)) + self.fleet_1.clear_chosen_enemy(roadblocks[0]) + return True + + return False + + def brute_clear_boss(self): + """ + Method to clear boss, using brute-force to find roadblocks. + Note: This method will use 2 fleets. + """ + boss = self.map.select(is_boss=True) + if boss: + logger.info('Brute clear BOSS') + fleet = 2 if self.config.FLEET_BOSS == 2 and self.config.FLEET_2 else 1 + grids = self.brute_find_roadblocks(boss[0], fleet=fleet) + if grids: + if self.brute_fleet_meet(): + return True + logger.info('Brute clear BOSS roadblocks') + grids = grids.sort('weight', 'cost') + logger.info('Grids: %s' % str(grids)) + self.clear_chosen_enemy(grids[0]) + return True + else: + return self.fleet_boss.clear_boss() + elif self.map.select(may_boss=True, is_caught_by_siren=True): + logger.info('BOSS appear on fleet grid') + self.fleet_2.switch_to() + return self.clear_chosen_enemy(self.map.select(may_boss=True, is_caught_by_siren=True)[0]) + else: + logger.warning('BOSS not detected, trying all boss spawn point.') + return self.clear_potential_boss() + + def brute_fleet_meet(self): + """ + Method to clear roadblocks between fleets, using brute-force to find roadblocks. + """ + if not self.config.FLEET_2 or not self.fleet_2_location: + return False + grids = self.brute_find_roadblocks(self.map[self.fleet_2_location], fleet=1) + if grids: + logger.info('Brute clear roadblocks between fleets.') + grids = grids.sort('weight', 'cost') + logger.info('Grids: %s' % str(grids)) + self.clear_chosen_enemy(grids[0]) + return True + else: + return False + + def clear_siren(self, **kwargs): + """ + Returns: + bool: True if clear an enemy. + """ + if not self.config.MAP_HAS_SIREN: + return False + + if self.config.FLEET_2: + kwargs['sort'] = ('weight', 'cost_2') + grids = self.map.select(is_siren=True) + grids = self.select_grids(grids, **kwargs) + + if grids: + logger.hr('Clear siren') + self.show_select_grids(grids, **kwargs) + self.clear_chosen_enemy(grids[0], expected='siren') + return True + + return False + + def fleet_2_step_on(self, grids, roadblocks): + """Fleet step on a grid which can reduce the ambush frequency another fleet. + Of course, you can simply use 'self.fleet_2.goto(grid)' and do the same thing. + However, roads can be block by enemy and this method can handle that. + + Args: + grids (SelectedGrids): + roadblocks (list[RoadGrids]): + + Returns: + bool: if clear an enemy. + """ + if not self.config.FLEET_2: + return False + for grid in grids: + if self.fleet_at(grid=grid, fleet=2): + return False + # if grids.count == len([grid for grid in grids if grid.is_enemy or grid.is_cleared]): + # logger.info('Fleet 2 step on, no need') + # return False + all_cleared = grids.select(is_cleared=True).count == grids.count + + logger.info('Fleet 2 step on') + for grid in grids: + if grid.is_enemy or (not all_cleared and grid.is_cleared): + continue + if self.check_accessibility(grid=grid, fleet=2): + logger.info('Fleet_2 step on %s' % grid) + self.fleet_2.goto(grid) + self.fleet_1.switch_to() + return False + + logger.info('Fleet_2 step on got roadblocks.') + clear = self.fleet_1.clear_roadblocks(roadblocks) + self.fleet_1.clear_all_mystery() + return clear + + def fleet_2_break_siren_caught(self): + if not self.config.FLEET_2: + return False + if not self.config.MAP_HAS_SIREN or not self.config.MAP_HAS_MOVABLE_ENEMY: + return False + if not self.map.select(is_caught_by_siren=True): + logger.info('No fleet caught by siren.') + return False + if not self.fleet_2_location or not self.map[self.fleet_2_location].is_caught_by_siren: + logger.warning('Appear caught by siren, but not fleet_2.') + for grid in self.map: + grid.is_caught_by_siren = False + return False + + logger.info(f'Break siren caught, fleet_2: {self.fleet_2_location}') + self.fleet_2.switch_to() + self.ensure_edge_insight() + self.clear_chosen_enemy(self.map[self.fleet_2_location]) + self.fleet_1.switch_to() + for grid in self.map: + grid.is_caught_by_siren = False + return True + + def fleet_2_push_forward(self): + """Move fleet 2 to the grid with lower grid.weight + This will reduce the possibility of Boss fleet get stuck by enemies, especially for those one-way-road map + from chapter 7 to chapter 9. + + Know more (in Chinese simplified): + 9章道中战最小化路线规划 (Route Planning for battle minimization in chapter 9) + https://wiki.biligame.com/blhx/9%E7%AB%A0%E9%81%93%E4%B8%AD%E6%88%98%E6%9C%80%E5%B0%8F%E5%8C%96%E8%B7%AF%E7%BA%BF%E8%A7%84%E5%88%92 + + Returns: + bool: If pushed forward. + """ + if not self.config.FLEET_2: + return False + + logger.info('Fleet_2 push forward') + grids = self.map.select(is_land=False).sort('weight', 'cost') + if self.map[self.fleet_2_location].weight <= grids[0].weight: + logger.info('Fleet_2 pushed to destination') + self.fleet_1.switch_to() + return False + + fleets = SelectedGrids([self.map[self.fleet_1_location], self.map[self.fleet_2_location]]) + grids = grids.select(is_accessible_2=True, is_sea=True).delete(fleets) + if not grids: + logger.info('Fleet_2 has no where to push') + return False + if self.map[self.fleet_2_location].weight <= grids[0].weight: + logger.info('Fleet_2 pushed to closest grid') + return False + + logger.info(f'Grids: {grids}') + logger.info(f'Push forward: {grids[0]}') + self.fleet_2.goto(grids[0]) + self.fleet_1.switch_to() + return True + + def fleet_2_rescue(self, grid): + """Use mob fleet to rescue boss fleet. + + Args: + grid (GridInfo): Destination. Usually to be boss spawn grid. + + Returns: + bool: If clear an enemy. + """ + if not self.config.FLEET_2: + return False + + grids = self.brute_find_roadblocks(grid, fleet=2) + if not grids: + return False + logger.info('Fleet_2 rescue') + grids = self.select_grids(grids) + if not grids: + return False + + self.clear_chosen_enemy(grids[0]) + return True + + def fleet_2_protect(self): + """ + Mob fleet moves around boss fleet, clear any approaching sirens. + + Returns: + bool: If clear an enemy. + """ + if not self.config.FLEET_2 or not self.config.MAP_HAS_MOVABLE_ENEMY: + return False + + for n in range(20): + if not self.map.select(is_siren=True): + return False + + nearby = self.map.select(cost_2=1).add(self.map.select(cost_2=2)) + approaching = nearby.select(is_siren=True) + if approaching: + grids = self.select_grids(approaching, sort=('cost_2', 'cost_1')) + self.clear_chosen_enemy(grids[0], expected='siren') + return True + else: + grids = nearby.delete(self.map.select(is_fleet=True)) + grids = self.select_grids(grids, sort=('cost_2', 'cost_1')) + self.goto(grids[0]) + continue + + logger.warning('fleet_2_protect no siren approaching') + return False diff --git a/module/map/map_base.py b/module/map/map_base.py index 9ca5a898a..7dd537246 100644 --- a/module/map/map_base.py +++ b/module/map/map_base.py @@ -1,617 +1,653 @@ -import copy - -from module.base.utils import location2node, node2location -from module.logger import logger -from module.map.map_grids import SelectedGrids -from module.map.utils import * -from module.map_detection.grid_info import GridInfo - - -class CampaignMap: - def __init__(self, name=None): - self.name = name - self.grids = {} - self._shape = (0, 0) - self._map_data = '' - self._map_data_loop = '' - self._weight_data = '' - self._wall_data = '' - self._portal_data = [] - self._spawn_data = [] - self._spawn_data_stack = [] - self._camera_data = [] - self._camera_data_spawn_point = [] - self._map_covered = SelectedGrids([]) - self.in_map_swipe_preset_data = None - self.poor_map_data = False - self.camera_sight = (-3, -1, 3, 2) - self.grid_connection = {} - - def __iter__(self): - return iter(self.grids.values()) - - def __getitem__(self, item): - """ - Args: - item: - - Returns: - GridInfo: - """ - return self.grids[tuple(item)] - - def __contains__(self, item): - return tuple(item) in self.grids - - @staticmethod - def _parse_text(text): - text = text.strip() - for y, row in enumerate(text.split('\n')): - row = row.strip() - for x, data in enumerate(row.split(' ')): - yield (x, y), data - - @property - def shape(self): - return self._shape - - @shape.setter - def shape(self, scale): - self._shape = node2location(scale.upper()) - for y in range(self._shape[1] + 1): - for x in range(self._shape[0] + 1): - grid = GridInfo() - grid.location = (x, y) - self.grids[(x, y)] = grid - - # camera_data can be generate automatically, but it's better to set it manually. - self.camera_data = [location2node(loca) for loca in camera_2d(self._shape, sight=self.camera_sight)] - self.camera_data_spawn_point = [] - # weight_data set to 10. - for grid in self: - grid.weight = 10. - - @property - def map_data(self): - return self._map_data - - @map_data.setter - def map_data(self, text): - self._map_data = text - self._load_map_data(text) - - @property - def map_data_loop(self): - return self._map_data_loop - - @map_data_loop.setter - def map_data_loop(self, text): - self._map_data_loop = text - - def load_map_data(self, use_loop=False): - """ - Args: - use_loop (bool): If at clearing mode. - clearing mode (Correct name) == fast forward (in old Alas) == loop (in lua files) - """ - has_loop = len(self.map_data_loop) - logger.info(f'Load map_data, has_loop={has_loop}, use_loop={use_loop}') - if has_loop and use_loop: - self._load_map_data(self.map_data_loop) - else: - self._load_map_data(self.map_data) - - def _load_map_data(self, text): - if not len(self.grids.keys()): - grids = np.array([loca for loca, _ in self._parse_text(text)]) - self.shape = location2node(tuple(np.max(grids, axis=0))) - - for loca, data in self._parse_text(text): - self.grids[loca].decode(data) - - @property - def wall_data(self): - return self._wall_data - - @wall_data.setter - def wall_data(self, text): - self._wall_data = text - - @property - def portal_data(self): - return self._portal_data - - @portal_data.setter - def portal_data(self, portal_list): - """ - Args: - portal_list (list[tuple]): [(start, end),] - """ - for nodes in portal_list: - node1, node2 = location_ensure(nodes[0]), location_ensure(nodes[1]) - self._portal_data.append((node1, node2)) - self[node1].is_portal = True - - def grid_connection_initial(self, wall=False, portal=False): - """ - Args: - wall (bool): If use wall_data - portal (bool): If use portal_data - - Returns: - bool: If used wall data. - """ - logger.info(f'grid_connection: wall={wall}, portal={portal}') - - # Generate grid connection. - total = set([grid for grid in self.grids.keys()]) - for grid in self: - connection = set() - for arr in np.array([(0, -1), (0, 1), (-1, 0), (1, 0)]): - arr = tuple(arr + grid.location) - if arr in total: - connection.add(arr) - self.grid_connection[grid.location] = connection - - # Use wall_data to delete connection. - if wall and self._wall_data: - wall = [] - for y, line in enumerate([l for l in self._wall_data.split('\n') if l]): - for x, letter in enumerate(line[4:-2]): - if letter != ' ': - wall.append((x, y)) - wall = np.array(wall) - vert = wall[np.all([wall[:, 0] % 4 == 2, wall[:, 1] % 2 == 0], axis=0)] - hori = wall[np.all([wall[:, 0] % 4 == 0, wall[:, 1] % 2 == 1], axis=0)] - disconnect = [] - for loca in (vert - (2, 0)) // (4, 2): - disconnect.append([loca, loca + (1, 0)]) - for loca in (hori - (0, 1)) // (4, 2): - disconnect.append([loca, loca + (0, 1)]) - for g1, g2 in disconnect: - g1 = self[g1] - g2 = self[g2] - self.grid_connection[g1].remove(g2) - self.grid_connection[g2].remove(g1) - - # Create portal link - for start, end in self._portal_data: - if portal: - self.grid_connection[start].add(end) - self[start].is_portal = True - self[start].portal_link = end - else: - if end in self.grid_connection[start]: - self.grid_connection[start].remove(end) - self[start].is_portal = False - self[start].portal_link = None - - return True - - def show(self): - # logger.info('Showing grids:') - logger.info(' ' + ' '.join([' ' + chr(x + 64 + 1) for x in range(self.shape[0] + 1)])) - for y in range(self.shape[1] + 1): - text = str(y + 1) + ' ' + ' '.join( - [self[(x, y)].str if (x, y) in self else ' ' for x in range(self.shape[0] + 1)]) - logger.info(text) - - def update(self, grids, camera, mode='normal'): - """ - Args: - grids: - camera (tuple): - mode (str): Scan mode, such as 'normal', 'carrier', 'movable' - """ - offset = np.array(camera) - np.array(grids.center_loca) - grids.show() - - failed_count = 0 - for grid in grids.grids.values(): - loca = tuple(offset + grid.location) - if loca in self.grids: - if not copy.copy(self.grids[loca]).merge(grid, mode=mode): - logger.warning(f"Wrong Prediction. {self.grids[loca]} = '{grid.str}'") - failed_count += 1 - - if failed_count < 2: - for grid in grids.grids.values(): - loca = tuple(offset + grid.location) - if loca in self.grids: - self.grids[loca].merge(grid, mode=mode) - return True - else: - logger.warning('Too many wrong prediction') - return False - - def reset(self): - for grid in self: - grid.reset() - - def reset_fleet(self): - for grid in self: - grid.is_current_fleet = False - - @property - def camera_data(self): - """ - Returns: - SelectedGrids: - """ - return self._camera_data - - @camera_data.setter - def camera_data(self, nodes): - """ - Args: - nodes (list): Contains str. - """ - self._camera_data = SelectedGrids([self[node2location(node)] for node in nodes]) - - @property - def camera_data_spawn_point(self): - """Additional camera_data to detect fleets at spawn point. - - Returns: - SelectedGrids: - """ - return self._camera_data_spawn_point - - @camera_data_spawn_point.setter - def camera_data_spawn_point(self, nodes): - """ - Args: - nodes (list): Contains str. - """ - self._camera_data_spawn_point = SelectedGrids([self[node2location(node)] for node in nodes]) - - @property - def spawn_data(self): - return self._spawn_data - - @spawn_data.setter - def spawn_data(self, data_list): - self._spawn_data = data_list - spawn = {'battle': 0, 'enemy': 0, 'mystery': 0, 'siren': 0, 'boss': 0} - for data in data_list: - spawn['battle'] = data['battle'] - spawn['enemy'] += data.get('enemy', 0) - spawn['mystery'] += data.get('mystery', 0) - spawn['siren'] += data.get('siren', 0) - spawn['boss'] += data.get('boss', 0) - self._spawn_data_stack.append(spawn.copy()) - - @property - def spawn_data_stack(self): - return self._spawn_data_stack - - @property - def weight_data(self): - return self._weight_data - - @weight_data.setter - def weight_data(self, text): - self._weight_data = text - for loca, data in self._parse_text(text): - self[loca].weight = float(data) - - @property - def map_covered(self): - """ - Returns: - SelectedGrids: - """ - covered = [] - for grid in self: - covered += self.grid_covered(grid).grids - return SelectedGrids(covered).add(self._map_covered) - - @map_covered.setter - def map_covered(self, nodes): - """ - Args: - nodes (list): Contains str. - """ - self._map_covered = SelectedGrids([self[node2location(node)] for node in nodes]) - - @property - def is_map_data_poor(self): - if not self.select(may_enemy=True) or not self.select(may_boss=True) or not self.select(is_spawn_point=True): - return False - if not len(self.spawn_data): - return False - return True - - def show_cost(self): - logger.info(' ' + ' '.join([' ' + chr(x + 64 + 1) for x in range(self.shape[0] + 1)])) - for y in range(self.shape[1] + 1): - text = str(y + 1) + ' ' + ' '.join( - [str(self[(x, y)].cost).rjust(4) if (x, y) in self else ' ' for x in range(self.shape[0] + 1)]) - logger.info(text) - - def show_connection(self): - logger.info(' ' + ' '.join([' ' + chr(x + 64 + 1) for x in range(self.shape[0] + 1)])) - for y in range(self.shape[1] + 1): - text = str(y + 1) + ' ' + ' '.join( - [location2node(self[(x, y)].connection) if (x, y) in self and self[(x, y)].connection else ' ' for x in - range(self.shape[0] + 1)]) - logger.info(text) - - def find_path_initial(self, location, has_ambush=True): - """ - Args: - location (tuple(int)): Grid location - has_ambush (bool): MAP_HAS_AMBUSH - """ - location = location_ensure(location) - ambush_cost = 10 if has_ambush else 1 - for grid in self: - grid.cost = 9999 - grid.connection = None - start = self[location] - start.cost = 0 - visited = [start] - visited = set(visited) - - while 1: - new = visited.copy() - for grid in visited: - for arr in self.grid_connection[grid.location]: - arr = self[arr] - if arr.is_land: - continue - cost = ambush_cost if arr.may_ambush else 1 - cost += grid.cost - - if cost < arr.cost: - arr.cost = cost - arr.connection = grid.location - elif cost == arr.cost: - if abs(arr.location[0] - grid.location[0]) == 1: - arr.connection = grid.location - if arr.is_sea: - new.add(arr) - if len(new) == len(visited): - break - visited = new - - # self.show_cost() - # self.show_connection() - - def find_path_initial_multi_fleet(self, location_dict, current, has_ambush): - """ - Args: - location_dict (dict): Key: int, fleet index. Value: tuple(int), grid location. - current (tuple): Current location. - has_ambush (bool): MAP_HAS_AMBUSH - """ - location_dict = sorted(location_dict.items(), key=lambda kv: (int(kv[1] == current),)) - for fleet, location in location_dict: - self.find_path_initial(location, has_ambush=has_ambush) - attr = f'cost_{fleet}' - for grid in self: - grid.__setattr__(attr, grid.cost) - - def _find_path(self, location): - """ - Args: - location (tuple): - - Returns: - list[tuple]: walking route. - - Examples: - MAP_7_2._find_path(node2location('H2')) - [(2, 2), (3, 2), (4, 2), (5, 2), (6, 2), (6, 1), (7, 1)] # ['C3', 'D3', 'E3', 'F3', 'G3', 'G2', 'H2'] - """ - if self[location].cost == 0: - return [location] - if self[location].connection is None: - return None - res = [location] - while 1: - location = self[location].connection - if len(res) > 30: - logger.warning('Route too long') - logger.warning(res) - # exit(1) - if location is not None: - res.append(location) - else: - break - res.reverse() - - if len(res) == 0: - logger.warning('No path found. Destination: %s' % str(location)) - return [location, location] - - return res - - def _find_route_node(self, route, step=0): - """ - Args: - route (list[tuple]): list of grids. - step (int): Fleet step in event map. Default to 0. - - Returns: - list[tuple]: list of walking node. - - Examples: - MAP_7_2._find_route_node([(2, 2), (3, 2), (4, 2), (5, 2), (6, 2), (6, 1), (7, 1)]) - [(6, 2), (7, 1)] - """ - res = [] - diff = np.abs(np.diff(route, axis=0)) - turning = np.diff(diff, axis=0)[:, 0] - indexes = np.where(turning == -1)[0] + 1 - for index in indexes: - if not self[route[index]].is_fleet: - res.append(index) - else: - logger.info(f'Path_node_avoid: {self[route[index]]}') - if (index > 1) and (index - 1 not in indexes): - res.append(index - 1) - if (index < len(route) - 2) and (index + 1 not in indexes): - res.append(index + 1) - res.append(len(route) - 1) - # res = [6, 8] - if step == 0: - return [route[index] for index in res] - - res.insert(0, 0) - inserted = [] - for left, right in zip(res[:-1], res[1:]): - for index in list(range(left, right, step))[1:]: - way_node = self[route[index]] - if way_node.is_fleet or way_node.is_portal: - logger.info(f'Path_node_avoid: {way_node}') - if (index > 1) and (index - 1 not in res): - inserted.append(index - 1) - if (index < len(route) - 2) and (index + 1 not in res): - inserted.append(index + 1) - else: - inserted.append(index) - inserted.append(right) - res = inserted - # res = [3, 6, 8] - return [route[index] for index in res] - - def find_path(self, location, step=0): - location = location_ensure(location) - - path = self._find_path(location) - if path is None or not len(path): - logger.warning('No path found. Return destination.') - return [location] - logger.info('Full path: %s' % '[' + ', ' .join([location2node(grid) for grid in path]) + ']') - - portal_path = [] - index = [0] - for i, loca in enumerate(zip(path[:-1], path[1:])): - if self[loca[0]].is_portal and self[loca[0]].portal_link == loca[1]: - index += [i, i + 1] - index.append(len(path)) - for start, end in zip(index[:-1], index[1:]): - if end - start == 1 and self[path[start]].is_portal and self[path[start]].portal_link == path[end]: - continue - local_path = path[start:end + 1] - local_path = self._find_route_node(local_path, step=step) - portal_path += local_path - logger.info('Path: %s' % '[' + ', ' .join([location2node(grid) for grid in local_path]) + ']') - path = portal_path - - return path - - def grid_covered(self, grid, location=None): - """ - Args: - grid (GridInfo) - location (list[tuple[int]]): Relative coordinate of the covered grid. - - Returns: - SelectedGrids: - """ - if location is None: - covered = [tuple(np.array(grid.location) + upper) for upper in grid.covered_grid()] - else: - covered = [tuple(np.array(grid.location) + upper) for upper in location] - covered = [self[upper] for upper in covered if upper in self] - return SelectedGrids(covered) - - def missing_get(self, battle_count, mystery_count=0, siren_count=0, carrier_count=0, mode='normal'): - try: - missing = self.spawn_data_stack[battle_count].copy() - except IndexError: - missing = self.spawn_data_stack[-1].copy() - may = {'enemy': 0, 'mystery': 0, 'siren': 0, 'boss': 0, 'carrier': 0} - missing['enemy'] -= battle_count - siren_count - missing['mystery'] -= mystery_count - missing['siren'] -= siren_count - missing['carrier'] = carrier_count - self.select(is_enemy=True, may_enemy=False).count - for grid in self: - for attr in ['enemy', 'mystery', 'siren', 'boss']: - if grid.__getattribute__('is_' + attr): - missing[attr] -= 1 - - for upper in self.map_covered: - if upper.may_enemy and not upper.is_enemy: - may['enemy'] += 1 - if upper.may_mystery and not upper.is_mystery: - may['mystery'] += 1 - if (upper.may_siren or mode == 'movable') and not upper.is_siren: - may['siren'] += 1 - if upper.may_boss and not upper.is_boss: - may['boss'] += 1 - if upper.may_carrier: - may['carrier'] += 1 - - logger.attr('enemy_missing', - ', '.join([f'{k[:2].upper()}:{str(v).rjust(2)}' for k, v in missing.items() if k != 'battle'])) - logger.attr('enemy_may____', - ', '.join([f'{k[:2].upper()}:{str(v).rjust(2)}' for k, v in may.items()])) - return may, missing - - def missing_is_none(self, battle_count, mystery_count=0, siren_count=0, carrier_count=0, mode='normal'): - if self.poor_map_data: - return False - - may, missing = self.missing_get(battle_count, mystery_count, siren_count, carrier_count, mode) - - for key in may.keys(): - if missing[key] != 0: - return False - - return True - - def missing_predict(self, battle_count, mystery_count=0, siren_count=0, carrier_count=0, mode='normal'): - if self.poor_map_data: - return False - - may, missing = self.missing_get(battle_count, mystery_count, siren_count, carrier_count, mode) - - # predict - for upper in self.map_covered: - for attr in ['enemy', 'mystery', 'siren', 'boss']: - if upper.__getattribute__('may_' + attr) and missing[attr] > 0 and missing[attr] == may[attr]: - logger.info('Predict %s to be %s' % (location2node(upper.location), attr)) - upper.__setattr__('is_' + attr, True) - if carrier_count: - if upper.may_carrier and missing['carrier'] > 0 and missing['carrier'] == may['carrier']: - logger.info('Predict %s to be enemy' % location2node(upper.location)) - upper.__setattr__('is_enemy', True) - - def select(self, **kwargs): - """ - Args: - **kwargs: Attributes of Grid. - - Returns: - SelectedGrids: - """ - result = [] - for grid in self: - flag = True - for k, v in kwargs.items(): - if grid.__getattribute__(k) != v: - flag = False - if flag: - result.append(grid) - - return SelectedGrids(result) - - def to_selected(self, grids): - """ - Args: - grids (list): - - Returns: - SelectedGrids: - """ - return SelectedGrids([self[location_ensure(loca)] for loca in grids]) - - def flatten(self): - """ - Returns: - list[GridInfo]: - """ - return self.grids.values() +import copy + +from module.base.utils import location2node, node2location +from module.logger import logger +from module.map.map_grids import SelectedGrids +from module.map.utils import * +from module.map_detection.grid_info import GridInfo + + +class CampaignMap: + def __init__(self, name=None): + self.name = name + self.grids = {} + self._shape = (0, 0) + self._map_data = '' + self._map_data_loop = '' + self._weight_data = '' + self._wall_data = '' + self._portal_data = [] + self._land_based_data = [] + self._spawn_data = [] + self._spawn_data_stack = [] + self._camera_data = [] + self._camera_data_spawn_point = [] + self._map_covered = SelectedGrids([]) + self.in_map_swipe_preset_data = None + self.poor_map_data = False + self.camera_sight = (-3, -1, 3, 2) + self.grid_connection = {} + + def __iter__(self): + return iter(self.grids.values()) + + def __getitem__(self, item): + """ + Args: + item: + + Returns: + GridInfo: + """ + return self.grids[tuple(item)] + + def __contains__(self, item): + return tuple(item) in self.grids + + @staticmethod + def _parse_text(text): + text = text.strip() + for y, row in enumerate(text.split('\n')): + row = row.strip() + for x, data in enumerate(row.split(' ')): + yield (x, y), data + + @property + def shape(self): + return self._shape + + @shape.setter + def shape(self, scale): + self._shape = node2location(scale.upper()) + for y in range(self._shape[1] + 1): + for x in range(self._shape[0] + 1): + grid = GridInfo() + grid.location = (x, y) + self.grids[(x, y)] = grid + + # camera_data can be generate automatically, but it's better to set it manually. + self.camera_data = [location2node(loca) for loca in camera_2d(self._shape, sight=self.camera_sight)] + self.camera_data_spawn_point = [] + # weight_data set to 10. + for grid in self: + grid.weight = 10. + + @property + def map_data(self): + return self._map_data + + @map_data.setter + def map_data(self, text): + self._map_data = text + self._load_map_data(text) + + @property + def map_data_loop(self): + return self._map_data_loop + + @map_data_loop.setter + def map_data_loop(self, text): + self._map_data_loop = text + + def load_map_data(self, use_loop=False): + """ + Args: + use_loop (bool): If at clearing mode. + clearing mode (Correct name) == fast forward (in old Alas) == loop (in lua files) + """ + has_loop = len(self.map_data_loop) + logger.info(f'Load map_data, has_loop={has_loop}, use_loop={use_loop}') + if has_loop and use_loop: + self._load_map_data(self.map_data_loop) + else: + self._load_map_data(self.map_data) + + def _load_map_data(self, text): + if not len(self.grids.keys()): + grids = np.array([loca for loca, _ in self._parse_text(text)]) + self.shape = location2node(tuple(np.max(grids, axis=0))) + + for loca, data in self._parse_text(text): + self.grids[loca].decode(data) + + @property + def wall_data(self): + return self._wall_data + + @wall_data.setter + def wall_data(self, text): + self._wall_data = text + + @property + def portal_data(self): + return self._portal_data + + @portal_data.setter + def portal_data(self, portal_list): + """ + Args: + portal_list (list[tuple]): [(start, end),] + """ + for nodes in portal_list: + node1, node2 = location_ensure(nodes[0]), location_ensure(nodes[1]) + self._portal_data.append((node1, node2)) + self[node1].is_portal = True + + @property + def land_based_data(self): + return self._land_based_data + + @land_based_data.setter + def land_based_data(self, data): + self._land_based_data = data + + def _load_land_base_data(self, data): + """ + land_based_data need to be set after map_data. + + Args: + data (list[list[str]]): Such as [['H7', 'up'], ['D5', 'left'], ['G3', 'down'], ['C2', 'right']] + """ + rotation_dict = { + 'up': [(0, -1), (0, -2), (0, -3)], + 'down': [(0, 1), (0, 2), (0, 3)], + 'left': [(-1, 0), (-2, 0), (-3, 0)], + 'right': [(1, 0), (2, 0), (3, 0)], + } + self._land_based_data = data + for land_based in data: + grid, rotation = land_based + grid = self.grids[location_ensure(grid)] + trigger = self.grid_covered(grid=grid, location=[(0, -1), (0, 1), (-1, 0), (1, 0)]).select(is_land=False) + block = self.grid_covered(grid=grid, location=rotation_dict[rotation]).select(is_land=False) + trigger.set(is_mechanism_trigger=True, mechanism_trigger=trigger, mechanism_block=block) + block.set(is_mechanism_block=True) + + def load_mechanism(self, land_based=False): + logger.info(f'Load mechanism. land_base={land_based}') + if land_based: + self._load_land_base_data(self.land_based_data) + + def grid_connection_initial(self, wall=False, portal=False): + """ + Args: + wall (bool): If use wall_data + portal (bool): If use portal_data + + Returns: + bool: If used wall data. + """ + logger.info(f'grid_connection: wall={wall}, portal={portal}') + + # Generate grid connection. + total = set([grid for grid in self.grids.keys()]) + for grid in self: + connection = set() + for arr in np.array([(0, -1), (0, 1), (-1, 0), (1, 0)]): + arr = tuple(arr + grid.location) + if arr in total: + connection.add(arr) + self.grid_connection[grid.location] = connection + + # Use wall_data to delete connection. + if wall and self._wall_data: + wall = [] + for y, line in enumerate([l for l in self._wall_data.split('\n') if l]): + for x, letter in enumerate(line[4:-2]): + if letter != ' ': + wall.append((x, y)) + wall = np.array(wall) + vert = wall[np.all([wall[:, 0] % 4 == 2, wall[:, 1] % 2 == 0], axis=0)] + hori = wall[np.all([wall[:, 0] % 4 == 0, wall[:, 1] % 2 == 1], axis=0)] + disconnect = [] + for loca in (vert - (2, 0)) // (4, 2): + disconnect.append([loca, loca + (1, 0)]) + for loca in (hori - (0, 1)) // (4, 2): + disconnect.append([loca, loca + (0, 1)]) + for g1, g2 in disconnect: + g1 = self[g1] + g2 = self[g2] + self.grid_connection[g1].remove(g2) + self.grid_connection[g2].remove(g1) + + # Create portal link + for start, end in self._portal_data: + if portal: + self.grid_connection[start].add(end) + self[start].is_portal = True + self[start].portal_link = end + else: + if end in self.grid_connection[start]: + self.grid_connection[start].remove(end) + self[start].is_portal = False + self[start].portal_link = None + + return True + + def show(self): + # logger.info('Showing grids:') + logger.info(' ' + ' '.join([' ' + chr(x + 64 + 1) for x in range(self.shape[0] + 1)])) + for y in range(self.shape[1] + 1): + text = str(y + 1) + ' ' + ' '.join( + [self[(x, y)].str if (x, y) in self else ' ' for x in range(self.shape[0] + 1)]) + logger.info(text) + + def update(self, grids, camera, mode='normal'): + """ + Args: + grids: + camera (tuple): + mode (str): Scan mode, such as 'normal', 'carrier', 'movable' + """ + offset = np.array(camera) - np.array(grids.center_loca) + grids.show() + + failed_count = 0 + for grid in grids.grids.values(): + loca = tuple(offset + grid.location) + if loca in self.grids: + if not copy.copy(self.grids[loca]).merge(grid, mode=mode): + logger.warning(f"Wrong Prediction. {self.grids[loca]} = '{grid.str}'") + failed_count += 1 + + if failed_count < 2: + for grid in grids.grids.values(): + loca = tuple(offset + grid.location) + if loca in self.grids: + self.grids[loca].merge(grid, mode=mode) + return True + else: + logger.warning('Too many wrong prediction') + return False + + def reset(self): + for grid in self: + grid.reset() + + def reset_fleet(self): + for grid in self: + grid.is_current_fleet = False + + @property + def camera_data(self): + """ + Returns: + SelectedGrids: + """ + return self._camera_data + + @camera_data.setter + def camera_data(self, nodes): + """ + Args: + nodes (list): Contains str. + """ + self._camera_data = SelectedGrids([self[node2location(node)] for node in nodes]) + + @property + def camera_data_spawn_point(self): + """Additional camera_data to detect fleets at spawn point. + + Returns: + SelectedGrids: + """ + return self._camera_data_spawn_point + + @camera_data_spawn_point.setter + def camera_data_spawn_point(self, nodes): + """ + Args: + nodes (list): Contains str. + """ + self._camera_data_spawn_point = SelectedGrids([self[node2location(node)] for node in nodes]) + + @property + def spawn_data(self): + return self._spawn_data + + @spawn_data.setter + def spawn_data(self, data_list): + self._spawn_data = data_list + spawn = {'battle': 0, 'enemy': 0, 'mystery': 0, 'siren': 0, 'boss': 0} + for data in data_list: + spawn['battle'] = data['battle'] + spawn['enemy'] += data.get('enemy', 0) + spawn['mystery'] += data.get('mystery', 0) + spawn['siren'] += data.get('siren', 0) + spawn['boss'] += data.get('boss', 0) + self._spawn_data_stack.append(spawn.copy()) + + @property + def spawn_data_stack(self): + return self._spawn_data_stack + + @property + def weight_data(self): + return self._weight_data + + @weight_data.setter + def weight_data(self, text): + self._weight_data = text + for loca, data in self._parse_text(text): + self[loca].weight = float(data) + + @property + def map_covered(self): + """ + Returns: + SelectedGrids: + """ + covered = [] + for grid in self: + covered += self.grid_covered(grid).grids + return SelectedGrids(covered).add(self._map_covered) + + @map_covered.setter + def map_covered(self, nodes): + """ + Args: + nodes (list): Contains str. + """ + self._map_covered = SelectedGrids([self[node2location(node)] for node in nodes]) + + @property + def is_map_data_poor(self): + if not self.select(may_enemy=True) or not self.select(may_boss=True) or not self.select(is_spawn_point=True): + return False + if not len(self.spawn_data): + return False + return True + + def show_cost(self): + logger.info(' ' + ' '.join([' ' + chr(x + 64 + 1) for x in range(self.shape[0] + 1)])) + for y in range(self.shape[1] + 1): + text = str(y + 1) + ' ' + ' '.join( + [str(self[(x, y)].cost).rjust(4) if (x, y) in self else ' ' for x in range(self.shape[0] + 1)]) + logger.info(text) + + def show_connection(self): + logger.info(' ' + ' '.join([' ' + chr(x + 64 + 1) for x in range(self.shape[0] + 1)])) + for y in range(self.shape[1] + 1): + text = str(y + 1) + ' ' + ' '.join( + [location2node(self[(x, y)].connection) if (x, y) in self and self[(x, y)].connection else ' ' for x in + range(self.shape[0] + 1)]) + logger.info(text) + + def find_path_initial(self, location, has_ambush=True): + """ + Args: + location (tuple(int)): Grid location + has_ambush (bool): MAP_HAS_AMBUSH + """ + location = location_ensure(location) + ambush_cost = 10 if has_ambush else 1 + for grid in self: + grid.cost = 9999 + grid.connection = None + start = self[location] + start.cost = 0 + visited = [start] + visited = set(visited) + + while 1: + new = visited.copy() + for grid in visited: + for arr in self.grid_connection[grid.location]: + arr = self[arr] + if arr.is_land or arr.is_mechanism_block: + continue + cost = ambush_cost if arr.may_ambush else 1 + cost += grid.cost + + if cost < arr.cost: + arr.cost = cost + arr.connection = grid.location + elif cost == arr.cost: + if abs(arr.location[0] - grid.location[0]) == 1: + arr.connection = grid.location + if arr.is_sea: + new.add(arr) + if len(new) == len(visited): + break + visited = new + + # self.show_cost() + # self.show_connection() + + def find_path_initial_multi_fleet(self, location_dict, current, has_ambush): + """ + Args: + location_dict (dict): Key: int, fleet index. Value: tuple(int), grid location. + current (tuple): Current location. + has_ambush (bool): MAP_HAS_AMBUSH + """ + location_dict = sorted(location_dict.items(), key=lambda kv: (int(kv[1] == current),)) + for fleet, location in location_dict: + self.find_path_initial(location, has_ambush=has_ambush) + attr = f'cost_{fleet}' + for grid in self: + grid.__setattr__(attr, grid.cost) + + def _find_path(self, location): + """ + Args: + location (tuple): + + Returns: + list[tuple]: walking route. + + Examples: + MAP_7_2._find_path(node2location('H2')) + [(2, 2), (3, 2), (4, 2), (5, 2), (6, 2), (6, 1), (7, 1)] # ['C3', 'D3', 'E3', 'F3', 'G3', 'G2', 'H2'] + """ + if self[location].cost == 0: + return [location] + if self[location].connection is None: + return None + res = [location] + while 1: + location = self[location].connection + if len(res) > 30: + logger.warning('Route too long') + logger.warning(res) + # exit(1) + if location is not None: + res.append(location) + else: + break + res.reverse() + + if len(res) == 0: + logger.warning('No path found. Destination: %s' % str(location)) + return [location, location] + + return res + + def _find_route_node(self, route, step=0): + """ + Args: + route (list[tuple]): list of grids. + step (int): Fleet step in event map. Default to 0. + + Returns: + list[tuple]: list of walking node. + + Examples: + MAP_7_2._find_route_node([(2, 2), (3, 2), (4, 2), (5, 2), (6, 2), (6, 1), (7, 1)]) + [(6, 2), (7, 1)] + """ + res = [] + diff = np.abs(np.diff(route, axis=0)) + turning = np.diff(diff, axis=0)[:, 0] + indexes = np.where(turning == -1)[0] + 1 + for index in indexes: + if not self[route[index]].is_fleet: + res.append(index) + else: + logger.info(f'Path_node_avoid: {self[route[index]]}') + if (index > 1) and (index - 1 not in indexes): + res.append(index - 1) + if (index < len(route) - 2) and (index + 1 not in indexes): + res.append(index + 1) + res.append(len(route) - 1) + # res = [6, 8] + if step == 0: + return [route[index] for index in res] + + res.insert(0, 0) + inserted = [] + for left, right in zip(res[:-1], res[1:]): + for index in list(range(left, right, step))[1:]: + way_node = self[route[index]] + if way_node.is_fleet or way_node.is_portal: + logger.info(f'Path_node_avoid: {way_node}') + if (index > 1) and (index - 1 not in res): + inserted.append(index - 1) + if (index < len(route) - 2) and (index + 1 not in res): + inserted.append(index + 1) + else: + inserted.append(index) + inserted.append(right) + res = inserted + # res = [3, 6, 8] + return [route[index] for index in res] + + def find_path(self, location, step=0): + location = location_ensure(location) + + path = self._find_path(location) + if path is None or not len(path): + logger.warning('No path found. Return destination.') + return [location] + logger.info('Full path: %s' % '[' + ', ' .join([location2node(grid) for grid in path]) + ']') + + portal_path = [] + index = [0] + for i, loca in enumerate(zip(path[:-1], path[1:])): + if self[loca[0]].is_portal and self[loca[0]].portal_link == loca[1]: + index += [i, i + 1] + index.append(len(path)) + for start, end in zip(index[:-1], index[1:]): + if end - start == 1 and self[path[start]].is_portal and self[path[start]].portal_link == path[end]: + continue + local_path = path[start:end + 1] + local_path = self._find_route_node(local_path, step=step) + portal_path += local_path + logger.info('Path: %s' % '[' + ', ' .join([location2node(grid) for grid in local_path]) + ']') + path = portal_path + + return path + + def grid_covered(self, grid, location=None): + """ + Args: + grid (GridInfo) + location (list[tuple[int]]): Relative coordinate of the covered grid. + + Returns: + SelectedGrids: + """ + if location is None: + covered = [tuple(np.array(grid.location) + upper) for upper in grid.covered_grid()] + else: + covered = [tuple(np.array(grid.location) + upper) for upper in location] + covered = [self[upper] for upper in covered if upper in self] + return SelectedGrids(covered) + + def missing_get(self, battle_count, mystery_count=0, siren_count=0, carrier_count=0, mode='normal'): + try: + missing = self.spawn_data_stack[battle_count].copy() + except IndexError: + missing = self.spawn_data_stack[-1].copy() + may = {'enemy': 0, 'mystery': 0, 'siren': 0, 'boss': 0, 'carrier': 0} + missing['enemy'] -= battle_count - siren_count + missing['mystery'] -= mystery_count + missing['siren'] -= siren_count + missing['carrier'] = carrier_count - self.select(is_enemy=True, may_enemy=False).count + for grid in self: + for attr in ['enemy', 'mystery', 'siren', 'boss']: + if grid.__getattribute__('is_' + attr): + missing[attr] -= 1 + + for upper in self.map_covered: + if upper.may_enemy and not upper.is_enemy: + may['enemy'] += 1 + if upper.may_mystery and not upper.is_mystery: + may['mystery'] += 1 + if (upper.may_siren or mode == 'movable') and not upper.is_siren: + may['siren'] += 1 + if upper.may_boss and not upper.is_boss: + may['boss'] += 1 + if upper.may_carrier: + may['carrier'] += 1 + + logger.attr('enemy_missing', + ', '.join([f'{k[:2].upper()}:{str(v).rjust(2)}' for k, v in missing.items() if k != 'battle'])) + logger.attr('enemy_may____', + ', '.join([f'{k[:2].upper()}:{str(v).rjust(2)}' for k, v in may.items()])) + return may, missing + + def missing_is_none(self, battle_count, mystery_count=0, siren_count=0, carrier_count=0, mode='normal'): + if self.poor_map_data: + return False + + may, missing = self.missing_get(battle_count, mystery_count, siren_count, carrier_count, mode) + + for key in may.keys(): + if missing[key] != 0: + return False + + return True + + def missing_predict(self, battle_count, mystery_count=0, siren_count=0, carrier_count=0, mode='normal'): + if self.poor_map_data: + return False + + may, missing = self.missing_get(battle_count, mystery_count, siren_count, carrier_count, mode) + + # predict + for upper in self.map_covered: + for attr in ['enemy', 'mystery', 'siren', 'boss']: + if upper.__getattribute__('may_' + attr) and missing[attr] > 0 and missing[attr] == may[attr]: + logger.info('Predict %s to be %s' % (location2node(upper.location), attr)) + upper.__setattr__('is_' + attr, True) + if carrier_count: + if upper.may_carrier and missing['carrier'] > 0 and missing['carrier'] == may['carrier']: + logger.info('Predict %s to be enemy' % location2node(upper.location)) + upper.__setattr__('is_enemy', True) + + def select(self, **kwargs): + """ + Args: + **kwargs: Attributes of Grid. + + Returns: + SelectedGrids: + """ + result = [] + for grid in self: + flag = True + for k, v in kwargs.items(): + if grid.__getattribute__(k) != v: + flag = False + if flag: + result.append(grid) + + return SelectedGrids(result) + + def to_selected(self, grids): + """ + Args: + grids (list): + + Returns: + SelectedGrids: + """ + return SelectedGrids([self[location_ensure(loca)] for loca in grids]) + + def flatten(self): + """ + Returns: + list[GridInfo]: + """ + return self.grids.values() diff --git a/module/map/map_grids.py b/module/map/map_grids.py index 4cf2e0eb5..f7d973ccd 100644 --- a/module/map/map_grids.py +++ b/module/map/map_grids.py @@ -84,6 +84,17 @@ class SelectedGrids: return SelectedGrids(result) + def set(self, **kwargs): + """ + Set attribute to each grid. + + Args: + **kwargs: + """ + for grid in self: + for key, value in kwargs.items(): + grid.__setattr__(key, value) + def add(self, grids): """ Args: diff --git a/module/map_detection/grid_info.py b/module/map_detection/grid_info.py index 1000814c5..e6eb35644 100644 --- a/module/map_detection/grid_info.py +++ b/module/map_detection/grid_info.py @@ -1,265 +1,280 @@ -from module.base.utils import location2node -from module.logger import logger - - -class GridInfo: - """ - Class that gather basic information of a grid in map_v1. - - Visit 碧蓝航线WIKI(Chinese Simplified) http://wiki.biligame.com/blhx, to get basic info of a map_v1. - For example, visit http://wiki.biligame.com/blhx/7-2, to know more about campaign 7-2, - which includes boss point, enemy spawn point. - - A grid contains these unchangeable properties which can known from WIKI. - | print_name | property_name | description | - |------------|----------------|-------------------------| - | ++ | is_land | fleet can't go to land | - | -- | is_sea | sea | - | __ | | submarine spawn point | - | SP | is_spawn_point | fleet may spawns here | - | ME | may_enemy | enemy may spawns here | - | MB | may_boss | boss may spawns here | - | MM | may_mystery | mystery may spawns here | - | MA | may_ammo | fleet can get ammo here | - | MS | may_siren | Siren/Elite enemy spawn | - """ - - # is_sea -- - is_land = False # ++ - is_spawn_point = False # SP - - may_enemy = False # ME - may_boss = False # MB - may_mystery = False # MM - may_ammo = False # MA - may_siren = False # MS - may_ambush = False - - is_enemy = False # example: 0L 1M 2C 3T 3E - is_boss = False # BO - is_mystery = False # MY - is_ammo = False # AM - is_fleet = False # FL - is_current_fleet = False - is_submarine = False # ss - is_siren = False # SI - is_portal = False - portal_link = () - - enemy_scale = 0 - enemy_genre = None # Light, Main, Carrier, Treasure, Enemy(unknown) - - is_cleared = False - is_caught_by_siren = False - is_carrier = False - is_movable = False - cost = 9999 - cost_1 = 9999 - cost_2 = 9999 - connection = None - weight = 1 - - location = None - - def decode(self, text): - text = text.upper() - dic = { - '++': 'is_land', - 'SP': 'is_spawn_point', - 'ME': 'may_enemy', - 'MB': 'may_boss', - 'MM': 'may_mystery', - 'MA': 'may_ammo', - 'MS': 'may_siren', - } - valid = text in dic - for k, v in dic.items(): - self.__setattr__(v, valid and bool(k == text)) - - self.may_ambush = not (self.may_enemy or self.may_boss or self.may_mystery or self.may_mystery) - # if self.may_siren: - # self.may_enemy = True - # if self.may_boss: - # self.may_enemy = True - - def encode(self): - dic = { - '++': 'is_land', - 'BO': 'is_boss', - } - for key, value in dic.items(): - if self.__getattribute__(value): - return key - - if self.is_siren: - name = self.enemy_genre[6:8].upper() if self.enemy_genre else 'SU' - return name if name else 'SU' - - if self.is_enemy: - return '%s%s' % ( - self.enemy_scale if self.enemy_scale else 0, - self.enemy_genre[0].upper() if self.enemy_genre else 'E') - - dic = { - 'FL': 'is_current_fleet', - 'Fc': 'is_caught_by_siren', - 'Fl': 'is_fleet', - 'ss': 'is_submarine', - 'MY': 'is_mystery', - 'AM': 'is_ammo', - '==': 'is_cleared' - } - for key, value in dic.items(): - if self.__getattribute__(value): - return key - - return '--' - - def __str__(self): - return location2node(self.location) - - __repr__ = __str__ - - def __hash__(self): - return hash(self.location) - - @property - def str(self): - return self.encode() - - @property - def is_sea(self): - return False if self.is_land or self.is_enemy or self.is_boss else True - - @property - def may_carrier(self): - return self.is_sea and not self.may_enemy - - @property - def is_accessible(self): - return self.cost < 9999 - - @property - def is_accessible_1(self): - return self.cost_1 < 9999 - - @property - def is_accessible_2(self): - return self.cost_2 < 9999 - - @property - def is_nearby(self): - return self.cost < 20 - - def merge(self, info, mode='normal'): - """ - Args: - info (GridInfo): - mode (str): Scan mode, such as 'normal', 'carrier', 'movable' - - Returns: - bool: If success. - """ - if info.is_caught_by_siren: - if self.is_sea: - self.is_fleet = True - self.is_caught_by_siren = True - else: - return False - if info.is_fleet: - if self.is_sea: - self.is_fleet = True - if info.is_current_fleet: - self.is_current_fleet = True - return True - else: - return False - if info.is_boss: - if not self.is_land and self.may_boss: - self.is_boss = True - return True - else: - return False - if info.is_siren: - if not self.is_land and self.may_siren: - self.is_siren = True - self.enemy_scale = 0 - self.enemy_genre = info.enemy_genre - return True - elif (mode == 'movable' or self.is_movable) and not self.is_land: - self.is_siren = True - self.enemy_scale = 0 - self.enemy_genre = info.enemy_genre - return True - else: - return False - if info.is_enemy: - if not self.is_land and (self.may_enemy or self.is_carrier): - self.is_enemy = True - if info.enemy_scale: - self.enemy_scale = info.enemy_scale - if info.enemy_genre: - self.enemy_genre = info.enemy_genre - return True - elif mode == 'carrier' and not self.is_land and self.may_carrier: - self.is_enemy = True - self.is_carrier = True - if info.enemy_scale: - self.enemy_scale = info.enemy_scale - if info.enemy_genre: - self.enemy_genre = info.enemy_genre - return True - else: - return False - if info.is_mystery: - if self.may_mystery: - self.is_mystery = info.is_mystery - return True - else: - return False - if info.is_ammo: - if self.may_ammo: - self.is_ammo = info.is_ammo - return True - else: - return False - - return True - - def wipe_out(self): - """ - Call this method when a fleet step on grid. - """ - self.is_enemy = False - self.enemy_scale = 0 - self.enemy_genre = None - self.is_mystery = False - self.is_boss = False - self.is_ammo = False - self.is_siren = False - self.is_caught_by_siren = False - self.is_carrier = False - self.is_movable = False - - def reset(self): - """ - Call this method after entering a map. - """ - self.wipe_out() - self.is_fleet = False - self.is_current_fleet = False - self.is_submarine = False - self.is_cleared = False - - def covered_grid(self): - """Relative coordinate of the covered grid. - - Returns: - list[tuple]: - """ - if self.is_current_fleet: - return [(0, -1), (0, -2)] - if self.is_fleet or self.is_siren or self.is_mystery: - return [(0, -1)] - - return [] +from module.base.utils import location2node +from module.logger import logger + + +class GridInfo: + """ + Class that gather basic information of a grid in map_v1. + + Visit 碧蓝航线WIKI(Chinese Simplified) http://wiki.biligame.com/blhx, to get basic info of a map_v1. + For example, visit http://wiki.biligame.com/blhx/7-2, to know more about campaign 7-2, + which includes boss point, enemy spawn point. + + A grid contains these unchangeable properties which can known from WIKI. + | print_name | property_name | description | + |------------|----------------|-------------------------| + | ++ | is_land | fleet can't go to land | + | -- | is_sea | sea | + | __ | | submarine spawn point | + | SP | is_spawn_point | fleet may spawns here | + | ME | may_enemy | enemy may spawns here | + | MB | may_boss | boss may spawns here | + | MM | may_mystery | mystery may spawns here | + | MA | may_ammo | fleet can get ammo here | + | MS | may_siren | Siren/Elite enemy spawn | + """ + + # is_sea -- + is_land = False # ++ + is_spawn_point = False # SP + + may_enemy = False # ME + may_boss = False # MB + may_mystery = False # MM + may_ammo = False # MA + may_siren = False # MS + may_ambush = False + + is_enemy = False # example: 0L 1M 2C 3T 3E + is_boss = False # BO + is_mystery = False # MY + is_ammo = False # AM + is_fleet = False # FL + is_current_fleet = False + is_submarine = False # ss + is_siren = False # SI + is_portal = False + portal_link = () + + enemy_scale = 0 + enemy_genre = None # Light, Main, Carrier, Treasure, Enemy(unknown) + + is_cleared = False + is_caught_by_siren = False + is_carrier = False # Is carrier spawn in mystery + is_movable = False # Is movable enemy + is_mechanism_trigger = False # Mechanism has triggered + is_mechanism_block = False # Blocked by mechanism + mechanism_trigger = None # SelectedGrids + mechanism_block = None # SelectedGrids + mechanism_wait = 2 # Seconds to wait the mechanism unlock animation + cost = 9999 + cost_1 = 9999 + cost_2 = 9999 + connection = None + weight = 1 + + location = None + + def decode(self, text): + text = text.upper() + dic = { + '++': 'is_land', + 'SP': 'is_spawn_point', + 'ME': 'may_enemy', + 'MB': 'may_boss', + 'MM': 'may_mystery', + 'MA': 'may_ammo', + 'MS': 'may_siren', + } + valid = text in dic + for k, v in dic.items(): + self.__setattr__(v, valid and bool(k == text)) + + self.may_ambush = not (self.may_enemy or self.may_boss or self.may_mystery or self.may_mystery) + # if self.may_siren: + # self.may_enemy = True + # if self.may_boss: + # self.may_enemy = True + + def encode(self): + dic = { + '++': 'is_land', + 'BO': 'is_boss', + } + for key, value in dic.items(): + if self.__getattribute__(value): + return key + + if self.is_siren: + name = self.enemy_genre[6:8].upper() if self.enemy_genre else 'SU' + return name if name else 'SU' + + if self.is_enemy: + return '%s%s' % ( + self.enemy_scale if self.enemy_scale else 0, + self.enemy_genre[0].upper() if self.enemy_genre else 'E') + + dic = { + 'FL': 'is_current_fleet', + 'Fc': 'is_caught_by_siren', + 'Fl': 'is_fleet', + 'ss': 'is_submarine', + 'MY': 'is_mystery', + 'AM': 'is_ammo', + '==': 'is_cleared' + } + for key, value in dic.items(): + if self.__getattribute__(value): + return key + + return '--' + + def __str__(self): + return location2node(self.location) + + __repr__ = __str__ + + def __hash__(self): + return hash(self.location) + + def __eq__(self, other): + return self.location == other.location + + @property + def str(self): + return self.encode() + + @property + def is_sea(self): + return False if self.is_land or self.is_enemy or self.is_boss else True + + @property + def may_carrier(self): + return self.is_sea and not self.may_enemy + + @property + def is_accessible(self): + return self.cost < 9999 + + @property + def is_accessible_1(self): + return self.cost_1 < 9999 + + @property + def is_accessible_2(self): + return self.cost_2 < 9999 + + @property + def is_nearby(self): + return self.cost < 20 + + def merge(self, info, mode='normal'): + """ + Args: + info (GridInfo): + mode (str): Scan mode, such as 'normal', 'carrier', 'movable' + + Returns: + bool: If success. + """ + if info.is_caught_by_siren: + if self.is_sea: + self.is_fleet = True + self.is_caught_by_siren = True + else: + return False + if info.is_fleet: + if self.is_sea: + self.is_fleet = True + if info.is_current_fleet: + self.is_current_fleet = True + return True + else: + return False + if info.is_boss: + if not self.is_land and self.may_boss: + self.is_boss = True + return True + else: + return False + if info.is_siren: + if not self.is_land and self.may_siren: + self.is_siren = True + self.enemy_scale = 0 + self.enemy_genre = info.enemy_genre + return True + elif (mode == 'movable' or self.is_movable) and not self.is_land: + self.is_siren = True + self.enemy_scale = 0 + self.enemy_genre = info.enemy_genre + return True + else: + return False + if info.is_enemy: + if not self.is_land and (self.may_enemy or self.is_carrier): + self.is_enemy = True + if info.enemy_scale: + self.enemy_scale = info.enemy_scale + if info.enemy_genre: + self.enemy_genre = info.enemy_genre + return True + elif mode == 'carrier' and not self.is_land and self.may_carrier: + self.is_enemy = True + self.is_carrier = True + if info.enemy_scale: + self.enemy_scale = info.enemy_scale + if info.enemy_genre: + self.enemy_genre = info.enemy_genre + return True + else: + return False + if info.is_mystery: + if self.may_mystery: + self.is_mystery = info.is_mystery + return True + else: + return False + if info.is_ammo: + if self.may_ammo: + self.is_ammo = info.is_ammo + return True + else: + return False + + return True + + def wipe_out(self): + """ + Call this method when a fleet step on grid. + """ + self.is_enemy = False + self.enemy_scale = 0 + self.enemy_genre = None + self.is_mystery = False + self.is_boss = False + self.is_ammo = False + self.is_siren = False + self.is_caught_by_siren = False + self.is_carrier = False + self.is_movable = False + if self.is_mechanism_trigger: + self.mechanism_trigger.set(is_mechanism_trigger=False) + self.mechanism_block.set(is_mechanism_block=False) + + def reset(self): + """ + Call this method after entering a map. + """ + self.wipe_out() + self.is_fleet = False + self.is_current_fleet = False + self.is_submarine = False + self.is_cleared = False + self.is_mechanism_trigger = False + self.is_mechanism_block = False + self.mechanism_trigger = None + self.mechanism_block = None + + def covered_grid(self): + """Relative coordinate of the covered grid. + + Returns: + list[tuple]: + """ + if self.is_current_fleet: + return [(0, -1), (0, -2)] + if self.is_fleet or self.is_siren or self.is_mystery: + return [(0, -1)] + + return []