【Python/tkinter】オセロ(リバーシ)ゲームの作り方

tkinterでのオセロゲームの作り方の解説ページアイキャッチ

このページでは、tkinter を用いたオセロ(リバーシ)アプリの作り方と、そのオセロアプリのスクリプトの紹介をしていきたいと思います。

作成するオセロアプリ

まず今回作成するオセロアプリがどのようなものであるかについて解説します。

オセロアプリの UI

今回作成するオセロアプリの見た目は下の図のようになります。

オセロアプリの起動画面

スポンサーリンク

オセロアプリの遊び方

黄色のマスが次に石を置けるマスを表しています。

この黄色のマスをマウスでクリックすることでそのマスに石が置かれ、挟んだ位置の相手の石が裏返ります。

オセロアプリの遊び方1

石を置くと、次はコンピュータが石を置く番になり、同様にコンピュータが石を置いて石が裏返ると、次はあなたの番になります。

オセロアプリの遊び方2

これを繰り返し、両者の石を置けるマスがなくなったらゲーム終了です。

この際には、最終的な石の数をカウントし、その結果がメッセージボックス上に表示されます。

オセロゲームの結果表示

オセロアプリの作り方

続いてこのオセロアプリの作り方を解説していきます。

オセロアプリを作成する時にポイントになるのは下記の4つだと思います。

  • オセロの画面の実現
  • 石が置けるマスかどうかの判断の実現
  • 石を置く処理の実現
  • 盤面上の石の管理

ですので、ここでは、このポイント4つについて説明していきたいと思います。

他の点も含めた詳細な解説は、スクリプトを確認しながら最後のスクリプトの解説で行いたいと思います。

オセロの画面の実現

1つ目のポイントはオセロの画面の実現です。

今回作成するアプリでは、キャンバスを利用してこのオセロの画面の実現をしたいと思います。

マスに関しては、キャンバス上に正方形を描画することで表現します。この正方形は、キャンバスに用意された create_rectangle メソッドにより描画することができます。

create_rectangleでオセロのマスを描画する説明図

また石に関しては、キャンバス上に円を描画することで実現します。この円は、キャンバスに用意された create_oval メソッドにより描画することができます。

create_rectangleでオセロの石を描画する説明図

特にこのオセロアプリでポイントになるのが描画した図形の “色” です。

オセロでは石の色でどのプレイヤーの石であるかを区別します。つまり、どのプレイヤーが置いた石かどうかによって描画する円の塗りつぶし色を切り替える必要があります。

この「描画時の図形の塗りつぶし色の変更」は、create_rectanglecreate_oval 実行時に fill オプションを指定することで実現することができます。

create_xxx時にfillオプションで塗りつぶし色を変更する説明図

また、オセロでは石が挟まれた時に石が裏返って色が変化します。つまり、石が裏返しされた際にはその石を表現している円の塗りつぶし色を変更する必要があります。

さらに、今回のアプリでは、プレイヤーが次に石を置けるマスが分かるように、次に石が置けるマスの色を他のマスと違う色にするようにします。

石が置かれるたびに次に石が置けるマスの位置が変わりますので、石が置かれるたびにマスを表現する正方形の塗りつぶし色を変更する必要があります。

このような「描画後の図形の塗りつぶし色の変更」は、キャンバスクラスの itemconfig メソッドにより fill オプションを変更することで実現できます。

itemconfigで描画済みの図形のfillオプションを変更する説明図

ただし、itemconfig メソッドの第1引数では、”どの図形” の fill オプションを変更するかを指定する必要があります。この指定を行うために、マスを表現する正方形や石を表現する円を描画する際にはタグ付けを行うようにし、itemconfig メソッドの第1引数にはこのタグを指定するようにしています。

この辺りのキャンバスへの図形の描画(create_rectanglecreate_oval など)や描画した図形への操作(itemconfig など)については下記のページで解説していますので、詳しく知りたい方はこちらを是非読んでみてください。

tkinterキャンバスに図形を描画する方法解説ページのアイキャッチTkinterの使い方:Canvasクラスで図形を描画する tkinterキャンバスの図形の操作方法解説ページのアイキャッチTkinterの使い方:Canvasクラスで描画した図形を操作する

スポンサーリンク

石が置けるマスかどうかの判断の実現

またオセロアプリにおいては、プレイヤーが石を置こうとしたマスが「次に石が置けるマス」であるかどうかを判断する必要があります。これは、プレイヤーがルールに従わずに石を置くことができないように制御するためです。

石が置けるマスとは

オセロのルール上、「次に石が置けるマス」は下記の3つを満たすマスになります。

  • 盤面内のマス
  • 石が置かれていないマス
  • 石を置くことで相手の石を裏返すことができるマス

1点目と2点目に関してはそのままの意味なので説明不要だと思います。

3点目に関してはちょっと複雑なので、ここで詳細を解説しておきます。

石を置くことで相手の石を裏返すことができるマスとは

3点目の「石を置くことで相手の石を裏返すことができるマス」とは、より具体的にいうと、「上・右上・右・右下・下・左下・左・左上」の8方向のいずれかに関して下記の3つの条件が成立するマスになります。

  1. その方向の隣のマスに相手の石が置かれている
  2. その方向の隣のマス以外に自分の石が置かれている
  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

この時、左方向ではプレイヤーが石を置くマスから “一番近いプレイヤーの石” までに相手の石が “連続して” 置かれているのでその間の石を裏返すことができます。

石の裏返しの例2

一方で、右方向ではプレイヤーが石を置くマスから “一番近いプレイヤーの石” までに相手の石が “連続して” 置かれていないので、石を裏返すことができません。

石の裏返しの例3

また、右上方向では石を置いた場合に裏返される石は下の図のように1つのみになります。

石の裏返しの例4

右上方向にはプレイヤーの石が他にも置かれていますが、あくまでも裏返されるのは “一番近いプレイヤーの石” までの相手の石のみです。それよりも遠い位置にプレイヤーの石があっても、その間の石が裏返されるというわけではないという点に注意が必要です。

また前述の通り、石の裏返しは今回のアプリでは、キャンバスクラスの itemconfig メソッドにより fill オプションを変更することで実現します。

石を置く

相手の石の裏返しが完了したら、あとは指定されたマスに実際に石を置く処理を行います。

前述の通り、石を置く処理はキャンバス上に円を描画することで実現します。より具体的にはキャンバスに用意された create_oval メソッドを実行して円を描画することで盤面上に石を置きます。

盤面上の石の管理

石が置けるマスかどうかの判断の実現石を置く処理の実現で解説したように、これらを実現するためには盤面上のマスの状態、つまり「どのマスにどの色の石(どのプレイヤーの石)が置かれているか」を参照できるようにしておかないといけません。

今回は盤面上のマスの状態を2次元リスト board で管理します。

より具体的には board[y][x] には盤面上の座標 (x, y) に置かれている石の色を格納するようにし、これを参照しながら石が置けるマスかどうかの判断や石を置く処理を行うようにします。

boardに各マスの石の色が格納されている様子

例えば下の図のような盤面の状態であれば、

boardの例

リストの中身は下記のようになります(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のキャンバスの作り方解説ページアイキャッチTkinterの使い方:キャンバスウィジェットの作り方

setEvents

setEvents はオセロアプリに必要なイベント受付の設定を行うメソッドです。

今回はキャンバス上でのマウスのクリックイベントのみを受け付け、このイベントが発生した際に後述する click メソッドを実行するように設定しています。

イベントについて詳しく知りたい方は是非下記ページを参考にしていただければと思います。

イベント処理解説ページのアイキャッチTkinterの使い方:イベント処理を行う

initOthello

initOthello はオセロの初期化を行うメソッドです。

このメソッドでは大きく分けて下記の5つの処理を行なっています。

  • 盤面管理リスト board の初期化
  • マスのサイズの計算
  • マスの描画
  • 初期状態の石の描画
  • 石が置ける場所の表示

盤面管理リスト board の初期化

まずはオセロの盤面に一つも石が置かれていない状態を表現するため、board の全ての要素を None に設定します。

boardの初期化
# 盤面上の石を管理する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 の石が置かれたことを記録しておく)。

boardの更新
# 描画した円の色を管理リストに記憶させておく
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つの条件が成立するマスでしたね!

  1. その方向の隣のマスに相手の石が置かれている
  2. その方向の隣のマス以外に自分の石が置かれている
  3. ↑ の自分の石からそのマスまでの間に “石が置かれていないマス” が存在しない

各方向に対してこの3つの条件が成立するかどうかを判断しているのが下記のループ内の処理になります。

各方向に対するループ
for j in range(-1, 2):
	for i in range(-1, 2):

このループの ij が方向を示すパラメータになります。より具体的にいうと、各方向は下記の ij の組み合わせにより表現しています。

  • 左上:i = -1j = -1
  • 上 :i = 0j = -1
  • 右上:i = 1j = -1
  • 左 :i = -1j = 0
  • 右 :i = 1j = 0
  • 左下:i = -1j = 1
  • 上 :i = 0j = 1
  • 右下:i = 1j = 1

したがって、(x, y) 座標の隣のマスに置かれているのが相手の石かどうかは、下記のように board を参照して判断することができます。つまり、これにより条件 1. が成立するかどうかを判断することができます(条件 1. が成立しない場合は continue で次の方向の判断に移ります)。

相手の石が裏返せるかの判断1
# 隣が相手の色でなければその方向に石を置いても裏返せない
if self.board[y + j][x + i] != self.color[other]:
	continue

さらに ij それぞれに正数を乗算することで、その方向のマスに置かれている石の色を board から取得することが可能です。乗算した値分、(x, y) 座標のマスから ij の方向に離れた位置のマスの石の色を取得することができます。

座標(x,y)から左下方向のマスを辿る様子

これを利用して、下記により条件 2. と条件 3. が成立するかどうかを判断しています。

相手の石が裏返せるかの判断1
# 置こうとしているマスから遠い方向へ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.xevent.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.playerNone の場合は、もう石を置くことのできるプレイヤーがいないことになりますので、後述で解説する 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

要は、石が裏返すことができると判断した ij の方向及び x, y 座標からの距離 s に対し、(x + i * 1, y + j * 1) から (x + i * (s-1), y + j * (s-1)) の座標の石の裏返しを行なっています。

石の裏返しとは、要は石の色を相手の石のものからプレイヤーの石のものに変更することなので、上記のように itemcget メソッドを利用して fill オプションの変更、つまり石の色の変更を行なうことで石の裏返しを実現しています。

また、石の裏返しにより盤面上の石の色が変化しますので、それに合わせて盤面上の石を管理する board の更新も行っています。

boardの更新
self.board[y + j * n][x + i * n] = self.color[self.player]

nextPlayer

nextPlayer は次に石を置くプレイヤーを決定するメソッドになります。

オセロでは、基本的にプレイヤーは交互に入れ替わることになりますが、石が置けない場合などはスキップされるような場合もあります。この辺りを判断して次に石を置くプレイヤーを決定するのがこのメソッドになります。

まず現在のプレイヤーから相手側のプレイヤーに変更した際に “石を置けるマスがあるかどうか” を確認します。この確認は getPlacable メソッドで石を置くことのできるマスの座標のリストを取得し、その座標のリストの長さを調べることで行うことができます(長さが 0 なら石を置くことのできるマスがない)。

石を置けるマスがあるかを確認1
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

石が置けるマスがあるのであれば、次のプレイヤーは相手側のプレイヤーに変更になります。

もし次のプレイヤーが石を置くことのできるマスがないのであれば、再度元々のプレイヤーに戻して石が置けるマスがあるかどうかを確認します。

石を置けるマスがあるかを確認2
# スキップ後のプレイヤーが石を置けるかどうかを確認
	placable = self.getPlacable()

	if len(placable) == 0:
		# それでも置けないのであれば両者とも石を置けないということ
		self.player = None

石がおけるマスがあるのであれば、次のプレイヤーは元々のプレイヤーに戻ることになります。

もし石を置くマスがないのであれば、両プレイヤーともに石を置くマスがないということなので、self.player には None を設定してメソッドを終了します。

showResult

showResult は、ゲーム終了時に各プレイヤーの石の数を表示するメソッドになります。

単純に board を参照して各色の石の数をカウントし、それをメッセージボックスで表示しているだけになります。

スポンサーリンク

com

com は石を置くマスを決定し、そのマスに石を置く処理を行うメソッドです。ユーザーではなくプログラムが石を置くマスを決定するので、対戦相手のコンピュータが石を置くためのメソッドになります。

このあたりを作り込むことで対戦相手のコンピュータを強くすることができます。ただし、今回は getPlacable メソッドで石を置くことができるマスの座標のリストを取得し、そのリストの先頭の座標に石を置く簡単なものになっています。なので、かなり弱いです…。

まとめ

このページでは tkinter でのオセロゲーム(リバーシ)の作り方について解説しました!

オセロゲームの画面はキャンバスにより作成可能で、石を置くときや石を裏返すときの円の色の設定(fill オプション)がポイントになると思います。

またオセロゲームとして機能させるためには「どのマスに石が置けるか」や「どのマスの石が裏返るのか」を判断するあたりがポイントになると思います。

この判断を行うためにはオセロゲームの仕様(オセロゲームのルール)をしっかり理解することが必要です。この仕様を理解することはどんなアプリを作る上でも重要なので、いろんなアプリを作ってみることでプログラミングだけでなく仕様を理解する力も付けて行ってください!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です