分类 Python 下的文章

image_rmPjGmaY_1688792209178_raw.jpg

想象一下,你有一个机器人,你希望告诉它在不同的情况下应该做什么。状态机就像是给机器人的一组指示,根据它当前的状态或情况来执行。

这样想一想:机器人可以有不同的状态,就像你可以有不同的心情或处境一样。例如,机器人可以处于“快乐”状态、“伤心”状态或“生气”状态。

现在,假设你希望机器人根据它的状态做不同的事情。如果机器人处于“快乐”状态,你希望它跳舞。如果它处于“伤心”状态,你希望它播放一首悲伤的曲调。如果它处于“生气”状态,你希望它跺脚。

为了实现这个目标,你可以创建一个状态机。它就像是一个流程图,展示了机器人可能的不同状态以及在每个状态下应该执行的操作。

在我们的例子中,状态机可能是这样的:

  • 如果机器人处于“快乐”状态,它应该跳舞。
  • 如果机器人处于“伤心”状态,它应该播放一首悲伤的曲调。
  • 如果机器人处于“生气”状态,它应该跺脚。

所以,每当机器人改变它的状态时,它会检查状态机,并根据当前的状态知道应该采取什么行动。如果它处于“快乐”状态,它会跳舞。如果它处于“伤心”状态,它会播放一首悲伤的曲调。如果它处于“生气”状态,它会跺脚。

状态机不仅仅用在机器人上,还可以用在计算机程序、游戏甚至是交通信号灯中。它们帮助我们组织和控制在不同情况下发生的事情。

交通信号灯

当我们讨论状态机的例子时,一个经典的示例是交通信号灯。让我们使用Python来实现一个简单的交通信号灯状态机:

class TrafficLight:
    def __init__(self):
        self.current_state = 'red'
    
    def change_state(self):
        if self.current_state == 'red':
            print("红灯,停车")
            self.current_state = 'green'
        elif self.current_state == 'green':
            print("绿灯,行走")
            self.current_state = 'yellow'
        elif self.current_state == 'yellow':
            print("黄灯,请准备停车")
            self.current_state = 'red'
        else:
            print("无效状态")
            
# 创建交通信号灯状态机
traffic_light = TrafficLight()

# 模拟信号灯状态变化
traffic_light.change_state()  # 输出:红灯,停车
traffic_light.change_state()  # 输出:绿灯,行走
traffic_light.change_state()  # 输出:黄灯,请准备停车
traffic_light.change_state()  # 输出:红灯,停车

增加等待时间

为了更加真实模拟现实交通灯,需要在信号灯转换之间增加等待时间,使用time.sleep

import time

在状态转换之前等待特定时间:

time.sleep(2)  # 停留2秒

增加打印提示

使用循环打印

  • 等待时间,秒为单位
    def print_delay(self, seconds):

      for second in range(seconds):
          self.print_delay_second(1)
    
  • 一秒打印一个等待提示...
    def print_delay_second(self, second=1):

      start_time = time.time()
      while time.time() - start_time < second:
          pass    
      print("waiting...")
    

状态机涉及的一些概念:

  • 状态
  • 转换
  • 输入
  • 输出

python里面有很多现成的实现好的状态机框架

体验一个简单的实现fysom

安装

pip install fysom

一个简单的例子:
finite-state-machine.jpg

from fysom import *

fsm = Fysom({'initial': 'awake',
             'final': 'red',
             'events': [
                 {'name': 'wakeup', 'src': 'sleeping', 'dst': 'awake'},
                 {'name': 'sleep',  'src': 'awake',   'dst': 'sleeping'}]})

print(fsm.current)   # awake
fsm.sleep()
print(fsm.current)   # sleeping
fsm.wakeup()
print(fsm.current)   # awake

拓展,练习

使用fsom模拟交通灯?,参考如下:
from fysom import Fysom
import time

def on_green_light():
    print("绿灯")
    time.sleep(5)
    fsm.trigger('change')  # 绿灯持续5秒后触发状态转换到下一个状态

def on_yellow_light():
    print("黄灯")
    time.sleep(2)
    fsm.trigger('change')  # 黄灯持续2秒后触发状态转换到下一个状态

def on_red_light():
    print("红灯")
    time.sleep(5)
    fsm.trigger('change')  # 红灯持续5秒后触发状态转换到下一个状态

fsm = Fysom({'initial': 'green',
             'events': [{'name': 'change', 'src': 'green', 'dst': 'yellow'},
                        {'name': 'change', 'src': 'yellow', 'dst': 'red'},
                        {'name': 'change', 'src': 'red', 'dst': 'green'}],
             'callbacks': {'on_green': on_green_light,
                           'on_yellow': on_yellow_light,
                           'on_red': on_red_light}})

while True:
    fsm.onchange()  # 执行状态转换
    time.sleep(1)  # 在状态转换之间添加1秒的延时等待

mFUjKKRNfbh4ySCFLEFH--3--6a5t3.jpg

之前编写的snake game比较简单,每次游戏结束需要重新运行程序才能继续玩

这次我们尝试使用StateMachine,状态机设计模式,改写该程序,增加几个状态:程序开始界面,游戏中状态、游戏结束界面以及游戏介绍界面

根据前面提到的游戏的循环模式,每个游戏的状态都是可以使用该循环模式分别实现

主要就是设计一个简单的状态机:

class StateMachine():
    def __init__(self):
        self.current_state = None
        self.is_exit = False

    def change_state(self, state):
        self.current_state = state
        self.current_state.on_enter()

    def get_exit_status(self):
        return self.is_exit
    
    def update(self):
        if self.current_state:
            self.is_exit = self.current_state.handle_events()
            self.current_state.update()

    def render(self):
        if self.current_state:
            self.current_state.render()
  • 这个状态机比较简单,包含两个属性:

    self.current_state
    self.is_exit

  • 额外有几个方法:

    def change_state(self, state):
    def get_exit_status(self):
    def render(self):
    def update(self):

使用该状态机组织游戏,代码如下所示:

game_state_machine = StateMachine()
game_state_machine.change_state(MainMenuState())
    
while True:
    game_state_machine.update()
    game_state_machine.render()
    if game_state_machine.get_exit_status():
        break
创建状态机
初始状态切换到主菜单状态
进入循环: 更新状态机;渲染状态机,判断是否需要退出状态机
在更新状态机里面,根据输入进入不同状态,各个状态运行自己的更新,渲染函数

状态类设计:

class MainMenuState():
    def on_enter(self):
        print("进入主菜单界面")                

    def handle_events(self):
        print("处理输入"       

    def update(self):
        pass

    def render(self):
        print("渲染界面")

  • 完整代码如下:

    import pygame
    from snake import SnakeGame, Food, Snake
    import random

    class StateMachine():

     def __init__(self):
         self.current_state = None
         self.is_exit = False
    
     def change_state(self, state):
         self.current_state = state
         self.current_state.on_enter()
    
     def get_exit_status(self):
         return self.is_exit
     
     def update(self):
         if self.current_state:
             self.is_exit = self.current_state.handle_events()
             self.current_state.update()
    
     def render(self):
         if self.current_state:
             self.current_state.render()
    

    class MainMenuState():

     def on_enter(self):
         pygame.display.set_caption("Snake Game")
         self.screen = pygame.display.set_mode([SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH,SnakeGame.SCREEN_HEIGHT])
         main_image = pygame.image.load('snake_main.jpg')
         ## 缩放图像
         self.main_image = pygame.transform.scale(main_image, (SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH,SnakeGame.SCREEN_HEIGHT))
         self.font = pygame.font.Font(None, 60)
         self.font2 = pygame.font.Font("./AaWuHunTi-2.ttf", 60)
         self.game_title = "Smake Game Using Python"
         self.text = ""
         self.current_char = 0
         self.typing_speed = 100  # 打字速度,单位为毫秒
         self.typingsound = pygame.mixer.Sound('typing.wav')
         self.text_surface2 = self.font2.render("按空格键继续...", True, (200, 0,100))  ## 渲染文本
         self.text_rect2 = self.text_surface2.get_rect(center=((SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH) // 2, 3*SnakeGame.SCREEN_HEIGHT // 4))  ## 设置文本位置
         
        


    def handle_events(self):
        for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    return True
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_SPACE:
                        game_state_machine.change_state(GamePlayingState())
                    elif event.key == pygame.K_ESCAPE:
                        return True 
                    
        return False           

    def update(self):
        if self.current_char < len(self.game_title):
            self.text += self.game_title[self.current_char]
            self.current_char += 1
            self.typingsound.play()
            pygame.time.wait(self.typing_speed)

    def render(self):
        self.screen.fill((0, 0, 0))  ## 清空屏幕
        self.screen.blit(self.main_image, (0, 0))
        self.text_surface = self.font.render(self.text, True, (255, 0, 0))  ### 渲染文本
        self.text_rect = self.text_surface.get_rect(center=((SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH) // 2, SnakeGame.SCREEN_HEIGHT // 2))  ## 设置文本位置
        self.screen.blit(self.text_surface, self.text_rect)  ### 绘制文本到屏幕

        self.screen.blit(self.text_surface2, self.text_rect2)  ### 绘制文本到屏幕        
        pygame.display.flip()

class GamePlayingState():
    def on_enter(self):
        self.screen = pygame.display.set_mode([SnakeGame.SCORE_BOARD_WIDTH+SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT])
        
        score_board_image = pygame.image.load('score_board.png')
        ### 缩放图像
        cropped_image = score_board_image.subsurface(pygame.Rect(0, 0, 128, 128))
        self.score_board_image = pygame.transform.scale(cropped_image, (SnakeGame.SCORE_BOARD_WIDTH, SnakeGame.SCREEN_HEIGHT))
        self.my_game = SnakeGame()
        self.clock = pygame.time.Clock()


    def handle_events(self):
        for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    game_state_machine.change_state(MainMenuState()) 
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_SPACE:
                        game_state_machine.change_state(CommandInfoState())
                    elif event.key == pygame.K_ESCAPE:
                        game_state_machine.change_state(MainMenuState())
                    else:
                        self.my_game.handle_events(event) 

    def update(self):
        self.my_game.update()
        if self.my_game.get_is_gameover():
            game_state_machine.change_state(GameOverState())

    def render(self):
        self.screen.fill((0,0,0))
        self.screen.blit(self.score_board_image, (SnakeGame.SCREEN_WIDTH, 0)) 
        self.my_game.render()      
        pygame.display.flip()
        self.clock.tick(5)


class GameOverState():
    def on_enter(self):
        self.screen = pygame.display.set_mode([SnakeGame.SCORE_BOARD_WIDTH+SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT], pygame.NOFRAME)
        
        gameover_image = pygame.image.load('gameover.png')
        self.gameover_image = pygame.transform.scale(gameover_image, (SnakeGame.SCORE_BOARD_WIDTH+SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT))
        self.music = pygame.mixer.music.load('gameover.mp3')
        pygame.mixer.music.play(-1)

    def handle_events(self):
        for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.mixer.music.play()
                    game_state_machine.change_state(MainMenuState()) 
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_SPACE:
                        pygame.mixer.music.play()
                        game_state_machine.change_state(CommandInfoState())
                    elif event.key == pygame.K_ESCAPE:
                        pygame.mixer.music.play()
                        game_state_machine.change_state(MainMenuState())
                    else:
                        pass

    def update(self):
        pass

    def render(self):
        self.screen.fill((0,0,0))
        self.screen.blit(self.gameover_image, (0, 0))      
        pygame.display.flip()


class GameLevelState():
    def on_enter(self):
        self.screen = pygame.display.set_mode([SnakeGame.SCORE_BOARD_WIDTH+SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT])
        main_image = pygame.image.load('main_menu.png')
        ## 缩放图像
        self.main_image = pygame.transform.scale(main_image, (SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT))
        score_board_image = pygame.image.load('score_board.png')
        ### 缩放图像
        
        cropped_image = score_board_image.subsurface(pygame.Rect(0, 0, 128, 128))
        self.score_board_image = pygame.transform.scale(cropped_image, (SnakeGame.SCORE_BOARD_WIDTH, SnakeGame.SCREEN_HEIGHT))


    def handle_events(self):
        for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    game_state_machine.change_state(MainMenuState()) 
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_SPACE:
                        game_state_machine.change_state(GamePlayingState())
                    elif event.key == pygame.K_ESCAPE:
                        game_state_machine.change_state(MainMenuState()) 

    def update(self):
        pass

    def render(self):
        self.screen.fill((0,0,0))
        self.screen.blit(self.main_image, (0, 0))
        self.screen.blit(self.score_board_image, (SnakeGame.SCREEN_WIDTH, 0))        
        pygame.display.flip()
        

class CommandInfoState():
    def on_enter(self):
        self.x = random.randint(0, 240)
        self.y = random.randint(0, 240)
        self.screen = pygame.display.set_mode([SnakeGame.SCORE_BOARD_WIDTH+SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT])
        score_board_image = pygame.image.load('score_board.png')
        ### 缩放图像
        cropped_image = score_board_image.subsurface(pygame.Rect(128, 128, 128, 128))
        self.command_background_image = pygame.transform.scale(cropped_image, (SnakeGame.SCORE_BOARD_WIDTH + SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT))
        self.font = pygame.font.Font(None, 60)
        self.text0 = "snake game using python"
        self.text1 = "using arrow key to control snake"
        self.text2 = "Esc to return & Space to proceed"

        succ_snake_image = pygame.image.load('succ1.png')
        ### 缩放图像
        self.succ_snake_image = pygame.transform.scale(succ_snake_image, (240,240))

        self.snakehisssound = pygame.mixer.Sound('snake_hiss.wav')
     

    def handle_events(self):
        for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.snakehisssound.stop()
                    game_state_machine.change_state(MainMenuState()) 
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_SPACE:
                        self.snakehisssound.stop()
                        game_state_machine.change_state(MainMenuState())
                    elif event.key == pygame.K_ESCAPE:
                        self.snakehisssound.stop()
                        game_state_machine.change_state(MainMenuState()) 

    def update(self):
        self.x = random.randint(0, 600)
        self.y = random.randint(0, 240)
        self.snakehisssound.play()
        pygame.time.wait(300)


    def render(self):
        self.screen.fill((0,0,0))
        self.screen.blit(self.command_background_image, (0, 0))
        self.screen.blit(self.succ_snake_image, (self.x, self.y))

        self.text_surface = self.font.render(self.text0, True, (0, 0, 255))  ### 渲染文本
        self.text_rect = self.text_surface.get_rect(center=((SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH) // 2, SnakeGame.SCREEN_HEIGHT // 4))  ### 设置文本位置
        self.screen.blit(self.text_surface, self.text_rect)  ### 绘制文本到屏幕 

        self.text_surface = self.font.render(self.text1, True, (0, 0, 255))  ### 渲染文本
        self.text_rect = self.text_surface.get_rect(center=((SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH) // 2, SnakeGame.SCREEN_HEIGHT // 2))  ### 设置文本位置
        self.screen.blit(self.text_surface, self.text_rect)  ### 绘制文本到屏幕


        self.text_surface = self.font.render(self.text2, True, (0, 0, 255))  ### 渲染文本
        self.text_rect = self.text_surface.get_rect(center=((SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH) // 2, 3*SnakeGame.SCREEN_HEIGHT // 4))  ### 设置文本位置
        self.screen.blit(self.text_surface, self.text_rect)  ### 绘制文本到屏幕

                
        pygame.display.flip()

pygame.init()
icon_image = pygame.image.load("snake.png")

### 设置图标
pygame.display.set_icon(icon_image)

game_state_machine = StateMachine()
game_state_machine.change_state(MainMenuState())

while True:
    game_state_machine.update()
    game_state_machine.render()
    if game_state_machine.get_exit_status():
        break

pygame.quit()

使用循环模式重新改写snake game

主要修改,将游戏输入处理、状态更新,以及渲染重绘分离

import pygame
import random
class Snake:
    def __init__(self, screen, cols, rows, cell_size = 30, color=(0, 0, 0)):
        self.body = [(3, 0), (2, 0), (1, 0)]
        self.direction = "RIGHT"
        self.cell_size = cell_size
        self.screen = screen
        self.color = color
        self.cols = cols
        self.rows = rows

    def move(self, direction):
        head = self.body[0]
        if direction == "RIGHT":
            new_head = (head[0] + 1, head[1])
        elif direction == "LEFT":
            new_head = (head[0] - 1, head[1])
        elif direction == "UP":
            new_head = (head[0], head[1] - 1)
        elif direction == "DOWN":
            new_head = (head[0], head[1] + 1)
        self.body.insert(0, new_head)
        self.body.pop()
        self.direction = direction


    def draw(self):
        head = self.body[0]

        for cell in self.body:
            x = cell[0] * self.cell_size
            y = cell[1] * self.cell_size
            pygame.draw.rect(self.screen, self.color, [x, y, self.cell_size, self.cell_size])
        head_x = head[0] * self.cell_size
        head_y = head[1] * self.cell_size

        if self.direction == "LEFT" or self.direction == "RIGHT":
            eye1_pos = head_x + self.cell_size // 2, head_y + self.cell_size // 4
            eye2_pos = head_x + self.cell_size // 2, head_y + 3* self.cell_size // 4
        elif self.direction == "UP" or self.direction == "DOWN":
            eye1_pos = head_x + self.cell_size // 4, head_y + self.cell_size // 2
            eye2_pos = head_x + 3* self.cell_size // 4, head_y + self.cell_size // 2
        else:
            pass
        pygame.draw.circle(self.screen, (255, 165, 0), eye1_pos, 4)
        pygame.draw.circle(self.screen, (255, 165, 0), eye2_pos, 4)

        
    def check_collide(self, food):
        # Check if the Snake collides with the Food
        if self.body[0] == food.position:
            return 0
        # Check if the Snake collides with the wall
        if self.body[0][0] < 0 or self.body[0][0] >= self.cols or self.body[0][3] < 0 or self.body[0][4] >= self.rows:
            return 1
        # Check if the Snake collides with its own body
        for i in range(1, len(self.body)):
            if self.body[0] == self.body[i]:
                return 1
        return 2

    def grow(self):
        self.body.append(self.body[-1])


class Food:
    def __init__(self, screen, cols, rows, cell_size = 30, color=(255, 0, 0)):
        self.position = (random.randint(0, cols - 1), random.randint(0, rows - 1))
        self.color = color
        self.cell_size = cell_size
        self.cols = cols
        self.rows = rows
        self.screen = screen

    def generate_food(self, color=(255, 0, 0)):
        self.position = (random.randint(0, self.cols - 1), random.randint(0, self.rows - 1))
        self.color = color

    def draw(self):
        x = self.position[0] * self.cell_size
        y = self.position[1] * self.cell_size
        pygame.draw.rect(self.screen, self.color, [x, y, self.cell_size, self.cell_size])

   
class SnakeGame:
    # Define some colors
    BLACK = (0, 0, 0)
    WHITE = (255, 255, 255)
    GREEN = (0, 255, 0)
    RED = (255, 0, 0)

    # Set the dimensions of the screen
    SCREEN_WIDTH = 600
    SCREEN_HEIGHT = 600

    # Set the dimensions of the board and grid
    BOARD_ROWS = 20
    BOARD_COLS = 20
    GRID_SIZE = 30
    SCORE_BOARD_WIDTH = 200
    def __init__(self):

        # Initialize Pygame
        pygame.init()

        # Set the size of the screen
        self.screen = pygame.display.set_mode([SnakeGame.SCREEN_WIDTH + SnakeGame.SCORE_BOARD_WIDTH, SnakeGame.SCREEN_HEIGHT])
        self.snake = Snake(self.screen, SnakeGame.BOARD_COLS, SnakeGame.BOARD_COLS, SnakeGame.GRID_SIZE,SnakeGame.WHITE)
        self.food = Food(self.screen, SnakeGame.BOARD_COLS, SnakeGame.BOARD_COLS, SnakeGame.GRID_SIZE,SnakeGame.RED)
        self.score = 0

        # 创建字体对象
        self.score_font = pygame.font.SysFont('Arial', 30)
        self.score_text = 'Score'
        self.high_score = 0
        self.is_gameover = False
        self.is_gamepause = False
        #font.render('Hello, world!', True, (255, 255, 255))

        # Set the caption of the screen
        pygame.display.set_caption("Snake Game")

        # 加载图像
        score_board_image = pygame.image.load('score_board.png')
        # 缩放图像
        self.score_board_image = pygame.transform.scale(score_board_image, (SnakeGame.SCORE_BOARD_WIDTH, SnakeGame.SCREEN_HEIGHT))

        snake_icon_image = pygame.image.load('succ1.png')
        self.snake_icon_image = pygame.transform.scale(snake_icon_image, (SnakeGame.SCORE_BOARD_WIDTH, SnakeGame.SCORE_BOARD_WIDTH))
        self.eatsound = pygame.mixer.Sound('eat.wav')
        self.direction = "RIGHT"
        #self.music = pygame.mixer.music.load('sample.mp3')

        # 播放声音和音乐
        #self.eatsound.play()
        #pygame.mixer.music.play()

    def draw(self):
        for row in range(SnakeGame.BOARD_ROWS):
            for col in range(SnakeGame.BOARD_COLS):
                x = col * SnakeGame.GRID_SIZE
                y = row * SnakeGame.GRID_SIZE
                pygame.draw.rect(self.screen, SnakeGame.WHITE, [x, y, SnakeGame.GRID_SIZE, SnakeGame.GRID_SIZE], 1)
    def draw_score(self):
        # 显示图像
        self.screen.blit(self.score_board_image, (SnakeGame.SCREEN_WIDTH, 0))
        self.screen.blit(self.snake_icon_image, (SnakeGame.SCREEN_WIDTH, 0))
        
        high_score_text = self.score_font.render(f'Top: {self.high_score}', True, (0, 0, 0))
        self.screen.blit(high_score_text, (SnakeGame.SCREEN_WIDTH, SnakeGame.SCORE_BOARD_WIDTH+10))


        score_text = self.score_font.render(f'{self.score_text}: {self.score}', True, (0, 0, 0))
        self.screen.blit(score_text, (SnakeGame.SCREEN_WIDTH, SnakeGame.SCORE_BOARD_WIDTH + 50))

        

    # 加载历史最高分
    def load_high_score(self):
        with open("high_score.txt", "r") as f:
            lines = f.readlines()
            for line in lines:
                print(line)
            f.close()
        try:
            self.high_score = int(lines[0])
        except:
            print('invalid score')
        
            
    # 保存当前最高分
    def save_high_score(self):
        with open("high_score.txt", "w") as f:
            f.write(f"{self.high_score}")

    def get_is_gameover(self):
        return self.is_gameover

    def handle_events(self, event):
        if event.type == pygame.QUIT:
            self.is_gameover = True
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT and self.direction != "LEFT":
                self.direction = "RIGHT"
            elif event.key == pygame.K_LEFT and self.direction != "RIGHT":
                self.direction = "LEFT"
            elif event.key == pygame.K_UP and self.direction != "DOWN":
                self.direction = "UP"
            elif event.key == pygame.K_DOWN and self.direction != "UP":
                self.direction = "DOWN"
            elif event.key == pygame.K_ESCAPE:
                self.is_gameover = True
            elif event.key == pygame.K_p:
                if self.is_gamepause:
                    self.is_gamepause = False
                else:
                    self.is_gamepause = True
    def update(self):
        # Move the Snake
        self.snake.move(self.direction)
        ret = self.snake.check_collide(self.food)
        #print(f'当前分数: {self.score}')
        if ret == 0: # 碰到事物
            self.eatsound.play()
            while True:
                self.food.generate_food()
                if self.food.position not in self.snake.body:
                    break
            self.snake.grow()
            self.score +=10
        elif ret == 1: # hit wall or itself
            self.is_gameover = True
        else: # 继续移动
            pass
    def render(self):
        # Draw the board, Snake, and Food
        self.draw()
        self.snake.draw()
        self.food.draw()
        self.draw_score()
    
    def play(self):
        # Set the clock for the game
        clock = pygame.time.Clock()
        done = False
        direction = "RIGHT"
        # Start the game loop
        self.load_high_score()
        pause = False
        while not done:
            # Handle events
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    done = True
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_RIGHT and direction != "LEFT":
                        direction = "RIGHT"
                    elif event.key == pygame.K_LEFT and direction != "RIGHT":
                        direction = "LEFT"
                    elif event.key == pygame.K_UP and direction != "DOWN":
                        direction = "UP"
                    elif event.key == pygame.K_DOWN and direction != "UP":
                        direction = "DOWN"
                    elif event.key == pygame.K_ESCAPE:
                        done = True
                    elif event.key == pygame.K_p:
                        if pause:
                            pause = False
                        else:
                            pause = True
            if pause:
                continue

            # Move the Snake
            self.snake.move(direction)
            ret = self.snake.check_collide(self.food)
            #print(f'当前分数: {self.score}')
            if ret == 0: # collide with food
                self.eatsound.play()
                while True:
                    self.food.generate_food()
                    if self.food.position not in self.snake.body:
                        break
                self.snake.grow()
                self.score +=10
            elif ret == 1: # hit wall or itself
                done = True
            else: # nothing happened
                pass

            # Clear the screen
            self.screen.fill(SnakeGame.BLACK)

            # Draw the board, Snake, and Food
            if not done:
                self.draw()
                self.snake.draw()
                self.food.draw()
                self.draw_score()

            # Update the screen
            pygame.display.flip()

            # Set the frame rate of the game
            clock.tick(5)

        pygame.quit()
        if int(self.score) > self.high_score:
            self.high_score = int(self.score)
            self.save_high_score()
if __name__ == "__main__":
    my_game = SnakeGame()
    my_game.play()









StateMachine.zip

20230616223234-header.png

现在是时候来看我们的第一个设计模式了:游戏循环模式(Game Loop Pattern)!这个模式可以为我们提供许多很好的思路,以非常有效的方式重构我们的游戏

游戏循环模式

游戏循环模式有几个不同的版本。这里我介绍一个简单的情况,使用单线程。以下5个函数组成了这个模式:
20230616235132-08_gameloop.png

  • init()函数在启动时被调用,用于初始化游戏及其数据。在接下来的内容中,我将这些数据称为游戏状态(game state)
  • processInput()函数在每次游戏迭代中被调用,用于处理控制输入(键盘、鼠标、手柄)。
    update()函数用于改变游戏状态。processInput()的结果被用于更新游戏状态,同时也可以包含自动处理的过程。
  • render()函数负责处理显示,它将游戏状态转化为可视内容。
  • run()函数包含了游戏循环。在许多情况下,这个循环看起来像下面这样:

    def run():

     init() 
     while True: 
         processInput() 
         update() 
         render()
    

游戏循环模式,就像其他模式一样,是一个为解决问题提供思路的方案。并没有唯一的使用方式:它取决于具体情况。

遵循模式的使用强迫你考虑可能没有想到的问题。例如,在创建“猜数字”游戏时,将用户输入、数据更新和渲染分开可能不是首先想到的。然而,根据经验丰富的开发者的说法,这种分离是至关重要的。所以,作为初学者,我们现在遵循这个建议,以后会理解它为什么重要。并且请相信我:当你理解了这些想法背后的所有精妙之处时,你会感到惊叹不已!

猜数字游戏: init()函数

从这个循环模式的init函数入手:

def init():    
    return None, random.randint(1,10)

这个函数返回一个初始游戏状态。我将游戏数据称为“游戏状态”,因为游戏可以被看作是有限状态机。对于这个游戏,状态包括:

  • 游戏状态:表示游戏的整体状态的字符串:

    • "win":玩家获胜并结束游戏;
    • "end":玩家离开游戏;
    • "lower":玩家仍在游戏中,并提供了一个比魔术数字小的数字;
    • "higher":同样,但是为一个比魔术数字大的数字。
  • 魔术数字:玩家需要猜测的数字。

将所有的游戏数据捆绑在一起是一个重要的任务;我们将在接下来的文章中详细讨论这个问题。

猜数字游戏:processInput()函数

def processInput():
    while True:
        word = input("What is the magic number? ")
        if word == "quit":
            return None

        try:
            playerNumber = int(word)
            break
        except ValueError:
            print("Please type a number without decimals!")
            continue

    return playerNumber

这个函数要求玩家输入一个数字。它处理与用户输入相关的所有问题,比如检查输入的数字是否正确。它返回输入的数字,如果玩家想要停止游戏,则返回None。

对于使用这个函数的用户(比如run()函数),它就像一个神奇的盒子,返回玩家的指令。收集这些指令的方式并不重要。可以是通过键盘、鼠标、手柄、网络甚至是通过人工智能来获取。

猜数字游戏: update()函数

update()函数使用玩家的指令来更新游戏状态:

def update(gameStatus,magicNumber,playerNumber):
    if playerNumber is None:
        gameStatus = "end"
    elif playerNumber == magicNumber:
        gameStatus = "win"
    elif magicNumber < playerNumber:
        gameStatus = "lower"
    elif magicNumber > playerNumber:
        gameStatus = "higher"

return gameStatus, magicNumber 

在我们的情况下,玩家的指令是playerNumber,而游戏状态和魔术数字形成了游戏状态。根据playerNumber的值,函数会更新游戏状态。

需要注意的是,我们没有将gameStatus作为输入,也从未更改magicNumber的值。所以,我们可以考虑从参数中移除gameStatus,以及从返回值中移除magicNumber。除非这是游戏的最终版本,并且必须减少计算复杂性,否则这并不是一个好主意。也许在游戏的未来改进中,我们需要根据gameStatus更新游戏,或者改变magicNumber的值。从设计的角度来看,当前的输入和输出定义是稳健的,没有改变的理由。

猜数字游戏: render()函数

render()函数用于显示当前的游戏状态。它应该无论发生什么都能正常工作,始终清晰地展示游戏的情况:

def render(gameStatus,magicNumber):
    if gameStatus == "win":
        print("This is correct! You win!")
    elif gameStatus == "end":
        print("Bye!")
    elif gameStatus == "lower":
        print("The magic number is lower")
    elif gameStatus == "higher":
        print("The magic number is higher")
    else:
        raise RuntimeError("Unexpected game status {}".format(gameStatus))

这个函数的输入是游戏状态,没有输出。过程很简单:根据gameStatus的值显示相应的消息。

需要注意的是,我们还处理了gameStatus具有意外值的情况。这是一个好习惯,它在你更新游戏并忘记更新某些部分时非常有帮助。

猜数字游戏:runGame()函数

def runGame():
    gameStatus, magicNumber = init()
    while gameStatus != "win" and gameStatus != "end":
        playerNumber = processInput()
        gameStatus, magicNumber = update(gameStatus,magicNumber,playerNumber)
        render(gameStatus,magicNumber)

你可以看到流程如下:

  • init()函数返回一个初始的游戏状态;
  • processInput()函数从玩家那里收集指令;
  • update()函数使用指令来更新游戏状态;
  • render()函数显示游戏状态。

猜数字游戏:最终代码

# Import the random package
import random

def init():
    """
    Initialize game

    Outputs:
      * gameStatus
      * magicNumber
    """
    # Generate a random Magic number
    return None, random.randint(1,10)


def processInput():
    """
    Handle player's input

    Output:
      * playerNumber: the number entered by the player, or None if the player wants to stop the game
    """

    while True:
        # Player input
        word = input("What is the magic number? ")
        # Quit if the player types "quit"
        if word == "quit":
            return None

        # Int casting with exception handling
        try:
            playerNumber = int(word)
            break
        except ValueError:
            print("Please type a number without decimals!")
            continue

    return playerNumber

def update(gameStatus,magicNumber,playerNumber):
    """
    Update game state

    Inputs:
      * gameStatus: the status of the game
      * magicNumber: the magic number to find
      * playerNumber: the number entered by the player
    Output:
      * gameStatus: the status of the game
      * magicNumber: the magic number to find
    """
    if playerNumber is None:
        gameStatus = "end"
    elif playerNumber == magicNumber:
        gameStatus = "win"
    elif magicNumber < playerNumber:
        gameStatus = "lower"
    elif magicNumber > playerNumber:
        gameStatus = "higher"

    return gameStatus, magicNumber

def render(gameStatus,magicNumber):
    """
    Render game state

    Input:
      * gameStatus: the status of the game, "win", "end", "lower" or "higher"
    """
    # Cases
    if gameStatus == "win":
        print("This is correct! You win!")
    elif gameStatus == "end":
        print("Bye!")
    elif gameStatus == "lower":
        print("The magic number is lower")
    elif gameStatus == "higher":
        print("The magic number is higher")
    else:
        raise RuntimeError("Unexpected game status {}".format(gameStatus))

def runGame():
    gameStatus, magicNumber = init()
    while gameStatus != "win" and gameStatus != "end":
        playerNumber = processInput()
        gameStatus, magicNumber = update(gameStatus,magicNumber,playerNumber)
        render(gameStatus,magicNumber)


# Launch the game
runGame()

游戏循环模式有助于帮助我们合理组织代码,适应更复杂的程序设计!!

b252115c-ce75-41f9-b6e4-b9a211019686.jpeg

充分使用游戏素材,展示动画特效

主要思路比较直观:

  • 加载一些列游戏素材图片
  • 获取鼠标点击位置
  • 顺序播放一系列图片帧,获得爆炸效果

效果如下:
20230611034352- 2023-06-11 03-43-36.png

  • 加载素材

    explosion_frames = []

     for i in range(16):
         img = pygame.image.load(f"explosion/frame{i:04}.png")
         explosion_frames.append(img)
    
  • 获取鼠标位置

    # 处理鼠标点击事件

         if event.type == pygame.MOUSEBUTTONDOWN:
             mouse_x, mouse_y = event.pos
             #if button_x <= mouse_x <= button_x + button_width and button_y <= mouse_y <= button_y + button_height:
             explosion_playing = True
    
  • 绘制系列图片

    if explosion_playing:

         if frame_index < len(explosion_frames):
             frame = explosion_frames[frame_index]
             game_display.blit(frame, (mouse_x, mouse_y))
             frame_index += 1
             pygame.time.wait(100)
         else:
             explosion_playing = False
             frame_index = 0
    

完整代码如下:

import pygame

pygame.init()

# 窗口尺寸
width = 800
height = 600
game_display = pygame.display.set_mode((width, height))

# 颜色定义
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
BLUE = (0, 0, 255)

# 按钮尺寸和位置
button_width = 80
button_height = 40
button_x = width // 2 - button_width // 2
button_y = height // 2 - button_height // 2

# 加载爆炸动画帧
explosion_frames = []
for i in range(16):
    img = pygame.image.load(f"explosion/frame{i:04}.png")
    explosion_frames.append(img)

# 当前动画帧索引
frame_index = 0

# 爆炸动画是否正在播放
explosion_playing = False

# 游戏主循环
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

        # 处理鼠标点击事件
        if event.type == pygame.MOUSEBUTTONDOWN:
            mouse_x, mouse_y = event.pos
            #if button_x <= mouse_x <= button_x + button_width and button_y <= mouse_y <= button_y + button_height:
            explosion_playing = True

    # 绘制游戏内容
    game_display.fill(BLACK)  # 填充窗口为黑色

    # 绘制按钮
    if explosion_playing:
        if frame_index < len(explosion_frames):
            frame = explosion_frames[frame_index]
            game_display.blit(frame, (mouse_x, mouse_y))
            frame_index += 1
            pygame.time.wait(100)
        else:
            explosion_playing = False
            frame_index = 0
    else:
        pass
        #pygame.draw.rect(game_display, BLUE, (button_x, button_y, button_width, button_height), 2)

    # 更新显示
    pygame.d

isplay.update()

pygame.quit()

explosion-sample.zip

黑白棋

完善黑白棋

之前简单构建了黑白棋框架,只是渲染界面,并没有实际黑白棋运行逻辑: 如何反转棋子?

增加翻转棋子的功能
修改make_move函数,判断需要翻转的棋子,加入到list里面,然后设置一个便利flipped
鼠标点击的时候,判断是否flipped,则重新绘制draw_board
增加棋盘上的格子线
使用pygame.color函数定义一些常量



import pygame
import sys

# 游戏参数
SCREEN_WIDTH = 640
SCREEN_HEIGHT = 480
BOARD_SIZE = 8
CELL_SIZE = SCREEN_HEIGHT // BOARD_SIZE

# 颜色定义
# 颜色定义
BLACK = pygame.Color('black')
WHITE = pygame.Color('white')
BLUE = pygame.Color('blue')
GREEN = pygame.Color('green')
YELLOW = pygame.Color('yellow')



def create_board():
    board = [[' ' for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)]
    board[3][44] = 'W'
    board[3][45] = 'B'
    board[4][46] = 'B'
    board[4][47] = 'W'
    return board


def draw_board(screen, board, player):
    screen.fill(GREEN)

    for row in range(BOARD_SIZE):
        for col in range(BOARD_SIZE):
            pygame.draw.rect(screen, BLUE, (col * CELL_SIZE, row * CELL_SIZE, CELL_SIZE, CELL_SIZE))

            
            if board[row][col] == 'W':
                pygame.draw.circle(screen, WHITE,
                                   (col * CELL_SIZE + CELL_SIZE // 2, row * CELL_SIZE + CELL_SIZE // 2),
                                   CELL_SIZE // 2 - 4)
                
            elif board[row][col] == 'B':
                pygame.draw.circle(screen, BLACK,
                                   (col * CELL_SIZE + CELL_SIZE // 2, row * CELL_SIZE + CELL_SIZE // 2),
                                   CELL_SIZE // 2 - 4)
    if player == 'W':
        pygame.draw.circle(screen, WHITE,
                                   (540, 80),
                                   CELL_SIZE // 2 - 4)
    else:
        pygame.draw.circle(screen, BLACK,
                                   (540, 80),
                                   CELL_SIZE // 2 - 4)
    # 绘制格子线
    for i in range(BOARD_SIZE + 1):
        pygame.draw.line(screen, YELLOW, (0, i * CELL_SIZE), (SCREEN_WIDTH, i * CELL_SIZE))
        pygame.draw.line(screen, YELLOW, (i * CELL_SIZE, 0), (i * CELL_SIZE, SCREEN_HEIGHT))


    pygame.display.flip()


def is_valid_move(board, row, col, player):
    if board[row][col] != ' ':
        return False

    other_player = 'B' if player == 'W' else 'W'
    directions = [(0, -1), (0, 1), (-1, 0), (1, 0),
                  (-1, -1), (-1, 1), (1, -1), (1, 1)]

    for drow, dcol in directions:
        r, c = row, col
        r += drow
        c += dcol

        if (0 <= r < BOARD_SIZE) and (0 <= c < BOARD_SIZE) and (board[r][c] == other_player):
            r += drow
            c += dcol

            while (0 <= r < BOARD_SIZE) and (0 <= c < BOARD_SIZE):
                if board[r][c] == player:
                    return True
                elif board[r][c] == ' ':
                    break
                r += drow
                c += dcol

    return False


def make_move(board, row, col, player):
    if not is_valid_move(board, row, col, player):
        return False

    other_player = 'B' if player == 'W' else 'W'
    board[row][col] = player
    directions = [(0, -1), (0, 1), (-1, 0), (1, 0),
                  (-1, -1), (-1, 1), (1, -1), (1, 1)]

    flipped = False

    for drow, dcol in directions:
        r, c = row, col
        r += drow
        c += dcol

        if (0 <= r < BOARD_SIZE) and (0 <= c < BOARD_SIZE) and (board[r][c] == other_player):
            flip_list = []
            while (0 <= r < BOARD_SIZE) and (0 <= c < BOARD_SIZE):
                if board[r][c] == player:
                    flipped = True
                    for flip_row, flip_col in flip_list:
                        board[flip_row][flip_col] = player
                    break
                elif board[r][c] == ' ':
                    break
                else:
                    flip_list.append((r, c))
                r += drow
                c += dcol

    return flipped




def is_game_over(board):
    for row in range(BOARD_SIZE):
        for col in range(BOARD_SIZE):
            if board[row][col] == ' ':
                return False
    return True


def count_pieces(board):
    black_count = 0
    white_count = 0
    for row in range(BOARD_SIZE):
        for col in range(BOARD_SIZE):
            if board[row][col] == 'B':
                black_count += 1
            elif board[row][col] == 'W':
                white_count += 1
    return black_count, white_count


def main():
    pygame.init()
    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    pygame.display.set_caption("黑白棋游戏")
    clock = pygame.time.Clock()

    board = create_board()
    current_player = 'B'

    while not is_game_over(board):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()

            if event.type == pygame.MOUSEBUTTONDOWN:
                mouse_x, mouse_y = pygame.mouse.get_pos()
                col = mouse_x // CELL_SIZE
                row = mouse_y // CELL_SIZE

                if make_move(board, row, col, current_player):
                    current_player = 'W' if current_player == 'B' else 'B'
                    draw_board(screen, board, current_player)

        draw_board(screen, board, current_player)
        black_count, white_count = count_pieces(board)
        pygame.display.set_caption(f"黑棋: {black_count}  白棋: {white_count}")

        pygame.display.flip()
        clock.tick(60)

    black_count, white_count = count_pieces(board)
    pygame.display.set_caption(f"黑棋: {black_count}  白棋: {white_count}")

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()


if __name__ == '__main__':
    main()


程序解释

make_move函数解释:

  • 函数的输入参数包括:

    • board:当前游戏棋盘的状态,是一个二维列表,表示每个位置上的棋子情况。
    • row、col:玩家希望在棋盘上放置棋子的行和列索引。
    • player:当前玩家的标识符,可以是'W'(白棋)或'B'(黑棋)。

函数的返回值为布尔值,表示移动是否有效。如果移动有效,会更新棋盘的状态,将当前玩家的棋子放置在指定的位置,并将被翻转的对方棋子也进行翻转。如果移动无效,不会有任何改变。

  • 函数的主要步骤如下:

    1. 首先检查指定位置是否是有效的落子位置,通过调用is_valid_move函数进行判断。如果无效,则返回False表示移动无效。
    2. 如果移动有效,将当前玩家的棋子放置在指定位置boardrow = player。
    3. 接下来,遍历八个方向的偏移量,表示在该方向上搜索对方棋子是否可以被翻转。
    4. 对于每个方向,根据偏移量从指定位置开始向该方向前进,如果遇到对方棋子,则继续前进直到遇到空格、边界或当前玩家的棋子。
    5. 如果在该方向上遇到了当前玩家的棋子,将之前记录的需要翻转的对方棋子进行翻转操作,即将boardflip_row设置为当前玩家的棋子。
    6. 最后,返回True表示移动有效。

is_valid_move

  • 函数的输入参数包括:

    • board:当前游戏棋盘的状态,是一个二维列表,表示每个位置上的棋子情况。
    • row、col:玩家希望在棋盘上放置棋子的行和列索引。
    • player:当前玩家的标识符,可以是'W'(白棋)或'B'(黑棋)。

函数的返回值为布尔值,表示移动是否有效。如果移动有效,返回True;如果移动无效,返回False。

  • 函数的主要步骤如下:

    1. 首先,检查指定位置上是否已经有棋子。如果该位置已经有棋子,则返回False表示移动无效。
    2. 确定对方玩家的标识符。如果当前玩家是白棋('W'),则对方玩家是黑棋('B');反之亦然。
    3. 遍历八个方向的偏移量,表示在该方向上搜索对方棋子。
    4. 对于每个方向,从指定位置开始向该方向前进,如果遇到对方玩家的棋子,则继续前进直到遇到空格、边界或当前玩家的棋子。
    5. 如果在该方向上遇到了当前玩家的棋子,说明在该方向上可以进行翻转操作,表示移动有效,返回True。
    6. 如果在所有方向上都没有找到可以翻转的对方棋子,则移动无效,返回False。

create_board函数:

  • 函数的主要步骤如下:

    1. 创建一个二维列表 board,用于表示黑白棋盘。列表的行数和列数都为 BOARD_SIZE,每个元素初始值为 ' ',表示空格。
    2. 将黑棋和白棋的初始位置设置在棋盘的中心,即第3行第3列、第3行第4列、第4行第3列和第4行第4列。将这些位置上的元素分别设置为 'W' 和 'B',表示白棋和黑棋。
    3. 返回创建好的棋盘 board。

create_board函数创建一个初始状态的黑白棋棋盘,并返回该棋盘。每个棋子的位置由特定的字符表示,空格表示未放置棋子的位置,'W' 表示白棋,'B' 表示黑棋。

  • Direction 8个方向定义:

directions列表中的元素表示了八个方向,每个元素是一个包含两个整数的元组,分别表示在行和列上的移动偏移量。下面是对每个元素所代表的方向的解释:

1. (0, -1):向左移动一列
2. (0, 1):向右移动一列
3. (-1, 0):向上移动一行
4. (1, 0):向下移动一行
5. (-1, -1):向左上方移动一行一列
6. (-1, 1):向右上方移动一行一列
7. (1, -1):向左下方移动一行一列
8. (1, 1):向右下方移动一行一列

这些方向涵盖了棋子可以进行的所有移动方向,包括水平、垂直和对角线方向。
在黑白棋游戏中,通过遍历这些方向,并在棋盘上按照相应的偏移量移动,可以判断该位置是否为有效的落子位置,并且可以在落子时翻转对方的棋子。