13
9

More than 3 years have passed since last update.

Python(Kivy)で作るオセロアプリ(iOSアプリ)

Posted at

はじめに

Pythonでマルチタップアプリを開発するためのオープンソースライブラリkivyを使ってオセロアプリを作成してみました。
また、最終的にiOSのSimulatorで(xcode)でビルドしました。

環境

python: 3.7.7
kivy: 1.11.1
xcode: 11.7

作成物

完成形
Sep-06-2020 20-30-30.gif

ソースコード解説

今回作成したオセロアプリを開発した順番を追いながら説明します。

1. オセロ盤面と初期石を配置

下記の状態まで作成
スクリーンショット 2020-09-06 20.44.30.png

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

WhiteStoneBlackStoneクラスはLabelクラスを継承しており、単純にマスRectangle(pos=self.pos, size=self.size)の上に楕円の石Ellipse(pos=self.pos, size=self.size)を描画しているだけです。

PutButtonクラスはButtonクラスを継承しており、まだ押した時の処理はありません。
tile_idとして、grid上のどの位置のマスかをインスタンス自身が記憶できるようにしています。

2.マスをタップした時に石を置く

下記の状態まで作成
Sep-06-2020 21-29-16.gif

下記のようにPutButtonクラスにon_pressファンクションを作成し、マスをタップされた時の処理を追加する。

PutButtonクラス
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_xput_yにタップされたマスの番号を代入し、turnに現在のターンを代入する。
親のクラス(OthelloGrid)のtileのタップされたマスの場所にturnの値を代入し、put_stoneファンクションを呼び出す。
put_stoneOthelloGridに下記のように作成したファンクションで、tileの中身から盤面を再作成するファンクションである。

OthelloGridクラス
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.挟んだ石をひっくり返す(ひっくり返せないマスをタップした場合は何もしない)

下記の状態まで作成
Sep-06-2020 21-50-38.gif

下記のようにOthelloGridクラスにマスの座標と現在のターンを起点にひっくり返すことができる石があるかをチェックするファンクションcan_reverse_checkreverse_listを追加する。

OthelloGridクラス
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_checkPutButtonクラスがタップされた時に(on_press内で)呼び出し、戻り値のリストの中身があった場合tileの値を更新して、盤面を作り直す(put_stoneを呼び出す)。
リストの中身が無かった場合は何もしない。

PutButtonクラス
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.パス機能とゲーム終了時の処理の追加

石を置く場所がなくなった時のパス機能
Sep-06-2020 22-25-46.gif

ゲーム終了時の勝敗判定
Sep-06-2020 22-29-12.gif

OthelloGridクラスのput_stoneを下記のように拡張する

OthelloGridクラス
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_flagfinish_flagを用意し、パスするかゲームを終了するかの判定に用いる。
tileの中の何も置かれていない全てのマス(値が' 'のマス)に対して、次のターンのプレイヤーがそのマスに石を置いた時にひっくり返す石があるかを確認し、もしなければ次のターンをスキップする。
その際にPopupでスキップしたことを画面に表示するようにする。

もし、tileの中の何も置かれていないマスがなければゲーム終了とみなし、下記のjudge_winnerファンクションでどちらが勝ったかを判別して、Popupで画面に表示する。

OthelloGridクラス
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)

main.py
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

13
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
9