Jerome Palayoor

Contact

Sir Count A Lot

A Discord bot for a counting game, members count up one number at a time, and the chain resets the moment someone breaks the rules.

The counting rules

Every message in a set-up channel gets checked. If it's not a plain number, the bot ignores it, someone can still chat normally in the same channel.

if not message.content.lstrip("-").isdigit():
    return

From there, three things can happen: the number is correct, the same person tries to count twice in a row, or the number is just wrong.

if record["Previous"] == message.author.id:
    record["Previous"] = 0
    record["Current"] = 0
    save_data(data)
    await message.add_reaction("❌")
    await message.channel.send(
        f"{message.author.mention} broke the chain. You cannot count twice in a row. Next number is 1"
    )
elif old + 1 == new:
    record["Current"] = new
    record["Highest"] = max(record["Highest"], new)
    record["Previous"] = message.author.id
    save_data(data)
    await message.add_reaction("✅")
else:
    record["Current"] = 0
    record["Previous"] = 0
    save_data(data)
    await message.add_reaction("❌")
    await message.channel.send(f"{message.author.mention} broke the chain. Next number is 1")

record["Previous"] stores whoever counted last. If the same person's ID shows up twice in a row, the chain resets, even if the number itself would've been correct. That's a different failure than just posting the wrong number, so it gets its own message.

Sir Count A Lot resetting the chain because the same user counted twice in a row Sir Count A Lot resetting the chain because the wrong number was posted

Either way, the count resets to 0 and the next expected number goes back to 1. A correct count just bumps Current up by one and tracks Highest if it's a new record.

Setting up a channel

>setup and >remove are administrator functions

@bot.command(name="setup")
@commands.has_permissions(administrator=True)
async def setup_command(ctx):
    async with data_lock:
        data = load_data()
        existing = get_channel_record(data, str(ctx.guild.id), str(ctx.channel.id))
        if existing is not None:
            await ctx.send(f"Sir Count A Lot is already set up in #{ctx.channel.name}")
            return
        ensure_channel_record(data, ctx.guild, ctx.channel)
        save_data(data)
    await ctx.send(f"Added Sir Count A Lot to #{ctx.channel.name}, start counting now!")

Each server and channel gets its own independent record, so the bot can run a separate counting game in as many channels as admins set up, across as many servers as it's in.

Sir Count A Lot being set up in a Discord channel Sir Count A Lot counting being removed from a Discord channel

Tracking stats

Data persists to a plain JSON file on disk, loaded and saved around every command and every counted message.

@bot.command(name="showstats")
async def showstats_command(ctx):
    async with data_lock:
        data = load_data()
        record = get_channel_record(data, str(ctx.guild.id), str(ctx.channel.id))
    if record is None:
        await ctx.send(f"Sir Count A Lot isn't set up in #{ctx.channel.name}. Run `>setup` first.")
        return
    embed = discord.Embed(title=f"Stats for #{ctx.channel.name}", color=discord.Color.from_rgb(0, 255, 4))
    embed.add_field(name="Current count", value=record["Current"], inline=True)
    embed.add_field(name="Highest count", value=record["Highest"], inline=True)
    await ctx.send(embed=embed)
Sir Count A Lot showing current and highest count stats for a channel

data_lock is an asyncio.Lock, wrapped around every read and write to the data file. Discord messages can arrive faster than a single load-modify-save cycle finishes, without the lock, two counts arriving close together could both read the same "before" state and step on each other's update.

Contact Me