|
| 1 | +"""Contains cog classes for any Everest interactions.""" |
| 2 | + |
| 3 | +import logging |
| 4 | +from enum import Enum |
| 5 | +from typing import TYPE_CHECKING, override |
| 6 | + |
| 7 | +import discord |
| 8 | + |
| 9 | +from utils import TeXBotBaseCog |
| 10 | + |
| 11 | +if TYPE_CHECKING: |
| 12 | + from collections.abc import Sequence |
| 13 | + from collections.abc import Set as AbstractSet |
| 14 | + from logging import Logger |
| 15 | + from typing import Final |
| 16 | + |
| 17 | + from utils import TeXBotApplicationContext, TeXBotAutocompleteContext |
| 18 | + |
| 19 | +__all__: "Sequence[str]" = ("EverestCommandCog",) |
| 20 | + |
| 21 | +logger: "Final[Logger]" = logging.getLogger("TeX-Bot") |
| 22 | + |
| 23 | +MOUNT_EVEREST_TOTAL_STEPS: "Final[int]" = 44250 |
| 24 | + |
| 25 | + |
| 26 | +class _CourseTypes(Enum): |
| 27 | + B_SC = "B.Sc." |
| 28 | + M_SCI = "M.Sci." |
| 29 | + |
| 30 | + def get_course_year_weighting(self, course_year: int) -> float: |
| 31 | + """Calculate the grade weighting the given year has for this course type.""" |
| 32 | + match (self, course_year): |
| 33 | + case _CourseTypes.B_SC, 1: |
| 34 | + return 0 |
| 35 | + case _CourseTypes.M_SCI, 1: |
| 36 | + return 0 |
| 37 | + case _CourseTypes.B_SC, 2: |
| 38 | + return 0.25 |
| 39 | + case _CourseTypes.M_SCI, 2: |
| 40 | + return 0.2 |
| 41 | + case _CourseTypes.B_SC, 3: |
| 42 | + return 0.75 |
| 43 | + case _CourseTypes.M_SCI, 3: |
| 44 | + return 0.4 |
| 45 | + case _CourseTypes.M_SCI, 4: |
| 46 | + return 0.4 |
| 47 | + case _: |
| 48 | + INVALID_COURSE_YEAR_OR_TYPE_MESSAGE: Final[str] = ( |
| 49 | + f"Cannot calculate weighting for given course year ('{course_year}') " |
| 50 | + f"and type ('{self}')." |
| 51 | + ) |
| 52 | + raise ValueError(INVALID_COURSE_YEAR_OR_TYPE_MESSAGE) |
| 53 | + |
| 54 | + @override |
| 55 | + def __str__(self) -> str: |
| 56 | + return self.value |
| 57 | + |
| 58 | + |
| 59 | +class EverestCommandCog(TeXBotBaseCog): |
| 60 | + """Cog class that defines the "/everest" command and its call-back method.""" |
| 61 | + |
| 62 | + async def autocomplete_get_course_years( |
| 63 | + self, ctx: "TeXBotAutocompleteContext" |
| 64 | + ) -> "AbstractSet[discord.OptionChoice] | AbstractSet[int] | AbstractSet[str]": |
| 65 | + """Autocomplete for the course year option.""" |
| 66 | + try: |
| 67 | + selected_course_type: _CourseTypes | str = ctx.options["course-type"] |
| 68 | + except KeyError: |
| 69 | + return {1, 2, 3, 4} |
| 70 | + |
| 71 | + if not isinstance(selected_course_type, _CourseTypes): |
| 72 | + selected_course_type = selected_course_type.strip() |
| 73 | + |
| 74 | + if not selected_course_type: |
| 75 | + return {1, 2, 3, 4} |
| 76 | + |
| 77 | + try: |
| 78 | + selected_course_type = _CourseTypes[selected_course_type] |
| 79 | + except ValueError: |
| 80 | + return set() |
| 81 | + |
| 82 | + match selected_course_type: |
| 83 | + case _CourseTypes.B_SC: |
| 84 | + return {1, 2, 3} |
| 85 | + case _CourseTypes.M_SCI: |
| 86 | + return {1, 2, 3, 4} |
| 87 | + |
| 88 | + @discord.slash_command( # type: ignore[no-untyped-call, misc] |
| 89 | + name="everest", description="How many steps of everest is your assignment worth?" |
| 90 | + ) |
| 91 | + @discord.option( # type: ignore[no-untyped-call, misc] |
| 92 | + name="course-type", |
| 93 | + description="The type of your university course.", |
| 94 | + input_type=str, |
| 95 | + choices=( # NOTE: Display name is stored in the enum's value. |
| 96 | + discord.OptionChoice(name=course_type.value, value=course_type.name) |
| 97 | + for course_type in _CourseTypes |
| 98 | + ), |
| 99 | + required=True, |
| 100 | + parameter_name="raw_course_type", |
| 101 | + ) |
| 102 | + @discord.option( # type: ignore[no-untyped-call, misc] |
| 103 | + name="course-year", |
| 104 | + description="The current year of your university course.", |
| 105 | + input_type=int, |
| 106 | + autocomplete=autocomplete_get_course_years, # NOTE: Choices cannot be used for validation as they are static an preclude the ability to have dynamic autocomplete |
| 107 | + required=True, |
| 108 | + parameter_name="course_year", |
| 109 | + ) |
| 110 | + @discord.option( # type: ignore[no-untyped-call, misc] |
| 111 | + name="percentage-of-module", |
| 112 | + description="The percentage of the module that the assignment is worth.", |
| 113 | + input_type=float, |
| 114 | + autocomplete=discord.utils.basic_autocomplete( # NOTE: Pycord documents that they accept any iterable, testing shows that they only accept lists (generators do not work correctly). |
| 115 | + [ |
| 116 | + discord.OptionChoice(name=f"{percentage * 5:.1f}%", value=percentage * 5) |
| 117 | + for percentage in range(1, 21) |
| 118 | + ] |
| 119 | + ), |
| 120 | + required=True, |
| 121 | + parameter_name="percentage_of_module", |
| 122 | + ) |
| 123 | + async def everest( # type: ignore[misc] |
| 124 | + self, |
| 125 | + ctx: "TeXBotApplicationContext", |
| 126 | + raw_course_type: str, |
| 127 | + course_year: int, |
| 128 | + percentage_of_module: float, |
| 129 | + ) -> None: |
| 130 | + """Calculate how many steps of Mount Everest an assignment is worth.""" |
| 131 | + try: |
| 132 | + course_type: _CourseTypes = _CourseTypes[raw_course_type] |
| 133 | + except KeyError: |
| 134 | + await self.command_send_error( |
| 135 | + ctx=ctx, message=f"Invalid course type: '{raw_course_type}'." |
| 136 | + ) |
| 137 | + return |
| 138 | + |
| 139 | + if course_year < 1 or course_year > 10: |
| 140 | + await self.command_send_error( |
| 141 | + ctx=ctx, message=f"Invalid course year: '{course_year}'." |
| 142 | + ) |
| 143 | + return |
| 144 | + |
| 145 | + if percentage_of_module < 0 or percentage_of_module > 100: |
| 146 | + await self.command_send_error( |
| 147 | + ctx=ctx, |
| 148 | + message=( |
| 149 | + f"Percentage of module: '{percentage_of_module}' is not valid. " |
| 150 | + "Please enter a percentage between 0 - 100." |
| 151 | + ), |
| 152 | + ) |
| 153 | + return |
| 154 | + |
| 155 | + try: |
| 156 | + course_year_weighting: float = course_type.get_course_year_weighting(course_year) |
| 157 | + except KeyError: |
| 158 | + await self.command_send_error( |
| 159 | + ctx, |
| 160 | + message=( |
| 161 | + f"Invalid course year ('{course_year}') for course type '{course_type}'." |
| 162 | + ), |
| 163 | + ) |
| 164 | + return |
| 165 | + |
| 166 | + logger.debug("User %s used '/everest' command", ctx.user) |
| 167 | + |
| 168 | + await ctx.respond( |
| 169 | + content=( |
| 170 | + f"**Course**: {course_type}\n" |
| 171 | + f"**Year**: {course_year}\n" |
| 172 | + f"**Percentage of Module**: {percentage_of_module:.1f}%\n" |
| 173 | + f"This assignment is worth { # NOTE: Assumes all modules are 20 credits |
| 174 | + int( |
| 175 | + (percentage_of_module / 100) |
| 176 | + * (1 / 6) |
| 177 | + * course_year_weighting |
| 178 | + * MOUNT_EVEREST_TOTAL_STEPS |
| 179 | + ) |
| 180 | + } steps of Mt. Everest!" |
| 181 | + ), |
| 182 | + ephemeral=True, |
| 183 | + ) |
0 commit comments