Add Tournaments UI #32
@ -8,9 +8,9 @@ from src.ezgg_lan_manager.types.TournamentBase import MatchStatus, TournamentErr
|
|||||||
|
|
||||||
|
|
||||||
class MatchParticipant:
|
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
|
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")
|
raise TournamentError("Invalid slot number")
|
||||||
self.slot_number = slot_number
|
self.slot_number = slot_number
|
||||||
|
|
||||||
@ -99,7 +99,9 @@ class Match:
|
|||||||
def next_match_lose_id(self) -> Optional[int]:
|
def next_match_lose_id(self) -> Optional[int]:
|
||||||
return self._next_match_lose_id
|
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)
|
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) < 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:
|
if len(self._participants) == 1 and self._participants[-1].slot_number == new_participant.slot_number:
|
||||||
@ -131,3 +133,28 @@ class Match:
|
|||||||
)
|
)
|
||||||
return (f"<Match id={self._match_id} round={self._round_number} bracket={self._bracket.name} "
|
return (f"<Match id={self._match_id} round={self._round_number} bracket={self._bracket.name} "
|
||||||
f"status={self.status.name} participants=[{participants}] winner={self.winner}>")
|
f"status={self.status.name} participants=[{participants}] winner={self.winner}>")
|
||||||
|
|
||||||
|
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"<Match id={self._match_id} round={self._round_number} bracket={self._bracket.name} "
|
||||||
|
f"status={self.status.name} participants=[{participants}] winner={self.winner}>")
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from datetime import datetime
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from math import ceil, log2
|
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.Participant import Participant
|
||||||
from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, TournamentError, Bracket, MatchStatus
|
from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, TournamentError, Bracket, MatchStatus
|
||||||
|
|
||||||
@ -128,10 +128,12 @@ class Tournament:
|
|||||||
bracket = "SINGLE"
|
bracket = "SINGLE"
|
||||||
elif fmt.name.startswith("DOUBLE_ELIMINATION"):
|
elif fmt.name.startswith("DOUBLE_ELIMINATION"):
|
||||||
bracket = "DOUBLE"
|
bracket = "DOUBLE"
|
||||||
|
elif fmt.name.startswith("FFA"):
|
||||||
|
bracket = "FINAL"
|
||||||
else:
|
else:
|
||||||
raise TournamentError(f"Unsupported tournament format: {fmt}")
|
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
|
bo = 1
|
||||||
elif fmt.name.endswith("_BO_3"):
|
elif fmt.name.endswith("_BO_3"):
|
||||||
bo = 3
|
bo = 3
|
||||||
@ -149,7 +151,28 @@ class Tournament:
|
|||||||
num_participants = len(self.participants)
|
num_participants = len(self.participants)
|
||||||
match_id_counter = 1
|
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 ---
|
# --- single-elimination as before ---
|
||||||
num_rounds = ceil(log2(num_participants))
|
num_rounds = ceil(log2(num_participants))
|
||||||
rounds: list[list[Match]] = []
|
rounds: list[list[Match]] = []
|
||||||
|
|||||||
@ -16,9 +16,10 @@ class TournamentFormat(Enum):
|
|||||||
DOUBLE_ELIMINATION_BO_1 = 4
|
DOUBLE_ELIMINATION_BO_1 = 4
|
||||||
DOUBLE_ELIMINATION_BO_3 = 5
|
DOUBLE_ELIMINATION_BO_3 = 5
|
||||||
DOUBLE_ELIMINATION_BO_5 = 6
|
DOUBLE_ELIMINATION_BO_5 = 6
|
||||||
|
FFA = 7
|
||||||
|
|
||||||
def tournament_format_to_display_texts(tournament_format: TournamentFormat) -> tuple[str, str]:
|
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:
|
if tournament_format == TournamentFormat.SINGLE_ELIMINATION_BO_1:
|
||||||
return "Single Elimination", "1"
|
return "Single Elimination", "1"
|
||||||
elif tournament_format == TournamentFormat.SINGLE_ELIMINATION_BO_3:
|
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"
|
return "Double Elimination", "3"
|
||||||
elif tournament_format == TournamentFormat.DOUBLE_ELIMINATION_BO_5:
|
elif tournament_format == TournamentFormat.DOUBLE_ELIMINATION_BO_5:
|
||||||
return "Double Elimination", "5"
|
return "Double Elimination", "5"
|
||||||
|
elif tournament_format == TournamentFormat.FFA:
|
||||||
|
return "Free for All", "1"
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(f"Unknown tournament status: {str(tournament_format)}")
|
raise RuntimeError(f"Unknown tournament status: {str(tournament_format)}")
|
||||||
|
|
||||||
|
|||||||
@ -64,4 +64,19 @@ class TournamentDomainTests(unittest.TestCase):
|
|||||||
self.assertEqual(sm.status, MatchStatus.WAITING)
|
self.assertEqual(sm.status, MatchStatus.WAITING)
|
||||||
self.assertEqual(sm.participants[0].participant_id, self.participant_c.id)
|
self.assertEqual(sm.participants[0].participant_id, self.participant_c.id)
|
||||||
self.assertEqual(sm.participants[0].slot_number, 1)
|
self.assertEqual(sm.participants[0].slot_number, 1)
|
||||||
self.assertIsNone(sm.winner)
|
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))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user