The main feature here is to try every combination on the alternate equipments and determine which equipment set would produce the highest average damage rate. Although it's designed for TMS (Taiwan MapleStory), many things are similar to other areas. I designed it for generic purpose, the player abilities, skills, and equipments data are all from JSON files, so if there is anything missing, you can easily modify it, or feel free to ask me.
Fill the data in player.json
, monster.json
, alternates.json
. See sample files for more details.
Run the cells in sequential order.
Export this notebook to maplestory_calculator.py
by clicking File > Download as > Python (.py)
.
pylint maplestory_calculator.py --disable=invalid-name,missing-docstring,line-too-long,trailing-whitespace,too-many-arguments
In [1]:
import codecs
import itertools
import json
import math
import pickle
import time
import unittest
import warnings
import numpy as np
from scipy.special import binom
In [2]:
ITEM_KEYWORD = {
'equipment': ['ring', 'pocket',
'pendant', 'weapon', 'belt',
'cap', 'fore_head', 'eye_acc', 'clothes', 'pants', 'shoes',
'ear_acc', 'shoulder', 'gloves', 'android',
'emblem', 'badge', 'medal', 'sub_weapon', 'cape', 'heart',
'totem',
'cash_ring',
'cash_weapon',
'cash_cap', 'cash_fore_head', 'cash_eye_acc', 'cash_clothes', 'cash_pants', 'cash_shoes',
'cash_ear_acc', 'cash_gloves',
'cash_sub_weapon', 'cash_cape',
'pet',
'additional',
'__set_effects'
],
'skill': ['blessing', 'alliance_will', 'link', 'permanent', 'buff'],
'misc': ['ammunition', 'title', 'monster_familiar', 'bits']
}
MAX_EQUIP_SIZE = {
'ring': 4,
'pendant': 2,
'totem': 3,
'cash_ring': 4,
'pet': 3
}
STAT_KEYWORDS = ['name', 'type', 'category', 'superior', 'required_level', 'set',
'upgrades_available', 'enchants_available', 'scroll_available', 'upgrades_use', 'enchants_use',
'primary_stat', 'secondary_stat', 'all_stat_pct', 'primary_stat_pct', 'secondary_stat_pct',
'attack', 'attack_pct', 'damage_pct', 'boss_damage_pct',
'ignore_defense_pct', 'ignore_resistance_pct',
'critical_rate', 'critical_damage',
'final_damage_list', 'final_damage_boost'
]
# May be inaccurate, the values of higher star levels are calculated by exponential data fitting.
ENCHANT_TABLE = {
'non_superior': {
'all': {
'stat': ([2] * 5) + ([3] * 10) + ([11] * 10)
},
'weapon': {
'attack': ([0] * 15) + [8, 9, 9, 10, 11, 12, 13, 14, 15, 16]
},
'non_weapon_150': {
'attack': ([0] * 15) + [9, 10, 11, 12, 13, 14, 16, 17, 19, 21]
}
},
'superior': {
'armor': {
'stat': [19, 20, 22, 25, 29] + ([0] * 10),
'attack': ([0] * 5) + [9, 10, 11, 12, 13, 15, 17, 19, 21, 23]
}
}
}
def deepcopy(obj):
return pickle.loads(pickle.dumps(obj, -1))
def get_sum_value_list(list1, list2):
list1 = deepcopy(list1)
list2 = deepcopy(list2)
len1 = len(list1)
len2 = len(list2)
if len1 < len2:
(len1, len2) = (len2, len1)
(list1, list2) = (list2, list1)
diff_len = len1 - len2
for _ in range(diff_len):
list2.append([])
combined = [None] * len1
for index in range(len1):
sum_value = sum(list1[index]) + sum(list2[index])
combined[index] = [sum_value]
return combined
def get_combined_stat(orig_stat, ext_stat):
combined_stat = deepcopy(orig_stat)
for stat_keyword, stat_value in ext_stat.items():
if stat_keyword not in combined_stat:
if stat_keyword != 'ignore_defense_pct':
combined_stat[stat_keyword] = 0
else:
combined_stat[stat_keyword] = []
if stat_keyword != 'ignore_defense_pct':
combined_stat[stat_keyword] += stat_value
else:
combined_stat[stat_keyword].extend(stat_value)
return combined_stat
def get_obj_to_items(obj):
if not isinstance(obj, list):
return [obj]
else:
return obj
def get_player_equip_to_array(player, excluded_keywords):
equip_array = []
player_equip = player['equipment']
for equip_keyword in ITEM_KEYWORD['equipment']:
if equip_keyword not in excluded_keywords:
obj = player_equip[equip_keyword]
equips = get_obj_to_items(obj)
equip_array.extend(equips)
return equip_array
def is_default_value(value):
return not value
def is_equal_equip(equip1, equip2):
for stat_keyword in STAT_KEYWORDS:
if stat_keyword in equip1:
equip1_value = equip1[stat_keyword]
if stat_keyword in equip2:
equip2_value = equip2[stat_keyword]
else:
equip2_value = None
if (not is_default_value(equip1_value) and not equip2_value) or (equip1_value != equip2_value):
return False
elif stat_keyword in equip2:
equip2_value = equip2[stat_keyword]
if stat_keyword in equip1:
equip1_value = equip1[stat_keyword]
else:
equip1_value = None
if (not is_default_value(equip2_value) and not equip1_value) or (equip2_value != equip1_value):
return False
return True
def get_diff_equip_group(equip_group_old, equip_group_new):
unchanged = []
addition = []
deletion = []
for equip_old in equip_group_old:
existed = False
for equip_new in equip_group_new:
if is_equal_equip(equip_old, equip_new):
existed = True
break
if existed:
unchanged.append(equip_old)
else:
deletion.append(equip_old)
for equip_new in equip_group_new:
existed = False
for equip_old in equip_group_old:
if is_equal_equip(equip_old, equip_new):
existed = True
break
if not existed:
addition.append(equip_new)
return {
'unchanged': unchanged,
'addition': addition,
'deletion': deletion
}
def read_json_file(filename):
with codecs.open(filename, mode='r', encoding='utf-8') as data_file:
data = json.load(data_file)
return data
The formula here calculates the skill output and is mainly composed by:
In addition to the damage components written in StreategyWiki, by analyzing WZ file shown in this topic, there are also 4 extra damage components used, so I improvised the new formula:
$$ [ AD \times Skill\% \times (1 + TD\% + BD\%) \times (1 + CD\%) ] \times \\ (1 - [MD\% \times (1 - ID\%)]) \times \\ (1 - [MR \times (1 - IR\%)]) \times \\ [(1 + FD\%_1) \times (1 + FD\%_2) \times (1 + FD\%_3) \times \cdots \times (1 + FD\%_n)] + \\ FDB $$These functions are not aware of the file format.
For other legend see StrategyWiki > Output > Legend.
In [3]:
def get_stat_value(primary_stat, secondary_stat):
return primary_stat * 4 + secondary_stat
def get_one_stat(base, mw, bonus, pct, all_pct, ability, card, hyper):
base_stat = math.floor(base * (1.0 + (mw / 100.0)) + bonus)
pct_stat = 1.0 + ((pct + all_pct) / 100.0)
other_stat = ability + card + hyper
return math.floor(base_stat * pct_stat + other_stat)
def get_attack(bonus, pct):
base_attack = bonus
pct_attack = 1.0 + (pct / 100.0)
return math.floor(base_attack * pct_attack)
def get_max_shown_damage(weapon_mul, stat, attack, total_damage_pct, final_damage_boost):
base_damage = math.floor(weapon_mul * stat * attack)
pct_damage = 1.0 + (total_damage_pct / 100.0)
return math.floor(base_damage * pct_damage / 100.0 + final_damage_boost)
def get_min_shown_damage(weapon_mul, stat, attack, total_damage_pct, mastery, final_damage_boost):
base_damage = math.floor(weapon_mul * stat * attack)
pct_damage = 1.0 + (total_damage_pct / 100.0)
return math.floor(base_damage * pct_damage / 100.0 * (mastery / 100.0) + final_damage_boost)
def get_max_actual_damage(weapon_mul, stat, attack):
return math.floor(weapon_mul * stat * (attack / 100.0))
def get_min_actual_damage(weapon_mul, stat, attack, mastery):
return math.floor(weapon_mul * stat * (attack / 100.0) * (mastery / 100.0))
def get_actual_skill_damage(actual_damage, skill_damage_pct, total_damage_pct, boss_damage_pct, crit_damage_pct,
monster_def_pct, ignore_def_pct, monster_resist, ignore_resist_pct,
final_damage_list, final_damage_boost, is_boss=True):
if not is_boss:
boss_damage_pct = 0
base_damage = math.floor(actual_damage * (skill_damage_pct / 100.0) *
(1.0 + ((total_damage_pct + boss_damage_pct) / 100.0)) *
(1.0 + (crit_damage_pct / 100.0)))
defense_mul = (1.0 - ((monster_def_pct / 100.0) * (1.0 - (ignore_def_pct / 100.0))))
resis_mul = (1.0 + (monster_resist * (1.0 - (ignore_resist_pct / 100.0))))
final_damage_prod = [(1.0 + (damage / 100.0)) for damage in final_damage_list]
final_damage = np.prod(final_damage_prod)
return math.floor(base_damage * defense_mul * resis_mul * final_damage + final_damage_boost)
def get_damage_per_second(damage, attack_speed, attack_count):
return damage * attack_speed * attack_count
def get_time_to_kill(hp, damage_per_second):
return hp / damage_per_second
In [4]:
def get_property_sum(player, stat_keyword):
total = 0
categories = ['equipment', 'skill', 'misc']
for category_name in categories:
player_category = player[category_name]
for item_name in ITEM_KEYWORD[category_name]:
if not item_name in player_category:
continue
obj = player_category[item_name]
items = get_obj_to_items(obj)
for item in items:
if stat_keyword in item:
total += item[stat_keyword]
return total
def get_property_inv_mul(player, stat_keyword):
product = 1.0
categories = ['equipment', 'skill', 'misc']
for category_name in categories:
player_category = player[category_name]
for item_name in ITEM_KEYWORD[category_name]:
if not item_name in player_category:
continue
obj = player_category[item_name]
items = get_obj_to_items(obj)
for item in items:
if stat_keyword in item:
values = get_obj_to_items(item[stat_keyword])
for value in values:
product *= (1.0 - (value / 100.0))
return product
def get_property_list_sum(player, stat_keyword):
list_sum = []
categories = ['equipment', 'skill', 'misc']
for category_name in categories:
player_category = player[category_name]
for item_name in ITEM_KEYWORD[category_name]:
if not item_name in player_category:
continue
obj = player_category[item_name]
items = get_obj_to_items(obj)
for item in items:
if stat_keyword in item:
list_sum = get_sum_value_list(list_sum, item[stat_keyword])
list_sum = [value[0] for value in list_sum]
return list_sum
def get_equip_set_stat(equips, set_effect):
set_stat = {}
record = {}
for equip in equips:
if 'set' in equip:
set_name = equip['set']
if set_name:
if set_name in record:
record[set_name] += 1
else:
record[set_name] = 1
for set_name, count in record.items():
effects = set_effect[set_name]
for set_index in range(1, count+1):
set_index_str = str(set_index)
if set_index_str in effects:
effect = effects[set_index_str]
set_stat = get_combined_stat(set_stat, effect)
return set_stat
def get_bonus_one_stat(player, stat_type):
if stat_type == 'primary':
stat_keyword = 'primary_stat'
elif stat_type == 'secondary':
stat_keyword = 'secondary_stat'
else:
raise ValueError('Unexpected stat type')
return get_property_sum(player, stat_keyword)
def get_all_stat_pct(player):
return get_property_sum(player, 'all_stat_pct')
def get_one_stat_pct(player, stat_type):
if stat_type == 'primary':
stat_keyword = 'primary_stat_pct'
elif stat_type == 'secondary':
stat_keyword = 'secondary_stat_pct'
else:
raise ValueError('Unexpected stat type')
return get_property_sum(player, stat_keyword)
def get_total_damage_pct(player):
return (player['ability']['inner_ability']['damage_pct'] +
player['ability']['character_card']['damage_pct'] +
player['ability']['hyper_stat']['damage_pct'] +
get_property_sum(player, 'damage_pct'))
def get_boss_damage_pct(player):
return (player['ability']['inner_ability']['boss_damage_pct'] +
player['ability']['character_card']['boss_damage_pct'] +
player['ability']['hyper_stat']['boss_damage_pct'] +
get_property_sum(player, 'boss_damage_pct'))
def get_critical_damage(player):
critical_damage = (player['ability']['base']['critical_damage'] +
player['ability']['hyper_stat']['critical_damage'] +
get_property_sum(player, 'critical_damage'))
return critical_damage
def get_ignore_defense_pct(player):
inv_mul = ((1.0 - (player['ability']['inner_ability']['ignore_defense_pct'] / 100.0)) *
(1.0 - (player['ability']['character_card']['ignore_defense_pct'] / 100.0)) *
(1.0 - (player['ability']['hyper_stat']['ignore_defense_pct'] / 100.0)) *
(1.0 - (player['ability']['trait']['ignore_defense_pct'] / 100.0)) *
get_property_inv_mul(player, 'ignore_defense_pct'))
return 100.0 - (100.0 * inv_mul)
def get_ignore_resistance_pct(player):
return math.floor(player['ability']['trait']['ignore_resistance_pct'] +
get_property_sum(player, 'ignore_resistance_pct'))
def get_final_damage_list(player):
return get_property_list_sum(player, 'final_damage_list')
def get_final_damage_boost(player):
return (player['ability']['inner_ability']['damage_conversion'] +
player['ability']['character_card']['bonus_damage'] +
get_property_sum(player, 'final_damage_boost'))
def inject_equip_set(player):
excluded_keywords = ['__set_effects']
equips = get_player_equip_to_array(player, excluded_keywords)
set_effect = player['set']
equip_set_stat = get_equip_set_stat(equips, set_effect)
player['equipment']['__set_effects'] = equip_set_stat
def calc_one_stat(player, stat_type):
if stat_type == 'primary':
stat_keyword = 'primary_stat'
elif stat_type == 'secondary':
stat_keyword = 'secondary_stat'
else:
raise ValueError('Unexpected stat type')
base = player['ability']['base'][stat_keyword]
mw = player['skill']['maple_warrior']
bonus = get_bonus_one_stat(player, stat_type)
pct = get_one_stat_pct(player, stat_type)
all_pct = get_all_stat_pct(player)
ability = player['ability']['inner_ability'][stat_keyword]
card = player['ability']['character_card'][stat_keyword]
hyper = player['ability']['hyper_stat'][stat_keyword]
return get_one_stat(base, mw, bonus, pct, all_pct, ability, card, hyper)
def calc_attack(player):
bonus = (get_property_sum(player, 'attack') +
player['ability']['inner_ability']['attack'])
pct = get_property_sum(player, 'attack_pct')
return get_attack(bonus, pct)
def calc_max_shown_damage(player, stat_value, attack):
weapon_mul = player['ability']['weapon']['multiplier']
total_damage_pct = get_total_damage_pct(player)
final_damage_boost = get_final_damage_boost(player)
return get_max_shown_damage(weapon_mul, stat_value, attack, total_damage_pct, final_damage_boost)
def calc_min_shown_damage(player, stat_value, attack):
weapon_mul = player['ability']['weapon']['multiplier']
total_damage_pct = get_total_damage_pct(player)
mastery = player['skill']['mastery']
final_damage_boost = get_final_damage_boost(player)
return get_min_shown_damage(weapon_mul, stat_value, attack, total_damage_pct, mastery, final_damage_boost)
def calc_max_actual_damage(player, stat_value, attack):
weapon_mul = player['ability']['weapon']['multiplier']
return get_max_actual_damage(weapon_mul, stat_value, attack)
def calc_min_actual_damage(player, stat_value, attack):
weapon_mul = player['ability']['weapon']['multiplier']
mastery = player['skill']['mastery']
return get_min_actual_damage(weapon_mul, stat_value, attack, mastery)
def calc_actual_skill_damage(player, monster, actual_damage, critical_damage_pct):
skill_damage_pct = player['skill']['active_damage_pct']
total_damage_pct = get_total_damage_pct(player)
boss_damage_pct = get_boss_damage_pct(player)
monster_def_pct = monster['ability']['defense']
ignore_def_pct = get_ignore_defense_pct(player)
monster_resist = monster['ability']['resistance']
ignore_resist_pct = get_ignore_resistance_pct(player)
final_damage_list = get_final_damage_list(player)
final_damage_boost = get_final_damage_boost(player)
is_boss = (monster['type'] == 'boss')
return get_actual_skill_damage(actual_damage, skill_damage_pct, total_damage_pct, boss_damage_pct,
critical_damage_pct,
monster_def_pct, ignore_def_pct, monster_resist, ignore_resist_pct,
final_damage_list, final_damage_boost, is_boss)
def calc_critical_rate(player):
critical_rate = (player['ability']['base']['critical_rate'] +
player['ability']['hyper_stat']['critical_rate'] +
get_property_sum(player, 'critical_rate'))
return min(100, critical_rate)
def calc_damage_per_second(player, damage):
attack_speed = player['ability']['weapon']['attack_speed']
attack_count = player['skill']['active_attack_count']
return get_damage_per_second(damage, attack_speed, attack_count)
def calc_output(player, monster, debug=False, copy=True):
if copy:
player_copy = deepcopy(player)
else:
player_copy = player
inject_equip_set(player_copy)
primary_stat = calc_one_stat(player_copy, 'primary')
secondary_stat = calc_one_stat(player_copy, 'secondary')
attack = calc_attack(player_copy)
stat_value = get_stat_value(primary_stat, secondary_stat)
max_actual_damage = calc_max_actual_damage(player_copy, stat_value, attack)
min_actual_damage = calc_min_actual_damage(player_copy, stat_value, attack)
avg_actual_damage = (max_actual_damage + min_actual_damage) / 2.0
critical_rate = calc_critical_rate(player_copy)
critical_damage = get_critical_damage(player_copy)
actual_skill_damage_avg_crit = calc_actual_skill_damage(player_copy, monster, avg_actual_damage, critical_damage)
actual_skill_damage_avg_no_crit = calc_actual_skill_damage(player_copy, monster, avg_actual_damage, 0)
avg_skill_damage = ((critical_rate / 100.0) * actual_skill_damage_avg_crit +
(1.0 - (critical_rate / 100.0)) * actual_skill_damage_avg_no_crit)
avg_damage_per_second = calc_damage_per_second(player_copy, avg_skill_damage)
if debug:
total_damage_pct = get_total_damage_pct(player_copy)
boss_damage_pct = get_boss_damage_pct(player_copy)
ignore_defense_pct = get_ignore_defense_pct(player_copy)
ignore_resistance_pct = get_ignore_resistance_pct(player_copy)
final_damage_list = get_final_damage_list(player_copy)
final_damage_boost = get_final_damage_boost(player_copy)
max_shown_damage = calc_max_shown_damage(player_copy, stat_value, attack)
min_shown_damage = calc_min_shown_damage(player_copy, stat_value, attack)
actual_skill_damage_max_crit = calc_actual_skill_damage(player_copy, monster, max_actual_damage,
critical_damage)
actual_skill_damage_min_crit = calc_actual_skill_damage(player_copy, monster, min_actual_damage,
critical_damage)
actual_skill_damage_max_no_crit = calc_actual_skill_damage(player_copy, monster, max_actual_damage, 0)
actual_skill_damage_min_no_crit = calc_actual_skill_damage(player_copy, monster, min_actual_damage, 0)
print('primary_stat={}, secondary_stat={}'.format(primary_stat, secondary_stat))
print('attack={}'.format(attack))
print('stat_value={}'.format(stat_value))
print('total_damage_pct={}'.format(total_damage_pct))
print('boss_damage_pct={}'.format(boss_damage_pct))
print('ignore_defense_pct={}'.format(ignore_defense_pct))
print('ignore_resistance_pct={}'.format(ignore_resistance_pct))
print('final_damage_list={}'.format(final_damage_list))
print('final_damage_boost={}'.format(final_damage_boost))
print('critical_rate={}'.format(critical_rate))
print('critical_damage={}'.format(critical_damage))
print('max_shown_damage={}, min_shown_damage={}'.format(max_shown_damage, min_shown_damage))
print('max_actual_damage={}, min_actual_damage={}'.format(max_actual_damage, min_actual_damage))
print('actual_skill_damage_max_crit={}'.format(actual_skill_damage_max_crit))
print('actual_skill_damage_min_crit={}'.format(actual_skill_damage_min_crit))
print('actual_skill_damage_max_no_crit={}'.format(actual_skill_damage_max_no_crit))
print('actual_skill_damage_min_no_crit={}'.format(actual_skill_damage_min_no_crit))
print('actual_skill_damage_avg_crit={}'.format(actual_skill_damage_avg_crit))
print('actual_skill_damage_avg_no_crit={}'.format(actual_skill_damage_avg_no_crit))
print('avg_skill_damage={}'.format(avg_skill_damage))
print('avg_damage_per_second={}'.format(avg_damage_per_second))
return avg_damage_per_second
def calc_time_to_kill(monster, damage_per_second, efficiency=1.0):
hp = monster['ability']['hp']
return get_time_to_kill(hp, damage_per_second * efficiency)
In [5]:
def get_upgraded_equip(equip, scroll):
if 'upgrades_available' not in equip or not equip['scroll_available']:
return equip
upgrades_use = equip['upgrades_use']
upgrades_available = equip['upgrades_available']
if upgrades_use > upgrades_available:
raise ValueError('Exceeded range of upgrades')
scroll_available = equip['scroll_available']
scroll_use = scroll[scroll_available]
for _ in range(upgrades_use):
equip = get_combined_stat(equip, scroll_use)
equip['upgrades_available'] -= 1
return equip
def get_enchanted_result(equip, enchant_level):
if equip['category'] not in ['weapon', 'armor', 'accessory']:
raise ValueError('Unrecognized equipment type')
enchant_result = {
'primary_stat': 0,
'secondary_stat': 0,
'attack': 0
}
equip_category = equip['category']
is_superior = equip['superior']
req_level = equip['required_level']
attack = equip['attack']
for level in range(1, enchant_level+1):
if not is_superior:
if level >= 1 and level <= 15:
enchant_result['primary_stat'] += ENCHANT_TABLE['non_superior']['all']['stat'][(level - 1)]
enchant_result['secondary_stat'] += ENCHANT_TABLE['non_superior']['all']['stat'][(level - 1)]
if equip_category == 'weapon':
enchant_result['attack'] += (math.floor(attack / 50.0) + 1)
elif level >= 16 and level <= 25:
enchant_result['primary_stat'] += ENCHANT_TABLE['non_superior']['all']['stat'][(level - 1)]
enchant_result['secondary_stat'] += ENCHANT_TABLE['non_superior']['all']['stat'][(level - 1)]
if equip_category == 'armor' or equip_category == 'accessory':
increase_attack = ENCHANT_TABLE['non_superior']['non_weapon_150']['attack'][(level - 1)]
adjust_by_level = (math.floor(req_level / 10) - 15)
enchant_result['attack'] += (increase_attack + adjust_by_level)
elif equip_category == 'weapon':
enchant_result['attack'] += ENCHANT_TABLE['non_superior']['weapon']['attack'][(level - 1)]
else:
raise ValueError('Enchant level out of range')
else:
if level >= 1 and level <= 15:
enchant_result['primary_stat'] += ENCHANT_TABLE['superior']['armor']['stat'][(level - 1)]
enchant_result['secondary_stat'] += ENCHANT_TABLE['superior']['armor']['stat'][(level - 1)]
enchant_result['attack'] += ENCHANT_TABLE['superior']['armor']['attack'][(level - 1)]
else:
raise ValueError('Enchant level out of range')
return enchant_result
def get_enchanted_equip(equip):
if 'enchants_available' not in equip:
return equip
enchant_level = equip['enchants_use']
if enchant_level > equip['enchants_available']:
raise ValueError('Exceeded range of enchant')
enchant_result = get_enchanted_result(equip, enchant_level)
return get_combined_stat(equip, enchant_result)
In [6]:
def prepare_alternates_calc(player_copy, alternates_copy, remove_orig_equips):
orig_player_equip = player_copy['equipment']
if remove_orig_equips:
for equip_keyword in ITEM_KEYWORD['equipment']:
orig_player_equip[equip_keyword] = []
player_copy['set'] = {}
else:
equip_alternates = alternates_copy['equipment_alternates']
excluded_keywords = ['__set_effects']
for equip_keyword in ITEM_KEYWORD['equipment']:
if not equip_keyword in excluded_keywords:
if equip_keyword not in equip_alternates:
equip_alternates[equip_keyword] = []
orig_equip_group = get_obj_to_items(orig_player_equip[equip_keyword])
equip_alternates[equip_keyword].extend(orig_equip_group)
player_copy['set'].update(alternates_copy['set'])
def get_equip_select_size(player, equip_category):
if equip_category in MAX_EQUIP_SIZE:
player_equip_size = player['equipment_size']
if equip_category in player_equip_size:
max_player_equip_size = player_equip_size[equip_category]
select_size = min(MAX_EQUIP_SIZE[equip_category], max_player_equip_size)
else:
select_size = MAX_EQUIP_SIZE[equip_category]
else:
select_size = 1
return select_size
def get_size_of_all_equip_set(player, alternates, remove_orig_equips):
size = 1
player_copy = deepcopy(player)
alternates_copy = deepcopy(alternates)
prepare_alternates_calc(player_copy, alternates_copy, remove_orig_equips)
equip_alternates = alternates_copy['equipment_alternates']
for equip_category, available_equips in equip_alternates.items():
available_equips_size = len(available_equips)
if available_equips_size == 0:
continue
select_size = get_equip_select_size(player, equip_category)
if available_equips_size < select_size:
available_equips_size = select_size
combination_size = int(binom(available_equips_size, select_size))
size *= combination_size
return size
def get_all_equip_set(player, alternates, copy=False):
equip_list_of_groups = []
equip_alternates = alternates['equipment_alternates']
for equip_category, available_equips in equip_alternates.items():
available_equips_size = len(available_equips)
if available_equips_size == 0:
continue
select_size = get_equip_select_size(player, equip_category)
if copy:
available_equips_copy = deepcopy(available_equips)
else:
available_equips_copy = available_equips
empty_size = max(0, select_size - available_equips_size)
empty_equip = {
'__equip_category': equip_category
}
for _ in range(empty_size):
available_equips_copy.append(empty_equip)
combinations = itertools.combinations(available_equips_copy, select_size)
equip_groups = [list(combination) for combination in combinations]
equip_list_of_groups.append(equip_groups)
return itertools.product(*equip_list_of_groups)
def check_valid_equip_set(equip_set):
record_clothes_pants = []
for equip_group in equip_set:
for equip in equip_group:
equip_category = equip['__equip_category']
if equip_category == 'clothes':
if 'type' in equip:
equip_type = equip['type']
record_clothes_pants.append(equip_type)
elif equip_category == 'pants':
if 'name' in equip and equip['name']:
record_clothes_pants.append('pants')
if 'overall' in record_clothes_pants and 'pants' in record_clothes_pants:
return False
return True
def get_enhanced_equip(equip, scroll):
equip = get_upgraded_equip(equip, scroll)
equip = get_enchanted_equip(equip)
return equip
def inject_equip_category(alternates):
equip_alternates = alternates['equipment_alternates']
for equip_category, available_equips in equip_alternates.items():
for available_equip in available_equips:
items = get_obj_to_items(available_equip)
for item in items:
item['__equip_category'] = equip_category
def enhance_equip_set(player_equip, equip_set, scroll):
for equip_group in equip_set:
if not equip_group:
continue
enhanced_equip_group = []
equip_category = equip_group[0]['__equip_category']
for equip in equip_group:
enhanced_equip = get_enhanced_equip(equip, scroll)
enhanced_equip_group.append(enhanced_equip)
player_equip[equip_category] = enhanced_equip_group
def calc_best_equip_set(player, monster, alternates, remove_orig_equips):
best_player = None
best_equip_set = None
best_avg_damage_per_second = 0
player_orig_copy = deepcopy(player)
alternates_copy = deepcopy(alternates)
prepare_alternates_calc(player_orig_copy, alternates_copy, remove_orig_equips)
inject_equip_category(alternates_copy)
all_equip_set = get_all_equip_set(player_orig_copy, alternates_copy)
scroll = alternates_copy['scroll']
for equip_set in all_equip_set:
if not check_valid_equip_set(equip_set):
continue
player_copy = deepcopy(player_orig_copy)
player_equip = player_copy['equipment']
enhance_equip_set(player_equip, equip_set, scroll)
avg_damage_per_second = calc_output(player_copy, monster, copy=False)
if avg_damage_per_second > best_avg_damage_per_second or not best_equip_set:
best_player = player_copy
best_equip_set = equip_set
best_avg_damage_per_second = avg_damage_per_second
return (best_player, best_equip_set, best_avg_damage_per_second)
In [7]:
class OldPlayerTestCase(unittest.TestCase):
def test_player_damage(self):
my_player = read_json_file('player_old.json')
the_monster = read_json_file('monster_chaos_vellum.json')
avg_damage_per_second = calc_output(my_player, the_monster)
efficiency = 1.0
time_to_kill = calc_time_to_kill(the_monster, avg_damage_per_second, efficiency)
self.assertAlmostEqual(avg_damage_per_second, 389241624.38, places=2)
self.assertAlmostEqual(time_to_kill, 308.29, places=2)
def test_best_equip_set(self):
my_player = read_json_file('player_old.json')
the_monster = read_json_file('monster_chaos_vellum.json')
alternates = read_json_file('alternates_small.json')
remove_orig_equips = True
(best_player, best_equip_set, best_avg_damage_per_second) = calc_best_equip_set(my_player, the_monster, alternates,
remove_orig_equips)
new_avg_damage_per_second = calc_output(best_player, the_monster)
self.assertAlmostEqual(best_avg_damage_per_second, new_avg_damage_per_second, places=2)
self.assertAlmostEqual(best_avg_damage_per_second, 40616480.24, places=2)
for equip_group in best_equip_set:
for equip in equip_group:
equip_category = equip['__equip_category']
self.assertIn(equip_category, ITEM_KEYWORD['equipment'])
suite = unittest.TestLoader().loadTestsFromTestCase(OldPlayerTestCase)
unittest.TextTestRunner().run(suite)
Out[7]:
In [8]:
def calc_my_player():
'''Calculate player's average damage rate against the target monster.'''
my_player = read_json_file('player.json')
the_monster = read_json_file('monster.json')
avg_damage_per_second = calc_output(my_player, the_monster, debug=True)
efficiency = 1.0
time_to_kill = calc_time_to_kill(the_monster, avg_damage_per_second, efficiency)
print()
print('name = {}, job = {}, level = {}'.format(my_player['info']['name'], my_player['info']['job'],
my_player['info']['level']))
print('average damage per second = {:.2f} (damage/second)'.format(avg_damage_per_second))
print('time to kill = {:.2f} (seconds)'.format(time_to_kill))
calc_my_player()
In [9]:
def get_equip_set_to_dict(equip_set):
if not equip_set:
return {}
equip = {}
excluded_keywords = ['__set_effects']
for equip_keyword in ITEM_KEYWORD['equipment']:
if equip_keyword in excluded_keywords:
continue
for equip_group in equip_set:
equip_category = equip_group[0]['__equip_category']
if equip_category == equip_keyword:
equip[equip_keyword] = equip_group
break
return equip
def get_removed_orig_equips_player(player, remove_orig_equips):
if remove_orig_equips:
player_copy = deepcopy(player)
for equip_keyword in ITEM_KEYWORD['equipment']:
player_copy['equipment'][equip_keyword] = []
else:
player_copy = player
return player_copy
def print_equip_set_diff(player, best_equip_dict):
player_equip = player['equipment']
excluded_keywords = ['__set_effects']
for equip_keyword in ITEM_KEYWORD['equipment']:
if equip_keyword in excluded_keywords:
continue
if equip_keyword in best_equip_dict:
best_equip_group = best_equip_dict[equip_keyword]
orig_equip_group = get_obj_to_items(player_equip[equip_keyword])
diff = get_diff_equip_group(orig_equip_group, best_equip_group)
for deleted_equip in diff['deletion']:
equip_name = deleted_equip['name'] if 'name' in deleted_equip else None
print('- {} = {}'.format(equip_keyword, equip_name))
for added_equip in diff['addition']:
equip_name = added_equip['name'] if 'name' in added_equip else None
print('+ {} = {}'.format(equip_keyword, equip_name))
for unchanged_equip in diff['unchanged']:
equip_name = unchanged_equip['name'] if 'name' in unchanged_equip else None
print(' {} = {}'.format(equip_keyword, equip_name))
def find_my_best_equip_set():
'''Find the best alternate equipment set with highest average damage rate.'''
start_time = time.perf_counter()
# Calculate player's average damage with the original equipment set
my_player = read_json_file('player.json')
the_monster = read_json_file('monster.json')
orig_avg_damage_per_second = calc_output(my_player, the_monster)
# Find the best alternate equipment set
alternates = read_json_file('alternates.json')
remove_orig_equips = True
warn_size_of_all_equip_set = 10 ** 4
size_of_all_equip_set = get_size_of_all_equip_set(my_player, alternates, remove_orig_equips)
if size_of_all_equip_set > warn_size_of_all_equip_set:
warnings.warn('Run time might be very long: {} sets of equipments'.format(size_of_all_equip_set))
(best_player, best_equip_set, best_avg_damage_per_second) = calc_best_equip_set(my_player, the_monster, alternates,
remove_orig_equips)
print('description = {}'.format(alternates['info']['description']))
print('best average damage per second = {:.2f} (damage/second)'.format(best_avg_damage_per_second))
# Calculate the increase ratio with the best alternate equipment set
increase_pct = ((best_avg_damage_per_second / orig_avg_damage_per_second) - 1.0) * 100.0
print('increase={:.2f}%'.format(increase_pct))
# Print equipment set difference
my_player = get_removed_orig_equips_player(my_player, remove_orig_equips)
best_equip = get_equip_set_to_dict(best_equip_set)
print()
print_equip_set_diff(my_player, best_equip)
# Print player with the new equipment set debug info
print()
calc_output(best_player, the_monster, debug=True)
# Print elapsed time
elapsed_time = time.perf_counter() - start_time
print()
print('run time={:.2f} (seconds)'.format(elapsed_time))
find_my_best_equip_set()
In [ ]: