From 5ec02d68f825a09e6296e6f764feff999108fa0f Mon Sep 17 00:00:00 2001 From: Dadmehr <134191240+BDadmehr0@users.noreply.github.com> Date: Sun, 24 Nov 2024 20:28:16 +0000 Subject: [PATCH] fix pylint --- data.json | 1 + main.py | 350 ++++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 251 insertions(+), 100 deletions(-) create mode 100644 data.json diff --git a/data.json b/data.json new file mode 100644 index 0000000..22c70fb --- /dev/null +++ b/data.json @@ -0,0 +1 @@ +{"position": 88, "score": 300, "lives": 3, "enemies": []} \ No newline at end of file diff --git a/main.py b/main.py index 84f4a3f..37239e4 100755 --- a/main.py +++ b/main.py @@ -32,17 +32,17 @@ BOX_CH = "B" # character a box # Global variables -player_position = 0 # Initial player position in the map -GAME_STATUS = False # Start with game not running -old_settings = None # Define old_settings globally +ATTACK_MESSAGE_SHOWN = False # To make sure message is only shown once +SHOW_ATTACK_MESSAGE = False # To control when to show the attack message +PLAYER_DATA_FILE = "data.json" # File to save player data +PLAYER_POSITION = 0 # Initial player position in the map +PLAYER_LIVES = 3 # Player starts with 3 lives +OLD_SETTINGS = None # Define old_settings globally keys_pressed = set() # Set to track currently pressed keys +GAME_STATUS = False # Start with game not running enemies = [] # List to hold enemy positions boxes = [] # List to hold box positions -score = 0 # Player's score -player_lives = 3 # Player starts with 3 lives -player_data_file = "data.json" # File to save player data -show_attack_message = False # To control when to show the attack message -attack_message_shown = False # To make sure message is only shown once +SCORE = 0 # Player's score # Allowed keys: only 'w', 'a', 's', 'd', and control keys like Ctrl+Z, Ctrl+C ALLOWED_KEYS = { @@ -54,8 +54,9 @@ } -# Color definitions class FGColors: + """Foreground Colors""" + RESET = "\033[0m" RED = "\033[91m" GREEN = "\033[92m" @@ -65,8 +66,19 @@ class FGColors: CYAN = "\033[96m" WHITE = "\033[97m" + @classmethod + def all_colors(cls): + """Returns a dictionary of all colors.""" + return { + attr: value + for attr, value in cls.__dict__.items() + if not attr.startswith("__") + } + class BGColors: + """Background Colors""" + RESET = "\033[49m" RED = "\033[41m" GREEN = "\033[42m" @@ -76,56 +88,111 @@ class BGColors: CYAN = "\033[46m" WHITE = "\033[47m" + @classmethod + def all_colors(cls): + """Returns a dictionary of all colors.""" + return { + attr: value + for attr, value in cls.__dict__.items() + if not attr.startswith("__") + } + class SystemCall: + """ + A utility class for handling system-level operations in the terminal. + + This class provides static methods to: + - Hide or show the terminal cursor. + - Clear the terminal screen and display player-related information. + - Retrieve the terminal's size. + - Handle exit signals and restore terminal settings. + - Disable and restore terminal input echoing for secure password input. + + Methods: + hide_cursor() - Hides the terminal cursor. + show_cursor() - Displays the terminal cursor. + clear_screen() - Clears the terminal screen and displays player data. + get_terminal_size() - Returns the current size of the terminal. + handle_exit_signal() - Handles the exit signal, restoring settings and saving data. + disable_echo() - Disables terminal input echo (used for secure input). + restore_echo() - Restores the terminal input echo settings. + """ + @staticmethod def hide_cursor(): + """Hides the terminal cursor.""" sys.stdout.write("\033[?25l") sys.stdout.flush() @staticmethod def show_cursor(): + """Displays the terminal cursor.""" sys.stdout.write("\033[?25h") sys.stdout.flush() @staticmethod def clear_screen(): + """Clears the terminal screen and displays player data.""" sys.stdout.write("\033[H\033[J") sys.stdout.write( - f"Player Lives: {player_lives}/3\tScore: {score}\n\n" + f"Player Lives: {PLAYER_LIVES}/3\tScore: {SCORE}\n\n" ) # Display player lives sys.stdout.flush() @staticmethod def get_terminal_size(): + """Returns the current size of the terminal as a tuple (columns, lines).""" return os.get_terminal_size() @staticmethod def handle_exit_signal(signum, frame): + """Handles the exit signal, restoring settings and saving player data.""" SystemCall.show_cursor() # Ensure the cursor is shown os.system("clear") - global old_settings # Make sure we're using the global old_settings - SystemCall.restore_echo(old_settings) + global OLD_SETTINGS # Make sure we're using the global old_settings + SystemCall.restore_echo(OLD_SETTINGS) save_player_data() # Save player data before exiting sys.exit(0) @staticmethod def disable_echo(): + """Disables terminal input echo for secure input (e.g., password).""" fd = sys.stdin.fileno() - old_settings = termios.tcgetattr(fd) + OLD_SETTINGS = termios.tcgetattr(fd) new_settings = termios.tcgetattr(fd) new_settings[3] = new_settings[3] & ~termios.ECHO # Disable echo termios.tcsetattr(fd, termios.TCSADRAIN, new_settings) - return old_settings + return OLD_SETTINGS @staticmethod - def restore_echo(old_settings): + def restore_echo(OLD_SETTINGS): + """Restores the terminal input echo settings.""" fd = sys.stdin.fileno() - termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + termios.tcsetattr(fd, termios.TCSADRAIN, OLD_SETTINGS) class SystemInputs: + """ + A class to handle and track keyboard inputs in the system. + + This class listens for key presses and releases, keeping track of the keys that are pressed + and whether the Shift key is held down. It also provides a method to stop the listener. + + Attributes: + keys_pressed (set): A set of characters and/or keys currently pressed by the user. + shift_pressed (bool): A flag indicating whether the Shift key is currently pressed. + + Methods: + on_press(key) - Callback method for when a key is pressed. + on_release(key) - Callback method for when a key is released. + stop() - Stops the keyboard listener. + """ + def __init__(self): + """ + Initializes the SystemInputs class, setting up the listener and key states. + """ self.keys_pressed = set() self.shift_pressed = False # Track shift key state self.listener = keyboard.Listener( @@ -134,6 +201,15 @@ def __init__(self): self.listener.start() def on_press(self, key): + """ + Callback method that is called when a key is pressed. + + Args: + key (pynput.keyboard.Key): The key that was pressed. + + This method adds the key to the set of pressed keys, and checks if the Shift key + was pressed. + """ try: if hasattr(key, "char"): if key.char in ALLOWED_KEYS: @@ -148,6 +224,15 @@ def on_press(self, key): pass def on_release(self, key): + """ + Callback method that is called when a key is released. + + Args: + key (pynput.keyboard.Key): The key that was released. + + This method removes the key from the set of pressed keys, and checks if the Shift key + was released. + """ try: if hasattr(key, "char"): if key.char in ALLOWED_KEYS: @@ -162,32 +247,73 @@ def on_release(self, key): pass def stop(self): + """ + Stops the keyboard listener. + + This method stops the listener, effectively halting the tracking of key presses and releases. + """ self.listener.stop() class Map: + """ + A class to represent the game map and handle map-related operations. + + This class generates and manages the game map, including the layout, enemies, and boxes. + It also handles the movement of enemies towards the player, checking for attack options, + and rendering the map on the screen. + + Attributes: + columns (int): The number of columns in the map (based on terminal size). + lines (int): The number of lines in the terminal (used for map generation). + generated_map (str): A string representing the map layout with characters such as '.', '_', '⌂', and '↟'. + enemies (list): A list of positions where enemies are located on the map. + boxes (list): A list of positions where boxes are located on the map. + + Methods: + __init__(): Initializes the map, generates the layout, and creates enemies and boxes. + generate_random_map(): Generates a random map layout with weighted probabilities for different characters. + generate_boxes(): Generates boxes on the map with a 0.5% chance for each column. + generate_enemies(): Generates enemies on the map with a 20% chance, excluding the first and last 5 columns. + move_enemies_towards_player(): Moves enemies one step towards the player, and decreases player lives if enemies hit the player. + draw(): Renders the map on the terminal, displaying the player, enemies, boxes, and other elements. + check_new_map(): Checks if the player has reached the end of the map and moves them to a new map. + check_attack_option(): Checks if the player is adjacent to an enemy or box and displays the corresponding attack message. + """ + def __init__(self): + """ + Initializes the map with the terminal size, generates the random layout, + and creates enemies and boxes. + """ self.columns, self.lines = ( SystemCall.get_terminal_size() ) # Use the SystemCall method to get terminal size - self.generated_map = ( - self.generate_random_map() - ) # Generate random charachter map + self.generated_map = self.generate_random_map() # Generate random character map self.generate_enemies() # Generate enemies when map is created self.generate_boxes() # Generate boxes when map is created def generate_random_map(self): - """Generate a random map layout with weighted probabilities for '.', '_', '⌂', and '↟'.""" + """ + Generates a random map layout with weighted probabilities for different characters. + + Returns: + str: A string representing the random map layout. + """ characters = [".", "_", "⌂", "↟"] - weights = [40, 40, 1, 10] # احتمال بیشتر برای '.' و '_' + weights = [40, 40, 1, 10] # Heavier probability for '.' and '_' - # تولید نقشه با انتخاب کاراکترها با وزن‌های مشخص شده + # Generate map by selecting characters based on weighted probabilities random_map = "".join(random.choices(characters, weights, k=self.columns)) return random_map def generate_boxes(self): - """Generate boxes with a 0.5% chance on the map.""" + """ + Generates boxes on the map with a 0.5% chance for each column. + + This method places boxes at random positions on the map and updates the global `boxes` list. + """ global boxes boxes = [] # Reset boxes for a new game for i in range(self.columns): @@ -195,45 +321,60 @@ def generate_boxes(self): boxes.append(i) # Add box at the position i def generate_enemies(self): - """Generate enemies with a 20% chance, excluding the first and last 5 columns.""" + """ + Generates enemies on the map with a 5% chance for each column (excluding the first and last 5 columns). + + This method updates the global `enemies` list with the positions of the generated enemies. + """ global enemies - enemies = [] # ریست کردن دشمن‌ها برای نقشه جدید - start_range = 50 # شروع از ۵ ستون اول - end_range = self.columns - 5 # پایان در ۵ ستون آخر + enemies = [] # Reset enemies for the new map + start_range = 50 # Starting from the 5th column + end_range = self.columns - 5 # Ending at the 5th column from the end for i in range(start_range, end_range): - if random.random() < 0.05: # احتمال ۲۰٪ برای تولید دشمن - enemies.append(i) # دشمن در این موقعیت ایجاد می‌شود + if random.random() < 0.05: # 5% chance for enemy generation + enemies.append(i) # Enemy is created at position i def move_enemies_towards_player(self): - """Move enemies one step towards the player.""" - global enemies, player_lives, GAME_STATUS + """ + Moves enemies one step towards the player. If an enemy reaches the player, the player's lives are decreased. + + This method updates the `enemies` list and checks if any enemy has collided with the player. + """ + global enemies, PLAYER_LIVES, GAME_STATUS new_enemies = [] for enemy_pos in enemies: - if enemy_pos < player_position: - new_pos = enemy_pos + 1 # Move right towards player - elif enemy_pos > player_position: - new_pos = enemy_pos - 1 # Move left towards player + if enemy_pos < PLAYER_POSITION: + new_pos = enemy_pos + 1 # Move right towards the player + elif enemy_pos > PLAYER_POSITION: + new_pos = enemy_pos - 1 # Move left towards the player else: # Enemy reached the player, decrease player's lives - player_lives -= 1 + PLAYER_LIVES -= 1 print( - f"{FGColors.RED}Enemy hit you! Lives remaining: {player_lives}{FGColors.RESET}" + f"{FGColors.RED}Enemy hit you! Lives remaining: {PLAYER_LIVES}{FGColors.RESET}" ) - if player_lives <= 0: + if PLAYER_LIVES <= 0: print(f"{FGColors.RED}Game Over!{FGColors.RESET}") GAME_STATUS = False return - continue # Do not add this enemy to new list, it's dead now + continue # Do not add this enemy to the new list, it's dead now new_enemies.append(new_pos) # Update enemies' positions enemies = new_enemies time.sleep(0.1) # Slows down enemy movement def draw(self): + """ + Renders the game map on the terminal screen, displaying the player, enemies, boxes, + and other map elements. + + This method updates the terminal display to show the current state of the map, including + the player's position, enemies, boxes, and special characters. + """ indexed_line = "" for i in range(self.columns): - if i == player_position: + if i == PLAYER_POSITION: indexed_line += f"{FGColors.BLUE}{PLAYER_CH}{FGColors.RESET}" elif i in enemies: indexed_line += f"{FGColors.RED}{ENEMY_CH}{FGColors.RESET}" @@ -257,34 +398,43 @@ def draw(self): self.check_new_map() def check_new_map(self): - """If the player reaches the last character, move them to a new map.""" - global player_position - if player_position == self.columns - 1: + """ + Checks if the player has reached the end of the map and moves them to a new map. + + If the player reaches the last column, this method resets the player's position and + generates a new map with enemies and boxes. + """ + global PLAYER_POSITION + if PLAYER_POSITION == self.columns - 1: print( f"{FGColors.BLUE}You reached the end! Loading new map...{FGColors.RESET}" ) time.sleep(2) - player_position = 0 # Move player to the beginning of a new map + PLAYER_POSITION = 0 # Move player to the beginning of a new map self.generate_enemies() # Creates new enemies self.generate_boxes() # Creates new boxes def check_attack_option(self): - """Check if player is next to an enemy or box and show corresponding messages.""" - global show_attack_message, attack_message_shown + """ + Checks if the player is next to an enemy or box and displays the corresponding message. + + If the player is adjacent to an enemy or box, this method informs the player that they + can attack or open the box. + """ + global SHOW_ATTACK_MESSAGE, ATTACK_MESSAGE_SHOWN if ( - player_position in enemies - or player_position - 1 in enemies - or player_position + 1 in enemies + PLAYER_POSITION in enemies + or PLAYER_POSITION - 1 in enemies + or PLAYER_POSITION + 1 in enemies ): - if not attack_message_shown: # Only show the message once + if not ATTACK_MESSAGE_SHOWN: # Only show the message once print( f"{FGColors.YELLOW}You can attack by pressing 'E'.{FGColors.RESET}" ) - attack_message_shown = True - show_attack_message = True - # time.sleep(2) # Pause for 2 seconds after showing the message - elif player_position in boxes: + ATTACK_MESSAGE_SHOWN = True + SHOW_ATTACK_MESSAGE = True + elif PLAYER_POSITION in boxes: print( f"{FGColors.GREEN}You found a box! Press 'E' to open it.{FGColors.RESET}" ) @@ -292,7 +442,7 @@ def check_attack_option(self): def show_menu(): """Display the welcome menu and handle user input.""" - global GAME_STATUS, old_settings + global GAME_STATUS, OLD_SETTINGS while True: os.system("clear") # Clear the screen print("Welcome to the Game!") @@ -301,11 +451,11 @@ def show_menu(): print("3. Exit") # Enable echo for user input in the menu - old_settings = SystemCall.restore_echo(old_settings) + OLD_SETTINGS = SystemCall.restore_echo(OLD_SETTINGS) choice = input("Please choose an option (1, 2 or 3): ") # Restore echo to disabled after menu input - old_settings = SystemCall.disable_echo() + OLD_SETTINGS = SystemCall.disable_echo() if choice == "1": load_player_data() # Load player data before starting @@ -359,46 +509,46 @@ def show_help(): def save_player_data(): """Save player position, score, lives, and enemies to a JSON file.""" player_data = { - "position": player_position, - "score": score, - "lives": player_lives, + "position": PLAYER_POSITION, + "score": SCORE, + "lives": PLAYER_LIVES, "enemies": enemies, # Save enemies positions } - with open(player_data_file, "w") as file: + with open(PLAYER_DATA_FILE, "w") as file: json.dump(player_data, file) def load_player_data(): """Load player position, score, lives, and enemies from a JSON file.""" - global player_position, score, player_lives, enemies, GAME_STATUS, old_settings + global PLAYER_POSITION, SCORE, PLAYER_LIVES, enemies, GAME_STATUS, OLD_SETTINGS while True: - if os.path.exists(player_data_file): + if os.path.exists(PLAYER_DATA_FILE): os.system("clear") print("1. Start new game") print("2. Load old game") print("3. Back") - old_settings = SystemCall.restore_echo(old_settings) + OLD_SETTINGS = SystemCall.restore_echo(OLD_SETTINGS) choice_menu2 = input("Please choose an option (1, 2 or 3): ") # Restore echo to disabled after menu input - old_settings = SystemCall.disable_echo() + OLD_SETTINGS = SystemCall.disable_echo() if choice_menu2 == "1": - player_position = 0 - score = 0 - player_lives = 3 + PLAYER_POSITION = 0 + SCORE = 0 + PLAYER_LIVES = 3 enemies = [] GAME_STATUS = True break elif choice_menu2 == "2": - with open(player_data_file, "r") as file: + with open(PLAYER_DATA_FILE, "r") as file: player_data = json.load(file) - player_position = player_data.get( + PLAYER_POSITION = player_data.get( "position", 0 ) # Default to 0 if not found - score = player_data.get("score", 0) # Default to 0 if not found - player_lives = player_data.get( + SCORE = player_data.get("score", 0) # Default to 0 if not found + PLAYER_LIVES = player_data.get( "lives", 3 ) # Default to 3 lives if not found enemies = player_data.get( @@ -412,9 +562,9 @@ def load_player_data(): print("Invalid choice. Please choose 1, 2 or 3.") time.sleep(1) # Pause before showing the menu again else: - player_position = 0 - score = 0 - player_lives = 3 + PLAYER_POSITION = 0 + SCORE = 0 + PLAYER_LIVES = 3 enemies = [] GAME_STATUS = True break @@ -425,7 +575,7 @@ def load_player_data(): signal.signal(signal.SIGINT, SystemCall.handle_exit_signal) # Handles Ctrl+C (SIGINT) # Disable input and save old settings -old_settings = SystemCall.disable_echo() +OLD_SETTINGS = SystemCall.disable_echo() # Create an instance of SystemInputs input_handler = SystemInputs() @@ -447,41 +597,41 @@ def load_player_data(): # Check for attack or box opening if "e" in input_handler.keys_pressed: - if player_position in enemies: - enemies.remove(player_position) # Eliminate the enemy - score += 100 # Increase points - elif player_position - 1 in enemies: - enemies.remove(player_position - 1) # Eliminate the enemy on the left. - score += 100 - elif player_position + 1 in enemies: - enemies.remove(player_position + 1) # Eliminate the enemy on the right. - score += 100 - elif player_position in boxes: - boxes.remove(player_position) # Delete box after opening + if PLAYER_POSITION in enemies: + enemies.remove(PLAYER_POSITION) # Eliminate the enemy + SCORE += 100 # Increase points + elif PLAYER_POSITION - 1 in enemies: + enemies.remove(PLAYER_POSITION - 1) # Eliminate the enemy on the left. + SCORE += 100 + elif PLAYER_POSITION + 1 in enemies: + enemies.remove(PLAYER_POSITION + 1) # Eliminate the enemy on the right. + SCORE += 100 + elif PLAYER_POSITION in boxes: + boxes.remove(PLAYER_POSITION) # Delete box after opening reward = random.choice(["Extra Life", "Score Boost", "Nothing"]) reward = random.choice( ["Extra Life", "Score Boost", "Speed Boost", "Penalty"] ) if reward == "Extra Life": - player_lives += 1 + PLAYER_LIVES += 1 print( - f"{FGColors.GREEN}Extra life! Lives: {player_lives}{FGColors.RESET}" + f"{FGColors.GREEN}Extra life! Lives: {PLAYER_LIVES}{FGColors.RESET}" ) elif reward == "Score Boost": - score += 50 + SCORE += 50 print( f"{FGColors.GREEN}You received a score boost! " - f"Score: {score}{FGColors.RESET}" + f"Score: {SCORE}{FGColors.RESET}" ) elif reward == "Speed Boost": # Improved player movement speed for a short time speed_boost = True elif reward == "Penalty": - player_lives -= 1 + PLAYER_LIVES -= 1 print( f"{FGColors.RED}The box was cursed! You lost a life! " - f"Lives: {player_lives}{FGColors.RESET}" + f"Lives: {PLAYER_LIVES}{FGColors.RESET}" ) else: @@ -489,19 +639,19 @@ def load_player_data(): time.sleep(0.5) # Adjust player position based on key presses - if "a" in input_handler.keys_pressed and player_position > 0: + if "a" in input_handler.keys_pressed and PLAYER_POSITION > 0: if input_handler.shift_pressed: # If shift is pressed, move faster - player_position -= 2 # Move 2 steps left + PLAYER_POSITION -= 2 # Move 2 steps left else: - player_position -= 1 # Normal speed + PLAYER_POSITION -= 1 # Normal speed elif ( "d" in input_handler.keys_pressed - and player_position < map_instance.columns - 1 + and PLAYER_POSITION < map_instance.columns - 1 ): if input_handler.shift_pressed: # If shift is pressed, move faster - player_position += 2 # Move 2 steps right + PLAYER_POSITION += 2 # Move 2 steps right else: - player_position += 1 # Normal speed + PLAYER_POSITION += 1 # Normal speed # time.sleep(0.01) # Slow down the game loop a bit except KeyboardInterrupt: @@ -509,4 +659,4 @@ def load_player_data(): input_handler.stop() # Stop the listener -SystemCall.restore_echo(old_settings) +SystemCall.restore_echo(OLD_SETTINGS)