diff --git a/README.md b/README.md index 9c52c65..5e0866d 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,6 @@ ## Installation & Usage npm i -npm run build - cp config.example.json config.json edit config.json diff --git a/data/generate_table b/data/generate_table deleted file mode 100755 index 181922d..0000000 Binary files a/data/generate_table and /dev/null differ diff --git a/package.json b/package.json index 8a04c1b..37a93e5 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,7 @@ "x64" ], "scripts": { - "start": "node ./src/index.mjs", - "build": "cd ./data && ./generate_table" + "start": "node ./src/index.mjs" }, "author": "Flummi", "license": "ISC", diff --git a/src/inc/handranker.mjs b/src/inc/handranker.mjs index 8c4456e..15d62c6 100644 --- a/src/inc/handranker.mjs +++ b/src/inc/handranker.mjs @@ -1,44 +1,84 @@ -import fs from 'node:fs'; +import constants from './constants.mjs'; export default new class handranker { - #ranks; + rankHands(board, hand) { + let bestHand = this.calcBestHand([...board, ...hand].map(c => this.toCard(c))); - constructor() { - 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); + if(bestHand > 6000) bestHand += 2000; return { - //handType: p >> 12, - handRank: p & 0x00000fff, - value: p, - handName: this.handtypes[p >> 12] + rank: bestHand, + percentage: (9999 - bestHand) / 100, + combination: this.toCombination(bestHand) }; }; + + 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]; + }; }; diff --git a/src/inc/helper.mjs b/src/inc/helper.mjs index 77a7a45..a0cac56 100644 --- a/src/inc/helper.mjs +++ b/src/inc/helper.mjs @@ -9,7 +9,7 @@ try { } 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, ''); rand = (max = 1) => ~~(Math.random() * (max - 1) + 1); diff --git a/src/inc/hr.mjs b/src/inc/hr.mjs deleted file mode 100644 index cd5d0cb..0000000 --- a/src/inc/hr.mjs +++ /dev/null @@ -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]; - }; -}; diff --git a/src/index.mjs b/src/index.mjs index 782b6c0..f0e3762 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -1,16 +1,15 @@ import cuffeo from 'cuffeo'; import handranker from './inc/handranker.mjs'; import helper from './inc/helper.mjs'; -import hr from './inc/hr.mjs'; const cfg = helper.config; export const bot = await new cuffeo(cfg.getFull().clients); 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, board: false, - winchance: false, + odds: false, joined: false, callamount: 0, pot: 0, @@ -43,10 +42,9 @@ bot.on("message", async e => { let rank; try { - rank = hr.rankHands(board, hand); + rank = handranker.rankHands(board, hand); } catch(err) { - console.log(err); - return e.reply(JSON.stringify(err)); + return e.reply(err); } e.reply(JSON.stringify(rank)); @@ -162,7 +160,7 @@ bot.on("message", async e => { // bot's turn if(e.message.match(new RegExp(`Current player: ${e.self.me.nickname}( |$)`))) { let action = 'c'; // default - if(env.gamestate === 'preflop' || !env.winchance) { // preflop + if(env.gamestate === 'preflop' || !env.odds) { // preflop env.lastaction = action; return e.reply(action); // checkcall } @@ -173,7 +171,7 @@ bot.on("message", async e => { if(env.callamount > env.bank) // not enough money lol 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 action = 'c'; } @@ -181,32 +179,32 @@ bot.on("message", async e => { action = 'f'; } } - else if(env.winchance > 7000) { // decent hand, raise + else if(env.odds > 55) { // decent hand, raise if(helper.rand(5) === 1) { // 20% 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% 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); } } else { // checkphase - if(env.winchance > 7000) { // decend hand, raise + if(env.odds > 55) { // decend hand, raise if(helper.rand(5) === 1) { // 20% 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% 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); } } @@ -226,8 +224,8 @@ bot.on("message", async e => { if(cfg.get('debug')) e.reply(`${oldstate} -> ${env.gamestate}`); - const rank = handranker.evalHand([...env.board, ...env.hand]); - env.winchance = rank.value; + const rank = handranker.rankHands(env.board, env.hand); + env.odds = rank.percentage; } // end of game @@ -246,13 +244,13 @@ bot.on("message", async e => { } const oldstate = env.gamestate; - env.gamestate = 'notstarted, ' + (cfg.get('autojoin') ? 'autojoin' : 'no autojoin'); + env.gamestate = 'not started, ' + (cfg.get('autojoin') ? 'autojoin' : 'no autojoin'); if(cfg.get('debug')) e.reply(`${oldstate} -> ${env.gamestate}`); env.hand = false; env.board = false; - env.winchance = false; + env.odds = false; env.joined = false; env.pot = 0; env.lastaction = false;