Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ Changes since 2026.06.04
- Changes the display of .duck stats to be a bit nicer
- Bot accounts are no longer able to participate in the duck hunt
- The caller of the .duck spawn command can no longer participate in the hunt
- Adds a new /duck next command to show admins the next duck spawn time
- Fuzzes duck spawn time to make it less predictable

### Hangman
- /hangman start is no longer logged in discord
Expand Down
56 changes: 50 additions & 6 deletions modules/fun/duck.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import discord
from discord import Color as embed_colors
from discord import app_commands
from discord.ext import commands

import configuration
Expand All @@ -35,13 +36,18 @@ class DuckHunt(cogs.LoopCog):
"""Class for the actual duck commands

Attributes:
duck_group (app_commands.Group): The group for the /duck commands
DUCK_PIC_URL (str): The picture for the duck
BEFRIEND_URL (str): The picture for the befriend target
KILL_URL (str): The picture for the kill target
ON_START (bool): ???
CHANNELS_KEY (str): The config item for the channels that the duck hunt should run
"""

duck_group: app_commands.Group = app_commands.Group(
name="duck", description="...", extras={"module": "duck"}
)

DUCK_PIC_URL: str = (
"https://www.iconarchive.com/download/i107380/google/"
+ "noto-emoji-animals-nature/22276-duck.512.png"
Expand All @@ -60,20 +66,33 @@ async def loop_preconfig(self: Self) -> None:
"""Preconfig for cooldowns"""
self.cooldowns = {}

# "guild_id": datetime
self.next_duck: dict[str, datetime.datetime] = {}

async def wait(self: Self, guild: discord.Guild) -> None:
"""Waits a random amount of time before sending another duck
This function shouldn't be manually called

Args:
guild (discord.Guild): The guild where the duck is running
guild (discord.Guild): The guild where the duck is going to appear
"""
await asyncio.sleep(
random.randint(
configuration.get_config_entry(guild.id, "duck_min_wait") * 3600,
configuration.get_config_entry(guild.id, "duck_max_wait") * 3600,
)
min_wait = configuration.get_config_entry(guild.id, "duck_min_wait") * 3600
max_wait = configuration.get_config_entry(guild.id, "duck_max_wait") * 3600

fuzzed_min = int(min_wait * random.uniform(0.9, 1.1))
fuzzed_max = int(max_wait * random.uniform(0.9, 1.1))

Copilot AI May 4, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The independent fuzzing of min_wait and max_wait may result in fuzzed_min being greater than fuzzed_max, potentially causing random.randint to throw an error. Consider ensuring that the lower bound is always less than or equal to the upper bound.

Suggested change
if fuzzed_min > fuzzed_max:
fuzzed_min, fuzzed_max = fuzzed_max, fuzzed_min

Copilot uses AI. Check for mistakes.
if fuzzed_min > fuzzed_max:
fuzzed_min, fuzzed_max = fuzzed_max, fuzzed_min

wait_time = random.randint(fuzzed_min, fuzzed_max)

self.next_duck[str(guild.id)] = datetime.datetime.now() + datetime.timedelta(
seconds=wait_time
)

await asyncio.sleep(wait_time)

async def execute(
self: Self,
guild: discord.Guild,
Expand Down Expand Up @@ -108,6 +127,7 @@ async def execute(
use_channel = channel

self.cooldowns[guild.id] = {}
del self.next_duck[str(guild.id)]

embed = discord.Embed(
title="*Quack Quack*",
Expand Down Expand Up @@ -409,6 +429,30 @@ async def get_global_record(self: Self, guild_id: int) -> float:

return float(min(speed_records, key=float))

@app_commands.checks.has_permissions(administrator=True)
@duck_group.command(
name="next",
description="Displays the time for the next duck for this guild",
)
async def lookup_next_duck(self: Self, interaction: discord.Interaction) -> None:
"""A simple command to show an admin when the next duck will be spawning

Args:
interaction (discord.Interaction): The interaction that called this command
"""
if str(interaction.guild.id) not in self.next_duck:
embed = auxiliary.prepare_deny_embed(
"Couldn't find a future duck for this guild."
)
await interaction.response.send_message(embed=embed, ephemeral=True)
return

embed = auxiliary.prepare_confirm_embed(
"The next duck in this guild:"
f"<t:{int(self.next_duck[str(interaction.guild.id)].timestamp())}>"
)
await interaction.response.send_message(embed=embed, ephemeral=True)

@commands.group(
brief="Executes a duck command",
description="Executes a duck command",
Expand Down
Loading