commit 742357af7b92b65a7be937869367693c9d7c7bce Author: Anry Das Date: Sun Oct 26 17:46:25 2025 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8d86f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/dist/ +/das_tetris_gui.spec +/other/ +/.venv/ +/.idea/ diff --git a/Serializer.py b/Serializer.py new file mode 100644 index 0000000..a7166ee --- /dev/null +++ b/Serializer.py @@ -0,0 +1,227 @@ +import json +from enum import Enum +from typing import Any, Dict, List, Union, Type +from pathlib import Path + +try: + import pygame + + PYGAME_AVAILABLE = True +except ImportError: + PYGAME_AVAILABLE = False + + +class EnumSerializer: + """ + A class to serialize and deserialize Enum classes and collections to/from JSON files. + Handles dynamic Enum structures, various collection types, and Pygame keys. + """ + + # Cache for Pygame key mappings + _pygame_key_to_name = None + _pygame_name_to_key = None + + @classmethod + def _init_pygame_mappings(cls): + """Initialize Pygame key mappings if not already done.""" + if not PYGAME_AVAILABLE or cls._pygame_key_to_name is not None: + return + + cls._pygame_key_to_name = {} + cls._pygame_name_to_key = {} + + # Get all K_* constants from pygame + for attr_name in dir(pygame): + if attr_name.startswith('K_'): + key_value = getattr(pygame, attr_name) + cls._pygame_key_to_name[key_value] = attr_name + cls._pygame_name_to_key[attr_name] = key_value + + @classmethod + def _is_pygame_key(cls, value: Any) -> bool: + """Check if a value is a Pygame key constant.""" + if not PYGAME_AVAILABLE or not isinstance(value, int): + return False + + cls._init_pygame_mappings() + return value in cls._pygame_key_to_name + + @classmethod + def _serialize_pygame_key(cls, key_value: int) -> Dict[str, Any]: + """Serialize a Pygame key to human-readable format.""" + cls._init_pygame_mappings() + return { + '__type__': 'PygameKey', + 'name': cls._pygame_key_to_name[key_value], + 'value': key_value # Keep numeric value as backup + } + + @staticmethod + def _serialize_enum_class(enum_class: Type[Enum]) -> Dict[str, Any]: + """Serialize an Enum class definition to a dictionary.""" + return { + '__type__': 'EnumClass', + 'name': enum_class.__name__, + 'members': {member.name: member.value for member in enum_class} + } + + @staticmethod + def _serialize_enum_instance(enum_instance: Enum) -> Dict[str, Any]: + """Serialize an Enum instance to a dictionary.""" + return { + '__type__': 'EnumInstance', + 'class_name': enum_instance.__class__.__name__, + 'name': enum_instance.name, + 'value': enum_instance.value + } + + @staticmethod + def _serialize_value(value: Any) -> Any: + """Recursively serialize a value, handling Enums, Pygame keys, and collections.""" + # Check for Pygame key first + if EnumSerializer._is_pygame_key(value): + return EnumSerializer._serialize_pygame_key(value) + elif isinstance(value, Enum): + return EnumSerializer._serialize_enum_instance(value) + elif isinstance(value, dict): + return {k: EnumSerializer._serialize_value(v) for k, v in value.items()} + elif isinstance(value, (list, tuple, set)): + serialized = [EnumSerializer._serialize_value(item) for item in value] + return { + '__type__': type(value).__name__, + 'items': serialized + } + else: + return value + + @staticmethod + def save_to_file(data: Dict[str, Any], filename: str) -> None: + """ + Save data (Enums, Pygame keys, and collections) to a JSON file. + + Args: + data: Dictionary containing the data to save. Keys are variable names, + values can be Enum classes, Enum instances, Pygame keys, or collections. + filename: Path to the JSON file where data will be saved. + + Example: + serializer = EnumSerializer() + data = { + 'color_enum': Color, + 'current_color': Color.RED, + 'color_list': [Color.RED, Color.BLUE], + 'settings': {'theme': Color.DARK, 'count': 5}, + 'move_left': pygame.K_LEFT, + 'controls': {'left': pygame.K_LEFT, 'right': pygame.K_RIGHT} + } + serializer.save_to_file(data, 'config.json') + """ + serialized_data = {} + + for key, value in data.items(): + if isinstance(value, type) and issubclass(value, Enum): + # Serialize Enum class definition + serialized_data[key] = EnumSerializer._serialize_enum_class(value) + else: + # Serialize value (could be Enum instance, Pygame key, or collection) + serialized_data[key] = EnumSerializer._serialize_value(value) + + Path(filename).parent.mkdir(parents=True, exist_ok=True) + + with open(filename, 'w', encoding='utf-8') as f: + json.dump(serialized_data, f, indent=2, ensure_ascii=False) + + @staticmethod + def _deserialize_enum_class(data: Dict[str, Any]) -> Type[Enum]: + """Recreate an Enum class from serialized data.""" + name = data['name'] + members = data['members'] + return Enum(name, members) + + @classmethod + def _deserialize_value(cls, value: Any, enum_registry: Dict[str, Type[Enum]]) -> Any: + """Recursively deserialize a value, handling Enums, Pygame keys, and collections.""" + if isinstance(value, dict): + if '__type__' in value: + type_name = value['__type__'] + + if type_name == 'PygameKey': + # Restore Pygame key + if PYGAME_AVAILABLE: + cls._init_pygame_mappings() + key_name = value['name'] + if key_name in cls._pygame_name_to_key: + return cls._pygame_name_to_key[key_name] + # Fallback to numeric value if Pygame not available + return value['value'] + + elif type_name == 'EnumInstance': + # Restore Enum instance + class_name = value['class_name'] + if class_name in enum_registry: + enum_class = enum_registry[class_name] + return enum_class[value['name']] + else: + # Return raw data if Enum class not found + return value + + elif type_name in ('list', 'tuple', 'set'): + # Restore collection + items = [cls._deserialize_value(item, enum_registry) + for item in value['items']] + if type_name == 'tuple': + return tuple(items) + elif type_name == 'set': + return set(items) + else: + return items + else: + # Regular dictionary + return {k: cls._deserialize_value(v, enum_registry) + for k, v in value.items()} + else: + return value + + @staticmethod + def load_from_file(filename: str) -> Dict[str, Any]: + """ + Load data (Enums, Pygame keys, and collections) from a JSON file. + + Args: + filename: Path to the JSON file to load. + + Returns: + Dictionary with deserialized data. Enum classes, instances, and Pygame keys are restored. + + Example: + serializer = EnumSerializer() + data = serializer.load_from_file('config.json') + Color = data['color_enum'] + current_color = data['current_color'] + left_key = data['move_left'] # Will be pygame.K_LEFT + """ + with open(filename, 'r', encoding='utf-8') as f: + serialized_data = json.load(f) + + # First pass: restore all Enum class definitions + enum_registry = {} + for key, value in serialized_data.items(): + if isinstance(value, dict) and value.get('__type__') == 'EnumClass': + enum_class = EnumSerializer._deserialize_enum_class(value) + enum_registry[enum_class.__name__] = enum_class + + # Second pass: deserialize all values + deserialized_data = {} + for key, value in serialized_data.items(): + if isinstance(value, dict) and value.get('__type__') == 'EnumClass': + # Return the Enum class itself + deserialized_data[key] = enum_registry[value['name']] + else: + # Deserialize value + deserialized_data[key] = EnumSerializer._deserialize_value(value, enum_registry) + + return deserialized_data + + +if __name__ == '__main__': + pass diff --git a/configs/blocks_000.json b/configs/blocks_000.json new file mode 100644 index 0000000..66816d2 --- /dev/null +++ b/configs/blocks_000.json @@ -0,0 +1,75 @@ +{ + "ALL_BLOCKS": { + "__type__": "EnumClass", + "name": "BlockType", + "members": { + "O": { + "name": "Square", + "color": [255, 255, 0], + "min_score": 0, + "shapes": [ + [[1,1],[1,1]] + ] + }, + "I": { + "name": "Line", + "color": [0, 255, 255], + "min_score": 0, + "shapes": [ + [[1,1,1,1]],[[1],[1],[1],[1]] + ] + }, + "L": { + "name": "L-Shape", + "color": [255, 0, 255], + "min_score": 0, + "shapes": [ + [[1, 0], [1, 0], [1, 1]], + [[1, 1, 1], [1, 0, 0]], + [[1, 1], [0, 1], [0, 1]], + [[0, 0, 1], [1, 1, 1]] + ] + }, + "J": { + "name": "J-Shape", + "color": [0, 0, 255], + "min_score": 900, + "shapes": [ + [[0, 1], [0, 1], [1, 1]], + [[1, 0, 0], [1, 1, 1]], + [[1, 1], [1, 0], [1, 0]], + [[1, 1, 1], [0, 0, 1]] + ] + }, + "S": { + "name": "S-Shape", + "color": [0, 255, 0], + "min_score": 1000, + "shapes": [ + [[0, 1, 1], [1, 1, 0]], + [[1, 0], [1, 1], [0, 1]] + ] + }, + "Z": { + "name": "Z-Shape", + "color": [255, 0, 0], + "min_score": 2500, + "shapes": [ + [[1, 1, 0], [0, 1, 1]], + [[0, 1], [1, 1], [1, 0]] + ] + }, + "T": { + "name": "T-Shape", + "color": [255, 255, 255], + "min_score": 2500, + "shapes": [ + [[1, 1, 1], [0, 1, 0]], + [[1, 0], [1, 1], [1, 0]], + [[0, 1, 0], [1, 1, 1]], + [[0, 1], [1, 1], [0, 1]] + ] + } + } + } +} \ No newline at end of file diff --git a/configs/blocks_ADD_000.json b/configs/blocks_ADD_000.json new file mode 100644 index 0000000..78eb71f --- /dev/null +++ b/configs/blocks_ADD_000.json @@ -0,0 +1,49 @@ +{ + "ADDITIONAL_BLOCKS": { + "__type__": "EnumClass", + "name": "BlockType", + "members": { + "U": { + "name": "U-Shape", + "color": [135, 135, 135], + "min_score": 4500, + "shapes": [ + [[1, 0, 1], [1, 1, 1]], + [[1, 1], [1, 0], [1, 1]], + [[1, 1, 1], [1, 0, 1]], + [[1, 1], [0, 1], [1, 1]] + ] + }, + "T1": { + "name": "T-Shape+", + "color": [0, 200, 255], + "min_score": 10000, + "shapes": [ + [[1, 1, 1], [0, 1, 0], [0, 1, 0]], + [[0, 0, 1], [1, 1, 1], [0, 0, 1]], + [[0, 1, 0], [0, 1, 0], [1, 1, 1]], + [[1, 0, 0], [1, 1, 1], [1, 0, 0]] + ] + }, + "U1": { + "name": "U-Shape+", + "color": [255, 150, 0], + "min_score": 10000, + "shapes": [ + [[1, 1, 1], [1, 0, 1], [1, 0, 1]], + [[1, 1, 1], [1, 0, 0], [1, 1, 1]], + [[1, 0, 1], [1, 0, 1], [1, 1, 1]], + [[1, 1, 1], [0, 0, 1], [1, 1, 1]] + ] + }, + "X": { + "name": "X-Shape", + "color": [50, 150, 100], + "min_score": 15000, + "shapes": [ + [[0, 1, 0], [1, 1, 1], [0, 1, 0]] + ] + } + } + } +} \ No newline at end of file diff --git a/configs/config_000.json b/configs/config_000.json new file mode 100644 index 0000000..1d51871 --- /dev/null +++ b/configs/config_000.json @@ -0,0 +1,70 @@ +{ + "CONFIG": { + "keys": { + "left": { + "__type__": "PygameKey", + "name": "K_LEFT" + }, + "right": { + "__type__": "PygameKey", + "name": "K_RIGHT" + }, + "rotate": { + "__type__": "PygameKey", + "name": "K_UP" + }, + "drop": { + "__type__": "PygameKey", + "name": "K_DOWN" + }, + "pause": { + "__type__": "PygameKey", + "name": "K_p" + }, + "restart": { + "__type__": "PygameKey", + "name": "K_r" + }, + "exit": { + "__type__": "PygameKey", + "name": "K_ESCAPE" + } + }, + "controls_text": { + "__type__": "list", + "items": [ + "Controls:", + "Left/Right Move", + "Up - Rotate", + "Dn - Drop", + "P - Pause", + "R - Restart", + "ESC - Exit" + ] + }, + "colors": { + "background": { + "__type__": "tuple", + "items": [20, 20, 40] + }, + "field": { + "__type__": "tuple", + "items": [30, 30, 60] + }, + "border": { + "__type__": "tuple", + "items": [100, 100, 150] + }, + "text": { + "__type__": "tuple", + "items": [255, 255, 255] + }, + "game_over": { + "__type__": "tuple", + "items": [255, 0, 0] + } + }, + "store_width": 10, + "block_size": 30 + } +} \ No newline at end of file diff --git a/das_tetris_gui.py b/das_tetris_gui.py new file mode 100644 index 0000000..9d18245 --- /dev/null +++ b/das_tetris_gui.py @@ -0,0 +1,638 @@ +import os +import sys +os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide" +import pygame +import random +from typing import List, Tuple +import tetris_config as cfg + +# Initialize Pygame +pygame.init() + +# ============================================================================ +# CONFIGURATION SECTION +# ============================================================================ + +cfg.load_config() +BLOCK_TYPE = cfg.BLOCK_TYPE +KEY_CONFIG = cfg.KEY_CONFIG +CONTROLS_TEXT = cfg.CONTROLS_TEXT +COLORS = cfg.COLORS +STORE_WIDTH = cfg.STORE_WIDTH +BLOCK_SIZE = cfg.BLOCK_SIZE + +# Be carefully with this method call due to it may rewrite current configuration +# cfg.store_config() + +# ============================================================================ +# TETRIS GAME CLASS +# ============================================================================ + +class TetrisGame: + def __init__(self, width: int = STORE_WIDTH): + self.width = width + self.height = 20 + + self.score = 0 + self.lines_cleared = 0 + self.game_over = False + self.paused = False + + # Game field: 0 = empty, 1-7 = block type + self.field = [[0 for _ in range(self.width)] for _ in range(self.height)] + + # Current falling piece + self.current_block = None + self.current_block_x = 0 + self.current_block_y = 0 + self.current_block_rotation = 0 + + # Next piece preview + self.next_block = None + self.next_block_rotation = 0 + + # Timing + self.fall_time = 0 + self.fall_speed = 0.5 # seconds + self.last_time = pygame.time.get_ticks() + self.key_pressed = {} # Track which keys are pressed + self.key_cooldown = {} # Cooldown timers for each key + self.key_cooldown_time = 100 # milliseconds + self.pause_key_pressed = False # Track pause key state for toggle + + # Initialize game + self.spawn_piece() + + def get_block_min_score(self, block_type: cfg.BlockType) -> int: + """Get minimal score of block to be shown""" + return block_type.value['min_score'] + + def get_block_shapes(self, block_type: cfg.BlockType) -> List[List[List[int]]]: + """Get all rotation shapes for a block type""" + return block_type.value['shapes'] + + def get_block_color(self, block_type: cfg.BlockType) -> Tuple[int, int, int]: + """Get color for a block type""" + return block_type.value['color'] + + def spawn_piece(self): + """Spawn a new piece at the top""" + if self.next_block is None: + self.current_block = self.choice_random_block() + self.current_block_rotation = 0 + self.next_block = self.choice_random_block() + self.next_block_rotation = 0 + else: + self.current_block = self.next_block + self.current_block_rotation = self.next_block_rotation + while self.current_block == self.next_block: + self.next_block = self.choice_random_block() + self.next_block_rotation = 0 + + self.current_block_x = self.width // 2 - 2 + self.current_block_y = 0 + + # Check if game is over (can't place new piece) + if not self.can_place_block(self.current_block_x, self.current_block_y, + self.current_block_rotation, self.current_block): + self.game_over = True + + def choice_random_block(self) -> cfg.BlockType: + block: cfg.BlockType = random.choice(list(cfg.BlockType)) + while self.get_block_min_score(block) > self.score: + block = random.choice(list(cfg.BlockType)) + return block + + def get_block_shape(self, block_type: cfg.BlockType, rotation: int) -> List[List[int]]: + """Get the shape matrix for a block at specific rotation""" + shapes = self.get_block_shapes(block_type) + return shapes[rotation % len(shapes)] + + def can_place_block(self, x: int, y: int, rotation: int, block_type: cfg.BlockType) -> bool: + """Check if a block can be placed at given position""" + shape = self.get_block_shape(block_type, rotation) + + for row_idx, row in enumerate(shape): + for col_idx, cell in enumerate(row): + if cell == 0: + continue + + field_x = x + col_idx + field_y = y + row_idx + + # Check boundaries + if field_x < 0 or field_x >= self.width or field_y >= self.height: + return False + + # Check collisions + if field_y >= 0 and self.field[field_y][field_x] != 0: + return False + + return True + + def place_block(self, x: int, y: int, rotation: int, block_type: cfg.BlockType, block_id: int): + """Place a block on the field""" + shape = self.get_block_shape(block_type, rotation) + + for row_idx, row in enumerate(shape): + for col_idx, cell in enumerate(row): + if cell == 0: + continue + + field_x = x + col_idx + field_y = y + row_idx + + if field_y >= 0 and 0 <= field_x < self.width: + self.field[field_y][field_x] = block_id + + def move_left(self): + """Move current block left""" + if self.can_place_block(self.current_block_x - 1, self.current_block_y, + self.current_block_rotation, self.current_block): + self.current_block_x -= 1 + + def move_right(self): + """Move current block right""" + if self.can_place_block(self.current_block_x + 1, self.current_block_y, + self.current_block_rotation, self.current_block): + self.current_block_x += 1 + + def rotate(self): + """Rotate current block""" + new_rotation = (self.current_block_rotation + 1) % len( + self.get_block_shapes(self.current_block)) + + if self.can_place_block(self.current_block_x, self.current_block_y, + new_rotation, self.current_block): + self.current_block_rotation = new_rotation + + def drop(self): + """Move block down, return True if block settled""" + if self.can_place_block(self.current_block_x, self.current_block_y + 1, + self.current_block_rotation, self.current_block): + self.current_block_y += 1 + return False + else: + # Block can't move down, place it + self.place_block(self.current_block_x, self.current_block_y, + self.current_block_rotation, self.current_block, + list(cfg.BlockType).index(self.current_block) + 1) + + # Check for complete lines + self.clear_lines() + + # Spawn new piece + self.spawn_piece() + return True + + def clear_lines(self): + """Clear complete lines and return number cleared""" + # Build new field without complete lines + new_field = [] + cleared_count = 0 + + # Check each row from bottom to top + for row_idx in range(self.height): + # If row is NOT complete, keep it + if not all(cell != 0 for cell in self.field[row_idx]): + new_field.append(self.field[row_idx][:]) # Copy the row + else: + cleared_count += 1 + + # Add empty rows at the top for each cleared line + for _ in range(cleared_count): + new_field.insert(0, [0] * self.width) + + # Replace the field + self.field = new_field + + # Update score and counter + if cleared_count > 0: + self.lines_cleared += cleared_count + # Bonus scoring: 1 line = 100, 2 lines = 300, 3 lines = 500, 4 lines = 800 + score_bonus = [0, 100, 300, 500, 800] + self.score += score_bonus[min(cleared_count, 4)] + + return cleared_count + + def update(self, current_ticks: int): + """Update game state""" + elapsed = (current_ticks - self.last_time) / 1000.0 # Convert to seconds + self.last_time = current_ticks + + if self.score > 5000: + self.fall_speed = 0.55 + elif self.score > 10000: + self.fall_speed = 0.6 + elif self.score > 15000: + self.fall_speed = 0.65 + + if not self.game_over and not self.paused: + self.fall_time += elapsed + if self.fall_time >= self.fall_speed: + self.fall_time = 0 + self.drop() + + def is_key_available(self, key: int, current_ticks: int) -> bool: + """Check if enough time has passed since last key press""" + if key not in self.key_cooldown: + self.key_cooldown[key] = 0 + + if current_ticks - self.key_cooldown[key] >= self.key_cooldown_time: + self.key_cooldown[key] = current_ticks + return True + return False + + def handle_input(self, current_ticks: int, screen): + """Handle keyboard input""" + keys = pygame.key.get_pressed() + + if keys[KEY_CONFIG['exit']]: + return not show_question_dialogue(screen, 'Are you sure to exit game?') + + # Handle pause toggle (detect key press, not hold) + if keys[KEY_CONFIG['pause']]: + if not self.pause_key_pressed: + self.paused = not self.paused + self.pause_key_pressed = True + else: + self.pause_key_pressed = False + + if keys[KEY_CONFIG['restart']]: + self.reset() + return True + + if self.game_over or self.paused: + return True + + # Prevent double key pressing with cooldown + if keys[KEY_CONFIG['left']] and self.is_key_available(KEY_CONFIG['left'], current_ticks): + self.move_left() + + if keys[KEY_CONFIG['right']] and self.is_key_available(KEY_CONFIG['right'], current_ticks): + self.move_right() + + if keys[KEY_CONFIG['rotate']] and self.is_key_available(KEY_CONFIG['rotate'], current_ticks): + self.rotate() + + if keys[KEY_CONFIG['drop']] and self.is_key_available(KEY_CONFIG['drop'], current_ticks): + self.drop() + + return True + + def reset(self): + """Reset the game""" + self.score = 0 + self.lines_cleared = 0 + self.game_over = False + self.paused = False + self.field = [[0 for _ in range(self.width)] for _ in range(self.height)] + self.current_block = None + self.next_block = None + self.fall_time = 0 + self.key_cooldown = {} + self.pause_key_pressed = False + self.spawn_piece() + + +class TetrisRenderer: + def __init__(self, width: int = STORE_WIDTH): + self.width = width + self.height = 20 + self.block_size = BLOCK_SIZE + + # Calculate window size + self.field_width = self.width * self.block_size + self.field_height = self.height * self.block_size + self.ui_width = 250 + + self.window_width = self.field_width + self.ui_width + 60 + self.window_height = self.field_height + 80 + + self.screen = pygame.display.set_mode((self.window_width, self.window_height)) + pygame.display.set_caption("Tetris by -=:dAs:=-") + + self.clock = pygame.time.Clock() + self.font_large = pygame.font.Font(None, 48) + self.font_medium = pygame.font.Font(None, 32) + self.font_small = pygame.font.Font(None, 24) + + # Positioning + self.field_x = 20 + self.field_y = 20 + self.ui_x = self.field_x + self.field_width + 30 + self.ui_y = self.field_y + + def draw_block(self, x: int, y: int, color: Tuple[int, int, int], size: int = None): + """Draw a single block""" + if size is None: + size = self.block_size + + pygame.draw.rect(self.screen, color, (x, y, size - 2, size - 2)) + pygame.draw.rect(self.screen, (255, 255, 255), (x, y, size - 2, size - 2), 1) + + def render(self, game: TetrisGame): + """Render the entire game""" + self.screen.fill(COLORS['background']) + + # Draw field border + pygame.draw.rect(self.screen, COLORS['border'], + (self.field_x - 2, self.field_y - 2, + self.field_width + 4, self.field_height + 4), 2) + + # Draw field background + pygame.draw.rect(self.screen, COLORS['field'], + (self.field_x, self.field_y, self.field_width, self.field_height)) + + # Draw placed blocks + for row_idx, row in enumerate(game.field): + for col_idx, cell in enumerate(row): + if cell != 0: + block_type = list(cfg.BlockType)[cell - 1] + color = game.get_block_color(block_type) + x = self.field_x + col_idx * self.block_size + y = self.field_y + row_idx * self.block_size + self.draw_block(x, y, color) + + # Draw current falling piece + if game.current_block is not None: + shape = game.get_block_shape(game.current_block, game.current_block_rotation) + color = game.get_block_color(game.current_block) + + for row_idx, row in enumerate(shape): + for col_idx, cell in enumerate(row): + if cell == 1: + x = self.field_x + (game.current_block_x + col_idx) * self.block_size + y = self.field_y + (game.current_block_y + row_idx) * self.block_size + + if y >= self.field_y: # Only draw if visible + self.draw_block(x, y, color) + + # Draw UI panel + self._draw_ui(game) + + # Draw game over screen + if game.game_over: + self._draw_game_over() + + # Draw pause screen + if game.paused: + self._draw_paused() + + pygame.display.flip() + + def _draw_ui(self, game: TetrisGame): + """Draw UI panel with score and next piece""" + # Title + title = self.font_medium.render("-=:dAs:=- TETRIS", True, COLORS['text']) + self.screen.blit(title, (self.ui_x, self.ui_y)) + + # Score + score_label = self.font_small.render("Score:", True, COLORS['text']) + score_value = self.font_medium.render(str(game.score), True, (255, 255, 0)) + self.screen.blit(score_label, (self.ui_x, self.ui_y + 60)) + self.screen.blit(score_value, (self.ui_x, self.ui_y + 90)) + + # Lines + lines_label = self.font_small.render("Lines:", True, COLORS['text']) + lines_value = self.font_medium.render(str(game.lines_cleared), True, (255, 255, 0)) + self.screen.blit(lines_label, (self.ui_x, self.ui_y + 140)) + self.screen.blit(lines_value, (self.ui_x, self.ui_y + 170)) + + # Next block + next_label = self.font_small.render("Next:", True, COLORS['text']) + self.screen.blit(next_label, (self.ui_x, self.ui_y + 220)) + + if game.next_block is not None: + shape = game.get_block_shape(game.next_block, game.next_block_rotation) + color = game.get_block_color(game.next_block) + + preview_x = self.ui_x + 10 + preview_y = self.ui_y + 260 + block_preview_size = 15 + + for row_idx, row in enumerate(shape): + for col_idx, cell in enumerate(row): + if cell == 1: + x = preview_x + col_idx * block_preview_size + y = preview_y + row_idx * block_preview_size + self.draw_block(x, y, color, block_preview_size) + + # Controls + controls_y = self.ui_y + 380 + + for i, text in enumerate(CONTROLS_TEXT): + if i == 0: + control_surf = self.font_small.render(text, True, (100, 200, 255)) + else: + control_surf = self.font_small.render(text, True, (150, 150, 200)) + self.screen.blit(control_surf, (self.ui_x - 5, controls_y + i * 25)) + + def _draw_game_over(self): + """Draw game over overlay""" + overlay = pygame.Surface((self.window_width, self.window_height)) + overlay.set_alpha(128) + overlay.fill((0, 0, 0)) + self.screen.blit(overlay, (0, 0)) + + game_over_text = self.font_large.render("GAME OVER", True, COLORS['game_over']) + restart_text = self.font_medium.render("Press R to Restart", True, COLORS['text']) + + text_rect = game_over_text.get_rect(center=(self.window_width // 2, self.window_height // 2 - 40)) + restart_rect = restart_text.get_rect(center=(self.window_width // 2, self.window_height // 2 + 40)) + + self.screen.blit(game_over_text, text_rect) + self.screen.blit(restart_text, restart_rect) + + def _draw_paused(self): + """Draw pause overlay""" + overlay = pygame.Surface((self.window_width, self.window_height)) + overlay.set_alpha(128) + overlay.fill((0, 0, 0)) + self.screen.blit(overlay, (0, 0)) + + paused_text = self.font_large.render("PAUSED", True, (100, 200, 255)) + continue_text = self.font_medium.render("Press P to Continue", True, COLORS['text']) + + text_rect = paused_text.get_rect(center=(self.window_width // 2, self.window_height // 2 - 40)) + continue_rect = continue_text.get_rect(center=(self.window_width // 2, self.window_height // 2 + 40)) + + self.screen.blit(paused_text, text_rect) + self.screen.blit(continue_text, continue_rect) + + def get_fps(self) -> float: + """Get current FPS""" + return self.clock.get_fps() + + +def show_question_dialogue(screen, question_text, font=None): + """ + Shows a question dialogue with Yes and No buttons. + + Args: + screen: Pygame display surface + question_text: The question to display + font: Pygame font object (optional, will create default if None) + + Returns: + True if Yes is clicked, False if No is clicked + """ + # Initialize font if not provided + if font is None: + font = pygame.font.Font(None, 36) + + # Get screen dimensions + screen_width, screen_height = screen.get_size() + + # Define colors + OVERLAY_COLOR = (0, 0, 0, 180) # Semi-transparent black + DIALOG_BG = (50, 50, 50) + TEXT_COLOR = (255, 255, 255) + YES_COLOR = (50, 150, 50) + YES_HOVER = (70, 180, 70) + NO_COLOR = (150, 50, 50) + NO_HOVER = (180, 70, 70) + BUTTON_TEXT = (255, 255, 255) + + # Create dialogue box dimensions + dialog_width = 500 + dialog_height = 250 + dialog_x = (screen_width - dialog_width) // 2 + dialog_y = (screen_height - dialog_height) // 2 + + # Button dimensions + button_width = 120 + button_height = 50 + button_spacing = 40 + yes_button_x = dialog_x + dialog_width // 2 - button_width - button_spacing // 2 + no_button_x = dialog_x + dialog_width // 2 + button_spacing // 2 + button_y = dialog_y + dialog_height - button_height - 30 + + yes_button_rect = pygame.Rect(yes_button_x, button_y, button_width, button_height) + no_button_rect = pygame.Rect(no_button_x, button_y, button_width, button_height) + + # Render question text (word wrap) + words = question_text.split() + lines = [] + current_line = [] + max_width = dialog_width - 40 + + for word in words: + test_line = ' '.join(current_line + [word]) + if font.size(test_line)[0] <= max_width: + current_line.append(word) + else: + if current_line: + lines.append(' '.join(current_line)) + current_line = [word] + if current_line: + lines.append(' '.join(current_line)) + + # Main dialogue loop + running = True + result = None + + while running: + mouse_pos = pygame.mouse.get_pos() + yes_hovered = yes_button_rect.collidepoint(mouse_pos) + no_hovered = no_button_rect.collidepoint(mouse_pos) + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + sys.exit() + + if event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: # Left click + if yes_hovered: + result = True + running = False + elif no_hovered: + result = False + running = False + + # Optional: keyboard shortcuts + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_y: + result = True + running = False + elif event.key == pygame.K_n: + result = False + running = False + + # Draw everything + # Create semi-transparent overlay + overlay = pygame.Surface((screen_width, screen_height)) + overlay.set_alpha(180) + overlay.fill((0, 0, 0)) + screen.blit(overlay, (0, 0)) + + # Draw dialogue box + pygame.draw.rect(screen, DIALOG_BG, (dialog_x, dialog_y, dialog_width, dialog_height)) + pygame.draw.rect(screen, TEXT_COLOR, (dialog_x, dialog_y, dialog_width, dialog_height), 2) + + # Draw question text + text_y = dialog_y + 30 + for line in lines: + text_surface = font.render(line, True, TEXT_COLOR) + text_rect = text_surface.get_rect(center=(dialog_x + dialog_width // 2, text_y)) + screen.blit(text_surface, text_rect) + text_y += font.get_height() + 5 + + # Draw Yes button + yes_color = YES_HOVER if yes_hovered else YES_COLOR + pygame.draw.rect(screen, yes_color, yes_button_rect, border_radius=5) + pygame.draw.rect(screen, TEXT_COLOR, yes_button_rect, 2, border_radius=5) + yes_text = font.render("Yes", True, BUTTON_TEXT) + yes_text_rect = yes_text.get_rect(center=yes_button_rect.center) + screen.blit(yes_text, yes_text_rect) + + # Draw No button + no_color = NO_HOVER if no_hovered else NO_COLOR + pygame.draw.rect(screen, no_color, no_button_rect, border_radius=5) + pygame.draw.rect(screen, TEXT_COLOR, no_button_rect, 2, border_radius=5) + no_text = font.render("No", True, BUTTON_TEXT) + no_text_rect = no_text.get_rect(center=no_button_rect.center) + screen.blit(no_text, no_text_rect) + + pygame.display.flip() + + return result + + +def main(): + """Main game loop""" + game = TetrisGame(width=STORE_WIDTH) + renderer = TetrisRenderer(width=STORE_WIDTH) + + running = True + + while running: + current_ticks = pygame.time.get_ticks() + + # Handle events + for event in pygame.event.get(): + if event.type == pygame.QUIT and show_question_dialogue(renderer.screen, 'Are you sure to exit game?'): + running = False + break + + if not running: + break # Exit main loop if quit was requested + + # Handle input + if not game.handle_input(current_ticks, renderer.screen): + running = False + continue + + # Update game + game.update(current_ticks) + + # Render + renderer.render(game) + renderer.clock.tick(60) # 60 FPS + + pygame.quit() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/images/icon_255x255.png b/images/icon_255x255.png new file mode 100644 index 0000000..d280258 Binary files /dev/null and b/images/icon_255x255.png differ diff --git a/images/logo_02_512x512.png b/images/logo_02_512x512.png new file mode 100644 index 0000000..db8ed63 Binary files /dev/null and b/images/logo_02_512x512.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..81e5291 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pygame~=2.6.1 \ No newline at end of file diff --git a/tetris_config.py b/tetris_config.py new file mode 100644 index 0000000..113d55f --- /dev/null +++ b/tetris_config.py @@ -0,0 +1,224 @@ +import dataclasses +import json +import os.path +import sys +from enum import Enum +import pygame +from Serializer import EnumSerializer + +CONFIG_JSON = 'configs/config.json' +CONFIG_BLOCKS_JSON = 'configs/blocks.json' +CONFIG_ADD_BLOCKS_JSON = 'configs/blocks_ADD.json' + + +@dataclasses.dataclass +class Block: + name: str + color: tuple[int, int, int] + min_score: int + shapes: list + + +class BlockType(Enum): + """ + Define block types with their shapes and colors + All shapes draws starting in top left corner from left to right + """ + O = { + 'name': 'Square', + 'color': (255, 255, 0), # Yellow + 'min_score': 0, + 'shapes': [ + [ + [1, 1], + [1, 1] + ] + ] + } + I = { + 'name': 'Line', + 'color': (0, 255, 255), # Cyan + 'min_score': 0, + 'shapes': [ + [[1, 1, 1, 1]], + [[1], [1], [1], [1]] + ] + } + L = { + 'name': 'L-Shape', + 'color': (255, 0, 255), # Magenta + 'min_score': 0, + 'shapes': [ + [[1, 0], [1, 0], [1, 1]], + [[1, 1, 1], [1, 0, 0]], + [[1, 1], [0, 1], [0, 1]], + [[0, 0, 1], [1, 1, 1]] + ] + } + J = { + 'name': 'J-Shape', + 'color': (0, 0, 255), # Blue + 'min_score': 900, + 'shapes': [ + [[0, 1], [0, 1], [1, 1]], + [[1, 0, 0], [1, 1, 1]], + [[1, 1], [1, 0], [1, 0]], + [[1, 1, 1], [0, 0, 1]] + ] + } + S = { + 'name': 'S-Shape', + 'color': (0, 255, 0), # Green + 'min_score': 1000, + 'shapes': [ + [[0, 1, 1], [1, 1, 0]], + [[1, 0], [1, 1], [0, 1]] + ] + } + Z = { + 'name': 'Z-Shape', + 'color': (255, 0, 0), # Red + 'min_score': 2500, + 'shapes': [ + [[1, 1, 0], [0, 1, 1]], + [[0, 1], [1, 1], [1, 0]] + ] + } + T = { + 'name': 'T-Shape', + 'color': (255, 255, 255), # White + 'min_score': 2500, + 'shapes': [ + [[1, 1, 1], [0, 1, 0]], + [[1, 0], [1, 1], [1, 0]], + [[0, 1, 0], [1, 1, 1]], + [[0, 1], [1, 1], [0, 1]] + ] + } + U = { + 'name': 'U-Shape', + 'color': (135, 135, 135), # Change color + 'min_score': 4500, + 'shapes': [ + [[1, 0, 1], [1, 1, 1]], + [[1, 1], [1, 0], [1, 1]], + [[1, 1, 1], [1, 0, 1]], + [[1, 1], [0, 1], [1, 1]] + ] + } + # T1 = { + # 'name': 'T-Shape+', + # 'color': (200, 200, 200), # Change color + # 'min_score': 10000, + # 'shapes': [ + # [[1, 1, 1], [0, 1, 0], [0, 1, 0]], + # [[0, 0, 1], [1, 1, 1], [0, 0, 1]], + # [[0, 1, 0], [0, 1, 0], [1, 1, 1]], + # [[1, 0, 0], [1, 1, 1], [1, 0, 0]] + # ] + # } + # U1 = { + # 'name': 'U-Shape+', + # 'color': (250, 250, 250), # Change color + # 'min_score': 10000, + # 'shapes': [ + # [[1, 1, 1], [1, 0, 1], [1, 0, 1]], + # [[1, 1, 1], [1, 0, 0], [1, 1, 1]], + # [[1, 0, 1], [1, 0, 1], [1, 1, 1]], + # [[1, 1, 1], [0, 0, 1], [1, 1, 1]] + # ] + # } + # X = { + # 'name': 'X-Shape', + # 'color': (250, 250, 250), # Change color + # 'min_score': 15000, + # 'shapes': [ + # [[0, 1, 0], [1, 1, 1], [0, 1, 0]] + # ] + # } + +BLOCK_TYPE = BlockType + +# Key configuration +KEY_CONFIG = { + 'left': pygame.K_LEFT, + 'right': pygame.K_RIGHT, + 'rotate': pygame.K_UP, + 'drop': pygame.K_DOWN, + 'pause': pygame.K_p, + 'restart': pygame.K_r, + 'exit': pygame.K_ESCAPE +} + +CONTROLS_TEXT = [ + "Controls:", + "Left/Right Move", + "Up - Rotate", + "Dn - Drop", + "P - Pause", + "R - Restart", + "ESC - Exit" +] + +# Store width configuration +STORE_WIDTH = 10 # number of blocks wide +BLOCK_SIZE = 30 # pixels per block + +# Colors +COLORS = { + 'background': (20, 20, 40), + 'field': (30, 30, 60), + 'border': (100, 100, 150), + 'text': (255, 255, 255), + 'game_over': (255, 0, 0) +} + + +def store_config(): + blocks_to_save = {'ALL_BLOCKS': BlockType} + es = EnumSerializer() + es.save_to_file(blocks_to_save, CONFIG_BLOCKS_JSON) + config_to_save = { + 'CONFIG': { + 'keys': KEY_CONFIG, + 'controls_text': CONTROLS_TEXT, + 'colors': COLORS, + 'store_width': STORE_WIDTH, + 'block_size': BLOCK_SIZE + } + } + es.save_to_file(config_to_save, CONFIG_JSON) + + +def load_config(): + global KEY_CONFIG, COLORS, CONTROLS_TEXT, STORE_WIDTH, BLOCK_SIZE, BLOCK_TYPE + es = EnumSerializer + + if not os.path.isfile(CONFIG_JSON) or not os.path.isfile(CONFIG_JSON) : + print('No config files was found') + sys.exit(10) + + config_data = es.load_from_file(CONFIG_JSON) + KEY_CONFIG = config_data['CONFIG']['keys'] + CONTROLS_TEXT = config_data['CONFIG']['controls_text'] + COLORS = config_data['CONFIG']['colors'] + STORE_WIDTH = config_data['CONFIG']['store_width'] + BLOCK_SIZE = config_data['CONFIG']['block_size'] + + with open(CONFIG_BLOCKS_JSON) as block_fd: + blocks_data = json.load(block_fd) + + blocks0 = blocks_data['ALL_BLOCKS']['members'] + + blocks1 = {} + if os.path.isfile(CONFIG_ADD_BLOCKS_JSON): + with open(CONFIG_ADD_BLOCKS_JSON) as block_fd: + blocks_data1 = json.load(block_fd) + blocks1 = blocks_data1['ADDITIONAL_BLOCKS']['members'] + + blocks = blocks0 | blocks1 + BLOCK_TYPE = Enum('BlockType', blocks) + + +if __name__ == '__main__': + pass