happy5214/competitions-cup

View on GitHub
competitions/cup/__init__.py

Summary

Maintainability
A
2 hrs
Test Coverage
# -*- coding: utf-8  -*-
"""Cup package."""

# Copyright (C) 2015 Alexander Jones
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from __future__ import unicode_literals

from competitions.match import Match


def init_nested_list(count):
    """Initialize an empty nested list.

    @param count: The length of the list
    @type count: int
    @return: A nested list
    @rtype: list
    """
    return [[] for __ in range(count)]


class Bracket(object):

    """Base class for tournament brackets."""

    def __init__(self, match_class=Match):
        """Constructor."""
        self.winner = None
        self.matches = []
        self.index = [0, -1]
        self.MatchClass = match_class

    def _build_bracket(self):
        """Build the nested list representing the bracket."""
        raise NotImplementedError

    def _assign_winner(self, winner):
        """Assign winner to their next match."""
        raise NotImplementedError

    def _set_current_match(self):
        """Set the current match."""
        self.index[1] += 1
        round = self.matches[self.index[0]]
        match = None
        try:
            match = round[self.index[1]]
        except IndexError:
            self.index[0] += 1
            self.index[1] = 0
            round = self.matches[self.index[0]]
            match = round[0]
        self.current_match = match

    def play_match(self):
        """Play a cup match.

        @return: The winner of the simulated match
        @raise CupFinished: If the cup is finished
        """
        self._set_current_match()
        winner = None
        while not winner:
            self.current_match.play()
            winner = self.current_match.winner
        try:
            self._assign_winner(winner)
        except IndexError:
            self.winner = winner
            raise CupFinished(winner)

        return winner

    @property
    def round_over(self):
        """Whether the current round is over.

        @return: Whether the current round is over
        @rtype: bool
        """
        return (self.index[1] + 1) >= len(self.matches[self.index[0]])


class PrintableBracket(Bracket):

    """Mixin for a bracket printable to the console."""

    def _generate_layout(self):
        """Generate the bracket layout for display."""
        raise NotImplementedError

    def _match_for_layout(self, test, round_num, match_num, first_team):
        """Generate the layout entry for a match to place in a displayed bracket.

        @param test: Whether to print a team or a blank space
        @type test: bool
        @param round_num: The round number in self.matches
        @type round_num: int
        @param match_num: The match number for the round in self.matches
        @type match_num: int
        @param first_team: Whether to return the string for the first team
        @type first_team: bool
        @return: ((match_num, new first_team), (match, old first_team))
        @rtype: tuple
        """
        if test:
            match = self.matches[round_num][match_num]
            if match.is_walkover:
                return (((match_num, False), (None, None)) if first_team
                        else ((match_num + 1, True), (None, None)))
            else:
                return (((match_num, False), (match, 1)) if first_team
                        else ((match_num + 1, True), (match, 2)))
        else:
            return ((match_num, first_team), (None, None))

    def _generate_printout(self, layout):
        """Generate the console printout for the bracket."""
        printout = '\n'
        for row in layout:
            for cell in row:
                match, team_num = cell
                if not team_num:
                    team, score = '', ''
                elif team_num == 1:
                    team, score = match.team1, match.score1
                else:
                    team, score = match.team2, match.score2
                printout += '{:<30} {:>4}     '.format(team, score)
            printout += '\n'
        return printout

    def print_cup(self, display=True):
        """Print the cup to a string and (optionally) the console.

        @param display: Whether to print to the console.
        @type display: bool
        @return: The displayed bracket
        @rtype: str
        """
        layout = self._generate_layout()
        printout = self._generate_printout(layout)
        if display:
            print(printout)
        return printout


class StandardBracket(PrintableBracket):

    """A standard bracket."""

    pass


class Cup(Bracket):

    """Base class for knockout cups."""

    def __init__(self, match_class, team_count, teams=[], rounds=0):
        """Constructor.

        @param match_class: The class of the match simulator
        @type match_class: Any Match-like class
        @param team_count: The expected number of teams
        @type team_count: int
        @param teams: The teams in this cup
        @type teams: list
        @param rounds: The number of rounds
        @type rounds: int
        @raise ValueError: If the list of teams has the wrong number of teams
        """
        super(Cup, self).__init__(match_class=match_class)
        if not teams:
            self.teams = list(map(lambda x: 'Team ' + str(x),
                                  range(1, team_count + 1)))
        else:
            self.teams = teams
        if len(self.teams) != team_count:
            raise ValueError('Wrong number of teams')
        self.round_count = rounds
        self._build_bracket()

    @property
    def team_count(self):
        """The number of teams in this cup."""
        return len(self.teams)

    def play_cup(self):
        """Play the whole cup.

        @return: The winner of the cup
        """
        try:
            while True:
                self.play_match()
        except CupFinished as e:
            return e.winner

    def update_teams(self, teams):
        """Update the list of teams and the first-round matches.

        @param teams: The new list of teams
        @type teams: list
        """
        raise NotImplementedError


class PrintableCup(Cup, PrintableBracket):

    """Combo class for a full cup printable to the console."""

    pass


class StandardCup(PrintableCup):

    """A standard cup."""

    pass


class CupFinished(RuntimeError):

    """Exception for cup simulating its final match.

    This exception signals that the cup is over and that no more matches can be
    simulated. The consumer of the cup object should note the winner and move on.
    """

    def __init__(self, winner):
        """Constructor.

        @param winner: The winner of the cup
        @type winner: The type of teams in the cup
        """
        self.winner = winner