Compare commits
10 Commits
b4d84b6125
...
cdfe54cdf7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdfe54cdf7 | ||
|
|
5496c96b32 | ||
|
|
4bd5a0b786 | ||
|
|
bbb4233b45 | ||
|
|
cd8cf0aee8 | ||
|
|
1aaf4ccb50 | ||
|
|
bb3894d6f1 | ||
|
|
4e71c33344 | ||
|
|
2b4b84e193 | ||
|
|
9bb84ce287 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -60,3 +60,5 @@ card-creation/
|
|||||||
.dockerignore
|
.dockerignore
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
venv/
|
venv/
|
||||||
|
CLAUDE.md
|
||||||
|
/.claude**
|
||||||
|
|||||||
@ -34,4 +34,12 @@ class Player(pydantic.BaseModel):
|
|||||||
strat_code: Optional[str] = None
|
strat_code: Optional[str] = None
|
||||||
bbref_id: Optional[str] = None
|
bbref_id: Optional[str] = None
|
||||||
injury_rating: Optional[str] = None
|
injury_rating: Optional[str] = None
|
||||||
sbaplayer_id: Optional[int] = None
|
sbaplayer_id: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_one_player(player_id: int) -> Player:
|
||||||
|
data = await db_get('players', object_id=player_id)
|
||||||
|
if not data:
|
||||||
|
log_exception(ApiException(f'No player found with ID {player_id}'))
|
||||||
|
|
||||||
|
return Player(**data)
|
||||||
|
|||||||
@ -22,4 +22,5 @@ class Team(pydantic.BaseModel):
|
|||||||
thumbnail: Optional[str] = None
|
thumbnail: Optional[str] = None
|
||||||
color: Optional[str] = None
|
color: Optional[str] = None
|
||||||
dice_color: Optional[str] = None
|
dice_color: Optional[str] = None
|
||||||
season: int
|
season: int
|
||||||
|
salary_cap: Optional[float] = None
|
||||||
@ -9,7 +9,7 @@ from api_calls.draft_pick import DraftPick, get_one_draftpick, patch_draftpick
|
|||||||
from api_calls.player import Player
|
from api_calls.player import Player
|
||||||
from exceptions import ApiException, log_exception
|
from exceptions import ApiException, log_exception
|
||||||
from helpers import *
|
from helpers import *
|
||||||
from db_calls import db_get, db_patch, put_player, db_post, get_player_by_name
|
from db_calls import db_delete, db_get, db_patch, put_player, db_post, get_player_by_name
|
||||||
from discord.ext import commands, tasks
|
from discord.ext import commands, tasks
|
||||||
|
|
||||||
from discord import TextChannel, app_commands
|
from discord import TextChannel, app_commands
|
||||||
@ -215,7 +215,7 @@ class Draft(commands.Cog):
|
|||||||
|
|
||||||
for x in p_query['players']:
|
for x in p_query['players']:
|
||||||
if count < 5:
|
if count < 5:
|
||||||
core_players_string += f'{x["pos_1"]} {x["name"]} ({x["wara"]})\n'
|
core_players_string += f'{x["pos_1"]} {x["name"]} ({x["wara"]:.2f})\n'
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
count += 1
|
count += 1
|
||||||
@ -439,16 +439,18 @@ class Draft(commands.Cog):
|
|||||||
|
|
||||||
total_swar += x['wara']
|
total_swar += x['wara']
|
||||||
|
|
||||||
if total_swar > 32.00001:
|
team_cap = get_team_salary_cap(draft_pick.owner)
|
||||||
|
if exceeds_salary_cap(total_swar, draft_pick.owner):
|
||||||
return {
|
return {
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': f'Drafting {player["name"]} would put you at {total_swar:.2f} '
|
'error': f'Drafting {player["name"]} would put you at {total_swar:.2f} '
|
||||||
f'sWAR, friendo.'
|
f'sWAR (cap {team_cap:.1f}), friendo.'
|
||||||
}
|
}
|
||||||
logger.info(
|
logger.info(
|
||||||
f'{draft_pick.owner.lname} selects {player["name"]} with the #{draft_pick.overall} overall pick'
|
f'{draft_pick.owner.lname} selects {player["name"]} with the #{draft_pick.overall} overall pick'
|
||||||
)
|
)
|
||||||
draft_pick.player = Player(**player)
|
draft_pick.player = Player(**player)
|
||||||
|
logger.info(f'draft_pick test: {draft_pick}')
|
||||||
await patch_draftpick(draft_pick) # TODO: uncomment for live draft
|
await patch_draftpick(draft_pick) # TODO: uncomment for live draft
|
||||||
|
|
||||||
player['team']['id'] = draft_pick.owner.id
|
player['team']['id'] = draft_pick.owner.id
|
||||||
@ -780,13 +782,34 @@ class Draft(commands.Cog):
|
|||||||
|
|
||||||
temp_player['team'] = fa_team
|
temp_player['team'] = fa_team
|
||||||
this_player = await put_player(temp_player)
|
this_player = await put_player(temp_player)
|
||||||
|
|
||||||
# this_player = await get_player_by_name(current.season, this_pick['player']['id'])
|
t_query = await db_get(
|
||||||
# this_player = await db_get('players', object_id=this_pick['player']['id'])
|
'transactions',
|
||||||
await interaction.edit_original_response(
|
params=[('season', 12), ('move_id', f'draft-overall-{this_pick.overall}')]
|
||||||
content='Don\'t forget to delete the transaction from the database.',
|
|
||||||
embed=await get_player_embed(this_player, current)
|
|
||||||
)
|
)
|
||||||
|
if not t_query or t_query.get('count', 0) == 0:
|
||||||
|
await interaction.edit_original_response(
|
||||||
|
content='I couldn\'t find the transaction record of this move so that\'s Cal\'s problem now. The pick is wiped, though.',
|
||||||
|
embed=await get_player_embed(this_player, current)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
del_query = await db_delete(
|
||||||
|
'transactions',
|
||||||
|
object_id=t_query['transactions'][0]['moveid']
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(f'Could not delete draft transaction: {e}')
|
||||||
|
await interaction.edit_original_response(
|
||||||
|
content='I couldn\'t delete the transaction record of this move so that\'s Cal\'s problem now. The pick is wiped, though.',
|
||||||
|
embed=await get_player_embed(this_player, current)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await interaction.edit_original_response(
|
||||||
|
content=f'[I gotchu]({random_gif("blows kiss")}) boo boo, like it never even happened.',
|
||||||
|
embed=await get_player_embed(this_player, current)
|
||||||
|
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if pick_lock is not None:
|
if pick_lock is not None:
|
||||||
|
|||||||
@ -800,7 +800,7 @@ class Gameday(commands.Cog):
|
|||||||
@commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME)
|
@commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME)
|
||||||
async def new_game_command(self, ctx):
|
async def new_game_command(self, ctx):
|
||||||
current = await db_get('current')
|
current = await db_get('current')
|
||||||
this_team = await get_team_by_owner(current['season'], ctx.author.id)
|
this_team = await get_team_by_owner(current.season, ctx.author.id)
|
||||||
|
|
||||||
await ctx.send('**Note:** Make sure this is the channel where you will be playing commands. Once this is set, '
|
await ctx.send('**Note:** Make sure this is the channel where you will be playing commands. Once this is set, '
|
||||||
'I will only take gameplay commands here.')
|
'I will only take gameplay commands here.')
|
||||||
@ -828,9 +828,9 @@ class Gameday(commands.Cog):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
this_matchup = await get_one_schedule(
|
this_matchup = await get_one_schedule(
|
||||||
season=current['season'],
|
season=current.season,
|
||||||
team_abbrev1=this_team['abbrev'],
|
team_abbrev1=this_team['abbrev'],
|
||||||
week=current['week']
|
week=current.week
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
home_team = await get_team('home')
|
home_team = await get_team('home')
|
||||||
|
|||||||
@ -998,12 +998,15 @@ class Players(commands.Cog):
|
|||||||
param_list.append(('week', current.week))
|
param_list.append(('week', current.week))
|
||||||
|
|
||||||
g_query = await db_get('games', params=param_list)
|
g_query = await db_get('games', params=param_list)
|
||||||
if g_query['count'] == 0:
|
if not g_query or g_query['count'] == 0:
|
||||||
await interaction.edit_original_response(
|
await interaction.edit_original_response(
|
||||||
content=f'Hm. I don\'t see any games then.'
|
content=f'Hm. I don\'t see any games then.'
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Sort games by game_id
|
||||||
|
g_query['games'] = sorted(g_query['games'], key=lambda game: game['id'])
|
||||||
|
|
||||||
if team_abbrev is not None:
|
if team_abbrev is not None:
|
||||||
title = f'{this_team["lname"]} Schedule'
|
title = f'{this_team["lname"]} Schedule'
|
||||||
embed = get_team_embed(title=title, team=this_team)
|
embed = get_team_embed(title=title, team=this_team)
|
||||||
@ -1191,19 +1194,19 @@ class Players(commands.Cog):
|
|||||||
|
|
||||||
async def get_division_standings(self, current) -> discord.Embed:
|
async def get_division_standings(self, current) -> discord.Embed:
|
||||||
d1_query = await db_get('standings', params=[
|
d1_query = await db_get('standings', params=[
|
||||||
('season', current.season), ('division_abbrev', 'SD')
|
('season', current.season), ('division_abbrev', 'TC')
|
||||||
])
|
])
|
||||||
div_one = d1_query['standings']
|
div_one = d1_query['standings']
|
||||||
d2_query = await db_get('standings', params=[
|
d2_query = await db_get('standings', params=[
|
||||||
('season', current.season), ('division_abbrev', 'DC')
|
('season', current.season), ('division_abbrev', 'ETSOS')
|
||||||
])
|
])
|
||||||
div_two = d2_query['standings']
|
div_two = d2_query['standings']
|
||||||
d3_query = await db_get('standings', params=[
|
d3_query = await db_get('standings', params=[
|
||||||
('season', current.season), ('division_abbrev', 'FIP')
|
('season', current.season), ('division_abbrev', 'APL')
|
||||||
])
|
])
|
||||||
div_three = d3_query['standings']
|
div_three = d3_query['standings']
|
||||||
d4_query = await db_get('standings', params=[
|
d4_query = await db_get('standings', params=[
|
||||||
('season', current.season), ('division_abbrev', 'DOC')
|
('season', current.season), ('division_abbrev', 'BBC')
|
||||||
])
|
])
|
||||||
div_four = d4_query['standings']
|
div_four = d4_query['standings']
|
||||||
|
|
||||||
@ -1233,10 +1236,10 @@ class Players(commands.Cog):
|
|||||||
f'{progress["games_played"]}/{progress["game_count"]} games played',
|
f'{progress["games_played"]}/{progress["game_count"]} games played',
|
||||||
color=0xB70000)
|
color=0xB70000)
|
||||||
embed.add_field(name=f'**Full Standings**', value=SBA_STANDINGS_URL, inline=False)
|
embed.add_field(name=f'**Full Standings**', value=SBA_STANDINGS_URL, inline=False)
|
||||||
embed.add_field(name=f'**Snow Day**', value=div_one_standings, inline=False)
|
embed.add_field(name=f'**Traveling Circus**', value=div_one_standings, inline=False)
|
||||||
embed.add_field(name=f'**Dinger Central**', value=div_two_standings, inline=False)
|
embed.add_field(name=f'**ETSOS**', value=div_two_standings, inline=False)
|
||||||
embed.add_field(name=f'**Just the FIP**', value=div_three_standings, inline=False)
|
embed.add_field(name=f'**Apple**', value=div_three_standings, inline=False)
|
||||||
embed.add_field(name=f'**Division of Cook**', value=div_four_standings, inline=False)
|
embed.add_field(name=f'**Big Chungus**', value=div_four_standings, inline=False)
|
||||||
|
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
@ -1520,7 +1523,7 @@ class Players(commands.Cog):
|
|||||||
setup_tab = scorecard.worksheet_by_title('Setup')
|
setup_tab = scorecard.worksheet_by_title('Setup')
|
||||||
|
|
||||||
scorecard_version = setup_tab.get_value('V35')
|
scorecard_version = setup_tab.get_value('V35')
|
||||||
if int(scorecard_version) != current.bet_week:
|
if scorecard_version != current.bet_week:
|
||||||
await interaction.edit_original_response(
|
await interaction.edit_original_response(
|
||||||
content=f'It looks like this scorecard is out of date. Did you create a new card at the start of the '
|
content=f'It looks like this scorecard is out of date. Did you create a new card at the start of the '
|
||||||
f'game? If you did, let Cal know about this error. If not, I\'ll need you to use an up to '
|
f'game? If you did, let Cal know about this error. If not, I\'ll need you to use an up to '
|
||||||
@ -2337,8 +2340,7 @@ class Players(commands.Cog):
|
|||||||
|
|
||||||
@app_commands.command(name='charts')
|
@app_commands.command(name='charts')
|
||||||
async def chart_command(self, interaction: discord.Interaction, chart_name: Literal[
|
async def chart_command(self, interaction: discord.Interaction, chart_name: Literal[
|
||||||
'block-plate', 'defense-matters', 'fly-b', 'g1', 'g2', 'g3', 'groundball', 'hit-and-run',
|
'rob-hr', 'defense', 'fly-b', 'g1', 'g2', 'g3', 'groundball','hit-and-run', 'rest', 'sac-bunt', 'squeeze-bunt']):
|
||||||
'rest', 'rob-hr', 'sac-bunt', 'squeeze-bunt']):
|
|
||||||
gb_url = 'https://sombaseball.ddns.net/static/images/season04/ground-ball-chart'
|
gb_url = 'https://sombaseball.ddns.net/static/images/season04/ground-ball-chart'
|
||||||
all_charts = {
|
all_charts = {
|
||||||
'rest': [f'{SBA_IMAGE_URL}/season05/charts/rest.png'],
|
'rest': [f'{SBA_IMAGE_URL}/season05/charts/rest.png'],
|
||||||
@ -2350,8 +2352,8 @@ class Players(commands.Cog):
|
|||||||
f'{SBA_IMAGE_URL}/season05/charts/squeeze-bunt.png',
|
f'{SBA_IMAGE_URL}/season05/charts/squeeze-bunt.png',
|
||||||
f'{SBA_IMAGE_URL}/season05/charts/squeeze-bunt-help.png'
|
f'{SBA_IMAGE_URL}/season05/charts/squeeze-bunt-help.png'
|
||||||
],
|
],
|
||||||
'rob-hr': [f'{SBA_IMAGE_URL}/season05/charts/rob-hr.png'],
|
'rob-hr': [f'https://sba-cards-2024.s3.us-east-1.amazonaws.com/static-images/rob-hr.png'],
|
||||||
'defense-matters': [f'{SBA_IMAGE_URL}/season05/charts/defense-matters.png'],
|
'defense': [f'https://sba-cards-2024.s3.us-east-1.amazonaws.com/static-images/defense.png'],
|
||||||
'block-plate': [f'{SBA_IMAGE_URL}/season05/charts/block-plate.png'],
|
'block-plate': [f'{SBA_IMAGE_URL}/season05/charts/block-plate.png'],
|
||||||
'hit-and-run': [
|
'hit-and-run': [
|
||||||
f'{SBA_IMAGE_URL}/season05/charts/hit-and-run.png',
|
f'{SBA_IMAGE_URL}/season05/charts/hit-and-run.png',
|
||||||
@ -2854,7 +2856,7 @@ class Players(commands.Cog):
|
|||||||
|
|
||||||
update_string = ''
|
update_string = ''
|
||||||
if swar is not None:
|
if swar is not None:
|
||||||
update_string += f'sWAR: {this_player["wara"]} => {swar}\n'
|
update_string += f'sWAR: {this_player["wara"]:.2f} => {swar:.2f}\n'
|
||||||
this_player['wara'] = swar
|
this_player['wara'] = swar
|
||||||
if injury_rating is not None:
|
if injury_rating is not None:
|
||||||
update_string += f'injury_rating: {this_player["injury_rating"]} => {injury_rating}\n'
|
update_string += f'injury_rating: {this_player["injury_rating"]} => {injury_rating}\n'
|
||||||
|
|||||||
@ -1,14 +1,232 @@
|
|||||||
import re
|
import re
|
||||||
import copy
|
import copy
|
||||||
|
import random
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Tuple, Optional, Dict, Set
|
||||||
|
|
||||||
from helpers import *
|
from helpers import *
|
||||||
from api_calls.current import get_current
|
from api_calls.current import get_current
|
||||||
from db_calls import db_get, db_patch, get_team_by_owner, get_team_by_abbrev, get_player_by_name, put_player, db_post
|
from db_calls import db_get, db_patch, get_team_by_owner, get_team_by_abbrev, get_player_by_name, put_player, db_post
|
||||||
from discord.ext import commands, tasks
|
from discord.ext import commands, tasks
|
||||||
OFFSEASON_FLAG = True
|
OFFSEASON_FLAG = False
|
||||||
|
USE_NEW_TRANSACTION_LOGIC = os.getenv('USE_NEW_TRANSACTION_LOGIC', 'true').lower() == 'true'
|
||||||
logger = logging.getLogger('discord_app')
|
logger = logging.getLogger('discord_app')
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TransactionPriority:
|
||||||
|
"""Data class to hold transaction priority calculation results"""
|
||||||
|
roster_priority: int # 1 = major league, 2 = minor league
|
||||||
|
win_percentage: float # Lower is better (worse teams get priority)
|
||||||
|
random_tiebreaker: int # Random number for final tie resolution
|
||||||
|
move_id: str
|
||||||
|
major_league_team_abbrev: str # The major league team (without MiL)
|
||||||
|
contested_players: List[str] # List of contested player names in this transaction
|
||||||
|
|
||||||
|
def get_sort_tuple(self) -> Tuple[int, float, int]:
|
||||||
|
"""Return tuple for sorting - lower values have higher priority"""
|
||||||
|
return (self.roster_priority, self.win_percentage, self.random_tiebreaker)
|
||||||
|
|
||||||
|
|
||||||
|
def get_major_league_team_abbrev(old_team_abbrev: str, new_team_abbrev: str) -> str:
|
||||||
|
"""
|
||||||
|
Extract the major league team abbreviation from a transaction.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
old_team_abbrev: Source team abbreviation
|
||||||
|
new_team_abbrev: Destination team abbreviation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Major league team abbreviation (the one that's not FA and doesn't end in MiL)
|
||||||
|
"""
|
||||||
|
# Find the team that is not FA and strip MiL if needed
|
||||||
|
if old_team_abbrev != 'FA':
|
||||||
|
return old_team_abbrev.replace('MiL', '') if old_team_abbrev.endswith('MiL') else old_team_abbrev
|
||||||
|
elif new_team_abbrev != 'FA':
|
||||||
|
return new_team_abbrev.replace('MiL', '') if new_team_abbrev.endswith('MiL') else new_team_abbrev
|
||||||
|
else:
|
||||||
|
raise ValueError("Both teams cannot be FA")
|
||||||
|
|
||||||
|
|
||||||
|
def determine_roster_priority(moves: List[dict]) -> int:
|
||||||
|
"""
|
||||||
|
Determine roster priority for a transaction based on all movements.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
moves: List of all movements in the transaction
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
1 for major league priority, 2 for minor league priority
|
||||||
|
"""
|
||||||
|
# If ANY move involves major league roster (no MiL suffix), entire transaction gets major league priority
|
||||||
|
for move in moves:
|
||||||
|
old_abbrev = move['oldteam']['abbrev']
|
||||||
|
new_abbrev = move['newteam']['abbrev']
|
||||||
|
|
||||||
|
# Check if either team is major league (not FA and doesn't end with MiL)
|
||||||
|
if (old_abbrev != 'FA' and not old_abbrev.endswith('MiL')) or \
|
||||||
|
(new_abbrev != 'FA' and not new_abbrev.endswith('MiL')):
|
||||||
|
return 1 # Major league priority
|
||||||
|
|
||||||
|
return 2 # Minor league priority (all moves involve MiL teams)
|
||||||
|
|
||||||
|
|
||||||
|
async def calculate_transaction_priority(move_id: str, moves: List[dict], current_season: int,
|
||||||
|
contested_players: Set[str]) -> TransactionPriority:
|
||||||
|
"""
|
||||||
|
Calculate priority for an entire transaction (all moves with same move_id).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
move_id: The transaction ID
|
||||||
|
moves: List of all movements in this transaction
|
||||||
|
current_season: Current season number
|
||||||
|
contested_players: Set of all contested player names
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TransactionPriority object with calculated values
|
||||||
|
"""
|
||||||
|
if not moves:
|
||||||
|
raise ValueError("No moves provided for transaction")
|
||||||
|
|
||||||
|
# Get major league team abbreviation from any move in the transaction
|
||||||
|
first_move = moves[0]
|
||||||
|
major_league_team_abbrev = get_major_league_team_abbrev(
|
||||||
|
first_move['oldteam']['abbrev'],
|
||||||
|
first_move['newteam']['abbrev']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Determine roster priority for entire transaction
|
||||||
|
roster_priority = determine_roster_priority(moves)
|
||||||
|
|
||||||
|
# Calculate win percentage using major league team's record
|
||||||
|
if major_league_team_abbrev == 'FA':
|
||||||
|
win_percentage = 0.0
|
||||||
|
else:
|
||||||
|
major_league_team = await get_team_by_abbrev(major_league_team_abbrev, current_season)
|
||||||
|
if major_league_team is None:
|
||||||
|
raise ValueError(f'Team `{major_league_team_abbrev}` not found')
|
||||||
|
standings_query = await db_get('standings', params=[
|
||||||
|
('season', current_season),
|
||||||
|
('team_id', major_league_team['id']),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if standings_query is None:
|
||||||
|
raise ValueError(f'Standings not found for `{major_league_team_abbrev}`')
|
||||||
|
standings = standings_query['standings'][0]
|
||||||
|
total_games = standings['wins'] + standings['losses']
|
||||||
|
win_percentage = standings['wins'] / total_games if total_games > 0 else 0.0
|
||||||
|
|
||||||
|
# Find which players in this transaction are contested
|
||||||
|
transaction_contested_players = [
|
||||||
|
move['player']['name'] for move in moves
|
||||||
|
if move['player']['name'] in contested_players
|
||||||
|
]
|
||||||
|
|
||||||
|
return TransactionPriority(
|
||||||
|
roster_priority=roster_priority,
|
||||||
|
win_percentage=win_percentage,
|
||||||
|
random_tiebreaker=random.randint(1, 100000),
|
||||||
|
move_id=move_id,
|
||||||
|
major_league_team_abbrev=major_league_team_abbrev,
|
||||||
|
contested_players=transaction_contested_players
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def resolve_contested_transactions(moves: List[dict], current_season: int) -> Tuple[List[str], List[str]]:
|
||||||
|
"""
|
||||||
|
Resolve disputes for contested players at the transaction level.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
moves: List of all transaction moves from database
|
||||||
|
current_season: Current season number
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (winning_move_ids, losing_move_ids)
|
||||||
|
"""
|
||||||
|
# Group moves by move_id (transaction)
|
||||||
|
transactions = {}
|
||||||
|
for move in moves:
|
||||||
|
move_id = move['moveid']
|
||||||
|
if move_id not in transactions:
|
||||||
|
transactions[move_id] = []
|
||||||
|
transactions[move_id].append(move)
|
||||||
|
|
||||||
|
# Group moves by player name to identify contested players
|
||||||
|
player_to_move_ids = {}
|
||||||
|
for move in moves:
|
||||||
|
player_name = move['player']['name']
|
||||||
|
move_id = move['moveid']
|
||||||
|
if player_name not in player_to_move_ids:
|
||||||
|
player_to_move_ids[player_name] = set()
|
||||||
|
player_to_move_ids[player_name].add(move_id)
|
||||||
|
|
||||||
|
# Find contested players (claimed by multiple transactions)
|
||||||
|
contested_players = {
|
||||||
|
player_name for player_name, move_ids in player_to_move_ids.items()
|
||||||
|
if len(move_ids) > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"Found {len(contested_players)} contested players: {list(contested_players)}")
|
||||||
|
|
||||||
|
# Find transactions that claim any contested player
|
||||||
|
contested_move_ids = set()
|
||||||
|
for player_name in contested_players:
|
||||||
|
contested_move_ids.update(player_to_move_ids[player_name])
|
||||||
|
|
||||||
|
logger.info(f"Found {len(contested_move_ids)} transactions claiming contested players")
|
||||||
|
|
||||||
|
# Calculate priorities for contested transactions
|
||||||
|
contested_priorities = []
|
||||||
|
for move_id in contested_move_ids:
|
||||||
|
priority = await calculate_transaction_priority(
|
||||||
|
move_id, transactions[move_id], current_season, contested_players
|
||||||
|
)
|
||||||
|
contested_priorities.append(priority)
|
||||||
|
|
||||||
|
# Group contested transactions by the players they're fighting over
|
||||||
|
player_disputes = {}
|
||||||
|
for priority in contested_priorities:
|
||||||
|
for player_name in priority.contested_players:
|
||||||
|
if player_name not in player_disputes:
|
||||||
|
player_disputes[player_name] = []
|
||||||
|
player_disputes[player_name].append(priority)
|
||||||
|
|
||||||
|
# Resolve each player dispute
|
||||||
|
winning_move_ids = set()
|
||||||
|
losing_move_ids = set()
|
||||||
|
|
||||||
|
for player_name, competing_priorities in player_disputes.items():
|
||||||
|
# Sort by priority tuple (lower values = higher priority)
|
||||||
|
sorted_priorities = sorted(competing_priorities, key=lambda p: p.get_sort_tuple())
|
||||||
|
|
||||||
|
winner = sorted_priorities[0]
|
||||||
|
losers = sorted_priorities[1:]
|
||||||
|
|
||||||
|
winning_move_ids.add(winner.move_id)
|
||||||
|
for loser in losers:
|
||||||
|
losing_move_ids.add(loser.move_id)
|
||||||
|
|
||||||
|
# Log the resolution decision
|
||||||
|
logger.info(f"Contested player '{player_name}' resolution:")
|
||||||
|
logger.info(f" Winner: {winner.major_league_team_abbrev} (move_id={winner.move_id}, "
|
||||||
|
f"roster_priority={winner.roster_priority}, win_pct={winner.win_percentage:.3f}, "
|
||||||
|
f"random={winner.random_tiebreaker})")
|
||||||
|
for i, loser in enumerate(losers):
|
||||||
|
logger.info(f" Loser {i+1}: {loser.major_league_team_abbrev} (move_id={loser.move_id}, "
|
||||||
|
f"roster_priority={loser.roster_priority}, win_pct={loser.win_percentage:.3f}, "
|
||||||
|
f"random={loser.random_tiebreaker})")
|
||||||
|
|
||||||
|
# Add non-contested transactions to winners
|
||||||
|
non_contested_move_ids = [
|
||||||
|
move_id for move_id in transactions.keys()
|
||||||
|
if move_id not in contested_move_ids
|
||||||
|
]
|
||||||
|
winning_move_ids.update(non_contested_move_ids)
|
||||||
|
|
||||||
|
return list(winning_move_ids), list(losing_move_ids)
|
||||||
|
|
||||||
|
|
||||||
class SBaTransaction:
|
class SBaTransaction:
|
||||||
def __init__(self, channel, current, move_type, this_week=False, first_team=None, team_role=None):
|
def __init__(self, channel, current, move_type, this_week=False, first_team=None, team_role=None):
|
||||||
self.players = {} # Example: { <Player ID>: { "player": { Strat Player Dict }, "to": { Strat Team Dict } } }
|
self.players = {} # Example: { <Player ID>: { "player": { Strat Player Dict }, "to": { Strat Team Dict } } }
|
||||||
@ -76,13 +294,39 @@ class SBaTransaction:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_gms(self, bot, team=None):
|
async def get_gms(self, bot, team=None):
|
||||||
if team is None:
|
if team is None:
|
||||||
return [bot.get_user(x) for x in self.gms]
|
gms = []
|
||||||
|
for gm_id in self.gms:
|
||||||
|
try:
|
||||||
|
user = await bot.fetch_user(gm_id)
|
||||||
|
if user:
|
||||||
|
gms.append(user)
|
||||||
|
except discord.NotFound:
|
||||||
|
logger.warning(f'User {gm_id} not found')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Error fetching user {gm_id}: {e}')
|
||||||
|
return gms
|
||||||
else:
|
else:
|
||||||
these_gms = [bot.get_user(self.teams[team]['team']['gmid'])]
|
these_gms = []
|
||||||
|
try:
|
||||||
|
user = await bot.fetch_user(self.teams[team]['team']['gmid'])
|
||||||
|
if user:
|
||||||
|
these_gms.append(user)
|
||||||
|
except discord.NotFound:
|
||||||
|
logger.warning(f'User {self.teams[team]["team"]["gmid"]} not found')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Error fetching user {self.teams[team]["team"]["gmid"]}: {e}')
|
||||||
|
|
||||||
if self.teams[team]['team']['gmid2']:
|
if self.teams[team]['team']['gmid2']:
|
||||||
these_gms.append(bot.get_user(self.teams[team]['team']['gmid2']))
|
try:
|
||||||
|
user = await bot.fetch_user(self.teams[team]['team']['gmid2'])
|
||||||
|
if user:
|
||||||
|
these_gms.append(user)
|
||||||
|
except discord.NotFound:
|
||||||
|
logger.warning(f'User {self.teams[team]["team"]["gmid2"]} not found')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Error fetching user {self.teams[team]["team"]["gmid2"]}: {e}')
|
||||||
return these_gms
|
return these_gms
|
||||||
|
|
||||||
async def send(self, content=None, embed=None):
|
async def send(self, content=None, embed=None):
|
||||||
@ -96,7 +340,7 @@ class SBaTransaction:
|
|||||||
# Get player string
|
# Get player string
|
||||||
player_string = ''
|
player_string = ''
|
||||||
for x in self.players:
|
for x in self.players:
|
||||||
player_string += f'**{self.players[x]["player"]["name"]}** ({self.players[x]["player"]["wara"]}) from ' \
|
player_string += f'**{self.players[x]["player"]["name"]}** ({self.players[x]["player"]["wara"]:.2f}) from ' \
|
||||||
f'{self.players[x]["player"]["team"]["abbrev"]} to {self.players[x]["to"]["abbrev"]}\n'
|
f'{self.players[x]["player"]["team"]["abbrev"]} to {self.players[x]["to"]["abbrev"]}\n'
|
||||||
|
|
||||||
if len(player_string) == 0:
|
if len(player_string) == 0:
|
||||||
@ -196,11 +440,11 @@ class SBaTransaction:
|
|||||||
|
|
||||||
# If player is leaving this team, remove from roster and subtract WARa
|
# If player is leaving this team, remove from roster and subtract WARa
|
||||||
if x['oldteam'] == this_team:
|
if x['oldteam'] == this_team:
|
||||||
team_roster.remove(x['player'])
|
team_roster = [p for p in team_roster if p['id'] != x['player']['id']]
|
||||||
wara -= x['player']['wara']
|
wara -= x['player']['wara']
|
||||||
# If player is leaving MiL team, remove from roster and subtract WARa
|
# If player is leaving MiL team, remove from roster and subtract WARa
|
||||||
elif x['oldteam']['abbrev'] == f'{this_team["abbrev"]}MiL':
|
elif x['oldteam']['abbrev'] == f'{this_team["abbrev"]}MiL':
|
||||||
mil_roster.remove(x['player'])
|
mil_roster = [p for p in mil_roster if p['id'] != x['player']['id']]
|
||||||
mil_wara -= x['player']['wara']
|
mil_wara -= x['player']['wara']
|
||||||
|
|
||||||
logger.info(f'updating rosters')
|
logger.info(f'updating rosters')
|
||||||
@ -222,7 +466,7 @@ class SBaTransaction:
|
|||||||
if self.players[x]['player']['team'] == this_team:
|
if self.players[x]['player']['team'] == this_team:
|
||||||
logger.info(f'major league player')
|
logger.info(f'major league player')
|
||||||
# logger.info(f'team roster: {team_roster}')
|
# logger.info(f'team roster: {team_roster}')
|
||||||
team_roster.remove(self.players[x]['player'])
|
team_roster = [p for p in team_roster if p['id'] != self.players[x]['player']['id']]
|
||||||
# 06-13: COMMENTED OUT TO RESOLVE MID-WEEK IL REPLACEMENT BEING SENT BACK DOWN
|
# 06-13: COMMENTED OUT TO RESOLVE MID-WEEK IL REPLACEMENT BEING SENT BACK DOWN
|
||||||
# if self.effective_week != self.current.week:
|
# if self.effective_week != self.current.week:
|
||||||
wara -= self.players[x]['player']['wara']
|
wara -= self.players[x]['player']['wara']
|
||||||
@ -230,7 +474,7 @@ class SBaTransaction:
|
|||||||
# If player is leaving MiL team next week, remove from roster and subtract WARa
|
# If player is leaving MiL team next week, remove from roster and subtract WARa
|
||||||
if self.players[x]['player']['team']['abbrev'] == f'{this_team["abbrev"]}MiL':
|
if self.players[x]['player']['team']['abbrev'] == f'{this_team["abbrev"]}MiL':
|
||||||
logger.info(f'minor league player')
|
logger.info(f'minor league player')
|
||||||
mil_roster.remove(self.players[x]['player'])
|
mil_roster = [p for p in mil_roster if p['id'] != self.players[x]['player']['id']]
|
||||||
# logger.info(f'mil roster: {mil_roster}')
|
# logger.info(f'mil roster: {mil_roster}')
|
||||||
if self.effective_week != self.current.week:
|
if self.effective_week != self.current.week:
|
||||||
mil_wara -= self.players[x]['player']['wara']
|
mil_wara -= self.players[x]['player']['wara']
|
||||||
@ -329,6 +573,7 @@ class Transactions(commands.Cog):
|
|||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.trade_season = False
|
self.trade_season = False
|
||||||
|
self.weekly_warning_sent = False
|
||||||
|
|
||||||
self.weekly_loop.start()
|
self.weekly_loop.start()
|
||||||
|
|
||||||
@ -342,47 +587,64 @@ class Transactions(commands.Cog):
|
|||||||
|
|
||||||
@tasks.loop(minutes=1)
|
@tasks.loop(minutes=1)
|
||||||
async def weekly_loop(self):
|
async def weekly_loop(self):
|
||||||
if OFFSEASON_FLAG:
|
try:
|
||||||
return
|
logger.info(f'Inside weekly_loop')
|
||||||
|
if OFFSEASON_FLAG:
|
||||||
|
logger.info(f'Exiting weekly_loop; OFFSEASON_FLAG: {OFFSEASON_FLAG}')
|
||||||
|
return
|
||||||
|
|
||||||
current = await get_current()
|
current = await get_current()
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
logger.debug(f'Datetime: {now} / weekday: {now.weekday()}')
|
logger.info(f'Datetime: {now} / weekday: {now.weekday()} / current: {current}')
|
||||||
|
|
||||||
# Begin Freeze
|
# Begin Freeze
|
||||||
# if now.weekday() == 0 and now.hour == 5 and not current.freeze: # Spring/Summer
|
# if now.weekday() == 0 and now.hour == 5 and not current.freeze: # Spring/Summer
|
||||||
if now.weekday() == 0 and now.hour == 0 and not current.freeze: # Fall/Winter
|
if now.weekday() == 0 and now.hour == 0 and not current.freeze: # Fall/Winter
|
||||||
current.week += 1
|
logger.info(f'weekly_loop - setting the freeze')
|
||||||
await db_patch('current', object_id=current['id'], params=[('week', current.week), ('freeze', True)])
|
current.week += 1
|
||||||
await self.run_transactions(current)
|
await db_patch('current', object_id=current.id, params=[('week', current.week), ('freeze', True)])
|
||||||
|
await self.run_transactions(current)
|
||||||
|
|
||||||
logger.debug(f'Building freeze string')
|
logger.debug(f'Building freeze string')
|
||||||
week_num = f'Week {current.week}'
|
week_num = f'Week {current.week}'
|
||||||
stars = f'{"":*<32}'
|
stars = f'{"":*<32}'
|
||||||
freeze_message = f'```\n' \
|
freeze_message = f'```\n' \
|
||||||
f'{stars}\n'\
|
f'{stars}\n'\
|
||||||
f'{week_num: >9} Freeze Period Begins\n' \
|
f'{week_num: >9} Freeze Period Begins\n' \
|
||||||
f'{stars}\n```'
|
f'{stars}\n```'
|
||||||
logger.debug(f'Freeze string:\n\n{freeze_message}')
|
logger.debug(f'Freeze string:\n\n{freeze_message}')
|
||||||
await send_to_channel(self.bot, 'transaction-log', freeze_message)
|
await send_to_channel(self.bot, 'transaction-log', freeze_message)
|
||||||
|
|
||||||
if current.week > 0 and current.week <= 18:
|
if current.week > 0 and current.week <= 18:
|
||||||
await self.post_weekly_info(current)
|
await self.post_weekly_info(current)
|
||||||
|
|
||||||
# End Freeze
|
# End Freeze
|
||||||
# elif now.weekday() == 5 and now.hour == 5 and current.freeze: # Spring/Summer
|
# elif now.weekday() == 5 and now.hour == 5 and current.freeze: # Spring/Summer
|
||||||
elif now.weekday() == 5 and now.hour == 0 and current.freeze: # Fall/Winter
|
elif now.weekday() == 5 and now.hour == 0 and current.freeze: # Fall/Winter
|
||||||
await db_patch('current', object_id=current['id'], params=[('freeze', False)])
|
logger.info(f'weekly_loop - running the un-freeze')
|
||||||
|
await db_patch('current', object_id=current.id, params=[('freeze', False)])
|
||||||
|
|
||||||
week_num = f'Week {current.week}'
|
week_num = f'Week {current.week}'
|
||||||
stars = f'{"":*<30}'
|
stars = f'{"":*<30}'
|
||||||
freeze_message = f'```\n' \
|
freeze_message = f'```\n' \
|
||||||
f'{stars}\n'\
|
f'{stars}\n'\
|
||||||
f'{week_num: >9} Freeze Period Ends\n' \
|
f'{week_num: >9} Freeze Period Ends\n' \
|
||||||
f'{stars}\n```'
|
f'{stars}\n```'
|
||||||
await self.process_freeze_moves(current)
|
await self.process_freeze_moves(current)
|
||||||
await send_to_channel(self.bot, 'transaction-log', freeze_message)
|
await send_to_channel(self.bot, 'transaction-log', freeze_message)
|
||||||
self.trade_season = False
|
self.trade_season = False
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.info(f'weekly_loop - No freeze actions being taken')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Unhandled exception in weekly_loop: {e}')
|
||||||
|
error_message = f"⚠️ **Weekly Freeze Task Failed**\n```\nError: {str(e)}\nTime: {datetime.datetime.now()}\nTask: weekly_loop in transactions.py\n```"
|
||||||
|
try:
|
||||||
|
if not self.weekly_warning_sent:
|
||||||
|
await send_owner_notification(error_message)
|
||||||
|
self.weekly_warning_sent = True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Failed to send error message :( {e}')
|
||||||
|
|
||||||
@weekly_loop.before_loop
|
@weekly_loop.before_loop
|
||||||
async def before_notif_check(self):
|
async def before_notif_check(self):
|
||||||
@ -443,7 +705,8 @@ class Transactions(commands.Cog):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f'Could not notifiy GM2 of {team["abbrev"]} ({team["gmid2"]})of move cancellation: {e}')
|
logger.error(f'Could not notifiy GM2 of {team["abbrev"]} ({team["gmid2"]})of move cancellation: {e}')
|
||||||
|
|
||||||
async def process_freeze_moves(self, current):
|
async def process_freeze_moves_backup(self, current):
|
||||||
|
"""Original implementation - kept for rollback purposes"""
|
||||||
# all_moves = await get_transactions(
|
# all_moves = await get_transactions(
|
||||||
# season=current.season,
|
# season=current.season,
|
||||||
# week_start=current.week,
|
# week_start=current.week,
|
||||||
@ -538,6 +801,84 @@ class Transactions(commands.Cog):
|
|||||||
await db_patch('transactions', object_id=move_id, params=[('frozen', False)])
|
await db_patch('transactions', object_id=move_id, params=[('frozen', False)])
|
||||||
await self.post_move_to_transaction_log(move_id)
|
await self.post_move_to_transaction_log(move_id)
|
||||||
|
|
||||||
|
async def process_freeze_moves(self, current):
|
||||||
|
"""
|
||||||
|
Route to either new or backup implementation based on feature flag.
|
||||||
|
Set USE_NEW_TRANSACTION_LOGIC=false to use original implementation.
|
||||||
|
"""
|
||||||
|
if USE_NEW_TRANSACTION_LOGIC:
|
||||||
|
logger.info("Using new transaction priority logic")
|
||||||
|
await self.process_freeze_moves_new(current)
|
||||||
|
else:
|
||||||
|
logger.info("Using backup transaction priority logic")
|
||||||
|
await self.process_freeze_moves_backup(current)
|
||||||
|
|
||||||
|
async def process_freeze_moves_new(self, current):
|
||||||
|
"""
|
||||||
|
New implementation of freeze move processing with proper transaction-level priority resolution.
|
||||||
|
|
||||||
|
This function is designed to be easily testable by extracting business logic
|
||||||
|
into pure functions.
|
||||||
|
"""
|
||||||
|
# Get all frozen transactions for this week
|
||||||
|
moves_query = await db_get('transactions', params=[
|
||||||
|
('season', current.season),
|
||||||
|
('week_start', current.week),
|
||||||
|
('week_end', current.week + 1),
|
||||||
|
('frozen', True)
|
||||||
|
])
|
||||||
|
|
||||||
|
if moves_query['count'] == 0:
|
||||||
|
logger.warning(f'No transactions to process for the freeze in week {current.week}')
|
||||||
|
return
|
||||||
|
|
||||||
|
moves = moves_query['transactions']
|
||||||
|
logger.info(f'Processing {len(moves)} frozen moves across multiple transactions for week {current.week}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Resolve contested transactions using extracted business logic
|
||||||
|
winning_move_ids, losing_move_ids = await resolve_contested_transactions(moves, current.season)
|
||||||
|
|
||||||
|
# Cancel losing transactions (entire transactions, not just individual moves)
|
||||||
|
for losing_move_id in losing_move_ids:
|
||||||
|
# Get all moves in this losing transaction for notification
|
||||||
|
losing_moves = [m for m in moves if m['moveid'] == losing_move_id]
|
||||||
|
|
||||||
|
# Cancel all moves in this transaction
|
||||||
|
await db_patch('transactions', object_id=losing_move_id,
|
||||||
|
params=[('frozen', False), ('cancelled', True)])
|
||||||
|
|
||||||
|
# Notify the losing team about the cancelled transaction
|
||||||
|
if losing_moves:
|
||||||
|
# Use first move to identify the team for notification
|
||||||
|
first_move = losing_moves[0]
|
||||||
|
# Find the non-FA team for notification
|
||||||
|
team_for_notification = (first_move['newteam']
|
||||||
|
if first_move['newteam']['abbrev'] != 'FA'
|
||||||
|
else first_move['oldteam'])
|
||||||
|
|
||||||
|
await self.notify_cancel([first_move['player'], team_for_notification])
|
||||||
|
|
||||||
|
contested_players_in_transaction = [
|
||||||
|
move['player']['name'] for move in losing_moves
|
||||||
|
]
|
||||||
|
logger.info(f"Cancelled entire transaction {losing_move_id} due to contested players: "
|
||||||
|
f"{contested_players_in_transaction}")
|
||||||
|
|
||||||
|
# Unfreeze winning transactions
|
||||||
|
for winning_move_id in winning_move_ids:
|
||||||
|
await db_patch('transactions', object_id=winning_move_id, params=[('frozen', False)])
|
||||||
|
await self.post_move_to_transaction_log(winning_move_id)
|
||||||
|
|
||||||
|
logger.info(f"Processed successful transaction {winning_move_id}")
|
||||||
|
|
||||||
|
logger.info(f"Freeze processing complete: {len(winning_move_ids)} successful transactions, "
|
||||||
|
f"{len(losing_move_ids)} cancelled transactions")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error during freeze processing: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
async def post_move_to_transaction_log(self, move_id):
|
async def post_move_to_transaction_log(self, move_id):
|
||||||
current = await get_current()
|
current = await get_current()
|
||||||
# all_moves = await get_transactions(
|
# all_moves = await get_transactions(
|
||||||
@ -566,7 +907,7 @@ class Transactions(commands.Cog):
|
|||||||
if week_num is None:
|
if week_num is None:
|
||||||
week_num = move['week']
|
week_num = move['week']
|
||||||
|
|
||||||
move_string += f'**{move["player"]["name"]}** ({move["player"]["wara"]}) from ' \
|
move_string += f'**{move["player"]["name"]}** ({move["player"]["wara"]:.2f}) from ' \
|
||||||
f'{move["oldteam"]["abbrev"]} to {move["newteam"]["abbrev"]}\n'
|
f'{move["oldteam"]["abbrev"]} to {move["newteam"]["abbrev"]}\n'
|
||||||
|
|
||||||
embed = get_team_embed(f'Week {week_num} Transaction', this_team)
|
embed = get_team_embed(f'Week {week_num} Transaction', this_team)
|
||||||
@ -624,7 +965,7 @@ class Transactions(commands.Cog):
|
|||||||
@commands.is_owner()
|
@commands.is_owner()
|
||||||
async def process_freeze_helper_command(self, ctx):
|
async def process_freeze_helper_command(self, ctx):
|
||||||
current = await get_current()
|
current = await get_current()
|
||||||
await self.process_freeze_moves(current)
|
await self.process_freeze_moves_new(current)
|
||||||
await ctx.send(random_conf_gif())
|
await ctx.send(random_conf_gif())
|
||||||
|
|
||||||
@commands.command(name='post-weekly')
|
@commands.command(name='post-weekly')
|
||||||
@ -643,7 +984,7 @@ class Transactions(commands.Cog):
|
|||||||
await ctx.send(f'The trade deadline is **week {current.trade_deadline}**. Since it is currently **week '
|
await ctx.send(f'The trade deadline is **week {current.trade_deadline}**. Since it is currently **week '
|
||||||
f'{current.week}** I am just going to stop you right there.')
|
f'{current.week}** I am just going to stop you right there.')
|
||||||
return
|
return
|
||||||
if current.week < -2:
|
if current.week < -2 or current.week == -1:
|
||||||
await ctx.send(await get_emoji(ctx, 'oof', False))
|
await ctx.send(await get_emoji(ctx, 'oof', False))
|
||||||
await ctx.send(f'Patience, grasshopper. Trades open soon.')
|
await ctx.send(f'Patience, grasshopper. Trades open soon.')
|
||||||
return
|
return
|
||||||
@ -704,7 +1045,7 @@ class Transactions(commands.Cog):
|
|||||||
while True:
|
while True:
|
||||||
prompt = 'Are you adding a team to this deal? (Yes/No)'
|
prompt = 'Are you adding a team to this deal? (Yes/No)'
|
||||||
this_q = Question(self.bot, trade.channel, prompt, 'yesno', 30)
|
this_q = Question(self.bot, trade.channel, prompt, 'yesno', 30)
|
||||||
resp = await this_q.ask(trade.get_gms(self.bot))
|
resp = await this_q.ask(await trade.get_gms(self.bot))
|
||||||
|
|
||||||
if resp is None:
|
if resp is None:
|
||||||
await trade.send('RIP this move. Maybe next time.')
|
await trade.send('RIP this move. Maybe next time.')
|
||||||
@ -715,7 +1056,7 @@ class Transactions(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
this_q.prompt = 'Please enter the team\'s abbreviation.'
|
this_q.prompt = 'Please enter the team\'s abbreviation.'
|
||||||
this_q.qtype = 'text'
|
this_q.qtype = 'text'
|
||||||
resp = await this_q.ask(trade.get_gms(self.bot))
|
resp = await this_q.ask(await trade.get_gms(self.bot))
|
||||||
|
|
||||||
if not resp:
|
if not resp:
|
||||||
await trade.send('RIP this move. Maybe next time.')
|
await trade.send('RIP this move. Maybe next time.')
|
||||||
@ -736,7 +1077,7 @@ class Transactions(commands.Cog):
|
|||||||
while True:
|
while True:
|
||||||
prompt = f'Are you trading a player between teams?'
|
prompt = f'Are you trading a player between teams?'
|
||||||
this_q = Question(self.bot, trade.channel, prompt, 'yesno', 300)
|
this_q = Question(self.bot, trade.channel, prompt, 'yesno', 300)
|
||||||
resp = await this_q.ask(trade.get_gms(self.bot))
|
resp = await this_q.ask(await trade.get_gms(self.bot))
|
||||||
|
|
||||||
if resp is None:
|
if resp is None:
|
||||||
await trade.send('RIP this move. Maybe next time.')
|
await trade.send('RIP this move. Maybe next time.')
|
||||||
@ -747,7 +1088,7 @@ class Transactions(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
this_q.prompt = 'Which player is being traded?'
|
this_q.prompt = 'Which player is being traded?'
|
||||||
this_q.qtype = 'text'
|
this_q.qtype = 'text'
|
||||||
resp = await this_q.ask(trade.get_gms(self.bot))
|
resp = await this_q.ask(await trade.get_gms(self.bot))
|
||||||
|
|
||||||
if not resp:
|
if not resp:
|
||||||
pass
|
pass
|
||||||
@ -773,7 +1114,7 @@ class Transactions(commands.Cog):
|
|||||||
await trade.send(f'Ope. {player["name"]} is already one the move next week.')
|
await trade.send(f'Ope. {player["name"]} is already one the move next week.')
|
||||||
else:
|
else:
|
||||||
this_q.prompt = 'Where are they going? Please enter the destination team\'s abbreviation.'
|
this_q.prompt = 'Where are they going? Please enter the destination team\'s abbreviation.'
|
||||||
resp = await this_q.ask(trade.get_gms(self.bot))
|
resp = await this_q.ask(await trade.get_gms(self.bot))
|
||||||
|
|
||||||
if resp is None:
|
if resp is None:
|
||||||
await trade.send('RIP this move. Maybe next time.')
|
await trade.send('RIP this move. Maybe next time.')
|
||||||
@ -799,7 +1140,7 @@ class Transactions(commands.Cog):
|
|||||||
# while True and current.pick_trade_end >= current.week >= current.pick_trade_start:
|
# while True and current.pick_trade_end >= current.week >= current.pick_trade_start:
|
||||||
# prompt = f'Are you trading any draft picks?'
|
# prompt = f'Are you trading any draft picks?'
|
||||||
# this_q = Question(self.bot, trade.channel, prompt, 'yesno', 300)
|
# this_q = Question(self.bot, trade.channel, prompt, 'yesno', 300)
|
||||||
# resp = await this_q.ask(trade.get_gms(self.bot))
|
# resp = await this_q.ask(await trade.get_gms(self.bot))
|
||||||
# effective_season = current.season if OFFSEASON_FLAG else current.season + 1
|
# effective_season = current.season if OFFSEASON_FLAG else current.season + 1
|
||||||
# team_season = current.season
|
# team_season = current.season
|
||||||
#
|
#
|
||||||
@ -813,7 +1154,7 @@ class Transactions(commands.Cog):
|
|||||||
# # Get first pick
|
# # Get first pick
|
||||||
# this_q.prompt = 'Enter the pick\'s original owner and round number like this: TIT 17'
|
# this_q.prompt = 'Enter the pick\'s original owner and round number like this: TIT 17'
|
||||||
# this_q.qtype = 'text'
|
# this_q.qtype = 'text'
|
||||||
# resp = await this_q.ask(trade.get_gms(self.bot))
|
# resp = await this_q.ask(await trade.get_gms(self.bot))
|
||||||
#
|
#
|
||||||
# if not resp:
|
# if not resp:
|
||||||
# await trade.send('RIP this move. Maybe next time.')
|
# await trade.send('RIP this move. Maybe next time.')
|
||||||
@ -834,7 +1175,7 @@ class Transactions(commands.Cog):
|
|||||||
# f'round number, please.')
|
# f'round number, please.')
|
||||||
# else:
|
# else:
|
||||||
# this_q.prompt = 'Now enter the return pick\'s original owner and round number like this: TIT 17'
|
# this_q.prompt = 'Now enter the return pick\'s original owner and round number like this: TIT 17'
|
||||||
# resp = await this_q.ask(trade.get_gms(self.bot))
|
# resp = await this_q.ask(await trade.get_gms(self.bot))
|
||||||
#
|
#
|
||||||
# if not resp:
|
# if not resp:
|
||||||
# await trade.send('RIP this move. Maybe next time.')
|
# await trade.send('RIP this move. Maybe next time.')
|
||||||
@ -884,7 +1225,7 @@ class Transactions(commands.Cog):
|
|||||||
while True:
|
while True:
|
||||||
this_q.prompt = f'{trade.teams[team]["role"].mention}\nAre you making an FA drop?'
|
this_q.prompt = f'{trade.teams[team]["role"].mention}\nAre you making an FA drop?'
|
||||||
this_q.qtype = 'yesno'
|
this_q.qtype = 'yesno'
|
||||||
resp = await this_q.ask(trade.get_gms(self.bot, team))
|
resp = await this_q.ask(await trade.get_gms(self.bot, team))
|
||||||
|
|
||||||
if resp is None:
|
if resp is None:
|
||||||
await trade.send('RIP this move. Maybe next time.')
|
await trade.send('RIP this move. Maybe next time.')
|
||||||
@ -895,7 +1236,7 @@ class Transactions(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
this_q.prompt = 'Who are you dropping?'
|
this_q.prompt = 'Who are you dropping?'
|
||||||
this_q.qtype = 'text'
|
this_q.qtype = 'text'
|
||||||
resp = await this_q.ask(trade.get_gms(self.bot, team))
|
resp = await this_q.ask(await trade.get_gms(self.bot, team))
|
||||||
|
|
||||||
if resp is None:
|
if resp is None:
|
||||||
await trade.send('RIP this move. Maybe next time.')
|
await trade.send('RIP this move. Maybe next time.')
|
||||||
@ -937,20 +1278,22 @@ class Transactions(commands.Cog):
|
|||||||
roster_errors = []
|
roster_errors = []
|
||||||
for team in trade.teams:
|
for team in trade.teams:
|
||||||
data = await trade.check_major_league_errors(team)
|
data = await trade.check_major_league_errors(team)
|
||||||
|
team_obj = trade.teams[team]["team"]
|
||||||
|
team_cap = get_team_salary_cap(team_obj)
|
||||||
logger.warning(f'Done checking data - checking sWAR now ({data["wara"]}')
|
logger.warning(f'Done checking data - checking sWAR now ({data["wara"]}')
|
||||||
|
|
||||||
if data['wara'] > 32.001 and not OFFSEASON_FLAG:
|
if exceeds_salary_cap(data['wara'], team_obj) and not OFFSEASON_FLAG:
|
||||||
errors.append(f'- {trade.teams[team]["team"]["abbrev"]} would have {data["wara"]:.2f} WARa')
|
errors.append(f'- {team_obj["abbrev"]} would have {data["wara"]:.2f} WARa (cap {team_cap:.1f})')
|
||||||
|
|
||||||
logger.warning(f'Now checking roster {len(data["roster"])}')
|
logger.warning(f'Now checking roster {len(data["roster"])}')
|
||||||
if len(data['roster']) > 26 and not OFFSEASON_FLAG:
|
if len(data['roster']) > 26 and not OFFSEASON_FLAG:
|
||||||
errors.append(f'- {trade.teams[team]["team"]["abbrev"]} would have {len(data["roster"])} players')
|
errors.append(f'- {team_obj["abbrev"]} would have {len(data["roster"])} players')
|
||||||
|
|
||||||
logger.warning(f'Any errors? {errors}')
|
logger.warning(f'Any errors? {errors}')
|
||||||
if (data['wara'] > 32.001 or len(data['roster']) > 26) and not OFFSEASON_FLAG:
|
if (exceeds_salary_cap(data['wara'], team_obj) or len(data['roster']) > 26) and not OFFSEASON_FLAG:
|
||||||
roster_string = ''
|
roster_string = ''
|
||||||
for x in data['roster']:
|
for x in data['roster']:
|
||||||
roster_string += f'{x["wara"]: >5} - {x["name"]}\n'
|
roster_string += f'{x["wara"]: >5.2f} - {x["name"]}\n'
|
||||||
roster_errors.append(f'- This is the roster I have for {trade.teams[team]["team"]["abbrev"]}:\n'
|
roster_errors.append(f'- This is the roster I have for {trade.teams[team]["team"]["abbrev"]}:\n'
|
||||||
f'```\n{roster_string}```')
|
f'```\n{roster_string}```')
|
||||||
|
|
||||||
@ -967,7 +1310,7 @@ class Transactions(commands.Cog):
|
|||||||
for team in trade.teams:
|
for team in trade.teams:
|
||||||
this_q.prompt = f'{trade.teams[team]["role"].mention}\nDo you accept this trade?'
|
this_q.prompt = f'{trade.teams[team]["role"].mention}\nDo you accept this trade?'
|
||||||
this_q.qtype = 'yesno'
|
this_q.qtype = 'yesno'
|
||||||
resp = await this_q.ask(trade.get_gms(self.bot, team))
|
resp = await this_q.ask(await trade.get_gms(self.bot, team))
|
||||||
|
|
||||||
if not resp:
|
if not resp:
|
||||||
await trade.send('RIP this move. Maybe next time.')
|
await trade.send('RIP this move. Maybe next time.')
|
||||||
@ -1044,7 +1387,7 @@ class Transactions(commands.Cog):
|
|||||||
# while True:
|
# while True:
|
||||||
# prompt = 'Are you adding a team to this deal? (Yes/No)'
|
# prompt = 'Are you adding a team to this deal? (Yes/No)'
|
||||||
# this_q = Question(self.bot, trade.channel, prompt, 'yesno', 30)
|
# this_q = Question(self.bot, trade.channel, prompt, 'yesno', 30)
|
||||||
# resp = await this_q.ask(trade.get_gms(self.bot))
|
# resp = await this_q.ask(await trade.get_gms(self.bot))
|
||||||
#
|
#
|
||||||
# if resp is None:
|
# if resp is None:
|
||||||
# await trade.send('RIP this move. Maybe next time.')
|
# await trade.send('RIP this move. Maybe next time.')
|
||||||
@ -1055,7 +1398,7 @@ class Transactions(commands.Cog):
|
|||||||
# else:
|
# else:
|
||||||
# this_q.prompt = 'Please enter the team\'s abbreviation.'
|
# this_q.prompt = 'Please enter the team\'s abbreviation.'
|
||||||
# this_q.qtype = 'text'
|
# this_q.qtype = 'text'
|
||||||
# resp = await this_q.ask(trade.get_gms(self.bot))
|
# resp = await this_q.ask(await trade.get_gms(self.bot))
|
||||||
#
|
#
|
||||||
# if not resp:
|
# if not resp:
|
||||||
# await trade.send('RIP this move. Maybe next time.')
|
# await trade.send('RIP this move. Maybe next time.')
|
||||||
@ -1077,7 +1420,7 @@ class Transactions(commands.Cog):
|
|||||||
# while True:
|
# while True:
|
||||||
# prompt = f'Are you trading any draft picks?'
|
# prompt = f'Are you trading any draft picks?'
|
||||||
# this_q = Question(self.bot, trade.channel, prompt, 'yesno', 300)
|
# this_q = Question(self.bot, trade.channel, prompt, 'yesno', 300)
|
||||||
# resp = await this_q.ask(trade.get_gms(self.bot))
|
# resp = await this_q.ask(await trade.get_gms(self.bot))
|
||||||
# effective_season = current.season if OFFSEASON_FLAG else current.season + 1
|
# effective_season = current.season if OFFSEASON_FLAG else current.season + 1
|
||||||
# team_season = current.season
|
# team_season = current.season
|
||||||
#
|
#
|
||||||
@ -1091,7 +1434,7 @@ class Transactions(commands.Cog):
|
|||||||
# # Get first pick
|
# # Get first pick
|
||||||
# this_q.prompt = 'Enter the overall pick number being traded.'
|
# this_q.prompt = 'Enter the overall pick number being traded.'
|
||||||
# this_q.qtype = 'int'
|
# this_q.qtype = 'int'
|
||||||
# resp = await this_q.ask(trade.get_gms(self.bot))
|
# resp = await this_q.ask(await trade.get_gms(self.bot))
|
||||||
#
|
#
|
||||||
# if not resp:
|
# if not resp:
|
||||||
# await trade.send('RIP this move. Maybe next time.')
|
# await trade.send('RIP this move. Maybe next time.')
|
||||||
@ -1107,7 +1450,7 @@ class Transactions(commands.Cog):
|
|||||||
# await trade.send(f'Frick, I couldn\'t find pick #{resp}.')
|
# await trade.send(f'Frick, I couldn\'t find pick #{resp}.')
|
||||||
# else:
|
# else:
|
||||||
# this_q.prompt = 'Now enter the return pick\'s overall pick number.'
|
# this_q.prompt = 'Now enter the return pick\'s overall pick number.'
|
||||||
# resp = await this_q.ask(trade.get_gms(self.bot))
|
# resp = await this_q.ask(await trade.get_gms(self.bot))
|
||||||
#
|
#
|
||||||
# if not resp:
|
# if not resp:
|
||||||
# await trade.send('RIP this move. Maybe next time.')
|
# await trade.send('RIP this move. Maybe next time.')
|
||||||
@ -1181,7 +1524,7 @@ class Transactions(commands.Cog):
|
|||||||
# for team in trade.teams:
|
# for team in trade.teams:
|
||||||
# this_q.prompt = f'{trade.teams[team]["role"].mention}\nDo you accept this trade?'
|
# this_q.prompt = f'{trade.teams[team]["role"].mention}\nDo you accept this trade?'
|
||||||
# this_q.qtype = 'yesno'
|
# this_q.qtype = 'yesno'
|
||||||
# resp = await this_q.ask(trade.get_gms(self.bot, team))
|
# resp = await this_q.ask(await trade.get_gms(self.bot, team))
|
||||||
#
|
#
|
||||||
# if not resp:
|
# if not resp:
|
||||||
# await trade.send('RIP this move. Maybe next time.')
|
# await trade.send('RIP this move. Maybe next time.')
|
||||||
@ -1254,7 +1597,7 @@ class Transactions(commands.Cog):
|
|||||||
while True and not OFFSEASON_FLAG:
|
while True and not OFFSEASON_FLAG:
|
||||||
prompt = f'Are you adding someone to the Minor League roster?'
|
prompt = f'Are you adding someone to the Minor League roster?'
|
||||||
this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300)
|
this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300)
|
||||||
resp = await this_q.ask(dropadd.get_gms(self.bot))
|
resp = await this_q.ask(await dropadd.get_gms(self.bot))
|
||||||
|
|
||||||
if resp is None:
|
if resp is None:
|
||||||
await dropadd.send('RIP this move. Maybe next time.')
|
await dropadd.send('RIP this move. Maybe next time.')
|
||||||
@ -1265,7 +1608,7 @@ class Transactions(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
this_q.prompt = 'Who are you sending to the MiL?'
|
this_q.prompt = 'Who are you sending to the MiL?'
|
||||||
this_q.qtype = 'text'
|
this_q.qtype = 'text'
|
||||||
resp = await this_q.ask(dropadd.get_gms(self.bot))
|
resp = await this_q.ask(await dropadd.get_gms(self.bot))
|
||||||
|
|
||||||
if resp is None:
|
if resp is None:
|
||||||
await dropadd.send('RIP this move. Maybe next time.')
|
await dropadd.send('RIP this move. Maybe next time.')
|
||||||
@ -1307,7 +1650,7 @@ class Transactions(commands.Cog):
|
|||||||
while True and not OFFSEASON_FLAG:
|
while True and not OFFSEASON_FLAG:
|
||||||
prompt = f'Are you adding any players to the Major League roster?'
|
prompt = f'Are you adding any players to the Major League roster?'
|
||||||
this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300)
|
this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300)
|
||||||
resp = await this_q.ask(dropadd.get_gms(self.bot))
|
resp = await this_q.ask(await dropadd.get_gms(self.bot))
|
||||||
|
|
||||||
if resp is None:
|
if resp is None:
|
||||||
await dropadd.send('RIP this move. Maybe next time.')
|
await dropadd.send('RIP this move. Maybe next time.')
|
||||||
@ -1318,7 +1661,7 @@ class Transactions(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
this_q.prompt = 'Who are you adding?'
|
this_q.prompt = 'Who are you adding?'
|
||||||
this_q.qtype = 'text'
|
this_q.qtype = 'text'
|
||||||
resp = await this_q.ask(dropadd.get_gms(self.bot))
|
resp = await this_q.ask(await dropadd.get_gms(self.bot))
|
||||||
|
|
||||||
if resp is None:
|
if resp is None:
|
||||||
await dropadd.send('RIP this move. Maybe next time.')
|
await dropadd.send('RIP this move. Maybe next time.')
|
||||||
@ -1367,7 +1710,7 @@ class Transactions(commands.Cog):
|
|||||||
while True:
|
while True:
|
||||||
prompt = f'Are you dropping anyone to FA?'
|
prompt = f'Are you dropping anyone to FA?'
|
||||||
this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300)
|
this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300)
|
||||||
resp = await this_q.ask(dropadd.get_gms(self.bot))
|
resp = await this_q.ask(await dropadd.get_gms(self.bot))
|
||||||
|
|
||||||
if resp is None:
|
if resp is None:
|
||||||
await dropadd.send('RIP this move. Maybe next time.')
|
await dropadd.send('RIP this move. Maybe next time.')
|
||||||
@ -1378,7 +1721,7 @@ class Transactions(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
this_q.prompt = 'Who are you dropping to FA?'
|
this_q.prompt = 'Who are you dropping to FA?'
|
||||||
this_q.qtype = 'text'
|
this_q.qtype = 'text'
|
||||||
resp = await this_q.ask(dropadd.get_gms(self.bot))
|
resp = await this_q.ask(await dropadd.get_gms(self.bot))
|
||||||
|
|
||||||
if resp is None:
|
if resp is None:
|
||||||
await dropadd.send('RIP this move. Maybe next time.')
|
await dropadd.send('RIP this move. Maybe next time.')
|
||||||
@ -1422,21 +1765,23 @@ class Transactions(commands.Cog):
|
|||||||
roster_errors = []
|
roster_errors = []
|
||||||
for team in dropadd.teams:
|
for team in dropadd.teams:
|
||||||
data = await dropadd.check_major_league_errors(team)
|
data = await dropadd.check_major_league_errors(team)
|
||||||
|
team_obj = dropadd.teams[team]["team"]
|
||||||
|
team_cap = get_team_salary_cap(team_obj)
|
||||||
logger.warning(f'Done checking data - checking WARa now ({data["wara"]})')
|
logger.warning(f'Done checking data - checking WARa now ({data["wara"]})')
|
||||||
|
|
||||||
if data['wara'] > 32.001 and not OFFSEASON_FLAG:
|
if exceeds_salary_cap(data['wara'], team_obj) and not OFFSEASON_FLAG:
|
||||||
errors.append(f'- {dropadd.teams[team]["team"]["abbrev"]} would have {data["wara"]:.2f} sWAR')
|
errors.append(f'- {team_obj["abbrev"]} would have {data["wara"]:.2f} sWAR (cap {team_cap:.1f})')
|
||||||
|
|
||||||
logger.warning(f'Now checking roster {len(data["roster"])}')
|
logger.warning(f'Now checking roster {len(data["roster"])}')
|
||||||
if len(data['roster']) > 26 and not OFFSEASON_FLAG:
|
if len(data['roster']) > 26 and not OFFSEASON_FLAG:
|
||||||
errors.append(f'- {dropadd.teams[team]["team"]["abbrev"]} would have {len(data["roster"])} players')
|
errors.append(f'- {team_obj["abbrev"]} would have {len(data["roster"])} players')
|
||||||
|
|
||||||
logger.warning(f'Any errors? {errors}')
|
logger.warning(f'Any errors? {errors}')
|
||||||
if (data['wara'] > 32.001 or len(data['roster']) > 26) and not OFFSEASON_FLAG:
|
if (exceeds_salary_cap(data['wara'], team_obj) or len(data['roster']) > 26) and not OFFSEASON_FLAG:
|
||||||
roster_string = ''
|
roster_string = ''
|
||||||
for x in data['roster']:
|
for x in data['roster']:
|
||||||
roster_string += f'{x["wara"]: >5} - {x["name"]}\n'
|
roster_string += f'{x["wara"]: >5.2f} - {x["name"]}\n'
|
||||||
errors.append(f'- This is the roster I have for {dropadd.teams[team]["team"]["abbrev"]}:\n'
|
errors.append(f'- This is the roster I have for {team_obj["abbrev"]}:\n'
|
||||||
f'```\n{roster_string}```')
|
f'```\n{roster_string}```')
|
||||||
|
|
||||||
mil_cap = 6 if current.week <= FA_LOCK_WEEK else 14
|
mil_cap = 6 if current.week <= FA_LOCK_WEEK else 14
|
||||||
@ -1458,7 +1803,7 @@ class Transactions(commands.Cog):
|
|||||||
for team in dropadd.teams:
|
for team in dropadd.teams:
|
||||||
this_q.prompt = f'{dropadd.teams[team]["role"].mention}\nWould you like me to run this move?'
|
this_q.prompt = f'{dropadd.teams[team]["role"].mention}\nWould you like me to run this move?'
|
||||||
this_q.qtype = 'yesno'
|
this_q.qtype = 'yesno'
|
||||||
resp = await this_q.ask(dropadd.get_gms(self.bot, team))
|
resp = await this_q.ask(await dropadd.get_gms(self.bot, team))
|
||||||
|
|
||||||
if not resp:
|
if not resp:
|
||||||
await dropadd.send('RIP this move. Maybe next time.')
|
await dropadd.send('RIP this move. Maybe next time.')
|
||||||
@ -1484,7 +1829,7 @@ class Transactions(commands.Cog):
|
|||||||
q_string = player_name.replace(' ', '%20')
|
q_string = player_name.replace(' ', '%20')
|
||||||
dest_url = f'https://www.bing.com/images/search?q=rule%2034%20{q_string}&safesearch=off'
|
dest_url = f'https://www.bing.com/images/search?q=rule%2034%20{q_string}&safesearch=off'
|
||||||
else:
|
else:
|
||||||
dest_url = 'https://docs.google.com/spreadsheets/d/1kGNSQbwocG5NuXKw5NXFPJ1F2V7zM0_qsMv_1o7QjLs/edit?gid=0#gid=0'
|
dest_url = 'https://docs.google.com/spreadsheets/d/1nNkStOm1St1U2aQ0Jrdhyti5Qe0b-sI8xTUGCTOLuXE/edit?usp=sharing'
|
||||||
|
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
content=f'To play the 50/50, click the \'Trust Links\' button that pops up for [Bing](<https://www.bing.com/>) and for [Sheets](<https://docs.google.com/spreadsheets/u/0/>).\n\n[Rule 34 Draft](<{dest_url}>)'
|
content=f'To play the 50/50, click the \'Trust Links\' button that pops up for [Bing](<https://www.bing.com/>) and for [Sheets](<https://docs.google.com/spreadsheets/u/0/>).\n\n[Rule 34 Draft](<{dest_url}>)'
|
||||||
@ -1540,7 +1885,7 @@ class Transactions(commands.Cog):
|
|||||||
while True:
|
while True:
|
||||||
prompt = f'Are you sending someone to the IL?'
|
prompt = f'Are you sending someone to the IL?'
|
||||||
this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300)
|
this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300)
|
||||||
resp = await this_q.ask(dropadd.get_gms(self.bot))
|
resp = await this_q.ask(await dropadd.get_gms(self.bot))
|
||||||
|
|
||||||
if resp is None:
|
if resp is None:
|
||||||
await dropadd.send('RIP this move. Maybe next time.')
|
await dropadd.send('RIP this move. Maybe next time.')
|
||||||
@ -1551,7 +1896,7 @@ class Transactions(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
this_q.prompt = 'Who are you sending to the IL?'
|
this_q.prompt = 'Who are you sending to the IL?'
|
||||||
this_q.qtype = 'text'
|
this_q.qtype = 'text'
|
||||||
resp = await this_q.ask(dropadd.get_gms(self.bot))
|
resp = await this_q.ask(await dropadd.get_gms(self.bot))
|
||||||
|
|
||||||
if resp is None:
|
if resp is None:
|
||||||
await dropadd.send('RIP this move. Maybe next time.')
|
await dropadd.send('RIP this move. Maybe next time.')
|
||||||
@ -1583,7 +1928,7 @@ class Transactions(commands.Cog):
|
|||||||
while True and not OFFSEASON_FLAG:
|
while True and not OFFSEASON_FLAG:
|
||||||
prompt = f'Are you adding someone to the Major League team?'
|
prompt = f'Are you adding someone to the Major League team?'
|
||||||
this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300)
|
this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300)
|
||||||
resp = await this_q.ask(dropadd.get_gms(self.bot))
|
resp = await this_q.ask(await dropadd.get_gms(self.bot))
|
||||||
|
|
||||||
if resp is None:
|
if resp is None:
|
||||||
await dropadd.send('RIP this move. Maybe next time.')
|
await dropadd.send('RIP this move. Maybe next time.')
|
||||||
@ -1594,7 +1939,7 @@ class Transactions(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
this_q.prompt = 'Who are you adding?'
|
this_q.prompt = 'Who are you adding?'
|
||||||
this_q.qtype = 'text'
|
this_q.qtype = 'text'
|
||||||
resp = await this_q.ask(dropadd.get_gms(self.bot))
|
resp = await this_q.ask(await dropadd.get_gms(self.bot))
|
||||||
|
|
||||||
if resp is None:
|
if resp is None:
|
||||||
await dropadd.send('RIP this move. Maybe next time.')
|
await dropadd.send('RIP this move. Maybe next time.')
|
||||||
@ -1625,7 +1970,7 @@ class Transactions(commands.Cog):
|
|||||||
while True and not OFFSEASON_FLAG:
|
while True and not OFFSEASON_FLAG:
|
||||||
prompt = f'Are you adding someone to the Minor League team?'
|
prompt = f'Are you adding someone to the Minor League team?'
|
||||||
this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300)
|
this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300)
|
||||||
resp = await this_q.ask(dropadd.get_gms(self.bot))
|
resp = await this_q.ask(await dropadd.get_gms(self.bot))
|
||||||
|
|
||||||
if resp is None:
|
if resp is None:
|
||||||
await dropadd.send('RIP this move. Maybe next time.')
|
await dropadd.send('RIP this move. Maybe next time.')
|
||||||
@ -1636,7 +1981,7 @@ class Transactions(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
this_q.prompt = 'Who are you sending to the MiL?'
|
this_q.prompt = 'Who are you sending to the MiL?'
|
||||||
this_q.qtype = 'text'
|
this_q.qtype = 'text'
|
||||||
resp = await this_q.ask(dropadd.get_gms(self.bot))
|
resp = await this_q.ask(await dropadd.get_gms(self.bot))
|
||||||
|
|
||||||
if resp is None:
|
if resp is None:
|
||||||
await dropadd.send('RIP this move. Maybe next time.')
|
await dropadd.send('RIP this move. Maybe next time.')
|
||||||
@ -1671,7 +2016,7 @@ class Transactions(commands.Cog):
|
|||||||
while True:
|
while True:
|
||||||
prompt = f'Are you dropping anyone to FA?'
|
prompt = f'Are you dropping anyone to FA?'
|
||||||
this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300)
|
this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300)
|
||||||
resp = await this_q.ask(dropadd.get_gms(self.bot))
|
resp = await this_q.ask(await dropadd.get_gms(self.bot))
|
||||||
|
|
||||||
if resp is None:
|
if resp is None:
|
||||||
await dropadd.send('RIP this move. Maybe next time.')
|
await dropadd.send('RIP this move. Maybe next time.')
|
||||||
@ -1682,7 +2027,7 @@ class Transactions(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
this_q.prompt = 'Who are you dropping to FA?'
|
this_q.prompt = 'Who are you dropping to FA?'
|
||||||
this_q.qtype = 'text'
|
this_q.qtype = 'text'
|
||||||
resp = await this_q.ask(dropadd.get_gms(self.bot))
|
resp = await this_q.ask(await dropadd.get_gms(self.bot))
|
||||||
|
|
||||||
if resp is None:
|
if resp is None:
|
||||||
await dropadd.send('RIP this move. Maybe next time.')
|
await dropadd.send('RIP this move. Maybe next time.')
|
||||||
@ -1724,18 +2069,20 @@ class Transactions(commands.Cog):
|
|||||||
roster_errors = []
|
roster_errors = []
|
||||||
for team in dropadd.teams:
|
for team in dropadd.teams:
|
||||||
data = await dropadd.check_major_league_errors(team)
|
data = await dropadd.check_major_league_errors(team)
|
||||||
|
team_obj = dropadd.teams[team]["team"]
|
||||||
|
team_cap = get_team_salary_cap(team_obj)
|
||||||
|
|
||||||
if data['wara'] > 32.001 and not OFFSEASON_FLAG:
|
if exceeds_salary_cap(data['wara'], team_obj) and not OFFSEASON_FLAG:
|
||||||
errors.append(f'- {dropadd.teams[team]["team"]["abbrev"]} would have {data["wara"]:.2f} WARa')
|
errors.append(f'- {team_obj["abbrev"]} would have {data["wara"]:.2f} WARa (cap {team_cap:.1f})')
|
||||||
|
|
||||||
if len(data['roster']) > 26 and not OFFSEASON_FLAG:
|
if len(data['roster']) > 26 and not OFFSEASON_FLAG:
|
||||||
errors.append(f'- {dropadd.teams[team]["team"]["abbrev"]} would have {len(data["roster"])} players')
|
errors.append(f'- {team_obj["abbrev"]} would have {len(data["roster"])} players')
|
||||||
|
|
||||||
if (data['wara'] > 32.001 or len(data['roster']) > 26) and not OFFSEASON_FLAG:
|
if (exceeds_salary_cap(data['wara'], team_obj) or len(data['roster']) > 26) and not OFFSEASON_FLAG:
|
||||||
roster_string = ''
|
roster_string = ''
|
||||||
for x in data['roster']:
|
for x in data['roster']:
|
||||||
roster_string += f'{x["wara"]: >5} - {x["name"]}\n'
|
roster_string += f'{x["wara"]: >5.2f} - {x["name"]}\n'
|
||||||
errors.append(f'- This is the roster I have for {dropadd.teams[team]["team"]["abbrev"]}:\n'
|
errors.append(f'- This is the roster I have for {team_obj["abbrev"]}:\n'
|
||||||
f'```\n{roster_string}```')
|
f'```\n{roster_string}```')
|
||||||
|
|
||||||
if len(errors) + len(roster_errors) > 0:
|
if len(errors) + len(roster_errors) > 0:
|
||||||
@ -1751,7 +2098,7 @@ class Transactions(commands.Cog):
|
|||||||
for team in dropadd.teams:
|
for team in dropadd.teams:
|
||||||
this_q.prompt = f'{dropadd.teams[team]["role"].mention}\nWould you like me to run this move?'
|
this_q.prompt = f'{dropadd.teams[team]["role"].mention}\nWould you like me to run this move?'
|
||||||
this_q.qtype = 'yesno'
|
this_q.qtype = 'yesno'
|
||||||
resp = await this_q.ask(dropadd.get_gms(self.bot, team))
|
resp = await this_q.ask(await dropadd.get_gms(self.bot, team))
|
||||||
|
|
||||||
if not resp:
|
if not resp:
|
||||||
await dropadd.send('RIP this move. Maybe next time.')
|
await dropadd.send('RIP this move. Maybe next time.')
|
||||||
@ -1816,7 +2163,7 @@ class Transactions(commands.Cog):
|
|||||||
guaranteed[x["moveid"]] = []
|
guaranteed[x["moveid"]] = []
|
||||||
|
|
||||||
guaranteed[x["moveid"]].append(
|
guaranteed[x["moveid"]].append(
|
||||||
f'**{x["player"]["name"]}** ({x["player"]["wara"]}) from '
|
f'**{x["player"]["name"]}** ({x["player"]["wara"]:.2f}) from '
|
||||||
f'{x["oldteam"]["abbrev"]} to {x["newteam"]["abbrev"]}\n'
|
f'{x["oldteam"]["abbrev"]} to {x["newteam"]["abbrev"]}\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1825,7 +2172,7 @@ class Transactions(commands.Cog):
|
|||||||
frozen[x["moveid"]] = []
|
frozen[x["moveid"]] = []
|
||||||
|
|
||||||
frozen[x["moveid"]].append(
|
frozen[x["moveid"]].append(
|
||||||
f'**{x["player"]["name"]}** ({x["player"]["wara"]}) from '
|
f'**{x["player"]["name"]}** ({x["player"]["wara"]:.2f}) from '
|
||||||
f'{x["oldteam"]["abbrev"]} to {x["newteam"]["abbrev"]}\n'
|
f'{x["oldteam"]["abbrev"]} to {x["newteam"]["abbrev"]}\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1866,6 +2213,7 @@ class Transactions(commands.Cog):
|
|||||||
embed.description = f'Week {current.week + 1}'
|
embed.description = f'Week {current.week + 1}'
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
|
team_cap = get_team_salary_cap(this_team)
|
||||||
|
|
||||||
sil_wara = roster['shortil']['WARa']
|
sil_wara = roster['shortil']['WARa']
|
||||||
total_wara = roster['active']['WARa']
|
total_wara = roster['active']['WARa']
|
||||||
@ -1874,8 +2222,8 @@ class Transactions(commands.Cog):
|
|||||||
wara_string += f' ({sil_wara:.2f} IL)'
|
wara_string += f' ({sil_wara:.2f} IL)'
|
||||||
|
|
||||||
embed.add_field(name='sWAR', value=wara_string)
|
embed.add_field(name='sWAR', value=wara_string)
|
||||||
if total_wara > 32.001:
|
if exceeds_salary_cap(total_wara, this_team):
|
||||||
errors.append(f'- sWAR currently {total_wara:.2f} (cap 32.0)')
|
errors.append(f'- sWAR currently {total_wara:.2f} (cap {team_cap:.1f})')
|
||||||
|
|
||||||
player_count = len(roster["active"]["players"])
|
player_count = len(roster["active"]["players"])
|
||||||
embed.add_field(name='Player Count', value=f'{player_count}')
|
embed.add_field(name='Player Count', value=f'{player_count}')
|
||||||
@ -1952,7 +2300,7 @@ class Transactions(commands.Cog):
|
|||||||
if player['team']['id'] != team['id']:
|
if player['team']['id'] != team['id']:
|
||||||
await ctx.send(f'Omg stop trying to make {player["name"]} happen. It\'s not going to happen.')
|
await ctx.send(f'Omg stop trying to make {player["name"]} happen. It\'s not going to happen.')
|
||||||
else:
|
else:
|
||||||
output_string += f'**{player["name"]}** ({player["wara"]}) to MiL\n'
|
output_string += f'**{player["name"]}** ({player["wara"]:.2f}) to MiL\n'
|
||||||
if player['team']['id'] == team['id']:
|
if player['team']['id'] == team['id']:
|
||||||
moves.append({
|
moves.append({
|
||||||
'week': 1,
|
'week': 1,
|
||||||
|
|||||||
@ -160,6 +160,12 @@ async def db_delete(endpoint: str, object_id: int, api_ver: int = 3, timeout=3):
|
|||||||
raise ValueError(f'DB: {e}')
|
raise ValueError(f'DB: {e}')
|
||||||
|
|
||||||
|
|
||||||
|
# async def db_delete_transaction(move_id: str):
|
||||||
|
# req_url = get_req_url('transactions', api_ver=3, object_id=move_id)
|
||||||
|
# log_string = f'delete:\n{endpoint} {object_id}'
|
||||||
|
# logger.info(log_string) if master_debug else logger.debug(log_string)
|
||||||
|
|
||||||
|
|
||||||
async def get_team_by_abbrev(team_abbrev: str, season: int):
|
async def get_team_by_abbrev(team_abbrev: str, season: int):
|
||||||
t_query = await db_get('teams', params=[('season', season), ('team_abbrev', team_abbrev)])
|
t_query = await db_get('teams', params=[('season', season), ('team_abbrev', team_abbrev)])
|
||||||
if not t_query or t_query['count'] == 0:
|
if not t_query or t_query['count'] == 0:
|
||||||
|
|||||||
85
helpers.py
85
helpers.py
@ -14,6 +14,7 @@ import json
|
|||||||
|
|
||||||
import discord
|
import discord
|
||||||
import requests
|
import requests
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from difflib import get_close_matches
|
from difflib import get_close_matches
|
||||||
@ -25,6 +26,10 @@ PD_SEASON = 9
|
|||||||
FA_LOCK_WEEK = 14
|
FA_LOCK_WEEK = 14
|
||||||
SBA_COLOR = 'a6ce39'
|
SBA_COLOR = 'a6ce39'
|
||||||
|
|
||||||
|
# Salary cap constants
|
||||||
|
DEFAULT_SALARY_CAP = 32.0
|
||||||
|
SALARY_CAP_TOLERANCE = 0.001 # Small tolerance for floating point comparisons
|
||||||
|
|
||||||
SBA_ROSTER_KEY = '1bt7LLJe6h7axkhDVlxJ4f319l8QmFB0zQH-pjM0c8a8'
|
SBA_ROSTER_KEY = '1bt7LLJe6h7axkhDVlxJ4f319l8QmFB0zQH-pjM0c8a8'
|
||||||
SBA_STATS_KEY = '1fnqx2uxC7DT5aTnx4EkXh83crwrL0W6eJefoC1d4KH4'
|
SBA_STATS_KEY = '1fnqx2uxC7DT5aTnx4EkXh83crwrL0W6eJefoC1d4KH4'
|
||||||
SBA_STANDINGS_KEY = '1cXZcPY08RvqV_GeLvZ7PY5-0CyM-AijpJxsaFisZjBc'
|
SBA_STANDINGS_KEY = '1cXZcPY08RvqV_GeLvZ7PY5-0CyM-AijpJxsaFisZjBc'
|
||||||
@ -37,7 +42,8 @@ SBA_SEASON7_DRAFT_KEY = '1BgySsUlQf9K21_uOjQOY7O0GrRfF6zt1BBaEFlvBokY'
|
|||||||
SBA_SEASON8_DRAFT_KEY = '1FG4cAs8OeTdrreRqu8D-APxibjB3RiEzn34KTTBLLDk'
|
SBA_SEASON8_DRAFT_KEY = '1FG4cAs8OeTdrreRqu8D-APxibjB3RiEzn34KTTBLLDk'
|
||||||
SBA_SEASON9_DRAFT_KEY = '1eyHqaVU9rtmhG1p0ZktOrz7FMDp3c_unCcFyMMYceLc'
|
SBA_SEASON9_DRAFT_KEY = '1eyHqaVU9rtmhG1p0ZktOrz7FMDp3c_unCcFyMMYceLc'
|
||||||
DRAFT_KEY = {
|
DRAFT_KEY = {
|
||||||
11: '1Fz3GcTb7b9tLe8vkpyn59wRwC6P2QzxnLKtp7371sUc'
|
11: '1Fz3GcTb7b9tLe8vkpyn59wRwC6P2QzxnLKtp7371sUc',
|
||||||
|
12: '1OF-sAFykebc_2BrcYCgxCR-4rJo0GaNmTstagV-PMBU'
|
||||||
}
|
}
|
||||||
SBA_STANDINGS_URL = f'{SBA_BASE_URL}/standings'
|
SBA_STANDINGS_URL = f'{SBA_BASE_URL}/standings'
|
||||||
SBA_SCHEDULE_URL = f'{SBA_BASE_URL}/schedule'
|
SBA_SCHEDULE_URL = f'{SBA_BASE_URL}/schedule'
|
||||||
@ -213,9 +219,12 @@ class Question:
|
|||||||
def text(mes):
|
def text(mes):
|
||||||
return mes.channel == self.channel and mes.author in responders
|
return mes.channel == self.channel and mes.author in responders
|
||||||
|
|
||||||
|
logger.info(f'Sending watched message')
|
||||||
await self.channel.send(content=self.prompt, embed=self.embed)
|
await self.channel.send(content=self.prompt, embed=self.embed)
|
||||||
|
logger.info(f'message is sent')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
logger.info(f'waiting for a response')
|
||||||
resp = await self.bot.wait_for(
|
resp = await self.bot.wait_for(
|
||||||
'message',
|
'message',
|
||||||
timeout=self.timeout,
|
timeout=self.timeout,
|
||||||
@ -864,7 +873,7 @@ async def get_player_embed(player, current, ctx=None, season=None):
|
|||||||
embed.add_field(name='P Injury', value=f'{player["pitcher_injury"]} (6-{13 - player["pitcher_injury"]})')
|
embed.add_field(name='P Injury', value=f'{player["pitcher_injury"]} (6-{13 - player["pitcher_injury"]})')
|
||||||
else:
|
else:
|
||||||
embed.add_field(name='P Injury', value=f'{player["pitcher_injury"]} (---)')
|
embed.add_field(name='P Injury', value=f'{player["pitcher_injury"]} (---)')
|
||||||
if d_query['count'] > 0:
|
if d_query and d_query.get('count') > 0:
|
||||||
pick = d_query['picks'][0]
|
pick = d_query['picks'][0]
|
||||||
num = pick["overall"] % 16
|
num = pick["overall"] % 16
|
||||||
if num == 0:
|
if num == 0:
|
||||||
@ -895,7 +904,7 @@ async def get_player_embed(player, current, ctx=None, season=None):
|
|||||||
|
|
||||||
b_query = await db_get('battingstats/totals', params=[
|
b_query = await db_get('battingstats/totals', params=[
|
||||||
('season', player['season']), ('player_id', player['id']), ('s_type', 'regular')])
|
('season', player['season']), ('player_id', player['id']), ('s_type', 'regular')])
|
||||||
if b_query['count'] > 0:
|
if b_query and b_query.get('count') > 0:
|
||||||
b = b_query['stats'][0]
|
b = b_query['stats'][0]
|
||||||
|
|
||||||
if b['ab'] > 0:
|
if b['ab'] > 0:
|
||||||
@ -926,7 +935,7 @@ async def get_player_embed(player, current, ctx=None, season=None):
|
|||||||
|
|
||||||
p_query = await db_get('pitchingstats/totals', params=[
|
p_query = await db_get('pitchingstats/totals', params=[
|
||||||
('season', player['season']), ('player_id', player['id']), ('s_type', 'regular')])
|
('season', player['season']), ('player_id', player['id']), ('s_type', 'regular')])
|
||||||
if p_query['count'] > 0:
|
if p_query and p_query.get('count') > 0:
|
||||||
p = p_query['stats'][0]
|
p = p_query['stats'][0]
|
||||||
|
|
||||||
if p['ip'] > 0:
|
if p['ip'] > 0:
|
||||||
@ -966,7 +975,7 @@ async def get_player_embed(player, current, ctx=None, season=None):
|
|||||||
batter_priority = True
|
batter_priority = True
|
||||||
b, p, batting_string, pitching_string = None, None, None, None
|
b, p, batting_string, pitching_string = None, None, None, None
|
||||||
|
|
||||||
if b_query['count'] > 0:
|
if b_query and b_query.get('count') > 0:
|
||||||
b = b_query['stats'][0]
|
b = b_query['stats'][0]
|
||||||
batting_string = f'```\n' \
|
batting_string = f'```\n' \
|
||||||
f' AVG OBP SLG\n' \
|
f' AVG OBP SLG\n' \
|
||||||
@ -976,7 +985,7 @@ async def get_player_embed(player, current, ctx=None, season=None):
|
|||||||
f' PA H RBI 2B 3B HR SB\n' \
|
f' PA H RBI 2B 3B HR SB\n' \
|
||||||
f'{b["pa"]: >3} {b["hit"]: ^3} {b["rbi"]: ^3} {b["double"]: >2} {b["triple"]: >2} ' \
|
f'{b["pa"]: >3} {b["hit"]: ^3} {b["rbi"]: ^3} {b["double"]: >2} {b["triple"]: >2} ' \
|
||||||
f'{b["hr"]: >2} {b["sb"]: >2}```\n'
|
f'{b["hr"]: >2} {b["sb"]: >2}```\n'
|
||||||
if p_query['count'] > 0:
|
if p_query and p_query.get('count') > 0:
|
||||||
p = p_query['stats'][0]
|
p = p_query['stats'][0]
|
||||||
if b is None or p["tbf"] > b["pa"]:
|
if b is None or p["tbf"] > b["pa"]:
|
||||||
batter_priority = False
|
batter_priority = False
|
||||||
@ -1182,3 +1191,67 @@ def random_from_list(data_list: list):
|
|||||||
|
|
||||||
def get_team_url(this_team):
|
def get_team_url(this_team):
|
||||||
return f'https://sba.manticorum.com/teams/{this_team["season"]}/{this_team["abbrev"]}'
|
return f'https://sba.manticorum.com/teams/{this_team["season"]}/{this_team["abbrev"]}'
|
||||||
|
|
||||||
|
|
||||||
|
async def send_owner_notification(message: str):
|
||||||
|
"""
|
||||||
|
Send a notification message to the bot owner via Discord webhook.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (str): The message content to send
|
||||||
|
"""
|
||||||
|
logger = logging.getLogger('discord_app')
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
webhook_url = "https://discord.com/api/webhooks/1408811717424840876/7RXG_D5IqovA3Jwa9YOobUjVcVMuLc6cQyezABcWuXaHo5Fvz1en10M7J43o3OJ3bzGW"
|
||||||
|
payload = {
|
||||||
|
"content": message
|
||||||
|
}
|
||||||
|
await session.post(webhook_url, json=payload)
|
||||||
|
except Exception as webhook_error:
|
||||||
|
logger.error(f'Failed to send owner notification: {webhook_error}')
|
||||||
|
|
||||||
|
|
||||||
|
def get_team_salary_cap(team) -> float:
|
||||||
|
"""
|
||||||
|
Get the salary cap for a team, falling back to the default if not set.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
team: Team data - can be a dict or Pydantic model with 'salary_cap' attribute.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: The team's salary cap, or DEFAULT_SALARY_CAP (32.0) if not set.
|
||||||
|
|
||||||
|
Why: Teams may have custom salary caps (e.g., for expansion teams or penalties).
|
||||||
|
This centralizes the fallback logic so all cap checks use the same source of truth.
|
||||||
|
"""
|
||||||
|
if team is None:
|
||||||
|
return DEFAULT_SALARY_CAP
|
||||||
|
|
||||||
|
# Handle both dict and Pydantic model (or any object with salary_cap attribute)
|
||||||
|
if isinstance(team, dict):
|
||||||
|
salary_cap = team.get('salary_cap')
|
||||||
|
else:
|
||||||
|
salary_cap = getattr(team, 'salary_cap', None)
|
||||||
|
|
||||||
|
if salary_cap is not None:
|
||||||
|
return salary_cap
|
||||||
|
return DEFAULT_SALARY_CAP
|
||||||
|
|
||||||
|
|
||||||
|
def exceeds_salary_cap(wara: float, team) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a WAR total exceeds the team's salary cap.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wara: The total WAR value to check
|
||||||
|
team: Team data - can be a dict or Pydantic model
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if wara exceeds the team's salary cap (with tolerance)
|
||||||
|
|
||||||
|
Why: Centralizes the salary cap comparison logic with proper floating point
|
||||||
|
tolerance handling. All cap validation should use this function.
|
||||||
|
"""
|
||||||
|
cap = get_team_salary_cap(team)
|
||||||
|
return wara > (cap + SALARY_CAP_TOLERANCE)
|
||||||
|
|||||||
102
salary_cap_refactor_plan.json
Normal file
102
salary_cap_refactor_plan.json
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"project": "Dynamic Salary Cap Refactor",
|
||||||
|
"description": "Replace hardcoded salary cap values (32.0/32.001) with Team.salary_cap field",
|
||||||
|
"created": "2025-12-09",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": "SWAR-001",
|
||||||
|
"name": "Add salary cap helper function",
|
||||||
|
"description": "Create a helper function in helpers.py that retrieves the salary cap for a team, with fallback to default 32.0 for backwards compatibility",
|
||||||
|
"files": ["helpers.py"],
|
||||||
|
"lines": [1215, 1257],
|
||||||
|
"priority": 1,
|
||||||
|
"completed": true,
|
||||||
|
"tested": true,
|
||||||
|
"notes": "Added get_team_salary_cap() and exceeds_salary_cap() functions with Pydantic model support"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "SWAR-002",
|
||||||
|
"name": "Update draft cap space check",
|
||||||
|
"description": "Replace hardcoded 32.00001 cap check in draft.py with team.salary_cap from the Team model",
|
||||||
|
"files": ["cogs/draft.py"],
|
||||||
|
"lines": [442, 447],
|
||||||
|
"priority": 2,
|
||||||
|
"completed": true,
|
||||||
|
"tested": false,
|
||||||
|
"notes": "Now uses exceeds_salary_cap() and shows actual cap in error message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "SWAR-003",
|
||||||
|
"name": "Update trade validation sWAR check",
|
||||||
|
"description": "Replace hardcoded 32.001 cap threshold in trade validation with team.salary_cap",
|
||||||
|
"files": ["cogs/transactions.py"],
|
||||||
|
"lines": [1281, 1293],
|
||||||
|
"priority": 2,
|
||||||
|
"completed": true,
|
||||||
|
"tested": false,
|
||||||
|
"notes": "Now uses exceeds_salary_cap() and shows actual cap in error message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "SWAR-004",
|
||||||
|
"name": "Update first drop/add validation",
|
||||||
|
"description": "Replace hardcoded 32.001 cap threshold in first drop/add validation block with team.salary_cap",
|
||||||
|
"files": ["cogs/transactions.py"],
|
||||||
|
"lines": [1768, 1780],
|
||||||
|
"priority": 2,
|
||||||
|
"completed": true,
|
||||||
|
"tested": false,
|
||||||
|
"notes": "Now uses exceeds_salary_cap() and shows actual cap in error message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "SWAR-005",
|
||||||
|
"name": "Update second drop/add validation",
|
||||||
|
"description": "Replace hardcoded 32.001 cap threshold in second drop/add validation block with team.salary_cap",
|
||||||
|
"files": ["cogs/transactions.py"],
|
||||||
|
"lines": [2072, 2081],
|
||||||
|
"priority": 2,
|
||||||
|
"completed": true,
|
||||||
|
"tested": false,
|
||||||
|
"notes": "Now uses exceeds_salary_cap() and shows actual cap in error message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "SWAR-006",
|
||||||
|
"name": "Update roster validation error message",
|
||||||
|
"description": "Replace hardcoded '(cap 32.0)' in error message with actual team salary_cap value",
|
||||||
|
"files": ["cogs/transactions.py"],
|
||||||
|
"lines": [2216, 2226],
|
||||||
|
"priority": 2,
|
||||||
|
"completed": true,
|
||||||
|
"tested": false,
|
||||||
|
"notes": "Now uses exceeds_salary_cap() and shows actual cap in error message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "SWAR-007",
|
||||||
|
"name": "Add default salary cap constant",
|
||||||
|
"description": "Define DEFAULT_SALARY_CAP = 32.0 constant in helpers.py for use as fallback when team.salary_cap is None",
|
||||||
|
"files": ["helpers.py"],
|
||||||
|
"lines": [30, 31],
|
||||||
|
"priority": 1,
|
||||||
|
"completed": true,
|
||||||
|
"tested": true,
|
||||||
|
"notes": "Added DEFAULT_SALARY_CAP and SALARY_CAP_TOLERANCE constants"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "SWAR-008",
|
||||||
|
"name": "Verify Team model salary_cap field",
|
||||||
|
"description": "Confirm api_calls/team.py has salary_cap field properly defined and API returns this value",
|
||||||
|
"files": ["api_calls/team.py"],
|
||||||
|
"lines": [26],
|
||||||
|
"priority": 1,
|
||||||
|
"completed": true,
|
||||||
|
"tested": true,
|
||||||
|
"notes": "Field exists: salary_cap: Optional[float] = None"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"completion_checklist": [
|
||||||
|
"All hardcoded 32.0/32.001 values replaced - DONE",
|
||||||
|
"Helper function created with fallback logic - DONE",
|
||||||
|
"All affected cogs tested manually - PENDING",
|
||||||
|
"Unit tests added for salary cap validation - DONE (21 tests)",
|
||||||
|
"Error messages display correct cap values - DONE"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -61,7 +61,7 @@ async def test_patch_draftpick_success():
|
|||||||
round=1,
|
round=1,
|
||||||
origowner=Team(id=1, abbrev='AAA', sname='Alpha', lname='Alpha Squad', season=2025),
|
origowner=Team(id=1, abbrev='AAA', sname='Alpha', lname='Alpha Squad', season=2025),
|
||||||
owner=Team(id=2, abbrev='BBB', sname='Beta', lname='Beta Crew', season=2025),
|
owner=Team(id=2, abbrev='BBB', sname='Beta', lname='Beta Crew', season=2025),
|
||||||
player=Player(id=99, name="Test Player", team=Team(id=2, abbrev='BBB', sname='Beta', lname='Beta Crew', season=2025))
|
player=Player(id=99, name="Test Player", wara=2.5, image="test.png", season=2025, pos_1="SS", team=Team(id=2, abbrev='BBB', sname='Beta', lname='Beta Crew', season=2025))
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_response = {
|
mock_response = {
|
||||||
|
|||||||
421
tests/test_salary_cap.py
Normal file
421
tests/test_salary_cap.py
Normal file
@ -0,0 +1,421 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for salary cap helper functions in helpers.py.
|
||||||
|
|
||||||
|
These tests verify:
|
||||||
|
1. get_team_salary_cap() returns correct cap values with fallback behavior
|
||||||
|
2. exceeds_salary_cap() correctly identifies when WAR exceeds team cap
|
||||||
|
3. Edge cases around None values and floating point tolerance
|
||||||
|
|
||||||
|
Why these tests matter:
|
||||||
|
- Salary cap validation is critical for league integrity during trades/drafts
|
||||||
|
- The helper functions centralize logic previously scattered across cogs
|
||||||
|
- Proper fallback behavior ensures backwards compatibility
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from helpers import (
|
||||||
|
DEFAULT_SALARY_CAP,
|
||||||
|
SALARY_CAP_TOLERANCE,
|
||||||
|
get_team_salary_cap,
|
||||||
|
exceeds_salary_cap
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetTeamSalaryCap:
|
||||||
|
"""Tests for get_team_salary_cap() function."""
|
||||||
|
|
||||||
|
def test_returns_team_salary_cap_when_set(self):
|
||||||
|
"""
|
||||||
|
When a team has a custom salary_cap value set, return that value.
|
||||||
|
|
||||||
|
Why: Some teams may have different caps (expansion teams, penalties, etc.)
|
||||||
|
"""
|
||||||
|
team = {'abbrev': 'TEST', 'salary_cap': 35.0}
|
||||||
|
result = get_team_salary_cap(team)
|
||||||
|
assert result == 35.0
|
||||||
|
|
||||||
|
def test_returns_default_when_salary_cap_is_none(self):
|
||||||
|
"""
|
||||||
|
When team.salary_cap is None, return the default cap (32.0).
|
||||||
|
|
||||||
|
Why: Most teams use the standard cap; None indicates no custom value.
|
||||||
|
"""
|
||||||
|
team = {'abbrev': 'TEST', 'salary_cap': None}
|
||||||
|
result = get_team_salary_cap(team)
|
||||||
|
assert result == DEFAULT_SALARY_CAP
|
||||||
|
assert result == 32.0
|
||||||
|
|
||||||
|
def test_returns_default_when_salary_cap_key_missing(self):
|
||||||
|
"""
|
||||||
|
When the salary_cap key doesn't exist in team dict, return default.
|
||||||
|
|
||||||
|
Why: Backwards compatibility with older team data structures.
|
||||||
|
"""
|
||||||
|
team = {'abbrev': 'TEST', 'sname': 'Test Team'}
|
||||||
|
result = get_team_salary_cap(team)
|
||||||
|
assert result == DEFAULT_SALARY_CAP
|
||||||
|
|
||||||
|
def test_returns_default_when_team_is_none(self):
|
||||||
|
"""
|
||||||
|
When team is None, return the default cap.
|
||||||
|
|
||||||
|
Why: Defensive programming - callers may pass None in edge cases.
|
||||||
|
"""
|
||||||
|
result = get_team_salary_cap(None)
|
||||||
|
assert result == DEFAULT_SALARY_CAP
|
||||||
|
|
||||||
|
def test_returns_default_when_team_is_empty_dict(self):
|
||||||
|
"""
|
||||||
|
When team is an empty dict, return the default cap.
|
||||||
|
|
||||||
|
Why: Edge case handling for malformed team data.
|
||||||
|
"""
|
||||||
|
result = get_team_salary_cap({})
|
||||||
|
assert result == DEFAULT_SALARY_CAP
|
||||||
|
|
||||||
|
def test_respects_zero_salary_cap(self):
|
||||||
|
"""
|
||||||
|
When salary_cap is explicitly 0, return 0 (not default).
|
||||||
|
|
||||||
|
Why: Zero is a valid value (e.g., suspended team), distinct from None.
|
||||||
|
"""
|
||||||
|
team = {'abbrev': 'BANNED', 'salary_cap': 0.0}
|
||||||
|
result = get_team_salary_cap(team)
|
||||||
|
assert result == 0.0
|
||||||
|
|
||||||
|
def test_handles_integer_salary_cap(self):
|
||||||
|
"""
|
||||||
|
When salary_cap is an integer, return it as-is.
|
||||||
|
|
||||||
|
Why: API may return int instead of float; function should handle both.
|
||||||
|
"""
|
||||||
|
team = {'abbrev': 'TEST', 'salary_cap': 30}
|
||||||
|
result = get_team_salary_cap(team)
|
||||||
|
assert result == 30
|
||||||
|
|
||||||
|
|
||||||
|
class TestExceedsSalaryCap:
|
||||||
|
"""Tests for exceeds_salary_cap() function."""
|
||||||
|
|
||||||
|
def test_returns_false_when_under_cap(self):
|
||||||
|
"""
|
||||||
|
WAR of 30.0 should not exceed default cap of 32.0.
|
||||||
|
|
||||||
|
Why: Normal case - team is under cap and should pass validation.
|
||||||
|
"""
|
||||||
|
team = {'abbrev': 'TEST', 'salary_cap': 32.0}
|
||||||
|
result = exceeds_salary_cap(30.0, team)
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
def test_returns_false_when_exactly_at_cap(self):
|
||||||
|
"""
|
||||||
|
WAR of exactly 32.0 should not exceed cap (within tolerance).
|
||||||
|
|
||||||
|
Why: Teams should be allowed to be exactly at cap limit.
|
||||||
|
"""
|
||||||
|
team = {'abbrev': 'TEST', 'salary_cap': 32.0}
|
||||||
|
result = exceeds_salary_cap(32.0, team)
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
def test_returns_false_within_tolerance(self):
|
||||||
|
"""
|
||||||
|
WAR slightly above cap but within tolerance should not exceed.
|
||||||
|
|
||||||
|
Why: Floating point math may produce values like 32.0000001;
|
||||||
|
tolerance prevents false positives from rounding errors.
|
||||||
|
"""
|
||||||
|
team = {'abbrev': 'TEST', 'salary_cap': 32.0}
|
||||||
|
# 32.0005 is within 0.001 tolerance of 32.0
|
||||||
|
result = exceeds_salary_cap(32.0005, team)
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
def test_returns_true_when_over_cap(self):
|
||||||
|
"""
|
||||||
|
WAR of 33.0 clearly exceeds cap of 32.0.
|
||||||
|
|
||||||
|
Why: Core validation - must reject teams over cap.
|
||||||
|
"""
|
||||||
|
team = {'abbrev': 'TEST', 'salary_cap': 32.0}
|
||||||
|
result = exceeds_salary_cap(33.0, team)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
def test_returns_true_just_over_tolerance(self):
|
||||||
|
"""
|
||||||
|
WAR just beyond tolerance should exceed cap.
|
||||||
|
|
||||||
|
Why: Tolerance has a boundary; values beyond it must fail.
|
||||||
|
"""
|
||||||
|
team = {'abbrev': 'TEST', 'salary_cap': 32.0}
|
||||||
|
# 32.002 is beyond 0.001 tolerance
|
||||||
|
result = exceeds_salary_cap(32.002, team)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
def test_uses_team_custom_cap(self):
|
||||||
|
"""
|
||||||
|
Should use team's custom cap, not default.
|
||||||
|
|
||||||
|
Why: Teams with higher/lower caps must be validated correctly.
|
||||||
|
"""
|
||||||
|
team = {'abbrev': 'EXPANSION', 'salary_cap': 28.0}
|
||||||
|
# 30.0 is under default 32.0 but over custom 28.0
|
||||||
|
result = exceeds_salary_cap(30.0, team)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
def test_uses_default_cap_when_team_cap_none(self):
|
||||||
|
"""
|
||||||
|
When team has no custom cap, use default for comparison.
|
||||||
|
|
||||||
|
Why: Backwards compatibility - existing teams without salary_cap field.
|
||||||
|
"""
|
||||||
|
team = {'abbrev': 'TEST', 'salary_cap': None}
|
||||||
|
result = exceeds_salary_cap(33.0, team)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
result = exceeds_salary_cap(31.0, team)
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
def test_handles_none_team(self):
|
||||||
|
"""
|
||||||
|
When team is None, use default cap for comparison.
|
||||||
|
|
||||||
|
Why: Defensive programming for edge cases.
|
||||||
|
"""
|
||||||
|
result = exceeds_salary_cap(33.0, None)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
result = exceeds_salary_cap(31.0, None)
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestPydanticModelSupport:
|
||||||
|
"""Tests for Pydantic model support in helper functions."""
|
||||||
|
|
||||||
|
def test_get_team_salary_cap_with_pydantic_model(self):
|
||||||
|
"""
|
||||||
|
Should work with Pydantic models that have salary_cap attribute.
|
||||||
|
|
||||||
|
Why: Team objects in the codebase are often Pydantic models,
|
||||||
|
not just dicts. The helper must support both.
|
||||||
|
"""
|
||||||
|
class MockTeam:
|
||||||
|
salary_cap = 35.0
|
||||||
|
abbrev = 'TEST'
|
||||||
|
|
||||||
|
team = MockTeam()
|
||||||
|
result = get_team_salary_cap(team)
|
||||||
|
assert result == 35.0
|
||||||
|
|
||||||
|
def test_get_team_salary_cap_with_pydantic_model_none_cap(self):
|
||||||
|
"""
|
||||||
|
Pydantic model with salary_cap=None should return default.
|
||||||
|
|
||||||
|
Why: Many existing Team objects have salary_cap=None.
|
||||||
|
"""
|
||||||
|
class MockTeam:
|
||||||
|
salary_cap = None
|
||||||
|
abbrev = 'TEST'
|
||||||
|
|
||||||
|
team = MockTeam()
|
||||||
|
result = get_team_salary_cap(team)
|
||||||
|
assert result == DEFAULT_SALARY_CAP
|
||||||
|
|
||||||
|
def test_get_team_salary_cap_with_object_missing_attribute(self):
|
||||||
|
"""
|
||||||
|
Object without salary_cap attribute should return default.
|
||||||
|
|
||||||
|
Why: Defensive handling for objects that don't have the attribute.
|
||||||
|
"""
|
||||||
|
class MockTeam:
|
||||||
|
abbrev = 'TEST'
|
||||||
|
|
||||||
|
team = MockTeam()
|
||||||
|
result = get_team_salary_cap(team)
|
||||||
|
assert result == DEFAULT_SALARY_CAP
|
||||||
|
|
||||||
|
def test_exceeds_salary_cap_with_pydantic_model(self):
|
||||||
|
"""
|
||||||
|
exceeds_salary_cap should work with Pydantic-like objects.
|
||||||
|
|
||||||
|
Why: Draft and transaction code passes Team objects directly.
|
||||||
|
"""
|
||||||
|
class MockTeam:
|
||||||
|
salary_cap = 28.0
|
||||||
|
abbrev = 'EXPANSION'
|
||||||
|
|
||||||
|
team = MockTeam()
|
||||||
|
# 30.0 exceeds custom cap of 28.0
|
||||||
|
result = exceeds_salary_cap(30.0, team)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
# 27.0 does not exceed custom cap of 28.0
|
||||||
|
result = exceeds_salary_cap(27.0, team)
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestEdgeCases:
|
||||||
|
"""Tests for edge cases and boundary conditions."""
|
||||||
|
|
||||||
|
def test_negative_salary_cap(self):
|
||||||
|
"""
|
||||||
|
Negative salary cap should be returned as-is (even if nonsensical).
|
||||||
|
|
||||||
|
Why: Function should not validate business logic - just return the value.
|
||||||
|
If someone sets a negative cap, that's a data issue, not a helper issue.
|
||||||
|
"""
|
||||||
|
team = {'abbrev': 'BROKE', 'salary_cap': -5.0}
|
||||||
|
result = get_team_salary_cap(team)
|
||||||
|
assert result == -5.0
|
||||||
|
|
||||||
|
def test_negative_wara_under_cap(self):
|
||||||
|
"""
|
||||||
|
Negative WAR should not exceed any positive cap.
|
||||||
|
|
||||||
|
Why: Teams with negative WAR (all bad players) are clearly under cap.
|
||||||
|
"""
|
||||||
|
team = {'abbrev': 'TEST', 'salary_cap': 32.0}
|
||||||
|
result = exceeds_salary_cap(-10.0, team)
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
def test_negative_wara_with_negative_cap(self):
|
||||||
|
"""
|
||||||
|
Negative WAR vs negative cap - WAR higher than cap exceeds it.
|
||||||
|
|
||||||
|
Why: Edge case where both values are negative. -3.0 > -5.0 + 0.001
|
||||||
|
"""
|
||||||
|
team = {'abbrev': 'BROKE', 'salary_cap': -5.0}
|
||||||
|
# -3.0 > -4.999 (which is -5.0 + 0.001), so it exceeds
|
||||||
|
result = exceeds_salary_cap(-3.0, team)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
# -6.0 < -4.999, so it does not exceed
|
||||||
|
result = exceeds_salary_cap(-6.0, team)
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
def test_very_large_salary_cap(self):
|
||||||
|
"""
|
||||||
|
Very large salary cap values should work correctly.
|
||||||
|
|
||||||
|
Why: Ensure no overflow or precision issues with large numbers.
|
||||||
|
"""
|
||||||
|
team = {'abbrev': 'RICH', 'salary_cap': 1000000.0}
|
||||||
|
result = get_team_salary_cap(team)
|
||||||
|
assert result == 1000000.0
|
||||||
|
|
||||||
|
result = exceeds_salary_cap(999999.0, team)
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
result = exceeds_salary_cap(1000001.0, team)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
def test_very_small_salary_cap(self):
|
||||||
|
"""
|
||||||
|
Very small (but positive) salary cap should work.
|
||||||
|
|
||||||
|
Why: Some hypothetical penalty scenario with tiny cap.
|
||||||
|
"""
|
||||||
|
team = {'abbrev': 'TINY', 'salary_cap': 0.5}
|
||||||
|
result = exceeds_salary_cap(0.4, team)
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
result = exceeds_salary_cap(0.6, team)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
def test_float_precision_boundary(self):
|
||||||
|
"""
|
||||||
|
Test exact boundary of tolerance (cap + 0.001).
|
||||||
|
|
||||||
|
Why: Ensure the boundary condition is handled correctly.
|
||||||
|
The check is wara > (cap + tolerance), so exactly at boundary should NOT exceed.
|
||||||
|
"""
|
||||||
|
team = {'abbrev': 'TEST', 'salary_cap': 32.0}
|
||||||
|
# Exactly at cap + tolerance = 32.001
|
||||||
|
result = exceeds_salary_cap(32.001, team)
|
||||||
|
assert result is False # Not greater than, equal to
|
||||||
|
|
||||||
|
# Just barely over
|
||||||
|
result = exceeds_salary_cap(32.0011, team)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestRealTeamModel:
|
||||||
|
"""Tests using the actual Team Pydantic model from api_calls."""
|
||||||
|
|
||||||
|
def test_with_real_team_model(self):
|
||||||
|
"""
|
||||||
|
Test with the actual Team Pydantic model used in production.
|
||||||
|
|
||||||
|
Why: Ensures the helper works with real Team objects, not just mocks.
|
||||||
|
"""
|
||||||
|
from api_calls.team import Team
|
||||||
|
|
||||||
|
team = Team(
|
||||||
|
id=1,
|
||||||
|
abbrev='TEST',
|
||||||
|
sname='Test Team',
|
||||||
|
lname='Test Team Long Name',
|
||||||
|
season=12,
|
||||||
|
salary_cap=28.5
|
||||||
|
)
|
||||||
|
result = get_team_salary_cap(team)
|
||||||
|
assert result == 28.5
|
||||||
|
|
||||||
|
def test_with_real_team_model_none_cap(self):
|
||||||
|
"""
|
||||||
|
Real Team model with salary_cap=None should use default.
|
||||||
|
|
||||||
|
Why: This is the most common case in production.
|
||||||
|
"""
|
||||||
|
from api_calls.team import Team
|
||||||
|
|
||||||
|
team = Team(
|
||||||
|
id=2,
|
||||||
|
abbrev='STD',
|
||||||
|
sname='Standard Team',
|
||||||
|
lname='Standard Team Long Name',
|
||||||
|
season=12,
|
||||||
|
salary_cap=None
|
||||||
|
)
|
||||||
|
result = get_team_salary_cap(team)
|
||||||
|
assert result == DEFAULT_SALARY_CAP
|
||||||
|
|
||||||
|
def test_exceeds_with_real_team_model(self):
|
||||||
|
"""
|
||||||
|
exceeds_salary_cap with real Team model.
|
||||||
|
|
||||||
|
Why: End-to-end test with actual production model.
|
||||||
|
"""
|
||||||
|
from api_calls.team import Team
|
||||||
|
|
||||||
|
team = Team(
|
||||||
|
id=3,
|
||||||
|
abbrev='EXP',
|
||||||
|
sname='Expansion',
|
||||||
|
lname='Expansion Team',
|
||||||
|
season=12,
|
||||||
|
salary_cap=28.0
|
||||||
|
)
|
||||||
|
# 30.0 exceeds 28.0 cap
|
||||||
|
assert exceeds_salary_cap(30.0, team) is True
|
||||||
|
# 27.0 does not exceed 28.0 cap
|
||||||
|
assert exceeds_salary_cap(27.0, team) is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestConstants:
|
||||||
|
"""Tests for salary cap constants."""
|
||||||
|
|
||||||
|
def test_default_salary_cap_value(self):
|
||||||
|
"""
|
||||||
|
DEFAULT_SALARY_CAP should be 32.0 (league standard).
|
||||||
|
|
||||||
|
Why: Ensures constant wasn't accidentally changed.
|
||||||
|
"""
|
||||||
|
assert DEFAULT_SALARY_CAP == 32.0
|
||||||
|
|
||||||
|
def test_tolerance_value(self):
|
||||||
|
"""
|
||||||
|
SALARY_CAP_TOLERANCE should be 0.001.
|
||||||
|
|
||||||
|
Why: Tolerance must be small enough to catch real violations
|
||||||
|
but large enough to handle floating point imprecision.
|
||||||
|
"""
|
||||||
|
assert SALARY_CAP_TOLERANCE == 0.001
|
||||||
Loading…
Reference in New Issue
Block a user