How to Write a Bot

From Bot Brackets Wiki
Jump to: navigation, search

To learn how to write a bot script, we will start by looking at a very simple bot:

return $(team2).Seed - $(team1).Seed;

One thing you might have noticed right away are the team1 and team2 values. These are parameters your script will get to identify the two teams to pick between. Unlike a bracket contest for humans, your script will consider every possibility for every round. (You don't need to write any code for that — the system calls the script over and over for each combination.)

Return Value

Next, note that you need to return a value from your script. In a bracket contest for humans, you'd pick one of the two teams as the winner and, if that team does win, you get the points for that round. Not so here: bots can pick somewhere in the middle. The return value is a number between -1 and 1, where 1 means you get all the points if team1 wins (but nothing if they lose) and -1 means you get all the available points only if team2 wins. A value in the middle returns a proportion of the points. It's a risk-vs-reward tradeoff your script needs to make.

An easy way to remember which end is positive and which is negative: it's similar to a standard comparison function in many other languages. A value of < 0 means team1 < team2 (so team2 wins) and a value of > 0 means that team1 > team2 (so team1 wins).

Maybe a few quick examples will help. Let's look at a round one game between the 5 seed and the 12 seed:

If team1 Wins If team2 Wins
Seed 5 12
Points available (≈ 5 * Seed / 2) 15 (5 * [5 seed = 3]) 30 (5 * [12 seed = 6])
If you return 1 15 0
If you return -1 0 30
If you return 0 7.5 (half) 15 (half)
If you return 0.5 (3/4 along -1..1) 11.25 (3/4 * 15) 7.5 (1/4 * 30)
If you return 1/3 (2/3 along -1..1) 10 (2/3 * 15) 10 (1/3 * 30)
If you return 2 (Clipped to 1) 15 0
If you return "potato" 0 0

Those last two examples above illustrate the range checking done on your script's output. If you return anything outside the range -1..1, then the value you return is clipped to the range. In other words, you can't return something greater than 1 to get > 100% of the points; if you try, it will act as though you only returned 1. A word of caution, though: if you return something that is not a number (or forget to return anything at all), then it will not give you any points no matter what happens, and there will be no warning.

Accessing Team Stats

Now that we have the basics, let's talk about what that funky "$" thing is in the script.

The "$" function gives access to a variety of stats about a team:

  • win/loss record ("W" and "L" properties)
  • season points for/against ("PTF" and "PTA", respectively)
  • goals made/attempts ("FGM" / "FGA", "3FG" / "3FGA", and "FT" / "FTA")
  • rebounds ("ORB", "DRB", and "TRB")
  • other season stats ("AST", "TO", "ST", "BLK", and "PF")
  • similar totals for opponents faced ("Opp" prefixed on most of the above: "OppFGM", "OppFGA", "Opp3FG", "Opp3FGA", "OppFT", "OppFTA", "OppORB", "OppDRB", "OppTRB", "OppAST", "OppTO", "OppST", "OppBLK", and "OppPF")
  • an array of all the games played that season ("Games"), where each element has:
    • whether it was a win or a loss ("W" = 1 or "L" = 1)
    • points for/against ("PTF" / "PTA")
    • who the opponent was ("Opponent")
  • the conference ("Conf") the team plays in, abbreviated
  • if the team is competing in the tournament, their "Seed" (1-16) and "Position" (1-64)

Note: There will be some "Opponent" values for which $(Opponent) will return undefined since some teams play outside of Division I. Always check return values.

Note 2: To access the properties that start with a number ("3FG" and "3FGA"), you will need to use the name as a string since otherwise the dot notation would be ambiguous with floats. Instead of "$(team1).3FG", use "$(team1)["3FG"]".

So, let's look at our sample bot again:

return $(team2).Seed - $(team1).Seed;

Using the "$" function allows us to get to the stats for the two teams, and we're accessing the "Seed" property, which gives us their seed in the tournament.

$(team1).Seed $(team2).Seed Return Value Meaning
5 12 7 (clipped to 1) 1 > 0, so team1 is weighted 100%
12 5 -7 (clipped to -1) 1 < 0, so team2 is weighted 100%
1 1 0 both teams are weighted 50% / 50%

So, the example bot is actually implementing the strategy "high seed wins".

Additional Parameters

There are a few additional parameters provided to your script:

  • Since $(team1) and $(team2) are likely to be used often, they are given the shortcuts "$1" and "$2", respectively. That means the example bot above could also be written as "return $2.Seed - $1.Seed;".
  • If you want to know what round (1-6) the two teams are competing in, that is given in "$Round".
  • For convenience, there is a "$Score1" and "$Score2" that shows how many points you will get if you pick 100% for team1 or team2, respectively.
  • There is a print() function that can be used to output info for debugging purposes.

Another Example

Nothing says that you have to use any team stats per se. The following bot will acheive the same score no matter who wins:

// If team1 gets A points if they win and team2 gets B points if they win, 
// their scores will equalize when using a weight x where x*A = (1-x)*B. 
// Solving for x, this gives (1-x)/x = A/B => (1/x)-1 = A/B => 
// (1/x) = A/B + 1 => x = 1/(A/B + 1) 
// Then, to give the return value that gives that weight, multiply by 2 and subtract 1. 
var a = $Score1; 
var b = $Score2; 
var x = 1 / (a/b + 1); 
print("a = " + a + ", b = " + b + ", x = " + x); 
return 2*x - 1;

More Complex Stats

The stats provided are regular JavaScript objects, so you can attach expando properties for your own additional stats. For example, the following rather complex bot computes pseudo-RPI with caching:

// Some quick utility functions.... 

function Contains(list, item) 
   for (var i = 0; i < list.length; i++) 
      if (list[i] === item) 
         return true; 
   return false; 

function SetProp(obj, prop, val) 
   if (!obj.hasOwnProperty(prop)) 
      obj[prop] = val; 

function AverageProps(obj) 
   var sum = 0; 
   var count = 0; 
   for (var prop in obj) 
      if (obj.hasOwnProperty(prop)) 
         sum += obj[prop]; 
   return count > 0 ? (sum / count) : 0; 

// Functions used to calculate winning percentages.... 
// Not extensively tested: may have bugs. Use at your own risk. 

function WinPct(team, excludeTeams) 
   var games = $(team).Games; 
   var wins = 0; 
   var losses = 0; 
   for (var i = 0; i < games.length; i++) 
      if (!Contains(excludeTeams, games[i].Opponent) && $(games[i].Opponent)) 
         wins += games[i].W; 
         losses += games[i].L; 

   if (losses == 0) return wins > 0 ? 1 : 0; 
   else return wins / (wins + losses); 

function OpponentWinPct(team) 
   var hashtable = {}; // to de-dup multiple games against same opponent 
   var games = $(team).Games; 
   for (var i = 0; i < games.length; i++) 
      var opp = games[i].Opponent; 
      if ($(opp) && !hashtable[opp]) 
         SetProp(hashtable, opp, WinPct(opp, [team])); 

   return AverageProps(hashtable); 

function OpponentOpponentWinPct(team) 
   var hashtable = {}; 
   var games = $(team).Games; 
   for (var i = 0; i < games.length; i++) 
      var opp = games[i].Opponent; 
      var $opp = $(opp); 
      if ($opp) 
         for (var j = 0; j < $opp.Games.length; j++) 
            var oppopp = $opp.Games[j].Opponent; 
            var key = "" + opp + "__" + oppopp; 
            if ($(oppopp) && !hashtable[key]) 
               hashtable[key] = WinPct(oppopp, [opp, team]); 

   return AverageProps(hashtable); 

function CalculateRPI(team) 
   return (0.25 * WinPct(team, [])) + 
      (0.5 * OpponentWinPct(team)) + 
      (0.25 * OpponentOpponentWinPct(team)); 

// The actual script.... 
// Note how we attach the calculation to an expando property so we only have to 
// calculate it the first time the script runs for a given team. 
if (!$1.RPI) $1.RPI = CalculateRPI(team1); 
if (!$2.RPI) $2.RPI = CalculateRPI(team2); 
print("team1 = " + $1.RPI + ", team2 = " + $2.RPI); // debugging 

if ($1.RPI > $2.RPI) return 1; 
else if ($1.RPI < $2.RPI) return -1; 
else return 0;

The RPI is calculated by adding three parts:

  1. 25% — Team winning percentage. The actual NCAA formula also takes into account whether the game is home or away, but we don't have that information. Also, we need to exclude games against non-Division I opponents.
  2. 50% — Average opponent win percentage. Exclude games played against the team from #1 and games against non-Division I opponents.
  3. 25% — Average opponent-opponent win percentage. Exclude games played against teams from #1, #2, and non-Division I opponents.

The key is in the last few lines: if an "RPI" expando property already exists on the stats object for that team, we use the cached value; otherwise, we calculate it and set the property. When the script runs for real to generate the contest matrix, it will be called multiple times, so caching allows us to avoid recomputing unnecessarily.

This example also shows how you can loop over the list of games and look at other teams' stats. Note how before accessing the "Games" property, it tests $(team) for truthiness (since undefined, meaning the team is not Division I, is falsy).