Skip to content

Commit dbba354

Browse files
committed
✨ New shiny drop data management all in one place.
Along with a new script to estimate base drop rates and a revamped explore vs lemonade drop comparison tool.
1 parent 0d5a8c6 commit dbba354

File tree

6 files changed

+408
-20
lines changed

6 files changed

+408
-20
lines changed

py/base_droprates.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from fractions import Fraction
2+
3+
import typer
4+
5+
import droprates
6+
7+
8+
def base_droprates(cider: bool = False, limit: int = 20) -> None:
9+
for loc, loc_explores in sorted(
10+
droprates.compile_drops(explore=True, cider=cider).locations.items()
11+
):
12+
drops_per_explore = loc_explores.drops / loc_explores.explores
13+
rounded = Fraction(drops_per_explore).limit_denominator(limit)
14+
num, denom = rounded.as_integer_ratio()
15+
print(f"{loc}: {num}/{denom} ({loc_explores.explores})")
16+
17+
18+
if __name__ == "__main__":
19+
typer.run(base_droprates)

py/droprates.py

+133
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,141 @@
11
import sys
2+
import time
3+
from collections import defaultdict
4+
from typing import Any, Union
25

6+
import attrs
7+
8+
import fixtures
39
import parse_logs
410

511

12+
def when_dropped(item: fixtures.Item) -> range:
13+
first_dropped = item.first_dropped or item.first_seen or 0
14+
last_dropped = item.last_dropped or round((time.time() + 10000000) * 1000)
15+
return range(first_dropped, last_dropped + 1)
16+
17+
18+
@attrs.define
19+
class ItemDrops:
20+
explores: int = 0
21+
lemonades: int = 0
22+
ciders: int = 0
23+
fishes: int = 0
24+
nets: int = 0
25+
drops: int = 0
26+
27+
28+
@attrs.define
29+
class LocationDrops:
30+
explores: int = 0
31+
lemonades: int = 0
32+
ciders: int = 0
33+
fishes: int = 0
34+
nets: int = 0
35+
drops: int = 0
36+
items: dict[str, ItemDrops] = attrs.Factory(lambda: defaultdict(ItemDrops))
37+
38+
39+
@attrs.define
40+
class Drops:
41+
explores: int = 0
42+
lemonades: int = 0
43+
ciders: int = 0
44+
fishes: int = 0
45+
nets: int = 0
46+
drops: int = 0
47+
locations: dict[str, LocationDrops] = attrs.Factory(
48+
lambda: defaultdict(LocationDrops)
49+
)
50+
51+
52+
def count_sources(
53+
drops: Union[Drops, LocationDrops, ItemDrops], row: dict[str, Any]
54+
) -> None:
55+
if row["type"] == "explore":
56+
drops.explores += row["results"]["stamina"]
57+
elif row["type"] == "lemonade":
58+
drops.lemonades += 1
59+
elif row["type"] == "cider":
60+
drops.ciders += 1
61+
# This is kind of wrong for global and location stats since not all explores count
62+
# for all items but it's more correct than not.
63+
drops.explores += row["results"].get("explores", row["results"]["stamina"])
64+
elif row["type"] == "fish":
65+
drops.fishes += 1
66+
elif row["type"] == "net":
67+
drops.nets += 1
68+
69+
70+
def compile_drops(
71+
explore: bool = False,
72+
lemonade: bool = False,
73+
cider: bool = False,
74+
fish: bool = False,
75+
net: bool = False,
76+
) -> Drops:
77+
when_items_dropped = {
78+
item.name: when_dropped(item) for item in fixtures.load_items()
79+
}
80+
# loc_items = {loc.name: loc.items for loc in fixtures.load_locations()}
81+
types: set[str] = set()
82+
if explore:
83+
types.add("explore")
84+
if lemonade:
85+
types.add("lemonade")
86+
if cider:
87+
types.add("cider")
88+
if fish:
89+
types.add("fish")
90+
if net:
91+
types.add("net")
92+
explores = Drops()
93+
logs = [row for row in parse_logs.parse_logs() if row["type"] in types]
94+
# First run through to count drops and work out which items are in each locations.
95+
for row in logs:
96+
location_name = row["results"].get("location")
97+
if not location_name:
98+
continue
99+
overflow_items: set[str] = {
100+
item["item"] for item in row["results"]["items"] if item["overflow"]
101+
}
102+
for item in row["results"]["items"]:
103+
if row["ts"] not in when_items_dropped[item["item"]]:
104+
# Ignore out-of-bounds drops. This allows accounting for stuff like drop
105+
# rates changing substantially by manually resetting firstDropped.
106+
continue
107+
if row["type"] == "cider" and item["item"] in overflow_items:
108+
# Cider overflow always reports 0 drops so any item that overflows during
109+
# a cider has to be ignored.
110+
continue
111+
explores.locations[location_name].items[item["item"]].drops += item.get(
112+
"quantity", 1
113+
)
114+
explores.locations[location_name].drops += item.get("quantity", 1)
115+
explores.drops += item.get("quantity", 1)
116+
# Second pass to get the explore counts.
117+
for row in logs:
118+
location_name = row["results"].get("location")
119+
if not location_name:
120+
continue
121+
overflow_items: set[str] = {
122+
item["item"] for item in row["results"]["items"] if item["overflow"]
123+
}
124+
for item, item_explores in explores.locations[location_name].items.items():
125+
if row["ts"] not in when_items_dropped[item]:
126+
# Item couldn't drop, this doesn't count.
127+
continue
128+
if row["type"] == "cider" and item in overflow_items:
129+
# Cider overflow always reports 0 drops so any item that overflows during
130+
# a cider has to be ignored.
131+
continue
132+
count_sources(item_explores, row)
133+
count_sources(explores.locations[location_name], row)
134+
count_sources(explores, row)
135+
136+
return explores
137+
138+
6139
def total_drops() -> dict[str, dict[str, int]]:
7140
totals = {}
8141
for row in parse_logs.parse_logs("explore"):

py/exp_vs_lem.py

+72-20
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,73 @@
1+
import statistics
2+
from typing import Optional
3+
4+
import typer
5+
16
import droprates
2-
import lemonade
3-
4-
lem = lemonade.drop_rates()
5-
# exp = droprates.rates_per_stam()
6-
exp = droprates.drop_rates()
7-
8-
locations = set(lem.keys()) | set(exp.keys())
9-
10-
for loc in sorted(locations):
11-
print(f"{loc}:")
12-
lem_data = lem.get(loc, {})
13-
exp_data = exp.get(loc, {})
14-
items = set(lem_data.keys()) | set(exp_data.keys())
15-
for item in sorted(items):
16-
lem_rate = lem_data.get(item, 0)
17-
exp_rate = exp_data.get(item, 0)
18-
if lem_rate == 0 or exp_rate == 0:
19-
print(f"\t{item}: NO MATCH")
20-
else:
21-
print(f"\t{item}: {lem_rate / exp_rate}")
7+
8+
BASE_DROP_RATES = {
9+
"Black Rock Canyon": 1 / 3,
10+
"Cane Pole Ridge": 2 / 7,
11+
"Ember Lagoon": 1 / 3,
12+
"Forest": 1 / 3,
13+
"Highland Hills": 1 / 4,
14+
"Misty Forest": 1 / 3,
15+
"Mount Banon": 1 / 3,
16+
"Small Cave": 2 / 5,
17+
"Small Spring": 1 / 3,
18+
"Whispering Creek": 4 / 15,
19+
}
20+
21+
22+
def exp_vs_lem(drop_threshold: Optional[int] = None) -> None:
23+
explores = droprates.compile_drops(explore=True)
24+
lemonade = droprates.compile_drops(lemonade=True)
25+
for location in sorted(explores.locations.keys() | lemonade.locations.keys()):
26+
explore_loc = explores.locations[location]
27+
lemonade_loc = lemonade.locations[location]
28+
base_drop_rate = BASE_DROP_RATES[location]
29+
30+
if explore_loc.explores == 0:
31+
print(f"{location} NO EXPLORES")
32+
continue
33+
if lemonade_loc.lemonades == 0:
34+
print(f"{location} NO LEMONADES")
35+
continue
36+
37+
exp_vs_lem_rates = []
38+
adj_exp_vs_lem_rates = []
39+
40+
for item in sorted(explore_loc.items.keys() | lemonade_loc.items.keys()):
41+
if explore_loc.items[item].drops == 0:
42+
print(f"{location} {item} NO EXPLORE DROPS")
43+
continue
44+
if lemonade_loc.items[item].drops == 0:
45+
print(f"{location} {item} NO LEMONADE DROPS")
46+
continue
47+
if drop_threshold is not None and (
48+
explore_loc.items[item].drops < drop_threshold
49+
or lemonade_loc.items[item].drops < drop_threshold
50+
):
51+
print(f"{location} {item} NOT ENOUGH DROPS")
52+
continue
53+
54+
explore_drop_rate = explore_loc.items[item].drops / explore_loc.drops
55+
lemonade_drop_rate = lemonade_loc.items[item].drops / lemonade_loc.drops
56+
drops_per_explore = (
57+
explore_loc.items[item].drops / explore_loc.items[item].explores
58+
)
59+
adjusted_drops_per_explore = drops_per_explore / base_drop_rate
60+
exp_vs_lem_rate = explore_drop_rate / lemonade_drop_rate
61+
adj_exp_vs_lem_rate = adjusted_drops_per_explore / lemonade_drop_rate
62+
print(f"{location} {item} {exp_vs_lem_rate:.3f} {adj_exp_vs_lem_rate:.3f}")
63+
exp_vs_lem_rates.append(exp_vs_lem_rate)
64+
adj_exp_vs_lem_rates.append(adj_exp_vs_lem_rate)
65+
66+
if exp_vs_lem_rates and adj_exp_vs_lem_rates:
67+
print(
68+
f"{location} AVERAGE {statistics.mean(exp_vs_lem_rates):.3f} {statistics.mean(adj_exp_vs_lem_rates):.3f}"
69+
)
70+
71+
72+
if __name__ == "__main__":
73+
typer.run(exp_vs_lem)

py/fixtures.py

+19
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Iterable, Optional
55

66
import attr
7+
import attrs
78
from frozendict import frozendict
89

910
comment_re = re.compile(r"^//.*$")
@@ -17,6 +18,7 @@
1718
"fleaMarket": "flea_market",
1819
"growthTime": "growth_time",
1920
"firstSeen": "first_seen",
21+
"firstDropped": "first_dropped",
2022
}
2123

2224

@@ -51,13 +53,30 @@ class Item:
5153
event: bool = False
5254
growth_time: Optional[int] = None
5355
first_seen: Optional[int] = None
56+
first_dropped: Optional[int] = None
57+
last_dropped: Optional[int] = None
5458

5559

5660
def load_items() -> Iterable[Item]:
5761
for item in load_fixture("items"):
5862
yield Item(**{item_case_mappings.get(k, k): v for k, v in item.items()})
5963

6064

65+
@attrs.define(frozen=True)
66+
class Location:
67+
id: str
68+
type: str
69+
name: str
70+
items: tuple[str]
71+
72+
73+
def load_locations(type: Optional[str] = None) -> Iterable[Location]:
74+
for location in load_fixture("locations"):
75+
if type is not None and type != location["type"]:
76+
continue
77+
yield Location(**location)
78+
79+
6180
if __name__ == "__main__":
6281
items = [
6382
{item_case_mappings.get(k, k): v for k, v in item.items()}

py/parse_logs.py

+6
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ def parse_logs(log_type=None, since=int(os.environ.get("SINCE", 0))):
2424
if items and "item" not in items[0]:
2525
continue
2626

27+
if row["type"] == "cider" and "explores" not in row["results"]:
28+
# Backfill this.
29+
row["results"]["explores"] = (
30+
1250 if row["results"]["stamina"] >= 750 else 1000
31+
)
32+
2733
if row["ts"] not in all_logs:
2834
# Fix up the old zone/loc thing.
2935
results = row.get("results")

0 commit comments

Comments
 (0)