19
\$\begingroup\$

I've created a simulator for the Premier League, which takes in all of the 20 teams and plays them against each other, recording the results and outputting a table at the end.

The skill levels are based off a prediction in an article by The Telegraph.

I am wondering whether or not my code can be improved, condensed or even added to with other features. Very new to coding so expecting lots of repetition/errors.

import math, random

# HIGHER RATED TEAM
higher = 1.148698355
# LOWER RATED TEAM
lower = 0.8705505633


# DEFINING THE TEAM CLASS
class Team:
    def __init__(self, name, skill):
        self.name = name
        self.skill = skill
        self.points = self.gf = self.ga = self.wins = self.draws = self.losses = 0

    def add_goals(self, goals):
        self.gf += goals


# INITIALISING ALL OF THE TEAMS - NAMES AND SKILL LEVELS
arsenal = Team("Arsenal", 16)
aston_villa = Team("Aston Villa", 6)
bournemouth = Team("AFC Bournemouth", 8)
brighton = Team("Brighton and Hove Albion", 5)
burnley = Team("Burnley", 4)
chelsea = Team("Chelsea", 17)
crystal_palace = Team("Crystal Palace", 11)
everton = Team("Everton", 14)
leicester = Team("Leicester City", 12)
liverpool = Team("Liverpool", 19)
man_city = Team("Manchester City", 20)
man_united = Team("Manchester United", 15)
newcastle = Team("Newcastle United", 3)
norwich = Team("Norwich City", 2)
sheffield_united = Team("Sheffield United", 1)
southampton = Team("Southampton", 7)
tottenham = Team("Tottenham Hotspur", 18)
watford = Team("Watford", 9)
west_ham = Team("West Ham United", 10)
wolves = Team("Wolverhampton Wanderers", 13)

# HOME/AWAY TEAMS FOR PLAYING EACH TIME AGAINST EACH OTHER
teams = [arsenal, aston_villa, bournemouth, brighton, burnley, chelsea,
         crystal_palace, everton, leicester, liverpool, man_city, man_united,
         newcastle, norwich, sheffield_united, southampton, tottenham, watford,
         west_ham, wolves]

# PRINT ALL TEAMS' NAME AND SKILL
for team in teams:
    print(team.name, team.skill)

# RANDOM SYSTEM FOR HOME GOALS
def home_score(home, away):
    homeSkill = home.skill / 3
    awaySkill = away.skill / 3

    if homeSkill == awaySkill:
        raise ValueError

    if homeSkill > awaySkill:
        homeGoals = 0
        lambHome = higher ** (homeSkill - awaySkill)
        z = random.random()
        while z > 0:
            z = z - (((lambHome ** homeGoals) * math.exp(-1 * lambHome)) /
                     math.factorial(homeGoals))
            homeGoals += 1
        return (homeGoals - 1)

    if homeSkill < awaySkill:
        homeGoals = 0
        lambHome = higher ** (homeSkill - awaySkill)
        z = random.random()
        while z > 0:
            z = z - (((lambHome ** homeGoals) * math.exp(-1 * lambHome)) /
                     math.factorial(homeGoals))
            homeGoals += 1

        return (homeGoals - 1)

# RANDOM SYSTEM FOR AWAY GOALS
def away_score(home, away):
    homeSkill = home.skill / 3
    awaySkill = away.skill / 3

    if homeSkill == awaySkill:
        return "Teams cannot play themselves!!!"

    if awaySkill > homeSkill:
        awayGoals = 0
        lambAway = lower ** (homeSkill - awaySkill)
        x = random.random()
        while x > 0:
           x = x - (((lambAway ** awayGoals) * math.exp(-1 * lambAway)) /
                    math.factorial(awayGoals))
           awayGoals += 1
        return (awayGoals - 1)

    if awaySkill < homeSkill:
        awayGoals = 0
        lambAway = lower ** (homeSkill - awaySkill)
        x = random.random()
        while x > 0:
           x = x - (((lambAway ** awayGoals) * math.exp(-1 * lambAway)) /
                    math.factorial(awayGoals))
           awayGoals += 1
        return (awayGoals - 1)

# LEAGUE SIZE AND SETTING UP THE LEAGUE
league_size = 20
POINTS = []
GOALS_FOR = []
GOALS_AGAINST = []
WINS =[]
DRAWS = []
LOSSES = []
for x in range(league_size):
    POINTS += [0]
    GOALS_FOR += [0]
    GOALS_AGAINST += [0]
    WINS += [0]
    DRAWS += [0]
    LOSSES += [0]

# PLAYING ALL TEAMS AGAINST EACH OTHER AND UPDATING STATISTICS
for x in range(league_size):
    print("========================================")
    print(teams[x].name + "'s home games: ")
    print("========================================")
    for y in range(league_size):
        error = 0
        try:
            homeScore = home_score(teams[x], teams[y])
        except ValueError:
            pass
            error += 1
        try:
            awayScore = away_score(teams[x], teams[y])
        except ValueError:
            pass
        if error == 0:
            print(teams[x].name, homeScore, ":", awayScore, teams[y].name)
            GOALS_FOR[x] += homeScore
            GOALS_FOR[y] += awayScore
            GOALS_AGAINST[x] += awayScore
            GOALS_AGAINST[y] += homeScore
            if homeScore > awayScore:
                WINS[x] += 1
                LOSSES[y] += 1
                POINTS[x] += 3
            elif homeScore == awayScore:
                DRAWS[x] += 1
                DRAWS[y] += 1
                POINTS[x] += 1
                POINTS[y] += 1
            else:
                WINS[y] += 1
                LOSSES[x] += 1
                POINTS[y] += 3
        else:
            pass

# ASSIGNING STATISTICS TO EACH TEAM
for x in range(league_size):
    teams[x].points = POINTS[x]
    teams[x].gf = GOALS_FOR[x]
    teams[x].ga = GOALS_AGAINST[x]
    teams[x].wins = WINS[x]
    teams[x].draws = DRAWS[x]
    teams[x].losses = LOSSES[x]

sorted_teams = sorted(teams, key=lambda t: t.points, reverse=True)


# PRITNING THE FINAL LEAGUE TABLE
print("| TEAM                      | POINTS | WINS | DRAWS | LOSSES | GOALS FOR | GOALS AGAINST |")
for team in sorted_teams:
    print("|",team.name," "*(24 - len(team.name)),"|  ",team.points," "*(3 - len(str(team.points))),"| ",team.wins," "*(2 - len(str(team.wins))),"|  ",
          team.draws," "*(2 - len(str(team.draws))),"|  ",team.losses," "*(3 - len(str(team.losses))),"|    ",team.gf," "*(4 - len(str(team.gf))),"|     ",
          team.ga," "*(7 - len(str(team.ga))),"|")
\$\endgroup\$
5
  • 9
    \$\begingroup\$ Think there's a typo in your code. It should be arsenal = Team("Arsenal", 20) \$\endgroup\$
    – ediblecode
    Commented Aug 7, 2019 at 9:56
  • 5
    \$\begingroup\$ Spotted the typo - should be arsenal = Team("Arsenal, 1) \$\endgroup\$ Commented Aug 7, 2019 at 10:00
  • 3
    \$\begingroup\$ that hurts buddy \$\endgroup\$
    – ediblecode
    Commented Aug 7, 2019 at 10:03
  • 6
    \$\begingroup\$ @ediblecode could be worse, I ran the code on TIO and it took 120 attempts for Man Utd to win the league... \$\endgroup\$ Commented Aug 7, 2019 at 14:59
  • \$\begingroup\$ @crazyloonybin That's commitment to the cause \$\endgroup\$
    – ediblecode
    Commented Aug 8, 2019 at 12:42

6 Answers 6

14
\$\begingroup\$

This looks good overall. For the code review, I'll start with general comments and then try to get into smaller details.

Documentation

Documentating the code looks like an easy task but doing it properly so that it actually adds interesting information without adding too much noise can be pretty hard.

Let's see what could be improved here.

For a start, you do not need to write the comments in upper case. it actually makes things harder to read.

In order to document a module, a class, a function, a method, you can use docstrings. You'll find more details about this in PEP 257 -- Docstring Conventions.

As you document the code, a pretty common tip is to say that Code Tells You How, Comments Tell You Why.

Here are a few instances:

  • # DEFINING THE TEAM CLASS tells you nothing that the code does not show. Having a docstring explaining the point of the Team class whould be more helpful.
  • # INITIALISING ALL OF THE TEAMS - NAMES AND SKILL LEVELS - here again, we can easily see that this is initialising teams.

Style

Python has a Style Guide called PEP 8. It could be a good idea to read it and try to see what could be applied to your code. A good example to start with could be the variable names.

You'll find various tools online to check your code compliancy to PEP 8 and/or to fix it.

Code organisation

It is good practice to split the definitions from your code such as functions and classes from the part of your code actually doing something when the script is called with an if __name__ == "__main__": guard.

This helps for the re-usability of your code (and also makes the organisation clearer/more conventional).

Improving home_score & away_score

These 2 functions are complicated, pretty long and fairly similar. For all these reasons, there is probably something we can improve in them.

For a start, we could rename z into x to make the 2 functions even more similar and be able to see what differs from one to another.

Same for lambAway and lambHome renamed into lamb.

(Disclaimer: nothing here has been even remotely tested)

At this stage, we have:


def home_score(home, away):
    homeSkill = home.skill / 3
    awaySkill = away.skill / 3

    if homeSkill == awaySkill:
        raise ValueError

    if homeSkill > awaySkill:
        goals = 0
        lamb = higher ** (homeSkill - awaySkill)
        x = random.random()
        while x > 0:
            x = x - (((lamb ** goals) * math.exp(-1 * lamb)) / math.factorial(goals))
            goals += 1
        return (goals - 1)

    if homeSkill < awaySkill:
        goals = 0
        lamb = higher ** (homeSkill - awaySkill)
        x = random.random()
        while x > 0:
            x = x - (((lamb ** goals) * math.exp(-1 * lamb)) / math.factorial(goals))
            goals += 1

        return (goals - 1)

def away_score(home, away):
    homeSkill = home.skill / 3
    awaySkill = away.skill / 3

    if homeSkill == awaySkill:
        raise ValueError

    if awaySkill > homeSkill:
        goals = 0
        lamb = lower ** (homeSkill - awaySkill)
        x = random.random()
        while x > 0:
           x = x - (((lamb ** goals) * math.exp(-1 * lamb)) / math.factorial(goals))
           goals += 1
        return (goals - 1)

    if awaySkill < homeSkill:
        goals = 0
        lamb = lower ** (homeSkill - awaySkill)
        x = random.random()
        while x > 0:
           x = x - (((lamb ** goals) * math.exp(-1 * lamb)) / math.factorial(goals))
           goals += 1
        return (goals - 1)

which is not really that much better.

Trying to factorise out the common parts from the if homeSkill > awaySkill and if homeSkill < awaySkill, it looks like we could have:

def home_score(home, away):
    homeSkill = home.skill / 3
    awaySkill = away.skill / 3

    if homeSkill == awaySkill:
        raise ValueError

    goals = 0
    lamb = higher ** (homeSkill - awaySkill)
    x = random.random()
    while x > 0:
        x = x - (((lamb ** goals) * math.exp(-1 * lamb)) / math.factorial(goals))
        goals += 1
    return (goals - 1)


def away_score(home, away):
    homeSkill = home.skill / 3
    awaySkill = away.skill / 3

    if homeSkill == awaySkill:
        raise ValueError

    goals = 0
    lamb = lower ** (homeSkill - awaySkill)
    x = random.random()
    while x > 0:
       x = x - (((lamb ** goals) * math.exp(-1 * lamb)) / math.factorial(goals))
       goals += 1
    return (goals - 1)

But we could go further and extract the common parts of the function in a different function:

def home_score(home, away):
    return generate_random_score(home.skill / 3, away.skill / 3, higher)

def away_score(home, away):
    return generate_random_score(home.skill / 3, away.skill / 3, lower)

def generate_random_score(home_skill, away_skill, param):
    if home_skill == away_skill:
        raise ValueError

    goals = 0
    lamb = param ** (home_skill - away_skill)
    x = random.random()
    while x > 0:
       x = x - (((lamb ** goals) * math.exp(-1 * lamb)) / math.factorial(goals))
       goals += 1
    return (goals - 1)

It looks pretty good so far but we can still improve details.

For a start, it is clear now that lower and higher are not such great names. Maybe something mentionning home and away would be better.

Also, maybe the 2 methods do not really correspond to what you want: what you usually want is to simulate a full game and not just the score for a team. You could define a function returning a tuple:

def generate_random_score(home, away):
    delta_skill = (home - away) / 3
    return (generate_random_goal_number(delta_skill, higher), generate_random_goal_number(delta_skill, lower))


def generate_random_goal_number(delta_skill, param):
    if delta_skill == 0:
        raise ValueError

    goals = 0
    lamb = param ** delta_skill
    x = random.random()
    while x > 0:
       x = x - (((lamb ** goals) * math.exp(-1 * lamb)) / math.factorial(goals))
       goals += 1
    return (goals - 1)

A tiny improvement could be to rewrite:

x = x - long_expression

as:

x -= long_expression

Settings for the league

Havnig "league_size = 20" is a bit obscure and easy to break. You probably should have league_size = len(teams).

Also, in order to initialise the different arrays, you could have something like:

  GOALS_AGAINST = [0] * league_size

Finally, the team list could be initialised directly without defining so many variables that won't get reused.

You could write:

teams = [
    Team("Arsenal", 16),
    Team("Aston Villa", 6),
    Team("AFC Bournemouth", 8),
...
]

More code organisation

At the moment, you keep tracks of the stats for the teams in the class instances and in separate lists.

It would probably make sense to define a function/method simulate_game taking 2 teams as parameters and that would take care of generating a score and updating the team stats accordingly.

By the way, you do not necessarly need to keep track of the points. You could define a method in the Team objects to compute it on demand from the other statistics.

Special tip

I've said many things and there are still many things to say.

For learning purposes (and because I may have gotten things wrong in a few places), it could be a good idea to try to perform the changes described on your side.

Also, when random elements are involved, it can be hard to detect when you break something. My suggestion would be to initialise the random number generator with your favorite seed (for instance random.seed(42)), run your script, save the output and then keep that seed during your developments. If everything goes fine, the output should stay the same.

It does NOT exactly mean that:

  • it the output stays the same, nothing got broken

  • if the output changes, something got broken

but it does help to give you some confidence as you go.


Here is the updated version of the code based on the comments above and more:

import math
import random

random.seed(42)  # Removing randomness

class Team:
    def __init__(self, name, skill):
        self.name = name
        self.skill = skill
        self.points = self.gf = self.ga = self.wins = self.draws = self.losses = 0


def generate_random_score(home, away):
    param_home = 1.148698355
    param_away = 0.8705505633
    delta_skill = (home.skill / 3 - away.skill / 3)
    return (generate_random_number_of_goals(delta_skill, param_home),
            generate_random_number_of_goals(delta_skill, param_away))


def generate_random_number_of_goals(delta_skill, param):
    if delta_skill == 0:
        raise ValueError
    goals = 0
    lamb = param ** delta_skill
    x = random.random()
    while x > 0:
       x = x - (((lamb ** goals) * math.exp(-1 * lamb)) / math.factorial(goals))
       goals += 1
    return (goals - 1)


def simulate_league(teams):
    """Play all teams against each other and update statistics."""
    for home_team in teams:
        print("========================================")
        print(home_team.name + "'s home games: ")
        print("========================================")
        for away_team in teams:
            if home_team != away_team:
                home_score, away_score = generate_random_score(home_team, away_team)
                print(home_team.name, home_score, ":", away_score, away_team.name)
                home_team.gf += home_score
                away_team.gf += away_score
                home_team.ga += away_score
                away_team.ga += home_score
                if home_score == away_score:
                    home_team.draws += 1
                    away_team.draws += 1
                    home_team.points += 1
                    away_team.points += 1
                else:
                    winning, losing = (home_team, away_team) if (home_score > away_score) else (away_team, home_team)
                    winning.wins += 1
                    winning.points += 3
                    losing.losses += 1


if __name__ == "__main__":
    teams = [
        Team("Arsenal", 16),
        Team("Aston Villa", 6),
        Team("AFC Bournemouth", 8),
        Team("Brighton and Hove Albion", 5),
        Team("Burnley", 4),
        Team("Chelsea", 17),
        Team("Crystal Palace", 11),
        Team("Everton", 14),
        Team("Leicester City", 12),
        Team("Liverpool", 19),
        Team("Manchester City", 20),
        Team("Manchester United", 15),
        Team("Newcastle United", 3),
        Team("Norwich City", 2),
        Team("Sheffield United", 1),
        Team("Southampton", 7),
        Team("Tottenham Hotspur", 18),
        Team("Watford", 9),
        Team("West Ham United", 10),
        Team("Wolverhampton Wanderers", 13),
    ]

    for team in teams:
        print(team.name, team.skill)

    simulate_league(teams)

    # printing the final league table
    print("| TEAM                      | POINTS | WINS | DRAWS | LOSSES | GOALS FOR | GOALS AGAINST |")
    for team in sorted(teams, key=lambda t: t.points, reverse=True):
        print("|",team.name.ljust(25),"|",str(team.points).ljust(6),"|",str(team.wins).ljust(4),"|",
              str(team.draws).ljust(5),"|",str(team.losses).ljust(6),"|",str(team.gf).ljust(9),"|",
              str(team.ga).ljust(13),"|")

\$\endgroup\$
3
  • 1
    \$\begingroup\$ Thank you very much! I am going to make some changes based on your advice and I will edit the post with my updated code! \$\endgroup\$ Commented Aug 7, 2019 at 9:47
  • 3
    \$\begingroup\$ @woody101298 You shouldn't edit the code in the question after getting a review. For details read here: What should I do when someone answers my question?. \$\endgroup\$
    – Georgy
    Commented Aug 7, 2019 at 10:19
  • 1
    \$\begingroup\$ @Georgy Thanks for letting me know, I'll post any updates in another 'answer' to this question! \$\endgroup\$ Commented Aug 7, 2019 at 10:33
12
\$\begingroup\$

Very nice program for a beginner! Great attempt! Here are some points.

Imports

For multiple imports for the same module, you use ,

from x_module import y, z

But for our purpose,

import math
import random

works better. Python is concerned about readability and space saving might not always be the best option.

Naming

Python follows a style convention known as PEP8. The normal variable naming convention can be summarised as:

  • use snake_case instead of PascalCase or camelCase. my_pencil instead of MyPencil and myPencil
  • use CAPITAL for constants. PI = 3.14, as the value of this variable won't change but like pencils = [], pencils will be reduced and expanded.

Some improvements

homeSkill -> home_skill

POINTS -> points as we see points being modified at POINTS += [0]

higher = 1.148698355 -> HIGHER = 1.148698355 since it is a constant

A note on objects

Since i see nowhere you needed to use the teams individually and since this is a simulation, teams could be defined as:

teams = [
    Team("Arsenal", 16),
    Team("Aston Villa", 6),
    Team("AFC Bournemouth", 8),
    ...,
    Team("Wolverhampton Wanderers", 13)
]

String formatting

The table can be simplified. This

print("| TEAM                      | POINTS | WINS | DRAWS | LOSSES | GOALS FOR | GOALS AGAINST |")

can be written as

print("| {:<20} | {:^10} | {:^10} | {:^10} | {:^10} | {:^10} | {:^10} |".format('TEAM',
    'POINTS', 'WINS', 'DRAWS', 'LOSSES', 'GOALS FOR', 'GOALS')

so that the following loop can be simplified to

for team in sorted_teams:
    print("| {:<20} | {:<10} | {:<10} | {:<10} | {:<10} | {:<10} | {:<10} |".format(team.name, 
    team.points, team.wins, team.draws, team.losses, team.gf, team.ga))

where < means left align and ^ means align to the center.

Looping

league_size = 20
POINTS = []
GOALS_FOR = []
...
for x in range(league_size):
    POINTS += [0]
    ...

can be written as

league_size = 20
POINTS = [0] * league_size
GOALS_FOR = [0] * league_size
...
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Thank you for your help! I am going to do some updates based on advice and I will post them later! \$\endgroup\$ Commented Aug 7, 2019 at 9:48
7
\$\begingroup\$

I won't comment on the code itself, since I am not a Python person, but in terms of your approach, I will say this:

Using estimated table position seems like a poor predictor of performance, since it is has very low precision. It also does not allow for groupings of teams at a similar level.

Take for example last season's top 3:

man_city = Team("Manchester City", 20)
liverpool = Team("Liverpool", 19)
tottenham = Team("Tottenham Hotspur", 18)

Was the difference in quality between Man City and Liverpool somewhat similar to the difference in quality between Liverpool and Spurs? The points tally would suggest that it was not even close - Liverpool got ~34% more points than Spurs, while City only clinched the title by a single point.

Similarly, six of last year's mid-table teams were separated by only 7 points.

52  Leicester
52  West Ham
50  Watford
49  Crystal Palace
45  Newcastle
45  Bournemouth

Under your system, Leicester would be 5 "skill points" higher than Bournemouth - a seemingly huge gulf in class - while in reality the table would suggest that they're both at a similar level. Without a late equalizer in this game, these two would have even closer. When one goal has the potential to throw off your whole prediction, you know something's up.

I suspect that a much better predictor would be the points tally obtained in the previous season. Of course, for the 3 promoted teams you will not have this information. For these, you could look at the historic difference between a previous season's Championship points tally against their Premier League performance the following season. If there's a correlation then you could use this to estimate a points tally.

You could then start to factor in other factors such as net spend over the summer, number of arrivals and departures, etc. This may start to account for the fact that a team like Chelsea did well last season but have lost an expensive key player.

You could of course start to go much deeper and start to analyse previous results individually and use that to predict the outcome of each game more accurately. Maybe Arsenal do well at home and poorly away, or Burnley get better results against stronger opposition, or a derby game generally has fewer goals than a normal one.

\$\endgroup\$
5
\$\begingroup\$

I have incorporated advice from @Josay and @Abdur-Rahmaan Janhangeer and updated my code. I will also work on using a system to better set skill levels to teams so that the simulation becomes more accurate as @Michael suggested. Thank you for all your help on my first project.

Changes include:

  • cleaning up all of the formatting errors i.e. upper-case in the wrong places.

  • home and away goals functions simplified and made into a single function as suggested by @Josay.

  • using

if __name__ = "__main__"

to separate the functions from the code that will be run.

  • cleaned up the table as suggested by @Abdur-Rahmaan Janhangeer.

I have also included a goal difference concept to the league and I was wondering how I can sort the list of teams first by points and then by goal difference?

Below is the full revised code:

import math
import random

H_PARAMETER = 1.148698355
A_PARAMETER = 0.8705505633


class Team:
    def __init__(self, name, skill):
        self.name = name
        self.skill = skill
        self.points = self.gf = self.ga = self.wins = self.draws = self.losses = self.gdiff = self.mp = 0


def generate_random_goals(delta_skill, parameter):
    if delta_skill == 0:
        raise ValueError
    goals = 0
    lamb = parameter ** (delta_skill)
    z = random.random()
    while z > 0:
        z = z - (((lamb ** goals) * math.exp(-1 * lamb)) /
                 math.factorial(goals))
        goals += 1
    return goals - 1


def generate_random_score(home, away):
    delta_skill = (home.skill - away.skill) / 3
    return (generate_random_goals(delta_skill,
                                  H_PARAMETER), generate_random_goals(delta_skill, A_PARAMETER))


def simulate_league(teams):
    for home_team in teams:
        print("=" * 50)
        print(home_team.name + "'s home games: ")
        print("=" * 50)
        for away_team in teams:
            if home_team == away_team:
                pass
            if home_team != away_team:
                home_score, away_score = generate_random_score(home_team, away_team)
                print(home_team.name, home_score, ":", away_score, away_team.name)
                home_team.gf += home_score
                away_team.gf += away_score
                home_team.ga += away_score
                away_team.ga += home_score
                home_team.gdiff += (home_score - away_score)
                away_team.gdiff += (away_score - home_score)
                home_team.mp += 1
                away_team.mp += 1
                if home_score == away_score:
                    home_team.draws += 1
                    away_team.draws += 1
                    home_team.points += 1
                    away_team.points += 1
                if home_score > away_score:
                    home_team.wins += 1
                    away_team.losses += 1
                    home_team.points += 3
                if away_score > home_score:
                    away_team.wins += 1
                    home_team.losses += 1
                    away_team.points += 3


if __name__ == "__main__":
    teams = [
        Team("Arsenal", 16), Team("Aston Villa", 6), Team("AFC Bournemouth", 8), Team("Brighton and Hove Albion", 5),
        Team("Burnley", 4), Team("Chelsea", 17), Team("Crystal Palace", 11), Team("Everton", 14),
        Team("Leicester City", 12), Team("Liverpool", 19), Team("Manchester City", 20), Team("Manchester United", 15),
        Team("Newcastle United", 3), Team("Norwich City", 2), Team("Sheffield United", 1), Team("Southampton", 7),
        Team("Tottenham Hotspur", 18), Team("Watford", 9), Team("West Ham United", 10),
        Team("Wolverhampton Wanderers", 13)
    ]

    for team in teams:
        print(team.name, team.skill)

    simulate_league(teams)

    sorted_teams = sorted(teams, key=lambda t: t.points, reverse=True)

    print("=" * 108)
    print(
        "| {:<25} | {:^4} | {:^3} | {:^3} | {:^3} | {:^4} | {:^4} | {:^4} | {:^6} |".format("CLUB", "MP", "W", "D",
                                                                                            "L", "GF",
                                                                                            "GA",
                                                                                            "GD", "PTS"))
    for team in sorted_teams:
        print("| {:<25} | {:^4} | {:^3} | {:^3} | {:^3} | {:^4} | {:^4} | {:^4} | {:^6} |".format(team.name, team.mp,
                                                                                                  team.wins,
                                                                                                  team.draws,
                                                                                                  team.losses,
                                                                                                  team.gf,
                                                                                                  team.ga, team.gdiff,
                                                                                                  team.points))
\$\endgroup\$
2
  • 1
    \$\begingroup\$ A pleasure to read! \$\endgroup\$ Commented Aug 7, 2019 at 15:39
  • \$\begingroup\$ I suggest you add methods on the Team class to record home/away games: def record_home_score() and def record_away_score(). This would move most of the code inside your simulate_leage function into the class, and make both sets of code more clear. \$\endgroup\$
    – aghast
    Commented Aug 8, 2019 at 20:40
5
\$\begingroup\$

At the end of my first year programming I developed the exact same program with Python as well.

What I encourage you to do, and what I did, is to read the teams and their skill levels from a separate file. This way you can even prompt the user for which league they want to use.

i.e.

Which country's league do you wish to simulate?
$ England

And it will look for the file called England.csv and read its content for clubs and skill levels.

Also, read how many lines are in the file so that you can make the amount of clubs in the division dynamic and not simply 20 all the time.

Good luck and happy programming!

\$\endgroup\$
1
\$\begingroup\$

Further development...

As @Michael suggested the skill levels previously used were fairly inaccurate and did not sufficiently describe the level of each team.

I have used the previous year's point tally in the code below and also incorporated a method to re-run the season with the points tally simulated. For now, promoted teams are given skill levels just below the lowest positioned teams from the Premier League last season.

if __name__ == "__main__":
    teams = [
        Team("Arsenal", 70),
        Team("Aston Villa", 33),
        Team("AFC Bournemouth", 45),
        Team("Brighton and Hove Albion", 36),
        Team("Burnley", 40),
        Team("Chelsea", 72),
        Team("Crystal Palace", 49),
        Team("Everton", 54),
        Team("Leicester City", 53),
        Team("Liverpool", 97),
        Team("Manchester City", 98),
        Team("Manchester United", 66),
        Team("Newcastle United", 46),
        Team("Norwich City", 35),
        Team("Sheffield United", 34),
        Team("Southampton", 39),
        Team("Tottenham Hotspur", 71),
        Team("Watford", 50),
        Team("West Ham United", 52),
        Team("Wolverhampton Wanderers", 57)
    ]

    for team in teams:
        print(team.name, team.skill)

    simulate_league(teams)

    sorted_teams = sorted(teams, key=lambda t: t.points, reverse=True)

    print("=" * 108)
    print(
        "| {:<25} | {:^4} | {:^3} | {:^3} | {:^3} | {:^4} | {:^4} | {:^4} | {:^6} |".format("CLUB", "MP", "W", "D",
                                                                                            "L", "GF",
                                                                                            "GA",
                                                                                            "GD", "PTS"))
    for team in sorted_teams:
        print("| {:<25} | {:^4} | {:^3} | {:^3} | {:^3} | {:^4} | {:^4} | {:^4} | {:^6} |".format(team.name, team.mp,
                                                                                                  team.wins,
                                                                                                  team.draws,
                                                                                                  team.losses, team.gf,
                                                                                                  team.ga, team.gdiff,
                                                                                                  team.points))
    for team in teams:
        print(team.name, team.skill)
        team.gf = team.ga = team.gdiff = team.mp = team.wins = team.losses = team.draws = team.points = 0

    simulate_league(teams)

    sorted_teams = sorted(teams, key=lambda t: t.points, reverse=True)

    print("=" * 108)
    print("| {:<25} | {:^4} | {:^3} | {:^3} | {:^3} | {:^4} | {:^4} | {:^4} | {:^6} |".format("CLUB", "MP", "W", "D",
                                                                                            "L", "GF",
                                                                                            "GA",
                                                                                            "GD", "PTS"))
    for team in sorted_teams:
        print("| {:<25} | {:^4} | {:^3} | {:^3} | {:^3} | {:^4} | {:^4} | {:^4} | {:^6} |".format(team.name, team.mp,
                                                                                                  team.wins,
                                                                                                  team.draws,
                                                                                                  team.losses, team.gf,
                                                                                                  team.ga, team.gdiff,
                                                                                                  team.points))

A small change was made to the goal-scoring function in order to allow for there to now be identical skill levels (in case two teams get the same point tally in the first simulated league).

# Used this
if home == away:
# Instead of this
if delta_skill == 0:

I am wondering how I could incorporate a new feature which introduces the possibility of teams suffering from injuries and therefore affecting their performance?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.