From a72abc01a3de81507a35197b19ef9d1f49166192 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Wed, 25 Feb 2026 16:21:26 -0600 Subject: [PATCH] Add FullCard/CardColumn/CardResult models and card builder pipeline - card_layout.py: Port PlayResult, PLAY_RESULTS, EXACT_CHANCES, get_chances(), CardResult, CardColumn, FullCard, FullBattingCard, FullPitchingCard from database/app/card_creation.py. card_output() uses col_* key names. get_chances() always returns Decimal to avoid float/Decimal type errors. - batters/card_builder.py: Port get_batter_card_data() algorithm as build_batter_full_cards(ratings_vl, ratings_vr, offense_col, player_id, hand). assign_bchances() returns float tuples for compatibility with float-based BattingCardRatingsModel fields. - pitchers/card_builder.py: Port get_pitcher_card_data() algorithm as build_pitcher_full_cards(). assign_pchances() returns float tuples. Includes card.add_fatigue() at end of each card iteration. - batters/calcs_batter.py: Integrate card builder in get_batter_ratings(). After computing raw ratings, call build_batter_full_cards() and merge 9 col_* rendered column fields into each ratings dict. Lazy import to avoid circular dependency. - pitchers/calcs_pitcher.py: Same integration for get_pitcher_ratings(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- batters/calcs_batter.py | 15 +- batters/card_builder.py | 802 +++++++++++++++++++++++++++++ card_layout.py | 1015 +++++++++++++++++++++++++++++++++++++ pitchers/calcs_pitcher.py | 15 +- pitchers/card_builder.py | 712 ++++++++++++++++++++++++++ 5 files changed, 2557 insertions(+), 2 deletions(-) create mode 100644 batters/card_builder.py create mode 100644 card_layout.py create mode 100644 pitchers/card_builder.py diff --git a/batters/calcs_batter.py b/batters/calcs_batter.py index 0a7b928..99029c7 100644 --- a/batters/calcs_batter.py +++ b/batters/calcs_batter.py @@ -622,4 +622,17 @@ def get_batter_ratings(df_data) -> List[dict]: else: logger.debug(f'total chances: {vr_total_chances}') - return [vl.custom_to_dict(), vr.custom_to_dict()] + vl_dict = vl.custom_to_dict() + vr_dict = vr.custom_to_dict() + + try: + from batters.card_builder import build_batter_full_cards + vl_card, vr_card = build_batter_full_cards( + vl, vr, int(df_data['offense_col']), int(df_data['player_id']), df_data['bat_hand'] + ) + vl_dict.update(vl_card.card_output()) + vr_dict.update(vr_card.card_output()) + except Exception as e: + logger.warning(f'Card layout builder failed for {df_data.name}: {e}') + + return [vl_dict, vr_dict] diff --git a/batters/card_builder.py b/batters/card_builder.py new file mode 100644 index 0000000..82109f8 --- /dev/null +++ b/batters/card_builder.py @@ -0,0 +1,802 @@ +""" +Batter card-building algorithm, ported from database/app/card_creation.py (~lines 1357-2226). + +Converts BattingCardRatingsModel instances (vL and vR) into FullBattingCard objects +that represent the physical card layout. +""" +import copy +import math +import logging +from decimal import Decimal + +from card_layout import FullBattingCard, PLAY_RESULTS, PlayResult, EXACT_CHANCES, get_chances +from batters.calcs_batter import BattingCardRatingsModel + +logger = logging.getLogger(__name__) + + +def build_batter_full_cards( + ratings_vl: BattingCardRatingsModel, + ratings_vr: BattingCardRatingsModel, + offense_col: int, + player_id: int, + hand: str, # player's batting hand: 'R', 'L', or 'S' +) -> tuple: + """Build vL and vR FullBattingCard objects from pre-calculated ratings. + + Returns (vl_card, vr_card). + """ + player_binary = player_id % 2 + + vl = FullBattingCard(offense_col=offense_col, alt_direction=player_binary) + vr = FullBattingCard(offense_col=offense_col, alt_direction=player_binary) + + def assign_bchances(this_card, play, chances, secondary_play=None): + r_data = this_card.add_result(play, chances, secondary_play) + if r_data: + return float(r_data[0]), float(r_data[1]) + else: + for x in EXACT_CHANCES: + if x < math.floor(chances): + r_data = this_card.add_result(play, Decimal(math.floor(chances)), secondary_play) + if r_data: + return float(r_data[0]), float(r_data[1]) + break + if x < chances: + r_data = this_card.add_result(play, x, secondary_play) + if r_data: + return float(r_data[0]), float(r_data[1]) + return 0, 0 + + def get_pullside_of(vs_hand): + if hand == 'L': + return 'rf' + elif hand == 'R': + return 'lf' + elif vs_hand == 'L': + return 'lf' + else: + return 'rf' + + def get_preferred_mif(ratings): + if hand == 'L' and ratings.slap_rate > .24: + return 'ss' + elif hand == 'L' or (hand == 'R' and ratings.slap_rate > .24): + return '2b' + else: + return 'ss' + + for card, data, vs_hand in [ + (vl, copy.deepcopy(ratings_vl), 'L'), + (vr, copy.deepcopy(ratings_vr), 'R'), + ]: + logger.info(f'\n\nBeginning v{vs_hand}') + + new_ratings = BattingCardRatingsModel( + battingcard_id=data.battingcard_id, + bat_hand=data.bat_hand, + vs_hand=vs_hand, + hard_rate=data.hard_rate, + med_rate=data.med_rate, + soft_rate=data.soft_rate, + pull_rate=data.pull_rate, + center_rate=data.center_rate, + slap_rate=data.slap_rate, + ) + pull_of = get_pullside_of(vs_hand) + pref_mif = get_preferred_mif(data) + + # BP Homerun + res_chances = data.bp_homerun + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_bchances(card, PLAY_RESULTS['bp-hr'], ch) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + res_chances -= r_val[0] + new_ratings.bp_homerun += r_val[0] + + # HBP + retries = 0 + res_chances = data.hbp + while res_chances > 0: + if res_chances < 1 or retries > 0: + break + ch = get_chances(res_chances) + r_val = assign_bchances(card, PlayResult(full_name='HBP', short_name='HBP'), ch) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + res_chances -= r_val[0] + new_ratings.hbp += r_val[0] + if r_val[0] == 0: + retries += 1 + + # Homerun + retries = 0 + res_chances = data.homerun + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.double_pull > 0: + data.double_pull += res_chances + elif data.double_two > 0: + data.double_two += res_chances + elif data.triple > 0: + data.triple += res_chances + elif data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + break + + ch = get_chances(res_chances) + if data.double_pull > (data.flyout_rf_b + data.flyout_lf_b) and data.double_pull > max(1 - ch, 0): + secondary = PLAY_RESULTS[f'do-{pull_of}'] + elif data.flyout_lf_b > data.flyout_rf_b and data.flyout_lf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-lf'] + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-rf'] + elif data.double_pull > max(1 - ch, 0): + secondary = PLAY_RESULTS[f'do-{pull_of}'] + elif data.double_three > max(1 - ch, 0): + secondary = PLAY_RESULTS['do***'] + elif data.double_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['do**'] + elif data.triple > max(1 - ch, 0): + secondary = PLAY_RESULTS['tr'] + else: + secondary = None + + r_val = assign_bchances(card, PLAY_RESULTS['hr'], ch, secondary) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + res_chances -= r_val[0] + new_ratings.homerun += r_val[0] + if r_val[1] > 0: + if secondary.short_name[:4] == 'DO (': + data.double_pull -= r_val[1] + new_ratings.double_pull += r_val[1] + elif 'lf' in secondary.short_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'rf' in secondary.short_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif '***' in secondary.short_name: + data.double_three -= r_val[1] + new_ratings.double_three += r_val[1] + elif '**' in secondary.short_name: + data.double_two -= r_val[1] + new_ratings.double_two += r_val[1] + elif 'TR' in secondary.short_name: + data.triple -= r_val[1] + new_ratings.triple += r_val[1] + + if r_val[0] == 0: + retries += 1 + + # Triple + retries = 0 + res_chances = data.triple + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.double_pull > 0: + data.double_pull += res_chances + elif data.double_two > 0: + data.double_two += res_chances + elif data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + break + + ch = get_chances(res_chances) + if data.single_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['si**'] + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-lf'] + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-rf'] + elif data.double_pull > max(1 - ch, 0): + secondary = PLAY_RESULTS[f'do-{pull_of}'] + elif data.double_three > max(1 - ch, 0): + secondary = PLAY_RESULTS['do***'] + elif data.double_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['do**'] + else: + secondary = None + + r_val = assign_bchances(card, PLAY_RESULTS['tr'], ch, secondary) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + res_chances -= r_val[0] + new_ratings.triple += r_val[0] + if r_val[1] > 0: + if 'DO (' in secondary.short_name: + data.double_pull -= r_val[1] + new_ratings.double_pull += r_val[1] + elif 'lf' in secondary.short_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'rf' in secondary.short_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif '***' in secondary.short_name: + data.double_three -= r_val[1] + new_ratings.double_three += r_val[1] + elif 'SI' in secondary.short_name: + data.single_two -= r_val[1] + new_ratings.single_two += r_val[1] + elif '**' in secondary.short_name: + data.double_two -= r_val[1] + new_ratings.double_two += r_val[1] + + if r_val[0] == 0: + retries += 1 + + # Double*** + retries = 0 + res_chances = data.double_three + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.double_pull > 0: + data.double_pull += res_chances + elif data.double_two > 0: + data.double_two += res_chances + elif data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + break + + ch = get_chances(res_chances) + if data.single_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['si**'] + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-lf'] + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-rf'] + elif data.double_pull > max(1 - ch, 0): + secondary = PLAY_RESULTS[f'do-{pull_of}'] + elif data.double_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['do**'] + else: + secondary = None + + r_val = assign_bchances(card, PLAY_RESULTS['do***'], ch, secondary) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + res_chances -= r_val[0] + new_ratings.double_three += r_val[0] + if r_val[1] > 0: + if 'DO (' in secondary.short_name: + data.double_pull -= r_val[1] + new_ratings.double_pull += r_val[1] + elif 'lf' in secondary.short_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'rf' in secondary.short_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif 'SI' in secondary.short_name: + data.single_two -= r_val[1] + new_ratings.single_two += r_val[1] + elif '**' in secondary.short_name: + data.double_two -= r_val[1] + new_ratings.double_two += r_val[1] + + if r_val[0] == 0: + retries += 1 + + # Double pull-side + retries = 0 + res_chances = data.double_pull + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.double_two > 0: + data.double_two += res_chances + elif data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + break + + ch = get_chances(res_chances) + if data.flyout_lf_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'fly (lf) B', short_name=f'fly B') + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'fly (rf) B', short_name=f'fly b') + elif data.single_one > max(1 - ch, 0): + secondary = PLAY_RESULTS['si*'] + elif data.single_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['si**'] + else: + secondary = None + + r_val = assign_bchances(card, PLAY_RESULTS[f'do-{pull_of}'], ch, secondary) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + res_chances -= r_val[0] + new_ratings.double_pull += r_val[0] + if r_val[1] > 0: + if 'lf' in secondary.full_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'rf' in secondary.full_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif '***' in secondary.short_name: + data.double_three -= r_val[1] + new_ratings.double_three += r_val[1] + elif 'SI' in secondary.short_name: + data.single_two -= r_val[1] + new_ratings.single_two += r_val[1] + elif '**' in secondary.short_name: + data.double_two -= r_val[1] + new_ratings.double_two += r_val[1] + + if r_val[0] == 0: + retries += 1 + + # Double** + retries = 0 + res_chances = data.double_two + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + elif data.walk > 0: + data.walk += res_chances + break + + ch = get_chances(res_chances) + if data.single_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['si**'] + elif data.single_center > max(1 - ch, 0): + secondary = PLAY_RESULTS['si-cf'] + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-lf'] + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-rf'] + else: + secondary = None + + r_val = assign_bchances(card, PLAY_RESULTS['do**'], ch, secondary) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + res_chances -= r_val[0] + new_ratings.double_two += r_val[0] + if r_val[1] > 0: + if 'lf' in secondary.full_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'rf' in secondary.full_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif 'SI' in secondary.short_name: + data.single_two -= r_val[1] + new_ratings.single_two += r_val[1] + + if r_val[0] == 0: + retries += 1 + + # Single** + retries = 0 + res_chances = data.single_two + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + elif data.walk > 0: + data.walk += res_chances + break + + ch = get_chances(res_chances) + if data.groundout_a > max(1 - ch, 0): + secondary = PlayResult(full_name=f'gb ({pref_mif}) A', short_name=f'gb ({pref_mif}) A') + elif data.groundout_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'gb ({pref_mif}) B', short_name=f'gb ({pref_mif}) B') + elif data.groundout_c > max(1 - ch, 0): + secondary = PlayResult(full_name=f'gb ({pref_mif}) C', short_name=f'gb ({pref_mif}) C') + elif data.lineout > max(1 - ch, 0): + secondary = PlayResult(full_name=f'lo ({pref_mif})', short_name=f'lo ({pref_mif})') + else: + secondary = None + + r_val = assign_bchances(card, PLAY_RESULTS['si**'], ch, secondary) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + res_chances -= r_val[0] + new_ratings.single_two += r_val[0] + if r_val[1] > 0: + if 'C' in secondary.short_name: + data.groundout_c -= r_val[1] + new_ratings.groundout_c += r_val[1] + elif 'B' in secondary.short_name: + data.groundout_b -= r_val[1] + new_ratings.groundout_b += r_val[1] + elif 'A' in secondary.short_name: + data.groundout_a -= r_val[1] + new_ratings.groundout_a += r_val[1] + elif 'lo' in secondary.short_name: + data.lineout -= r_val[1] + new_ratings.lineout += r_val[1] + + if r_val[0] == 0: + retries += 1 + + # Single (cf) + retries = 0 + res_chances = data.single_center + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.single_one > 0: + data.single_one += res_chances + elif data.walk > 0: + data.walk += res_chances + break + + ch = get_chances(res_chances) + if data.flyout_bq > max(1 - ch, 0): + secondary = PlayResult(full_name=f'fly B?', short_name=f'fly B?') + elif data.flyout_lf_b > max(1 - ch, 0) and data.flyout_lf_b > data.flyout_rf_b: + secondary = PlayResult(full_name=f'fly (LF) B', short_name=f'fly B') + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'fly (RF) B', short_name=f'fly B') + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'fly (LF) B', short_name=f'fly B') + elif data.lineout > max(1 - ch, 0): + secondary = PlayResult(full_name=f'lo ({pref_mif})', short_name=f'lo ({pref_mif})') + else: + secondary = None + + r_val = assign_bchances(card, PLAY_RESULTS['si-cf'], ch, secondary) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + res_chances -= r_val[0] + new_ratings.single_center += r_val[0] + if r_val[1] > 0: + if '?' in secondary.short_name: + data.flyout_bq -= r_val[1] + new_ratings.flyout_bq += r_val[1] + elif 'LF' in secondary.full_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'RF' in secondary.full_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif 'lo' in secondary.short_name: + data.lineout -= r_val[1] + new_ratings.lineout += r_val[1] + + if r_val[0] == 0: + retries += 1 + + # Single* + retries = 0 + res_chances = data.single_one + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.walk > 0: + data.walk += res_chances + break + + ch = get_chances(res_chances) + if data.groundout_c > max(1 - ch, 0): + secondary = PlayResult(full_name=f'gb ({pref_mif}) C', short_name=f'gb ({pref_mif}) C') + elif data.groundout_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'gb ({pref_mif}) B', short_name=f'gb ({pref_mif}) B') + elif data.groundout_a > max(1 - ch, 0): + secondary = PlayResult(full_name=f'gb ({pref_mif}) A', short_name=f'gb ({pref_mif}) A') + elif data.lineout > max(1 - ch, 0): + secondary = PlayResult(full_name=f'lo ({pref_mif})', short_name=f'lo ({pref_mif})') + else: + secondary = None + + r_val = assign_bchances(card, PLAY_RESULTS['si*'], ch, secondary) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + res_chances -= r_val[0] + new_ratings.single_one += r_val[0] + if r_val[1] > 0: + if 'C' in secondary.short_name: + data.groundout_c -= r_val[1] + new_ratings.groundout_c += r_val[1] + elif 'B' in secondary.short_name: + data.groundout_b -= r_val[1] + new_ratings.groundout_b += r_val[1] + elif 'A' in secondary.short_name: + data.groundout_a -= r_val[1] + new_ratings.groundout_a += r_val[1] + elif 'lo' in secondary.short_name: + data.lineout -= r_val[1] + new_ratings.lineout += r_val[1] + + if r_val[0] == 0: + retries += 1 + + # Walk + retries = 0 + res_chances = data.walk + while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + + ch = get_chances(res_chances) + if data.strikeout > max(1 - ch, 0): + secondary = PlayResult(full_name=f'strikeout', short_name=f'so') + else: + secondary = None + + r_val = assign_bchances(card, PLAY_RESULTS['walk'], ch, secondary) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + res_chances -= r_val[0] + new_ratings.walk += r_val[0] + if r_val[1] > 0: + data.strikeout -= r_val[1] + new_ratings.strikeout += r_val[1] + + if r_val[0] == 0: + retries += 1 + + # BP Single + retries = 0 + res_chances = data.bp_single + while res_chances > 0: + if res_chances < 1 or retries > 0: + break + + ch = get_chances(res_chances) + r_val = assign_bchances(card, PLAY_RESULTS['bp-si'], ch) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + res_chances -= r_val[0] + new_ratings.bp_single += r_val[0] + + if r_val[0] == 0: + retries += 1 + + # Special lomax result + r_val = assign_bchances( + card, PlayResult(full_name=f'lo ({pref_mif}) max', short_name=f'lo ({pref_mif}) max'), Decimal(1)) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + data.lineout -= r_val[0] + new_ratings.lineout += r_val[0] + + # Popout + retries = 0 + res_chances = data.popout + while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + + ch = get_chances(res_chances) + this_if = '2b' if pref_mif == 'ss' else 'ss' + r_val = assign_bchances( + card, + PlayResult(full_name=f'popout ({this_if})', short_name=f'popout ({this_if})'), + Decimal(math.floor(ch)) + ) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if r_val[0] == 0: + data.lineout += res_chances + break + else: + res_chances -= r_val[0] + new_ratings.popout += r_val[0] + + # Flyout A + retries = 0 + res_chances = data.flyout_a + while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + + ch = get_chances(res_chances) + r_val = assign_bchances( + card, PlayResult(full_name=f'fly (cf) A', short_name=f'fly (cf) A'), Decimal(math.floor(ch))) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if r_val[0] == 0: + data.strikeout += res_chances if data.strikeout > 2 else 0 + break + else: + res_chances -= r_val[0] + new_ratings.flyout_a += r_val[0] + + # Flyout LF B + retries = 0 + res_chances = data.flyout_lf_b + while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + + ch = get_chances(res_chances) + r_val = assign_bchances( + card, PlayResult(full_name=f'fly (lf) B', short_name=f'fly (lf) B'), Decimal(math.floor(ch))) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if r_val[0] == 0: + data.strikeout += res_chances if data.strikeout > 2 else 0 + break + else: + res_chances -= r_val[0] + new_ratings.flyout_lf_b += r_val[0] + + # Flyout RF B + retries = 0 + res_chances = data.flyout_rf_b + while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + + ch = get_chances(res_chances) + r_val = assign_bchances( + card, PlayResult(full_name=f'fly (rf) B', short_name=f'fly (rf) B'), Decimal(math.floor(ch))) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if r_val[0] == 0: + data.strikeout += res_chances if data.strikeout > 2 else 0 + break + else: + res_chances -= r_val[0] + new_ratings.flyout_rf_b += r_val[0] + + # Groundout A + count_gb = 0 + + def get_gb_if(): + if count_gb % 4 == 1: + return pref_mif + elif count_gb % 4 == 2: + return '2b' if pref_mif == 'ss' else 'ss' + elif count_gb % 4 == 3: + return '1b' if pref_mif == '2b' else 'p' + else: + return '3b' if pref_mif == 'ss' else 'p' + + retries = 0 + res_chances = data.groundout_a + while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + + count_gb += 1 + this_if = get_gb_if() + ch = get_chances(res_chances) + r_val = assign_bchances( + card, PlayResult(full_name=f'gb ({this_if}) A', short_name=f'gb ({this_if}) A'), + Decimal(math.floor(ch))) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if r_val[0] == 0: + data.groundout_b += res_chances + break + else: + res_chances -= r_val[0] + new_ratings.groundout_a += r_val[0] + + # Groundout B + retries = 0 + res_chances = data.groundout_b + while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + + count_gb += 1 + this_if = get_gb_if() + ch = get_chances(res_chances) + r_val = assign_bchances( + card, PlayResult(full_name=f'gb ({this_if}) B', short_name=f'gb ({this_if}) B'), + Decimal(math.floor(ch))) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if r_val[0] == 0: + data.groundout_c += res_chances + break + else: + res_chances -= r_val[0] + new_ratings.groundout_b += r_val[0] + + # Groundout C + retries = 0 + res_chances = data.groundout_c + while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + + count_gb += 1 + this_if = get_gb_if() + ch = get_chances(res_chances) + r_val = assign_bchances( + card, PlayResult(full_name=f'gb ({this_if}) C', short_name=f'gb ({this_if}) C'), + Decimal(math.floor(ch))) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if r_val[0] == 0: + data.strikeout += res_chances + break + else: + res_chances -= r_val[0] + new_ratings.groundout_c += r_val[0] + + # Lineout + retries = 0 + res_chances = data.lineout + while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + + ch = get_chances(res_chances) + this_if = '3b' if pref_mif == 'ss' else '1b' + r_val = assign_bchances( + card, + PlayResult(full_name=f'lineout ({this_if})', short_name=f'lineout ({this_if})'), + Decimal(math.floor(ch)) + ) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if r_val[0] == 0: + break + else: + res_chances -= r_val[0] + new_ratings.lineout += r_val[0] + + # Strikeout + retries = 0 + res_chances = data.strikeout + while res_chances >= 1: + if res_chances < 1 or retries > 0: + break + + ch = get_chances(res_chances) + r_val = assign_bchances( + card, PlayResult(full_name=f'strikeout', short_name=f'strikeout'), Decimal(math.floor(ch))) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if r_val[0] == 0: + break + else: + res_chances -= r_val[0] + new_ratings.strikeout += r_val[0] + + # Filler loop — fill any remaining empty card slots + plays = sorted( + [(data.strikeout, 'so'), (data.lineout, 'lo'), (data.groundout_c, 'gb'), (data.popout, 'po')], + key=lambda z: z[0], + reverse=True + ) + count_filler = -1 + while not card.is_complete(): + count_filler += 1 + this_play = plays[count_filler % 4] + if this_play[1] == 'so': + play_res = PlayResult(full_name=f'strikeout', short_name=f'strikeout') + elif this_play[1] == 'lo': + this_if = '3b' if pref_mif == 'ss' else '1b' + play_res = PlayResult(full_name=f'lineout ({this_if})', short_name=f'lineout ({this_if})') + elif this_play[1] == 'gb': + count_gb += 1 + this_if = get_gb_if() + play_res = PlayResult(full_name=f'gb ({this_if}) C', short_name=f'gb ({this_if}) C') + else: + play_res = PlayResult(full_name=f'popout (c)', short_name=f'popout (c)') + + logger.debug(f'Send Card Fill\n{play_res}') + r_raw = card.card_fill(play_res) + r_val = (float(r_raw[0]), float(r_raw[1])) + logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n') + + if this_play[1] == 'so': + new_ratings.strikeout += r_val[0] + elif this_play[1] == 'lo': + new_ratings.lineout += r_val[0] + elif this_play[1] == 'gb': + new_ratings.groundout_c += r_val[0] + else: + new_ratings.popout += r_val[0] + + new_ratings.calculate_rate_stats() + + return vl, vr diff --git a/card_layout.py b/card_layout.py new file mode 100644 index 0000000..863a8c0 --- /dev/null +++ b/card_layout.py @@ -0,0 +1,1015 @@ +""" +Card layout models: PlayResult, CardResult, CardColumn, FullCard, FullBattingCard, FullPitchingCard. + +Adapted from database/app/card_creation.py for use in the card-creation pipeline. +These models represent the actual card layout (three 2d6 columns with text results) +as opposed to the raw rating chances stored in BattingCardRatingsModel / PitchingCardRatingsModel. +""" +import logging +import math +import re + +import pydantic + +from decimal import Decimal +from pydantic import validator +from typing import Optional + + +EXACT_CHANCES = [ + Decimal('5.7'), Decimal('5.4'), Decimal('5.1'), Decimal('4.8'), Decimal('4.75'), Decimal('4.5'), Decimal('4.25'), + Decimal('4.2'), Decimal('3.9'), Decimal('3.8'), Decimal('3.75'), Decimal('3.6'), Decimal('3.5'), Decimal('3.4'), + Decimal('3.3'), Decimal('3.25'), Decimal('3.2'), Decimal('2.85'), Decimal('2.8'), Decimal('2.75'), Decimal('2.7'), + Decimal('2.6'), Decimal('2.55'), Decimal('2.5'), Decimal('2.4'), Decimal('2.25'), Decimal('2.2'), Decimal('2.1'), + Decimal('1.95'), Decimal('1.9'), Decimal('1.8'), Decimal('1.75'), Decimal('1.7'), Decimal('1.65'), Decimal('1.6'), + Decimal('1.5'), Decimal('1.4'), Decimal('1.35'), Decimal('1.3'), Decimal('1.25'), Decimal('1.2'), Decimal('1.1'), + Decimal('1.05') +] + + +class PlayResult(pydantic.BaseModel): + full_name: str + short_name: str + is_offense: bool = True + + @validator("is_offense", always=True) + def offense_validator(cls, v, values, **kwargs): + return values['short_name'][:2] in ['HR', 'TR', 'DO', 'SI', 'WA', 'HB', '◆B', '▼B'] + + +PLAY_RESULTS = { + 'hr': PlayResult(full_name='HOMERUN', short_name='HR'), + 'bp-hr': PlayResult(full_name='◆BP-HR', short_name='◆BP-HR'), + 'tr': PlayResult(full_name='TRIPLE', short_name='TR'), + 'do-lf': PlayResult(full_name=f'DOUBLE (lf)', short_name=f'DO (lf)'), + 'do-cf': PlayResult(full_name=f'DOUBLE (cf)', short_name=f'DO (cf)'), + 'do-rf': PlayResult(full_name=f'DOUBLE (rf)', short_name=f'DO (rf)'), + 'do***': PlayResult(full_name=f'DOUBLE***', short_name=f'DO***'), + 'do**': PlayResult(full_name=f'DOUBLE**', short_name=f'DO**'), + 'si**': PlayResult(full_name='SINGLE**', short_name='SI**'), + 'si*': PlayResult(full_name='SINGLE*', short_name='SI*'), + 'si-cf': PlayResult(full_name='SINGLE (cf)', short_name='SI (cf)'), + 'bp-si': PlayResult(full_name='▼BP-SI', short_name='▼BP-SI'), + 'walk': PlayResult(full_name='WALK', short_name='WALK'), + 'fly-rf': PlayResult(full_name=f'fly (rf) B', short_name=f'fly (rf) B'), + 'fly-lf': PlayResult(full_name=f'fly (lf) B', short_name=f'fly (lf) B'), + 'fly-cf': PlayResult(full_name=f'fly (cf) B', short_name=f'fly (cf) B'), + 'fly-bq': PlayResult(full_name=f'fly B?', short_name=f'fly B?') +} + + +def get_chances(total_chances, apply_limits=True) -> Decimal: + """Convert a raw chance value to a Decimal suitable for card slot assignment.""" + if total_chances > 12.5 and apply_limits: + return Decimal(6) + elif total_chances > 10.5 and apply_limits: + return Decimal(5) + elif total_chances > 8.5 and apply_limits: + return Decimal(4) + elif total_chances > 5.5 and apply_limits: + return Decimal(3) + else: + val = min(float(total_chances), 6.0) + return Decimal(str(val)) + + +class CardResult(pydantic.BaseModel): + result_one: str = None + result_two: str = None + d20_one: str = None + d20_two: str = None + bold_one: bool = False + bold_two: bool = False + + def __str__(self): + res_text = f'Empty' + if self.result_one is not None: + res_text = f'{self.result_one}' + if self.d20_one is not None: + res_text += f' | {self.d20_one}' + if self.result_two is not None: + res_text += f'\n{self.result_two} | {self.d20_two}' + return res_text + + def is_full(self): + return self.result_one is not None + + def assign_play(self, play: PlayResult, secondary_play: Optional[PlayResult] = None, d20: Optional[int] = None): + if secondary_play is None: + self.result_one = play.full_name + if '++' in play.full_name: + logging.warning(f'Too many plus symbols: {play.full_name}') + self.result_one = re.sub(r'\++', '+', play.full_name) + + if play.is_offense: + self.bold_one = True + else: + self.result_one = play.short_name + self.result_two = secondary_play.short_name + self.d20_one = f'1-{d20}' + if d20 == 19: + self.d20_two = f'20' + else: + self.d20_two = f'{d20 + 1}-20' + + if play.is_offense: + self.bold_one = True + if secondary_play.is_offense: + self.bold_two = True + + logging.debug(f'this result: {self}') + + +class CardColumn(pydantic.BaseModel): + two: CardResult = CardResult() # 1 chance + three: CardResult = CardResult() # 2 chances + four: CardResult = CardResult() # 3 chances + five: CardResult = CardResult() # 4 chances + six: CardResult = CardResult() # 5 chances + seven: CardResult = CardResult() # 6 chances + eight: CardResult = CardResult() # 5 chances + nine: CardResult = CardResult() # 4 chances + ten: CardResult = CardResult() # 3 chances + eleven: CardResult = CardResult() # 2 chances + twelve: CardResult = CardResult() # 1 chance + num_splits: int = 0 + num_lomax: int = 0 + num_plusgb: int = 0 + + def __str__(self): + return (f'2-{self.two}\n' + f'3-{self.three}\n' + f'4-{self.four}\n' + f'5-{self.five}\n' + f'6-{self.six}\n' + f'7-{self.seven}\n' + f'8-{self.eight}\n' + f'9-{self.nine}\n' + f'10-{self.ten}\n' + f'11-{self.eleven}\n' + f'12-{self.twelve}') + + def get_text(self) -> dict: + sixes = '' + results = '' + d20 = '' + + def bold(text): + return f'{text}' + + def blank(): + return ' ' + + for count, x in enumerate( + [self.two, self.three, self.four, self.five, self.six, self.seven, self.eight, self.nine, + self.ten, self.eleven, self.twelve], start=2): + if x.bold_one: + this_six = bold(f'{count}-') + this_result = bold(x.result_one) + this_d20 = bold(x.d20_one) if x.d20_one is not None else blank() + else: + this_six = f'{count}-' + this_result = f'{x.result_one}' + this_d20 = f'{x.d20_one}' if x.d20_one is not None else blank() + + if x.result_two is not None: + if x.bold_two: + this_six += f'
{bold(blank())}' + this_result += f'
{bold(x.result_two)}' + this_d20 += f'
{bold(x.d20_two)}' + else: + this_six += f'
{blank()}' + this_result += f'
{x.result_two}' + this_d20 += f'
{x.d20_two}' + + sixes += f'{this_six}
' + results += f'{this_result}
' + d20 += f'{this_d20}
' + + return {'sixes': sixes, 'results': results, 'd20': d20} + + def is_full(self): + return (self.two.is_full() and self.three.is_full() and self.four.is_full() and self.five.is_full() and + self.six.is_full() and self.seven.is_full() and self.eight.is_full() and self.nine.is_full() and + self.ten.is_full() and self.eleven.is_full() and self.twelve.is_full()) + + def add_result( + self, play: PlayResult, alt_direction: int, chances: Decimal, + secondary_play: Optional[PlayResult] = None): + if chances > Decimal(6.0): + logging.error(f'Cannot assign more than 6 chances per call\n' + f'Play: {play}\nAlt Direction: {alt_direction}\nChances: {chances}\n' + f'Secondary Play: {secondary_play}') + raise ValueError(f'Cannot assign more than 6 chances per call') + elif math.floor(chances) != chances and secondary_play is None: + if chances > Decimal(1.0): + chances = Decimal(math.floor(chances)) + else: + logging.error(f'Must have secondary play for fractional chances; could not round down to an integer\n' + f'Play: {play}\nChances: {chances}\nSecondary Play: {secondary_play}') + return False + + # Chances is whole number + if math.floor(chances) == chances: + if chances == Decimal(6): + if not self.seven.is_full(): + self.seven.assign_play(play) + return chances, 0 + + # Plus one + if not self.six.is_full(): + if not self.two.is_full(): + self.six.assign_play(play) + self.two.assign_play(play) + return chances, 0 + elif not self.twelve.is_full(): + self.six.assign_play(play) + self.twelve.assign_play(play) + return chances, 0 + + # Plus one + if not self.eight.is_full(): + if not self.two.is_full(): + self.eight.assign_play(play) + self.two.assign_play(play) + return chances, 0 + elif not self.twelve.is_full(): + self.eight.assign_play(play) + self.twelve.assign_play(play) + return chances, 0 + + # Plus two + if not self.five.is_full(): + if not self.three.is_full(): + self.five.assign_play(play) + self.three.assign_play(play) + return chances, 0 + elif not self.eleven.is_full(): + self.five.assign_play(play) + self.eleven.assign_play(play) + return chances, 0 + + # Bulk 2, 3, 4 and 10, 11, 12 + if not self.three.is_full() and not self.two.is_full() and not self.four.is_full(): + self.four.assign_play(play) + self.three.assign_play(play) + self.two.assign_play(play) + return chances, 0 + + if not self.ten.is_full() and not self.eleven.is_full() and not self.twelve.is_full(): + self.ten.assign_play(play) + self.eleven.assign_play(play) + self.twelve.assign_play(play) + return chances, 0 + + if not self.nine.is_full(): + if not self.three.is_full(): + self.nine.assign_play(play) + self.three.assign_play(play) + return chances, 0 + elif not self.eleven.is_full(): + self.nine.assign_play(play) + self.eleven.assign_play(play) + return chances, 0 + + if chances == Decimal(5): + if not self.six.is_full(): + self.six.assign_play(play) + return chances, 0 + + if not self.eight.is_full(): + self.eight.assign_play(play) + return chances, 0 + + # Bulk 3, 4 and 10, 11 + if not self.three.is_full() and not self.four.is_full(): + self.four.assign_play(play) + self.three.assign_play(play) + return chances, 0 + + if not self.ten.is_full() and not self.eleven.is_full(): + self.ten.assign_play(play) + self.eleven.assign_play(play) + return chances, 0 + + # Plus one + if not self.five.is_full(): + if not self.two.is_full(): + self.five.assign_play(play) + self.two.assign_play(play) + return chances, 0 + elif not self.twelve.is_full(): + self.five.assign_play(play) + self.twelve.assign_play(play) + return chances, 0 + + # Plus one + if not self.nine.is_full(): + if not self.two.is_full(): + self.nine.assign_play(play) + self.two.assign_play(play) + return chances, 0 + elif not self.twelve.is_full(): + self.nine.assign_play(play) + self.twelve.assign_play(play) + return chances, 0 + + # Plus two + if not self.four.is_full(): + if not self.three.is_full(): + self.four.assign_play(play) + self.three.assign_play(play) + return chances, 0 + elif not self.eleven.is_full(): + self.four.assign_play(play) + self.eleven.assign_play(play) + return chances, 0 + + # Plus two + if not self.ten.is_full(): + if not self.three.is_full(): + self.ten.assign_play(play) + self.three.assign_play(play) + return chances, 0 + elif not self.eleven.is_full(): + self.ten.assign_play(play) + self.eleven.assign_play(play) + return chances, 0 + + if chances == Decimal(4): + if not self.five.is_full(): + self.five.assign_play(play) + return chances, 0 + + if not self.nine.is_full(): + self.nine.assign_play(play) + return chances, 0 + + # Plus one + if not self.four.is_full(): + if not self.two.is_full(): + self.four.assign_play(play) + self.two.assign_play(play) + return chances, 0 + elif not self.twelve.is_full(): + self.four.assign_play(play) + self.twelve.assign_play(play) + return chances, 0 + + # Plus one + if not self.ten.is_full(): + if not self.two.is_full(): + self.ten.assign_play(play) + self.two.assign_play(play) + return chances, 0 + elif not self.twelve.is_full(): + self.ten.assign_play(play) + self.twelve.assign_play(play) + return chances, 0 + + if not self.three.is_full() and not self.eleven.is_full(): + self.three.assign_play(play) + self.eleven.assign_play(play) + return chances, 0 + + if chances == Decimal(3): + if not self.four.is_full(): + self.four.assign_play(play) + return chances, 0 + + if not self.ten.is_full(): + self.ten.assign_play(play) + return chances, 0 + + # Plus one + if not self.three.is_full(): + if not self.two.is_full(): + self.three.assign_play(play) + self.two.assign_play(play) + return chances, 0 + elif not self.twelve.is_full(): + self.three.assign_play(play) + self.twelve.assign_play(play) + return chances, 0 + + # Plus one + if not self.eleven.is_full(): + if not self.twelve.is_full(): + self.eleven.assign_play(play) + self.twelve.assign_play(play) + return chances, 0 + if not self.two.is_full(): + self.eleven.assign_play(play) + self.two.assign_play(play) + return chances, 0 + + if chances == Decimal(2): + if not self.three.is_full(): + self.three.assign_play(play) + return chances, 0 + + if not self.eleven.is_full(): + self.eleven.assign_play(play) + return chances, 0 + + if not self.two.is_full() and not self.twelve.is_full(): + self.two.assign_play(play) + self.twelve.assign_play(play) + return chances, 0 + + if chances == Decimal(1): + if not self.two.is_full(): + self.two.assign_play(play) + return chances, 0 + + if not self.twelve.is_full(): + self.twelve.assign_play(play) + return chances, 0 + + return False + + logging.debug(f'Not a whole number | Chances: {chances}') + if chances in EXACT_CHANCES and self.num_splits < 4 and secondary_play is not None: + logging.debug(f'In Exact Chances!') + if chances >= 3: + self.num_splits += 1 + logging.debug(f'Chances is greater than 3') + if chances == Decimal('3.2'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 16) + return chances, Decimal('0.8') + elif not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 16) + return chances, Decimal('0.8') + elif chances == Decimal('3.25'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 13) + return chances, Decimal('1.75') + elif not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 13) + return chances, Decimal('1.75') + elif chances == Decimal('3.3') and not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 11) + return chances, Decimal('2.7') + elif chances == Decimal('3.4'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 17) + return chances, Decimal('0.6') + elif not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 17) + return chances, Decimal('0.6') + elif chances == Decimal('3.5'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 14) + return chances, Decimal('1.5') + elif not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 14) + return chances, Decimal('1.5') + elif chances == Decimal('3.6'): + if not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 18) + return chances, Decimal('0.4') + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 18) + return chances, Decimal('0.4') + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 12) + return chances, Decimal('2.4') + elif chances == Decimal('3.75'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 15) + return chances, Decimal('1.25') + elif not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 15) + return chances, Decimal('1.25') + elif chances == Decimal('3.8'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 19) + return chances, Decimal('0.2') + elif not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 19) + return chances, Decimal('0.2') + elif chances == Decimal('3.9'): + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 13) + return chances, Decimal('2.1') + elif chances == Decimal('4.2'): + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 14) + return chances, Decimal('1.8') + elif chances == Decimal('4.25'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 17) + return chances, Decimal('0.75') + elif not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 17) + return chances, Decimal('0.75') + elif chances == Decimal('4.5'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 18) + return chances, Decimal('0.5') + if not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 18) + return chances, Decimal('0.5') + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 15) + return chances, Decimal('1.5') + elif chances == Decimal('4.75'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 19) + return chances, Decimal('0.25') + elif not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 19) + return chances, Decimal('0.25') + elif chances == Decimal('4.8'): + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 16) + return chances, Decimal('1.2') + elif chances == Decimal('5.1'): + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 17) + return chances, Decimal('0.9') + elif chances == Decimal('5.4'): + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 18) + return chances, Decimal('0.6') + elif chances == Decimal('5.7'): + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 19) + return chances, Decimal('0.3') + elif chances >= 1: + self.num_splits += 1 + logging.debug(f'Chances is greater than 1') + if chances == Decimal('1.05'): + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 7) + return chances, Decimal('1.95') + elif not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 7) + return chances, Decimal('1.95') + if chances == Decimal('1.1'): + if not self.three.is_full(): + self.three.assign_play(play, secondary_play, 11) + return chances, Decimal('0.9') + elif not self.eleven.is_full(): + self.eleven.assign_play(play, secondary_play, 11) + return chances, Decimal('0.9') + if chances == Decimal('1.2'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 6) + return chances, Decimal('2.8') + elif not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 6) + return chances, Decimal('2.8') + elif not self.four.is_full(): + self.four.assign_play(play, secondary_play, 8) + return chances, Decimal('1.8') + elif not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 8) + return chances, Decimal('1.8') + elif not self.three.is_full(): + self.three.assign_play(play, secondary_play, 12) + return chances, Decimal('0.8') + elif not self.eleven.is_full(): + self.eleven.assign_play(play, secondary_play, 12) + return chances, Decimal('0.8') + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 4) + return chances, Decimal('4.8') + if chances == Decimal('1.25'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 5) + return chances, Decimal('3.75') + elif not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 5) + return chances, Decimal('3.75') + if chances == Decimal('1.3'): + if not self.three.is_full(): + self.three.assign_play(play, secondary_play, 13) + return chances, Decimal('0.7') + elif not self.eleven.is_full(): + self.eleven.assign_play(play, secondary_play, 13) + return chances, Decimal('0.7') + if chances == Decimal('1.35'): + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 9) + return chances, Decimal('1.65') + elif not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 9) + return chances, Decimal('1.65') + if chances == Decimal('1.4'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 7) + return chances, Decimal('2.6') + elif not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 7) + return chances, Decimal('2.6') + elif not self.three.is_full(): + self.three.assign_play(play, secondary_play, 14) + return chances, Decimal('0.6') + elif not self.eleven.is_full(): + self.eleven.assign_play(play, secondary_play, 14) + return chances, Decimal('0.6') + if chances == Decimal('1.5'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 6) + return chances, Decimal('3.5') + elif not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 6) + return chances, Decimal('3.5') + elif not self.four.is_full(): + self.four.assign_play(play, secondary_play, 10) + return chances, Decimal('1.5') + elif not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 10) + return chances, Decimal('1.5') + elif not self.three.is_full(): + self.three.assign_play(play, secondary_play, 15) + return chances, Decimal('0.5') + elif not self.eleven.is_full(): + self.eleven.assign_play(play, secondary_play, 15) + return chances, Decimal('0.5') + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 5) + return chances, Decimal('4.5') + if chances == Decimal('1.6'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 8) + return chances, Decimal('2.4') + elif not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 8) + return chances, Decimal('2.4') + elif not self.three.is_full(): + self.three.assign_play(play, secondary_play, 16) + return chances, Decimal('0.4') + elif not self.eleven.is_full(): + self.eleven.assign_play(play, secondary_play, 16) + return chances, Decimal('0.4') + if chances == Decimal('1.65'): + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 11) + return chances, Decimal('1.35') + elif not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 11) + return chances, Decimal('1.35') + if chances == Decimal('1.7'): + if not self.three.is_full(): + self.three.assign_play(play, secondary_play, 17) + return chances, Decimal('0.3') + elif not self.eleven.is_full(): + self.eleven.assign_play(play, secondary_play, 17) + return chances, Decimal('0.3') + if chances == Decimal('1.75'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 7) + return chances, Decimal('3.25') + elif not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 7) + return chances, Decimal('3.25') + if chances == Decimal('1.8'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 9) + return chances, Decimal('2.2') + if not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 9) + return chances, Decimal('2.2') + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 12) + return chances, Decimal('1.2') + if not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 12) + return chances, Decimal('1.2') + if not self.three.is_full(): + self.three.assign_play(play, secondary_play, 18) + return chances, Decimal('0.2') + if not self.eleven.is_full(): + self.eleven.assign_play(play, secondary_play, 18) + return chances, Decimal('0.2') + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 6) + return chances, Decimal('4.2') + if chances == Decimal('1.9'): + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 13) + return chances, Decimal('1.1') + if not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 13) + return chances, Decimal('1.1') + if not self.three.is_full(): + self.three.assign_play(play, secondary_play, 19) + return chances, Decimal('0.1') + if not self.eleven.is_full(): + self.eleven.assign_play(play, secondary_play, 19) + return chances, Decimal('0.1') + if chances == Decimal('1.95'): + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 13) + return chances, Decimal('1.05') + elif not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 13) + return chances, Decimal('1.05') + if chances == Decimal('2.1'): + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 14) + return chances, Decimal('0.9') + if not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 14) + return chances, Decimal('0.9') + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 7) + return chances, Decimal('3.9') + if chances == Decimal('2.2'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 11) + return chances, Decimal('1.8') + if not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 11) + return chances, Decimal('1.8') + if chances == Decimal('2.25'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 9) + return chances, Decimal('2.75') + if not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 9) + return chances, Decimal('2.75') + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 15) + return chances, Decimal('0.75') + if not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 15) + return chances, Decimal('0.75') + if chances == Decimal('2.4'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 12) + return chances, Decimal('1.6') + if not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 12) + return chances, Decimal('1.6') + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 16) + return chances, Decimal('0.6') + if not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 16) + return chances, Decimal('0.6') + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 8) + return chances, Decimal('3.6') + if chances == Decimal('2.5'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 10) + return chances, Decimal('2.5') + if not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 10) + return chances, Decimal('2.5') + if chances == Decimal('2.55'): + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 17) + return chances, Decimal('0.45') + if not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 17) + return chances, Decimal('0.45') + if chances == Decimal('2.6'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 13) + return chances, Decimal('1.4') + if not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 13) + return chances, Decimal('1.4') + if chances == Decimal('2.7'): + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 18) + return chances, Decimal('0.3') + if not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 18) + return chances, Decimal('0.3') + if not self.seven.is_full(): + self.seven.assign_play(play, secondary_play, 9) + return chances, Decimal('3.3') + if chances == Decimal('2.75'): + if not self.six.is_full(): + self.six.assign_play(play, secondary_play, 11) + return chances, Decimal('2.25') + if not self.eight.is_full(): + self.eight.assign_play(play, secondary_play, 11) + return chances, Decimal('2.25') + if chances == Decimal('2.8'): + if not self.five.is_full(): + self.five.assign_play(play, secondary_play, 14) + return chances, Decimal('1.2') + if not self.nine.is_full(): + self.nine.assign_play(play, secondary_play, 14) + return chances, Decimal('1.2') + if chances == Decimal('2.85'): + if not self.four.is_full(): + self.four.assign_play(play, secondary_play, 19) + return chances, Decimal('0.15') + if not self.ten.is_full(): + self.ten.assign_play(play, secondary_play, 19) + return chances, Decimal('0.15') + else: + logging.debug(f'Chances is less than 1') + return False + + self.num_splits -= 1 + + else: + logging.debug(f'Not a whole number and not in Exact Chances! Trying to add a subset') + for x in EXACT_CHANCES: + if x < chances and ((chances - x) == round(chances - x)): + logging.debug(f'Trying to add {x} chances') + return self.add_result(play, alt_direction, x, secondary_play) + logging.debug(f'Could not find a valid match') + return False + + def total_chances(self): + total = 0 + total += 1 if self.two.is_full() else 0 + total += 2 if self.three.is_full() else 0 + total += 3 if self.four.is_full() else 0 + total += 4 if self.five.is_full() else 0 + total += 5 if self.six.is_full() else 0 + total += 6 if self.seven.is_full() else 0 + total += 5 if self.eight.is_full() else 0 + total += 4 if self.nine.is_full() else 0 + total += 3 if self.ten.is_full() else 0 + total += 2 if self.eleven.is_full() else 0 + total += 1 if self.twelve.is_full() else 0 + return total + + def add_fatigue(self, num_chances: int, k_only: bool = False): + def is_valid_result(this_result: CardResult): + if k_only: + return this_result.result_one == 'strikeout' and '•' not in this_result.result_one + else: + return (this_result.result_two is None and not this_result.bold_one + and 'X' not in this_result.result_one and '•' not in this_result.result_one) + + if num_chances == 6: + if is_valid_result(self.seven): + self.seven.result_one += ' •' + return 6 + elif num_chances == 5: + if is_valid_result(self.six): + self.six.result_one += ' •' + return 5 + if is_valid_result(self.eight): + self.eight.result_one += ' •' + return 5 + elif num_chances == 4: + if is_valid_result(self.five): + self.five.result_one += ' •' + return 4 + if is_valid_result(self.nine): + self.nine.result_one += ' •' + return 4 + + return 0 + + +class FullCard(pydantic.BaseModel): + col_one: CardColumn = CardColumn() + col_two: CardColumn = CardColumn() + col_three: CardColumn = CardColumn() + offense_col: int + alt_direction: int = 1 + num_plusgb: int = 0 + num_lomax: int = 0 + is_batter: bool = False + + class Config: + arbitrary_types_allowed = True + + def get_columns(self, is_offense: bool): + if is_offense: + if self.offense_col == 1: + first = self.col_one + second, third = (self.col_two, self.col_three) if self.alt_direction else (self.col_three, self.col_two) + elif self.offense_col == 2: + first = self.col_two + second, third = (self.col_three, self.col_one) if self.alt_direction else (self.col_one, self.col_three) + else: + first = self.col_three + second, third = (self.col_one, self.col_two) if self.alt_direction else (self.col_two, self.col_one) + else: + if self.offense_col == 1: + third = self.col_one + first, second = (self.col_two, self.col_three) if self.alt_direction else (self.col_three, self.col_two) + elif self.offense_col == 2: + third = self.col_two + first, second = (self.col_three, self.col_one) if self.alt_direction else (self.col_one, self.col_three) + else: + third = self.col_three + first, second = (self.col_one, self.col_two) if self.alt_direction else (self.col_two, self.col_one) + + return first, second, third + + def is_complete(self): + return self.col_one.is_full() and self.col_two.is_full() and self.col_three.is_full() + + def sample_output(self): + return (f'{"" if self.is_complete() else "NOT "}COMPLETE\n' + f'Column 1\n{self.col_one}\n\n' + f'Column 2\n{self.col_two}\n\n' + f'Column 3\n{self.col_three}') + + def add_result(self, play: PlayResult, chances: Decimal, secondary_play: Optional[PlayResult] = None): + first, second, third = self.get_columns(is_offense=play.is_offense) + + if 'gb' in play.full_name and chances + self.num_plusgb <= 6 and self.is_batter: + play.full_name += '+' + + for x in [first, second, third]: + r_data = x.add_result(play, self.alt_direction, chances, secondary_play) + if r_data: + if '+' in play.full_name: + self.num_plusgb += r_data[0] + elif 'max' in play.full_name: + self.num_lomax += r_data[0] + return r_data + + return False + + def card_fill(self, play: PlayResult): + for x in range(6, 0, -1): + r_data = self.add_result(play, Decimal(x)) + if r_data: + return r_data + + return 0, 0 + + def card_output(self) -> dict: + """Return the pre-rendered card columns as 9 HTML strings suitable for direct storage/display.""" + c1_output = self.col_one.get_text() + c2_output = self.col_two.get_text() + c3_output = self.col_three.get_text() + + return { + 'col_one_2d6': c1_output['sixes'], + 'col_one_results': c1_output['results'], + 'col_one_d20': c1_output['d20'], + 'col_two_2d6': c2_output['sixes'], + 'col_two_results': c2_output['results'], + 'col_two_d20': c2_output['d20'], + 'col_three_2d6': c3_output['sixes'], + 'col_three_results': c3_output['results'], + 'col_three_d20': c3_output['d20'], + } + + def total_chances(self): + return self.col_one.total_chances() + self.col_two.total_chances() + self.col_three.total_chances() + + def add_fatigue(self): + first, second, third = self.get_columns(is_offense=False) + + total_added = 0 + for x in [first, second, third]: + resp = x.add_fatigue(6, k_only=True) + if resp: + total_added += resp + break + + if total_added == 0: + for x in [first, second, third]: + resp = x.add_fatigue(6, k_only=False) + if resp: + total_added += resp + break + + if total_added == 0: + for x in [first, second, third]: + resp = x.add_fatigue(5, k_only=True) + if resp: + total_added += resp + break + + if total_added == 0: + for x in [first, second, third]: + resp = x.add_fatigue(5, k_only=False) + if resp: + total_added += resp + break + + if total_added != 10: + for x in [first, second, third]: + resp = x.add_fatigue(10 - total_added, k_only=True) + if resp: + total_added += resp + break + + if total_added != 10: + for x in [first, second, third]: + resp = x.add_fatigue(10 - total_added, k_only=False) + if resp: + total_added += resp + break + + if total_added != 10: + logging.error(f'FullCard add_fatigue - Could not add all fatigue results / total_added: {total_added}') + + +class FullBattingCard(FullCard): + is_batter: bool = True + + +class FullPitchingCard(FullCard): + is_batter: bool = False diff --git a/pitchers/calcs_pitcher.py b/pitchers/calcs_pitcher.py index 4950b5a..3256948 100644 --- a/pitchers/calcs_pitcher.py +++ b/pitchers/calcs_pitcher.py @@ -376,7 +376,20 @@ def get_pitcher_ratings(df_data) -> List[dict]: logger.info(f'vL: Total chances: {vl.total_chances()}') logger.info(f'vR: Total chances: {vr.total_chances()}') - return [vl.custom_to_dict(), vr.custom_to_dict()] + vl_dict = vl.custom_to_dict() + vr_dict = vr.custom_to_dict() + + try: + from pitchers.card_builder import build_pitcher_full_cards + vl_card, vr_card = build_pitcher_full_cards( + vl, vr, int(df_data['offense_col']), int(df_data['player_id']), df_data['pitch_hand'] + ) + vl_dict.update(vl_card.card_output()) + vr_dict.update(vr_card.card_output()) + except Exception as e: + logger.warning(f'Card layout builder failed for {df_data.name}: {e}') + + return [vl_dict, vr_dict] def total_chances(chance_data): diff --git a/pitchers/card_builder.py b/pitchers/card_builder.py new file mode 100644 index 0000000..4470925 --- /dev/null +++ b/pitchers/card_builder.py @@ -0,0 +1,712 @@ +import copy +import math +import logging +from decimal import Decimal + +from card_layout import FullPitchingCard, PLAY_RESULTS, PlayResult, EXACT_CHANCES, get_chances +from pitchers.calcs_pitcher import PitchingCardRatingsModel + +logger = logging.getLogger(__name__) + + +def build_pitcher_full_cards( + ratings_vl: PitchingCardRatingsModel, + ratings_vr: PitchingCardRatingsModel, + offense_col: int, + player_id: int, + hand: str, +) -> tuple: + """Build vL and vR FullPitchingCard objects from pre-calculated ratings. + + Returns (vl_card, vr_card). + """ + player_binary = player_id % 2 + + vl = FullPitchingCard(offense_col=offense_col, alt_direction=player_binary) + vr = FullPitchingCard(offense_col=offense_col, alt_direction=player_binary) + + def assign_pchances(this_card, play, chances, secondary_play=None): + r_data = this_card.add_result(play, chances, secondary_play) + if r_data: + return float(r_data[0]), float(r_data[1]) + else: + for x in EXACT_CHANCES + [Decimal('0.95')]: + if x < math.floor(chances - Decimal('0.05')): + r_data = this_card.add_result(play, Decimal(math.floor(chances)), secondary_play) + if r_data: + return float(r_data[0]), float(r_data[1]) + break + if x < chances and secondary_play is not None: + r_data = this_card.add_result(play, x, secondary_play) + if r_data: + return float(r_data[0]), float(r_data[1]) + return 0, 0 + + def get_preferred_mif(ratings): + if hand == 'L' and ratings.vs_hand == 'L': + return 'ss' + elif hand == 'L' or (hand == 'R' and ratings.vs_hand == 'R'): + return '2b' + else: + return 'ss' + + for card, data, vs_hand in [ + (vl, copy.deepcopy(ratings_vl), 'L'), + (vr, copy.deepcopy(ratings_vr), 'R'), + ]: + new_ratings = PitchingCardRatingsModel( + pitchingcard_id=data.pitchingcard_id, + pit_hand=data.pit_hand, + vs_hand=vs_hand, + hard_rate=data.hard_rate, + med_rate=data.med_rate, + soft_rate=data.soft_rate, + xcheck_p=0.0, xcheck_c=0.0, xcheck_1b=0.0, xcheck_2b=0.0, + xcheck_3b=0.0, xcheck_ss=0.0, xcheck_lf=0.0, xcheck_cf=0.0, xcheck_rf=0.0, + ) + + res_chances = data.bp_homerun + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PLAY_RESULTS['bp-hr'], ch) + res_chances -= r_val[0] + new_ratings.bp_homerun += r_val[0] + + res_chances = data.hbp + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name='HBP', short_name='HBP'), ch) + res_chances -= r_val[0] + new_ratings.hbp += r_val[0] + if r_val[0] == 0: + break + + res_chances = data.xcheck_p + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name='GB (p) X', short_name='GB (p) X'), ch) + res_chances -= r_val[0] + new_ratings.xcheck_p += r_val[0] + + res_chances = data.xcheck_c + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name='CATCH X', short_name='CATCH X'), ch) + res_chances -= r_val[0] + new_ratings.xcheck_c += r_val[0] + + res_chances = data.xcheck_1b + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name='GB (1b) X', short_name='GB (1b) X'), ch) + res_chances -= r_val[0] + new_ratings.xcheck_1b += r_val[0] + + res_chances = data.xcheck_3b + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name='GB (3b) X', short_name='GB (3b) X'), ch) + res_chances -= r_val[0] + new_ratings.xcheck_3b += r_val[0] + + res_chances = data.xcheck_rf + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name='FLY (rf) X', short_name='FLY (rf) X'), ch) + res_chances -= r_val[0] + new_ratings.xcheck_rf += r_val[0] + + res_chances = data.xcheck_lf + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name='FLY (lf) X', short_name='FLY (lf) X'), ch) + res_chances -= r_val[0] + new_ratings.xcheck_lf += r_val[0] + + res_chances = data.xcheck_2b + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name='GB (2b) X', short_name='GB (2b) X'), ch) + res_chances -= r_val[0] + new_ratings.xcheck_2b += r_val[0] + + res_chances = data.xcheck_cf + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name='FLY (cf) X', short_name='FLY (cf) X'), ch) + res_chances -= r_val[0] + new_ratings.xcheck_cf += r_val[0] + + res_chances = data.xcheck_ss + while res_chances > 0: + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name='GB (ss) X', short_name='GB (ss) X'), ch) + res_chances -= r_val[0] + new_ratings.xcheck_ss += r_val[0] + + res_chances = data.walk + while res_chances >= 1: + ch = get_chances(res_chances) + if data.strikeout > max(1 - ch, 0): + secondary = PlayResult(full_name='strikeout', short_name='so') + else: + secondary = None + + r_val = assign_pchances(card, PLAY_RESULTS['walk'], ch, secondary) + res_chances -= r_val[0] + new_ratings.walk += r_val[0] + if r_val[1] > 0: + data.strikeout -= r_val[1] + new_ratings.strikeout += r_val[1] + + if r_val[0] == 0: + break + + res_chances = data.homerun + retries = 0 + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.double_cf > 0: + data.double_cf += res_chances + elif data.double_two > 0: + data.double_two += res_chances + elif data.triple > 0: + data.triple += res_chances + elif data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + elif data.single_center > 0: + data.single_center += res_chances + break + + ch = get_chances(res_chances) + if data.double_cf > (data.flyout_rf_b + data.flyout_lf_b) and data.double_cf > max(1 - ch, 0): + secondary = PLAY_RESULTS['do-cf'] + elif data.flyout_cf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-cf'] + elif data.flyout_lf_b > data.flyout_rf_b and data.flyout_lf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-lf'] + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-rf'] + elif data.double_cf > max(1 - ch, 0): + secondary = PLAY_RESULTS['do-cf'] + elif data.double_three > max(1 - ch, 0): + secondary = PLAY_RESULTS['do***'] + elif data.double_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['do**'] + elif data.triple > max(1 - ch, 0): + secondary = PLAY_RESULTS['tr'] + else: + secondary = None + + r_val = assign_pchances(card, PLAY_RESULTS['hr'], ch, secondary) + res_chances -= r_val[0] + new_ratings.homerun += r_val[0] + if r_val[1] > 0: + if 'DO (' in secondary.short_name: + data.double_cf -= r_val[1] + new_ratings.double_cf += r_val[1] + elif 'lf' in secondary.short_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'cf' in secondary.short_name: + data.flyout_cf_b -= r_val[1] + new_ratings.flyout_cf_b += r_val[1] + elif 'rf' in secondary.short_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif '***' in secondary.short_name: + data.double_three -= r_val[1] + new_ratings.double_three += r_val[1] + elif '**' in secondary.short_name: + data.double_two -= r_val[1] + new_ratings.double_two += r_val[1] + elif 'TR' in secondary.short_name: + data.triple -= r_val[1] + new_ratings.triple += r_val[1] + + if r_val[0] == 0: + retries += 1 + + res_chances = data.triple + retries = 0 + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.double_cf > 0: + data.double_cf += res_chances + elif data.double_two > 0: + data.double_two += res_chances + elif data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + break + + ch = get_chances(res_chances) + if data.single_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['si**'] + elif data.flyout_cf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-cf'] + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-lf'] + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-rf'] + elif data.double_cf > max(1 - ch, 0): + secondary = PLAY_RESULTS['do-cf'] + elif data.double_three > max(1 - ch, 0): + secondary = PLAY_RESULTS['do***'] + elif data.double_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['do**'] + else: + secondary = None + + r_val = assign_pchances(card, PLAY_RESULTS['tr'], ch, secondary) + res_chances -= r_val[0] + new_ratings.triple += r_val[0] + if r_val[1] > 0: + if 'DO (' in secondary.short_name: + data.double_cf -= r_val[1] + new_ratings.double_cf += r_val[1] + elif 'lf' in secondary.short_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'cf' in secondary.short_name: + data.flyout_cf_b -= r_val[1] + new_ratings.flyout_cf_b += r_val[1] + elif 'rf' in secondary.short_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif '***' in secondary.short_name: + data.double_three -= r_val[1] + new_ratings.double_three += r_val[1] + elif 'SI' in secondary.short_name: + data.single_two -= r_val[1] + new_ratings.single_two += r_val[1] + elif '**' in secondary.short_name: + data.double_two -= r_val[1] + new_ratings.double_two += r_val[1] + + if r_val[0] == 0: + retries += 1 + + res_chances = data.double_three + retries = 0 + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.double_cf > 0: + data.double_cf += res_chances + elif data.double_two > 0: + data.double_two += res_chances + elif data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + break + + ch = get_chances(res_chances) + if data.single_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['si**'] + elif data.flyout_cf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-cf'] + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-lf'] + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-rf'] + elif data.double_cf > max(1 - ch, 0): + secondary = PLAY_RESULTS['do-cf'] + elif data.double_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['do**'] + else: + secondary = None + + r_val = assign_pchances(card, PLAY_RESULTS['do***'], ch, secondary) + res_chances -= r_val[0] + new_ratings.double_three += r_val[0] + if r_val[1] > 0: + if 'DO (' in secondary.short_name: + data.double_cf -= r_val[1] + new_ratings.double_cf += r_val[1] + elif 'lf' in secondary.short_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'cf' in secondary.short_name: + data.flyout_cf_b -= r_val[1] + new_ratings.flyout_cf_b += r_val[1] + elif 'rf' in secondary.short_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif 'SI' in secondary.short_name: + data.single_two -= r_val[1] + new_ratings.single_two += r_val[1] + elif '**' in secondary.short_name: + data.double_two -= r_val[1] + new_ratings.double_two += r_val[1] + + if r_val[0] == 0: + retries += 1 + + res_chances = data.double_cf + retries = 0 + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.double_two > 0: + data.double_two += res_chances + elif data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + break + + ch = get_chances(res_chances) + if data.flyout_cf_b > max(1 - ch, 0): + secondary = PlayResult(full_name='fly (cf) B', short_name='fly B') + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PlayResult(full_name='fly (lf) B', short_name='fly B') + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PlayResult(full_name='fly (rf) B', short_name='fly b') + elif data.single_one > max(1 - ch, 0): + secondary = PLAY_RESULTS['si*'] + elif data.single_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['si**'] + else: + secondary = None + + r_val = assign_pchances(card, PLAY_RESULTS['do-cf'], ch, secondary) + res_chances -= r_val[0] + new_ratings.double_cf += r_val[0] + if r_val[1] > 0: + if 'lf' in secondary.full_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'rf' in secondary.full_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif 'cf' in secondary.full_name: + data.flyout_cf_b -= r_val[1] + new_ratings.flyout_cf_b += r_val[1] + elif '***' in secondary.short_name: + data.double_three -= r_val[1] + new_ratings.double_three += r_val[1] + elif 'SI' in secondary.short_name: + data.single_two -= r_val[1] + new_ratings.single_two += r_val[1] + elif '**' in secondary.short_name: + data.double_two -= r_val[1] + new_ratings.double_two += r_val[1] + + if r_val[0] == 0: + retries += 1 + + res_chances = data.double_two + retries = 0 + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.single_two > 0: + data.single_two += res_chances + elif data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + elif data.walk > 0: + data.walk += res_chances + break + + ch = get_chances(res_chances) + if data.single_two > max(1 - ch, 0): + secondary = PLAY_RESULTS['si**'] + elif data.single_center > max(1 - ch, 0): + secondary = PLAY_RESULTS['si-cf'] + elif data.flyout_cf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-cf'] + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-lf'] + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-rf'] + else: + secondary = None + + r_val = assign_pchances(card, PLAY_RESULTS['do**'], ch, secondary) + res_chances -= r_val[0] + new_ratings.double_two += r_val[0] + if r_val[1] > 0: + if 'lf' in secondary.full_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'rf' in secondary.full_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif 'cf' in secondary.full_name: + data.flyout_cf_b -= r_val[1] + new_ratings.flyout_cf_b += r_val[1] + elif 'SI' in secondary.short_name: + data.single_two -= r_val[1] + new_ratings.single_two += r_val[1] + + if r_val[0] == 0: + retries += 1 + + res_chances = data.single_two + retries = 0 + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.single_center > 0: + data.single_center += res_chances + elif data.single_one > 0: + data.single_one += res_chances + elif data.walk > 0: + data.walk += res_chances + break + + pref_mif = get_preferred_mif(new_ratings) + ch = get_chances(res_chances) + if data.groundout_a > max(1 - ch, 0): + temp_mif = get_preferred_mif(new_ratings) + pref_mif = 'ss' if temp_mif == '2b' else '2b' + secondary = PlayResult(full_name=f'gb ({pref_mif}) A', short_name=f'gb ({pref_mif}) A') + elif data.groundout_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'gb ({pref_mif}) B', short_name=f'gb ({pref_mif}) B') + elif data.flyout_cf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-cf'] + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-lf'] + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PLAY_RESULTS['fly-rf'] + else: + secondary = None + + r_val = assign_pchances(card, PLAY_RESULTS['si**'], ch, secondary) + res_chances -= r_val[0] + new_ratings.single_two += r_val[0] + if r_val[1] > 0: + if 'B' in secondary.short_name: + data.groundout_b -= r_val[1] + new_ratings.groundout_b += r_val[1] + elif 'A' in secondary.short_name: + data.groundout_a -= r_val[1] + new_ratings.groundout_a += r_val[1] + elif 'lf' in secondary.full_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'rf' in secondary.full_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + elif 'cf' in secondary.full_name: + data.flyout_cf_b -= r_val[1] + + if r_val[0] == 0: + retries += 1 + + res_chances = data.single_center + retries = 0 + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.single_one > 0: + data.single_one += res_chances + elif data.walk > 0: + data.walk += res_chances + break + + ch = get_chances(res_chances) + if data.flyout_cf_b > max(1 - ch, 0): + secondary = PlayResult(full_name='fly (cf) B', short_name='fly B') + elif data.flyout_lf_b > max(1 - ch, 0) and data.flyout_lf_b > data.flyout_rf_b: + secondary = PlayResult(full_name='fly (lf) B', short_name='fly B') + elif data.flyout_rf_b > max(1 - ch, 0): + secondary = PlayResult(full_name='fly (rf) B', short_name='fly B') + elif data.flyout_lf_b > max(1 - ch, 0): + secondary = PlayResult(full_name='fly (lf) B', short_name='fly B') + else: + secondary = None + + r_val = assign_pchances(card, PLAY_RESULTS['si-cf'], ch, secondary) + res_chances -= r_val[0] + new_ratings.single_center += r_val[0] + if r_val[1] > 0: + if 'CF' in secondary.short_name: + data.flyout_cf_b -= r_val[1] + new_ratings.flyout_cf_b += r_val[1] + elif 'LF' in secondary.full_name: + data.flyout_lf_b -= r_val[1] + new_ratings.flyout_lf_b += r_val[1] + elif 'RF' in secondary.full_name: + data.flyout_rf_b -= r_val[1] + new_ratings.flyout_rf_b += r_val[1] + + if r_val[0] == 0: + retries += 1 + + res_chances = data.single_one + retries = 0 + while res_chances > 0: + if res_chances < 1 or retries > 0: + if data.walk > 0: + data.walk += res_chances + break + + pref_mif = get_preferred_mif(new_ratings) + ch = get_chances(res_chances) + if data.groundout_b > max(1 - ch, 0): + secondary = PlayResult(full_name=f'gb ({pref_mif}) B', short_name=f'gb ({pref_mif}) B') + elif data.groundout_a > max(1 - ch, 0): + temp_mif = get_preferred_mif(new_ratings) + pref_mif = 'ss' if temp_mif == '2b' else '2b' + secondary = PlayResult(full_name=f'gb ({pref_mif}) A', short_name=f'gb ({pref_mif}) A') + else: + secondary = None + + r_val = assign_pchances(card, PLAY_RESULTS['si*'], ch, secondary) + res_chances -= r_val[0] + new_ratings.single_one += r_val[0] + if r_val[1] > 0: + if 'B' in secondary.short_name: + data.groundout_b -= r_val[1] + new_ratings.groundout_b += r_val[1] + elif 'A' in secondary.short_name: + data.groundout_a -= r_val[1] + new_ratings.groundout_a += r_val[1] + + if r_val[0] == 0: + retries += 1 + + res_chances = data.bp_single + retries = 0 + while res_chances > 0: + if retries > 0: + break + ch = get_chances(res_chances) + r_val = assign_pchances(card, PLAY_RESULTS['bp-si'], ch) + res_chances -= r_val[0] + new_ratings.bp_single += r_val[0] + if r_val[0] == 0: + retries += 1 + + res_chances = data.strikeout + retries = 0 + while res_chances > 0: + if retries > 0: + break + ch = get_chances(res_chances) + r_val = assign_pchances(card, PlayResult(full_name='strikeout', short_name='so'), ch) + res_chances -= r_val[0] + new_ratings.strikeout += r_val[0] + if r_val[0] == 0: + retries += 1 + + res_chances = data.flyout_cf_b + retries = 0 + while res_chances > 0: + if retries > 0: + break + ch = get_chances(res_chances) + r_val = assign_pchances(card, PLAY_RESULTS['fly-cf'], ch) + res_chances -= r_val[0] + new_ratings.flyout_cf_b += r_val[0] + if r_val[0] == 0: + retries += 1 + + res_chances = data.flyout_lf_b + retries = 0 + while res_chances > 0: + if retries > 0: + break + ch = get_chances(res_chances) + r_val = assign_pchances(card, PLAY_RESULTS['fly-lf'], ch) + res_chances -= r_val[0] + new_ratings.flyout_lf_b += r_val[0] + if r_val[0] == 0: + retries += 1 + + res_chances = data.flyout_rf_b + retries = 0 + while res_chances > 0: + if retries > 0: + break + ch = get_chances(res_chances) + r_val = assign_pchances(card, PLAY_RESULTS['fly-rf'], ch) + res_chances -= r_val[0] + new_ratings.flyout_rf_b += r_val[0] + if r_val[0] == 0: + retries += 1 + + res_chances = data.groundout_a + retries = 0 + while res_chances > 0: + if retries > 0: + break + + temp_mif = get_preferred_mif(new_ratings) + pref_mif = 'ss' if temp_mif == '2b' else '2b' + ch = get_chances(res_chances) + r_val = assign_pchances( + card, + PlayResult(full_name=f'gb ({pref_mif}) A', short_name=f'gb ({pref_mif}) A'), + ch, + ) + res_chances -= r_val[0] + new_ratings.groundout_a += r_val[0] + + if r_val[0] == 0: + retries += 1 + + res_chances = data.groundout_b + retries = 0 + while res_chances > 0: + if retries > 0: + break + + pref_mif = get_preferred_mif(new_ratings) + ch = get_chances(res_chances) + r_val = assign_pchances( + card, + PlayResult(full_name=f'gb ({pref_mif}) B', short_name=f'gb ({pref_mif}) B'), + ch, + ) + res_chances -= r_val[0] + new_ratings.groundout_b += r_val[0] + + if r_val[0] == 0: + retries += 1 + + plays = sorted( + [(data.strikeout, 'so'), (data.groundout_a, 'gb'), (data.flyout_lf_b, 'lf'), (data.flyout_rf_b, 'rf')], + key=lambda z: z[0], + reverse=True, + ) + count_filler = -1 + pref_mif = get_preferred_mif(new_ratings) + while not card.is_complete(): + count_filler += 1 + this_play = plays[count_filler % 4] + if this_play[1] == 'so': + play_res = PlayResult(full_name='strikeout', short_name='strikeout') + elif this_play[1] == 'gb': + this_if = '3b' if pref_mif == 'ss' else '1b' + play_res = PlayResult(full_name=f'gb ({this_if}) A', short_name=f'gb ({this_if}) A') + elif this_play[1] == 'lf': + play_res = PLAY_RESULTS['fly-lf'] + else: + play_res = PLAY_RESULTS['fly-rf'] + + r_raw = card.card_fill(play_res) + r_val = (float(r_raw[0]), float(r_raw[1])) + + if this_play[1] == 'so': + new_ratings.strikeout += r_val[0] + elif this_play[1] == 'gb': + new_ratings.groundout_a += r_val[0] + elif this_play[1] == 'lf': + new_ratings.flyout_lf_b += r_val[0] + else: + new_ratings.flyout_rf_b += r_val[0] + + card.add_fatigue() + new_ratings.calculate_rate_stats() + + return vl, vr