Fix tag game logic: time counting, penalties, and holder updates#2
Conversation
- Fixed `process_new_tag` to correctly identify the tagger as the holder and apply penalties to them. - Fixed `process_new_tag` to update `GameSettings.current_tag_holder`. - Updated `GameSettings.save` to automatically update `tag_holder_since` when holder changes. - Updated `calculate_leaderboard` to subtract penalties from the giver (holder) and include live pending penalties. - Added `current_tag_holder` and `tag_holder_since` to Admin UI. Co-authored-by: magi-9 <156084637+magi-9@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
There was a problem hiding this comment.
Pull request overview
This PR addresses critical bugs in the tag game's scoring and state management logic. The changes ensure that time penalties are correctly applied to taggers (not receivers), that the initial tag holder's time is properly tracked, and that the current tag holder state is accurately maintained.
Changes:
- Added automatic
tag_holder_sincetimestamp tracking whencurrent_tag_holderchanges inGameSettings.save() - Fixed penalty application to charge taggers for time held before tagging (instead of charging the tagged player)
- Updated leaderboard calculation to include pending penalties for the current holder
- Exposed tag holder state fields in Django admin for better game management
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| backend/game/models.py | Added logic to automatically set tag_holder_since when current_tag_holder changes |
| backend/game/game_engine.py | Fixed penalty logic to charge tagger instead of tagged player; added pending penalty calculation for current holder in leaderboard; updated holder state after tag creation |
| backend/game/admin.py | Added Current State fieldset to expose current_tag_holder and tag_holder_since in admin interface |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Check if current_tag_holder changed | ||
| try: | ||
| old_instance = GameSettings.objects.get(pk=1) | ||
| if old_instance.current_tag_holder != self.current_tag_holder: | ||
| self.tag_holder_since = timezone.now() | ||
| except GameSettings.DoesNotExist: | ||
| # First time creation | ||
| if self.current_tag_holder and not self.tag_holder_since: | ||
| self.tag_holder_since = timezone.now() |
There was a problem hiding this comment.
The automatic update of tag_holder_since in the save() method can lead to a race condition. When process_new_tag() calls settings.save() (line 193 in game_engine.py), there's a window between fetching the old instance (line 64) and saving the new one (line 72) where another concurrent tag operation could interfere. This could result in incorrect tag_holder_since timestamps or the current_tag_holder being out of sync.
Consider using select_for_update() to lock the row during the update, or handle the tag_holder_since update explicitly in process_new_tag() before calling save() to avoid relying on implicit behavior that's vulnerable to race conditions.
| hours_held = time_diff.total_seconds() / 3600 | ||
| pending_penalty = int(hours_held) * settings.time_penalty_per_hour |
There was a problem hiding this comment.
The pending penalty calculation uses int(hours_held) which truncates the decimal part. This means if someone holds the tag for 1.99 hours, they only get penalized for 1 hour. While this might be intentional, it's inconsistent with the calculation at line 160 which uses the same pattern. Consider documenting this truncation behavior or using round() or math.floor() for clarity, or even better, use math.ceil() if partial hours should count as full hours for penalty purposes.
| hours_held = time_diff.total_seconds() / 3600 | ||
| pending_penalty = int(hours_held) * settings.time_penalty_per_hour |
There was a problem hiding this comment.
Same issue as above - the pending penalty calculation uses int(hours_held) which truncates decimal hours. This means a player holding for 1.99 hours only gets penalized for 1 hour. For consistency and clarity, consider using the same rounding function throughout (e.g., math.floor(), round(), or math.ceil()) and documenting the intended behavior.
| # No penalty for tagged user (they just got it) | ||
| # No time held for tagged user (they just got it) |
There was a problem hiding this comment.
The comment states "No penalty for tagged user (they just got it)" and "No time held for tagged user (they just got it)" but these comments are misleading. The tagged user (receiver) never had a penalty or time_held to apply in the first place - those belong to the tagger. These comments make it sound like we're intentionally skipping something that should be done. Consider rewording to: "Tagged user only increments received count (penalty and time tracking apply to tagger)" or simply removing these comments as the code is self-explanatory.
| if settings.tag_holder_since: | ||
| time_held = timezone.now() - settings.tag_holder_since |
There was a problem hiding this comment.
There's a subtle timing issue here. The code calculates time_held using timezone.now() at line 149, then later creates the Tag with tagged_at=timezone.now() at line 180. If there's any delay between these two calls (database queries, leaderboard calculation at line 163), the time_held stored in the Tag record won't exactly match the difference between the new Tag's tagged_at and the previous tag_holder_since. Consider capturing timezone.now() once at the start of the function and using it consistently for both time_held calculation and the Tag's tagged_at field.
| # Fallback if settings.tag_holder_since is missing (should not happen if configured correctly) | ||
| last_received_tag = Tag.objects.filter(tagged=tagger, verified=True).order_by('-tagged_at').first() | ||
| if last_received_tag: | ||
| time_held = timezone.now() - last_received_tag.tagged_at | ||
| else: | ||
| time_held = timedelta() |
There was a problem hiding this comment.
The fallback logic when settings.tag_holder_since is missing queries for tags received by the tagger (line 152). However, if the tagger is the initial holder and has never received a tag, this will correctly return None and time_held will be timedelta() (line 156). But if the tagger received tags in the past and then became holder through admin assignment (not through a tag event), this will calculate time from their last received tag instead of when they became holder, which is incorrect. The comment on line 151 acknowledges this shouldn't happen, but it's worth validating that tag_holder_since is always set when current_tag_holder changes, or raising an error here instead of using a potentially incorrect fallback.
| # Fallback if settings.tag_holder_since is missing (should not happen if configured correctly) | |
| last_received_tag = Tag.objects.filter(tagged=tagger, verified=True).order_by('-tagged_at').first() | |
| if last_received_tag: | |
| time_held = timezone.now() - last_received_tag.tagged_at | |
| else: | |
| time_held = timedelta() | |
| # If this happens, the game settings are in an inconsistent state and we cannot | |
| # reliably determine how long the current holder has held the tag. | |
| raise ValueError("Tag holder start time (tag_holder_since) is not set; cannot calculate time held") |
| @@ -58,11 +55,25 @@ def calculate_leaderboard(): | |||
| # If this is the last tag and user is current holder | |||
| if user == current_holder: | |||
| time_diff = timezone.now() - tag.tagged_at | |||
There was a problem hiding this comment.
The time_held calculation logic has an issue. At lines 50-65, when calculating time_held from tags_received, the code uses tag.tagged_at (when the user received the tag). However, for the current holder case at line 57, it should use settings.tag_holder_since instead of tag.tagged_at to be consistent with how time is tracked in process_new_tag. If tag.tagged_at and settings.tag_holder_since differ (which they shouldn't but could due to timing or the race condition in the save() method), this will produce inconsistent results.
| time_diff = timezone.now() - tag.tagged_at | |
| # Use tag_holder_since as the canonical start time when available | |
| start_time = settings.tag_holder_since or tag.tagged_at | |
| time_diff = timezone.now() - start_time |
| # Update Game Settings with new holder | ||
| settings.current_tag_holder = tagged | ||
| # tag_holder_since will be updated automatically by GameSettings.save() | ||
| settings.save() |
There was a problem hiding this comment.
The critical bug fixes in this PR (penalty application to tagger instead of tagged, time counting for initial holder, tag_holder_since updates) lack test coverage. Consider adding tests that verify:
- When a tag is created, the tagger's total_points is decreased by time_penalty (not the tagged user's)
- The tag_holder_since is updated when current_tag_holder changes
- The initial holder's time is correctly counted when they have no received tags
- Pending penalties are correctly calculated in the leaderboard for the current holder
This is especially important given these are bug fixes for critical game logic.
|
@jules check copilot review above |
Thank you for the heads-up. I've reviewed the code and identified a critical flaw in the leaderboard calculation (it was incorrectly summing intervals between received tags). I've refactored |
…ility - Refactored `calculate_leaderboard` to sum `time_held` from `tags_given` (completed holds) instead of inferring from received tag intervals. - Implemented pending time calculation for the current holder in `calculate_leaderboard`. - Added fallback logic: if `tag_holder_since` is missing (legacy running game state), calculate pending time from the last received tag. - Verified that `GameSettings` model fields `current_tag_holder` and `tag_holder_since` exist and are correctly used. - Verified that `Tag.objects.create` receives the calculated `time_held`. Co-authored-by: magi-9 <156084637+magi-9@users.noreply.github.com>
This PR fixes several critical bugs in the game logic:
GameSettings.tag_holder_sinceand ensuring it's updated correctly.GameSettings.current_tag_holderinprocess_new_tag.PR created automatically by Jules for task 8585411131460583284 started by @magi-9