feat: implement ranked game mode (#24) #73
No reviewers
Labels
No Label
ai-changes-requested
ai-failed
ai-pr-opened
ai-reviewed
ai-reviewing
ai-working
ai-working
bug
enhancement
feature
in-queue
performance
security
tech-debt
todo
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: cal/paper-dynasty-discord#73
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "ai/paper-dynasty-discord-24"
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?
Summary
/new-game rankedslash command (human-vs-human, blocks AI teams)Gamewithranked=Trueandgame_type='ranked'/new-game unlimited'ranked'case toget_scorebug_embedso the scorebug title shows" - Ranked"instead of falling through to the default" - Unlimited"Files changed
cogs/gameplay.py— newnew_game_ranked_commandmethod on theGameplaycogcommand_logic/logic_gameplay.py— oneelifadded toget_scorebug_embedOther observations
complete_gameinlogic_gameplay.py:4074already handles the ranked label viaelif this_game.ranked: game_des = "Ranked", so the score-report embed is already correct without changes.cogs/gameplay.pywas auto-reformatted by the project's ruff hook on edit, which accounts for the large diff. The functional change is thenew_game_ranked_commandmethod and therankedscorebug case.new_game_unlimited_commandpattern.AI Code Review
Files Reviewed
cogs/gameplay.py(modified — newnew_game_ranked_commandmethod + ruff reformat)command_logic/logic_gameplay.py(modified — oneelifinget_scorebug_embed+ ruff reformat)Findings
Correctness
Gamemodel hasranked: bool | None = Field(default=False)(line 114) andgame_type: str(line 122) — both fields exist and accept the values set by the new command.game_typefield_validator(line 149) only lowercases the value; no whitelist restriction, so"ranked"is valid.new_game_ranked_commandcorrectly setsranked=True, game_type="ranked"— the boolean and string fields complement each other (complete_game already useselif this_game.ranked:at line 4074 for the score-report label).new_game_ranked(notnew_game_unlimited).get_scorebug_embeduses==for ranked (exact match) rather thanin(substring). This is intentional and correct — unlikegauntletorflashback,"ranked"has no parameterized variants (e.g."gauntlet-run-1-cardset-2").Security
@app_commands.checks.has_any_role(PD_PLAYERS_ROLE_NAME)) is present, consistent with other game commands.Style & Conventions
new_game_checks,get_full_roster_from_sheets,ask_with_buttons).Suggestions
Verdict: APPROVED
Clean feature addition. The ranked command faithfully follows the unlimited command pattern with correct field values. The scorebug fix is minimal and correct. No blocking issues.
Automated review by Claude PR Reviewer
Hold — merge after next-release cleanup.
This has conflicts and touches core gameplay files. Deferring until the
next-release→mainconsolidation is complete and the PR backlog is cleared. Will need a fresh rebase againstmainat that point.AI Code Review
Files Reviewed
cogs/gameplay.py(modified — large ruff reformatting + newnew_game_ranked_commandmethod)command_logic/logic_gameplay.py(modified — scorebug ranked case + ruff reformatting)Findings
Correctness
Bug:
game_typefornew_game_unlimited_commandis set to"exhibition"(pre-existing, carried forward)The new
new_game_unlimited_commandblock (added lines ~1626–1638) creates aGamewithgame_type="exhibition". Looking at the diff, the original code also hadgame_type='exhibition'at that location — so this is not a regression introduced by this PR. However, it is worth flagging:/new-game unlimitedcreates a game tagged as"exhibition"type, which seems semantically incorrect. If this is intentional (legacy behavior), it should be documented.Potential bug: roster pull executes outside the
if away_roster_id and home_roster_idguard in ranked commandIn
new_game_ranked_command, thesession.add(this_game)/session.commit()block is correctly inside theif away_roster_id and home_roster_id:guard (lines ~1565–1568 in diff context). However,get_full_roster_from_sheets(...)for both teams is called unconditionally after the guard block (lines ~1573–1584). If either button prompt times out or returnsNone,away_roster_idwill still be the raw string"Primary"/"Secondary"/"Ranked"(because the int-conversion only happens inside the guard), and the game will not have been committed to the DB yet — but the roster fetch still runs. This is the same pattern as the existingnew_game_unlimited_command, so it is not a new bug per se, but the ranked command replicates the same structural risk.More specifically: if
away_roster_idisNone(timeout/cancel),get_full_roster_from_sheetsis still called withNoneas the roster ID. Whether that crashes depends on the function's internal handling, which is unchanged in this PR — but callers should ensure it is safe. If it is not safe, both commands are affected.Minor:
new_game_ranked_commanddoes not callsession.add/session.commiton the game if roster buttons time outIf
if away_roster_id and home_roster_id:isFalse, the game object is never persisted to the DB, but the roster fetch still fires. The game will exist in memory but never be committed. This means subsequent commands in the same channel won't find an active game, but the user also receives no error or cancellation message. A guard and user-facing message ("Roster selection timed out, game cancelled.") should be added if the existingnew_game_unlimited_commandalso lacks this — which it appears to, making this a pre-existing issue also replicated here.Security
No issues found. No user input is directly injected into queries, credentials are not hardcoded, and the
@app_commands.checks.has_any_role(PD_PLAYERS_ROLE_NAME)decorator is present onnew_game_ranked_command(line ~1447 in diff), consistent with all other new-game commands.Style & Conventions
The bulk of the diff (~95%) is ruff auto-reformatting (single quotes to double quotes, multi-line argument expansion, trailing comma normalization). This is consistent with the project's ruff hook and does not affect behavior. The functional changes are well-scoped.
One inconsistency: the
"ranked"case inget_scorebug_embedusesthis_game.game_type == "ranked"(exact match), while the"exhibition"case uses"exhibition" in this_game.game_type(substring match). This inconsistency is inherited from the existing code structure. The ranked case is correct as written, but for long-term consistency it would be worth normalizing the pattern.Suggestions
not (away_roster_id and home_roster_id)innew_game_ranked_command, to give players feedback if they miss the button prompt. This mirrors what a production-quality UX should do.game_type="exhibition"for the unlimited command is the intended permanent behavior or a long-standing bug that should be addressed separately.Required Change
The issue at
cogs/gameplay.pyinnew_game_ranked_command: if the roster buttons time out (away_roster_idorhome_roster_idisNone/falsy), there is no user-facing cancellation message and the function falls through to callget_full_roster_from_sheetsanyway. Add anelsebranch after theif away_roster_id and home_roster_id:block:This pattern is appropriate for a human-vs-human game mode. Whether to also fix it in
new_game_unlimited_commandis a judgment call, but it should at minimum be consistent in the new ranked command.Verdict: REQUEST_CHANGES
The core ranked game mode implementation is correct and the diff description is accurate. The
ranked=True/game_type="ranked"fields are set, the AI-block check fires properly, and the scorebug fix is wired correctly. The one required change is adding a graceful early exit when roster selection fails in the ranked command — the current code falls through toget_full_roster_from_sheetswith potentially unresolved roster IDs, which is a silent failure path. Themergeable: falseflag from Gitea should also be investigated before merge.Automated review by Claude PR Reviewer
@ -1129,1 +1539,4 @@await interaction.edit_original_response(content=f"We will leave {check_sp.player.name} on the lineup card.")returnIf
away_roster_idorhome_roster_idisNone(button timeout/cancel), execution falls through toget_full_roster_from_sheetswith the raw string value orNoneas the roster ID, and the game is never committed to the DB. Add anelsebranch here with a user-facing message andreturnto prevent silent failures.AI Code Review
Files Reviewed
cogs/gameplay.py(modified — newnew_game_ranked_commandmethod + ruff reformat)command_logic/logic_gameplay.py(modified — oneelifinget_scorebug_embed+ ruff reformat)Findings
Correctness
new_game_ranked_commandcorrectly creates aGamewithranked=Trueandgame_type="ranked", matching the PR description.if away_team.is_ai or home_team.is_ai) correctly blocks AI teams with an appropriate user-facing message.elif this_game.game_type == "ranked": gt_string = " - Ranked"is correctly placed after the"exhibition"check.game_type == "ranked"never contains "exhibition", so there is no overlap.complete_gameat line 4074 already handleselif this_game.ranked: game_des = "Ranked"— the score-report embed is correct without additional changes.new_game_unlimited_commandexactly: same roster-selection flow, sameask_with_buttonscall with["Primary", "Secondary", "Ranked"]options, samesession.commit()pattern. No logical divergence.mergeable: false— PR has conflicts withmain. The code itself is correct; branch needs a rebase before merge.Security
away_team_abbrev/home_team_abbrevpass throughnew_game_checksbefore use — consistent with all other game-start commands.Style & Conventions
new_game_ranked_command) follows thenew_game_*_commandconvention.Suggestions
game_typefield for unlimited games remains"exhibition"(pre-existing). This is out of scope for this PR but worth a future cleanup to use"unlimited"for semantic clarity.Verdict: APPROVED
Clean, minimal implementation. The ranked game command is a faithful copy of the unlimited pattern with the correct
ranked=True/game_type="ranked"overrides. The scorebug fix is correct. No blocking issues. Branch needs a rebase to resolve the merge conflict before it can land.Automated review by Claude PR Reviewer
Checkout
From your project repository, check out a new branch and test the changes.