import os import random import ctypes from ctypes import * import sdl2 import sdl2.ext from sdl2.ext.compat import byteify 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 self.h_offset = self.h_start_offset 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) # 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.delay = 30 self.performance = 0 self.last_status_text = "" self.stats_sprite = None self.mean_fps = 0 self.fpss = [] self.text_width = 0 self.text_height = 0 self.ammo_text = "" # White flash effect state self.white_flash_active = False self.white_flash_start_time = 0 self.white_flash_opacity = 255 # Input handling self.key_down, self.key_up, self.axis_scroll = key_callback self.button_cursor = [0, 0] self.buttons = {} # 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): """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) sdl2.SDL_BlitSurface(tile[0], None, bg_surface, dstrect) bg_texture = self.factory.from_surface(bg_surface) sdl2.SDL_FreeSurface(bg_surface) return bg_texture 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() new_data = [] for item in datas: if item[:3] == transparent_color: new_data.append((255, 255, 255, 0)) else: new_data.append(item) image.putdata(new_data) # Scale image scale = self.cell_size // 20 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 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=None, 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) 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): """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)) def delete_tag(self, tag): """Placeholder for tag deletion (not implemented)""" pass # ====================== # UI METHODS # ====================== def dialog(self, text, **kwargs): """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)) # 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") # Draw scores if provided if scores := kwargs.get("scores"): 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_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 update_status(self, text): """Update and display the status bar with FPS information""" fps = int(1000 / self.performance) if self.performance != 0 else 0 # at 10% of probability print fps if len(self.fpss) > 20: self.mean_fps = round(sum(self.fpss) / len(self.fpss)) if self.fpss else fps #print(f"FPS: {self.mean_fps}") self.fpss.clear() else: self.fpss.append(fps) status_text = f"FPS: {self.mean_fps} - {text}" if status_text != self.last_status_text: self.last_status_text = status_text font = self.fonts[20] self.stats_sprite = self.factory.from_text(status_text, color=sdl2.ext.Color(0, 0, 0), fontmanager=font) if self.text_width != self.stats_sprite.size[0] or self.text_height != self.stats_sprite.size[1]: self.text_width, self.text_height = self.stats_sprite.size # create a background for the status text using texture self.stats_background = self.factory.from_color(sdl2.ext.Color(255, 255, 255), (self.text_width + 10, self.text_height + 4)) # self.renderer.fill((3, 3, self.text_width + 10, self.text_height + 4), sdl2.ext.Color(255, 255, 255)) self.renderer.copy(self.stats_background, dstrect=sdl2.SDL_Rect(3, 3, self.text_width + 10, self.text_height + 4)) self.renderer.copy(self.stats_sprite, dstrect=sdl2.SDL_Rect(8, 5, self.text_width, self.text_height)) def update_ammo(self, ammo, assets): """Update and display the ammo count""" ammo_text = f"{ammo['bomb']['count']}/{ammo['bomb']['max']} {ammo['mine']['count']}/{ammo['mine']['max']} {ammo['nuclear']['count']}/{ammo['nuclear']['max']} " if self.ammo_text != ammo_text: self.ammo_text = ammo_text font = self.fonts[20] self.ammo_sprite = self.factory.from_text(ammo_text, color=sdl2.ext.Color(0, 0, 0), fontmanager=font) text_width, text_height = self.ammo_sprite.size self.ammo_background = self.factory.from_color(sdl2.ext.Color(255, 255, 255), (text_width + 10, text_height + 4)) text_width, text_height = self.ammo_sprite.size position = (self.target_size[0] - text_width - 10, self.target_size[1] - text_height - 5) #self.renderer.fill((position[0] - 5, position[1] - 2, text_width + 10, text_height + 4), sdl2.ext.Color(255, 255, 255)) self.renderer.copy(self.ammo_background, dstrect=sdl2.SDL_Rect(position[0] - 5, position[1] - 2, text_width + 10, text_height + 4)) self.renderer.copy(self.ammo_sprite, dstrect=sdl2.SDL_Rect(position[0], position[1], text_width, text_height)) self.renderer.copy(assets["BMP_BOMB0"], dstrect=sdl2.SDL_Rect(position[0]+25, position[1], 20, 20)) self.renderer.copy(assets["mine"], dstrect=sdl2.SDL_Rect(position[0]+80, position[1], 20, 20)) self.renderer.copy(assets["BMP_NUCLEAR"], dstrect=sdl2.SDL_Rect(position[0]+130, position[1], 20, 20)) # ====================== # VIEW & NAVIGATION # ====================== def scroll_view(self, pointer): """Adjust the view offset based on pointer coordinates""" x, y = pointer # 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): """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"]() # Update and draw white flash effect if self.update_white_flash(): self.draw_white_flash() # Handle SDL events events = sdl2.ext.get_events() for event in events: if event.type == sdl2.SDL_QUIT: self.running = False elif event.type == sdl2.SDL_KEYDOWN and self.key_down: # print in file keycode keycode = event.key.keysym.sym open("keycode.txt", "a").write(f"{keycode}\n") key = sdl2.SDL_GetKeyName(event.key.keysym.sym).decode('utf-8') # Check for Right Ctrl key to trigger white flash if event.key.keysym.sym == sdl2.SDLK_RCTRL: self.trigger_white_flash() else: self.key_down(key) 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) elif event.type == sdl2.SDL_MOUSEMOTION: self.key_down("mouse", coords=(event.motion.x, event.motion.y)) elif event.type == sdl2.SDL_JOYBUTTONDOWN: key = event.jbutton.button self.key_down(key) elif event.type == sdl2.SDL_JOYBUTTONUP: key = event.jbutton.button self.key_up(key) # Present the rendered frame self.renderer.present() # 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) # ====================== # SPECIAL EFFECTS # ====================== def trigger_white_flash(self): """Trigger the white flash effect""" self.white_flash_active = True self.white_flash_start_time = sdl2.SDL_GetTicks() self.white_flash_opacity = 255 def update_white_flash(self): """Update the white flash effect and return True if it should be drawn""" if not self.white_flash_active: return False current_time = sdl2.SDL_GetTicks() elapsed_time = current_time - self.white_flash_start_time if elapsed_time < 1000: # First 1 second : full white self.white_flash_opacity = 255 return True elif elapsed_time < 3000: # Next 2 seconds: fade out # Calculate fade based on remaining time (2000ms fade duration) fade_progress = (elapsed_time - 1000) / 2000.0 # 0.0 to 1.0 self.white_flash_opacity = int(255 * (1.0 - fade_progress)) return True else: # Effect is complete self.white_flash_active = False self.white_flash_opacity = 0 return False def draw_white_flash(self): """Draw the white flash overlay""" if self.white_flash_opacity > 0: # Create a white surface with the current opacity white_surface = sdl2.SDL_CreateRGBSurface( 0, self.target_size[0], self.target_size[1], 32, 0x000000FF, # R mask 0x0000FF00, # G mask 0x00FF0000, # B mask 0xFF000000 # A mask ) if white_surface: # Fill surface with white sdl2.SDL_FillRect(white_surface, None, sdl2.SDL_MapRGBA(white_surface.contents.format, 255, 255, 255, self.white_flash_opacity)) # Convert to texture and draw white_texture = self.factory.from_surface(white_surface) white_texture.position = (0, 0) # Enable alpha blending for the texture sdl2.SDL_SetTextureBlendMode(white_texture.texture, sdl2.SDL_BLENDMODE_BLEND) # Draw the white overlay self.renderer.copy(white_texture, dstrect=sdl2.SDL_Rect(0, 0, self.target_size[0], self.target_size[1])) # Clean up sdl2.SDL_FreeSurface(white_surface) # ====================== # UTILITY METHODS # ====================== def new_cycle(self, delay, callback): """Placeholder for cycle management (not implemented)""" pass def full_screen(self, flag): """Toggle fullscreen mode""" sdl2.SDL_SetWindowFullscreen(self.window.window, flag) def get_perf_counter(self): """Get performance counter for timing""" return sdl2.SDL_GetPerformanceCounter() def close(self): """Close the game window and cleanup""" self.running = False sdl2.ext.quit() # ====================== # MAIN GAME LOOP # ====================== # ====================== # SPECIAL EFFECTS # ====================== def generate_blood_surface(self): """Generate a dynamic blood splatter surface using SDL2""" size = self.cell_size # Create RGBA surface for blood splatter blood_surface = sdl2.SDL_CreateRGBSurface( 0, size, size, 32, 0x000000FF, # R mask 0x0000FF00, # G mask 0x00FF0000, # B mask 0xFF000000 # A mask ) if not blood_surface: return None # Lock surface for pixel manipulation sdl2.SDL_LockSurface(blood_surface) # Get pixel data pixels = cast(blood_surface.contents.pixels, POINTER(c_uint32)) pitch = blood_surface.contents.pitch // 4 # Convert pitch to pixels (32-bit) # Blood color variations (ABGR format) blood_colors = [ 0xFF00008B, # Dark red 0xFF002222, # Brick red 0xFF003C14, # Crimson 0xFF0000FF, # Pure red 0xFF000080, # Reddish brown ] # Generate splatter with diffusion algorithm center_x, center_y = size // 2, size // 2 max_radius = size // 3 + random.randint(-3, 5) for y in range(size): for x in range(size): # Calculate distance from center distance = ((x - center_x) ** 2 + (y - center_y) ** 2) ** 0.5 # Calculate blood probability based on distance if distance <= max_radius: # Closer to center = higher probability probability = max(0, 1 - (distance / max_radius)) # Add noise for irregular shape noise = random.random() * 0.7 if random.random() < probability * noise: # Choose random blood color color = random.choice(blood_colors) # 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: # Transparent pixel pixels[y * pitch + x] = 0x00000000 else: # Outside radius, transparent pixels[y * pitch + x] = 0x00000000 # 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) if 0 <= drop_x < size and 0 <= drop_y < size: drop_size = random.randint(1, 3) for dy in range(-drop_size, drop_size + 1): for dx in range(-drop_size, drop_size + 1): 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]) # Darker colors for drops alpha = random.randint(100, 200) color = (color & 0x00FFFFFF) | (alpha << 24) pixels[ny * pitch + nx] = color # Unlock surface sdl2.SDL_UnlockSurface(blood_surface) return blood_surface def draw_blood_surface(self, blood_surface, position): """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 blood surface to temporary surface sdl2.SDL_BlitSurface(blood_surface, None, temp_surface, None) sdl2.SDL_FreeSurface(blood_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 combined surface combined_surface = sdl2.SDL_CreateRGBSurface( 0, self.cell_size, self.cell_size, 32, 0x000000FF, # R mask 0x0000FF00, # G mask 0x00FF0000, # B mask 0xFF000000 # A mask ) if combined_surface is None: return existing_surface # Lock surfaces for pixel manipulation sdl2.SDL_LockSurface(existing_surface) sdl2.SDL_LockSurface(new_surface) sdl2.SDL_LockSurface(combined_surface) # Get pixel data existing_pixels = cast(existing_surface.contents.pixels, POINTER(c_uint32)) 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 # Convert pitch to pixels (32-bit) # Combine pixels with additive blending for y in range(self.cell_size): for x in range(self.cell_size): idx = y * pitch + x existing_pixel = existing_pixels[idx] new_pixel = new_pixels[idx] # Extract RGBA components existing_a = (existing_pixel >> 24) & 0xFF existing_r = (existing_pixel >> 16) & 0xFF existing_g = (existing_pixel >> 8) & 0xFF existing_b = existing_pixel & 0xFF new_a = (new_pixel >> 24) & 0xFF new_r = (new_pixel >> 16) & 0xFF new_g = (new_pixel >> 8) & 0xFF new_b = new_pixel & 0xFF # 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 final_r = min(255, existing_r + (new_r // 2)) final_g = min(255, existing_g + (new_g // 2)) final_b = min(255, existing_b + (new_b // 2)) final_a = min(255, existing_a + (new_a // 2)) else: # Use new pixel color final_r = new_r final_g = new_g final_b = new_b final_a = new_a else: # Use existing pixel color final_r = existing_r final_g = existing_g final_b = existing_b final_a = existing_a # Pack the final pixel combined_pixels[idx] = (final_a << 24) | (final_r << 16) | (final_g << 8) | final_b # Unlock surfaces sdl2.SDL_UnlockSurface(existing_surface) sdl2.SDL_UnlockSurface(new_surface) sdl2.SDL_UnlockSurface(combined_surface) return combined_surface def free_surface(self, surface): """Safely free an SDL surface""" if surface is not None: sdl2.SDL_FreeSurface(surface)