Python 贪吃蛇-项目目录结构

时间:2024-05-07 07:23:28

在这里插入图片描述

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