-
-
Notifications
You must be signed in to change notification settings - Fork 0
Mesmerizing Meteors #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6d74edf
443f5d4
73a92e2
1fe3439
b22ffad
4af8f32
992c0c6
fc03e5a
6245642
ba490d5
7873a34
72e4376
216c2e9
839d5db
dcc26a2
d3d4b01
9beb689
ab8635c
49456cf
4ec3689
c51d1e9
ccc52fc
b18aabb
4694e33
9ac5f77
1e87f61
0dd727d
d92f649
7733960
2a2f129
cad8a57
16fe710
577d13f
adb53b6
55a89b1
7dca12a
280fb6b
df2813a
5a61eff
2f642d8
984d3ae
c4ffe1f
6a3ce70
e33a45a
462da5b
8585bbf
74455f2
89abaf1
c6d8a22
fc5d53d
d9569fb
079572d
bf737fb
fc38372
02b05a8
7a87460
e0faa7e
3d424c8
dd9ca6b
f03b484
b5d8108
1c1c5ed
3d55b1b
74c4eee
15b8250
55bd764
615f9d8
728308e
73f9942
fa6ed5d
347b4d5
48ace86
1b0a653
1b78071
bc2781a
2a8ba6b
a036f61
4d9d7c8
3520fb2
00546e8
cfdc4f6
6283e43
c581efe
6061fad
f87619c
671f6ea
592ad9b
9cf5cc6
2073a8c
90b3efa
e5a17c5
aadca88
2a57440
c44d6e2
6bb379b
27babb1
be028f4
7ab5832
0a6360c
ab881a2
2000708
7045fc3
1dae3cc
64d0954
cade1e1
7db0753
7de8c00
ccc062e
3826441
2f738b7
293d7a2
65ca4d7
cd83e29
4ff1e79
8797a8d
79304d3
61b4898
b659ffa
9c67f45
a3f60c6
f5cdc4c
b4d80b4
bdfe7d0
43d67ac
756d474
c19ea61
9eedcd6
4d7b1c7
d4e049a
515d192
82dfb36
516196f
89f5e42
b4dc4a3
8d02c36
c406721
5eb2e4b
1250418
1094eae
8190110
6d7f8d4
b38dab9
3ef273a
9298681
50e98a3
8d669d5
967c92d
e1fd747
357d851
53fa278
666eb21
6ace6fd
1b92379
39dcfc6
8b5905a
53d2bf6
4bbc074
d01c8cc
d00be16
844c9b9
650b840
743825d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # GitHub Action workflow enforcing our code style. | ||
|
|
||
| name: Lint | ||
|
|
||
| # Trigger the workflow on both push (to the main repository, on the main branch) | ||
| # and pull requests (against the main repository, but from any repo, from any branch). | ||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| pull_request: | ||
|
|
||
| # Brand new concurrency setting! This ensures that not more than one run can be triggered for the same commit. | ||
| # It is useful for pull requests coming from the main repository since both triggers will match. | ||
| concurrency: lint-${{ github.sha }} | ||
|
|
||
| jobs: | ||
| lint: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| env: | ||
| # The Python version your project uses. Feel free to change this if required. | ||
| PYTHON_VERSION: "3.12" | ||
|
|
||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up Python ${{ env.PYTHON_VERSION }} | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: ${{ env.PYTHON_VERSION }} | ||
|
|
||
| - name: Run pre-commit hooks | ||
| uses: pre-commit/action@v3.0.1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # Files generated by the interpreter | ||
| __pycache__/ | ||
| *.py[cod] | ||
|
|
||
| # Environment specific | ||
| .venv | ||
| venv | ||
| .env | ||
| env | ||
|
|
||
| # Unittest reports | ||
| .coverage* | ||
|
|
||
| # Logs | ||
| *.log | ||
|
|
||
| # PyEnv version selector | ||
| .python-version | ||
|
|
||
| # Built objects | ||
| *.so | ||
| dist/ | ||
| build/ | ||
|
|
||
| # IDEs | ||
| # PyCharm | ||
| .idea/ | ||
| # VSCode | ||
| .vscode/ | ||
| # MacOS | ||
| .DS_Store | ||
| # vim | ||
| *.swp |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # Pre-commit configuration. | ||
| # See https://github.com/python-discord/code-jam-template/tree/main#pre-commit-run-linting-before-committing | ||
|
|
||
| repos: | ||
| - repo: https://github.com/pre-commit/pre-commit-hooks | ||
| rev: v4.6.0 | ||
| hooks: | ||
| - id: check-toml | ||
| - id: check-yaml | ||
| - id: end-of-file-fixer | ||
| - id: trailing-whitespace | ||
| args: [--markdown-linebreak-ext=md] | ||
|
|
||
| - repo: https://github.com/astral-sh/ruff-pre-commit | ||
| rev: v0.5.0 | ||
| hooks: | ||
| - id: ruff-format | ||
| - id: ruff |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| Copyright 2024 Gustav Odinger, Berkin İlkan Seçkin, Jaspe Michael Ingabire, Mouelle Ewane Richard C., Ifeanyichukwu Goodness | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,220 @@ | ||
| # Python Adventures | ||
|
|
||
| https://github.com/user-attachments/assets/8f392225-6d21-464f-9e55-bc74ca643df3 | ||
|
|
||
| *🔈Volume on* | ||
|
|
||
| Python Adventures is a Discord game bot where you learn about Python features while progressing your character through a map filled with challenges and secrets! | ||
|
|
||
| ## Table of contents | ||
| - [Python Adventures](#python-adventures) | ||
| - [Table of contents](#table-of-contents) | ||
| - [Theme: Information Overload](#theme-information-overload) | ||
| - [Gameplay](#gameplay) | ||
| - [Map](#map) | ||
| - [Navigation](#navigation) | ||
| - [Levels](#levels) | ||
| - [Limited lives](#limited-lives) | ||
| - [Code evaluation](#code-evaluation) | ||
| - [Hints](#hints) | ||
| - [Success/defeat/win screens](#successdefeatwin-screens) | ||
| - [Software architecture](#software-architecture) | ||
| - [Level/questions](#levelquestions) | ||
| - [Adding a new level](#adding-a-new-level) | ||
| - [Adding a new question type](#adding-a-new-question-type) | ||
| - [Modifying default behavior](#modifying-default-behavior) | ||
| - [Setup instructions](#setup-instructions) | ||
| - [Discord API Token](#discord-api-token) | ||
| - [How to Obtain a Discord API Token](#how-to-obtain-a-discord-api-token) | ||
| - [Discord Developer Portal Settings](#discord-developer-portal-settings) | ||
| - [Installation and Setup](#installation-and-setup) | ||
| - [Contributions](#contributions) | ||
|
|
||
|
|
||
| ## Theme: Information Overload | ||
| Python Adventures immerses players in a learning environment filled with information-rich levels. Each level combines multiple-choice questions and intricate coding challenges, demanding players process and apply vast amounts of information quickly and accurately. The special code golfing levels take this to the next level by requiring players to deal with dense, compact code — all while keeping it light and playful. | ||
|
|
||
| # Gameplay | ||
|
|
||
| Follow the main character Maria as she sets out on an epic journey to becoming a true Pythonista! The main storyline of the game consists of 11 levels, where completing one unlocks the next. | ||
|
|
||
| ## Map | ||
|
|
||
|  | ||
|
|
||
| The levels 1-11 consist of both multiple-choice questions and code writing challenges, to make learning exciting and really challenge your unstanding of the topics at hand. | ||
|
|
||
| There are also three *special* levels A, B and C, outside of the main storyline, which tackle code golfing. If your brain doesn't get overloaded from the compact information in oneliners and hacky ways of writing code, these levels perfect for you! | ||
|
|
||
| ### Navigation | ||
|  | ||
|
|
||
| To move between levels in Python Adventures, we have created a vibrant map that can be easily navigated using Discord's interaction buttons. The map is dynamically generated for the user, taking into account factors such as the following: | ||
| - **Unlocked levels**, including special levels | ||
| - **Completed levels** | ||
| - **Current player position**, persisted between sessions | ||
| - **Player display name**, for the name tag | ||
|
|
||
| In order to not clutter up the chat, navigation simply edits the original interaction response embed with the new map state. Buttons are also dynamically enabled/disabled based on whether or not an action can be taken. For example, if a tree is in the way, moving that direction will be disabled. | ||
|
|
||
| ## Levels | ||
| ### Limited lives | ||
|  | ||
|
|
||
| With three lives comes a maximum of three mistakes before you have to restart the level. This keeps the game exciting and turns the difficulty of even the multiple choice questions up a notch. | ||
|
|
||
| ### Code evaluation | ||
| The most important part of learning to code is writing code and actually trying things yourself. Because of this, we have made code writing an integral part of the levels, providing a Code Playground to test run your code before submitting. And once submitted, we run a battery of unit tests on the code to ensure it passes the requirements. | ||
|
|
||
| By utilizing the [Tio.run](https://tio.run) API to evaluate code, we are able to evaluate and test user code without needing to set up a sandboxed environment and without risking malicious input causing issues for the machine running the bot. | ||
|
|
||
| ### Hints | ||
| Sometimes you get stuck on a problem. While revealing the answer straight away won't help you much in learning, getting a hint or two in the right direction can be a game changer. We have provided hints for all questions that can be accessed one at a time by pressing the **Hint** button: | ||
|
|
||
|  | ||
|
|
||
| ### Success/defeat/win screens | ||
|  | ||
|
|
||
| With vibrant success/fail messages depending on how a level went for the player, we hope to keep the player engaged and excited to move toward the next goal. The images above are some of these and are displayed in the following situations: | ||
| - **Level failed**: the user fails a level | ||
| - **Level success**: the user completes a level | ||
| - **New level unlocked**: displayed after "level success" when the B or C level is unlocked | ||
| - **You win**: displayed when completing the final level of the game (level C) together with a message from the developers :) | ||
|
|
||
| # Software architecture | ||
|
|
||
| In order to create a game that is easy to maintain and simple to add new features to without breaking existing ones, we have put a lot of effort into designing a software architecture for the game that will allow precisely that. | ||
|
|
||
| ## Level/questions | ||
|
|
||
| Here is a diagram that explains the general level-question software architecture: | ||
|
|
||
|  | ||
|
|
||
| ### Adding a new level | ||
|
|
||
| This is how a new level is added: | ||
| - Subclass `Level` and set the desired attributes | ||
| - Adding `Level.register()` in the `register_all_levels()` function | ||
| - Add the level's questions in `questions.json` | ||
|
|
||
| The new level is now accessible and ready to be used anywhere in the bot through the `Controller` class! The map description, buttons and functionality at the provided coordinte will automatically match what was provided in the `Level` class. Better yet, all questions in `questions.json` will be automatically parsed, loaded, and ready to be used. | ||
|
|
||
| ### Adding a new question type | ||
|
|
||
| At one point while developing the game, we decided to add a code golf class. In a few minutes, the class was ready to go! By subclassing `WriteCodeQuestion` (which itself is a subclass of `Question`), the code golf question type got all the code execution, unit test and normal "question" logic from the `WriteCodeQuestion` and `Question` classes. | ||
|
|
||
| This is how a new question type is added: | ||
| - Subclass `Question` and set the desired attributes | ||
| - *Optional*: overwrite functions for question parsing, running, on_success, on_fail and other methods | ||
| - Add the question type to `Question.view` | ||
| - Add the question type to `question_factory` | ||
|
|
||
| Your question type is now fully supported by the bot and questions of the type can be added to `questions.json`! | ||
|
|
||
| ### Modifying default behavior | ||
|
|
||
| One major feature of the software architecture at hand, is the ability to extend and overwrite default functionality in question types and levels without touching the base classes. Here are a few of the supported overwrites: | ||
|
|
||
| **Question** | ||
| - `check_response`: logic to check if a given answer is correct | ||
| - `get_embed_description` and `embed`: customize the embed send for the question | ||
|
|
||
| **QuestionView** | ||
| - `on_quit`: called when the user presses "Quit" | ||
| - `on_success`: called when the user succeeds with a question | ||
| - `on_fail`: called when the user answers a question incorrectly | ||
|
|
||
| **Level** | ||
| - `run`: asks the user questions, displays status screen when level is over and then returns the user to the map | ||
| - `on_failure`: called when the user fails a level | ||
| - `on_success`: called when the user completes a level | ||
| - `_success_page` and `_success_more_pages`: used to customize the embeds being sent when the user completes the level, for presenting newly unlocked levels or win screens | ||
|
|
||
| These customization options have been crucial for the following and more: showing a quickguide about lives before the first level, unlocking special levels at the right times and customizing the success screens of levels to showcase the current status. In the future, these features would also be great for integrating more story into the game or giving the player certain rewards for completing levels, questions or other tasks. | ||
|
|
||
| # Setup instructions | ||
| ## Discord API Token | ||
| What is a Discord API Token? | ||
| A Discord API Token is a unique identifier used to authenticate requests to the Discord API. It acts as a password for your bot, allowing it to interact with Discord's servers, join channels, send messages, and perform other actions as defined by the Discord API. | ||
|
|
||
| ## How to Obtain a Discord API Token | ||
| 1. Create a New Application: | ||
| - Go to the [***Discord Developer Portal***](https://discord.com/developers/applications) | ||
| - Click on ***New Application*** | ||
| - Give your application a name and click **Create**. | ||
|
|
||
| 2. Create a Bot: | ||
| - Navigate to the ***Bot*** section in the sidebar. | ||
| - Click on "Add Bot" and confirm by clicking ***Yes, do it!*** | ||
|
|
||
| 3. Copy the Token: | ||
| - Under the "TOKEN" section, click "Copy" to copy your bot's token. | ||
| - Keep this token secure and never share it publicly. If your token is exposed, you should regenerate it immediately. | ||
|
|
||
| ## Discord Developer Portal Settings | ||
|
|
||
| 1. Log in to **[Discord Developer Portal](https://discord.com/developers/)** | ||
| 2. Create your new project with the ``New Application`` button. | ||
| 3. Activate the ``PRESENCE INTENT``, ``SERVER MEMBERS INTENT``, ``MESSAGE CONTENT INTENT`` in the ``Bot > Privileged Gateway Intents`` section. | ||
| 4. After creating your project, make the necessary settings in the ``Settings > OAuth2`` tab. | ||
| - Select the ``bot`` option from the OAuth2 URL Generator section | ||
| - Below, select the permissions you wish for the bot to have in the server. We recommend `Administrator` for testing purposes | ||
| 5. After clicking on the ``bot``, you can choose the permissions your bot will have from the window that opens. | ||
|
|
||
| **Recommended settings:** | ||
| ```` | ||
| Bot > General Permissions | ||
| - Manage Expressions | ||
| - Create Expressions | ||
| - View Channels | ||
| - View Server Insights | ||
|
|
||
| Bot > Text Permissions | ||
| - Send Messages | ||
| - Manage Messages | ||
| - Embed Links | ||
| - Attach Files | ||
| - Use External Emojis | ||
| - Use External Stickers | ||
| - Add Reactions | ||
| - Use Slash Commands | ||
| - Use Embeded activites | ||
| - Create Polls | ||
| ```` | ||
| - Depending on the options you set, you can add your bot to your server by opening the ``GENERATED URL`` in your browser, authenticating with discord, and adding it to a server of your choosing. | ||
| - Make sure you give your bot enough room to play. | ||
|
|
||
|
|
||
| ## Installation and Setup | ||
| To get your Discord bot up and running, follow these steps: | ||
| 1. **Clone the repository**: `git clone https://github.com/gustavwilliam/cj11-mesmerizing-meteors.git && cd cj11-mesmerizing-meteors` | ||
|
|
||
| 2. **Install Requirements**: | ||
| > - You can create a virtual environment by `python -m venv .venv` | ||
| > - Activate it by `source .venv/bin/activate` then pursue the setup | ||
|
|
||
| Install the dependencies using `pip install -r requirements.txt`. If you are developing the bot and not just running it, also consider installing dev requirements: `pip install -r dev-requirements.txt`. | ||
|
|
||
| 3. **Set Up Your Environment Variables**: | ||
| - Create a .env file in the root of your project and add your Discord API Token: `DISCORD_BOT_KEY="your-discord-token-here"` | ||
|
|
||
| 4. **Update Emoji Config**: | ||
| - Under `bot/assets/icons` you will find all the required emojis for the bot | ||
| - Add all these emojis to a server that your bot will be invited to | ||
| - Copy the ID of every emoji (send the custom emoji in a Discord channel and add `\` before it to see the ID. It will look something like this: `<:arrowright:1265077270515552339>` | ||
| - Update the IDs of every emoji in `bot/config.py` | ||
|
|
||
| 5. **Run the Bot**: `python bot/main.py` | ||
|
|
||
| # Contributions | ||
|
|
||
| This is a rough overview of what each team member has contributed: | ||
| - @gustavwilliam: graphics, map functionality and question+level classes | ||
| - @Noble-47: database functionality | ||
| - @jspmic: level questions, documentation and a (sadly not merged in time) help command | ||
| - @HeavenMercy: game intro video in this readme, linting and dotenv usage | ||
| - @Deja-Vu1: basic bot setup and discord developer portal instructions | ||
|
|
||
| Furthermore, each team member has contributed with valuable ideas and feedback throughout the development process. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Folder Containing the project itself | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: this README doesn't seem to add much value nor can I think of something you could add here that wouldn't just go in the root README. Might be better to remove it. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| # bot __init__ file | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: redundant comment |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| from enum import Enum | ||
|
|
||
|
|
||
| class Emoji(Enum): | ||
| """Configuration for custom Emojis.""" | ||
|
|
||
| CHECK = "<:check:1265079659448766506>" | ||
| ARROW_LEFT = "<:arrowleft:1265077268951339081>" | ||
| ARROW_RIGHT = "<:arrowright:1265077270515552339>" | ||
| ARROW_UP = "<:arrowup:1265077271970975874>" | ||
| ARROW_DOWN = "<:arrowdown:1265077267965673587>" | ||
| HINT = "<:hint:1265996402891292675>" | ||
| CROSS = "<:exit:1265999816023080991>" | ||
| LETTER_A = "<:letter_a:1265996405164474368>" | ||
| LETTER_B = "<:letter_b:1265996406028636232>" | ||
| LETTER_C = "<:letter_c:1265996407647768716>" | ||
| LETTER_D = "<:letter_d:1265996409040277595>" | ||
| LETTER_E = "<:letter_e:1265996410453622945>" | ||
| LETTER_F = "<:letter_f:1265996412601241663>" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| from typing import TYPE_CHECKING, ClassVar | ||
|
|
||
| if TYPE_CHECKING: | ||
| from levels import Level | ||
|
|
||
|
|
||
| class Controller: | ||
| """Manage levels and run them when requested.""" | ||
|
|
||
| _instance = None | ||
| levels: ClassVar[list[type["Level"]]] = [] | ||
|
|
||
| def __new__(cls, *args, **kwargs) -> "Controller": # noqa: ANN002, ANN003 | ||
| """Create a singleton instance of the Controller. | ||
|
|
||
| This allows the Levels to sign up directly to the Controller. Since there will only be one | ||
| Controller instance in the program, the Levels can be sure that they are signing up to the | ||
| correct Controller, without needeing to pass it as an argument. | ||
| """ | ||
| if not isinstance(cls._instance, cls): | ||
| cls._instance = object.__new__(cls, *args, **kwargs) | ||
| return cls._instance | ||
|
|
||
| def add_level(self, level: type["Level"]) -> None: | ||
| """Add a level to the controller. | ||
|
|
||
| Raises a ValueError if a level with the same id or map position already exists. | ||
| """ | ||
| if level.id in self.levels: | ||
| raise ValueError | ||
| if level.map_position in [level.map_position for level in self.levels]: | ||
| raise ValueError | ||
| self.levels.append(level) | ||
|
|
||
| def get_level(self, position: tuple[int, int]) -> type["Level"] | None: | ||
| """Get the level at a given map position, or return None if no level exists.""" | ||
| for level in self.levels: | ||
| if level.map_position == position: | ||
| return level | ||
| return None | ||
|
|
||
| def get_level_by_id(self, id: int) -> type["Level"] | None: | ||
| """Get the level with the given id, or return None if no level exists.""" | ||
| for level in self.levels: | ||
| if level.id == id: | ||
| return level | ||
| return None | ||
|
Comment on lines
+35
to
+47
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can optimise one of these cases by storing the levels in a dictionary. |
||
|
|
||
| def is_level(self, position: tuple[int, int]) -> bool: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: I think |
||
| """Check if a level exists at the given map position.""" | ||
| return any(level.map_position == position for level in self.levels) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think some of this information was duplicated in the
discord_guide.md