This commit addresses critical bugs in the injury command system and establishes best practices for Discord command groups. ## Critical Fixes ### 1. GroupCog → app_commands.Group Migration - **Problem**: `commands.GroupCog` has a duplicate interaction processing bug causing "404 Unknown interaction" errors when deferring responses - **Root Cause**: GroupCog triggers command handler twice, consuming the interaction token before the second execution can respond - **Solution**: Migrated InjuryCog to InjuryGroup using `app_commands.Group` pattern (same as ChartManageGroup and ChartCategoryGroup) - **Result**: Reliable command execution, no more 404 errors ### 2. GiphyService GIF URL Fix - **Problem**: Giphy service returned web page URLs (https://giphy.com/gifs/...) instead of direct image URLs, preventing Discord embed display - **Root Cause**: Code accessed `data.url` instead of `data.images.original.url` - **Solution**: Updated both `get_disappointment_gif()` and `get_gif()` methods to use correct API response path for embeddable GIF URLs - **Result**: GIFs now display correctly in Discord embeds ## Documentation ### Command Groups Best Practices (commands/README.md) Added comprehensive section documenting: - **Critical Warning**: Never use `commands.GroupCog` - use `app_commands.Group` - **Technical Explanation**: Why GroupCog fails (duplicate execution bug) - **Migration Guide**: Step-by-step conversion from GroupCog to Group - **Comparison Table**: Key differences between the two approaches - **Working Examples**: References to ChartManageGroup, InjuryGroup patterns ## Architecture Changes ### Injury Commands (`commands/injuries/`) - Converted from `commands.GroupCog` to `app_commands.Group` - Registration via `bot.tree.add_command()` instead of `bot.add_cog()` - Removed workarounds for GroupCog duplicate interaction issues - Clean defer/response pattern with `@logged_command` decorator ### GiphyService (`services/giphy_service.py`) - Centralized from `commands/soak/giphy_service.py` - Now returns direct GIF image URLs for Discord embeds - Maintains Trump GIF filtering (legacy behavior) - Added gif_url to log output for debugging ### Configuration (`config.py`) - Added `giphy_api_key` and `giphy_translate_url` settings - Environment variable support via `GIPHY_API_KEY` - Default values provided for out-of-box functionality ## Files Changed - commands/injuries/: New InjuryGroup with app_commands.Group pattern - services/giphy_service.py: Centralized service with GIF URL fix - commands/soak/giphy_service.py: Backwards compatibility wrapper - commands/README.md: Command groups best practices documentation - config.py: Giphy configuration settings - services/__init__.py: GiphyService exports 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
13 KiB
Injury Commands
Command Group: /injury
Permission Required: SBA Players role (for set-new and clear)
Subcommands: roll, set-new, clear
Overview
The injury command family provides comprehensive player injury management for the SBA league. Team managers can roll for injuries using official Strat-o-Matic injury tables, record confirmed injuries, and clear injuries when players return.
Commands
/injury roll
Roll for injury based on a player's injury rating using 3d6 dice and official injury tables.
Usage:
/injury roll <player_name>
Parameters:
player_name(required, autocomplete): Name of the player - uses smart autocomplete prioritizing your team's players
Injury Rating Format:
The player's injury_rating field contains both the games played and rating in format #p##:
- Format:
1p70,4p50,2p65, etc. - First character: Games played in current series (1-6)
- Remaining characters: Injury rating (p70, p65, p60, p50, p40, p30, p20)
Examples:
1p70= 1 game played, p70 rating4p50= 4 games played, p50 rating2p65= 2 games played, p65 rating
Dice Roll:
- Rolls 3d6 (3-18 range)
- Automatically extracts games played and rating from player's injury_rating field
- Looks up result in official Strat-o-Matic injury tables
- Returns injury duration based on rating and games played
Possible Results:
- OK: No injury
- REM: Remainder of game (batters) or Fatigued (pitchers)
- Number: Games player will miss (1-24 games)
Example:
/injury roll Mike Trout
Response Fields:
- Roll: Total rolled and individual dice (e.g., "15 (3d6: 5 + 5 + 5)")
- Player: Player name and position
- Injury Rating: Full rating with parsed details (e.g., "4p50 (p50, 4 games)")
- Result: Injury outcome (OK, REM, or number of games)
- Team: Player's current team
Response Colors:
- Green: OK (no injury)
- Gold: REM (remainder of game/fatigued)
- Orange: Number of games (injury occurred)
Error Handling:
If a player's injury_rating is not in the correct format, an error message will be displayed:
Invalid Injury Rating Format
{Player} has an invalid injury rating: `{rating}`
Expected format: #p## (e.g., 1p70, 4p50)
/injury set-new
Record a new injury for a player on your team.
Usage:
/injury set-new <player_name> <this_week> <this_game> <injury_games>
Parameters:
player_name(required): Name of the player to injurethis_week(required): Current week numberthis_game(required): Current game number (1-4)injury_games(required): Total number of games player will be out
Validation:
- Player must exist in current season
- Player cannot already have an active injury
- Game number must be between 1 and 4
- Injury duration must be at least 1 game
Automatic Calculations: The command automatically calculates:
- Injury start date (adjusts for game 4 edge case)
- Return date based on injury duration
- Week rollover when games exceed 4 per week
Example:
/injury set-new Mike Trout 5 2 4
This records an injury occurring in week 5, game 2, with player out for 4 games (returns week 6, game 2).
Response:
- Confirmation embed with injury details
- Player's name, position, and team
- Total games missed
- Calculated return date
/injury clear
Clear a player's active injury and mark them as eligible to play.
Usage:
/injury clear <player_name>
Parameters:
player_name(required): Name of the player whose injury to clear
Validation:
- Player must exist in current season
- Player must have an active injury
Example:
/injury clear Mike Trout
Response:
- Confirmation that injury was cleared
- Shows previous return date
- Shows total games that were missed
- Player's team information
Date Format
All injury dates use the format w##g#:
w##= Week number (zero-padded to 2 digits)g#= Game number (1-4)
Examples:
w05g2= Week 5, Game 2w12g4= Week 12, Game 4w01g1= Week 1, Game 1
Injury Calculation Logic
Basic Calculation
For an injury of N games starting at week W, game G:
-
Calculate weeks and remaining games:
out_weeks = floor(N / 4) out_games = N % 4 -
Calculate return date:
return_week = W + out_weeks return_game = G + 1 + out_games -
Handle week rollover:
if return_game > 4: return_week += 1 return_game -= 4
Special Cases
Game 4 Edge Case
If injury occurs during game 4, the start date is adjusted:
start_week = W + 1
start_game = 1
Examples
Example 1: Simple injury (same week)
- Current: Week 5, Game 1
- Injury: 2 games
- Return: Week 5, Game 4
Example 2: Week rollover
- Current: Week 5, Game 3
- Injury: 3 games
- Return: Week 6, Game 3
Example 3: Multi-week injury
- Current: Week 5, Game 2
- Injury: 8 games
- Return: Week 7, Game 3
Example 4: Game 4 start
- Current: Week 5, Game 4
- Injury: 2 games
- Start: Week 6, Game 1
- Return: Week 6, Game 3
Database Schema
Injury Model
class Injury(SBABaseModel):
id: int # Injury ID
season: int # Season number
player_id: int # Player ID
total_games: int # Total games player will be out
start_week: int # Week injury started
start_game: int # Game number injury started (1-4)
end_week: int # Week player returns
end_game: int # Game number player returns (1-4)
is_active: bool # Whether injury is currently active
API Integration
The commands interact with the following API endpoints:
GET /api/v3/injuries- Query injuries with filtersPOST /api/v3/injuries- Create new injury recordPATCH /api/v3/injuries/{id}- Update injury (clear active status)PATCH /api/v3/players/{id}- Update player's il_return field
Service Layer
InjuryService
Location: services/injury_service.py
Key Methods:
get_active_injury(player_id, season)- Get active injury for playerget_injuries_by_player(player_id, season, active_only)- Get all injuries for playerget_injuries_by_team(team_id, season, active_only)- Get team injuriescreate_injury(...)- Create new injury recordclear_injury(injury_id)- Deactivate injury
Permissions
Required Roles
For /injury check:
- No role required (available to all users)
For /injury set-new and /injury clear:
- SBA Players role required
- Configured via
SBA_PLAYERS_ROLE_NAMEenvironment variable
Permission Checks
The commands use has_player_role() method to verify user has appropriate role:
def has_player_role(self, interaction: discord.Interaction) -> bool:
"""Check if user has the SBA Players role."""
player_role = discord.utils.get(
interaction.guild.roles,
name=get_config().sba_players_role_name
)
return player_role in interaction.user.roles if player_role else False
Error Handling
Common Errors
Player Not Found:
❌ Player Not Found
I did not find anybody named **{player_name}**.
Already Injured:
❌ Already Injured
Hm. It looks like {player_name} is already hurt.
Not Injured:
❌ No Active Injury
{player_name} isn't injured.
Invalid Input:
❌ Invalid Input
Game number must be between 1 and 4.
Permission Denied:
❌ Permission Denied
This command requires the **SBA Players** role.
Logging
All injury commands use the @logged_command decorator for automatic logging:
@app_commands.command(name="check")
@logged_command("/injury check")
async def injury_check(self, interaction, player_name: str):
# Command implementation
Log Context:
- Command name
- User ID and username
- Player name
- Season
- Injury details (duration, dates)
- Success/failure status
Example Log:
{
"level": "INFO",
"command": "/injury set-new",
"user_id": "123456789",
"player_name": "Mike Trout",
"season": 12,
"injury_games": 4,
"return_date": "w06g2",
"message": "Injury set for Mike Trout"
}
Testing
Test Coverage
Location: tests/test_services_injury.py
Test Categories:
- Model Tests (5 tests) - Injury model creation and properties
- Service Tests (8 tests) - InjuryService CRUD operations with API mocking
- Roll Logic Tests (8 tests) - Injury rating parsing, table lookup, and dice roll logic
- Calculation Tests (5 tests) - Date calculation logic for injury duration
Total: 26 comprehensive tests
Running Tests:
# Run all injury tests
python -m pytest tests/test_services_injury.py -v
# Run specific test class
python -m pytest tests/test_services_injury.py::TestInjuryService -v
python -m pytest tests/test_services_injury.py::TestInjuryRollLogic -v
# Run with coverage
python -m pytest tests/test_services_injury.py --cov=services.injury_service --cov=commands.injuries
Injury Roll Tables
Table Structure
The injury tables are based on official Strat-o-Matic rules with the following structure:
Ratings: p70, p65, p60, p50, p40, p30, p20 (higher is better) Games Played: 1-6 games in current series Roll: 3d6 (results from 3-18)
Rating Availability by Games Played
Not all ratings are available for all games played combinations:
- 1 game: All ratings (p70-p20)
- 2 games: All ratings (p70-p20)
- 3 games: p65-p20 (p70 exempt)
- 4 games: p60-p20 (p70, p65 exempt)
- 5 games: p60-p20 (p70, p65 exempt)
- 6 games: p40-p20 (p70, p65, p60, p50 exempt)
When a rating/games combination has no table, the result is automatically "OK" (no injury).
Example Table (p65, 1 game):
| Roll | Result |
|---|---|
| 3 | 2 |
| 4 | 2 |
| 5 | OK |
| 6 | REM |
| 7 | 1 |
| ... | ... |
| 18 | 12 |
UI/UX Design
Embed Colors
- Roll (OK): Green - No injury
- Roll (REM): Gold - Remainder of game/Fatigued
- Roll (Injury): Orange - Number of games
- Set New: Success (green) -
EmbedTemplate.success() - Clear: Success (green) -
EmbedTemplate.success() - Errors: Error (red) -
EmbedTemplate.error()
Response Format
All successful responses use Discord embeds with:
- Clear title indicating action/status
- Well-organized field layout
- Team information when applicable
- Consistent formatting for dates
Integration with Player Model
The Player model includes injury-related fields:
class Player(SBABaseModel):
# ... other fields ...
pitcher_injury: Optional[int] # Pitcher injury rating
injury_rating: Optional[str] # General injury rating
il_return: Optional[str] # Injured list return date (w##g#)
When an injury is set or cleared, the player's il_return field is automatically updated via PlayerService.
Future Enhancements
Possible improvements for future versions:
- Injury History - View player's injury history for a season
- Team Injury Report - List all injuries for a team
- Injury Notifications - Automatic notifications when players return from injury
- Injury Statistics - Track injury trends and statistics
- Injury Chart Image - Display the official injury chart as an embed image
Migration from Legacy
Legacy Commands
The legacy injury commands were located in:
discord-app/cogs/players.py-set_injury_slash()andclear_injury_slash()discord-app/cogs/players.py-injury_roll_slash()with manual rating/games input
Key Improvements
- Cleaner Command Structure: Using GroupCog for organized subcommands (
/injury roll,/injury set-new,/injury clear) - Simplified Interface: Single parameter for injury roll - games played automatically extracted from player data
- Smart Injury Ratings: Automatically reads and parses player's injury rating from database
- Player Autocomplete: Modern autocomplete with team prioritization for better UX
- Better Error Handling: User-friendly error messages via EmbedTemplate with format validation
- Improved Logging: Automatic logging via @logged_command decorator
- Service Layer: Separated business logic from command handlers
- Type Safety: Full type hints and Pydantic models
- Testability: Comprehensive unit tests (26 tests) with mocked API calls
- Modern UI: Consistent embed-based responses with color coding
- Official Tables: Complete Strat-o-Matic injury tables built into the command
Migration Details
Old: /injuryroll <rating> <games> - Manual rating and games selection
New: /injury roll <player> - Single parameter, automatic rating and games extraction from player's injury_rating field
Old: /setinjury <player> <week> <game> <duration>
New: /injury set-new <player> <week> <game> <duration> - Same functionality, better naming
Old: /clearinjury <player>
New: /injury clear <player> - Same functionality, better naming
Database Field Update
The injury_rating field format has changed to include games played:
- Old Format:
p65,p70, etc. (rating only) - New Format:
1p70,4p50,2p65, etc. (games + rating)
Players must have their injury_rating field updated to the new format for the /injury roll command to work.
Last Updated: January 2025 Version: 2.0 Status: Active