python implementation 2048 -- 1. Interface & Logic

Posted by Firestorm ZERO on Thu, 17 Feb 2022 15:14:00 +0100

preface

This time, let's try 2048. Those who don't know can play it by themselves. It won't be launched here.
We still use pygame, because we have had several pygame projects before. This time, I'm going to hurry up and mainly talk about the logic part.

Interface construction

The pygame call in this part will pass by. If you don't know the principle, take a look at this:
Previous part

The effect of 2048 is as follows:

4 * 4 grid, there is a number on each grid, and our score is below.

This time, instead of using the method of creating classes to store rect objects and numbers, we separate them into two.
num two-dimensional array: store data;
rectangle array: stores rect objects.
Then match the numbers to the squares one by one.

The whole interface is rendered with the background color, and then each possible number is given a color different from the background color, and our square is slightly narrower, so that a beautiful dividing line can be displayed.
Then, on the block with non-zero number, we display the number and realize the function.

"""2048 Games"""
import pygame
from fuc import *
from time import sleep
# Game status
"""When the variable is 0, end the loop and exit the game"""
game_stats = 1

# colour
"""Background color, dark gray"""
bg_color = (105, 105, 105)
"""Each number corresponds to rgb Color, encapsulated in dictionary, easy to find"""
color = {
    0  :(211, 211, 211),
    2  :(255, 255, 255),
    4  :(253, 245, 230),
    8  :(255, 228, 196),
    16 :(255, 222, 173),
    32 :(255, 193, 193),
    64 :(255, 106, 106),
    128:(255, 255,  0 ),
    256:(218, 165,  32),
    512:(184, 134,  11)
}

# Width and height of grid
lattice = 99

# score
score = 0

# Parameter encapsulation
"""Parameters are encapsulated into a list, which is convenient for us to modify variables"""
game = [game_stats, score]

# Create matrix
"""Question 1"""
num = [ [0]*4 for i in range(4)]
# rect object array, this is a one-dimensional array!, And this xy order is annoying
"""Question 2"""
rectangle = [pygame.Rect(x*lattice+x+1,y*lattice+y+1,lattice,lattice) for y in range(4) for x in range(4)]

# Create screen object
pygame.init()
screen = pygame.display.set_mode((400,430))
pygame.display.set_caption('2048')

# Start the game
"""Be a gentleman, or you'll play a ghost"""
new(num,game)
while game[0]:
	""Refresh""""
    update(screen,bg_color,color,num,rectangle,game)
    """event detection """
    check_events(num,game)
update(screen,bg_color,color,num,rectangle,game)
sleep(3)

Question:

  1. If we use [[0] * 4] * 4, you will generate a 4 * 4 matrix, but when you modify the elements, ha ha, explode in place.
    First, we use [0] 4 to generate a [0,0,0,0] list. Each content is an immutable type. It's really nothing to modify, but in the second layer, [list] * 4 actually points each row of a two-dimensional array to the same one-dimensional list. The list is a variable type. When you modify it, it is changed on the basis of the previous one, which will lead to the change of the position pointing to all four lists.


    Here I use a list parsing. The external for loop has only one counting function.
  2. Note here that our rect object is stored in a one-dimensional list. Don't call it in a two-dimensional list like me and crash.
    Rect function parameters: x and y coordinates (upper left corner), as well as the width and height of the rectangle
    The main reason here is that the logic is annoying. i want 0123 \n 4567, so i need to traverse y first and then x, so that our elements in row i and column j can be called with 4*i+j.

Here, I add the processing function to the event detection. After determining the event, we call the corresponding function.

At the end of the game, we refresh the screen. This is because when we judge the end of the game, we have no chance to refresh again, so the results will be imperfect. (in fact, you can also put update after check. Anyway, players are not as fast as computers.)

Function part

Next is the logic part.
The game will detect up, down, left and right, and then exit with a closed interface.
After detecting the event, we need to judge whether the movement is effective, because effective movement needs to generate a new box;
At the same time, after each new box is generated, the game may end, so we need a test after generation.

event detection

def check_events(num,game):
    for event in pygame.event.get():
    	"""Detection of exit events"""
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN:
        	"""Key event"""
        	pass

Then we use event key == pygame. K_ Up to determine the direction of the key, similar to K_DOWN,K_LEFT,K_RIGHT these.

Take up as an example.
In the upward event, we move and merge the numbers upward. Because each direction is different, I made an integration:

for j in range(4):
	s = [num[i][j] for i in range(4)]
	s_ret = my_sort(s,game)
	for i in range(4):
		num[i][j] = s_ret[i]

my_sort function implements the operation of sorting a list to 0 subscript. In the up operation, we need to sort it up, so we pass in the list derivation s and call the function to move and integrate a column.
Another problem is that after we successfully move, we need to generate a new box. The function here is called new(), which generates a 2 or 4 at position 0. Then the question is how to judge whether the move is successful.

ret = 0
for j in range(4):
	s = [num[i][j] for i in range(4)]
	s_ret = my_sort(s,game)
	if s_ret == s:
		ret += 1
	else:
		for i in range(4):
			num[i][j] = s_ret[i]
if ret != 4:
	new(num,game)

For each column, we save the generated list and the returned list. If they are the same, it means that the column has not been moved successfully. If they are different, it means that there are changes and the move is successful.

whole:
(I still can't integrate the four directions. I didn't think of a good way. I hope dl can give you some advice)

def check_events(num,game):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_UP:
                ret = 0
                for j in range(4):
                    s = [num[i][j] for i in range(4)]
                    s_ret = my_sort(s,game)
                    if s_ret == s:
                        ret += 1
                    else:
                        for i in range(4):
                            num[i][j] = s_ret[i]
                if ret != 4:
                    new(num,game)
            elif event.key == pygame.K_DOWN:
                ret = 0
                for j in range(4):
                    s = [num[i][j] for i in range(3,-1,-1)]
                    s_ret = my_sort(s,game)
                    if s_ret == s:
                        ret += 1
                    else:
                        for i in range(4):
                            num[3-i][j] = s_ret[i]
                if ret != 4:
                    new(num,game)
            elif event.key == pygame.K_LEFT:
                ret = 0
                for i in range(4):
                    s = num[i]
                    s_ret = my_sort(s,game)
                    if s_ret == s:
                        ret += 1
                    else:
                        num[i] = s_ret
                if ret != 4:
                    new(num,game)
            elif event.key == pygame.K_RIGHT:
                ret = 0
                for i in range(4):
                    s = num[i][::-1]
                    s_ret = my_sort(s,game)
                    if s_ret == s:
                        ret += 1
                    else:
                        num[i] = s_ret[::-1]
                if ret != 4:
                    new(num,game)

Next, you only need to implement check, sort and new.

generate

This is fairly simple

def new(num,game):
    """Generate a new box"""
    success = 1
    x = y = 0
    """First check whether it can be generated. Generally speaking, it is not used"""
    for i in range(4):
        for j in range(4):
            if not num[i][j]:
                success = 0
    if success:
        game[0] = 0
        return
    # What is generated by random number judgment
    while not success:
        x = randint(0,3)
        y = randint(0,3)
        if not num[x][y]:
            if (x+y)%2:
                num[x][y] = 2
            else:
                num[x][y] = 4
            break
    check(num,game)

Check function

i've been thinking about this part for a long time, but i can only give a more i complex one, that is, simulate all four directions, and then judge whether they all failed.

def check(num,game):
    stats = 0
    ret = 0
    for i in range(4):
        s = num[i]
        s_ret = my_sort(s,game)
        # One column failed to move successfully
        if s == s_ret:
            ret += 1
        # None of the four columns moved successfully
        if ret == 4:
            stats += 1
    ret = 0
    for i in range(4):
        s = num[i][::-1]
        s_ret = my_sort(s,game)
        if s == s_ret:
            ret += 1
        if ret ==4:
            stats += 1
    ret = 0
    for j in range(4):
        s = [num[i][j] for i in range(4)]
        s_ret = my_sort(s,game)
        if s == s_ret:
            ret += 1
        if ret == 4:
            stats += 1
    ret = 0
    for j in range(4):
        s = [num[3-i][j] for i in range(4)]
        s_ret = my_sort(s,game)
        if s == s_ret:
            ret += 1
        if ret == 4:
            stats += 1
    # All four directions are useless, which means it's dead
    if stats == 4:
        game[0] = 0

The whole process counting is basically the same as our event detection, but the data is not assigned back.

sort

Well, I'm still in this part. I've been stuck for several days (in fact, fishing during the holiday is too cruel).
I sorted out what we want to achieve for the four numbers:

  1. Mark down 0 to move. There can't be empty in the middle
  2. Two elements that are adjacent or have only 0 in the middle can be merged
  3. When merging, the subscript smaller * 2 and the subscript larger is 0
  4. The most important point of merging is 422, for example. We can only merge to 44 instead of an 8, that is, the points involved in the merger cannot be merged again

I've tried the fast and slow pointer to find two elements, and I've tried the while loop until there is no space in the middle (complex and does not meet the fourth point)

Finally, I got such an algorithm:
Take out all non-zero elements and merge them again.
Then we complement 0 according to the number of four elements (because it moves to the 0 subscript, we complement 0 later)

def my_sort(s,game):
    """The row that will need to be processed/Take out a column, a list of four elements
    We're going to s[0]As the top"""
    # Process first and then merge
    s = [i for i in s if i]
    for i in range(1,len(s)):
        if s[i-1] == s[i]:
            s[i-1] *= 2
            s[i] = 0
            # game[1] is our score, which can be added together when merging
            game[1] += s[i-1]
    s = [i for i in s if i]
    s += [0]*(4-len(s))
    return s

The first step is to remove the 0 element. We only need to judge the two adjacent elements.

After merging, the difficult surface appears 0. Let's remove it again, and then add it later.

Refresh

Make up this part and our game can run.

def update(screen,bg_color,color,num,rectangle,game):
    screen.fill(bg_color)
    font = pygame.font.SysFont(None,48)
    for i in range(4):
        for j in range(4):
        	# Identify numbers and rect objects
            number = num[i][j]
            the_rect = rectangle[i*4+j]
            # Draw background color
            col = color[number]
            pygame.draw.rect(screen,col,the_rect)
            if number:
                # Draw numbers
                msg_image = font.render(str(number),True,(0, 0, 0),col)
                msg_rect = msg_image.get_rect()
                msg_rect.center = the_rect.center
                screen.blit(msg_image,msg_rect)
    # Draw score
    string = "score:"
    string += str(game[1])
    msg_image = font.render(string,True,(255, 255, 255),bg_color)
    screen.blit(msg_image,(0,400))
    pygame.display.flip()

They all call ready-made pygame functions. If you are not clear, you can take a look at several small games in the past.
Greedy snake
TicTacToe

epilogue

In this way, our whole game is finished, but I think of several optimization points, which I may launch in the follow-up blog.

But I've already written some, so I'm not going to do it again.

Save score
If it is simple, you can see this:
txt read in and write the highest score
If you want to play with json files, look this (it is mainly aimed at multiple variables, one of which is not practical)

If you want more elements, for example, we add a start game button in the start interface, or add a pop-up window after death to display the score and maximum score:
Look at this

Topics: pygame