diff --git a/assets/Rat/mine.png b/assets/Rat/mine.png new file mode 100644 index 0000000..2e123cc Binary files /dev/null and b/assets/Rat/mine.png differ diff --git a/colorize_assets.py b/colorize_assets.py new file mode 100644 index 0000000..e69de29 diff --git a/conf/keybinding_game.json b/conf/keybinding_game.json index f800bbf..f82b282 100644 --- a/conf/keybinding_game.json +++ b/conf/keybinding_game.json @@ -9,5 +9,6 @@ "scroll_left": ["Left", 10], "scroll_right": ["Right", 11], "spawn_bomb": ["Space", 1], + "spawn_mine": ["Left Ctrl", "Control_R", 17], "pause": ["P", 16] } \ No newline at end of file diff --git a/convert_audio.py b/convert_audio.py new file mode 100644 index 0000000..d9541e2 --- /dev/null +++ b/convert_audio.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +""" +Audio conversion script to convert mine.wav to u8 22100 Hz format +""" + +from pydub import AudioSegment +import os + +def convert_mine_wav(): + """Convert mine.wav to u8 format at 22100 Hz""" + + # Input and output paths + input_path = "sound/mine.wav" + output_path = "sound/mine_converted.wav" + + if not os.path.exists(input_path): + print(f"Error: {input_path} not found!") + return + + try: + # Load the audio file + print(f"Loading {input_path}...") + audio = AudioSegment.from_wav(input_path) + + # Print current format info + print(f"Original format:") + print(f" Sample rate: {audio.frame_rate} Hz") + print(f" Channels: {audio.channels}") + print(f" Sample width: {audio.sample_width} bytes ({audio.sample_width * 8} bits)") + print(f" Duration: {len(audio)} ms") + + # Convert to mono if stereo + if audio.channels > 1: + print("Converting to mono...") + audio = audio.set_channels(1) + + # Convert to 22100 Hz sample rate + print("Converting sample rate to 22100 Hz...") + audio = audio.set_frame_rate(22100) + + # Convert to 8-bit unsigned (u8) + print("Converting to 8-bit unsigned format...") + audio = audio.set_sample_width(1) # 1 byte = 8 bits + + # Export the converted audio + print(f"Saving to {output_path}...") + audio.export(output_path, format="wav") + + # Print new format info + converted_audio = AudioSegment.from_wav(output_path) + print(f"\nConverted format:") + print(f" Sample rate: {converted_audio.frame_rate} Hz") + print(f" Channels: {converted_audio.channels}") + print(f" Sample width: {converted_audio.sample_width} bytes ({converted_audio.sample_width * 8} bits)") + print(f" Duration: {len(converted_audio)} ms") + + print(f"\nConversion complete! Output saved as: {output_path}") + + # Optionally replace the original file + replace = input("\nReplace original mine.wav with converted version? (y/n): ").lower() + if replace == 'y': + import shutil + # Backup original + backup_path = "sound/mine_original.wav" + shutil.copy2(input_path, backup_path) + print(f"Original file backed up as: {backup_path}") + + # Replace original + shutil.copy2(output_path, input_path) + os.remove(output_path) + print(f"Original file replaced with converted version.") + + except Exception as e: + print(f"Error during conversion: {e}") + +if __name__ == "__main__": + convert_mine_wav() diff --git a/engine/controls.py b/engine/controls.py index dcfa30d..64bed6e 100644 --- a/engine/controls.py +++ b/engine/controls.py @@ -27,8 +27,9 @@ class KeyBindings: elif key in keybindings.get("scroll_right", []): self.start_scrolling("Right") elif key in keybindings.get("spawn_bomb", []): - self.render_engine.play_sound("PUTDOWN.WAV") self.spawn_bomb(self.pointer) + elif key in keybindings.get("spawn_mine", []): + self.spawn_mine(self.pointer) elif key in keybindings.get("pause", []): self.game_status = "paused" if self.game_status == "game" else "game" elif key in keybindings.get("start_game", []): diff --git a/engine/graphics.py b/engine/graphics.py index 3c58f49..67170cf 100644 --- a/engine/graphics.py +++ b/engine/graphics.py @@ -2,6 +2,7 @@ import os class Graphics(): def load_assets(self): + print("Loading graphics assets...") self.tunnel = self.render_engine.load_image("Rat/BMP_TUNNEL.png", surface=True) self.grasses = [self.render_engine.load_image(f"Rat/BMP_1_GRASS_{i+1}.png", surface=True) for i in range(4)] self.rat_assets = {} @@ -16,6 +17,7 @@ class Graphics(): for file in os.listdir("assets/Rat"): if file.endswith(".png"): self.assets[file[:-4]] = self.render_engine.load_image(f"Rat/{file}") + # ==================== RENDERING ==================== @@ -65,7 +67,7 @@ class Graphics(): self.blood_stains[position] = new_blood_surface # Regenerate background to include the updated blood stain - self.regenerate_background() + self.background_texture = None def scroll_cursor(self, x=0, y=0): if self.pointer[0] + x > self.map.width or self.pointer[1] + y > self.map.height: diff --git a/engine/unit_manager.py b/engine/unit_manager.py index 4b39fe5..c6941d9 100644 --- a/engine/unit_manager.py +++ b/engine/unit_manager.py @@ -1,6 +1,6 @@ import random import uuid -from units import rat, bomb +from units import rat, bomb, mine class UnitManager: def count_rats(self): @@ -17,7 +17,14 @@ class UnitManager: self.spawn_unit(rat_class, position) def spawn_bomb(self, position): + self.render_engine.play_sound("PUTDOWN.WAV") self.spawn_unit(bomb.Timer, position) + + def spawn_mine(self, position): + if self.map.is_wall(position[0], position[1]): + return + self.render_engine.play_sound("PUTDOWN.WAV") + self.spawn_unit(mine.Mine, position) def spawn_unit(self, unit, position, **kwargs): id = uuid.uuid4() diff --git a/rats.py b/rats.py index 05cb85c..e1316ea 100644 --- a/rats.py +++ b/rats.py @@ -96,14 +96,14 @@ class MiceMaze( return True - - if self.count_rats() > 200: + count_rats = self.count_rats() + if count_rats > 200: self.render_engine.stop_sound() self.render_engine.play_sound("WEWIN.WAV") self.game_end = (True, False) self.game_status = "paused" return True - if not len(self.units): + if not count_rats: self.render_engine.stop_sound() self.render_engine.play_sound("VICTORY.WAV") self.render_engine.play_sound("WELLDONE.WAV", tag="effects") diff --git a/resize_assets.py b/resize_assets.py new file mode 100644 index 0000000..d1dc9d3 --- /dev/null +++ b/resize_assets.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +""" +Script to resize PNG asset files to 18x18 pixels and center them on a 20x20 canvas. +Saves the result back to the same file. +""" + +import os +import glob +from PIL import Image, ImageOps +import argparse + +def resize_and_center_image(image_path, target_size=(18, 18), canvas_size=(20, 20)): + """ + Resize an image to target_size and center it on a canvas of canvas_size. + + Args: + image_path (str): Path to the image file + target_size (tuple): Size to resize the image to (width, height) + canvas_size (tuple): Size of the final canvas (width, height) + """ + try: + # Open the image + with Image.open(image_path) as img: + # Convert to RGBA to handle transparency + img = img.convert("RGBA") + + # Resize the image to target size using high-quality resampling + resized_img = img.resize(target_size, Image.Resampling.LANCZOS) + + # Create a new transparent canvas + canvas = Image.new("RGBA", canvas_size, (0, 0, 0, 0)) + + # Calculate position to center the resized image + x_offset = (canvas_size[0] - target_size[0]) // 2 + y_offset = (canvas_size[1] - target_size[1]) // 2 + + # Paste the resized image onto the canvas + canvas.paste(resized_img, (x_offset, y_offset), resized_img) + + # Save back to the same file + canvas.save(image_path, "PNG", optimize=True) + + print(f"✓ Processed: {os.path.basename(image_path)}") + + except Exception as e: + print(f"✗ Error processing {image_path}: {str(e)}") + +def process_directory(directory_path, file_pattern="*.png"): + """ + Process all PNG files in a directory. + + Args: + directory_path (str): Path to the directory containing PNG files + file_pattern (str): Pattern to match files (default: "*.png") + """ + if not os.path.exists(directory_path): + print(f"Error: Directory '{directory_path}' does not exist.") + return + + # Find all PNG files matching the pattern + search_pattern = os.path.join(directory_path, file_pattern) + png_files = glob.glob(search_pattern) + + if not png_files: + print(f"No PNG files found in '{directory_path}' matching pattern '{file_pattern}'") + return + + print(f"Found {len(png_files)} PNG files to process...") + + # Process each file + for png_file in png_files: + resize_and_center_image(png_file) + + print(f"\nCompleted processing {len(png_files)} files.") + +def process_single_file(file_path): + """ + Process a single PNG file. + + Args: + file_path (str): Path to the PNG file + """ + if not os.path.exists(file_path): + print(f"Error: File '{file_path}' does not exist.") + return + + if not file_path.lower().endswith('.png'): + print(f"Error: File '{file_path}' is not a PNG file.") + return + + print(f"Processing single file: {os.path.basename(file_path)}") + resize_and_center_image(file_path) + print("Processing complete.") + +def main(): + parser = argparse.ArgumentParser(description="Resize PNG assets to 18x18px and center on 20x20px canvas") + parser.add_argument("path", help="Path to PNG file or directory containing PNG files") + parser.add_argument("--pattern", default="*.png", help="File pattern to match (default: *.png)") + + args = parser.parse_args() + + if os.path.isfile(args.path): + process_single_file(args.path) + elif os.path.isdir(args.path): + process_directory(args.path, args.pattern) + else: + print(f"Error: '{args.path}' is not a valid file or directory.") + +if __name__ == "__main__": + # If run without arguments, process the assets/Rat directory by default + import sys + if len(sys.argv) == 1: + # Default to processing the assets/Rat directory + script_dir = os.path.dirname(os.path.abspath(__file__)) + assets_dir = os.path.join(script_dir, "assets", "Rat") + if os.path.exists(assets_dir): + print("No arguments provided. Processing assets/Rat directory by default...") + process_directory(assets_dir) + else: + print("assets/Rat directory not found. Please provide a path as argument.") + print("Usage: python resize_assets.py ") + else: + main() diff --git a/sound/mine.wav b/sound/mine.wav new file mode 100644 index 0000000..5fd82a7 Binary files /dev/null and b/sound/mine.wav differ diff --git a/sound/mine_original.wav b/sound/mine_original.wav new file mode 100644 index 0000000..b1c0d81 Binary files /dev/null and b/sound/mine_original.wav differ diff --git a/units/__init__.py b/units/__init__.py index 6d3e24a..3315cc8 100644 --- a/units/__init__.py +++ b/units/__init__.py @@ -6,3 +6,4 @@ from .unit import Unit from .rat import Rat, Male, Female from .bomb import Bomb, Timer, Explosion from .points import Point +from .mine import Mine diff --git a/units/mine.py b/units/mine.py new file mode 100644 index 0000000..d0c380d --- /dev/null +++ b/units/mine.py @@ -0,0 +1,59 @@ +from .unit import Unit +from .bomb import Explosion +class Mine(Unit): + def __init__(self, game, position=(0,0), id=None): + super().__init__(game, position, id) + self.speed = 1.0 # Mine doesn't move but needs speed for consistency + self.armed = True # Mine is active and ready to explode + + def move(self): + """Mines don't move, but we need to check for collision with rats each frame.""" + pass + + def collisions(self): + """Check if a rat steps on the mine (has position_before on mine's position).""" + if not self.armed: + return + + # Check for rats that have position_before on this mine's position + for rat_unit in self.game.unit_positions_before.get(self.position, []): + if hasattr(rat_unit, 'sex'): # Check if it's a rat (rats have sex attribute) + # Mine explodes and kills the rat + self.explode(rat_unit) + break + + def explode(self, victim_rat): + """Mine explodes, killing the rat and destroying itself.""" + if not self.armed: + return + + print("MINE EXPLOSION!") + self.game.render_engine.play_sound("BOMB.WAV") + + # Kill the rat that stepped on the mine + if victim_rat.id in self.game.units: + victim_rat.die(score=5) + + # Remove the mine from the game + self.die() + + def draw(self): + """Draw the mine using the mine asset.""" + if not self.armed: + return + + # Use mine asset + image = self.game.assets["mine"] + image_size = self.game.render_engine.get_image_size(image) + + # Center the mine in the cell + x_pos = self.position[0] * self.game.cell_size + (self.game.cell_size - image_size[0]) // 2 + y_pos = self.position[1] * self.game.cell_size + (self.game.cell_size - image_size[1]) // 2 + + self.game.render_engine.draw_image(x_pos, y_pos, image, anchor="nw", tag="unit") + + def die(self, score=None): + """Remove mine from game and disarm it.""" + self.armed = False + Explosion(self.game, self.position).draw() + super().die(score)