ヌルポインター親衛隊

社内でひとりエンジニアやってます。

Python学習6日め(オセロゲームを作ってみる2)

今日も『独学プログラマー』は一旦脇に置いておいて、Python学習5日目で作り出したオセロゲームのクラスを改良します。
試行錯誤を繰り返して、ようやくオブジェクト指向プログラミングの大原則中の大原則が理解できた感。

改良したとこ

まず、クラスの設計を見直しました。

Game  
├ Player 
└ Board - Piece 

前回からのクラス間の関係の変更点は、PlayerとPieceのhas - a関係を無くしただけです。また、Boardは8マス×8マスの64マス分の状態を保持させることにしましたが、この1つ1つがPieceに当たります。
前回までのPieceは、単純なオセロのコマ(裏表が白黒の丸いアレです)を表していましたが、それでは盤面上に何もないときの状態を表しにくいことに気が付きました。そこで、Pieceは盤面の1マスの状態を保持するオブジェクトとし、Board内で64個のPieceインスタンスを生成しています。こうすることによって随分ロジックが簡単になりました。以下が修正後の全体のコードです(裏返す場所の判定は未実装です)。

"""
2019.03.11 設計変更
Piece:
  盤面の状態を表す。・、●、○の3状態。
Player:
  色と手持ち数を持つ。
Board:
  盤面自体の状態を持つ。Pieceとhas - a関係。
Game:
  Player、Boardとhas - a関係。
"""

class Piece:
    states = {".":"・", "black":"●", "white":"○"}

    def __init__(self, x, y):
        self.state = self.states["."] #初期状態では全ての目が"・"
        self.x = x
        self.y = y

    def set_state(self, color):
        self.state = self.states[color]

    def reverse_piece(self):  # 裏返す
        if self.state == self.states["black"]:
            self.state = self.states["white"]
        if self.state == self.states["white"]:
            self.state = self.states["black"]
        else:
            self.state = self.states["."]

    def __str__(self):
        return self.state


class Board:
    pieces = [["" for i in range(8)] for j in range(8)]
    last_puted_rocation = []  # x, y #これ書いてみたけど使えそう。Boardクラスで大丈夫?

    def __init__(self):
        #盤面の生成(Piece64個)
        for x in range(0, 8):
            for y in range(0, 8):
                self.pieces[y][x] = Piece(x, y)

    def __str__(self):
        stage = ""
        for y in range(0, 8):
            for x in range(0, 8):
                stage += self.pieces[y][x].state
            stage += "\n"
        return stage

    def __add__(self, another):
        return self.__str__() + another

    def is_already_put(self, x, y):
        if self.pieces[y-1][x-1].state != "・":
            return True

    def set_piece_to(self, x, y, color):  # pieceを置くときに呼ぶ
        self.pieces[y-1][x-1].set_state(color)

    def calc_black_area(self):
        area = 0
        for y in self.pieces:
            for x in y:
                if x.state == "○":
                    area += 1
        return area

    def calc_white_area(self):
        area = 0
        for y in self.pieces:
            for x in y:
                if x.state == "●":
                    area += 1
        return area

    def same_color_row_rocation(self, y):  # 返り値x座標2つ(ない場合は自身の位置が入る)
        offset = self.last_puted_rocation[0] - 1
        for x in range(0, 8):
            if self.pieces[y][x] == self.pieces[y][x].get_state():
                pass

    def same_color_line_rocation(self):  # 返り値y座標2つ
        pass

    def update(self):  # boad上のpiece色を演算し、更新
        pass
        # 左右、前後、右上がり斜め、左上がり斜めの4パターンのチェック
        #   returnで座標を返す?
        #   チェックと更新は別メソッドにすべきか
        # self.last_puted_locationの場所を中心に、同色にぶち当たる場所までのベクトルを取得?

        # 例えば、Gameが盤面を確認する、ではなく、Boardが(自らの)状態を出力する、とか

    def black_is_win(self):
        if self.calc_black_area() > self.calc_white_area():
            return True
        if self.calc_black_area() < self.calc_white_area():
            return False
        return None  # Noneを返せば引き分け


class Player:
    colors = {"black":"black", "white":"white"}
    def __init__(self, name, color):
        self.piece_has = 32  # オセロのコマの所持数
        self.name = name
        self.color = self.colors[color]

    def put_piece(self):
        self.piece_has -= 1


class Game:
    def __init__(self):
        self.p1 = Player("Player1", "white")
        self.p2 = Player("Player2", "black")
        self.board = Board()

    def finish_game(self):
        if self.board.black_is_win() is not None:
            if self.board.black_is_win():
                print("黒が勝ちです。")
            if not self.board.black_is_win():
                print("白が勝ちです。")
        if self.board.black_is_win() is None:
            print("引き分けです。")
        exit()  # これってメモリ解放してくれるん...?

    def turn(self, player):
        while True:
            p_puts = input("{}の手番です([x y]で座標を指定してください):".format(player.name))
            p_puts = p_puts.strip().split(" ")
            if p_puts[0] == 'q':
                self.finish_game()
            px = int(p_puts[0])
            py = int(p_puts[1])
            if self.board.is_already_put(px, py):
                print("その場所には既にコマが置かれています。")
                continue
            if (px <= 0) or (px >= 9) or (py <= 0) or (py >= 9):
                print("範囲外です。")
                continue

            self.board.set_piece_to(px, py, player.color)
            print(self.board)
            break

    def play_game(self):
        self.board.set_piece_to(4, 4, "black")
        self.board.set_piece_to(5, 5, "black")
        self.board.set_piece_to(4, 5, "white")
        self.board.set_piece_to(5, 4, "white")
        # print(id(self.board.board))
        print(self.board + "\nゲームスタート!\n(qでゲームを中断して終了します)")

        while (self.p1.piece_has != 0) and (self.p1.piece_has != 0):
            self.turn(self.p1)
            self.board.update()

            self.turn(self.p2)
            self.board.update()

        self.finish_game()

if __name__ == "__main__":
    g = Game()
    g.play_game()

わかったこと

昨日、どうしたらうまくクラスが設計できるのかがまるでわかりませんでした。できることが色々ありすぎて、何をどこに配置するか、何に何の属性やメソッドをもたせるべきなのかの判断がつかなかったのです。

試行錯誤の果にわかったことは、持たせる属性(状態)の設計を間違えるな、ということに尽きます。例えば上の例なら、Pieceクラスは最初、「オセロのコマ」をオブジェクトにしたものでしたが、「オセロの盤面の1マス」が正解です。しかし、もしもGameがBoardとPlayerとPieceを持つようなクラスの設計にしたのならば、「オセロのコマ」で上手く実装できるかもしれません。重要なのは、適切でなければならないことです。

上手く動くプログラムは、意図せずとも、結果として限りなくシンプルなコードになると思います。だから、書いていてなんか「モヤっとするな」と思った時は上手い設計ができていないときだと思います。 恐らく、簡単に実装できた!という成功体験がクラス設計のセンスを養うのでしょう。