
- 今回はPythonでオセロのプログラムを作りましょう。
- 登場するのは、
initialize_board
関数、print_board
関数、is_valid_move
関数、place_move
関数、valid_moves_list
関数、print_winner
関数、othello
関数の7つの関数です。 - このうち、
is_valid_move
関数、place_move
関数の二つがオセロの肝となる部分です。
オセロのルール
- 黒と白が一手ずつ交互に石を打ちます:
自分の石で相手の石を挟むことで、相手の石をひっくり返します。

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

- 相手の石を一つもひっくり返せない場所に打つことはできません。
- 盤上にまだ打てる場所があるのにパスすることはできません。
- 盤上に一箇所も打てる場所がない場合は自動的にパスになります。
- 両者とも連続でパスしたら終局となります。
それでは始めましょう!
グローバルな定数
BLACK = "●"
WHITE = "○"
BOARD_SIZE = 8
- プログラムの初めに定数を定義します。
BLACK
とWHITE
はそれぞれ黒石と白石を表しています。BOARD_SIZE
は盤面のサイズです。8
なら8×8サイズということです。4
や6
など、他の盤面サイズにも対応できるようにプログラムしていきます。
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_SIZE
が8
ならば
[" ", " ", " ", " ", " ", " ", " ", " "]
このようになります。そして、
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_SIZE
を8
とすると
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
関数は、board
とplayer
を受け取ると、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
関数も活用して盤面およびプレイヤの初期化をしています。オセロは黒から始まるのでplayer
はBLACK
に設定されています。
- その後ろの
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_pass
をTrue
に設定し、continue
します。continue
というのは、そのターンの処理はおしまいにして次のターンに進むことです。 - 一方、もし合法手があった場合は、今回はパスではなかったということで変数
last_player_pass
にFalse
を代入し、入力処理へと進みます。
入力処理
# 入力部分
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
には整数値の2
、col
には整数値の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()