Skip to content

Commit eda6cc8

Browse files
committed
Default Swiss backend to py4swiss
1 parent 4c3531e commit eda6cc8

2 files changed

Lines changed: 133 additions & 6 deletions

File tree

server/tournament/swiss/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def _swisspairing_runtime() -> _SwissPairingRuntime:
126126

127127

128128
def _swiss_pairing_backend(tournament: Tournament | None = None) -> str:
129-
"""Return the effective backend, preferring bbpPairings when available."""
129+
"""Return the effective backend, defaulting to py4swiss."""
130130

131131
configured_backend = os.getenv(_SWISS_PAIRING_BACKEND_ENV, "").strip().lower()
132132
explicit_backend = configured_backend if configured_backend != "" else None
@@ -138,7 +138,7 @@ def _swiss_pairing_backend(tournament: Tournament | None = None) -> str:
138138
)
139139
explicit_backend = None
140140

141-
backend = explicit_backend or "bbp"
141+
backend = explicit_backend or "py4swiss"
142142
if backend == "bbp" and tournament is not None:
143143
reason = bbp_backend_unavailability_reason(tournament)
144144
if reason is not None:

tests/test_swiss_pairing.py

Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from types import SimpleNamespace
66
from unittest.mock import AsyncMock, patch
77

8-
from const import FLAG, TEST_PREFIX, T_FINISHED
8+
from const import FLAG, TEST_PREFIX, T_FINISHED, VARIANTEND
99
from glicko2.glicko2 import new_default_perf_map
1010
from newid import id8
1111
from pychess_global_app_state_utils import get_app_state
@@ -128,9 +128,31 @@ def _record_finished_round(
128128
pairing: list[tuple[str, str]],
129129
) -> None:
130130
result_cycle = ("1-0", "0-1", "1/2-1/2", "1-0", "0-1")
131+
result_specs = [
132+
(result_cycle[(round_number + board_index) % len(result_cycle)], None)
133+
for board_index in range(len(pairing))
134+
]
135+
self._record_finished_round_with_specs(
136+
tournament,
137+
round_number=round_number,
138+
pairing=pairing,
139+
result_specs=result_specs,
140+
)
141+
142+
def _record_finished_round_with_specs(
143+
self,
144+
tournament: SwissTestTournament,
145+
*,
146+
round_number: int,
147+
pairing: list[tuple[str, str]],
148+
result_specs: list[tuple[str, int | None]],
149+
) -> None:
150+
self.assertEqual(len(pairing), len(result_specs))
131151
base_time = datetime(2026, 3, 10, 12, 0, tzinfo=timezone.utc)
132152

133-
for board_index, (white_name, black_name) in enumerate(pairing):
153+
for board_index, ((white_name, black_name), (result, status)) in enumerate(
154+
zip(pairing, result_specs, strict=True)
155+
):
134156
white_user = tournament.get_player_by_name(white_name)
135157
black_user = tournament.get_player_by_name(black_name)
136158
self.assertIsNotNone(white_user)
@@ -151,15 +173,19 @@ def _record_finished_round(
151173
str(white_data.rating),
152174
black_name,
153175
str(black_data.rating),
154-
result_cycle[(round_number + board_index) % len(result_cycle)],
176+
result,
155177
base_time + timedelta(minutes=round_number * 10 + board_index),
156178
False,
157179
False,
180+
status=status,
158181
round_no=round_number,
159182
)
160183

161184
tournament.update_players(game)
162-
wpoint, bpoint, _wperf, _bperf = tournament.points_perfs(game)
185+
if tournament.variant == "janggi":
186+
wpoint, bpoint, _wperf, _bperf = tournament.points_perfs_janggi(game)
187+
else:
188+
wpoint, bpoint, _wperf, _bperf = tournament.points_perfs(game)
163189

164190
white_data.points.append(wpoint)
165191
black_data.points.append(bpoint)
@@ -380,6 +406,40 @@ async def test_create_pairing_async_uses_bbp_backend_output(self):
380406
self.assertEqual(pairing, fake_pairing)
381407
create_bbp_pairing.assert_awaited_once_with(self.tournament, waiting, 0)
382408

409+
async def test_create_pairing_defaults_to_py4swiss_when_backend_env_is_unset(self):
410+
app_state = get_app_state(self.app)
411+
tid = id8()
412+
self.tournament = SwissTestTournament(
413+
app_state, tid, before_start=1, rounds=3, with_clock=False
414+
)
415+
app_state.tournaments[tid] = self.tournament
416+
await self.tournament.join_players(4)
417+
418+
waiting = list(self.tournament.waiting_players())
419+
users_by_id = {index: user for index, user in enumerate(waiting, start=1)}
420+
fake_state = SimpleNamespace(
421+
trf=object(),
422+
waiting_ids=set(users_by_id),
423+
users_by_id=users_by_id,
424+
)
425+
426+
class _Engine:
427+
@staticmethod
428+
def generate_pairings(_trf):
429+
return [
430+
SimpleNamespace(white=1, black=2),
431+
SimpleNamespace(white=3, black=4),
432+
]
433+
434+
with (
435+
patch.dict("os.environ", {"SWISS_PAIRING_BACKEND": ""}),
436+
patch("tournament.swiss._build_dutch_pairing_state", return_value=fake_state),
437+
patch("tournament.swiss.DutchEngine", _Engine),
438+
):
439+
pairing = self.tournament.create_pairing(waiting)
440+
441+
self.assertEqual(pairing, [(waiting[0], waiting[1]), (waiting[2], waiting[3])])
442+
383443
async def test_build_bbp_trf_export_includes_point_system_and_absent_players(self):
384444
app_state = get_app_state(self.app)
385445
tid = id8()
@@ -552,6 +612,73 @@ async def test_real_swisspairing_backend_matches_py4swiss_after_late_join(self):
552612
{player_name for pair in round_2_pairing for player_name in pair},
553613
)
554614

615+
@unittest.skipUnless(
616+
_has_swisspairing_runtime(),
617+
"swisspairing import unavailable for live backend parity test",
618+
)
619+
async def test_real_swisspairing_backend_matches_py4swiss_for_janggi_scores(self):
620+
app_state = get_app_state(self.app)
621+
tid = id8()
622+
self.tournament = SwissTestTournament(
623+
app_state,
624+
tid,
625+
before_start=1,
626+
rounds=4,
627+
with_clock=False,
628+
variant="janggi",
629+
)
630+
app_state.tournaments[tid] = self.tournament
631+
await self.tournament.join_players(5)
632+
await self.tournament.start(datetime.now(timezone.utc))
633+
634+
self.tournament.current_round = 1
635+
round_1_pairing, round_1_byes = self._assert_backend_outcome_match(
636+
self.tournament,
637+
round_number=1,
638+
keep_swisspairing_byes=True,
639+
)
640+
self.assertEqual(len(round_1_byes), 1)
641+
642+
await self.tournament.persist_byes()
643+
self._record_finished_round_with_specs(
644+
self.tournament,
645+
round_number=1,
646+
pairing=round_1_pairing,
647+
result_specs=[
648+
("1-0", VARIANTEND),
649+
("0-1", FLAG),
650+
],
651+
)
652+
653+
late = User(
654+
app_state,
655+
username="late_join_janggi_backend_parity",
656+
perfs=make_test_perfs(),
657+
)
658+
app_state.users[late.username] = late
659+
late.tournament_sockets[tid] = set((None,))
660+
661+
join_error = await self.tournament.join(late)
662+
self.assertIsNone(join_error)
663+
664+
late_data = self.tournament.player_data_by_name(late.username)
665+
self.assertIsNotNone(late_data)
666+
assert late_data is not None
667+
self.assertEqual(late_data.joined_round, 2)
668+
self.assertEqual([getattr(game, "token", "") for game in late_data.games], ["H"])
669+
self.assertEqual([point[0] for point in late_data.points], [2])
670+
671+
self.tournament.current_round = 2
672+
round_2_pairing, round_2_byes = self._assert_backend_outcome_match(
673+
self.tournament,
674+
round_number=2,
675+
)
676+
self.assertEqual(round_2_byes, [])
677+
self.assertIn(
678+
late.username,
679+
{player_name for pair in round_2_pairing for player_name in pair},
680+
)
681+
555682
@unittest.skipUnless(
556683
_has_swisspairing_runtime(),
557684
"swisspairing import unavailable for live backend parity test",

0 commit comments

Comments
 (0)