From 962b9cf6f1d35faad3e52338e7983ee0d779b249 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sat, 21 Mar 2026 17:04:03 -0500 Subject: [PATCH] feat: implement tweak_archetype() and manual_adjustments() (#12) Closes #12 - tweak_archetype(): prompts user for updated archetype stats (avg/obp/slg/bb%/k% vs L and R, power and batted-ball profile, baserunning for batters), then recalculates D20 card ratings via the existing calculator - manual_adjustments(): prompts user to choose a split (vs L or vs R), displays all 22 D20 chance fields with running total, accepts field-number + value edits, and warns if total deviates from 108 Co-Authored-By: Claude Sonnet 4.6 --- custom_cards/interactive_creator.py | 164 ++++++++++++++++++++++++++-- 1 file changed, 156 insertions(+), 8 deletions(-) diff --git a/custom_cards/interactive_creator.py b/custom_cards/interactive_creator.py index 70c0c9d..9a1dde7 100644 --- a/custom_cards/interactive_creator.py +++ b/custom_cards/interactive_creator.py @@ -6,6 +6,7 @@ baseball archetypes with iterative review and refinement. """ import asyncio +import copy import sys from typing import Literal from datetime import datetime @@ -425,10 +426,68 @@ class CustomCardCreator: print("-" * 70) print("\nAdjust key percentages (press Enter to keep current value):\n") - # TODO: Implement percentage tweaking - # For now, return unchanged - print("(Feature coming soon - manual adjustments available in option 3)") - return card_data + def prompt_float(label: str, current: float) -> float: + val = input(f" {label} [{current:.3f}]: ").strip() + if not val: + return current + try: + return float(val) + except ValueError: + print(" Invalid value, keeping current.") + return current + + def prompt_int(label: str, current: int) -> int: + val = input(f" {label} [{current}]: ").strip() + if not val: + return current + try: + return int(val) + except ValueError: + print(" Invalid value, keeping current.") + return current + + arch = copy.copy(archetype) + + print("--- vs RHP/RHB ---") + arch.avg_vs_r = prompt_float("AVG vs R", arch.avg_vs_r) + arch.obp_vs_r = prompt_float("OBP vs R", arch.obp_vs_r) + arch.slg_vs_r = prompt_float("SLG vs R", arch.slg_vs_r) + arch.bb_pct_vs_r = prompt_float("BB% vs R", arch.bb_pct_vs_r) + arch.k_pct_vs_r = prompt_float("K% vs R", arch.k_pct_vs_r) + + print("\n--- vs LHP/LHB ---") + arch.avg_vs_l = prompt_float("AVG vs L", arch.avg_vs_l) + arch.obp_vs_l = prompt_float("OBP vs L", arch.obp_vs_l) + arch.slg_vs_l = prompt_float("SLG vs L", arch.slg_vs_l) + arch.bb_pct_vs_l = prompt_float("BB% vs L", arch.bb_pct_vs_l) + arch.k_pct_vs_l = prompt_float("K% vs L", arch.k_pct_vs_l) + + print("\n--- Power Profile ---") + arch.hr_per_hit = prompt_float("HR/Hit", arch.hr_per_hit) + arch.triple_per_hit = prompt_float("3B/Hit", arch.triple_per_hit) + arch.double_per_hit = prompt_float("2B/Hit", arch.double_per_hit) + + print("\n--- Batted Ball Profile ---") + arch.gb_pct = prompt_float("GB%", arch.gb_pct) + arch.fb_pct = prompt_float("FB%", arch.fb_pct) + arch.ld_pct = prompt_float("LD%", arch.ld_pct) + + if player_type == "batter": + print("\n--- Baserunning ---") + arch.speed_rating = prompt_int("Speed (1-10)", arch.speed_rating) # type: ignore[arg-type] + arch.steal_jump = prompt_int("Jump (1-10)", arch.steal_jump) # type: ignore[arg-type] + arch.xbt_pct = prompt_float("XBT%", arch.xbt_pct) # type: ignore[union-attr] + + # Recalculate card ratings with the modified archetype + if player_type == "batter": + calc = BatterRatingCalculator(arch) # type: ignore[arg-type] + ratings = calc.calculate_ratings(battingcard_id=0) + baserunning = calc.calculate_baserunning() + return {"ratings": ratings, "baserunning": baserunning} + else: + calc_p = PitcherRatingCalculator(arch) # type: ignore[arg-type] + ratings = calc_p.calculate_ratings(pitchingcard_id=0) + return {"ratings": ratings} async def manual_adjustments( self, player_type: Literal["batter", "pitcher"], card_data: dict @@ -439,10 +498,99 @@ class CustomCardCreator: print("-" * 70) print("\nDirectly edit D20 chances (must sum to 108):\n") - # TODO: Implement manual adjustments - # For now, return unchanged - print("(Feature coming soon)") - return card_data + D20_FIELDS = [ + "homerun", + "bp_homerun", + "triple", + "double_three", + "double_two", + "double_pull", + "single_two", + "single_one", + "single_center", + "bp_single", + "hbp", + "walk", + "strikeout", + "lineout", + "popout", + "flyout_a", + "flyout_bq", + "flyout_lf_b", + "flyout_rf_b", + "groundout_a", + "groundout_b", + "groundout_c", + ] + + # Choose which split to edit + print("Which split to edit?") + for i, rating in enumerate(card_data["ratings"]): + vs = rating["vs_hand"] + print(f" {i + 1}. vs {vs}{'HP' if player_type == 'batter' else 'HB'}") + + while True: + choice = input("\nSelect split (1-2): ").strip() + try: + idx = int(choice) - 1 + if 0 <= idx < len(card_data["ratings"]): + break + else: + print("Invalid choice.") + except ValueError: + print("Invalid input.") + + result = copy.deepcopy(card_data) + rating = result["ratings"][idx] + + while True: + vs = rating["vs_hand"] + print( + f"\n--- VS {vs}{'HP' if player_type == 'batter' else 'HB'} D20 Chances ---" + ) + total = 0.0 + for i, field in enumerate(D20_FIELDS, 1): + val = rating[field] + print(f" {i:2d}. {field:<20s}: {val:.2f}") + total += val + print(f"\n Total: {total:.2f} (target: 108.00)") + + user_input = input( + "\nEnter field number and new value (e.g. '1 3.5'), or 'done': " + ).strip() + if user_input.lower() in ("done", "q", ""): + break + + parts = user_input.split() + if len(parts) != 2: + print(" Enter a field number and a value separated by a space.") + continue + + try: + field_idx = int(parts[0]) - 1 + new_val = float(parts[1]) + except ValueError: + print(" Invalid input.") + continue + + if not (0 <= field_idx < len(D20_FIELDS)): + print(f" Field number must be between 1 and {len(D20_FIELDS)}.") + continue + + if new_val < 0: + print(" Value cannot be negative.") + continue + + rating[D20_FIELDS[field_idx]] = new_val + + total = sum(rating[f] for f in D20_FIELDS) + if abs(total - 108.0) > 0.01: + print( + f"\nWarning: Total is {total:.2f} (expected 108.00). " + "Ratings saved but card probabilities may be incorrect." + ) + + return result async def create_database_records( self,