一个 JavaScript 投注计算器





5.00/5 (2投票s)
一个 JavaScript 投注计算器
我算不上是个大赌徒,也深知赌博的危害有多大。尽管如此,去年我还是接受了几个亲密朋友的邀请,加入了一个小型投注组织。基本上,我们每周每人贡献 5 英镑,轮流赌上这周的彩池。这很有趣,最重要的是它能帮助我们保持联系。加入后,我自然而然地对投注如何运作产生了更大的兴趣,特别是累积投注和可以进行的各种累积投注组合。很明显,对于这些投注来说,计算潜在的回报远非易事。我想写一个 JavaScript 投注计算器函数,这会是一个有趣的练习。这不仅是我有点兴趣的领域,实际上也是一个相当有趣的数学谜题。
警告——在撰写本文时,我意识到,对于不了解英国投注基本知识(小数赔率、累积投注和全覆盖投注)的人来说,大部分代码将毫无意义。因此,在开始讨论代码之前,我将对这些概念进行一个简要(至少是我能管理的简要)概述。因此,这篇博文的内容会异常冗长,提前致歉。您可以在这里找到对投注赔率的另一种概述。
英国投注快速指南
在英国,投注赔率是**分数式**的。因此,对于一个有 25% 概率发生的事件,其赔率将表示为 3/1。对于你在 3/1 赔率的投注中下的每一英镑,如果投注获胜,你将收到你的赌注以及额外的 3 英镑。例如,如果你在 3/1 赔率的投注中下了 5 英镑的赌注并获胜,你将总共收到 20 英镑。这代表了 25% 的概率,因为从长远来看,如果你 25% 的时间获胜,你将收支平衡。像这样的单次投注被称为**独赢**。
因此,赢得彩金的计算可以用以下公式表示
r = s + (s*n/d)
其中 r = 潜在回报,s = 赌注,n = 赔率的分子,d = 赔率的分母。
所以,根据上面的例子
r = 5 + (5*3/1) = 20
情况对于**累积投注**变得更加复杂。累积投注是指你对多个事件进行投注——你的投注只有在所有选择都获胜时才会赢。最简单的累积投注是**双重投注**,你对 2 个事件的发生进行投注。包含 3 个选择的累积投注是**三重投注**,4 个选择是**四重投注**,5 个是**五重投注**,依此类推。
计算累积投注潜在回报的最简单方法是,首先将每个选择的分数式赔率转换为小数赔率。累积投注获胜的总赔率就是累积投注中所有小数赔率的乘积。我们上面看到,3/1 的赔率代表 25% 的获胜概率,因此其小数赔率为 0.25。因此,分数式赔率可以通过以下公式转换为小数赔率
dec = (n + d)/d
其中 dec = 小数赔率,n = 分子,d = 分母
例如,假设你下了 4 磅的赌注,进行了一次 4 重投注,包含四个赔率为 5/2、11/4、8/1 和 1/2 的选择,赌注为 4 磅。每个投注的小数赔率分别为 3.5、3.75、9 和 1.5。将它们相乘,我们就得到总的累积投注小数赔率为 117.1875。乘以我们的赌注金额,我们就能获得 708.75 的潜在收益。
以下是一些更多示例
累积投注类型 | 选择赔率(分数式) | 选择赔率(小数) | 总赔率 | 赌注 | 潜在回报 |
---|---|---|---|---|---|
双精度浮点型 | 3/1, 4/1 | 4, 5 | 20 | 5 | 100 |
三重投注 | 3/1, 4/1, 7/2 | 4, 5, 4.5 | 90 | 5 | 450 |
五重投注 | 2/1, 19/20, 5/2, 1/1, 4/9 | 3, 1.95, 3.5, 2, 1.44 | 59.15 | 10 | 591.50 |
为了进一步复杂化,你还可以下一种称为**全覆盖投注**的赌注。在全覆盖投注中,你做出多个选择,但不是只下一次累积投注,而是下多次投注——每次可能的累积投注都下一次注。因此,如果你做出三个选择并下全覆盖投注,实际上你下了**四次投注**:三次双重投注和一次三重投注,如下图所示
投注 | 投注类型 | 选择 |
---|---|---|
1 | 双精度浮点型 | 1 和 2 |
2 | 双精度浮点型 | 1 和 3 |
3 | 双精度浮点型 | 2 和 3 |
4 | 三重投注 | 1、2 和 3 |
对于这种全覆盖投注,你需要为每次投注提供赌注,因此对于 3 磅的赌注,你总共支付 12 磅。
许多全覆盖投注根据选择的数量有特殊的名称。我们刚刚看到的例子,一个包含 3 个选择的全覆盖投注,被称为**三连**。下表包含了一些其他流行的全覆盖投注
全覆盖投注名称 | 选择数量 | 投注数量 | 投注明细 |
---|---|---|---|
三连 | 3 | 4 | 3 次双重投注和 1 次三重投注 |
扬基 | 4 | 11 | 6 次双重投注,4 次三重投注和 1 次四重投注 |
超级扬基 | 5 | 26 | 10 次双重投注,10 次三重投注,5 次四重投注和 1 次五重投注 |
海因茨 | 6 | 57 | 15 次双重投注,20 次三重投注,15 次四重投注,6 次五重投注和 1 次六重投注 |
超级海因茨 | 7 | 120 | 21 次双重投注,35 次三重投注,35 次四重投注,21 次五重投注,7 次六重投注和 1 次七重投注 |
哥利亚 | 8 | 247 | 28 次双重投注,56 次三重投注,70 次四重投注,56 次五重投注,28 次六重投注,8 次七重投注和 1 次八重投注 |
最后的复杂之处:还有另一类投注,包含全覆盖投注**以及独赢**,也就是说,每次选择都单独进行一次独赢投注。通过这些投注,即使只有一个选择获胜,你也能获得回报。
因此,包含独赢的全覆盖投注,有四个选择,包括 15 次投注:四次独赢,六次双重投注,四次三重投注和一次四重投注(即构成扬基投注的 11 次投注,加上四次独赢)。这被称为**幸运 15**。
现在,这是包含独赢的全覆盖投注的名称
包含独赢的全覆盖投注名称 | 选择数量 | 投注数量 | 投注明细 |
---|---|---|---|
专利 | 3 | 7 | 3 次独赢,3 次双重投注和 1 次三重投注 |
幸运 15 | 4 | 15 | 4 次独赢,6 次双重投注,4 次三重投注和 1 次四重投注 |
幸运 31 | 5 | 31 | 5 次独赢,10 次双重投注,10 次三重投注,5 次四重投注和 1 次五重投注 |
幸运 63 | 6 | 63 | 6 次独赢,15 次双重投注,20 次三重投注,15 次四重投注,6 次五重投注和 1 次六重投注 |
如果我们包含“每路投注”,那将打开一个全新的领域,也许有一天我会这样做,但目前为止,这些是我希望我的代码支持的投注类型。
现在,终于——让我们来谈谈编程!
编写代码
简单来说——我想编写最简单的函数来计算全覆盖投注(含或不含独赢)的潜在回报。我意识到这取决于计算单次投注或累积投注回报的函数。我还认为,内部处理小数赔率比分数式赔率更容易,因此我首先编写了一个将分数式赔率转换为小数赔率的函数(“赔率”和“投注”这两个术语是可互换的)。
单元测试使用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; }
很简单。
现在,给定小数赔率,单次投注能赢多少?
// 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; }
同样,非常直接。别担心,后面会更有趣。
接下来,如何计算累积投注的潜在回报?我尝试了各种累积投注的表示方法,但最终决定使用数字数组,每个数字代表一个小数赔率。选择的数量自然就是隐含的。
// 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; }
我们只需遍历选择并累积乘以赔率。这样我们就可以获得累积投注的总赔率,然后将其传递给我们的 `getWinnings` 函数来计算我们的回报。
现在我们有了一个可以工作的框架。如何计算全覆盖投注(含或不含独赢)的潜在回报?
在函数接口方面,我决定采用一种结构,它同样由一个价格数组组成,外加一个整数,表示**每次投注的最小选择数**。换句话说,如果我们要包含独赢,第二个参数将是 1,或者对于任何其他全覆盖投注,则是 2。此外,它还将允许计算其他类型的投注组合,例如不含双重投注的全覆盖投注。
首先,一些单元测试
// 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); }); });
然后是实现。
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; }
这个问题的难度归结为**获取给定选择集的唯一排列**,其中排列是包含选择的某种组合的累积投注。所以对于幸运 15,有 15 个唯一的排列。
`getUniquePermutations` 函数接受一个价格数组和一个整数 `permLength`,该整数指定此函数调用的排列长度。它返回一个二维数组,即一个累积投注的数组。
因此,如果我们调用 `getUniquePermutations([2,4,5,3],2)`,我们的结果看起来会是这样的
[[2,4],[2,5],[2,3],[4,5],[4,3],[5,3]]
但是它是如何工作的呢?
让我们更仔细地看看递归循环
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))); }
我们正在遍历我们的数组,对于每个元素,我们再次对**不包含**该元素的数组调用 `getUniquePermutations`。
所以,在我们的例子 `getUniquePermutations([2,4,5,3],2)` 中,我们实际上是在以下数组上调用函数
[4, 5, 3]
(不包含 2)
[2, 5, 3]
(不包含 4)
[2, 4, 3]
(不包含 5)
[2, 4, 5]
(不包含 3)
……然后,在我们对数组 `[4, 5, 3]` 的调用中,我们对以下数组调用函数
[5, 3]
(不包含 4)
[4, 3]
(不包含 5)
[4, 5]
(不包含 3)
……等等。
递归何时停止?当传入数组的长度等于 `permLength` 的值时,因为此时我们只需将数组本身作为唯一的排列返回。这是所有递归函数都需要的**基本情况**。
因为我们正在构建一个二维数组,所以我们需要 `twoDimArrayUnion` 函数来允许我们将每个递归调用的结果累积添加到我们的总体结果集中。这个函数基本上组合了两个二维数组,并去除了重复项(即,集合的经典**并集**定义)。
然后,我们只需要计算从最小选择数到价格数组长度的所有可能长度的唯一排列,并对每个排列调用 `getAccumulatorPrice`,这样,我们就得到了全覆盖投注的价格。
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; }
你可以在这个plunk中查看和玩这个代码。
这篇题为JavaScript 投注计算器的文章最初发布在The Proactive Programmer。