feat: add `.adventure` command
Relevant Issues
This adds a text-based RPG adventure game. Closes #238.
Description
In this PR, we add a new adventure.py file, along with several JSON files as game assets.
The architecture of adventure.py is pretty similar is that of help.py:
- the
setupfunction loads theAdventurecog; - the
Adventurecog contains all the commands that can be invoked; - when
.adventure [game_code_or_index]is run, we instantiate aGameSessionwhich contains all the states, business logic, event handlers, and helper functions for the actual game to run. - when
.adventuresor.adventureis run, we instantiate aGameSessionwithoutgame_data, raising aGameCodeNotFoundErrorerror, and is subsequently handled by sending a message to the channel.
Note: we are currently working on a few feature additions, e.g. support display of images in the rooms, and a retry button for restarting the game after it has ended. However, we would still like to get some early feedback from the dev team. Thanks!
Scope
- [x] Write a playable prototype of your game as a bot command.
- [x] Use
.adventure [game_code]or.adventure [index]to play the game. - [x] Use
.adventuresor.adventureto view a list of available games.
- [x] Use
- [x] Make all player interactions reactions instead of having the player type commands.
- [x] Make the entire game happen in a single message that the bot edits, instead of having the bot post new messages.
- [x] Make a system that is possible to easily extend with new campaigns.
- [x] Define your own rooms, choices, collectibles (i.e., effects) and endings in a JSON format!
- [x] Customize game settings such as embed color, timeout seconds, etc.
- [x] Display a list of available games, or an error message if the game does not exist.
- [x] Support multiple concurrent games.
- [x] One player can instantiate multiple games at once.
- [x] More than one game can be played at the same time, and players can only react to their own game.
How to use
As a player
To test out the command, simply run .adventure or .adventures to see available games. You can then run .adventure [index] or .adventure [code] to start a game.
As a game developer
You need to include your game info in the available_games.json file. Then, design your rooms, collectibles and endings in [game_code].json.
You will need to include a starting room with "start" as key, and choices that lead to other rooms. Repeat this until your storyline reaches an ending (of course, you can have multiple endings). Trust us, the JSON format is very intuitive. Take a look at the three sample JSON files we've provided, and you shall understand it quickly. :)
The only interesting caveat here, is effect, requires_effect and effect_restricts (optional fields in OptionData). Basically, they're "items" or "collectibles" that you can get by choosing a specific option.
Once you have obtained the item, options with requires_effect set to that item will become unlocked, and is now a valid option for the player; at the same time, options with effect_restricts set to that item will become locked.
This "memory" mechanism allows you to design your game much more efficiently, and opens up a lot more interesting possibilities. Just as an example, you can introduce hidden paths, collectible items, ever-changing scenes, and much more!
Showcase video
https://github.com/user-attachments/assets/7e4a43e6-ca50-4315-97f3-a316f6e63667
Future considerations
- [ ] Allow aborting of games
- [ ] Allow display of images in the rooms
- [ ] Allow the user to see a report of their choices/ collectibles at the end of the game
- [ ] Add a retry button to restart the game after it has ended
- [ ] Annotate type of endings with "neural", "good" or "bad"
- [ ] Use
LinePaginatorfor the list of available games
Did you:
- [x] Join the Python Discord Community?
- [x] Read all the comments in this template?
- [x] Ensure there is an issue open, or link relevant discord discussions?
- [x] Read and agree to the contributing guidelines?
Thanks @wookie184, ~most of the comments have been addressed, except for one about GameData, which I've left some comments on. Could you please advise?~
edit: following an offline discussion over Discord, I have decided to refactor it via commit 8cb5f97. All comments are now addressed.
I've also included an extra commit 75d4044 that fixes an echo vulnerability, along with a QoL update that allows backticked game codes to be processed as normal:
@wookie184 Sorry for the delayed response - I've been quite busy recently.
That being said, I've just pushed a quick fix for this, though a little ugly.
I timeboxed half an hour for this, and unfortunately I couldn't come up with a clean solution for the race condition you mentioned (without refactoring a bunch of code) 🤔 Do we have any utility functions for mutex/ critical sections/ something like that?
Let me know what you think about this patch.
I timeboxed half an hour for this, and unfortunately I couldn't come up with a clean solution for the race condition you mentioned (without refactoring a bunch of code) 🤔 Do we have any utility functions for mutex/ critical sections/ something like that?
Using an asyncio.Lock would be an option to ensure you don't have two reaction handlers running at the same time. Refactoring to avoid mutable state as much as possible (e.g. trying to put as much logic as possible in @staticmethod functions) is also a good way to simplify code (as you no longer have to worry about variables changing their value half way through the function).
Your fix is good though.