Pythonでオセロをつくろう

  • 今回はPythonでオセロのプログラムを作りましょう。
  • 登場するのは、initialize_board関数、print_board関数、is_valid_move関数、place_move関数、valid_moves_list関数、print_winner関数、othello関数の7つの関数です。
  • このうち、is_valid_move関数、place_move関数の二つがオセロの肝となる部分です。

オセロのルール

  1. 一手ずつ交互に石を打ちます
    自分の石で相手の石を挟むことで、相手の石をひっくり返します。

2. 最終的に盤上の石の数が多い方の勝ちです(同数なら引き分け)。

  • 相手の石を一つもひっくり返せない場所に打つことはできません
  • 盤上にまだ打てる場所があるのにパスすることはできません
  • 盤上に一箇所も打てる場所がない場合は自動的にパスになります。
  • 両者とも連続でパスしたら終局となります。

それでは始めましょう!

グローバルな定数

BLACK = "●"
WHITE = "○"
BOARD_SIZE = 8
  • プログラムの初めに定数を定義します。
  • BLACKWHITEはそれぞれ黒石白石を表しています。
  • BOARD_SIZEは盤面のサイズです。8なら8×8サイズということです。46など、他の盤面サイズにも対応できるようにプログラムしていきます。

initialize_board関数

  • initialize_boardは、初期化された盤面を吐き出す関数です。
    初期化とは盤面を対局が始まる前の状態にすることです。
    オセロの場合、まっさらな盤面に、中央に四つの石が白黒交互に置かれている盤面(上の図)が初期化された盤面です。
def initialize_board():
    """boardを作り出して返す関数"""
    # 二次元リストで空の盤面を作る(" " は空きマス)
    board = [[" " for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)]
    # 初期配置(中央の4つ)
    board[BOARD_SIZE//2-1][BOARD_SIZE//2-1], board[BOARD_SIZE//2-1][BOARD_SIZE//2] = WHITE, BLACK  # ○ ●
    board[BOARD_SIZE//2][BOARD_SIZE//2-1], board[BOARD_SIZE//2][BOARD_SIZE//2] = BLACK, WHITE      # ● ○
    return board
  • オセロの盤面は二次元リストを使って表します。
    二次元リストとは、リストの中にリストがあるもののことです。
  • まず、
[" " for _ in range(BOARD_SIZE)]

ではリスト内包表記を使って、" "BOARD_SIZE個格納されたリストを作っています。
具体的にはBOARD_SIZE8ならば

[" ", " ", " ", " ", " ", " ", " ", " "]

このようになります。そして、

board = [[' ' for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)]

これはそんなリストがさらにBOARD_SIZE個あるという意味ですので、boardリストは以下のようになります。

[[" ", " ", " ", " ", " ", " ", " ", " "],
[" ", " ", " ", " ", " ", " ", " ", " "],
[" ", " ", " ", " ", " ", " ", " ", " "],
[" ", " ", " ", " ", " ", " ", " ", " "],
[" ", " ", " ", " ", " ", " ", " ", " "],
[" ", " ", " ", " ", " ", " ", " ", " "],
[" ", " ", " ", " ", " ", " ", " ", " "],
[" ", " ", " ", " ", " ", " ", " ", " "],]
  • 次に、
board[BOARD_SIZE//2-1][BOARD_SIZE//2-1],board[BOARD_SIZE//2-1][BOARD_SIZE//2]=WHITE,BLACK # ○●
board[BOARD_SIZE//2][BOARD_SIZE//2-1], board[BOARD_SIZE//2][BOARD_SIZE//2] = BLACK, WHITE # ●○

このコードでは中央に最初の4つの石を配置しています。
やはりBOARD_SIZE8とすると

board[3][3], board[3][4] = WHITE, BLACK # ○●
board[4][3], board[4][4] = BLACK, WHITE # ●○

ということになりますので、最終的にboardリストは

[[" ", " ", " ", " ", " ", " ", " ", " "],
[" ", " ", " ", " ", " ", " ", " ", " "],
[" ", " ", " ", " ", " ", " ", " ", " "],
[" ", " ", " ", "○", "●", " ", " ", " "],
[" ", " ", " ", "●", "○", " ", " ", " "],
[" ", " ", " ", " ", " ", " ", " ", " "],
[" ", " ", " ", " ", " ", " ", " ", " "],
[" ", " ", " ", " ", " ", " ", " ", " "]]

このように初期化された状態になります。

print_board関数

  • print_boardオセロの盤面を表示する関数です。
def print_board(board):
    """boardを受け取ったらそれを表示する関数"""
    # top1
    print("    " + "   ".join([f"{num}" for num in range(BOARD_SIZE)]))
    # top2
    print("  ┌───" + "───".join(["┬" for _ in range(BOARD_SIZE-1)]) + "───┐")
    # middle
    for row in range(BOARD_SIZE):
        print(f"{row} │ " + " │ ".join([board[row][col] for col in range(BOARD_SIZE)]) + " │")
        if(row != BOARD_SIZE-1):
            print("  ├─" +  "─┼─".join(["─" for _ in range(BOARD_SIZE)]) + "─┤")
        else:
            print("  └─" +  "─┴─".join(["─" for _ in range(BOARD_SIZE)]) + "─┘")
  • オセロ盤の枠の部分と、石(空マス)をjoinメソッドを使ってペタペタ貼り合わせて文字列を作り、それを1行ずつprintしています。

is_valid_move関数

  • is_valid_move関数は、今回のオセロプログラムの二つの山の一つ目です。
  • 盤面(board)と、打ちたい場所(row, col)と、プレイヤの色(player)を渡すと、True/False(打てる/打てない)を返します。
def is_valid_move(board, row, col, player):  # playerは今回打つ側の色、row,colは打てるか調べたい場所
    """boardにplayerが(row,col)の位置に打つのが合法ならTrueを、非合法ならFalseを返す関数"""
    # 8方向を確認する
    DIRECTIONS = [(0, -1), (-1, -1), (-1, 0), (-1, 1), (0, 1), (1, 1), (1, 0), (1, -1)]  # 左,左上,上,右上,右,右下,下,左下の順番
    for (dr, dc) in DIRECTIONS:
        (r, c) = (row + dr, col + dc)
        # 1つ先が盤内かつ相手の石なら。そうでなければ即探索を打ち切り、次の方向へ
        if (0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE) and (board[r][c] != ' ' and board[r][c] != player):
            # もう一つ先のマスに進んで自分の石が一つでも見つかればTrue(合法)
            (r, c) = (r + dr, c + dc)
            while 0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE:
                if board[r][c] == player:  # 自分の石
                    return True
                elif board[r][c] == ' ':  # 空きマスなら探索打ち切り、次の方向へ
                    break
                (r, c) = (r + dr, c + dc)
    return False

処理の流れ

  • まず、オセロにおいてある場所に「打てる」とはどういうことかを考えてみると、
    ・打った場所の上下・左右・斜めの計8方向のうちどれか一方向でも相手の石を裏返せるならその場所に打てる
    ・8方向のうち一方向も相手の石を裏返せないなら打てない
    ということになります。
  • よって、打てるか打てないかを判断するには8方向を調べる必要があります。これは以下のように書きます。
directions = [(0, -1), (-1, -1), (-1, 0), (-1, 1), (0, 1), (1, 1), (1, 0), (1, -1)]
for (dr, dc) in directions:
  • directionsリストの中身はそれぞれ
    左 左上, 上, 右上, 右, 右下, 下, 左下
    という八方向に対応しています。
  • どういうことかというと、例えば(1, 1)というマスを基準としたとき、
    (0, -1)を足したら左のマス(1,0)を意味し、
    (-1, -1)を足したら左上のマス(0,0)を意味し、
    (-1, 0)を足したら上のマス(0,1)を意味し、



    (1, -1)を足したら左下のマス(2,0)を意味する、
    ということです。
  • そういうわけで、このforループは8周するループで、一回一回のループは一つ一つの方向を調べるループである、ということになります。
  • 最初は左を調べるループから始まります。1周目の(dr, dc)(0, -1)だからです。
  • その後、左→左上右上右→…と調べていきます。

具体的な処理(右)

  • 今回は右を調べるループだとして、具体的な処理を見ていきましょう。
  • 手番はとします。
  • まず、すぐ右隣(一つ先のマス)のマスに注目します。一つ先のマスを表すには以下のように書きます。
(r, c) = (row + dr, col + dc)
  • (row, col)というのは今回打てるか調べたい場所で、今回調べたい方向はなので(dr, dc)(0, 1)です。それを足し合わせることで、(r, c)には今回打てるか調べたい場所すぐ右隣のマスが格納されるのです。

調査開始

  • さて、それでは一つ先のマスが何なのかによってどう判断が変わるのか見ていきます。

(1)すぐ隣が自分の石

  • 点線の場所に打ちたいとします。すぐ右隣が黒(自分と同じ色)だったら、さらにその先にどんな石があったとしても、右側の石は裏返せません

(2)すぐ隣が空マス

  • 同様に、すぐ右隣が空マスだったとしても、裏返すことはできません

(3)すぐ隣が相手の石

  • では、すぐ右隣が白(相手の石)だったらどうでしょうか?
  • すぐ右隣が白なら、その先の石次第では、裏返せる可能性があります
  • なお、上の3パターンの他に、右を探索するというときに

このように、すぐ右隣がそもそも存在しない、という場合ももちろん、即、探索を打ち切って次の方向に移ります。

  • 以上の処理は以下のように書くことができます。
if (0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE) and (board[r][c] != ' ' and board[r][c] != player):
  • 「もし、すぐ右隣(r,c)が盤内(0以上BOARD_SIZE未満)であり、かつ、空マスではなく(!= ‘ ‘)、かつ、自分の石でもない(!= player)」
    ということはすなわち
    「もしすぐ右隣が相手の石なら」
    ということです。
  • この条件が満たされなかった場合は今回の方向の探索は終了し、次の方向へと進みます。

すぐ右隣が白だった場合の処理

  • 以下、実際にすぐ右隣が白(相手の石)だったとして、if文内の処理へと進みましょう。
  • 一つ右の白石の、さらに一つ先の、二つ先のマスに調べる対象を移します。これは以下のように書きます。
(r, c) = (r + dr, c + dc)
  • (r, c)はさっきはすぐ右隣のマスを表していましたが、ここでもう一度(dr, dc)を足したことで、もう一つ右、すなわち二つ先のマスを表すようになりました。

(1)二つ先が空きマス

  • さて、その二つ先のマスが、空きマスだった場合はどうでしょう?
  • これは、裏返せません。即、探索をやめて次の方向に移ります。
if board[r][c] == ' ':  # 空きマスなら探索打ち切り、次の方向へ
        break

(2)二つ先が自分の石

  • では、二つ先のマスが黒(自分の石)だったらどうでしょうか。
  • これは、裏返せますよね。この時、まだ右下、下、左下といった未探索の方向が残っていても、少なくとも一方向は間違いなく返せる時点でこの場所には打てると判断して良いので、ここで即Trueを返して関数を終了できます。
elif board[r][c] == player:  # 自分の石なら
    return True

(3)二つ先が相手の石

  • では、二つ先のマスが白(相手の石)だった場合はどうでしょうか。
  • これは、ひっくり返せるかどうかはその先の石次第ですので、とりあえず何もせず次のマスに進みます
(r, c) = (r + dr, c + dc)  # 次のマスに進む

2マス先以降はwhileループで

  • さて、この先は、三つ先のマスも、四つ先のマスも、二つ先のマスの時の処理と同じです。
    空マスが現れたら探索を打ち切って次の方向へと移りますし、
    黒石なら即Trueを返しますし、
    白石なら判断は保留して次のマスに進みます。
    これを、盤面サイズを超えない間は繰り返します。
  • よって、以上の処理は以下のようにwhile文に包んで書くことになります。
while 0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE:
    if board[r][c] == player:  # 自分の石なら
        return True
    elif board[r][c] == ' ':  # 空きマスなら探索打ち切り、次の方向へ
        break
    (r, c) = (r + dr, c + dc)  # 一つ先のマスへ進む
  • 以上が、is_valid_move関数です。ここまでの流れを理解できたら、もう一度、is_valid_move関数全体を見て流れを再確認してみてください。

place_move関数

  • 二つ目の山の、place_move関数です。is_valid_move関数が理解できればきっと理解できるはずです。
  • is_valid_move関数と同様、盤面(board)と、打ちたい場所(row, col)と、プレイヤの色(player)を引数として受け取ります。返すのは着手・裏返し処理を施した盤面です。
def place_move(board, row, col, player):
    """boardに、<自分の石を置く処理>、<挟まれた相手の石をひっくり返す処理>、の二つを施して返す関数"""
    board[row][col] = player  # まずは自分の石を(row,col)に打つ
    directions = [(0, -1), (-1, -1), (-1, 0), (-1, 1), (0, 1), (1, 1), (1, 0), (1, -1)]  # 左,左上,上,右上,右,右下,下,左下の順番
    for (dr, dc) in directions:
        lst = []  # ひっくり返す可能性のある石のリスト
        (r, c) = (row + dr, col + dc)
        while (0<=r<BOARD_SIZE and 0<=c<BOARD_SIZE):
            if board[r][c] != player:  # もし相手の石ならリストに追加
                lst.append((r,c))
            elif board[r][c] == player:  # もし自分の石ならリストの石をひっくり返して、探索を打ち切り次の方向へ
                for lr, lc in lst:
                    board[lr][lc] = player
                break
            else:  # もし空マスなら探索を打ち切り次の方向へ
                break
            (r, c) = (r + dr, c + dc)
    return board

まずはとりあえず打つ

  • まずは、打ちたいところに打つだけ打ちます。
  • この処理は以下のように書きます。(row, col)は打ちたい場所を表しています。
board[row][col] = player

空のリストをつくっておく

  • また、このタイミングで、空のリストを作っておきます。これはすぐ後で登場します。
lst = []  # 「ひっくり返す可能性のある石リスト」

相手の石をひっくり返す

  • 相手の石をひっくり返す処理に移ります。is_valid_moveと同様、8つの方向を、一つ一つ見ていきます。
    directions = [(0, -1), (-1, -1), (-1, 0), (-1, 1), (0, 1), (1, 1), (1, 0), (1, -1)]  # 左,左上,上,右上,右,右下,下,左下の順番
    for (dr, dc) in directions:
  • 今回も、の方向を見ることにします。
  • まずは一つ先のマスに注目します。以下のように書きます。
(r, c) = (row + dr, col + dc)
  • (r, c)すぐ右隣のマスを表しています。
  • さて、それでは一つ先のマスが何なのかによってどう判断が変わるのか見ていきます。

(1)すぐ隣が空マス

  • まず、すぐ右隣が空マスだったら、何もひっくり返せません。即、次の方向へ移ります。

(2)すぐ隣が相手の石

  • すぐ右隣が白(相手の石)だったらどうでしょうか。
  • これは、ひっくり返せる可能性がありますよね。この先のマス次第なので実際にひっくり返すことになるかはわかりませんが、ひとまずループの最初に作っておいた「ひっくり返す可能性のある石リスト」に追加しておきます。
if board[r][c] != player:  # もし相手の石ならリストに追加
    lst.append((r,c))

(3)すぐ隣が自分の石

  • すぐ左隣が自分の石だったらどうでしょうか。
  • これは、ひっくり返せないので、この方向の処理は即終えて次の方向へ、

    …としてもいいのですが、実際には、このような処理になっています。
            if board[r][c] == player:  # もし自分の石ならリストの石をひっくり返して、探索を打ち切り次の方向へ
                for lr, lc in lst:
                    board[lr][lc] = player
                break
  • 何をしているかというと、「ひっくり返す可能性のある石リスト」に入っている石を全て実際にひっくり返すのです。
    今回のように、自分の石がすぐ隣にあった場合は、リストは空ですから、何もしないのと変わりません。
  • ですが、例えば、
  • このように、1つ先が白2つ先も白3つ先でようやく自分の石が登場した、という場合には、それまでの二回のループの間にlstリストには[(3,4), (3,5)]のように相手の石が格納されているはずです。
  • こういう処理にも対応できるように、自分の石があった場合は、リストの要素数に関係なく、リストの中身をとりあえず自分の色にする、という処理を行うのです。
  • さて、チェック作業が一通り済んだので、次のマスへと進みます。
(r, c) = (r + dr, c + dc)

2マス先以降はwhileループで

  • さて、この先は、三つ先のマスも、四つ先のマスも、二つ先のマスの時の処理と同じです。
    空マスが現れたら探索を打ち切って次の方向へと移りますし、
    黒石ならリスト内の相手の石を自分の色に変えてbreakし、
    白石ならとりあえずlstに石を追加します。
    そして一つ次のマスへ進みます。
    これを、盤面サイズを超えない間は繰り返します。
  • よって、以上の処理は以下のようにwhile文に包んで書くことになります。
while (0<=r<BOARD_SIZE and 0<=c<BOARD_SIZE):
    if board[r][c] != player:  # もし相手の石ならリストに追加
        lst.append((r,c))
    elif board[r][c] == player:  # もし自分の石ならリストの石をひっくり返して、探索を打ち切り次の方向へ
        for lr, lc in lst:
            board[lr][lc] = player
        break
    else:  # もし空マスなら探索を打ち切り次の方向へ
        break
    (r, c) = (r + dr, c + dc)
  • 以上がplace_move関数です。

valid_moves_list関数

  • valid_moves_list関数は、boardplayerを受け取ると、playerが受け取ったboardで打てる場所をリストで返してくれます。
def valid_moves_list(board, player):
    """playerがboardに打てる合法手のリストを返す関数"""
    lst = []
    for row in range(BOARD_SIZE):
        for col in range(BOARD_SIZE):
            if board[row][col] == ' ' and is_valid_move(board, row, col, player):
                lst.append((row, col))
    return lst
  • boardの左上隅から右下隅まで、上に登場したis_valid_move関数を利用してそこが合法かどうか一箇所一箇所調べて、最後に合法手をまとめたリストをreturnするという簡単な関数です。

print_winner関数

  • print_winner関数は、終局した盤面を受け取ると、石数を計算し、それに基づいて勝者を表示する関数です。オセロの勝敗は単に盤上の石が多い方が勝ちです。
def print_winner(board):
    """終局局面を受け取ると①石数の計算、②勝者表示、をする関数"""
    black_count = sum(row.count(BLACK) for row in board)
    white_count = sum(row.count(WHITE) for row in board)
    print(f"{BLACK}{black_count} - {WHITE}{white_count}")
    if black_count>white_count:
        print("黒の勝ち")
    elif white_count>black_count:
        print("白の勝ち")
    else:
        print("引き分け")
  • まず、boardは二次元リストなので、
black_count = sum(row.count(BLACK) for row in board)
white_count = sum(row.count(WHITE) for row in board)

ではリスト内のリストを一つ一つ見ていき、その中に含まれる黒石と白石それぞれをcount関数で数えたのち、最後にsum関数で全部足し合わせています。

そしてそれを

print(f"{BLACK}{black_count} - {WHITE}{white_count}")

で表示したのち、

    if black_count>white_count:
        print("黒の勝ち")
    elif white_count>black_count:
        print("白の勝ち")
    else:
        print("引き分け")

でその多い/少ないを比較し、勝者を表示します。

othello関数

  • othello関数は、オセロを遊ぶための関数です。
  • 内部的に盤面の状態プレイヤのターンを管理することはもちろん、盤面を表示したりユーザの入力を受け取ったりなど、ユーザとコンピュータの橋渡しをする役割も果たします。
  • これまでのマルバツゲームの処理部分五目並べプログラムgomoku関数とよく似ています。
def othello():
    """オセロを遊ぶための関数"""
    # 盤面とプレイヤの初期化
    board = init_board()
    player = BLACK
    # 初期盤面の表示
    print_board(board)
    last_player_pass = False  # 2回連続パス検知用フラグ
    (row, col) = (BOARD_SIZE, BOARD_SIZE)  # rowとcolを用意しておく。最初に格納しておく数値はダミー
    # ループ開始
    while True:
        # 合法手がなければ自動的にパスになる。2回連続でパスなら自動的に終局になる
        if(not valid_moves_list(board, player)):  # 今回合法手がないなら
            if last_player_pass:  # 前回も合法手がなかったら2連続パスなので終局
                break
            else:  # 今回が初めてのパス
                print(f"次は{player}の番です")
                print(f"row col: パス")
                print_board(board)
                player = WHITE if player == BLACK else BLACK
                last_player_pass = True
                continue
        else:  # 今回合法手があったなら
            last_player_pass = False
        print(f"合法手リスト:{valid_moves_list(board, player)}")
        print(f"次は{player}の番です")
        clear_output(wait=True)
        # 入力部分
        while True:
            try:
                (row, col) = map(int, input("row col:").split())
                if (0<=row<BOARD_SIZE and 0<=col<BOARD_SIZE)and((row, col) in valid_moves_list(board, player)):
                    break
                else:
                    print(f"合法手を選んでください")
            except ValueError:
                print("整数値を入力してください")
            print_board(board)
            clear_output(wait=True)
        # 入力を盤面に適用
        board = place_move(board, row, col, player)
        print_board(board)
        # プレイヤー交代
        player = WHITE if player == BLACK else BLACK
    print_winner(board)

準備部分

  • まず、
 board = init_board()
 player = BLACK

では、上に定義したinit_board関数も活用して盤面およびプレイヤの初期化をしています。オセロは黒から始まるのでplayerBLACKに設定されています。

  • その後ろの
    last_player_pass = False 

は、前回のプレイヤの手がパスだったかどうかを記録しておくための変数(フラグ)です。

  • 1回目のパスの時にこれをTrueにしておくことで、2回目のパスの時に2回目であることに気づくことができます。なお、パス以外の手を選択したときはFalseに戻しておきます。
 (row, col) = (BOARD_SIZE, BOARD_SIZE)

は、後で使用するために、rowとcolという変数を用意しています。

whileループの開始

  • 準備が完了したので、whileループが開始します。
    while True:
  • 最初の処理は以下です。
        if(not valid_moves_list(board, player)):  # 今回合法手がないなら
            if last_player_pass:  # 前回も合法手がなかったら2連続パスなので終局
                break
            else:  # 今回が初めてのパス
                print(f"次は{player}の番です")
                print(f"row col: パス")
                print_board(board)
                player = WHITE if player == BLACK else BLACK
                last_player_pass = True
                continue
        else:  # 今回合法手があったなら
            last_player_pass = False

これは今回のターン、合法手があったかどうかで分岐しています。

  • 今回、合法手がなかった場合はパスになりますが、その際、前回もパスだったかどうかを先ほど作った変数last_player_passを使って調べます。
  • もし前回もパスだったら、2回連続パスということでwhileループをbreakで抜けて終局し、勝者判定処理に向かいます。
  • 前回がパスでなければ、今回が初めてのパスということで、いつものターンのように盤面表示及びプレイヤ交替を済ませたら、last_player_passTrueに設定し、continueします。continueというのは、そのターンの処理はおしまいにして次のターンに進むことです。
  • 一方、もし合法手があった場合は、今回はパスではなかったということで変数last_player_passFalseを代入し、入力処理へと進みます。

入力処理

        # 入力部分
        while True:
            try:
                (row, col) = map(int, input("row col:").split())
                if (0<=row<BOARD_SIZE and 0<=col<BOARD_SIZE)and((row, col) in valid_moves_list(board, player)):
                    break
                else:
                    print(f"合法手を選んでください")
            except ValueError:
                print("整数値を入力してください")
            print_board(board)
            clear_output(wait=True)
        # 入力を盤面に適用
        board = place_move(board, row, col, player)
        print_board(board)
        # プレイヤー交代
        player = WHITE if player == BLACK else BLACK
  • これまでのマルバツゲームや五目並べの入力部分と同じく、try-except文を使ってユーザの入力を受け取っています。
(row, col) = map(int, input("row col:").split())

では、仮にユーザが"2 3"のように入力したら、rowには整数値の2colには整数値の3が格納されます。

if (0<=row<BOARD_SIZE and 0<=col<BOARD_SIZE)and((row, col) in valid_moves_list(board, player)):
  • では、受け取ったrow及びcolが盤面サイズに納まっているかどうか、また合法手かどうかを調べ、どちらも問題なければbreakし、入力を完了しています。
  • 整数以外を入力したり、盤外だったり非合法手であれば入力のループから抜けられません。

完成!

BLACK = "●"
WHITE = "○"
BOARD_SIZE = 8  # 4,6,8のいずれかに限定する

def initialize_board():
    """boardを作り出して返す関数"""
    # 二次元リストで空の盤面を作る(' ' は空きマス)
    board = [[' ' for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)]
    # 初期配置(中央の4つ)
    board[BOARD_SIZE//2-1][BOARD_SIZE//2-1], board[BOARD_SIZE//2-1][BOARD_SIZE//2] = WHITE, BLACK  # ○ ●
    board[BOARD_SIZE//2][BOARD_SIZE//2-1], board[BOARD_SIZE//2][BOARD_SIZE//2] = BLACK, WHITE      # ● ○
    return board

def print_board(board):
    """boardを受け取ったらそれを表示する関数"""
    # top1
    print("    " + "   ".join([f"{num}" for num in range(BOARD_SIZE)]))
    # top2
    print("  ┌───" + "───".join(["┬" for _ in range(BOARD_SIZE-1)]) + "───┐")
    # middle
    for row in range(BOARD_SIZE):
        print(f"{row} │ " + " │ ".join([board[row][col] for col in range(BOARD_SIZE)]) + " │")
        if(row != BOARD_SIZE-1):
            print("  ├─" +  "─┼─".join(["─" for _ in range(BOARD_SIZE)]) + "─┤")
        else:
            print("  └─" +  "─┴─".join(["─" for _ in range(BOARD_SIZE)]) + "─┘")

def is_valid_move(board, row, col, player):  # playerは今回打つ側の色、row,colは打てるか調べたい場所
    """boardにplayerが(row,col)の位置に打つのが合法ならTrueを、非合法ならFalseを返す関数"""
    # 8方向を確認する
    directions = [(0, -1), (-1, -1), (-1, 0), (-1, 1), (0, 1), (1, 1), (1, 0), (1, -1)]  # 左,左上,上,右上,右,右下,下,左下の順番
    for (dr, dc) in directions:
        (r, c) = (row + dr, col + dc)
        # 1つ先が盤内かつ相手の石なら。そうでなければ即探索を打ち切り、次の方向へ
        if (0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE) and (board[r][c] != ' ' and board[r][c] != player):
            # もう一つ先のマスに進んで自分の石が一つでも見つかればTrue(合法)
            (r, c) = (r + dr, c + dc)
            while 0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE:
                if board[r][c] == player:  # 自分の石
                    return True
                elif board[r][c] == ' ':  # 空きマスなら探索打ち切り、次の方向へ
                    break
                (r, c) = (r + dr, c + dc)
    return False


def place_move(board, row, col, player):
    """boardに、<自分の石を置く処理>、<挟まれた相手の石をひっくり返す処理>、の二つを施して返す関数"""
    board[row][col] = player  # まずは自分の石を(row,col)に打つ
    directions = [(0, -1), (-1, -1), (-1, 0), (-1, 1), (0, 1), (1, 1), (1, 0), (1, -1)]  # 左,左上,上,右上,右,右下,下,左下の順番
    for (dr, dc) in directions:
        lst = []  # ひっくり返す可能性のある石のリスト
        (r, c) = (row + dr, col + dc)
        while (0<=r<BOARD_SIZE and 0<=c<BOARD_SIZE):
            if board[r][c] != player:  # もし相手の石ならリストに追加
                lst.append((r,c))
            elif board[r][c] == player:  # もし自分の石ならリストの石をひっくり返して、探索を打ち切り次の方向へ
                for lr, lc in lst:
                    board[lr][lc] = player
                break
            else:  # もし空マスなら探索を打ち切り次の方向へ
                break
            (r, c) = (r + dr, c + dc)
    return board


def valid_moves_list(board, player):
    """playerがboardに打てる合法手のリストを返す関数"""
    lst = []
    for row in range(BOARD_SIZE):
        for col in range(BOARD_SIZE):
            if board[row][col] == ' ' and is_valid_move(board, row, col, player):
                lst.append((row, col))
    return lst

def print_winner(board):
    """終局局面を受け取ると①石数の計算、②勝者表示、をする関数"""
    black_count = sum(row.count(BLACK) for row in board)
    white_count = sum(row.count(WHITE) for row in board)
    print(f"{BLACK}{black_count} - {WHITE}{white_count}")
    if black_count>white_count:
        print("黒の勝ち")
    elif white_count>black_count:
        print("白の勝ち")
    else:
        print("引き分け")

def othello():
    """オセロを遊ぶための関数"""
    # 盤面とプレイヤの初期化
    board = initialize_board()
    player = BLACK
    # 初期盤面の表示
    print_board(board)
    last_player_pass = False  # 2回連続パス検知用フラグ
    (row, col) = (BOARD_SIZE, BOARD_SIZE)  # rowとcolを用意しておく。最初に格納しておく数値はダミー
    # ループ開始
    while True:
        # 合法手がなければ自動的にパスになる。2回連続でパスなら自動的に終局になる
        if(not valid_moves_list(board, player)):  # 今回合法手がないなら
            if last_player_pass:  # 前回も合法手がなかったら2連続パスなので終局
                break
            else:  # 今回が初めてのパス
                print(f"次は{player}の番です")
                print(f"row col: パス")
                print_board(board)
                player = WHITE if player == BLACK else BLACK
                last_player_pass = True
                continue
        else:  # 今回合法手があったなら
            last_player_pass = False
        print(f"合法手リスト:{valid_moves_list(board, player)}")
        print(f"次は{player}の番です")
        # 入力部分
        while True:
            try:
                (row, col) = map(int, input("row col:").split())
                if (0<=row<BOARD_SIZE and 0<=col<BOARD_SIZE)and((row, col) in valid_moves_list(board, player)):
                    break
                else:
                    print(f"合法手を選んでください")
            except ValueError:
                print("整数値を入力してください")
            print_board(board)
        # 入力を盤面に適用
        board = place_move(board, row, col, player)
        print_board(board)
        # プレイヤー交代
        player = WHITE if player == BLACK else BLACK
    print_winner(board)

othello()

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です