Compare commits

...

10 Commits

Author SHA1 Message Date
fc55bd2830 check for bank before raise 2023-07-18 16:31:52 +02:00
69f925e0f6 change odds 2023-07-16 19:10:57 +02:00
2cec666f76 readme schmeadme 2023-07-15 07:00:02 +02:00
4cbdf71f0a remove dependency 2023-07-15 06:57:19 +02:00
e5ca195346 verbose & debug in console or chat 2023-07-15 06:56:45 +02:00
bc47818a9d debug verbose level (without trigger lol) 2023-07-15 06:56:15 +02:00
a3be1cb8f1 Merge pull request 'uff' (#1) from uff into master
Reviewed-on: #1
2023-07-13 22:07:21 +00:00
890e0ab580 handranker 2023-07-14 00:06:15 +02:00
6fe4171c00 blah 2023-07-13 19:07:52 +02:00
f234fe0a08 uff 2023-07-12 18:38:42 +02:00
8 changed files with 1598 additions and 90 deletions

View File

@ -1,10 +1,10 @@
# schmirc
A poker bot for the hirc poker bot by nilscc: https://github.com/nilscc/hirc
## Installation & Usage
npm i
npm run build
cp config.example.json config.json
edit config.json

View File

@ -2,6 +2,7 @@
"bot": {
"autojoin": false,
"debug": false,
"verbose": 3,
"pokerbot": "hirc",
"channel": "#poker"
},

Binary file not shown.

View File

@ -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",

1410
src/inc/constants.mjs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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];
};
};

View File

@ -35,6 +35,18 @@ export default new class {
}
};
debug(msg, level = 2) {
const verbose = this.config.get('verbose');
if(level < verbose)
return;
switch(this.config.get('debug')) {
case 'chat': this.sendMsg(msg); break;
case true:
case 'console': console.log(msg); break;
default: return;
}
};
parseCards(msg, output = []) {
for(const c of this.stripColors(msg).match(/(\w+)([♠♣️♦♥️️])/g))
output.push(c.length === 3 ? `T${this.suits[c[2]]}` : `${c[0]}${this.suits[c[1]]}`);

View File

@ -6,10 +6,10 @@ 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,
@ -20,14 +20,36 @@ export const env = {
bot.on("notice", msg => {
if(msg.match(/^Your hand/)) {
env.hand = helper.parseCards(msg);
if(cfg.get('debug'))
console.log('hand:', env.hand);
helper.debug('hand: ' + env.hand.join(', '));
}
});
bot.on("message", async e => {
if(e.channel !== cfg.get('channel'))
return;
if(e.message.startsWith('.cards ')) {
let cards = [];
try {
cards = JSON.parse(e.message.slice(7));
} catch(err) {
return e.reply('That is not an array.');
}
const hand = cards.slice(0, 2);
const board = cards.slice(2);
let rank;
try {
rank = handranker.rankHands(board, hand);
} catch(err) {
return e.reply(err);
}
e.reply(JSON.stringify(rank));
return;
}
if(e.message === `.${e.self.me.nickname} help`) {
await e.write(`PRIVMSG ${e.user.nick} I always say hirc schmirc, available commands are:`);
const commands = [
@ -50,9 +72,13 @@ bot.on("message", async e => {
if(e.message.match(new RegExp(`^${e.self.me.nickname}: Your bank account`))) {
env.bank = +e.message.match(/is: (\d+) \(/)[1];
}
if(e.message === ".debug") {
const tmp = await cfg.set('debug', !cfg.get('debug'));
return e.reply(`debug mode ${tmp ? '[color=green]enabled[/color]' : '[color=red]disabled[/color]'}`);
if(e.message.startsWith(".debug")) {
const tmp = e.message.match(/\.debug (\w+)/)?.[1];
if(!tmp)
return e.reply(`debug mode ${await cfg.set('debug', !cfg.get('debug')) ? '[color=green]enabled[/color]' : '[color=red]disabled[/color]'}`);
if(!['console', 'chat'].includes(tmp))
return e.reply(`debug mode ${await cfg.set('debug', false) ? '[color=green]enabled[/color]' : '[color=red]disabled[/color]'}`);
return e.reply(`debug mode [color=green]enabled[/color] (${await cfg.set('debug', tmp)})`);
}
if(e.message === ".aj" || e.message === ".autojoin") {
const tmp = await cfg.set('autojoin', !cfg.get('autojoin'));
@ -80,8 +106,8 @@ bot.on("message", async e => {
return e.reply("f");
if(e.message === ".d" && !env.hand)
return e.reply("d");
if(e.message === ".env" && cfg.get('debug')) {
e.reply(JSON.stringify(env));
if(e.message === ".env") {
return helper.debug(env, 5);
}
if(e.user.nick !== cfg.get('pokerbot') || !env.hand)
@ -93,8 +119,7 @@ bot.on("message", async e => {
env.pot += env.callamount;
if(e.message.includes(e.self.me.nickname)) {
env.bank -= env.callamount;
if(cfg.get('debug'))
e.reply(`bank: ${env.bank}`);
helper.debug(`bank: ${env.bank}`);
}
return;
}
@ -103,8 +128,7 @@ bot.on("message", async e => {
env.pot += env.callamount;
if(e.message.includes(e.self.me.nickname)) {
env.bank -= env.callamount;
if(cfg.get('debug'))
e.reply(`bank: ${env.bank}`);
helper.debug(`bank: ${env.bank}`);
}
return;
}
@ -114,13 +138,12 @@ bot.on("message", async e => {
const oldstate = env.gamestate;
env.gamestate = 'preflop';
if(cfg.get('debug') && oldstate !== env.gamestate)
e.reply(`${oldstate} -> ${env.gamestate}`);
if(oldstate !== env.gamestate)
helper.debug(`${oldstate} -> ${env.gamestate}`);
if(e.message.includes(e.self.me.nickname)) {
env.bank -= env.callamount;
if(cfg.get('debug'))
e.reply(`bank: ${env.bank}`);
helper.debug(`bank: ${env.bank}`);
}
return;
}
@ -137,57 +160,86 @@ 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
let debug = false;
if(env.gamestate === 'preflop' || !env.odds) { // preflop
env.lastaction = action;
helper.debug('preflop, (checkcall) :(', 3);
return e.reply(action); // checkcall
}
if(e.message.endsWith('to call)')) { // callphase
if(e.message.includes('to call')) { // callphase
env.callamount = +e.message.match(/\((\d+) to call\)/)[1];
if(env.callamount > env.bank) // not enough money lol
return e.reply(['huan!', 'f']);
if(env.winchance < 6500) {
if(helper.rand(5) === 1 && env.callamount < (helper.rand(6,10) * 10)) { // bad hand, call anyway
action = 'c';
if(env.odds <= 40) {
if(helper.rand(20) === 1 && env.callamount < (helper.rand(2, 6) * 10)) { // bad hand, call anyway
action = 'call';
debug = `bad hand, call anyway (${env.odds}, callphase, 5%)`;
}
else { // bad hand, fold
action = 'f';
action = 'fold';
debug = `bad hand (${env.odds}, callphase, 95%)`;
}
}
else if(env.winchance > 7000) { // decent hand, raise
if(helper.rand(5) === 1) { // 20%
action = 'r ' + (env.callamount + helper.rand(5) * 10);
else if(env.odds > 40 && env.odds <= 50) { // decent hand, raise
if(helper.rand(10) === 1) { // 10%
const toRaise = env.callamount + helper.rand(5) * 10;
if(toRaise > env.bank) {
action = 'raise ' + toRaise.toString();
debug = `decent hand (${env.odds}, callphase, 10%)`;
}
}
else if(env.winchance > 15000) { // good hand lol
if(helper.rand(2) === 1) { // 50%
action = 'r ' + (env.callamount + helper.rand(6) * 10);
}
else if(env.odds > 50 && env.odds <= 70) { // good hand lol
if(helper.rand(3) === 1) { // 33%
const toRaise = env.callamount + helper.rand(6) * 10;
if(toRaise > env.bank) {
action = 'raise ' + toRaise.toString();
debug = `good hand lol (${env.odds}, callphase, 33%)`;
}
}
else if(env.winchance > 20000) { // fuck them all
action = 'r ' + (env.callamount + helper.rand(7) * 10);
}
else if(env.odds > 70) { // fuck them all
const toRaise = env.callamount + helper.rand(7) * 10;
if(toRaise > env.bank) {
action = 'raise ' + toRaise.toString();
debug = `fuck them all (${env.odds}, callphase)`;
}
}
}
else { // checkphase
if(env.winchance > 7000) { // decend hand, raise
if(env.odds > 55 && env.odds <= 70) { // decend hand, raise
if(helper.rand(5) === 1) { // 20%
action = 'r ' + (env.callamount + helper.rand(5) * 10);
const toRaise = env.callamount + helper.rand(5) * 10;
if(toRaise > env.bank) {
action = 'raise ' + toRaise.toString();
debug = `decent hand (${env.odds}, checkphase, 20%)`;
}
}
else if(env.winchance > 15000) { // good hand lol
}
else if(env.odds > 70 && env.odds <= 85) { // good hand lol
if(helper.rand(2) === 1) { // 50%
action = 'r ' + (env.callamount + helper.rand(6) * 10);
const toRaise = env.callamount + helper.rand(6) * 10;
if(toRaise > env.bank) {
action = 'raise ' + toRaise.toString();
debug = `good hand lol (${env.odds}, callphase, 50%)`;
}
}
else if(env.winchance > 20000) { // fuck them all
action = 'r ' + (env.callamount + helper.rand(7) * 10);
}
else if(env.odds > 85) { // fuck them all
const toRaise = env.callamount + helper.rand(7) * 10;
if(toRaise > env.bank) {
action = 'raise ' + toRaise.toString();
debug = `fuck them all (${env.odds}, callphase)`;
}
}
}
if(action) {
env.lastaction = action;
helper.debug(debug ? debug : 'check lol', 3);
return e.reply(action);
}
}
@ -198,11 +250,10 @@ bot.on("message", async e => {
const oldstate = env.gamestate;
env.gamestate = e.message.match(/(\w+): /)[1].toLowerCase();
if(cfg.get('debug'))
e.reply(`${oldstate} -> ${env.gamestate}`);
helper.debug(`${oldstate} -> ${env.gamestate}`, 2);
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
@ -216,24 +267,19 @@ bot.on("message", async e => {
env.bank += +e.message.match(/pot(: | of size )(\d+)/)[2];
}
if(cfg.get('debug'))
e.reply(`bank: ${env.bank}`);
helper.debug(`bank: ${env.bank}`);
}
const oldstate = env.gamestate;
env.gamestate = 'notstarted, ' + (cfg.get('autojoin') ? 'autojoin' : 'no autojoin');
if(cfg.get('debug'))
e.reply(`${oldstate} -> ${env.gamestate}`);
env.gamestate = 'not started, ' + (cfg.get('autojoin') ? 'autojoin' : 'no autojoin');
helper.debug(`${oldstate} -> ${env.gamestate}`, 2);
env.hand = false;
env.board = false;
env.winchance = false;
env.odds = false;
env.joined = false;
env.pot = 0;
env.lastaction = false;
return;
}
if(cfg.get('debug'))
console.log(env);
});