Pythonで三目並べゲームを作る
本記事では、コンピュータと対戦する三目並べを制作します。◯や×の印が3つ並んだかを調べる初歩的な判定方法から、コンピュータの思考という本格的なアルゴリズムまで、色々な知識を学んでいきます。
キャンバスにマス目を描く
はじめに三目並べのルールを説明し、ウィンドウを表示してマス目を描くところからプログラミングを始めていきます。
三目並べとは
三目並べとは、3×3のマスに、二人のプレイヤーが○と×の印を交互に付けていき、3つの印を縦、横、斜めのいずれかに先に並べたほうが勝ちとなるゲームです。
この遊びは地方によって呼び方に違いがあるようで、著者が生まれ育った地域では“まるばつ”と呼ばれていました。ジャンケンをして勝ったほうが○の印を付け(○が先手)、ジャンケンに負けた方は×の印を付ける(×が後手)というルールが一般的と思います。ここから先は○の印を付けることを「Oを置く」、×の印を付けることを「×を置く」と表現して説明します。
今回制作する三目並べは、コンピュータゲームをはじめて制作する人が、ゲームソフトの作り方を理解できるように、できるだけシンプルにプログラムを記述します。そのため先攻後攻の選択はなしで、プレイヤーが先手で○を置き、コンピュータが後手で×を置くものとします。

キャンバスにマス目を描く
マス目を描くところから三目並べの制作を始めます。tkinter を用いてウィンドウを作り、
キャンバスを配置して、線を引く命令でマス目を描きます。
次のプログラムを入力して実行し、動作を確認してください。
list_1.py
import tkinter
def masume():
cvs.create_line(200, 0, 200, 600, fill="gray", width=8)
cvs.create_line(400, 0, 400, 600, fill="gray", width=8)
cvs.create_line(0, 200, 600, 200, fill="gray", width=8)
cvs.create_line(0, 400, 600, 400, fill="gray", width=8)
root = tkinter.Tk()
root.title("三目並べ")
root.resizable(False, False)
cvs = tkinter.Canvas(width=600, height=600, bg="white")
cvs.pack()
masume()
root.mainloop()
実行結果

解説
ウィンドウを用いたゲームを制作するので、1行目でtkinter をインポートしています。
このプログラムではそれらの命令に加え、11行目にresizable) を記述し、ウィンドウの大きさを変更できなくしています。resizable() は第一引数でウィンドウの横方向のサイズ変更を許可するか、第二引数で縦方向のサイズ変更を許可するかを指定します。許可しないならFalse、許可するならTrueを記述します。
3~7行目がマス目を描く関数の定義です。関数名は判りやすいようにローマ字でmasume としました。masume()関数には、キャンバスの変数 cvs に対しcreate_line)で縦線と横線を引く処理を記述しています。
繰り返しで線を引く
4~7行目でcreate_line() を4回記述して線を引いています。それらの4行と同じ処理を
forを用いて次のように記述できます。
for i in range(1, 3):
cvs.create_line(200*i, 0, 200*i, 600, fill=”gray”, width=8) …①
cvs.create_line(0, i*200, 600, i*200, fill=”gray”, width=38) …②

この記述では①が縦線、②が横線になります。for文で4行の処理を3行に減らし、1行だけ削減できますが、処理の内容によっては、for を用いると行数をぐっと減らすことができます。
同じ命令を何度も記述するのではなく、forを用いるべきときがあることを覚えておきましょう。
リストでマス目を管理する
どのマスに印(○もしくは×)が付いているかを二次元リストで管理し、マスの中に○や×の印を描きます。

もっと色々描きたいと思われたあなた!
この動画でもっと学んでみてください。
https://www.youtube.com/watch?v=B07a0xhEkrA
https://www.youtube.com/watch?v=GboxH1yxdTU
二次元リストについて
縦3マス、横3マスの三目並べのマスを管理するために、次のような二次リスト(二次元配列)を用意します]
二次元リストはリスト名 [y][x] のように、yとxの2つの番号で要素を指定します。
例えば次のリストでは、masu[0][0] の値が1、masu[1][2] の値が2になります。
masu = [
[1, 0, 0],
[0, 0, 2],
[0, 0, 0]
]

リストってなに?
追加で見て理解しちゃってください。
https://www.youtube.com/watch?v=96pjZSwhWL4&t=2s

リスト?二重ループ?
という方は下の「プログラミング学習術」という本がおすすめです
○と×をリストで管理する
三目並べでは、印のないマスを0、○が置かれたマスを1、×が置かれたマスを2という数で管理します。
次のプログラムの動作を確認しましょう。このプログラムは確認用に、○と×を1つずつマスに置き、表示しています。
list2.py
import tkinter
masu = [
[1, 0, 0],
[0, 0, 2],
[0, 0, 0]
]
def masume():
for i in range(1, 3):
cvs.create_line(200*i, 0, 200*i, 600, fill="gray", width=8)
cvs.create_line(0, i*200, 600, i*200, fill="gray", width=8)
for y in range(3):
for x in range(3):
X = x * 200
Y = y * 200
if masu[y][x] == 1:
cvs.create_oval(X+20, Y+20, X+180, Y+180, outline="blue", width=12)
if masu[y][x] == 2:
cvs.create_line(X+20, Y+20, X+180, Y+180, fill="red", width=12)
cvs.create_line(X+180, Y+20, X+20, Y+180, fill="red", width=12)
root = tkinter.Tk()
root.title("三目並べ")
root.resizable(False, False)
cvs = tkinter.Canvas(width=600, height=600, bg="white")
cvs.pack()
masume()
root.mainloop()
実行結果

解説
3~7行目がマス目を管理する二次元リストの宣言です。印のないマスは0、○を置いたところは1、Xを置いたところは2とします。
マス目を描くmasume()関数に 13~21行目のように○と×を描く処理を追加しています。縦と横線を引く処理は、前の鏡で競開した繰り返しで行っています。
二重ループでマスの値を調べる
masume()関数を抜き出して説明します。
for y in range (3):
for x in range (3):
X=x*200
Y = y * 200
if masu[y][x] == 1:
cvs.create_oval(X+20, Y+20, X+180, Y+180, outline="blue", width=12)
if masu[y][x] = 2:
cvs.create_line (X+20, Y+20, X+180, Y+180, fill="red", width=12)
cvs.create_line(X+180, Y+20, X+20, Y+180, fill="red", width=12)
二次元リストの値を調べるために二重ループの for を用いています。外側のforは変数で繰り返し、yの値は0→1→2と変化します。内側のfor は変数xで繰り返し、こちらもは0→1→2と変化します。
大文字のXとYの変数に○やxを描く座標を代入しています。X、Yには各マスの左上角の座標が入ります。masu[y][x] の値を調べ、それが1なら、(X,Y)の位置のマスに○を、2なら×を描いています。
yの値が0のとき、xの値は 0→1→2と変化します。次にyが1になり、xは再び0→1→2
と変化します。次にyが2になり、×は0→1→2と変化します。yが2、xが2まで進むと二重ループの繰り返しが終わります。この図の線と矢印で示したように、二次元リストの左上角から右下角までを順に調べ、○や×を描く仕組みになっています。
クリックしたマスに印を付ける
この三目並べは、プレイヤーが○を、コンピュータが×を置くようにします。この節ではマス目をクリックして○を置けるようにします。
bind()でクリックイベントを取得
bind()命令でイベントの種類と関数を指定して、クリックやマウスの動きを取得する方法があります。ここではその方法でマウスのクリックイベントを受け取り、クリックしたマスに○を置けるようにします。
次のプログラムの動作を確認しましょう。クリックしたマスに○が描かれ、そのマスをもう一度クリックすると○が消えるようになっています。
list3.py
import tkinter
masu = [
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]
]
def masume():
cvs.delete("all")
for i in range(1, 3):
cvs.create_line(200*i, 0, 200*i, 600, fill="gray", width=8)
cvs.create_line(0, i*200, 600, i*200, fill="gray", width=8)
for y in range(3):
for x in range(3):
X = x * 200
Y = y * 200
if masu[y][x] == 1:
cvs.create_oval(X+20, Y+20, X+180, Y+180, outline="blue", width=12)
if masu[y][x] == 2:
cvs.create_line(X+20, Y+20, X+180, Y+180, fill="red", width=12)
cvs.create_line(X+180, Y+20, X+20, Y+180, fill="red", width=12)
def click(e):
mx = int(e.x/200)
my = int(e.y/200)
if mx>2: mx = 2
if my>2: my = 2
if masu[my][mx] == 0:
masu[my][mx] = 1
else:
masu[my][mx] = 0
masume()
root = tkinter.Tk()
root.title("三目並べ")
root.resizable(False, False)
root.bind("<Button>", click)
cvs = tkinter.Canvas(width=600, height=600, bg="white")
cvs.pack()
masume()
root.mainloop()
実行結果

解説
masume()関数に、キャンバスに描いたものを全て削除する cvs.delete(“all”)を追記しています。delete(“all”) を実行せずに絵や文字を次々に表示していくと、Pythonの処理が重くなることがあります。何度もグラフィックを描くプログラムでは、前に描いた絵や文字をこの命令で消すようにしましょう。
24~33行目でマウスボタンをクリックしたときに実行する click() 関数を定義しています。click) の引数e に .x と y を付けたe.x と e.yの値が、クリックした座標になります。
1マスのサイズを 200 × 200 ドットとしているので、e.x と eyの値を200で割ることで、どのマスをクリックしたかを知ることができます。その計算を行っている部分を抜き出して説明します。
mx =int(e.x/200)
my =int(e.y/200)
if mx>2: mx = 2
if my>2: my = 2
このmxとmyの値がマスの番号 ([masu~] の添え字)になります。mxとmyの値を整数とするために int() で小数点以下を切り捨てています。
ウィンドウの右端付近をクリックしたとき、e.xの値が600以上になることがあります。
その場合、mx=int(e.x/200)でmx の値は3になります。
3はマスの外側になるので、if mx>2:mx2 という if 文でmx が2を超えないようにしています。
ウィンドウの下端でクリックし、e.yが600 以上になったときも、 if my>2:my=2 で my が2を超えないようにしています。そして29~32行目のように、クリックしたマスの値が0なら1に、1なら0にすることで、○を置いたり消したりしています。
1マスの幅は 200ドット、高さは200ドットだから、クリックした座標を200で割ってリストの添え字を求めるのです。
リストの外側(存在しない要素)を参照しようとするとエラーが発生します。
それを防ぐためif mx>2:mx-2 と if my> 2:my=2 を記述しています。存在しない要素を参照してはならないことも覚えておいてください。
コンピュータが印を付ける~ターン制の処理~
プレイヤーが○を置くと、コンピュータが空いているマスに×を置くようにします。
プレイヤーとコンピュータが交互に行動して対戦したり、参加者全員に行動する番が順に回ってくるゲームルールをターン制といいます。ターン制をプログラミングするにはさまざまな方法が考えられますが、この三目並べはゲーム開発をはじめて行う人が理解できるように、最もシンプルな方法でターン制を組み込みます。
その具体的な方法は、次の図のように、マウスのクリックイベントでプレイヤーが○を置いたら、次にコンピュータがXを置くようにします。
click()→プレイヤーが○を置く→computer()を呼び出す→computer() →コンピュータが×を置く、という流れです。
コンピュータが×を置く computer() という関数を用意し、click()内でそれを呼び出します。computer() 関数を用意せず、click() 内にコンピュータがXを置く処理を記述することもできますが、コンピュータの思考ルーチンを改良するときなどに、処理を分けておくと作業しやすいので、このような関数を用意します。処理のまとまりを関数として分けると、プログラムがすっきりして判読しやすくなり、メンテナンスしやすいメリットがあることを覚えておきましょう。
なお、ある程度高度な内容のゲームは、一般的にリアルタイム処理を用いて制作します。
この節ではプレイヤーとコンピュータが交互に印を付ける処理を入れ、○や×が縦横斜めに並んだことを判定します。
時間調整に timeモジュールを用いる
プレイヤーが○を置き、コンピュータがメを置く処理が、一瞬のうちに進むとゲームの進行が判りにくくなります。そこで○を置いた後、若干の“間”を入れ、コンピュータがXを置くようにします。 time モジュールにある、一定時間、処理を停止させる sleep() という命令で、その“間”を作ります。
いくつ印を付けたか数える
これから確認するプログラムは、いくつ印を付けたかを数える変数を用意し、○と×を合計9個置いたら、それ以上は入力を受け付けなくしています。
list4.py
import tkinter
import random
import time
masu = [
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]
]
shirushi = 0
def masume():
cvs.delete("all")
for i in range(1, 3):
cvs.create_line(200*i, 0, 200*i, 600, fill="gray", width=8)
cvs.create_line(0, i*200, 600, i*200, fill="gray", width=8)
for y in range(3):
for x in range(3):
X = x * 200
Y = y * 200
if masu[y][x] == 1:
cvs.create_oval(X+20, Y+20, X+180, Y+180, outline="blue", width=12)
if masu[y][x] == 2:
cvs.create_line(X+20, Y+20, X+180, Y+180, fill="red", width=12)
cvs.create_line(X+180, Y+20, X+20, Y+180, fill="red", width=12)
cvs.update()
def click(e):
global shirushi
mx = int(e.x/200)
my = int(e.y/200)
if mx>2: mx = 2
if my>2: my = 2
if masu[my][mx] == 0:
masu[my][mx] = 1
shirushi = shirushi + 1
masume()
time.sleep(0.5)
if shirushi < 9:
computer()
def computer():
global shirushi
while True:
x = random.randint(0, 2)
y = random.randint(0, 2)
if masu[y][x] == 0:
masu[y][x] = 2
shirushi = shirushi + 1
masume()
time.sleep(0.5)
break
root = tkinter.Tk()
root.title("三目並べ")
root.resizable(False, False)
root.bind("<Button>", click)
cvs = tkinter.Canvas(width=600, height=600, bg="white")
cvs.pack()
masume()
root.mainloop()
実行結果

解説
masume()関数の26行目に追記した cvs.update() は、キャンバスの状態を更新して、図形や画像、文字列を即座にウィンドウに表示する命令です。この命令はなくてもかまわないこともありますが、画面を何度も描き変えたり、sleep()命令を使うときに記述しておくと、グラフィックがきちんと表示されます。
前のプログラムからの大きな変更箇所は click()関数内の36 ~ 40行目です。プレイヤーがクリックしてOを置いたら、印を付けた数を代入する変数shirushi の値を1増やし、マス目を描き、sleep()で0.5秒間処理を止めています。shirushi が9未満なら、次に computer()関数を呼び出し、コンピュータがXを置くようになっています。
sleep() は timeモジュールに備わる関数で、引数の秒数の間、一時的にプログラムを停止します。この関数で長時間、処理を止めると、ウィンドウがフリーズしたようになり、不具合が発生したときと似た現象が起きます。長時間は停止すべきでないことを頭の隅に置いておきましょう。
今回はそのようなことが起きないように、0.5秒だけ停止させています。
computer()関数について
コンピュータは空いているマスにランダムに×を置きます。そのマスは random モジュールの乱数を発生させる関数で決めています。42~52行目に定義した computer()関数を抜き出して説明します。
def computer():
global shirushi
while True:
x=random.randint(0, 2)
y = random.randint(0, 2)
if masu[y][x] == 0:
masu[y][x] = 2
shirushi = shirushi + 1
masume()
time.sleep(0.5)
break
この処理で変数xとyに乱数を代入し、masu[y][x] が空いているかを調べています。空いているマスを探し続けるように while Trueを用いています。
whileの条件式を True とすると処理を延々と繰り返します(赤矢印の線)。変数x、yに乱数を代入し、if文で masu[y][x] が0かを調べます。0なら空いているマスなので、masu[y][x]に2を代入して×を置き、breakでループを抜け(青矢印の線)次へ進みます。
この方法を使うときは、while Trueの無限ループから脱出できなくなることがないように注意しましょう。
全てのマス目に印が付いた状態で、この while True の処理に入ると、延々とループし続けてしまいます。
そのため、shirushi<9のときだけ computer() を呼び出しています。
連続してクリックすると…
このプログラムでマウスポインタをウィンドウ内で素早く動かしながら、マウスボタンを連打するようにクリックすると、○と×が交互に置かれず、○を2回続けて置くことがあります。そのような不具合はもちろん修正しなくてはなりません。これからそれを修正する if文を追加します。
縦、横、斜めに○や×が揃ったことを判定する処理
~二次元リストの値を調べる~
例えば左側の一列に○が揃ったとき、masu[0][0]、masu[1][0]、masu[2][0]の値は全て1になっています。
左の一列に○が揃ったとき
これは if masu[0][0]==1 and masu[1][0]==1 and masu[2][0]==1 という if文で判定でき
ます。またXの値は2なので、左の一列に×が並んだときは if masu[0][0]==2 and masu[1]
[0]==2 and masu[2][0]==2で判定できます。
繰り返しで効率良く調べる
三目並べは3×3のマスで構成されるので、縦方向に揃ったかを判定するには、先ほどの if文が3つ必要です。横方向の判定を行うのにも3つの if文が必要です。また左上から右下への斜め方向と、右上から左下への斜め方向の判定も行うので、○が揃ったかを判定するのには、3+3+2=8つの if文が必要です。Xも同様に判定するので合計8×2 = 16のif文が必要になります。
ゲーム開発をはじめて行う方は、難しいことは考えず、その16行分の if を記述してかまわないと考えます。一方、プログラミングの基本的な知識のある方や、簡単なミニゲームなら作れるという段階に達した方は、この判定を効率良く行う方法を考えてみましょう。
これから確認するプログラムは、繰り返しを用いて、○が揃ったかと×が揃ったかを順に
判定することで、if文を8行だけにしています。
タイトルバーに結果を表示する
このプログラムは制作過程の確認として、○やが揃ったら「何々が3つ揃いました」という文言をタイトルバーに表示します。次のプログラムの動作を確認してください。
list5.py
import tkinter
import random
import time
masu = [
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]
]
shirushi = 0
kachi = 0
def masume():
cvs.delete("all")
for i in range(1, 3):
cvs.create_line(200*i, 0, 200*i, 600, fill="gray", width=8)
cvs.create_line(0, i*200, 600, i*200, fill="gray", width=8)
for y in range(3):
for x in range(3):
X = x * 200
Y = y * 200
if masu[y][x] == 1:
cvs.create_oval(X+20, Y+20, X+180, Y+180, outline="blue", width=12)
if masu[y][x] == 2:
cvs.create_line(X+20, Y+20, X+180, Y+180, fill="red", width=12)
cvs.create_line(X+180, Y+20, X+20, Y+180, fill="red", width=12)
cvs.update()
def click(e):
global shirushi
if shirushi==1 or shirushi==3 or shirushi==5 or shirushi==7:
return
mx = int(e.x/200)
my = int(e.y/200)
if mx>2: mx = 2
if my>2: my = 2
if masu[my][mx] == 0:
masu[my][mx] = 1
shirushi = shirushi + 1
masume()
time.sleep(0.5)
hantei()
if shirushi < 9:
computer()
def computer():
global shirushi
while True:
x = random.randint(0, 2)
y = random.randint(0, 2)
if masu[y][x] == 0:
masu[y][x] = 2
shirushi = shirushi + 1
masume()
time.sleep(0.5)
hantei()
break
def hantei():
global kachi
kachi = 0
for n in range(1, 3):
#縦に並んだかを判定する
if masu[0][0]==n and masu[1][0]==n and masu[2][0]==n:
kachi = n
if masu[0][1]==n and masu[1][1]==n and masu[2][1]==n:
kachi = n
if masu[0][2]==n and masu[1][2]==n and masu[2][2]==n:
kachi = n
#横に並んだかを判定する
if masu[0][0]==n and masu[0][1]==n and masu[0][2]==n:
kachi = n
if masu[1][0]==n and masu[1][1]==n and masu[1][2]==n:
kachi = n
if masu[2][0]==n and masu[2][1]==n and masu[2][2]==n:
kachi = n
#斜めに並んだかを判定する
if masu[0][0]==n and masu[1][1]==n and masu[2][2]==n:
kachi = n
if masu[0][2]==n and masu[1][1]==n and masu[2][0]==n:
kachi = n
if kachi == 1:
root.title("〇が三つ揃いました")
if kachi == 2:
root.title("×が三つ揃いました")
root = tkinter.Tk()
root.title("三目並べ")
root.resizable(False, False)
root.bind("<Button>", click)
cvs = tkinter.Canvas(width=600, height=600, bg="white")
cvs.pack()
masume()
root.mainloop()
実行結果

解説
def hantei()に記述してある kachi は、○と×のどちらが揃ったかを管理する変数です。○が揃ったとき(プレイヤーが勝ったとき)は kachiに1を、×が揃ったとき(コンピュータが勝ったとき)は2を代入します。11行目でこの変数をグローバル変数として宣言しています。
グローバル変数としたのは、勝敗判定を行う関数を用意し、その関数でも kachiの値を使うためです。
hantei()関数では if masu[0][0]==n and masu[1][0]==n and masu[2][0]==nなどの8つのif文で、縦、横、斜めに印が揃ったかを調べています。○の値は1、Xの値は2なので、forn in range(1,3)でnの値を1→2 と変化させ、nの値で判定することで、○と×の判定を同じif文で順に行っています。
if文の条件式が成立し、○が揃ったときは kachiに1が、×が揃ったときは2が代入されます。
そしてkachiが1なら root.title(“Oが三つ揃いました”)、kachiが2ならroot.title(“×が三つ揃いました”)で、どちらが揃ったかをタイトルバーに表示しています。
○も×も揃ったときはタイトルバーに「×が三つ揃いました」とだけ表示されますが、完成させるときに、どちらかが揃った時点でゲーム終了となるので、ここで気にする必要はありません。
連打したときの不具合の修正
前の節のプログラムで、マウスボタンを連打したときに起きる不具合は、click()関数に次の if文を追加して修正しています。
if shirushi==1 or shirushi==3 or shirushi==5 or shirushi==7 :
return
印を付けた数が1、3、5、7のときはコンピュータがXを置く番なので、この if文のreturn
でclick()関数の処理を終え、○が連続して置かれないようにしています。
ゲーム終了時の処理を組み込む
勝敗結果を表示し、ゲームとして一通り遊べるようにします。
マスに置いた印を数える shirushi という変数の値で、ゲームが終了したことを管理します。
例えば○も×も揃わず、全てのマスに印を付けたら shirushiの値は9になります。そのときは「引き分け」と表示します。途中で○か×が揃って勝ち負けが決まれば、kachi という変数の値が1か2になります。
そのときはプレイヤーの勝ち、あるいはコンピュータの勝ちであることを表示し、ゲームを終了するために shirushi に9を代入します。
また shirushi の値が9のときにウィンドウをクリックすると、はじめから三目並べを遊べるようにします。以上の処理を組み込んだプログラムを確認します。
list6.py
import tkinter
import random
import time
masu = [
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]
]
shirushi = 0
kachi = 0
FNT = ("Times New Roman", 60)
def masume():
cvs.delete("all")
for i in range(1, 3):
cvs.create_line(200*i, 0, 200*i, 600, fill="gray", width=8)
cvs.create_line(0, i*200, 600, i*200, fill="gray", width=8)
for y in range(3):
for x in range(3):
X = x * 200
Y = y * 200
if masu[y][x] == 1:
cvs.create_oval(X+20, Y+20, X+180, Y+180, outline="blue", width=12)
if masu[y][x] == 2:
cvs.create_line(X+20, Y+20, X+180, Y+180, fill="red", width=12)
cvs.create_line(X+180, Y+20, X+20, Y+180, fill="red", width=12)
if shirushi == 0:
cvs.create_text(300, 300, text="スタート!", fill="navy", font=FNT)
cvs.update()
def click(e):
global shirushi
if shirushi == 9:
replay()
return
if shirushi==1 or shirushi==3 or shirushi==5 or shirushi==7:
return
mx = int(e.x/200)
my = int(e.y/200)
if mx>2: mx = 2
if my>2: my = 2
if masu[my][mx] == 0:
masu[my][mx] = 1
shirushi = shirushi + 1
masume()
time.sleep(0.5)
hantei()
syouhai()
if shirushi < 9:
computer()
def computer():
global shirushi
while True:
x = random.randint(0, 2)
y = random.randint(0, 2)
if masu[y][x] == 0:
masu[y][x] = 2
shirushi = shirushi + 1
masume()
time.sleep(0.5)
hantei()
syouhai()
break
def hantei():
global kachi
kachi = 0
for n in range(1, 3):
#縦に並んだかを判定する
if masu[0][0]==n and masu[1][0]==n and masu[2][0]==n:
kachi = n
if masu[0][1]==n and masu[1][1]==n and masu[2][1]==n:
kachi = n
if masu[0][2]==n and masu[1][2]==n and masu[2][2]==n:
kachi = n
#横に並んだかを判定する
if masu[0][0]==n and masu[0][1]==n and masu[0][2]==n:
kachi = n
if masu[1][0]==n and masu[1][1]==n and masu[1][2]==n:
kachi = n
if masu[2][0]==n and masu[2][1]==n and masu[2][2]==n:
kachi = n
#斜めに並んだかを判定する
if masu[0][0]==n and masu[1][1]==n and masu[2][2]==n:
kachi = n
if masu[0][2]==n and masu[1][1]==n and masu[2][0]==n:
kachi = n
def syouhai():
global shirushi
if kachi == 1:
cvs.create_text(300, 300, text="あなたの勝ち!", font=FNT, fill="cyan")
shirushi = 9
if kachi == 2:
cvs.create_text(300, 300, text="コンピュータ\nの勝ち!", font=FNT, fill="gold")
shirushi = 9
if kachi == 0 and shirushi == 9:
cvs.create_text(300, 300, text="引き分け", font=FNT, fill="lime")
def replay():
global shirushi
shirushi = 0
for y in range(3):
for x in range(3):
masu[y][x] = 0
masume()
root = tkinter.Tk()
root.title("三目並べ")
root.resizable(False, False)
root.bind("<Button>", click)
cvs = tkinter.Canvas(width=600, height=600, bg="white")
cvs.pack()
masume()
root.mainloop()
実行結果

解説
91~100行目で、どちらの勝ちか、あるいは引き分けかを表示するsyouhai)関数を定義しています。この関数は、プレイヤーが○を置き、3つ揃ったかを hantei()関数で調べた後、49行目で呼び出しています。またコンピュータが×を置き、3つ揃ったか調べた後の64行目でも呼び出しています。
hantei() 関数で、○が揃うとkachi の値が1に、×が揃うと2になります。
syouhai()関数では kachiが1なら「あなたの勝ち!」と表示して shirushiに9を代入し、kachi が2なら「コンピュータ(改行)の勝ち!」と表示して shirushiに9を代入します。kachi が0で shirushiが9なら、○も×も揃わなかったので「引き分け」と表示しています。
「コンピュータ \nの勝ち!」と表示するときに改行コードで文字列を改行させています。
Pythonでは print() 命令の他に、キャンバスに文字列を表示する create_text() 命令でも改行コードが使えます。
リプレイについて
ゲーム終了後、ウィンドウをクリックすれば、はじめから遊べるように、click()関数に次
のif文を追加しています。
if shirushi == 9:
replay()
return
このときに呼び出す replay()関数を定義しています。replay()関数はmasu[][] の全要素を0にし、マス目に何も印が付いていない状態にします。
コンピュータが弱い
ゲームとして遊べるようになりましたが、コンピュータが弱く、プレイヤーが負けることは、ほぼありません。これはコンピュータがランダムな位置に×を置いているからです。次の節でコンピュータの思考ルーチンを組み込み、コンピュータを強くします。
コンピュータの思考ルーチンを組み込む
コンピュータが思考するプログラムを組み込んで、強くなるようにします。
どのようなアルゴリズムで強くするか
どうすればコンピュータを強くできるか考えてみます。それには、三目並べを人同士です
るとき、人はどのように考えるかを想像してみましょう。多くの人は、次のようにするはず
です。
①自分が3つ揃えられるマスがあるなら、そこに印を付け、勝ちにいく
②相手が3つ揃えられるマスがあるなら、そこに印を付け、相手が勝つことを阻止する
例えば②の状態であっても、①の状態にあれば、わざわざ②は行いません。コンピュータ
にも①と②を行わせれば、ランダムに印を付けるよりも強くなります。コンピュータにこれ
らのことを行わせるには次のようにします。
・全てのマス目を1つずつ調べ、Xを置くとコンピュータの勝ちとなるマスを探す
・そのマスがあるなら、そこに×を置いて勝ちにいく
・そのマスがないなら、また全てのマスを1つずつ調べ、○を置くとコンピュータが負ける(○が3 つ揃い、プレイヤーの勝ちとなる)マスを探す
・そのマスがあるなら、そこに×を置いてプレイヤーが勝つことを阻止する
・いずれのマスも見つからなければ、ランダムな位置に×を置く
これから確認するプログラムには、以上のような思考 ルーチンが入っています。このような思考ルーチンは立派なアルゴリズムです。これをプログラミングすることは現時点で難しいとお考えになる方もいらっしゃると思いますが、全体を理解しようという気持ちで、プログラムとその動作を確認していきます。
思考ルーチンの確認
説明したアルゴリズムを組み込んだプログラムを確認します。完成版の三目並べということで、sanmoku_narabe.py というファイル名にしています。動作確認後にコンピュータの思考をどのようにプログラミングしたかを説明します。
sanmoknarabe.py
import tkinter
import random
import time
masu = [
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]
]
shirushi = 0
kachi = 0
FNT = ("Times New Roman", 60)
def masume():
cvs.delete("all")
for i in range(1, 3):
cvs.create_line(200*i, 0, 200*i, 600, fill="gray", width=8)
cvs.create_line(0, i*200, 600, i*200, fill="gray", width=8)
for y in range(3):
for x in range(3):
X = x * 200
Y = y * 200
if masu[y][x] == 1:
cvs.create_oval(X+20, Y+20, X+180, Y+180, outline="blue", width=12)
if masu[y][x] == 2:
cvs.create_line(X+20, Y+20, X+180, Y+180, fill="red", width=12)
cvs.create_line(X+180, Y+20, X+20, Y+180, fill="red", width=12)
if shirushi == 0:
cvs.create_text(300, 300, text="スタート!", fill="navy", font=FNT)
cvs.update()
def click(e):
global shirushi
if shirushi == 9:
replay()
return
if shirushi==1 or shirushi==3 or shirushi==5 or shirushi==7:
return
mx = int(e.x/200)
my = int(e.y/200)
if mx>2: mx = 2
if my>2: my = 2
if masu[my][mx] == 0:
masu[my][mx] = 1
shirushi = shirushi + 1
masume()
time.sleep(0.5)
hantei()
syouhai()
if shirushi < 9:
computer()
masume()
time.sleep(0.5)
hantei()
syouhai()
def computer():
global shirushi
#3つ揃うマスがあるか
for y in range(3):
for x in range(3):
if masu[y][x] == 0:
masu[y][x] = 2
hantei()
if kachi==2:
shirushi = shirushi + 1
return
masu[y][x] = 0
#プレイヤーが揃うのを阻止する
for y in range(3):
for x in range(3):
if masu[y][x] == 0:
masu[y][x] = 1
hantei()
if kachi==1:
masu[y][x] = 2
shirushi = shirushi + 1
return
masu[y][x] = 0
while True:
x = random.randint(0, 2)
y = random.randint(0, 2)
if masu[y][x] == 0:
masu[y][x] = 2
shirushi = shirushi + 1
break
def hantei():
global kachi
kachi = 0
for n in range(1, 3):
#縦に並んだかを判定する
if masu[0][0]==n and masu[1][0]==n and masu[2][0]==n:
kachi = n
if masu[0][1]==n and masu[1][1]==n and masu[2][1]==n:
kachi = n
if masu[0][2]==n and masu[1][2]==n and masu[2][2]==n:
kachi = n
#横に並んだかを判定する
if masu[0][0]==n and masu[0][1]==n and masu[0][2]==n:
kachi = n
if masu[1][0]==n and masu[1][1]==n and masu[1][2]==n:
kachi = n
if masu[2][0]==n and masu[2][1]==n and masu[2][2]==n:
kachi = n
#斜めに並んだかを判定する
if masu[0][0]==n and masu[1][1]==n and masu[2][2]==n:
kachi = n
if masu[0][2]==n and masu[1][1]==n and masu[2][0]==n:
kachi = n
def syouhai():
global shirushi
if kachi == 1:
cvs.create_text(300, 300, text="あなたの勝ち!", font=FNT, fill="cyan")
shirushi = 9
if kachi == 2:
cvs.create_text(300, 300, text="コンピュータ\nの勝ち!", font=FNT, fill="gold")
shirushi = 9
if kachi == 0 and shirushi == 9:
cvs.create_text(300, 300, text="引き分け", font=FNT, fill="lime")
def replay():
global shirushi
shirushi = 0
for y in range(3):
for x in range(3):
masu[y][x] = 0
masume()
root = tkinter.Tk()
root.title("三目並べ")
root.resizable(False, False)
root.bind("<Button>", click)
cvs = tkinter.Canvas(width=600, height=600, bg="white")
cvs.pack()
masume()
root.mainloop()
実行結果

hantei()関数を使い回している
computer()関数の部分が、新たに組み込んだ思考ルーチンです。それぞれ二重ループのforでマス全体を調べ、Xを置くべき場所を探しています。置くべきかどうかを知るために hantei()関数を用いています。この思考ルーチンは特別なことを行っているのではなく、
・ます全体を調べる
・その際、組み込み済みのhantei)関数を用いる
という、実はさほど複雑ではない仕組みになっています。
思考ルーチン追加に伴うその他の変更点
思考ルーチンの追加で、computer()関数の2か所に return を記述しました。コンピュー
タが×を置くべきマスを見つけたら、return で computer()の処理を終えるようにしたの
で、click()関数内でcomputer() を呼び出した後、masume() → time.sleep(0.5)→ hantei) →
syouhai() と4つの関数を実行するように変更しています。前のプログラムまでは、それらの
4つの関数を computer()内に記述していました。
ゲーム用のAIについて
ゲームソフト用の思考ルーチンは、ゲーム AIやゲーム用 AIなどと呼ばれます。AIとは人工知能のことです。ここで組み込んだ三目並べの思考ルーチンは、初歩的な人工知能といえ
ます。
ディープラーニングなどの新たな技術が登場し、そういった最先端の人工知能が何かと話題になりますが、ゲーム用のAIの多くは、いくつかの計算の組み合わせや、古くから知られる手法などを用いて、高度なプログラミングを行わずとも、実用に値するものが作れるのです。
三目並べは紙に鉛筆で書いて遊ぶ単純なゲームですが、そのようなシンプルなゲームにも、コンピュータで表現するときに、少し工夫して見た目の楽しさも加えると良いと思います。
また、アルゴリズムを知れば、もっとゲームに難易度をつけたり、ルールを加えたりすることができます。著書としては問題解決のための「アルゴリズム×数学」が基礎からしっかり身につく本 [ 米田優峻 ]がお勧めです。数学が苦手な文系の方にこそ読んでいただきたい一冊です。