Tired of being beated week after week by my facebook friends, I decided to investigate the most hidden secrets of Bewejeled Blitz. After one or two thousands games, I finally realize that the only secret was to be the fastest. As I was not the fastest, there was only one thing I could do to beat my friends… I would program a script that beat them for me.
Of course, it was an incredible opportunity to learn a little more about python also.
How the script works
Basically, the script do what a human do while playing:
- Look at the board. The script captures the Bejeweled Blitz board, and analyzes where the jewels are. To do so, I used the PIL library to capture the screen. After that, I posterize the image reducing the number of colors, making it easy to distinguish the jewels between each other.
- Find a goal. The script simulates a jeweled move in memory, and looks up for a goal (three-in-a-row). If there is a goal, the script moves the mouse (PyWin32 API) and actually makes the move.
- Repeat everything again and again until game-time ends.
How to use it
The first thing that you need is to find out the (x, y) coordinates of the Bejeweled Blitz Board. To do this, start a Bejewelled Blitz and capture the screen with the –save option of the script. The next command makes a capture of the screen and saves it with the name “my_capture.bmp”.
c:>python Bwbot.pyw --save my_capture.bmp 0 0
Then open the image with a program like GIMP (http://www.gimp.org/) and look up the upper left coordinates of the Bejewelled Blitz Board.
To make the script play, just start a new game, and execute the script passing it the x and y coordinates you has just find out, for example:
c:>python Bwbot.pyw 527 455
## ## Bwbot.pyw ## ## A python script to play Bejeweled Blitz ## ## Author: Fernando García ## Licence: The script is under public domain. You can use, distribute or ## modify it at your own risk. ## import win32api, win32con import random import Image import ImageChops import math, operator from PIL import ImageGrab from PIL import ImageOps import time import argparse def saveImage(filename): "Capture screen and save it to a bmp file" _im = ImageGrab.grab() _im.save(filename, "BMP") class Bwbot: WIDTH = 320 # width of the board HEIGHT = 320 # height of the board COLS = 8 # number of cols of the board ROWS = 8 # number of rows of the board LAST_ROW_INDEX = 7 # index of the last row of the board LAST_COL_INDEX = 7 # index of the last row of the board SQUARE_WIDTH = WIDTH / COLS # width of a grid square MID_SQUARE_WIDTH = SQUARE_WIDTH / 2 # mid width of a grid square SQUARE_HEIGHT = HEIGHT / ROWS # heigth of a grid square MID_SQUARE_HEIGHT = SQUARE_HEIGHT / 2 # mid height of a grid square def __init__(self, ox, oy): "Initializes instance variables" # # play time in secconds # self.playtime = 55 # # drag delay in miliseconds # 20.-human 15.-optimal 10.-quick # self.drag_delay = 20 # # screen position of the board in pixels # self.ox = ox # x screen coordinate of the board self.oy = oy # y screen coordinate of the board # # rect of screen to capture de board # self.grid = (self.ox, self.oy, self.ox + Bwbot.WIDTH, self.oy + Bwbot.HEIGHT) # # Inner representation of the board # self.matrix = [[None for _col in xrange(Bwbot.COLS)] \ for _row in xrange(Bwbot.ROWS)] # # Sample size. The size in pixels of the square used to sample colors # self.sample_size = 8 def toMouseCoordinates(self, pos): "Translates grid coordinates to mouse coordinates" _x = self.ox + pos * Bwbot.SQUARE_WIDTH + Bwbot.MID_SQUARE_WIDTH _y = self.oy + pos * Bwbot.SQUARE_HEIGHT + Bwbot.MID_SQUARE_HEIGHT return (_x, _y) def click(self, pos): "Make a mouse click. Pos especified in grid coordinates" _x, _y = self.toMouseCoordinates(pos) win32api.SetCursorPos((_x, _y)) win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, _x, _y, 0, 0) win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, _x, _y, 0, 0) def dragJewel(self, pos1, pos2): "Drags a jewel from pos1 to pos2" self.click(pos1) self.click(pos2) self.wait(self.drag_delay) def wait(self, delay): "Do nothing during delay miliseconds" _delay = delay / 1000.0 _ref = time.time() while (time.time() - _ref) < _delay: pass def captureBoard(self): "Capture and preproceses Bejeweled Blitz board" _im = ImageGrab.grab() # capture and _im = ImageOps.posterize(_im.crop(self.grid), 3) # reduce colors return _im def buildMatrix(self): "Build the matrix of colors representing the board" _im = self.captureBoard() for _row in range(Bwbot.ROWS): for _col in range(Bwbot.COLS): _cx = (_col * Bwbot.SQUARE_WIDTH) + Bwbot.MID_SQUARE_WIDTH _cy = (_row * Bwbot.SQUARE_HEIGHT) + Bwbot.MID_SQUARE_HEIGHT _rect = (_cx, _cy, _cx + self.sample_size, _cy + self.sample_size) _sample_im = _im.crop(_rect) # store only de predominant color self.matrix[_col][_row] = sorted(_sample_im.getcolors()) def evaluatePos(self, pos): "Returns True if matrix contains a goal in the specified pos" _col, _row = pos _val = self.matrix[_col][_row] _col_m1 = _col - 1 # col minus 1 _col_m2 = _col - 2 # col minus 2 _col_p1 = _col + 1 # col plus 1 _col_p2 = _col + 2 # col plus 2 _row_m1 = _row - 1 # row minus 1 _row_m2 = _row - 2 # row minus 2 _row_p1 = _row + 1 # row plus 1 _row_p2 = _row + 2 # row plus 2 # horizontal if _col_m1 >= 0 and _val == self.matrix[_col_m1][_row]: if _col_m2 >= 0 and _val == self.matrix[_col_m2][_row]: return True elif _col_p1 < Bwbot.COLS and _val == self.matrix[_col_p1][_row]: return True elif _col_p2 < Bwbot.COLS and _val == self.matrix[_col_p1][_row] \ and _val == self.matrix[_col_p2][_row]: return True # vertical if _row_m1 >=0 and _val == self.matrix[_col][_row_m1]: if _row_m2 >= 0 and _val == self.matrix[_col][_row_m2]: return True elif _row_p1 < Bwbot.ROWS and _val == self.matrix[_col][_row_p1]: return True elif _row_p2 < Bwbot.ROWS and _val == self.matrix[_col][_row_p1] \ and _val == self.matrix[_col][_row_p2]: return True # no goal return False def swap(self, pos1, pos2): "Swap the values of pos1 and pos2 in the matrix" _aux = self.matrix[pos1][pos1] self.matrix[pos1][pos1] = self.matrix[pos2][pos2] self.matrix[pos2][pos2] = _aux def tryMove(self, pos1, pos2): "Simulates de move in the matrix and return True if there is a goal" self.swap(pos1, pos2) # swap positions _goal = self.evaluatePos(pos1) or self.evaluatePos(pos2) # evaluate and self.swap(pos1, pos2) # restore matrix return _goal def play(self): "plays the game during" _tnow = _tini = time.time() while(_tnow - _tini) < self.playtime: self.buildMatrix() for _row in xrange(Bwbot.ROWS): for _col in xrange(Bwbot.LAST_COL_INDEX): # evaluating pos _pos1 = (_col, _row) # try swap right _pos2 = (_col + 1, _row) _goal = self.tryMove(_pos1, _pos2) # if not goal, try swap down if _row < Bwbot.LAST_ROW_INDEX and not _goal: _pos2 = (_col, _row + 1) _goal = self.tryMove(_pos1, _pos2) # if goal, do the move if _goal: self.dragJewel(_pos1, _pos2) # time elapsed _tnow = time.time() if __name__ == '__main__': parser = argparse.ArgumentParser(description="Bejeweled Blitz Bot.") parser.add_argument('x', type=int, nargs=1, \ help='xpos of the Bejeweled Blitz board') parser.add_argument('y', type=int, nargs=1, \ help='ypos of the Bejeweled Blitz board') parser.add_argument('--save', nargs=1, required=False, \ help='capture de screen and saves it to a file') args = parser.parse_args() if args.save: saveImage(args.save) else: bwb = Bwbot(args.x, args.y) bwb.play()
Requisites and Limitations
- Phyton 2.7 (http://python.org/download/)
- Phyton Imaging Library 1.1.7 (http://effbot.org/downloads/#pil)
- PyWin32 (http://sourceforge.net/projects/pywin32/files/)
- The script only works under Windows and I have only try it with a true color definition screen.
Future improvements and ToDo List
My only intention was to beat my friends, something that I successfully did. My hi-score is now over 1.000.000 points, much more than any human player could even dream. But the script is far to be perfect. It lacks of a more efficient evaluation routine that selects the best move at each moment, choosing a 5-in-a-row move before the others for example. Another important defect is its inability to detect multipliers and other special jewels.
I am not planning to improve the script, but if you want to improve it, I will appreciate that you email me, just to know how the script evolves.