""" Copyright (c) 2024 zhbaor This file is part of mower-ng (https://git.zhaozuohong.vip/mower-ng/mower-ng). Mower-ng is free software: you may copy, redistribute and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 or later. This file is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . This file incorporates work covered by the following copyright and permission notice: Copyright (c) 2023 Shawnsdaddy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import copy from datetime import datetime, timedelta from evalidate import Expr, base_eval_model from mower.utils import config from mower.utils.plan import PlanConfig from ..data import agent_arrange_order, agent_list, base_room_list from ..solvers.infra.record import save_action_to_sqlite_decorator from ..utils.log import logger class SkillUpgradeSupport: support_class = None level = 1 efficiency = 0 half_off = False add_on = False match = False use_booster = True name = "" swap_name = "" def __init__(self, name, skill_level, efficiency, match, swap_name="艾丽妮"): self.name = name self.level = skill_level self.efficiency = efficiency self.match = match if self.level > 1: self.half_off = True self.swap_name = swap_name class Operators: config = None operators = None exhaust_agent = [] exhaust_group = [] groups = None dorm = [] plan = None global_plan = None plan_condition = [] shadow_copy = {} current_room_changed_callback = None first_init = True skill_upgrade_supports = [] @property def party_time(self): return config.party_time def __init__(self, plan): self.operators = {} self.groups = {} self.exhaust_agent = [] self.exhaust_group = [] self.dorm = [] self.workaholic_agent = [] self.free_blacklist = [] self.global_plan = plan self.backup_plans = plan["backup_plans"] # 切换默认排班 self.swap_plan([False] * (len(self.backup_plans))) self.run_order_rooms = {} self.clues = [] self.current_room_changed_callback = None self.eval_model = base_eval_model.clone() self.eval_model.nodes.extend(["Call", "Attribute"]) self.eval_model.attributes.extend( [ "operators", "party_time", "is_working", "is_resting", "current_mood", "current_room", ] ) def __repr__(self): return f"Operators(operators={self.operators})" def calculate_switch_time(self, support: SkillUpgradeSupport): hour = 0 half_off = support.half_off level = support.level match = support.match efficiency = support.efficiency same = support.name == support.swap_name if level == 1: half_off = False # if left_minutes > 0 or left_hours > 0: # hour = left_minutes / 60 + left_hours # 基本5% basic = 5 if support.add_on: # 阿斯卡伦 basic += 5 if hour == 0: hour = level * 8 if half_off: hour = hour / 2 left = 0 if not same: left = 5 * (100 + basic + (30 if match else 0)) / 100 left = hour - left else: left = hour return left * 100 / (100 + efficiency + basic) def swap_plan(self, condition, refresh=False): self.plan = copy.deepcopy(self.global_plan["default_plan"].plan) self.config: PlanConfig = copy.deepcopy(self.global_plan["default_plan"].config) for index, success in enumerate(condition): if success: self.plan, self.config = self.merge_plan(index, self.config, self.plan) self.plan_condition = condition if refresh: self.first_init = True error = self.init_and_validate(True) self.first_init = False if error: return error def merge_plan(self, idx, ext_config, default_plan=None): if default_plan is None: default_plan = copy.deepcopy(self.global_plan["default_plan"].plan) plan = copy.deepcopy(self.global_plan["backup_plans"][idx]) # 更新切换排班表 for key, value in plan.plan.items(): if key in default_plan: for idx, operator in enumerate(value): if operator.agent != "Current": default_plan[key][idx] = operator return default_plan, ext_config.merge_config(plan.config) def generate_conditions(self, n): if n == 1: return [[True], [False]] else: prev_conditions = self.generate_conditions(n - 1) conditions = [] for condition in prev_conditions: conditions.append(condition + [True]) conditions.append(condition + [False]) return conditions def init_and_validate(self, update=False): self.groups = {} self.exhaust_agent = [] self.exhaust_group = [] self.workaholic_agent = [] self.shadow_copy = copy.deepcopy(self.operators) self.operators = {} for room in self.plan.keys(): for idx, data in enumerate(self.plan[room]): if data.agent not in list(agent_list.keys()) and data.agent != "Free": return f"干员名输入错误: 房间->{room}, 干员->{data.agent}" if data.agent in ["龙舌兰", "但书"]: return f"高效组不可用龙舌兰,但书 房间->{room}, 干员->{data.agent}" if data.agent == "菲亚梅塔" and idx == 1: return f"菲亚梅塔不能安排在2号位置 房间->{room}, 干员->{data.agent}" if data.agent == "菲亚梅塔" and not room.startswith("dorm"): return "菲亚梅塔必须安排在宿舍" if data.agent == "Free" and not room.startswith("dorm"): return f"Free只能安排在宿舍 房间->{room}, 干员->{data.agent}" if data.agent in self.operators and data.agent != "Free": return f"高效组干员不可重复 房间->{room},{self.operators[data.agent].room}, 干员->{data.agent}" self.add( Operator( data.agent, room, idx, data.group, data.replacement, "high", operator_type="high", ) ) missing_replacements = [] for room in self.plan.keys(): if room.startswith("dorm") and len(self.plan[room]) != 5: return f"宿舍 {room} 人数少于5人" for idx, data in enumerate(self.plan[room]): # 菲亚梅塔替换组做特例判断 if "龙舌兰" in data.replacement and "但书" in data.replacement: return f"替换组不可同时安排龙舌兰和但书 房间->{room}, 干员->{data.agent}" if "菲亚梅塔" in data.replacement: return f"替换组不可安排菲亚梅塔 房间->{room}, 干员->{data.agent}" r_count = len(data.replacement) if "龙舌兰" in data.replacement or "但书" in data.replacement: r_count -= 1 if r_count <= 0 and ( (data.agent != "Free" and (not room.startswith("dorm"))) or data.agent == "菲亚梅塔" ): missing_replacements.append(data.agent) for _replacement in data.replacement: if ( _replacement not in list(agent_list.keys()) and data.agent != "Free" ): return f"干员名输入错误: 房间->{room}, 干员->{_replacement}" if data.agent != "菲亚梅塔": # 普通替换 if ( _replacement in self.operators and self.operators[_replacement].is_high() ): return f"替换组不可用高效组干员: 房间->{room}, 干员->{_replacement}" self.add(Operator(_replacement, "")) else: if _replacement not in self.operators: return f"菲亚梅塔替换不在高效组列: 房间->{room}, 干员->{_replacement}" if ( _replacement in self.operators and not self.operators[_replacement].is_high() ): return f"菲亚梅塔替换只能为高效组干员: 房间->{room}, 干员->{_replacement}" # 判定替换缺失 if "菲亚梅塔" in missing_replacements: return "菲亚梅塔替换缺失" if len(missing_replacements): return f"以下干员替换组缺失:{','.join(missing_replacements)}" dorm_names = [k for k in self.plan.keys() if k.startswith("dorm")] dorm_names.sort(key=lambda d: d, reverse=False) added = [] # 竖向遍历出效率高到低 if not update: for dorm in dorm_names: free_found = False for _idx, _dorm in enumerate(self.plan[dorm]): if _dorm.agent == "Free" and _idx <= 1: if "波登可" not in [_agent.agent for _agent in self.plan[dorm]]: return "宿舍必须安排2个宿管" if _dorm.agent != "Free" and free_found: return "Free必须连续且安排在宿管后" if ( _dorm.agent == "Free" and not free_found and (dorm + str(_idx)) not in added and len(added) < self.config.max_resting_count ): self.dorm.append(Dormitory((dorm, _idx))) added.append(dorm + str(_idx)) free_found = True continue if not free_found: return "宿舍必须安排至少一个Free" # VIP休息位用完后横向遍历 for dorm in dorm_names: for _idx, _dorm in enumerate(self.plan[dorm]): if _dorm.agent == "Free" and (dorm + str(_idx)) not in added: self.dorm.append(Dormitory((dorm, _idx))) added.append(dorm + str(_idx)) else: for key, value in self.shadow_copy.items(): if key not in self.operators: self.add(Operator(key, "")) if len(self.dorm) < self.config.max_resting_count: return f"宿舍Free总数 {len(self.dorm)}小于最大分组数 {self.config.max_resting_count}" # 跑单 for x, y in self.plan.items(): if not x.startswith("room"): continue if any( ("但书" in obj.replacement or "龙舌兰" in obj.replacement) for obj in y ): self.run_order_rooms[x] = {} # 判定分组排班可能性 current_high = self.config.max_resting_count current_low = len(self.dorm) - self.config.max_resting_count for key in self.groups: high_count = 0 low_count = 0 _replacement = [] for name in self.groups[key]: _candidate = next( ( r for r in self.operators[name].replacement if r not in _replacement and r not in ["龙舌兰", "但书"] ), None, ) if _candidate is None: return f"{key} 分组无法排班,替换组数量不够" else: _replacement.append(_candidate) if self.operators[name].workaholic: continue if self.operators[name].resting_priority == "high": high_count += 1 else: low_count += 1 if high_count > current_high or low_count > current_low: return f"{key} 分组无法排班,宿舍可用高优先{current_high},低优先{current_low}->分组需要高优先{high_count},低优先{low_count}" # 设定令夕模式的心情阈值 self.init_mood_limit() for name in self.workaholic_agent: if name not in self.config.free_blacklist: self.config.free_blacklist.append(name) logger.info("宿舍黑名单:" + str(self.config.free_blacklist)) def set_mood_limit(self, name, upper_limit=24, lower_limit=0): if name in self.operators: self.operators[name].upper_limit = upper_limit self.operators[name].lower_limit = lower_limit logger.info(f"自动设置{name}心情下限为{lower_limit},上限为{upper_limit}") def init_mood_limit(self): # 设置心情阈值 for 夕,令, if self.config.ling_xi == 1: self.set_mood_limit("令", upper_limit=12) self.set_mood_limit("夕", lower_limit=12) elif self.config.ling_xi == 2: self.set_mood_limit("夕", upper_limit=12) self.set_mood_limit("令", lower_limit=12) elif self.config.ling_xi == 0: self.set_mood_limit("夕") self.set_mood_limit("令") # 设置同组心情阈值 finished = [] for name in ["夕", "令"]: if ( name in self.operators and self.operators[name].group != "" and self.operators[name].group not in finished ): for group_name in self.groups[self.operators[name].group]: if group_name not in ["夕", "令"]: if self.config.ling_xi in [1, 2]: self.set_mood_limit(group_name, lower_limit=12) elif self.config.ling_xi == 0: self.set_mood_limit(group_name, lower_limit=0) finished.append(self.operators[name].group) # 设置铅踝心情阈值 # 三种情况: # 1. 铅踝不是主力:不管 # 2. 铅踝是红云组主力,设置心情上限 12、下限 8,效率 37% # 3. 铅踝是普通主力:设置心情下限 20,效率 30% TOTTER = "铅踝" VERMEIL = "红云" if TOTTER in self.operators and self.operators[TOTTER].operator_type == "high": if ( VERMEIL in self.operators and self.operators[VERMEIL].operator_type == "high" and self.operators[VERMEIL].room == self.operators[TOTTER].room ): self.set_mood_limit(TOTTER, upper_limit=12, lower_limit=8) else: self.set_mood_limit(TOTTER, upper_limit=24, lower_limit=20) def evaluate_expression(self, expression): try: result = Expr(expression, self.eval_model).eval({"op_data": self}) return result except Exception as e: logger.exception(f"Error evaluating expression: {e}") return None def get_current_room(self, room, bypass=False, current_index=None): room_data = { v.current_index: v for k, v in self.operators.items() if v.current_room == room } res = [obj.agent for obj in self.plan[room]] not_found = False for idx, op in enumerate(res): if idx in room_data: res[idx] = room_data[idx].name else: res[idx] = "" if current_index is not None and idx not in current_index: continue not_found = True if not_found and not bypass: return None else: return res def predict_fia(self, operators, fia_mood, hours=240): recover_hours = (24 - fia_mood) / 2 for agent in operators: agent.mood -= agent.depletion_rate * recover_hours if agent.mood < 0.0: return False if recover_hours >= hours or 0 < recover_hours < 1: return True operators.sort( key=lambda x: (x.mood - x.lower_limit) / (x.upper_limit - x.lower_limit), reverse=False, ) fia_mood = operators[0].mood operators[0].mood = 24 return self.predict_fia(operators, fia_mood, hours - recover_hours) def reset_dorm_time(self): for name in self.operators.keys(): agent = self.operators[name] if agent.room.startswith("dorm"): agent.time_stamp = None @save_action_to_sqlite_decorator def update_detail(self, name, mood, current_room, current_index, update_time=False): agent = self.operators[name] if update_time: if agent.time_stamp is not None and agent.mood > mood: agent.depletion_rate = ( (agent.mood - mood) * 3600 / ((datetime.now() - agent.time_stamp).total_seconds()) ) agent.time_stamp = datetime.now() # 如果移出宿舍,则清除对应宿舍数据 且重新记录高效组心情(如果有备用班,则跳过高效组判定) if ( agent.current_room.startswith("dorm") and not current_room.startswith("dorm") and (agent.is_high() or self.backup_plans) ): self.refresh_dorm_time( agent.current_room, agent.current_index, {"agent": ""} ) if update_time: self.time_stamp = datetime.now() else: self.time_stamp = None agent.depletion_rate = 0 if ( self.get_dorm_by_name(name)[0] is not None and not current_room.startswith("dorm") and (agent.is_high() or self.backup_plans) ): _dorm = self.get_dorm_by_name(name)[1] _dorm.name = "" _dorm.time = None agent.current_room = current_room agent.current_index = current_index agent.mood = mood # 如果是高效组且没有记录时间,则返还index if agent.current_room.startswith("dorm") and ( agent.is_high() or self.backup_plans ): for dorm in self.dorm: if ( dorm.position[0] == current_room and dorm.position[1] == current_index and dorm.time is None ): return current_index if agent.name == "菲亚梅塔" and ( self.operators["菲亚梅塔"].time_stamp is None or self.operators["菲亚梅塔"].time_stamp < datetime.now() ): return current_index def refresh_dorm_time(self, room, index, agent): for idx, dorm in enumerate(self.dorm): # Filter out resting priority low # if idx >= self.config.max_resting_count: # break if dorm.position[0] == room and dorm.position[1] == index: # 如果人为高效组,则记录时间 _name = agent["agent"] if _name in self.operators.keys() and ( self.operators[_name].is_high() or self.config.free_room ): dorm.name = _name _agent = self.operators[_name] # 如果干员有心情上限,则按比例修改休息时间 if _agent.mood != 24: sec_remaining = ( (_agent.upper_limit - _agent.mood) * ((agent["time"] - _agent.time_stamp).total_seconds()) / (24 - _agent.mood) ) dorm.time = _agent.time_stamp + timedelta(seconds=sec_remaining) else: dorm.time = agent["time"] elif _name in list(agent_list.keys()): dorm.name = _name dorm.time = agent["time"] break def correct_dorm(self): for idx, dorm in enumerate(self.dorm): if dorm.name != "" and dorm.name in self.operators.keys(): op = self.operators[dorm.name] if not ( dorm.position[0] == op.current_room and dorm.position[1] == op.current_index ): self.dorm[idx].name = "" self.dorm[idx].time = None else: if ( self.dorm[idx].time is not None and self.dorm[idx].time < datetime.now() ): op.mood = op.upper_limit op.time_stamp = self.dorm[idx].time logger.debug( f"检测到{op.name}心情恢复满,设置心情至{op.upper_limit}" ) def get_train_support(self): for name in self.operators.keys(): agent = self.operators[name] if agent.current_room == "train" and agent.current_index == 0: return agent.name return None def get_refresh_index(self, room, plan): ret = [] if room.startswith("dorm") and self.config.free_room: return [i for i, x in enumerate(self.plan[room]) if x == "Free"] for idx, dorm in enumerate(self.dorm): # Filter out resting priority low if idx >= self.config.max_resting_count: if not self.config.free_room: break if dorm.position[0] == room: for i, _name in enumerate(plan): if _name not in self.operators.keys(): self.add(Operator(_name, "")) if not self.config.free_room: if self.operators[_name].is_high() and not self.operators[ _name ].room.startswith("dorm"): ret.append(i) elif not self.operators[_name].room.startswith("dorm"): ret.append(i) break return ret def get_dorm_by_name(self, name): for idx, dorm in enumerate(self.dorm): if dorm.name == name: return idx, dorm return None, None def add(self, operator): if operator.name not in list(agent_list.keys()): return if self.config.is_resting_priority(operator.name): operator.resting_priority = "low" operator.exhaust_require = self.config.is_exhaust_require(operator.name) operator.rest_in_full = self.config.is_rest_in_full(operator.name) operator.workaholic = self.config.is_workaholic(operator.name) operator.refresh_order_room = self.config.is_refresh_trading(operator.name) if operator.name in agent_arrange_order: operator.arrange_order = agent_arrange_order[operator.name] # 复制基建数据 if operator.name in self.shadow_copy: exist = self.shadow_copy[operator.name] operator.mood = exist.mood operator.time_stamp = exist.time_stamp operator.depletion_rate = exist.depletion_rate operator.current_room = exist.current_room operator.current_index = exist.current_index self.operators[operator.name] = operator # 需要用尽心情干员逻辑 if ( operator.exhaust_require or operator.group in self.exhaust_group ) and operator.name not in self.exhaust_agent: self.exhaust_agent.append(operator.name) if operator.group != "": self.exhaust_group.append(operator.group) # 干员分组逻辑 if operator.group != "": if operator.group not in self.groups.keys(): self.groups[operator.group] = [operator.name] else: self.groups[operator.group].append(operator.name) if operator.workaholic and operator.name not in self.workaholic_agent: self.workaholic_agent.append(operator.name) def available_free(self, free_type="high"): ret = 0 freeName = [] if free_type == "high": idx = 0 for dorm in self.dorm: if dorm.name == "" or ( dorm.name in self.operators.keys() and not self.operators[dorm.name].is_high() ): ret += 1 elif dorm.time is not None and dorm.time < datetime.now(): logger.info(f"检测到房间休息完毕,释放{dorm.name}宿舍位") freeName.append(dorm.name) ret += 1 if idx == self.config.max_resting_count - 1: break else: idx += 1 else: idx = self.config.max_resting_count for i in range(idx, len(self.dorm)): dorm = self.dorm[i] # 释放满休息位 # TODO 高效组且低优先可以相互替换 if dorm.name == "" or ( dorm.name in self.operators.keys() and not self.operators[dorm.name].is_high() ): ret += 1 elif dorm.time is not None and dorm.time < datetime.now(): logger.info(f"检测到房间休息完毕,释放{dorm.name}宿舍位") freeName.append(dorm.name) ret += 1 if len(freeName) > 0: for name in freeName: if name in list(agent_list.keys()): self.operators[name].mood = self.operators[name].upper_limit self.operators[name].depletion_rate = 0 self.operators[name].time_stamp = datetime.now() return ret def assign_dorm(self, name): is_high = self.operators[name].resting_priority == "high" if is_high: _room = next( obj for obj in self.dorm if obj.name not in self.operators.keys() or not self.operators[obj.name].is_high() ) else: _room = None for i in range(self.config.max_resting_count, len(self.dorm)): _name = self.dorm[i].name if _name == "" or not self.operators[_name].is_high(): _room = self.dorm[i] break _room.name = name return _room def get_current_operator(self, room, index): for key, value in self.operators.items(): if value.current_room == room and value.current_index == index: return value return None def print(self): ret = "{" op = [] dorm = [] for k, v in self.operators.items(): op.append("'" + k + "': " + str(vars(v))) ret += "'operators': {" + ",".join(op) + "}," for v in self.dorm: dorm.append(str(vars(v))) ret += "'dorms': [" + ",".join(dorm) + "]}" return ret class Dormitory: def __init__(self, position, name="", time=None): self.position = position self.name = name self.time = time def __repr__(self): return ( f"Dormitory(position={self.position},name='{self.name}',time='{self.time}')" ) class Operator: time_stamp = None depletion_rate = 0 workaholic = False arrange_order = [2, "false"] def __init__( self, name, room, index=-1, group="", replacement=[], resting_priority="low", current_room="", exhaust_require=False, mood=24, upper_limit=24, rest_in_full=False, current_index=-1, lower_limit=0, operator_type="low", depletion_rate=0, time_stamp=None, refresh_order_room=None, ): if refresh_order_room is not None: self.refresh_order_room = refresh_order_room self.refresh_order_room = [False, []] self.name = name self.room = room self.operator_type = operator_type self.index = index self.group = group self.replacement = replacement self.resting_priority = resting_priority self._current_room = None self.current_room = current_room self.exhaust_require = exhaust_require self.upper_limit = upper_limit self.rest_in_full = rest_in_full self.mood = mood self.current_index = current_index self.lower_limit = lower_limit self.depletion_rate = depletion_rate self.time_stamp = time_stamp @property def current_room(self): return self._current_room @current_room.setter def current_room(self, value): if self._current_room != value: self._current_room = value if Operators.current_room_changed_callback and self.refresh_order_room[0]: Operators.current_room_changed_callback(self) def is_high(self): return self.operator_type == "high" def is_resting(self): return self.current_room.startswith("dorm") def is_working(self): return self.current_room in base_room_list and not self.is_resting() def need_to_refresh(self, h=2, r=""): # 是否需要读取心情 if ( self.time_stamp is None or ( self.time_stamp is not None and self.time_stamp + timedelta(hours=h) < datetime.now() ) or (r.startswith("dorm") and not self.room.startswith("dorm")) ): return True def not_valid(self): if self.room == "train": return False if self.operator_type == "high": if self.workaholic: return ( self.current_room != self.room or self.index != self.current_index ) if not self.room.startswith("dorm") and self.current_room.startswith( "dorm" ): if self.mood == -1 or self.mood == 24: return True else: return False return ( self.need_to_refresh(2.5) or self.current_room != self.room or self.index != self.current_index ) return False def current_mood(self): predict = self.mood if self.time_stamp is not None: predict = ( self.mood - self.depletion_rate * (datetime.now() - self.time_stamp).total_seconds() / 3600 ) if 0 <= predict <= 24: return predict else: return self.mood def __repr__(self): return f"Operator(name='{self.name}', room='{self.room}', index={self.index}, group='{self.group}', replacement={self.replacement}, resting_priority='{self.resting_priority}', current_room='{self.current_room}',exhaust_require={self.exhaust_require},mood={self.mood}, upper_limit={self.upper_limit}, rest_in_full={self.rest_in_full}, current_index={self.current_index}, lower_limit={self.lower_limit}, operator_type='{self.operator_type}',depletion_rate={self.depletion_rate},time_stamp='{self.time_stamp}',refresh_order_room = {self.refresh_order_room})"