teecup/tests/test_net_hunt_display.py

222 lines
10 KiB
Python
Raw Permalink Normal View History

"""
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 ."""
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), 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