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
479
480
481
482
483
484
485
486
487
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
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
@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
508
509
510
511
512
513
514
515
516
517
518
519
520
521
@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
523
524
525
526
527
@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
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
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] = []

display_name property

display_name: str

The name for human-facing output: the stored name, plus a short UUID suffix when another player in the tournament shares it.

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 CommanderConfiguration.global_wr_seats

Source code in src/core.py
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
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 CommanderConfiguration.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:
                # Middle seats exist only in 3+ player pods, i.e.
                # Commander - see CommanderConfiguration.
                wr_seats: Sequence[float] = getattr(
                    self.tour.config, "global_wr_seats"
                )
                rates = list(wr_seats[0 : len(pod)])
                # Pods larger than the configured seats (e.g. 6) reuse
                # the last seat rate for the extra middle seats.
                rates += rates[-1:] * (len(pod) - 1 - len(rates))
                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
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
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
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
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
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
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
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
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
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
@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
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
@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, ratings: Mapping[Any, float] | 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
ratings Mapping[Any, float] | None

Optional precomputed full-field rating map, forwarded to rating() (see Player.rating).

None

Returns:

Name Type Description
float float

The point rate.

Source code in src/core.py
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
def pointrate(
    self,
    tour_round: Round | None = None,
    ratings: Mapping[Any, float] | None = None,
) -> float:
    """Calculates the point rate (actual points / maximum possible points).

    Args:
        tour_round: The round up to which to calculate.
        ratings: Optional precomputed full-field rating map, forwarded to
            rating() (see Player.rating).

    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
    scoring_logic = self.tour.get_scoring_logic(self.tour.config.scoring_logic)
    return self.rating(tour_round, ratings) / scoring_logic.pointrate_denominator(
        tour_round
    )

rating

rating(tour_round: Round | None, ratings: Mapping[Any, float] | None = 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
ratings Mapping[Any, float] | None

Optional precomputed full-field rating map (uid -> rating). get_standings computes it once and passes it down so the whole sort does not recompute the field once per player.

None

Returns:

Name Type Description
float float

The rating as a decimal (0.0 to 1.0).

Source code in src/core.py
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
def rating(
    self,
    tour_round: Round | None,
    ratings: Mapping[Any, float] | None = 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.
        ratings: Optional precomputed full-field rating map (uid -> rating).
            get_standings computes it once and passes it down so the whole
            sort does not recompute the field once per player.

    Returns:
        float: The rating as a decimal (0.0 to 1.0).
    """
    if tour_round is None:
        return 0
    if ratings is not None:
        return ratings.get(self.uid, 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
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
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 | None) -> Player.EResult

Retrieves the player's result for a specific round.

Parameters:

Name Type Description Default
tour_round Round | None

The round to query.

required

Returns:

Type Description
EResult

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

Source code in src/core.py
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
def result(self, tour_round: Round | None) -> 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 tour_round is None:
        return Player.EResult.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
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
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
2570
2571
2572
2573
2574
2575
2576
2577
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
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
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
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
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()
    # The pod's whole-match report, in play order. Validated and
    # replaced as a unit by record_result - see IRuleset.validate_report.
    self._games: list[IGameResult] = list()

balance property

balance: ndarray

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

games property

games: tuple[IGameResult, ...]

This pod's whole-match report, in play order.

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
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
@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
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
@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
3406
3407
3408
3409
3410
3411
3412
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)

record_result

record_result(games: list[IGameResult]) -> None

Validates and replaces this pod's whole match report.

A new report always replaces any earlier one - see IRuleset.validate_report and docs/tournament-log-spec.md, "Match report".

Parameters:

Name Type Description Default
games list[IGameResult]

The match's complete list of games, in play order.

required

Raises:

Type Description
ValueError

If games is not a valid report for this pod, per this tournament's ruleset.

Source code in src/core.py
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
def record_result(self, games: list[IGameResult]) -> None:
    """Validates and replaces this pod's whole match report.

    A new report always replaces any earlier one - see
    IRuleset.validate_report and docs/tournament-log-spec.md, "Match
    report".

    Args:
        games: The match's complete list of games, in play order.

    Raises:
        ValueError: If games is not a valid report for this pod, per
            this tournament's ruleset.
    """
    self.tour.ruleset.validate_report(self, games)
    self._games = list(games)

remove_result

remove_result(player: Player)

Strips a player from every recorded game; drops any game entry that becomes empty as a result.

Source code in src/core.py
3235
3236
3237
3238
3239
3240
3241
3242
3243
def remove_result(self, player: Player):
    """Strips a player from every recorded game; drops any game entry
    that becomes empty as a result."""
    new_games: list[IGameResult] = []
    for game in self._games:
        winners = game.winners - {player.uid}
        if winners:
            new_games.append(IGameResult(winners))
    self._games = new_games

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
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
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)

reset_result

reset_result()

Clears the pod's entire game history, reopening the match.

Source code in src/core.py
3245
3246
3247
def reset_result(self):
    """Clears the pod's entire game history, reopening the match."""
    self._games.clear()

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
 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
157
158
159
160
161
162
163
@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.log_path(self)
            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.display_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.display_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.display_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
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
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
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
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
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
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:
        # Reset disabled set to the previous round's baseline so that re-pairing
        # after reset_pods() + config change doesn't keep stale topcut entries.
        prev_round = self.tour.previous_round(self)
        if prev_round:
            self._disabled = set(prev_round._disabled)
        standings = self.tour.get_standings(prev_round)
        self.disable_topcut(standings)
        # Called for every playoff round, not just a hard-coded subset:
        # the base IPairingLogic.advance_topcut is a no-op, so a stage
        # whose pairing logic doesn't give seeded byes is unaffected.
        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 and self.tour.ruleset.SEAT_BALANCING:
        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
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
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
3806
3807
3808
3809
3810
3811
3812
3813
3814
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
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
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."""
    prev_round = self.tour.previous_round(self)
    standings = self.tour.get_standings(prev_round)

    # At the first playoff round (previous round is Swiss), draw the
    # top-N line against active players only - a player who dropped or
    # was disabled before the cut must not still occupy a top-N slot,
    # so the next-ranked active player advances instead. A later
    # playoff round must not re-filter: a semifinalist who drops must
    # not pull an extra Swiss-standings player into contention.
    if prev_round is not None and prev_round.stage == Round.Stage.SWISS:
        standings = [p for p in standings if p in self.active_players]

    # 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)

record_result

record_result(pod: Pod, games: list[IGameResult]) -> None

Validates and records pod's whole match report, replacing any earlier one, then clears bye/game-loss flags for the winners (the side effect today's set_result has on reported players).

Source code in src/core.py
4001
4002
4003
4004
4005
4006
4007
4008
def record_result(self, pod: Pod, games: list[IGameResult]) -> None:
    """Validates and records pod's whole match report, replacing any
    earlier one, then clears bye/game-loss flags for the winners (the
    side effect today's set_result has on reported players)."""
    pod.record_result(games)
    for uid in pod._result:
        self._byes.discard(uid)
        self._game_loss.discard(uid)

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
3833
3834
3835
3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
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
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
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
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
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
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
def sort_pods_by_power(self) -> None:
    """Sort pods by number of players and average rating to establish a power-level baseline."""
    ratings = self.tour.field_ratings(self)
    pods_sorted = sorted(
        self.pods,
        key=lambda x: (
            len(x.players),
            np.average([p.rating(self, ratings) 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
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
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
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
@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
423
424
425
426
427
428
429
430
431
432
433
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
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
1160
1161
1162
1163
1164
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()
    self.__config = config

    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
    self.created_at: datetime = datetime.now(timezone.utc)
    # Per-tournament log file override; see TournamentAction.log_path.
    self.log_path: str | None = None

    # Validates ruleset/pod_sizes/top_cut/ruleset_params on construction
    # (including inflate(), which constructs via this __init__ before
    # attaching players/rounds) - self.config is this same config
    # object, so the "ruleset changed after results exist" rule never
    # fires here, only via the config setter.
    self.__validate_config(config)
    super().__init__(uid=uid)

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.

has_results property

has_results: bool

Whether any round has pods, byes, or game losses recorded.

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.

__compute_stage_and_logic

__compute_stage_and_logic(seq: int, prev_stage: Stage | None) -> tuple[Round.Stage, IPairingLogic] | None

Computes the stage and pairing logic for a round at the given sequence position.

For a Swiss round, the pairing logic comes from config.pairing_logics (one name per Swiss round) when that seq is configured; otherwise it falls back to the adaptive default (see _adaptive_pairing_logic). Top-cut rounds follow the ruleset's playoff plan (IRuleset.PLAYOFFS[top_cut]): the previous round's stage decides which entry of the plan comes next.

Parameters:

Name Type Description Default
seq int

The 0-indexed sequence number of the round.

required
prev_stage Stage | None

The stage of the immediately preceding round, or None if this is the first round.

required

Returns:

Type Description
tuple[Stage, IPairingLogic] | None

A (stage, logic) tuple, or None if no more rounds should be created/configured.

Source code in src/core.py
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
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
def __compute_stage_and_logic(
    self, seq: int, prev_stage: Round.Stage | None
) -> tuple[Round.Stage, IPairingLogic] | None:
    """Computes the stage and pairing logic for a round at the given sequence position.

    For a Swiss round, the pairing logic comes from config.pairing_logics
    (one name per Swiss round) when that seq is configured; otherwise it
    falls back to the adaptive default (see _adaptive_pairing_logic).
    Top-cut rounds follow the ruleset's playoff plan
    (IRuleset.PLAYOFFS[top_cut]): the previous round's stage decides
    which entry of the plan comes next.

    Args:
        seq: The 0-indexed sequence number of the round.
        prev_stage: The stage of the immediately preceding round, or None if this is the first round.

    Returns:
        A (stage, logic) tuple, or None if no more rounds should be created/configured.
    """
    stage = Round.Stage.SWISS
    logic: IPairingLogic | None = None
    if seq >= self.config.n_rounds and prev_stage is not None:
        top_cut = int(self.config.top_cut)
        if top_cut == 0:
            Log.log("Maximum number of rounds reached.", level=Log.Level.WARNING)
            return None
        plan = self.ruleset.PLAYOFFS.get(top_cut)
        if plan is None:
            raise ValueError(f"Unknown top cut: {self.config.top_cut}")
        if prev_stage == Round.Stage.SWISS:
            entry = plan[0]
        else:
            stage_values = [value for value, _ in plan]
            try:
                idx = stage_values.index(prev_stage.value)
            except ValueError:
                Log.log("Tournament completed.")
                return None
            if idx + 1 >= len(plan):
                Log.log("Tournament completed.")
                return None
            entry = plan[idx + 1]
        stage_value, logic_name = entry
        stage = Round.Stage(stage_value)
        logic = self.get_pairing_logic(logic_name)
    else:
        configured = self.config.pairing_logics
        name = configured[seq] if seq < len(configured) else None
        if name is not None:
            logic = self.get_pairing_logic(name)
            # Respect the user's explicit choice, but warn if it does not
            # support the tournament's pod sizes (the GUI prevents this;
            # a hand-edited or CLI config can still reach here).
            if not logic.supports_pod_sizes(self.config.pod_sizes):
                Log.log(
                    f"Pairing logic {name} does not support pod sizes "
                    f"{list(self.config.pod_sizes)} (round {seq + 1}).",
                    level=Log.Level.WARNING,
                )
        else:
            logic = self._adaptive_pairing_logic(seq)

    if not logic:
        Log.log("No pairing logic found.", level=Log.Level.ERROR)
        return None
    return (stage, logic)

__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 round 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
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
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 round 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)
    prev_stage = self.last_round.stage if self.last_round else None
    result = self.__compute_stage_and_logic(seq, prev_stage)
    if result is None:
        return False
    stage, logic = result
    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.

Called from the config setter (before the new config replaces the old one - self.config below still returns the old config) and from init (where self.config already IS config, so rule 6 below can never fire there - it only guards a change on a running tournament).

Parameters:

Name Type Description Default
config TournamentConfiguration

The tournament configuration.

required

Returns:

Name Type Description
bool bool

True if the configuration is valid.

Raises:

Type Description
ValueError

If any rule below is violated.

Source code in src/core.py
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
def __validate_config(self, config: TournamentConfiguration) -> bool:
    """
    Validates the tournament configuration.

    Called from the config setter (before the new config replaces the
    old one - self.config below still returns the old config) and from
    __init__ (where self.config already IS config, so rule 6 below can
    never fire there - it only guards a change on a running tournament).

    Args:
        config: The tournament configuration.

    Returns:
        bool: True if the configuration is valid.

    Raises:
        ValueError: If any rule below is violated.
    """
    if len(self.swiss_rounds) > config.n_rounds:
        raise ValueError(
            "Tournament has already reached the maximum number of rounds."
        )

    self.__validate_config_values(config)

    if self.has_results and config.ruleset != self.config.ruleset:
        raise ValueError(
            "Cannot change ruleset after the tournament has pods, byes, "
            "or game losses."
        )

    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, or a UUID repeats (within the batch or against existing players); the whole batch is validated first, so nothing is added.

Source code in src/core.py
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
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
@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, or
            a UUID repeats (within the batch or against existing players);
            the whole batch is validated first, so nothing is added.
    """

    # 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]

    specs_to_add = []
    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}"
            )

        # Names may repeat; the UUID is the player's identity.
        if uid and uid in existing_uids:
            raise ValueError("Player with UID {} already enlisted.".format(uid))
        if uid:
            existing_uids.add(uid)
        specs_to_add.append((name, uid, decklist))

    new_players = []
    for name, uid, decklist in specs_to_add:
        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)
        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
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
@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. The Swiss pairing logic per round comes from config.pairing_logics (see __compute_stage_and_logic).

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
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
@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. The Swiss pairing logic per round comes from
    config.pairing_logics (see __compute_stage_and_logic).

    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
    else:
        # Round exists but is not done — refresh stage/logic so any config changes
        # made since the round was initialized (or since the last reset) are applied.
        tour_round = self.last_round
        seq = tour_round.seq
        prev_round = self.rounds[seq - 1] if seq > 0 else None
        prev_stage = prev_round.stage if prev_round else None
        result = self.__compute_stage_and_logic(seq, prev_stage)
        if result:
            tour_round.stage, tour_round.logic = result
    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
2109
2110
2111
2112
2113
2114
2115
2116
2117
@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
1900
1901
1902
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
@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
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
@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/logic/*/matching.py.

Source code in src/core.py
 992
 993
 994
 995
 996
 997
 998
 999
1000
@classmethod
def discover_pairing_logic(cls) -> None:
    """Discover and cache all pairing logic implementations from src/logic/*/matching.py."""
    if cls._pairing_logic_cache:
        return
    found: dict[str, Any] = {}
    cls._discover_logic("matching.py", IPairingLogic, found)
    # One assignment: another thread sees either no cache or a full one.
    cls._pairing_logic_cache = found

discover_ruleset classmethod

discover_ruleset() -> None

Discover and cache all ruleset implementations from src/logic/*/rules.py.

Source code in src/core.py
1090
1091
1092
1093
1094
1095
1096
1097
1098
@classmethod
def discover_ruleset(cls) -> None:
    """Discover and cache all ruleset implementations from src/logic/*/rules.py."""
    if cls._ruleset_cache:
        return
    found: dict[str, Any] = {}
    cls._discover_logic("rules.py", IRuleset, found)
    # One assignment: another thread sees either no cache or a full one.
    cls._ruleset_cache = found

discover_scoring_logic classmethod

discover_scoring_logic() -> None

Discover and cache all scoring logic implementations from src/logic/*/scoring.py.

Source code in src/core.py
1040
1041
1042
1043
1044
1045
1046
1047
1048
@classmethod
def discover_scoring_logic(cls) -> None:
    """Discover and cache all scoring logic implementations from src/logic/*/scoring.py."""
    if cls._scoring_logic_cache:
        return
    found: dict[str, Any] = {}
    cls._discover_logic("scoring.py", IScoringLogic, found)
    # One assignment: another thread sees either no cache or a full one.
    cls._scoring_logic_cache = found

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
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
@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

        # Only a player with no history (no seat, bye or game loss in any
        # round) can be deleted; otherwise keep them and mark dropped.
        if any(
            p.uid in r._byes or p.uid in r._game_loss or p.seated(r)
            for r in self.rounds
        ):
            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
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
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:
        # A bare filename has no directory part; it goes to the CWD.
        if os.path.dirname(var_export_param):
            os.makedirs(os.path.dirname(var_export_param), exist_ok=True)
        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)

field_ratings

field_ratings(tour_round: Round) -> Mapping[Any, float]

Computes every player's rating for a round in one call.

Callers that need many players' ratings for the same round (standings sort, pairing sort keys, pod power sort) must compute this once and pass it down as the ratings argument, instead of calling rating() per player - each such call otherwise replays the whole field.

Source code in src/core.py
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
def field_ratings(self, tour_round: Round) -> Mapping[Any, float]:
    """Computes every player's rating for a round in one call.

    Callers that need many players' ratings for the same round (standings
    sort, pairing sort keys, pod power sort) must compute this once and
    pass it down as the `ratings` argument, instead of calling rating()
    per player - each such call otherwise replays the whole field.
    """
    return self.get_scoring_logic(self.config.scoring_logic).compute_ratings(
        self, tour_round
    )

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
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
@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
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
@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 in cls._pairing_logic_cache:
        return cls._pairing_logic_cache[logic_name]
    for logic in cls._pairing_logic_cache.values():
        if logic_name in logic.ALIASES:
            return logic
    raise ValueError(f"Unknown pairing logic: {logic_name}")

get_pod_sizes

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

Determines possible pod sizes for a given number of players.

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
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
def get_pod_sizes(self, n) -> list[int] | None:
    """Determines possible pod sizes for a given number of players.

    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.
    """
    pod_sizes = self.config.pod_sizes
    min_pod_size = min(pod_sizes)
    # 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(pod_sizes):
            continue

        pod_size = 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 < min_pod_size:
            if self.config.allow_bye and rem <= self.config.max_byes:
                return current_solution + [pod_size]
            elif pod_size == 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 >= 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
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
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.display_name, p.rating(self.tour_round) or "0")
                for p in self.tour_round.unassigned
            ]
        )
    return export_str

get_ruleset classmethod

get_ruleset(name: str) -> IRuleset

Get a ruleset instance by name.

Parameters:

Name Type Description Default
name str

The name of the ruleset class.

required

Returns:

Type Description
IRuleset

The ruleset instance.

Raises:

Type Description
ValueError

If no ruleset by that name is discoverable.

Source code in src/core.py
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
@classmethod
def get_ruleset(cls, name: str) -> IRuleset:
    """Get a ruleset instance by name.

    Args:
        name: The name of the ruleset class.

    Returns:
        The ruleset instance.

    Raises:
        ValueError: If no ruleset by that name is discoverable.
    """
    cls.discover_ruleset()

    if name not in cls._ruleset_cache:
        raise ValueError(f"Unknown ruleset: {name}")

    return cls._ruleset_cache[name]

get_scoring_logic classmethod

get_scoring_logic(logic_name: str) -> IScoringLogic

Get a scoring logic instance by name.

Parameters:

Name Type Description Default
logic_name str

The name of the scoring logic.

required

Returns:

Type Description
IScoringLogic

The scoring logic instance.

Source code in src/core.py
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
@classmethod
def get_scoring_logic(cls, logic_name: str) -> IScoringLogic:
    """Get a scoring logic instance by name.

    Args:
        logic_name: The name of the scoring logic.

    Returns:
        The scoring logic instance.
    """
    cls.discover_scoring_logic()

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

    return cls._scoring_logic_cache[logic_name]

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
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
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
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
@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.
    """
    if tour_round is None:
        tour_round = self.tour_round
    if tour_round is None:
        # No round has been created yet - nothing to rank on.
        return sorted(self.players, key=lambda x: x.name)

    method = Player.SORT_METHOD
    order = Player.SORT_ORDER
    Player.SORT_METHOD = SortMethod.RANK
    Player.SORT_ORDER = SortOrder.ASCENDING
    playoffs = False
    if tour_round.stage == Round.Stage.SWISS:
        # Compute the whole field's ratings once and pass the map down the
        # ranking chain, so the sort does not recompute it once per player
        # (~players x opponents times) - costly under wagering scoring.
        ratings = self.field_ratings(tour_round)
        scoring = self.get_scoring_logic(self.config.scoring_logic)
        keys = scoring.standings_keys(self, tour_round, ratings)
        standings = sorted(
            self.players,
            key=lambda x: keys[x.uid],
            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] | None = None, style: Format | None = None, 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] | None

A list of StandingsExport.Field to include in the standings. Defaults to config.standings_export.fields.

None
style Format | None

The desired output format (e.g., PLAIN, CSV, JSON). Defaults to config.standings_export.format.

None
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
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
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
def get_standings_str(
    self,
    fields: list[StandingsExport.Field] | None = None,
    style: StandingsExport.Format | None = None,
    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.
            Defaults to config.standings_export.fields.
        style: The desired output format (e.g., PLAIN, CSV, JSON).
            Defaults to config.standings_export.format.
        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)
    if fields is None:
        fields = self.config.standings_export.fields
    if style is None:
        style = self.config.standings_export.format  # pyright: ignore[reportAttributeAccessIssue]

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

    def value(f: StandingsExport.Field, p: Player) -> Any:
        if tour_round is None:
            if f == StandingsExport.Field.STANDING:
                return standings.index(p) + 1
            if f == StandingsExport.Field.ID:
                return p.uid.hex
            if f == StandingsExport.Field.NAME:
                return p.display_name
            if f in (StandingsExport.Field.RECORD, StandingsExport.Field.SEAT_HISTORY):
                return ""
            return 0
        info = StandingsExport.info[f]
        v = info.get(p, context)  # pyright: ignore[reportAny]
        return v if info.denom is None else v * info.denom

    lines = [[StandingsExport.info[f].name for f in fields]]
    lines += [
        [
            StandingsExport.info[f].format.format(value(f, p))
            for f in fields
        ]
        for p in standings
    ]
    # Scoring-specific tiebreaker columns follow the core fields.
    scoring = self.get_scoring_logic(self.config.scoring_logic)
    extra_columns = scoring.standings_columns(self, tour_round) if tour_round else []
    if extra_columns:
        lines[0] += [header for header, _ in extra_columns]
        for i, p in enumerate(standings):
            lines[i + 1] += [values.get(p.uid, "") for _, values in extra_columns]

    if style == StandingsExport.Format.PLAIN:
        col_len = [0] * len(lines[0])
        for col in range(len(lines[0])):
            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(lines[0])):
                line[col] = line[col].ljust(col_len[col])
        # add new line at index 1
        lines.insert(1, ["-" * width for width in col_len])
        return "\n".join([" | ".join(line) for line in lines])
    elif style == StandingsExport.Format.CSV:
        buf = io.StringIO()
        csv.writer(buf, lineterminator="\n").writerows(lines)
        return buf.getvalue()
    elif style == StandingsExport.Format.JSON:
        return json.dumps([dict(zip(lines[0], line)) for line in lines[1:]], indent=2)

    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
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
@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.
    """
    format_version = data.get("format_version", cls.LOG_FORMAT_VERSION)
    if format_version not in cls.KNOWN_FORMAT_VERSIONS:
        Log.log(
            f"Loading tournament log with unknown format_version "
            f"{format_version!r} (known: {sorted(cls.KNOWN_FORMAT_VERSIONS)}).",
            level=Log.Level.WARNING,
        )

    config = TournamentConfiguration.inflate(data["config"])
    cls.__validate_config_values(config)
    tour_uid = UUID(data["uid"])
    if tour_uid in Tournament.CACHE:
        tour = Tournament.CACHE[tour_uid]
    else:
        tour = cls(config, tour_uid)
    # Older log files (before format_version existed) have no created_at;
    # __init__ already stamped tour.created_at with the load time, which
    # is the best available fallback.
    if "created_at" in data:
        tour.created_at = datetime.fromisoformat(data["created_at"])
    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
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
@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
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
@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
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
@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

pairing_logic_name_for

pairing_logic_name_for(seq: int, prev_stage: Stage | None = None) -> str | None

Names the pairing logic round seq will use, as stored Pairings.logic names it.

Parameters:

Name Type Description Default
seq int

The 0-indexed sequence number of the round.

required
prev_stage Stage | None

The stage of the preceding round. Defaults to the stage of round seq - 1, existing or as it would be created.

None

Returns:

Type Description
str | None

The logic name (e.g. "PairingSnake", "PairingTopCut"), or None if no

str | None

round would be created at that position.

Source code in src/core.py
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
def pairing_logic_name_for(
    self, seq: int, prev_stage: Round.Stage | None = None
) -> str | None:
    """Names the pairing logic round `seq` will use, as stored Pairings.logic names it.

    Args:
        seq: The 0-indexed sequence number of the round.
        prev_stage: The stage of the preceding round. Defaults to the stage
            of round seq - 1, existing or as it would be created.

    Returns:
        The logic name (e.g. "PairingSnake", "PairingTopCut"), or None if no
        round would be created at that position.
    """
    if prev_stage is None and seq > 0:
        if seq <= len(self.rounds):
            prev_stage = self.rounds[seq - 1].stage
        else:
            prev_stage = self.rounds[-1].stage if self.rounds else None
            for i in range(len(self.rounds), seq):
                step = self.__compute_stage_and_logic(i, prev_stage)
                if step is None:
                    return None
                prev_stage = step[0]
    result = self.__compute_stage_and_logic(seq, prev_stage)
    return result[1].name if result else None

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
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
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
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
@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:
        for pod in [x for x in self.tour_round.pods if not x.done]:
            self.tour_round.record_result(pod, self.ruleset.random_report(pod))

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.

Delegates to the tournament's configured scoring logic (config.scoring_logic, default "ScoringDefault") - see src/logic/commander/scoring.py.

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
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
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.

    Delegates to the tournament's configured scoring logic
    (config.scoring_logic, default "ScoringDefault") - see
    src/logic/commander/scoring.py.

    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.
    """
    # This answers one player's query and replays the whole field
    # under wagering scoring. get_standings() avoids calling it per
    # player: it computes the field map once via compute_ratings()
    # and passes it down the ranking chain (see get_standings). This
    # path is the fallback for single, off-standings rating lookups.
    return self.get_scoring_logic(self.config.scoring_logic).rating(
        player, tour_round
    )

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
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
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

Raises:

Type Description
ValueError

If the new name is empty.

Source code in src/core.py
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
@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.

    Raises:
        ValueError: If the new name is empty.
    """
    new_name = (new_name or "").strip()
    if not new_name:
        raise ValueError("Player name cannot be empty.")
    old_name = player.name
    if new_name == old_name:
        return
    # Pods and rounds hold this same Player object, so one assignment renames everywhere.
    player.name = new_name
    Log.log(
        "\tRenamed player {} to {}".format(old_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
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
@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]
        self.tour_round.set_result(players, Player.EResult.DRAW)

report_match

report_match(pod: Pod, games: list[IGameResult]) -> None

Reports a pod's whole match, replacing any earlier report.

Parameters:

Name Type Description Default
pod Pod

The pod being reported.

required
games list[IGameResult]

The match's complete list of games, in play order - see IRuleset.validate_report for what makes a report valid for this tournament's ruleset.

required
Source code in src/core.py
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
@TournamentAction.action
def report_match(self, pod: Pod, games: list[IGameResult]) -> None:
    """Reports a pod's whole match, replacing any earlier report.

    Args:
        pod: The pod being reported.
        games: The match's complete list of games, in play order - see
            IRuleset.validate_report for what makes a report valid for
            this tournament's ruleset.
    """
    assert self.tour_round is not None
    self.tour_round.record_result(pod, games)

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
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
@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]
        self.tour_round.set_result(players, 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
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
@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

reset_result

reset_result(pod: Pod) -> None

Clears a pod's match report, reopening it for a new report.

Parameters:

Name Type Description Default
pod Pod

The pod to reset.

required
Source code in src/core.py
2004
2005
2006
2007
2008
2009
2010
2011
@TournamentAction.action
def reset_result(self, pod: Pod) -> None:
    """Clears a pod's match report, reopening it for a new report.

    Args:
        pod: The pod to reset.
    """
    pod.reset_result()

ruleset_names classmethod

ruleset_names() -> list[str]

Names of every discovered ruleset.

Source code in src/core.py
1120
1121
1122
1123
1124
@classmethod
def ruleset_names(cls) -> list[str]:
    """Names of every discovered ruleset."""
    cls.discover_ruleset()
    return sorted(cls._ruleset_cache)

scoring_logic_names classmethod

scoring_logic_names() -> list[str]

Names of every discovered scoring logic.

Source code in src/core.py
1067
1068
1069
1070
1071
@classmethod
def scoring_logic_names(cls) -> list[str]:
    """Names of every discovered scoring logic."""
    cls.discover_scoring_logic()
    return sorted(cls._scoring_logic_cache)

selectable_pairing_logics classmethod

selectable_pairing_logics(pod_sizes: Sequence[int] | None = None) -> list[str]

Names of pairing logics a user may pick for a Swiss round.

Excludes top-cut pairings (SELECTABLE == False), which are chosen automatically by stage. When pod_sizes is given, also excludes any logic that does not support all of those sizes (see IPairingLogic.supports_pod_sizes).

Source code in src/core.py
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
@classmethod
def selectable_pairing_logics(
    cls, pod_sizes: Sequence[int] | None = None
) -> list[str]:
    """Names of pairing logics a user may pick for a Swiss round.

    Excludes top-cut pairings (SELECTABLE == False), which are chosen
    automatically by stage. When pod_sizes is given, also excludes any
    logic that does not support all of those sizes (see
    IPairingLogic.supports_pod_sizes).
    """
    cls.discover_pairing_logic()
    return sorted(
        name
        for name, obj in cls._pairing_logic_cache.items()
        if obj.SELECTABLE
        and (pod_sizes is None or obj.supports_pod_sizes(pod_sizes))
    )

selectable_scoring_logics classmethod

selectable_scoring_logics(pod_sizes: Sequence[int] | None = None) -> list[str]

Names of scoring logics a user may pick for this tournament.

When pod_sizes is given, excludes any logic that does not support all of those sizes (see IScoringLogic.supports_pod_sizes) - the same filter selectable_pairing_logics applies to pairing logic.

Source code in src/core.py
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
@classmethod
def selectable_scoring_logics(
    cls, pod_sizes: Sequence[int] | None = None
) -> list[str]:
    """Names of scoring logics a user may pick for this tournament.

    When pod_sizes is given, excludes any logic that does not support
    all of those sizes (see IScoringLogic.supports_pod_sizes) - the
    same filter selectable_pairing_logics applies to pairing logic.
    """
    cls.discover_scoring_logic()
    return sorted(
        name
        for name, obj in cls._scoring_logic_cache.items()
        if pod_sizes is None or obj.supports_pod_sizes(pod_sizes)
    )

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
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
@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
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
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["format_version"] = self.LOG_FORMAT_VERSION
    data["generator"] = {"name": "EDH_matchmaker"}
    data["created_at"] = self.created_at.isoformat()
    data["updated_at"] = datetime.now(timezone.utc).isoformat()
    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

swiss_logic_name_for

swiss_logic_name_for(seq: int) -> str

Names the pairing logic a Swiss round at seq would use.

Unlike pairing_logic_name_for, this ignores n_rounds and top_cut, so it also answers for Swiss rounds played past the planned count.

Parameters:

Name Type Description Default
seq int

The 0-indexed sequence number of the round.

required

Returns:

Type Description
str

The logic name, from config.pairing_logics when that seq is

str

configured, else the adaptive default.

Source code in src/core.py
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
def swiss_logic_name_for(self, seq: int) -> str:
    """Names the pairing logic a Swiss round at `seq` would use.

    Unlike pairing_logic_name_for, this ignores n_rounds and top_cut, so it
    also answers for Swiss rounds played past the planned count.

    Args:
        seq: The 0-indexed sequence number of the round.

    Returns:
        The logic name, from config.pairing_logics when that seq is
        configured, else the adaptive default.
    """
    configured = self.config.pairing_logics
    name = configured[seq] if seq < len(configured) else None
    return name or self._adaptive_pairing_logic(seq).name

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
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
@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
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
@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
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
@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.

Raises:

Type Description
Exception

If the file cannot be parsed or inflated.

Source code in src/core.py
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
@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.

    Raises:
        Exception: If the file cannot be parsed or inflated.
    """
    if os.path.exists(logdir):
        with open(logdir, "r") as f:
            tour_json = json.load(f)
        tour = Tournament.inflate(tour_json)
        tour.log_path = logdir
        return tour
    return None

log_path classmethod

log_path(tournament: Tournament) -> str | None

Returns the tournament's log file path, or None when logging is off.

Parameters:

Name Type Description Default
tournament Tournament

The tournament whose log path to resolve.

required
Source code in src/core.py
580
581
582
583
584
585
586
587
588
589
590
591
592
593
@classmethod
def log_path(cls, tournament: Tournament) -> str | None:
    """Returns the tournament's log file path, or None when logging is off.

    Args:
        tournament: The tournament whose log path to resolve.
    """
    if cls.LOGF is False:
        return None
    if tournament.log_path:
        return tournament.log_path
    if isinstance(cls.LOGF, str):
        return cls.LOGF
    return os.path.join(cls.LOG_DIR, f"tournament_{tournament.uid.hex}.json")

store classmethod

store(tournament: Tournament)

Stores the tournament state to its log file.

Parameters:

Name Type Description Default
tournament Tournament

The tournament instance to store.

required
Source code in src/core.py
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
@classmethod
def store(cls, tournament: Tournament):
    """Stores the tournament state to its log file.

    Args:
        tournament: The tournament instance to store.
    """
    logf = cls.log_path(tournament)
    if not logf:
        return
    logdir = os.path.dirname(logf)
    if logdir:
        os.makedirs(logdir, exist_ok=True)
    # Write to a unique temp file and rename over the target so a crash
    # mid-write never leaves a truncated log, and concurrent stores never
    # share a temp file.
    with cls._LOCK:
        data = tournament.serialize()
        fd, tmp_path = tempfile.mkstemp(dir=logdir or ".", suffix=".tmp")
        try:
            with os.fdopen(fd, "w") as f:
                json.dump(data, f, indent=4)
            os.replace(tmp_path, logf)
        except BaseException:
            os.unlink(tmp_path)
            raise

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
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
def __init__(self, **kwargs):
    """Initializes the TournamentConfiguration.

    Args:
        **kwargs: Arbitrary keyword arguments using the configuration.
    """
    # Class name of the IRuleset that owns this tournament's game rules
    # (src/logic/<game>/rules.py) - see IRuleset. Resolved first: it
    # also supplies the defaults of pod_sizes, scoring_logic and
    # standings_export.fields below when the caller does not pass them.
    default = {n: s.default for n, s in self.PARAM_SPEC.items()}
    self.ruleset: str = kwargs.get("ruleset", default["ruleset"])
    ruleset_cls = Tournament.get_ruleset(self.ruleset)

    self.pod_sizes: Sequence[int] = kwargs.get(
        "pod_sizes", list(ruleset_cls.DEFAULT_POD_SIZES)
    )
    self.allow_bye: bool = kwargs.get("allow_bye", default["allow_bye"])
    self.n_rounds: int = kwargs.get("n_rounds", default["n_rounds"])
    # Parse int or enum for TopCut
    tc_val: TournamentConfiguration.TopCut | int = kwargs.get(
        "top_cut", default["top_cut"]
    )
    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", default["max_byes"])
    self.auto_export: bool = kwargs.get("auto_export", default["auto_export"])
    self.standings_export: IStandingsExport = kwargs.get(
        "standings_export",
        StandingsExport(
            fields=[
                StandingsExport.Field[f]
                for f in ruleset_cls.DEFAULT_STANDINGS_FIELDS
            ]
        ),
    )
    for key, default in self.GAME_FIELDS.items():
        setattr(self, key, kwargs.get(key, copy.deepcopy(default)))
    # Scoring logic selection - see src/logic/commander/scoring.py.
    # scoring_params is opaque here: field names and defaults belong to
    # whichever IScoringLogic class scoring_logic names (its
    # DEFAULT_PARAMS), not to TournamentConfiguration - see
    # IScoringLogic.params()/._param().
    self.scoring_logic: str = kwargs.get(
        "scoring_logic", ruleset_cls.DEFAULT_SCORING_LOGIC
    )
    self.scoring_params: dict[str, Any] = dict(kwargs.get("scoring_params", {}))
    # Per Swiss round, the pairing configuration: one dict per round,
    # {"logic": <name or None>, "params": {<param>: value}, "ruleset_params":
    # {<param>: value}}. logic None (or a missing/short list) falls back to
    # the adaptive default in Tournament.__compute_stage_and_logic.
    # Top-cut rounds ignore this. params/ruleset_params are opaque here -
    # field names and defaults belong to each pairing class / the
    # ruleset (their DEFAULT_PARAMS), see CommonPairing.params() /
    # IRuleset.params().
    self.pairing_rounds: list[dict[str, Any]] = self._build_pairing_rounds(kwargs)
    # Deprecated (read, never written): old logs used a snake_pods flag
    # to choose round 2's Swiss pairing. Round 2 is always PairingSnake
    # now (CommanderRuleset.swiss_pairing_logic), so an old file with
    # snake_pods: false must pin round 2 to PairingDefault explicitly to
    # keep its saved behavior.
    if kwargs.get("snake_pods") is False:
        while len(self.pairing_rounds) < 2:
            self.pairing_rounds.append(
                {"logic": None, "params": {}, "ruleset_params": {}}
            )
        if self.pairing_rounds[1]["logic"] is None:
            self.pairing_rounds[1]["logic"] = "PairingDefault"
    # Ruleset param overrides per playoff stage: {stage_value:
    # {"ruleset_params": {...}}}. Stages not in the current playoff plan
    # are ignored. See ruleset_overrides.
    self.playoff_rounds: dict[int, dict[str, Any]] = {
        int(k): dict(v) for k, v in kwargs.get("playoff_rounds", {}).items()
    }

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.

pairing_logics property

pairing_logics: list[str | None]

The pairing-logic name per round (None where adaptive).

pairing_params property

pairing_params: list[dict[str, Any]]

The pairing params per round, aligned with pairing_logics.

ruleset_overrides

ruleset_overrides(tour_round: IRound) -> Mapping[str, Any]

This round's ruleset param overrides (see IRuleset.params()).

Source code in src/core.py
800
801
802
803
804
805
806
807
808
def ruleset_overrides(self, tour_round: IRound) -> Mapping[str, Any]:
    """This round's ruleset param overrides (see IRuleset.params())."""
    if tour_round.stage == Round.Stage.SWISS:
        seq = tour_round.seq
        rounds = self.pairing_rounds
        return rounds[seq].get("ruleset_params", {}) if seq < len(rounds) else {}
    return self.playoff_rounds.get(tour_round.stage.value, {}).get(
        "ruleset_params", {}
    )

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
169
170
171
172
173
174
175
176
177
178
179
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