From 2c0717948a1f7c4665e1cc72acafcdc57303e5a3 Mon Sep 17 00:00:00 2001 From: Rolf Date: Mon, 23 Mar 2026 21:16:10 +0100 Subject: [PATCH] Add straight up / net handicap option for skins games When creating a skins game, players can now choose between straight up (gross scores) or net scoring (strokes subtracted per hole based on playing handicap and stroke index). Net mode is stored on the game and applied in both the live scorecard and the summary. Co-Authored-By: Claude Sonnet 4.6 --- app/database.py | 1 + app/routers/games.py | 24 +++++++++++++++++------- app/templates/games/game.html | 9 +++++++-- app/templates/games/new_game.html | 18 ++++++++++++++++++ app/templates/games/summary.html | 2 +- 5 files changed, 44 insertions(+), 10 deletions(-) diff --git a/app/database.py b/app/database.py index 00559c1..63c4ba9 100644 --- a/app/database.py +++ b/app/database.py @@ -126,6 +126,7 @@ async def init_db(): "ALTER TABLE magic_link_tokens ADD COLUMN code TEXT", "ALTER TABLE games ADD COLUMN skin_value REAL", "ALTER TABLE games ADD COLUMN skin_double_back_nine INTEGER NOT NULL DEFAULT 0", + "ALTER TABLE games ADD COLUMN skins_net INTEGER NOT NULL DEFAULT 0", ]: try: await db.execute(migration) diff --git a/app/routers/games.py b/app/routers/games.py index c4b2977..07e5fa7 100644 --- a/app/routers/games.py +++ b/app/routers/games.py @@ -90,16 +90,22 @@ def _strokes_per_hole(players: list, holes: list, hole_stroke_indices: dict) -> def _calc_skins(players: list, scores: dict, holes: list, carryover: bool = True, - skin_value: float | None = None, skin_double_back9: bool = False): + skin_value: float | None = None, skin_double_back9: bool = False, + strokes_per_hole: dict | None = None): """Returns (skins_per_player, hole_results, unclaimed_pot, money_per_player, unclaimed_money) - money_per_player is None when skin_value is not set.""" + money_per_player is None when skin_value is not set. + strokes_per_hole is {uid: {hole: strokes}} for net scoring.""" skins = {p["id"]: 0 for p in players} money = {p["id"]: 0.0 for p in players} hole_results = {} pot = 0 money_pot = 0.0 for hole in holes: - scored = {uid: s["strokes"] for uid, s in scores.get(hole, {}).items() if s.get("strokes")} + raw = {uid: s["strokes"] for uid, s in scores.get(hole, {}).items() if s.get("strokes")} + if strokes_per_hole: + scored = {uid: g - (strokes_per_hole.get(uid) or {}).get(hole, 0) for uid, g in raw.items()} + else: + scored = raw if not scored: continue pot += 1 @@ -277,10 +283,11 @@ async def create_game(request: Request, holes_count: int = Form(...), skin_value_str = (form.get("skin_value") or "").strip() skin_value = float(skin_value_str) if skin_value_str else None skin_double_back_nine = 1 if form.get("skin_double_back_nine") == "1" else 0 + skins_net = 1 if form.get("skins_net") == "1" else 0 await db.execute( - "INSERT INTO games (id, format, holes_count, status, created_by, created_at, course_id, carryover, skin_value, skin_double_back_nine) VALUES (?,?,?,?,?,?,?,?,?,?)", - (game_id, format, holes_count, "active", user["id"], now, course_id, carryover, skin_value, skin_double_back_nine), + "INSERT INTO games (id, format, holes_count, status, created_by, created_at, course_id, carryover, skin_value, skin_double_back_nine, skins_net) VALUES (?,?,?,?,?,?,?,?,?,?,?)", + (game_id, format, holes_count, "active", user["id"], now, course_id, carryover, skin_value, skin_double_back_nine, skins_net), ) # Pre-fetch course holes for handicap calculation @@ -378,10 +385,12 @@ async def game_summary(game_id: str, request: Request, user: dict = Depends(get_ skins_won, skins_hole_results, unclaimed, money_won, unclaimed_money = None, None, 0, None, 0.0 skin_value = game["skin_value"] skin_double_back9 = bool(game["skin_double_back_nine"]) + skins_net = bool(game["skins_net"]) if game["format"] == "skins": carryover = bool(game["carryover"]) + net_sph = strokes_per_hole if skins_net else None skins_won, skins_hole_results, unclaimed, money_won, unclaimed_money = _calc_skins( - players, scores, holes, carryover, skin_value, skin_double_back9) + players, scores, holes, carryover, skin_value, skin_double_back9, net_sph) results = sorted(players, key=lambda p: skins_won.get(p["id"], 0), reverse=True) else: results = sorted(players, key=lambda p: totals.get(p["id"], {}).get("total", 0), reverse=True) @@ -395,7 +404,7 @@ async def game_summary(game_id: str, request: Request, user: dict = Depends(get_ "results": results, "skins_won": skins_won, "skins_hole_results": skins_hole_results, "unclaimed": unclaimed, "money_won": money_won, "unclaimed_money": unclaimed_money, "skin_value": skin_value, - "skin_double_back9": skin_double_back9, + "skin_double_back9": skin_double_back9, "skins_net": skins_net, "carryover": bool(game["carryover"]), }) @@ -436,6 +445,7 @@ async def game_page(game_id: str, request: Request, user: dict = Depends(get_cur "carryover": bool(game["carryover"]), "skin_value": game["skin_value"], "skin_double_back9": bool(game["skin_double_back_nine"]), + "skins_net": bool(game["skins_net"]), }) diff --git a/app/templates/games/game.html b/app/templates/games/game.html index c03fb7e..c19b813 100644 --- a/app/templates/games/game.html +++ b/app/templates/games/game.html @@ -280,6 +280,7 @@ const GAME_FORMAT = "{{ game.format }}"; const SKINS_CARRYOVER = {{ carryover | tojson }}; const SKIN_VALUE = {{ skin_value | tojson }}; const SKIN_DOUBLE_BACK9 = {{ skin_double_back9 | tojson }}; +const SKINS_NET = {{ skins_net | tojson }}; const HOLE_PARS = {{ hole_pars | tojson }}; const HOLE_SIS = {{ hole_stroke_indices | tojson }}; const STROKES_PER_HOLE = {{ strokes_per_hole | tojson }}; @@ -494,8 +495,12 @@ function gameApp() { if (this.isSkipped(hole)) continue; const holeScores = this.scores[hole] || {}; const scored = PLAYERS_DATA - .map(p => ({ id: p.id, strokes: holeScores[p.id]?.strokes || 0 })) - .filter(x => x.strokes > 0); + .map(p => { + const gross = holeScores[p.id]?.strokes || 0; + const net = SKINS_NET ? gross - ((STROKES_PER_HOLE[p.id] || {})[hole] || 0) : gross; + return { id: p.id, strokes: net, gross }; + }) + .filter(x => x.gross > 0); if (scored.length === 0) continue; pot += 1; if (SKIN_VALUE) moneyPot += SKIN_VALUE * (SKIN_DOUBLE_BACK9 && hole >= 10 ? 2 : 1); diff --git a/app/templates/games/new_game.html b/app/templates/games/new_game.html index 33e4001..a9de23b 100644 --- a/app/templates/games/new_game.html +++ b/app/templates/games/new_game.html @@ -30,6 +30,7 @@ carryoverEnabled: true, skinValue: '', skinDoubleBackNine: false, + skinsNet: false, holesCount: '', selectedPlayers: ['{{ user.id }}'], selectedTees: {}, @@ -104,6 +105,19 @@
+ +
+ + +
+
+ Scoring + +
Carryover diff --git a/app/templates/games/summary.html b/app/templates/games/summary.html index 5cc13ad..f7d79f8 100644 --- a/app/templates/games/summary.html +++ b/app/templates/games/summary.html @@ -7,7 +7,7 @@ ← Back
{{ game.course_name or 'Bingo Bango Bongo' }}
-
{{ game.holes_count }} holes · {{ game.created_at[:10] }}
+
{{ game.holes_count }} holes · {{ game.created_at[:10] }}{% if game.format == 'skins' and skins_net %} · net{% endif %}