diff --git a/AUTHORS b/AUTHORS index cf325b5..e9b09fb 100644 --- a/AUTHORS +++ b/AUTHORS @@ -14,3 +14,5 @@ Maintainers Patches and Suggestions ``````````````````````` + + - Nathan Zilora diff --git a/pychievements/trackers.py b/pychievements/trackers.py index 47cf836..d0cd3ac 100644 --- a/pychievements/trackers.py +++ b/pychievements/trackers.py @@ -12,6 +12,16 @@ class NotRegistered(Exception): pass +def median(lst): + n = len(lst) + if n < 1: + return None + if n % 2 == 1: + return sorted(lst)[n // 2] + else: + return sum(sorted(lst)[n // 2-1:n // 2 + 1])/ 2.0 + + class AchievementTracker(object): """ AchievementTracker tracks achievements and current levels for ``tracked_id`` using a configured @@ -77,7 +87,7 @@ def is_registered(self, achievement): """ return achievement in self._registry - def achievements(self, category=None, keywords=[]): + def achievements(self, category=None, keywords=None): """ Returns all registered achievements. @@ -88,9 +98,10 @@ def achievements(self, category=None, keywords=[]): keywords Filters returned achievements by keywords. Returned achievements will match all - given keywords + given keywords. This will be [] if not provided. """ achievements = [] + keywords = keywords if keywords is not None else [] for achievement in self._registry: if category is None or achievement.category == category: if not keywords or all([_ in achievement.keywords for _ in keywords]): @@ -117,9 +128,10 @@ class or a string of the name of an achievement class that has been registered w return self._backend.achievement_for_id(tracked_id, a[0]) raise NotRegistered('The achievement %s is not registered with this tracker' % achievement) - def achievements_for_id(self, tracked_id, category=None, keywords=[]): + def achievements_for_id(self, tracked_id, category=None, keywords=None): """ Returns all of the achievements for tracked_id that match the given category and - keywords """ + keywords. keywords will be [] if not provided.""" + keywords = keywords if keywords is not None else [] return self._backend.achievements_for_id(tracked_id, self.achievements(category, keywords)) def _check_signals(self, tracked_id, achievement, old_level, old_achieved): @@ -216,3 +228,39 @@ def get_tracked_ids(self): def remove_id(self, tracked_id): """ Remove all tracked information for tracked_id """ self._backend.remove_id(tracked_id) + + def compare_stats(self, tracked_ids, achievement): + """ + Fetches an achievement's statistics for each ID provided, and compares them. + + This will return a dictionary that looks like the following: + { + "mean": Number with the average current level. This will be rounded into a goal index. + "median": The median of all the current levels. + "max": The highest level among the IDs. + "min": The lowest level among the IDs. + } + """ + stats = { + "mean": 0, + "median": [], + "max": [], + "min": [] + } + + for tid in tracked_ids: + personal_achievement = self._backend.achievement_for_id(tid, achievement) + stats["mean"] += personal_achievement.current[0] + for stat in ["median", "max", "min"]: + stats[stat].append(personal_achievement.current[0]) + + stats["mean"] /= len(tracked_ids) + stats["median"] = median(stats["median"]) + stats["max"] = max(stats["max"]) + stats["min"] = min(stats["min"]) + + return stats + + def compare_global_stats(self, achievement): + """ Shorthand for tracker.compare_stats(tracker.get_tracked_ids(), ``achievement``) """ + return self.compare_stats(self.get_tracked_ids(), achievement) diff --git a/tests/__init__.py b/tests/__init__.py index 659dcef..a2cceb8 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -30,6 +30,24 @@ def setUp(self): self.tracker = AchievementTracker() self.tracker.register(ACHIEVEMENTS) + self.fixed_tracker = AchievementTracker() + + class FixedAchievement(Achievement): + name = "Fixed" + category = "test" + keywords = "unittest" + goals = ( + {'level': 10, 'name': 'Level 1', 'icon': icons.star, 'description': 'Level One'}, + {'level': 20, 'name': 'Level 2', 'icon': icons.star, 'description': 'Level Two'}, + {'level': 30, 'name': 'Level 3', 'icon': icons.star, 'description': 'Level Three'}, + ) + + self.fixed_achievement = FixedAchievement + self.fixed_tracker.register(FixedAchievement) + people = {"Greg": 10, "Cindy": 20, "Megan": 30} + for name, value in people.items(): + self.fixed_tracker.increment(name, FixedAchievement, value) + def test_bad_backend(self): self.assertRaises(ValueError, self.tracker.set_backend, AchievementTracker) @@ -107,6 +125,20 @@ def test_remove_id(self): print(self.tracker.get_tracked_ids()) self.assertEqual(len(self.tracker.get_tracked_ids()), len(TRACKED_IDS)-1) + def test_compare_stats(self): + results = self.fixed_tracker.compare_stats(["Greg", "Megan"], self.fixed_achievement) + self.assertEqual(results["mean"], 20) + self.assertEqual(results["median"], 20) + self.assertEqual(results["max"], 30) + self.assertEqual(results["min"], 10) + + def test_compare_global_stats(self): + results = self.fixed_tracker.compare_global_stats(self.fixed_achievement) + self.assertEqual(results["mean"], 20) + self.assertEqual(results["median"], 20) + self.assertEqual(results["max"], 30) + self.assertEqual(results["min"], 10) + class AchievementBackenedTests(unittest.TestCase): # only tests things that haven't been hit in TrackerTests @@ -128,7 +160,7 @@ def setUp(self): self.tracker.register(ACHIEVEMENTS) def tearDown(self): - os.remove(self.dbfile.name) + os.remove(self.dbfile.name) # Fails on windows. This should be looked into later. def test_increment(self): tid = random.choice(TRACKED_IDS)