From 5432734240d0adbe3bf5591aa754299292bfd3e7 Mon Sep 17 00:00:00 2001 From: Rolf Date: Mon, 23 Mar 2026 22:09:16 +0100 Subject: [PATCH] Add Skins as a side game for Bingo Bango Bongo Players can now opt into a Skins side game when creating a BBB round. The side game uses the same skins settings (carryover, net/gross, skin value, double back nine). The scorecard gains a third Skins tab during play and on the summary; the BBB leaderboard continues to rank by BBB points. Co-Authored-By: Claude Sonnet 4.6 --- app/database.py | 1 + app/routers/games.py | 12 +++++++++--- app/templates/games/game.html | 28 ++++++++++++++++++++++------ app/templates/games/new_game.html | 24 +++++++++++++++++++----- app/templates/games/summary.html | 9 +++++---- 5 files changed, 56 insertions(+), 18 deletions(-) diff --git a/app/database.py b/app/database.py index 63c4ba9..4129917 100644 --- a/app/database.py +++ b/app/database.py @@ -127,6 +127,7 @@ async def init_db(): "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", + "ALTER TABLE games ADD COLUMN side_skins_enabled INTEGER NOT NULL DEFAULT 0", ]: try: await db.execute(migration) diff --git a/app/routers/games.py b/app/routers/games.py index 9c20047..0560873 100644 --- a/app/routers/games.py +++ b/app/routers/games.py @@ -284,10 +284,11 @@ async def create_game(request: Request, holes_count: int = Form(...), 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 + side_skins_enabled = 1 if form.get("side_skins_enabled") == "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, skins_net) VALUES (?,?,?,?,?,?,?,?,?,?,?)", - (game_id, format, holes_count, "active", user["id"], now, course_id, carryover, skin_value, skin_double_back_nine, skins_net), + "INSERT INTO games (id, format, holes_count, status, created_by, created_at, course_id, carryover, skin_value, skin_double_back_nine, skins_net, side_skins_enabled) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", + (game_id, format, holes_count, "active", user["id"], now, course_id, carryover, skin_value, skin_double_back_nine, skins_net, side_skins_enabled), ) # Pre-fetch course holes for handicap calculation @@ -389,11 +390,14 @@ async def game_summary(game_id: str, request: Request, user: dict = Depends(get_ skin_value = game["skin_value"] skin_double_back9 = bool(game["skin_double_back_nine"]) skins_net = bool(game["skins_net"]) - if game["format"] == "skins": + side_skins_enabled = bool(game.get("side_skins_enabled", 0)) + if game["format"] == "skins" or side_skins_enabled: 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, net_sph) + + if game["format"] == "skins": 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) @@ -408,6 +412,7 @@ async def game_summary(game_id: str, request: Request, user: dict = Depends(get_ "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, "skins_net": skins_net, + "side_skins_enabled": side_skins_enabled, "carryover": bool(game["carryover"]), }) @@ -449,6 +454,7 @@ async def game_page(game_id: str, request: Request, user: dict = Depends(get_cur "skin_value": game["skin_value"], "skin_double_back9": bool(game["skin_double_back_nine"]), "skins_net": bool(game["skins_net"]), + "side_skins_enabled": bool(game.get("side_skins_enabled", 0)), }) diff --git a/app/templates/games/game.html b/app/templates/games/game.html index c19b813..a3f3514 100644 --- a/app/templates/games/game.html +++ b/app/templates/games/game.html @@ -11,10 +11,10 @@ {# ── Header ── #}
-
-
+
- +
@@ -281,6 +281,7 @@ 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 SIDE_SKINS_ENABLED = {{ side_skins_enabled | tojson }}; const HOLE_PARS = {{ hole_pars | tojson }}; const HOLE_SIS = {{ hole_stroke_indices | tojson }}; const STROKES_PER_HOLE = {{ strokes_per_hole | tojson }}; @@ -457,6 +458,22 @@ function gameApp() { prevHole() { if (this.currentHole > 1) { this.saveHole(this.currentHole); this.currentHole--; } }, nextHole() { if (this.currentHole < GAME_HOLES) { this.saveHole(this.currentHole); this.currentHole++; } }, + cardTabs() { + if (GAME_FORMAT === 'skins') return ['strokes', 'skins']; + if (SIDE_SKINS_ENABLED) return ['strokes', 'points', 'skins']; + return ['strokes', 'points']; + }, + cardNext() { + const tabs = this.cardTabs(); + const idx = tabs.indexOf(this.cardView); + if (idx < tabs.length - 1) this.cardView = tabs[idx + 1]; + }, + cardPrev() { + const tabs = this.cardTabs(); + const idx = tabs.indexOf(this.cardView); + if (idx > 0) this.cardView = tabs[idx - 1]; + }, + fmt(v) { return Number.isInteger(v) || v % 1 === 0 ? v.toFixed(0) : v.toFixed(1); }, @@ -548,8 +565,7 @@ function gameApp() { if (this.view === 'score') { if (dx < 0) this.nextHole(); else this.prevHole(); } else { - const secondary = GAME_FORMAT === 'skins' ? 'skins' : 'points'; - this.cardView = dx < 0 ? secondary : 'strokes'; + if (dx < 0) this.cardNext(); else this.cardPrev(); } }, }; diff --git a/app/templates/games/new_game.html b/app/templates/games/new_game.html index 678ee9c..8613fb7 100644 --- a/app/templates/games/new_game.html +++ b/app/templates/games/new_game.html @@ -31,6 +31,7 @@ skinValue: '', skinDoubleBackNine: false, skinsNet: false, + sideSkinsEnabled: false, holesCount: '', selectedPlayers: ['{{ user.id }}'], selectedTees: {}, @@ -80,6 +81,7 @@ backFromConfirm() { this.step = this.hasTees() ? 2 : 1; }, formatLabel() { if (this.format === 'skins') return 'Skins'; + if (this.sideSkinsEnabled) return 'Bingo Bango Bongo + Skins'; return 'Bingo Bango Bongo'; } }"> @@ -106,11 +108,18 @@ -
+
+ +
+
-
+
+ Side game + Skins +
+
Scoring
-
+
Carryover
-
+
Skin value
@@ -354,6 +367,7 @@ +
diff --git a/app/templates/games/summary.html b/app/templates/games/summary.html index f7d79f8..b0b8cd2 100644 --- a/app/templates/games/summary.html +++ b/app/templates/games/summary.html @@ -84,11 +84,12 @@ {# Scorecard toggle #}
- {% if game.format == 'skins' %} - - {% else %} + {% if game.format != 'skins' %} {% endif %} + {% if game.format == 'skins' or side_skins_enabled %} + + {% endif %}
{# Strokes scorecard #} @@ -204,7 +205,7 @@ {# Skins scorecard #} - {% if game.format == 'skins' %} + {% if game.format == 'skins' or side_skins_enabled %}