3目並べ(tic-toc-toe)のプログラムをPythonで書く|Python学習
プログラミングの学習用に3目並べ(俗にいう○×ゲーム)って向いていると思ったので、Pythonで作成してみました。作ってみたら、段階を踏めばプログラミングの初心者〜中級者までに対応できるものになるものだと感じました。
この記事では、ステップを追って3目並べ(tic-toc-toe)を作成する手順を解説します。
3目並べとは
英語ではtic-tac-toeと呼ばれるゲームです。日本では○×ゲームといった方がわかりやすいかもしれません。
ルールは、2人のプレイヤーが3×3のグリッド上に交互にマーク(○または×)をつけていくというシンプルなものです。最初のプレイヤーは○か×を選び、次のプレイヤーは残ったマークを使います。プレイヤーは自分の番が来たら、グリッドの空いているマスに自分のマークを置きます。
勝利条件は簡単です。先に縦、横、または斜めのいずれかに自分のマークを3つ並べたプレイヤーが勝ちとなります。もし、全てのマスが埋まってもどちらも3つ並べることができなかった場合、そのゲームは引き分けとなります。
このゲームの魅力は、そのシンプルさにあります。子供でもすぐに理解できるルールでありながら、戦略を考える余地が十分にあるため思考部分のプログラミグ教材としても利用できます。
この記事では、3目並べを行うプログラムをPythonを使って作成していきたいと思います。
ボードの表示(ステップ1)
ステップ1はボードの表示です。ボードの内容は3×3のリストに保存しており、数値の意味は以下になります。
値 | 値の意味 |
0 | 空白 |
1 | ×が書かれている |
-1 | ○が書かれている |
print_board
は、3×3のリストを上記のルールに従って表示させるものになります。
なおprint("-"*20)
は、--------------------
を表示し、ステップ毎の区切りを見やすくするための境界線を表示するものです。
# ボードを表示する
def print_board(board):
print("-"*20)
print(" a b c")
for i, row in enumerate(board):
print(i+1, end=" ")
for col in row:
if col == 0:
print('.', end=' ')
if col == 1:
print('X', end=' ')
if col == -1:
print('O', end=' ')
print()
board = [[0,0,0] for _ in range(3)]
print_board(board)
実行すると以下のような3目並べのマス目を表示します。a,b,c
と1,2,3
は、場所を指定するための記号です。入力では、例えば1a
といった感じでどこに○または×を書くか指定します。
--------------------
a b c
1 . . .
2 . . .
3 . . .
入力部分を作成(ステップ2)
ボードの表示ができたので次にキーボード入力を受け取る部分を作成していきます。
キーボード入力を受け取る
今回は、マス目を1a, 2a, 2b,...
といった形式で指定することにします。
python
では、input()
でキーボードの入力を受け取ることができます。以下は、input()
でキーボードの入力を受け取ってマスの(x,y)
を返す関数の定義になります。
今回は、1a
と入力してもa1
と入力してもOKなようにプログラムを書いています。
具体的には、以下の処理の流れになります。
- 一文字目が
0
〜9
の範囲(数字)であった場合には、一文字目を行、二文字目を列番号として変換する - そうでない場合は、一文字目を列、二文字目を行として変換する
数値の場合はint(s[0])
といった記述で、文字から整数型に変換できます。a,b,c
の場合はord()
を使って整数に変換します。ord('a')-ord('a')=0, ord('b')-ord('a')=1
になるので、a,b,cは0,1,2に変換されます。
def get_input() :
s = input()
print(s)
if '0' <= s[0] and s[0] <= '9' :
y = int(s[0])-1
x = ord(s[1]) - ord('a')
else :
y = int(s[1])-1
x = ord(s[0]) - ord('a')
return x, y
以下のコードでは、入力を文字列にマスの位置に変換し、ボードを更新して表示しています。
for i in range(9) :
print_board(board)
print("Enter position(ex. 1a):", end="")
x, y = get_input()
if i%2 == 0 :
board[y][x] = -1
else :
board[y][x] = 1
i%2
は、iを2で割ったあまりを計算するもので、0,1,0,1,0,..と01を繰り返します。つまり、交互に-1と1が指定したマスに代入されます。
ここまでのコード
以下が、ここまでのコードです(このコードは実行できます)。
# 入力を受け付ける
def print_board(board):
print("-"*20)
print(" a b c")
for i, row in enumerate(board):
print(i+1, end=" ")
for col in row:
if col == 0:
print('.', end=' ')
if col == 1:
print('X', end=' ')
if col == -1:
print('O', end=' ')
print()
def get_input() :
s = input()
print(s)
if '0' <= s[0] and s[0] <= '9' :
y = int(s[0])-1
x = ord(s[1]) - ord('a')
else :
y = int(s[1])-1
x = ord(s[0]) - ord('a')
return x, y
board = [[0,0,0] for _ in range(3)]
for i in range(9) :
print_board(board)
print("Enter position(ex. 1a):", end="")
x, y = get_input()
if i%2 == 0 :
board[y][x] = -1
else :
board[y][x] = 1
実行すると、例えば以下のようになります(赤文字は入力です)
--------------------
a b c
1 . . .
2 . . .
3 . . .
Enter position(ex. 1a):2b
2b
--------------------
a b c
1 . . .
2 . O .
3 . . .
Enter position(ex. 1a):1a
1a
--------------------
a b c
1 X . .
2 . O .
3 . . .
Enter position(ex. 1a):2c
2c
--------------------
a b c
1 X . .
2 . O O
3 . . .
Enter position(ex. 1a):
置けない場所の判定
先ほどのプログラムには問題点があります。
問題点は、既に○や×が書かれてもマス目として指定できることです。つまり、既に○や×が書かれているマスに上書きできてしまいます。
これを防ぐには、マス目が空白かどうかをチェックすればOKです。
具体的には、入力されたマス目が0かどうかをチェックして、0でない場合は入力を繰り返すことで解決できます。
while(1) :
x, y = get_input()
if board[y][x] != 0 :
print("already occupied. try again.")
else:
break
ここまでのコード
以下が、ここまでのコードです(このコードは実行できます)。
# 置けない場合は入力を繰り返す
def print_board(board):
print("-"*20)
print(" a b c")
for i, row in enumerate(board):
print(i+1, end=" ")
for col in row:
if col == 0:
print('.', end=' ')
if col == 1:
print('X', end=' ')
if col == -1:
print('O', end=' ')
print()
def get_input() :
s = input()
print(s)
if '0' <= s[0] and s[0] <= '9' :
y = int(s[0])-1
x = ord(s[1]) - ord('a')
else :
y = int(s[1])-1
x = ord(s[0]) - ord('a')
return x, y
board = [[0,0,0] for _ in range(3)]
for i in range(9) :
print_board(board)
print("Enter position(ex. 1a):", end="")
while(1) :
x, y = get_input()
if board[y][x] != 0 :
print("already occupied. try again.")
else:
break
if i%2 == 0 :
board[y][x] = -1
else :
board[y][x] = 1
このコードでは、既に書き込んだマスに○や×が書き込めないので、ルールに沿った3目並べを遊ぶことができます。
一応、ここまでで人対人の対戦が行えます。
勝利判定を作成(ステップ3)
せっかく、コンピュータ上で対戦を行うなら、勝ち負けの判定はコンピュータにやらせたいです。ここでは、結果を判定する関数を作成します。
3×3マスしかないので、判定も簡単です。判定アルゴリズムは以下のようになります。
- 縦、横に3つ揃っている判定
- 斜め((0,0)〜(2,2))に3つ揃っているか判定
- 斜め((0,2)〜(2,0))に3つ揃っているか判定
- ①〜③に該当しない場合は揃っていない
例えば、横に並んでいるかどうかは以下の条件で判定できます。
board[i][0] == board[i][1] == board[i][2]
これを、全ての方向に行います。
3×3と狭いので、全パターン書いてもそれほどキツくないです
戻り値ですが、一致していた値を返せばOKです。なお、board[i][0] != 0
などの判定で、空白が3つ並んでいる場合を判定から除外しています。
def check_win(board) :
for i in range(3) :
if board[i][0] != 0 and board[i][0] == board[i][1] == board[i][2] :
return board[i][0]
if board[0][i] != 0 and board[0][i] == board[1][i] == board[2][i] :
return board[0][i]
if board[0][0] != 0 and board[0][0] == board[1][1] == board[2][2] :
return board[0][0]
if board[0][2] != 0 and board[0][2] == board[1][1] == board[2][0] :
return board[0][2]
return 0
ここまでのコード
以下が、ここまでのコードです(このコードは実行できます)。このコードで、人対人の対戦と、終了判定が完了です。
def print_board(board):
print("-"*20)
print(" a b c")
for i, row in enumerate(board):
print(i+1, end=" ")
for col in row:
if col == 0:
print('.', end=' ')
if col == 1:
print('X', end=' ')
if col == -1:
print('O', end=' ')
print()
def get_input() :
s = input()
if '0' <= s[0] and s[0] <= '9' :
y = int(s[0])-1
x = ord(s[1]) - ord('a')
else :
y = int(s[1])-1
x = ord(s[0]) - ord('a')
return x, y
def check_win(board) :
for i in range(3) :
if board[i][0] != 0 and board[i][0] == board[i][1] == board[i][2] :
return board[i][0]
if board[0][i] != 0 and board[0][i] == board[1][i] == board[2][i] :
return board[0][i]
if board[0][0] != 0 and board[0][0] == board[1][1] == board[2][2] :
return board[0][0]
if board[0][2] != 0 and board[0][2] == board[1][1] == board[2][0] :
return board[0][2]
return 0
board = [[0,0,0] for _ in range(3)]
for i in range(9) :
print_board(board)
stat = check_win(board)
if stat == -1 :
print("-"*20)
print("Winner is player1")
print("-"*20)
exit(0)
if stat == 1 :
print("-"*20)
print("Winner is player2")
print("-"*20)
exit(0)
print("Enter position(ex. 1a):", end="")
while(1) :
x, y = get_input()
if board[y][x] != 0 :
print("already occupied. try again.")
else:
break
if i%2 == 0 :
board[y][x] = -1
else :
board[y][x] = 1
print_board(board)
print("-"*20)
print("Draw")
print("-"*20)
コンピュータ対戦の実現(ステップ4)
どうせなら、Player2をコンピュータにしてみます。
コンピュータの思考部分の考え方
ソースコードの変更
先ほどの人対人のコードを書き換えて、コンピュータ対戦のコードの雛形を作成します。
ハイライトしている部分が変更部分になります。
import random
def print_board(board):
print("-"*20)
print(" a b c")
for i, row in enumerate(board):
print(i+1, end=" ")
for col in row:
if col == 0:
print('.', end=' ')
if col == 1:
print('X', end=' ')
if col == -1:
print('O', end=' ')
print()
def get_input() :
s = input()
if '0' <= s[0] and s[0] <= '9' :
y = int(s[0])-1
x = ord(s[1]) - ord('a')
else :
y = int(s[1])-1
x = ord(s[0]) - ord('a')
return x, y
def check_win(board) :
for i in range(3) :
if board[i][0] != 0 and board[i][0] == board[i][1] == board[i][2] :
return board[i][0]
if board[0][i] != 0 and board[0][i] == board[1][i] == board[2][i] :
return board[0][i]
if board[0][0] != 0 and board[0][0] == board[1][1] == board[2][2] :
return board[0][0]
if board[0][2] != 0 and board[0][2] == board[1][1] == board[2][0] :
return board[0][2]
return 0
board = [[0,0,0] for _ in range(3)]
for i in range(9) :
print_board(board)
stat = check_win(board)
if stat == -1 :
print("-"*20)
print("Winner is player1")
print("-"*20)
exit(0)
if stat == 1 :
print("-"*20)
print("Winner is player2")
print("-"*20)
exit(0)
if i%2 == 0:
print("Enter position(ex. 1a):", end="")
while(1) :
x, y = get_input()
if board[y][x] != 0 :
print("already occupied. try again.")
else:
break
else:
# ここにコンピュータの処理を追加する
# x, y, _ = think(board, 1)
if i%2 == 0 :
board[y][x] = -1
else :
board[y][x] = 1
print_board(board)
print("-"*20)
print("Draw")
print("-"*20)
人が先行(Player1)で、Player2の時はコンピュータの思考処理を呼び出します。思考処理では、置く場所(x,y)
を返します。
置ける場所を見つけたらそこに置く
一番簡単な処理は、置ける場所を探して、そこを選択するというものです。
この処理では、上から順番に調べていき、空白があればそこを返します。
なお、置けない場合に呼び出されることはないので、見つからない場合はダミー(0,0)
を返しています。
# 最初に見つけた場所に置く
def think_seq(board) :
pos = []
for i in range(3):
for j in range(3) :
if board[i][j] == 0 : return (j, i)
return (0,0)
これで、一応コンピュータが自動で場所を選んでくれます。
ランダムに配置する
次は、置く場所をランダムに選ぶ様に変更してみます。
この処理では、置ける場所を全てリストに記憶しておき、その中から1つランダムに選んで返します。
# ランダムに置く場所を決める
def think_random(board) :
pos = []
for i in range(3):
for j in range(3) :
if board[i][j] == 0 : pos.append((j, i))
sel = random.randint(0, len(pos)-1)
return pos[sel]
負けないようにする
次のターンで相手が勝つ配置がある場合は、そこを塞ぐ様にします。
処理としては、一旦相手のターンで置いた場合の盤面を作成し、check_win
で相手が勝つかどうか判定します。
次の手で相手が勝つ場所がある場合はそこを優先します。
なお、一旦相手のコマを置いた状態を作っているので、board[i][j] = 0
で仮で置いた場所を元に戻しておきます。
なお、相手が勝つパターンがない場合は、ランダムにおきます。
# 負けない様ににする
def think_stop2(board, turn) :
pos = []
for i in range(3):
for j in range(3) :
if board[i][j] != 0 : continue
board[i][j] = -turn
if check_win(board) == -turn :
board[i][j] = 0
return (j, i)
board[i][j] = 0
pos.append((j, i))
sel = random.randint(0, len(pos)-1)
return pos[sel]
このコードで、相手が2マスを作った場合に負けを回避できるようになります。
このコンピュータの処理だけで、リーチ(2マスが繋がって次で3マスになる部分)を2箇所同時に作らないとコンピュータに勝つことができなくなります。
その他改良案
さらに、積極的にリーチを作っていくように変更するなど、いろいろ検討することができると思います。思考ルーチンを作るのは面白いので自分で考えてチャレンジしてみるのも良いと思います。
本格的な思考ルーチン(深さ優先探索):上級者向け?
本格的な思考ルーチンをつくることも可能です。3目並べの場合、9マスとマス数が少ないので読み切ることが可能です。
下記は深さ優先探索(DFS)で次の手を探索する処理の例です。処理としては、min-max法と呼ばれる手法を簡単化(スコアを計算せず、勝ち・負け・引き分けだけ)したものになります。
この関数の処理については詳しく説明しませんが、自分のターン、相手のターン…と再帰的に探索していきます。
「自分の勝ちは相手の負け」になるということを注意して実装すればそこまで難しくはありません。
このコードをコンピュータ側に設定すると、基本負けはなくなります(必ず引き分け以上になる)。
# 深さ優先探索で、最終的な勝者を返す
def think(board, turn) :
# 9マス全てが埋まっているかどうか確認
cnt = 0
for i in range(3):
for j in range(3):
if board[i][j] == 0 : cnt += 1
if cnt == 0 :
return -1, -1, 0 # 全て埋まっていればDRAW
x, y = 0,0
win = False
res = -turn
for i in range(3) :
for j in range(3) :
if board[i][j] != 0 : continue # 既に置かれている場合はスキップ
board[i][j] = turn
winner = check_win(board)
if winner == turn :
board[i][j] = 0
return j, i, turn # 置いて勝てば勝者はturnの人
# 次の手を考える
_, _, r = think(board, -turn)
board[i][j] = 0
if r == turn: # 自分が勝つ場合は、その手を覚える
win = True
x, y = j, i
res = turn
if win == False: # 自分の価値が決定してない場合
if r == 0 : # 引き分けの場合は、手を覚える
x, y = j, i
res = 0
if r == -turn and res == -turn: # 現在まだ引き分け以上の手が見つかっていなければ、場所を記録
x, y = j, i
return x, y, res
深さ優先探索については以下の記事を参考にしてください。
まとめ
3目並べを例題として、ステップを踏みながらプログラミングしていきました。プログラミングの学習としては、「人対人までは初心者でも作れる」ことと、「コンピュータの思考部分は中級者くらいまでがいろいろ工夫しながら作れる」という点で結構面白い題材だと思いますがどうでしょうか?
付録(コード全体)
以下、3目並べの全コードです。
import random
def print_board(board):
print("-"*20)
print(" a b c")
for i, row in enumerate(board):
print(i+1, end=" ")
for col in row:
if col == 0:
print('.', end=' ')
if col == 1:
print('X', end=' ')
if col == -1:
print('O', end=' ')
print()
def get_input() :
s = input()
if '0' <= s[0] and s[0] <= '9' :
y = int(s[0])-1
x = ord(s[1]) - ord('a')
else :
y = int(s[1])-1
x = ord(s[0]) - ord('a')
return x, y
def check_win(board) :
for i in range(3) :
if board[i][0] != 0 and board[i][0] == board[i][1] == board[i][2] :
return board[i][0]
if board[0][i] != 0 and board[0][i] == board[1][i] == board[2][i] :
return board[0][i]
if board[0][0] != 0 and board[0][0] == board[1][1] == board[2][2] :
return board[0][0]
if board[0][2] != 0 and board[0][2] == board[1][1] == board[2][0] :
return board[0][2]
return 0
# 最初に見つけた場所に置く
def think_seq(board) :
pos = []
for i in range(3):
for j in range(3) :
if board[i][j] == 0 : return (j, i)
return (0,0)
# ランダムに置く場所を決める
def think_random(board) :
pos = []
for i in range(3):
for j in range(3) :
if board[i][j] == 0 : pos.append((j, i))
sel = random.randint(0, len(pos)-1)
return pos[sel]
# 負けない様ににする
def think_stop2(board, turn) :
pos = []
for i in range(3):
for j in range(3) :
if board[i][j] != 0 : continue
board[i][j] = -turn
if check_win(board) == -turn :
board[i][j] = 0
return (j, i)
board[i][j] = 0
pos.append((j, i))
sel = random.randint(0, len(pos)-1)
return pos[sel]
# 深さ優先探索で、最終的な勝者を返す
def think(board, turn) :
# 9マス全てが埋まっているかどうか確認
cnt = 0
for i in range(3):
for j in range(3):
if board[i][j] == 0 : cnt += 1
if cnt == 0 :
return -1, -1, 0 # 全て埋まっていればDRAW
x, y = 0,0
win = False
res = -turn
for i in range(3) :
for j in range(3) :
if board[i][j] != 0 : continue # 既に置かれている場合はスキップ
board[i][j] = turn
winner = check_win(board)
if winner == turn :
board[i][j] = 0
return j, i, turn # 置いて勝てば勝者はturnの人
# 次の手を考える
_, _, r = think(board, -turn)
board[i][j] = 0
if r == turn: # 自分が勝つ場合は、その手を覚える
win = True
x, y = j, i
res = turn
if win == False: # 自分の価値が決定してない場合
if r == 0 : # 引き分けの場合は、手を覚える
x, y = j, i
res = 0
if r == -turn and res == -turn: # 現在まだ引き分け以上の手が見つかっていなければ、場所を記録
x, y = j, i
return x, y, res
board = [[0,0,0] for _ in range(3)]
for i in range(9) :
print_board(board)
stat = check_win(board)
if stat == -1 :
print("-"*20)
print("Winner is player1")
print("-"*20)
exit(0)
if stat == 1 :
print("-"*20)
print("Winner is player2")
print("-"*20)
exit(0)
if i%2 == 0:
print("Enter position(ex. 1a):", end="")
while(1) :
x, y = get_input()
if board[y][x] != 0 :
print("already occupied. try again.")
else:
break
else:
# x, y = think_seq(board)
# x, y = think_random(board)
# x, y = think_stop2(board, 1)
x, y, _ = think(board, 1)
if i%2 == 0 :
board[y][x] = -1
else :
board[y][x] = 1
print_board(board)
print("-"*20)
print("Draw")
print("-"*20)