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()