You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

416 lines
14 KiB

#!/usr/bin/env python
from __future__ import print_function
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)
def _format_range(min_v, max_v):
if min_v == max_v:
return "%5.2f" % min_v
else:
return "%5.2f to %5.2f" % (min_v, max_v)
def find_item(db, item_name, err_out):
item_row = db.get_item_by_name(item_name)
if item_row is None:
print("Item '%s' not found. Listing partial matches:" % item_name,
file=err_out)
terms = item_name.split()
for term in terms:
if len(term) < 2:
# single char terms aren't very useful, too many results
continue
print("= Matching term '%s'" % term, file=err_out)
rows = db.search_item_name(term, "Flesh")
for row in rows:
print(" ", row["name"], file=err_out)
return None
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)
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)))
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):
"""
Get a list of the quests for acquiring a given item and the probability
of getting the item, depending on cap or kill and luck skills.
"""
item_id = item_row["_id"]
quests = db.get_item_quest_objects(item_id)
print_monsters_and_rewards(db, item_row, out)
if not quests:
return
for q in quests:
out.write(unicode(q) + "\n")
out.write(" %20s" % "= Quest\n")
quest = QuestItemExpectedValue(item_id, q._rewards)
quest.check_totals(out)
quest.print(out, indent=2)
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
for m in monsters:
mid = m["monster_id"]
monster = db.get_monster(mid)
has_item = False
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)))
if shiny_ev:
out.write(" %20s %5.2f / 100\n" % ("Shiny", shiny_ev))
out.write("\n")
if __name__ == '__main__':
import sys
import os
import os.path
if len(sys.argv) != 2:
print("Usage: %s 'item name'" % sys.argv[0])
sys.exit(os.EX_USAGE)
item_name = sys.argv[1]
out = get_utf8_writer(sys.stdout)
err_out = get_utf8_writer(sys.stderr)
# TODO: doesn't work if script is symlinked
db_path = os.path.dirname(sys.argv[0])
db_path = os.path.join(db_path, "db", "mh4u.db")
db = mhdb.MHDB(db_path)
item_row = find_item(db, item_name, err_out)
if item_row is None:
sys.exit(os.EX_DATAERR)
print_quests_and_rewards(db, item_row, out)