Bowling League Secretary: Handicaps, Team Points and Frozen Weeks
EasyAI-assisted coding · AI-Assisted Debugging
Bowling League Secretary: Handicaps, Team Points and Frozen Weeks
You've inherited the scoring app for the Tuesday Night Mixed Bowling League. The secretary types each bowler's three games into it every week and expects it to print season averages, handicaps and the team standings. Three tickets are queued: the numbers on the sheet are wrong (Part 1), team matches are not built yet (Part 2), and the league board has just changed the handicap rule (Part 3). Game scores arrive as finished totals; there is no frame-by-frame scoring here.
Codebase
| File | What it does |
|---|---|
league/api.py | run(operations) entry point and the Dispatcher that routes each operation |
league/service.py | LeagueService: roster, recording scores, averages, handicaps, cards, matches |
league/handicap.py | Season average and handicap calculations |
league/scorebook.py | Scorebook: games per bowler and week, plus running season totals |
league/roster.py | Roster: bowlers and teams |
league/matches.py | MatchBook: which matches have been played |
league/standings.py | League table |
league/models.py | Bowler and MatchResult records |
league/config.py | League constants: handicap basis and percent, game limits, blind penalty, point values |
league/errors.py | LeagueError and every error code |
tests/ | Existing unit and end-to-end tests (some currently fail) |
How your code is run
run(operations: list[list]) -> list executes the operations in order against a fresh league and returns one result per operation. Every operation is [name, *args]. Errors are returned as strings ("error: <code>"), never raised. A *week* is an integer >= 1; weeks do not have to be recorded in order or consecutively.
| Operation | Arguments | Returns | Errors (checked in this order) |
|---|---|---|---|
add_bowler | bowler_id, team_id or bowler_id, team_id, entering_average | true | invalid arguments, bowler exists, team full |
roster | team_id | bowler ids in the order they joined | team not found |
record | week, bowler_id, [g1, g2, g3] | series total (sum of the three games) | invalid week, bowler not found, invalid scores, already recorded |
average | bowler_id | season average (int) | bowler not found, no average |
handicap | bowler_id | handicap (int) | bowler not found, no average |
card | bowler_id | {"bowler", "team", "games", "average", "handicap"} | bowler not found |
match | week, team_a, team_b | _Part 2_ | _Part 2_ |
standings | none | _Part 2_ | none |
- A team exists as soon as its first bowler is added; it holds at most 4 bowlers. A bowler id may be used once.
entering_averageis an integer 0-300 (not a bool) or omitted (the bowler is new and has no average on record). Empty or non-string ids areinvalid arguments. recordtakes exactly three integer games, each 0-300 (bools are not integers), and a bowler may be recorded once per week. The week must be an integer>= 1.cardshowsgames(games recorded), andaverage/handicapasnullwhen there is no average.- An unknown operation name is
"error: unknown operation". A wrong number of arguments, a non-list operation, or an empty operation is"error: invalid arguments". - League constants live in
league/config.py: handicap = floor((220 - average) x 90 / 100), never below 0.
Part 1 - The sheet's numbers are wrong
Goal: make average, handicap and card follow the league rules below. The secretary's complaints:
- "My average is a pin higher than I get on a calculator." (Averages are whole pins; check the rounding.)
- "The top bowler's handicap is *negative*, so he is being docked pins."
- "I joined with an entering average of 100, then bowled 200 in all three games, and the sheet still says 100."
Rules
- A bowler's average is the total pins over all games recorded so far (any week) divided by the number of games, dropping the fraction (floor).
- The entering average is used only while the bowler has no recorded games. As soon as one night is recorded it is replaced completely, not blended with the new games.
- A bowler with no recorded games and no entering average has no average:
averageandhandicapreturnerror: no average, andcardshowsnullfor both. So a new bowler's first handicap exists once that night's games are recorded. - Handicap = floor((220 - average) x 90 / 100), and never less than 0.
Worked example
[["add_bowler", "ann", "Pin Pals", 160],
["add_bowler", "ben", "Pin Pals"],
["handicap", "ann"],
["record", 1, "ann", [150, 150, 152]],
["average", "ann"],
["handicap", "ann"],
["record", 1, "ben", [182, 190, 177]],
["average", "ben"],
["handicap", "ben"],
["card", "ben"]]returns
[true, true, 54, 452, 150, 63, 549, 183, 33,
{"bowler": "ben", "team": "Pin Pals", "games": 3, "average": 183, "handicap": 33}]Ann's entering 160 gives handicap 54 until she bowls. Her 452 pins over 3 games average 150.67, which is 150. Ben has no entering average, so he has none until his night is recorded.
Part 2 - Team matches and standings
Goal: add match and standings. Two teams meet each week; each bowler's score is adjusted by their handicap, and the teams compete for points game by game and over the whole series.
["match", week, team_a, team_b] scores the match from what is recorded at that moment, files the result, and returns [half_points_a, half_points_b]. Points are counted in half-points so a tie can be split with integers: a win is worth 2 half-points, a tie 1 for each side.
Rules
- Errors, in order:
invalid week,team not found(either team,team_afirst),same team,team already played(either team already has a match filed for that week),no average(rule 3). A failed match changes nothing. - The lineup is every bowler on the team's roster at the moment
matchis called. A bowler with games recorded forweekis present; otherwise absent. - An absent bowler bowls blind: in each of the three games their raw score is their average minus 10 (an integer, may go below 0 for tiny averages), and their handicap comes from that same average. If an absent bowler has no average (see Part 1), the whole match fails with
no average. Blind scores are for the match only; they are never recorded as games. - Every lineup bowler, present or absent, gets their handicap added to each of their games. A present bowler's handicap and an absent bowler's average are the ones
handicapandaveragereport at that moment (everything recorded so far). - For each game, a team's total is the sum of its lineup's handicapped scores. The higher total wins the game (2 half-points); equal totals give 1 each. The series is the sum of a team's three game totals, and it is a fourth contest worth the same: higher wins 2, tied gives 1 each. A match therefore always distributes 8 half-points.
standingsreturns[[team_id, half_points], ...]for every team, most half-points first, ties byteam_idascending (plain string order). Teams that haven't played show 0.
Worked example
[["add_bowler", "ann", "Pin Pals", 150], ["add_bowler", "ben", "Pin Pals", 140],
["add_bowler", "cy", "Gutter Gang", 157], ["add_bowler", "di", "Gutter Gang", 145],
["record", 1, "ann", [150, 152, 148]], ["record", 1, "ben", [139, 141, 142]],
["record", 1, "cy", [170, 150, 153]],
["match", 1, "Pin Pals", "Gutter Gang"],
["standings"],
["match", 1, "Pin Pals", "Gutter Gang"]]returns [true, true, true, true, 450, 422, 473, [6, 2], [["Pin Pals", 6], ["Gutter Gang", 2]], "error: team already played"].
Handicaps: Ann 63, Ben 72, Cy 56, Di 67; Di is absent and bowls blind at 135. Game 1: Pin Pals 150+63 + 139+72 = 424, Gutter Gang 170+56 + 135+67 = 428, so the Gang wins it. Game 2: 428 vs 408, Game 3: 425 vs 411, both to Pin Pals. The series is 1277 vs 1247 to Pin Pals. Pin Pals: 2+2+2 = 6 half-points, Gutter Gang 2.
Tie example: two teams of one bowler each, entering average 150, bowl 150,150,150 and 160,140,150. Handicap 63: game 1 is 213 vs 223, game 2 is 213 vs 203, game 3 ties at 213, and the series ties at 639 each, so [4, 4].
Part 3 - The board freezes handicaps at the start of each week
Last season a bowler bowled a huge night and watched the handicap on the sheet drop before the match was scored. The board has voted: a bowler's handicap (and the average behind it) is fixed at the start of each week and does not move during the night. Matches must use it, and the recalculation you built in Part 2 is no longer allowed.
Goal: score matches with frozen, per-week averages and handicaps, and let the secretary ask for them.
Rules
- The *frozen average* for week
wuses only games recorded in weeks strictly beforew(floor of total pins / games). If there are none, the entering average stands in. If there is no entering average either, there is no frozen average. - The *frozen handicap* for week
wis the handicap formula applied to the frozen average. With no frozen average it is 0 (the bowler bowls scratch). This replaces Part 2's idea of building a new bowler's handicap from the night's own games. matchuses the frozen handicap of itsweekfor every lineup bowler, and the frozen average of itsweekfor blind scores. An absent bowler with no frozen average still makes the match fail withno average. Presence, error order, point values and standings work exactly as in Part 2. Matches are final: recording scores for a week after its match was filed never changes that result.["handicap", bowler_id, week]is new: it returns the frozen handicap for that week. Errors:bowler not found, theninvalid week(the week must be an integer>= 1). It never returnsno average(rule 2).- Everything else is unchanged:
average,cardand["handicap", bowler_id](one argument) still use all games recorded so far and still returnno averagewhen there is none.
Worked example
[["add_bowler", "ann", "Pin Pals", 150], ["add_bowler", "ben", "Pin Pals", 140],
["add_bowler", "cy", "Gutter Gang", 157], ["add_bowler", "di", "Gutter Gang", 145],
["record", 1, "ann", [150, 152, 148]], ["record", 1, "ben", [139, 141, 142]],
["record", 1, "cy", [170, 150, 153]],
["match", 1, "Pin Pals", "Gutter Gang"],
["record", 2, "ann", [200, 190, 210]],
["handicap", "ann"], ["handicap", "ann", 2], ["handicap", "ann", 1], ["handicap", "ann", 3]]returns [true, true, true, true, 450, 422, 473, [6, 2], 600, 40, 63, 63, 40].
After week 2 Ann's season average is 175 (handicap 40), but week 2 is frozen at her week-1 average of 150 (handicap 63); week 1 had no earlier games, so her entering 150 applied; week 3 sees both weeks. The Part 2 example above is unaffected: everyone there averages exactly their entering average, so freezing changes nothing.
A new bowler: with no entering average and one night of 180s, ["handicap", "nia"] is 36 but ["handicap", "nia", 1] is 0 and ["handicap", "nia", 2] is 36.
Example 1
- Input:
- [["add_bowler","ann","Pin Pals",160],["add_bowler","ben","Pin Pals"],["handicap","ann"],["record",1,"ann",[150,150,152]],["average","ann"],["handicap","ann"]]
- Output:
- [true,true,54,452,150,63]
- Explanation
- Ann's entering 160 gives handicap 54 until she bowls. Her 452 pins over three games average 150 (fraction dropped), so her handicap becomes floor(70 x 90 / 100) = 63.
Example 2
- Input:
- [...ann/ben (Pin Pals) and cy/di (Gutter Gang) with entering averages 150/140/157/145..., ["record",1,"ann",[150,152,148]],["record",1,"ben",[139,141,142]],["record",1,"cy",[170,150,153]],["match",1,"Pin Pals","Gutter Gang"],["standings"]]
- Output:
- [..., [6,2], [["Pin Pals",6],["Gutter Gang",2]]]
- Explanation
- Di is absent and bowls blind at 145 - 10 = 135 each game. Pin Pals win games 2 and 3 and the series; Gutter Gang win game 1. Wins are 2 half-points each.
Example 3
- Input:
- [...ann (entering 150) records 450 in week 1 and 600 in week 2..., ["handicap","ann"],["handicap","ann",2],["handicap","ann",3]]
- Output:
- [..., 40, 63, 40]
- Explanation
- Her season average after week 2 is 175 (handicap 40), but week 2's handicap was frozen at the start of the week from her week-1 average of 150 (handicap 63).
Guidelines
- Python 3 standard library only
- Averages and handicaps are integers; there is no floating-point arithmetic in any rule
- Team points are counted in integer half-points (win 2, tie 1 each)
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised