一、前言
中秋佳节又快到了,这是中国传统文化中非常重要的一个节日。在这个团圆的日子里,家人团聚,共享月光、赏悦月色,感受中秋的浪漫与温馨。
在此特别推出一个中秋版的推箱子小游戏 - 《兔子推着月饼上月球》。游戏以可爱的中秋元素为主题,控制小兔子挑战十关推箱子关卡,最终把月饼送到月亮指定位置,完成月球之旅。
通过这个小游戏,希望大家在度过一个温馨欢乐的中秋佳节的同时,也感受到一点古老的中秋文化的魅力。以下就让我们开始这趟奇妙的中秋之旅吧!
提前祝大家中秋快乐!愿这个小游戏能让你感受一点中秋的乐趣与魅力。

二、准备素材
我是在阿里巴巴的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()
        - 
pygame.init() 先初始化好pygame 游戏环境
 - 
RabbitBox() 初始化一些游戏属性
- 关卡
 - 游戏屏幕screen
 - 兔子坐标
 
 - 
pygame 游戏渲染 事件循环
- 
绘制游戏屏幕
self.game_screen.fill(BLACK) - 
绘制游戏地图
self.draw_map() - 
事件处理
 - 
游戏刷新展示
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()
        移动逻辑其实都是相似的,这里以向上移动进行讲解下
- 
玩家上方是箱子,箱子上方是空白或目的地
- 角色上移 
map_list[i - 1][j] = PLAYER_FLAG - 如果当前玩家与目的地重合要还原目的地,不是则是置为空白就行
 - 箱子上移 
map_list[i - 2][j] = BOX_FLAG 
 - 角色上移 
 - 
玩家上方是空白
- 玩家与空白交换位置
 - 玩家与目的地重合处理
 
 - 
玩家上方是目的地
- 让玩家和目的地重合 
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("全部游戏关卡已完成")
        
四、源代码
大家可以下载源码自己调节自己喜欢的游戏素材,以及配置游戏背景音乐。
也可以继续扩展游戏功能
- 重玩、回退策略
 - 游戏移动步数、计分
 - 每关使用时间
 


