はじめに
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