From f4ca5bba5b13e9a2ddf362384aae6b611a2b5648 Mon Sep 17 00:00:00 2001 From: Matteo Benedetto Date: Thu, 14 Aug 2025 17:29:58 +0200 Subject: [PATCH] Enhance rendering system: improve screen resolution handling, add audio system initialization, and refine blood surface generation and blending --- README.md | 245 ++++++++++++++++++++++---- engine/sdl2.py | 456 ++++++++++++++++++++++++++++++------------------- 2 files changed, 498 insertions(+), 203 deletions(-) diff --git a/README.md b/README.md index bbab9bb..688b42a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # Mice! Mice! is a strategic game where players must kill rats with bombs before they reproduce and become too numerous. The game is a clone of the classic game Rats! for Windows 95. + ## Compatibility *It's developed in Python 3.11, please use it* + ## Features - **Maze Generation**: Randomly generated mazes using Depth First Search (DFS) algorithm. @@ -11,27 +13,189 @@ Mice! is a strategic game where players must kill rats with bombs before they re - **Sound Effects**: Audio feedback for various game events. - **Scoring**: Points system to track player progress. +## Engine Architecture + +The Mice! game engine is built on a modular architecture designed for flexibility and maintainability. The engine follows a component-based design pattern where different systems handle specific aspects of the game. + +### Core Engine Components + +#### 1. **Rendering System** (`engine/sdl2.py`) +- **GameWindow Class**: Central rendering manager using SDL2 +- **Features**: + - Hardware-accelerated rendering via SDL2 + - Texture management and caching + - Sprite rendering with transparency support + - Text rendering with custom fonts + - Resolution-independent scaling + - Fullscreen/windowed mode switching +- **Implementation**: + - Uses SDL2 renderer for efficient GPU-accelerated drawing + - Implements double buffering for smooth animation + - Manages texture atlas for optimized memory usage + - Handles viewport transformations for different screen resolutions + +#### 2. **Input System** (`engine/controls.py`) +- **KeyBindings Class**: Handles all user input +- **Features**: + - Keyboard input mapping and handling + - Joystick/gamepad support + - Configurable key bindings + - Input state management +- **Implementation**: + - Event-driven input processing + - Key state buffering for smooth movement + - Support for multiple input devices simultaneously + - Customizable control schemes + +#### 3. **Map System** (`engine/maze.py`) +- **Map Class**: Manages the game world structure +- **Features**: + - Maze data loading and parsing + - Collision detection system + - Tile-based world representation + - Pathfinding support for AI units +- **Implementation**: + - Grid-based coordinate system + - Efficient collision detection using spatial partitioning + - Support for different tile types (walls, floors, special tiles) + - Integration with maze generation algorithms + +#### 4. **Audio System** +- **Sound Management**: Handles all audio playback +- **Features**: + - Sound effect playback + - Background music support + - Volume control + - Multiple audio channels +- **Implementation**: + - Uses subprocess module for audio playback + - Asynchronous sound loading and playing + - Audio file format support (WAV, MP3, OGG) + +### Game Loop Architecture + +The main game loop follows the standard pattern: +1. **Input Processing**: Capture and process user input +2. **Update Phase**: Update game state, unit logic, and physics +3. **Render Phase**: Draw all game objects to the screen +4. **Timing Control**: Maintain consistent frame rate + +``` +Input → Update → Render → Present → Repeat +``` + +## Units Implementation + +The game uses an object-oriented approach for all game entities. Each unit type inherits from a base unit class and implements specific behaviors. + +### Base Unit Architecture + +All units share common properties and methods: +- **Position and Movement**: 2D coordinates with movement capabilities +- **Unique Identification**: UUID-based unique identifiers +- **Collision Detection**: Bounding box collision system +- **State Management**: Current state tracking (alive, dead, exploding, etc.) +- **Rendering**: Sprite-based visual representation + +### Unit Types Implementation + +#### 1. **Rat Units** (`units/rat.py`) + +**Base Rat Class**: +- **AI Behavior**: Implements pathfinding using A* algorithm +- **Movement**: Grid-based movement with smooth interpolation +- **State Machine**: Multiple states (wandering, fleeing, reproducing) + +**Male Rat Class**: +- **Reproduction Logic**: Seeks female rats for mating +- **Territorial Behavior**: Defends territory from other males +- **Lifespan Management**: Age-based death system + +**Female Rat Class**: +- **Pregnancy System**: Gestation period simulation +- **Offspring Generation**: Creates new rat units +- **Maternal Behavior**: Protects offspring from threats + +**Implementation Details**: +```python +# Simplified rat behavior structure +class Rat: + def update(self): + self.process_ai() # Decision making + self.handle_movement() # Position updates + self.check_collisions() # Collision detection + self.update_state() # State transitions +``` + +#### 2. **Bomb Units** (`units/bomb.py`) + +**Bomb Class**: +- **Timer System**: Countdown mechanism before explosion +- **Placement Logic**: Player-controlled positioning +- **Damage Calculation**: Blast radius and damage computation + +**Explosion Class**: +- **Visual Effects**: Animated explosion graphics +- **Damage Dealing**: Affects units within blast radius +- **Temporary Entity**: Self-destructs after animation + +**Implementation Details**: +- **State Machine**: Armed → Countdown → Exploding → Cleanup +- **Collision System**: Different collision behaviors per state +- **Effect Propagation**: Chain reaction support for multiple bombs + +#### 3. **Point Units** (`units/points.py`) + +**Point Class**: +- **Collection Mechanics**: Player interaction system +- **Value System**: Different point values for different achievements +- **Visual Feedback**: Pickup animations and effects + +### Unit Interaction System + +Units interact through a centralized collision and event system: + +1. **Collision Detection**: + - Grid-based broad phase for efficiency + - Precise bounding box narrow phase + - Custom collision responses per unit type pair + +2. **Event System**: + - Unit death events + - Reproduction events + - Explosion events + - Point collection events + +3. **AI Communication**: + - Shared pathfinding data + - Pheromone trail system for rat behavior + - Danger awareness (bombs, explosions) + ## Technical Details -- **Language**: Python 3 +- **Language**: Python 3.11 - **Libraries**: - `sdl2` for graphics and window management - `Pillow` for image processing - `uuid` for unique unit identification - `subprocess` for playing sound effects - `tkinter` for maze generation visualization -- **Game Loop**: The game uses a main loop to handle events, update game state, and render graphics. -- **Collision Detection**: Each unit checks for collisions with other units and walls. -- **Sound Management**: Sound effects are managed using the `subprocess` module to play audio files. -- **Environment Variables**: - - `SDL_VIDEODRIVER` to set the video driver - - `RESOLUTION` to set the screen resolution -- **Engine**: The game engine is built using SDL2, providing efficient rendering and handling of game events. The engine supports: - - **Image Loading**: Using `Pillow` to load and process images. - - **Text Rendering**: Custom fonts and text rendering using SDL2's text capabilities. - - **Sound Playback**: Integration with SDL2's audio features for sound effects. - - **Joystick Support**: Handling joystick input for game controls. - - **Window Management**: Fullscreen and windowed modes, with adjustable resolution. +- **Performance Optimizations**: + - Spatial partitioning for collision detection + - Texture atlasing for reduced memory usage + - Object pooling for frequently created/destroyed units + - Delta time-based updates for frame rate independence +- **Memory Management**: + - Automatic cleanup of dead units + - Texture caching and reuse + - Efficient data structures for large numbers of units + +## Environment Variables + +- `SDL_VIDEODRIVER`: Set the video driver (x11, wayland, etc.) +- `RESOLUTION`: Set the screen resolution (format: WIDTHxHEIGHT) +- `FULLSCREEN`: Enable/disable fullscreen mode (true/false) +- `SOUND_ENABLED`: Enable/disable sound effects (true/false) ## Installation @@ -54,21 +218,42 @@ Mice! is a strategic game where players must kill rats with bombs before they re python rats.py ``` -## Project Files - -- `maze.py`: Contains the `MazeGenerator` class for generating and visualizing the maze. -- `rats.py`: Main game file that initializes the game and handles game logic. -- `engine/controls.py`: Contains the `KeyBindings` class for handling keyboard input. -- `engine/maze.py`: Contains the `Map` class for loading and managing the maze structure. -- `engine/sdl2.py`: Contains the `GameWindow` class for SDL2 window management and rendering. -- `units/bomb.py`: Contains the `Bomb` and `Explosion` classes for bomb units. -- `units/rat.py`: Contains the `Rat`, `Male`, and `Female` classes for rat units. -- `units/points.py`: Contains the `Point` class for point units. -- `assets/`: Directory containing game assets such as images and fonts. -- `sound/`: Directory containing sound effects. -- `README.md`: This file, containing information about the project. -- `requirements.txt`: Lists the Python dependencies for the project. -- `.env`: Environment variables for the project. -- `.gitignore`: Specifies files and directories to be ignored by Git. -- `scores.txt`: File for storing high scores. +## Project Structure + +``` +mice/ +├── engine/ # Core engine components +│ ├── controls.py # Input handling system +│ ├── maze.py # Map and collision system +│ └── sdl2.py # Rendering and window management +├── units/ # Game entity implementations +│ ├── bomb.py # Bomb and explosion logic +│ ├── rat.py # Rat AI and behavior +│ └── points.py # Collectible points +├── assets/ # Game resources +│ ├── images/ # Sprites and textures +│ └── fonts/ # Text rendering fonts +├── sound/ # Audio files +├── maze.py # Maze generation algorithms +├── rats.py # Main game entry point +├── requirements.txt # Python dependencies +├── .env # Environment configuration +└── README.md # This documentation +``` + +## Game Files Details + +- `maze.py`: Contains the `MazeGenerator` class implementing DFS algorithm for procedural maze generation +- `rats.py`: Main game controller, initializes engine systems and manages game state +- `engine/controls.py`: Input abstraction layer with configurable key bindings +- `engine/maze.py`: World representation with collision detection and pathfinding support +- `engine/sdl2.py`: Low-level graphics interface wrapping SDL2 functionality +- `units/bomb.py`: Explosive units with timer mechanics and blast radius calculations +- `units/rat.py`: AI-driven entities with reproduction, pathfinding, and survival behaviors +- `units/points.py`: Collectible scoring items with visual feedback systems +- `assets/`: Game resources including sprites, textures, and fonts +- `sound/`: Audio assets for game events and feedback +- `scores.txt`: Persistent high score storage +- `.env`: Runtime configuration and environment settings +- `.gitignore`: Version control exclusion rules diff --git a/engine/sdl2.py b/engine/sdl2.py index 6a7b87c..060b5dd 100644 --- a/engine/sdl2.py +++ b/engine/sdl2.py @@ -1,24 +1,28 @@ import os import random +import ctypes +from ctypes import * + import sdl2 import sdl2.ext from sdl2.ext.compat import byteify -from ctypes import * -import ctypes - -from PIL import Image from sdl2 import SDL_AudioSpec +from PIL import Image class GameWindow: def __init__(self, width, height, cell_size, title="Default", key_callback=None): - + # Display configuration self.cell_size = cell_size self.width = width * cell_size self.height = height * cell_size + + # Screen resolution handling actual_screen_size = os.environ.get("RESOLUTION", "640x480").split("x") actual_screen_size = tuple(map(int, actual_screen_size)) self.target_size = actual_screen_size if self.width > actual_screen_size[0] or self.height > actual_screen_size[1] else (self.width, self.height) + + # View offset calculations self.w_start_offset = (self.target_size[0] - self.width) // 2 self.h_start_offset = (self.target_size[1] - self.height) // 2 self.w_offset = self.w_start_offset @@ -26,32 +30,56 @@ class GameWindow: self.max_w_offset = self.target_size[0] - self.width self.max_h_offset = self.target_size[1] - self.height self.scale = self.target_size[1] // self.cell_size + print(f"Screen size: {self.width}x{self.height}") + # SDL2 initialization sdl2.ext.init(joystick=True) sdl2.SDL_Init(sdl2.SDL_INIT_AUDIO) - self.window = sdl2.ext.Window(title=title, size=self.target_size,)# flags=sdl2.SDL_WINDOW_FULLSCREEN) - self.delay = 30 - self.load_joystick() + # Window and renderer setup + self.window = sdl2.ext.Window(title=title, size=self.target_size) self.window.show() self.renderer = sdl2.ext.Renderer(self.window, flags=sdl2.SDL_RENDERER_ACCELERATED) self.factory = sdl2.ext.SpriteFactory(renderer=self.renderer) + + # Font system self.fonts = self.generate_fonts("assets/decterm.ttf") + + # Initial loading dialog self.dialog("Loading assets...") self.renderer.present() + + # Game state self.running = True - self.key_down, self.key_up, self.axis_scroll = key_callback + self.delay = 30 self.performance = 0 - self.audio_devs = {} + + # Input handling + self.key_down, self.key_up, self.axis_scroll = key_callback self.button_cursor = [0, 0] self.buttons = {} - self.audio_devs["base"] = sdl2.SDL_OpenAudioDevice(None, 0, SDL_AudioSpec(freq=22050, aformat=sdl2.AUDIO_U8, channels=1, samples=2048), None, 0) - self.audio_devs["effects"] = sdl2.SDL_OpenAudioDevice(None, 0, SDL_AudioSpec(freq=22050, aformat=sdl2.AUDIO_U8, channels=1, samples=2048), None, 0) - self.audio_devs["music"] = sdl2.SDL_OpenAudioDevice(None, 0, SDL_AudioSpec(freq=22050, aformat=sdl2.AUDIO_U8, channels=1, samples=2048), None, 0) + + # Audio system initialization + self._init_audio_system() + + # Input devices + self.load_joystick() + + def _init_audio_system(self): + """Initialize audio devices for different audio channels""" + audio_spec = SDL_AudioSpec(freq=22050, aformat=sdl2.AUDIO_U8, channels=1, samples=2048) + self.audio_devs = {} + self.audio_devs["base"] = sdl2.SDL_OpenAudioDevice(None, 0, audio_spec, None, 0) + self.audio_devs["effects"] = sdl2.SDL_OpenAudioDevice(None, 0, audio_spec, None, 0) + self.audio_devs["music"] = sdl2.SDL_OpenAudioDevice(None, 0, audio_spec, None, 0) + + # ====================== + # TEXTURE & IMAGE METHODS + # ====================== def create_texture(self, tiles: list): - # Always create a fresh surface since we free it after use + """Create a texture from a list of tiles""" bg_surface = sdl2.SDL_CreateRGBSurface(0, self.width, self.height, 32, 0, 0, 0, 0) for tile in tiles: dstrect = sdl2.SDL_Rect(tile[1], tile[2], self.cell_size, self.cell_size) @@ -60,20 +88,12 @@ class GameWindow: sdl2.SDL_FreeSurface(bg_surface) return bg_texture - def load_joystick(self): - sdl2.SDL_Init(sdl2.SDL_INIT_JOYSTICK) - sdl2.SDL_JoystickOpen(0) - - - def generate_fonts(self,font_file): - fonts = {} - for i in range(10, 70, 1): - fonts.update({i: sdl2.ext.FontManager(font_path=font_file, size=i)}) - return fonts - def load_image(self, path, transparent_color=None, surface=False): + """Load and process an image with optional transparency and scaling""" image_path = os.path.join("assets", path) image = Image.open(image_path) + + # Handle transparency if transparent_color: image = image.convert("RGBA") datas = image.getdata() @@ -84,99 +104,231 @@ class GameWindow: else: new_data.append(item) image.putdata(new_data) + + # Scale image scale = self.cell_size // 20 - if surface: - return sdl2.ext.pillow_to_surface(image.resize((image.width * scale, image.height * scale), Image.NEAREST)) image = image.resize((image.width * scale, image.height * scale), Image.NEAREST) + + if surface: + return sdl2.ext.pillow_to_surface(image) return self.factory.from_surface(sdl2.ext.pillow_to_surface(image)) + + def get_image_size(self, image): + """Get the size of an image sprite""" + return image.size + + # ====================== + # FONT MANAGEMENT + # ====================== + + def generate_fonts(self, font_file): + """Generate font managers for different sizes""" + fonts = {} + for i in range(10, 70, 1): + fonts.update({i: sdl2.ext.FontManager(font_path=font_file, size=i)}) + return fonts + + # ====================== + # DRAWING METHODS + # ====================== def draw_text(self, text, font, position, color): + """Draw text at specified position with given font and color""" sprite = self.factory.from_text(text, color=color, fontmanager=font) + + # Handle center positioning if position == "center": position = ("center", "center") if position[0] == "center": position = (self.target_size[0] // 2 - sprite.size[0] // 2, position[1]) if position[1] == "center": position = (position[0], self.target_size[1] // 2 - sprite.size[1] // 2) + sprite.position = position - #print(sprite.position) self.renderer.copy(sprite, dstrect=sprite.position) - + def draw_background(self, bg_texture): + """Draw background texture with current view offset""" self.renderer.copy(bg_texture, dstrect=sdl2.SDL_Rect(self.w_offset, self.h_offset, self.width, self.height)) + def draw_image(self, x, y, sprite, tag, anchor="nw"): + """Draw an image sprite at specified coordinates""" if not self.is_in_visible_area(x, y): return - sprite.position = (x+self.w_offset, y+self.h_offset) + sprite.position = (x + self.w_offset, y + self.h_offset) self.renderer.copy(sprite, dstrect=sprite.position) - - + def draw_rectangle(self, x, y, width, height, tag, outline="red", filling=None): + """Draw a rectangle with optional fill and outline""" if filling: self.renderer.fill((x, y, width, height), sdl2.ext.Color(*filling)) else: self.renderer.draw_rect((x, y, width, height), sdl2.ext.Color(*outline)) - + def draw_pointer(self, x, y): - x=x+self.w_offset - y=y+self.h_offset + """Draw a red pointer rectangle at specified coordinates""" + x = x + self.w_offset + y = y + self.h_offset for i in range(3): - self.renderer.draw_rect((x + i,y+i, self.cell_size-2*i, self.cell_size-2*i), color=sdl2.ext.Color(255, 0, 0)) - + self.renderer.draw_rect((x + i, y + i, self.cell_size - 2*i, self.cell_size - 2*i), + color=sdl2.ext.Color(255, 0, 0)) + def delete_tag(self, tag): + """Placeholder for tag deletion (not implemented)""" pass - + + # ====================== + # UI METHODS + # ====================== + def dialog(self, text, **kwargs): - self.draw_rectangle(50, 50, - self.target_size[0] - 100, self.target_size[1] - 100, "win", filling=(255, 255, 255)) + """Display a dialog box with text and optional extras""" + # Draw dialog background + self.draw_rectangle(50, 50, + self.target_size[0] - 100, self.target_size[1] - 100, + "win", filling=(255, 255, 255)) + + # Draw main text self.draw_text(text, self.fonts[self.target_size[1]//20], "center", sdl2.ext.Color(0, 0, 0)) + + # Draw subtitle if provided if subtitle := kwargs.get("subtitle"): - self.draw_text(subtitle, self.fonts[self.target_size[1]//30], ("center", self.target_size[1] // 2 + 50), sdl2.ext.Color(0, 0, 0)) + self.draw_text(subtitle, self.fonts[self.target_size[1]//30], + ("center", self.target_size[1] // 2 + 50), sdl2.ext.Color(0, 0, 0)) + + # Draw image if provided if image := kwargs.get("image"): image_size = self.get_image_size(image) self.draw_image(self.target_size[0] // 2 - image_size[0] // 2 - self.w_offset, - self.target_size[1] // 2 - image_size[1] * 2 - self.h_offset, - image, "win") + self.target_size[1] // 2 - image_size[1] * 2 - self.h_offset, + image, "win") + + # Draw scores if provided if scores := kwargs.get("scores"): - #self.draw_text("Scores:", self.fonts[self.target_size[1]//20], (self.target_size[0] // 2 - 50, self.target_size[1] // 2 + 50), sdl2.ext.Color(0, 0, 0)) - sprite = self.factory.from_text("Scores:", color=sdl2.ext.Color(0, 0, 0), fontmanager=self.fonts[self.target_size[1]//20]) + sprite = self.factory.from_text("Scores:", color=sdl2.ext.Color(0, 0, 0), + fontmanager=self.fonts[self.target_size[1]//20]) sprite.position = (self.target_size[0] // 2 - 50, self.target_size[1] // 2 + 30) self.renderer.copy(sprite, dstrect=sprite.position) + for i, score in enumerate(scores[:5]): - score = " - ".join(score) - self.draw_text(score, self.fonts[self.target_size[1]//40], ("center", self.target_size[1] // 2 + 50 + 30 * (i + 1)), sdl2.ext.Color(0, 0, 0)) + score_text = " - ".join(score) + self.draw_text(score_text, self.fonts[self.target_size[1]//40], + ("center", self.target_size[1] // 2 + 50 + 30 * (i + 1)), + sdl2.ext.Color(0, 0, 0)) + + def start_dialog(self, **kwargs): + """Display the welcome dialog""" + self.dialog("Welcome to the Mice!", subtitle="A game by Matteo because was bored", **kwargs) + + def draw_button(self, x, y, text, width, height, coords): + """Draw a button with text""" + # TODO: Fix outline parameter usage + color = (0, 0, 255) if self.button_cursor == list(coords) else (0, 0, 0) + self.draw_rectangle(x, y, width, height, "button", outline=color) + self.draw_text(text, self.fonts[20], (x + 10, y + 10), (0, 0, 0)) - def get_image_size(self, image): - return image.size - def update_status(self, text): + """Update and display the status bar with FPS information""" fps = int(1000 / self.performance) if self.performance != 0 else 0 - text = f"FPS: {fps} - {text}" + status_text = f"FPS: {fps} - {text}" font = self.fonts[20] - sprite = self.factory.from_text(text, color=sdl2.ext.Color(0, 0, 0), fontmanager=font) + sprite = self.factory.from_text(status_text, color=sdl2.ext.Color(0, 0, 0), fontmanager=font) text_width, text_height = sprite.size + + # Draw background for status text self.renderer.fill((3, 3, text_width + 10, text_height + 4), sdl2.ext.Color(255, 255, 255)) - self.draw_text(text, font, (8, 5), sdl2.ext.Color(0, 0, 0)) + self.draw_text(status_text, font, (8, 5), sdl2.ext.Color(0, 0, 0)) + + # ====================== + # VIEW & NAVIGATION + # ====================== + + def scroll_view(self, pointer): + """Adjust the view offset based on pointer coordinates""" + x, y = pointer - def new_cycle(self, delay, callback): - pass - - def full_screen(self,flag): - sdl2.SDL_SetWindowFullscreen(self.window.window, flag) + # Scale down and invert coordinates + x = -(x // 2) * self.cell_size + y = -(y // 2) * self.cell_size + + # Clamp horizontal offset to valid range + if x <= self.max_w_offset + self.cell_size: + x = self.max_w_offset + # Clamp vertical offset to valid range + if y < self.max_h_offset: + y = self.max_h_offset + + self.w_offset = x + self.h_offset = y + def is_in_visible_area(self, x, y): - return -self.w_offset -self.cell_size <= x <= self.width - self.w_offset and -self.h_offset -self.cell_size <= y <= self.height - self.h_offset - - def get_perf_counter(self): - return sdl2.SDL_GetPerformanceCounter() - + """Check if coordinates are within the visible area""" + return (-self.w_offset - self.cell_size <= x <= self.width - self.w_offset and + -self.h_offset - self.cell_size <= y <= self.height - self.h_offset) + + def get_view_center(self): + """Get the center coordinates of the current view""" + return self.w_offset + self.width // 2, self.h_offset + self.height // 2 + + # ====================== + # AUDIO METHODS + # ====================== + + def play_sound(self, sound_file, tag="base"): + """Play a sound file on the specified audio channel""" + sound_path = os.path.join("sound", sound_file) + rw = sdl2.SDL_RWFromFile(byteify(sound_path, "utf-8"), b"rb") + if not rw: + raise RuntimeError("Failed to open sound file") + + _buf = POINTER(sdl2.Uint8)() + _length = sdl2.Uint32() + + spec = SDL_AudioSpec(freq=22050, aformat=sdl2.AUDIO_U8, channels=1, samples=2048) + if sdl2.SDL_LoadWAV_RW(rw, 1, byref(spec), byref(_buf), byref(_length)) == None: + raise RuntimeError("Failed to load WAV") + + devid = self.audio_devs[tag] + # Clear any queued audio + sdl2.SDL_ClearQueuedAudio(devid) + # Start playing audio + sdl2.SDL_QueueAudio(devid, _buf, _length) + sdl2.SDL_PauseAudioDevice(devid, 0) + + def stop_sound(self): + """Stop all audio playback""" + for dev in self.audio_devs.values(): + sdl2.SDL_PauseAudioDevice(dev, 1) + sdl2.SDL_ClearQueuedAudio(dev) + + # ====================== + # INPUT METHODS + # ====================== + + def load_joystick(self): + """Initialize joystick support""" + sdl2.SDL_Init(sdl2.SDL_INIT_JOYSTICK) + sdl2.SDL_JoystickOpen(0) + + # ====================== + # MAIN GAME LOOP + # ====================== + def mainloop(self, **kwargs): + """Main game loop handling events and rendering""" while self.running: performance_start = sdl2.SDL_GetPerformanceCounter() self.renderer.clear() + + # Execute background update if provided if "bg_update" in kwargs: kwargs["bg_update"]() + + # Execute main update kwargs["update"]() + + # Handle SDL events events = sdl2.ext.get_events() for event in events: if event.type == sdl2.SDL_QUIT: @@ -184,10 +336,9 @@ class GameWindow: elif event.type == sdl2.SDL_KEYDOWN and self.key_down: key = sdl2.SDL_GetKeyName(event.key.keysym.sym).decode('utf-8') self.key_down(key) - elif event.type == sdl2.SDL_KEYUP and self.key_down: + elif event.type == sdl2.SDL_KEYUP and self.key_up: key = sdl2.SDL_GetKeyName(event.key.keysym.sym).decode('utf-8') self.key_up(key) - print(key) elif event.type == sdl2.SDL_MOUSEMOTION: self.key_down("mouse", coords=(event.motion.x, event.motion.y)) elif event.type == sdl2.SDL_JOYBUTTONDOWN: @@ -196,96 +347,57 @@ class GameWindow: elif event.type == sdl2.SDL_JOYBUTTONUP: key = event.jbutton.button self.key_up(key) - # elif event.type == sdl2.SDL_JOYAXISMOTION: - # self.axis_scroll(event.jaxis.axis, event.jaxis.value) - - - # Disegna qui gli sprite - #rect = sdl2.SDL_Rect(self.w_offset, self.h_offset, self.target_size[0], self.target_size[1]) - #sdl2.SDL_RenderSetClipRect(self.renderer.sdlrenderer, rect) + + # Present the rendered frame self.renderer.present() - self.performance = (sdl2.SDL_GetPerformanceCounter() - performance_start) / sdl2.SDL_GetPerformanceFrequency() * 1000 - if self.performance < self.delay: - delay = self.delay - round(self.performance) - else: - delay = 0 - sdl2.SDL_Delay(delay) - def close(self): - self.running = False - sdl2.ext.quit() + # Calculate performance and delay + self.performance = ((sdl2.SDL_GetPerformanceCounter() - performance_start) / + sdl2.SDL_GetPerformanceFrequency() * 1000) + + delay = max(0, self.delay - round(self.performance)) + sdl2.SDL_Delay(delay) - def scroll_view(self, pointer): - """ - Adjusts the view offset based on the given pointer coordinates. - Scales them down by half, then adjusts offsets, ensuring they don't - exceed maximum allowed values. - """ - x, y = pointer - - # Scale down and invert - x = -(x // 2) * self.cell_size - y = -(y // 2) * self.cell_size - - # Clamp horizontal offset - if x <= self.max_w_offset + self.cell_size: - x = self.max_w_offset - + # ====================== + # SPECIAL EFFECTS + # ====================== - # Clamp vertical offset - if y < self.max_h_offset: - y = self.max_h_offset + # ====================== + # UTILITY METHODS + # ====================== - self.w_offset = x - self.h_offset = y - - def play_sound(self, sound_file, tag="base"): - sound_file = os.path.join("sound", sound_file) - rw = sdl2.SDL_RWFromFile(byteify(sound_file, "utf-8"), b"rb") - if not rw: - raise RuntimeError("Failed to open sound file") + def new_cycle(self, delay, callback): + """Placeholder for cycle management (not implemented)""" + pass - _buf = POINTER(sdl2.Uint8)() - _length = sdl2.Uint32() + def full_screen(self, flag): + """Toggle fullscreen mode""" + sdl2.SDL_SetWindowFullscreen(self.window.window, flag) - spec = SDL_AudioSpec(freq=22050, aformat=sdl2.AUDIO_U8, channels=1, samples=2048) - if sdl2.SDL_LoadWAV_RW(rw, 1, byref(spec), byref(_buf), byref(_length)) == None: - raise RuntimeError("Failed to load WAV") - devid = self.audio_devs[tag] - # Clear any queued audio - sdl2.SDL_ClearQueuedAudio(devid) + def get_perf_counter(self): + """Get performance counter for timing""" + return sdl2.SDL_GetPerformanceCounter() - # Start playing audio - sdl2.SDL_QueueAudio(devid, _buf, _length) - sdl2.SDL_PauseAudioDevice(devid, 0) - - def stop_sound(self): - for dev in self.audio_devs: - if not dev[0]: - sdl2.SDL_PauseAudioDevice(dev[1], 1) - sdl2.SDL_ClearQueuedAudio(dev[1]) - - def start_dialog(self, **kwargs): - self.dialog("Welcome to the Mice!", subtitle="A game by Matteo because was bored", **kwargs) - center = self.get_view_center() - #self.draw_button(center[0], center[1] + 10 * self.scale, "Start", 120, 50, (0, 0)) - - def draw_button(self, x, y, text, width, height, coords): - if self.button_cursor[0] == coords[0] and self.button_cursor[1] == coords[1]: - color = (0, 0, 255) - self.draw_rectangle(x, y, width, height, "button", outline8u=color) - self.draw_text(text, self.fonts[20], (x + 10, y + 10), (0,0,0)) + def close(self): + """Close the game window and cleanup""" + self.running = False + sdl2.ext.quit() - def get_view_center(self): - return self.w_offset + self.width // 2, self.h_offset + self.height // 2 + # ====================== + # MAIN GAME LOOP + # ====================== + # ====================== + # SPECIAL EFFECTS + # ====================== + def generate_blood_surface(self): - """Genera dinamicamente una superficie di macchia di sangue usando SDL2""" + """Generate a dynamic blood splatter surface using SDL2""" size = self.cell_size - # Crea una superficie RGBA per la macchia di sangue + # Create RGBA surface for blood splatter blood_surface = sdl2.SDL_CreateRGBSurface( 0, size, size, 32, 0x000000FF, # R mask @@ -297,58 +409,56 @@ class GameWindow: if not blood_surface: return None - # Blocca la superficie per il disegno pixel per pixel + # Lock surface for pixel manipulation sdl2.SDL_LockSurface(blood_surface) - # Ottieni i dati dei pixel + # Get pixel data pixels = cast(blood_surface.contents.pixels, POINTER(c_uint32)) - pitch = blood_surface.contents.pitch // 4 # pitch in pixel (32-bit) + pitch = blood_surface.contents.pitch // 4 # Convert pitch to pixels (32-bit) - # Colori del sangue (variazioni di rosso in formato ABGR) + # Blood color variations (ABGR format) blood_colors = [ - 0xFF00008B, # Rosso scuro (A=FF, B=00, G=00, R=8B) - 0xFF002222, # Rosso mattone (A=FF, B=00, G=22, R=22) - 0xFF003C14, # Cremisi (A=FF, B=00, G=3C, R=14) - 0xFF0000FF, # Rosso puro (A=FF, B=00, G=00, R=FF) - 0xFF000080, # Marrone rossastro (A=FF, B=00, G=00, R=80) + 0xFF00008B, # Dark red + 0xFF002222, # Brick red + 0xFF003C14, # Crimson + 0xFF0000FF, # Pure red + 0xFF000080, # Reddish brown ] - # Genera la macchia con un algoritmo di diffusione + # Generate splatter with diffusion algorithm center_x, center_y = size // 2, size // 2 - - # Inizia dal centro e espandi verso l'esterno max_radius = size // 3 + random.randint(-3, 5) for y in range(size): for x in range(size): - # Calcola la distanza dal centro + # Calculate distance from center distance = ((x - center_x) ** 2 + (y - center_y) ** 2) ** 0.5 - # Probabilità di avere sangue basata sulla distanza + # Calculate blood probability based on distance if distance <= max_radius: - # Più vicino al centro, più probabile avere sangue + # Closer to center = higher probability probability = max(0, 1 - (distance / max_radius)) - # Aggiungi rumore per forma irregolare + # Add noise for irregular shape noise = random.random() * 0.7 if random.random() < probability * noise: - # Scegli un colore di sangue casuale + # Choose random blood color color = random.choice(blood_colors) - # Aggiungi variazione di alpha per trasparenza + # Add alpha variation for transparency alpha = int(255 * probability * random.uniform(0.6, 1.0)) color = (color & 0x00FFFFFF) | (alpha << 24) pixels[y * pitch + x] = color else: - # Pixel trasparente + # Transparent pixel pixels[y * pitch + x] = 0x00000000 else: - # Fuori dal raggio, trasparente + # Outside radius, transparent pixels[y * pitch + x] = 0x00000000 - # Aggiungi alcune gocce sparse intorno alla macchia principale + # Add scattered droplets around main splatter for _ in range(random.randint(3, 8)): drop_x = center_x + random.randint(-max_radius - 5, max_radius + 5) drop_y = center_y + random.randint(-max_radius - 5, max_radius + 5) @@ -360,36 +470,36 @@ class GameWindow: nx, ny = drop_x + dx, drop_y + dy if 0 <= nx < size and 0 <= ny < size: if random.random() < 0.6: - color = random.choice(blood_colors[:3]) # Colori più scuri per le gocce + color = random.choice(blood_colors[:3]) # Darker colors for drops alpha = random.randint(100, 200) color = (color & 0x00FFFFFF) | (alpha << 24) pixels[ny * pitch + nx] = color - # Sblocca la superficie + # Unlock surface sdl2.SDL_UnlockSurface(blood_surface) - # Converte la superficie in una texture usando il factory del gioco return blood_surface def draw_blood_surface(self, blood_surface, position): - # Create a new surface for the blood texture since bg_surface may have been freed + """Convert blood surface to texture and return it""" + # Create temporary surface for blood texture temp_surface = sdl2.SDL_CreateRGBSurface(0, self.cell_size, self.cell_size, 32, 0, 0, 0, 0) if temp_surface is None: sdl2.SDL_FreeSurface(blood_surface) return None - # Copy the blood surface to the temporary surface + # Copy blood surface to temporary surface sdl2.SDL_BlitSurface(blood_surface, None, temp_surface, None) sdl2.SDL_FreeSurface(blood_surface) - # Create texture from the temporary surface + # Create texture from temporary surface texture = self.factory.from_surface(temp_surface) sdl2.SDL_FreeSurface(temp_surface) return texture def combine_blood_surfaces(self, existing_surface, new_surface): """Combine two blood surfaces by blending them together""" - # Create a new surface for the combined result + # Create combined surface combined_surface = sdl2.SDL_CreateRGBSurface( 0, self.cell_size, self.cell_size, 32, 0x000000FF, # R mask @@ -401,7 +511,7 @@ class GameWindow: if combined_surface is None: return existing_surface - # Lock both surfaces for pixel manipulation + # Lock surfaces for pixel manipulation sdl2.SDL_LockSurface(existing_surface) sdl2.SDL_LockSurface(new_surface) sdl2.SDL_LockSurface(combined_surface) @@ -411,9 +521,9 @@ class GameWindow: new_pixels = cast(new_surface.contents.pixels, POINTER(c_uint32)) combined_pixels = cast(combined_surface.contents.pixels, POINTER(c_uint32)) - pitch = combined_surface.contents.pitch // 4 # pitch in pixels (32-bit) + pitch = combined_surface.contents.pitch // 4 # Convert pitch to pixels (32-bit) - # Combine pixels manually for better blending + # Combine pixels with additive blending for y in range(self.cell_size): for x in range(self.cell_size): idx = y * pitch + x @@ -432,7 +542,7 @@ class GameWindow: new_g = (new_pixel >> 8) & 0xFF new_b = new_pixel & 0xFF - # Blend the colors (additive blending for blood accumulation) + # Blend colors (additive blending for blood accumulation) if new_a > 0: # If new pixel has color if existing_a > 0: # If existing pixel has color # Combine both colors, making it darker/more opaque @@ -462,7 +572,7 @@ class GameWindow: sdl2.SDL_UnlockSurface(combined_surface) return combined_surface - + def free_surface(self, surface): """Safely free an SDL surface""" if surface is not None: