feat(#1996): add time management rating for game uploads#2007
feat(#1996): add time management rating for game uploads#2007Dblike wants to merge 8 commits intojackstenglein:devfrom
Conversation
- Add TIME_MANAGEMENT variant to TS and Go RatingSystem enums - Add -1 rating boundary entries for all cohorts - Add clock icon to RatingSystemIcons - Add formatRatingSystem display name - Add switch cases in RatingCard, PreferredRatingSystemForm - Include lock file updates from dependency resolution
- Create common/src/ratings/clockRating.ts with calculateTimeRating(), getPerfectLineSeconds(), ClockDatum interface, and constants - Replace frontend clockRating.ts with re-exports from common - Remove unused _side parameter from calculateTimeRating calls
- Add timeManagementRatingWhite/Black fields to Game schema (TS + Go) - Add numGames field to Rating interface for incremental aggregation - Create timeManagement.ts to extract clock data and calculate ratings - Skip games without real clock annotations (hasClockData guard) - Integrate into getGame() for creation and getGameUpdate() for updates - Add updateUserTimeManagementRating() with incremental aggregation - Guard against missing ratings map on user record
- Create common/src/ratings/timeManagement.ts with incremental model: updateTimeManagementAggregate() stores only currentRating + numGames - Provisional (< MIN_GAMES_FOR_ELO): running average - Established (>= MIN_GAMES_FOR_ELO): USCF Elo draw adjustment (K=32) - Wire DojoScoreCard to read real user.ratings[TimeManagement] - Derive provisional chip from numGames using named constant
- timeManagement.test.ts: 7 tests for PGN clock extraction (classical with clocks, blitz, short game, no clocks, no TimeControl, getGame integration for both positive and negative cases) - timeManagementAggregation.test.ts: 11 tests for pure calculation (clockRating edge cases, incremental aggregation provisional/Elo transition, draw adjustment direction and equality)
Single-pass script that scans all games, calculates per-game TM ratings from PGN clock annotations, writes them to game records, and rebuilds user-level aggregates. - Idempotent: skips games that already have TM ratings - User aggregates rebuilt from scratch (not incremental) - Run via: stage=dev npx tsx pgnService/game/backfillTimeManagement.ts
8d7eb9a to
bba2703
Compare
Summary of changes: - Extract 0.5 magic number as DRAW_SCORE constant in timeManagement.ts - Replace directional Elo tests with hand-computed exact-value assertions - Add 500-point gap test cases for larger rating differences
bba2703 to
f39c14e
Compare
TimeManagement is a computed internal aggregate, not a user-entered external rating system. Storing it in the RatingSystem enum caused it to leak into search fields, preferred rating dropdowns, and rating boundary tables. Summary of changes: - Remove TimeManagement from RatingSystem enum (TS + Go) - Add dedicated User.timeManagementRating field (TimeManagementAggregate) - Remove numGames from Rating interface (was TM-only) - Remove 22 boundary table entries (all were -1 placeholders) - Simplify DynamoDB writes to a single top-level field SET - Clean up 7 frontend files that had TM switch cases or filters
a0a8325 to
1d4d0d3
Compare
|
@Dblike Thanks for the PR! I haven't reviewed the code yet (planning to get to it tomorrow) but this line stood out to me in the description:
Does this mean if I import my game, then edit it later, my overall time management rating isn't updated? I think that presents a big problem if so. Imagine I start entering my OTB game, and only enter clock data for half the game. Then a day later, I come back and enter the rest of the clock data. My time management rating for that game will be drastically different, so my overall time management rating should also change. Your open questions:
Similar to what you have, but let's have the rating right-aligned so there is space between it and the label. Also if provisional, let's add a
I don't think we should store a full history array. The team had previously discussed a moving average or exponential moving average of the most recent X games, where the value of X had not yet been figured out. What does incremental aggregation mean in this case? Is it just a simple average? |
ticket: #1996
summary
overview
Adds a per-game "time management rating" (0–3000) computed from clock annotations in the PGN, persisted on game records, and aggregated into a user-level rating on the profile. Only classical games (30+ min) with real
{[%clk ...]}data and 5+ moves are scored. Includes a backfill script for existing games.before + after
timeManagementRatingWhite/BlackUser.timeManagementRatingfield (not in RatingSystem enum)key changes
common/src/ratings/clockRating.ts(single source of truth)User.timeManagementRatingfield — not in theRatingSystemenum, since it's a computed internal metric, not a user-entered external ratingbackfillTimeManagement.ts) — idempotent, single-pass scanDesign decisions
Dedicated field, not a RatingSystem member —
RatingSystemrepresents external, user-entered rating systems (Chess.com, Lichess, FIDE, etc.). TimeManagement is a computed aggregate with no username, no graduation boundary, and shouldn't appear in rating selectors or search fields. A top-levelUser.timeManagementRatingfield avoids needing filters everywhere the codebase iteratesObject.values(RatingSystem).Store both white and black per-game ratings — cheap to compute in one pass, avoids ambiguity about which side the owner played, enables future comparison features.
Incremental aggregation (no history array) — DynamoDB User records are under size pressure (~400KB limit). A
gameRatings[]array has unbounded growth and race conditions under concurrent Lambdas. Instead we storecurrentRating+numGamesand update incrementally. Per-game ratings on Game records are the durable source of truth — the backfill script doubles as repair.Skip games without clock annotations — without real
clkdata, the fallback produces a flat line and a meaningless 0 rating. ThehasClockDataguard prevents pollution.No aggregate update on game edit — PGN edits are rare, correction is complex, drift is at most one game. Backfill script corrects accumulated drift.
Open questions
Q1: How should time management be displayed on the profile?
Currently rendered as a simple icon + text + provisional chip in DojoScoreCard. Every other row in the card uses
ScoreboardProgresswith a progress bar. Options: keep as-is, add progress bar (e.g.3/10toward established), use rating boundaries like the main rating system, or extract to its own component. Happy to implement whichever fits.Q2: Is incremental aggregation acceptable, or should we store a full history array?
Currently we store only
currentRating+numGameson the User record and update incrementally. This avoids unbounded array growth and race conditions under concurrent Lambdas, but means the aggregate can't be recalculated from the User record alone — per-game ratings on Game records are the source of truth, and the backfill script serves as the repair path. If a full history array is preferred (e.g. for charting rating over time), we'd need a cap or pagination strategy to stay within DynamoDB's 400KB item limit.