Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Javascript

A JavaScript Betting Calculator

5.00/5 (2 votes)
12 Jul 2015CPOL9 min read 20.3K  
A JavaScript Betting Calculator

I am far from a big gambler, and am well aware of how harmful a habit gambling can be. Nevertheless last year I accepted an invitation to join a small betting syndicate by some close friends. Basically we each contribute £5 each week, and take it in turns to gamble the week’s pot. It is good fun and most importantly it helps us all to stay in touch. Since joining I have naturally taken more of an interest in how betting works, in particular accumulators and the various combinations of accumulators that can be placed. It became clear that for these bets it is far from straightforward to calculate potential returns. I thought it would be a fun exercise to write up a bet calculator function in JavaScript. Not only is this an area which I have taken something of an interest in, but it is actually a quite interesting mathematical puzzle.

Warning – whilst writing up this post I realised that much of the code would be completely meaningless to anyone who didn’t understand the basics of betting in the UK (fractional odds, accumulators and full cover bets). Therefore before we start talking about code, I will provide a brief (at least as brief as I could manage) overview of these concepts. As a consequence this has turned out to be an unusually long post, so apologies in advance. An alternative overview of betting odds can be found here.

A Quick Guide to Betting in the UK

In the UK, betting odds are fractional. So for an event which has a 25% chance of occurring, this would be expressed as a price of 3/1. For each pound you place on a 3/1 bet, you will receive your pound stake back plus an additional 3 pounds should your bet win. For example, if you place a £5 stake on a 3/1 bet and it wins, you will receive a total of £20. This represents a 25% chance, as you will break even in the long term if your bet wins 25% of the time. A bet on a single event like this is known as a single.

It follows that the calculation of winnings can be expressed by the following formula:

r = s + (s*n/d)

where r = potential returns, s = stake, n = numerator part of odds, d = denominator part of odds.

So using this in the example above:

r = 5 + (5*3/1) = 20

Things become more complex with accumulator bets. With an accumulator bet you are betting on more than one event – your bet only wins if all your selections win. The simplest accumulator bet is a double, where you bet on 2 events occurring. An accumulator consisting of 3 selections is a treble, 4 selections is a fourfold, 5 is a fivefold and so on.

The easiest way to calculate the potential returns of an accumulator bet is to first convert the fractional odds for each selection into decimal odds. The overall odds of the accumulator winning is then the product of all the decimal odds in your accumulator. We saw above that a 3/1 price represents a 25% chance of winning, so its decimal odds are 0.25. It follows that fractional odds can be converted to decimal odds with the formula:

dec = (n + d)/d

where dec = decimal odds, n = numerator, d = denominator

For example, let’s say you have placed a 4-fold bet, consisting of four selections which are priced 5/2, 11/4, 8/1 and 1/2, and a stake of 4 pounds. The decimal odds of each bet are 3.5, 3.75, 9 and 1.5 respectively. Multiplying these together we have a total accumulator decimal price of 117.1875. Multiply this by our stake amount and we have potential winnings of 708.75.

Here are some more examples:

Accumulator Type Selection Odds (fractional) Selection Odds (decimal) Overall Price Stake Potential Return
Double 3/1, 4/1 4, 5 20 5 100
Treble 3/1, 4/1, 7/2 4, 5, 4.5 90 5 450
Fivefold 2/1, 19/20, 5/2, 1/1, 4/9 3, 1.95, 3.5, 2, 1.44 59.15 10 591.50

To complicate matters further, you can also place something called a full cover bet. In a full cover bet, you make multiple selections, but rather than just placing a single accumulator bet you place multiple bets – one bet on each possible accumulator bet from your selections. So if you make three selections and place a full cover bet, you are actually placing four bets: three doubles and one treble, as shown below:

Bet Bet Type Selections
1 Double 1 and 2
2 Double 1 and 3
3 Double 2 and 3
4 Treble 1, 2 and 3

With this full cover bet you need to provide a stake for each bet, so for a £3 stake you pay a total of £12.

Many full cover bets have special names according to the number of selections. The example we just saw, a full cover bet from 3 selections, is known as a trixie. The table below includes some other popular full cover bets:

Full Cover Bet Name Number of Selections Number of Bets Bet Breakdown
Trixie 3 4 3 doubles and a treble
Yankee 4 11 6 doubles, 4 trebles and a fourfold
Super Yankee 5 26 10 doubles, 10 trebles, 5 fourfolds and a fivefold
Heinz 6 57 15 doubles, 20 trebles, 15 fourfolds, 6 fivefolds and a sixfold
Super Heinz 7 120 21 doubles, 35 trebles, 35 fourfolds, 21 fivefolds, 7 sixfolds and a sevenfold
Goliath 8 247 28 doubles, 56 trebles, 70 fourfolds, 56 fivefolds, 28 sixfolds, 8 sevenfolds and an eightfold

A final complication: there are another set of bets which consist of a full cover bet including singles, that is – one single bet on each selection by itself. With these bets you get a return even if only one of your selections wins.

So a full cover bet with singles from four selections consists of 15 bets: four singles, six doubles, four trebles and a fourfold (i.e. the 11 bets that make up a Yankee, plus four singles). This is known as a lucky 15.

Now here are the names of some full cover bets with singles:

Full Cover Bet With Singles Name Number of Selections Number of Bets Bet Breakdown
Patent 3 7 3 singles, 3 doubles and a treble
Lucky 15 4 15 4 singles, 6 doubles, 4 trebles and a fourfold
Lucky 31 5 31 5 singles, 10 doubles, 10 trebles, 5 fourfolds and a fivefold
Lucky 63 6 63 6 singles, 15 doubles, 20 trebles, 15 fourfolds, 6 fivefolds and a sixfold

If we were to include “each way” betting that would open a whole new can of worms, which I may do one day, but for now these are the types of bets I wanted my code to support.

Now, finally – let’s talk about programming!

Writing the Code

Put simply – I wanted to write the simplest possible function to calculate the potential returns from a full cover bet, with or without singles. I realised that this would depend upon functions to calculate the returns from a single bet or an accumulator. I also decided that I would find it easier to deal with decimal prices internally, rather than fractional prices, therefore I started by writing a function to convert a fractional price into a decimal price (the terms ‘price’ and ‘odds’ are interchangeable).

Unit tests were written using Jasmine.

// Tests
describe("getDecimalPrice tests", function() {
   it('returns 5 for 4/1', function(){
      var decimal = getDecimalPrice(4,1);
      expect(decimal).toBe(5);
   });
   
  it('returns 5 for 11/4', function(){
      var decimal = getDecimalPrice(11,4);
      expect(decimal).toBe(3.75);
   });
   
  it('returns 1.4 for 2/5', function(){
      var decimal = getDecimalPrice(2,5);
      expect(decimal).toBe(1.4);
   });
   
  it('returns 2 for 1/1', function(){
      var decimal = getDecimalPrice(1,1);
      expect(decimal).toBe(2);
   });   
});

 

function getDecimalPrice(numerator, denominator){
  return (numerator+denominator)/denominator;
}

Simple enough.

Now, given a decimal price, how much can you win from a single bet?

// Tests
describe("getWinnings tests", function() {
   it('returns 5 for 5*1', function(){
      var winnings = getWinnings(1,5);
      expect(winnings).toBe(5);
   });
   
   it('returns 25 for 0.5*50', function(){
      var winnings = getWinnings(0.5,50);
      expect(winnings).toBe(25);
   });
});

 

function getWinnings(stake, decimalPrice){
  return stake*decimalPrice;
}

Again, very straightforward. Don’t worry, it does get more interesting.

Next, how to calculate the potential returns from an accumulator? I toyed with various representations of an accumulator bet, but decided upon an array of numbers, each representing a decimal price. The number of selections is then obviously implicit.

// Tests
describe("getAccumulatorPrice tests", function() {
   it('single', function(){
      var price = getAccumulatorPrice([5]);
      expect(price).toBe(5);
   });
   
   it('double', function(){
      var price = getAccumulatorPrice([4, 5]);
      expect(price).toBe(20);
   });
   
   it('treble', function(){
      var price = getAccumulatorPrice([4, 5, 1.01]);
      expect(price).toBe(20.2);
   });
   
   it('five fold', function(){
      var price = getAccumulatorPrice([2, 2, 2, 2, 2]);
      expect(price).toBe(32);
   });      
});

 

function getAccumulatorPrice(priceArray){
  var result = 1;
  
  for (var i = 0; i < priceArray.length; i++)
    result = result * priceArray[i];
  
  return result;
}

We simply loop through the selections and cumulatively multiply by the price. In this way we get an overall price for our accumulator, which we can then pass in to our getWinnings function to calculate our return.

Now we have a framework we can work with. How can we calculate the potential returns from a full cover bet, with or without singles?

In terms of our function’s interface, I decided upon a structure which consists again of an array of prices, along with an integer which represents the minimum number of selections per bet. In other words, this second parameter would be 1 if we wanted to include singles, or 2 for any other full cover bet. In addition it would allow for other types of bet combinations to be calculated, such as a full cover bet excluding doubles.

First, some unit tests:

// Tests
describe("getCoverBetMaxReturns tests", function(){
  
  it('single', function(){
      var price = getCoverBetMaxReturns([4], 1, 1);
      expect(price).toBe(4);
  });
  
  it('double', function(){
      var price = getCoverBetMaxReturns([4,3], 2, 3);
      expect(price).toBe(36);
  });
  
  it('treble', function(){
      var price = getCoverBetMaxReturns([4,3,4], 3, 2);
      expect(price).toBe(96);
  });  
  
  it('trixie', function(){
      var price = getCoverBetMaxReturns([2, 4, 5], 2, 10);
      expect(price).toBe(780);
  });
  
  it('patent', function(){
      var price = getCoverBetMaxReturns([2, 4, 5], 1, 1.5);
      expect(price).toBe(133.5);
  });  
  
  it('yankee', function(){
      var price = getCoverBetMaxReturns([2, 4, 5, 3], 2, 4);
      expect(price).toBe(1380);
  });
  
  it('lucky 15', function(){
      var price = getCoverBetMaxReturns([2, 4, 5, 3], 1, 200);
      expect(price).toBe(71800);
  });  
});

Now for the implementation.

function getCoverBetMaxReturns(priceArray, minAccSize, stake){       
    
  var total = 0;
  
  for(var i = minAccSize; i <= priceArray.length; i++)
  {
    var perms = getUniquePermutations(priceArray, i);
    
    for(var j = 0; j < perms.length; j++)
      total += getAccumulatorPrice(perms[j])*stake;
  }
  
  return total;
}

function getUniquePermutations(arr, permLength)
{        
    if(arr.length <= permLength)
        return [arr];
    
    var permutations = [];
    var newArr = [];
    
    newArr = arr.slice(0);
    
    for(var i = 0; i < arr.length; i++)
    {        
        newArr = arr.slice(0);
        newArr.splice(i, 1);                        
        permutations = twoDimArrayUnion(permutations,(getUniquePermutations(newArr, permLength)));                         
    }
                            
    return permutations;
}

function twoDimArrayUnion(arr1, arr2)
{        
    for(var i = 0; i < arr2.length; i++)
    {
        var duplicate = false;
        
        for(var j = 0; j < arr1.length; j++)        
            if(arr1[j].length == arr2[i].length)
                for(var k = 0; k < arr1[j].length; k++)                
                    if(arr1[j][k] != arr2[i][k])
                        break;
                    else if(k == arr1[j].length-1)                    
                        duplicate = true;
        
        if(!duplicate)
            arr1.push(arr2[i]);
    }
    
    return arr1;
}

The difficulty with this problem boils down to that of obtaining the unique permutations given a set of selections, where a permutation is an accumulator consisting of some combination of the selections. So for a Lucky 15 there are 15 unique permutations.

The getUniquePermutations function takes an array of prices, and an integer, permLength, which specifies the length of permutation we are concerned with for this function call. It returns a two-dimensional array, that is, an array of accumulator bets.

So if we were to call getUniquePermutations([2,4,5,3],2) our result would look something like this:

[[2,4],[2,5],[2,3],[4,5],[4,3],[5,3]]

But how does it work?

Let’s look more closely at the recursive loop:

for(var i = 0; i < arr.length; i++)
    {        
        newArr = arr.slice(0); // make a copy of the array 
        newArr.splice(i, 1);   // remove one item                        
        // call again on smaller array and save results                      
        permutations = twoDimArrayUnion(permutations,(getUniquePermutations(newArr, permLength)));
    }

What we are doing is looping through our array, and for each item calling getUniquePermutations again on the array without that item.

So in our example of getUniquePermutations([2,4,5,3],2), we are actually calling the function on the following arrays:

[4, 5, 3]  (without the 2)

[2, 5, 3] (without the 4)

[2, 4, 3] (without the 5)

[2, 4, 5] (without the 3)

…and then inside our call on the array [4, 5, 3], we call the function on:

[5, 3]  (without the 4)

[4, 3]  (without the 5)

[4, 5]  (without the 3)

…and so on.

When does the recursion stop? When the length of the array passed in is equal to the value of permLength, because at this point we can simply return the array itself as the only unique permutation. This is the base case, which is required of all recursive functions.

Because we are building up a two-dimensional array, we need the twoDimArrayUnion function to allow us to cumulatively add the results from each recursive call to our overall results set. This function basically combines two two-dimensional arrays, removing duplicates (i.e. the classic definition of a union of two sets).

Then all that is left is for us to calculate unique permutations for all possible lengths, from the minimum number of selections to the length of our array of prices, and to call getAccumulatorPrice on each permutation and voilà, we have a price for our full cover bet.

function getCoverBetMaxReturns(priceArray, minAccSize, stake){       
    
  var total = 0;
  
  for(var i = minAccSize; i <= priceArray.length; i++)
  {
    var perms = getUniquePermutations(priceArray, i);
    
    for(var j = 0; j < perms.length; j++)
      total += getAccumulatorPrice(perms[j])*stake;
  }
  
  return total;
}

You can view and play with the code in this plunk.

The post A JavaScript Betting Calculator appeared first on The Proactive Programmer.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)