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