用 Python 编写经典贪吃蛇游戏

文章目录

  • 效果图:
  • 项目目录结构
  • main.py
  • game/apple.py
  • game/base.py
  • game/snake.py
  • constant.py
  • 效果图:

    请添加图片描述

    项目目录结构

    main.py

    from snake.game.apple import Apple  # 导入苹果类
    from snake.game.base import *  # 导入游戏基类
    from snake.game.snake import Snake  # 导入蛇类
    
    
    class SnakeGame(GameBase):
        """贪吃蛇游戏"""
    
        def __init__(self):
            """初始化游戏"""
            super(SnakeGame, self).__init__(
                game_name=GAME_NAME, icon=ICON,  # 调用基类的初始化方法
                screen_size=SCREEN_SIZE,
                display_mode=DISPLAY_MODE,
                loop_speed=LOOP_SPEED,
                font_size=FONT_SIZE,
                background=WHITE,
                font_name=None
            )
            # 绘制背景
            self.prepare_background()
            # 创建游戏对象
            self.apple_count = 0  # 苹果计数器
            self.high_score = 0  # 记录最高分
            self.snake = Snake(self)  # 创建蛇对象
            self.apple = Apple(self)  # 创建苹果对象
            # 绑定按键事件
            self.add_key_binding(KEY_UP, self.snake.turn, direction=UP)  # 绑定上方向键
            self.add_key_binding(KEY_DOWN, self.snake.turn, direction=DOWN)  # 绑定下方向键
            self.add_key_binding(KEY_LEFT, self.snake.turn, direction=LEFT)  # 绑定左方向键
            self.add_key_binding(KEY_RIGHT, self.snake.turn, direction=RIGHT)  # 绑定右方向键
            self.add_key_binding(KEY_RESTART, self.restart)  # 绑定R键(重启游戏)
            self.add_key_binding(KEY_PAUSE, self.pause)  # 绑定R键(重启游戏)
            self.add_key_binding(KEY_EXIT, self.quit)  # 绑定退出键
            # 添加绘图函数
            self.add_draw_action(self.draw_score)  # 添加绘制分数的函数
    
        def prepare_background(self):
            """准备背景"""
            self.background.fill(BACKGROUND_COLOR)  # 用背景颜色填充背景
            for _ in range(CELL_SIZE, SCREEN_WIDTH, CELL_SIZE):  # 绘制垂直网格线
                self.draw.line(self.background, GRID_COLOR, (_, 0), (_, SCREEN_HEIGHT))
            for _ in range(CELL_SIZE, SCREEN_HEIGHT, CELL_SIZE):  # 绘制水平网格线
                self.draw.line(self.background, GRID_COLOR, (0, _), (SCREEN_WIDTH, _))
    
        def restart(self):
            """重启游戏"""
            if not self.snake.is_alive:  # 如果蛇已经死亡
                self.apple_count = 0  # 重置苹果计数器
                self.apple.drop()  # 重新放置苹果
                self.snake.restart_pawn()  # 重生蛇
                self.running = True  # 继续游戏循环
    
        def draw_score(self):
            """绘制分数"""
            text = f"Apple: {self.apple_count}"  # 准备要绘制的文本
            self.high_score = max(self.high_score, self.apple_count)  # 更新最高分
            self.draw_text(text, (0, 0), (255, 255, 33))  # 绘制文本
    
            if not self.snake.is_alive:  # 如果蛇已经死亡
                self.draw_text(" 游戏结束 ", (SCREEN_WIDTH / 2 - 54, SCREEN_HEIGHT / 2 - 10),  # 绘制游戏结束文本
                               (255, 33, 33), WHITE)
    
                self.draw_text(" 按R键重启 ", (SCREEN_WIDTH / 2 - 85, SCREEN_HEIGHT / 2 + 20),  # 绘制重启提示文本
                               GREY, DARK_GREY)
    
                self.draw_text(f"当前最高分: {self.high_score}", (SCREEN_WIDTH / 2 - 114, SCREEN_HEIGHT / 2 + 50),  # 绘制最高分文本
                               (255, 33, 33), WHITE)  # 展示最高分
    
            if not self.running and self.snake.is_alive:  # 如果游戏暂停且蛇还活着
                self.draw_text("游戏暂停 ", (SCREEN_WIDTH / 2 - 55, SCREEN_HEIGHT / 2 - 10),  # 绘制游戏暂停文本
                               LIGHT_GREY, DARK_GREY)
    
    
    if __name__ == '__main__':
        SnakeGame().run()  # 运行游戏
    
    

    game/apple.py

    from random import randint
    from snake.constant import *
    
    
    class Apple:
        """
        苹果类:表示游戏中的苹果,蛇吃到苹果会增长身体长度。
        """
    
        def __init__(self, game):
            """
            初始化苹果对象。
            :param game: 游戏对象。
            """
            self.game = game
            self.x = self.y = 0  # 苹果的初始位置
            self.game.add_draw_action(self.draw)  # 将 draw 方法添加到游戏的绘制动作列表中
            self.drop()  # 生成一个新的苹果
    
        def drop(self):
            """
            生成一个新的苹果,确保苹果不在蛇的身体上。
            """
            snake = self.game.snake.body + [self.game.snake.head]  # 获取蛇的身体和头部的所有位置
            while True:
                (x, y) = randint(0, COLUMNS - 1), randint(0, ROWS - 1)  # 随机生成一个位置
                if (x, y) not in snake:  # 如果这个位置不在蛇的身体上
                    self.x, self.y = x, y  # 将苹果的位置设置为这个位置
                    break  # 退出循环
    
        def draw(self):
            """
            绘制苹果。
            """
            self.game.draw_cell(
                (self.x, self.y),  # 苹果的位置
                CELL_SIZE,  # 每个单元格的大小
                APPLE_COLOR_SKIN,  # 苹果的外框颜色
                APPLE_COLOR_BODY  # 苹果的主体颜色
            )
    

    game/base.py

    import os
    import sys
    import time
    from snake.constant import *
    
    # 使窗口居中
    os.environ["SDL_VIDEO_CENTERED"] = "1"
    
    # MyGame 默认值
    GAME_NAME = "贪吃蛇 By stormsha"
    SCREEN_SIZE = 640, 480
    DISPLAY_MODE = pygame.HWSURFACE | pygame.DOUBLEBUF
    LOOP_SPEED = 60
    # noinspection SpellCheckingInspection
    FONT_NAME = "resources/MONACO.ttf"
    FONT_SIZE = 16
    KEY_PAUSE = pygame.K_PAUSE
    
    
    class GameBase(object):
        """pygame模板类"""
    
        def __init__(self, **kwargs):
            """初始化
    
            可选参数:
                game_name       游戏名称
                icon            图标文件名
                screen_size     画面大小
                display_mode    显示模式
                loop_speed      主循环速度
                font_name       字体文件名
                font_size       字体大小
            """
            pygame.init()
            pygame.mixer.init()
            self.game_name = kwargs.get("game_name") or GAME_NAME
            pygame.display.set_caption(self.game_name)
            self.screen_size = kwargs.get("screen_size") or SCREEN_SIZE
            self.screen_width, self.screen_height = self.screen_size
            self.display_mode = kwargs.get("display_mode") or DISPLAY_MODE
            self.images = {}
            self.sounds = {}
            self.musics = {}
            self.icon = kwargs.get("icon") or None
            self.icon and pygame.display.set_icon(pygame.image.load(self.icon))
            self.screen = pygame.display.set_mode(self.screen_size,
                                                  self.display_mode)
            self.loop_speed = kwargs.get("loop_speed") or LOOP_SPEED
            self.font_size = kwargs.get("font_size") or FONT_SIZE
            self.background = None
    
            # noinspection SpellCheckingInspection
            ''' 支持中文的字体
            新细明体:PMingLiU
            细明体:MingLiU
            标楷体:DFKai - SB
            黑体:SimHei
            宋体:SimSun
            新宋体:NSimSun
            仿宋:FangSong
            楷体:KaiTi
            仿宋_GB2312:FangSong_GB2312
            楷体_GB2312:KaiTi_GB2312
            微软正黑体:Microsoft JhengHei
            微软雅黑体:Microsoft YaHei
            '''
            self.font_name = kwargs.get("font_name") or pygame.font.match_font('SimHei')  # 获取系统字体
    
            self.font = pygame.font.Font(self.font_name, self.font_size)
            self.clock = pygame.time.Clock()
            self.now = 0
            self.background_color = kwargs.get("background") or BLACK
            self.set_background()
            self.key_bindings = {}  # 按键与函数绑定字典
            self.add_key_binding(KEY_PAUSE, self.pause)
    
            self.game_actions = {}  # 游戏数据更新动作
    
            self.draw_actions = [self.draw_background]  # 画面更新动作列表
    
            self.running = True
            self.draw = pygame.draw
    
        def run(self):
            """主循环"""
            while True:
                self.now = pygame.time.get_ticks()
                self.process_events()
                if self.running:
                    self.update_game_data()
                self.update_display()
                self.clock.tick(self.loop_speed)
    
        def pause(self):
            """暂停游戏"""
            self.running = not self.running
            if self.running:
                for action in self.game_actions.values():
                    if action["next_time"]:
                        action["next_time"] = self.now + action["interval"]
    
        def process_events(self):
            """事件处理"""
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.quit()
                elif event.type == pygame.KEYDOWN:
                    action, kwargs = self.key_bindings.get(event.key, (None, None))
                    # noinspection all
                    action(**kwargs) if kwargs else action() if action else None
    
        def update_game_data(self):
            """更新游戏数据"""
            for action in self.game_actions.values():
                if not action["next_time"]:
                    action["run"]()
                elif self.now >= action["next_time"]:
                    action["next_time"] += action["interval"]
                    action["run"]()
    
        def update_display(self):
            """更新画面显示"""
            for action in self.draw_actions:
                action()
            pygame.display.flip()
    
        def draw_background(self):
            """绘制背景"""
            self.screen.blit(self.background, (0, 0))
    
        def add_key_binding(self, key, action, **kwargs):
            """增加按键绑定"""
            self.key_bindings[key] = action, kwargs
    
        # TODO: 更新动作若有次序要求,则用字典保存不合适
        def add_game_action(self, name, action, interval=0):
            """添加游戏数据更新动作"""
            next_time = self.now + interval if interval else None
            self.game_actions[name] = dict(
                run=action,
                interval=interval,
                next_time=next_time)
    
        def add_draw_action(self, action):
            """添加画面更新动作"""
            self.draw_actions.append(action)
    
        def draw_text(self, text, loc, color, bgcolor=None):
            if bgcolor:
                surface = self.font.render(text, True, color, bgcolor)
            else:
                surface = self.font.render(text, True, color)
            self.screen.blit(surface, loc)
    
        def draw_cell(self, xy, size, color1, color2=None):
            x, y = xy
            rect = pygame.Rect(x * size, y * size, size, size)
            self.screen.fill(color1, rect)
            if color2:
                self.screen.fill(color2, rect.inflate(-4, -4))
    
        @staticmethod
        def quit():
            """退出游戏"""
            pygame.quit()
            sys.exit(0)
    
        @staticmethod
        def load_music(filename):
            pygame.mixer.music.load(filename)
    
        @staticmethod
        def play_music():
            pygame.mixer.music.play(-1)
    
        @staticmethod
        def pause_music():
            pygame.mixer.music.pause()
    
        @staticmethod
        def resume_music():
            pygame.mixer.music.unpause()
    
        @staticmethod
        def stop_music():
            pygame.mixer.music.stop()
    
        def save_screenshots(self):
            filename = time.strftime('screenshots/%Y%m%d%H%M%S.png')
            pygame.image.save(self.screen, filename)
    
        def load_images(self, filename, sub_img=None):
            sub_img = sub_img or {}
            image = pygame.image.load(filename).convert_alpha()  # 文件打开失败
            for name, rect in sub_img.items():
                x, y, w, h = rect
                self.images[name] = image.subsurface(pygame.Rect((x, y), (w, h)))
    
        def set_background(self, background=None):
            if isinstance(background, str):
                self.background = pygame.image.load(background)
            else:
                self.background = pygame.Surface(self.screen_size)
                self.background_color = background \
                    if isinstance(background, tuple) else (0, 0, 0)
                self.background.fill(self.background_color)
    
        def load_sounds(self, **sounds):
            """
            @summary: 加载音乐
            :param sounds:
            :return:
            """
            for name, filename in sounds.items():
                self.sounds[name] = pygame.mixer.Sound(filename)
    
        def play_sound(self, name):
            self.sounds[name].play()
    
    
    if __name__ == '__main__':
        GameBase().run()
    
    

    game/snake.py

    import pygame
    
    from snake import constant
    
    
    class Snake:
        """贪吃蛇"""
    
        def __init__(self, game):
            self.game = game
            self.sound_hit = pygame.mixer.Sound("resources/hit.wav")
            self.sound_eat = pygame.mixer.Sound("resources/eat.wav")
            self.game.add_draw_action(self.draw)
    
            # 初始化数据
            self.head = (constant.SNAKE_X, constant.SNAKE_Y)  # 蛇头当前位置
            self.body = [(-1, -1)] * constant.SNAKE_BODY_LENGTH  # 蛇身长度
            self.direction = constant.SNAKE_DIRECTION  # 当前方向
            self.new_direction = constant.SNAKE_DIRECTION  # 移动方向
            self.speed = constant.SNAKE_SPEED  # 移动速度
            self.is_alive = True  # 是否存活
    
        def set_speed(self, speed):
            """
            @summary: 设置蛇的移动速度
            :param speed: 移动速度
            :return:
            """
            self._speed = speed
            self.game.add_game_action("snake.move", self.move, 1000 // speed)
    
        def get_speed(self):
            """
            @summary: 获取当前蛇的移动速度
            :return:
            """
            return self._speed
    
        @property
        def speed(self):
            return self._speed
    
        @speed.setter
        def speed(self, speed):
            self._speed = speed
            self.game.add_game_action("snake.move", self.move, 1000 // speed)
    
        def draw(self):
            """
            @summary: 绘制小蛇
            :return: 
            """
            skin_color = constant.SNAKE_COLOR_SKIN if self.is_alive else constant.SNAKE_COLOR_SKIN_DEAD
            body_color = constant.SNAKE_COLOR_BODY if self.is_alive else constant.SNAKE_COLOR_BODY_DEAD
            head_color = constant.SNAKE_COLOR_HEAD if self.is_alive else constant.SNAKE_COLOR_HEAD_DEAD
            for cell in self.body:
                self.game.draw_cell(cell, constant.CELL_SIZE, skin_color, body_color)
            self.game.draw_cell(self.head, constant.CELL_SIZE, skin_color, head_color)
    
        def turn(self, direction):
            """
            @summary: 改变小蛇方向
            :param direction: 
            :return: 
            """
            if (self.direction in (constant.LEFT, constant.RIGHT) and direction in (constant.UP, constant.DOWN) or
                    self.direction in (constant.UP, constant.DOWN) and direction in (constant.LEFT, constant.RIGHT)):
                self.new_direction = direction
    
        def move(self):
            """
            @summary: 移动小蛇
            :return: 
            """
            if not self.is_alive:
                return
            # 设定方向
            self.direction = self.new_direction
            # 检测前方
            x, y = meeting = (
                self.head[0] + self.direction[0],
                self.head[1] + self.direction[1]
            )
            # 死亡判断
            if meeting in self.body or x not in range(constant.COLUMNS) or y not in range(constant.ROWS):
                self.die()
                return
            # 判断是否吃了苹果
            if meeting == (self.game.apple.x, self.game.apple.y):
                self.sound_eat.play()
                self.game.apple.drop()
                self.game.apple_count += 1
            else:
                self.body.pop()
            # 蛇头变成脖子
            self.body = [self.head] + self.body
            # 蛇头移动到新位置
            self.head = meeting
    
        def restart_pawn(self):
            """重生"""
            self.head = (constant.SNAKE_X, constant.SNAKE_Y)
            self.body = [(-1, -1)] * constant.SNAKE_BODY_LENGTH
            self.direction = constant.SNAKE_DIRECTION
            self.new_direction = constant.SNAKE_DIRECTION
            self.speed = constant.SNAKE_SPEED
            self.is_alive = True
    
        def die(self):
            self.sound_hit.play()
            self.is_alive = False
    
    

    constant.py

    import pygame
    
    # 颜色设定
    BLACK = 0, 0, 0
    WHITE = 255, 255, 255
    DARK_GREY = 33, 33, 33
    GREY = 127, 127, 127
    LIGHT_GREY = 192, 192, 192
    BACKGROUND_COLOR = BLACK
    GRID_COLOR = DARK_GREY
    APPLE_COLOR_SKIN = 255, 127, 127
    APPLE_COLOR_BODY = 255, 66, 66
    SNAKE_COLOR_SKIN = 33, 255, 33
    SNAKE_COLOR_BODY = 33, 192, 33
    SNAKE_COLOR_HEAD = 192, 192, 33
    SNAKE_COLOR_SKIN_DEAD = LIGHT_GREY
    SNAKE_COLOR_BODY_DEAD = GREY
    SNAKE_COLOR_HEAD_DEAD = DARK_GREY
    
    # 一般设定
    GAME_NAME = "SnakeGame"
    SCREEN_SIZE = SCREEN_WIDTH, SCREEN_HEIGHT = 640, 480
    CELL_SIZE = 20
    COLUMNS, ROWS = SCREEN_WIDTH // CELL_SIZE, SCREEN_HEIGHT // CELL_SIZE
    DISPLAY_MODE = pygame.HWSURFACE | pygame.DOUBLEBUF
    LOOP_SPEED = 60
    # noinspection SpellCheckingInspection
    FONT_NAME = "resources/MONACO.TTF"
    FONT_SIZE = 16
    # noinspection SpellCheckingInspection
    ICON = "resources/snake.png"
    UP, DOWN, LEFT, RIGHT = (0, -1), (0, 1), (-1, 0), (1, 0)
    
    # 按键设定
    KEY_EXIT = pygame.K_ESCAPE
    KEY_UP = pygame.K_UP
    KEY_DOWN = pygame.K_DOWN
    KEY_LEFT = pygame.K_LEFT
    KEY_RIGHT = pygame.K_RIGHT
    KEY_RESTART = pygame.K_r
    K_PAUSE = pygame.K_PAUSE
    
    # 蛇的默认值
    SNAKE_X = 0
    SNAKE_Y = 0
    SNAKE_BODY_LENGTH = 5
    SNAKE_DIRECTION = RIGHT
    SNAKE_SPEED = 10
    
    

    源码地址:https://gitcode.com/stormsha1/games/overview

    作者:stormsha

    物联沃分享整理
    物联沃-IOTWORD物联网 » 用 Python 编写经典贪吃蛇游戏

    发表回复