move code into mhapi package
This commit is contained in:
0
mhapi/__init__.py
Normal file
0
mhapi/__init__.py
Normal file
178
mhapi/db.py
Executable file
178
mhapi/db.py
Executable file
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
Module for accessing the sqlite monster hunter db from
|
||||
"""
|
||||
|
||||
import string
|
||||
|
||||
import sqlite3
|
||||
|
||||
|
||||
class Quest(object):
|
||||
def __init__(self, quest_row, quest_rewards):
|
||||
self._row = quest_row
|
||||
self._rewards = quest_rewards
|
||||
|
||||
self._template = string.Template(
|
||||
"$name ($hub $stars* $rank)"
|
||||
+ "\n Goal: $goal"
|
||||
+ "\n Sub : $sub_goal"
|
||||
)
|
||||
|
||||
self.id = quest_row["_id"]
|
||||
self.name = quest_row["name"]
|
||||
self.stars = quest_row["stars"]
|
||||
self.hub = quest_row["hub"]
|
||||
self.goal = quest_row["goal"]
|
||||
self.sub_goal = quest_row["sub_goal"]
|
||||
self.rank = get_rank(self.hub, self.stars)
|
||||
|
||||
def __unicode__(self):
|
||||
return self._template.substitute(self.__dict__)
|
||||
|
||||
|
||||
def get_rank(hub, stars):
|
||||
if hub == "Caravan":
|
||||
if stars < 6:
|
||||
return "LR"
|
||||
elif stars < 10:
|
||||
return "HR"
|
||||
return "G"
|
||||
if hub == "Guild":
|
||||
if stars < 4:
|
||||
return "LR"
|
||||
elif stars < 8:
|
||||
return "HR"
|
||||
return "G"
|
||||
|
||||
|
||||
class MHDB(object):
|
||||
def __init__(self, path, use_cache=True):
|
||||
self.conn = sqlite3.connect(path)
|
||||
self.conn.row_factory = sqlite3.Row
|
||||
self.use_cache = use_cache
|
||||
self.cache = {}
|
||||
|
||||
def _get_memoized(self, key, query, *args):
|
||||
if self.use_cache:
|
||||
if key in self.cache:
|
||||
v = self.cache[key].get(args)
|
||||
if v is not None:
|
||||
return v
|
||||
else:
|
||||
self.cache[key] = {}
|
||||
cursor = self.conn.execute(query, args)
|
||||
v = cursor.fetchall()
|
||||
if self.use_cache:
|
||||
self.cache[key][args] = v
|
||||
return v
|
||||
|
||||
def get_item_names(self):
|
||||
v = self._get_memoized("item_names", """
|
||||
SELECT name FROM items
|
||||
WHERE type IN ('Bone', 'Flesh', 'Sac/Fluid')
|
||||
""")
|
||||
return v
|
||||
|
||||
def get_item(self, item_id):
|
||||
v = self._get_memoized("item", """
|
||||
SELECT * FROM items
|
||||
WHERE _id=?
|
||||
""", item_id)
|
||||
return v[0] if v else None
|
||||
|
||||
def get_item_by_name(self, name):
|
||||
v = self._get_memoized("item", """
|
||||
SELECT * FROM items
|
||||
WHERE name=?
|
||||
""", name)
|
||||
return v[0] if v else None
|
||||
|
||||
def search_item_name(self, term, item_type):
|
||||
"""
|
||||
Search for items containing @term somewhere in the name. Returns
|
||||
list of matching items.
|
||||
|
||||
Not memoized.
|
||||
"""
|
||||
cursor = self.conn.execute("""
|
||||
SELECT * FROM items
|
||||
WHERE name LIKE ?
|
||||
AND type = ?
|
||||
""", ("%%%s%%" % term, item_type))
|
||||
return cursor.fetchall()
|
||||
|
||||
def get_monster_by_name(self, name):
|
||||
v = self._get_memoized("monster", """
|
||||
SELECT * FROM monsters
|
||||
WHERE name=?
|
||||
""", name)
|
||||
return v[0] if v else None
|
||||
|
||||
def get_monster(self, monster_id):
|
||||
v = self._get_memoized("monster", """
|
||||
SELECT * FROM monsters
|
||||
WHERE _id=?
|
||||
""", monster_id)
|
||||
return v[0] if v else None
|
||||
|
||||
def get_quest(self, quest_id):
|
||||
v = self._get_memoized("quest", """
|
||||
SELECT * FROM quests
|
||||
WHERE _id=?
|
||||
""", quest_id)
|
||||
return v[0] if v else None
|
||||
|
||||
def get_quest_rewards(self, quest_id):
|
||||
v = self._get_memoized("quest_rewards", """
|
||||
SELECT * FROM quest_rewards
|
||||
WHERE quest_id=?
|
||||
""", quest_id)
|
||||
return v
|
||||
|
||||
def get_monster_rewards(self, monster_id, rank=None):
|
||||
q = """
|
||||
SELECT * FROM hunting_rewards
|
||||
WHERE monster_id=?
|
||||
"""
|
||||
if rank is not None:
|
||||
q += "AND rank=?"
|
||||
v = self._get_memoized("monster_rewards", q, monster_id, rank)
|
||||
else:
|
||||
v = self._get_memoized("monster_rewards", q, monster_id)
|
||||
return v
|
||||
|
||||
def get_quest_monsters(self, quest_id):
|
||||
v = self._get_memoized("quest_monsters", """
|
||||
SELECT monster_id FROM monster_to_quest
|
||||
WHERE quest_id=? AND unstable='no'
|
||||
""", quest_id)
|
||||
return v
|
||||
|
||||
def get_item_quest_objects(self, item_id):
|
||||
"""
|
||||
Get a list of quests that provide the specified item in quest
|
||||
reqards. Returns a list of quest objects, which encapsulate the
|
||||
quest details and the list of rewards.
|
||||
"""
|
||||
cursor = self.conn.execute("""
|
||||
SELECT DISTINCT quest_id FROM quest_rewards
|
||||
WHERE item_id=?
|
||||
""", (item_id,))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
|
||||
quests = []
|
||||
for r in rows:
|
||||
quest_row = self.get_quest(r["quest_id"])
|
||||
rewards_rows = self.get_quest_rewards(r["quest_id"])
|
||||
quests.append(Quest(quest_row, rewards_rows))
|
||||
|
||||
return quests
|
||||
|
||||
def get_item_monsters(self, item_id):
|
||||
v = self._get_memoized("item_monsters", """
|
||||
SELECT DISTINCT monster_id, rank FROM hunting_rewards
|
||||
WHERE item_id=?
|
||||
""", item_id)
|
||||
|
||||
return v
|
||||
415
mhapi/rewards.py
Executable file
415
mhapi/rewards.py
Executable file
@@ -0,0 +1,415 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import print_function
|
||||
import codecs
|
||||
|
||||
from mhapi.db import MHDB
|
||||
from mhapi import stats
|
||||
|
||||
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=stats.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 = [stats.quest_reward_expected_c(self.slot, skill)
|
||||
for skill in xrange(stats.LUCK_SKILL_NONE,
|
||||
stats.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=stats.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=stats.CAP_SKILL_NONE,
|
||||
carving_skill=stats.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 + stats.carve_delta_expected_c(skill)
|
||||
for skill in xrange(stats.CARVING_SKILL_PRO,
|
||||
stats.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 + stats.carve_delta_expected_c(skill)
|
||||
for skill in xrange(stats.CARVING_SKILL_PRO,
|
||||
stats.CARVING_SKILL_GOD+1)
|
||||
]
|
||||
elif self.condition == "Capture":
|
||||
self.skill = SKILL_CAP
|
||||
self.cap = True
|
||||
self.kill = False
|
||||
counts = [
|
||||
stats.capture_reward_expected_c(skill)
|
||||
for skill in xrange(stats.CAP_SKILL_NONE,
|
||||
stats.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=stats.CAP_SKILL_NONE,
|
||||
carving_skill=stats.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=stats.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=stats.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=stats.CARVING_SKILL_GOD)
|
||||
cap_ev[0] += hunt_item.expected_value(STRAT_CAP)
|
||||
cap_ev[1] += hunt_item.expected_value(STRAT_CAP,
|
||||
cap_skill=stats.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(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)
|
||||
261
mhapi/stats.py
Executable file
261
mhapi/stats.py
Executable file
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Calculate probability of getting at least one of a monster part from one
|
||||
# line in the quest rewards. Also calculates expected value of part
|
||||
# counts (N), and probabilities of getting a certain number of rewards (C).
|
||||
#
|
||||
# usage: mhprob.py reward_percent fixed_rewards gaurenteed_rewards \
|
||||
# [extend_percent]
|
||||
#
|
||||
# reward_percent - chance of getting the monster part in %
|
||||
# fixed_rewards - number of rewards in the reward list with 100% chance and
|
||||
# are not the item you are looking for. Takes away from
|
||||
# the total possible draw attempts for what you want.
|
||||
# Default 1 which is typical for line A in many quests.
|
||||
# gaurenteed_rewards - minimum number of quest rewards in the line,
|
||||
# including any fixed rewards. In Tri (see link
|
||||
# below) this is 3 for line A and 1 for line B.
|
||||
# Defaults to 3.
|
||||
# extend_percent - chance of getting one more reward in the line in %,
|
||||
# default 69
|
||||
#
|
||||
# You can use http://kiranico.com to get reward percent and fixed rewards
|
||||
# values.
|
||||
#
|
||||
# For extend percent, use the default unless you have the Lucky Cat or
|
||||
# Ultra Lucky Cat food skills or Good Luck or Great Luck armor skills:
|
||||
#
|
||||
# normal extra reward %: 69
|
||||
# good luck extra reward %: 81
|
||||
# great luck extra %: 90
|
||||
#
|
||||
# Can also be used to calculate chance of getting a part from carving,
|
||||
# using extend_percent=0, fixed_rewards=0, and gaurenteed_rewards=3 (or
|
||||
# 4 for monsters with 4 carves).
|
||||
#
|
||||
# Source:
|
||||
# http://www.gamefaqs.com/wii/943655-monster-hunter-tri/faqs/60448
|
||||
# Not sure if there are differences in 3U or 4U.
|
||||
#
|
||||
# Example Plain dangerous in 3U, has 2 fixed rewards in A, one in B, hardhorns
|
||||
# are 5% both A and B:
|
||||
# A: ./mhprop.py 5 2 3 69
|
||||
# B: ./mhprop.py 5 1 1 69
|
||||
# For great luck, replace 69 with 90
|
||||
|
||||
CAP_SKILL_NONE = 0
|
||||
CAP_SKILL_EXPERT = 1
|
||||
CAP_SKILL_MASTER = 2
|
||||
CAP_SKILL_GOD = 3
|
||||
|
||||
LUCK_SKILL_NONE = 0
|
||||
LUCK_SKILL_GOOD = 1
|
||||
LUCK_SKILL_GREAT = 2
|
||||
|
||||
QUEST_A = "A"
|
||||
QUEST_B = "B"
|
||||
QUEST_SUB = "Sub"
|
||||
|
||||
CARVING_SKILL_NONE = 0
|
||||
CARVING_SKILL_PRO = 0 # prevent knockbacks but no extra carves
|
||||
CARVING_SKILL_FELYNE_LOW = 1
|
||||
CARVING_SKILL_FELYNE_HI = 2
|
||||
CARVING_SKILL_CELEBRITY = 3
|
||||
CARVING_SKILL_GOD = 4
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
def _quest_reward_p(reward_percent, reward_count):
|
||||
"""
|
||||
Propability of getting at least one item from @reward_count draws
|
||||
with a @reward_percent chance.
|
||||
"""
|
||||
fail_percent = (100 - reward_percent)
|
||||
return 1.0 - (fail_percent / 100.0)**reward_count
|
||||
|
||||
def _reward_count_p(reward_count, min_rewards, max_rewards, extend_percent):
|
||||
"""
|
||||
Probability of getting a certain number of rewards for a given chance
|
||||
@extend_percent of getting one more drop.
|
||||
"""
|
||||
if reward_count == min_rewards:
|
||||
return (100 - extend_percent) / 100.0
|
||||
p = 1.0
|
||||
extend_p = extend_percent / 100.0
|
||||
stop_p = 1.0 - extend_p
|
||||
for i in xrange(min_rewards+1, reward_count+1):
|
||||
p *= extend_p
|
||||
if reward_count < max_rewards:
|
||||
p *= stop_p
|
||||
return p
|
||||
|
||||
def quest_reward_p(reward_percent, min_rewards, max_rewards, extend_percent=69):
|
||||
"""
|
||||
Probability of getting at least one of the item, given the item has
|
||||
@reward_percent chance, @min_rewards minimum number of attempts,
|
||||
@max_rewards max attempts, and @extend_percent chance of getting each
|
||||
extra attempt.
|
||||
"""
|
||||
p = 0.0
|
||||
for reward_count in xrange(min_rewards, max_rewards + 1):
|
||||
p += (_reward_count_p(reward_count, min_rewards, max_rewards,
|
||||
extend_percent)
|
||||
* _quest_reward_p(reward_percent, reward_count))
|
||||
return p * 100
|
||||
|
||||
|
||||
def reward_expected_c(min_rewards, max_rewards, extend_percent):
|
||||
"""
|
||||
Expected value for number of rewards, if @min_rewards are gauranteed
|
||||
and there is an @extend_percent chance of getting one more each time
|
||||
with at most @max_rewards.
|
||||
"""
|
||||
total_p = 0.0
|
||||
expected_attempts = 0.0
|
||||
for reward_count in xrange(min_rewards, max_rewards + 1):
|
||||
p = _reward_count_p(reward_count, min_rewards, max_rewards,
|
||||
extend_percent)
|
||||
expected_attempts += p * reward_count
|
||||
#print "P(C = %d) = %0.4f" % (reward_count, p)
|
||||
total_p += p
|
||||
assert abs(total_p - 1.0) < .00001, "total = %f" % total_p
|
||||
return expected_attempts
|
||||
|
||||
|
||||
def quest_reward_expected_c(line=QUEST_A, luck_skill=LUCK_SKILL_NONE):
|
||||
"""
|
||||
Expected number of rewards from specified quest line with given skills.
|
||||
|
||||
Note: if the quest has fixed rewards that aren't the desired item, it will
|
||||
reduce the expected count for the desired item. Just subtract the number
|
||||
of fixed items from the output to get the actual value.
|
||||
"""
|
||||
if luck_skill == LUCK_SKILL_NONE:
|
||||
extend_p = 69
|
||||
elif luck_skill == LUCK_SKILL_GOOD:
|
||||
extend_p = 81
|
||||
elif luck_skill == LUCK_SKILL_GREAT:
|
||||
extend_p = 90
|
||||
else:
|
||||
raise ValueError()
|
||||
|
||||
# TODO: not sure if these are correct for 4U, but I've
|
||||
# heard that 3U had 4/2 gaurenteed for A/B.
|
||||
if line == QUEST_A:
|
||||
min_c = 4
|
||||
max_c = 8
|
||||
elif line == QUEST_B:
|
||||
min_c = 2
|
||||
max_c = 8
|
||||
elif line == QUEST_SUB:
|
||||
min_c = 1
|
||||
max_c = 4
|
||||
else:
|
||||
raise ValueError()
|
||||
|
||||
return reward_expected_c(min_c, max_c, extend_p)
|
||||
|
||||
|
||||
def capture_reward_expected_c(cap_skill=CAP_SKILL_NONE):
|
||||
"""
|
||||
Expected value for number of capture rewards given the specified
|
||||
capture skill (none by default).
|
||||
"""
|
||||
if cap_skill == CAP_SKILL_NONE:
|
||||
min_c = 2
|
||||
max_c = 3
|
||||
extend_p = 69
|
||||
elif cap_skill == CAP_SKILL_EXPERT:
|
||||
return 3
|
||||
elif cap_skill == CAP_SKILL_MASTER:
|
||||
min_c = 3
|
||||
max_c = 4
|
||||
extend_p = 69
|
||||
elif cap_skill == CAP_SKILL_GOD:
|
||||
return 4
|
||||
else:
|
||||
raise ValueError()
|
||||
return reward_expected_c(min_c, max_c, extend_p)
|
||||
|
||||
|
||||
def carve_delta_expected_c(carve_skill):
|
||||
"""
|
||||
Expected value for the number of extra carves with the given skill.
|
||||
|
||||
Word on the street is that since Tri the felyne skills do not stack with
|
||||
the armor skills, i.e. if you have Carving Celebrity plus you get at
|
||||
least one extra carves and the felyne skills do nothing.
|
||||
"""
|
||||
if carve_skill == CARVING_SKILL_CELEBRITY:
|
||||
# Description: Increases the number of carving chances by one and prevents knockbacks while carving.
|
||||
return 1
|
||||
elif carve_skill == CARVING_SKILL_GOD:
|
||||
# Description: Increases the number of carving chances by one (maybe more) and prevents knockbacks while carving.
|
||||
# TODO: max 2 and 50% extend is a guess, find the actual values
|
||||
min_c = 1
|
||||
max_c = 2
|
||||
extend_p = 50
|
||||
elif carve_skill == CARVING_SKILL_FELYNE_LOW:
|
||||
min_c = 0
|
||||
max_c = 1
|
||||
extend_p = 25
|
||||
elif carve_skill == CARVING_SKILL_FELYNE_HI:
|
||||
min_c = 0
|
||||
max_c = 1
|
||||
extend_p = 50
|
||||
elif carve_skill in (CARVING_SKILL_NONE, CARVING_SKILL_PRO):
|
||||
# Description: Prevents knockbacks from attacks while carving.
|
||||
return 0
|
||||
else:
|
||||
raise ValueError()
|
||||
return reward_expected_c(min_c, max_c, extend_p)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# in percent
|
||||
reward_percent = int(sys.argv[1])
|
||||
if len(sys.argv) > 2:
|
||||
fixed_rewards = int(sys.argv[2])
|
||||
else:
|
||||
fixed_rewards = 1
|
||||
if len(sys.argv) > 3:
|
||||
guarenteed_rewards = int(sys.argv[3])
|
||||
else:
|
||||
guarenteed_rewards = 3
|
||||
if len(sys.argv) > 4:
|
||||
extend_percent = int(sys.argv[4])
|
||||
else:
|
||||
extend_percent = 69
|
||||
|
||||
min_rewards = guarenteed_rewards - fixed_rewards
|
||||
max_rewards = 8 - fixed_rewards
|
||||
|
||||
if min_rewards < 0:
|
||||
print "Error: fixed_rewards (%d) must be less than or equal to " \
|
||||
"guaranteeed_rewards (%d)" % (fixed_rewards, guarenteed_rewards)
|
||||
sys.exit(1)
|
||||
|
||||
total_p = 0.0
|
||||
expected_attempts = 0.0
|
||||
for reward_count in xrange(min_rewards, max_rewards + 1):
|
||||
p = _reward_count_p(reward_count, min_rewards, max_rewards,
|
||||
extend_percent)
|
||||
expected_attempts += p * reward_count
|
||||
# probability of getting @reward_count rewards that could be the
|
||||
# desired item
|
||||
print "P(C = %d) = %0.4f" % (reward_count, p)
|
||||
total_p += p
|
||||
# expected value for number of rewards that could be the desired item
|
||||
print "E(C) = %0.2f" % expected_attempts
|
||||
|
||||
# math check, make sure all possibilities add up to 1, allowing for
|
||||
# some floating point precision loss.
|
||||
assert abs(total_p - 1.0) < .00001, "total = %f" % total_p
|
||||
|
||||
p_at_least_one = quest_reward_p(reward_percent, min_rewards, max_rewards,
|
||||
extend_percent)
|
||||
expected = expected_attempts * reward_percent / 100.0
|
||||
|
||||
print "P(N > 0) = %0.2f%%" % p_at_least_one
|
||||
print "E(N) = %0.4f" % expected
|
||||
0
mhapi/web/__init__.py
Normal file
0
mhapi/web/__init__.py
Normal file
8
mhapi/web/fcgi.py
Executable file
8
mhapi/web/fcgi.py
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from flup.server.fcgi import WSGIServer
|
||||
|
||||
from mhapi.web.wsgi import application
|
||||
|
||||
if __name__ == '__main__':
|
||||
WSGIServer(application).run()
|
||||
122
mhapi/web/wsgi.py
Executable file
122
mhapi/web/wsgi.py
Executable file
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from webob import Request, Response, exc
|
||||
|
||||
from mhapi.db import MHDB
|
||||
from mhapi import rewards
|
||||
|
||||
DB_VERSION = "20150313"
|
||||
PREFIX = "/mhapi/"
|
||||
|
||||
# good for testing, use 1 hour = 3600 for deployment
|
||||
MAX_AGE = "60"
|
||||
|
||||
logging.basicConfig(filename="/tmp/reward_webapp.log", level=logging.INFO)
|
||||
|
||||
|
||||
class ThreadLocalDB(threading.local):
|
||||
def __init__(self, path):
|
||||
threading.local.__init__(self)
|
||||
self._db = MHDB(path)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._db, name)
|
||||
|
||||
|
||||
class App(object):
|
||||
def __init__(self):
|
||||
self.web_path = os.path.dirname(__file__)
|
||||
self.project_path = os.path.abspath(os.path.join(self.web_path,
|
||||
"..", ".."))
|
||||
|
||||
db_path = os.path.join(self.project_path, "db", "mh4u.db")
|
||||
self.db = ThreadLocalDB(db_path)
|
||||
|
||||
log_path = os.path.join(self.project_path, "web.log")
|
||||
|
||||
self.log = logging.getLogger("reward_webapp")
|
||||
self.log.info("app started")
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
req = Request(environ)
|
||||
resp = Response()
|
||||
resp.charset = "utf8"
|
||||
|
||||
if req.path_info in ("", "/", "/index.html"):
|
||||
resp = self.index(req, resp)
|
||||
elif req.path_info == PREFIX + "rewards":
|
||||
resp = self.find_item_rewards(req, resp)
|
||||
elif req.path_info == PREFIX + "item_name_list":
|
||||
resp = self.get_all_names(req, resp)
|
||||
else:
|
||||
resp = exc.HTTPNotFound()
|
||||
|
||||
return resp(environ, start_response)
|
||||
|
||||
def index(self, req, resp):
|
||||
resp.cache_control = "max-age=86400"
|
||||
html_path = os.path.join(self.web_path, "index.html")
|
||||
resp.content_type = "text/html"
|
||||
resp.body = open(html_path, "rb").read()
|
||||
return resp
|
||||
|
||||
def find_item_rewards(self, req, resp):
|
||||
version = "1"
|
||||
etag = DB_VERSION + version
|
||||
resp.cache_control = "public, max-age=" + MAX_AGE
|
||||
resp.etag = etag
|
||||
|
||||
if etag in req.if_none_match:
|
||||
return exc.HTTPNotModified()
|
||||
|
||||
resp.content_type = "text/plain"
|
||||
|
||||
item_name = req.params.get("item_name", "").strip()
|
||||
if not item_name:
|
||||
resp.body = "Please enter an item name"
|
||||
else:
|
||||
item_row = rewards.find_item(self.db, item_name, resp.body_file)
|
||||
if item_row is not None:
|
||||
rewards.print_quests_and_rewards(self.db, item_row,
|
||||
resp.body_file)
|
||||
return resp
|
||||
|
||||
def get_all_names(self, req, resp):
|
||||
version = "2"
|
||||
etag = DB_VERSION + version
|
||||
resp.cache_control = "public, max-age=" + MAX_AGE
|
||||
resp.etag = etag
|
||||
|
||||
if etag in req.if_none_match:
|
||||
return exc.HTTPNotModified()
|
||||
|
||||
resp.content_type = "application/json"
|
||||
resp.body_file.write("[")
|
||||
items = self.db.get_item_names()
|
||||
first = True
|
||||
for item in items:
|
||||
if first:
|
||||
first = False
|
||||
else:
|
||||
resp.body_file.write(", ")
|
||||
resp.body_file.write('"')
|
||||
resp.body_file.write(item["name"])
|
||||
resp.body_file.write('"')
|
||||
resp.body_file.write("]")
|
||||
return resp
|
||||
|
||||
|
||||
application = App()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from wsgiref.simple_server import make_server
|
||||
httpd = make_server("", 8080, application)
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print "^C"
|
||||
Reference in New Issue
Block a user