Files
mice/tests/test_rat_reproduction.py
T
enne2 319801d6e5 Fix rat reproduction collision check
Replace the brittle 'self.position == other.position_before' check
with a symmetric cell-intersection test: two rats are considered
colliding if any of their current/previous cells overlap. This handles
both 'both rats in same cell' and 'one rat enters the cell the other
just left'.

Also remove the erroneous hasattr(other_unit, 'fuck') guard that
prevented Male.fuck() from being called on Female (Female has no fuck
method, but only Male should initiate reproduction).

Add tests/test_rat_reproduction.py with 8 scenarios covering overlapping
rats, baby rats, already-pregnant females, far-apart rats, female
self-initiation, sound playback, and procreate interval spawning.
2026-06-17 10:29:26 +02:00

301 lines
10 KiB
Python

"""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!")