handranker
This commit is contained in:
parent
6fe4171c00
commit
890e0ab580
|
@ -3,8 +3,6 @@
|
||||||
## Installation & Usage
|
## Installation & Usage
|
||||||
npm i
|
npm i
|
||||||
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
cp config.example.json config.json
|
cp config.example.json config.json
|
||||||
|
|
||||||
edit config.json
|
edit config.json
|
||||||
|
|
Binary file not shown.
|
@ -7,8 +7,7 @@
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./src/index.mjs",
|
"start": "node ./src/index.mjs"
|
||||||
"build": "cd ./data && ./generate_table"
|
|
||||||
},
|
},
|
||||||
"author": "Flummi",
|
"author": "Flummi",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
|
|
@ -1,44 +1,84 @@
|
||||||
import fs from 'node:fs';
|
import constants from './constants.mjs';
|
||||||
|
|
||||||
export default new class handranker {
|
export default new class handranker {
|
||||||
#ranks;
|
rankHands(board, hand) {
|
||||||
|
let bestHand = this.calcBestHand([...board, ...hand].map(c => this.toCard(c)));
|
||||||
|
|
||||||
constructor() {
|
if(bestHand > 6000) bestHand += 2000;
|
||||||
this.#ranks = fs.readFileSync('./data/HandRanks.dat');
|
|
||||||
|
|
||||||
this.handtypes = [
|
|
||||||
"InvalidHand", "HighCard", "Pair",
|
|
||||||
"TwoPairs", "ThreeOfAKind", "Straight",
|
|
||||||
"Flush", "FullHouse", "FourOfAKind",
|
|
||||||
"StraightFlush"
|
|
||||||
];
|
|
||||||
|
|
||||||
this.cards = [''];
|
|
||||||
for(const cv of [..."23456789TJQKA"])
|
|
||||||
for(const cs of [..."CDHS"])
|
|
||||||
this.cards.push(cv + cs);
|
|
||||||
};
|
|
||||||
evalHand(cards) {
|
|
||||||
if(!this.#ranks)
|
|
||||||
throw new Error("HandRanks.dat not loaded, run 'npm run build' first!");
|
|
||||||
|
|
||||||
if(![7,6,5,3].includes(cards.length))
|
|
||||||
throw new Error("Hand must be 3, 5, 6, or 7 cards");
|
|
||||||
|
|
||||||
cards = cards.map(c => this.cards.indexOf(c));
|
|
||||||
|
|
||||||
let p = 53;
|
|
||||||
for(let i = 0; i < cards.length; i++)
|
|
||||||
p = this.#ranks.readUInt32LE((p + cards[i]) * 4);
|
|
||||||
|
|
||||||
if(cards.length == 5 || cards.length == 6)
|
|
||||||
p = this.#ranks.readUInt32LE(p * 4);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
//handType: p >> 12,
|
rank: bestHand,
|
||||||
handRank: p & 0x00000fff,
|
percentage: (9999 - bestHand) / 100,
|
||||||
value: p,
|
combination: this.toCombination(bestHand)
|
||||||
handName: this.handtypes[p >> 12]
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toCombination(r) {
|
||||||
|
if(r > 6185) return 'HighCard';
|
||||||
|
if(r > 3325) return 'Pair';
|
||||||
|
if(r > 2467) return 'TwoPairs';
|
||||||
|
if(r > 1609) return 'ThreeOfAKind';
|
||||||
|
if(r > 1599) return 'Straight';
|
||||||
|
if(r > 322) return 'Flush';
|
||||||
|
if(r > 166) return 'FullHouse';
|
||||||
|
if(r > 10) return 'FourOfAKind';
|
||||||
|
return 'StraightFlush';
|
||||||
|
};
|
||||||
|
|
||||||
|
cactusFastRankHand(hand) {
|
||||||
|
const [c0, c1, c2, c3, c4] = hand;
|
||||||
|
if((c0 & c1 & c2 & c3 & c4 & 0xf000) !== 0)
|
||||||
|
return constants.fastFlushes[(c0 | c1 | c2 | c3 | c4) >>> 16];
|
||||||
|
const r = constants.fastUnique5[(c0 | c1 | c2 | c3 | c4) >>> 16];
|
||||||
|
if(r)
|
||||||
|
return r;
|
||||||
|
let u = 0xe91aaa35 + (((c0 & 0xff) * (c1 & 0xff) * (c2 & 0xff) * (c3 & 0xff) * (c4 & 0xff)) | 0);
|
||||||
|
u = u ^ (u >>> 16);
|
||||||
|
u += u << 8;
|
||||||
|
u ^= u >>> 4;
|
||||||
|
return constants.hash[((u + (u << 2)) >>> 19) ^ (constants.hashAdjust[(u >>> 8) & 0x1ff] | 0)];
|
||||||
|
};
|
||||||
|
|
||||||
|
calcBestHand(hand) {
|
||||||
|
if(hand.length === 5)
|
||||||
|
return this.cactusFastRankHand([hand[0], hand[1], hand[2], hand[3], hand[4]])
|
||||||
|
if(hand.length === 6) {
|
||||||
|
const possibleHands = [
|
||||||
|
[hand[0], hand[1], hand[2], hand[3], hand[4]],
|
||||||
|
[hand[0], hand[1], hand[2], hand[3], hand[5]],
|
||||||
|
[hand[0], hand[1], hand[2], hand[4], hand[5]],
|
||||||
|
[hand[0], hand[1], hand[3], hand[4], hand[5]],
|
||||||
|
[hand[0], hand[2], hand[3], hand[4], hand[5]],
|
||||||
|
[hand[1], hand[2], hand[3], hand[4], hand[5]],
|
||||||
|
];
|
||||||
|
const sortedHands = possibleHands.map(h => this.cactusFastRankHand(h)).sort();
|
||||||
|
return sortedHands[0];
|
||||||
|
}
|
||||||
|
if(hand.length === 7) {
|
||||||
|
let r = 0;
|
||||||
|
let rank = 9999;
|
||||||
|
for(let i = 0; i < 21; i++) {
|
||||||
|
const inputHand = [
|
||||||
|
hand[constants.t7c5[i][0]],
|
||||||
|
hand[constants.t7c5[i][1]],
|
||||||
|
hand[constants.t7c5[i][2]],
|
||||||
|
hand[constants.t7c5[i][3]],
|
||||||
|
hand[constants.t7c5[i][4]],
|
||||||
|
];
|
||||||
|
r = this.cactusFastRankHand(inputHand);
|
||||||
|
if(r < rank)
|
||||||
|
rank = r;
|
||||||
|
}
|
||||||
|
return rank;
|
||||||
|
}
|
||||||
|
throw `Hand ranker doesn't support ${hand.length} cards`;
|
||||||
|
};
|
||||||
|
|
||||||
|
toCard(playingCard) {
|
||||||
|
const rank = constants.runeToRank[playingCard[0]];
|
||||||
|
const suit = constants.runeToSuit[playingCard[1]];
|
||||||
|
if(!suit || rank === undefined)
|
||||||
|
throw `Invalid playing card: ${playingCard}`;
|
||||||
|
return ((1 << rank) << 16) | (suit << 12) | (rank << 8) | constants.PRIMES[rank];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,7 +9,7 @@ try {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new class {
|
export default new class {
|
||||||
suits = { "♠": "s", "♣": "c", "♦": "d", "♥": "h" };
|
suits = { "♠": "S", "♣": "C", "♦": "D", "♥": "H" };
|
||||||
stripColors = msg => msg.replace(/\x03\d{0,2}(,\d{0,2}|\x02\x02)?/g, '');
|
stripColors = msg => msg.replace(/\x03\d{0,2}(,\d{0,2}|\x02\x02)?/g, '');
|
||||||
rand = (max = 1) => ~~(Math.random() * (max - 1) + 1);
|
rand = (max = 1) => ~~(Math.random() * (max - 1) + 1);
|
||||||
|
|
||||||
|
|
136
src/inc/hr.mjs
136
src/inc/hr.mjs
|
@ -1,136 +0,0 @@
|
||||||
import constants from './constants.mjs';
|
|
||||||
|
|
||||||
export default new class handranker {
|
|
||||||
rankHands(board, hand) {
|
|
||||||
const bestHand = this.calcBestHand(hand.map(c => this.toCard(c)), board.map(c => this.toCard(c)));
|
|
||||||
|
|
||||||
return {
|
|
||||||
rank: bestHand.rank,
|
|
||||||
percentage: (9999 - bestHand.rank) / 100,
|
|
||||||
combination: this.toCombination(bestHand.rank),
|
|
||||||
//madeHand: bestHand.madeHand.map((c) => this.toPlayingCard(c)),
|
|
||||||
//unused: bestHand.unused.map((c) => this.toPlayingCard(c)),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
calcBestHand(pocketCards, communityCards) {
|
|
||||||
const cards = [...pocketCards, ...communityCards];
|
|
||||||
const { rank, madeHand } = this.rank567cardHand([...pocketCards, ...communityCards]);
|
|
||||||
return {
|
|
||||||
rank,
|
|
||||||
madeHand,
|
|
||||||
unused: cards.filter((c) => !madeHand.find((mc) => mc === c)),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
toCombination(rank) {
|
|
||||||
const fixedRank = this.toFixedTexasRank(rank);
|
|
||||||
if(fixedRank === 10) return 'StraightFlush';
|
|
||||||
if(fixedRank === 166) return 'FourOfAKind';
|
|
||||||
if(fixedRank === 322) return 'FullHouse';
|
|
||||||
if(fixedRank === 1599) return 'Flush';
|
|
||||||
if(fixedRank === 1609) return 'Straight';
|
|
||||||
if(fixedRank === 2467) return 'ThreeOfAKind';
|
|
||||||
if(fixedRank === 3325) return 'TwoPairs';
|
|
||||||
if(fixedRank === 6185) return 'Pair';
|
|
||||||
if(fixedRank === 7462) return 'HighCard';
|
|
||||||
return 'Invalid';
|
|
||||||
};
|
|
||||||
|
|
||||||
cactusFastRankHand(hand) {
|
|
||||||
const [c0, c1, c2, c3, c4] = hand;
|
|
||||||
if((c0 & c1 & c2 & c3 & c4 & 0xf000) !== 0)
|
|
||||||
return constants.fastFlushes[(c0 | c1 | c2 | c3 | c4) >>> 16];
|
|
||||||
const r = constants.fastUnique5[(c0 | c1 | c2 | c3 | c4) >>> 16];
|
|
||||||
if(r)
|
|
||||||
return r;
|
|
||||||
let u = 0xe91aaa35 + (((c0 & 0xff) * (c1 & 0xff) * (c2 & 0xff) * (c3 & 0xff) * (c4 & 0xff)) | 0);
|
|
||||||
u = u ^ (u >>> 16);
|
|
||||||
u += u << 8;
|
|
||||||
u ^= u >>> 4;
|
|
||||||
return constants.hash[((u + (u << 2)) >>> 19) ^ (constants.hashAdjust[(u >>> 8) & 0x1ff] | 0)];
|
|
||||||
};
|
|
||||||
|
|
||||||
rank567cardHand(hand) {
|
|
||||||
if(hand.length === 5) {
|
|
||||||
return {
|
|
||||||
rank: this.cactusFastRankHand([hand[0], hand[1], hand[2], hand[3], hand[4]]),
|
|
||||||
madeHand: [hand[0], hand[1], hand[2], hand[3], hand[4]],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if(hand.length === 6) {
|
|
||||||
const possibleHands = [
|
|
||||||
[hand[0], hand[1], hand[2], hand[3], hand[4]],
|
|
||||||
[hand[0], hand[1], hand[2], hand[3], hand[5]],
|
|
||||||
[hand[0], hand[1], hand[2], hand[4], hand[5]],
|
|
||||||
[hand[0], hand[1], hand[3], hand[4], hand[5]],
|
|
||||||
[hand[0], hand[2], hand[3], hand[4], hand[5]],
|
|
||||||
[hand[1], hand[2], hand[3], hand[4], hand[5]],
|
|
||||||
];
|
|
||||||
const sortedHands = possibleHands.map(h => ({
|
|
||||||
rank: this.cactusFastRankHand(h),
|
|
||||||
madeHand: h,
|
|
||||||
})).sort((a, b) => a.rank - b.rank);
|
|
||||||
return sortedHands[0];
|
|
||||||
}
|
|
||||||
if (hand.length === 7) {
|
|
||||||
let r = 0;
|
|
||||||
let rank = 9999;
|
|
||||||
let bestHand = [hand[0], hand[1], hand[2], hand[3], hand[4]];
|
|
||||||
for (let i = 0; i < 21; i++) {
|
|
||||||
const inputHand = [
|
|
||||||
hand[constants.t7c5[i][0]],
|
|
||||||
hand[constants.t7c5[i][1]],
|
|
||||||
hand[constants.t7c5[i][2]],
|
|
||||||
hand[constants.t7c5[i][3]],
|
|
||||||
hand[constants.t7c5[i][4]],
|
|
||||||
];
|
|
||||||
r = this.cactusFastRankHand(inputHand);
|
|
||||||
if (r < rank) {
|
|
||||||
rank = r;
|
|
||||||
bestHand = inputHand;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
rank,
|
|
||||||
madeHand: bestHand,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
throw new Error(`Hand ranker doesn't support ${hand.length} cards`);
|
|
||||||
};
|
|
||||||
|
|
||||||
toFixedTexasRank(r) {
|
|
||||||
if(r <= 10) return 10; // StraightFlush
|
|
||||||
if(r <= 166) return 166; // FourOfAKind
|
|
||||||
if(r <= 322) return 322; // FullHouse
|
|
||||||
if(r <= 1599) return 1599; // Flush
|
|
||||||
if(r <= 1609) return 1609; // Straight
|
|
||||||
if(r <= 2467) return 2467; // ThreeOfAKind
|
|
||||||
if(r <= 3325) return 3325; // TwoPairs
|
|
||||||
if(r <= 6185) return 6185; // Pair
|
|
||||||
if(r != 65535) return 7462; // HighCard
|
|
||||||
return 65535; // Invalid
|
|
||||||
};
|
|
||||||
|
|
||||||
toPlayingCard(card) {
|
|
||||||
const rankRune = constants.rankToRune[(card >> 8) & 0xf];
|
|
||||||
const suitRune = constants.suitToRune[(card >> 12) & 0xf];
|
|
||||||
if (!rankRune || !suitRune) {
|
|
||||||
throw new Error(`Cannot convert Card ${card} to PlayingCard`);
|
|
||||||
}
|
|
||||||
return (rankRune + suitRune);
|
|
||||||
};
|
|
||||||
|
|
||||||
toCard(playingCard) {
|
|
||||||
const [rank, suit] = this.toRankAndSuit(playingCard);
|
|
||||||
return ((1 << rank) << 16) | (suit << 12) | (rank << 8) | constants.PRIMES[rank];
|
|
||||||
};
|
|
||||||
|
|
||||||
toRankAndSuit(playingCard) {
|
|
||||||
const rank = constants.runeToRank[playingCard[0]];
|
|
||||||
const suit = constants.runeToSuit[playingCard[1]];
|
|
||||||
if (!suit || rank === undefined)
|
|
||||||
throw new Error(`Invalid playing card: ${playingCard}`);
|
|
||||||
return [rank, suit];
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,16 +1,15 @@
|
||||||
import cuffeo from 'cuffeo';
|
import cuffeo from 'cuffeo';
|
||||||
import handranker from './inc/handranker.mjs';
|
import handranker from './inc/handranker.mjs';
|
||||||
import helper from './inc/helper.mjs';
|
import helper from './inc/helper.mjs';
|
||||||
import hr from './inc/hr.mjs';
|
|
||||||
|
|
||||||
const cfg = helper.config;
|
const cfg = helper.config;
|
||||||
export const bot = await new cuffeo(cfg.getFull().clients);
|
export const bot = await new cuffeo(cfg.getFull().clients);
|
||||||
|
|
||||||
export const env = {
|
export const env = {
|
||||||
gamestate: 'notstarted, ' + (cfg.get('autojoin') ? 'autojoin' : 'no autojoin'), // [notstarted,preflop,flop,turn,river]
|
gamestate: 'not started, ' + (cfg.get('autojoin') ? 'autojoin' : 'no autojoin'),
|
||||||
hand: false,
|
hand: false,
|
||||||
board: false,
|
board: false,
|
||||||
winchance: false,
|
odds: false,
|
||||||
joined: false,
|
joined: false,
|
||||||
callamount: 0,
|
callamount: 0,
|
||||||
pot: 0,
|
pot: 0,
|
||||||
|
@ -43,10 +42,9 @@ bot.on("message", async e => {
|
||||||
let rank;
|
let rank;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
rank = hr.rankHands(board, hand);
|
rank = handranker.rankHands(board, hand);
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.log(err);
|
return e.reply(err);
|
||||||
return e.reply(JSON.stringify(err));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
e.reply(JSON.stringify(rank));
|
e.reply(JSON.stringify(rank));
|
||||||
|
@ -162,7 +160,7 @@ bot.on("message", async e => {
|
||||||
// bot's turn
|
// bot's turn
|
||||||
if(e.message.match(new RegExp(`Current player: ${e.self.me.nickname}( |$)`))) {
|
if(e.message.match(new RegExp(`Current player: ${e.self.me.nickname}( |$)`))) {
|
||||||
let action = 'c'; // default
|
let action = 'c'; // default
|
||||||
if(env.gamestate === 'preflop' || !env.winchance) { // preflop
|
if(env.gamestate === 'preflop' || !env.odds) { // preflop
|
||||||
env.lastaction = action;
|
env.lastaction = action;
|
||||||
return e.reply(action); // checkcall
|
return e.reply(action); // checkcall
|
||||||
}
|
}
|
||||||
|
@ -173,7 +171,7 @@ bot.on("message", async e => {
|
||||||
if(env.callamount > env.bank) // not enough money lol
|
if(env.callamount > env.bank) // not enough money lol
|
||||||
return e.reply(['huan!', 'f']);
|
return e.reply(['huan!', 'f']);
|
||||||
|
|
||||||
if(env.winchance < 6500) {
|
if(env.odds < 6500) {
|
||||||
if(helper.rand(5) === 1 && env.callamount < (helper.rand(2, 6) * 10)) { // bad hand, call anyway
|
if(helper.rand(5) === 1 && env.callamount < (helper.rand(2, 6) * 10)) { // bad hand, call anyway
|
||||||
action = 'c';
|
action = 'c';
|
||||||
}
|
}
|
||||||
|
@ -181,32 +179,32 @@ bot.on("message", async e => {
|
||||||
action = 'f';
|
action = 'f';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(env.winchance > 7000) { // decent hand, raise
|
else if(env.odds > 55) { // decent hand, raise
|
||||||
if(helper.rand(5) === 1) { // 20%
|
if(helper.rand(5) === 1) { // 20%
|
||||||
action = 'r ' + (env.callamount + helper.rand(5) * 10);
|
action = 'r ' + (env.callamount + helper.rand(5) * 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(env.winchance > 15000) { // good hand lol
|
else if(env.odds > 70) { // good hand lol
|
||||||
if(helper.rand(2) === 1) { // 50%
|
if(helper.rand(2) === 1) { // 50%
|
||||||
action = 'r ' + (env.callamount + helper.rand(6) * 10);
|
action = 'r ' + (env.callamount + helper.rand(6) * 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(env.winchance > 20000) { // fuck them all
|
else if(env.odds > 85) { // fuck them all
|
||||||
action = 'r ' + (env.callamount + helper.rand(7) * 10);
|
action = 'r ' + (env.callamount + helper.rand(7) * 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else { // checkphase
|
else { // checkphase
|
||||||
if(env.winchance > 7000) { // decend hand, raise
|
if(env.odds > 55) { // decend hand, raise
|
||||||
if(helper.rand(5) === 1) { // 20%
|
if(helper.rand(5) === 1) { // 20%
|
||||||
action = 'r ' + (env.callamount + helper.rand(5) * 10);
|
action = 'r ' + (env.callamount + helper.rand(5) * 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(env.winchance > 15000) { // good hand lol
|
else if(env.odds > 70) { // good hand lol
|
||||||
if(helper.rand(2) === 1) { // 50%
|
if(helper.rand(2) === 1) { // 50%
|
||||||
action = 'r ' + (env.callamount + helper.rand(6) * 10);
|
action = 'r ' + (env.callamount + helper.rand(6) * 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(env.winchance > 20000) { // fuck them all
|
else if(env.odds > 85) { // fuck them all
|
||||||
action = 'r ' + (env.callamount + helper.rand(7) * 10);
|
action = 'r ' + (env.callamount + helper.rand(7) * 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -226,8 +224,8 @@ bot.on("message", async e => {
|
||||||
if(cfg.get('debug'))
|
if(cfg.get('debug'))
|
||||||
e.reply(`${oldstate} -> ${env.gamestate}`);
|
e.reply(`${oldstate} -> ${env.gamestate}`);
|
||||||
|
|
||||||
const rank = handranker.evalHand([...env.board, ...env.hand]);
|
const rank = handranker.rankHands(env.board, env.hand);
|
||||||
env.winchance = rank.value;
|
env.odds = rank.percentage;
|
||||||
}
|
}
|
||||||
|
|
||||||
// end of game
|
// end of game
|
||||||
|
@ -252,7 +250,7 @@ bot.on("message", async e => {
|
||||||
|
|
||||||
env.hand = false;
|
env.hand = false;
|
||||||
env.board = false;
|
env.board = false;
|
||||||
env.winchance = false;
|
env.odds = false;
|
||||||
env.joined = false;
|
env.joined = false;
|
||||||
env.pot = 0;
|
env.pot = 0;
|
||||||
env.lastaction = false;
|
env.lastaction = false;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user