Schedule fix, transaction priority fix
This commit is contained in:
parent
9bb84ce287
commit
2b4b84e193
@ -998,12 +998,15 @@ class Players(commands.Cog):
|
||||
param_list.append(('week', current.week))
|
||||
|
||||
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(
|
||||
content=f'Hm. I don\'t see any games then.'
|
||||
)
|
||||
return
|
||||
|
||||
# Sort games by game_id
|
||||
g_query['games'] = sorted(g_query['games'], key=lambda game: game['id'])
|
||||
|
||||
if team_abbrev is not None:
|
||||
title = f'{this_team["lname"]} Schedule'
|
||||
embed = get_team_embed(title=title, team=this_team)
|
||||
@ -2337,8 +2340,7 @@ class Players(commands.Cog):
|
||||
|
||||
@app_commands.command(name='charts')
|
||||
async def chart_command(self, interaction: discord.Interaction, chart_name: Literal[
|
||||
'block-plate', 'defense-matters', 'fly-b', 'g1', 'g2', 'g3', 'groundball', 'hit-and-run',
|
||||
'rest', 'rob-hr', 'sac-bunt', 'squeeze-bunt']):
|
||||
'rob-hr', 'defense', 'fly-b', 'g1', 'g2', 'g3', 'groundball','hit-and-run', 'rest', 'sac-bunt', 'squeeze-bunt']):
|
||||
gb_url = 'https://sombaseball.ddns.net/static/images/season04/ground-ball-chart'
|
||||
all_charts = {
|
||||
'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-help.png'
|
||||
],
|
||||
'rob-hr': [f'{SBA_IMAGE_URL}/season05/charts/rob-hr.png'],
|
||||
'defense-matters': [f'{SBA_IMAGE_URL}/season05/charts/defense-matters.png'],
|
||||
'rob-hr': [f'https://sba-cards-2024.s3.us-east-1.amazonaws.com/static-images/rob-hr.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'],
|
||||
'hit-and-run': [
|
||||
f'{SBA_IMAGE_URL}/season05/charts/hit-and-run.png',
|
||||
|
||||
@ -1,14 +1,232 @@
|
||||
import re
|
||||
import copy
|
||||
import random
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Tuple, Optional, Dict, Set
|
||||
|
||||
from helpers import *
|
||||
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 discord.ext import commands, tasks
|
||||
OFFSEASON_FLAG = False
|
||||
USE_NEW_TRANSACTION_LOGIC = os.getenv('USE_NEW_TRANSACTION_LOGIC', 'true').lower() == 'true'
|
||||
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:
|
||||
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 } } }
|
||||
@ -342,16 +560,19 @@ class Transactions(commands.Cog):
|
||||
|
||||
@tasks.loop(minutes=1)
|
||||
async def weekly_loop(self):
|
||||
logger.info(f'Inside weekly_loop')
|
||||
if OFFSEASON_FLAG:
|
||||
logger.info(f'Exiting weekly_loop; OFFSEASON_FLAG: {OFFSEASON_FLAG}')
|
||||
return
|
||||
|
||||
current = await get_current()
|
||||
now = datetime.datetime.now()
|
||||
logger.debug(f'Datetime: {now} / weekday: {now.weekday()}')
|
||||
logger.info(f'Datetime: {now} / weekday: {now.weekday()} / current: {current}')
|
||||
|
||||
# Begin Freeze
|
||||
# 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
|
||||
logger.info(f'weekly_loop - setting the freeze')
|
||||
current.week += 1
|
||||
await db_patch('current', object_id=current.id, params=[('week', current.week), ('freeze', True)])
|
||||
await self.run_transactions(current)
|
||||
@ -372,6 +593,7 @@ class Transactions(commands.Cog):
|
||||
# End Freeze
|
||||
# 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
|
||||
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}'
|
||||
@ -383,6 +605,9 @@ class Transactions(commands.Cog):
|
||||
await self.process_freeze_moves(current)
|
||||
await send_to_channel(self.bot, 'transaction-log', freeze_message)
|
||||
self.trade_season = False
|
||||
|
||||
else:
|
||||
logger.info(f'weekly_loop - No freeze actions being taken')
|
||||
|
||||
@weekly_loop.before_loop
|
||||
async def before_notif_check(self):
|
||||
@ -443,7 +668,8 @@ class Transactions(commands.Cog):
|
||||
except Exception as 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(
|
||||
# season=current.season,
|
||||
# week_start=current.week,
|
||||
@ -538,6 +764,84 @@ class Transactions(commands.Cog):
|
||||
await db_patch('transactions', object_id=move_id, params=[('frozen', False)])
|
||||
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):
|
||||
current = await get_current()
|
||||
# all_moves = await get_transactions(
|
||||
@ -624,7 +928,7 @@ class Transactions(commands.Cog):
|
||||
@commands.is_owner()
|
||||
async def process_freeze_helper_command(self, ctx):
|
||||
current = await get_current()
|
||||
await self.process_freeze_moves(current)
|
||||
await self.process_freeze_moves_new(current)
|
||||
await ctx.send(random_conf_gif())
|
||||
|
||||
@commands.command(name='post-weekly')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user