Matteo Benedetto 7 months ago
parent
commit
8a32aad877
  1. 1
      conf/keybinding_game.json
  2. 27
      convert_audio.py
  3. 2
      engine/controls.py
  4. 5
      engine/graphics.py
  5. 2
      engine/scoring.py
  6. 132
      engine/sdl2.py
  7. 27
      engine/unit_manager.py
  8. 31
      rats.py
  9. BIN
      sound/NUCLEAR.WAV
  10. BIN
      sound/mine_converted.wav
  11. BIN
      sound/nuke.wav
  12. 57
      units/bomb.py
  13. 7
      units/rat.py

1
conf/keybinding_game.json

@ -10,5 +10,6 @@
"scroll_right": ["Right", 11],
"spawn_bomb": ["Space", 1],
"spawn_mine": ["Left Ctrl", 2, 17],
"spawn_nuclear_bomb": ["N", 3],
"pause": ["P", 16]
}

27
convert_audio.py

@ -1,17 +1,20 @@
#!/usr/bin/env python3
"""
Audio conversion script to convert mine.wav to u8 22100 Hz format
Audio conversion script to convert audio files to u8 22100 Hz format
"""
from pydub import AudioSegment
import os
import sys
import argparse
def convert_mine_wav():
"""Convert mine.wav to u8 format at 22100 Hz"""
def convert_audio(input_path, output_path=None):
"""Convert audio file to u8 format at 22100 Hz"""
# Input and output paths
input_path = "sound/mine.wav"
output_path = "sound/mine_converted.wav"
# Generate output path if not provided
if output_path is None:
base_name = os.path.splitext(input_path)[0]
output_path = f"{base_name}_converted.wav"
if not os.path.exists(input_path):
print(f"Error: {input_path} not found!")
@ -73,5 +76,15 @@ def convert_mine_wav():
except Exception as e:
print(f"Error during conversion: {e}")
def main():
"""Main function to handle command line arguments"""
parser = argparse.ArgumentParser(description='Convert audio files to u8 22100 Hz format')
parser.add_argument('input_file', help='Input audio file path')
parser.add_argument('-o', '--output', help='Output file path (optional)')
args = parser.parse_args()
convert_audio(args.input_file, args.output)
if __name__ == "__main__":
convert_mine_wav()
main()

2
engine/controls.py

@ -30,6 +30,8 @@ class KeyBindings:
self.spawn_bomb(self.pointer)
elif key in keybindings.get("spawn_mine", []):
self.spawn_mine(self.pointer)
elif key in keybindings.get("spawn_nuclear_bomb", []):
self.spawn_nuclear_bomb(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", []):

5
engine/graphics.py

@ -6,11 +6,16 @@ class Graphics():
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 = {}
self.rat_assets_textures = {}
self.bomb_assets = {}
for sex in ["MALE", "FEMALE", "BABY"]:
self.rat_assets[sex] = {}
for direction in ["UP", "DOWN", "LEFT", "RIGHT"]:
self.rat_assets[sex][direction] = self.render_engine.load_image(f"Rat/BMP_{sex}_{direction}.png", transparent_color=(128, 128, 128))
for sex in ["MALE", "FEMALE", "BABY"]:
self.rat_assets_textures[sex] = {}
for direction in ["UP", "DOWN", "LEFT", "RIGHT"]:
self.rat_assets_textures[sex][direction] = self.render_engine.load_image(f"Rat/BMP_{sex}_{direction}.png", transparent_color=(128, 128, 128), surface=False)
for n in range(5):
self.bomb_assets[n] = self.render_engine.load_image(f"Rat/BMP_BOMB{n}.png", transparent_color=(128, 128, 128))
self.assets = {}

2
engine/scoring.py

@ -16,7 +16,7 @@ class Scoring:
for row in rows:
table.append(row.split(" - "))
table.sort(key=lambda x: int(x[1]), reverse=True)
return table
return table[:3]
def add_point(self, value):
self.points += value

132
engine/sdl2.py

@ -54,6 +54,18 @@ class GameWindow:
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
@ -151,7 +163,7 @@ class GameWindow:
"""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"):
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
@ -225,20 +237,50 @@ class GameWindow:
# 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))
#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
status_text = f"FPS: {fps} - {text}"
font = self.fonts[20]
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(status_text, font, (8, 5), sdl2.ext.Color(0, 0, 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
# ======================
@ -328,6 +370,10 @@ class GameWindow:
# 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:
@ -335,7 +381,11 @@ class GameWindow:
self.running = False
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)
# 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)
@ -362,6 +412,64 @@ class GameWindow:
# 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
# ======================

27
engine/unit_manager.py

@ -17,19 +17,38 @@ class UnitManager:
self.spawn_unit(rat_class, position)
def spawn_bomb(self, position):
if self.ammo["bomb"]["count"] <= 0:
return
self.render_engine.play_sound("PUTDOWN.WAV")
self.spawn_unit(bomb.Timer, position)
self.ammo["bomb"]["count"] -= 1
def spawn_nuclear_bomb(self, position):
"""Spawn a nuclear bomb at the specified position"""
if self.ammo["nuclear"]["count"] <= 0:
return
if self.map.is_wall(position[0], position[1]):
return
self.render_engine.play_sound("NUCLEAR.WAV")
self.ammo["nuclear"]["count"] -= 1
self.spawn_unit(bomb.NuclearBomb, position)
def spawn_mine(self, position):
if self.ammo["mine"]["count"] <= 0:
return
if self.map.is_wall(position[0], position[1]):
return
self.render_engine.play_sound("PUTDOWN.WAV")
self.spawn_unit(mine.Mine, position)
self.ammo["mine"]["count"] -= 1
self.spawn_unit(mine.Mine, position, on_bottom=True)
def spawn_unit(self, unit, position, **kwargs):
def spawn_unit(self, unit, position, on_bottom=False, **kwargs):
id = uuid.uuid4()
self.units[id] = unit(self, position, id, **kwargs)
if on_bottom:
self.units = {id: unit(self, position, id, **kwargs), **self.units}
else:
self.units[id] = unit(self, position, id, **kwargs)
def choose_start(self):
if not hasattr(self, '_valid_positions'):
self._valid_positions = [

31
rats.py

@ -1,13 +1,11 @@
#!/usr/bin/python3
import random
import uuid
from engine import maze, sdl2 as engine, controls, graphics, unit_manager, scoring
import os
import datetime
import json
from engine import maze, sdl2 as engine, controls, graphics, unit_manager, scoring
class MiceMaze(
controls.KeyBindings,
unit_manager.UnitManager,
@ -40,6 +38,20 @@ class MiceMaze(
self.start_game()
self.background_texture = None
self.configs = self.get_config()
self.ammo = {
"bomb": {
"count": 2,
"max": 5
},
"nuclear": {
"count": 1,
"max": 1
},
"mine": {
"count": 5,
"max": 5
}
}
def get_config(self):
configs = {}
@ -57,6 +69,15 @@ class MiceMaze(
# ==================== GAME LOGIC ====================
def refill_ammo(self):
for ammo_type, data in self.ammo.items():
if ammo_type == "bomb":
if random.random() < 0.01:
data["count"] = min(data["count"] + 1, data["max"])
elif ammo_type == "mine":
if random.random() < 0.05:
data["count"] = min(data["count"] + 1, data["max"])
def update_maze(self):
if self.game_over():
return
@ -79,6 +100,8 @@ class MiceMaze(
unit.collisions()
unit.draw()
self.render_engine.update_status(f"Mice: {self.count_rats()} - Points: {self.points}")
self.refill_ammo()
self.render_engine.update_ammo(self.ammo, self.assets)
self.scroll()
self.render_engine.new_cycle(50, self.update_maze)

BIN
sound/NUCLEAR.WAV

Binary file not shown.

BIN
sound/mine_converted.wav

Binary file not shown.

BIN
sound/nuke.wav

Binary file not shown.

57
units/bomb.py

@ -1,9 +1,12 @@
from .unit import Unit
from . import rat
from .points import Point
import uuid
import random
# Costanti
AGE_THRESHOLD = 200
NUCLEAR_TIMER = 50 # 1 second at ~50 FPS
class Bomb(Unit):
@ -106,4 +109,58 @@ class Explosion(Bomb):
x_pos = self.position_before[0] * self.game.cell_size + (self.game.cell_size - image_size[0]) // 2 + partial_x
y_pos = self.position_before[1] * self.game.cell_size + (self.game.cell_size - image_size[1]) // 2 + partial_y
self.game.render_engine.draw_image(x_pos, y_pos, image, anchor="nw", tag="unit")
class NuclearBomb(Unit):
def __init__(self, game, position=(0,0), id=None):
super().__init__(game, position, id)
self.speed = 1 # Slow countdown
self.fight = False
self.timer = NUCLEAR_TIMER # 1 second timer
def move(self):
"""Count down the nuclear timer"""
self.timer -= 1
if self.timer <= 0:
self.explode()
def collisions(self):
pass
def explode(self):
"""Nuclear explosion that affects all rats on the map"""
print("NUCLEAR EXPLOSION!")
# Play nuclear explosion sound
self.game.render_engine.play_sound("nuke.wav")
# Trigger white screen effect
self.game.render_engine.trigger_white_flash()
# Remove the nuclear bomb from the game
if self.id in self.game.units:
self.game.units.pop(self.id)
# Kill 90% of all rats on the map
rats_to_kill = []
# If unit is a child class of Rat
for unit in self.game.units.values():
if isinstance(unit, rat.Rat):
if random.random() < 0.7: # 70% chance to kill each rat
rats_to_kill.append(unit)
for unit in rats_to_kill:
unit.die(score=None)
print(f"Nuclear explosion killed {len(rats_to_kill)} rats!")
def draw(self):
"""Draw nuclear bomb on position"""
image = self.game.assets["BMP_NUCLEAR"]
image_size = self.game.render_engine.get_image_size(image)
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")

7
units/rat.py

@ -98,8 +98,9 @@ class Rat(Unit):
self.game.units.pop(target_unit.id)
# Rat-specific behavior: spawn points
self.game.spawn_unit(Point, death_position, value=score)
if score not in [None, 0]:
self.game.spawn_unit(Point, death_position, value=score)
# Add blood stain directly to background
self.game.add_blood_stain(death_position)
@ -108,7 +109,7 @@ class Rat(Unit):
direction = self.calculate_rat_direction()
sex = self.sex if self.age > AGE_THRESHOLD else "BABY"
image = self.game.rat_assets[sex][direction]
image = self.game.rat_assets_textures[sex][direction]
image_size = self.game.render_engine.get_image_size(image)
self.rat_image = image
partial_x, partial_y = 0, 0

Loading…
Cancel
Save