paper-dynasty-card-creation/card_layout.py
Cal Corum a72abc01a3 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>
2026-02-25 16:21:26 -06:00

1016 lines
46 KiB
Python

"""
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'<b>{text}</b>'
def blank():
return '&nbsp;'
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'<br>{bold(blank())}'
this_result += f'<br>{bold(x.result_two)}'
this_d20 += f'<br>{bold(x.d20_two)}'
else:
this_six += f'<br>{blank()}'
this_result += f'<br>{x.result_two}'
this_d20 += f'<br>{x.d20_two}'
sixes += f'{this_six}<br>'
results += f'{this_result}<br>'
d20 += f'{this_d20}<br>'
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