How I beat Bejeweled Blitz

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:

  1. 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.
  2. 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.
  3. 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.

Bejeweled Blitz Board coordinates

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

The script

##
## 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[0] * Bwbot.SQUARE_WIDTH + Bwbot.MID_SQUARE_WIDTH
        _y = self.oy + pos[1] * 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())[0][1]
                    
    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[0]][pos1[1]]
        self.matrix[pos1[0]][pos1[1]] = self.matrix[pos2[0]][pos2[1]]
        self.matrix[pos2[0]][pos2[1]] = _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[0])
    else:
        bwb = Bwbot(args.x[0], args.y[0])
        bwb.play()

Requisites and Limitations

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.

Enjoy!

One thought on “How I beat Bejeweled Blitz

  1. Thanks for posting the code. Many bots have been created for bejeweled blitz but for some reason people are reluctant to give out the source code. I’ve been learning programming for a while with VB and python and was planning to create a bot as project. I will use your code as a start and develop it so that it will recognize special gems, evaluate the best move, and also use elite technique. I’m also learning c++ in the mean time so I may be able to optimize it so that it’s quick enough.

    The goal I have in mind may seem a bit of a long shot but it is actually to achieve the most optimum play possible. I mean at any given time there must be a way to establish the best possible move and also evaluate and execute fast enough that the limitation of speed no longer resides with the software but how fast the jewels fall. That’s what I mean by best possible play. Which may translate to a high scores that approach the ceiling.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>