Game Development 18 min read

How to Build a Python Bot to Auto‑Play 4399’s “Pet Link‑Match Classic”

This tutorial walks through creating a Windows‑10 Python script that captures the 4399 mini‑game “Pet Link‑Match Classic 2”, identifies icons via image processing, builds a matrix, finds connectable pairs with a path‑finding algorithm, and simulates mouse clicks to automatically clear the board.

MaGe Linux Operations
MaGe Linux Operations
MaGe Linux Operations
How to Build a Python Bot to Auto‑Play 4399’s “Pet Link‑Match Classic”

1. Introduction

This article uses the 4399 mini‑game “Pet Link‑Match Classic 2” as a test case to demonstrate how to recognize small icons, simulate mouse clicks, and quickly complete matches. It is useful for anyone interested in learning game‑scripting techniques.

2. Development Outlook

Game‑assistant scripts are popular for automating repetitive actions. Two main approaches exist: reading and modifying in‑memory game data (similar to cheats) or simulating user behavior such as mouse clicks and keyboard input. This example follows the latter, focusing on user‑behavior simulation.

3. Development Process

3.1 Obtain window handle and bring to front

Using the win32gui module, FindWindow() retrieves the window handle (passing 0 for the parent and the full Chrome title as the window name). SetForegroundWindow() then brings the game window to the foreground.

3.2 Capture game screen and split icons

The script captures the game area with ImageGrab.grab() using the top‑left and bottom‑right coordinates of the game board. The captured image is divided into 8 rows × 12 columns of 39‑pixel squares, each stored in an image_list dictionary.

Each icon is then compared to previously seen icons. If an icon is new, it is added to image_type_list and assigned a new numeric ID; otherwise the existing ID is reused. The comparison uses a Hamming‑distance‑based hash: icons are resized to 20×20, converted to grayscale, turned into binary strings, and the number of differing bits is counted. A threshold of 10 treats the icons as identical.

The resulting numeric matrix ( self.im2num_arr) represents the board, with zeros for empty cells.

4. Core Icon‑Connection Algorithm (Path Finding)

The algorithm checks whether two cells with the same numeric ID can be connected according to the game rules (8 rows × 12 columns, with a surrounding border of zeros that allows paths to wrap around). The steps are:

Gather the set of horizontally and vertically reachable empty cells for each coordinate ( getDirectConnectList).

Compare the two sets; if any pair of reachable cells are also directly connectable, the original cells are considered connectable.

Key helper functions: isReachable(x1, y1, x2, y2) – verifies identical IDs and then checks connectivity. isDirectConnect(x1, y1, x2, y2) – determines if two cells are on the same row or column and have a clear line of zeros between them. isRowConnect and isColConnect – test adjacency or a continuous zero line.

When a connectable pair is found, clickAndSetZero simulates mouse clicks on the two icons (calculating screen coordinates from the board indices) and sets the corresponding matrix entries to zero.

5. Development Summary

Building a game‑assistant script like this helps cultivate programming interest and can be extended beyond games to automate repetitive desktop tasks such as sending messages, scrolling, or keyboard input.

6. Source Code

Note: the source code is provided for learning purposes only.

# -*- coding:utf-8 -*-
import win32gui
import time
from PIL import ImageGrab, Image
import numpy as np
import operator
from pymouse import PyMouse

class GameAssist:
    def __init__(self, wdname):
        """Initialize"""
        # Get window handle
        self.hwnd = win32gui.FindWindow(0, wdname)
        if not self.hwnd:
            print("Window not found, please verify the handle name: %s" % wdname)
            exit()
        # Bring window to front
        win32gui.SetForegroundWindow(self.hwnd)
        # Icon‑ID matrix
        self.im2num_arr = []
        # Screenshot coordinates (left, top, right, bottom)
        self.scree_left_and_right_point = (299, 251, 768, 564)
        # Icon size
        self.im_width = 39
        # Mouse controller
        self.mouse = PyMouse()

    def screenshot(self):
        """Capture the screen"""
        image = ImageGrab.grab(self.scree_left_and_right_point)
        image_list = {}
        offset = self.im_width
        for x in range(8):
            image_list[x] = {}
            for y in range(12):
                top = x * offset
                left = y * offset
                right = (y + 1) * offset
                bottom = (x + 1) * offset
                im = image.crop((left, top, right, bottom))
                image_list[x][y] = im
        return image_list

    def image2num(self, image_list):
        """Convert icon matrix to numeric matrix"""
        arr = np.zeros((10, 14), dtype=np.int32)
        image_type_list = []
        for i in range(len(image_list)):
            for j in range(len(image_list[0])):
                im = image_list[i][j]
                index = self.getIndex(im, image_type_list)
                if index < 0:
                    image_type_list.append(im)
                    arr[i + 1][j + 1] = len(image_type_list)
                else:
                    arr[i + 1][j + 1] = index + 1
        print("Icon count:", len(image_type_list))
        self.im2num_arr = arr
        return arr

    def getIndex(self, im, im_list):
        for i in range(len(im_list)):
            if self.isMatch(im, im_list[i]):
                return i
        return -1

    def isMatch(self, im1, im2):
        image1 = im1.resize((20, 20), Image.ANTIALIAS).convert("L")
        image2 = im2.resize((20, 20), Image.ANTIALIAS).convert("L")
        pixels1 = list(image1.getdata())
        pixels2 = list(image2.getdata())
        avg1 = sum(pixels1) / len(pixels1)
        avg2 = sum(pixels2) / len(pixels2)
        hash1 = "".join(map(lambda p: "1" if p > avg1 else "0", pixels1))
        hash2 = "".join(map(lambda p: "1" if p > avg2 else "0", pixels2))
        match = sum(map(operator.ne, hash1, hash2))
        return match < 10

    def isAllZero(self, arr):
        for i in range(1, 9):
            for j in range(1, 13):
                if arr[i][j] != 0:
                    return False
        return True

    def isReachable(self, x1, y1, x2, y2):
        if self.im2num_arr[x1][y1] != self.im2num_arr[x2][y2]:
            return False
        list1 = self.getDirectConnectList(x1, y1)
        list2 = self.getDirectConnectList(x2, y2)
        for x1_, y1_ in list1:
            for x2_, y2_ in list2:
                if self.isDirectConnect(x1_, y1_, x2_, y2_):
                    return True
        return False

    def getDirectConnectList(self, x, y):
        plist = []
        for px in range(0, 10):
            for py in range(0, 14):
                if self.im2num_arr[px][py] == 0 and self.isDirectConnect(x, y, px, py):
                    plist.append([px, py])
        return plist

    def isDirectConnect(self, x1, y1, x2, y2):
        if x1 == x2 and y1 == y2:
            return False
        if x1 != x2 and y1 != y2:
            return False
        if x1 == x2 and self.isRowConnect(x1, y1, y2):
            return True
        if y1 == y2 and self.isColConnect(y1, x1, x2):
            return True
        return False

    def isRowConnect(self, x, y1, y2):
        minY = min(y1, y2)
        maxY = max(y1, y2)
        if maxY - minY == 1:
            return True
        for y0 in range(minY + 1, maxY):
            if self.im2num_arr[x][y0] != 0:
                return False
        return True

    def isColConnect(self, y, x1, x2):
        minX = min(x1, x2)
        maxX = max(x1, x2)
        if maxX - minX == 1:
            return True
        for x0 in range(minX + 1, maxX):
            if self.im2num_arr[x0][y] != 0:
                return False
        return True

    def clickAndSetZero(self, x1, y1, x2, y2):
        p1_x = int(self.scree_left_and_right_point[0] + (y1 - 1) * self.im_width + (self.im_width / 2))
        p1_y = int(self.scree_left_and_right_point[1] + (x1 - 1) * self.im_width + (self.im_width / 2))
        p2_x = int(self.scree_left_and_right_point[0] + (y2 - 1) * self.im_width + (self.im_width / 2))
        p2_y = int(self.scree_left_and_right_point[1] + (x2 - 1) * self.im_width + (self.im_width / 2))
        time.sleep(0.2)
        self.mouse.click(p1_x, p1_y)
        time.sleep(0.2)
        self.mouse.click(p2_x, p2_y)
        self.im2num_arr[x1][y1] = 0
        self.im2num_arr[x2][y2] = 0
        print("Eliminated: (%d, %d) (%d, %d)" % (x1, y1, x2, y2))

    def start(self):
        image_list = self.screenshot()
        self.image2num(image_list)
        print(self.im2num_arr)
        while not self.isAllZero(self.im2num_arr):
            for x1 in range(1, 9):
                for y1 in range(1, 13):
                    if self.im2num_arr[x1][y1] == 0:
                        continue
                    for x2 in range(1, 9):
                        for y2 in range(1, 13):
                            if self.im2num_arr[x2][y2] == 0 or (x1 == x2 and y1 == y2):
                                continue
                            if self.isReachable(x1, y1, x2, y2):
                                self.clickAndSetZero(x1, y1, x2, y2)

if __name__ == "__main__":
    wdname = u'宠物连连看经典版2,宠物连连看经典版2小游戏,4399小游戏 www.4399.com - Google Chrome'
    demo = GameAssist(wdname)
    demo.start()
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Automationimage-processingwin32guiGame Botpymouse
MaGe Linux Operations
Written by

MaGe Linux Operations

Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.