SoFunction
Updated on 2024-11-21

Python implementation of the script to automatically play connect-the-dots sharing

preamble

Recently my girlfriend is playing connect the dots and hasn't cleared it after a week of playing, it's really a dish.

I just couldn't look at it anymore and just wrote a script code in python for a handful of games a minute.

It's fast, but it's easy to get yelled at for playing online, heh heh heh.

Implementation steps

Module Import

import cv2
import numpy as np
import win32api
import win32gui
import win32con
from PIL import ImageGrab
import time
import random

Form Title Used to position the game form

WINDOW_TITLE = "Back-to-back."

Time interval randomization [MIN,MAX]

TIME_INTERVAL_MAX = 0.06
TIME_INTERVAL_MIN = 0.1

The x offset of the game area from the vertices

MARGIN_LEFT = 10

The y offset of the game area from the vertices

MARGIN_HEIGHT = 180

Number of horizontal squares

H_NUM = 19

Number of vertical squares

V_NUM = 11

Square width

POINT_WIDTH = 31

Square Height

POINT_HEIGHT = 35

Empty image number

EMPTY_ID = 0

The upper left and lower right coordinates when slicing and processing:

SUB_LT_X = 8
SUB_LT_Y = 8
SUB_RB_X = 27
SUB_RB_Y = 27

Maximum number of eliminations in a game

MAX_ROUND = 200

Get the position of the form coordinates

def getGameWindow():
    # FindWindow(lpClassName=None, lpWindowName=None) Window class name Window title name
    window = (None, WINDOW_TITLE)

    # No game form is positioned
    while not window:
        print('Failed to locate the game window , reposition the game window after 10 seconds...')
        (10)
        window = (None, WINDOW_TITLE)

    # Locate the game form
    # Topping the game window
    (window)
    pos = (window)
    print("Game windows at " + str(pos))
    return (pos[0], pos[1])

Get Screenshot

def getScreenImage():
    print('Shot screen...')
    # Get Screenshot Image type object
    scim = ()
    ('')
    # Read screenshots with opencv
    # Get ndarray
    return ("")

Distinguish images from screenshots and process them into maps.

def getAllSquare(screen_image, game_pos):
    print('Processing pictures...')
    # Positioning through the game form
    # Add an offset to get the play area
    game_x = game_pos[0] + MARGIN_LEFT
    game_y = game_pos[1] + MARGIN_HEIGHT

    # From the top left of the playing area
    # Cut the image into identical chunks of a specific size
    # Cutting standards are based on the horizontal and vertical coordinates of the parcel
    all_square = []
    for x in range(0, H_NUM):
        for y in range(0, V_NUM):
            # Slicing methods for ndarray: [vertical start position: vertical end position, horizontal start position: horizontal end position]
            square = screen_image[game_y + y * POINT_HEIGHT:game_y + (y + 1) * POINT_HEIGHT,
                     game_x + x * POINT_WIDTH:game_x + (x + 1) * POINT_WIDTH]
            all_square.append(square)

    # Because the edges of some images can be intrusive, uniformly shrink the image inward by one turn
    # Process all the squares, remove a circle from the edge and return #
    finalresult = []
    for square in all_square:
        s = square[SUB_LT_Y:SUB_RB_Y, SUB_LT_X:SUB_RB_X]
        (s)
    return finalresult

Determine whether the same figure exists in the list

Presence return to determine the id of the image

Otherwise return -1

def isImageExist(img, img_list):
    i = 0
    for existed_img in img_list:
        # Two images are compared The standard deviation of the two images is returned
        b = (existed_img, img)
        # If the standard deviation is zero, then there is no difference between the two images.
        if not (b):
            return i
        i = i + 1
    return -1

Get all cube types

def getAllSquareTypes(all_square):
    print("Init pictures types...")
    types = []
    The # number list is used to record the number of occurrences of each id.
    number = []
    # Currently the most frequently occurring square
    # Here we're defaulting to the most frequent square being the blank square #
    nowid = 0;
    for square in all_square:
        nid = isImageExist(square, types)
        # Insert list if this image does not exist
        if nid == -1:
            (square)
            (1);
        else:
            # If this image exists, give the counter + 1
            number[nid] = number[nid] + 1
            if (number[nid] > number[nowid]):
                nowid = nid
    # Update EMPTY_ID
    # i.e. determine the id of the blank block in the current diagram
    global EMPTY_ID
    EMPTY_ID = nowid
    print('EMPTY_ID = ' + str(EMPTY_ID))
    return types

Converting a 2D picture matrix to a 2D numeric matrix

Note that because the screenshot is sliced above with the columns first

So each row of the generated record 2D matrix actually holds the number of each column in the game screen

To put it another way, the record is actually a list of what's in the center of the game screen after symmetry

def getAllSquareRecord(all_square_list, types):
    print("Change map...")
    record = []
    line = []
    for square in all_square_list:
        num = 0
        for type in types:
            res = (square, type)
            if not (res):
                (num)
                break
            num += 1
        # of each column V_NUM
        # Then we consider this column processed when there are V_NUM squares in the current line list.
        if len(line) == V_NUM:
            print(line);
            (line)
            line = []
    return record

Determine whether the given two images can be eliminated

def canConnect(x1, y1, x2, y2, r):
    result = r[:]

    # If one of the two images is 0, return False.
    if result[x1][y1] == EMPTY_ID or result[x2][y2] == EMPTY_ID:
        return False
    if x1 == x2 and y1 == y2:
        return False
    if result[x1][y1] != result[x2][y2]:
        return False
    # Judging lateral connectivity
    if horizontalCheck(x1, y1, x2, y2, result):
        return True
    # Judging vertical connectivity
    if verticalCheck(x1, y1, x2, y2, result):
        return True
    # Judging a point of inflection to be connectable
    if turnOnceCheck(x1, y1, x2, y2, result):
        return True
    # Judging two points of inflection to be connectable
    if turnTwiceCheck(x1, y1, x2, y2, result):
        return True
    # Unconnectable returns False
    return False

Judgement of horizontal connectivity

def horizontalCheck(x1, y1, x2, y2, result):
    if x1 == x2 and y1 == y2:
        return False
    if x1 != x2:
        return False
    startY = min(y1, y2)
    endY = max(y1, y2)
    # Determine if two squares are adjacent
    if (endY - startY) == 1:
        return True
    # Determine whether the two square paths are both 0, one is not, it means that can not be connected, return false
    for i in range(startY + 1, endY):
        if result[x1][i] != EMPTY_ID:
            return False
    return True

Judging Vertical Connectivity

def verticalCheck(x1, y1, x2, y2, result):
    if x1 == x2 and y1 == y2:
        return False

    if y1 != y2:
        return False
    startX = min(x1, x2)
    endX = max(x1, x2)
    # Determine if two squares are adjacent
    if (endX - startX) == 1:
        return True
    # Determine whether two squares are connectable on the access road.
    for i in range(startX + 1, endX):
        if result[i][y1] != EMPTY_ID:
            return False
    return True

Judging an inflection point can be linked

def turnOnceCheck(x1, y1, x2, y2, result):
    if x1 == x2 or y1 == y2:
        return False

    cx = x1
    cy = y2
    dx = x2
    dy = y1
    # If the inflection point is empty and the whole path is passable from the first point to the inflection point and from the inflection point to the second point, then the whole path is passable.
    if result[cx][cy] == EMPTY_ID:
        if horizontalCheck(x1, y1, cx, cy, result) and verticalCheck(cx, cy, x2, y2, result):
            return True
    if result[dx][dy] == EMPTY_ID:
        if verticalCheck(x1, y1, dx, dy, result) and horizontalCheck(dx, dy, x2, y2, result):
            return True
    return False

Judging two inflection points can be linked

def turnTwiceCheck(x1, y1, x2, y2, result):
    if x1 == x2 and y1 == y2:
        return False

    # Iterate over the whole array to find the right inflection point #
    for i in range(0, len(result)):
        for j in range(0, len(result[1])):
            # Not being empty can't be an inflection point
            if result[i][j] != EMPTY_ID:
                continue
            # Not in the same row as the selected square cannot be used as an inflection point.
            if i != x1 and i != x2 and j != y1 and j != y2:
                continue
            # A square that's an intersection can't be an inflection point #
            if (i == x1 and j == y2) or (i == x2 and j == y1):
                continue
            if turnOnceCheck(x1, y1, i, j, result) and (
                    horizontalCheck(i, j, x2, y2, result) or verticalCheck(i, j, x2, y2, result)):
                return True
            if turnOnceCheck(i, j, x2, y2, result) and (
                    horizontalCheck(x1, y1, i, j, result) or verticalCheck(x1, y1, i, j, result)):
                return True
    return False

spontaneous elimination

def autoRelease(result, game_x, game_y):
    # Traversing the map
    for i in range(0, len(result)):
        for j in range(0, len(result[0])):
            # The current position is not empty
            if result[i][j] != EMPTY_ID:
                # Traversing the map again, looking for another image that meets the criteria #
                for m in range(0, len(result)):
                    for n in range(0, len(result[0])):
                        if result[m][n] != EMPTY_ID:
                            # If elimination can be implemented
                            if canConnect(i, j, m, n, result):
                                # Eliminate two positions set to empty
                                result[i][j] = EMPTY_ID
                                result[m][n] = EMPTY_ID
                                print('Remove :' + str(i + 1) + ',' + str(j + 1) + ' and ' + str(m + 1) + ',' + str(
                                    n + 1))

                                # Calculate where the current two positions of the image should exist in the game
                                x1 = game_x + j * POINT_WIDTH
                                y1 = game_y + i * POINT_HEIGHT
                                x2 = game_x + n * POINT_WIDTH
                                y2 = game_y + m * POINT_HEIGHT

                                # Simulate a mouse click where the first image is located
                                ((x1 + 15, y1 + 18))
                                win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x1 + 15, y1 + 18, 0, 0)
                                win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x1 + 15, y1 + 18, 0, 0)

                                # Wait for a random time to prevent detection
                                ((TIME_INTERVAL_MIN, TIME_INTERVAL_MAX))

                                # Simulate a mouse click where the second image is located
                                ((x2 + 15, y2 + 18))
                                win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x2 + 15, y2 + 18, 0, 0)
                                win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x2 + 15, y2 + 18, 0, 0)
                                ((TIME_INTERVAL_MIN, TIME_INTERVAL_MAX))
                                # Returns True when elimination is performed
                                return True
    return False

You have to upload a video for the effect, but the screenshot doesn't show the effect, so you can try it on your own.

All Codes

# -*- coding:utf-8 -*-
import cv2
import numpy as np
import win32api
import win32gui
import win32con
from PIL import ImageGrab
import time
import random

# Form title Used to position the game form
WINDOW_TITLE = "Back-to-back."
# Interval randomization [MIN,MAX]
TIME_INTERVAL_MAX = 0.06
TIME_INTERVAL_MIN = 0.1
# x offset of the game area from the vertices
MARGIN_LEFT = 10
# y offset of the game area from the vertices
MARGIN_HEIGHT = 180
# of horizontal squares
H_NUM = 19
# of squares vertically
V_NUM = 11
# Square width
POINT_WIDTH = 31
# Height of the cube
POINT_HEIGHT = 35
# Empty image number
EMPTY_ID = 0
# The upper-left and lower-right coordinates at the time of the slice processing:
SUB_LT_X = 8
SUB_LT_Y = 8
SUB_RB_X = 27
SUB_RB_Y = 27
# Maximum number of eliminations for the game
MAX_ROUND = 200


def getGameWindow():
    # FindWindow(lpClassName=None, lpWindowName=None) Window class name Window title name
    window = (None, WINDOW_TITLE)

    # No game form is positioned
    while not window:
        print('Failed to locate the game window , reposition the game window after 10 seconds...')
        (10)
        window = (None, WINDOW_TITLE)

    # Locate the game form
    # Topping the game window
    (window)
    pos = (window)
    print("Game windows at " + str(pos))
    return (pos[0], pos[1])

def getScreenImage():
    print('Shot screen...')
    # Get Screenshot Image type object
    scim = ()
    ('')
    # Read screenshots with opencv
    # Get ndarray
    return ("")

def getAllSquare(screen_image, game_pos):
    print('Processing pictures...')
    # Positioning through the game form
    # Add an offset to get the play area
    game_x = game_pos[0] + MARGIN_LEFT
    game_y = game_pos[1] + MARGIN_HEIGHT

    # From the top left of the playing area
    # Cut the image into identical chunks of a specific size
    # Cutting standards are based on the horizontal and vertical coordinates of the parcel
    all_square = []
    for x in range(0, H_NUM):
        for y in range(0, V_NUM):
            # Slicing methods for ndarray: [vertical start position: vertical end position, horizontal start position: horizontal end position]
            square = screen_image[game_y + y * POINT_HEIGHT:game_y + (y + 1) * POINT_HEIGHT,
                     game_x + x * POINT_WIDTH:game_x + (x + 1) * POINT_WIDTH]
            all_square.append(square)

    # Because the edges of some images can be intrusive, uniformly shrink the image inward by one turn
    # Process all the squares, remove a circle from the edge and return #
    finalresult = []
    for square in all_square:
        s = square[SUB_LT_Y:SUB_RB_Y, SUB_LT_X:SUB_RB_X]
        (s)
    return finalresult


# Determine if the same figure exists in the list
# Presence return to determine the id of where the image is located
# Otherwise return -1
def isImageExist(img, img_list):
    i = 0
    for existed_img in img_list:
        # Two images are compared The standard deviation of the two images is returned
        b = (existed_img, img)
        # If the standard deviation is zero, then there is no difference between the two images.
        if not (b):
            return i
        i = i + 1
    return -1

def getAllSquareTypes(all_square):
    print("Init pictures types...")
    types = []
    The # number list is used to record the number of occurrences of each id.
    number = []
    # Currently the most frequently occurring square
    # Here we're defaulting to the most frequent square being the blank square #
    nowid = 0;
    for square in all_square:
        nid = isImageExist(square, types)
        # Insert list if this image does not exist
        if nid == -1:
            (square)
            (1);
        else:
            # If this image exists, give the counter + 1
            number[nid] = number[nid] + 1
            if (number[nid] > number[nowid]):
                nowid = nid
    # Update EMPTY_ID
    # i.e. determine the id of the blank block in the current diagram
    global EMPTY_ID
    EMPTY_ID = nowid
    print('EMPTY_ID = ' + str(EMPTY_ID))
    return types


# Convert 2D picture matrices to 2D numeric matrices
# Note that because when slicing the screenshot above it is sliced first as a column
# So each row of the generated record 2D matrix actually holds the number of each column in the game screen
# To put it another way, the record is actually a list of what's in the center of the game screen #
def getAllSquareRecord(all_square_list, types):
    print("Change map...")
    record = []
    line = []
    for square in all_square_list:
        num = 0
        for type in types:
            res = (square, type)
            if not (res):
                (num)
                break
            num += 1
        # of each column V_NUM
        # Then we consider this column processed when there are V_NUM squares in the current line list.
        if len(line) == V_NUM:
            print(line);
            (line)
            line = []
    return record

def canConnect(x1, y1, x2, y2, r):
    result = r[:]

    # If one of the two images is 0, return False.
    if result[x1][y1] == EMPTY_ID or result[x2][y2] == EMPTY_ID:
        return False
    if x1 == x2 and y1 == y2:
        return False
    if result[x1][y1] != result[x2][y2]:
        return False
    # Judging lateral connectivity
    if horizontalCheck(x1, y1, x2, y2, result):
        return True
    # Judging vertical connectivity
    if verticalCheck(x1, y1, x2, y2, result):
        return True
    # Judging a point of inflection to be connectable
    if turnOnceCheck(x1, y1, x2, y2, result):
        return True
    # Judging two points of inflection to be connectable
    if turnTwiceCheck(x1, y1, x2, y2, result):
        return True
    # Unconnectable returns False
    return False

def horizontalCheck(x1, y1, x2, y2, result):
    if x1 == x2 and y1 == y2:
        return False
    if x1 != x2:
        return False
    startY = min(y1, y2)
    endY = max(y1, y2)
    # Determine if two squares are adjacent
    if (endY - startY) == 1:
        return True
    # Determine whether the two square paths are both 0, one is not, it means that can not be connected, return false
    for i in range(startY + 1, endY):
        if result[x1][i] != EMPTY_ID:
            return False
    return True

def verticalCheck(x1, y1, x2, y2, result):
    if x1 == x2 and y1 == y2:
        return False

    if y1 != y2:
        return False
    startX = min(x1, x2)
    endX = max(x1, x2)
    # Determine if two squares are adjacent
    if (endX - startX) == 1:
        return True
    # Determine whether two squares are connectable on the access road.
    for i in range(startX + 1, endX):
        if result[i][y1] != EMPTY_ID:
            return False
    return True


def turnOnceCheck(x1, y1, x2, y2, result):
    if x1 == x2 or y1 == y2:
        return False

    cx = x1
    cy = y2
    dx = x2
    dy = y1
    # If the inflection point is empty and the whole path is passable from the first point to the inflection point and from the inflection point to the second point, then the whole path is passable.
    if result[cx][cy] == EMPTY_ID:
        if horizontalCheck(x1, y1, cx, cy, result) and verticalCheck(cx, cy, x2, y2, result):
            return True
    if result[dx][dy] == EMPTY_ID:
        if verticalCheck(x1, y1, dx, dy, result) and horizontalCheck(dx, dy, x2, y2, result):
            return True
    return False


def turnTwiceCheck(x1, y1, x2, y2, result):
    if x1 == x2 and y1 == y2:
        return False

    # Iterate over the whole array to find the right inflection point #
    for i in range(0, len(result)):
        for j in range(0, len(result[1])):
            # Not being empty can't be an inflection point
            if result[i][j] != EMPTY_ID:
                continue
            # Not in the same row as the selected square cannot be used as an inflection point.
            if i != x1 and i != x2 and j != y1 and j != y2:
                continue
            # A square that's an intersection can't be an inflection point #
            if (i == x1 and j == y2) or (i == x2 and j == y1):
                continue
            if turnOnceCheck(x1, y1, i, j, result) and (
                    horizontalCheck(i, j, x2, y2, result) or verticalCheck(i, j, x2, y2, result)):
                return True
            if turnOnceCheck(i, j, x2, y2, result) and (
                    horizontalCheck(x1, y1, i, j, result) or verticalCheck(x1, y1, i, j, result)):
                return True
    return False


def autoRelease(result, game_x, game_y):
    # Traversing the map
    for i in range(0, len(result)):
        for j in range(0, len(result[0])):
            # The current position is not empty
            if result[i][j] != EMPTY_ID:
                # Traversing the map again, looking for another image that meets the criteria #
                for m in range(0, len(result)):
                    for n in range(0, len(result[0])):
                        if result[m][n] != EMPTY_ID:
                            # If elimination can be implemented
                            if canConnect(i, j, m, n, result):
                                # Eliminate two positions set to empty
                                result[i][j] = EMPTY_ID
                                result[m][n] = EMPTY_ID
                                print('Remove :' + str(i + 1) + ',' + str(j + 1) + ' and ' + str(m + 1) + ',' + str(
                                    n + 1))

                                # Calculate where the current two positions of the image should exist in the game
                                x1 = game_x + j * POINT_WIDTH
                                y1 = game_y + i * POINT_HEIGHT
                                x2 = game_x + n * POINT_WIDTH
                                y2 = game_y + m * POINT_HEIGHT

                                # Simulate a mouse click where the first image is located
                                ((x1 + 15, y1 + 18))
                                win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x1 + 15, y1 + 18, 0, 0)
                                win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x1 + 15, y1 + 18, 0, 0)

                                # Wait for a random time to prevent detection
                                ((TIME_INTERVAL_MIN, TIME_INTERVAL_MAX))

                                # Simulate a mouse click where the second image is located
                                ((x2 + 15, y2 + 18))
                                win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x2 + 15, y2 + 18, 0, 0)
                                win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x2 + 15, y2 + 18, 0, 0)
                                ((TIME_INTERVAL_MIN, TIME_INTERVAL_MAX))
                                # Returns True when elimination is performed
                                return True
    return False


def autoRemove(squares, game_pos):
    game_x = game_pos[0] + MARGIN_LEFT
    game_y = game_pos[1] + MARGIN_HEIGHT
    # Repeat an elimination until the maximum number of eliminations is reached
    while True:
        if not autoRelease(squares, game_x, game_y):
            # End when there are no more squares to eliminate, return the number of squares eliminated.
            return


if __name__ == '__main__':
    ()
    # i. Positioning the game form
    game_pos = getGameWindow()
    (1)
    # ii. Getting screenshots
    screen_image = getScreenImage()
    # iii. Slicing the screenshot to form a two-dimensional map
    all_square_list = getAllSquare(screen_image, game_pos)
    # iv. get all types of graphics and number them
    types = getAllSquareTypes(all_square_list)
    # v. Convert acquired image maps into digital matrices
    result = (getAllSquareRecord(all_square_list, types))
    # vi. Perform the elimination and output the number of eliminations.
    print('The total elimination amount is ' + str(autoRemove(result, game_pos)))

Guys, go try it.

Above is Python to realize the automatic play serial script to share the details, more information about Python serial please pay attention to my other related articles!