feat(gauntlet): post completion recap embed on 10-win finish (Roadmap 2.4a) #164
No reviewers
Labels
No Label
ai-changes-requested
ai-failed
ai-pr-opened
ai-reviewed
ai-reviewing
ai-working
autonomous
bug
enhancement
feature
in-queue
performance
security
size:M
size:S
tech-debt
tests
todo
type:feature
type:stability
No Milestone
No project
No Assignees
3 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: cal/paper-dynasty-discord#164
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "autonomous/feat-gauntlet-results-recap"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Autonomous Pipeline Finding
Source: growth-po nightly sweep (2026-04-10)
Roadmap: Phase 2.4a
Category: feature / engagement
What
On gauntlet completion, bot auto-posts a results recap embed (champion, bracket, prizes) to the gauntlet's channel. Previously completion was silent.
Why
Gauntlets are a core engagement loop but players have no social validation for wins. Public recaps drive winner pride and FOMO for non-participants — standard TCG tournament pattern.
Implementation
build_gauntlet_recap_embed(run, event, main_team, rewards)— pure sync builder, gold colour, champion field (team name + user mention), final record, win-progression ladder, prize distribution table with earned/unearned markers.post_gauntlet_recap(run, event, main_team, channel)— async sender; fetches full rewards list then calls builder; gracefully skips if channel is None or any step raises (non-fatal).post_resultat thewins == 10completion path.Test Plan
🤖 Generated via Paper Dynasty autonomous pipeline (2026-04-10)
AI Code Review
Files Reviewed
gauntlets.py(modified — new functions + hook)in_game/gameplay_queries.py(modified — formatting/refactor only)tests/test_gauntlet_recap.py(added)Findings
Correctness
Bug:
loss_max or 99is falsy-unsafe whenloss_max=0— wrong marker on perfect-bonus tierIn
build_gauntlet_recap_embed, the marker logic for the prize distribution table is:The expression
loss_max or 99evaluates0as falsy and substitutes99. For a reward withloss_max=0(the 10-0 perfect-run bonus), any player withlosses >= 1will always evaluatelosses > 99asFalse, and receive⬜("not yet reached") instead of❌("definitively missed"). This is incorrect — a 10-1 finisher did not miss the perfect-bonus due to a pending condition.Fix:
The existing test
test_unearned_perfect_bonus_marked_correctlyonly asserts"✅" not in bonus_line, which passes for both⬜and❌, so it does not catch this bug. After applying the fix, tighten the assertion toassert "❌" in bonus_line.Verdict if this were a two-user repo: REQUEST_CHANGES (single correctness bug; everything else is clean).
Security
No issues found. The
<@gmid>mention is sourced from a server-controlled API integer, not user input. No PII is logged or leaked.Style & Conventions
gameplay_queries.pychanges are pure formatting plus narrowingexcept Exception→except NoResultFound— both improvements.is_win=Trueafter thewins == 10branch, never on a loss or mid-run win.post_gauntlet_recapcorrectly wraps all work in a singletry/except Exceptionso no failure path can interrupt gauntlet completion.Suggestions (non-blocking)
test_unearned_perfect_bonus_marked_correctlyto assert"❌" in bonus_line.post_gauntlet_recapre-fetches all rewards viadb_geteven thoughpost_resultalready fetched per-milestone rewards. Minor redundancy (one extra API call); acceptable given the non-fatal wrapper, but worth noting for future optimisation.Summary
One correctness bug:
loss_max or 99misclassifies the perfect-run bonus as "pending" instead of "missed" for any run with losses > 0. One-line fix. Hook logic, failure isolation, security, and overall test structure are solid. Do not merge until the marker bug is fixed.Automated review by Claude PR Reviewer — verdict: REQUEST_CHANGES (posted as COMMENT due to single-user repo restriction)
AI Code Review
Files Reviewed
gauntlets.py(modified — newbuild_gauntlet_recap_embed,post_gauntlet_recap, hook inpost_result)in_game/gameplay_queries.py(modified — import reformatting +NoResultFoundnarrowing)tests/test_gauntlet_recap.py(added — 14 tests for builder + async sender)Findings
Correctness
[BLOCKING]
gauntlets.py—(loss_max or 99)falsy-zero bug in prize marker logicWhen
loss_max=0(the perfect-run bonus, e.g. "10-0"), Python evaluates(0 or 99)→99. A player finishing 10-1 seeslosses > 99→ False →"⬜"instead of"❌". The ❌ marker for a missed perfect-run reward is silently replaced with a neutral indicator.Fix:
[BLOCKING]
tests/test_gauntlet_recap.py:test_unearned_perfect_bonus_marked_correctly— test does not catch the bug aboveThis assertion only verifies that the ✅ is absent, not that ❌ is present. The test passes even with the
(loss_max or 99)bug because"⬜"also satisfies"✅" not in bonus_line. The test needs:Security
No issues found.
Style & Conventions
Scope creep —
in_game/gameplay_queries.pychanges belong in PR #163This PR bundles
in_game/gameplay_queries.py(import reformatting +NoResultFoundnarrowing) alongside the gauntlet recap feature. PR #163 ("fix(gameplay): replace bare except with NoResultFound") is still open and targets the same file. The overlap creates merge conflict risk and makes both PRs harder to review independently. The gameplay_queries changes should be limited to PR #163; this PR should only containgauntlets.pyandtests/test_gauntlet_recap.py.Suggestions
post_resulthook at lineif this_run["wins"] == 10is correct — wins == 10 is the gauntlet completion threshold andthis_runis already updated before this point.post_gauntlet_recap(bareexcept Exception+logger.warning) is the right pattern for a recap side-effect that must never interrupt game flow.post_gauntlet_recapfunction correctly usesthis_event["id"]inside the try block — a missingidkey would be caught and logged, not crash the bot.Verdict: REQUEST_CHANGES
One correctness bug:
(loss_max or 99)treatsloss_max=0as99, causing a missed perfect-run reward to display⬜instead of❌. The existing test does not catch this. Fix both the production code and the assertion before merge.Automated review by Claude PR Reviewer
Review Feedback Addressed (
079f9ac)Blocking fixes
1.
loss_max or 99falsy-zero bug — fixedgauntlets.py:2475— changed:A player finishing 10-1 now correctly sees
❌on the 10-0 perfect-run bonus instead of⬜.2. Tightened test assertion — fixed
tests/test_gauntlet_recap.py:test_unearned_perfect_bonus_marked_correctly— changed:Scope cleanup
3. Reverted
in_game/gameplay_queries.pytomainThe import reformatting and
NoResultFoundnarrowing in this file belong in PR #163. Restored to main state to eliminate the merge conflict risk and keep this PR scoped to the gauntlet recap feature only.AI Code Review
Files Reviewed
gauntlets.py(modified —build_gauntlet_recap_embed,post_gauntlet_recap, hook inpost_result)tests/test_gauntlet_recap.py(added)Findings
Correctness
Previous blocking issues resolved:
loss_max=0bug — fixed. Prior version used(loss_max or 99)which treatedloss_max=0as99. Current code usesloss_max == 0for label andloss_max is not Noneforearned/marker checks. Python correctly evaluates0 is not NoneasTrue, so the 10-0 perfect-run bonus now correctly produces❌whenlosses > 0.Test assertion strengthened — fixed.
test_unearned_perfect_bonus_marked_correctlynow asserts"❌" in bonus_line(was asserting absence of✅, which didn't catch the bug). Traced: wins=10, losses=1, loss_max=0 → earned=False, losses > loss_max → marker❌. Test passes and regression-catches the prior bug.Scope issue — resolved.
in_game/gameplay_queries.pyis no longer in the diff. No merge conflict risk with PR #163.No new correctness issues found.
Security
No issues found. No user input reaches queries or commands.
gmidis interpolated into a Discord mention string (<@{gmid}>) — safe within Discord embed context; no HTML/injection risk.Style & Conventions
try/except Exception+logger.warning(exc_info=True)).Nonechannel guard logs and returns early — matches existing patterns in the codebase.GOLD = 0xFFD700defined inside the function body; could be module-level, but this is stylistic preference only and not a blocker.Suggestions
win_num = r.get("win_num", "?")— if a malformed reward row omitswin_num, the"?"string would causeTypeErrorin thewins >= win_numcomparison. The outertry/exceptinpost_gauntlet_recapcatches this gracefully, but a"?"default that can't be compared to an int is a latent footgun. Considerwin_num = r.get("win_num")and skipping rows wherewin_num is None. Not a blocker given the try/except guard.Verdict: APPROVED
Both blocking issues from the prior review are fixed with correct logic and strengthened tests. Implementation is clean, non-fatal, and scoped correctly to
gauntlets.py. Ready to merge.Automated review by Claude PR Reviewer
AI Code Review
Files Reviewed
gauntlets.py(modified —build_gauntlet_recap_embed,post_gauntlet_recap, hook inpost_result)tests/test_gauntlet_recap.py(added — 315 lines, 14 tests)Findings
Correctness
No issues found.
if is_win:block afterawait channel.send(...)at line 2546. All four args (this_run,this_event,main_team,channel) are in scope at that point.this_run["wins"]at the check point reflects the already-incremented value fromdb_patch— the== 10test is correct.earnedternary parses correctly:wins >= win_num and losses <= loss_max if loss_max is not None else wins >= win_num.markerlogic correctly distinguishes ✅ (earned) / ❌ (definitively missed, losses > loss_max) / ⬜ (pending).loss_max == 0label guard,loss_max is not Noneearned guard,"❌" in bonus_linetest assertion.Security
No issues found.
gmidused in Discord mention format comes from the teams API dict, not user input. No hardcoded credentials.Style & Conventions
No issues found. Non-fatal pattern, try/except with
logger.warning(exc_info=True), anddb_getcall style all consistent with the codebase.Suggestions
gauntlets.py—win_num = r.get("win_num", "?")(prize loop): the"?"default is used for the display label but is also implicitly compared viawins >= win_num— ifwin_numis ever"?", aTypeErrorpropagates up topost_gauntlet_recap's outerexcept Exception. Non-blocking since it's caught, and API responses should always includewin_num, butr.get("win_num", 0)would be a safer default that degrades gracefully instead of raising.Verdict: APPROVED
Clean implementation. Logic is correct, non-fatal pattern is consistent with the codebase, and test coverage is thorough (builder unit tests + async smoke tests covering the None channel, send failure, and db_get failure paths). Ready to merge.
Automated review by Claude PR Reviewer
Checkout
From your project repository, check out a new branch and test the changes.