Posts tagged "basketball" - Nate's Website
Skip to content

NBA Schedule Generator (Part 1) Base Case

One of the requirements for my Basketball Simulator project is to create a way to dynamically create schedules for seasons. If you were simulating seasons from the NBA’s past, you could use the actual historical schedules, but we are building an application that will allow the user to simulate seasons well into the future (2022 and beyond).

Ideally, the application will have a schedule generator that can generate a schedule based on any custom league design the user wants. But to start, we’re going to build an NBA Schedule Generator using the same rules and considerations that are used to make the NBA schedule. In this first blog post, I will highlight how to build a base case NBA schedule. Eventually, our schedule generator will factor in such considerations as travel time, team popularity, back-to-backs, and TV broadcast considerations.

One last thing before we get started, there’s been a good deal of research and open-source projects related to the NBA schedule that I have been able to access and learn from. The nba-scheduler Python project has been particularly useful so big thanks to the creator!

So what are the requirements of the NBA Schedule?

First, let’s go over the NBA’s league structure:

  • 30 Teams
  • 2 conferences (15 teams in each conference)
  • 6 divisions (3 divisions in each conference, 5 teams in each division)

Each of these teams must play 82 games and the schedule adheres to the following rules:

  • 4 games against the 4 intra-division teams (16 games, 8 home, 8 away)
  • 2 games against the 15 teams from the other conference (30 games, 15 home, 15 away)
  • 4 games against 6 in-conference, out-of-division teams (24 games, 12 home, 12 away)
  • 3 games against 4 in-conference, out-of-division teams (12 games, 2 teams 2 home, 1 away and 2 teams 2 away, 1 home)

From a coding perspective, the first 2 rules are pretty easy to reconcile. We just need to loop through each team and find the other teams that are in their division and not in their conference. That will get us 46 of the 82 games.

The 3rd and 4th rules is where things are not as simple. Here, we are working with 10 opponent teams that are in the team’s conference but not in their division. We then need to split these 10 teams into two buckets, which we will call common (6 teams) and rare (4 teams). Seems simple enough, right? But how do you do this dynamically?

Solving the Problem

One thing I first realized is that if you could choose the rare opponents for a given team, then you could find out the common opponents by finding those in-conference, out-of-division opponents that are not in the rare bucket. I decided to choose the rare opponents first because there were less of them.

My first attempt at choosing the rare opponents for each team had me doing a simple loop through each team and then getting a random opponent (of the 10 teams) and putting each team in each other’s rare bucket. The problem with this approach is that by the time I reached the end of my loop for each team, the teams at the tail end of the loop didn’t have a sufficient number of teams to choose from to fill out there schedule. So how do you solve this problem?

With recursion! For those that do not know, recursion is a concept in computer programming, where a function/algorithm calls itself until a certain condition is met. So for each of the 30 teams, we call this recursive function and pass the team as a parameter to the function:

What recursion allows us to do is have some more flexibility when choosing our rare opponents. The recursive function (which we are calling setRareNonDivisionOpponents) will only stop calling itself when all 30 teams have 4 teams in its’ rare bucket. To get there, it accounts for when there is less than 4 teams in the bucket and more than 4 teams in the bucket.

When there is less than 4 teams, we get a random opponent, check to make to make sure the teams aren’t already in the team’s rare bucket (otherwise we call our function again), and then add them to each team’s rare bucket.

else if (numScheduledRareGames < 4) {
      const opp = sample(possibleRareOpponents);

      if (!opp) {
        throw new Error(
          "We need a random opponent to continue. If not, the team has no chance of completing"
        );
      }

      //the randomly picked opponent is already a rare opponent, call again to make another choice
      if (
        this.teamSchedulerObj[team.abbrev].rareNonDivisionOpponents.includes(
          opp.abbrev
        ) ||
        this.teamSchedulerObj[opp.abbrev].rareNonDivisionOpponents.includes(
          team.abbrev
        )
      ) {
        this.setRareNonDivisionOpponents(opp);
        this.setRareNonDivisionOpponents(team);
        return;
      }

      // We can use this opponent so push them to each team's holders
      this.teamSchedulerObj[team.abbrev].rareNonDivisionOpponents.push(
        opp.abbrev
      );
      this.teamSchedulerObj[opp.abbrev].rareNonDivisionOpponents.push(
        team.abbrev
      );
      // call this function again for both teams

      this.setRareNonDivisionOpponents(team);
      this.setRareNonDivisionOpponents(opp);
    }

In the case where there is more than 4 teams in the rare bucket, we randomly select one of those teams and remove them from each team’s rare bucket.

    if (numScheduledRareGames > 4) {
      //current team has more than four opponents, need to remove some
      //pick random team from current rare opponents
      const randAbbrev = sample(rareNonDivisionOpponents)!;

      //remove the team from current team and opponents rare opponents
      this.teamSchedulerObj[team.abbrev].rareNonDivisionOpponents =
        this.teamSchedulerObj[team.abbrev].rareNonDivisionOpponents.filter(
          (team: string) => team !== randAbbrev
        );
      this.teamSchedulerObj[randAbbrev].rareNonDivisionOpponents =
        this.teamSchedulerObj[randAbbrev].rareNonDivisionOpponents.filter(
          (randTeam: string) => randTeam !== team.abbrev
        );

      //re-run this function for both teams
      this.setRareNonDivisionOpponents(team);
      this.setRareNonDivisionOpponents(this.getTeamByAbbrev(randAbbrev));
    }

The end result is a recursive function that will add and remove from the rare buckets until it randomly finds the perfect fit. Pretty magical!

If you would like to work with the NBA Schedule generator yourself or would like to checkout the code, please visit the GitHub repo.

Thanks for reading!