Skip to content

Commit 1bd710f

Browse files
feat: add "effect" mechanism for hidden paths (#8)
* feat: added scary haunted mansion story and choices now affect the game later * refactor: refactored and cherry picked according to comments * refactor: refactored and cherry picked according to comments * feat: added effect_restricts * docs: changed comments describing effect_restrics --------- Co-authored-by: Johan Nilsson <[email protected]> Co-authored-by: Strengthless <[email protected]>
1 parent af2d859 commit 1bd710f

File tree

3 files changed

+240
-16
lines changed

3 files changed

+240
-16
lines changed

bot/exts/fun/adventure.py

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import json
44
from contextlib import suppress
55
from pathlib import Path
6-
from typing import Literal, NamedTuple, TypedDict
6+
from typing import Literal, NamedTuple, NotRequired, TypedDict
77

88
from discord import Embed, HTTPException, Message, Reaction, User
99
from discord.ext import commands
@@ -48,6 +48,9 @@ class OptionData(TypedDict):
4848
text: str
4949
leads_to: str
5050
emoji: str
51+
requires_effect: NotRequired[str]
52+
effect_restricts: NotRequired[str]
53+
effect: NotRequired[str]
5154

5255

5356
class RoomData(TypedDict):
@@ -117,8 +120,9 @@ def __init__(
117120
self.message = None
118121

119122
# init session states
120-
self._current_room = "start"
121-
self._path = [self._current_room]
123+
self._current_room: str = "start"
124+
self._path: list[str] = [self._current_room]
125+
self._effects: list[str] = []
122126

123127
# session settings
124128
self.timeout_message = (
@@ -201,9 +205,7 @@ async def on_reaction_add(self, reaction: Reaction, user: User) -> None:
201205
emoji = str(reaction.emoji)
202206

203207
# check if valid action
204-
current_room = self._current_room
205-
available_options = self.game_data[current_room]["options"]
206-
acceptable_emojis = [option["emoji"] for option in available_options]
208+
acceptable_emojis = [option["emoji"] for option in self.available_options]
207209
if emoji not in acceptable_emojis:
208210
return
209211

@@ -214,7 +216,9 @@ async def on_reaction_add(self, reaction: Reaction, user: User) -> None:
214216
await self.message.clear_reactions()
215217

216218
# Run relevant action method
217-
await self.pick_option(acceptable_emojis.index(emoji))
219+
all_emojis = [option["emoji"] for option in self.all_options]
220+
221+
await self.pick_option(all_emojis.index(emoji))
218222

219223

220224
async def on_message_delete(self, message: Message) -> None:
@@ -237,20 +241,17 @@ def add_reactions(self) -> None:
237241
if self.is_in_ending_room:
238242
return
239243

240-
current_room = self._current_room
241-
available_options = self.game_data[current_room]["options"]
242-
reactions = [option["emoji"] for option in available_options]
244+
pickable_emojis = [option["emoji"] for option in self.available_options]
243245

244-
for reaction in reactions:
246+
for reaction in pickable_emojis:
245247
self._bot.loop.create_task(self.message.add_reaction(reaction))
246248

247249
def _format_room_data(self, room_data: RoomData) -> str:
248250
"""Formats the room data into a string for the embed description."""
249251
text = room_data["text"]
250-
options = room_data["options"]
251252

252253
formatted_options = "\n".join(
253-
f"{option["emoji"]} {option["text"]}" for option in options
254+
f"{option["emoji"]} {option["text"]}" for option in self.available_options
254255
)
255256

256257
return f"{text}\n\n{formatted_options}"
@@ -310,12 +311,39 @@ def is_in_ending_room(self) -> bool:
310311

311312
return self.game_data[current_room].get("type") == "end"
312313

314+
@property
315+
def all_options(self) -> list[OptionData]:
316+
"""Get all options in the current room."""
317+
return self.game_data[self._current_room]["options"]
318+
319+
@property
320+
def available_options(self) -> bool:
321+
"""
322+
Get "available" options in the current room.
323+
324+
This filters out options that require an effect that the user doesn't have or options that restrict an effect.
325+
"""
326+
filtered_options = filter(
327+
lambda option: (
328+
"requires_effect" not in option or option.get("requires_effect") in self._effects
329+
) and (
330+
"effect_restricts" not in option or option.get("effect_restricts") not in self._effects
331+
),
332+
self.all_options
333+
)
334+
335+
return filtered_options
336+
313337
async def pick_option(self, index: int) -> None:
314338
"""Event that is called when the user picks an option."""
315-
current_room = self._current_room
316-
next_room = self.game_data[current_room]["options"][index]["leads_to"]
339+
chosen_option = self.all_options[index]
340+
341+
next_room = chosen_option["leads_to"]
342+
new_effect = chosen_option.get("effect")
317343

318-
# update the path and current room
344+
# update all the game states
345+
if new_effect:
346+
self._effects.append(new_effect)
319347
self._path.append(next_room)
320348
self._current_room = next_room
321349

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
{
2+
"start": {
3+
"text": "You are Gurfelt, a curious purple dog, standing outside a creepy haunted mansion. The wind howls, and the front door creaks ominously. What will you do?",
4+
"options": [
5+
{
6+
"text": "Walk around the mansion",
7+
"leads_to": "grave",
8+
"emoji": "🐾"
9+
},
10+
{
11+
"text": "Enter the mansion",
12+
"leads_to": "lobby",
13+
"emoji": "🏚️"
14+
}
15+
]
16+
},
17+
"grave": {
18+
"text": "You circle around the mansion and come across an old, neglected grave. Something glimmers in the moonlight. Gurfelt pricks up his ears, unsure if he should investigate.",
19+
"options": [
20+
{
21+
"text": "Dig up the grave",
22+
"leads_to": "digged_grave",
23+
"emoji": "⛏️",
24+
"effect_restricts": "rusty_key"
25+
},
26+
{
27+
"text": "Leave the grave and go inside the mansion",
28+
"leads_to": "lobby",
29+
"emoji": "🏃"
30+
}
31+
]
32+
},
33+
"digged_grave": {
34+
"text": "You dig up the grave and find a rusty key! Gurfelt picks it up, should he take it with him?.",
35+
"options": [
36+
{
37+
"text": "Take the key",
38+
"leads_to": "lobby",
39+
"emoji": "🔑",
40+
"effect": "rusty_key"
41+
},
42+
{
43+
"text": "Leave the grave and go inside the mansion",
44+
"leads_to": "lobby",
45+
"emoji": "🏃"
46+
}
47+
]
48+
},
49+
"lobby": {
50+
"text": "Stepping through the door, Gurfelt is immediately greeted by flickering lights and a sudden BANG—something just slammed shut behind him! Gurfelt has the feeling that he is not alone here...",
51+
"options": [
52+
{
53+
"text": "Go upstairs",
54+
"leads_to": "upstairs",
55+
"emoji": "🚪"
56+
},
57+
{
58+
"text": "Explore the living room",
59+
"leads_to": "living_room",
60+
"emoji": "🛋️"
61+
}
62+
]
63+
},
64+
"living_room": {
65+
"text": "The living room is dimly lit. Old portraits line the walls, and a faint shimmer appears near the fireplace. Suddenly, a ghost emerges from the shadows!",
66+
"options": [
67+
{
68+
"text": "Talk to the ghost",
69+
"leads_to": "ghost_info",
70+
"emoji": "👻"
71+
},
72+
{
73+
"text": "Proceed to the kitchen",
74+
"leads_to": "kitchen",
75+
"emoji": "🍽️"
76+
},
77+
{
78+
"text": "Return to the lobby",
79+
"leads_to": "lobby",
80+
"emoji": "🏚️"
81+
}
82+
]
83+
},
84+
"ghost_info": {
85+
"text": "The ghost tells you a disturbing secret: 'the mansion once belonged to a reclusive inventor who vanished under mysterious circumstances. The inventor tried to create a potato which could feed thousands of people but something went wrong' Chills run down Gurfelt’s spine.",
86+
"options": [
87+
{
88+
"text": "Check out the kitchen",
89+
"leads_to": "kitchen",
90+
"emoji": "🍽️"
91+
},
92+
{
93+
"text": "Head back to the lobby",
94+
"leads_to": "lobby",
95+
"emoji": "🏚️"
96+
}
97+
]
98+
},
99+
"kitchen": {
100+
"text": "You enter a dusty kitchen filled with rusty utensils and scattered knives. There is also a single potato masher on the counter...",
101+
"options": [
102+
{
103+
"text": "Take a knife and go to the lobby",
104+
"leads_to": "lobby",
105+
"emoji": "🔪",
106+
"effect": "knife",
107+
"effect_restricts": "knife"
108+
},
109+
{
110+
"text": "Take the potato masher and go to the lobby",
111+
"leads_to": "lobby",
112+
"emoji": "🥔",
113+
"effect": "potato_masher",
114+
"effect_restricts": "potato_masher"
115+
},
116+
{
117+
"text": "Return to the lobby empty-handed",
118+
"leads_to": "lobby",
119+
"emoji": "🚪"
120+
}
121+
]
122+
},
123+
"upstairs": {
124+
"text": "You carefully climb the creaky stairs. A dusty corridor extends ahead with two doors: one leads to the attic, and another looks locked, possibly requiring a key.",
125+
"options": [
126+
{
127+
"text": "Go to the attic",
128+
"leads_to": "attic",
129+
"emoji": "🔦"
130+
},
131+
{
132+
"text": "Open the secret room",
133+
"leads_to": "secret_room",
134+
"emoji": "🗝️",
135+
"requires_effect": "rusty_key"
136+
}
137+
]
138+
},
139+
"secret_room": {
140+
"text": "You unlock the door with the rusty key, revealing a trove of gold coins and... a copy of GTA 6?! Overjoyed, Gurfelt decides this is enough excitement (and wealth) for one day!",
141+
"type": "end",
142+
"emoji": "🎉"
143+
},
144+
"attic": {
145+
"text": "The attic is dark and cluttered with old boxes. Suddenly, a giant potato monster lumbers out of the shadows, roaring at Gurfelt!",
146+
"options": [
147+
{
148+
"text": "Eat the monster",
149+
"leads_to": "end_eat_monster",
150+
"emoji": "😖"
151+
},
152+
{
153+
"text": "Use the knife",
154+
"leads_to": "end_knife_monster",
155+
"emoji": "🔪",
156+
"requires_effect": "knife"
157+
},
158+
{
159+
"text": "Try to charm the monster",
160+
"leads_to": "end_charm_monster",
161+
"emoji": "🪄"
162+
},
163+
{
164+
"text": "Mash the monster",
165+
"leads_to": "end_mash_monster",
166+
"emoji": "🥔",
167+
"requires_effect": "potato_masher"
168+
}
169+
]
170+
},
171+
"end_eat_monster": {
172+
"text": "Gurfelt tries to eat the potato monster. It tastes terrible! Horrified by the awful taste, Gurfelt bolts away in disgust. The adventure ends here.",
173+
"type": "end",
174+
"emoji": "🤢"
175+
},
176+
"end_knife_monster": {
177+
"text": "Gurfelt raises the knife, ready to strike, but hesitates. A question grips him—does his life hold more value than the monster's? Doubt consumes him. He sinks to his knees, lost in uncertainty. The adventure ends here.",
178+
"type": "end",
179+
"emoji": "🗿"
180+
},
181+
"end_charm_monster": {
182+
"text": "Gurfelt tries to charm the potato monster with a blown kiss and a wagging tail, but it only angers the beast. Gurfelt flees, defeated and spooked. The adventure ends here.",
183+
"type": "end",
184+
"emoji": "😱"
185+
},
186+
"end_mash_monster": {
187+
"text": "Armed with the potato masher, Gurfelt reduces the monstrous spud to harmless mash! Victorious, Gurfelt claims the haunted attic as conquered. The adventure ends in triumph!",
188+
"type": "end",
189+
"emoji": "🏆"
190+
}
191+
}

bot/resources/fun/adventures/available_games.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,10 @@
88
"id": "dragon_slayer",
99
"name": "Dragon Slayer",
1010
"description": "A dragon is terrorizing the kingdom! You are a brave knight, tasked with rescuing the princess and defeating the dragon."
11+
},
12+
{
13+
"id": "Gurfelts_haunted_mansion",
14+
"name": "Gurfelt's Haunted Mansion",
15+
"description": "Explore a haunted mansion and uncover its secrets!"
1116
}
1217
]

0 commit comments

Comments
 (0)