Tools

Module with tools used all across the project. They are implemented in a way to be as reusable as possible.

  1"""Module with tools used all across the project. They are implemented in a way to be as reusable as possible.
  2"""
  3
  4import customtkinter as ctk
  5from PIL import Image
  6import configparser
  7import sys
  8import os
  9from datetime import datetime
 10import sounddevice
 11from typing import Any
 12import json
 13from platform import system
 14
 15def resource_path(relative_path: str) -> str:
 16    """Function obtaining the absolute path to desired relative path.
 17    Ensures That pyinstaller executable will work properly.
 18
 19    Args:
 20        relative_path (str): Relative or absolute path to resource.
 21
 22    Returns:
 23        str: Absolute path to resource.
 24    """
 25    try:
 26        base_path = sys._MEIPASS2 # type: ignore
 27    except Exception:
 28        base_path = os.path.abspath(".")
 29    return os.path.join(base_path, relative_path)
 30
 31def get_from_config(variable: str) -> str | int:
 32    """Functions reading specific value from the config file.
 33
 34    Args:
 35        variable (str): variable name from config file.
 36
 37    Returns:
 38        str | int: Color, size or font name
 39    """
 40    config = configparser.ConfigParser()
 41    config.read(resource_path(os.path.join('assets', 'config.ini')))
 42    db_variable = config['database'][variable]
 43    if variable == 'size':
 44        return int(db_variable)
 45    elif variable == 'font_name':
 46        if system() == 'Linux':
 47            db_variable = list(db_variable.split(' '))[0]
 48    return db_variable
 49
 50def change_config(change_variable: str, value: str | int) -> None:
 51    """Updates specific variable in config file.
 52
 53    Args:
 54        change_variable (str): Variable name to change
 55        value (str | int): Value to which the variable will be updated.
 56    """
 57    config = configparser.ConfigParser()
 58    config.read(resource_path(os.path.join('assets', 'config.ini')))
 59    if isinstance(value, int):
 60        value = str(value)
 61    config['database'][change_variable] = value    
 62    with open(resource_path(os.path.join('assets', 'config.ini')), 'w') as configfile:
 63        config.write(configfile)
 64
 65def load_menu_image(option: str, resize: float = 1.5) -> ctk.CTkImage | None:
 66    """Function loading images for menu.
 67
 68    Args:
 69        option (str): Option image name.
 70        resize (float, optional): Resize value [original_val // resize]. Defaults to 1.5.
 71
 72    Returns:
 73        ctk.CTkImage | None: Image object.
 74    """
 75    setting_icon_path = resource_path(os.path.join('assets', 'menu', f'{option}.png'))
 76    try:
 77        size = int(get_from_config('size')) // resize
 78        setting_icon = Image.open(setting_icon_path).convert('RGBA')
 79        return ctk.CTkImage(light_image=setting_icon, dark_image=setting_icon, size=(size, size))
 80    except (FileNotFoundError, FileExistsError) as e:
 81        update_error_log(e)
 82    return None
 83
 84def get_colors() -> dict:
 85    """Function loading colors from config file.
 86
 87    Returns:
 88        dict: Dictionary (later enum) of color name : color code.
 89    """
 90    config = configparser.ConfigParser()
 91    config.read(resource_path(os.path.join('assets', 'config.ini')))
 92    colors = dict(config['Colors'])
 93    return colors
 94
 95def change_color(color_name: str, color_value: str) -> None:
 96    """Function changing color value in config file.
 97
 98    Args:
 99        color_name (str): Color name to change.
100        color_value (str): New color value.
101    """
102    config = configparser.ConfigParser()
103    config.read(resource_path(os.path.join('assets', 'config.ini')))
104    config['Colors'][color_name] = color_value    
105    with open(resource_path(os.path.join('assets', 'config.ini')), 'w') as configfile:
106        config.write(configfile)
107
108def update_error_log(error: Exception) -> None:
109    """Appends new error to error log.
110
111    Args:
112        error (Exception): Error to append log file with.
113    """
114    now: str = str(datetime.now())
115    with open(resource_path('error.log'), 'a') as file:
116        file.write(f'[{now}]: Error occurred: {error} in {os.path.relpath(__file__)}\n')
117
118def play_sound(data: Any) -> None:
119    """Plays sound.
120
121    Args:
122        data (Any): Array like with raw sound data.
123    """
124    try:
125        sounddevice.play(data)
126    except Exception as e:
127        update_error_log(e)
128
129def create_save_file(save_info: dict[tuple[int, int] | str, tuple[str, str, bool] | list[str]], current_turn: str,
130                     white_moves: list[str], black_moves: list[str], game_over: bool, save_name: str | None=None) -> None:
131    """Creates save file in saves directory. Save is .json file with all positions, current turn information, previous notation and game over information.
132
133    Args:
134        save_info (dict[tuple[int, int] | str, tuple[str, str, bool] | list[str]]): Information to be saved in file.
135        current_turn (str): Information about color of the current player.
136        white_moves (list[str]): Notation from previous white moves.
137        black_moves (list[str]): Notation from previous black moves.
138        game_over (bool): Information about general state of the game. 
139        save_name (str | None, optional): Name of the save file. Defaults to None.
140    """
141    save_info_serialized = {f"{k[0]},{k[1]}": v for k, v in save_info.items()}
142    save_data = {
143        'current_turn': current_turn,
144        'board_state' : save_info_serialized,
145        'white_moves' : white_moves,
146        'black_moves' : black_moves,
147        'game_over'   : game_over
148    }
149    if not save_name:
150        files: list[str] = [f for f in os.listdir(resource_path('saves')) if 'chess_game_' in f]
151        if files:
152            new_file: str = f'chess_game_{len(files)+1}.json'
153        else:
154            new_file = 'chess_game_1.json'
155        with open(resource_path(os.path.join('saves', new_file)), 'w') as file:
156            json.dump(save_data, file, indent=2)
157    else:
158        new_file = f'{save_name}.json'
159        with open(resource_path(os.path.join('saves', new_file)), 'w') as file:
160            json.dump(save_data, file, indent=2)
161
162def delete_save_file(file_name: str) -> bool:
163    """Removes save .json file from saves directory.
164
165    Args:
166        file_name (str): Name of the file to delete.
167
168    Returns:
169        bool: Returns True if file was removed successfully, False otherwise.
170    """
171    file_path: str = resource_path(os.path.join('saves', file_name))
172    if os.path.exists(file_path):
173        os.remove(file_path)
174        return True
175    else:
176        update_error_log(FileNotFoundError(f'Couldn\'t delete the file: {file_name}. File not found.'))
177        return False
178
179def get_save_info(file_name: str) -> dict:
180    """Gathers data from .json save file.
181
182    Args:
183        file_name (str): Name of the save to be loaded.
184
185    Returns:
186        dict: All needed information to load the game state.
187    """
188    with open(resource_path(os.path.join('saves', file_name)), "r") as file:
189        data: dict = json.load(file)
190    return data
def resource_path(relative_path: str) -> str:
16def resource_path(relative_path: str) -> str:
17    """Function obtaining the absolute path to desired relative path.
18    Ensures That pyinstaller executable will work properly.
19
20    Args:
21        relative_path (str): Relative or absolute path to resource.
22
23    Returns:
24        str: Absolute path to resource.
25    """
26    try:
27        base_path = sys._MEIPASS2 # type: ignore
28    except Exception:
29        base_path = os.path.abspath(".")
30    return os.path.join(base_path, relative_path)

Function obtaining the absolute path to desired relative path. Ensures That pyinstaller executable will work properly.

Arguments:
  • relative_path (str): Relative or absolute path to resource.
Returns:

str: Absolute path to resource.

def get_from_config(variable: str) -> str | int:
32def get_from_config(variable: str) -> str | int:
33    """Functions reading specific value from the config file.
34
35    Args:
36        variable (str): variable name from config file.
37
38    Returns:
39        str | int: Color, size or font name
40    """
41    config = configparser.ConfigParser()
42    config.read(resource_path(os.path.join('assets', 'config.ini')))
43    db_variable = config['database'][variable]
44    if variable == 'size':
45        return int(db_variable)
46    elif variable == 'font_name':
47        if system() == 'Linux':
48            db_variable = list(db_variable.split(' '))[0]
49    return db_variable

Functions reading specific value from the config file.

Arguments:
  • variable (str): variable name from config file.
Returns:

str | int: Color, size or font name

def change_config(change_variable: str, value: str | int) -> None:
51def change_config(change_variable: str, value: str | int) -> None:
52    """Updates specific variable in config file.
53
54    Args:
55        change_variable (str): Variable name to change
56        value (str | int): Value to which the variable will be updated.
57    """
58    config = configparser.ConfigParser()
59    config.read(resource_path(os.path.join('assets', 'config.ini')))
60    if isinstance(value, int):
61        value = str(value)
62    config['database'][change_variable] = value    
63    with open(resource_path(os.path.join('assets', 'config.ini')), 'w') as configfile:
64        config.write(configfile)

Updates specific variable in config file.

Arguments:
  • change_variable (str): Variable name to change
  • value (str | int): Value to which the variable will be updated.
def load_menu_image( option: str, resize: float = 1.5) -> customtkinter.windows.widgets.image.ctk_image.CTkImage | None:
66def load_menu_image(option: str, resize: float = 1.5) -> ctk.CTkImage | None:
67    """Function loading images for menu.
68
69    Args:
70        option (str): Option image name.
71        resize (float, optional): Resize value [original_val // resize]. Defaults to 1.5.
72
73    Returns:
74        ctk.CTkImage | None: Image object.
75    """
76    setting_icon_path = resource_path(os.path.join('assets', 'menu', f'{option}.png'))
77    try:
78        size = int(get_from_config('size')) // resize
79        setting_icon = Image.open(setting_icon_path).convert('RGBA')
80        return ctk.CTkImage(light_image=setting_icon, dark_image=setting_icon, size=(size, size))
81    except (FileNotFoundError, FileExistsError) as e:
82        update_error_log(e)
83    return None

Function loading images for menu.

Arguments:
  • option (str): Option image name.
  • resize (float, optional): Resize value [original_val // resize]. Defaults to 1.5.
Returns:

ctk.CTkImage | None: Image object.

def get_colors() -> dict:
85def get_colors() -> dict:
86    """Function loading colors from config file.
87
88    Returns:
89        dict: Dictionary (later enum) of color name : color code.
90    """
91    config = configparser.ConfigParser()
92    config.read(resource_path(os.path.join('assets', 'config.ini')))
93    colors = dict(config['Colors'])
94    return colors

Function loading colors from config file.

Returns:

dict: Dictionary (later enum) of color name : color code.

def change_color(color_name: str, color_value: str) -> None:
 96def change_color(color_name: str, color_value: str) -> None:
 97    """Function changing color value in config file.
 98
 99    Args:
100        color_name (str): Color name to change.
101        color_value (str): New color value.
102    """
103    config = configparser.ConfigParser()
104    config.read(resource_path(os.path.join('assets', 'config.ini')))
105    config['Colors'][color_name] = color_value    
106    with open(resource_path(os.path.join('assets', 'config.ini')), 'w') as configfile:
107        config.write(configfile)

Function changing color value in config file.

Arguments:
  • color_name (str): Color name to change.
  • color_value (str): New color value.
def update_error_log(error: Exception) -> None:
109def update_error_log(error: Exception) -> None:
110    """Appends new error to error log.
111
112    Args:
113        error (Exception): Error to append log file with.
114    """
115    now: str = str(datetime.now())
116    with open(resource_path('error.log'), 'a') as file:
117        file.write(f'[{now}]: Error occurred: {error} in {os.path.relpath(__file__)}\n')

Appends new error to error log.

Arguments:
  • error (Exception): Error to append log file with.
def play_sound(data: Any) -> None:
119def play_sound(data: Any) -> None:
120    """Plays sound.
121
122    Args:
123        data (Any): Array like with raw sound data.
124    """
125    try:
126        sounddevice.play(data)
127    except Exception as e:
128        update_error_log(e)

Plays sound.

Arguments:
  • data (Any): Array like with raw sound data.
def create_save_file( save_info: dict[tuple[int, int] | str, tuple[str, str, bool] | list[str]], current_turn: str, white_moves: list[str], black_moves: list[str], game_over: bool, save_name: str | None = None) -> None:
130def create_save_file(save_info: dict[tuple[int, int] | str, tuple[str, str, bool] | list[str]], current_turn: str,
131                     white_moves: list[str], black_moves: list[str], game_over: bool, save_name: str | None=None) -> None:
132    """Creates save file in saves directory. Save is .json file with all positions, current turn information, previous notation and game over information.
133
134    Args:
135        save_info (dict[tuple[int, int] | str, tuple[str, str, bool] | list[str]]): Information to be saved in file.
136        current_turn (str): Information about color of the current player.
137        white_moves (list[str]): Notation from previous white moves.
138        black_moves (list[str]): Notation from previous black moves.
139        game_over (bool): Information about general state of the game. 
140        save_name (str | None, optional): Name of the save file. Defaults to None.
141    """
142    save_info_serialized = {f"{k[0]},{k[1]}": v for k, v in save_info.items()}
143    save_data = {
144        'current_turn': current_turn,
145        'board_state' : save_info_serialized,
146        'white_moves' : white_moves,
147        'black_moves' : black_moves,
148        'game_over'   : game_over
149    }
150    if not save_name:
151        files: list[str] = [f for f in os.listdir(resource_path('saves')) if 'chess_game_' in f]
152        if files:
153            new_file: str = f'chess_game_{len(files)+1}.json'
154        else:
155            new_file = 'chess_game_1.json'
156        with open(resource_path(os.path.join('saves', new_file)), 'w') as file:
157            json.dump(save_data, file, indent=2)
158    else:
159        new_file = f'{save_name}.json'
160        with open(resource_path(os.path.join('saves', new_file)), 'w') as file:
161            json.dump(save_data, file, indent=2)

Creates save file in saves directory. Save is .json file with all positions, current turn information, previous notation and game over information.

Arguments:
  • save_info (dict[tuple[int, int] | str, tuple[str, str, bool] | list[str]]): Information to be saved in file.
  • current_turn (str): Information about color of the current player.
  • white_moves (list[str]): Notation from previous white moves.
  • black_moves (list[str]): Notation from previous black moves.
  • game_over (bool): Information about general state of the game.
  • save_name (str | None, optional): Name of the save file. Defaults to None.
def delete_save_file(file_name: str) -> bool:
163def delete_save_file(file_name: str) -> bool:
164    """Removes save .json file from saves directory.
165
166    Args:
167        file_name (str): Name of the file to delete.
168
169    Returns:
170        bool: Returns True if file was removed successfully, False otherwise.
171    """
172    file_path: str = resource_path(os.path.join('saves', file_name))
173    if os.path.exists(file_path):
174        os.remove(file_path)
175        return True
176    else:
177        update_error_log(FileNotFoundError(f'Couldn\'t delete the file: {file_name}. File not found.'))
178        return False

Removes save .json file from saves directory.

Arguments:
  • file_name (str): Name of the file to delete.
Returns:

bool: Returns True if file was removed successfully, False otherwise.

def get_save_info(file_name: str) -> dict:
180def get_save_info(file_name: str) -> dict:
181    """Gathers data from .json save file.
182
183    Args:
184        file_name (str): Name of the save to be loaded.
185
186    Returns:
187        dict: All needed information to load the game state.
188    """
189    with open(resource_path(os.path.join('saves', file_name)), "r") as file:
190        data: dict = json.load(file)
191    return data

Gathers data from .json save file.

Arguments:
  • file_name (str): Name of the save to be loaded.
Returns:

dict: All needed information to load the game state.