Pythonで五目並べをつくろう

今回は、五目並べをコーディングしていきます。マルバツゲーム#1~#3.5の拡張編です。
大きく変わっているのは盤面表示部分勝利判定部分のみで、他はマルバツゲームと似通っています。

五目並べのルール

  • 五目並べとは、◯×ゲーム(三目並べ)五目になったものです。盤面サイズは15×15です。
  • 黒石●と白石○を交互に盤面の交点上に打っていき、タテ・ヨコ・ナナメのいずれかで先に五つ揃った方の勝ちです。

それでは始めましょう!

グローバルな定数

BLACK = "●"  # 黒丸。黒石を表現
WHITE = "○"  # 白丸。白石を表現
DOUBLE_BLACK = "◉"  # 二重黒丸。直近の手を表現。あくまで表示用に使われる一時的な値
DOUBLE_WHITE = "◎"  # 二重白丸。直近の手を表現。あくまで表示用に使われる一時的な値
SPACE = " "  # 一マスの空白。空点を表現
BOARD_SIZE = 15  # 盤面サイズ
WIN_SEQUENCE = 5  # いくつ連続で並んだら勝利か
  • BLACK, WHITE, DOUBLE_BLACK, DOUBLE_WHITE, SPACEは石や空点を表しています。定数として管理することで、一括変更を可能にしています。
  • BOARD_SIZEで画面サイズを、WIN_SEQUENCEでいくつ石が連続で並んだら勝利かを定義しています。デフォルトではそれぞれ155ですが、例えばどちらも3に設定したらマルバツゲームになります。なお、
    BOARD_SIZE3以上15以下、
    WIN_SEQUENCE3以上BOARD_SIZE以下
    とします。
BOARD_SIZE = 3
WIN_SEQUENCE = 3
BOARD_SIZE = 15
WIN_SEQUENCE = 5

print_board関数

  • print_board関数は、盤面をまさに印刷機のごとく上から順番にprintしていく関数です。このままコピペすることをお勧めいたします。
def print_board(board):
    """boardを元に盤面を表示する関数"""
    # top1
    print("    " + "".join([f"{i:<2}" for i in range(1, BOARD_SIZE+1)]))
    # top2
    print("  ┌─" + "─".join(["─" for _ in range(BOARD_SIZE)]) + "─┐")
    # top3
    print("1 │ " + ("┌" if board[0] == " " else board[0]) + "─" \
         + "─".join([f"{'┬' if board[k] == ' ' else board[k]}" for k in range(1,BOARD_SIZE-1)])  \
         + "─" + ("┐" if board[BOARD_SIZE-1] == " " else board[BOARD_SIZE-1]) + " │")
    # middle
    for i in range(1, BOARD_SIZE-1):
        print(f"{i+1:<2}│ " + ("├" if board[BOARD_SIZE * i] == " " else board[BOARD_SIZE * i]) + "─" \
             + "─".join([f"{'┼' if board[BOARD_SIZE * i + k] == ' ' else board[BOARD_SIZE * i + k]}" for k in range(1,BOARD_SIZE-1)]) \
             + "─" + ("┤" if board[BOARD_SIZE * (i+1) - 1] == " " else board[BOARD_SIZE * (i+1) - 1]) + " │")
    # bottom1
    print(f"{BOARD_SIZE:<2}│ " + ("└" if board[BOARD_SIZE*(BOARD_SIZE-1)] == " " else board[BOARD_SIZE*(BOARD_SIZE-1)])  \
            + "─" + "─".join([f"{'┴' if board[BOARD_SIZE*(BOARD_SIZE-1) + k] == ' ' else board[BOARD_SIZE*(BOARD_SIZE-1) + k]}" for k in range(1,BOARD_SIZE-1)])  \
            + "─" + ("┘" if board[BOARD_SIZE*BOARD_SIZE-1] == " " else board[BOARD_SIZE*BOARD_SIZE-1]) + " │")
    # bottom2
    print("  └─" + "─".join(["─" for _ in range(BOARD_SIZE)]) + "─┘")
  • joinというのは、ノリみたいにペタペタ文字列同士を繋ぎ合わせる関数(メソッド)です。
    例えば
    " + ".join(["1", "2", "3"])
    これはどうなるかというと
    "1 + 2 + 3"
    という文字列になります。" + "という部分がノリのような役割を果たしているのがわかると思います。
    joinメソッドに渡すのはリスト以外にもタプルなども可能ですが、その中身は文字列である必要があります。
  • 一つ一つの点の位置については、「石があるなら石を表示、ない(空点)なら格子点を表示」となるようにif文を使って分岐処理をしています。
  • 左側の行番号:<2と書くことによって、左揃え・幅2にしています。
    なお、右揃えにしたい場合は:>2中央揃えなら:^2と書きます。

check_winner関数

  • この部分が今回のプログラムの肝です。
def check_winner(player, board):
    """boardでplayerが勝利していればtrueを、そうでなければfalseを返す"""
    # 横が揃っていればTrueを返す
    for row in range(BOARD_SIZE):
        for col in range(BOARD_SIZE - WIN_SEQUENCE + 1):
            sequence_set = set()
            for i in range(WIN_SEQUENCE):
                sequence_set.add(board[(row * BOARD_SIZE + col) + i])
            if len(sequence_set) == 1 and sequence_set.pop() == player:
                return True
    # 縦が揃っていればTrueを返す
    for row in range(BOARD_SIZE-WIN_SEQUENCE+1):
        for col in range(BOARD_SIZE):
            sequence_set = set()
            for i in range(WIN_SEQUENCE):
                sequence_set.add(board[(row * BOARD_SIZE + col) + i * BOARD_SIZE])
            if len(sequence_set) == 1 and sequence_set.pop() == player:
                return True
    # 右斜め下が揃っていればTrueを返す
    for row in range(BOARD_SIZE-WIN_SEQUENCE+1):
        for col in range(BOARD_SIZE-WIN_SEQUENCE+1):
            sequence_set = set()
            for i in range(WIN_SEQUENCE):
                sequence_set.add(board[(row * BOARD_SIZE + col) + i * BOARD_SIZE + i])
            if len(sequence_set) == 1 and sequence_set.pop() == player:
                return True
    # 左斜め下が揃っていればTrueを返す
    for row in range(BOARD_SIZE-WIN_SEQUENCE+1):
        for col in range(WIN_SEQUENCE-1, BOARD_SIZE):
            sequence_set = set()
            for i in range(WIN_SEQUENCE):
                sequence_set.add(board[(row * BOARD_SIZE + col) + i * BOARD_SIZE - i])
            if len(sequence_set) == 1 and sequence_set.pop() == player:
                return True
    # 上の4パターンどれにも当てはまらなければFalseを返す
    return False

マルバツゲームの勝利判定

  • マルバツゲームでは、勝利のパターンが縦横斜めの計8パターンしかなかったため
    if (board[0] == board[1] == board[2] == current_player) or \
    (board[3] == board[4] == board[5] == current_player) or \
    (board[6] == board[7] == board[8] == current_player) or \
    (board[0] == board[3] == board[6] == current_player) or \
    (board[1] == board[4] == board[7] == current_player) or \
    (board[2] == board[5] == board[8] == current_player) or \
    (board[0] == board[4] == board[8] == current_player) or \
    (board[2] == board[4] == board[6] == current_player):

    のように全パターンを数え上げてゴリ押し勝利判定することが可能でした。

五目並べの勝敗判定はどうする?

  • しかし、五目並べでそれをやるのは大変です。
  • そこで、5つ連続で揃っているかどうか、を横・縦・右斜め下・左斜め下それぞれについて、左上隅から右下隅まで調べていくことにします。
右斜め下
左斜め下

横の処理

  • まずはの処理について見ていきます。
for row in range(BOARD_SIZE):
    for col in range(BOARD_SIZE - WIN_SEQUENCE + 1):
        sequence_set = set()
        for i in range(WIN_SEQUENCE):
            sequence_set.add(board[(row * BOARD_SIZE + col) + i])
        if len(sequence_set) == 1 and sequence_set.pop() == player:
            return True
  • rowというのは(横)のことで、colというのは(縦)のことです。例えば15*15の盤面で横5個を調べる場合は15行11列調べることになることを確認してください。
  • sequence_set = set()というのは、セットを作っています。セットというのは重複を許さないリストのことです。
  • for i in range(WIN_SEQUENCE):
    sequence_set.add(board[(row * BOARD_SIZE + col) + i])
    というところでは、今回調べる5つの点の位置に、boardリストでは何が格納されていたのかをsequence_setに追加しています。
  • if len(sequence_set) == 1 and sequence_set.pop() == player:
    return True
    では、調べた5つの点にあったものを全てsequence_setに追加していった結果、その長さが1であり、かつ要素がBLACK すなわち"●"であれば黒が5つ揃ったということ、要素がWHITEすなわち"○"であれば白が5つ揃ったということで、Trueを返しています。
  • popメソッドは、セットの中から要素をランダムに取り出す関数ですが、今回はセットの長さが1なので、取り出される要素は一つに定まります。

縦・右斜め下・左斜め下も同じ

  • ここまで、について見てきましたが、縦・右斜め下・左斜め下それぞれの処理についても同様ですので、確認していただければと思います。

gomoku関数

def gomoku():
    """五目並べ関数"""
    # 適切な盤面サイズ及び勝利石数かチェック
    if(not (3<=BOARD_SIZE<=15) or not (3<=WIN_SEQUENCE<=BOARD_SIZE)):
        print("盤面サイズもしくは勝利石数が適切ではありません")
        return
    # 変数定義
    current_player = BLACK  # 現在のプレイヤー
    turn = 0  # 今何ターン目か。引き分けを検知するために使う(もっともマルバツゲームと異なり五目並べで引き分けることは稀と考えられる)
    board = [SPACE] * BOARD_SIZE * BOARD_SIZE  # 盤面(リスト)。空点は一マスの空白(" ")で表現、黒石と白石はBLACKとWHITEで表現
    # 処理開始
    print_board(board)  # 最初の盤面を表示
    while turn < (BOARD_SIZE*BOARD_SIZE):  # ループ開始
        turn += 1
        print(f"次は {current_player} の番です")  # 次の手番の表示
        clear_output(wait=True)
        while True:  # 入力部分
            try:
                row, col = map(int, input(f"{current_player} の手: ").split())
                index = (row - 1) * BOARD_SIZE + (col - 1)
                if 0 <= index < BOARD_SIZE*BOARD_SIZE and board[index] != BLACK and board[index] != WHITE:
                    break
                else:
                    print(f"(1, 1) ~ ({BOARD_SIZE}, {BOARD_SIZE})の空いているマスを選んでください")
            except ValueError:
                print("整数値を入力してください")
            print_board(board)
            clear_output(wait=True)
        # print_board用にいったん今回のインデックス(最新手)に二重丸を格納
        board[index] = DOUBLE_BLACK if current_player == BLACK else DOUBLE_WHITE
        print_board(board)
        board[index] = current_player  # 普通の丸に戻す
        if  check_winner(current_player, board):  # 勝利判定
            print(f"勝者: {current_player}")
            break
        current_player = BLACK if current_player == WHITE else WHITE  # 手番交代
    else:  # ループを回り切ったら引き分け
        print("引き分け")
  • gomoku関数は、これまでのマルバツゲームのコードとほとんど同じです。最初に盤面サイズ及び勝利石数の確認があるのと、入力部分と、盤面表示部分とが、少しだけ違います。

盤面サイズ及び勝利石数の確認

  • BOARD_SIZE3以上15以下、
    WIN_SEQUENCE3以上BOARD_SIZE以下
    という範囲内に設定されていないとプログラムを開始できないようにしています。
if(not (3<=BOARD_SIZE<=15) or not (3<=WIN_SEQUENCE<=BOARD_SIZE)):
    print("盤面サイズもしくは勝利石数が適切ではありません")
    return

入力部分

  • 入力部分については、マルバツゲームでは打ちたい場所を"4"のようにマスのインデックスで直接的に表現していました。
# マルバツゲームではこうなっていた
index = int(input(f"{current_player} の手 : "))
  • 一方、今回の五目並べプログラムでは"行番号 列番号"という形で行と列をスペースで区切る形で入力しています。
# 五目並べではこうなった
row, col = map(int, input(f"{current_player} の手: ").split())
index = (row - 1) * BOARD_SIZE + (col - 1)

それでは流れを見ていきます。

入力の流れ

  1. まず、プレイヤが例えば"3 4"のように入力すると、これはsplit関数によって["3", "4"]というリストに変換されます。
  2. 次にmap関数によって、リスト["3", "4"]の各要素にint関数が適用されます。この結果、文字列を格納していたリスト["3", "4"][3, 4]という整数値を格納するリストに変換されます。
  3. 最後に、row, col = [3, 4]と書くことで、rowには3が、colには4が格納されます。このように右側のリスト(やタプルなど)の各要素を左側の変数にそれぞれ代入することをアンパッキングといいます。なお、アンパッキングの際には右側の要素数と左側の変数の数とが一致している必要があります(*を使わない場合)。
  4. index = (row - 1) * BOARD_SIZE + (col - 1)
    では、受け取ったrowcolboardリストの対応するインデックスに変換しています。

盤面表示部分

board[index] = DOUBLE_BLACK if current_player == BLACK else DOUBLE_WHITE  # 二重丸を格納
print_board(board)
board[index] = current_player  # 普通の丸に戻す
  • 225(15*15)マスもあると最後にどこに打ったかがわかりづらいので、最新の手が二重丸になるようになっています。
  • 具体的には、盤面表示直前に二重丸を格納し、盤面表示が済んだらすぐに普通の丸に置き換えています。こうすることで、この後の勝利判定等のロジックに影響を与えることがないようにしています。

完成!

from IPython.display import clear_output

BLACK = "●"  # 黒丸。黒石を表現
WHITE = "○"  # 白丸。白石を表現
DOUBLE_BLACK = "◉"  # 二重黒丸。直近の手を表現
DOUBLE_WHITE = "◎"  # 二重白丸。直近の手を表現
SPACE = " "  # 一マスの空白。空点を表現
BOARD_SIZE = 15  # 盤面サイズ(3~15)
WIN_SEQUENCE = 5  # いくつ連続で並んだら勝利か

def print_board(board):
    """boardを元に盤面を表示する関数"""
    # top1
    print("    " + "".join([f"{i:<2}" for i in range(1, BOARD_SIZE+1)]))
    # top2
    print("  ┌─" + "─".join(["─" for _ in range(BOARD_SIZE)]) + "─┐")
    # top3
    print("1 │ " + ("┌" if board[0] == " " else board[0]) + "─" \
         + "─".join([f"{'┬' if board[k] == ' ' else board[k]}" for k in range(1,BOARD_SIZE-1)])  \
         + "─" + ("┐" if board[BOARD_SIZE-1] == " " else board[BOARD_SIZE-1]) + " │")
    # middle
    for i in range(1, BOARD_SIZE-1):
        print(f"{i+1:<2}│ " + ("├" if board[BOARD_SIZE * i] == " " else board[BOARD_SIZE * i]) + "─" \
             + "─".join([f"{'┼' if board[BOARD_SIZE * i + k] == ' ' else board[BOARD_SIZE * i + k]}" for k in range(1,BOARD_SIZE-1)]) \
             + "─" + ("┤" if board[BOARD_SIZE * (i+1) - 1] == " " else board[BOARD_SIZE * (i+1) - 1]) + " │")
    # bottom1
    print(f"{BOARD_SIZE:<2}│ " + ("└" if board[BOARD_SIZE*(BOARD_SIZE-1)] == " " else board[BOARD_SIZE*(BOARD_SIZE-1)])  \
            + "─" + "─".join([f"{'┴' if board[BOARD_SIZE*(BOARD_SIZE-1) + k] == ' ' else board[BOARD_SIZE*(BOARD_SIZE-1) + k]}" for k in range(1,BOARD_SIZE-1)])  \
            + "─" + ("┘" if board[BOARD_SIZE*BOARD_SIZE-1] == " " else board[BOARD_SIZE*BOARD_SIZE-1]) + " │")
    # bottom2
    print("  └─" + "─".join(["─" for _ in range(BOARD_SIZE)]) + "─┘")

def check_winner(player, board):
    """boardでplayerが勝利していればtrueを、そうでなければfalseを返す"""
    # 横が揃っていればTrueを返す
    for row in range(BOARD_SIZE):
        for col in range(BOARD_SIZE - WIN_SEQUENCE + 1):
            sequence_set = set()
            for i in range(WIN_SEQUENCE):
                sequence_set.add(board[(row * BOARD_SIZE + col) + i])
            if len(sequence_set) == 1 and sequence_set.pop() == player:
                return True
    # 縦が揃っていればTrueを返す
    for row in range(BOARD_SIZE-WIN_SEQUENCE+1):
        for col in range(BOARD_SIZE):
            sequence_set = set()
            for i in range(WIN_SEQUENCE):
                sequence_set.add(board[(row * BOARD_SIZE + col) + i * BOARD_SIZE])
            if len(sequence_set) == 1 and sequence_set.pop() == player:
                return True
    # 右斜め下が揃っていればTrueを返す
    for row in range(BOARD_SIZE-WIN_SEQUENCE+1):
        for col in range(BOARD_SIZE-WIN_SEQUENCE+1):
            sequence_set = set()
            for i in range(WIN_SEQUENCE):
                sequence_set.add(board[(row * BOARD_SIZE + col) + i * BOARD_SIZE + i])
            if len(sequence_set) == 1 and sequence_set.pop() == player:
                return True
    # 左斜め下が揃っていればTrueを返す
    for row in range(BOARD_SIZE-WIN_SEQUENCE+1):
        for col in range(WIN_SEQUENCE-1, BOARD_SIZE):
            sequence_set = set()
            for i in range(WIN_SEQUENCE):
                sequence_set.add(board[(row * BOARD_SIZE + col) + i * BOARD_SIZE - i])
            if len(sequence_set) == 1 and sequence_set.pop() == player:
                return True
    # 上の4パターンどれにも当てはまらなければFalseを返す
    return False

def gomoku():
    """五目並べ関数"""
    # 適切な盤面サイズ及び勝利石数かチェック
    if(not (3<=BOARD_SIZE<=15) or not (3<=WIN_SEQUENCE<=BOARD_SIZE)):
        print("盤面サイズもしくは勝利石数が適切ではありません")
        return
    # 変数定義
    current_player = BLACK  # 現在のプレイヤー
    turn = 0  # 今何ターン目か。引き分けを検知するために使う(もっともマルバツゲームと異なり五目並べで引き分けることは稀と考えられる)
    board = [SPACE] * BOARD_SIZE * BOARD_SIZE  # 盤面(リスト)。空点は一マスの空白(" ")で表現、黒石と白石はBLACKとWHITEで表現
    # 処理開始
    print_board(board)  # 最初の盤面を表示
    while turn < (BOARD_SIZE*BOARD_SIZE):  # ループ開始
        turn += 1
        print(f"次は {current_player} の番です")  # 次の手番の表示
        clear_output(wait=True)
        while True:  # 入力部分
            try:
                row, col = map(int, input(f"{current_player} の手: ").split())
                index = (row - 1) * BOARD_SIZE + (col - 1)
                if 0 <= index < BOARD_SIZE*BOARD_SIZE and board[index] != BLACK and board[index] != WHITE:
                    break
                else:
                    print(f"(1, 1) ~ ({BOARD_SIZE}, {BOARD_SIZE})の空いているマスを選んでください")
            except ValueError:
                print("整数値を入力してください")
            print_board(board)
            clear_output(wait=True)
        # print_board用にいったん今回のインデックス(最新手)に二重丸を格納
        board[index] = DOUBLE_BLACK if current_player == BLACK else DOUBLE_WHITE
        print_board(board)
        board[index] = current_player  # 普通の丸に戻す
        if  check_winner(current_player, board):  # 勝利判定
            print(f"勝者: {current_player}")
            break
        current_player = BLACK if current_player == WHITE else WHITE  # 手番交代
    else:  # ループを回り切ったら引き分け
        print("引き分け")

gomoku()

コメントを残す

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