Add draft monitor auto-start and on-clock announcement embed
- Start draft monitor when timer enabled via /draft-admin timer - Auto-start monitor when /draft-admin set-pick is used with active timer - Add _ensure_monitor_running() helper for consistent monitor management - Create on-clock announcement embed with: - Team name, pick info, and deadline - Team sWAR and cap space - Last 5 picks - Top 5 roster players by sWAR - Implement smart polling intervals: - 30s when >60s remaining - 15s when 30-60s remaining - 5s when <30s remaining - Add get_top_free_agents() to player service - Fix DraftAdminGroup to accept bot parameter 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c43f32fb41
commit
ada4feef3e
@ -56,7 +56,7 @@ async def setup_draft(bot: commands.Bot):
|
||||
|
||||
# Load draft admin group (app_commands.Group pattern)
|
||||
try:
|
||||
bot.tree.add_command(DraftAdminGroup())
|
||||
bot.tree.add_command(DraftAdminGroup(bot))
|
||||
logger.info("✅ Loaded DraftAdminGroup")
|
||||
successful += 1
|
||||
except Exception as e:
|
||||
|
||||
@ -22,13 +22,35 @@ from views.embeds import EmbedTemplate
|
||||
class DraftAdminGroup(app_commands.Group):
|
||||
"""Draft administration command group."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
super().__init__(
|
||||
name="draft-admin",
|
||||
description="Admin commands for draft management"
|
||||
)
|
||||
self.bot = bot
|
||||
self.logger = get_contextual_logger(f'{__name__}.DraftAdminGroup')
|
||||
|
||||
def _ensure_monitor_running(self) -> str:
|
||||
"""
|
||||
Ensure the draft monitor task is running.
|
||||
|
||||
Returns:
|
||||
Status message about the monitor state
|
||||
"""
|
||||
from tasks.draft_monitor import setup_draft_monitor
|
||||
|
||||
if not hasattr(self.bot, 'draft_monitor') or self.bot.draft_monitor is None:
|
||||
self.bot.draft_monitor = setup_draft_monitor(self.bot)
|
||||
self.logger.info("Draft monitor task started")
|
||||
return "\n\n🤖 **Draft monitor started** - auto-draft and warnings active"
|
||||
elif not self.bot.draft_monitor.monitor_loop.is_running():
|
||||
# Task exists but was stopped/cancelled - create a new one
|
||||
self.bot.draft_monitor = setup_draft_monitor(self.bot)
|
||||
self.logger.info("Draft monitor task recreated")
|
||||
return "\n\n🤖 **Draft monitor restarted** - auto-draft and warnings active"
|
||||
else:
|
||||
return "\n\n🤖 Draft monitor already running"
|
||||
|
||||
@app_commands.command(name="info", description="View current draft configuration")
|
||||
@league_admin_only()
|
||||
@logged_command("/draft-admin info")
|
||||
@ -94,14 +116,29 @@ class DraftAdminGroup(app_commands.Group):
|
||||
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||
return
|
||||
|
||||
# Start draft monitor task if timer is enabled
|
||||
monitor_status = ""
|
||||
if enabled:
|
||||
monitor_status = self._ensure_monitor_running()
|
||||
|
||||
# Success message
|
||||
status = "enabled" if enabled else "disabled"
|
||||
description = f"Draft timer has been **{status}**."
|
||||
|
||||
if enabled and minutes:
|
||||
description += f"\n\nPick duration: **{minutes} minutes**"
|
||||
elif enabled:
|
||||
description += f"\n\nPick duration: **{updated.pick_minutes} minutes**"
|
||||
if enabled:
|
||||
# Show pick duration
|
||||
pick_mins = minutes if minutes else updated.pick_minutes
|
||||
description += f"\n\n**Pick duration:** {pick_mins} minutes"
|
||||
|
||||
# Show current pick number
|
||||
description += f"\n**Current Pick:** #{updated.currentpick}"
|
||||
|
||||
# Show deadline
|
||||
if updated.pick_deadline:
|
||||
deadline_timestamp = int(updated.pick_deadline.timestamp())
|
||||
description += f"\n**Deadline:** <t:{deadline_timestamp}:T> (<t:{deadline_timestamp}:R>)"
|
||||
|
||||
description += monitor_status
|
||||
|
||||
embed = EmbedTemplate.success("Timer Updated", description)
|
||||
await interaction.followup.send(embed=embed)
|
||||
@ -173,10 +210,13 @@ class DraftAdminGroup(app_commands.Group):
|
||||
if pick.owner:
|
||||
description += f"\n\n{pick.owner.abbrev} {pick.owner.sname} is now on the clock."
|
||||
|
||||
# Add timer status
|
||||
# Add timer status and ensure monitor is running if timer is active
|
||||
if updated.timer and updated.pick_deadline:
|
||||
deadline_timestamp = int(updated.pick_deadline.timestamp())
|
||||
description += f"\n\n⏱️ **Timer Active** - Deadline <t:{deadline_timestamp}:R>"
|
||||
# Ensure monitor is running
|
||||
monitor_status = self._ensure_monitor_running()
|
||||
description += monitor_status
|
||||
else:
|
||||
description += "\n\n⏸️ **Timer Inactive**"
|
||||
|
||||
@ -298,4 +338,4 @@ class DraftAdminGroup(app_commands.Group):
|
||||
|
||||
async def setup(bot: commands.Bot):
|
||||
"""Setup function for loading the draft admin commands."""
|
||||
bot.tree.add_command(DraftAdminGroup())
|
||||
bot.tree.add_command(DraftAdminGroup(bot))
|
||||
|
||||
@ -245,15 +245,35 @@ class PlayerService(BaseService[Player]):
|
||||
async def is_free_agent(self, player: Player) -> bool:
|
||||
"""
|
||||
Check if a player is a free agent.
|
||||
|
||||
|
||||
Args:
|
||||
player: Player instance to check
|
||||
|
||||
|
||||
Returns:
|
||||
True if player is a free agent
|
||||
"""
|
||||
return player.team_id == get_config().free_agent_team_id
|
||||
|
||||
|
||||
async def get_top_free_agents(self, season: int, limit: int = 5) -> List[Player]:
|
||||
"""
|
||||
Get top free agents sorted by sWAR (wara) descending.
|
||||
|
||||
Args:
|
||||
season: Season number (required)
|
||||
limit: Maximum number of players to return (default 5)
|
||||
|
||||
Returns:
|
||||
List of top free agent players sorted by sWAR
|
||||
"""
|
||||
try:
|
||||
free_agents = await self.get_free_agents(season)
|
||||
# Sort by wara descending and take top N
|
||||
sorted_fa = sorted(free_agents, key=lambda p: p.wara if p.wara else 0.0, reverse=True)
|
||||
return sorted_fa[:limit]
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get top free agents: {e}")
|
||||
return []
|
||||
|
||||
async def get_players_by_position(self, position: str, season: int) -> List[Player]:
|
||||
"""
|
||||
Get players by position.
|
||||
|
||||
@ -16,8 +16,11 @@ from services.draft_pick_service import draft_pick_service
|
||||
from services.draft_list_service import draft_list_service
|
||||
from services.player_service import player_service
|
||||
from services.team_service import team_service
|
||||
from services.roster_service import roster_service
|
||||
from utils.logging import get_contextual_logger
|
||||
from utils.helpers import get_team_salary_cap
|
||||
from views.embeds import EmbedTemplate, EmbedColors
|
||||
from views.draft_views import create_on_clock_announcement_embed
|
||||
from config import get_config
|
||||
|
||||
|
||||
@ -50,10 +53,35 @@ class DraftMonitorTask:
|
||||
"""Stop the task when cog is unloaded."""
|
||||
self.monitor_loop.cancel()
|
||||
|
||||
@tasks.loop(seconds=15)
|
||||
def _get_poll_interval(self, time_remaining: float) -> int:
|
||||
"""
|
||||
Get the appropriate polling interval based on time remaining.
|
||||
|
||||
Args:
|
||||
time_remaining: Seconds until deadline
|
||||
|
||||
Returns:
|
||||
Poll interval in seconds:
|
||||
- 30s when > 60s remaining
|
||||
- 15s when 30-60s remaining
|
||||
- 5s when < 30s remaining
|
||||
"""
|
||||
if time_remaining > 60:
|
||||
return 30
|
||||
elif time_remaining > 30:
|
||||
return 15
|
||||
else:
|
||||
return 5
|
||||
|
||||
@tasks.loop(seconds=30)
|
||||
async def monitor_loop(self):
|
||||
"""
|
||||
Main monitoring loop - checks draft state every 15 seconds.
|
||||
Main monitoring loop - checks draft state with dynamic intervals.
|
||||
|
||||
Polling frequency increases as deadline approaches:
|
||||
- Every 30s when > 60s remaining
|
||||
- Every 15s when 30-60s remaining
|
||||
- Every 5s when < 30s remaining
|
||||
|
||||
Self-terminates when draft timer is disabled.
|
||||
"""
|
||||
@ -82,6 +110,12 @@ class DraftMonitorTask:
|
||||
# Calculate time remaining
|
||||
time_remaining = (deadline - now).total_seconds()
|
||||
|
||||
# Adjust polling interval based on time remaining
|
||||
new_interval = self._get_poll_interval(time_remaining)
|
||||
if self.monitor_loop.seconds != new_interval:
|
||||
self.monitor_loop.change_interval(seconds=new_interval)
|
||||
self.logger.debug(f"Adjusted poll interval to {new_interval}s (time remaining: {time_remaining:.0f}s)")
|
||||
|
||||
if time_remaining <= 0:
|
||||
# Timer expired - auto-draft
|
||||
await self._handle_expired_timer(draft_data)
|
||||
@ -180,6 +214,11 @@ class DraftMonitorTask:
|
||||
)
|
||||
# Advance to next pick
|
||||
await draft_service.advance_pick(draft_data.id, draft_data.currentpick)
|
||||
# Post on-clock announcement for next team
|
||||
await self._post_on_clock_announcement(ping_channel, draft_data)
|
||||
# Reset warning flags
|
||||
self.warning_60s_sent = False
|
||||
self.warning_30s_sent = False
|
||||
return
|
||||
|
||||
# Try each player in order
|
||||
@ -207,6 +246,8 @@ class DraftMonitorTask:
|
||||
)
|
||||
# Advance to next pick
|
||||
await draft_service.advance_pick(draft_data.id, draft_data.currentpick)
|
||||
# Post on-clock announcement for next team
|
||||
await self._post_on_clock_announcement(ping_channel, draft_data)
|
||||
# Reset warning flags
|
||||
self.warning_60s_sent = False
|
||||
self.warning_30s_sent = False
|
||||
@ -219,6 +260,11 @@ class DraftMonitorTask:
|
||||
)
|
||||
# Advance to next pick anyway
|
||||
await draft_service.advance_pick(draft_data.id, draft_data.currentpick)
|
||||
# Post on-clock announcement for next team
|
||||
await self._post_on_clock_announcement(ping_channel, draft_data)
|
||||
# Reset warning flags
|
||||
self.warning_60s_sent = False
|
||||
self.warning_30s_sent = False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error("Error auto-drafting player", error=e)
|
||||
@ -294,6 +340,80 @@ class DraftMonitorTask:
|
||||
self.logger.error(f"Error attempting to draft {player.name}", error=e)
|
||||
return False
|
||||
|
||||
async def _post_on_clock_announcement(self, ping_channel, draft_data) -> None:
|
||||
"""
|
||||
Post the on-clock announcement embed for the next team.
|
||||
|
||||
Called after advance_pick() to announce who is now on the clock.
|
||||
|
||||
Args:
|
||||
ping_channel: Discord channel to post in
|
||||
draft_data: Current draft configuration (will be refreshed)
|
||||
"""
|
||||
try:
|
||||
config = get_config()
|
||||
|
||||
# Refresh draft data to get updated currentpick and deadline
|
||||
updated_draft_data = await draft_service.get_draft_data()
|
||||
if not updated_draft_data:
|
||||
self.logger.error("Could not refresh draft data for announcement")
|
||||
return
|
||||
|
||||
# Get the new current pick
|
||||
next_pick = await draft_pick_service.get_pick(
|
||||
config.sba_season,
|
||||
updated_draft_data.currentpick
|
||||
)
|
||||
|
||||
if not next_pick or not next_pick.owner:
|
||||
self.logger.error(f"Could not get pick #{updated_draft_data.currentpick} for announcement")
|
||||
return
|
||||
|
||||
# Get recent picks (last 5 completed)
|
||||
recent_picks = await draft_pick_service.get_recent_picks(
|
||||
config.sba_season,
|
||||
updated_draft_data.currentpick - 1, # Start from previous pick
|
||||
limit=5
|
||||
)
|
||||
|
||||
# Get team roster for sWAR calculation
|
||||
team_roster = await roster_service.get_team_roster(next_pick.owner.id, "current")
|
||||
roster_swar = team_roster.total_wara if team_roster else 0.0
|
||||
cap_limit = get_team_salary_cap(next_pick.owner)
|
||||
|
||||
# Get top 5 most expensive players on team roster
|
||||
top_roster_players = []
|
||||
if team_roster:
|
||||
all_players = team_roster.all_players
|
||||
sorted_players = sorted(all_players, key=lambda p: p.wara if p.wara else 0.0, reverse=True)
|
||||
top_roster_players = sorted_players[:5]
|
||||
|
||||
# Create and send the embed
|
||||
embed = await create_on_clock_announcement_embed(
|
||||
current_pick=next_pick,
|
||||
draft_data=updated_draft_data,
|
||||
recent_picks=recent_picks if recent_picks else [],
|
||||
roster_swar=roster_swar,
|
||||
cap_limit=cap_limit,
|
||||
top_roster_players=top_roster_players
|
||||
)
|
||||
|
||||
# Mention the team's GM if available
|
||||
gm_mention = ""
|
||||
if next_pick.owner.gmid:
|
||||
gm_mention = f"<@{next_pick.owner.gmid}> "
|
||||
|
||||
await ping_channel.send(content=gm_mention, embed=embed)
|
||||
self.logger.info(f"Posted on-clock announcement for pick #{updated_draft_data.currentpick}")
|
||||
|
||||
# Reset poll interval to 30s for new pick
|
||||
if self.monitor_loop.seconds != 30:
|
||||
self.monitor_loop.change_interval(seconds=30)
|
||||
self.logger.debug("Reset poll interval to 30s for new pick")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error("Error posting on-clock announcement", error=e)
|
||||
|
||||
async def _send_warnings_if_needed(self, draft_data, time_remaining: float):
|
||||
"""
|
||||
Send warnings at 60s and 30s remaining.
|
||||
|
||||
@ -300,7 +300,10 @@ async def create_draft_board_embed(
|
||||
player_display = "TBD"
|
||||
|
||||
team_display = pick.owner.abbrev if pick.owner else "???"
|
||||
picks_str += f"**Pick {pick.overall % 16 or 16}:** {team_display} - {player_display}\n"
|
||||
round_pick = pick.overall % 16 or 16
|
||||
# Format: `RR.PP (#OOO)` - padded for alignment (rounds 1-99, picks 1-16, overall 1-999)
|
||||
pick_info = f"{round_num:>2}.{round_pick:<2} (#{pick.overall:>3})"
|
||||
picks_str += f"`{pick_info}` {team_display} - {player_display}\n"
|
||||
|
||||
embed.add_field(
|
||||
name="Picks",
|
||||
@ -347,7 +350,7 @@ async def create_pick_success_embed(
|
||||
team: Team,
|
||||
pick_overall: int,
|
||||
projected_swar: float,
|
||||
cap_limit: float = None
|
||||
cap_limit: float | None = None
|
||||
) -> discord.Embed:
|
||||
"""
|
||||
Create embed for successful pick.
|
||||
@ -365,19 +368,24 @@ async def create_pick_success_embed(
|
||||
from utils.helpers import get_team_salary_cap
|
||||
|
||||
embed = EmbedTemplate.success(
|
||||
title="Pick Confirmed",
|
||||
description=f"{team.abbrev} selects **{player.name}**"
|
||||
title=f"{team.sname} select **{player.name}**",
|
||||
description=format_pick_display(pick_overall)
|
||||
)
|
||||
|
||||
if team.thumbnail is not None:
|
||||
embed.set_thumbnail(url=team.thumbnail)
|
||||
|
||||
embed.set_image(url=player.image)
|
||||
|
||||
embed.add_field(
|
||||
name="Pick",
|
||||
value=format_pick_display(pick_overall),
|
||||
name="Player ID",
|
||||
value=f"{player.id}",
|
||||
inline=True
|
||||
)
|
||||
|
||||
if hasattr(player, 'wara') and player.wara is not None:
|
||||
embed.add_field(
|
||||
name="Player sWAR",
|
||||
name="sWAR",
|
||||
value=f"{player.wara:.2f}",
|
||||
inline=True
|
||||
)
|
||||
@ -389,7 +397,7 @@ async def create_pick_success_embed(
|
||||
embed.add_field(
|
||||
name="Projected Team sWAR",
|
||||
value=f"{projected_swar:.2f} / {cap_limit:.2f}",
|
||||
inline=True
|
||||
inline=False
|
||||
)
|
||||
|
||||
return embed
|
||||
@ -472,3 +480,103 @@ async def create_admin_draft_info_embed(
|
||||
embed.set_footer(text="Use /draft-admin to modify draft settings")
|
||||
|
||||
return embed
|
||||
|
||||
|
||||
async def create_on_clock_announcement_embed(
|
||||
current_pick: DraftPick,
|
||||
draft_data: DraftData,
|
||||
recent_picks: List[DraftPick],
|
||||
roster_swar: float,
|
||||
cap_limit: float,
|
||||
top_roster_players: List[Player]
|
||||
) -> discord.Embed:
|
||||
"""
|
||||
Create announcement embed for when a team is on the clock.
|
||||
|
||||
Used to post in the ping channel when:
|
||||
- Timer is enabled and pick advances
|
||||
- Auto-draft completes
|
||||
- Pick is skipped
|
||||
|
||||
Args:
|
||||
current_pick: The current DraftPick (team now on the clock)
|
||||
draft_data: Current draft configuration (for timer/deadline info)
|
||||
recent_picks: Last 5 completed picks
|
||||
roster_swar: Team's current total sWAR
|
||||
cap_limit: Team's salary cap limit
|
||||
top_roster_players: Top 5 most expensive players on the team's roster
|
||||
|
||||
Returns:
|
||||
Discord embed announcing team is on the clock
|
||||
"""
|
||||
if not current_pick.owner:
|
||||
raise ValueError("Pick must have owner")
|
||||
|
||||
team = current_pick.owner
|
||||
|
||||
# Create embed with team color if available
|
||||
team_color = int(team.color, 16) if team.color else EmbedColors.PRIMARY
|
||||
embed = EmbedTemplate.create_base_embed(
|
||||
title=f"⏰ {team.lname} On The Clock",
|
||||
description=format_pick_display(current_pick.overall),
|
||||
color=team_color
|
||||
)
|
||||
|
||||
# Set team thumbnail
|
||||
if team.thumbnail:
|
||||
embed.set_thumbnail(url=team.thumbnail)
|
||||
|
||||
# Deadline field (if timer active)
|
||||
if draft_data.timer and draft_data.pick_deadline:
|
||||
deadline_timestamp = int(draft_data.pick_deadline.timestamp())
|
||||
embed.add_field(
|
||||
name="⏱️ Deadline",
|
||||
value=f"<t:{deadline_timestamp}:T> (<t:{deadline_timestamp}:R>)",
|
||||
inline=True
|
||||
)
|
||||
|
||||
# Team sWAR
|
||||
embed.add_field(
|
||||
name="💰 Team sWAR",
|
||||
value=f"{roster_swar:.2f} / {cap_limit:.2f}",
|
||||
inline=True
|
||||
)
|
||||
|
||||
# Cap space remaining
|
||||
cap_remaining = cap_limit - roster_swar
|
||||
embed.add_field(
|
||||
name="📊 Cap Space",
|
||||
value=f"{cap_remaining:.2f}",
|
||||
inline=True
|
||||
)
|
||||
|
||||
# Last 5 picks
|
||||
if recent_picks:
|
||||
recent_str = ""
|
||||
for pick in recent_picks[:5]:
|
||||
if pick.player and pick.owner:
|
||||
recent_str += f"**#{pick.overall}** {pick.owner.abbrev} - {pick.player.name}\n"
|
||||
if recent_str:
|
||||
embed.add_field(
|
||||
name="📋 Last 5 Picks",
|
||||
value=recent_str,
|
||||
inline=False
|
||||
)
|
||||
|
||||
# Top 5 most expensive players on team roster
|
||||
if top_roster_players:
|
||||
expensive_str = ""
|
||||
for player in top_roster_players[:5]:
|
||||
pos = player.pos_1 if hasattr(player, 'pos_1') and player.pos_1 else "?"
|
||||
expensive_str += f"**{player.name}** ({pos}) - {player.wara:.2f}\n"
|
||||
embed.add_field(
|
||||
name="🌟 Top Roster sWAR",
|
||||
value=expensive_str,
|
||||
inline=False
|
||||
)
|
||||
|
||||
# Footer with pick info
|
||||
if current_pick.is_traded:
|
||||
embed.set_footer(text="📝 This pick was acquired via trade")
|
||||
|
||||
return embed
|
||||
|
||||
Loading…
Reference in New Issue
Block a user