Refactor x-check chart data

This commit is contained in:
Cal Corum 2025-10-22 20:41:21 -05:00
parent 268ac9a547
commit d7dc9ed2bf
4 changed files with 532 additions and 380 deletions

385
commands/dice/chart_data.py Normal file
View File

@ -0,0 +1,385 @@
"""
Dice Rolling Chart Data
Contains all chart dictionaries for baseball dice rolling mechanics.
Extracted from rolls.py for better maintainability.
"""
# ============================================================================
# X-CHART DATA
# ============================================================================
INFIELD_X_CHART = {
'si1': {
'rp': 'Runner on first: Line drive hits the runner! Runner on first is out. Batter goes to first with single '
'and all other runners hold.\nNo runner on first: batter singles, runners advance 1 base.',
'e1': 'Single and Error, batter to second, runners advance 2 bases.',
'e2': 'Single and Error, batter to third, all runners score.',
'no': 'Single, runners advance 1 base.'
},
'spd': {
'rp': 'No effect; proceed with speed check',
'e1': 'Single and Error, batter to second, runners advance 2 bases.',
'e2': 'Single and Error, batter to third, all runners score.',
'no': 'Speed check, safe range equals batter\'s running rating, SI* result if safe, gb C if out'
},
'po': {
'rp': 'The batters hits a popup. None of the fielders take charge on the play and the ball drops in the '
'infield for a single! All runners advance 1 base.',
'e1': 'The catcher drops a popup for an error. All runners advance 1 base.',
'e2': 'The catcher grabs a squib in front of the plate and throws it into right field. The batter goes to '
'second and all runners score.',
'no': 'The batter pops out to the catcher.'
},
'wp': {
'rp': 'Automatic wild pitch. Catcher has trouble finding it and all base runners advance 2 bases.',
'no': 'Automatic wild pitch, all runners advance 1 base and batter rolls AB again.'
},
'x': {
'rp': 'Runner(s) on base: pitcher trips during his delivery and the ball sails for automatic wild pitch, '
'runners advance 1 base and batter rolls AB again.',
'no': 'Wild pitch check (credited as a PB). If a passed ball occurs, batter rerolls AB. '
'If no passed ball occurs, the batter fouls out to the catcher.'
},
'fo': {
'rp': 'Batter swings and misses, but is awarded first base on a catcher interference call! Baserunners advance '
'only if forced.',
'e1': 'The catcher drops a foul popup for an error. Batter rolls AB again.',
'e2': 'The catcher drops a foul popup for an error. Batter rolls AB again.',
'no': 'Runner(s) on base: make a passed ball check. If no passed ball, batter pops out to the catcher. If a '
'passed ball occurs, batter roll his AB again.\nNo runners: batter pops out to the catcher'
},
'g1': {
'rp': 'Runner on first: runner on first breaks up the double play, but umpires call runner interference and '
'the batter is out on GIDP.\nNo runners: Batter grounds out.',
'e1': 'Error, batter to first, runners advance 1 base.',
'e2': 'Error, batter to second, runners advance 2 bases.',
'no': 'Consult Groundball Chart: `!gbA`'
},
'g2': {
'rp': 'Batter lines the ball off the pitcher to the fielder who makes the play to first for the out! Runners '
'advance only if forced.',
'e1': 'Error, batter to first, runners advance 1 base.',
'e2': 'Error, batter to second, runners advance 2 bases.',
'no': 'Consult Groundball Chart: `!gbB`'
},
'g3': {
'rp': 'Batter lines the ball off the mound and deflects to the fielder who makes the play to first for the '
'out! Runners advance 1 base.',
'e1': 'Error, batter to first, runners advance 1 base.',
'e2': 'Error, batter to second, runners advance 2 bases.',
'no': 'Consult Groundball Chart: `!gbC`'
},
}
OUTFIELD_X_CHART = {
'si2': {
'rp': 'Batter singles, baserunners advance 2 bases. As the batter rounds first, the fielder throws behind him '
'and catches him off the bag for an out!',
'e1': 'Single and error, batter to second, runners advance 2 bases.',
'e2': 'Single and error, batter to third, all runners score.',
'e3': 'Single and error, batter to third, all runners score',
'no': 'Single, all runners advance 2 bases.'
},
'do2': {
'rp': 'Batter doubles, runners advance 2 bases. The outfielder throws the ball to the shortstop who executes a '
'hidden ball trick! Runner on second is called out!',
'e1': 'Double and error, batter to third, all runners score.',
'e2': 'Double and error, batter to third, and all runners score.',
'e3': 'Double and error, batter and all runners score. Little league home run!',
'no': 'Double, all runners advance 2 bases.'
},
'do3': {
'rp': 'Runner(s) on base: batter doubles and runners advance three bases as the outfielders collide!\n'
'No runners: Batter doubles, but the play is appealed. The umps rule the batter missed first base so is '
'out on the appeal!',
'e1': 'Double and error, batter to third, all runners score.',
'e2': 'Double and error, batter and all runners score. Little league home run!',
'e3': 'Double and error, batter and all runners score. Little league home run!',
'no': 'Double, all runners score.'
},
'tr3': {
'rp': 'Batter hits a ball into the gap and the outfielders collide trying to make the play! The ball rolls to '
'the wall and the batter trots home with an inside-the-park home run!',
'e1': 'Triple and error, batter and all runners score. Little league home run!',
'e2': 'Triple and error, batter and all runners score. Little league home run!',
'e3': 'Triple and error, batter and all runners score. Little league home run!',
'no': 'Triple, all runners score.'
},
'f1': {
'rp': 'The outfielder races back and makes a diving catch and collides with the wall! In the time he takes to '
'recuperate, all baserunners tag-up and advance 2 bases.',
'e1': '1 base error, runners advance 1 base.',
'e2': '2 base error, runners advance 2 bases.',
'e3': '3 base error, batter to third, all runners score.',
'no': 'Flyball A'
},
'f2': {
'rp': 'The outfielder catches the flyball for an out. If there is a runner on third, he tags-up and scores. '
'The play is appealed and the umps rule that the runner left early and is out on the appeal!',
'e1': '1 base error, runners advance 1 base.',
'e2': '2 base error, runners advance 2 bases.',
'e3': '3 base error, batter to third, all runners score.',
'no': 'Flyball B'
},
'f3': {
'rp': 'The outfielder makes a running catch in the gap! The lead runner lost track of the ball and was '
'advancing - he cannot return in time and is doubled off by the outfielder.',
'e1': '1 base error, runners advance 1 base.',
'e2': '2 base error, runners advance 2 bases.',
'e3': '3 base error, batter to third, all runners score.',
'no': 'Flyball C'
}
}
# ============================================================================
# FIELDING RANGE CHARTS
# ============================================================================
INFIELD_RANGES = {
1: 'G3# SI1 ----SI2----',
2: 'G2# SI1 ----SI2----',
3: 'G2# G3# SI1 --SI2--',
4: 'G2# G3# SI1 --SI2--',
5: 'G1 --G3#-- SI1 SI2',
6: 'G1 G2# G3# SI1 SI2',
7: 'G1 G2 --G3#-- SI1',
8: 'G1 G2 --G3#-- SI1',
9: 'G1 G2 G3 --G3#--',
10: '--G1--- G2 --G3#--',
11: '--G1--- G2 G3 G3#',
12: '--G1--- G2 G3 G3#',
13: '--G1--- G2 --G3---',
14: '--G1--- --G2--- G3',
15: '----G1----- G2 G3',
16: '----G1----- G2 G3',
17: '------G1------- G3',
18: '------G1------- G2',
19: '------G1------- G2',
20: '--------G1---------'
}
OUTFIELD_RANGES = {
1: 'F1 DO2 DO3 --TR3--',
2: 'F2 SI2 DO2 DO3 TR3',
3: 'F2 SI2 --DO2-- DO3',
4: 'F2 F1 SI2 DO2 DO3',
5: '--F2--- --SI2-- DO2',
6: '--F2--- --SI2-- DO2',
7: '--F2--- F1 SI2 DO2',
8: '--F2--- F1 --SI2--',
9: '----F2----- --SI2--',
10: '----F2----- --SI2--',
11: '----F2----- --SI2--',
12: '----F2----- F1 SI2',
13: '----F2----- F1 SI2',
14: 'F3 ----F2----- SI2',
15: 'F3 ----F2----- SI2',
16: '--F3--- --F2--- F1',
17: '----F3----- F2 F1',
18: '----F3----- F2 F1',
19: '------F3------- F2',
20: '--------F3---------'
}
CATCHER_RANGES = {
1: 'G3 ------SI1------',
2: 'G3 SPD ----SI1----',
3: '--G3--- SPD --SI1--',
4: 'G2 G3 --SPD-- SI1',
5: 'G2 --G3--- --SPD--',
6: '--G2--- G3 --SPD--',
7: 'PO G2 G3 --SPD--',
8: 'PO --G2--- G3 SPD',
9: '--PO--- G2 G3 SPD',
10: 'FO PO G2 G3 SPD',
11: 'FO --PO--- G2 G3',
12: '--FO--- PO G2 G3',
13: 'G1 FO PO G2 G3',
14: 'G1 --FO--- PO G2',
15: '--G1--- FO PO G2',
16: '--G1--- FO PO G2',
17: '----G1----- FO PO',
18: '----G1----- FO PO',
19: '----G1----- --FO---',
20: '------G1------- FO'
}
PITCHER_RANGES = {
1: 'G3 ------SI1------',
2: 'G3 ------SI1------',
3: '--G3--- ----SI1----',
4: '----G3----- --SI1--',
5: '------G3------- SI1',
6: '------G3------- SI1',
7: '--------G3---------',
8: 'G2 ------G3-------',
9: 'G2 ------G3-------',
10: 'G1 G2 ----G3-----',
11: 'G1 G2 ----G3-----',
12: 'G1 G2 ----G3-----',
13: '--G1--- G2 --G3---',
14: '--G1--- --G2--- G3',
15: '--G1--- ----G2-----',
16: '--G1--- ----G2-----',
17: '----G1----- --G2---',
18: '----G1----- --G2---',
19: '------G1------- G2',
20: '--------G1---------'
}
# ============================================================================
# ERROR CHARTS
# ============================================================================
FIRST_BASE_ERRORS = {
18: '2-base error for e3 -> e12, e19 -> e28\n1-base error for e1, e2, e30',
17: '2-base error for e13 -> e28\n1-base error for e1, e5, e8, e9, e29',
16: '2-base error for e29, e30\n1-base error for e2, e8, e16, e19, e23',
15: '1-base error for e3, e8, e10 -> e12, e20, e26, e30',
14: '1-base error for e4, e5, e9, e15, e18, e22, e24 -> e28',
13: '1-base error for e6, e13, e24, e26 -> e28, e30',
12: '1-base error for e14 -> e18, e21 -> e26, e28 -> e30',
11: '1-base error for e10, e13, e16 -> e20, e23 -> e25, e27 -> e30',
10: '1-base error for e19 -> e21, e23, e29',
9: '1-base error for e7, e12, e14, e21, e25, e26, e29',
8: '1-base error for e11, e27',
7: '1-base error for e9, e15, e22, e27, e28',
6: '1-base error for e8, e11, e12, e17, e20',
5: 'No error',
4: 'No error',
3: '2-base error for e8 -> e12, e24 -> e28\n1-base error for e2, e3, e6, e7, e14, e16, e17, e21'
}
SECOND_BASE_ERRORS = {
18: '2-base error for e4 -> e19, e28 -> e41, e53 -> e65\n1-base error for e22, e24, e25, e27, e44, e50',
17: '2-base error for e20 -> e41, e68, e71\n1-base error for e3, e4, e8 -> e12, e15, e16, e19',
16: '2-base error for e53 -> 71\n1-base error for e5 -> 10, e14, e16, e29, e37',
15: '1-base error for e11, e12, e14, e16, e17, e19, e26 -> e28, e30, e32, e37, e50 -> e62, e71',
14: '1-base error for e13, e15, e34, e47, e65',
13: '1-base error for e18, e20, e21, e26 -> e28, e39, e41, e50, e56, e59, e65, e71',
12: '1-base error for e22, e30, e34, e39, e44, e47, e53, e56, e62, e68, e71',
11: '1-base error for e23 -> e25, e29, e32, e37, e41, e50, e53, e59, e62, e68',
10: '1-base error for e68',
9: '1-base error for e44',
8: 'No error',
7: '1-base error for e47, e65',
6: '1-base error for e17, e19, e56 -> 62',
5: 'No error',
4: '1-base error for e10, e21',
3: '2-base error for e12 -> e19, e37 -> e41, e59 -> e65\n1-base error for e2 -> e4, e6, e20, e25, e28, e29'
}
THIRD_BASE_ERRORS = {
18: '2-base error for e11 -> e18, e32, e33, e37, e53, e62, e65\n1-base error for e4, e8, e19, e21, e22, e27, e41',
17: '2-base error for e3 -> e10, e17, e18, e25 -> e27, e34 -> e37, e44, e47\n1-base error for e11, e19, e32, e56',
16: '2-base error for e11 -> e18, e32, e33, e37, e53, e62, e65\n1-base error for e4, e8, e19, e21, e22, e27, e41',
15: '2-base error for e19 -> 27, e32, e33, e37, e39, e44, e50, e59\n1-base error for e5 -> e8, e11, e14, e15, e17, e18, e28 -> e31, e34',
14: '2-base error for e28 -> e31, e34, e35, e50\n1-base error for e14, e16, e19, e20, e22, e32, e39, e44, e56, e62',
13: '2-base error for e41, e47, e53, e59\n1-base error for e10, e15, e23, e25, e28, e30, e32, e33, e35, e44, e65',
12: '2-base error for e62\n1-base error for e12, e17, e22, e24, e27, e29, e34 -> e50, e56 -> e59, e65',
11: '2-base error for e56, e65\n1-base error for e13, e18, e20, e21, e23, e26, e28, e31 -> e33, e35, e37, e41 -> e53, e59',
10: '1-base error for e26, e31, e41, e53 -> 65',
9: '1-base error for e24, e27, e29, e34, e37, e39, e47 -> e65',
8: '1-base error for e25, e30, e33, e47, e53, e56, e62, e65',
7: '1-base error for e16, e19, e39, e59 -> e65',
6: '1-base error for e21, e25, e30, e34, e53',
5: 'No error',
4: '1-base error for e2, e3, e6, e14, e16, e44',
3: '2-base error for e10, e15, e16, e23, e24, e56\n1-base error for e1 -> e4, e8, e14'
}
SHORTSTOP_ERRORS = {
18: '2-base error for e4 -> e12, e22 -> e32, e40 -> e48, e64, e68\n1-base error for e1, e18, e34, e52, e56',
17: '2-base error for e14 -> 32, e52, e56, e72 -> e84\n1-base error for e3 -> e5, e8 ,e10, e36',
16: '2-base error for e33 -> 56, e72\n1-base error for e6 -> e10, e17, e18, e20, e28, e31, e88',
15: '2-base error for e60 -> e68, e76 -> 84\n1-base error for e12, e14, e17, e18, e20 -> e22, e24, e28, e31 -> 36, e40, e48, e72',
14: '1-base error for e16, e19, e38, e42, e60, e68',
13: '1-base error for e23, e25, e32 -> 38, e44, e52, e72 -> 84',
12: '1-base error for e26, e27, e30, e42, e48, e56, e64, e68, e76 -> e88',
11: '1-base error for e29, e40, e52 -> e60, e72, e80 -> e88',
10: '1-base error for e84',
9: '1-base error for e64, e68, e76, e88',
8: '1-base error for e44',
7: '1-base error for e60',
6: '1-base error for e21, e22, e24, e28, e31, e48, e64, e72',
5: 'No error',
4: '2-base error for e72\n1-base error for e14, e19, e20, e24, e25, e30, e31, e80',
3: '2-base error for e10, e12, e28 -> e32, e48, e84\n1-base error for e2, e5, e7, e23, e27'
}
CORNER_OUTFIELD_ERRORS = {
18: '3-base error for e4 -> e12, e19 -> e25\n2-base error for e18\n1-base error for e2, e3, e15',
17: '3-base error for e13 -> e25\n2-base error for e1, e6, e8, e10',
16: '2-base error for e2\n1-base error for e7 -> 12, e22, e24, e25',
15: '2-base error for e3, e4, e7, e8, e10, e11, e13, e20, e21',
14: '2-base error for e5, e6, e10, e12, e14, e15, e22, e23',
13: '2-base error for e11, e12, e16, e20, e24, e25',
12: '2-base error for e13 -> e18, e21 -> e23, e25',
11: '2-base error for e9, e18 -> e21, e23 -> e25',
10: '2-base error for e19',
9: '2-base error for e22',
8: '2-base error for e24',
7: '1-base error for e19 -> e21, e23',
6: '2-base error for e7, e8\n1-base error for e13 -> e18, e22, e24, e25',
5: 'No error',
4: '2-base error for e1, e5, e6, e9\n1-base error for e14 -> e16, e20 -> e23',
3: '3-base error for e16 -> e25\n2-base error for e1, e3, e4, e7, e9, e11\n1-base error for e17'
}
CENTER_FIELD_ERRORS = {
18: '3-base error for e4 -> e19\n2-base error for e2, e25\n1-base error for e3, e23',
17: '3-base error for e20 -> e25\n2-base error for e1, e2, e5, e7, e9, e13 -> e15, e17',
16: '2-base error for e3 -> e5, e8, e23\n1-base error for e10 -> e18',
15: '2-base error for e6 -> e8, e12, e13, e19',
14: '2-base error for e9, e10, e16 -> e18, e20 -> e23',
13: '2-base error for e11, e18, e20, e23 -> e25',
12: '2-base error for e14, e15, e21, e22, e24',
11: '2-base error for e19, e25',
10: 'No error',
9: 'No error',
8: 'No error',
7: '2-base error for e16, e17',
6: '2-base error for e12, e13\n1-base error for e19 -> e25',
5: 'No error',
4: '2-base error for e10, e12, e13, e20, e22, e23\n1-base error for e3 -> e9, e15 -> e18',
3: '3-base error for e12 -> e19\n2-base error for e10, e11\n1-base error for e2, e3, e7 -> e9, e21 -> e23'
}
CATCHER_ERRORS = {
18: '2-base error for e4 -> 16\n1-base error for e2, e3',
17: '1-base error for e1, e2, e4, e5, e12 -> e14, e16',
16: '1-base error for e3 -> e5, e7, e12 -> e14, e16',
15: '1-base error for e7, e8, e12, e13, e15',
14: '1-base error for e6',
13: '1-base error for e9',
12: '1-base error for e10, e14',
11: '1-base error for e11, e15',
10: 'No error',
9: 'No error',
8: 'No error',
7: '1-base error for e16',
6: '1-base error for e8, e12, e13',
5: 'No error',
4: '1-base error for e5, e13',
3: '2-base error for e12 -> e16\n1-base error for e2, e3, e7, e11'
}
PITCHER_ERRORS = {
18: '2-base error for e4 -> e12, e19 -> e28, e34 -> e43, e46 -> e48',
17: '2-base error for e13 -> e28, e44 -> e50',
16: '2-base error for e30 -> e48, e50, e51\n1-base error for e8, e11, e16, e23',
15: '2-base error for e50, e51\n1-base error for e10 -> e12, e19, e20, e24, e26, e30, e35, e38, e40, e46, e47',
14: '1-base error for e4, e14, e18, e21, e22, e26, e31, e35, e42, e43, e48 -> e51',
13: '1-base error for e6, e13, e14, e21, e22, e26, e27, e30 -> 34, e38 -> e51',
12: '1-base error for e7, e11, e12, e15 -> e19, e22 -> e51',
11: '1-base error for e10, e13, e15, e17, e18, e20, e21, e23, e24, e27 -> 38, e40, e42, e44 -> e51',
10: '1-base error for e20, e23, e24, e27 -> e51',
9: '1-base error for e16, e19, e26, e28, e34 -> e36, e39 -> e51',
8: '1-base error for e22, e33, e38, e39, e43 -> e51',
7: '1-base error for e14, e21, e36, e39, e42 -> e44, e47 -> e51',
6: '1-base error for e8, e22, e38, e39, e43 -> e51',
5: 'No error',
4: '1-base error for e15, e16, e40',
3: '2-base error for e8 -> e12, e26 -> e28, e39 -> e43\n1-base error for e2, e3, e7, e14, e15'
}

View File

@ -19,6 +19,22 @@ from utils.decorators import logged_command
from utils.team_utils import get_user_major_league_team
from utils.text_utils import split_text_for_fields
from views.embeds import EmbedColors, EmbedTemplate
from .chart_data import (
INFIELD_X_CHART,
OUTFIELD_X_CHART,
INFIELD_RANGES,
OUTFIELD_RANGES,
CATCHER_RANGES,
PITCHER_RANGES,
FIRST_BASE_ERRORS,
SECOND_BASE_ERRORS,
THIRD_BASE_ERRORS,
SHORTSTOP_ERRORS,
CORNER_OUTFIELD_ERRORS,
CENTER_FIELD_ERRORS,
CATCHER_ERRORS,
PITCHER_ERRORS,
)
@dataclass
@ -30,128 +46,6 @@ class DiceRoll:
rolls: list[int]
total: int
INFIELD_X_CHART = {
'si1': {
'rp': 'Runner on first: Line drive hits the runner! Runner on first is out. Batter goes to first with single '
'and all other runners hold.\nNo runner on first: batter singles, runners advance 1 base.',
'e1': 'Single and Error, batter to second, runners advance 2 bases.',
'e2': 'Single and Error, batter to third, all runners score.',
'no': 'Single, runners advance 1 base.'
},
'spd': {
'rp': 'No effect; proceed with speed check',
'e1': 'Single and Error, batter to second, runners advance 2 bases.',
'e2': 'Single and Error, batter to third, all runners score.',
'no': 'Speed check, safe range equals batter\'s running rating, SI* result if safe, gb C if out'
},
'po': {
'rp': 'The batters hits a popup. None of the fielders take charge on the play and the ball drops in the '
'infield for a single! All runners advance 1 base.',
'e1': 'The catcher drops a popup for an error. All runners advance 1 base.',
'e2': 'The catcher grabs a squib in front of the plate and throws it into right field. The batter goes to '
'second and all runners score.',
'no': 'The batter pops out to the catcher.'
},
'wp': {
'rp': 'Automatic wild pitch. Catcher has trouble finding it and all base runners advance 2 bases.',
'no': 'Automatic wild pitch, all runners advance 1 base and batter rolls AB again.'
},
'x': {
'rp': 'Runner(s) on base: pitcher trips during his delivery and the ball sails for automatic wild pitch, '
'runners advance 1 base and batter rolls AB again.',
'no': 'Wild pitch check (credited as a PB). If a passed ball occurs, batter rerolls AB. '
'If no passed ball occurs, the batter fouls out to the catcher.'
},
'fo': {
'rp': 'Batter swings and misses, but is awarded first base on a catcher interference call! Baserunners advance '
'only if forced.',
'e1': 'The catcher drops a foul popup for an error. Batter rolls AB again.',
'e2': 'The catcher drops a foul popup for an error. Batter rolls AB again.',
'no': 'Runner(s) on base: make a passed ball check. If no passed ball, batter pops out to the catcher. If a '
'passed ball occurs, batter roll his AB again.\nNo runners: batter pops out to the catcher'
},
'g1': {
'rp': 'Runner on first: runner on first breaks up the double play, but umpires call runner interference and '
'the batter is out on GIDP.\nNo runners: Batter grounds out.',
'e1': 'Error, batter to first, runners advance 1 base.',
'e2': 'Error, batter to second, runners advance 2 bases.',
'no': 'Consult Groundball Chart: `!gbA`'
},
'g2': {
'rp': 'Batter lines the ball off the pitcher to the fielder who makes the play to first for the out! Runners '
'advance only if forced.',
'e1': 'Error, batter to first, runners advance 1 base.',
'e2': 'Error, batter to second, runners advance 2 bases.',
'no': 'Consult Groundball Chart: `!gbB`'
},
'g3': {
'rp': 'Batter lines the ball off the mound and deflects to the fielder who makes the play to first for the '
'out! Runners advance 1 base.',
'e1': 'Error, batter to first, runners advance 1 base.',
'e2': 'Error, batter to second, runners advance 2 bases.',
'no': 'Consult Groundball Chart: `!gbC`'
},
}
OUTFIELD_X_CHART = {
'si2': {
'rp': 'Batter singles, baserunners advance 2 bases. As the batter rounds first, the fielder throws behind him '
'and catches him off the bag for an out!',
'e1': 'Single and error, batter to second, runners advance 2 bases.',
'e2': 'Single and error, batter to third, all runners score.',
'e3': 'Single and error, batter to third, all runners score',
'no': 'Single, all runners advance 2 bases.'
},
'do2': {
'rp': 'Batter doubles, runners advance 2 bases. The outfielder throws the ball to the shortstop who executes a '
'hidden ball trick! Runner on second is called out!',
'e1': 'Double and error, batter to third, all runners score.',
'e2': 'Double and error, batter to third, and all runners score.',
'e3': 'Double and error, batter and all runners score. Little league home run!',
'no': 'Double, all runners advance 2 bases.'
},
'do3': {
'rp': 'Runner(s) on base: batter doubles and runners advance three bases as the outfielders collide!\n'
'No runners: Batter doubles, but the play is appealed. The umps rule the batter missed first base so is '
'out on the appeal!',
'e1': 'Double and error, batter to third, all runners score.',
'e2': 'Double and error, batter and all runners score. Little league home run!',
'e3': 'Double and error, batter and all runners score. Little league home run!',
'no': 'Double, all runners score.'
},
'tr3': {
'rp': 'Batter hits a ball into the gap and the outfielders collide trying to make the play! The ball rolls to '
'the wall and the batter trots home with an inside-the-park home run!',
'e1': 'Triple and error, batter and all runners score. Little league home run!',
'e2': 'Triple and error, batter and all runners score. Little league home run!',
'e3': 'Triple and error, batter and all runners score. Little league home run!',
'no': 'Triple, all runners score.'
},
'f1': {
'rp': 'The outfielder races back and makes a diving catch and collides with the wall! In the time he takes to '
'recuperate, all baserunners tag-up and advance 2 bases.',
'e1': '1 base error, runners advance 1 base.',
'e2': '2 base error, runners advance 2 bases.',
'e3': '3 base error, batter to third, all runners score.',
'no': 'Flyball A'
},
'f2': {
'rp': 'The outfielder catches the flyball for an out. If there is a runner on third, he tags-up and scores. '
'The play is appealed and the umps rule that the runner left early and is out on the appeal!',
'e1': '1 base error, runners advance 1 base.',
'e2': '2 base error, runners advance 2 bases.',
'e3': '3 base error, batter to third, all runners score.',
'no': 'Flyball B'
},
'f3': {
'rp': 'The outfielder makes a running catch in the gap! The lead runner lost track of the ball and was '
'advancing - he cannot return in time and is doubled off by the outfielder.',
'e1': '1 base error, runners advance 1 base.',
'e2': '2 base error, runners advance 2 bases.',
'e3': '3 base error, batter to third, all runners score.',
'no': 'Flyball C'
}
}
class DiceRollCommands(commands.Cog):
"""Dice rolling command handlers for gameplay."""
@ -620,107 +514,19 @@ class DiceRollCommands(commands.Cog):
def _get_infield_range(self, d20_roll: int) -> str:
"""Get infield range result based on d20 roll."""
infield_ranges = {
1: 'G3# SI1 ----SI2----',
2: 'G2# SI1 ----SI2----',
3: 'G2# G3# SI1 --SI2--',
4: 'G2# G3# SI1 --SI2--',
5: 'G1 --G3#-- SI1 SI2',
6: 'G1 G2# G3# SI1 SI2',
7: 'G1 G2 --G3#-- SI1',
8: 'G1 G2 --G3#-- SI1',
9: 'G1 G2 G3 --G3#--',
10: '--G1--- G2 --G3#--',
11: '--G1--- G2 G3 G3#',
12: '--G1--- G2 G3 G3#',
13: '--G1--- G2 --G3---',
14: '--G1--- --G2--- G3',
15: '----G1----- G2 G3',
16: '----G1----- G2 G3',
17: '------G1------- G3',
18: '------G1------- G2',
19: '------G1------- G2',
20: '--------G1---------'
}
return infield_ranges.get(d20_roll, 'Unknown')
return INFIELD_RANGES.get(d20_roll, 'Unknown')
def _get_outfield_range(self, d20_roll: int) -> str:
"""Get outfield range result based on d20 roll."""
outfield_ranges = {
1: 'F1 DO2 DO3 --TR3--',
2: 'F2 SI2 DO2 DO3 TR3',
3: 'F2 SI2 --DO2-- DO3',
4: 'F2 F1 SI2 DO2 DO3',
5: '--F2--- --SI2-- DO2',
6: '--F2--- --SI2-- DO2',
7: '--F2--- F1 SI2 DO2',
8: '--F2--- F1 --SI2--',
9: '----F2----- --SI2--',
10: '----F2----- --SI2--',
11: '----F2----- --SI2--',
12: '----F2----- F1 SI2',
13: '----F2----- F1 SI2',
14: 'F3 ----F2----- SI2',
15: 'F3 ----F2----- SI2',
16: '--F3--- --F2--- F1',
17: '----F3----- F2 F1',
18: '----F3----- F2 F1',
19: '------F3------- F2',
20: '--------F3---------'
}
return outfield_ranges.get(d20_roll, 'Unknown')
return OUTFIELD_RANGES.get(d20_roll, 'Unknown')
def _get_catcher_range(self, d20_roll: int) -> str:
"""Get catcher range result based on d20 roll."""
catcher_ranges = {
1: 'G3 ------SI1------',
2: 'G3 SPD ----SI1----',
3: '--G3--- SPD --SI1--',
4: 'G2 G3 --SPD-- SI1',
5: 'G2 --G3--- --SPD--',
6: '--G2--- G3 --SPD--',
7: 'PO G2 G3 --SPD--',
8: 'PO --G2--- G3 SPD',
9: '--PO--- G2 G3 SPD',
10: 'FO PO G2 G3 SPD',
11: 'FO --PO--- G2 G3',
12: '--FO--- PO G2 G3',
13: 'G1 FO PO G2 G3',
14: 'G1 --FO--- PO G2',
15: '--G1--- FO PO G2',
16: '--G1--- FO PO G2',
17: '----G1----- FO PO',
18: '----G1----- FO PO',
19: '----G1----- --FO---',
20: '------G1------- FO'
}
return catcher_ranges.get(d20_roll, 'Unknown')
return CATCHER_RANGES.get(d20_roll, 'Unknown')
def _get_pitcher_range(self, d20_roll: int) -> str:
"""Get pitcher range result based on d20 roll."""
pitcher_ranges = {
1: 'G3 ------SI1------',
2: 'G3 ------SI1------',
3: '--G3--- ----SI1----',
4: '----G3----- --SI1--',
5: '------G3------- SI1',
6: '------G3------- SI1',
7: '--------G3---------',
8: 'G2 ------G3-------',
9: 'G2 ------G3-------',
10: 'G1 G2 ----G3-----',
11: 'G1 G2 ----G3-----',
12: 'G1 G2 ----G3-----',
13: '--G1--- G2 --G3---',
14: '--G1--- --G2--- G3',
15: '--G1--- ----G2-----',
16: '--G1--- ----G2-----',
17: '----G1----- --G2---',
18: '----G1----- --G2---',
19: '------G1------- G2',
20: '--------G1---------'
}
return pitcher_ranges.get(d20_roll, 'Unknown')
return PITCHER_RANGES.get(d20_roll, 'Unknown')
def _get_rare_play(self, position: str, d20_total: int) -> str:
"""Get the rare play result for a position and d20 total"""
@ -767,179 +573,35 @@ class DiceRollCommands(commands.Cog):
def _get_3b_error(self, d6_total: int) -> str:
"""Get 3B error result based on 3d6 total."""
errors = {
18: '2-base error for e11 -> e18, e32, e33, e37, e53, e62, e65\n1-base error for e4, e8, e19, e21, e22, e27, e41',
17: '2-base error for e3 -> e10, e17, e18, e25 -> e27, e34 -> e37, e44, e47\n1-base error for e11, e19, e32, e56',
16: '2-base error for e11 -> e18, e32, e33, e37, e53, e62, e65\n1-base error for e4, e8, e19, e21, e22, e27, e41',
15: '2-base error for e19 -> 27, e32, e33, e37, e39, e44, e50, e59\n1-base error for e5 -> e8, e11, e14, e15, e17, e18, e28 -> e31, e34',
14: '2-base error for e28 -> e31, e34, e35, e50\n1-base error for e14, e16, e19, e20, e22, e32, e39, e44, e56, e62',
13: '2-base error for e41, e47, e53, e59\n1-base error for e10, e15, e23, e25, e28, e30, e32, e33, e35, e44, e65',
12: '2-base error for e62\n1-base error for e12, e17, e22, e24, e27, e29, e34 -> e50, e56 -> e59, e65',
11: '2-base error for e56, e65\n1-base error for e13, e18, e20, e21, e23, e26, e28, e31 -> e33, e35, e37, e41 -> e53, e59',
10: '1-base error for e26, e31, e41, e53 -> 65',
9: '1-base error for e24, e27, e29, e34, e37, e39, e47 -> e65',
8: '1-base error for e25, e30, e33, e47, e53, e56, e62, e65',
7: '1-base error for e16, e19, e39, e59 -> e65',
6: '1-base error for e21, e25, e30, e34, e53',
5: 'No error',
4: '1-base error for e2, e3, e6, e14, e16, e44',
3: '2-base error for e10, e15, e16, e23, e24, e56\n1-base error for e1 -> e4, e8, e14'
}
return errors.get(d6_total, 'No error')
return THIRD_BASE_ERRORS.get(d6_total, 'No error')
def _get_1b_error(self, d6_total: int) -> str:
"""Get 1B error result based on 3d6 total."""
errors = {
18: '2-base error for e3 -> e12, e19 -> e28\n1-base error for e1, e2, e30',
17: '2-base error for e13 -> e28\n1-base error for e1, e5, e8, e9, e29',
16: '2-base error for e29, e30\n1-base error for e2, e8, e16, e19, e23',
15: '1-base error for e3, e8, e10 -> e12, e20, e26, e30',
14: '1-base error for e4, e5, e9, e15, e18, e22, e24 -> e28',
13: '1-base error for e6, e13, e24, e26 -> e28, e30',
12: '1-base error for e14 -> e18, e21 -> e26, e28 -> e30',
11: '1-base error for e10, e13, e16 -> e20, e23 -> e25, e27 -> e30',
10: '1-base error for e19 -> e21, e23, e29',
9: '1-base error for e7, e12, e14, e21, e25, e26, e29',
8: '1-base error for e11, e27',
7: '1-base error for e9, e15, e22, e27, e28',
6: '1-base error for e8, e11, e12, e17, e20',
5: 'No error',
4: 'No error',
3: '2-base error for e8 -> e12, e24 -> e28\n1-base error for e2, e3, e6, e7, e14, e16, e17, e21'
}
return errors.get(d6_total, 'No error')
return FIRST_BASE_ERRORS.get(d6_total, 'No error')
def _get_2b_error(self, d6_total: int) -> str:
"""Get 2B error result based on 3d6 total."""
errors = {
18: '2-base error for e4 -> e19, e28 -> e41, e53 -> e65\n1-base error for e22, e24, e25, e27, e44, e50',
17: '2-base error for e20 -> e41, e68, e71\n1-base error for e3, e4, e8 -> e12, e15, e16, e19',
16: '2-base error for e53 -> 71\n1-base error for e5 -> 10, e14, e16, e29, e37',
15: '1-base error for e11, e12, e14, e16, e17, e19, e26 -> e28, e30, e32, e37, e50 -> e62, e71',
14: '1-base error for e13, e15, e34, e47, e65',
13: '1-base error for e18, e20, e21, e26 -> e28, e39, e41, e50, e56, e59, e65, e71',
12: '1-base error for e22, e30, e34, e39, e44, e47, e53, e56, e62, e68, e71',
11: '1-base error for e23 -> e25, e29, e32, e37, e41, e50, e53, e59, e62, e68',
10: '1-base error for e68',
9: '1-base error for e44',
8: 'No error',
7: '1-base error for e47, e65',
6: '1-base error for e17, e19, e56 -> 62',
5: 'No error',
4: '1-base error for e10, e21',
3: '2-base error for e12 -> e19, e37 -> e41, e59 -> e65\n1-base error for e2 -> e4, e6, e20, e25, e28, e29'
}
return errors.get(d6_total, 'No error')
return SECOND_BASE_ERRORS.get(d6_total, 'No error')
def _get_ss_error(self, d6_total: int) -> str:
"""Get SS error result based on 3d6 total."""
errors = {
18: '2-base error for e4 -> e12, e22 -> e32, e40 -> e48, e64, e68\n1-base error for e1, e18, e34, e52, e56',
17: '2-base error for e14 -> 32, e52, e56, e72 -> e84\n1-base error for e3 -> e5, e8 ,e10, e36',
16: '2-base error for e33 -> 56, e72\n1-base error for e6 -> e10, e17, e18, e20, e28, e31, e88',
15: '2-base error for e60 -> e68, e76 -> 84\n1-base error for e12, e14, e17, e18, e20 -> e22, e24, e28, e31 -> 36, e40, e48, e72',
14: '1-base error for e16, e19, e38, e42, e60, e68',
13: '1-base error for e23, e25, e32 -> 38, e44, e52, e72 -> 84',
12: '1-base error for e26, e27, e30, e42, e48, e56, e64, e68, e76 -> e88',
11: '1-base error for e29, e40, e52 -> e60, e72, e80 -> e88',
10: '1-base error for e84',
9: '1-base error for e64, e68, e76, e88',
8: '1-base error for e44',
7: '1-base error for e60',
6: '1-base error for e21, e22, e24, e28, e31, e48, e64, e72',
5: 'No error',
4: '2-base error for e72\n1-base error for e14, e19, e20, e24, e25, e30, e31, e80',
3: '2-base error for e10, e12, e28 -> e32, e48, e84\n1-base error for e2, e5, e7, e23, e27'
}
return errors.get(d6_total, 'No error')
return SHORTSTOP_ERRORS.get(d6_total, 'No error')
def _get_corner_of_error(self, d6_total: int) -> str:
"""Get LF/RF error result based on 3d6 total."""
errors = {
18: '3-base error for e4 -> e12, e19 -> e25\n2-base error for e18\n1-base error for e2, e3, e15',
17: '3-base error for e13 -> e25\n2-base error for e1, e6, e8, e10',
16: '2-base error for e2\n1-base error for e7 -> 12, e22, e24, e25',
15: '2-base error for e3, e4, e7, e8, e10, e11, e13, e20, e21',
14: '2-base error for e5, e6, e10, e12, e14, e15, e22, e23',
13: '2-base error for e11, e12, e16, e20, e24, e25',
12: '2-base error for e13 -> e18, e21 -> e23, e25',
11: '2-base error for e9, e18 -> e21, e23 -> e25',
10: '2-base error for e19',
9: '2-base error for e22',
8: '2-base error for e24',
7: '1-base error for e19 -> e21, e23',
6: '2-base error for e7, e8\n1-base error for e13 -> e18, e22, e24, e25',
5: 'No error',
4: '2-base error for e1, e5, e6, e9\n1-base error for e14 -> e16, e20 -> e23',
3: '3-base error for e16 -> e25\n2-base error for e1, e3, e4, e7, e9, e11\n1-base error for e17'
}
return errors.get(d6_total, 'No error')
return CORNER_OUTFIELD_ERRORS.get(d6_total, 'No error')
def _get_cf_error(self, d6_total: int) -> str:
"""Get CF error result based on 3d6 total."""
errors = {
18: '3-base error for e8 -> e16, e24 -> e32\n2-base error for e1, e2, e40\n1-base error for e17, e19, e21, e36',
17: '3-base error for e17 -> e32, e34, e36, e38\n2-base error for e3 -> e7, e10, e12, e14, e22',
16: '2-base error for e1, e2, e4, e8 -> e12, e17, e19, e24, e26, e28, e32, e34',
15: '2-base error for e5 -> e8, e13, e15 -> e19, e21, e24, e28, e30, e36, e38, e40',
14: '2-base error for e9 -> e11, e14, e20, e22, e26, e30, e34, e38',
13: '2-base error for e12 -> e21, e23, e25, e26, e32, e36, e40',
12: '2-base error for e22 -> e25, e27 -> e32, e34 -> e40',
11: '2-base error for e26, e27, e29 -> e34, e36 -> e40',
10: '2-base error for e28',
9: '2-base error for e29',
8: '2-base error for e30',
7: '1-base error for e27, e28, e31, e32, e35',
6: '2-base error for e15, e16\n1-base error for e23 -> e32, e34 -> e40',
5: 'No error',
4: '2-base error for e9 -> e13, e17, e19 -> e21\n1-base error for e24, e25, e29 -> e38',
3: '3-base error for e24 -> e32, e36 -> e40\n2-base error for e1 -> e8, e10, e14\n1-base error for e15'
}
return errors.get(d6_total, 'No error')
return CENTER_FIELD_ERRORS.get(d6_total, 'No error')
def _get_catcher_error(self, d6_total: int) -> str:
"""Get Catcher error result based on 3d6 total."""
errors = {
18: '2-base error for e4 -> 16\n1-base error for e2, e3',
17: '1-base error for e1, e2, e4, e5, e12 -> e14, e16',
16: '1-base error for e3 -> e5, e7, e12 -> e14, e16',
15: '1-base error for e7, e8, e12, e13, e15',
14: '1-base error for e6',
13: '1-base error for e9',
12: '1-base error for e10, e14',
11: '1-base error for e11, e15',
10: 'No error',
9: 'No error',
8: 'No error',
7: '1-base error for e16',
6: '1-base error for e8, e12, e13',
5: 'No error',
4: '1-base error for e5, e13',
3: '2-base error for e12 -> e16\n1-base error for e2, e3, e7, e11'
}
return errors.get(d6_total, 'No error')
return CATCHER_ERRORS.get(d6_total, 'No error')
def _get_pitcher_error(self, d6_total: int) -> str:
"""Get Pitcher error result based on 3d6 total."""
errors = {
18: '2-base error for e4 -> e12, e19 -> e28, e34 -> e43, e46 -> e48',
17: '2-base error for e13 -> e28, e44 -> e50',
16: '2-base error for e30 -> e48, e50, e51\n1-base error for e8, e11, e16, e23',
15: '2-base error for e50, e51\n1-base error for e10 -> e12, e19, e20, e24, e26, e30, e35, e38, e40, e46, e47',
14: '1-base error for e4, e14, e18, e21, e22, e26, e31, e35, e42, e43, e48 -> e51',
13: '1-base error for e6, e13, e14, e21, e22, e26, e27, e30 -> 34, e38 -> e51',
12: '1-base error for e7, e11, e12, e15 -> e19, e22 -> e51',
11: '1-base error for e10, e13, e15, e17, e18, e20, e21, e23, e24, e27 -> 38, e40, e42, e44 -> e51',
10: '1-base error for e20, e23, e24, e27 -> e51',
9: '1-base error for e16, e19, e26, e28, e34 -> e36, e39 -> e51',
8: '1-base error for e22, e33, e38, e39, e43 -> e51',
7: '1-base error for e14, e21, e36, e39, e42 -> e44, e47 -> e51',
6: '1-base error for e8, e22, e38, e39, e43 -> e51',
5: 'No error',
4: '1-base error for e15, e16, e40',
3: '2-base error for e8 -> e12, e26 -> e28, e39 -> e43\n1-base error for e2, e3, e7, e14, e15'
}
return errors.get(d6_total, 'No error')
return PITCHER_ERRORS.get(d6_total, 'No error')
def _get_pitcher_rare_play(self, d20_total: int) -> str:
return (

View File

@ -71,27 +71,46 @@ class Team(SBABaseModel):
return super().from_api_data(team_data)
def roster_type(self) -> RosterType:
"""Determine the roster type based on team abbreviation and name."""
"""
Determine the roster type based on team abbreviation.
CRITICAL: Handles edge cases like "BHMIL" (BHM + IL) vs "NYYMIL" (NYY + MIL).
Uses sname to disambiguate when abbreviation ends in "MIL".
Returns:
RosterType based on team abbreviation pattern
Examples:
- "NYY" (3 chars) MAJOR_LEAGUE
- "NYYMIL" with sname "RailRiders" MINOR_LEAGUE
- "BHMIL" with sname "Iron IL" INJURED_LIST (BHM + IL, not BH + MIL)
- "NYYIL" INJURED_LIST
"""
if len(self.abbrev) <= 3:
return RosterType.MAJOR_LEAGUE
# Use sname as the definitive source of truth for IL teams
# If "IL" is in sname and abbrev ends in "IL" → Injured List
if self.abbrev.upper().endswith('IL') and 'IL' in self.sname:
return RosterType.INJURED_LIST
abbrev_upper = self.abbrev.upper()
# If abbrev ends with "MiL" (exact case) and "IL" not in sname → Minor League
if self.abbrev.endswith('MiL') and 'IL' not in self.sname:
# Handle ambiguous "MIL" ending which could be:
# 1. [Team] + "MIL" (Minor League) - e.g., "NYYMIL"
# 2. [Team ending in M] + "IL" (Injured List) - e.g., "BHMIL" = "BHM" + "IL"
if abbrev_upper.endswith('MIL'):
# Use sname to disambiguate - IL teams have "IL" in their name
# But check for word boundaries to avoid matching "Island", "Illinois", etc.
if (self.sname.endswith('IL') or
self.sname.endswith(' IL') or
' IL ' in self.sname):
# This is actually a [Team]IL roster (e.g., "BHMIL" = "BHM" + "IL")
return RosterType.INJURED_LIST
# Otherwise it's a genuine Minor League team (e.g., "NYYMIL" = "NYY" + "MIL")
return RosterType.MINOR_LEAGUE
# Handle other patterns
abbrev_lower = self.abbrev.lower()
if abbrev_lower.endswith('mil'):
return RosterType.MINOR_LEAGUE
elif abbrev_lower.endswith('il'):
# Check for Injured List (2-char suffix "IL" that doesn't end in "MIL")
if abbrev_upper.endswith('IL'):
return RosterType.INJURED_LIST
else:
return RosterType.MAJOR_LEAGUE
# Default to Major League for any other pattern
return RosterType.MAJOR_LEAGUE
def _get_base_abbrev(self) -> str:
"""

View File

@ -86,6 +86,92 @@ class TestTeamModel:
team = Team(id=3, abbrev='SD', sname='Padres', lname='San Diego Padres', season=12)
assert str(team) == 'SD - San Diego Padres'
def test_team_roster_type_major_league(self):
"""Test roster type detection for Major League teams."""
from models.team import RosterType
# 3 chars or less → Major League
team = Team(id=1, abbrev='NYY', sname='Yankees', lname='New York Yankees', season=12)
assert team.roster_type() == RosterType.MAJOR_LEAGUE
team = Team(id=2, abbrev='BOS', sname='Red Sox', lname='Boston Red Sox', season=12)
assert team.roster_type() == RosterType.MAJOR_LEAGUE
# Even "BHM" (ends in M) should be Major League
team = Team(id=3, abbrev='BHM', sname='Iron', lname='Birmingham Iron', season=12)
assert team.roster_type() == RosterType.MAJOR_LEAGUE
def test_team_roster_type_minor_league(self):
"""Test roster type detection for Minor League teams."""
from models.team import RosterType
# Standard Minor League: [Team] + "MIL"
team = Team(id=4, abbrev='NYYMIL', sname='RailRiders', lname='Staten Island RailRiders', season=12)
assert team.roster_type() == RosterType.MINOR_LEAGUE
team = Team(id=5, abbrev='PORMIL', sname='Portland MiL', lname='Portland Minor League', season=12)
assert team.roster_type() == RosterType.MINOR_LEAGUE
# Case insensitive
team = Team(id=6, abbrev='LAAmil', sname='Bees', lname='Salt Lake Bees', season=12)
assert team.roster_type() == RosterType.MINOR_LEAGUE
def test_team_roster_type_injured_list(self):
"""Test roster type detection for Injured List teams."""
from models.team import RosterType
# Standard Injured List: [Team] + "IL"
team = Team(id=7, abbrev='NYYIL', sname='Yankees IL', lname='New York Yankees IL', season=12)
assert team.roster_type() == RosterType.INJURED_LIST
team = Team(id=8, abbrev='PORIL', sname='Loggers IL', lname='Portland Loggers IL', season=12)
assert team.roster_type() == RosterType.INJURED_LIST
# Case insensitive
team = Team(id=9, abbrev='LAAil', sname='Angels IL', lname='Los Angeles Angels IL', season=12)
assert team.roster_type() == RosterType.INJURED_LIST
def test_team_roster_type_edge_case_bhmil(self):
"""
Test critical edge case: "BHMIL" should be Injured List, not Minor League.
This is BHM (Birmingham, ends in M) + IL (Injured List).
NOT BH + MIL (Minor League).
Bug history: Originally failed because "BHMIL" ends with "MIL", so it was
incorrectly classified as Minor League.
"""
from models.team import RosterType
# "BHMIL" = "BHM" + "IL" → sname contains "IL" → INJURED_LIST
team = Team(id=10, abbrev='BHMIL', sname='Iron IL', lname='Birmingham Iron IL', season=12)
assert team.roster_type() == RosterType.INJURED_LIST
# Compare with a real Minor League team that has "Island" in name
# "NYYMIL" = "NYY" + "MIL", even though sname has "Island" → MINOR_LEAGUE
team = Team(id=11, abbrev='NYYMIL', sname='Staten Island RailRiders', lname='Staten Island RailRiders', season=12)
assert team.roster_type() == RosterType.MINOR_LEAGUE
# Another IL edge case with sname containing "IL" at word boundary
team = Team(id=12, abbrev='WVMIL', sname='WV IL', lname='West Virginia IL', season=12)
assert team.roster_type() == RosterType.INJURED_LIST
def test_team_roster_type_sname_disambiguation(self):
"""Test that sname is used correctly to disambiguate MIL vs IL."""
from models.team import RosterType
# MiL team - sname does NOT have "IL" as a word
team = Team(id=13, abbrev='WVMIL', sname='Miners', lname='West Virginia Miners', season=12)
assert team.roster_type() == RosterType.MINOR_LEAGUE
# IL team - sname has "IL" at word boundary
team = Team(id=14, abbrev='WVMIL', sname='Miners IL', lname='West Virginia Miners IL', season=12)
assert team.roster_type() == RosterType.INJURED_LIST
# MiL team - sname has "IL" but only in "Island" (substring, not word boundary)
team = Team(id=15, abbrev='CHIMIL', sname='Island Hoppers', lname='Chicago Island Hoppers', season=12)
assert team.roster_type() == RosterType.MINOR_LEAGUE
class TestPlayerModel:
"""Test Player model functionality."""