234 lines
6.2 KiB
Python
234 lines
6.2 KiB
Python
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
|