このページでは、tkinter を用いたオセロ(リバーシ)アプリの作り方と、そのオセロアプリのスクリプトの紹介をしていきたいと思います。
Contents
作成するオセロアプリ
まず今回作成するオセロアプリがどのようなものであるかについて解説します。
オセロアプリの UI
今回作成するオセロアプリの見た目は下の図のようになります。
スポンサーリンク
オセロアプリの遊び方
黄色のマスが次に石を置けるマスを表しています。
この黄色のマスをマウスでクリックすることでそのマスに石が置かれ、挟んだ位置の相手の石が裏返ります。
石を置くと、次はコンピュータが石を置く番になり、同様にコンピュータが石を置いて石が裏返ると、次はあなたの番になります。
これを繰り返し、両者の石を置けるマスがなくなったらゲーム終了です。
この際には、最終的な石の数をカウントし、その結果がメッセージボックス上に表示されます。
オセロアプリの作り方
続いてこのオセロアプリの作り方を解説していきます。
オセロアプリを作成する時にポイントになるのは下記の4つだと思います。
- オセロの画面の実現
- 石が置けるマスかどうかの判断の実現
- 石を置く処理の実現
- 盤面上の石の管理
ですので、ここでは、このポイント4つについて説明していきたいと思います。
他の点も含めた詳細な解説は、スクリプトを確認しながら最後のスクリプトの解説で行いたいと思います。
オセロの画面の実現
1つ目のポイントはオセロの画面の実現です。
今回作成するアプリでは、キャンバスを利用してこのオセロの画面の実現をしたいと思います。
マスに関しては、キャンバス上に正方形を描画することで表現します。この正方形は、キャンバスに用意された create_rectangle
メソッドにより描画することができます。
また石に関しては、キャンバス上に円を描画することで実現します。この円は、キャンバスに用意された create_oval
メソッドにより描画することができます。
特にこのオセロアプリでポイントになるのが描画した図形の “色” です。
オセロでは石の色でどのプレイヤーの石であるかを区別します。つまり、どのプレイヤーが置いた石かどうかによって描画する円の塗りつぶし色を切り替える必要があります。
この「描画時の図形の塗りつぶし色の変更」は、create_rectangle
や create_oval
実行時に fill
オプションを指定することで実現することができます。
また、オセロでは石が挟まれた時に石が裏返って色が変化します。つまり、石が裏返しされた際にはその石を表現している円の塗りつぶし色を変更する必要があります。
さらに、今回のアプリでは、プレイヤーが次に石を置けるマスが分かるように、次に石が置けるマスの色を他のマスと違う色にするようにします。
石が置かれるたびに次に石が置けるマスの位置が変わりますので、石が置かれるたびにマスを表現する正方形の塗りつぶし色を変更する必要があります。
このような「描画後の図形の塗りつぶし色の変更」は、キャンバスクラスの itemconfig
メソッドにより fill
オプションを変更することで実現できます。
ただし、itemconfig
メソッドの第1引数では、”どの図形” の fill
オプションを変更するかを指定する必要があります。この指定を行うために、マスを表現する正方形や石を表現する円を描画する際にはタグ付けを行うようにし、itemconfig
メソッドの第1引数にはこのタグを指定するようにしています。
この辺りのキャンバスへの図形の描画(create_rectangle
や create_oval
など)や描画した図形への操作(itemconfig
など)については下記のページで解説していますので、詳しく知りたい方はこちらを是非読んでみてください。
スポンサーリンク
石が置けるマスかどうかの判断の実現
またオセロアプリにおいては、プレイヤーが石を置こうとしたマスが「次に石が置けるマス」であるかどうかを判断する必要があります。これは、プレイヤーがルールに従わずに石を置くことができないように制御するためです。
石が置けるマスとは
オセロのルール上、「次に石が置けるマス」は下記の3つを満たすマスになります。
- 盤面内のマス
- 石が置かれていないマス
- 石を置くことで相手の石を裏返すことができるマス
1点目と2点目に関してはそのままの意味なので説明不要だと思います。
3点目に関してはちょっと複雑なので、ここで詳細を解説しておきます。
石を置くことで相手の石を裏返すことができるマスとは
3点目の「石を置くことで相手の石を裏返すことができるマス」とは、より具体的にいうと、「上・右上・右・右下・下・左下・左・左上」の8方向のいずれかに関して下記の3つの条件が成立するマスになります。
- その方向の隣のマスに相手の石が置かれている
- その方向の隣のマス以外に自分の石が置かれている
- ↑ の自分の石からそのマスまでの間に “石が置かれていないマス” が存在しない
例えば自分の石の色が黒色、相手の石の色が白色とした時に、下の図のオレンジ色のマスに石が置けるかどうかを考えてみましょう。
「上・右上・右・右下・下・左下・左・左上」の8方向において、上記の3つの条件の成立・不成立を示すと次のようになります(2. が不成立の場合は 3. も不成立にしています)。
- 上 :1. 成立・2. 成立・3. 不成立
- 右上:1. 不成立・2. 成立・3. 成立
- 右 :1. 成立・2. 成立・3. 不成立
- 右下:1. 不成立・2. 不成立・3. 不成立
- 下 :1. 成立・2. 不成立・3. 不成立
- 左下:1. 成立・2. 成立・3. 成立
- 左 :1. 不成立・2. 不成立・3. 不成立
- 左上:1. 不成立・2. 不成立・3. 不成立
左下方向においては条件が3つとも成立しています。8方向のうちいずれか1方向に関して成立すれば良いので、このオレンジ色のマスには “石が置ける” ということになります。
逆に下の図のオレンジ色のマスのように8方向全ての方向で不成立の場合は、そのマスには石を置くことができません。
石を置く処理の実現
さらに、オセロアプリを作成する上では「石を置く」処理を実現する必要があります。
オセロにおいては石を置いた場合は “相手の石を裏返す” ことになるので、石を置く際には下記の2つの処理を行う必要があります。
- 相手の石を裏返す(石を置いた際に裏返すことのできる相手の石を裏返す)
- 石を置く
相手の石を裏返す
石が置かれる際には、その石を置くことで挟んだ相手の石を裏返す処理を行います。
挟んだ相手の石とはつまり、プレイヤーが石を置くマスから「上・右上・右・右下・下・左下・左・左上」の各方向に対して下記を満たす相手の石となります。
- プレイヤーが石を置くマスから “一番近いプレイヤーの石” までに “連続して” 置かれた相手の石
例えば石を置くプレイヤーの石の色が黒色、相手の石の色が白色として、下の図の石の配置でプレイヤーがオレンジ色のマスに石を置くとしましょう。
この時、左方向ではプレイヤーが石を置くマスから “一番近いプレイヤーの石” までに相手の石が “連続して” 置かれているのでその間の石を裏返すことができます。
一方で、右方向ではプレイヤーが石を置くマスから “一番近いプレイヤーの石” までに相手の石が “連続して” 置かれていないので、石を裏返すことができません。
また、右上方向では石を置いた場合に裏返される石は下の図のように1つのみになります。
右上方向にはプレイヤーの石が他にも置かれていますが、あくまでも裏返されるのは “一番近いプレイヤーの石” までの相手の石のみです。それよりも遠い位置にプレイヤーの石があっても、その間の石が裏返されるというわけではないという点に注意が必要です。
また前述の通り、石の裏返しは今回のアプリでは、キャンバスクラスの itemconfig
メソッドにより fill
オプションを変更することで実現します。
石を置く
相手の石の裏返しが完了したら、あとは指定されたマスに実際に石を置く処理を行います。
前述の通り、石を置く処理はキャンバス上に円を描画することで実現します。より具体的にはキャンバスに用意された create_oval
メソッドを実行して円を描画することで盤面上に石を置きます。
盤面上の石の管理
石が置けるマスかどうかの判断の実現や石を置く処理の実現で解説したように、これらを実現するためには盤面上のマスの状態、つまり「どのマスにどの色の石(どのプレイヤーの石)が置かれているか」を参照できるようにしておかないといけません。
今回は盤面上のマスの状態を2次元リスト board
で管理します。
より具体的には board[y][x]
には盤面上の座標 (x
, y
) に置かれている石の色を格納するようにし、これを参照しながら石が置けるマスかどうかの判断や石を置く処理を行うようにします。
例えば下の図のような盤面の状態であれば、
リストの中身は下記のようになります(None
は石が置かれていないことを示しています)。
[['black', None], ['white', 'black']]
下記の処理を行う際には、盤面上のマスの状態が変化する(置かれている石が増えたり石の色が変化したりする)ため、これらの処理に合わせて board
のリストも更新する必要があります。
- 石を置く
- 相手の石を裏返す
スポンサーリンク
オセロアプリのスクリプト
下記がオセロアプリのサンプルスクリプトになります。
# -*- coding:utf-8 -*-
import tkinter
import tkinter.messagebox
# キャンバスの横方向・縦方向のサイズ(px)
CANVAS_SIZE = 400
# 横方向・縦方向のマスの数
NUM_SQUARE = 8
# 色の設定
BOARD_COLOR = 'green' # 盤面の背景色
YOUR_COLOR = 'black' # あなたの石の色
COM_COLOR = 'white' # 相手の石の色
PLACABLE_COLOR = 'yellow' # 次に石を置ける場所を示す色
# プレイヤーを示す値
YOU = 1
COM = 2
class Othello():
def __init__(self, master):
'''コンストラクタ'''
self.master = master # 親ウィジェット
self.player = YOU # 次に置く石の色
self.board = None # 盤面上の石を管理する2次元リスト
self.color = { # 石の色を保持する辞書
YOU : YOUR_COLOR,
COM : COM_COLOR
}
# ウィジェットの作成
self.createWidgets()
# イベントの設定
self.setEvents()
# オセロゲームの初期化
self.initOthello()
def createWidgets(self):
'''ウィジェットを作成・配置する'''
# キャンバスの作成
self.canvas = tkinter.Canvas(
self.master,
bg=BOARD_COLOR,
width=CANVAS_SIZE+1, # +1は枠線描画のため
height=CANVAS_SIZE+1, # +1は枠線描画のため
highlightthickness=0
)
self.canvas.pack(padx=10, pady=10)
def setEvents(self):
'''イベントを設定する'''
# キャンバス上のマウスクリックを受け付ける
self.canvas.bind('<ButtonPress>', self.click)
def initOthello(self):
'''ゲームの初期化を行う'''
# 盤面上の石を管理する2次元リストを作成(最初は全てNone)
self.board = [[None] * NUM_SQUARE for i in range(NUM_SQUARE)]
# 1マスのサイズ(px)を計算
self.square_size = CANVAS_SIZE // NUM_SQUARE
# マスを描画
for y in range(NUM_SQUARE):
for x in range(NUM_SQUARE):
# 長方形の開始・終了座標を計算
xs = x * self.square_size
ys = y * self.square_size
xe = (x + 1) * self.square_size
ye = (y + 1) * self.square_size
# 長方形を描画
tag_name = 'square_' + str(x) + '_' + str(y)
self.canvas.create_rectangle(
xs, ys,
xe, ye,
tag=tag_name
)
# あなたの石の描画位置を計算
your_init_pos_1_x = NUM_SQUARE // 2
your_init_pos_1_y = NUM_SQUARE // 2
your_init_pos_2_x = NUM_SQUARE // 2 - 1
your_init_pos_2_y = NUM_SQUARE // 2 - 1
your_init_pos = (
(your_init_pos_1_x, your_init_pos_1_y),
(your_init_pos_2_x, your_init_pos_2_y)
)
# 計算した描画位置に石(円)を描画
for x, y in your_init_pos:
self.drawDisk(x, y, self.color[YOU])
# 対戦相手の石の描画位置を計算
com_init_pos_1_x = NUM_SQUARE // 2 - 1
com_init_pos_1_y = NUM_SQUARE // 2
com_init_pos_2_x = NUM_SQUARE // 2
com_init_pos_2_y = NUM_SQUARE // 2 - 1
com_init_pos = (
(com_init_pos_1_x, com_init_pos_1_y),
(com_init_pos_2_x, com_init_pos_2_y)
)
# 計算した描画位置に石(円)を描画
for x, y in com_init_pos:
self.drawDisk(x, y, self.color[COM])
# 最初に置くことができる石の位置を取得
placable = self.getPlacable()
# その位置を盤面に表示
self.showPlacable(placable)
def drawDisk(self, x, y, color):
'''(x,y)に色がcolorの石を置く(円を描画する)'''
# (x,y)のマスの中心座標を計算
center_x = (x + 0.5) * self.square_size
center_y = (y + 0.5) * self.square_size
# 中心座標から円の開始座標と終了座標を計算
xs = center_x - (self.square_size * 0.8) // 2
ys = center_y - (self.square_size * 0.8) // 2
xe = center_x + (self.square_size * 0.8) // 2
ye = center_y + (self.square_size * 0.8) // 2
# 円を描画する
tag_name = 'disk_' + str(x) + '_' + str(y)
self.canvas.create_oval(
xs, ys,
xe, ye,
fill=color,
tag=tag_name
)
# 描画した円の色を管理リストに記憶させておく
self.board[y][x] = color
def getPlacable(self):
'''次に置くことができる石の位置を取得'''
placable = []
for y in range(NUM_SQUARE):
for x in range(NUM_SQUARE):
# (x,y) の位置のマスに石が置けるかどうかをチェック
if self.checkPlacable(x, y):
# 置けるならその座標をリストに追加
placable.append((x, y))
return placable
def checkPlacable(self, x, y):
'''(x,y)に石が置けるかどうかをチェック'''
# その場所に石が置かれていれば置けない
if self.board[y][x] != None:
return False
if self.player == YOU:
other = COM
else:
other = YOU
# (x,y)座標から縦横斜め全方向に対して相手の意思が裏返せるかどうかを確認
for j in range(-1, 2):
for i in range(-1, 2):
# 真ん中方向はチェックしてもしょうがないので次の方向の確認に移る
if i == 0 and j == 0:
continue
# その方向が盤面外になる場合も次の方向の確認に移る
if x + i < 0 or x + i >= NUM_SQUARE or y + j < 0 or y + j >= NUM_SQUARE:
continue
# 隣が相手の色でなければその方向に石を置いても裏返せない
if self.board[y + j][x + i] != self.color[other]:
continue
# 置こうとしているマスから遠い方向へ1マスずつ確認
for s in range(2, NUM_SQUARE):
# 盤面外のマスはチェックしない
if x + i * s >= 0 and x + i * s < NUM_SQUARE and y + j * s >= 0 and y + j * s < NUM_SQUARE:
if self.board[y + j * s][x + i * s] == None:
# 自分の石が見つかる前に空きがある場合
# この方向の石は裏返せないので次の方向をチェック
break
# その方向に自分の色の石があれば石が裏返せる
if self.board[y + j * s][x + i * s] == self.color[self.player]:
return True
# 裏返せる石がなかったので(x,y)に石は置けない
return False
def showPlacable(self, placable):
'''placableに格納された次に石が置けるマスの色を変更する'''
for y in range(NUM_SQUARE):
for x in range(NUM_SQUARE):
# fillを変更して石が置けるマスの色を変更
tag_name = 'square_' + str(x) + '_' + str(y)
if (x, y) in placable:
self.canvas.itemconfig(
tag_name,
fill=PLACABLE_COLOR
)
else:
self.canvas.itemconfig(
tag_name,
fill=BOARD_COLOR
)
def click(self, event):
'''盤面がクリックされた時の処理'''
if self.player != YOU:
# COMが石を置くターンの時は何もしない
return
# クリックされた位置がどのマスであるかを計算
x = event.x // self.square_size
y = event.y // self.square_size
if self.checkPlacable(x, y):
# 次に石を置けるマスであれば石を置く
self.place(x, y, self.color[self.player])
def place(self, x, y, color):
'''(x,y)に色がcolorの石を置く'''
# (x,y)に石が置かれた時に裏返る石を裏返す
self.reverse(x, y)
# (x,y)に石を置く(円を描画する)
self.drawDisk(x, y, color)
# 次に石を置くプレイヤーを決める
before_player = self.player
self.nextPlayer()
if before_player == self.player:
# 前と同じプレイヤーであればスキップされたことになるのでそれを表示
if self.player != YOU:
tkinter.messagebox.showinfo('結果', 'あなたのターンをスキップしました')
else:
tkinter.messagebox.showinfo('結果', 'COMのターンをスキップしました')
elif not self.player:
# 次に石が置けるプレイヤーがいない場合はゲーム終了
self.showResult()
return
# 次に石がおける位置を取得して表示
placable = self.getPlacable()
self.showPlacable(placable)
if self.player == COM:
# 次のプレイヤーがCOMの場合は1秒後にCOMに石を置く場所を決めさせる
self.master.after(1000, self.com)
def reverse(self, x, y):
'''(x,y)に石が置かれた時に裏返す必要のある石を裏返す'''
if self.board[y][x] != None:
# (x,y)にすでに石が置かれている場合は何もしない
return
if self.player == COM:
other = YOU
else:
other = COM
for j in range(-1, 2):
for i in range(-1, 2):
# 真ん中方向はチェックしてもしょうがないので次の方向の確認に移る
if i == 0 and j == 0:
continue
if x + i < 0 or x + i >= NUM_SQUARE or y + j < 0 or y + j >= NUM_SQUARE:
continue
# 隣が相手の色でなければその方向で裏返せる石はない
if self.board[y + j][x + i] != self.color[other]:
continue
# 置こうとしているマスから遠い方向へ1マスずつ確認
for s in range(2, NUM_SQUARE):
# 盤面外のマスはチェックしない
if x + i * s >= 0 and x + i * s < NUM_SQUARE and y + j * s >= 0 and y + j * s < NUM_SQUARE:
if self.board[y + j * s][x + i * s] == None:
# 自分の石が見つかる前に空きがある場合
# この方向の石は裏返せないので次の方向をチェック
break
# その方向に自分の色の石があれば石が裏返せる
if self.board[y + j * s][x + i * s] == self.color[self.player]:
for n in range(1, s):
# 盤面の石の管理リストを石を裏返した状態に更新
self.board[y + j * n][x + i * n] = self.color[self.player]
# 石の色を変更することで石の裏返しを実現
tag_name = 'disk_' + str(x + i * n) + '_' + str(y + j * n)
self.canvas.itemconfig(
tag_name,
fill=self.color[self.player]
)
break
def nextPlayer(self):
'''次に石を置くプレイヤーを決める'''
before_player = self.player
# 石を置くプレイヤーを切り替える
if self.player == YOU:
self.player = COM
else:
self.player = YOU
# 切り替え後のプレイヤーが石を置けるかどうかを確認
placable = self.getPlacable()
if len(placable) == 0:
# 石が置けないのであればスキップ
self.player = before_player
# スキップ後のプレイヤーが石を置けるかどうかを確認
placable = self.getPlacable()
if len(placable) == 0:
# それでも置けないのであれば両者とも石を置けないということ
self.player = None
def showResult(self):
'''ゲーム終了時の結果を表示する'''
# それぞれの色の石の数を数える
num_your = 0
num_com = 0
for y in range(NUM_SQUARE):
for x in range(NUM_SQUARE):
if self.board[y][x] == YOUR_COLOR:
num_your += 1
elif self.board[y][x] == COM_COLOR:
num_com += 1
# 結果をメッセージボックスで表示する
tkinter.messagebox.showinfo('結果', 'あなた' + str(num_your) + ':COM' + str(num_com))
def com(self):
'''COMに石を置かせる'''
# 石が置けるマスを取得
placable = self.getPlacable()
# 最初のマスを次に石を置くマスとする
x, y = placable[0]
# 石を置く
self.place(x, y, COM_COLOR)
# スクリプト処理ここから
app = tkinter.Tk()
app.title('othello')
othello = Othello(app)
app.mainloop()
スクリプトを実行すれば作成するオセロアプリで紹介したようなアプリが起動し、オセロをプレイすることができます。
対戦相手はコンピュータで、あなたが石を置いた1秒後くらいにコンピュータが石を置くようになっています。
スクリプトの解説
続いてスクリプトの解説をしていきたいと思います。
このスクリプトではほぼ Othello
クラスのインスタンスが処理を行うようになっていますので、Othello
クラスのメソッドごとに解説をしていきたいと思います。
__init__
__init__
はコンストラクタです。これを実行することで Othello
クラスのインスタンスが生成され、さらに必要なウィジェットの作成やイベントの受付、オセロゲームの初期化まで実行します。
__init__
では下記の属性の初期化も行っています。以降で解説する各メソッドでは、必要に応じてこれらの属性を参照・更新しながらオセロゲームを実現しています。
master
:オセロアプリの作成先となるウィジェット(基本的にメインウィンドウ)board
:各マスに置かれている石の色を管理する二次元リストplayer
:次に石を置くプレイヤーcolor
:各プレイヤーの石の色
スポンサーリンク
createWidgets
createWidgets
はオセロアプリに必要なウィジェットの作成と配置を行うメソッドになります。
今回はキャンバスを一つのみ作成して配置するようにしています。
ここで作成するキャンバスのサイズはスクリプト先頭部分の下記で設定可能です。
# キャンバスの横方向・縦方向のサイズ(px)
CANVAS_SIZE = 400
キャンバスウィジェットの作り方を詳しく知りたい方は是非下記ページを参考にしていただければと思います。
Tkinterの使い方:キャンバスウィジェットの作り方setEvents
setEvents
はオセロアプリに必要なイベント受付の設定を行うメソッドです。
今回はキャンバス上でのマウスのクリックイベントのみを受け付け、このイベントが発生した際に後述する click
メソッドを実行するように設定しています。
イベントについて詳しく知りたい方は是非下記ページを参考にしていただければと思います。
Tkinterの使い方:イベント処理を行うinitOthello
initOthello
はオセロの初期化を行うメソッドです。
このメソッドでは大きく分けて下記の5つの処理を行なっています。
- 盤面管理リスト
board
の初期化 - マスのサイズの計算
- マスの描画
- 初期状態の石の描画
- 石が置ける場所の表示
盤面管理リスト board
の初期化
まずはオセロの盤面に一つも石が置かれていない状態を表現するため、board
の全ての要素を None
に設定します。
# 盤面上の石を管理する2次元リストを作成(最初は全てNone)
self.board = [[None] * NUM_SQUARE for i in range(NUM_SQUARE)]
以降では、下記の場合にこの board
を更新しながら盤面上の石を管理していくことになります。
- 石が置かれる
- 石が裏返される
マスのサイズの計算
続いて、各マスのサイズ(幅と高さ)の計算を行います。
キャンバスいっぱいにマスが配置されるように、各マスのサイズは下記でキャンバスのサイズとマスの数から計算しています。
# 1マスのサイズ(px)を計算
self.square_size = CANVAS_SIZE // NUM_SQUARE
また、ここで参照している縦方向&横方向マスの数はスクリプト先頭部分の下記で設定可能です。
# 横方向・縦方向のマスの数
NUM_SQUARE = 8
このマスのサイズ self.square_size
は、マスの描画を行う時やマウスでどのマスがクリックされたかを判断するときなどに参照します。
マスの描画
続いて、各マスを表現するためにキャンバス上に長方形をマスの分だけ描画します。
前述の通り、マスはキャンバス上に正方形を描画することで表現します。
各マスのサイズは self.square_size
になりますので、各マスをこのサイズ分ずらしながら2次元的に配置されるように座標を計算しながら長方形を描画します。
# マスを描画
for y in range(NUM_SQUARE):
for x in range(NUM_SQUARE):
# 長方形の開始・終了座標を計算
xs = x * self.square_size
ys = y * self.square_size
xe = (x + 1) * self.square_size
ye = (y + 1) * self.square_size
# 長方形を描画
tag_name = 'square_' + str(x) + '_' + str(y)
self.canvas.create_rectangle(
xs, ys,
xe, ye,
tag=tag_name
)
ここでのポイントは create_rectangle
実行時に tag
の設定を行なっていることです。tag
の設定を行なっておくことで、後から描画した図形に tag
を指定して参照したりオプションの変更を行うようなことが可能になります。
さらに、tag
に座標の情報を含ませておくことで、座標から tag
を辿ることができるようになり、後から図形への参照やオプションの変更を行うことが簡単になります。
tag_name = 'square_' + str(x) + '_' + str(y)
本アプリにおいては、この tag
を後からマスの色を変化させることに利用します。
初期状態の石の描画
オセロでは下の図のように黒色の石と白色の石が2つずつ置かれた状態でゲームが開始されます。
本アプリでは、この石をマス上の円により表現しますので、ゲーム初期化時にこれらの位置への円の描画を行います。この円の描画は後述で紹介する drawDisk
メソッドで実行できるようにしています。
なので、この initOthello
で行うことは、上の図のように石が配置されるようにマスの座標の計算とそのマスに円を描画するように drawDisk
メソッドを実行する処理だけになります。
# あなたの石の描画位置を計算
your_init_pos_1_x = NUM_SQUARE // 2
your_init_pos_1_y = NUM_SQUARE // 2
your_init_pos_2_x = NUM_SQUARE // 2 - 1
your_init_pos_2_y = NUM_SQUARE // 2 - 1
your_init_pos = (
(your_init_pos_1_x, your_init_pos_1_y),
(your_init_pos_2_x, your_init_pos_2_y)
)
# 計算した描画位置に石(円)を描画
for x, y in your_init_pos:
self.drawDisk(x, y, self.color[YOU])
# 対戦相手の石の描画位置を計算
com_init_pos_1_x = NUM_SQUARE // 2 - 1
com_init_pos_1_y = NUM_SQUARE // 2
com_init_pos_2_x = NUM_SQUARE // 2
com_init_pos_2_y = NUM_SQUARE // 2 - 1
com_init_pos = (
(com_init_pos_1_x, com_init_pos_1_y),
(com_init_pos_2_x, com_init_pos_2_y)
)
# 計算した描画位置に石(円)を描画
for x, y in com_init_pos:
self.drawDisk(x, y, self.color[COM])
石が置ける場所の表示
石が置かれたあとは、次に石が置けるマスが分かるように、「次に石が置けるマス表示する」処理を行います。より具体的には次に石が置けるマスの色を変化させます(上記のスクリプトでは黄色に変化させています)。
次に石が置けるマスは、後述する getPlacable
メソッドで取得できるようにしていますし、その石が置けるマスの色を変させる処理は showPlacable
メソッドで実行できるようにしています。
したがって、この initOthello
では下記のように getPlacable
メソッドと showPlacable
メソッドの実行だけを行なっています。
# 最初に置くことができる石の位置を取得
placable = self.getPlacable()
# その位置を盤面に表示
self.showPlacable(placable)
以上により、オセロの盤面上に黄色のマスが表示され、どのマスに石が置けるかをプレイヤーに伝えることができます。
スポンサーリンク
drawDisk
drawDisk
は円をキャンバス上に描画するメソッドです。前述の通りこの円がオセロの石を表現します。
drawDisk
では引数で指定されたマスの座標 (x
, y
) に色が color
の円を create_oval
メソッドで描画します。
円描画時のポイントは create_oval
メソッドにマスの座標ではなくキャンバス上の座標を指定する必要があることと、円の開始座標と終了座標を指定する必要があるところだと思います。
そのため、まずは下記で座標 (x
, y
) のマスの中心座標(キャンバス上の座標)を計算しています。
# (x,y)のマスの中心座標を計算
center_x = (x + 0.5) * self.square_size
center_y = (y + 0.5) * self.square_size
さらに、その中心座標から円の開始座標と終了座標を計算しています。
# 中心座標から円の開始座標と終了座標を計算
xs = center_x - (self.square_size * 0.8) // 2
ys = center_y - (self.square_size * 0.8) // 2
xe = center_x + (self.square_size * 0.8) // 2
ye = center_y + (self.square_size * 0.8) // 2
そして、この円の開始座標と終了座標、そして引数で指定される石の色 color
をオプションに指定して create_oval
を実行して円を描画しています。
# 円を描画する
tag_name = 'disk_' + str(x) + '_' + str(y)
self.canvas.create_oval(
xs, ys,
xe, ye,
fill=color,
tag=tag_name
)
開始座標と終了座標計算時に指定している 0.8
は円のサイズの調整値です。この値を 1
にすればマスに接するように円が描画されますし、もっと小さくすれば描画される円のサイズも小さくなります。
また、マス描画時と同様に、後から円の色を変化させることができるように(石の裏返し時に利用)、create_oval
実行時に tag
の設定も行っています。
この tag
も座標から辿れるように、座標の情報を含ませたものを指定するようにしています。
tag_name = 'disk_' + str(x) + '_' + str(y)
さらに、円を描画することでそのマスに石が置かれたことになりますので、盤面上の石を管理する board
を下記で更新しています((x
, y
) 上に色が color
の石が置かれたことを記録しておく)。
# 描画した円の色を管理リストに記憶させておく
self.board[y][x] = color
getPlacable
getPlacable
メソッドは、次に石を置くプレイヤーが置くことが可能なマスのリストを取得するメソッドです。
といっても、下記のようにマス数分のループの中で次に説明する chekckPlacable
メソッドを実行し、checkPlacable
メソッドの戻り値が True
の時(そのマスに石が置ける場合)にそのマスの座標 (x
, y
) をリスト placable
に記録させているだけになります。
if self.checkPlacable(x, y):
# 置けるならその座標をリストに追加
placable.append((x, y))
checkPlacable
getPlacable
メソッドから実行されているこの checkPlacable
が、引数で指定された (x
, y
) 座標のマスに石が置けるかどうかを判断するメソッドになります。
この判断のやり方は石が置けるマスかどうかの判断の実現で解説していますので、この解説内容と合わせてスクリプトを読んでいただければ何をやっているかが分かりやすいと思います。
特に「石を置くことで相手の石を裏返すことができるマス」とは、「上・右上・右・右下・下・左下・左・左上」の8方向のいずれかに関して下記の3つの条件が成立するマスでしたね!
- その方向の隣のマスに相手の石が置かれている
- その方向の隣のマス以外に自分の石が置かれている
- ↑ の自分の石からそのマスまでの間に “石が置かれていないマス” が存在しない
各方向に対してこの3つの条件が成立するかどうかを判断しているのが下記のループ内の処理になります。
for j in range(-1, 2):
for i in range(-1, 2):
このループの i
と j
が方向を示すパラメータになります。より具体的にいうと、各方向は下記の i
と j
の組み合わせにより表現しています。
- 左上:
i = -1
、j = -1
- 上 :
i = 0
、j = -1
- 右上:
i = 1
、j = -1
- 左 :
i = -1
、j = 0
- 右 :
i = 1
、j = 0
- 左下:
i = -1
、j = 1
- 上 :
i = 0
、j = 1
- 右下:
i = 1
、j = 1
したがって、(x
, y
) 座標の隣のマスに置かれているのが相手の石かどうかは、下記のように board
を参照して判断することができます。つまり、これにより条件 1. が成立するかどうかを判断することができます(条件 1. が成立しない場合は continue
で次の方向の判断に移ります)。
# 隣が相手の色でなければその方向に石を置いても裏返せない
if self.board[y + j][x + i] != self.color[other]:
continue
さらに i
と j
それぞれに正数を乗算することで、その方向のマスに置かれている石の色を board
から取得することが可能です。乗算した値分、(x
, y
) 座標のマスから i
と j
の方向に離れた位置のマスの石の色を取得することができます。
これを利用して、下記により条件 2. と条件 3. が成立するかどうかを判断しています。
# 置こうとしているマスから遠い方向へ1マスずつ確認
for s in range(2, NUM_SQUARE):
# 盤面外のマスはチェックしない
if x + i * s >= 0 and x + i * s < NUM_SQUARE and y + j * s >= 0 and y + j * s < NUM_SQUARE:
if self.board[y + j * s][x + i * s] == None:
# 自分の石が見つかる前に空きがある場合
# この方向の石は裏返せないので次の方向をチェック
break
# その方向に自分の色の石があれば石が裏返せる
if self.board[y + j * s][x + i * s] == self.color[self.player]:
return True
全て成立する場合は上記で return True
が実行されて、座標 (x
, y
) に石が置けることをメソッドの呼び出し元に伝えて関数を終了します。
どの方向でも3つの条件が成立しない場合は石が置けないことになるので、メソッドの最後で False
を返却して関数を終了します。
スポンサーリンク
showPlacable
showPlacable
は引数で指定されたリスト placable
に含まれる座標のマスの色を変化させるメソッドになります。
やっていることは単純で、各マスの座標が placable
に含まれているかどうかを確認し、含まれている場合は itemconfig
メソッドで fill
オプションを PLACABLE_COLOR
に、それ以外は fill
オプションを BOARD_COLOR
に設定してマスの色を変更しているだけです。
# fillを変更して石が置けるマスの色を変更
tag_name = 'square_' + str(x) + '_' + str(y)
if (x, y) in placable:
self.canvas.itemconfig(
tag_name,
fill=PLACABLE_COLOR
)
else:
self.canvas.itemconfig(
tag_name,
fill=BOARD_COLOR
)
この itemconfig
メソッドの第1引数は描画済みの図形の ID もしくは tag
になります。initOthello で解説したように、座標 (x
, y
) のマスを描画する時には square_x_y
という風に座標の情報を含ませたタグを指定するようにしていますので、座標からタグ名を生成して itemconfig
に設定するようにしています。
tag_name = 'square_' + str(x) + '_' + str(y)
また、fill
オプションに指定するマスの色はスクリプト先頭部分の下記で設定することが可能です。
# 色の設定
BOARD_COLOR = 'green' # 盤面の背景色
PLACABLE_COLOR = 'yellow' # 次に石を置ける場所を示す色
click
本アプリではマスをマウスでクリックすることで石を置けるようにしています。
そのマウスクリック時に実行されるのがこの click
メソッドになります。
まず click
メソッドではマウスでクリックされたキャンバス上の座標が、どのマス上の座標であるかを計算しています。
マスのサイズは self.square_size
で、座標 (0
, 0
) からマスを描画していっているので、単純に下記のように割り算でマスの座標は計算可能です。
# クリックされた位置がどのマスであるかを計算
x = event.x // self.square_size
y = event.y // self.square_size
event.x
と event.y
はマウスでクリックされたキャンバス上の座標になります。
マスの座標を計算後、そのマスに石が置けるかどうかを前述で解説した checkPlacable
で判断し、石が置ける場合のみ次に説明する place
メソッドを実行しています。
place
place
メソッドでは石を置く処理及び、次に石を置くプレイヤーの決定を行います。
石が置かれた場合には必ず相手の石が裏返されることになりますので、まずは次に説明する reverse
メソッドを実行し、さらに続いて drawDisk
メソッドを実行して実際に石をキャンバス上に描画しています。
# (x,y)に石が置かれた時に裏返る石を裏返す
self.reverse(x, y)
# (x,y)に石を置く(円を描画する)
self.drawDisk(x, y, color)
さらに、後述で解説する nextPlayer
メソッドを実行して次に石を置くプレイヤーを決定しています。基本は石を置くプレイヤーは交互に入れ替わりますが、石を置くことができるマスがない場合、そのプレイヤーが石を置く番はスキップされることになります。この辺りの判断を行なっているのが nextPlayer
です。
nextPlayer
メソッド実行前と nextPlayer
メソッド実行後で self.player
が同じ場合は、同じプレイヤーが連続して石を置くことになります。
つまり、プレイヤーもしくは対戦相手の番がスキップされたことになるので、この場合は下記でメッセージボックスでスキップされたことを表示するようにしています。
if before_player == self.player:
# 前と同じプレイヤーであればスキップされたことになるのでそれを表示
if self.player != YOU:
tkinter.messagebox.showinfo('結果', 'あなたのターンをスキップしました')
else:
tkinter.messagebox.showinfo('結果', 'COMのターンをスキップしました')
また、nextPlayer
メソッド実行後の self.player
が None
の場合は、もう石を置くことのできるプレイヤーがいないことになりますので、後述で解説する showResult
メソッドを実行してゲームの結果を表示するようにしています。
elif not self.player:
# 次に石が置けるプレイヤーがいない場合はゲーム終了
self.showResult()
return
最後に、もし次のプレイヤーがコンピュータ(COM)の場合は after
メソッドにより 1
秒後に com
メソッドを実行してコンピュータに石を置かせる処理を行うようにしています。
if self.player == COM:
# 次のプレイヤーがCOMの場合は1秒後にCOMに石を置く場所を決めさせる
self.master.after(1000, self.com)
スポンサーリンク
reverse
reverse
メソッドでは引数で指定された座標 (x
, y
) のマスに石が置かれた際の石を裏返す処理を行います。
処理の大枠は getPlacable
とほとんど同じだと思います。
ただ、getPlacable
とは異なり、reverse
メソッドでは石の裏返しを行う必要があるのでその点が異なります。
この裏返しを行なっているのが下記部分になります。
for n in range(1, s):
# 盤面の石の管理リストを石を裏返した状態に更新
self.board[y + j * n][x + i * n] = self.color[self.player]
# 石の色を変更することで石の裏返しを実現
tag_name = 'disk_' + str(x + i * n) + '_' + str(y + j * n)
self.canvas.itemconfig(
tag_name,
fill=self.color[self.player]
)
break
要は、石が裏返すことができると判断した i
、j
の方向及び x
, y
座標からの距離 s
に対し、(x + i * 1
, y + j * 1
) から (x + i * (s-1)
, y + j * (s-1)
) の座標の石の裏返しを行なっています。
石の裏返しとは、要は石の色を相手の石のものからプレイヤーの石のものに変更することなので、上記のように itemcget
メソッドを利用して fill
オプションの変更、つまり石の色の変更を行なうことで石の裏返しを実現しています。
また、石の裏返しにより盤面上の石の色が変化しますので、それに合わせて盤面上の石を管理する board
の更新も行っています。
self.board[y + j * n][x + i * n] = self.color[self.player]
nextPlayer
nextPlayer
は次に石を置くプレイヤーを決定するメソッドになります。
オセロでは、基本的にプレイヤーは交互に入れ替わることになりますが、石が置けない場合などはスキップされるような場合もあります。この辺りを判断して次に石を置くプレイヤーを決定するのがこのメソッドになります。
まず現在のプレイヤーから相手側のプレイヤーに変更した際に “石を置けるマスがあるかどうか” を確認します。この確認は getPlacable
メソッドで石を置くことのできるマスの座標のリストを取得し、その座標のリストの長さを調べることで行うことができます(長さが 0
なら石を置くことのできるマスがない)。
before_player = self.player
# 石を置くプレイヤーを切り替える
if self.player == YOU:
self.player = COM
else:
self.player = YOU
# 切り替え後のプレイヤーが石を置けるかどうかを確認
placable = self.getPlacable()
if len(placable) == 0:
# 石が置けないのであればスキップ
self.player = before_player
石が置けるマスがあるのであれば、次のプレイヤーは相手側のプレイヤーに変更になります。
もし次のプレイヤーが石を置くことのできるマスがないのであれば、再度元々のプレイヤーに戻して石が置けるマスがあるかどうかを確認します。
# スキップ後のプレイヤーが石を置けるかどうかを確認
placable = self.getPlacable()
if len(placable) == 0:
# それでも置けないのであれば両者とも石を置けないということ
self.player = None
石がおけるマスがあるのであれば、次のプレイヤーは元々のプレイヤーに戻ることになります。
もし石を置くマスがないのであれば、両プレイヤーともに石を置くマスがないということなので、self.player
には None
を設定してメソッドを終了します。
showResult
showResult
は、ゲーム終了時に各プレイヤーの石の数を表示するメソッドになります。
単純に board
を参照して各色の石の数をカウントし、それをメッセージボックスで表示しているだけになります。
スポンサーリンク
com
com
は石を置くマスを決定し、そのマスに石を置く処理を行うメソッドです。ユーザーではなくプログラムが石を置くマスを決定するので、対戦相手のコンピュータが石を置くためのメソッドになります。
このあたりを作り込むことで対戦相手のコンピュータを強くすることができます。ただし、今回は getPlacable
メソッドで石を置くことができるマスの座標のリストを取得し、そのリストの先頭の座標に石を置く簡単なものになっています。なので、かなり弱いです…。
おすすめ書籍(PR)
対戦相手のコンピュータをもっと強くしたい、コンピュータの思考ルーチン・動作アルゴリズムについて知りたいという方には下記の「Pythonで作って学べるゲームのアルゴリズム入門」がオススメです。オセロ(リバーシ)の思考ルーチンについても解説されています。
題名の通り、ゲームのアルゴリズム “入門” 本なので、もちろんこの本を読めば最強のオセロ AI が作成できるというわけではないですが、ゲームにおいてコンピュータ側の動作のさせ方を考えるときの基本的な知識を学ぶことが出来ます。
このページではユーザー自身がオセロをプレイすることに重点を置いて解説してきましたが、対戦相手(コンピュータ)の動作にもこだわってプログラミングしていきたいという方にはぜひ上記のような書籍も参考にしてみてください。
まとめ
このページでは tkinter でのオセロゲーム(リバーシ)の作り方について解説しました!
オセロゲームの画面はキャンバスにより作成可能で、石を置くときや石を裏返すときの円の色の設定(fill
オプション)がポイントになると思います。
またオセロゲームとして機能させるためには「どのマスに石が置けるか」や「どのマスの石が裏返るのか」を判断するあたりがポイントになると思います。
この判断を行うためにはオセロゲームの仕様(オセロゲームのルール)をしっかり理解することが必要です。この仕様を理解することはどんなアプリを作る上でも重要なので、いろんなアプリを作ってみることでプログラミングだけでなく仕様を理解する力も付けて行ってください!
オススメ参考書(PR)
簡単なアプリやゲームを作りながら Python について学びたいという方には、下記の Pythonでつくる ゲーム開発 入門講座 がオススメです!ちなみに私が Python を始めるときに最初に買った書籍です!
下記ようなゲームを作成しながら Python の基本が楽しく学べます!素材もダウンロードして利用できるため、作成したゲームの見た目にも満足できると思います。
- すごろく
- おみくじ
- 迷路ゲーム
- 落ち物パズル
- RPG
また本書籍は下記のような構成になっているため、Python 初心者でも内容を理解しやすいです。
- プログラミング・Python の基礎から解説
- 絵を用いた解説が豊富
- ライブラリの使い方から解説(tkitner と Pygame)
- ソースコードの1行1行に注釈
ゲーム開発は楽しくプログラミングを学べるだけでなく、ゲームで学んだことは他の分野のプログラミングにも活かせるものが多いですし(キーボードの入力受付のイベントや定期的な処理・画像や座標を扱い方等)、逆に他の分野のプログラミングで学んだ知識を活かしやすいことも特徴だと思います(例えばコンピュータの動作に機械学習を取り入れるなど)。
プログラミングを学ぶのにゲーム開発は相性抜群だと思います。
Python の基礎や tkinter・Pygame の使い方をご存知なのであれば、下記の 実践編 をいきなり読むのもアリです。
実践編 では「シューティングゲーム」や「アクションゲーム」「3D カーレース」等のより難易度の高いゲームを作りながらプログラミングの力をつけていくことができます!
また、単にゲームを作るのではなく、対戦相手となるコンピュータの動作のアルゴリズムにも興味のある方は下記の「Pythonで作って学べるゲームのアルゴリズム入門」がオススメです。
この本はゲームのコンピュータ(AI)の動作アルゴリズム(思考ルーチン)に対する入門解説本になります。例えばオセロゲームにおけるコンピュータが、どのような思考によって石を置く場所を決めているか等の基本的な知識を得ることが出来ます。
プログラミングを挫折せずに続けていくためには楽しさを味わいながら学習することが大事ですので、特にゲームに興味のある方は、この辺りの参考書と一緒に Python を学んでいくのがオススメです!