diff --git a/src/ezgg_lan_manager/types/Match.py b/src/ezgg_lan_manager/types/Match.py index 409d00e..da3a493 100644 --- a/src/ezgg_lan_manager/types/Match.py +++ b/src/ezgg_lan_manager/types/Match.py @@ -8,9 +8,9 @@ from src.ezgg_lan_manager.types.TournamentBase import MatchStatus, TournamentErr class MatchParticipant: - def __init__(self, participant_id: int, slot_number: Literal[1, 2]) -> None: + def __init__(self, participant_id: int, slot_number: Literal[-1, 1, 2]) -> None: self._participant_id = participant_id - if slot_number not in (1, 2): + if slot_number not in (-1, 1, 2): raise TournamentError("Invalid slot number") self.slot_number = slot_number @@ -99,7 +99,9 @@ class Match: def next_match_lose_id(self) -> Optional[int]: return self._next_match_lose_id - def assign_participant(self, participant_id: int, slot: Literal[1, 2]) -> None: + def assign_participant(self, participant_id: int, slot: Literal[-1, 1, 2]) -> None: + if slot == -1: + raise TournamentError("Normal match does not support slot -1") new_participant = MatchParticipant(participant_id, slot) if len(self._participants) < 2 and not any(p.participant_id == participant_id for p in self._participants): if len(self._participants) == 1 and self._participants[-1].slot_number == new_participant.slot_number: @@ -131,3 +133,28 @@ class Match: ) return (f"") + +class FFAMatch(Match): + """ + Specialized match that supports infinite participants + """ + def __init__(self, match_id: int, tournament_id: int, round_number: int, bracket: Bracket, best_of: int, status: MatchStatus, + next_match_win_lose_ids: tuple[Optional[int], Optional[int]], match_has_ended_callback: Callable) -> None: + super().__init__(match_id, tournament_id, round_number, bracket, best_of, status, next_match_win_lose_ids, match_has_ended_callback) + + @property + def is_fully_seeded(self) -> bool: + return len(self._participants) > 1 + + def assign_participant(self, participant_id: int, slot: Literal[-1, 1, 2]) -> None: + if slot != -1: + raise TournamentError("FFAMatch does not support slot 1 and 2") + new_participant = MatchParticipant(participant_id, slot) + self._participants.append(new_participant) + + def __repr__(self) -> str: + participants = ", ".join( + f"{p.participant_id}" for p in self._participants + ) + return (f"") diff --git a/src/ezgg_lan_manager/types/Tournament.py b/src/ezgg_lan_manager/types/Tournament.py index 578bcaa..076e06f 100644 --- a/src/ezgg_lan_manager/types/Tournament.py +++ b/src/ezgg_lan_manager/types/Tournament.py @@ -3,7 +3,7 @@ from datetime import datetime from typing import Optional from math import ceil, log2 -from src.ezgg_lan_manager.types.Match import Match +from src.ezgg_lan_manager.types.Match import Match, FFAMatch from src.ezgg_lan_manager.types.Participant import Participant from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, TournamentError, Bracket, MatchStatus @@ -128,10 +128,12 @@ class Tournament: bracket = "SINGLE" elif fmt.name.startswith("DOUBLE_ELIMINATION"): bracket = "DOUBLE" + elif fmt.name.startswith("FFA"): + bracket = "FINAL" else: raise TournamentError(f"Unsupported tournament format: {fmt}") - if fmt.name.endswith("_BO_1"): + if fmt.name.endswith("_BO_1") or fmt.name.endswith("FFA"): bo = 1 elif fmt.name.endswith("_BO_3"): bo = 3 @@ -149,7 +151,28 @@ class Tournament: num_participants = len(self.participants) match_id_counter = 1 - if bracket_type == "SINGLE": + if bracket_type == "FINAL": + rounds: list[list[Match]] = [] + round_matches = [] + match = FFAMatch( + match_id=match_id_counter, + tournament_id=self._id, + round_number=1, + bracket=Bracket.FINAL, + best_of=best_of, + status=MatchStatus.WAITING, + next_match_win_lose_ids=(None, None), + match_has_ended_callback=self.match_has_ended_callback + ) + + for participant in self.participants: + match.assign_participant(participant.id, -1) + + round_matches.append(match) + rounds.append(round_matches) + self._matches = [match] + + elif bracket_type == "SINGLE": # --- single-elimination as before --- num_rounds = ceil(log2(num_participants)) rounds: list[list[Match]] = [] diff --git a/src/ezgg_lan_manager/types/TournamentBase.py b/src/ezgg_lan_manager/types/TournamentBase.py index 039a7d7..3e9c487 100644 --- a/src/ezgg_lan_manager/types/TournamentBase.py +++ b/src/ezgg_lan_manager/types/TournamentBase.py @@ -16,9 +16,10 @@ class TournamentFormat(Enum): DOUBLE_ELIMINATION_BO_1 = 4 DOUBLE_ELIMINATION_BO_3 = 5 DOUBLE_ELIMINATION_BO_5 = 6 + FFA = 7 def tournament_format_to_display_texts(tournament_format: TournamentFormat) -> tuple[str, str]: - """ Returns tuple where idx 0 is SE/DE string and idx 1 is match count """ + """ Returns tuple where idx 0 is SE/DE/FFA string and idx 1 is match count """ if tournament_format == TournamentFormat.SINGLE_ELIMINATION_BO_1: return "Single Elimination", "1" elif tournament_format == TournamentFormat.SINGLE_ELIMINATION_BO_3: @@ -31,6 +32,8 @@ def tournament_format_to_display_texts(tournament_format: TournamentFormat) -> t return "Double Elimination", "3" elif tournament_format == TournamentFormat.DOUBLE_ELIMINATION_BO_5: return "Double Elimination", "5" + elif tournament_format == TournamentFormat.FFA: + return "Free for All", "1" else: raise RuntimeError(f"Unknown tournament status: {str(tournament_format)}") diff --git a/testing/unittests/TournamentDomainTests.py b/testing/unittests/TournamentDomainTests.py index f20ac8a..7e2b12f 100644 --- a/testing/unittests/TournamentDomainTests.py +++ b/testing/unittests/TournamentDomainTests.py @@ -64,4 +64,19 @@ class TournamentDomainTests(unittest.TestCase): self.assertEqual(sm.status, MatchStatus.WAITING) self.assertEqual(sm.participants[0].participant_id, self.participant_c.id) self.assertEqual(sm.participants[0].slot_number, 1) - self.assertIsNone(sm.winner) \ No newline at end of file + self.assertIsNone(sm.winner) + + def test_ffa_tournament_with_15_participants_gets_generated_correctly(self) -> None: + tournament_under_test = generate_new_tournament("Among Us", "It's Among Us", GameTitle("Among Us", "", "", ""), TournamentFormat.FFA, self.start_time, 32, TournamentStatus.OPEN) + + for i in range(1, 16): + tournament_under_test.add_participant(Participant(i, ParticipantType.PLAYER)) + tournament_under_test.start() + + # Assert Tournament was switched to ONGOING + self.assertEqual(TournamentStatus.ONGOING, tournament_under_test.status) + + matches_in_tournament = sorted(tournament_under_test.matches, key=lambda m: m.match_id) + + self.assertEqual(1, len(matches_in_tournament)) + self.assertEqual(15, len(matches_in_tournament[0].participants))