改造版: Pythonのtkinterでブロック崩しを作ってみた。

@motty0713 さんの「[Pythonのtkinterでブロック崩しを作ってみた](https://qiita.com/motty0713/items/ed6beb74c02515b35248)」を拝見したところ、いくつかの点が気になってしまいました。

* グローバル変数を使用
* クラス変数とインスタンス変数を混同？
* 関数とメソッドを混同？
* 責務が重い

そこで、以下の点を考慮して改造させていただきました。

* グローバル変数をなくす
* クラス変数ではなくデフォルト引数にする
* クラス変数はインスタンス共有値のみ
* 責務(クラス)分割・委譲
* 処理(メソッド)分割・委譲
* ブロックは列ごとなどに色や点数を変えられるようにする
* パドルが複数になっても対応できる
* ボールが複数になっても対応できる

以下にコードを示します。
~~本当なら、モデル(データ)とビュー(表示)にクラスを分けるべきですが、一体型になってます。~~
Model-View一体型とMVC(Model-View-Controller)分離型の2種類を作りました。
更に、クリーンアーキテクチャ版へのリンク追記しました（プログラミング言語はJavaですが）。
なにか参考になることがあれば幸いです。

## ソースコード

### Model-View一体型

```py
import tkinter as tk


class Shape:

    def __init__(self, x, y, width, height, center=False):
        if center:
            x -= width // 2
            y -= height // 2
        self.x1 = x
        self.y1 = y
        self.x2 = x + width
        self.y2 = y + height

    def intersect(self, target):
        hit_x = max(self.x1, target.x1) <= min(self.x2, target.x2)
        hit_y = max(self.y1, target.y1) <= min(self.y2, target.y2)
        return hit_x and hit_y


class Paddle(Shape):

    def __init__(self, x, y, width=45, height=8, speed=6, color="blue"):
        super().__init__(x, y, width, height, center=True)
        self.speed = speed
        self.color = color
        self.name = "paddle"

    def right(self, event):
        self.x1 += self.speed
        self.x2 += self.speed

    def left(self, event):
        self.x1 -= self.speed
        self.x2 -= self.speed

    def limit(self, area):
        adjust = (max(self.x1, area.x1) - self.x1 or
                  min(self.x2, area.x2) - self.x2)
        self.x1 += adjust
        self.x2 += adjust

    def move(self):
        pass

    def draw(self, canvas):
        canvas.delete(self.name)
        canvas.create_rectangle(self.x1, self.y1, self.x2, self.y2,
                                fill=self.color, tag=self.name)

    def delete(self, canvas):
        canvas.delete(self.name)


class Ball(Shape):

    def __init__(self, x, y, size=10, dx=2, dy=2, color="red"):
        super().__init__(x, y, size, size, center=True)
        self.dx = dx
        self.dy = dy
        self.color = color
        self.name = "ball"

    def move(self):
        self.x1 += self.dx
        self.y1 += self.dy
        self.x2 += self.dx
        self.y2 += self.dy

    def limit(self, area):
        if self.x1 <= area.x1 or area.x2 <= self.x2:
            self.dx *= -1
        if self.y1 <= area.y1 or area.y2 <= self.y2:
            self.dy *= -1

    def bound(self, target):
        if not self.intersect(target):
            return False
        center_x = (self.x1 + self.x2) // 2
        center_y = (self.y1 + self.y2) // 2
        if (self.dx > 0 and center_x <= target.x1 or
            self.dx < 0 and target.x2 <= center_x):
                self.dx *= -1
        if (self.dy > 0 and center_y <= target.y1 or
            self.dy < 0 and target.y2 <= center_y):
                self.dy *= -1
        return True

    def draw(self, canvas):
        canvas.delete(self.name)
        canvas.create_oval(self.x1, self.y1, self.x2, self.y2,
                           fill=self.color, tag=self.name)

    def delete(self, canvas):
        canvas.delete(self.name)


class Block(Shape):

    def __init__(self, x, y, width, height, gap_x=0, gap_y=0, center=False,
                 color="orange", point=1):
        super().__init__(x + gap_x, y + gap_y,
                         width - gap_x * 2, height - gap_y * 2, center=center)
        self.point = point
        self.color = color
        self.name = f"block{x}.{y}"
        self.exists = True

    def break_and_bound(self, target):
        if self.exists and target.bound(self):
            self.exists = False
            return self.point
        else:
            return 0

    def is_broken(self):
        return not self.exists

    def draw(self, canvas):
        canvas.delete(self.name)
        if self.exists:
            canvas.create_rectangle(self.x1, self.y1, self.x2, self.y2,
                                    fill=self.color, tag=self.name)


class BlockRow:
    def __init__(self, color, point):
        self.color = color
        self.point = point


class Blocks:
    #ROWS = [BlockRow("orange", 1)] * 3
    ROWS = BlockRow("cyan", 10), BlockRow("yellow", 20), BlockRow("orange", 30)

    def __init__(self, x, y, width, height, columns=12, rows=None):
        rows = (rows or self.ROWS)[::-1]
        w = width // columns
        h = height // len(rows)
        self.blocks = [Block(x + dx, y + dy, w, h, gap_x=6, gap_y=12,
                             color=row.color, point=row.point)
                       for dy, row in zip(range(0, h * len(rows) + 1, h), rows)
                       for dx in range(0, w * columns + 1, w)]

    def break_and_bound(self, target):
        return sum(block.break_and_bound(target) for block in self.blocks)

    def are_wiped(self):
        return all(block.is_broken() for block in self.blocks)

    def draw(self, canvas):
        for block in self.blocks:
            block.draw(canvas)


class Wall(Shape):
    CATCH_LINES = 3

    def __init__(self, x, y, width, height):
        super().__init__(x, y, width, height)
        self.catch_line = self.y2 - self.CATCH_LINES

    def catch(self, target):
        return target.y2 >= self.catch_line


class Score:
    FONT = ('FixedSys', 16)

    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.score = 0
        self.name = "score"

    def add(self, point):
        self.score += point

    def draw(self, canvas):
        canvas.delete(self.name)
        canvas.create_text(self.x, self.y, text=f"Score = {self.score}",
                           font=self.FONT, tag=self.name)


class Breakout:
    TICK = 20  # 更新間隔
    FONT = ('FixedSys', 40)

    def __init__(self, width, height):
        self.center = (width // 2, height // 2)
        self.root = tk.Tk()
        self.root.title("ブロック崩し")
        self.root.minsize(width, height)
        self.root.maxsize(width, height)
        self.canvas = tk.Canvas(self.root, width=width, height=height)
        self.paddle = Paddle(width // 2, height - 30)
        self.ball = Ball(width // 3, height * 2 // 3)
        self.blocks = Blocks(0, 40, width, 120)
        self.wall = Wall(0, 0, width, height)
        self.score = Score(width - 70, 20)
        self.keybind()
        self.draw()

    def keybind(self):
        self.root.bind("q", self.quit)
        self.root.bind("<Right>", self.paddle.right)
        self.root.bind("<Left>", self.paddle.left)

    def start(self):
        self.playing = True
        self.play()
        try:
            self.root.mainloop()
        except KeyboardInterrupt:
            self.quit()

    def end(self, message):
        self.paddle.delete(self.canvas)
        self.ball.delete(self.canvas)
        self.canvas.create_text(*self.center, text=message, font=self.FONT)
        self.canvas.pack()
        self.playing = False 

    def quit(self, *args):
        self.root.quit()

    def play(self):
        try:
            if self.playing:
                self.operate()
                self.draw()
                self.root.after(self.TICK, self.play)
            else:
                input("Hit return key to end")
                self.quit()
        except KeyboardInterrupt:
            self.quit()

    def operate(self):
        self.paddle.move()
        self.paddle.limit(self.wall)
        self.ball.move()
        self.ball.limit(self.wall)
        self.ball.bound(self.paddle)
        point = self.blocks.break_and_bound(self.ball)
        self.score.add(point)
        self.over()
        self.clear()

    def draw(self):
        self.ball.draw(self.canvas)
        self.paddle.draw(self.canvas)
        self.blocks.draw(self.canvas)
        self.score.draw(self.canvas)
        self.canvas.pack()

    def over(self):
        if self.wall.catch(self.ball):
            self.end("GAME OVER(T_T)")

    def clear(self):
        if self.blocks.are_wiped():
            self.end("GAME CLEAR(^0^)")


if __name__ == '__main__':
    Breakout(width=600, height=480).start()
```

### MVC(Model-View-Controller)分離型

```py
import tkinter as tk


class Shape:

    def __init__(self, x, y, width, height, center=False):
        if center:
            x -= width // 2
            y -= height // 2
        self.x1 = x
        self.y1 = y
        self.x2 = x + width
        self.y2 = y + height

    def intersect(self, target):
        hit_x = max(self.x1, target.x1) <= min(self.x2, target.x2)
        hit_y = max(self.y1, target.y1) <= min(self.y2, target.y2)
        return hit_x and hit_y


class Paddle(Shape):

    def __init__(self, x, y, width=45, height=8, speed=6, color="blue"):
        super().__init__(x, y, width, height, center=True)
        self.speed = speed
        self.color = color
        self.name = "paddle"

    def right(self, event):
        self.x1 += self.speed
        self.x2 += self.speed

    def left(self, event):
        self.x1 -= self.speed
        self.x2 -= self.speed

    def move(self):
        pass

    def limit(self, area):
        adjust = (max(self.x1, area.x1) - self.x1 or
                  min(self.x2, area.x2) - self.x2)
        self.x1 += adjust
        self.x2 += adjust


class Ball(Shape):

    def __init__(self, x, y, size=10, dx=2, dy=2, color="red"):
        super().__init__(x, y, size, size, center=True)
        self.dx = dx
        self.dy = dy
        self.color = color
        self.name = "ball"

    def move(self):
        self.x1 += self.dx
        self.y1 += self.dy
        self.x2 += self.dx
        self.y2 += self.dy

    def limit(self, area):
        if self.x1 <= area.x1 or area.x2 <= self.x2:
            self.dx *= -1
        if self.y1 <= area.y1 or area.y2 <= self.y2:
            self.dy *= -1

    def bound(self, target):
        if not self.intersect(target):
            return False
        center_x = (self.x1 + self.x2) // 2
        center_y = (self.y1 + self.y2) // 2
        if (self.dx > 0 and center_x <= target.x1 or
            self.dx < 0 and target.x2 <= center_x):
                self.dx *= -1
        if (self.dy > 0 and center_y <= target.y1 or
            self.dy < 0 and target.y2 <= center_y):
                self.dy *= -1
        return True


class Block(Shape):

    def __init__(self, x, y, width, height, gap_x=0, gap_y=0, center=False,
                 color="orange", point=1):
        super().__init__(x + gap_x, y + gap_y,
                         width - gap_x * 2, height - gap_y * 2, center=center)
        self.point = point
        self.color = color
        self.name = f"block{x}.{y}"
        self.exists = True

    def break_and_bound(self, target):
        if self.exists and target.bound(self):
            self.exists = False
            return self.point
        else:
            return 0

    def is_broken(self):
        return not self.exists


class BlockRow:
    def __init__(self, color, point):
        self.color = color
        self.point = point


class Blocks:
    #ROWS = [BlockRow("orange", 1)] * 3
    ROWS = BlockRow("cyan", 10), BlockRow("yellow", 20), BlockRow("orange", 30)

    def __init__(self, x, y, width, height, columns=12, rows=None):
        rows = (rows or self.ROWS)[::-1]
        w = width // columns
        h = height // len(rows)
        self.blocks = [Block(x + dx, y + dy, w, h, gap_x=6, gap_y=12,
                             color=row.color, point=row.point)
                       for dy, row in zip(range(0, h * len(rows) + 1, h), rows)
                       for dx in range(0, w * columns + 1, w)]

    def __iter__(self):
        return iter(self.blocks)

    def break_and_bound(self, target):
        return sum(block.break_and_bound(target) for block in self.blocks)

    def are_wiped(self):
        return all(block.is_broken() for block in self.blocks)


class Wall(Shape):
    CATCH_LINES = 3

    def __init__(self, x, y, width, height):
        super().__init__(x, y, width, height)
        self.catch_line = self.y2 - self.CATCH_LINES

    def catch(self, target):
        return target.y2 >= self.catch_line


class Score:

    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.score = 0
        self.name = "score"

    def __str__(self):
        return str(self.score)

    def add(self, point):
        self.score += point


class BreakoutModel:

    def __init__(self, width, height):
        self.paddle = Paddle(width // 2, height - 30)
        self.ball = Ball(width // 3, height * 2 // 3)
        self.blocks = Blocks(0, 40, width, 120)
        self.wall = Wall(0, 0, width, height)
        self.score = Score(width - 70, 20)
        self.message = None

    def update(self):
        self.paddle.move()
        self.paddle.limit(self.wall)
        self.ball.move()
        self.ball.limit(self.wall)
        self.ball.bound(self.paddle)
        point = self.blocks.break_and_bound(self.ball)
        self.score.add(point)
        self.over()
        self.clear()

    def over(self):
        if self.wall.catch(self.ball):
            self.message = "GAME OVER(T_T)"

    def clear(self):
        if self.blocks.are_wiped():
            self.message = "GAME CLEAR(^0^)"


class BreakoutView:
    SCORE_FONT = ('FixedSys', 16)
    MESSAGE_FONT = ('FixedSys', 40)

    def __init__(self, window, width, height):
        self.canvas = tk.Canvas(window, width=width, height=height)
        self.center = (width // 2, height // 2)
        window.title("ブロック崩し")
        window.minsize(width, height)
        window.maxsize(width, height)
        self.rectangle = self.canvas.create_rectangle
        self.oval = self.canvas.create_oval
        self.text = self.canvas.create_text

    def update(self, model):
        self.ball(model.ball)
        self.paddle(model.paddle)
        self.blocks(model.blocks)
        self.score(model.score)
        self.message(model.message)
        self.canvas.pack()

    def delete(self, model):
        self.canvas.delete(model.name)

    def paddle(self, paddle):
        self.delete(paddle)
        self.rectangle(paddle.x1, paddle.y1, paddle.x2, paddle.y2,
                       fill=paddle.color, tag=paddle.name)

    def ball(self, ball):
        self.delete(ball)
        self.oval(ball.x1, ball.y1, ball.x2, ball.y2,
                  fill=ball.color, tag=ball.name)

    def block(self, block):
        self.delete(block)
        if block.exists:
            self.rectangle(block.x1, block.y1, block.x2, block.y2,
                           fill=block.color, tag=block.name)

    def blocks(self, blocks):
        for block in blocks:
            self.block(block)

    def score(self, score):
        self.delete(score)
        self.text(score.x, score.y, text=f"Score = {score}",
                  font=self.SCORE_FONT, tag=score.name)

    def message(self, message):
        if message:
            self.text(*self.center, text=message, font=self.MESSAGE_FONT)


class Breakout:
    TICK = 20  # 更新間隔

    def __init__(self, width, height):
        self.controller = window = tk.Tk()
        self.model = BreakoutModel(width, height)
        self.view = BreakoutView(window, width, height)
        self.view.update(self.model)
        self.keybind()

    def keybind(self):
        self.controller.bind("q", self.controller.quit)
        self.controller.bind("<Right>", self.model.paddle.right)
        self.controller.bind("<Left>", self.model.paddle.left)

    def start(self):
        self.update()
        try:
            self.controller.mainloop()
        except KeyboardInterrupt:
            self.controller.quit()

    def update(self):
        try:
            if self.model.message:
                input("Hit return key to end")
                self.controller.quit()
            else:
                self.model.update()
                self.view.update(self.model)
                self.controller.after(self.TICK, self.update)
        except KeyboardInterrupt:
            self.controller.quit()


if __name__ == '__main__':
    Breakout(width=600, height=480).start()
```

## さらに

### クリーンアーキテクチャ版

最近は、クリーンアーキテクチャでの設計が話題です。
ソースコードはJavaですが、クリーンアーキテクチャでブロック崩しゲームを作りましたので是非参考にしてみてください。
参考： [クリーンアーキテクチャでブロック崩しゲームを設計・実装 - Qiita](https://qiita.com/shiracamus/items/54b7bf1af018ce829335)