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を持つようなクラスの設計にしたのならば、「オセロのコマ」で上手く実装できるかもしれません。重要なのは、適切でなければならないことです。
上手く動くプログラムは、意図せずとも、結果として限りなくシンプルなコードになると思います。だから、書いていてなんか「モヤっとするな」と思った時は上手い設計ができていないときだと思います。 恐らく、簡単に実装できた!という成功体験がクラス設計のセンスを養うのでしょう。