Skip to content

Instantly share code, notes, and snippets.

@adrianseeley
Last active April 1, 2017 23:05
Show Gist options
  • Save adrianseeley/5917819 to your computer and use it in GitHub Desktop.
Save adrianseeley/5917819 to your computer and use it in GitHub Desktop.
Texas Hold'em JS
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