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.
main
Bryce Allen 11 years ago
parent ddfefc99c4
commit 591ac4bdeb

@ -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,93 +44,285 @@ def find_item(db, item_name, err_out):
return item_row
def print_monsters_and_rewards(db, item_row, out):
item_id = item_row["_id"]
monsters = db.get_item_monsters(item_id)
for m in monsters:
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)
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:
cap = kill = shiny = False
if reward["item_id"] != item_id:
if reward["item_id"] != self.item_id:
continue
if not has_item:
has_item = True
out.write("%s %s\n" % (monster["name"], rank))
if reward["condition"] == "Body Carve":
totals = [
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)
]
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 = [
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)
]
cap = kill = True
elif reward["condition"] == "Capture":
totals = [
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)
]
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
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'"
% 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")
% 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
"""
if has_item:
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)
for m in monsters:
mid = m["monster_id"]
rank = m["rank"]
monster = db.get_monster(mid)
reward_rows = db.get_monster_rewards(mid, rank)
hunt_item = HuntItemExpectedValue(item_id, reward_rows)
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)))
@ -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)
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 = QuestItemExpectedValue(item_id, q._rewards)
quest.check_totals(out)
quest.print(out, indent=2)
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,76 +362,29 @@ 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:
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)
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")
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")

Loading…
Cancel
Save