def test_clone_ball(self, mock_pygame, mock_load_png): self._configure_mocks(mock_pygame, mock_load_png) ball = Ball((100, 100), 2.32, 8) sprite, bounce, on_collide, offscreen = Mock(), Mock(), Mock(), Mock() ball._collidable_sprites.__iter__.return_value = [sprite] ball.add_collidable_sprite(sprite, bounce_strategy=bounce, speed_adjust=2, on_collide=on_collide) clone = ball.clone(start_pos=(200, 200), start_angle=3.01, base_speed=9, top_speed=14, normalisation_rate=0.3, off_screen_callback=offscreen) self.assertEqual(clone._start_pos, (200, 200)) self.assertEqual(clone._start_angle, 3.01) self.assertEqual(clone.base_speed, 9) self.assertEqual(clone._top_speed, 14) self.assertEqual(clone.normalisation_rate, 0.3) self.assertEqual(clone._off_screen_callback, offscreen) clone._collidable_sprites.add.assert_has_calls([call(sprite)]) self.assertEqual(clone._collision_data, ball._collision_data)
def test_speed_normalised_up_when_no_collision(self, mock_pygame, mock_load_png): self._configure_mocks(mock_pygame, mock_load_png) mock_pygame.sprite.spritecollide.return_value = [] ball = Ball((100, 100), 2.36, 8, normalisation_rate=0.03) ball.speed = 5 # Reduce the speed below the base speed. ball.update() self.assertEqual(ball.speed, 5.03)
def test_speed_normalised_down_when_no_collision(self, mock_pygame, mock_load_png): self._configure_mocks(mock_pygame, mock_load_png) mock_pygame.sprite.spritecollide.return_value = [] ball = Ball((100, 100), 2.36, 8, normalisation_rate=0.03) ball.speed = 12 # Increase the speed above the base speed. ball.update() self.assertEqual(ball.speed, 11.97)
def test_add_collidable_sprite(self, mock_pygame, mock_load_png): mock_load_png.return_value = Mock(), Mock() mock_sprite, mock_bounce, mock_on_collide = Mock(), Mock(), Mock() ball = Ball((100, 100), 2.36, 8) ball.add_collidable_sprite(mock_sprite, bounce_strategy=mock_bounce, speed_adjust=0.05, on_collide=mock_on_collide) ball._collidable_sprites.add.assert_called_once_with(mock_sprite) self.assertEqual(len(ball._collision_data), 1)
def test_calculate_new_position(self, mock_pygame, mock_load_png): self._configure_mocks(mock_pygame, mock_load_png) mock_pygame.sprite.spritecollide.return_value = [] ball = Ball((100, 100), 2.36, 8) ball.update() # Offset x: -5.678340450896964 # Offset y: 5.63528612616141 self.assertAlmostEqual(ball.rect.x, 95.0) self.assertAlmostEqual(ball.rect.y, 105.0)
def test_invoke_offscreen_callback(self, mock_pygame, mock_load_png): self._configure_mocks(mock_pygame, mock_load_png, offscreen=False) mock_pygame.sprite.spritecollide.return_value = [] mock_offscreen_callback = Mock() ball = Ball((100, 100), 2.36, 8, off_screen_callback=mock_offscreen_callback) ball.update() mock_offscreen_callback.assert_called_with(ball)
def __init__(self, sensor, round_class=Round1, lives=3): self.lives = lives self.score = 0 self._sensor = sensor self._screen = pygame.display.get_surface() self._life_img, _ = load_png('paddle_life.png') self._life_rects = [] self.round = round_class(TOP_OFFSET) self.paddle = Paddle(left_offset=self.round.edges.left.rect.width, right_offset=self.round.edges.right.rect.width, bottom_offset=60, speed=PADDLE_SPEED, game=self) ball = Ball(start_pos=self.paddle.rect.midtop, start_angle=BALL_START_ANGLE_RAD, base_speed=BALL_BASE_SPEED, top_speed=BALL_TOP_SPEED, normalisation_rate=BALL_SPEED_NORMALISATION_RATE, off_screen_callback=self._off_screen) self.balls = [ball] self.active_powerup = None self.enemies = [] self.sprites = [] self._create_event_handlers() self.over = False self.state = GameStartState(self)
def __init__(self, round_class=Round1, lives=3): """Initialise a new Game. Args: round_class: The class of the round to start, default Round1. lives: Optional number of lives for the player, default 3. """ # Keep track of the score and lives throughout the game. self.lives = lives self.score = 0 # Reference to the main screen. self._screen = pygame.display.get_surface() # The life graphic. self._life_img, _ = load_png('paddle_life.png') # The life graphic positions. self._life_rects = [] # The current round. self.round = round_class(TOP_OFFSET) # The sprites in the game. self.paddle = Paddle(left_offset=self.round.edges.left.rect.width, right_offset=self.round.edges.right.rect.width, bottom_offset=60, speed=PADDLE_SPEED) ball = Ball(start_pos=self.paddle.rect.midtop, start_angle=BALL_START_ANGLE_RAD, base_speed=BALL_BASE_SPEED, top_speed=BALL_TOP_SPEED, normalisation_rate=BALL_SPEED_NORMALISATION_RATE, off_screen_callback=self._off_screen) # The game starts with a single ball in play initially. self.balls = [ball] # The currently applied powerup, if any. self.active_powerup = None # The current enemies in the game. self.enemies = [] # Hold a reference to all the sprites for redrawing purposes. self.sprites = [] # Create event handlers required by the game. self._create_event_handlers() # Whether the game is finished. self.over = False # The current game state which handles the behaviour for the # current stage of the game. self.state = GameStartState(self)
def test_release_anchor(self, mock_pygame, mock_load_png): """Test that an anchored ball is released.""" self._configure_mocks(mock_pygame, mock_load_png) mock_pygame.sprite.spritecollide.return_value = [] ball = Ball((100, 100), 2.32, 8) ball.anchor((200, 200)) ball.update() ball.release(4.01) self.assertIsNone(ball._anchor) self.assertEqual(ball.angle, 4.01)
def test_calc_new_angle_right_invalid(self, mock_pygame, mock_load_png): """Test that the default bounce calculation does not calculate a new angle when the right of the ball collides but the angle is invalid for a right collision. Angles are calculated clockwise from the righthand x-axis, so in order for a right collision to occur, the ball must be travelling at an angle greater than 4.71 or less than 1.57. """ mock_sprite = self._configure_mocks(mock_pygame, mock_load_png) mock_pygame.sprite.spritecollide.return_value = [mock_sprite] def collidepoint(point): points = [(111.0, 93.0), (111.0, 103.0)] return point in points mock_sprite.rect.collidepoint.side_effect = collidepoint ball = Ball((100, 100), 2.32, 8) # Invalid angle for right collision. ball.add_collidable_sprite(mock_sprite) ball.update() # Due to the invalid state, the ball's angle is not recalculated. self.assertGreaterEqual(ball.angle, 2.32 - RANDOM_RANGE) self.assertLess(ball.angle, 2.32 + RANDOM_RANGE + 0.03)
def test_anchor_static_object(self, mock_pygame, mock_load_png): """Test that the ball's position is not recalculated when the ball is anchored to a static object. """ self._configure_mocks(mock_pygame, mock_load_png) mock_pygame.sprite.spritecollide.return_value = [] ball = Ball((100, 100), 2.32, 8) ball.anchor((200, 200)) ball.update() # Assert that a new Rectangle mock was called with the fixed # position and dimensions of the anchored ball. mock_pygame.Rect.assert_called_once_with((200, 200), (10, 10))
def test_calc_new_angle_all_corners(self, mock_pygame, mock_load_png): """Test that the default bounce calculation correctly calculates the angle when the ball collides with all corners of a sprite (is effectively inside the sprite). """ mock_sprite = self._configure_mocks(mock_pygame, mock_load_png) mock_pygame.sprite.spritecollide.return_value = [mock_sprite] mock_sprite.rect.collidepoint.return_value = True ball = Ball((100, 100), 4.01, 8) ball.add_collidable_sprite(mock_sprite) ball.update() self.assertAlmostEqual(ball.angle, 0.87, places=2)
def test_no_collision_when_ball_anchored(self, mock_pygame, mock_load_png): """Test that the ball's collision detection behaviour does not execute when the ball is anchored (not free moving itself). """ mock_sprite = self._configure_mocks(mock_pygame, mock_load_png) mock_sprite.rect.left = 305 mock_sprite.rect.top = 429 mock_pygame.sprite.spritecollide.return_value = [] ball = Ball((100, 100), 2.32, 8) ball.anchor(mock_sprite, rel_pos=(5, 5)) ball.update() self.assertEqual(mock_pygame.sprite.spritecollide.call_count, 0)
def test_anchor_sprite(self, mock_pygame, mock_load_png): """Test that the ball's position is not recalculated when the ball is anchored relative to another sprite. """ mock_sprite = self._configure_mocks(mock_pygame, mock_load_png) mock_sprite.rect.left = 305 mock_sprite.rect.top = 429 mock_pygame.sprite.spritecollide.return_value = [] ball = Ball((100, 100), 2.32, 8) ball.anchor(mock_sprite, rel_pos=(5, 5)) ball.update() # Assert that a new Rectangle mock was called with the position # of the sprite taking into account the relative position. mock_pygame.Rect.assert_called_once_with(305 + 5, 429 + 5, 10, 10)
def test_single_sprite_collision_default(self, mock_pygame, mock_load_png): """Test that the default bounce calculation is used, the ball speed adjusted, and a collision callback invoked when the ball collides with a single sprites which does not have an associated ball bounce strategy callback. """ mock_sprite = self._configure_mocks(mock_pygame, mock_load_png) mock_on_collide, mock_calc_new_angle = Mock(), Mock() mock_pygame.sprite.spritecollide.return_value = [mock_sprite] ball = Ball((100, 100), 2.36, 8) ball.add_collidable_sprite(mock_sprite, speed_adjust=0.5, on_collide=mock_on_collide) ball._calc_new_angle = mock_calc_new_angle ball.update() self.assertEqual(ball.speed, 8.5) mock_on_collide.assert_called_once_with(mock_sprite, ball) mock_calc_new_angle.assert_called_once_with([mock_sprite.rect])
def test_calc_new_angle_three_corners(self, mock_pygame, mock_load_png): """Test that the default bounce calculation correctly calculates the angle when the ball collides with three corners of a sprite. """ mock_sprite = self._configure_mocks(mock_pygame, mock_load_png) mock_pygame.sprite.spritecollide.return_value = [mock_sprite] def collidepoint(point): points = [(95.0, 94.0), (105.0, 94.0), (95.0, 104.0)] return point in points mock_sprite.rect.collidepoint.side_effect = collidepoint ball = Ball((100, 100), 4.01, 8) ball.add_collidable_sprite(mock_sprite) ball.update() self.assertAlmostEqual(ball.angle, 0.87, places=2)
def test_remove_collidable_sprite_no_exist(self, mock_pygame, mock_load_png): mock_load_png.return_value = Mock(), Mock() mock_sprite1, mock_sprite2, mock_bounce, mock_on_collide = (Mock(), Mock(), Mock(), Mock()) ball = Ball((100, 100), 2.36, 8) ball.add_collidable_sprite(mock_sprite2, bounce_strategy=mock_bounce, speed_adjust=0.05, on_collide=mock_on_collide) ball.remove_collidable_sprite(mock_sprite1) # Does not exist. ball._collidable_sprites.remove.assert_called_once_with(mock_sprite1) self.assertEqual(len(ball._collision_data), 1)
def test_calc_new_angle_single_corner(self, mock_pygame, mock_load_png): """Test that the default bounce calculation correctly calculates the angle when the ball collides with a single corner of a sprite. """ mock_sprite = self._configure_mocks(mock_pygame, mock_load_png) mock_pygame.sprite.spritecollide.return_value = [mock_sprite] def collidepoint(point): return point == (95.0, 95.0) mock_sprite.rect.collidepoint.side_effect = collidepoint ball = Ball((100, 100), 3.92, 8) ball.add_collidable_sprite(mock_sprite) ball.update() self.assertGreaterEqual(ball.angle, 0.78 - RANDOM_RANGE) self.assertLess(ball.angle, 0.78 + RANDOM_RANGE + 0.03)
def test_calc_new_angle_bottom_collision(self, mock_pygame, mock_load_png): """Test that the default bounce calculation correctly calculates the angle when the bottom of the ball collides with another sprite. """ mock_sprite = self._configure_mocks(mock_pygame, mock_load_png) mock_pygame.sprite.spritecollide.return_value = [mock_sprite] def collidepoint(point): points = [(95.0, 115.0), (105.0, 115.0)] return point in points mock_sprite.rect.collidepoint.side_effect = collidepoint ball = Ball((100, 100), 2.32, 8) ball.add_collidable_sprite(mock_sprite) ball.update() self.assertGreaterEqual(ball.angle, 3.96 - RANDOM_RANGE) self.assertLess(ball.angle, 3.96 + RANDOM_RANGE + 0.03)
def test_calc_new_angle_left_collision_2(self, mock_pygame, mock_load_png): """Test that the default bounce calculation correctly calculates the angle when the left of the ball collides with another sprite and the current angle is greater than PI. """ mock_sprite = self._configure_mocks(mock_pygame, mock_load_png) mock_pygame.sprite.spritecollide.return_value = [mock_sprite] def collidepoint(point): points = [(95.0, 94.0), (95.0, 104.0)] return point in points mock_sprite.rect.collidepoint.side_effect = collidepoint ball = Ball((100, 100), 4.01, 8) ball.add_collidable_sprite(mock_sprite) ball.update() self.assertGreaterEqual(ball.angle, 5.41 - RANDOM_RANGE) self.assertLess(ball.angle, 5.41 + RANDOM_RANGE + 0.03)
def test_calc_new_angle_right_collision_horizontal(self, mock_pygame, mock_load_png): """Test that the when the right of the ball bounces at a near horizontal angle, that the angle of bounce is adjusted to more that what it would naturally be. This is to overcome bounce loops. """ mock_sprite = self._configure_mocks(mock_pygame, mock_load_png) mock_pygame.sprite.spritecollide.return_value = [mock_sprite] def collidepoint(point): points = [(117.0, 100.0), (117.0, 110.0)] return point in points mock_sprite.rect.collidepoint.side_effect = collidepoint ball = Ball((100, 100), 6.25, 8) ball.add_collidable_sprite(mock_sprite) ball.update() self.assertGreaterEqual(ball.angle, 3.52 - RANDOM_RANGE) self.assertLess(ball.angle, 3.52 + RANDOM_RANGE + 0.03)
def test_calc_new_angle_corner_oblique_bottom_right_v( self, mock_pygame, mock_load_png): """Test that the default bounce calculation correctly calculates the angle when the ball collides with a single corner of a sprite but the angle of collision is vertically oblique against the bottom right corner of the ball, rather than head on. """ mock_sprite = self._configure_mocks(mock_pygame, mock_load_png) mock_pygame.sprite.spritecollide.return_value = [mock_sprite] def collidepoint(point): return point == (115.0, 105.0) # Bottom left corner mock_sprite.rect.collidepoint.side_effect = collidepoint ball = Ball((100, 100), 5.5, 8) ball.add_collidable_sprite(mock_sprite) ball.update() self.assertGreaterEqual(ball.angle, 3.92 - RANDOM_RANGE) self.assertLess(ball.angle, 3.92 + RANDOM_RANGE + 0.03)
def test_single_sprite_collision(self, mock_pygame, mock_load_png): """Test that a bounce strategy is used, the ball speed adjusted, and a collision callback invoked when the ball collides with a single sprite. """ mock_sprite = self._configure_mocks(mock_pygame, mock_load_png) mock_bounce, mock_on_collide = Mock(), Mock() mock_pygame.sprite.spritecollide.return_value = [mock_sprite] mock_bounce.return_value = 2.4 ball = Ball((100, 100), 2.36, 8) ball.add_collidable_sprite(mock_sprite, bounce_strategy=mock_bounce, speed_adjust=0.5, on_collide=mock_on_collide) ball.update() self.assertEqual(ball.speed, 8.5) mock_on_collide.assert_called_once_with(mock_sprite, ball) mock_bounce.assert_called_once_with(mock_sprite.rect, ball.rect) self.assertEqual(ball.angle, 2.4)
def test_multiple_sprite_collision(self, mock_pygame, mock_load_png): """Test that the default bounce calculation is used, the ball speed adjusted, and collision callbacks invoked when the ball collides with multiple sprites. """ mock_sprite = self._configure_mocks(mock_pygame, mock_load_png) (mock_bounce, mock_on_collide, mock_calc_new_angle, mock_sprite2, mock_sprite3) = (Mock(), Mock(), Mock(), Mock(), Mock()) mock_pygame.sprite.spritecollide.return_value = [ mock_sprite, mock_sprite2, mock_sprite3 ] mock_bounce.return_value = 3.2 ball = Ball((100, 100), 2.36, 8, top_speed=9) ball.add_collidable_sprite(mock_sprite, bounce_strategy=mock_bounce, speed_adjust=0.5, on_collide=mock_on_collide) ball.add_collidable_sprite(mock_sprite2, bounce_strategy=mock_bounce, speed_adjust=0.5, on_collide=mock_on_collide) ball.add_collidable_sprite(mock_sprite3, bounce_strategy=mock_bounce, speed_adjust=0.5, on_collide=mock_on_collide) ball._calc_new_angle = mock_calc_new_angle ball.update() self.assertEqual(ball.speed, 9.0) # Gone up, but not above top speed. mock_on_collide.assert_has_calls([ call(mock_sprite, ball), call(mock_sprite2, ball), call(mock_sprite3, ball) ]) mock_calc_new_angle.assert_called_once_with( [mock_sprite.rect, mock_sprite2.rect, mock_sprite3.rect])