├── .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 [](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 | Combo |
37 | Win |
38 | Loose |
39 | Tie |
40 | Iterations |
41 |
42 |
43 |
44 |
45 | All |
46 | ${winRate}% |
47 | ${looseRate}% |
48 | ${tieRate}% |
49 | ${iterations} |
50 |
51 |
52 |
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 | Combo |
48 | Win |
49 | Loose |
50 | Tie |
51 | Iterations |
52 |
53 |
54 |
55 |
56 | All |
57 | ${winRate}% |
58 | ${looseRate}% |
59 | ${tieRate}% |
60 | ${iterations} |
61 |
62 | ${comboRows}
63 |
64 |
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 |
--------------------------------------------------------------------------------