Archive for October 2010


How I beat Bejeweled Blitz

October 21st, 2010 — 6:36pm

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!

1 comment » | python

Back to top