Repository navigation
Redesign user rating to reward skill instead of AC rate - #1048
Conversation
The old rating was accepted/submitted x 1000, which anyone could inflate by only submitting easy problems. The new 0-3000 rating is computed from one userinfo.php page: attempt-weighted solve volume (saturating), shrunk first-try and per-attempt accuracy that peaks at 45-60%, active days, and a per-day AC soft cap against bulk pasting. It never uses the current time, so every viewer computes the same value. Colors move to six tiers and the cache key is renamed so stale AC-rate values are not reused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp
Reviewer's GuideThe PR replaces the easily farmed AC-rate score with a deterministic 0–3000 rating based on solve volume, retry behavior, daily activity, accuracy, and consistency; it also migrates caching, expands rating color tiers, and updates the profile regression expectation. Sequence diagram for rating cache migration and profile renderingsequenceDiagram
participant Viewer
participant Script
participant Cache
participant UserInfoPage
Viewer->>Script: GetUserInfo(Username)
Script->>Cache: Read UserRatingV2
alt Cached V2 rating is valid
Cache-->>Script: Return rating and EmailHash
else Cache miss or expired
Script->>UserInfoPage: Fetch userinfo.php
UserInfoPage-->>Script: Profile HTML
Script->>Script: CalculateUserRating(ParsedDocument)
Script->>Cache: Write UserRatingV2
end
Script-->>Viewer: Render rating and username color tiers
Flow diagram for deterministic user rating calculationflowchart TD
A[User profile page] --> B[Parse p id attempts list]
A --> C[Read daily activity history]
B --> D[Calculate solve volume with retry weighting]
C --> E[Apply daily burst cap]
B --> F[Calculate shrunk accuracy]
C --> G[Calculate consistency from active days]
D --> H[Combine weighted score]
E --> H
F --> H
G --> H
H --> I[Scale to deterministic rating 0-3000]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Deploying xmoj-script-dev-channel with
|
| Latest commit: |
23c9c02
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://dea424df.xmoj-script-dev-channel.pages.dev |
| Branch Preview URL: | https://redesign-rating.xmoj-script-dev-channel.pages.dev |
There was a problem hiding this comment.
1 issue found across 4 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Update.json">
<violation number="1" location="Update.json:3885">
P3: The rating documentation URL is rendered as non-clickable text because `Notes` is inserted as HTML. Wrap the URL in an `<a>` so readers can open the linked explanation.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| "Description": "Redesign user rating to reward skill instead of AC rate" | ||
| } | ||
| ], | ||
| "Notes": "用户评分重新设计:从「AC 率 × 1000」改为综合做题量、准确度和坚持度的 0–3000 分,更难刷分,用户名颜色改为六档。详见 https://docs.xmoj-script.uk/features/rating.html" |
There was a problem hiding this comment.
P3: The rating documentation URL is rendered as non-clickable text because Notes is inserted as HTML. Wrap the URL in an <a> so readers can open the linked explanation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Update.json, line 3885:
<comment>The rating documentation URL is rendered as non-clickable text because `Notes` is inserted as HTML. Wrap the URL in an `<a>` so readers can open the linked explanation.</comment>
<file context>
@@ -3872,6 +3872,17 @@
+ "Description": "Redesign user rating to reward skill instead of AC rate"
+ }
+ ],
+ "Notes": "用户评分重新设计:从「AC 率 × 1000」改为综合做题量、准确度和坚持度的 0–3000 分,更难刷分,用户名颜色改为六档。详见 https://docs.xmoj-script.uk/features/rating.html"
}
}
</file context>
| "Notes": "用户评分重新设计:从「AC 率 × 1000」改为综合做题量、准确度和坚持度的 0–3000 分,更难刷分,用户名颜色改为六档。详见 https://docs.xmoj-script.uk/features/rating.html" | |
| "Notes": "<p>用户评分重新设计:从「AC 率 × 1000」改为综合做题量、准确度和坚持度的 0–3000 分,更难刷分,用户名颜色改为六档。详见 <a href=\"https://docs.xmoj-script.uk/features/rating.html\">评分说明</a></p>" |
There was a problem hiding this comment.
1 issue found and verified against the latest diff
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="XMOJ.user.js">
<violation number="1" location="XMOJ.user.js:2419">
P2: Missing or unparseable activity history sets `burst` to 1, so parsed solves receive full uncapped volume and bypass the bulk-solving penalty. Use a conservative fallback when daily counts are unavailable.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| totalAC += count; | ||
| cappedAC += count <= DailyCap ? count : DailyCap + DailyCap * Math.log(1 + (count - DailyCap) / DailyCap); | ||
| } | ||
| let burst = totalAC > 0 ? cappedAC / totalAC : 1; |
There was a problem hiding this comment.
P2: Missing or unparseable activity history sets burst to 1, so parsed solves receive full uncapped volume and bypass the bulk-solving penalty. Use a conservative fallback when daily counts are unavailable.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At XMOJ.user.js, line 2419:
<comment>Missing or unparseable activity history sets `burst` to 1, so parsed solves receive full uncapped volume and bypass the bulk-solving penalty. Use a conservative fallback when daily counts are unavailable.</comment>
<file context>
@@ -2393,6 +2397,43 @@ function GetProfileActivityData(root = document) {
+ totalAC += count;
+ cappedAC += count <= DailyCap ? count : DailyCap + DailyCap * Math.log(1 + (count - DailyCap) / DailyCap);
+ }
+ let burst = totalAC > 0 ? cappedAC / totalAC : 1;
+ // Volume: each solve is worth 1 on the first try and decays slowly with retries.
+ let weighted = attempts.reduce((sum, count) => sum + 1 / (1 + 0.4 * Math.log(count)), 0);
</file context>
| let burst = totalAC > 0 ? cappedAC / totalAC : 1; | |
| let burst = totalAC > 0 ? cappedAC / totalAC : 0; |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: afea98ef2d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| for (let script of root.querySelector("#statics")?.querySelectorAll("script") || []) { | ||
| for (let match of script.textContent.matchAll(/\bp\(\s*\d+\s*,\s*(\d+)\s*\)/g)) attempts.push(Math.max(1, Number(match[1]))); |
There was a problem hiding this comment.
Handle profiles that expose solved problems as links
When a profile contains solved-problem <a href="problem.php?id=..."> elements but no raw p(id, attempts) script, this loop leaves attempts empty and the function returns a rating of 0. That representation is explicitly supported by GetProfileSolvedProblems, so affected established users will display a zero rating and receive the lowest username color both on their profile and wherever GetUserInfo is used. The rating parser should handle the link-based representation as well, using a safe fallback when an attempt count is unavailable.
Useful? React with 👍 / 👎.
The number after each solved problem on userinfo.php is the total submit count for that problem, including submissions after AC, so it cannot measure attempts before AC. Volume now uses the solved count, and accuracy uses the exact verdict totals: solved / (solved + PE+WA+TLE+MLE+OLE+RE), shrunk with 40 virtual submissions and peaking at 40%-55%. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp
/web/contest/<cid>/rank-correct never called GetUsernameHTML, so usernames had no avatar, rating color or badge, unlike contestrank-oi.php. Vue owns the rank table and reuses rows when it re-sorts, so hide its link and add a script-owned copy keyed by username, redrawn only when the row's user changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="XMOJ.user.js">
<violation number="1" location="XMOJ.user.js:3235">
P2: Wait to hide the Vue anchor until its replacement renders: `GetUsernameHTML` catches lookup or badge failures and leaves only the spinner/text, removing the row's working profile link.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| if (span?.dataset.username === username) continue; | ||
| span?.remove(); | ||
| if (!username) continue; | ||
| link.hidden = true; |
There was a problem hiding this comment.
P2: Wait to hide the Vue anchor until its replacement renders: GetUsernameHTML catches lookup or badge failures and leaves only the spinner/text, removing the row's working profile link.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At XMOJ.user.js, line 3235:
<comment>Wait to hide the Vue anchor until its replacement renders: `GetUsernameHTML` catches lookup or badge failures and leaves only the spinner/text, removing the row's working profile link.</comment>
<file context>
@@ -3219,6 +3219,27 @@ async function InitializeContestWebApp() {
+ if (span?.dataset.username === username) continue;
+ span?.remove();
+ if (!username) continue;
+ link.hidden = true;
+ span = MakeControl("span", "rank-user", "");
+ span.dataset.username = username;
</file context>
Decode the server's cell colors into tries counts and shaded green/red backgrounds like contestrank-oi.php, show the rank in a badge instead of the server's orange cell, render nick and AC count as plain text, stop the general .well theme rule from boxing problem cells, center cells vertically, and use the black MonochromeUI header. Vue resets inline colors when it patches a row, so the observer now also watches style changes and each cell remembers the color Vue set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp
Sourcery withdrew this approval because it has stopped reviewing this pull request.
|
Sourcery has withdrawn its approval of this pull request. It auto-reviews a pull request 5 times, and this push is past that limit, so the approval no longer reflects code Sourcery has read. Comment |
806b65c to
db57c1d
Compare
The rank table's .xmoj-scroll-x wrapper clipped the right columns once tries counts widened the cells, and macOS hides its scrollbar. Let it overflow into the page as contestrank-oi.php does. Also drop the MonochromeUI underline, which is a border-bottom, from the nick and AC count links. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp
The server labels first place "Winner" while contestrank-oi.php shows the number. Other ranks already arrive as numbers and "*" stays as is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp
The new rating is cached under "-UserRatingV2", which left every old AC-rate value under "UserScript-User-<name>-UserRating" behind. Delete those once, guarded by a marker key so localStorage is not rescanned on every page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp
Bootstrap's link-* colors change with theme and hover: the green was too dark on the dark background and the yellow nearly unreadable on white. Rating tiers now use their own classes with one palette, tuned for at least 3.4:1 contrast on both backgrounds, injected on legacy pages and in /web. The yellow tier becomes a vivid orange. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp
Like Bootstrap's link-* hover, darken the tier color by 20% and make the underline opaque. The shades are fixed, so they match on every page and theme. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="XMOJ.user.js">
<violation number="1" location="XMOJ.user.js:694">
P2: The hover/focus shades fall below the stated 3.4:1 dark-theme contrast minimum: red is 3.05:1 on `#1a1a1a` (orange, blue, and gray also fail). Use hover shades that preserve the minimum.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| // Bootstrap, hover darkens the color by 20% and makes the underline opaque. | ||
| const RatingColorCSS = ` | ||
| a.xmoj-rating-red { color: #e8434b !important; text-decoration-color: rgba(232, 67, 75, 0.5) !important; } | ||
| a.xmoj-rating-red:hover, a.xmoj-rating-red:focus { color: #ba363c !important; text-decoration-color: #ba363c !important; } |
There was a problem hiding this comment.
P2: The hover/focus shades fall below the stated 3.4:1 dark-theme contrast minimum: red is 3.05:1 on #1a1a1a (orange, blue, and gray also fail). Use hover shades that preserve the minimum.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At XMOJ.user.js, line 694:
<comment>The hover/focus shades fall below the stated 3.4:1 dark-theme contrast minimum: red is 3.05:1 on `#1a1a1a` (orange, blue, and gray also fail). Use hover shades that preserve the minimum.</comment>
<file context>
@@ -685,6 +685,25 @@ const ThemeCanvasCSS = `
+// Bootstrap, hover darkens the color by 20% and makes the underline opaque.
+const RatingColorCSS = `
+ a.xmoj-rating-red { color: #e8434b !important; text-decoration-color: rgba(232, 67, 75, 0.5) !important; }
+ a.xmoj-rating-red:hover, a.xmoj-rating-red:focus { color: #ba363c !important; text-decoration-color: #ba363c !important; }
+ a.xmoj-rating-orange { color: #e8590c !important; text-decoration-color: rgba(232, 89, 12, 0.5) !important; }
+ a.xmoj-rating-orange:hover, a.xmoj-rating-orange:focus { color: #ba470a !important; text-decoration-color: #ba470a !important; }
</file context>
The two were only about 30 degrees apart in hue at the same lightness and read as one color. Red moves toward crimson and orange toward amber, more than doubling their perceptual distance while staying readable on both backgrounds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp
dc8147e to
ffd6b47
Compare
The amber looked muddy. With red now crimson, the vivid orange stays clearly distinct from it and has better contrast on white. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp
CalculateUserRating only read p(id, count) script calls, so a profile that lists solved problems as rendered problem.php links rated 0. Use GetProfileSolvedProblems, which handles both forms. Per-problem attempt counts are not used by the rating, so no fallback is needed for them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp
What does this PR aim to accomplish?:
The old rating was
accepted / submitted × 1000. Anyone could raise it by only submitting easy problems they were sure of. Strong users who attempt hard problems scored about the same as careful beginners, around 400.This PR replaces it with a rating that meets four goals:
Docs PR: XMOJ-Script-dev/docs#9
How does this PR accomplish the above?:
New function
CalculateUserRating(root). It is a pure function of oneuserinfo.phppage and never reads the current time. It only needs data the script already fetches: the solved list, the verdict totals and thed1/d2daily history.The number after each solved problem is the total submit count for that problem, including submissions after AC. It is not used.
1 − e^(−S·B/350), whereSis the number of solved problems.B: daily ACs above 8 grow only logarithmically. A day with 120 ACs counts as about 30. Normal training and contest days are almost unaffected.P = (S + 10) / (S + X + 40).Xis the total of PE, WA, TLE, MLE, OLE and RE verdicts; CE and ignored submissions are excluded. Resubmitting AC code changes neitherSnorX, so it can't inflate the score. The 40 virtual submissions keep small samples from scoring high. Credit peaks atP= 40–55% and falls off above that, because near-perfect accuracy over hundreds of solves looks copied. It is weighted byS / (S + 80).1 − e^(−days/250).round(3000 × score^0.9).Other changes:
GetUserInfoandInitializeUserProfileboth call the new function.-UserRatingV2, so cached AC-rate values are not read as new ratings. A one-time sweep, guarded by theUserScript-RatingV2-Migratedmarker, deletes the oldUserScript-User-*-UserRatingentries. Email hashes, timestamps and settings are kept.#dc2f5a, ≥2000 orange#e8590c(replaces yellow), ≥1600 blue#337cf2, ≥1200 green#2b9f4f, ≥800 cyan#1798b0, otherwise gray#78828c. They usexmoj-rating-*classes from a sharedRatingColorCSS, injected on legacy pages and in/web, instead of Bootstrap'slink-*classes. Those change with theme and hover: the old green had only 3.4:1 contrast on dark, and the yellow 1.6:1 on white. The new palette is the same on every page and in both themes, with at least 3.3:1 contrast on white and on dark./web/contest/<cid>/rank-correctnever calledGetUsernameHTML, so usernames there had no avatar, rating color or badge, unlikecontestrank-oi.php. The newEnhanceRank()runs in the existingEnhance()observer loop. Vue owns the table and reuses rows when it re-sorts, so the script hides Vue's link and adds an owned copy keyed by username. A row is redrawn only when its user changes, which means the asyncGetUsernameHTMLupdate can't re-trigger itself. I checked this on a mock Vue table, including a re-sort.contestrank-oi.phpdoes: tries counts like100 (3)/50 (2), green or red shading by tries, and purple for first blood..welltheme rule no longer boxes the problem cells, and cells are vertically centered.style.contestrank-oi.php, instead of being clipped by the.xmoj-scroll-xscroll box. macOS hides that box's scrollbar, so the clipping looked like a bug.profile-pagetest expectation is updated for the fixture: 400 → 33, checked by hand against the formula.How it was checked:
npm testsuite passes locally: 99/99, run against a locally cached Chromium build. The rank-cell logic was also tested in real Chromium against the server's actual color values, including Vue re-patching the color, the text, or both.Known limitations:
By submitting this pull request, I confirm the following:
🤖 Generated with Claude Code
https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp