From 45811ad4ea81f9ee114fc1989dd9767cbaf75fdb Mon Sep 17 00:00:00 2001 From: beiju Date: Tue, 4 Mar 2025 01:23:18 -0800 Subject: [PATCH 1/6] Add formula for advancement on hits --- formulas.py | 18 ++++++++++++++++++ resim.py | 16 ++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/formulas.py b/formulas.py index d8242c2..84c27cb 100644 --- a/formulas.py +++ b/formulas.py @@ -637,3 +637,21 @@ def get_triple_threshold( return float("nan") opw_pow = pitcher_opw**1.5 return 0.042 + 0.2 * batter_gf - 0.056 * opw_pow - 0.05 * fielder_chase + 0.1 * ballpark_sum + +def get_advance_on_hit_threshold( + runner: PlayerData, + fielder: PlayerData, + pitching_team: TeamData, + stadium: StadiumData, + meta: StatRelevantData, +): + # no vibes + fielder_tenac = fielder.multiplied( + "chasiness", get_multiplier(fielder, pitching_team, "fielder", "chasiness", meta, stadium) + ) + + # No mods or vibes here -- not sure if I need to use .multiplied at all + runner_cont = runner.multiplied("continuation", 1.0) + + threshold = 0.7 - fielder_tenac + 0.6 * runner_cont + return max(min(threshold, 0.95), 0.01) \ No newline at end of file diff --git a/resim.py b/resim.py index 9bbd281..00354ff 100644 --- a/resim.py +++ b/resim.py @@ -36,7 +36,7 @@ get_fly_or_ground_threshold, get_out_threshold, get_double_threshold, - get_triple_threshold, + get_triple_threshold, get_advance_on_hit_threshold, ) from item_gen import ItemRollType, roll_item @@ -2212,17 +2212,25 @@ def handle_hit_advances(self, bases_hit, defender_roll): for runner_id, base, roll_outcome in calculate_advances( bases_before, bases_after, bases_hit, base_before_home + 1 ): + runner = self.data.get_player(runner_id) + fielder = self.get_fielder_for_roll(defender_roll) + + advance_threshold = get_advance_on_hit_threshold( + runner, + fielder, + self.pitching_team, + self.stadium, + self.get_stat_meta(), + ) # work around missing data in next_update if self.event["created"] == "2021-04-14T15:11:04.159Z": roll_outcome = False - roll = self.roll(f"adv ({base}, {roll_outcome}") - runner = self.data.get_player(runner_id) + roll = self.roll(f"adv ({base}, {roll_outcome}", passed=roll_outcome, threshold=advance_threshold) if runner.undefined(): self.roll("undefined (runner adv?)") pass - fielder = self.get_fielder_for_roll(defender_roll) if fielder.undefined(): self.roll("undefined (runner adv? from fielder)") From 005405ff3925a76aea479deed8e4f76b6e4c77f2 Mon Sep 17 00:00:00 2001 From: beiju Date: Tue, 4 Mar 2025 02:20:57 -0800 Subject: [PATCH 2/6] Actually use the correct attribute --- formulas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/formulas.py b/formulas.py index 84c27cb..18cbc6c 100644 --- a/formulas.py +++ b/formulas.py @@ -647,7 +647,7 @@ def get_advance_on_hit_threshold( ): # no vibes fielder_tenac = fielder.multiplied( - "chasiness", get_multiplier(fielder, pitching_team, "fielder", "chasiness", meta, stadium) + "tenaciousness", get_multiplier(fielder, pitching_team, "fielder", "tenaciousness", meta, stadium) ) # No mods or vibes here -- not sure if I need to use .multiplied at all From c396715d15240b5f7474d9b86b2253f2ca2fa372 Mon Sep 17 00:00:00 2001 From: beiju Date: Wed, 5 Mar 2025 23:39:32 -0800 Subject: [PATCH 3/6] Thresholds for mild pitch and party --- resim.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/resim.py b/resim.py index 00354ff..8fddcc6 100644 --- a/resim.py +++ b/resim.py @@ -1436,7 +1436,9 @@ def handle_batter_reverb(self): self.roll("psychic bug reverb") def handle_mild(self): - mild_roll = self.roll("mild") + threshold = 0.0005 + 0.004 * self.stadium.mysticism + + mild_roll = self.roll("mild", threshold=threshold, passed=self.ty == EventType.MILD_PITCH) if self.ty == EventType.MILD_PITCH: # skipping mild proc @@ -3766,16 +3768,22 @@ def handle_consumers(self): self.log_roll(Csv.CONSUMERS, "Miss", attack_roll, False, attacked_team=team) def handle_party(self): + party_threshold = 0.0055 if self.season < 20 else 0.00525 + if self.season == 23 and "SIM_PARTY_TIME" not in self.data.sim["attr"]: return if self.season != 16 or self.day >= 85: # lol. turns out it just rolls party all the time and throws out the roll if the team isn't partying - party_roll = self.roll("party time") + + # If this event is a Party we know this roll must have passed, but if it's not a Party we don't know + # anything, since there's a possibility that the roll still passed but the team wasn't in party time + passed = None + if self.ty == EventType.PARTY: + passed = True + party_roll = self.roll("party time", threshold=party_threshold, passed=passed) else: party_roll = 1 - party_threshold = 0.0055 if self.season < 20 else 0.00525 - if self.ty == EventType.PARTY: self.log_roll(Csv.PARTY, "Party", party_roll, True) team_roll = self.roll("target team") # <0.5 for home, >0.5 for away From b5412f043f67c2f390fa2f5df268b8b803484b82 Mon Sep 17 00:00:00 2001 From: beiju Date: Fri, 21 Aug 2026 20:07:56 -0700 Subject: [PATCH 4/6] Thresholds for ground outs --- formulas.py | 25 ++++++++++++++++++++++++- pyproject.toml | 9 +++++++++ resim.py | 25 +++++++++++++++++++------ 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/formulas.py b/formulas.py index 18cbc6c..cd68be3 100644 --- a/formulas.py +++ b/formulas.py @@ -654,4 +654,27 @@ def get_advance_on_hit_threshold( runner_cont = runner.multiplied("continuation", 1.0) threshold = 0.7 - fielder_tenac + 0.6 * runner_cont - return max(min(threshold, 0.95), 0.01) \ No newline at end of file + return max(min(threshold, 0.95), 0.01) + +def get_advance_on_ground_out_threshold( + runner: PlayerData, + fielder: PlayerData, + pitching_team: TeamData, + stadium: StadiumData, + meta: StatRelevantData, +): + runner_vibes = runner.vibes(meta.day) + fielder_vibes = fielder.vibes(meta.day) + + # no mods + runner_ind = runner.multiplied("indulgence", 1.0) * (1 + 0.2 * runner_vibes) + + fielder_tenac = fielder.multiplied( + "tenaciousness", get_multiplier(fielder, pitching_team, "fielder", "tenaciousness", meta, stadium) + ) * (1 + 0.2 * fielder_vibes) + + elong = stadium.elongation - 0.5 + incon = stadium.inconvenience - 0.5 + + return 0.5 + 0.35 * runner_ind - 0.1 * fielder_tenac - 0.1 * elong - 0.1 * incon + diff --git a/pyproject.toml b/pyproject.toml index 95f318f..1903117 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,12 @@ +[project] +name = "resim" +version = "0.0.1" +dependencies = [ + "dataclasses-json>=0.6.7", + "requests>=2.34.2", + "tqdm>=4.70.0", +] + [tool.black] line-length = 120 extend-exclude = '\.ipynb$' diff --git a/resim.py b/resim.py index 8fddcc6..e5307e2 100644 --- a/resim.py +++ b/resim.py @@ -36,7 +36,9 @@ get_fly_or_ground_threshold, get_out_threshold, get_double_threshold, - get_triple_threshold, get_advance_on_hit_threshold, + get_triple_threshold, + get_advance_on_hit_threshold, + get_advance_on_ground_out_threshold, ) from item_gen import ItemRollType, roll_item @@ -2038,9 +2040,16 @@ def did_advance(base, runner_id): is_next_free = True roll_outcome = did_advance(base, runner_id) + advance_threshold = get_advance_on_hit_threshold( + runner, + fielder, + self.pitching_team, + self.stadium, + self.get_stat_meta(), + ) if is_next_free: - adv_roll = self.roll(f"adv? {base}/{runner.name} ({roll_outcome})") + adv_roll = self.roll(f"adv? {base}/{runner.name} ({roll_outcome})", threshold=advance_threshold, passed=roll_outcome) self.log_roll( Csv.FLYOUT, f"advance_{base}", adv_roll, roll_outcome, fielder=fielder, relevant_runner=runner ) @@ -2168,7 +2177,11 @@ def did_advance(base, runner_id): roll_outcome = did_advance(base, runner_id) if not was_forced else None # needs... fielder tenaciousness and runner indulgence? - adv_roll = self.roll(f"adv? {base}/{runner.name} ({roll_outcome})") + adv_roll = self.roll( + f"adv? {base}/{runner.name} ({roll_outcome})", + threshold=get_advance_on_ground_out_threshold(runner, fielder, self.pitching_team, self.stadium, self.get_stat_meta()), + passed=roll_outcome, + ) if self.batter.undefined() and base == base_before_home: # sac? # self.roll("undefined (advance batter)") pass @@ -4088,7 +4101,7 @@ def handle_grind_rail(self): self.roll("trick 1 name") - m = re.search("They do a .*? \(([0-9]+)\)", self.desc) + m = re.search(r"They do a .*? \(([0-9]+)\)", self.desc) expected_score_1 = int(m.group(1)) pro_factor = 2 if "Pro Skater" in self.desc else 1 self.print(f"(press: {runner.pressurization}, cinn: {runner.cinnamon})") @@ -4126,7 +4139,7 @@ def handle_grind_rail(self): if "lose their balance and bail!" not in self.desc: self.roll("trick 2 name") - m = re.search("They(?: land|'re tagged out doing) a .*? \(([0-9]+)\)", self.desc) + m = re.search(r"They(?: land|'re tagged out doing) a .*? \(([0-9]+)\)", self.desc) expected_score_2 = int(m.group(1)) lo2 = runner.pressurization * 500 hi2 = runner.cinnamon * 3000 + 1000 @@ -4253,7 +4266,7 @@ def handle_steal(self): break def create_item(self, event, roll_type: ItemRollType, prev_event): - match = re.search("(?:gained|The Winner gets) (.+?)( and ditched| and dropped|\.?$)", self.desc) + match = re.search(r"(?:gained|The Winner gets) (.+?)( and ditched| and dropped|\.?$)", self.desc) expected_item_name = match.group(1) if match else "" if roll_type == ItemRollType.PRIZE: From bec3b634ab85a5f7ea41b33e9a68b9ab99a0c5ea Mon Sep 17 00:00:00 2001 From: beiju Date: Fri, 21 Aug 2026 21:19:05 -0700 Subject: [PATCH 5/6] Fix threshold for flyouts and add it for double plays --- formulas.py | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++ resim.py | 34 ++++++++++++++++------------ 2 files changed, 85 insertions(+), 14 deletions(-) diff --git a/formulas.py b/formulas.py index cd68be3..e54df7f 100644 --- a/formulas.py +++ b/formulas.py @@ -638,6 +638,41 @@ def get_triple_threshold( opw_pow = pitcher_opw**1.5 return 0.042 + 0.2 * batter_gf - 0.056 * opw_pow - 0.05 * fielder_chase + 0.1 * ballpark_sum +def get_double_play_threshold( + batter: PlayerData, + pitcher: PlayerData, + fielder: PlayerData, + batting_team: TeamData, + pitching_team: TeamData, + stadium: StadiumData, + meta: StatRelevantData, +): + pitcher_vibes = pitcher.vibes(meta.day) + fielder_vibes = fielder.vibes(meta.day) + + hype = stadium.hype * (1 if meta.top_of_inning else -1) + + # no vibes, yes multipliers + batter_trag = batter.multiplied( + "tragicness", get_multiplier(batter, batting_team, "batter", "tragicness", meta, stadium) + ) + inv_batter_trag = 1 - batter_trag + + pitcher_shakes = pitcher.multiplied( + "shakespearianism", 1 / get_multiplier(pitcher, pitching_team, "pitcher", "shakespearianism", meta, stadium) + ) + pitcher_shakes = (pitcher_shakes + 0.2 * hype) * (1 + 0.2 * pitcher_vibes) + + fielder_tenac = fielder.multiplied( + "tenaciousness", get_multiplier(fielder, pitching_team, "fielder", "tenaciousness", meta, stadium) + ) + fielder_tenac = (fielder_tenac + 0.2 * hype) * (1 + 0.2 * fielder_vibes) + + elong = stadium.elongation - 0.5 + + return -0.05 + 0.4 * pitcher_shakes - 0.18 * inv_batter_trag + 0.1 * fielder_tenac - 0.16 * elong + + def get_advance_on_hit_threshold( runner: PlayerData, fielder: PlayerData, @@ -678,3 +713,33 @@ def get_advance_on_ground_out_threshold( return 0.5 + 0.35 * runner_ind - 0.1 * fielder_tenac - 0.1 * elong - 0.1 * incon + +def get_advance_on_flyout_threshold( + advancing_from: int, + runner: PlayerData, + batting_team: TeamData, + stadium: StadiumData, + meta: StatRelevantData, +): + runner_vibes = runner.vibes(meta.day) + + runner_ind = runner.multiplied( + "indulgence", get_multiplier(runner, batting_team, "batter", "indulgence", meta, stadium) + ) + runner_ind = runner_ind * (1 + 0.2 * runner_vibes) + runner_ind_sq = runner_ind ** 2 + runner_ind_p4 = runner_ind ** 4 + + elong = stadium.elongation - 0.5 + inconv = stadium.inconvenience - 0.5 + park_factor = 0.10 * elong + 0.10 * inconv + + if advancing_from == 0: + return -0.085 + 0.36 * runner_ind - 0.38 * runner_ind_sq + 0.24 * runner_ind_p4 - park_factor + elif advancing_from == 1: + return 0.045 + 0.065 * runner_ind + 0.30 * runner_ind_sq - park_factor + elif advancing_from == 2: + return 0.45 + 0.35 * runner_ind - park_factor + else: + raise RuntimeError(f"Unexpected advancing-from base {advancing_from} (expected 0, 1, or 2).") + diff --git a/resim.py b/resim.py index e5307e2..c7bf3b7 100644 --- a/resim.py +++ b/resim.py @@ -39,6 +39,8 @@ get_triple_threshold, get_advance_on_hit_threshold, get_advance_on_ground_out_threshold, + get_double_play_threshold, + get_advance_on_flyout_threshold, ) from item_gen import ItemRollType, roll_item @@ -2040,10 +2042,10 @@ def did_advance(base, runner_id): is_next_free = True roll_outcome = did_advance(base, runner_id) - advance_threshold = get_advance_on_hit_threshold( + advance_threshold = get_advance_on_flyout_threshold( + base, runner, - fielder, - self.pitching_team, + self.batting_team, self.stadium, self.get_stat_meta(), ) @@ -2070,7 +2072,13 @@ def did_advance(base, runner_id): elif self.ty == EventType.GROUND_OUT: if len(self.update["basesOccupied"]) > 0: # roll needs batter tragicness, fielder tenaciousness, pitcher shakespearianism - dp_roll = self.roll("dp?") + dp_threshold = get_double_play_threshold( + self.batter, self.pitcher, fielder, self.batting_team, self.pitching_team, self.stadium, self.get_stat_meta() + ) + + is_dp = "into a double play!" in self.desc + + dp_roll = self.roll("dp?", threshold=dp_threshold, passed=is_dp) if self.batter.undefined(): # tragicness? # the control flow is really weird here @@ -2082,8 +2090,6 @@ def did_advance(base, runner_id): if fielder.undefined(): self.roll("undefined (dp fielder)") pass - - is_dp = "into a double play!" in self.desc is_fc = "on fielder's choice" in self.desc self.log_roll(Csv.GROUNDOUT_FORMULAS, "DP", dp_roll, is_dp, fielder=fielder) @@ -4204,18 +4210,18 @@ def handle_steal(self): ): runner = self.data.get_player(self.update["baseRunners"][i]) - steal_roll = self.roll(f"steal ({base})") + was_success = self.ty == EventType.STOLEN_BASE and ( + base + 1 == base_stolen + or base_stolen == Base.FIFTH + and bool(filter(lambda i: i.name == "The Fifth Base", runner.items)) + ) + + steal_roll = self.roll(f"steal ({base})", passed=was_success) if steal_fielder.undefined(): self.roll("undefined (fielder steal)") if runner.undefined(): self.roll(f"undefined (runner steal {base})") self.roll(f"undefined (runner steal {base})") - - was_success = self.ty == EventType.STOLEN_BASE and ( - base + 1 == base_stolen - or base_stolen == Base.FIFTH - and filter(lambda i: i.name == "The Fifth Base", runner.items) - ) self.log_roll( Csv.STEAL_ATTEMPT, f"StealAttempt{base}", @@ -4233,9 +4239,9 @@ def handle_steal(self): if runner.undefined(): self.roll("undefined (steal success runner)") - success_roll = self.roll("steal success") was_caught = "caught stealing" in self.desc + success_roll = self.roll("steal success", passed=not was_caught) self.log_roll( Csv.STEAL_SUCCESS, f"StealSuccess{base}", From 169827192abecc69551556621b3e4378ed9d7868 Mon Sep 17 00:00:00 2001 From: beiju Date: Fri, 21 Aug 2026 22:03:50 -0700 Subject: [PATCH 6/6] Fix misplaced inversion. I think --- formulas.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/formulas.py b/formulas.py index e54df7f..8d48a95 100644 --- a/formulas.py +++ b/formulas.py @@ -654,12 +654,12 @@ def get_double_play_threshold( # no vibes, yes multipliers batter_trag = batter.multiplied( - "tragicness", get_multiplier(batter, batting_team, "batter", "tragicness", meta, stadium) + "tragicness", 1 / get_multiplier(batter, batting_team, "batter", "tragicness", meta, stadium) ) inv_batter_trag = 1 - batter_trag pitcher_shakes = pitcher.multiplied( - "shakespearianism", 1 / get_multiplier(pitcher, pitching_team, "pitcher", "shakespearianism", meta, stadium) + "shakespearianism", get_multiplier(pitcher, pitching_team, "pitcher", "shakespearianism", meta, stadium) ) pitcher_shakes = (pitcher_shakes + 0.2 * hype) * (1 + 0.2 * pitcher_vibes)