DasTetris/das_tetris_gui.py

646 lines
23 KiB
Python

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: BLOCK_TYPE) -> int:
"""Get minimal score of block to be shown"""
return block_type.value['min_score']
def get_block_shapes(self, block_type: BLOCK_TYPE) -> List[List[List[int]]]:
"""Get all rotation shapes for a block type"""
return block_type.value['shapes']
def get_block_color(self, block_type: BLOCK_TYPE) -> 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) -> BLOCK_TYPE:
block: BLOCK_TYPE = random.choice(list(BLOCK_TYPE))
while self.get_block_min_score(block) > self.score:
block = random.choice(list(BLOCK_TYPE))
return block
def get_block_shape(self, block_type: BLOCK_TYPE, 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: BLOCK_TYPE) -> 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: BLOCK_TYPE, 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(BLOCK_TYPE).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 cfg.HIGH_SCORE_INC_SPEED:
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
else:
if self.score > 5000:
self.fall_speed = 0.45
elif self.score > 10000:
self.fall_speed = 0.4
elif self.score > 15000:
self.fall_speed = 0.35
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(BLOCK_TYPE)[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()