def test_advance_time_with_delays():
    period = build_match_period(0, 50)
    delays = [
        Delay(time=1, delay=1),
        Delay(time=5, delay=2),
    ]
    clock = MatchPeriodClock(period, delays)
    curr_time = clock.current_time
    assert 0 == curr_time, "Should start at the start of the period"

    clock.advance_time(1)  # plus a delay of 2 at time=1

    curr_time = clock.current_time
    assert 2 == curr_time, "Time should advance by the given amount (1)" \
                           " plus the size of the delay it meets"

    clock.advance_time(2)

    curr_time = clock.current_time
    assert 4 == curr_time, "Time should advance by the given amount (2)" \
                           " only; there are no intervening delays"

    clock.advance_time(2)  # takes us to 6, plus a delay of 2 at time=5

    curr_time = clock.current_time
    assert 8 == curr_time, "Time should advance by the given amount (2)" \
                           " plus the size of the intervening delay (2)"

    clock.advance_time(2)

    curr_time = clock.current_time
    assert 10 == curr_time, "Time should advance by the given amount (2)" \
                            " only; there are no intervening delays"
def test_current_time_beyond_max_end_with_delay():
    period = build_match_period(0, 1, 2)
    clock = MatchPeriodClock(period, [Delay(time=1, delay=2)])

    clock.advance_time(1)
    # now at 3
    check_out_of_time(clock)
def test_current_time_beyond_end_with_delay():
    period = build_match_period(0, 1)
    clock = MatchPeriodClock(period, [Delay(time=1, delay=1)])

    clock.advance_time(1)
    # now at 2
    check_out_of_time(clock)
def test_current_time_at_end_no_delay():
    period = build_match_period(0, 1)
    clock = MatchPeriodClock(period, [])

    clock.advance_time(1)

    curr_time = clock.current_time
    assert 1 == curr_time, "Should be able to query time when at end"
def test_current_time_at_end_no_delay():
    period = build_match_period(0, 1)
    clock = MatchPeriodClock(period, [])

    clock.advance_time(1)

    curr_time = clock.current_time
    assert 1 == curr_time, "Should be able to query time when at end"
def test_current_time_at_max_end_no_delay():
    period = build_match_period(0, 1, 2)
    clock = MatchPeriodClock(period, [])

    clock.advance_time(2)

    check_out_of_time(clock, "Should be out of time when at max_end due" \
                             " to over-advancing")
def test_current_time_at_max_end_no_delay():
    period = build_match_period(0, 1, 2)
    clock = MatchPeriodClock(period, [])

    clock.advance_time(2)

    check_out_of_time(clock, "Should be out of time when at max_end due" \
                             " to over-advancing")
def test_current_time_at_max_end_with_delay():
    period = build_match_period(0, 1, 2)
    clock = MatchPeriodClock(period, [Delay(time=1, delay=1)])

    clock.advance_time(1)

    curr_time = clock.current_time
    assert 2 == curr_time, "Should be able to query time when at max_end" \
                           " due to delays"
def test_current_time_at_max_end_with_delay():
    period = build_match_period(0, 1, 2)
    clock = MatchPeriodClock(period, [Delay(time=1, delay=1)])

    clock.advance_time(1)

    curr_time = clock.current_time
    assert 2 == curr_time, "Should be able to query time when at max_end" \
                           " due to delays"
Exemple #10
0
    def _build_matchlist(self, yamldata):
        """Build the match list."""
        self.matches = []
        if yamldata is None:
            self.n_planned_league_matches = 0
            return

        match_numbers = sorted(yamldata.keys())
        self.n_planned_league_matches = len(match_numbers)

        if tuple(match_numbers) != tuple(range(len(match_numbers))):
            raise Exception("Matches are not a complete 0-N range")

        # Effectively just the .values(), except that it's ordered by number
        raw_matches = [yamldata[m] for m in match_numbers]

        match_n = 0

        for period in self.match_periods:
            # Fill this period with matches

            clock = MatchPeriodClock(period, self.delays)

            # No extra spacing for matches at the start of a period

            # Fill this match period with matches
            for start in clock.iterslots(self.match_duration):
                try:
                    arenas = raw_matches.pop(0)
                except IndexError:
                    # no more matches left
                    break

                m = {}

                end_time = start + self.match_duration
                for arena_name, teams in arenas.items():
                    teams = self.remove_drop_outs(teams, match_n)
                    display_name = 'Match {n}'.format(n=match_n)
                    match = Match(match_n,
                                  display_name,
                                  arena_name,
                                  teams,
                                  start,
                                  end_time,
                                  MatchType.league,
                                  use_resolved_ranking=False)
                    m[arena_name] = match

                period.matches.append(m)
                self.matches.append(m)

                match_n += 1

                extra_spacing = self._spacing.get(match_n)
                if extra_spacing:
                    clock.advance_time(extra_spacing)
def test_slots_delay_after():
    period = build_match_period(0, 4)
    clock = MatchPeriodClock(period, [Delay(time=6, delay=2)])

    curr_time = clock.current_time
    assert 0 == curr_time, "Should start at the start of the period"

    slots = list(clock.iterslots(1))
    expected = list(range(5))
    assert expected == slots
def test_slots_delay_after():
    period = build_match_period(0, 4)
    clock = MatchPeriodClock(period, [Delay(time=6, delay=2)])

    curr_time = clock.current_time
    assert 0 == curr_time, "Should start at the start of the period"

    slots = list(clock.iterslots(1))
    expected = list(range(5))
    assert expected == slots
def test_advance_time_with_delays():
    period = build_match_period(0, 50)
    delays = [
        Delay(time=1, delay=1),
        Delay(time=5, delay=2),
    ]
    clock = MatchPeriodClock(period, delays)
    curr_time = clock.current_time
    assert 0 == curr_time, "Should start at the start of the period"

    clock.advance_time(1) # plus a delay of 2 at time=1

    curr_time = clock.current_time
    assert 2 == curr_time, "Time should advance by the given amount (1)" \
                           " plus the size of the delay it meets"

    clock.advance_time(2)

    curr_time = clock.current_time
    assert 4 == curr_time, "Time should advance by the given amount (2)" \
                           " only; there are no intervening delays"

    clock.advance_time(2) # takes us to 6, plus a delay of 2 at time=5

    curr_time = clock.current_time
    assert 8 == curr_time, "Time should advance by the given amount (2)" \
                           " plus the size of the intervening delay (2)"

    clock.advance_time(2)

    curr_time = clock.current_time
    assert 10 == curr_time, "Time should advance by the given amount (2)" \
                            " only; there are no intervening delays"
Exemple #14
0
    def _build_matchlist(self, yamldata):
        """Build the match list."""
        self.matches = []
        if yamldata is None:
            self.n_planned_league_matches = 0
            return

        match_numbers = sorted(yamldata.keys())
        self.n_planned_league_matches = len(match_numbers)

        if tuple(match_numbers) != tuple(range(len(match_numbers))):
            raise Exception("Matches are not a complete 0-N range")

        # Effectively just the .values(), except that it's ordered by number
        raw_matches = [yamldata[m] for m in match_numbers]

        match_n = 0

        for period in self.match_periods:
            # Fill this period with matches

            clock = MatchPeriodClock(period, self.delays)

            # No extra spacing for matches at the start of a period

            # Fill this match period with matches
            for start in clock.iterslots(self.match_duration):
                try:
                    arenas = raw_matches.pop(0)
                except IndexError:
                    # no more matches left
                    break

                m = {}

                end_time = start + self.match_duration
                for arena_name, teams in arenas.items():
                    teams = self.remove_drop_outs(teams, match_n)
                    display_name = 'Match {n}'.format(n=match_n)
                    match = Match(match_n, display_name, arena_name, teams,
                                  start, end_time, MatchType.league,
                                  use_resolved_ranking=False)
                    m[arena_name] = match

                period.matches.append(m)
                self.matches.append(m)

                match_n += 1

                extra_spacing = self._spacing.get(match_n)
                if extra_spacing:
                    clock.advance_time(extra_spacing)
def test_slots_extra_gap():
    period = build_match_period(0, 6)
    clock = MatchPeriodClock(period, [])
    slots = []
    first_time = True
    for start in clock.iterslots(2):
        slots.append(clock.current_time)
        if first_time:
            # Advance an extra 3 the first time
            clock.advance_time(3)
            # Now at 5
            first_time = False
    expected = [0, 5]
    assert expected == slots
def test_slots_extra_gap():
    period = build_match_period(0, 6)
    clock = MatchPeriodClock(period, [])
    slots = []
    first_time = True
    for start in clock.iterslots(2):
        slots.append(clock.current_time)
        if first_time:
            # Advance an extra 3 the first time
            clock.advance_time(3)
            # Now at 5
            first_time = False
    expected = [0, 5]
    assert expected == slots
def test_advance_time_overlapping_delays():
    period = build_match_period(0, 10)
    delays = [
        Delay(time=1, delay=2),  # from 1 -> 3
        Delay(time=2, delay=1),  # extra at 2, so 1 -> 4
    ]
    clock = MatchPeriodClock(period, delays)
    curr_time = clock.current_time
    assert 0 == curr_time, "Should start at the start of the period"

    clock.advance_time(2)  # plus a total delay of 3

    curr_time = clock.current_time
    assert 5 == curr_time, "Time should advance by the given amount (2)" \
                           " plus the size of the intervening delays (1+2)"
def test_advance_time_overlapping_delays():
    period = build_match_period(0, 10)
    delays = [
        Delay(time=1, delay=2), # from 1 -> 3
        Delay(time=2, delay=1), # extra at 2, so 1 -> 4
    ]
    clock = MatchPeriodClock(period, delays)
    curr_time = clock.current_time
    assert 0 == curr_time, "Should start at the start of the period"

    clock.advance_time(2) # plus a total delay of 3

    curr_time = clock.current_time
    assert 5 == curr_time, "Time should advance by the given amount (2)" \
                           " plus the size of the intervening delays (1+2)"
def test_advance_time_touching_delays():
    period = build_match_period(0, 10)
    delays = [
        Delay(time=1, delay=1), # from 1 -> 2
        Delay(time=2, delay=1), # from 2 -> 3
    ]
    clock = MatchPeriodClock(period, delays)
    curr_time = clock.current_time
    assert 0 == curr_time, "Should start at the start of the period"

    clock.advance_time(2) # plus a total delay of 2

    curr_time = clock.current_time
    assert 4 == curr_time, "Time should advance by the given amount (2)" \
                           " plus the size of the intervening delays (1+1)"
def test_advance_time_touching_delays():
    period = build_match_period(0, 10)
    delays = [
        Delay(time=1, delay=1),  # from 1 -> 2
        Delay(time=2, delay=1),  # from 2 -> 3
    ]
    clock = MatchPeriodClock(period, delays)
    curr_time = clock.current_time
    assert 0 == curr_time, "Should start at the start of the period"

    clock.advance_time(2)  # plus a total delay of 2

    curr_time = clock.current_time
    assert 4 == curr_time, "Time should advance by the given amount (2)" \
                           " plus the size of the intervening delays (1+1)"
def test_advance_time_no_delays():
    period = build_match_period(0, 10)
    clock = MatchPeriodClock(period, [])
    curr_time = clock.current_time
    assert 0 == curr_time, "Should start at the start of the period"

    clock.advance_time(1)

    curr_time = clock.current_time
    assert 1 == curr_time, "Time should advance by the given amount (1)"

    clock.advance_time(2)

    curr_time = clock.current_time
    assert 3 == curr_time, "Time should advance by the given amount (2)"
    def add_knockouts(self):
        """Add the knockouts to the schedule."""

        period = self.config["match_periods"]["knockout"][0]

        self.period = MatchPeriod(period["start_time"], period["end_time"],
                                  period["end_time"], period["description"],
                                  [], MatchType.knockout)

        self.clock = MatchPeriodClock(self.period, self.schedule.delays)

        knockout_conf = self.config["knockout"]
        round_spacing = timedelta(seconds=knockout_conf["round_spacing"])

        self._add_first_round(conf_arity=knockout_conf.get('arity'))

        while len(self.knockout_rounds[-1]) > 1:

            # Add the delay between rounds
            self.clock.advance_time(round_spacing)

            # Number of rounds remaining to be added
            rounds_remaining = self.get_rounds_remaining(self.knockout_rounds[-1])

            if rounds_remaining <= knockout_conf["single_arena"]["rounds"]:
                arenas = knockout_conf["single_arena"]["arenas"]
            else:
                arenas = self.arenas

            if len(self.knockout_rounds[-1]) == 2:
                "Extra delay before the final match"
                final_delay = timedelta(seconds=knockout_conf["final_delay"])
                self.clock.advance_time(final_delay)

            self._add_round(arenas, rounds_remaining - 1)
def test_current_time_start():
    period = build_match_period(0, 4)
    clock = MatchPeriodClock(period, [])

    curr_time = clock.current_time

    assert 0 == curr_time, "Should start at the start of the period"
def test_current_time_start_delayed():
    period = build_match_period(0, 4)
    clock = MatchPeriodClock(period, [Delay(time=0, delay=1)])

    curr_time = clock.current_time

    assert 1 == curr_time, "Start time should include delays"
def test_current_time_start_delayed_twice():
    period = build_match_period(0, 10)
    delays = [
        Delay(time=0, delay=2),
        Delay(time=1, delay=3),
    ]
    clock = MatchPeriodClock(period, delays)

    curr_time = clock.current_time

    assert 5 == curr_time, "Start time should include cumilative delays"
    def add_knockouts(self):
        """Add the knockouts to the schedule."""

        period = self.config["match_periods"]["knockout"][0]

        self.period = MatchPeriod(period["start_time"], period["end_time"],
                                  period["end_time"], period["description"],
                                  [], MatchType.knockout)

        self.clock = MatchPeriodClock(self.period, self.schedule.delays)

        knockout_conf = self.config["knockout"]
        round_spacing = timedelta(seconds=knockout_conf["round_spacing"])

        self._add_first_round(conf_arity=knockout_conf.get('arity'))

        while len(self.knockout_rounds[-1]) > 1:

            # Add the delay between rounds
            self.clock.advance_time(round_spacing)

            # Number of rounds remaining to be added
            rounds_remaining = self.get_rounds_remaining(
                self.knockout_rounds[-1])

            if rounds_remaining <= knockout_conf["single_arena"]["rounds"]:
                arenas = knockout_conf["single_arena"]["arenas"]
            else:
                arenas = self.arenas

            if len(self.knockout_rounds[-1]) == 2:
                "Extra delay before the final match"
                final_delay = timedelta(seconds=knockout_conf["final_delay"])
                self.clock.advance_time(final_delay)

            self._add_round(arenas, rounds_remaining - 1)
def test_advance_time_no_delays():
    period = build_match_period(0, 10)
    clock = MatchPeriodClock(period, [])
    curr_time = clock.current_time
    assert 0 == curr_time, "Should start at the start of the period"

    clock.advance_time(1)

    curr_time = clock.current_time
    assert 1 == curr_time, "Time should advance by the given amount (1)"

    clock.advance_time(2)

    curr_time = clock.current_time
    assert 3 == curr_time, "Time should advance by the given amount (2)"
Exemple #28
0
    def delay_at(self, date):
        """
        Calculates the active delay at a given ``date``. Intended for use
        only in exposing the current delay value -- scheduling should be
        done using a :class:`.MatchPeriodClock` instead.

        :param datetime date: The date to find the delay for.
        :return: A :class:`datetime.timedelta` specifying the active delay.
        """

        total = timedelta()
        period = self.period_at(date)
        if not period:
            # No current period, no delays active
            return total

        delays = MatchPeriodClock.delays_for_period(period, self.delays)
        for delay in delays:
            if delay.time > date:
                break

            total += delay.delay

        return total
Exemple #29
0
    def delay_at(self, date):
        """
        Calculates the active delay at a given ``date``. Intended for use
        only in exposing the current delay value -- scheduling should be
        done using a :class:`.MatchPeriodClock` instead.

        :param datetime date: The date to find the delay for.
        :return: A :class:`datetime.timedelta` specifying the active delay.
        """

        total = timedelta()
        period = self.period_at(date)
        if not period:
            # No current period, no delays active
            return total

        delays = MatchPeriodClock.delays_for_period(period, self.delays)
        for delay in delays:
            if delay.time > date:
                break

            total += delay.delay

        return total
class KnockoutScheduler(object):
    """
    A class that can be used to generate a knockout schedule.

    :param schedule: The league schedule.
    :param scores: The scores.
    :param dict arenas: The arenas.
    :param dict teams: The teams.
    :param config: Custom configuration for the knockout scheduler.
    """

    def __init__(self, schedule, scores, arenas, teams, config):
        self.schedule = schedule
        self.scores = scores
        self.arenas = arenas
        self.teams = teams
        self.config = config

        # The knockout matches appear in the normal matches list
        # but this list provides them in groups of rounds.
        # e.g. self.knockout_rounds[-2] gives the semi-final matches
        # and self.knockout_rounds[-1] gives the final match (in a list)
        # Note that the ordering of the matches within the rounds
        # in this list is important (e.g. self.knockout_rounds[0][0] is
        # will involve the top seed, whilst self.knockout_rounds[0][-1] will
        # involve the second seed).
        self.knockout_rounds = []

        self.R = stable_random.Random()

    def _played_all_league_matches(self):
        """
        Check if all league matches have been played.

        :return: :py:bool:`True` if we've played all league matches.
        """

        for arena_matches in self.schedule.matches:
            for match in arena_matches.values():
                if match.type != MatchType.league:
                    continue

                if (match.arena, match.num) not in \
                        self.scores.league.game_points:
                    return False

        return True

    @staticmethod
    def get_match_display_name(rounds_remaining, round_num, global_num):
        """
        Get a human-readable match display name.

        :param rounds_remaining: The number of knockout rounds remaining.
        :param knockout_num: The match number within the knockout round.
        :param global_num: The global match number.
        """

        if rounds_remaining == 0:
            display_name = 'Final (#{global_num})'
        elif rounds_remaining == 1:
            display_name = 'Semi {round_num} (#{global_num})'
        elif rounds_remaining == 2:
            display_name = 'Quarter {round_num} (#{global_num})'
        else:
            display_name = 'Match {global_num}'
        return display_name.format(round_num=round_num + 1,
                                   global_num=global_num)

    def _add_round_of_matches(self, matches, arenas, rounds_remaining):
        """
        Add a whole round of matches.

        :param list matches: A list of lists of teams for each match.
        """

        self.knockout_rounds += [[]]

        round_num = 0
        while len(matches):
            # Deliberately not using iterslots since we need to ensure
            # that the time advances even after we've run out of matches
            start_time = self.clock.current_time
            end_time = start_time + self.schedule.match_duration

            new_matches = {}
            for arena in arenas:
                teams = matches.pop(0)

                if len(teams) < 4:
                    "Fill empty zones with None"
                    teams += [None] * (4 - len(teams))

                # Randomise the zones
                self.R.shuffle(teams)

                num = len(self.schedule.matches)
                display_name = self.get_match_display_name(rounds_remaining,
                                                           round_num, num)

                match = Match(num, display_name, arena, teams,
                              start_time, end_time, MatchType.knockout,
                              # Just the finals don't use the resolved ranking
                              use_resolved_ranking = rounds_remaining != 0)

                self.knockout_rounds[-1].append(match)
                new_matches[arena] = match

                if len(matches) == 0:
                    break

            self.clock.advance_time(self.schedule.match_duration)
            self.schedule.matches.append(new_matches)
            self.period.matches.append(new_matches)

            round_num += 1

    def get_ranking(self, game):
        """
        Get a ranking of the given match's teams.

        :param game: A game.
        """
        desc = (game.arena, game.num)

        # Get the resolved positions if present (will be a tla -> position map)
        positions = self.scores.knockout.resolved_positions.get(desc, None)

        if positions is None:
            "Given match hasn't been scored yet"
            return [UNKNOWABLE_TEAM] * 4

        # Extract just TLAs
        return list(positions.keys())

    def get_winners(self, game):
        """
        Find the parent match's winners.

        :param game: A game.
        """

        ranking = self.get_ranking(game)
        return ranking[:2]

    def _add_round(self, arenas, rounds_remaining):
        prev_round = self.knockout_rounds[-1]
        matches = []

        for i in range(0, len(prev_round), 2):
            winners = []
            for parent in prev_round[i:i + 2]:
                winners += self.get_winners(parent)

            matches.append(winners)

        self._add_round_of_matches(matches, arenas, rounds_remaining)

    def _get_non_dropped_out_teams(self, for_match):
        teams = list(self.scores.league.positions.keys())
        teams = [tla for tla in teams
                 if self.teams[tla].is_still_around(for_match)]
        return teams

    def _add_first_round(self, conf_arity=None):
        next_match_num = len(self.schedule.matches)
        teams = self._get_non_dropped_out_teams(next_match_num)
        if not self._played_all_league_matches():
            teams = [UNKNOWABLE_TEAM] * len(teams)

        arity = len(teams)
        if conf_arity is not None and conf_arity < arity:
            arity = conf_arity

        # Seed the random generator with the seeded team list
        # This makes it unpredictable which teams will be in which zones
        # until the league scores have been established
        self.R.seed("".join(teams).encode("utf-8"))

        matches = []

        for seeds in knockout.first_round_seeding(arity):
            match_teams = [teams[seed] for seed in seeds]
            matches.append(match_teams)

        rounds_remaining = self.get_rounds_remaining(matches)
        self._add_round_of_matches(matches, self.arenas, rounds_remaining)

    @staticmethod
    def get_rounds_remaining(prev_matches):
        return int(math.log(len(prev_matches), 2))

    def add_knockouts(self):
        """Add the knockouts to the schedule."""

        period = self.config["match_periods"]["knockout"][0]

        self.period = MatchPeriod(period["start_time"], period["end_time"],
                                  period["end_time"], period["description"],
                                  [], MatchType.knockout)

        self.clock = MatchPeriodClock(self.period, self.schedule.delays)

        knockout_conf = self.config["knockout"]
        round_spacing = timedelta(seconds=knockout_conf["round_spacing"])

        self._add_first_round(conf_arity=knockout_conf.get('arity'))

        while len(self.knockout_rounds[-1]) > 1:

            # Add the delay between rounds
            self.clock.advance_time(round_spacing)

            # Number of rounds remaining to be added
            rounds_remaining = self.get_rounds_remaining(self.knockout_rounds[-1])

            if rounds_remaining <= knockout_conf["single_arena"]["rounds"]:
                arenas = knockout_conf["single_arena"]["arenas"]
            else:
                arenas = self.arenas

            if len(self.knockout_rounds[-1]) == 2:
                "Extra delay before the final match"
                final_delay = timedelta(seconds=knockout_conf["final_delay"])
                self.clock.advance_time(final_delay)

            self._add_round(arenas, rounds_remaining - 1)
class KnockoutScheduler(object):
    """
    A class that can be used to generate a knockout schedule.

    :param schedule: The league schedule.
    :param scores: The scores.
    :param dict arenas: The arenas.
    :param dict teams: The teams.
    :param config: Custom configuration for the knockout scheduler.
    """
    def __init__(self, schedule, scores, arenas, teams, config):
        self.schedule = schedule
        self.scores = scores
        self.arenas = arenas
        self.teams = teams
        self.config = config

        # The knockout matches appear in the normal matches list
        # but this list provides them in groups of rounds.
        # e.g. self.knockout_rounds[-2] gives the semi-final matches
        # and self.knockout_rounds[-1] gives the final match (in a list)
        # Note that the ordering of the matches within the rounds
        # in this list is important (e.g. self.knockout_rounds[0][0] is
        # will involve the top seed, whilst self.knockout_rounds[0][-1] will
        # involve the second seed).
        self.knockout_rounds = []

        self.R = stable_random.Random()

    def _played_all_league_matches(self):
        """
        Check if all league matches have been played.

        :return: :py:bool:`True` if we've played all league matches.
        """

        for arena_matches in self.schedule.matches:
            for match in arena_matches.values():
                if match.type != MatchType.league:
                    continue

                if (match.arena, match.num) not in \
                        self.scores.league.game_points:
                    return False

        return True

    @staticmethod
    def get_match_display_name(rounds_remaining, round_num, global_num):
        """
        Get a human-readable match display name.

        :param rounds_remaining: The number of knockout rounds remaining.
        :param knockout_num: The match number within the knockout round.
        :param global_num: The global match number.
        """

        if rounds_remaining == 0:
            display_name = 'Final (#{global_num})'
        elif rounds_remaining == 1:
            display_name = 'Semi {round_num} (#{global_num})'
        elif rounds_remaining == 2:
            display_name = 'Quarter {round_num} (#{global_num})'
        else:
            display_name = 'Match {global_num}'
        return display_name.format(round_num=round_num + 1,
                                   global_num=global_num)

    def _add_round_of_matches(self, matches, arenas, rounds_remaining):
        """
        Add a whole round of matches.

        :param list matches: A list of lists of teams for each match.
        """

        self.knockout_rounds += [[]]

        round_num = 0
        while len(matches):
            # Deliberately not using iterslots since we need to ensure
            # that the time advances even after we've run out of matches
            start_time = self.clock.current_time
            end_time = start_time + self.schedule.match_duration

            new_matches = {}
            for arena in arenas:
                teams = matches.pop(0)

                if len(teams) < 4:
                    "Fill empty zones with None"
                    teams += [None] * (4 - len(teams))

                # Randomise the zones
                self.R.shuffle(teams)

                num = len(self.schedule.matches)
                display_name = self.get_match_display_name(
                    rounds_remaining, round_num, num)

                match = Match(
                    num,
                    display_name,
                    arena,
                    teams,
                    start_time,
                    end_time,
                    MatchType.knockout,
                    # Just the finals don't use the resolved ranking
                    use_resolved_ranking=rounds_remaining != 0)

                self.knockout_rounds[-1].append(match)
                new_matches[arena] = match

                if len(matches) == 0:
                    break

            self.clock.advance_time(self.schedule.match_duration)
            self.schedule.matches.append(new_matches)
            self.period.matches.append(new_matches)

            round_num += 1

    def get_ranking(self, game):
        """
        Get a ranking of the given match's teams.

        :param game: A game.
        """
        desc = (game.arena, game.num)

        # Get the resolved positions if present (will be a tla -> position map)
        positions = self.scores.knockout.resolved_positions.get(desc, None)

        if positions is None:
            "Given match hasn't been scored yet"
            return [UNKNOWABLE_TEAM] * 4

        # Extract just TLAs
        return list(positions.keys())

    def get_winners(self, game):
        """
        Find the parent match's winners.

        :param game: A game.
        """

        ranking = self.get_ranking(game)
        return ranking[:2]

    def _add_round(self, arenas, rounds_remaining):
        prev_round = self.knockout_rounds[-1]
        matches = []

        for i in range(0, len(prev_round), 2):
            winners = []
            for parent in prev_round[i:i + 2]:
                winners += self.get_winners(parent)

            matches.append(winners)

        self._add_round_of_matches(matches, arenas, rounds_remaining)

    def _get_non_dropped_out_teams(self, for_match):
        teams = list(self.scores.league.positions.keys())
        teams = [
            tla for tla in teams if self.teams[tla].is_still_around(for_match)
        ]
        return teams

    def _add_first_round(self, conf_arity=None):
        next_match_num = len(self.schedule.matches)
        teams = self._get_non_dropped_out_teams(next_match_num)
        if not self._played_all_league_matches():
            teams = [UNKNOWABLE_TEAM] * len(teams)

        arity = len(teams)
        if conf_arity is not None and conf_arity < arity:
            arity = conf_arity

        # Seed the random generator with the seeded team list
        # This makes it unpredictable which teams will be in which zones
        # until the league scores have been established
        self.R.seed("".join(teams).encode("utf-8"))

        matches = []

        for seeds in knockout.first_round_seeding(arity):
            match_teams = [teams[seed] for seed in seeds]
            matches.append(match_teams)

        rounds_remaining = self.get_rounds_remaining(matches)
        self._add_round_of_matches(matches, self.arenas, rounds_remaining)

    @staticmethod
    def get_rounds_remaining(prev_matches):
        return int(math.log(len(prev_matches), 2))

    def add_knockouts(self):
        """Add the knockouts to the schedule."""

        period = self.config["match_periods"]["knockout"][0]

        self.period = MatchPeriod(period["start_time"], period["end_time"],
                                  period["end_time"], period["description"],
                                  [], MatchType.knockout)

        self.clock = MatchPeriodClock(self.period, self.schedule.delays)

        knockout_conf = self.config["knockout"]
        round_spacing = timedelta(seconds=knockout_conf["round_spacing"])

        self._add_first_round(conf_arity=knockout_conf.get('arity'))

        while len(self.knockout_rounds[-1]) > 1:

            # Add the delay between rounds
            self.clock.advance_time(round_spacing)

            # Number of rounds remaining to be added
            rounds_remaining = self.get_rounds_remaining(
                self.knockout_rounds[-1])

            if rounds_remaining <= knockout_conf["single_arena"]["rounds"]:
                arenas = knockout_conf["single_arena"]["arenas"]
            else:
                arenas = self.arenas

            if len(self.knockout_rounds[-1]) == 2:
                "Extra delay before the final match"
                final_delay = timedelta(seconds=knockout_conf["final_delay"])
                self.clock.advance_time(final_delay)

            self._add_round(arenas, rounds_remaining - 1)
def test_current_time_beyond_end_no_delay():
    period = build_match_period(0, 1)
    clock = MatchPeriodClock(period, [])

    clock.advance_time(5)
    check_out_of_time(clock)
def test_slots_no_delays_2():
    period = build_match_period(0, 4)
    clock = MatchPeriodClock(period, [])
    slots = list(clock.iterslots(2))
    expected = [0, 2, 4]
    assert expected == slots
def test_slots_no_delays_2():
    period = build_match_period(0, 4)
    clock = MatchPeriodClock(period, [])
    slots = list(clock.iterslots(2))
    expected = [0, 2, 4]
    assert expected == slots
def test_current_time_beyond_max_end_no_delay():
    period = build_match_period(0, 1, 2)
    clock = MatchPeriodClock(period, [])

    clock.advance_time(5)
    check_out_of_time(clock)
def test_slots_delay_during():
    period = build_match_period(0, 4, 5)
    clock = MatchPeriodClock(period, [Delay(time=1, delay=3)])
    slots = list(clock.iterslots(2))
    expected = [0, 5]
    assert expected == slots
def test_slots_no_delays_1():
    period = build_match_period(0, 4)
    clock = MatchPeriodClock(period, [])
    slots = list(clock.iterslots(1))
    expected = list(range(5))
    assert expected == slots
def test_slots_delay_during():
    period = build_match_period(0, 4, 5)
    clock = MatchPeriodClock(period, [Delay(time=1, delay=3)])
    slots = list(clock.iterslots(2))
    expected = [0, 5]
    assert expected == slots
def test_slots_no_delays_1():
    period = build_match_period(0, 4)
    clock = MatchPeriodClock(period, [])
    slots = list(clock.iterslots(1))
    expected = list(range(5))
    assert expected == slots