基于Python+Kivy的消灭星星小游戏

    写在前: 本小游戏只实现了消灭星星的基本功能,包括消除星星、下落、分数更新,以及两个功能按钮:重新开始和炸弹。

游戏展示:

 界面设计结构图:

主要函数说明:

        初始化函数(init_stars)

        按钮点击函数(on_button_press)

        消灭星星函数(eliminate_stars)

        移动方块函数(move_blocks_down)


代码实现

1.初始化界面

a.设置界面背景颜色以及常量ROWS,COLS

Window.size = 500,650
Window.clearcolor=get_color_from_hex('#00023A') #设置背景颜色
ROWS = 10
COLS = 10

b.定义Scene类,用来设计界面。

定义初始化函数

class Scene(BoxLayout):
    def __init__(self): #初始化变量
        super().__init__()
        self.image=[
            "",
            './picture/1.png',
            './picture/2.png',
            './picture/3.png',
            './picture/4.png',
            './picture/5.png'
        ]
        self.orientation = 'vertical'
        self.spacing=3
        self.matrix_stars = [] #存储按钮
        self.score = 0  # 计分
        self.high_score_file = 'E:/high_score.txt' #记录最高分
        if not os.path.exists(self.high_score_file):
            with open(self.high_score_file, 'w') as f:
                f.write('0')
        self.high = self.get_high_score(self.high_score_file)
        self.n = 0  # 计算相连个数
        self.flag = 0  # 没有相同颜色相连的标志
        self.residual_sum = 0  # 剩余星星个数
        self.residual_score = 0  # 剩余星星奖励分数
        self.f = 0  # 标记效果加分是否成功

        self.blabel=False #记录炸弹按钮按下的状态
        self.boom_1=0 #炸弹按钮按下次数
        self.count_old=0 #记录空列数
        self.init_stars() #初始化按钮
        self.init_matrix() #初始化矩阵
        self.dcolor = [[0 for _ in range(COLS)] for _ in range(ROWS)]
        self.matrix0 = [[0 for _ in range(COLS)] for _ in range(ROWS)]

c.初始化界面(init_stars函数)

界面上方放置重新开始按钮、炸弹按钮和分数标签

    def init_stars(self): #初始化stars按钮矩阵
        self.clear_widgets()
        #上方放置功能键和积分
        button_layout = FloatLayout()
        self.add_widget(button_layout)
        # 重新开始
        restart_button = Button(text='重新开始', size_hint=(.2, .6),
                                font_name='msyh.ttf', pos_hint={'x': 0.1, 'y': .2})
        restart_button.bind(on_press=self.restart_game)
        button_layout.add_widget(restart_button)
       

重新开始按钮绑定了restart_button点击事件

#炸弹按钮
        self.boom_button = Button(text='炸弹', size_hint=(.2, .6),background_color=(1,1,1,.5),
                            background_normal="",font_name='msyh.ttf', pos_hint={'x': 0.32, 'y': .2},
                            disabled=True)  # 初始时不能点击按钮
        button_layout.add_widget(self.boom_button)
        self.boom_button.bind(on_press=self.boom_star)
        self.boom_button.bind(on_release=self.boom_touch_up)

炸弹按钮绑定了boom_star点击事件以及boom_touch_up释放事件。

 #分数按钮
        self.score_label = Label(text='Score: {}'.format(self.score), color=(1, 1, 1),
                                 pos_hint={'x': 0.3, 'y': 0},font_size=30, )
        self.add_widget(self.score_label)
        self.highscore_label = Label(size_hint=(1,.2),text='Highest: {}'.format(self.high), color=(1, 1, 1, .5),
                                     pos_hint={'x': 0.3, 'y': .4}, font_size=20)
        self.add_widget(self.highscore_label)
        self.small_label = Label(size_hint=(1,.5), color=(0, 0, 1, 1),
                                 pos_hint={'x': 0, 'y': 2}, font_size=20,
                                 text='{} eliminate, score+ {}'.format(self.n, self.n * self.n))
        self.add_widget(self.small_label)
        #最后游戏结束的提示标签
        self.reward_label = Label(text='{} star remaining, reward {} points'.\
                                  format(self.residual_sum, self.residual_score),
                                  color=(1, 0, 0, 1), pos_hint={'x': 0, 'y': 3}, font_size=30)
        self.add_widget(self.reward_label)

下方添加10*10的按钮矩阵来代表星星,使用self.matrix_stars数组来存储所有按钮

        self.matrix_stars = [] #按钮置空
        # 创建按钮矩阵
        for i in range(ROWS):  # 每行的按钮
            row_layout = BoxLayout(orientation='horizontal', spacing=3)
            self.add_widget(row_layout)
            row_buttons = []  # 记录每行的方块
            for j in range(COLS):  # 每列的按钮
                btn = Button(size=(80, 80), background_down='',background_normal='')
                btn.bind(on_press=self.on_button_press)  # 绑定点击事件
                row_layout.add_widget(btn)  # 行布局中添加按钮
                row_buttons.append(btn)
            self.matrix_stars.append(row_buttons)  # 将行方块添加到矩阵中
        self.update_score() #更新分数

设置星星按钮点击时无图像使用background_down='',以及默认无背景图像background_normal='',每个按钮都绑定on_button_press事件。

d.初始化颜色矩阵(init_matrix函数)

设置每个方块的颜色随机,同时限制每种颜色的星星数量不超过一半

    def init_matrix(self): #初始化颜色矩阵
        # 存储每个按钮的颜色
        self.matrix = [[random.randint(1, len(self.image)-1 )
                        for _ in range(COLS)] for _ in range(ROWS)] #数字0-5
        # 每种颜色的星星数量不超过一半
        max_count = ROWS * COLS // 2
        for _ in range(max_count):
            # 保证每种颜色的星星数量不超过一半
            color = random.randint(1, len(self.image)-1 )
            count = sum(row.count(color) for row in self.matrix) #该颜色在矩阵中的数量
            while count >= max_count:  #若数量超过一半--->重新生成一个颜色
                color = random.randint(1, len(self.image)-1 )
                count = sum(row.count(color) for row in self.matrix)
            # 随机生成星星的位置
            row = random.randint(0, ROWS - 1)
            col = random.randint(0, COLS - 1)
            # 将随机位置的星星颜色设置为指定颜色
            self.matrix[row][col] = color

将每个星星方块的颜色添加到按钮的背景图像中,使用到background_normal属性。

        #更新按钮背景图像
        for i in range(ROWS):
            for j in range(COLS):
                c = self.matrix[i][j]
                self.matrix_stars[i][j].background_normal=self.image[c] #背景图片

e.更新按钮背景颜色作为一个函数,方块后续调用

     def update_matrix(self,dt):#更新按钮颜色
        self.init_stars()
        for i in range(ROWS):
            for j in range(COLS):
                c = self.matrix[i][j]
                self.matrix_stars[i][j].background_normal=self.image[c] #背景图片
                if c==0:  #变透明
                    self.matrix_stars[i][j].background_color=(0,0,0,0)

2.星星按钮点击事件

a.绑定按钮点击事件(on_button_press函数)

如果点击了炸弹:

    def on_button_press(self, instance):
        #如果点击了炸弹
        if self.blabel==True:
            self.boom_1=1  #炸弹按钮按下一次
            for i in range(ROWS):
                for j in range(COLS):
                    if instance == self.matrix_stars[i][j]:
                        for x in range(ROWS):
                            self.matrix[x][j]=0  #消除行
                        for y in range(COLS):
                            self.matrix[i][y]=0 #消除列
                        break
            self.move_blocks_down() #移动按钮,下落
            Clock.schedule_once(self.update_matrix, .5) #更新颜色
            self.blabel=False

设置Clock.schedule_once()是为了在方块下落的动画完成后再进行下一步操作。

如果是正常点击:进行消除星星、移动按钮以及更新矩阵,同时还要判断是否是最后的游戏结束状态

        else:#正常点击
            # 获取被点击按钮的位置
            for i in range(ROWS):
                for j in range(COLS):
                    if instance == self.matrix_stars[i][j]:
                        # 检查并消除星星
                        self.eliminate_stars(i, j)
                        # 移动方块
                        self.move_blocks_down()
                        Clock.schedule_once(self.update_matrix, .5) #0.5秒后再更新,即等动画完成后
                        self.different_stars()
                        return

b.消灭星星函数(eliminate_stars)

使用深度优先遍历点击方块的上下左右位置,找到所有连续的相同颜色的方块,将它们的颜色置为0,即空方块。还需要判断同色方块数是否大于一,如果只有一个方块则不进行消除。

    def eliminate_stars(self, row, col): #消灭星星
        target_color = self.matrix[row][col]
        count = [0]  #记录相同方块的个数
        #dfs深度优先算法寻找连续的相同颜色的星星
        def dfs(r, c):
            if (r < 0 or r >= ROWS or c < 0 or c >= COLS #越界
                    or self.matrix[r][c] != target_color or self.matrix[r][c] == 0):
                return
            count[0] += 1  # 更新列表中的计数器值
            if count[0] > 1: #相同颜色的方块数大于1
                self.matrix[r][c] = 0
            dfs(r + 1, c)
            dfs(r - 1, c)
            dfs(r, c + 1)
            dfs(r, c - 1)
        dfs(row, col)
        if (count[0] - 1) <= 0:
            self.n = 0
        else:
            self.n = count[0] - 1

c.移动方块函数(move_blocks_down)

①下移方块:

即当方块消除后变为0,而上方还有非0的方块,需要将上方的非0方块进行下移,同时制作动画效果,模拟方块下落的过程。算法过程如下图所示:当第j列中间存在0值时,从底向上遍历,找到0值的位置x,再次向上找到非0值的方块,记录下此时的0值方块数n=2,将,x-2的方块移动x位置上,将x-2-1的方块移动到x-1位置上。实际上是两个按钮的交换。

    def move_blocks_down(self): #移动星星
        # 从底部往上遍历每列,将空位上面的方块往下移动
        diss = self.matrix_stars[1][0].y - self.matrix_stars[2][0].y #纵距离
        for j in range(COLS): #遍历每一列
            for i in range(ROWS - 1, -1, -1): #从底向上遍历
                x=i  #此时行标
                if self.matrix[i][j] == 0:
                    n=1  #记录空格
                    self.matrix_stars[i][j].background_color=(1,1,1,0)  #颜色变透明

(这里让变为0的方块变透明,是为了在有色方块下落时能够显示出来) 

如果找到了非空方块,则将上面的所有非空方块移动到下面的空方块位置,由于已经记录了空方块个数,设置动画让按钮移动相应的距离(空方块数*单个方块高度)

                    for k in range(i - 1, -1, -1): #该行向上
                        if self.matrix[k][j] != 0:
                            #改变按钮坐标
                            anim= Animation(y=self.matrix_stars[k][j].y-n*diss, duration=.5)
                            anim.start(self.matrix_stars[k][j])

按钮进行动画下落后,还需要更新self.matrix_stars和self.matrix存储的值,实现真正的交换

                            #交换按钮
                            self.matrix_stars[k][j],self.matrix_stars[k+n][j]=self.matrix_stars[k+n][j],self.matrix_stars[k][j]
                            # 交换颜色矩阵
                            self.matrix[k+n][j] = self.matrix[k][j]
                            self.matrix[k][j] = 0

如果没有找到非空方块,证明该位置仍然是空方块,记录空方块数n+1,让改按钮变透明,同时继续向上遍历

                        else:  #=0
                            n+=1
                            self.matrix_stars[k][j].background_color = (1, 1, 1, 0)  # 颜色变透明
                            x -= 1  # 移动的行标向上

②左移方块

当方块中存在某列全为空的情况,就会触发左移。需要先找到空列的位置,再向右继续遍历直到找到非空列,同时记录下空列的个数count,将右边的非空列按顺序移动到左边的空列位置上,注意是整列的移动。算法过程如下图所示:图中有2个空列,即count=2,b为第一个空列的索引,从左往右遍历,找到空列b,将b+count列移至b列位置上,将b+count+1列移动至b+1列位置上,实现了空列的向左补齐。实际上还是按钮的交换。

遍历每列,计算每列的空列数到lie数组中,如果找到全为空的列(空方块数=行数)则计数count+1,需要注意已经移动到右边的空列不再进行统计,所以要用count_old变量记录。

        #从右往左移 整列移动
        count=0  #统计空列数
        lie=[] #每列的空格数
        for j in range(COLS):
            n=0
            for i in range(ROWS):
                if self.matrix[i][j]==0: #统计j列空值
                    n+=1
            lie.append(n)
        for j in range(COLS-self.count_old): #已经移动的列不算
            if lie[j]==ROWS:#整列为空
                count+=1
        self.count_old+=count #已经移动的空列数

从左往右遍历每列,如果找到空列,记该列为j,从上往下遍历每行,遍历j列后面的列b,找到非空列(b+count),将b+count列左移至b列位置上,设置动画,让方块移动相应的距离,同时交换两列的按钮和颜色矩阵。

        diss = self.matrix_stars[1][1].x - self.matrix_stars[1][0].x #横距离
        for j in range(COLS-1):
            #存在空列
            if lie[j]==ROWS: #空格数=行数
                for a in range(ROWS):
                    for b in range(j,COLS-count): #将空列右边的列左移
                        self.matrix_stars[a][b].background_color = (1, 1, 1, 0)  # 颜色变透明
                        #移动
                        anim = Animation(x=self.matrix_stars[a][b+count].x-diss*count, duration=.5)
                        anim.start(self.matrix_stars[a][b+count])
                        # 交换按钮
                        self.matrix_stars[a][b], self.matrix_stars[a][b+count] = self.matrix_stars[a][b+count], \
                            self.matrix_stars[a][b]
                        #交换颜色矩阵
                        self.matrix[a][b],self.matrix[a][b+count]=self.matrix[a][b+count],self.matrix[a][b]
                    # self.matrix[a][COLS-1]=0 #最后一列置为0
                break #找到一次非空列,就将所有列进行了移动,退出循环

d.更新方块颜色函数(update_matrix)

方块完成移动后在相应的矩阵中(self.matrix)已经更新了值,需要根据更新的值重新设置每个按钮的背景图像。首先需要重新初始化界面,再遍历每个按钮进行设置。

    def update_matrix(self,dt):#更新按钮颜色
        self.init_stars()
        for i in range(ROWS):
            for j in range(COLS):
                c = self.matrix[i][j]
                self.matrix_stars[i][j].background_normal=self.image[c] #背景图片
                if c==0:  #变透明
                    self.matrix_stars[i][j].background_color=(0,0,0,0)

e.判断游戏结束函数(different_stars)

游戏结束的判断方法为:界面中没有相连的相同颜色的星星

再次使用dfs函数遍历每个方块查看相连方块的颜色状态

    def different_stars(self):
        for i in range(ROWS): #临时保存
            for j in range(COLS):
                self.matrix0[i][j] = self.matrix[i][j]
        for i in range(ROWS):
            for j in range(COLS):
                target_color = self.matrix[i][j]
                count = [0]  # 使用列表来保存计数器
                def dfs(r, c):
                    if (r < 0 or r >= ROWS or c < 0 or c >= COLS
                            or self.matrix[r][c] != target_color or self.matrix[r][c] == 0):
                        return
                    count[0] += 1  # 更新列表中的计数器值
                    if count[0] > 1:
                        self.matrix[r][c] = 0
                    dfs(r + 1, c)
                    dfs(r - 1, c)
                    dfs(r, c + 1)
                    dfs(r, c - 1)
                dfs(i, j)

将方块的颜色状态改为0和1,即如果没有相连的同色方块,则状态为1,否则为0

                if (count[0] - 1) <= 0:
                    count[0] = 0
                else:
                    count[0] = count[0] - 1
                if count[0] == 0:
                    self.dcolor[i][j] = 1
                else:
                    self.dcolor[i][j] = 0

统计所有方块的颜色状态和,如果总数为100,即所有方块都没有相连的颜色,代表游戏结束。

        sum = 0
        for i in range(ROWS):
            for j in range(COLS):
                sum += self.dcolor[i][j]
        if sum == COLS * ROWS:
            self.flag = 1
        else:
            self.flag = 0
        for i in range(ROWS):
            for j in range(COLS):
                self.matrix[i][j] = self.matrix0[i][j]
        self.highscore_label.text = 'Highest: {}'.format(self.high)

3.更新分数

当每次点击了方块进行消除后,都会有相应的得分,需要加入到总分标签中,计算公式为消灭星星的方块数n的平方,这样每次消灭的方块数越多,就可以产生越高的得分。

①定义更新得分函数update_score

    def update_score(self):  # 更新计分
        # 分数标签
        self.score += self.n * self.n
        self.score_label.text = 'Score: {}'.format(self.score)
        self.high = self.get_high_score(self.high_score_file)
        self.reward_label.text = ''

首先记录每次消除的分数到small_label标签中

         # 小分标签
        if self.n == 0:
            self.small_label.text = ''
        else:
            self.small_label.text = '{} eliminate, score+{}'.format(self.n, self.n * self.n)

如果已经没有相同颜色的相连方块,则代表游戏结束,需要更新游戏结束的状态,同时在界面中显示游戏结束的提示词,记录到reward_label中

         # 历史分数标签
            if self.flag == 1:
                self.residual_stars()
                if self.f == 1:
                    self.score += self.residual_score
                    self.reward_label.text = '{} star remaining, reward {} points'.format(self.residual_sum,
                                                                                          self.residual_score)
                    self.f += 1
                else:
                    self.f += 1

如果此次的游戏总分大于历史最高分,则需要更新最高分标签highscore_label

                if self.score > self.high:
                    self.update_high_score(self.score, self.high_score_file)
                    self.high = self.get_high_score(self.high_score_file)
                    self.highscore_label.text = 'Highest: {}'.format(self.high)
                else:
                    self.highscore_label.text = 'Highest: {}'.format(self.high)
            elif self.flag == 0:
                self.highscore_label.text = 'Highest: {}'.format(self.high)

同时,在更新分数时,要判断总分是否超过200,若超过200分即可开启炸弹按钮

        # 炸弹按钮
            if self.score > 200 and self.boom_1 == 0 and self.flag == 0:  # 大于200分且没按过按钮且不是最后状态可以开启炸弹按钮
                self.boom_button.disabled = False

②更新游戏状态函数(residual_stars),主要是用来记录最后的剩余星星数,如果剩余数小于10,则可以进行加分,否则不加分。

 def residual_stars(self): #剩余星星加分
        self.f+=1
        for i in range(ROWS):
            for j in range(COLS):
                if self.matrix[i][j]!=0:
                    self.residual_sum+=1

        if  self.residual_sum<10:
            self.residual_score=(10-self.residual_sum)*10
        else:
            self.residual_score=0

③读取最高分函数(get_high_score)

读取文件中的最高分

    def get_high_score(self, high_score_file): # 读取最高分
        with open(high_score_file, 'r') as f:
            return int(f.read())

④更新最高分函数(update_high_score)

向最高分文件中写入当前最高分

    def update_high_score(self, score, high_score_file): #更新最高分
        with open(high_score_file, 'w') as f:
            f.write(str(score))

4.功能按钮

①重新开始游戏(restart_game函数)

重玩按钮绑定了重新开始函数,需要将所有变量置为0,重新初始化界面

    def restart_game(self,instance):#重新开始
        self.score = 0
        self.residual_score = 0
        self.residual_sum = 0
        self.f = 0
        self.n=0
        self.flag = 0
        self.high = self.get_high_score(self.high_score_file)
        self.count_old = 0  # 记录空列数
        self.small_label.text = ''
        self.reward_label.text = ''
        self.boom_button.disabled = True  # 按钮不可按
        self.boom_1 = 0
        self.init_stars()  # 重绘画布
        self.init_matrix()

②炸弹功能

首先设置了炸弹按钮不可点击,需要达到一定的条件才可开启(在init_star函数中已经设置)。

炸弹按钮绑定事件相应函数(boom_star函数),将此时的blabel设置为True,表示开启了炸弹功能

    def boom_star(self,instance): #炸弹功能
        self.blabel=True

炸弹按钮绑定释放按钮事件(boom_touch_up函数),将炸弹按钮设置为红色,提示玩家此时点击了炸弹按钮

    def boom_touch_up(self,instance):#点击后变换按钮颜色
        instance.background_color = (1, 0, 0, 1)  # 设置为红色

在星星按钮点击事件中添加炸弹功能:如果点击了炸弹按钮,则消除点击按钮的同行同列按钮,接着移动方块和更新矩阵。(在on_press函数中已经设置)

5.App类调用

class StarApp(App):
    def build(self):
        return Scene()

StarApp().run()

程序运行结果

①主界面

②消灭星星

点击相同颜色的方块可进行消灭星星,同时会显示得分,星星可下移和左移:

 

③炸弹按钮

当分数大于200分,炸弹按钮会开启

点击后按钮变为红色,再点击星星方块可消除同行同列:

④游戏结束

当界面中没有相连的同色方块时,游戏结束,上方显示剩余方块以及奖励分数:

⑤重新开始

游戏结束或游戏中途,都能点击重新开始游戏开启新一局的游戏,星星方块重新随机分布,分数会重新清零:


总结

        该小游戏是移动平台设计与开发课程的期末项目,历时几个周完成,时间比较紧张,所以还有很多地方没有完善,除了炸弹按钮,还可以设计出其他的辅助性道具来增加游戏的趣味性;另外一轮游戏结束后界面只会进行提示,玩家必须手动点击重新开始才能开始新的游戏,可以设计不同的关卡来提升游戏的难度。

        事实上,在游戏测试中左移方块有问题T_T,当空的列是间隔的而不是连续时(使用炸弹会出现此问题)没办法正确左移,还需要改进。另外使用dfs算法遍历寻找相同颜色的方块时间复杂度较大,可以考虑使用bfs算法。

        最后,由于Kivy打包过程中遇到了一些问题,并没有成功打包成手机程序,会出现闪退问题,可能是版本不兼容问题。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值