Automatic implementation of snake eating program in Python

Posted by vbracknell on Tue, 15 Feb 2022 12:47:35 +0100

Realization effect
Let's see the effect first

It's much faster than my manual game, and it's stand-alone. The automatic game doesn't provoke me to scold. Ha ha, the whole automatic game of multiplayer game will be scolded to death~

code
If the software is not installed, install the software first. If the module is not installed, install the pygame module.

1
pip install pygame
Import module

import pygame,sys,time,random
from pygame.locals import *

Define color variables

redColour = pygame.Color(255,0,0)
blackColour = pygame.Color(0,0,0)
whiteColour = pygame.Color(255,255,255)
greenColour = pygame.Color(0,255,0)
headColour = pygame.Color(0,119,255)

In all subsequent divisions, in order to prevent bias in pygame output, divisor (/ /) must be taken instead of simple division (/)

Program interface

Row 0, HEIGHT row, column 0 and WIDTH column are fences, so the actual size is 13 * 13

IGHT = 15
WIDTH = 15
FIELD_SIZE = HEIGHT * WIDTH
# The snake head is in the first element of the snake array
HEAD = 0

Numbers are used to represent different objects. Because each grid on the matrix will be processed into the path length to reach the food during movement, there needs to be a large enough interval (> height * width) between the three variables to distinguish each other. Lowercase is generally coordinates and uppercase represents constants.

FOOD = 0
UNDEFINED = (HEIGHT + 1) * (WIDTH + 1)
SNAKE = 2 * UNDEFINED

snake is a one-dimensional array. Directly adding the following values to the corresponding elements means moving in four directions.

LEFT = -1
RIGHT = 1
UP = -WIDTH # One dimensional array, so you need to add the whole width to represent up and down movement.
DOWN = WIDTH 

Error code

1
ERR = -2333
One dimensional array is used to represent two-dimensional things. board represents the rectangular field of snake movement. The initial snake head is at (1,1), and the initial snake length is 1.

board = [0] * FIELD_SIZE #[0,0,0,......]
snake = [0] * (FIELD_SIZE+1)
snake[HEAD] = 1*WIDTH+1
snake_size = 1

The temporary variable corresponding to the above variable is used when the snake moves tentatively.

tmpboard = [0] * FIELD_SIZE
tmpsnake = [0] * (FIELD_SIZE+1)
tmpsnake[HEAD] = 1*WIDTH+1
tmpsnake_size = 1

Food: the initial position of food is (4, 7), best_move: direction of motion.

food = 4 * WIDTH + 7
best_move = ERR

Movement direction array, game score (snake length)

mov = [LEFT, RIGHT, UP, DOWN]                                           
score = 1

Check whether a cell is covered by a snake. If it is not covered, it is free and returns true.

def is_cell_free(idx, psize, psnake):
    return not (idx in psnake[:psize]) 

Check whether a position idx can move in the direction of move

def is_move_possible(idx, move):
    flag = False
    if move == LEFT:
        #Because the actual range is 13 * 13, [1,13] * [1,13], you cannot run to the left when idx is 1. At this time, the remainder is 1, so > 1
        flag = True if idx%WIDTH > 1 else False
    elif move == RIGHT:
        #The < width-2 here is the same as the above
        flag = True if idx%WIDTH < (WIDTH-2) else False
    elif move == UP:
        #The upward judgment drawing here is very easy to understand, because there is another one outside the actual motion range of [1,13] * [1,13]
        #The big frame is a fence, which is the rows and columns mentioned earlier. The conditions for downward movement are similar
        flag = True if idx > (2*WIDTH-1) else False
    elif move == DOWN:
        flag = True if idx < (FIELD_SIZE-2*WIDTH) else False
    return flag

Reset board

board_ After BFS, the UNDEFINED value changes to the path length to reach the food.

If you need to restore, reset it.

def board_reset(psnake, psize, pboard):
    for i in range(FIELD_SIZE):
        if i == food:
            pboard[i] = FOOD
        elif is_cell_free(i, psize, psnake): # The location is empty
            pboard[i] = UNDEFINED
        else: # This position is the snake body
            pboard[i] = SNAKE

Breadth first search traverses the whole board and calculates the path length of each non SNAKE element in the board to the food.

def board_BFS(pfood, psnake, pboard):
    queue = []
    queue.append(pfood)
    inqueue = [0] * FIELD_SIZE
    found = False
    # At the end of the while cycle, in addition to the snake's body,
    # The number in each other square is the Manhattan distance from it to the food
    while len(queue)!=0: 
        idx = queue.pop(0)#Initially, idx is the coordinate of food 
        if inqueue[idx] == 1: continue
        inqueue[idx] = 1
        for i in range(4):#Left and right up and down
            if is_move_possible(idx, mov[i]):
                if idx + mov[i] == psnake[HEAD]:
                    found = True
                if pboard[idx+mov[i]] < SNAKE: # If the point is not the body of the snake
                    if pboard[idx+mov[i]] > pboard[idx]+1:#No matter when it is less than, otherwise the existing path data will be overwritten.
                        pboard[idx+mov[i]] = pboard[idx] + 1
                    if inqueue[idx+mov[i]] == 0:
                        queue.append(idx+mov[i])
    return found

Starting from the snake head, select the shortest path from the four field points around the snake head according to the element value in the board.

def choose_shortest_safe_move(psnake, pboard):
    best_move = ERR
    min = SNAKE
    for i in range(4):
        if is_move_possible(psnake[HEAD], mov[i]) and pboard[psnake[HEAD]+mov[i]]<min:
          #The minimum judgment here and the maximum judgment of the following functions are assigned first and compared with each other
            min = pboard[psnake[HEAD]+mov[i]]
            best_move = mov[i]
    return best_move

Check whether it can follow the snake tail, that is, there is a path between the snake head and the snake tail, in order to avoid the snake head falling into a dead end. Virtual operations are performed in tmpboard and tmpsnake.

def is_tail_inside():
    global tmpboard, tmpsnake, food, tmpsnake_size
    tmpboard[tmpsnake[tmpsnake_size-1]] = 0 # Virtually turn the snake tail into food (because it is virtual, it is carried out in tmpsnake and tmpboard)
    tmpboard[food] = SNAKE # The place where food is placed is regarded as the body of a snake
    result = board_BFS(tmpsnake[tmpsnake_size-1], tmpsnake, tmpboard) # Find the path length from each position to the snake tail
    for i in range(4): # Returns False if the head and tail of the snake are next to each other. That is, you cannot follow_tail, chasing the snake tail
        if is_move_possible(tmpsnake[HEAD], mov[i]) and tmpsnake[HEAD]+mov[i]==tmpsnake[tmpsnake_size-1] and tmpsnake_size>3:
            result = False
    return result

Let the snake head run one step towards the snake tail, regardless of the obstruction of the snake body, run towards the snake tail.

def follow_tail():
    global tmpboard, tmpsnake, food, tmpsnake_size
    tmpsnake_size = snake_size
    tmpsnake = snake[:]
    board_reset(tmpsnake, tmpsnake_size, tmpboard) # Reset virtual board
    tmpboard[tmpsnake[tmpsnake_size-1]] = FOOD # Make snake tail food
    tmpboard[food] = SNAKE # Turn the place of food into a snake
    board_BFS(tmpsnake[tmpsnake_size-1], tmpsnake, tmpboard) # Find the path length from each position to the snake tail
    tmpboard[tmpsnake[tmpsnake_size-1]] = SNAKE # Restore snake tail
    return choose_longest_safe_move(tmpsnake, tmpboard) # Return to the running direction (let the snake head move for 1 step)

When all schemes fail, just find a feasible direction (step 1)

def any_possible_move():
    global food , snake, snake_size, board
    best_move = ERR
    board_reset(snake, snake_size, board)
    board_BFS(food, snake, board)
    min = SNAKE
 
    for i in range(4):
        if is_move_possible(snake[HEAD], mov[i]) and board[snake[HEAD]+mov[i]]<min:
            min = board[snake[HEAD]+mov[i]]
            best_move = mov[i]
    return best_move

Convert array function

def shift_array(arr, size):
    for i in range(size, 0, -1):
        arr[i] = arr[i-1]
 
def new_food():#Generate new food by random function
    global food, snake_size
    cell_free = False
    while not cell_free:
        w = random.randint(1, WIDTH-2)
        h = random.randint(1, HEIGHT-2)
        food = WIDTH*h + w
        cell_free = is_cell_free(food, snake_size, snake)
    pygame.draw.rect(playSurface,redColour,Rect(18*(food//WIDTH), 18*(food%WIDTH),18,18))

The real snake is in this function, towards pbest_move take one step.

def make_move(pbest_move):
    global snake, board, snake_size, score
    shift_array(snake, snake_size)
    snake[HEAD] += pbest_move
    p = snake[HEAD]
    for body in snake:#Draw snake, body, head and tail
      pygame.draw.rect(playSurface,whiteColour,Rect(18*(body//WIDTH), 18*(body%WIDTH),18,18))
    pygame.draw.rect(playSurface,greenColour,Rect(18*(snake[snake_size-1]//WIDTH),18*(snake[snake_size-1]%WIDTH),18,18))
    pygame.draw.rect(playSurface,headColour,Rect(18*(p//WIDTH), 18*(p%WIDTH),18,18))
    #The following line is to fill out the first white block bug that will appear in the initial situation
    pygame.draw.rect(playSurface,(255,255,0),Rect(0,0,18,18))
    # Refresh pygame display layer
    pygame.display.flip() 
     
    # If the newly added snakehead is the location of the food
    # Add 1 to the snake length to generate new food and reset the board (because the original path length is no longer available)
    if snake[HEAD] == food:
        board[snake[HEAD]] = SNAKE # New snake head
        snake_size += 1
        score += 1
        if snake_size < FIELD_SIZE: new_food()
    else: # If the newly added snakehead is not the location of food
        board[snake[HEAD]] = SNAKE # New snake head
        board[snake[snake_size]] = UNDEFINED # The snake tail turns UNDEFINED and black
        pygame.draw.rect(playSurface,blackColour,Rect(18*(snake[snake_size]//WIDTH),18*(snake[snake_size]%WIDTH),18,18))
        # Refresh pygame display layer
        pygame.display.flip() 
        

Run it virtually once, and then check whether the operation is feasible at the calling place. Only when it is feasible can it run truly.

After the virtual operation eats the food, get the position of the virtual snake on the board.

def virtual_shortest_move():
    global snake, board, snake_size, tmpsnake, tmpboard, tmpsnake_size, food
    tmpsnake_size = snake_size
    tmpsnake = snake[:] # If tmpsnake=snake directly, they point to the same memory
    tmpboard = board[:] # The length of the path from each position to the food is already in the board, so there is no need to calculate it
    board_reset(tmpsnake, tmpsnake_size, tmpboard)
     
    food_eated = False
    while not food_eated:
        board_BFS(food, tmpsnake, tmpboard)    
        move = choose_shortest_safe_move(tmpsnake, tmpboard)
        shift_array(tmpsnake, tmpsnake_size)
        tmpsnake[HEAD] += move # Add a new position in front of the snake head
        # If the location of the newly added snake head is exactly the location of the food
        # Add 1 to the length, reset the board, and the position of the food becomes part of the snake
        if tmpsnake[HEAD] == food:
            tmpsnake_size += 1
            board_reset(tmpsnake, tmpsnake_size, tmpboard) # After the virtual operation, the snake is in the position of the board
            tmpboard[food] = SNAKE
            food_eated = True
        else: # If the snakehead is not the location of food, the newly added location is snakehead, and the last one becomes a space
            tmpboard[tmpsnake[HEAD]] = SNAKE
            tmpboard[tmpsnake[tmpsnake_size]] = UNDEFINED

Call this function if there is a path between the snake and the food.

def find_safe_way():
    global snake, board
    safe_move = ERR
    # Run virtually once, because it has been ensured that there is a path between the snake and the food, so the execution is effective
    # After running, get the position of the virtual snake in the board, that is, tmpboard
    virtual_shortest_move() # The function is unique
    if is_tail_inside(): # If there is a path between snake head and snake tail after virtual operation, select the shortest circuit operation (step 1)
        return choose_shortest_safe_move(snake, board)
    safe_move = follow_tail() # Otherwise follow virtually_ Step 1 of tail. If it can be done, return true
    return safe_move

Initialize pygame module

1
pygame.init()
Define a variable to control the speed of the game

1

fpsClock = pygame.time.Clock()
establish pygame Display layer

1
2

playSurface = pygame.display.set_mode((270,270))
pygame.display.set_caption('Greedy snake')

Draw pygame display layer

1

playSurface.fill(blackColour)

Initialize food

pygame.draw.rect(playSurface,redColour,Rect(18*(food//WIDTH), 18*(food%WIDTH),18,18))
 
while True:
    for event in pygame.event.get():#Loop listening for keyboard and exit events
        if event.type == QUIT:#If you click Close
            print(score)#Print scores at the end of the game
            pygame.quit()
            sys.exit()
        elif event.type == KEYDOWN:#If the esc key is pressed
            if event.key==K_ESCAPE:
                print(score)#Print scores at the end of the game
                pygame.quit()
                sys.exit()
    # Refresh pygame display layer
    pygame.display.flip()  
    #Draw the fence. 255255,0 is yellow and the border is 36 because the pygame rectangle starts with the edge and fills the border around
    pygame.draw.rect(playSurface,(255,255,0),Rect(0,0,270,270),36)
    # Reset distance
    board_reset(snake, snake_size, board)
    # If snakes can eat food, board_BFS returns true
    # Besides the snake body (= SNAKE), other element values in the board indicate the shortest path from the point to the food
    if board_BFS(food, snake, board):
        best_move  = find_safe_way() # find_ safe_ The only use of way
    else:
        best_move = follow_tail()
    if best_move == ERR:
        best_move = any_possible_move()
    # The last time I thought about it, I had to go in one direction and take one step
    if best_move != ERR: make_move(best_move)
    else:
        print(score)
        #Print scores at the end of the game
        break
    # Control game speed
    fpsClock.tick(20)
    #20 looks just right

The above is the detailed content of Python's automatic snake playing program. For more information about Python's automatic snake playing, please pay attention to the introduction to Python tutorial and learn other relevant articles!
Python learning roadmap:( Route)

Topics: Python Programmer crawler pygame