今回は、五目並べをコーディングしていきます。マルバツゲーム#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
でいくつ石が連続で並んだら勝利かを定義しています。デフォルトではそれぞれ15
と5
ですが、例えばどちらも3
に設定したらマルバツゲームになります。なお、BOARD_SIZE
は3
以上15
以下、WIN_SEQUENCE
は3
以上BOARD_SIZE
以下
とします。

WIN_SEQUENCE = 3

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