'playoff' som ny tiebreak_winner_method-verdi -- rangeres som 'none' (appen kan ikke avgjøre et omspill selv), men regelen vises proaktivt i Oppsett og som banner på leaderboardet. HCP: la til handicap_index_ snapshot i TournamentParticipantUpdate + inline redigering i Deltakere-lista, uten å røre allerede frosne runde-verdier. Migrasjon 086 lagt til, men IKKE anvendt mot ekte teecup_db ennå.
221 lines
11 KiB
Python
221 lines
11 KiB
Python
"""
|
|
Tiebreak ved likt resultat i individuelle turneringer (migrasjon 085,
|
|
2026-08-18). Bruker: "Ved likt resultat ... Skal laveste hcp plasseres
|
|
foran, beste siste 18/9/6/3/siste hull plasseres foran, eller deles
|
|
plasseringene. Her kan svaret være forskjellig for vinner og resten av
|
|
feltet." Metoden er en fri organisator-innstilling, ikke fastlåst --
|
|
to UAVHENGIGE felt (vinner/felt), og påvirker ALDRI Cut-grensen (kun
|
|
sluttresultatets plasseringsvisning).
|
|
"""
|
|
|
|
from app.routers.individual_tournaments import (
|
|
HoleUpdate as TournamentHoleUpdate,
|
|
apply_cut,
|
|
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_single_round(org_id, owner_id, method="stroke_gross"):
|
|
tournament_id = await create_tournament(org_id, name="Tiebreak-turnering")
|
|
course_id = await create_course(org_id, name="Tiebreak 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 _set_tiebreak(org_id, tournament_id, winner: str, field: str):
|
|
await update_tournament(
|
|
tournament_id,
|
|
TournamentUpdate(tiebreak_winner_method=winner, tiebreak_field_method=field),
|
|
organization_id=org_id,
|
|
)
|
|
|
|
|
|
async def test_countback_resolves_winner_tie_by_better_back_nine(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_single_round(org_id, owner_id)
|
|
await _set_tiebreak(org_id, tournament_id, winner="countback", field="none")
|
|
|
|
p1 = await create_tournament_participant(org_id, tournament_id, await create_player(org_id, display_name="P1"))
|
|
p2 = await create_tournament_participant(org_id, tournament_id, await create_player(org_id, display_name="P2"))
|
|
p3 = await create_tournament_participant(org_id, tournament_id, await create_player(org_id, display_name="P3"))
|
|
|
|
rp1 = await create_tournament_round_participant(org_id, round_id, p1, tee_id)
|
|
rp2 = await create_tournament_round_participant(org_id, round_id, p2, tee_id)
|
|
rp3 = await create_tournament_round_participant(org_id, round_id, p3, tee_id)
|
|
|
|
# P1: front9 alle par (36), back9 to birdier (34) -> total 70, back9=34.
|
|
p1_scores = [4] * 9 + [3, 3] + [4] * 7
|
|
# P2: front9 to birdier (34), back9 alle par (36) -> total 70, back9=36 (dårligere enn P1 sin back9).
|
|
p2_scores = [3, 3] + [4] * 7 + [4] * 9
|
|
# P3: klart bak, ingen uavgjort.
|
|
p3_scores = [5] * 18
|
|
|
|
await _score_holes(tournament_id, round_id, rp1, org_id, owner_id, p1_scores)
|
|
await _score_holes(tournament_id, round_id, rp2, org_id, owner_id, p2_scores)
|
|
await _score_holes(tournament_id, round_id, rp3, org_id, owner_id, p3_scores)
|
|
|
|
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" # bedre back9 -> sole leder, IKKE "T1"
|
|
assert by_id[p1].is_leader is True
|
|
assert by_id[p2].position == "2"
|
|
assert by_id[p2].is_leader is False
|
|
assert by_id[p3].position == "3"
|
|
|
|
|
|
async def test_lowest_hcp_resolves_field_tie_but_not_winner(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_single_round(org_id, owner_id)
|
|
# Vinner-tiebreak IKKE satt (none) -- kun felt-tiebreak skal virke.
|
|
await _set_tiebreak(org_id, tournament_id, winner="none", field="lowest_hcp")
|
|
|
|
leader = await create_tournament_participant(org_id, tournament_id, await create_player(org_id, display_name="Leader"))
|
|
p2 = await create_tournament_participant(org_id, tournament_id, await create_player(org_id, display_name="P2"))
|
|
p3 = await create_tournament_participant(org_id, tournament_id, await create_player(org_id, display_name="P3"))
|
|
|
|
async with app_db.org_connection(org_id) as conn:
|
|
await conn.execute("UPDATE tournament_participant SET handicap_index_snapshot = 8.0 WHERE id = $1", p2)
|
|
await conn.execute("UPDATE tournament_participant SET handicap_index_snapshot = 20.0 WHERE id = $1", p3)
|
|
|
|
rp_leader = await create_tournament_round_participant(org_id, round_id, leader, tee_id)
|
|
rp2 = await create_tournament_round_participant(org_id, round_id, p2, tee_id)
|
|
rp3 = await create_tournament_round_participant(org_id, round_id, p3, tee_id)
|
|
|
|
await _score_holes(tournament_id, round_id, rp_leader, org_id, owner_id, [3] * 18) # klar leder, -18
|
|
await _score_holes(tournament_id, round_id, rp2, org_id, owner_id, [4] * 18) # uavgjort med p3, E
|
|
await _score_holes(tournament_id, round_id, rp3, org_id, owner_id, [4] * 18) # uavgjort med p2, E
|
|
|
|
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[leader].is_leader is True
|
|
assert by_id[p2].position == "2" # lavere HCP (8.0 < 20.0) -> foran
|
|
assert by_id[p3].position == "3"
|
|
|
|
|
|
async def test_tiebreak_method_none_keeps_shared_position(pool):
|
|
"""Standard/uendret oppførsel -- ingen tiebreak konfigurert i det hele
|
|
tatt (default 'none' for begge felt, ingen regresjon)."""
|
|
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_single_round(org_id, owner_id)
|
|
|
|
p1 = await create_tournament_participant(org_id, tournament_id, await create_player(org_id, display_name="P1"))
|
|
p2 = await create_tournament_participant(org_id, tournament_id, await create_player(org_id, display_name="P2"))
|
|
rp1 = await create_tournament_round_participant(org_id, round_id, p1, tee_id)
|
|
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 == "T1"
|
|
assert by_id[p2].position == "T1"
|
|
assert by_id[p1].is_leader is True
|
|
assert by_id[p2].is_leader is True
|
|
|
|
|
|
async def test_playoff_method_keeps_shared_position_like_none(pool):
|
|
"""'playoff' (migrasjon 086) -- appen kan ikke selv avgjøre et
|
|
omspill, så en tiebreak_winner_method='playoff'-turnering skal
|
|
oppføre seg NØYAKTIG som 'none' i selve rangeringen (delt plassering,
|
|
ingen automatisk oppløsning) -- forskjellen er ren informasjon
|
|
(tiebreak_playoff_rule), ikke rangeringslogikk."""
|
|
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_single_round(org_id, owner_id)
|
|
await update_tournament(
|
|
tournament_id,
|
|
TournamentUpdate(tiebreak_winner_method="playoff", tiebreak_playoff_rule="Sudden death fra hull 18."),
|
|
organization_id=org_id,
|
|
)
|
|
|
|
p1 = await create_tournament_participant(org_id, tournament_id, await create_player(org_id, display_name="P1"))
|
|
p2 = await create_tournament_participant(org_id, tournament_id, await create_player(org_id, display_name="P2"))
|
|
rp1 = await create_tournament_round_participant(org_id, round_id, p1, tee_id)
|
|
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 == "T1"
|
|
assert by_id[p2].position == "T1"
|
|
assert by_id[p1].is_leader is True
|
|
assert by_id[p2].is_leader is True
|
|
|
|
|
|
async def test_tiebreak_never_affects_cut_boundary(pool):
|
|
"""Bruker bekreftet eksplisitt: tiebreak påvirker KUN sluttresultatet,
|
|
ALDRI selve cut-grensen -- full delt plass på grensen uansett
|
|
tiebreak-innstilling."""
|
|
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, round1_id = await _setup_single_round(org_id, owner_id)
|
|
round2_id = await create_tournament_round(org_id, tournament_id, course_id, sequence=2)
|
|
# Countback konfigurert -- skal likevel IKKE påvirke cutten.
|
|
await _set_tiebreak(org_id, tournament_id, winner="countback", field="countback")
|
|
await update_tournament(
|
|
tournament_id, TournamentUpdate(cut_after_round=1, cut_size=1), organization_id=org_id,
|
|
)
|
|
|
|
p1 = await create_tournament_participant(org_id, tournament_id, await create_player(org_id, display_name="P1"))
|
|
p2 = await create_tournament_participant(org_id, tournament_id, await create_player(org_id, display_name="P2"))
|
|
rp1 = await create_tournament_round_participant(org_id, round1_id, p1, tee_id)
|
|
rp2 = await create_tournament_round_participant(org_id, round1_id, p2, tee_id)
|
|
# Identisk totalt OG identisk back9 -- ekte fullt uavgjort, selv med countback slått på.
|
|
await _score_holes(tournament_id, round1_id, rp1, org_id, owner_id, [4] * 18)
|
|
await _score_holes(tournament_id, round1_id, rp2, org_id, owner_id, [4] * 18)
|
|
|
|
result = await apply_cut(tournament_id, organization_id=org_id)
|
|
assert result.cut_size == 1
|
|
assert result.survivors == 2 # begge slipper gjennom -- delt 1.-plass, IKKE brutt av countback
|
|
assert result.cut_count == 0
|