中秋特辑-《兔子推着月饼上月球》

一、前言

中秋佳节又快到了,这是中国传统文化中非常重要的一个节日。在这个团圆的日子里,家人团聚,共享月光、赏悦月色,感受中秋的浪漫与温馨。

在此特别推出一个中秋版的推箱子小游戏 - 《兔子推着月饼上月球》。游戏以可爱的中秋元素为主题,控制小兔子挑战十关推箱子关卡,最终把月饼送到月亮指定位置,完成月球之旅。

通过这个小游戏,希望大家在度过一个温馨欢乐的中秋佳节的同时,也感受到一点古老的中秋文化的魅力。以下就让我们开始这趟奇妙的中秋之旅吧!
提前祝大家中秋快乐!愿这个小游戏能让你感受一点中秋的乐趣与魅力。

二、准备素材

我是在阿里巴巴的iconfont寻找一些素材

游戏图片

  • 墙体:灯笼🏮、烟花🎆

  • 角色:兔子🐰、嫦娥

  • 箱子:月饼🥮、甜甜圈🍩

  • 目的地:月球🌕

  • 空:烟花🎆、星星🌟

游戏背景音乐

  • 卡农

  • 龙珠

三、编写代码

编码环境

环境名称 版本
python 3.10
pygame 2.5.1

安装依赖

pip install pygame

准备游戏地图

python 复制代码
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author: Hui
# @Desc: { 游戏地图 }
# @Date: 2023/09/13 12:10
from typing import Dict, List

EMPTY_FLAG = 0  # 表示空
WALL_FLAG = 1  # 表示墙
BOX_FLAG = 2  # 表示月饼
PLAYER_FLAG = 3  # 表示兔子
DEST_FLAG = 4  # 表示目的地 (月亮)
FINISH_BOX_FLAG = 5  # 表示已完成的箱子
PLAYER_DEST_FLAG = 6  # 表示角色与目的地重合
BOX_DEST_FLAG = 7  # 表示箱子与目的地重合
BG_FLAG = 9  # 表示墙外面 其他

# 游戏地图 level => 二维列表地图
GAME_MAP: Dict[int, List[List[int]]] = {
    1: [
        [9, 9, 1, 1, 1, 9, 9, 9],
        [9, 9, 1, 4, 1, 9, 9, 9],
        [9, 9, 1, 0, 1, 1, 1, 1],
        [1, 1, 1, 2, 0, 2, 4, 1],
        [1, 4, 0, 2, 3, 1, 1, 1],
        [1, 1, 1, 1, 2, 1, 9, 9],
        [9, 9, 9, 1, 4, 1, 9, 9],
        [9, 9, 9, 1, 1, 1, 9, 9]
    ],
    2: [
        [9, 9, 1, 1, 1, 1, 9, 9],
        [9, 9, 1, 4, 4, 1, 9, 9],
        [9, 1, 1, 0, 4, 1, 1, 9],
        [9, 1, 0, 0, 2, 4, 1, 9],
        [1, 1, 0, 2, 3, 0, 1, 1],
        [1, 0, 0, 1, 2, 2, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 1],
        [1, 1, 1, 1, 1, 1, 1, 1]
    ],
    3: [
        [9, 9, 1, 1, 1, 1, 9, 9],
        [9, 1, 1, 0, 0, 1, 9, 9],
        [9, 1, 3, 2, 0, 1, 9, 9],
        [9, 1, 1, 2, 0, 1, 1, 9],
        [9, 1, 1, 0, 2, 0, 1, 9],
        [9, 1, 4, 2, 0, 0, 1, 9],
        [9, 1, 4, 4, 6, 4, 1, 9],
        [9, 1, 1, 1, 1, 1, 1, 9]
    ]
    ]
}

文章里就准备了三张地图,因为太占行数了,就不全部贴出来了。通过一个字典来当作推箱子的游戏地图,key 是游戏的关卡,value 为二维列表当做游戏地图。里面的数字代表游戏素材。

  • 0 表示空
  • 1 表示墙
  • 2 表示月饼
  • 3 表示兔子
  • 4 表示目的地 (月亮)
  • 5 表示已完成的箱子
  • 6 表示角色与目的地重合
  • 7 表示箱子与目的地重合

这个地图是推箱子的精髓,根据数字标识、坐标(x,y),来画游戏关卡地图非常方便,上下左右控制兔子🐰移动,也只需改变地图里数字标识然后重新绘制就能达到移动的效果。

pygame 渲染绘制

地图准备好了就可以通过pygame直接画了

python 复制代码
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author: Hui
# @Desc: { 游戏入口模块 }
# @Date: 2023/09/13 12:20
import sys

import pygame
from pygame import Surface
from src.game_map import GAME_MAP, WALL_FLAG, RABBIT_FLAG, MOON_CAKE_FLAG, TERMINAL_FLAG

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


class RabbitBox(object):
    GRID_SIZE = 64  # 单个方块大小
    GAME_TITLE = "🐰兔子推着月饼🥮上月球🌕"

    def __init__(self, game_level: int = 1):
        self._init_game()

        self.game_screen: Surface = None
        self.game_level = min(game_level, len(GAME_MAP))
        self.rabbit_pos: tuple = None

        self.setup_game_screen()

    def _init_game(self):
        pygame.init()
        pygame.display.set_caption(self.GAME_TITLE)

    def setup_game_screen(self):
        """根据游戏地图配置游戏屏幕"""

        rows = GAME_MAP[self.game_level]
        row_num = len(rows)
        col_num = len(rows[0])

        self.game_screen = pygame.display.set_mode(size=[self.GRID_SIZE * row_num, self.GRID_SIZE * col_num])

    def draw_map(self):
        """遍历地图数据绘制"""

        # 获取游戏地图
        game_map = GAME_MAP[self.game_level]

        for y in range(len(game_map)):
            for x in range(len(game_map[y])):
                if game_map[y][x] == WALL_FLAG:
                    # 画墙
                    pygame.draw.rect(
                        surface=self.game_screen,
                        color=BLACK,
                        rect=[x * self.GRID_SIZE, y * self.GRID_SIZE, self.GRID_SIZE, self.GRID_SIZE]
                    )

                elif game_map[y][x] == RABBIT_FLAG:
                    # 画兔子
                    pygame.draw.rect(
                        surface=self.game_screen,  # 游戏屏幕
                        color=RED,  # 颜色
                        rect=[
                            x * self.GRID_SIZE,  # x 坐标
                            y * self.GRID_SIZE,  # y 坐标
                            self.GRID_SIZE,  # 宽 width
                            self.GRID_SIZE,  # 高 height
                        ]
                    )
                    self.rabbit_pos = (x, y)  # 标识兔子坐标

                elif game_map[y][x] == MOON_CAKE_FLAG:
                    # 画月饼
                    pygame.draw.circle(
                        surface=self.game_screen,
                        color=WHITE,
                        center=[x * self.GRID_SIZE + self.GRID_SIZE / 2, y * self.GRID_SIZE + self.GRID_SIZE / 2],
                        radius=self.GRID_SIZE / 3
                    )

                elif game_map[y][x] == TERMINAL_FLAG:
                    # 画月饼的目的地(月球)
                    pygame.draw.circle(
                        surface=self.game_screen,
                        color=WHITE,
                        center=[x * self.GRID_SIZE + self.GRID_SIZE / 2, y * self.GRID_SIZE + self.GRID_SIZE / 2],
                        radius=self.GRID_SIZE / 2
                    )

    def run_game(self):

        # 主循环事件监听与渲染
        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    sys.exit()

                elif event.type == pygame.KEYDOWN:
                    # 监听键盘事件,上下左右控制兔子,记录方向
                    if event.key == pygame.K_UP:
                        pass
                    elif event.key == pygame.K_DOWN:
                        pass
                    elif event.key == pygame.K_LEFT:
                        pass
                    elif event.key == pygame.K_RIGHT:
                        pass

            self.game_screen.fill(BLACK)
            self.draw_map()

            pygame.display.flip()


def main():
    RabbitBox().run_game()


if __name__ == '__main__':
    main()
  1. pygame.init() 先初始化好pygame 游戏环境

  2. RabbitBox() 初始化一些游戏属性

    1. 关卡
    2. 游戏屏幕screen
    3. 兔子坐标
  3. pygame 游戏渲染 事件循环

    1. 绘制游戏屏幕 self.game_screen.fill(BLACK)

    2. 绘制游戏地图 self.draw_map()

    3. 事件处理

    4. 游戏刷新展示 pygame.display.flip()

当前绘制的一些矩形、圆等图形不是游戏素材图片,先大概看看地图的样子

添加游戏素材图片

python 复制代码
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author: Hui
# @Desc: { 游戏入口模块 }
# @Date: 2023/09/13 12:20
import os
import sys
import random

import pygame
from pygame import Surface
from src.game_map import GAME_MAP, WALL_FLAG, PLAYER_FLAG, BOX_FLAG, TERMINAL_FLAG, EMPTY_FLAG

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


class RabbitBox(object):
    GRID_SIZE = 64  # 单个方块大小
    GAME_TITLE = "🐰兔子推着月饼🥮上月球🌕"

    BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

    WALLS = [
        pygame.image.load(os.path.join(BASE_DIR, "res/img/lantern.png"))
    ]

    PLAYERS = [
        pygame.image.load(os.path.join(BASE_DIR, "res/img/rabbit.png"))
    ]

    BOXS = [
        pygame.image.load(os.path.join(BASE_DIR, "res/img/moon_cake.png"))
    ]

    TERMINAL_BOXS = [
        pygame.image.load(os.path.join(BASE_DIR, "res/img/moon_02.png"))
    ]

    EMPTY_BOXS = [
        pygame.image.load(os.path.join(BASE_DIR, "res/img/fireworks.png")),
        pygame.image.load(os.path.join(BASE_DIR, "res/img/fireworks_02.png")),
        pygame.image.load(os.path.join(BASE_DIR, "res/img/star.png")),
    ]

    def __init__(self, game_level: int = 1):
        self._init_game()

        self.game_screen: Surface = None
        self.game_level = min(game_level, len(GAME_MAP))
        self.player_pos: tuple = None

        self.box: Surface = None
        self.player: Surface = None
        self.wall: Surface = None
        self.terminal_box: Surface = None
        self.finished_box: Surface = None
        self.empty_box: Surface = None

        self.setup_game_screen()

        self.random_game_material()

    def random_game_material(self):
        """随机游戏素材"""
        self.wall = random.choice(self.WALLS)
        self.player = random.choice(self.PLAYERS)
        self.box = random.choice(self.BOXS)
        self.terminal_box = random.choice(self.TERMINAL_BOXS)
        self.empty_box = random.choice(self.EMPTY_BOXS)

    def _init_game(self):
        pygame.init()
        pygame.display.set_caption(self.GAME_TITLE)

    def setup_game_screen(self):
        """根据游戏地图配置游戏屏幕"""

        rows = GAME_MAP[self.game_level]
        row_num = len(rows)
        col_num = len(rows[0])

        self.game_screen = pygame.display.set_mode(size=[self.GRID_SIZE * row_num, self.GRID_SIZE * col_num])
    
    def draw_map(self):
        """遍历地图数据绘制"""
    
        # 获取游戏地图
        game_map = GAME_MAP[self.game_level]
    
        for i, row in enumerate(game_map):
            for j, col in enumerate(row):
    
                # 计算偏移坐标
                offset_x = j * self.GRID_SIZE
                offset_y = i * self.GRID_SIZE
    
                # 判断标识
                num_flag = game_map[i][j]
                if num_flag == WALL_FLAG:
                    # 画墙 (灯笼)
                    self.game_screen.blit(source=self.wall, dest=(offset_x, offset_y))
    
                elif num_flag == PLAYER_FLAG:
                    # 画角色(兔子)
                    self.game_screen.blit(source=self.player, dest=(offset_x, offset_y))
                    self.player_pos = (i, j)  # 标识兔子坐标
    
                elif num_flag == BOX_FLAG:
                    # 画箱子(月饼)
                    self.game_screen.blit(source=self.box, dest=(offset_x, offset_y))
    
                elif num_flag == TERMINAL_FLAG:
                    # 画月饼的目的地(月球)
                    self.game_screen.blit(source=self.terminal_box, dest=(offset_x, offset_y))
    
                elif num_flag == EMPTY_FLAG:
                    # 画空(背景)
                    self.game_screen.blit(source=self.empty_box, dest=(offset_x, offset_y))
    
        return self.player_pos

    def run_game(self):

        # 主循环事件监听与渲染
        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    sys.exit()

            self.game_screen.fill(BLACK)
            self.draw_map()

            pygame.display.flip()


def main():
    RabbitBox().run_game()


if __name__ == '__main__':
    main()
  • pygame.image.load(os.path.join(BASE_DIR, "res/img/rabbit.png")) 加载游戏图片

  • self.game_screen.blit(source=self.player, dest=(x, y)) 绘制在游戏屏幕上

    • x, y 图片坐标,通过二维列表的位置 与 图片的大小进行计算
  • self.player = random.choice(self.PLAYERS) 随机游戏素材

    • 后面可以通过调节列表大小来控制出现的概率

事件处理

地图已经绘制完毕,接下来就是兔子移动事件监听与处理。

python 复制代码
def move_up(self):
    """
    玩家向上移动处理
    """
    print("move_up")

    i, j = self.player_pos
    map_list = GAME_MAP[self.game_level]

    def handle_player_dest():
        """人和目的地重合处理"""
        if map_list[i][j] == PLAYER_DEST_FLAG:
            # 当前位置是人和目的地重合处理
            map_list[i][j] = DEST_FLAG  # 把原来角色位置改成目的地
        else:
            map_list[i][j] = EMPTY_FLAG  # 把原来角色位置改成空白

    if map_list[i - 1][j] == BOX_FLAG and \
            (map_list[i - 2][j] == EMPTY_FLAG or map_list[i - 2][j] == DEST_FLAG):
        print('up box')
        # 玩家上边(i - 1)是箱子
        # 且箱子的上边只能是空白或者目的地才可向上
        map_list[i - 1][j] = PLAYER_FLAG  # 角色向上移动改变角色位置

        # 人和目的地重合判断处理
        handle_player_dest()

        map_list[i - 2][j] = BOX_FLAG  # 把箱子向上移改变位置

    elif map_list[i - 1][j] == EMPTY_FLAG:
        # 玩家上边(i - 1)是空白
        print('up empty')
        map_list[i - 1][j] = PLAYER_FLAG  # 角色向上移动改变角色位置

        handle_player_dest()

    elif map_list[i - 1][j] == DEST_FLAG:
        # 玩家上边是目的地
        print('up destination')
        map_list[i - 1][j] = PLAYER_DEST_FLAG  # 让角色和目的地重合

        handle_player_dest()

        
def _event_handle(self):
    """事件处理"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()

        key_pressed = pygame.key.get_pressed()
        if key_pressed[K_a] or key_pressed[K_LEFT]:
            # 玩家向左移动
            self.move_lef()

        elif key_pressed[K_d] or key_pressed[K_RIGHT]:
            # 玩家向右移动
            self.move_right()

        elif key_pressed[K_w] or key_pressed[K_UP]:
            # 玩家向上移动
            self.move_up()

        elif key_pressed[K_s] or key_pressed[K_DOWN]:
            # 玩家上下移动
            self.move_down()

def run_game(self):

    # 主循环事件监听与渲染
    while True:
        # 设置游戏刷新帧率
        pygame.time.Clock().tick(self.game_fps)

        # 绘制地图
        self.game_screen.fill(BLACK)
        self.draw_map()

        # 事件处理
        self._event_handle()

        pygame.display.flip()

移动逻辑其实都是相似的,这里以向上移动进行讲解下

  1. 玩家上方是箱子,箱子上方是空白或目的地

    1. 角色上移 map_list[i - 1][j] = PLAYER_FLAG
    2. 如果当前玩家与目的地重合要还原目的地,不是则是置为空白就行
    3. 箱子上移 map_list[i - 2][j] = BOX_FLAG
  2. 玩家上方是空白

    1. 玩家与空白交换位置
    2. 玩家与目的地重合处理
  3. 玩家上方是目的地

    1. 让玩家和目的地重合 map_list[i - 1][j] = PLAYER_DEST_FLAG

有一个特殊一点就是玩家和目的地重合要还原目的地

python 复制代码
def handle_player_dest():
    """人和目的地重合处理"""
    if map_list[i][j] == PLAYER_DEST_FLAG:
        # 当前位置是人和目的地重合处理
        map_list[i][j] = DEST_FLAG  # 把原来角色位置改成目的地
    else:
        map_list[i][j] = EMPTY_FLAG  # 把原来角色位置改成空白

但还是有点小问题就是箱子和目的重合判断没有做,发现上下左右都要做重合处理。看下统一处理下

python 复制代码
def _player_move_event_handle(self, direction: MoveDirection):
    """
    玩家上下左右移动事件处理
    Args:
        direction: 移动的方向

    Returns:

    """

    # 记录上下左右待判断的位置
    # i,j 玩家原来位置 上下 m,k  左右 n,v
    i, j = self.player_pos
    m, n = self.player_pos
    k, v = self.player_pos
    map_list = GAME_MAP[self.game_level]

    # 根据不同的移动方向确定判定条件
    if direction == MoveDirection.UP:  # 向上 (i - 1, j)、(i - 2, j)
        m = i - 1
        k = i - 2
    elif direction == MoveDirection.DOWN:  # 向下 (i + 1, j)、(i + 2, j)
        m = i + 1
        k = i + 2
    elif direction == MoveDirection.LEFT:  # 向左 (i, j - 1)、(i, j - 2)
        n = j - 1
        v = j - 2
    elif direction == MoveDirection.RIGHT:  # 向右 (i, j + 1)、(i, j + 2)
        n = j + 1
        v = j + 2

    def handle_player_dest_coincide():
        """
        角色和目的地重合判断处理
        """
        if map_list[i][j] == PLAYER_DEST_FLAG:
            map_list[i][j] = DEST_FLAG  # 是,把原来角色位置改成目的地
        else:
            map_list[i][j] = EMPTY_FLAG  # 不是,把原来角色位置改成空白

    # 玩家(上下左右)边是箱子或者箱子和目的地重合
    # 且箱子的(上下左右)边只能是空白或者目的地才可向上
    if map_list[m][n] in [BOX_FLAG, BOX_DEST_FLAG] and \
            map_list[k][v] in [EMPTY_FLAG, DEST_FLAG]:

        if map_list[m][n] == BOX_DEST_FLAG:  # 如果移动的位置是箱子与目的地的重合
            map_list[m][n] = PLAYER_DEST_FLAG  # 让角色和目的地重合
        else:
            map_list[m][n] = PLAYER_FLAG  # 角色向上移动改变角色位置

        # 判断当前位置是否是角色和目的地重合
        handle_player_dest_coincide()

        # 判断箱子是否与目的地重合
        if map_list[k][v] == DEST_FLAG:
            map_list[k][v] = BOX_DEST_FLAG  # 标记箱子和目的地重合
        else:
            map_list[k][v] = BOX_FLAG  # 把箱子向上移改变位置

    elif map_list[m][n] == EMPTY_FLAG:  # 判断(上下左右)边是否空白

        map_list[m][n] = PLAYER_FLAG  # 角色向上移动改变角色位置

        # 判断当前位置是否是角色和目的地重合
        handle_player_dest_coincide()

    elif map_list[m][n] == DEST_FLAG:  # 判断(上下左右)边是否是目的地

        map_list[m][n] = PLAYER_DEST_FLAG  # 让角色和目的地重合

        # 判断当前位置是否是角色和目的地重合
        handle_player_dest_coincide()

def _event_handle(self):
    """事件处理"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()

        key_pressed = pygame.key.get_pressed()
        if key_pressed[K_a] or key_pressed[K_LEFT]:
            # 玩家向左移动
            self._player_move_event_handle(direction=MoveDirection.LEFT)
            # self.move_lef()

        elif key_pressed[K_d] or key_pressed[K_RIGHT]:
            # 玩家向右移动
            self._player_move_event_handle(direction=MoveDirection.RIGHT)
            # self.move_right()

        elif key_pressed[K_w] or key_pressed[K_UP]:
            # 玩家向上移动
            self._player_move_event_handle(direction=MoveDirection.UP)
            # self.move_up()

        elif key_pressed[K_s] or key_pressed[K_DOWN]:
            # 玩家上下移动
            self._player_move_event_handle(direction=MoveDirection.DOWN)
            # self.move_down()

这里通用标识移动方向统一处理上下左右逻辑,减少了代码冗余,但阅读没有这么清晰了。

最后就是去除了画格子当背景,而是通过绘制全局的背景图这样更好看些。然后补齐了游戏通关判断。

python 复制代码
def _handle_game_finish(self):
    """
    游戏过关判断处理
    """
    exist_dest = False  # 标明是否存在目的地
    map_list = GAME_MAP[self.game_level]
    for row in map_list:
        if DEST_FLAG in row or PLAYER_DEST_FLAG in row:
            exist_dest = True

    # 不存在目的地游戏通关
    if not exist_dest:
        print('game pass %s' % self.game_level)
        self.game_level = self.game_level + 1
        self.random_game_material()

        if self.game_level > len(GAME_MAP):
            self.game_level = 1
            print("全部游戏关卡已完成")

四、源代码

大家可以下载源码自己调节自己喜欢的游戏素材,以及配置游戏背景音乐。

github.com/HuiDBK/Rabb...

也可以继续扩展游戏功能

  • 重玩、回退策略
  • 游戏移动步数、计分
  • 每关使用时间
相关推荐
暮毅21 分钟前
四、Drf认证组件
python·django·drf
DanCheng-studio41 分钟前
毕设 基于大数据情感分析的网络舆情分析系统(源码+论文)
python·毕业设计·毕设
DanCheng-studio43 分钟前
大数据 机器学习毕业设计任务书帮助
python·毕业设计·毕设
985小水博一枚呀1 小时前
【深度学习基础模型】稀疏自编码器 (Sparse Autoencoders, SAE)详细理解并附实现代码。
人工智能·python·深度学习·学习·sae·autoencoder
爱写代码的小朋友1 小时前
Python 中的高阶函数与闭包
开发语言·python
子墨7772 小时前
yield:生成器 ----------------
python
sheng12345678rui2 小时前
电脑提示d3dcompiler_47.dll缺失怎么修复,仔细介绍dll的解决方法
游戏·3d·电脑·dll文件·dll修复工具·1024程序员节
为啥不能修改昵称啊2 小时前
python的extend和append
开发语言·python
Bonne journée2 小时前
python调用父类同名成员
开发语言·python·php
凡人的AI工具箱2 小时前
15分钟学 Python 第38天 :Python 爬虫入门(四)
开发语言·人工智能·后端·爬虫·python