Initial commit
This commit is contained in:
commit
742357af7b
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/dist/
|
||||||
|
/das_tetris_gui.spec
|
||||||
|
/other/
|
||||||
|
/.venv/
|
||||||
|
/.idea/
|
||||||
227
Serializer.py
Normal file
227
Serializer.py
Normal file
@ -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
|
||||||
75
configs/blocks_000.json
Normal file
75
configs/blocks_000.json
Normal file
@ -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]]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
configs/blocks_ADD_000.json
Normal file
49
configs/blocks_ADD_000.json
Normal file
@ -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]]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
configs/config_000.json
Normal file
70
configs/config_000.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
638
das_tetris_gui.py
Normal file
638
das_tetris_gui.py
Normal file
@ -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()
|
||||||
BIN
images/icon_255x255.png
Normal file
BIN
images/icon_255x255.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
BIN
images/logo_02_512x512.png
Normal file
BIN
images/logo_02_512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
pygame~=2.6.1
|
||||||
224
tetris_config.py
Normal file
224
tetris_config.py
Normal file
@ -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
|
||||||
Loading…
x
Reference in New Issue
Block a user