diff --git a/django/stats/models.py b/django/stats/models.py index 5a8c6a9..2a8c624 100644 --- a/django/stats/models.py +++ b/django/stats/models.py @@ -1047,7 +1047,9 @@ def award(participation, **kwargs): MatchBadge.award_kills_in_one_round_badges(participation, 5, 'ace', **kwargs) MatchBadge.award_kills_in_one_round_badges(participation, 4, 'quad-kill', **kwargs) MatchBadge.award_margin_badge(participation, 'carrier', order = '-adr', margin = 1.8, emoji = '🍆', **kwargs) - MatchBadge.award_margin_badge(participation, 'peach', order = 'adr', margin = 0.667, emoji = '🍑', **kwargs) + MatchBadge.award_margin_badge( + participation, 'peach', order = 'adr', margin = 0.67, emoji = '🍑', max_adr = 50, max_kd = 0.5, **kwargs, + ) @staticmethod def award_with_history(participation, old_participations): @@ -1095,21 +1097,41 @@ def award_kills_in_one_round_badges(participation, kill_number, badge_type_slug, m.squad.notify_on_discord(text) @staticmethod - def award_margin_badge(participation, badge_type_slug, order, margin, emoji, mute_discord = False): + def award_margin_badge(participation, badge_type_slug, order, margin, emoji, mute_discord = False, **bounds): kpi = order[1:] if order[0] in '+-' else order badge_type = MatchBadgeType.objects.get(slug = badge_type_slug) if MatchBadge.objects.filter(badge_type=badge_type, participation=participation).exists(): return teammates = participation.pmatch.matchparticipation_set.filter(team = participation.team).order_by(order) - awarded = teammates[0].pk == participation.pk and any( - ( - order[0] == '-' and getattr(teammates[0], kpi) > margin * getattr(teammates[1], kpi), - order[0] != '-' and getattr(teammates[0], kpi) < margin * getattr(teammates[1], kpi), - ) - ) + # Define the requirements for the badge + requirements = [ + teammates[0].pk == participation.pk, + any( + ( + order[0] == '-' and getattr(teammates[0], kpi) > margin * getattr(teammates[1], kpi), + order[0] != '-' and getattr(teammates[0], kpi) < margin * getattr(teammates[1], kpi), + ) + ), + ] - if awarded: + # Add the bound checks to the requirements + req_bounds = list() + for bound_key, bound_val in bounds.items(): + func_name, attr_name = bound_key.split('_') + attr = getattr(participation, attr_name) + match func_name: + case 'min': + req_bounds.append(attr >= bound_val) + case 'max': + req_bounds.append(attr <= bound_val) + case _: + raise ValueError(f'Invalid function name: "{func_name}"') + if req_bounds: + requirements.append(any(req_bounds)) + + # Check the requirements and award the badge + if all(requirements): log.info(f'{participation.player.name} received the {badge_type.name}') MatchBadge.objects.create(badge_type = badge_type, participation = participation) text = ( diff --git a/django/stats/tests.py b/django/stats/tests.py index e32d7ed..8bc2ae6 100644 --- a/django/stats/tests.py +++ b/django/stats/tests.py @@ -229,19 +229,28 @@ def test(self): ) -@patch('stats.models.Match.award_badges') class MatchBadge__award(TestCase): - def test_no_awards(self, mock__Match__award_badges): - pmatch = Match__create_from_data().test() - participation = pmatch.get_participation('76561197967680028') + def setUp(self): + with patch('stats.models.Match.award_badges'): + self.pmatch = Match__create_from_data().test() + self.mp5 = self.pmatch.get_participation('76561197962477966') + self.teammates = list( + self.mp5.pmatch.matchparticipation_set.filter( + team = self.mp5.team, + ).exclude( + pk = self.mp5.pk, + ).order_by('-adr') + ) + + def test_no_awards(self): + participation = self.pmatch.get_participation('76561197967680028') models.MatchBadge.award(participation) self.assertEqual(len(models.MatchBadge.objects.filter(participation = participation)), 0) - def test_quad_kill(self, mock__Match__award_badges): - pmatch = Match__create_from_data().test() - mp1 = pmatch.get_participation('76561197967680028') - mp2 = pmatch.get_participation('76561197961345487') + def test_quad_kill(self): + mp1 = self.pmatch.get_participation('76561197967680028') + mp2 = self.pmatch.get_participation('76561197961345487') models.KillEvent.objects.all().delete() models.KillEvent.objects.bulk_create( [ @@ -260,10 +269,9 @@ def test_quad_kill(self, mock__Match__award_badges): self.assertEqual(badge.participation.pk, mp1.pk) self.assertEqual(badge.frequency, 1) - def test_quad_kill_twice(self, mock__Match__award_badges): - pmatch = Match__create_from_data().test() - mp1 = pmatch.get_participation('76561197967680028') - mp2 = pmatch.get_participation('76561197961345487') + def test_quad_kill_twice(self): + mp1 = self.pmatch.get_participation('76561197967680028') + mp2 = self.pmatch.get_participation('76561197961345487') models.KillEvent.objects.all().delete() models.KillEvent.objects.bulk_create( [ @@ -283,10 +291,9 @@ def test_quad_kill_twice(self, mock__Match__award_badges): self.assertEqual(badge.participation.pk, mp1.pk) self.assertEqual(badge.frequency, 2) - def test_ace(self, mock__Match__award_badges): - pmatch = Match__create_from_data().test() - mp1 = pmatch.get_participation('76561197967680028') - mp2 = pmatch.get_participation('76561197961345487') + def test_ace(self): + mp1 = self.pmatch.get_participation('76561197967680028') + mp2 = self.pmatch.get_participation('76561197961345487') models.KillEvent.objects.all().delete() models.KillEvent.objects.bulk_create( [ @@ -303,10 +310,9 @@ def test_ace(self, mock__Match__award_badges): self.assertEqual(badge.participation.pk, mp1.pk) self.assertEqual(badge.frequency, 1) - def test_ace_twice(self, mock__Match__award_badges): - pmatch = Match__create_from_data().test() - mp1 = pmatch.get_participation('76561197967680028') - mp2 = pmatch.get_participation('76561197961345487') + def test_ace_twice(self): + mp1 = self.pmatch.get_participation('76561197967680028') + mp2 = self.pmatch.get_participation('76561197961345487') models.KillEvent.objects.all().delete() models.KillEvent.objects.bulk_create( [ @@ -328,10 +334,9 @@ def test_ace_twice(self, mock__Match__award_badges): self.assertEqual(badge.participation.pk, mp1.pk) self.assertEqual(badge.frequency, 2) - def test_carrier_badge(self, mock__Match__award_badges): - pmatch = Match__create_from_data().test() - mp1 = pmatch.get_participation('76561197967680028') - mp2 = pmatch.get_participation('76561197961345487') + def test_carrier_badge(self): + mp1 = self.pmatch.get_participation('76561197967680028') + mp2 = self.pmatch.get_participation('76561197961345487') # Test with ADR right below the threshold mp1.adr = 1.79 * mp2.adr @@ -347,24 +352,79 @@ def test_carrier_badge(self, mock__Match__award_badges): models.MatchBadge.award(mp1) self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'carrier', participation = mp1)), 1) - def test_peach_price(self, mock__Match__award_badges): - pmatch = Match__create_from_data().test() - mp4 = pmatch.get_participation('76561198067716219') - mp5 = pmatch.get_participation('76561197962477966') - - # Test with ADR right below the threshold - mp5.adr = 0.668 * mp4.adr - mp5.save() - models.MatchBadge.award(mp5) - self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'peach', participation = mp5)), 0) + def test_peach_price__within_bounds(self): + """ + Test the 🍑 Peach Price when the constraints ✅ "ADR <50" and ✅ "K/D <0.5" are met. + """ + for mp in self.teammates: + mp.adr = 50 + mp.save() + self.mp5.kills = 1 + self.mp5.deaths = 3 # Test with ADR right above the threshold - mp5.adr = 0.666 * mp4.adr - mp5.save() + self.mp5.adr = 0.68 * self.teammates[-1].adr + self.mp5.save() + models.MatchBadge.award(self.mp5) + self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'peach', participation = self.mp5)), 0) + + # Test with ADR right below the threshold + self.mp5.adr = 0.66 * self.teammates[-1].adr + self.mp5.save() for itr in range(2): # Test twice, the badge should be awarded only once with self.subTest(itr = itr): - models.MatchBadge.award(mp5) - self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'peach', participation = mp5)), 1) + models.MatchBadge.award(self.mp5) + self.assertEqual( + len(models.MatchBadge.objects.filter(badge_type = 'peach', participation = self.mp5)), 1, + ) + + def test_peach_price__kd_too_high(self): + """ + Test the 🍑 Peach Price when the constraint ✅ "ADR <50" is met but ❌ "K/D <0.5" is not. + """ + for mp in self.teammates: + mp.adr = 50 + mp.save() + self.mp5.kills = 2 + self.mp5.deaths = 3 + + # Test with ADR right below the threshold + self.mp5.adr = 0.66 * self.teammates[-1].adr + self.mp5.save() + models.MatchBadge.award(self.mp5) + self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'peach', participation = self.mp5)), 1) + + def test_peach_price__adr_too_high(self): + """ + Test the 🍑 Peach Price when the constraint ✅ "K/D <0.5" is met but ❌ "ADR <50" is not. + """ + for mp in self.teammates: + mp.adr = 100 + mp.save() + self.mp5.kills = 1 + self.mp5.deaths = 3 + + # Test with ADR right below the threshold + self.mp5.adr = 0.66 * self.teammates[-1].adr + self.mp5.save() + models.MatchBadge.award(self.mp5) + self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'peach', participation = self.mp5)), 1) + + def test_peach_price__kd_and_adr_too_high(self): + """ + Test the 🍑 Peach Price when the constraint ❌ "ADR <50" and ❌ "K/D <0.5" both are not met. + """ + for mp in self.teammates: + mp.adr = 100 + mp.save() + self.mp5.kills = 2 + self.mp5.deaths = 3 + + # Test with ADR right below the threshold + self.mp5.adr = 0.66 * self.teammates[-1].adr + self.mp5.save() + models.MatchBadge.award(self.mp5) + self.assertEqual(len(models.MatchBadge.objects.filter(badge_type = 'peach', participation = self.mp5)), 0) class MatchBadge__award_with_history(TestCase):