Fantasy League: Scoring, Lineup Locks and Weekly Standings
EasyAI-assisted coding · AI-Assisted Debugging
Fantasy League: Scoring, Lineup Locks and Weekly Standings
You've joined the team behind Gridiron League, a fantasy-football app. Managers draft real players, set a weekly lineup, and the app turns each player's game stats into points. Support has a ticket about wrong scores, and two features are queued behind it: lineups that lock when a player's game kicks off, and weekly head-to-head matchups with a standings table that has to stay correct when the league publishes stat corrections. All point values are integer tenths of a point inside the code (10 = 1.0 pt); clients see them as one-decimal strings such as "12.5".
Codebase
| File | What it does |
|---|---|
fantasy/api.py | run(operations) entry point and the Dispatcher that routes each operation |
fantasy/service.py | FantasyService: one method per operation |
fantasy/scoring.py | Turns one player's stat totals into tenths of a point |
fantasy/stats.py | StatBook: raw stat totals per player |
fantasy/lineup.py | LineupManager: who starts in which slot |
fantasy/league.py | League: players, teams, rosters, kickoff times |
fantasy/season.py | Placeholder for weekly play (Part 3) |
fantasy/models.py, config.py, points.py, validation.py, errors.py | Data classes, league constants (slots, eligibility, scoring tables), point formatting, argument checks, error codes |
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, ts, ...args]; ts is an integer timestamp (it never decreases). Errors are returned as "error: <code>" strings, never raised. An unknown name returns "error: unknown operation"; a wrong number of arguments or a non-integer ts returns "error: invalid arguments", as does an empty or non-string player, team or slot id. Every id is a case-sensitive string. Checks run in the order listed in the "Errors" column.
| Operation | Arguments after ts | Returns | Errors |
|---|---|---|---|
player | pid, pos, kickoff | true | invalid position, player exists |
kickoff | pid, kickoff | true | player not found |
team | team_id | true | team exists |
draft | team_id, pid | true | team not found, player not found, player taken |
start | team_id, slot, pid | true | team not found, player not on roster, invalid slot |
lineup | team_id | {slot: pid or null} for the 7 slots in SLOT_ORDER | team not found |
stat | pid, stat, amount | the stat's new total | player not found, unknown stat, invalid amount |
player_score | pid | score string | player not found |
team_score | team_id | score string | team not found |
posisQB,RB,WRorTE.kickoffis a non-negative integer ornull(no game); any other value isinvalid arguments. A player can be drafted by one team only. Rosters have no size limit.stataddsamountto the player's running total for that stat (so a game arrives as many small updates).amountmust be an integer (not a boolean) and may be negative or zero, elseinvalid amount. Stat names are inconfig.STAT_NAMES.team_scoreis the sum of the scores of the players currently in the team's lineup slots. Empty slots score 0. Scores render withpoints.format_points("0.0","-0.5","21.0").
Part 1: Fix the scoring (SUP-4410)
Goal: make player_score and team_score follow the league's scoring rules.
Support has three kinds of complaints: "my 100-yard rusher didn't get his bonus", "my running back who lost two yards on the game was docked a point", and "my receiver caught 5 passes and was paid for 4". Scoring tables live in fantasy/config.py; the arithmetic lives in fantasy/scoring.py.
Rules (all in tenths internally)
- Scoring uses a player's game totals for each stat, never individual updates.
pass_td= 4.0,rush_td=rec_td= 6.0,int=fumble_lost= -2.0, per occurrence.- Yardage pays 1.0 per full unit of the total: 25 passing yards, 10 rushing or receiving yards. Totals are rounded toward zero: 9 rush yards = 0.0, -9 = 0.0, -10 = -1.0.
- Every reception is worth 0.5 (half-PPR).
- Milestone bonuses: rushing or receiving yards
>= 100pay +3.0 and>= 200pay +5.0; passing yards>= 300pay +3.0 and>= 400pay +5.0. Only the highest tier reached is paid (a 250-yard rusher gets +3.0, not +8.0). Each stat's bonus is separate, so a player can earn a rushing bonus and a receiving bonus in the same game.
Example
[["player",1,"qb1","QB",null],["player",1,"rb1","RB",null],["team",1,"sharks"],
["draft",2,"sharks","qb1"],["draft",2,"sharks","rb1"],
["stat",3,"qb1","pass_yds",312],["stat",3,"qb1","pass_td",2],["stat",3,"qb1","int",1],
["stat",4,"rb1","rush_yds",57],["stat",4,"rb1","rush_yds",47],["stat",4,"rb1","rec",3],
["player_score",5,"qb1"],["player_score",5,"rb1"],
["start",6,"sharks","QB","qb1"],["start",6,"sharks","RB1","rb1"],["team_score",7,"sharks"]]returns [true,true,true,true,true,312,2,1,57,104,3,"21.0","14.5",true,true,"35.5"]. (qb1: 12.0 yards + 8.0 TDs - 2.0 INT + 3.0 bonus; rb1: 10.0 yards + 3.0 bonus + 1.5 receptions.)
Part 2: Lineup slots and kickoff locks (LGE-218)
Goal: make start enforce slot eligibility and lock players at their own kickoff, and add bench. Slots and their eligible positions are in config.SLOT_ELIGIBILITY: QB; RB1, RB2 (RB); WR1, WR2 (WR); TE; FLEX (RB, WR or TE).
| Operation | Arguments after ts | Returns | Errors |
|---|---|---|---|
start | team_id, slot, pid | true | team not found, player not on roster, invalid slot, not eligible, player locked, slot locked |
bench | team_id, slot | true | team not found, invalid slot, slot empty, slot locked |
Rules
- A player is locked at operation time
tsif hiskickoffis notnullandts >= kickoff(the kickoff instant itself is locked). Lock status is computed from the player's *current* kickoff each time; thekickoffoperation can move a game later (unlocking) or earlier. startputs the player in the slot. The incoming player must not be locked (player locked), and neither must the player currently occupying the slot (slot locked; checked second). A rejectedstartchanges nothing.- A displaced occupant goes back to the bench. A player fills at most one slot: starting a player who is already in another slot moves him, leaving the old slot empty. Starting a player into the slot he already holds succeeds and changes nothing.
benchempties a slot; the occupant must not be locked.team_scoreis unchanged: it counts whoever is in the slots when it is asked.
Example (rb1 and qb1 kick off at 1000, wr1 at 1300; all three are on team t)
[["start",900,"t","FLEX","rb1"],["start",900,"t","FLEX","qb1"],["start",1000,"t","RB1","rb1"],
["start",1100,"t","FLEX","wr1"],["bench",1100,"t","FLEX"],["start",1100,"t","WR1","wr1"],
["start",1300,"t","FLEX","wr1"],["lineup",1300,"t"]]returns [true,"error: not eligible","error: player locked","error: slot locked","error: slot locked",true,"error: player locked",{"QB":null,"RB1":null,"RB2":null,"WR1":"wr1","WR2":null,"TE":null,"FLEX":"rb1"}].
Part 3: Weeks, matchups, corrections and standings (LGE-231)
Goal: the app now plays a season. Stats and lineups belong to a week; teams are paired head to head; and the league publishes stat corrections after games, which must change old scores and therefore old results. Until now the league lived in an implicit week 1.
| Operation | Arguments after ts | Returns | Errors |
|---|---|---|---|
advance_week | none | the new current week number | none |
pair | week, team_a, team_b | true | invalid week, week closed, team not found, same team, already paired |
correct | week, pid, stat, value | the stat's new total | invalid week, player not found, unknown stat, invalid amount |
standings | none | list of [team, wins, losses, ties, points_for] | none |
player_score / team_score | pid / team_id, optional week | score string | invalid week, then the old errors |
Rules
- The current week starts at 1.
advance_weekcloses it: every team's lineup is captured as that week's final lineup, every player's kickoff is reset tonull(the new week's schedule is set withkickoff; until then nobody is locked), and the week number increases. Live lineups carry over unchanged into the new week; later changes to them never affect closed weeks. statadds to the current week's totals. Weeks are independent; a new week starts with no stats.correctoverwrites one stat total for any week from 1 to the current one (invalid weekotherwise, including non-integers) andvaluefollows theamountrules.player_score/team_scoretake an optional week (default: current). A past week's team score uses that week's captured lineup and that week's stats as currently corrected; the current week uses the live lineup. A team created after a week closed scores"0.0"for it.pairrecords a matchup for the current or a later week, in the order given. Each team has at most one matchup per week. A week is evaluated for standings only once it is closed.standingscounts matchups in closed weeks, scoring both teams at the moment of the call. A higher score wins; equal scores tie.points_foris the sum of the team's scores in its matchup weeks (a bye adds nothing). Every team appears, even with no games. Order: more wins, then more ties, then higherpoints_for, thenteam_idascending (plain string order).- Parts 1-2 behave as before within a week; hidden tests re-check them.
Example (rb1 starts for team a, wr1 for team b; both teams exist, own those players, and the lineups are set)
[["pair",2,1,"a","b"],["stat",3,"rb1","rush_yds",80],["stat",3,"wr1","rec",4],
["advance_week",4],["bench",5,"a","RB1"],["team_score",6,"a",1],["team_score",6,"a"],
["standings",6],["correct",7,1,"wr1","rec_yds",130],["standings",8]]returns [true,80,4,2,true,"8.0","0.0",[["a",1,0,0,"8.0"],["b",0,1,0,"2.0"]],130,[["b",1,0,0,"18.0"],["a",0,1,0,"8.0"]]].
Notes
points.py,validation.py,models.pyanderrors.pyare known-good.- You may use the AI assistant freely; you are expected to verify what it produces.
- Parts 1-2 is a solid result; Part 3 is the full ticket.
Example 1
- Input:
- [["player",1,"qb1","QB",null],["player",1,"rb1","RB",null],["team",1,"sharks"],["draft",2,"sharks","qb1"],["draft",2,"sharks","rb1"],["stat",3,"qb1","pass_yds",312],["stat",3,"qb1","pass_td",2],["stat",3,"qb1","int",1],["stat",4,"rb1","rush_yds",57],["stat",4,"rb1","rush_yds",47],["stat",4,"rb1","rec",3],["player_score",5,"qb1"],["player_score",5,"rb1"],["start",6,"sharks","QB","qb1"],["start",6,"sharks","RB1","rb1"],["team_score",7,"sharks"]]
- Output:
- [true,true,true,true,true,312,2,1,57,104,3,"21.0","14.5",true,true,"35.5"]
- Explanation
- The quarterback earns 12.0 for yards, 8.0 for touchdowns, -2.0 for the interception and a 3.0 bonus. The running back's two rushing updates total 104 yards: 10.0 plus a 3.0 bonus, and 1.5 for three half-PPR receptions.
Example 2
- Input:
- [... rb1 (kickoff 1000), qb1 (kickoff 1000), wr1 (kickoff 1300) on team t ..., ["start",900,"t","FLEX","rb1"],["start",900,"t","FLEX","qb1"],["start",1000,"t","RB1","rb1"],["start",1100,"t","FLEX","wr1"]]
- Output:
- [true,"error: not eligible","error: player locked","error: slot locked"]
- Explanation
- FLEX takes rb1 before kickoff. A quarterback is not FLEX-eligible. At 1000 rb1 is locked and cannot be moved to RB1, and at 1100 his FLEX slot cannot be given to wr1 even though wr1's game has not started.
Example 3
- Input:
- [... team a starts rb1, team b starts wr1 ..., ["pair",2,1,"a","b"],["stat",3,"rb1","rush_yds",80],["stat",3,"wr1","rec",4],["advance_week",4],["standings",6],["correct",7,1,"wr1","rec_yds",130],["standings",8]]
- Output:
- [true,80,4,2,[["a",1,0,0,"8.0"],["b",0,1,0,"2.0"]],130,[["b",1,0,0,"18.0"],["a",0,1,0,"8.0"]]]
- Explanation
- Week 1 closes with a ahead 8.0 to 2.0. A later correction gives wr1 130 receiving yards (13.0 + 3.0 bonus + 2.0), so b now wins week 1 and the standings flip.
Guidelines
- Python 3 standard library only
- All points are integer tenths internally; there is no floating-point arithmetic anywhere in scoring
- Timestamps are integers that never decrease from one operation to the next
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised
- Ids (players, teams, slots) are case-sensitive strings