TabbycatDebate/tabbycat

View on GitHub
tabbycat/draw/generator/bpelimination.py

Summary

Maintainability
A
0 mins
Test Coverage
F
23%
from django.utils.translation import gettext as _

from .common import BaseBPDrawGenerator, DrawFatalError, DrawUserError, EliminationDrawMixin
from .pairing import BPPairing
from .utils import ispow2


class BaseBPEliminationDrawGenerator(EliminationDrawMixin, BaseBPDrawGenerator):

    requires_even_teams = False
    DEFAULT_OPTIONS = {}

    def _four_way_fold(self, teams, start_rank=0):
        """Returns pairings folded four-way, with room ranks numbered from
        start_rank+1."""
        if len(teams) % 4 != 0:
            raise DrawFatalError("Tried to do a four-way fold with non-multiple of four: %d" % len(teams))

        n = len(teams) // 4  # number of debates
        pools = (teams[0:n], teams[n:2*n][::-1], teams[2*n:3*n], teams[3*n:4*n][::-1])  # Reverse 1 & 3
        pairings = list()
        for i, ts in enumerate(zip(*pools), start=start_rank+1):
            pairing = BPPairing(ts, bracket=0, room_rank=i)
            pairings.append(pairing)
        return pairings

    def _get_advancing_teams(self):
        """Collates the advancing teams from `self.results`, checks them for
        validity, and returns them in a list of lists, ordered by room rank."""
        self.results.sort(key=lambda x: x.room_rank)
        advancing = [pairing.advancing for pairing in self.results]
        advancing_counts = [len(teams) for teams in advancing]
        if advancing_counts.count(0) > 0:
            raise DrawUserError(_("%d debates in the previous round don't have a result.") % advancing_counts.count(0))
        if advancing_counts.count(2) != len(advancing_counts):
            raise DrawUserError(_("%d debates in the previous round don't have exactly two "
                "teams advancing.") % (len(advancing_counts) - advancing_counts.count(2)))
        return advancing


class PartialBPEliminationDrawGenerator(BaseBPEliminationDrawGenerator):
    """For a partial elimination round, i.e., the first elimination round where
    the break is 6*2^n."""

    def make_pairings(self):
        nteams = len(self.teams)
        if nteams % 6 != 0 or not ispow2(nteams // 6):
            # This should have been caught by the draw manager
            raise DrawFatalError("Tried to do a partial elimination draw with invalid break size: %d" % nteams)

        # Take the non-bypassing teams and fold four-way
        # Convention is to label room ranks after the highest ranked team in the
        # room, e.g. in partial octos, the room ranks go from 9 to 12
        start = nteams // 3
        teams = self.teams[start:]
        return self._four_way_fold(teams, start)


class AfterPartialBPEliminationDrawGenerator(BaseBPEliminationDrawGenerator):
    """For the round immediately following a partial elimination round, i.e.,
    the second elimination round where the break size is 6*2^n."""

    requires_prev_results = True

    def make_pairings(self):
        # e.g. if lowest room rank was 9, then 8 teams should bypass
        nbypassing = min([pairing.room_rank for pairing in self.results]) - 1
        if nbypassing % 2 != 0:
            raise DrawUserError(_("The room ranks of the partial elimination round indicate that "
                "an odd number of teams (%(nbypassing)d) bypassed it.") % {'nbypassing': nbypassing})
        ndebates = nbypassing // 2
        if len(self.results) != ndebates:
            raise DrawUserError(_("The room ranks of the partial elimination round indicate "
                "that %(nbypassing)d teams bypassed it, but %(nadvancing)d teams advanced from "
                "it." % {'nbypassing': nbypassing, 'nadvancing': ndebates * 2}))

        # Fold the bypassing teams two-way
        bypassing = self.teams[:nbypassing]
        bypassing_top = bypassing[:ndebates]
        bypassing_bottom = bypassing[ndebates:][::-1]

        # Get (and check) the advancing teams
        advancing = self._get_advancing_teams()

        # Pair them together
        if len(advancing) != ndebates:
            # This should have been caught in one of the errors above
            raise DrawFatalError("%d advancing pairs, but %d debates from bypassing pairs" % (len(advancing), ndebates))

        pairings = list()
        for i, (team1, team2, adv) in enumerate(zip(bypassing_top, bypassing_bottom, advancing), start=1):
            teams = [team1, team2] + adv
            pairing = BPPairing(teams, bracket=0, room_rank=i)
            pairings.append(pairing)
        return pairings


class FirstBPEliminationDrawGenerator(BaseBPEliminationDrawGenerator):
    """For the first elimination round where the break size is 4*2^n."""

    def make_pairings(self):
        nteams = len(self.teams)
        if nteams % 4 != 0 or not ispow2(nteams // 4):
            # This should have been caught by the draw manager
            raise DrawFatalError("Tried to do a first elimination draw with invalid break size: %d" % nteams)

        return self._four_way_fold(self.teams)


class SubsequentBPEliminationDrawGenerator(BaseBPEliminationDrawGenerator):
    """For all elimination rounds after the first one if the break size is
    4*2^n, or after the second one if the break size is 6*2^n."""

    requires_prev_results = True

    def make_pairings(self):
        advancing = self._get_advancing_teams()
        if not (len(advancing) >= 2 and ispow2(len(advancing))):
            raise DrawUserError(_("The number of debates (%d) in the last round is not a "
                "power of two.") % (2 * len(advancing)))

        # Fold the pairs of advancing teams
        ndebates = len(advancing) // 2
        top = advancing[:ndebates]
        bottom = advancing[ndebates:]
        bottom.reverse()
        pairings = list()
        for i, (teams1, teams2) in enumerate(zip(top, bottom), start=1):
            teams = teams1 + teams2  # join lists
            pairing = BPPairing(teams, bracket=0, room_rank=i)
            pairings.append(pairing)
        return pairings