diff --git a/pyproject.toml b/pyproject.toml index bb13960..e08c389 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tetr_cli" -version = "0.8.0" +version = "1.0.0" description = "Setup script for the tetr_cli package." authors = [ { name = "Airun_Iru", email = "hugo_s_tanaka@yahoo.com" } diff --git a/setup.py b/setup.py index 21c4546..96079ed 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="tetr_cli", - version="0.8.0", + version="1.0.0", packages=find_packages(), entry_points={"console_scripts": ["tetr_cli = tetr_cli.starter:starter"]}, python_requires=">=3.7", diff --git a/tetr_cli/main.py b/tetr_cli/main.py index ffc3469..101a87e 100644 --- a/tetr_cli/main.py +++ b/tetr_cli/main.py @@ -30,7 +30,7 @@ window, KEY_RESIZE, ) -from pygame import mixer + from pygame.mixer import Sound from tetr_cli.tetr_modules.keyboard_handlers.curses_handler import curses_key_name @@ -42,7 +42,12 @@ ) from tetr_cli.tetr_modules.modules.debug import DebugClass from tetr_cli.tetr_modules.modules.database import get_setting -from tetr_cli.tetr_modules.modules.sound import load_sfx, play_sounds +from tetr_cli.tetr_modules.modules.sound import ( + load_sfx, + play_sounds, + stop_all_sounds, + update_volume, +) # O, I, T, L, J, S, Z @@ -50,16 +55,29 @@ TRANSITION_LIST: Dict[str, str] = { "Main_Menu": "main_menu", "Solo_Menu": "solo.solo_menu", - - # Option Modes + # Audio Options "Option_Menu": "options.option", - "Audio_Options": "options.audio_options", - # "Gameplay_Options": "options.gameplay_options", - + "Audio_Options": "options.audio_options.audio_options", + "BGM_Option": "options.audio_options.bgm_option", + "SFX_Option": "options.audio_options.sfx_option", + # Game Options + "Gameplay_Options": "options.gameplay_options.gameplay_options", + "DAS_Option": "options.gameplay_options.das_option", + "ARR_Option": "options.gameplay_options.arr_option", + "Ghost_Piece_Option": "options.gameplay_options.ghost_piece_option", + # Graphic Options + "Graphic_Options": "options.graphic_options.graphic_options", + "Color_Option": "options.graphic_options.color_option", + "Mino_Style_Option": "options.graphic_options.mino_design", + "FPS_Option": "options.graphic_options.fps_option", + # Control Options + "Control_Options": "options.control_options.control_options", + "Change_Keybind": "options.control_options.change_keybind", "Score_Screen": "score_screen", - # Solo Modes "Marathon": "solo.marathon", + "Sprint": "solo.sprint", + "Ultra": "solo.ultra", } @@ -82,8 +100,6 @@ async def main( audio_check: bool = not no_music_mode try: - mixer.init() - mixer.music.set_volume(0.25) sound_effect_dict: Dict[str, Sound] = await load_sfx() except Exception: audio_check = False @@ -104,13 +120,13 @@ async def main( use_default_colors() if curses.COLORS >= 256: - init_pair(1, COLOR_YELLOW, -1) # O - init_pair(2, COLOR_CYAN, -1) # I - init_pair(3, COLOR_MAGENTA, -1) # T + init_pair(1, 214, -1) # O + init_pair(2, 51, -1) # I + init_pair(3, 201, -1) # T init_pair(4, 208, -1) # L - init_pair(5, COLOR_BLUE, -1) # J - init_pair(6, COLOR_GREEN, -1) # S - init_pair(7, COLOR_RED, -1) # Z + init_pair(5, 27, -1) # J + init_pair(6, 46, -1) # S + init_pair(7, 196, -1) # Z init_pair(8, 244, -1) # Garbage else: init_pair(1, COLOR_YELLOW, -1) # O @@ -125,7 +141,7 @@ async def main( start_time: float = 0.0 elapsed_time: float = 0.0 - frame_limit: int = int(get_setting("FPS_limit")) + frame_limit: int = int(get_setting("fps_limit", "30")) frame_duration: float = 1 / frame_limit try: @@ -194,15 +210,18 @@ async def main( stdscr.refresh() if "update_fps" in actions: - frame_limit = int(get_setting("FPS_limit")) + frame_limit = int(get_setting("fps_limit", "30")) frame_duration = 1 / frame_limit + + if "update_volume" in actions and audio_check: + await update_volume(sound_effect_dict=sound_effect_dict) + elapsed_time = perf_counter() - start_time except KeyboardInterrupt: pass - if mixer and audio_check: - mixer.music.stop() - mixer.quit() + if audio_check: + await stop_all_sounds() nocbreak() noecho() curs_set(True) diff --git a/tetr_cli/starter.py b/tetr_cli/starter.py index 8f5ff56..a1e35a8 100644 --- a/tetr_cli/starter.py +++ b/tetr_cli/starter.py @@ -32,6 +32,7 @@ "--curses, --ncurses, --c": "Enable ncurses mode for terminal-based UI.", "--no-music, --nm": "Disable music playback during the game.", "--reset-db, --reset-database, --r": "Reset the game database to default settings.", + "--input-test, --it": "Run the input test mode to check key inputs.", } @@ -62,7 +63,6 @@ def starter() -> None: no_music_mode: bool = parse_flag(["--no-music", "-nm"]) reset_database: bool = parse_flag(["--reset-db", "--reset-database", "-r"]) print_help_call: bool = parse_flag(["--help", "-h"]) - input_test: bool = parse_flag(["--input-test", "-it"]) if print_help_call: diff --git a/tetr_cli/tetr_modules/menu_core/base_mode.py b/tetr_cli/tetr_modules/menu_core/base_mode.py index 1d50de1..191caf2 100644 --- a/tetr_cli/tetr_modules/menu_core/base_mode.py +++ b/tetr_cli/tetr_modules/menu_core/base_mode.py @@ -12,9 +12,7 @@ class BaseModeClass: def __init__(self) -> None: """Initialize the base mode class.""" - self.__fps: int = int(get_setting("FPS_limit")) - if self.__fps == 0: - self.__fps = 30 + self.__fps: int = int(get_setting("fps_limit", "30")) self.__action: Dict[str, List[str]] = {} self.__sound_action: Dict[str, List[str]] = {"BGM": ["stop"], "SFX": []} self.__user_keybinds: Dict[str, Dict[str, Set[str]]] = load_keybinds() diff --git a/tetr_cli/tetr_modules/menu_core/menu_mode.py b/tetr_cli/tetr_modules/menu_core/menu_mode.py index ffa4501..1885755 100644 --- a/tetr_cli/tetr_modules/menu_core/menu_mode.py +++ b/tetr_cli/tetr_modules/menu_core/menu_mode.py @@ -11,6 +11,8 @@ safe_addstr, ) +from tetr_cli.tetr_modules.modules.database import get_setting, set_setting + class VerticalMenuModeClass(BaseModeClass): """This class holds the basic features for the menu mode.""" @@ -25,6 +27,11 @@ def __init__( self.__options: List[str] = option_list self.__option_to_action: Dict[str, Dict[str, str]] = option_to_action + @property + def selected_option(self) -> int: + """This will return the selected option.""" + return self.__selected_option + def menu_control(self, pressed_keys: Set) -> None: """This will handle the menu controls.""" if self.__key_cooldown > 0: @@ -72,84 +79,98 @@ def display_menu(self, stdscr, title: str) -> None: ) -class TwoDimmMenuModeClass(BaseModeClass): - """This class holds the basic features for the 2D menu mode.""" +class SideMenuToggleClass(BaseModeClass): + """This class will handle option to increase/decrease values.""" def __init__( - self, option_list: List[List[str]], option_to_action: Dict[str, Dict[str, str]] + self, + toggle_name: str, + toggle_to_action: Dict[str, Dict[str, str]], + lower: int, + upper: int, + step: int = 1, ) -> None: """This will initialize this class.""" super().__init__() - self.__selected_option: List[int] = [0, 0] self.__key_cooldown: int = 0 - self.__options: List[List[str]] = option_list - self.__option_to_action: Dict[str, Dict[str, str]] = option_to_action + self.__toggle_name: str = toggle_name + self.__toggle_to_action: Dict[str, Dict[str, str]] = toggle_to_action + self.__lower: int = lower + self.__upper: int = upper + self.__step: int = step + self.__current_value: int = int( + get_setting(self.__toggle_name, default_value=str(lower)) + ) + self.__old_value: int = self.__current_value + + @property + def current_value(self) -> int: + """This will return the current value.""" + return self.__current_value + + @property + def old_value(self) -> int: + """This will return the old value.""" + return self.__old_value def menu_control(self, pressed_keys: Set) -> None: """This will handle the menu controls.""" if self.__key_cooldown > 0: self.__key_cooldown -= 1 - elif self.get_user_keybind("menu_up", menu_mode=True) & pressed_keys: - self.__selected_option[1] = max(0, self.__selected_option[1] - 1) - self.__key_cooldown = 3 - self.sound_action["SFX"].append("select_move") - elif self.get_user_keybind("menu_down", menu_mode=True) & pressed_keys: - self.__selected_option[1] = min( - len(self.__options[0]) - 1, self.__selected_option[1] + 1 - ) - self.__key_cooldown = 3 - self.sound_action["SFX"].append("select_move") elif self.get_user_keybind("menu_left", menu_mode=True) & pressed_keys: - self.__selected_option[0] = max(0, self.__selected_option[0] - 1) - self.__key_cooldown = 3 self.sound_action["SFX"].append("select_move") - elif self.get_user_keybind("menu_right", menu_mode=True) & pressed_keys: - self.__selected_option[0] = min( - len(self.__options) - 1, self.__selected_option[0] + 1 - ) + self.__current_value = max(self.__lower, self.__current_value - self.__step) self.__key_cooldown = 3 + elif self.get_user_keybind("menu_right", menu_mode=True) & pressed_keys: self.sound_action["SFX"].append("select_move") + self.__current_value = min(self.__upper, self.__current_value + self.__step) + self.__key_cooldown = 3 elif self.get_user_keybind("menu_confirm", menu_mode=True) & pressed_keys: - transition_name: str = self.__options[ - self.__selected_option[0] - ][self.__selected_option[1]] - self.action["transition"] = [ - self.__option_to_action[transition_name]["action"] - ] - self.sound_action["SFX"].append( - self.__option_to_action[transition_name]["sound"] - ) + self.sound_action["SFX"].append("select_confirm") + set_setting(self.__toggle_name, str(self.__current_value)) + self.action["transition"] = [self.__toggle_to_action["Confirm"]["action"]] + self.sound_action["SFX"].append(self.__toggle_to_action["Confirm"]["sound"]) elif self.get_user_keybind("menu_back", menu_mode=True) & pressed_keys: - self.action["transition"] = [self.__option_to_action["Go_Back"]["action"]] - self.sound_action["SFX"].append(self.__option_to_action["Go_Back"]["sound"]) + self.action["transition"] = [self.__toggle_to_action["Go_Back"]["action"]] + self.sound_action["SFX"].append(self.__toggle_to_action["Go_Back"]["sound"]) - def display_menu(self, stdscr, title: str) -> None: - """This will display the menu.""" + def display_toggle(self, stdscr, title: str) -> None: + """This will display the toggle.""" start_y, start_x, width = calculate_centered_menu( - stdscr, [item for sublist in self.__options for item in sublist] + stdscr, [""] ) safe_addstr(stdscr, start_y - 2, (width - len(title)) // 2, title, A_BOLD) - for row_index, row in enumerate(self.__options): - for col_index, option in enumerate(row): - prefix: str = " " - attr: int = 0 - if ( - row_index == self.__selected_option[0] - and col_index == self.__selected_option[1] - ): - prefix = "> " - attr = A_REVERSE - option_x = start_x + col_index * (len(option) + 4) - option_y = start_y + row_index - safe_addstr( - stdscr, - option_y, - option_x, - f"{prefix}{option}", - attr, - ) + bar_width: int = 20 + filled_length = int( + (self.__current_value - self.__lower) + / (self.__upper - self.__lower) + * bar_width + ) + bar: str = ( + self.__toggle_name + + "[" + + "#" * filled_length + + "-" * (bar_width - filled_length) + + "]" + ) + safe_addstr( + stdscr, + start_y, + start_x - (len(bar) // 2), + bar, + A_REVERSE, + ) + + value_str = str(self.__current_value).rjust(3) + safe_addstr( + stdscr, + start_y + 1, + start_x - len(value_str) // 2, + value_str, + A_BOLD, + ) if __name__ == "__main__": diff --git a/tetr_cli/tetr_modules/modes/options/audio_options/__init__.py b/tetr_cli/tetr_modules/modes/options/audio_options/__init__.py new file mode 100644 index 0000000..3c5b616 --- /dev/null +++ b/tetr_cli/tetr_modules/modes/options/audio_options/__init__.py @@ -0,0 +1,4 @@ +"""This is an init file for modes.audio_options.""" + +if __name__ == "__main__": + print("This is an init file for the modes.audio_options, please run starter.py.") diff --git a/tetr_cli/tetr_modules/modes/options/audio_options/audio_options_mode.py b/tetr_cli/tetr_modules/modes/options/audio_options/audio_options_mode.py new file mode 100644 index 0000000..532313b --- /dev/null +++ b/tetr_cli/tetr_modules/modes/options/audio_options/audio_options_mode.py @@ -0,0 +1,37 @@ +"""This will handle the audio options menu.""" +# coding: utf-8 + +from typing import Dict, List, Set + +from curses import window + +from tetr_cli.tetr_modules.menu_core.menu_mode import VerticalMenuModeClass + +AUDIO_OPTION_TO_ACTION: Dict[str, Dict[str, str]] = { + "BGM": {"action": "BGM_Option", "sound": "select_confirm"}, + "SFX": {"action": "SFX_Option", "sound": "select_confirm"}, + "Go_Back": {"action": "Main_Menu", "sound": "select_back"}, +} + +AUDIO_OPTION_LIST: List[str] = [ + "BGM", + "SFX", + "Go_Back", +] + + +class ModeClass(VerticalMenuModeClass): + """This will handle the option mode menu.""" + + def __init__(self) -> None: + """This will initialize this class.""" + super().__init__(AUDIO_OPTION_LIST, AUDIO_OPTION_TO_ACTION) + + def increment_frame(self, stdscr: window, pressed_keys: Set[str]) -> None: + """This will progress the menu based on the inputs.""" + self.menu_control(pressed_keys) + self.display_menu(stdscr, "Audio Options") + + +if __name__ == "__main__": + print("This is a audio_options module, please run starter.py.") diff --git a/tetr_cli/tetr_modules/modes/options/audio_options/bgm_option_mode.py b/tetr_cli/tetr_modules/modes/options/audio_options/bgm_option_mode.py new file mode 100644 index 0000000..7c9ae6d --- /dev/null +++ b/tetr_cli/tetr_modules/modes/options/audio_options/bgm_option_mode.py @@ -0,0 +1,47 @@ +"""This will handle the BGM option menu.""" + +from typing import Dict, Set +from curses import window + +from tetr_cli.tetr_modules.menu_core.menu_mode import SideMenuToggleClass +from tetr_cli.tetr_modules.modules.database import set_setting + +TOGGLE_TO_ACTION: Dict[str, Dict[str, str]] = { + "Confirm": {"action": "Audio_Options", "sound": "select_confirm"}, + "Go_Back": {"action": "Audio_Options", "sound": "select_back"}, +} + + +class ModeClass(SideMenuToggleClass): + """This will handle the option mode menu.""" + + def __init__(self) -> None: + """This will initialize this class.""" + super().__init__( + toggle_name="music_volume", + toggle_to_action=TOGGLE_TO_ACTION, + lower=0, + upper=100, + step=5 + ) + self.sound_action["BGM"] = ["korobeiniki"] + + def increment_frame(self, stdscr: window, pressed_keys: Set[str]) -> None: + """This will progress the menu based on the inputs.""" + self.menu_control(pressed_keys) + self.display_toggle(stdscr, "BGM Volume") + if ( + self.get_user_keybind("menu_left", menu_mode=True) & pressed_keys + or self.get_user_keybind("menu_right", menu_mode=True) & pressed_keys + or self.get_user_keybind("menu_confirm", menu_mode=True) & pressed_keys + or self.get_user_keybind("menu_back", menu_mode=True) & pressed_keys + ): + self.action["update_volume"] = [] + if self.get_user_keybind("menu_back", menu_mode=True) & pressed_keys: + set_setting("music_volume", str(self.old_value)) + return + set_setting("music_volume", str(self.current_value)) + + +if __name__ == "__main__": + print("This is a bgm_option module, please run starter.py.") diff --git a/tetr_cli/tetr_modules/modes/options/audio_options/sfx_option_mode.py b/tetr_cli/tetr_modules/modes/options/audio_options/sfx_option_mode.py new file mode 100644 index 0000000..90762f7 --- /dev/null +++ b/tetr_cli/tetr_modules/modes/options/audio_options/sfx_option_mode.py @@ -0,0 +1,46 @@ +"""This will handle the BGM option menu.""" + +from typing import Dict, Set +from curses import window + +from tetr_cli.tetr_modules.menu_core.menu_mode import SideMenuToggleClass +from tetr_cli.tetr_modules.modules.database import set_setting + +TOGGLE_TO_ACTION: Dict[str, Dict[str, str]] = { + "Confirm": {"action": "Audio_Options", "sound": "select_confirm"}, + "Go_Back": {"action": "Audio_Options", "sound": "select_back"}, +} + + +class ModeClass(SideMenuToggleClass): + """This will handle the option mode menu.""" + + def __init__(self) -> None: + """This will initialize this class.""" + super().__init__( + toggle_name="sfx_volume", + toggle_to_action=TOGGLE_TO_ACTION, + lower=0, + upper=100, + step=5 + ) + + def increment_frame(self, stdscr: window, pressed_keys: Set[str]) -> None: + """This will progress the menu based on the inputs.""" + self.menu_control(pressed_keys) + self.display_toggle(stdscr, "SFX Volume") + if ( + self.get_user_keybind("menu_left", menu_mode=True) & pressed_keys + or self.get_user_keybind("menu_right", menu_mode=True) & pressed_keys + or self.get_user_keybind("menu_confirm", menu_mode=True) & pressed_keys + or self.get_user_keybind("menu_back", menu_mode=True) & pressed_keys + ): + self.action["update_volume"] = [] + if self.get_user_keybind("menu_back", menu_mode=True) & pressed_keys: + set_setting("sfx_volume", str(self.old_value)) + return + set_setting("sfx_volume", str(self.current_value)) + + +if __name__ == "__main__": + print("This is a sfx_option module, please run starter.py.") diff --git a/tetr_cli/tetr_modules/modes/options/control_options/__init__.py b/tetr_cli/tetr_modules/modes/options/control_options/__init__.py new file mode 100644 index 0000000..d64d3f6 --- /dev/null +++ b/tetr_cli/tetr_modules/modes/options/control_options/__init__.py @@ -0,0 +1,4 @@ +"""This is an init file for control_options.""" + +if __name__ == "__main__": + print("This is an init file for the control_options, please run starter.py.") diff --git a/tetr_cli/tetr_modules/modes/options/control_options/change_keybind_mode.py b/tetr_cli/tetr_modules/modes/options/control_options/change_keybind_mode.py new file mode 100644 index 0000000..fec687b --- /dev/null +++ b/tetr_cli/tetr_modules/modes/options/control_options/change_keybind_mode.py @@ -0,0 +1,115 @@ +"""This will handle the Keybind Change mode.""" + +from typing import Dict, Set, List, Optional +from curses import window + +from tetr_cli.tetr_modules.menu_core.base_mode import BaseModeClass +from tetr_cli.tetr_modules.modules.safe_curses import ( + calculate_centered_menu, + safe_addstr, +) + +from tetr_cli.tetr_modules.modules.database import get_temp, update_keybind + + +OPTION_TO_ACTION: Dict[str, Dict[str, str]] = { + "Go_Back": {"action": "Control_Options", "sound": "select_back"}, + "select_confirm": {"action": "Control_Options", "sound": "select_confirm"}, +} + + +class ModeClass(BaseModeClass): + """This will handle the Keybind Change mode.""" + + def __init__(self) -> None: + """This will initialize this class.""" + super().__init__() + self.keybind_name: str = get_temp("rebind_control") + self.key_counter: int = 0 + self.new_keybinds: List[Optional[str]] = [] + + def increment_frame(self, stdscr: window, pressed_keys: Set[str]) -> None: + """This will progress the menu based on the inputs.""" + start_y, start_x, width = calculate_centered_menu(stdscr, ["temp"]) + statement: str = "" + statement2: str = "" + if self.key_counter == 0: + statement = ( + f"Press a key to bind to '{self.keybind_name}' or 'Esc' to cancel." + ) + elif self.key_counter == 1: + statement = ( + f"Press a second key to bind to '{self.keybind_name}' or 'Esc' to pass." + ) + statement2 = ( + f"First Key: '{self.new_keybinds[0]}'." + ) + else: + success: bool = update_keybind( + self.keybind_name, + self.new_keybinds[0], # type: ignore + self.new_keybinds[1] if len(self.new_keybinds) > 1 else None, + ) + keys: str = f"{self.new_keybinds[0]}" + if len(self.new_keybinds) == 2 and self.new_keybinds[1] is not None: + keys = f"{self.new_keybinds[0]}' and '{self.new_keybinds[1]}" + if not success: + statement = ( + f"Failed to bind '{self.keybind_name}' " + + f"to '{keys}'." + ) + else: + statement = ( + f"Successfully bound '{self.keybind_name}' " + + f"to '{keys}'." + ) + statement2 = "Press any to go back." + safe_addstr( + stdscr, + start_y, + (width - len(statement)) // 2, + statement, + ) + if statement2: + safe_addstr( + stdscr, + start_y + 1, + (width - len(statement2)) // 2, + statement2, + ) + if not pressed_keys: + return + self.action["clear"] = [] + new_key: str = pressed_keys.pop() + if new_key == "esc": + if self.key_counter == 0: + self.action["transition"] = [OPTION_TO_ACTION["Go_Back"]["action"]] + self.sound_action["SFX"].append(OPTION_TO_ACTION["Go_Back"]["sound"]) + return + if self.key_counter == 2: + self.action["transition"] = [ + OPTION_TO_ACTION["select_confirm"]["action"] + ] + self.sound_action["SFX"].append( + OPTION_TO_ACTION["select_confirm"]["sound"] + ) + return + self.new_keybinds.append(None) + self.key_counter += 1 + self.sound_action["SFX"].append(OPTION_TO_ACTION["select_confirm"]["sound"]) + return + elif self.key_counter < 2: + self.new_keybinds.append(new_key) + self.key_counter += 1 + self.sound_action["SFX"].append(OPTION_TO_ACTION["select_confirm"]["sound"]) + return + if self.key_counter >= 2: + self.action["transition"] = [ + OPTION_TO_ACTION["select_confirm"]["action"] + ] + self.sound_action["SFX"].append(OPTION_TO_ACTION["select_confirm"]["sound"]) + return + + +if __name__ == "__main__": + print("This is a change_keybind_mode module, please run starter.py.") diff --git a/tetr_cli/tetr_modules/modes/options/control_options/control_options_mode.py b/tetr_cli/tetr_modules/modes/options/control_options/control_options_mode.py new file mode 100644 index 0000000..6761070 --- /dev/null +++ b/tetr_cli/tetr_modules/modes/options/control_options/control_options_mode.py @@ -0,0 +1,47 @@ +"""This is the control option mode.""" + +# coding: utf-8 + +from typing import Dict, Set + +from curses import window + +from tetr_cli.tetr_modules.menu_core.menu_mode import VerticalMenuModeClass + +from tetr_cli.tetr_modules.modules.constants import CONTROLS_LIST +from tetr_cli.tetr_modules.modules.database import set_temp + + +OPTION_TO_ACTION: Dict[str, Dict[str, str]] = {} + +for control_option in CONTROLS_LIST: + OPTION_TO_ACTION[control_option] = { + "action": "Change_Keybind", + "sound": "select_confirm", + } + +OPTION_TO_ACTION["Go_Back"] = { + "action": "Option_Menu", + "sound": "select_back", +} + + +class ModeClass(VerticalMenuModeClass): + """This will handle the control option mode menu.""" + + def __init__(self) -> None: + """This will initialize this class.""" + self.control_options = list(OPTION_TO_ACTION.keys()) + super().__init__(self.control_options, OPTION_TO_ACTION) + + def increment_frame(self, stdscr: window, pressed_keys: Set[str]) -> None: + """This will progress the menu based on the inputs.""" + self.menu_control(pressed_keys) + self.display_menu(stdscr, "Control Options") + if self.get_user_keybind("menu_confirm", menu_mode=True) & pressed_keys: + selected_option: str = self.control_options[self.selected_option] + set_temp("rebind_control", selected_option) + + +if __name__ == "__main__": + print("This is a control_options module, please run starter.py.") diff --git a/tetr_cli/tetr_modules/modes/options/gameplay_option_mode.py b/tetr_cli/tetr_modules/modes/options/gameplay_option_mode.py deleted file mode 100644 index 792b951..0000000 --- a/tetr_cli/tetr_modules/modes/options/gameplay_option_mode.py +++ /dev/null @@ -1,15 +0,0 @@ -"""This is the gameplay option mode.""" -# coding: utf-8 - -# from typing import Dict, List, Set - -# from curses import window - -# from tetr_cli.tetr_modules.menu_core.menu_mode import TwoDimmMenuModeClass - - -# OPTION_LIST: List[str] = [ -# "FPS_Limit", -# "Ghost Piece", -# "Go_Back", -# ] diff --git a/tetr_cli/tetr_modules/modes/options/gameplay_options/__init__.py b/tetr_cli/tetr_modules/modes/options/gameplay_options/__init__.py new file mode 100644 index 0000000..9e19159 --- /dev/null +++ b/tetr_cli/tetr_modules/modes/options/gameplay_options/__init__.py @@ -0,0 +1,4 @@ +"""This is an init file for modes.options.gameplay_options.""" + +if __name__ == "__main__": + print("This is an init file for the modes.options.gameplay_options, please run starter.py.") diff --git a/tetr_cli/tetr_modules/modes/options/gameplay_options/arr_option_mode.py b/tetr_cli/tetr_modules/modes/options/gameplay_options/arr_option_mode.py new file mode 100644 index 0000000..3c53f2c --- /dev/null +++ b/tetr_cli/tetr_modules/modes/options/gameplay_options/arr_option_mode.py @@ -0,0 +1,38 @@ +"""This will handle the ARR option mode menu.""" + +from typing import Dict, Set + +from curses import window + +from tetr_cli.tetr_modules.menu_core.menu_mode import SideMenuToggleClass +from tetr_cli.tetr_modules.modules.database import set_setting + +TOGGLE_TO_ACTION: Dict[str, Dict[str, str]] = { + "Confirm": {"action": "Gameplay_Options", "sound": "select_confirm"}, + "Go_Back": {"action": "Gameplay_Options", "sound": "select_back"}, +} + + +class ModeClass(SideMenuToggleClass): + """This will handle the ARR option mode menu.""" + + def __init__(self) -> None: + """This will initialize this class.""" + super().__init__( + toggle_name="arr", + toggle_to_action=TOGGLE_TO_ACTION, + lower=0, + upper=15, + step=1 + ) + + def increment_frame(self, stdscr: window, pressed_keys: Set[str]) -> None: + """This will progress the menu based on the inputs.""" + self.menu_control(pressed_keys) + self.display_toggle(stdscr, "ARR Options (Frames)") + if self.get_user_keybind("menu_confirm", menu_mode=True) & pressed_keys: + set_setting("arr", str(self.current_value)) + + +if __name__ == "__main__": + print("This is a arr_option module, please run starter.py.") diff --git a/tetr_cli/tetr_modules/modes/options/gameplay_options/das_option_mode.py b/tetr_cli/tetr_modules/modes/options/gameplay_options/das_option_mode.py new file mode 100644 index 0000000..f0709a8 --- /dev/null +++ b/tetr_cli/tetr_modules/modes/options/gameplay_options/das_option_mode.py @@ -0,0 +1,38 @@ +"""This will handle the DAS option mode menu.""" + +from typing import Dict, Set + +from curses import window + +from tetr_cli.tetr_modules.menu_core.menu_mode import SideMenuToggleClass +from tetr_cli.tetr_modules.modules.database import set_setting + +TOGGLE_TO_ACTION: Dict[str, Dict[str, str]] = { + "Confirm": {"action": "Gameplay_Options", "sound": "select_confirm"}, + "Go_Back": {"action": "Gameplay_Options", "sound": "select_back"}, +} + + +class ModeClass(SideMenuToggleClass): + """This will handle the DAS option mode menu.""" + + def __init__(self) -> None: + """This will initialize this class.""" + super().__init__( + toggle_name="das", + toggle_to_action=TOGGLE_TO_ACTION, + lower=0, + upper=20, + step=1 + ) + + def increment_frame(self, stdscr: window, pressed_keys: Set[str]) -> None: + """This will progress the menu based on the inputs.""" + self.menu_control(pressed_keys) + self.display_toggle(stdscr, "DAS Options (Frames)") + if self.get_user_keybind("menu_confirm", menu_mode=True) & pressed_keys: + set_setting("das", str(self.current_value)) + + +if __name__ == "__main__": + print("This is a das_option module, please run starter.py.") diff --git a/tetr_cli/tetr_modules/modes/options/gameplay_options/gameplay_options_mode.py b/tetr_cli/tetr_modules/modes/options/gameplay_options/gameplay_options_mode.py new file mode 100644 index 0000000..6fb55ca --- /dev/null +++ b/tetr_cli/tetr_modules/modes/options/gameplay_options/gameplay_options_mode.py @@ -0,0 +1,42 @@ +"""This is the gameplay option mode.""" + +# coding: utf-8 + +from typing import Dict, List, Set + +from curses import window + +from tetr_cli.tetr_modules.menu_core.menu_mode import VerticalMenuModeClass + + +GAMEPLAY_OPTION_TO_ACTION: Dict[str, Dict[str, str]] = { + "Delayed Auto Shift (DAS)": {"action": "DAS_Option", "sound": "select_confirm"}, + "Auto Repeat Rate (ARR)": {"action": "ARR_Option", "sound": "select_confirm"}, + "Ghost Piece": {"action": "Ghost_Piece_Option", "sound": "select_confirm"}, + "Go_Back": {"action": "Main_Menu", "sound": "select_back"}, +} + + +GAMEPLAY_OPTION_LIST: List[str] = [ + "Delayed Auto Shift (DAS)", + "Auto Repeat Rate (ARR)", + "Ghost Piece", + "Go_Back", +] + + +class ModeClass(VerticalMenuModeClass): + """This will handle the gameplay option mode menu.""" + + def __init__(self) -> None: + """This will initialize this class.""" + super().__init__(GAMEPLAY_OPTION_LIST, GAMEPLAY_OPTION_TO_ACTION) + + def increment_frame(self, stdscr: window, pressed_keys: Set[str]) -> None: + """This will progress the menu based on the inputs.""" + self.menu_control(pressed_keys) + self.display_menu(stdscr, "Gameplay Options") + + +if __name__ == "__main__": + print("This is a gameplay_options module, please run starter.py.") diff --git a/tetr_cli/tetr_modules/modes/options/gameplay_options/ghost_piece_option_mode.py b/tetr_cli/tetr_modules/modes/options/gameplay_options/ghost_piece_option_mode.py new file mode 100644 index 0000000..73bd345 --- /dev/null +++ b/tetr_cli/tetr_modules/modes/options/gameplay_options/ghost_piece_option_mode.py @@ -0,0 +1,39 @@ +"""This will handle the Ghost Piece option mode menu.""" + +from typing import Dict, List, Set +from curses import window + +from tetr_cli.tetr_modules.menu_core.menu_mode import VerticalMenuModeClass +from tetr_cli.tetr_modules.modules.database import set_setting + +OPTION_TO_ACTION: Dict[str, Dict[str, str]] = { + "Visible": {"action": "Gameplay_Options", "sound": "select_confirm"}, + "Hidden": {"action": "Gameplay_Options", "sound": "select_confirm"}, + "Go_Back": {"action": "Gameplay_Options", "sound": "select_back"}, +} + +OPTION_LIST: List[str] = [ + "Visible", + "Hidden", +] + + +class ModeClass(VerticalMenuModeClass): + """This will handle the Ghost Piece option mode menu.""" + + def __init__(self) -> None: + """This will initialize this class.""" + super().__init__(OPTION_LIST, OPTION_TO_ACTION) + + def increment_frame(self, stdscr: window, pressed_keys: Set[str]) -> None: + """This will progress the menu based on the inputs.""" + self.menu_control(pressed_keys) + self.display_menu(stdscr, "Ghost Piece Options") + if self.get_user_keybind("menu_confirm", menu_mode=True) & pressed_keys: + selected_option: str = OPTION_LIST[self.selected_option] + ghost_piece_value: str = "true" if selected_option == "Visible" else "false" + set_setting("ghost_piece", ghost_piece_value) + + +if __name__ == "__main__": + print("This is a ghost_piece_option module, please run starter.py.") diff --git a/tetr_cli/tetr_modules/modes/options/graphic_options/__init__.py b/tetr_cli/tetr_modules/modes/options/graphic_options/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tetr_cli/tetr_modules/modes/options/graphic_options/color_option_mode.py b/tetr_cli/tetr_modules/modes/options/graphic_options/color_option_mode.py new file mode 100644 index 0000000..b5ab40c --- /dev/null +++ b/tetr_cli/tetr_modules/modes/options/graphic_options/color_option_mode.py @@ -0,0 +1,41 @@ +"""This is the color option mode.""" + +from typing import Dict, List, Set +from curses import window + +from tetr_cli.tetr_modules.menu_core.menu_mode import VerticalMenuModeClass + +from tetr_cli.tetr_modules.modules.database import set_setting + +COLOR_OPTION_TO_ACTION: Dict[str, Dict[str, str]] = { + "True": {"action": "Graphic_Options", "sound": "select_confirm"}, + "False": {"action": "Graphic_Options", "sound": "select_confirm"}, + "Go_Back": {"action": "Graphic_Options", "sound": "select_back"}, +} + + +OPTION_LIST: List[str] = [ + "True", + "False", +] + + +class ModeClass(VerticalMenuModeClass): + """This will handle the color option mode menu.""" + + def __init__(self) -> None: + """This will initialize this class.""" + super().__init__(OPTION_LIST, COLOR_OPTION_TO_ACTION) + + def increment_frame(self, stdscr: window, pressed_keys: Set[str]) -> None: + """This will progress the menu based on the inputs.""" + self.menu_control(pressed_keys) + self.display_menu(stdscr, "Color Options") + if self.get_user_keybind("menu_confirm", menu_mode=True) & pressed_keys: + selected_option: str = OPTION_LIST[self.selected_option] + color_value: str = "true" if selected_option == "True" else "false" + set_setting("color_mode", color_value) + + +if __name__ == "__main__": + print("This is a color_option_mode module, please run starter.py.") diff --git a/tetr_cli/tetr_modules/modes/options/graphic_options/fps_option_mode.py b/tetr_cli/tetr_modules/modes/options/graphic_options/fps_option_mode.py new file mode 100644 index 0000000..855f616 --- /dev/null +++ b/tetr_cli/tetr_modules/modes/options/graphic_options/fps_option_mode.py @@ -0,0 +1,48 @@ +"""This will handle the frame option mode menu.""" + +from typing import Dict, Set +from curses import window + +from tetr_cli.tetr_modules.menu_core.menu_mode import SideMenuToggleClass +from tetr_cli.tetr_modules.modules.database import set_setting + + +TOGGLE_TO_ACTION: Dict[str, Dict[str, str]] = { + "Confirm": {"action": "Graphic_Options", "sound": "select_confirm"}, + "Go_Back": {"action": "Graphic_Options", "sound": "select_back"}, +} + + +class ModeClass(SideMenuToggleClass): + """This will handle the fps option mode menu.""" + + def __init__(self) -> None: + """This will initialize this class.""" + super().__init__( + toggle_name="frame_rate", + toggle_to_action=TOGGLE_TO_ACTION, + lower=15, + upper=60, + step=15 + ) + + def increment_frame(self, stdscr: window, pressed_keys: Set[str]) -> None: + """This will progress the menu based on the inputs.""" + self.menu_control(pressed_keys) + self.display_toggle(stdscr, "Frame Rate Limit") + if ( + self.get_user_keybind("menu_left", menu_mode=True) & pressed_keys + or self.get_user_keybind("menu_right", menu_mode=True) & pressed_keys + or self.get_user_keybind("menu_confirm", menu_mode=True) & pressed_keys + or self.get_user_keybind("menu_back", menu_mode=True) & pressed_keys + ): + if self.get_user_keybind("menu_back", menu_mode=True) & pressed_keys: + set_setting("fps_limit", str(self.old_value)) + return + if self.get_user_keybind("menu_confirm", menu_mode=True) & pressed_keys: + set_setting("fps_limit", str(self.current_value)) + self.action["update_fps"] = [] + + +if __name__ == "__main__": + print("This is a fps_option module, please run starter.py.") diff --git a/tetr_cli/tetr_modules/modes/options/graphic_options/graphic_options_mode.py b/tetr_cli/tetr_modules/modes/options/graphic_options/graphic_options_mode.py new file mode 100644 index 0000000..d4f4d84 --- /dev/null +++ b/tetr_cli/tetr_modules/modes/options/graphic_options/graphic_options_mode.py @@ -0,0 +1,42 @@ +"""This is the gameplay option mode.""" + +# coding: utf-8 + +from typing import Dict, List, Set + +from curses import window + +from tetr_cli.tetr_modules.menu_core.menu_mode import VerticalMenuModeClass + + +GAMEPLAY_OPTION_TO_ACTION: Dict[str, Dict[str, str]] = { + "Color Option": {"action": "Color_Option", "sound": "select_confirm"}, + "Mino Design": {"action": "Mino_Style_Option", "sound": "select_confirm"}, + "Frame Rate": {"action": "FPS_Option", "sound": "select_confirm"}, + "Go_Back": {"action": "Main_Menu", "sound": "select_back"}, +} + + +GAMEPLAY_OPTION_LIST: List[str] = [ + "Color Option", + "Mino Design", + "Frame Rate", + "Go_Back", +] + + +class ModeClass(VerticalMenuModeClass): + """This will handle the gameplay option mode menu.""" + + def __init__(self) -> None: + """This will initialize this class.""" + super().__init__(GAMEPLAY_OPTION_LIST, GAMEPLAY_OPTION_TO_ACTION) + + def increment_frame(self, stdscr: window, pressed_keys: Set[str]) -> None: + """This will progress the menu based on the inputs.""" + self.menu_control(pressed_keys) + self.display_menu(stdscr, "Gameplay Options") + + +if __name__ == "__main__": + print("This is a gameplay_options module, please run starter.py.") diff --git a/tetr_cli/tetr_modules/modes/options/graphic_options/mino_design_mode.py b/tetr_cli/tetr_modules/modes/options/graphic_options/mino_design_mode.py new file mode 100644 index 0000000..5963c4f --- /dev/null +++ b/tetr_cli/tetr_modules/modes/options/graphic_options/mino_design_mode.py @@ -0,0 +1,39 @@ +"""This will handle the Mino Style option mode menu.""" + +from typing import Dict, Set +from curses import window + +from tetr_cli.tetr_modules.menu_core.menu_mode import VerticalMenuModeClass + +from tetr_cli.tetr_modules.modules.constants import MINO_TO_GHOST +from tetr_cli.tetr_modules.modules.database import set_setting + +OPTION_TO_ACTION: Dict[str, Dict[str, str]] = { + "Go_Back": {"action": "Gameplay_Options", "sound": "select_back"}, +} + +for mino_style in MINO_TO_GHOST.keys(): + OPTION_TO_ACTION[mino_style] = { + "action": "Gameplay_Options", + "sound": "select_confirm", + } + + +class ModeClass(VerticalMenuModeClass): + """This will handle the Mino Style option mode menu.""" + + def __init__(self) -> None: + """This will initialize this class.""" + super().__init__(list(MINO_TO_GHOST.keys()), OPTION_TO_ACTION) + + def increment_frame(self, stdscr: window, pressed_keys: Set[str]) -> None: + """This will progress the menu based on the inputs.""" + self.menu_control(pressed_keys) + self.display_menu(stdscr, "Mino Style Options") + if self.get_user_keybind("menu_confirm", menu_mode=True) & pressed_keys: + selected_option: str = list(MINO_TO_GHOST.keys())[self.selected_option] + set_setting("mino_style", selected_option) + + +if __name__ == "__main__": + print("This is a mino_style_option module, please run starter.py.") diff --git a/tetr_cli/tetr_modules/modes/options/option_mode.py b/tetr_cli/tetr_modules/modes/options/option_mode.py index 9d2fb17..66e7595 100644 --- a/tetr_cli/tetr_modules/modes/options/option_mode.py +++ b/tetr_cli/tetr_modules/modes/options/option_mode.py @@ -8,7 +8,7 @@ from tetr_cli.tetr_modules.menu_core.menu_mode import VerticalMenuModeClass OPTION_TO_ACTION: Dict[str, Dict[str, str]] = { - "Graphics": {"action": "Graphics_Options", "sound": "select_confirm"}, + "Graphics": {"action": "Graphic_Options", "sound": "select_confirm"}, "Audio": {"action": "Audio_Options", "sound": "select_confirm"}, "Controls": {"action": "Control_Options", "sound": "select_confirm"}, "Gameplay": {"action": "Gameplay_Options", "sound": "select_confirm"}, diff --git a/tetr_cli/tetr_modules/modes/score_screen_mode.py b/tetr_cli/tetr_modules/modes/score_screen_mode.py index de2b5de..bda91a9 100644 --- a/tetr_cli/tetr_modules/modes/score_screen_mode.py +++ b/tetr_cli/tetr_modules/modes/score_screen_mode.py @@ -45,16 +45,33 @@ def get_high_score(self) -> None: high_score_list: List[Tuple[str, int, str, str]] = get_scores(self.score_type) if self.score_type != "Sprint": high_score_list.sort(key=lambda x: x[1], reverse=True) + else: + high_score_list.sort(key=lambda x: x[1]) self.score_list = [ (player, score, date) for player, score, _, date in high_score_list[:5] ] + def frames_to_time(self, frames: int) -> str: + """Convert frames to a time string (MM:SS.mmm) given the FPS.""" + total_seconds = frames / 60 # Base is set at 60 FPS + minutes = int(total_seconds // 60) + seconds = int(total_seconds % 60) + milliseconds = int((total_seconds % 1) * 1000) + return f"{minutes:02}:{seconds:02}.{milliseconds:03}" + def initialize_score_list(self) -> bool: """This will initialize the mode.""" self.get_high_score() inserted = False + if self.score == -1: + return inserted for list_index, (_, score, _) in enumerate(self.score_list): - if self.score >= score: + if ( + self.score >= score + and self.score_type != "Sprint" + or self.score <= score + and self.score_type == "Sprint" + ): self.score_list.insert( list_index, (self.user_name, self.score, datetime.now().strftime("%Y-%m-%d")), @@ -119,9 +136,15 @@ def increment_frame(self, stdscr: window, pressed_keys: Set[str]) -> None: display_score_list: List[str] = [] for list_index, (player, score, date) in enumerate(self.score_list): - display_score_list.append( - f"{list_index + 1:>2}. {player:<12} : {score:>10} at {date}" - ) + if self.score_type == "Sprint": + display_score_list.append( + f"{list_index + 1:>2}. {player:<12} : " + + f"{self.frames_to_time(score):>10} at {date}" + ) + else: + display_score_list.append( + f"{list_index + 1:>2}. {player:<12} : {score:>10} at {date}" + ) start_y, start_x, width = calculate_centered_menu(stdscr, display_score_list) # Display score screen diff --git a/tetr_cli/tetr_modules/modes/solo/marathon_mode.py b/tetr_cli/tetr_modules/modes/solo/marathon_mode.py index a1a910c..f87c4f0 100644 --- a/tetr_cli/tetr_modules/modes/solo/marathon_mode.py +++ b/tetr_cli/tetr_modules/modes/solo/marathon_mode.py @@ -1,4 +1,4 @@ -"""This will handle the solo game mode.""" +"""This will handle the solo marathon game mode.""" # coding: utf-8 @@ -13,7 +13,7 @@ DRAW_BOARD_HEIGHT, DRAW_BOARD_WIDTH, ) -from tetr_cli.tetr_modules.modules.database import set_temp +from tetr_cli.tetr_modules.modules.database import set_temp, get_setting from tetr_cli.tetr_modules.modules.safe_curses import safe_addstr @@ -28,6 +28,8 @@ def __init__(self) -> None: self.mino_list_generator(initial=True) self.counter: int = self.fps_limit * 3 # Formally countdown self.mode: str = "countdown" + self.__das: int = int((int(get_setting("das", "10")) * self.fps_limit) / 60) + self.__arr: int = int((int(get_setting("arr", "2")) * self.fps_limit) / 60) def show_stats(self, stdscr: window) -> None: """This will show the stats on bottom right.""" @@ -56,9 +58,14 @@ def show_stats(self, stdscr: window) -> None: # stdscr, # self.offset[0] + DRAW_BOARD_HEIGHT + 1, # self.offset[1] + DRAW_BOARD_WIDTH + 2, - # f"Combo: {self.combo_count}", + # f"Soft drop: {self.current_mino.soft_drop_counter if self.current_mino else 0:<2}", + # ) + # safe_addstr( + # stdscr, + # self.offset[0] + DRAW_BOARD_HEIGHT + 2, + # self.offset[1] + DRAW_BOARD_WIDTH + 2, + # f"Fall Counter: {self.current_mino.fall_counter if self.current_mino else 0:<2}", # ) - # return def clear_action_text(self, stdscr: window) -> None: """Clears the action text.""" @@ -147,7 +154,11 @@ def play_mode(self, stdscr: window, pressed_keys: Set[str]) -> None: if not self.current_mino: new_mino_type: str = self.mino_list.pop(0) self.current_mino = Mino( - mino_type=new_mino_type, level=self.level, fps_limit=self.fps_limit + mino_type=new_mino_type, + level=self.level, + fps_limit=self.fps_limit, + das=self.__das, + arr=self.__arr, ) if self.check_game_over(): self.mode = "game_over" @@ -155,6 +166,9 @@ def play_mode(self, stdscr: window, pressed_keys: Set[str]) -> None: self.sound_action["BGM"] = ["stop"] return + # Level up for every 10 lines cleared + self.level = max(self.level, (self.lines_cleared // 10) + 1) + self.check_keyinput_pressed(pressed_keys=pressed_keys) if not self.current_mino: self.board.draw_minos_on_board( @@ -174,14 +188,14 @@ def play_mode(self, stdscr: window, pressed_keys: Set[str]) -> None: self.current_mino.lock_info["lock_height"] = self.current_mino.position[ 0 ] - self.current_mino.lock_info["lock_count"] = 15 + self.current_mino.reset_lock_count(level=self.level) elif ( pressed_keys and not pressed_keys & {"down", "space"} and self.current_mino.lock_info["lock_count"] > 0 ): self.current_mino.lock_info["lock_count"] -= 1 - self.current_mino.lock_info["lock_delay"] = int(0.5 * self.fps_limit) + self.current_mino.reset_lock_delay(level=self.level) elif self.current_mino.lock_info["lock_delay"] > 0: self.current_mino.lock_info["lock_delay"] -= 1 else: @@ -194,14 +208,14 @@ def play_mode(self, stdscr: window, pressed_keys: Set[str]) -> None: self.reset_mino() if self.current_mino: - if self.current_mino.fall_delay > 0: - self.current_mino.fall_delay -= 1 + if self.current_mino.fall_counter > 0: + self.current_mino.fall_counter -= 1 elif not self.mino_touching_bottom(self.current_mino) and not ( pressed_keys & {"down", "space"} ): - self.current_mino.move_down(is_position_valid=self.is_position_valid) - self.current_mino.fall_delay = self.current_mino.reset_fall_delay( - self.level + self.current_mino.natural_drop( + mino_touching_bottom_func=self.mino_touching_bottom, + is_position_valid=self.is_position_valid, ) self.board.draw_minos_on_board( diff --git a/tetr_cli/tetr_modules/modes/solo/solo_menu_mode.py b/tetr_cli/tetr_modules/modes/solo/solo_menu_mode.py index 72a71f6..23e78f0 100644 --- a/tetr_cli/tetr_modules/modes/solo/solo_menu_mode.py +++ b/tetr_cli/tetr_modules/modes/solo/solo_menu_mode.py @@ -1,4 +1,5 @@ """ "This will handle the solo mode menu.""" + # coding: utf-8 from typing import Dict, List, Set diff --git a/tetr_cli/tetr_modules/modes/solo/sprint_mode.py b/tetr_cli/tetr_modules/modes/solo/sprint_mode.py new file mode 100644 index 0000000..bc0de3d --- /dev/null +++ b/tetr_cli/tetr_modules/modes/solo/sprint_mode.py @@ -0,0 +1,393 @@ +"""This will handle the solo sprint game mode.""" + +# coding: utf-8 + +from curses import window, A_BOLD +from typing import Optional, List, Set, Tuple + +from tetr_cli.tetr_modules.solo_core.base import SoloBaseMode +from tetr_cli.tetr_modules.solo_core.mino import Mino +from tetr_cli.tetr_modules.modules.constants import ( + MIN_X, + MIN_Y, + DRAW_BOARD_HEIGHT, + DRAW_BOARD_WIDTH, +) +from tetr_cli.tetr_modules.modules.database import set_temp, get_setting +from tetr_cli.tetr_modules.modules.safe_curses import safe_addstr + + +class ModeClass(SoloBaseMode): + """This will handle the solo game mode.""" + + def __init__(self) -> None: + """This will initialize this class.""" + super().__init__(given_level=5) + # For countdown: 3 seconds countdown + # For animation: 0.5 second animation + self.mino_list_generator(initial=True) + self.counter: int = self.fps_limit * 3 # Formally countdown + self.mode: str = "countdown" + self.__das: int = int((int(get_setting("das", "10")) * self.fps_limit) / 60) + self.__arr: int = int((int(get_setting("arr", "2")) * self.fps_limit) / 60) + self.__time: int = 0 + + def frames_to_time(self, frames: int) -> str: + """Convert frames to a time string (MM:SS.mmm) given the FPS.""" + total_seconds = frames / 60 # Base is set at 60 FPS + minutes = int(total_seconds // 60) + seconds = int(total_seconds % 60) + milliseconds = int((total_seconds % 1) * 1000) + return f"{minutes:02}:{seconds:02}.{milliseconds:03}" + + def show_stats(self, stdscr: window) -> None: + """This will show the stats on bottom right.""" + # Note: DRAW_BOARD_HEIGHT is the border height + # so we minus 2 to get the inner height + safe_addstr( + stdscr, + self.offset[0] + DRAW_BOARD_HEIGHT - 2, + self.offset[1] + DRAW_BOARD_WIDTH + 2, + f"Lines: {self.lines_cleared}", + ) + safe_addstr( + stdscr, + self.offset[0] + DRAW_BOARD_HEIGHT - 1, + self.offset[1] + DRAW_BOARD_WIDTH + 2, + f"Time: {self.frames_to_time(self.__time)}", + ) + # Debug info + # safe_addstr( + # stdscr, + # self.offset[0] + DRAW_BOARD_HEIGHT + 1, + # self.offset[1] + DRAW_BOARD_WIDTH + 2, + # f"Soft drop: {self.current_mino.soft_drop_counter if self.current_mino else 0:<2}", + # ) + # safe_addstr( + # stdscr, + # self.offset[0] + DRAW_BOARD_HEIGHT + 2, + # self.offset[1] + DRAW_BOARD_WIDTH + 2, + # f"Fall Counter: {self.current_mino.fall_counter if self.current_mino else 0:<2}", + # ) + + def clear_action_text(self, stdscr: window) -> None: + """Clears the action text.""" + blank_line: str = " " * (self.offset[1] - 1) + _y_offset: int = self.offset[0] + 12 + _x_offset: int = 0 + for _line_num in range(3): + safe_addstr( + stdscr, + _y_offset + _line_num, + _x_offset, + blank_line, + ) + + def display_action_text(self, stdscr: window) -> None: + """Displays action text above the board.""" + + # Display action text if available + if "action_text" in self.action: + self.clear_action_text(stdscr) + self.counter = 2 * self.fps_limit + _y_offset: int = self.offset[0] + 12 + for _line_num in range(len(self.action["action_text"])): + safe_addstr( + stdscr, + _y_offset + _line_num, + self.offset[1] - len(self.action["action_text"][_line_num]) - 2, + self.action["action_text"][_line_num], + A_BOLD, + ) + return + + # Countdown the counter + if self.counter > 0: + self.counter -= 1 + if self.counter == 0: + self.clear_action_text(stdscr) + + def check_game_over(self) -> bool: + """This will check if the game is over.""" + if self.current_mino and not self.is_position_valid( + self.current_mino.get_block_positions() + ): + return True + return False + + def check_clear(self) -> bool: + """This will check if the game is cleared.""" + if self.lines_cleared >= 40: + return True + return False + + def display_game_over(self, stdscr: window) -> None: + """This will display game over text.""" + center_y: int = self.max_yx[0] // 2 + center_x: int = self.max_yx[1] // 2 + self.board.draw_minos_on_board( + stdscr=stdscr, + offset=self.offset, + max_yx=self.max_yx, + current_mino=self.current_mino, + ghost_position=( + self.current_mino.position if self.current_mino else (-1, -1) + ), + ) + safe_addstr( + stdscr, + center_y - 1, + center_x - len("GAME OVER!") // 2, + "GAME OVER!", + A_BOLD, + ) + confirm_keys: Set[str] = self.get_user_keybind("menu_confirm", menu_mode=True) + confirm_string: str = ( + "/".join(sorted(confirm_keys)) + if len(confirm_keys) > 1 + else next(iter(confirm_keys)) + ) + string_len: int = len(f"Press {confirm_string} to Continue") + safe_addstr( + stdscr, + center_y + 1, + center_x - (string_len // 2), + f"Press {confirm_string} to Continue", + A_BOLD, + ) + + def display_game_cleared(self, stdscr: window) -> None: + """This will display game cleared text.""" + center_y: int = self.max_yx[0] // 2 + center_x: int = self.max_yx[1] // 2 + self.board.draw_minos_on_board( + stdscr=stdscr, + offset=self.offset, + max_yx=self.max_yx, + current_mino=self.current_mino, + ghost_position=( + self.current_mino.position if self.current_mino else (-1, -1) + ), + ) + safe_addstr( + stdscr, + center_y - 1, + center_x - len("CLEAR!") // 2, + "CLEAR!", + A_BOLD, + ) + confirm_keys: Set[str] = self.get_user_keybind("menu_confirm", menu_mode=True) + confirm_string: str = ( + "/".join(sorted(confirm_keys)) + if len(confirm_keys) > 1 + else next(iter(confirm_keys)) + ) + string_len: int = len(f"Press {confirm_string} to Continue") + safe_addstr( + stdscr, + center_y + 1, + center_x - (string_len // 2), + f"Press {confirm_string} to Continue", + A_BOLD, + ) + + def play_mode(self, stdscr: window, pressed_keys: Set[str]) -> None: + """This will play the mode.""" + if len(self.mino_list) <= 14: + self.mino_list_generator() + if not self.current_mino: + new_mino_type: str = self.mino_list.pop(0) + self.current_mino = Mino( + mino_type=new_mino_type, + level=self.level, + fps_limit=self.fps_limit, + das=self.__das, + arr=self.__arr, + ) + if self.check_game_over(): + self.mode = "game_over" + self.display_game_over(stdscr) + self.sound_action["BGM"] = ["stop"] + return + + self.check_keyinput_pressed(pressed_keys=pressed_keys) + if not self.current_mino: + self.board.draw_minos_on_board( + stdscr=stdscr, + offset=self.offset, + max_yx=self.max_yx, + current_mino=self.current_mino, + ghost_position=(-1, -1), # Placeholder value + ) + + if self.mino_touching_bottom(self.current_mino): + # print(self.current_mino.type, self.current_mino.lock_info) + if ( + self.current_mino.position[0] + < self.current_mino.lock_info["lock_height"] + ): + self.current_mino.lock_info["lock_height"] = self.current_mino.position[ + 0 + ] + self.current_mino.reset_lock_count(level=self.level) + elif ( + pressed_keys + and not pressed_keys & {"down", "space"} + and self.current_mino.lock_info["lock_count"] > 0 + ): + self.current_mino.lock_info["lock_count"] -= 1 + self.current_mino.reset_lock_delay(level=self.level) + elif self.current_mino.lock_info["lock_delay"] > 0: + self.current_mino.lock_info["lock_delay"] -= 1 + else: + self.board.place_mino( + self.current_mino.type, + self.current_mino.orientation, + self.current_mino.position, + ) + self.calculate_score() + self.reset_mino() + + if self.current_mino: + if self.current_mino.fall_counter > 0: + self.current_mino.fall_counter -= 1 + elif not self.mino_touching_bottom(self.current_mino) and not ( + pressed_keys & {"down", "space"} + ): + self.current_mino.natural_drop( + mino_touching_bottom_func=self.mino_touching_bottom, + is_position_valid=self.is_position_valid, + ) + + self.board.draw_minos_on_board( + stdscr=stdscr, + offset=self.offset, + max_yx=self.max_yx, + current_mino=self.current_mino, + ghost_position=self.ghost_mino_position(self.current_mino), + ) + self.display_action_text(stdscr) + + def countdown_mode(self, stdscr: window) -> None: + """This will handle the countdown mode.""" + if self.counter >= 0: + safe_addstr( + stdscr, + self.max_yx[0] // 2, + self.max_yx[1] // 2, + str((self.counter // self.fps_limit) + 1), + A_BOLD, + ) + + if self.counter % self.fps_limit == 0: + if self.counter > 0: + self.sound_action["SFX"].append("countdown") + + self.counter -= 1 + if self.counter <= 0: + self.mode = "play_music_wait" + self.sound_action["SFX"].append("go") + safe_addstr(stdscr, self.max_yx[0] // 2, self.max_yx[1] // 2, "Go", A_BOLD) + self.counter = self.fps_limit // 2 + + def increment_frame(self, stdscr: window, pressed_keys: Set[str]) -> None: + """This will increment the frame.""" + check_max_yx: Tuple[int, int] = stdscr.getmaxyx() + if check_max_yx != self.max_yx: + self.max_yx = check_max_yx + self.offset = ( + max(1, (self.max_yx[0] - DRAW_BOARD_HEIGHT) // 2), + max(1, (self.max_yx[1] - DRAW_BOARD_WIDTH) // 2), + ) + self.invalidate_draw_cache() + + if check_max_yx[0] < MIN_Y or check_max_yx[1] < MIN_X: + return + + if self.check_clear(): + self.mode = "cleared" + self.display_game_cleared(stdscr) + self.sound_action["BGM"] = ["stop"] + return + + if self.mode == "game_over": + if self.get_user_keybind("menu_confirm", menu_mode=True) & pressed_keys: + self.action["transition"] = ["Score_Screen"] + set_temp("score", "-1") + set_temp("score_type", "Sprint") + self.sound_action["SFX"].append("select_confirm") + return + self.display_game_over(stdscr) + return + if self.mode == "cleared": + if self.get_user_keybind("menu_confirm", menu_mode=True) & pressed_keys: + self.action["transition"] = ["Score_Screen"] + set_temp("score", str(self.__time)) + set_temp("score_type", "Sprint") + self.sound_action["SFX"].append("select_confirm") + return + self.display_game_cleared(stdscr) + return + + queue_to_draw: List[str] = self.mino_list[0:5] + hold_to_draw: Optional[str] = ( + self.current_hold.type if self.current_hold else None + ) + + self.board.draw_blank_board(stdscr, self.offset) + self.show_stats(stdscr) + self.board.add_title(stdscr, self.offset, "Sprint") + + if queue_to_draw != self._last_drawn_queue: + self.board.draw_queue( + stdscr, + offset=self.offset, + max_yx=self.max_yx, + queue_list=self.mino_list[0:5], + ) + self._last_drawn_queue = queue_to_draw.copy() + + if hold_to_draw != self._last_drawn_hold: + self.board.draw_hold( + stdscr, + offset=self.offset, + max_yx=self.max_yx, + hold_used=self.hold_used, + hold_mino=self.current_hold, + ) + self._last_drawn_hold = hold_to_draw + + if self.get_user_keybind("restart") & pressed_keys: + self.action["transition"] = ["Sprint"] + self.sound_action["SFX"].append("select_confirm") + return + if self.get_user_keybind("menu_back", menu_mode=True) & pressed_keys: + self.action["transition"] = ["Solo_Menu"] + self.sound_action["SFX"].append("select_back") + return + + if self.mode == "countdown": + self.countdown_mode(stdscr) + return + + self.play_mode(stdscr, pressed_keys) + + self.__time += 60 // self.fps_limit + + if self.mode == "play_music_wait": + self.counter -= 1 + if self.counter <= 0: + self.mode = "play" + self.sound_action["BGM"] = ["Kalinka"] + return + safe_addstr( + stdscr, + self.max_yx[0] // 2, + self.max_yx[1] // 2, + "Go", + A_BOLD, + ) + + +if __name__ == "__main__": + print("This is a module, please run starter.py.") diff --git a/tetr_cli/tetr_modules/modes/solo/ultra_mode.py b/tetr_cli/tetr_modules/modes/solo/ultra_mode.py new file mode 100644 index 0000000..973711d --- /dev/null +++ b/tetr_cli/tetr_modules/modes/solo/ultra_mode.py @@ -0,0 +1,401 @@ +"""This will handle the solo ultra game mode.""" + +# coding: utf-8 + +from curses import window, A_BOLD +from typing import Optional, List, Set, Tuple + +from tetr_cli.tetr_modules.solo_core.base import SoloBaseMode +from tetr_cli.tetr_modules.solo_core.mino import Mino +from tetr_cli.tetr_modules.modules.constants import ( + MIN_X, + MIN_Y, + DRAW_BOARD_HEIGHT, + DRAW_BOARD_WIDTH, +) +from tetr_cli.tetr_modules.modules.database import set_temp, get_setting +from tetr_cli.tetr_modules.modules.safe_curses import safe_addstr + + +class ModeClass(SoloBaseMode): + """This will handle the solo game mode.""" + + def __init__(self) -> None: + """This will initialize this class.""" + super().__init__(given_level=5) + # For countdown: 3 seconds countdown + # For animation: 0.5 second animation + self.mino_list_generator(initial=True) + self.counter: int = self.fps_limit * 3 # Formally countdown + self.mode: str = "countdown" + self.__das: int = int((int(get_setting("das", "10")) * self.fps_limit) / 60) + self.__arr: int = int((int(get_setting("arr", "2")) * self.fps_limit) / 60) + self.__time: int = 180 * self.fps_limit # 3 minutes in frames + + def frames_to_time(self, frames: int) -> str: + """Convert frames to a time string (MM:SS.mmm) given the FPS.""" + total_seconds = frames / self.fps_limit + if total_seconds < 0: + total_seconds = 0 + minutes = int(total_seconds // 60) + seconds = int(total_seconds % 60) + milliseconds = int((total_seconds % 1) * 1000) + return f"{minutes:02}:{seconds:02}.{milliseconds:03}" + + def show_stats(self, stdscr: window) -> None: + """This will show the stats on bottom right.""" + # Note: DRAW_BOARD_HEIGHT is the border height + # so we minus 2 to get the inner height + safe_addstr( + stdscr, + self.offset[0] + DRAW_BOARD_HEIGHT - 2, + self.offset[1] + DRAW_BOARD_WIDTH + 2, + f"Lines: {self.lines_cleared}", + ) + safe_addstr( + stdscr, + self.offset[0] + DRAW_BOARD_HEIGHT - 1, + self.offset[1] + DRAW_BOARD_WIDTH + 2, + f"Score: {self.score}", + ) + safe_addstr( + stdscr, + self.offset[0] + DRAW_BOARD_HEIGHT, + self.offset[1] + DRAW_BOARD_WIDTH + 2, + f"Time: {self.frames_to_time(self.__time)}", + ) + # Debug info + # safe_addstr( + # stdscr, + # self.offset[0] + DRAW_BOARD_HEIGHT + 1, + # self.offset[1] + DRAW_BOARD_WIDTH + 2, + # f"Soft drop: {self.current_mino.soft_drop_counter if self.current_mino else 0:<2}", + # ) + # safe_addstr( + # stdscr, + # self.offset[0] + DRAW_BOARD_HEIGHT + 2, + # self.offset[1] + DRAW_BOARD_WIDTH + 2, + # f"Fall Counter: {self.current_mino.fall_counter if self.current_mino else 0:<2}", + # ) + + def clear_action_text(self, stdscr: window) -> None: + """Clears the action text.""" + blank_line: str = " " * (self.offset[1] - 1) + _y_offset: int = self.offset[0] + 12 + _x_offset: int = 0 + for _line_num in range(3): + safe_addstr( + stdscr, + _y_offset + _line_num, + _x_offset, + blank_line, + ) + + def display_action_text(self, stdscr: window) -> None: + """Displays action text above the board.""" + + # Display action text if available + if "action_text" in self.action: + self.clear_action_text(stdscr) + self.counter = 2 * self.fps_limit + _y_offset: int = self.offset[0] + 12 + for _line_num in range(len(self.action["action_text"])): + safe_addstr( + stdscr, + _y_offset + _line_num, + self.offset[1] - len(self.action["action_text"][_line_num]) - 2, + self.action["action_text"][_line_num], + A_BOLD, + ) + return + + # Countdown the counter + if self.counter > 0: + self.counter -= 1 + if self.counter == 0: + self.clear_action_text(stdscr) + + def check_game_over(self) -> bool: + """This will check if the game is over.""" + if self.current_mino and not self.is_position_valid( + self.current_mino.get_block_positions() + ): + return True + return False + + def check_clear(self) -> bool: + """This will check if the game is cleared.""" + if self.__time <= 0: + return True + return False + + def display_game_over(self, stdscr: window) -> None: + """This will display game over text.""" + center_y: int = self.max_yx[0] // 2 + center_x: int = self.max_yx[1] // 2 + self.board.draw_minos_on_board( + stdscr=stdscr, + offset=self.offset, + max_yx=self.max_yx, + current_mino=self.current_mino, + ghost_position=( + self.current_mino.position if self.current_mino else (-1, -1) + ), + ) + safe_addstr( + stdscr, + center_y - 1, + center_x - len("GAME OVER!") // 2, + "GAME OVER!", + A_BOLD, + ) + confirm_keys: Set[str] = self.get_user_keybind("menu_confirm", menu_mode=True) + confirm_string: str = ( + "/".join(sorted(confirm_keys)) + if len(confirm_keys) > 1 + else next(iter(confirm_keys)) + ) + string_len: int = len(f"Press {confirm_string} to Continue") + safe_addstr( + stdscr, + center_y + 1, + center_x - (string_len // 2), + f"Press {confirm_string} to Continue", + A_BOLD, + ) + + def display_game_cleared(self, stdscr: window) -> None: + """This will display game cleared text.""" + center_y: int = self.max_yx[0] // 2 + center_x: int = self.max_yx[1] // 2 + self.board.draw_minos_on_board( + stdscr=stdscr, + offset=self.offset, + max_yx=self.max_yx, + current_mino=self.current_mino, + ghost_position=( + self.current_mino.position if self.current_mino else (-1, -1) + ), + ) + safe_addstr( + stdscr, + center_y - 1, + center_x - len("CLEAR!") // 2, + "CLEAR!", + A_BOLD, + ) + confirm_keys: Set[str] = self.get_user_keybind("menu_confirm", menu_mode=True) + confirm_string: str = ( + "/".join(sorted(confirm_keys)) + if len(confirm_keys) > 1 + else next(iter(confirm_keys)) + ) + string_len: int = len(f"Press {confirm_string} to Continue") + safe_addstr( + stdscr, + center_y + 1, + center_x - (string_len // 2), + f"Press {confirm_string} to Continue", + A_BOLD, + ) + + def play_mode(self, stdscr: window, pressed_keys: Set[str]) -> None: + """This will play the mode.""" + if len(self.mino_list) <= 14: + self.mino_list_generator() + if not self.current_mino: + new_mino_type: str = self.mino_list.pop(0) + self.current_mino = Mino( + mino_type=new_mino_type, + level=self.level, + fps_limit=self.fps_limit, + das=self.__das, + arr=self.__arr, + ) + if self.check_game_over(): + self.mode = "game_over" + self.display_game_over(stdscr) + self.sound_action["BGM"] = ["stop"] + return + + self.check_keyinput_pressed(pressed_keys=pressed_keys) + if not self.current_mino: + self.board.draw_minos_on_board( + stdscr=stdscr, + offset=self.offset, + max_yx=self.max_yx, + current_mino=self.current_mino, + ghost_position=(-1, -1), # Placeholder value + ) + + if self.mino_touching_bottom(self.current_mino): + # print(self.current_mino.type, self.current_mino.lock_info) + if ( + self.current_mino.position[0] + < self.current_mino.lock_info["lock_height"] + ): + self.current_mino.lock_info["lock_height"] = self.current_mino.position[ + 0 + ] + self.current_mino.reset_lock_count(level=self.level) + elif ( + pressed_keys + and not pressed_keys & {"down", "space"} + and self.current_mino.lock_info["lock_count"] > 0 + ): + self.current_mino.lock_info["lock_count"] -= 1 + self.current_mino.reset_lock_delay(level=self.level) + elif self.current_mino.lock_info["lock_delay"] > 0: + self.current_mino.lock_info["lock_delay"] -= 1 + else: + self.board.place_mino( + self.current_mino.type, + self.current_mino.orientation, + self.current_mino.position, + ) + self.calculate_score() + self.reset_mino() + + if self.current_mino: + if self.current_mino.fall_counter > 0: + self.current_mino.fall_counter -= 1 + elif not self.mino_touching_bottom(self.current_mino) and not ( + pressed_keys & {"down", "space"} + ): + self.current_mino.natural_drop( + mino_touching_bottom_func=self.mino_touching_bottom, + is_position_valid=self.is_position_valid, + ) + + self.board.draw_minos_on_board( + stdscr=stdscr, + offset=self.offset, + max_yx=self.max_yx, + current_mino=self.current_mino, + ghost_position=self.ghost_mino_position(self.current_mino), + ) + self.display_action_text(stdscr) + + def countdown_mode(self, stdscr: window) -> None: + """This will handle the countdown mode.""" + if self.counter >= 0: + safe_addstr( + stdscr, + self.max_yx[0] // 2, + self.max_yx[1] // 2, + str((self.counter // self.fps_limit) + 1), + A_BOLD, + ) + + if self.counter % self.fps_limit == 0: + if self.counter > 0: + self.sound_action["SFX"].append("countdown") + + self.counter -= 1 + if self.counter <= 0: + self.mode = "play_music_wait" + self.sound_action["SFX"].append("go") + safe_addstr(stdscr, self.max_yx[0] // 2, self.max_yx[1] // 2, "Go", A_BOLD) + self.counter = self.fps_limit // 2 + + def increment_frame(self, stdscr: window, pressed_keys: Set[str]) -> None: + """This will increment the frame.""" + check_max_yx: Tuple[int, int] = stdscr.getmaxyx() + if check_max_yx != self.max_yx: + self.max_yx = check_max_yx + self.offset = ( + max(1, (self.max_yx[0] - DRAW_BOARD_HEIGHT) // 2), + max(1, (self.max_yx[1] - DRAW_BOARD_WIDTH) // 2), + ) + self.invalidate_draw_cache() + + if check_max_yx[0] < MIN_Y or check_max_yx[1] < MIN_X: + return + + if self.check_clear(): + self.mode = "cleared" + self.display_game_cleared(stdscr) + self.sound_action["BGM"] = ["stop"] + return + + if self.mode == "game_over": + if self.get_user_keybind("menu_confirm", menu_mode=True) & pressed_keys: + self.action["transition"] = ["Score_Screen"] + set_temp("score", "-1") + set_temp("score_type", "Ultra") + self.sound_action["SFX"].append("select_confirm") + return + self.display_game_over(stdscr) + return + if self.mode == "cleared": + if self.get_user_keybind("menu_confirm", menu_mode=True) & pressed_keys: + self.action["transition"] = ["Score_Screen"] + set_temp("score", str(self.score)) + set_temp("score_type", "Ultra") + self.sound_action["SFX"].append("select_confirm") + return + self.display_game_cleared(stdscr) + return + + queue_to_draw: List[str] = self.mino_list[0:5] + hold_to_draw: Optional[str] = ( + self.current_hold.type if self.current_hold else None + ) + + self.board.draw_blank_board(stdscr, self.offset) + self.show_stats(stdscr) + self.board.add_title(stdscr, self.offset, "Ultra") + + if queue_to_draw != self._last_drawn_queue: + self.board.draw_queue( + stdscr, + offset=self.offset, + max_yx=self.max_yx, + queue_list=self.mino_list[0:5], + ) + self._last_drawn_queue = queue_to_draw.copy() + + if hold_to_draw != self._last_drawn_hold: + self.board.draw_hold( + stdscr, + offset=self.offset, + max_yx=self.max_yx, + hold_used=self.hold_used, + hold_mino=self.current_hold, + ) + self._last_drawn_hold = hold_to_draw + + if self.get_user_keybind("restart") & pressed_keys: + self.action["transition"] = ["Ultra"] + self.sound_action["SFX"].append("select_confirm") + return + if self.get_user_keybind("menu_back", menu_mode=True) & pressed_keys: + self.action["transition"] = ["Solo_Menu"] + self.sound_action["SFX"].append("select_back") + return + + if self.mode == "countdown": + self.countdown_mode(stdscr) + return + + self.play_mode(stdscr, pressed_keys) + + self.__time -= 1 + + if self.mode == "play_music_wait": + self.counter -= 1 + if self.counter <= 0: + self.mode = "play" + self.sound_action["BGM"] = ["Ivean_Polkka"] + return + safe_addstr( + stdscr, + self.max_yx[0] // 2, + self.max_yx[1] // 2, + "Go", + A_BOLD, + ) + + +if __name__ == "__main__": + print("This is a module, please run starter.py.") diff --git a/tetr_cli/tetr_modules/modules/constants.py b/tetr_cli/tetr_modules/modules/constants.py index 0591d1a..843fb28 100644 --- a/tetr_cli/tetr_modules/modules/constants.py +++ b/tetr_cli/tetr_modules/modules/constants.py @@ -23,6 +23,40 @@ DRAW_BOARD_WIDTH: int = BOARD_WIDTH * 2 # Each cell is 2 chars wide DRAW_BOARD_HEIGHT: int = 20 # Show only 22 rows 20 + 2 for extra +# For master mode +FALL_DELAY_TABLE = { + 1: 29.5, + 2: 29.5, + 3: 29.5, + 4: 29.5, + 5: 29.5, + 6: 29.5, + 7: 29.5, + 8: 29.5, + 9: 29.5, + 10: 29.5, + 11: 27.6, + 12: 25.2, + 13: 22.8, + 14: 20.4, + 15: 17.5, + 16: 16.8, + 17: 15.6, + 18: 15.0, + 19: 13.2, + 20: 11.5, + 21: 11.4, + 22: 10.8, + 23: 10.2, + 24: 9.6, + 25: 8.5, + 26: 8.4, + 27: 7.8, + 28: 7.2, + 29: 6.6, + 30: 5.5, +} + MINO_TYPES: Set[str] = {"O", "I", "T", "L", "J", "S", "Z"} MINO_COLOR: Dict[str, int] = {"O": 1, "I": 2, "T": 3, "L": 4, "J": 5, "S": 6, "Z": 7} MINO_ORIENTATIONS: List[str] = ["N", "E", "S", "W"] @@ -76,6 +110,27 @@ }, } +MINO_TO_GHOST: Dict[str, str] = {"[]": "||", "00": "()", "●●": "--", "██": "▒▒"} + + +CONTROLS_LIST: List[str] = [ + "move_left", + "move_right", + "rotate_cw", + "rotate_ccw", + "soft_drop", + "hard_drop", + "hold_piece", + "restart", + "menu_confirm", + "menu_back", + "menu_up", + "menu_down", + "menu_left", + "menu_right", +] + + SCORE_TABLE: Dict[str, Dict[int, int]] = { "regular": { 1: 100, diff --git a/tetr_cli/tetr_modules/modules/database.py b/tetr_cli/tetr_modules/modules/database.py index 120a864..f2c9f29 100644 --- a/tetr_cli/tetr_modules/modules/database.py +++ b/tetr_cli/tetr_modules/modules/database.py @@ -30,9 +30,14 @@ DEFAULT_SETTINGS: List[Tuple[str, str]] = [ - ("music_volume", "70"), - ("sfx_volume", "80"), - ("FPS_limit", "30"), + ("music_volume", "20"), + ("sfx_volume", "70"), + ("fps_limit", "30"), + ("mino_style", "[]"), + ("color_mode", "true"), + ("ghost_piece", "true"), + ("das", "10"), + ("arr", "2"), ] @@ -187,25 +192,6 @@ def reset_all(cursor: Cursor) -> None: create_temp_table(cursor) -# def check_table_exists(cursor: Cursor, table_type: str) -> None: -# """Check if the scores table exists. If not create it.""" -# cursor.execute( -# """ -# SELECT name FROM sqlite_master WHERE type='table' AND name=?; -# """, -# (table_type,), -# ) -# if not cursor.fetchone(): -# if table_type == "scores": -# create_scores_table(cursor) -# elif table_type == "keybinds": -# create_keybinds_table(cursor) -# elif table_type == "settings": -# create_settings_table(cursor) -# elif table_type == "temps": -# create_temp_table(cursor) - - def initialize_database(reset: bool = False) -> None: """This will connect to the database and create the tables if they do not exist.""" @@ -300,27 +286,80 @@ def load_keybinds() -> Dict[str, Dict[str, Set[str]]]: def update_keybind( key_name: str, key_value1: str, key_value2: Optional[str] = None -) -> None: +) -> bool: """Update keybind in the database.""" + + default_key_value1: str = "" + default_key_value2: Optional[str] = None + + success: bool = True + with connect(DB_FILE) as conn: cursor: Cursor = conn.cursor() + # Get current keybinds for rollback. + cursor.execute( + """ + SELECT key_name1, key_name2 FROM keybinds WHERE input_name = ? + """, + (key_name,), + ) + result = cursor.fetchone() + if result: + default_key_value1, default_key_value2 = result + + # Update keybinds if key_value2 is not None: cursor.execute( """ - UPDATE keybind SET key_value1 = ?, key_value2 = ? WHERE key_name = ? + UPDATE keybinds SET key_name1 = ?, key_name2 = ? WHERE input_name = ? """, (key_value1, key_value2, key_name), ) else: cursor.execute( """ - UPDATE keybind SET key_value1 = ? WHERE key_name = ? + UPDATE keybinds SET key_name1 = ?, key_name2 = ? WHERE input_name = ? + """, + (key_value1, None, key_name), + ) + + # Validate keybinds + rows: List[Tuple[str, bool, str, Optional[str]]] = [] + cursor = conn.cursor() + cursor.execute( + "SELECT input_name, is_menu_keybind, key_name1, key_name2 FROM keybinds" + ) + rows = cursor.fetchall() + menu_keybinds: Dict[str, Set[str]] = {} + game_keybinds: Dict[str, Set[str]] = {} + + for input_name, is_menu_keybind, key_name1, key_name2 in rows: + if is_menu_keybind: + if input_name not in menu_keybinds: + menu_keybinds[input_name] = set() + menu_keybinds[input_name].add(key_name1) + if key_name2 is not None: + menu_keybinds[input_name].add(key_name2) + else: + if input_name not in game_keybinds: + game_keybinds[input_name] = set() + game_keybinds[input_name].add(key_name1) + if key_name2 is not None: + game_keybinds[input_name].add(key_name2) + + if not validate_keybinds(menu_keybinds) or not validate_keybinds(game_keybinds): + # Rollback keybinds + cursor.execute( + """ + UPDATE keybinds SET key_name1 = ?, key_name2 = ? WHERE input_name = ? """, - (key_value1, key_name), + (default_key_value1, default_key_value2, key_name), ) + success = False conn.commit() + return success def get_scores(score_type: str) -> List[Tuple[str, int, str, str]]: @@ -370,7 +409,7 @@ def set_scores( conn.commit() -def get_setting(setting_name: str) -> str: +def get_setting(setting_name: str, default_value: str) -> str: """Get a setting value from the settings table.""" with connect(DB_FILE) as conn: cursor: Cursor = conn.cursor() @@ -383,7 +422,7 @@ def get_setting(setting_name: str) -> str: ) result = cursor.fetchone() - return result[0] if result else "0" + return result[0] if result else default_value def set_setting(setting_name: str, setting_value: str) -> None: diff --git a/tetr_cli/tetr_modules/modules/sound.py b/tetr_cli/tetr_modules/modules/sound.py index f35d078..e432e0c 100644 --- a/tetr_cli/tetr_modules/modules/sound.py +++ b/tetr_cli/tetr_modules/modules/sound.py @@ -7,6 +7,7 @@ from pygame import mixer from pygame.mixer import Sound +from tetr_cli.tetr_modules.modules.database import get_setting current_path: Path = Path(__file__).parent.parent.resolve() sound_path: Path = current_path / "sounds" @@ -16,6 +17,7 @@ async def load_sfx() -> Dict[str, Sound]: """Load all sound effects.""" mixer.init() + mixer.music.set_volume(float(get_setting("music_volume", "25")) / 100) sound_effects = { "select_move": mixer.Sound(str(sound_path / "sfx/select_move.wav")), @@ -30,6 +32,12 @@ async def load_sfx() -> Dict[str, Sound]: "countdown": mixer.Sound(str(sound_path / "sfx/countdown.wav")), "go": mixer.Sound(str(sound_path / "sfx/go.wav")), } + + sfx_volume = float(get_setting("sfx_volume", "50")) / 100 + + for sfx in sound_effects.values(): + sfx.set_volume(sfx_volume) + return sound_effects @@ -61,5 +69,23 @@ async def play_sounds( return current_bgm +async def update_volume(sound_effect_dict: Dict[str, Sound]) -> None: + """Update the volume of the music.""" + if mixer: + bgm_volume: float = float(get_setting("music_volume", "25")) / 100 + mixer.music.set_volume(bgm_volume) + + sfx_volume: float = float(get_setting("sfx_volume", "50")) / 100 + for sfx in sound_effect_dict.values(): + sfx.set_volume(sfx_volume) + + +async def stop_all_sounds() -> None: + """Stop all sounds.""" + if mixer: + mixer.music.stop() + mixer.quit() + + if __name__ == "__main__": print("This module is not meant to be run directly.") diff --git a/tetr_cli/tetr_modules/solo_core/base.py b/tetr_cli/tetr_modules/solo_core/base.py index 7c6df15..a74a571 100644 --- a/tetr_cli/tetr_modules/solo_core/base.py +++ b/tetr_cli/tetr_modules/solo_core/base.py @@ -23,12 +23,12 @@ class SoloBaseMode(BaseModeClass): """This is the base class for all modes.""" - def __init__(self) -> None: + def __init__(self, given_level: int = 1) -> None: """This will initialize this class.""" super().__init__() # Game stats - self.level: int = 1 + self.level: int = given_level self.back_to_back: bool = False self.combo_count: int = 0 self.lines_cleared: int = 0 @@ -258,9 +258,6 @@ def calculate_score(self, rows_dropped: int = 0) -> None: elif lines_clear_detected == 4: self.sound_action["SFX"].append("quad") - # Level up for every 10 lines cleared - self.level = max(self.level, (self.lines_cleared // 10) + 1) - def check_keyinput_pressed(self, pressed_keys: Set[str]) -> None: """This will check the keyinput pressed.""" @@ -286,14 +283,9 @@ def check_keyinput_pressed(self, pressed_keys: Set[str]) -> None: ): self.current_mino.rotate("right", self.is_position_valid) self.keyinput_cooldown.add("cw") - if pressed_keys & ( - (self.get_user_keybind("move_left")).union( - self.get_user_keybind("move_right") - ) - ): - self.current_mino.handle_sideways_auto_repeat( - pressed_keys, self.mino_touching_side - ) + self.current_mino.handle_sideways_auto_repeat( + pressed_keys, self.mino_touching_side, self.get_user_keybind + ) if pressed_keys & self.get_user_keybind("soft_drop"): if not self.mino_touching_bottom(self.current_mino): self.current_mino.soft_drop( @@ -333,9 +325,7 @@ def check_keyinput_pressed(self, pressed_keys: Set[str]) -> None: self.current_mino = temp self.current_mino.position = (21, BOARD_WIDTH // 2 - 1) self.current_mino.orientation = "N" - self.current_mino.fall_delay = self.current_mino.reset_fall_delay( - level=self.level - ) + self.current_mino.reset_fall_delay() self.current_mino.lock_info = { "lock_delay": int(0.5 * self.fps_limit), "lock_count": 15, diff --git a/tetr_cli/tetr_modules/solo_core/board.py b/tetr_cli/tetr_modules/solo_core/board.py index 0c3d448..d91cb2b 100644 --- a/tetr_cli/tetr_modules/solo_core/board.py +++ b/tetr_cli/tetr_modules/solo_core/board.py @@ -17,10 +17,12 @@ DRAW_BOARD_WIDTH, MINO_COLOR, MINO_DRAW_LOCATION, + MINO_TO_GHOST, T_SPIN_CORNER_CHECKS, ) from tetr_cli.tetr_modules.modules.safe_curses import safe_addstr from tetr_cli.tetr_modules.solo_core.mino import Mino +from tetr_cli.tetr_modules.modules.database import get_setting class Board: @@ -32,6 +34,13 @@ def __init__(self) -> None: self.__board: List[List[int]] = [ ([0] * BOARD_WIDTH) for _ in range(BOARD_HEIGHT) ] + self.__ghost_piece_setting_str: str = get_setting("ghost_piece", "true") + self.__ghost_piece_setting: bool = ( + True if self.__ghost_piece_setting_str.lower() == "true" else False + ) + self.__mino_style: str = get_setting("mino_style", "[]") + self.__ghost_mino_style: str = MINO_TO_GHOST.get(self.__mino_style, "??") + self.__mino_color_setting: bool = get_setting("color_mode", "true").lower() == "true" def clear(self) -> None: """This will clear the board.""" @@ -174,6 +183,24 @@ def draw_blank_board( A_BOLD, ) + def add_ghost_mino( + self, + current_mino: "Mino", # type: ignore + draw_board: List[List[int]], + ghost_position: Tuple[int, int], + ) -> List[List[int]]: + """This will add the ghost mino to the board.""" + ghost_shape: List[Tuple[int, int]] = MINO_DRAW_LOCATION[current_mino.type][ + current_mino.orientation + ] + for y_offset, x_offset in ghost_shape: + y_pos = ghost_position[0] + y_offset + x_pos = ghost_position[1] + x_offset + if 0 <= y_pos < BOARD_HEIGHT and 0 <= x_pos < BOARD_WIDTH: + if draw_board[y_pos][x_pos] == 0: + draw_board[y_pos][x_pos] = -MINO_COLOR[current_mino.type] + return draw_board + def draw_minos_on_board( self, stdscr: window, @@ -191,14 +218,10 @@ def draw_minos_on_board( mino_shape = MINO_DRAW_LOCATION[current_mino.type][current_mino.orientation] # Draw ghost Mino - for y_offset, x_offset in mino_shape: - y_pos = ghost_position[0] + y_offset - x_pos = ghost_position[1] + x_offset - if 0 <= y_pos < BOARD_HEIGHT and 0 <= x_pos < BOARD_WIDTH: - if draw_board[y_pos][x_pos] == 0: - draw_board[y_pos][x_pos] = -MINO_COLOR[ - current_mino.type - ] # Ghost block + if self.__ghost_piece_setting: + draw_board = self.add_ghost_mino( + current_mino, draw_board, ghost_position + ) # Draw current Mino mino_position: Tuple[int, int] = current_mino.position @@ -222,24 +245,27 @@ def draw_minos_on_board( for x_counter, cell in enumerate(row): char: str = " " if visible_rows[y_counter][x_counter] > 0: - char = "██" + char = self.__mino_style # Debug for marking pivot # char = "██" if visible_rows[y_counter][x_counter] < 10 else "●●" elif visible_rows[y_counter][x_counter] < 0: - char = "▒▒" + char = self.__ghost_mino_style elif y_counter == 20: char = "- " # The extra -1 is to adjust for zero indexing y: int = offset[0] + ((max_rows - 1) - y_counter) - adjusted_height x: int = offset[1] + x_counter * 2 - color: int = abs(cell) + color: int = 0 + if self.__mino_color_setting and cell: + color = abs(cell) + mino_color: int = color_pair(color) if cell else A_BOLD if 0 <= y < max_yx[0] and 0 <= x < max_yx[1] - 1: safe_addstr( stdscr, y, x, char, - color_pair(color) if cell else A_BOLD, + mino_color ) def draw_queue( @@ -309,6 +335,10 @@ def draw_queue( mino_shape: List[Tuple[int, int]] = MINO_DRAW_LOCATION[mino][ orientation ] + color: int = 0 + if self.__mino_color_setting: + color = MINO_COLOR.get(mino, 0) + mino_color: int = color_pair(color) for y_offset, x_offset in mino_shape: pos = ( mino_offset[0] + (mino_height - 1 - y_offset), @@ -319,8 +349,8 @@ def draw_queue( stdscr, pos[0], pos[1], - "██", - color_pair(MINO_COLOR.get(mino, 0)), + self.__mino_style, + mino_color, ) def draw_hold( @@ -384,15 +414,16 @@ def draw_hold( mino_type: str = hold_mino.type if mino_type not in MINO_DRAW_LOCATION: return - mino_char: str = "██" - if hold_used: - mino_char = "▒▒" + mino_char: str = self.__mino_style mino_height: int = 2 mino_width: int = 4 mino_offset: Tuple[int, int] = (hold_offset[0] + 3, hold_offset[1] + 5) orientation: str = "N" + # if hold_used: + # mino_char = self.__ghost_mino_style + for y in range(mino_height): for x in range(-2, mino_width * 2): clear_y = mino_offset[0] + y @@ -400,8 +431,15 @@ def draw_hold( if 0 <= clear_y < max_yx[0] and 0 <= clear_x < max_yx[1] - 1: safe_addstr(stdscr, clear_y, clear_x, " ", A_BOLD) + # # Draw the hold mino using block positions mino_shape: List[Tuple[int, int]] = MINO_DRAW_LOCATION[mino_type][orientation] + color: int = 0 + if self.__mino_color_setting: + color = MINO_COLOR.get(mino_type, 0) + mino_color: int = color_pair( + color if not hold_used else 8 + ) for y_offset, x_offset in mino_shape: pos = ( mino_offset[0] + (mino_height - 1 - y_offset), @@ -413,7 +451,7 @@ def draw_hold( pos[0], pos[1], mino_char, - color_pair(MINO_COLOR.get(mino_type, 0)), + mino_color ) diff --git a/tetr_cli/tetr_modules/solo_core/mino.py b/tetr_cli/tetr_modules/solo_core/mino.py index 2e4e5b4..f5a985b 100644 --- a/tetr_cli/tetr_modules/solo_core/mino.py +++ b/tetr_cli/tetr_modules/solo_core/mino.py @@ -10,13 +10,16 @@ MINO_DRAW_LOCATION, MINO_ORIENTATIONS, ) + from tetr_cli.tetr_modules.solo_core.srs import SRS_WALL_KICK_DATA class Mino: """This will handle the mino.""" - def __init__(self, mino_type: str, level: int, fps_limit: int) -> None: + def __init__( + self, mino_type: str, level: int, fps_limit: int, das: int, arr: int + ) -> None: """This will initialize this class.""" self.__type: str = mino_type self.__orientation: str = "N" @@ -27,14 +30,23 @@ def __init__(self, mino_type: str, level: int, fps_limit: int) -> None: self.__soft_drop_counter: int = 0 self.__fps_limit: int = fps_limit - self.__fall_delay: int = self.reset_fall_delay(level) + + # Drops per frame variables + self.__drop_per_frame: int = 1 + self.__fall_delay: int = 0 + self.__fall_counter: int = 0 + self.calculate_blocks_per_frame(level) self.__lock_info: Dict[str, int] = { - "lock_delay": int(0.5 * self.__fps_limit), - "lock_count": 15, + "lock_delay": 0, + "lock_count": 0, "lock_height": 21, } + self.reset_lock_delay(level=level) + self.reset_lock_count(level=level) - self.__auto_repeat_delay: int = self.calculate_das() + self.__das: int = das + self.__arr: int = arr + self.__auto_repeat_delay: int = self.__das self.__last_sideways_direction: str = "" @property @@ -73,16 +85,6 @@ def kick_number(self, value: int) -> None: """This will set the kick number.""" self.__kick_number = max(0, value) - @lru_cache(maxsize=4) - def calculate_das(self) -> int: - """This will return the delayed auto shift.""" - return int(0.05 * self.__fps_limit) - - @lru_cache(maxsize=4) - def calculate_arr(self) -> int: - """This will return the auto repeat rate.""" - return int(0.01 * self.__fps_limit) - @property def lock_info(self) -> Dict[str, int]: """This will return the lock info.""" @@ -93,27 +95,6 @@ def lock_info(self, value: Dict[str, int]) -> None: """This will set the lock info.""" self.__lock_info = value - @property - def auto_repeat_delay(self) -> int: - """This will return the auto repeat delay.""" - return self.__auto_repeat_delay - - @auto_repeat_delay.setter - def auto_repeat_delay(self, value: int) -> None: - """This will set the auto repeat delay.""" - self.__auto_repeat_delay = max(0, value) - - @property - def last_sideways_direction(self) -> str: - """This will return the last sideways direction.""" - return self.__last_sideways_direction - - @last_sideways_direction.setter - def last_sideways_direction(self, value: str) -> None: - """This will set the last sideways direction.""" - if value in ["left", "right", ""]: - self.__last_sideways_direction = value - def get_block_positions( self, position: Tuple[int, int] = (-1, -1), @@ -190,39 +171,44 @@ def handle_sideways_auto_repeat( self, pressed_keys: Set[str], mino_touching_side_func: Callable[[str, "Mino"], bool], + get_user_keybinds: Callable[[str, bool], Set[str]], ) -> None: """Handles auto-repeat for left/right movement.""" direction: str = "" - if "left" in pressed_keys and "right" not in pressed_keys: + if get_user_keybinds("move_left", False) & pressed_keys and not ( + get_user_keybinds("move_right", False) & pressed_keys + ): direction = "left" - elif "right" in pressed_keys and "left" not in pressed_keys: + elif get_user_keybinds("move_right", False) & pressed_keys and not ( + get_user_keybinds("move_left", False) & pressed_keys + ): direction = "right" + if mino_touching_side_func(direction, self): - direction = "" + self.__auto_repeat_delay = self.__das + return if direction == "": - self.last_sideways_direction = "" - self.auto_repeat_delay = self.calculate_das() + self.__last_sideways_direction = "" + self.__auto_repeat_delay = self.__das return - if self.last_sideways_direction != direction: - self.auto_repeat_delay = self.calculate_das() - self.last_sideways_direction = direction + if self.__last_sideways_direction != direction: + self.__auto_repeat_delay = self.__das if not mino_touching_side_func(direction, self): self.move_sideways(direction) self.__kick_number = 0 - else: - self.auto_repeat_delay = self.calculate_das() - else: - if self.auto_repeat_delay > 0: - self.auto_repeat_delay -= 1 - else: - if not mino_touching_side_func(direction, self): - self.move_sideways(direction) - self.__kick_number = 0 - self.auto_repeat_delay = self.calculate_arr() - else: - self.auto_repeat_delay = self.calculate_das() + self.__last_sideways_direction = direction + return + + if self.__auto_repeat_delay > 0: + self.__auto_repeat_delay -= 1 + return + + if not mino_touching_side_func(direction, self): + self.move_sideways(direction) + self.__kick_number = 0 + self.__auto_repeat_delay = self.__arr def handle_sideways_curses_input( self, @@ -237,8 +223,7 @@ def handle_sideways_curses_input( self.__kick_number = 0 def move_down( - self, - is_position_valid: Callable[[List[Tuple[int, int]]], bool] + self, is_position_valid: Callable[[List[Tuple[int, int]]], bool] ) -> None: """This will move the current mino down.""" new_position = (self.position[0] - 1, self.position[1]) @@ -247,32 +232,90 @@ def move_down( self.__kick_number = 0 @lru_cache(maxsize=4) - def get_fall_seconds(self, level: int) -> float: - """This will return the fall seconds for the given level.""" - return pow((0.8 - ((level - 1) * 0.007)), (level - 1)) + def calculate_blocks_per_frame(self, level: int) -> None: + """This will calculate the blocks dropped per frame.""" + seconds_per_drop: float = self.get_fall_delay(level) + frames_per_drop: float = seconds_per_drop * self.__fps_limit + if seconds_per_drop > 0 and frames_per_drop < 1: + self.__drop_per_frame = int(1 / frames_per_drop) + self.__fall_delay = max(1, int(frames_per_drop)) + self.__fall_counter = self.__fall_delay - @lru_cache(maxsize=4) - def reset_fall_delay(self, level: int) -> int: - """This will return the fall delay for the given level.""" - seconds: float = self.get_fall_seconds(level) - return max(0, int(seconds * self.__fps_limit)) + @property + def fall_counter(self) -> int: + """This will return the fall counter.""" + return self.__fall_counter + + @fall_counter.setter + def fall_counter(self, value: int) -> None: + """This will set the fall counter.""" + self.__fall_counter = max(0, value) @property - def fall_delay(self) -> int: - """This will return the fall delay.""" - return self.__fall_delay + def drop_per_frame(self) -> int: + """This will return the drops per frame.""" + return self.__drop_per_frame - @fall_delay.setter - def fall_delay(self, value: int) -> None: - """This will set the fall delay.""" - self.__fall_delay = max(0, value) + def natural_drop( + self, + mino_touching_bottom_func: Callable[["Mino"], bool], + is_position_valid: Callable[[List[Tuple[int, int]]], bool], + ) -> None: + """This will handle the natural drop.""" + self.__fall_counter -= 1 + if self.__fall_counter <= 0: + self.__fall_counter = self.__fall_delay # Reset the fall counter + for _ in range(self.__drop_per_frame): + if mino_touching_bottom_func(self): + break + new_position = (self.position[0] - 1, self.position[1]) + if is_position_valid( + self.get_block_positions(new_position, self.orientation) + ): + self.position = new_position + self.__kick_number = 0 + + @lru_cache(maxsize=4) + def get_fall_delay(self, level: int) -> float: + """This will return the fall delay (in seconds) for the given level.""" + base_delay: float = 0.8 - ((min(level, 20) - 1) * 0.007) + fall_delay_in_seconds: float = pow(base_delay, (min(level, 20) - 1)) + return fall_delay_in_seconds + + def reset_fall_delay(self) -> None: + """This will reset the fall delay for the given level.""" + self.__fall_counter = self.__fall_delay + + def reset_lock_delay(self, level: int) -> None: + """This will reset the lock delay.""" + lock_delay_frames: int = int(0.5 * self.__fps_limit) + if level <= 20: + self.__lock_info["lock_delay"] = lock_delay_frames + return + # every level after level 20, reduce lock delay by 1 frame, minimum 1 frame + frames_to_reduce: int = level - 20 + self.__lock_info["lock_delay"] = max(lock_delay_frames - frames_to_reduce, 1) + + def reset_lock_count(self, level: int) -> None: + """This will reset the lock count.""" + lock_count: int = 15 + if level <= 40: + self.__lock_info["lock_count"] = lock_count + return + # every level after level 40, reduce lock count by 1, minimum 1 + counts_to_reduce: int = level - 40 + self.__lock_info["lock_count"] = max(lock_count - counts_to_reduce, 1) + + @property + def soft_drop_counter(self) -> int: + """This will return the soft drop counter.""" + return self.__soft_drop_counter @lru_cache(maxsize=4) - def get_soft_drop_delay(self, level: int) -> int: - """This will return the soft drop delay for the given level.""" - seconds: float = self.get_fall_seconds(level) - soft_drop_seconds: float = seconds / 20 # Soft drop is 20 times faster - return max(0, int(soft_drop_seconds * self.__fps_limit)) + def get_soft_drop_delay(self, level: int) -> float: + """This will return the soft drop delay (in seconds) for the given level.""" + # Soft drop is 20 times faster than normal drop + return self.get_fall_delay(level) / 20 def soft_drop( self, @@ -281,13 +324,24 @@ def soft_drop( ) -> None: """This will handle the soft drop.""" self.__soft_drop_counter += 1 - delay: int = self.get_soft_drop_delay(level) - if self.__soft_drop_counter >= delay: + delay_seconds: float = self.get_soft_drop_delay(level) + delay_per_frame: float = delay_seconds * self.__fps_limit + block_movement_repeats: int = 1 + if delay_per_frame < 1: + block_movement_repeats = int(1 / delay_per_frame) + delay_per_frame = 1 + if self.__soft_drop_counter < delay_per_frame: + return + for _ in range(block_movement_repeats): new_position = (self.position[0] - 1, self.position[1]) - if is_position_valid(self.get_block_positions(new_position, self.orientation)): + if is_position_valid( + self.get_block_positions(new_position, self.orientation) + ): self.position = new_position self.__soft_drop_counter = 0 self.__kick_number = 0 + else: + break def hard_drop( self,