Bruker beskrev en turneringsform (start på -spillehandicap, brutto mot
par løpende, lavest sluttsum vinner) og spurte om dette egentlig var en
presentasjonsform fremfor en ny turneringsform. Riktig observasjon:
sluttsummen er matematisk identisk med stroke_net sitt netto-til-par.
Ny tournament.net_display_style ('standard'/'hunt', migrasjon 092), kun
gyldig sammen med scoring_method='stroke_net'. _attach_stroke_play_columns
fikk én stroke_val()-hjelper som bytter net_total (gradvis slagindeks-
basert) med gross_total - playing_handicap (hele handicapet trukket fra
med én gang) når hunt er aktiv -- ingen ny motorfunksjon. Ny, generelt
tilgjengelig tiebreak-verdi lowest_playing_hcp.
5 nye pytest-tester + full regresjon (198/198), tsc rent, full-stack
scratch-verifisert (hånd-utregnede tall matchet eksakt, offentlig
følg-live + utskrift arvet visningen uten kodeendring der).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
221 lines
10 KiB
Python
221 lines
10 KiB
Python
"""
|
|
Visningsmodus for netto-scoring ("Jakter Jesper", migrasjon 092,
|
|
2026-08-23). IKKE en ny scoring_method -- sluttallet er matematisk
|
|
identisk med stroke_net sitt netto-til-par (`net_total - par_played`).
|
|
Det eneste som faktisk er nytt er den LØPENDE visningen: standard
|
|
"tjener inn" handicap-slag gradvis per slagindeks etter hvert som hull
|
|
spilles, mens 'hunt' trekker fra HELE spillehandicapet med én gang og
|
|
viser brutto-til-par-differansen løpende. Se
|
|
`_attach_stroke_play_columns` i individual_tournaments.py.
|
|
"""
|
|
|
|
from fastapi import HTTPException
|
|
import pytest
|
|
|
|
from app.routers.individual_tournaments import (
|
|
HoleUpdate as TournamentHoleUpdate,
|
|
individual_leaderboard,
|
|
update_hole as tournament_update_hole,
|
|
)
|
|
from app.routers.tournaments import TournamentUpdate, update_tournament
|
|
from app.auth import CurrentUser
|
|
|
|
from tests.conftest import (
|
|
add_membership,
|
|
create_org,
|
|
create_org_hole,
|
|
create_course,
|
|
create_player,
|
|
create_tee,
|
|
create_tournament,
|
|
create_tournament_participant,
|
|
create_tournament_round,
|
|
create_tournament_round_participant,
|
|
create_user,
|
|
)
|
|
|
|
import app.db as app_db
|
|
|
|
|
|
async def _score_holes(tournament_id, round_id, rp_id, org_id, user_id, scores: list[int]) -> None:
|
|
user = CurrentUser(user_id=user_id)
|
|
for n, gross in enumerate(scores, start=1):
|
|
await tournament_update_hole(
|
|
tournament_id, round_id, rp_id, n,
|
|
TournamentHoleUpdate(gross_strokes=gross, expected_version=None),
|
|
organization_id=org_id, user=user,
|
|
)
|
|
|
|
|
|
async def _setup_stroke_net_round(org_id, method="stroke_net"):
|
|
tournament_id = await create_tournament(org_id, name="Jakter Jesper-turnering")
|
|
course_id = await create_course(org_id, name="Hunt Links")
|
|
for n in range(1, 19):
|
|
await create_org_hole(org_id, course_id, hole_number=n, par=4, stroke_index=n)
|
|
tee_id = await create_tee(org_id, course_id)
|
|
async with app_db.org_connection(org_id) as conn:
|
|
await conn.execute(
|
|
"UPDATE tournament SET format_type = 'individual', scoring_method = $2 WHERE id = $1",
|
|
tournament_id, method,
|
|
)
|
|
round_id = await create_tournament_round(org_id, tournament_id, course_id, sequence=1)
|
|
return tournament_id, course_id, tee_id, round_id
|
|
|
|
|
|
async def _add_participant_with_playing_hcp(org_id, tournament_id, round_id, tee_id, name, playing_handicap):
|
|
player_id = await create_player(org_id, display_name=name)
|
|
tp_id = await create_tournament_participant(org_id, tournament_id, player_id)
|
|
rp_id = await create_tournament_round_participant(org_id, round_id, tp_id, tee_id)
|
|
async with app_db.org_connection(org_id) as conn:
|
|
await conn.execute(
|
|
"UPDATE tournament_round_participant SET course_handicap = $1, playing_handicap = $1 WHERE id = $2",
|
|
playing_handicap, rp_id,
|
|
)
|
|
return tp_id, rp_id
|
|
|
|
|
|
async def test_hunt_differs_mid_round_but_converges_at_completion(pool):
|
|
org_id = await create_org()
|
|
owner_id = await create_user()
|
|
await add_membership(org_id, owner_id, role="owner")
|
|
tournament_id, course_id, tee_id, round_id = await _setup_stroke_net_round(org_id)
|
|
p1, rp1 = await _add_participant_with_playing_hcp(org_id, tournament_id, round_id, tee_id, "P1", 12)
|
|
|
|
# Hull 1, par -- stroke_index 1 (<=12) -> mottar 1 slag under standard.
|
|
await _score_holes(tournament_id, round_id, rp1, org_id, owner_id, [4])
|
|
|
|
entries = await individual_leaderboard(tournament_id, organization_id=org_id)
|
|
by_id = {e.tournament_participant_id: e for e in entries}
|
|
# Standard (default): netto = 4-1=3, til par = 3-4 = -1 -- slaget er
|
|
# kun DELVIS "tjent inn" ennå (1 av 12 spilte hull).
|
|
assert by_id[p1].total_label == "-1"
|
|
|
|
await update_tournament(tournament_id, TournamentUpdate(net_display_style="hunt"), organization_id=org_id)
|
|
entries = await individual_leaderboard(tournament_id, organization_id=org_id)
|
|
by_id = {e.tournament_participant_id: e for e in entries}
|
|
# Hunt: gross(4) - FULLT spillehandicap(12) = -8, til par = -8-4 = -12
|
|
# -- hele handicapet trukket fra med én gang, mye "dypere" tall.
|
|
assert by_id[p1].total_label == "-12"
|
|
|
|
# Fullfør runden med par på alle resterende hull.
|
|
await _score_holes(tournament_id, round_id, rp1, org_id, owner_id, [4] * 17)
|
|
entries = await individual_leaderboard(tournament_id, organization_id=org_id)
|
|
by_id = {e.tournament_participant_id: e for e in entries}
|
|
# Fortsatt -12 under hunt -- flatt hele veien når hvert hull er par.
|
|
assert by_id[p1].total_label == "-12"
|
|
|
|
# Bytt tilbake til standard nå som runden er fullført -- KONVERGERER
|
|
# til nøyaktig samme tall som hunt (selve beviset på ekvivalensen).
|
|
await update_tournament(tournament_id, TournamentUpdate(net_display_style="standard"), organization_id=org_id)
|
|
entries = await individual_leaderboard(tournament_id, organization_id=org_id)
|
|
by_id = {e.tournament_participant_id: e for e in entries}
|
|
assert by_id[p1].total_label == "-12"
|
|
|
|
|
|
async def test_hunt_display_style_rejected_for_non_stroke_net(pool):
|
|
org_id = await create_org()
|
|
owner_id = await create_user()
|
|
await add_membership(org_id, owner_id, role="owner")
|
|
tournament_id = await create_tournament(org_id)
|
|
async with app_db.org_connection(org_id) as conn:
|
|
await conn.execute(
|
|
"UPDATE tournament SET format_type = 'individual', scoring_method = 'stableford' WHERE id = $1",
|
|
tournament_id,
|
|
)
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await update_tournament(tournament_id, TournamentUpdate(net_display_style="hunt"), organization_id=org_id)
|
|
assert exc_info.value.status_code == 400
|
|
|
|
|
|
async def test_switching_away_from_stroke_net_rejected_while_hunt_active(pool):
|
|
org_id = await create_org()
|
|
owner_id = await create_user()
|
|
await add_membership(org_id, owner_id, role="owner")
|
|
tournament_id, course_id, tee_id, round_id = await _setup_stroke_net_round(org_id)
|
|
await update_tournament(tournament_id, TournamentUpdate(net_display_style="hunt"), organization_id=org_id)
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await update_tournament(
|
|
tournament_id, TournamentUpdate(scoring_method="stableford"), organization_id=org_id
|
|
)
|
|
assert exc_info.value.status_code == 400
|
|
|
|
# Men lov når begge felt endres sammen i samme kall.
|
|
out = await update_tournament(
|
|
tournament_id,
|
|
TournamentUpdate(scoring_method="stableford", net_display_style="standard"),
|
|
organization_id=org_id,
|
|
)
|
|
assert out.scoring_method == "stableford"
|
|
assert out.net_display_style == "standard"
|
|
|
|
|
|
async def test_lowest_playing_hcp_tiebreak_resolves_field_tie(pool):
|
|
"""Speiler `test_lowest_hcp_resolves_field_tie_but_not_winner`
|
|
(test_tiebreak.py) sitt oppsett nøyaktig, men med `stroke_gross`
|
|
(handicap påvirker IKKE selve rangeringen der -- kun tiebreaken --
|
|
slik at identiske bruttoscorer faktisk BLIR en reell uavgjort å
|
|
løse). `stroke_net` ville gjort dette meningsløst: ulikt
|
|
spillehandicap med identisk brutto gir ulik netto, altså ingen
|
|
uavgjort i det hele tatt å teste tiebreaken på."""
|
|
org_id = await create_org()
|
|
owner_id = await create_user()
|
|
await add_membership(org_id, owner_id, role="owner")
|
|
tournament_id, course_id, tee_id, round_id = await _setup_stroke_net_round(org_id, method="stroke_gross")
|
|
await update_tournament(
|
|
tournament_id,
|
|
TournamentUpdate(tiebreak_winner_method="none", tiebreak_field_method="lowest_playing_hcp"),
|
|
organization_id=org_id,
|
|
)
|
|
|
|
leader, rp_leader = await _add_participant_with_playing_hcp(org_id, tournament_id, round_id, tee_id, "Leader", 5)
|
|
p2, rp2 = await _add_participant_with_playing_hcp(org_id, tournament_id, round_id, tee_id, "P2", 8)
|
|
p3, rp3 = await _add_participant_with_playing_hcp(org_id, tournament_id, round_id, tee_id, "P3", 20)
|
|
|
|
await _score_holes(tournament_id, round_id, rp_leader, org_id, owner_id, [3] * 18) # klar leder
|
|
await _score_holes(tournament_id, round_id, rp2, org_id, owner_id, [4] * 18) # uavgjort brutto med p3
|
|
await _score_holes(tournament_id, round_id, rp3, org_id, owner_id, [4] * 18) # uavgjort brutto med p2
|
|
|
|
entries = await individual_leaderboard(tournament_id, organization_id=org_id)
|
|
by_id = {e.tournament_participant_id: e for e in entries}
|
|
|
|
assert by_id[leader].position == "1"
|
|
assert by_id[p2].position == "2" # lavere spillehandicap (8 < 20) -> foran
|
|
assert by_id[p3].position == "3"
|
|
|
|
|
|
async def test_lowest_playing_hcp_missing_value_sorts_last(pool):
|
|
"""Speiler `lowest_hcp` sin regel: manglende data kan ALDRI vinne en
|
|
tiebreak ved å mangle -- sorteres sist. `stroke_gross`, samme
|
|
begrunnelse som testen over -- spillehandicap er ikke påkrevd for
|
|
brutto-scoring (`_SCORING_METHODS_REQUIRING_HANDICAP` gjelder ikke
|
|
her), så et reelt NULL-scenario for tiebreak-metrikken kan faktisk
|
|
oppstå (ulikt stroke_net, der manglende handicap gjør at netto-
|
|
tallet aldri blir beregnet i det hele tatt -- deltakeren havner da
|
|
helt utenfor rangeringen, ikke i en tiebreak-gruppe)."""
|
|
org_id = await create_org()
|
|
owner_id = await create_user()
|
|
await add_membership(org_id, owner_id, role="owner")
|
|
tournament_id, course_id, tee_id, round_id = await _setup_stroke_net_round(org_id, method="stroke_gross")
|
|
# Kun to deltakere, begge uavgjort for 1.-plass -- tiebreak_winner_method
|
|
# er den relevante innstillingen her (rank==1), ikke feltmetoden.
|
|
await update_tournament(
|
|
tournament_id,
|
|
TournamentUpdate(tiebreak_winner_method="lowest_playing_hcp", tiebreak_field_method="none"),
|
|
organization_id=org_id,
|
|
)
|
|
|
|
p1, rp1 = await _add_participant_with_playing_hcp(org_id, tournament_id, round_id, tee_id, "P1", 10)
|
|
# P2 -- ingen spillehandicap beregnet (NULL, samme som "aldri kjørt
|
|
# gjennom _compute_round_participant_handicap").
|
|
player2 = await create_player(org_id, display_name="P2")
|
|
p2 = await create_tournament_participant(org_id, tournament_id, player2)
|
|
rp2 = await create_tournament_round_participant(org_id, round_id, p2, tee_id)
|
|
|
|
await _score_holes(tournament_id, round_id, rp1, org_id, owner_id, [4] * 18)
|
|
await _score_holes(tournament_id, round_id, rp2, org_id, owner_id, [4] * 18)
|
|
|
|
entries = await individual_leaderboard(tournament_id, organization_id=org_id)
|
|
by_id = {e.tournament_participant_id: e for e in entries}
|
|
assert by_id[p1].position == "1" # har en verdi -> vinner uavgjortheten
|
|
assert by_id[p2].position == "2" # mangler verdi -> sist
|