Poker Real Time Odds on Mac OS X: Part 2

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:

  1. Find the top-most PokerStars poker table window, if any
  2. If no such window exists go to step 7
  3. Grab a screenshot of the window
  4. Determine the player’s hole cards and the community cards using some form of screen scraping/OCR
  5. Calculate the % chance of winning, assuming the villains have random hands
  6. Display the % chance of winning
  7. Wait a second or so
  8. Go back to step 1

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:

  • Does the window owner name start with PokerStars? This “start with” clause allows this approach to work with PokerStarsES, PokerStarsFR, PokerStarsEU, etc?
  • Does the window name NOT include “Tournament Buy-in”?
  • Does the window name NOT include “Chat”?
  • Does the window name NOT include “Cashier”?
  • Does the window name NOT include “PokerStars Lobby”?
  • Does the window name include at least one hyphens?
  • If the window name contains exactly two hyphens, Is the window’s aspect ratio (width/height) NOT 1.45 +/- 0.01? (That excludes the hand replayer window).
  • Does the window name include “Logged In as”?

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!

In code form, most of the above looks like this:

- (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.