A Poker Copilot review with a nice title
It’s called “How To Use Your Mac To Win At Poker (and get your money back)”.
It’s called “How To Use Your Mac To Win At Poker (and get your money back)”.
Poker Copilot 3.18 is now available to download.
What’s changed:
Known problem:
(And if you don’t own it yet, click here to buy Poker Copilot, securely and instantly.)
<!– This update needs to update your database. –>
The continuing story of creating a tool called SeeingStars, offering real time odds for PokerStars. Read from the beginning here.
If you need to evaluate poker hands on a computer quickly and reliably, you need poker-eval. It is highly-optimised C library to evaluate poker hands. I suspect it is used by most poker software and poker rooms.
I added poker-eval to SeeingStars to evaluate hands that go to the flop, turn, and river. I use the Monte Carlo method for the flop and turn, but once the hand goes to the river, it is quick to evaluate every possible combination.
Once I added poker-eval to SeeingStars, I had real-time odds fully functioning. Here’s a video I made of a heads-up SNG I played today. with real-time odds:
As you can see, I have a proof-of-concept working. That is just the beginning of the required work. There are remaining steps to make this useful more generally, including quality control, packaging, getting a logo, producing a website, working with smaller window sizes, supporting different PokerStars room themes, tables other than heads-up tables. Then I have to decide if I should charge money for this, as a stand-alone product, or whether it belongs in PokerZebra. Can I sell it in the Mac App Store, or do sandbox restrictions get in the way? How much will this add to my workload?
Would you pay money for a fully working SeeingStars? Say 10 dollars/10 euros? Without financial incentive I’m unlikely to spend too much time finishing this proof-of-concept.
My friend Valentina Pokerina is a rare combination: she plays poker well and teaches well – in five or six languages. She lives here in Barcelona and runs regular poker workshops. Mostly in Spanish, but also in English. Heck, ask her, and she’ll teach you poker in Catalan, German, or Portuguese. And French too, I think.
And if you are a poker bigwig, I bet she is willing to travel to run poker workshops.
I do, however, mostly beat her in our home games!
The continuing story of creating a tool called SeeingStars, offering real time odds for PokerStars. Read from the beginning here.
Calculating odds can be time-consuming. A short-cut preflop is to use pre-calculated tables. I found a table of preflop odds for all starting hands here. I copied this into SeeingStars, and created some code to look up in this table your starting hand by # of players. It works well, and outputs as follows:
cards = hand: Jc 5s, playerCount: 2, board: : 47.1% chance of winning
But who wants to read tiny text while playing? I made a HUD-style window, found a scoreboard style typeface, and displayed the number in a large size:
Not looking too bad, I think.
However this is only working preflop. After the flop, turn and/or river are revealed, it is more complicated. A look-up approach would require more than 500 billion recalculated values. Two possible approaches are
I think with the Monte Carlo method the odds calculated by sampled values will soon approach the actual odds. The trick is determining the amount of possible outcomes to sample that balances time constraints and accuracy.
The continuing story of creating a tool called SeeingStars, offering real time odds for PokerStars. Read part 1, part 2, and part 3.
In the last part I showed how I turned a screenshot into a 2-bit picture, which in text form looks like this:
00000000000000000000000000
00000000011111000000000000
00000001110011110000000000
00000111100000111000000000
00001111000000111100000000
00001110000000011110000000
00011110000000011111000000
00011110000000001111000000
00011100000000001111000000
00111100000000001111000000
00111100000000001111000000
00111100000000001111100000
00111100000000000111100000
00111100000000001111000000
00111110000000001111000000
00011110000000001111000000
00011110000000001111000000
00001110000000001110000000
00001111000000011110000000
00000111100000011100000000
00000011110000111000000000
00000000111111100000000000
00000000001111000000000000
00000000001111000000000000
00000000001111100000000000
00000000001111110000000000
00000000000111111000000000
00000000000011111111000000
00000000000000011100000000
00000000000000000000000000
I played on PokerStars for a couple of hours while my embryonic program grabbed screenshots, turned it into a string of 1’s and 0’s, then saved it. I now had a corpus of screen scrapings. Using a text editor, I manually augmented the text file with the character I could see. For example, a section of the text file looks like this:
It’s a manual way of training a computer to do optical character recognition.
Now I can compare future scraped characters to this corpus to find out what character it is.
The process of scraping doesn’t always return identical matches; sometimes the character ‘Q’ might have a couple more 1’s or a couple more 0’s. So I compare the scraped stream of 1’s and 0’s with my corpus using an “edit distance” algorithm. If the edit distance of the scraped text from an item in the corpus is no more than 4, then I count it as a match.
Let’s fire up PokerStars, play a hand, and see what SeeingStars does so far. Here’s the table:
Here’s what SeeingStars thinks is on the table:
cards = hand: Ks 4s, playerCount: 2, board: 3c 6c 2h 2d 5h
Perfect!
I think the hard part is done. Now to turn those cards and board into real time odds. That’s coming in part 5.
There’s a question in Spanish in the Poker Copilot discussion forum:
Estoy empezando a utilizar poker copilot y tengo problemas para poder subir mi historial. Solicito el historial en PokerStars, me da el formato csv. pero cuando entro en copilot y le doy a exportar el archivo, este me aparece pero no me lo deja seleccionar. Alguien me podria indicar como debo de hacerlo?
Can you help? Answer here.
The continuing story of creating a tool called SeeingStars, offering real time odds for PokerStars. Read part 1 and part 2.
Now the tricky stuff starts: having found the PokerStars table window, how do we determine the cards? This is not an easy programming problem. To make it easier, I’ll add some constraints. For the purpose of creating proof that real time odds is possible, here are the constraints I’m imposing:
These constraints can be relaxed later, once I get the basics working.
Using the window id obtained in part 2 of this series we can get the screenshot:
- (NSBitmapImageRep *)createSingleWindowShot:(CGWindowID)windowID {
CGImageRef cgImageRef = CGWindowListCreateImage(
CGRectNull,
kCGWindowListOptionIncludingWindow,
windowID,
kCGWindowImageBoundsIgnoreFraming);
NSImage *const nsImage = [[NSImage alloc] initWithCGImage:cgImageRef size:NSZeroSize];
return [[NSBitmapImageRep alloc] initWithData:[nsImage TIFFRepresentation]];
}
From the screen image we can determine the suit of a card by sampling the colour of one pixel. I used stackoverflow.com for information on finding the nearest colour to a pixel, and then map that colour to a suit. Some reading and experimentation shows the colour’s hue is a good method for differentiating between colours:
- (NSString *)getSuit:(NSBitmapImageRep *)bitmapImageRep x:(NSInteger)x y:(NSInteger)y {
NSDictionary *hues = @{
@0.57f:@"d",
@1.0f:@"d",
@0.29f:@"c",
@0.0f:@"s"
};
CGFloat pixelHue = ([bitmapImageRep colorAtX:x + 2 y:y + 4]).hueComponent;
double smallestDiff = fabs(0.38 - pixelHue); // 0.38 is the hue of the table felt
NSString *match = nil;
for (NSNumber *hue in hues.allKeys) {
const double diff = fabs((hue.floatValue) - pixelHue);
if (diff < smallestDiff) {
smallestDiff = diff;
match = [hues objectForKey:hue];
}
}
return match;
}
What about a card’s rank? This is harder. From experience I know that “thresholding” is helpful. Thresholding turns an image into a one-bit image: every pixel becomes either black or white, on or off, according to a threshold algorithm.
I started up Pixelmator and opened a PokerStars screenshot. Using Pixelmator’s threshold effect, I found that a setting of 80% worked well.
Most detail is gone, but the ranks of the cards are visible.
I turned this into Objective-C code:
- (BOOL)threshold:(NSColor *)color {
float value = 0.8f;
return color.redComponent > value &&
color.blueComponent > value &&
color.greenComponent > value;
}
- (void)logOneBitImage:(NSBitmapImageRep *)bitmapImageRep startX:(NSInteger)startX startY:(NSInteger)startY {
int width = 26;
int height = 30;
int ary[width * height];
for (int y = 0; y < height; y++) {
NSString *row = @"";
for (int x = 0; x < width; x++) {
NSColor *color = [bitmapImageRep colorAtX:x + startX y:y + startY];
ary[x * height + y] = [self threshold:color] ? 1 : 0;
row = [row stringByAppendingString:(ary[x * height + y]) == 0 ? @"0" : @"1"];
}
NSLog(@"row = %@", row);
}
NSLog(@" ");
}
This gives output in which you can clearly see the rank:
00000000000000000000000000
00000000011111000000000000
00000001110011110000000000
00000111100000111000000000
00001111000000111100000000
00001110000000011110000000
00011110000000011111000000
00011110000000001111000000
00011100000000001111000000
00111100000000001111000000
00111100000000001111000000
00111100000000001111100000
00111100000000000111100000
00111100000000001111000000
00111110000000001111000000
00011110000000001111000000
00011110000000001111000000
00001110000000001110000000
00001111000000011110000000
00000111100000011100000000
00000011110000111000000000
00000000111111100000000000
00000000001111000000000000
00000000001111000000000000
00000000001111100000000000
00000000001111110000000000
00000000000111111000000000
00000000000011111111000000
00000000000000011100000000
00000000000000000000000000
In the next article in this series, I’ll show how that set of 1’s and 0’s can be turned in a ‘Q’, in a reasonably fault-tolerant way.
This update is important for you if you play on the Merge Network. Poker Copilot crashes with Merge Network since their update overnight. This update fixes that.
What’s changed:
Known problem:
(And if you don’t own it yet, click here to buy Poker Copilot, securely and instantly.)
<!– This update needs to update your database. –>
The continuing story of creating a tool called SeeingStars, offering real time odds for PokerStars. Read part 1 here.
If you are an Objective-C programmer, I’m going to give you enough code to get the basics working in this series of articles. If you are not a programmer at all, you probably won’t enjoy much of what follows. Important to note is that I seldom code in Objective-C, so my code isn’t always idiomatic for Objective-C and may have obvious bugs. Please point this out in the comments so I can learn.
Here’s the basic strategy:
The hardest bit for me will be step 4, which is determining the player’s hole cards and the community cards. I already have a fair idea how to do the other steps.
First, a timer will achieve the “wait a second or so” of step 7 and 8:
- (IBAction)startRepeatingTimer:sender {
[NSTimer scheduledTimerWithTimeInterval:1.0
target:self selector:@selector(targetMethod:)
userInfo:[self userInfo] repeats:YES];
}
- (void)targetMethod:(NSTimer *)theTimer {
NSDictionary *windowDictionary = [[PokerRoomController alloc] findPokerStarsTableWindows];
if (windowDictionary != nil) {
NSString *title = [windowDictionary objectForKey:@"kCGWindowName"];
NSLog(@"title = %@", title);
}
}
Is one second a reasonable time, that balances snappy updates with low CPU load? I don’t know. I’ll know later in the story. But for now, it is an adequate starting point. I tend to go for “good enough” solutions until I have evidence that the solution is not good. I’ll address performance issues as they arise.
Now for step 1 – find the top-most PokerStars window. There’s a handy function to find all the windows open on your Mac, introduced in Mac OS X 10.5 (Leopard). It is called CGWindowListCopyWindowInfo. It is part of Quartz Window Services. It returns a key-value dictionary of information for each window. Conveniently it returns windows ordered from top to bottom; the front-most window is first in the array of dictionaries. “Normal” windows have a layer of 0.
- (NSDictionary *)findFrontNormalWindow {
NSArray *windowList = (__bridge NSArray *)
CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
for (NSDictionary *dictionary in windowList) {
NSNumber *windowLayer = [dictionary objectForKey:@"kCGWindowLayer"];
if ([windowLayer isEqualToNumber:[NSNumber numberWithInt:0]]) {
return dictionary;
}
}
return nil;
}
Now we need to make sure the front-most window is a PokerStars table window. For Poker Copilot, I’ve created a set of heuristics to determine this as follows. If all the following are true, then this is a PokerStars table window:
I’ve sneakily left out a couple of obscure edge cases from that list. Hey, it’s taken four years to build up this knowledge for Poker Copilot; I’m not going to give all my secrets away!
- (BOOL)isTableWindow:(NSString *)title bounds:(CGRect)bounds {
BOOL buyinWindow = [title rangeOfString:@"Tournament Buy-in"].location == 0;
BOOL chatWindow = [title rangeOfString:@"Chat"].location == 0;
BOOL cashierWindow = [title rangeOfString:@"Cashier"].location == 0;
BOOL lobbyWindow = [title rangeOfString:@"PokerStars Lobby"].location == 0;
BOOL hasHyphens = [[title componentsSeparatedByString:@"-"] count] - 1 > 1;
double aspectRatio = bounds.size.width / (bounds.size.height - 22);
double epsilon = fabs(1.45 - aspectRatio);
NSUInteger count = [[title componentsSeparatedByString:@"-"] count] - 1;
BOOL isReplayerWindow = count == 2 && epsilon < 0.01;
BOOL hasLoggedIn = ([title rangeOfString:@"Logged In as"].location != NSNotFound);
return !buyinWindow &&
!chatWindow &&
!lobbyWindow &&
!cashierWindow &&
!isReplayerWindow &&
hasHyphens &&
hasLoggedIn;
}
Put it all together, and the PokerRoomController is as follows:
@implementation PokerRoomController {
}
- (NSDictionary *)findPokerStarsTableWindows {
NSDictionary *windowDictionary = [self findFrontNormalWindow];
if (windowDictionary == nil) {
return nil;
}
NSString *windowOwnerName = [windowDictionary objectForKey:@"kCGWindowOwnerName"];
if ([windowOwnerName rangeOfString:@"PokerStars"].location != 0) {
return nil;
}
NSString *title = [windowDictionary objectForKey:@"kCGWindowName"];
CGRect bounds;
CGRectMakeWithDictionaryRepresentation(
(__bridge CFDictionaryRef)
([windowDictionary objectForKey:@"kCGWindowBounds"]), &bounds);
if (!([self isTableWindow:title bounds:bounds])) {
return nil;
}
return windowDictionary;
}
- (NSDictionary *)findFrontNormalWindow {
NSArray *windowList = (__bridge NSArray *)
CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
for (NSDictionary *dictionary in windowList) {
NSNumber *windowLayer = [dictionary objectForKey:@"kCGWindowLayer"];
if ([windowLayer isEqualToNumber:[NSNumber numberWithInt:0]]) {
return dictionary;
}
}
return nil;
}
- (BOOL)isTableWindow:(NSString *)title bounds:(CGRect)bounds {
BOOL buyinWindow = [title rangeOfString:@"Tournament Buy-in"].location == 0;
BOOL chatWindow = [title rangeOfString:@"Chat"].location == 0;
BOOL cashierWindow = [title rangeOfString:@"Cashier"].location == 0;
BOOL lobbyWindow = [title rangeOfString:@"PokerStars Lobby"].location == 0;
BOOL hasHyphens = [[title componentsSeparatedByString:@"-"] count] - 1 > 1;
double aspectRatio = bounds.size.width / (bounds.size.height - 22);
double epsilon = fabs(1.45 - aspectRatio);
NSUInteger count = [[title componentsSeparatedByString:@"-"] count] - 1;
BOOL isReplayerWindow = count == 2 && epsilon < 0.01;
BOOL hasLoggedIn = ([title rangeOfString:@"Logged In as"].location != NSNotFound);
return !buyinWindow &&
!chatWindow &&
!lobbyWindow &&
!cashierWindow &&
!isReplayerWindow &&
hasHyphens &&
hasLoggedIn;
}
@end
So now I’ve got code running once a second that finds the top-most PokerStars table window, if any. This is a key step in creating Poker Copilot’s HUD. However from hereon, the approach I’ll be showing differs from Poker Copilot’s HUD.
In the next part of this blog series, I’ll grab the screenshot and do some processing on it.