paper-dynasty-discord/tests/players_refactor/test_gauntlet.py
Cal Corum ee80cd72ae fix: apply Black formatting and resolve ruff lint violations
Run Black formatter across 83 files and fix 1514 ruff violations:
- E722: bare except → typed exceptions (17 fixes)
- E711/E712/E721: comparison style fixes with noqa for SQLAlchemy (44 fixes)
- F841: unused variable assignments (70 fixes)
- F541/F401: f-string and import cleanup (1383 auto-fixes)

Remaining 925 errors are all F403/F405 (star imports) — structural,
requires converting to explicit imports in a separate effort.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 11:37:46 -05:00

877 lines
30 KiB
Python

"""
Comprehensive tests for the gauntlet.py module.
Tests gauntlet game mode functionality including:
- Gauntlet status command
- Start new gauntlet run command
- Reset gauntlet team command
- Draft management
- Progress tracking
- Gauntlet game logic
"""
import pytest
from unittest.mock import Mock, patch
# Import the module to test (this will need to be updated when modules are moved)
try:
from cogs.players_new.gauntlet import Gauntlet
except ImportError:
# Create a mock class for testing structure
class Gauntlet:
def __init__(self, bot):
self.bot = bot
@pytest.mark.asyncio
class TestGauntlet:
"""Test suite for Gauntlet cog functionality."""
@pytest.fixture
def gauntlet_cog(self, mock_bot):
"""Create Gauntlet cog instance for testing."""
return Gauntlet(mock_bot)
@pytest.fixture
def mock_active_gauntlet_data(self):
"""Mock active gauntlet data for testing."""
return {
"gauntlet_id": 1,
"team_id": 31,
"status": "active",
"current_round": 3,
"wins": 2,
"losses": 0,
"draft_complete": True,
"created_at": "2024-08-06T10:00:00Z",
"roster": [
{
"card_id": 1,
"player_name": "Mike Trout",
"position": "CF",
"draft_position": 1,
},
{
"card_id": 2,
"player_name": "Mookie Betts",
"position": "RF",
"draft_position": 2,
},
{
"card_id": 3,
"player_name": "Ronald Acuña Jr.",
"position": "LF",
"draft_position": 3,
},
{
"card_id": 4,
"player_name": "Juan Soto",
"position": "DH",
"draft_position": 4,
},
{
"card_id": 5,
"player_name": "Freddie Freeman",
"position": "1B",
"draft_position": 5,
},
{
"card_id": 6,
"player_name": "Corey Seager",
"position": "SS",
"draft_position": 6,
},
{
"card_id": 7,
"player_name": "Manny Machado",
"position": "3B",
"draft_position": 7,
},
{
"card_id": 8,
"player_name": "José Altuve",
"position": "2B",
"draft_position": 8,
},
{
"card_id": 9,
"player_name": "Salvador Perez",
"position": "C",
"draft_position": 9,
},
{
"card_id": 10,
"player_name": "Gerrit Cole",
"position": "SP",
"draft_position": 10,
},
],
"opponents_defeated": [
{"opponent": "Arizona Diamondbacks", "round": 1, "score": "8-5"},
{"opponent": "Atlanta Braves", "round": 2, "score": "6-4"},
],
"next_opponent": "Baltimore Orioles",
"draft_pool_remaining": 0,
"reward_tier": "Bronze",
}
@pytest.fixture
def mock_inactive_gauntlet_data(self):
"""Mock inactive gauntlet data for testing."""
return {
"gauntlet_id": None,
"team_id": 31,
"status": "inactive",
"last_completed": "2024-08-05T15:30:00Z",
"best_run": {
"wins": 5,
"losses": 3,
"reward_tier": "Silver",
"completed_at": "2024-08-04T12:00:00Z",
},
"total_runs": 3,
"total_wins": 12,
"total_losses": 9,
}
@pytest.fixture
def mock_draft_pool(self):
"""Mock draft pool data for starting a new gauntlet."""
return {
"available_cards": [
{
"card_id": 101,
"player_name": "Shohei Ohtani",
"position": "SP/DH",
"cost": 500,
"rarity": "Legendary",
},
{
"card_id": 102,
"player_name": "Aaron Judge",
"position": "RF",
"cost": 450,
"rarity": "All-Star",
},
{
"card_id": 103,
"player_name": "Vladimir Guerrero Jr.",
"position": "1B",
"cost": 400,
"rarity": "All-Star",
},
{
"card_id": 104,
"player_name": "Fernando Tatis Jr.",
"position": "SS",
"cost": 380,
"rarity": "All-Star",
},
{
"card_id": 105,
"player_name": "Jacob deGrom",
"position": "SP",
"cost": 350,
"rarity": "All-Star",
},
],
"draft_rules": {
"roster_size": 10,
"budget_cap": 2000,
"position_requirements": {
"C": 1,
"1B": 1,
"2B": 1,
"3B": 1,
"SS": 1,
"LF": 1,
"CF": 1,
"RF": 1,
"DH": 1,
"SP": 1,
},
},
}
async def test_init(self, gauntlet_cog, mock_bot):
"""Test cog initialization."""
assert gauntlet_cog.bot == mock_bot
@patch("helpers.get_team_by_owner")
@patch("gauntlets.get_gauntlet_status")
async def test_gauntlet_status_active(
self,
mock_get_status,
mock_get_by_owner,
gauntlet_cog,
mock_interaction,
sample_team_data,
mock_active_gauntlet_data,
mock_embed,
):
"""Test gauntlet status command with active gauntlet."""
mock_get_by_owner.return_value = sample_team_data
mock_get_status.return_value = mock_active_gauntlet_data
async def mock_gauntlet_status_command(interaction):
await interaction.response.defer()
team = await mock_get_by_owner(interaction.user.id)
if not team:
await interaction.followup.send(
"You need a team to participate in the gauntlet!"
)
return
gauntlet_data = await mock_get_status(team["id"])
if gauntlet_data["status"] == "active":
embed = mock_embed
embed.title = "🏟️ Gauntlet Status - Active"
embed.color = 0x00FF00 # Green for active
# Current progress
embed.add_field(
name="Progress",
value=f"Round {gauntlet_data['current_round']}{gauntlet_data['wins']}-{gauntlet_data['losses']}",
inline=True,
)
# Next opponent
if gauntlet_data.get("next_opponent"):
embed.add_field(
name="Next Opponent",
value=gauntlet_data["next_opponent"],
inline=True,
)
# Roster summary
roster_summary = "\n".join(
[
f"{card['position']}: {card['player_name']}"
for card in gauntlet_data["roster"][:5]
]
)
if len(gauntlet_data["roster"]) > 5:
roster_summary += (
f"\n... and {len(gauntlet_data['roster']) - 5} more"
)
embed.add_field(name="Roster", value=roster_summary, inline=False)
await interaction.followup.send(embed=embed)
else:
await interaction.followup.send("No active gauntlet run.")
await mock_gauntlet_status_command(mock_interaction)
mock_interaction.response.defer.assert_called_once()
mock_get_by_owner.assert_called_once_with(mock_interaction.user.id)
mock_get_status.assert_called_once_with(sample_team_data["id"])
mock_interaction.followup.send.assert_called_once()
@patch("helpers.get_team_by_owner")
@patch("gauntlets.get_gauntlet_status")
async def test_gauntlet_status_inactive(
self,
mock_get_status,
mock_get_by_owner,
gauntlet_cog,
mock_interaction,
sample_team_data,
mock_inactive_gauntlet_data,
mock_embed,
):
"""Test gauntlet status command with inactive gauntlet."""
mock_get_by_owner.return_value = sample_team_data
mock_get_status.return_value = mock_inactive_gauntlet_data
async def mock_gauntlet_status_command(interaction):
await interaction.response.defer()
team = await mock_get_by_owner(interaction.user.id)
gauntlet_data = await mock_get_status(team["id"])
if gauntlet_data["status"] == "inactive":
embed = mock_embed
embed.title = "🏟️ Gauntlet Status - Inactive"
embed.color = 0xFF0000 # Red for inactive
# Best run stats
if gauntlet_data.get("best_run"):
best = gauntlet_data["best_run"]
embed.add_field(
name="Best Run",
value=f"{best['wins']}-{best['losses']} ({best['reward_tier']})",
inline=True,
)
# Overall stats
embed.add_field(
name="Overall Stats",
value=f"Runs: {gauntlet_data['total_runs']}\nRecord: {gauntlet_data['total_wins']}-{gauntlet_data['total_losses']}",
inline=True,
)
embed.add_field(
name="Action",
value="Use `/gauntlet start` to begin a new run!",
inline=False,
)
await interaction.followup.send(embed=embed)
await mock_gauntlet_status_command(mock_interaction)
mock_interaction.followup.send.assert_called_once()
@patch("helpers.get_team_by_owner")
async def test_gauntlet_status_no_team(
self, mock_get_by_owner, gauntlet_cog, mock_interaction
):
"""Test gauntlet status command when user has no team."""
mock_get_by_owner.return_value = None
async def mock_gauntlet_status_command(interaction):
await interaction.response.defer()
team = await mock_get_by_owner(interaction.user.id)
if not team:
await interaction.followup.send(
"You need a team to participate in the gauntlet!"
)
return
await mock_gauntlet_status_command(mock_interaction)
mock_interaction.followup.send.assert_called_once_with(
"You need a team to participate in the gauntlet!"
)
@patch("helpers.get_team_by_owner")
@patch("gauntlets.get_gauntlet_status")
@patch("gauntlets.get_draft_pool")
@patch("gauntlets.start_new_gauntlet")
async def test_gauntlet_start_success(
self,
mock_start_gauntlet,
mock_get_draft_pool,
mock_get_status,
mock_get_by_owner,
gauntlet_cog,
mock_interaction,
sample_team_data,
mock_inactive_gauntlet_data,
mock_draft_pool,
mock_embed,
):
"""Test successful gauntlet start command."""
mock_get_by_owner.return_value = sample_team_data
mock_get_status.return_value = mock_inactive_gauntlet_data
mock_get_draft_pool.return_value = mock_draft_pool
mock_start_gauntlet.return_value = {"gauntlet_id": 2, "status": "draft_phase"}
async def mock_gauntlet_start_command(interaction):
await interaction.response.defer()
team = await mock_get_by_owner(interaction.user.id)
if not team:
await interaction.followup.send(
"You need a team to participate in the gauntlet!"
)
return
# Check if already active
gauntlet_status = await mock_get_status(team["id"])
if gauntlet_status["status"] == "active":
await interaction.followup.send(
"You already have an active gauntlet run! Use `/gauntlet status` to check progress."
)
return
# Get draft pool
draft_pool = await mock_get_draft_pool()
if not draft_pool or not draft_pool.get("available_cards"):
await interaction.followup.send(
"No cards available for drafting. Please try again later."
)
return
# Start new gauntlet
await mock_start_gauntlet(team["id"], draft_pool)
embed = mock_embed
embed.title = "🏟️ New Gauntlet Started!"
embed.color = 0x00FF00
embed.description = "Your gauntlet run has begun! Time to draft your team."
embed.add_field(
name="Draft Rules",
value=f"• Roster Size: {draft_pool['draft_rules']['roster_size']}\n• Budget Cap: ${draft_pool['draft_rules']['budget_cap']}",
inline=False,
)
embed.add_field(
name="Available Cards",
value=f"{len(draft_pool['available_cards'])} cards in draft pool",
inline=True,
)
await interaction.followup.send(embed=embed)
await mock_gauntlet_start_command(mock_interaction)
mock_get_by_owner.assert_called_once()
mock_get_status.assert_called_once()
mock_get_draft_pool.assert_called_once()
mock_start_gauntlet.assert_called_once()
mock_interaction.followup.send.assert_called_once()
@patch("helpers.get_team_by_owner")
@patch("gauntlets.get_gauntlet_status")
async def test_gauntlet_start_already_active(
self,
mock_get_status,
mock_get_by_owner,
gauntlet_cog,
mock_interaction,
sample_team_data,
mock_active_gauntlet_data,
):
"""Test gauntlet start command when already active."""
mock_get_by_owner.return_value = sample_team_data
mock_get_status.return_value = mock_active_gauntlet_data
async def mock_gauntlet_start_command(interaction):
await interaction.response.defer()
team = await mock_get_by_owner(interaction.user.id)
gauntlet_status = await mock_get_status(team["id"])
if gauntlet_status["status"] == "active":
await interaction.followup.send(
"You already have an active gauntlet run! Use `/gauntlet status` to check progress."
)
return
await mock_gauntlet_start_command(mock_interaction)
mock_interaction.followup.send.assert_called_once_with(
"You already have an active gauntlet run! Use `/gauntlet status` to check progress."
)
@patch("helpers.get_team_by_owner")
@patch("gauntlets.get_gauntlet_status")
@patch("gauntlets.get_draft_pool")
async def test_gauntlet_start_no_draft_pool(
self,
mock_get_draft_pool,
mock_get_status,
mock_get_by_owner,
gauntlet_cog,
mock_interaction,
sample_team_data,
mock_inactive_gauntlet_data,
):
"""Test gauntlet start command when no draft pool available."""
mock_get_by_owner.return_value = sample_team_data
mock_get_status.return_value = mock_inactive_gauntlet_data
mock_get_draft_pool.return_value = {"available_cards": []}
async def mock_gauntlet_start_command(interaction):
await interaction.response.defer()
team = await mock_get_by_owner(interaction.user.id)
await mock_get_status(team["id"])
draft_pool = await mock_get_draft_pool()
if not draft_pool or not draft_pool.get("available_cards"):
await interaction.followup.send(
"No cards available for drafting. Please try again later."
)
return
await mock_gauntlet_start_command(mock_interaction)
mock_interaction.followup.send.assert_called_once_with(
"No cards available for drafting. Please try again later."
)
@patch("helpers.get_team_by_owner")
@patch("gauntlets.get_gauntlet_status")
@patch("gauntlets.reset_gauntlet")
async def test_gauntlet_reset_success(
self,
mock_reset_gauntlet,
mock_get_status,
mock_get_by_owner,
gauntlet_cog,
mock_interaction,
sample_team_data,
mock_active_gauntlet_data,
):
"""Test successful gauntlet reset command."""
mock_get_by_owner.return_value = sample_team_data
mock_get_status.return_value = mock_active_gauntlet_data
mock_reset_gauntlet.return_value = {
"success": True,
"message": "Gauntlet reset successfully",
}
async def mock_gauntlet_reset_command(interaction):
await interaction.response.defer()
team = await mock_get_by_owner(interaction.user.id)
if not team:
await interaction.followup.send(
"You need a team to participate in the gauntlet!"
)
return
gauntlet_status = await mock_get_status(team["id"])
if gauntlet_status["status"] != "active":
await interaction.followup.send(
"You don't have an active gauntlet to reset."
)
return
# Confirm reset (in real implementation, this would use buttons/confirmation)
reset_result = await mock_reset_gauntlet(team["id"])
if reset_result["success"]:
await interaction.followup.send(
"✅ Gauntlet reset successfully! You can start a new run when ready."
)
else:
await interaction.followup.send(
"❌ Failed to reset gauntlet. Please try again."
)
await mock_gauntlet_reset_command(mock_interaction)
mock_get_by_owner.assert_called_once()
mock_get_status.assert_called_once()
mock_reset_gauntlet.assert_called_once()
mock_interaction.followup.send.assert_called_once_with(
"✅ Gauntlet reset successfully! You can start a new run when ready."
)
@patch("helpers.get_team_by_owner")
@patch("gauntlets.get_gauntlet_status")
async def test_gauntlet_reset_no_active(
self,
mock_get_status,
mock_get_by_owner,
gauntlet_cog,
mock_interaction,
sample_team_data,
mock_inactive_gauntlet_data,
):
"""Test gauntlet reset command when no active gauntlet."""
mock_get_by_owner.return_value = sample_team_data
mock_get_status.return_value = mock_inactive_gauntlet_data
async def mock_gauntlet_reset_command(interaction):
await interaction.response.defer()
team = await mock_get_by_owner(interaction.user.id)
gauntlet_status = await mock_get_status(team["id"])
if gauntlet_status["status"] != "active":
await interaction.followup.send(
"You don't have an active gauntlet to reset."
)
return
await mock_gauntlet_reset_command(mock_interaction)
mock_interaction.followup.send.assert_called_once_with(
"You don't have an active gauntlet to reset."
)
def test_draft_budget_validation(self, gauntlet_cog, mock_draft_pool):
"""Test draft budget validation logic."""
def validate_draft_budget(selected_cards, budget_cap):
"""Validate that selected cards fit within budget."""
total_cost = sum(card["cost"] for card in selected_cards)
return total_cost <= budget_cap, total_cost
# Test valid budget
selected_cards = [
{"card_id": 101, "cost": 500},
{"card_id": 102, "cost": 450},
{"card_id": 103, "cost": 400},
]
is_valid, total = validate_draft_budget(selected_cards, 2000)
assert is_valid is True
assert total == 1350
# Test over budget
expensive_cards = [
{"card_id": 101, "cost": 500},
{"card_id": 102, "cost": 600},
{"card_id": 103, "cost": 700},
{"card_id": 104, "cost": 800},
]
is_valid, total = validate_draft_budget(expensive_cards, 2000)
assert is_valid is False
assert total == 2600
def test_position_requirements_validation(self, gauntlet_cog):
"""Test position requirements validation for draft."""
def validate_position_requirements(selected_cards, requirements):
"""Validate that all required positions are filled."""
position_counts = {}
for card in selected_cards:
pos = card["position"]
# Handle multi-position players (e.g., "SP/DH")
if "/" in pos:
pos = pos.split("/")[0] # Use primary position
position_counts[pos] = position_counts.get(pos, 0) + 1
missing_positions = []
for pos, required_count in requirements.items():
if position_counts.get(pos, 0) < required_count:
missing_positions.append(pos)
return len(missing_positions) == 0, missing_positions
requirements = {
"C": 1,
"1B": 1,
"2B": 1,
"3B": 1,
"SS": 1,
"LF": 1,
"CF": 1,
"RF": 1,
"DH": 1,
"SP": 1,
}
# Test valid roster
complete_roster = [
{"position": "C"},
{"position": "1B"},
{"position": "2B"},
{"position": "3B"},
{"position": "SS"},
{"position": "LF"},
{"position": "CF"},
{"position": "RF"},
{"position": "DH"},
{"position": "SP"},
]
is_valid, missing = validate_position_requirements(
complete_roster, requirements
)
assert is_valid is True
assert missing == []
# Test incomplete roster
incomplete_roster = [
{"position": "C"},
{"position": "1B"},
{"position": "2B"},
{"position": "3B"},
{"position": "SS"},
{"position": "LF"},
{"position": "CF"},
{"position": "RF"},
]
is_valid, missing = validate_position_requirements(
incomplete_roster, requirements
)
assert is_valid is False
assert "DH" in missing
assert "SP" in missing
def test_gauntlet_reward_tiers(self, gauntlet_cog):
"""Test gauntlet reward tier calculation."""
def calculate_reward_tier(wins, losses):
"""Calculate reward tier based on wins/losses."""
if wins >= 8:
return "Legendary"
elif wins >= 6:
return "Gold"
elif wins >= 4:
return "Silver"
elif wins >= 2:
return "Bronze"
else:
return "Participation"
assert calculate_reward_tier(8, 2) == "Legendary"
assert calculate_reward_tier(6, 3) == "Gold"
assert calculate_reward_tier(4, 4) == "Silver"
assert calculate_reward_tier(2, 6) == "Bronze"
assert calculate_reward_tier(1, 8) == "Participation"
def test_opponent_difficulty_scaling(self, gauntlet_cog):
"""Test opponent difficulty scaling by round."""
def get_opponent_difficulty(round_number):
"""Get opponent difficulty based on round."""
if round_number <= 2:
return "Easy"
elif round_number <= 5:
return "Medium"
elif round_number <= 8:
return "Hard"
else:
return "Expert"
assert get_opponent_difficulty(1) == "Easy"
assert get_opponent_difficulty(3) == "Medium"
assert get_opponent_difficulty(6) == "Hard"
assert get_opponent_difficulty(10) == "Expert"
def test_gauntlet_statistics_tracking(self, gauntlet_cog):
"""Test gauntlet statistics tracking calculations."""
gauntlet_history = [
{"wins": 5, "losses": 3, "reward_tier": "Silver"},
{"wins": 3, "losses": 5, "reward_tier": "Bronze"},
{"wins": 7, "losses": 2, "reward_tier": "Gold"},
{"wins": 2, "losses": 6, "reward_tier": "Bronze"},
]
# Calculate overall stats
total_runs = len(gauntlet_history)
total_wins = sum(run["wins"] for run in gauntlet_history)
total_losses = sum(run["losses"] for run in gauntlet_history)
win_percentage = total_wins / (total_wins + total_losses)
# Find best run
best_run = max(gauntlet_history, key=lambda x: x["wins"])
assert total_runs == 4
assert total_wins == 17
assert total_losses == 16
assert abs(win_percentage - 0.515) < 0.01 # ~51.5%
assert best_run["wins"] == 7
assert best_run["reward_tier"] == "Gold"
def test_draft_card_sorting(self, gauntlet_cog, mock_draft_pool):
"""Test draft card sorting and filtering."""
available_cards = mock_draft_pool["available_cards"]
# Sort by cost descending
by_cost = sorted(available_cards, key=lambda x: x["cost"], reverse=True)
assert by_cost[0]["cost"] == 500 # Shohei Ohtani
# Sort by position
by_position = sorted(available_cards, key=lambda x: x["position"])
positions = [card["position"] for card in by_position]
assert positions == sorted(positions)
# Filter by rarity
legendary_cards = [
card for card in available_cards if card["rarity"] == "Legendary"
]
all_star_cards = [
card for card in available_cards if card["rarity"] == "All-Star"
]
assert len(legendary_cards) == 1
assert len(all_star_cards) == 4
assert legendary_cards[0]["player_name"] == "Shohei Ohtani"
def test_gauntlet_progress_display(self, gauntlet_cog, mock_active_gauntlet_data):
"""Test gauntlet progress display formatting."""
def format_progress_display(gauntlet_data):
"""Format gauntlet progress for display."""
progress = {
"title": f"Round {gauntlet_data['current_round']}",
"record": f"{gauntlet_data['wins']}-{gauntlet_data['losses']}",
"status": gauntlet_data["status"].title(),
"opponents_defeated": len(gauntlet_data.get("opponents_defeated", [])),
"next_opponent": gauntlet_data.get("next_opponent", "TBD"),
}
return progress
progress = format_progress_display(mock_active_gauntlet_data)
assert progress["title"] == "Round 3"
assert progress["record"] == "2-0"
assert progress["status"] == "Active"
assert progress["opponents_defeated"] == 2
assert progress["next_opponent"] == "Baltimore Orioles"
@patch("logging.getLogger")
async def test_error_handling_and_logging(self, mock_logger, gauntlet_cog):
"""Test error handling and logging for gauntlet operations."""
mock_logger_instance = Mock()
mock_logger.return_value = mock_logger_instance
# Test draft validation error
with patch("gauntlets.validate_draft") as mock_validate:
mock_validate.side_effect = ValueError("Invalid draft selection")
try:
mock_validate([])
except ValueError:
# In actual implementation, this would be caught and logged
pass
def test_permission_checks(self, gauntlet_cog, mock_interaction):
"""Test permission checking for gauntlet commands."""
# Test role check
mock_member_with_role = Mock()
mock_member_with_role.roles = [Mock(name="Paper Dynasty")]
mock_interaction.user = mock_member_with_role
# Test channel check
with patch("helpers.legal_channel") as mock_legal_check:
mock_legal_check.return_value = True
result = mock_legal_check(mock_interaction.channel)
assert result is True
def test_multi_position_player_handling(self, gauntlet_cog):
"""Test handling of multi-position players in draft."""
multi_pos_player = {
"card_id": 201,
"player_name": "Shohei Ohtani",
"position": "SP/DH",
"cost": 500,
}
# Test position parsing
positions = multi_pos_player["position"].split("/")
assert "SP" in positions
assert "DH" in positions
assert len(positions) == 2
# Primary position should be first
primary_position = positions[0]
assert primary_position == "SP"
def test_gauntlet_elimination_logic(self, gauntlet_cog):
"""Test gauntlet elimination conditions."""
def is_eliminated(wins, losses, max_losses=3):
"""Check if gauntlet run should be eliminated."""
return losses >= max_losses
def is_completed(wins, losses, max_wins=8):
"""Check if gauntlet run is completed successfully."""
return wins >= max_wins
# Test active runs
assert not is_eliminated(5, 2)
assert not is_completed(5, 2)
# Test elimination
assert is_eliminated(3, 3)
assert is_eliminated(0, 3)
# Test completion
assert is_completed(8, 0)
assert is_completed(8, 2)
# Edge cases
assert not is_eliminated(7, 2) # Still active
assert is_completed(8, 3) # Completed despite losses