import numpy as np import urllib.request, json, time, os, copy, sys from scipy.optimize import linprog from collections import defaultdict as ddict global penguin_url, headers, LanguageMap penguin_url = 'https://penguin-stats.io/PenguinStats/api/v2/' headers = {'User-Agent':'ArkPlanner'} LanguageMap = {'CN': 'zh', 'US': 'en', 'JP': 'ja', 'KR': 'ko'} Price = dict() with open('price.txt', 'r', encoding='utf8') as f: for line in f.readlines(): name, value = line.split() Price[name] = int(value) class MaterialPlanning(object): def __init__(self, filter_freq=200, filter_stages=[], url_stats='result/matrix?show_closed_zone=true', url_rules='formula', path_stats='data/matrix.json', path_rules='data/formula.json', update=False, banned_stages={}, # expValue=30, ConvertionDR=0.18, display_main_only=True): """ Object initialization. Args: filter_freq: int or None. The lowest frequence that we consider. No filter will be applied if None. url_stats: string. url to the dropping rate stats data. url_rules: string. url to the composing rules data. path_stats: string. local path to the dropping rate stats data. path_rules: string. local path to the composing rules data. """ self.banned_stages = banned_stages # for debugging self.display_main_only = display_main_only self.ConvertionDR = ConvertionDR self.update(force=update) def get_item_id(self): items = get_json('items') item_array, item_id_to_name = [], {} item_name_to_id = {'id': {}, 'zh': {}, 'en': {}, 'ja': {}, 'ko': {}} additional_items = [ {'itemId': '4001', 'name_i18n': {'ko': '용문폐', 'ja': '龍門幣', 'en': 'LMD', 'zh': '龙门币'}}, {'itemId': '0010', 'name_i18n': {'ko': '작전기록', 'ja': '作戦記録', 'en': 'Battle Record', 'zh': '作战记录'}} ] for x in items + additional_items: item_array.append(x['itemId']) item_id_to_name.update({x['itemId']: {'id': x['itemId'], 'zh': x['name_i18n']['zh'], 'en': x['name_i18n']['en'], 'ja': x['name_i18n']['ja'], 'ko': x['name_i18n']['ko']}}) item_name_to_id['id'].update({x['itemId']: x['itemId']}) item_name_to_id['zh'].update({x['name_i18n']['zh']: x['itemId']}) item_name_to_id['en'].update({x['name_i18n']['en']: x['itemId']}) item_name_to_id['ja'].update({x['name_i18n']['ja']: x['itemId']}) item_name_to_id['ko'].update({x['name_i18n']['ko']: x['itemId']}) self.item_array = item_array self.item_id_to_name = item_id_to_name self.item_name_to_id = item_name_to_id self.item_dct_rv = {v: k for k, v in enumerate(item_array)} # from id to idx self.item_name_rv = {item_id_to_name[v]['zh']: k for k, v in enumerate(item_array)} # from (zh) name to idx def _pre_processing(self, material_probs): """ Compute costs, convertion rules and items probabilities from requested dictionaries. Args: material_probs: List of dictionaries recording the dropping info per stage per item. Keys of instances: ["itemID", "times", "itemName", "quantity", "apCost", "stageCode", "stageID"]. convertion_rules: List of dictionaries recording the rules of composing. Keys of instances: ["id", "name", "level", "source", "madeof"]. """ # construct item id projections. # construct stage id projections. stage_array = [] for drop in material_probs['matrix']: if drop['stageId'] not in stage_array: stage_array.append(drop['stageId']) stage_dct_rv = {v: k for k, v in enumerate(stage_array)} servers = ['CN', 'US', 'JP', 'KR'] languages = ['zh', 'en', 'ja', 'ko'] valid_stages = {server: [False]*len(stage_array) for server in servers} stage_code = {server: ['' for _ in stage_array] for server in servers} stages = {} stage_name_rv = {lang: {} for lang in languages} stage_id_to_name = {} for server in servers: try: stages[server] = get_json(f'stages?server={server}') except Exception as e: print(f'Failed to load server {server}, Error: {e}') return -1 for stage in stages[server]: if stage['stageId'] not in stage_dct_rv or 'dropInfos' not in stage: continue valid_stages[server][stage_dct_rv[stage['stageId']]] = True stage_code[server][stage_dct_rv[stage['stageId']]] = stage['code_i18n'][LanguageMap[server]] for lang in languages: stage_name_rv[lang][stage['code_i18n'][lang]] = stage_dct_rv[stage['stageId']] stage_id_to_name[stage['stageId']] = {lang: stage['code_i18n'][lang] for lang in languages} # Fix KeyError('id') stage_id_to_name[stage['stageId']]["id"] = stage['stageId'] try: self.get_item_id() except Exception as e: print(f'Failed to load item list, Error: {e}') return -1 self.stage_array = stage_array self.stage_dct_rv = stage_dct_rv self.stage_code = stage_code self.valid_stages = valid_stages self.stage_name_rv = stage_name_rv self.stage_id_to_name = stage_id_to_name # To format dropping records into sparse probability matrix self.cost_lst = np.zeros(len(self.stage_array)) for server in servers: for stage in stages[server]: if stage['stageId'] in self.stage_dct_rv: self.cost_lst[self.stage_dct_rv[stage['stageId']]] = stage['apCost'] self.update_stage() self.stage_array = np.array(self.stage_array) self.probs_matrix = np.zeros([len(self.stage_array), len(self.item_array)]) for drop in material_probs['matrix']: try: self.probs_matrix[self.stage_dct_rv[drop['stageId']], self.item_dct_rv[drop['itemId']]] = drop['quantity']/float(drop['times']) except: print(f'Failed to parse {drop}. (出现此条请带报错信息联系根派)') # 添加LS, CE, S4-6, S5-2等的掉落 及 默认龙门币掉落 for k, stage in enumerate(self.stage_array): self.probs_matrix[k, self.item_name_rv['龙门币']] = self.cost_lst[k]*12 self.update_droprate() # To build equavalence relationship from convert_rule_dct. self.update_convertion() self.convertions_dct = {} convertion_matrix = [] convertion_outc_matrix = [] convertion_cost_lst = [] for rule in self.convertion_rules: convertion = np.zeros(len(self.item_array)) convertion[self.item_name_rv[rule['name']]] = 1 comp_dct = {comp['name']:comp['count'] for comp in rule['costs']} self.convertions_dct[rule['name']] = comp_dct for iname in comp_dct: convertion[self.item_name_rv[iname]] -= comp_dct[iname] convertion[self.item_name_rv['龙门币']] -= rule['goldCost'] convertion_matrix.append(copy.deepcopy(convertion)) outc_dct = {outc['name']:outc['count'] for outc in rule['extraOutcome']} outc_wgh = {outc['name']:outc['weight'] for outc in rule['extraOutcome']} weight_sum = float(rule['totalWeight']) for iname in outc_dct: convertion[self.item_name_rv[iname]] += outc_dct[iname]*self.ConvertionDR*outc_wgh[iname]/weight_sum convertion_outc_matrix.append(convertion) convertion_cost_lst.append(0) self.convertion_matrix = np.array(convertion_matrix) self.convertion_outc_matrix = np.array(convertion_outc_matrix) self.convertion_cost_lst = convertion_cost_lst def _set_lp_parameters(self): """ Object initialization. Args: convertion_matrix: matrix of shape [n_rules, n_items]. Each row represent a rule. convertion_cost_lst: list. Cost in equal value to the currency spent in convertion. probs_matrix: sparse matrix of shape [n_stages, n_items]. Items per clear (probabilities) at each stage. cost_lst: list. Costs per clear at each stage. """ assert len(self.probs_matrix)==len(self.cost_lst) assert len(self.convertion_matrix)==len(self.convertion_cost_lst) assert self.probs_matrix.shape[1]==self.convertion_matrix.shape[1] def update(self, filter_freq=200, filter_stages=[], url_stats='result/matrix?show_closed_zone=true', url_rules='formula', path_stats='data/matrix.json', path_rules='data/formula.json', force=True): """ To update parameters when probabilities change or new items added. Args: url_stats: string. url to the dropping rate stats data. url_rules: string. url to the composing rules data. path_stats: string. local path to the dropping rate stats data. path_rules: string. local path to the composing rules data. """ print(f'Start to update data {time.asctime(time.localtime(time.time()))}.') if not force: # if not force to update, try loading data from file. try: material_probs, self.convertion_rules = load_data(path_stats, path_rules) except: # loading failed, try loading from server. force = True if force: # load from server. try: print('Requesting data from web resources (i.e., penguin-stats.io)...', end=' ') material_probs, self.convertion_rules = request_data(penguin_url+url_stats, penguin_url+url_rules, path_stats, path_rules) print('done.') except: return if filter_freq: filtered_probs = [] for drop in material_probs['matrix']: if drop['times']>=filter_freq and drop['stageId'] not in filter_stages: filtered_probs.append(drop) material_probs['matrix'] = filtered_probs if self._pre_processing(material_probs) != -1: self._set_lp_parameters() def _get_plan_no_prioties(self, demand_lst, outcome, gold_demand, exp_demand, convertion_dr, probs_matrix, convertion_matrix, convertion_outc_matrix, cost_lst, convertion_cost_lst): """ To solve linear programming problem without prioties. Args: demand_lst: list of materials demand. Should include all items (zero if not required). Returns: strategy: list of required clear times for each stage. fun: estimated total cost. """ if convertion_dr != 0.18: convertion_outc_matrix = (convertion_outc_matrix - convertion_matrix)/0.18*convertion_dr+convertion_matrix A_ub = (np.vstack([probs_matrix, convertion_outc_matrix]) if outcome else np.vstack([probs_matrix, convertion_matrix])).T cost = (np.hstack([cost_lst, convertion_cost_lst])) assert np.any(cost_lst>=0) excp_factor = 1.0 dual_factor = 1.0 while excp_factor>1e-7: solution = linprog(c=cost, A_ub=-A_ub, b_ub=-np.array(demand_lst)*excp_factor, method='interior-point') if solution.status != 4: break excp_factor /= 10.0 while dual_factor>1e-7: dual_solution = linprog(c=-np.array(demand_lst)*excp_factor*dual_factor, A_ub=A_ub.T, b_ub=cost, method='interior-point') if solution.status != 4: break dual_factor /= 10.0 return solution, dual_solution, excp_factor def get_plan(self, requirement_dct={}, deposited_dct={}, print_output=True, outcome=False, gold_demand=True, exp_demand=True, exclude=[], store=False, convertion_dr=0.18, input_lang='zh', output_lang='zh', server='CN'): """ User API. Computing the material plan given requirements and owned items. Args: requirement_dct: dictionary. Contain only required items with their numbers. deposit_dct: dictionary. Contain only owned items with their numbers. """ status_dct = {0: 'Optimization terminated successfully. ', 1: 'Iteration limit reached. ', 2: 'Problem appears to be infeasible. ', 3: 'Problem appears to be unbounded. ', 4: 'Numerical difficulties encountered.'} demand_lst = np.zeros(len(self.item_array)) for k, v in requirement_dct.items(): demand_lst[self.item_dct_rv[self.item_name_to_id[input_lang][k]]] = v if gold_demand: try: demand_lst[self.item_name_rv['龙门币']] = 1e9 if gold_demand is True else int(gold_demand) except: demand_lst[self.item_name_rv['龙门币']] = 1e9 if exp_demand: try: demand_lst[self.item_name_rv['作战记录']] = 1e9 if exp_demand is True else int(exp_demand) except: demand_lst[self.item_name_rv['作战记录']] = 1e9 for k, v in deposited_dct.items(): demand_lst[self.item_dct_rv[self.item_name_to_id[input_lang][k]]] -= v if gold_demand == False and exp_demand == True: # 如果不需要龙门币 并 需要经验, 就删掉赤金到经验的转化 convertion_matrix = self.convertion_matrix[:-1] convertion_outc_matrix = self.convertion_outc_matrix[:-1] convertion_cost_lst = self.convertion_cost_lst[:-1] else: convertion_matrix = self.convertion_matrix convertion_outc_matrix = self.convertion_outc_matrix convertion_cost_lst = self.convertion_cost_lst def alive(stage): if stage in exclude: return False if self.stage_code[server][self.stage_dct_rv[stage]] in exclude: return False return self.valid_stages[server][self.stage_dct_rv[stage]] is_stage_alive = [alive(stage) for stage in self.stage_array] stage_array = self.stage_array[is_stage_alive] cost_lst = self.cost_lst[is_stage_alive] probs_matrix = self.probs_matrix[is_stage_alive] stage_dct_rv = {v:k for k,v in enumerate(stage_array)} solution, dual_solution, excp_factor = self._get_plan_no_prioties( demand_lst, outcome, gold_demand, exp_demand, convertion_dr, probs_matrix, convertion_matrix, convertion_outc_matrix, cost_lst, convertion_cost_lst) x, status = solution.x/excp_factor, solution.status y, slack = dual_solution.x, dual_solution.slack n_looting, n_convertion = x[:len(cost_lst)], x[len(cost_lst):] if status != 0: raise ValueError(status_dct[status]) values = [{"level":'1', "items":[]}, {"level":'2', "items":[]}, {"level":'3', "items":[]}, {"level":'4', "items":[]}, {"level":'5', "items":[]}] item_values = dict() for i, item in enumerate(self.item_array): if y[i] >= 0 and '作战记录' not in self.item_id_to_name[item]['zh'] and\ self.item_id_to_name[item]['zh'] not in ['龙门币', '赤金', '碳', '碳素', '碳素组', '经验', '家具'] and\ '技巧概要' not in self.item_id_to_name[item]['zh']: if y[i]>0.1: item_value = { "name": self.item_id_to_name[item][output_lang], "value": '%.2f'%y[i] } else: item_value = { "name": self.item_id_to_name[item][output_lang], "value": '%.5f'%(y[i]) } if self.item_array[i][-1] in '12345' and len(self.item_array[i]) == 5: values[int(self.item_array[i][-1])-1]['items'].append(item_value) item_values[item] = y[i] for group in values: group["items"] = sorted(group["items"], key=lambda k: float(k['value']), reverse=True) cost = np.dot(x[:len(cost_lst)], cost_lst) gcost = -np.dot(x[len(cost_lst):], convertion_matrix[:, self.item_name_rv['龙门币']]) gold = np.dot(n_looting, probs_matrix[:, self.item_name_rv['龙门币']]) exp = np.dot(n_looting, probs_matrix[:, self.item_name_rv['基础作战记录']])*200 +\ np.dot(n_looting, probs_matrix[:, self.item_name_rv['初级作战记录']])*400 +\ np.dot(n_looting, probs_matrix[:, self.item_name_rv['中级作战记录']])*1000 +\ np.dot(n_looting, probs_matrix[:, self.item_name_rv['作战记录']]) stages = [] for i, t in enumerate(n_looting): if t >= 0.1: stage_name = stage_array[i] if self.is_gold_or_exp(stage_name): cost -= t*cost_lst[i] gold -= t*probs_matrix[i, self.item_name_rv['龙门币']] exp -= t*(probs_matrix[i, self.item_name_rv['基础作战记录']]*200 + probs_matrix[i, self.item_name_rv['初级作战记录']]*400 + probs_matrix[i, self.item_name_rv['中级作战记录']]*1000 + probs_matrix[i, self.item_name_rv['作战记录']]) if self.stage_code['CN'][self.stage_dct_rv[stage_name]][:2] in ['SK', 'AP', 'CE', 'LS', 'PR'] and\ self.display_main_only: continue target_items = np.where(probs_matrix[i]>0.02)[0] items = {self.item_id_to_name[self.item_array[idx]][output_lang]: float2str(probs_matrix[i, idx]*t) for idx in target_items if len(self.item_array[idx])==5 and self.item_array[idx] != 'furni'} stage = { "stage": self.stage_id_to_name[stage_array[i]][output_lang], "count": float2str(t), "items": items } stages.append(stage) syntheses = [] for i,t in enumerate(n_convertion): if t >= 0.1: target_item = self.item_array[np.argmax(convertion_matrix[i])] if self.item_id_to_name[target_item]['zh'] in ['作战记录', '龙门币']: # 不显示经验和龙门币的转化 continue materials = {self.item_id_to_name[self.item_name_to_id['zh'][k]][output_lang]: f'{v * t:.1f}' for k, v in self.convertions_dct[self.item_id_to_name[target_item]['zh']].items()} synthesis = { "target": self.item_id_to_name[target_item][output_lang], "count": str(int(t+0.9)), "materials": materials } syntheses.append(synthesis) res = { "cost": int(cost), "gcost": int(gcost), 'gold': int(gold), 'exp': int(exp), "stages": stages, "syntheses": syntheses, "values": list(reversed(values)) } if store: green = {item['name']: '%.3f' % (float(item['value'])/Price[self.item_id_to_name[self.item_name_to_id[output_lang][item['name']]]['zh']]) for item in values[2]['items']} yellow = {item['name']: '%.3f' % (float(item['value'])/Price[self.item_id_to_name[self.item_name_to_id[output_lang][item['name']]]['zh']]) for item in values[3]['items']} res.update({'green': green, 'yellow': yellow}) if print_output: print('Estimated total cost: %d, gold: %d, exp: %d.'%(res['cost'], res['gold'], res['exp'])) print('Loot at following stages:') for stage in stages: display_lst = [k + '(%s) '%stage['items'][k] for k in stage['items']] print('Stage ' + self.stage_code[server][self.stage_name_rv[output_lang][stage['stage']]] + '(%s times) ===> '%stage['count'] + ', '.join(display_lst)) print('\nSynthesize following items:') for synthesis in syntheses: display_lst = [k + '(%s) '%synthesis['materials'][k] for k in synthesis['materials']] print(synthesis['target'] + '(%s) <=== '%synthesis['count'] + ', '.join(display_lst)) print('\nItems Values:') for i, group in reversed(list(enumerate(values))): display_lst = ['%s:%s'%(item['name'], item['value']) for item in group['items']] print('Level %d items: '%(i+1)) print(', '.join(display_lst)) return res def is_gold_or_exp(self, stage_name): return self.stage_code['CN'][self.stage_dct_rv[stage_name]][:2] in ['LS', 'CE'] def update_droprate(self): self.update_droprate_processing('S4-6', '龙门币', 3228) self.update_droprate_processing('S5-2', '龙门币', 2484) self.update_droprate_processing('S6-4', '龙门币', 2700, 'update') self.update_droprate_processing('CE-1', '龙门币', 1700, 'update') self.update_droprate_processing('CE-2', '龙门币', 2800, 'update') self.update_droprate_processing('CE-3', '龙门币', 4100, 'update') self.update_droprate_processing('CE-4', '龙门币', 5700, 'update') self.update_droprate_processing('CE-5', '龙门币', 7500, 'update') self.update_droprate_processing('LS-1', '作战记录', 1600, 'update') self.update_droprate_processing('LS-2', '作战记录', 2800, 'update') self.update_droprate_processing('LS-3', '作战记录', 3900, 'update') self.update_droprate_processing('LS-4', '作战记录', 5900, 'update') self.update_droprate_processing('LS-5', '作战记录', 7400, 'update') def update_convertion_processing(self, target_item: tuple, cost: int, source_item: dict, extraOutcome: dict): ''' target_item: (item, itemCount) cost: number of 龙门币 source_item: {item: itemCount} extraOutcome: {outcome: {item: weight}, rate, totalWeight} ''' toAppend = dict() Outcome, rate, totalWeight = extraOutcome toAppend['costs'] = [{'count':x[1]/target_item[1], 'id':self.item_name_rv[x[0]], 'name':x[0]} for x in source_item.items()] toAppend['extraOutcome'] = [{'count': rate, 'id': self.item_name_rv[x[0]], 'name': x[0], 'weight': x[1]/target_item[1]} for x in Outcome.items()] toAppend['goldCost'] = cost/target_item[1] toAppend['id'] = self.item_name_to_id['zh'][target_item[0]] toAppend['name'] = target_item[0] toAppend['totalWeight'] = totalWeight self.convertion_rules.append(toAppend) def update_convertion(self): # 之前的TODO: 考虑芯片/基建材料的不同副产物掉落 <- 不做了, 一般人不用planner做芯片规划 self.update_convertion_processing(('作战记录', 200), 0, {'基础作战记录': 1}, ({}, 0, 1)) self.update_convertion_processing(('作战记录', 400), 0, {'初级作战记录': 1}, ({}, 0, 1)) self.update_convertion_processing(('作战记录', 1000), 0, {'中级作战记录': 1}, ({}, 0, 1)) self.update_convertion_processing(('作战记录', 1000), 0, {'高级作战记录': 1}, ({}, 0, 1)) # 这里一定保证这一条在最后! # ENSURE THIS LINE IS THE LAST LINE! self.update_convertion_processing(('作战记录', 400), 0, {'赤金': 1}, ({}, 0, 1)) def update_stage(self): self.update_stage_processing('LS-1', 10, 'wk_kc_1') self.update_stage_processing('LS-2', 15, 'wk_kc_2') self.update_stage_processing('LS-3', 20, 'wk_kc_3') self.update_stage_processing('LS-4', 25, 'wk_kc_4') self.update_stage_processing('LS-5', 30, 'wk_kc_5') self.update_stage_processing('CE-1', 10, 'wk_melee_1') self.update_stage_processing('CE-2', 15, 'wk_melee_2') self.update_stage_processing('CE-3', 20, 'wk_melee_3') self.update_stage_processing('CE-4', 25, 'wk_melee_4') self.update_stage_processing('CE-5', 30, 'wk_melee_5') def update_stage_processing(self, stage_name: str, cost: int, code: str) -> None: self.stage_array.append(code) self.stage_dct_rv.update({code: len(self.stage_array)-1}) self.cost_lst = np.append(self.cost_lst, cost) servers = ['CN', 'US', 'JP', 'KR'] for server in servers: self.stage_code[server].append(stage_name) self.valid_stages[server].append(True) def update_droprate_processing(self, stage, item, droprate, mode='add'): if stage not in self.stage_name_rv['zh']: print(f'stage {stage} not found') return if item not in self.item_name_rv: print(f'item {item} not found') return stageid = self.stage_name_rv['zh'][stage] itemid = self.item_name_rv[item] if mode == 'add': self.probs_matrix[stageid][itemid] += droprate elif mode == 'update': self.probs_matrix[stageid][itemid] = droprate def get_json(s): req = urllib.request.Request(penguin_url + s, None, headers) with urllib.request.urlopen(req, timeout=5) as response: return json.loads(response.read().decode()) def Cartesian_sum(arr1, arr2): arr_r = [] for arr in arr1: arr_r.append(arr+arr2) arr_r = np.vstack(arr_r) return arr_r def float2str(x, offset=0.5): if x < 1.0: out = '%.1f'%x else: out = '%d'%(int(x+offset)) return out def request_data(url_stats, url_rules, save_path_stats, save_path_rules): """ To request probability and convertion rules from web resources and store at local. Args: url_stats: string. url to the dropping rate stats data. url_rules: string. url to the composing rules data. save_path_stats: string. local path for storing the stats data. save_path_rules: string. local path for storing the composing rules data. Returns: material_probs: dictionary. Content of the stats json file. convertion_rules: dictionary. Content of the rules json file. """ try: os.mkdir(os.path.dirname(save_path_stats)) except: pass try: os.mkdir(os.path.dirname(save_path_rules)) except: pass req = urllib.request.Request(url_stats, None, headers) with urllib.request.urlopen(req, timeout=5) as response: material_probs = json.loads(response.read().decode()) with open(save_path_stats, 'w') as outfile: json.dump(material_probs, outfile) req = urllib.request.Request(url_rules, None, headers) with urllib.request.urlopen(req, timeout=5) as response: response = urllib.request.urlopen(req) convertion_rules = json.loads(response.read().decode()) with open(save_path_rules, 'w') as outfile: json.dump(convertion_rules, outfile) return material_probs, convertion_rules def load_data(path_stats, path_rules): """ To load stats and rules data from local directories. Args: path_stats: string. local path to the stats data. path_rules: string. local path to the composing rules data. Returns: material_probs: dictionary. Content of the stats json file. convertion_rules: dictionary. Content of the rules json file. """ with open(path_stats) as json_file: material_probs = json.load(json_file) with open(path_rules) as json_file: convertion_rules = json.load(json_file) return material_probs, convertion_rules