Last active
April 1, 2017 23:05
-
-
Save adrianseeley/5917819 to your computer and use it in GitHub Desktop.
Texas Hold'em JS
This file contains 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
var pack_of_cards = ['AS', '2S', '3S', '4S', '5S', '6S', '7S', '8S', '9S', 'TS', 'JS', 'QS', 'KS', 'AC', '2C', '3C', '4C', '5C', '6C', '7C', '8C', '9C', 'TC', 'JC', 'QC', 'KC', 'AD', '2D', '3D', '4D', '5D', '6D', '7D', '8D', '9D', 'TD', 'JD', 'QD', 'KD', 'AH', '2H', '3H', '4H', '5H', '6H', '7H', '8H', '9H', 'TH', 'JH', 'QH', 'KH']; | |
var hash_of_cards = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; | |
var little_blind = 1; | |
var big_blind = 2; | |
var player_pool = | |
[ | |
{id: 'aether', fn: 'aether'}, | |
{id: 'Random Benchmark A', fn: 'random'}, | |
{id: 'Random Benchmark B', fn: 'random'}, | |
{id: 'Random Benchmark C', fn: 'random'}, | |
{id: 'Random Benchmark D', fn: 'random'}, | |
{id: 'Random Benchmark E', fn: 'random'} | |
]; | |
var redis = require('redis'); | |
var db = redis.createClient(); | |
var heart = redis.createClient(); | |
var last_keys = 0; | |
var trail = []; | |
var cluster = require('cluster'); | |
var cpus = require('os').cpus().length; | |
if (cluster.isMaster) { | |
cluster.on('exit', function(worker, code, signal) { throw 'worked died' }); | |
for (var c = 0; c < cpus; c++) cluster.fork(); | |
setInterval(function () { | |
heart.dbsize(function (err, res) { | |
if (err) throw err; | |
console.log('keys: ' + res + '(+' + (res - last_keys) + ')'); | |
last_keys = res; | |
}); | |
}, 10000); | |
return; | |
} | |
console.log('starting process'); | |
function letter_actions (actions) { | |
var ret = actions.join('').split('fold') .join('F') | |
.split('check').join('C') | |
.split('call') .join('P') | |
.split('raise').join('R') | |
.split('allin').join('A'); | |
return ret; | |
}; | |
function unletter_action (action) { | |
return action.split('F').join('fold') | |
.split('C').join('check') | |
.split('P').join('call') | |
.split('R').join('raise') | |
.split('A').join('allin'); | |
}; | |
function copy (object, cb) { | |
return JSON.parse(JSON.stringify(object)); | |
}; | |
function shuffle (deck, cb) { | |
var shuffled = []; | |
while (deck.length > 0) shuffled.push(deck.splice(Math.floor(Math.random() * deck.length), 1)[0]); | |
return shuffled; | |
}; | |
function play_game (players, cb) { | |
for (var p in players) players[p].chips = 10000; | |
do_hand(players, [], function (results) { | |
var nets = ''; for (var r in results) nets += results[r].id.split('Random Benchmark ').join('') + ','; | |
console.log(nets); | |
players = copy(player_pool); | |
setImmediate(function () { play_game(players, cb)}); | |
}); | |
}; | |
function do_hand (players, results, cb) { | |
// move any players out of chips to results | |
for (var p = players.length - 1; p >= 0; p--) if (players[p].chips < little_blind) results.push(players.splice(p, 1)[0]); | |
if (players.length < 2) { | |
// need two players for a hand, end game | |
if (players.length > 0) results.push(players[0]); | |
return cb(results.reverse()); | |
} else { | |
// reset players, play hand | |
play_hand(reset_players(players), function (players, community) { | |
// transmit end state | |
for (var p in players) get_player_action(players[p].fn, '', players, community, function () {}); | |
// try to do another hand | |
setImmediate(function () { do_hand(players, results, cb); }); | |
}); | |
} | |
}; | |
function play_hand (players, cb) { | |
// get a new deck and shuffle it | |
var deck = shuffle(copy(pack_of_cards)); | |
var community = []; | |
// take blinds | |
players = take_blinds(players); | |
// deal hands | |
for (var p in players) players[p].hand = [deck.shift(), deck.shift()]; | |
// betting round | |
betting_round(0, players, community, function (players) { | |
// burn | |
deck.shift(); | |
// flop | |
community = community.concat(deck.splice(0, 3)); | |
// betting round | |
betting_round(0, players, community, function (players) { | |
// burn | |
deck.shift(); | |
// turn | |
community.push(deck.shift()); | |
// betting round | |
betting_round(0, players, community, function (players) { | |
// burn | |
deck.shift(); | |
// river | |
community.push(deck.shift()); | |
// betting round | |
betting_round(0, players, community, function (players) { | |
// showdown | |
return tabulate_results(players, community, cb); | |
}); | |
}); | |
}); | |
}); | |
}; | |
function take_blinds (players, cb) { | |
if (players.length == 2) { | |
players[0].chips -= big_blind; | |
players[0].infor += big_blind; | |
players[0].history.push('big blind'); | |
if (players[0].chips == 0) { | |
players[0].history.push('allin'); | |
players[0].allin = 1; | |
} | |
players[1].chips -= little_blind; | |
players[1].infor += little_blind; | |
players[1].history.push('little blind'); | |
if (players[1].chips == 0) { | |
players[1].history.push('allin'); | |
players[1].allin = 1; | |
} | |
} else { | |
players[1].chips -= little_blind; | |
players[1].infor += little_blind; | |
players[1].history.push('little blind'); | |
if (players[1].chips == 0) { | |
players[1].history.push('allin'); | |
players[1].allin = 1; | |
} | |
players[2].chips -= big_blind; | |
players[2].infor += big_blind; | |
players[2].history.push('big blind'); | |
if (players[2].chips == 0) { | |
players[2].history.push('allin'); | |
players[2].allin = 1; | |
} | |
} | |
return players; | |
}; | |
function betting_round (p, players, community, cb) { | |
var biggest_infor = 0; for (var b in players) if (players[b].infor > biggest_infor) biggest_infor = players[b].infor; | |
if (p >= players.length) { | |
// betting round completed | |
// ensure that everyone who is in is even | |
for (var b in players) { | |
if (!players[b].folded && !players[b].allin && players[b].infor < biggest_infor) { | |
// a player is uneven, start a new betting round | |
return setImmediate(function () { betting_round(0, players, community, cb); }); | |
} | |
} | |
// reaching here implies that all players are even and the betting round is complete | |
return cb(players); | |
} | |
if (players[p].allin || players[p].folded) { | |
// this player is already all in, or has folded | |
players[p].history.push('-'); | |
return betting_round(p + 1, players, community, cb); | |
} | |
var actions = 'fold'; | |
if (players[p].infor == biggest_infor) { | |
// player is at current bet, they can check | |
actions += ',check'; | |
} else { | |
var must_call = biggest_infor - players[p].infor; | |
if (players[p].chips <= must_call) { | |
actions += ',allin'; | |
} else { | |
actions += ',call'; | |
if (players[p].chips <= must_call + little_blind) { | |
actions += ',allin'; | |
} else { | |
actions += ',raise'; | |
} | |
} | |
} | |
get_player_action(players[p].fn, actions, players, community, function (action) { | |
if (actions.split(',').indexOf(action) == -1) { | |
throw 'invalid action: ' + action; | |
action = 'fold'; | |
} | |
switch (action) { | |
case 'fold': | |
players[p].folded = 1; | |
break; | |
case 'check': | |
break; | |
case 'call': | |
players[p].infor += must_call; | |
players[p].chips -= must_call; | |
break; | |
case 'raise': | |
players[p].infor += (must_call + little_blind); | |
players[p].chips -= (must_call + little_blind); | |
break; | |
case 'allin': | |
players[p].allin = 1; | |
players[p].infor += players[p].chips; | |
players[p].chips = 0; | |
break; | |
} | |
players[p].history.push(action); | |
return setImmediate(function () { betting_round(p + 1, players, community, cb); }); | |
}); | |
}; | |
function sort_number (a, b) { | |
var order = 'A23456789TJQK'; | |
return order.indexOf(a.split('')[0]) - order.indexOf(b.split('')[0]); | |
}; | |
function sort_suit (a, b) { | |
var order = 'HCSD'; | |
return order.indexOf(a.split('')[1]) - order.indexOf(b.split('')[1]); | |
}; | |
function value_distance (a, b) { | |
var order = 'A23456789TJQK'; | |
return order.indexOf(a.split('')[0]) - order.indexOf(b.split('')[0]); | |
}; | |
function suit_distance (a, b) { | |
var order = 'HCSD'; | |
return order.indexOf(a.split('')[1]) - order.indexOf(b.split('')[1]); | |
}; | |
function calculate_score (player, community) { | |
if (player.folded) { | |
return -1; | |
} else { | |
var score = 0; | |
var cards = player.hand.concat(community); | |
// check for flush | |
cards.sort(sort_suit); | |
var longest_flush = 0; | |
var current_flush = 0; | |
for (var c = 1; c < cards.length; c++) { | |
var distance = suit_distance(cards[c - 1], cards[c]); | |
if (distance == 0) { | |
current_flush++; | |
if (current_flush > longest_flush) { | |
longest_flush = current_flush; | |
} | |
} else { | |
current_flush = 0; | |
} | |
} | |
if (longest_flush >= 4) { | |
// has flush | |
score += 5; | |
} | |
// check for straight | |
cards.sort(sort_number); | |
var longest_straight = 0; | |
var current_straight = 0; | |
for (var c = 1; c < cards.length; c++) { | |
var distance = value_distance(cards[c - 1], cards[c]); | |
if (distance == -1) { | |
current_straight++; | |
if (current_straight > longest_straight) { | |
longest_straight = current_straight; | |
} | |
} else { | |
current_straight = 0; | |
} | |
} | |
if (longest_straight >= 4) { | |
// has straight | |
score += 4; | |
} | |
// check for multiples | |
for (var c in cards) { | |
cards[c] = {value: cards[c].split('')[0], count: 1}; | |
} | |
for (var c0 = 0; c0 < cards.length - 1; c0++) { | |
for (var c1 = c0 + 1; c1 < cards.length; c1++) { | |
if (cards[c0].value == cards[c1].value) { | |
cards[c0].count++; | |
cards.splice(c1, 1); | |
c1 = c0; | |
} | |
} | |
} | |
cards.sort(function (a, b) { | |
return b.count - a.count; | |
}); | |
if (cards[0].count == 4) { | |
score = Math.max(score, 7); | |
} else if (cards[0].count == 3 && cards[1].count == 2) { | |
score = Math.max(score, 6); | |
} else if (cards[0].count == 3) { | |
score = Math.max(score, 3); | |
} else if (cards[0].count == 2 && cards[1].count == 2) { | |
score = Math.max(score, 2); | |
} else if (cards[0].count == 2) { | |
score = Math.max(score, 1); | |
} | |
return score; | |
} | |
}; | |
function tabulate_results (players, community, cb) { | |
var pot = 0; | |
for (var p in players) { | |
players[p].net = -players[p].infor || 0; | |
pot += players[p].infor; | |
} | |
var still_in = []; | |
for (var p in players) { | |
if (!players[p].folded) { | |
still_in.push(p); | |
} | |
} | |
var winner = [-1]; | |
var winner_score = -1; | |
for (var p in still_in) { | |
var player_score = calculate_score(players[still_in[p]], community); | |
players[still_in[p]].score = player_score; | |
if (player_score == winner_score) { | |
winner.push(still_in[p]); | |
} else if (player_score > winner_score) { | |
winner_score = player_score; | |
winner = [still_in[p]]; | |
} | |
} | |
if (winner.length == 1 && winner[0] == -1) { | |
// everyone folded, pot is forfeit | |
} else { | |
for (var w in winner) { | |
players[winner[w]].net += pot / winner.length; | |
} | |
} | |
return cb(players, community); | |
}; | |
function reset_players (players) { | |
for (var p in players) { | |
players[p].infor = 0; | |
players[p].history = []; | |
delete players[p].net; | |
delete players[p].score; | |
delete players[p].folded; | |
delete players[p].hand; | |
} | |
// rotate dealer | |
players.push(players.shift()); | |
return players; | |
}; | |
function get_player_action (fn, actions, players, community, cb) { | |
actions = actions.split(','); | |
switch (fn) { | |
case 'random': | |
return cb ? cb(actions[Math.floor(Math.random() * actions.length)]) : null; | |
break; | |
case 'alwaysfold': | |
return cb ? cb('fold') : null; | |
break; | |
case 'aether': | |
var cards = []; | |
var net = null; | |
for (var p in players) { | |
if (players[p].id == 'aether') { | |
cards = players[p].hand.concat(community); | |
net = players[p].net || 0; | |
break; | |
} | |
} | |
cards.sort(); | |
for (var c in cards) { | |
cards[c] = hash_of_cards[pack_of_cards.indexOf(cards[c])]; | |
} | |
actions = letter_actions(actions); | |
var hash = cards.join('') + actions; | |
if (actions.length == 0) { | |
// feedback state | |
var feedback = db.multi(); | |
while (trail.length > 0) { | |
var hop = trail.shift(); | |
feedback.hincrby(hop.hash, hop.action, net); | |
} | |
return feedback.exec(function (err, res) { | |
if (err) throw err; | |
return cb(); | |
}); | |
} else { | |
// regular state | |
// get action from redis | |
db.hgetall(hash, function (err, res) { | |
if (err) throw err; | |
var take_action = ''; | |
var keys_explored = Object.keys(res || {}); | |
var keys_unexplored = ''; | |
for (var a in actions) { | |
if (keys_explored.indexOf(actions[a]) == -1) { | |
keys_unexplored += actions[a]; | |
} | |
} | |
if (keys_unexplored.length > 0) { | |
// grab a random unexplored key | |
take_action = keys_unexplored[Math.floor(Math.random() * keys_unexplored.length)]; | |
} else { | |
// all keys explored, take most performant key | |
var highest = null; | |
var at = null; | |
for (var u in keys_explored) { | |
if (highest == null || keys_explored[u] > highest) { | |
at = u; | |
highest = keys_explored[u]; | |
} | |
} | |
take_action = highest; | |
} | |
trail.push({hash: hash, action: take_action}); | |
return cb(unletter_action(take_action)); | |
}); | |
} | |
break; | |
default: | |
throw 'unknown'; | |
break; | |
} | |
}; | |
play_game(copy(player_pool)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment