はじめに
Pythonでマルチタップアプリを開発するためのオープンソースライブラリkivyを使ってオセロアプリを作成してみました。
また、最終的にiOSのSimulatorで(xcode)でビルドしました。
環境
python: 3.7.7
kivy: 1.11.1
xcode: 11.7
作成物
ソースコード解説
今回作成したオセロアプリを開発した順番を追いながら説明します。
1. オセロ盤面と初期石を配置
class OthelloApp(App):
title = 'オセロ'
def build(self):
return OthelloGrid()
OthelloApp().run()
APPクラスの中でOthelloGridクラスをreturnしている。
今回のアプリの処理はこのOthelloGridクラスの中で行うようにしている。
class OthelloGrid(Widget):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.num = 8
self.tile = [[' ' for x in range(self.num)] for x in range(self.num)]
self.turn = 'W'
self.grid = GridLayout(cols=self.num, spacing=[3,3], size=(Window.width, Window.height))
for x in range(self.num):
for y in range(self.num):
if x == 3 and y == 3 or x == 4 and y == 4:
self.grid.add_widget(WhiteStone())
self.tile[x][y] = 'W'
elif x == 4 and y == 3 or x == 3 and y == 4:
self.grid.add_widget(BlackStone())
self.tile[x][y] = 'B'
else:
self.grid.add_widget(PutButton(background_color=(0.451,0.3059,0.1882,1), background_normal='', tile_id=[x, y]))
self.add_widget(self.grid)
self.numはオセロ盤面の縦、横のマスの数である。
self.tileは盤面の状態を記憶しておくためのリストで白が置かれている時'W'、黒が置かれている時'B'、何も置かれていない時' 'の値をとる。
self.turnは現在のターンが黒か白かを記憶しているもので、初期は白ターンから始まるようにしている。
実際に画面に描画する盤面はself.grid(GridLayout)で定義しており、既に石が置かれているマスには白の石の場合WhiteStoneクラス、黒の石の場合BlackStoneクラスをadd_widgetしており、まだ石が置かれていないマスにはPutButtonクラスをadd_widgetしています。
class WhiteStone(Label):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(pos=self.update)
self.bind(size=self.update)
self.update()
def update(self, *args):
self.canvas.clear()
self.canvas.add(Color(0.451,0.3059,0.1882,1))
self.canvas.add(Rectangle(pos=self.pos, size=self.size))
self.canvas.add(Color(1,1,1,1))
self.canvas.add(Ellipse(pos=self.pos, size=self.size))
class BlackStone(Label):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(pos=self.update)
self.bind(size=self.update)
self.update()
def update(self, *args):
self.canvas.clear()
self.canvas.add(Color(0.451,0.3059,0.1882,1))
self.canvas.add(Rectangle(pos=self.pos, size=self.size))
self.canvas.add(Color(0,0,0,1))
self.canvas.add(Ellipse(pos=self.pos, size=self.size))
class PutButton(Button):
def __init__(self, tile_id, **kwargs):
super().__init__(**kwargs)
self.tile_id = tile_id
WhiteStone、BlackStoneクラスはLabelクラスを継承しており、単純にマスRectangle(pos=self.pos, size=self.size)の上に楕円の石Ellipse(pos=self.pos, size=self.size)を描画しているだけです。
PutButtonクラスはButtonクラスを継承しており、まだ押した時の処理はありません。
tile_idとして、grid上のどの位置のマスかをインスタンス自身が記憶できるようにしています。
2.マスをタップした時に石を置く
下記のようにPutButtonクラスにon_pressファンクションを作成し、マスをタップされた時の処理を追加する。
def on_press(self):
put_x = self.tile_id[0]
put_y = self.tile_id[1]
turn = self.parent.parent.turn
self.parent.parent.tile[put_x][put_y] = turn
self.parent.parent.put_stone()
put_x、put_yにタップされたマスの番号を代入し、turnに現在のターンを代入する。
親のクラス(OthelloGrid)のtileのタップされたマスの場所にturnの値を代入し、put_stoneファンクションを呼び出す。
put_stoneはOthelloGridに下記のように作成したファンクションで、tileの中身から盤面を再作成するファンクションである。
def put_stone(self):
self.clear_widgets()
self.grid = GridLayout(cols=self.num, spacing=[3,3], size=(Window.width, Window.height))
next_turn = 'W' if self.turn == 'B' else 'B'
for x in range(self.num):
for y in range(self.num):
if self.tile[x][y] == 'W':
self.grid.add_widget(WhiteStone())
elif self.tile[x][y] == 'B':
self.grid.add_widget(BlackStone())
else:
self.grid.add_widget(PutButton(background_color=(0.451,0.3059,0.1882,1), background_normal='', tile_id=[x, y]))
3.挟んだ石をひっくり返す(ひっくり返せないマスをタップした場合は何もしない)
下記のようにOthelloGridクラスにマスの座標と現在のターンを起点にひっくり返すことができる石があるかをチェックするファンクションcan_reverse_checkとreverse_listを追加する。
def can_reverse_check(self, check_x, check_y, turn):
check =[]
# 左上確認
check += self.reverse_list(check_x, check_y, -1, -1, turn)
# 上確認
check += self.reverse_list(check_x, check_y, -1, 0, turn)
# 右上確認
check += self.reverse_list(check_x, check_y, -1, 1, turn)
# 右確認
check += self.reverse_list(check_x, check_y, 0, 1, turn)
# 右下確認
check += self.reverse_list(check_x, check_y, 1, 1, turn)
# 下確認
check += self.reverse_list(check_x, check_y, 1, 0, turn)
# 左下確認
check += self.reverse_list(check_x, check_y, 1, -1, turn)
# 左確認
check += self.reverse_list(check_x, check_y, 0, -1, turn)
return check
def reverse_list(self, check_x, check_y, dx, dy, turn):
tmp = []
while True:
check_x += dx
check_y += dy
if check_x < 0 or check_x > 7:
tmp = []
break
if check_y < 0 or check_y > 7:
tmp = []
break
if self.tile[check_x][check_y] == turn:
break
elif self.tile[check_x][check_y] == ' ':
tmp = []
break
else:
tmp.append((check_x, check_y))
return tmp
can_reverse_checkでは石を置こうとしているマスにからそれぞれの方向に対して、ひっくり返すことができる石があるかをチェックするファンクションであるreverse_listを呼んでいる。
戻り値として、ひっくり返すことができる石の座標のリストが返ってくるようにしている。
下記のように、このcan_reverse_checkをPutButtonクラスがタップされた時に(on_press内で)呼び出し、戻り値のリストの中身があった場合tileの値を更新して、盤面を作り直す(put_stoneを呼び出す)。
リストの中身が無かった場合は何もしない。
def on_press(self):
put_x = self.tile_id[0]
put_y = self.tile_id[1]
check =[]
turn = self.parent.parent.turn
check += self.parent.parent.can_reverse_check(self.tile_id[0], self.tile_id[1], turn)
if check:
self.parent.parent.tile[put_x][put_y] = turn
for x, y in check:
self.parent.parent.tile[x][y] = turn
self.parent.parent.put_stone()
4.パス機能とゲーム終了時の処理の追加
OthelloGridクラスのput_stoneを下記のように拡張する
def put_stone(self):
pass_flag = True
finish_flag = True
check = []
self.clear_widgets()
self.grid = GridLayout(cols=self.num, spacing=[3,3], size=(Window.width, Window.height))
next_turn = 'W' if self.turn == 'B' else 'B'
for x in range(self.num):
for y in range(self.num):
if self.tile[x][y] == 'W':
self.grid.add_widget(WhiteStone())
elif self.tile[x][y] == 'B':
self.grid.add_widget(BlackStone())
else:
self.grid.add_widget(PutButton(background_color=(0.451,0.3059,0.1882,1), background_normal='', tile_id=[x, y]))
for x in range(self.num):
for y in range(self.num):
if self.tile[x][y] == ' ':
finish_flag = False
check += self.can_reverse_check(x, y, next_turn)
if check:
pass_flag = False
break
if finish_flag:
content = Button(text=self.judge_winner())
popup = Popup(title='Game set!', content=content, auto_dismiss=False, size_hint=(None, None), size=(Window.width, Window.height/3))
content.bind(on_press=popup.dismiss)
popup.open()
else:
if pass_flag:
skip_turn_text = 'White Turn' if self.turn == 'B' else 'Black Turn'
content = Button(text='OK')
popup = Popup(title=skip_turn_text+' Skip!', content=content, auto_dismiss=False, size_hint=(None, None), size=(Window.width, Window.height/3))
content.bind(on_press=popup.dismiss)
popup.open()
else:
self.turn = next_turn
self.add_widget(self.grid)
pass_flagとfinish_flagを用意し、パスするかゲームを終了するかの判定に用いる。
tileの中の何も置かれていない全てのマス(値が' 'のマス)に対して、次のターンのプレイヤーがそのマスに石を置いた時にひっくり返す石があるかを確認し、もしなければ次のターンをスキップする。
その際にPopupでスキップしたことを画面に表示するようにする。
もし、tileの中の何も置かれていないマスがなければゲーム終了とみなし、下記のjudge_winnerファンクションでどちらが勝ったかを判別して、Popupで画面に表示する。
def judge_winner(self):
white = 0
black = 0
for x in range(self.num):
for y in range(self.num):
if self.tile[x][y] == 'W':
white += 1
elif self.tile[x][y] == 'B':
black += 1
print(white)
print(black)
return 'White Win!' if white >= black else 'Black Win!'
ここまでで、オセロの処理としては一通り終わりとなります。
ソースコード全体
他にもResetButtonやターンを表示するラベルなども追加していますが、その辺りは下記のソースコード全体でご確認ください。
(gitにもあげています。https://github.com/fu-yuta/kivy-project/tree/master/Othello)
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.core.window import Window
from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.popup import Popup
from kivy.graphics import Color, Ellipse, Rectangle
class OthelloGrid(Widget):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.num = 8
self.tile = [[' ' for x in range(self.num)] for x in range(self.num)]
self.turn = 'W'
self.grid = GridLayout(cols=self.num, spacing=[3,3], size_hint_y=7)
for x in range(self.num):
for y in range(self.num):
if x == 3 and y == 3 or x == 4 and y == 4:
self.grid.add_widget(WhiteStone())
self.tile[x][y] = 'W'
elif x == 4 and y == 3 or x == 3 and y == 4:
self.grid.add_widget(BlackStone())
self.tile[x][y] = 'B'
else:
self.grid.add_widget(PutButton(background_color=(0.451,0.3059,0.1882,1), background_normal='', tile_id=[x, y]))
self.creat_view('White Turn')
def put_stone(self):
self.grid = GridLayout(cols=self.num, spacing=[3,3], size_hint_y=7)
pass_flag = True
finish_flag = True
check = []
next_turn = 'W' if self.turn == 'B' else 'B'
for x in range(self.num):
for y in range(self.num):
if self.tile[x][y] == 'W':
self.grid.add_widget(WhiteStone())
elif self.tile[x][y] == 'B':
self.grid.add_widget(BlackStone())
else:
self.grid.add_widget(PutButton(background_color=(0.451,0.3059,0.1882,1), background_normal='', tile_id=[x, y]))
for x in range(self.num):
for y in range(self.num):
if self.tile[x][y] == ' ':
finish_flag = False
check += self.can_reverse_check(x, y, next_turn)
if check:
pass_flag = False
break
if finish_flag:
content = Button(text=self.judge_winner())
popup = Popup(title='Game set!', content=content, auto_dismiss=False, size_hint=(None, None), size=(Window.width, Window.height/3))
content.bind(on_press=popup.dismiss)
popup.open()
self.restart_game()
else:
if pass_flag:
skip_turn_text = 'White Turn' if self.turn == 'B' else 'Black Turn'
content = Button(text='OK')
popup = Popup(title=skip_turn_text+' Skip!', content=content, auto_dismiss=False, size_hint=(None, None), size=(Window.width, Window.height/3))
content.bind(on_press=popup.dismiss)
popup.open()
else:
self.turn = next_turn
turn_text = 'Black Turn' if self.turn == 'B' else 'White Turn'
self.creat_view(turn_text)
def can_reverse_check(self, check_x, check_y, turn):
check =[]
# 左上確認
check += self.reverse_list(check_x, check_y, -1, -1, turn)
# 上確認
check += self.reverse_list(check_x, check_y, -1, 0, turn)
# 右上確認
check += self.reverse_list(check_x, check_y, -1, 1, turn)
# 右確認
check += self.reverse_list(check_x, check_y, 0, 1, turn)
# 右下確認
check += self.reverse_list(check_x, check_y, 1, 1, turn)
# 下確認
check += self.reverse_list(check_x, check_y, 1, 0, turn)
# 左下確認
check += self.reverse_list(check_x, check_y, 1, -1, turn)
# 左確認
check += self.reverse_list(check_x, check_y, 0, -1, turn)
return check
def reverse_list(self, check_x, check_y, dx, dy, turn):
tmp = []
while True:
check_x += dx
check_y += dy
if check_x < 0 or check_x > 7:
tmp = []
break
if check_y < 0 or check_y > 7:
tmp = []
break
if self.tile[check_x][check_y] == turn:
break
elif self.tile[check_x][check_y] == ' ':
tmp = []
break
else:
tmp.append((check_x, check_y))
return tmp
def judge_winner(self):
white = 0
black = 0
for x in range(self.num):
for y in range(self.num):
if self.tile[x][y] == 'W':
white += 1
elif self.tile[x][y] == 'B':
black += 1
print(white)
print(black)
return 'White Win!' if white >= black else 'Black Win!'
def restart_game(self):
print("restart game")
self.tile = [[' ' for x in range(self.num)] for x in range(self.num)]
self.turn = 'W'
self.grid = GridLayout(cols=self.num, spacing=[3,3], size_hint_y=7)
for x in range(self.num):
for y in range(self.num):
if x == 3 and y == 3 or x == 4 and y == 4:
self.grid.add_widget(WhiteStone())
self.tile[x][y] = 'W'
elif x == 4 and y == 3 or x == 3 and y == 4:
self.grid.add_widget(BlackStone())
self.tile[x][y] = 'B'
else:
self.grid.add_widget(PutButton(background_color=(0.451,0.3059,0.1882,1), background_normal='', tile_id=[x, y]))
self.creat_view('White Turn')
def creat_view(self, turn_text):
self.clear_widgets()
self.turn_label = Label(text=turn_text, width=Window.width , size_hint_y=1, font_size='30sp')
self.restart_button = RestartButton(text='Restart')
self.layout = BoxLayout(orientation='vertical', spacing=10, size=(Window.width, Window.height))
self.layout.add_widget(self.turn_label)
self.layout.add_widget(self.grid)
self.layout.add_widget(self.restart_button)
self.add_widget(self.layout)
class WhiteStone(Label):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(pos=self.update)
self.bind(size=self.update)
self.update()
def update(self, *args):
self.canvas.clear()
self.canvas.add(Color(0.451,0.3059,0.1882,1))
self.canvas.add(Rectangle(pos=self.pos, size=self.size))
self.canvas.add(Color(1,1,1,1))
self.canvas.add(Ellipse(pos=self.pos, size=self.size))
class BlackStone(Label):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(pos=self.update)
self.bind(size=self.update)
self.update()
def update(self, *args):
self.canvas.clear()
self.canvas.add(Color(0.451,0.3059,0.1882,1))
self.canvas.add(Rectangle(pos=self.pos, size=self.size))
self.canvas.add(Color(0,0,0,1))
self.canvas.add(Ellipse(pos=self.pos, size=self.size))
class PutButton(Button):
def __init__(self, tile_id, **kwargs):
super().__init__(**kwargs)
self.tile_id = tile_id
def on_press(self):
print(self.tile_id)
put_x = self.tile_id[0]
put_y = self.tile_id[1]
check =[]
turn = self.parent.parent.parent.turn
check += self.parent.parent.parent.can_reverse_check(self.tile_id[0], self.tile_id[1], turn)
if check:
self.parent.parent.parent.tile[put_x][put_y] = turn
for x, y in check:
self.parent.parent.parent.tile[x][y] = turn
self.parent.parent.parent.put_stone()
class RestartButton(Button):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def on_press(self):
content = Button(text='OK')
popup = Popup(title='Restart Game!', content=content, auto_dismiss=False, size_hint=(None, None), size=(Window.width, Window.height/3))
content.bind(on_press=popup.dismiss)
popup.open()
self.parent.parent.restart_game()
class OthelloApp(App):
title = 'オセロ'
def build(self):
return OthelloGrid()
OthelloApp().run()
iOS Simulaterでのビルド
下記の記事を参考にさせていただきました。
https://qiita.com/sobassy/items/b06e76cf23046a78ba05
Xcodeのコマンドラインツールが入っていない場合は下記コマンドを実行してください。
xcode-select --install
依存関係をインストールする
brew install autoconf automake libtool pkg-config
brew link libtool
Cythonをインストールする
pip install cython
kivy-iosをgit cloneする。
git clone https://github.com/kivy/kivy-ios.git
cd kivy-ios
iOS用のkivyをビルドするために下記コマンドを実行する(完了までに数十分ほどかかるかも)
python toolchain.py build kivy
上記が完了したらkivyプログラムをXcode用にビルドする。
python toolchain.py create [Xcodeのプロジェクト名(任意の名前)] [kivyプログラムのフォルダ名]
この時、kivyプログラムのファイルの名前はmain.pyにしておかなければならない。
Xcodeのプロジェクト名をAppとした場合、App-iosというディレクトリが作成されており、その中にApp.xcodeprojが作成されている。
このプロジェクトをXcodeで開く。
open App.xcodeproj
XcodeでSimulatorを指定してビルドすれば、アプリが立ち上がるハズである。
もし、kivyプログラムを更新した場合には、下記コマンドでXcodeのプロジェクトも更新しなければならない。
python toolchain.py update App
終わりに
pythonのライブラリの1つであるkivyを使って、オセロアプリを作成し、iOSのSimulaterでビルドしてみました。
今回は、main.pyの中で全ての処理を書いていましたがkivyにはkv言語というもので、wighetなどを分けて書くことができるので、そちらへの移植も今後考えていきたい。
また、オセロのAIを組み込んでプレイヤー対CPUの対戦機能も今後追加していきたい。
参考
https://qiita.com/sobassy/items/b06e76cf23046a78ba05
https://github.com/PrestaMath/reverse_tile





