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.
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
"""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!")
|
||||
Reference in New Issue
Block a user