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:
- Interface: responsible for displaying all related work
- Game flow control: judging game wins and losses, game initialization, etc
- 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.