import dataclasses import json import os.path import sys from enum import Enum import pygame from Serializer import EnumSerializer # Resolve configs next to the script (or next to the executable when frozen by PyInstaller), # so the game does not depend on the current working directory BASE_DIR = os.path.dirname(sys.executable if getattr(sys, 'frozen', False) else os.path.abspath(__file__)) CONFIG_DIR = os.path.join(BASE_DIR, 'configs') CONFIG_JSON = os.path.join(CONFIG_DIR, 'config.json') CONFIG_BLOCKS_JSON = os.path.join(CONFIG_DIR, 'blocks.json') CONFIG_ADD_BLOCKS_JSON = os.path.join(CONFIG_DIR, 'blocks_ADD.json') # Default configs ('*_000.json') are used when the local ones above are absent DEFAULT_SUFFIX = '_000' @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 Every line mean the share rotated in clockwise direction """ 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]] ] } I3 = { 'name': 'Line3', 'color': (55, 180, 210), 'min_score': 0, 'shapes': [ [[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]] ] } 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) } HIGH_SCORE_INC_SPEED = False 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 _config_path(path: str) -> str | None: """Return the local config path, or its '*_000.json' default if the local one is absent""" if os.path.isfile(path): return path root, ext = os.path.splitext(path) default = root + DEFAULT_SUFFIX + ext return default if os.path.isfile(default) else None def _fail(message: str): print(f'Config error: {message}') sys.exit(10) def _read_json(path: str) -> dict: try: with open(path, encoding='utf-8') as fd: return json.load(fd) except (OSError, json.JSONDecodeError) as e: _fail(f'cannot read {path}: {e}') def load_config(): global KEY_CONFIG, COLORS, CONTROLS_TEXT, STORE_WIDTH, BLOCK_SIZE, BLOCK_TYPE, HIGH_SCORE_INC_SPEED es = EnumSerializer config_path = _config_path(CONFIG_JSON) blocks_path = _config_path(CONFIG_BLOCKS_JSON) if config_path is None or blocks_path is None: _fail(f'no config files were found in {CONFIG_DIR}') _read_json(config_path) # validate JSON syntax with a clear message try: config = es.load_from_file(config_path)['CONFIG'] KEY_CONFIG = config['keys'] CONTROLS_TEXT = config['controls_text'] COLORS = config['colors'] STORE_WIDTH = config['store_width'] BLOCK_SIZE = config['block_size'] except (KeyError, TypeError) as e: _fail(f'missing key {e} in {config_path}') HIGH_SCORE_INC_SPEED = str(config.get('high_score_inc_speed', False)).lower() == 'true' try: blocks0 = _read_json(blocks_path)['ALL_BLOCKS']['members'] except (KeyError, TypeError) as e: _fail(f'missing key {e} in {blocks_path}') blocks1 = {} add_blocks_path = _config_path(CONFIG_ADD_BLOCKS_JSON) if add_blocks_path is not None: try: blocks1 = _read_json(add_blocks_path)['ADDITIONAL_BLOCKS']['members'] except (KeyError, TypeError) as e: _fail(f'missing key {e} in {add_blocks_path}') blocks = blocks0 | blocks1 if not any(block['min_score'] <= 0 for block in blocks.values()): _fail('at least one block must have min_score 0') BLOCK_TYPE = Enum('BlockType', blocks) if __name__ == '__main__': pass