sir-lancebot icon indicating copy to clipboard operation
sir-lancebot copied to clipboard

feat: add `.adventure` command

Open Strengthless opened this issue 9 months ago • 3 comments

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 setup function loads the Adventure cog;
  • the Adventure cog contains all the commands that can be invoked;
  • when .adventure [game_code_or_index] is run, we instantiate a GameSession which contains all the states, business logic, event handlers, and helper functions for the actual game to run.
  • when .adventures or .adventure is run, we instantiate a GameSession without game_data, raising a GameCodeNotFoundError error, 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 .adventures or .adventure to view a list of available games.
  • [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 LinePaginator for the list of available games

Did you:

Strengthless avatar Mar 01 '25 00:03 Strengthless

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: image

Strengthless avatar Mar 02 '25 15:03 Strengthless

@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.

Strengthless avatar Apr 01 '25 20:04 Strengthless

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.

wookie184 avatar Apr 08 '25 12:04 wookie184