"""Test for rat reproduction collision logic. Simulates Male.fuck() being called on collision between adult male and female rats in various positions (overlapping, entering same cell, etc.) to verify that reproduction triggers correctly in all expected cases. """ import sys import os sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) import random from unittest.mock import MagicMock from engine.collision_system import CollisionSystem from engine import maze from units import rat as rat_module from units.unit import UnitType def make_game(cell_size=40, width=10, height=10, with_sound=True): game = MagicMock() game.cell_size = cell_size # Minimal maze: everything empty m = maze.Map.__new__(maze.Map) m.width = width m.height = height m.tiles = [[maze.MAP_EMPTY for _ in range(width)] for _ in range(height)] m.in_bounds = lambda x, y: 0 <= x < width and 0 <= y < height m.is_wall = lambda x, y: False m.is_empty = lambda x, y: True m.is_traversable = lambda x, y: True m.is_tunnel = lambda x, y: False game.map = m game.collision_system = CollisionSystem(cell_size, width, height) game.units = {} game.unit_positions = {} game.unit_positions_before = {} game.scoring = MagicMock() # Mock graphics with realistic image sizes for rat bbox graphics = MagicMock() image_size = (36, 36) graphics.rat_image_sizes = { "MALE": {d: image_size for d in ["UP", "DOWN", "LEFT", "RIGHT"]}, "FEMALE": {d: image_size for d in ["UP", "DOWN", "LEFT", "RIGHT"]}, "BABY": {d: (24, 24) for d in ["UP", "DOWN", "LEFT", "RIGHT"]}, } graphics.rat_assets_textures = {} graphics.bomb_assets = {} graphics.assets = {} graphics.blood_layer_sprites = [] graphics.cave_foreground_tiles = [] graphics.draw_maze = MagicMock() graphics.regenerate_background = MagicMock() graphics.draw_blood_layer = MagicMock() game.graphics = graphics game.profile_integration = MagicMock() game.profile_integration.get_device_leaderboard = MagicMock(return_value=[]) game.current_level = 0 game.points = 0 game.sound_volume = 50 game.music_volume = 50 game.ammo = {"bomb": {"count": 0, "max": 0}} game.game_status = "playing" game.combined_scores = None game.game_end = (False, None) game.audio = False game.ammo_sound = None # Mock render engine render = MagicMock() render.play_sound = MagicMock() if not with_sound: render.play_sound = MagicMock(side_effect=AssertionError("sound should not play")) game.render_engine = render # Unit manager manager = MagicMock() manager.get_unit_by_id = lambda uid: game.units.get(uid) manager.spawn_rat = MagicMock(side_effect=lambda pos=None: None) game.unit_manager = manager return game def make_rat(game, rat_class, position, age=None): rat = rat_class(game, position=position) if age is not None: rat.age = age else: rat.age = rat_module.AGE_THRESHOLD + 10 # adult # Make sure stop and partial_move are valid rat.stop = 0 rat.partial_move = 0.0 # Compute bbox for collision system rat._update_render_position() # Add to game's units so collisions can find them game.units[rat.id] = rat # Register in collision system cs = game.collision_system cs.register_unit(rat.id, rat.bbox, rat.position, rat.position_before, rat.collision_layer) return rat def test_reproduction_when_both_rats_share_current_cell(): """Both rats end up in the same cell at the same time.""" game = make_game() male = make_rat(game, rat_module.Male, (3, 3)) female = make_rat(game, rat_module.Female, (3, 3)) # Both just arrived in (3,3) - position_before == position for both. # bbox uses position (not position_before) when partial_move==0 after arrival. male.position = (3, 3) male.position_before = (3, 3) female.position = (3, 3) female.position_before = (3, 3) female.pregnant = False female.babies = 0 # Trigger collision check male.collisions() assert female.pregnant == rat_module.PREGNANCY_DURATION, \ f"Female should be pregnant, got pregnant={female.pregnant}" assert female.babies >= 1 and female.babies <= 3, \ f"Babies count should be 1-3, got {female.babies}" assert male.stop == 100 assert female.stop == 200 print("PASS: reproduction when both rats share current cell") def test_reproduction_when_male_enters_female_previous_cell(): """Male just stepped into the cell the female was previously in.""" game = make_game() male = make_rat(game, rat_module.Male, (2, 3)) female = make_rat(game, rat_module.Female, (3, 3)) # Male is in (3,3) having moved from (2,3); female is transitioning from (3,3) to (4,3) # at partial_move ~0.3, so her bbox straddles cell 3 and cell 4. male.position = (3, 3) male.position_before = (3, 3) male.partial_move = 0.0 female.position = (4, 3) female.position_before = (3, 3) female.partial_move = 0.3 female.pregnant = False female.babies = 0 male.direction = "RIGHT" female.direction = "RIGHT" male._update_render_position() female._update_render_position() game.collision_system.clear() game.collision_system.register_unit(male.id, male.bbox, male.position, male.position_before, male.collision_layer) game.collision_system.register_unit(female.id, female.bbox, female.position, female.position_before, female.collision_layer) male.collisions() assert female.pregnant == rat_module.PREGNANCY_DURATION, \ f"Female should be pregnant, got pregnant={female.pregnant}" print("PASS: reproduction when male enters female's previous cell") def test_no_reproduction_when_already_pregnant(): """Female already pregnant: fuck should be a no-op (idempotent).""" game = make_game() male = make_rat(game, rat_module.Male, (3, 3)) female = make_rat(game, rat_module.Female, (3, 3)) male.position = (3, 3) male.position_before = (2, 3) female.position = (3, 3) female.position_before = (4, 3) female.pregnant = True female.pregnant_counter = 100 # track if re-set female.babies = 2 male.collisions() # pregnancy counter should not be reset assert female.pregnant is True assert female.babies == 2, f"Babies should remain 2, got {female.babies}" print("PASS: no reproduction when already pregnant") def test_no_reproduction_when_rats_far_apart(): """Rats in different cells: no reproduction.""" game = make_game() male = make_rat(game, rat_module.Male, (2, 3)) female = make_rat(game, rat_module.Female, (5, 5)) male.position = (2, 3) male.position_before = (2, 3) female.position = (5, 5) female.position_before = (5, 5) female.pregnant = False female.babies = 0 male.collisions() assert female.pregnant is False, \ f"Female should NOT be pregnant, got pregnant={female.pregnant}" print("PASS: no reproduction when rats far apart") def test_no_reproduction_when_baby_rats(): """Baby rats (age < threshold) should not reproduce.""" game = make_game() male = make_rat(game, rat_module.Male, (3, 3), age=10) female = make_rat(game, rat_module.Female, (3, 3), age=10) male.position = (3, 3) male.position_before = (2, 3) female.position = (3, 3) female.position_before = (4, 3) female.pregnant = False female.babies = 0 male.collisions() assert female.pregnant is False, \ "Baby rats should not reproduce" print("PASS: no reproduction for baby rats") def test_female_does_not_initiate_reproduction(): """Female calling collisions() should not initiate reproduction on its own. This is intentional: only Male has the fuck() method. Female is the receiver. """ game = make_game() male = make_rat(game, rat_module.Male, (3, 3)) female = make_rat(game, rat_module.Female, (3, 3)) male.position = (3, 3) male.position_before = (2, 3) female.position = (3, 3) female.position_before = (4, 3) female.pregnant = False female.babies = 0 # Female calling collisions() should NOT change pregnant state female.collisions() assert female.pregnant is False, \ "Female should not initiate her own pregnancy" print("PASS: female does not initiate reproduction") def test_reproduction_sound_played(): """Verify SEX.WAV sound is played on reproduction.""" game = make_game() male = make_rat(game, rat_module.Male, (3, 3)) female = make_rat(game, rat_module.Female, (3, 3)) male.position = (3, 3) male.position_before = (2, 3) female.position = (3, 3) female.position_before = (4, 3) female.pregnant = False female.babies = 0 male.collisions() game.render_engine.play_sound.assert_called_with("SEX.WAV") print("PASS: SEX.WAV sound played") def test_procreate_spawns_baby_at_interval(): """Female.procreate() spawns a baby after each BABY_INTERVAL tick of pregnancy.""" game = make_game() female = make_rat(game, rat_module.Female, (3, 3)) female.babies = 3 # procreate() first decrements pregnant, then checks if pregnant == babies * BABY_INTERVAL. # So pregnant must start at babies * BABY_INTERVAL + 1 to trigger after decrement. female.pregnant = female.babies * rat_module.BABY_INTERVAL + 1 female.position = (3, 3) female.position_before = (3, 3) initial_babies = female.babies female.procreate() assert game.unit_manager.spawn_rat.called, "spawn_rat should be called" assert female.babies == initial_babies - 1, \ f"Babies should decrease by 1, got {female.babies}" assert female.stop == 20 print("PASS: procreate spawns baby at correct interval") if __name__ == "__main__": test_reproduction_when_both_rats_share_current_cell() test_reproduction_when_male_enters_female_previous_cell() test_no_reproduction_when_already_pregnant() test_no_reproduction_when_rats_far_apart() test_no_reproduction_when_baby_rats() test_female_does_not_initiate_reproduction() test_reproduction_sound_played() test_procreate_spawns_baby_at_interval() print("\nAll tests passed!")