Pythonでブロック崩しを作った

2024/12/08


ブロック崩しプレイ画面
ゲーム画面

画像みたいなブロック崩しをPythonで作りました。
ソースコードはこのページで公開しています。

このプログラムによっていかなる損害が発生した場合でも、当方は一切責任を取りません。収益目的のプレイ動画配信やソースコードの改造などをする場合でも、当方の許諾やクレジットへの表記などは不要です。ただ、動画を公開したことや、面白い特殊ブロックや新マップができた場合は、教えていただけると喜びます。

以上のことをご承知おき頂いた上で、皆さんも自己責任の範囲でご自由にお楽しみください。なおソースコードの実行にはPythonだけでなく、Pygameなど各種ライブラリーのダウンロードが必要になります。


目次

ゲームの説明

このゲームは煮詰まったときの気分転換で遊ぶことを目的として作成しました。なので、いくらボールを落としてもゲームオーバーにならないどころか、大したペナルティもないなどゲーム性は低くなっています。また、その場しのぎの衝突判定を放置しているためにボールがブロックの角に当たった時の挙動が変である、など気ままに遊ぶ分にはあまり気にならないのでそのままにしているアラも多少あります。そのあたりの雑さが気になる方はぜひ、ソースコードを改造して自分の満足の行くブロック崩しを作ってみてください。

ブロック崩しと呼んでいるので、このゲームは当然ボールを当てるとブロックが消えます。ただし、普通にボールが当たると消えるブロックに加えて、特殊な挙動をするブロックも導入しています。以下が、このゲームで登場するブロックの紹介です。なお、表中のタイプ番号は、ソースコードやマップのcsvファイル(後述)でのそのブロックの呼出し番号です。


ブロックのタイプ
タイプ番号名前説明
0無色空白元から何もない場所や、ブロックが消えた場所
1通常ブロック1回の衝突で素直に消える普通のブロック
2硬化ブロック2回衝突しないと消えないブロック
3爆発ブロックボールが衝突すると縦横斜めに隣接するブロックも一緒に消えるブロック
4お邪魔ブロックボールが衝突すると、一定時間ボールが見えなくなるブロック
5浮動ブロック横方向に動くブロック

上記のタイプ番号を使って表現したマップをcsvファイルとして保存すると、そのマップでプレイすることができます。
ただしマップのcsvファイルはソースコードと同じフォルダ内に置いてください。またマップのcsvファイルのファイル名は「map_*.csv」(アスタリスクは数字)にしてください。アスタリスク部分の数字が、ゲームの面数に対応します。例えばmap_3.csvに保存したマップは、3面で登場します。 ファイルが見つからない場合はデフォルトのマップでプレイすることになります。

例えばマップのcsvファイル「map_1.csv」に下のような内容が保存されていた場合、

    
0,0,0,0,0,2,0,0
0,0,0,1,3,1,0,0
0,0,1,3,2,4,0,0
0,0,0,1,5,5,5,1        
    

画像のようなマップがゲーム上で展開されます。


ブロック崩しマップ例
表示されるマップ

マップのcsvファイルのダウンロードリンクは後述します。ダウンロードしたマップのcsvファイルを、ソースコードと同じフォルダ内に保存しておくと、そのマップで遊ぶことができます。よければ自作マップ作製の参考にしてみてください。


遊び方

プログラムを実行すると、「PRESS TO START」が画面に表示されます。そこで右クリックするとゲームが始まります。
マウスの操作で、バーを左右に動かすことができます。
全てのブロックを消すと、クリア画面に移行し、クリア時間とボールを落とした回数が表示されます。ここで右クリックすると、最初の「PRESS TO START」の画面に戻ります。ここで右クリックすればもう一度ゲームが始まります。
ゲーム中は、閉じるボタンもしくはエスケープキーを押すと、いかなる場合でもゲームを終わらせることができます。


ダウンロードリンク

上のリンクからゲームのソースコードを保存した「block.py」という名前のpyファイルと、マップを保存した「map_1.csv」という名前のcsvファイルをダウンロードできます。map_1.csvをダウンロードしなくてもソースコードは実行することができます。
ダウンロードしたくないという方もいるかと思いますので、block.pyの内容は以下に記載しています。記載した内容をそのままコピペすれば同じように実行することができるはずです。


ソースコード

ブロック崩しのソースコードです。そのままコピペして実行すれば遊べます。ただし、Pythonの他にPygameなどのライブラリをインストールしてください。




        
import pygame
import math
from pygame.locals import *
import numpy as np
import random
import os

#サイズ設定
screen_size = np.array([560,600])
block_size = np.array([60,15])
ball_size = 10

#初期化
pygame.init()
clock = pygame.time.Clock()

#画面設定
surface = pygame.display.set_mode(screen_size)

#ブロッククラス
class Block():
    
    def __init__(self, loc, typ):
        #ブロックの色
        self.block_clr_list = [[0,0,0],[0,255,0],[255,140,0],[255,0,0],[135,17,238],[120,250,255],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[150,150,150]]
        #ブロックの形状、位置の設定
        self.block = pygame.Rect(loc,block_size)
        #ブロックのタイプ
        self.typ = typ
        
        self.block_color = self.block_clr_list[self.typ]
        self.vs = np.array([1,0])
    def attack(self, bomb=False):   #衝突時の処理
        if bomb and self.typ != 10:
            self.typ = 0
        elif self.typ == 2:
            self.typ = 1
            self.block_color = [255,255,0]
        elif not (self.typ in [0,10]):#!= 0:
            self.typ = 0

#ボールクラス
class Ball():
    def __init__(self, loc, vs):
        #設定
        self.start = loc
        self.loc = loc
        self.vs = vs
        self.color = [255,255,255]
    def fall(self):
        #落ちた時の処理
        self.color = [255,255,255]
        self.loc = self.start
        vabs = 8
        angle = 90 + random.uniform(15,30) * random.sample([-1,1], k=1)[0]
        self.vs = np.array([math.cos(math.radians(angle)) * vabs, math.sin(math.radians(angle)) * vabs]) 
#バークラス
class Bar():
    def __init__(self, loc, size):
        #設定
        self.loc = np.array(loc)
        self.size = np.array(size)
        self.bar = pygame.Rect(self.loc-(self.size/2), self.size)

#ゲーム実行部分    
def main(path):    
    points = np.array([[ball_size,0],[-ball_size,0],[0,ball_size],[0,-ball_size]])
    hoko = np.array([0,0,1,1])
    
    #ボールの落下方向の設定
    vabs = 8
    angle = 90 + random.uniform(15,30) * random.sample([-1,1], k=1)[0]
    vs = np.array([math.cos(math.radians(angle)) * vabs, math.sin(math.radians(angle)) * vabs])
    #ボールの設定
    ball = (Ball([280,300], vs))
    
    #デフォルトのマップ
    block_map = np.array([[1,1,1,1,1,1,1,1],
                            [1,1,1,1,1,1,1,1],
                            [2,1,1,1,1,1,1,2],
                            [2,1,1,1,1,1,1,2],
                            [1,5,1,1,1,1,1,1],
                            [1,1,1,1,1,1,5,1],
                            [1,1,1,1,1,1,1,1],
                            [1,2,3,1,1,3,2,1],
                            [1,1,4,1,1,4,1,1],
                            [0,0,0,0,0,0,0,0],
                            [1,1,2,1,1,2,1,1],
                            [1,1,1,2,2,1,1,1]])

    #0:無
    #1:普通のブロック
    #2:硬化ブロック(2回当てないと消えない)
    #3:爆弾ブロック(隣接する4ブロックも一緒に消す)
    #4:お邪魔ブロック(消えるとボールがしばらく見えなくなる)
    #5:浮動ブロック(横に動く)
    
    map_name = 'Default'
    
    #マップのcsvファイルが存在した場合
    if os.path.isfile(path):
        with open(path, 'r') as f:
            block_map = np.array([[int(i) for i in row.split(',')] for row in f.readlines()])
            map_name = path
    #マップのサイズ        
    y_lim,x_lim = block_map.shape
    
    #ブロックの設定
    blocks = []
    for x in range(8):
        for y in range(y_lim):
            blocks.append(Block(np.array([40,40]) + block_size * np.array([x,y]), block_map[y,x]))
    
    #バーの設定
    bar = Bar([280,550],[80,10]) 
    
    stop = True
    running = True
    cnt = 0
    blk_cnt = 0
    
    #スタート画面の設定
    font = pygame.font.Font(None, 30)  
    text = font.render("PRESS TO START", True, (255, 255, 255))
    #スタート画面 クリックされるまで繰り返し
    while stop:
        surface.fill((50,50,50))
        surface.blit(text, [190, 80])
        pygame.draw.circle(surface, ball.color, ball.loc, ball_size)
        mouseX, mouseY = pygame.mouse.get_pos()
        bar.bar.left = np.clip(mouseX-bar.size[0]/2,0,screen_size[0]-bar.size[0])
        pygame.draw.rect(surface,"WHITE", bar.bar)
        pygame.display.update()
        for event in pygame.event.get():    # イベント
            if event.type == pygame.MOUSEBUTTONDOWN and event.button == pygame.BUTTON_LEFT:# 左釦押下    
                stop = False
            if (event.type == pygame.QUIT) or (event.type == KEYDOWN and event.key == K_ESCAPE):
                stop = False
                running = False
                pygame.quit()
                return "end"
    
    #落ちた階数のカウント
    fall_num = 0
    st_time = pygame.time.get_ticks()
    
    #ゲーム本編
    while running:
        #画面設定
        surface.fill((50,50,50))
        
        #ボール不可視化解除
        if blk_cnt >= 20:
            ball.color = [255,255,255]
        
        #ボール移動
        if cnt >= 30:
            ball.loc = [np.clip(l+v, 0+ball_size, s-ball_size) for l,v,s in zip(ball.loc, ball.vs, screen_size)]
        
        #ボールの壁での跳ね返り
        ball.vs = np.array([-v if l<=0+ball_size or l>=s-ball_size else v for l,v,s in zip(ball.loc, ball.vs, screen_size)])
        
        #ボールをバーで跳ね返す
        if bar.bar.collidepoint(ball.loc + np.array([0,ball_size])):
            angle = -130 + ball.loc[0] - bar.bar.left            
            ball.vs = np.array([math.cos(math.radians(angle)) * vabs, math.sin(math.radians(angle)) * vabs])
        
        #ボールを落としたとき
        if ball.loc[1] >= screen_size[1] - ball_size:
            fall_num += 1
            ball.fall()
            cnt = 0
        
        #バーの移動
        mouseX, mouseY = pygame.mouse.get_pos()
        bar.bar.left = np.clip(mouseX-bar.size[0]/2,0,screen_size[0]-bar.size[0])
        
        #ブロックの処理
        trig = 0
        new_vs = ball.vs
        for num,block in enumerate(blocks):
            if block.typ == 5:  #浮動ブロックの移動処理
                block.block.left += block.vs[0]
                if not(40 <= block.block.left <= 460) or len(block.block.collidelistall([piece.block for piece in blocks[0:num]+blocks[num+1:] if piece.typ != 0])) > 0:
                    block.vs *= -1
                    block.block.left += block.vs[0]
            
            #ブロックとボールの衝突処理
            coll_cnt = 0
            for p,h in zip(points, hoko):
                if block.block.collidepoint(ball.loc+p) and block.typ != 0:
                    new_vs[h] = -1 * ball.vs[h]
                    coll_cnt = 1
            if coll_cnt == 1:
                if block.typ == 3:  #爆発ブロック衝突時の処理
                    block.block.scale_by_ip(1.5,1.5)
                    idx = block.block.collidelistall([piece.block for piece in blocks])
                    for i in idx:
                        blocks[i].attack(bomb=True)
                elif block.typ == 4:    #お邪魔ブロック衝突時の処理
                    ball.color = [50,50,50]
                    blk_cnt = 0
                block.attack()
            #壊れていないブロックの表示
            if block.typ != 0:
                pygame.draw.rect(surface, block.block_color, block.block)
                pygame.draw.rect(surface, (10,10,10), block.block,1)
                if block.typ != 10:
                    trig = 1
            
        ball.vs = new_vs  #ブロック衝突時のボールの反射

        pygame.draw.circle(surface, ball.color, ball.loc, ball_size)    #ボールの表示
        pygame.draw.rect(surface,"WHITE", bar.bar)                      #バーの表示
        
        #画面更新
        pygame.display.update()
        
        #フレームレート設定
        clock.tick(60)
        cnt += 1
        blk_cnt += 1
        
        if trig==0: #クリア時の処理
            ed_time = pygame.time.get_ticks()
            text = font.render(map_name + " CLEAR!", True, (0, 255, 0))  
            surface.blit(text, [150, 80])
            result = ed_time - st_time
            mini, result = divmod(result,60000)
            sec,mill = divmod(result,1000)
            result_txt = font.render('time   :{:02}:{:02}.{:03}'.format(mini,sec,mill), True, (0, 255, 0))  
            surface.blit(result_txt, [150, 130])
            fall_txt = font.render('fall     :{:02}'.format(fall_num), True, (0, 255, 0))
            surface.blit(fall_txt, [150, 180])
            pygame.display.update()
            return "continue"
        
        #ゲーム終了時処理
        for event in pygame.event.get():
            if (event.type == pygame.QUIT) or (event.type == KEYDOWN and event.key == K_ESCAPE):
                return "end"
                break   
        else:
            continue
        break
    
game = True
level = 1
#ゲームの繰り返し実行
while game:    
    path = 'map_' + str(level) + '.csv' #マップのファイルの相対パス定義
    #ゲーム本編の実行
    isend = main(path)
    
    #ゲームを終わらせるか
    if isend == "end":
        pygame.quit()
        break
    end = True
    
    #クリア後の待機画面
    while end:
        for event in pygame.event.get():
            if (event.type == pygame.QUIT) or (event.type == KEYDOWN and event.key == K_ESCAPE):
                end = False
                game = False
                pygame.quit()
            if (event.type == pygame.MOUSEBUTTONDOWN and event.button == pygame.BUTTON_LEFT):
                end = False

    level += 1



        
    





[ 感想を送る]