1
0
mirror of https://github.com/sui-feng-cb/AzurLaneAutoScript1.git synced 2026-08-15 03:40:37 +08:00
Files
AzurLaneAutoScript/module/island_handler/restaurant.py
2026-08-12 01:45:51 +08:00

485 lines
18 KiB
Python

from datetime import datetime, timedelta
import cv2
import numpy as np
from yaml import safe_load
import module.config.server as server
from module.base.button import ButtonGrid
from module.base.decorator import cached_property, del_cached_property
from module.base.timer import Timer
from module.base.utils import color_similar, color_similarity_2d, load_image
from module.config.utils import get_server_next_update
from module.island.assets import ISLAND_CLICK_SAFE_AREA
from module.island.data import DIC_ISLAND_ITEM, DIC_ISLAND_RESTAURANT_MENU_TO_RECIPE
from module.island.utils import (
get_stuck_season_order_requirements,
load_hard_floor_items,
normalize_item_keys,
)
from module.island_handler.assets import *
from module.island_handler.dock import IslandDock
from module.island_handler.dock_scanner import CharacterScanner
from module.island_handler.restaurant_config import (
RESTAURANT_IDS,
WAITRESS_ANY,
WAITRESS_NONE,
get_config_key,
get_restaurant_config,
get_selected_named_waitresses,
get_waitress_effect,
get_waitress_slots,
)
from module.logger import logger
from module.ocr.ocr import Digit
from module.statistics.item import Item, ItemGrid
from module.statistics.utils import load_folder
RESTAURANT_SWIPE_AREA = (583, 208, 1023, 400)
ISLAND_RESTAURANT_ITEM_ORDER_PRICE = {
DIC_ISLAND_ITEM[item_id]['name']['en'].replace(' ', '_'): {
'id': item_id,
'order_price': DIC_ISLAND_ITEM[item_id]['order_price'],
}
for menu in DIC_ISLAND_RESTAURANT_MENU_TO_RECIPE.values()
for item_id in menu.keys()
}
class WaitressOccupied(Exception):
pass
class RestaurantItem(Item):
IMAGE_SHAPE = (83, 87)
def predict_valid(self):
mask = color_similarity_2d(self.image, (207, 209, 211))
cv2.inRange(mask, 0, 201, dst=mask)
sum_ = np.count_nonzero(mask)
return sum_ > 400
class RestaurantItemAmount(Digit):
def pre_process(self, image):
image = super().pre_process(image)
image = cv2.copyMakeBorder(image, 4, 2, 0, 0, cv2.BORDER_CONSTANT, value=(255, 255, 255))
return image
class RestaurantItemGrid(ItemGrid):
item_class = RestaurantItem
similarity = 0.75
def __init__(self, grid: ButtonGrid):
super().__init__(
grid,
templates={},
template_area=(12, 21, 72, 67),
amount_area=(42, 67, 77, 85),
tag_area=(66, 2, 72, 5)
)
if server.server == 'jp':
self.amount_ocr = RestaurantItemAmount([], letter=(220, 220, 220), lang='azur_lane', threshold=128, name='Amount_ocr')
else:
self.amount_ocr = Digit([], lang='cnocr', threshold=160, name='Amount_ocr')
self.load_template_folder('./assets/island/restaurant')
@staticmethod
def predict_tag(image):
color = cv2.mean(np.array(image))[:3]
if color_similar(color, (79, 201, 108), threshold=30):
return 'bonus'
return None
def predict(self, image, name=True, amount=True, cost=False, price=True, tag=True):
super().predict(image, name=True, amount=True, cost=False, price=False, tag=True)
for item in self.items:
item.id = ISLAND_RESTAURANT_ITEM_ORDER_PRICE.get(item.name, {}).get('id', 0)
item.price = ISLAND_RESTAURANT_ITEM_ORDER_PRICE.get(item.name, {}).get('order_price', 0)
items = self.items
grids = self.grids
if len(items):
min_row = grids[0, 0].area[1]
row = [str(item) for item in items if item.button[1] == min_row]
logger.info(f'Item row 1: {row}')
row = [str(item) for item in items if item.button[1] != min_row]
logger.info(f'Item row 2: {row}')
return items
class IslandRestaurant(IslandDock):
working_restaurant_id = None
@staticmethod
def get_initial_capacity_from_grade(grade):
if grade == 'bronze':
return 5
elif grade in ['silver', 'gold', 'diamond']:
return 6
else:
raise ValueError(f"Invalid grade: {grade}")
@cached_property
def restaurant_capacity(self):
capacity = {}
for restaurant_id in RESTAURANT_IDS:
config_data = get_restaurant_config(restaurant_id)
grade = self.config.cross_get(
get_config_key(restaurant_id, config_data['grade_key'])
)
slots = get_waitress_slots(self.config, restaurant_id)
capacity_delta, _ = get_waitress_effect(restaurant_id, slots)
capacity[restaurant_id] = self.get_initial_capacity_from_grade(grade) + capacity_delta
return capacity
@staticmethod
def get_quantity_from_grade(grade):
if grade in ['bronze', 'silver']:
return 2
elif grade == 'gold':
return 3
elif grade == 'diamond':
return 4
else:
raise ValueError(f"Invalid grade: {grade}")
@cached_property
def restaurant_quantity(self):
quantity = {}
for restaurant_id in RESTAURANT_IDS:
config_data = get_restaurant_config(restaurant_id)
grade = self.config.cross_get(
get_config_key(restaurant_id, config_data['grade_key'])
)
quantity[restaurant_id] = self.get_quantity_from_grade(grade)
return quantity
def is_in_island_restaurant(self):
return self.appear(ISLAND_RESTAURANT_CHECK, offset=(0, 20))
@cached_property
def restaurant_has_event(self):
return self.appear(ISLAND_RESTAURANT_EVENT_CHECK, offset=(20, 20))
# 364 and 214
@cached_property
def _restaurant_offset_x(self):
if self.restaurant_has_event:
return 150
else:
return 0
@cached_property
def _restaurant_offset(self):
return (self._restaurant_offset_x - 20, -20, self._restaurant_offset_x + 20, 20)
def receive_revenue(self):
confirm_timer = Timer(1, count=3)
for _ in self.loop():
if self.appear_then_click(ISLAND_RESTAURANT_RECEIVE, offset=self._restaurant_offset, interval=2):
confirm_timer.reset()
continue
if self.appear(ISLAND_RESTAURANT_RESULT, offset=(20, 20), interval=2):
self.device.click(ISLAND_CLICK_SAFE_AREA)
confirm_timer.reset()
continue
if self.handle_island_additional():
confirm_timer.reset()
continue
# End
if (self.appear(ISLAND_RESTAURANT_RECOMMEND, offset=self._restaurant_offset)
or self.restaurant_resting()):
if confirm_timer.reached():
return True
@cached_property
def restaurant_grid(self):
return ButtonGrid(
origin=(583 + self._restaurant_offset_x, 208), delta=(89.5, 92),
button_shape=(83, 87), grid_shape=(5, 2), name='restaurant_grid'
)
def swipe_top_to_bottom(self):
if not self.match_template_color(ISLAND_RESTAURANT_SCROLL_TOP, offset=self._restaurant_offset):
return False
box = (RESTAURANT_SWIPE_AREA[0] + self._restaurant_offset_x, RESTAURANT_SWIPE_AREA[1],
RESTAURANT_SWIPE_AREA[2] + self._restaurant_offset_x, RESTAURANT_SWIPE_AREA[3])
self.device.swipe_vector((0, -150), box=box, padding=-5)
for _ in self.loop(timeout=0.8):
pass
return True
def swipe_bottom_to_top(self):
if not self.match_template_color(ISLAND_RESTAURANT_SCROLL_BOTTOM, offset=self._restaurant_offset):
return False
box = (RESTAURANT_SWIPE_AREA[0] + self._restaurant_offset_x, RESTAURANT_SWIPE_AREA[1],
RESTAURANT_SWIPE_AREA[2] + self._restaurant_offset_x, RESTAURANT_SWIPE_AREA[3])
self.device.swipe_vector((0, 150), box=box, padding=-5)
for _ in self.loop(timeout=0.8):
pass
return True
def scan_item_grid(self, grid: ButtonGrid):
item_grid = RestaurantItemGrid(grid)
if self.config.SHOP_EXTRACT_TEMPLATE:
item_grid.extract_template(self.device.image, folder='./assets/island/restaurant')
item_grid.predict(self.device.image, tag=True)
return item_grid.items
def scan_all_items(self):
items = []
items_set = set()
self.swipe_bottom_to_top()
result = self.scan_item_grid(self.restaurant_grid)
for item in result:
if item.name not in items_set:
items.append(item)
items_set.add(item.name)
if self.swipe_top_to_bottom():
result = self.scan_item_grid(self.restaurant_grid.move((0, 24)))
for item in result:
if item.name not in items_set:
items.append(item)
items_set.add(item.name)
return items
@cached_property
def event_buff(self):
if not self.restaurant_has_event:
return 0
ocr = Digit(ISLAND_RESTAURANT_EVENT_BUFF, lang='cnocr', letter=(67, 71, 23), threshold=160, alphabet='0123IDB')
result = ocr.ocr(self.device.image)
if not result in [10, 20, 30]:
logger.warning(f'Unexpected event buff OCR result: {result}, default to 10')
result = 10
return result
def get_sell_plan(self):
capacity = self.restaurant_capacity[self.working_restaurant_id]
menu_key = get_restaurant_config(self.working_restaurant_id)['menu_key']
menu_text = self.config.cross_get(
get_config_key(self.working_restaurant_id, menu_key),
default="{}",
)
menu = normalize_item_keys(safe_load(menu_text))
protected_items = self.restaurant_protected_items
def total_revenue_estimate(item):
amount = min(item.amount, capacity)
if item.tag == 'bonus':
return item.price * amount * (1 + self.event_buff / 100)
return item.price * amount
def has_sellable_capacity(item):
return item.amount >= capacity + protected_items.get(item.id, 0)
items = self.scan_all_items()
menu_items = [
item for item in items
if item.id in menu
# Sell one full waitress-capacity tranche while preserving manual
# hard floors and remaining season-order requirements. Reserves and
# daily buffers are soft and may be consumed by restaurants.
and has_sellable_capacity(item)
]
surplus_items = [
item for item in items
if item.id not in menu
and has_sellable_capacity(item)
]
sellable_items = menu_items + surplus_items
quantity = self.restaurant_quantity[self.working_restaurant_id]
items = sorted(sellable_items, key=total_revenue_estimate, reverse=True)
if len(items) < quantity:
quantity = len(items)
plan = items[:quantity]
logger.info(f'Sell plan: {[str(item) for item in plan]}')
return plan
@cached_property
def restaurant_protected_items(self):
hard_floor_items = normalize_item_keys(load_hard_floor_items(
self.config.cross_get("IslandProduction.IslandProduction.HardFloorItems", "")
))
stuck_season_order_id = self.config.cross_get(
"IslandOrder.IslandOrder.StuckSeasonOrderId", 0
)
season_order_items = normalize_item_keys(
get_stuck_season_order_requirements(stuck_season_order_id)
)
item_ids = set(hard_floor_items) | set(season_order_items)
return {
item_id: max(hard_floor_items.get(item_id, 0), 0)
+ max(season_order_items.get(item_id, 0), 0)
for item_id in item_ids
}
@cached_property
def waitress_lists(self):
return {
restaurant_id: list(get_waitress_slots(self.config, restaurant_id))
for restaurant_id in RESTAURANT_IDS
}
def unavailable_waitress_list(self):
current_waitresses = get_selected_named_waitresses(
get_waitress_slots(self.config, self.working_restaurant_id)
)
lst = set()
for restaurant_id in RESTAURANT_IDS:
if restaurant_id == self.working_restaurant_id:
continue
lst.update(
get_selected_named_waitresses(
get_waitress_slots(self.config, restaurant_id)
)
)
return sorted(lst - current_waitresses)
def restaurant_running(self):
return self.appear(ISLAND_RESTAURANT_RUNNING, offset=self._restaurant_offset)
def restaurant_resting(self):
return self.appear(ISLAND_RESTAURANT_RESTING, offset=self._restaurant_offset)
def choose_waitress(self):
waitress_list = list(get_waitress_slots(self.config, self.working_restaurant_id))
active_waitresses = [
waitress for waitress in waitress_list
if waitress != WAITRESS_NONE
]
if not active_waitresses:
return False
unavailable_waitress_list = set(self.unavailable_waitress_list())
named_waitresses = [
waitress for waitress in active_waitresses
if waitress not in (WAITRESS_ANY, WAITRESS_NONE)
]
all_named_waitresses = set(named_waitresses)
selected_waitresses = set()
if unavailable_waitress_list:
logger.warning(f"Unavailable waitress list: {sorted(unavailable_waitress_list)}")
for _ in self.loop():
if self.appear_then_click(ISLAND_RESTAURANT_SELECT_CHARACTER, offset=self._restaurant_offset, interval=2):
continue
if self.is_in_island_dock():
break
success = True
for waitress in named_waitresses:
if waitress in unavailable_waitress_list:
logger.warning(
f"Waitress {waitress} is already assigned to another restaurant, "
f"skip it for restaurant {self.working_restaurant_id}"
)
unavailable_waitress_list.remove(waitress)
continue
candidate = self.island_dock_find_character(waitress)
if candidate is not None and candidate.status == 'free':
self.island_dock_select_one(candidate.button)
selected_waitresses.add(candidate.identity)
continue
if candidate is not None:
time_until_update = get_server_next_update("00:00") - datetime.now()
if time_until_update >= timedelta(hours=8):
logger.warning(
f"Waitress {waitress} not available, delaying restaurant "
f"{self.working_restaurant_id} for 8 hours"
)
self.ui_back(check_button=self.is_in_island_restaurant)
raise WaitressOccupied(
f"Waitress {waitress} is occupied, delaying restaurant "
f"{self.working_restaurant_id} for 8 hours"
)
self.ensure_dock_page_at_top()
fallback_blacklist = (
unavailable_waitress_list
| selected_waitresses
| all_named_waitresses
)
selected = self.island_dock_select_character_with_blacklist(fallback_blacklist)
if selected is None:
success = False
else:
selected_waitresses.add(selected)
for _ in range(active_waitresses.count(WAITRESS_ANY)):
blacklist = unavailable_waitress_list | selected_waitresses | all_named_waitresses
selected = self.island_dock_select_character_with_blacklist(blacklist)
if selected is None:
success = False
else:
selected_waitresses.add(selected)
if not success:
logger.warning("Failed to choose waitress")
self.ui_back(check_button=self.is_in_island_restaurant)
else:
self.island_dock_select_confirm(check_button=self.is_in_island_restaurant, skip_first=False)
return success
def select_dishes(self):
plan = self.get_sell_plan()
if not plan:
return False
self.swipe_bottom_to_top()
result = self.scan_item_grid(self.restaurant_grid)
plan_to_click = []
for item in result:
if item in plan:
plan_to_click.append(item)
for _ in self.loop():
for item in plan_to_click:
if self.is_button_selected(item._button):
plan.remove(item)
plan_to_click.remove(item)
else:
self.device.click(item._button)
if not plan_to_click:
break
if not plan:
return True
if self.swipe_top_to_bottom():
result = self.scan_item_grid(self.restaurant_grid.move((0, 24)))
plan_to_click = []
for item in result:
if item in plan:
plan_to_click.append(item)
for _ in self.loop():
for item in plan_to_click:
if self.is_button_selected(item._button):
plan.remove(item)
plan_to_click.remove(item)
else:
self.device.click(item._button)
if not plan_to_click:
break
return not plan
def restaurant_start(self):
for _ in self.loop():
if self.handle_island_additional():
continue
if self.appear_then_click(ISLAND_RESTAURANT_START, offset=self._restaurant_offset, interval=2):
continue
if self.restaurant_running():
return True
def run(self):
if self.restaurant_running():
logger.info("Restaurant is already running, skip this round")
return False
self.receive_revenue()
if self.restaurant_resting():
logger.info("Restaurant is resting, finish this round")
return False
if not self.choose_waitress():
logger.warning("Failed to choose waitress, skip this round")
return False
if not self.select_dishes():
logger.warning("Failed to select all dishes, skip this round")
return False
if not self.restaurant_start():
logger.warning("Failed to start restaurant, skip this round")
return False
return True