Last active
October 4, 2019 13:06
-
-
Save dleshem/2697034d082ebd9e213b111eae1b2ffe to your computer and use it in GitHub Desktop.
Smashing PokerStars All-In Cash Out for Fun and Profit
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const d3 = require('d3-array'); | |
const Combinatorics = require('js-combinatorics'); | |
const OddsCalculator = require('cardplayer-odds-calculator'); | |
const PromiseThrottle = require('promise-throttle'); | |
const oddsCalculator = new OddsCalculator({timeout: 30000}); | |
const odds = async (holes, dead = []) => { | |
switch(holes[0].length) { | |
case 2: return oddsCalculator.texasHoldem({holes, dead}); | |
case 4: return oddsCalculator.omahaHoldem({holes, dead}); | |
default: throw new Error(`${holes[0].length} hole cards are not supported`); | |
} | |
}; | |
const promiseThrottle = new PromiseThrottle({ | |
requestsPerSecond: 3, | |
promiseImplementation: Promise | |
}); | |
const calcEv2 = ({win, lose, tie}) => (win + tie/2) / (win + lose + tie); | |
const stringifyHole = hole => hole.join(''); | |
const sample = async (deck, holeCards, players) => { | |
d3.shuffle(deck); | |
const indices = [...Array(players)].map((_, i) => i); | |
const holes = indices.map(i => deck.slice(i*holeCards, (i+1)*holeCards)); | |
const options = []; | |
let playersPart; | |
const playersCmb = Combinatorics.combination(indices, 2); | |
while (playersPart = playersCmb.next()) { | |
const twoHoles = []; | |
const dead = []; | |
for (let i = 0; i < players; ++i) { | |
if ((i === playersPart[0]) || (i === playersPart[1])) { | |
twoHoles.push(holes[i]); | |
} else { | |
dead.push(holes[i][0]); | |
dead.push(holes[i][1]); | |
} | |
} | |
options.push({ | |
index1: playersPart[0], | |
index2: playersPart[1], | |
twoHoles, | |
futureAssumedOdds: promiseThrottle.add(odds.bind(this, twoHoles)), | |
futureRealOdds: promiseThrottle.add(odds.bind(this, twoHoles, dead)) | |
}); | |
} | |
const results = []; | |
const best = {edge: 0}; | |
for (let i = 0; i < options.length; ++i) { | |
const option = options[i]; | |
const assumedEV = calcEv2((await option.futureAssumedOdds)[0]); | |
const realEV = calcEv2((await option.futureRealOdds)[0]); | |
const edge = Math.round(10000*Math.abs(assumedEV - realEV))/10000; | |
results.push({ | |
index1: option.index1, | |
index2: option.index2, | |
twoHoles: option.twoHoles, | |
assumedEV, | |
realEV, | |
edge | |
}); | |
if (edge > best.edge) { | |
best.index1 = option.index1, | |
best.index2 = option.index2, | |
best.edge = edge; | |
best.twoHoles = option.twoHoles; | |
best.assumedEV = assumedEV; | |
best.realEV = realEV; | |
} | |
} | |
console.log(holes); | |
results.forEach(({twoHoles, assumedEV, realEV, edge}) => { | |
console.log(`${stringifyHole(twoHoles[0])} vs ${stringifyHole(twoHoles[1])}: assumed: ${assumedEV}, actual: ${realEV}}, edge: ${edge}`); | |
}); | |
console.log('------------'); | |
console.log(`${stringifyHole(best.twoHoles[0])} vs ${stringifyHole(best.twoHoles[1])} has best edge: ${best.edge}`); | |
return best; | |
}; | |
//////////////////////////////////////// | |
const players = 10; | |
const holeCards = 2; | |
const deck = Combinatorics.cartesianProduct(['2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K', 'A'], ['s', 'c', 'd', 'h']).map(x => x.join('')); | |
(async () => { | |
try { | |
await sample(deck, holeCards, players); | |
} catch (e) {} | |
})(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const d3 = require('d3-array'); | |
const Combinatorics = require('js-combinatorics'); | |
const OddsCalculator = require('cardplayer-odds-calculator'); | |
const PromiseThrottle = require('promise-throttle'); | |
const oddsCalculator = new OddsCalculator({timeout: 30000}); | |
const odds = async (holes, dead = []) => { | |
switch(holes[0].length) { | |
case 2: return oddsCalculator.texasHoldem({holes, dead}); | |
case 4: return oddsCalculator.omahaHoldem({holes, dead}); | |
default: throw new Error(`${holes[0].length} hole cards are not supported`); | |
} | |
}; | |
const promiseThrottle = new PromiseThrottle({ | |
requestsPerSecond: 3, | |
promiseImplementation: Promise | |
}); | |
const calcEv2 = ({win, lose, tie}) => (win + tie/2) / (win + lose + tie); | |
const stringifyHole = hole => hole.join(''); | |
const sample = async (deck, holeCards, players) => { | |
d3.shuffle(deck); | |
const indices = [...Array(players)].map((_, i) => i); | |
const holes = indices.map(i => deck.slice(i*holeCards, (i+1)*holeCards)); | |
const options = []; | |
let playersPart; | |
const playersCmb = Combinatorics.combination(indices, 2); | |
while (playersPart = playersCmb.next()) { | |
const twoHoles = []; | |
const dead = []; | |
for (let i = 0; i < players; ++i) { | |
if ((i === playersPart[0]) || (i === playersPart[1])) { | |
twoHoles.push(holes[i]); | |
} else { | |
dead.push(holes[i][0]); | |
dead.push(holes[i][1]); | |
} | |
} | |
options.push({ | |
index1: playersPart[0], | |
index2: playersPart[1], | |
twoHoles, | |
futureAssumedOdds: promiseThrottle.add(odds.bind(this, twoHoles)), | |
futureRealOdds: promiseThrottle.add(odds.bind(this, twoHoles, dead)) | |
}); | |
} | |
const results = []; | |
const best = {edge: 0}; | |
for (let i = 0; i < options.length; ++i) { | |
const option = options[i]; | |
const assumedEV = calcEv2((await option.futureAssumedOdds)[0]); | |
const realEV = calcEv2((await option.futureRealOdds)[0]); | |
const edge = Math.round(10000*Math.abs(assumedEV - realEV))/10000; | |
results.push({ | |
index1: option.index1, | |
index2: option.index2, | |
twoHoles: option.twoHoles, | |
assumedEV, | |
realEV, | |
edge | |
}); | |
if (edge > best.edge) { | |
best.index1 = option.index1, | |
best.index2 = option.index2, | |
best.edge = edge; | |
best.twoHoles = option.twoHoles; | |
best.assumedEV = assumedEV; | |
best.realEV = realEV; | |
} | |
} | |
console.log(holes); | |
results.forEach(({twoHoles, assumedEV, realEV, edge}) => { | |
console.log(`${stringifyHole(twoHoles[0])} vs ${stringifyHole(twoHoles[1])}: assumed: ${assumedEV}, actual: ${realEV}}, edge: ${edge}`); | |
}); | |
console.log('------------'); | |
console.log(`${stringifyHole(best.twoHoles[0])} vs ${stringifyHole(best.twoHoles[1])} has best edge: ${best.edge}`); | |
return best; | |
}; | |
//////////////////////////////////////// | |
// see https://www.pokerstars.com/poker/room/rake/ | |
const rake = pot => Math.min(pot * 0.05, 2.5); | |
const players = 10; | |
const holeCards = 2; | |
const deck = Combinatorics.cartesianProduct(['2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K', 'A'], ['s', 'c', 'd', 'h']).map(x => x.join('')); | |
const stacks = []; | |
for (let i = 0; i < players; ++i) { | |
stacks[i] = 0; | |
} | |
let bankroll = 0; | |
(async () => { | |
let hands = 0; | |
while (true) { | |
console.log(`hands = ${hands}`); | |
++hands; | |
console.log(`Bankroll = ${bankroll}`); | |
console.log(stacks); | |
let total = bankroll; | |
for (let i = 0; i < players; ++i) { | |
total += stacks[i]; | |
const reload = Math.max(100 - stacks[i], 0); | |
bankroll -= reload; | |
stacks[i] += reload; | |
} | |
console.log(`total = ${total}`); | |
let best; | |
while (true) { | |
try { | |
best = await sample(deck, holeCards, players); | |
break; | |
} catch (e) {} | |
} | |
let allinIndex, cashoutIndex, cashoutOdds, allinOdds; | |
if (best.realEV > best.assumedEV) { | |
allinIndex = best.index1; | |
cashoutIndex = best.index2; | |
cashoutOdds = 1 - best.assumedEV; | |
allinOdds = best.realEV; | |
} else { | |
allinIndex = best.index2; | |
cashoutIndex = best.index1; | |
cashoutOdds = best.assumedEV; | |
allinOdds = 1 - best.realEV; | |
} | |
const effectiveAllin = Math.min(stacks[allinIndex], stacks[cashoutIndex]); | |
stacks[allinIndex] -= effectiveAllin; | |
stacks[cashoutIndex] -= effectiveAllin; | |
const pot = (2 * effectiveAllin) - rake(2 * effectiveAllin); | |
stacks[cashoutIndex] += pot * 0.99 * cashoutOdds; | |
if (Math.random() < allinOdds) { | |
stacks[allinIndex] += pot; | |
} | |
} | |
})(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Hand 1 | Hand 2 | PokerStars | Real | Edge | |
---|---|---|---|---|---|
J♠Q♣ | A♣9♦ | 42.65% | 39.74% | 2.92% | |
J♠Q♣ | K♠9♣ | 42.13% | 43.59% | 1.46% | |
A♣9♦ | K♠9♣ | 74.42% | 80.20% | 5.78% | |
J♠Q♣ | T♠7♦ | 67.63% | 62.05% | 5.58% | |
A♣9♦ | T♠7♦ | 61.56% | 57.88% | 3.68% | |
K♠9♣ | T♠7♦ | 62.61% | 54.35% | 8.26% | |
J♠Q♣ | 3♥Q♠ | 73.32% | 70.02% | 3.30% | |
A♣9♦ | 3♥Q♠ | 63.48% | 64.03% | 0.55% | |
K♠9♣ | 3♥Q♠ | 64.81% | 61.10% | 3.70% | |
T♠7♦ | 3♥Q♠ | 42.74% | 48.73% | 5.99% | |
J♠Q♣ | K♦4♣ | 44.48% | 43.97% | 0.51% | |
A♣9♦ | K♦4♣ | 63.83% | 64.90% | 1.07% | |
K♠9♣ | K♦4♣ | 69.93% | 65.45% | 4.48% | |
T♠7♦ | K♦4♣ | 42.55% | 48.56% | 6.01% | |
3♥Q♠ | K♦4♣ | 36.12% | 36.52% | 0.40% |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
╔═══════════╦════════════╦════════╦════════╗ | |
║ Combo ║ PokerStars ║ Real ║ Edge ║ | |
╠═══════════╬════════════╬════════╬════════╣ | |
║ J♠Q♣|A♣9♦ ║ 42.65% ║ 39.74% ║ 2.92% ║ | |
║ J♠Q♣|K♠9♣ ║ 42.13% ║ 43.59% ║ 1.46% ║ | |
║ A♣9♦|K♠9♣ ║ 74.42% ║ 80.20% ║ 5.78% ║ | |
║ J♠Q♣|T♠7♦ ║ 67.63% ║ 62.05% ║ 5.58% ║ | |
║ A♣9♦|T♠7♦ ║ 61.56% ║ 57.88% ║ 3.68% ║ | |
║ K♠9♣|T♠7♦ ║ 62.61% ║ 54.35% ║ 8.26% ║ ← information edge | |
║ J♠Q♣|3♥Q♠ ║ 73.32% ║ 70.02% ║ 3.30% ║ | |
║ A♣9♦|3♥Q♠ ║ 63.48% ║ 64.03% ║ 0.55% ║ | |
║ K♠9♣|3♥Q♠ ║ 64.81% ║ 61.10% ║ 3.70% ║ | |
║ T♠7♦|3♥Q♠ ║ 42.74% ║ 48.73% ║ 5.99% ║ | |
║ J♠Q♣|K♦4♣ ║ 44.48% ║ 43.97% ║ 0.51% ║ | |
║ A♣9♦|K♦4♣ ║ 63.83% ║ 64.90% ║ 1.07% ║ | |
║ K♠9♣|K♦4♣ ║ 69.93% ║ 65.45% ║ 4.48% ║ | |
║ T♠7♦|K♦4♣ ║ 42.55% ║ 48.56% ║ 6.01% ║ | |
║ 3♥Q♠|K♦4♣ ║ 36.12% ║ 36.52% ║ 0.40% ║ | |
╚═══════════╩════════════╩════════╩════════╝ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment