Developed in a Windows environment, this article is suitable for Python novices

HelloGitHub-Anthony

This is the first day of my participation in Gwen Challenge

HelloGitHub launches the open source project series, this installment introduces Python hands-on project — snake!

I wanted to recommend an open source project called Python-console-Snake, but since the last update to the project was 8 years ago, there were a lot of problems running it. I simply rewrote a snake game in Python.

Project address: github.com/AnthonySun2…

Let’s use Python to implement a simple and interesting command line snake game.

git clone https://github.com/AnthonySun256/easy_games
cd easy_games
python snake
Copy the code

This article contains design and explanation, and is divided into two parts: the first part is about the Python command line graphical library Curses and then snake related code.

I first met Curses

Python already has the Curses library built in, but we need to install a patch for Windows.

Installation completion package in Windows:

pip install windows-curses

Curses is a widely used graphical function library that allows you to draw simple user interfaces inside terminals.

Here we only carry on the simple introduction, only studies the gluttonous snake needs the function

If you’ve already touched curses, skip this section.

1.1 Simple Use

The curses library is built into Python. Using it is very simple. The following script displays the current key number:

Import the required libraries
import curses
import time

The STDSCR is a window object that represents the command line interface
stdscr = curses.initscr()
# Turn off the command line echo using the noecho method
curses.noecho()
# use nodelay(True) to make getch wait for non-blocking (execution continues even without input)
stdscr.nodelay(True)
while True:
    # Clear the contents of the STDSCR window (remove residual symbols)
    stdscr.erase()
    Get user input and put back the corresponding key number
    # Return -1 if no input is entered in non-blocking wait mode
    key = stdscr.getch()
    # Display text in the first line, third column of the STDSCR
    stdscr.addstr(1.3."Hello GitHub.")
    # Display text in the second row and third column of the STDSCR
    stdscr.addstr(2.3."Key: %d" % key)
    Refresh the window so that addstr takes effect
    stdscr.refresh()
    Wait 0.1s to give the user enough reaction time to view the text
    time.sleep(0.1)
Copy the code

You can also try to run it again by changing nodelay(True) to nodelay(False), which blocks at stdscr.getch() and only continues when you press the key.

1.2 Hourly patterns

You might think that the above example is too much, that you can get the same result with just a few prints, but now let’s do a little bit of a trick to get something that you can’t get with normal output.

1.2.1 Create a subwindow

No amount of words can be more realistic than a picture:

If we want to implement Game over! Window, you can use the newwin method:

import curses
import time

stdscr = curses.initscr()
curses.noecho()
stdscr.addstr(1.2."HelloGitHub")
Create a new window with a height of 5 and a width of 25 in the command line window
new_win = curses.newwin(5.25.4.6)
Use blocking wait mode
new_win.nodelay(False)
Add text to the new window at row 2 and column 3
new_win.addstr(2.3."www.HelloGitHub.com")
# Add a border to the new window, where the border symbol can be this is, here use the default character
new_win.border()
# refresh window
stdscr.refresh()
# wait for character input
new_win.getch()
Delete the new window object
del new_win
Erase everything (more thoroughly than erase)
stdscr.clear()
# readd text
stdscr.addstr(1.2."HelloGitHub")
# refresh window
stdscr.refresh()
# Wait 2 seconds
time.sleep(2)
End curses mode and return to normal command line mode
curses.endwin()
Copy the code

In addition to curses. Newwin creating a separate window, we can also create child Windows on any window using subwin or subpad methods. Subpad, new_win.subwin, new_win.subpad, etc., are used in the same way as new_win or STDSCR created in this section, except that new Windows use a separate cache. The child window and parent window share the cache.

If a window is to be deleted after use, it is best to create a separate window using the Newwin method to prevent the deletion of child Windows from causing problems with the cached contents of the parent window.

1.2.2 Dot color

While white and black can look boring over time, Curses offers built-in colors that allow us to customize the back and back backgrounds.

Before using color mode we need to initialize with curses.start_corlor() :

import curses
import time
stdscr = curses.initscr()
stdscr.nodelay(False)
curses.noecho()
Initialize color mode
curses.start_color()
# Add a color pair with green foreground color and black background color at position 1
curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK)
# Display text in line and column, using color # 1
stdscr.addstr(1.1."HelloGitHub!", curses.color_pair(1))
Block wait for the key and end the program
stdscr.getch()
curses.endwin()
Copy the code

It should be noted that the color of position 0 is the default black and white color and cannot be changed

1.2.3 Give some details

At the end of this section, we will talk about how to add a little text effect to the text:

import curses
import time
stdscr = curses.initscr()
stdscr.nodelay(False)
curses.noecho()
The text after # is underlined until attroff is called
stdscr.attron(curses.A_UNDERLINE)
stdscr.addstr(1.1."www.HelloGitHub.com")
stdscr.getch()
Copy the code

Two, greedy snake

With all that said, it’s finally time for our main course. In this section, I will teach you step by step how to make a simple yet detailed gluttonous snake from scratch.

2.1 design

For a project, it is better to spend some time on the necessary design than to write the first line of code as soon as possible. After all, structure determines function, and a project without a good structure has no future.

Snake divides the game into three parts:

  1. Interface: responsible for displaying all related work
  2. Game flow control: judging game wins and losses, game initialization, etc
  3. Snake and food: move itself, determine if it is dead, eaten, etc

Each piece is made into a separate object, and the game can be played with each other. Let’s take a look at how this should be done.

2.2 the snake speakers

There are three things a snake can do in snake: move, die (eat itself, hit a wall) and eat

Around these three functions, we can start by writing a crude snake with its class diagram as shown below:

The snake can check if it’s dead, if it’s eaten, and update its location.

Body and last_body are lists that store the current and previous snake coordinates, respectively. By default, the first element of the list is the snake head. Direction is the current direction of travel, and window_size is the size of the area where the snake can move.

The rest method is used to reset the state of the snake, which along with __init__ is responsible for the initialization of the snake:

class Snake(object) :
    def __init__(self) - >None:
        # Position is my custom class that only has x and y attributes and stores a coordinate point
        Initialize the size of the snake's range of movement
        self.window_size = Position(game_config.game_sizes["width"], game_config.game_sizes["height"])
        Initialize the direction of movement
        self.direction = game_config.D_Down
        # Reset body list
        self.body = []
        self.last_body = []
        # Generate a new body, default in the upper left corner, head down, three cells long
        for i in range(3):
            self.body.append(Position(2.3 - i))
	# rest resets the relevant attributes
    def reset(self) - >None:
        self.direction = game_config.D_Down
        self.body = []
        self.last_body = []
        for i in range(3):
            self.body.append(Position(2.3 - i))
Copy the code

Position is my custom class, only x and Y attributes, store a coordinate point

It’s normal to have a vague sense of what these attributes should be at first, but not be completely clear about what they are and how they should be initialized. All we need to do is continue to implement the required features, adding and refining the original ideas in practice.

After that, we continue to implement it from top to bottom, and we should implement update_snake_pos, which updates the position of the snake. This part is very simple:

def update_snake_pos(self) - >None:
    # This function, at the bottom of the article, gets how much the snake increases in the x and y directions
    dis_increment_factor = self.get_dis_inc_factor()
    Note that deep copy is used here.
    self.last_body = copy.deepcopy(self.body)
	# Move the snake's head first, then the body forward
    for index, item in enumerate(self.body):
        if index < 1:
            item.x += dis_increment_factor.x
            item.y += dis_increment_factor.y
        else:  # The rest should follow the previous part
            item.x = self.last_body[index - 1].x
            item.y = self.last_body[index - 1].y
Copy the code

Last_body can only record the last modified body

There is a detail here. If we are writing this function for the first time, in order for the snake head to move correctly as the player moves, we need to know how much the snake head element moves in the X and y directions.

The simplest way is to add a string of if-elif to determine the direction:

if self.direction == LEFT:
    head.x -= 1
elif self.direction == RIGHT:
    head.x += 1.Copy the code

The problem with this, however, is that if our requirements change (for example, I’m now saying that snakes can move two squares at a time, or that special items like X and Y move a different distance, etc.) it would be painful to directly modify such code.

So the better solution here is to use a dis_increment_factor to store how much the snake moves on x and y, and create a new get_dis_inc_factor to determine:

def get_dis_inc_factor(self) -> Position:
    # initialization
    dis_increment_factor = Position(0.0)

    Modify the speed in each direction
    if self.direction == game_config.D_Up:
        dis_increment_factor.y = -1
    elif self.direction == game_config.D_Down:
        dis_increment_factor.y = 1
    elif self.direction == game_config.D_Left:
        dis_increment_factor.x = -1
    elif self.direction == game_config.D_Right:
        dis_increment_factor.x = 1

    return dis_increment_factor
Copy the code

Sure, this might be a bit redundant, but trying to make a function do only one thing helps simplify our code and reduces the likelihood of writing long, smelly code that is difficult to debug.

Check_eat_food (‘ eat_food ‘, ‘eat_food’, ‘eat_food’);

def eat_food(self, food) - >None:
    self.body.append(self.last_body[-1])  # Grow an element

def check_eat_food(self, foods: list) - >int:  # Return to the food you ate
    # Walk through the food to see if the current food overlaps with the snake's head
    for index, food in enumerate(foods):
        if food == self.body[0] :# Call eat_food to handle the snake's body size
            self.eat_food(food)
            # Popupeat food
            foods.pop(index)
            # return the sequence number of the food eaten, or -1 if not
            return index
    return -1
Copy the code

In this case, Foods is a list of all food locations, and the check_EAT_food function is called every time the snake moves to check if it has eaten a particular food.

It can be found that I divided the two actions of “eat” and “eat down” into two functions to ensure that each function is “undivided” and convenient for later modification.

Now, our snake can run and eat. But as a snake that can only take care of itself, we also need to be able to judge our current state. For example, basically I need to know if I just bit myself, just by looking at whether the snake’s head has moved inside:

def check_eat_self(self) - >bool:
    return self.body[0] in self.body[1:]  # Determine if the head of the snake overlaps with the body
Copy the code

Or I wonder if I ran too fast and hit a wall:

def check_hit_wall(self) - >bool:
    Is it between the top and bottom border
    is_between_top_bottom = self.window_size.y - 1 > self.body[0].y > 0
    Is it between the left and right borders
    is_between_left_right = self.window_size.x - 1 > self.body[0].x > 0
    # return yes or no hit the wall
    return not (is_between_top_bottom and is_between_left_right)
Copy the code

These functions could not be simpler, but trust yourself that a few lines of code will enable a snake to perform complex actions at your command

Full code: github.com/AnthonySun2…

2.3 Cli? Drawing board!

In the last section we implemented the first character in the game: the snake. To display it we now need to transform our command line into an “artboard”.

We also thought before we started: What do we need to draw on our command line? Go straight to the class diagram:

Do you feel so dazzled that you don’t know where to start? There are many Graphic class methods, but most simply perform a specific function, and every time you update the game, you just call draw_game:

def draw_game(self, snake: Snake, foods, lives, scores, highest_score) - >None:
    # Clean up window characters
    self.window.erase()
    # Draw help information
    self.draw_help()
    # Update the current frame rate
    self.update_fps()
    Draw frame rate information
    self.draw_fps()
    Draw life and score information
    self.draw_lives_and_scores(lives, scores, highest_score)
    # draw border
    self.draw_border()
    # Draw food
    self.draw_foods(foods)
    # Draw the snake body
    self.draw_snake_body(snake)
    # Update interface
    self.window.refresh()
    # Update interface
    self.game_area.refresh()
    # Delay some time to control the frame rate
    time.sleep(self.delay_time)
Copy the code

Follow the principle of designing from the top down and implementing from the bottom up

You can see that draw_game has actually completed all of Graphic’s functionality.

Draw_foods and draw_snake_body are implemented in the same way, traversing the coordinate list and adding characters directly to the corresponding position:

def draw_snake_body(self, snake: Snake) - >None:
    for item in snake.body:
        self.game_area.addch(item.y, item.x,
                             game_config.game_themes["tiles"] ["snake_body"],
                             self.C_snake)

def draw_foods(self, foods) - >None:
    for item in foods:
        self.game_area.addch(item.y, item.x,
                             game_config.game_themes["tiles"] ["food"],
                             self.C_food)
Copy the code

It is also implemented separately to keep the code clean and easy to understand and modify later. Draw_help, draw_fps, and draw_lives_and_scores also print different text messages, without any new tricks.

Update_fps allows you to estimate the frame rate and adjust the wait time to stabilize the frame rate:

def esp_fps(self) - >bool:  Returns whether the FPS has been updated
    # fPS_update_interval is calculated once per fPS_update_interval frame
    if self.frame_count < self.fps_update_interval:
        self.frame_count += 1
        return False
    # Count the time spent
    time_span = time.time() - self.last_time
    Reset the start time
    self.last_time = time.time()
    # Estimate the frame rate
    self.true_fps = 1.0 / (time_span / self.frame_count)
    # reset count
    self.frame_count = 0
    return True

def update_fps(self) - >None:
    # If the frame rate is reestimated
    if self.esp_fps():
        # Calculation error
        err = self.true_fps - self.target_fps
        Adjust wait time to stabilize FPS
        self.delay_time += 0.00001 * err
Copy the code

Draw_message_window draws victory and defeat:

def draw_message_window(self, texts: list) - >None:  # receive a STR list
    text1 = "Press any key to continue."
    nrows = 6 + len(texts)  Leave space between travel and travel
    ncols = max(* [len(len_tex) for len_tex in texts], len(text1)) + 20
	Center the display window
    x = (self.window.getmaxyx()[1] - ncols) / 2
    y = (self.window.getmaxyx()[0] - nrows) / 2
    pos = Position(int(x), int(y))
    Create a separate window
    message_win = curses.newwin(nrows, ncols, pos.y, pos.x)
    # block wait, implement any key continue effect
    message_win.nodelay(False)
    # Draw text prompt
    # Center the bottom text
    pos.y = nrows - 2
    pos.x = self.get_middle(ncols, len(text1))
    message_win.addstr(pos.y, pos.x, text1, self.C_default)
	# Draw other information
    pos.y = 2
    for text in texts:
        pos.x = self.get_middle(ncols, len(text))
        message_win.addstr(pos.y, pos.x, text, self.C_default)
        pos.y += 1
	# draw border
    message_win.border()
	# Refresh content
    message_win.refresh()
    # Wait for any key
    message_win.getch()
    Restore non-blocking mode
    message_win.nodelay(True)
    # Empty window
    message_win.clear()
    # delete window
    del message_win
Copy the code

Thus, we realize the display of the game animation!

2.4 control!

So far, we’ve done game content drawing and game character implementation. In this section, we’ll look at snake’s last piece of work: controls.

As a rule of thumb, we should think before we start coding: if we were to write a Control class, what methods would it contain?

If you think about it, it’s not hard to think about it: there should be a loop where you keep playing as long as you don’t lose or win, and each round should update the graphics, snake movement, and so on. This is our start:

def start(self) - >None:
    # Reset the game
    self.reset()
	# Game run flag
    while self.game_flag:
		# Draw the game
        self.graphic.draw_game(self.snake, self.foods, self.lives, self.scores, self.highest_score)
		# Read key control
        if not self.update_control():
            continue
        # Control the game speed
        if time.time() - self.start_time < 1/game_config.snake_config["speed"] :continue
        self.start_time = time.time()
        # update snake
        self.update_snake()
Copy the code

As long as we write start, we can easily implement the rest of the structure, such as reading the key control is the most basic comparison of whether the numbers are the same:

def update_control(self) - >bool:
    key = self.graphic.game_area.getch()

    # 180 degree turns are not allowed
    if key == curses.KEY_UP andself.snake.direction ! = game_config.D_Down: self.snake.direction = game_config.D_Upelif key == curses.KEY_DOWN andself.snake.direction ! = game_config.D_Up: self.snake.direction = game_config.D_Downelif key == curses.KEY_LEFT andself.snake.direction ! = game_config.D_Right: self.snake.direction = game_config.D_Leftelif key == curses.KEY_RIGHT andself.snake.direction ! = game_config.D_Left: self.snake.direction = game_config.D_Right# Decide whether to quit
    elif key == game_config.keys['Q']:
        self.game_flag = False
        return False
    # Determine whether to reopen
    elif key == game_config.keys['R']:
        self.reset()
        return False
Copy the code

Update the snake status by checking whether it is dead, victorious, or eaten:

def update_snake(self) - >None:
    self.snake.update_snake_pos()
    index = self.snake.check_eat_food(self.foods)
    ifindex ! = -1:  # If you eat food
        # + 1 score
        self.scores += 1
        # Win if you fill the game area
        if len(self.snake.body) >= (self.snake.window_size.x - 2) * (self.snake.window_size.y - 2) :# Snake bodies have filled the game area
            self.win()
        else:
            # Place one more food item
            self.span_food()
	# If you die, see if the game is over
    if not self.snake.check_alive():
        self.game_over()
Copy the code

2.5 Direct Use

To enable the package to use Python Snake directly to start the game, let’s look at __main__.py:

import game

g = game.Game()
g.start()
g.quit()
Copy the code

When we try to run a package directly, Python starts from __main__.py, and for the code we’ve written, it only takes three lines to start the game!

Third, the end

Here is how to write a snake game is over! In fact, writing a small game is not difficult for beginners, the difficulty is how to organize the program structure. What I’ve implemented is just one way of doing it, and everyone will write different code based on their understanding of game structure. Either way, we should follow one goal: try to follow code specifications and develop a good style. Not only is it easier for others to read your code, it’s also easier for you to troubleshoot bugs and add new features.

Finally, thank you for reading. Here is HelloGitHub sharing interesting, entry-level open source projects on GitHub. Your every like, message, share is the biggest encouragement to us!


Follow the HelloGitHub official account to receive updates as soon as possible.

There are more open source projects and treasure projects waiting to be discovered.