├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── race-range.js ├── webworker-board.js └── webworker.js ├── lib ├── board.js └── common.js ├── package.json ├── pec.background.js ├── pec.js ├── pec.worker.js └── test ├── pec.all.board.range.js ├── pec.all.board.single.js ├── pec.all.range.js ├── pec.all.single.js ├── pec.board.range.js ├── pec.board.single.js ├── pec.range.js ├── pec.single.js └── util ├── ocat.js └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | - 8 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Thorsten Lorenz. 2 | All rights reserved. 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pec [![build status](https://secure.travis-ci.org/thlorenz/pec.svg)](http://travis-ci.org/thlorenz/pec) 2 | 3 | Poker equity calculator. Compares two combos or one combo against a range to compute winning equity. 4 | 5 | ```js 6 | const { raceRange, rates } = require('pec') 7 | 8 | const combo = [ 'Jh', 'Js' ] 9 | const range = [ 10 | [ 'Kh', 'Ks' ], [ 'Kh', 'Kd' ], [ 'Kh', 'Kc' ], 11 | [ 'Ks', 'Kd' ], [ 'Ks', 'Kc' ], [ 'Kd', 'Kc' ], 12 | [ 'Qh', 'Qs' ], [ 'Qh', 'Qd' ], [ 'Qh', 'Qc' ], 13 | [ 'Qs', 'Qd' ], [ 'Qs', 'Qc' ], [ 'Qd', 'Qc' ] 14 | ] 15 | 16 | const { win, loose, tie } = raceRange(combo, range, 1E4) 17 | const { winRate, looseRate, tieRate } = rates({ win, loose, tie }) 18 | 19 | console.log('JJ performs as follows vs. [ KK, QQ ]') 20 | console.log('win: %d%% (%d times)', winRate, win) 21 | console.log('loose: %d%% (%d times)', looseRate, loose) 22 | console.log('tie: %d%% (%d times)', tieRate, tie) 23 | ``` 24 | 25 | JJ performs as follows vs. [ KK, QQ ] 26 | win: 18.13% (21750 times) 27 | loose: 81.43% (97718 times) 28 | tie: 0.44% (532 times) 29 | 30 | For more examples see the [tests](test/pec.single.js) and the [webworker example](examples/webworker.js). 31 | 32 | You can launch the web worker via `npm install && npm run demo`. 33 | 34 | ## Installation 35 | 36 | npm install pec 37 | 38 | ## [API](https://thlorenz.github.io/pec) 39 | 40 | 41 | 42 | ### BackgroundWorker.raceRange 43 | 44 | **Parameters** 45 | 46 | - `combo` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>** to race i.e. `[ 'As', 'Ad' ]` 47 | - `total` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** the total number of times to race, `100` are processed 48 | each time and `update` invoked until the `total` is reached 49 | - `trackCombos` **[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)?** if `true` the counts for each combos are tracked (optional, default `false`) 50 | - `board` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>?** if supplied the range will be raced 51 | against subsets boards that include all cards of the given board (optional, default `null`) 52 | 53 | Returns **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** the uid generated to identify this background job, 54 | the same uid will be included in the message the result to identify it with the job 55 | 56 | ### BackgroundWorker.stop 57 | 58 | Stops any races in progress. 59 | 60 | ### createBackgroundWorker 61 | 62 | Creates a background worker which uses a web worker 63 | under the hood to process _race_ requests. 64 | 65 | **Parameters** 66 | 67 | - `update` **funcion** will be called with updates: `{ win, loose, tie, iterations, uid }` 68 | 69 | Returns **BackgroundWorker** backgroundWorker 70 | 71 | ### raceCodesForBoard 72 | 73 | Same as @see raceCombosForBoard, except that the combo cards are given 74 | as their codes obtained via [phe](https://github.com/thlorenz/phe) `cardCodes`. 75 | 76 | **Parameters** 77 | 78 | - `combo1` 79 | - `combo2` 80 | - `times` 81 | - `board` 82 | 83 | ### raceCodes 84 | 85 | Same as @see raceCombos, except that the combo cards are given 86 | as their codes obtained via [phe](https://github.com/thlorenz/phe) `cardCodes`. 87 | 88 | **Parameters** 89 | 90 | - `combo1` 91 | - `combo2` 92 | - `times` 93 | 94 | ### raceRangeCodesForBoard 95 | 96 | Same as @see raceRangeForBoard, except that the combo, range cards and board are given 97 | as their codes obtained via [phe](https://github.com/thlorenz/phe) `cardCodes`. 98 | 99 | **Parameters** 100 | 101 | - `comboCodes` 102 | - `rangeCodes` 103 | - `times` 104 | - `trackCombos` 105 | - `boardCodes` 106 | 107 | ### raceRangeCodes 108 | 109 | Same as @see raceRange, except that the combo and range cards are given 110 | as their codes obtained via [phe](https://github.com/thlorenz/phe) `cardCodes`. 111 | 112 | **Parameters** 113 | 114 | - `combo1` 115 | - `range` 116 | - `times` 117 | - `trackCombos` 118 | 119 | ### raceCombosForBoard 120 | 121 | Races two combos against each other. 122 | 123 | **Parameters** 124 | 125 | - `combo1` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>** first combo to race i.e. `[ 'As', 'Ad' ]` 126 | - `combo2` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>** second combo to race i.e. `[ 'As', 'Ad' ]` 127 | - `times` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)?** the number of times to race, if not supplied combos are races against all possible boards (optional, default `null`) 128 | - `board` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>?** omit for preflop, but provide for 129 | postflop to race against boards that just add a turn or river card to the given one (optional, default `null`) 130 | 131 | Returns **any** count of how many times combo1 wins, looses or ties, i.e. `{ win, loose, tie }` 132 | 133 | ### raceCombos 134 | 135 | Races two combos against each other. 136 | 137 | **Parameters** 138 | 139 | - `combo1` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>** first combo to race i.e. `[ 'As', 'Ad' ]` 140 | - `combo2` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>** second combo to race i.e. `[ 'As', 'Ad' ]` 141 | - `times` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)?** the number of times to race, if not supplied combos are races against all possible boards (optional, default `null`) 142 | 143 | Returns **any** count of how many times combo1 wins, looses or ties, i.e. `{ win, loose, tie }` 144 | 145 | ### raceRangeForBoard 146 | 147 | Race the given combo vs. the given combo to count number of wins, losses and ties. 148 | The boards created for the race will include all cards of the given board. 149 | 150 | **Parameters** 151 | 152 | - `combo` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>** to race i.e. `[ 'As', 'Ad' ]` 153 | - `range` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>>** multiple combos to raise against it, i.e. `[ [ 'Ks', 'Kd' ], [ 'Qs', 'Qd' ] ]` 154 | - `times` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)?** the number of times to race, if not supplied combos are races against all possible boards (optional, default `null`) 155 | - `trackCombos` **[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)?** if `true` the counts for each combos are tracked (optional, default `false`) 156 | - `board` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>?** omit for preflop, but provide for 157 | postflop to race against boards that just add a turn or river card to the given one (optional, default `null`) 158 | 159 | Returns **any** count of how many times the combo wins, looses or ties, i.e. `{ win, loose, tie }` 160 | 161 | ### raceRange 162 | 163 | Race the given combo vs. the given combo to count number of wins, losses and ties. 164 | 165 | **Parameters** 166 | 167 | - `combo` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>** to race i.e. `[ 'As', 'Ad' ]` 168 | - `range` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>>** multiple combos to raise against it, i.e. `[ [ 'Ks', 'Kd' ], [ 'Qs', 'Qd' ] ]` 169 | - `times` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)?** the number of times to race, if not supplied combos are races against all possible boards (optional, default `null`) 170 | - `trackCombos` **[Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)?** if `true` the counts for each combos are tracked (optional, default `false`) 171 | 172 | Returns **any** count of how many times the combo wins, looses or ties, i.e. `{ win, loose, tie }` 173 | 174 | ### rates 175 | 176 | Given win, loose and tie count it converts those to winning rates 177 | in percent. 178 | 179 | **Parameters** 180 | 181 | - `$0` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** 182 | - `$0.win` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** number of wins 183 | - `$0.loose` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** number of losses 184 | - `$0.tie` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** number of ties 185 | - `$0.combos` **[Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)<[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String), [Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)>?** map of counts per combo, 186 | if given their rates are calculated as well (optional, default `null`) 187 | 188 | Returns **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** win rates \`{ winRate, looseRate, tieRate, combos? } 189 | 190 | ## License 191 | 192 | MIT 193 | -------------------------------------------------------------------------------- /examples/race-range.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { raceRange, rates } = require('../') 4 | 5 | const combo = [ 'Jh', 'Js' ] 6 | const range = [ 7 | [ 'Kh', 'Ks' ], [ 'Kh', 'Kd' ], [ 'Kh', 'Kc' ], 8 | [ 'Ks', 'Kd' ], [ 'Ks', 'Kc' ], [ 'Kd', 'Kc' ], 9 | [ 'Qh', 'Qs' ], [ 'Qh', 'Qd' ], [ 'Qh', 'Qc' ], 10 | [ 'Qs', 'Qd' ], [ 'Qs', 'Qc' ], [ 'Qd', 'Qc' ] 11 | ] 12 | 13 | const { win, loose, tie } = raceRange(combo, range, 1E4) 14 | const { winRate, looseRate, tieRate } = rates({ win, loose, tie }) 15 | 16 | console.log('JJ performs as follows vs. [ KK, QQ ]') 17 | console.log('win: %d%% (%d times)', winRate, win) 18 | console.log('loose: %d%% (%d times)', looseRate, loose) 19 | console.log('tie: %d%% (%d times)', tieRate, tie) 20 | -------------------------------------------------------------------------------- /examples/webworker-board.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { rates } = require('../') 4 | const backgroundWorker = require('../pec.background') 5 | const { expandRange, arryifyCombo } = require('../test/util/util') 6 | const assert = require('assert') 7 | 8 | const worker = backgroundWorker(onupdate) 9 | 10 | const range = 'QQ+, AK+, AQs+' 11 | const combo = 'JhJs' 12 | const expandedRange = expandRange(range) 13 | const expandedCombo = arryifyCombo(combo) 14 | const board = '8s Th 9d'.split(' ') 15 | 16 | const div = document.createElement('div') 17 | document.body.append(div) 18 | 19 | const trackCombos = false 20 | const raceId = worker.raceRange(expandedCombo, expandedRange, 1E6, trackCombos, board) 21 | 22 | function onupdate({ win, loose, tie, iterations, combos, uid }) { 23 | assert.equal(raceId, uid, 'uid in response needs to match id of initiated race') 24 | const { winRate, looseRate, tieRate } = rates({ 25 | win 26 | , loose 27 | , tie 28 | , combos 29 | }) 30 | 31 | div.innerHTML = ` 32 |
Combo: ${combo} vs. Range: ${range} on Flop: ${board}
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
ComboWinLooseTieIterations
All${winRate}%${looseRate}%${tieRate}%${iterations}
53 | ` 54 | } 55 | -------------------------------------------------------------------------------- /examples/webworker.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { rates } = require('../') 4 | const assert = require('assert') 5 | const backgroundWorker = require('../pec.background') 6 | const { expandRange, arryifyCombo } = require('../test/util/util') 7 | 8 | const worker = backgroundWorker(onupdate) 9 | 10 | const range = 'TT+, AK+, AQs+' 11 | const combo = 'JhJs' 12 | const expandedRange = expandRange(range) 13 | const expandedCombo = arryifyCombo(combo) 14 | 15 | const div = document.createElement('div') 16 | document.body.append(div) 17 | 18 | const trackCombos = true 19 | const raceId = worker.raceRange(expandedCombo, expandedRange, 1E6, trackCombos) 20 | 21 | function onupdate({ win, loose, tie, iterations, combos, uid }) { 22 | assert.equal(raceId, uid, 'uid in response needs to match id of initiated race') 23 | const { winRate, looseRate, tieRate, combos: comboRates } = rates({ 24 | win 25 | , loose 26 | , tie 27 | , combos 28 | }) 29 | 30 | var comboRows = '' 31 | for (const [ k, { winRate, looseRate, tieRate } ] of comboRates) { 32 | comboRows += ( 33 | ` 34 | ${k} 35 | ${winRate}% 36 | ${looseRate}% 37 | ${tieRate}% 38 | ` 39 | ) 40 | } 41 | 42 | div.innerHTML = ` 43 |
Combo: ${combo} vs. Range: ${range}
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | ${comboRows} 63 | 64 |
ComboWinLooseTieIterations
All${winRate}%${looseRate}%${tieRate}%${iterations}
65 | ` 66 | } 67 | -------------------------------------------------------------------------------- /lib/board.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { cardsArrayMinusBlockers } = require('./common') 4 | 5 | function getPermutations(flop1Max, flop2Max, flop3Max, turnMax, riverMax) { 6 | // I bet there is some sound mathematical way to do this, but am at a loss ATM 7 | // PRs welcome ;) 8 | var count = 0 9 | for (var flop1 = 0; flop1 < flop1Max; flop1++) { 10 | for (var flop2 = flop1 + 1; flop2 < flop2Max; flop2++) { 11 | for (var flop3 = flop2 + 1; flop3 < flop3Max; flop3++) { 12 | for (var turn = flop3 + 1; turn < turnMax; turn++) { 13 | for (var river = turn + 1; river < riverMax; river++) { 14 | count++ 15 | } 16 | } 17 | } 18 | } 19 | } 20 | return count 21 | } 22 | 23 | function getPermutationsFlop(turnMax, riverMax) { 24 | var count = 0 25 | for (var turn = 0; turn < turnMax; turn++) { 26 | for (var river = turn + 1; river < riverMax; river++) { 27 | count++ 28 | } 29 | } 30 | return count 31 | } 32 | 33 | // order of cards is not important to determine hand strength 34 | function allPossibleFullBoardCodes(blockers) { 35 | const codes = cardsArrayMinusBlockers(blockers) 36 | const len = codes.length 37 | 38 | const flop1Max = len - 4 39 | const flop2Max = len - 3 40 | const flop3Max = len - 2 41 | const turnMax = len - 1 42 | const riverMax = len 43 | 44 | const permutations = getPermutations(flop1Max, flop2Max, flop3Max, turnMax, riverMax) 45 | const boards = new Uint8Array(permutations * 5) 46 | 47 | var i = 0 48 | for (var flop1 = 0; flop1 < flop1Max; flop1++) { 49 | for (var flop2 = flop1 + 1; flop2 < flop2Max; flop2++) { 50 | for (var flop3 = flop2 + 1; flop3 < flop3Max; flop3++) { 51 | for (var turn = flop3 + 1; turn < turnMax; turn++) { 52 | for (var river = turn + 1; river < riverMax; river++) { 53 | boards[i++] = codes[flop1] 54 | boards[i++] = codes[flop2] 55 | boards[i++] = codes[flop3] 56 | boards[i++] = codes[turn] 57 | boards[i++] = codes[river] 58 | } 59 | } 60 | } 61 | } 62 | } 63 | return boards 64 | } 65 | 66 | function allPossibleTurnAndRiverBoardCodes(blockers, boardCodes) { 67 | // assumes that the given boardCodes has exactly 3 cards 68 | // and that they are already included in the blockers 69 | const flop1Code = boardCodes[0] 70 | const flop2Code = boardCodes[1] 71 | const flop3Code = boardCodes[2] 72 | 73 | const codes = cardsArrayMinusBlockers(blockers) 74 | const len = codes.length 75 | 76 | const turnMax = len - 1 77 | const riverMax = len 78 | 79 | const permutations = getPermutationsFlop(turnMax, riverMax) 80 | const boards = new Uint8Array(permutations * 5) 81 | 82 | var i = 0 83 | for (var turn = 0; turn < turnMax; turn++) { 84 | for (var river = turn + 1; river < riverMax; river++) { 85 | boards[i++] = flop1Code 86 | boards[i++] = flop2Code 87 | boards[i++] = flop3Code 88 | boards[i++] = codes[turn] 89 | boards[i++] = codes[river] 90 | } 91 | } 92 | return boards 93 | } 94 | 95 | function allPossibleRiverBoardCodes(blockers, boardCodes) { 96 | // assumes that the given boardCodes has exactly 4 cards 97 | // and that they are already included in the blockers 98 | const flop1Code = boardCodes[0] 99 | const flop2Code = boardCodes[1] 100 | const flop3Code = boardCodes[2] 101 | const turnCode = boardCodes[3] 102 | 103 | const codes = cardsArrayMinusBlockers(blockers) 104 | const len = codes.length 105 | const permutations = len 106 | const riverMax = len 107 | const boards = new Uint8Array(permutations * 5) 108 | 109 | var i = 0 110 | for (var river = 0; river < riverMax; river++) { 111 | boards[i++] = flop1Code 112 | boards[i++] = flop2Code 113 | boards[i++] = flop3Code 114 | boards[i++] = turnCode 115 | boards[i++] = codes[river] 116 | } 117 | return boards 118 | } 119 | 120 | function allPossiblePostFlopBoardCodes(blockers, boardCodes) { 121 | const bs = new Set(blockers) 122 | for (var i = 0; i < boardCodes.length; i++) bs.add(boardCodes[i]) 123 | 124 | // only supporting flop and turn cases 125 | const len = boardCodes.length 126 | 127 | if (len < 3 || len > 5) { 128 | throw new Error('Only supporting flop(3) or turn(4) or just returning river(5), but given ' + len) 129 | } 130 | 131 | // River 132 | if (len === 5) return boardCodes 133 | 134 | return len === 3 135 | ? allPossibleTurnAndRiverBoardCodes(bs, boardCodes) 136 | : allPossibleRiverBoardCodes(bs, boardCodes) 137 | } 138 | 139 | module.exports = { 140 | allPossibleFullBoardCodes 141 | , allPossiblePostFlopBoardCodes 142 | } 143 | -------------------------------------------------------------------------------- /lib/common.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { cardCode } = require('phe') 4 | 5 | const ranks = [ 'A', 'K', 'Q', 'J', 'T', '9', '8', '7', '6', '5', '4', '3', '2' ] 6 | const suits = [ 'h', 's', 'd', 'c' ] 7 | 8 | const allCardCodes = new Set() 9 | for (var ri = 0; ri < ranks.length; ri++) { 10 | for (var si = 0; si < suits.length; si++) { 11 | allCardCodes.add(cardCode(ranks[ri], suits[si])) 12 | } 13 | } 14 | 15 | function cardsArrayMinusBlockers(blockers) { 16 | const cardCodes = new Set(allCardCodes) 17 | if (blockers != null && blockers.size > 0) { 18 | for (const v of blockers) cardCodes.delete(v) 19 | } 20 | return Array.from(cardCodes) 21 | } 22 | 23 | module.exports = { ranks, suits, allCardCodes, cardsArrayMinusBlockers } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pec", 3 | "version": "0.7.0", 4 | "description": "BetterPoker equity calculator. Compares two combos or one combo against a range.", 5 | "main": "pec.js", 6 | "scripts": { 7 | "test": "set -e; for t in test/*.js; do node $t; done", 8 | "demo": "budo ./examples/webworker.js", 9 | "demo-board": "budo ./examples/webworker-board.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/thlorenz/pec.git" 14 | }, 15 | "homepage": "https://github.com/thlorenz/pec", 16 | "dependencies": { 17 | "phe": "~0.4.1", 18 | "webworkify": "~1.4.0" 19 | }, 20 | "devDependencies": { 21 | "budo": "~10.0.3", 22 | "ocat": "~0.1.0", 23 | "pdetail": "~0.3.0", 24 | "prange": "~0.2.2", 25 | "pretty-hrtime": "~1.0.3", 26 | "spok": "~0.8.1", 27 | "tape": "~4.6.3" 28 | }, 29 | "keywords": [], 30 | "author": { 31 | "name": "Thorsten Lorenz", 32 | "email": "thlorenz@gmx.de", 33 | "url": "http://thlorenz.com" 34 | }, 35 | "license": { 36 | "type": "MIT", 37 | "url": "https://github.com/thlorenz/pec/blob/master/LICENSE" 38 | }, 39 | "engine": { 40 | "node": ">=0.10" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pec.background.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const work = require('webworkify') 4 | const now = Date.now 5 | const workerBlob = require('./pec.worker') 6 | const stopMsg = JSON.stringify({ stop: true }) 7 | 8 | function readMessage(msg, trackCombos) { 9 | if (!trackCombos) { 10 | const [ win, loose, tie, iterations, uid ] = msg 11 | return { win, loose, tie, iterations, uid } 12 | } 13 | const { win, loose, tie, iterations, combos, uid } = msg 14 | return { win, loose, tie, iterations, combos: new Map(combos), uid } 15 | } 16 | 17 | class BackgroundWorker { 18 | constructor(update) { 19 | this._update = update 20 | this._worker = work(workerBlob) 21 | this._onresult = this._onresult.bind(this) 22 | this._stopped = true 23 | this._worker.addEventListener('message', this._onresult) 24 | } 25 | 26 | /** 27 | * 28 | * @name BackgroundWorker.raceRange 29 | * @function 30 | * @param {Array.} combo to race i.e. `[ 'As', 'Ad' ]` 31 | * @param {Array.> range multiple combos to raise against it, i.e. `[ [ 'Ks', 'Kd' ], [ 'Qs', 'Qd' ] ]` 32 | * @param {Number} total the total number of times to race, `100` are processed 33 | * each time and `update` invoked until the `total` is reached 34 | * @param {Boolean} [trackCombos=false] if `true` the counts for each combos are tracked 35 | * @param {Array.} [board=null] if supplied the range will be raced 36 | * against subsets boards that include all cards of the given board 37 | * @return {Number} the uid generated to identify this background job, 38 | * the same uid will be included in the message the result to identify it with the job 39 | */ 40 | raceRange(combo, range, total, trackCombos, board) { 41 | this._trackCombos = !!trackCombos 42 | this._stopped = false 43 | const runAll = total == null 44 | const uid = now() 45 | if (runAll) { 46 | const msg = JSON.stringify({ combo, range, runAll, trackCombos: this._trackCombos, board, uid }) 47 | this._worker.postMessage(msg) 48 | } else { 49 | // let's do 100 at a time to come back with at least some result quickly 50 | // progress communication is a simple array with 3 elements which shouldn't add too much overload 51 | const times = Math.min(total, 100) 52 | const repeat = Math.round(total / times) 53 | const msg = JSON.stringify({ 54 | combo 55 | , range 56 | , runAll 57 | , times 58 | , repeat 59 | , trackCombos: this._trackCombos 60 | , board 61 | , uid 62 | }) 63 | this._worker.postMessage(msg) 64 | } 65 | return uid 66 | } 67 | 68 | /** 69 | * Stops any races in progress. 70 | * 71 | * @name BackgroundWorker.stop 72 | * @function 73 | */ 74 | stop() { 75 | if (this._stopped) return 76 | this._stopped = true 77 | this._worker.postMessage(stopMsg) 78 | } 79 | 80 | _onresult(e) { 81 | if (this._stopped) return 82 | const res = readMessage(e.data, this._trackCombos) 83 | this._update(res) 84 | } 85 | } 86 | 87 | /** 88 | * Creates a background worker which uses a web worker 89 | * under the hood to process _race_ requests. 90 | * 91 | * @name createBackgroundWorker 92 | * @function 93 | * @param {funcion} update will be called with updates: `{ win, loose, tie, iterations, uid }` 94 | * @return {BackgroundWorker} backgroundWorker 95 | */ 96 | module.exports = function createBackgroundWorker(update) { 97 | return new BackgroundWorker(update) 98 | } 99 | -------------------------------------------------------------------------------- /pec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { cardCodes, stringifyCardCode } = require('phe') 4 | const evaluate7Cards = require('phe/lib/evaluator7') 5 | 6 | const { allPossibleFullBoardCodes, allPossiblePostFlopBoardCodes } = require('./lib/board') 7 | const { cardsArrayMinusBlockers } = require('./lib/common') 8 | const EMPTY_ARRAY = [] 9 | 10 | function stringifyTrackedCardComboKeys(codedMap) { 11 | const map = new Map() 12 | for (const code of codedMap) { 13 | const k = code[0] 14 | const v = code[1] 15 | const s1 = stringifyCardCode(k[0]) 16 | const s2 = stringifyCardCode(k[1]) 17 | map.set(s1 + s2, v) 18 | } 19 | return map 20 | } 21 | 22 | // 23 | // Compare Combos/Board 24 | // 25 | function compareTwoWithBoardExpanded(combo1First, combo1Second, combo2First, combo2Second, b1, b2, b3, b4, b5) { 26 | const strength1 = evaluate7Cards(b1, b2, b3, b4, b5, combo1First, combo1Second) 27 | const strength2 = evaluate7Cards(b1, b2, b3, b4, b5, combo2First, combo2Second) 28 | return ( 29 | strength1 === strength2 ? 0 30 | : strength1 < strength2 ? -1 31 | : 1 32 | ) 33 | } 34 | 35 | function compareTwoWithBoard(combo1First, combo1Second, combo2First, combo2Second, board) { 36 | return compareTwoWithBoardExpanded( 37 | combo1First, combo1Second, combo2First, combo2Second, 38 | board[0], board[1], board[2], board[3], board[4]) 39 | } 40 | 41 | // allow excluding up to 4 (flop + turn) 42 | function randomCardIdx(max, a, b, c, d) { 43 | while (true) { 44 | const n = Math.floor(Math.random() * max) 45 | if (a < 0) return n 46 | if (n === a) continue 47 | 48 | if (b < 0) return n 49 | if (n === b) continue 50 | 51 | if (c < 0) return n 52 | if (n === c) continue 53 | 54 | if (d < 0) return n 55 | if (n !== d) return n 56 | } 57 | } 58 | 59 | // 60 | // Generate Board 61 | // 62 | function randomBoard(cardArray, max) { 63 | const flop1 = randomCardIdx(max, -1, -1, -1, -1) 64 | const flop2 = randomCardIdx(max, flop1, -1, -1, -1) 65 | const flop3 = randomCardIdx(max, flop1, flop2, -1, -1) 66 | const turn = randomCardIdx(max, flop1, flop2, flop3, -1) 67 | const river = randomCardIdx(max, flop1, flop2, flop3, turn) 68 | // avoiding slower array.map 69 | const flopCode1 = cardArray[flop1] 70 | const flopCode2 = cardArray[flop2] 71 | const flopCode3 = cardArray[flop3] 72 | const turnCode = cardArray[turn] 73 | const riverCode = cardArray[river] 74 | 75 | return [ flopCode1, flopCode2, flopCode3, turnCode, riverCode ] 76 | } 77 | 78 | function randomRemainingBoard(boardCodes, cardArray, max) { 79 | // Assumes that the boardCodes already are removed from cardArray 80 | const len = boardCodes.length 81 | const flop1 = len > 0 ? 999 : randomCardIdx(max, -1, -1, -1, -1) 82 | const flop2 = len > 1 ? 999 : randomCardIdx(max, flop1, -1, -1, -1) 83 | const flop3 = len > 2 ? 999 : randomCardIdx(max, flop1, flop2, -1, -1) 84 | const turn = len > 3 ? 999 : randomCardIdx(max, flop1, flop2, flop3, -1) 85 | const river = randomCardIdx(max, flop1, flop2, flop3, turn) 86 | 87 | const flopCode1 = len > 0 ? boardCodes[0] : cardArray[flop1] 88 | const flopCode2 = len > 1 ? boardCodes[1] : cardArray[flop2] 89 | const flopCode3 = len > 2 ? boardCodes[2] : cardArray[flop3] 90 | const turnCode = len > 3 ? boardCodes[3] : cardArray[turn] 91 | const riverCode = cardArray[river] 92 | 93 | return [ flopCode1, flopCode2, flopCode3, turnCode, riverCode ] 94 | } 95 | 96 | // 97 | // Race Codes 98 | // 99 | function raceCodesAllForBoard(combo1, combo2, hasBoard, boardCodes) { 100 | const combo1First = combo1[0] 101 | const combo1Second = combo1[1] 102 | const combo2First = combo2[0] 103 | const combo2Second = combo2[1] 104 | 105 | const blockers = new Set([ combo1First, combo1Second, combo2First, combo2Second ]) 106 | 107 | const boards = hasBoard 108 | ? allPossiblePostFlopBoardCodes(blockers, boardCodes) 109 | : allPossibleFullBoardCodes(blockers) 110 | 111 | var win = 0 112 | var loose = 0 113 | var tie = 0 114 | 115 | // Evaluate all 116 | for (var b = 0; b < boards.length; b += 5) { 117 | const res = compareTwoWithBoardExpanded( 118 | combo1First, combo1Second, combo2First, combo2Second, 119 | boards[b], boards[b + 1], boards[b + 2], boards[b + 3], boards[b + 4]) 120 | 121 | if (res === 0) tie++ 122 | else if (res < 0) win++ 123 | else loose++ 124 | } 125 | 126 | return { win, loose, tie } 127 | } 128 | 129 | function raceCodesRandomForBoard(combo1, combo2, times, hasBoard, boardCodes) { 130 | const combo1First = combo1[0] 131 | const combo1Second = combo1[1] 132 | const combo2First = combo2[0] 133 | const combo2Second = combo2[1] 134 | 135 | const blockers = new Set([ combo1First, combo1Second, combo2First, combo2Second ]) 136 | if (hasBoard) { 137 | for (var bi = 0; bi < boardCodes.length; bi++) blockers.add(boardCodes[bi]) 138 | } 139 | 140 | const cardArray = cardsArrayMinusBlockers(blockers) 141 | const cardArrayLen = cardArray.length 142 | 143 | var win = 0 144 | var loose = 0 145 | var tie = 0 146 | 147 | for (var i = 0; i < times; i++) { 148 | const board = hasBoard 149 | ? randomRemainingBoard(boardCodes, cardArray, cardArrayLen) 150 | : randomBoard(cardArray, cardArrayLen) 151 | 152 | const res = compareTwoWithBoard(combo1First, combo1Second, combo2First, combo2Second, board) 153 | if (res === 0) tie++ 154 | else if (res < 0) win++ 155 | else loose++ 156 | } 157 | 158 | return { win, loose, tie } 159 | } 160 | 161 | function _raceCodesForBoard(combo1, combo2, times, hasBoard, boardCodes) { 162 | return times == null 163 | ? raceCodesAllForBoard(combo1, combo2, hasBoard, boardCodes) 164 | : raceCodesRandomForBoard(combo1, combo2, times, hasBoard, boardCodes) 165 | } 166 | 167 | /** 168 | * Same as @see raceCombosForBoard, except that the combo cards are given 169 | * as their codes obtained via [phe](https://github.com/thlorenz/phe) `cardCodes`. 170 | */ 171 | function raceCodesForBoard(combo1, combo2, times, board) { 172 | return _raceCodesForBoard(combo1, combo2, times, true, board) 173 | } 174 | 175 | /** 176 | * Same as @see raceCombos, except that the combo cards are given 177 | * as their codes obtained via [phe](https://github.com/thlorenz/phe) `cardCodes`. 178 | */ 179 | function raceCodes(combo1, combo2, times) { 180 | return _raceCodesForBoard(combo1, combo2, times, false, EMPTY_ARRAY) 181 | } 182 | 183 | // 184 | // Race Range Codes 185 | // 186 | function _raceRangeCodesForBoard(comboCodes1, rangeCodes, times, trackCombos, hasBoard, boardCodes) { 187 | var winCombo = 0 188 | var winRange = 0 189 | var tieBoth = 0 190 | trackCombos = !!trackCombos 191 | const comboCodeMap = trackCombos ? new Map() : null 192 | for (var ci = 0; ci < rangeCodes.length; ci++) { 193 | const comboCodes2 = rangeCodes[ci] 194 | const { win, loose, tie } = hasBoard 195 | ? _raceCodesForBoard(comboCodes1, comboCodes2, times, hasBoard, boardCodes) 196 | : raceCodes(comboCodes1, comboCodes2, times) 197 | 198 | if (trackCombos) comboCodeMap.set(comboCodes2, { win, loose, tie }) 199 | winCombo += win 200 | winRange += loose 201 | tieBoth += tie 202 | } 203 | const res = { win: winCombo, loose: winRange, tie: tieBoth } 204 | if (trackCombos) res.combos = comboCodeMap 205 | return res 206 | } 207 | 208 | /** 209 | * Same as @see raceRangeForBoard, except that the combo, range cards and board are given 210 | * as their codes obtained via [phe](https://github.com/thlorenz/phe) `cardCodes`. 211 | */ 212 | function raceRangeCodesForBoard(comboCodes, rangeCodes, times, trackCombos, boardCodes) { 213 | return _raceRangeCodesForBoard(comboCodes, rangeCodes, times, trackCombos, true, boardCodes) 214 | } 215 | 216 | /** 217 | * Same as @see raceRange, except that the combo and range cards are given 218 | * as their codes obtained via [phe](https://github.com/thlorenz/phe) `cardCodes`. 219 | */ 220 | function raceRangeCodes(combo1, range, times, trackCombos) { 221 | return _raceRangeCodesForBoard(combo1, range, times, trackCombos, false, EMPTY_ARRAY) 222 | } 223 | 224 | // 225 | // Race Combos 226 | // 227 | function _raceCombosForBoard(combo1, combo2, times, hasBoard, boardCodes) { 228 | const comboCodes1 = cardCodes(combo1) 229 | const comboCodes2 = cardCodes(combo2) 230 | return _raceCodesForBoard(comboCodes1, comboCodes2, times, hasBoard, boardCodes) 231 | } 232 | 233 | /** 234 | * Races two combos against each other. 235 | * 236 | * @name raceCombosForBoard 237 | * @function 238 | * @param {Array.} combo1 first combo to race i.e. `[ 'As', 'Ad' ]` 239 | * @param {Array.} combo2 second combo to race i.e. `[ 'As', 'Ad' ]` 240 | * @param {Number} [times=null] the number of times to race, if not supplied combos are races against all possible boards 241 | * @param {Array.}[board=null] omit for preflop, but provide for 242 | * postflop to race against boards that just add a turn or river card to the given one 243 | * @return count of how many times combo1 wins, looses or ties, i.e. `{ win, loose, tie }` 244 | */ 245 | function raceCombosForBoard(combo1, combo2, times, board) { 246 | const boardCodes = cardCodes(board) 247 | return _raceCombosForBoard(combo1, combo2, times, true, boardCodes) 248 | } 249 | 250 | /** 251 | * Races two combos against each other. 252 | * 253 | * @name raceCombos 254 | * @function 255 | * @param {Array.} combo1 first combo to race i.e. `[ 'As', 'Ad' ]` 256 | * @param {Array.} combo2 second combo to race i.e. `[ 'As', 'Ad' ]` 257 | * @param {Number} [times=null] the number of times to race, if not supplied combos are races against all possible boards 258 | * @return count of how many times combo1 wins, looses or ties, i.e. `{ win, loose, tie }` 259 | */ 260 | function raceCombos(combo1, combo2, times) { 261 | return _raceCombosForBoard(combo1, combo2, times, false, EMPTY_ARRAY) 262 | } 263 | 264 | function _raceRangeForBoard(combo, range, times, trackCombos, hasBoard, boardCodes) { 265 | const comboCodes = cardCodes(combo) 266 | const rangeCodes = range.map(cardCodes) 267 | const res = hasBoard 268 | ? raceRangeCodesForBoard(comboCodes, rangeCodes, times, trackCombos, boardCodes) 269 | : raceRangeCodes(comboCodes, rangeCodes, times, trackCombos) 270 | 271 | if (trackCombos) res.combos = stringifyTrackedCardComboKeys(res.combos) 272 | return res 273 | } 274 | 275 | // 276 | // Race Range 277 | // 278 | /** 279 | * Race the given combo vs. the given combo to count number of wins, losses and ties. 280 | * The boards created for the race will include all cards of the given board. 281 | * 282 | * @name raceRangeForBoard 283 | * @function 284 | * @param {Array.} combo to race i.e. `[ 'As', 'Ad' ]` 285 | * @param {Array.>} range multiple combos to raise against it, i.e. `[ [ 'Ks', 'Kd' ], [ 'Qs', 'Qd' ] ]` 286 | * @param {Number} [times=null] the number of times to race, if not supplied combos are races against all possible boards 287 | * @param {Boolean} [trackCombos=false] if `true` the counts for each combos are tracked 288 | * @param {Array.}[board=null] omit for preflop, but provide for 289 | * postflop to race against boards that just add a turn or river card to the given one 290 | * @return count of how many times the combo wins, looses or ties, i.e. `{ win, loose, tie }` 291 | */ 292 | function raceRangeForBoard(combo, range, times, trackCombos, board) { 293 | const boardCodes = cardCodes(board) 294 | return _raceRangeForBoard(combo, range, times, trackCombos, true, boardCodes) 295 | } 296 | 297 | /** 298 | * Race the given combo vs. the given combo to count number of wins, losses and ties. 299 | * 300 | * @name raceRange 301 | * @function 302 | * @param {Array.} combo to race i.e. `[ 'As', 'Ad' ]` 303 | * @param {Array.>} range multiple combos to raise against it, i.e. `[ [ 'Ks', 'Kd' ], [ 'Qs', 'Qd' ] ]` 304 | * @param {Number} [times=null] the number of times to race, if not supplied combos are races against all possible boards 305 | * @param {Boolean} [trackCombos=false] if `true` the counts for each combos are tracked 306 | * @return count of how many times the combo wins, looses or ties, i.e. `{ win, loose, tie }` 307 | */ 308 | function raceRange(combo, range, times, trackCombos) { 309 | return _raceRangeForBoard(combo, range, times, trackCombos, false, EMPTY_ARRAY) 310 | } 311 | 312 | /** 313 | * Given win, loose and tie count it converts those to winning rates 314 | * in percent. 315 | * 316 | * @name rates 317 | * @function 318 | * @param {Object} $0 319 | * @param {Number} $0.win number of wins 320 | * @param {Number} $0.loose number of losses 321 | * @param {Number} $0.tie number of ties 322 | * @param {Map.} [$0.combos=null] map of counts per combo, 323 | * if given their rates are calculated as well 324 | * @return {Object} win rates `{ winRate, looseRate, tieRate, combos? } 325 | */ 326 | function rates({ win, loose, tie, combos }) { 327 | const total = win + loose + tie 328 | const winRate = Math.round(win / total * 100 * 100) / 100 329 | const looseRate = Math.round(loose / total * 100 * 100) / 100 330 | const tieRate = Math.round(tie / total * 100 * 100) / 100 331 | 332 | if (combos == null) return { winRate, looseRate, tieRate } 333 | 334 | const map = new Map() 335 | for (const combo of combos) { 336 | const k = combo[0] 337 | const v = combo[1] 338 | map.set(k, rates(v)) 339 | } 340 | return { winRate, looseRate, tieRate, combos: map } 341 | } 342 | 343 | module.exports = { 344 | raceCodes 345 | , raceCodesForBoard 346 | , raceRangeCodes 347 | , raceRangeCodesForBoard 348 | , raceCombos 349 | , raceCombosForBoard 350 | , raceRange 351 | , raceRangeForBoard 352 | , rates 353 | } 354 | -------------------------------------------------------------------------------- /pec.worker.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { raceRange, raceRangeForBoard } = require('./') 4 | 5 | function createMessage(win, loose, tie, iterations, combos, trackCombos, uid) { 6 | // prefer numeric array message when possible 7 | if (!trackCombos) return [ win, loose, tie, iterations, uid ] 8 | return { 9 | win: win 10 | , loose: loose 11 | , tie: tie 12 | , iterations 13 | , combos: Array.from(combos) 14 | , uid 15 | } 16 | } 17 | 18 | function Worker(hub) { 19 | this._stopped = false 20 | this._onmessage = this._onmessage.bind(this) 21 | this._hub = hub 22 | this._hub.addEventListener('message', this._onmessage) 23 | } 24 | 25 | const proto = Worker.prototype 26 | 27 | proto._onmessage = function _onmessage(e) { 28 | const { 29 | stop = false 30 | , combo 31 | , range 32 | , runAll 33 | , times 34 | , repeat 35 | , trackCombos 36 | , board = null 37 | , uid 38 | } = JSON.parse(e.data) 39 | 40 | this._stopped = stop 41 | if (stop) return 42 | 43 | this._combo = combo 44 | this._range = range 45 | this._trackCombos = trackCombos 46 | this._board = board 47 | this._uid = uid 48 | 49 | if (runAll) return this._runAll() 50 | 51 | this._win = 0 52 | this._loose = 0 53 | this._tie = 0 54 | if (trackCombos) this._combos = new Map() 55 | 56 | this._times = times 57 | this._repeat = repeat 58 | this._run() 59 | } 60 | 61 | proto._runAll = function _runAll() { 62 | const hasBoard = this._board != null 63 | 64 | const { win, loose, tie, combos } = hasBoard 65 | ? raceRangeForBoard(this._combo, this._range, null, this._trackCombos, this._board) 66 | : raceRange(this._combo, this._range, null, this._trackCombos) 67 | const msg = createMessage(win, loose, tie, 1, combos, this._trackCombos, this._uid) 68 | this._hub.postMessage(msg) 69 | } 70 | 71 | proto._updateCombos = function _updateCombos(combos) { 72 | for (const combo of combos) { 73 | const k = combo[0] 74 | const v = combo[1] 75 | 76 | if (!this._combos.has(k)) this._combos.set(k, { win: 0, loose: 0, tie: 0 }) 77 | const val = this._combos.get(k) 78 | val.win += v.win 79 | val.loose += v.loose 80 | val.tie += v.tie 81 | } 82 | } 83 | 84 | proto._run = function _run() { 85 | const self = this 86 | const combo = this._combo 87 | const range = this._range 88 | const board = this._board 89 | const hasBoard = board != null 90 | var i = 0 91 | function dorun() { 92 | const { win, loose, tie, combos } = hasBoard 93 | ? raceRangeForBoard(combo, range, self._times, self._trackCombos, board) 94 | : raceRange(combo, range, self._times, self._trackCombos) 95 | 96 | // Did we get a new request and are handling that currently? 97 | // If so cancel (forget about) the current one. 98 | // Also if we got stopped entirely we are done. 99 | if (self._stopped || combo !== self._combo || range !== self._range || board !== self._board) return 100 | 101 | // Otherwise update the data and send it up 102 | self._win += win 103 | self._loose += loose 104 | self._tie += tie 105 | if (self._trackCombos) self._updateCombos(combos) 106 | 107 | const msg = createMessage( 108 | self._win 109 | , self._loose 110 | , self._tie 111 | , i * self._times 112 | , self._combos 113 | , self._trackCombos 114 | , self._uid 115 | ) 116 | self._hub.postMessage(msg) 117 | 118 | // give messages a chance to process, so we can be stopped and/or 119 | // get a new task to do 120 | if (i++ <= self._repeat) setTimeout(dorun, 0) 121 | } 122 | dorun() 123 | } 124 | 125 | module.exports = function create(hub) { 126 | return new Worker(hub) 127 | } 128 | -------------------------------------------------------------------------------- /test/pec.all.board.range.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const { rateComboVsRangeForBoard } = require('./util/util') 5 | const spok = require('spok') 6 | /* eslint-disable no-unused-vars */ 7 | const ocat = require('./util/ocat') 8 | 9 | test('\nflop:all race combo vs range not tracking combos', function(t) { 10 | const combo = 'AsAh' 11 | const range = 'KK, QQ, JJ' 12 | const board = 'Ad Ts 9h'.split(' ') 13 | const res = rateComboVsRangeForBoard(combo, range, null, false, board) 14 | 15 | spok(t, res, 16 | { winRate: 96.67, looseRate: 3.33, tieRate: 0 }) 17 | 18 | t.end() 19 | }) 20 | 21 | test('\nflop:all race combo vs range tracking combos', function(t) { 22 | const combo = 'AsAh' 23 | const range = 'KK, QQ, JJ' 24 | const board = 'Ad Ts 9h'.split(' ') 25 | const res = rateComboVsRangeForBoard(combo, range, null, true, board) 26 | 27 | spok(t, res, 28 | { winRate: 96.67, looseRate: 3.33, tieRate: 0 }) 29 | 30 | spok(t, Array.from(res.combos), 31 | [ [ 'KhKs', { winRate: 98.28, looseRate: 1.72, tieRate: 0 } ] 32 | , [ 'KhKd', { winRate: 98.28, looseRate: 1.72, tieRate: 0 } ] 33 | , [ 'KhKc', { winRate: 98.28, looseRate: 1.72, tieRate: 0 } ] 34 | , [ 'KsKd', { winRate: 98.28, looseRate: 1.72, tieRate: 0 } ] 35 | , [ 'KsKc', { winRate: 98.28, looseRate: 1.72, tieRate: 0 } ] 36 | , [ 'KdKc', { winRate: 98.28, looseRate: 1.72, tieRate: 0 } ] 37 | , [ 'QhQs', { winRate: 96.67, looseRate: 3.33, tieRate: 0 } ] 38 | , [ 'QhQd', { winRate: 96.67, looseRate: 3.33, tieRate: 0 } ] 39 | , [ 'QhQc', { winRate: 96.67, looseRate: 3.33, tieRate: 0 } ] 40 | , [ 'QsQd', { winRate: 96.67, looseRate: 3.33, tieRate: 0 } ] 41 | , [ 'QsQc', { winRate: 96.67, looseRate: 3.33, tieRate: 0 } ] 42 | , [ 'QdQc', { winRate: 96.67, looseRate: 3.33, tieRate: 0 } ] 43 | , [ 'JhJs', { winRate: 95.05, looseRate: 4.95, tieRate: 0 } ] 44 | , [ 'JhJd', { winRate: 95.05, looseRate: 4.95, tieRate: 0 } ] 45 | , [ 'JhJc', { winRate: 95.05, looseRate: 4.95, tieRate: 0 } ] 46 | , [ 'JsJd', { winRate: 95.05, looseRate: 4.95, tieRate: 0 } ] 47 | , [ 'JsJc', { winRate: 95.05, looseRate: 4.95, tieRate: 0 } ] 48 | , [ 'JdJc', { winRate: 95.05, looseRate: 4.95, tieRate: 0 } ] ]) 49 | 50 | t.end() 51 | }) 52 | -------------------------------------------------------------------------------- /test/pec.all.board.single.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const { checkSingleAllForBoard } = require('./util/util') 5 | 6 | const { raceCodesForBoard, rates } = require('../') 7 | const { cardCodes } = require('phe') 8 | 9 | const spok = require('spok') 10 | 11 | test('\nflop:single:all various samples', function(t) { 12 | [ [ 'AsAh', 'KsKh', 'Kd Td 9h', [ 9, 12 ], [ 88, 91 ], [ 0, 0 ] ] 13 | , [ 'AsAh', 'KsKh', 'Ad Td 9h', [ 97, 99 ], [ 1, 2 ], [ 0, 0 ] ] 14 | , [ 'AsAh', 'KsKh', 'Kd Jd Th', [ 19, 24 ], [ 76, 80 ], [ 0.2, 1 ] ] 15 | , [ 'As8h', 'Js6h', '4s 5s 5h', [ 73, 76 ], [ 24, 27 ], [ 0.7, 1.2 ] ] 16 | ].forEach(x => checkSingleAllForBoard.apply(null, [ t ].concat(x))) 17 | t.end() 18 | }) 19 | 20 | test('\nturn:single:all various samples', function(t) { 21 | [ [ 'AsAh', 'KsKh', 'Kd Td 9h 4s', [ 4.2, 5.2 ], [ 94.5, 96 ], [ 0, 0 ] ] 22 | , [ 'As8h', 'Js6d', '4d 5s 5d 2d', [ 60.5, 62 ], [ 38, 39.5 ], [ 0, 0 ] ] 23 | ].forEach(x => checkSingleAllForBoard.apply(null, [ t ].concat(x))) 24 | 25 | t.end() 26 | }) 27 | 28 | test('\nturn:single:all racing codes', function(t) { 29 | const combo1 = cardCodes([ 'As', 'Ah' ]) 30 | const combo2 = cardCodes([ 'Ks', 'Kh' ]) 31 | const board = cardCodes('Kd Td 9h 4s'.split(' ')) 32 | const counts = raceCodesForBoard(combo1, combo2, null, board) 33 | const res = rates(counts) 34 | 35 | spok(t, res, 36 | { winRate: spok.range(4.0, 5.2) 37 | , looseRate: spok.range(94.5, 96) 38 | , tieRate: 0 }) 39 | t.end() 40 | }) 41 | -------------------------------------------------------------------------------- /test/pec.all.range.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const { checkRangeAll } = require('./util/util') 5 | 6 | test('\npreflop:all:range: pair vs. other pairs', function(t) { 7 | [ [ 'AsAh', 'KK, QQ, JJ', 81, 18 ] 8 | , [ 'AsAh', 'JJ+', 59, 13 ] 9 | ].forEach(x => checkRangeAll.apply(null, [ t ].concat(x))) 10 | t.end() 11 | }) 12 | -------------------------------------------------------------------------------- /test/pec.all.single.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const { checkSingleAll } = require('./util/util') 5 | 6 | test('\npreflop:all:single: pair vs. pair', function(t) { 7 | [ [ 'AsAh', 'KsKh', 82 ] 8 | , [ 'JsJh', '7s7h', 81 ] 9 | ].forEach(x => checkSingleAll.apply(null, [ t ].concat(x))) 10 | t.end() 11 | }) 12 | 13 | test('\npreflop:all:single: pair vs. overcards', function(t) { 14 | [ [ 'JsJh', 'KsQh', 56 ] 15 | , [ 'JsJh', 'AsQh', 56 ] 16 | ].forEach(x => checkSingleAll.apply(null, [ t ].concat(x))) 17 | t.end() 18 | }) 19 | 20 | test('\npreflop:all:single: pair vs. undercards', function(t) { 21 | [ [ 'JsJh', 'Ts9h', 86 ] 22 | , [ 'JsJh', '6s2h', 88 ] 23 | ].forEach(x => checkSingleAll.apply(null, [ t ].concat(x))) 24 | t.end() 25 | }) 26 | 27 | test('\npreflop:all:single: pair vs. overcard and undercard', function(t) { 28 | [ [ 'JsJh', 'Ks9h', 72 ] 29 | , [ 'JsJh', 'Qs6h', 72 ] 30 | ].forEach(x => checkSingleAll.apply(null, [ t ].concat(x))) 31 | t.end() 32 | }) 33 | 34 | test('\npreflop:all:single: pair vs. overcard and card match', function(t) { 35 | [ [ 'JsJh', 'KsJh', 70 ] 36 | , [ 'JsJh', 'QsJh', 69 ] 37 | ].forEach(x => checkSingleAll.apply(null, [ t ].concat(x))) 38 | t.end() 39 | }) 40 | 41 | test('\npreflop:all:single: pair vs. undercard and card match', function(t) { 42 | [ [ 'JsJh', 'Js9h', 89 ] 43 | , [ 'JsJh', 'Js6h', 93 ] 44 | ].forEach(x => checkSingleAll.apply(null, [ t ].concat(x))) 45 | t.end() 46 | }) 47 | 48 | test('\npreflop:all:single: two high cards vs. two undercards', function(t) { 49 | [ [ 'KsTh', '7s4h', 66 ] 50 | , [ 'AsQh', 'Js6h', 68 ] 51 | ].forEach(x => checkSingleAll.apply(null, [ t ].concat(x))) 52 | t.end() 53 | }) 54 | 55 | test('\npreflop:all:single: high low card vs. two cards in between', function(t) { 56 | [ [ 'Ks3h', 'Js4h', 59 ] 57 | , [ 'As8h', 'Js9h', 57 ] 58 | ].forEach(x => checkSingleAll.apply(null, [ t ].concat(x))) 59 | t.end() 60 | }) 61 | 62 | test('\npreflop:all:single: high and mid card vs. one between and one below', function(t) { 63 | [ [ 'Ks8h', 'Ts6h', 63 ] 64 | , [ 'As8h', 'Js6h', 64 ] 65 | ].forEach(x => checkSingleAll.apply(null, [ t ].concat(x))) 66 | t.end() 67 | }) 68 | 69 | test('\npreflop:all:single: high and low, matched low card', function(t) { 70 | [ [ 'Ks8h', 'Ts8d', 69 ] 71 | , [ 'As8h', 'Js8d', 71 ] 72 | ].forEach(x => checkSingleAll.apply(null, [ t ].concat(x))) 73 | t.end() 74 | }) 75 | 76 | test('\npreflop:all:single: high vs. matched high and low', function(t) { 77 | [ [ 'Ks8h', 'Kd6h', 56 ] 78 | , [ 'As8h', 'Ad6h', 56 ] 79 | , [ 'AsKh', 'Ad9c', 72 ] 80 | ].forEach(x => checkSingleAll.apply(null, [ t ].concat(x))) 81 | t.end() 82 | }) 83 | -------------------------------------------------------------------------------- /test/pec.board.range.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const { rateComboVsRangeForBoard, expandRange } = require('./util/util') 5 | const { cardCodes } = require('phe') 6 | const { raceRangeCodesForBoard, rates } = require('../') 7 | const spok = require('spok') 8 | /* eslint-disable no-unused-vars */ 9 | const ocat = require('./util/ocat') 10 | 11 | const ITER = 1E4 12 | 13 | test('\nflop: race combo vs range not tracking combos', function(t) { 14 | const combo = 'AsAh' 15 | const range = 'KK, QQ, JJ' 16 | const board = 'Ad Ts 9h'.split(' ') 17 | const res = rateComboVsRangeForBoard(combo, range, ITER, false, board) 18 | spok(t, res, 19 | { winRate: spok.range(95, 98) 20 | , looseRate: spok.range(2, 5) 21 | , tieRate: 0 22 | , combos: spok.notDefined 23 | }) 24 | 25 | t.end() 26 | }) 27 | 28 | test('\nflop:single: race combo codes vs range codes not tracking combos', function(t) { 29 | const combo = [ 'As', 'Ah' ] 30 | const range = expandRange('KK, QQ, JJ') 31 | const board = 'Ad Ts 9h'.split(' ') 32 | 33 | const comboCodes = cardCodes(combo) 34 | const boardCodes = cardCodes(board) 35 | const rangeCodes = range.map(cardCodes) 36 | const counts = raceRangeCodesForBoard(comboCodes, rangeCodes, ITER, false, boardCodes) 37 | const res = rates(counts) 38 | 39 | spok(t, res, 40 | { winRate: spok.range(95, 98) 41 | , looseRate: spok.range(2, 5) 42 | , tieRate: 0 }) 43 | 44 | t.end() 45 | }) 46 | 47 | test('\nturn: race combo vs range not tracking combos', function(t) { 48 | const combo = 'AsAh' 49 | const range = 'KK, QQ, JJ' 50 | const board = 'Ad Ts 9h 8h'.split(' ') 51 | const res = rateComboVsRangeForBoard(combo, range, ITER, false, board) 52 | spok(t, res, 53 | { winRate: spok.range(88, 92) 54 | , looseRate: spok.range(8, 12) 55 | , tieRate: 0 56 | , combos: spok.notDefined 57 | }) 58 | 59 | t.end() 60 | }) 61 | 62 | test('\nturn: race combo codes vs range codes not tracking combos', function(t) { 63 | const combo = [ 'As', 'Ah' ] 64 | const range = expandRange('KK, QQ, JJ') 65 | const board = 'Ad Ts 9h 8h'.split(' ') 66 | 67 | const comboCodes = cardCodes(combo) 68 | const boardCodes = cardCodes(board) 69 | const rangeCodes = range.map(cardCodes) 70 | const counts = raceRangeCodesForBoard(comboCodes, rangeCodes, ITER, false, boardCodes) 71 | const res = rates(counts) 72 | spok(t, res, 73 | { winRate: spok.range(88, 92) 74 | , looseRate: spok.range(8, 12) 75 | , tieRate: 0 76 | , combos: spok.notDefined 77 | }) 78 | 79 | t.end() 80 | }) 81 | 82 | test('\nturn: 4 street on board, race combo vs range not tracking combos', function(t) { 83 | const combo = 'AsAh' 84 | const range = 'KK, QQ, JJ' 85 | const board = 'Qh Ts 9h 8h'.split(' ') 86 | const res = rateComboVsRangeForBoard(combo, range, ITER, false, board) 87 | spok(t, res, 88 | { winRate: spok.range(38, 42) 89 | , looseRate: spok.range(52, 60) 90 | , tieRate: spok.range(2, 6) 91 | , combos: spok.notDefined 92 | }) 93 | 94 | t.end() 95 | }) 96 | 97 | test('\nflop: race combo vs range tracking combos', function(t) { 98 | const combo = 'AsAh' 99 | const range = 'KK, QQ, JJ' 100 | const board = 'Ad Ts 9h'.split(' ') 101 | const res = rateComboVsRangeForBoard(combo, range, ITER, true, board) 102 | 103 | const winRate = spok.range(89, 99) 104 | const looseRate = spok.range(1, 11) 105 | const tieRate = 0 106 | 107 | spok(t, Array.from(res.combos), 108 | [ [ 'KhKs', { winRate, looseRate, tieRate } ] 109 | , [ 'KhKd', { winRate, looseRate, tieRate } ] 110 | , [ 'KhKc', { winRate, looseRate, tieRate } ] 111 | , [ 'KsKd', { winRate, looseRate, tieRate } ] 112 | , [ 'KsKc', { winRate, looseRate, tieRate } ] 113 | , [ 'KdKc', { winRate, looseRate, tieRate } ] 114 | , [ 'QhQs', { winRate, looseRate, tieRate } ] 115 | , [ 'QhQd', { winRate, looseRate, tieRate } ] 116 | , [ 'QhQc', { winRate, looseRate, tieRate } ] 117 | , [ 'QsQd', { winRate, looseRate, tieRate } ] 118 | , [ 'QsQc', { winRate, looseRate, tieRate } ] 119 | , [ 'QdQc', { winRate, looseRate, tieRate } ] 120 | , [ 'JhJs', { winRate, looseRate, tieRate } ] 121 | , [ 'JhJd', { winRate, looseRate, tieRate } ] 122 | , [ 'JhJc', { winRate, looseRate, tieRate } ] 123 | , [ 'JsJd', { winRate, looseRate, tieRate } ] 124 | , [ 'JsJc', { winRate, looseRate, tieRate } ] 125 | , [ 'JdJc', { winRate, looseRate, tieRate } ] ]) 126 | t.end() 127 | }) 128 | -------------------------------------------------------------------------------- /test/pec.board.single.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const { checkSingleForBoard } = require('./util/util') 5 | 6 | const { raceCodesForBoard, rates } = require('../') 7 | const { cardCodes } = require('phe') 8 | 9 | const spok = require('spok') 10 | 11 | // There are a total of 49 * 48 (2,352) boards for a given flop 12 | // However we're running randomly, so 1E4 (default ITER) should cover us 13 | 14 | test('\nflop:single: various samples', function(t) { 15 | [ [ 'AsAh', 'KsKh', 'Kd Td 9h', [ 9, 12 ], [ 88, 91 ], [ 0, 0 ] ] 16 | , [ 'AsAh', 'KsKh', 'Ad Td 9h', [ 97, 99 ], [ 1, 3 ], [ 0, 0 ] ] 17 | , [ 'AsAh', 'KsKh', 'Kd Jd Th', [ 19, 24 ], [ 76, 80 ], [ 0.2, 1 ] ] 18 | , [ 'As8h', 'Js6h', '4s 5s 5h', [ 73, 76 ], [ 23, 27 ], [ 0.7, 1.2 ] ] 19 | ].forEach(x => checkSingleForBoard.apply(null, [ t ].concat(x))) 20 | t.end() 21 | }) 22 | 23 | test('\nturn:single: various samples', function(t) { 24 | [ [ 'AsAh', 'KsKh', 'Kd Td 9h 4s', [ 4.0, 5.2 ], [ 94.5, 96 ], [ 0, 0 ] ] 25 | , [ 'As8h', 'Js6d', '4d 5s 5d 2d', [ 60, 64 ], [ 38, 39.5 ], [ 0, 0 ] ] 26 | ].forEach(x => checkSingleForBoard.apply(null, [ t ].concat(x))) 27 | 28 | t.end() 29 | }) 30 | 31 | const ITER = 1E4 32 | test('\nflop:single: racing codes', function(t) { 33 | const combo1 = cardCodes([ 'As', 'Ah' ]) 34 | const combo2 = cardCodes([ 'Ks', 'Kh' ]) 35 | const board = cardCodes('Kd Td 9h 4s'.split(' ')) 36 | const counts = raceCodesForBoard(combo1, combo2, ITER, board) 37 | const res = rates(counts) 38 | 39 | spok(t, res, 40 | { winRate: spok.range(4.0, 5.2) 41 | , looseRate: spok.range(94.5, 96) 42 | , tieRate: 0 }) 43 | t.end() 44 | }) 45 | -------------------------------------------------------------------------------- /test/pec.range.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const { checkRange, rateComboVsRange } = require('./util/util') 5 | 6 | test('\npreflop:range: pair vs. other pairs', function(t) { 7 | [ [ 'AsAh', 'KK, QQ, JJ', 81, 18 ] 8 | , [ 'AsAh', 'JJ+', 59, 13 ] 9 | , [ 'JsJh', 'QQ+', 18, 81 ] 10 | , [ 'JsJh', '88+', 43, 43 ] 11 | ].forEach(x => checkRange.apply(null, [ t ].concat(x))) 12 | t.end() 13 | }) 14 | 15 | test('\npreflop:range: ensuring per combo data', function(t) { 16 | const { 17 | winRate 18 | , looseRate 19 | , tieRate 20 | , combos 21 | } = rateComboVsRange('AsAh', 'KK, QQ, JJ', 10000, true) 22 | 23 | /* eslint-disable yoda */ 24 | t.ok(80 < winRate && winRate < 85, 'winrate') 25 | t.ok(15 < looseRate && looseRate < 20, 'looserate') 26 | t.ok(0.1 < tieRate && tieRate < 1, 'tierate') 27 | 28 | ;['KhKs', 'KhKd', 'KhKc', 'KsKd', 'KsKc', 'KdKc', 29 | 'QhQs', 'QhQd', 'QhQc', 'QsQd', 'QsQc', 'QdQc', 30 | 'JhJs', 'JhJd', 'JhJc', 'JsJd', 'JsJc', 'JdJc' ] 31 | .forEach(x => { 32 | t.ok(combos.has(x), 'combos include ' + x) 33 | const { winRate, looseRate, tieRate } = combos.get(x) 34 | t.ok(75 < winRate && winRate < 85, 'combo ' + x + ' winrate') 35 | t.ok(12 < looseRate && looseRate < 22, 'combo ' + x + ' looserate') 36 | t.ok(0.01 < tieRate && tieRate < 1, 'combo ' + x + ' tierate') 37 | }) 38 | t.end() 39 | }) 40 | -------------------------------------------------------------------------------- /test/pec.single.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const { checkSingle } = require('./util/util') 5 | 6 | test('\npreflop:single: pair vs. pair', function(t) { 7 | [ [ 'AsAh', 'KsKh', 82 ] 8 | , [ 'JsJh', '7s7h', 81 ] 9 | , [ 'JsJh', 'TsTh', 82 ] 10 | , [ 'AsAh', '2s2h', 83 ] 11 | ].forEach(x => checkSingle.apply(null, [ t ].concat(x))) 12 | t.end() 13 | }) 14 | 15 | test('\npreflop:single: pair vs. overcards', function(t) { 16 | [ [ 'JsJh', 'KsQh', 56 ] 17 | , [ 'JsJh', 'AsQh', 56 ] 18 | , [ 'JsJh', 'AcQc', 54 ] 19 | , [ 'JsJh', 'KcQc', 53 ] 20 | , [ '2s2h', 'KcQc', 48 ] 21 | ].forEach(x => checkSingle.apply(null, [ t ].concat(x))) 22 | t.end() 23 | }) 24 | 25 | test('\npreflop:single: pair vs. undercards', function(t) { 26 | [ [ 'JsJh', 'Ts9h', 86 ] 27 | , [ 'JsJh', '6s2h', 88 ] 28 | , [ 'JsJh', 'Tc9c', 81 ] 29 | , [ 'JsJh', '6c2c', 82 ] 30 | , [ '6s6h', '2c3c', 79 ] 31 | ].forEach(x => checkSingle.apply(null, [ t ].concat(x))) 32 | t.end() 33 | }) 34 | 35 | test('\npreflop:single: pair vs. overcard and undercard', function(t) { 36 | [ [ 'JsJh', 'Ks9h', 72 ] 37 | , [ 'JsJh', 'Qs6h', 72 ] 38 | , [ 'JsJh', 'Kc9c', 68 ] 39 | , [ 'JsJh', 'Kc6c', 68 ] 40 | , [ '6s6h', 'Tc3c', 65 ] 41 | ].forEach(x => checkSingle.apply(null, [ t ].concat(x))) 42 | t.end() 43 | }) 44 | 45 | test('\npreflop:single: pair vs. overcard and card match', function(t) { 46 | [ [ 'JsJh', 'KsJh', 70 ] 47 | , [ 'JsJh', 'QsJh', 69 ] 48 | , [ 'JsJh', 'KcJc', 63 ] 49 | , [ 'JsJh', 'KcJc', 63 ] 50 | , [ '6s6h', 'Tc6c', 61 ] 51 | ].forEach(x => checkSingle.apply(null, [ t ].concat(x))) 52 | t.end() 53 | }) 54 | 55 | test('\npreflop:single: pair vs. undercard and card match', function(t) { 56 | [ [ 'JsJh', 'Js9h', 89 ] 57 | , [ 'JsJh', 'Js6h', 93 ] 58 | , [ 'JsJh', 'Jc9c', 81 ] 59 | , [ 'JsJh', 'Jc6c', 85 ] 60 | , [ '6s6h', '6c3c', 83 ] 61 | ].forEach(x => checkSingle.apply(null, [ t ].concat(x))) 62 | t.end() 63 | }) 64 | 65 | test('\npreflop:single: two high cards vs. two undercards', function(t) { 66 | [ [ 'KsTh', '7s4h', 66 ] 67 | , [ 'AsQh', 'Js6h', 68 ] 68 | , [ 'KsTh', '7c4c', 60 ] 69 | , [ 'AsQh', 'Jc6c', 63 ] 70 | , [ 'Js8h', '6c3c', 60 ] 71 | ].forEach(x => checkSingle.apply(null, [ t ].concat(x))) 72 | t.end() 73 | }) 74 | 75 | test('\npreflop:single: high low card vs. two cards in between', function(t) { 76 | [ [ 'Ks3h', 'Js4h', 59 ] 77 | , [ 'As8h', 'Js9h', 57 ] 78 | , [ 'Ks3h', '7c4c', 53 ] 79 | , [ 'As8h', 'JcTc', 52 ] 80 | , [ 'Js3h', '6c4c', 52 ] 81 | ].forEach(x => checkSingle.apply(null, [ t ].concat(x))) 82 | t.end() 83 | }) 84 | 85 | test('\npreflop:single: high and mid card vs. one between and one below', function(t) { 86 | [ [ 'Ks8h', 'Ts6h', 63 ] 87 | , [ 'As8h', 'Js6h', 64 ] 88 | , [ 'Ks3h', '7c2c', 59 ] 89 | , [ 'As8h', 'Jc5c', 59 ] 90 | , [ 'Js3h', '6c2c', 57 ] 91 | ].forEach(x => checkSingle.apply(null, [ t ].concat(x))) 92 | t.end() 93 | }) 94 | 95 | test('\npreflop:single: high and low, matched low card', function(t) { 96 | [ [ 'Ks8h', 'Ts8d', 69 ] 97 | , [ 'As8h', 'Js8d', 71 ] 98 | , [ 'Ks3h', '7c3c', 65 ] 99 | , [ 'As8h', 'Jc8c', 65 ] 100 | , [ 'Js3h', '6c3c', 61 ] 101 | ].forEach(x => checkSingle.apply(null, [ t ].concat(x))) 102 | t.end() 103 | }) 104 | 105 | test('\npreflop:single: high vs. matched high and low', function(t) { 106 | [ [ 'Ks8h', 'Kd6h', 56 ] 107 | , [ 'As8h', 'Ad6h', 57 ] 108 | , [ 'Ks3h', 'Kc2c', 26 ] 109 | , [ 'As8h', 'Ac5c', 52 ] 110 | , [ 'Js3h', 'Jd2c', 28 ] 111 | , [ 'AsKh', 'Ad9c', 71 ] 112 | ].forEach(x => checkSingle.apply(null, [ t ].concat(x))) 113 | t.end() 114 | }) 115 | -------------------------------------------------------------------------------- /test/util/ocat.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var ocat = require('ocat') 4 | ocat.opts = { 5 | prefix: ' spok(t, res, \n', 6 | suffix: ')', 7 | indent: ' ', 8 | depth: 5 9 | } 10 | 11 | module.exports = ocat 12 | -------------------------------------------------------------------------------- /test/util/util.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { 4 | raceCombos 5 | , raceCombosForBoard 6 | , raceRange 7 | , raceRangeForBoard 8 | , rates 9 | } = require('../../') 10 | const prange = require('prange') 11 | const { detailRange } = require('pdetail') 12 | const spok = require('spok') 13 | const ocat = require('./ocat') 14 | 15 | // 10,000 iterations seems minimum to get somewhat reliable results 16 | // Still we use maxDeviation to keep tests from failing too often 17 | const ITER = 1E4 18 | 19 | function arryifyCombo(combo) { 20 | return [ combo[0] + combo[1], combo[2] + combo[3] ] 21 | } 22 | 23 | function expandRange(range) { 24 | const rangeSetArray = prange(range).map(detailRange) 25 | const rangeArray = [] 26 | rangeSetArray.forEach(x => Array.from(x).forEach(c => { 27 | rangeArray.push(arryifyCombo(c)) 28 | })) 29 | return rangeArray 30 | } 31 | 32 | function raceSingle(combo1, combo2, times) { 33 | const arr1 = arryifyCombo(combo1) 34 | const arr2 = arryifyCombo(combo2) 35 | const res = raceCombos(arr1, arr2, times) 36 | return rates(res) 37 | } 38 | 39 | function raceSingleForBoard(combo1, combo2, times, board) { 40 | const arr1 = arryifyCombo(combo1) 41 | const arr2 = arryifyCombo(combo2) 42 | const res = raceCombosForBoard(arr1, arr2, times, board) 43 | return rates(res) 44 | } 45 | 46 | function checkSingle(t, combo1, combo2, expectedWinRate, maxDeviation = 4, times = ITER) { 47 | const { winRate: win, looseRate: loose, tieRate: tie } = raceSingle(combo1, combo2, times) 48 | const msg = `${combo1} wins ${expectedWinRate}% vs.${combo2}, actual ${win}% vs ${loose}%, tie: ${tie}%` 49 | const pass = expectedWinRate - maxDeviation < win && win < expectedWinRate + maxDeviation 50 | t.ok(pass, msg) 51 | } 52 | 53 | function checkSingleForBoard(t, combo1, combo2, board, win, loose, tie, times = ITER) { 54 | const res = raceSingleForBoard(combo1, combo2, ITER, board.split(' ')) 55 | if (win == null) { 56 | ocat.log(res) 57 | return 58 | } 59 | spok(t, res, 60 | { $topic : `${combo1} vs ${combo2} on ${board}` 61 | , winRate : spok.range(win[0], win[1]) 62 | , looseRate : spok.range(loose[0], loose[1]) 63 | , tieRate : spok.range(tie[0], tie[1]) 64 | } 65 | ) 66 | } 67 | 68 | function checkSingleAll(t, combo1, combo2, expectedWinRate, maxDeviation = 4) { 69 | const { winRate: win, looseRate: loose, tieRate: tie } = raceSingle(combo1, combo2, null) 70 | const msg = `${combo1} wins ${expectedWinRate}% vs.${combo2}, actual ${win}% vs ${loose}%, tie: ${tie}%` 71 | const pass = expectedWinRate - maxDeviation < win && win < expectedWinRate + maxDeviation 72 | t.ok(pass, msg) 73 | } 74 | 75 | function checkSingleAllForBoard(t, combo1, combo2, board, win, loose, tie) { 76 | const res = raceSingleForBoard(combo1, combo2, null, board.split(' ')) 77 | if (win == null) { 78 | ocat.log(res) 79 | return 80 | } 81 | spok(t, res, 82 | { $topic : `${combo1} vs ${combo2} on ${board}` 83 | , winRate : spok.range(win[0], win[1]) 84 | , looseRate : spok.range(loose[0], loose[1]) 85 | , tieRate : spok.range(tie[0], tie[1]) 86 | } 87 | ) 88 | } 89 | 90 | function raceComboVsRange(combo, range, times, trackCombos) { 91 | const arr = arryifyCombo(combo) 92 | const expanded = expandRange(range) 93 | return raceRange(arr, expanded, times, trackCombos) 94 | } 95 | 96 | function raceComboVsRangeForBoard(combo, range, times, trackCombos, board) { 97 | const arr = arryifyCombo(combo) 98 | const expanded = expandRange(range) 99 | return raceRangeForBoard(arr, expanded, times, trackCombos, board) 100 | } 101 | 102 | function rateComboVsRange(combo, range, times, trackCombos) { 103 | const res = raceComboVsRange(combo, range, times, trackCombos) 104 | return rates(res) 105 | } 106 | 107 | function rateComboVsRangeForBoard(combo, range, times, trackCombos, board) { 108 | const res = raceComboVsRangeForBoard(combo, range, times, trackCombos, board) 109 | return rates(res) 110 | } 111 | 112 | function checkRange(t, combo, range, expectedWin, expectedLoose, maxDeviation = 4, times = ITER) { 113 | const { winRate: win, looseRate: loose, tieRate: tie } = rateComboVsRange(combo, range, times) 114 | const msg = `${combo} wins ${expectedWin}% and looses ${expectedLoose}% vs ${range}, actual ${win}% vs ${loose}%, tie: ${tie}` 115 | const pass = expectedWin - maxDeviation < win && win < expectedWin + maxDeviation && 116 | expectedLoose - maxDeviation < loose && loose < expectedLoose + maxDeviation 117 | t.ok(pass, msg) 118 | } 119 | 120 | function checkRangeAll(t, combo, range, expectedWin, expectedLoose, maxDeviation = 4) { 121 | const { winRate: win, looseRate: loose, tieRate: tie } = rateComboVsRange(combo, range, null) 122 | const msg = `${combo} wins ${expectedWin}% and looses ${expectedLoose}% vs ${range}, actual ${win}% vs ${loose}%, tie: ${tie}` 123 | const pass = expectedWin - maxDeviation < win && win < expectedWin + maxDeviation && 124 | expectedLoose - maxDeviation < loose && loose < expectedLoose + maxDeviation 125 | t.ok(pass, msg) 126 | } 127 | 128 | module.exports = { 129 | checkSingle 130 | , checkSingleForBoard 131 | , checkSingleAll 132 | , checkSingleAllForBoard 133 | , checkRange 134 | , checkRangeAll 135 | , expandRange 136 | , arryifyCombo 137 | , raceComboVsRange 138 | , rateComboVsRange 139 | , raceComboVsRangeForBoard 140 | , rateComboVsRangeForBoard 141 | , raceSingleForBoard 142 | } 143 | --------------------------------------------------------------------------------