Skip to content

src.core

DataExport

Namespace for data export constants and enums.

Format

Bases: Enum

Enum for export formats.

Target

Bases: Enum

Enum for export targets.

Log

Handles logging messages with different severity levels.

LogEntry

LogEntry(msg, level)

Represents a single log entry.

Initializes a LogEntry.

Parameters:

Name Type Description Default
msg

The log message.

required
level

The severity level of the log.

required
Source code in src/core.py
464
465
466
467
468
469
470
471
472
def __init__(self, msg, level):
    """Initializes a LogEntry.

    Args:
        msg: The log message.
        level: The severity level of the log.
    """
    self.msg = msg
    self.level = level

export classmethod

export(fpath)

Exports the log to a file.

Parameters:

Name Type Description Default
fpath

The file path to export to.

required
Source code in src/core.py
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
@classmethod
def export(cls, fpath):
    """Exports the log to a file.

    Args:
        fpath: The file path to export to.
    """
    try:
        from pathlib import Path

        Path(fpath).parent.mkdir(parents=True, exist_ok=True)
        with open(fpath, "w") as f:
            f.writelines([str(s) + "\n" for s in cls.output])
    except Exception as e:
        cls.log(str(e), level=cls.Level.ERROR)

log classmethod

log(str_log, level=Level.NONE)

Logs a message with a specified level.

Parameters:

Name Type Description Default
str_log

The message to log.

required
level

The severity level (default: Level.NONE).

NONE
Source code in src/core.py
493
494
495
496
497
498
499
500
501
502
503
504
505
506
@classmethod
def log(cls, str_log, level=Level.NONE):
    """Logs a message with a specified level.

    Args:
        str_log: The message to log.
        level: The severity level (default: Level.NONE).
    """
    if cls.DISABLE:
        return
    entry = Log.LogEntry(str_log, level)
    cls.output.append(entry)
    if cls.PRINT:
        print(entry)

print classmethod

print()

Prints all log entries to the console.

Source code in src/core.py
508
509
510
511
512
@classmethod
def print(cls):
    """Prints all log entries to the console."""
    for entry in cls.output:
        print(entry)

Player

Player(tour: Tournament, name: str, uid: UUID | None = None, decklist: str | None = None)

Bases: IPlayer

Represents a player in the tournament.

Attributes:

Name Type Description
SORT_METHOD SortMethod

The method used for sorting players (ID, Name, Rank).

SORT_ORDER SortOrder

The order used for sorting (Ascending, Descending).

CACHE dict[UUID, Player]

Global cache of player instances.

Initializes a new Player instance.

Parameters:

Name Type Description Default
tour Tournament

The tournament the player belongs to.

required
name str

The name of the player.

required
uid UUID | None

The unique identifier for the player. If None, a new UUID is generated.

None
decklist str | None

Optional URL or string representing the player's decklist.

None
Source code in src/core.py
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
def __init__(
    self,
    tour: Tournament,
    name: str,
    uid: UUID | None = None,
    decklist: str | None = None,
):
    """Initializes a new Player instance.

    Args:
        tour: The tournament the player belongs to.
        name: The name of the player.
        uid: The unique identifier for the player. If None, a new UUID is generated.
        decklist: Optional URL or string representing the player's decklist.
    """
    self._tour = tour.uid
    super().__init__(uid=uid)
    self.name = name
    self.decklist = decklist
    self.CACHE[self.uid] = self
    self._pod_id: UUID | None = None  # Direct reference to current pod
    self.table_preference: list[int] = []

average_seat

average_seat(rounds: list[Round]) -> np.float64

Expressed in percentage. In a 4 pod game: seat 0: 100% seat 1: 66.66% seat 2: 33.33% seat 3: 0% In a 3 pod game: seat 0: 100% seat 1: 50% seat 2: 0%

Higher percentage means better seats, statistically. In subsequent matching attempts, these will get lower priority on early seats.

We are now using a weighted average of all the pods the player has been in. Weights are based on TC.global_wr_seats

Source code in src/core.py
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
def average_seat(self, rounds: list[Round]) -> np.float64:
    """
    Expressed in percentage.
    In a 4 pod game:
        seat 0: 100%
        seat 1: 66.66%
        seat 2: 33.33%
        seat 3: 0%
    In a 3 pod game:
        seat 0: 100%
        seat 1: 50%
        seat 2: 0%

    Higher percentage means better seats, statistically.
    In subsequent matching attempts, these will get lower priority on early seats.

    We are now using a weighted average of all the pods the player has been in.
    Weights are based on TC.global_wr_seats
    """
    pods = [
        self.pod(round)
        for round in rounds
        if self.pod(round) is not None and self.pod(round).done
    ]
    if not pods:
        return np.float64(0.5)
    n_pods = len([p for p in pods if isinstance(p, Pod)])
    if n_pods == 0:
        return np.float64(0.5)
    score = 0
    for pod in pods:
        if isinstance(pod, Pod):
            index = ([x.uid for x in pod.players]).index(self.uid)
            if index == 0:
                score += 1
            elif index == len(pod) - 1:
                continue
            else:
                rates = self.tour.config.global_wr_seats[0 : len(pod)]
                norm_scale = 1 - (np.cumsum(rates) - rates[0]) / (
                    np.sum(rates) - rates[0]
                )
                score += norm_scale[index]
    return np.float64(score / n_pods)

byes

byes(tour_round: Round | None = None)

Counts the number of byes received.

Parameters:

Name Type Description Default
tour_round Round | None

The round up to which to count.

None

Returns:

Name Type Description
int

The number of byes.

Source code in src/core.py
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
def byes(self, tour_round: Round | None = None):
    """Counts the number of byes received.

    Args:
        tour_round: The round up to which to count.

    Returns:
        int: The number of byes.
    """
    if tour_round is None:
        tour_round = self.tour.tour_round
    return len([p for p in self.record(tour_round) if p is Player.EResult.BYE])

draws

draws(tour_round: Round | None = None)

Counts the number of draws.

Parameters:

Name Type Description Default
tour_round Round | None

The round up to which to count.

None

Returns:

Name Type Description
int

The number of draws.

Source code in src/core.py
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
def draws(self, tour_round: Round | None = None):
    """Counts the number of draws.

    Args:
        tour_round: The round up to which to count.

    Returns:
        int: The number of draws.
    """
    if tour_round is None:
        tour_round = self.tour.tour_round
    return len(
        [
            p
            for p in self.games(tour_round)
            if p.result_type == Pod.EResult.DRAW and self.uid in p._result
        ]
    )

games

games(tour_round: Round | None = None)

Retrieves all completed games (pods) excluding byes and other non-game locations.

Parameters:

Name Type Description Default
tour_round Round | None

The round up to which to check.

None

Returns:

Type Description

list[Pod]: A list of actual completed game pods.

Source code in src/core.py
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
def games(self, tour_round: Round | None = None):
    """Retrieves all completed games (pods) excluding byes and other non-game locations.

    Args:
        tour_round: The round up to which to check.

    Returns:
        list[Pod]: A list of actual completed game pods.
    """
    if tour_round is None:
        tour_round = self.tour.tour_round
    return [p for p in self.pods(tour_round) if isinstance(p, Pod) and p.done]

losses

losses(tour_round: Round | None = None)

Counts the number of losses.

Parameters:

Name Type Description Default
tour_round Round | None

The round up to which to count.

None

Returns:

Name Type Description
int

The number of losses.

Source code in src/core.py
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
def losses(self, tour_round: Round | None = None):
    """Counts the number of losses.

    Args:
        tour_round: The round up to which to count.

    Returns:
        int: The number of losses.
    """
    if tour_round is None:
        tour_round = self.tour.tour_round
    return len(
        [
            p
            for p in self.games(tour_round)
            if p.result_type != Pod.EResult.PENDING and self.uid not in p._result
        ]
    )

played

played(tour_round: IRound | None = None) -> list[IPlayer]

Retrieves a list of unique opponents played against in completed pods.

Parameters:

Name Type Description Default
tour_round IRound | None

The round up to which to check.

None

Returns:

Type Description
list[IPlayer]

list[Player]: A list of unique opponents.

Source code in src/core.py
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
@override
def played(self, tour_round: IRound | None = None) -> list[IPlayer]:
    """Retrieves a list of unique opponents played against in completed pods.

    Args:
        tour_round: The round up to which to check.

    Returns:
        list[Player]: A list of unique opponents.
    """
    if tour_round is None:
        tour_round = self.tour.tour_round
    players = set()
    for p in self.pods(tour_round):
        if isinstance(p, Pod) and p.done:
            players.update(p.players)
    if players:
        players.discard(self)
    return list(players)

pods

pods(tour_round: IRound | None = None) -> list[IPod | Player.ELocation]

Retrieves all pods or locations the player has been assigned to.

Parameters:

Name Type Description Default
tour_round IRound | None

The round up to which to include pods.

None

Returns:

Type Description
list[IPod | ELocation]

list[Pod|Player.ELocation]: A list of pods or location markers (e.g. BYE).

Source code in src/core.py
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
@override
def pods(self, tour_round: IRound | None = None) -> list[IPod | Player.ELocation]:
    """Retrieves all pods or locations the player has been assigned to.

    Args:
        tour_round: The round up to which to include pods.

    Returns:
        list[Pod|Player.ELocation]: A list of pods or location markers (e.g. BYE).
    """
    if tour_round is None:
        tour_round = self.tour.tour_round
    pods: list[Pod | Player.ELocation] = [None for _ in range(tour_round.seq + 1)]  # type: ignore
    tour_rounds = self.tour.rounds

    for i, itr_round in enumerate(tour_rounds):
        pod = itr_round.get_location(self)
        if pod == None:
            pods[i] = itr_round.get_location_type(self)
        else:
            pods[i] = pod
        if itr_round == tour_round:
            break
    return pods

pointrate

pointrate(tour_round: Round | None = None) -> float

Calculates the point rate (actual points / maximum possible points).

Parameters:

Name Type Description Default
tour_round Round | None

The round up to which to calculate.

None

Returns:

Name Type Description
float float

The point rate.

Source code in src/core.py
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
def pointrate(self, tour_round: Round | None = None) -> float:
    """Calculates the point rate (actual points / maximum possible points).

    Args:
        tour_round: The round up to which to calculate.

    Returns:
        float: The point rate.
    """
    if len(self.games(tour_round)) == 0:
        return 0
    if tour_round is None:
        tour_round = self.tour.tour_round
    return self.rating(tour_round) / (
        self.tour.config.win_points * (tour_round.seq + 1)
    )

rating

rating(tour_round: Round | None) -> float

Calculates the player's rating (win percentage) up to a specific round.

Parameters:

Name Type Description Default
tour_round Round | None

The round up to which to calculate rating. If None, uses current round.

required

Returns:

Name Type Description
float float

The rating as a decimal (0.0 to 1.0).

Source code in src/core.py
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
def rating(self, tour_round: Round | None) -> float:
    """Calculates the player's rating (win percentage) up to a specific round.

    Args:
        tour_round: The round up to which to calculate rating. If None, uses current round.

    Returns:
        float: The rating as a decimal (0.0 to 1.0).
    """
    if tour_round is None:
        return 0
    return self.tour.rating(self, tour_round)

record

record(tour_round: Round | None = None) -> list[Player.EResult]

Retrieves the full history of results.

Parameters:

Name Type Description Default
tour_round Round | None

The round up to which to retrieve the record.

None

Returns:

Type Description
list[EResult]

list[Player.EResult]: A chronological list of results.

Source code in src/core.py
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
def record(self, tour_round: Round | None = None) -> list[Player.EResult]:
    """Retrieves the full history of results.

    Args:
        tour_round: The round up to which to retrieve the record.

    Returns:
        list[Player.EResult]: A chronological list of results.
    """
    seq = list()
    if tour_round is None:
        tour_round = self.tour.tour_round
    pods: list[Pod | Player.ELocation] = []
    for i, p in enumerate(self.pods(tour_round)):
        # if i < tour_round.seq:
        pods.append(p)
    for pod in pods:
        if pod == Player.ELocation.BYE:
            seq.append(Player.EResult.BYE)
        elif pod == Player.ELocation.GAME_LOSS:
            seq.append(Player.EResult.LOSS)
        elif isinstance(pod, Pod):
            if pod.result_type != Pod.EResult.PENDING:
                if pod.result_type == Pod.EResult.WIN and self.uid in pod._result:
                    seq.append(Player.EResult.WIN)
                elif (
                    pod.result_type == Pod.EResult.DRAW and self.uid in pod._result
                ):
                    seq.append(Player.EResult.DRAW)
                else:
                    seq.append(Player.EResult.LOSS)
            else:
                seq.append(Player.EResult.PENDING)
    return seq

result

result(tour_round: Round) -> Player.EResult

Retrieves the player's result for a specific round.

Parameters:

Name Type Description Default
tour_round Round

The round to query.

required

Returns:

Type Description
EResult

Player.EResult: The result (WIN, LOSS, DRAW, BYE, PENDING).

Source code in src/core.py
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
def result(self, tour_round: Round) -> Player.EResult:
    """Retrieves the player's result for a specific round.

    Args:
        tour_round: The round to query.

    Returns:
        Player.EResult: The result (WIN, LOSS, DRAW, BYE, PENDING).
    """
    if self.uid in tour_round._byes:
        return Player.EResult.BYE
    if self.uid in tour_round._game_loss:
        return Player.EResult.LOSS
    pod = self.pod(tour_round)
    if pod and len(pod._result) > 0:
        if self.uid in pod._result:
            if len(pod._result) == 1:
                return Player.EResult.WIN
            return Player.EResult.DRAW
        else:
            return Player.EResult.LOSS

    return Player.EResult.PENDING

seat_history

seat_history(tour_round: Round | None = None) -> str

Generates a string representation of the player's seat history.

Format: "seat/pod_size" for each round.

Parameters:

Name Type Description Default
tour_round Round | None

The round up to which to generate history.

None

Returns:

Name Type Description
str str

The seat history string.

Source code in src/core.py
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
def seat_history(self, tour_round: Round | None = None) -> str:
    """Generates a string representation of the player's seat history.

    Format: "seat/pod_size" for each round.

    Args:
        tour_round: The round up to which to generate history.

    Returns:
        str: The seat history string.
    """
    if tour_round is None:
        tour_round = self.tour.tour_round
    pods = self.pods(tour_round)
    if sum([1 for p in pods if isinstance(p, Pod) and p.done]) == 0:
        return "N/A"
    ret_str = " ".join(
        [
            "{}/{}".format(
                ([x.uid for x in p.players]).index(self.uid) + 1, len(p.players)
            )
            if isinstance(p, Pod)
            else "N/A"
            for p in pods
        ]
    )
    return ret_str

set_result

set_result(tour_round: Round, result: EResult) -> None

Sets the result for the player in a specific round.

Parameters:

Name Type Description Default
tour_round Round

The round where the result occurred.

required
result EResult

The result to set (WIN, LOSS, DRAW, BYE).

required
Source code in src/core.py
2052
2053
2054
2055
2056
2057
2058
2059
def set_result(self, tour_round: Round, result: Player.EResult) -> None:
    """Sets the result for the player in a specific round.

    Args:
        tour_round: The round where the result occurred.
        result: The result to set (WIN, LOSS, DRAW, BYE).
    """
    tour_round.set_result(self, result)

wins

wins(tour_round: Round | None = None)

Counts the number of wins.

Parameters:

Name Type Description Default
tour_round Round | None

The round up to which to count.

None

Returns:

Name Type Description
int

The number of wins.

Source code in src/core.py
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
def wins(self, tour_round: Round | None = None):
    """Counts the number of wins.

    Args:
        tour_round: The round up to which to count.

    Returns:
        int: The number of wins.
    """
    if tour_round is None:
        tour_round = self.tour.tour_round
    return len(
        [
            p
            for p in self.games(tour_round)
            if p.result_type == Pod.EResult.WIN and self.uid in p._result
        ]
    )

Pod

Pod(tour_round: Round, table: int, cap=0, uid: UUID | None = None)

Bases: IPod

Represents a single pod (table) in a round.

Attributes:

Name Type Description
table int

The table number.

cap int

The capacity of the pod (maximum number of players).

_players list[UUID]

List of player UUIDs in the pod.

Initializes a new Pod instance.

Parameters:

Name Type Description Default
tour_round Round

The round the pod belongs to.

required
table int

The table number.

required
cap

The player capacity of the pod.

0
uid UUID | None

Optional UUID.

None
Source code in src/core.py
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
def __init__(self, tour_round: Round, table: int, cap=0, uid: UUID | None = None):
    """Initializes a new Pod instance.

    Args:
        tour_round: The round the pod belongs to.
        table: The table number.
        cap: The player capacity of the pod.
        uid: Optional UUID.
    """
    self._tour = tour_round._tour
    self._round = tour_round.uid
    super().__init__(uid=uid)
    self.cap: int = cap
    self._players: list[UUID] = list()
    self._result: set[UUID] = set()

balance property

balance: ndarray

Returns a list of count of players above 50% average seat and below 50% average seat

name property

name

Returns the name of the pod. The name of the pod is "Pod {}".format(self.table).

players property

players: list[Player]

Returns the list of players in the pod.

result property

result: set[Player]

Retrieves the players involved in the result of the pod (e.g., winners or drawers).

Returns:

Type Description
set[Player]

set[Player]: A set of players.

table property

table: int

Returns the table number of the pod. The table number is determined by the pod's index in the round's pod list (+1).

add_player

add_player(player: Player, manual=False, player_pod_map=None) -> bool

Adds a player to the pod.

Parameters:

Name Type Description Default
player Player

The player to add.

required
manual

If True, allows exceeding the pod's capacity.

False
player_pod_map

Optional map to update player locations (internal use).

None

Returns:

Name Type Description
bool bool

True if the player was added, False otherwise (e.g., if full and not manual).

Source code in src/core.py
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
@override
def add_player(self, player: Player, manual=False, player_pod_map=None) -> bool:
    """Adds a player to the pod.

    Args:
        player: The player to add.
        manual: If True, allows exceeding the pod's capacity.
        player_pod_map: Optional map to update player locations (internal use).

    Returns:
        bool: True if the player was added, False otherwise (e.g., if full and not manual).
    """
    if len(self) >= self.cap and self.cap and not manual:
        return False
    if pod := player.pod(self.tour_round):
        pod.remove_player(player)
    self.tour_round._byes.discard(player.uid)
    self._players.append(player.uid)
    self.tour_round.player_locations_map[player.uid] = self
    # player.location = Player.ELocation.SEATED
    # player.pod = self  # Update player's pod reference
    return True

auto_assign_seats

auto_assign_seats()

Assigns seats to players in the pod.

Seat assignment attempts to balance seating positions based on players' history, giving preference to players who have had poor seat variance in the past.

Source code in src/core.py
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
@override
def auto_assign_seats(self):
    """Assigns seats to players in the pod.

    Seat assignment attempts to balance seating positions based on players' history,
    giving preference to players who have had poor seat variance in the past.
    """
    # Average seating positions
    average_positions = [
        p.average_seat(self.tour.ended_rounds) for p in self.players
    ]
    n = len(average_positions)

    if not any(average_positions):
        random.shuffle(self.players)
        return

    # partially sort players based on seating positions
    # those that have same average_seat should be randomly ordered
    seat_assignment = [0] * n
    for i in range(n):
        seat_assignment[i] = (
            sum([1 for x in average_positions if x < average_positions[i]]) + 1
        )
    # randomize players with same average_seat
    seat_assignment = [x + random.random() for x in seat_assignment]
    # sort players based on seat assignment
    self._players[:] = np.take(self._players, np.argsort(seat_assignment))

    pass

clear

clear()

Clears the pod of all players. This method is called by the round when the pod is removed.

Source code in src/core.py
2824
2825
2826
2827
2828
2829
2830
def clear(self):
    """Clears the pod of all players.
    This method is called by the round when the pod is removed.
    """
    players = [p for p in self.players]
    for p in players:
        self.remove_player(p, cleanup=False)

reorder_players

reorder_players(order: list[int]) -> None

Reorders the players in the pod. Passing range(len(self._players)) will result no change in the order. Passing [0, 1, 2, 3] will result no change in the order. Passing [3, 2, 1, 0] will reverse the order of the players.

Parameters:

Name Type Description Default
order list[int]

A list of integers representing the new order of the players.

required
Source code in src/core.py
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
def reorder_players(self, order: list[int]) -> None:
    """Reorders the players in the pod.
    Passing range(len(self._players)) will result no change in the order.
    Passing [0, 1, 2, 3] will result no change in the order.
    Passing [3, 2, 1, 0] will reverse the order of the players.

    Args:
        order: A list of integers representing the new order of the players.
    """
    if len(order) != len(self._players):
        raise ValueError("Order must have the same length as the number of players")
    if any([x not in range(len(self._players)) for x in order]):
        raise ValueError("Order must contain all integers from 0 to n-1")
    if len(set(order)) != len(order):
        raise ValueError("Order must not contain duplicate integers")

    self._players[:] = np.take(self._players, order)

PodsExport

Bases: DataExport

Handles the export of tournament pods.

auto_export classmethod

auto_export(func: _F) -> _F

Decorator to automatically export pods after a function call.

Parameters:

Name Type Description Default
func _F

The function to decorate.

required

Returns:

Type Description
_F

The decorated function.

Source code in src/core.py
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
@classmethod
def auto_export(cls, func: _F) -> _F:
    """Decorator to automatically export pods after a function call.

    Args:
        func: The function to decorate.

    Returns:
        The decorated function.
    """

    @functools.wraps(func)
    def auto_pods_export_wrapper(  # pyright: ignore[reportAny]
        self: Tournament,
        *original_args,
        **original_kwargs,  # pyright: ignore[reportUnknownParameterType, reportMissingParameterType]
    ):
        try:
            tour_round = self.tour_round
        except (KeyError, ValueError):
            tour_round = None
        ret = func(self, *original_args, **original_kwargs)
        try:
            tour_round = tour_round or self.tour_round
        except (KeyError, ValueError):
            tour_round = None
        if self.config.auto_export:
            logf = TournamentAction.LOGF
            if logf and tour_round:
                # Export pods to a file named {tournament_name}_round_{round_number}.txt
                # And also export it into {log_directory}/pods.txt
                context = TournamentContext(
                    self, tour_round, self.get_standings(tour_round)
                )
                export_str: str = "\n\n".join(
                    [pod.__repr__(context=context) for pod in tour_round.pods]
                )
                game_lost: list[Player] = [
                    x
                    for x in tour_round.active_players
                    if x.result == Player.EResult.LOSS
                ]
                byes = [
                    x
                    for x in tour_round.unassigned
                    if x.location == Player.ELocation.UNASSIGNED
                    and x.result == Player.EResult.BYE
                ]
                if len(game_lost) + len(byes) > 0:
                    max_len = max([len(p.name) for p in game_lost + byes])
                    if self.config.allow_bye and byes:
                        export_str += "\n\nByes:\n" + "\n".join(
                            [
                                "\t{} | pts: {}".format(
                                    p.name.ljust(max_len),
                                    p.rating(tour_round) or "0",
                                )
                                for p in tour_round.unassigned
                                if p.result == Player.EResult.BYE
                            ]
                        )
                    if game_lost:
                        export_str += "\n\nGame losses:\n" + "\n".join(
                            [
                                "\t{} | pts: {}".format(
                                    p.name.ljust(max_len), p.rating(tour_round)
                                )
                                for p in game_lost
                            ]
                        )

                path = os.path.join(  # pyright: ignore[reportUnknownVariableType]
                    os.path.dirname(logf),  # pyright: ignore[reportCallIssue, reportUnknownArgumentType, reportArgumentType]
                    os.path.basename(logf).replace(".json", ""),  # pyright: ignore[reportUnknownArgumentType, reportCallIssue, reportArgumentType]
                    os.path.basename(logf).replace(  # pyright: ignore[reportCallIssue, reportUnknownArgumentType, reportArgumentType]
                        ".json", "_R{}.txt".format(tour_round.seq)
                    ),
                )
                if not os.path.exists(os.path.dirname(path)):  # pyright: ignore[reportUnknownArgumentType]
                    os.makedirs(os.path.dirname(path))  # pyright: ignore[reportUnknownArgumentType]

                self.export_str(export_str, path, DataExport.Target.FILE)
                if os.getenv("EXPORT_ONLINE_API_URL") and os.getenv(
                    "EXPORT_ONLINE_API_KEY"
                ):
                    self.export_str(export_str, None, DataExport.Target.WEB)

                path = os.path.join(os.path.dirname(logf), "pods.txt")  # pyright: ignore[reportCallIssue, reportUnknownArgumentType, reportArgumentType]
                self.export_str(export_str, path, DataExport.Target.FILE)

        return ret

    return cast(_F, auto_pods_export_wrapper)

Round

Round(tour: Tournament, seq: int, stage: Stage, pairing_logic: IPairingLogic, uid: UUID | None = None, dropped: set[UUID] | None = None, disabled: set[UUID] | None = None, byes: set[UUID] | None = None, game_loss: set[UUID] | None = None)

Bases: IRound

Represents a single round in the tournament.

Attributes:

Name Type Description
seq int

The sequence number of the round (0-indexed).

stage Stage

The stage of the round (Swiss, Top X).

logic IPairingLogic

The pairing logic used for this round.

CACHE dict[UUID, Round]

Global cache of round instances.

Initializes a new Round instance.

Parameters:

Name Type Description Default
tour Tournament

The tournament the round belongs to.

required
seq int

The sequence number of the round.

required
stage Stage

The stage of the round (e.g., Swiss, Top 4).

required
pairing_logic IPairingLogic

The logic used for pairing players in this round.

required
uid UUID | None

Optional UUID.

None
dropped set[UUID] | None

Optional set of dropped player UUIDs.

None
disabled set[UUID] | None

Optional set of disabled player UUIDs.

None
byes set[UUID] | None

Optional set of player UUIDs who have byes.

None
game_loss set[UUID] | None

Optional set of player UUIDs who have game losses.

None
Source code in src/core.py
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
def __init__(
    self,
    # Required
    tour: Tournament,
    seq: int,
    stage: Stage,
    pairing_logic: IPairingLogic,
    # Optional
    uid: UUID | None = None,
    dropped: set[UUID] | None = None,
    disabled: set[UUID] | None = None,
    byes: set[UUID] | None = None,
    game_loss: set[UUID] | None = None,
):
    """Initializes a new Round instance.

    Args:
        tour: The tournament the round belongs to.
        seq: The sequence number of the round.
        stage: The stage of the round (e.g., Swiss, Top 4).
        pairing_logic: The logic used for pairing players in this round.
        uid: Optional UUID.
        dropped: Optional set of dropped player UUIDs.
        disabled: Optional set of disabled player UUIDs.
        byes: Optional set of player UUIDs who have byes.
        game_loss: Optional set of player UUIDs who have game losses.
    """
    self._tour: UUID = tour.uid
    super().__init__(uid=uid)
    self.tour.ROUND_CACHE[self.uid] = self
    self.seq: int = seq
    self.stage: Round.Stage = stage

    self._pods: list[UUID] = list()
    self._players: list[UUID] = list()

    self._logic = pairing_logic.name
    self._byes: set[UUID] = set() if byes is None else byes
    self._game_loss: set[UUID] = set() if game_loss is None else game_loss
    self._dropped: set[UUID] = set() if dropped is None else dropped
    self._disabled: set[UUID] = set() if disabled is None else disabled

    self.player_locations_map: dict[UUID, Pod] = {}

all_players_assigned property

all_players_assigned

Checks if all active players are assigned to pods.

Returns:

Name Type Description
bool

True if all active players are assigned to pods, False otherwise.

done property

done

Checks if the round is completed.

A round is considered done if all pods within it have reported results (i.e., no pending pods remain).

Returns:

Name Type Description
bool

True if the round is done, False otherwise.

seated property

seated: set[Player]

Returns the set of players who are currently assigned to pods.

Returns:

Type Description
set[Player]

set[Player]: A set of Player instances that are assigned to pods.

unassigned property

unassigned: set[Player]

Returns the set of players who are not currently assigned to pods.

Returns:

Type Description
set[Player]

set[Player]: A set of Player instances that are not assigned to pods.

advancing_players

advancing_players(standings) -> list[Player]

Determines which players advance to the next round.

This is primarily used for transitions from Swiss to Top Cut, or between Top Cut rounds.

Parameters:

Name Type Description Default
standings

A list of players sorted by their current standing.

required

Returns:

Type Description
list[Player]

list[Player]: The list of players who advance. - For Swiss rounds, typically all active players return. - For Top Cut rounds, only winners (and potentially high-seeded byes) advance.

Source code in src/core.py
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
def advancing_players(self, standings) -> list[Player]:
    """Determines which players advance to the next round.

    This is primarily used for transitions from Swiss to Top Cut, or between Top Cut rounds.

    Args:
       standings: A list of players sorted by their current standing.

    Returns:
        list[Player]: The list of players who advance.
            - For Swiss rounds, typically all active players return.
            - For Top Cut rounds, only winners (and potentially high-seeded byes) advance.
    """
    # Create index map for O(1) standings lookup instead of O(n) index() calls
    standings_index = {player: idx for idx, player in enumerate(standings)}

    if self.stage == Round.Stage.SWISS:
        return sorted(
            self.active_players,
            key=lambda x: standings_index.get(x, len(standings)),
        )
    else:
        active_players_set = self.active_players  # Cache set lookup

        # Collect players in three groups to maintain proper ordering:
        # 1. Byes (sorted by standings)
        # 2. Wins (sorted by standings)
        # 3. Draws (one per draw pod, sorted by standings)
        bye_players: list[Player] = []
        win_players: list[Player] = []
        draw_players: list[Player] = []
        processed_draw_pods: set[Pod] = (
            set()
        )  # Track processed draw pods to avoid duplicates

        for p in standings:
            if p not in active_players_set:
                continue

            # Handle byes
            if p in self.byes:
                bye_players.append(p)
                continue

            # Get pod once per player
            pod = p.pod(self)
            if pod is None:
                continue

            # Handle WIN results
            if pod.done and pod.result_type == Pod.EResult.WIN:
                if p in pod.result:
                    win_players.append(p)

            # Handle DRAW results (only process once per pod)
            elif (
                pod.result_type == Pod.EResult.DRAW
                and pod not in processed_draw_pods
            ):
                processed_draw_pods.add(pod)
                if pod.result:
                    # Filter to only active players in the draw result
                    active_in_draw = [
                        p for p in pod.result if p in active_players_set
                    ]
                    if active_in_draw:
                        advancing_player = min(
                            active_in_draw,
                            key=lambda x: standings_index.get(x, len(standings)),
                        )
                        draw_players.append(advancing_player)

        # Sort each group by standings and concatenate in order
        bye_players.sort(key=lambda x: standings_index.get(x, len(standings)))
        win_players.sort(key=lambda x: standings_index.get(x, len(standings)))
        draw_players.sort(key=lambda x: standings_index.get(x, len(standings)))

        return bye_players + win_players + draw_players

create_pairings

create_pairings() -> None

Executes the pairing logic to assign players to pods.

This method uses the round's pairing_logic to determine match-ups and assigns players to the pods created by create_pods.

Source code in src/core.py
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
def create_pairings(self) -> None:
    """Executes the pairing logic to assign players to pods.

    This method uses the round's `pairing_logic` to determine match-ups and assigns
    players to the pods created by `create_pods`.
    """
    if self.stage != Round.Stage.SWISS:
        standings = self.tour.get_standings(self.tour.previous_round(self))
        self.disable_topcut(standings)
        if self.stage in [
            Round.Stage.TOP_7,
            Round.Stage.TOP_10,
            Round.Stage.TOP_13,
            Round.Stage.TOP_16,
            Round.Stage.TOP_40,
        ]:
            self.logic.advance_topcut(self, cast(list[IPlayer], standings))

    self.create_pods()
    pods = [p for p in self.pods if all([not p.done, len(p) < p.cap])]

    self.logic.make_pairings(self, cast(set[IPlayer], self.unassigned), pods)

    if self.seq < self.tour.config.n_rounds:
        for pod in self.pods:
            pod.auto_assign_seats()

    self.sort_pods()

create_pods

create_pods() -> None

Creates empty pod slots for the round.

This method calculates the number and size of pods required based on the number of active players and the tournament configuration, then initializes these pods.

Source code in src/core.py
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
def create_pods(self) -> None:
    """Creates empty pod slots for the round.

    This method calculates the number and size of pods required based on the number of
    active players and the tournament configuration, then initializes these pods.
    """
    seats_required = len(self.unassigned) - sum(
        [pod.cap - len(pod) for pod in self.pods if not pod.done]
    )
    if seats_required == 0:
        return
    pod_sizes = self.tour.get_pod_sizes(seats_required)
    if pod_sizes is None:
        Log.log("Can not make pods.", level=Log.Level.WARNING)
        return None
    start_table = len(self._pods) + 1
    for i, size in enumerate(pod_sizes):
        pod = Pod(self, start_table + i, cap=size)
        self._pods.append(pod.uid)

delete

delete() -> bool

Deletes the current round if it's not completed.

This method removes the current round from the tournament and clears all pods and bye list.

Returns:

Name Type Description
bool bool

True if the round was deleted, False otherwise.

Source code in src/core.py
3208
3209
3210
3211
3212
3213
3214
3215
3216
def delete(self) -> bool:
    """Deletes the current round if it's not completed.

    This method removes the current round from the tournament and clears all pods and bye list.

    Returns:
        bool: True if the round was deleted, False otherwise.
    """
    return self.tour.delete_round(self)

disable_topcut

disable_topcut(standings: list[Player])

Disable players who don't advance to top cut. They remain in the tournament but won't participate in top cut rounds.

Source code in src/core.py
3270
3271
3272
3273
3274
3275
3276
3277
def disable_topcut(self, standings: list[Player]):
    """Disable players who don't advance to top cut.
    They remain in the tournament but won't participate in top cut rounds."""
    standings = self.tour.get_standings(self.tour.previous_round(self))

    # Disable players from bottom of standings until we reach top_cut size
    for p in standings[self.stage.value : :]:
        self.disable_player(p, set_disabled=True)

remove_pod

remove_pod(pod: Pod) -> bool

Removes a pod from the round, clearing its assignments.

Parameters:

Name Type Description Default
pod Pod

The pod to remove.

required

Returns:

Name Type Description
bool bool

True if the pod was successfully removed, False otherwise.

Source code in src/core.py
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
def remove_pod(self, pod: Pod) -> bool:
    """Removes a pod from the round, clearing its assignments.

    Args:
        pod: The pod to remove.

    Returns:
        bool: True if the pod was successfully removed, False otherwise.
    """
    # if not pod.done:
    pod.clear()
    self._pods.remove(pod.uid)
    return True

reset_pods

reset_pods() -> bool

Resets all pods in the round, clearing their assignments.

This method removes all players from all pods and clears the bye list. It is useful for resetting the round before creating new pairings.

Returns:

Name Type Description
bool bool

True if the reset was successful, False otherwise.

Source code in src/core.py
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
def reset_pods(self) -> bool:
    """Resets all pods in the round, clearing their assignments.

    This method removes all players from all pods and clears the bye list.
    It is useful for resetting the round before creating new pairings.

    Returns:
        bool: True if the reset was successful, False otherwise.
    """
    pods = [Pod.get(self.tour, x) for x in self._pods]
    # if any([not pod.done for pod in pods]):
    #    return False
    self._byes.clear()
    for pod in pods:
        self.remove_pod(pod)
    return True

sort_pods

sort_pods() -> bool

Try to apply table preferences for players. Pod index is table number. Preserves the relative power-sorted order of non-locked pods. Prioritizes maximizing the number of satisfied preferences.

Returns:

Name Type Description
bool bool

True if table preferences were applied, False otherwise.

Source code in src/core.py
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
def sort_pods(self) -> bool:
    """Try to apply table preferences for players. Pod index is table number.
    Preserves the relative power-sorted order of non-locked pods.
    Prioritizes maximizing the number of satisfied preferences.

    Returns:
        bool: True if table preferences were applied, False otherwise.
    """
    self.sort_pods_by_power()

    pods = self.pods  # Expect pods to be already sorted by power
    n = len(pods)
    if n == 0:
        return False

    result_pods = [None] * n
    assigned_pod_uids = set()
    any_swapped = False

    # Pass 1: Handle Locked Pods (Best effort satisfaction)
    # We want to satisfy as many preferences as possible.
    # Primary priority: Number of players satisfied in that pod.
    # Secondary priority: Original power order (tie-breaker).

    possibilities = []
    for rank, pod in enumerate(pods):
        # Aggregated preferences for players in this pod (1-indexed)
        counts = {}
        for p in pod.players:
            for pref in p.table_preference:
                target_idx = pref - 1  # Convert to 0-indexed
                if 0 <= target_idx < n:
                    counts[target_idx] = counts.get(target_idx, 0) + 1
                else:
                    Log.log(
                        f"Player {p.name} has invalid table preference: {pref}. Max table index is {n}",
                        level=Log.Level.WARNING,
                    )

        for target_idx, count in counts.items():
            # Sort key: (-count, rank, target_idx)
            # (Higher count first, then higher power first, then lower index first)
            possibilities.append((-count, rank, pod, target_idx))

    possibilities.sort()

    for _, _, pod, target_idx in possibilities:
        if pod.uid not in assigned_pod_uids and result_pods[target_idx] is None:
            result_pods[target_idx] = pod
            assigned_pod_uids.add(pod.uid)
            any_swapped = True

    # Pass 2: Fill gaps with remaining pods (Preserving relative power order)
    pod_iter = iter(pods)
    for i in range(n):
        if result_pods[i] is None:
            # Find the next pod that hasn't been assigned yet
            try:
                while True:
                    next_pod = next(pod_iter)
                    if next_pod.uid not in assigned_pod_uids:
                        result_pods[i] = next_pod
                        break
            except StopIteration:
                break  # Should not happen

    # Update the underlying UUID list
    self._pods[:] = [p.uid for p in result_pods if p is not None]

    return any_swapped

sort_pods_by_power

sort_pods_by_power() -> None

Sort pods by number of players and average rating to establish a power-level baseline.

Source code in src/core.py
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
def sort_pods_by_power(self) -> None:
    """Sort pods by number of players and average rating to establish a power-level baseline."""
    pods_sorted = sorted(
        self.pods,
        key=lambda x: (
            len(x.players),
            np.average([p.rating(self) for p in x.players]),
        ),
        reverse=True,
    )
    self._pods[:] = [pod.uid for pod in pods_sorted]

SortMethod

Bases: Enum

Enum for sorting methods.

SortOrder

Bases: Enum

Enum for sorting order.

StandingsExport

StandingsExport(fields=None, format: Format = DataExport.Format.PLAIN, dir: Union[str, None] = None)

Bases: DataExport, IStandingsExport

Source code in src/core.py
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
def __init__(
    self,
    fields=None,
    format: DataExport.Format = DataExport.Format.PLAIN,
    dir: Union[str, None] = None,
):
    if fields is None:
        self.fields = self.DEFAULT_FIELDS
    else:
        self.fields = fields
    self.format = format
    if dir is None:
        self.dir = "./logs/standings" + self.ext[self.format]
    else:
        self.dir = dir

inflate classmethod

inflate(data: dict)

Creates a StandingsExport instance from a dictionary.

Parameters:

Name Type Description Default
data dict

The dictionary containing the configuration.

required

Returns:

Type Description

A new StandingsExport instance.

Source code in src/core.py
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
@classmethod
def inflate(cls, data: dict):
    """Creates a StandingsExport instance from a dictionary.

    Args:
        data: The dictionary containing the configuration.

    Returns:
        A new StandingsExport instance.
    """
    return cls(
        [StandingsExport.Field(f) for f in data["fields"]],
        StandingsExport.Format(data["format"]),
        data["dir"],
    )

serialize

serialize()

Serializes the export configuration.

Returns:

Type Description

A dictionary containing the serialized configuration.

Source code in src/core.py
408
409
410
411
412
413
414
415
416
417
418
def serialize(self):
    """Serializes the export configuration.

    Returns:
        A dictionary containing the serialized configuration.
    """
    return {
        "fields": [f.value for f in self.fields],
        "format": self.format.value,
        "dir": self.dir,
    }

Tournament

Tournament(config: TournamentConfiguration | None = None, uid: UUID | None = None)

Bases: ITournament

Represents a tournament, managing players, rounds, and pairings.

Attributes:

Name Type Description
CACHE dict[UUID, Tournament]

Global cache of tournament instances.

_pairing_logic_cache dict[str, type[IPairingLogic]]

Cache of discovered pairing logic implementations.

Initializes a new Tournament instance.

Parameters:

Name Type Description Default
config TournamentConfiguration | None

The configuration object for the tournament. If None, a default configuration is used.

None
uid UUID | None

The unique identifier for the tournament. If None, a new UUID is generated.

None
Source code in src/core.py
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
def __init__(
    self,
    config: TournamentConfiguration | None = None,
    uid: UUID | None = None,
):  # type: ignore
    """Initializes a new Tournament instance.

    Args:
        config: The configuration object for the tournament. If None, a default configuration is used.
        uid: The unique identifier for the tournament. If None, a new UUID is generated.
    """
    if config is None:
        config = TournamentConfiguration()
    super().__init__(uid=uid)
    self.__config = config
    # self.CACHE[self.uid] = self

    self.PLAYER_CACHE: dict[UUID, Player] = {}
    self.POD_CACHE: dict[UUID, Pod] = {}
    self.ROUND_CACHE: dict[UUID, Round] = {}

    self._rounds: list[UUID] = list()
    self._players: set[UUID] = set()
    # self._dropped: list[UUID] = list()
    # self._disabled: list[UUID] = list()  # Players disabled from top cut (but still in tournament)
    self._round: UUID | None = None

config property writable

config: TournamentConfiguration

Returns the tournament configuration.

Returns:

Name Type Description
TournamentConfiguration TournamentConfiguration
  • The tournament configuration.

draw_rate property

draw_rate: float

Calculates the draw rate for the tournament.

Returns:

Name Type Description
float float
  • The draw rate as a float.

ended_rounds property

ended_rounds

Returns the list of completed rounds in the tournament.

Returns:

Type Description

list[Round]: - The list of completed rounds.

final_swiss_round property

final_swiss_round: Round | None

Returns the final Swiss round of the tournament.

Returns:

Type Description
Round | None

Round|None: - The final Swiss round if it has been played. - None if the final round has not been played yet.

last_round property

last_round: Round | None

Returns the last round of the tournament.

Returns:

Type Description
Round | None

Round|None: - The last round if it has been played. - None if no rounds have been played.

pods property

pods: list[Pod] | None

Returns the list of pods in the current round.

Returns:

Type Description
list[Pod] | None

list[Pod]|None: - The list of pods if the current round has been set. - None if the current round has not been set.

rounds property writable

rounds: list[Round]

Returns the list of rounds in the tournament.

Returns:

Type Description
list[Round]

list[Round]: - The list of rounds.

swiss_rounds property

swiss_rounds

Returns the list of swiss rounds in the tournament.

Returns:

Type Description

list[Round]: - The list of swiss rounds.

__initialize_round

__initialize_round() -> bool

Initializes a new round in the tournament.

This method determines the appropriate stage (Swiss, Top Cut) and pairing logic based on the tournament configuration and current progress. It does not create pairings, only sets up the round structure.

Returns:

Name Type Description
bool bool

True if a new ro und was successfully initialized. False if a round is already in progress, the maximum number of rounds has been reached, or the tournament is completed.

Source code in src/core.py
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
def __initialize_round(self) -> bool:
    """Initializes a new round in the tournament.

    This method determines the appropriate stage (Swiss, Top Cut) and pairing logic based on the
    tournament configuration and current progress. It does not create pairings, only sets up the round structure.

    Returns:
        bool: True if a new ro  und was successfully initialized. False if a round is already in progress,
              the maximum number of rounds has been reached, or the tournament is completed.
    """
    if self._round is not None and not self.tour_round.done:
        return False
    seq = len(self.rounds)
    stage = Round.Stage.SWISS
    logic = None
    if seq >= self.config.n_rounds and self.last_round:
        if self.config.top_cut == TournamentConfiguration.TopCut.NONE:
            Log.log("Maximum number of rounds reached.", level=Log.Level.WARNING)
            return False
        if self.config.top_cut == TournamentConfiguration.TopCut.TOP_4:
            if self.last_round.stage == Round.Stage.SWISS:
                logic = self.get_pairing_logic("PairingTop4")
                stage = Round.Stage.TOP_4
            else:
                Log.log("Tournament completed.")
                return False
        elif self.config.top_cut == TournamentConfiguration.TopCut.TOP_7:
            if self.last_round.stage == Round.Stage.SWISS:
                stage = Round.Stage.TOP_7
                logic = self.get_pairing_logic("PairingTop7")
            elif self.last_round.stage == Round.Stage.TOP_7:
                stage = Round.Stage.TOP_4
                logic = self.get_pairing_logic("PairingTop4")
            else:
                Log.log("Tournament completed.")
                return False
        elif self.config.top_cut == TournamentConfiguration.TopCut.TOP_10:
            if self.last_round.stage == Round.Stage.SWISS:
                stage = Round.Stage.TOP_10
                logic = self.get_pairing_logic("PairingTop10")
            elif self.last_round.stage == Round.Stage.TOP_10:
                stage = Round.Stage.TOP_4
                logic = self.get_pairing_logic("PairingTop4")
            else:
                Log.log("Tournament completed.")
                return False
        elif self.config.top_cut == TournamentConfiguration.TopCut.TOP_13:
            if self.last_round.stage == Round.Stage.SWISS:
                stage = Round.Stage.TOP_13
                logic = self.get_pairing_logic("PairingTop13")
            elif self.last_round.stage == Round.Stage.TOP_13:
                stage = Round.Stage.TOP_4
                logic = self.get_pairing_logic("PairingTop4")
            else:
                Log.log("Tournament completed.")
                return False
        elif self.config.top_cut == TournamentConfiguration.TopCut.TOP_16:
            if self.last_round.stage == Round.Stage.SWISS:
                stage = Round.Stage.TOP_16
                logic = self.get_pairing_logic("PairingTop16")
            elif self.last_round.stage == Round.Stage.TOP_16:
                stage = Round.Stage.TOP_4
                logic = self.get_pairing_logic("PairingTop4")
            else:
                Log.log("Tournament completed.")
                return False
        elif self.config.top_cut == TournamentConfiguration.TopCut.TOP_40:
            if self.last_round.stage == Round.Stage.SWISS:
                stage = Round.Stage.TOP_40
                logic = self.get_pairing_logic("PairingTop40")
            elif self.last_round.stage == Round.Stage.TOP_40:
                stage = Round.Stage.TOP_16
                logic = self.get_pairing_logic("PairingTop16")
            elif self.last_round.stage == Round.Stage.TOP_16:
                stage = Round.Stage.TOP_4
                logic = self.get_pairing_logic("PairingTop4")
            else:
                Log.log("Tournament completed.")
                return False
        else:
            raise ValueError(f"Unknown top cut: {self.config.top_cut}")
    else:
        if seq == 0:
            logic = self.get_pairing_logic("PairingRandom")
        elif seq == 1 and self.config.snake_pods:
            logic = self.get_pairing_logic("PairingSnake")
        else:
            logic = self.get_pairing_logic("PairingDefault")

    if not logic:
        Log.log("No pairing logic found.", level=Log.Level.ERROR)
        return False
    elif not stage:
        Log.log("No stage found.", level=Log.Level.ERROR)
        return False
    new_round = Round(
        self,
        len(self.rounds),
        stage,
        logic,
        dropped=self.tour_round._dropped if self._round else set(),
        disabled=self.tour_round._disabled if self._round else set(),
    )
    self._rounds.append(new_round.uid)
    self.tour_round = new_round
    return True

__validate_config

__validate_config(config: TournamentConfiguration) -> bool

Validates the tournament configuration.

Parameters:

Name Type Description Default
config TournamentConfiguration

The tournament configuration.

required

Returns:

Name Type Description
bool bool
  • True if the configuration is valid.
  • False if the configuration is invalid.
Source code in src/core.py
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
def __validate_config(self, config: TournamentConfiguration) -> bool:
    """
    Validates the tournament configuration.

    Args:
        config: The tournament configuration.

    Returns:
        bool:
            - True if the configuration is valid.
            - False if the configuration is invalid.
    """
    if len(self.rounds) >= config.n_rounds:
        raise ValueError(
            "Tournament has already reached the maximum number of rounds."
        )
    return True

add_player

add_player(*specs: Any, **player_attrs) -> list[Player]

Adds players to the tournament.

This method supports flexible input formats for defining players.

Parameters:

Name Type Description Default
*specs Any

Variable length argument list. Each argument can be: - A Player object. - A tuple/list of (name,), (name, uid/decklist), or (name, uid, decklist). - A dictionary containing 'name', and optionally 'uid' and 'decklist'. - A string representing the player's name.

()
**player_attrs

arbitrary keyword arguments to be applied to all new players (e.g., decklist="link", uid=UUID(...)). If only one positional argument is provided and it's a string or dict, these attributes are merged with it.

{}

Returns:

Type Description
list[Player]

list[Player]: A list of the newly created and added Player objects.

Raises:

Type Description
ValueError

If the player specification is invalid or incomplete.

Source code in src/core.py
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
@TournamentAction.action
def add_player(self, *specs: Any, **player_attrs) -> list[Player]:
    """Adds players to the tournament.

    This method supports flexible input formats for defining players.

    Args:
        *specs: Variable length argument list. Each argument can be:
            - A Player object.
            - A tuple/list of (name,), (name, uid/decklist), or (name, uid, decklist).
            - A dictionary containing 'name', and optionally 'uid' and 'decklist'.
            - A string representing the player's name.
        **player_attrs: arbitrary keyword arguments to be applied to all new players
                        (e.g., decklist="link", uid=UUID(...)).
                        If only one positional argument is provided and it's a string or dict,
                        these attributes are merged with it.

    Returns:
        list[Player]: A list of the newly created and added Player objects.

    Raises:
        ValueError: If the player specification is invalid or incomplete.
    """

    # Handle keyword arguments merging with a single positional spec
    if player_attrs and len(specs) == 1 and "name" not in player_attrs:
        spec = specs[0]
        if isinstance(spec, str):
            data = [{"name": spec, **player_attrs}]
        elif isinstance(spec, dict):
            data = [{**spec, **player_attrs}]
        else:
            data = list(specs) + [player_attrs]
    else:
        data = list(specs)
        if player_attrs:
            data.append(player_attrs)

    # Handle backward compatibility: single positional list
    if len(data) == 1 and isinstance(data[0], list):
        data = data[0]

    new_players = []
    existing_names = set([p.name for p in self.players])
    existing_uids = set([p.uid for p in self.players])

    for entry in data:
        if entry is None:
            continue

        name, uid, decklist = None, None, None

        if isinstance(entry, (tuple, list)):
            # Handle 1-tuple (name), 2-tuple (smart: name, uid/decklist), or 3-tuple (name, uid, decklist)
            if len(entry) == 1:
                name = entry[0]
            elif len(entry) == 2:
                name, second = entry
                if isinstance(second, UUID):
                    uid = second
                elif isinstance(second, (str, type(None))):
                    decklist = second
                else:
                    raise ValueError(
                        f"Unknown type for second element in player tuple: {type(second)}"
                    )
            elif len(entry) == 3:
                name, uid, decklist = entry
            else:
                raise ValueError(
                    f"Player tuple/list must have 1-3 elements, got {len(entry)}"
                )
        elif isinstance(entry, dict):
            # Handle dictionary specification
            name = entry.get("name")
            uid = entry.get("uid")
            decklist = entry.get("decklist")
        elif isinstance(entry, str):
            # Handle single string as name
            name = entry
        else:
            raise ValueError(
                f"Invalid player specification type: {type(entry)}. Expected str, dict, tuple, or list."
            )

        if not name or not isinstance(name, str):
            raise ValueError(
                f"Player name must be a non-empty string, got {type(name)}: {name}"
            )

        if name in existing_names:
            Log.log(
                "\tPlayer {} already enlisted.".format(name),
                level=Log.Level.WARNING,
            )
            continue
        if uid and uid in existing_uids:
            Log.log(
                "\tPlayer with UID {} already enlisted.".format(uid),
                level=Log.Level.WARNING,
            )
            continue

        # Create and register the player
        p = Player(self, name, uid, decklist)
        self._players.add(p.uid)
        if self._round and p.uid not in self.tour_round._players:
            self.tour_round._players.append(p.uid)
        new_players.append(p)
        existing_names.add(name)
        existing_uids.add(p.uid)
        Log.log("\tAdded player {}".format(p.name), level=Log.Level.INFO)
    return new_players

bench_players

bench_players(players: Iterable[Player] | Player)

Removes player(s) from their current pod, effectively benching them.

Parameters:

Name Type Description Default
players Iterable[Player] | Player

The player or iterable of players to bench.

required
Source code in src/core.py
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
@TournamentAction.action
def bench_players(self, players: Iterable[Player] | Player):
    """Removes player(s) from their current pod, effectively benching them.

    Args:
        players: The player or iterable of players to bench.
    """
    assert self.tour_round is not None
    if not isinstance(players, Iterable):
        players = [players]
    for player in players:
        self.remove_player_from_pod(player)

create_pairings

create_pairings() -> bool

Creates pairings for the current round.

If the round has not been initialized or previous rounds are not complete, this method attempts to handle those states.

Returns:

Name Type Description
bool bool

True if pairings were successfully created (or were already created). False if pairings could not be created (e.g., due to initialization failure).

Source code in src/core.py
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
@TournamentAction.action
def create_pairings(self) -> bool:
    """Creates pairings for the current round.

    If the round has not been initialized or previous rounds are not complete, this method attempts
    to handle those states.

    Returns:
        bool: True if pairings were successfully created (or were already created). False if
              pairings could not be created (e.g., due to initialization failure).
    """
    if self.last_round is None or self.last_round.done:
        ok = self.__initialize_round()
        if not ok:
            return False
    # self.last_round._byes.clear()
    assert self.last_round is not None
    if not self.last_round.all_players_assigned:
        self.last_round.create_pairings()
        return True
    return False

delete_pod

delete_pod(pod: Pod)

Deletes a specified pod from the current round.

Parameters:

Name Type Description Default
pod Pod

The pod to delete.

required
Source code in src/core.py
1656
1657
1658
1659
1660
1661
1662
1663
1664
@TournamentAction.action
def delete_pod(self, pod: Pod):
    """Deletes a specified pod from the current round.

    Args:
        pod: The pod to delete.
    """
    if self.tour_round:
        self.tour_round.remove_pod(pod)

delete_round

delete_round(tour_round: Round) -> bool

Deletes a round from the tournament if it's not completed.

This method removes the round from the tournament and clears all its pods and bye list.

Parameters:

Name Type Description Default
tour_round Round

The round to delete.

required

Returns:

Type Description
bool

True if the round was successfully deleted, False otherwise.

Source code in src/core.py
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
@TournamentAction.action
def delete_round(self, tour_round: Round) -> bool:
    """Deletes a round from the tournament if it's not completed.

    This method removes the round from the tournament and clears all its pods and bye list.

    Args:
        tour_round: The round to delete.

    Returns:
        True if the round was successfully deleted, False otherwise.
    """
    if tour_round.done:
        return False

    if self.last_round != tour_round:
        return False

    if tour_round.uid not in self._rounds:
        return False

    ok = tour_round.reset_pods()
    if not ok:
        return False

    if tour_round.uid in self.ROUND_CACHE:
        del self.ROUND_CACHE[tour_round.uid]

    self._rounds.remove(tour_round.uid)

    if self._round == tour_round.uid:
        self._round = self._rounds[-1] if self._rounds else None

    return True

disable_player

disable_player(players: list[Player] | Player, set_disabled: bool = True) -> bool

Disables or enables players for top cut participation.

Disabled players remain in the tournament structure but are excluded from top cut calculations and pairings.

Parameters:

Name Type Description Default
players list[Player] | Player

The player or list of players to disable/enable.

required
set_disabled bool

If True, disables the player. If False, re-enables them.

True

Returns:

Name Type Description
bool bool

Always returns True.

Source code in src/core.py
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
@TournamentAction.action
def disable_player(
    self, players: list[Player] | Player, set_disabled: bool = True
) -> bool:
    """Disables or enables players for top cut participation.

    Disabled players remain in the tournament structure but are excluded from top cut calculations and pairings.

    Args:
        players: The player or list of players to disable/enable.
        set_disabled: If True, disables the player. If False, re-enables them.

    Returns:
        bool: Always returns True.
    """
    if not isinstance(players, list):
        players = [players]
    for p in players:
        self.tour_round.disable_player(p, set_disabled=set_disabled)
    return True

discover_pairing_logic classmethod

discover_pairing_logic() -> None

Discover and cache all pairing logic implementations from src/pairing_logic.

Source code in src/core.py
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
@classmethod
def discover_pairing_logic(cls) -> None:
    """Discover and cache all pairing logic implementations from src/pairing_logic."""
    if cls._pairing_logic_cache:
        return

    # Get the base directory of the project
    base_dir = Path(__file__).parent.parent
    pairing_logic_dir = base_dir / "src" / "pairing_logic"

    # Walk through all Python files in the pairing_logic directory
    for module_info in pkgutil.iter_modules([str(pairing_logic_dir)]):
        try:
            # Import the module
            module = importlib.import_module(
                f"src.pairing_logic.{module_info.name}"
            )

            # Find all classes that implement IPairingLogic
            for name, obj in module.__dict__.items():
                if (
                    isinstance(obj, type)
                    and issubclass(obj, IPairingLogic)
                    and obj != IPairingLogic
                    and obj.IS_COMPLETE
                ):
                    if obj.__name__ in cls._pairing_logic_cache:
                        raise ValueError(
                            f"Pairing logic {obj.__name__} already exists"
                        )
                    cls._pairing_logic_cache[obj.__name__] = obj(
                        name=f"{obj.__name__}"
                    )
        except Exception as e:
            Log.log(
                f"Failed to import pairing logic module {module_info.name}: {e}",
                level=Log.Level.WARNING,
            )

drop_player

drop_player(players: list[Player] | Player) -> bool

Drops a player or list of players from the tournament.

Dropped players are removed from future pairings but their history remains. If a player is dropped during an active round, they might need to be resolved in the current pod first.

Parameters:

Name Type Description Default
players list[Player] | Player

The player or list of players to drop.

required

Returns:

Name Type Description
bool bool

True if the drop was successful, False otherwise.

Source code in src/core.py
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
@TournamentAction.action
def drop_player(self, players: list[Player] | Player) -> bool:
    """Drops a player or list of players from the tournament.

    Dropped players are removed from future pairings but their history remains.
    If a player is dropped during an active round, they might need to be resolved in the current pod first.

    Args:
        players: The player or list of players to drop.

    Returns:
        bool: True if the drop was successful, False otherwise.
    """
    if not isinstance(players, list):
        players = [players]
    for p in players:
        if self.tour_round and p.seated(self.tour_round):
            if self.tour_round.done and self.tour_round != self.last_round:
                # Log.log('Can\'t drop {} during an active tour_round.\nComplete the tour_round or remove player from pod first.'.format(
                #    p.name), level=Log.Level.WARNING)
                return False

        # If player has not played yet, it can safely be deleted without being saved
        if p.played(self.tour_round):
            self.tour_round.drop_player(p)
        else:
            self._players.remove(p.uid)
            self.tour_round._players.remove(p.uid)
        # Remove from disabled set if they were disabled
        self.tour_round.disable_player(p, set_disabled=False)
    return True

export_str

export_str(data: str, var_export_param: Any, target_type: Target) -> None

Exports a string of data to a specified target (file, web, console).

Parameters:

Name Type Description Default
data str

The string data to export.

required
var_export_param Any

Parameter specific to the target type (e.g., file path, log level).

required
target_type Target

The target for the export (FILE, WEB, CONSOLE).

required
Source code in src/core.py
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
def export_str(
    self,
    data: str,
    var_export_param: Any,  # pyright: ignore[reportExplicitAny, reportAny]
    target_type: StandingsExport.Target,
) -> None:
    """Exports a string of data to a specified target (file, web, console).

    Args:
        data: The string data to export.
        var_export_param: Parameter specific to the target type (e.g., file path, log level).
        target_type: The target for the export (FILE, WEB, CONSOLE).
    """
    if StandingsExport.Target.FILE == target_type:
        if not os.path.exists(os.path.dirname(var_export_param)):
            os.makedirs(os.path.dirname(var_export_param))
        with open(var_export_param, "w", encoding="utf-8") as f:
            f.writelines(data)

    if StandingsExport.Target.WEB == target_type:
        api = os.getenv("EXPORT_ONLINE_API_URL")
        key = os.getenv("EXPORT_ONLINE_API_KEY")
        if not key or not api:
            Log.log(
                "Error: EXPORT_ONLINE_API_URL or EXPORT_ONLINE_API_KEY not set in the environment variables."
            )
            return
        tournament_id = os.getenv("TOURNAMENT_ID")
        url = f"{api}?tournamentId={tournament_id}"

        # Send as POST request to the Express app with authentication
        headers = {"x-api-key": key}
        request_data = {
            "title": "Tournament Update",
            "timestamp": f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
            "text": data,
            "tournament": self.serialize(),
        }

        thread = threading.Thread(
            target=self.send_request, args=(url, request_data, headers)
        )
        thread.start()

    if StandingsExport.Target.CONSOLE == target_type:
        if not isinstance(var_export_param, Log.Level):
            var_export_param = Log.Level.INFO
        Log.log(data, level=var_export_param)

get classmethod

get(uid: UUID) -> Tournament

Retrieves a tournament by its UUID.

Parameters:

Name Type Description Default
uid UUID

The tournament UUID.

required

Returns:

Type Description
Tournament

The tournament instance.

Source code in src/core.py
850
851
852
853
854
855
856
857
858
859
860
@classmethod
def get(cls, uid: UUID) -> Tournament:
    """Retrieves a tournament by its UUID.

    Args:
        uid: The tournament UUID.

    Returns:
        The tournament instance.
    """
    return cls.CACHE[uid]

get_pairing_logic classmethod

get_pairing_logic(logic_name: str) -> IPairingLogic

Get a pairing logic instance by name.

Parameters:

Name Type Description Default
logic_name str

The name of the pairing logic.

required

Returns:

Type Description
IPairingLogic

The pairing logic class.

Source code in src/core.py
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
@classmethod
def get_pairing_logic(cls, logic_name: str) -> IPairingLogic:
    """Get a pairing logic instance by name.

    Args:
        logic_name: The name of the pairing logic.

    Returns:
        The pairing logic class.
    """
    cls.discover_pairing_logic()

    if logic_name not in cls._pairing_logic_cache:
        raise ValueError(f"Unknown pairing logic: {logic_name}")

    return cls._pairing_logic_cache[logic_name]

get_pod_sizes

get_pod_sizes(n) -> list[int] | None

Determines possible pod sizes for a given number of players based on configuration.

Parameters:

Name Type Description Default
n

The number of players.

required

Returns:

Type Description
list[int] | None

A list of integers representing the sizes of the pods, or None if no valid combination is found.

Source code in src/core.py
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
def get_pod_sizes(self, n) -> list[int] | None:
    """Determines possible pod sizes for a given number of players based on configuration.

    Args:
        n: The number of players.

    Returns:
        A list of integers representing the sizes of the pods, or None if no valid combination is found.
    """
    # Stack to store (remaining_players, current_pod_size_index, current_solution)
    stack = [(n, 0, [])]

    while stack:
        remaining, pod_size_idx, current_solution = stack.pop()

        # If we've processed all pod sizes, continue to next iteration
        if pod_size_idx >= len(self.config.pod_sizes):
            continue

        pod_size = self.config.pod_sizes[pod_size_idx]
        rem = remaining - pod_size

        # Skip if this pod size would exceed remaining players
        if rem < 0:
            stack.append((remaining, pod_size_idx + 1, current_solution))
            continue

        # If this pod size exactly matches remaining players, we found a solution
        if rem == 0:
            return current_solution + [pod_size]

        # Handle case where remaining players is less than minimum pod size
        if rem < self.config.min_pod_size:
            if self.config.allow_bye and rem <= self.config.max_byes:
                return current_solution + [pod_size]
            elif pod_size == self.config.pod_sizes[-1]:
                continue
            else:
                stack.append((remaining, pod_size_idx + 1, current_solution))
                continue

        # If remaining players is valid, try this pod size and continue with remaining players
        if rem >= self.config.min_pod_size:
            stack.append((remaining, pod_size_idx + 1, current_solution))
            stack.append((rem, 0, current_solution + [pod_size]))

    return None

get_pods_str

get_pods_str() -> str

Generates a string representation of the current round's pods.

Returns:

Type Description
str

A formatted string showing the pods and players, including byes if applicable.

Source code in src/core.py
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
def get_pods_str(self) -> str:
    """Generates a string representation of the current round's pods.

    Returns:
        A formatted string showing the pods and players, including byes if applicable.
    """
    if not self.tour_round:
        return ""
    standings = self.get_standings(self.tour_round)
    export_str = "\n\n".join(
        [
            pod.__repr__(TournamentContext(self, self.tour_round, standings))
            for pod in self.tour_round.pods
        ]
    )

    if self.config.allow_bye and self.tour_round.unassigned:
        export_str += "\n\nByes:\n" + "\n:".join(
            [
                "\t{}\t| pts: {}".format(p.name, p.rating(self.tour_round) or "0")
                for p in self.tour_round.unassigned
            ]
        )
    return export_str

get_standings

get_standings(tour_round: Round | None = None) -> list[Player]

Calculates and retrieves the standings for a specific round.

Use this instead of accessing the players list directly, as this method ensures players are sorted according to the tournament's ranking configuration.

Parameters:

Name Type Description Default
tour_round Round | None

The round for which to calculate standings. If None, uses the current round.

None

Returns:

Type Description
list[Player]

list[Player]: A list of players sorted by their current standing.

Source code in src/core.py
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
@override
def get_standings(self, tour_round: Round | None = None) -> list[Player]:
    """Calculates and retrieves the standings for a specific round.

    Use this instead of accessing the players list directly, as this method ensures
    players are sorted according to the tournament's ranking configuration.

    Args:
        tour_round: The round for which to calculate standings.
                    If None, uses the current round.

    Returns:
        list[Player]: A list of players sorted by their current standing.
    """
    method = Player.SORT_METHOD
    order = Player.SORT_ORDER
    Player.SORT_METHOD = SortMethod.RANK
    Player.SORT_ORDER = SortOrder.ASCENDING
    playoffs = False
    if tour_round is None:
        tour_round = self.tour_round
    if tour_round.stage == Round.Stage.SWISS:
        standings = sorted(
            self.players,
            key=lambda x: self.config.ranking(x, tour_round),
            reverse=True,
        )
    else:
        final_swiss = self.final_swiss_round
        assert final_swiss is not None
        playoff_stage = tour_round.seq - final_swiss.seq - 1

        if playoff_stage > 0:
            # TODO: take the standings of previous playoff round and modify them to current results
            previous_round = self.previous_round(tour_round)
            assert previous_round is not None
            standings = self.get_standings(previous_round)
            advancing_players = tour_round.advancing_players(standings)
            non_advancing = [p for p in standings if p not in advancing_players]
            standings = advancing_players + non_advancing
            pass
        else:
            swiss_standings = self.get_standings(final_swiss)
            advancing_players = tour_round.advancing_players(swiss_standings)
            non_advancing = [
                p for p in swiss_standings if p not in advancing_players
            ]

            # Sort non-advancing players: draws rank above losses, then by original standings
            standings_index = {
                player: idx for idx, player in enumerate(swiss_standings)
            }
            non_advancing.sort(
                key=lambda x: (
                    0
                    if tour_round and x.result(tour_round) == Player.EResult.DRAW
                    else 1,  # Draws first (0), losses second (1)
                    standings_index.get(
                        x, len(swiss_standings)
                    ),  # Then by original standings position
                )
            )

            standings = advancing_players + non_advancing
            pass

        # TODO: Implement playoff standings
        pass

    Player.SORT_METHOD = method
    Player.SORT_ORDER = order
    return standings

get_standings_str

get_standings_str(fields: list[Field] = StandingsExport.DEFAULT_FIELDS, style: Format = StandingsExport.Format.PLAIN, tour_round: Round | None = None, standings: list[Player] | None = None) -> str

Generates a formatted string of the tournament standings.

Parameters:

Name Type Description Default
fields list[Field]

A list of StandingsExport.Field to include in the standings.

DEFAULT_FIELDS
style Format

The desired output format (e.g., PLAIN, CSV, JSON).

PLAIN
tour_round Round | None

The round for which to generate standings. Defaults to the current round.

None
standings list[Player] | None

Pre-calculated standings. If None, standings will be calculated.

None

Returns:

Type Description
str

A string containing the formatted standings.

Raises:

Type Description
ValueError

If an invalid style is provided.

Source code in src/core.py
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
def get_standings_str(
    self,
    fields: list[StandingsExport.Field] = StandingsExport.DEFAULT_FIELDS,
    style: StandingsExport.Format = StandingsExport.Format.PLAIN,
    tour_round: Round | None = None,
    standings: list[Player] | None = None,
) -> str:
    """Generates a formatted string of the tournament standings.

    Args:
        fields: A list of StandingsExport.Field to include in the standings.
        style: The desired output format (e.g., PLAIN, CSV, JSON).
        tour_round: The round for which to generate standings. Defaults to the current round.
        standings: Pre-calculated standings. If None, standings will be calculated.

    Returns:
        A string containing the formatted standings.

    Raises:
        ValueError: If an invalid style is provided.
    """
    # raise DeprecationWarning("get_standings_str is deprecated. Use get_standings instead.")
    if tour_round is None:
        tour_round = self.tour_round
    if standings is None:
        standings = self.get_standings(tour_round)

    # Create context with all available data
    context = TournamentContext(
        tour=self,
        tour_round=tour_round,
        standings=standings,
    )

    lines = [[StandingsExport.info[f].name for f in fields]]
    lines += [
        [
            (StandingsExport.info[f].format).format(
                StandingsExport.info[f].get(p, context)  # pyright: ignore[reportAny]
                if StandingsExport.info[f].denom is None
                else StandingsExport.info[f].get(p, context)
                * StandingsExport.info[f].denom
            )
            for f in fields
        ]
        for p in standings
    ]
    if style == StandingsExport.Format.PLAIN:
        col_len = [0] * len(fields)
        for col in range(len(fields)):
            for line in lines:
                if len(line[col]) > col_len[col]:
                    col_len[col] = len(line[col])
        for line in lines:
            for col in range(len(fields)):
                line[col] = line[col].ljust(col_len[col])
        # add new line at index 1
        lines.insert(1, ["-" * width for width in col_len])
        lines = "\n".join([" | ".join(line) for line in lines])
        return lines

        # Log.log('Log saved: {}.'.format(
        #    fdir), level=Log.Level.INFO)
    elif style == StandingsExport.Format.CSV:
        Log.log(
            "Log not saved - CSV not implemented.",
            level=Log.Level.WARNING,
        )
    elif style == StandingsExport.Format.JSON:
        Log.log(
            "Log not saved - JSON not implemented.",
            level=Log.Level.WARNING,
        )

    raise ValueError("Invalid style: {}".format(style))

inflate classmethod

inflate(data: dict[str, Any]) -> ITournament

Creates a Tournament instance from serialized data.

This method reconstructs the entire tournament state, including players, rounds, and pods, linking them back together using their UUIDs.

Parameters:

Name Type Description Default
data dict[str, Any]

A dictionary containing the serialized tournament data (as produced by serialize).

required

Returns:

Name Type Description
Tournament ITournament

The reconstructed Tournament instance.

Source code in src/core.py
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
@classmethod
def inflate(cls, data: dict[str, Any]) -> ITournament:
    """Creates a Tournament instance from serialized data.

    This method reconstructs the entire tournament state, including players, rounds, and pods,
    linking them back together using their UUIDs.

    Args:
        data: A dictionary containing the serialized tournament data (as produced by `serialize`).

    Returns:
        Tournament: The reconstructed Tournament instance.
    """
    config = TournamentConfiguration.inflate(data["config"])
    tour_uid = UUID(data["uid"])
    if tour_uid in Tournament.CACHE:
        tour = Tournament.CACHE[tour_uid]
    else:
        tour = cls(config, tour_uid)
    tour._players = {UUID(d_player["uid"]) for d_player in data["players"]}
    # tour._dropped = [UUID(d_player['uid']) for d_player in data['dropped']]
    # tour._disabled = [UUID(d_player['uid']) for d_player in data['disabled']]
    # tour._players.extend(tour._dropped)
    # tour._players.extend(tour._disabled)
    # Load disabled players (backward compatible: may not exist in old saves)
    # if 'disabled' in data:
    #    tour._disabled = {UUID(uid) for uid in data['disabled']}
    for d_player in data["players"]:
        Player.inflate(tour, d_player)
    # for d_player in data['dropped']:
    #    Player.inflate(tour, d_player)
    tour._rounds = [UUID(d_round["uid"]) for d_round in data["rounds"]]
    for _, d_round in tqdm(enumerate(data["rounds"]), desc="Inflating rounds"):
        r = Round.inflate(tour, d_round)
        tour._round = r.uid
    return tour

manual_pod

manual_pod(players: list[Player])

Creates a manual pod with the specified players.

Parameters:

Name Type Description Default
players list[Player]

A list of players to include in the manual pod.

required
Source code in src/core.py
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
@TournamentAction.action
def manual_pod(self, players: list[Player]):
    """Creates a manual pod with the specified players.

    Args:
        players: A list of players to include in the manual pod.
    """
    if self.tour_round is None or self.tour_round.done:
        if not self.new_round():
            return
    assert isinstance(self.tour_round, Round)
    cap = min(self.config.max_pod_size, len(self.tour_round.unassigned))
    pod = Pod(self.tour_round, len(self.tour_round.pods), cap=cap)
    self.tour_round._pods.append(pod.uid)

    for player in players:
        pod.add_player(player)
    self.tour_round.pods.append(pod)

move_player_to_pod

move_player_to_pod(pod: Pod, players: list[Player] | Player, manual=False)

Moves a player or list of players to a specified pod.

Parameters:

Name Type Description Default
pod Pod

The target pod.

required
players list[Player] | Player

The player or list of players to move.

required
manual

If True, allows adding players even if the pod is full.

False
Source code in src/core.py
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
@TournamentAction.action
def move_player_to_pod(
    self, pod: Pod, players: list[Player] | Player, manual=False
):
    """Moves a player or list of players to a specified pod.

    Args:
        pod: The target pod.
        players: The player or list of players to move.
        manual: If True, allows adding players even if the pod is full.
    """
    if not isinstance(players, list):
        players = [players]
    for player in players:
        if player.pod(self.tour_round) == pod:
            continue
            # player.pod(self.tour_round).remove_player(player)
            # Log.log('Removed player {} from {}.'.format(
            #    player.name, old_pod), level=Log.Level.INFO)
        if ok := pod.add_player(player, manual=manual):
            pass

new_round

new_round() -> bool

Starts a new round.

Returns:

Type Description
bool

True if a new round was successfully started, False otherwise.

Source code in src/core.py
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
@TournamentAction.action
def new_round(self) -> bool:
    """Starts a new round.

    Returns:
        True if a new round was successfully started, False otherwise.
    """
    if not self.last_round or self.last_round.done:
        return self.__initialize_round()
    return False

previous_round

previous_round(tour_round: Round | None = None) -> Round | None

Returns the previous round of the tournament.

Parameters:

Name Type Description Default
tour_round Round | None

The current round.

None

Returns:

Type Description
Round | None

Round|None: - The previous round if it exists. - None if the current round is the first round.

Source code in src/core.py
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
def previous_round(self, tour_round: Round | None = None) -> Round | None:
    """
    Returns the previous round of the tournament.

    Args:
        tour_round: The current round.

    Returns:
        Round|None:
            - The previous round if it exists.
            - None if the current round is the first round.
    """
    if tour_round is None:
        tour_round = self.tour_round
    return (
        self.rounds[self.rounds.index(tour_round) - 1]
        if self.rounds.index(tour_round) > 0
        else None
    )

random_results

random_results()

Generates random results for all incomplete pods in the current round.

Source code in src/core.py
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
@TournamentAction.action
def random_results(self):
    """Generates random results for all incomplete pods in the current round."""
    if not self.tour_round:
        # Log.log(
        #    'A tour_round is not in progress.\nCreate pods first!',
        #    level=Log.Level.ERROR
        # )
        return
    if self.tour_round.pods:
        draw_rate = 1 - sum(self.config.global_wr_seats)
        # for each pod
        # generate a random result based on global_winrates_by_seat
        # each value corresponds to the pointrate of the player in that seat
        # the sum of percentages is less than 1, so there is a chance of a draw (1-sum(winrates))

        for pod in [x for x in self.tour_round.pods if not x.done]:
            # generate a random result
            result = random.random()
            rates = np.array(
                self.config.global_wr_seats[0 : len(pod.players)] + [draw_rate]
            )
            rates = np.cumsum(rates / sum(rates))
            draw = result > rates[-2]
            if not draw:
                win = np.argmax([result < x for x in rates])
                # Log.log('won "{}"'.format(pod.players[win].name))
                self.tour_round.set_result(pod.players[win], Player.EResult.WIN)
                # player = random.sample(pod.players, 1)[0]
                # Log.log('won "{}"'.format(player.name))
                # self.tour_round.won([player])
            else:
                players = pod.players
                # Log.log('draw {}'.format(
                #    ' '.join(['"{}"'.format(p.name) for p in players])))
                for p in players:
                    self.tour_round.set_result(p, Player.EResult.DRAW)
            pass
    pass

rating

rating(player: Player, tour_round: Round) -> float

Calculate the rating of a player for a given round. The rating is the sum of the points for the player in the Swiss rounds up to and including the given round. If the round is not a Swiss round, the rating is the sum of the points for the player in the last Swiss round.

Parameters:

Name Type Description Default
player Player

The player for whom to calculate the rating.

required
tour_round Round

The round up to which to calculate the rating.

required

Returns:

Type Description
float

The player's rating as a float.

Source code in src/core.py
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
def rating(self, player: Player, tour_round: Round) -> float:
    """
    Calculate the rating of a player for a given round.
    The rating is the sum of the points for the player in the Swiss rounds up to and including the given round.
    If the round is not a Swiss round, the rating is the sum of the points for the player in the last Swiss round.

    Args:
        player: The player for whom to calculate the rating.
        tour_round: The round up to which to calculate the rating.

    Returns:
        The player's rating as a float.
    """
    points = 0
    for i, i_tour_round in enumerate(self.rounds):
        if i_tour_round.stage != Round.Stage.SWISS:
            break
        round_result = player.result(i_tour_round)
        if round_result == Player.EResult.WIN:
            points += self.config.win_points
        elif round_result == Player.EResult.DRAW:
            points += self.config.draw_points
        elif round_result == Player.EResult.BYE:
            points += self.config.bye_points
        if i_tour_round == tour_round:
            break
    return points

remove_player_from_pod

remove_player_from_pod(player: Player)

Removes a player from their current pod.

Parameters:

Name Type Description Default
player Player

The player to remove.

required
Source code in src/core.py
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
def remove_player_from_pod(self, player: Player):
    """Removes a player from their current pod.

    Args:
        player: The player to remove.
    """
    assert self.tour_round is not None
    pod = player.pod(self.tour_round)
    if pod:
        pod.remove_player(player)

rename_player

rename_player(player, new_name)

Renames a player in the tournament.

This updates the player's name across all historical records in the tournament (pods, rounds).

Parameters:

Name Type Description Default
player

The player object to rename.

required
new_name

The new name for the player.

required
Source code in src/core.py
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
@TournamentAction.action
def rename_player(self, player, new_name):
    """Renames a player in the tournament.

    This updates the player's name across all historical records in the tournament (pods, rounds).

    Args:
        player: The player object to rename.
        new_name: The new name for the player.
    """
    if player.name == new_name:
        return
    if new_name in [p.name for p in self.active_players]:
        Log.log(
            "\tPlayer {} already enlisted.".format(new_name),
            level=Log.Level.WARNING,
        )
        return
    if new_name:
        player.name = new_name
        for tour_round in self.rounds:
            for pod in tour_round.pods:
                for p in pod.players:
                    if p.name == player.name:
                        p.name = new_name
        Log.log(
            "\tRenamed player {} to {}".format(player.name, new_name),
            level=Log.Level.INFO,
        )

report_draw

report_draw(players: list[Player] | Player)

Reports a draw for the specified player(s) in the current round.

Parameters:

Name Type Description Default
players list[Player] | Player

The player or list of players who drew.

required
Source code in src/core.py
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
@TournamentAction.action
def report_draw(self, players: list[Player] | Player):
    """Reports a draw for the specified player(s) in the current round.

    Args:
        players: The player or list of players who drew.
    """
    if self.tour_round:
        if not isinstance(players, list):
            players = [players]
        for p in players:
            self.tour_round.set_result(p, Player.EResult.DRAW)

report_win

report_win(players: list[Player] | Player)

Reports a win for the specified player(s) in the current round.

Parameters:

Name Type Description Default
players list[Player] | Player

The player or list of players who won.

required
Source code in src/core.py
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
@TournamentAction.action
def report_win(self, players: list[Player] | Player):
    """Reports a win for the specified player(s) in the current round.

    Args:
        players: The player or list of players who won.
    """
    if self.tour_round:
        if not isinstance(players, list):
            players = [players]
        for p in players:
            self.tour_round.set_result(p, Player.EResult.WIN)

reset_pods

reset_pods() -> bool

Resets the pods for the current round.

Returns:

Type Description
bool

True if pods were reset, False otherwise.

Source code in src/core.py
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
@TournamentAction.action
def reset_pods(self) -> bool:
    """Resets the pods for the current round.

    Returns:
        True if pods were reset, False otherwise.
    """
    if not self.tour_round:
        return False
    if not self.tour_round.done:
        return self.tour_round.reset_pods()
    return False

send_request staticmethod

send_request(api: str, data: dict[str, Any], headers: dict[str, str]) -> None

Sends a POST request to a specified API endpoint.

Parameters:

Name Type Description Default
api str

The API endpoint URL.

required
data dict[str, Any]

The JSON data to send.

required
headers dict[str, str]

A dictionary of HTTP headers.

required
Source code in src/core.py
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
@staticmethod
def send_request(api: str, data: dict[str, Any], headers: dict[str, str]) -> None:  # pyright: ignore[reportExplicitAny]
    """Sends a POST request to a specified API endpoint.

    Args:
        api: The API endpoint URL.
        data: The JSON data to send.
        headers: A dictionary of HTTP headers.
    """
    try:
        response = requests.post(api, json=data, headers=headers, timeout=10)
        if response.status_code == 200:
            Log.log("Data successfully sent to the server!")
        else:
            Log.log(f"Failed to send data. Status code: {response.status_code}")
    except Exception as e:
        Log.log(f"Error sending data: {e}", level=Log.Level.ERROR)

serialize

serialize() -> dict[str, Any]

Serializes the tournament state to a JSON-compatible dictionary.

The serialization includes: - specific tournament configuration - list of players (including their state) - list of pods (including their state) - list of rounds (including pairings and results)

All objects are cross-referenced by their unique IDs to maintain integrity upon restoration.

Returns:

Name Type Description
dict dict[str, Any]

A dictionary representing the serialized tournament data.

Source code in src/core.py
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
def serialize(self) -> dict[str, Any]:
    """Serializes the tournament state to a JSON-compatible dictionary.

    The serialization includes:
    - specific tournament configuration
    - list of players (including their state)
    - list of pods (including their state)
    - list of rounds (including pairings and results)

    All objects are cross-referenced by their unique IDs to maintain integrity upon restoration.

    Returns:
        dict: A dictionary representing the serialized tournament data.
    """

    data: dict[str, Any] = {}
    data["uid"] = str(self.uid)
    data["config"] = self.config.serialize()
    data["players"] = list(p.serialize() for p in self.players)
    # data['dropped'] = [p.serialize() for p in self.dropped_players]
    # data['disabled'] = [str(p.uid) for p in self.disabled_players]
    data["rounds"] = [r.serialize() for r in self.rounds]
    return data

toggle_bye

toggle_bye(players: Iterable[Player] | Player)

Toggles the bye status for player(s).

If a player is assigned a bye, they are removed from their pod and marked as having a bye. If they already have a bye, it is removed.

Parameters:

Name Type Description Default
players Iterable[Player] | Player

The player or iterable of players to toggle bye for.

required
Source code in src/core.py
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
@TournamentAction.action
def toggle_bye(self, players: Iterable[Player] | Player):
    """Toggles the bye status for player(s).

    If a player is assigned a bye, they are removed from their pod and marked as having a bye.
    If they already have a bye, it is removed.

    Args:
        players: The player or iterable of players to toggle bye for.
    """
    if not isinstance(players, Iterable):
        players = [players]
    for player in players:
        if player.uid in self.tour_round._byes:
            self.tour_round._byes.remove(player.uid)
        else:
            if player.pod(self.tour_round) is not None:
                self.remove_player_from_pod(player)
            self.tour_round.set_result(player, Player.EResult.BYE)

toggle_game_loss

toggle_game_loss(players: Iterable[Player] | Player)

Toggles the game loss status for player(s).

If a player is assigned a game loss, they are removed from their pod and marked as having lost. If they already have a game loss, it is removed.

Parameters:

Name Type Description Default
players Iterable[Player] | Player

The player or iterable of players to toggle game loss for.

required
Source code in src/core.py
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
@TournamentAction.action
def toggle_game_loss(self, players: Iterable[Player] | Player):
    """Toggles the game loss status for player(s).

    If a player is assigned a game loss, they are removed from their pod and marked as having lost.
    If they already have a game loss, it is removed.

    Args:
        players: The player or iterable of players to toggle game loss for.
    """
    if not isinstance(players, Iterable):
        players = [players]

    for player in players:
        if player.uid in self.tour_round._game_loss:
            self.tour_round._game_loss.remove(player.uid)
        else:
            # if player.pod(self.tour_round) is not None:
            #    self.remove_player_from_pod(player)
            player.set_result(self.tour_round, Player.EResult.LOSS)

TournamentAction

Serializable action that will be stored in tournament log and can be restored

action classmethod

action(func: _F) -> _F

Decorator to mark a function as a tournament action.

Parameters:

Name Type Description Default
func _F

The function to decorate.

required

Returns:

Type Description
_F

The decorated function.

Source code in src/core.py
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
@classmethod
def action(cls, func: _F) -> _F:
    """Decorator to mark a function as a tournament action.

    Args:
        func: The function to decorate.

    Returns:
        The decorated function.
    """

    @StandingsExport.auto_export
    @PodsExport.auto_export
    @functools.wraps(func)
    def wrapper(self: Tournament, *original_args, **original_kwargs):
        # before = self.serialize()
        ret = func(self, *original_args, **original_kwargs)
        cls.store(self)
        return ret

    return cast(_F, wrapper)

load classmethod

load(logdir='logs/default.json') -> Tournament | None

Loads the tournament state from a log file.

Parameters:

Name Type Description Default
logdir

The path to the log file (default: 'logs/default.json').

'logs/default.json'

Returns:

Type Description
Tournament | None

The loaded tournament instance, or None if the file does not exist.

Source code in src/core.py
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
@classmethod
def load(cls, logdir="logs/default.json") -> Tournament | None:
    """Loads the tournament state from a log file.

    Args:
        logdir: The path to the log file (default: 'logs/default.json').

    Returns:
        The loaded tournament instance, or None if the file does not exist.
    """
    if os.path.exists(logdir):
        cls.LOGF = logdir
        # try:
        with open(cls.LOGF, "r") as f:
            tour_json = json.load(f)
            tour = Tournament.inflate(tour_json)
        return tour
        # except Exception as e:
        #    Log.log(str(e), level=Log.Level.ERROR)
        #    return None
    return None

store classmethod

store(tournament: Tournament)

Stores the tournament state to a log file.

Parameters:

Name Type Description Default
tournament Tournament

The tournament instance to store.

required
Source code in src/core.py
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
@classmethod
def store(cls, tournament: Tournament):
    """Stores the tournament state to a log file.

    Args:
        tournament: The tournament instance to store.
    """
    if cls.LOGF is None:
        cls.LOGF = cls.DEFAULT_LOGF
    if cls.LOGF:
        assert isinstance(cls.LOGF, str)
        if not os.path.exists(os.path.dirname(cls.LOGF)):
            os.makedirs(os.path.dirname(cls.LOGF))
        with open(cls.LOGF, "w") as f:
            json.dump(tournament.serialize(), f, indent=4)

TournamentConfiguration

TournamentConfiguration(**kwargs)

Bases: ITournamentConfiguration

Initializes the TournamentConfiguration.

Parameters:

Name Type Description Default
**kwargs

Arbitrary keyword arguments using the configuration.

{}
Source code in src/core.py
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
def __init__(self, **kwargs):
    """Initializes the TournamentConfiguration.

    Args:
        **kwargs: Arbitrary keyword arguments using the configuration.
    """
    self.pod_sizes: Sequence[int] = kwargs.get("pod_sizes", [4, 3])
    self.allow_bye: bool = kwargs.get("allow_bye", True)
    self.win_points: int = kwargs.get("win_points", 5)
    self.bye_points: int = kwargs.get("bye_points", 4)
    self.draw_points: int = kwargs.get("draw_points", 1)
    self.snake_pods: bool = kwargs.get("snake_pods", True)
    self.n_rounds: int = kwargs.get("n_rounds", 5)
    # Parse int or enum for TopCut
    tc_val: TournamentConfiguration.TopCut | int = kwargs.get(
        "top_cut", TournamentConfiguration.TopCut.NONE
    )
    if isinstance(tc_val, TournamentConfiguration.TopCut):
        self.top_cut: TournamentConfiguration.TopCut = tc_val
    else:
        # If it's already an int, map to Enum
        try:
            self.top_cut = TournamentConfiguration.TopCut(tc_val)
        except Exception:
            self.top_cut = TournamentConfiguration.TopCut.NONE
    self.max_byes: int = kwargs.get("max_byes", 2)
    self.auto_export: bool = kwargs.get("auto_export", True)
    self.standings_export: IStandingsExport = kwargs.get(
        "standings_export", StandingsExport()
    )
    self.global_wr_seats: Sequence[float] = kwargs.get(
        "global_wr_seats",
        [
            # 0.2553,
            # 0.2232,
            # 0.1847,
            # 0.1428,
            # New data: all 50+ player events since [2024-09-30;2025-05-05]
            0.2470,
            0.1928,
            0.1672,
            0.1458,
        ],
    )

max_pod_size property

max_pod_size

Returns the maximum pod size.

Returns:

Type Description

The maximum pod size.

min_pod_size property

min_pod_size

Returns the minimum pod size.

Returns:

Type Description

The minimum pod size.

ranking staticmethod

ranking(x: IPlayer, tour_round: IRound) -> tuple[int | float | str, ...]

Calculates the ranking score for a player.

Parameters:

Name Type Description Default
x IPlayer

The player.

required
tour_round IRound

The current round.

required

Returns:

Type Description
tuple[int | float | str, ...]

A tuple of ranking criteria.

Source code in src/core.py
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
@staticmethod
@override
def ranking(x: IPlayer, tour_round: IRound) -> tuple[int | float | str, ...]:
    """Calculates the ranking score for a player.

    Args:
        x: The player.
        tour_round: The current round.

    Returns:
        A tuple of ranking criteria.
    """
    return (
        x.rating(tour_round),
        len(x.games(tour_round)),
        np.round(x.opponent_pointrate(tour_round), 10),
        len(x.players_beaten(tour_round)),
        -x.average_seat([r for r in x.tour.rounds if r.seq <= tour_round.seq]),
        -x.uid if isinstance(x.uid, int) else -int(x.uid.int),
    )

TournamentContext

TournamentContext(tour: Tournament, tour_round: Round, standings: list[Player])

Context object holding tournament state for export operations.

Initializes the TournamentContext.

Parameters:

Name Type Description Default
tour Tournament

The tournament instance.

required
tour_round Round

The specific round of the tournament.

required
standings list[Player]

The list of players in the current standings.

required
Source code in src/core.py
162
163
164
165
166
167
168
169
170
171
172
def __init__(self, tour: Tournament, tour_round: Round, standings: list[Player]):
    """Initializes the TournamentContext.

    Args:
        tour: The tournament instance.
        tour_round: The specific round of the tournament.
        standings: The list of players in the current standings.
    """
    self.tour = tour
    self.tour_round = tour_round
    self.standings = standings