# Scoring rules All implemented in `src/lib/scoring/` as pure functions. Tests must cover every rule below. ## 1. Cell value (team × criterion) - **Direct game:** the admin score row. Missing → not scored. Judge rows (e.g. left over from a mode switch) are ignored. - **Judged game:** - If an admin override row exists → use it. - Else average of non-revoked judges' values for that cell. Missing judges are ignored (not zero). - No judge values → not scored. ## 2. Team raw score for a game `raw = Σ (cellValue × criterion.weight)` over scored criteria. A team with zero scored cells is **unscored** for that game (not 0). `raw` is rounded to 6dp before ranking so float noise from averaging can't break a real tie. ## 3. Game points - **raw mode:** `points = round2(raw × multiplier)`. - **placement mode:** rank scored teams by raw desc. Award `placement_points[rank-1] ?? 0`. - Ties: tied teams share the **average** of the places they span, rounded to 2dp. e.g. two teams tied for 1st with [10,7,5] → each gets 8.5; next team gets 5. - Then × multiplier. - Unscored teams get 0 and are not ranked. - Only games with status `live` or `done` count toward totals. `upcoming` games contribute nothing (prevents half-set-up games leaking). ## 4. Event total `total = Σ game points + Σ adjustments.points` ## 5. Standings Sort by total desc. Ties share rank (standard competition: 1,1,3). Tie display order: team `sort_order`. ## Test cases to include (minimum) 1. Karaoke: 5 criteria × 1–5, 3 judges, one judge missing a team → average uses 2 judges. 2. Override beats judge average. 3. Revoked judge excluded. 4. Weights applied. 5. Placement with a 2-way tie at 1st and 3 teams. 6. Placement with more teams than placement_points → extra teams get 0. 7. Multiplier on both modes. 8. Upcoming game ignored. 9. Adjustments (+ and −) added; adjustment tied to an upcoming game still counts (it's explicit). 10. Standings tie → shared rank.