-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Implement punch combination detection #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,6 +12,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||
| from utils.pose_detector import PoseDetector | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from utils.punch_counter import PunchCounter | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from utils.ui_manager import UIManager | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from utils.combo_detector import ComboDetector # Added | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from utils.data_manager import DataManager | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from utils.calibration import Calibrator | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -24,6 +25,17 @@ def __init__(self): | |||||||||||||||||||||||||||||||||||||||||||||||||||
| self.data_manager = DataManager() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.calibrator = Calibrator() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Combo Detection related attributes | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.combo_detector = ComboDetector() # Uses default combos | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.current_detected_combo = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.last_combo_detected_time = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.combo_display_duration = 3.0 # seconds | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.combo_stats = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'attempts': 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'successes': 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'detected_combos': {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Application state | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.is_running = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.is_calibrating = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -49,6 +61,11 @@ def start_session(self): | |||||||||||||||||||||||||||||||||||||||||||||||||||
| """Start a new punching session""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.session_start_time = datetime.now() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.punch_counter.reset_counter() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Reset combo stats for the new session | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.current_detected_combo = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.last_combo_detected_time = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.combo_stats = {'attempts': 0, 'successes': 0, 'detected_combos': {}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| print("Combo stats reset for new session.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.data_manager.create_new_session() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| print(f"New session started at {self.session_start_time}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -60,7 +77,8 @@ def end_session(self): | |||||||||||||||||||||||||||||||||||||||||||||||||||
| 'duration': session_duration, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'total_punches': self.punch_counter.total_count, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'punch_types': self.punch_counter.get_punch_types_count(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'punches_per_minute': self.punch_counter.total_count / (session_duration / 60) if session_duration > 0 else 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'punches_per_minute': self.punch_counter.total_count / (session_duration / 60) if session_duration > 0 else 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'combo_stats': self.combo_stats | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.data_manager.save_session_data(session_data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| print(f"Session ended. {self.punch_counter.total_count} punches recorded over {session_duration:.1f} seconds.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -83,7 +101,11 @@ def process_frame(self, frame): | |||||||||||||||||||||||||||||||||||||||||||||||||||
| self.punch_counter.get_punch_types_count(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.session_start_time, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.punch_counter.velocity_threshold, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| paused=True | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| paused=True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| current_detected_combo=self.current_detected_combo, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| last_combo_detected_time=self.last_combo_detected_time, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| combo_display_duration=self.combo_display_duration, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| combo_stats=self.combo_stats | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self.is_calibrating: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -96,6 +118,24 @@ def process_frame(self, frame): | |||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Normal processing - detect punches | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| punches_detected = self.punch_counter.detect_punches(poses) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Combo Detection Logic | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(self.punch_counter.punch_event_history) > 0: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| detected_combo_name = self.combo_detector.detect_combo(self.punch_counter.punch_event_history) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if detected_combo_name: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self.current_detected_combo != detected_combo_name or \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| (time.time() - self.last_combo_detected_time > self.combo_display_duration): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| print(f"COMBO DETECTED: {detected_combo_name}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.current_detected_combo = detected_combo_name | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.last_combo_detected_time = time.time() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.combo_stats['successes'] += 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): combo_stats['attempts'] never gets incremented Increment 'attempts' when detect_combo is called or when a combo window starts to accurately track attempts. |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.combo_stats['detected_combos'][detected_combo_name] = \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.combo_stats['detected_combos'].get(detected_combo_name, 0) + 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.punch_counter.punch_event_history.clear() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+126
to
+138
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (code-quality): Merge nested if conditions (
Suggested change
ExplanationToo much nesting can make code difficult to understand, and this is especiallytrue in Python, where there are no brackets to help out with the delineation of different nesting levels. Reading deeply nested code is confusing, since you have to keep track of which |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Add visual feedback if punches are detected | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if punches_detected: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -111,7 +151,11 @@ def process_frame(self, frame): | |||||||||||||||||||||||||||||||||||||||||||||||||||
| self.punch_counter.get_punch_types_count(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.session_start_time, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.punch_counter.velocity_threshold, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| paused=False | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| paused=False, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| current_detected_combo=self.current_detected_combo, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| last_combo_detected_time=self.last_combo_detected_time, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| combo_display_duration=self.combo_display_duration, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| combo_stats=self.combo_stats | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Show debug visualization if enabled | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| import unittest | ||
| import time | ||
| from collections import deque | ||
| from utils.combo_detector import ComboDetector # Assuming utils is in PYTHONPATH or accessible | ||
|
|
||
| class TestComboDetector(unittest.TestCase): | ||
|
|
||
| def setUp(self): | ||
| # Using default combos from the ComboDetector class for these tests | ||
| self.detector = ComboDetector() | ||
| # Test specific combos for more fine-grained tests if needed | ||
| self.custom_combos = [ | ||
| {'name': 'Test-Jab-Cross', 'sequence': ['jab', 'cross'], 'timing': [0.5]}, | ||
| {'name': 'Test-Triple-Jab', 'sequence': ['jab', 'jab', 'jab'], 'timing': [0.3, 0.3]} | ||
| ] | ||
| self.custom_detector = ComboDetector(predefined_combos=self.custom_combos) | ||
|
|
||
| def test_empty_history(self): | ||
| history = deque(maxlen=5) | ||
| self.assertIsNone(self.detector.detect_combo(history)) | ||
|
|
||
| def test_history_too_short_for_any_combo(self): | ||
| history = deque(maxlen=5) | ||
| history.append(('jab', time.time())) | ||
| self.assertIsNone(self.detector.detect_combo(history)) | ||
|
|
||
| def test_simple_jab_cross_correct_timing(self): | ||
| history = deque(maxlen=5) | ||
| history.append((ComboDetector.PUNCH_JAB, time.time() - 0.4)) # 0.4s ago | ||
| history.append((ComboDetector.PUNCH_CROSS, time.time())) # now | ||
| # Uses default combo: {'name': 'Jab-Cross', 'sequence': ['jab', 'cross'], 'timing': [0.7]} | ||
| self.assertEqual(self.detector.detect_combo(history), 'Jab-Cross') | ||
|
|
||
| def test_simple_jab_cross_too_slow(self): | ||
| history = deque(maxlen=5) | ||
| history.append((ComboDetector.PUNCH_JAB, time.time() - 1.0)) # 1.0s ago | ||
| history.append((ComboDetector.PUNCH_CROSS, time.time())) # now | ||
| # Default Jab-Cross timing is 0.7s | ||
| self.assertIsNone(self.detector.detect_combo(history)) | ||
|
|
||
| def test_jab_jab_cross_correct_timing(self): | ||
| history = deque(maxlen=7) | ||
| history.append((ComboDetector.PUNCH_JAB, time.time() - 0.8)) # t0 | ||
| history.append((ComboDetector.PUNCH_JAB, time.time() - 0.4)) # t0 + 0.4s (within 0.5s for J-J) | ||
| history.append((ComboDetector.PUNCH_CROSS, time.time())) # t0 + 0.8s (0.4s after last J, within 0.7s for J-C) | ||
| # Default: {'name': 'Jab-Jab-Cross', 'sequence': ['jab', 'jab', 'cross'], 'timing': [0.5, 0.7]} | ||
| self.assertEqual(self.detector.detect_combo(history), 'Jab-Jab-Cross') | ||
|
|
||
| def test_jab_jab_cross_first_timing_too_slow(self): | ||
| history = deque(maxlen=7) | ||
| # Control timestamps precisely for testability | ||
| base_time = time.time() | ||
| history.append((ComboDetector.PUNCH_JAB, base_time - 1.0)) # t0 | ||
| history.append((ComboDetector.PUNCH_JAB, base_time - 0.3)) # t1 = t0 + 0.7s (too slow for J-J's 0.5s) | ||
| history.append((ComboDetector.PUNCH_CROSS, base_time)) # t2 = t1 + 0.3s | ||
| # Jab-Jab-Cross should fail. Jab-Cross (last two punches) should be detected. | ||
| self.assertEqual(self.detector.detect_combo(history), 'Jab-Cross') | ||
|
|
||
| def test_jab_jab_cross_second_timing_too_slow(self): | ||
| history = deque(maxlen=7) | ||
| history.append((ComboDetector.PUNCH_JAB, time.time() - 1.0)) # t0 | ||
| history.append((ComboDetector.PUNCH_JAB, time.time() - 0.8)) # t0 + 0.2s (ok for J-J's 0.5s) | ||
| history.append((ComboDetector.PUNCH_CROSS, time.time())) # t0 + 1.0s (0.8s after last J, too slow for J-C's 0.7s) | ||
| self.assertIsNone(self.detector.detect_combo(history)) | ||
|
|
||
| def test_incorrect_punch_type_in_sequence(self): | ||
| history = deque(maxlen=5) | ||
| history.append((ComboDetector.PUNCH_JAB, time.time() - 0.4)) | ||
| history.append((ComboDetector.PUNCH_HOOK, time.time())) # Should be CROSS for Jab-Cross | ||
| self.assertIsNone(self.detector.detect_combo(history)) | ||
|
|
||
| def test_longer_history_ending_in_valid_combo(self): | ||
| history = deque(maxlen=7) | ||
| history.append((ComboDetector.PUNCH_UPPERCUT, time.time() - 2.0)) | ||
| history.append((ComboDetector.PUNCH_HOOK, time.time() - 1.5)) | ||
| history.append((ComboDetector.PUNCH_JAB, time.time() - 0.4)) # Start of Jab-Cross | ||
| history.append((ComboDetector.PUNCH_CROSS, time.time())) # End of Jab-Cross | ||
| self.assertEqual(self.detector.detect_combo(history), 'Jab-Cross') | ||
|
|
||
| def test_sub_combo_detection_priority(self): | ||
| # DEFAULT_COMBOS are sorted by length descending in __init__ | ||
| # Jab-Jab-Cross vs Jab-Cross | ||
| history = deque(maxlen=7) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (code-quality): Extract duplicate code into method ( |
||
| # This sequence is a valid Jab-Jab-Cross | ||
| history.append((ComboDetector.PUNCH_JAB, time.time() - 0.8)) # t0 | ||
| history.append((ComboDetector.PUNCH_JAB, time.time() - 0.4)) # t0 + 0.4s (J-J OK) | ||
| history.append((ComboDetector.PUNCH_CROSS, time.time())) # t0 + 0.8s (J-C OK) | ||
| # Expected: Jab-Jab-Cross because it's longer and checked first | ||
| self.assertEqual(self.detector.detect_combo(history), 'Jab-Jab-Cross') | ||
|
|
||
| # This sequence is a valid Jab-Cross but NOT Jab-Jab-Cross due to first timing | ||
| history2 = deque(maxlen=7) | ||
| history2.append((ComboDetector.PUNCH_JAB, time.time() - 1.0)) # t0 | ||
| history2.append((ComboDetector.PUNCH_JAB, time.time() - 0.3)) # t0 + 0.7s (J-J too slow) | ||
| history2.append((ComboDetector.PUNCH_CROSS, time.time())) # t0 + 1.0s (J-C OK from last two punches) | ||
| # Expected: Jab-Cross because Jab-Jab-Cross fails on timing | ||
| self.assertEqual(self.detector.detect_combo(history2), 'Jab-Cross') | ||
|
|
||
|
|
||
| def test_custom_combos_correct_timing(self): | ||
| history = deque(maxlen=5) | ||
| history.append(('jab', time.time() - 0.3)) | ||
| history.append(('cross', time.time())) | ||
| self.assertEqual(self.custom_detector.detect_combo(history), 'Test-Jab-Cross') | ||
|
|
||
| def test_custom_combos_too_slow(self): | ||
| history = deque(maxlen=5) | ||
| history.append(('jab', time.time() - 0.6)) # Custom timing is 0.5s | ||
| history.append(('cross', time.time())) | ||
| self.assertIsNone(self.custom_detector.detect_combo(history)) | ||
|
|
||
| def test_exact_timing_boundary(self): | ||
| # Test with timing exactly at the boundary | ||
| # Jab-Cross default timing is 0.7s | ||
| # Construct timestamps to ensure exact difference | ||
| punch1_ts = time.time() | ||
| punch2_ts = punch1_ts + 0.7 # Exact difference of 0.7 | ||
|
||
|
|
||
| history = deque(maxlen=5) | ||
| history.append((ComboDetector.PUNCH_JAB, punch1_ts)) | ||
| history.append((ComboDetector.PUNCH_CROSS, punch2_ts)) | ||
| self.assertEqual(self.detector.detect_combo(history), 'Jab-Cross', "Should detect combo at exact timing boundary") | ||
|
|
||
| def test_just_over_timing_boundary(self): | ||
| # Test with timing just over the boundary | ||
| # Construct timestamps to ensure exact difference | ||
| punch1_ts = time.time() | ||
| punch2_ts = punch1_ts + 0.701 # Exact difference of 0.701 | ||
|
|
||
| history = deque(maxlen=5) | ||
| history.append((ComboDetector.PUNCH_JAB, punch1_ts)) | ||
| history.append((ComboDetector.PUNCH_CROSS, punch2_ts)) | ||
| self.assertIsNone(self.detector.detect_combo(history), "Should not detect combo just over timing boundary") | ||
|
|
||
| if __name__ == '__main__': | ||
| # This allows running the tests directly from this file | ||
| # You might need to adjust PYTHONPATH if utils is not found: | ||
| # Example: export PYTHONPATH=$PYTHONPATH:$(pwd)/.. (if tests is subdir of project root) | ||
| unittest.main() | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,160 @@ | ||||||||||||||||||||||
| import time | ||||||||||||||||||||||
| from collections import deque | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| class ComboDetector: | ||||||||||||||||||||||
| PUNCH_JAB = "jab" | ||||||||||||||||||||||
| PUNCH_CROSS = "cross" | ||||||||||||||||||||||
| PUNCH_HOOK = "hook" | ||||||||||||||||||||||
| PUNCH_UPPERCUT = "uppercut" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| DEFAULT_COMBOS = [ | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| 'name': 'Jab-Cross', | ||||||||||||||||||||||
| 'sequence': [PUNCH_JAB, PUNCH_CROSS], | ||||||||||||||||||||||
| 'timing': [0.7] # Max 0.7 seconds between Jab and Cross | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| 'name': 'Jab-Jab-Cross', | ||||||||||||||||||||||
| 'sequence': [PUNCH_JAB, PUNCH_JAB, PUNCH_CROSS], | ||||||||||||||||||||||
| 'timing': [0.5, 0.7] # Max 0.5s between J-J, Max 0.7s between J-C | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| 'name': 'Jab-Cross-Hook', | ||||||||||||||||||||||
| 'sequence': [PUNCH_JAB, PUNCH_CROSS, PUNCH_HOOK], | ||||||||||||||||||||||
| 'timing': [0.7, 0.7] # Max 0.7s between J-C, Max 0.7s between C-H | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| 'name': 'Hook-Cross-Hook', | ||||||||||||||||||||||
| 'sequence': [PUNCH_HOOK, PUNCH_CROSS, PUNCH_HOOK], | ||||||||||||||||||||||
| 'timing': [0.7, 0.7] | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| ] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def __init__(self, predefined_combos=None): | ||||||||||||||||||||||
| if predefined_combos is None: | ||||||||||||||||||||||
| self.predefined_combos = sorted(self.DEFAULT_COMBOS, key=lambda c: len(c['sequence']), reverse=True) | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. security (return-in-init): Source: opengrep |
||||||||||||||||||||||
| else: | ||||||||||||||||||||||
| self.predefined_combos = sorted(predefined_combos, key=lambda c: len(c['sequence']), reverse=True) | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. security (return-in-init): Source: opengrep |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def detect_combo(self, punch_history): | ||||||||||||||||||||||
| ''' | ||||||||||||||||||||||
| Detects a combo from a list of recent punches. | ||||||||||||||||||||||
| Args: | ||||||||||||||||||||||
| punch_history: A deque of tuples, where each tuple is (punch_type, timestamp). | ||||||||||||||||||||||
| The history is ordered from oldest to newest. | ||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||
| The name of the detected combo, or None if no combo is detected. | ||||||||||||||||||||||
| ''' | ||||||||||||||||||||||
| if not punch_history or len(punch_history) < 2: | ||||||||||||||||||||||
| return None | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| for combo in self.predefined_combos: | ||||||||||||||||||||||
| combo_sequence = combo['sequence'] | ||||||||||||||||||||||
| combo_timings = combo['timing'] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Check if the recent history is long enough for this combo | ||||||||||||||||||||||
| if len(punch_history) < len(combo_sequence): | ||||||||||||||||||||||
| continue | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Take the most recent punches from history that match the length of the combo sequence | ||||||||||||||||||||||
| relevant_history = list(punch_history)[-len(combo_sequence):] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Check punch sequence | ||||||||||||||||||||||
| match = True | ||||||||||||||||||||||
| for i, expected_punch in enumerate(combo_sequence): | ||||||||||||||||||||||
| if relevant_history[i][0] != expected_punch: | ||||||||||||||||||||||
| match = False | ||||||||||||||||||||||
| break | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
Comment on lines
+64
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (code-quality): We've found these issues:
Suggested change
|
||||||||||||||||||||||
| if not match: | ||||||||||||||||||||||
| continue | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Check timing | ||||||||||||||||||||||
| # relevant_history is [(punch_type, timestamp), (punch_type, timestamp), ...] | ||||||||||||||||||||||
| # combo_timings is [max_time_between_punch1_and_punch2, max_time_between_punch2_and_punch3, ...] | ||||||||||||||||||||||
| # There is one less timing value than there are punches in the sequence. | ||||||||||||||||||||||
| time_match = True | ||||||||||||||||||||||
| epsilon = 1e-6 # Using a slightly larger epsilon | ||||||||||||||||||||||
| for i in range(len(combo_timings)): | ||||||||||||||||||||||
| time_diff = relevant_history[i+1][1] - relevant_history[i][1] | ||||||||||||||||||||||
| if time_diff > (combo_timings[i] + epsilon): | ||||||||||||||||||||||
| time_match = False | ||||||||||||||||||||||
| break | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if time_match: | ||||||||||||||||||||||
| # Combo detected | ||||||||||||||||||||||
| return combo['name'] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return None | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if __name__ == '__main__': | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (testing): Demo test code in module; consider moving to separate tests Consider relocating the |
||||||||||||||||||||||
| # Example Usage and Basic Test | ||||||||||||||||||||||
| detector = ComboDetector() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Test Case 1: Jab-Cross | ||||||||||||||||||||||
| history1 = deque(maxlen=5) | ||||||||||||||||||||||
| history1.append(("jab", time.time() - 0.5)) | ||||||||||||||||||||||
| history1.append(("cross", time.time())) | ||||||||||||||||||||||
| print(f"History 1: {list(history1)}") | ||||||||||||||||||||||
| detected1 = detector.detect_combo(history1) | ||||||||||||||||||||||
| print(f"Detected Combo 1: {detected1}") # Expected: Jab-Cross | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Test Case 2: Jab-Jab-Cross | ||||||||||||||||||||||
| history2 = deque(maxlen=5) | ||||||||||||||||||||||
| history2.append(("jab", time.time() - 1.0)) | ||||||||||||||||||||||
| history2.append(("jab", time.time() - 0.4)) | ||||||||||||||||||||||
| history2.append(("cross", time.time())) | ||||||||||||||||||||||
| print(f"History 2: {list(history2)}") | ||||||||||||||||||||||
| detected2 = detector.detect_combo(history2) | ||||||||||||||||||||||
| print(f"Detected Combo 2: {detected2}") # Expected: Jab-Cross (Jab-Jab-Cross fails due to timing on J-J > 0.5s) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Test Case 3: Jab-Cross-Hook (Correct timing) | ||||||||||||||||||||||
| history3 = deque(maxlen=5) | ||||||||||||||||||||||
| history3.append(("jab", time.time() - 1.2)) | ||||||||||||||||||||||
| history3.append(("cross", time.time() - 0.6)) # 0.6s after jab | ||||||||||||||||||||||
| history3.append(("hook", time.time())) # 0.6s after cross | ||||||||||||||||||||||
| print(f"History 3: {list(history3)}") | ||||||||||||||||||||||
| detected3 = detector.detect_combo(history3) | ||||||||||||||||||||||
| print(f"Detected Combo 3: {detected3}") # Expected: Jab-Cross-Hook | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Test Case 4: Jab-Cross-Hook (Incorrect timing - too slow) | ||||||||||||||||||||||
| history4 = deque(maxlen=5) | ||||||||||||||||||||||
| history4.append(("jab", time.time() - 2.0)) | ||||||||||||||||||||||
| history4.append(("cross", time.time() - 0.8)) # 1.2s after jab (too slow for default J-C 0.7) | ||||||||||||||||||||||
| history4.append(("hook", time.time())) # 0.8s after cross | ||||||||||||||||||||||
| print(f"History 4: {list(history4)}") | ||||||||||||||||||||||
| detected4 = detector.detect_combo(history4) | ||||||||||||||||||||||
| print(f"Detected Combo 4: {detected4}") # Expected: None | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Test Case 5: Partial match (should not detect) | ||||||||||||||||||||||
| history5 = deque(maxlen=5) | ||||||||||||||||||||||
| history5.append(("jab", time.time() - 0.5)) | ||||||||||||||||||||||
| print(f"History 5: {list(history5)}") | ||||||||||||||||||||||
| detected5 = detector.detect_combo(history5) | ||||||||||||||||||||||
| print(f"Detected Combo 5: {detected5}") # Expected: None | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Test Case 6: Different combo | ||||||||||||||||||||||
| history6 = deque(maxlen=5) | ||||||||||||||||||||||
| history6.append(("hook", time.time() - 1.0)) | ||||||||||||||||||||||
| history6.append(("cross", time.time() - 0.5)) | ||||||||||||||||||||||
| history6.append(("hook", time.time())) | ||||||||||||||||||||||
| print(f"History 6: {list(history6)}") | ||||||||||||||||||||||
| detected6 = detector.detect_combo(history6) | ||||||||||||||||||||||
| print(f"Detected Combo 6: {detected6}") # Expected: Hook-Cross-Hook | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Test Case 7: No match | ||||||||||||||||||||||
| history7 = deque(maxlen=5) | ||||||||||||||||||||||
| history7.append(("uppercut", time.time() - 1.0)) | ||||||||||||||||||||||
| history7.append(("jab", time.time() - 0.5)) | ||||||||||||||||||||||
| history7.append(("hook", time.time())) | ||||||||||||||||||||||
| print(f"History 7: {list(history7)}") | ||||||||||||||||||||||
| detected7 = detector.detect_combo(history7) | ||||||||||||||||||||||
| print(f"Detected Combo 7: {detected7}") # Expected: None | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Test Case 8: Empty history | ||||||||||||||||||||||
| history8 = deque(maxlen=5) | ||||||||||||||||||||||
| print(f"History 8: {list(history8)}") | ||||||||||||||||||||||
| detected8 = detector.detect_combo(history8) | ||||||||||||||||||||||
| print(f"Detected Combo 8: {detected8}") # Expected: None | ||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Consider using a direct truthiness check (e.g., 'if self.punch_counter.punch_event_history:') for improved readability.