Fix trivia, update agents/champions, rename /status, fix OLLAMA env var
- Fix trivia: 25 questions with 4 answer buttons (A/B/C/D), 30s timeout, per-user answer tracking, gaming + internet culture categories - Update Valorant agents: add Clove, Tejo, Waylay, Vyse, Veto (28 total) - Update LoL champions: expand from ~70 to ~166 champions across all lanes - Rename /status to /health to avoid Wordle conflict - Fix OLLAMA_HOST env var mismatch (code read OLLAMA_URL, .env has OLLAMA_HOST) - Add /trivia to help command Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
147
bot.py
147
bot.py
@@ -68,7 +68,7 @@ HYTALE_SERVER_UUID = os.getenv('HYTALE_SERVER_UUID', '7a656836-c3f3-491e-ac55-66
|
|||||||
STATUS_UPDATE_INTERVAL = int(os.getenv('STATUS_UPDATE_INTERVAL', '300'))
|
STATUS_UPDATE_INTERVAL = int(os.getenv('STATUS_UPDATE_INTERVAL', '300'))
|
||||||
|
|
||||||
# --- Issue #7: Hardcoded URLs - Use env vars for internal services ---
|
# --- Issue #7: Hardcoded URLs - Use env vars for internal services ---
|
||||||
OLLAMA_URL = os.getenv('OLLAMA_URL', 'http://10.10.10.157:11434')
|
OLLAMA_URL = os.getenv('OLLAMA_HOST', 'http://10.10.10.157:11434')
|
||||||
|
|
||||||
# --- Issue #11: Magic Numbers - Extract constants ---
|
# --- Issue #11: Magic Numbers - Extract constants ---
|
||||||
MIN_USERNAME_LENGTH = 3
|
MIN_USERNAME_LENGTH = 3
|
||||||
@@ -114,7 +114,7 @@ class ConfigValidator:
|
|||||||
OPTIONAL = {
|
OPTIONAL = {
|
||||||
'MINECRAFT_RCON_PASSWORD': 'Minecraft commands',
|
'MINECRAFT_RCON_PASSWORD': 'Minecraft commands',
|
||||||
'PELICAN_API_KEY': 'Pelican integration',
|
'PELICAN_API_KEY': 'Pelican integration',
|
||||||
'OLLAMA_URL': '/ask command'
|
'OLLAMA_HOST': '/ask command'
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -404,7 +404,7 @@ class CustomBot(commands.Bot):
|
|||||||
if not PELICAN_API_KEY:
|
if not PELICAN_API_KEY:
|
||||||
logger.warning("PELICAN_API_KEY not set - Pelican features disabled")
|
logger.warning("PELICAN_API_KEY not set - Pelican features disabled")
|
||||||
if not OLLAMA_URL:
|
if not OLLAMA_URL:
|
||||||
logger.warning("OLLAMA_URL not set - /ask command will fail")
|
logger.warning("OLLAMA_HOST not set - /ask command will fail")
|
||||||
|
|
||||||
load_whitelisted_usernames()
|
load_whitelisted_usernames()
|
||||||
|
|
||||||
@@ -741,6 +741,7 @@ async def help_command(interaction: discord.Interaction):
|
|||||||
embed.add_field(name="/random", value="Generate a random number between specified range", inline=False)
|
embed.add_field(name="/random", value="Generate a random number between specified range", inline=False)
|
||||||
embed.add_field(name="/rps", value="Play Rock Paper Scissors against the bot", inline=False)
|
embed.add_field(name="/rps", value="Play Rock Paper Scissors against the bot", inline=False)
|
||||||
embed.add_field(name="/poll", value="Create a simple yes/no poll", inline=False)
|
embed.add_field(name="/poll", value="Create a simple yes/no poll", inline=False)
|
||||||
|
embed.add_field(name="/trivia", value="Play a trivia game with random questions", inline=False)
|
||||||
|
|
||||||
# Game Commands
|
# Game Commands
|
||||||
embed.add_field(name="/agent", value="Get a random Valorant agent with their role", inline=False)
|
embed.add_field(name="/agent", value="Get a random Valorant agent with their role", inline=False)
|
||||||
@@ -761,7 +762,7 @@ async def help_command(interaction: discord.Interaction):
|
|||||||
embed.add_field(name="/clear", value="Clears specified number of messages from the channel", inline=False)
|
embed.add_field(name="/clear", value="Clears specified number of messages from the channel", inline=False)
|
||||||
embed.add_field(name="/lockdown", value="Lock a channel to prevent messages", inline=False)
|
embed.add_field(name="/lockdown", value="Lock a channel to prevent messages", inline=False)
|
||||||
embed.add_field(name="/unlock", value="Unlock a previously locked channel", inline=False)
|
embed.add_field(name="/unlock", value="Unlock a previously locked channel", inline=False)
|
||||||
embed.add_field(name="/status", value="Check bot system health and service status", inline=False)
|
embed.add_field(name="/health", value="Check bot system health and service status", inline=False)
|
||||||
|
|
||||||
embed.set_footer(text="Made by https://lotusguild.org")
|
embed.set_footer(text="Made by https://lotusguild.org")
|
||||||
await interaction.response.send_message(embed=embed)
|
await interaction.response.send_message(embed=embed)
|
||||||
@@ -1198,10 +1199,10 @@ async def revive(interaction: discord.Interaction, member: discord.Member):
|
|||||||
@handle_command_errors
|
@handle_command_errors
|
||||||
async def agent(interaction: discord.Interaction):
|
async def agent(interaction: discord.Interaction):
|
||||||
agents = {
|
agents = {
|
||||||
"Duelists": ["Jett", "Phoenix", "Raze", "Reyna", "Yoru", "Neon", "Iso"],
|
"Duelists": ["Jett", "Phoenix", "Raze", "Reyna", "Yoru", "Neon", "Iso", "Waylay"],
|
||||||
"Controllers": ["Brimstone", "Viper", "Omen", "Astra", "Harbor"],
|
"Controllers": ["Brimstone", "Viper", "Omen", "Astra", "Harbor", "Clove"],
|
||||||
"Initiators": ["Sova", "Breach", "Skye", "KAY/O", "Fade", "Gekko"],
|
"Initiators": ["Sova", "Breach", "Skye", "KAY/O", "Fade", "Gekko", "Tejo"],
|
||||||
"Sentinels": ["Killjoy", "Cypher", "Sage", "Chamber", "Deadlock"]
|
"Sentinels": ["Killjoy", "Cypher", "Sage", "Chamber", "Deadlock", "Vyse", "Veto"]
|
||||||
}
|
}
|
||||||
|
|
||||||
category = random.choice(list(agents.keys()))
|
category = random.choice(list(agents.keys()))
|
||||||
@@ -1222,11 +1223,45 @@ async def agent(interaction: discord.Interaction):
|
|||||||
@handle_command_errors
|
@handle_command_errors
|
||||||
async def champion(interaction: discord.Interaction):
|
async def champion(interaction: discord.Interaction):
|
||||||
champions = {
|
champions = {
|
||||||
"Top": ["Aatrox", "Camille", "Darius", "Fiora", "Garen", "Irelia", "Jax", "K'Sante", "Malphite", "Mordekaiser", "Nasus", "Ornn", "Riven", "Sett", "Teemo"],
|
"Top": [
|
||||||
"Jungle": ["Bel'Veth", "Diana", "Elise", "Evelynn", "Graves", "Hecarim", "Kayn", "Kindred", "Lee Sin", "Master Yi", "Nidalee", "Rammus", "Viego", "Warwick"],
|
"Aatrox", "Ambessa", "Aurora", "Camille", "Cho'Gath", "Darius",
|
||||||
"Mid": ["Ahri", "Akali", "Annie", "Cassiopeia", "Fizz", "Katarina", "LeBlanc", "Lux", "Orianna", "Syndra", "Twisted Fate", "Veigar", "Yasuo", "Zed"],
|
"Dr. Mundo", "Fiora", "Gangplank", "Garen", "Gnar", "Gragas",
|
||||||
"Bot": ["Aphelios", "Ashe", "Caitlyn", "Draven", "Ezreal", "Jhin", "Jinx", "Kai'Sa", "Lucian", "Miss Fortune", "Samira", "Tristana", "Vayne", "Xayah"],
|
"Gwen", "Illaoi", "Irelia", "Jax", "Jayce", "K'Sante", "Kennen",
|
||||||
"Support": ["Blitzcrank", "Brand", "Janna", "Leona", "Lulu", "Morgana", "Nautilus", "Pyke", "Rakan", "Senna", "Seraphine", "Soraka", "Thresh", "Yuumi"]
|
"Kled", "Malphite", "Mordekaiser", "Nasus", "Olaf", "Ornn",
|
||||||
|
"Poppy", "Quinn", "Renekton", "Riven", "Rumble", "Sett", "Shen",
|
||||||
|
"Singed", "Sion", "Teemo", "Trundle", "Tryndamere", "Urgot",
|
||||||
|
"Vladimir", "Volibear", "Wukong", "Yone", "Yorick"
|
||||||
|
],
|
||||||
|
"Jungle": [
|
||||||
|
"Amumu", "Bel'Veth", "Briar", "Diana", "Ekko", "Elise",
|
||||||
|
"Evelynn", "Fiddlesticks", "Graves", "Hecarim", "Ivern",
|
||||||
|
"Jarvan IV", "Kayn", "Kha'Zix", "Kindred", "Lee Sin", "Lillia",
|
||||||
|
"Maokai", "Master Yi", "Nidalee", "Nocturne", "Nunu", "Olaf",
|
||||||
|
"Rek'Sai", "Rengar", "Sejuani", "Shaco", "Skarner", "Taliyah",
|
||||||
|
"Udyr", "Vi", "Viego", "Warwick", "Xin Zhao", "Zac"
|
||||||
|
],
|
||||||
|
"Mid": [
|
||||||
|
"Ahri", "Akali", "Akshan", "Annie", "Aurelion Sol", "Azir",
|
||||||
|
"Cassiopeia", "Corki", "Ekko", "Fizz", "Galio", "Heimerdinger",
|
||||||
|
"Hwei", "Irelia", "Katarina", "LeBlanc", "Lissandra", "Lux",
|
||||||
|
"Malzahar", "Mel", "Naafiri", "Neeko", "Orianna", "Qiyana",
|
||||||
|
"Ryze", "Sylas", "Syndra", "Talon", "Twisted Fate", "Veigar",
|
||||||
|
"Vex", "Viktor", "Vladimir", "Xerath", "Yasuo", "Yone", "Zed",
|
||||||
|
"Zoe"
|
||||||
|
],
|
||||||
|
"Bot": [
|
||||||
|
"Aphelios", "Ashe", "Caitlyn", "Draven", "Ezreal", "Jhin",
|
||||||
|
"Jinx", "Kai'Sa", "Kalista", "Kog'Maw", "Lucian",
|
||||||
|
"Miss Fortune", "Nilah", "Samira", "Sivir", "Smolder",
|
||||||
|
"Tristana", "Twitch", "Varus", "Vayne", "Xayah", "Zeri"
|
||||||
|
],
|
||||||
|
"Support": [
|
||||||
|
"Alistar", "Bard", "Blitzcrank", "Brand", "Braum", "Janna",
|
||||||
|
"Karma", "Leona", "Lulu", "Lux", "Milio", "Morgana", "Nami",
|
||||||
|
"Nautilus", "Pyke", "Rakan", "Rell", "Renata Glasc", "Senna",
|
||||||
|
"Seraphine", "Sona", "Soraka", "Swain", "Taric", "Thresh",
|
||||||
|
"Yuumi", "Zilean", "Zyra"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
lane = random.choice(list(champions.keys()))
|
lane = random.choice(list(champions.keys()))
|
||||||
@@ -1246,22 +1281,82 @@ async def champion(interaction: discord.Interaction):
|
|||||||
@client.tree.command(name="trivia", description="Play a trivia game")
|
@client.tree.command(name="trivia", description="Play a trivia game")
|
||||||
@handle_command_errors
|
@handle_command_errors
|
||||||
async def trivia(interaction: discord.Interaction):
|
async def trivia(interaction: discord.Interaction):
|
||||||
|
questions = [
|
||||||
|
# Gaming
|
||||||
|
{"q": "What year was the original Super Mario Bros. released?", "options": ["1983", "1985", "1987", "1990"], "answer": 1},
|
||||||
|
{"q": "Which game features the quote 'The cake is a lie'?", "options": ["Half-Life 2", "Portal", "BioShock", "Minecraft"], "answer": 1},
|
||||||
|
{"q": "What is the max level in League of Legends?", "options": ["16", "18", "20", "25"], "answer": 1},
|
||||||
|
{"q": "Which Valorant agent has the codename 'Deadeye'?", "options": ["Jett", "Sova", "Chamber", "Cypher"], "answer": 2},
|
||||||
|
{"q": "How many Ender Dragon eggs can exist in a vanilla Minecraft world?", "options": ["1", "2", "Unlimited", "0"], "answer": 0},
|
||||||
|
{"q": "What was the first battle royale game to hit mainstream popularity?", "options": ["Fortnite", "PUBG", "H1Z1", "Apex Legends"], "answer": 2},
|
||||||
|
{"q": "In Minecraft, what is the rarest ore?", "options": ["Diamond", "Emerald", "Ancient Debris", "Lapis Lazuli"], "answer": 1},
|
||||||
|
{"q": "What is the name of the main character in The Legend of Zelda?", "options": ["Zelda", "Link", "Ganondorf", "Epona"], "answer": 1},
|
||||||
|
{"q": "Which game has the most registered players of all time?", "options": ["Fortnite", "Minecraft", "League of Legends", "Roblox"], "answer": 1},
|
||||||
|
{"q": "What type of animal is Sonic?", "options": ["Fox", "Hedgehog", "Rabbit", "Echidna"], "answer": 1},
|
||||||
|
{"q": "In Among Us, what is the maximum number of impostors?", "options": ["1", "2", "3", "4"], "answer": 2},
|
||||||
|
{"q": "What does GG stand for in gaming?", "options": ["Get Good", "Good Game", "Go Go", "Great Going"], "answer": 1},
|
||||||
|
{"q": "Which company developed Valorant?", "options": ["Blizzard", "Valve", "Riot Games", "Epic Games"], "answer": 2},
|
||||||
|
{"q": "What is the highest rank in Valorant?", "options": ["Immortal", "Diamond", "Radiant", "Challenger"], "answer": 2},
|
||||||
|
{"q": "In League of Legends, what is Baron Nashor an anagram of?", "options": ["Baron Roshan", "Roshan", "Nashor Baron", "Nash Robot"], "answer": 1},
|
||||||
|
# General/Internet culture
|
||||||
|
{"q": "What does HTTP stand for?", "options": ["HyperText Transfer Protocol", "High Tech Transfer Program", "HyperText Transmission Process", "Home Tool Transfer Protocol"], "answer": 0},
|
||||||
|
{"q": "What year was Discord founded?", "options": ["2013", "2015", "2017", "2019"], "answer": 1},
|
||||||
|
{"q": "What programming language has a logo that is a snake?", "options": ["Java", "Ruby", "Python", "Go"], "answer": 2},
|
||||||
|
{"q": "How many bits are in a byte?", "options": ["4", "8", "16", "32"], "answer": 1},
|
||||||
|
{"q": "What does 'RGB' stand for?", "options": ["Really Good Build", "Red Green Blue", "Red Gold Black", "Rapid Gaming Boost"], "answer": 1},
|
||||||
|
{"q": "What is the most subscribed YouTube channel?", "options": ["PewDiePie", "MrBeast", "T-Series", "Cocomelon"], "answer": 1},
|
||||||
|
{"q": "What does 'AFK' stand for?", "options": ["A Free Kill", "Away From Keyboard", "Always Fun Killing", "Another Fake Knockdown"], "answer": 1},
|
||||||
|
{"q": "What animal is the Linux mascot?", "options": ["Fox", "Penguin", "Cat", "Dog"], "answer": 1},
|
||||||
|
{"q": "What does 'NPC' stand for?", "options": ["Non-Player Character", "New Player Content", "Normal Playing Conditions", "Never Played Competitively"], "answer": 0},
|
||||||
|
{"q": "In what year was the first iPhone released?", "options": ["2005", "2006", "2007", "2008"], "answer": 2},
|
||||||
|
]
|
||||||
|
|
||||||
|
question = random.choice(questions)
|
||||||
|
labels = ["A", "B", "C", "D"]
|
||||||
|
|
||||||
class TriviaView(discord.ui.View):
|
class TriviaView(discord.ui.View):
|
||||||
def __init__(self, correct_answer):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__(timeout=30)
|
||||||
self.correct_answer = correct_answer
|
self.answered_users = set()
|
||||||
|
|
||||||
|
async def handle_answer(self, interaction: discord.Interaction, selected: int):
|
||||||
|
if interaction.user.id in self.answered_users:
|
||||||
|
await interaction.response.send_message("You already answered!", ephemeral=True)
|
||||||
|
return
|
||||||
|
self.answered_users.add(interaction.user.id)
|
||||||
|
if selected == question["answer"]:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"Correct! The answer was **{labels[question['answer']]}. {question['options'][question['answer']]}**",
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"Wrong! The correct answer was **{labels[question['answer']]}. {question['options'][question['answer']]}**",
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
@discord.ui.button(label="A", style=discord.ButtonStyle.primary)
|
@discord.ui.button(label="A", style=discord.ButtonStyle.primary)
|
||||||
async def answer_a(self, interaction: discord.Interaction, button: discord.ui.Button):
|
async def answer_a(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
if "A" == self.correct_answer:
|
await self.handle_answer(interaction, 0)
|
||||||
await interaction.response.send_message("Correct!", ephemeral=True)
|
|
||||||
else:
|
|
||||||
await interaction.response.send_message("Wrong!", ephemeral=True)
|
|
||||||
|
|
||||||
embed = discord.Embed(title="Trivia Question", description="What is 2+2?")
|
@discord.ui.button(label="B", style=discord.ButtonStyle.primary)
|
||||||
embed.add_field(name="A", value="4")
|
async def answer_b(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
embed.add_field(name="B", value="5")
|
await self.handle_answer(interaction, 1)
|
||||||
await interaction.response.send_message(embed=embed, view=TriviaView("A"))
|
|
||||||
|
@discord.ui.button(label="C", style=discord.ButtonStyle.primary)
|
||||||
|
async def answer_c(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
await self.handle_answer(interaction, 2)
|
||||||
|
|
||||||
|
@discord.ui.button(label="D", style=discord.ButtonStyle.primary)
|
||||||
|
async def answer_d(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
await self.handle_answer(interaction, 3)
|
||||||
|
|
||||||
|
embed = discord.Embed(title="Trivia Time!", color=discord.Color.from_rgb(152, 0, 0))
|
||||||
|
embed.description = question["q"]
|
||||||
|
for i, option in enumerate(question["options"]):
|
||||||
|
embed.add_field(name=f"{labels[i]}", value=option, inline=True)
|
||||||
|
embed.set_footer(text="You have 30 seconds to answer!")
|
||||||
|
await interaction.response.send_message(embed=embed, view=TriviaView())
|
||||||
|
|
||||||
|
|
||||||
@client.tree.command(name="userinfo", description="Get information about a user")
|
@client.tree.command(name="userinfo", description="Get information about a user")
|
||||||
@@ -1276,10 +1371,10 @@ async def userinfo(interaction: discord.Interaction, member: discord.Member):
|
|||||||
|
|
||||||
|
|
||||||
# --- Issue #16: Health Check Command ---
|
# --- Issue #16: Health Check Command ---
|
||||||
@client.tree.command(name="status", description="Check bot system status")
|
@client.tree.command(name="health", description="Check bot system status")
|
||||||
@has_role_check(ADMIN_ROLE_ID)
|
@has_role_check(ADMIN_ROLE_ID)
|
||||||
@handle_command_errors
|
@handle_command_errors
|
||||||
async def status(interaction: discord.Interaction):
|
async def health(interaction: discord.Interaction):
|
||||||
"""Show bot health metrics"""
|
"""Show bot health metrics"""
|
||||||
stats = metrics.get_stats()
|
stats = metrics.get_stats()
|
||||||
uptime_hours = stats["uptime_seconds"] / 3600
|
uptime_hours = stats["uptime_seconds"] / 3600
|
||||||
|
|||||||
Reference in New Issue
Block a user