Skip to content

Commit 338ef32

Browse files
committed
Madlibs - Add "End Game" and "Choose for me" buttons
1 parent 58fb7d6 commit 338ef32

File tree

1 file changed

+161
-12
lines changed

1 file changed

+161
-12
lines changed

bot/exts/fun/madlibs.py

Lines changed: 161 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import json
23
from pathlib import Path
34
from random import choice
@@ -9,11 +10,11 @@
910
from bot.bot import Bot
1011
from bot.constants import Colours, NEGATIVE_REPLIES
1112

12-
TIMEOUT = 60.0
13+
TIMEOUT = 120
1314

1415

1516
class MadlibsTemplate(TypedDict):
16-
"""Structure of a template in the madlibs JSON file."""
17+
"""Structure of a template in the madlibs_templates JSON file."""
1718

1819
title: str
1920
blanks: list[str]
@@ -27,6 +28,10 @@ def __init__(self, bot: Bot):
2728
self.bot = bot
2829
self.templates = self._load_templates()
2930
self.edited_content = {}
31+
self.submitted_words = {}
32+
self.view = None
33+
self.wait_task: asyncio.Task | None = None
34+
self.end_game = False
3035
self.checks = set()
3136

3237
@staticmethod
@@ -73,8 +78,15 @@ async def madlibs(self, ctx: commands.Context) -> None:
7378
"""
7479
random_template = choice(self.templates)
7580

81+
self.end_game = False
82+
7683
def author_check(message: discord.Message) -> bool:
77-
return message.channel.id == ctx.channel.id and message.author.id == ctx.author.id
84+
if message.channel.id != ctx.channel.id or message.author.id != ctx.author.id:
85+
return False
86+
87+
# Ignore commands while a game is running
88+
prefix = ctx.prefix or ""
89+
return not (prefix and message.content.startswith(prefix))
7890

7991
self.checks.add(author_check)
8092

@@ -83,17 +95,49 @@ def author_check(message: discord.Message) -> bool:
8395
)
8496
original_message = await ctx.send(embed=loading_embed)
8597

86-
submitted_words = {}
87-
8898
for i, part_of_speech in enumerate(random_template["blanks"]):
8999
inputs_left = len(random_template["blanks"]) - i
90100

101+
if self.view and getattr(self.view, "cooldown_task", None) and not self.view.cooldown_task.done():
102+
self.view.cooldown_task.cancel()
103+
104+
self.view = MadlibsView(ctx, self, 60, part_of_speech, i)
105+
91106
madlibs_embed = self.madlibs_embed(part_of_speech, inputs_left)
92-
await original_message.edit(embed=madlibs_embed)
107+
await original_message.edit(embed=madlibs_embed, view=self.view)
93108

109+
self.view.cooldown_task = asyncio.create_task(self.view.enable_random_button_after(original_message))
110+
111+
self.wait_task = asyncio.create_task(
112+
self.bot.wait_for("message", timeout=TIMEOUT, check=author_check)
113+
)
94114
try:
95-
message = await self.bot.wait_for("message", check=author_check, timeout=TIMEOUT)
115+
message = await self.wait_task
116+
self.submitted_words[i] = message.content
117+
except asyncio.CancelledError:
118+
if self.end_game:
119+
if self.view:
120+
self.view.stop()
121+
for child in self.view.children:
122+
if isinstance(child, discord.ui.Button):
123+
child.disabled = True
124+
125+
# cancel cooldown cleanly
126+
task = getattr(self.view, "cooldown_task", None)
127+
if task and not task.done():
128+
task.cancel()
129+
130+
await original_message.edit(view=self.view)
131+
self.checks.remove(author_check)
132+
133+
return
134+
# else: "Choose for me" set self.submitted_words[i]; just continue
96135
except TimeoutError:
136+
# If we ended the game around the same time, don't show timeout
137+
if self.end_game:
138+
self.checks.remove(author_check)
139+
return
140+
97141
timeout_embed = discord.Embed(
98142
title=choice(NEGATIVE_REPLIES),
99143
description="Uh oh! You took too long to respond!",
@@ -102,16 +146,24 @@ def author_check(message: discord.Message) -> bool:
102146

103147
await ctx.send(ctx.author.mention, embed=timeout_embed)
104148

105-
for msg_id in submitted_words:
106-
self.edited_content.pop(msg_id, submitted_words[msg_id])
149+
self.view.stop()
150+
for child in self.view.children:
151+
if isinstance(child, discord.ui.Button):
152+
child.disabled = True
153+
154+
await original_message.edit(view=self.view)
155+
156+
for j in self.submitted_words:
157+
self.edited_content.pop(j, self.submitted_words[j])
107158

108159
self.checks.remove(author_check)
109160

110161
return
162+
finally:
163+
# Clean up so the next iteration doesn't see an old task
164+
self.wait_task = None
111165

112-
submitted_words[message.id] = message.content
113-
114-
blanks = [self.edited_content.pop(msg_id, submitted_words[msg_id]) for msg_id in submitted_words]
166+
blanks = [self.submitted_words[j] for j in range(len(random_template["blanks"]))]
115167

116168
self.checks.remove(author_check)
117169

@@ -134,6 +186,20 @@ def author_check(message: discord.Message) -> bool:
134186

135187
await ctx.send(embed=story_embed)
136188

189+
# After sending the story, disable the view and cancel all wait tasks
190+
if self.view:
191+
task = getattr(self.view, "cooldown_task", None)
192+
if task and not task.done():
193+
task.cancel()
194+
self.view.stop()
195+
for child in self.view.children:
196+
if isinstance(child, discord.ui.Button):
197+
child.disabled = True
198+
await original_message.edit(view=self.view)
199+
200+
if self.wait_task and not self.wait_task.done():
201+
self.wait_task.cancel()
202+
137203
@madlibs.error
138204
async def handle_madlibs_error(self, ctx: commands.Context, error: commands.CommandError) -> None:
139205
"""Error handler for the Madlibs command."""
@@ -142,6 +208,89 @@ async def handle_madlibs_error(self, ctx: commands.Context, error: commands.Comm
142208
error.handled = True
143209

144210

211+
class MadlibsView(discord.ui.View):
212+
"""A set of buttons to control a Madlibs game."""
213+
214+
def __init__(self, ctx: commands.Context, cog: "Madlibs", cooldown: float = 0,
215+
part_of_speech: str = "", index: int = 0):
216+
super().__init__(timeout=120)
217+
self.disabled = None
218+
self.ctx = ctx
219+
self.cog = cog
220+
self.word_bank = self._load_word_bank()
221+
self.part_of_speech = part_of_speech
222+
self.index = index
223+
self._cooldown = cooldown
224+
225+
# Reference to the async task that will re-enable the button
226+
self.cooldown_task: asyncio.Task | None = None
227+
228+
if cooldown > 0:
229+
self.random_word_button.disabled = True
230+
231+
async def enable_random_button_after(self, message: discord.Message) -> None:
232+
"""Function that controls the cooldown of the "Choose for me" button to prevent spam."""
233+
if self._cooldown <= 0:
234+
return
235+
await asyncio.sleep(self._cooldown)
236+
237+
# Game ended or this view is no longer the active one
238+
if self.is_finished() or self is not self.cog.view:
239+
return
240+
241+
self.random_word_button.disabled = False
242+
await message.edit(view=self)
243+
244+
@staticmethod
245+
def _load_word_bank() -> dict[str, list[str]]:
246+
word_bank = Path("bot/resources/fun/madlibs_word_bank.json")
247+
248+
with open(word_bank) as file:
249+
return json.load(file)
250+
251+
@discord.ui.button(style=discord.ButtonStyle.green, label="Choose for me")
252+
async def random_word_button(self, interaction: discord.Interaction, *_) -> None:
253+
"""Button that randomly chooses a word for the user if they cannot think of a word."""
254+
if interaction.user == self.ctx.author:
255+
random_word = choice(self.word_bank[self.part_of_speech])
256+
self.cog.submitted_words[self.index] = random_word
257+
258+
wait_task = getattr(self.cog, "wait_task", None)
259+
if wait_task and not wait_task.done():
260+
wait_task.cancel()
261+
262+
if self.cooldown_task and not self.cooldown_task.done():
263+
self.cooldown_task.cancel()
264+
265+
await interaction.response.send_message(f"Randomly chosen word: {random_word}", ephemeral=True)
266+
267+
# Re-disable the button and restart the cooldown (so it can't be clicked again immediately)
268+
self.random_word_button.disabled = True
269+
await interaction.followup.edit_message(view=self)
270+
else:
271+
await interaction.response.send_message("Only the owner of the game can end it!", ephemeral=True)
272+
273+
@discord.ui.button(style=discord.ButtonStyle.red, label="End Game")
274+
async def end_button(self, interaction: discord.Interaction, *_) -> None:
275+
"""Button that ends the current game."""
276+
if interaction.user == self.ctx.author:
277+
# Cancel the wait task if it's running
278+
self.cog.end_game = True
279+
wait_task = getattr(self.cog, "wait_task", None)
280+
if wait_task and not wait_task.done():
281+
wait_task.cancel()
282+
283+
# Disable all buttons in the view
284+
for child in self.children:
285+
if isinstance(child, discord.ui.Button):
286+
child.disabled = True
287+
288+
await interaction.response.send_message("Ended the current game.", ephemeral=True)
289+
await interaction.followup.edit_message(message_id=interaction.message.id, view=self)
290+
else:
291+
await interaction.response.send_message("Only the owner of the game can end it!", ephemeral=True)
292+
293+
145294
async def setup(bot: Bot) -> None:
146295
"""Load the Madlibs cog."""
147296
await bot.add_cog(Madlibs(bot))

0 commit comments

Comments
 (0)