feat(compare): /compare command — side-by-side card stat embed (Roadmap 2.5b) #165
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#165
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "autonomous/feat-compare-command"
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.5b
Category: feature / scouting depth
What
New
/compareslash command: side-by-side stat embed for two cards with directional arrows highlighting deltas. Gated on matching card type.Batter stats: Cost (overall proxy), Rarity, Running, Steal Low, Steal High, Bunting, Hit & Run
Pitcher stats: Cost (overall proxy), Rarity, Starter Rating, Relief Rating, Closer Rating, Balk, Wild Pitch
Arrow semantics:
▲— card2 is better on this stat▼— card1 is better on this stat═— tied or missing dataWhy
Scouting depth is a Phase 2 engagement pillar. Card comparison is a staple of every competitive TCG and players currently have to eyeball stats manually. Becomes a building block for the future trade system (2.3a) — players will want to compare before accepting trades.
Implementation Notes
The database's per-card stats live in the battingcards/pitchingcards endpoints (running, steal range, starter/relief/closer ratings, balk, wild pitch). The battingcardratings endpoint with AVG/OBP/SLG per hand is team-auth-gated and not used here. Cost serves as the overall quality proxy since it is dynamically priced based on card performance.
Test Plan
Generated via Paper Dynasty autonomous pipeline (2026-04-10)
AI Code Review
Files Reviewed
cogs/compare.py(added — 425 lines, /compare cog)gauntlets.py(modified — gauntlet recap embed)in_game/gameplay_queries.py(modified — NoResultFound narrowing + reformatting)paperdynasty.py(modified — cog registration)tests/test_compare_command.py(added)tests/test_gauntlet_recap.py(added)Findings
Correctness
Bug:
(loss_max or 99)falsy-zero trap —gauntlets.py:build_gauntlet_recap_embedWhen
loss_max == 0(perfect-run-only reward), Python evaluates0 or 99 == 99. So for any completed run with losses (e.g. 10-1), the marker for the 10-0 reward evaluates as"❌" if 1 > 99→False→"⬜". A player who went 10-1 sees ⬜ (pending/not yet reached) for an unearnable reward instead of ❌ (ineligible). Since this embed only fires on run completion, there is no "pending" state — every marker is final.Fix:
The test
test_unearned_perfect_bonus_marked_correctlydoes not catch this — it only asserts"✅" not in bonus_line, which passes for both "⬜" and "❌". A tighter assertion (assert "❌" in bonus_line) is needed.Security
Style & Conventions
Scope: PR bundles three separate features
The diff contains changes from what appear to be three distinct autonomous PRs:
/comparecommand —cogs/compare.py+tests/test_compare_command.py(this PR)gauntlets.py+tests/test_gauntlet_recap.py(cf. PR #164, still open)in_game/gameplay_queries.py(cf. PR #163, still open)The PR title/body describes only the /compare feature. The gauntlet recap and NoResultFound changes are undisclosed. PRs #163 and #164 are still open with overlapping content, so merging this would cause conflicts.
cogs/compare.py— no issuesThe /compare implementation is clean:
grade_fieldpath in_delta_arrowignoreshigher_is_better(so theFalsein_BATTER_STATSfor bunting/hit_and_run is harmless though misleading in comment).CompareMismatchError) is intentional and correctly annotated._fetch_player_by_namewithlimit=1is fine given autocomplete.player.get("player_id") or player.get("id")safely handles both field name variants.Suggestions
test_unearned_perfect_bonus_marked_correctlytoassert "❌" in bonus_line(not just absence of ✅) regardless of the bug fix, so the test actually pins the intended behavior.Verdict: REQUEST_CHANGES
One correctness bug:
(loss_max or 99)ingauntlets.py:build_gauntlet_recap_embedproduces incorrect "⬜" markers for unearned perfect-run rewards. Fix is a one-liner. The test suite does not catch it.Automated review by Claude PR Reviewer
Addressed review feedback (
cc72827):Bug fix —
gauntlets.py:build_gauntlet_recap_embed(loss_max or 99)→(loss_max if loss_max is not None else 99)to avoid the Python falsy-zero trap. Aloss_max=0(perfect-run reward) was being treated as 99, so a 10-1 run would show ⬜ instead of ❌ for the 10-0 bonus.Test fix —
tests/test_gauntlet_recap.py:test_unearned_perfect_bonus_marked_correctlyassert "✅" not in bonus_linetoassert "❌" in bonus_line. The old assertion passed for both ⬜ and ❌, so it didn't catch the bug.Scope note: The review flagged that this PR bundles changes from three separate features (compare, gauntlet recap, NoResultFound narrowing). Since PRs #163 and #164 are still open with overlapping content, this creates merge-conflict risk. I've left the scope as-is — resolving that would require a rebase/cherry-pick strategy that should be decided by the author rather than automated feedback addressing.
AI Code Review
Files Reviewed
cogs/compare.py(added)gauntlets.py(modified — gauntlet recap embed)in_game/gameplay_queries.py(modified — NoResultFound + reformat)paperdynasty.py(modified — cog registration)tests/test_compare_command.py(added)tests/test_gauntlet_recap.py(added)Findings
Correctness
loss_maxbug previously flagged in this PR's earlier iteration ((loss_max or 99)treatingloss_max=0as99) is now correctly written as(loss_max if loss_max is not None else 99). The corresponding test (test_unearned_perfect_bonus_marked_correctly) has also been strengthened to assert"❌" in bonus_line, not just absence of"✅"— both the bug and the test gap are resolved.NoResultFoundimport ingameplay_queries.pyis used at 10 call sites — not unused.bunting,hit_and_run):_delta_arrowcorrectly ignoreshigher_is_betterwhengrade_field=True, using index-based comparison (A=0 wins over B=1). Settinghigher_is_better=Falseon those stat rows is effectively dead code but documents intent — harmless.compare_command(pre-fetch check +CompareMismatchErrorcatch) is intentional defence-in-depth; the "should not reach here" log confirms the author is aware.Security
card1,card2) is passed as query params todb_get(HTTP API), not directly into SQL. Role gate (PD_PLAYERS_ROLE_NAME) correctly guards the command.Style & Conventions
defer,edit_original_response). Pure builder function is testable without Discord state.Suggestions
/compare(its title), the gauntlet recap embed (also the subject of open PR #164), andgameplay_queries.pyNoResultFound narrowing (also the subject of open PR #163). Merging this while #163 or #164 are still open will cause merge conflicts or duplicate changes on those branches. Recommend closing #163 and #164 in favour of this PR, or rebasing them off this branch once it lands._build_card_data— None player_id edge case:pid = player.get("player_id") or player.get("id")falls back toNoneif neither key is present, which would passNoneto the battingcards/pitchingcards API. Practically impossible given valid API responses, but worth aif not pid: return {"player": player, "battingcard": None, "pitchingcard": None}guard.Verdict: COMMENT
Compare implementation is correct, well-tested, and follows project conventions. The gauntlet recap code bundled here is actually a fixed version of what's blocking PR #164. No correctness issues. Main action item is coordinating closure of #163 and #164 to avoid conflicts.
Automated review by Claude PR Reviewer
AI Code Review
Files Reviewed
cogs/compare.py(added — 425 lines, /compare command + embed builder)tests/test_compare_command.py(added — 395 lines, 30 tests)gauntlets.py(modified — gauntlet recap embed, same as PR #164)in_game/gameplay_queries.py(modified — NoResultFound narrowing + reformat, same as PR #163)paperdynasty.py(modified — cog registration)Findings
Correctness
No issues found.
_delta_arrowlogic is correct across all three paths (numeric higher/lower, grade fields, tied/missing). Thegrade_field=Truepath correctly short-circuits beforehigher_is_betteris consulted — grade semantics are self-contained.GRADE_ORDER.index("A") = 0;▲whenn2 < n1(card2 has a lower/better grade index). Verified againsttest_grade_field_a_beats_b.rarity_valuecomparison usesrarity.value(int) while display usesrarity.name(string) — intentional and correct.CompareMismatchErrorbranch in the command is correctly guarded with a comment noting it should not be reachable._fetch_player_by_nameusesplayers/searchendpoint confirmed to exist (utilities/autocomplete.py:79) and return{"players": [...]}— response key matches._build_card_datausesplayer.get("player_id") or player.get("id")— acceptableor-chain fallback given player IDs are non-zero.gauntlets.py/gameplay_queries.pychanges unchanged from prior approved reviews of #163 and #164.Security
No issues found. All API calls go through parameterised
db_get. No user input reaches raw queries or commands.Style & Conventions
No issues. Follows project patterns:
db_get,player_autocomplete,@app_commands.checks.has_any_role, try/except withlogger.error,await interaction.response.defer().Suggestions
Scope: close #163 and #164 before merging this PR. This PR still bundles all three features. #163 and #164 are open. If either merges to
mainfirst, this branch will need a rebase ongameplay_queries.py/gauntlets.py. Safest path: merge #163 → merge #164 → rebase this branch → merge this PR. Alternatively, close #163 and #164 since their changes are already included here._BATTER_STATStuple comment reads(label, key_in_card, key_in_player, higher_is_better)but the third element issource(the data source:"player","battingcard","pitchingcard"), not a key within the player dict. Minor comment inaccuracy, not a code issue.higher_is_better=Falseon grade-field entries ("bunting","hit_and_run") is a no-op — thegrade_field=Truepath ignores it entirely. The values could be anything without affecting output. ConsiderTrueor a note in the comment to avoid future confusion.Verdict: APPROVED
Clean implementation. Arrow semantics correct, edge cases covered,
players/searchendpoint confirmed. The only action item before merge is resolving the scope overlap with #163 and #164.Automated review by Claude PR Reviewer
Checkout
From your project repository, check out a new branch and test the changes.