From 591ac4bdeb1584e8906776dea22b10294a11f1ff Mon Sep 17 00:00:00 2001 From: Bryce Allen Date: Fri, 20 Mar 2015 19:15:26 -0500 Subject: [PATCH] refactor expected value calculations Output should be the same as before refactor, except for max cap reward for quests. The fix for using carving god on tail carves in the cap calculation was only applied to the print_monsters_and_rewards copy of the code. It should be less than the previous value by the differece between min and max for tail carve. --- mhrewards.py | 519 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 315 insertions(+), 204 deletions(-) diff --git a/mhrewards.py b/mhrewards.py index 07bbbaa..9951eb1 100755 --- a/mhrewards.py +++ b/mhrewards.py @@ -6,6 +6,14 @@ import codecs import mhdb import mhprob +SKILL_CARVING = "carving" +SKILL_CAP = "cap" +SKILL_NONE = None + +STRAT_KILL = "kill" +STRAT_CAP = "cap" +STRAT_SHINY = "shiny" + def get_utf8_writer(writer): return codecs.getwriter("utf8")(writer) @@ -36,6 +44,263 @@ def find_item(db, item_name, err_out): return item_row +class QuestReward(object): + def __init__(self, reward, fixed_rewards): + self.slot = reward["reward_slot"] + self.stack_size = reward["stack_size"] + self.percentage = reward["percentage"] + self.item_id = reward["item_id"] + + self.fixed_rewards = fixed_rewards[self.slot] + + self.skill_delta = 0 + self.evs = self._calculate_ev() + + def expected_value(self, luck_skill=mhprob.LUCK_SKILL_NONE, + cap_skill=None, carving_skill=None): + return self.evs[luck_skill] + + def expected_values(self): + return self.evs + + def _calculate_ev(self): + if self.percentage == 100: + # fixed reward, always one draw regardless of luck skill + evs = [1 * self.percentage * self.stack_size] * 3 + self.skill_delta = 0 + else: + # variable reward, expected number of draws depends on luck skill + counts = [mhprob.quest_reward_expected_c(self.slot, skill) + for skill in xrange(mhprob.LUCK_SKILL_NONE, + mhprob.LUCK_SKILL_GREAT+1)] + + + evs = [((count - self.fixed_rewards) + * self.stack_size * self.percentage) + for count in counts] + self.skill_delta = evs[-1] - evs[0] + return evs + + def print(self, out, indent=2): + out.write("%s%20s %d %5.2f / 100" + % (" " * indent, self.slot, self.stack_size, + self.evs[0])) + out.write(" (%2d each)" % self.percentage) + if self.skill_delta: + out.write(" %s" % " ".join("%0.2f" % i for i in self.evs[1:])) + out.write("\n") + + +class QuestItemExpectedValue(object): + """ + Calculate the expected value for an item across all rewards for a quest. + + @param item_id: database id of item + @param quest_rewards: list of rows from quest_rewards table for a single + quest + """ + def __init__(self, item_id, quest_rewards): + self.item_id = item_id + + self.fixed_rewards = dict(A=0, B=0, Sub=0) + self.total_reward_p = dict(A=0, B=0, Sub=0) + + # dict mapping slot name to list of lists + # of the form (slot, list_of_expected_values). + self.slot_rewards = dict(A=[], B=[], Sub=[]) + self.total_expected_values = [0, 0, 0] + + self._set_rewards(quest_rewards) + + def is_sub(self): + """Item is available from sub quest""" + return len(self.slot_rewards["Sub"]) > 0 + + def is_main(self): + """Item is available from main quest""" + return (len(self.slot_rewards["A"]) > 0 + or len(self.slot_rewards["B"]) > 0) + + def expected_value(self, luck_skill=mhprob.LUCK_SKILL_NONE, + cap_skill=None, carving_skill=None): + return self.total_expected_values[luck_skill] + + def check_totals(self, outfile): + # sanity check values from the db + for slot in self.total_reward_p.keys(): + if self.total_reward_p[slot] not in (0, 100): + print("WARNING: bad total p for %s = %d" + % (slot, self.total_reward_p[slot]), file=outfile) + + def _set_rewards(self, rewards): + # preprocessing step - figure out how many fixed rewards there + # are, which we need to know in order to figure out how many + # chances there are to get other rewards. + for reward in rewards: + slot = reward["reward_slot"] + if reward["percentage"] == 100: + self.fixed_rewards[slot] += 1 + else: + self.total_reward_p[slot] += reward["percentage"] + + for reward in rewards: + if reward["item_id"] != self.item_id: + continue + self._add_reward(reward) + + def _add_reward(self, r): + reward = QuestReward(r, self.fixed_rewards) + + self.slot_rewards[reward.slot].append(reward) + evs = reward.expected_values() + for i in xrange(len(evs)): + self.total_expected_values[i] += evs[i] + + def print(self, out, indent=2): + for slot in ("A", "B", "Sub"): + for qr in self.slot_rewards[slot]: + qr.print(out, indent) + + +class HuntReward(object): + def __init__(self, reward): + self.condition = reward["condition"] + self.stack_size = reward["stack_size"] + self.percentage = reward["percentage"] + self.item_id = reward["item_id"] + + self.cap = False + self.kill = False + self.shiny = False + self.skill = SKILL_NONE + + self.evs = self._calculate_evs() + + def expected_value(self, strategy, luck_skill=None, + cap_skill=mhprob.CAP_SKILL_NONE, + carving_skill=mhprob.CARVING_SKILL_NONE): + if strategy == STRAT_CAP: + if not self.cap: + return 0 + elif strategy == STRAT_KILL: + if not self.kill: + return 0 + elif strategy == STRAT_SHINY: + if not self.shiny: + return 0 + else: + raise ValueError("strategy must be STRAT_CAP or STRAT_KILL") + + if self.skill == SKILL_CAP: + return self.evs[cap_skill] + elif self.skill == SKILL_CARVING: + return self.evs[carving_skill] + else: + return self.evs[0] + + def print(self, out, indent=2): + out.write("%s%20s %d %5.2f / 100" + % (" " * indent, self.condition, + self.stack_size, self.evs[0])) + out.write(" (%2d each)" % self.percentage) + if len(self.evs) > 1: + out.write(" " + " ".join("%0.2f" % i for i in self.evs[1:])) + out.write("\n") + + def _calculate_evs(self): + if self.condition == "Body Carve": + self.skill = SKILL_CARVING + self.cap = False + self.kill = True + counts = [ + 3 + mhprob.carve_delta_expected_c(skill) + for skill in xrange(mhprob.CARVING_SKILL_PRO, + mhprob.CARVING_SKILL_GOD+1) + ] + elif self.condition == "Body Carve (Apparent Death)": + # assume one carve, it's dangerous to try for two + counts = [1] + self.cap = True + self.kill = True + elif self.condition == "Tail Carve": + self.skill = SKILL_CARVING + self.cap = True + self.kill = True + counts = [ + 1 + mhprob.carve_delta_expected_c(skill) + for skill in xrange(mhprob.CARVING_SKILL_PRO, + mhprob.CARVING_SKILL_GOD+1) + ] + elif self.condition == "Capture": + self.skill = SKILL_CAP + self.cap = True + self.kill = False + counts = [ + mhprob.capture_reward_expected_c(skill) + for skill in xrange(mhprob.CAP_SKILL_NONE, + mhprob.CAP_SKILL_GOD+1) + ] + else: + counts = [1] + if self.condition.startswith("Shiny"): + # don't include shiny in total, makes it easier to + # calculate separately since shinys are variable by + # monster + self.cap = False + self.kill = False + self.shiny = True + elif self.condition.startswith("Break"): + self.cap = True + self.kill = True + else: + raise ValueError("Unknown condition: '%s'" + % self.condition) + + evs = [(i * self.stack_size * self.percentage) for i in counts] + return evs + + +class HuntItemExpectedValue(object): + """ + Calculate the expected value for an item from hunting a monster, including + all ways of getting the item. + + @param item_id: database id of item + @param hunt_rewards: list of rows from hunt_rewards table for a single + monster and rank + """ + + def __init__(self, item_id, hunt_rewards): + self.item_id = item_id + self.matching_rewards = [] + self._set_rewards(hunt_rewards) + + def expected_value(self, strategy, luck_skill=None, + cap_skill=mhprob.CAP_SKILL_NONE, + carving_skill=mhprob.CARVING_SKILL_NONE): + ev = 0 + for reward in self.matching_rewards: + ev += reward.expected_value(strategy, + luck_skill=luck_skill, + cap_skill=cap_skill, + carving_skill=carving_skill) + return ev + + def print(self, out, indent=2): + for hr in self.matching_rewards: + hr.print(out, indent) + + def _set_rewards(self, rewards): + for reward in rewards: + if reward["item_id"] != self.item_id: + continue + self._add_reward(reward) + + def _add_reward(self, r): + reward = HuntReward(r) + self.matching_rewards.append(reward) + + def print_monsters_and_rewards(db, item_row, out): item_id = item_row["_id"] monsters = db.get_item_monsters(item_id) @@ -43,94 +308,29 @@ def print_monsters_and_rewards(db, item_row, out): mid = m["monster_id"] rank = m["rank"] monster = db.get_monster(mid) - cap_ev = [0, 0] - kill_ev = [0, 0] - shiny_ev = 0 - has_item = False - rewards = db.get_monster_rewards(mid, rank) - for reward in rewards: - cap = kill = shiny = False - if reward["item_id"] != item_id: - continue - if not has_item: - has_item = True - out.write("%s %s\n" % (monster["name"], rank)) - if reward["condition"] == "Body Carve": - totals = [ - 3 + mhprob.carve_delta_expected_c(skill) - for skill in xrange(mhprob.CARVING_SKILL_PRO, - mhprob.CARVING_SKILL_GOD+1) - ] - cap = False - kill = True - elif reward["condition"] == "Body Carve (Apparent Death)": - # assume one carve, is dangerous to try for two - totals = [1] - cap = True - kill = True - elif reward["condition"] == "Tail Carve": - totals = [ - 1 + mhprob.carve_delta_expected_c(skill) - for skill in xrange(mhprob.CARVING_SKILL_PRO, - mhprob.CARVING_SKILL_GOD+1) - ] - cap = kill = True - elif reward["condition"] == "Capture": - totals = [ - mhprob.capture_reward_expected_c(skill) - for skill in xrange(mhprob.CAP_SKILL_NONE, - mhprob.CAP_SKILL_GOD+1) - ] - cap = True - kill = False - else: - totals = [1] - # don't include Shiny in ev calculations - if reward["condition"].startswith("Shiny"): - cap = kill = False - shiny = True - elif reward["condition"].startswith("Break"): - cap = kill = True - else: - raise ValueError("Unknown condition: '%s'" - % reward["condition"]) - - evs = [i * reward["stack_size"] * reward["percentage"] - for i in totals] - if cap: - cap_ev[0] += evs[0] - if reward["condition"] == "Capture": - # It's very hard to get both cap skills and carve skills, - # assume only the capture rewards will get boosted. - cap_ev[1] += evs[-1] - else: - # can carve a tail when capping, but very hard to - # get cap skills and carve skills, so use the - # unboosted amount in the total with-skill expected value. - cap_ev[1] += evs[0] - if kill: - kill_ev[0] += evs[0] - kill_ev[1] += evs[-1] - if shiny: - shiny_ev += evs[0] - - out.write(" %20s %d %5.2f / 100" % (reward["condition"], - reward["stack_size"], - evs[0])) - out.write(" (%2d each)" % reward["percentage"]) - if len(totals) > 1: - out.write(" " + " ".join("%0.2f" % i for i in evs[1:])) - out.write("\n") + reward_rows = db.get_monster_rewards(mid, rank) + hunt_item = HuntItemExpectedValue(item_id, reward_rows) - if has_item: - out.write(" %20s\n" % "= Totals") - out.write(" %20s %s / 100\n" - % ("Kill", _format_range(*kill_ev))) - out.write(" %20s %s / 100\n" - % ("Cap", _format_range(*cap_ev))) - if shiny_ev: - out.write(" %20s %5.2f / 100\n" % ("Shiny", shiny_ev)) - out.write("\n") + out.write("%s %s\n" % (monster["name"], rank)) + hunt_item.print(out, indent=2) + + kill_ev = [0, 0] + kill_ev[0] = hunt_item.expected_value(STRAT_KILL) + kill_ev[1] = hunt_item.expected_value(STRAT_KILL, + carving_skill=mhprob.CARVING_SKILL_GOD) + cap_ev = [0, 0] + cap_ev[0] = hunt_item.expected_value(STRAT_CAP) + cap_ev[1] = hunt_item.expected_value(STRAT_CAP, + cap_skill=mhprob.CAP_SKILL_GOD) + shiny_ev = hunt_item.expected_value(STRAT_SHINY) + out.write(" %20s\n" % "= Totals") + out.write(" %20s %s / 100\n" + % ("Kill", _format_range(*kill_ev))) + out.write(" %20s %s / 100\n" + % ("Cap", _format_range(*cap_ev))) + if shiny_ev: + out.write(" %20s %5.2f / 100\n" % ("Shiny", shiny_ev)) + out.write("\n") def print_quests_and_rewards(db, item_row, out): @@ -146,57 +346,15 @@ def print_quests_and_rewards(db, item_row, out): for q in quests: out.write(unicode(q) + "\n") out.write(" %20s" % "= Quest\n") - quest_ev = 0 - sub_used = False - - fixed_rewards = dict(A=0, B=0, Sub=0) - total_reward_p = dict(A=0, B=0, Sub=0) - for reward in q._rewards: - slot = reward["reward_slot"] - #reward_item_row = db.get_item(reward["item_id"]) - #print slot, reward_item_row["name"], reward["percentage"] - if reward["percentage"] == 100: - fixed_rewards[slot] += 1 - else: - total_reward_p[slot] += reward["percentage"] - # sanity check values from the db - for slot in total_reward_p.keys(): - if total_reward_p[slot] not in (0, 100): - print("WARNING: bad total p for %s = %d" - % (slot, total_reward_p[slot]), file=out) + quest = QuestItemExpectedValue(item_id, q._rewards) + quest.check_totals(out) + quest.print(out, indent=2) - for reward in q._rewards: - slot = reward["reward_slot"] - #reward_item_row = db.get_item(reward["item_id"]) - #print slot, reward_item_row["name"], reward["percentage"] - if reward["item_id"] == item_id: - if reward["percentage"] == 100: - totals = [100] - evs = [100 * reward["stack_size"]] - else: - totals = [mhprob.quest_reward_expected_c(slot, skill) - for skill in xrange(mhprob.LUCK_SKILL_NONE, - mhprob.LUCK_SKILL_GREAT+1)] - - - evs = [((i - fixed_rewards[slot]) - * reward["stack_size"] * reward["percentage"]) - for i in totals] - - out.write(" %20s %d %5.2f / 100" % (reward["reward_slot"], - reward["stack_size"], - evs[0])) - out.write(" (%2d each)" % reward["percentage"]) - if len(totals) > 1: - out.write(" %s" % " ".join("%0.2f" % i for i in evs[1:])) - out.write("\n") - - quest_ev += evs[0] - if reward["reward_slot"] == "Sub": - sub_used = True monsters = db.get_quest_monsters(q.id) + quest_ev = quest.expected_value() + cap_ev = [quest_ev, quest_ev] kill_ev = [quest_ev, quest_ev] shiny_ev = 0 @@ -204,79 +362,32 @@ def print_quests_and_rewards(db, item_row, out): mid = m["monster_id"] monster = db.get_monster(mid) has_item = False - rewards = db.get_monster_rewards(mid, q.rank) - for reward in rewards: - cap = kill = shiny = False - if reward["item_id"] == item_id: - if not has_item: - has_item = True - out.write(" %20s\n" - % ("= " + monster["name"] + " " + q.rank)) - if reward["condition"] == "Body Carve": - totals = [ - 3 + mhprob.carve_delta_expected_c(skill) - for skill in xrange(mhprob.CARVING_SKILL_PRO, - mhprob.CARVING_SKILL_GOD+1) - ] - cap = False - kill = True - elif reward["condition"] == "Body Carve (Apparent Death)": - # assume one carve, is dangerous to try for two - totals = [1] - cap = True - kill = True - elif reward["condition"] == "Tail Carve": - totals = [ - 1 + mhprob.carve_delta_expected_c(skill) - for skill in xrange(mhprob.CARVING_SKILL_PRO, - mhprob.CARVING_SKILL_GOD+1) - ] - cap = kill = True - elif reward["condition"] == "Capture": - totals = [ - mhprob.capture_reward_expected_c(skill) - for skill in xrange(mhprob.CAP_SKILL_NONE, - mhprob.CAP_SKILL_GOD+1) - ] - cap = True - kill = False - else: - totals = [1] - # don't include Shiny in ev calculations - if reward["condition"].startswith("Shiny"): - cap = kill = False - shiny = True - elif reward["condition"].startswith("Break"): - cap = kill = True - else: - - raise ValueError("Unknown condition: '%s'" - % reward["condition"]) - - evs = [i * reward["stack_size"] * reward["percentage"] - for i in totals] - if cap: - cap_ev[0] += evs[0] - cap_ev[1] += evs[-1] - if kill: - kill_ev[0] += evs[0] - kill_ev[1] += evs[-1] - if shiny: - shiny_ev += evs[0] - - out.write(" %20s %d %5.2f / 100" % (reward["condition"], - reward["stack_size"], - evs[0])) - out.write(" (%2d each)" % reward["percentage"]) - if len(totals) > 1: - out.write(" " + " ".join("%0.2f" % i for i in evs[1:])) - out.write("\n") + reward_rows = db.get_monster_rewards(mid, q.rank) + hunt_item = HuntItemExpectedValue(item_id, reward_rows) + + kill_ev[0] += hunt_item.expected_value(STRAT_KILL) + kill_ev[1] += hunt_item.expected_value(STRAT_KILL, + carving_skill=mhprob.CARVING_SKILL_GOD) + cap_ev[0] += hunt_item.expected_value(STRAT_CAP) + cap_ev[1] += hunt_item.expected_value(STRAT_CAP, + cap_skill=mhprob.CAP_SKILL_GOD) + shiny_ev = hunt_item.expected_value(STRAT_SHINY) + + if kill_ev[0] == 0 and cap_ev[0] == 0 and shiny_ev == 0: + continue + + out.write(" %20s\n" % ("= " + monster["name"] + " " + q.rank)) + + hunt_item.print(out, indent=2) + out.write(" %20s\n" % "= Totals") - out.write(" %20s %s / 100\n" % ("Kill", _format_range(*kill_ev))) - out.write(" %20s %s / 100\n" % ("Cap", _format_range(*cap_ev))) + out.write(" %20s %s / 100\n" + % ("Kill", _format_range(*kill_ev))) + out.write(" %20s %s / 100\n" + % ("Cap", _format_range(*cap_ev))) if shiny_ev: out.write(" %20s %5.2f / 100\n" % ("Shiny", shiny_ev)) - out.write("\n") + out.write("\n") if __name__ == '__main__':