diff --git a/settings.toml b/settings.toml index 80e013e..f608da0 100644 --- a/settings.toml +++ b/settings.toml @@ -1,21 +1,20 @@ -[keybinds] -rotate_colockwise = "up arrow" -hard_drop = "space" -hold = "shift+c" -rotate_counter_colockwise = "ctrl+z" -pause = "esc+F1" -move_left = "left arrow" -move_right = "right arrow" -move_down = "down arrow" +[General] +pause = ["esc", "F1"] +quit = ["q"] -[numpad] -hold = "numpad 0" -hard_drop = "numpad 5" -move_left = "numpad 4" -move_right = "numpad 6" -soft_drop = "numpad 2" -rotate_colockwise_1 = "numpad 1" -rotate_colockwise_2 = "numpad 5" -rotate_colockwise_3 = "numpad 9" -rotate_counter_colockwise_1 = "numpad 3" -rotate_counter_colockwise_2 = "numpad 7" +[Movement] +left = ["left", "kp4"] +right = ["right", "kp6"] +down = ["down", "kp2"] + +[Rotation] +cw = ["x", "up", "kp1", "kp5", "kp9"] # clockwise +ccw = ["ctrl", "z", "kp3", "kp7"] # counter-clockwise + +[Action] +hold = ["shift", "c", "kp0"] +drop = ["space", "kp5"] + +[Sound] +music = true +volume = 0.5 diff --git a/src/game/screens/game.py b/src/game/screens/game.py index 9f57e41..b764c27 100644 --- a/src/game/screens/game.py +++ b/src/game/screens/game.py @@ -1,4 +1,4 @@ -import sys +from typing import Any import pygame from utils import CONFIG, Figure, GameMode @@ -27,8 +27,9 @@ class Game(BaseScreen): music: Music that plays in the background. """ - def __init__(self, game_mode: GameMode) -> None: + def __init__(self, game_mode: GameMode, settings: dict[str, Any]) -> None: self.game_mode = game_mode + self.settings = settings self._initialize_game_components() self._start_background_music() @@ -60,7 +61,9 @@ class Game(BaseScreen): self.clock = pygame.time.Clock() self.next_figure: Figure = self._generate_next_figure() - self.tetris = Tetris(self._get_next_figure, self._update_score, self.game_mode) + self.tetris = Tetris( + self._get_next_figure, self._update_score, self.game_mode, self.settings + ) self.score = Score(self.game_mode) self.preview = Preview() diff --git a/src/game/screens/main.py b/src/game/screens/main.py index 0b5f234..573e05f 100644 --- a/src/game/screens/main.py +++ b/src/game/screens/main.py @@ -2,7 +2,7 @@ import sys from typing import Optional import pygame -from utils import CONFIG, GameMode +from utils import CONFIG, PYGAME_EVENT, GameMode, read_settings from game.log import log @@ -13,13 +13,14 @@ from .game import Game class Main(BaseScreen, SceenElement, TextScreen): def __init__(self, mode: GameMode) -> None: - # log.info("Initializing the game") + log.info("Initializing the game") self._initialize_pygame() self._initialize_surface() self._initialize_rect() self._initialize_font() self._set_buttons() self._initialize_increment_height() + self.settings = read_settings() self.game_mode = mode self.game: Optional[Game] = None @@ -36,7 +37,9 @@ class Main(BaseScreen, SceenElement, TextScreen): if event.type == pygame.QUIT: self.exit() elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_q: + if event.key in [ + PYGAME_EVENT[key] for key in self.settings["General"]["quit"] + ]: self.exit() if not self.game: @@ -61,13 +64,13 @@ class Main(BaseScreen, SceenElement, TextScreen): def exit(self) -> None: """Exit the game.""" - # log.info("Exiting the game") + log.info("Exiting the game") pygame.quit() sys.exit() def play(self) -> "Main": self._draw_background() - self.game = Game(self.game_mode) + self.game = Game(self.game_mode, self.settings) return self def _set_buttons(self) -> None: diff --git a/src/game/screens/tetris.py b/src/game/screens/tetris.py index cb43f91..409212a 100644 --- a/src/game/screens/tetris.py +++ b/src/game/screens/tetris.py @@ -2,7 +2,7 @@ from typing import Any, Callable, Optional import numpy as np import pygame -from utils import CONFIG, Direction, Figure, GameMode, Rotation +from utils import CONFIG, PYGAME_EVENT, Direction, Figure, GameMode, Rotation from game.log import log from game.sprites import Block, Tetromino @@ -45,10 +45,12 @@ class Tetris(BaseScreen): get_next_figure: Callable[[], Figure], update_score: Callable[[int, int, int], None], game_mode: GameMode, + settings: dict[str, Any], ) -> None: self._initialize_surface() self._initialize_rect() self._initialize_sprites() + self.settings = settings self.get_next_figure = get_next_figure self.update_score = update_score @@ -150,8 +152,8 @@ class Tetris(BaseScreen): self._check_finished_rows() self.game_over: bool = self._check_game_over() - # if self.game_over: - # self.restart() + if self.game_over: + self.restart() self.tetromino = Tetromino( self.sprites, @@ -171,13 +173,13 @@ class Tetris(BaseScreen): """ for block in self.tetromino.blocks: if block.pos.y <= 0: - # log.info("Game over!") + log.info("Game over!") return True return False def restart(self) -> None: """Restart the game.""" - # log.info("Restarting the game") + log.info("Restarting the game") self._reset_game_state() self._initialize_field_and_tetromino() self.game_over = False @@ -356,17 +358,19 @@ class Tetris(BaseScreen): """ Handle movement keys. - Move right [K_d, K_l]. - Move left [K_a, K_h]. + See `settings.toml` for the default key bindings. """ - right_keys = keys[pygame.K_d] or keys[pygame.K_l] - left_keys = keys[pygame.K_a] or keys[pygame.K_h] + right_keys = [PYGAME_EVENT[key] for key in self.settings["Movement"]["right"]] + right_key_pressed = any(keys[key] for key in right_keys) + + left_keys = [PYGAME_EVENT[key] for key in self.settings["Movement"]["left"]] + left_key_pressed = any(keys[key] for key in left_keys) if not self.timers.horizontal.active: - if left_keys: + if left_key_pressed: self.move_left() self.timers.horizontal.activate() - elif right_keys: + elif right_key_pressed: self.move_right() self.timers.horizontal.activate() @@ -374,46 +378,49 @@ class Tetris(BaseScreen): """ Handle rotation keys. - Rotation clockwise [K_RIGHT, K_UP, K_r, K_w, K_k]. - Rotation counter-clockwise [K_LEFT, K_e, K_i]. + See `settings.toml` for the default key bindings. """ - clockwise_keys = ( - keys[pygame.K_r] - or keys[pygame.K_UP] - or keys[pygame.K_w] - or keys[pygame.K_k] - or keys[pygame.K_RIGHT] - ) + cw_keys = [PYGAME_EVENT[key] for key in self.settings["Rotation"]["cw"]] + cw_key_pressed = any(keys[key] for key in cw_keys) - counter_clockwise_keys = ( - keys[pygame.K_e] or keys[pygame.K_i] or keys[pygame.K_LEFT] - ) + ccw_keys = [PYGAME_EVENT[key] for key in self.settings["Rotation"]["ccw"]] + ccw_key_pressed = any(keys[key] for key in ccw_keys) if not self.timers.rotation.active: - if clockwise_keys: + if cw_key_pressed: self.rotate() self.timers.rotation.activate() - if counter_clockwise_keys: + if ccw_key_pressed: self.rotate_reverse() self.timers.rotation.activate() def _handle_down_key(self, keys: pygame.key.ScancodeWrapper) -> None: - """Handle the down key [K_DOWN, K_s, K_j].""" - down_keys = keys[pygame.K_DOWN] or keys[pygame.K_s] or keys[pygame.K_j] - if not self.down_pressed and down_keys: + """ + Handle the down key. + + See `settings.toml` for the default key bindings. + """ + down_keys = [PYGAME_EVENT[key] for key in self.settings["Movement"]["down"]] + down_key_pressed = any(keys[key] for key in down_keys) + if not self.down_pressed and down_key_pressed: self.down_pressed = True self.timers.vertical.duration = self.increased_block_speed - if self.down_pressed and not down_keys: + if self.down_pressed and not down_key_pressed: self.down_pressed = False self.timers.vertical.duration = self.initial_block_speed def _handle_drop_key(self, keys: pygame.key.ScancodeWrapper) -> None: - """Handle the drop key [K_SPACE].""" - drop_keys = keys[pygame.K_SPACE] + """ + Handle the drop key. - if not self.timers.drop.active and drop_keys: + See `settings.toml` for the default key bindings. + """ + drop_keys = [PYGAME_EVENT[key] for key in self.settings["Action"]["drop"]] + drop_key_pressed = any(keys[key] for key in drop_keys) + + if not self.timers.drop.active and drop_key_pressed: self.drop() self.timers.drop.activate() diff --git a/src/game/sprites/tetromino.py b/src/game/sprites/tetromino.py index 5da521e..9863d84 100644 --- a/src/game/sprites/tetromino.py +++ b/src/game/sprites/tetromino.py @@ -203,7 +203,7 @@ class Tetromino: """ return all( 0 <= pos.x < CONFIG.game.columns - and 0 <= pos.y < CONFIG.game.rows + and -2 <= pos.y < CONFIG.game.rows and not self.field[int(pos.y), int(pos.x)] for pos in new_positions ) diff --git a/src/utils/__init__.py b/src/utils/__init__.py index ab9befe..38d1538 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,11 +1,11 @@ from .config import CONFIG from .enum import Direction, GameMode, Rotation +from .events import PYGAME_EVENT from .figure import Figure, FigureConfig from .log import log from .path import BASE_PATH from .settings import read_settings, save_settings -from .tuples import BestMove, Size -from .weights import Weights +from .tuples import Size __all__ = [ "BASE_PATH", @@ -17,8 +17,7 @@ __all__ = [ "Direction", "Rotation", "GameMode", - "Weights", - "BestMove", "read_settings", "save_settings", + "PYGAME_EVENT", ] diff --git a/src/utils/config.py b/src/utils/config.py index 5e7c6a7..27a0a97 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -21,7 +21,7 @@ class Game: size: Size = Size(columns * cell.width, rows * cell.width) pos: Vec2 = Vec2(padding, padding) offset: Vec2 = Vec2(columns // 2, -1) - initial_speed: float | int = 100 + initial_speed: float | int = 300 movment_delay: int = 150 rotation_delay: int = 200 drop_delay: int = 200 diff --git a/src/utils/events.py b/src/utils/events.py new file mode 100644 index 0000000..1e56eac --- /dev/null +++ b/src/utils/events.py @@ -0,0 +1,94 @@ +import pygame + +PYGAME_EVENT: dict[str, int] = { + "up": pygame.K_UP, + "down": pygame.K_DOWN, + "left": pygame.K_LEFT, + "right": pygame.K_RIGHT, + "space": pygame.K_SPACE, + "escape": pygame.K_ESCAPE, + "q": pygame.K_q, + "w": pygame.K_w, + "e": pygame.K_e, + "r": pygame.K_r, + "t": pygame.K_t, + "y": pygame.K_y, + "u": pygame.K_u, + "i": pygame.K_i, + "o": pygame.K_o, + "p": pygame.K_p, + "a": pygame.K_a, + "s": pygame.K_s, + "d": pygame.K_d, + "f": pygame.K_f, + "g": pygame.K_g, + "h": pygame.K_h, + "j": pygame.K_j, + "k": pygame.K_k, + "l": pygame.K_l, + "z": pygame.K_z, + "x": pygame.K_x, + "c": pygame.K_c, + "v": pygame.K_v, + "b": pygame.K_b, + "n": pygame.K_n, + "m": pygame.K_m, + "1": pygame.K_1, + "2": pygame.K_2, + "3": pygame.K_3, + "4": pygame.K_4, + "5": pygame.K_5, + "6": pygame.K_6, + "7": pygame.K_7, + "8": pygame.K_8, + "9": pygame.K_9, + "0": pygame.K_0, + "shift": pygame.K_LSHIFT, + "ctrl": pygame.K_LCTRL, + "alt": pygame.K_LALT, + "tab": pygame.K_TAB, + "capslock": pygame.K_CAPSLOCK, + "return": pygame.K_RETURN, + "backspace": pygame.K_BACKSPACE, + "insert": pygame.K_INSERT, + "delete": pygame.K_DELETE, + "home": pygame.K_HOME, + "end": pygame.K_END, + "pageup": pygame.K_PAGEUP, + "pagedown": pygame.K_PAGEDOWN, + "numlock": pygame.K_NUMLOCK, + "printscreen": pygame.K_PRINTSCREEN, + "scrolllock": pygame.K_SCROLLLOCK, + "pause": pygame.K_PAUSE, + "F1": pygame.K_F1, + "F2": pygame.K_F2, + "F3": pygame.K_F3, + "F4": pygame.K_F4, + "F5": pygame.K_F5, + "F6": pygame.K_F6, + "F7": pygame.K_F7, + "F8": pygame.K_F8, + "F9": pygame.K_F9, + "F10": pygame.K_F10, + "F11": pygame.K_F11, + "F12": pygame.K_F12, + "F13": pygame.K_F13, + "F14": pygame.K_F14, + "F15": pygame.K_F15, + "kp0": pygame.K_KP0, + "kp1": pygame.K_KP1, + "kp2": pygame.K_KP2, + "kp3": pygame.K_KP3, + "kp4": pygame.K_KP4, + "kp5": pygame.K_KP5, + "kp6": pygame.K_KP6, + "kp7": pygame.K_KP7, + "kp8": pygame.K_KP8, + "kp9": pygame.K_KP9, + "kp_divide": pygame.K_KP_DIVIDE, + "kp_multiply": pygame.K_KP_MULTIPLY, + "kp_minus": pygame.K_KP_MINUS, + "kp_plus": pygame.K_KP_PLUS, + "kp_enter": pygame.K_KP_ENTER, + "kp_equals": pygame.K_KP_EQUALS, +} diff --git a/src/utils/settings.py b/src/utils/settings.py index 920cd12..34742e0 100644 --- a/src/utils/settings.py +++ b/src/utils/settings.py @@ -1,10 +1,11 @@ from pathlib import Path -from typing import Optional +from typing import Any, Optional import toml from .config import CONFIG, Config from .log import log +from .path import BASE_PATH def save_settings(settings: Config, file_path: Path) -> None: @@ -12,7 +13,9 @@ def save_settings(settings: Config, file_path: Path) -> None: toml.dump(settings, file) -def read_settings(file_path: Path) -> Optional[dict[str, str]]: +def read_settings( + file_path: Path = BASE_PATH / "settings.toml", +) -> dict[str, Any]: """ Read and parse a TOML file and return the content as a dictionary. @@ -27,7 +30,7 @@ def read_settings(file_path: Path) -> Optional[dict[str, str]]: return toml.load(file) except FileNotFoundError: log.error(f"Error: The file '{file_path}' does not exist.") - return None + return {} except toml.TomlDecodeError as e: log.error(f"rror decoding TOML file: {e}") - return None + return {} diff --git a/src/utils/tuples.py b/src/utils/tuples.py index f7c9b98..d545952 100644 --- a/src/utils/tuples.py +++ b/src/utils/tuples.py @@ -11,8 +11,3 @@ class Size(NamedTuple): if isinstance(other, Size): return Size(self.width - other.width, self.height - other.height) return Size(self.width - other, self.height - other) - - -class BestMove(NamedTuple): - rotation: int - x: int diff --git a/src/utils/weights.py b/src/utils/weights.py deleted file mode 100644 index 6328075..0000000 --- a/src/utils/weights.py +++ /dev/null @@ -1,9 +0,0 @@ -from attrs import define - - -@define -class Weights: - height: float - lines: float - holes: float - bumpiness: float